背景:
Redis的快取資料庫是為快速響應用戶端減輕資料庫壓力的有效手段之一,其中有一種功能是失效緩衝,其優點是可以不週期性釋放使用頻率低的業務空間而增加有限的記憶體,但對於同步資料庫和緩衝之間的資料來說需要面臨一個問題就是:在並發量比較大的情況下當一個快取資料失效之後會導致同時有多個並發線程去向後端資料庫發起請求去擷取同一業務資料(每次緩衝失效的時候,我們理想的話,是有1個線程去資料庫取資料,然後把這1份資料寫入redis中就可以,但是在高並發的環境下,可能會有100個線程去資料庫擷取資料,然後也會把這100份資料寫到redis中,導致資料庫壓力大,有大量的緩衝失效。),這樣如果在一段時間內同時產生了大量的緩衝,然後在另外一段時間內又有大量的緩衝失效,這樣就會導致後端資料庫的壓力陡增,這種現象就可以稱為“緩衝到期產生的驚群現象”。
處理思路:
緩衝內真實失效時間time1
緩衝value中存放人為失效時間戳記 :time2 ( time2 永遠小於time1)
緩衝value對應的lock鎖(就是一個與value 對應的 另一個key),主要用於判斷是第幾個線程來讀取redis的value
當把資料庫的資料寫入緩衝後,這時有用戶端第一次來讀取緩衝,取當前系統時間:system_time 如果system_time >= time2 則認為預設緩衝已到期(如果system_time< time1 則還沒真實失效 ),這時再擷取value的lock鎖,調用redis的incr函數(單線程自增函數)判斷是第幾個擷取鎖的線程,若且唯若是第一個線程時返回1,以後都逐漸遞增。第一個訪問的線程到資料庫中擷取最新值重新放入緩衝並刪除lock鎖的key,並重新設定時間戳記;在刪除lock之前所有訪問value用戶端線程擷取lock的value都大於1,這些線程仍然讀取redis中的舊值,而不會集中訪問資料庫。
解決問題python代碼:
import jsonimport pickleimport redisimport timeimport mathclass RedisApi(redis.Redis): def get_json(self, name): """ 如果和老的api設定的值可以用這個方法取 :param name: :return: """ value = self.get(name) if value is None: return None try: return json.loads(value) except Exception: if value.startswith(b'!'): try: return json.loads(pickle.loads(value[1:])) except pickle.PickleError: return None else: return json.loads(value.decode().replace("'", '"')) def get_bylock(self, key): """ 避免redis逾時時的驚群現象,請必須配合 `set_bylock` 使用 調用方法與`get`一樣 """ lock_key = key + ".lock" data = self.get(key) current = int(time.time()) if not data: return None else: real_data = json.loads(data) # 如果人為設定的逾時時間逾時了 if real_data['expireat'] <= current: # 如果擷取到鎖 if self.set(lock_key, "x", ex=2, nx=True): return None # 如果沒擷取到鎖 else: return real_data['data'] else: return real_data['data'] def set_bylock(self, key, data, expire_time): """ 避免redis逾時時的驚群現象,請必須配合`get_bylock`使用 調用方法與`setex`一樣 """ current = int(time.time()) real_data = {'data': data, 'expireat': current + expire_time - math.ceil(expire_time / 2)} self.setex(key, json.dumps(real_data), expire_time)
RedisAPI類繼承了redis.Redis類,用法盒Redis類一樣,只是使用者在使用.get()和.setex()的方法時,對應的替換成.get_bylock()和.set_bylock()方法