用户服务分库分表设计思维全景

image-4890b528

damai_pro\damai-server\damai-user-service\src\main\resources\shardingsphere-user-local.yaml

官方文档

image-4191437e

一、设计思维框架

分库分表设计思维-50963729

二、需求分析阶段:问对问题

2.1 拿到需求后的灵魂五问

用户服务需求分析-a464cf9f

2.2 用户服务的实体关系梳理

用户服务实体关系分析-84e23298

三、问题建模阶段:识别核心挑战

3.1 分库分表面临的通用挑战

分库分表核心挑战-dd1b922a

3.2 用户服务的特有挑战

用户服务痛点-9870e6ef

四、策略选择阶段:权衡利弊

4.1 多维查询的三大解决策略

多维查询的三大解决策略-3ef658b0

4.2 用户服务的策略选择决策树

策略选择决策过程-29230dcb

4.3 购票人表的策略选择

购票人表的策略选择-5da20db5

五、方案设计阶段:详细设计

5.1 整体架构设计

用户服务分库分表架构-d9199980

5.2 表设计与分片策略

表分片策略设计-431f8a64

5.3 业务流程设计

用户注册流程-5d3a4ca7 手机号登录流程-a0770da1

六、分片算法设计:避免同频共振

6.1 问题复现

问题复现-6cd670ce

6.2 正确设计:层级分片

层级分片-4d6b4bbc

6.3 算法实现

package com.damai.sharding;

import org.apache.shardingsphere.sharding.api.sharding.standard.PreciseShardingValue;
import org.apache.shardingsphere.sharding.api.sharding.standard.StandardShardingAlgorithm;
import org.apache.shardingsphere.sharding.spi.ShardingAlgorithm;

import java.util.Collection;
import java.util.Properties;

/**
 * 层级分片算法 (Hierarchical Sharding Algorithm)
 * <p>
 * 解决场景:当分库数量和分表数量一致(如 2库2表)且使用相同分片键时,
 * 如果单纯使用 hash%2,会导致奇数落入库1表1,偶数落入库0表0,
 * 造成一半的表(库0表1、库1表0)永远无数据的数据倾斜问题。
 * <p>
 * 核心逻辑:
 * 将分库分表视为一个整体,计算全局索引 (Global Index)。
 * 假设 2库 * 2表 = 4个全局槽位 (0, 1, 2, 3)。
 * <p>
 * 分库逻辑 (sharding-type=DATABASE): globalIndex / tableCount (整除) -> 0,0,1,1
 * 分表逻辑 (sharding-type=TABLE):    globalIndex % tableCount (取余) -> 0,1,0,1
 */
public class HierarchicalShardingAlgorithm implements StandardShardingAlgorithm<Comparable<?>> {

    private int dbCount;
    private int tableCount;
    private String shardingType; // 标识当前是用于分库(DATABASE)还是分表(TABLE)

    /**
     * 初始化配置,从 YAML 的 props 中读取属性
     */
    @Override
    public void init(Properties props) {
        // 数据库总数
        this.dbCount = Integer.parseInt(props.getProperty("db-count", "2"));
        // 每个库中的表总数
        this.tableCount = Integer.parseInt(props.getProperty("table-count", "2"));
        // 算法类型:用于区分当前是计算库路由还是表路由
        this.shardingType = props.getProperty("sharding-type", "DATABASE");
    }

    /**
     * 执行精确分片路由 (=, IN)
     */
    @Override
    public String doSharding(Collection<String> availableTargetNames,
                             PreciseShardingValue<Comparable<?>> shardingValue) {

        // 1. 计算哈希值 (使用 hashCode 并取绝对值,防止负数)
        // 注意:生产环境建议对 String 使用更均匀的 Hash 算法(如 MurmurHash),这里使用 hashCode 演示
        int hashCode = Math.abs(shardingValue.getValue().hashCode());

        // 2. 计算全局索引 (Global Slot)
        // 公式:总槽位 = 库数 * 表数
        // 例如 2库2表,总槽位为 4。hash % 4 结果为 0, 1, 2, 3
        int totalSlots = dbCount * tableCount;
        int globalIndex = hashCode % totalSlots;

        // 3. 根据类型计算目标索引 (Target Index)
        int targetIndex;
        if ("DATABASE".equals(shardingType)) {
            // 分库策略:使用 商 (Quotient)
            // 0 / 2 = 0 -> ds_0
            // 1 / 2 = 0 -> ds_0
            // 2 / 2 = 1 -> ds_1
            // 3 / 2 = 1 -> ds_1
            targetIndex = globalIndex / tableCount;
        } else {
            // 分表策略:使用 余数 (Remainder)
            // 0 % 2 = 0 -> table_0
            // 1 % 2 = 1 -> table_1
            // 2 % 2 = 0 -> table_0
            // 3 % 2 = 1 -> table_1
            targetIndex = globalIndex % tableCount;
        }

        // 4. 匹配目标名称
        // availableTargetNames 包含实际的节点名列表,如 ["ds_0", "ds_1"] 或 ["d_user_mobile_0", "d_user_mobile_1"]
        // 我们通过后缀匹配来找到正确的目标
        String suffix = String.valueOf(targetIndex);
        return availableTargetNames.stream()
                .filter(name -> name.endsWith(suffix)) // 匹配以 targetIndex 结尾的节点
                .findFirst()
                .orElseThrow(() -> new UnsupportedOperationException(
                        "Cannot find target node for index: " + targetIndex + ", available: " + availableTargetNames));
    }

