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别自己排序。踩过的坑都是经验,下次遇到类似场景别重蹈覆辙。
延伸阅读: