锁的理论与实现(基于Python和Redis)

一、锁的分类

锁在理论上大的分类可以分为以下几种(参考这篇文章

  1. 共享锁(S锁)
  2. 排它锁(X锁)
  3. 互斥锁
  4. 悲观锁
  5. 乐观锁
  6. 行级锁、表级锁、页级锁

二、锁的特性

  1. 互斥性。锁的最基本属性,任意时刻只有一个客户端能持有锁。
  2. 不会发生死锁。即使有一个客户端在持有锁的期间内崩溃了,也要保证其它客户端能加锁。
  3. 具有容错性。只要大部分redis节点能正常运行,客户端就能加锁和解锁。
  4. 解铃还须系铃人。加锁和解锁必须同一个客户端,客户端不能解别人的锁。
  5. 锁不能自己失效。正常执行过程中,锁不能因为某些原因失效。

以上5条1、2、4相对容易做到,3和5相对有难度。参考B站链接


三、基于Redis的锁的实现

实现锁的思路,一般是基于redis的 INCRSETNX 来实现的(从 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的实现可以通过动态调整超时时间来解决(这只是个思路个人理解);还有若没有指定超时时间,必须要有一个默认的超时时间,避免死锁。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值