适用读者:分库分表之后发现"事务不好使、分页不对、join 不能写"的人。
前置:S08-A01(分库分表)、A02(多数据源)、J06(进阶机制)、S11-A04、S17(分布式)。
覆盖旧文(章号版):3068(第15章 MyBatis Plus)、3070(第17章 分布式应用)、3071(第18章 微服务架构)、3072(第19章 Mapper 接口进阶)→ 4 章 / 约 4.1 万字符。
扩展点:E-S08-028(分片后的四类 SQL 失效与改写)、E-S08-029(动态数据源与事务失效的因果链)、E-S08-030(微服务数据归属的三种跨服务查询方案)。
下一篇:S08-X02 · 重复导出比对登记。
1. 一句话结论
分库分表不是"加个中间件",是把数据库的一部分能力收回去:跨片 join、跨片事务、全局排序、精确分页都要你重新实现。能用单库解决的,别分片(判据见 S11-A04 三条触发线)。
出处:
A-3070(分布式应用)、A-3071(微服务架构)、A-3068(MyBatis Plus)。
2. 分片后的四类 SQL 失效与改写(E-S08-028)
| 能力 | 单库 | 分片后 | 改写方式 |
|---|---|---|---|
| join | 直接 join | 跨片无法 join | ① 冗余字段(把常用维度写进主表)② 广播表(小字典表复制到每片)③ 应用层两次查询 + 内存组装 |
| 排序 + 分页 | ORDER BY … LIMIT |
各片排序后需归并 | ① 内存归并(LIMIT offset+size 从每片取,再截断)② 二次查询法(用最小值/最大值定位) |
| 聚合 | COUNT/SUM |
各片聚合再合并 | 中间件归并或预聚合汇总表 |
| 事务 | 本地事务 | 跨片无本地事务 | 见 §3 |
-- 分片后的「二次查询法」分页(避免每片都扫 offset+size)
-- 第 1 步:各片取 offset+size 条,得到各片最小值 min_i
-- 第 2 步:以 min(min_i) 为下界重新查各片,取 size 条
-- 第 3 步:内存合并排序 → 精确第 offset 页
判据:分片键选错,上面四项全部失效——如果 80% 查询不带分片键,分片只会让所有查询变成广播。
3. 动态数据源与事务失效的因果链(E-S08-029)
public class RoutingDataSource extends AbstractRoutingDataSource {
@Override protected Object determineCurrentLookupKey() {
return DataSourceContext.get(); // ThreadLocal
}
}
失效链(四步,务必按序排查):
① 切换发生在事务开启之后 → 事务已绑定旧连接,切换无效
修:切换必须在 @Transactional 外层(或用 @Order 更高的 AOP)
② 只切了 DataSource 没切 SqlSessionFactory → SqlSession 仍指向旧库
修:为每个数据源配独立 SqlSessionFactory + MapperScan basePackages
③ ThreadLocal 未清理 → 线程池复用导致"上一个请求的库"被沿用
修:finally 里 clear()
④ 嵌套事务(REQUIRES_NEW)切换时父事务连接仍占用 → 连接数翻倍
判据:"多数据源 + 一个 @Transactional 里切库"本质上不支持——事务与连接是绑定的。要跨库写,要么拆成两个事务(+ 补偿),要么用分布式事务方案(见下)。
4. 跨服务/跨库写:三种方案与边界
| 方案 | 机制 | 一致性 | 成本 | 适用 |
|---|---|---|---|---|
| 本地消息表(outbox) | 本地事务写业务表 + 消息表 → 独立投递 | 最终一致 | 低(无需中间件事务) | 默认推荐(见 S18-A06 §3) |
| TCC / SAGA | 显式补偿(Try-Confirm-Cancel / 正向-补偿) | 最终一致 | 中高(要写补偿逻辑) | 业务流程清晰、可补偿 |
| XA / 2PC(Seata AT) | 全局事务协调 | 强一致(理论) | 高(锁持有时间长、性能差) | 极少;短事务、低并发 |
判据:能设计成"幂等 + 可补偿"的,就不要上 2PC——2PC 的锁等待会在大促时放大成雪崩。
5. 微服务的数据归属与跨服务查询(E-S08-030)
原则:一个服务拥有自己的库(private schema),其他服务只能通过接口访问
| 跨服务查询需求 | 方案 | 代价 |
|---|---|---|
| 需要另一服务的实时单条数据 | API 组合(BFF 聚合) | 延迟叠加、需超时与降级 |
| 需要另一服务的列表/统计 | 数据副本(订阅变更/CDC → 本地只读表) | 最终一致、需重建机制 |
| 需要多服务联合筛选/排序 | CQRS 读库(写模型 + 读模型) | 双模型维护、同步链路 |
| 报表/分析 | 数仓/ES(离线/近线同步) | 延迟(分钟级) |
反例(最常见):"共用一个库,各服务各写各的表" —— 这是伪微服务,改表要协调 N 个服务,等于分布式单体(判定见 S18-A05 §4)。
6. MyBatis Plus 在分布式场景的两个用法(3068)
| 能力 | 用法 | 边界 |
|---|---|---|
| 多租户插件 | TenantLineInnerInterceptor 自动注入 tenant_id 条件 |
需排除字典表;手写 SQL 也要走插件 |
| 动态表名 | DynamicTableNameInnerInterceptor 按月/按片路由 |
表名拼接必须白名单校验(防注入) |
| 乐观锁 | @Version + OptimisticLockerInnerInterceptor |
只对 updateById 生效;批量更新需手写 |
| 分页插件 | PaginationInnerInterceptor 指定 DbType |
分片场景需分片中间件的分页能力,插件本身不跨片 |
判据:插件解决"横切条件",不解决"跨片语义"——把分片逻辑交给中间件(ShardingSphere 等),MP 只负责单库内的便利。
7. 反例:六种"分片后才发现"
| 现象 | 根因 |
|---|---|
| 分页总数不对 | 各片独立 count 后未归并去重 |
| 事务回滚一半成功 | 跨片写用了本地事务 |
| 切库后写入旧库 | 切换在事务内,或 SqlSessionFactory 未分开 |
| 线程池串库 | ThreadLocal 未清理 |
| join 查询报错 | 跨片 join 不支持,需应用层组装 |
| 全局唯一 ID 冲突 | 用了数据库自增(应雪花/号段) |
8. 自测
- 分片后失效的四类 SQL 能力?各自改写方式?
- 动态数据源失效的四步因果链?
- 跨库写三种方案的边界?
- 微服务跨服务查询四种方案与代价?
- 为什么"共用一个库的微服务"是伪微服务?
答案
- join/排序分页/聚合/事务;冗余字段或广播表、二次查询法、中间件归并、补偿或 outbox。
- 切换晚于事务、SqlSessionFactory 未分、ThreadLocal 未清、嵌套事务占用连接。
- outbox 默认、TCC/SAGA 可补偿、2PC 少用。
- API 组合、数据副本、CQRS 读库、数仓;分别对应延迟、最终一致、双模型、分钟级延迟。
- 数据边界形同虚设,改表需协调多服务,退化为分布式单体。
9. 下一篇
S08-X02 · 重复导出比对登记 —— 26 对同文章节的字符数比对表、比对方法,以及"为什么这批旧文重复率高达 98%"。
本篇属于系列《S08 · MyBatis 持久层》。旧文来源:A-3068、A-3070/3071/3072(章号版 4 章);扩展点 E-S08-028/029/030。