随着互联网产业的蓬勃发展,在互联网应用上产生的数据也是与日俱增。产生大量的交易记录和行为记录,它们的存放和分析是咱们须要面对的问题。算法
图片来自 Pexels数据库
例如:单表中出现了,动辄百万甚至千万级别的数据。“分表分库”就成为解决上述问题的有效工具。今天和你们一块儿看看,如何进行分表分库以及期间遇到的问题吧。缓存
为何会分表分库服务器
数据库数据会随着业务的发展而不断增多,所以数据操做,如增删改查的开销也会愈来愈大。架构
再加上物理服务器的资源有限(CPU、磁盘、内存、IO 等)。最终数据库所能承载的数据量、数据处理能力都将遭遇瓶颈。并发
换句话说须要合理的数据库架构来存放不断增加的数据,这个就是分库分表的设计初衷。目的就是为了缓解数据库的压力,最大限度提升数据操做的效率。app
数据分表负载均衡
若是单表的数据量过大,例如千万级甚至更多,那么在操做表的时候就会加大系统的开销。框架
每次查询会消耗数据库大量资源,若是须要多表的联合查询,这种劣势就更加明显了。异步
以 MySQL 为例,在插入数据的时候,会对表进行加锁,分为表锁定和行锁定。
不管是哪一种锁定方式,都意味着前面一条数据在操做表或者行的时候,后面的请求都在排队,当访问量增长的时候,都会影响数据库的效率。
那么既然必定要分表,那么每张表分配多大的数据量比较合适呢?这里建议根据业务场景和实际状况具体分析。
通常来讲 MySQL 数据库单表记录最好控制在 500 万条(这是个经验数字)。既然须要将数据从一个表分别存放到多个表中,那么来看看下面两种分表方式吧。
垂直分表
根据业务把一个表中的字段(Field)分到不一样的表中。这些被分出去的数据一般根据业务须要,例如分出去一些不是常用的字段,一些长度较长的字段。
通常被拆分的表的字段数比较多。主要是避免查询的时候出现由于数据量大而形成的“跨页”问题。
通常这种拆分在数据库设计之初就会考虑,尽可能在系统上线以前考虑调整。已经上线的项目,作这种操做是要慎重考虑的。
水平分表
将一个表中的数据,按照关键字(例如:ID)(或取 Hash 以后)对一个具体的数字取模,获得的余数就是须要存放到的新表的位置。

用 ID 取模的分表方式分配记录
ID 分别为 01-04 的四条记录,若是分配到 3 个表中,那么对 3 取模获得的余数分别是:
ID:01 对 3 取模余数为 1 ,存到“表 1”。
ID:02 对 3 取模余数为 2 ,存到“表 2”。
ID:03 对 3 取模余数为 3 ,存到“表 3”。
ID:04 对 3 取模余数为 1 ,存到“表 1”。
固然这里只是一个例子,实际状况须要对 ID 作 Hash 以后再计算。同时还能够针对不一样表所在的不一样的数据库的资源来设置存储数据的多少。针对每一个表所在的库的资源设置权值。
用这种方式存放数据之后,在访问具体数据的时候须要经过一个 Mapping Table 获取对应要响应的数据来自哪一个数据表。目前比较流行的数据库中间件已经帮助咱们实现了这部分的功能。
也就是说不用你们本身去创建这个 Mapping Table,在作查询的时候中间件帮助你实现了 Mapping Table 的功能。因此,咱们这里只须要了解其实现原理就能够了。

Mapping Table 协助分表
水平拆分还有一种状况是根据数据产生的先后顺序来拆分存放。例如,主表只存放最近 2 个月的信息,其余比较老旧的信息拆分到其余的表中。经过时间来作数据区分。更有甚者是经过服务的地域来作数据区分的。

按照时间作的数据分表
须要注意的是因为分表形成一系列记录级别的问题,例如 Join 和 ID 生成,事务处理,
同时存在这些表须要跨数据库的可能性:
Join:须要作两次查询,把两次查询的结果在应用层作合并。这种作法是最简单的,在应用层设计的时候须要考虑。
ID:可使用 UUID,或者用一张表来存放生成的 Sequence,不过效率都不算高。UUID 实现起来比较方便,可是占用的空间比较大。
Sequence 表的方式节省了空间,可是全部的 ID 都依赖于单表。这里介绍一个大厂用的 Snowflake 的方式。
Snowflake 是 Twitter 开源的分布式 ID 生成算法,结果是一个 long 型的 ID。
其核心思想是:使用 41bit 做为毫秒数,10bit 做为机器的 ID(5 个 bit 是数据中心,5 个 bit 的机器 ID),12bit 做为毫秒内的流水号(意味着每一个节点在每毫秒能够产生 4096 个 ID),最后还有一个符号位,永远是 0。

