mysql大数据量下的操作以及优化




当我们操作mysql的时候,如果数据量很小,那么我们如何处理都没有问题。但是当一张表非常大的时候,我们一个大查询,一个堆大插入,一个count(*),一个limit都是非常恐怖的,因此,我在下面说几种常用的优化方式。
当表数据非常多的时候,我们不能一次把查询结果load进内存中,那会以下就OOM的,需要采用流式读取,也就是Hibernate中的ScrollableResult的方式,它的底层实现就是jdbc的流式读取。
1.JDBC流式读取 (Hibernate ScrollableResult)
读取操作开始遇到的问题是当sql查询数据量比较大时候 程序直接抛错,或是 读不出来ResultSet的next方法阻塞 。
Root Casue: mysql driver 默认的行为是需要把整个结果全部读取到 内存(ResultSet)中,才允许读取结果。当遇到大数据的时候,这显然会导致OOM。这显然与期望的行为不一致,期望的行为是jdbc流的方式读取,当结果从mysql服务端返回后立即开始读取处理。这样应用就不需要大量内存 来存储这个结果集。
正确的jdbc流式读取代码:

PreparedStatement ps = connection.prepareStatement("select .. from ..", ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY);

//forward only read only也是mysql 驱动的默认值,所以不指定也是可以的 比如: PreparedStatement ps = connection.prepareStatement("select .. from .."); ps.setFetchSize(Integer.MIN_VALUE); // 也可以修改jdbc url通过defaultFetchSize参数来设置,这样默认所以的返回结果都是通过流方式读取. ResultSet rs = ps.executeQuery();

while (rs.next()) {
System.out.println(rs.getString("fieldName"));
}


代码分析:下面是mysql判断是否开启流式读取结果的方法,有三个条件forward-only,read-only,fatch size是Integer.MIN_VALUE
/**
* We only stream result sets when they are forward-only, read-only, and the
* fetch size has been set to Integer.MIN_VALUE
*
* @return true if this result set should be streamed row at-a-time, rather
* than read all at once.
*/
protected boolean createStreamingResultSet() {
try {
synchronized(checkClosed().getConnectionMutex()) {
return ((this.resultSetType == java.sql.ResultSet.TYPE_FORWARD_ONLY)
&& (this.resultSetConcurrency == java.sql.ResultSet.CONCUR_READ_ONLY) && (this.fetchSize == Integer.MIN_VALUE));
}
} catch (SQLException e) {
// we can't break the interface, having this be no-op in case of error is ok

return false;
}
}
2. JDBC批量写入
当需要很多的数据一次性写入表中。如果 是一条一条的执行insert来写入,非常慢。
Root Cause: 第一, 单条写入需要大量的Database 请求响应交互。每个insert请求都是一个独立的Transaction commit。这样网络延迟大的情况下多次请求会有大量的时间消耗的网络延迟上。第二,是由于每个Transaction,Database都会 有刷新磁盘操作写事务日志,保证事务的持久性。由于每个事务只是写入一条数据,所以磁盘io利用率不高,因为对于磁盘io是按块来的,所以连续写入大量数据效率 更好。
所以,必须改成批量插入的方式,减少请求数与Transaction。
下面是批量插入的例子:还有jdbc连接串必须加下rewriteBatchedStatements=true
int batchSize = 1000;
PreparedStatement ps = connection.prepareStatement("insert into tb1 (c1,c2,c3...) values (?,?,?...)");

for (int i = 0; i < list.size(); i++) {

ps.setXXX(list.get(i).getC1());
ps.setYYY(list.get(i).getC2());
ps.setZZZ(list.get(i).getC3());

ps.addBatch();

if ((i + 1) % batchSize == 0) {
ps.executeBatch();
}
}

if (list.size() % batchSize != 0) {
ps.executeBatch();
}

上面代码示例是每1000条数据发送一次请求。mysql驱动内部在应用端会把多次addBatch()的参数合并成一条multi value的insert语句发送给db去执行
比如insert into tb1(c1,c2,c3) values (v1,v2,v3),(v4,v5,v6),(v7,v8,v9)...
这样可以比每条一个insert 明显少很多请求。减少了网络延迟消耗时间与磁盘io时间,从而提高了tps。

