Loading [MathJax]/jax/output/CommonHTML/config.js
前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >游戏服务器中常见的数据一致性问题分析

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

作者头像
韩伟
发布于 2021-03-15 12:05:07
发布于 2021-03-15 12:05:07
1.8K00
代码可运行
举报
文章被收录于专栏:韩伟的专栏韩伟的专栏
运行总次数:0
代码可运行

什么是一致性问题

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

譬如服务器上有一个怪物,玩家 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 对象这个锁,则必须等待。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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() 方法调用时,都必须“锁”住此对象,以便多个线程对此方法的调用,保证是“依次排队”处理的:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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 (也是访问地址)给调用者。
本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2021-03-03,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 韩大 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
12 经典游戏服务器端架构概述
现代电子游戏,基本上都会使用一定的网络功能。从验证正版,到多人交互等等,都需要架设一些专用的服务器,以及编写在服务器上的程序。因此,游戏服务器端软件的架构,本质上也是游戏服务器这个特定领域的软件架构。 软件架构的分析,可以通过不同的层面入手。比较经典的软件架构描述,包含了以下几种架构: 1.运行时架构——这种架构关心如何解决运行效率问题,通常以程序进程图、数据流图为表达方式。在大多数开发团队的架构设计文档中,都会包含运行时架构,说明这是一种非常重要的设计方面。这种架构也会显著的影响软件代码的开发效率和部署效率。本文主要讨论的是这种架构。 2.逻辑架构——这种架构关心软件代码之间的关系,主要目的是为了提高软件应对需求变更的便利性。人们往往会以类图、模块图来表达这种架构。这种架构设计在需要长期运营和重用性高的项目中,有至关重要的作用。因为软件的可扩展性和可重用度基本是由这个方面的设计决定的。特别是在游戏领域,需求变更的频繁程度,在多个互联网产业领域里可以说是最高的。本文会涉及一部分这种架构的内容,但不是本文的讨论重点。 3.物理架构——关心软件如何部署,以机房、服务器、网络设备为主要描述对象。 4.数据架构——关心软件涉及的数据结构的设计,对于数据分析挖掘,多系统协作有较大的意义。 5.开发架构——关心软件开发库之间的关系,以及版本管理、开发工具、编译构建的设计,主要为了提高多人协作开发,以及复杂软件库引用的开发效率。现在流行的集成构建系统就是一种开发架构的理论。
范蠡
2018/07/25
8K1
12 经典游戏服务器端架构概述
经典游戏服务器端架构概述 (1)
本文介绍了多进程模型在游戏服务器端开发中的实践,重点讲解了如何利用多进程模型实现游戏服务器的负载均衡、服务状态管理、无缝扩展和容灾备份等方面的技术和实现方式。
韩伟
2016/12/02
7.4K0
经典游戏服务器端架构概述 (1)
再谈游戏服务器架构
一、服务器划分原则 在现有的网络游戏服务器端架构中,多是以功能和场景来划分服务器结构的。负载均衡和集群暂且不在本文中讨论(bigworld、atlas)。服务器划分可以基于以下原则: 分离游戏中占用系统资源(cpu,内存,IO等)较多的功能,独立成服务器。 以多线程或多进程的编程方式适应多核处理器。 在同一个服务器架构下,应尽可能的复用某些服务器(进程级别的复用,比如场景服务器)。 运行时玩家数据的保存、修改及数据流向应该是设计的焦点,它同时也决定了服务器应该如何划分。 服务器的划分应该适度,在保
李海彬
2018/03/22
4.6K0
再谈游戏服务器架构
在游戏服务器中使用分布式事务
游戏业务通常有个特点是模块相关性非常高,模块之间的联动也非常密集且复杂。要保持各个相关模块的数据一致性,同时又兼顾效率和,没有一个通用的方法。通常的做法是走有损服务(也叫柔性服务)和自动修复的方式。比如支付服务一般的做法是在2PC的基础上增加redo log,对于发放和订单确认这两方,如果失败了会尝试几次补发。又或者好友系统或者公会,因为涉及多个对象的数据相互索引,一些做法是玩家在线的时候定期去检查数据是否正确,如果不正确走修复流程。
owent
2020/06/28
2.3K0
Java在游戏服务器开发中应用【面试+提高】
Java在游戏服务器开发中的应用 随着游戏市场的兴起,特别是网页游戏、手机游戏的崛起,对游戏开发技术的需求越来越多。网络游戏开发是一个庞大的体系,总体来说是客户端与服务器端。客户端是玩家接触的游戏图像显示端,服务器是处理游戏运行中的各种数据,由于一台服务器要支持众多玩家的请求,所以服务器的性能高低决定了同一个游戏的用户数量。 我们公司选择使用Java做服务器开发语言,主要原因是:1.Java是跨平台的,方便部署;2.Java是安全的高级语言,可以提高开发效率;3.Java是面向对象的,代码可以重用;4.Ja
Java帮帮
2018/03/12
2.1K0
Java在游戏服务器开发中应用【面试+提高】
经典游戏服务器端架构概述 (2)
根据文章内容总结的摘要
韩伟
2016/12/02
5.9K0
经典游戏服务器端架构概述 (2)
Golang语言社区--游戏服务器开发都要学什么
大家好,我是Golang语言社区(www.golang.ltd)主编彬哥,本篇给大家转载一篇关于游戏服务器开发都要学什么的文章;主要帮助初学者了解下游戏服务器都涉及到什么知识。
李海彬
2018/03/18
3.4K0
Golang语言社区--游戏服务器开发都要学什么
LollipopGo游戏服务器-数据一致性设计
本期课程给大家谈谈数据一致性,因为经常有同学问到,今天就给大家讲讲,数据一致性大致可分为三类:
李海彬
2020/04/07
1.2K0
LollipopGo游戏服务器-数据一致性设计
游戏服务器概述
(1)了解常见查找/排序算法的特点:利用算法来改善性能,胜于通过编译器选项、编程技巧;
Zoctopus
2018/08/03
4.9K0
游戏服务器概述
游戏服务器端有什么特别
在游戏服务器端开发所有要面对的问题中,有两个是最核心和最普遍的:一是和客户端的通讯;二是游戏登录用户的数据处理。
韩伟
2018/03/05
3.3K1
游戏服务器端有什么特别
教你从头写游戏服务器框架
大概已经有差不多一年没写技术文章了,原因是今年投入了一些具体游戏项目的开发。这些新的游戏项目,比较接近独立游戏的开发方式。我觉得公司的“祖传”服务器框架技术不太适合,所以从头写了一个游戏服务器端的框架,以便获得更好的开发效率和灵活性。现在项目将近上线,有时间就想总结一下,这样一个游戏服务器框架的设计和实现过程。
韩伟
2019/01/30
4.3K1
教你从头写游戏服务器框架
Wind分布式游戏服务器引擎的实现
Wind是一款面向云的高性能、高效率以及高扩展性的大型分布式游戏服务器引擎。Wind利用Python语言的简洁语法以及丰富的生态库来提高游戏业务的开发效率,针对一些对性能有要求的游戏业务功能(如实时战斗功能),Wind利用Golang的高并发特性来保证服务的高性能,同时Wind接入云的组件来保证游戏服务的动态扩展性,提高服务资源的利用率。
Wind09
2022/05/15
2.3K0
Wind分布式游戏服务器引擎的实现
Java中常用的锁介绍
  乐观锁是一种乐观思想,即认为读多写少,遇到并发写的可能性低,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,采取在写时先读出当前版本号,然后加锁操作(比较跟上一次的版本号,如果一样则更新),如果失败则要重复读-比较-写的操作。   Java中的乐观锁基本都是通过CAS操作实现的,CAS是一种更新的原子操作,比较当前值跟传入值是否一样,一样则更新,否则失败。
程序员云帆哥
2022/05/12
3560
游戏服务器的架构演进(完整版)
游戏服务器端,是一个会长期运行的程序,并且它还要服务于多个不定时,不定点的网络请求。所以这类软件的特点是要非常关注稳定性和性能。这类程序如果需要多个协作来提高承载能力,则还要关注部署和扩容的便利性;同时,还需要考虑如何实现某种程度容灾需求。由于多进程协同工作,也带来了开发的复杂度,这也是需要关注的问题。
曲水流觞
2020/07/13
5.7K0
游戏服务器的架构演进(完整版)
9 百万用户级游戏服务器架构设计
所谓服务器结构,也就是如何将服务器各部分合理地安排,以实现最初的功能需求。所以,结构本无所谓正确与错误;当然,优秀的结构更有助于系统的搭建,对系统的可扩展性及可维护性也有更大的帮助。
范蠡
2018/07/25
4.2K0
9 百万用户级游戏服务器架构设计
经典游戏服务器端架构概述(上)
架构的分析模型 一、 讨论的背景 现代电子游戏,基本上都会使用一定的网络功能。从验证正版,到多人交互等等,都需要架设一些专用的服务器,以及编写在服务器上的程序。因此,游戏服务器端软件的架构,本质上也是游戏服务器这个特定领域的软件架构。 软件架构的分析,可以通过不同的层面入手。比较经典的软件架构描述,包含了以下几种架构: 运行时架构——这种架构关心如何解决运行效率问题,通常以程序进程图、数据流图为表达方式。在大多数开发团队的架构设计文档中,都会包含运行时架构,说明这是一种非常重要的设计方面。这种架构也会显著的
韩伟
2018/03/05
3K0
经典游戏服务器端架构概述(上)
【专业技术】如何搭建游戏服务器?
存在问题: 手游越来越火了,听听业内人士的分析,他山之石,多多借鉴,那么手游的服务器到底如何搭建的? 解决方案: 从事游戏服务器开发差不多两年时间,两年间参与了不少项目,学到了很多游戏服务器开发技术,参与过几个不同架构的服务器开发,就随便聊聊游戏服务器开发需要的技术。(以下所指游戏服务器更偏向于手游,因为我对端游和页游开发接触并不多) 一.聊聊服务器开发有哪些东西要考虑。 1.开发语言的选择: 工欲善其事,必先利其器,选择一门适合的开发语法对后期开发有着事半功倍的作用。 业界主要的是c/c++ + Pyt
程序员互动联盟
2018/03/15
12.7K0
Redis在游戏服务器中的应用
排行榜 游戏服务器中涉及到很多排行信息,比如玩家等级排名、金钱排名、战斗力排名等。 一般情况下仅需要取排名的前N名就可以了,这时可以利用数据库的排序功能,或者自己维护一个元素数量有限的top集合。 但是有时候我们需要每一个玩家的排名,玩家的数量太多,不能利用数据库(全表排序压力太大),自己维护也会比较麻烦。 使用Redis可以很好的解决这个问题。它提供的有序Set,支持每个键值(比如玩家id)拥有一个分数(score),每次往这个set里添加元素, Redis会对其进行排序,修改某一元素的score后,也会
李海彬
2018/03/21
2.4K0
关于游戏服务器的服务拆分
先阐明一下观点,可以使用单体(单线程)应用程序解决的问题,都不应该使用分布式系统来解决,因为分布式真的很复杂。
重归混沌
2021/09/14
8970
游戏服务端究竟解决了什么问题?
当讨论到游戏服务端的时候,我们首先想到的会是什么?要回答这个问题,我们需要从游戏服务端的需求起源说起。
李海彬
2018/07/26
1.4K0
游戏服务端究竟解决了什么问题?
相关推荐
12 经典游戏服务器端架构概述
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
本文部分代码块支持一键运行,欢迎体验
本文部分代码块支持一键运行,欢迎体验