什么是一致性问题

在游戏服务器的开发中,我们经常会碰到所谓“一致性”问题,以及碰到各种为了解决这种问题所做的“方案”,那么,什么是一致性问题呢?其实非常简单,就是有两个客户端进程,都需要修改同一个数据,造成的问题。

譬如服务器上有一个怪物,玩家 A 释放了一个火球,根据业务逻辑,火球会扣减 10% 的最大 HP 值作为伤害;玩家 B 对怪物砍了一刀,扣减怪物的 HP 需要计算玩家 B 的攻击力和怪物的防御力。那么一般我们编写程序的时候,就会先从“怪物”和“玩家”读取其数值,包括“攻击力”,“最大 HP”,“防御力”,“现存 HP”这些数据,然后根据“火球术”和“刀砍”进行伤害计算,然后算出怪物遭受的伤害,以及受伤后应该剩下多少 HP。按上面的方法,就会有包含了 2 次“读”数据和“写”数据的过程。

如果这两次“先读后写”的操作,在并行的两个线程中执行,那么就会出现所谓“一致性问题”:先读了同一份数据,导致最终的操作互相覆盖了。下图是一个“增加数值”的一致性问题的描述,“203”是需要修改的数据的名字,这个数据的值一开始 100,又叫 key,A 进程试图进行的是“增加 10”这个操作,B 进程试图进行的是“增加 20”这个操作,但是如果同时执行,可能结果会是 110,或者是 120,但正确的结果应该是 130。

以上的问题,在一个进程内的多个线程中可能出现,在一个集群中的多个互相通信的进程也可能出现。为了解决以上的问题,人们想了很多方法,但是大多数可以分为两类:

  1. 悲观锁
  2. 乐观锁

下面具体说说这两类思路的实际常见的表现形式。

悲观锁

多线程同步锁

在 Java 语言中,有一个关键字叫 synchronized ,这个关键字可以加用括号来表示“锁”住的对象。下面的写法,表示执行下面 { ... } 的代码块时,必须尝试获得 obj 对象的“锁”,如果其他线程正在使用 obj 对象这个锁,则必须等待。

synchronized(obj) {int hp = obj.GetHp();hp += 10;obj.SetHp(hp);
}

对于 Java 来说,直接拿任何一个“对象”作为“锁”的标记都可以。这种做法,实际上是让多个线程,在执行某些代码的时候,“依次排队”执行,以避免“一致性问题”。在 Linux C 的 pthread 库里面,同样也有类似的 API 实现锁,都是针对多线程处理的。

异步模型

后来出现了以 Epoll 为代表的异步编程方式,这对于主要是网络 IO 造成阻塞的游戏服务器开发,带来了新的解决“一致性问题”的手段。由于不需要为每个 TCP 连接开一个线程,所以可以整个服务器就一个线程,依次处理每个到达服务器的网络数据请求。在这种编程模式下,由于来源的数据请求,本身就被 epoll 的处理方式,转换成一种“依次排队”的执行方式了,所以可以说是天然的上了一个锁,所有的需要并行处理的逻辑,都自动变成了串行处理。

元语

有一些团队,会喜欢使用 Redis 来处理一致性问题。尽管 Redis 自己也是单线程异步模式运行的,但如果仅仅使用其 get 和 set 命令,还是会造成同样的一致性问题。幸好 Redis 有一系列的“数据类型”,譬如:

  1. List 这个类型,就提供了 lpush 这个命令作为“插入队列”,这个命令本身,就是一种需要“先读后写”的任务,因为需要先读取队列的“头/尾”,然后写入数据。
  2. Zset 这个类型,提供的带排序功能的插入 zadd 命令,会先读数值,然后按写入位置,也是一种“先读后写”的操作。

这种处理方式,又可以被称为一种叫“元语”的方式。也就是说,把需要读写的多个操作,打包成一个命令来执行。如前文所说的“增加10”,“增加20”的操作,就可以设计成“+=”这样的一种元语。由于最终执行命令的程序,是一个单线程的模式,所以元语们,也被“依次排队”的执行了。

游戏服务器处理

在游戏服务器领域,这个方法更是一种“基本模型”:

  1. 我们会把游戏运行时所需要的数据,设计成存放在一个个游戏服务器进程的内存里
  2. 我们会设计很多所谓“ SS 协议”,也就是服务器进程之间的协议,每个 SS 协议,都是一种“元语”,这种协议的处理过程,往往都带有很复杂的,对数据的读写运算
  3. 当有业务逻辑需要处理的时候,我们把处理命令,以 SS 协议的方式,发送到“数据所在的进程”
  4. 数据所在进程,以单线程的方式,“依次排队”的处理所有的 SS 协议,实现了避免一致性问题

队列处理

有一些业务系统,会使用“消息队列”这种中间件,让处理的请求,天然的就以“队列”这种形态存在,这样以单线程“依次排队”消费队列里的消息,就会非常的自然。类似的消息队列中间件,在开源产品里也有很多,譬如 ActiveMQ,kafaka 等等。

在对数据持久化的情况下,为了同样的一致性问题,很多开发者也会专门编写一个类似 MySQL Proxy 之类的独立进程,专门把数据持久化操作,以队列的形式“依次排队”处理,尽管这样往往需要一些额外的开发,为逻辑上认定不会互相影响的数据,建立多个处理队列,以避免由于等待一个存储连接,导致严重的性能下降。实际上,在 MySQL 内部,也会有防止多个 SQL (在不同连接上)进行并发修改,而设计的“锁”,如古老的 MyISAM 表结构就是“表锁”,新的 InnoDB 表结构是“行锁”

总结

悲观锁的本质就是队列,也就是“依次排队”执行,不管这个队列,是由于多线程同步锁形成,还是异步 IO 系统内部实现的,还是专门设计的队列处理流程,都是一样的思想。

由于需要排队执行,所以如果没有认真规划那些一定要排队的操作,很容易造成性能的浪费,譬如多个线程在等一个锁,多个进程在等一个队列处理。而且,对于“队列”本身的处理,也会耗费额外的通信和协调的资源。异步编程模型,就是要求程序员,必须很清楚那些可能存在“等待”的操作,然后用回调或者事件查询的方式,来手工编程的切分开,但是这样也对程序员提出了更高的要求,毕竟每个函数、方法的调用,都必须知道这个调用是否会堵塞。

对于使用原语的系统,用什么方式定义原语是一个重要的问题,如 redis,天然提供了依附于某些数据结构的原语,但如果这些命令还满足的不了需求,就需要提供一种手段,让使用者自己定义这些原语,于是 redis 就开始支持 lua 脚本,编写自定义的命令。而对于游戏服务器开发,开发者们天天都在编写这种原语,其处理代码和业务逻辑本身就是一份代码。

乐观锁

乐观锁的基本处理方法,就是给每一次的读、写操作,都带上一个额外的数据:版本号。这个版本号,代表着数据被修改的次数。这样就能辨识出在某次写操作之前,此数据是否已经被其他线程/进程修改过。

这种处理方案,在每次写入操作的时候,会返回“是否成功”的结果,需要业务逻辑处理。一般来说,如果发生写入错误,就需要重新再读取数据,然后再处理后写入。这个“重试”的过程一般来说不复杂,但是,如果在特别频繁变化的数据上,这种“重试”多次都有可能会失败。幸好游戏服务一般都是“有损服务”,对于很多数据,是容忍一定程度上的失败和丢失的。

由于乐观锁提供了一种“通用”的一致性问题解决方案,所以特别适合在某些数据库、缓存中间件提供。但是缺点也很明显,就是需要使用者清醒的认识到,每一次写入都可能失败,需要预备失败的处理。对于特别复杂的逻辑来说,可能存在上百个需要修改的数据,编写这样的代码就会特别费劲。所以乐观锁也不应该用在“所有”的数据和处理逻辑上。

