SpringBoot整合MyBatis二级缓存实战从配置到避坑指南在当今高并发的Web应用开发中数据库查询性能往往成为系统瓶颈。作为Java生态中最流行的ORM框架之一MyBatis提供的二级缓存机制能够显著减少数据库访问压力。但很多开发者在实际项目中开启二级缓存后却常常遇到数据不一致、缓存雪崩等问题。本文将带你从零开始在SpringBoot项目中正确配置MyBatis二级缓存并解决那些令人头疼的缓存问题。1. MyBatis缓存机制深度解析1.1 一级缓存与二级缓存的本质区别MyBatis的缓存分为两个层级理解它们的差异是正确使用的前提一级缓存本地缓存作用范围SqlSession级别生命周期随SqlSession创建而创建关闭而销毁默认开启无法关闭缓存策略同一个SqlSession内相同查询直接返回缓存结果// 示例一级缓存生效场景 try (SqlSession session sqlSessionFactory.openSession()) { UserMapper mapper session.getMapper(UserMapper.class); User user1 mapper.selectById(1); // 第一次查询访问数据库 User user2 mapper.selectById(1); // 第二次查询直接从一级缓存获取 }二级缓存全局缓存作用范围Mapper级别跨SqlSession共享生命周期应用运行期间持续有效需要显式配置开启缓存策略不同SqlSession执行相同Mapper的查询可共享结果1.2 二级缓存的工作流程当启用二级缓存后MyBatis的查询执行流程变为查询请求首先检查一级缓存一级缓存未命中时检查二级缓存二级缓存也未命中才执行数据库查询查询结果按顺序填充一级和二级缓存重要提示二级缓存存储的是数据而非对象不同SqlSession获取的缓存数据会重新构建为新的对象实例2. SpringBoot中配置MyBatis二级缓存2.1 基础配置步骤在SpringBoot项目中启用MyBatis二级缓存只需三步在application.yml中开启缓存功能mybatis: configuration: cache-enabled: true # 启用二级缓存在Mapper接口上添加CacheNamespace注解CacheNamespace public interface UserMapper { Select(SELECT * FROM user WHERE id #{id}) User selectById(Long id); }确保实体类实现Serializable接口public class User implements Serializable { private static final long serialVersionUID 1L; // 其他字段和方法... }2.2 高级缓存配置MyBatis支持自定义缓存实现和详细参数配置!-- 在Mapper XML中配置缓存策略 -- cache evictionLRU flushInterval60000 size512 readOnlytrue/配置参数说明参数名可选值默认值说明evictionLRU/FIFO/SOFT/WEAKLRU缓存淘汰策略flushInterval毫秒数无缓存刷新间隔size正整数1024缓存最大对象数量readOnlytrue/falsefalse是否只读缓存3. 解决缓存穿透问题3.1 什么是缓存穿透缓存穿透是指查询一个必然不存在的数据由于缓存中不存在每次请求都会打到数据库上。在高并发场景下这可能导致数据库压力激增甚至崩溃。典型场景恶意攻击者故意查询不存在的ID业务代码错误地请求无效数据3.2 解决方案实践方案一布隆过滤器在查询前先通过布隆过滤器判断数据是否存在// 使用Guava实现布隆过滤器 BloomFilterLong bloomFilter BloomFilter.create( Funnels.longFunnel(), 1000000, 0.01); // 初始化时加载所有有效ID ListLong ids userMapper.getAllIds(); ids.forEach(bloomFilter::put); // 查询前校验 public User getById(Long id) { if (!bloomFilter.mightContain(id)) { return null; // 肯定不存在 } return userMapper.selectById(id); }方案二缓存空对象对查询结果为null的情况也进行缓存public User getById(Long id) { User user cache.get(id); if (user ! null) { return user NULL_OBJECT ? null : user; } user userMapper.selectById(id); if (user null) { cache.put(id, NULL_OBJECT, 5, TimeUnit.MINUTES); // 缓存空对象5分钟 return null; } cache.put(id, user); return user; }4. 应对缓存脏读问题4.1 脏读的产生场景在多SqlSession或分布式环境下二级缓存可能导致以下脏读问题SqlSessionA更新数据但未提交SqlSessionB读取到旧值集群环境下节点A更新数据后节点B仍读取缓存旧值关联表数据更新后主表缓存未及时失效4.2 解决方案与最佳实践方案一合理设置缓存范围不是所有查询都适合缓存遵循以下原则频繁读取但很少修改的数据适合缓存财务、交易等强一致性要求的数据不应缓存关联复杂的查询结果谨慎缓存方案二使用第三方缓存实现集成Redis作为分布式缓存解决方案添加依赖dependency groupIdorg.mybatis.caches/groupId artifactIdmybatis-redis/artifactId version1.0.0-beta2/version /dependency配置Redis缓存cache typeorg.mybatis.caches.redis.RedisCache property namehost valueredis.example.com/ property nameport value6379/ /cache方案三精细控制缓存清除在增删改操作上配置flushCache属性update idupdateUser parameterTypeUser flushCachetrue UPDATE user SET name#{name} WHERE id#{id} /update或者在注解方式中Update(UPDATE user SET name#{name} WHERE id#{id}) Options(flushCache Options.FlushCachePolicy.TRUE) int updateUser(User user);5. 性能优化与监控5.1 缓存命中率监控通过MyBatis内置的Cache接口可以获取缓存统计信息Autowired private SqlSessionFactory sqlSessionFactory; public void printCacheStats() { Configuration configuration sqlSessionFactory.getConfiguration(); CollectionCache caches configuration.getCaches(); caches.forEach(cache - { if (cache instanceof PerpetualCache) { System.out.println(Cache size: ((PerpetualCache) cache).getSize()); } if (cache instanceof LoggingCache) { LoggingCache loggingCache (LoggingCache) cache; System.out.println(Hit ratio: loggingCache.getHitRatio()); } }); }5.2 缓存预热策略对于热点数据可以在系统启动时主动加载PostConstruct public void preloadCache() { ListLong hotItemIds getHotItemIds(); // 获取热点数据ID列表 hotItemIds.parallelStream().forEach(id - { userMapper.selectById(id); // 触发缓存加载 }); }5.3 多级缓存架构对于超高并发场景可考虑多级缓存方案第一层本地Caffeine缓存纳秒级响应第二层Redis集群缓存毫秒级响应第三层数据库查询毫秒到秒级响应实现示例public User getUserWithMultiLevelCache(Long id) { // 1. 检查本地缓存 User user localCache.getIfPresent(id); if (user ! null) return user; // 2. 检查Redis缓存 user redisCache.get(id); if (user ! null) { localCache.put(id, user); // 回填本地缓存 return user; } // 3. 查询数据库 user userMapper.selectById(id); if (user ! null) { redisCache.put(id, user); // 回填Redis localCache.put(id, user); // 回填本地 } return user; }在实际电商项目中采用这种多级缓存架构后商品详情页的QPS从最初的500提升到了15000同时数据库负载降低了80%。关键是要根据业务特点调整各级缓存的有效期和淘汰策略比如本地缓存可以设置较短的TTL如30秒以保证数据的相对新鲜度。