作为一个前端专业的人来说,对于事务的理解,一直停留在“要么都成功,要么都不成功”的小白阶段。既然自己将2018年定义为”深入理解“的一年,那么就从深入理解事务开始吧。
什么是事务?
正如文章开头所说的:事务是一系列的动作,这些动作必须全部完成,如果有一个失败,那么事务就会回滚到最开始的状态,仿佛什么都没发生过一样。在企业级应用的开发过程中,事务管理是必不可少的技术,用来确保数据的完整性和一致性。
事务有四个特性,也就是经常被提到的ACID:
- 原子性(Atomicity):所谓的原子性就是说,在整个事务中的所有操作,要么全部完成,要么全部不做,没有中间状态。对于事务在执行中发生错误,所有的操作都会被回滚,整个事务就像从没被执行过一样。
- 一致性(Consistency):事务的执行必须保证系统的一致性,就拿转账为例,A有500元,B有300元,如果在一个事务里A成功转给B50元,那么不管并发多少,不管发生什么,只要事务执行成功了,那么最后A账户一定是450元,B账户一定是350元。
- 隔离性(Isolation):所谓的隔离性就是说,事务与事务之间不会互相影响,一个事务的中间状态不会被其他事务感知。
- 持久性(Durability):所谓的持久性,就是说一单事务完成了,那么事务对数据所做的变更就完全保存在了数据库中,即使发生停电,系统宕机也是如此。
上面我们说到的事务,也可以称为是”本地事务“。目前许多框架,都能够很方便的支持本地事务。比如Spring Boot,只需要在方法前加上”@Transaction“的注解,就可以愉快的使用事务了。
但是,事务到此未知就结束了吗?不是的,随着企业应用越来越复杂,应用的架构也从单体架构演变到了SOA,还有现在炙手可热的微服务。这时候,又出现了分布式事务的概念。
分布式事务
分布式事务,简单来说就是指对数据库的处理操作分布在不同的节点之上,而且操作的数据,分布于不同的数据库。分布式事务,需要保证不同数据库的数据一致性。
分布式事务产生的原因
数据库分库分表
处于数据量或者数据隔离的考虑,实际开发中需要进行分库分表。原来一个库现在变成了多个库,这时候要保证数据一致性,就要用到分布式事务。
应用SOA化
所谓的SOA话,就是业务的服务化。比如原来单机支撑了整个电商网站,现在对整个网站进行拆解,分离出了订单中心、用户中心、库存中心。对于订单中心,有专门的数据库存储订单信息,用户中心也有专门的数据库存储用户信息,库存中心也会有专门的数据库存储库存信息。这时候如果要同时对订单和库存进行操作,那么就会涉及到订单数据库和库存数据库,为了保证数据一致性,就需要用到分布式事务。
CPA与BASE
说到分布式事务,就离不开CPA原则与BASE方案。
CPA
CPA指的是,在一个分布式系统中,一致性(C)、可用性(A)、分区容错性(P),三者不可兼得。CPA是NoSQL数据库的基石。
- 一致性:在分布式系统中的所有数据备份,在同一时刻是否同样的值。(等同于所有节点访问同一份最新的数据副本)
- 可用性:在集群中一部分节点故障后,集群整体是否还能响应客户端的读写请求。(对数据更新具备高可用性)
- 分区容错性:以实际效果而言,分区相当于对通信的时限要求。系统如果不能在时限内达成数据一致性,就意味着发生了分区的情况,必须就当前操作在C和A之间做出选择。
CAP理论就是说在分布式存储系统中,最多只能实现上面的两点。而由于当前的网络硬件肯定会出现延迟丢包等问题,所以分区容忍性是我们必须需要实现的。所以我们只能在一致性和可用性之间进行权衡,没有NoSQL系统能同时保证这三点。
BASE
BASE就是为了解决关系数据库强一致性引起的问题而引起的可用性降低而提出的解决方案。
BASE是下面三个术语的缩写:
- 基本可用(Basically Available)
- 软状态(Soft state)
- 最终一致(Eventually consistent)
常见的分布式事务解决方案
两阶段提交
两阶段提交(Two Phase Commit, 2PC), 具有强一致性, 是CP系统的一种典型实现,常见的标准是XA,JTA等。例如Oracle的数据库支持XA。
下面是两阶段提交的示意图:
图的上半是两阶段提交成功的演示, 下半是两阶段提交失败的演示。
两阶段提交目前并不是主流的解决方案,其主要原因是:协调者需要等待所有参与者发出yes请求,或者一个参与者发出no请求后,才能执行提交或者终端操作。这会造成长时间锁住多个资源,造成性能瓶颈。如果参与者有一个耗时长的操作, 性能损耗会更明显;还有一个缺点,就是实现复杂,不利于系统的扩展。
TCC(Try-Confirm-Cancel)
TCC, 是基于补偿型事务的AP系统的一种实现, 具有最终一致性。所谓的TCC编程模式,也是两阶段提交的一个变种。TCC提供了一个编程框架,将整个业务逻辑分为三块:Try、Confirm和Cancel三个操作。以在线下单为例,Try阶段会去扣库存,Confirm阶段则是去更新订单状态,如果更新订单失败,则进入Cancel阶段,会去恢复库存。总之,TCC就是通过代码人为实现了两阶段提交,不同的业务场景所写的代码都不一样,复杂度也不一样,因此,这种模式并不能很好地被复用。
异步确保型
通过将一系列同步的事务操作变为基于消息执行的异步操作, 避免了分布式事务中的同步阻塞操作的影响。基于消息执行就是基于消息中间件的两阶段提交,本质上是对消息中间件的一种特殊利用,它是将本地事务和发消息放在了一个分布式事务里,保证要么本地操作成功成功并且对外发消息成功,要么两者都失败,开源的RocketMQ就支持这一特性,具体原理如下:
执行步骤如下:
- MQ发送方发送远程事务消息到MQ Server;
- MQ Server给予响应, 表明事务消息已成功到达MQ Server.
- MQ发送方Commit本地事务.
- 若本地事务Commit成功, 则通知MQ Server允许对应事务消息被消费; 若本地事务失败, 则通知MQ Server对应事务消息应被丢弃.
- 若MQ发送方超时未对MQ Server作出本地事务执行状态的反馈, 那么需要MQ Servfer向MQ发送方主动回查事务状态, 以决定事务消息是否能被消费.
- 当得知本地事务执行成功时, MQ Server允许MQ订阅方消费本条事务消息.
需要额外说明的一点, 就是事务消息投递到MQ订阅方后, 并不一定能够成功执行. 需要MQ订阅方主动给予消费反馈(ack)
- 如果MQ订阅方执行远程事务成功, 则给予消费成功的ack, 那么MQ Server可以安全将事务消息移除;
- 如果执行失败, MQ Server需要对消息重新投递, 直至消费成功.
- 根据业务逻辑的具体实现不同,还可能需要对消息中间件增加消息不重复, 不乱序等其它要求.
此方案适用于执行周期较长,实时性要求不高的场景。
最大努力通知型
这是分布式事务中要求最低的一种, 也可以通过消息中间件实现, 与前面异步确保型操作不同的一点是, 在消息由MQ Server投递到消费者之后, 允许在达到最大重试次数之后正常结束事务.这种方案适用于交易结果消息的通知等
微服务的事务
最近两年,微服务的呼声越来越高,不可避免的,微服务也会面临事务的困扰。
- 首先,对于微服务架构来说,数据访问变得更加复杂,这是因为数据都是微服务私有的,唯一可访问的方式就是通过 API。这种打包数据访问方式使得微服务之间松耦合,并且彼此之间独立,更容易进行性能扩展。
- 其次,不同的微服务经常使用不同的数据库。应用会产生各种不同类型的数据,关系型数据库并不一定是最佳选择基于微服务的应用一般都使用 SQL 和 NoSQL 结合的模式。但是这些非关系型数据大多数并不支持 2PC。
总结
事务,尤其是分布式事务,是一个很大的话题,除了上述列出的几种解决方案,根据不同的业务要求,还有许多其他的解决方案。按照控制力度,分布式事务分为部分控制和完全控制两种:
- 部分控制就是各种变种的两阶段提交,包括上面提到的异步确保型、TCC模式
- 完全控制就是完全实现两阶段提交。部分控制的好处是并发量和性能很好,缺点是数据一致性减弱了,完全控制则是牺牲了性能,保障了一致性。
具体用哪种方式,最终还是取决于业务场景。