美文网首页
Mybatis一级缓存和mybatisplus 踩坑记

Mybatis一级缓存和mybatisplus 踩坑记

作者: cengel | 来源:发表于2020-04-01 17:16 被阅读0次

    事故描述

    某商品的照片分两种类型:A商品外观照片 和 B商品配件照片两个相册 ,它们保存在同一张picture表中。

    在一个事务内按照片类型批量更新商品照片,但操作人只有保存A类型照片的权限,因需要将该商品A照片清空,然后插入新A类照片,然后取商品所有照片,仅发现B类照片,未发现新插入的A类照片。

    事务的隔离性

    start transaction;

    插入id=1数据

    INSERT INTO

    当前会话内 查询id=1的数据可见

    select * from ? where id='1';
    commit ;

    其他事务查询id=1的数据可见

    select * from car_picture where id='1';

    因此,在同一个事务内,删除数据a,再插入数据b,查询得到的应该是b,但就结果没有拿到b. 导致在同步第三方数据同台时出现少数据的线上问题。

    问题分析

    事务的传播行为

    会不会是因为插入行为在另一事务内?

    • 查阅代码发现事务传播行为为默认属性:required ,也就是不会创建新事务,而是加入调用者的事务。

    • 况且即使发起新事务,只要事务B提交,就能查到数据b(在没用使用多线程的情况下,事务的隔离级别默认为readCommited) .

    image.png

    一级缓存

    会不会是一级缓存的问题?

    image.png

    每一个sqlsession有自己的Executor,每一个executor有一个local cache.

    当用户发起查询时,mybatis会根据当前statement生成一个key,去localcache中查询,如果缓存命中直接返回,未命中,访问db,写入localcache然后返回

    信息量:

    • 一级缓存默认开启
    • 一级缓存是session级别的
    • sqlsession执行dml (insert/update/delete)、close、clearCache等方法,会释放localcache中的对象(引用),一级缓存不可用

    综上,删除再插入,然后重新获取时不会使用一级缓存。因此不应该是一级缓存的锅。

    1. debug sqlSession.selectList()
    image.png

    但事实上在第二次selectList的过程中,发现控制台没有打sqlLog 并且debug到sqlSession.selectList方法上,手动执行前调用sqlSession.clearCache(), 发现获取到了最新数据(不调用clearCache控制台不打sqlLog,取到脏数据),这也就是说缓存还是生效了,尽管对图片表delete和insert过,那么问题在哪?

    1. 难道是因为一个事务开启了多个sqlSession?

    debug事务内部所有sql操作,查看sqlSession的内存地址

    理论上在一个事务内,一个mapper对应开启一个sqlSession。

    打印:update和selectList的sqlSession的内存地址

    意外发现mybaits-plus在updateBatch的时候和update用的不是同一个sqlSession,这实在太坑了。

    /** com.baomidou.mybatisplus.extension.service.impl.ServiceImpl */ public class ServiceImpl<M extends BaseMapper<T>, T> implements IService<T> { @Transactional(rollbackFor = Exception.class) @Override public boolean updateBatchById(Collection<T> entityList, int batchSize) { Assert.notEmpty(entityList, "error: entityList must not be empty"); String sqlStatement = sqlStatement(SqlMethod.UPDATE_BY_ID); try (SqlSession batchSqlSession = sqlSessionBatch()) { int i = 0; for (T anEntityList : entityList) { MapperMethod.ParamMap<T> param = new MapperMethod.ParamMap<>(); param.put(Constants.ENTITY, anEntityList); batchSqlSession.update(sqlStatement, param); if (i >= 1 && i % batchSize == 0) { batchSqlSession.flushStatements(); } i++; } batchSqlSession.flushStatements(); } return true; } @Override public boolean updateById(T entity) { return retBool(baseMapper.updateById(entity)); } // 其他 }

    如上代码片断,mybatis-plus在updateBatch时的处理逻辑 使用Serivice内部打开的sqlSession ,而普通的updateById则走的mapper更新,mapper更新用的则是另一套session. 这也就是说,

    image.png

    如前文所说,sqlSessionA未监听到update/delete句柄,因此未执行移除缓存的操作,这使得第二次selectList的时候未执行sql语句,直接从缓存中取。

    总结

    1. mytabis一级缓存在表被删除更新操作时缓存对象引用会被移除
    2. 一级缓存是会话级别的
    3. mybatis-plus selectList和updateBatchBy方法使用了两个不同的sqlSession.

    因第3条的缘故,使得一级缓存没有在理想状态下被移除从而引发事故。

    至于mybatis-plus为什么selectList和updateBatchBy方法使用了两个不同的sqlSession,感觉是在偷懒,后面可以再另出文章专门探讨。

    image.png

    参考文献

    相关文章

      网友评论

          本文标题:Mybatis一级缓存和mybatisplus 踩坑记

          本文链接:https://www.haomeiwen.com/subject/rlvauhtx.html