大部分的开发者,都还是比较倾向,对大多数比较方便进行分割的数据,分别存放在不同的进程上,然后用以“悲观锁”的策略进行处理。而对于不变分割的数据,采用乐观锁的策略进行处理。

远程对象系统

悲观锁在开发上的表现形式有很多种,但是基本上都离不开需要锁的“数据”和操作数据的“方法”,这和面向对象概念中的“对象”,“方法”不谋而合。正如 Java 语言,可以使用以下方式对方法加锁,表示任何一个线程,在对一个 Cat 类对象的 Eat() 方法调用时,都必须“锁”住此对象,以便多个线程对此方法的调用,保证是“依次排队”处理的:

class Cat {private int hp_ = 100;public Player() {}synchronized public void Eat(int eng) {hp_ += eng;}
}

其实任何游戏服务器中的对象,都可以类似的形式进行加锁——如果我们的处理逻辑是单线程的,那么所有的“方法”都会是“依次排队”执行的。如果我们能自动把 SS 协议原语,映射到特定对象的方法上,那么就可以非常自然的把悲观锁实现成“对远程对象的方法调用”这种形态了。

尽管上述方法,用“对象的方法”包装了悲观锁的概念,但是如果需要修改的数据无法被定位在一个进程内,那么可能需要使用乐观锁的概念,来实现另外一种更通用的数据修改方法。同样,我们可以采用“对象”的模型来包装:getter/setter——对于对象属性的存取器方法。我们可以让所有的存取器的都自动的带上“乐观锁”的特性,让远程方法自动处理。

基于乐观锁的设计,对于 setter 方法的调用,就有可能返回错误,然后需要业务逻辑自己处理。如此,我们就可以通过一种编程模型,统一乐观锁和悲观锁两种数据一致性问题处理方法:

  1. 定义一般的远程方法,会以悲观锁的方式执行
  2. 定义特殊的属性存储器,以乐观锁的方式执行

最后的问题,就是如何实现一个“远程对象的方法调用”,这里给出几个需要重点处理的问题:

  1. 远程对象如何在集群中(一批进程)中表示。这种表示方式也是远程调用的地址。我们可以通过一个 32/64 位的整数来表达,也可以通过设计某种容量更大的数据结构。这个地址都需要集群系统懂得如何快速的路由到对应的进程上。
  2. 远程对象的建立和销毁应该如何处置。
    1. 一种方法是先定义一个“远程函数”的系统,先通过服务器进程 ID 的表达,然后通过这种远程函数进行对象建立/销毁。
    2. 另外一种方法,是预先以某种配置方式,自动建立对象。任何一个客户端进程,都可以向集群任何节点发起“建立对象”的请求,然后集群自动根据预定义规则建立对象,返回对象 ID (也是访问地址)给调用者。

