• 欢迎访问极客公园网站,WordPress信息,WordPress教程,推荐使用最新版火狐浏览器和Chrome浏览器访问本网站,欢迎加入极客公园 QQ群
  • Git主题现已支持滚动公告栏功能,兼容其他浏览器,看到的就是咯,在后台最新消息那里用li标签添加即可。
  • 最新版Git主题已支持说说碎语功能,可像添加文章一样直接添加说说,新建说说页面即可,最后重新保存固定连接,演示地址
  • 百度口碑求点赞啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊http://koubei.baidu.com/s/gitcafe.net
  • 如果您觉得本站非常有看点,那么赶紧使用Ctrl+D 收藏极客公园吧

Web策略类游戏开发(三):多线程下数据库并发更新的处理

未分类 博客教主 14年前 (2010-09-28) 2146次浏览 0个评论

1 多线程下数据库并发更新的处理

1.1 背景

不知道大家在玩《Travian》时有没有做过这样的事情:
同时打开多个集结点,并设定好要出发的士兵及数量,在快到压秒的时候,快速切换页面,不断的点确定,以确保游戏不会通讯问题导致压秒失败。

再看一个教科书里经常提到的数据库脏数据的案例:
A操作从表里获得数据D=10,在计算的时候,线程刚好进行切换,切换到B,B也需要操作D,并从数据库里取道值为10,在进行简单操作(D=D- 2)后将D=8的值写回数据库。B操作处理结束后,线程再切换回A操作,这时A在做自己的操作时,仍然采用先前取到D(10)的值,在进行一个简单操作 (D=D-1)后,仍然写回数据库。这时,数据库里的值变为9,而实际上D的值应该是7(D=10-2-1).。造成这个问题主要是因为CPU在执行A、 B操作时没有按照顺序来执行,而是让B抢先在A执行完之前执行,导致它们在计算D的时候,因为数据没有同步而发生写入脏数(A的数据覆盖了B的数据)据的 问题。
A操作伪代码:
{
D=GetDB()
D=D-1
SetDB(D)
}
B操作伪代码:
{
D=GetDB()
D=D-2
SetDB(D)
}
一个简单的图表表示它们的操作:

CPU A操作 B操作 数据库当前值
D=GetDB() 10
线程切换 D=GetDB() 10
D=D-2 10
SetDB(D) 8
线程切换 D=D-1 8
SetDB(D) 9

仿照第一个例子,当一位玩家同时打开多个页面点出兵进攻后,这些这些请求会同时到达服务器,服务器会根据这些Http请求创建相应的线程来处理进攻动作:
进攻的伪代码:
{
村庄士兵数量=GetDB()
if (村庄士兵数量 > 这次进攻士兵数量)
{
村庄士兵数量=村庄士兵数量-这次进攻士兵数量
SetDB(村庄士兵数量)
}
}
按照第二个脏数据的例子,应该很容易想到,我们在这次进攻的时候,很有可能派出了2只部队,但是只减少了1只部队的士兵。
多线程未同步是造成游戏bug的原因之一。

1.2 多线程并发与互斥
关于什么是多线程,以及多线程下面的同步及互斥的方法我这里就不过多的介绍了,相关内容可以到网上搜索,本文主要是讨论同步的时机以及避免死锁。

1.3 WebGame里多线程数据同步的方法

1.3.1 在asp.net下用lock进行加锁操作
在我们的WebGame里,采用asp.net的lock方法。具体就是在方法体里,用lock里锁住一个对象,使其它方法在访问这个对象的时候被阻塞。当第一个访问对象的线程退出并释放锁以后,其它的线程才能取消阻塞状态继续操作该对象。
我们通过修改上面进攻的伪代码,增加lock操作:
{
lock(锁定的对象)
{
村庄士兵数量=GetDB()
if (村庄士兵数量 > 这次进攻士兵数量)
{
村庄士兵数量=村庄士兵数量-这次进攻士兵数量
SetDB(村庄士兵数量)
}
}//unlock(锁对象)
}
CPU执行流程表:

CPU 线程A 线程B
lock(锁对象)
村庄士兵数量=GetDB()
if (村庄士兵数量 > 这次进攻士兵数量)
线程切换 lock(锁对象) 对象被锁,线程进入阻塞状态
线程切换 村庄士兵数量=村庄士兵数量-这次进攻士兵数量
SetDB(村庄士兵数量)
//unlock(锁对象)
线程切换 村庄士兵数量=GetDB() A线程释放锁,B线程被唤醒
if (村庄士兵数量 > 这次进攻士兵数量)
村庄士兵数量=村庄士兵数量-这次进攻士兵数量
SetDB(村庄士兵数量)

