本文介绍一下我在多个游戏项目中,对Redis使用的经验。Redis是个好东西,可以用在不同的业务场景。
Redis最大的特点是所有的数据都存放在内存里,它常用于数据库、cache和消息队列等场景。
首先,redis数据都在内存里,其次,整体来看可以把redis看成一个大hashmap,所以基于key索引数据都可以在O(1)时间复杂度内完成,所以一个Redis节点(Redis进程)的QPS可以达到10w,特别适合作为传统数据库(mongo/mysql)的补充提升读写效率。
所有的读写请求都是单线程按照顺序执行的,每个请求都是原子的,不会被打断。
Redis支持分布式集群,比如阿里云会在集群的多个节点前面放一个负载均衡,就可以像访问单节点一样访问redis集群。
如下图所示,是我们在阿里云的一个16节点redis集群。
阿里云的redis集群
由于阿里云提供的redis集群节点数量总是有上限的(其实可以做到无上限),因此为了获得更好和更灵活的水平扩展能力,我们游戏在业务层又增加了一层分布式功能。在操作redis的同时,可以传入一个参数作为index通过hash映射到某一个redis集群上。
因为支持持久化,若同时开启RDB和AOF持久化选项,可以作为持久化数据库保存数据,重启redis数据不会丢失。
阿里云同时开启了RDB和AOF(存疑)。
少量数据的持久化存储可以使用redis,但是大量数据不可以。因为redis会把数据全部放到内存里,所以对于大量数量,内存成本太高。
比如好友关系这类关系信息属于多个玩家之间的信息,保存在玩家身上也不合适。而且这类信息使用比较频繁并且数据量不大,就可以持久化存储在redis中。
Redis还有一些高级特性:
redis整体来看存储的是一个很大的hashmap,其中key是一个字符串,value可以有多种数据结构。
value支持String、Hash、List、Set、SortedSet五种数据存储结构,可以实现不同的业务需求:·
大家使用redis时,使用方式的就是通过接口操作这些数据结构,所以建议大家看一下redis的基本操作 http://doc.redisfans.com/
一般来说,游戏会使用mongo或者mysql存储各类游戏信息,但这种数据库将数据存在硬盘里,读些速度比较慢。而redis将数据放在内存里,读写速度可以提高一个数量级。因此,可以配合mongo/mysql使用。
redis最常见的功能就是作为cache使用,游戏中很多逻辑如果去读mongodb,会有性能问题,所以使用redis作为缓存,降低对mongodb的读写。
此外,还有些需求可以使用Reids作为缓存,比如玩家个人简要信息。当我们查看其他玩家信息时,往往不需要全量信息,只需要部分信息(在线状态、等级、部分信息等),而这些信息要是去读取mongo会产生非常大的压力。因此,可以将指定的玩家信息放在Redis里,提高访问效率。
因为redis本身就支持持久化存储,所以redis在一定程度上可以替代mongo。
那么,什么数据适合存在mongo,什么数据适合存在redis呢?
对于一个数据应该放在redis还是mongo中存储,主要考虑两个方面:1.数据量。2.数据访问频率。
若数据量较小,比如记录哪些区域聊天频道需要关闭,这种信息一般使用redis记录即可。
若数据量较大但不是特别大,但数据访问频率比较高,比如玩家好友关系信息,那么一般也可以使用redis存储。
若数据量特别大,比如玩家/家族全量信息,一般使用mongo进行存储。
一般游戏服务器都是有状态的,也就是说内存里是有数据的。而无状态的服务的意思是内存里不保存数据(有可能在执行逻辑过程中有些临时变量)。
关于有状态无状态的描述,可以参考 https://blog.csdn.net/yinxiangbing/article/details/53353940
内存里不保存状态,那么状态存在哪里呢?redis就可以保存状态的地方。
举个例子,我们要开发一个组队服务,那么就需要一个组队匹配池,我们可以将匹配池保存在服务内存中(有状态),当然也可以将这个匹配池信息保存在redis中(无状态)。
无状态(信息保存在redis)的好处如下:
坏处:成本高、编程麻烦。
我在网易曾经写过一套微服务框架,其实就是用的无状态的原理写游戏逻辑。为服务框架非常适合写非战斗逻辑,对大DAU游戏可以提供非常强力的支持。后面有机会可以写文章介绍下思想。
有些不同的进程需要共享数据,可以使用redis存储这些共享数据。
比如有个游戏需求是全服玩家给最喜爱的角色投票,所有玩家在不同的进程都需要操作这些信息,就可以将这些信息保存在redis中进行共享。
redis的数据支持生命周期,所以若一些数据是临时的(比如活动数据,活动结束后就不需要了),就可以将数据存在redis中,并且给一个生存时间(通过EXPIRE
命令),当时间超过生存时间就会自动删除。
Redis的SortedSet非常适合做排行榜功能,在几年前Redis不火时,如何写一个好用的游戏排行榜还是一个课题,近两年就再也没有人提这个事情了,没啥好说的,用redis就好了。
不同进程中的逻辑若希望使用锁写同步逻辑,可以基于redis的SETNX
命令实现。
参考: https://juejin.im/post/5b737b9b518825613d3894f4
游戏中不同的进程之间的通信,也可以使用redis作为消息队列来通信。
比如给游戏服务器发一个控制命令,就可以将命令存在redis的list中,游戏服务器实时检查redis是否有命令,有则pop出来执行。
Redis能否作为消息队列使用是一个常常被争论的话题,因为有更专业的消息队列,比如Kfaka。我曾经服务的一个百万级DAU项目曾经使用Redis作为简单功能的消息队列(只有消息传输功能),由此可见Redis作为消息队列在生产环境使用问题并不大。
大key表示一个key对应的value非常大。比如,我们希望记录每个玩家的当前在线状态,若我们创建了一个key为online_status
的hashmap,这个hashmap的格式为uid:status
。那么,所有的玩家的在线信息都保存在这个key中。这种情况,就会导致online_status这个key对应的value极大。
redis虽然没有限制一个key对应的value的大小(应该没有),但是不建议使用大key。正常的使用方式是将每个玩家的信息作为一个字符串直接保存在redis中,比如key为online_status_{uid}
,value为是否在线。
原因是redis基于key来做的分布式,若创建了一个大key,就会导致分布式功能失效,所有的请求都会到达同一个redis节点,导致这个节点卡顿,其他节点空闲。
Bigkey没办法检测
hotkey的含义是某个key读写频率很高,特别繁忙。
需要避免这种情况的原因和大key类似,会导致分布式失效,某些节点卡顿。
hotkey在Redis4.0版本有检测方式
大家需要创建一个新的key前,需要保证key在所有业务中是独一无二的,因此key最好有namespace的概念。
我们可以通过给key前面加一个前缀,来避免key重复的情况。比如我们需要记录玩家在线信息,key设计为player:online_status
,而当我们又需要记录家族的活跃状态,key设计为family:online_status
。
(举个栗子,家族活跃状态应该叫active_status更合适...)
上文说过,Redis不适合做大数据的持久化存储,因为Redis会将所有数据都放在内存里,相比硬盘存储,成本极高(作为一个成功游戏项目都觉得贵)。
其实有一种折中方案,就是将数据分为冷数据(表示长时间不活跃的数据,比如流失玩家数据)和热数据(活跃数据,比如经常登录玩家信息),将冷数据放在硬盘里,将热数据放在内存里。然后当长期不活跃玩家回流后,就将这个玩家的数据从冷数据转为热数据,从硬盘中转到内存中,玩家长期不在线则把他的数据从热数据转为冷数据,从内存中转到硬盘中。这样的话,内存成本就会降低很多。
之前项目同事曾经尝试实现过这个功能。就是写一个proxy,后面挂一个redis和mongo,proxy实现一套redis协议解析,并且实现冷热数据切换和管理。冷数据存在mongo,当访问冷数据时将数据加载到redis中转为热数据,热数据超过一个ttl后,又变为冷数据存到mongo中。
这个有一些问题,比如redis数据和mongo数据格式如何对应等,如何保证数据一致性和高可用性等。此外,对redis的使用API各种限制也比较多。
腾讯云有一个类似的产品TcaplusDB, 但是不支持Redis命令,据说腾讯游戏内部用的都是这个东西(腾讯的同学可以说说)。腾讯还有一个类似的开源项目DCache,估计就是TcaplusDB的开源产品。
阿里云已经有Redis冷热数据分离的beta版本,等成熟了,甚至可以一定程度上取代mongodb。我认为这个项目用在游戏行业非常合适,关注已久。
云数据库Redis_混合型存储_冷热数据分离_节约成本promotion.aliyun.com/ntms/act/hybridstore.html编辑