使用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 
 6     def __init__(self, key):  7         self.redis_conn = get_redis_conn()  8         self.lock_key = "{}_redis_gil".format(key)  9 
10  @staticmethod 11     def get_lock_value(cls): 12         """
13  獲取value 14  :param cls: 15  :return: 16         """
17         cls.get_lok = cls.redis_conn.get(cls.lock_key) 18         return cls.get_lok 19 
20  @staticmethod 21     def set_lock(cls, random_value): 22         """
23  不能使用setnx 沒有設置過期時間,可能會出現死鎖 24  引入random_value :自己加的鎖只能自己釋放 25  :param cls: 26  :param random_value: 27  :return: 28         """
29         cls._lock = cls.redis_conn.set(cls.lock_key, random_value, nx=True, ex=5) 30 
31         # 如果返回null 表示key存在存在並發
32         if cls._lock: 33             return True 34         else: 35             LOGGER = logging.getLogger('core.utils') 36             LOGGER.warning(u"試題復制存在並發") 37             raise RsError("試題復制存在並發,請稍后再試") 38 
39  @staticmethod 40     def release(cls): 41         """
42  釋放鎖 43  :param cls: 44  :return: 45         """
46  cls.redis_conn.delete(cls.lock_key) 47 
48  @staticmethod 49     def redis_lock(cls): 50         """
51  只有當設置的value與do_something執行完后所獲取的值相同時才刪除key 52  防止在分布式中: clientA由於執行時間過期(clientA的執行時間比設置的過期時間大),clientB獲取鎖, 53  clientA執行完后釋放鎖(刪除key),其實這時候刪除的是B的key, 54  為防止這種情況引入random_value 只有當前值為random_value時才刪除 55  :param cls: 56  :return: 57         """
58         random_value = time.time() 59         if cls.set_lock(cls, random_value): 60  do_something() 61             now_value = cls.get_lock_value(cls) 62             if now_value == random_value: 63  cls.release() 64                 return True 65         else: 66             return False 67 
68 
69 def do_something(): 70     pass

 

 在實際業務中調用Redis全局鎖,進行加鎖示例:

 

 1 # 公庫試題復制到平台考慮並發問題,加鎖處理
 2 if self.visible_scope == 10:  3     key = hash(self.question_id)  4     cls = RedisLock(key)  5  cls.redis_lock(cls)  6     try:  7  self.insert_question()  8     except Exception:  9         raise RsError("試題插入失敗") 10     finally: 11         cls.release(cls)

 

如果是Redis集群下此方法可能仍然有問題,試想下:在一個redis集群中,主節點由於某種原因掛掉了,從節點變成了主節點,而此時redis鎖還未同步到原從節點中,那么這個鎖也就失效了,當其它進程申請鎖時仍然可以申請成功。

針對這個問題,新版的redis引入了redlock,通過redlock.Redlock對多個redis節點進行加鎖,當超過一半的節點加鎖成功時鎖才生效。這樣在一定程度上提高了高可用性,但由於每次加鎖和釋放鎖要對多個節點進行讀寫,所以性能上肯定是沒有單節點鎖高的。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM