偷偷摘套内射激情视频,久久精品99国产国产精,中文字幕无线乱码人妻,中文在线中文a,性爽19p

基于 Redis 構建簡單分布式鎖的局限

數(shù)據(jù)庫 Redis
Redis 官方為用戶提供了 Lua 腳本支持,用戶可以向 Redis 服務器發(fā)送 Lua 腳本執(zhí)行自定義的邏輯,Redis 服務器會單線程原子性的執(zhí)行 Lua 腳本。

簡介

業(yè)務中,常有分布式鎖的需求,常見的解決方案便是基于 Redis 作為中心節(jié)點實現(xiàn)偽分布式效果,因為存在中心節(jié)點,所以我將其定義為偽分布式。

回歸主題,這篇文章,主要理一下,基于 Redis 實現(xiàn)簡單分布式鎖的一些問題,Redis 支持 RedLock(紅鎖)等復雜的實現(xiàn),以后的文章再討論。

基于 SETNX 命令實現(xiàn)分布式鎖

使用 SETNX 命令構建分布式鎖是最常見的實現(xiàn)方式,具體而言:

1. 通過 SETNX key value 向 Redis 新增一個值,SETNX 命令只有當 key 不存在時,才會插入值并返回成功,否則返回失敗,而 KEY 便可以作為分布式鎖的鎖名,通?;跇I(yè)務來決定該鎖名;

2. 通過 DEL key 命令刪除 key,從而實現(xiàn)釋放鎖的效果,當鎖釋放后,其他線程才可以通過 SETNX 獲得鎖(相同的 KEY);

3. 利用 EXPIRE key timeout 對 KEY 設置超時時間,從而實現(xiàn)鎖的超時自動釋放的效果,避免資源一直被占用。

redis-py (https://github.com/redis/redis-py) 這個庫便基于這種形式實現(xiàn) Redis 分布式鎖,將其源碼中相關代碼復制出來,如下:

# 獲得分布式鎖
def do_acquire(self, token):
# 利用SETNX實現(xiàn)分布式鎖
if self.redis.setnx(self.name, token):
if self.timeout:
timeout = int(self.timeout * 1000) # 轉成毫秒
# 設置分布式超時時間
self.redis.pexpire(self.name, timeout)
return True
return False

# 釋放分布式鎖
def do_release(self, expected_token):
name = self.name

def execute_release(pipe):
lock_value = pipe.get(name)
if lock_value != expected_token:
raise LockError("Cannot release a lock that's no longer owned")
# 利用DEL value實現(xiàn)鎖的釋放
pipe.delete(name)

self.redis.transaction(execute_release, name)

這種方式,存在一些問題,下文進行簡單的分析。

SETNX 與 EXPIRE 非原子性問題

SETNX 與 EXPIRE 是兩個操作,在 Redis 中不是原子操作。

如果 SETNX 成功(即獲得鎖),但在通過 EXPIRE 設置鎖超時時間時,服務器掛機、網(wǎng)絡中斷等問題,導致 EXPIRE 沒有成功執(zhí)行,此時鎖就變成了沒有超時時間的鎖了,如果業(yè)務邏輯沒有處理好鎖的釋放,則容易出現(xiàn)死鎖。

Redis 官方考慮到了這種情況,讓 SET 命令可以直接設置 Timeout 并實現(xiàn) SETNX 效果,SET 支持的語法變?yōu)椋篠ETEX key value NX timeout,這樣就不再需要通過 EXPIRE 設置超時時間,從而實現(xiàn)原子性了。

當然,在 Redis 官方還沒有實現(xiàn)這一功能時,很多開源庫也考慮到了這個問題,然后使用 Lua 腳本實現(xiàn) SETEX 與 EXPIRE 兩個操作的原子性。

因為用戶希望自定義若干指令來完成特定的業(yè)務,Redis 官方為這些用戶提供了 Lua 腳本支持,用戶可以向 Redis 服務器發(fā)送 Lua 腳本執(zhí)行自定義的邏輯,Redis 服務器會單線程原子性的執(zhí)行 Lua 腳本。

鎖誤解除

鎖誤解除也是常見的情況。

