尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

MyBatis两级缓存机制详解与性能优化实践

MyBatis两级缓存机制详解与性能优化实践 1. MyBatis缓存机制全景解析作为Java生态中最受欢迎的ORM框架之一MyBatis的缓存设计直接影响着系统性能表现。在实际项目中我发现很多开发者对缓存的工作顺序存在误解导致出现数据不一致或性能不升反降的情况。今天我们就来深入拆解MyBatis缓存查询的完整流程以及其中容易被忽视的关键细节。MyBatis采用两级缓存架构一级缓存本地缓存默认开启生命周期与SqlSession绑定二级缓存全局缓存需要显式配置由多个SqlSession共享。当执行查询时缓存检查的顺序和命中逻辑直接决定了最终的性能表现和数据一致性。理解这个顺序对于处理缓存穿透、雪崩以及分布式环境下的数据同步等问题至关重要。2. 缓存查询顺序的完整流程2.1 一级缓存优先检查原则当执行select语句时MyBatis会首先检查一级缓存。这个缓存的实现原理是在BaseExecutor中维护了一个PerpetualCache实例其本质就是一个HashMap。检查时会使用完整的SQL语句包括参数值作为key因此以下两个查询会被视为不同的缓存项// 示例1两个不同参数的查询 User user1 session.selectOne(getUserById, 1); // 首次查询访问数据库 User user2 session.selectOne(getUserById, 1); // 命中一级缓存 User user3 session.selectOne(getUserById, 2); // 新参数访问数据库一级缓存的生效需要满足三个条件相同的SQL ID和参数相同的分页参数RowBounds未执行过任何insert/update/delete操作除非配置了flushCachetrue重要提示在Spring集成环境中默认的SqlSessionTemplate会在每次查询后关闭会话导致一级缓存失效。需要通过事务注解Transactional保持会话。2.2 二级缓存的检查时机如果一级缓存未命中MyBatis会检查是否启用了二级缓存。二级缓存的配置需要两步在mybatis-config.xml中全局启用settings setting namecacheEnabled valuetrue/ /settings在Mapper XML中为具体namespace配置mapper namespacecom.example.UserMapper cache evictionLRU flushInterval60000 size512/ /mapper二级缓存的工作特点跨SqlSession共享同一个Mapper namespace内默认采用LRU淘汰策略序列化存储要求实体类实现Serializable事务提交后才放入缓存避免脏读2.3 数据库查询的最终执行当两级缓存均未命中时才会执行真正的数据库查询。查询结果会按照以下顺序回填结果首先放入一级缓存当前SqlSession可见如果配置了二级缓存且事务提交成功结果会加入二级缓存这里有个关键细节即使启用了二级缓存新查询的结果也会先存在于一级缓存直到会话关闭或事务提交后才会提升到二级缓存。这种设计保证了事务隔离性。3. 缓存顺序的典型问题与解决方案3.1 缓存穿透的防御策略当查询一个不存在的数据时按照正常流程会穿透缓存直达数据库。恶意攻击者可能利用这点发起大量非法ID查询。解决方案空值缓存在二级缓存中存储null结果并设置较短过期时间cache property namecacheNullValues valuetrue/ /cache布隆过滤器前置检查需自行实现public User getUserByIdWithBloomFilter(Long id) { if(!bloomFilter.mightContain(id)) { return null; } return userMapper.selectById(id); }3.2 缓存雪崩的预防措施当大量缓存同时失效会导致请求直接打到数据库。建议采用差异化过期时间cache evictionLRU flushInterval60000 size512 randomExpiration300000/热点数据永不过期后台刷新Scheduled(fixedRate 3600000) public void refreshHotData() { // 定时刷新热点数据 }3.3 分布式环境的一致性问题在集群部署时二级缓存需要特殊处理使用集中式缓存替代默认实现cache typeorg.mybatis.caches.redis.RedisCache/通过消息队列同步各节点缓存RabbitListener(queues cacheEvict) public void handleCacheEvict(String cacheKey) { redisCache.removeObject(cacheKey); }4. 缓存顺序的调试与验证4.1 日志监控方案在logback.xml中增加以下配置logger nameorg.apache.ibatis.cache levelDEBUG/ logger nameorg.apache.ibatis.executor.BaseExecutor levelTRACE/典型日志输出示例DEBUG - Cache Hit Ratio [com.example.UserMapper]: 0.5 TRACE - Retrieving cached result from com.example.UserMapper4.2 代码验证方法可以通过强制清除缓存来验证执行顺序Test public void testCacheOrder() { // 第一次查询数据库 User user1 mapper.getUser(1); // 第二次查询一级缓存 User user2 mapper.getUser(1); // 清除一级缓存 session.clearCache(); // 第三次查询二级缓存 User user3 mapper.getUser(1); // 清除二级缓存 mapper.flushCache(); // 第四次查询数据库 User user4 mapper.getUser(1); }5. 高级缓存策略定制5.1 自定义缓存实现实现Cache接口创建自定义缓存public class CustomCache implements Cache { private final String id; private final MapObject, Object cache new ConcurrentHashMap(); public CustomCache(String id) { this.id id; } Override public Object getObject(Object key) { // 自定义获取逻辑 } // 实现其他接口方法... }在Mapper中引用cache typecom.example.CustomCache/5.2 细粒度缓存控制通过CacheNamespaceRef注解实现方法级缓存配置CacheNamespaceRef(UserMapper.class) public interface UserMapper { Options(useCache false) Select(SELECT * FROM user WHERE id #{id}) User selectByIdNoCache(Long id); }5.3 混合缓存策略示例结合本地缓存与分布式缓存public class HybridCache implements Cache { private final Cache localCache new PerpetualCache(local); private final Cache distributedCache new RedisCache(distributed); Override public Object getObject(Object key) { Object value localCache.getObject(key); if(value null) { value distributedCache.getObject(key); if(value ! null) { localCache.putObject(key, value); } } return value; } }6. 性能优化实战建议批量查询优化对于批量ID查询先检查缓存再批量查库public ListUser batchGetUsers(ListLong ids) { ListUser cached ids.stream() .filter(localCache::containsKey) .map(localCache::get) .collect(Collectors.toList()); ListLong missingIds ids.stream() .filter(id - !localCache.containsKey(id)) .collect(Collectors.toList()); if(!missingIds.isEmpty()) { ListUser dbResults mapper.batchSelect(missingIds); dbResults.forEach(user - localCache.put(user.getId(), user)); cached.addAll(dbResults); } return cached; }缓存预热方案系统启动时加载热点数据PostConstruct public void preloadHotData() { ListLong hotUserIds getHotUserIdsFromMonitor(); hotUserIds.forEach(id - mapper.getUser(id)); }动态过期策略根据访问频率调整缓存时间public class DynamicExpirationCache implements Cache { Override public void putObject(Object key, Object value) { int ttl calculateTTL(key); // 根据key特征计算存活时间 // 实现带TTL的存储逻辑 } }在实际项目中我发现合理配置缓存顺序和策略可以使系统QPS提升3-5倍。特别是在读多写少的场景下正确的缓存设计能显著降低数据库压力。一个常见的误区是过度依赖二级缓存而忽视一级缓存的作用实际上在事务性操作中一级缓存往往能提供更好的性能表现。
返回列表