You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

123 lines
8.9 KiB
Markdown

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# 第27讲 | 如何制作游戏内容保存和缓存处理?
我们在打完游戏的关卡之后,需要保存游戏进度。单机游戏的进度都保存在本地磁盘上,如果是网络游戏的话该怎么办呢?这一节,我就来讲这个内容。
首先,我们要了解游戏内容的保存,需要先了解缓存处理。
为什么要了解缓存的处理呢?那是因为在大量用户的情况下,我们所保存的内容都是为了下次读取,如果每一次都从硬盘或者数据库读取,会导致用户量巨大数据库死锁,或者造成读取速度变慢,所以在服务器端,缓存的功能是一定要加上的。
## Redis不仅是内存缓存
缓存机制里有个叫Redis的软件。它是一种内存数据库很多开发者把Redis当作单纯的内存缓存来使用事实上这种说法并不准确Redis完全可以当作一般数据库来使用。
Redis是一种key-value型的存储系统。它支持存储的value类型很多包括字符串、链表、集合、有序集合和哈希类型。这些数据类型都支持push/pop、add/remove及取交集并集和差集及更丰富的操作而且这些操作都具有原子性。
Redis还支持各种不同方式的排序。为了保证效率数据一般都会缓存在内存中而Redis会周期性地把更新的数据写入磁盘或者把修改操作写入追加的记录文件并且在此基础上实现master-slave主从的同步。
说到Redis就不得不说缓存机制的老前辈Memcached。同样是缓存机制Memcached的做法是多线程非阻塞的IO复用的网络模型。
多线程分监听线程和工作子线程。监听线程监听网络连接接受请求了之后将连接描述字使用管道传递给工作线程进行读写。网络层的事件使用libevent封装。多线程模型可以发挥多核的作用。Memcached所有操作都要对全局变量加锁进行计数等工作所以会有性能损耗。
而Redis使用单线程IO复用模型自己封装了一个简单的事件处理框架对于单纯只有IO操作的模型来说单线程可以将速度优势发挥到最大但是Redis也提供了一些简单的计算功能比如排序、聚合等。
Redis还可以在某些场景下对关系数据库比如MySQL起到较好的补充作用。它提供了多种编程语言的接口开发人员调用起来也很方便。
Redis支持主从同步。通过配置文件可以将主服务器上的数据往任意数量的从服务器上同步从服务器A1也是主服务器BB是关联到其他从服务器B1B2的主服务器同时又是主服务器A的从服务器A1
这种做法就使得Redis可以执行单层的树结构的复制。Redis实现了发布/订阅publisher/subscriber的机制。所谓发布和订阅就是订阅者接收发布者的消息的时候发布者和订阅者都不用去管对方是什么状态只管各司其职就好了在这种状态下可以订阅一个频道并接收主服务器完整的消息发布记录。
## 编写Redis接口代码
我们尝试使用Python编写Redis接口的代码。
要使Python支持Redis编程必须安装一个包“redis”在使用的时候import一下。
```
import redis
```
然后我们开启Redis服务在Windows下可以运行redis-server.exe使用默认配置即可。
现在,我们尝试使用代码连接一下数据库服务,并且往数据库存放并取出、删除内容。
```
r = redis.Redis(host='127.0.0.1', port=6379, db=0)
r.set('foo', 'my_redis')
print r.get('foo')
r.delete('foo')
print r.dbsize(
```
运行结果为输出 my\_redis 和 0。
当然如果我们没有运行Redis则会抛出一个异常
![](https://static001.geekbang.org/resource/image/44/a3/44d41a80ab192dfa5514a25bb66c80a3.jpg)
r对象为连接Redis服务器的对象其中db=0表示使用 redis 的0号数据库可以随你喜欢切换为1号、2号等等。如果Redis设置了密码还可以在初始化的时候输入密码。
Redis的初始函数是这样定义的
```
__init__(self, host='localhost', port=6379, db=0, password=None, socket_timeout=None, connection_pool=None, charset='utf-8', errors='strict', decode_responses=False, unix_socket_path=None)
```
在之后的代码中r.set 表明将 key 为 foovalue为 my\_redis的内容写入数据库。
最后输出 0 号数据库的内容长度。
值得一提的是Redis对于存储的内容是来者不拒有什么扔什么所以你如果往Redis里插入二进制、UTF-8编码、图片等等任何东西都可以。理论上只要不超过内存大小的数据都可以往里面扔。
最后,我们可以这么写:
```
r.save()
```
强制Redis往硬盘里写入数据这样我们就能保证数据不会因为电脑发生异常而丢失。这样就将内存的数据同步了下来。
我们常说的木桶理论其实在这里也适用。比如电脑的速度取决于电脑设备中最慢的那个设备就像水在桶中的高度始终取决于水桶里面最下方的那个漏水处。而磁盘I/O始终是拖慢电脑速度的重要力量。
前面我们介绍了Redis所以我们可以使用Redis对文件进行缓存。Redis可以当作普通缓存也可以当作文件缓存在Redis中放入任何东西当然也包括放入二进制文件Redis也不会有任何异常出现从Redis缓存中取出二进制文件的速度也非常快因为是直接从内存中取出数据。
我们假设网络游戏保存下来的数据很大因为有人物属性、人物装备、地图NPC位置和怪兽等等。这些玩家退出后游戏保存的数据文件被保存在关系型数据库中或者保存在服务器硬盘的文件中。我们不可能每次都去读取关系数据库中的游戏内容或者硬盘文件内容所以可以用一种方案来存放游戏保存的文件和缓存。
## 如何存放文件和缓存?
这套机制并不局限于读取保存文件,某些大文件,或者数据文件的读取和缓存上,都可以使用这种思路去做。
首先我们假定文件存放在某一个目录所有的负载均衡服务器都存放有这个目录的副本其他分布式服务器存放其他文件和目录我们先暂定A服务器存放文件A1、A2、A3。
这些都是游戏的保存文件在服务器初始启动的时候Redis并不读取任何文件当有请求过来的时候服务器程序通知Redis读取某个文件。
这时我们需要一个机制为了保证服务器的内存开销也为了保证缓存速度我们必须保证被读取量最大的文件被缓存而不是所有文件这时候Python程序可以另开一个线程或者进程暂且命名为 T 线程,记录某文件被缓存。
服务器程序每次得到请求的时候都会将需要递交的被读取文件告诉Python线程T说文件 A1 被缓存了 N 次,文件 A2 被缓存了 N 次在这种策略下T线程通过几个小时或者几天的计数就能明确知道 比如A2 文件被递交次数最多于是它始终通知Redis将A2文件进行缓存而A1由于到了某一天递交次数下降在某一个时间节点上线程T就告知Redis A1文件可以从缓存文件中撤出来节省内存开销让位给读取频次更高更高的文件。
这样,一套完整的缓存计数和缓存的解决方案就出现了。
当然并不是说MySQL等关系型数据库不能做这些工作但从效率和开发成本来讲Redis缓存的开发成本和效率显然更胜一筹。因为在几十万几百万甚至上亿等级用户量的时候就算是Redis在这种量级的情况下也是吃不消的所以如果不在上层做更多层的缓存底层数据库一定是会死锁或者出现各种各样的问题。
那么你可能会说,我可以做索引啊,要知道在连接数足够多的时候,做索引、读写分离,主从数据库等方案,也只是救急只用,无法真正实现稳固的架构体系。
## 小结
我来总结一下今天的内容。
* Redis不仅仅可以用作普通的缓存机制使用也可以当作正常的数据库使用Redis也支持主从同步要按照应用场景不同来配置不同的Redis使用场景。
* 缓存机制不仅仅针对读取游戏保存文件这么一种方案,也可以用作各种数据文件的读取和写入操作。
* 使用现成的Redis等缓存数据软件是一个好的方案。而设计好的框架、好的缓存机制、好的网络模型是一款好网游必不可少的条件。
现在给你留一个小问题吧。
有没有可能将网络游戏的内容保存在客户端本地的电脑上,如果可以的话,请问如果玩家换了一台电脑,怎么同步内容呢?保存在客户端本地的意义是什么?
欢迎留言说出你的看法。