作者:禅与计算机程序设计艺术
1.简介
随着互联网的迅速发展,网站日渐庞大,用户访问量呈现爆炸性增长,单个数据库服务器无法支撑如此大的访问量,需要进行横向扩展,利用多个数据库服务器,将数据分布到不同的数据库中,以提高吞吐率、减少延时,也为了实现异地多活等业务需求。 在数据库横向扩展方面,通常采用垂直拆分和水平拆分两种方式,而在分布式事务方面,又可以分为基于XA协议的两阶段提交(2PC)和基于BASE协议的柔性事务传播(TCC)。本文将从以下几点深入探讨数据库的分片与分布式事务。
2.什么是数据库分片?
数据库分片是指通过切分数据并存储到不同数据库服务器上,达到提升性能、解决海量数据的存储问题。传统的数据分片方法主要集中于垂直分片和水平分片,即将一个数据库按照业务维度拆分成多个库,每个库的数据存储在一个物理节点上,这样能够有效的解决数据量过大的问题;另外一种是分布式数据库,即将一个数据库分布到多个物理节点上,这样便可避免单个节点的瓶颈问题,提高数据库的整体容量和处理能力。 最常见的数据库分片策略包括如下三种:
- 水平分片:即将同类数据存储到相同的数据库服务器上,通过主键或其他分区键将数据划分到不同的表空间,使得单个表的大小变小,同时避免跨越数据库的网络传输。典型代表产品有MySQL InnoDB Cluster、CockroachDB、TiDB。
- 垂直分片:即将不同的业务数据存储到不同的数据库服务器上,优化查询效率和资源分配,降低单台服务器负载,提高系统的稳定性。典型代表产品有ShardingSphere、Aurora、PostgreSQL、MongoDB。
- 混合分片:结合了水平分片和垂直分片的优点。典型代表产品有Couchbase、HBase。 数据库分片的关键在于根据应用特点、数据量大小、性能要求和业务压力,选择合适的分片策略,并通过标准化的方式自动管理和监控分片集群,确保服务的高可用性。
3.什么是分布式事务?
分布式事务是指事务的参与者、支持事务的服务器、资源服务器以及协调者分别位于不同的位置,因此,每一次事务提交时,都需要经过参与者的确认,只有所有参与者都完成了确认后,才表示事务成功结束。分布式事务一般是在异构环境下(例如,不同公司的数据库服务器之间)进行数据交换,需要保证事务的ACID特性,且保持各个数据库服务器间的数据一致性。 两种主流的分布式事务协议包括两阶段提交协议(Two-Phase Commit, 2PC)和三阶段提交协议(Three-Phase Commit, 3PC),前者是对XA协议的改进,后者是在2PC的基础上增加了准备阶段,用于同步各个数据库服务器上的事务日志。由于2PC协议存在一些缺陷,目前较多的公司选择更加激进的柔性事务传播协议( BASE: Basically Available, Soft state, Eventually consistent)代替之。4.实现数据库分片的方法
4.1 水平分片
InnoDB Cluster是一个分布式InnoDB引擎的开源集群解决方案,由MySQL贡献,提供无限水平扩展能力。InnoDB Cluster支持一主多从的复制架构,具备读写分离、自动故障转移、秒级恢复、数据强一致性等优点。它支持数据库的水平扩展,通过切分逻辑表及数据分片来解决单个物理服务器存储容量不足的问题。其核心设计理念是将整个数据库按行切分,将一个逻辑表的数据切分到不同的分片表中,然后在每个分片表上建立索引。 当写入的数据仅对应于一个分片表时,InnoDB Cluster就会路由到对应的分片表,通过该分片表的索引快速定位到目标记录。反之,如果数据涉及多个分片表,则需要将这些数据合并再插入到聚集索引表中。 如下图所示,当写入数据包含主键值23时,InnoDB Cluster通过主键索引定位到对应的分片表shard_001中的主键值为23的记录,通过聚集索引表索引定位到其他关联表中的记录,之后将写入操作在多个分片表上执行。4.2 垂直分片
分库分表是实现数据分片的另一种方式,也称为数据库垂直切分。它把相关数据放在同一个数据库中,但是把不同的表存放在不同的数据库服务器上,以达到降低资源消耗和提高查询效率的目的。 一般情况下,垂直切分和水平切分配合使用,每个数据库按照业务维度被切分成多个库,每个库的数据存储在一个物理节点上。每个库里面包含相关的表,表之间的关联关系也是尽可能的减少。这种做法能够有效的解决单个物理节点的资源瓶颈问题,同时还能有效的提高数据库的整体容量和处理能力。 如下图所示,典型的数据库垂直切分策略就是把用户数据和订单数据放在一起,但把交易记录和商品信息放在另一个数据库中,从而方便查询和维护。4.3 混合分片
上面提到的两种分片策略都是针对关系型数据库设计的,另外一种是NoSQL数据库的分片策略。由于NoSQL数据库没有支持完整ACID的事务机制,所以只能在弱一致性的模式下进行分片。常用的混合分片策略包括水平切分和垂直切分的组合,其中水平切分的范围依然可以选择单表还是全库,垂直切分的方向也可以是单库还是多库。 如下图所示,Amazon Aurora通过切分多个集群来提高可用性和可伸缩性,每个集群包含多个实例,每个实例包含多个分片组。每个分片组包含多个分片,每个分片存储一部分数据。当用户数据量增加时,Aurora可以通过添加分片的形式进行水平扩展。4.4 分片策略选择建议
在决定采用哪种分片策略之前,首先要评估当前业务的读写比例、查询模式、数据量、性能要求等,以及技术团队的知识储备、工具使用经验、实施风险承受能力等因素。下面给出几个常见的分片策略选择建议。4.4.1 固定数量的分片
固定数量的分片是最简单也是最常用的方法,主要是为了满足某些特定场景下的需求。例如,对于电商网站来说,可以固定将数据按年、月、日分别放入三个分片表中。这种简单的分片策略能够方便管理数据,但可能会导致热点问题。例如,假设用户频繁访问某个时间段,那么这个时间段的所有请求都会集中在同一个分片表中,导致后续数据读取时的热点问题。4.4.2 均匀分布的分片
均匀分布的分片也是一种简单有效的分片策略。这种策略往往用来应对突发流量或者数据量增长时的数据分片需求。例如,可以按照时间戳将数据均匀分散到不同分片表中。这种方式的好处在于将访问热点集中到少数几个分片表中,不会引起集中式瓶颈问题。缺点是当分片数量增长时,扩容和数据迁移工作量会变大。4.4.3 哈希函数分片
哈希函数分片也是一种常用分片策略。这种策略的思路是在创建分片表时,对分片键值进行哈希运算,根据得到的哈希值决定落入哪个分片。例如,可以把订单号做哈希运算,得到的结果作为分片序号,然后将数据均匀的分布到相应的分片表中。这种分片策略能够将热点问题平均分配到各个分片中,但它对业务的侵入性较强,难以兼顾性能与易用性。4.4.4 按照业务维度划分的分片
按照业务维度划分的分片策略最具有灵活性。这种策略通过定义业务实体(例如用户、订单、产品)以及相应的业务属性(例如城市、性别、类别等),将数据分布到不同的分片表中。这种策略能够最大程度的兼顾性能与易用性,但管理起来比较复杂。4.4.5 客户端分片
客户端分片是一种基于应用层的分片策略,不需要改变底层数据库的结构和架构,只需在应用端进行处理。这种分片策略的实现方式可以是将应用程序本身拆分为多个子模块,每个子模块负责不同分片表的读写操作,从而避免跨进程、跨线程、跨主机的网络通信问题。 不过,客户端分片也存在很多问题,比如: - 网络传输延时:由于请求必须穿越网络,因此引入额外的网络开销,导致整体性能下降;
- 数据倾斜问题:由于数据不是均匀分布的,所以单个分片可能成为热点,导致整体性能下降;
- 分片扩容问题:因为使用了客户端分片,扩容需要修改应用程序代码,并且可能引入其他问题;
- 不保证事务完整性:由于采用了非标准化的客户端分片方式,导致事务完整性不能得到保证。 除以上原因外,客户端分片仍然是一个有效的分片策略,但不能完全替代数据库分片功能。
总的来说,各种分片策略都有自己的优缺点,业务场景也很重要,应该综合考虑后再选取一种最合适的分片策略。
5.分布式事务的原理和实现
分布式事务可以说是分布式计算领域的里程碑事件,相信各位读者对它的了解已经到了一个相当深刻的程度。理解分布式事务背后的原理有助于更好的认识分布式事务,并帮助我们正确地使用分布式事务,减少潜在的错误发生。
5.1 分布式事务概述
分布式事务就是指事务的参与者、支持事务的服务器、资源服务器以及协调者分别位于不同的位置,因此,每一次事务提交时,都需要经过参与者的确认,只有所有参与者都完成了确认后,才表示事务成功结束。目前,分布式事务有两种协议,一种是基于XA协议的两阶段提交(2PC)协议,另一种是基于BASE协议的柔性事务传播(TCC)协议。
5.2 XA协议(Two-Phase Commit Protocol)
XA是一种分布式事务的标准协议。XA协议定义了事务管理器(TM)、资源管理器(RM)、全局事务标识符(GTRID)、分支事务标识符(BTRID)四个角色。一个事务通常包括一个二阶段过程:prepare和commit两个阶段。第一阶段是prepare阶段,协调者通知所有的参与者,事务准备就绪;第二阶段是commit阶段,如果所有参与者都同意提交事务,事务管理器通知所有的参与者提交事务,否则通知所有的参与者回滚事务。如果出现任何异常情况,则终止事务。
5.3 TCC协议(Try-Confirm-Cancel Protocol)
TCC是一种基于补偿机制的事务型模型。它是一种非常有弹性的事务型模型,可以在一定程度上避免万劫不复的情况。TCC包括三个操作:尝试(try)、确认(confirm)和取消(cancel)。事务发起方先向其后端系统发送TRY消息,询问是否可以执行事务操作,然后进入预备状态;接着事务参与方会对其本地资源进行预留(try),也就是在事务执行过程中对数据库资源进行锁定;待事务所有参与方的TRY都返回“预提交”响应时,事务发起方再次向后端系统发送确认消息,通知后端系统可以真正提交事务;当遇到失败情况时,事务发起方会向后端系统发送回滚消息,通知后端系统取消之前的事务操作,并释放资源;如果所有参与方都正常完成了事务操作,事务最终会成功,否则会继续回滚。
5.4 CAP理论
CAP理论是指在分布式系统中,Consistency(一致性),Availability(可用性),Partition tolerance(分区容错性)这三个要件不能同时被满足。CAP理论认为,在一个分布式系统中,不可能同时满足一致性(Consistency),可用性(Availability)以及分区容错性(Partition tolerance)。
- Consistency(一致性): 在分布式系统中,一致性是指数据在多个副本之间是否完全相同。为了保证一致性,系统需要做到以下事情:
- 所有节点访问同一份最新数据副本
- 系统通过分布式协议来保证各节点数据的同步
- Availability(可用性): 可用性是指系统持续提供服务的时间占比。为了保证可用性,系统需要做到以下事情:
- 保证任何非失败节点总是可服务的
- Partition tolerance(分区容错性): 分区容错性是指分布式系统在遇到分区故障的时候,仍然能够正常运行。为了保证分区容错性,系统需要做到以下事情:
- 可以自动检测和容忍失效的分区
- 限制网络带宽,降低时延,增加容错能力
5.5 分布式事务实现
5.5.1 XA协议
在MySQL中,实现XA协议主要是依靠InnoDB引擎的内部XA接口。InnoDB引擎提供了对XA的支持,使得InnoDB可以像其它支持XA的数据库一样,实现分布式事务。
- 准备事务: 一旦客户端向数据库申请启动一个事务,MySQL Server就会给客户端生成一个唯一的xid,并记录它的状态为'Prepared'。至此,事务就准备好了,但数据库并不实际执行事务,仅仅是记录事务的当前状态,等待提交或者回滚指令。
- 提交事务: 当事务中所有的参与者准备好后,事务管理器TM就会发起提交事务的命令,并将xid告诉所有的参与者。参与者收到TM的提交请求后,会判断该事务是否是其事务,如果是,它就会开始提交事务。事务开始的过程就是在InnoDB引擎层调用其自身的提交函数innodb_commit(),将提交点(Undo Log)的信息写到磁盘上。提交完成后,事务管理器会更新事务的状态为Committed。
- 回滚事务: 如果事务管理器收到参与者的回滚请求,他就会向所有参与者发起回滚事务的命令。当参与者收到回滚事务的请求时,他们会开始回滚事务。回滚的过程就是在InnoDB引擎层调用其自身的回滚函数innodb_rollback(),将回滚点(Undo Log)的信息写到磁盘上,然后清理掉事务相关的数据。回滚完成后,事务管理器会更新事务的状态为Aborted。 通过以上过程,我们知道InnoDB引擎的分布式事务是如何实现的。但是,通过客户端直接的SQL语句,无法做到自动的去提交或者回滚事务,我们需要自己写代码来实现事务的提交或者回滚。我们可以使用MySQL Connector/J连接器来实现。
5.5.2 Spring Transaction API
Spring框架提供了beginTransaction()、commit()和rollback()三个方法来实现事务的控制。@Autowired private ApplicationContext context;
public void testTransaction() throws Exception { // 开启事务 DataSource dataSource = (DataSource)context.getBean("dataSource"); Connection connection = dataSource.getConnection(); PreparedStatement statement = connection.prepareStatement("INSERT INTO t_test values ()"); try { // 执行sql语句 statement.executeUpdate();
// 手动提交事务
transactionManager.commit(status);
} catch (Exception e) {
// 手动回滚事务
transactionManager.rollback(status);
throw new RuntimeException(e);
} finally {
// 关闭资源
if (statement!= null) {
statement.close();
}
if (connection!= null) {
connection.close();
}
}
}
``` 通过上下文获取DataSource对象,然后获取连接和预编译的PreparedStatement对象。执行sql语句并手动提交或者回滚事务。注意,如果出现异常,需要手动回滚事务,否则会导致数据库中一直存在脏数据。