今天一同事線上遇到一個問題倒槐,程序不明原因的進(jìn)入了死循環(huán)焙贷。最后通過一步步分析代碼的線程運(yùn)行情況镜廉,定位出是事務(wù)產(chǎn)生的并發(fā)問題
我們看下代碼入口贺喝。
@Transactional
public Result<Boolean> execute(Map<String, String> currentRow, Map<String, String> contextInfo) {
...
synchronized (this) {
userVipCardDO.setCardNo(userVipCardService.getVipCardNo(contextInfo.get("storeId")));
}
}
此方法可能多線程運(yùn)行
第2個方法代碼
private int getVipCardNo(String storeId) {
//獲取數(shù)據(jù)庫當(dāng)前值
SerialNumberDO serialNumberDO = serialNumberDao.getByKey(Long.parseLong(storeId), SerialBusiType.HY.getCode());
if (serialNumberDO == null) {
...
return 1;
}
//查出數(shù)據(jù)庫當(dāng)前值
long counter = serialNumberDO.getCounter();
SerialNumberDO updateDO = new SerialNumberDO();
...
updateDO.setCounter(counter);
int i = serialNumberDao.increase(updateDO);
if(i == 0){
return getVipCardNo(storeId);
}else {
return counter;
}
}
相當(dāng)于一個樂觀鎖 compare And Set
此處呢,由于外面synchronized單線程運(yùn)行
這是一個遞歸調(diào)用
第3個dao層sql
<update id="increase" parameterType="SerialNumberDO">
update sh_serial_number
set counter = counter+1
where store_id = #{storeId}
and busi_type = #{busiType}
and counter = #{counter}
</update>
樂觀鎖sql
問題出在哪呢斗幼。
- 第一斷代碼澎蛛,當(dāng)時所有線程進(jìn)來讀到的值都是8
-
@Transactional
默認(rèn)事務(wù)隔離級別Repeatable read
可重復(fù)讀- 這就意味著第一步多個線程進(jìn)來后。讀到的值孟岛,其它事務(wù)進(jìn)行修改提交之后瓶竭。它還是讀的當(dāng) 時進(jìn)來的這個值8。
- 第一個線程讀到8 當(dāng)時數(shù)據(jù)庫確實是8 所以修改成 9 修改成功
- 第一個之后的線程呢 渠羞。
- 當(dāng)時進(jìn)來時開啟事務(wù)數(shù)據(jù)庫里是8
- synchronized等待
- 等第一個修改成功之后進(jìn)去select
- 由于事務(wù)隔離級別
Repeatable read
的原因斤贰,雖然數(shù)據(jù)庫里改成了9,它這里仍然讀到是8 - 然后
update .... and counter=#{counter}
比較的時候呢,卻是比較數(shù)據(jù)庫真實的值次询,所有8=9
一直不成功 - 一直重試遞歸調(diào)用 進(jìn)入了死循環(huán)
問題找到了荧恍,那么最簡單的解決方案就是@Transactional(isolation = Isolation.READ_COMMITTED)
將隔離級別降低一級。讀取已提交的
最后分下這個代碼
- 如果不加
synchronized
其實也會出問題。只是出的幾率小一點(diǎn)送巡。 - 為了其它service方法使用默認(rèn)隔離級別摹菠,調(diào)用
getVipCardNo
不出類似問題。我覺得應(yīng)該利用事務(wù)傳播行為骗爆,
將getVipCardNo
開啟一個新的事務(wù)次氨,再將隔離級別設(shè)置成讀取已提交的