Sharding-JDBC 交易流水分库分表实战:取模分片 + 一致性哈希分片(解决扩容灾难)

一、前言:为什么交易流水一定要分库分表?

在支付、电商、金融系统中,交易流水、支付流水、账户流水是最典型的海量数据场景,具备三个核心特征:

  • 数据量级爆炸:日流水几十万~上千万,单表轻松突破2000万,MySQL单表性能断崖式下跌
  • 数据永久不可删除:财务对账、风控溯源、合规留存,不允许删数据、归档成本极高
  • 查询高度集中:90%以上场景都是 根据用户ID查询个人账单/流水

传统单库单表、定时归档、分表备份,都无法支撑高并发、亿级存量数据。Sharding-JDBC 水平分库分表是互联网公司交易流水的标准落地方案。

二、交易流水分片核心选型原则

2.1 分片键选择(行业统一标准)

交易流水严禁按时间分片(按时间分片会导致用户流水散落在不同月份表,用户查询需要跨表扫描,性能极差)。

最优分片键:user_id

  • 同一用户所有流水,全部落在同库同表
  • 用户查询精准单表路由,无跨库、无广播查询
  • 写入均匀、无热点、无数据倾斜

2.2 两套生产分片方案对比

目前企业主流两套水平分片方案:普通取模分片一致性哈希分片,各有适用场景。

分片方案

优点

致命缺点

适用场景

普通取模分片

算法简单、分布均匀、路由高效

扩容节点变化,全部数据路由错乱,需要全量数据迁移(扩容灾难)

短期业务、表数量固定、无需扩容

一致性哈希分片

扩容仅迁移少量数据、支持平滑扩容、无全量迁移灾难

算法复杂,需配置虚拟节点防倾斜

交易流水、支付流水、长期存量数据(首选)

三、方案一:普通取模分库分表(基础版)

3.1 分片规则设计

  • 分库策略:user_id % 2,共2个数据库 ds0、ds1
  • 分表策略:user_id % 8,单库8张表
  • 总分片:2库 * 8表 = 16张流水表
  • 表名:pay_log_0 ~ pay_log_7

3.2 完整YAML配置

Plain Text
spring:
  shardingsphere:
    datasource:
      names: ds0,ds1
      ds0:
        type: com.alibaba.druid.pool.DruidDataSource
        driver-class-name: com.mysql.cj.jdbc.Driver
        url: jdbc:mysql://localhost:3306/pay_db0
        username: root
        password: 123456
      ds1:
        type: com.alibaba.druid.pool.DruidDataSource
        driver-class-name: com.mysql.cj.jdbc.Driver
        url: jdbc:mysql://localhost:3306/pay_db1
        username: root
        password: 123456

    rules:
      sharding:
        tables:
          pay_log:
            actual-data-nodes: ds$->{0..1}.pay_log_$->{0..7}
            database-strategy:
              standard:
                sharding-column: user_id
                sharding-algorithm-name: database-inline
            table-strategy:
              standard:
                sharding-column: user_id
                sharding-algorithm-name: table-inline

        sharding-algorithms:
          database-inline:
            type: INLINE
            props:
              algorithm-expression: ds$->{user_id % 2}
          table-inline:
            type: INLINE
            props:
              algorithm-expression: pay_log_$->{user_id % 8}

    props:
      sql-show: true

3.3 路由效果演示

以 userId=1001 为例:

  • 分库:1001 % 2 = 1 → ds1
  • 分表:1001 % 8 = 5 → pay_log_5
  • 最终路由:ds1.pay_log_5

3.4 核心痛点

如果后续流量上涨,需要从 2库8表扩容为4库8表

取模基数改变,90%以上用户的路由库表全部错乱,必须全量停机迁移数据,风险极高、成本极大。

四、方案二:一致性哈希分库分表

4.1 核心解决的问题

彻底解决取模分片扩容灾难,实现:节点扩容、仅迁移少量数据、不停服平滑扩容,完美适配交易流水长期迭代、数据永久留存的业务特性。

4.2 关键核心:虚拟节点

