最近在研究缓存击穿
缓存击穿是指缓存中没有但数据库中有的数据(一般是缓存时间到期),这时由于并发用户特别多,
同时读缓存没读到数据,又同时去数据库去取数据,引起数据库压力瞬间增大,造成过大压力
缓存击穿的一个解决方案就是加互斥锁
1)缓存中有数据,取出缓存
2)缓存中没有数据,第1个进入的线程,获取锁并从数据库去取数据
,没释放锁之前,其他并行进入的线程会等待100ms,
再重新去缓存取数据。这样就防止都去数据库重复取数据,
重复往缓存中更新数据情况出现。
3)当然这是简化处理,理论上如果能根据key值加锁就更好了,
就是线程A从数据库取key1的数据并不妨碍线程B取key2的数据,
上面代码明显做不到这点。
而正好我需要实现一个基于key的锁
本身想用Python实现,但是Python自带的Lock不太适合
而利用Redis设计一个带key的锁
- pyRedisLock 1.0版本
- pyRedisLock 2.0版本
- redlock-py介绍以及原理解析
pyRedisLock 1.0版本
Redis是单线程
redis的setnx也是原子操作
并发问题redis帮我们考虑好了
我们只需要设计业务逻辑就好
本身就用了Redis,所以不会给项目带来额外的复杂架构
大部分原理解析在代码注释中
import redis
cache = redis.Redis(
host="127.0.0.1",
port=6379,
db=1,
password=""
)
class RedisLock:
def __init__(self, name):
self.name = f"lock_{name}"
def lock(self, expire):
# 加锁
# setnx指的是,如果key不存在,设置key,如果key存在,返回None
# 本身是使用setnx + expire命令来完成加锁逻辑
# 但这种方式如果expire请求挂了
# Lock会一直不释放
# Redis在2.6.12版本为SET命令增加了一系列选项
# 可以在setnx的同时设置过期时间
# http://www.redis.cn/commands/set.html
result = cache.set(self.name, "", nx=True, ex=expire)
return result if result is True else False
def release(self):
# 释放锁
return cache.delete(self.name)
def expire(self, time):
# 增加锁的自动释放时间
cache.expire(self.name, time)
@property
def lock_status(self):
# 获取锁资源是否被获取,锁资源被获取的话会返回True,未被获取会返回False,
result = cache.get(self.name)
return False if result is None else True
第一个版本得 未解决问题
一、高并发下会出现如下问题
- 客户端A 加锁(set)
- 客户端A 解锁 ,网络阻塞,解锁(expire)命令没到redis的时候锁过期
- 客户端B 加锁(set)
- 客户端A 的解锁(expire)命令到达redis,删除掉客户端B 的锁
- 但客户端B会认为自己持有锁,并去执行接下来的业务逻辑
二、redis挂了锁也挂了
pyRedisLock 2.0版本
具体逻辑在代码注释中
import redis
import time
r = redis.Redis(
host="127.0.0.1",
port=6379,
db=1,
password=""
)
class RedisLock:
_private_key = "_redis_lock_"
def __init__(self):
pass
def _lock_key(self, key):
return self._private_key + key
def lock(self, key, expire):
# 加锁
# 使用UUID作为VALUE得话 10000次需要0.204秒,
# 使用 "key名称" + "timestamp"的话10000次只需要0.12秒
# 与上面逻辑类似,但存储value供解锁时使用
real_key = self._lock_key(key)
value = real_key + str(time.time())
result = r.set(real_key, value, nx=True, ex=expire)
return (True, value) if result is True else (False, "")
def release(self, key, value):
# 释放锁
# 释放锁的时候使用value解锁
# 执行LUA脚本
# 如果key存在且redis内部存的value等于解锁操作客户端的value
# 执行删除键操作
# 这样可以避免因为网络阻塞与键过期导致的删除其他客户端的锁的问题
lua_script = f"""
if redis.call("get",KEYS[1]) == ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0
end
"""
real_key = self._lock_key(key)
return r.eval(lua_script, 1, real_key, value)
def expire(self, key, expire):
# 增加锁的自动释放时间
real_key = self._lock_key(key)
r.expire(real_key, expire)
def lock_status(self, key):
# 获取锁资源是否被获取
real_key = self._lock_key(key)
result = r.get(real_key)
return False if result is None else True
redis_lock = RedisLock()
第二个版本得 未解决问题
- Redis挂了,无法提供锁服务
redlock-py介绍以及原理解析
基于第二个版本未解决的问题,我发现了开源代码redlock-py
背景
redlock同时维护与N个redis节点的连接,防止因为Redis服务挂掉导致无法提供锁服务
源码
https://github.com/SPSCommerce/redlock-py
Redis官方文档分布式锁实现
https://redis.io/topics/distlock
简要逻辑:
获取锁时只要在N/2+1个redis服务器上获取成功则代表加锁成功, 释放锁时需要在所有客户端上释放锁
代码加锁逻辑
- ttl指的是我们设置的锁过期时间,单位毫秒
- 获取当前时间(毫秒)(start_time )
- 向N个节点获取锁,如果失败则跳过,并计算获取锁消耗的时间(elapsed_time )
- 当在N/2+1个redis服务器上获取成功,并且锁的过期时间(ttl)大于(加锁用时(elapsed_time ) + 到期精度时间差值(drift)),则加锁成功 *到期精度时间差值(dirft)等于 (ttl * 0.01 + 2) 毫秒,这个是因为redis的key到期精度问题,原文如下 (Add 2 milliseconds to the drift to account for Redis expires precision, which is 1 millisecond, plus 1 millisecond min drift for small TTLs.)
- 如果锁获取成功了,则有效期重新计算后返回加锁成功以及新的锁有效时间
- 如果锁获取失败了,则在所有redis服务器上进行解锁操作
即便如此,锁还是有局限性,下面两篇文章讲述了redlock-py的局限性,不在重复讲解局限性,文章很有意思
两篇redlock-py的局限性的文章
https://mp.weixin.qq.com/s/JTsJCDuasgIJ0j95K8Ay8w
https://mp.weixin.qq.com/s/4CUe7OpM6y1kQRK8TOC_qQ?