mysql併發更新問題

2022-04-12 01:02:54 字數 3609 閱讀 4170

問題背景:

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

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

•如果end_at早於當前時間,則設定start_at為當前時間,end_at為當前時間加上續買的月數

•如果end_at等於或晚於當前時間,則設定end_at=end_at+續買的月數

•續買後active_status必須為1(即被啟用)

問題分析:

對於上面這種情況,我們一般會先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 < now():

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 < now() 

then now()

else start_at

end,

end_at = case

when end_at < now()

then date_add(now(), interval #duration:integer# month)

else date_add(end_at, interval #duration:integer# 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 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 < now():

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 < now():

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則表明存在併發更新,那麼只需要重試一下就好了。

方案比較:

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

•第一種方案利用一條比較複雜的sql解決問題,不利於維護,因為把具體業務糅在sql裡了,以後修改業務時不但需要讀懂這條sql,還很有可能會修改成更複雜的sql

•第二種方案寫獨佔鎖,可以解決問題,但不常用

•第三種方案應該是比較中庸的解決方案,並且甚至可以不加事務,也是我個人推薦的方案

•如果對讀的響應度要求非常高,比如**交易系統,那麼適合用樂觀鎖,因為悲觀鎖會阻塞讀

•如果讀遠多於寫,那麼也適合用樂觀鎖,因為用悲觀鎖會導致大量讀被少量的寫阻塞

•如果寫操作頻繁並且衝突比例很高,那麼適合用悲觀寫獨佔鎖

mysql子查詢更新問題

mysql不支援對同乙個表查詢後做修改 update delete 操作,是其功能問題。原來的sql語句 update t collection contract base base set base.total interest penalty select sum rp.remain inter...

hcharts更新問題

顯示 hcharts的顯示這裡就不多說了,基本官網上的例子都有 需要說明一點的是,在柱狀圖中表示時,可以使用座標的方式指定表示的橫座標,而不是只能像官網上一樣全部寫完 data可以寫成 1,5 2,8 其中1代表橫座標,5代表縱座標 更新 hcharts更新時series不會自動增加或減少,需要我們...

pip更新問題

更新命令 requirement already up to date 在pycharm的虛擬環境中,特別容易出現執行更新pip的命令後,提示要求己經更新,但實際上檢視版本還是沒更新成功的情況 直接使用pip install upgrade pip 這簡直是死迴圈了,使用python m pip i...