自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

如何用MySQL設(shè)計(jì)一個(gè)分布式鎖?

數(shù)據(jù)庫 MySQL
關(guān)于如何實(shí)現(xiàn)分布式鎖,大家可能對基于Redis?實(shí)現(xiàn)比較熟悉,但是往往很多情況是一些并發(fā)量不大的項(xiàng)目用不上Redis,Redis往往適用于并發(fā)量比較大的場景。但是MySQL基本都是有的,所以今天我來談?wù)勅绾位贛ySQL實(shí)現(xiàn)我們的分布式鎖。

?前言

分布式鎖想必大家都不陌生,可以用來解決在分布式環(huán)境下,多個(gè)用戶在同一時(shí)間讀取/更新相同的資源帶來的問題。比如秒殺場景下的庫存問題、redis key失效情況下請求直接打到MySQL中造成MySQL負(fù)載過大的問題,這些問題都可以通過分布式鎖來解決。

圖片

關(guān)于如何實(shí)現(xiàn)分布式鎖,大家可能對基于Redis?實(shí)現(xiàn)比較熟悉,但是往往很多情況是一些并發(fā)量不大的項(xiàng)目用不上Redis,Redis往往適用于并發(fā)量比較大的場景。但是MySQL基本都是有的,所以今天我來談?wù)勅绾位贛ySQL實(shí)現(xiàn)我們的分布式鎖。

設(shè)計(jì)目標(biāo)

  1. 互斥。不同機(jī)器上許多進(jìn)程/線程中只有一個(gè)可以訪問特定資源,其他進(jìn)程/線程應(yīng)該等到鎖被釋放才可以用。
  2. TTL。從CAP理論我們知道,網(wǎng)絡(luò)總是不可靠的,任何一臺(tái)服務(wù)器都有可能宕機(jī)一段時(shí)間。所以我們在設(shè)計(jì)分布式鎖服務(wù)的時(shí)候,需要考慮到可能有一個(gè)持有鎖的客戶端宕機(jī),無法釋放鎖,從而阻塞所有等待獲取同一個(gè)鎖的客戶端。所以我們需要一種機(jī)制,可以在這種情況下自動(dòng)釋放鎖來解鎖其他客戶端。
  3. 相關(guān)API
  • lock():獲取鎖
  • unlock():釋放鎖
  • tryLock(): 可選,更高級的API,例如:客戶端可以指定獲取鎖的最大等待時(shí)間。如果不能在窗口內(nèi)獲得鎖,則錯(cuò)誤返回而不是繼續(xù)等待。
  1. 高性能
  • 低延遲:在正常情況下,鎖定和解鎖應(yīng)該非???。比如實(shí)際的業(yè)務(wù)邏輯處理只需要1ms,而單純的獲取和釋放鎖,處理一個(gè)請求又需要100ms,那么最大QPS只能達(dá)到10,這對于現(xiàn)在的很多服務(wù)來說已經(jīng)很低了。在這種情況下,服務(wù)器可以處理的最大 QPS 受到鎖性能的限制。
  • 通知機(jī)制:分布式鎖理想情況下應(yīng)該提供通知機(jī)制。如果服務(wù)器進(jìn)程A由于被另一個(gè)服務(wù)器進(jìn)程B持有而無法獲得鎖,那么A不應(yīng)該一直等待并占用CPU。相反,A 應(yīng)該空閑以避免浪費(fèi) CPU資源 。然后當(dāng)鎖可用時(shí),鎖服務(wù)通知A,A將獲得CPU資源并恢復(fù)運(yùn)行。
  • 避免驚群效應(yīng)。假設(shè)有 100 個(gè)進(jìn)程想要獲取同一個(gè)鎖,當(dāng)鎖可用時(shí),理想情況下應(yīng)該只通知隊(duì)列中的“下一個(gè)”進(jìn)程,而不是突然調(diào)用所有 100 個(gè)進(jìn)程來競爭鎖。
  1. 公平。先到先得。等待時(shí)間最長的人應(yīng)該下一個(gè)獲得鎖。如果是這樣,則該鎖被認(rèn)為是公平鎖。否則就是非公平鎖。這兩種鎖在現(xiàn)實(shí)中都有實(shí)際使用。
  2. 重入鎖。 想象一下,一個(gè)節(jié)點(diǎn)或服務(wù)器進(jìn)程獲取了一個(gè)鎖,開始處理業(yè)務(wù)邏輯,然后遇到一個(gè)代碼片段要求再次獲取同一個(gè)鎖,在這種情況下,節(jié)點(diǎn)或進(jìn)程不應(yīng)死鎖,相反,它應(yīng)該能夠再次獲取相同的鎖,因?yàn)樗呀?jīng)持有鎖。

MySQL如何實(shí)現(xiàn)分布式鎖?

1. 唯一鍵約束

