深入剖析 Sharding-JDBC:分库分表的轻量级利器

目录

【理论知识】

一、如何解决高并发和数据量大的问题

二、基本概念

2.1 真实表

2.2 数据节点

2.3 绑定表

2.4 广播表

2.5 分片键

2.6 分片算法

2.7 分片策略

2.8 自增主键生成策略

2.9 SQL解析

2.10 SQL路由

2.11 标准路由

2.12 笛卡尔路由

2.13 SQL改写

2.14 SQL执行

2.14.1 内存限制模式

2.14.2 连接限制模式

2.17 结果归并

2.18 内存归并

2.19 流式归并 

三、核心定位 

四、架构全景:从单库到分布式的平滑演进

零侵入式设计

五、核心能力:四大场景一次掌握

5.1 分库分表(Sharding)

5.2 读写分离(Master-Slave)

5.3 弹性伸缩(Elastic)

5.4 影子库(Shadow)

【项目实战】

1. pom.xml 添加依赖

2. application.yml 配置

3. 项目结构

4. OrderMapper.xml

5. 实体层代码

6. 数据访问层代码

7. 业务层代码

8. 业务实现层代码

9. 控制层代码

10.自动生成表

11. 测试

12. 查看日志

13. 测试查询订单列表数据

14. 日志

15. 性能与一致性:如何平衡?

性能优化

事务一致性

16. 避坑指南:常见误区与解决方案

写在最后的话


【理论知识】

一、如何解决高并发和数据量大的问题

  1. 高并发情况下:会造成IO读写频繁,自然就会造成读写缓慢,甚至是宕机。一般单库不要超过2k并发,除非加大服务器配置;

  2. 数据量大的问题:主要由于底层索引实现导致,MySQL的索引实现为B+TREE,索引与数据都在一个文件中,会导致索引树十分庞大,造成查询缓慢;

解决

将一个表拆成N个表,就是让每个表的数据量控制在一定范围内,保证SQL的性能。 一个表数据建议不要超过500W。

二、基本概念

在了解Sharding-JDBC的执行原理前,需要了解以下概念:

逻辑表

水平拆分的数据表的总称。例:订单数据表根据主键尾数拆分为10张表,分别是 t_order_0 、 t_order_1 到t_order_9 ,他们的逻辑表名为 t_order 。

2.1 真实表

在分片的数据库中真实存在的物理表。即上个示例中的 t_order_0 到 t_order_9 。

2.2 数据节点

数据分片的最小物理单元。由数据源名称和数据表组成,例: ds_0.t_order_0 。

2.3 绑定表

指分片规则一致的主表和子表。例如: t_order 表和 t_order_item 表,均按照 order_id 分片,绑定表之间的分区键完全相同,则此两张表互为绑定表关系。

绑定表之间的多表关联查询不会出现笛卡尔积关联,关联查询效率将大大提升。举例说明,如果SQL为:

SELECT i.* FROM t_order o JOIN t_order_item i ON o.order_id=i.order_id WHERE o.order_id in (10,11);

在不配置绑定表关系时,假设分片键 order_id 将数值10路由至第0片,将数值11路由至第1片,那么路由后的SQL应该为4条,它们呈现为笛卡尔积:

SELECT i.* FROM t_order_0 o JOIN t_order_item_0 i ON o.order_id=i.order_id WHERE o.order_id in (10, 11);
SELECT i.* FROM t_order_0 o JOIN t_order_item_1 i ON o.order_id=i.order_id WHERE o.order_id in (10, 11);
SELECT i.* FROM t_order_1 o JOIN t_order_item_0 i ON o.order_id=i.order_id WHERE o.order_id in (10, 11);
SELECT i.* FROM t_order_1 o JOIN t_order_item_1 i ON o.order_id=i.order_id WHERE o.order_id in (10, 11);

在配置绑定表关系后,路由的SQL应该为2条:

SELECT i.* FROM t_order_0 o JOIN t_order_item_0 i ON o.order_id=i.order_id WHERE o.order_id in (10, 11);
SELECT i.* FROM t_order_1 o JOIN t_order_item_1 i ON o.order_id=i.order_id WHERE o.order_id in (10, 11);

2.4 广播表

指所有的分片数据源中都存在的表,表结构和表中的数据在每个数据库中均完全一致。适用于数据量不大且需要与海量数据的表进行关联查询的场景,例如:字典表。

2.5 分片键

用于分片的数据库字段,是将数据库(表)水平拆分的关键字段。例:将订单表中的订单主键的尾数取模分片,则订单主键为分片字段。 SQL中如果无分片字段,将执行全路由,性能较差。 除了对单分片字段的支持,ShardingJdbc也支持根据多个字段进行分片。

2.6 分片算法

