問題背景:
假設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操作。
那么是否包上事務就萬事大吉了呢?