1.概述
1.1.什么是 ShardingSphere-JDBC?
ShardingSphere-JDBC是轻量级 Java 框架,在 Java 的 JDBC 层提供的额外服务。它使⽤客⼾端直连数据库,以 jar 包形式 提供服务,⽆需额外部署和依赖,可理解为增强版的 JDBC 驱动,完全兼容 JDBC 和各种 ORM 框架。
本文基于 ShardingSphere 5.1.0 版本编写测试,不同版本之间有些许差异,可参考官方文档进行调整。
官方文档地址:https://shardingsphere.apache.org/document/current/cn/overview/
1.2.核心特性
| 特性 | 描述 | 适用场景 |
|---|---|---|
| 数据分片 | 水平拆分数据库/表 | 大数据量、高并发 |
| 读写分离 | 主从架构自动路由 | 读多写少 |
| 数据加密 | 透明化数据加密 | 敏感数据保护 |
| 分布式事务 | XA/BASE事务支持 | 跨库事务一致性 |
| 影子库压测 | 全链路压测隔离 | 生产压测 |
| 弹性伸缩 | 动态扩缩容 | 业务快速增长 |
2.环境搭建
2.1.系统要求
xml
<!-- 环境依赖矩阵 -->
+----------------+-----------------+-------------------+
| 组件 | 最低版本 | 推荐版本 |
+----------------+-----------------+-------------------+
| JDK | 1.8 | 11/17 (LTS) |
| Maven | 3.5 | 3.8+ |
| HighGoDB | 4.5 | 4.5+ |
| Spring Boot | 2.0 | 2.7+/3.0+ |
+----------------+-----------------+-------------------+
2.2.基础环境搭建
2.2.1.安装环境
- jdk: 要求jdk必须是1.8版本及以上
- 搭建瀚高数据库
2.2.2.搭建springboot程序
- 组件说明:
- SpringBoot 2.7.14
- MyBatisPlus 3.5.3.1
- ShardingSphere-JDBC 5.1.0
- Hikari
- hgdb v4.5
- 引入依赖
xml
<properties>
<java.version>17</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<shardingsphere.version>5.1.1</shardingsphere.version>
<mybatisplus.version>3.3.1</mybatisplus.version>
<hgdb.version>42.5.0</hgdb.version>
</properties>
<dependencies>
<!-- Spring Boot -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- MyBatis Plus -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>${mybatis-plus.version}</version>
</dependency>
<!-- ShardingSphere -->
<dependency>
<groupId>org.apache.shardingsphere</groupId>
<artifactId>shardingsphere-jdbc-core-spring-boot-starter</artifactId>
<version>${shardingsphere.version}</version>
</dependency>
<!-- HighGoDB -->
<dependency>
<groupId>com.highgo</groupId>
<artifactId>hgdb-pgjdbc</artifactId>
<version>${hgdb.version}</version>
</dependency>
<!-- HikariCP (Spring Boot默认使用) -->
<dependency>
<groupId>com.zaxxer</groupId>
<artifactId>HikariCP</artifactId>
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- 加密工具 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
3.分表不分库
3.1.数据库准备
sql
-- 创建数据库
CREATE DATABASE test_db;
-- 切换到test_db数据库
-- 创建分表 t_order_0 和 t_order_1
CREATE TABLE IF NOT EXISTS t_order_0 (
order_id BIGINT PRIMARY KEY,
user_id BIGINT NOT NULL,
order_no VARCHAR(50) NOT NULL,
amount DECIMAL(10,2) NOT NULL,
status INT DEFAULT 0,
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
deleted INT DEFAULT 0
);
CREATE INDEX ON t_order_0(user_id);
CREATE INDEX ON t_order_0(create_time);
CREATE TABLE IF NOT EXISTS t_order_1 (
order_id BIGINT PRIMARY KEY,
user_id BIGINT NOT NULL,
order_no VARCHAR(50) NOT NULL,
amount DECIMAL(10,2) NOT NULL,
status INT DEFAULT 0,
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
deleted INT DEFAULT 0
);
CREATE INDEX ON t_order_1(user_id);
CREATE INDEX ON t_order_1(create_time);
3.2.主配置文件 (application.yml)
yaml
spring:
# ShardingSphere 配置
shardingsphere:
# 数据源配置
datasource:
names: ds
ds:
type: com.zaxxer.hikari.HikariDataSource
driver-class-name: org.postgresql.Driver
jdbc-url: jdbc:postgresql://192.168.100.108:5866/test_db
username: sysdba
password: Qwer@12345
hikari:
connection-timeout: 30000
idle-timeout: 600000
max-lifetime: 1800000
maximum-pool-size: 20
minimum-idle: 5
# 分片规则配置
rules:
sharding:
# 分表规则
tables:
t_order:
# 实际数据节点,支持groovy表达式
actual-data-nodes: ds.t_order_$->{0..1}
# 分表策略
table-strategy:
standard:
sharding-column: user_id
sharding-algorithm-name: order-table-inline
# 主键生成策略
key-generate-strategy:
column: order_id
key-generator-name: snowflake
# 分片算法
sharding-algorithms:
order-table-inline:
type: INLINE
props:
algorithm-expression: t_order_$->{user_id % 2}
# 主键生成器
key-generators:
snowflake:
type: SNOWFLAKE
# 属性配置
props:
sql-show: true # 显示SQL
# MyBatis Plus 配置
mybatis-plus:
configuration:
map-underscore-to-camel-case: true
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
global-config:
db-config:
id-type: ASSIGN_ID
logic-delete-field: deleted
logic-delete-value: 1
logic-not-delete-value: 0
# 应用配置
server:
port: 8080
3.3.实体类
java
package com.yoyo.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
* 订单实体类
*/
@Data
@TableName("t_order") // 逻辑表名
public class Order {
/**
* 订单ID,使用Snowflake算法生成
*/
@TableId(type = IdType.ASSIGN_ID)
private Long orderId;
/**
* 用户ID,作为分片键
*/
private Long userId;
/**
* 订单号
*/
private String orderNo;
/**
* 订单金额
*/
private BigDecimal amount;
/**
* 订单状态:0-待支付,1-已支付,2-已完成,3-已取消
*/
private Integer status;
/**
* 创建时间
*/
private LocalDateTime createTime;
/**
* 更新时间
*/
private LocalDateTime updateTime;
/**
* 逻辑删除标识
*/
private Integer deleted;
}
3.4.Mapper接口
java
package com.yoyo.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.yoyo.entity.Order;
import org.apache.ibatis.annotations.Mapper;
/**
* 订单Mapper接口
*/
@Mapper
public interface OrderMapper extends BaseMapper<Order> {
}
3.5.Service接口
java
package com.yoyo.service;
import com.yoyo.entity.Order;
import java.util.List;
public interface OrderService {
/**
* 创建订单
*/
Order createOrder(Order order);
/**
* 批量创建订单
*/
List<Order> batchCreateOrders(List<Order> orders);
/**
* 根据用户ID查询订单
*/
List<Order> getOrdersByUserId(Long userId);
/**
* 测试跨表查询
*/
List<Order> queryAllOrders();
}
3.6.Service实现
java
package com.yoyo.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.yoyo.entity.Order;
import com.yoyo.mapper.OrderMapper;
import com.yoyo.service.OrderService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
import java.util.List;
@Slf4j
@Service
public class OrderServiceImpl extends ServiceImpl<OrderMapper, Order> implements OrderService {
@Override
@Transactional(rollbackFor = Exception.class)
public Order createOrder(Order order) {
order.setCreateTime(LocalDateTime.now());
order.setUpdateTime(LocalDateTime.now());
order.setDeleted(0);
this.save(order);
log.info("创建订单成功,订单ID: {}, 用户ID: {}",
order.getOrderId(), order.getUserId());
return order;
}
@Override
@Transactional(rollbackFor = Exception.class)
public List<Order> batchCreateOrders(List<Order> orders) {
for (Order order : orders) {
order.setCreateTime(LocalDateTime.now());
order.setUpdateTime(LocalDateTime.now());
order.setDeleted(0);
}
this.saveBatch(orders);
log.info("批量创建订单成功,共{}条", orders.size());
return orders;
}
@Override
public List<Order> getOrdersByUserId(Long userId) {
QueryWrapper<Order> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("user_id",userId);
List<Order> orders = this.list(queryWrapper);
log.info("根据用户ID: {} 查询到 {} 条订单", userId, orders.size());
return orders;
}
@Override
public List<Order> queryAllOrders() {
// 注意:这会查询所有分片表
List<Order> orders = this.list();
log.info("跨表查询到 {} 条订单", orders.size());
return orders;
}
}
3.7.测试用例
java
package com.yoyo;
import com.yoyo.entity.Order;
import com.yoyo.service.impl.OrderServiceImpl;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
@Slf4j
@SpringBootTest
class SpringbootShardingJdbcDemoApplicationTests {
@Autowired
private OrderServiceImpl orderService;
@Test
void testInsertShardingTable() {
Order order1 = new Order();
order1.setUserId(1L); // 会插入到 t_order_1 (1 % 2 = 1)
order1.setOrderNo("TEST001");
order1.setAmount(BigDecimal.valueOf(100.50));
order1.setStatus(1);
Order order2 = new Order();
order2.setUserId(2L); // 会插入到 t_order_0 (2 % 2 = 0)
order2.setOrderNo("TEST002");
order2.setAmount(BigDecimal.valueOf(200.00));
order2.setStatus(2);
orderService.createOrder(order1);
orderService.createOrder(order2);
log.info("测试数据插入完成");
}
@Test
void testBatchInsertShardingTable() {
List<Order> orders = new ArrayList<>();
for (int i = 0; i < 20; i++) {
Order order = new Order();
order.setUserId((long) i); // 根据用户ID分片
order.setOrderNo("BATCH" + i);
order.setAmount(BigDecimal.valueOf(i * 50));
order.setStatus(i % 4);
orders.add(order);
}
orderService.batchCreateOrders(orders);
log.info("批量插入完成,共{}条记录", orders.size());
}
@Test
void testQueryByShardingKeyShardingTable() {
Long userId = 3L;
List<Order> orders = orderService.getOrdersByUserId(userId);
log.info("用户ID: {} 的订单数量: {}", userId, orders.size());
for (Order order : orders) {
log.info("订单: {}, 金额: {}", order.getOrderNo(), order.getAmount());
}
}
@Test
void testCrossTableQueryShardingTable() {
List<Order> allOrders = orderService.queryAllOrders();
log.info("所有订单数量: {}", allOrders.size());
// 统计各分片表的订单数量
long count0 = allOrders.stream()
.filter(order -> order.getUserId() % 2 == 0)
.count();
long count1 = allOrders.stream()
.filter(order -> order.getUserId() % 2 == 1)
.count();
log.info("t_order_0 表订单数: {}", count0);
log.info("t_order_1 表订单数: {}", count1);
}
}
4.分库不分表
4.1.数据准备
shell
-- 创建数据库0
CREATE DATABASE test_db_0;
-- 创建数据库1
CREATE DATABASE test_db_1;
-- 切换到 test_db_0
-- 创建订单表
CREATE TABLE IF NOT EXISTS t_order (
order_id BIGINT PRIMARY KEY,
user_id BIGINT NOT NULL,
order_no VARCHAR(50) NOT NULL,
amount DECIMAL(10,2) NOT NULL,
status INT DEFAULT 0,
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
deleted INT DEFAULT 0
);
CREATE INDEX ON t_order(user_id);
CREATE INDEX ON t_order(create_time);
-- 切换到 test_db_1
-- 创建订单表
CREATE TABLE IF NOT EXISTS t_order (
order_id BIGINT PRIMARY KEY,
user_id BIGINT NOT NULL,
order_no VARCHAR(50) NOT NULL,
amount DECIMAL(10,2) NOT NULL,
status INT DEFAULT 0,
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
deleted INT DEFAULT 0
);
CREATE INDEX ON t_order(user_id);
CREATE INDEX ON t_order(create_time);
4.2.主配置文件 (application.yml)
yaml
spring:
shardingsphere:
# 数据源配置 - 两个数据库实例
datasource:
names: ds0,ds1
ds0:
type: com.zaxxer.hikari.HikariDataSource
driver-class-name: org.postgresql.Driver
jdbc-url: jdbc:postgresql://192.168.100.108:5866/test_db_0
username: sysdba
password: Qwer@12345
hikari:
connection-timeout: 30000
idle-timeout: 600000
max-lifetime: 1800000
maximum-pool-size: 20
minimum-idle: 5
ds1:
type: com.zaxxer.hikari.HikariDataSource
driver-class-name: org.postgresql.Driver
jdbc-url: jdbc:postgresql://192.168.100.108:5866/test_db_1
username: sysdba
password: Qwer@12345
hikari:
connection-timeout: 30000
idle-timeout: 600000
max-lifetime: 1800000
maximum-pool-size: 20
minimum-idle: 5
# 分片规则配置
rules:
sharding:
# 默认数据库分片策略
default-database-strategy:
standard:
sharding-column: user_id
sharding-algorithm-name: database-inline
# 表配置(不分表)
tables:
t_order:
# 实际数据节点,分布在两个数据库
actual-data-nodes: ds$->{0..1}.t_order
# 主键生成策略
key-generate-strategy:
column: order_id
key-generator-name: snowflake
# 分片算法
sharding-algorithms:
database-inline:
type: INLINE
props:
algorithm-expression: ds$->{user_id % 2}
# 主键生成器
key-generators:
snowflake:
type: SNOWFLAKE
# 属性配置
props:
sql-show: true # 显示SQL
sql-simple: true # 简化SQL日志
# MyBatis Plus 配置
mybatis-plus:
configuration:
map-underscore-to-camel-case: true
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
global-config:
db-config:
id-type: ASSIGN_ID
logic-delete-field: deleted
logic-delete-value: 1
logic-not-delete-value: 0
# 应用配置
server:
port: 8080
4.3.实体类
同上。
4.4.Mapper接口
同上。
4.5.Service接口
同上。
4.6.Service实现
同上。
4.7.测试用例
java
@Test
void testInsertShardingDatabase() {
Order order1 = new Order();
order1.setUserId(1L); // 会插入到 test_db_1.t_order (1 % 2 = 1)
order1.setOrderNo("TEST001");
order1.setAmount(BigDecimal.valueOf(100.50));
order1.setStatus(1);
Order order2 = new Order();
order2.setUserId(2L); // 会插入到 test_db_0.t_order (2 % 2 = 0)
order2.setOrderNo("TEST002");
order2.setAmount(BigDecimal.valueOf(200.00));
order2.setStatus(2);
orderService.createOrder(order1);
orderService.createOrder(order2);
log.info("测试数据插入完成");
}
@Test
void testBatchInsertShardingDatabase() {
List<Order> orders = new ArrayList<>();
for (int i = 0; i < 20; i++) {
Order order = new Order();
order.setUserId((long) i); // 根据用户ID分片
order.setOrderNo("BATCH" + i);
order.setAmount(BigDecimal.valueOf(i * 50));
order.setStatus(i % 4);
orders.add(order);
}
orderService.batchCreateOrders(orders);
log.info("批量插入完成,共{}条记录", orders.size());
}
@Test
void testQueryByShardingKeyShardingDatabase() {
Long userId = 3L;
List<Order> orders = orderService.getOrdersByUserId(userId);
log.info("用户ID: {} 的订单数量: {}", userId, orders.size());
for (Order order : orders) {
log.info("订单: {}, 金额: {}", order.getOrderNo(), order.getAmount());
}
}
@Test
void testCrossTableQueryShardingDatabase() {
List<Order> allOrders = orderService.queryAllOrders();
log.info("所有订单数量: {}", allOrders.size());
// 统计各分片表的订单数量
long count0 = allOrders.stream()
.filter(order -> order.getUserId() % 2 == 0)
.count();
long count1 = allOrders.stream()
.filter(order -> order.getUserId() % 2 == 1)
.count();
log.info("test_db_0.t_order 表订单数: {}", count0);
log.info("test_db_1.t_order 表订单数: {}", count1);
}
5.分库分表
5.1.数据准备
sql
-- 创建数据库0
CREATE DATABASE test_db_0;
-- 创建数据库1
CREATE DATABASE test_db_1;
-- 切换到 test_db_0
-- 创建订单表t_order_0..1
CREATE TABLE IF NOT EXISTS t_order_0 (
order_id BIGINT PRIMARY KEY,
user_id BIGINT NOT NULL,
order_no VARCHAR(50) NOT NULL,
amount DECIMAL(10,2) NOT NULL,
status INT DEFAULT 0,
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
deleted INT DEFAULT 0
);
CREATE INDEX ON t_order_0(user_id);
CREATE INDEX ON t_order_0(create_time);
CREATE TABLE IF NOT EXISTS t_order_1 (
order_id BIGINT PRIMARY KEY,
user_id BIGINT NOT NULL,
order_no VARCHAR(50) NOT NULL,
amount DECIMAL(10,2) NOT NULL,
status INT DEFAULT 0,
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
deleted INT DEFAULT 0
);
CREATE INDEX ON t_order_1(user_id);
CREATE INDEX ON t_order_1(create_time);
-- 切换到 test_db_1
-- 创建订单表t_order0..1
CREATE TABLE IF NOT EXISTS t_order_0 (
order_id BIGINT PRIMARY KEY,
user_id BIGINT NOT NULL,
order_no VARCHAR(50) NOT NULL,
amount DECIMAL(10,2) NOT NULL,
status INT DEFAULT 0,
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
deleted INT DEFAULT 0
);
CREATE INDEX ON t_order_0(user_id);
CREATE INDEX ON t_order_0(create_time);
CREATE TABLE IF NOT EXISTS t_order_1 (
order_id BIGINT PRIMARY KEY,
user_id BIGINT NOT NULL,
order_no VARCHAR(50) NOT NULL,
amount DECIMAL(10,2) NOT NULL,
status INT DEFAULT 0,
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
deleted INT DEFAULT 0
);
CREATE INDEX ON t_order_1(user_id);
CREATE INDEX ON t_order_1(create_time);
5.2.主配置文件 (application.yml)
yaml
spring:
shardingsphere:
# 数据源配置 - 两个数据库实例
datasource:
names: ds0,ds1
ds0:
type: com.zaxxer.hikari.HikariDataSource
driver-class-name: org.postgresql.Driver
jdbc-url: jdbc:postgresql://192.168.100.108:5866/test_db_0
username: sysdba
password: Qwer@12345
hikari:
connection-timeout: 30000
idle-timeout: 600000
max-lifetime: 1800000
maximum-pool-size: 20
minimum-idle: 5
ds1:
type: com.zaxxer.hikari.HikariDataSource
driver-class-name: org.postgresql.Driver
jdbc-url: jdbc:postgresql://192.168.100.108:5866/test_db_1
username: sysdba
password: Qwer@12345
hikari:
connection-timeout: 30000
idle-timeout: 600000
max-lifetime: 1800000
maximum-pool-size: 20
minimum-idle: 5
# 分片规则配置
rules:
sharding:
# 默认数据库分片策略
default-database-strategy:
standard:
sharding-column: user_id
sharding-algorithm-name: database-inline
# 表配置(分库分表)
tables:
# 订单表 - 分库分表 (2库 × 2表 = 4个分片)
t_order:
# 实际数据节点,分布在两个数据库
actual-data-nodes: ds$->{0..1}.t_order_$->{0..1}
# 数据库分片策略:按 user_id 分库
database-strategy:
standard:
sharding-column: user_id
sharding-algorithm-name: database-inline
# 表分片策略:按 order_id 分表
table-strategy:
standard:
sharding-column: order_id
sharding-algorithm-name: order-table-inline
# 分片算法
sharding-algorithms:
# 数据库分片算法:user_id % 2
database-inline:
type: INLINE
props:
algorithm-expression: ds$->{user_id % 2}
# 表分片算法:order_id % 2
order-table-inline:
type: INLINE
props:
algorithm-expression: t_order_$->{order_id % 2}
# 属性配置
props:
sql-show: true # 显示SQL
sql-simple: true # 简化SQL日志
# MyBatis Plus 配置
mybatis-plus:
configuration:
map-underscore-to-camel-case: true
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
global-config:
db-config:
id-type: ASSIGN_ID
logic-delete-field: deleted
logic-delete-value: 1
logic-not-delete-value: 0
# 应用配置
server:
port: 8080
5.3.实体类
同上。
5.4.Mapper接口
同上。
5.5.Service接口
同上。
5.6.Service实现
同上。
5.7.测试用例
yaml
@Test
void testInsertShardingDatabaseAndTable() {
Order order1 = new Order();
order1.setOrderId(1001L); // 1001 % 2 = 1 → t_order_1
order1.setUserId(101L); // 101 % 2 = 1 → ds1
order1.setOrderNo("TEST001");
order1.setAmount(BigDecimal.valueOf(100.50));
order1.setStatus(1);
Order order2 = new Order();
order2.setOrderId(1002L); // 1002 % 2 = 0 → t_order_0
order2.setUserId(101L); // 101 % 2 = 1 → ds1
order2.setOrderNo("TEST002");
order2.setAmount(BigDecimal.valueOf(200.50));
order2.setStatus(2);
Order order3 = new Order();
order3.setOrderId(1003L); // 1003 % 2 = 1 → t_order_1
order3.setUserId(102L); // 102 % 2 = 0 → ds0
order3.setOrderNo("TEST003");
order3.setAmount(BigDecimal.valueOf(200.50));
order3.setStatus(3);
Order order4 = new Order();
order4.setOrderId(1004L); // 1004 % 2 = 0 → t_order_0
order4.setUserId(102L); // 102 % 2 = 0 → ds0
order4.setOrderNo("TEST004");
order4.setAmount(BigDecimal.valueOf(200.50));
order4.setStatus(4);
orderService.createOrder(order1);
orderService.createOrder(order2);
orderService.createOrder(order3);
orderService.createOrder(order4);
log.info("测试数据插入完成");
}
@Test
void testBatchInsertShardingDatabaseAndTable() {
List<Order> orders = new ArrayList<>();
Random random = new Random();
for (int i = 0; i < 20; i++) {
Order order = new Order();
long orderId = 1000L + i;
long userId = random.nextInt(100);
order.setOrderId(orderId); // 根据订单ID分表
order.setUserId(userId); // 根据用户ID分库
order.setOrderNo("BATCH" + i);
order.setAmount(BigDecimal.valueOf(i * 50));
order.setStatus(i % 4);
orders.add(order);
}
orderService.batchCreateOrders(orders);
log.info("批量插入完成,共{}条记录", orders.size());
}
@Test
void testCrossTableQueryShardingDatabaseAndTable() {
List<Order> allOrders = orderService.queryAllOrders();
log.info("所有订单数量: {}", allOrders.size());
// 统计各分片表的订单数量
long count0 = allOrders.stream()
.filter(order -> order.getUserId() % 2 == 0 && order.getOrderId() % 2 == 0)
.count();
long count1 = allOrders.stream()
.filter(order -> order.getUserId() % 2 == 0 && order.getOrderId() % 2 == 1)
.count();
long count2 = allOrders.stream()
.filter(order -> order.getUserId() % 2 == 1 && order.getOrderId() % 2 == 0)
.count();
long count3 = allOrders.stream()
.filter(order -> order.getUserId() % 2 == 1 && order.getOrderId() % 2 == 1)
.count();
log.info("test_db_0.t_order_0 表订单数: {}", count0);
log.info("test_db_0.t_order_1 表订单数: {}", count1);
log.info("test_db_1.t_order_0 表订单数: {}", count2);
log.info("test_db_1.t_order_1 表订单数: {}", count3);
}
6.自定义分片策略
6.1.自定义分库策略
java
package com.yoyo.sharding.algorithm;
import org.apache.shardingsphere.sharding.api.sharding.standard.PreciseShardingValue;
import org.apache.shardingsphere.sharding.api.sharding.standard.RangeShardingValue;
import org.apache.shardingsphere.sharding.api.sharding.standard.StandardShardingAlgorithm;
import java.util.Collection;
import java.util.Properties;
/**
* 自定义分库策略
* 根据 user_id 进行分库
*/
public class CustomDatabaseShardingAlgorithm implements StandardShardingAlgorithm<Long> {
private Properties props = new Properties();
@Override
public String doSharding(Collection<String> availableTargetNames,
PreciseShardingValue<Long> shardingValue) {
// availableTargetNames: 可用的数据源名称,如 ds0, ds1
// shardingValue: 分片键的值,如 user_id=123
Long userId = shardingValue.getValue();
String shardingColumn = shardingValue.getColumnName();
// 自定义分库逻辑:根据user_id的奇偶分库
// 实际业务中可以根据业务规则,如用户地区、用户类型等
// 简单的奇偶分库
long dbIndex = userId % 2;
for (String each : availableTargetNames) {
if (each.endsWith(String.valueOf(dbIndex))) {
return each;
}
}
throw new IllegalArgumentException("未找到对应的数据库分片: userId=" + userId);
}
@Override
public Collection<String> doSharding(Collection<String> availableTargetNames,
RangeShardingValue<Long> shardingValue) {
// 范围查询的分片逻辑
// 这里简单返回所有数据源,实际业务需要根据范围优化
return availableTargetNames;
}
@Override
public Properties getProps() {
return props;
}
@Override
public void init() {
}
@Override
public String getType() {
return "CUSTOM_DB_SHARDING";
}
}
6.2.自定义分表策略
java
package com.yoyo.sharding.algorithm;
import org.apache.shardingsphere.sharding.api.sharding.standard.PreciseShardingValue;
import org.apache.shardingsphere.sharding.api.sharding.standard.RangeShardingValue;
import org.apache.shardingsphere.sharding.api.sharding.standard.StandardShardingAlgorithm;
import java.time.LocalDateTime;
import java.util.Collection;
import java.util.Properties;
/**
* 自定义分表策略
* 根据 order_id 进行分表
*/
public class CustomTableShardingAlgorithm implements StandardShardingAlgorithm<Long> {
private Properties props = new Properties();
@Override
public String doSharding(Collection<String> availableTargetNames,
PreciseShardingValue<Long> shardingValue) {
Long orderId = shardingValue.getValue();
String logicTableName = shardingValue.getLogicTableName();
//方案1: 简单的奇偶分表
long tableIndex = orderId % 2;
// 方案2: 基于时间戳的分表(如果order_id包含时间戳)
// 例如:雪花算法生成的ID包含时间戳信息
//LocalDateTime createTime = extractTimeFromOrderId(orderId);
//int yearMonth = createTime.getYear() * 100 + createTime.getMonthValue();
//long tableIndex = yearMonth % 2; // 按年月分表
// 方案3: 哈希分表(更均匀)
// long tableIndex = Math.abs(orderId.hashCode()) % availableTargetNames.size();
// 方案4: 根据业务规则分表(如订单类型)
// long tableIndex = getOrderType(orderId) % 2;
// 查找对应的表
for (String each : availableTargetNames) {
if (each.endsWith("_" + tableIndex)) {
return each;
}
}
throw new IllegalArgumentException("未找到对应的表分片: orderId=" + orderId);
}
/**
* 从订单ID中提取时间信息(如果使用雪花算法)
*/
private LocalDateTime extractTimeFromOrderId(Long orderId) {
// 雪花算法示例:41位时间戳 + 10位机器ID + 12位序列号
// 这里假设order_id是雪花算法生成的
if (orderId == null) {
return LocalDateTime.now();
}
long timestamp = (orderId >> 22) + 1288834974657L; // Twitter雪花算法起始时间
return LocalDateTime.ofEpochSecond(timestamp / 1000, 0, java.time.ZoneOffset.UTC);
}
/**
* 获取订单类型(示例)
*/
private int getOrderType(Long orderId) {
// 假设订单ID的第3-4位表示订单类型
return (int) ((orderId / 10000) % 100);
}
@Override
public Collection<String> doSharding(Collection<String> availableTargetNames,
RangeShardingValue<Long> shardingValue) {
// 范围查询的分表逻辑
// 这里简单返回所有表,实际需要根据时间范围等优化
return availableTargetNames;
}
@Override
public Properties getProps() {
return props;
}
@Override
public void init() {
}
@Override
public String getType() {
return "CUSTOM_TABLE_SHARDING";
}
}
6.3.复合分片策略
java
package com.yoyo.sharding.algorithm;
import org.apache.shardingsphere.sharding.api.sharding.complex.ComplexKeysShardingAlgorithm;
import org.apache.shardingsphere.sharding.api.sharding.complex.ComplexKeysShardingValue;
import java.util.*;
/**
* 复合分片策略
* 同时考虑 user_id 和 order_time 进行分片
*/
public class ComplexShardingAlgorithm implements ComplexKeysShardingAlgorithm<Comparable<?>> {
private Properties props = new Properties();
@Override
public Collection<String> doSharding(Collection<String> availableTargetNames,
ComplexKeysShardingValue<Comparable<?>> shardingValue) {
Map<String, Collection<Comparable<?>>> columnNameAndShardingValuesMap =
shardingValue.getColumnNameAndShardingValuesMap();
String logicTableName = shardingValue.getLogicTableName();
// 获取分片键的值
Collection<Comparable<?>> userIdValues = columnNameAndShardingValuesMap.get("user_id");
Collection<Comparable<?>> orderTimeValues = columnNameAndShardingValuesMap.get("order_time");
Set<String> result = new HashSet<>();
// 处理精确查询
if (shardingValue.getColumnNameAndRangeValuesMap().isEmpty()) {
for (Comparable<?> userId : userIdValues) {
for (Comparable<?> orderTime : orderTimeValues) {
String targetName = calculateTargetName(
logicTableName,
(Long) userId,
(Date) orderTime
);
if (availableTargetNames.contains(targetName)) {
result.add(targetName);
}
}
}
} else {
// 处理范围查询,返回所有可能的分片
result.addAll(availableTargetNames);
}
return result;
}
private String calculateTargetName(String logicTableName, Long userId, Date orderTime) {
// 自定义复合分片逻辑
// 例如:按用户地区分库,按订单月份分表
// 1. 根据user_id确定数据库
int dbIndex = calculateDbIndex(userId);
// 2. 根据order_time确定表
int tableIndex = calculateTableIndex(orderTime);
return String.format("ds%d.%s_%d", dbIndex, logicTableName, tableIndex);
}
private int calculateDbIndex(Long userId) {
// 根据用户所在地区或其他属性分库
// 这里简单按奇偶分
return (int) (userId % 2);
}
private int calculateTableIndex(Date orderTime) {
Calendar calendar = Calendar.getInstance();
calendar.setTime(orderTime);
int month = calendar.get(Calendar.MONTH);
// 按月分表,每6个月一张表
return month / 6;
}
@Override
public Properties getProps() {
return props;
}
@Override
public void init() {
}
@Override
public String getType() {
return "COMPLEX_SHARDING";
}
}
6.4.基于日期的分表策略
java
package com.yoyo.sharding.algorithm;
import org.apache.shardingsphere.sharding.api.sharding.standard.PreciseShardingValue;
import org.apache.shardingsphere.sharding.api.sharding.standard.RangeShardingValue;
import org.apache.shardingsphere.sharding.api.sharding.standard.StandardShardingAlgorithm;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.Properties;
/**
* 基于日期的分表策略
* 按月分表:t_order_202312, t_order_202401, ...
*/
public class DateShardingAlgorithm implements StandardShardingAlgorithm<Date> {
private Properties props = new Properties();
private DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyyMM");
@Override
public String doSharding(Collection<String> availableTargetNames,
PreciseShardingValue<Date> shardingValue) {
Date createTime = shardingValue.getValue();
String logicTableName = shardingValue.getLogicTableName();
// 将Date转换为LocalDate
LocalDate localDate = convertToLocalDate(createTime);
// 生成表后缀:年月,如 202312
String tableSuffix = localDate.format(formatter);
// 查找对应的表
String targetTable = logicTableName + "_" + tableSuffix;
for (String each : availableTargetNames) {
if (each.equals(targetTable)) {
return each;
}
}
// 如果表不存在,创建默认表或抛出异常
// 这里可以根据配置创建新表或路由到默认表
return logicTableName + "_default";
}
@Override
public Collection<String> doSharding(Collection<String> availableTargetNames,
RangeShardingValue<Date> shardingValue) {
// 处理时间范围查询
Date lower = (Date) shardingValue.getValueRange().lowerEndpoint();
Date upper = (Date) shardingValue.getValueRange().upperEndpoint();
LocalDate startDate = convertToLocalDate(lower);
LocalDate endDate = convertToLocalDate(upper);
Collection<String> result = new ArrayList<>();
// 获取时间范围内的所有表
LocalDate current = startDate;
while (!current.isAfter(endDate)) {
String tableName = shardingValue.getLogicTableName() + "_" + current.format(formatter);
if (availableTargetNames.contains(tableName)) {
result.add(tableName);
}
current = current.plusMonths(1);
}
return result;
}
private LocalDate convertToLocalDate(Date date) {
if (date == null) {
return LocalDate.now();
}
return new java.sql.Timestamp(date.getTime()).toLocalDateTime().toLocalDate();
}
@Override
public Properties getProps() {
return props;
}
@Override
public void init() {
}
@Override
public String getType() {
return "DATE_SHARDING";
}
}
6.5.application.yml 配置
yaml
spring:
shardingsphere:
# 数据源配置 - 两个数据库实例
datasource:
names: ds0,ds1
ds0:
type: com.zaxxer.hikari.HikariDataSource
driver-class-name: org.postgresql.Driver
jdbc-url: jdbc:postgresql://192.168.100.108:5866/test_db_0
username: sysdba
password: Qwer@12345
hikari:
connection-timeout: 30000
idle-timeout: 600000
max-lifetime: 1800000
maximum-pool-size: 20
minimum-idle: 5
ds1:
type: com.zaxxer.hikari.HikariDataSource
driver-class-name: org.postgresql.Driver
jdbc-url: jdbc:postgresql://192.168.100.108:5866/test_db_1
username: sysdba
password: Qwer@12345
hikari:
connection-timeout: 30000
idle-timeout: 600000
max-lifetime: 1800000
maximum-pool-size: 20
minimum-idle: 5
# 分片规则配置
rules:
sharding:
# 分表配置
tables:
t_order:
# 实际数据节点:ds0.t_order_0, ds0.t_order_1, ds1.t_order_0, ds1.t_order_1
actual-data-nodes: ds$->{0..1}.t_order_$->{0..1}
# 分库策略
database-strategy:
standard:
sharding-column: user_id
# 使用自定义分库算法
sharding-algorithm-name: custom-database-sharding
# 表表策略
table-strategy:
standard:
sharding-column: order_id
# 使用自定义分表算法
sharding-algorithm-name: custom-table-sharding
# 分片算法
sharding-algorithms:
# 自定义分库算法
custom-database-sharding:
type: CLASS_BASED
props:
strategy: standard
algorithmClassName: com.yoyo.sharding.algorithm.CustomDatabaseShardingAlgorithm
# 自定义分表算法
custom-table-sharding:
type: CLASS_BASED
props:
strategy: standard
algorithmClassName: com.yoyo.sharding.algorithm.CustomTableShardingAlgorithm
# 复合分片算法
complex-sharding:
type: CLASS_BASED
props:
strategy: complex
algorithmClassName: com.yoyo.sharding.algorithm.ComplexShardingAlgorithm
# 日期分片算法
date-sharding:
type: CLASS_BASED
props:
strategy: standard
algorithmClassName: com.yoyo.sharding.algorithm.DateShardingAlgorithm
# 属性配置
props:
sql-show: true # 显示SQL
sql-simple: true # 简化SQL日志
# MyBatis Plus 配置
mybatis-plus:
configuration:
map-underscore-to-camel-case: true
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
global-config:
db-config:
id-type: ASSIGN_ID
logic-delete-field: deleted
logic-delete-value: 1
logic-not-delete-value: 0
# 应用配置
server:
port: 8080
6.6.测试用例
java
@Test
void testCustomSharding() {
List<Order> orders = new ArrayList<>();
// 生成测试数据,验证自定义分片逻辑
for (int i = 0; i < 100; i++) {
Order order = new Order();
// 使用自定义的ID生成策略
// 或者让分片算法处理分片逻辑
long orderId = 1000000000L + i;
long userId = (long) (Math.random() * 1000);
order.setOrderId(orderId);
order.setUserId(userId);
order.setOrderNo("ORDER_" + System.currentTimeMillis() + "_" + i);
order.setAmount(BigDecimal.valueOf((i + 1) * 100));
order.setStatus(i % 4);
orders.add(order);
// 打印分片信息
String expectedDb = (userId % 2 == 0) ? "ds0" : "ds1";
String expectedTable = (orderId % 2 == 0) ? "t_order_0" : "t_order_1";
log.info("订单{}: userId={}({}库), orderId={}({}表)",
i, userId, expectedDb, orderId, expectedTable);
}
// 批量插入
orderService.batchCreateOrders(orders);
log.info("自定义分片测试完成,共插入{}条记录", orders.size());
}
@Test
void testQueryWithCustomSharding() {
List<Order> allOrders = orderService.queryAllOrders();
log.info("所有订单数量: {}", allOrders.size());
// 统计各分片表的订单数量
long count0 = allOrders.stream()
.filter(order -> order.getUserId() % 2 == 0 && order.getOrderId() % 2 == 0)
.count();
long count1 = allOrders.stream()
.filter(order -> order.getUserId() % 2 == 0 && order.getOrderId() % 2 == 1)
.count();
long count2 = allOrders.stream()
.filter(order -> order.getUserId() % 2 == 1 && order.getOrderId() % 2 == 0)
.count();
long count3 = allOrders.stream()
.filter(order -> order.getUserId() % 2 == 1 && order.getOrderId() % 2 == 1)
.count();
log.info("test_db_0.t_order_0 表订单数: {}", count0);
log.info("test_db_0.t_order_1 表订单数: {}", count1);
log.info("test_db_1.t_order_0 表订单数: {}", count2);
log.info("test_db_1.t_order_1 表订单数: {}", count3);
}
7.数据加密
7.1.数据准备
sql
--创建数据库
create database encryption_db;
-- 切换到 encryption_db 数据库
CREATE TABLE t_user(
id SERIAL PRIMARY KEY,
name VARCHAR NOT NULL,
phone VARCHAR,
email VARCHAR,
id_card VARCHAR,
password VARCHAR
);
7.2.主配置文件(application.yaml)
yaml
spring:
shardingsphere:
datasource:
names: ds0
ds0:
type: com.zaxxer.hikari.HikariDataSource
driver-class-name: org.postgresql.Driver
jdbc-url: jdbc:postgresql://192.168.100.108:5866/encryption_db
username: sysdba
password: Qwer@12345
rules:
encrypt:
encryptors:
aes_encryptor:
type: CUSTOM_AES
base64_encryptor:
type: CUSTOM_BASE64
md5_encryptor:
type: CUSTOM_MD5
sm4_encryptor:
type: CUSTOM_SM4
tables:
t_user:
columns:
email:
cipher-column: email
encryptor-name: aes_encryptor
phone:
cipher-column: phone
encryptor-name: base64_encryptor
password:
cipher-column: password
encryptor-name: sm4_encryptor
id_card:
cipher-column: id_card
encryptor-name: md5_encryptor
props:
sql-show: true
query-with-cipher-column: true # 是否使用密文列查询
# MyBatis Plus 配置
mybatis-plus:
configuration:
map-underscore-to-camel-case: true
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
global-config:
db-config:
id-type: ASSIGN_ID
logic-delete-field: deleted
logic-delete-value: 1
logic-not-delete-value: 0
# 应用配置
server:
port: 8080
7.3.自定义加密算法
7.3.1.AES加密实现
CustomAesEncryptAlgorithm.java
java
package com.yoyo.sharding.encrypt;
import org.apache.shardingsphere.encrypt.spi.EncryptAlgorithm;
import org.apache.shardingsphere.encrypt.spi.context.EncryptContext;
import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.SecureRandom;
public class CustomAesEncryptAlgorithm implements EncryptAlgorithm<String,String> {
private static final String ALGORITHM = "AES";
private static final String SECRET_KEY = "1234567890123456";
@Override
public void init() {
}
@Override
public String encrypt(String plainValue, EncryptContext encryptContext) {
if (plainValue == null) return null;
try {
KeyGenerator keyGen = KeyGenerator.getInstance(ALGORITHM);
SecureRandom secureRandom = SecureRandom.getInstance("SHA1PRNG");
secureRandom.setSeed(SECRET_KEY.getBytes());
keyGen.init(128, secureRandom);
SecretKey secretKeySpec = new SecretKeySpec(keyGen.generateKey().getEncoded(), ALGORITHM);
Cipher cipher = Cipher.getInstance(ALGORITHM);
cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec);
byte[] encrypted = cipher.doFinal(plainValue.getBytes(StandardCharsets.UTF_8));
return bytesToHex(encrypted);
} catch (Exception e) {
throw new RuntimeException("AES encryption failed", e);
}
}
@Override
public String decrypt(String cipherValue, EncryptContext encryptContext) {
if (cipherValue == null) return null;
try {
KeyGenerator keyGen = KeyGenerator.getInstance(ALGORITHM);
SecureRandom secureRandom = SecureRandom.getInstance("SHA1PRNG");
secureRandom.setSeed(SECRET_KEY.getBytes());
keyGen.init(128, secureRandom);
SecretKey secretKeySpec = new SecretKeySpec(keyGen.generateKey().getEncoded(), ALGORITHM);
Cipher cipher = Cipher.getInstance(ALGORITHM);
cipher.init(Cipher.DECRYPT_MODE, secretKeySpec);
byte[] decrypted = cipher.doFinal(hexToBytes(cipherValue));
return new String(decrypted, StandardCharsets.UTF_8);
} catch (Exception e) {
throw new RuntimeException("AES decryption failed", e);
}
}
@Override
public String getType() {
return "CUSTOM_AES"; // 必须唯一,用于 YAML 中引用
}
// 字节数组转十六进制字符串
private String bytesToHex(byte[] bytes) {
StringBuilder hexString = new StringBuilder();
for (byte b : bytes) {
String hex = Integer.toHexString(0xff & b);
if (hex.length() == 1) {
hexString.append('0');
}
hexString.append(hex);
}
return hexString.toString();
}
// 十六进制字符串转字节数组
private byte[] hexToBytes(String hex) {
int len = hex.length();
byte[] data = new byte[len / 2];
for (int i = 0; i < len; i += 2) {
data[i / 2] = (byte) ((Character.digit(hex.charAt(i), 16) << 4)
+ Character.digit(hex.charAt(i + 1), 16));
}
return data;
}
}
7.3.2.BASE64加密实现
CustomBase64EncryptAlgorithm.java
java
package com.yoyo.sharding.encrypt;
import org.apache.shardingsphere.encrypt.spi.EncryptAlgorithm;
import org.apache.shardingsphere.encrypt.spi.context.EncryptContext;
import org.springframework.stereotype.Component;
import java.util.Base64;
@Component
public class CustomBase64EncryptAlgorithm implements EncryptAlgorithm<String,String> {
@Override
public void init() {
}
@Override
public String encrypt(String plainValue, EncryptContext encryptContext) {
if (plainValue == null) return null;
return Base64.getEncoder().encodeToString(plainValue.getBytes());
}
@Override
public String decrypt(String cipherValue, EncryptContext encryptContext) {
if (cipherValue == null) return null;
return new String(Base64.getDecoder().decode(cipherValue));
}
@Override
public String getType() {
return "CUSTOM_BASE64"; // 必须唯一,用于 YAML 中引用
}
}
7.3.3.MD5加密实现
CustomMd5EncryptAlgorithm.java
java
package com.yoyo.sharding.encrypt;
import org.apache.shardingsphere.encrypt.spi.EncryptAlgorithm;
import org.apache.shardingsphere.encrypt.spi.context.EncryptContext;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
public class CustomMd5EncryptAlgorithm implements EncryptAlgorithm<String,String> {
private static final String ALGORITHM = "MD5";
@Override
public void init() {
}
@Override
public String encrypt(String plainValue, EncryptContext encryptContext) {
if (plainValue == null) {
return null;
}
try {
MessageDigest md = MessageDigest.getInstance(ALGORITHM);
byte[] digest = md.digest(plainValue.getBytes(StandardCharsets.UTF_8));
return bytesToHex(digest);
} catch (Exception e) {
throw new RuntimeException("MD5 encryption failed", e);
}
}
@Override
public String decrypt(String cipherValue, EncryptContext encryptContext) {
return cipherValue;
}
@Override
public String getType() {
return "CUSTOM_MD5";
}
// 字节数组转十六进制字符串
private String bytesToHex(byte[] bytes) {
StringBuilder hexString = new StringBuilder();
for (byte b : bytes) {
String hex = Integer.toHexString(0xff & b);
if (hex.length() == 1) {
hexString.append('0');
}
hexString.append(hex);
}
return hexString.toString();
}
}
7.3.4.SM4加密实现
CustomSm4EncryptAlgorithm.java
java
package com.yoyo.sharding.encrypt;
import org.apache.shardingsphere.encrypt.spi.EncryptAlgorithm;
import org.apache.shardingsphere.encrypt.spi.context.EncryptContext;
import org.springframework.stereotype.Component;
import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
@Component
public class CustomSm4EncryptAlgorithm implements EncryptAlgorithm<String,String> {
private static final String ALGORITHM = "SM4";
private static final String TRANSFORMATION = "SM4/ECB/PKCS5Padding";
private static final String SECRET_KEY = "1234567890abcdef";
@Override
public void init() {
}
@Override
public String encrypt(String plainValue, EncryptContext encryptContext) {
if (plainValue == null) {
return null;
}
try {
KeyGenerator keyGenerator = KeyGenerator.getInstance(ALGORITHM, "BC");
keyGenerator.init(128);
SecretKey sm4Key = new SecretKeySpec(SECRET_KEY.getBytes(StandardCharsets.UTF_8), ALGORITHM);
Cipher cipher = Cipher.getInstance(TRANSFORMATION,"BC");
cipher.init(Cipher.ENCRYPT_MODE, sm4Key);
byte[] encrypted = cipher.doFinal(plainValue.getBytes(StandardCharsets.UTF_8));
return bytesToHex(encrypted);
} catch (Exception e) {
throw new RuntimeException("SM4 encryption failed", e);
}
}
@Override
public String decrypt(String cipherValue, EncryptContext encryptContext) {
if (cipherValue == null) {
return null;
}
try {
SecretKey sm4Key = new SecretKeySpec(SECRET_KEY.getBytes(StandardCharsets.UTF_8), ALGORITHM);
Cipher cipher = Cipher.getInstance(TRANSFORMATION, "BC");
cipher.init(Cipher.DECRYPT_MODE, sm4Key);
byte[] decrypted = cipher.doFinal(hexToBytes(cipherValue));
return new String(decrypted, StandardCharsets.UTF_8);
} catch (Exception e) {
throw new RuntimeException("SM4 decryption failed", e);
}
}
@Override
public String getType() {
return "CUSTOM_SM4";
}
// 字节数组转十六进制字符串
private String bytesToHex(byte[] bytes) {
StringBuilder hexString = new StringBuilder();
for (byte b : bytes) {
String hex = Integer.toHexString(0xff & b);
if (hex.length() == 1) {
hexString.append('0');
}
hexString.append(hex);
}
return hexString.toString();
}
// 十六进制字符串转字节数组
private byte[] hexToBytes(String hex) {
int len = hex.length();
byte[] data = new byte[len / 2];
for (int i = 0; i < len; i += 2) {
data[i / 2] = (byte) ((Character.digit(hex.charAt(i), 16) << 4)
+ Character.digit(hex.charAt(i + 1), 16));
}
return data;
}
}
7.4.SPI注册自定义算法配置
resources/META-INF/services/org.apache.shardingsphere.encrypt.spi.EncryptAlgorithm
plain
com.yoyo.sharding.encrypt.CustomBase64EncryptAlgorithm
com.yoyo.sharding.encrypt.CustomAesEncryptAlgorithm
com.yoyo.sharding.encrypt.CustomSm4EncryptAlgorithm
com.yoyo.sharding.encrypt.CustomMd5EncryptAlgorithm
7.5.实体类
java
package com.yoyo.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
@Data
@TableName("t_user")
public class User {
@TableId(type = IdType.AUTO)
private Long id;
private String name;
private String phone; // 会被 Base64 加密
private String email; // 会被 AES 加密
@TableField(value = "id_card")
private String idCard; // 会被 md5 加密
private String password; // 会被 SM4 加密
}
7.6.Mapper接口
java
package com.yoyo.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.yoyo.entity.User;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface UserMapper extends BaseMapper<User>{
}
7.7.Service接口
java
package com.yoyo.service;
import com.yoyo.entity.User;
public interface UserService {
boolean saveUser(User u);
User getUserById(Long id);
}
7.8.Service实现
java
package com.yoyo.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.yoyo.entity.User;
import com.yoyo.mapper.UserMapper;
import com.yoyo.service.UserService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
@Service
@Slf4j
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
@Override
public boolean saveUser(User u) {
return this.save(u);
}
@Override
public User getUserById(Long id) {
return this.getById(id);
}
}
7.9.测试用例
java
@Test
void testUserInsert() {
User u = new User();
u.setId(1L);
u.setName("zhangsan");
u.setEmail("zhangsan@example.com");
u.setPhone("18888888888");
u.setIdCard("371425999999999999");
u.setPassword("1111111111");
if(userService.saveUser(u)){
log.info("数据写入成功");
} else {
log.info("数据写入失败");
}
}
@Test
void testGetUserById() {
Long userId = 1L;
User user = userService.getUserById(userId);
if(user != null){
System.out.println("+++++++++++++++++++++++++++");
System.out.println("用户ID:" + user.getId());
System.out.println("用户NAME:" + user.getName());
System.out.println("用户PASSWORD:" + user.getPassword());
System.out.println("用户EMAIL:" + user.getEmail());
System.out.println("用户PHONE:" + user.getPhone());
System.out.println("用户ID_CARD:" + user.getIdCard());
System.out.println("+++++++++++++++++++++++++++");
}
}
8.读写分离
8.1.数据准备
sql
--创建数据库
create database master_db;
create database slave1_db;
create database slave2_db;
-- 在主库和所有从库中执行
CREATE TABLE IF NOT EXISTS t_role (
id BIGSERIAL PRIMARY KEY,
role_name VARCHAR(50) NOT NULL,
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
deleted INTEGER DEFAULT 0
);
8.2.主配置文件(application.yaml)
yaml
spring:
shardingsphere:
datasource:
names: master,slave1,slave2
master:
type: com.zaxxer.hikari.HikariDataSource
driver-class-name: org.postgresql.Driver
jdbc-url: jdbc:postgresql://192.168.100.108:5866/master_db
username: sysdba
password: Qwer@12345
hikari:
connection-timeout: 30000
idle-timeout: 600000
max-lifetime: 1800000
maximum-pool-size: 20
minimum-idle: 5
slave1:
type: com.zaxxer.hikari.HikariDataSource
driver-class-name: org.postgresql.Driver
jdbc-url: jdbc:postgresql://192.168.100.108:5866/slave1_db
username: sysdba
password: Qwer@12345
hikari:
connection-timeout: 30000
idle-timeout: 600000
max-lifetime: 1800000
maximum-pool-size: 20
minimum-idle: 5
slave2:
type: com.zaxxer.hikari.HikariDataSource
driver-class-name: org.postgresql.Driver
jdbc-url: jdbc:postgresql://192.168.100.108:5866/slave2_db
username: sysdba
password: Qwer@12345
hikari:
connection-timeout: 30000
idle-timeout: 600000
max-lifetime: 1800000
maximum-pool-size: 20
minimum-idle: 5
rules:
readwrite-splitting:
data-sources:
rw_ds: # 读写分离数据源名称(逻辑数据源)
type: Static # 静态读写分离配置
props:
# 写数据源名称(必须配置)
write-data-source-name: master
# 读数据源名称列表(必须配置,多个用逗号分隔)
read-data-source-names: slave1,slave2
# 负载均衡算法名称(可选)
load-balancer-name: round_robin
# 负载均衡算法配置
load-balancers:
# 轮询算法
round_robin:
type: ROUND_ROBIN
# 随机访问算法
random:
type: RANDOM
# 权重访问算法
weight:
type: WEIGHT
props:
slave1: 2
slave2: 1
props:
sql-show: true
query-with-cipher-column: true # 是否使用密文列查询
# MyBatis Plus 配置
mybatis-plus:
configuration:
map-underscore-to-camel-case: true
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
global-config:
db-config:
id-type: ASSIGN_ID
logic-delete-field: deleted
logic-delete-value: 1
logic-not-delete-value: 0
# 应用配置
server:
port: 8080
8.3.实体类
java
package com.yoyo.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.time.LocalDateTime;
@Data
@TableName("t_role")
public class Role {
@TableId(value = "id", type = IdType.AUTO)
private Long id;
@TableField("role_name")
private String roleName;
@TableField(value = "create_time")
private LocalDateTime createTime;
@TableField(value = "update_time")
private LocalDateTime updateTime;
private Integer deleted;
}
8.4.Mapper接口
java
package com.yoyo.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.yoyo.entity.Role;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface RoleMapper extends BaseMapper<Role> {
}
8.5.Service接口
java
package com.yoyo.service;
import com.yoyo.entity.Role;
import java.util.List;
public interface RoleService {
/**
* 添加角色(写操作)
*/
boolean addUser(Role role);
/**
* 根据角色名称查询(读操作)
*/
Role getRoleByName(String name);
/**
* 更新角色信息(写操作)
*/
boolean updateRole(Role role);
/**
* 删除角色(写操作)
*/
boolean deleteRole(Long id);
}
8.6.Service实现
java
package com.yoyo.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.yoyo.entity.Order;
import com.yoyo.entity.Role;
import com.yoyo.mapper.OrderMapper;
import com.yoyo.mapper.RoleMapper;
import com.yoyo.service.OrderService;
import com.yoyo.service.RoleService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
import java.util.List;
@Slf4j
@Service
public class RoleServiceImpl extends ServiceImpl<RoleMapper, Role> implements RoleService {
@Override
public boolean addUser(Role role) {
return this.save(role);
}
@Override
public Role getRoleByName(String name) {
QueryWrapper<Role> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("role_name",name);
return this.getOne(queryWrapper);
}
@Override
public boolean updateRole(Role role) {
return this.updateById(role);
}
@Override
public boolean deleteRole(Long id) {
return this.removeById(id);
}
}
8.7.测试用例
java
@Test
void testRoleInsert() {
Role role = new Role();
role.setId(1L);
role.setRoleName("管理员");
role.setCreateTime(LocalDateTime.now());
if(roleService.save(role)){
log.info("数据写入成功");
} else {
log.info("数据写入失败");
}
}
@Test
void testGetRoleByName() {
String roleName = "管理员";
Role role = roleService.getRoleByName(roleName);
if(role != null){
System.out.println("+++++++++++++++++++++++++++");
System.out.println("角色ID:" + role.getId());
System.out.println("用户NAME:" + role.getRoleName());
System.out.println("+++++++++++++++++++++++++++");
}
}
@Test
void testUpdateRoleById() {
Role role = new Role();
role.setId(1L);
role.setRoleName("审计员");
role.setUpdateTime(LocalDateTime.now());
if(roleService.updateRole(role)){
log.info("数据修改成功");
} else {
log.info("数据修改失败");
}
}
@Test
void testDeleteRoleById() {
if(roleService.deleteRole(1L)){
log.info("数据删除成功");
} else {
log.info("数据删除失败");
}
}