Snowflake 示意图
排序/分页:
数据分配到水平的几个表中的时候,作排序和分页或者一些集合操做是不容易的。
这里根据经验介绍两种方法。对分表的数据先进行排序/分页/聚合,再进行合并。对分表的数据先进行合并再作排序/分页/聚合。
事务:
存在分布式事务的可能,须要考虑补偿事务或者用 TCC(Try Confirm Cancel)协助完成,这部分的内容咱们下面会为你们介绍。
数据分库
说完了分表,再来谈谈分库。每一个物理数据库支持数据都是有限的,每一次的数据库请求都会产生一次数据库连接,当一个库没法支持更多访问的时候,咱们会把原来的单个数据库分红多个,帮助分担压力。
这里有几类分库的原则,能够根据具体场景进行选择:

单个表会分到不一样的数据库中
一般数据分库以后,每个数据库包含多个数据表,多个数据库会组成一个 Cluster/Group,提升了数据库的可用性,而且能够把读写作分离。
Master 库主要负责写操做,Slave 库主要负责读操做。在应用访问数据库的时候会经过一个负载均衡代理,经过判断读写操做把请求路由到对应的数据库。
若是是读操做,也会根据数据库设置的权重或者平均分配请求。
另外,还有数据库健康监控机制,定时发送心跳检测数据库的健康情况。
若是 Slave 出现问题,会启动熔断机制中止对其的访问;若是 Master 出现问题,经过选举机制选择新的 Master 代替。

主从数据库简图
数据库扩容
分库以后的数据库会遇到数据扩容或者数据迁移的状况。这里推荐两种数据库扩容的方案。
主从数据库扩容
咱们这里假设有两个数据库集群,每一个集群分别有 M1 S1 和 M2 S2 互为主备。

两个数据库集群示意图
因为 M1 和 S1 互为主备因此数据是同样的,M2 和 S2 一样。把原有的 ID %2 模式切换成 ID %4 模式,也就是把两个数据集群扩充到 4 个数据库集群。
负载均衡器直接把数据路由到原来两个 S1 和 S2 上面,同时 S1 和 S2 会中止与 M1 和 M2 的数据同步,单独做为主库(写操做)存在。
这些修改不须要重启数据库服务,只须要修改代理配置就能够完成。因为 M1 M2 S1 S2 中会存在一些冗余的数据,能够后台起服务将这些冗余数据删除,不会影响数据使用。

两个集群中的两个主从,分别扩展成四个集群中的四个主机
此时,再考虑数据库可用性,将扩展后的 4 个主库进行主备操做,针对每一个主库都创建对应的从库,前者负责写操做,后者负责读操做。下次若是须要扩容也能够按照相似的操做进行。

从两个集群扩展成四个集群
双写数据库扩容
在没有数据库主从配置的状况下的扩容,假设有数据库 M1 M2 以下图:

扩展前的两个主库
须要对目前的两个数据库作扩容,扩容以后是 4 个库以下图。新增的库是 M3,M4 路由的方式分别是 ID%2=0 和 ID%2=1。

新增两个主库
这个时候新的数据会同时进入 M1 M2 M3 M4 四个库中,而老数据的使用依旧从 M1 M2 中获取。
与此同时,后台服务对 M1 M3,M2 M4 作数据同步,建议先作全量同步再作数据校验。

老库给新库作数据同步
当完成数据同步以后,四个库的数据保持一致了,修改负载均衡代理的配置为 ID%4 的模式。此时扩容就完成了,从原来的 2 个数据库扩展成 4 个数据库。
固然会存在部分的数据冗余,须要像上面一个方案同样经过后台服务删除这些冗余数据,删除的过程不会影响业务。

数据同步之后作 Hash 切分
分布式事务原理
架构设计的分表分库带来的结果是咱们不得不考虑分布式事务,今天咱们来看看分布式事务须要记住哪两个原理。
CAP
互联网应用大多会使用分表分库的操做,这个时候业务代码极可能会同时访问两个不一样的数据库,作不一样的操做。同时这两个操做有可能放在同一个事务中处理。
这里引出分布式系统的 CAP 理论,他包括如下三个属性:
一致性(Consistency):
分布式系统中的全部数据,同一时刻有一样的值。
业务代码往数据库 01 这个节点写入记录 A,数据库 01 把 A 记录同步到数据库 02,业务代码再从数据库 02 中读出的记录也是 A。那么两个数据库存放的数据就是一致的。

