Spring Boot 3 + ShardingSphere-JDBC 实战:一次把读写分离和分库分表跑通

完整代码已开源,可直接到Gitee仓库复制使用。

数据库扛不住的时候,出路无非两条。查询压力大,加从库,把读分出去;单表数据量压下来,拆表。这两件事手工做起来都不轻松,路由查询该走哪个库、写入该落到哪张表,这些判断写散在业务代码里,后面谁来接手都头疼。

ShardingSphere-JDBC 把这些活儿收到中间件这一层。它不是一个独立部署的代理服务,而是一个"加强版 JDBC 驱动",直接塞进应用的 JVM 里,SQL 出去之前先在本地完成解析、路由、改写、归并。业务侧的感受基本就是:换个依赖,加个配置文件,SQL 照写。

我拿一个订单业务做了个最小的可跑示例,把读写分离、按 user_id 分表、分布式主键、跨分片分页这几件事串成了一条线。


一、示例解决什么问题

先说说背景。一张订单表涨到几千万行以后,会同时遇到两种疼:一种是查询慢,尤其是列表和分页,扫描的数据量太大;另一种是写入和存储压力集中在单个实例上,扩不动。

示例用一套配置同时应对这两件事:

  • 读写分离:写操作(INSERT、UPDATE、DELETE)全部打向主库,查询(SELECT)分给从库。读多写少的业务,加一个从库通常就能明显缓解。
  • 分库分表:这里演示的是分表。按用户 ID 取模,把订单数据分散到两张物理表里,单表数据量直接减半,后续加表也只需要改一行配置。
  • 分布式主键:分表之后自增主键会撞车,两张表各自从 1 开始,合起来必然重复。示例改用雪花算法生成全局唯一的 Long 型 ID。
  • 跨分片分页:不带分片键的分页查询会被路由到所有分片,ShardingSphere 负责把各分片的结果归并、排序、裁剪。这块能跑通,但代价不小,后面专门讲。

二、技术栈与整体结构

版本这块先把底交清楚,ShardingSphere 的配置项在不同大版本之间改过好几轮,用错版本的写法会直接启动报错。

技术 版本 作用
Java 17 运行时
Spring Boot 3.5.16 应用框架
ShardingSphere-JDBC 5.5.2 分片与读写分离中间件
MyBatis-Plus 3.5.15 ORM 与分页插件
MySQL 8.x 主库、从库
HikariCP 随 Spring Boot 连接池
Lombok - 少写样板代码

项目结构不复杂,一个启动类,加配置、实体、Mapper、Service、Controller:

bash 复制代码
sharding-sphere-master-slave/
├── src/main/java/com/song/sharding/sphere/
│   ├── MSApplication.java              # 启动类
│   ├── config/MybatisPlusConfig.java   # 分页插件配置
│   ├── entity/Order.java               # 订单实体
│   ├── mapper/OrderMapper.java         # Mapper 接口
│   ├── service/OrderService.java       # 业务逻辑
│   └── controller/OrderController.java # REST 接口
├── src/main/resources/
│   ├── application.yml                 # Spring Boot 配置
│   ├── sharding-ms.yaml                # 读写分离 + 分片规则
└── pom.xml

请求进来之后的流向大致是这样:

css 复制代码
客户端请求
   ↓
OrderController(REST 接口)
   ↓
OrderService(业务逻辑)
   ↓
OrderMapper(MyBatis-Plus)
   ↓
ShardingSphere-JDBC(解析 → 路由 → 改写 → 执行 → 归并)
   ↓
写操作 → 主库 write_ds          读操作 → 从库 read_ds_0
   ↓                                ↓
        t_order_0  |  t_order_1(物理分片表)

三、先把环境铺好

需要准备的东西不多,但有几个地方容易忽略(来自Shardingsphere官网):

  • Shardingsphere不处理主库和从库的数据同步
  • Shardingsphere不处理主库和从库的数据同步延迟导致的数据不一致
  • Shardingsphere不支持主库多写
  • Shardingsphere不处理主从库间的事务一致性。主从模型中,事务中的数据读写均用主库。

