兩個事務併發寫,能保證資料唯一嗎?
“ 本文正在參加「金石計劃 . 瓜分6萬現金大獎」 ”
喲,又是我小白。最近有點高產了。
連我自己都害怕了。
直接進入正題吧。
兩個事務併發寫,能保證資料唯一嗎?
我先來解釋下標題講的是個啥。
我們假設有這麼一個使用者註冊的場景。使用者併發請求註冊新使用者。
你有一張資料庫表,也就是下面的user表。
產品經理要求使用者和使用者之間,電話號碼不能重複,為了保證這一點。我們想到了先查一下資料庫,再判斷一下,如果存在,就退出,否則插入一條資料。類似下面這樣的虛擬碼。
sql
select user where phone_no =2; // 查詢sql
if (user 存在) {
return
} else {
insert user; // 插入sql
}
但這是兩條sql語句,先執行查詢sql,判斷後再決定要不要執行插入sql。每次使用者註冊的時候都會執行這麼一段邏輯。
那如果,此時有多個使用者在做操作,就會併發執行這段邏輯。
如果都併發執行,第一條sql語句執行完之後,都會發現沒有使用者存在。此時都執行了插入,這樣就出現了兩條一樣的資料才對。
所以,有人就想了,這兩條sql語句邏輯應該是一個整體,不應該拆開,於是就想到了事務,通過事務把這兩個sql作為一個整體,要麼一起執行,要麼都回滾。
這正是資料庫ACID裡的A(Atomicity),原子性的完美體現啊。
虛擬碼類似下面這樣。
sql
begin;
select user where phone_no =2; // 查詢sql
if (user 存在) {
return
} else {
insert user; // 插入sql
}
commit;
那麼問題來了,這段邏輯,併發執行,能保證資料唯一?
當然是不能。
事務內的多條sql語句,確實是原子的,要麼一起成功,要麼一起失敗,這沒錯,但跟這個場景沒什麼太大關係。事務是併發執行的,第一個事務執行查詢使用者,並不會阻塞另一個事務查詢使用者,所以都有可能查到使用者不存在,此時兩個事務邏輯都判斷為使用者不存在,然後插入資料庫。事務內兩條sql都執行成功了,於是就插入了兩條一樣的資料。
怎麼保證資料唯一?
那麼我們接下來聊聊,怎麼保證上面這種場景下,插入的資料是唯一的。方法有很多種,但我們今天只討論mysql內部的做法,不考慮其他外部中介軟體(比如redis分散式鎖這些)。
唯一索引
通過下面的命令,可以為資料庫user表的phone_no欄位加入唯一索引。
ALTER TABLE `user` ADD unique(`phone_no`);
我們執行一條寫操作時,比如下面這句,
sql
INSERT INTO `user` (`user_name`, `phone_no`) VALUES('小紅', 2);
第一次會插入成功,第二次再執行插入,則會出現報錯。
sql
Duplicate entry '2' for key 'phone_no'
含義是phone_no這個欄位是唯一的,加兩次phone_no=2會導致重複。
於是乎回到我們文章開頭的場景裡,就完美解決了重複插入的問題了。
那麼問題來了。
為什麼唯一索引能保證資料唯一?
我們看看一句寫操作,會經歷什麼。
首先,mysql作為一個數據庫,內部主要分為兩層,一層是server層,一層是儲存引擎層(一般是innodb)。
server層主要管的是資料庫連結,許可權校驗,以及sql語句校驗和優化之類的工作。請求打到儲存引擎層,才是真正的查詢和更新資料的操作。
大家都知道資料庫是持久化儲存,且最後都是把資料存到磁碟上的。
那資料庫讀寫是直接讀寫磁碟資料嗎?
不是,如果直接讀寫磁碟的話,那就太慢了,為了提升速度。
它在磁碟前面加了一層記憶體,叫buffer pool。它裡面有很多細節,但最主要的就是個雙向連結串列,裡面放的是一個個資料頁,每個資料頁的大小預設是 16kb,資料頁裡面放的就是磁碟的資料。
於是有了這層buffer pool記憶體,mysql的讀和寫操作都可以先操作這部分記憶體,如果想要讀寫的資料頁不在buffer pool裡,再跑到磁盤裡去撈。由於讀寫記憶體的速度比讀寫磁碟快得多。
所以引擎讀寫都快多了。
但這還不夠,很多時候寫操作,我的訴求就是把xx更新為xx,或插入xx,資料庫光知道這一點就夠了,我根本不需要知道資料頁原來長什麼樣子。
有點抽象?舉個例子吧。
比方說我想要把id=1的這條資料的phone_no欄位更新為100,資料庫知道這一點就夠了,至於這條資料原來phone_no究竟是等於20,還是30,這根本不重要,反正最後都會變成我想要的phone_no=100。
也就是說,如果有那麼一塊記憶體,記錄下我準備把資料改成什麼樣子,然後後續非同步慢慢更新到磁碟資料上。那我甚至到不需要在一開始就把這塊資料從磁碟讀到buffer pool中,按照這個思路,change buffer就來了。
於是乎,寫加了普通索引的資料,它只要把想要寫的內容寫到change buffer上,就立馬結束返回了。後面innodb引擎拿著這個change buffer,再非同步讀入磁碟資料到記憶體,將change buffer的資料修改到資料頁中,再寫回磁碟,這速度就上來了,秒啊。
但這個change buffer,放在唯一索引這裡就不管用了,畢竟,它得保證資料真的只有一條,那就得去看下資料庫裡,是不是真的有這條資料。
所以,對於insert場景,普通索引把需求扔到change buffer就完事返回了,而唯一索引需要真的把資料從磁碟讀到記憶體來,看下是不是有重複的,沒重複的再插入資料。
這唯一索引,在效能上就輸了一截了。
所以回到唯一索引為什麼能保證資料唯一的問題上,一句話概括就是,唯一索引會繞過change buffer,確保把磁碟資料讀到記憶體後再判斷資料是否存在,不存在才能插入資料,否則報錯,以此來保證資料是唯一的。
總結
- 加唯一索引可以保證資料併發寫入時資料唯一,而且最省事省心。
- 資料庫通過引入一層buffer pool記憶體來提升讀寫速度,普通索引可以利用change buffer提高資料插入的效能。
- 唯一索引會繞過change buffer,確保把磁碟資料讀到記憶體後再判斷資料是否存在,不存在才能插入資料,否則報錯,以此來保證資料是唯一的。
給大家留個問題唄,前面也提到了,innodb中,利用了change buffer,為普通索引做了加速。有沒有哪些場景下,change buffer不僅不能給普通索引加速,還起到反作用的呢?
最後
大家也別笑,文章開頭提到的通過開事務來保證資料唯一性的錯誤操作,其實很容易犯,而且我曾經也遇到過不止一次這樣的事情。
做這個操作的人,還會信誓旦旦,言之鑿鑿的說出他的理解,在我解釋了幾遍發現無果之後,我選擇低頭假裝思考,然後說:"你說的有點道理,我再回去好好想想",然後默默的為資料表加上唯一索引......
我相信對方肯定已經理解了。那一刻,我感覺我寫的不是程式碼,我寫的是人情世故。
如果文章對你有幫助,歡迎.....
算了。