掃二維碼與項目經(jīng)理溝通
我們在微信上24小時期待你的聲音
解答本文疑問/技術咨詢/運營咨詢/技術建議/互聯(lián)網(wǎng)交流
分布式鎖想必大家都不陌生,可以用來解決在分布式環(huán)境下,多個用戶在同一時間讀取/更新相同的資源帶來的問題。比如秒殺場景下的庫存問題、redis key失效情況下請求直接打到MySQL中造成MySQL負載過大的問題,這些問題都可以通過分布式鎖來解決。

創(chuàng)新互聯(lián)建站為企業(yè)級客戶提高一站式互聯(lián)網(wǎng)+設計服務,主要包括成都網(wǎng)站建設、做網(wǎng)站、App定制開發(fā)、小程序定制開發(fā)、宣傳片制作、LOGO設計等,幫助客戶快速提升營銷能力和企業(yè)形象,創(chuàng)新互聯(lián)各部門都有經(jīng)驗豐富的經(jīng)驗,可以確保每一個作品的質(zhì)量和創(chuàng)作周期,同時每年都有很多新員工加入,為我們帶來大量新的創(chuàng)意。
關于如何實現(xiàn)分布式鎖,大家可能對基于Redis?實現(xiàn)比較熟悉,但是往往很多情況是一些并發(fā)量不大的項目用不上Redis,Redis往往適用于并發(fā)量比較大的場景。但是MySQL基本都是有的,所以今天我來談談如何基于MySQL實現(xiàn)我們的分布式鎖。
我們可以使用MySQL的唯一性約束來實現(xiàn)分布式鎖,整體的思路如下:
現(xiàn)在我們來簡單實現(xiàn)下,創(chuàng)建一個lock?表,其中l(wèi)ock_key字段有唯一性約束。
CREATE TABLE `lock` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`lock_key` varchar(256) NOT NULL,
`holder` varchar(256) NOT NULL,
`creation_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `uniq_lock_key` (`lock_key`)
);
獲取鎖:
INSERT INTO `lock`(`lock_key`, `holder`) VALUES ('project1_uid1', 'server1_ip1_tid1');
釋放鎖:
DELETE FROM `lock` WHERE `lock_key` = 'project1_uid1';
上面的方案已經(jīng)基本滿足通過MySQL實現(xiàn)分布式鎖的基本要求?,F(xiàn)在讓我們考慮一些特殊情況,看看它是否對分布式系統(tǒng)中的常見故障具有魯棒性。
如果客戶端 A 獲取了鎖,向 DB 中插入了一行,但后來客戶端 A 崩潰了,或者網(wǎng)絡分區(qū)和客戶端 A 無法訪問 DB 怎么辦?在這種情況下,該行將保留在數(shù)據(jù)庫中,不會被刪除。換句話說,對于其他客戶端來說,就好像客戶端 A 仍然持有鎖(即使 A 已經(jīng)崩潰了?。?。其他客戶端將無法獲取鎖,并返回錯誤。
一種常用的方法是為每個鎖分配一個 TTL。這個想法很簡單:如果客戶端 A 崩潰并且無法釋放鎖,那么其他人應該執(zhí)行刪除 DB 中的行從而釋放鎖的工作。假設通常客戶端 A 需要 3 分鐘才能完成任務。我們可以將 TTL 設置為 5 分鐘。然后我們需要構(gòu)建另一個服務來不斷掃描lock表,并刪除超過 5 分鐘前創(chuàng)建的任何行。但是,還有其他問題:
第一個問題用MySQL很難完全解決。我們可以考慮A在獲取到分布式鎖后,新起個線程去檢查鎖是否快要過期了,比如發(fā)現(xiàn)TTL還剩下1/3時間,但是A還沒有結(jié)束,這時候去擴大TTL時間,這就是鎖的續(xù)簽機制。但是在現(xiàn)實中,對于大部分的業(yè)務案例,我們總是可以設置一個足夠大的TTL,使得這種情況很少發(fā)生,以至于對公司業(yè)務的影響幾乎察覺不到。
現(xiàn)在讓我們看看第2個問題怎么解決?
我們可以在lock?表中添加一列來存儲上次獲取鎖的時間戳last_lock_time。
CREATE TABLE `lock` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`lock_key` varchar(128) NOT NULL,
`holder` varchar(128) NOT NULL DEFAULT '',
`version` int(11) not null,
`creation_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
`last_lock_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `uniq_lock_key` (`lock_key`)
);
現(xiàn)在我們用${timeout}表示分布式鎖的TTL。
獲取鎖:
當客戶端 B 試圖獲取鎖時,我們可以添加`last_lock_time` < ${now} - ${timeout}?作為where條件的一部分。
UPDATE `lock` SET `holder` = 'server1_ip1_tid1', `last_lock_time` = ${now} WHERE `lock_key` = 'project1_uid1' and `last_lock_time` < ${now} - ${timeout};
在這種情況下,只有當`last_lock_time` < ${now} - ${timeout}?客戶端 B 可以獲取鎖、將 holder? 更改為其 ID 并將其重置last_lock_time?為當前時間戳時。假設后面客戶端 B 掛了,不能釋放鎖,最壞的情況是等待${timeout}TTL時間以后,其他客戶端就能拿到鎖。
釋放鎖:
我們可以把last_lock_time?更新為一個很小時間戳,例如‘1970–01–01 00:00:01’。
UPDATE `lock` SET `holder` = '', `last_lock_time` = ${min_timestamp} WHERE `lock_key` = 'project1_uid1' and `holder` = 'server1_ip1_tid1';
在WHERE語句中,我們添加了`holder` = ‘server1_ip1_tid1’,這是為了避免其他客戶端不小心釋放了當前客戶端持有的鎖。
成功釋放鎖后,holder?將其設置為空,并將last_lock_time設置為最小時間戳,以便其他客戶端可以輕松獲取鎖。
現(xiàn)在我們解決了TTL問題,但是在上面的實現(xiàn)中,如果持有鎖,其他客戶端將需要一直循環(huán)重試,等待鎖釋放后再獲取鎖。如果分布式鎖服務可以通知等待的客戶端鎖可用,那就更好了,我們思考下在MySQL中該如何實現(xiàn)。
MySQL具有行級鎖功能,在RC隔離級別下,當我們使用FOR UPDATE?時,MySQL會為所有符合過濾條件的行加行級鎖。當一個客戶端會話獲得鎖時,所有其他客戶端都將等待鎖。此外,等待客戶端喚醒并獲取鎖的順序與它們首次嘗試獲取鎖時的順序相同。只要持有鎖的客戶端在 SQL 事務內(nèi)執(zhí)行邏輯,F(xiàn)OR UPDATE 就可以執(zhí)行多次。換句話說,鎖是重入鎖。
另外,針對FOR UPDATE?,MySQL還支持兩種模式:NOWAIT? 和 SKIP LOCKED。
通過這兩個選項,我們可以實現(xiàn)tryLock行為,即客戶端嘗試獲取鎖,獲取不到鎖則立即返回,而不是等待。
我們可以簡化我們的lock表以僅包含兩個字段:
CREATE TABLE `lock` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`lock_key` varchar(128) NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `uniq_lock_key` (`lock_key`)
);
獲取鎖:
BEGIN;
SELECT * FROM `demo`.`lock` WHERE `lock_key` = 'project1_uid1' FOR UPDATE;
這里關于啟動新事務BEGIN? 做一個說明,只有在第一次獲取鎖時才需要它。后續(xù)重入時,不要執(zhí)行BEGIN,否則會啟動一個新的事務,現(xiàn)有的事務結(jié)束,實際上是在事務結(jié)束時釋放鎖。
非阻塞嘗試鎖tryLock():
BEGIN;
SELECT * FROM `demo`.`lock` WHERE `lock_key` = 'project1_uid1' FOR UPDATE NOWAIT;
釋放鎖:
COMMIT;
提交事務就可以釋放鎖。
我們現(xiàn)在回頭來看看基于MySQL實現(xiàn)分布式鎖,是否滿足我們一開始定下的設計目標:
看來基本上是沒什么問題的,但是還有一點,我們需要提前向lock表中插入資源鎖的數(shù)據(jù),然后獲取/嘗試/釋放鎖的 API 才能按預期工作。
參考:https://medium.com/@bb8s/design-distributed-lock-with-mysql-9bc28ac59629

我們在微信上24小時期待你的聲音
解答本文疑問/技術咨詢/運營咨詢/技術建議/互聯(lián)網(wǎng)交流