这些要自己处理。

示例里的约定:

  • 主库(写):localhost:3308/order_db_0
  • 从库(读):localhost:3306/order_db_0

两个库里都要有一样的表结构:

sql 复制代码
CREATE DATABASE IF NOT EXISTS order_db_0;
USE order_db_0;
​
CREATE TABLE t_order_0 (
    id BIGINT PRIMARY KEY COMMENT '订单ID(分布式ID)',
    order_no VARCHAR(64) NOT NULL COMMENT '订单编号',
    user_id BIGINT NOT NULL COMMENT '用户ID(分片键)',
    goods_name VARCHAR(128) COMMENT '商品名称',
    order_amount DECIMAL(10,2) COMMENT '订单金额',
    create_time DATETIME COMMENT '创建时间',
    INDEX idx_user_id (user_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单表0';
​
CREATE TABLE t_order_1 (
    id BIGINT PRIMARY KEY COMMENT '订单ID(分布式ID)',
    order_no VARCHAR(64) NOT NULL COMMENT '订单编号',
    user_id BIGINT NOT NULL COMMENT '用户ID(分片键)',
    goods_name VARCHAR(128) COMMENT '商品名称',
    order_amount DECIMAL(10,2) COMMENT '订单金额',
    create_time DATETIME COMMENT '创建时间',
    INDEX idx_user_id (user_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单表1';

从库的表结构要和主库保持一致,生产上靠 MySQL 主从复制自动同步,本地演示也可以手动建一份。


四、配置逐项拆解

真正决定行为的是 sharding-ms.yaml。我拆得细一点,因为读写分离和分片混在一个文件里,缩进和层级写错是最常见的问题,5.x 的配置比 4.x 严格不少,错一个缩进直接启动失败。

4.1 数据源:谁写谁读

yaml 复制代码
dataSources:
  write_ds:  # 写数据源(主库)
    dataSourceClassName: com.zaxxer.hikari.HikariDataSource
    driverClassName: com.mysql.cj.jdbc.Driver
    jdbcUrl: jdbc:mysql://localhost:3308/order_db_0?serverTimezone=UTC&useSSL=false&useUnicode=true&characterEncoding=UTF-8
    username: root
    password: 你的密码
​
  read_ds_0:  # 读数据源(从库)
    dataSourceClassName: com.zaxxer.hikari.HikariDataSource
    driverClassName: com.mysql.cj.jdbc.Driver
    jdbcUrl: jdbc:mysql://localhost:3306/order_db_0?serverTimezone=UTC&useSSL=false&useUnicode=true&characterEncoding=UTF-8
    username: root
    password: 你的密码

这里定义了物理上真实存在的两个连接池。write_ds 指向 3308 的主库,read_ds_0 指向 3306 的从库。名字是自己起的,但后面的规则里要引用,所以起名字时保持语义清楚。

4.2 运行模式

yaml 复制代码
mode:
  type: Standalone  # 单机模式
  repository:
    type: JDBC      # 用 JDBC 持久化元数据

单机模式表示配置只存在本地,没有接配置中心。多人协作或需要动态改规则的场景,可以换成 Cluster 模式接 ZooKeeper 或 Nacos。repository 决定元数据存在哪,JDBC 表示存在本地库表里。

4.3 读写分离规则

yaml 复制代码
rules:
  - !READWRITE_SPLITTING
    dataSourceGroups:
      readwrite_ds:
        writeDataSourceName: write_ds
        readDataSourceNames:
          - read_ds_0
        transactionalReadQueryStrategy: PRIMARY  # 事务中强制读主库
        loadBalancerName: random
    loadBalancers:
      random:
        type: RANDOM  # 随机负载均衡

它是读写分离的核心,逐项说:

  • dataSourceGroups 是一个逻辑数据源分组,组名 readwrite_ds 可以随便起。注意 5.5.x 用 dataSourceGroups 包一层,早期 5.3 之前的写法是直接把 dataSources 放规则下面,两者不通用。
  • writeDataSourceName 指定写走到哪个物理数据源,这里就是 write_ds。
  • readDataSourceNames 是个列表,可以放多个从库。只写一个就是单从库,加两个 ShardingSphere 会自动在它们之间做负载均衡。
  • transactionalReadQueryStrategy: PRIMARY 是个很关键的开关。它规定:只要当前处于事务中,事务内的 SELECT 也强制走主库。不设这一段,就会出现"刚写入马上查不到"的情况------写进了主库,查询却从从库读,而从库还没同步过来。
  • loadBalancerName: random 引用下面定义的负载均衡算法。

loadBalancers 里 type: RANDOM 表示随机挑一个从库。可选还有 ROUND_ROBIN(轮询)等策略。单从库时这一步其实没有实际影响。

4.4 分片规则

yaml 复制代码
  - !SHARDING
    tables:
      t_order:
        actualDataNodes: readwrite_ds.t_order_${0..1}  # 逻辑表映射到2张物理表
        tableStrategy:
          standard:
            shardingColumn: user_id                     # 分片键
            shardingAlgorithmName: t_order_inline
    bindingTables:
      - t_order
    shardingAlgorithms:
      t_order_inline:
        type: INLINE
        props:
          algorithm-expression: t_order_${user_id % 2}

几个概念别混了:

  • 逻辑表 t_order:业务代码里就写这个名字,像一张普通表一样用。物理上它并不存在。
  • actualDataNodes :逻辑表到底对应哪些物理表。readwrite_ds.t_order_${0..1} 里的 ${0..1} 是行表达式,展开后就是 readwrite_ds.t_order_0 和 readwrite_ds.t_order_1。前面的 readwrite_ds 是数据源组的名字,不是物理数据源名------这一步容易写错,写成 write_ds 就找不到了。
  • tableStrategy.standard :标准分片策略,针对单个分片键。shardingColumn: user_id 指明分片键是谁。
  • shardingAlgorithmName:指向下面具体的算法。
  • bindingTables:绑定表配置。把有关联关系的表放在一起(比如订单表和订单明细表都按同一个键分片),避免关联查询时出现笛卡尔积。这里只有一张表,单列出来是留位置。
  • shardingAlgorithms :算法本体。type: INLINE 是行表达式分片算法,algorithm-expression: t_order_${user_id % 2} 表示用 user_id 对 2 取模决定去哪个表。

路由规则:

  • user_id % 2 == 0,数据落到 t_order_0
  • user_id % 2 == 1,数据落到 t_order_1

比如 user_id = 1000 进 t_order_0,user_id = 1001 进 t_order_1。行表达式底层是 Groovy 语法,除了取模,还能写范围 ${0..3}、枚举 ${['a','b']} 这类写法。

4.5 Spring Boot 侧的配置

application.yml 里两件事,一个是让应用走 ShardingSphere 驱动,一个是 MyBatis-Plus 的常规配置:

yaml 复制代码
spring:
  datasource:
    driver-class-name: org.apache.shardingsphere.driver.ShardingSphereDriver
    url: jdbc:shardingsphere:classpath:sharding-ms.yaml
​
mybatis-plus:
  mapper-locations: classpath:mapper/*.xml
  type-aliases-package: com.song.sharding.sphere.mapper
  configuration:
    map-underscore-to-camel-case: true  # 下划线转驼峰

数据源交给 ShardingSphere 驱动接管,URL 用 jdbc:shardingsphere: 前缀指向上面的规则文件。这样 Spring Boot 里就不会再单独配 MySQL 的 URL 和账号了,连接的控制权全部交给 ShardingSphere。

map-underscore-to-camel-case: true 负责把数据库的 user_id、create_time 这类下划线字段自动映射到 Java 的驼峰属性,省掉一堆 @Results 注解。

调试的时候可以把 SQL 日志打开,在sharding-ms.yaml里配置:

sql 复制代码
props:
  sql-show: true

打开后控制台会打印实际执行的 SQL 和真实表名,验证路由是不是按预期走。


五、核心代码怎么写

配置负责"怎么路由",代码负责"用起来像普通单库一样"。我喜欢用 ShardingSphere-JDBC的地方:业务代码对分片基本无感知。

5.1 pom.xml

xml 复制代码
<?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 http://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.5.16</version>
        <relativePath/>
    </parent>
    <groupId>com.song</groupId>
    <artifactId>sharding-sphere-master-slave</artifactId>
    <version>1.0-SNAPSHOT</version>
​
    <name>sharding-sphere-master-slave</name>
​
    <description>SpringBoot3+ShardingSphere-JDBC5读写分离实战</description>
​
    <properties>
        <java.version>17</java.version>
        <shardingsphere.version>5.5.2</shardingsphere.version>
        <mybatis-plus.version>3.5.15</mybatis-plus.version>
    </properties>
​
    <dependencies>
        <!-- SpringBoot Web 基础依赖 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
​
        <!-- shardingsphere-jdbc -->
        <dependency>
            <groupId>org.apache.shardingsphere</groupId>
            <artifactId>shardingsphere-jdbc</artifactId>
            <scope>compile</scope>
            <version>${shardingsphere.version}</version>
        </dependency>
​
        <!-- MySQL8 官方驱动 -->
        <dependency>
            <groupId>com.mysql</groupId>
            <artifactId>mysql-connector-j</artifactId>
            <scope>runtime</scope>
        </dependency>
​
        <!-- MyBatis-Plus 简化CRUD操作 -->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-spring-boot3-starter</artifactId>
            <version>${mybatis-plus.version}</version>
        </dependency>
        <!-- 分页插件 -->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-jsqlparser</artifactId>
            <version>${mybatis-plus.version}</version>
            <scope>compile</scope>
        </dependency>
​
        <!-- Lombok 简化实体类代码 -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </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>

5.2 实体类和主键策略

kotlin 复制代码
@Data
@TableName("t_order")
public class Order {
    @TableId(type = IdType.ASSIGN_ID)  // 雪花算法生成分布式ID
    private Long id;
​
    private String orderNo;
    private Long userId;               // 分片键
    private String goodsName;
    private BigDecimal orderAmount;
    private LocalDateTime createTime;
}

两个点值得说。

第一,@TableName("t_order") 写的是逻辑表名 ,不是 t_order_0 或 t_order_1。物理表名由 ShardingSphere 在改写阶段替换,业务代码看不见。

第二,@TableId(type = IdType.ASSIGN_ID) 指定用雪花算法生成主键。MyBatis-Plus 3.3.0 以后默认就是雪花算法,显式写出来是为了让意图清楚,也避免全局配置被改后行为跑偏。雪花算法生成的是 19 位的 Long 型 ID,趋势递增,全局唯一,正好补上分表之后自增主键失效这个洞。

5.3 Mapper、Service 和 Controller

Mapper , Service 简单到没什么可写的:

scala 复制代码
@Mapper
public interface OrderMapper extends BaseMapper<Order> {
}
​
@Service
public class OrderService extends ServiceImpl<OrderMapper, Order> {
}

Controller 就是一层转发,把 URL 参数接进来:

less 复制代码
@RestController
@RequestMapping("/order")
@RequiredArgsConstructor
public class OrderController {
    private final OrderService orderService;
​
    @GetMapping("/add")     // 新增订单
    public String add(@RequestParam Long userId) { /* ... */ }
​
    @GetMapping("/listByUserId")   // 携带分片键 → 精准路由
    public Object listByUserId(@RequestParam("userId") Long userId) { /* ... */ }
​
    @GetMapping("/list")   // 不带分片键 → 广播查询
    public Object list() { /* ... */ }
​
    @GetMapping("/page")
    public Object pageOrders(
        @RequestParam(value = "pageNum", defaultValue = "1") Integer pageNum,
        @RequestParam(value = "pageSize", defaultValue = "10") Integer pageSize,
        @RequestParam(value = "userId", required = false) Long userId)  { /* ... */ }
    
    @GetMapping("/batchAdd")
    public String batchAddOrders() { /* ... */ }
}

5.4 分页插件别忘了配

MyBatis-Plus 的分页靠拦截器实现,不配它,selectPage 出来的还是全量数据:

java 复制代码
@Configuration
public class MybatisPlusConfig {
​
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
        return interceptor;
    }
}

六、一条 SQL 进来之后发生了什么

配置和代码都摆好了,但真正理解这套东西,最好跟着一条 SQL 走一遍。看日志记录:

yaml 复制代码
2026-10-30T20:58:42.985+08:00  INFO 2856 --- [nio-8080-exec-3] ShardingSphere-SQL                       : Logic SQL: SELECT  id,order_no,user_id,goods_name,order_amount,create_time  FROM t_order      WHERE  (user_id = ?)
2026-10-30T20:58:42.985+08:00  INFO 2856 --- [nio-8080-exec-3] ShardingSphere-SQL                       : Actual SQL: read_ds_0 ::: SELECT  id,order_no,user_id,goods_name,order_amount,create_time  FROM t_order_1      WHERE  (user_id = ?) ::: [1001]

它进到 ShardingSphere 之后,会依次经过五步:

解析。 SQL 先被解析成抽象语法树,提取出表名、操作类型、WHERE 条件里的字段和值。比如看到 WHERE user_id = 1001,就能拿到分片键 user_id 和它的值 1001。

路由。 这一步决定 SQL 去哪个(些)库、哪张表。判断逻辑是两条线并行看的:先看操作类型定读写,INSERT / UPDATE / DELETE 和带 FOR UPDATE、SELECT ... INTO 的强一致读,直接绑主库;普通 SELECT 按负载均衡算法挑一个从库。再看有没有分片键,带上分片键的查询能精确定位到某一张表,没带就会路由到所有分片。

改写。 逻辑表名在这里被替换成物理表名。t_order 加上 user_id = 1001 这个条件,改写后就变成 t_order_1。分页查询在这一步还会做特殊处理,去各个分片捞够数据再合并。

执行。 改写后的 SQL 交给 HikariCP 执行。多分片的查询是并发发出去的,不会一个分片接一个分片地等。

归并。 各分片返回结果后,框架在内存里做二次处理:排序、去重、分页裁剪、聚合计算。比如 ORDER BY create_time 跨了两个分片,就要把两边结果合起来重新排序,再取前 N 条。

把这几步和示例对应起来看会更清楚。调用 /order/listByUserId?userId=1001,带了分片键,路由能一步定位到 t_order_1,只在从库上查这一张表,效率最高。改成 /order/list,没有分片键,SQL 会被广播到 t_order_0 和 t_order_1,两边各查一半再合并------功能没问题,但代价成倍上升。

关于事务这块,有个容易忽略的细节。ShardingSphere 会监听 Spring 的事务上下文,只要当前线程被 @Transactional 包住,路由上下文就会锁死在主库。换句话说,同一个事务里哪怕写的是 SELECT,走的也是主库。这正是前面配置里 transactionalReadQueryStrategy: PRIMARY 在实际运行中发挥的作用,它从机制上堵住了"写完立刻读不到"这类脏读问题。这个设计对读写分离场景相当重要,没有它,凡是"下单后立刻查订单"这种流程都得额外加保险。


七、跑起来该看到什么

先把项目运行起来:

bash 复制代码
mvn clean package -DskipTests
java -jar target/sharding-sphere-master-slave-1.0-SNAPSHOT.jar

或者直接在 IDEA 里跑 MSApplication.java。服务起来后监听 8080,看日志:

yaml 复制代码
........
........
  .   ____          _            __ _ _
 /\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )___ | '_ | '_| | '_ / _` | \ \ \ \
 \/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |___, | / / / /
 =========|_|==============|___/=/_/_/_/