假設現(xiàn)在有 A、B 兩個線程在工作并競爭同一把鎖,線程 A 獲得了鎖,并將鎖的超時時間設置完成 30s,但線程 A 在處理業(yè)務邏輯時,因為數(shù)據(jù)庫 SQL 超時,原本 20s 就可以完成的任務,現(xiàn)在需要 40s 才能完成,當線程 A 花費 30s 時,鎖會自動釋放,此時線程 B 會獲得這把鎖,當線程 A 處理完業(yè)務邏輯時,會通過 DEL 去釋放鎖,此時釋放的是線程 B 的鎖,直觀如下圖所示:

解決方法便是添加唯一標識,在釋放鎖時,校驗 KEY 對應的唯一標識是否被當前線程持有,在 redis-py 中,通過 UUID 生成了當前線程的唯一標識 token,并在釋放鎖時,判斷當前線程是否擁有相同的 token,相關代碼如下 (你會發(fā)現(xiàn)與上面復制出來的代碼不同,這是因為舊文中使用的 redis-py 版本為 2.10.6,現(xiàn)在使用的 redis-py 版本為 3.5.3,相關的 bug 已經(jīng)被修改了,舊文的代碼,只是為了引出問題):

class Lock(object):
def __init__(self, redis, name, timeout=None, sleep=0.1,
blocking=True, blocking_timeout=None, thread_local=True):
# 線程本地存儲
self.local = threading.local() if self.thread_local else dummy()
self.local.token = None


def acquire(self, blocking=None, blocking_timeout=None, token=None):
sleep = self.sleep
if token is None:
# 基于UUID算法生成唯一token
token = uuid.uuid1().hex.encode()
# 省略剩余代碼...

def do_acquire(self, token):
if self.timeout:
timeout = int(self.timeout * 1000)
else:
timeout = None
# Token會通過set方法存入到Redis中
if self.redis.set(self.name, token, nx=True, px=timeout):
return True
return False

redis-py 基于 uuid 庫生成 token,并將其存到當前線程的本地存儲空間中(獨立于其他線程),在釋放時,判斷當前線程的 token 與加鎖時存儲的 token 釋放相同,redis-py 中利用 Lua 來實現(xiàn)這個過程,相關代碼如下:

def release(self):
"Releases the already acquired lock"
# 從線程本地存儲中獲得token
expected_token = self.local.token
if expected_token is None:
raise LockError("Cannot release an unlocked lock")
self.local.token = None
self.do_release(expected_token)

def do_release(self, expected_token):
# 利用Lua來釋放鎖,并實現(xiàn)判斷token是否相同的邏輯
if not bool(self.lua_release(keys=[self.name],
args=[expected_token],
client=self.redis)):
raise LockNotOwnedError("Cannot release a lock"
" that's no longer owned")

其中 lua_release 變量具體的值為:

LUA_RELEASE_SCRIPT = """
local token = redis.call('get', KEYS[1])
if not token or token ~= ARGV[1] then
return 0
end
redis.call('del', KEYS[1])
return 1
"""

上述 Lua 代碼中,通過 get 獲得 KEY 的 value,這個 value 就是 token,然后判斷與傳入的 token 是否相同,不相同的話,便不會執(zhí)行 DEL 命令,即不會釋放鎖。

鎖超時導致的并發(fā)

這種情況與鎖誤解除類似,同樣假設有線程 A、B,線程 A 獲得鎖并設置過期時間 30s,當線程 A 執(zhí)行時間超過 30s 時,鎖過期釋放,此時線程 B 獲得鎖,如果線程 A 與線程 B 是在業(yè)務上是有順序依賴的,此時出現(xiàn)了并發(fā)情況,便會導致業(yè)務結果的錯誤,直觀如下圖:

線程 A、B 同時執(zhí)行導致業(yè)務錯誤是我們不希望出現(xiàn)的,對于這種情況,有兩種解決方案:

1. 增大鎖的過期時間,讓業(yè)務邏輯有充足的執(zhí)行時間;

2. 添加守護線程,當鎖過期時,添加過期時間。

建議使用第一種方案,簡單直接,此外,可以添加單一線程,對 Redis 的 key 做監(jiān)控,對于時長特別長的 key,做監(jiān)控報警。

輪詢等待的效率問題

依舊是線程 A、B,當線程 A 獲得鎖時,線程 B 也想獲得鎖,此時就需要等待,直到線程 A 釋放鎖或者鎖過期自己釋放了,看 redis-py 的源碼,其等待的邏輯就是一個死循環(huán),相關代碼如下:

