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

用Redis做红色围栏,怎么保护资源不被抢占,有点像栅栏的感觉

要实现一个像栅栏一样保护资源不被抢占的机制,用Redis来做是一个非常直接和高效的选择,这个机制的核心思想是,当一个任务(比如一个后台作业、一次数据处理请求或者一个抢购活动)需要独占某个资源时,它必须先成功地“放下”一个栅栏,表明“这个资源我现在占用了,在我松开之前,别人不能动”,如果放栅栏失败了(因为栅栏已经存在),那就说明资源已经被别人占用了,后来的任务就必须等待或者直接放弃。

这种模式在分布式系统中非常常见,Redis凭借其单线程执行命令和丰富数据结构的特性,能很好地胜任这个角色,下面详细说明几种用Redis实现这种“红色围栏”的具体方法。

第一种最基础也最常用的方法是使用SET命令的NX和PX选项。 这个方法来源于Redis官方文档中对分布式锁的实现建议,它的做法是这样的:我们把这个“栅栏”看作是一把锁,而锁的本质就是在Redis里创建一个有生存时间的键(Key)。

具体操作是:当一个任务想要获取资源时,它向Redis执行一个命令:SET resource_lock_name “一个随机值” NX PX 30000,我们来拆解这个命令:

  • resource_lock_name:这是锁的名字,也代表了被保护的资源,比如保护商品库存,可以叫stock_product_123
  • “一个随机值”:这很重要,不能是一个固定的值,通常用UUID或者随机生成的字符串,这是为了安全地释放锁,后面会讲到。
  • NX:意思是“Only set the key if it does not already exist”,这就是“栅栏”的精髓,只有当这个锁不存在时,SET操作才会成功,Redis会返回OK,表示你成功放下了栅栏,占用了资源,如果锁已经存在,SET操作会失败,返回nil,表示资源已被抢占。
  • PX 30000:这是给这个键设置一个过期时间,比如30000毫秒(30秒),这是为了防止死锁,万一那个成功放下栅栏的任务因为某种原因(比如机器宕机)没能主动移除栅栏,这个键也会在30秒后自动消失,让其他任务有机会再次尝试获取资源,避免了资源被永远锁死。

当任务处理完资源后,它需要主动“抬起”栅栏,也就是删除这个键,但删除不能简单地用DEL resource_lock_name,因为有可能因为任务处理超时,锁已经自动过期了,并且被另一个任务获取了,如果你这时删掉的,就是别人刚设置的锁,会造成混乱,所以安全的做法是,先用GET命令读取锁的值,确认这个值和自己当初设置的那个随机值相等,然后再删除,这个过程需要用Lua脚本来保证原子性,因为GET和DEL是两个操作,不能分开执行,一个典型的Lua脚本是这样的:

用Redis做红色围栏,怎么保护资源不被抢占,有点像栅栏的感觉

if redis.call("get",KEYS[1]) == ARGV[1] then
    return redis.call("del",KEYS[1])
else
    return 0
end

这样,只有锁的持有者才能安全地释放锁。

第二种方法是使用Redisson等客户端库。 如果你使用的是Java语言,那么Redisson这个Redis客户端库提供了现成的、非常完善的分布式锁实现,这种方法的思想来源是Java界的标准实践,你不需要自己去拼接SET NX PX命令和编写Lua脚本,Redisson已经帮你封装好了所有细节,并且解决了诸如锁续期(看门狗机制)、可重入性(同一个线程可以多次获取同一把锁)等更复杂的问题。

使用起来非常简单:

用Redis做红色围栏,怎么保护资源不被抢占,有点像栅栏的感觉

RLock lock = redisson.getLock("resource_lock_name");
// 尝试加锁,最多等待100秒,上锁以后10秒自动解锁
boolean isLocked = lock.tryLock(100, 10, TimeUnit.SECONDS);
if (isLocked) {
    try {
        // 成功获取到锁,处理受保护的资源
        ... 
    } finally {
        // 最终无论如何都要释放锁
        lock.unlock();
    }
}

Redisson的内部也是基于Redis命令实现的,但它做了大量优化和可靠性保障,对于大多数应用场景来说,是更省心、更可靠的选择。

第三种方法,针对更简单的协调场景,可以使用Redis的键过期(EXPIRE)和存在性检查(EXISTS)。 这种方法适用于对一致性要求不是极端高,但需要简单实现“冷却”或“全局开关”效果的场景,你想让一个任务每小时只运行一次,或者某个API调用后,5分钟内不允许再次调用。

做法是:任务在执行前,先检查一个特定的键是否存在(EXISTS cooldown_key),如果不存在,说明可以执行,任务立即执行,并且在执行后立刻用SETEX cooldown_key 300 “1”命令设置一个存活300秒(5分钟)的键,在这5分钟内,任何其他任务来检查时,都会发现键已存在,于是就知道资源处于“冷却”状态,从而放弃操作,这种方法比锁更简单,因为它不涉及复杂的获取和释放逻辑,更像一个全局的信号灯,但它不适合需要精确互斥的场景,因为在检查和设置之间有一个极短的时间窗口,可能存在 race condition。

用Redis做红色围栏(资源防抢占),最严谨和通用的是基于SET NX PX命令的分布式锁方案;如果追求开发效率和可靠性,特别是在Java生态中,直接使用Redisson是上策;而对于简单的协调或限流场景,利用键的自动过期特性也能实现一个轻量级的栅栏,无论哪种方法,核心都是利用Redis单线程和原子操作的特性,在分布式环境中创建一个所有进程都能看到的、唯一的“占位符”。