通过分片算法将数据分片,支持通过 = 、 BETWEEN 和 IN 分片。分片算法需要应用方开发者自行实现,可实现的灵活度非常高。包括:精确分片算法 、范围分片算法 ,复合分片算法 等。例如:where order_id = ? 将采用精确分片算法,where order_id in (?,?,?)将采用精确分片算法,where order_id BETWEEN ? and ? 将采用范围分片算法,复合分片算法用于分片键有多个复杂情况。

2.7 分片策略

包含分片键和分片算法,由于分片算法的独立性,将其独立抽离。真正可用于分片操作的是分片键 + 分片算法,也就是分片策略。内置的分片策略大致可分为尾数取模、哈希、范围、标签、时间等。由用户方配置的分片策略则更加灵活,常用的使用行表达式配置分片策略,它采用Groovy表达式表示,如: t_user_$->{u_id % 8} 表示t_user表根据u_id模8,而分成8张表,表名称为 t_user_0 到 t_user_7 。

2.8 自增主键生成策略

通过在客户端生成自增主键替换以数据库原生自增主键的方式,做到分布式主键无重复。

2.9 SQL解析

当Sharding-JDBC接受到一条SQL语句时,会陆续执行 SQL解析 => 查询优化 => SQL路由 => SQL改写 => SQL执行 =>结果归并 ,最终返回执行结果。

SQL解析过程分为词法解析和语法解析。 词法解析器用于将SQL拆解为不可再分的原子符号,称为Token。并根据不同数据库方言所提供的字典,将其归类为关键字,表达式,字面量和操作符。 再使用语法解析器将SQL转换为抽象语法树。

例如,以下SQL:

SELECT id, name FROM t_user WHERE status = 'ACTIVE' AND age > 18

解析之后的为抽象语法树见下图:

为了便于理解,抽象语法树中的关键字的Token用绿色表示,变量的Token用红色表示,灰色表示需要进一步拆分。最后,通过对抽象语法树的遍历去提炼分片所需的上下文,并标记有可能需要SQL改写(后边介绍)的位置。 供分片使用的解析上下文包含查询选择项(Select Items)、表信息(Table)、分片条件(Sharding Condition)、自增主键信息(Auto increment Primary Key)、排序信息(Order By)、分组信息(Group By)以及分页信息(Limit、Rownum、Top)。

2.10 SQL路由

SQL路由就是把针对逻辑表的数据操作映射到对数据结点操作的过程。

根据解析上下文匹配数据库和表的分片策略,并生成路由路径。 对于携带分片键的SQL,根据分片键操作符不同可以划分为单片路由(分片键的操作符是等号)、多片路由(分片键的操作符是IN)和范围路由(分片键的操作符是BETWEEN),不携带分片键的SQL则采用广播路由。根据分片键进行路由的场景可分为直接路由、标准路由、笛卡尔路由等。

2.11 标准路由

标准路由是Sharding-Jdbc最为推荐使用的分片方式,它的适用范围是不包含关联查询或仅包含绑定表之间关联查询的SQL。 当分片运算符是等于号时,路由结果将落入单库(表),当分片运算符是BETWEEN或IN时,则路由结果不一定落入唯一的库(表),因此一条逻辑SQL最终可能被拆分为多条用于执行的真实SQL。 举例说明,如果按照 order_id 的奇数和偶数进行数据分片,一个单表查询的SQL如下:

SELECT * FROM t_order WHERE order_id IN (1, 2);

那么路由的结果应为:

SELECT * FROM t_order_0 WHERE order_id IN (1, 2);
SELECT * FROM t_order_1 WHERE order_id IN (1, 2);

绑定表的关联查询与单表查询复杂度和性能相当。举例说明,如果一个包含绑定表的关联查询的 SQL如下:

SELECT * FROM t_order o JOIN t_order_item i ON o.order_id=i.order_id  WHERE order_id IN (1, 2);

那么路由的结果应为:

SELECT * FROM t_order_0 o JOIN t_order_item_0 i ON o.order_id=i.order_id  WHERE order_id IN (1,2);
SELECT * FROM t_order_1 o JOIN t_order_item_1 i ON o.order_id=i.order_id  WHERE order_id IN (1,2);

可以看到,SQL拆分的数目与单表是一致的

2.12 笛卡尔路由

笛卡尔路由是最复杂的情况,它无法根据绑定表的关系定位分片规则,因此非绑定表之间的关联查询需要拆解为笛

卡尔积组合执行。 如果上个示例中的SQL并未配置绑定表关系,那么路由的结果应为:

SELECT * FROM t_order_0 o JOIN t_order_item_0 i ON o.order_id=i.order_id  WHERE order_id IN (1,2);
SELECT * FROM t_order_0 o JOIN t_order_item_1 i ON o.order_id=i.order_id  WHERE order_id IN (1,2);
SELECT * FROM t_order_1 o JOIN t_order_item_0 i ON o.order_id=i.order_id  WHERE order_id IN (1,2);
SELECT * FROM t_order_1 o JOIN t_order_item_1 i ON o.order_id=i.order_id  WHERE order_id IN (1,2);