def acquire(self, blocking=None, blocking_timeout=None, token=None):
# ...省略部分代碼

# 死循環(huán)等待獲得鎖
while True:
if self.do_acquire(token):
self.local.token = token
return True
if not blocking:
return False
next_try_at = mod_time.time() + sleep
if stop_trying_at is not None and next_try_at > stop_trying_at:
return False
# 阻塞睡眠一段時間
mod_time.sleep(sleep)

簡單而言,這種方式就是在客戶端輪詢,未獲得鎖時,就等待一段時間再嘗試去獲得鎖,直到成功獲得鎖或等待超時,這種方式實現(xiàn)簡單,但當并發(fā)量比較大時,輪詢的方式會耗費比較多資源,影響服務器性能。

更好的一種方式是使用 Redis 發(fā)布訂閱功能,當線程 B 獲取鎖失敗時,訂閱鎖釋放的消息,當線程 A 執(zhí)行完業(yè)務釋放鎖時,會發(fā)送鎖釋放信息,線程 B 獲得信息后,再去獲取鎖,這樣就不需要一直輪詢了,而是直接休眠等待到鎖釋放消息則可。

Redis 集群主從切換

比較復雜的項目會使用多個 Redis 服務構建集群,Redis 集群采用主從方式部署,簡單而言,通過算法選擇出 Redis 集群中的主節(jié)點,所有寫操作都會落到主節(jié)點上,主節(jié)點會將指令記錄在 buffer 中,再通過異步的方式將 buffer 中的指令同步到其他從節(jié)點,從節(jié)點執(zhí)行相同的指令,便會獲得與主節(jié)點相同的數(shù)據(jù)結構。

當我們基于 Redis 集群來構建分布式鎖時,可能會出現(xiàn)主從切換導致鎖丟失的問題。

依舊以例子來說明,客戶端 A 通過 Redis 集群成功加鎖,這個操作首先會發(fā)生在主節(jié)點,但由于某些問題,當前 Redis 集群的主節(jié)點 down 了,此時根據(jù)相應的算法,Redis 集群會從從節(jié)點中選出新的主節(jié)點,這個過程對客戶端 A 而言是透明的,但如果在主從切換時,客戶端 A 在舊主節(jié)點加鎖的指令還未同步它就 down 了,那么新的主節(jié)點就不會有客戶端 A 加速的信息,此時,如果有新的客戶端 B 要加鎖,便可以輕松加上。

Redis 集群腦裂腦裂

這次確實挺抽象的,簡單而言,Redis 集群中因為網(wǎng)絡問題,某些從節(jié)點無法感知到主節(jié)點了,此時這些從節(jié)點會認為主節(jié)點 down 了,便會選出新的主節(jié)點,而客戶端卻可以連接上兩個主節(jié)點,從而會出現(xiàn)兩個客戶端擁有同一把鎖的情況。

結尾復雜分布式系統(tǒng)中鎖的問題一直是個設計難題,學無止境呀。

責任編輯:武曉燕 來源: 懶編程
相關推薦

2019-06-19 15:40:06

分布式鎖RedisJava

2021-06-03 00:02:43

RedisRedlock算法

2021-07-30 00:09:21

Redlock算法Redis

2017-10-24 11:28:23

Zookeeper分布式鎖架構

2023-09-22 08:00:00

分布式鎖Redis

2017-04-13 10:51:09

Consul分布式

2022-06-16 08:01:24

redis分布式鎖

2022-01-06 10:58:07

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

2023-08-21 19:10:34

Redis分布式

2019-02-26 09:51:52

分布式鎖RedisZookeeper

2022-10-27 10:44:14

分布式Zookeeper

2021-11-01 12:25:56

Redis分布式

2020-07-15 09:20:48

MyCatMySQL分布式

2022-03-08 15:24:23

BitMapRedis數(shù)據(jù)

2023-03-01 08:07:51

2022-09-19 08:17:09

Redis分布式

2020-11-16 12:55:41

Redis分布式鎖Zookeeper

2024-10-07 10:07:31

2021-06-10 06:57:39

Redis存儲數(shù)據(jù)庫

2019-07-16 09:22:10

RedisZookeeper分布式鎖
點贊
收藏

51CTO技術棧公眾號