分库分表下的分页查询:底层逻辑、全场景坑点与生产级最优解

简介: 分库分表环境下分页查询的挑战与解决方案 在分库分表架构中,传统分页查询面临数据错乱、性能下降等核心问题。本文剖析了五种主流解决方案: 全局视野法:全量查询后归并排序,保证准确性但性能随分页深度下降 游标分页法:基于值定位,性能稳定但仅支持顺序翻页 分片键路由法:精准定位分片,性能最优但需携带分片键 ES索引法:支持复杂查询和跳页,但引入额外组件 范围分片优化:减少扫描分片数,仅适用于范围分片场景 生产实践需注意排序字段唯一性、深分页限制、分片键选择等关键点。

在分布式系统架构中,分库分表是解决MySQL单表千万级以上数据量性能瓶颈的核心方案。但绝大多数开发者在落地分库分表后,都会遇到分页查询的致命问题:要么分页结果数据错乱、重复丢失,要么深分页接口直接超时、应用OOM,甚至引发数据库集群雪崩。

本文将从底层原理出发,彻底讲透分库分表分页查询的核心难题,拆解所有主流方案的实现逻辑、优缺点与适用场景,同时梳理99%开发者踩过的坑与最优避坑方案,兼顾深度与可读性,让你既能夯实底层基础,又能直接解决线上实际问题。

一、先搞懂:为什么分库分表后,分页查询会成为难题?

1.1 单库单表分页的底层逻辑

MySQL单库单表的分页查询,标准语法为SELECT * FROM t_order ORDER BY id LIMIT offset, size;,其底层执行逻辑为:

  • 扫描满足条件的前offset + size行数据;
  • 丢弃前offset行,返回剩余的size行结果。 该逻辑在单表中可以保证结果的准确性,因为所有数据都在同一个物理表中,排序是全局有序的。即使是深分页(offset过大),最多也只是单表扫描性能下降,不会出现结果错乱的问题。

1.2 分库分表的核心分片模型

本文仅讨论对分页影响最大的水平分片(垂直分片不改变单表数据结构,分页逻辑与单库一致),主流分片规则分为两类:

  • 哈希分片:按分片键的哈希值取模,将数据均匀分散到多个分片表/库中,比如t_order_${order_id % 2},数据分布均匀,无热点问题,但无法预判数据所在的分片范围。
  • 范围分片:按分片键的数值范围划分分片,比如按id范围,分片1存1-10000,分片2存10001-20000,或按时间按月分片,可精准定位数据所在分片,范围查询性能优异,但易出现数据热点问题。

1.3 分库分表分页的核心矛盾

分库分表后,数据分散在多个独立的物理分片上,每个分片仅存储全局数据的一部分,没有任何一个分片持有全局有序的完整数据集,这就是分页查询的核心矛盾。

这里用一个极简的示例,让你一眼看懂常规分页方案的错误: 假设我们有2个分片表,采用哈希分片,分片键为id,分片规则为id % 2

  • t_order_0(分片0)存储id为偶数的数据:2、4、6、8、10、12
  • t_order_1(分片1)存储id为奇数的数据:1、3、5、7、9、11

业务需求:按id升序分页,取第2页,每页2条数据,即offset=2,size=2,正确结果应为3、4。

如果直接在每个分片执行单表分页SQL:

SELECT * FROM t_order_0 ORDER BY id LIMIT 2,2;
SELECT * FROM t_order_1 ORDER BY id LIMIT 2,2;

执行结果:

  • 分片0返回:6、8
  • 分片1返回:5、7 将两个分片的结果合并排序后,取前2条得到5、6,与正确结果3、4完全不符,出现了严重的数据错乱。

这就是绝大多数开发者落地分库分表时踩的第一个坑:直接把单表分页逻辑照搬到分库分表中,导致分页结果完全错误。

二、分库分表分页的主流方案全拆解:底层逻辑、代码实现与适用场景

本章节所有示例均采用当前最新稳定版组件:

  • Spring Boot 3.3.5
  • ShardingSphere-JDBC 5.5.0(分布式数据治理中间件)
  • MyBatis-Plus 3.5.9
  • MySQL 8.4
  • Maven项目管理

