談起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節點進行加鎖,當超過一半的節點加鎖成功時鎖才生效。這樣在一定程度上提高了高可用性,但由於每次加鎖和釋放鎖要對多個節點進行讀寫,所以性能上肯定是沒有單節點鎖高的。