zoukankan      html  css  js  c++  java
  • web策略类游戏开发(三) 多线程下数据库并发更新的处理


    作者:Yahle
    曾用网名:Dogvane
    原载:http://www.cnblogs.com/yahle
    版权所有。转载时必须以链接形式注明作者和原始出处。



    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 多线程并发与互斥
    关于什么是多线程,以及多线程下面的同步及互斥的方法我这里就不过多的介绍了,相关内容可以到网上搜索,本文主要是讨论同步的时机以及避免死锁
    cnblogs.com相关主题

    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端执行。


     

  • 相关阅读:
    codevs 1569 最佳绿草

    luogu P3378 【模板】堆
    cogs 762. [USACO Open09] 奶牛队列
    各种 Python 实现的简单介绍与比较
    与 的区别
    Python3 print()函数sep,end,file参数用法练习
    python基础
    servlet篇 之 跳转问题
    servlet篇 之 servlet的访问
  • 原文地址:https://www.cnblogs.com/yahle/p/1088574.html
Copyright © 2011-2022 走看看