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 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 |
上面的这个实现在只有一个客户端的时候可以执行得很好。 但是, 当多个客户端同时对同一个键进行这样的操作时, 就会产生竞争条件。
举个例子, 如果客户端 A 和 B 都读取了键原来的值, 比如 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,那么五分钟之后,这条记录就会在内存中删除。这样不仅可以节省内存,而且增加了服务器的性能