随着业务的发展,单表数据量突破千万级后,查询性能会急剧下降。这时候,分库分表就成了必不可少的解决方案。本文将基于Apache ShardingSphere 5.x版本,通过一个完整的订单系统示例,详细讲解如何实现分库分表,包括订单的增删改查操作。
在这里插入图片描述

一、ShardingSphere简介

Apache ShardingSphere是一套开源的分布式数据库中间件解决方案,由JDBC、Proxy和Sidecar(规划中)这3款既能够独立部署,又支持混合部署配合使用的产品组成。它们均提供标准化的数据分片、分布式事务和数据库治理功能,可适用于如Java同构、异构语言、云原生等各种多样化的应用场景。

核心特性
数据分片:支持分库分表,提供多种分片策略
读写分离:支持多种读写分离策略
分布式事务:提供XA和BASE两种分布式事务解决方案
数据库治理:配置动态化、数据脱敏、SQL监控等

二、分库分表方案设计

2.1 分片策略
在订单系统中,我们采用以下分片策略:

分库策略:根据user_id进行取模运算

user_id % 2 = 0 → demo_ds_0
user_id % 2 = 1 → demo_ds_1

分表策略:根据order_id进行取模运算

order_id % 2 = 0 → t_order_0 / t_order_item_0
order_id % 2 = 1 → t_order_1 / t_order_item_1

2.2 表结构设计
在这里插入图片描述

订单表(t_order)

CREATE TABLE`t_order_0` (
`order_id`BIGINTNOTNULLCOMMENT'订单ID',
`user_id`BIGINTNOTNULLCOMMENT'用户ID',
`order_no`VARCHAR(64) NOTNULLCOMMENT'订单编号',
`total_amount`DECIMAL(10,2) NOTNULLCOMMENT'订单总金额',
`status`TINYINTNOTNULLDEFAULT0COMMENT'订单状态',
`address`VARCHAR(255) NOTNULLCOMMENT'收货地址',
`phone`VARCHAR(20) NOTNULLCOMMENT'联系电话',
`receiver_name`VARCHAR(64) NOTNULLCOMMENT'收货人',
`create_time` DATETIME NOTNULLDEFAULTCURRENT_TIMESTAMP,
`update_time` DATETIME NOTNULLDEFAULTCURRENT_TIMESTAMPONUPDATECURRENT_TIMESTAMP,
  PRIMARY KEY (`order_id`),
KEY`idx_user_id` (`user_id`)
) ENGINE=InnoDBDEFAULTCHARSET=utf8mb4;

订单项表(t_order_item)

CREATE TABLE`t_order_item_0` (
`item_id`BIGINTNOTNULLCOMMENT'订单项ID',
`order_id`BIGINTNOTNULLCOMMENT'订单ID',
`product_id`BIGINTNOTNULLCOMMENT'商品ID',
`product_name`VARCHAR(128) NOTNULLCOMMENT'商品名称',
`unit_price`DECIMAL(10,2) NOTNULLCOMMENT'商品单价',
`quantity`INTNOTNULLCOMMENT'购买数量',
`total_price`DECIMAL(10,2) NOTNULLCOMMENT'小计金额',
  PRIMARY KEY (`item_id`),
KEY`idx_order_id` (`order_id`)
) ENGINE=InnoDBDEFAULTCHARSET=utf8mb4;

2.3 绑定表配置
订单表和订单项表配置为绑定表,这样在关联查询时会路由到同一个分片,避免跨库Join:

sharding:
  binding-tables:
    - t_order,t_order_item

三、项目搭建

3.1 Maven依赖

<properties>
    <sharding-sphere.version>5.3.2</sharding-sphere.version>
    <mybatis-plus.version>3.5.3.1</mybatis-plus.version>
</properties>
<dependencies>
    <!-- ShardingSphere-JDBC -->
    <dependency>
        <groupId>org.apache.shardingsphere</groupId>
        <artifactId>shardingsphere-jdbc-core-spring-boot-starter</artifactId>
        <version>${sharding-sphere.version}</version>
    </dependency>

    <!-- MyBatis-Plus -->
    <dependency>
        <groupId>com.baomidou</groupId>
        <artifactId>mybatis-plus-boot-starter</artifactId>
        <version>${mybatis-plus.version}</version>
    </dependency>