我們可以使用MySQL的唯一性約束來實(shí)現(xiàn)分布式鎖,整體的思路如下:

  • 客戶端 A 正在嘗試獲取鎖。此時(shí)沒有其他客戶端持有鎖,所以客戶端A成功獲取到了鎖,并向MySQL表中插入一行數(shù)據(jù)。
  • 現(xiàn)在客戶端 B 想要獲取相同的鎖,先查詢DB,發(fā)現(xiàn)客戶端A插入的行已經(jīng)存在。在這種情況下,客戶端B無法獲取到鎖。然后客戶端 B 將等待一段時(shí)間后重試。客戶端 B 會(huì)在指定的 TTL 窗口內(nèi)不斷重試幾次,最終要么在客戶端 A 釋放鎖后成功獲取鎖,要么因?yàn)?TTL 而失敗。
  • 一旦客戶端 A 完成其任務(wù),它將通過簡單地刪除 DB 表中的行來釋放鎖lock。現(xiàn)在其他客戶端能夠獲取鎖。

現(xiàn)在我們來簡單實(shí)現(xiàn)下,創(chuàng)建一個(gè)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`)
);
  • lock_key? 是鎖的唯一名稱。我們可以使用 project_name + resource_id 作為鎖的名稱,表明要搶的資源是什么,具備唯一性。
  • holder?是當(dāng)前持有鎖的客戶端ID。我們可以使用service_name +IP 地址 + thread_id 來標(biāo)識(shí)分布式環(huán)境中的客戶端。

獲取鎖:

INSERT INTO `lock`(`lock_key`, `holder`) VALUES ('project1_uid1', 'server1_ip1_tid1');

釋放鎖:

DELETE FROM `lock` WHERE `lock_key` = 'project1_uid1';

上面的方案已經(jīng)基本滿足通過MySQL實(shí)現(xiàn)分布式鎖的基本要求?,F(xiàn)在讓我們考慮一些特殊情況,看看它是否對分布式系統(tǒng)中的常見故障具有魯棒性。

如果客戶端 A 獲取了鎖,向 DB 中插入了一行,但后來客戶端 A 崩潰了,或者網(wǎng)絡(luò)分區(qū)和客戶端 A 無法訪問 DB 怎么辦?在這種情況下,該行將保留在數(shù)據(jù)庫中,不會(huì)被刪除。換句話說,對于其他客戶端來說,就好像客戶端 A 仍然持有鎖(即使 A 已經(jīng)崩潰了?。F渌蛻舳藢o法獲取鎖,并返回錯(cuò)誤。

一種常用的方法是為每個(gè)鎖分配一個(gè) TTL。這個(gè)想法很簡單:如果客戶端 A 崩潰并且無法釋放鎖,那么其他人應(yīng)該執(zhí)行刪除 DB 中的行從而釋放鎖的工作。假設(shè)通??蛻舳?A 需要 3 分鐘才能完成任務(wù)。我們可以將 TTL 設(shè)置為 5 分鐘。然后我們需要構(gòu)建另一個(gè)服務(wù)來不斷掃描lock表,并刪除超過 5 分鐘前創(chuàng)建的任何行。但是,還有其他問題:

  • 如果 A 沒有崩潰,它只需要比平時(shí)多一點(diǎn)時(shí)間來完成任務(wù)怎么辦?
  • 如果我們?yōu)閽呙鑜ock表而構(gòu)建的這項(xiàng)新服務(wù)本身崩潰了怎么辦?

第一個(gè)問題用MySQL很難完全解決。我們可以考慮A在獲取到分布式鎖后,新起個(gè)線程去檢查鎖是否快要過期了,比如發(fā)現(xiàn)TTL還剩下1/3時(shí)間,但是A還沒有結(jié)束,這時(shí)候去擴(kuò)大TTL時(shí)間,這就是鎖的續(xù)簽機(jī)制。但是在現(xiàn)實(shí)中,對于大部分的業(yè)務(wù)案例,我們總是可以設(shè)置一個(gè)足夠大的TTL,使得這種情況很少發(fā)生,以至于對公司業(yè)務(wù)的影響幾乎察覺不到。

現(xiàn)在讓我們看看第2個(gè)問題怎么解決?

2. 使用時(shí)間戳+唯一鍵約束

我們可以在lock?表中添加一列來存儲(chǔ)上次獲取鎖的時(shí)間戳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。

獲取鎖:

當(dāng)客戶端 B 試圖獲取鎖時(shí),我們可以添加`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};

在這種情況下,只有當(dāng)`last_lock_time` < ${now} - ${timeout}?客戶端 B 可以獲取鎖、將 holder? 更改為其 ID 并將其重置last_lock_time?為當(dāng)前時(shí)間戳?xí)r。假設(shè)后面客戶端 B 掛了,不能釋放鎖,最壞的情況是等待${timeout}TTL時(shí)間以后,其他客戶端就能拿到鎖。

釋放鎖:

我們可以把last_lock_time?更新為一個(gè)很小時(shí)間戳,例如‘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’,這是為了避免其他客戶端不小心釋放了當(dāng)前客戶端持有的鎖。

成功釋放鎖后,holder?將其設(shè)置為空,并將last_lock_time設(shè)置為最小時(shí)間戳,以便其他客戶端可以輕松獲取鎖。

