mybatis 缓存
【参考文章】:深入理解MyBatis——缓存
【参考文章】:MyBatis 查询缓存
【参考文章】:MyBatis -- 整合Redis二级缓存
【参考文章】:MyBatis 随笔
1. 简介
MyBatis中的缓存分为两种:一级缓存和二级缓存。
一级缓存:sqlSession级别,当使用同一个sqlSession时,查询到的数据可能是一级缓存;
二级缓存:mapper级别,当使用同一个mapper时,查询到的数据可能是二级缓存。
a. 为每一个Mapper分配一个Cache缓存对象(使用<cache>节点配置)
b. 多个Mapper共用一个Cache缓存对象(使用<cache-ref>节点配置)
查询顺序:二级缓存 --> 一级缓存 --> 数据库
2. 缓存相关的默认设置
2.1 查询语句
select 标签的 useCache 属性:
默认为 true,其结果被二级缓存。
2.2 数据变更语句
insert,update,delete 的 flushCache属性:
默认为true,数据变更语句被调用,都会导致本地缓存和二级缓存都会被清空。
2.3 二级缓存配置
默认情况下是没有开启缓存的,除了局部的 session 缓存,可以增强变现而且处理循环 依赖也是必须的。要开启二级缓存,你需要在你的 SQL 映射文件中添加一行:
<cache eviction="FIFO" flushInterval="60000" size="1024" readOnly="true"/>
3. 一级缓存
MyBatis默认一级查询缓存是开启状态,且不能关闭。
SqlSession是将任务交给Executor来完成对数据库的各种操作,Executor执行查询前,会先去查询缓存,缓存命中失败再去数据库查询。
BaseExecutor 的 query();
@Override public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException { BoundSql boundSql = ms.getBoundSql(parameter); CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql); return query(ms, parameter, rowBounds, resultHandler, key, boundSql); }
BaseExecutor 的缓存维护:
使用 cache 接口的实现类 PerpetualCache
protected PerpetualCache localCache; protected PerpetualCache localOutputParameterCache;
PerpetualCache 的实现原理:
内部维护一个 HashMap
public class PerpetualCache implements Cache { private Map<Object, Object> cache = new HashMap<Object, Object>(); }
HashMap的键为CacheKey,
cacheKey的构建终于真相大白:根据 statementId 、 rowBounds.offset 、rowBounds.limit 和 SQL的参数 决定key中的hashcode。
因此,相同的操作就会有相同的hashcode,来保证一个cacheKey对应一个操作。
@Override public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) { if (closed) { throw new ExecutorException("Executor was closed."); } CacheKey cacheKey = new CacheKey(); cacheKey.update(ms.getId()); cacheKey.update(rowBounds.getOffset()); cacheKey.update(rowBounds.getLimit()); cacheKey.update(boundSql.getSql()); List<ParameterMapping> parameterMappings = boundSql.getParameterMappings(); }
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); }
4. 二级缓存
二级缓存开始时 实体类必须实现 Serializable 接口;
4.1 缓存策略实现原理
mybatis 二级缓存策略通过装饰者模式实现
二级缓存策略在 一级缓存策略 PerpetualCache 的基础上通过装饰进行功能增强。
4. 缓存测试
4.1 一级缓存是否存在,数据变更是否刷新缓存
private static String base = "com.skd.entity.UserMapper."; public static void main(String[] args) { testCacheExist(23,false); } private static void testCacheExist(int id, boolean flush) { System.out.println(); SqlSession session = ConnectUtil.getSession(); String namespace = base + "getUser"; User user = (User)session.selectOne(namespace, id); System.out.println(user); if(flush){ String namespace2 = base + "updateUser"; user.setName("flush"); session.update(namespace2, user); } user = (User)session.selectOne(namespace, id); System.out.println(user); }
flush 为 false 时:
第一次执行SQL语句查询;
第二次没有执行SQL语句,说明是在缓存中查询的
flush 为 true 时:
第一次执行SQL语句查询;
第二次执行SQL语句查询,
结论:
一级缓存存在;
数据变更会刷新缓存;
4.2 一级缓存是针对同一个sqlSession吗?
private static void differentSession(int id) { String namespace = base + "getUser"; SqlSession session = ConnectUtil.getSession(); User user1 = (User)session.selectOne(namespace, id); System.out.println(user1); session.close(); SqlSession session2 = ConnectUtil.getSession(); User user2 = (User)session2.selectOne(namespace, id); System.out.println(user2); session2.close(); }
测试结果:
结论:
不同的 sqlSession 查询时会执行SQL查询,不会存缓存中查询。
一级缓存针对的时同一个 sqlSession 。
4.3 二级缓存测试
开启二级缓:
全局配置:
<configuration> <settings> <setting name="cacheEnabled" value="true" /> <!-- 打印查询语句 --> <setting name="logImpl" value="STDOUT_LOGGING" /> </settings> </configuration>
SQL映射文件配置:
<mapper namespace="com.skd.entity.UserMapper"> <cache></cache> </mappe>
测试代码:
private static void differentSession(int id) { String namespace = base + "getUser"; SqlSession session = ConnectUtil.getSession(); User user1 = (User)session.selectOne(namespace, id); System.out.println(user1); session.close(); SqlSession session2 = ConnectUtil.getSession(); User user2 = (User)session2.selectOne(namespace, id); System.out.println(user2); }
注意:如果不把session关掉,查询结果是不会设置到缓存中的,一个缓存一次只允许一个会话操作。
因为考虑到如果两个会话同时操作一份缓存,会引起线程并发修改的问题,所以要关掉最先查询的那个session,查询结果才会被写到二级缓存中。