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

基于Redis构建广告栈与HTTP API轮询广告投放方案咨询

最优轮询投放方案:基于Redis原子操作的无侵入实现

这需求很贴合实际场景——既要保证轮询顺序、支持动态扩展,又不能动原始数据引发频繁读写,用「记录最后投放位置」的思路完全可行,我给你梳理一套落地性极强的方案:

核心设计思路

我们把数据分成三个独立的Redis结构,彻底隔离原始数据、轮询顺序和投放指针,所有操作都是原子性的,完全避免竞态问题:

  1. 哈希表KEY1:存原始键值对(只读,仅新增时写入)
  2. 有序集合KEY1_ORDER:维护轮询的顺序(新增时写入,保证插入顺序的稳定性)
  3. 计数器KEY1_POINTER:记录上一次投放的偏移位置(每次请求仅原子递增这个值)

为什么用有序集合而不是列表?因为列表的LINDEX操作在数据量大时是O(N)复杂度,而有序集合的ZRANGE取指定位置元素是O(logN),性能更优,适合高并发场景。

具体实现步骤

1. 初始化数据

先把初始数据和辅助结构搭好:

# 存储原始键值对
HSET KEY1 V1 A1 V2 A2 V3 A3

# 初始化顺序计数器(用于生成有序集合的score,保证插入顺序)
SET KEY1_NEXT_SCORE 1

# 把初始键加入有序集合,同时递增计数器
ZADD KEY1_ORDER 1 V1
INCR KEY1_NEXT_SCORE
ZADD KEY1_ORDER 2 V2
INCR KEY1_NEXT_SCORE
ZADD KEY1_ORDER 3 V3
INCR KEY1_NEXT_SCORE

# 初始化投放指针(-1表示第一次请求会从0开始)
SET KEY1_POINTER -1

2. 动态新增键值对

新增时用Lua脚本保证原子性,避免并发下的顺序错乱:

-- 新增键值对的Lua脚本
redis.call('HSET', KEYS[1], ARGV[1], ARGV[2])
local score = redis.call('INCR', KEYS[2])
redis.call('ZADD', KEYS[3], score, ARGV[1])
return 1

调用时传入参数:

  • KEYS[1]:KEY1(哈希表)
  • KEYS[2]:KEY1_NEXT_SCORE(顺序计数器)
  • KEYS[3]:KEY1_ORDER(有序集合)
  • ARGV[1]:新增的键(比如V4)
  • ARGV[2]:对应的值(比如A4)

3. 轮询投放的核心逻辑

同样用Lua脚本封装所有操作,保证原子性,避免并发冲突:

-- 轮询投放的Lua脚本
local order_len = redis.call('ZCARD', KEYS[2])
if order_len == 0 then
    return nil -- 无数据时返回空
end

-- 原子递增指针,保证每次请求的偏移唯一
local pointer = redis.call('INCR', KEYS[3])
-- 取模实现循环轮询
local index = pointer % order_len
-- 获取当前轮询的键
local key_name = redis.call('ZRANGE', KEYS[2], index, index)[1]
-- 获取对应的值
local value = redis.call('HGET', KEYS[1], key_name)

return {key_name, value}

调用时传入参数:

  • KEYS[1]:KEY1(哈希表)
  • KEYS[2]:KEY1_ORDER(有序集合)
  • KEYS[3]:KEY1_POINTER(投放指针)

4. 控制器示例(Python)

用Redis客户端调用脚本即可实现API逻辑:

import redis

# 初始化Redis连接
r = redis.Redis(host='localhost', port=6379, db=0)

# 加载Lua脚本到Redis(提升性能,避免每次传输脚本)
poll_script = """
local order_len = redis.call('ZCARD', KEYS[2])
if order_len == 0 then
    return nil
end
local pointer = redis.call('INCR', KEYS[3])
local index = pointer % order_len
local key_name = redis.call('ZRANGE', KEYS[2], index, index)[1]
local value = redis.call('HGET', KEYS[1], key_name)
return {key_name, value}
"""
poll_script_sha = r.script_load(poll_script)

add_script = """
redis.call('HSET', KEYS[1], ARGV[1], ARGV[2])
local score = redis.call('INCR', KEYS[2])
redis.call('ZADD', KEYS[3], score, ARGV[1])
return 1
"""
add_script_sha = r.script_load(add_script)

# 轮询API接口
def get_poll_data():
    result = r.evalsha(poll_script_sha, 3, 'KEY1', 'KEY1_ORDER', 'KEY1_POINTER')
    if result:
        key, value = result
        return {"key": key.decode('utf-8'), "value": value.decode('utf-8')}
    return {"error": "No available data"}

# 新增键值对的API接口
def add_new_pair(key, value):
    r.evalsha(add_script_sha, 3, 'KEY1', 'KEY1_NEXT_SCORE', 'KEY1_ORDER', key, value)
    return {"status": "success"}

方案优势

  • 零侵入原始数据:原始哈希表KEY1仅在新增时写入,完全避免频繁修改带来的性能损耗
  • 并发安全:所有核心操作都通过Lua脚本原子执行,不会出现竞态问题
  • 动态扩展友好:新增键值对只需要一次原子操作,新增后立即纳入轮询队列
  • 性能优异:所有Redis操作都是O(1)或O(logN),支撑高并发毫无压力
  • 顺序稳定:严格按照插入顺序轮询,完全符合你首次返回V1、再次返回V2的需求

额外注意事项

  • 如果需要删除键值对,记得同时从KEY1和KEY1_ORDER中删除,指针会自动适配新的集合长度,不影响轮询逻辑
  • 重启服务后,指针会保留上次的位置,轮询会从断点继续,无需额外处理
  • 若需重置轮询顺序,只需执行SET KEY1_POINTER -1即可

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.15 03:41:43