現(xiàn)在我們解決了TTL問題,但是在上面的實(shí)現(xiàn)中,如果持有鎖,其他客戶端將需要一直循環(huán)重試,等待鎖釋放后再獲取鎖。如果分布式鎖服務(wù)可以通知等待的客戶端鎖可用,那就更好了,我們思考下在MySQL中該如何實(shí)現(xiàn)。

3.使用FOR UPDATE?實(shí)現(xiàn)鎖釋放通知

MySQL具有行級鎖功能,在RC隔離級別下,當(dāng)我們使用FOR UPDATE?時(shí),MySQL會(huì)為所有符合過濾條件的行加行級鎖。當(dāng)一個(gè)客戶端會(huì)話獲得鎖時(shí),所有其他客戶端都將等待鎖。此外,等待客戶端喚醒并獲取鎖的順序與它們首次嘗試獲取鎖時(shí)的順序相同。只要持有鎖的客戶端在 SQL 事務(wù)內(nèi)執(zhí)行邏輯,F(xiàn)OR UPDATE 就可以執(zhí)行多次。換句話說,鎖是重入鎖。

另外,針對FOR UPDATE?,MySQL還支持兩種模式:NOWAIT? 和 SKIP LOCKED。

  • NOWAIT:不等待鎖的釋放。如果鎖被其他客戶端持有,無法獲取,則立即返回鎖沖突消息。
  • SKIP LOCKED:讀取數(shù)據(jù)時(shí),跳過行級鎖被其他客戶端持有的行。

通過這兩個(gè)選項(xiàng),我們可以實(shí)現(xiàn)tryLock行為,即客戶端嘗試獲取鎖,獲取不到鎖則立即返回,而不是等待。

我們可以簡化我們的lock表以僅包含兩個(gè)字段:

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;

這里關(guān)于啟動(dòng)新事務(wù)BEGIN? 做一個(gè)說明,只有在第一次獲取鎖時(shí)才需要它。后續(xù)重入時(shí),不要執(zhí)行BEGIN,否則會(huì)啟動(dòng)一個(gè)新的事務(wù),現(xiàn)有的事務(wù)結(jié)束,實(shí)際上是在事務(wù)結(jié)束時(shí)釋放鎖。

非阻塞嘗試鎖tryLock():

BEGIN;
SELECT * FROM `demo`.`lock` WHERE `lock_key` = 'project1_uid1' FOR UPDATE NOWAIT;

釋放鎖:

COMMIT;

提交事務(wù)就可以釋放鎖。

總結(jié)

我們現(xiàn)在回頭來看看基于MySQL實(shí)現(xiàn)分布式鎖,是否滿足我們一開始定下的設(shè)計(jì)目標(biāo):

  1. 互斥,最基本的功能,肯定是可以的。
  2. TTL 機(jī)制,MySQL 本地管理客戶端會(huì)話。如果客戶端由于機(jī)器故障或網(wǎng)絡(luò)故障而斷開連接,MySQL 將自動(dòng)釋放行級鎖。
  3. 支持所有 3 個(gè) API:獲取/嘗試/釋放鎖。
  4. 高性能:釋放鎖時(shí),MySQL只會(huì)通知隊(duì)列中等待的下一個(gè)客戶端,而不是一次性通知所有客戶端,避免雷群問題。
  5. 公平。MySQL 行鎖本身支持。
  6. 重入。MySQL 行鎖本身也支持。記住第一次獲取鎖就開始事務(wù),以后再入時(shí)不要再開始新的事務(wù)。

看來基本上是沒什么問題的,但是還有一點(diǎn),我們需要提前向lock表中插入資源鎖的數(shù)據(jù),然后獲取/嘗試/釋放鎖的 API 才能按預(yù)期工作。

參考:https://medium.com/@bb8s/design-distributed-lock-with-mysql-9bc28ac59629

責(zé)任編輯:武曉燕 來源: JAVA旭陽
相關(guān)推薦

2024-07-15 08:25:07

2021-11-01 12:25:56

Redis分布式

2020-07-30 09:35:09

Redis分布式鎖數(shù)據(jù)庫

2024-02-19 00:00:00

Redis分布式

2024-07-29 09:57:47

2022-04-14 07:56:30

公平鎖Java線程

2016-09-30 10:13:07

分布式爬蟲系統(tǒng)

2022-08-01 08:01:04

ID發(fā)號(hào)器系統(tǒng)

2022-08-11 18:27:50

面試Redis分布式鎖

2021-06-24 10:27:48

分布式架構(gòu)系統(tǒng)

2021-06-25 10:45:43

Netty 分布式框架 IO 框架

2018-09-06 22:49:31

分布式架構(gòu)服務(wù)器

2023-09-21 22:22:51

開發(fā)分布式鎖

2023-09-04 08:45:07

分布式配置中心Zookeeper

2022-11-11 08:19:03

redis分布式

2024-05-08 10:20:00

Redis分布式

2023-08-21 19:10:34

Redis分布式

2022-06-14 10:47:00

分布式事務(wù)數(shù)據(jù)

2019-06-19 15:40:06

分布式鎖RedisJava

2019-01-28 11:46:53

架構(gòu)運(yùn)維技術(shù)
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)