分布式事务的几种解决方案

分布式系统,与传统单体架构系统相比,其结构复杂的多,系统间靠网络传输数据,系统存在诸多不确定因素,如硬件故障、网络波动等,都会影响整个系统的稳定性。而分布式事务更是分布式系统的一大难题,本篇将讨论业界分布式事务的几种常见解决方案。 1. 数据库事务 百度百科对事务的定义: 数据库事务( transaction)是访问并可能操作各种数据项的一个数据库操作序列,这些操作要么全部执行,要么全部不执行,是一个不可分割的工作单位。 解读一下这个定义: 事务产生于一个数据库操作序列:一组读取或者更新数据库等操作时,事务是一种机制,来保证了最终数据的一致性, 事务是一个不可分割的单元:一组操作不能拆分,是一个整体 事务执行:要么全部执行,要么全部不执行,不能部分执行 通常,数据库的事务是通过数据库日志来保证的,所有数据库的操作都记录日志,如果数据库发生宕机,那么通过读取日志可以知道操作时需要回滚还是提交。 众所周知,数据库事务有ACID四大特性,满足ACID特性的事务,一般称之为刚性事务,常见的单体应用(单个数据源)内部都是采用刚性事务。 最常见的事务的例子就是银行转账: 2. 分布式事务 分布式事务,指的是分布式系统间完成的事务机制,百度百科定义如下: 数据库事务( transaction)是访问并可能操作各种数据项的一个数据库操作序列,这些操作要么全部执行,要么全部不执行,是一个不可分割的工作单位。 分布式同数据库事务一样,都用以保证数据的一致性,只是事务的参与者由原来的单数据源的一组操作序列变成了分布式系统的多个子系统。 举个例子: 用户下单流程,如下图所示: 首先,订单系统订单支付完成,然后调用库存系统扣减库存,最后再调用积分系统给账户添加积分,第1、2、3步必须全部执行成功,那么下单操作才算成功,否则,有一步出现错误,那么下单操作就是失败的。与传统事务相比,分布式事务范围在数据库事务之上进行了放大,各个系统除了有本系统事务外,还需要符合整个分布式系统的事务机制,才能保证最终的数据一致性。 但是,在分布式系统中,由于其固有的复杂性,很难保证事务的ACID特性,这种刚性事务在分布式系统中不适用,这就要提到两个概念:CAP定理和BASE理论。 2.1. CAP理论 2000年,名为Eric Brewer的教授在PODC研讨会上提出了CAP:一致性、可用性和分区容错性三者无法在分布式系统中被同时满足,并且最多只能满足其中两个!他还对CAP进行明确的定义: C(Consistency,一致性):所有的节点上的数据时刻保持同步 A(Avallable,可用性):每个请求都能接受到一个响应,无论响应成功或失败 P(Partition tolerance,分区容错性):系统应该能持续提供服务,即使系统内部有消息丢失(分区) CAP理论的提出,揭露了分布式系统的本质。 怎么理解CAP? 一致性,其实就是分布式系统各节点间数据达成一致;可用性,分布式系统始终保持可用,即使出现了数据不一致;分区容错性,分布式系统必须有一定的容错能力,当系统数据丢失或者某些节点不可用(分区)系统还能继续提供服务,系统容错是不可缺少的。 既然CAP三者不能同时满足,而分布式系统中,分区容错性是不可或缺的,因此,只能在CP和AP中间选择。 由于在CAP理论要求各节点数据时刻保持同步,但是这样的强一致性只能在CP中保证,而选择满足AP时无法保证,这就引出BASE理论。 2.2. BASE理论 eBay的架构师Dan Pritchett源于对大规模分布式系统的实践总结,在ACM上发表文章提出BASE理论,BASE理论是对CAP理论的延伸,核心思想是即使无法做到强一致性,但应用可以采用适合的方式达到最终一致性(Eventual Consitency)。 BA: Basically Available(基本可用) S: Soft state(软状态) E: Eventually consistent(最终一致性) 显然,BASE理论满足CAP中的AP,从而舍弃了强一致性C。 满足BASE理论,放弃强一致性,数据最终达到一致的事务我们称之为柔性事务(Flexible Transactions)。 如果分布式系统满足AP,那么虽然数据不可能时刻保持一致,但是可以达到最终一致;如果满足产CP,那么分布式系统舍弃了高可用性,却可以保持数据强一致。分布式系统中,常见的两个服务注册中心Eureka、Consul,前者满足的是AP,而后者满足的是CP。 3. 分布式事务的解决方案 分布式事务解决的核心问题就是分布式系统中各个节点的数据一致性问题,选择强一致还是最终一致,需要根据具体业务需要合理做出选择。目前,常见的几种分布式事务解决方案有2PC(两阶段提交)、3PC(三阶段提交)、TCC(补偿性事务)、本地消息表等。 3.1. 2PC 两阶段提交(2PC),其实是XA协议的标准实现,XA协议由Tuxedo首先提出的,并交给X/Open组织,作为资源管理器(数据库)与事务管理器的接口标准。目前,Oracle、Informix、DB2和Sybase等各大数据库厂家都提供对XA的支持。 2PC事务包括几个角色:事务协调者(coordinator)、事务参与者(participant )、资源管理器(resource manager,RM): 事务协调者:负责收集事务参与者的信息,并决定事务提交/回滚操作 事务参与者:按照事务协调者要求参与事务,可进行事务预提交、提交或者回滚操作 资源管理器:事务参与者管理的数据库 2PC将事务的管理分为两个阶段: 1、第一阶段(prepare):事务协调者要求每个事务参与者预提交(precommit)事务,事务并不会真正提交,并反馈(vote)提交结果 2、第二阶段(commit/rollback):事务协调者收集参与者信息,如果每个参与者都反馈可提交事务,那么协调者下发commit指令给参与者要求提交事务,否则,如果有参与者反馈回滚事务,则协调者下发abort指令要求参与者都回滚事务 通俗的讲:协调者首先发起事务,说参与者们检查一下看看各自操作是否能够正常执行啊,不管可以与否都给我一个反馈。然后,参与者检查完成,都反馈说,老大,我们都能正常执行,那么协调者就说好吧,你们都提交事务吧;如果某一个参与者说,完了,我操作执行失败了,不能提交事务,那么协调者就告诉各参与者:某个哥们儿不能提交事务,数据一致性没法保证了,你们都回滚吧! ...

