当前位置:首页 > 问答 > 正文

用Redis搞个锁,简单说说怎么实现和用法示例分享

用Redis搞个锁,说白了就是利用Redis的一个特性:因为它是个单线程的程序,所以它能保证同一个时刻只有一个命令能被执行,我们就是利用这个特性,让多个程序或者多个线程在抢一个“钥匙”,谁先把这个钥匙设置到Redis里,谁就抢到了锁,就可以去执行那些不能同时多人干的活儿了。

最基本的实现思路

最直接的想法是这样的:当一个程序(我们叫它客户端A)需要锁的时候,它就在Redis里创建一个特定的键值对,比如键叫 my_lock,值可以随便设一个能唯一标识这个客户端的值(比如用UUID),创建的时候,我们给这个键设置一个过期时间,比如10秒钟。

为什么一定要设置过期时间?这是为了防止死锁,万一客户端A抢到锁之后,因为网络问题或者自己崩溃了,没有来得及释放锁,那么这个锁就会永远留在Redis里,其他客户端就永远拿不到锁了,整个系统就卡死了,设置了过期时间,哪怕客户端A出了意外,这个锁也会在10秒后自动消失,其他客户端就能继续竞争了。

当客户端A做完事情后,它需要释放锁,释放锁就是去Redis里把那个 my_lock 键删掉,但是这里有个关键点:只能删自己设置的锁,为什么?想象一下这个场景:

用Redis搞个锁,简单说说怎么实现和用法示例分享

  1. 客户端A抢到锁,设置锁的过期时间是10秒。
  2. 客户端A的业务操作比较慢,干了15秒,这时候锁因为过期已经自动消失了。
  3. 客户端B趁虚而入,成功设置了 my_lock,抢到了锁。
  4. 客户端A终于干完活了,它就去执行删除锁的操作。
  5. 如果客户端A不管三七二十一直接删,它就会把客户端B刚创建的锁给删掉!这就乱套了。

正确的做法是,在删除之前,要先检查一下这个锁的值是不是还是自己当初设置的那个UUID,如果是,才能删;如果不是,说明锁已经属于别人了,就不能删。

一个简单的用法示例(使用Python语言和redis-py库)

下面我用伪代码混合Python代码的逻辑来演示一下这个过程,这样更容易理解。

用Redis搞个锁,简单说说怎么实现和用法示例分享

import redis
import uuid
# 连接到Redis服务器
redis_client = redis.Redis(host='localhost', port=6379, db=0)
# 定义一个唯一的锁键名
lock_key = "order_lock"
# 生成一个唯一标识符,代表当前客户端
client_identifier = str(uuid.uuid4())
# 定义锁的过期时间,单位是秒
lock_timeout = 10
# 尝试获取锁的函数
def acquire_lock():
    # 使用SET命令,并加上NX和PX参数。
    # NX表示“只有当这个key不存在时才设置”,这保证了只有一个客户端能设置成功。
    # PX表示过期时间,单位是毫秒,这里我们设置10000毫秒(10秒)。
    result = redis_client.set(lock_key, client_identifier, nx=True, px=lock_timeout * 1000)
    # 如果设置成功,result为True,表示获取到了锁;失败则为False。
    return result is True
# 释放锁的函数
def release_lock():
    # 使用Lua脚本保证原子性操作
    lua_script = """
    if redis.call("get", KEYS[1]) == ARGV[1] then
        return redis.call("del", KEYS[1])
    else
        return 0
    end
    """
    # 执行Lua脚本:传入锁的键名和我们自己的标识符
    result = redis_client.eval(lua_script, 1, lock_key, client_identifier)
    # 如果删除成功,返回1;如果锁已经不是自己的了,返回0。
    return result == 1
# 业务代码的使用方式
if acquire_lock():
    print("成功抢到锁,开始处理关键任务...")
    try:
        # 这里写你的业务逻辑,比如扣减库存、生成订单等。
        # ... 执行一些需要互斥访问的操作 ...
        print("任务处理中...")
        # 模拟一个耗时操作,注意这个时间最好不要超过锁的过期时间
        # time.sleep(5)
    finally:
        # 无论业务逻辑是否出现异常,最终都要尝试释放锁
        release_success = release_lock()
        if release_success:
            print("任务完成,锁已释放。")
        else:
            # 这里可能意味着锁已经过期并被其他客户端获取,日志记录一下就好,一般不需要特殊处理
            print("锁释放失败(可能已超时自动释放)。")
else:
    print("没能抢到锁,可能是其他程序正在处理,稍后再试。")

关于上面代码的一些重要解释

  1. 原子性:在获取锁的时候,我们使用了 set key value NX PX timeout 这个命令,这个命令是原子性的,意思是“判断是否存在”和“设置值并设置超时”这两个动作是一气呵成的,中间不会被打断,这非常重要,如果分成两个命令(先判断exists,再setex),就可能出现多个客户端同时判断为“不存在”,然后都去设置,导致锁被多个客户端同时获得。
  2. 释放锁的原子性:释放锁时,我们使用了Lua脚本,因为判断值是否相等和删除键是两个操作,如果不放在一个原子操作里,也可能出问题,比如客户端A判断值相等,正准备删除时,锁过期了,客户端B设置了新锁,然后客户端A的删除命令才执行,就又误删了客户端B的锁,使用Lua脚本可以确保Redis会连续执行脚本里的所有命令,中间不会插入其他客户端的命令。
  3. 过期时间设置:设置一个合理的过期时间是个经验活,太短了,业务没处理完锁就没了,可能导致多个客户端同时进入临界区;太长了,万一客户端挂掉,其他客户端要等待很久才能继续,有时候可能需要设计一个“看门狗”机制,在业务处理期间不断延长锁的过期时间。

这种简单锁的局限性

这种用Redis实现的锁,通常被称为“互斥锁”或者“分布式锁”的简易版,它在很多场景下够用,但并不完美,比如它存在一个问题:如果Redis采用主从复制模式,客户端在主节点上设置了锁,但还没来得及同步到从节点,主节点就宕机了,之后从节点升级为主节点,但新的主节点上没有这个锁,其他客户端就可以再次获取到锁,导致锁失效,对于要求极端严格的场景,可能需要更复杂的算法(比如Redlock),但对于一般的防止重复提交、缓解并发冲突等场景,上面这种简单实现已经非常有用了。

用Redis搞个锁的核心就是“占坑+过期时间+安全释放”,理解了这三点,你就掌握了最基本也是最实用的方法。