</dependencies>

3.2 核心配置

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/demo_ds_0?useSSL=false&serverTimezone=Asia/Shanghai
        username:root
        password:root
      ds1:
        type:com.alibaba.druid.pool.DruidDataSource
        driver-class-name:com.mysql.cj.jdbc.Driver
        url:jdbc:mysql://localhost:3306/demo_ds_1?useSSL=false&serverTimezone=Asia/Shanghai
        username:root
        password:root

    # 分片规则配置
    rules:
      sharding:
        # 绑定表
        binding-tables:
          -t_order,t_order_item

        # 分片表配置
        tables:
          t_order:
            actual-data-nodes:ds$->{0..1}.t_order_$->{0..1}
            database-strategy:
              standard:
                sharding-column:user_id
                sharding-algorithm-name:db-mod
            table-strategy:
              standard:
                sharding-column:order_id
                sharding-algorithm-name:table-mod

          t_order_item:
            actual-data-nodes:ds$->{0..1}.t_order_item_$->{0..1}
            database-strategy:
              standard:
                sharding-column:order_id
                sharding-algorithm-name:order-db-mod
            table-strategy:
              standard:
                sharding-column:order_id
                sharding-algorithm-name:table-mod

        # 分片算法
        sharding-algorithms:
          db-mod:
            type:MOD
            props:
              sharding-count:2
          table-mod:
            type:MOD
            props:
              sharding-count:2

四、订单CRUD操作实现

4.1 实体类定义

@Data
@Builder
@TableName("t_order")
public class Order {
    @TableId(type = IdType.ASSIGN_ID)
    private Long orderId;

    private Long userId;
    private String orderNo;
    private BigDecimal totalAmount;
    private Integer status;
    private String address;
    private String phone;
    private String receiverName;
    private LocalDateTime createTime;
    private LocalDateTime updateTime;
}

4.2 创建订单
在这里插入图片描述

Service实现

@Transactional(rollbackFor = Exception.class)
public Long createOrder(Long userId, BigDecimal totalAmount,
                       String address, String phone, String receiverName) {
    // 构建订单对象
    Order order = Order.builder()
            .userId(userId)
            .orderNo(generateOrderNo())
            .totalAmount(totalAmount)
            .status(0) // 待支付
            .address(address)
            .phone(phone)
            .receiverName(receiverName)
            .createTime(LocalDateTime.now())
            .updateTime(LocalDateTime.now())
            .build();

    // 保存订单 - ShardingSphere会根据user_id进行分库
    orderMapper.insert(order);

    log.info("订单创建成功,订单ID: {}, 路由到: ds{}.t_order_{}",
            order.getOrderId(),
            userId % 2,
            order.getOrderId() % 2);

    return order.getOrderId();
}

路由过程:

SQL解析:解析INSERT语句,提取分片键user_id
SQL路由:计算user_id % 2,确定目标数据库
SQL改写:将逻辑表名t_order改为物理表名t_order_0或t_order_1
SQL执行:在目标分片执行INSERT操作
结果归并:返回执行结果

4.3 查询订单
根据订单ID精确查询

public Order getOrderByOrderId(Long orderId) {
    // 根据order_id精确路由
    Order order = orderMapper.selectById(orderId);

    if (order != null) {
        log.info("查询成功,订单所在分片: ds{}.t_order_{}",
                order.getUserId() % 2,
                orderId % 2);
    }

    return order;
}

根据用户ID查询订单列表

public List<Order> getOrdersByUserId(Long userId) {
    // ShardingSphere会根据user_id路由到对应数据库
    // 然后查询该库下所有分表
    LambdaQueryWrapper<Order> wrapper = new LambdaQueryWrapper<>();
    wrapper.eq(Order::getUserId, userId)
            .orderByDesc(Order::getCreateTime);

    return orderMapper.selectList(wrapper);
}

在这里插入图片描述

4.4 更新订单

