【MyBatis源码】CacheKey缓存键的原理分析
文章目录
- Mybatis缓存设计
- 缓存KEY的设计
- CacheKey类主体
- CacheKey组成
- CacheKey如何保证缓存key的唯一性
Mybatis缓存设计
MyBatis 每秒过滤众多数据库查询操作,这对 MyBatis 缓存键的设计提出了很高的要求。MyBatis缓存键要满足以下几点。
无碰撞:必须保证两条不同的查询请求生成的键不一致,这是最重要也是必须满足的要求。否则会引发查询操作命中错误的缓存,并返回错误的结果。
高效比较:每次缓存查询操作都可能会引发键之间的多次比较,因此该操作必须是高效的。
高效生成:每次缓存查询和写入操作前都需要生成缓存的键,因此该操作也必须是高效的。
在编程中,我们常使用数值、字符串等简单类型作为键,然而,这类键容易产生碰撞。为了防止碰撞的发生,需要将键的生成机制设计得非常复杂,这又降低了键的比较效率和生成效率。因此,准确度和效率之间往往是相互制约的。
为了解决以上问题,MyBatis设计了一个 CacheKey类作为缓存键。整个 CacheKey设计得并不复杂,但又非常精巧。
设计图解释:
【1】 Mybatis缓存的使用和我们一般使用缓存方式相同,使用一个内存缓存(map)作为本地容器。对于查询请求优先查询本地缓存,如果有直接返回,没有查询数据库,并将数据库查询结果写入到缓存中。
【2】 Mybatis使用CacheKey类作为缓存(map)的key,重写了其hashcode和equal方法。
【3】 CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql); Mybatis主要根据mapper信息,参数值,分页信息,SQL信息设计缓存key
缓存KEY的设计
CacheKey类主体
public class CacheKey implements Cloneable, Serializable {/*** 乘数,用来计算hashcode时使用*/private final int multiplier;/*** 哈希值,整个CacheKey的哈希值*/private int hashcode;/*** 求和校验码*/private long checksum;/*** 更新次数,整个CacheKey的更新次数*/private int count;/*** 更新历史*/private List<Object> updateList;public void update(Object object) {int baseHashCode = object == null ? 1 : ArrayUtil.hashCode(object);count++;checksum += baseHashCode;baseHashCode *= count;hashcode = multiplier * hashcode + baseHashCode;updateList.add(object);}public void updateAll(Object[] objects) {for (Object o : objects) {update(o);}}@Overridepublic boolean equals(Object object) {if (this == object) {return true;}if (!(object instanceof CacheKey)) {return false;}final CacheKey cacheKey = (CacheKey) object;if (hashcode != cacheKey.hashcode) {return false;}if (checksum != cacheKey.checksum) {return false;}if (count != cacheKey.count) {return false;}for (int i = 0; i < updateList.size(); i++) {Object thisObject = updateList.get(i);Object thatObject = cacheKey.updateList.get(i);if (!ArrayUtil.equals(thisObject, thatObject)) {return false;}}return true;}@Overridepublic int hashCode() {return hashcode;}
}
CacheKey组成
org.apache.ibatis.executor.BaseExecutor#createCacheKey
// 创建CacheKey对象CacheKey cacheKey = new CacheKey();// mapper-idcacheKey.update(ms.getId());// 分页参数cacheKey.update(rowBounds.getOffset());cacheKey.update(rowBounds.getLimit());// 执行的SQL(带有占位符的)cacheKey.update(boundSql.getSql());// 执行SQL参数value值cacheKey.update(value);// 环境配置idcacheKey.update(configuration.getEnvironment().getId());
CacheKey主要由5部分组成:
【1】 mapper接口对应的statementId
【2】 分页参数
【3】 执行SQL
【4】 传入参数的值
【5】 当前环境的ID
CacheKey如何保证缓存key的唯一性
CacheKey首先类设计了多个重要属性,这些属性为结合传入的参数信息进行组合计算以提高缓存key的唯一性,并能够以较高的性能进行比较计算。
其中hashcode的计算主要通过update方法进行计算
public void update(Object object) {int baseHashCode = object == null ? 1 : ArrayUtil.hashCode(object);count++;checksum += baseHashCode;baseHashCode *= count;hashcode = multiplier * hashcode + baseHashCode;updateList.add(object);}
在比较 CacheKey对象是否相等时,会先进行类型判断,然后进行 hashcode、checksum、count的比较,只要有一项不相同则表明两个对象不同。以上操作都比较简单,能在很短的时间内完成。如果上面的各项属性完全一致,则会详细比较两个CacheKey 对象的变动历史 updateList,这一步操作相对复杂,但是能保证绝对不会出现碰撞问题。
【CacheKey生成的结果示例】
2042432675:5771996351:user.selectById:0:2147483647:select * from t_user where id = ? and name = ?:1:null:development
MyBatis 还准备了一个 NullCacheKey,该类用来充当一个空键使用。在缓存查询中,如果发现某个 CacheKey信息不全,则会返回 NullCacheKey对象,类似于返回一个null值。但是 NullCacheKey毕竟是 CacheKey的子类,在接下来的处理中不会引发空指针异常。这种设计方式也非常值得我们借鉴。
MyBatis生成的 CacheKey 对象中包含了这次查询的所有信息,包括查询语句的 id、查询的翻页限制、数据总量、完整的 SQL语句,这些信息一致就保证了两次查询的一致。结合 CacheKey的 equals方法,我们知道只要通过 equals方法判断两个CacheKey对象相等,则两次查询操作的条件必定是完全一致的。