​
 :: Spring Boot ::               (v3.5.16)
​
.......
.......
​
2026-10-30T20:59:53.440+08:00  INFO 5044 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat initialized with port 8080 (http)
2026-10-30T20:59:53.452+08:00  INFO 5044 --- [           main] o.apache.catalina.core.StandardService   : Starting service [Tomcat]
2026-10-30T20:59:53.452+08:00  INFO 5044 --- [           main] o.apache.catalina.core.StandardEngine    : Starting Servlet engine: [Apache Tomcat/10.1.55]
2026-10-30T20:59:53.498+08:00  INFO 5044 --- [           main] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring embedded WebApplicationContext
2026-10-30T20:59:53.498+08:00  INFO 5044 --- [           main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 800 ms
 _ _   |_  _ _|_. ___ _ |    _ 
| | |/|_)(_| | |_\  |_)||_|_\ 
     /               |         
                        3.5.15 
2026-10-30T20:59:54.085+08:00  INFO 5044 --- [           main] m.e.s.MybatisPlusApplicationContextAware : Register ApplicationContext instances org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext@74cadd41
2026-10-30T20:59:54.164+08:00  INFO 5044 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port 8080 (http) with context path '/'
2026-10-30T20:59:54.171+08:00  INFO 5044 --- [           main] com.song.sharding.sphere.MSApplication   : Started MSApplication in 2.131 seconds (process running for 2.862)

