游戏开发到后期,随之而来的是各种性能测试调优。前两天看两年前写的数据库服务器,那叫一个囧,当初设计是单线程的,也就是所有的数据库操作请求放入一个队列中,逐个取出来做数据库查询操作。虽然mysql 的性能很赞,但要是这样游戏有个几万人在线,数据库服务器肯定吃不消。得益于innodb 的行锁支持,很多数据库的请求都是可以被并行处理的,比如两个角色A 、B 同时登录,进行登录验证并且获取角色列表请求,角色A 和B 数据是完全不相干的,所有可以并行处理。
后期数据库改动可是个大工程,不过为了性能还是忍了,狂改了一个星期,才把所有的数据库操作搞定。
数据库操作优化从两个方面入手:
1. 多个工作线程,并行处理 脏数据
2. 数据库操作API 用预处理方式(mysql_stmt_* 系列函数)
这里多个线程并行工作不用多说,用预处理方式操作数据库,在同一个连接上再执行相同的操作时,mysql 省去了解析sql 语句的工作,直接传入参数执行查询,对于大量复杂的相同查询,用预处理方式节约不少。
接下来讨论一下服务器的工作模式( 线程模型) 。考虑一下这样一种情况,同一个角色发送了两个请求,并且都是针对同一条数据操作,第一个写第二个读。那么为了保证数据的正确性,两个请求必须保证顺序执行。要是使用简单的生产者- 消费者模型,来一个请求,只要有空闲的工作线程就被处理,这种方式的话两个请求的执行顺序是不能保证的。这样我们必须保证同一个角色的数据请求会被顺序执行。
为了保证这一点,我们就得为每个请求加一个标记,比如角色ID ,这里称这个标记为阻塞项。用阻塞项让请求按照我们的期望顺序执行,为了避免线程竞争加锁带来的开销,可以采用boss-worker 模型,即由一个boss 线程来为其他worker 线程分配工作。
这样思路就清晰了,boss 线程从等待队列中顺序取请求,然后检查该请求是否可以被立即处理,若不可以则加入队列,等待再次调度,若可以则分配请求到一个空闲的worker 线程,分配工作后boss 线程更新当前正在被处理请求的阻塞项集合。
这里有个问题,当请求不能被立即调用时,我们先把它放入一个队列,取下一个请求再来进行相同的操作,那么什么时候再执行这些请求呢?当检查请求不能被立即执行的时候,其实我们是先跳过它,那么可以设个计数N ,从有一个请求被放入等待队列开始,再向后取N 条请求处理,这时再回过头来处理等待队列中的请求,注意处理等待队列中的请求时,boss 线程必须逐个为请求分配工作线程,若不满足执行条件,则boss 线程等待直到条件满足。
还有个细节问题如何维护正在执行的阻塞项集合?显然只有两种操作,添加和删除。当boss 线程分配工作给worker 线程时,需要添加阻塞项到阻塞项集合,这个操作不需要加锁;当worker 线程处理一个请求结束后,必须要将处理完的请求的阻塞项从集合里删除,为了避免加锁带来的开销,这个删除操作可以由boss 线程分配工作检查是否有空闲线程时,判断哪个线程的工作已经完成,然后删除对应的阻塞项。