笛卡尔路由查询性能较低,需谨慎使用。

2.13 全库表路由

对于不携带分片键的SQL,则采取广播路由的方式。根据SQL类型又可以划分为全库表路由、全库路由、全实例路由、单播路由和阻断路由这5种类型。其中全库表路由用于处理对数据库中与其逻辑表相关的所有真实表的操作,主要包括不带分片键的DQL(数据查询)和DML(数据操纵),以及DDL(数据定义)等。例如:

SELECT * FROM t_order WHERE good_prority IN (1, 10);

则会遍历所有数据库中的所有表,逐一匹配逻辑表和真实表名,能够匹配得上则执行。路由后成为

SELECT * FROM t_order_0 WHERE good_prority IN (1, 10);
SELECT * FROM t_order_1 WHERE good_prority IN (1, 10);
SELECT * FROM t_order_2 WHERE good_prority IN (1, 10);
SELECT * FROM t_order_3 WHERE good_prority IN (1, 10);

2.13 SQL改写

工程师面向逻辑表书写的SQL,并不能够直接在真实的数据库中执行,SQL改写用于将逻辑SQL改写为在真实数据库中可以正确执行的SQL。

如一个简单的例子,若逻辑SQL为:

SELECT order_id FROM t_order WHERE order_id=1;

假设该SQL配置分片键order_id,并且order_id=1的情况,将路由至分片表1。那么改写之后的SQL应该为:

SELECT order_id FROM t_order_1 WHERE order_id=1;

再比如,Sharding-JDBC需要在结果归并时获取相应数据,但该数据并未能通过查询的SQL返回。 这种情况主要是针对GROUP BY和ORDER BY。结果归并时,需要根据 GROUP BY 和 ORDER BY 的字段项进行分组和排序,但如果原始SQL的选择项中若并未包含分组项或排序项,则需要对原始SQL进行改写。 先看一下原始SQL中带有结果归并所需信息的场景:

SELECT order_id, user_id FROM t_order ORDER BY user_id;

由于使用user_id进行排序,在结果归并中需要能够获取到user_id的数据,而上面的SQL是能够获取到user_id数据的,因此无需补列。

如果选择项中不包含结果归并时所需的列,则需要进行补列,如以下SQL:

SELECT order_id FROM t_order ORDER BY user_id;

由于原始SQL中并不包含需要在结果归并中需要获取的user_id,因此需要对SQL进行补列改写。补列之后的SQL是:

SELECT order_id, user_id AS ORDER_BY_DERIVED_0 FROM t_order ORDER BY user_id;

2.14 SQL执行

Sharding-JDBC采用一套自动化的执行引擎,负责将路由和改写完成之后的真实SQL安全且高效发送到底层数据源执行。 它不是简单地将SQL通过JDBC直接发送至数据源执行;也并非直接将执行请求放入线程池去并发执行。它更关注平衡数据源连接创建以及内存占用所产生的消耗,以及最大限度地合理利用并发等问题。 执行引擎的目标是自动化的平衡资源控制与执行效率,他能在以下两种模式自适应切换:

2.14.1 内存限制模式

使用此模式的前提是, Sharding-JDBC对一次操作所耗费的数据库连接数量不做限制。 如果实际执行的SQL需要对某数据库实例中的200张表做操作,则对每张表创建一个新的数据库连接,并通过多线程的方式并发处理,以达成执行效率最大化。

2.14.2 连接限制模式

使用此模式的前提是,Sharding-JDBC严格控制对一次操作所耗费的数据库连接数量。 如果实际执行的SQL需要对

某数据库实例中的200张表做操作,那么只会创建唯一的数据库连接,并对其200张表串行处理。 如果一次操作中的分片散落在不同的数据库,仍然采用多线程处理对不同库的操作,但每个库的每次操作仍然只创建一个唯一的数据库连接。

内存限制模式适用于OLAP操作,可以通过放宽对数据库连接的限制提升系统吞吐量; 连接限制模式适用于OLTP操作,OLTP通常带有分片键,会路由到单一的分片,因此严格控制数据库连接,以保证在线系统数据库资源能够被更多的应用所使用,是明智的选择。

2.17 结果归并

将从各个数据节点获取的多数据结果集,组合成为一个结果集并正确的返回至请求客户端,称为结果归并。Sharding-JDBC支持的结果归并从功能上可分为遍历、排序、分组、分页和聚合5种类型,它们是组合而非互斥的关系。归并引擎的整体结构划分如下图。

结果归并从结构划分可分为流式归并、内存归并和装饰者归并。流式归并和内存归并是互斥的,装饰者归并可以在流式归并和内存归并之上做进一步的处理。

2.18 内存归并

