盘点分库分表中,你一定要避开的那些坑!
扫描下方海报二维码,试听课程:
例如:单表中出现了,动辄百万甚至千万级别的数据。“分表分库”就成为解决上述问题的有效工具。
今天和大家一起看看,如何进行分表分库以及期间遇到的问题吧。
为什么会分表分库
数据库数据会随着业务的发展而不断增多,因此数据操作,如增删改查的开销也会越来越大。
再加上物理服务器的资源有限(CPU、磁盘、内存、IO 等)。最终数据库所能承载的数据量、数据处理能力都将遭遇瓶颈。
换句话说需要合理的数据库架构来存放不断增长的数据,这个就是分库分表的设计初衷。目的就是为了缓解数据库的压力,最大限度提高数据操作的效率。
数据分表
如果单表的数据量过大,例如千万级甚至更多,那么在操作表的时候就会加大系统的开销。
每次查询会消耗数据库大量资源,如果需要多表的联合查询,这种劣势就更加明显了。
以 MySQL 为例,在插入数据的时候,会对表进行加锁,分为表锁定和行锁定。
无论是哪种锁定方式,都意味着前面一条数据在操作表或者行的时候,后面的请求都在排队,当访问量增加的时候,都会影响数据库的效率。
那么既然一定要分表,那么每张表分配多大的数据量比较合适呢?这里建议根据业务场景和实际情况具体分析。
一般来说 MySQL 数据库单表记录最好控制在 500 万条(这是个经验数字)。既然需要将数据从一个表分别存放到多个表中,那么来看看下面两种分表方式吧。
垂直分表
根据业务把一个表中的字段(Field)分到不同的表中。这些被分出去的数据通常根据业务需要,例如分出去一些不是经常使用的字段,一些长度较长的字段。
一般被拆分的表的字段数比较多。主要是避免查询的时候出现因为数据量大而造成的“跨页”问题。
一般这种拆分在数据库设计之初就会考虑,尽量在系统上线之前考虑调整。已经上线的项目,做这种操作是要慎重考虑的。
水平分表
用 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”。
也就是说不用大家自己去建立这个 Mapping Table,在做查询的时候中间件帮助你实现了 Mapping Table 的功能。所以,我们这里只需要了解其实现原理就可以了。
水平拆分还有一种情况是根据数据产生的前后顺序来拆分存放。例如,主表只存放最近 2 个月的信息,其他比较老旧的信息拆分到其他的表中。通过时间来做数据区分。更有甚者是通过服务的地域来做数据区分的。
需要注意的是由于分表造成一系列记录级别的问题,例如 Join 和 ID 生成,事务处理,同时存在这些表需要跨数据库的可能性:
Join:需要做两次查询,把两次查询的结果在应用层做合并。这种做法是最简单的,在应用层设计的时候需要考虑。
ID:可以使用 UUID,或者用一张表来存放生成的 Sequence,不过效率都不算高。UUID 实现起来比较方便,但是占用的空间比较大。
Sequence 表的方式节省了空间,但是所有的 ID 都依赖于单表。这里介绍一个大厂用的 Snowflake 的方式。
其核心思想是:使用 41bit 作为毫秒数,10bit 作为机器的 ID(5 个 bit 是数据中心,5 个 bit 的机器 ID),12bit 作为毫秒内的流水号(意味着每个节点在每毫秒可以产生 4096 个 ID),最后还有一个符号位,永远是 0。
数据分库
说完了分表,再来谈谈分库。
每个物理数据库支持数据都是有限的,每一次的数据库请求都会产生一次数据库链接,当一个库无法支持更多访问的时候,我们会把原来的单个数据库分成多个,帮助分担压力。
这里有几类分库的原则,可以根据具体场景进行选择:
根据业务不同分库,这种情况都会把主营业务和其他功能分开。例如可以分为订单数据库,核算数据库,评论数据库。
根据冷热数据进行分库,用数据访问频率来划分,例如:近一个月的交易数据属于高频数据,2-6 个月的交易数据属于中频数据,大于 6 个月的数据属于低频数据。
根据访问数据的地域/时间范围进行分库。
如果 Slave 出现问题,会启动熔断机制停止对其的访问;如果 Master 出现问题,通过选举机制选择新的 Master 代替。
数据库扩容
主从数据库扩容
我们这里假设有两个数据库集群,每个集群分别有 M1 S1 和 M2 S2 互为主备。
这些修改不需要重启数据库服务,只需要修改代理配置就可以完成。由于 M1 M2 S1 S2 中会存在一些冗余的数据,可以后台起服务将这些冗余数据删除,不会影响数据使用。
此时,再考虑数据库可用性,将扩展后的 4 个主库进行主备操作,针对每个主库都建立对应的从库,前者负责写操作,后者负责读操作。
下次如果需要扩容也可以按照类似的操作进行。
双写数据库扩容
在没有数据库主从配置的情况下的扩容,假设有数据库 M1 M2 如下图:
需要对目前的两个数据库做扩容,扩容之后是 4 个库如下图。新增的库是 M3,M4 路由的方式分别是 ID%2=0 和 ID%2=1。
与此同时,后台服务对 M1 M3,M2 M4 做数据同步,建议先做全量同步再做数据校验。
当然会存在部分的数据冗余,需要像上面一个方案一样通过后台服务删除这些冗余数据,删除的过程不会影响业务。
分布式事务原理
CAP
互联网应用大多会使用分表分库的操作,这个时候业务代码很可能会同时访问两个不同的数据库,做不同的操作。同时这两个操作有可能放在同一个事务中处理。
业务代码往数据库 01 这个节点写入记录 A,数据库 01 把 A 记录同步到数据库 02,业务代码再从数据库 02 中读出的记录也是 A。那么两个数据库存放的数据就是一致的。
那么业务代码可以从数据库 02 中获取记录 A。也就是在节点出现问题的时候,还保证数据的可用性。
这种情况的前提是两个节点的通讯失败了,写入数据库 01 记录的时候,需要锁住数据库 02 记录不让其他的业务代码修改,直到数据库 01 记录完成修改。因此 C 和 A 在此刻是矛盾的。两者不能兼得。
BASE
软状态( Soft State):一条数据如果存在多个副本,允许副本之间同步的延迟,在较短时间内能够容忍不一致。这个正在同步并且还没有完成同步的状态称为软状态。
这里“进程 1”和“进程 2” 的操作就存在因果关系。“进程 2” 的计算依赖于进程 1 写入的 X,如果没有 X 的值,“进程 2”无法计算。
“进程 1”写入变量 X 之后,该进程可以获取自己写入的这个值。
如果一个会话中实现来读己之所写。一旦数据更新,客户端只要在同一个会话中就可以看到这个更新的值。
“进程 1”如果有三个操作分别是 1,2,3。“进程 2”有两个操作分别是 1,2。当进程请求系统时,系统会保证按照进程中操作的先后顺序来执行。
分布式事务方案
两阶段提交
第二阶段(提交阶段):如果协调者收到了参与者失败或者超时的消息,会给参与者发送回滚(rollback)消息;否则,发送提交(commit)消息。
两种情况处理如下:
协调者向所有参与者发出正式提交事务的请求(即 commit 请求)。
参与者执行 commit 请求,并释放整个事务期间占用的资源。
各参与者向协调者反馈 ack(应答)完成的消息。
协调者收到所有参与者反馈的 ack 消息后,即完成事务提交。
协调者向所有参与者发出回滚请求(即 rollback 请求)。
参与者使用第一阶段中的 undo 信息执行回滚操作,并释放整个事务期间占用的资源。
各参与者向协调者反馈 ack 完成的消息。
协调者收到所有参与者反馈的 ack 消息后,即完成事务。
TCC(Try,Confirm,Cancel)
我们来看看用 TCC 的方式如何解决:
这里的交易数据库设计除了有金额字段,还要有转出金额或者转入金额的字段,在 Cancel 回滚的时候使用。
“B 账户”和“C 账户”分别设置总金额为 80=50+30 和 130=60+70,也把交易状态设置为“转账成功”。则整个事务完成。
那么“B 服务”和“C 服务”需要把入账的金额从总金额里面减去,也就是 50=80-30 和 60=130-70。
TCC 接口实现
例如上面所说的检查资源,执行业务,回滚业务等操作。目前有很多开源的架构例如:ByteTCC、TCC-transaction 可以借鉴。
TCC 可靠性
TCC 通过记录事务处理日志来保证可靠性。一旦 Try,Confirm,Cancel 操作的时候服务挂掉或者出现异常,TCC 会提供重试机制。
另外如果服务存在异步的情况可以采用消息队列的方式通信保持事务一致。
分库表中间件介绍
这里介绍几个比较常用的中间件:
基于代理方式的有 MySQL Proxy 和 Amoeba。
基于 Hibernate 框架的有 Hibernate Shards。
基于 JDBC 的有当当 Sharding-JDBC。
基于 MyBatis 的类似 Maven 插件式的蘑菇街 TSharding。
实际上如果我们理解了分表分库的原理之后,实现并不难,很多大厂都提供了产品。
总结
简介:十六年开发和架构经验,曾担任过惠普武汉交付中心技术专家,需求分析师,项目经理,后在创业公司担任技术/产品经理。善于学习,乐于分享。目前专注于技术架构与研发管理。
如有收获,请划至底部,点击“在看”,谢谢!
BAT架构经验倾囊相授