1、前言
平時開發(fā)我們經(jīng)常使用 Spring 事務(wù),而 Spring 默認(rèn)使用 mysql 的事務(wù)。mysql 事務(wù)默認(rèn)的隔離級別為:可重復(fù)讀。我們就以可重復(fù)讀為例子看一下代碼(事務(wù)的隔離性是用鎖做的,如果不是操作同一行數(shù)據(jù)就不會鎖)。
@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);
// 樂觀鎖更新 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,最后使用樂觀鎖更新,提交事務(wù)。
這種代碼在我們平時開發(fā)中非常常見,有時候因為可能有多個數(shù)據(jù)庫操作,這個方法會加上 @Transactional 注解來使用事務(wù),保證事務(wù)的原子性。我們可能太注意原子性,而忽略了事務(wù)的隔離性。在默認(rèn)情況下,mysql 的隔離性為可重復(fù)讀(一個事務(wù)在最開始讀到的數(shù)據(jù)在事務(wù)執(zhí)行過程中不隨著其他事務(wù)的操作而改變)。
這段代碼在實際運(yùn)行中會有什么問題?我聽到了以下幾個不同的意見:
- 1.這段代碼會鎖表
- 2.在事務(wù)1執(zhí)行 userMapper.updateUser(user, oldStatus) 后事務(wù)沒提交之前,事務(wù)2執(zhí)行 userMapper.selectUser() 是不能防止查找同一條數(shù)據(jù)
- 3.在事務(wù)1、2查到同一條數(shù)據(jù),事務(wù)1執(zhí)行 userMapper.updateUser(user, oldStatus) 沒提交之前,事務(wù)2執(zhí)行 userMapper.updateUser(user, oldStatus) 會卡住,但是1執(zhí)行完畢之后,最后事務(wù)2什么都不會更新
上面三個說法哪個正確呢?
- 1.說法1是錯誤的,一般鎖表的情況下很多,除非表就一條數(shù)據(jù),否則的話,在可重復(fù)讀的情況下,mysql 對于程序不是修改同一條數(shù)據(jù)不會鎖?。ㄓH測),修改同一行數(shù)據(jù)只會鎖行,更不可能鎖表。
- 2.說法2是正確的,因為事務(wù)1沒提交之前(雖然已經(jīng)執(zhí)行了 update),可重復(fù)讀的情況下,數(shù)據(jù)修改對于其他事務(wù)是不可見的,事務(wù)2仍然能夠查詢相同的數(shù)據(jù)
- 3.說法3是正確的,在同一條數(shù)據(jù)的情況下,修改同一條數(shù)據(jù)會卡住,修改不同數(shù)據(jù)不會。因為先修改的數(shù)據(jù)會拿到行鎖,直到提交才會釋放。后面拿到行鎖的事務(wù),因為我這邊有一個樂觀鎖修改,前面事務(wù)已經(jīng)修改狀態(tài),而這個事務(wù)會查不到改狀態(tài)的數(shù)據(jù),從而不修改數(shù)據(jù)。
說了那么多其實就像說明一點(diǎn),而平時開發(fā)中,如果用到了事務(wù),針對數(shù)據(jù)庫狀態(tài)的問題要多多考慮。我為什么會說這話,因為就在周五我們討論面單池申請修改的問題,我說:只要我先拿到這條數(shù)據(jù),后更新狀態(tài)為其他,別人就拿不到了。但是被別人當(dāng)場反駁:你的事務(wù)沒提交,不能防止別人查不到這條數(shù)據(jù)。所以讓我的方案頓時失效,雖然后面可以重新開一個事務(wù),在另外的事務(wù)中做這個事,或者將鎖提升到事務(wù)外部,用 redis 來控制取面單。但主要我是沒有考慮好事務(wù)的問題,所以導(dǎo)致我想問題異常簡單。。。。。
3、幻讀
在這里說一個幻讀的定義的矯正。
很多人說幻讀是可重復(fù)讀的情況下,事務(wù)1執(zhí)行事務(wù),先 select 沒有,后 insert,但是事務(wù)1還未提交;事務(wù)2也是先 select 沒有,后 select 發(fā)現(xiàn)多一條數(shù)據(jù)。這里以 A(id, name) 操作如下:
| 事務(wù)1 | 事務(wù)2 |
|---|---|
| 開啟事務(wù) | 開啟事務(wù) |
| select * from A where id = 1(啥數(shù)據(jù)都沒有) | select * from A where id = 1 (啥數(shù)據(jù)都沒有) |
| insert into A values(1, 'ppp') | |
| select * from A where id = 1 (突然發(fā)現(xiàn)多了一條數(shù)據(jù)) | |
| 事務(wù)提交 | 事務(wù)提交 |
說實話,上面的說法無比扯淡。既然都是可重復(fù)讀了,在事務(wù)2在一個事務(wù)中怎么讀取跟原來不同的結(jié)果呢?都違背了可重復(fù)讀的定義(否管 innerdb 咋實現(xiàn)的)。
正確的結(jié)果如下:
| 事務(wù)1 | 事務(wù)2 |
|---|---|
| 開啟事務(wù) | 開啟事務(wù) |
| select * from A where id = 1(啥數(shù)據(jù)都沒有) | select * from A where id = 1 (啥數(shù)據(jù)都沒有) |
| insert into A values(1, 'ppp') | |
| select * from A where id = 1 (還是啥數(shù)據(jù)都沒有) | |
| insert into A values(1, 'ppp') (報主鍵沖突,插入失?。?/td> | |
| 事務(wù)提交 | 事務(wù)提交 |
所以,幻讀并不是指同一個事務(wù)執(zhí)行兩次相同的select語句得到的結(jié)果不同, 而是指select時不存在某記錄,但準(zhǔn)備插入時發(fā)現(xiàn)此記錄已存在,無法插入,這就產(chǎn)生了幻讀。
4、資料
這里有一篇美團(tuán)的文章,事務(wù)講的特別好:https://tech.meituan.com/2014/08/20/innodb-lock.html