mybatis二级缓存默认非线程安全,依赖显式配置readwritelock(如readonly="false")才能保障并发读写安全;未配置时多线程写入易引发concurrentmodificationexception或脏数据,且跨namespace关联表更新会导致数据不一致。

MyBatis 二级缓存本身不是线程安全的默认实现,但它的设计支持线程安全——关键在于是否启用读写锁(ReadWriteLock)以及缓存实现是否具备并发能力。当搭配线程池进行高并发更新时,线程安全问题不会自动消失,反而会因共享缓存和跨线程操作被放大。
二级缓存的线程安全机制依赖显式配置
MyBatis 默认的 PerpetualCache 是基于 HashMap 的内存缓存,本身不保证线程安全。它只有在你为某个 <cache></cache> 标签显式配置了 readOnly="false" 并启用 ReadWriteLock(如 <cache readonly></cache>)时,才会包装一层 SynchronizedCache 或使用 ReentrantReadWriteLock 控制并发读写。
- 未配置
readOnly="false"时:多个线程同时写入可能引发ConcurrentModificationException或脏数据覆盖 - 配置后:读操作可并发,写操作互斥,但仅限于单个 namespace 内部缓存对象
- 注意:
readOnly="true"(默认)表示缓存值不可变,MyBatis 会直接返回对象引用,此时若业务层修改了返回对象,会影响其他线程——这不是缓存本身的问题,而是对象共享导致的副作用
线程池加剧缓存失效与竞争风险
使用线程池执行大量更新操作(如批量 update、delete)时,每个线程都可能触发所在 namespace 的缓存清空逻辑(flushCacheIfRequired),而这个清空动作是同步且全量的:
- 一个线程执行
updateUser,立刻清空整个UserMappernamespace 下所有 select 缓存 - 多个线程高频更新同一 namespace,会导致缓存频繁失效、命中率趋近于零,还可能因锁竞争拖慢吞吐
- 如果多个线程同时执行不同 namespace 的更新(如
UserMapper.update()和OrderMapper.update()),它们互不影响;但若共用同一个 namespace,则清空动作会相互干扰
真正危险的是跨 namespace 数据一致性
二级缓存按 namespace 隔离,但业务上常存在关联表操作。例如:
-
UserMapper缓存了用户信息 -
OrderMapper缓存了订单列表(含用户昵称) - 当线程池中某线程更新了用户昵称(触发
UserMapper缓存清空),但OrderMapper缓存未失效 → 订单列表仍显示旧昵称 - 这种不一致无法靠 MyBatis 自身机制解决,因为缓存隔离天然割裂了业务语义
更稳妥的替代方案
与其在线程池 + 二级缓存组合中“修修补补”,不如换用更可控的缓存策略:
- 关闭 MyBatis 二级缓存,在 Service 层统一接入 Redis,并由业务控制 key 粒度(如
user:1001、order:list:uid_1001)和失效时机 - 对强一致性要求高的场景,直接禁用缓存,或采用数据库行级锁 + 乐观锁(version 字段)保障更新安全
- 若坚持用二级缓存,务必限定在单表、低更新频次、无跨表关联查询的只读或读多写少场景,并配合
flushInterval定期刷新,而非依赖 DML 触发清空
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南











