Redis - 在游戏开发中的几种应用场景
1. 内存数据库
Redis 数据主要存储在内存,综合性能标准 100k+ QPS。
需要说明下,十万QPS只是个综合参考,实际性能跟CPU性能、操作的命令复杂度有较大关系,对于简单的 set/get 操作50万QPS也没问题。
2. 丰富的数据结构
所有 Redis 的数据都是以 key-value 键值对的形式存在,value 可以是不同的数据结构,最常用的五种就是string, list, hash, set, sorted set。
string:字符串,二进制安全,最大512MB,通常建议不超过5KB。
list:string 组成的有序队列,也可以理解为链表
hash:一个或多个 field:value 的集合。
set:string 类型元素的无需集合,元素不重复。
sorted set:在set的基础上,元素有分数,按分数顺序存储。
3. 高效的函数和API
Redis 在上述数据结构基础上构建了丰富和高效的功能函数。这些功能函数可以说是 Redis 最核心的部分,涵盖了大部分常用数据结构及相关操作。业务不再需要重复去实现这类函数,为开发人员减负。
常用场景
场景一:临时数据
在游戏中,常常需要存储一些临时数据,这些数据通常有一定的时效性(一段时间后过期重置),而且读写也较为频繁,某些场景下可能还需要一定递增或递减。
比如 MOBA 游戏中的 buf,简化模型之后可以使用如下方式实现:
SET my_role buff EX 5 # 设置一个5秒后过期的buff
GET my_role # 5秒后nil
游戏中的连击数,可以通过递增命令增加,同时更新一下时长,在一段时间内有效:
INCR hit_points # 连击数+1
EXPIRE hit_points 3 # 重设为3秒内有效
游戏中经常有基于地理位置的需求:
下面有三个分别位于不同城市的玩家,所处于的经纬度分别如下:
广州玩家 23.1291° N, 113.2644° E
深圳玩家 22.5431° N, 114.0579° E
汕头玩家 23.3541° N, 116.6820° E
GEOADD game 113.26 23.12 Guangzhou
GEOADD game 114.06 22.54 Shenzhen
GEOADD game 116.68 23.35 Shantou
GEODIST game Guangzhou Shenzhen km"104.3422" # 计算广州和深圳玩家间的距离
GEORADIUSBYMEMBER game Guangzhou 200 km 1) "Shenzhen"2) "Guangzhou" # 统计广州玩家200KM以内的其他玩家
因为地理位置信息属于经常变化的临时数据,用 Redis 存储和读写非常方便。
场景二:排行榜
创建排名榜leaderboard,并给四名玩家添加分数
ZADD leaderboard 10000 user:1 21000 user:2ZADD leaderboard 34000 user:3 35000 user:4
ZREVRANGE leaderboard 0 2 1) "user:4"2) "user:3"3) "user:2" # 获取前三排名信息
ZREVRANK leaderboard user:4(integer) 0 # 获取指定用户排名
注意:相同分数的用户数据,后记录却排序在前的解决方式
由于游戏分数都是整数的,redis中的分数是存入的double类型,所以决定在小数点后面做文章。
整体思想:数据的时间先后标识值,与一个提前定义的Integer.MAX_VALUE差值,添加到小数点后面。这样以来后添加的数据分数值肯定最大,但是与Integer.MAX_VALUE差值就是最小的,相同分数后添加的这就排在后面了。
-
有两种方案:
1)分数后面添加时间戳(如果同时在一个时间点操作的,当前运行时间的时间戳会有相同的情况,不如方案2)
2)游戏记录先入库后,获取新增记录的数据库主键ID,在分数后面添加(推荐,先后顺序交给数据库来定夺,肯定不会重复)
-
修正后:
场景三:好友关系
社交元素几乎是每个游戏的标配,通过共同好友建立更多好友关系,可以增加用户黏度。在 Redis 中使用 set 数据类型可以方便地计算社交关系。
SADD my_friends Ben Lucy Mary # 添加我的朋友
SADD your_friends Lucy Jack Pony # 添加你的朋友
SINTER my_friends your_friends # 交集操作,计算共同好友"Lucy"
场景四:在线玩家
为了统计活跃人数,通常需要记录特定范围内的在线人数,这个同样用set数据结构可以简单地实现。
如果要计算我的好友中有谁在线,要怎么操作,也是很简单地将在线玩家和我的好友做一个交集就能实现。
SINTER my_friends online_players # "Ben"
场景五:限流
游戏中有时会对提供的功能进行一定的访问次数的限制,避免被恶意利用,比如玩家作弊等行为。
Redis 很容易可以记录玩家在一定时间内的访问次数实现,可以编写一个简单的逻辑:
用户访问服务器,服务器去Redis中查询该用户ID,如果数据库没有记录,则设置为1,同时设置一个时效,比如5秒。
如果服务器查到该用户的记录,则服务器往 Redis 的记录上加一,并判断是否已经超过上限阈值(比如3次)
如果没超过上限则返回0,表示可以继续访问;如果已超过则返回1,拒绝该用户的访问。
但这里有两个问题需要考虑:
1)服务器对数据库至少要两次操作,才能完成完整的流程,有没有办法一步到位?
2)两次操作在单一服务器的环境下尚能工作,但在分布式应用环境下,存在一致性问题,导致可能超过上限的访问次数。为了解决一致性问题可能又需要另外引入锁来解决,就变得比较复杂了。
在 Redis 中可以通过编写 Lua 脚本来解决上述两个问题。Redis 内置 Lua 脚本解释器,可以实现在 Redis 中完成逻辑操作,并且每次操作都是一次原子操作。
将上述逻辑编写成一个简单的 Lua 程序:
local cnt = redis.call('INCR', KEYS[1]) if cnt > tonumber(ARGV[1]) then return 1 end if cnt == 1 then redis.call('PEXPIRE', KEYS[1], ARGV[2]) end return 0
该脚本每秒执行一次,模拟一个玩家发送请求,前3次返回0表示允许访问,第4、5次返回1表示拒绝访问。5秒之后重新刷新访问权限。
入门必踩的坑
Redis 是个单线程服务,虽然因为速度快可以处理很高的并发,但也会容易出现某个命令卡住整个服务的情况。以下两个是比较典型的例子:
1)使用可能会导致 Redis 阻塞的命令,如KEYS *,可以使用SCAN代替。
2)巨大 Key 的操作会导致Redis阻塞,要合理控制集合的元素个数(比如5000个)和 string 的大小(比如5K以内)。
现场问答
序列化与反序列化
Q:Redis 用做数据缓存,存储序列化后的数据。一方面数据量会比较大,另一方面频繁序列化和反序列化也增加了服务器的负载,有什么方法解决?
建议选择合适的 Redis 支持的数据结构,尽量利用 Redis 的功能来完成业务上的功能,减少序列化的需求。Redis 原生支持丰富的数据结构和功能,另一方面也支持模块(modules)功能,比如 rejson 模块让 Redis 可以支持 json 操作,业务甚至可以根据需求开发自己的模块。
持久化
Q:关于持久化。
对于开启了持久化的 Redis,数据会落盘,同时写操作可能会受到磁盘IO影响,如果开启了AOF持久化,根据经验会有20%-30%左右的性能损耗。建议能不开持久化就尽量不要开,要开持久化也尽量在从节点上开。
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· PowerShell开发游戏 · 打蜜蜂
· 在鹅厂做java开发是什么体验
· 百万级群聊的设计实践
· WPF到Web的无缝过渡:英雄联盟客户端的OpenSilver迁移实战
· 永远不要相信用户的输入:从 SQL 注入攻防看输入验证的重要性