一、初识MyBatis缓存
在正式开始之前,让我们先来了解MyBatis的整体架构。MyBatis采用分层设计,而缓存模块作为基础支撑层的核心组件,承担着提升查询性能的重要使命。
缓存的价值何在?
想象这样一个场景:你的系统每秒需要查询1000次用户信息。
无缓存时:1000次数据库查询/秒
有缓存时:1次数据库查询 + 999次内存读取/秒
性能提升:近1000倍!
MyBatis的两级防线
MyBatis提供了两级缓存机制,就像双重保险:
二、缓存架构
MyBatis的缓存设计堪称教科书级别的装饰器模式应用。
Cache接口:缓存的灵魂
所有缓存实现都遵循这个核心接口:
publicinterfaceCache{StringgetId();// 缓存标识voidputObject(Objectkey,Objectvalue);// 存入缓存ObjectgetObject(Objectkey);// 获取缓存ObjectremoveObject(Objectkey);// 移除缓存voidclear();// 清空缓存intgetSize();// 缓存大小ReadWriteLockgetReadWriteLock();// 读写锁}装饰器大家族
MyBatis通过装饰器模式为缓存"穿衣服",每个装饰器赋予缓存一种新能力:
PerpetualCache:万丈高楼平地起
这是最基础的缓存实现,简单而高效:
publicclassPerpetualCacheimplementsCache{privatefinalStringid;privatefinalMap<Object,Object>cache=newHashMap<>();@OverridepublicvoidputObject(Objectkey,Objectvalue){cache.put(key,value);}@OverridepublicObjectgetObject(Objectkey){returncache.get(key);}// ... 其他方法实现}三、一级缓存:会话的专属记忆
一级缓存是SqlSession级别的缓存,默认开启,无需配置。
工作原理解析
一级缓存的核心逻辑在BaseExecutor中实现:
public<E>List<E>query(MappedStatementms,Objectparameter,RowBoundsrowBounds,ResultHandlerresultHandler){//创建缓存键CacheKeykey=createCacheKey(ms,parameter,rowBounds,boundSql);//先查缓存List<E>list=(List<E>)localCache.getObject(key);if(list!=null){returnlist;// 缓存命中!}//缓存未命中,查询数据库list=queryFromDatabase(ms,parameter,rowBounds,resultHandler,key,boundSql);//结果写入缓存localCache.putObject(key,list);returnlist;}CacheKey:缓存的身份证
CacheKey由多个元素组成,确保唯一性:
CacheKey的组成:
├──MappedStatement的ID ├── 查询参数 ├── 分页信息(RowBounds) ├── SQL语句 └── 环境ID判断缓存命中时,这些元素必须完全一致:
@Overridepublicbooleanequals(Objectobject){finalCacheKeycacheKey=(CacheKey)object;returnhashcode==cacheKey.hashcode// 哈希码相同&&checksum==cacheKey.checksum// 校验和相同&&count==cacheKey.count// 元素数量相同&&updateList.equals(cacheKey.updateList);// 元素列表相同}五种失效场景
一级缓存在以下情况会被清空:
执行增删改操作
手动清空
提交事务
回滚事务
关闭会话
@Overridepublicintupdate(MappedStatementms,Objectparameter){clearLocalCache();//清空缓存returndoUpdate(ms,parameter);}session.clearCache();//主动清理session.commit();//提交时清空session.rollback();//回滚时清空session.close();//会话结束,缓存消失实战演示
SqlSessionsession=sqlSessionFactory.openSession();UserMappermapper=session.getMapper(UserMapper.class);// 第一次查询 - 访问数据库Useruser1=mapper.selectById(1L);System.out.println("首次查询: "+user1);// 第二次查询 - 从缓存获取Useruser2=mapper.selectById(1L);System.out.println("再次查询: "+user2);// 验证是否为同一对象System.out.println("同一对象? "+(user1==user2));//truesession.close();四、二级缓存:跨会话的共享空间
二级缓存是Mapper级别的缓存,可以在不同SqlSession之间共享数据。
开启二级缓存
在Mapper XML中添加配置:
<mapper namespace="com.example.mapper.UserMapper"><!--开启二级缓存--><cache eviction="LRU"flushInterval="60000"size="1024"readOnly="true"/><select id="selectById"resultType="User">SELECT*FROM t_userWHEREid=#{id}</select></mapper>配置参数详解:
四大淘汰策略
LRU(推荐)
FIFO
SOFT
WEAK
<cache eviction="LRU"/>最近最少使用,淘汰最久未访问的数据<cache eviction="FIFO"/>先进先出,按写入顺序淘汰<cache eviction="SOFT"/>软引用,内存不足时才回收<cache eviction="WEAK"/>弱引用,GC时即可回收
CachingExecutor:二级缓存的指挥官
publicclassCachingExecutorimplementsExecutor{privatefinalTransactionalCacheManagertcm=newTransactionalCacheManager();@Overridepublic<E>List<E>query(...){Cachecache=ms.getCache();if(cache!=null){//尝试从二级缓存获取List<E>list=(List<E>)tcm.getObject(cache,key);if(list!=null){returnlist;//命中!}}//委托给BaseExecutor查询(会走一级缓存)List<E>list=delegate.query(...);//结果写入二级缓存if(cache!=null){tcm.putObject(cache,key,list);}returnlist;}}TransactionalCache:事务缓存管家
事务缓存确保只有提交后的数据才会进入二级缓存:
publicclassTransactionalCacheimplementsCache{privatefinalMap<Object,Object>entriesToAddOnCommit;@OverridepublicvoidputObject(Objectkey,Objectvalue){//暂存,不立即写入entriesToAddOnCommit.put(key,value);}publicvoidcommit(){// 提交时才真正写入缓存for(Map.Entry<Object,Object>entry:entriesToAddOnCommit.entrySet()){delegate.putObject(entry.getKey(),entry.getValue());}}publicvoidrollback(){// 回滚时丢弃暂存数据entriesToAddOnCommit.clear();}}跨会话共享示例
//会话1SqlSessionsession1=sqlSessionFactory.openSession();UserMappermapper1=session1.getMapper(UserMapper.class);Useruser1=mapper1.selectById(1L);System.out.println("会话1查询: "+user1);session1.commit();//提交,写入二级缓存session1.close();//会话2SqlSessionsession2=sqlSessionFactory.openSession();UserMappermapper2=session2.getMapper(UserMapper.class);Useruser2=mapper2.selectById(1L);//从二级缓存获取System.out.println("会话2查询: "+user2);//对比结果System.out.println("同一对象? "+(user1==user2));//falseSystem.out.println("值相等? "+user1.equals(user2));//truesession2.close();五、缓存命中流程
全景理解缓存的完整查询流程,是优化性能的关键。
完整查询链路
查询请求 ↓ 检查二级缓存 ├─ 命中 → 直接返回 └─ 未命中 ↓ 检查一级缓存 ├─ 命中 → 直接返回 └─ 未命中 ↓ 查询数据库 ↓ 写入一级缓存 ↓ 写入二级缓存(提交后) ↓ 返回结果源码实现
public<E>List<E>query(MappedStatementms,Objectparameter,...){BoundSqlboundSql=ms.getBoundSql(parameter);CacheKeykey=createCacheKey(ms,parameter,rowBounds,boundSql);// 步骤1:查二级缓存Cachecache=ms.getCache();if(cache!=null&&ms.isUseCache()){List<E>list=(List<E>)tcm.getObject(cache,key);if(list!=null){returnlist;// 二级缓存命中}}// 步骤2:查一级缓存List<E>list=(List<E>)localCache.getObject(key);if(list!=null){returnlist;// 一级缓存命中}//步骤3:查数据库list=queryFromDatabase(ms,parameter,...);//步骤4:写入缓存localCache.putObject(key,list);if(cache!=null){tcm.putObject(cache,key,list);}returnlist;}六、装饰器模式的运用
MyBatis缓存的装饰器设计堪称经典,让我们看看如何"给缓存穿衣服"。
LruCache:智能淘汰
publicclassLruCacheimplementsCache{privatefinalCachedelegate;privateMap<Object,Object>keyMap;// LinkedHashMap实现LRUprivateObjecteldestKey;@OverridepublicObjectgetObject(Objectkey){keyMap.get(key);//访问即刷新顺序returndelegate.getObject(key);}@OverridepublicvoidputObject(Objectkey,Objectvalue){delegate.putObject(key,value);cycleKeyList(key);//淘汰最久未用的}}ScheduledCache:定时清理
publicclassScheduledCacheimplementsCache{privatelongclearInterval=3600000;// 1小时privatelonglastClear;@OverridepublicObjectgetObject(Objectkey){if(System.currentTimeMillis()-lastClear>clearInterval){clear();//时间到,清空缓存returnnull;}returndelegate.getObject(key);}}SerializedCache:深拷贝保护
publicclassSerializedCacheimplementsCache{@OverridepublicvoidputObject(Objectkey,Objectvalue){// 序列化存储delegate.putObject(key,serialize((Serializable)value));}@OverridepublicObjectgetObject(Objectkey){// 反序列化返回,每次都是新对象Objectobject=delegate.getObject(key);returnobject==null?null:deserialize((byte[])object);}}SynchronizedCache:线程安全卫士
publicclassSynchronizedCacheimplementsCache{@OverridepublicsynchronizedvoidputObject(Objectkey,Objectvalue){delegate.putObject(key,value);}@OverridepublicsynchronizedObjectgetObject(Objectkey){returndelegate.getObject(key);}}装饰器链的构建
privateCachesetStandardDecorators(Cachecache){//按顺序穿衣服if(blocking){cache=newBlockingCache(cache);}if(readWrite){cache=newSerializedCache(cache);}if(scheduled){cache=newScheduledCache(cache);}if(logging){cache=newLoggingCache(cache);}if(sync){cache=newSynchronizedCache(cache);}//LRU通常是最外层cache=newLruCache(cache);returncache;}七、最佳实践
推荐做法
1.一级缓存- 保持默认开启,适合单会话重复查询
2.二级缓存- 仅在读多写少的场景开启
3.LRU策略- 大多数场景的最佳选择
4.合理设置容量- 根据业务量评估,避免内存溢出
5.只读缓存- 不可变对象使用
readOnly=“true”
避免做法
1.在频繁更新的表上开启二级缓存
2.缓存大对象或包含敏感信息的对象
3.忽略缓存带来的数据一致性问题
4.不监控缓存命中率就盲目使用
性能优化技巧
热点数据优先
合理设置TTL
只读缓存加速
监控命中率
<!--核心业务表单独配置--><cache size="2048"eviction="LRU"/><!--根据数据更新频率设置--><cache flushInterval="300000"/><!--5分钟--><!--不可变数据使用只读缓存--><cache readOnly="true"/><!--开启日志记录--><cache><property name="logging"value="true"/></cache>常见问题速查
问题1:二级缓存不生效
<!--解决方案--><!--1.检查全局配置--><settings><setting name="cacheEnabled"value="true"/></settings><!--2.检查Mapper配置--><cache/><!--3.确保实体类实现Serializable-->publicclassUserimplementsSerializable{privatestaticfinallongserialVersionUID=1L;}问题2:数据不一致
<!--解决方案:及时刷新缓存--><update id="updateUser"flushCache="true">UPDATE t_userSETname=#{name}WHEREid=#{id}</update><!--或设置自动刷新--><cache flushInterval="60000"/>问题3:内存溢出
<!--解决方案1:限制容量--><cache size="512"/><!--解决方案2:使用软引用--><cache eviction="SOFT"/><!--解决方案3:定时清理--><cache flushInterval="3600000"/>实战案例
场景:电商系统商品查询优化
<mapper namespace="com.shop.mapper.ProductMapper"><!--商品信息变化不频繁,适合二级缓存 使用LRU淘汰策略 设置1小时自动刷新 容量2048,覆盖热门商品--><cache eviction="LRU"flushInterval="3600000"size="2048"readOnly="false"/><select id="selectById"resultType="Product">SELECT*FROM t_productWHEREid=#{id}</select><!--更新操作强制刷新缓存--><update id="updateProduct"flushCache="true">UPDATE t_productSETprice=#{price}WHEREid=#{id}</update></mapper>八、总结
一级缓存
SqlSession级别
默认开启
增删改自动清空
适合单会话重复查询
二级缓存
Mapper级别
需手动配置
跨SqlSession共享
适合读多写少场景
装饰器模式
灵活组合功能
支持多种淘汰策略
可扩展自定义实现
CacheKey机制
多元素组成
确保唯一性
精确命中判断