Mybatis一级缓存和mybatisplus 踩坑记

时间:2024-03-23 16:52:18

事故描述

某商品的照片分两种类型: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) .

Mybatis一级缓存和mybatisplus 踩坑记

 

 

一级缓存 

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

 

Mybatis一级缓存和mybatisplus 踩坑记

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

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

信息量:

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

 

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

  1. debug sqlSession.selectList()

Mybatis一级缓存和mybatisplus 踩坑记

但事实上在第二次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. 这也就是说,

Mybatis一级缓存和mybatisplus 踩坑记

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

 

总结

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

 

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

 

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

Mybatis一级缓存和mybatisplus 踩坑记

参考文献