事故描述
某商品的照片分兩種類型: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,感覺是在偷懶,后面可以再另出文章專門探討。