MySQL中SELECT UPDATE併發更新問題

2021-09-24 11:34:15 字數 3402 閱讀 8568

2019獨角獸企業重金招聘python工程師標準》

問題背景:

假設mysql資料庫有一張會員表vip_member(innodb表),結構如下:

當乙個會員想續買會員(只能續買1個月、3個月或6個月)時,必須滿足以下業務要求:

問題分析:

對於上面這種情況,我們一般會先select查出這條記錄,然後根據查出記錄的end_at再update start_at和end_at,偽**如下(為uid是1001的會員續1個月):

vipmember = select * from vip_member where uid=1001 limit 1 # 查uid為1001的會員

if vipmember.end_at 

update vip_member set start_at=now(), end_at=date_add(now(), interval 1 month), active_status=1, updated_at=now() where uid=1001

else:

update vip_member set end_at=date_add(end_at, interval 1 month), active_status=1, updated_at=now() where uid=1001

假如同時有兩個執行緒執行上面的**,很顯然存在「資料覆蓋」問題(即乙個是續1個月,乙個續2個月,但最終可能只續了2個月,而不是加起來的3個月)。

解決方案:

a、我想到的第一種方案是把select和update合成一條sql,如下:

update vip_member 

set 

start_at = case

when end_at 

then now()

else start_at

end,

end_at = case

when end_at 

then date_add(now(), interval 1 month)

else date_add(end_at, interval 1 month)

end,

active_status=1,

updated_at=now()

where uid=#uid:bigint#

limit 1;

so easy!

b、第二種方案:事務

,即用乙個事務來包裹上面的select+update操作。

那麼是否包上事務就萬事大吉了呢?

顯然不是。因為如果同時有兩個事務都分別select到相同的vip_member記錄,那麼一樣的會發生資料覆蓋問題。那有什麼辦法可以解決呢?難道要設定事務隔離級別為serializable,考慮到效能不現實。

我們知道innodb支援行鎖。

檢視mysql官方文件(

innodb locking reads

)了解到innodb在讀取行資料時可以加兩種鎖:讀共享鎖和寫獨佔鎖。

讀共享鎖是通過下面這樣的sql獲得的:

select * from parent where name = 'jones' lock in share mode;
如果事務a獲得了先獲得了讀共享鎖,那麼事務b之後仍然可以讀取加了讀共享鎖的行資料,但必須等事務a commit或者roll back之後才可以更新或者刪除加了讀共享鎖的行資料。

寫獨佔鎖是通過select...for update獲得:

select counter_field from child_codes for update;

update child_codes set counter_field = counter_field + 1;

如果事務a先獲得了某行的寫獨佔鎖,那麼事務b就必須等待事務a commit或者roll back之後才可以訪問行資料。

顯然要解決會員狀態更新問題,不能加讀共享鎖,只能加寫獨佔鎖,即將前面的sql改寫成如下:

vipmember = select * from vip_member where uid=1001 limit 1 for update # 查uid為1001的會員

if vipmember.end_at 

update vip_member set start_at=now(), end_at=date_add(now(), interval 1 month), active_status=1, updated_at=now() where uid=1001

else:

update vip_member set end_at=date_add(end_at, interval 1 month), active_status=1, updated_at=now() where uid=1001

c、第三種方案:樂觀鎖,類cas機制

第二種加鎖方案是一種悲觀鎖機制。而且select...for update方式也不太常用,聯想到cas實現的樂觀鎖機制,於是我想到了第三種解決方案:樂觀鎖。

具體來說也挺簡單,首先select sql不作任何修改,然後在update sql的where條件中加上select出來的vip_memer的end_at條件。如下:

vipmember = select * from vip_member where uid=1001 limit 1 # 查uid為1001的會員

cur_end_at = vipmember.end_at

if vipmember.end_at 

update vip_member set start_at=now(), end_at=date_add(now(), interval 1 month), active_status=1, updated_at=now() where uid=1001 and end_at=cur_end_at

else:

update vip_member set end_at=date_add(end_at, interval 1 month), active_status=1, updated_at=now() where uid=1001 and end_at=cur_end_at

這樣可以根據update返回值來判斷是否更新成功,如果返回值是0則表明存在併發更新,那麼只需要重試一下就好了。

方案比較:

三種方案各自優劣也許眾說紛紜,只說說我自己的看法:

php中mysql函式 php中mysql有關函式

1.mysql query 一般是用來查詢資料裡面的資料。如 username post name sql select from members where login name username result mysql query sql 以上程式是檢測資料庫中是否存在表單傳送過來的使用者名稱...

mysql中 變數 mysql中的變數

toc 變數 mysql本質是一種程式語言,需要很多變數來儲存資料。mysql中很多的屬性控制都是通過mysql中固有的變數來實現的。系統變數 系統內部定義的變數,系統變數針對所有使用者 mysql客戶端 有效。檢視系統所有變數 show variables like pattern mysql允許...

mysql中 變數 MYSQL中的變數 MySQL

bitscn.com 只記很基礎的知識,細節東西太麻煩了,而且我也用不到。變數分為使用者變數與系統變數。使用者變數 使用者變數與資料庫連線有關,在這個連線中宣告的變數,在連線斷開的時候,就會消失。在此連線中宣告的變數無法在另一連線中使用。使用者變數的變數名的形式為 varname的形式。名字必須以 ...