    // 处理范围查询 (BETWEEN, > , <) 的逻辑,此处省略,生产环境需根据业务决定是否支持范围扫描
    @Override
    public Collection<String> doSharding(Collection<String> availableTargetNames,
                                         org.apache.shardingsphere.sharding.api.sharding.standard.RangeShardingValue<Comparable<?>> shardingValue) {
        // 默认返回所有节点(全路由),或者抛出异常禁止范围查询
        return availableTargetNames;
    }

    @Override
    public String getType() {
        return "HIERARCHICAL";
    }
}

七、完整配置方案

# shardingsphere-user.yaml 配置详解

# =================================================================================
# 1. 数据源配置 (Data Sources)
# 定义了底层的物理数据库实例。
# =================================================================================
dataSources:
  # 第一个物理库 (Database 0)
  ds_0:
    dataSourceClassName: com.zaxxer.hikari.HikariDataSource
    driverClassName: com.mysql.cj.jdbc.Driver
    # 实际连接地址,指向 damai_user_0 库
    jdbcUrl: jdbc:mysql://127.0.0.1:3306/damai_user_0?...
    username: xxx
    password: xxx

  # 第二个物理库 (Database 1)
  ds_1:
    dataSourceClassName: com.zaxxer.hikari.HikariDataSource
    driverClassName: com.mysql.cj.jdbc.Driver
    # 实际连接地址,指向 damai_user_1 库
    jdbcUrl: jdbc:mysql://127.0.0.1:3306/damai_user_1?...
    username: xxx
    password: xxx

