Redis 在游戏中的缓存应用
概述
Redis 是游戏服务器出场率最高的中间件:内存高速读写、丰富的数据结构、原子操作,几乎为游戏场景量身打造。本文系统讲解 Redis 在游戏中的四大核心应用:玩家在线数据缓存(Hash)、排行榜(ZSet)、计数器(INCR)、分布式锁(Redisson),并给出键设计与工程化建议。
一、Redis 在游戏中的角色
1.1 三大定位
缓存层:玩家数据、配置数据的读加速
状态层:在线状态、Session、Token(短生命周期)
计算层:排行榜、计数、限流(数据结构原子操作)1.2 与内存态 Player 的关系
在线玩家:内存态 Player(业务权威)
Redis 角色:
分布式共享状态(顶号、跨节点查询在线)
排行榜等需要全局排序的数据
短生命周期数据(Token、验证码、限流计数)
落库:MySQL(最终权威)
三层关系:内存(快) → Redis(共享) → MySQL(持久)分工原则:
需要全局共享/跨节点 → Redis
单节点内存就够 → 不浪费 Redis 往返
最终要可靠 → MySQL二、玩家在线数据缓存(Hash)
2.1 数据结构选型
| 场景 | 结构 | 理由 |
|---|---|---|
| 玩家在线状态 | String | 简单标记 |
| 玩家属性快照 | Hash | 多字段、可部分更新 |
| Token → playerId | String | 简单映射 |
| 会话键值 | String | KV 直取 |
Hash 存玩家属性的例子:
HSET player:10001 nickname "小明" level 12 coin 5000
HGET player:10001 coin → 单字段读取
HINCRBY player:10001 coin 100 → 原子增减2.2 Hash 缓存玩家数据
适用场景:
排行榜用的简要资料(昵称、头像、段位)
跨节点需要读取的公共信息
Session 关联数据
不适用:
大字段(背包明细)→ 用内存态 Player
频繁全量读 → 用 String 序列化2.3 缓存一致性
读写路径:
读:先查 Redis → 未命中查 MySQL → 回填 Redis
写:更新 MySQL → 更新/删除 Redis
策略选择:
删除缓存(推荐):下次读重建,简单可靠
更新缓存:并发写易不一致
双写一致性 + 过期兜底(TTL 双保险)三、排行榜(ZSet)
3.1 ZSet 核心能力
ZADD leaderboard 10000 playerId # 写入/更新分数
ZREVRANGE leaderboard 0 9 # 取 Top10
ZREVRANK leaderboard playerId # 查排名
ZINCRBY leaderboard 100 playerId # 分数累加
ZSCORE leaderboard playerId # 查分数为什么适合排行榜:
底层跳表,插入/查询 O(logN)
排序、排名、TopN 全内置
单命令原子操作,天然支持并发加分3.2 排行榜设计
键设计:
leaderboard:全局 → 总榜
leaderboard:20260806 → 日榜(按日期分键)
leaderboard:season:3 → 赛季榜
leaderboard:room:1001 → 房间榜(低频场景)
同分排名策略:
ZSet 按 (score, member) 字典序排
同分先到者在前 → 用组合分数:
score = 战绩 × 10000 + (MAX - 时间戳)3.3 缓存与持久化
榜单缓存:
TopN 结果缓存(Redis List/String),定时刷新
分段排行(日/周/月/总)各自独立键
持久化:
定期把 ZSet 快照落 MySQL(防 Redis 重启丢)
赛季结束:归档 → 发奖 → 清空重建四、计数器(INCR)
4.1 常用场景
| 场景 | 命令 | 说明 |
|---|---|---|
| 在线人数 | INCR/DECR | 上线加、下线减 |
| 今日登录次数 | INCR | 按日期键 |
| 领取次数限制 | INCR + TTL | 防刷 |
| 限流计数 | INCR + TTL | 窗口限流 |
| 全局自增 ID | INCR | 低量级场景 |
典型写法(防刷每日签到):
EXISTS sign:10001:20260806
→ 若不存在:INCR + EXPIRE 24h
→ 若已存在:拒绝(已签到)
原子性:用 Lua 或 INCR 后判断第一次4.2 INCR 与 TTL 组合
窗口限流(1 分钟 10 次):
key = rate:10001:minute:202608061030
INCR → 首次 EXPIRE 60s
>10 → 拒绝
时间窗口粒度决定限流精度注意:
INCR 返回 1 说明是新 key → 需设置过期
避免 key 无 TTL 永久累积
批量场景用 Pipeline 减少 RTT五、分布式锁(Redisson)
5.1 为什么需要分布式锁
单体游戏服务器:JVM 锁就够
多节点游戏服务器:
同玩家操作可能落在不同节点
抢奖、发奖、跨服结算需全局互斥
→ 需要跨进程锁:Redis 分布式锁5.2 Redisson 使用
java
// 引入 redisson-spring-boot-starter 后自动配置
@Autowired
private RedissonClient redisson;
public void drawLottery(long playerId) {
RLock lock = redisson.getLock("lottery:" + playerId);
try {
// 尝试加锁,超时 3s
if (lock.tryLock(3, 10, TimeUnit.SECONDS)) {
// 抽奖逻辑
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}Redisson 分布式锁特性:
基于 Redis + Lua 原子实现
支持可重入、自动续期(看门狗)
失败自动重试 / 等待锁
相比手写 SETNX 更完善(防死锁、可重入)5.3 锁的粒度
粒度设计原则:锁越细越好
玩家级:lottery:playerId(同玩家互斥)
房间级:room:1001(同房间互斥)
全局级:guild:100(公会操作)
避免:
全局大锁(所有玩家抢一把锁 → 性能瓶颈)
锁住 DB 操作太久(锁内不做慢操作)5.4 锁的正确使用
规范:
锁内只放必要临界区,快进快出
统一 try/finally 释放
设置合理过期时间(防客户端崩溃死锁)
用 Redisson 的看门狗自动续期,不手写续期六、键设计与工程规范
6.1 键命名规范
统一格式:业务:实体:ID:属性
例:
player:10001:nickname
token:abc123
leaderboard:daily:20260806
lock:lottery:10001
要点:
语义清晰、冒号分层
全部可加前缀防环境串(dev/prod)
控制 key 数量(长 key 占内存)6.2 序列化与内存
值序列化:
简单值用字符串
对象用 JSON / Protobuf
不要用 Java 原生序列化(体积大)
内存控制:
设置合理 TTL(不设过期会累积)
大对象拆分(Hash 分字段)
监控 bigkey / 内存水位6.3 高可用
生产配置:
哨兵 / Cluster 高可用
主从 + 自动故障转移
持久化:AOF(游戏场景需防丢数据)
降级策略:
Redis 故障 → 缓存降级直连 MySQL
排行榜降级 → 读库临时榜单七、常见问题
| 问题 | 处理 |
|---|---|
| 缓存穿透 | 空值缓存 + 布隆过滤器 |
| 缓存击穿 | 热点 key 互斥重建 |
| 缓存雪崩 | TTL 加随机抖动 |
| bigkey | 拆分字段/结构 |
| 锁失效 | Redisson 看门狗续期 |
八、小结
Redis 在游戏里扮演"共享内存 + 数据结构计算"的角色:Hash 缓存玩家在线数据并支持原子增减,ZSet 一站式实现排行榜的写入、排名与 TopN,INCR 配合 TTL 完成计数、限流与防刷,Redisson 分布式锁解决多节点下的互斥。使用上坚持"键规范、设 TTL、控内存、保高可用"四条工程纪律,并注意缓存穿透/击穿/雪崩三类经典问题。Redis 与内存态 Player、MySQL 三层配合:内存扛热读写、Redis 扛共享与排序、MySQL 扛持久,各司其职。