您当前的位置:首页 > 电脑百科 > 程序开发 > 架构

Java架构-MYSQL大数据量下的操作与优化

时间:2019-08-27 11:01:39  来源:  作者:

 

前言

当我们操作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);
 //.....
}

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<Student>)list){//(List<Student>)强制类型转换

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里开启了。

}

Java架构-MYSQL大数据量下的操作与优化

 

4.%20表中大数据分页

我们先从一个常用但性能很差的查询来看一看。

SELECT%20*
FROM%20city
ORDER%20BY%20id%20DESC
LIMIT%200,%2015

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

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

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

SELECT%20*
FROM%20city
ORDER%20BY%20id%20DESC
LIMIT%20100000,%2015;

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

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

SELECT%20COUNT(*)
FROM%20city;

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

SELECT%20SQL_CALC_FOUND_ROWS%20*
FROM%20city
ORDER%20BY%20id%20DESC
LIMIT%20100000,%2015;

这个语句耗时20.02sec,是上一个的两倍。事实证明使用SQL_CALC_FOUND_ROWS做分页是很糟糕的想法。

下面来看看到底如何优化。文章分为两部分,第一部分是如何获取记录的总数目,第二部分是获取真正的记录。

高效的计算行数

如果采用的引擎是MyISAM,可以直接执行COUNT(*)去获取行数即可。相似的,在堆表中也会将行数存储到表的元信息中。但如果引擎是InnoDB情况就会复杂一些,因为InnoDB不保存表的具体行数。

我们可以将行数缓存起来,然后可以通过一个守护进程定期更新或者用户的某些操作导致缓存失效时,执行下面的语句:

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

获取记录

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

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

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

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

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

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

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

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

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

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

SET%20p:=%200;
UPDATE%20news%20SET%20page=CEIL((p:=%20p%20+%201)%20/%20$perpage)%20ORDER%20BY%20id%20DESC;

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

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

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

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

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

CREATE%20TEMPORARY%20TABLE%20_tmp%20(KEY%20SORT(random))
SELECT%20id,%20FLOOR(RAND()%20*%200x8000000)%20random
FROM%20city;
%20
ALTER%20TABLE%20_tmp%20ADD%20OFFSET%20INT%20UNSIGNED%20PRIMARY%20KEY%20AUTO_INCREMENT,%20DROP%20INDEX%20SORT,%20ORDER%20BY%20random;

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

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

简单来说,对于分页的优化就是。。。避免数据量大时扫描过多的记录。

软件开发中,常用要用到分页、计算总数,数据量超过千万、上亿的时候,往往count%20的需要超过%201s%20的执行时间,甚至%203-5s,对于一个追求性能的前沿团队来说,这个不能忍啊!

那么我们再从头分析以下为什么会慢?

mysql%20会对所有符合的条件做一次扫描。

select%20count(*)%20from%20table_a%20where%20a%20=%20'%d'%20...

如果%20a=%d%20的数据有%201000W%20条,那么数据库就会扫描一次%201000W%20条数据库。如果不带查询条件,那这种全表扫描将更可怕。

count(*)%20和%20count(1)、count(0)

count(expr)%20为统计%20expr%20不为空的记录

count(*)%20它会计算总行数,不管你字段是否有值都会列入计算范围。

coount(0),count(1)%20没有差别,它会计算总行数

Example%201:

mysql>%20explain%20extended%20select%20count(*)%20from%20user;
...
1%20row%20in%20set,%201%20warning%20(0.34%20sec)
%20
mysql>%20show%20warnings;
+-------+------+--------------------------------------------------+
|%20Level%20|%20Code%20|%20Message%20|
+-------+------+--------------------------------------------------+
|%20Note%20|%201003%20|%20select%20count(0)%20AS%20`count(*)`%20from%20`user`%20|

Example%202:

mysql>%20select%20count(*)%20from%20login_log
%20->%20;
+----------+
|%20count(*)%20|
+----------+
|%202513%20|
+----------+
1%20rows%20in%20set%20(0.00%20sec)
%20
mysql>%20select%20count(logoutTime)%20from%20login_log;
+-------------------+
|%20count(logoutTime)%20|
+-------------------+
|%20308%20|
+-------------------+
1%20rows%20in%20set%20(0.00%20sec)

怎么解决?

MyISAM%20DB

MyISAM%20引擎很容易获得总行数的统计,查询速度变得更快。因为%20MyISAM%20存储引擎已经存储了表的总行数。

