1870 字
约 6 分钟
6
S08-A04 · 分布式与微服务下的持久层

适用读者:分库分表之后发现"事务不好使、分页不对、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. 自测

  1. 分片后失效的四类 SQL 能力?各自改写方式?
  2. 动态数据源失效的四步因果链?
  3. 跨库写三种方案的边界?
  4. 微服务跨服务查询四种方案与代价?
  5. 为什么"共用一个库的微服务"是伪微服务?
答案
  1. join/排序分页/聚合/事务;冗余字段或广播表、二次查询法、中间件归并、补偿或 outbox。
  2. 切换晚于事务、SqlSessionFactory 未分、ThreadLocal 未清、嵌套事务占用连接。
  3. outbox 默认、TCC/SAGA 可补偿、2PC 少用。
  4. API 组合、数据副本、CQRS 读库、数仓;分别对应延迟、最终一致、双模型、分钟级延迟。
  5. 数据边界形同虚设,改表需协调多服务,退化为分布式单体。

9. 下一篇

S08-X02 · 重复导出比对登记 —— 26 对同文章节的字符数比对表、比对方法,以及"为什么这批旧文重复率高达 98%"。


本篇属于系列《S08 · MyBatis 持久层》。旧文来源:A-3068、A-3070/3071/3072(章号版 4 章);扩展点 E-S08-028/029/030。

S08-A04 · 分布式与微服务下的持久层
https://www.clxhxhhr.top/posts/5296/
作者
clxstart
发布于
2024-12-31
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。