redis在游戏开发中的应用

Redis是一个新兴的NoSql数据缓存组件,与memcache类似,但是功能却比memcache多一些。
首先,Redismemcache都是基于内存的,所以读取和写入速度都非常快。但是memcache只支持简单的key-value数据的存储方式,而Rediskey-value ,hash,list,set,SortSet等数据结构有很好的支持。

下面就Redis在游戏的开发应用中做一些简单的介绍。

数据的缓存

在这一点上,redismemcache是一样的。都是把数据提前放入到内存中。当逻辑处理中需要用到数据时,先从内存中读取,相同的,写的时候也先向内存中写入,然后再操作数据库,以增加数据处理的速度。不同的是,redis带有把数据写入到硬盘的功能,具体的写入策略可以在redis的配置文件中配置。这样当主机突然出现故障时,比如断电,重启机器不会造成数据的丢失。这个在游戏的应用中特别重要。一般在游戏开发中,数据的处理会采用:缓存 + 持久化队列 + 数据库(mysql)的架构。执行的流程是先把数据写入到缓存,然后把需要持久化的数据放入到持久化队列中,启动一个守护线程,从持久化队列中不断的取出数据,并存入或更新到数据库。如果使用memcache这样没有写入到硬盘功能的缓存组件,出现故障时,持久人队列中如果还有没有处理完的数据,那么就会造成数据的丢失,引用玩家的数据出现短暂的回档。当然这些也可以自己开发一些功能去防止,但是增加了开发成本。

不丢失数据的持久化队列的实现

上面说过Redis具有把数据写入到硬盘的功能,而且支持多种数据结构。那么就可以利用Redislist实现持久化队列,而且当机器出现故障时,不会出现队列中数据丢失的情况,重启之后,数据会自动加载到redislist之中。
具体实现方法:

(1)在Redis中构造一个list存储
(2)一个线程使用Redislpush方法,向list的左边加入数据
(3)另外一个线程使用Redisrpop方法,从list取出数据进行处理,并且从list中删除了取出的数据。这样就实现了一个简单的生产者–消费者模式的队列

对并发操作的控制(跨房间)

一般来说,我们操作一个数据的流程是这样的,取出–处理—存储,这样在单线程中操作是没有任务问题的,但是在多线程环境中就不适用了,我们必须考虑数据同步的问题,保证数据操作的原子性。如果在游戏中,对玩家战队的属性进行更新,一般在数据库中都会保存一个TeamInfo表,里面有玩家相应的属性,比如名字,等级,金币,钻石等等。在memcache中保存一个TeamInfo对象,这时玩家获得金币,我们就需要取出玩家所有的属性,然后set金币,完成后再存储整个对象。这个时候就得考虑数据的同步了,如果在操作的时候,另外一个线程B修改了钻石,并完成了存储,而这个时候我把金币修改完成之后,再存储,这时,就出现了数据混乱的结果。考虑数据同步无非也是加锁或乐观同步。不但增加了代码量,还增加了维护的难度。而在Redis中,它支持对hash数据结构的操作。我们可以把玩家的对象按每个字段存储到redishash中。

当我需要更新金币时,比如增加或减少,我可以使用Redis自带的原子操作方法:hincrby(String key,String field,int value)进行操作,value是正为加,是负为减,这样就简化和避免了一些并发操作,而且这个操做还减少了对数据的操作步骤,因为没有取出,再操作的过程了,只有一次写入。而且在游戏中很少一次更新非常多的字段,如果有这样的情况,下面的方法可以解决。

对事务的支持

redis提供了一个事务操作的机制,MULTI命令用于开启一个事务,它总是返回OK
MULTI执行之后, 客户端可以继续向服务器发送任意多条命令, 这些命令不会立即被执行, 而是被放到一个队列中, 当EXEC命令被调用时, 所有队列中的命令才会被执行。
另一方面, 通过调用DISCARD, 客户端可以清空事务队列, 并放弃执行事务。
以下是一个事务例子, 它原子地增加了foobar两个键的值:

1
2
3
4
5
6
7
8
9
> MULTI
OK
> INCR foo
QUEUED
> INCR bar
QUEUED
> EXEC
1) (integer) 1
2) (integer) 1

EXEC命令的回复是一个数组, 数组中的每个元素都是执行事务中的命令所产生的回复。 其中, 回复元素的先后顺序和命令发送的先后顺序一致。当客户端处于事务状态时, 所有传入的命令都会返回一个内容为QUEUED的状态回复(status reply), 这些被入队的命令将在EXEC命令被调用时执行。从Redis 2.6.5开始,服务器会对命令入队失败的情况进行记录,并在客户端调用 EXEC命令时,拒绝执行并自动放弃这个事务。

提供外部的CAS行为,实现乐观锁机制

在游戏开发中,有时候需要我们自己在外部实现乐观锁机制,WATCH命令可以为Redis事务提供check-and-set (CAS)行为,被WATCH的键会被监视,并会发觉这些键是否被改动过了。 如果有至少一个被监视的键在EXEC执行之前被修改了, 那么整个事务都会被取消, EXEC返回空多条批量回复(null multi-bulk reply)来表示事务已经失败。
举个例子, 假设我们需要原子性地为某个值进行增 1 操作(假设INCR不存在)。
首先我们可能会这样做:

1
2
3
val = GET mykey
val = val + 1
SET mykey $val

上面的这个实现在只有一个客户端的时候可以执行得很好。 但是, 当多个客户端同时对同一个键进行这样的操作时, 就会产生竞争条件。
举个例子, 如果客户端AB都读取了键原来的值, 比如10, 那么两个客户端都会将键的值设为11, 但正确的结果应该是12 才对。
有了WATCH, 我们就可以轻松地解决这类问题了:

1
2
3
4
5
6
WATCH mykey
val = GET mykey
val = val + 1
MULTI
SET mykey $val
EXEC

使用上面的代码, 如果在WATCH执行之后,EXEC执行之前, 有其他客户端修改了mykey的值, 那么当前客户端的事务就会失败。 程序需要做的, 就是不断重试这个操作, 直到没有发生碰撞为止。这种形式的锁被称作乐观锁, 它是一种非常强大的锁机制。 并且因为大多数情况下, 不同的客户端会访问不同的键, 碰撞的情况一般都很少, 所以通常并不需要进行重试。

缓存生命周期的控制

在游戏服务器中,为了节省性能,我们没有必要把所有玩家的信息都缓存到内存中。比如有一些不常登陆的玩家,那么他的信息就没必要一直呆在缓存中了,需要清除。Redis为这个功能提供了一个方法:expire,它可以为key设置以秒为单位的生命周期,比如设置为300s,那么五分钟之后,这条记录就会在内存中删除。这样不仅可以节省内存,而且增加了服务器的性能

排行榜

游戏服务器中涉及到很多排行信息,比如玩家等级排名、金钱排名、战斗力排名等。
一般情况下仅需要取排名的前N名就可以了,这时可以利用数据库的排序功能,或者自己维护一个元素数量有限的top集合。
但是有时候我们需要每一个玩家的排名,玩家的数量太多,不能利用数据库(全表排序压力太大),自己维护也会比较麻烦。
使用Redis可以很好的解决这个问题。它提供的有序Set,支持每个键值(比如玩家id)拥有一个分数(score),每次往这个set里添加元素,Redis会对其进行排序,修改某一元素的score后,也会更新排序,在获取数据时,可以指定排序范围。
更重要的是,这个排序结果会被保存起来,不用在服务器启动时重新计算。
通过它,排行榜的实时刷新、全服排行都不再成为麻烦事。


El mundo es un pañuelo.