Redis:我承载了上千万人的火影青春

Posted 腾讯云数据库

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Redis:我承载了上千万人的火影青春相关的知识,希望对你有一定的参考价值。

作者:李世顺,腾讯游戏天美J1工作室游戏后台高级工程师,先后参与过剑灵手游、火影忍者手游、写实赛车项目的研发与维护工作。


火影忍者手游已经上线4年多,活动是火影重要的运营手段,火影的活动除了签到、礼包、任务等商业化运营,最大的特色是有很多“玩法”级别的活动,包括了游戏战斗、sns等元素。如何持续稳定的输出高品质活动成了火影当前最大的挑战之一。

火影当前活动已经400+,而且还将持续稳定扩展,按理说,活动本身逻辑与核心数据逻辑是弱耦的,但是现状却是与gamesvr(火影对应的是zonesvr)耦合在一起,不但影响了核心服务的可用性,也极大限制了活动本身的扩展性。



改造思路



1. 服务拆分


目前火影大部分需求都是新增的活动,而活动逻辑一直在 gamesvr 中,新增活动严重影响了 gamesvr 的可靠性,拆分出独立的actsvr后,还能提高活动服的扩展性。

Redis:我承载了上千万人的火影青春


2. 状态迁移导致的核心问题


游戏逻辑决定活动一定会产生状态,状态不能放在svr的内存中,要转移到全局的数据库中,从gamesvr中抽出活动逻辑需要解耦,大体包含3个问题:

2.1 数据库:


  • 数据库的内存暴增

  • 数据库的 QPS 暴增

  • 对数据库提供的数据结构也有一定需求

2.2 数据一致性:


  • 最终一致性无法满足所有需求

  • 强一致性需求需要分布式锁,但要避免不必要的阻塞

2.3 模块耦合:


  • 有状态服务大量模块耦合,剥离新服务困难重重


Redis



1. 为什么是redis


目前项目组使用的 tcaplus 是互娱研制的一款高速分布式的key-value数据库,效率上没有太大问题,但是没有多样化的数据结构、lua脚本等功能,难以应对无状态化编程带来的挑战。

而 redis 作为业界标准,不仅效率高、还支持多样化的数据结构以及lua脚本等功能,公司也有专门团队提供支持,因此活动svr选用了 redis 作为数据库。

tredis 团队除了原生的redis集群支持,还自研了tredis SSD模式,该模式主要特点是用SSD替换了内存,解决了redis 的内存问题,当然也是有代价的。火影活动数据基本都是周期性数据,一般上线一两个星期,下线后数据都可以自动过期清理,不会常驻内存,数据量本身可控,所以采用低延时的 redis cache 方案更合理。

2. Redis 内存


redis 的瓶颈一般都是内存,只要游戏逻辑合理,QPS一般问题不大。游戏逻辑需要的数据经过 pb 压缩后已经没有优化的可能,在数据量固定不变的情况下,提高 redis 的内存利用率,可以极大的压缩 redis 内存。

2.1 redis string 类型内存模型


Redis:我承载了上千万人的火影青春


假定redis使用的内存分配器是jemalloc,dicEntry、SDS、redisObject 等结构都是独立分配的内存块,大小只能是 16、32、64…字节(value不大时redisObject和SDS可能会共用一块内存),即使没用完也会统计到used_memory 指标中。


2.2 redis string 类型内存利用率


Redis:我承载了上千万人的火影青春


实际需求中,大部分 key 的长度在 24-55 字节之间,value 的长度在 8-39字节之间(value一般都会用 pb 等压缩,所以数据不多的情况下长度不会太长)。按照一定的规则规范化 key,可以缩减 key 长度到 23字节以内,每条记录可以缩减 32 字节的内存,value 的内存利用率将提升25%。


2.3 规范化key


Redis:我承载了上千万人的火影青春


实现方法可以用 protobuf 描述层次关系,用反射特性实现名字到id的转换,例如 RedisKey.actsvr.task 被转换成 1|1:

Redis:我承载了上千万人的火影青春


2.4 如何大幅度提高value内存利用率


如果把要使用的 redis 数据都集中到一起,集中存放,则 value 的大小会远大于 key 和其他内存结构的大小,从而使内存利用率达到 50%~99%。然而此方案也有弊端:如果只想取某个子模块的数据也必须把整体数据都拉下来,无状态化的情况下本来就会频繁读写数据,此方案将显著增加 redis 的CPU压力。

redis 的 hash 类型既可以把数据集中存放,也支持 key 分开读写。

2.5 redis hash类型的ziplist内存模型


redis hash 类型在 key 数量少于 512字节(可配置),最大value 成员不超过 64 字节(可配置)时,会采用 ziplist 结构压缩内存占用。

Redis:我承载了上千万人的火影青春


假设数据分为n个模块,每个模块的数据量都不大(8~39字节),两种方案:使用 n 个 string类型分别存储模块数据、使用一个 hash 表n个field存储模块数据的内存利用率对比如下:

Redis:我承载了上千万人的火影青春


模块数越多,hash类型提升内存利用率的效果越明显,主要原因是redis内部存储的辅助数据结构占用空间大大减少,次要原因是 hash 表的 field 对应的 key 缩短了不少,因为只需要标识子模块信息即可。


3. 分布式锁


无状态化后,总有一些需求对数据有强一致性要求,这种情况下,只能用分布式锁:互斥锁虽然能满足大多数需求,但是会影响效率,如果不是必要,可以考虑条件锁,符合条件的情况下即使并行也不会阻塞。

