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中的旧值,而不会集中访问数据库。
import json
import pickle
import redis
import time
import math
class 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()方法