使用shardingsphere实现分库分表和读写分离教程
作者:gb4215287
组件信息:
ShardingSphere-JDBC 4.1.1 + Spring Boot 2.5.10 + MyBatis-Plus 3.4.2 + MySQL 8.0.33的分库分表示例,所有代码都在com.shardingdatabasetable.shardingsphere包下。
一、项目完整结构
sharding-jdbc-demo/
├── pom.xml
├── src/main/java/com/shardingdatabasetable/shardingsphere/
│ ├── ShardingJdbcApplication.java
│ ├── entity/
│ │ └── Order.java
│ ├── mapper/
│ │ ├── OrderMapper.java
│ │ └── xml/
│ │ └── OrderMapper.xml
│ ├── service/
│ │ ├── OrderService.java
│ │ └── impl/
│ │ └── OrderServiceImpl.java
│ └── controller/
│ └── OrderController.java
├── src/main/resources/
│ ├── application.yml
│ └── mapper/
│ └── OrderMapper.xml (如果放在这里,需要配置路径)
└── src/test/java/com/shardingdatabasetable/shardingsphere/
└── ShardingJdbcTest.java二、pom.xml 完整配置
<?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
http://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>2.5.10</version>
<relativePath/>
</parent>
<groupId>com.example</groupId>
<artifactId>sharding-jdbc-demo</artifactId>
<version>1.0.0</version>
<name>sharding-jdbc-demo</name>
<description>ShardingSphere-JDBC 4.1.1 分库分表示例</description>
<properties>
<java.version>1.8</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<sharding-sphere.version>4.1.1</sharding-sphere.version>
<druid.version>1.2.8</druid.version>
<mybatis-plus.version>3.4.2</mybatis-plus.version>
<mysql.version>8.0.33</mysql.version>
<druid.version>1.1.18</druid.version>
</properties>
<dependencies>
<!-- Spring Boot Web Starter -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Spring Boot Test Starter -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- ShardingSphere-JDBC Spring Boot Starter -->
<dependency>
<groupId>org.apache.shardingsphere</groupId>
<artifactId>sharding-jdbc-spring-boot-starter</artifactId>
<version>${sharding-sphere.version}</version>
</dependency>
<!-- MyBatis-Plus Spring Boot Starter -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>${mybatis-plus.version}</version>
</dependency>
<!-- Druid 连接池 Spring Boot Starter -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>${druid.version}</version>
</dependency>
<!-- MySQL 驱动 (适配 MySQL 8.0.33) -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>${mysql.version}</version>
<scope>runtime</scope>
</dependency>
<!-- Lombok 简化代码 -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- Groovy 用于分片算法表达式 -->
<dependency>
<groupId>org.codehaus.groovy</groupId>
<artifactId>groovy</artifactId>
<version>2.4.5</version>
</dependency>
<!-- JUnit 4 (Spring Boot 2.5.x 默认兼容) -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
<!-- fastjson -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.83</version>
</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>
<resources>
<!-- 确保 resources 目录下的文件都被打包 -->
<resource>
<directory>src/main/resources</directory>
<includes>
<include>**/*.*</include>
</includes>
</resource>
<!-- 确保 java 目录下的 xml 文件也被打包(如果放在这里) -->
<resource>
<directory>src/main/java</directory>
<includes>
<include>**/*.xml</include>
</includes>
</resource>
</resources>
</build>
</project>三、数据库初始化脚本 (MySQL 8.0.33)
3.1 创建数据库
-- 创建两个分库,使用 MySQL 8.0 的字符集和排序规则 CREATE DATABASE IF NOT EXISTS `ds0` DEFAULT CHARACTER SET utf8mb4 DEFAULT COLLATE utf8mb4_0900_ai_ci; CREATE DATABASE IF NOT EXISTS `ds1` DEFAULT CHARACTER SET utf8mb4 DEFAULT COLLATE utf8mb4_0900_ai_ci; -- 创建用户并授权(如果需要) -- CREATE USER 'fund_center'@'%' IDENTIFIED BY '123456'; -- GRANT ALL PRIVILEGES ON ds0.* TO 'fund_center'@'%'; -- GRANT ALL PRIVILEGES ON ds1.* TO 'fund_center'@'%'; -- FLUSH PRIVILEGES;
3.2 在 ds0 库中创建分表
-- 在 ds0 库中执行
USE ds0;
-- 创建 t_order_0
CREATE TABLE IF NOT EXISTS `t_order_0` (
`order_id` BIGINT NOT NULL COMMENT '订单ID,主键(雪花算法生成)',
`user_id` BIGINT NOT NULL COMMENT '用户ID(分片键)',
`order_no` VARCHAR(32) NOT NULL COMMENT '订单编号',
`amount` DECIMAL(10,2) NOT NULL DEFAULT 0.00 COMMENT '订单金额',
`status` TINYINT NOT NULL DEFAULT 0 COMMENT '订单状态:0-待支付,1-已支付,2-已取消',
`create_time` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间(毫秒精度)',
PRIMARY KEY (`order_id`),
INDEX `idx_user_id` (`user_id`),
INDEX `idx_create_time` (`create_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='订单表_0';
-- 创建 t_order_1
CREATE TABLE IF NOT EXISTS `t_order_1` (
`order_id` BIGINT NOT NULL COMMENT '订单ID,主键(雪花算法生成)',
`user_id` BIGINT NOT NULL COMMENT '用户ID(分片键)',
`order_no` VARCHAR(32) NOT NULL COMMENT '订单编号',
`amount` DECIMAL(10,2) NOT NULL DEFAULT 0.00 COMMENT '订单金额',
`status` TINYINT NOT NULL DEFAULT 0 COMMENT '订单状态:0-待支付,1-已支付,2-已取消',
`create_time` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间(毫秒精度)',
PRIMARY KEY (`order_id`),
INDEX `idx_user_id` (`user_id`),
INDEX `idx_create_time` (`create_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='订单表_1';3.3 在 ds1 库中创建分表
-- 在 ds1 库中执行
USE ds1;
-- 创建 t_order_0
CREATE TABLE IF NOT EXISTS `t_order_0` (
`order_id` BIGINT NOT NULL COMMENT '订单ID,主键(雪花算法生成)',
`user_id` BIGINT NOT NULL COMMENT '用户ID(分片键)',
`order_no` VARCHAR(32) NOT NULL COMMENT '订单编号',
`amount` DECIMAL(10,2) NOT NULL DEFAULT 0.00 COMMENT '订单金额',
`status` TINYINT NOT NULL DEFAULT 0 COMMENT '订单状态:0-待支付,1-已支付,2-已取消',
`create_time` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间(毫秒精度)',
PRIMARY KEY (`order_id`),
INDEX `idx_user_id` (`user_id`),
INDEX `idx_create_time` (`create_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='订单表_0';
-- 创建 t_order_1
CREATE TABLE IF NOT EXISTS `t_order_1` (
`order_id` BIGINT NOT NULL COMMENT '订单ID,主键(雪花算法生成)',
`user_id` BIGINT NOT NULL COMMENT '用户ID(分片键)',
`order_no` VARCHAR(32) NOT NULL COMMENT '订单编号',
`amount` DECIMAL(10,2) NOT NULL DEFAULT 0.00 COMMENT '订单金额',
`status` TINYINT NOT NULL DEFAULT 0 COMMENT '订单状态:0-待支付,1-已支付,2-已取消',
`create_time` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间(毫秒精度)',
PRIMARY KEY (`order_id`),
INDEX `idx_user_id` (`user_id`),
INDEX `idx_create_time` (`create_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='订单表_1';四、application.yml 完整配置
server:
port: 8006
servlet:
context-path: /
spring:
application:
name: sharding-jdbc-demo
# 关闭所有数据源相关的自动配置
autoconfigure:
exclude:
- org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration
- org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration
- org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration
- com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceAutoConfigure
main:
allow-bean-definition-overriding: true
# ShardingSphere 配置
shardingsphere:
datasource:
# 数据源名称列表 - 新增主库ds2,原来的ds0和ds1变为从库
names: ds2, ds0, ds1
# 数据源 ds2 (主库)
ds2:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://10.16.66.88:3306/ds2?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true&rewriteBatchedStatements=true&nullCatalogMeansCurrent=true
username: center
password: 123456
# Druid连接池配置
initial-size: 5
min-idle: 5
max-active: 20
max-wait: 60000
time-between-eviction-runs-millis: 60000
min-evictable-idle-time-millis: 300000
validation-query: SELECT 1
test-while-idle: true
test-on-borrow: false
test-on-return: false
pool-prepared-statements: true
max-pool-prepared-statement-per-connection-size: 20
filters: stat,wall
# 数据源 ds0 (从库1)
ds0:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://10.16.66.88:3306/ds0?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true&rewriteBatchedStatements=true&nullCatalogMeansCurrent=true
username: center
password: 123456
initial-size: 5
min-idle: 5
max-active: 20
max-wait: 60000
validation-query: SELECT 1
# 数据源 ds1 (从库2)
ds1:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://10.16.66.88:3306/ds1?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true&rewriteBatchedStatements=true&nullCatalogMeansCurrent=true
username: center
password: 123456
initial-size: 5
min-idle: 5
max-active: 20
max-wait: 60000
validation-query: SELECT 1
# 分片规则配置
sharding:
# 主从规则配置 - 新增部分 [citation:1][citation:3]
master-slave-rules:
# 主从逻辑名称,可以自定义
ms_ds:
# 指定主库数据源名称
master-data-source-name: ds2
# 指定从库数据源名称列表,多个用逗号分隔 [citation:3]
slave-data-source-names: ds0, ds1
# 从库负载均衡算法:round_robin(轮询) / random(随机) [citation:3]
load-balance-algorithm-type: round_robin
# 默认的分库策略:按user_id取模
default-database-strategy:
inline:
sharding-column: user_id
# 注意:这里用的是主从逻辑名 ms_ds,而不是具体的数据库名
algorithm-expression: ms_ds
# 绑定表
binding-tables:
- t_order
# 具体的表分片规则
tables:
# 逻辑表名
t_order:
# 实际数据节点:使用主从逻辑名,后面跟实际的物理表 [citation:6]
actual-data-nodes: ms_ds.t_order_$->{0..1}
# 分表策略:按order_id取模
table-strategy:
inline:
sharding-column: order_id
algorithm-expression: t_order_$->{order_id % 2}
# 分布式主键生成策略(雪花算法)
key-generator:
column: order_id
type: SNOWFLAKE
props:
worker.id: 1
# 默认数据源(可选)
default-data-source-name: ms_ds
# 属性配置
props:
sql:
show: true # 打印SQL日志,便于调试
executor.size: 10
mybatis-plus:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
map-underscore-to-camel-case: true
cache-enabled: false
global-config:
db-config:
id-type: none
# 日志级别配置
logging:
level:
com.shardingdatabasetable.shardingsphere.mapper: debug
org.apache.shardingsphere: info
com.alibaba.druid: info
pattern:
console: '%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n'五、Java代码实现
5.1 启动类 (ShardingJdbcApplication.java)
package com.shardingdatabasetable.shardingsphere;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration;
import org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.transaction.annotation.EnableTransactionManagement;
@SpringBootApplication(exclude = {
DataSourceAutoConfiguration.class,
DataSourceTransactionManagerAutoConfiguration.class,
HibernateJpaAutoConfiguration.class,
com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceAutoConfigure.class
})
@ComponentScan("com.shardingdatabasetable.shardingsphere")
@EnableTransactionManagement
public class ShardingJdbcApplication {
public static void main(String[] args) {
SpringApplication.run(ShardingJdbcApplication.class, args);
}
}5.2 实体类 (Order.java)
package com.shardingdatabasetable.shardingsphere.entity;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.Date;
/**
* 订单实体类
* 逻辑表名:t_order,ShardingSphere会自动路由到物理表
*/
@Data
@TableName("t_order")
public class Order {
/**
* 订单ID,使用ShardingSphere的雪花算法自动生成
* 注意:不需要@TableId注解,让ShardingSphere处理主键
*/
private Long orderId;
/**
* 用户ID(分库分表键)
*/
private Long userId;
/**
* 订单编号
*/
private String orderNo;
/**
* 订单金额
*/
private BigDecimal amount;
/**
* 订单状态:0-待支付,1-已支付,2-已取消
*/
private Integer status;
/**
* 创建时间
*/
//private LocalDateTime createTime;
private Date createTime;
}5.3 Mapper接口 (OrderMapper.java)
package com.shardingdatabasetable.shardingsphere.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.shardingdatabasetable.shardingsphere.entity.Order;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import java.util.List;
/**
* 订单Mapper接口
* 继承BaseMapper获得基础的CRUD方法
*/
@Mapper
public interface OrderMapper extends BaseMapper<Order> {
/**
* 自定义查询:根据用户ID和状态查询订单
* 使用XML文件实现
*/
List<Order> selectByUserIdAndStatus(@Param("userId") Long userId, @Param("status") Integer status);
/**
* 批量插入订单
* 使用XML文件实现
*/
int batchInsert(@Param("list") List<Order> orders);
/**
* 统计某个用户的订单总数
* 使用注解方式实现
*/
@Select("SELECT COUNT(*) FROM t_order WHERE user_id = #{userId}")
int countByUserId(@Param("userId") Long userId);
/**
* 根据用户ID列表查询订单(演示多键查询)
* 使用XML文件实现
*/
List<Order> selectByUserIds(@Param("userIds") List<Long> userIds);
}5.4 Mapper XML文件 (OrderMapper.xml)
创建在 src/main/resources/mapper/OrderMapper.xml:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.shardingdatabasetable.shardingsphere.mapper.OrderMapper">
<!-- 通用结果映射 -->
<resultMap id="BaseResultMap" type="com.shardingdatabasetable.shardingsphere.entity.Order">
<id column="order_id" property="orderId" jdbcType="BIGINT"/>
<result column="user_id" property="userId" jdbcType="BIGINT"/>
<result column="order_no" property="orderNo" jdbcType="VARCHAR"/>
<result column="amount" property="amount" jdbcType="DECIMAL"/>
<result column="status" property="status" jdbcType="TINYINT"/>
<result column="create_time" property="createTime" jdbcType="TIMESTAMP"/>
</resultMap>
<!-- 通用查询列 -->
<sql id="Base_Column_List">
order_id, user_id, order_no, amount, status, create_time
</sql>
<!-- 批量插入 -->
<insert id="batchInsert" parameterType="list">
INSERT INTO t_order (
order_id, user_id, order_no, amount, status, create_time
) VALUES
<foreach collection="list" item="item" index="index" separator=",">
(
#{item.orderId},
#{item.userId},
#{item.orderNo},
#{item.amount},
#{item.status},
#{item.createTime}
)
</foreach>
</insert>
<!-- 根据用户ID和状态查询 -->
<select id="selectByUserIdAndStatus" resultMap="BaseResultMap">
SELECT
<include refid="Base_Column_List"/>
FROM t_order
WHERE user_id = #{userId}
<if test="status != null">
AND status = #{status}
</if>
ORDER BY create_time DESC
</select>
<!-- 根据用户ID列表查询 -->
<select id="selectByUserIds" resultMap="BaseResultMap">
SELECT
<include refid="Base_Column_List"/>
FROM t_order
WHERE user_id IN
<foreach collection="userIds" item="userId" open="(" separator="," close=")">
#{userId}
</foreach>
ORDER BY user_id, create_time DESC
</select>
</mapper>5.5 Service接口 (OrderService.java)
package com.shardingdatabasetable.shardingsphere.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.shardingdatabasetable.shardingsphere.entity.Order;
import java.util.List;
/**
* 订单服务接口
*/
public interface OrderService extends IService<Order> {
/**
* 批量插入订单
* @param orders 订单列表
* @return 是否成功
*/
boolean insertBatch(List<Order> orders);
/**
* 根据用户ID查询订单列表
* @param userId 用户ID
* @return 订单列表
*/
List<Order> getOrdersByUserId(Long userId);
/**
* 复杂查询:同时使用分片键和状态
* @param userId 用户ID
* @param status 订单状态
* @return 订单列表
*/
List<Order> complexQuery(Long userId, Integer status);
/**
* 根据用户ID列表查询
* @param userIds 用户ID列表
* @return 订单列表
*/
List<Order> getOrdersByUserIds(List<Long> userIds);
/**
* 生成测试订单数据
* @param userId 用户ID
* @return 测试订单
*/
Order generateTestOrder(Long userId);
/**
* 初始化测试数据
* @param count 每个用户生成的订单数
*/
void initTestData(int count);
/**
* 获取订单的路由信息(用于展示)
*/
String getRoutingInfo(Order order);
/**
* 插入数据
*/
boolean save(Order entity);
}5.6 Service实现类 (OrderServiceImpl.java)
package com.shardingdatabasetable.shardingsphere.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.shardingdatabasetable.shardingsphere.entity.Order;
import com.shardingdatabasetable.shardingsphere.mapper.OrderMapper;
import com.shardingdatabasetable.shardingsphere.service.OrderService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.UUID;
/**
* 订单服务实现类
*/
@Slf4j
@Service
public class OrderServiceImpl extends ServiceImpl<OrderMapper, Order> implements OrderService {
@Override
@Transactional(rollbackFor = Exception.class)
public boolean insertBatch(List<Order> orders) {
if (orders == null || orders.isEmpty()) {
return false;
}
// 补全订单信息(但不设置 orderId,让 ShardingSphere 的雪花算法生成)
orders.forEach(order -> {
if (order.getOrderNo() == null) {
order.setOrderNo("ORD" + System.currentTimeMillis() +
UUID.randomUUID().toString().replace("-", "").substring(0, 4).toUpperCase());
}
if (order.getCreateTime() == null) {
order.setCreateTime(new Date());
}
// 确保 orderId 为 null,让 ShardingSphere 生成
order.setOrderId(null);
});
// 使用 MyBatis-Plus 的 saveBatch 方法
boolean success = this.saveBatch(orders);
if (success) {
log.info("批量插入成功,数量:{}", orders.size());
// 方法1:根据订单号和用户ID组合查询(最精确)
for (Order order : orders) {
LambdaQueryWrapper<Order> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(Order::getUserId, order.getUserId())
.eq(Order::getOrderNo, order.getOrderNo())
.orderByDesc(Order::getCreateTime)
.last("LIMIT 1");
Order savedOrder = this.getOne(wrapper);
if (savedOrder != null) {
// 将查询到的 orderId 设置回原对象
order.setOrderId(savedOrder.getOrderId());
// 也可以更新创建时间,如果需要
order.setCreateTime(savedOrder.getCreateTime());
}
}
// 打印路由信息
orders.forEach(order -> {
if (order.getOrderId() != null) {
log.info("插入订单 - {}", getRoutingInfo(order));
} else {
log.warn("订单 {} 的 orderId 未能获取", order.getOrderNo());
}
});
} else {
log.error("批量插入失败,数量:{}", orders.size());
}
return success;
}
@Override
public List<Order> getOrdersByUserId(Long userId) {
LambdaQueryWrapper<Order> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(Order::getUserId, userId)
.orderByDesc(Order::getCreateTime);
return list(wrapper);
}
@Override
public List<Order> complexQuery(Long userId, Integer status) {
return baseMapper.selectByUserIdAndStatus(userId, status);
}
@Override
public List<Order> getOrdersByUserIds(List<Long> userIds) {
if (userIds == null || userIds.isEmpty()) {
return new ArrayList<>();
}
return baseMapper.selectByUserIds(userIds);
}
@Override
public Order generateTestOrder(Long userId) {
Order order = new Order();
order.setUserId(userId);
order.setOrderNo("ORD" + System.currentTimeMillis());
order.setAmount(new BigDecimal(Math.random() * 1000).setScale(2, BigDecimal.ROUND_HALF_UP));
order.setStatus((int)(Math.random() * 3)); // 0,1,2随机
order.setCreateTime(new Date()); // 使用java.util.Date
return order;
}
@Override
public boolean save(Order entity) {
// 先保存
boolean result = super.save(entity);
if (result) {
// 保存成功后,根据订单号查询
LambdaQueryWrapper<Order> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(Order::getUserId, entity.getUserId())
.eq(Order::getOrderNo, entity.getOrderNo())
.orderByDesc(Order::getCreateTime)
.last("LIMIT 1");
Order savedOrder = this.getOne(wrapper);
if (savedOrder != null) {
entity.setOrderId(savedOrder.getOrderId());
entity.setCreateTime(savedOrder.getCreateTime());
}
}
return result;
}
@Override
public void initTestData(int count) {
// 测试10个用户
Long[] userIds = {1L, 2L, 3L, 4L, 5L, 6L, 7L, 8L, 9L, 10L};
List<Order> allOrders = new ArrayList<>();
for (Long userId : userIds) {
for (int i = 0; i < count; i++) {
Order order = generateTestOrder(userId);
completeOrder(order);
allOrders.add(order);
}
}
if (!allOrders.isEmpty()) {
// 分批插入,避免一次性插入太多
int batchSize = 100;
for (int i = 0; i < allOrders.size(); i += batchSize) {
int end = Math.min(i + batchSize, allOrders.size());
List<Order> batch = allOrders.subList(i, end);
baseMapper.batchInsert(batch);
}
log.info("初始化测试数据完成,共插入 {} 条订单", allOrders.size());
}
}
@Override
public String getRoutingInfo(Order order) {
String db = (order.getUserId() % 2 == 0) ? "ds0" : "ds1";
String table = (order.getOrderId() % 2 == 0) ? "t_order_0" : "t_order_1";
return String.format("%s.%s (userId: %d, orderId: %d)", db, table, order.getUserId(), order.getOrderId());
}
/**
* 补全订单信息
*/
private void completeOrder(Order order) {
if (order.getOrderNo() == null) {
order.setOrderNo(generateOrderNo());
}
if (order.getCreateTime() == null) {
//order.setCreateTime(LocalDateTime.now());
order.setCreateTime(new Date());
}
}
/**
* 生成订单号
* 格式:ORD + 时间戳 + 随机数
*/
private String generateOrderNo() {
return "ORD" + System.currentTimeMillis() +
UUID.randomUUID().toString().replace("-", "").substring(0, 8).toUpperCase();
}
}5.7 Controller (OrderController.java)
package com.shardingdatabasetable.shardingsphere.controller;
import com.shardingdatabasetable.shardingsphere.entity.Order;
import com.shardingdatabasetable.shardingsphere.service.OrderService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 订单控制器 - 提供REST API
*/
@RestController
@RequestMapping("/api/order")
public class OrderController {
@Autowired
private OrderService orderService;
/**
* 创建单个订单
*/
@PostMapping("/create")
public Map<String, Object> createOrder(@RequestParam Long userId) {
Order order = orderService.generateTestOrder(userId);
orderService.save(order);
Map<String, Object> result = new HashMap<>();
result.put("success", true);
result.put("message", "订单创建成功");
result.put("orderId", order.getOrderId());
result.put("userId", userId);
result.put("routing", orderService.getRoutingInfo(order));
return result;
}
/**
* 批量创建订单
*/
@PostMapping("/batch")
public Map<String, Object> batchCreateOrders(
@RequestParam Long userId,
@RequestParam(defaultValue = "10") Integer count) {
List<Order> orders = new java.util.ArrayList<>();
for (int i = 0; i < count; i++) {
orders.add(orderService.generateTestOrder(userId));
}
boolean success = orderService.insertBatch(orders);
Map<String, Object> result = new HashMap<>();
result.put("success", success);
result.put("message", success ? "批量创建成功" : "批量创建失败");
result.put("userId", userId);
result.put("count", count);
return result;
}
/**
* 根据用户ID查询订单
*/
@GetMapping("/user/{userId}")
public List<Order> getOrdersByUser(@PathVariable Long userId) {
return orderService.getOrdersByUserId(userId);
}
/**
* 复杂查询
*/
@GetMapping("/query")
public List<Order> complexQuery(
@RequestParam Long userId,
@RequestParam(required = false) Integer status) {
return orderService.complexQuery(userId, status);
}
/**
* 根据多个用户ID查询
*/
@PostMapping("/query/users")
public List<Order> getOrdersByUserIds(@RequestBody List<Long> userIds) {
return orderService.getOrdersByUserIds(userIds);
}
/**
* 初始化测试数据
*/
@PostMapping("/init")
public Map<String, Object> initTestData(@RequestParam(defaultValue = "5") Integer count) {
orderService.initTestData(count);
Map<String, Object> result = new HashMap<>();
result.put("success", true);
result.put("message", "测试数据初始化完成,每个用户生成 " + count + " 条订单");
return result;
}
/**
* 根据订单ID查询(演示全表扫描)
*/
@GetMapping("/{orderId}")
public Order getOrderById(@PathVariable Long orderId) {
// 注意:只根据order_id查询会进行全库全表扫描
return orderService.getById(orderId);
}
/**
* 获取数据分布统计
*/
/*@GetMapping("/distribution")
public Map<String, Object> getDistribution() {
Long[] userIds = {1L, 2L, 3L, 4L, 5L, 6L, 7L, 8L, 9L, 10L};
Map<String, Integer> stats = new HashMap<>();
for (Long userId : userIds) {
int count = orderService.countByUserId(userId);
String db = (userId % 2 == 0) ? "ds0" : "ds1";
stats.put("用户" + userId + "(" + db + ")", count);
}
Map<String, Object> result = new HashMap<>();
result.put("success", true);
result.put("stats", stats);
return result;
}*/
}六、单元测试类
6.1 测试类 (ShardingJdbcTest.java)
package com.shardingdatabasetable.shardingsphere;
import com.alibaba.fastjson.JSON;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.shardingdatabasetable.shardingsphere.entity.Order;
import com.shardingdatabasetable.shardingsphere.mapper.OrderMapper;
import com.shardingdatabasetable.shardingsphere.service.OrderService;
import lombok.extern.slf4j.Slf4j;
import net.minidev.json.JSONObject;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.transaction.annotation.Transactional;
import java.util.Arrays;
import java.util.List;
import static org.junit.Assert.*;
/**
* ShardingSphere分库分表测试类
*/
@Slf4j
@RunWith(SpringRunner.class)
@SpringBootTest
public class ShardingJdbcTest {
@Autowired
private OrderService orderService;
@Autowired
private OrderMapper orderMapper;
@Before
public void setUp() {
log.info("========== 开始测试 ==========");
}
@After
public void tearDown() {
log.info("========== 测试结束 ==========");
}
/**
* 测试1: 测试Spring上下文和Bean注入
*/
@Test
public void testContextLoads() {
log.info("测试1: 验证Bean注入");
assertNotNull("OrderService注入失败", orderService);
assertNotNull("OrderMapper注入失败", orderMapper);
log.info("✅ OrderService 注入成功: {}", orderService.getClass().getName());
log.info("✅ OrderMapper 注入成功: {}", orderMapper.getClass().getName());
}
/**
* 测试2: 测试插入单个订单
*/
@Test
public void testInsertSingleOrder() {
/**
String db = (order.getUserId() % 2 == 0) ? "ds0" : "ds1";
String table = (order.getOrderId() % 2 == 0) ? "t_order_0" : "t_order_1";
用户ID字段userId是偶数在ds0库,是奇数在ds1库。
订单ID字段(使用雪花算法自动生成)orderId是偶数在t_order_0库,是奇数在t_order_1库。
*/
log.info("测试2: 插入单个订单");
// 测试不同用户ID的路由
Long[] userIds = {1L, 2L, 3L, 4L};
for (Long userId : userIds) {
Order order = orderService.generateTestOrder(userId);
boolean saved = orderService.save(order);
assertTrue("订单保存失败", saved);
assertNotNull("订单ID不应为null", order.getOrderId());
log.info("插入订单 - {}", orderService.getRoutingInfo(order));
// 验证查询
/* System.out.println("验证查询前数据:"+order.getOrderId());
Order retrieved = orderService.getById(order.getOrderId());
assertNotNull("查询到的订单不应为null", retrieved);
assertEquals("订单ID应匹配", order.getOrderId(), retrieved.getOrderId());*/
}
}
/**
* 测试3: 测试批量插入
*/
@Test
public void testBatchInsert() {
/**
String db = (order.getUserId() % 2 == 0) ? "ds0" : "ds1";
String table = (order.getOrderId() % 2 == 0) ? "t_order_0" : "t_order_1";
用户ID字段userId是偶数在ds0库,是奇数在ds1库。
订单ID字段(使用雪花算法自动生成)orderId是偶数在t_order_0库,是奇数在t_order_1库。
*/
log.info("测试3: 批量插入订单");
List<Order> orders = Arrays.asList(
orderService.generateTestOrder(1L),
orderService.generateTestOrder(2L),
orderService.generateTestOrder(3L),
orderService.generateTestOrder(4L),
orderService.generateTestOrder(5L)
);
boolean inserted = orderService.insertBatch(orders);
assertTrue("批量插入失败", inserted);
log.info("批量插入 {} 条订单成功", orders.size());
orders.forEach(order ->
log.info("插入订单 - {}", orderService.getRoutingInfo(order))
);
}
/**
* 测试4: 测试根据用户ID查询
*/
@Test
public void testQueryByUserId() {
log.info("测试4: 根据用户ID查询订单");
Long userId = 2L; // 偶数 -> ds0
String expectedDb = "ds0";
// 先插入几条测试数据
for (int i = 0; i < 3; i++) {
orderService.save(orderService.generateTestOrder(userId));
}
// 查询
List<Order> orders = orderService.getOrdersByUserId(userId);
assertNotNull("查询结果不应为null", orders);
assertTrue("订单数量应大于0", orders.size() > 0);
log.info("用户ID {} ({}) 的订单数: {}", userId, expectedDb, orders.size());
orders.forEach(order -> {
assertEquals("用户ID应匹配", userId, order.getUserId());
log.info(" 订单 {} -> {}", order.getOrderId(), orderService.getRoutingInfo(order));
});
}
/**
* 测试5: 测试复杂查询(带状态过滤)
*/
@Test
public void testComplexQuery() {
log.info("测试5: 复杂查询(用户ID + 状态)");
Long userId = 3L; // 奇数 -> ds1
Integer targetStatus = 1; // 已支付
// 插入不同状态的订单
for (int i = 0; i < 5; i++) {
Order order = orderService.generateTestOrder(userId);
order.setStatus(i % 3); // 0,1,2 循环
orderService.save(order);
}
// 查询状态为1的订单
List<Order> orders = orderService.complexQuery(userId, targetStatus);
log.info("用户ID {} 状态 {} 的订单数: {}", userId, targetStatus, orders.size());
orders.forEach(order -> {
assertEquals("用户ID应匹配", userId, order.getUserId());
assertEquals("状态应匹配", targetStatus, order.getStatus());
log.info(" 订单 {} - 状态{}", order.getOrderId(), order.getStatus());
});
}
/**
* 测试6: 测试统计功能
*/
@Test
public void testCountByUserId() {
log.info("测试6: 统计用户订单总数");
Long userId = 4L; // 偶数 -> ds0
int expectedCount = 3;
// 插入指定数量的订单
for (int i = 0; i < expectedCount; i++) {
orderService.save(orderService.generateTestOrder(userId));
}
int count = orderMapper.countByUserId(userId);
assertTrue("统计数量应至少为" + expectedCount, count >= expectedCount);
log.info("用户ID {} 的订单总数: {}", userId, count);
}
/**
* 测试7: 测试数据分布(运行报错)
*/
@Test
public void testDataDistribution() {
log.info("测试7: 测试数据分布情况");
// 初始化测试数据
orderService.initTestData(3);
Long[] userIds = {1L, 2L, 3L, 4L, 5L, 6L, 7L, 8L, 9L, 10L};
log.info("\n数据分布统计:");
log.info("用户ID\t数据库\t订单数");
log.info("-------------------");
for (Long userId : userIds) {
int count = orderMapper.countByUserId(userId);
String db = (userId % 2 == 0) ? "ds0" : "ds1";
log.info("{}\t{}\t{}", userId, db, count);
}
}
/**
* 测试8: 测试多用户ID查询
*/
@Test
public void testQueryByUserIds() {
log.info("测试8: 根据多个用户ID查询");
// 先插入测试数据
//orderService.initTestData(2);
List<Long> userIds = Arrays.asList(1L, 3L, 5L); // 都是奇数 -> ds1
List<Order> orders = orderService.getOrdersByUserIds(userIds);
log.info("查询用户 {} 的订单数: {}", userIds, orders.size());
orders.forEach(order ->
log.info(" 用户{} - 订单{}", order.getUserId(), order.getOrderId())
);
// 验证所有订单的用户ID都在查询列表中
orders.forEach(order ->
assertTrue("用户ID不在查询列表中", userIds.contains(order.getUserId()))
);
}
/**
* 测试9: 测试不带分片键的查询(全表扫描)
*/
@Test
public void testFullTableScan() {
log.info("测试9: 全表扫描查询(不带分片键)");
// 注意:这个查询会扫描所有库所有表
List<Order> allOrders = orderService.list();
log.info("全库订单总数: {}", allOrders.size());
if (allOrders.size() > 0) {
log.info("前5条订单记录:");
allOrders.stream().limit(5).forEach(order ->
log.info(" 订单{} - 用户{}", order.getOrderId(), order.getUserId())
);
}
}
/**
* 测试10: 测试事务
*/
@Test
public void testTransaction() {
log.info("测试10: 测试事务");
Long userId = 6L; // 偶数 -> ds0
try {
// 批量插入,故意让其中一个失败来测试事务
List<Order> orders = Arrays.asList(
orderService.generateTestOrder(userId),
orderService.generateTestOrder(userId),
orderService.generateTestOrder(userId)
);
// 正常插入
boolean success = orderService.insertBatch(orders);
assertTrue("批量插入失败", success);
// 验证插入成功
int count = orderMapper.countByUserId(userId);
log.info("用户ID {} 当前订单数: {}", userId, count);
} catch (Exception e) {
log.error("事务测试异常", e);
fail("事务测试失败: " + e.getMessage());
}
}
/**
* 测试读写分离
* Order retrieved = orderService.getById(order.getOrderId());(这块感觉有问题)
*/
@Test
public void testReadWriteSplittingFirst() {
log.info("测试读写分离");
// 1. 写操作 - 应该走主库 ds2
Long userId = 6L; // 偶数,按分片规则应该到 ms_ds
Order order = orderService.generateTestOrder(userId);
orderService.save(order);
log.info("写操作完成,订单ID: {}", order.getOrderId());
// 2. 读操作 - 应该走从库 (ds0 或 ds1,轮询)
Order retrieved = orderService.getById(order.getOrderId());
log.info("读操作完成,查询到的订单: {}", retrieved != null ? "成功" : "失败");
// 3. 批量读 - 测试负载均衡
for (int i = 0; i < 5; i++) {
List<Order> orders = orderService.getOrdersByUserId(userId);
log.info("第{}次查询,获取到{}条记录", i+1, orders.size());
}
// 4. 事务内操作 - 应该都走主库
@Transactional
class TransactionTest {
public void test() {
Order order1 = orderService.generateTestOrder(userId);
orderService.save(order1); // 写 - 主库
Order query = orderService.getById(order1.getOrderId()); // 读 - 也走主库(事务内)
log.info("事务内查询: {}", query != null ? "成功" : "失败");
}
}
}
/**
* 完整的读写分离测试
*/
@Test
public void testCompleteReadWriteSplitting() {
log.info("========== 完整读写分离测试 ==========");
// 测试不同的用户ID(奇数和偶数)
Long[] userIds = {1L, 2L, 3L, 4L, 5L, 6L};
for (Long userId : userIds) {
log.info("\n--- 测试用户ID: {} ---", userId);
// 1. 插入数据(写操作 -> 主库 ds2)
Order order = orderService.generateTestOrder(userId);
orderService.save(order);
log.info("插入订单 - {}", orderService.getRoutingInfo(order));
// 2. 根据ID查询(读操作 -> 从库)
Order queryById = orderService.getById(order.getOrderId());
log.info("ID查询 - {}", orderService.getRoutingInfo(queryById));
// 3. 根据用户ID查询列表(读操作 -> 从库)
List<Order> userOrders = orderService.getOrdersByUserId(userId);
log.info("用户查询 - 获取到 {} 条记录", userOrders.size());
if (!userOrders.isEmpty()) {
log.info(" 第一条记录路由: {}",
orderService.getRoutingInfo(userOrders.get(0)));
}
// 4. 更新操作(写操作 -> 主库)
order.setStatus(2);
orderService.updateById(order);
log.info("更新订单状态完成");
// 5. 再次查询验证更新(读操作 -> 从库,可能存在主从延迟)
Order updated = orderService.getById(order.getOrderId());
log.info("更新后查询 - 状态: {}", updated.getStatus());
}
}
/**
* 测试主从延迟场景
*/
@Test
public void testMasterSlaveDelay() {
log.info("========== 测试主从延迟场景 ==========");
Long userId = 8L;
// 1. 插入数据(主库)
Order order = orderService.generateTestOrder(userId);
orderService.save(order);
log.info("数据已插入主库,订单ID: {}", order.getOrderId());
// 2. 立即查询(可能因为主从延迟查不到)
Order immediately = orderService.getById(order.getOrderId());
log.info("立即查询结果: {}", immediately != null ? "成功" : "失败(可能主从延迟)");
// 3. 等待1秒后查询
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
Order afterWait = orderService.getById(order.getOrderId());
log.info("等待后查询结果: {}", afterWait != null ? "成功" : "失败");
// 4. 强制走主库查询(事务内)
@Transactional
class ForceMasterRead {
public Order query() {
return orderService.getById(order.getOrderId());
}
}
Order forceMaster = new ForceMasterRead().query();
log.info("强制主库查询结果: {}", forceMaster != null ? "成功" : "失败");
}
/**
* 测试负载均衡效果
*/
@Test
public void testLoadBalance() {
log.info("========== 测试从库负载均衡 ==========");
Long userId = 10L;
// 先插入一些测试数据
for (int i = 0; i < 5; i++) {
orderService.save(orderService.generateTestOrder(userId));
}
log.info("开始执行读操作,观察路由分布:");
// 执行20次查询,观察路由到哪个从库
for (int i = 0; i < 20; i++) {
List<Order> orders = orderService.getOrdersByUserId(userId);
if (!orders.isEmpty()) {
String routing = orderService.getRoutingInfo(orders.get(0));
log.info("第{}次查询 - 路由到: {}", i+1, routing.split(" ")[0]);
}
}
}
/**
* 测试读写分离 - 使用 LambdaQueryWrapper 替代 getById
*/
@Test
public void testReadWriteSplitting() {
log.info("========== 测试读写分离 ==========");
// 1. 写操作 - 应该走主库 ds2
/*
//偶数录入
Long userId = 6L;
Order order = orderService.generateTestOrder(userId);
boolean saved = orderService.save(order);
assertTrue("订单保存失败", saved);
log.info("✅ 写操作完成 - 用户ID: {}, 订单号: {}", userId, order.getOrderNo());*/
//奇数录入
/*Long userId = 9L;
Order order = orderService.generateTestOrder(userId);
boolean saved = orderService.save(order);
assertTrue("订单保存失败", saved);
log.info("✅ 写操作完成 - 用户ID: {}, 订单号: {}", userId, order.getOrderNo());*/
//造数据测试读操作
//用户ID字段userId是偶数在ds0库,是奇数在ds1库
//下面的userId是偶数所以在ds0库
/*Long userId = 6L;
Order order = new Order();
order.setOrderNo("ORD1772503153629");*/
//下面的userId是奇数所以在ds1库
Long userId = 9L;
Order order = new Order();
order.setOrderNo("ORD1772507872251");
// 2. 读操作 - 使用 LambdaQueryWrapper 查询(单条读) 下面的在userId是偶数在ds0库是对的
LambdaQueryWrapper<Order> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(Order::getUserId, userId)
.eq(Order::getOrderNo, order.getOrderNo());
Order retrieved = orderService.getOne(queryWrapper);
log.info("读操作查询: {},", JSON.toJSONString(retrieved));
assertNotNull("查询到的订单不应为null", retrieved);
// 获取路由信息(需要确保 retrieved 有 orderId)
String routingInfo = (retrieved.getOrderId() != null) ?
orderService.getRoutingInfo(retrieved) : "未知路由";
log.info("✅ 读操作完成 - 订单ID: {}, 路由: {}",
retrieved.getOrderId(), routingInfo);
// 3. 批量读 - 测试负载均衡
log.info("\n📊 测试从库负载均衡(多次查询):");
for (int i = 0; i < 5; i++) {
LambdaQueryWrapper<Order> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(Order::getUserId, userId);
List<Order> orders = orderService.list(wrapper);
String routeInfo = "未知路由";
if (!orders.isEmpty() && orders.get(0).getOrderId() != null) {
routeInfo = orderService.getRoutingInfo(orders.get(0));
}
//这块只打印出来了1条
log.info(" 第{}次查询 - 获取到{}条记录, 路由: {}",
i+1, orders.size(), routeInfo);
}//for end
// 4. 更新操作 - 写操作,应该走主库
/* retrieved.setStatus(1);
boolean updated = orderService.updateById(retrieved);
assertTrue("订单更新失败", updated);
log.info("✅ 更新操作完成 - 订单状态已修改为: 已支付");
// 5. 再次查询验证更新
LambdaQueryWrapper<Order> verifyWrapper = new LambdaQueryWrapper<>();
verifyWrapper.eq(Order::getUserId, userId)
.eq(Order::getOrderNo, order.getOrderNo());
Order updatedOrder = orderService.getOne(verifyWrapper);
assertNotNull("更新后查询失败", updatedOrder);
assertEquals("状态应该已更新", Integer.valueOf(1), updatedOrder.getStatus());
String verifyRoute = (updatedOrder.getOrderId() != null) ?
orderService.getRoutingInfo(updatedOrder) : "未知路由";
log.info("✅ 验证查询完成 - 订单状态: {}, 路由: {}",
updatedOrder.getStatus(), verifyRoute);*/
}
/**
* 测试不同用户的读写分离
*/
@Test
public void testReadWriteSplittingWithDifferentUsers() {
log.info("========== 测试多用户读写分离 ==========");
Long[] userIds = {1L, 2L, 3L, 4L, 5L};
for (Long userId : userIds) {
log.info("\n--- 测试用户ID: {} ---", userId);
// 插入数据
Order order = orderService.generateTestOrder(userId);
orderService.save(order);
// 查询数据
LambdaQueryWrapper<Order> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(Order::getUserId, userId)
.eq(Order::getOrderNo, order.getOrderNo());
Order queried = orderService.getOne(wrapper);
assertNotNull("查询失败", queried);
String expectedDb = (userId % 2 == 0) ? "ds0" : "ds1";
log.info("用户{}的数据应分布在: {}, 实际查询到订单ID: {}",
userId, expectedDb, queried.getOrderId());
}
}
}配置加载类代码如下所示:
package com.shardingdatabasetable.shardingsphere.config;
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.core.MybatisConfiguration;
import com.baomidou.mybatisplus.core.config.GlobalConfig;
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.type.JdbcType;
import org.mybatis.spring.SqlSessionTemplate;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.core.io.support.ResourcePatternResolver;
import javax.sql.DataSource;
/**
* MyBatis-Plus 配置类
* 解决 ShardingSphere + MyBatis-Plus 集成问题
*/
@Configuration
@MapperScan(basePackages = "com.shardingdatabasetable.shardingsphere.mapper", sqlSessionFactoryRef = "sqlSessionFactory")
public class MyBatisPlusConfig {
@Autowired
@Qualifier("shardingDataSource")
private DataSource shardingDataSource;
/**
* 创建SqlSessionFactory
*/
@Bean
public SqlSessionFactory sqlSessionFactory() throws Exception {
MybatisSqlSessionFactoryBean sqlSessionFactoryBean = new MybatisSqlSessionFactoryBean();
// 设置数据源为ShardingSphere的数据源
sqlSessionFactoryBean.setDataSource(shardingDataSource);
// 设置mapper.xml文件位置
ResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
sqlSessionFactoryBean.setMapperLocations(resolver.getResources("classpath*:mapper/**/*.xml"));
// 设置实体类包路径
sqlSessionFactoryBean.setTypeAliasesPackage("com.shardingdatabasetable.shardingsphere.entity");
// 使用MybatisConfiguration,而不是原生的Configuration
MybatisConfiguration configuration = new MybatisConfiguration();
configuration.setMapUnderscoreToCamelCase(true);
configuration.setCacheEnabled(false);
configuration.setJdbcTypeForNull(JdbcType.NULL);
sqlSessionFactoryBean.setConfiguration(configuration);
// 设置MyBatis-Plus拦截器
sqlSessionFactoryBean.setPlugins(mybatisPlusInterceptor());
// 设置全局配置
GlobalConfig globalConfig = new GlobalConfig();
GlobalConfig.DbConfig dbConfig = new GlobalConfig.DbConfig();
dbConfig.setIdType(com.baomidou.mybatisplus.annotation.IdType.NONE); // 使用ShardingSphere的主键生成策略
globalConfig.setDbConfig(dbConfig);
sqlSessionFactoryBean.setGlobalConfig(globalConfig);
return sqlSessionFactoryBean.getObject();
}
/**
* 创建SqlSessionTemplate
*/
@Bean
public SqlSessionTemplate sqlSessionTemplate() throws Exception {
return new SqlSessionTemplate(sqlSessionFactory());
}
/**
* MyBatis-Plus 拦截器(分页等功能)
*/
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 添加分页插件
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return interceptor;
}
}七、运行和测试
7.1 启动应用
- 执行数据库脚本:先在MySQL中执行建库建表语句
- 修改配置:根据实际环境修改
application.yml中的数据库连接信息 - 启动应用:运行
ShardingJdbcApplication.main()方法
7.2 执行单元测试
在IDE中右键点击ShardingJdbcTest类,选择"Run 'ShardingJdbcTest'"执行所有测试。
7.3 使用curl测试API
# 1. 初始化测试数据(每个用户生成5条订单) curl -X POST "http://localhost:8006/api/order/init?count=5" # 2. 创建单个订单 curl -X POST "http://localhost:8006/api/order/create?userId=1" # 3. 批量创建订单(用户2创建10条) curl -X POST "http://localhost:8006/api/order/batch?userId=2&count=10" # 4. 查询用户1的订单 curl "http://localhost:8006/api/order/user/1" # 5. 复杂查询(用户3的状态为1的订单) curl "http://localhost:8006/api/order/query?userId=3&status=1" # 6. 获取数据分布统计 curl "http://localhost:8006/api/order/distribution"
7.4 查看控制台输出
启动后,可以在控制台看到ShardingSphere打印的SQL日志:
2026-02-28 15:30:45.123 [http-nio-8006-exec-1] INFO ShardingSphere-SQL -
Logic SQL: INSERT INTO t_order (user_id, amount, status) VALUES (?, ?, ?)
Actual SQL: ds0 ::: INSERT INTO t_order_0 (user_id, amount, status) VALUES (?, ?, ?) ::: [2, 100.00, 1]
八、常见问题及解决方案
8.1 MySQL连接问题
如果出现连接错误,检查:
- URL中的
allowPublicKeyRetrieval=true参数 - 用户名密码是否正确
- MySQL 8.0的驱动版本是否正确
8.2 表不存在错误
确保已在两个数据库中分别创建了t_order_0和t_order_1表。
8.3 主键生成问题
ShardingSphere的雪花算法生成的主键是Long类型,确保实体类中的类型是Long而不是long。
8.4 Mapper XML找不到
检查application.yml中的mapper-locations配置是否正确。
这个完整的示例包含了所有必要的组件和配置,应该能够正常运行。如果还有问题,请查看控制台的具体错误信息。
总结
以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。