MyISAM%20会为每张表维护一个%20row%20count%20的计数器,每次新增加一行,这个计数器就加%201。但是如果有查询条件,那么%20MyISAM%20也%20game%20over%20了,MyISAM%20引擎不支持条件缓存。

On%20MyISAM,%20doing%20a%20query%20that%20does%20SELECT%20COUNT(*)%20FROM%20{some_table},%20is%20very%20fast,%20since%20MyISAM%20keeps%20the%20information%20in%20the%20index

其他%20DB%20引擎

受到%20MySIAM%20DB%20的启发,我们可以手动维护总数缓存在表的索引中了。

1、如果%20ID%20连续,且基本不会断开。直接取最大值%20ID

2、如果表中存在连续的数字列并设为索引,那么通过页码即可计算出此字段的范围,直接作范围查询即可:

start%20=%20(page-1)*pagesize+1%20
end%20=%20page*pagesize%20
select%20*%20from%20table%20where%20id%20>start%20and%20id%20<=end

1、涉及到总数操作,专门维护一个总数。新增一个用户,总数值加%201,%20需要总数的时候直接拿这个总数,%20比如分页时。如果有多个条件,那么就需要维护多个总数列。该方案的扩展性更好,随着用户表数量增大,%20水平切分用户表,要获取用户总数,直接查询这个总数表即可。

分页正反偏移

数据库自带的%20skip%20和%20limit%20的限制条件为我们创建了分页的查询方式,但是如果利用不对,性能会出现千倍万倍差异。

简单一点描述:limit%20100000,20%20的意思扫描满足条件的%20100020%20行,扔掉前面的%20100000%20行,返回最后的%2020%20行,问题就在这里。如果我反向查询%20oder%20by%20xx%20desc%20limit%200,20,那么我只要索引%2020%20条数据。

Example%203

mysql>%20select%20count(*)%20from%20elastic_task_log_copy;
+----------+
|%20count(*)%20|
+----------+
|%201705162%20|
+----------+
1%20rows%20in%20set%20(2.31%20sec)

正向偏移查询。超级浪费的查询,需要先%20skip%20大量的符合条件的查询。

mysql>%20select%20id%20from%20elastic_task_log_copy%20order%20by%20id%20asc%20limit%201705152,10;
+---------+
|%20id%20|
+---------+
|%201705157%20|
|%201705158%20|
|%201705159%20|
|%201705160%20|
|%201705161%20|
|%201705162%20|
|%201705163%20|
|%201705164%20|
|%201705165%20|
|%201705166%20|
+---------+
10%20rows%20in%20set%20(2.97%20sec)

反向偏移查询。同样的查询结果,千差万别的结果。

mysql>%20select%20id%20from%20elastic_task_log_copy%20order%20by%20id%20desc%20limit%200,10;
+---------+
|%20id%20|
+---------+
|%201705166%20|
|%201705165%20|
|%201705164%20|
|%201705163%20|
|%201705162%20|
|%201705161%20|
|%201705160%20|
|%201705159%20|
|%201705158%20|
|%201705157%20|
+---------+
10%20rows%20in%20set%20(0.01%20sec)

这两条%20sql%20是为查询最后一页的翻页%20sql%20查询用的。由于一次翻页往往只需要查询较小的数据,如%2010%20条,但需要向后扫描大量的数据,也就是越往后的翻页查询,扫描的数据量会越多,查询的速度也就越来越慢。

由于查询的数据量大小是固定的,如果查询速度不受翻页的页数影响,或者影响最低,那么这样是最佳的效果了(查询最后最几页的速度和开始几页的速度一致)。

在翻页的时候,往往需要对其中的某个字段做排序(这个字段在索引中),升序排序。那么可不可以利用索引的有序性%20来解决上面遇到的问题。

比如有%2010000%20条数据需要做分页,那么前%205000%20条做%20asc%20排序,后%205000%20条%20desc%20排序,在%20limit%20startnum,pagesize%20参数中作出相应的调整。

但是这无疑给应用程序带来复杂,这条%20sql%20是用于论坛回复帖子的%20sql,往往用户在看帖子的时候,一般都是查看前几页和最后几页,那么在翻页的时候最后几页的翻页查询采用%20desc%20的方式来实现翻页,这样就可以较好的提高性能。

游标:上一页的最大值或者最小值