2020-04-15 · 1 min · 170 words · Hank

使用Redis实现分布式锁

在单体Java应用中,由于代码运行于同一个JVM,使用实现资源加锁是比较容易的,例如使用synchronized或ReentrantLock加锁来控制并发访问;但是在分布式系统中,多个分布式系统之间也需要控制并发访问,由于处于不同的JVM,此时就不能简单使用java的锁机制来进行控制。这种跨进程或者跨服务器的加锁,需要额外使用全局的获取锁的服务,就是本文探讨的分布式锁。 1. 为什么需要分布式锁 分布式锁解决的问题:保证分布式系统的共享资源在某一时刻只被一个客户端访问,保证数据的准确性。 举个例子: 如图所示,订单服务下单前需要保证库存足够,库存服务首先会检查库存充足,然后在将订单的库存数量锁定,如果此时管理系统对库存数量进行了修改,那么由于跨系统的并发操作可能操作库存数据的不正确。此时,对库存的操作就需要考虑分布式锁,将库存锁定,暂时不能更改。 这样一来,对库存的更改和扣减操作使用同一把锁来锁定,每次只有一个客户端能够操作成功,要么订单服务先扣减服务,要么管理系统先修改库存,反正两个不能同时进行。 2. 分布式锁的现有方案 分布式锁的整体思路是:在分布式系统中,有一个全局的东西(中间件或服务),各个服务需要加锁时都向它获取锁,然后给锁一个标记(例如锁的名称),如果标记相同则认为是同一把锁,这样就可以控制各个系统的资源共享。 目前,分布式锁的方案大致有以下几种: 基于Zookeeper的临时节点 基于Redis的SET命令 这里仅仅讨论Redis的分布式锁实现。 3. Redis实现分布式锁的原理 基于Redis来实现分布式锁,其原理很简单:在Redis中设置一个Key,表示加锁,如果其他系统来加锁时发现这个Key已经存在,表示已经加了锁,则它获取锁失败,然后它再不断重试加锁,直到加锁成功,然后才能执行后续业务逻辑。释放锁也很简单,直接将这个KEY删除即可。 锁一旦被创建,就必须能够释放,否则会引起死锁(其他系统永远获取不到锁),一般会使用Redis的过期机制,让KEY一段时间后自动过期以避免死锁。 加锁时,过程如下: 首先,使用SET命令来为某一个KEY(可以作为锁名称)设置一个唯一的值,仅当KEY不存在时才能加锁成功,如果KEY存在则设置失败,表明锁已经存在; 其次,为该KEY设置一个过期时间,来避免死锁问题; 释放锁时,先获取锁是否存在,如果存在则调用DEL命令删除该KEY。 无论是加锁,还是释放锁,都需要保证命令的原子性执行(要么都成功,要么都失败,试想一下,如果加锁时SET命令成功,然后在调用EXPIRE命令设置过期时间,未完成时Redis宕机了,会造成死锁)。例如,加锁时,SET命令和设置过期时间需要为一个原子命令,Redis已经提供了原子命令,如下: // NX是指如果key不存在就成功,key存在返回false,PX指定过期时间,单位毫秒 SET anyLock unique_value NX PX 30000 释放锁时,获取锁和删除KEY为一个原子操作,Redis没有提供获取KEY然后DEL的原子命令,这里需要用到LUA脚本以保证原子性: // 执行LUA脚本保证原子性,先获取锁,然后调用DEL删除 if redis.call("get",KEYS[1]) == ARGV[1] then return redis.call("del",KEYS[1]) else return 0 end 需要注意的是,加锁时设置的KEY值value必须是唯一的,这是因为在释放锁时需要获取到该值以便验证释放锁的客户端和加锁的客户端是同一客户端,如果value值不唯一则可能客户端A加了锁,但是由客户端B给释放了,引起业务混乱而没有达到加锁的目的。 4. 三种部署方式下的锁问题 Redis有三种部署方式,每种方式下的分布式锁都存在一些问题: 1、单机部署 这种方式下,很明显的缺点就是单点问题,Redis故障了,那么分布式锁就不能使用了。 2、Master-Slave + Sentinel模式 主从+哨兵模式,主节点挂了,哨兵会重新选择一个从节点作为主节点,数据会复制到从节点上,但是复制过程需要一定的时间,如果主节点挂了,它上边的锁可能还没有复制到从节点上,就会造成锁丢失。 3、Cluster模式 集群部署模式,同理,在某一个节点上的锁可能还没有复制到其他节点上,同样会造成锁丢失。 使用Redis的分布式锁,其优点是性能很高,支持高并发分布式锁场景,缺点则是如果加锁失败,需要不断循环重试加锁,消耗资源,另外,Redis集群下可能造成锁丢失的极端情况,对于这种情况,Redis的作者也考虑到了,他提出了RedLock算法,具体可以看 这里。 5. 使用Redisson的分布式锁 一般而言,不推荐自己实现Redis分布式锁,因为需要考虑诸如锁重入等多种情况,Java的Redisson框架已经为我们提供了分布式锁的支持。 Redisson是一个Java版的Redis Client,提供了大量的基于Redis的分布式特性支持,例如 分布式锁、分布式任务调度、分布式远程服务、分布式集合等等,Redisson官网: https://redisson.org/ 要使用Redisson的分布式锁非常简单,基本的代码如下: RLock lock = redisson.getLock("anyLock"); lock.lock(); // do something …… lock.unlock(); ...

2019-10-17 · 2 min · 392 words · Hank