服务公告

服务公告 > 综合新闻 > Redis - Redis高级应用场景 完全配置指南

Redis - Redis高级应用场景 完全配置指南

发布时间:2026-04-29 20:01
Redis高级应用场景实战指南:分布式锁、延迟队列与排行榜系统设计与优化

一、前言

干了10年运维,Redis用过不下百个集群,最让人头疼的不是性能调优,而是那些"看似简单、实则坑多"的高级场景。分布式锁并发撞车、延迟队列消息丢失、排行榜数据错乱,这些问题你遇到过几个?今天把这三个硬骨头掰开了揉碎了讲,手把手带你从入坑到填坑。

二、操作步骤

步骤1:分布式锁——先判断再删除的坑你踩过没

用SET NX做分布式锁是标配,但很多人写成了这样:

# 步骤1:加锁 SET lock_key unique_value NX PX 30000 # 返回OK表示获取成功 # 返回nil表示锁被占用 # 步骤2:释放锁——这是错的 DEL lock_key

这样写的问题是:锁可能被其他客户端释放。正确做法是用Lua脚本保证原子性:

-- unlock.lua if redis.call("GET", KEYS[1]) == ARGV[1] then return redis.call("DEL", KEYS[1]) else return 0 end

然后用redis-cli执行:

redis-cli --eval unlock.lua lock_key , unique_value # 预期输出: # (integer) 1 表示删除成功 # (integer) 0 表示锁已过期或被他人持有

步骤2:Redisson客户端封装的分布式锁

手写Lua脚本麻烦,Javaer直接用Redisson更省心:

// 加锁示例 RLock lock = redissonClient.getLock("order:123"); try { // 等待30秒锁,锁自动过期300秒 lock.lock(300, TimeUnit.SECONDS); // 业务逻辑 processOrder(); } finally { lock.unlock(); }

预期输出:

# 正常情况下无输出,业务处理完自动释放 # 锁等待超时抛异常:IllegalMonitorStateException # 分布式场景下,redisson_client_2日志显示: # [INFO] Lock acquired by clientId=xxx at 300000ms

步骤3:延迟队列——ZSET的score踩坑记

Redis原生没有延迟队列,用ZSET封装一个。score存任务执行时间戳:

# 添加延迟任务,10秒后执行 ZADD delay_queue 1700000000 '{"task_id":"001","data":"test"}' # 预期输出: # (integer) 1

然后写个轮询脚本来消费:

# poll_delay.sh #!/bin/bash while true; do # 取出所有已到期的任务(score <= 当前时间戳) tasks=$(redis-cli ZRANGEBYSCORE delay_queue -inf $((`date +%s`)) LIMIT 0 10)) if [ -n "$tasks" ]; then echo "发现待处理任务: $tasks" # 这里模拟处理 echo "$tasks" | while read task; do redis-cli ZREM delay_queue "$task" && echo "已处理: $task" done fi sleep 1 done

⚠️ 注意:并发消费时ZREM可能误删其他客户端刚加入的同score任务

步骤4:延迟队列并发安全改造

为了解决并发问题,引入任务ID去重:

# 用Lua保证原子性:取出任务ID的同时删除 -- consume_delay.lua local tasks = redis.call("ZRANGEBYSCORE", KEYS[1], "-inf", ARGV[1], "LIMIT", 0, 10) for i, task in ipairs(tasks) do redis.call("ZREM", KEYS[1], task) end return tasks

执行验证:

redis-cli --eval consume_delay.lua delay_queue , $(date +%s) # 预期输出: # 1) "{\"task_id\":\"001\",\"data\":\"test\"}" # 2) "{\"task_id\":\"002\",\"data\":\"test2\"}" # 返回空数组表示暂无待处理任务

步骤5:排行榜系统——Sorted Set的正确打开方式

游戏或直播场景需要实时排行榜,用ZSET做:

# 添加玩家得分 ZADD leaderboard 12500 "player_001" ZADD leaderboard 8300 "player_002" ZADD leaderboard 15800 "player_003" # 预期输出: # (integer) 1 (三个都返回1,表示都添加成功)

查询Top10(从高到低):

