最近写了一个关于 铁道部购票系统的若干文章
正好遇到一个博友,咨询了一个问题,这个问题正好可以作为分布式系统的数据一致性的简单例子,当然,这个只是比较简单的情况
现在先抛出问题,假设有一个主数据中心在北京M,然后有成都A,上海B两个地方数据中心,现在的问题是,假设成都上海各自的数据中心有记录变更,需要先同步到主数据中心,主数据中心更新完成之后,在把最新的数据分发到上海,成都的地方数据中心A,地方数据中心更新数据,保持和主数据中心一致性(数据库结构完全一致)。数据更新的消息是通过一台中心的MQ进行转发。
先把问题简单化处理,假设A增加一条记录Message_A,发送到M,B增加一条记录 MESSAGE_B发送到M,都是通过MQ服务器进行转发,那么M系统接收到条消息,增加两条数据,那么M在把增加的消息群发给A,B,A和B找到自己缺失的数据,更新数据库。这样就完成了一个数据的同步。
从正常情况下来看,都没有问题,逻辑完全合理,但是请考虑以下三个问题
1 如何保证A->M的消息,M一定接收到了,同样,如何保证M->A的消息,M一定接收到了
2 如果数据需要一致性更新,比如A发送了三条消息给M,M要么全部保存,要么全部不保存,不能够只保存其中的几条记录。我们假设更新的数据是一条条发送的。
3 假设同时A发送了多条更新请求,如何保证顺序性要求?
这两个问题就是分布式环境下数据一致性的问题
对于第一个问题,比较好解决,我们先看看一个tcp/ip协议链接建立的过程
我们的思路可以从这个上面出发,在简化一下,就一个请求,一个应答。
简单的通信模型是这样的
A->M : 你收到我的一条消息没有,消息的ID是12345
M->A: 我收到了你的一条消息数据,消息数据是ID;12345
这样就一个请求,一个应答,就完成了一次可靠性的传输。如果A一致没有收到M的应答,就不断的重试。这个时候M就必须保证幂等性。不能重复的处理消息。那么最极端的情况是,怎么也收不到M的应答,这个时候是系统故障。自己检查一下吧。
这么设计就要求,A在发送消息的时候持久化这个消息的数据内容,然后不断的重试,一旦接收到M的应答,就删除这条消息。同样,M端也是一样的。不要相信MQ的持久化机制,不是很靠谱的。
那么M给A发送消息也采取类似的原理就可以了。
下面在看看第二个问题,如何保持数据的一致性更新,这个还是可以参考TCP/IP的协议。
首先A发送一条消息给M:我要发送一批消息数据给你,批次号是10000,数据是5条。
M发送一条消息给A:ok,我准备好了,批次号是10000,发送方你A
接着A发送5条消息给M,消息ID分别为1,2,3,4,5 ,批次号是10000,
紧接着,A发送一个信息给M:我已经完成5小消息的发送,你要提交数据更新了
程序的设计模式往往与计算机的体系结构有很大关系,以函数作为协议的主要表现方式,语言具有简单严格的语法结构,应该与冯·诺依曼体系,或者更准确的说与代码线性循序执行的方式不无关系。
冯·诺依曼体系是图灵机的实现,但从实现之初,两者便无多大交集,图灵机具有理想性质,是不考虑控制和执行成本的,而冯·诺依曼机器,最初的程序设计对计算成本是非常关注的,而且按照图灵机思想设计的程序,转换成通常的程序,会比较复杂而且显得不直观。正如lex与bison生成的程序代码,我们只会认为程序是对的,而很少会去阅读。
以控制机器的思想设计程序,是图灵机程序设计的主要方式,而对于计算细节(主要是比较运算)和控制细节(主要是条件转移)的实现则基本不予关注。以机器的思想设计程序,以机器的思想而不是以库函数的方式构建系统的结构,其实在编译、业务逻辑表示、流程等很多方面都可以让问题得到简化。而用于执行的程序本身,则如数据一样,可以动态动态组装、动态执行。
在本质上冯·诺依曼体系与图灵机并无大的区别,但在形式上却有比较大的区别,可以这样说,图灵机侧重规则的描述,而冯·诺依曼体系编程则侧重于规则的执行,在计算机的计算能力受限制的过去,我们关注于执行,关注于算法。而今我们是否是更应关注于我们所要解决的问题,及其这些问题的上下文,以及解决这些问题相关知识的描述。
现在我们试着以图灵机的方式书写一个程序,掀开面纱,看看她的真容:
/*
因为真实的图灵机过于原始,为方便编程,在程序中做了以下约定:
1、图灵机有输入的条带,有输出的条带。
2、非特别指定,匹配动作只在输入条带上执行。
3、非特别指定,打印只在输出条带上执行。
4、预定义匹配: =,>,< .。
=,>,< 操作语义,与序列比较的含义一致。
. 不匹配条件。
5、预定义状态:s0, e0 ,f0
s0 表示开始状态。
e0 表示正常结束状态。如未有跟随的动作,则自动在输出条带上打印字符 '1'。
f0 表示非正常结束状态。如未有跟随的动作,则自动在输出条带上打印字符 '0'。
6、预定义动作: -> <- #
-> 表示条带指针右移一格。
<- 表示条带指针左移一格。
# 表示把输入条带当前指定的内容,打印到输出条带上,同时输出条带的指针右移。
? 对整个输出条带的数据,做相应的转换。
7、句型:
定义: <标志符> <参数> <参数> ...
句子: <当前状态> <匹配表达式> <下一状态> <动作> <动作> ...
--: 函数用 '--' 间隔
*/
//>=操作
!>= x
-------------
s0 x e0
s0 >x e0
--------------
//<=操作
!<= x
--------------------------
s0 x e0
s1 <x e0
-------------------------
//范围操作
!.. x y
---------------------------
s0 x e0
s0 y e0
s0 >x n1
n1 <y e0
---------------------------
//字母
!letter
--------------------------
s0 'a'..'z' e0
s0 'A'..'Z' e0
--------------------------
//数字
!digital
--------------------------
s0 '0'..'9' e0
--------------------------
//字母数字
!letterdigital
----------------------------
s0 letter e0
s0 digital e0
----------------------------
//标志符
!indent
----------------------------
s0 letter n1 -> #
n1 letterdigital n1 -> #
n1 . e0 <- ?string
----------------------------
//数字
!number
----------------------------
s0 digital n1 -> #
n1 digital n1 -> #
n1 '.' n2 -> #
n1 . e0 <- ?int
n2 digital n2 -> #
n2 'e' n3 -> #
n2 'E' n3 -> #
n3 digital n3 -> #
n2 . e0 <- ?double
n3 . e0 <- ?double
----------------------------
上面只是一个伪程序,但可以看出,这样的编程方式比较适合于以规则为主体的各种应用,比如编译、业务规则、业务流程等方面,同时作为自动机的控制输入也是比较合适的。
接下来可能发送两种情况
1 那么M发送消息给A:ok,我收到了5条消息,开始提交数据
2 那么M也可以发送给A:我收到了5条消息,但是还缺少,请你重新发送,那么A就继续发送,直到A收到M成功的应答。
整个过程相当复杂。这个也就是数据一旦分布了,带来最大的问题就是数据一致性的问题。这个成本非常高。
对于第三个问题,这个就比较复杂了
这个最核心的问题就是消息的顺序性,我们只能在每个消息发一个消息的序列号,但是还是没有最好解决这个问题的办法。因为消息接收方不知道顺序。因为即使给他了序列号,也没有办法告诉他,这个应该何时处理。最好的办法是在第二种方式的基础作为一个批次来更新。
这个只是以最简单的例子来说明一下分布式系统的要保证数据一致性是一件代价很大的事情。当然有的博主会说,这个何必这么复杂,直接数据库同步不就可以了。这个例子当然是没有问题的,万一这个几个库的模型都不一样,我发送消息要处理的事情不一样的。怎么办?
程序的设计模式往往与计算机的体系结构有很大关系,以函数作为协议的主要表现方式,语言具有简单严格的语法结构,应该与冯·诺依曼体系,或者更准确的说与代码线性循序执行的方式不无关系。
冯·诺依曼体系是图灵机的实现,但从实现之初,两者便无多大交集,图灵机具有理想性质,是不考虑控制和执行成本的,而冯·诺依曼机器,最初的程序设计对计算成本是非常关注的,而且按照图灵机思想设计的程序,转换成通常的程序,会比较复杂而且显得不直观。正如lex与bison生成的程序代码,我们只会认为程序是对的,而很少会去阅读。
以控制机器的思想设计程序,是图灵机程序设计的主要方式,而对于计算细节(主要是比较运算)和控制细节(主要是条件转移)的实现则基本不予关注。以机器的思想设计程序,以机器的思想而不是以库函数的方式构建系统的结构,其实在编译、业务逻辑表示、流程等很多方面都可以让问题得到简化。而用于执行的程序本身,则如数据一样,可以动态动态组装、动态执行。
在本质上冯·诺依曼体系与图灵机并无大的区别,但在形式上却有比较大的区别,可以这样说,图灵机侧重规则的描述,而冯·诺依曼体系编程则侧重于规则的执行,在计算机的计算能力受限制的过去,我们关注于执行,关注于算法。而今我们是否是更应关注于我们所要解决的问题,及其这些问题的上下文,以及解决这些问题相关知识的描述。
现在我们试着以图灵机的方式书写一个程序,掀开面纱,看看她的真容:
/*
因为真实的图灵机过于原始,为方便编程,在程序中做了以下约定:
1、图灵机有输入的条带,有输出的条带。
2、非特别指定,匹配动作只在输入条带上执行。
3、非特别指定,打印只在输出条带上执行。
4、预定义匹配: =,>,< .。
=,>,< 操作语义,与序列比较的含义一致。
. 不匹配条件。
5、预定义状态:s0, e0 ,f0
s0 表示开始状态。
e0 表示正常结束状态。如未有跟随的动作,则自动在输出条带上打印字符 '1'。
f0 表示非正常结束状态。如未有跟随的动作,则自动在输出条带上打印字符 '0'。
6、预定义动作: -> <- #
-> 表示条带指针右移一格。
<- 表示条带指针左移一格。
# 表示把输入条带当前指定的内容,打印到输出条带上,同时输出条带的指针右移。
? 对整个输出条带的数据,做相应的转换。
7、句型:
定义: <标志符> <参数> <参数> ...
句子: <当前状态> <匹配表达式> <下一状态> <动作> <动作> ...
--: 函数用 '--' 间隔
*/
//>=操作
!>= x
-------------
s0 x e0
s0 >x e0
--------------
//<=操作
!<= x
--------------------------
s0 x e0
s1 <x e0
-------------------------
//范围操作
!.. x y
---------------------------
s0 x e0
s0 y e0
s0 >x n1
n1 <y e0
---------------------------
//字母
!letter
--------------------------
s0 'a'..'z' e0
s0 'A'..'Z' e0
--------------------------
//数字
!digital
--------------------------
s0 '0'..'9' e0
--------------------------
//字母数字
!letterdigital
----------------------------
s0 letter e0
s0 digital e0
----------------------------
//标志符
!indent
----------------------------
s0 letter n1 -> #
n1 letterdigital n1 -> #
n1 . e0 <- ?string
----------------------------
//数字
!number
----------------------------
s0 digital n1 -> #
n1 digital n1 -> #
n1 '.' n2 -> #
n1 . e0 <- ?int
n2 digital n2 -> #
n2 'e' n3 -> #
n2 'E' n3 -> #
n3 digital n3 -> #
n2 . e0 <- ?double
n3 . e0 <- ?double
----------------------------
上面只是一个伪程序,但可以看出,这样的编程方式比较适合于以规则为主体的各种应用,比如编译、业务规则、业务流程等方面,同时作为自动机的控制输入也是比较合适的。