事故描述
某商品的照片分两种类型: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) .
一级缓存
会不会是一级缓存的问题?
每一个sqlsession有自己的Executor,每一个executor有一个local cache.
当用户发起查询时,mybatis会根据当前statement生成一个key,去localcache中查询,如果缓存命中直接返回,未命中,访问db,写入localcache然后返回
信息量:
- 一级缓存默认开启
- 一级缓存是session级别的
- sqlsession执行dml (insert/update/delete)、close、clearCache等方法,会释放localcache中的对象(引用),一级缓存不可用
综上,删除再插入,然后重新获取时不会使用一级缓存。因此不应该是一级缓存的锅。
- debug sqlSession.selectList()
但事实上在第二次selectList的过程中,发现控制台没有打sqlLog 并且debug到sqlSession.selectList方法上,手动执行前调用sqlSession.clearCache(), 发现获取到了最新数据(不调用clearCache控制台不打sqlLog,取到脏数据),这也就是说缓存还是生效了,尽管对图片表delete和insert过,那么问题在哪?
- 难道是因为一个事务开启了多个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. 这也就是说,
如前文所说,sqlSessionA未监听到update/delete句柄,因此未执行移除缓存的操作,这使得第二次selectList的时候未执行sql语句,直接从缓存中取。
总结
- mytabis一级缓存在表被删除更新操作时缓存对象引用会被移除
- 一级缓存是会话级别的
- mybatis-plus selectList和updateBatchBy方法使用了两个不同的sqlSession.
因第3条的缘故,使得一级缓存没有在理想状态下被移除从而引发事故。
至于mybatis-plus为什么selectList和updateBatchBy方法使用了两个不同的sqlSession,感觉是在偷懒,后面可以再另出文章专门探讨。
参考文献