
TL;DR场景MyBatis 持久层开发需要跨 SqlSession 共享热点数据进一步减少数据库访问结论二级缓存基于 Mapper namespace 共享commit/增删改自动清空必须配合 Serializable 序列化产出掌握二级缓存原理、配置步骤、commit 清空机制与 useCache/flushCache 高级控制核心关键词MyBatis、二级缓存、Mapper namespace、cacheEnabled、cache、commit 清空、Serializable 序列化、useCache、flushCache、MyBatis 3.5版本矩阵功能状态说明二级缓存默认行为⚠️ 需手动开启cacheEnabled 默认 true但 Mapper 需显式声明cache/才生效Mapper namespace 作用域✅ 已验证多个 SqlSession 共享同一 namespace 的缓存同 namespace 共享缓存区✅ 已验证多个 Mapper 文件 namespace 相同时会缓存到同一区域commit 自动清空缓存✅ 已验证INSERT/UPDATE/DELETE commit 触发 namespace 缓存清空Serializable 强制要求✅ 已验证缓存对象需实现 Serializable 以支持反序列化到不同介质useCache 控制单条 SQL✅ 已验证设置 false 可禁用该 select 的二级缓存默认 trueflushCache 自动刷新✅ 已验证增删改默认 flushCachetrue无需手动设置自定义 Cache 实现✅ 已验证通过 type 参数指定实现了 Cache 接口的自定义类MyBatis 3.5.x 支持✅ 已验证2020 年发布至今持续维护缓存机制稳定二级缓存二级缓存原理和一级缓存的原理是一样的第一次查询将数据放入到数据库中第二次查询就会去缓存中取但是一级缓存是基于 SqlSession 的而二级缓存是基于 Mapper 的 namespace 的也就是说多个 SqlSession 可以共享一个 Mapper 中的二级缓存并且如果两个 Mapper 的 namespace 相同的话即使是两个 Mapper也会缓存到相同的区域中。MyBatis 提供了两级缓存机制一级缓存和二级缓存。一级缓存是 SqlSession 级别的作用域仅限于当前会话默认开启。而二级缓存是跨 SqlSession 的作用域为 Mapper 映射文件级别需要手动配置开启。工作原理二级缓存是一个共享的缓存区域存储的查询结果可以被多个 SqlSession 复用从而减少数据库查询操作提高性能。其核心工作流程如下查询时的流程先从二级缓存中查找数据。如果二级缓存未命中则去一级缓存中查找。一级缓存也未命中则执行 SQL 查询数据库并将结果存入一级缓存和二级缓存。更新时的流程当某个操作如 INSERT、UPDATE 或 DELETE导致数据发生改变时MyBatis 会自动清空与该 Mapper 相关的二级缓存。优缺点优点减少数据库访问次数提高系统性能。跨 SqlSession 共享对频繁查询的数据特别有用。缺点数据一致性问题如果数据库数据发生变化缓存数据可能不一致。内存占用增加缓存需要消耗内存存储数据。复杂度增加需要额外管理缓存的清理和更新。开启缓存二级缓存默认是关闭的我们需要开启需要修改 sqlMapConfig.xmlsettingssettingnamecacheEnabledvaluetrue//settings对应的截图如下所示其次我们需要到对应的 Mapper 中进行缓存的开启这里我们用 UserMapper 作为例子在其中加入!--开启二级缓存--cache/cache对应的截图如下所示可以看到 Mapper.xml 中文件就这么一个空的标签这里其实是可以加一些参数的如果不加的话会用默认的实现类。我们也可以实现 Cache 接口来用我们自己的缓存类。使用 type 参数来更换实现类注意事项由于开启了二级缓存之后还需要将缓存中的 model 对象实现 Serializable 接口为了将缓存数据取出执行反序列化操作因为二级缓存的数据可能会有不同的介质不一定是在内存中存储的如果是到 Redis、磁盘等就需要进行序列化和反序列化。DataBuilderAllArgsConstructorNoArgsConstructorpublicclassWzkUserimplementsSerializable{privateintid;privateStringusername;privateStringpassword;privateDatebirthday;privateListWzkOrderorderList;privateListWzkRoleroleList;}对应的截图如下所示编写代码这里我们测试 二级缓存。publicclassWzkicuCache03{publicstaticvoidmain(String[]args)throwsIOException{InputStreaminputStreamResources.getResourceAsStream(sqlMapConfig.xml);SqlSessionFactorysqlSessionFactorynewSqlSessionFactoryBuilder().build(inputStream);SqlSessionsqlSession1sqlSessionFactory.openSession();SqlSessionsqlSession2sqlSessionFactory.openSession();UserMapperuserMapper1sqlSession1.getMapper(UserMapper.class);UserMapperuserMapper2sqlSession2.getMapper(UserMapper.class);ListWzkUserusers1userMapper1.findAll();sqlSession1.close();ListWzkUserusers2userMapper2.findAll();sqlSession2.close();}}测试代码运行上述的代码控制台的输出结果如下24/11/1310:52:09 DEBUG UserMapper.findAll:Preparing:select*,o.id oid from wzk_user u leftjoinwzk_orders o onu.ido.uid;24/11/1310:52:09 DEBUG UserMapper.findAll:Parameters:24/11/1310:52:09 DEBUG UserMapper.findAll:Total:3对应的截图如下所示开启了二级缓存之后可以看到 两个不同的 SqlSession第二次查询依然不会发出 SQL。编写代码这里我们通过 commit 清空缓存来进行测试publicclassWzkicuCache04{publicstaticvoidmain(String[]args)throwsIOException{InputStreaminputStreamResources.getResourceAsStream(sqlMapConfig.xml);SqlSessionFactorysqlSessionFactorynewSqlSessionFactoryBuilder().build(inputStream);SqlSessionsqlSession1sqlSessionFactory.openSession();SqlSessionsqlSession2sqlSessionFactory.openSession();SqlSessionsqlSession3sqlSessionFactory.openSession();UserMapperuserMapper1sqlSession1.getMapper(UserMapper.class);UserMapperuserMapper2sqlSession2.getMapper(UserMapper.class);UserMapperuserMapper3sqlSession3.getMapper(UserMapper.class);ListWzkUserusers1userMapper1.findAll();sqlSession1.close();WzkUserwzkUserWzkUser.builder().id(1).username(wzk-update).password(123-update).build();userMapper2.updateById(wzkUser);sqlSession2.commit();sqlSession2.close();ListWzkUserusers3userMapper3.findAll();sqlSession3.close();}}对应的截图如下所示测试代码执行上述的代码结果如下:24/11/1310:58:50 DEBUG UserMapper.updateById:Preparing: UPDATE wzk_user SETusername?,password? WHEREid?24/11/1310:58:50 DEBUG UserMapper.updateById:Parameters: wzk-update(String),123-update(String),1(Integer)24/11/1310:58:50 DEBUG UserMapper.updateById:Updates:124/11/1310:58:50 DEBUG jdbc.JdbcTransaction: Committing JDBC Connection[com.mysql.cj.jdbc.ConnectionImpl4690b489]24/11/1310:58:50 DEBUG jdbc.JdbcTransaction: Resetting autocommit totrueon JDBC Connection[com.mysql.cj.jdbc.ConnectionImpl4690b489]24/11/1310:58:50 DEBUG jdbc.JdbcTransaction: Closing JDBC Connection[com.mysql.cj.jdbc.ConnectionImpl4690b489]24/11/1310:58:50 DEBUG pooled.PooledDataSource: Returned connection1183888521to pool.24/11/1310:58:50 DEBUG mapper.UserMapper: Cache Hit Ratio[icu.wzk.mapper.UserMapper]:0.024/11/1310:58:50 DEBUG jdbc.JdbcTransaction: Opening JDBC Connection24/11/1310:58:50 DEBUG pooled.PooledDataSource: Checked out connection1183888521from pool.24/11/1310:58:50 DEBUG jdbc.JdbcTransaction: Setting autocommit tofalseon JDBC Connection[com.mysql.cj.jdbc.ConnectionImpl4690b489]24/11/1310:58:50 DEBUG UserMapper.findAll:Preparing:select*,o.id oid from wzk_user u leftjoinwzk_orders o onu.ido.uid;24/11/1310:58:50 DEBUG UserMapper.findAll:Parameters:对应的截图如下所示可以看到commit 之后二级缓存也被清空了所以后续的查询还是发出了 SQL。额外配置MyBatis 中还配以 useCache 和 flushCache 等配置项目useCache 是用来设置是否禁用二级缓存的在 statement 中设置 useCache false 可以禁用当前 select 语句的二级缓存即每次查询都会发送 SQL 去查询默认情况是 true即该 SQL 使用二级缓存selectidfindAllresultMapuserMapuseCachefalseselect *,o.id oid from wzk_user u left join wzk_orders o on u.ido.uid;/select一般情况下执行完 commit 都需要刷新缓存flushCache true 表示刷新缓存这样可以避免数据库脏读所以不需要设置默认即可。错误速查卡症状根因定位修复开启二级缓存后报序列化错误model 对象未实现 Serializable检查实体类声明实体类加implements Serializable不同 SqlSession 第二次查询仍发 SQLMapper 中未配置cache/标签检查对应 Mapper.xml添加cache/标签开启 namespace 缓存缓存命中率始终为 0sqlMapConfig.xml 未启用 cacheEnabled检查全局配置设置setting namecacheEnabled valuetrue/commit 后数据未刷新到缓存缺少 commit 操作检查 sqlSession.commit() 是否调用增删改后必须执行 commit 才会清空二级缓存期望走缓存却发 SQL该 select 配置了 useCachefalse检查 select 标签移除 useCachefalse 或按需保留多个 Mapper 缓存互相污染namespace 命名冲突检查 mapper namespace 是否重复用cache-ref namespace.../显式共享缓存与数据库不一致高并发下其他服务修改了数据库检查数据源是否有写操作绕开 MyBatis引入分布式缓存失效通知或缩短 flushInterval作者武子康的个人博客