事故描述
某商品的照片分兩種類型:A商品外觀照片 和 B商品配件照片兩個相冊 ,它們保存在同一張picture表中。
在一個事務內按照片類型批量更新商品照片查乒,但操作人只有保存A類型照片的權限憔晒,因需要將該商品A照片清空从撼,然后插入新A類照片,然后取商品所有照片雄妥,僅發(fā)現B類照片,未發(fā)現新插入的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. 導致在同步第三方數據同臺時出現少數據的線上問題。
問題分析
事務的傳播行為
會不會是因為插入行為在另一事務內绽昏?
查閱代碼發(fā)現事務傳播行為為默認屬性:required 卷员,也就是不會創(chuàng)建新事務叙凡,而是加入調用者的事務狭姨。
況且即使發(fā)起新事務教硫,只要事務B提交,就能查到數據b(在沒用使用多線程的情況下,事務的隔離級別默認為readCommited) .
一級緩存
會不會是一級緩存的問題舀瓢?
每一個sqlsession有自己的Executor,每一個executor有一個local cache.
當用戶發(fā)起查詢時皇型,mybatis會根據當前statement生成一個key,去localcache中查詢汰聋,如果緩存命中直接返回,未命中喊积,訪問db,寫入localcache然后返回
信息量:
- 一級緩存默認開啟
- 一級緩存是session級別的
- sqlsession執(zhí)行dml (insert/update/delete)烹困、close、clearCache等方法乾吻,會釋放localcache中的對象(引用),一級緩存不可用
綜上髓梅,刪除再插入拟蜻,然后重新獲取時不會使用一級緩存。因此不應該是一級緩存的鍋枯饿。
- debug sqlSession.selectList()
但事實上在第二次selectList的過程中酝锅,發(fā)現控制臺沒有打sqlLog 并且debug到sqlSession.selectList方法上,手動執(zhí)行前調用sqlSession.clearCache(), 發(fā)現獲取到了最新數據(不調用clearCache控制臺不打sqlLog,取到臟數據)奢方,這也就是說緩存還是生效了搔扁,盡管對圖片表delete和insert過,那么問題在哪袱巨?
- 難道是因為一個事務開啟了多個sqlSession?
debug事務內部所有sql操作阁谆,查看sqlSession的內存地址
理論上在一個事務內,一個mapper對應開啟一個sqlSession愉老。
打映÷獭:update和selectList的sqlSession的內存地址
意外發(fā)現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未監(jiān)聽到update/delete句柄垫竞,因此未執(zhí)行移除緩存的操作澎粟,這使得第二次selectList的時候未執(zhí)行sql語句,直接從緩存中取欢瞪。
總結
- mytabis一級緩存在表被刪除更新操作時緩存對象引用會被移除
- 一級緩存是會話級別的
- mybatis-plus selectList和updateBatchBy方法使用了兩個不同的sqlSession.
因第3條的緣故活烙,使得一級緩存沒有在理想狀態(tài)下被移除從而引發(fā)事故。
至于mybatis-plus為什么selectList和updateBatchBy方法使用了兩個不同的sqlSession遣鼓,感覺是在偷懶啸盏,后面可以再另出文章專門探討。