3.1 lua 互斥锁


在 redis 中,同一个 key 可以保证在一台机器上,redis 的单线程执行确保了针对此 key 操作时数据的强一致性。实现分布式锁还需要注意判断加锁解锁的条件、防止死锁等问题,用 lua 脚本实现锁可以很方便的避开以上问题。

a. 上锁

 
   
   
 
SetNxTtl

已经上锁了会直接返回失败,上锁的同时还会设置过期时间,防止死锁。

b. 释放锁

 
   
   
 
lua_str = "if (redis.call('get', KEYS[1]) == ARGV[1]) then return redis.call('del', KEYS[1]) end"

有了对内容的对比,确保只释放自己加的锁,不会误释放其他人加的锁。

3.2 lua 条件锁


条件锁可以减少不必要的阻塞,比如同时加入队伍场景下,可以设置条件锁:仅队伍人数小于5才能加入队伍:
"if (redis.call('zcard', KEYS[1]) < 5) then return redis.call('zadd', KEYS[1], ARGV[1], ARGV[2]) end"


程序框架



程序框架需要解决模块耦合的问题,还需要提供统一的数据管理功能,包括读写数据、加锁解锁等。

1. 模块切分


以插件化的思路分离各模块,每个模块作为插件自治,可以单独处理客户端发来的拉详情、指定命令、GM 指令。每个活动都可以灵活的 load 指定插件集合,无需关心插件内部实现,只需要实现活动特有逻辑即可。

Redis:我承载了上千万人的火影青春


2. 数据管理


以 hash 类型存储整个活动数据,每个模块占用一个 field,既可以支持整体活动数据的读写,也支持各模块单独读写数据,便于插件模块自治,同时 redis 也能保持很高的内存利用率。

对数据一致性要求很高的模块有对整体或插件模块上锁的需求,从而实现精准控制冲突域。

Redis:我承载了上千万人的火影青春


接口众多,新增需求实现起来必定很复杂,封装一个宏来自动生成代码就显得很有必要了,一个宏可包含以上所有接口及实现:

Redis:我承载了上千万人的火影青春



五人派对活动设计实例



1. 玩法


五人派对活动是为了增加玩家活跃而设计的组队玩法,玩家可以邀请好友组成最多五人的小队,每个队员只可以翻一张牌,五张牌都不一样,翻牌进度共享,翻牌进度会触发所有队员的任务进度,五个人都翻完牌后所有人都能领取丰厚的任务奖励。


Redis:我承载了上千万人的火影青春


2. 队伍核心操作


2.1 创建队伍


玩家1邀请玩家2、3,玩家2、3同时接受邀请,有可能会创建两个队伍,所以需要加锁

Redis:我承载了上千万人的火影青春


2.2 加入和退出队伍


队伍已存在时,队伍成员是个 set 类型,即使多名玩家同时操作也不会有问题。

Redis:我承载了上千万人的火影青春


2.3 同时加入触发队伍满


用lua条件锁保证后来的成员一定抢不到锁,加入失败。

仅队伍人数小于5才能加入队伍
"if (redis.call('zcard', KEYS[1]) < 5) then return redis.call('zadd', KEYS[1], ARGV[1], ARGV[2]) end"


Redis:我承载了上千万人的火影青春


3. 活动玩法核心操作


翻牌集合做成 set 类型,同时翻不同的牌不会冲突。

3.1  玩家同时翻到相同的牌


用 lua 脚本实现条件锁,仅此牌没被翻才翻牌,此牌已翻翻牌失败:
 
   
   
 
if (redis.call('zadd', KEYS[1], ARGV[1], ARGV[2]) == 1then return redis.call('zcard', KEYS[1]) end


Redis:我承载了上千万人的火影青春



总结与展望



1. 开发效率保障


插件化的活动框架极大提高了开发效率,插件模块的高度自治使得新活动开发不再需要关注任务、排行、队伍、分享、兑换等功能。

统一的数据管理方案确保了数据的高效可靠,完备的锁机制为数据一致性问题提供了保障。

2. 服务可用性


将不断扩展的活动从 gamesvr 中解耦,独立成actsvr,不仅增强了核心服务的稳定性,也给了活动服良好的扩展性。把活动数据的状态从 gamesvr 转移到redis中,也就是把gamesvr上的部分风险转移到了redis中。一系列优化措施有效稳定了redis的内存、QPS,确保风险可控。一方面redis作为业界标准,可靠性有保障,另一方面火影使用的redis 集群是由公司专业团队提供的支持,不仅支持在线扩缩容,还有完善的监控告警。

目前火影已经为1000w+用户提供了稳定的游戏活动体验,而redis集群的内存和QPS都在掌控中。

Redis:我承载了上千万人的火影青春



扫码报名赢公仔! 



↓↓200份公仔等你来拿~

以上是关于Redis:我承载了上千万人的火影青春的主要内容,如果未能解决你的问题,请参考以下文章

上千万人同时在线,我研究了D音的技术架构,我想说牛X!

热血漫少年已经渐渐走远

阿里AI破纪录超人类;世界杯的火眼睛睛,伤了多少人的心;天才黑客决定给马斯克打工12周...

终于,字节跳动要取消大小周了,我 1.7 万人的票圈都快炸了!

122万人的生活工作和死亡数据分析

瑞典让华为失去了一个1000万人口的市场,却同时让爱立信失去了一个140000万人的市场...