如果你知道上一页和下一页的临界值,那么翻页查询也是信手拈来了,直接就告诉了数据库我的起始查询在哪,也就没有什么性能问题了。我更愿意称这个东西为游标%20(Cursor)。

如果做下拉刷新,那么就直接避免掉分页的问题了。根据上一页的最后一个值去请求新数据。

mysql>%20select%20id%20from%20elastic_task_log_copy%20where%20id%20>=%201699999%20limit%2010;
+---------+
|%20id%20|
+---------+
|%201699999%20|
|%201700000%20|
|%201700001%20|
|%201700002%20|
|%201700003%20|
|%201700004%20|
|%201700005%20|
|%201700006%20|
|%201700007%20|
|%201700008%20|
+---------+
10%20rows%20in%20set%20(0.01%20sec)

缓存和不精准

数据量达到一定程度的时候,用户根本就不关心精准的总数,%20没人关心差几个。看看知乎、微博、微信订阅号,不精准的统计到处都是。

如果每次点击分页的时候都进行一次%20count%20操作,那速度肯定不会快到哪里去。他们一般也是采用计数器的办法。每次新增加一个粉丝,就把值加%201,直接在用户信息存储一个总数,一段时间后重新查询一次,更新该缓存。这样分页的时候直接拿这个总数进行分页,显示的时候直接显示模糊之就行。

那为什么微信公众号的阅读量只有%2010W+%20这个量级呢?100W+%20级去哪了!

5.%20其他的建议

1、mysql%20的数据查询,%20大小字段要分开,%20这个还是有必要的,%20除非一点就是你查询的都是索引内容而不是表内容,%20比如只查询%20id%20等等

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

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

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

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

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

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

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

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

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

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

如果对此有什么疑问欢迎留言!!!

 