代码分析: 从代码可以看出,
1 rewriteBatchedStatements=true,insert是参数化语句且不是insert ... select 或者 insert... on duplicate key update with an id=last_insert_id(...)的话会执行
executeBatchedInserts,也就是muti value的方式
2 rewriteBatchedStatements=true 语句是都是参数化(没有addbatch(sql)方式加入的)的而且mysql server版本在4.1以上 语句超过三条,则执行executePreparedBatchAsMultiStatement
就是将多个语句通过; 分隔一次提交多条sql。比如 "insert into tb1(c1,c2,c3) values (v1,v2,v3); insert into tb1(c1,c2,c3) values (v1,v2,v3)..."
3 其余的执行executeBatchSerially,也就是还是一条条处理
public void addBatch(String sql)throws SQLException { synchronized(checkClosed().getConnectionMutex()) { this.batchHasPlainStatements = true; super.addBatch(sql); } }public int[] executeBatch()throws SQLException { //... if (!this.batchHasPlainStatements && this.connection.getRewriteBatchedStatements()) {if (canRewriteAsMultiValueInsertAtSqlLevel()) { return executeBatchedInserts(batchTimeout); }if (this.connection.versionMeetsMinimum(4, 1, 0) && !this.batchHasPlainStatements && this.batchedArgs != null && this.batchedArgs.size() > 3 /* cost of option setting rt-wise */ ) { return executePreparedBatchAsMultiStatement(batchTimeout); } }return executeBatchSerially(batchTimeout); //..... }


【mysql大数据量下的操作以及优化】
executeBatchedInserts相比executePreparedBatchAsMultiStatement的方式传输效率更好,因为一次请求只重复一次前面的insert table (c1,c2,c3)
mysql server 对请求报文的最大长度有限制,如果batch size 太大造成请求报文超过最大限制,mysql 驱动会内部按最大报文限制查分成多个报文。所以要真正减少提交次数
还要检查下mysql server的max_allowed_packet 否则batch size 再大也没用.
mysql> show VARIABLES like '%max_allowed_packet%';
+--------------------+-----------+
| Variable_name | Value |
+--------------------+-----------+
| max_allowed_packet | 167772160 |
+--------------------+-----------+
1 row in set (0.00 sec)

要想验证mysql 发送了正确的sql 有两种方式
1 抓包,下图是wireshark在 应用端抓包mysql的报文


2 另一个办法是在mysql server端开启general log 可以查看mysql收到的所有sql

3 在jdbc url上加上参数traceProtocol=true 或者profileSQL=true or autoGenerateTestcaseScript=true

性能测试对比
import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.SQLException; import com.alibaba.druid.pool.DruidDataSource; public class BatchInsert {public static void main(String[] args) throws SQLException {int batchSize = 1000; int insertCount = 1000; testDefault(batchSize, insertCount); testRewriteBatchedStatements(batchSize,insertCount); }private static void testDefault(int batchSize, int insertCount) throws SQLException {long start = System.currentTimeMillis(); doBatchedInsert(batchSize, insertCount,""); long end = System.currentTimeMillis(); System.out.println("default:" + (end -start) + "ms"); }private static void testRewriteBatchedStatements(int batchSize, int insertCount) throws SQLException {long start = System.currentTimeMillis(); doBatchedInsert(batchSize, insertCount, "rewriteBatchedStatements=true"); long end = System.currentTimeMillis(); System.out.println("rewriteBatchedStatements:" + (end -start) + "ms"); }private static void doBatchedInsert(int batchSize, int insertCount, String mysqlProperties) throws SQLException { DruidDataSource dataSource = new DruidDataSource(); dataSource.setUrl("jdbc:mysql://ip:3306/test?" + mysqlProperties); dataSource.setUsername("name"); dataSource.setPassword("password"); dataSource.init(); Connection connection = dataSource.getConnection(); PreparedStatement preparedStatement = connection.prepareStatement("insert into Test (name,gmt_created,gmt_modified) values (?,now(),now())"); for (int i = 0; i < insertCount; i++) { preparedStatement.setString(1, i+" "); preparedStatement.addBatch(); if((i+1) % batchSize == 0) { preparedStatement.executeBatch(); } } preparedStatement.executeBatch(); connection.close(); dataSource.close(); }}


网络环境ping测试延迟是35ms ,测试结果:
default:75525ms rewriteBatchedStatements:914ms 3. 批量更新 //Session是持久层操作的基础,相当于JDBC中的Connection。 Session session = sessionFactory.openSession(); try{//为保持事务的原子性,必须捕捉异常。所有事务都放在这一代码块里。 // 操作事务时(增、删、改)必须显式的调用Transaction,如果不启动Transaction,数据库不会有变化(默认:session.autoCommit=false)。 Transaction tx = session.beginTransaction(); for(int i=0; i<=1000; i++){ Student stu = new Student(...); session.save(stu); //set value to stu //批量更新:为防止内存不足,分成每20个一批发送过去。 如果不是大批量更新,则不需要这样 if(i%20==0){ //强制内存中数据同步到mysql,sql打印出并执行,只是事务没有commit,其他的线程看不到 session.flush(); session.clear(); } } //transaction commit默认会自动flush(查询之前、事务提交时都会自动flush,之前手动flush只是为了内存考虑)。 tx.commit(); //提交事务,Hibernate不喜欢抛异常,如有需要,自己捕捉。 //查询方法。如果有必要,也可以用事务(调用Transaction) String hql = "from Student s where s.stuNo like ? and s.Sal > ?"; //Student是类而不是表 List list = session.createQuery(hql) .setString(0, "a00_").setDouble(1, 3000.0)//设置HQL的第一二个问号取值 .list(); //Hibernate里面,没有返回值的都默认返回List StringBuffer sb = new StringBuffer(); for(Student st :(List)list){//(List)强制类型转换 sb.append(st.getOid()+""+st.getName()+"\n"); //拿到Student类里的属性 } System.out.print(sb.toString()); //直接打印sb也可以,它也是调用toString,但这样写效率更高 } catch (HibernateException e) { e.printStackTrace(); session.getTransaction().rollback(); //如果事务不成功,则rollback } finally { // 如果是openSession()方法创建的session,必须手动关闭 session.close(); //注意关闭顺序,session先关,Factory最后关(因为它可以启动多个session) sessionFactory.close(); //关闭SessionFactory,虽然这里没看到它,但在HbnUtil里开启了。 } 4. 表中大数据分页 我们先从一个常用但性能很差的查询来看一看。

SELECT *
FROM city
ORDER BY id DESC
LIMIT 0, 15


这个查询耗时0.00sec。So,这个查询有什么问题呢?实际上,这个查询语句和参数都没有问题,因为它用到了下面表的主键,而且只读取15条记录。


CREATE TABLE city (
id int(10) unsigned NOT NULL AUTO_INCREMENT,
city varchar(128) NOT NULL,
PRIMARY KEY (id)
) ENGINE=InnoDB;


真正的问题在于offset(分页偏移量)很大的时候,像下面这样:


SELECT *
FROM city
ORDER BY id DESC
LIMIT 100000, 15;


上面的查询在有2M行记录时需要0.22sec,通过EXPLAIN查看SQL的执行计划可以发现该SQL检索了100015行,但最后只需要15行。大的分页偏移量会增加使用的数据,MySQL会将大量最终不会使用的数据加载到内存中。就算我们假设大部分网站的用户只访问前几页数据,但少量的大的分页偏移量的请求也会对整个系统造成危害。Facebook意识到了这一点,但Facebook并没有为了每秒可以处理更多的请求而去优化数据库,而是将重心放在将请求响应时间的方差变小。


对于分页请求,还有一个信息也很重要,就是总共的记录数。我们可以通过下面的查询很容易的获取总的记录数。


SELECT COUNT(*)
FROM city;


然而,上面的SQL在采用InnoDB为存储引擎时需要耗费9.28sec。一个不正确的优化是采用SQL_CALC_FOUND_ROWS,SQL_CALC_FOUND_ROWS可以在能够在分页查询时事先准备好符合条件的记录数,随后只要执行一句select FOUND_ROWS(); 就能获得总记录数。但是在大多数情况下,查询语句简短并不意味着性能的提高。不幸的是,这种分页查询方式在许多主流框架中都有用到,下面看看这个语句的查询性能。


SELECT SQL_CALC_FOUND_ROWS *
FROM city
ORDER BY id DESC
LIMIT 100000, 15;


这个语句耗时20.02sec,是上一个的两倍。事实证明使用SQL_CALC_FOUND_ROWS做分页是很糟糕的想法。
下面来看看到底如何优化。文章分为两部分,第一部分是如何获取记录的总数目,第二部分是获取真正的记录。


高效的计算行数


如果采用的引擎是MyISAM,可以直接执行COUNT(*)去获取行数即可。相似的,在堆表中也会将行数存储到表的元信息中。但如果引擎是InnoDB情况就会复杂一些,因为InnoDB不保存表的具体行数。
我们可以将行数缓存起来,然后可以通过一个守护进程定期更新或者用户的某些操作导致缓存失效时,执行下面的语句:


SELECT COUNT(*)
FROM city
USE INDEX(PRIMARY);


获取记录


下面进入这篇文章最重要的部分,获取分页要展示的记录。上面已经说过了,大的偏移量会影响性能,所以我们要重写查询语句。为了演示,我们创建一个新的表“news”,按照时事性排序(最新发布的在最前面),实现一个高性能的分页。为了简单,我们就假设最新发布的新闻的Id也是最大的。


CREATE TABLE news(
id INT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
title VARCHAR(128) NOT NULL
) ENGINE=InnoDB;


一个比较高效的方式是基于用户展示的最后一个新闻Id。查询下一页的语句如下,需要传入当前页面展示的最后一个Id。


SELECT *
FROM news WHERE id< $last_id
ORDER BY id DESC
LIMIT $perpage


查询上一页的语句类似,只不过需要传入当前页的第一个Id,并且要逆序。


SELECT *
FROM news WHERE id> $last_id
ORDER BY id ASC
LIMIT $perpage


上面的查询方式适合实现简易的分页,即不显示具体的页数导航,只显示“上一页”和“下一页”,例如博客中页脚显示“上一页”,“下一页”的按钮。但如果要实现真正的页面导航还是很难的,下面看看另一种方式。


SELECT id
FROM (
SELECT id, ((@cnt:= @cnt + 1) + $perpage - 1) % $perpage cnt
FROM news
JOIN (SELECT @cnt:= 0)T
WHERE id< $last_id
ORDER BY id DESC
LIMIT $perpage * $buttons
)C
WHERE cnt = 0;


通过上面的语句可以为每一个分页的按钮计算出一个offset对应的id。这种方法还有一个好处。假设,网站上正在发布一片新的文章,那么所有文章的位置都会往后移一位,所以如果用户在发布文章时换页,那么他会看见一篇文章两次。如果固定了每个按钮的offset Id,这个问题就迎刃而解了。Mark Callaghan发表过一篇类似的博客,利用了组合索引和两个位置变量,但是基本思想是一致的。


如果表中的记录很少被删除、修改,还可以将记录对应的页码存储到表中,并在该列上创建合适的索引。采用这种方式,当新增一个记录的时候,需要执行下面的查询重新生成对应的页号。


SET p:= 0;
UPDATE news SET page=CEIL((p:= p + 1) / $perpage) ORDER BY idDESC;


当然,也可以新增一个专用于分页的表,可以用个后台程序来维护。


UPDATE pagination T
JOIN (
SELECT id,CEIL((p:= p + 1) / $perpage) page
FROM news
ORDER BY id
)C
ON C.id = T.id
SET T.page = C.page;


现在想获取任意一页的元素就很简单了:


SELECT *
FROM news A
JOIN pagination B ON A.id=B.ID
WHERE page=$offset;


还有另外一种与上种方法比较相似的方法来做分页,这种方式比较试用于数据集相对小,并且没有可用的索引的情况下—比如处理搜索结果时。在一个普通的服务器上执行下面的查询,当有2M条记录时,要耗费2sec左右。这种方式比较简单,创建一个用来存储所有Id的临时表即可(这也是最耗费性能的地方)。


CREATE TEMPORARY TABLE _tmp (KEY SORT(random))
SELECT id,FLOOR(RAND() * 0x8000000) random
FROM city;

ALTER TABLE _tmp ADD OFFSET INT UNSIGNED PRIMARY KEY AUTO_INCREMENT, DROP INDEX SORT, ORDER BY random;


接下来就可以向下面一样执行分页查询了。


SELECT *
FROM _tmp
WHERE OFFSET>= $offset
ORDER BY OFFSET
LIMIT $perpage;


简单来说,对于分页的优化就是。。。避免数据量大时扫描过多的记录。 软件开发中,常用要用到分页、计算总数,数据量超过千万、上亿的时候,往往 count 的需要超过 1s 的执行时间,甚至 3-5s,对于一个追求性能的前沿团队来说,这个不能忍啊! 那么我们再从头分析以下为什么会慢? mysql 会对所有符合的条件做一次扫描。
select count(*) from table_a where a = '%d' ...

如果 a=%d 的数据有 1000W 条,那么数据库就会扫描一次 1000W 条数据库。如果不带查询条件,那这种全表扫描将更可怕。
count(*) 和 count(1)、count(0)
  • count(expr) 为统计 expr 不为空的记录
  • count(*) 它会计算总行数,不管你字段是否有值都会列入计算范围。
  • coount(0),count(1) 没有差别,它会计算总行数
Example 1:
mysql> explain extended select count(*) from user; ... 1 row in set, 1 warning (0.34 sec)mysql> show warnings; +-------+------+--------------------------------------------------+ | Level | Code | Message | +-------+------+--------------------------------------------------+ | Note | 1003 | select count(0) AS `count(*)` from `user` |

Example 2:
mysql> select count(*) from login_log -> ; +----------+ | count(*) | +----------+ | 2513 | +----------+ 1 rows in set (0.00 sec)mysql> select count(logoutTime) from login_log; +-------------------+ | count(logoutTime) | +-------------------+ | 308 | +-------------------+ 1 rows in set (0.00 sec)

怎么解决? MyISAM DB
MyISAM 引擎很容易获得总行数的统计,查询速度变得更快。因为 MyISAM 存储引擎已经存储了表的总行数。
MyISAM 会为每张表维护一个 row count 的计数器,每次新增加一行,这个计数器就加 1。但是如果有查询条件,那么 MyISAM 也 game over 了,MyISAM 引擎不支持条件缓存。
On MyISAM, doing a query that does SELECT COUNT(*) FROM {some_table}, is very fast, since MyISAM keeps the information in the index

其他 DB 引擎
受到 MySIAM DB 的启发,我们可以手动维护总数缓存在表的索引中了。
1、如果 ID 连续,且基本不会断开。直接取最大值 ID
2、如果表中存在连续的数字列并设为索引,那么通过页码即可计算出此字段的范围,直接作范围查询即可:
start = (page-1)*pagesize+1 end = page*pagesize select * from table where id >start and id <=end

1、涉及到总数操作,专门维护一个总数。新增一个用户,总数值加 1, 需要总数的时候直接拿这个总数, 比如分页时。如果有多个条件,那么就需要维护多个总数列。该方案的扩展性更好,随着用户表数量增大, 水平切分用户表,要获取用户总数,直接查询这个总数表即可。
分页正反偏移
数据库自带的 skip 和 limit 的限制条件为我们创建了分页的查询方式,但是如果利用不对,性能会出现千倍万倍差异。
简单一点描述:limit 100000,20 的意思扫描满足条件的 100020 行,扔掉前面的 100000 行,返回最后的 20 行,问题就在这里。如果我反向查询 oder by xx desc limit 0,20,那么我只要索引 20 条数据。
Example 3
mysql> select count(*) from elastic_task_log_copy; +----------+ | count(*) | +----------+ | 1705162 | +----------+ 1 rows in set (2.31 sec)

正向偏移查询。超级浪费的查询,需要先 skip 大量的符合条件的查询。
mysql> select id from elastic_task_log_copy order by id asc limit 1705152,10; +---------+ | id | +---------+ | 1705157 | | 1705158 | | 1705159 | | 1705160 | | 1705161 | | 1705162 | | 1705163 | | 1705164 | | 1705165 | | 1705166 | +---------+ 10 rows in set (2.97 sec)

反向偏移查询。同样的查询结果,千差万别的结果。
mysql> select id from elastic_task_log_copy order by id desc limit 0,10; +---------+ | id | +---------+ | 1705166 | | 1705165 | | 1705164 | | 1705163 | | 1705162 | | 1705161 | | 1705160 | | 1705159 | | 1705158 | | 1705157 | +---------+ 10 rows in set (0.01 sec)

这两条 sql 是为查询最后一页的翻页 sql 查询用的。由于一次翻页往往只需要查询较小的数据,如 10 条,但需要向后扫描大量的数据,也就是越往后的翻页查询,扫描的数据量会越多,查询的速度也就越来越慢。
由于查询的数据量大小是固定的,如果查询速度不受翻页的页数影响,或者影响最低,那么这样是最佳的效果了(查询最后最几页的速度和开始几页的速度一致)。
在翻页的时候,往往需要对其中的某个字段做排序(这个字段在索引中),升序排序。那么可不可以利用索引的有序性 来解决上面遇到的问题。
比如有 10000 条数据需要做分页,那么前 5000 条做 asc 排序,后 5000 条 desc 排序,在 limit startnum,pagesize 参数中作出相应的调整。
但是这无疑给应用程序带来复杂,这条 sql 是用于论坛回复帖子的 sql,往往用户在看帖子的时候,一般都是查看前几页和最后几页,那么在翻页的时候最后几页的翻页查询采用 desc 的方式来实现翻页,这样就可以较好的提高性能。
游标:上一页的最大值或者最小值
如果你知道上一页和下一页的临界值,那么翻页查询也是信手拈来了,直接就告诉了数据库我的起始查询在哪,也就没有什么性能问题了。我更愿意称这个东西为游标 (Cursor)。
如果做下拉刷新,那么就直接避免掉分页的问题了。根据上一页的最后一个值去请求新数据。
mysql> select id from elastic_task_log_copy where id >= 1699999 limit 10; +---------+ | id | +---------+ | 1699999 | | 1700000 | | 1700001 | | 1700002 | | 1700003 | | 1700004 | | 1700005 | | 1700006 | | 1700007 | | 1700008 | +---------+ 10 rows in set (0.01 sec)

缓存和不精准
数据量达到一定程度的时候,用户根本就不关心精准的总数, 没人关心差几个。看看知乎、微博、微信订阅号,不精准的统计到处都是。 如果每次点击分页的时候都进行一次 count 操作,那速度肯定不会快到哪里去。他们一般也是采用计数器的办法。每次新增加一个粉丝,就把值加 1,直接在用户信息存储一个总数,一段时间后重新查询一次,更新该缓存。这样分页的时候直接拿这个总数进行分页,显示的时候直接显示模糊之就行。
那为什么微信公众号的阅读量只有 10W+ 这个量级呢?100W+ 级去哪了! 5. 其他的建议 1、mysql 的数据查询, 大小字段要分开, 这个还是有必要的, 除非一点就是你查询的都是索引内容而不是表内容, 比如只查询 id 等等


2、查询速度和索引有很大关系也就是索引的大小直接影响你的查询效果, 但是查询条件一定要建立索引, 这点上注意的是索引字段不能太多,太多索引文件就会很大那样搜索只能变慢,


3、查询指定的记录最好通过 Id 进行 in 查询来获得真实的数据. 其实不是最好而是必须,也就是你应该先查询出复合的 ID 列表, 通过 in 查询来获得数据


4、mysql 千万级别数据肯定是没问题的, 毕竟现在的流向 web2.0 网站大部分是 mysql 的


5、合理分表也是必须的, 主要涉及横向分表与纵向分表, 如把大小字段分开, 或者每 100 万条记录在一张表中等等, 像上面的这个表可以考虑通过 uid 的范围分表, 或者通过只建立索引表, 去掉相对大的字段来处理.


6、count() 时间比较长, 但是本身是可以缓存在数据库中或者缓存在程序中的, 因为我们当时使用在后台所以第一页比较慢但是后面比较理想


7、SELECT id 相对 SELECT差距还是比较大的, 可以通过上面的方法来使用 SELECT id + SELECT … IN 查询来提高性能


8、必要的索引是必须的, 还是要尽量返回 5%-20% 的结果级别其中小于 5% 最理想;


9、mysql 分页的前面几页速度很快, 越向后性能越差, 可以考虑只带上一页, 下一页不带页面跳转的方法, 呵呵这个比较垃圾但是也算是个方案, 只要在前后多查一条就能解决了. 比如 100,10 你就差 99,12 呵呵,这样看看前后是否有结果.


10、前台还是要通过其他手段来处理, 比如 lucene/Solr+mysql 结合返回翻页结果集, 或者上面的分表


11、总数可能是存在内存中, 这样分页计算的时候速度很快。累加操作的时候将内存中的值加 1。总数这个值要持久化,还是要存到磁盘上的,也就是数据库中 (可以是关系型数据库,也可以是 mongdb 这样的数据库很适合存储计数)。把总数放在内存中,只是避免频繁的磁盘 i/0 操作 (操作数据库就要涉及到磁盘读写)。

























    推荐阅读