一致性简图
可用性(Availability):
分布式系统中一部分节点出现故障,分布式系统仍旧能够响应用户的请求。
假设数据库 01 和 02 同时存放记录 A,因为数据库 01 挂掉了,业务代码不能从中获取数据。
那么业务代码能够从数据库 02 中获取记录 A。也就是在节点出现问题的时候,还保证数据的可用性。

可用性简图
分区容错性(Partition tolerance):
假设两个数据库节点分别在两个区,而两个区的通信发生了问题。就不能达成数据一致,这就是分区的状况,我就须要从 C 和 A 之间作出选择。
是选择可用性(A),获取其中一个区的数据。仍是选择一致性(C),等待两个区的数据同步了再去获取数据。
这种状况的前提是两个节点的通信失败了,写入数据库 01 记录的时候,须要锁住数据库 02 记录不让其余的业务代码修改,直到数据库 01 记录完成修改。所以 C 和 A 在此刻是矛盾的。二者不能兼得。

分区容错简图
BASE
Base 原理普遍应用在数据量大,高并发的互联网场景。
一块儿来看看都包含哪些:
基本可用(Basically Available):
不会由于某个节点出现问题就影响用户的请求。
即便在流量激增的状况下,也会考虑经过限流降级的办法保证用户的请求是可用的。
好比,电商系统在流量激增的时候,资源会向核心业务倾斜,其余的业务降级处理。
软状态( Soft State):一条数据若是存在多个副本,容许副本之间同步的延迟,在较短期内可以容忍不一致。这个正在同步而且尚未完成同步的状态称为软状态。

最终一致性( Eventual Consistency):
最终一致性是相对于强一致性来讲的,强一致性是要保证全部的数据都是一致的,是实时同步。
而最终一致性会容忍一小段时间数据的不一致,但过了这段时间之后数据会保证一致。
其包含如下几种“一致性”:
①因果一致性(Causal Consistency)
若是有两个进程 1 和 2 都对变量 X 进行操做,“进程 1” 写入变量 X,“进程 2”须要读取变量 X,而后用这个 X 来计算 X+2。
这里“进程 1”和“进程 2” 的操做就存在因果关系。“进程 2” 的计算依赖于进程 1 写入的 X,若是没有 X 的值,“进程 2”没法计算。

两个进程对同一变量进行操做
②读己之所写(Read Your Writes)
“进程 1”写入变量 X 以后,该进程能够获取本身写入的这个值。

进程写入的值的同时获取值
③会话一致性(Session Consistency)
若是一个会话中实现来读己之所写。一旦数据更新,客户端只要在同一个会话中就能够看到这个更新的值。

多进程在同一会话须要看到相同的值
④单调写一致性(Monotonic Write Consistency)
“进程 1”若是有三个操做分别是 1,2,3。“进程 2”有两个操做分别是 1,2。当进程请求系统时,系统会保证按照进程中操做的前后顺序来执行。

多进程多操做经过队列方式执行
分布式事务方案
说完了分布式的原理,再来提一下分布式的方案。因为所处场景不同,因此方案也各有不一样,这里介绍两种比较流行的方案,两段式和 TCC(Try,Confirm,Cancel)。
两阶段提交
顾名思义,事务会进行两次提交。这里须要介绍两个概念,一个是事务协调者,也叫事物管理器。
它是用来协调事务的,全部事务何时准备好了,何时能够提交了,都由它来协调和管理。
另外一个是参与者,也叫资源管理器。它主要是负责处理具体事务的,管理者须要处理的资源。例如:订票业务,扣款业务。
第一阶段(准备阶段):
事务协调者(事务管理器)给每一个参与者(资源管理器)发送 Prepare 消息,发这个消息的目的是问“你们是否是都准备好了,咱们立刻就要执行事务了”。
参与者会根据自身业务和资源状况进行检查,而后给出反馈。
这个检查过程根据业务内容不一样而不一样。
例如:订票业务,就要检查是否有剩余票。扣款业务就要检查,余额是否足够。一旦检查经过了才能返回就绪(Ready)信息。
不然,事务将终止,而且等待下次询问。因为这些检查须要作一些操做,这些操做可能再以后回滚时用到,因此须要写 redo 和 undo 日志,当事务失败重试,或者事务失败回滚的时候使用。
第二阶段(提交阶段):
若是协调者收到了参与者失败或者超时的消息,会给参与者发送回滚(rollback)消息;不然,发送提交(commit)消息。
两种状况处理以下:
状况 1,当全部参与者均反馈 yes,提交事务:
协调者向全部参与者发出正式提交事务的请求(即 commit 请求)。
参与者执行 commit 请求,并释放整个事务期间占用的资源。
各参与者向协调者反馈 ack(应答)完成的消息。
协调者收到全部参与者反馈的 ack 消息后,即完成事务提交。
状况 2,当有一个参与者反馈 no,回滚事务:
协调者向全部参与者发出回滚请求(即 rollback 请求)。
参与者使用第一阶段中的 undo 信息执行回滚操做,并释放整个事务期间占用的资源。
各参与者向协调者反馈 ack 完成的消息。
协调者收到全部参与者反馈的 ack 消息后,即完成事务。

