前言
缓存是Mybatis里比较有意思的一个特性,一定程度上可以提高查询效率,降低数据库I/O压力。应对的场景是这样:在短时间内,频繁的反复执行相同的查询语句,如果任由其调用数据库,会对系统性能造成负面影响,所以缓存机制就出现了。分了两个层级,会话级和命名空间级别,这一章先介绍会话级缓存。
缓存类图
从上面这个图中,能看到PerpetualCache类是被独立列出来的,有三个原因:
- PerpetualCache是会话级【一级】缓存的默认实现,注意:一级缓存只会用到PerpetualCache;
- Cache的其他实现,比如soft、weak等,都是PerpetualCache的一个封装;
- Cache的其他实现,基本都用于二级缓存;
所以本章后续的内容,都是针对PerpetualCache展开的。
执行器
一级缓存在BaseExecutor中就有体现,下面列出的是BaseExecutor构造函数。很明显,一级缓存是默认开启的,和cacheEnabled是否设置为true没有关系。
protected BaseExecutor(Configuration configuration, Transaction transaction) {this.transaction = transaction;this.deferredLoads = new ConcurrentLinkedQueue<>();// 本地缓存,新建PerpetualCache对象this.localCache = new PerpetualCache("LocalCache");this.localOutputParameterCache = new PerpetualCache("LocalOutputParameterCache");this.closed = false;this.configuration = configuration;this.wrapper = this;}protected BaseExecutor(Configuration configuration, Transaction transaction) { this.transaction = transaction; this.deferredLoads = new ConcurrentLinkedQueue<>(); // 本地缓存,新建PerpetualCache对象 this.localCache = new PerpetualCache("LocalCache"); this.localOutputParameterCache = new PerpetualCache("LocalOutputParameterCache"); this.closed = false; this.configuration = configuration; this.wrapper = this; }protected BaseExecutor(Configuration configuration, Transaction transaction) { this.transaction = transaction; this.deferredLoads = new ConcurrentLinkedQueue<>(); // 本地缓存,新建PerpetualCache对象 this.localCache = new PerpetualCache("LocalCache"); this.localOutputParameterCache = new PerpetualCache("LocalOutputParameterCache"); this.closed = false; this.configuration = configuration; this.wrapper = this; }
执行过程
用户每一次与数据库交互,Mybatis都会创建一个SqlSession对象,在sqlSession中有本地缓存对象,在同一个会话中如果出现重复查询,都会根据查询条件去本地缓存中查询是否存在,如果有缓存,则从缓存中读取数据,直接返回给调用方。否则从数据库中读取数据,加载到缓存中,然后返回给用户。
大概分为五步:
- 根据查询条件,查询语句等条件,生成缓存key;
- 根据key去本地缓存中查找,如果找到则返回给用户;
- 如果没有缓存,则去数据库中查询;
- 从数据库中读取到数据之后,将数据放到缓存;
- 返回
生命周期
会话级本地缓存的生命周期基本上是和sqlSession绑定的,同生共死的关系,毕竟其是在创建sqlSession时,作为内部属性对象生成的。当然也有一些例外,比如在会话活动期间,穿插执行了更新或者删除语句,那么会删除对应缓存,避免返回脏数据,造成业务故障。
缓存删除的常见场景:
- 更新/删除DB数据;
- 在Mapper接口方法定义了更新策略:Options.FlushCachePolicy.TRUE;
- sqlSession关闭;
下面列出的是BaseExecutor里的update方法,在执行具体更新方法之前,就会强制清除缓存。
public int update(MappedStatement ms, Object parameter) throws SQLException {// 清除缓存clearLocalCache();// 执行继承类里的方法return doUpdate(ms, parameter);}public int update(MappedStatement ms, Object parameter) throws SQLException { // 清除缓存 clearLocalCache(); // 执行继承类里的方法 return doUpdate(ms, parameter); }public int update(MappedStatement ms, Object parameter) throws SQLException { // 清除缓存 clearLocalCache(); // 执行继承类里的方法 return doUpdate(ms, parameter); }
更新策略
下面是Mapper里的一个查询方法,定义了更新策略为true,即每次执行方法都需要更新缓存
@Select("select * from tb_image where md5 = #{md5}")@Options(flushCache = Options.FlushCachePolicy.TRUE, useCache = true)ImageInfo byMd5(@Param(value = "md5") String md5);@Select("select * from tb_image where md5 = #{md5}") @Options(flushCache = Options.FlushCachePolicy.TRUE, useCache = true) ImageInfo byMd5(@Param(value = "md5") String md5);@Select("select * from tb_image where md5 = #{md5}") @Options(flushCache = Options.FlushCachePolicy.TRUE, useCache = true) ImageInfo byMd5(@Param(value = "md5") String md5);
使用此配置的地方,在MapperAnnotationBuilder里构建方法MappedStatement对象的时候:
boolean isSelect = sqlCommandType == SqlCommandType.SELECT;boolean flushCache = !isSelect;boolean useCache = isSelect;if (options != null) {if (FlushCachePolicy.TRUE.equals(options.flushCache())) {flushCache = true;} else if (FlushCachePolicy.FALSE.equals(options.flushCache())) {flushCache = false;}useCache = options.useCache();}boolean isSelect = sqlCommandType == SqlCommandType.SELECT; boolean flushCache = !isSelect; boolean useCache = isSelect; if (options != null) { if (FlushCachePolicy.TRUE.equals(options.flushCache())) { flushCache = true; } else if (FlushCachePolicy.FALSE.equals(options.flushCache())) { flushCache = false; } useCache = options.useCache(); }boolean isSelect = sqlCommandType == SqlCommandType.SELECT; boolean flushCache = !isSelect; boolean useCache = isSelect; if (options != null) { if (FlushCachePolicy.TRUE.equals(options.flushCache())) { flushCache = true; } else if (FlushCachePolicy.FALSE.equals(options.flushCache())) { flushCache = false; } useCache = options.useCache(); }
逻辑如下:
- 默认情况下,非查询方法需要更新缓存,查询方法会用到缓存;
- 如果配置了@Options注解,则根据更新策略,设置flushCache的值;
- useCache也会根据@Options的配置进行重新赋值;
缓存结构
对于PerpetualCache来说,结构是比较简单的,缓存数据都保存在一个Map里面。id是在创建的时候指定的,比如上文提到的LocalCache、LocalOutputParameterCache。
public class PerpetualCache implements Cache {// 缓存名称,比如上文提到的LocalCache、LocalOutputParameterCacheprivate final String id;// 缓存结构private final Map<Object, Object> cache = new HashMap<>();// 构造方法public PerpetualCache(String id) {this.id = id;}...}public class PerpetualCache implements Cache { // 缓存名称,比如上文提到的LocalCache、LocalOutputParameterCache private final String id; // 缓存结构 private final Map<Object, Object> cache = new HashMap<>(); // 构造方法 public PerpetualCache(String id) { this.id = id; } ... }public class PerpetualCache implements Cache { // 缓存名称,比如上文提到的LocalCache、LocalOutputParameterCache private final String id; // 缓存结构 private final Map<Object, Object> cache = new HashMap<>(); // 构造方法 public PerpetualCache(String id) { this.id = id; } ... }
Key的生成
public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {CacheKey cacheKey = new CacheKey();// ms的id,一般是Mapper的类路径+方法名称cacheKey.update(ms.getId());// 分页参数,表坐标cacheKey.update(rowBounds.getOffset());// 分页参数,数据条数限制cacheKey.update(rowBounds.getLimit());// 在Mapper方法里定义的查询语句cacheKey.update(boundSql.getSql());List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();// 读取参数for (ParameterMapping parameterMapping : parameterMappings) {// 提取输入参数if (parameterMapping.getMode() != ParameterMode.OUT) {String propertyName = parameterMapping.getProperty();// 读取输入参数的值cacheKey.update(getValue(propertyName));}}if (configuration.getEnvironment() != null) {// 环境id,内容为:SqlSessionFactoryBean.class.getSimpleName()cacheKey.update(configuration.getEnvironment().getId());}return cacheKey;}public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) { CacheKey cacheKey = new CacheKey(); // ms的id,一般是Mapper的类路径+方法名称 cacheKey.update(ms.getId()); // 分页参数,表坐标 cacheKey.update(rowBounds.getOffset()); // 分页参数,数据条数限制 cacheKey.update(rowBounds.getLimit()); // 在Mapper方法里定义的查询语句 cacheKey.update(boundSql.getSql()); List<ParameterMapping> parameterMappings = boundSql.getParameterMappings(); // 读取参数 for (ParameterMapping parameterMapping : parameterMappings) { // 提取输入参数 if (parameterMapping.getMode() != ParameterMode.OUT) { String propertyName = parameterMapping.getProperty(); // 读取输入参数的值 cacheKey.update(getValue(propertyName)); } } if (configuration.getEnvironment() != null) { // 环境id,内容为:SqlSessionFactoryBean.class.getSimpleName() cacheKey.update(configuration.getEnvironment().getId()); } return cacheKey; }public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) { CacheKey cacheKey = new CacheKey(); // ms的id,一般是Mapper的类路径+方法名称 cacheKey.update(ms.getId()); // 分页参数,表坐标 cacheKey.update(rowBounds.getOffset()); // 分页参数,数据条数限制 cacheKey.update(rowBounds.getLimit()); // 在Mapper方法里定义的查询语句 cacheKey.update(boundSql.getSql()); List<ParameterMapping> parameterMappings = boundSql.getParameterMappings(); // 读取参数 for (ParameterMapping parameterMapping : parameterMappings) { // 提取输入参数 if (parameterMapping.getMode() != ParameterMode.OUT) { String propertyName = parameterMapping.getProperty(); // 读取输入参数的值 cacheKey.update(getValue(propertyName)); } } if (configuration.getEnvironment() != null) { // 环境id,内容为:SqlSessionFactoryBean.class.getSimpleName() cacheKey.update(configuration.getEnvironment().getId()); } return cacheKey; }
主要由五个元素构成:
1.MappedStatement 的id,一般是Mapper的类路径+方法名称;
2.分页参数,表坐标offset;
3.分页参数,数据条数限制limit;
4.在Mapper方法里定义的查询语句;
5.输入参数的值;
6.环境id,内容为:SqlSessionFactoryBean.class.getSimpleName()
上文中提到的查询方法byMd5,生成的key为:271108400:3910348202:com.essay.mybatis.mapper.ImageInfoMapper.byMd5:0:2147483647:select * from tb_image where md5 = ?:6e705a7733ac5gbwopmp02:SqlSessionFactoryBean
思考
如何才能用到一级缓存?
用到一级缓存的前提条件是在同一个sqlSession的生命周期之内,多次重复查询。如果用spring注入的mapper,在同一个事务之内进行多次查询,是不能用到一级缓存的,因为每个查询方法,都会创建一个executor来执行,执行完成之后关闭sqlSession,而一级缓存是绑定的executor的。
手动获取sqlSession
下面这个测试方法,用sqlSession手动获取mapper,然后在其未关闭之前进行两次查询,就会用到一级缓存了。
@Testpublic void sqlSession() {SqlSession sqlSession = sqlSessionFactory.openSession();ImageInfoMapper mapper = sqlSession.getMapper(ImageInfoMapper.class);ImageInfo info = mapper.byMd5("6e705a7733ac5gbwopmp02");log.info(">>>>>>>>>>>>>>>>>>>=>,{}", info);info = mapper.byMd5("6e705a7733ac5gbwopmp02");log.info("=>,{}", info);}@Test public void sqlSession() { SqlSession sqlSession = sqlSessionFactory.openSession(); ImageInfoMapper mapper = sqlSession.getMapper(ImageInfoMapper.class); ImageInfo info = mapper.byMd5("6e705a7733ac5gbwopmp02"); log.info(">>>>>>>>>>>>>>>>>>>=>,{}", info); info = mapper.byMd5("6e705a7733ac5gbwopmp02"); log.info("=>,{}", info); }@Test public void sqlSession() { SqlSession sqlSession = sqlSessionFactory.openSession(); ImageInfoMapper mapper = sqlSession.getMapper(ImageInfoMapper.class); ImageInfo info = mapper.byMd5("6e705a7733ac5gbwopmp02"); log.info(">>>>>>>>>>>>>>>>>>>=>,{}", info); info = mapper.byMd5("6e705a7733ac5gbwopmp02"); log.info("=>,{}", info); }
事务内
在同一个事务里面,sqlSession也是复用的,所以也会用到一级缓存。如果把@Transactional注解拿掉,则在方法执行期间,会创建两个sqlSession,这种情况下,两个sqlSession的一级缓存是隔离开的,需要走两次数据库查询。
@Transactional(rollbackFor = Exception.class)public void selectDuplicateTransaction(String md5){ImageInfo info = imageInfoMapper.byMd5(md5);log.info(">>>>>>>>>>>>>>>>>>>=>,{}", info);ImageInfo info1 = imageInfoMapper.byMd5(md5);log.info(">>>>>>>>>>>>>>>>>>>=>,{}", info1);}@Transactional(rollbackFor = Exception.class) public void selectDuplicateTransaction(String md5){ ImageInfo info = imageInfoMapper.byMd5(md5); log.info(">>>>>>>>>>>>>>>>>>>=>,{}", info); ImageInfo info1 = imageInfoMapper.byMd5(md5); log.info(">>>>>>>>>>>>>>>>>>>=>,{}", info1); }@Transactional(rollbackFor = Exception.class) public void selectDuplicateTransaction(String md5){ ImageInfo info = imageInfoMapper.byMd5(md5); log.info(">>>>>>>>>>>>>>>>>>>=>,{}", info); ImageInfo info1 = imageInfoMapper.byMd5(md5); log.info(">>>>>>>>>>>>>>>>>>>=>,{}", info1); }
内存溢出?
在一级缓存里,是没有自动过期的概念的,cache也没有容量限制,如果一个sqlSession生存时间足够长,是有可能导致内存溢出的。这就需要开发者注意了,一般情况下,使用spring注入的mapper对象进行查询,是不会有这样的疑虑的,因为sqlSession的生存周期非常短暂,也很少重复利用。