将所有分片结果集的数据都遍历并存储在内存中,再通过统一的分组、排序以及聚合等计算之后,再将其封装成为逐条访问的数据结果集返回

2.19 流式归并 

指每一次从数据库结果集中获取到的数据,都能够通过游标逐条获取的方式返回正确的单条数据,它与数据库原生的返回结果集的方式最为契合。

下边举例说明排序归并的过程,如下图是一个通过分数进行排序的示例图,它采用流式归并方式。 图中展示了3张表返回的数据结果集,每个数据结果集已经根据分数排序完毕,但是3个数据结果集之间是无序的。 将3个数据结果集的当前游标指向的数据值进行排序,并放入优先级队列,t_score_0的第一个数据值最大,t_score_2的第一个数据值次之,t_score_1的第一个数据值最小,因此优先级队列根据t_score_0,t_score_2和t_score_1的方式排序队列。

下图则展现了进行next调用的时候,排序归并是如何进行的。 通过图中我们可以看到,当进行第一次next调用时,排在队列首位的t_score_0将会被弹出队列,并且将当前游标指向的数据值(也就是100)返回至查询客户端,并且将游标下移一位之后,重新放入优先级队列。 而优先级队列也会根据t_score_0的当前数据结果集指向游标的数据值(这里是90)进行排序,根据当前数值,t_score_0排列在队列的最后一位。 之前队列中排名第二的t_score_2的数据结果集则自动排在了队列首位。

在进行第二次next时,只需要将目前排列在队列首位的t_score_2弹出队列,并且将其数据结果集游标指向的值返回至客户端,并下移游标,继续加入队列排队,以此类推。 当一个结果集中已经没有数据了,则无需再次加入队列。

可以看到,对于每个数据结果集中的数据有序,而多数据结果集整体无序的情况下,Sharding-JDBC无需将所有的数据都加载至内存即可排序。 它使用的是流式归并的方式,每次next仅获取唯一正确的一条数据,极大的节省了内存的消耗。装饰者归并是对所有的结果集归并进行统一的功能增强,比如归并时需要聚合SUM前,在进行聚合计算前,都会通过内存归并或流式归并查询出结果集。因此,聚合归并是在之前介绍的归并类型之上追加的归并能力,即装饰者模式。

三、核心定位 

Sharding-JDBC 是 Apache ShardingSphere 的轻量级内核,以 jar 包形式嵌入 Java 应用,无需额外部署,直接通过 JDBC 层实现分库分表、读写分离、弹性伸缩等分布式数据库中间件功能。

四、架构全景:从单库到分布式的平滑演进

🚀 零侵入式设计

零侵入: 完全兼容现有 JDBC、MyBatis、JPA 等框架,无需修改业务代码。

零依赖: 仅依赖标准 JDBC 接口,可直接替换现有数据源。

零中心: 无需额外部署中心节点,单节点即服务。

五、核心能力:四大场景一次掌握

5.1 分库分表(Sharding)

通过 水平分片 (按行拆分)和 垂直分片 (按列拆分)解决单表数据量过大、性能瓶颈问题。支持哈希、范围、复杂表达式等多种分片算法。

5.2 读写分离(Master-Slave)

将写操作路由到主库,读操作路由到从库,提升查询性能。支持主从延迟检测与故障转移。

5.3 弹性伸缩(Elastic)

动态调整分片策略,无需重启应用即可实现数据迁移或扩容。

5.4 影子库(Shadow)

基于影子库进行灰度发布、数据回滚、测试数据隔离等场景。

【项目实战】

1. pom.xml 添加依赖

<!-- sharding-jdbc -->
<dependency>
    <groupId>org.apache.shardingsphere</groupId>
    <artifactId>sharding-jdbc-spring-boot-starter</artifactId>
    <version>4.0.0-RC1</version>
</dependency>
<dependency>
    <groupId>org.apache.shardingsphere</groupId>
    <artifactId>sharding-core-common</artifactId>
    <version>4.0.0-RC1</version>
</dependency>
<!-- 改用 Druid 数据源 -->
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid</artifactId>
    <version>1.1.6</version>
</dependency>

数据库驱动需要换成

<dependency>

<groupId>mysql</groupId>

<artifactId>mysql-connector-java</artifactId>

<version>8.0.33</version>

</dependency>

不然和 Sharding-Jdbc 有冲突

2. application.yml 配置

server:
  port: 8098