原生一致性哈希存在数据倾斜问题(节点少、数据扎堆),Sharding-JDBC 提供 虚拟节点 v-node-count

  • 生产统一配置 64/128 个虚拟节点
  • 打散数据分布,彻底解决分片倾斜
  • 保证每张表数据量基本均匀

4.3 一致性哈希完整生产配置

Plain Text
spring:
  shardingsphere:
    datasource:
      names: ds0,ds1
      ds0:
        type: com.alibaba.druid.pool.DruidDataSource
        driver-class-name: com.mysql.cj.jdbc.Driver
        url: jdbc:mysql://localhost:3306/pay_db0
        username: root
        password: 123456
      ds1:
        type: com.alibaba.druid.pool.DruidDataSource
        driver-class-name: com.mysql.cj.jdbc.Driver
        url: jdbc:mysql://localhost:3306/pay_db1
        username: root
        password: 123456

    rules:
      sharding:
        tables:
          pay_log:
            actual-data-nodes: ds$->{0..1}.pay_log_$->{0..7}
            database-strategy:
              standard:
                sharding-column: user_id
                sharding-algorithm-name: database-consistent-hash
            table-strategy:
              standard:
                sharding-column: user_id
                sharding-algorithm-name: table-consistent-hash

        sharding-algorithms:
          # 分库一致性哈希
          database-consistent-hash:
            type: CONSISTENT_HASH
            props:
              v-node-count: 64
          # 分表一致性哈希
          table-consistent-hash:
            type: CONSISTENT_HASH
            props:
              v-node-count: 64

    props:
      sql-show: true

4.4 扩容原理(面试高频)

一致性哈希将所有节点分布在一个环形哈希空间中:

  1. 新增数据库/表节点时,仅相邻区间数据需要迁移
  1. 绝大部分数据哈希路由位置不变
  1. 支持业务不停服、灰度平滑扩容

完美适配交易流水数据不可删、不能停服、长期增长的特点。

五、业务代码完全无侵入(零改造)

Sharding-JDBC 基于 JDBC 层拦截,业务代码、Mapper、SQL 完全无需改动,对开发者透明。

实体类

Plain Text
@Data
@TableName("pay_log")
public class PayLog {
    private Long id;
    private Long userId;     // 核心分片键
    private String orderNo;  // 订单号
    private Long amount;     // 交易金额
    private Integer status;  // 交易状态
    private Date createTime; // 交易时间
}

增删查业务代码

Plain Text
// 1. 新增流水 - 自动哈希路由对应库表
payLogMapper.insert(payLog);

// 2. 根据用户查询流水 - 精准单库单表路由
List<PayLog> logList = this.lambdaQuery()
        .eq(PayLog::getUserId, userId)
        .orderByDesc(PayLog::getCreateTime)
        .list();

六、生产强制规范 & 避坑总结

6.1 必须遵守的开发规范

  • 所有查询必须携带分片键 user_id:否则触发全库全表广播查询,性能雪崩
  • 禁止跨库事务:交易流水采用最终一致性(MQ兜底)
  • 分页、排序必须携带分片键,避免内存归并大数据量
  • 线上必须开启 sql-show,持续监控路由是否准确

6.2 两种分片方案最终选型结论

  • 固定表数量、短期业务、无需扩容:使用 普通取模分片(简单高效)
  • 交易/支付流水、长期存量、需迭代扩容:强制使用 一致性哈希分片

七、总结

针对交易流水数据量大、永久留存、查询集中用户维度、需要长期扩容的业务痛点,我基于 Sharding-JDBC 实现了分库分表架构。

对比传统取模分片存在的扩容全量迁移灾难,我采用user_id 一致性哈希分片 + 虚拟节点防数据倾斜的生产方案,实现了流水数据均匀分片、精准路由、不停服平滑扩容。

该方案无业务代码侵入,解决了单表数据瓶颈,支撑亿级交易流水存储,是金融支付类系统的企业级标准落地架构。

Logo

电商企业物流数字化转型必备!快递鸟 API 接口,72 小时快速完成物流系统集成。全流程实战1V1指导,营造开放的API技术生态圈。

更多推荐