
在数据量快速增长的今天,分表分库已成为解决数据库性能瓶颈的常用手段。Mybatis Plus作为优秀的ORM框架,ShardingSphere-JDBC作为成熟的分库分表中间件,两者组合使用可以大大提升开发效率和系统性能。然而,组合使用时也存在诸多容易踩坑的地方,本文将详细探讨这些避坑点,并提供丰富的实战案例和解决方案。
Mybatis-Plus 负责对象关系映射(ORM)和单表操作的增强,而 ShardingSphere-JDBC 负责拦截 SQL 并进行分库分表的路由、改写和执行结果归并。它们两个是协同工作的关系,Mybatis-Plus 并不感知分库分表的存在。
IService, BaseMapper)、强大的条件构造器(QueryWrapper, UpdateWrapper)、分页插件(PaginationInnerInterceptor)等。MappedStatement 和 SQL 语句。DataSource, Connection, PreparedStatement 等核心接口。应用层代码操作的是 Sharding-JDBC 提供的数据源,而真正的物理数据库连接被隐藏在底层。当你在一个项目中同时引入 MP 和 Sharding-JDBC 后,一个 SQL 请求的生命周期如下:
步骤 1: 应用层调用你的代码调用 MP 提供的 baseMapper.insert(entity) 或 service.page(page, queryWrapper) 等方法。
步骤 2: MP 发挥作用
userMapper.selectById(1),MP 会生成 SELECT * FROM t_user WHERE id = 1。@TableName 中指定的名字,比如 t_user。它并不知道这个表被分片了。步骤 3: Sharding-JDBC 拦截并处理(核心)这是最核心的一步。Sharding-JDBC 像一道关卡,拦截了所有从 MP (或者说从 MyBatis) 发出的 SQL。
t_user 表根据 id % 2 分表(t_user_0, t_user_1),那么对于 id = 1 的查询,路由引擎会计算出它应该被路由到 t_user_1 表。LIMIT 10, 20 的 SQL。但在分片环境下,直接使用 LIMIT 会导致结果错误。Sharding-JDBC 会将其改写为 LIMIT 0, 30 ((10+20)),在每个分片上获取更多数据,为后续的结果归并做准备。SELECT * FROM t_user WHERE id = 1 改写为 SELECT * FROM t_user_1 WHERE id = 1。id = 1),通常只会路由到一个节点,无需复杂归并。LIMIT 0, 30 的结果集,在内存中进行排序和合并,最终跳过前 10 条,取接下来的 20 条,返回给客户端,模拟完整数据库的分页行为。COUNT(), SUM(),需要对各个分片的结果进行汇总计算。步骤 4: 结果返回
问题描述:ShardingSphere-JDBC需要对SQL进行解析以确定分片,而Mybatis Plus的自定义SQL或复杂动态SQL可能导致SQL解析失败。
深度解析:
<if>嵌套多层)会增加解析难度解决方案:
# application.yml
spring:
shardingsphere:
rules:
sharding:
tables:
t_order:
actual-data-nodes:ds_${0..1}.t_order_${0..1}
table-strategy:
inline:
sharding-column:order_id
algorithm-expression:t_order_${order_id%2}
# 确保SQL解析器正确配置
sql-parser:
type:ShardingSphereSQLParser
# 可选:设置解析器参数
props:
sql-comment-enabled:true
sql-pretty-enabled:true
最佳实践:
QueryWrapper或LambdaQueryWrapper生成SQL<if>@Select注解并确保格式规范问题描述:Mybatis Plus自带的分页插件(PageHelper)与ShardingSphere-JDBC的分页处理机制冲突,可能导致分页结果不准确或性能下降。
深度解析:
LIMIT子句解决方案:
// 正确做法:使用ShardingSphere-JDBC的分页功能
Page<Order> page = new Page<>(1, 10);
Page<Order> result = orderMapper.selectPage(page, null);
// 避免使用Mybatis Plus的PageHelper
// PageHelper.startPage(1, 10);
// List<Order> orders = orderMapper.selectList(null);
性能对比:
问题描述:Mybatis Plus的主键生成策略(如@TableId(type = IdType.AUTO))与ShardingSphere-JDBC的主键生成策略冲突。
深度解析:
IdType.AUTO依赖数据库自增ID解决方案:
// 实体类配置
publicclass Order {
@TableId(type = IdType.INPUT)
private Long id;
// 其他字段
}
// ShardingSphere-JDBC配置
spring:
shardingsphere:
rules:
sharding:
tables:
t_order:
table-strategy:
inline:
sharding-column: order_id
algorithm-expression: t_order_${order_id % 2}
key-generate-strategy:
column: id
key-generator-name: snowflake
key-generators:
snowflake:
type: SNOWFLAKE
props:
worker-id: 123
注意:IdType.INPUT表示主键由应用层生成,ShardingSphere-JDBC会使用配置的主键生成器。
问题描述:查询条件中不包含分片键,导致ShardingSphere-JDBC无法准确路由,执行全表扫描。
深度解析:
全表扫描在数据量大时性能极差解决方案:
// 正确做法:包含分片键
QueryWrapper<Order> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("user_id", 123); // user_id是分片键
// 使用Hint手动指定分片键(当无法在查询条件中包含时)
try (HintManager hintManager = HintManager.getInstance()) {
hintManager.addTableHint("t_order", "user_id=123");
List<Order> orders = orderMapper.selectList(queryWrapper);
}
性能对比:
问题描述:跨分片的事务管理变得复杂,可能导致数据不一致。
深度解析:
解决方案:
# application.yml
spring:
shardingsphere:
rules:
sharding:
# 配置分布式事务
transaction:
type: Seata
provider-type: SeataAT
最佳实践:
问题描述:对于需要全节点查询的表(如字典表),未正确配置为广播表,导致查询性能下降。
深度解析:
解决方案:
spring:
shardingsphere:
rules:
sharding:
tables:
t_dict:
actual-data-nodes: ds_${0..1}.t_dict
# 标记为广播表
broadcast-tables: t_dict
使用示例:
// 查询广播表,ShardingSphere-JDBC自动处理
List<Dict> dicts = dictMapper.selectList(null);
// 实际执行的SQL会自动路由到所有分片
问题描述:组合使用时,动态SQL和分片条件结合可能增加SQL注入风险。
深度解析:
解决方案:
// 使用条件构造器,避免SQL注入
QueryWrapper<Order> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("user_id", userId); // userId应经过严格校验
// 严格校验用户输入
if (!userId.matches("^\\d+$")) {
throw new IllegalArgumentException("无效的用户ID");
}
安全最佳实践:
问题描述:分片算法设计不合理,导致数据分布不均,出现热点分片。
深度解析:
解决方案:
// 自定义分片算法示例
publicclass UserOrderShardingAlgorithm implements PreciseShardingAlgorithm<Long> {
@Override
public String doSharding(Collection<String> availableTargetNames, PreciseShardingValue<Long> shardingValue) {
Long userId = shardingValue.getValue();
int shardCount = availableTargetNames.size();
// 使用一致性哈希算法,避免热点
int shardIndex = (int) (userId.hashCode() % shardCount);
return availableTargetNames.stream()
.skip(shardIndex)
.findFirst()
.orElse(availableTargetNames.iterator().next());
}
}
分片键选择原则:
问题描述:在并发场景下,多个用户或服务同时更新同一数据时,若未采取并发控制机制,后执行的更新操作可能覆盖前一个用户的修改,导致数据不一致。
深度解析:
updateById更新所有非空字段直接覆盖数据库中的数据覆盖先执行的更新解决方案:
// 实体类配置
publicclass Order {
@TableId(type = IdType.AUTO)
private Long id;
@Version
private Integer version;
// 其他字段
}
// 配置MyBatis Plus乐观锁
mybatis-plus:
global-config:
optimistic-locker:
enable: true
// 更新操作示例
Order order = new Order();
order.setId(1001L);
order.setStatus("PAID");
// 执行更新时自动校验version字段
int rows = orderMapper.updateById(order);
if (rows == 0) {
thrownew RuntimeException("数据已被其他用户修改,请重试");
}
原理:在SQL更新语句中自动添加WHERE version = #{version}条件,确保更新基于最新版本。
// 查询当前订单的最新状态
Order currentOrder = orderMapper.selectById(1001L);
// 构造更新条件,确保原始状态与数据库一致
QueryWrapper<Order> updateWrapper = new QueryWrapper<>();
updateWrapper.eq("id", 1001L).eq("status", currentOrder.getStatus());
Order newOrder = new Order();
newOrder.setStatus("SHIPPED");
int rows = orderMapper.update(newOrder, updateWrapper);
if (rows == 0) {
throw new RuntimeException("数据已被其他用户修改");
}
适用场景:业务逻辑中需要校验特定字段的原始值。
// 仅更新状态字段,避免覆盖其他字段
UpdateWrapper<Order> updateWrapper = new UpdateWrapper<>();
updateWrapper.eq("id", 1001L).set("status", "SHIPPED");
int rows = orderMapper.update(null, updateWrapper);
优势:通过UpdateWrapper指定需要更新的字段,确保未修改的字段不会被覆盖为null。
深度对比:
方法 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
乐观锁 | 自动处理,简单高效 | 需要额外字段 | 大多数业务场景 |
显式校验 | 灵活,可校验特定字段 | 需要额外查询 | 业务逻辑特殊需求 |
字段级更新 | 避免覆盖,无需额外字段 | 无法处理并发 | 低并发场景 |
避坑点 | 核心建议 | 实施要点 |
|---|---|---|
SQL解析 | 避免复杂SQL | 使用QueryWrapper,避免XML中嵌套复杂条件 |
分页查询 | 用ShardingSphere-JDBC分页 | 禁用Mybatis Plus分页插件 |
主键生成 | 使用ShardingSphere-JDBC主键生成器 | @TableId(type = IdType.INPUT) + 配置key-generator |
查询条件 | 包含分片键 | 确保查询条件包含分片键,必要时用Hint |
事务管理 | 避免跨分片事务 | 业务设计尽量单分片,必要时用Seata |
广播表 | 正确配置广播表 | broadcast-tables配置 |
SQL注入 | 严格校验输入 | 使用条件构造器,禁止SQL拼接 |
分片算法 | 选择高基数字段 | 避免热点,考虑一致性哈希 |
更新覆盖 | 优先使用乐观锁 | @Version + optimistic-locker或 禁用updateById,只更新需要更新的值 |
Mybatis Plus与ShardingSphere-JDBC的组合使用是解决大数据量下数据库性能瓶颈的有效方案,但需要特别注意SQL解析、分页处理、主键生成、查询条件、事务管理、广播表配置、SQL注入、分片算法设计以及更新操作中的并发控制。
通过合理配置和遵循最佳实践,可以充分发挥两者的优点,构建高性能、高可用、高一致性的系统。在实际项目中,建议:
分表分库不是一蹴而就的过程,需要根据业务发展和数据增长持续优化,才能真正发挥其价值。希望本文能帮助你避开Mybatis Plus与ShardingSphere-JDBC组合使用中的各种坑,构建更加健壮的系统。