7.1 验证分片是不是真的生效

连加两笔订单,让分片键分别是偶数和奇数:

bash 复制代码
# user_id=1000,1000 % 2 = 0,数据落到 t_order_0
curl "http://localhost:8080/order/add?userId=1000"

日志:

ruby 复制代码
2026-10-30T21:01:27.987+08:00  INFO 5044 --- [nio-8080-exec-1] ShardingSphere-SQL                       : Logic SQL: INSERT INTO t_order  ( id, order_no, user_id, goods_name, order_amount, create_time )  VALUES (  ?, ?, ?, ?, ?, ?  )
2026-10-30T21:01:27.987+08:00  INFO 5044 --- [nio-8080-exec-1] ShardingSphere-SQL                       : Actual SQL: write_ds ::: INSERT INTO t_order_0  ( id, order_no, user_id, goods_name, order_amount, create_time )  VALUES (?, ?, ?, ?, ?, ?) ::: [2116153499200319490, b4ca58b8-3b29-4b34-858c-2929d958d756, 1000, 实战测试商品, 99.99, 2026-10-30T21:01:25.049747]
bash 复制代码
# user_id=1001,1001 % 2 = 1,数据落到 t_order_1
curl "http://localhost:8080/order/add?userId=1001"

日志:

ruby 复制代码
2026-10-30T21:02:31.629+08:00  INFO 5044 --- [nio-8080-exec-2] ShardingSphere-SQL                       : Logic SQL: INSERT INTO t_order  ( id, order_no, user_id, goods_name, order_amount, create_time )  VALUES (  ?, ?, ?, ?, ?, ?  )
2026-10-30T21:02:31.629+08:00  INFO 5044 --- [nio-8080-exec-2] ShardingSphere-SQL                       : Actual SQL: write_ds ::: INSERT INTO t_order_1  ( id, order_no, user_id, goods_name, order_amount, create_time )  VALUES (?, ?, ?, ?, ?, ?) ::: [2116153778213810178, 6eb2ec21-1431-468c-981a-b93e973d1a86, 1001, 实战测试商品, 99.99, 2026-10-30T21:02:31.624313700]