两个阶段提交事务示意图
TCC(Try,Confirm,Cancel)
对于一些要求高一致性的分布式事务,例如:支付系统,交易系统,咱们会采用 TCC。
它包括,Try 尝试,Confirm 确认,Cancel 取消。看下面一个例子可否帮助你们理解。
假设咱们有一个转帐服务,须要把“A 银行”“A 帐户”中的钱分别转到“B银行”“B 帐户”和“C 银行”“C 帐户”中去。
假设这三个银行都有各自的转帐服务,那么此次转帐事务就造成了一次分布式事务。
咱们来看看用 TCC 的方式如何解决:

转帐业务示意图
首先是 Try 阶段,主要检测资源是否可用,例如检查帐户余额是否足够,缓存,数据库,队列是否可用等等。
并不执行具体的逻辑。如上图,这里从“A 帐户”转出以前要检查,帐户的总金额是否大于 100,而且记录转出金额和剩余金额。
对于“B 帐户”和“C 帐户”来讲须要知道帐户原有总金额和转入的金额,从而能够计算转入后的金额。
这里的交易数据库设计除了有金额字段,还要有转出金额或者转入金额的字段,在 Cancel 回滚的时候使用。

Try 阶段示意图
若是 Try 阶段成功,那么就进入 Confirm 阶段,也就是执行具体的业务逻辑。
这里从“A 帐户”转出 100 元成功,剩余总金额=220-100=120,把这个剩余金额写入到总金额中保存,而且把交易的状态设置为“转帐成功”。
“B 帐户”和“C 帐户”分别设置总金额为 80=50+30 和 130=60+70,也把交易状态设置为“转帐成功”。则整个事务完成。

Confirm 阶段示意图
若是 Try 阶段没有成功,那么服务 A B C 都要作回滚的操做。对于“A帐户”来讲须要把扣除的 100 元加回,因此总金额 220=120+100。
那么“B 服务”和“C 服务”须要把入帐的金额从总金额里面减去,也就是 50=80-30 和 60=130-70。

Cancel 阶段示意图
TCC 接口实现
这里须要注意的是,须要针对每一个服务去实现 Try,Confirm,Cancel 三个阶段的代码。
例如上面所说的检查资源,执行业务,回滚业务等操做。目前有不少开源的架构例如:ByteTCC、TCC-transaction 能够借鉴。

TCC 实现接口示意图
TCC 可靠性
TCC 经过记录事务处理日志来保证可靠性。一旦 Try,Confirm,Cancel 操做的时候服务挂掉或者出现异常,TCC 会提供重试机制。另外若是服务存在异步的状况能够采用消息队列的方式通讯保持事务一致。

重试机制示意图
分库表中间件介绍
若是以为分表分库以后,须要考虑的问题不少,可使用市面上的现成的中间件帮咱们实现。
这里介绍几个比较经常使用的中间件:
基于代理方式的有 MySQL Proxy 和 Amoeba。
基于 Hibernate 框架的有 Hibernate Shards。
基于 JDBC 的有当当 Sharding-JDBC。
基于 MyBatis 的相似 Maven 插件式的蘑菇街 TSharding。
另外着重介绍 Sharding-JDBC 的架构,它的构成和“服务注册中心”很像。
Sharding-JDBC 会提供一个 Sharding-Proxy 作代理,他会链接一个注册中心(registry center),一旦数据库的节点挂接到系统中,会在这个中心注册,同时也会监控数据库的健康情况作心跳检测。
而 Sharding-Proxy 自己在业务代码(Business Code)请求数据库的时候能够协助作负载均衡和路由。
同时 Sharding-Proxy 自己也能够支持被 MySQL Cli 和 MySQL Workbench 查看。
实际上若是咱们理解了分表分库的原理以后,实现并不难,不少大厂都提供了产品。

Sharding-Proxy 实现原理图
总结
由于数据量的上升,为了提升性能会对系统进行分表分库。从分表来讲,有水平分表和垂直分表两种方式。
能够根据业务,冷热数据等来进行分库,分库之后经过主从库来实现读写分离。
若是对分库以后数据库作扩容,有两种方式,主从数据库扩容和双写数据库扩容。
分表分库会带来分布式事务,咱们须要掌握 CAP 和 BASE 原理,同时介绍了两阶段提交和 TCC 两个分布式事务方案。最后,介绍了流行的分表分库中间件,以及其实现原理。