基于Redis构建广告栈与HTTP API轮询广告投放方案咨询
最优轮询投放方案:基于Redis原子操作的无侵入实现
这需求很贴合实际场景——既要保证轮询顺序、支持动态扩展,又不能动原始数据引发频繁读写,用「记录最后投放位置」的思路完全可行,我给你梳理一套落地性极强的方案:
核心设计思路
我们把数据分成三个独立的Redis结构,彻底隔离原始数据、轮询顺序和投放指针,所有操作都是原子性的,完全避免竞态问题:
- 哈希表
KEY1:存原始键值对(只读,仅新增时写入) - 有序集合
KEY1_ORDER:维护轮询的顺序(新增时写入,保证插入顺序的稳定性) - 计数器
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
相关产品推荐
相关产品推荐

