关注

SpringBoot3集成ShardingSphere分库分表实战指南,告别单表瓶颈

做后端开发的同学应该都有同感,业务跑起来之后,数据量涨得比预期快得多。我们项目的订单模块刚上线时单库单表跑得好好的,最近半年订单量翻了几倍,查询卡顿、写入延迟的问题越来越明显,排查下来单表数据量过大是核心原因。

趁着版本迭代,直接上手用SpringBoot3整合ShardingSphere-JDBC做水平分库分表,踩了不少版本适配和配置写法的坑,也摸透了5.x版本的实战规范。本文从库表搭建、依赖引入、核心配置到测试验证,一步一步写清楚,看完直接复制代码就能跑通,适合直接落地到实际项目里。

一、为什么需要分库分表?

从性能方面来说,由于关系型数据库大多采用B+树类型的索引,在数据量超过阈值的情况下,索引深度的增加也将使得磁盘访问的IO次数增加,进而导致查询性能的下降;同时,高并发访问请求也使得集中式数据库成为系统的最大瓶颈。

从可用性的方面来讲,服务化的无状态型,能够达到较小成本的随意扩容,这必然导致系统的最终压力都落在数据库之上。而单一的数据节点,或者简单的主从架构,已经越来越难以承担。

当一个数据库实例中的数据达到阈值以上,对于DBA的运维压力就会增大。一般来讲,单一数据库实例的数据的阈值在1TB之内,是比较合理的范围。

二、ShardingSphere核心概念

ShardingSphere是一套开源的分布式数据库中间件解决方案组成的生态圈,它由Sharding-JDBC、Sharding-Proxy和Sharding-Sidecar这3款相互独立的产品组成。

ShardingSphere-JDBC:定位为轻量级Java框架,在Java的JDBC层提供的额外服务。它使用客户端直连数据库,以jar包形式提供服务,无需额外部署和依赖,可理解为增强版的JDBC驱动,完全兼容JDBC和各种ORM框架。本项目采用这种方式。

ShardingSphere-Proxy:定位为透明化的数据库代理端,提供封装了数据库二进制协议的服务端版本,可用于异构语言支持。

本次实战的核心版本:

  • Spring Boot:3.2.5

  • ShardingSphere-JDBC:5.5.2

  • MyBatis-Plus:3.5.15

  • Java:17

  • MySQL:8.0

三、实战场景:按用户ID分库分表

本次实战场景:按用户ID取模实现水平分库分表,搭建2个分库(ds0、ds1),每个库对应2张订单物理表(t_order_0、t_order_1),总共4张表分散数据压力,代码层无感操作,无需改动原有业务逻辑。

3.1 前期准备:手动创建分库分表

实战中一定要先把物理库表搭建到位,而且所有分表结构必须完全一致,这是分库分表正常运行的基础。

-- 1. 创建2个分库
CREATE DATABASE IF NOT EXISTS order_db_0;
CREATE DATABASE IF NOT EXISTS order_db_1;

-- 2. 在order_db_0中创建分表
USE order_db_0;

