You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Redis In Action中公平信号量实现能否防范特定竞争条件?

《Redis实战》公平信号量实现的竞争条件疑问与解答

问题描述

我正在研究《Redis实战》(Redis In Action)一书中的fair semaphore(公平信号量)实现,无法理解该实现如何防范特定的竞争条件。其实现代码如下:

def acquire_fair_semaphore(conn, semname, limit, timeout=10):
    identifier = str(uuid.uuid4())
    czset = semname + ':owner'
    ctr = semname + ':counter'

    now = time.time()
    pipeline = conn.pipeline(True)
    pipeline.zremrangebyscore(semname, '-inf', now - timeout)
    pipeline.zinterstore(czset, {czset: 1, semname: 0})

    pipeline.incr(ctr)
    counter = pipeline.execute()[-1]

    pipeline.zadd(semname, identifier, now)
    pipeline.zadd(czset, identifier, counter)

    pipeline.zrank(czset, identifier)
    if pipeline.execute()[-1] < limit:
        return identifier

    pipeline.zrem(semname, identifier)
    pipeline.zrem(czset, identifier)
    pipeline.execute()
    return None

该实现的核心特性(暂时忽略超时逻辑)是原子性地递增计数器,然后将新的UUID插入有序集合,使用刚递增得到的计数器值作为分数,再通过UUID在有序集合中的排名判断是否获取到信号量。

但我发现该实现存在两次pipeline.execute()调用,意味着使用了两个独立的事务,这会导致两个并发的信号量获取请求的操作可能发生交错。关键在于,计数器递增操作在第一个事务中,而UUID插入有序集合的操作在第二个事务中。因此可能出现如下交错操作序列:

  • A递增计数器 -> B递增计数器 -> B插入有序集合 -> A插入有序集合

假设在该序列发生前,信号量仅剩1个可用许可,计数器值为100。那么A得到的计数器值为101,B得到的为102。当B插入其UUID时,由于A尚未插入,B的排名在许可限制内,认为已获取信号量;之后A插入其UUID,由于A的分数低于B,其排名与B之前的排名相同,也会认为已获取信号量。

请问该实现中是否存在防范这类竞争条件的机制?


解答

你提到的这个竞争场景是真实存在的,而原实现并没有针对这类竞争的防范机制——这是该实现的一处设计缺陷。

问题的根源在于代码把「计数器递增」和「UUID插入有序集合并检查排名」拆成了两个独立的Redis事务(两次pipeline.execute()调用),并发请求的操作序列完全可能出现你描述的交错情况,最终导致信号量许可被超额分配,突破设定的limit限制。

要解决这个问题,必须将「清理超时条目、递增计数器、插入UUID、检查排名」这些关键步骤合并为一个原子操作,最可靠的方式是使用Redis Lua脚本(因为Lua脚本在Redis中是原子执行的),避免中间步骤被其他请求打断。

比如可以改写为以下Lua脚本实现的逻辑:

local semname = KEYS[1]
local limit = tonumber(ARGV[1])
local timeout = tonumber(ARGV[2])
local identifier = ARGV[3]
local czset = semname .. ':owner'
local ctr = semname .. ':counter'
local now = tonumber(ARGV[4])

-- 清理超时的信号量持有者
redis.call('ZREMRANGEBYSCORE', semname, '-inf', now - timeout)
redis.call('ZINTERSTORE', czset, 2, czset, semname, 'WEIGHTS', 1, 0)

-- 递增计数器并记录值
local counter = redis.call('INCR', ctr)
-- 将UUID插入两个有序集合
redis.call('ZADD', semname, now, identifier)
redis.call('ZADD', czset, counter, identifier)

-- 检查当前UUID的排名
local rank = redis.call('ZRANK', czset, identifier)
if rank < limit then
    return identifier
else
    -- 未获取到信号量,清理已插入的数据
    redis.call('ZREM', semname, identifier)
    redis.call('ZREM', czset, identifier)
    return nil
end

通过这种方式,所有关键操作都在一个原子执行的Lua脚本中完成,彻底避免了并发交错导致的竞争问题。


内容的提问来源于stack exchange,提问作者solarein

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.30 08:07:53