1、前言
平時(shí)開發(fā)我們經(jīng)常使用 Spring 事務(wù)期虾,而 Spring 默認(rèn)使用 mysql 的事務(wù)驴一。mysql 事務(wù)默認(rèn)的隔離級(jí)別為:可重復(fù)讀。我們就以可重復(fù)讀為例子看一下代碼(事務(wù)的隔離性是用鎖做的甚牲,如果不是操作同一行數(shù)據(jù)就不會(huì)鎖)义郑。
@Transactional
public void testTransaction(String name) {
// select * from user where 'delete' = 0 order by id asc limit 1;
User user = userMapper.selectUser();
if(user != null){
Boolean oldStatus = user.getStatus();
user.setName(name);
user.setStatus(Boolean.TRUE);
// 樂(lè)觀鎖更新 update user set name = #{user.name} and status = #{user.status} where id = #{user.id} and status = #{oldStatus}
userMapper.updateUser(user, oldStatus);
}
User user1 = userMapper.select(user.getId());
logger.info(user1.getName());
}
2、分析
上面代碼的意思是:查詢最早的一條狀態(tài)為0的數(shù)據(jù)丈钙,然后設(shè)置狀態(tài)為1非驮,最后使用樂(lè)觀鎖更新,提交事務(wù)雏赦。
這種代碼在我們平時(shí)開發(fā)中非常常見(jiàn)院尔,有時(shí)候因?yàn)榭赡苡卸鄠€(gè)數(shù)據(jù)庫(kù)操作,這個(gè)方法會(huì)加上 @Transactional 注解來(lái)使用事務(wù)喉誊,保證事務(wù)的原子性邀摆。我們可能太注意原子性,而忽略了事務(wù)的隔離性伍茄。在默認(rèn)情況下栋盹,mysql 的隔離性為可重復(fù)讀(一個(gè)事務(wù)在最開始讀到的數(shù)據(jù)在事務(wù)執(zhí)行過(guò)程中不隨著其他事務(wù)的操作而改變)。
這段代碼在實(shí)際運(yùn)行中會(huì)有什么問(wèn)題敷矫?我聽到了以下幾個(gè)不同的意見(jiàn):
- 1.這段代碼會(huì)鎖表
- 2.在事務(wù)1執(zhí)行 userMapper.updateUser(user, oldStatus) 后事務(wù)沒(méi)提交之前例获,事務(wù)2執(zhí)行 userMapper.selectUser() 是不能防止查找同一條數(shù)據(jù)
- 3.在事務(wù)1、2查到同一條數(shù)據(jù)曹仗,事務(wù)1執(zhí)行 userMapper.updateUser(user, oldStatus) 沒(méi)提交之前榨汤,事務(wù)2執(zhí)行 userMapper.updateUser(user, oldStatus) 會(huì)卡住,但是1執(zhí)行完畢之后怎茫,最后事務(wù)2什么都不會(huì)更新
上面三個(gè)說(shuō)法哪個(gè)正確呢收壕?
- 1.說(shuō)法1是錯(cuò)誤的妓灌,一般鎖表的情況下很多,除非表就一條數(shù)據(jù)蜜宪,否則的話虫埂,在可重復(fù)讀的情況下,mysql 對(duì)于程序不是修改同一條數(shù)據(jù)不會(huì)鎖灼匝椤(親測(cè))描馅,修改同一行數(shù)據(jù)只會(huì)鎖行县踢,更不可能鎖表。
- 2.說(shuō)法2是正確的,因?yàn)槭聞?wù)1沒(méi)提交之前(雖然已經(jīng)執(zhí)行了 update)谈截,可重復(fù)讀的情況下婴削,數(shù)據(jù)修改對(duì)于其他事務(wù)是不可見(jiàn)的晦炊,事務(wù)2仍然能夠查詢相同的數(shù)據(jù)
- 3.說(shuō)法3是正確的击狮,在同一條數(shù)據(jù)的情況下,修改同一條數(shù)據(jù)會(huì)卡住栗精,修改不同數(shù)據(jù)不會(huì)闯参。因?yàn)橄刃薷牡臄?shù)據(jù)會(huì)拿到行鎖,直到提交才會(huì)釋放悲立。后面拿到行鎖的事務(wù)鹿寨,因?yàn)槲疫@邊有一個(gè)樂(lè)觀鎖修改,前面事務(wù)已經(jīng)修改狀態(tài)薪夕,而這個(gè)事務(wù)會(huì)查不到改狀態(tài)的數(shù)據(jù)脚草,從而不修改數(shù)據(jù)。
說(shuō)了那么多其實(shí)就像說(shuō)明一點(diǎn)原献,而平時(shí)開發(fā)中馏慨,如果用到了事務(wù),針對(duì)數(shù)據(jù)庫(kù)狀態(tài)的問(wèn)題要多多考慮姑隅。我為什么會(huì)說(shuō)這話写隶,因?yàn)榫驮谥芪逦覀冇懻撁鎲纬厣暾?qǐng)修改的問(wèn)題,我說(shuō):只要我先拿到這條數(shù)據(jù)讲仰,后更新狀態(tài)為其他慕趴,別人就拿不到了。但是被別人當(dāng)場(chǎng)反駁:你的事務(wù)沒(méi)提交鄙陡,不能防止別人查不到這條數(shù)據(jù)冕房。所以讓我的方案頓時(shí)失效,雖然后面可以重新開一個(gè)事務(wù)趁矾,在另外的事務(wù)中做這個(gè)事耙册,或者將鎖提升到事務(wù)外部,用 redis 來(lái)控制取面單毫捣。但主要我是沒(méi)有考慮好事務(wù)的問(wèn)題详拙,所以導(dǎo)致我想問(wèn)題異常簡(jiǎn)單帝际。。溪厘。胡本。牌柄。
3畸悬、幻讀
在這里說(shuō)一個(gè)幻讀的定義的矯正。
很多人說(shuō)幻讀是可重復(fù)讀的情況下珊佣,事務(wù)1執(zhí)行事務(wù)蹋宦,先 select 沒(méi)有,后 insert咒锻,但是事務(wù)1還未提交冷冗;事務(wù)2也是先 select 沒(méi)有,后 select 發(fā)現(xiàn)多一條數(shù)據(jù)惑艇。這里以 A(id, name) 操作如下:
事務(wù)1 | 事務(wù)2 |
---|---|
開啟事務(wù) | 開啟事務(wù) |
select * from A where id = 1(啥數(shù)據(jù)都沒(méi)有) | select * from A where id = 1 (啥數(shù)據(jù)都沒(méi)有) |
insert into A values(1, 'ppp') | |
select * from A where id = 1 (突然發(fā)現(xiàn)多了一條數(shù)據(jù)) | |
事務(wù)提交 | 事務(wù)提交 |
說(shuō)實(shí)話蒿辙,上面的說(shuō)法無(wú)比扯淡。既然都是可重復(fù)讀了滨巴,在事務(wù)2在一個(gè)事務(wù)中怎么讀取跟原來(lái)不同的結(jié)果呢思灌?都違背了可重復(fù)讀的定義(否管 innerdb 咋實(shí)現(xiàn)的)。
正確的結(jié)果如下:
事務(wù)1 | 事務(wù)2 |
---|---|
開啟事務(wù) | 開啟事務(wù) |
select * from A where id = 1(啥數(shù)據(jù)都沒(méi)有) | select * from A where id = 1 (啥數(shù)據(jù)都沒(méi)有) |
insert into A values(1, 'ppp') | |
select * from A where id = 1 (還是啥數(shù)據(jù)都沒(méi)有) | |
insert into A values(1, 'ppp') (報(bào)主鍵沖突恭取,插入失斕┏ァ) | |
事務(wù)提交 | 事務(wù)提交 |
所以,幻讀并不是指同一個(gè)事務(wù)執(zhí)行兩次相同的select語(yǔ)句得到的結(jié)果不同, 而是指select時(shí)不存在某記錄,但準(zhǔn)備插入時(shí)發(fā)現(xiàn)此記錄已存在,無(wú)法插入,這就產(chǎn)生了幻讀蜈垮。
4耗跛、資料
這里有一篇美團(tuán)的文章,事務(wù)講的特別好:https://tech.meituan.com/2014/08/20/innodb-lock.html