mybatis一级缓存与postgresql序列问题
新公司用的数据库是postgresql,修改之前的业务测试时候发现一个问题,批量文件处理其中的一个方法有问题,执行到此方法插入数据 违法数据库索引,不能插入,后来经过查找原因发现是 因为触发mybatis一级缓存造成的,记录下来。
MyBatis中的缓存
mybatis缓存分为一级缓存,二级缓存和自定义缓存。
SqlSession:代表和数据库的一次会话,向用户提供了操作数据库的方法
- MapperedStatement:代表要往数据库发送的要执行的指令,可以理解为sql的抽象表示
- Executor:用来和数据库交互的执行器,接收MapperedStatement作为参数
二:一级缓存
1.一级缓存的介绍:
mybatis一级缓存有两种:一种是SESSION级别的,针对同一个会话SqlSession中,执行多次条件完全相同的同一个sql,那么会共享这一缓存,默认是SESSION级别的缓存;一种是STATEMENT级别的,缓存只针对当前执行的这一statement有效。
整个流程是这样的:
- 针对某个查询的statement,生成唯一的key
- 在Local Cache 中根据key查询数据是否存在
- 如果存在,则命中,跳过数据库查询,继续往下走
- 如果没命中:
- 去数据库中查询,得到查询结果
- 将key和查询结果放到Local Cache中
- 将查询结果返回
- 判断是否是STATEMENT级别缓存,如果是,则清除缓存
接下来针对一级缓存的几种情况,来进行验证。
情况1:SESSION级别缓存,同一个Mapper代理对象执行条件相同的同一个查询sql1 2 3 4
| SqlSession sqlSession = getSqlSessionFactory().openSession(); GoodsDao goodsMapper = sqlSession.getMapper(GoodsDao.class); goodsMapper.selectGoodsById("1"); goodsMapper.selectGoodsById("1");
|
结果1 2 3 4 5 6
| Setting autocommit to false on JDBC Connection [com.mysql.jdbc.JDBC4Connection@3dd44d5e] ==> Preparing: select * from goods where id = ? ==> Parameters: 1(String) <== Columns: id, name, detail, remark <== Row: 1, title1, null, null <== Total: 1
|
总结:只向数据库进行了一次查询,第二次用了缓存
情况2:SESSION级别缓存,同一个Mapper代理对象执行条件不同的同一个查询sql1 2 3 4 5 6
| public void selectGoodsTest(){ SqlSession sqlSession = getSqlSessionFactory().openSession(); GoodsDao goodsMapper = sqlSession.getMapper(GoodsDao.class); goodsMapper.selectGoodsById("1"); goodsMapper.selectGoodsById("2"); }
|
结果1 2 3 4 5 6 7 8 9 10 11
| ==> Preparing: select * from goods where id = ? ==> Parameters: 1(String) <== Columns: id, name, detail, remark <== Row: 1, title1, null, null <== Total: 1
==> Preparing: select * from goods where id = ? ==> Parameters: 2(String) <== Columns: id, name, detail, remark <== Row: 2, title2, null, null <== Total: 1
|
总结:因为查询条件不同,所以是两个不同的statement,生成了两个不同key,缓存中是没有的
情况3:SESSION级别缓存,针对同一个Mapper接口生成两个代理对象,然后执行查询条件完全相同的同一条sql1 2 3 4 5 6 7
| public void selectGoodsTest(){ SqlSession sqlSession = getSqlSessionFactory().openSession(); GoodsDao goodsMapper = sqlSession.getMapper(GoodsDao.class); GoodsDao goodsMapper2 = sqlSession.getMapper(GoodsDao.class); goodsMapper.selectGoodsById("1"); goodsMapper2.selectGoodsById("1"); }
|
结果1 2 3 4 5
| ==> Preparing: select * from goods where id = ? ==> Parameters: 1(String) <== Columns: id, name, detail, remark <== Row: 1, title1, null, null <== Total: 1
|
总结:这种情况满足:同一个SqlSession会话,查询条件完全相同的同一条sql。所以,第二次查询是从缓存中查找的。
情况4:SESSION级别缓存,在同一次会话中,对数据库进行了修改操作,一级缓存是否是失效。1 2 3 4 5 6 7 8 9 10 11 12
| @Test public void selectGoodsTest(){ SqlSession sqlSession = getSqlSessionFactory().openSession(); GoodsDao goodsMapper = sqlSession.getMapper(GoodsDao.class); Goods goods = new Goods(); goods.setId("2"); goods.setName("篮球"); goodsMapper.selectGoodsById("1"); goodsMapper.updateGoodsById(goods); goodsMapper.selectGoodsById("1"); }
|
结果:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| ==> Preparing: select * from goods where id = ? ==> Parameters: 1(String) <== Columns: id, name, detail, remark <== Row: 1, title1, null, null <== Total: 1
==> Preparing: update goods set name = ? where id = ? ==> Parameters: 篮球(String), 2(String) <== Updates: 1
==> Preparing: select * from goods where id = ? ==> Parameters: 1(String) <== Columns: id, name, detail, remark <== Row: 1, title1, null, null <== Total: 1
|
总结:在同一个SqlSession会话中,如果对数据库进行了修改操作,那么该会话中的缓存都会被清除。但是,并不会影响其它会话中的缓存。
情况5:SESSION级别缓存,开启两个SqlSession,在SqlSession1中查询操作,在SqlSession2中执行修改操作,那么SqlSession1中的一级缓存是否仍然有效?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| public void selectGoodsTest(){ SqlSession sqlSession = getSqlSessionFactory().openSession(); GoodsDao goodsMapper = sqlSession.getMapper(GoodsDao.class); SqlSession sqlSession2 = getSqlSessionFactory().openSession(); GoodsDao goodsMapper2 = sqlSession2.getMapper(GoodsDao.class); Goods goods = new Goods(); goods.setId("1"); goods.setName("篮球"); Goods goods1 = goodsMapper.selectGoodsById("1"); System.out.println("name="+goods1.getName()); System.out.println("******************************************************"); goodsMapper2.updateGoodsById(goods); Goods goodsResult = goodsMapper.selectGoodsById("1"); System.out.println("******************************************************"); System.out.println("name="+goodsResult.getName()); }
|
结果:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| ==> Preparing: select * from goods where id = ? ==> Parameters: 1(String) <== Columns: id, name, detail, remark <== Row: 1, title1, null, null <== Total: 1 name=title1 ****************************************************** Opening JDBC Connection Created connection 644010817. Setting autocommit to false on JDBC Connection [com.mysql.jdbc.JDBC4Connection@2662d341] ==> Preparing: update goods set name = ? where id = ? ==> Parameters: 篮球(String), 1(String) <== Updates: 1 ****************************************************** name=title1
|
总结:在SqlSession2中对id=1的数据做了修改,但是在SqlSession1中的最后一次查询中,仍然是从一级缓存中取得数据,说明了一级缓存只在SqlSession内部共享,SqlSession对数据库的修改操作不影响其它SqlSession中的一级缓存。
情况6:SqlSession的缓存级别设置为STATEMENT,即在配置文件中添加如下代码:
1 2 3 4
| mybatis-plus.configuration.localCacheScope=STATEMENT <settings> <setting name="localCacheScope" value="STATEMENT"/> </settings>
|
执行代码:
1 2 3 4 5 6 7
| public void selectGoodsTest(){ SqlSession sqlSession = getSqlSessionFactory().openSession(); GoodsDao goodsMapper = sqlSession.getMapper(GoodsDao.class); goodsMapper.selectGoodsById("1"); System.out.println("****************************************"); goodsMapper.selectGoodsById("1"); }
|
1 2 3 4 5 6 7 8 9 10 11
| ==> Preparing: select * from goods where id = ? ==> Parameters: 1(String) <== Columns: id, name, detail, remark <== Row: 1, title1, null, null <== Total: 1 **************************************** ==> Preparing: select * from goods where id = ? ==> Parameters: 1(String) <== Columns: id, name, detail, remark <== Row: 1, title1, null, null <== Total: 1
|
总结:STATEMENT级别的缓存,只针对当前执行的这一statement有效
一级缓存是如何被存取的?
我们知道,当与数据库建立一次连接,就会创建一个SqlSession对象,默认是DefaultSqlSession这个实现,这个对象给用户提供了操作数据库的各种方法,与此同时,也会创建一个Executor执行器,缓存信息就是维护在Executor中,Executor有一个抽象子类BaseExecutor,这个类中有个属性PerpetualCache类,这个类就是真正用于维护一级缓存的地方。通过看源码,可以知道如何根据cacheKey,取出和存放缓存的。
在查询数据库前,先从缓存中查找,进入BaseExecutor类的query方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException { ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId()); if (closed) throw new ExecutorException("Executor was closed."); if (queryStack == 0 && ms.isFlushCacheRequired()) { clearLocalCache(); } List<E> list; try { queryStack++; list = resultHandler == null ? (List<E>) localCache.getObject(key) : null; if (list != null) { handleLocallyCachedOutputParameters(ms, key, parameter, boundSql); } else { list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql); } } finally { queryStack--; } if (queryStack == 0) { for (DeferredLoad deferredLoad : deferredLoads) { deferredLoad.load(); } deferredLoads.clear(); if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) { clearLocalCache(); } } return list; }
|
当从数据库中查询到数据后,需要把数据存放到缓存中的,然后再返回数据,这个就是存放缓存的过程,进入queryFromDatabase方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException { List<E> list; localCache.putObject(key, EXECUTION_PLACEHOLDER); try { list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql); } finally { localCache.removeObject(key); } localCache.putObject(key, list); if (ms.getStatementType() == StatementType.CALLABLE) { localOutputParameterCache.putObject(key, parameter); } return list; }
|
CacheKey是如何确定唯一的?
我们知道,如果两次查询完全相同,那么第二次查询就从缓存中取数据,换句话说,怎么判断两次查询是不是相同的?是否相同是根据CacheKey来判断的,那么看下CacheKey的生成过程,就知道影响CacheKey是否相同的元素有哪些了。
进入BaseExecutor类的createCacheKey方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| 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()); List<ParameterMapping> parameterMappings = boundSql.getParameterMappings(); TypeHandlerRegistry typeHandlerRegistry = ms.getConfiguration().getTypeHandlerRegistry(); for (int i = 0; i < parameterMappings.size(); i++) { ParameterMapping parameterMapping = parameterMappings.get(i); if (parameterMapping.getMode() != ParameterMode.OUT) { Object value; String propertyName = parameterMapping.getProperty(); if (boundSql.hasAdditionalParameter(propertyName)) { value = boundSql.getAdditionalParameter(propertyName); } else if (parameterObject == null) { value = null; } else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) { value = parameterObject; } else { MetaObject metaObject = configuration.newMetaObject(parameterObject); value = metaObject.getValue(propertyName); } cacheKey.update(value); } } return cacheKey; }
|
所以影响Cachekey是否相同的因素有:statementId,offset,limit,sql语句,参数
接下来进入cacheKey.update方法,看它如何处理以上这五个元素的:
1 2 3 4 5 6 7 8 9 10 11
| private void doUpdate(Object object) { int baseHashCode = object == null ? 1 : object.hashCode(); count++; checksum += baseHashCode; baseHashCode *= count; hashcode = multiplier * hashcode + baseHashCode; updateList.add(object); }
|
CahceKey的属性和构造方法:
1 2 3 4 5 6 7 8 9
| private int multiplier;private int hashcode;private long checksum; private int count; private List<Object> updateList; public CacheKey() { this.hashcode = DEFAULT_HASHCODE; this.multiplier = DEFAULT_MULTIPLYER; this.count = 0; this.updateList = new ArrayList<Object>(); }
|
CacheKey中最重要的一个方法来了,如何判断两个CacheKey是否相等?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| public 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 (thisObject == null) { if (thatObject != null) return false; } else { if (!thisObject.equals(thatObject)) return false; } } return true; }
|
一级缓存的生命周期是多长?
开始:mybatis建立一次数据库会话时,就会生成一系列对象:SqlSession—>Executor—>PerpetualCache,也就开启了对一级缓存的维护。
结束:
- 会话结束,会释放掉以上生成的一系列对象,缓存也就不可用了。
- 调用sqlSession.close方法,会释放掉PerpetualCache对象,一级缓存不可用
- 调用sqlSession.clearCache方法,会清空PerpetualCache对象中的缓存数据,该对象可用,一级缓存不可用
- 调用sqlSession的update,insert,delete方法,会清空PerpetualCache对象中的缓存数据,该对象可用,一级缓存不可用(遇到的是 插入别的表会清空缓存,更新本表会清空)
end
postgresql的序列是自增的数字,更新其他的表没有清空缓存,插入别的表数据 清空缓存,
配置打印查询sql到日志里
properties
1 2
| mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImp logging.level.${com.test.xsj}=debug
|