单服务器架构下的多线程下数据库并发更新的处理方案

来源:互联网 发布:seo推广学院招聘 编辑:程序博客网 时间:2024/06/07 05:10

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

原创粉丝点击
热门问题 老师的惩罚 人脸识别 我在镇武司摸鱼那些年 重生之率土为王 我在大康的咸鱼生活 盘龙之生命进化 天生仙种 凡人之先天五行 春回大明朝 姑娘不必设防,我是瞎子 u盘文件夹被隐藏怎么办 u盘恶意隐藏文件怎么办 结婚7年没孩子怎么办 htc手机时间变了怎么办 下的视频没声音怎么办 ipad很卡反应慢怎么办 微信视频声音卡怎么办 苹果6s没有声音怎么办 靠上班工资太低怎么办 楼上太吵怎么办能报警 转页风扇有噪音怎么办 住的地方太吵怎么办 租的房子太吵怎么办 苹果手机音量键坏了怎么办 xp系统声音没了怎么办 柿子烂到窗台上怎么办 小窗户厨房太暗怎么办 抬东西把腰闪了怎么办 搬重东西后腰疼怎么办 闪了腰怎么办一动就疼 窗户的把手断了怎么办 窗户寸漏不了水怎么办 窗户打开关不上怎么办 新装修的房子有甲醛怎么办 橄榄核上油花了怎么办 虫子飞到耳朵里怎么办 手被虫子咬肿了怎么办 梦见牙掉出血该怎么办 黑户急需5万块钱怎么办 家里欠了好多钱怎么办 欠好多网贷我该怎么办 外面欠了很多钱怎么办 欠了好多网贷怎么办 欠那么多钱我该怎么办 急用钱怎么办谁给指条路 晚上睡不着觉怎么办白天又醒不来 胃疼了好几天怎么办 手机移动卡怎么办副卡 大学我好累我怎么办 感觉婚姻很累了怎么办 一个人的心累了怎么办