spring:
  jpa:
    database-platform: org.hibernate.dialect.MySQL5InnoDBDialect
    hibernate:
      ddl-auto: update
      naming-strategy: org.hibernate.cfg.ImprovedNamingStrategy
    show-sql: true
  # 允许覆盖重复定义的 Bean
  main:
    allow-bean-definition-overriding: true

  ###################################################### sharding-jdbc 配置 #################################################################
  shardingsphere:

    # 定义主从数据源
    datasource:
      names: master,slave

      # 主库配置
      master:
        url: jdbc:mysql://127.0.0.1:3307/sjdbc_test?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B8
        username: root
        password: 12345678
        driver-class-name: com.mysql.cj.jdbc.Driver
        type: com.alibaba.druid.pool.DruidDataSource

      # 从库配置
      slave:
        url: jdbc:mysql://127.0.0.1:3308/sjdbc_test?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B8
        username: root
        password: 12345678
        driver-class-name: com.mysql.cj.jdbc.Driver
        type: com.alibaba.druid.pool.DruidDataSource

    # 打印 SQL 语句
    props:
      sql:
        show: true
    sharding:
      # 读写分离配置
      master-slave-rules:
        ds0:
          master-data-source-name: master
          slave-data-source-names: slave
      # 分表 t_order & t_order_item 配置
      tables:
        t_order:
          actual-data-nodes: ds0.t_order_${0..2}
          table-strategy:
            inline:
              algorithm-expression: t_order_${order_id % 4}
              sharding-column: order_id
        t_order_item:
          actual-data-nodes: ds0.t_order_item_${0..2}
          table-strategy:
            inline:
              algorithm-expression: t_order_item_${order_id % 4}
              sharding-column: order_id
        # 指定ID生成策略
        shard:
          key-generator:
            column: order_id
            type: SNOWFLAKE
            props:
              # workerId
              worker:
                id: 1000
              # 最大容忍的时钟回拨毫秒数,雪花算法依据时间戳来生成的,一旦时间戳回拨就会造成 id 重复的可能
              max:
                tolerate:
                  time:
                    difference:
                      millseconds: 5
      # 绑定表关系 / 没有这个绑定关系就会出现笛卡尔积查询
      binding-tables[0]: t_order,t_order_item
      # 配置广播表
      broadcast-tables: t_config
# MyBatis-Plus 配置文件
mybatis-plus:
  mapper-location: classpath:mapper/*.xml

3. 项目结构

4. OrderMapper.xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.springbootweb.dao.OrderDAO">
    <!-- 根据订单号查询订单明细 -->
    <select id="listOrderInfoByOrderSn" resultType="com.example.springbootweb.entity.vo.OrderVO">
        SELECT
            o.order_id AS  orderId,
            o.create_time AS createTime,
            o.order_sn AS orderSn,
            i.price,
            i.num,
            i.goods_id AS goodsId
        FROM t_order o
            LEFT JOIN t_order_item i ON o.order_id = i.order_id
        WHERE o.delete_status = 0
        AND o.order_sn = #{orderSn}
    </select>
</mapper>

5. 实体层代码

// Order.java
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
@Table(appliesTo = "t_order")
@Entity(name = "t_order")
@TableName("t_order")
public class Order implements Serializable {
    @Id
    @TableId(type = IdType.ASSIGN_ID)
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long orderId;
    /**
     * 如果不用 Column 注解,Swagger 配合下面用
     * <p>
     *  <dependency>
     *      <groupId>com.github.biyanwen</groupId>
     *      <artifactId>jpa-comment-spring-boot-starter</artifactId>
     *      <version>1.0.0</version>
     *  </dependency>
     * </p>
     */
    @Column(columnDefinition = "VARCHAR(50) DEFAULT '' COMMENT '订单编号'")
    private String orderSn;
    @Column(columnDefinition = "BIGINT(20) DEFAULT NULL COMMENT '用户ID'")
    private Long userId;
    @Column(columnDefinition = "DECIMAL(10,2) DEFAULT NULL COMMENT '订单价格'")
    private BigDecimal price;
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    @TableField(fill = FieldFill.INSERT_UPDATE)
    @Column(columnDefinition = "DATETIME DEFAULT NULL COMMENT '创建时间'")
    private Date createTime;
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    @TableField(fill = FieldFill.INSERT_UPDATE)
    @Column(columnDefinition = "DATETIME DEFAULT NULL COMMENT '更新时间'")
    private Date updateTime;
    @Column(columnDefinition = "TINYINT(1) DEFAULT 0 COMMENT '删除状态 1是0否'")
    private Integer deleteStatus;
}
// OrderItem.java
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Table(appliesTo = "t_order_item")
@Entity(name = "t_order_item")
@TableName("t_order_item")
public class OrderItem implements Serializable {
    /**
     * TABLE : 使用一个特定的数据库表格来保存主键 (用的比较少)
     * SEQUENCE : 根据底层数据库的序列来生成主键,条件是数据库支持序列 (mysql不支持,其实和auto一样)
     * IDENTITY : 主键由数据库自动生成(主要是自动增长型 & 这个用的比较多)
     * AUTO : 主键由程序控制 (不是自增)
     */
    @Id
    @TableId(type = IdType.ASSIGN_ID)
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long orderItemId;
    @Column(columnDefinition = "BIGINT(20) DEFAULT NULL COMMENT '订单ID'")
    private Long orderId;
    @Column(columnDefinition = "BIGINT(20) DEFAULT NULL COMMENT '商品ID'")
    private Long goodsId;
    @Column(columnDefinition = "DECIMAL(10,2) DEFAULT NULL COMMENT '单价'")
    private BigDecimal price;
    @Column(columnDefinition = "TINYINT(1) DEFAULT 0 COMMENT '数量'")
    private Integer num;
}