然后到数据库里对一下账:

sql 复制代码
SELECT * FROM t_order_0;  -- 应该只有 user_id 为偶数的记录
SELECT * FROM t_order_1;  -- 应该只有 user_id 为奇数的记录

两张表各有各的数据,说明取模分片按预期生效了。

7.2 验证读写分离

把 SQL 日志打开(就是前面那行 props: sql-show: true 配置),连续调几次查询和写入接口,盯控制台输出的 SQL。写操作的日志打在主库连接上,查询的日志打在从库连接上,两者分开出现,说明读写已经分到了不同的数据源。

vbnet 复制代码
2026-10-30T21:03:40.386+08:00  INFO 5044 --- [nio-8080-exec-3] ShardingSphere-SQL                       : Logic SQL: SELECT COUNT(*) AS total FROM t_order WHERE (user_id = ?)
2026-10-30T21:03:40.386+08:00  INFO 5044 --- [nio-8080-exec-3] ShardingSphere-SQL                       : Actual SQL: read_ds_0 ::: SELECT COUNT(*) AS total FROM t_order_0 WHERE (user_id = ?) ::: [1000]
2026-10-30T21:03:40.445+08:00  INFO 5044 --- [nio-8080-exec-3] ShardingSphere-SQL                       : Logic SQL: SELECT  id,order_no,user_id,goods_name,order_amount,create_time  FROM t_order      WHERE  (user_id = ?) LIMIT ?
2026-10-30T21:03:40.445+08:00  INFO 5044 --- [nio-8080-exec-3] ShardingSphere-SQL                       : Actual SQL: read_ds_0 ::: SELECT  id,order_no,user_id,goods_name,order_amount,create_time  FROM t_order_0      WHERE  (user_id = ?) LIMIT ? ::: [1000, 10]