Tags:Java架构   点击:()  评论:()
声明:本站部分内容及图片来自互联网,转载是出于传递更多信息之目的,内容观点仅代表作者本人,如有任何标注错误或版权侵犯请与我们联系(Email:2595517585@qq.com),我们将及时更正、删除,谢谢。
▌相关推荐
1. 前言对于Flink中各个组件(JobMaster、TaskManager、Dispatcher等),其底层RPC框架基于Akka实现,本文着重分析Flink中的Rpc框架实现机制及梳理其通信流程。 2. Akka介绍由于Fli...【详细内容】
2019-10-09  Tags: Java架构  点击:(103)  评论:(0)  加入收藏
一、数据库架构原则 高可用 高性能 一致性 扩展性二、常见的架构方案方案一:主备架构,只有主库提供读写服务,备库冗余作故障转移用 jdbc:mysql://vip:3306/xxdb1、高可用分析:高...【详细内容】
2019-09-27  Tags: Java架构  点击:(121)  评论:(0)  加入收藏
一、数据库命令规范&middot; 所有数据库对象名称必须使用小写字母并用下划线分割&middot; 所有数据库对象名称禁止使用mysql保留关键字(如果表名中包含关键字查询时,需要将其...【详细内容】
2019-08-29  Tags: Java架构  点击:(158)  评论:(0)  加入收藏
前言当我们操作mysql的时候,如果数据量很小,那么我们如何处理都没有问题。但是当一张表非常大的时候,我们一个大查询,一个堆大插入,一个count(*),一个limit都是非常恐怖的,因此,我...【详细内容】
2019-08-27  Tags: Java架构  点击:(330)  评论:(0)  加入收藏
引言这是一篇讲解微服务系统在扩展性伸缩性方面的演进文章,Jonas Boner认为目前普通的微服务最终将演进为事件驱动的响应式微系统架构。今天系统架构大概有三种:单体Monolith...【详细内容】
2019-08-14  Tags: Java架构  点击:(250)  评论:(0)  加入收藏
我本人曾经使用过 ZooKeeper 作为 Dubbo 的注册中心,另外在搭建 Solr 集群的时候,我使用到了 ZooKeeper 作为 Solr 集群的管理工具。前几天,总结项目经验的时候,我突然问自己 Zo...【详细内容】
2019-07-11  Tags: Java架构  点击:(299)  评论:(0)  加入收藏
多人都想知道架构师是做什么?我们看看下面的一段对话。菜鸟 &mdash;&mdash; 刚入门的程序员老鸟 &mdash;&mdash; 资深架构师老鸟:菜鸟,你的目标是什么?菜鸟:我要成为一个软件架构...【详细内容】
2019-06-18  Tags: Java架构  点击:(400)  评论:(0)  加入收藏
一、前言一个成熟的大型网站(如淘宝、京东等)的系统架构并不是开始设计就具备完整的高性能、高可用、安全等特性,它总是随着用户量的增加,业务功能的扩展逐渐演变完善的,在这个过...【详细内容】
2019-06-17  Tags: Java架构  点击:(332)  评论:(0)  加入收藏
说起MySQL的查询优化,相信大家收藏了一堆奇技淫巧:不能使用SELECT *、不使用NULL字段、合理创建索引、为字段选择合适的数据类型..... 你是否真的理解这些优化技巧?是否理解其...【详细内容】
2019-05-05  Tags: Java架构  点击:(467)  评论:(0)  加入收藏
▌简易百科推荐
为了构建高并发、高可用的系统架构,压测、容量预估必不可少,在发现系统瓶颈后,需要有针对性地扩容、优化。结合楼主的经验和知识,本文做一个简单的总结,欢迎探讨。1、QPS保障目标...【详细内容】
2021-12-27  大数据架构师    Tags:架构   点击:(5)  评论:(0)  加入收藏
前言 单片机开发中,我们往往首先接触裸机系统,然后到RTOS,那么它们的软件架构是什么?这是我们开发人员必须认真考虑的问题。在实际项目中,首先选择软件架构是非常重要的,接下来我...【详细内容】
2021-12-23  正点原子原子哥    Tags:架构   点击:(7)  评论:(0)  加入收藏
现有数据架构难以支撑现代化应用的实现。 随着云计算产业的快速崛起,带动着各行各业开始自己的基于云的业务创新和信息架构现代化,云计算的可靠性、灵活性、按需计费的高性价...【详细内容】
2021-12-22    CSDN  Tags:数据架构   点击:(10)  评论:(0)  加入收藏
▶ 企业级项目结构封装释义 如果你刚毕业,作为Java新手程序员进入一家企业,拿到代码之后,你有什么感觉呢?如果你没有听过多模块、分布式这类的概念,那么多半会傻眼。为什么一个项...【详细内容】
2021-12-20  蜗牛学苑    Tags:微服务   点击:(9)  评论:(0)  加入收藏
我是一名程序员关注我们吧,我们会多多分享技术和资源。进来的朋友,可以多了解下青锋的产品,已开源多个产品的架构版本。Thymeleaf版(开源)1、采用技术: springboot、layui、Thymel...【详细内容】
2021-12-14  青锋爱编程    Tags:后台架构   点击:(21)  评论:(0)  加入收藏
在了解连接池之前,我们需要对长、短链接建立初步认识。我们都知道,网络通信大部分都是基于TCP/IP协议,数据传输之前,双方通过“三次握手”建立连接,当数据传输完成之后,又通过“四次挥手”释放连接,以下是“三次握手”与“四...【详细内容】
2021-12-14  架构即人生    Tags:连接池   点击:(17)  评论:(0)  加入收藏
随着移动互联网技术的快速发展,在新业务、新领域、新场景的驱动下,基于传统大型机的服务部署方式,不仅难以适应快速增长的业务需求,而且持续耗费高昂的成本,从而使得各大生产厂商...【详细内容】
2021-12-08  架构驿站    Tags:分布式系统   点击:(23)  评论:(0)  加入收藏
本系列为 Netty 学习笔记,本篇介绍总结Java NIO 网络编程。Netty 作为一个异步的、事件驱动的网络应用程序框架,也是基于NIO的客户、服务器端的编程框架。其对 Java NIO 底层...【详细内容】
2021-12-07  大数据架构师    Tags:Netty   点击:(17)  评论:(0)  加入收藏
前面谈过很多关于数字化转型,云原生,微服务方面的文章。虽然自己一直做大集团的SOA集成平台咨询规划和建设项目,但是当前传统企业数字化转型,国产化和自主可控,云原生,微服务是不...【详细内容】
2021-12-06  人月聊IT    Tags:架构   点击:(23)  评论:(0)  加入收藏
微服务看似是完美的解决方案。从理论上来说,微服务提高了开发速度,而且还可以单独扩展应用的某个部分。但实际上,微服务带有一定的隐形成本。我认为,没有亲自动手构建微服务的经历,就无法真正了解其复杂性。...【详细内容】
2021-11-26  GreekDataGuy  CSDN  Tags:单体应用   点击:(35)  评论:(0)  加入收藏
最新更新
栏目热门
栏目头条