一、锁的分类
锁在理论上大的分类可以分为以下几种(参考这篇文章)
- 共享锁(S锁)
- 排它锁(X锁)
- 互斥锁
- 悲观锁
- 乐观锁
- 行级锁、表级锁、页级锁
二、锁的特性
- 互斥性。锁的最基本属性,任意时刻只有一个客户端能持有锁。
- 不会发生死锁。即使有一个客户端在持有锁的期间内崩溃了,也要保证其它客户端能加锁。
- 具有容错性。只要大部分redis节点能正常运行,客户端就能加锁和解锁。
- 解铃还须系铃人。加锁和解锁必须同一个客户端,客户端不能解别人的锁。
- 锁不能自己失效。正常执行过程中,锁不能因为某些原因失效。
以上5条1、2、4相对容易做到,3和5相对有难度。参考B站链接
三、基于Redis的锁的实现
实现锁的思路,一般是基于redis的
INCR
或SETNX
来实现的(从 Redis 2.6.12 版本开始, SET 命令的行为可以通过一系列参数来修改:)
SET key value [EX seconds] [PX milliseconds] [NX|XX]
- EX second :设置键的过期时间为 second 秒。 SET key value EX second 效果等同于 SETEX key second value 。
- PX millisecond :设置键的过期时间为 millisecond 毫秒。 SET key value PX millisecond 效果等同于 PSETEX key millisecond value 。
- NX :只在键不存在时,才对键进行设置操作。 SET key value NX 效果等同于 SETNX key value 。
- XX :只在键已经存在时,才对键进行设置操作。
目前我们先实现锁的特性中的1、2、4。
- 特性1:不用说了,这是锁最基本的特性
- 特性2:可以通过设置超时时间来做到,但是单纯的设置超时时间又会与第5条冲突。这个放到后面讨论,目前的实现方案还是设置超时时间。
- 特性4:这个可以通过在加锁时设置一个随机uuid作为key的value来实现,在解锁必须要求uuid对得上才有权限解锁。
以下代码参考知乎
def LOCK(conn, lock_name, acquire_timeout=3, lock_timeout=2):
"""
基于 Redis 实现的分布式锁
:param conn: Redis 连接
:param lock_name: 锁的名称
:param acquire_timeout: 获取锁的超时时间,默认 3 秒
:param lock_timeout: 锁的超时时间,默认 2 秒
:return:
"""
identifier = str(uuid.uuid4())
lockname = f'lock:{lock_name}'
lock_timeout = int(math.ceil(lock_timeout))
end = time.time() + acquire_timeout
while time.time() < end:
# 如果不存在这个锁则加锁并设置过期时间,避免死锁
if conn.set(lockname, identifier, ex=lock_timeout, nx=True):
return identifier
time.sleep(0.001)
return False
def UNLOCK(conn, lock_name, identifier):
"""
释放锁
:param conn: Redis 连接
:param lockname: 锁的名称
:param identifier: 锁的标识
:return:
"""
unlock_script = """
if redis.call("get",KEYS[1]) == ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0
end
"""
lockname = f'lock:{lock_name}'
unlock = conn.register_script(unlock_script)
result = unlock(keys=[lockname], args=[identifier])
if result:
return True
else:
return False
注意
redis-py
本身是支持lock的,官方github上有一个示例
注意到这个例子隐藏了随机生成的uuid作为标识;
建议还是使用官网提供的加锁方案
自己写的版本可以有助于加深对锁的理解。
try:
with r.lock('my-lock-key', blocking_timeout=5) as lock:
# code you want executed only after the lock has been acquired
except LockError:
# the lock wasn't acquired
最后我们说一下锁的特性3和特性5
- 特性3主要是针对redis集群的,单机讨论没有意义,这里给出一个名词叫红锁和简单的解释:
redis常用的方式有单节点、主从模式、哨兵模式、集群模式。
单节点在生产环境基本上不会使用,因为不能达到高可用,且连RDB或AOF备份都只能放在master上,所以基本上不会使用。
另外几种模式都无法避免两个问题:异步数据丢失、裂问题。
所以redis官方针对这种情况提出了红锁(Redlock)的概念。
假设有5个redis节点,这些节点之间既没有主从,也没有集群关系。客户端用相同的key和随机值在5个节点上请求锁,请求锁的超时时间应小于锁自动释放时间。当在3个(超过半数)redis上请求到锁的时候,才算是真正获取到了锁。如果没有获取到锁,则把部分已锁的redis释放掉。具体参考官方地址
- 特性5的实现可以通过动态调整超时时间来解决(这只是个思路个人理解);还有若没有指定超时时间,必须要有一个默认的超时时间,避免死锁。