使用Redis構建全域性併發鎖
談起Redis的用途,小夥伴們都會說使用它作為快取,目前很多公司都用Redis作為快取,但是使用Redis僅僅作為快取未免太大材小用了。深究Redis的原理後你會發現它有很多用途,在很多場景下能夠使用它快速地解決問題。常見的用途有:分散式鎖控制併發、結合bloom filter用於推薦去重、HyperLogLog用於統計UV、限流控制流量等等;這裡我談下Redis分散式鎖控制併發的問題。
高併發是個老生常談的問題,當產品達到一定規模使用者量後,這個問題是不得不考慮的,即使當前使用者量不大(例如博主現在的公司),但自己平時在設計API的時候最好也儘可能地考慮到併發問題。
Redis分散式鎖控制併發主要是通過在Redis裡面建立一個key,當其它程序準備佔用的時候只能等待key釋放再佔用。Redis裡面有一個原子性指令setnx,當key存在時,它返回0,表示當前已有程序佔用,當它返回1時可以執行業務邏輯,此時沒有程序佔用,等邏輯執行完後,可以刪除key釋放鎖,這樣可以簡單的控制併發。
但是細想之下你會發現,在業務邏輯執行的過程中如果發生異常,此時key並沒有刪除,這樣就會造成死鎖,死鎖帶來的後果想必大家都很清楚。為了解決這個問題,可以在setnx加鎖後設置key的過期時間,當key到期自動刪除。
但是仔細想想你還會發現,如果在執行setnx後,執行expire前Redis發生宕機了,這樣就不會執行expire,也會造成死鎖。由於setnx與expire是兩條命令,並且expire依賴setnx的執行結果,為了解決這個問題可以使用set key value [expiration EX seconds|PX milliseconds] [NX|XX] ,這是一條原子性的指令,同時包含setnx和expire。
使用python實現的程式碼:
1 class RedisLock(object): 2""" 3踩坑 Redis併發鎖 4""" 5 6def __init__(self, key): 7self.redis_conn = get_redis_conn() 8self.lock_key = "{}_redis_gil".format(key) 9 10@staticmethod 11def get_lock_value(cls): 12""" 13獲取value 14:param cls: 15:return: 16""" 17cls.get_lok = cls.redis_conn.get(cls.lock_key) 18return cls.get_lok 19 20@staticmethod 21def set_lock(cls, random_value): 22""" 23不能使用setnx沒有設定過期時間,可能會出現死鎖 24引入random_value :自己加的鎖只能自己釋放 25:param cls: 26:param random_value: 27:return: 28""" 29cls._lock = cls.redis_conn.set(cls.lock_key, random_value, nx=True, ex=5) 30 31# 如果返回null 表示key存在存在併發 32if cls._lock: 33return True 34else: 35LOGGER = logging.getLogger('core.utils') 36LOGGER.warning(u"試題複製存在併發") 37raise RsError("試題複製存在併發,請稍後再試") 38 39@staticmethod 40def release(cls): 41""" 42釋放鎖 43:param cls: 44:return: 45""" 46cls.redis_conn.delete(cls.lock_key) 47 48@staticmethod 49def redis_lock(cls): 50""" 51只有當設定的value與do_something執行完後所獲取的值相同時才刪除key 52防止在分散式redis中: clientA由於執行時間過期,clientB獲取鎖, 53clientA執行完後釋放鎖(刪除key),其實這時候刪除的是B的key, 54為防止這種情況引入random_value 只有當前值為random_value時才刪除 55:param cls: 56:return: 57""" 58random_value = time.time() 59if cls.set_lock(cls, random_value): 60do_something() 61now_value = cls.get_lock_value(cls) 62if now_value == random_value: 63cls.release() 64return True 65else: 66return False 67 68 69 def do_something(): 70pass
在實際業務中呼叫Redis全域性鎖,進行加鎖示例:
1 # 公庫試題複製到平臺考慮併發問題,加鎖處理 2 if self.visible_scope == 10: 3key = hash(self.question_id) 4cls = RedisLock(key) 5cls.redis_lock(cls) 6try: 7self.insert_question() 8except Exception: 9raise RsError("試題插入失敗") 10finally: 11cls.release(cls)
如果是Redis叢集下此方法可能仍然有問題,試想下:在一個redis叢集中,主節點由於某種原因掛掉了,從節點變成了主節點,而此時redis鎖還未同步到原從節點中,那麼這個鎖也就失效了,當其它程序申請鎖時仍然可以申請成功。
針對這個問題,新版的redis引入了redlock,通過redlock.Redlock對多個redis節點進行加鎖,當超過一半的節點加鎖成功時鎖才生效。這樣在一定程度上提高了高可用性,但由於每次加鎖和釋放鎖要對多個節點進行讀寫,所以效能上肯定是沒有單節點鎖高的。