5.1 一级缓存与二级缓存


文档摘要

5.1 一级缓存与二级缓存 本节摘要:一级缓存挂在 SqlSession 上、默认开启,二级缓存挂在命名空间上、默认关闭。本节用可复现的实验验证一级缓存的命中与失效,解剖缓存键的构成,配置并测试二级缓存,最后复现一次跨命名空间的脏读并给出防线。 术后监护的第一台设备:缓存。它能挡住重复查询,也能在你看不见的地方端出旧数据。 一级缓存:会话内的记忆 一级缓存默认开启,不需要任何配置。用实验说话: 日志只出现两条 SELECT(id=1 与 id=2 各一条),第二次 findById(1) 没有触库。更扎眼的细节是 a == b 为 true——一级缓存返回的是同一个对象引用,改它等于改缓存里那份。 缓存键由什么构成 同一个方法、同一个参数就命中吗?不够。

5.1 一级缓存与二级缓存

本节摘要:一级缓存挂在 SqlSession 上、默认开启,二级缓存挂在命名空间上、默认关闭。本节用可复现的实验验证一级缓存的命中与失效,解剖缓存键的构成,配置并测试二级缓存,最后复现一次跨命名空间的脏读并给出防线。

术后监护的第一台设备:缓存。它能挡住重复查询,也能在你看不见的地方端出旧数据。

一级缓存:会话内的记忆

一级缓存默认开启,不需要任何配置。用实验说话:

try (SqlSession session = factory.openSession()) { UserMapper mapper = session.getMapper(UserMapper.class); User a = mapper.findById(1); // 第一次查询:打到数据库 User b = mapper.findById(1); // 第二次查询:? User c = mapper.findById(2); // 换参数再查:? System.out.println(a == b); // true:同一个对象引用,缓存命中 System.out.println(a == c); // false:不同键,两次查询 }

日志只出现两条 SELECT(id=1 与 id=2 各一条),第二次 findById(1) 没有触库。更扎眼的细节是 a == b 为 true——**一级缓存返回的是同一个对象引用**,改它等于改缓存里那份。

缓存键由什么构成

同一个方法、同一个参数就命中吗?不够。缓存键由四要素拼成:语句 id、行偏移(分页参数)、SQL 文本、传入参数值。两个查询要命中同一份缓存,四要素必须完全一致。这解释了几个常见困惑:

// 同一方法同参数,但分页参数不同:未命中 List<User> p1 = mapper.page(0, 10); List<User> p2 = mapper.page(10, 10); // 新键,触库 // SQL 文本不同的两条语句(哪怕语义等价):未命中 mapper.findByUsername("a"); // WHERE username = ? mapper.findByName("a"); // 另一条语句 id,另算一键

五种失效情形

缓存的价值取决于它多久被正确地作废。一级缓存的失效纪律:

try (SqlSession session = factory.openSession()) { UserMapper mapper = session.getMapper(UserMapper.class); mapper.findById(1); // 入缓存 mapper.updateEmail(someUser); // 情形一:本会话发生写操作 mapper.findById(1); // 缓存被清空,重新触库 mapper.findById(1); // 再次入缓存 session.clearCache(); // 情形二:手动清空 mapper.findById(1); // 触库 } // 情形三:会话关闭,缓存随会话消亡

五种情形归纳:本会话任何增删改(flushCache 默认 true)、手动 clearCache、会话关闭、换个会话(另一条会话各有各的一级缓存)、localCacheScope 收窄为 STATEMENT(每次语句后即清,等于关闭会话级共享)。最后一种值得多说一句——它正是 Spring 集成后一级缓存"好像失效了"的原因,5.5 节会接上这条线。

二级缓存:命名空间共享的记忆

一级缓存的记忆随会话关闭而消失。要让"跨会话"的重复查询也省掉,用二级缓存——它的归属对象从 SqlSession 换成了命名空间(Mapper)

开启两步:

<!-- 第一步:全局总开关(默认即 true,显式写出以明意图) --> <settings> <setting name="cacheEnabled" value="true"/> </settings>
<!-- 第二步:在命名空间内声明缓存 --> <mapper namespace="com.example.mapper.UserMapper"> <!-- eviction 回收策略;flushInterval 刷新间隔毫秒;size 对象数上限;readOnly 只读开关 --> <cache eviction="LRU" flushInterval="60000" size="512" readOnly="true"/> <select id="findById" resultType="User" useCache="true"> SELECT id, username, email FROM users WHERE id = #{id} </select> </mapper>
// 两个独立会话验证共享 try (SqlSession s1 = factory.openSession()) { s1.getMapper(UserMapper.class).findById(1); } // s1 关闭时,其结果进入该命名空间的二级缓存 try (SqlSession s2 = factory.openSession()) { s2.getMapper(UserMapper.class).findById(1); // 日志:Cache Hit Ratio [com.example.mapper.UserMapper]: 0.5 // 命中率 0.5 = 两次查询一次命中 }

三条硬性要求:实体类要实现序列化接口(缓存数据要落盘或传输时必须可序列化);readOnly 为 true 时缓存返回共享引用,调用方绝不能改,需要改就设 false 拿副本;命名空间内任何写操作会清空本命名空间的二级缓存。

05-01-fig01-3

脏读复现:跨命名空间的盲区

背景:订单查询要显示用户昵称,OrderMapper 里写了 join users 的查询;UserMapper 负责用户资料维护。两个命名空间都开了二级缓存。

复现步骤

// 第 1 步:订单查询走 OrderMapper,结果进入 OrderMapper 的二级缓存 // join 出来的昵称"张三"被缓存 try (SqlSession s = factory.openSession()) { s.getMapper(OrderMapper.class).findWithUserName(101); } // 第 2 步:用户改名,走 UserMapper try (SqlSession s = factory.openSession()) { s.getMapper(UserMapper.class) .updateNickname(1, "张三丰")); s.commit(); // UserMapper 的二级缓存被清空 } // 第 3 步:再查订单 try (SqlSession s = factory.openSession()) { s.getMapper(OrderMapper.class).findWithUserName(101); // 日志:Cache Hit Ratio [...OrderMapper]: 0.5 —— 命中旧缓存 // 返回的昵称仍是"张三":脏读 }

解读:写操作只清本命名空间(UserMapper)的二级缓存,而带用户数据的缓存在 OrderMapper 里——后者的缓存不知道前者的更新。join 越多张表,盲区越大。

防线三选一

<!-- 防线一:cache-ref 让 OrderMapper 的缓存挂到 UserMapper 名下 任一方更新都清同一份缓存(注意循环引用会导致初始化失败) --> <mapper namespace="com.example.mapper.OrderMapper"> <cache-ref namespace="com.example.mapper.UserMapper"/> </mapper>
<!-- 防线二:join 查询不进二级缓存,宁可多查一次 --> <select id="findWithUserName" resultMap="orderWithUserMap" useCache="false"> ... </select>

防线三是决策级方案:读多改少、单表归属清晰的数据(字典、配置)才开二级缓存;频繁更新或多表 join 的查询一律不开。生产系统里更常见的位置是引入独立的缓存中间件(如 Redis),由业务控制失效逻辑,把二级缓存留给真正合适的窄场景。

⚠️ 常见坑:readOnly=true 时拿到的是缓存里的共享对象,改它就是改缓存,会污染后续所有读请求。排查"字段莫名其妙变了"时,把readOnly 改 false 是快速验证手段。

💡 关键直觉:一级缓存防的是"同一会话内的重复查询",二级缓存防的是"跨会话的重复查询"。前者几乎没有代价,后者要拿一致性来换——开之前先问:这份数据多久改一次、被谁改、join 了几张表。

本节要点回顾

  • 一级缓存三事实:默认开启、会话私有、命中返回同一对象引用
  • 缓存键四要素:语句 id、分页偏移、SQL 文本、参数值,任一不同即未命中
  • 失效五情形:本会话写操作、clearCache、会话关闭、换会话、范围收窄 STATEMENT
  • 二级缓存三要求:实体可序列化、readOnly 谨慎开、命名空间内写即清空
  • 脏读盲区:写只清本命名空间,join 进来的别家数据成了无人通知的旧值
  • 选型判断:字典配置类开、频繁更新关、跨表 join 默认 useCache=false

记忆设备看完,下一节是另一条隐形缝合线:TypeHandler,Java 类型与数据库类型之间的翻译官。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U