@Transactional(rollbackFor = Exception.class)
public boolean updateOrderStatus(Long orderId, Integer status) {
    Order order = Order.builder()
            .orderId(orderId)
            .status(status)
            .updateTime(LocalDateTime.now())
            .build();

    int rows = orderMapper.updateById(order);

    log.info("更新完成,影响行数: {}", rows);

    return rows > 0;
}

4.5 删除订单

@Transactional(rollbackFor = Exception.class)
public boolean deleteOrder(Long orderId) {
    // 先查询订单信息,确定分片
    Order order = orderMapper.selectById(orderId);
    if (order == null) {
        returnfalse;
    }

    // 删除订单项
    orderItemService.deleteByOrderId(orderId);

    // 删除订单
    int rows = orderMapper.deleteById(orderId);

    return rows > 0;
}

在这里插入图片描述

五、分片算法详解

5.1 MOD取模算法
最常用的分片算法,计算简单,数据分布均匀:

table-mod:
  type: MOD
  props:
    sharding-count: 2  # 分片数量

5.2 INLINE行表达式算法
支持复杂分片规则:

inline-strategy:
  type: INLINE
  props:
    algorithm-expression: ds_${user_id % 2}

5.3 STANDARD标准分片算法
支持自定义分片规则:

public class ModShardingAlgorithm implements StandardShardingAlgorithm<Long> {

    @Override
    public String doSharding(Collection<String> availableTargetNames,
                           PreciseShardingValue<Long> shardingValue) {
        Long value = shardingValue.getValue();
        int index = (int) (value % 2);
        for (String name : availableTargetNames) {
            if (name.endsWith(String.valueOf(index))) {
                return name;
            }
        }
        thrownew IllegalArgumentException("无法路由到目标数据源");
    }
}

六、Docker一键部署

在这里插入图片描述

6.1 docker-compose.yml

version: '3.8'

services:
mysql-ds0:
    image:mysql:8.0
    container_name:sharding-mysql-ds0
    environment:
      MYSQL_ROOT_PASSWORD:root
      MYSQL_DATABASE:demo_ds_0
    ports:
      -"3307:3306"
    volumes:
      -./init-sql:/docker-entrypoint-initdb.d

mysql-ds1:
    image:mysql:8.0
    container_name:sharding-mysql-ds1
    environment:
      MYSQL_ROOT_PASSWORD:root
      MYSQL_DATABASE:demo_ds_1
    ports:
      -"3308:3306"
    volumes:
      -./init-sql:/docker-entrypoint-initdb.d

sharding-app:
    build:.
    container_name:shardingphere-app
    ports:
      -"8080:8080"
    depends_on:
      -mysql-ds0
      -mysql-ds1

七、生产实践

7.1 分片键选择
在这里插入图片描述

选择原则:

选择查询频率高的字段作为分片键
避免使用随机性强的字段
考虑数据增长趋势和分布

常见方案:

用户表:user_id
订单表:user_id(分库)+ order_id(分表)
商品表:category_id + product_id

7.2 性能优化
1. 使用广播表

broadcast-tables:
  - t_config  # 配置表,每个库都保存完整数据

2. 避免全路由查询

// 不推荐:查询所有订单
List<Order> all = orderMapper.selectList(null);

// 推荐:指定分片键查询
List<Order> orders = orderMapper.selectList(
    new LambdaQueryWrapper<Order>()
        .eq(Order::getUserId, 1001L)
);

3. 合理使用绑定表将经常关联查询的表配置为绑定表,避免跨库Join:

binding-tables:
  - t_order,t_order_item
  - t_user,t_user_profile

7.3 监控告警
启用SQL日志监控慢查询:

spring:
  shardingsphere:
    props:
      sql-show: true  # 显示SQL

八、总结

本文通过一个完整的订单系统示例,详细讲解了ShardingSphere分库分表的实现方案,包括:
分库分表方案设计
ShardingSphere配置与使用
订单CRUD操作实现
Docker容器化部署
生产环境最佳实践

核心要点:
合理选择分片键是关键
利用绑定表避免跨库查询
生产环境要做好容量规划
监控和性能优化不可忽视

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