一、同库分表的核心场景与优势
适用场景:
- 单表数据量超500万行,但业务暂不适合跨库拆分。
- 需优化单表查询性能(如分页查询耗时过长),但不想引入跨库复杂度。
核心优势:
- 避免跨库开销:所有分表在同一数据库内,无需处理跨库JOIN、分布式事务。
- 简化运维:单库备份、监控更便捷
二、分表策略与分片规则设计
(一)三大主流分片策略
-
取模分片(最常用)
- 规则:通过
分片键 % 分表数
计算目标表名。 - 案例:订单表按
order_id % 10
拆分为10张表(order_0
至order_9
)。 - 适用场景:高频查询基于分片键(如按
order_id
查询订单)。
- 规则:通过
-
范围分片(时序数据首选)
- 规则:按时间、ID范围划分(如按年份分表:
order_2023
、order_2024
)。 - 案例:日志表按月份分表,每月生成新表
log_202506
、log_202507
。 - 优势:天然支持时序查询(如查2025年6月日志直接定位
log_202506
表)。
- 规则:按时间、ID范围划分(如按年份分表:
-
哈希分片(数据均匀分布)
- 规则:对分片键哈希后取模(如
hash(user_id) % 10
)。 - 适用场景:需确保数据均匀分布,且无明显查询维度。
- 规则:对分片键哈希后取模(如
(二)分片键选择原则
- 必须高频查询:若分片键为
user_id
,则高频查询需包含user_id
(如SELECT * FROM order WHERE user_id=123
)。 - 避免跨表JOIN:分片后若需关联查询,需提前设计冗余字段(如订单表冗余
user_name
避免JOIN用户表)。
三、分表实现的三种技术方案
(一)方案一:应用层手动分表
- 核心逻辑:在代码中动态拼接表名,直接操作分表。
- 示例(Java代码):
// 按user_id取模分表
String tableSuffix = String.valueOf(user_id % 10);
String sql = "SELECT * FROM user_" + tableSuffix + " WHERE id = ?";
PreparedStatement ps = connection.prepareStatement(sql);
- 优缺点:
- 优点:无额外依赖,适合简单场景(如分表数固定且逻辑简单)。
- 缺点:代码侵入性强,分表逻辑散落在各处,维护成本高。
(二)方案二:MyBatis插件分表(半自动化)
- 实现方式:通过MyBatis拦截器修改SQL表名。
- 配置示例(MyBatis拦截器):
@Intercepts({@Signature(method = "prepare", type = StatementHandler.class)})
public class TableShardingInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) {
// 获取原始SQL
String sql = ((BoundSql) invocation.getArgs()[0].getBoundSql()).getSql();
// 解析分片键(如从SQL参数中获取user_id)
int userId = getUserIdFromParams(...);
// 计算表后缀
String tableSuffix = String.valueOf(userId % 10);
// 替换表名
String newSql = sql.replace("user", "user_" + tableSuffix);
// 执行新SQL
...
}
}
(三)方案三:中间件自动分表
- Sharding-JDBC配置示例:
spring:
shardingsphere:
datasource:
names: master
master:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/test_db
username: root
password: 123456
sharding:
tables:
user:
actual-data-nodes: master.user_$->{0..9} # 分10张表
table-strategy:
inline:
sharding-column: user_id
algorithm-expression: user_$->{user_id % 10}
binding-tables: [user] # 绑定表,优化关联查询
- 优势:
- 透明分表:应用无需关心表名拼接,SQL按原表名书写。
- 功能完善:支持分表后分页、排序、聚合查询自动合并结果。
四、分表后的分页查询问题
(一)应用层手动分页查询
(二)数据库层分页:存储过程+临时表
(三)中间件自动优化(Sharding-JDBC)
示例:Spring Boot集成Sharding-JDBC实现分页查询
1.添加依赖(Maven)
<!-- ShardingSphere-JDBC -->
<dependency>
<groupId>org.apache.shardingsphere</groupId>
<artifactId>shardingsphere-jdbc-core-spring-boot-starter</artifactId>
<version>5.3.2</version>
</dependency>
<!-- MyBatis-Plus(可选,简化CRUD) -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.3.1</version>
</dependency>
2.配置Sharding-JDBC分片规则
spring:
shardingsphere:
mode:
type: Memory # 内存模式,适合开发测试
datasource:
names: ds0 # 数据源名称
ds0:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/test_db?useSSL=false
username: root
password: root
rules:
sharding:
tables:
user: # 逻辑表名
actual-data-nodes: ds0.user_$->{0..3} # 物理表:user_0到user_3
table-strategy:
standard:
sharding-column: user_id # 分片键
sharding-algorithm-name: user-mod # 分片算法
sharding-algorithms:
user-mod:
type: MOD # 取模算法
props:
sharding-count: 4 # 分4张表
props:
# 归并引擎类型:STREAM(流式)/MEMORY(内存)
merge.engine.type: STREAM
# 大分页查询优化(超过此阈值自动优化)
query.with.cipher.column.threshold: 1000
# 结果集最大行数(防止OOM)
max-rows: 100000
props:
sql-show: true # 打印SQL
sql-simple: false # 复杂SQL格式化
3.User实体类
@Data
@TableName("user") // 逻辑表名
public class User {
private Long id;
private String username;
private Integer age;
private Long userId; // 分片键
}
4.Mybatis接口
public interface UserMapper extends BaseMapper<User> {
// 使用MyBatis-Plus的分页查询
IPage<User> selectByPage(Page<User> page, @Param("age") Integer age);
}
<select id="selectByPage" resultType="User">
SELECT * FROM user WHERE age > #{age}
ORDER BY user_id <!-- 排序字段建议包含分片键 -->
</select>
5.UserService
@Service
public class UserServiceImpl implements UserService {
@Autowired
private UserMapper userMapper;
@Override
public IPage<User> getUsersByPage(Integer pageNum, Integer pageSize, Integer age) {
// 创建分页对象
Page<User> page = new Page<>(pageNum, pageSize);
// 执行分页查询(自动路由到各分表)
return userMapper.selectByPage(page, age);
}
}
6.UserController
@RestController
@RequestMapping("/users")
public class UserController {
@Autowired
private UserService userService;
@GetMapping
public IPage<User> listUsers(
@RequestParam(defaultValue = "1") Integer page,
@RequestParam(defaultValue = "10") Integer size,
@RequestParam(required = false) Integer age) {
return userService.getUsersByPage(page, size, age);
}
}