# =================================================================================
# 2. 规则配置 (Rules)
# 包含分片规则 (!SHARDING) 和 加密规则 (!ENCRYPT)
# =================================================================================
rules:
  # -------------------------------------------------------------------------------
  # 2.1 分片规则 (!SHARDING)
  # -------------------------------------------------------------------------------
  - !SHARDING
    tables:
      # ========== 用户手机号映射表 (d_user_mobile) ==========
      # 作用:解决使用手机号登录时的“全路由”问题,通过手机号定位到 user_id。
      d_user_mobile:
        # 实际数据节点:ds_0.d_user_mobile_0, ds_0.d_user_mobile_1, ds_1.d_user_mobile_0, ds_1.d_user_mobile_1
        actualDataNodes: ds_${0..1}.d_user_mobile_${0..1}

        # 分库策略:使用自定义的层级算法 (HIERARCHICAL)
        # 解决了 Hash(mobile)%2 同时用于分库和分表导致的数据倾斜问题(一半表为空)。
        databaseStrategy:
          standard:
            shardingColumn: mobile
            shardingAlgorithmName: mobileDbAlgorithm

        # 分表策略:同样使用层级算法,但参数不同 (sharding-type: TABLE)
        tableStrategy:
          standard:
            shardingColumn: mobile
            shardingAlgorithmName: mobileTableAlgorithm

      # ========== 用户邮箱映射表 (d_user_email) ==========
      # 作用:同上,解决使用邮箱登录时的路由问题。
      d_user_email:
        actualDataNodes: ds_${0..1}.d_user_email_${0..1}
        # 分库策略:使用层级算法
        databaseStrategy:
          standard:
            shardingColumn: email
            shardingAlgorithmName: emailDbAlgorithm
        # 分表策略:使用层级算法
        tableStrategy:
          standard:
            shardingColumn: email
            shardingAlgorithmName: emailTableAlgorithm

      # ========== 用户主表 (d_user) ==========
      # 核心业务表,使用 user_id 进行分片。
      d_user:
        actualDataNodes: ds_${0..1}.d_user_${0..1}
        # 分库策略:标准取模 (MOD)
        # ID 为 Long 类型数值,直接取模即可,不需要层级算法。
        databaseStrategy:
          standard:
            shardingColumn: id
            shardingAlgorithmName: userDbAlgorithm
        # 分表策略:使用自定义类算法 (CLASS_BASED)
        # 这里演示了如何指向一个具体的 Java 类实现分表逻辑。
        tableStrategy:
          standard:
            shardingColumn: id
            shardingAlgorithmName: userTableAlgorithm

      # ========== 购票人表 (d_ticket_user) ==========
      # 业务关联表,绑定在 user_id 上,确保同一用户的购票人数据和用户数据在同一个库中。
      d_ticket_user:
        actualDataNodes: ds_${0..1}.d_ticket_user_${0..1}
        databaseStrategy:
          standard:
            shardingColumn: user_id
            shardingAlgorithmName: ticketUserDbAlgorithm
        tableStrategy:
          standard:
            shardingColumn: user_id
            shardingAlgorithmName: ticketUserTableAlgorithm

    # -------------------------------------------------------------------------------
    # 分片算法定义 (Sharding Algorithms)
    # -------------------------------------------------------------------------------
    shardingAlgorithms:
      # 1. 手机号 - 分库算法 (sharding-type: DATABASE)
      mobileDbAlgorithm:
        type: CLASS_BASED
        props:
          strategy: STANDARD
          algorithmClassName: com.damai.sharding.HierarchicalShardingAlgorithm # 修改为实际包路径
          db-count: 2
          table-count: 2
          sharding-type: DATABASE

     # 2. 手机号 - 分表算法 (sharding-type: TABLE)
      mobileTableAlgorithm:
        type: CLASS_BASED
        props:
          strategy: STANDARD
          algorithmClassName: com.damai.sharding.HierarchicalShardingAlgorithm # 修改为实际包路径
          db-count: 2
          table-count: 2
          sharding-type: TABLE

      # 3. 邮箱 - 分库算法
      emailDbAlgorithm:
        type: CLASS_BASED
        props:
          strategy: STANDARD
          algorithmClassName: com.damai.sharding.HierarchicalShardingAlgorithm # 修改为实际包路径
          db-count: 2
          table-count: 2
          sharding-type: DATABASE

      # 4. 邮箱 - 分表算法
      emailTableAlgorithm:
        type: CLASS_BASED
        props:
          strategy: STANDARD
          algorithmClassName: com.damai.sharding.HierarchicalShardingAlgorithm # 修改为实际包路径
          db-count: 2
          table-count: 2
          sharding-type: TABLE

      # 用户主表分库 - 标准取模
      # 逻辑:id % 2
      userDbAlgorithm:
        type: MOD
        props:
          sharding-count: 2 # 分片总数

      # 用户主表分表 - 基于类实现
      # 指向具体的 Java 类,灵活性最高
      userTableAlgorithm:
        type: CLASS_BASED
        props:
          strategy: STANDARD
          algorithmClassName: com.damai.sharding.UserTableAlgorithm

      # 购票人表分库 - 标准取模 (为了和用户主表路由一致)
      ticketUserDbAlgorithm:
        type: MOD
        props:
          sharding-count: 2

      # 购票人表分表 - 基于类实现
      ticketUserTableAlgorithm:
        type: CLASS_BASED
        props:
          strategy: STANDARD
          algorithmClassName: com.damai.sharding.UserTableAlgorithm

  # -------------------------------------------------------------------------------
  # 2.2 加密规则 (!ENCRYPT)
  # 确保敏感数据在数据库中以密文存储,防止拖库导致隐私泄露。
  # -------------------------------------------------------------------------------
  - !ENCRYPT
    tables:
      # 用户主表加密
      d_user:
        columns:
          mobile:
            cipherColumn: mobile
            encryptorName: user_encryption_algorithm
          password:
            cipherColumn: password
            encryptorName: user_encryption_algorithm
          id_number:
            cipherColumn: id_number
            encryptorName: user_encryption_algorithm

      # [重要新增] 用户手机映射表加密
      # 必须加密!否则:1. 数据泄露风险;2. 若分片键使用密文计算,此处不加密会导致路由逻辑不一致。
      d_user_mobile:
        columns:
          mobile:
            cipherColumn: mobile
            encryptorName: user_encryption_algorithm

      # [重要新增] 用户邮箱映射表加密
      d_user_email:
        columns:
          email:
            cipherColumn: email
            encryptorName: user_encryption_algorithm

    # 加密算法定义 (此处省略了 encryptors 部分的详细配置,通常需要配合 encryptors 定义算法类型和密钥)

# =================================================================================
# 3. 属性配置 (Props)
# =================================================================================
props:
  # 开发调试时开启,可在控制台打印出真实的 SQL 重写和路由结果
  sql-show: true

八、设计验证清单

设计验证清单-82ecf30b

九、设计思维总结

设计思维总结-aa970ea2

企业级项目导航:⬅️ 02-分库分表最佳实践:从基础原理到高阶架构设计 | 01-用户服务分库分表设计思维全景 | ➡️ 02-节目服务与支付服务分库分表设计思维全景