Redis
是一个新兴的NoSql
数据缓存组件,与memcache
类似,但是功能却比memcache
多一些。
首先,Redis
和memcache
都是基于内存的,所以读取和写入速度都非常快。但是memcache
只支持简单的key-value
数据的存储方式,而Redis
对key-value ,hash,list,set,SortSet
等数据结构有很好的支持。
下面就Redis在游戏的开发应用中做一些简单的介绍。
数据的缓存
在这一点上,redis
和memcache
是一样的。都是把数据提前放入到内存中。当逻辑处理中需要用到数据时,先从内存中读取,相同的,写的时候也先向内存中写入,然后再操作数据库,以增加数据处理的速度。不同的是,redis
带有把数据写入到硬盘的功能,具体的写入策略可以在redis
的配置文件中配置。这样当主机突然出现故障时,比如断电,重启机器不会造成数据的丢失。这个在游戏的应用中特别重要。一般在游戏开发中,数据的处理会采用:缓存 + 持久化队列 + 数据库(mysql
)的架构。执行的流程是先把数据写入到缓存,然后把需要持久化的数据放入到持久化队列中,启动一个守护线程,从持久化队列中不断的取出数据,并存入或更新到数据库。如果使用memcache
这样没有写入到硬盘功能的缓存组件,出现故障时,持久人队列中如果还有没有处理完的数据,那么就会造成数据的丢失,引用玩家的数据出现短暂的回档。当然这些也可以自己开发一些功能去防止,但是增加了开发成本。
不丢失数据的持久化队列的实现
上面说过Redis
具有把数据写入到硬盘的功能,而且支持多种数据结构。那么就可以利用Redis
的list
实现持久化队列,而且当机器出现故障时,不会出现队列中数据丢失的情况,重启之后,数据会自动加载到redis
的list
之中。
具体实现方法:
(1)在
Redis
中构造一个list
存储
(2)一个线程使用Redis
的lpush
方法,向list
的左边加入数据
(3)另外一个线程使用Redis
的rpop
方法,从list
取出数据进行处理,并且从list
中删除了取出的数据。这样就实现了一个简单的生产者–消费者模式的队列
对并发操作的控制(跨房间)
一般来说,我们操作一个数据的流程是这样的,取出–处理—存储,这样在单线程中操作是没有任务问题的,但是在多线程环境中就不适用了,我们必须考虑数据同步的问题,保证数据操作的原子性。如果在游戏中,对玩家战队的属性进行更新,一般在数据库中都会保存一个TeamInfo
表,里面有玩家相应的属性,比如名字,等级,金币,钻石等等。在memcache
中保存一个TeamInfo
对象,这时玩家获得金币,我们就需要取出玩家所有的属性,然后set
金币,完成后再存储整个对象。这个时候就得考虑数据的同步了,如果在操作的时候,另外一个线程B
修改了钻石,并完成了存储,而这个时候我把金币修改完成之后,再存储,这时,就出现了数据混乱的结果。考虑数据同步无非也是加锁或乐观同步。不但增加了代码量,还增加了维护的难度。而在Redis
中,它支持对hash
数据结构的操作。我们可以把玩家的对象按每个字段存储到redis
的hash
中。
当我需要更新金币时,比如增加或减少,我可以使用Redis
自带的原子操作方法:hincrby(String key,String field,int value)
进行操作,value
是正为加,是负为减,这样就简化和避免了一些并发操作,而且这个操做还减少了对数据的操作步骤,因为没有取出,再操作的过程了,只有一次写入。而且在游戏中很少一次更新非常多的字段,如果有这样的情况,下面的方法可以解决。
对事务的支持
redis
提供了一个事务操作的机制,MULTI
命令用于开启一个事务,它总是返回OK
。MULTI
执行之后, 客户端可以继续向服务器发送任意多条命令, 这些命令不会立即被执行, 而是被放到一个队列中, 当EXEC
命令被调用时, 所有队列中的命令才会被执行。
另一方面, 通过调用DISCARD
, 客户端可以清空事务队列, 并放弃执行事务。
以下是一个事务例子, 它原子地增加了foo
和bar
两个键的值:
1 | > MULTI |
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 | val = GET mykey |
上面的这个实现在只有一个客户端的时候可以执行得很好。 但是, 当多个客户端同时对同一个键进行这样的操作时, 就会产生竞争条件。
举个例子, 如果客户端A
和B
都读取了键原来的值, 比如10, 那么两个客户端都会将键的值设为11, 但正确的结果应该是12 才对。
有了WATCH
, 我们就可以轻松地解决这类问题了:
1 | WATCH mykey |
使用上面的代码, 如果在WATCH
执行之后,EXEC
执行之前, 有其他客户端修改了mykey
的值, 那么当前客户端的事务就会失败。 程序需要做的, 就是不断重试这个操作, 直到没有发生碰撞为止。这种形式的锁被称作乐观锁, 它是一种非常强大的锁机制。 并且因为大多数情况下, 不同的客户端会访问不同的键, 碰撞的情况一般都很少, 所以通常并不需要进行重试。
缓存生命周期的控制
在游戏服务器中,为了节省性能,我们没有必要把所有玩家的信息都缓存到内存中。比如有一些不常登陆的玩家,那么他的信息就没必要一直呆在缓存中了,需要清除。Redis
为这个功能提供了一个方法:expire
,它可以为key
设置以秒为单位的生命周期,比如设置为300s,那么五分钟之后,这条记录就会在内存中删除。这样不仅可以节省内存,而且增加了服务器的性能
排行榜
游戏服务器中涉及到很多排行信息,比如玩家等级排名、金钱排名、战斗力排名等。
一般情况下仅需要取排名的前N名就可以了,这时可以利用数据库的排序功能,或者自己维护一个元素数量有限的top集合。
但是有时候我们需要每一个玩家的排名,玩家的数量太多,不能利用数据库(全表排序压力太大),自己维护也会比较麻烦。
使用Redis
可以很好的解决这个问题。它提供的有序Set,支持每个键值(比如玩家id
)拥有一个分数(score)
,每次往这个set
里添加元素,Redis
会对其进行排序,修改某一元素的score
后,也会更新排序,在获取数据时,可以指定排序范围。
更重要的是,这个排序结果会被保存起来,不用在服务器启动时重新计算。
通过它,排行榜的实时刷新、全服排行都不再成为麻烦事。