6. 数据访问层代码

// OrderDAO.java
public interface OrderDAO extends BaseMapper<Order> {
    /**
     * 根据订单编号查询订单明细
     * @param orderSn 订单编号
     * @return List<OrderVO>
     */
    List<OrderVO> listOrderInfoByOrderSn(@Param("orderSn") String orderSn);
}
// OrderItem.java
public interface OrderItemDAO extends BaseMapper<OrderItem> {}

7. 业务层代码

// IOrderService.java
public interface IOrderService extends IService<Order> {
    /**
     * 根据订单编号查询订单明细
     * @param orderSn 订单编号
     * @return List<OrderVO>
     */
    List<OrderVO> listOrderInfoByOrderSn(String orderSn);
}
// IOrderItemService.java
public interface IOrderItemService extends IService<OrderItem> {}

8. 业务实现层代码

// OrderServiceImpl.java
@Service
public class OrderServiceImpl extends ServiceImpl<OrderDAO, Order> implements IOrderService {
    @Resource
    private OrderDAO orderDAO;
    @Override
    public List<OrderVO> listOrderInfoByOrderSn(String orderSn) {
        return orderDAO.listOrderInfoByOrderSn(orderSn);
    }
}
// OrderItemServiceImpl.java
@Service
public class OrderItemServiceImpl extends ServiceImpl<OrderItemDAO, OrderItem> implements IOrderItemService {}

9. 控制层代码

@RestController
@RequestMapping("/order")
@Slf4j
public class OrderController {

    @Resource
    private IOrderService orderService;

    @Resource
    private IOrderItemService orderItemService;

    private final static String DELETE_STATUS = "delete_status";
    private final static Integer N0 = 0;
    private final static Boolean condition = true;
    private final static String COLUMN_ORDERID = "order_id";

    /**
     * 测试查询从从库取数据
     * @param orderId 订单ID
     * @return Order 对象
     */
    @GetMapping("/getOrderById")
    public Order getOrderById(@RequestParam("orderId") Long orderId) {
        return orderService.getById(orderId);
    }

    /**
     * 测试插入数据到主库/从库通过 binlog 日志自动同步过去
     * @return true 成功 false 失败
     */
    @GetMapping("/add")
    public Boolean add() {
        Order build = Order.builder().orderSn("110220").userId(1L).price(new BigDecimal("6.06")).build();
        return orderService.save(build);
    }

    /**
     * 测试从多个分片表查询数据
     * @return List<Order>
     */
    @GetMapping("/list")
    public List<Order> list() {
        QueryWrapper<Order> orderQueryWrapper = new QueryWrapper<>();
        orderQueryWrapper.eq(true, "price", BigDecimal.valueOf(6.06));
        return orderService.list(orderQueryWrapper);
    }

    /**
     * 测试插入 t_order & t_order_item 表
     * @return String
     */
    @GetMapping("/insertAll")
    public String insertOrderAndOrderItem() {
        for (long i = 1; i <= 5; i++) {
            Order order = Order.builder().orderSn("0000" + i).userId(i).price(BigDecimal.valueOf(6.1 + i)).build();
            boolean orderBoolean = orderService.save(order);
            log.info("save order success ? {}", orderBoolean);
            OrderItem orderItem = OrderItem.builder().orderId(order.getOrderId()).price(BigDecimal.valueOf(1.1 + i)).goodsId(i).num((int)i).build();
            boolean orderItemBoolean = orderItemService.save(orderItem);
            OrderItem orderItem2 = OrderItem.builder().orderId(order.getOrderId()).price(BigDecimal.valueOf(2.1 + i)).goodsId(1+i).num((int)i+1).build();
            boolean orderItemBoolean2 = orderItemService.save(orderItem2);
            log.info("save orderItem success ? {}, {}", orderItemBoolean, orderItemBoolean2);
        }
        return "success...";
    }

    /**
     * 根据订单编号查询订单明细
     * @param orderSn 订单编号
     * @return List<OrderVO>
     */
    @GetMapping("/listOrderInfoByOrderSn")
    public List<OrderVO> listOrderInfoByOrderSn(@RequestParam(name = "orderSn", value = "00001") String orderSn) {
        return orderService.listOrderInfoByOrderSn(orderSn);
    }

