目录
什么是分库分表
️ 基本概念
生活化理解
为什么需要分库分表
性能瓶颈场景
1. 数据量爆炸式增长
2. 查询性能急剧下降
3. 并发压力过大
✅ 分库分表带来的收益
分库分表的类型
垂直拆分(按业务功能划分)
垂直分库实例
垂直分表实例
↔️ 水平拆分(按数据量划分)
水平分库实例
水平分表实例
分库分表策略详解
分片键选择原则
1. 查询频率分析
2. 数据分布均匀性
分片算法详解
1. 取模算法(适用于均匀分布数据)
2. 范围算法(适用于有明显范围特征的数据)
3. 哈希算法(适用于字符串类型分片键)
4. 一致性哈希算法(适用于动态扩容场景)
具体实现方法(重点)
️ 方案一:应用层手动分片(推荐新手学习)
1. 项目结构设计
2. 数据源配置
3. 分片路由器实现
4. DAO层实现
5. Service层实现
方案二:ShardingSphere中间件(推荐生产环境)
1. 项目依赖
2. 详细配置文件
3. 自定义分片算法
4. 实体类定义
5. Repository接口
6. Service层使用
数据迁移实战
数据迁移策略
1. 停机迁移(适合小规模数据)
2. 双写迁移(推荐生产环境)
3. 数据补偿机制
常见问题与解决方案
⚠️ 分布式事务问题
1. 问题描述
2. 解决方案:Seata分布式事务
3. TCC补偿模式
跨库查询问题
1. 数据冗余解决方案
2. 应用层聚合解决方案
3. 搜索引擎解决方案
全局唯一ID问题
1. 雪花算法实现
2. Redis分布式ID生成器
性能优化实践
查询优化
1. 索引优化策略
2. 查询路由优化
3. 缓存优化策略
连接池优化
完整项目案例
电商订单系统完整实现
1. 项目结构
2. 核心配置文件
3. 实体类设计
4. 完整的订单服务实现
5. 控制器实现
运维监控
监控指标设计
1. 分片性能监控
2. 业务监控指标
3. 告警配置
4. 自动化运维脚本
5. Grafana监控面板配置
6. 容量规划工具
总结与最佳实践
关键要点回顾
1. 何时选择分库分表?
2. 分片策略选择指南
3. 实施步骤最佳实践
⚡ 性能优化秘籍
1. 查询优化
2. 索引设计
3. 连接池配置
常见坑点和避坑指南
1. 分片键选择错误
2. 跨分片事务处理不当
3. 扩容时数据迁移错误
进阶技巧
1. 动态扩容方案
2. 读写分离+分库分表
上线前检查清单
发展趋势和建议
1. 技术演进方向
2. 选型建议
结语
分库分表是一种数据库架构设计模式,通过将单一数据库或表拆分成多个数据库或表来提升系统性能和可扩展性。
想象你经营一家超大型图书馆:
原始状态(单库单表)
巨型图书馆
├── 1000万本书全部堆在一个大厅
├── 只有一个管理员
├── 找书需要翻遍整个大厅
└── 借书还书都排长队
分库(水平分库)
连锁图书馆
├── 按地区分:北京馆、上海馆、广州馆、深圳馆
├── 每个馆250万本书
├── 每个馆独立管理员
└── 就近借书,速度更快
分表(水平分表)
单个图书馆内部分区
├── 按类型分:文学区、科技区、历史区、艺术区
├── 每个区域独立编号
├── 找书直接去对应区域
└── 管理更有序
-- 用户表增长轨迹
2020年:100万用户
2021年:500万用户
2022年:2000万用户 ← 查询开始变慢
2023年:5000万用户 ← 已经很慢了
2024年:1亿用户 ← 必须分库分表
-- 单表数据量与查询性能关系
数据量 查询时间 索引大小
100万 10ms 100MB
500万 50ms 500MB
1000万 200ms 1GB
5000万 2000ms 5GB ← 不可接受
1亿 10000ms 10GB ← 系统崩溃边缘
单数据库连接池:200个连接
高峰期并发:2000个请求
结果:连接不够用,大量请求等待或失败
优化项目 | 优化前 | 优化后 | 提升倍数 |
---|---|---|---|
查询性能 | 2000ms | 20ms | 100倍 |
并发能力 | 200QPS | 2000QPS | 10倍 |
存储容量 | 单机限制 | 无限扩展 | ∞ |
可用性 | 单点故障 | 部分故障 | 10倍 |
电商系统拆分前:
ecommerce_db
├── users (用户表)
├── products (商品表)
├── orders (订单表)
├── payments (支付表)
├── inventory (库存表)
├── reviews (评论表)
├── coupons (优惠券表)
└── logistics (物流表)
拆分后:
用户中心库 (user_center_db)
├── users
├── user_profiles
└── user_settings
商品中心库 (product_center_db)
├── products
├── categories
├── brands
└── specifications
订单中心库 (order_center_db)
├── orders
├── order_items
└── order_status_log
支付中心库 (payment_center_db)
├── payments
├── payment_methods
└── payment_logs
用户表拆分前:
CREATE TABLE users (
id BIGINT PRIMARY KEY,
username VARCHAR(50), -- 基础信息
email VARCHAR(100), -- 基础信息
password VARCHAR(255), -- 基础信息
phone VARCHAR(20), -- 基础信息
avatar TEXT, -- 扩展信息(大字段)
description TEXT, -- 扩展信息(大字段)
preferences JSON, -- 扩展信息(大字段)
last_login TIMESTAMP, -- 统计信息
login_count INT, -- 统计信息
create_time TIMESTAMP, -- 基础信息
update_time TIMESTAMP -- 基础信息
);
拆分后:
-- 基础信息表(高频访问)
CREATE TABLE user_basic (
id BIGINT PRIMARY KEY,
username VARCHAR(50),
email VARCHAR(100),
password VARCHAR(255),
phone VARCHAR(20),
create_time TIMESTAMP,
update_time TIMESTAMP
);
-- 扩展信息表(低频访问,大字段)
CREATE TABLE user_profile (
user_id BIGINT PRIMARY KEY,
avatar TEXT,
description TEXT,
preferences JSON,
INDEX idx_user_id (user_id)
);
-- 统计信息表(分析用)
CREATE TABLE user_statistics (
user_id BIGINT PRIMARY KEY,
last_login TIMESTAMP,
login_count INT,
total_orders INT,
total_amount DECIMAL(10,2),
INDEX idx_user_id (user_id)
);
订单系统分库:
-- 分库策略:按用户ID取模
用户ID % 4 = 0 → order_db_0
用户ID % 4 = 1 → order_db_1
用户ID % 4 = 2 → order_db_2
用户ID % 4 = 3 → order_db_3
-- 示例分配
用户12345 → 12345 % 4 = 1 → order_db_1
用户12346 → 12346 % 4 = 2 → order_db_2
用户12347 → 12347 % 4 = 3 → order_db_3
用户12348 → 12348 % 4 = 0 → order_db_0
按时间分表:
-- 日志表按月分表
log_202401 -- 2024年1月数据
log_202402 -- 2024年2月数据
log_202403 -- 2024年3月数据
...
-- 订单表按ID分表
orders_0 -- order_id % 8 = 0
orders_1 -- order_id % 8 = 1
orders_2 -- order_id % 8 = 2
...
orders_7 -- order_id % 8 = 7
-- 分析现有查询模式
SELECT
COUNT(*) as query_count,
query_type,
where_conditions
FROM slow_query_log
WHERE query_time > 1
GROUP BY query_type, where_conditions
ORDER BY query_count DESC;
-- 常见查询模式
90% 查询:SELECT * FROM orders WHERE user_id = ?
5% 查询:SELECT * FROM orders WHERE order_id = ?
3% 查询:SELECT * FROM orders WHERE create_time BETWEEN ? AND ?
2% 查询:其他复杂查询
-- 结论:选择 user_id 作为分片键
-- 检查数据分布
SELECT
user_id % 8 as shard_id,
COUNT(*) as data_count
FROM orders
GROUP BY user_id % 8
ORDER BY shard_id;
-- 理想结果(数据均匀分布)
shard_id | data_count
---------|----------
0 | 1250000
1 | 1250000
2 | 1250000
3 | 1250000
4 | 1250000
5 | 1250000
6 | 1250000
7 | 1250000
/**
* 取模分片算法
* 优点:简单、数据分布均匀
* 缺点:扩容困难(需要重新分布数据)
*/
public class ModShardingAlgorithm {
private final int shardCount;
public ModShardingAlgorithm(int shardCount) {
this.shardCount = shardCount;
}
/**
* 计算分库索引
*/
public int getDbIndex(Long userId) {
return (int) (userId % shardCount);
}
/**
* 计算分表索引
*/
public int getTableIndex(Long orderId) {
return (int) (orderId % shardCount);
}
/**
* 获取完整的分片信息
*/
public ShardInfo getShardInfo(Long userId, Long orderId) {
return ShardInfo.builder()
.dbIndex(getDbIndex(userId))
.tableIndex(getTableIndex(orderId))
.dbName("order_db_" + getDbIndex(userId))
.tableName("orders_" + getTableIndex(orderId))
.build();
}
}
// 使用示例
ModShardingAlgorithm algorithm = new ModShardingAlgorithm(8);
ShardInfo shard = algorithm.getShardInfo(12345L, 67890L);
// 结果:dbName=order_db_1, tableName=orders_2
/**
* 范围分片算法
* 优点:范围查询效率高、扩容相对简单
* 缺点:可能存在热点问题
*/
public class RangeShardingAlgorithm {
/**
* 按时间范围分表
*/
public String getTableByTime(LocalDateTime createTime) {
String yearMonth = createTime.format(DateTimeFormatter.ofPattern("yyyyMM"));
return "orders_" + yearMonth;
}
/**
* 按ID范围分表
*/
public String getTableById(Long id) {
if (id < 1000000) {
return "user_0";
} else if (id < 2000000) {
return "user_1";
} else if (id < 3000000) {
return "user_2";
} else {
return "user_3";
}
}
/**
* 按地区分库
*/
public String getDbByRegion(String region) {
switch (region.toLowerCase()) {
case "北京":
case "天津":
case "河北":
return "order_db_north";
case "上海":
case "江苏":
case "浙江":
return "order_db_east";
case "广东":
case "广西":
case "海南":
return "order_db_south";
default:
return "order_db_default";
}
}
}
/**
* 哈希分片算法
* 优点:分布更加均匀
* 缺点:不支持范围查询
*/
public class HashShardingAlgorithm {
private final int shardCount;
public HashShardingAlgorithm(int shardCount) {
this.shardCount = shardCount;
}
/**
* 使用一致性哈希算法
*/
public int getShardIndex(String key) {
return Math.abs(key.hashCode()) % shardCount;
}
/**
* 使用CRC32哈希(分布更均匀)
*/
public int getCrc32ShardIndex(String key) {
CRC32 crc32 = new CRC32();
crc32.update(key.getBytes());
return (int) (Math.abs(crc32.getValue()) % shardCount);
}
/**
* 使用MD5哈希(安全性更高)
*/
public int getMd5ShardIndex(String key) {
try {
MessageDigest md5 = MessageDigest.getInstance("MD5");
byte[] hash = md5.digest(key.getBytes());
// 取hash值的前4个字节转换为int
int hashCode = ByteBuffer.wrap(hash).getInt();
return Math.abs(hashCode) % shardCount;
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException("MD5算法不支持", e);
}
}
}
/**
* 一致性哈希算法
* 优点:扩容时数据迁移量小
* 缺点:实现复杂,可能存在数据倾斜
*/
public class ConsistentHashShardingAlgorithm {
private final TreeMap hashRing = new TreeMap<>();
private final int virtualNodes = 150; // 虚拟节点数量
public ConsistentHashShardingAlgorithm(List nodes) {
initHashRing(nodes);
}
/**
* 初始化哈希环
*/
private void initHashRing(List nodes) {
for (String node : nodes) {
addNode(node);
}
}
/**
* 添加节点
*/
public void addNode(String node) {
for (int i = 0; i < virtualNodes; i++) {
String virtualNode = node + "#" + i;
long hash = hash(virtualNode);
hashRing.put(hash, node);
}
}
/**
* 移除节点
*/
public void removeNode(String node) {
for (int i = 0; i < virtualNodes; i++) {
String virtualNode = node + "#" + i;
long hash = hash(virtualNode);
hashRing.remove(hash);
}
}
/**
* 获取分片节点
*/
public String getNode(String key) {
if (hashRing.isEmpty()) {
return null;
}
long hash = hash(key);
Map.Entry entry = hashRing.ceilingEntry(hash);
// 如果没有找到大于等于hash的节点,则使用第一个节点
if (entry == null) {
entry = hashRing.firstEntry();
}
return entry.getValue();
}
/**
* 哈希函数
*/
private long hash(String key) {
CRC32 crc32 = new CRC32();
crc32.update(key.getBytes());
return crc32.getValue();
}
}
// 使用示例
List nodes = Arrays.asList("db_0", "db_1", "db_2", "db_3");
ConsistentHashShardingAlgorithm algorithm = new ConsistentHashShardingAlgorithm(nodes);
// 查找数据应该存储的节点
String node = algorithm.getNode("user_12345");
System.out.println("数据存储节点:" + node);
// 动态添加节点
algorithm.addNode("db_4");
src/main/java/com/example/sharding/
├── config/
│ ├── DataSourceConfig.java # 数据源配置
│ ├── ShardingConfig.java # 分片配置
│ └── TransactionConfig.java # 事务配置
├── algorithm/
│ ├── ShardingAlgorithm.java # 分片算法接口
│ ├── ModShardingAlgorithm.java # 取模算法实现
│ └── RangeShardingAlgorithm.java # 范围算法实现
├── router/
│ ├── ShardingRouter.java # 分片路由器
│ └── DataSourceRouter.java # 数据源路由器
├── service/
│ ├── UserService.java # 业务服务
│ └── OrderService.java # 订单服务
└── dao/
├── UserDao.java # 用户数据访问层
└── OrderDao.java # 订单数据访问层
/**
* 多数据源配置
*/
@Configuration
public class DataSourceConfig {
/**
* 配置多个数据源
*/
@Bean("dataSource0")
@ConfigurationProperties(prefix = "spring.datasource.db0")
public DataSource dataSource0() {
return DruidDataSourceBuilder.create().build();
}
@Bean("dataSource1")
@ConfigurationProperties(prefix = "spring.datasource.db1")
public DataSource dataSource1() {
return DruidDataSourceBuilder.create().build();
}
@Bean("dataSource2")
@ConfigurationProperties(prefix = "spring.datasource.db2")
public DataSource dataSource2() {
return DruidDataSourceBuilder.create().build();
}
@Bean("dataSource3")
@ConfigurationProperties(prefix = "spring.datasource.db3")
public DataSource dataSource3() {
return DruidDataSourceBuilder.create().build();
}
/**
* 数据源映射
*/
@Bean
public Map dataSourceMap() {
Map map = new HashMap<>();
map.put("db_0", dataSource0());
map.put("db_1", dataSource1());
map.put("db_2", dataSource2());
map.put("db_3", dataSource3());
return map;
}
}
配置文件(application.yml):
spring:
datasource:
db0:
url: jdbc:mysql://192.168.1.100:3306/order_db_0?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT%2B8
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
initial-size: 5
min-idle: 5
max-active: 20
db1:
url: jdbc:mysql://192.168.1.101:3306/order_db_1?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT%2B8
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
initial-size: 5
min-idle: 5
max-active: 20
db2:
url: jdbc:mysql://192.168.1.102:3306/order_db_2?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT%2B8
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
initial-size: 5
min-idle: 5
max-active: 20
db3:
url: jdbc:mysql://192.168.1.103:3306/order_db_3?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT%2B8
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
initial-size: 5
min-idle: 5
max-active: 20
# 分片配置
sharding:
database:
count: 4
prefix: order_db_
table:
count: 8
prefix: orders_
/**
* 分片路由器 - 核心组件
*/
@Component
public class ShardingRouter {
@Autowired
private Map dataSourceMap;
@Autowired
private ShardingAlgorithm shardingAlgorithm;
@Value("${sharding.database.count}")
private int dbCount;
@Value("${sharding.table.count}")
private int tableCount;
/**
* 获取数据源
*/
public DataSource getDataSource(Long shardingKey) {
int dbIndex = shardingAlgorithm.getDbIndex(shardingKey, dbCount);
String dbName = "db_" + dbIndex;
DataSource dataSource = dataSourceMap.get(dbName);
if (dataSource == null) {
throw new RuntimeException("数据源不存在:" + dbName);
}
return dataSource;
}
/**
* 获取表名
*/
public String getTableName(Long shardingKey, String baseTableName) {
int tableIndex = shardingAlgorithm.getTableIndex(shardingKey, tableCount);
return baseTableName + "_" + tableIndex;
}
/**
* 获取完整的分片信息
*/
public ShardInfo getShardInfo(Long dbShardingKey, Long tableShardingKey, String baseTableName) {
DataSource dataSource = getDataSource(dbShardingKey);
String tableName = getTableName(tableShardingKey, baseTableName);
return ShardInfo.builder()
.dataSource(dataSource)
.tableName(tableName)
.dbIndex(shardingAlgorithm.getDbIndex(dbShardingKey, dbCount))
.tableIndex(shardingAlgorithm.getTableIndex(tableShardingKey, tableCount))
.build();
}
}
/**
* 分片信息封装类
*/
@Data
@Builder
public class ShardInfo {
private DataSource dataSource;
private String tableName;
private int dbIndex;
private int tableIndex;
}
/**
* 订单DAO - 手动分片实现
*/
@Repository
public class OrderDao {
@Autowired
private ShardingRouter shardingRouter;
@Autowired
private JdbcTemplate jdbcTemplate;
/**
* 保存订单
*/
public void saveOrder(Order order) {
// 获取分片信息
ShardInfo shardInfo = shardingRouter.getShardInfo(
order.getUserId(), // 分库键
order.getOrderId(), // 分表键
"orders" // 基础表名
);
// 构建SQL
String sql = String.format(
"INSERT INTO %s (order_id, user_id, product_id, amount, status, create_time) VALUES (?, ?, ?, ?, ?, ?)",
shardInfo.getTableName()
);
// 使用指定数据源执行SQL
JdbcTemplate shardJdbcTemplate = new JdbcTemplate(shardInfo.getDataSource());
shardJdbcTemplate.update(sql,
order.getOrderId(),
order.getUserId(),
order.getProductId(),
order.getAmount(),
order.getStatus(),
order.getCreateTime()
);
log.info("订单保存成功 - 数据库索引:{},表名:{}",
shardInfo.getDbIndex(), shardInfo.getTableName());
}
/**
* 根据订单ID查询订单
*/
public Order getOrderById(Long orderId, Long userId) {
ShardInfo shardInfo = shardingRouter.getShardInfo(userId, orderId, "orders");
String sql = String.format(
"SELECT * FROM %s WHERE order_id = ?",
shardInfo.getTableName()
);
JdbcTemplate shardJdbcTemplate = new JdbcTemplate(shardInfo.getDataSource());
try {
return shardJdbcTemplate.queryForObject(sql, new BeanPropertyRowMapper<>(Order.class), orderId);
} catch (EmptyResultDataAccessException e) {
return null;
}
}
/**
* 根据用户ID查询订单列表(需要查询所有分表)
*/
public List getOrdersByUserId(Long userId) {
List allOrders = new ArrayList<>();
// 确定数据库
DataSource dataSource = shardingRouter.getDataSource(userId);
JdbcTemplate shardJdbcTemplate = new JdbcTemplate(dataSource);
// 遍历所有分表
for (int i = 0; i < 8; i++) {
String tableName = "orders_" + i;
String sql = String.format(
"SELECT * FROM %s WHERE user_id = ? ORDER BY create_time DESC",
tableName
);
try {
List orders = shardJdbcTemplate.query(sql,
new BeanPropertyRowMapper<>(Order.class), userId);
allOrders.addAll(orders);
} catch (Exception e) {
log.warn("查询表{}失败:{}", tableName, e.getMessage());
}
}
// 排序(因为是从多个表查询的结果)
allOrders.sort((o1, o2) -> o2.getCreateTime().compareTo(o1.getCreateTime()));
return allOrders;
}
/**
* 更新订单状态
*/
public boolean updateOrderStatus(Long orderId, Long userId, String status) {
ShardInfo shardInfo = shardingRouter.getShardInfo(userId, orderId, "orders");
String sql = String.format(
"UPDATE %s SET status = ?, update_time = ? WHERE order_id = ?",
shardInfo.getTableName()
);
JdbcTemplate shardJdbcTemplate = new JdbcTemplate(shardInfo.getDataSource());
int affected = shardJdbcTemplate.update(sql, status, new Date(), orderId);
return affected > 0;
}
/**
* 批量查询订单(跨分片查询)
*/
public List getOrdersByIds(List orderIds, Long userId) {
if (orderIds.isEmpty()) {
return new ArrayList<>();
}
// 按分片分组
Map> shardOrderIds = new HashMap<>();
for (Long orderId : orderIds) {
ShardInfo shardInfo = shardingRouter.getShardInfo(userId, orderId, "orders");
String key = shardInfo.getDbIndex() + "_" + shardInfo.getTableName();
shardOrderIds.computeIfAbsent(key, k -> new ArrayList<>()).add(orderId);
}
List allOrders = new ArrayList<>();
// 分片查询
for (Map.Entry> entry : shardOrderIds.entrySet()) {
String[] parts = entry.getKey().split("_");
int dbIndex = Integer.parseInt(parts[0]);
String tableName = parts[1] + "_" + parts[2];
DataSource dataSource = shardingRouter.getDataSource(userId);
JdbcTemplate shardJdbcTemplate = new JdbcTemplate(dataSource);
String sql = String.format(
"SELECT * FROM %s WHERE order_id IN (%s)",
tableName,
entry.getValue().stream().map(id -> "?").collect(Collectors.joining(","))
);
List orders = shardJdbcTemplate.query(sql,
new BeanPropertyRowMapper<>(Order.class),
entry.getValue().toArray());
allOrders.addAll(orders);
}
return allOrders;
}
}
/**
* 订单服务层
*/
@Service
@Transactional
public class OrderService {
@Autowired
private OrderDao orderDao;
@Autowired
private IdGenerator idGenerator; // 全局ID生成器
/**
* 创建订单
*/
public Order createOrder(OrderCreateRequest request) {
// 生成全局唯一订单ID
Long orderId = idGenerator.nextId();
// 构建订单对象
Order order = Order.builder()
.orderId(orderId)
.userId(request.getUserId())
.productId(request.getProductId())
.amount(request.getAmount())
.status("CREATED")
.createTime(new Date())
.updateTime(new Date())
.build();
// 保存到分片数据库
orderDao.saveOrder(order);
log.info("订单创建成功:{}", orderId);
return order;
}
/**
* 查询订单详情
*/
public Order getOrderDetail(Long orderId, Long userId) {
return orderDao.getOrderById(orderId, userId);
}
/**
* 查询用户订单列表
*/
public PageResult getUserOrders(Long userId, int page, int size) {
// 查询所有订单
List allOrders = orderDao.getOrdersByUserId(userId);
// 手动分页
int total = allOrders.size();
int start = (page - 1) * size;
int end = Math.min(start + size, total);
List pageOrders = allOrders.subList(start, end);
return PageResult.builder()
.data(pageOrders)
.total(total)
.page(page)
.size(size)
.build();
}
/**
* 更新订单状态
*/
public boolean updateOrderStatus(Long orderId, Long userId, String status) {
boolean success = orderDao.updateOrderStatus(orderId, userId, status);
if (success) {
log.info("订单状态更新成功:{} -> {}", orderId, status);
} else {
log.warn("订单状态更新失败:{}", orderId);
}
return success;
}
/**
* 统计用户订单数量(需要跨分片统计)
*/
public OrderStatistics getUserOrderStatistics(Long userId) {
List orders = orderDao.getOrdersByUserId(userId);
Map statusCount = orders.stream()
.collect(Collectors.groupingBy(Order::getStatus, Collectors.counting()));
BigDecimal totalAmount = orders.stream()
.map(Order::getAmount)
.reduce(BigDecimal.ZERO, BigDecimal::add);
return OrderStatistics.builder()
.totalCount(orders.size())
.totalAmount(totalAmount)
.statusCount(statusCount)
.build();
}
}
org.apache.shardingsphere
shardingsphere-jdbc-core-spring-boot-starter
5.3.2
mysql
mysql-connector-java
8.0.33
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-starter-data-jpa
# application.yml
spring:
shardingsphere:
# 数据源配置
datasource:
names: ds0,ds1,ds2,ds3
# 数据源0
ds0:
type: com.zaxxer.hikari.HikariDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
jdbc-url: jdbc:mysql://192.168.1.100:3306/order_db_0?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai
username: root
password: 123456
hikari:
maximum-pool-size: 20
minimum-idle: 5
connection-timeout: 30000
idle-timeout: 600000
max-lifetime: 1800000
# 数据源1
ds1:
type: com.zaxxer.hikari.HikariDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
jdbc-url: jdbc:mysql://192.168.1.101:3306/order_db_1?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai
username: root
password: 123456
hikari:
maximum-pool-size: 20
minimum-idle: 5
connection-timeout: 30000
idle-timeout: 600000
max-lifetime: 1800000
# 数据源2
ds2:
type: com.zaxxer.hikari.HikariDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
jdbc-url: jdbc:mysql://192.168.1.102:3306/order_db_2?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai
username: root
password: 123456
hikari:
maximum-pool-size: 20
minimum-idle: 5
connection-timeout: 30000
idle-timeout: 600000
max-lifetime: 1800000
# 数据源3
ds3:
type: com.zaxxer.hikari.HikariDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
jdbc-url: jdbc:mysql://192.168.1.103:3306/order_db_3?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai
username: root
password: 123456
hikari:
maximum-pool-size: 20
minimum-idle: 5
connection-timeout: 30000
idle-timeout: 600000
max-lifetime: 1800000
# 分片规则配置
rules:
sharding:
# 分片算法配置
sharding-algorithms:
# 数据库分片算法
database-mod:
type: MOD
props:
sharding-count: 4
# 订单表分片算法
order-table-mod:
type: MOD
props:
sharding-count: 8
# 用户表分片算法
user-table-mod:
type: MOD
props:
sharding-count: 4
# 按时间分片算法
order-time-range:
type: CLASS_BASED
props:
strategy: STANDARD
algorithmClassName: com.example.algorithm.OrderTimeShardingAlgorithm
# 分布式序列算法
key-generators:
snowflake:
type: SNOWFLAKE
props:
worker-id: 1
# 表分片配置
tables:
# 订单表分片配置
t_order:
# 实际数据节点
actual-data-nodes: ds$->{0..3}.t_order_$->{0..7}
# 分库策略
database-strategy:
standard:
sharding-column: user_id
sharding-algorithm-name: database-mod
# 分表策略
table-strategy:
standard:
sharding-column: order_id
sharding-algorithm-name: order-table-mod
# 主键生成策略
key-generate-strategy:
column: order_id
key-generator-name: snowflake
# 用户表分片配置
t_user:
actual-data-nodes: ds$->{0..3}.t_user_$->{0..3}
database-strategy:
standard:
sharding-column: user_id
sharding-algorithm-name: database-mod
table-strategy:
standard:
sharding-column: user_id
sharding-algorithm-name: user-table-mod
key-generate-strategy:
column: user_id
key-generator-name: snowflake
# 订单项表分片配置
t_order_item:
actual-data-nodes: ds$->{0..3}.t_order_item_$->{0..7}
database-strategy:
standard:
sharding-column: user_id
sharding-algorithm-name: database-mod
table-strategy:
standard:
sharding-column: order_id
sharding-algorithm-name: order-table-mod
# 绑定表配置(关联表,确保在同一个分片)
binding-tables:
- t_order,t_order_item
# 广播表配置(每个数据库都有完整数据的表)
broadcast-tables:
- t_config
- t_dictionary
# 属性配置
props:
# 是否显示SQL
sql-show: true
# 是否在日志中显示简单风格的SQL
sql-simple: false
# 工作线程数量,默认为CPU核数 * 2
executor-size: 16
# 每个物理数据库为每次查询分配的最大连接数量
max-connections-size-per-query: 1
# 是否在启动时检查分片元数据的一致性
check-table-metadata-enabled: false
# JPA配置
spring:
jpa:
hibernate:
ddl-auto: none
show-sql: false
properties:
hibernate:
dialect: org.hibernate.dialect.MySQL8Dialect
format_sql: true
# 日志配置
logging:
level:
org.apache.shardingsphere: INFO
com.example: DEBUG
/**
* 自定义时间范围分片算法
*/
public class OrderTimeShardingAlgorithm implements StandardShardingAlgorithm {
@Override
public String doSharding(Collection availableTargetNames,
PreciseShardingValue shardingValue) {
Date createTime = shardingValue.getValue();
String suffix = DateFormatUtils.format(createTime, "yyyyMM");
for (String targetName : availableTargetNames) {
if (targetName.endsWith(suffix)) {
return targetName;
}
}
// 如果没有找到对应的表,使用默认表
return availableTargetNames.iterator().next();
}
@Override
public Collection doSharding(Collection availableTargetNames,
RangeShardingValue shardingValue) {
Range range = shardingValue.getValueRange();
Date start = range.lowerEndpoint();
Date end = range.upperEndpoint();
Set result = new HashSet<>();
// 计算时间范围内涉及的所有表
LocalDate startDate = start.toInstant().atZone(ZoneId.systemDefault()).toLocalDate();
LocalDate endDate = end.toInstant().atZone(ZoneId.systemDefault()).toLocalDate();
LocalDate current = startDate.withDayOfMonth(1);
while (!current.isAfter(endDate)) {
String suffix = current.format(DateTimeFormatter.ofPattern("yyyyMM"));
for (String targetName : availableTargetNames) {
if (targetName.endsWith(suffix)) {
result.add(targetName);
break;
}
}
current = current.plusMonths(1);
}
return result;
}
}
<