成人免费xxxxx在线视频软件_久久精品久久久_亚洲国产精品久久久_天天色天天色_亚洲人成一区_欧美一级欧美三级在线观看

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

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

簡介

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

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

基于 SETNX 命令實現分布式鎖

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

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

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

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

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

# 獲得分布式鎖
def do_acquire(self, token):
# 利用SETNX實現分布式鎖
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實現鎖的釋放
pipe.delete(name)

self.redis.transaction(execute_release, name)

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

SETNX 與 EXPIRE 非原子性問題

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

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

Redis 官方考慮到了這種情況,讓 SET 命令可以直接設置 Timeout 并實現 SETNX 效果,SET 支持的語法變為:SETEX key value NX timeout,這樣就不再需要通過 EXPIRE 設置超時時間,從而實現原子性了。

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

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

鎖誤解除

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

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

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

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 來實現這個過程,相關代碼如下:

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來釋放鎖,并實現判斷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 是否相同,不相同的話,便不會執行 DEL 命令,即不會釋放鎖。

鎖超時導致的并發

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

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

1. 增大鎖的過期時間,讓業務邏輯有充足的執行時間;

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

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

輪詢等待的效率問題

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

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

# 死循環等待獲得鎖
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)

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

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

Redis 集群主從切換

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

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

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

Redis 集群腦裂腦裂

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

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

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

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數據分布式鎖

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數據

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存儲數據庫

2019-07-16 09:22:10

RedisZookeeper分布式鎖
點贊
收藏

51CTO技術棧公眾號

主站蜘蛛池模板: 黑人中文字幕一区二区三区 | 久久大陆| 亚洲成人精品在线 | 国产精品特级毛片一区二区三区 | 日本三级黄视频 | 亚洲国产欧美国产综合一区 | 亚洲视频在线观看 | 黄色国产视频 | 狠狠插狠狠操 | 欧美成人高清视频 | 色综合欧美 | 亚洲精品福利在线 | 毛片网站在线观看视频 | 特黄毛片视频 | 精品av | 国产精品99999 | 成人免费黄色 | 天天操网| 欧美在线色 | 欧美日本高清 | 国产精品久久久久久久久久东京 | 久久i | 麻豆av电影网 | 在线免费观看黄色 | 九九热在线视频 | 中文字幕一区二区三区四区 | www.天天操.com | 亚洲精品在线免费播放 | 亚洲天堂久久新 | 国产免费一区二区 | 国产精品成av人在线视午夜片 | 欧美精品一区二区在线观看 | 亚洲精品一区二区三区中文字幕 | 日韩久久久久久久久久久 | 亚洲精品在线免费 | 免费成人高清在线视频 | caoporn国产精品免费公开 | 精品国产乱码久久久久久中文 | 北条麻妃一区二区三区在线观看 | 特级做a爰片毛片免费看108 | 欧美国产精品一区二区三区 |