MyBatis 的缓存机制是其性能优化的关键模块之一,通过减少对数据库的直接访问来大幅提升查询效率。在深入二级缓存之前,理解整个缓存体系的基础(一级缓存)以及支撑它的核心源码至关重要。
🔍 缓存体系概览
MyBatis 提供两级缓存,均基于Cache接口实现。
- 一级缓存 (Local Cache):默认开启且无法关闭,作用域为
SqlSession级别。其本质是一个HashMap,执行相同的查询时,会先从该SqlSession的本地缓存中查找。 - 二级缓存 (Global Cache):作用域为
Mapper的namespace级别,可跨SqlSession共享,需要手动配置开启。
🏗️ 核心源码剖析:Cache 接口与装饰器模式
org.apache.ibatis.cache.Cache是所有缓存实现的核心接口,定义了几个基本方法:
String getId():获取缓存对象的唯一标识符。void putObject(Object key, Object value):将数据放入缓存。Object getObject(Object key):从缓存中获取数据。Object removeObject(Object key):从缓存中移除数据。void clear():清空整个缓存。
MyBatis 的缓存模块大量运用了装饰器模式,通过层层包装为基础缓存PerpetualCache增加多样化的功能。
PerpetualCache(核心组件):最基础的缓存实现,内部直接使用HashMap存储键值对,是缓存模块的"核心"。LruCache(装饰器):实现最近最少使用 (LRU)策略的缓存。它内部维护一个LinkedHashMap,并覆写removeEldestEntry方法,当缓存大小超过设定值 (setSize) 时,自动移除链表头部的元素(即最久未使用的条目)。SynchronizedCache(装饰器):一个线程安全的缓存装饰器,其所有方法都使用synchronized关键字修饰,确保多线程环境下的数据一致性。BlockingCache(装饰器):提供阻塞功能的缓存,用于防止缓存击穿。当一个线程在查询某个 Key 时,如果缓存未命中,该线程会获得这个 Key 的锁,并去数据库加载数据,其他线程必须等待,从而避免大量并发请求同时落到数据库。
此外,还有FifoCache(先进先出策略)和SoftCache/WeakCache(基于引用类型)等多种装饰器,可以根据需求灵活组合。
🔬 一级缓存源码分析 (以 SqlSession 为基础)
一级缓存的实现逻辑主要集中在org.apache.ibatis.executor.BaseExecutor类中。
- 缓存存储:
BaseExecutor维护了两个PerpetualCache实例:localCache:用于缓存查询结果。localOutputParameterCache:用于缓存存储过程的输出参数。
- 缓存键: 一级缓存使用
CacheKey对象作为键,以确保查询的精确匹配。CacheKey的构建涉及多个要素:- MappedStatement 的 ID(
namespace + id)。 - 查询结果的偏移量(
RowBounds.offset)和限制条数(RowBounds.limit)。 - 具体的 SQL 语句(
BoundSql.getSql())。 - SQL 语句中所有参数的实际值。
- 环境 ID(
configuration.getEnvironment().getId())。
- MappedStatement 的 ID(
- 执行流程 (BaseExecutor.query):
// 代码逻辑简化自 org.apache.ibatis.executor.BaseExecutorpublic<E>List<E>query(MappedStatementms,Objectparameter,RowBoundsrowBounds,ResultHandlerresultHandler)throwsSQLException{// 1. 根据查询条件生成CacheKeyCacheKeykey=createCacheKey(ms,parameter,rowBounds,boundSql);// 2. 检查是否配置了在查询前清空一级缓存 (flushCacheRequested)if(ms.isFlushCacheRequired()){clearLocalCache();// 清空localCache}List<E>list=null;try{// 3. 尝试从一级缓存(localCache)中获取结果list=queryFromDatabase(ms,parameter,rowBounds,resultHandler,key,boundSql);}catch(SQLExceptione){throwe;}// ... 其他处理returnlist;}private<E>List<E>queryFromDatabase(...){List<E>list=(List<E>)localCache.getObject(key);if(list!=null){returnlist;// 缓存命中,直接返回}// 缓存未命中,执行数据库查询list=doQuery(ms,parameter,rowBounds,resultHandler,boundSql