Skip to content

分库分表-支付服务

阅读此文前,建议小伙伴先阅读分库分表的前置知识,对分库分表 和 shardingsphere 有了大概的理解后,在继续阅读本文

技术精华-全面剖析分库分表

介绍

在开始介绍前,需要先知道大麦网中数据库表的关系是怎么样,关于数据库表设计的详细介绍,小伙伴可跳转到相关文档

基础讲解-数据库表关系

配置

引入 ShardingSphere 的相关依赖

xml
<properties>
	<shardingsphere.version>5.3.2</shardingsphere.version>
</properties>
<dependency>
    <groupId>org.apache.shardingsphere</groupId>
    <artifactId>shardingsphere-jdbc-core</artifactId>
    <version>${shardingsphere.version}</version>
    <exclusions>
        <exclusion>
            <artifactId>logback-classic</artifactId>
            <groupId>ch.qos.logback</groupId>
        </exclusion>
    </exclusions>
</dependency>

根据规则进行分库分表的规则配置

ShardingSphere 官网的规则配置说明:

数据分片 :: ShardingSphere

支付服务相关配置:

yaml
spring:
  datasource:
    driver-class-name: org.apache.shardingsphere.driver.ShardingSphereDriver
    url: jdbc:shardingsphere:classpath:shardingsphere-program.yaml

shardingsphere-pay.yaml配置:

yaml
dataSources:
  # 第一个支付库
  ds_0:
    dataSourceClassName: com.zaxxer.hikari.HikariDataSource
    driverClassName: com.mysql.cj.jdbc.Driver
    jdbcUrl: jdbc:mysql://127.0.0.1:3306/damai_pay_0?useUnicode=true&characterEncoding=UTF-8&rewriteBatchedStatements=true&allowMultiQueries=true&serverTimezone=Asia/Shanghai
    username: root
    password: root
  # 第二个支付库
  ds_1:
    dataSourceClassName: com.zaxxer.hikari.HikariDataSource
    driverClassName: com.mysql.cj.jdbc.Driver
    jdbcUrl: jdbc:mysql://127.0.0.1:3306/damai_pay_1?useUnicode=true&characterEncoding=UTF-8&rewriteBatchedStatements=true&allowMultiQueries=true&serverTimezone=Asia/Shanghai
    username: root
    password: root

rules:
  # 分库分表规则
  - !SHARDING
    tables:
      # 对d_pay_bill表进行分库分表
      d_pay_bill:
        # 库为damai_pay_0 damai_pay_1 表为d_pay_bill_0 至 d_pay_bill_1
        actualDataNodes: ds_${0..1}.d_pay_bill_${0..1}
        # 分库策略
        databaseStrategy:
          standard:
            # 使用out_order_no作为分片键
            shardingColumn: out_order_no
            # 用out_order_no列使用hash取模作为分库算法
            shardingAlgorithmName: databasePayHashModModel
        # 分表策略      
        tableStrategy:
          standard:
            # 使用out_order_no作为分片键
            shardingColumn: out_order_no
            # 用out_order_no列使用hash取模作为分表算法
            shardingAlgorithmName: tablePayHashModModel
      # 对d_refund_bill表进行分库分表
      d_refund_bill:
        # 库为damai_pay_0 damai_pay_1 表为d_refund_bill_0 至 d_refund_bill_1
        actualDataNodes: ds_${0..1}.d_refund_bill_${0..1}
        # 分库策略
        databaseStrategy:
          standard:
            # 使用out_order_no作为分片键
            shardingColumn: out_order_no
            # 用out_order_no列使用hash取模作为分库算法
            shardingAlgorithmName: databaseRefundHashModModel
        # 分表策略      
        tableStrategy:
          standard:
            # 使用out_order_no作为分片键
            shardingColumn: out_order_no
            # 用out_order_no列使用hash取模作为分表算法
            shardingAlgorithmName: tableRefundHashModModel
    # 具体的算法(采用基因位分离策略:分库用低位bit0,分表用次低位bit1,避免数据倾斜)
    shardingAlgorithms:
      # d_pay_bill表分库算法 - 使用out_order_no的hashCode低位(bit0)
      databasePayHashModModel:
        type: INLINE
        props:
          algorithm-expression: ds_${Math.abs(out_order_no.hashCode()) % 2}
      # d_pay_bill表分表算法 - 使用out_order_no的hashCode次低位(bit1)
      tablePayHashModModel:
        type: INLINE
        props:
          algorithm-expression: d_pay_bill_${(Math.abs(out_order_no.hashCode()).intdiv(2)) % 2}
      # d_refund_bill表分库算法 - 使用out_order_no的hashCode低位(bit0)
      databaseRefundHashModModel:
        type: INLINE
        props:
          algorithm-expression: ds_${Math.abs(out_order_no.hashCode()) % 2}
      # d_refund_bill表分表算法 - 使用out_order_no的hashCode次低位(bit1)
      tableRefundHashModModel:
        type: INLINE
        props:
          algorithm-expression: d_refund_bill_${(Math.abs(out_order_no.hashCode()).intdiv(2)) % 2}