要观察得更直观,可以在从库上故意停掉主从同步,再调一次写入后立刻查询------如果查不到刚写的数据,说明读确实走了从库;而如果在事务里做同样的操作又能查到,就印证了事务内强制读主库这条规则在起作用。

7.3 批量数据

arduino 复制代码
curl "http://localhost:8080/order/batchAdd"

这个接口一次生成 1000 条订单,user_id 从 1 递增。因为分片键连续取值,取模之后数据会均匀分到两张表里,每张大约 500 条。跑完再去 SELECT COUNT(*) 两张表,能直接看出数据分布是不是均衡------这也是验证分片键选得好不好的一个土办法,如果业务里分片键分布严重倾斜,就该考虑换个算法了。

7.4 分页

arduino 复制代码
curl "http://localhost:8080/order/page?pageNum=1&pageSize=10&userId=1001"

带上 userId 时,查询能定位到单个分片,分页由该分片自己完成,速度快。把 userId 去掉再调一次,就会变成跨分片分页:两个分片各取一部分,再在内存里归并裁剪。数据量小的时候感觉不出来,量一大就能明显看到两者之间的差距。


八、几个真会踩的坑

都是这套配置里最容易出问题的地方。

物理表必须手工建。 ShardingSphere 只负责路由,不会帮你 CREATE TABLE。actualDataNodes 里写了 t_order_0 和 t_order_1,这两张表就得先在库里建好,缺一张,跑到那个分片就报错。

