Files
travel/docs/分库分表方案/订单系统分表设计.md
2026-03-09 11:22:43 +08:00

29 KiB
Raw Blame History

订单系统分库分表设计方案

业务场景分析

核心查询场景

订单表 (order)

  • 订单列表:根据 userNo + 时间范围 + 订单类型 + 订单状态查询
  • 订单详情:根据 orderNo 查询

凭证表 (ticket)

  • 根据 orderNo 查询
  • 根据 userNo + 状态查询

支付单表 (payment)

  • 根据 orderNo 查询
  • 根据 paymentNo 查询
  • 根据 类型 + 状态查询

数据量预估

假设:

  • 日订单量100万
  • 年订单量3.6亿
  • 3年数据10亿+

分表策略

核心原则

  1. 按用户维度分表:大部分查询都带 userNo
  2. 保持关联关系:订单、凭证、支付单使用相同的分片键
  3. 支持多维度查询:通过冗余表或索引表解决

方案设计

方案一:按 userNo 分库分表(推荐)

分片规则

分片数16 个库 × 16 张表 = 256 个分片

路由规则:
db_index = userNo % 16
table_index = (userNo / 16) % 16

示例:
userNo = 123456
db_index = 123456 % 16 = 0  → order_db_0
table_index = (123456 / 16) % 16 = 8 → order_8

表结构设计