props:
  # 打印真实sql
  sql-show: true

分片算法详解(核心重点)

问题背景:为什么传统 HASH_MOD 算法会导致数据倾斜?

如果分库和分表都使用相同的 hashCode % 2 算法:

groovy
// 错误的配置方式
分库: ds_${Math.abs(out_order_no.hashCode()) % 2}
分表: d_pay_bill_${Math.abs(out_order_no.hashCode()) % 2}

导致的问题

plain
              表_0    表_1
库_0 (ds_0)   ✔️       ✖️    ← 偶数库的奇数表永远没数据
库_1 (ds_1)   ✖️       ✔️    ← 奇数库的偶数表永远没数据

解决方案:intdiv 基因位分离策略

groovy
// 正确的配置方式
分库: ds_${Math.abs(out_order_no.hashCode()) % 2}                              // 使用 bit0
分表: d_pay_bill_${(Math.abs(out_order_no.hashCode()).intdiv(2)) % 2}           // 使用 bit1

intdiv 是什么?

intdiv 是 Groovy 的整数除法,等价于除以2取整数部分:

groovy
hashCode.intdiv(2)  等价于  hashCode / 2  (取整数部分)

核心原理:使用 hashCode 的不同二进制位

plain
hashCode 的二进制表示: ... bit3  bit2  bit1  bit0
                              │     │     │     │
                              │     │     │     └─── hashCode % 2     → 分库
                              │     │     └─────── (hashCode/2) % 2 → 分表
  • hashCode % 2:提取最低位 (bit0),用于决定分库
  • (hashCode.intdiv(2)) % 2:先除以2,再取最低位,即提取次低位 (bit1),用于决定分表

完整数据分布表

hashCode % 4bit0 (hashCode%2)bit1 ((hashCode/2)%2)分库分表路由结果
000ds_0_0ds_0.d_pay_bill_0
110ds_1_0ds_1.d_pay_bill_0
201ds_0_1ds_0.d_pay_bill_1
311ds_1_1ds_1.d_pay_bill_1

结果:所有4个分片都有数据,均匀分布!

plain
              表_0    表_1
库_0 (ds_0)   ✔️       ✔️    ← 偶数库的奇数表和偶数表都有数据
库_1 (ds_1)   ✔️       ✔️    ← 奇数库的奇数表和偶数表都有数据

为什么支付和退款使用相同的分片键?

支付账单和退款账单都使用 out_order_no 作为分片键,好处:

  1. 关联查询优化:同一订单的支付和退款记录在同一个库中
  2. 事务一致性:便于在同一分片内处理支付和退款的事务

架构图

plain
┌─────────────────────────────────────────────────────────────┐
│                      Pay Service                             │
├─────────────────────────────────────────────────────────────┤
│                    ShardingSphere                            │
│              (基因位分离 - hashCode路由)                      │
├──────────────────────────┬──────────────────────────────────┤
│       damai_pay_0        │        damai_pay_1               │
├──────────────────────────┼──────────────────────────────────┤
│ d_pay_bill_0             │ d_pay_bill_0                     │
│ d_pay_bill_1             │ d_pay_bill_1                     │
│ d_refund_bill_0          │ d_refund_bill_0                  │
│ d_refund_bill_1          │ d_refund_bill_1                  │
└──────────────────────────┴──────────────────────────────────┘

数据分布示例

假设有以下订单号:

  • ORD202401001 → hashCode=123456
  • ORD202401002 → hashCode=789012
订单号hashCode分库分表
ORD202401001123456ds_0 (偶数)d_pay_bill_0
ORD202401002789012ds_0 (偶数)d_pay_bill_0

注:实际路由结果取决于订单号的实际 hashCode 值

更新: 2026-01-23 10:10:21
原文: https://www.yuque.com/u22210564/ykdrdh/gnvtmbv0csf2x9os

Java 后端面试知识库