1、分页目的
分页查询在软件开发中是极为常见的查询方式。计算机资源是有限的,尤其是内存资源。当要查询的数据为海量数据时,一次性加载所有数据显然是不可行的也不合理。会导致服务响应时间变长,吞吐量减小,内存溢出,严重的话还会引发由点到面的服务雪崩,最终导致整个系统不可用。使用分页,每次只查询一部分数据,减少内存消耗,减小响应时间,提高系统性能,提高系统稳定性,从而提高用户体验。
2、常见分页
最常见方式:offset + size
select * from t_user limit offset,size;
第 1 页:
select * from t_user limit 0,10;
select * from t_user limit 10;
第 100 页:
select * from t_user limit 990,10;
注:基于 B+ 数据结构。
分页查询执行过程:
第 1 页:
select * from t_user limit 0,10;
上面select后面带*,也就是要求获取记录行的所有列字段信息。
数据库服务层调用存储引擎接口,存储引擎在主键索引中读取0到10条完整数据返回数据库服务层,服务层把数据返回给调用客户端(可以是navicat ,SQLyog,Java驱动等)
第100页
select * from t_user limit 990,10;
数据库服务层会调用存储引擎接口,由于这次的offset=990,会在存储引擎的主键索引中获取到第0到(990+ 10)条完整行数据,返回给数据库服务层之后根据offset的值挨个抛弃,最后只留下最后面的size条,也就是10条数据,放到数据库服务层的结果集中,返回给客户端。可以看到,当offset越大,数据库服务层从存储引擎拉取的无用数据就越多,拉取这些无用的数据是要消耗机器磁盘IO、内存、CPU资源和增加响应耗时的。
针对上面查100页的优化
select * from t_user where id >= (select id from t_user order by id limit 990, 1) limit 10;
这里会先执行子查询 【select id from t_user order by id limit 990, 1】 这个操作,其实也是在存储引擎的主键索引中获取到(990+ 1)条数据,然后数据库服务层会抛弃前990条,只保留最后一条数据的id。但不同在于,在返回数据库服务层的过程中,只会拷贝数据行内的id这一列,而不会拷贝数据行的所有列,当数据量较大时,这部分的耗时还是比较明显的。
在拿到了上面的id之后,假设这个id正好等于990,那SQL就变成了:
select * from t_user where id >= 990 limit 10;
这和查第 1 页的逻辑就基本一样了。
3、深分页
当随着offset越来越大,甚至达到百万千万级别,俨然就变成了深分页问题。而深分页是无解的,随机深分页更致命。
如上图:把数据的所有页数都展示出来,并且可以随机点击想要的页数,这种就是常见的随机跳页。而当数据是海量的时候就形成了所谓的随机深分页问题。这种问题是没有解的。应该在设计的时候就要避免出现深随机深分页的场景。如果深分页没法避免,那就只限制小幅度跳页。下面我们看下大企业的分页设计:
百度:
bing:
Google:
从上面各大公司分页设计方案看出,即使是搞搜索引擎的大厂也没有一家是支持随机深分页的。
4、分页方案
常见的分页方案:利用已知条件分页,分库分表,利用搜索引擎等。
分库分表:原理是减小单表数据
搜索引擎:利用搜索引擎对大数据的处理能力
已知条件分页:
其实前面讲了当offset较大时,对分页语句进行了优化的思路很接近了。
select * from t_user limit 990,10;
select * from t_user where id >= (select id from t_user order by id limit 990, 1) limit 10;
在优化后的语句中子查询返回的id就是一个已知条件,后面查询的时候直接让【id>=990】,在这里990就是已知条件,只不过这个条件是数据库层面自己计算得到的。那是不是可以想办法我们认为的给出这个条件,答案是很定的,但这需要在交互设计上要配合。比如:
加载更多:可以设计成不显示页面,而变成加载更多,点击加载的时候给接口传参为上一次加载数据中最后一条数据的id。
语句如下
select * from t_user where id >= lastId
小范围点击跳页查询:如上面举例的百度、bing、Google 等就是小范围跳页方案,原理就是根据查询出的数据的最后一个id + 跳几页*每页大小。
比如:从第1页跳到第3页,那么对于第1页的最后1条数据的id索引位置来说往后移动1页即可
语句如下:
select * from t_user where id > lastId limit((3-1-1)*10),10;
5、搜索引擎优化
5.1、问题场景
系统上线多年,产生的数据累计达到一定量,系统的搜索慢查询越来越突出,特别是在早中晚高峰,服务的各种系统资源都在高位负载,磁盘资源尤其突出,随时有宕机风险。
5.2、系统架构:
5.3、存在问题:
1、搜索引擎使用的是ES5.x版本,支持父子索引结构。建立的索引为:1 + 5 模式,用到关联查询,但随着生产的数据量越来越大了,关联查询的弊端就显示出来。
2、es 单个索引字段数默认为1000个。而系统表单为动态表单,理论上用户可以无限拉表单字段,而一个字段会映射为一个索引字段。
3、es5.x 没鉴权能力
4、mongosync 为单点结构
5.4、解决方案:
1、使用宽表设计,以主表为维度,把和主表相关联的数据通过清洗组织存储到同一个索引中,到达了只查询一张索引就能满足以前的业务。
2、多字段问题,实际上系统中的控件也就是30来个,如果能按类型类存储到ES那就还很的解决了字段膨胀问题。
{
"te_0":"单行文本0",
"te_1":"单行文本1",
...
"te_n":"单行文本2",
}
[
{
"Key":"te_0",
"Value":"单行文本0"
},
{
"Key":"te_1",
"Value":"单行文本1"
},
{
"Key":"te_2",
"Value":"单行文本2"
}
]
如上所示,原来需要的 n 个字段类型,现在只需2个就可以解决 Key,Value。
3、升级到es7,但要支撑私有云,代码要支持两个版本的ES搜索引擎,这回导致jar冲突。为了解决这个问题新的搜索访问代码直接通过 http 方式调用。
4、mongosync 数据同步平台
使用ZK分布式协调组件的创建唯一节点能力(分布式锁)和 watcher 机制。分布式锁保证了主节点的唯一性,watcher机制保证了高可用。
选主、活检
6、数据治理
6.1、问题场景
系统上线多年,数据库存储的数据越来越多,最大单表到达10亿+,慢查询频繁告警,特别是在早中晚高峰,服务的各种系统资源都在高位负载,磁盘资源尤其突出,随时有宕机风险。为了系统健康稳定发展,需要对数据进行治理。
6.2、系统架构
6.3、存在问题
问题非常明显即不管是整个数据库,还是单表数据量都非常巨大。
6.4、解决方案
6.4.1、数据分片
分片为应对高吞吐量与大数据量提供了解决方案。使用分片减少了每个分片需要处理的请求数和存储量,因此,通过水平扩展,可以提高自己的存储容量和吞吐量。
6.4.1.1、优点
对于一般应用来说几乎是可以无限且不线下扩容。代码改动量小。
6.4.1.2、缺点
缺点也显而易见,费钱。按最基本的每个集群3台机器,一共为15台。
6.4.2、冷热分离
数据库存储数据量巨大,但绝大多数的数据没有经常被访问,即存在冷数据和热数据。根据业务场景定义冷数据,然后把这部分数据迁移存储到比较廉价的机器上(冷库),以达到降本增效,提高系统稳定等。
6.4.2.1 优点
设备资源成本少。
6.4.2.2 缺点
代码改动量很大,数据路由复杂。
6.4.2.3 挑战
冷热分离后用户无感知。