Java全能学习+面试指南:https://javaxiaobear.cn
今天我们聊一聊分布式数据存储,知识架构图如下所示。
主要包括以下内容:
MySQL 复制,有主从复制和主主复制两种;
数据分片(或称数据分区),具体为数据分片的原理、分片的方案、分片数据库的扩容;
数据库分布式部署的几种方案;
NoSQL 中的 CAP 原理,分布式系统的最终一致性及其实现方案。
MySQL 的主从复制,顾名思义就是将 MySQL 主数据库中的数据复制到从数据库中去。主要目的是实现数据库读写分离——写操作访问主数据库,读操作访问从数据库,从而使数据库具有更强大的访问负载能力,支撑更多的用户访问。
如下图所示,它的主要的复制原理是当应用程序客户端发送一条更新命令到数据库的时候,数据库会把这条更新命令同步记录到 Binlog 中,然后由另外一个线程从 Binlog 中读取这条日志,再通过远程通讯的方式将它复制到从服务器上面去,从服务器获得这条更新日志后,将其加入到自己的 Relay log 中,然后由另外一个 SQL 执行线程从 Relay log 中读取这条新的日志,并把它在本地的数据库中重新执行一遍,这样当客户端应用程序执行一个 update 命令的时候,这个命令会在主数据库和从数据库上同步执行,从而实现了主数据库向从数据库的复制,让从数据库和主数据库保持一样的数据。
MySQL 的主从复制是一种数据同步机制,除了可以将一个主数据库中的数据同步复制到一个从数据库上,还可以将一个主数据库上的数据同步复制到多个从数据库上,也就是所谓的 MySQL 的一主多从复制。
如下图所示,多个从数据库关联到主数据库后,将主数据库上的 Binlog 日志同步地复制到了多个从数据库上,通过执行日志,每个从数据库的数据都和主数据库上的数据保持了一致。这里面的数据更新操作表示的是所有数据库的更新操作,除了 SELECT 之类的查询读操作以外,其他的 INSERT、DELETE、UPDATE 这样的 DML 写操作,以及 CREATE TABLE、DROPT ABLE、ALTER TABLE 等 DDL 操作都可以同步复制到从数据库上去。
一主多从复制有四大优点:分摊负载、专机专用、便于冷备、高可用。
将只读操作分布在多个从数据库上,从而将负载分摊到多台服务器上。一般说来,对数据库做主从复制的时候,通常是为了进行读写分离,数据库的写操作会连接到主数据库上进行,而数据的读操作则连接到从服务器上进行,这样把读写两部分操作分别交给不同的服务器去处理,降低数据库的负载压力,而使用更多的从数据库向应用程序提供读服务,更好地减轻了整个数据库的访问压力。
可以针对不同类型的查询,使用不同的从服务器。比如有一台服务器专门用来做应用程序的读操作,另一个服务器专门用来执行数据报表类的操作,还有的服务器可能专门执行数据备份。通过这样的方式,将不同的操作连接到不同的从数据库上,从而实现了专机专用,也进一步改善了用户的体验。
即使数据库进行了一主多从的复制,在一些极端的情况下,比如整个机房遭遇了火灾、地震或其它一些极端灾害,也可能会导致整个数据中心的数据服务器都丢失。代码可以重新部署,但是数据库是我们过往的宝贵资源。如果历史数据都丢失了,用户信息、商品数据、订单数据……所有的数据永远丢失,那么即使程序和代码都在,整个系统也永远都不可恢复了。这种损失是一个系统不能够承受的,更是一个公司不能承受的。
所以通常说来很多公司会对数据做冷备,即把历史上的数据全部备份下来,把它存储到另外一个地方,甚至锁在保险柜里。当发生了极端灾害的时候,所有的数据都丢失的时候,还可以通过这些冷备的数据重新恢复整个网站。但是进行冷备的时候有一个困难点在于,数据库如果正在进行写操作,冷备的数据就可能不完整,数据文件可能处于损坏状态。以前的做法是停机冷备,也就是停止数据库的写操作,将数据文件拷贝出去后,再重新打开数据访问。而现在使用一主多从的复制就就可以实现零停机时间的备份。只需要关闭数据的数据复制进程,文件就处于关闭状态了,然后进行数据文件拷贝,拷贝完成后再重新打开数据复制就可以了。
如果一台服务器宕机了,只要不发请求给这台服务器就不会出问题。当这台服务器恢复的时候,再重新发请求到这台服务器。所以,在一主多从的情况下,某一台从服务器宕机不可用,对整个系统的影响是非常小的。
但是一主多从只能够实现从服务器上的这些优点,当主数据库宕机不可用的时候,数据依然是不能够写入的,因为数据不能够写入到从服务器上面去,从服务器是只读的。
为了解决主服务器的可用性问题,我们可以使用 MySQL 的主主复制方案。所谓的主主复制方案是指两台服务器都当作主服务器,任何一台服务器上收到的写操作都会复制到另一台服务器上。
如下图所示,来看主主复制原理。当客户端程序对主服务器 A 进行数据更新操作的时候,主服务器 A 会把更新操作写入到 Binlog 日志中,然后 Binlog 会将数据日志同步到主服务器 B,写入到主服务器的 Relay log 中,然后执行 Relay log,获得 Relay log 中的更新日志,执行 SQL 操作写入到数据库服务器 B 的本地数据库中。B 服务器上的更新也同样通过 Binlog 复制到了服务器 A 的 Relay log 中,然后通过 Relay log 将数据更新到服务器 A 中,通过这种方式,服务器 A 或者 B 任何一台服务器收到了数据的写操作都会同步更新到另一台服务器,实现了数据库主主复制。主主复制可以提高系统的写可用,实现写操作的高可用。
那么,当使用 MySQL 服务器实现主主复制时,数据库服务器失效该如何应对?
如下图所示,正常情况下用户会写入到主服务器 A 中,然后数据从 A 复制到主服务器 B 上。当主服务器 A 失效的时候,写操作会被发送到主服务器 B 中去,数据从 B 服务器复制到 A 服务器。
再具体看一下主主失效的维护过程,如下图。
最开始的时候,所有的主服务器都可以正常使用,当主服务器 A 失效的时候,进入故障状态,应用程序检测到主服务器 A 失效,检测过程可能需要几秒钟或者几分钟的时间,然后应用程序需要进行失效转移,将写操作发送到备份主服务器 B 上面去,将读操作发送到 B 服务器对应的从服务器上面去。一段时间后故障结束,A 服务器需要重建失效期间丢失的数据,也就是把自己当作从服务器去从 B 服务器上面同步数据,同步完成后系统才能恢复正常。这个时候 B 服务器是用户的主要访问服务器,A 服务器当作备份服务器。
使用 MySQL 进行主主复制的时候需要注意如下事项。
两个服务器对同一条记录进行写操作,互相进行数据复制的时候,数据库就不知道哪条数据是正确的。
因为数据复制后,所有的数据库存储的数据都是一样的,不管是主主复制,还是主从复制。如果存储资源不足、磁盘不够大,数据复制后,即使写到多个服务器上,存储依然是不够用的。同时,它也没有增加写并发能力,即使使用主主复制,应用程序一个时间内也只能向一个数据库写入。
比如要在一张表中增加一个字段,要执行一个 ALTER TABLE 操作,这个操作会导致同步延迟巨大,因为该操作会阻塞其它的 Binlog 日志同步,这时候主数据库的很多写操作都无法同步到从数据库上面去,导致数据不一致。所以在实践中需要更新表结构的操作,不要写入到 Binlog 中,也就是关闭更新表结构的 Binlog。如果要对表结构进行更新,应该有运维工程师 DBA 对所有主从数据库分别手动进行数据表结构的更新操作。
再看数据分片,上面提到过数据复制只能提高数据读并发操作能力,并不能提高数据写操作并发的能力以及数据整个的存储容量,也就是并不能提高数据库总存储记录数。如果我们数据库的写操作也有大量的并发请求需要满足,或者是我们的数据表特别大,单一的服务器甚至连一张表都无法存储,那怎么办?解决方案就是数据分片。
数据分片的主要目标是,将一张数据表切分成较小的片,不同的片存储到不同的服务器上面去,通过分片的方式使用多台服务器存储一张数据表,避免一台服务器记录存储处理整张数据表带来的存储及访问压力。
分片的主要特点是数据库服务器之间互相独立,不共享任何信息,即使有部分服务器故障,也不影响整个系统的可用性。另一个特点是通过分片键定位分片,也就是说一个分片存储到哪个服务器上面去,到哪个服务器上面去查找,是通过分片键进行路由分区算法计算出来的。在 SQL 语句里面,只要包含分片键,就可以访问特定的服务器,而不需要连接所有的服务器,跟其他的服务器进行通信。
分片的主要原理是将数据以某种方式进行切分,通常就是用刚才提到的分片键的路由算法。通过分片键,根据某种路由算法进行计算,使每台服务器都只存储一部分数据。
如下图示例,通过应用程序硬编码的方式实现数据分片。假设我们的数据库将数据表根据用户 ID 进行分片,分片的逻辑是用户 ID 为奇数的数据存储在服务器 2 中,用户 ID 为偶数的数据存储在服务器 1 中。那么,应用程序在编码的时候,就可以直接通过用户 ID 进行哈希计算,通常是余数计算。如果余数为奇数就连接到服务器 2 上,如果余数为偶数,就连接到服务器 1 上,这样就实现了一张用户表分片在两个服务器上。
这种硬编码主要的缺点在于,数据库的分片逻辑是应用程序自身实现的,应用程序需要耦合数据库分片逻辑,不利于应用程序的维护和扩展。
一个简单的解决办法就是将映射关系存储在外面。如下图所示,应用程序在连接数据库进行 SQL 操作的时候,通过查找外部的数据存储查询自己应该连接到哪台服务器上面,然后根据返回的服务器的编号,连接对应的服务器执行相应的操作。在这个例子中,用户 ID 33 查找服务器是 2,用户 ID 94 查找服务器也是 2,它们根据查找到的用户服务器的编号,连接对应的服务器,将数据写入到对应的服务器分片中。
通过这种手段实现数据分片的主要挑战在于,第一需要额外的代码,业务逻辑因此变得复杂。另一个就是无法执行分片的联合操作,也就是执行查询操作的时候,只能在一个分片上进行。多个分片多张表进行 join 联合查询,这种操作,分片技术是无法实现的。还有个问题在于无法使用数据库的事务。我们知道现在的数据库事务是通过数据库自身的日志实现,数据库将多个更新操作,当做一个事务来执行,要么全部完成,要么完全不执行。但是如果是通过分片的方式,不同的数据操作落在不同的服务器上,不同的服务器之间是无法实现所有的更新操作全部完成或者全部不执行的,因此也就无法使用数据库的事务。还有一个挑战就是系统的数据量是逐渐增加的,在增加的过程中,如果服务器不够用了,分片不够用了,需要扩大分片的时候,如何增加分片,增加服务器?
现在有一些专门的分布式数据库中间件来解决上述这些问题,比较知名的有 Mycat。Mycat 是一个专门的分布式数据库中间件,应用程序像连接数据库一样去连接 Mycat,而数据分片的操作完全交给了 Mycat 去完成。在下图所示的例子中,有 3 个分片数据库服务器——数据库服务器 dn1、dn2 和 dn3,它们的分片规则是根据 prov 字段进行分片。那么,当我们执行一个查询操作“select * from orders where prov=‘wuhan’”的时候,Mycat 会根据分片规则将这条 SQL 操作路由到 dn1 这个服务器节点上。dn1 执行数据查询操作返回结果后,Mycat 再返回给应用程序。通过使用 Mycat 这样的分布式数据库中间件,应用程序可以透明地、无感知地使用分片数据库。同时,Mycat 还一定程度上支持分片数据库的联合 join 查询以及数据库事务。
下面来看分片数据库如何进行扩容伸缩。一开始,数据量还不是太多,两个数据库服务器就够了。但是随着数据的不断增长,可能需要增加第三个、第四个、第五个,甚至更多的服务器。在增加服务器的过程中,分片规则需要改变。分片规则改变后,以前写入到原来的数据库中的数据,根据新的分片规则,可能要访问新的服务器,所以还需要进行数据迁移。
不管是更改分片的路由算法规则,还是进行数据迁移,都是一些比较麻烦和复杂的事情。因此在实践中通常的做法是数据分片使用逻辑数据库,也就是说一开始虽然只需要两个服务器就可以完成数据分片存储,但是依然在逻辑上把它切分成多个逻辑数据库。
在下图所示的例子中,我们将数据库切分成 32 个逻辑数据库,但是开始的时候只有两个物理服务器,我们把 32 个数据库分别启动在两个物理服务器上面。那么,路由算法就还是按照 32 进行路由分区,数据分片也是 32 片。当两台服务器不能够满足数据存储和访问要求的时候,我们只需要简单地将这些逻辑数据库迁移到其他的物理服务器上就可以完成扩容。因为迁移后数据分片还是 32 片,数据分片的算法不需要改变。数据迁移也仅仅是将逻辑数据库迁移到新的服务器上面去,而这种迁移通过数据库的主从复制就可以完成。
那么,实践中,数据库的部署方案有哪些?
最简单的就是单一服务和单一数据库。应用服务器可能有多个,但是它们完成的功能是单一的功能。多个完成单一功能的服务器,通过负载均衡对外提供服务。它们只连一台单一数据库服务器,这是应用系统早期用户量比较低的时候的一种架构方法,如下图所示。
如果对系统的可用性和对数据库的访问性能提出更高要求,就可以通过数据库的主从复制进行初步的伸缩。通过主从复制,实现一主多从。应用服务器的写操作连接主数据库,读操作从从服务器上进行读取,如下图所示。
随着业务更加复杂,为了提供更高的数据库处理能力,可以进行数据的业务分库。数据的业务分库是一种逻辑上的,是基于功能的一种分割,将不同用途的数据表存储在不同的物理数据库上面去。
在下图的例子中,我们有产品类目服务和用户服务,两个应用服务器集群,对应地,我们将数据库也拆分成两个,一个叫作类目数据库,一个叫作用户数据库。每个数据库依然使用主从复制。通过业务分库的方式,我们在同一个系统中,提供了更多的数据库存储,同时也就提供了更强大的数据访问能力,同时也使系统变得更加简单,系统的耦合变得更低。
而更复杂的综合部署方案,则是根据不同数据的访问特点,使用不同的解决方案进行应对。比如类目数据库,也许通过主从复制就能够满足所有的访问要求,但是如果用户量特别大,进行主从复制或主主复制,还是不能够满足数据存储以及写操作的访问压力,这时候就可以对用户数据库进行数据分片存储了,同时每个分片数据库也使用主从复制的方式进行部署,如下图所示。
像 MySQL 这类关系数据库,在历史上使用比较广泛,但是随着互联网业务的发展,出现了一种新的数据库叫作 NoSQL 数据库。这种数据库和传统的关系数据库不同,它的主要访问方式也不再使用 SQL 进行操作,所以被称作 NoSQL 数据库。NoSQL 数据库主要是解决大规模分布式数据的存储问题。
对于大规模的数据分布式存储有一个著名的 CAP 原理。CAP 原理是说,对于一个分布式系统,它不能够同时满足一致性(C)、可用性(A)以及分区耐受性(P)这三个特点。其中,一致性是说,任何时候集群中所有的数据的备份都是一致的;可用性是说,当分布式集群中某些服务器节点失效的时候,集群依然是可用的;分区耐受性是说所当网络失效的时候,节点无法通信的时候,系统依然是可用的。而 CAP 原理就是说这三者无法同时满足。通常一个分布式应用系统,可用性是必不可少的,而分区耐受性也是需要满足的。因此在现实中,很多系统是通过对数据的一致性做文章,来提供一个满足要求的分布式系统的。
我们先看一下数据是如何不一致的。如下图,假如说有一个分布式集群,有 ABC3 个服务节点,对于客户端 1,如果执行某个操作,对 ID 55 执行价格等于 99 的 update 操作,这个操作执行在节点 A 上。同时有另一个客户端,对 ID 55 执行价格等于 75 的操作,这个操作执行在节点 B 上。正常情况下 3 个节点之间互相通信,会把 ID 等于 55 的数据同步给其他的节点,三个节点存储的数据一致。但是当网络通信以及节点失效的情况下,这种数据同步就无法完成,就会导致节点 A 和节点 B 上对同一个 ID 存储的数据是不同的,这个时候如果有其他的应用程序连接数据存储集群,并且它们连接的分别是节点 A 和节点 B,那么得到的结果也是不同的,这就会导致数据的不一致。
那么,既然 CAP 原理说数据的一致性、可用性和分区耐受性是无法同时满足的,而可用性和分区耐受性我们通常又是不得不保证的。那么更多的时候,我们通过解决一致性的问题,来实现分布式系统。
解决一致性冲突的方案就是实现最终一致性。一致性是说,在任何时间,数据的多个备份存储都是一致的。而最终一致是说,在一个分布式系统中,在某个时候,不同服务器上存储的同一个数据可能是不一致的,但是它最终还是一致的,只要不一致的时间不影响应用程序的正确性,我们就是可以接受的,这种一致性叫作最终一致性。
比如说,如下图,客户端 A 连接到服务器 1,客户端 C 连接到服务器 2,客户端 A 对数据作出变更操作,服务器 1 将变更扩散到服务器 2,那么,客户端 C 连接到服务器 2 的时候,可能不会立即得到最新的数据,但是过一会儿,等变更扩散完成了,就可以获得最新的数据了。
当数据在写的过程中多个数据备份有冲突的时候,如何解决呢?
一种方法是根据时间戳进行判断。最后写入的,也就是时间戳在后面的,覆盖时间戳在前面的。在下图例子中,两个客户端连接到不同的服务器节点,对同一个数据做更新操作。那么,在数据复制的时候就会出现数据冲突,这个时候解决冲突的办法就是根据时间戳进行判断,时间戳大的,也就是说最新的数据,覆盖时间戳小的、旧的数据。
另一种解决冲突的机制是通过客户端进行冲突解决。在下图例子中,客户端 1 保存用户购物车编号,用户 123 的购物车编号是 55,客户端 2 保存用户 123 购车编号是 70。当客户端 3 去获取用户 123 的购物车编号时,它就得到了两个编号的购物车。解决办法就是将这两个购物车进行合并,合并成一个购物车,并且把它重新写入到分步式存储集群中。
还有一种冲突的解决方案是通过投票进行解决。典型的就是 Cassandra 中的冲突解决机制。如下图所示,一个客户端向一个分布式集群中写入数据的时候,它会同时向 3 个节点写入数据,并且至少等待两个节点响应写入成功。而另一个客户端想要读取这个数据的时候,至少要从 3 个节点中去读取数据,并且至少有 2 个节点有返回,然后根据返回的节点数进行投票,获取最新版本的数据。
分布式数据库和分布式存储是分布式系统中难度最大、挑战最大,也是最容易出问题的地方。解决的办法主要是数据库的复制,通过数据库的复制,提升数据库的读性能和系统的可用性。
如果对数据存储和数据库的写操作有更高要求的时候,就需要通过数据分片的方式来实现。在具体的部署过程中,可以混合使用数据复制、数据分库和数据分片几种技术方案。如果你的应用不是非要使用关系数据库的话,你还可以选择 NoSQL 数据库,NoSQL 数据库会提供更强大的数据存储能力和并发读写能力,但是 NoSQL 数据库因为 CAP 原理的约束可能会遇到数据不一致的问题。数据不一致的问题,可以通过时间戳合并、客户端判断以及投票这样的几种机制解决,实现最终一致性。