# 按分数逆序取前10 redis-cli ZREVRANGE leaderboard 0 9 WITHSCORES # 预期输出: # player_003 # 15800 # player_001 # 12500 # player_002 # 8300

查询玩家排名(从1开始):

redis-cli ZREVRANK leaderboard player_001 # 预期输出: # (integer) 1 表示排名第2(索引从0开始,所以实际排第2)

步骤6:排行榜数据聚合与定时同步

跨服排行榜需要定时聚合各服数据:

# 假设有多个服的排行榜,合并到全局排行榜 # 服1数据 ZADD server1:leaderboard 5000 "p1" 3000 "p2" # Lua脚本批量合并 -- merge_leaderboard.lua redis.call("ZUNIONSTORE", KEYS[1], 2, KEYS[1], KEYS[2], "AGGREGATE", "SUM") return redis.call("ZCARD", KEYS[1])

执行合并:

# 先清空全局榜,再合并服1和服2 redis-cli --eval merge_leaderboard.lua global:leaderboard server1:leaderboard server2:leaderboard # 预期输出: # (integer) 150 表示全局榜有150个玩家

⚠️ 警告:ZUNIONSTORE会覆盖目标键,执行前确认数据已备份

步骤7:Redis Stream实现可靠消息队列

Redis 5.0+的Stream比List更适合做消息队列,支持消费组:

# 创建消费组 XGROUP CREATE mystream consumer_group $ MKSTREAM # 预期输出: # OK (如果组已存在会报错:BUSYGROUP Consumer Group name already exists)

发送消息并消费:

# 生产者发送消息 XADD mystream "*" field1 value1 field2 value2 # 预期输出: # "1700000000000-0" 消息ID
# 消费者读取未处理消息 XREADGROUP GROUP consumer_group consumer1 COUNT 10 STREAMS mystream ">" # 预期输出: # 1) 1) "mystream" # 2) 1) 1) "1700000000000-0" # 2) 1) "field1" # "value1" # "field2" # "value2"

处理完成后ACK:

XACK mystream consumer_group 1700000000000-0 # 预期输出: # (integer) 1 表示确认成功

步骤8:Redis Pipeline批量操作优化

大量操作时别一条条发,用Pipeline减少RTT:

# 伪代码示例:批量设置1000个用户分数 redis-cli <

Python客户端Pipeline示例:

pipe = redis.pipeline() for user_id in range(1, 1001): pipe.hset(f"user:{user_id}", "last_login", int(time.time())) pipe.zadd("login_ranking", {user_id: int(time.time())}) pipe.execute() # 预期输出(时间对比): # 不使用Pipeline:~5000ms # 使用Pipeline:~200ms

三、常见问题FAQ

Q1:分布式锁超时了任务还没执行完怎么办?

这不是Redis的问题,是你设计的问题。锁超时时间要预估任务执行时间的150%,比如正常3秒,那就给10秒。另外别在锁内做外部IO操作,能拆就拆。实在不行换Redisson的看门狗机制,它会自动续期。

Q2:延迟队列机器宕机了任务丢了怎么补救?

ZSET方案天生不可靠,换Stream。Stream有持久化+消费组ACK机制,宕机重启后未ACK的消息会被重新投喂。但记住监控XPENDING,超过阈值要告警,别以为消息"丢失了"。

Q3:排行榜ZREVRANK返回的排名和实际不符怎么回事?

检查score类型——是字符串还是数字?ZINCRBY加分时如果用错数据类型,排序会乱。另一个坑:同分数玩家的排名是随机的,ZREVRANK返回的是第一个匹配的位置,不是唯一位置。

Q4:Pipeline执行失败怎么回滚?

Pipeline不支持事务回滚,这是设计取舍。想原子性用MULTI/EXEC,但会阻塞其他命令。折中方案:Pipeline分组,每组独立事务,用Lua脚本把逻辑包进去。

四、总结

Redis高级场景的核心就三点:选对数据结构、保证原子性、做好异常兜底。分布式锁用SET NX+Lua脚本别偷懒,延迟队列优先选Stream而非ZSET,排行榜直接上ZREVRANGE别自己排序。踩过的坑都是经验,下次遇到类似场景别重蹈覆辙。

延伸阅读:

上一篇: Drone - 常见问题 完全配置指南

已经是最后一篇啦!