激情久久久_欧美视频区_成人av免费_不卡视频一二三区_欧美精品在欧美一区二区少妇_欧美一区二区三区的

服務器之家:專注于服務器技術及軟件下載分享
分類導航

Mysql|Sql Server|Oracle|Redis|MongoDB|PostgreSQL|Sqlite|DB2|mariadb|Access|數據庫技術|

服務器之家 - 數據庫 - Redis - 基于 Redis 構建簡單分布式鎖的局限

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

2022-03-08 22:44懶編程ayuliao Redis

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

簡介

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

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

基于 SETNX 命令實現分布式鎖

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

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

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 的鎖,直觀如下圖所示:

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

解決方法便是添加唯一標識,在釋放鎖時,校驗 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 是在業務上是有順序依賴的,此時出現了并發情況,便會導致業務結果的錯誤,直觀如下圖:

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

線程 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 了,便會選出新的主節點,而客戶端卻可以連接上兩個主節點,從而會出現兩個客戶端擁有同一把鎖的情況。

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

原文地址:https://mp.weixin.qq.com/s/Z7RAImH2xW60xdiWnIYCyQ

延伸 · 閱讀

精彩推薦
  • RedisRedis全量復制與部分復制示例詳解

    Redis全量復制與部分復制示例詳解

    這篇文章主要給大家介紹了關于Redis全量復制與部分復制的相關資料,文中通過示例代碼介紹的非常詳細,對大家學習或者使用Redis爬蟲具有一定的參考學習...

    豆子先生5052019-11-27
  • RedisRedis 事務知識點相關總結

    Redis 事務知識點相關總結

    這篇文章主要介紹了Redis 事務相關總結,幫助大家更好的理解和學習使用Redis,感興趣的朋友可以了解下...

    AsiaYe8232021-07-28
  • Redisredis實現排行榜功能

    redis實現排行榜功能

    排行榜在很多地方都能使用到,redis的zset可以很方便地用來實現排行榜功能,本文就來簡單的介紹一下如何使用,具有一定的參考價值,感興趣的小伙伴們...

    乘月歸5022021-08-05
  • RedisRedis的配置、啟動、操作和關閉方法

    Redis的配置、啟動、操作和關閉方法

    今天小編就為大家分享一篇Redis的配置、啟動、操作和關閉方法,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧 ...

    大道化簡5312019-11-14
  • Redisredis 交集、并集、差集的具體使用

    redis 交集、并集、差集的具體使用

    這篇文章主要介紹了redis 交集、并集、差集的具體使用,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友...

    xiaojin21cen10152021-07-27
  • Redisredis中如何使用lua腳本讓你的靈活性提高5個逼格詳解

    redis中如何使用lua腳本讓你的靈活性提高5個逼格詳解

    這篇文章主要給大家介紹了關于redis中如何使用lua腳本讓你的靈活性提高5個逼格的相關資料,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具...

    一線碼農5812019-11-18
  • RedisRedis如何實現數據庫讀寫分離詳解

    Redis如何實現數據庫讀寫分離詳解

    Redis的主從架構,能幫助我們實現讀多,寫少的情況,下面這篇文章主要給大家介紹了關于Redis如何實現數據庫讀寫分離的相關資料,文中通過示例代碼介紹...

    羅兵漂流記6092019-11-11
  • Redis詳解Redis復制原理

    詳解Redis復制原理

    與大多數db一樣,Redis也提供了復制機制,以滿足故障恢復和負載均衡等需求。復制也是Redis高可用的基礎,哨兵和集群都是建立在復制基礎上實現高可用的...

    李留廣10222021-08-09
主站蜘蛛池模板: 国产一区视频在线免费观看 | 伊人午夜视频 | 亚洲男人的天堂在线视频 | 欧美一级鲁丝片免费看 | 亚洲精品无码不卡在线播放he | 色人阁导航 | 国产精品欧美久久久久一区二区 | 一级影片在线观看 | 18一20岁一级毛片 | 成人三级视频网站 | 在线成人精品视频 | 免费高清一级欧美片在线观看 | 日韩黄色免费电影 | 国产亚洲美女精品久久久2020 | 黄色免费入口 | 成人免费在线网 | 九九热免费精品 | 成人福利在线看 | 欧美日韩亚洲国产精品 | 国产第一页精品 | 午夜精品久久久久久久96蜜桃 | 日本黄色一级电影 | 精品人成 | 国产在线精品一区二区不卡 | 国产一区二区三区四区五区精品 | 亚洲一区二区三区高清 | 国产成年人视频 | 色婷婷久久一区二区 | 日韩精品免费看 | 精品国产一区在线 | 91av在线免费| 一级成人毛片 | 婷婷一区二区三区四区 | 欧美一级毛片免费观看视频 | 在线观看中文字幕国产 | 国产亚洲精品久久久久久久久久 | 午夜免费一区 | 国产日韩免费观看 | 精品一区二区久久久久 | 国产精品视频中文字幕 | 免费观看一级欧美大 |