主从同步是前提。 读写分离生效的前提是从库的数据和主库一致,这个一致性靠 MySQL 主从复制来保证。示例本身不管同步,同步没配好,读出来的就是旧数据。刚写完就查不到的现象,八成是同步延迟。

分片键写错的代价很大。 不写 user_id 的查询会路由到所有分片。列表、统计、后台导出这类查询最容易犯这个毛病。带分片键的查询只在单表上跑,不带分片键的查询要在全部分片上跑一遍再合并,性能差距是数量级的。

跨分片分页要小心。 分页查询在没有分片键时特别费劲,ShardingSphere 得从每个分片都捞数据,再在内存里排序裁剪。数据量大的时候,页码越往后翻越慢。这类查询要么想办法带上分片键,要么干脆另建一张专门用于展示的宽表。

分布式 ID 不能省。 分表之后自增主键必然重复,实体上那个 IdType.ASSIGN_ID 不是装饰,去掉就是主键冲突。

跨分片事务不在本示例范围内。 单分片内的事务 ShardingSphere 能正常处理,一旦一次写操作涉及多个分片,就需要 XA 这类分布式事务的支持,示例没有涉及,实际业务里得单独设计。


九、常见问题

问:怎么确认读写分离到底有没有生效? 打开 SQL 日志,观察控制台打印的 SQL。读和写会打在不同的数据源连接上,看日志。

