分表下的分页查询方案的设计与实现

Posted by WGrape的博客 on September 1, 2022

文章内容更新请以 WGrape GitHub博客 : 分表下的分页查询方案的设计与实现 为准

前言

本文原创,著作权归WGrape所有,未经授权,严禁转载

一、背景

随着业务量的增大,数据库的架构会向分库分表的设计过渡。但这不可避免的会引入新问题,可以参考《分库分表带来的问题和解决方案》文章。

本文主要讨论按分表下的分页查询问题,会从设计和实现角度上分析。

二、基础理论

1、为什么分表

在MySQL存储中,一般人为数据量超过500万行时,整体性能会开始下降。

以支付系统为例,每一个人每天大概会产生10条支付记录,在DAU为1000万的情况下,支付系统中每天会产生惊人的1亿条记录!即使DAU只有10万,那么每天产生的数据量也达到了百万级别。

所以在面对这种日均十万、甚至百万的业务量下,单表的MySQL已无法满足性能需要,必须选择分表。

2、如何做分表

一般情况下,分表有基于用户和基于日期(月表和年表)这两种方案。

  • 基于用户分表 :适用于关系、属性类的数据
  • 基于日期分表 :适用于账单、记录类的数据

3、保证线上服务稳定

大家都知道,为了确保线上服务安全稳定,一般都会禁止在线上使用任何复杂的SQL语句,比如Union / Join等多表关联的语句,至于视图、临时表等特性更是明令禁止的。

所以本文讨论的分页查询方案,也不会基于上述功能实现。

三、日期分表下的问题

无论是基于用户分表和基于日期分表,都会面临新的问题。本文只讨论基于日期分表下,面临的分页查询问题。

其实基于用户分表也会面临分页查询问题,只是基于日期分表下的分页查询问题更为常见和经典

1、单表下的分页

我们知道,一般使用的分页查询方式是基于单表的Limit / Offset语句实现的。

image

2、分表下的分页

如下有按月的table_202108/table_202107/table_202106三张分表,它直接破坏了Limit / Offset分页查询的最基础条件,使得分页查询变成了一件极其困难的事情。

image

3、常见的分表分页方案

使用Canal或esupdater把数据表同步至ES,ES底层会基于分片查询实现多表分页。

目前市面上针对分表下的分页有很多尝试性的方案,但设计过于复杂且包含各种不确定性,不建议生产环境使用。目前最成熟可靠的方案之一是使用ES,它可以完美解决分表下的分页查询问题。

不过使用ES的成本较高,有没有一种简单且稳定的方案可以实现呢 ?

四、解决思路

虽然分表下的分页查询很困难,但是单表分页查询很简单,这样我们就可以把问题简化为如何把分表下的分页查询转化为单表下的分页查询。

每次查询固定一张表,在这张表下使用单表分页查询,直到全部查询。然后固定下一张表,继续开始新的一轮分表查询,如此周而复始。这便是本文章的解决分表下分页查询问题的主要思想。

image

五、设计方案

1、基于缓存

为了实现每次分页的时候,服务端都可以固定一张表查询,所以这里会借助Redis。

image

每一个用户在使用分页查询时,服务端都会记录一个Hash类型的Key,它主要由一个table字段和保留字段组成

  • table字段 :用于实现查询时固定一张表
  • 保留字段 :后续扩展使用

2、实现原理

用户首次分页时,前端传来的参数会传来如下的参数

{
    "start": 0, // 查询的下标
    "size": 10, 
    "is_first": 1 // 是否是首次分页
}

服务端判断为首次请求,会删除user_query这个Key,并重新把table="table_202108"写入这个Key中。这时就会固定table_202108为分表查询的单表,如果查询到的数据为空,会再次把table="table_202107"写入Key中,表示下次分表查询的单表为上一张表,然后返回如下数据

{
    "list": [],
    "has_more": 1, // 是否还有更多
    "start": 0, // 前端下次迭代的下标
}

简言之,当上一张表分页查询完后,将table字段置为下一张表,并返回结果。

3、应用场景

由于方案是基于表顺序迭代的方式,所以只适用于如下的连续分页场景

  • APP或PC端的下拉式刷新

4、优缺点

本设计方案设计简单且非常容易实现和扩展,但是缺点是它必须依赖Redis缓存,而且每一个用户一个Key,有些浪费,也会增加Redis的流量,需要做好限流操作。