1. 订单表 (order_0 ~ order_15)
CREATE TABLE `order_{0-15}` (
  `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键',
  `order_no` VARCHAR(64) NOT NULL COMMENT '订单编号(全局唯一)',
  `user_no` VARCHAR(64) NOT NULL COMMENT '用户编号(分片键)',
  `order_type` TINYINT NOT NULL COMMENT '订单类型1-实时公交 2-定制巴士',
  `order_status` TINYINT NOT NULL COMMENT '订单状态1-待支付 2-已支付 3-已完成 4-已取消',
  `route_id` VARCHAR(64) COMMENT '线路ID',
  `schedule_id` VARCHAR(64) COMMENT '班次ID',
  `passenger_count` INT COMMENT '乘客数量',
  `total_amount` DECIMAL(10,2) COMMENT '订单金额',
  `create_time` DATETIME NOT NULL COMMENT '创建时间',
  `update_time` DATETIME NOT NULL COMMENT '更新时间',
  `pay_time` DATETIME COMMENT '支付时间',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_order_no` (`order_no`),
  KEY `idx_user_create` (`user_no`, `create_time`),
  KEY `idx_user_status` (`user_no`, `order_status`, `create_time`),
  KEY `idx_user_type` (`user_no`, `order_type`, `create_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单表';

索引说明:

  • uk_order_no:支持按订单号查询详情
  • idx_user_create:支持用户订单列表(按时间排序)
  • idx_user_status:支持按状态筛选
  • idx_user_type:支持按类型筛选
2. 凭证表 (ticket_0 ~ ticket_15)
CREATE TABLE `ticket_{0-15}` (
  `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键',
  `ticket_no` VARCHAR(64) NOT NULL COMMENT '凭证编号(全局唯一)',
  `order_no` VARCHAR(64) NOT NULL COMMENT '订单编号',
  `user_no` VARCHAR(64) NOT NULL COMMENT '用户编号(分片键)',
  `ticket_type` TINYINT NOT NULL COMMENT '凭证类型1-单程票 2-往返票',
  `ticket_status` TINYINT NOT NULL COMMENT '凭证状态1-未使用 2-已使用 3-已过期 4-已作废',
  `route_id` VARCHAR(64) COMMENT '线路ID',
  `schedule_id` VARCHAR(64) COMMENT '班次ID',
  `seat_no` VARCHAR(32) COMMENT '座位号',
  `qr_code` VARCHAR(256) COMMENT '二维码',
  `valid_start_time` DATETIME COMMENT '有效期开始',
  `valid_end_time` DATETIME COMMENT '有效期结束',
  `use_time` DATETIME COMMENT '使用时间',
  `create_time` DATETIME NOT NULL COMMENT '创建时间',
  `update_time` DATETIME NOT NULL COMMENT '更新时间',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_ticket_no` (`ticket_no`),
  KEY `idx_order_no` (`order_no`),
  KEY `idx_user_status` (`user_no`, `ticket_status`, `create_time`),
  KEY `idx_user_create` (`user_no`, `create_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='凭证表';

索引说明:

  • uk_ticket_no:支持按凭证号查询
  • idx_order_no:支持按订单号查询凭证
  • idx_user_status:支持用户凭证列表(按状态筛选)
3. 支付单表 (payment_0 ~ payment_15)
CREATE TABLE `payment_{0-15}` (
  `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键',
  `payment_no` VARCHAR(64) NOT NULL COMMENT '支付单编号(全局唯一)',
  `order_no` VARCHAR(64) NOT NULL COMMENT '订单编号',
  `user_no` VARCHAR(64) NOT NULL COMMENT '用户编号(分片键)',
  `payment_type` TINYINT NOT NULL COMMENT '支付类型1-支付 2-退款',
  `payment_status` TINYINT NOT NULL COMMENT '支付状态1-待支付 2-支付中 3-支付成功 4-支付失败',
  `payment_channel` VARCHAR(32) COMMENT '支付渠道ALIPAY/WECHAT/BALANCE',
  `payment_amount` DECIMAL(10,2) NOT NULL COMMENT '支付金额',
  `third_party_no` VARCHAR(128) COMMENT '第三方支付流水号',
  `callback_time` DATETIME COMMENT '回调时间',
  `create_time` DATETIME NOT NULL COMMENT '创建时间',
  `update_time` DATETIME NOT NULL COMMENT '更新时间',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_payment_no` (`payment_no`),
  KEY `idx_order_no` (`order_no`),
  KEY `idx_user_create` (`user_no`, `create_time`),
  KEY `idx_user_type_status` (`user_no`, `payment_type`, `payment_status`),
  KEY `idx_third_party` (`third_party_no`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='支付单表';

索引说明:

  • uk_payment_no:支持按支付单号查询
  • idx_order_no:支持按订单号查询支付单
  • idx_user_type_status:支持按类型和状态筛选
  • idx_third_party:支持第三方回调查询

方案二:订单号路由索引表(解决按 orderNo 查询)

问题

按 userNo 分表后,如果只有 orderNo无法确定在哪个分片。

解决方案:订单号路由表

-- 单独的路由表(不分表或按 orderNo 分表)
CREATE TABLE `order_route` (
  `id` BIGINT NOT NULL AUTO_INCREMENT,
  `order_no` VARCHAR(64) NOT NULL COMMENT '订单编号',
  `user_no` VARCHAR(64) NOT NULL COMMENT '用户编号',
  `db_index` TINYINT NOT NULL COMMENT '库索引',
  `table_index` TINYINT NOT NULL COMMENT '表索引',
  `create_time` DATETIME NOT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_order_no` (`order_no`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单路由表';

使用方式:

@Service
public class OrderQueryService {
    
    /**
     * 根据订单号查询订单详情
     */
    public Order getOrderByOrderNo(String orderNo) {
        // 1. 从路由表查询 userNo
        OrderRoute route = orderRouteRepository.findByOrderNo(orderNo);
        if (route == null) {
            return null;
        }
        
        // 2. 根据 userNo 路由到具体分片查询
        return orderRepository.findByOrderNo(route.getUserNo(), orderNo);
    }
}

优化:路由表也可以分表

-- 按 orderNo 后4位分表16张表
CREATE TABLE `order_route_{0-15}` (
  ...
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

-- 路由规则
table_index = orderNo 4 % 16

方案三:业务单号中包含 userNo 信息(推荐)

统一编号生成规则

所有业务单号(订单号、凭证号、支付单号)都包含 userNo 信息,实现自路由。

1. 订单号生成规则
订单号格式:{业务前缀}{时间戳}{userNo后6位}{序列号}
示例OD20240309123456789012001

解析:
- OD: 订单前缀
- 20240309: 日期
- 123456: 时间(时分秒)
- 789012: userNo 后6位用于路由
- 001: 序列号
2. 凭证号生成规则
凭证号格式:{业务前缀}{时间戳}{userNo后6位}{序列号}
示例TK20240309123456789012001

解析:
- TK: 凭证前缀 (Ticket)
- 20240309: 日期
- 123456: 时间(时分秒)
- 789012: userNo 后6位用于路由
- 001: 序列号
3. 支付单号生成规则
支付单号格式:{业务前缀}{时间戳}{userNo后6位}{序列号}
示例PY20240309123456789012001

解析:
- PY: 支付单前缀 (Payment)
- 20240309: 日期
- 123456: 时间(时分秒)
- 789012: userNo 后6位用于路由
- 001: 序列号

核心优点:

  • 所有业务单号都支持自路由
  • 无需额外的路由表
  • 直接从单号解析出分片信息
  • 性能最优

实现:

/**
 * 统一业务单号生成器
 */
@Service
public class BusinessNoGenerator {
    
    @Autowired
    private RedisTemplate<String, Long> redisTemplate;
    
    /**
     * 生成订单号
     */
    public String generateOrderNo(String userNo) {
        return generateBusinessNo("OD", userNo);
    }
    
    /**
     * 生成凭证号
     */
    public String generateTicketNo(String userNo) {
        return generateBusinessNo("TK", userNo);
    }
    
    /**
     * 生成支付单号
     */
    public String generatePaymentNo(String userNo) {
        return generateBusinessNo("PY", userNo);
    }
    
    /**
     * 统一生成业务单号
     */
    private String generateBusinessNo(String prefix, String userNo) {
        // 时间部分yyyyMMddHHmmss
        String timestamp = LocalDateTime.now()
            .format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss"));
        
        // userNo 后6位用于路由
        String userSuffix = extractUserSuffix(userNo);
        
        // 序列号3位- 使用 Redis 保证唯一性
        String sequence = String.format("%03d", getSequence(prefix + timestamp));
        
        return prefix + timestamp + userSuffix + sequence;
    }
    
    /**
     * 提取 userNo 后6位
     */
    private String extractUserSuffix(String userNo) {
        if (userNo.length() <= 6) {
            return String.format("%06d", Long.parseLong(userNo));
        }
        return userNo.substring(userNo.length() - 6);
    }
    
    /**
     * 获取序列号Redis 自增)
     */
    private Long getSequence(String key) {
        Long seq = redisTemplate.opsForValue().increment("seq:" + key);
        return seq % 1000; // 3位序列号循环使用
    }
    
    /**
     * 从业务单号解析 userNo 后缀
     */
    public String parseUserSuffix(String businessNo) {
        // OD20240309123456789012001
        // TK20240309123456789012001
        // PY20240309123456789012001
        // 从第16位开始取6位
        if (businessNo.length() >= 22) {
            return businessNo.substring(16, 22);
        }
        return null;
    }
    
    /**
     * 根据业务单号计算分片信息
     */
    public ShardingInfo getShardingInfo(String businessNo) {
        String userSuffix = parseUserSuffix(businessNo);
        if (userSuffix == null) {
            throw new IllegalArgumentException("Invalid businessNo: " + businessNo);
        }
        
        long userNoSuffix = Long.parseLong(userSuffix);
        int dbIndex = (int) (userNoSuffix % 16);
        int tableIndex = (int) ((userNoSuffix / 16) % 16);
        
        return new ShardingInfo(dbIndex, tableIndex);
    }
    
    /**
     * 判断业务单号类型
     */
    public BusinessNoType getBusinessNoType(String businessNo) {
        if (businessNo.startsWith("OD")) {
            return BusinessNoType.ORDER;
        } else if (businessNo.startsWith("TK")) {
            return BusinessNoType.TICKET;
        } else if (businessNo.startsWith("PY")) {
            return BusinessNoType.PAYMENT;
        }
        throw new IllegalArgumentException("Unknown businessNo type: " + businessNo);
    }
}

/**
 * 业务单号类型枚举
 */
public enum BusinessNoType {
    ORDER,      // 订单
    TICKET,     // 凭证
    PAYMENT     // 支付单
}

/**
 * 分片信息
 */
@Data
@AllArgsConstructor
public class ShardingInfo {
    private int dbIndex;
    private int tableIndex;
    
    public String getDbName() {
        return "ds" + dbIndex;
    }
    
    public String getTableSuffix() {
        return String.valueOf(tableIndex);
    }
}

查询实现

1. 订单列表查询(带 userNo

@Service
public class OrderQueryService {
    
    /**
     * 查询用户订单列表
     */
    public PageResult<Order> queryOrderList(OrderQueryDTO query) {
        // 参数userNo, startTime, endTime, orderType, orderStatus
        
        // 1. 根据 userNo 路由到具体分片
        ShardingInfo sharding = calculateSharding(query.getUserNo());
        
        // 2. 构建查询条件
        QueryWrapper<Order> wrapper = new QueryWrapper<>();
        wrapper.eq("user_no", query.getUserNo());
        
        if (query.getStartTime() != null) {
            wrapper.ge("create_time", query.getStartTime());
        }
        if (query.getEndTime() != null) {
            wrapper.le("create_time", query.getEndTime());
        }
        if (query.getOrderType() != null) {
            wrapper.eq("order_type", query.getOrderType());
        }
        if (query.getOrderStatus() != null) {
            wrapper.eq("order_status", query.getOrderStatus());
        }
        
        wrapper.orderByDesc("create_time");
        
        // 3. 分页查询
        Page<Order> page = new Page<>(query.getPageNo(), query.getPageSize());
        Page<Order> result = orderMapper.selectPage(page, wrapper);
        
        return PageResult.of(result);
    }
}

SQL 示例:

-- 路由到 order_db_0.order_8
SELECT * FROM order_8
WHERE user_no = '123456'
  AND create_time >= '2024-01-01'
  AND create_time <= '2024-03-09'
  AND order_type = 1
  AND order_status = 2
ORDER BY create_time DESC
LIMIT 0, 20;

-- 使用索引idx_user_type (user_no, order_type, create_time)

2. 订单详情查询(只有 orderNo

@Service
public class OrderQueryService {
    
    /**
     * 根据订单号查询详情
     */
    public Order getOrderDetail(String orderNo) {
        // 方案一:从订单号解析分片信息(推荐)
        ShardingInfo sharding = orderNoGenerator.getShardingInfo(orderNo);
        
        // 方案二:查询路由表
        // OrderRoute route = orderRouteRepository.findByOrderNo(orderNo);
        // ShardingInfo sharding = new ShardingInfo(route.getDbIndex(), route.getTableIndex());
        
        // 查询订单
        return orderMapper.selectByOrderNo(sharding, orderNo);
    }
}

SQL 示例:

-- 路由到 order_db_0.order_8
SELECT * FROM order_8
WHERE order_no = 'OD20240309123456789012001';

-- 使用索引uk_order_no

3. 凭证查询

@Service
public class TicketQueryService {
    
    @Autowired
    private BusinessNoGenerator businessNoGenerator;
    
    @Autowired
    private TicketMapper ticketMapper;
    
    /**
     * 根据凭证号查询凭证详情(核心场景)
     */
    public Ticket getTicketByTicketNo(String ticketNo) {
        // 从凭证号直接解析分片信息
        ShardingInfo sharding = businessNoGenerator.getShardingInfo(ticketNo);
        
        // 查询凭证
        return ticketMapper.selectByTicketNo(sharding, ticketNo);
    }
    
    /**
     * 根据订单号查询凭证列表
     */
    public List<Ticket> getTicketsByOrderNo(String orderNo) {
        // 从订单号解析分片信息
        ShardingInfo sharding = businessNoGenerator.getShardingInfo(orderNo);
        
        // 查询凭证(凭证和订单在同一分片)
        return ticketMapper.selectByOrderNo(sharding, orderNo);
    }
    
    /**
     * 查询用户凭证列表
     */
    public PageResult<Ticket> getUserTickets(TicketQueryDTO query) {
        // 根据 userNo 路由
        ShardingInfo sharding = calculateSharding(query.getUserNo());
        
        QueryWrapper<Ticket> wrapper = new QueryWrapper<>();
        wrapper.eq("user_no", query.getUserNo());
        
        if (query.getStatus() != null) {
            wrapper.eq("ticket_status", query.getStatus());
        }
        if (query.getStartTime() != null) {
            wrapper.ge("create_time", query.getStartTime());
        }
        if (query.getEndTime() != null) {
            wrapper.le("create_time", query.getEndTime());
        }
        
        wrapper.orderByDesc("create_time");
        
        Page<Ticket> page = new Page<>(query.getPageNo(), query.getPageSize());
        Page<Ticket> result = ticketMapper.selectPage(page, wrapper);
        
        return PageResult.of(result);
    }
    
    /**
     * 凭证核销(扫码场景)
     */
    public Result<Boolean> verifyTicket(String ticketNo) {
        // 1. 从凭证号解析分片
        ShardingInfo sharding = businessNoGenerator.getShardingInfo(ticketNo);
        
        // 2. 查询凭证
        Ticket ticket = ticketMapper.selectByTicketNo(sharding, ticketNo);
        
        if (ticket == null) {
            return Result.fail("TICKET_NOT_FOUND", "凭证不存在");
        }
        
        if (ticket.getTicketStatus() != TicketStatus.UNUSED.getCode()) {
            return Result.fail("TICKET_INVALID", "凭证状态异常");
        }
        
        // 3. 更新凭证状态
        ticket.setTicketStatus(TicketStatus.USED.getCode());
        ticket.setUseTime(LocalDateTime.now());
        ticketMapper.updateById(ticket);
        
        return Result.success(true);
    }
}

SQL 示例:

-- 1. 根据凭证号查询(从凭证号解析到 ticket_db_5.ticket_12
SELECT * FROM ticket_12
WHERE ticket_no = 'TK20240309123456789012001';
-- 使用索引uk_ticket_no
-- 执行时间:<5ms

-- 2. 根据订单号查询凭证(从订单号解析到同一分片)
SELECT * FROM ticket_12
WHERE order_no = 'OD20240309123456789012001';
-- 使用索引idx_order_no
-- 执行时间:<10ms

-- 3. 用户凭证列表
SELECT * FROM ticket_12
WHERE user_no = '123456'
  AND ticket_status = 1
  AND create_time >= '2024-01-01'
ORDER BY create_time DESC
LIMIT 0, 20;
-- 使用索引idx_user_status
-- 执行时间:<30ms

4. 支付单查询

@Service
public class PaymentQueryService {
    
    @Autowired
    private BusinessNoGenerator businessNoGenerator;
    
    @Autowired
    private PaymentMapper paymentMapper;
    
    @Autowired
    private PaymentRouteRepository paymentRouteRepository;
    
    /**
     * 根据支付单号查询(核心场景)
     */
    public Payment getPaymentByPaymentNo(String paymentNo) {
        // 从支付单号直接解析分片信息
        ShardingInfo sharding = businessNoGenerator.getShardingInfo(paymentNo);
        
        return paymentMapper.selectByPaymentNo(sharding, paymentNo);
    }
    
    /**
     * 根据订单号查询支付单列表
     */
    public List<Payment> getPaymentsByOrderNo(String orderNo) {
        // 从订单号解析分片信息
        ShardingInfo sharding = businessNoGenerator.getShardingInfo(orderNo);
        
        return paymentMapper.selectByOrderNo(sharding, orderNo);
    }
    
    /**
     * 查询用户支付单列表
     */
    public PageResult<Payment> getUserPayments(PaymentQueryDTO query) {
        // 根据 userNo 路由
        ShardingInfo sharding = calculateSharding(query.getUserNo());
        
        QueryWrapper<Payment> wrapper = new QueryWrapper<>();
        wrapper.eq("user_no", query.getUserNo());
        
        if (query.getPaymentType() != null) {
            wrapper.eq("payment_type", query.getPaymentType());
        }
        if (query.getPaymentStatus() != null) {
            wrapper.eq("payment_status", query.getPaymentStatus());
        }
        if (query.getStartTime() != null) {
            wrapper.ge("create_time", query.getStartTime());
        }
        if (query.getEndTime() != null) {
            wrapper.le("create_time", query.getEndTime());
        }
        
        wrapper.orderByDesc("create_time");
        
        Page<Payment> page = new Page<>(query.getPageNo(), query.getPageSize());
        Page<Payment> result = paymentMapper.selectPage(page, wrapper);
        
        return PageResult.of(result);
    }
    
    /**
     * 第三方支付回调查询(特殊场景)
     * 
     * 问题:第三方支付流水号不包含 userNo 信息,无法直接路由
     * 解决方案:使用路由表
     */
    public Payment getPaymentByThirdPartyNo(String thirdPartyNo) {
        // 方案一:查询路由表(推荐)
        PaymentRoute route = paymentRouteRepository.findByThirdPartyNo(thirdPartyNo);
        
        if (route == null) {
            return null;
        }
        
        ShardingInfo sharding = new ShardingInfo(route.getDbIndex(), route.getTableIndex());
        return paymentMapper.selectByPaymentNo(sharding, route.getPaymentNo());
    }
    
    /**
     * 创建支付单时,同步写入路由表
     */
    @Transactional
    public void createPayment(Payment payment) {
        // 1. 写入支付单
        paymentMapper.insert(payment);
        
        // 2. 写入路由表(用于第三方回调查询)
        if (StringUtils.isNotBlank(payment.getThirdPartyNo())) {
            PaymentRoute route = new PaymentRoute();
            route.setPaymentNo(payment.getPaymentNo());
            route.setThirdPartyNo(payment.getThirdPartyNo());
            route.setUserNo(payment.getUserNo());
            
            ShardingInfo sharding = businessNoGenerator.getShardingInfo(payment.getPaymentNo());
            route.setDbIndex(sharding.getDbIndex());
            route.setTableIndex(sharding.getTableIndex());
            route.setCreateTime(LocalDateTime.now());
            
            paymentRouteRepository.save(route);
        }
    }
}

支付单路由表设计:

-- 支付单路由表(不分表,或按 third_party_no 分表)
CREATE TABLE `payment_route` (
  `id` BIGINT NOT NULL AUTO_INCREMENT,
  `payment_no` VARCHAR(64) NOT NULL COMMENT '支付单编号',
  `third_party_no` VARCHAR(128) NOT NULL COMMENT '第三方支付流水号',
  `user_no` VARCHAR(64) NOT NULL COMMENT '用户编号',
  `db_index` TINYINT NOT NULL COMMENT '库索引',
  `table_index` TINYINT NOT NULL COMMENT '表索引',
  `create_time` DATETIME NOT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_third_party_no` (`third_party_no`),
  KEY `idx_payment_no` (`payment_no`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='支付单路由表';

SQL 示例:

-- 1. 根据支付单号查询(从支付单号解析到 payment_db_3.payment_7
SELECT * FROM payment_7
WHERE payment_no = 'PY20240309123456789012001';
-- 使用索引uk_payment_no
-- 执行时间:<5ms

-- 2. 根据订单号查询支付单(从订单号解析到同一分片)
SELECT * FROM payment_7
WHERE order_no = 'OD20240309123456789012001';
-- 使用索引idx_order_no
-- 执行时间:<10ms

-- 3. 第三方回调查询(先查路由表)
-- Step 1: 查询路由表
SELECT payment_no, db_index, table_index 
FROM payment_route
WHERE third_party_no = '2024030912345678901234567890';
-- 执行时间:<5ms

-- Step 2: 根据路由信息查询支付单
SELECT * FROM payment_7
WHERE payment_no = 'PY20240309123456789012001';
-- 执行时间:<5ms

-- 总耗时:<10ms

-- 4. 用户支付单列表
SELECT * FROM payment_7
WHERE user_no = '123456'
  AND payment_type = 1
  AND payment_status = 3
  AND create_time >= '2024-01-01'
ORDER BY create_time DESC
LIMIT 0, 20;
-- 使用索引idx_user_type_status
-- 执行时间:<30ms

ShardingSphere 配置

1. 数据源配置

spring:
  shardingsphere:
    datasource:
      names: ds0,ds1,ds2,ds3,ds4,ds5,ds6,ds7,ds8,ds9,ds10,ds11,ds12,ds13,ds14,ds15
      
      # 配置16个数据源
      ds0:
        type: com.zaxxer.hikari.HikariDataSource
        driver-class-name: com.mysql.cj.jdbc.Driver
        jdbc-url: jdbc:mysql://localhost:3306/order_db_0
        username: root
        password: password
      
      ds1:
        type: com.zaxxer.hikari.HikariDataSource
        driver-class-name: com.mysql.cj.jdbc.Driver
        jdbc-url: jdbc:mysql://localhost:3306/order_db_1
        username: root
        password: password
      
      # ... ds2 ~ ds15 配置类似
    
    rules:
      sharding:
        # 分片算法
        sharding-algorithms:
          database-inline:
            type: INLINE
            props:
              algorithm-expression: ds$->{user_no.toLong() % 16}
          
          table-inline:
            type: INLINE
            props:
              algorithm-expression: order_$->{(user_no.toLong() / 16) % 16}
        
        # 订单表分片规则
        tables:
          order:
            actual-data-nodes: ds$->{0..15}.order_$->{0..15}
            database-strategy:
              standard:
                sharding-column: user_no
                sharding-algorithm-name: database-inline
            table-strategy:
              standard:
                sharding-column: user_no
                sharding-algorithm-name: table-inline
            key-generate-strategy:
              column: id
              key-generator-name: snowflake
          
          # 凭证表分片规则
          ticket:
            actual-data-nodes: ds$->{0..15}.ticket_$->{0..15}
            database-strategy:
              standard:
                sharding-column: user_no
                sharding-algorithm-name: database-inline
            table-strategy:
              standard:
                sharding-column: user_no
                sharding-algorithm-name: table-inline
          
          # 支付单表分片规则
          payment:
            actual-data-nodes: ds$->{0..15}.payment_$->{0..15}
            database-strategy:
              standard:
                sharding-column: user_no
                sharding-algorithm-name: database-inline
            table-strategy:
              standard:
                sharding-column: user_no
                sharding-algorithm-name: table-inline
        
        # 绑定表(关联查询优化)
        binding-tables:
          - order,ticket,payment
        
        # 广播表(不分片的表)
        broadcast-tables:
          - order_route
    
    props:
      sql-show: true

2. 自定义分片算法(支持订单号路由)

@Component
public class OrderNoShardingAlgorithm implements StandardShardingAlgorithm<String> {
    
    @Autowired
    private OrderNoGenerator orderNoGenerator;
    
    @Override
    public String doSharding(Collection<String> availableTargetNames, 
                            PreciseShardingValue<String> shardingValue) {
        String columnValue = shardingValue.getValue();
        
        // 判断是 userNo 还是 orderNo
        if (columnValue.startsWith("OD")) {
            // 订单号,解析出分片信息
            ShardingInfo sharding = orderNoGenerator.getShardingInfo(columnValue);
            
            if ("user_no".equals(shardingValue.getColumnName())) {
                return "ds" + sharding.getDbIndex();
            } else {
                return "order_" + sharding.getTableIndex();
            }
        } else {
            // userNo直接计算
            long userNo = Long.parseLong(columnValue);
            int index = (int) (userNo % 16);
            
            if ("user_no".equals(shardingValue.getColumnName())) {
                return "ds" + index;
            } else {
                return "order_" + ((int) ((userNo / 16) % 16));
            }
        }
    }
    
    @Override
    public Collection<String> doSharding(Collection<String> availableTargetNames,
                                        RangeShardingValue<String> shardingValue) {
        // 范围查询,返回所有分片
        return availableTargetNames;
    }
}

性能优化建议

1. 索引优化

-- 订单表核心索引
ALTER TABLE order_0 ADD INDEX idx_user_create (user_no, create_time);
ALTER TABLE order_0 ADD INDEX idx_user_status (user_no, order_status, create_time);
ALTER TABLE order_0 ADD INDEX idx_user_type (user_no, order_type, create_time);

-- 联合索引覆盖查询
ALTER TABLE order_0 ADD INDEX idx_user_list (
  user_no, order_type, order_status, create_time, order_no, total_amount
);

2. 分区表(单表内再分区)

-- 按月分区
CREATE TABLE `order_0` (
  ...
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
PARTITION BY RANGE (TO_DAYS(create_time)) (
  PARTITION p202401 VALUES LESS THAN (TO_DAYS('2024-02-01')),
  PARTITION p202402 VALUES LESS THAN (TO_DAYS('2024-03-01')),
  PARTITION p202403 VALUES LESS THAN (TO_DAYS('2024-04-01')),
  ...
);

3. 冷热数据分离

-- 热数据表近3个月
CREATE TABLE order_hot_0 LIKE order_0;

-- 冷数据表3个月以上
CREATE TABLE order_cold_0 LIKE order_0;

-- 定时任务迁移冷数据

4. 读写分离

spring:
  shardingsphere:
    rules:
      readwrite-splitting:
        data-sources:
          ds0:
            write-data-source-name: ds0-master
            read-data-source-names:
              - ds0-slave1
              - ds0-slave2
            load-balancer-name: round-robin

总结

推荐方案

订单号包含 userNo 信息 + 按 userNo 分库分表

优点:

  • 所有查询都能精确路由到单个分片
  • 无需额外的路由表
  • 性能最优
  • 实现简单

分片规则:

  • 16个库 × 16张表 = 256个分片
  • 单分片数据量10亿 / 256 ≈ 400万可控

查询性能:

  • 订单列表(带 userNo单分片查询<50ms
  • 订单详情orderNo单分片查询<10ms
  • 凭证查询:单分片查询,<20ms
  • 支付单查询:单分片查询,<20ms

这个方案在大数据量场景下能保证高性能和可扩展性。