基础环境配置

maven核心依赖,可直接编译运行:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">

   <modelVersion>4.0.0</modelVersion>
   <parent>
       <groupId>org.springframework.boot</groupId>
       <artifactId>spring-boot-starter-parent</artifactId>
       <version>3.3.5</version>
       <relativePath/>
   </parent>
   <groupId>com.example</groupId>
   <artifactId>sharding-page-demo</artifactId>
   <version>0.0.1-SNAPSHOT</version>
   <name>sharding-page-demo</name>
   <description>分库分表分页查询示例</description>
   <properties>
       <java.version>17</java.version>
       <shardingsphere.version>5.5.0</shardingsphere.version>
       <mybatis-plus.version>3.5.9</mybatis-plus.version>
   </properties>
   <dependencies>
       <dependency>
           <groupId>org.springframework.boot</groupId>
           <artifactId>spring-boot-starter-web</artifactId>
       </dependency>
       <dependency>
           <groupId>org.apache.shardingsphere</groupId>
           <artifactId>shardingsphere-jdbc-core-spring-boot-starter</artifactId>
           <version>${shardingsphere.version}</version>
       </dependency>
       <dependency>
           <groupId>com.baomidou</groupId>
           <artifactId>mybatis-plus-spring-boot3-starter</artifactId>
           <version>${mybatis-plus.version}</version>
       </dependency>
       <dependency>
           <groupId>com.mysql</groupId>
           <artifactId>mysql-connector-j</artifactId>
           <scope>runtime</scope>
       </dependency>
       <dependency>
           <groupId>org.projectlombok</groupId>
           <artifactId>lombok</artifactId>
           <optional>true</optional>
       </dependency>
       <dependency>
           <groupId>org.springframework.boot</groupId>
           <artifactId>spring-boot-starter-test</artifactId>
           <scope>test</scope>
       </dependency>
   </dependencies>
   <build>
       <plugins>
           <plugin>
               <groupId>org.springframework.boot</groupId>
               <artifactId>spring-boot-maven-plugin</artifactId>
               <configuration>
                   <excludes>
                       <exclude>
                           <groupId>org.projectlombok</groupId>
                           <artifactId>lombok</artifactId>
                       </exclude>
                   </excludes>
               </configuration>
           </plugin>
       </plugins>
   </build>
</project>

MySQL表结构语句:

CREATE TABLE IF NOT EXISTS `t_order_0` (
 `order_id` BIGINT NOT NULL COMMENT '订单ID',
 `user_id` BIGINT NOT NULL COMMENT '用户ID',
 `order_amount` DECIMAL(10,2) NOT NULL COMMENT '订单金额',
 `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
 PRIMARY KEY (`order_id`),
 KEY `idx_user_id` (`user_id`),
 KEY `idx_create_time_order_id` (`create_time`,`order_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='订单表0';

CREATE TABLE IF NOT EXISTS `t_order_1` (
 `order_id` BIGINT NOT NULL COMMENT '订单ID',
 `user_id` BIGINT NOT NULL COMMENT '用户ID',
 `order_amount` DECIMAL(10,2) NOT NULL COMMENT '订单金额',
 `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
 PRIMARY KEY (`order_id`),
 KEY `idx_user_id` (`user_id`),
 KEY `idx_create_time_order_id` (`create_time`,`order_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='订单表1';

ShardingSphere基础配置(application.yml),哈希分片规则:

spring:
 shardingsphere:
   datasource:
     names: ds0
     ds0:
       type: com.zaxxer.hikari.HikariDataSource
       driver-class-name: com.mysql.cj.jdbc.Driver
       jdbc-url: jdbc:mysql://127.0.0.1:3306/sharding_demo?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
       username: root
       password: root
   rules:
     sharding:
       tables:
         t_order:
           actual-data-nodes: ds0.t_order_${0..1}
           table-strategy:
             standard:
               sharding-column: order_id
               sharding-algorithm-name: order-inline
       sharding-algorithms:
         order-inline:
           type: INLINE
           props:
             algorithm-expression: t_order_${order_id % 2}
   props:
     sql-show: true

实体类Order:

package com.example.shardingpagedemo.entity;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;

import java.math.BigDecimal;
import java.time.LocalDateTime;

@Data
@TableName("t_order")
public class Order {
   @TableId(type = IdType.ASSIGN_ID)
   private Long orderId;
   private Long userId;
   private BigDecimal orderAmount;
   private LocalDateTime createTime;
}

Mapper接口OrderMapper:

package com.example.shardingpagedemo.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.example.shardingpagedemo.entity.Order;
import org.apache.ibatis.annotations.Mapper;

@Mapper
public interface OrderMapper extends BaseMapper<Order> {
}

MyBatis-Plus分页配置类:

package com.example.shardingpagedemo.config;

import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class MybatisPlusConfig {
   @Bean
   public MybatisPlusInterceptor mybatisPlusInterceptor() {
       MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
       interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
       return interceptor;
   }
}

2.1 方案一:全局视野法(二次排序法)

2.1.1 底层逻辑

要保证分库分表分页结果的全局准确性,必须让每个分片都返回从第0行到offset+size行的全量数据,在应用层/中间件层完成全局排序后,再截取offset到offset+size的目标数据。

回到前文的示例,offset=2,size=2,正确的执行逻辑为:

  1. 每个分片执行SELECT * FROM t_order_${0..1} ORDER BY id LIMIT 0, 4;(offset+size=4)
  2. 分片0返回:2、4、6、8;分片1返回:1、3、5、7
  3. 应用层将两个分片的结果合并,按id全局排序得到:1、2、3、4、5、6、7、8
  4. 截取offset=2开始的2条数据,得到正确结果:3、4

ShardingSphere-JDBC已经内置了该方案的完整实现,开发者无需手动编写合并排序逻辑,只需正常编写分页SQL,中间件会自动完成SQL改写、结果合并排序。

2.1.2 底层优化:流式归并排序

很多开发者担心全量数据加载到内存会导致OOM,ShardingSphere采用了流式归并排序优化该问题:

  • 每个分片的查询结果都是按排序字段有序的;
  • 采用优先级队列(PriorityQueue)实现多路归并排序,无需将所有数据加载到内存,仅需保存每个分片的当前游标;
  • 跳过前offset条数据时,仅移动游标,不加载全量数据到内存,极大降低了内存占用。

2.1.3 代码实现

Service层分页实现:

package com.example.shardingpagedemo.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.example.shardingpagedemo.entity.Order;
import com.example.shardingpagedemo.mapper.OrderMapper;
import com.example.shardingpagedemo.service.OrderService;
import org.springframework.stereotype.Service;

@Service
public class OrderServiceImpl extends ServiceImpl<OrderMapper, Order> implements OrderService {
   @Override
   public IPage<Order> pageList(int pageNum, int pageSize) {
       Page<Order> page = new Page<>(pageNum, pageSize);
       LambdaQueryWrapper<Order> wrapper = new LambdaQueryWrapper<Order>()
               .orderByAsc(Order::getOrderId);
       return this.page(page, wrapper);
   }
}

Controller层接口:

package com.example.shardingpagedemo.controller;

import com.baomidou.mybatisplus.core.metadata.IPage;
import com.example.shardingpagedemo.entity.Order;
import com.example.shardingpagedemo.service.OrderService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/order")
public class OrderController {
   @Autowired
   private OrderService orderService;

   @GetMapping("/page")
   public IPage<Order> pageList(@RequestParam int pageNum, @RequestParam int pageSize) {
       return orderService.pageList(pageNum, pageSize);
   }
}

当调用/order/page?pageNum=2&pageSize=2时,ShardingSphere会自动改写SQL,在每个分片执行全量查询,完成归并排序后返回正确结果。

2.1.4 优缺点与适用场景

优点 缺点
100%保证分页结果准确性 offset越大,每个分片需要扫描的数据量越大,数据库IO压力呈线性增长
实现简单,无需修改业务代码,ShardingSphere内置支持 深分页场景下,数据传输量极大,网络IO与应用排序压力剧增,易引发超时与OOM
支持任意跳页,满足后台管理系统的分页需求 分片数量越多,性能下降越明显

适用场景:后台管理系统的分页查询,且严格限制最大翻页数(建议不超过100页,offset不超过10000);小数据量、少分片的分库分表场景;无法使用其他优化方案的兜底场景。绝对禁止场景:无限制的深分页查询,C端用户的无限滚动列表。

2.2 方案二:游标分页法(滚动分页、禁止跳页法)

这是分库分表场景下,解决深分页问题的生产级最优方案,也是业内C端场景的通用标准方案。

2.2.1 底层逻辑

摒弃传统的offset行号定位方式,采用基于值的游标定位

  • 每次分页查询,返回结果的最后一条数据的排序字段值作为下一次查询的游标;
  • 下一次查询时,通过WHERE 排序字段 > 上一次的游标值过滤数据,再执行LIMIT size
  • 每个分片仅需扫描符合条件的size条数据,无需扫描前offset行,无论翻多少页,性能始终稳定。

该方案的核心前提:排序字段必须是全局唯一、单调递增/递减的,保证排序的确定性,避免出现数据重复或丢失。推荐使用主键ID创建时间+全局唯一ID作为排序字段,禁止仅用非唯一的创建时间排序。

回到前文的示例,采用游标分页的执行逻辑:

  1. 第一页查询,size=2,执行SQL:SELECT * FROM t_order ORDER BY id ASC LIMIT 2;
  2. 返回结果:1、2,最后一条数据的id=2,作为下一页的游标;
  3. 第二页查询,游标id=2,执行SQL:SELECT * FROM t_order WHERE id > 2 ORDER BY id ASC LIMIT 2;
  4. ShardingSphere在每个分片执行对应SQL,分片0返回:4、6;分片1返回:3、5;
  5. 合并排序后得到:3、4、5、6,取前2条,得到正确结果3、4。

可以看到,每个分片仅需扫描2条数据,无论翻到第100页还是第1000页,每个分片的扫描行数始终是size条,性能完全稳定,无任何深分页问题。

2.2.2 代码实现

Service层游标分页实现:

package com.example.shardingpagedemo.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.example.shardingpagedemo.entity.Order;
import com.example.shardingpagedemo.mapper.OrderMapper;
import com.example.shardingpagedemo.service.OrderService;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
public class OrderServiceImpl extends ServiceImpl<OrderMapper, Order> implements OrderService {
   @Override
   public List<Order> cursorPage(Long lastId, int pageSize) {
       LambdaQueryWrapper<Order> wrapper = new LambdaQueryWrapper<Order>()
               .gt(lastId != null, Order::getOrderId, lastId)
               .orderByAsc(Order::getOrderId)
               .last("LIMIT " + pageSize);
       return this.list(wrapper);
   }
}

Controller层接口:

@GetMapping("/cursorPage")
public List<Order> cursorPage(@RequestParam(required = false) Long lastId, @RequestParam int pageSize) {
   return orderService.cursorPage(lastId, pageSize);
}

首次调用/order/cursorPage?pageSize=2,lastId不传,返回第一页数据;第二次调用/order/cursorPage?lastId=2&pageSize=2,返回第二页正确数据,性能稳定。

2.2.3 优缺点与适用场景

优点 缺点
性能极致稳定,无论翻多少页,每个分片仅扫描size条数据,无深分页问题 不支持跳页,仅支持上一页/下一页的滚动查询
100%保证分页结果准确性 排序字段必须全局唯一、单调,无法支持复杂的多字段非唯一排序
实现简单,无额外组件依赖,业务改造成本极低 无法直接获取总条数与总页数,需额外查询count
分片数量对性能影响极小,支持大规模分片集群

适用场景:C端APP、小程序的无限滚动列表(如订单列表、商品列表、信息流);大数据量、深分页的无跳页查询场景;大规模分片集群的分页查询。不适用场景:后台管理系统需要任意跳页的分页查询。

2.3 方案三:分片键精准路由法

这是分库分表分页查询的性能天花板方案,没有之一。

2.3.1 底层逻辑

分库分表的核心是分片键,若查询条件中携带了完整的分片键,ShardingSphere可直接通过分片算法计算出数据所在的唯一分片,将查询请求精准路由到该分片,后续的分页查询逻辑与单库单表完全一致,无任何额外开销,性能与单表分页完全相同。

比如,我们将订单表的分片键改为user_id,分片规则为user_id % 2,当用户查询自己的订单列表时,必然会携带user_id查询条件,ShardingSphere可直接路由到对应的分片表,无需全分片扫描,分页查询完全无性能问题。

2.3.2 代码实现

修改ShardingSphere分片规则,分片键改为user_id:

spring:
 shardingsphere:
   rules:
     sharding:
       tables:
         t_order:
           actual-data-nodes: ds0.t_order_${0..1}
           table-strategy:
             standard:
               sharding-column: user_id
               sharding-algorithm-name: order-inline
       sharding-algorithms:
         order-inline:
           type: INLINE
           props:
             algorithm-expression: t_order_${user_id % 2}

Service层带分片键的分页实现:

@Override
public IPage<Order> pageListByUserId(Long userId, int pageNum, int pageSize) {
   Page<Order> page = new Page<>(pageNum, pageSize);
   LambdaQueryWrapper<Order> wrapper = new LambdaQueryWrapper<Order>()
           .eq(Order::getUserId, userId)
           .orderByDesc(Order::getCreateTime)
           .orderByDesc(Order::getOrderId);
   return this.page(page, wrapper);
}

当调用pageListByUserId(1001, 100, 10)时,ShardingSphere会通过1001 % 2 = 1计算出分片为t_order_1,直接路由到该分片执行分页SQL,与单库单表分页完全一致,即使offset=10000,也仅在单个分片扫描,性能无任何问题。

2.3.3 优缺点与适用场景

优点 缺点
性能天花板,与单库单表分页完全一致,无任何额外开销 必须携带完整的分片键查询条件,否则无法使用
100%保证数据准确性,支持任意跳页与深分页 分片键的选择必须与高频查询场景完全匹配
实现简单,无业务改造成本

适用场景:C端用户查询自己的私有数据(如个人订单、个人收藏、个人账单);所有能携带分片键的分页查询场景,优先使用该方案。不适用场景:无法携带分片键的全量数据分页查询(如后台管理系统的全平台订单查询)。

2.4 方案四:ES二级索引法(全局索引法)

该方案是解决大数据量、支持跳页、深分页场景的唯一成熟生产级方案,兼顾了跳页能力与深分页性能。

2.4.1 底层逻辑

分库分表分页的核心矛盾是没有全局有序的数据集,该方案通过引入全局索引组件解决该问题:

  1. 通过Binlog同步组件(Canal/Maxwell)将分库分表中的分页查询关键字段(排序字段、查询条件、主键ID、分片键)实时同步到Elasticsearch中;
  2. 分页查询时,先在ES中执行分页查询,获取符合条件的主键ID列表与分片键;
  3. 通过主键ID+分片键,精准路由到对应分片,批量查询完整数据;
  4. 组装数据后返回给前端。

该方案的核心优势:ES天生支持分布式分页,深分页性能优异,同时支持复杂的条件查询与跳页,完美解决了分库分表分页的核心矛盾。

架构图如下:

graph LR

A[业务应用] --> B[Elasticsearch全局索引]

A --> C[ShardingSphere-JDBC]

C --> D[分库分表集群]

E[Binlog同步组件Canal] --> D

E --> B

2.4.2 核心实现代码

ES索引设计,仅存储分页查询所需的关键字段,避免数据冗余:

{
 "mappings": {
   "properties": {
     "order_id": { "type": "long" },
     "user_id": { "type": "long" },
     "order_amount": { "type": "double" },
     "create_time": { "type": "date", "format": "yyyy-MM-dd HH:mm:ss" }
   }
 }
}

Service层ES分页实现:

@Override
public IPage<Order> esPage(int pageNum, int pageSize) {
   int offset = (pageNum - 1) * pageSize;
   // 1. 在ES中执行分页查询,获取order_id列表
   SearchRequest searchRequest = new SearchRequest("order_index");
   SearchSourceBuilder sourceBuilder = new SearchSourceBuilder()
           .query(QueryBuilders.matchAllQuery())
           .sort("create_time", SortOrder.DESC)
           .sort("order_id", SortOrder.DESC)
           .from(offset)
           .size(pageSize)
           .fetchSource(new String[]{"order_id", "user_id"}, null);
   searchRequest.source(sourceBuilder);

   SearchResponse response;
   try {
       response = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);
   } catch (IOException e) {
       throw new RuntimeException("ES查询失败", e);
   }

   // 解析ES返回的order_id列表
   List<Long> orderIdList = Arrays.stream(response.getHits().getHits())
           .map(hit -> Long.parseLong(hit.getId()))
           .toList();
   long total = response.getHits().getTotalHits().value;

   if (orderIdList.isEmpty()) {
       return new Page<>(pageNum, pageSize, total);
   }

   // 2. 批量查询分库分表中的完整数据,带order_id分片键可精准路由
   List<Order> orderList = this.listByIds(orderIdList);

   // 3. 按ES返回的顺序排序,避免in查询导致的顺序错乱
   Map<Long, Order> orderMap = orderList.stream()
           .collect(Collectors.toMap(Order::getOrderId, o -> o));
   List<Order> sortedList = orderIdList.stream()
           .map(orderMap::get)
           .filter(Objects::nonNull)
           .toList();

   // 4. 组装分页结果
   Page<Order> page = new Page<>(pageNum, pageSize, total);
   page.setRecords(sortedList);
   return page;
}

2.4.3 优缺点与适用场景

优点 缺点
支持任意跳页与深分页,性能稳定,无offset限制 引入额外组件,架构复杂度提升,运维成本增加
支持复杂的多条件查询、模糊查询、范围查询,远超MySQL的查询能力 存在数据一致性延迟,binlog同步有毫秒级延迟,无法满足强一致性场景
仅需在ES中存储关键字段,存储成本低,查询性能优异 需保证ES与MySQL的数据一致性,异常场景需处理数据修复

适用场景:后台管理系统的全平台大数据量分页查询,需要跳页且有深分页需求;复杂多条件的分页查询场景,MySQL索引无法满足需求;分库分表集群规模大,无法使用全局视野法的场景。不适用场景:强一致性要求的实时数据查询,小数据量简单分页场景。

2.5 方案五:范围分片优化法

该方案是针对范围分片场景的专属优化方案,可大幅降低分页查询的分片扫描数量。

2.5.1 底层逻辑

若采用范围分片(如按id范围、时间范围分片),数据的分片范围是可预判的,分页查询时可先通过计算定位到offset所在的分片范围,仅需扫描相关分片,无需全分片扫描,大幅提升性能。

比如,按id范围分为3个分片:

  • 分片0:id 1-10000
  • 分片1:id 10001-20000
  • 分片2:id 20001-30000

当需要查询offset=15000,size=10时,可直接定位到分片1和分片2,分片0无需扫描,减少了1/3的数据库压力与数据传输量。

2.5.2 优缺点与适用场景

优点 缺点
大幅减少扫描的分片数量,提升分页查询性能 易出现数据热点问题,最新数据集中在单个分片
范围查询性能优异,适合按时间维度的报表分页 分片扩容难度大,需调整范围规则
支持跳页与深分页,数据准确性100% 仅适用于范围分片场景,无法用于哈希分片

适用场景:按时间范围分片的日志、报表、账单数据的分页查询;数据冷热分离的场景,冷数据分片无需扫描;范围查询为主的业务场景。不适用场景:哈希分片场景,高并发写入易出现热点的业务场景。

三、生产级避坑指南:99%开发者踩过的分页陷阱

3.1 排序字段非全局唯一导致的数据丢失/重复

坑点描述:很多开发者仅用create_time作为排序字段,同一毫秒内会产生多条数据,跨分片时,合并排序的顺序无法保证确定性,导致分页时出现数据重复或丢失。错误示例SELECT * FROM t_order ORDER BY create_time DESC LIMIT offset, size;解决方案:排序字段必须包含全局唯一字段,保证排序的确定性,正确示例:SELECT * FROM t_order ORDER BY create_time DESC, order_id DESC LIMIT offset, size;,同时必须为排序字段建立联合索引idx_create_time_order_id (create_time, order_id)

3.2 深分页使用全局视野法导致的OOM与数据库雪崩

坑点描述:无限制的深分页查询,比如offset=100万,size=10,10个分片的场景下,每个分片需要扫描1000010条数据,总扫描行数超过1亿,直接导致数据库CPU打满,应用OOM,甚至集群雪崩。解决方案:严格限制最大翻页数,后台管理系统最多允许翻100页,offset不超过10000;超过限制的查询,强制要求用户添加查询条件缩小数据范围;C端场景必须使用游标分页,禁止使用offset分页。

3.3 分片键选择错误导致的分页性能灾难

坑点描述:分片键选择与业务高频查询场景不匹配,比如分片键选了order_id,但90%的查询都是按user_id分页,无法精准路由,每次查询都全分片扫描,性能极差。解决方案:分片键的选择必须遵循高频查询优先原则,优先选择业务中最常用的查询维度作为分片键,比如用户系统选user_id,商户系统选merchant_id

3.4 跨分片JOIN分页查询的结果错乱

坑点描述:分库分表后,直接使用JOIN关联多个分表进行分页查询,比如订单表关联订单明细表,跨分片JOIN无法保证关联结果的准确性,分页结果完全错乱。解决方案:禁止跨分片JOIN分页查询,采用字段冗余,将常用的关联字段冗余到主表中,避免JOIN;采用应用层组装,先分页查询主表数据,再通过主表ID批量查询明细表数据,在应用层组装结果。

3.5 COUNT查询的性能灾难

坑点描述:分页查询通常需要返回总条数,分库分表后,SELECT COUNT(*) FROM t_order需要全分片扫描,每个分片执行全表count,大数据量下性能极差,甚至超时。解决方案:C端无限滚动场景,无需返回总条数,直接去掉count查询;后台管理系统场景,采用Redis定时统计总条数,或使用MySQL的信息表估算行数,禁止实时全分片count;有条件的count查询,优先使用ES方案,ES的count性能远超MySQL。

四、全场景方案选型决策指南

4.1 分页方案决策流程图

4.2 全场景选型矩阵

业务场景 最优方案 可用方案 禁止方案
C端用户个人数据查询(带分片键) 精准路由法 游标分页法 全局视野法深分页
C端无限滚动列表(无跳页) 游标分页法 ES二级索引法 offset深分页
后台管理系统分页(≤100页) 全局视野法 ES二级索引法 无限制深分页
后台管理系统大数据量深分页 ES二级索引法 范围分片优化法 全局视野法
按时间范围的报表分页 范围分片优化法 ES二级索引法 哈希分片全局视野法
复杂多条件模糊查询分页 ES二级索引法 精准路由法 全局视野法
小表字典数据分页 广播表法 单表分页 分库分表

五、性能优化终极最佳实践

  1. 优先带分片键查询:这是性能最高的方案,没有之一,所有分页查询优先考虑能否携带分片键。
  2. C端场景100%使用游标分页:摒弃offset分页,从根源上解决深分页问题,这是业内C端场景的通用标准。
  3. 严格限制翻页深度:后台管理系统必须限制最大翻页数,禁止无限制的深分页查询,超过限制必须强制添加查询条件。
  4. 排序字段必须建联合索引:排序字段必须包含全局唯一字段,且建立对应的联合索引,避免全表扫描。
  5. 禁止SELECT:分页查询仅返回业务所需的字段,减少数据传输量与内存占用,尤其是全局视野法场景。
  6. 复杂查询优先使用ES:多条件、模糊查询、大数据量分页,优先使用ES二级索引方案,避免MySQL的性能瓶颈。
  7. 避免跨分片JOIN:分库分表后禁止跨分片JOIN分页,采用字段冗余或应用层组装,保证结果准确性与性能。

分库分表下的分页查询,本质上是分布式系统中CAP理论的体现:数据分片带来了扩展性与性能提升,必然会牺牲全局数据视图的便利性。没有万能的最优方案,只有最适合业务场景的方案。

目录
相关文章
|
20天前
|
算法 数据可视化 Java
Java 规则引擎封神指南:从底层原理到生产落地,零冗余全干货实战
规则引擎解耦业务规则与系统代码,实现规则快速迭代。本文系统讲解规则引擎原理与实战,对比Drools、EasyRules等主流方案,剖析Rete算法核心逻辑。通过电商风控系统实战,展示生产级规则引擎架构,包含规则持久化、动态热更新等关键功能。总结性能优化、规则治理等最佳实践,解答常见问题,帮助开发者掌握规则引擎选型与落地。规则引擎适用于规则频繁变更场景,能将规则迭代周期从天级压缩到分钟级,但需根据业务复杂度合理选用。
155 2
|
Java 中间件 数据库连接
分库分表的4种方案
分库分表的4种方案
2280 0
|
20天前
|
人工智能 API
重磅!阿里云Coding Plan全面上线Qwen3.5、GLM-5、MiniMax M2.5、Kimi K2.5
阿里云Coding Plan上线Qwen3.5、GLM-5、M2.5、K2.5四大顶尖开源模型,支持Qwen Code等工具自由切换。Lite/Pro套餐首月仅7.9元/39.9元,分别享1.8万/9万次请求。Qwen3.5以397B总参、17B激活参数实现高性价比,全面优化编程与Agent能力。
|
4天前
|
算法 Java 关系型数据库
JVM GC 深度破局:G1 与 ZGC 底层原理、生产调优全链路实战
本文深度解析JDK17主流GC:G1(默认,兼顾吞吐与延迟)与ZGC(革命性低延迟,STW&lt;1ms)。涵盖核心理论(可达性分析、三色标记)、内存布局、全流程机制(SATB写屏障 vs 染色指针+读屏障)、关键参数调优及生产选型指南,助你精准定位性能瓶颈,高效优化JVM。
139 4
|
21天前
|
SQL 人工智能 分布式计算
关于阿里云 ODPS(MaxCompute)的全面介绍和应用
不忘初心,方得始终,终身学习,终身收益。
366 1
|
21天前
|
安全 Java API
将若依(RuoYi)框架从适配 Spring Boot 2 的版本升级到 Spring Boot 3
将若依(RuoYi)框架从适配 Spring Boot 2 的版本升级到 Spring Boot 3,这是一个涉及依赖、配置、API 兼容等多方面的系统性升级工作。
268 5
|
3月前
|
存储 缓存 监控
从GC日志小白到分析大神:GCEasy实战全攻略
GCEasy是Java GC日志分析利器,支持多种垃圾收集器,通过可视化报表与智能诊断,帮助开发者快速定位内存泄漏、GC频繁等问题。本文结合实战案例,详解其原理、使用方法及性能优化策略,提升系统稳定性与并发能力。
554 1
|
24天前
|
机器学习/深度学习 监控 算法
基于 YOLO26 的摔倒智能检测系统(中英文双版) | 附完整源码与效果演示
本文介绍了一个基于 YOLO26 的摔倒智能检测系统,该系统结合了先进的深度学习技术和友好的用户界面,实现了从数据集管理、模型训练到实时推理测试的完整工作流程。系统具有以下特点: 技术先进:采用最新的 YOLO26 目标检测算法,在精度和速度方面都达到了较高水平 易于使用:提供友好的图形界面,非专业用户也能轻松使用 功能完整:涵盖数据管理、模型训练、推理测试等完整功能 性能优异:支持 GPU 加速和多线程处理,保证高性能 可扩展性强:采用模块化设计,便于功能扩展和定制
基于 YOLO26 的摔倒智能检测系统(中英文双版) | 附完整源码与效果演示

热门文章

最新文章