1.3.2 锁的粒度
上面的伪代码是对游戏里的逻辑代码进行了加锁处理,但只是简单的描述该方法体里需要进行加锁以及加锁的范围。但在实际的代码里,我们需要明确lock里锁定的对象。如果这个对象选择不合适,很有可能会造成性能损失或者死锁。

1.3.2.1 数据库锁
数据库锁是最一种简单的方法就,凡是在存在发生数据库写操作的代码里,都需要进行加锁处理,并同意用一个数据库锁对象。
这种方法使用简单,不容易造成死锁。当存在的问题也是明显,就是在发生写数据库操作时不能并发操作,这点特别是当用户访问量增大可能会造成一定的性能瓶颈。

1.3.2.2 村庄锁
为了提升写数据库的效率,我们必须解决锁粒度过大的问题,因此在我们的游戏系统里,对锁的粒度进行的细化,细化到村庄级别的对象。
在游戏一张村庄表对应是整个游戏里所有的村庄对象,而一个村庄对象在村庄表里只是一条记录。在使用数据库锁时,其实是告诉其它方法,现在我要写数据库,大 家都等一下,等我写好后再写。当我们将锁的对象细化到村庄(一条数据库表记录)的时候,实际是告诉数据库,我现在要修改XXX村庄,大家都别动它,但你要 修改YYY村庄我不管。

死锁
在前面虽然降低了锁的粒度,提高了数据库并发性能,但随之而来就很容易发生一个问题–死锁。
例如当两个村庄需要发生交易,这时我们需要同时修改A、B两个村庄对象,需要对其进行顺序锁定(先锁定A,再锁定B),这时候,又发生另外一个操作,也需要同时对A、B两个村庄进行锁定,恰巧这个锁定的顺序是B、A。这样就造成两个现在互相等待形成死锁。

中间变量解决死锁
对于死锁,在关于多线程介绍方面有很多解决方案,这里就不过多阐述。在WebGame里预防死锁,可以采用结合游戏的操作流程,对游戏处理流程及数据进行 拆分,来预防死锁问题。简单的来说,就是将涉及2个村庄修改的流程拆分为2个流程,并用中间变量予以表示,两个处理流程涉及变化的值都在中间变量里予以保 存。
以前面提到的交易为例,在游戏设计里,两个村庄在交易的时候,并不是瞬时交易,而是通过商人进行运输并交易。这样我们就以商人作为中间变量。
A<–>C<–>B
在上图里,交易开始,是由玩家触发交易事件,这时以村庄A为锁对象,进行锁定。C作为要交易资源从村庄A里被扣除。并将A修改后的数据回写到数据库。等过 了一段事件后,商人C到达了目的地,这时由系统触发后续的交易事件,这时以村庄B作为锁对象进行锁定。村庄B在获得资源后写回数据库,整个交易事件就算完 成了。
当然实际游戏的交易比上面的例子稍微复杂一点点,因为交易双方都有资源的减少与获得。完整的流程应该是需要锁定4次,并产生2个中间变量。
村庄A–>商人1–>村庄B
村庄A<–商人2<–村庄B

游戏里其它地方的锁定
基本上,凡是某个事件涉及到两个村庄修改的地方,都可以用上面的锁定的方法对处理流程进行修改。例如村庄A攻打村庄A。当然其它事件在数据修改方面只涉及1个村庄,那么就不需要怎么麻烦,直接对村庄加锁锁定即可。
好在在策略类的WebGame里涉及两个村庄的情况不多,涉及到的基本可以用中间变量对操作流程进行拆分,因此这种锁定方式在策略类WebGame还是比较合适的。当然实在不行也只能按照以往的预防死锁的方法进行处理

1.4 非Asp.net里同步的方案

1.4.1 Java
java在线程同步机制上与asp.net基本一致,因此上面所述的asp.net的方法也适合与java

1.4.2 php
了解不多,不过好像php没有线程锁与同步这个概念,如果直接通过语言环境进行同步可能比较困难。
不过在MySQL里,存在一个表锁定的方法,可以通过lock table的方法锁定表,不允许其它MySQL用户去进行操作。基本上和前面提到的数据库锁类一样,只不多执行方法的时候是在MySQL端执行。

作者:Yahle

极客公园 , 版权所有丨如未注明 , 均为原创丨本网站采用BY-NC-SA协议进行授权
转载请注明原文链接:Web策略类游戏开发(三):多线程下数据库并发更新的处理
喜欢 (0)

您必须 登录 才能发表评论!