@Transactional 與線程鎖:為何聯(lián)用會(huì)失效?
很多小伙伴使用Spring事務(wù)時(shí),為了省事都喜歡使用@Transactional。但是@Transactional配合鎖,會(huì)導(dǎo)致一些預(yù)期之外的問(wèn)題!
在此舉例說(shuō)明。
數(shù)據(jù)準(zhǔn)備
我們將在該表中,實(shí)現(xiàn)level數(shù)據(jù)遞減的并發(fā)操作。
圖片
Controller中,簡(jiǎn)單模擬10個(gè)線程各自執(zhí)行10次:
圖片
@Transactional是如何導(dǎo)致鎖失效的
1、不加鎖
// service代碼
public void test() {
// 簡(jiǎn)單的select + update 模擬業(yè)務(wù)場(chǎng)景
Model model = mapper.choseOne("99");
// 實(shí)現(xiàn) level -- 操作
Model updater = new Model();
updater.setId("99");
updater.setLevel(model.getLevel() - 1);
mapper.updateOne(updater);
}
執(zhí)行結(jié)果:我們發(fā)現(xiàn),level只扣減了26,說(shuō)明存在并發(fā)問(wèn)題!
圖片
2、使用鎖
// service代碼
private Lock lock = new ReentrantLock();
public void test() {
try {
//加鎖
lock.lock();
// 簡(jiǎn)單的select + update 模擬業(yè)務(wù)場(chǎng)景
Model model = mapper.choseOne("99");
// 實(shí)現(xiàn) level -- 操作
Model updater = new Model();
updater.setId("99");
updater.setLevel(model.getLevel() - 1);
mapper.updateOne(updater);
} finally {
lock.unlock(); // 解鎖
}
}
執(zhí)行結(jié)果:我們發(fā)現(xiàn),使用鎖是可以控制并發(fā)問(wèn)題。
圖片
3、使用鎖+@Transactional
// service代碼
private Lock lock = new ReentrantLock();
@Transactional
public void test() {
try {
//加鎖
lock.lock();
// 簡(jiǎn)單的select + update 模擬業(yè)務(wù)場(chǎng)景
Model model = mapper.choseOne("99");
// 實(shí)現(xiàn) level -- 操作
Model updater = new Model();
updater.setId("99");
updater.setLevel(model.getLevel() - 1);
mapper.updateOne(updater);
} finally {
lock.unlock(); // 解鎖
}
}
執(zhí)行結(jié)果:我們發(fā)現(xiàn),level只扣減了86!用了@Transactional之后,鎖怎么就失效了呢!
圖片
4、問(wèn)題分析
我們都知道,@Transactional是通過(guò)使用AOP,在目標(biāo)方法執(zhí)行前后進(jìn)行事務(wù)的開(kāi)啟和提交。所以,Lock鎖住的代碼,其實(shí)并沒(méi)有包含住一整個(gè)事務(wù)!
通過(guò)下面的圖理解一下:
圖片
當(dāng)線程A將level設(shè)置為99時(shí),此時(shí)鎖已經(jīng)釋放了,但是事務(wù)還沒(méi)提交!!線程B此時(shí)可以獲取到鎖并進(jìn)行查詢,查詢出來(lái)的level還是線程A修改之前的100,所以出現(xiàn)了并發(fā)問(wèn)題。
解決方案
1、@Transactional單獨(dú)一個(gè)方法
private Lock lock = new ReentrantLock();
@Transactional
public void test1() {
// 簡(jiǎn)單的select + update 模擬業(yè)務(wù)場(chǎng)景
Model model = mapper.choseOne("99");
// 實(shí)現(xiàn) level -- 操作
Model updater = new Model();
updater.setId("99");
updater.setLevel(model.getLevel() - 1);
mapper.updateOne(updater);
}
@Autowired
@Lazy
private CommonService commonService;
public void test() {
try {
// 加鎖
lock.lock();
// 自己注入自己,以使用到其代理類
commonService.test1();
} finally {
lock.unlock(); // 解鎖
}
}
執(zhí)行結(jié)果:沒(méi)有并發(fā)問(wèn)題出現(xiàn)!
圖片
或者直接在controller層加鎖,也是一樣的道理。
2、使用編程式事務(wù)
// service代碼
private Lock lock = new ReentrantLock();
@Autowired
private PlatformTransactionManager transactionManager;
public void test() {
try {
//加鎖
lock.lock();
// 編程式事務(wù)
TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition());
// 簡(jiǎn)單的select + update 模擬業(yè)務(wù)場(chǎng)景
Model model = mapper.choseOne("99");
// 實(shí)現(xiàn) level -- 操作
Model updater = new Model();
updater.setId("99");
updater.setLevel(model.getLevel() - 1);
mapper.updateOne(updater);
// 在鎖中提交
transactionManager.commit(status);
} finally {
lock.unlock(); // 解鎖
}
}
執(zhí)行結(jié)果:我們發(fā)現(xiàn),將整個(gè)事務(wù)都鎖住,就沒(méi)問(wèn)題了!
圖片