    @GetMapping("/listByPage")
    public ResultJson<List<OrderInfoVO>> listOrderInfoByPage(OrderDTO orderDTO) {

        // 1. 封装未删除订单条件
        QueryWrapper<Order> orderQueryWrapper = new QueryWrapper<>();
        orderQueryWrapper.eq(DELETE_STATUS, N0);

        // 2. 构造分页 Page / 需要客户端传递 offset & pageSize 这两个参数
        IPage<Order> orderPage = new Page<>(orderDTO.getOffset(), orderDTO.getPageSize());

        // 3. 查询 Order 集合
        IPage<Order> page = orderService.page(orderPage, orderQueryWrapper);
        long total = page.getTotal();
        List<Order> orders = page.getRecords();

        // 4. 订单 ID 集合
        ArrayList<Long> orderIds = new ArrayList<>();
        orders.forEach(e-> orderIds.add(e.getOrderId()));

        // 5. 批量查询 OrderItem 集合
        QueryWrapper<OrderItem> orderItemQueryWrapper = new QueryWrapper<>();
        orderItemQueryWrapper.in(condition, COLUMN_ORDERID, orderIds);
        List<OrderItem> orderItems = orderItemService.list(orderItemQueryWrapper);

        // 6. 根据 Map<订单ID,OrderItem集合> 的格式封装
        HashMap<Long, List<OrderItem>> orderItemsMap = new HashMap<>();
        orderItems.forEach(e -> {
            List<OrderItem> list = new ArrayList<>();
            List<OrderItem> ois = orderItemsMap.get(e.getOrderId());
            if (Objects.nonNull(ois) && ois.size() > 0) {
                ois.add(e);
            } else {
                list.add(e);
                orderItemsMap.put(e.getOrderId(), list);
            }
        });

        // 7. 封装成 List<OrderInfoVO> 返回给客户端
        List<OrderInfoVO> orderInfoVOList = new ArrayList<>();
        orders.forEach(e -> {
            OrderInfoVO build = OrderInfoVO.builder()
                    .orderId(e.getOrderId())
                    .orderSn(e.getOrderSn())
                    .createTime(DateUtils.INSTANCE.parseDateToStr(DateUtils.YYYY_MM_DD_HH_MM_SS, e.getCreateTime()))
                    .list(orderItemsMap.get(e.getOrderId()))
                    .build();
            orderInfoVOList.add(build);
        });

        return ResultJson.success(total, orderInfoVOList);
    }
}

10.自动生成表

运行项目后根据分表规则生成表如下:

11. 测试

测试按分片规则插入 t_order & t_order_item 表数据

/**
 * fixme 测试按分片规则插入 t_order & t_order_item 表数据
 * @return String
 */
@GetMapping("/insertAll")
public String insertOrderAndOrderItem() {
    for (long i = 1; i <= 10; i++) {
        Order order = Order.builder().orderSn("0000" + i).userId(i).price(BigDecimal.valueOf(6.1 + i)).build();
        boolean orderBoolean = orderService.save(order);
        log.info("save order success ? {}", orderBoolean);
        OrderItem orderItem = OrderItem.builder().orderId(order.getOrderId()).price(BigDecimal.valueOf(1.1 + i)).goodsId(i).num((int)i).build();
        boolean orderItemBoolean = orderItemService.save(orderItem);
        OrderItem orderItem2 = OrderItem.builder().orderId(order.getOrderId()).price(BigDecimal.valueOf(2.1 + i)).goodsId(1+i).num((int)i+1).build();
        boolean orderItemBoolean2 = orderItemService.save(orderItem2);
        log.info("save orderItem success ? {}, {}", orderItemBoolean, orderItemBoolean2);
    }
    return "success...";
}

12. 查看日志

// 插入 t_order 表逻辑 SQL
Logic SQL: INSERT INTO t_order  ( order_id,order_sn,user_id,price,create_time,update_time )  VALUES  ( ?,?,?,?,?,? )

// 插入 t_order 表实际SQL 直接插入 master 主表
Actual SQL: master ::: INSERT INTO t_order_2   (order_id, order_sn, user_id, price, create_time, update_time) VALUES (?, ?, ?, ?, ?, ?) ::: [1656935254636032002, 00001, 1, 7.1, 2023-05-12 16:12:03.731, null]

// 插入 t_order_item 表逻辑 SQL
Logic SQL: INSERT INTO t_order_item  ( order_item_id,order_id,goods_id,price,num )  VALUES  ( ?,?,?,?,? )

// 插入 t_order_item 表实际SQL 直接插入 master 主表
Actual SQL: master ::: INSERT INTO t_order_item_2   (order_item_id, order_id, goods_id, price, num) VALUES (?, ?, ?, ?, ?) ::: [1656935254698946562, 1656935254636032002, 1, 2.1, 1]

13. 测试查询订单列表数据

/**
 * fixme 6.测试查询订单列表数据
 * @param orderDTO OrderDTO
 * @return ResultJson<List<OrderInfoVO>>
 */