CREATE TABLE `t_order_0` (
  `id` bigint NOT NULL COMMENT '订单主键(分布式ID)',
  `order_no` varchar(64) NOT NULL COMMENT '订单编号',
  `user_id` bigint NOT NULL COMMENT '用户ID(分片键)',
  `goods_name` varchar(255) DEFAULT NULL COMMENT '商品名称',
  `order_amount` decimal(10,2) DEFAULT NULL COMMENT '订单金额',
  `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='订单表';

-- 直接复制表结构,保证完全一致
CREATE TABLE `t_order_1` LIKE `t_order_0`;

-- 3. 在order_db_1中创建分表(表结构完全一致)
USE order_db_1;
CREATE TABLE `t_order_0` LIKE order_db_0.t_order_0;
CREATE TABLE `t_order_1` LIKE order_db_0.t_order_0;

3.2 SpringBoot3项目搭建,引入正确核心依赖

新建普通SpringBoot3项目,以下是适配SpringBoot3的完整实测依赖:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 
         https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.2.5</version>
        <relativePath/>
    </parent>
    
    <groupId>com.example</groupId>
    <artifactId>shardingsphere-demo</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>shardingsphere-demo</name>
    <description>SpringBoot3+ShardingSphere-JDBC分库分表实战</description>
    
    <properties>
        <java.version>17</java.version>
        <shardingsphere.version>5.5.2</shardingsphere.version>
        <mybatis-plus.version>3.5.15</mybatis-plus.version>
    </properties>
    
    <dependencies>
        <!-- SpringBoot Web 基础依赖 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        
        <!-- ShardingSphere-JDBC 核心依赖 -->
        <dependency>
            <groupId>org.apache.shardingsphere</groupId>
            <artifactId>shardingsphere-jdbc</artifactId>
            <version>${shardingsphere.version}</version>
        </dependency>
        
        <!-- MySQL8 官方驱动 -->
        <dependency>
            <groupId>com.mysql</groupId>
            <artifactId>mysql-connector-j</artifactId>
            <scope>runtime</scope>
        </dependency>
        
        <!-- MyBatis-Plus SpringBoot3 适配版 -->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-spring-boot3-starter</artifactId>
            <version>${mybatis-plus.version}</version>
        </dependency>
        
        <!-- 常用工具包 -->
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.8.25</version>
        </dependency>
        
        <!-- Lombok 简化实体类代码 -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        
        <!-- 测试依赖 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>
    
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <excludes>
                        <exclude>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                        </exclude>
                    </excludes>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

3.3 核心配置:分库分表规则

ShardingSphere 5.x版本推荐使用分离式配置,把核心数据源和分片规则单独抽离成配置文件,相比单yml内嵌配置,加载更稳定、可读性更强,也能彻底避免因配置嵌套缩进导致的规则加载失败问题。

application.yml(放在resources目录下):

spring:
  application:
    name: shardingsphere-demo
  
  # 关闭自动配置的数据源,使用ShardingSphere的数据源
  autoconfigure:
    exclude: com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceAutoConfigure
  
  # ShardingSphere配置
  shardingsphere:
    datasource:
      names: ds0, ds1
      ds0:
        type: com.zaxxer.hikari.HikariDataSource
        driver-class-name: com.mysql.cj.jdbc.Driver
        jdbc-url: jdbc:mysql://localhost:3306/order_db_0?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
        username: root
        password: 123456
      ds1:
        type: com.zaxxer.hikari.HikariDataSource
        driver-class-name: com.mysql.cj.jdbc.Driver
        jdbc-url: jdbc:mysql://localhost:3306/order_db_1?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
        username: root
        password: 123456
    
    rules:
      sharding:
        tables:
          t_order:
            actual-data-nodes: ds$->{0..1}.t_order_$->{0..1}
            database-strategy:
              standard:
                sharding-column: user_id
                sharding-algorithm-name: database-inline
            table-strategy:
              standard:
                sharding-column: user_id
                sharding-algorithm-name: table-inline
            key-generate-strategy:
              column: id
              key-generator-name: snowflake
        
        sharding-algorithms:
          database-inline:
            type: INLINE
            props:
              algorithm-expression: ds$->{user_id % 2}
          table-inline:
            type: INLINE
            props:
              algorithm-expression: t_order_$->{user_id % 2}
        
        key-generators:
          snowflake:
            type: SNOWFLAKE
    
    props:
      sql-show: true  # 开启SQL日志,在控制台打印实际执行的SQL语句,便于调试

配置要点说明:

  • actual-data-nodesds$->{0..1}.t_order_$->{0..1} 表示数据分布在ds0、ds1两个库,每个库的t_order_0、t_order_1两张表

  • 分库策略:根据user_id % 2的结果,决定数据进入ds0还是ds1

  • 分表策略:根据user_id % 2的结果,决定数据进入t_order_0还是t_order_1

  • 主键生成策略:使用雪花算法生成分布式ID,避免分表后自增ID冲突

四、业务代码编写:和单表操作完全一致

ShardingSphere-JDBC最大的优势就是无侵入性,不需要修改原有业务代码,照常编写实体类、Mapper、Service、Controller即可,和操作单表流程完全一样。

4.1 订单实体类(对应逻辑表t_order)

package com.example.shardingspheredemo.entity;

import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;

import java.math.BigDecimal;
import java.time.LocalDateTime;

/**
 * 订单实体类
 * 仅对应逻辑表t_order,无需关注物理分表
 */
@Data
@TableName("t_order")  // 逻辑表名
public class Order {
    private Long id;              // 分布式ID,由ShardingSphere自动生成
    private String orderNo;        // 订单编号
    private Long userId;           // 用户ID(分片键)
    private String goodsName;      // 商品名称
    private BigDecimal orderAmount; // 订单金额
    private LocalDateTime createTime; // 创建时间
}

4.2 Mapper层

package com.example.shardingspheredemo.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.example.shardingspheredemo.entity.Order;
import org.apache.ibatis.annotations.Mapper;

@Mapper
public interface OrderMapper extends BaseMapper<Order> {
}

4.3 Service层

package com.example.shardingspheredemo.service;

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.example.shardingspheredemo.entity.Order;
import com.example.shardingspheredemo.mapper.OrderMapper;
import org.springframework.stereotype.Service;

@Service
public class OrderService extends ServiceImpl<OrderMapper, Order> {
}

4.4 Controller测试接口

package com.example.shardingspheredemo.controller;

import cn.hutool.core.util.IdUtil;
import com.example.shardingspheredemo.entity.Order;
import com.example.shardingspheredemo.service.OrderService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import java.math.BigDecimal;
import java.time.LocalDateTime;

@RestController
@RequestMapping("/order")
public class OrderController {

    @Autowired
    private OrderService orderService;

    /**
     * 新增订单测试接口
     * @param userId 用户ID(分片键)
     */
    @GetMapping("/add")
    public String addOrder(Long userId) {
        Order order = new Order();
        // ID由ShardingSphere的雪花算法自动生成,无需手动设置
        order.setOrderNo(IdUtil.fastSimpleUUID());  // 使用Hutool生成简单订单号
        order.setUserId(userId);
        order.setGoodsName("测试商品-" + System.currentTimeMillis());
        order.setOrderAmount(new BigDecimal("199.00"));
        order.setCreateTime(LocalDateTime.now());
        
        orderService.save(order);
        return "订单添加成功,用户ID: " + userId;
    }

    /**
     * 根据用户ID查询订单(带分片键查询,能精确路由)
     */
    @GetMapping("/list")
    public Object listOrders(Long userId) {
        return orderService.lambdaQuery()
                .eq(Order::getUserId, userId)
                .list();
    }

    /**
     * 查询所有订单(不带分片键,会全库全表扫描)
     */
    @GetMapping("/all")
    public Object listAll() {
        return orderService.list();
    }
}

五、实战测试:验证分库分表生效

启动项目,调用测试接口,通过控制台日志判断路由是否生效。

5.1 测试接口调用

# 新增订单接口1:userId=1001(1001 % 2 = 1,路由至ds1库t_order_1表)
GET http://localhost:8080/order/add?userId=1001

# 新增订单接口2:userId=1002(1002 % 2 = 0,路由至ds0库t_order_0表)
GET http://localhost:8080/order/add?userId=1002

# 新增订单接口3:userId=1003(1003 % 2 = 1,路由至ds1库t_order_1表)
GET http://localhost:8080/order/add?userId=1003

# 新增订单接口4:userId=1004(1004 % 2 = 0,路由至ds0库t_order_0表)
GET http://localhost:8080/order/add?userId=1004

5.2 查看控制台日志

开启SQL日志后,控制台会清晰打印逻辑SQL和实际执行的物理SQL,能直观看到路由结果:

Logic SQL: INSERT INTO t_order ( order_no, user_id, goods_name, order_amount, create_time ) VALUES ( ?, ?, ?, ?, ? )
Actual SQL: ds1 ::: INSERT INTO t_order_1 (id, order_no, user_id, goods_name, order_amount, create_time) VALUES (?, ?, ?, ?, ?, ?) 
Actual SQL: ds0 ::: INSERT INTO t_order_0 (id, order_no, user_id, goods_name, order_amount, create_time) VALUES (?, ?, ?, ?, ?, ?) 

5.3 数据库数据校验

连接数据库查看,数据会按照user_id取模规则,精准分散到对应的物理库表中,不会集中在单表,分库分表配置生效。

order_db_0.t_order_0表:存放userId为偶数的订单(1002、1004...)
order_db_0.t_order_1表:暂无数据(userId偶数且取模2=0,不会进这里)
order_db_1.t_order_1表:存放userId为奇数的订单(1001、1003...)
order_db_1.t_order_0表:暂无数据(userId奇数且取模2=1,不会进这里)

5.4 不带分片键查询测试

调用查询所有订单接口:

GET http://localhost:8080/order/all

控制台会显示该查询路由到所有分片表执行,即执行了4次查询(ds0.t_order_0、ds0.t_order_1、ds1.t_order_0、ds1.t_order_1),然后将结果合并返回。

六、分片策略详解

6.1 常用分片算法

ShardingSphere提供了多种分片算法:

算法类型说明适用场景
INLINE基于Groovy表达式,如 ds$->{user_id % 2}简单的取模、条件判断
MOD取模分片,如 user_id % 4数据分布均匀的场景
HASH_MOD哈希取模,先计算哈希值再取模分片键不是数字类型时
RANGE_MOD范围+取模结合需要范围查询的场景
INTERVAL按时间间隔分片,如按月分表日志、订单等时间序列表
COMPLEX复合分片,支持多个分片键复杂业务规则

6.2 常见分片键选择原则

分片键的选择直接影响分片效果和查询性能:

  • 分布均匀:选择分布均匀的字段,如用户ID、订单ID,避免用枚举、状态字段防止数据倾斜

  • 频繁查询:尽量用经常作为查询条件的字段做分片键

  • 不变性:分片键的值一旦确定不应修改,否则数据迁移成本高

  • 函数处理:查询时分片字段不要使用函数处理,否则分片算法失效导致全节点查询

七、分布式ID生成方案

分库分表后,数据库自增主键无法保证全局唯一,必须使用分布式ID。ShardingSphere内置了多种分布式ID生成器:

7.1 雪花算法(Snowflake)

spring:
  shardingsphere:
    rules:
      sharding:
        key-generators:
          snowflake:
            type: SNOWFLAKE
            props:
              worker-id: 1  # 机器ID,集群环境下每个节点不同

雪花算法生成的ID是一个64位的整数,包括时间戳、数据中心ID、机器ID和序列号,能够保证全局唯一且有序递增。

7.2 UUID

key-generators:
  uuid:
    type: UUID

UUID能保证全局唯一,但不是有序递增,会影响索引性能,不建议作为主键。

八、实战避坑总结

8.1 物理表必须统一

所有分库分表字段、类型、索引必须完全一致,不允许差异化,否则会导致查询写入报错。

8.2 分片键不要使用函数

查询时分片字段不要使用函数处理,否则分片算法失效导致全节点查询:

-- 错误示例:对分片键使用函数,导致全表扫描
SELECT * FROM t_order WHERE DATE_FORMAT(create_time, '%Y%m%d') = '20231001';

-- 正确做法:直接使用分片键
SELECT * FROM t_order WHERE create_time BETWEEN '2023-10-01 00:00:00' AND '2023-10-01 23:59:59';

8.3 主键不能用数据库自增

必须使用分布式ID(雪花算法、UUID等),禁用数据库自增,避免分表主键冲突。

8.4 避免跨库关联查询

分库分表后,跨库的关联查询性能较差,建议在应用层做数据处理,或使用字段冗余设计。

8.5 事务问题

跨库事务需要分布式事务支持,ShardingSphere集成了Seata、XA等分布式事务方案,但会带来性能损耗,尽量设计业务避免跨库事务。

8.6 dynamic-datasource不兼容

如果项目中同时使用了dynamic-datasource-spring-boot-starter动态数据源框架,需要排除,两者不兼容。

九、性能优化进阶建议

除分库分表外,还需配合以下优化措施:

  1. 建立合理的索引结构:根据查询模式创建索引

  2. 优化SQL语句编写:避免SELECT *,只查询必要字段

  3. 引入多级缓存机制:Redis缓存热点数据

  4. 实施读写分离方案:主库写、从库读,分散压力

  5. 分页查询优化:带分片键的分页性能更好,避免深分页

十、总结

SpringBoot3集成ShardingSphere-JDBC 5.5.2实现分库分表,核心就是找准正确依赖、写清分片规则,全程实操下来没有复杂原理,对业务代码零侵入,上手难度极低。

单表数据量过大导致的性能问题,通过水平分库分表能快速缓解,适配大部分中小业务场景。后续如果业务扩容,还可以基于这套规范,扩展更多分库分表、读写分离等功能,适配更大的数据量需求。

ShardingSphere作为Java世界中最成熟、最活跃的分库分表中间件,适合绝大多数业务场景,尤其适合Spring Boot用户接入。希望通过本文的实战指南,能帮助你在项目中顺利应用分库分表,彻底告别单表瓶颈。

转载自CSDN-专业IT技术社区

原文链接:https://blog.csdn.net/peraglobal/article/details/159042733

评论

赞0

评论列表

微信小程序
QQ小程序

关于作者

点赞数:0
关注数:0
粉丝:0
文章:0
关注标签:0
加入于:--