yaml 复制代码
2026-10-30T21:01:27.987+08:00  INFO 5044 --- [nio-8080-exec-1] ShardingSphere-SQL                       : Logic SQL: INSERT INTO t_order  ( id, order_no, user_id, goods_name, order_amount, create_time )  VALUES (  ?, ?, ?, ?, ?, ?  )
2026-10-30T21:01:27.987+08:00  INFO 5044 --- [nio-8080-exec-1] ShardingSphere-SQL                       : Actual SQL: write_ds ::: INSERT INTO t_order_0  ( id, order_no, user_id, goods_name, order_amount, create_time )  VALUES (?, ?, ?, ?, ?, ?) ::: [2116153499200319490, b4ca58b8-3b29-4b34-858c-2929d958d756, 1000, 实战测试商品, 99.99, 2026-10-30T21:01:25.049747]
.......
2026-10-30T21:03:40.386+08:00  INFO 5044 --- [nio-8080-exec-3] ShardingSphere-SQL                       : Logic SQL: SELECT COUNT(*) AS total FROM t_order WHERE (user_id = ?)
2026-10-30T21:03:40.386+08:00  INFO 5044 --- [nio-8080-exec-3] ShardingSphere-SQL                       : Actual SQL: read_ds_0 ::: SELECT COUNT(*) AS total FROM t_order_0 WHERE (user_id = ?) ::: [1000]
2026-10-30T21:03:40.445+08:00  INFO 5044 --- [nio-8080-exec-3] ShardingSphere-SQL                       : Logic SQL: SELECT  id,order_no,user_id,goods_name,order_amount,create_time  FROM t_order      WHERE  (user_id = ?) LIMIT ?
2026-10-30T21:03:40.445+08:00  INFO 5044 --- [nio-8080-exec-3] ShardingSphere-SQL                       : Actual SQL: read_ds_0 ::: SELECT  id,order_no,user_id,goods_name,order_amount,create_time  FROM t_order_0      WHERE  (user_id = ?) LIMIT ? ::: [1000, 10]

问:为什么插入之后马上查不到? 大概率是主从同步延迟。事务内查询会强制走主库;或者开启并行复制(MTS)、排查从库负载和大事务。

问:想加更多分片怎么办? 三步。先在两个库里把新表建出来,比如 t_order_2、t_order_3;再把配置里的 actualDataNodes 改成 readwrite_ds.t_order_${0..3};最后把分片算法改成 t_order_${user_id % 4}。必须同时做存量数据迁移/再均衡。

问:多个从库怎么加? 在 readDataSourceNames 下面继续往下排就行:

markdown 复制代码
readDataSourceNames:
  - read_ds_0
  - read_ds_1
  - read_ds_2

加完之后,loadBalancers 里配的随机策略就会在三个从库之间分发请求。


十、源代码

完整代码已开源:sharding-sphere-master-slave(Gitee)

转载请注明作者与原文链接

参考

相关推荐
泡海椒1 小时前
04-饼图评级构成-信用评级构成
后端
知守观1 小时前
多模块架构演进:横向分层 vs 纵向领域,13 个真实项目的切分对照
java·后端·微服务
系统集成-鹰翔2 小时前
ERP推单数据库怎么备份:SQLite WAL模式下只复制db文件为什么可能漏任务
javascript·数据库·经验分享·后端
软件技术NINI3 小时前
基于SpringBoot和Vue的摇滚乐鉴赏网站
vue.js·spring boot·后端·spring·html·毕业设计·毕设
Tsuki_tl3 小时前
【总结】Redis五大数据类型应用场景及基本命令
数据库·redis·后端·缓存·缓存中间件·redis命令·redis 五大数据类型
后台模板学习3 小时前
Go Web开发中任务调度框架的源码解析与高可用实现方案
开发语言·后端·golang
系统集成-鹰翔3 小时前
ERP对接升级提示数据库迁移失败:SQLite逐版本事务到底回滚到哪一步
javascript·数据库·经验分享·后端
IT_陈寒3 小时前
深入理解SpringBoot:核心原理与最佳实践
前端·人工智能·后端
学长毕设3 小时前
基于SpringBoot的校园礼服租赁系统的设计与实现
java·spring boot·后端