@GetMapping("/listByPage")
public ResultJson<List<OrderInfoVO>> listOrderInfoByPage(OrderDTO orderDTO) {

    // 1. 封装未删除订单条件
    QueryWrapper<Order> orderQueryWrapper = new QueryWrapper<>();
    orderQueryWrapper.eq(DELETE_STATUS, N0);

    // 2. 构造分页 Page / 需要客户端传递 offset & pageSize 这两个参数
    IPage<Order> orderPage = new Page<>(orderDTO.getOffset(), orderDTO.getPageSize());

    // 3. 查询 Order 集合
    IPage<Order> page = orderService.page(orderPage, orderQueryWrapper);
    long total = page.getTotal();
    List<Order> orders = page.getRecords();

    // 4. 订单 ID 集合
    ArrayList<Long> orderIds = new ArrayList<>();
    orders.forEach(e-> orderIds.add(e.getOrderId()));

    // 5. 批量查询 OrderItem 集合
    QueryWrapper<OrderItem> orderItemQueryWrapper = new QueryWrapper<>();
    orderItemQueryWrapper.in(condition, COLUMN_ORDERID, orderIds);
    List<OrderItem> orderItems = orderItemService.list(orderItemQueryWrapper);

    // 6. 根据 Map<订单ID,OrderItem集合> 的格式封装
    HashMap<Long, List<OrderItem>> orderItemsMap = new HashMap<>();
    orderItems.forEach(e -> {
        List<OrderItem> list = new ArrayList<>();
        List<OrderItem> ois = orderItemsMap.get(e.getOrderId());
        if (Objects.nonNull(ois) && ois.size() > 0) {
            ois.add(e);
        } else {
            list.add(e);
            orderItemsMap.put(e.getOrderId(), list);
        }
    });

    // 7. 封装成 List<OrderInfoVO> 返回给客户端
    List<OrderInfoVO> orderInfoVOList = new ArrayList<>();
    orders.forEach(e -> {
        OrderInfoVO build = OrderInfoVO.builder()
                .orderId(e.getOrderId())
                .orderSn(e.getOrderSn())
                .createTime(DateUtils.INSTANCE.parseDateToStr(DateUtils.YYYY_MM_DD_HH_MM_SS, e.getCreateTime()))
                .list(orderItemsMap.get(e.getOrderId()))
                .build();
        orderInfoVOList.add(build);
    });

    return ResultJson.success(total, orderInfoVOList);
}

14. 浏览器访问 http://localhost:8098/order/listByPage?orderSn=00003&offset=0&pageSize=2

结果如下

{
	"total": 10,
	"code": 200,
	"data": [{
		"orderId": 1656935255005130753,
		"createTime": "2023-05-12 16:12:04",
		"orderSn": "00003",
		"list": [{
			"orderItemId": 1656935255042879490,
			"orderId": 1656935255005130753,
			"goodsId": 3,
			"price": 4.10,
			"num": 3
		}, {
			"orderItemId": 1656935255101599745,
			"orderId": 1656935255005130753,
			"goodsId": 4,
			"price": 5.10,
			"num": 4
		}]
	}, {
		"orderId": 1656935255302926337,
		"createTime": "2023-05-12 16:12:04",
		"orderSn": "00005",
		"list": [{
			"orderItemId": 1656935255340675073,
			"orderId": 1656935255302926337,
			"goodsId": 5,
			"price": 6.10,
			"num": 5
		}, {
			"orderItemId": 1656935255411978242,
			"orderId": 1656935255302926337,
			"goodsId": 6,
			"price": 7.10,
			"num": 6
		}]
	}],
	"desc": "成功",
	"currentTime": "2023-05-12 16:21:41"
}

14. 日志

15. 性能与一致性:如何平衡?

  • 性能优化

通过 本地缓存 减少跨节点查询, 路由优化 避免全表扫描。

  • 事务一致性

支持 本地事务 和 XA分布式事务 ,通过两阶段提交保证ACID特性。

16. 避坑指南:常见误区与解决方案

  • 误区1: 认为Sharding-JDBC会降低性能。实际上,通过本地缓存和路由优化,性能损耗通常<5%。

  • 误区2: 跨分片JOIN性能差。建议通过冗余字段或ES同步解决。

  • 误区3: 分片键必须是主键。实际上可以是任意列,但建议选择高选择性列。

写在最后的话

Sharding-JDBC让分库分表像换数据源一样简单。


🌟 感谢您耐心阅读到这里!

🚀 技术成长没有捷径,但每一次的阅读、思考和实践,都在默默缩短您与成功的距离。

💡 如果本文对您有所启发,欢迎点赞👍、收藏📌、分享📤给更多需要的伙伴!

🗣️ 期待在评论区看到您的想法、疑问或建议,我会认真回复,让我们共同探讨、一起进步~

🔔 关注我,持续获取更多干货内容!

🤗 我们下篇文章见!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Thomas.Sir

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值