游戏服务器中常见的数据一致性问题分析相关推荐

  1. 修改游戏服务器中的数据,修改游戏服务器中的数据库

    修改游戏服务器中的数据库 内容精选 换一换 业界对备份一致性的定义包括如下三类:不一致备份:备份的文件.磁盘不在同一个时间点.云备份中的云服务器备份提供对弹性云服务器和裸金属服务器的基于多云硬盘一致性 ...

  2. Redis在游戏服务器中的应用

    https://www.cnblogs.com/agent-k/p/Redis.html Redis在游戏服务器中的应用 Agent`K 最近在使用Redis,忽然发现以前很多费神的事情都迎刃而解了, ...

  3. 怎样修改游戏服务器里的数据库,修改游戏服务器中的数据库

    修改游戏服务器中的数据库 内容精选 换一换 业界对备份一致性的定义包括如下三类:不一致备份:备份的文件.磁盘不在同一个时间点.崩溃一致性备份:崩溃一致性备份会捕获备份时磁盘上已存在的数据,文件/磁盘数 ...

  4. 什么鬼?我能通过依赖混淆攻击在 Halo 游戏服务器中执行命令,微软不 care?!...

     聚焦源代码安全,网罗国内外最新资讯! 编译:奇安信代码卫士 研究员可以利用依赖混淆攻击在 Halo Microsoft 服务器中执行命令,但微软安全响应中心 (MSRC) 表示并非问题. 神秘依赖关 ...

  5. 3D游戏引擎中常见的三维场景管理方法

    对于一个有很多物体的3D场景来说,渲染这个场景最简单的方式就是用一个List将这些物体进行存储,并送入GPU进行渲染.当然,这种做法在效率上来说是相当低下的,因为真正需要渲染的物体应该是视椎体内的物体 ...

  6. 「游戏」游戏服务器中AOI的原理及四叉树实现

    前言 要不是想起来这篇文章想写一个关于游戏服务器开发过程中关于AOI相关的文章,我都差不点忘了我是一个游戏服务器开发人员

  7. Db2数据库中常见的堵塞问题分析与处理方法

    Db2 数据库堵塞怎么办 作为一个数据库管理员,工作中经常会遇到的一个问题:当数据库出现故障的情况下,如何快速定位问题和找到解决方案.尤其是在运维非常重要系统的时候,解决问题恢复服务是分秒必争.Db2 ...

  8. 游戏服务器——中心服

    一.中心服介绍 中心服用于应用统一调度中心和数据脚本发放中心. 一 服务器信息调度:DB Server第一个启动,随后中心服启动,中心服启动前会和DB Server进行连接,之后其他服务器(网关服.登 ...

  9. 计算机中常见乱码原因及分析

    弄懂计算机中的进制和编码转换及乱码 第四节:计算机中的进制和编码 4.1 二进制 4.2 计算机中的单位 4.3 32位和64位系统的区别 4.4 字符编码基础知识 4.5 字符编码介绍 4.5.1 ...

最新文章

  1. C#类方法中使用数组参数params关键字的作用
  2. react 父子组件传值
  3. phpstudy+dvwa搭建
  4. 驱动级的自动按键_茶陵定做自动伸缩门定制,防火门厂家直销-湖南富扬门窗
  5. 小程序 - 学习笔记
  6. Highcharts 本地导出图片 Java
  7. android 调用restful,android调用springmvc写的restful
  8. mysql-索引操作
  9. 学习写DSHOW 框架下的FILTER 之一
  10. Drupal 修复远程代码执行漏洞
  11. Node.js中使用pipe拷贝大文件不能完全拷贝的解决办法
  12. 【Mendeley】自定义文献引用格式(国标GB2005)
  13. hadoop配置启动historyserver
  14. Struts1和Struts2对比
  15. RCLAMP0524P超低电容TVS二极管阵列,DFN-10L封装
  16. 在Outlook中更改签名
  17. ShowType=0,● 交换机命令show interfaces type 0/port_# switchport|trunk用于显示中继连接的配置情况,下面 - 赏学吧...
  18. p 值的意义是什么?终于有人讲明白了
  19. 汽车变速系统的换挡律
  20. Java新手小白入门篇 Java项目的构建

热门文章

  1. Docker折腾记: (3)Docker Compose构建Gitlab,从配置(https,邮箱验证)到基本可用
  2. android编辑keynote,Keynote文本如何编辑?文本编辑方法图文分享
  3. Keepalive高可用 漂移
  4. 【中标通知】塔望咨询中标新疆农发集团 品牌规划建设项目
  5. 降噪耳机哪个牌子好?2022年高性价比降噪蓝牙耳机
  6. OPPO Watch与vivo Watch,哪个更值得入手?
  7. 女生,能为程序员男友做点什么吗?
  8. 计算机视觉:图片的边缘检测、映射和油画效果
  9. 移动端开发touchstart,touchmove,touchend事件详解使用
  10. 英语口语练习:点饮料