你们在关于微服务间数据一致性问题,是如何解决的?
传统应用的事务管理
本地事务
事务的特征
ACID原子(原子, 一致, 隔离, 持久)
并发事务问题
- 脏读 (读到了其他事务没有提交的修改)
- 不可重复读 (读到了其他事务提交的修改)
- 幻影读 (做count操作的时候在同一事务中获得结果不一致)
事务隔离级别
- read uncommit
可以读到没有提交的修改 - read commit
可以读到已经提交的修改 - repeatable read
可重复读, 在一个事务中 - serializable
串行化, 只有在一个事务完成之后再执行下一个事务事务和Connection 在JDBC的事务操作中,必须操作的是同一个Connection连接吗?
当JDBC程序向DriverManagement获得一个Connection对象时,默认情况下这个Connection对象会自动向数据库提交在它上面发送的SQL语句.
statement.executeQuery(). 然后就直接提交给数据库了.若想关闭这种默认提交方式,让多条SQL在一个事务中执行,可使用下列语句:再扩展下,我们直到SQL语句到达MySql之后会有针对SQL语句的编译缓存,也就是执行计划缓存,由于对SQL语句解析和执行计划分析是比较耗时的,于是如果同样的SQL语句发送过来,并且是同一个Connection的,其实也就是同一个TCP链接的,就可以直接从缓存中读取,(因为事务的等级是repeatable read)当Connection断开也就是TCP链接断开,缓存也就失效了。1
2
3Connection.setAutoCommit(false); // 相当于start transaction
Connection.rollback(); //rollback
Connection.commit(); //commit
在MyBatis中的事务管理有两种模式
1 | org.apache.ibatis.transaction.jdbc.JdbcTransaction |
其实前者org.apache.ibatis.transaction.jdbc.JdbcTransaction就是封装了java.sql.Connection类的事务操作而已,禁止了自动提交模式
也就是说在数据库自己的事务层次,一次Connection的提交是处理基本单元,即认为Connection的一次提交就是一次事务。所以在ORM层次的MyBatis,封装JDBC Connection 的事务逻辑中,始终都是围绕着Connection类的.
1 |
|
本地事务的的实现
传统单机应用使用一个RDBMS作为数据源。应用开启事务,进行CRUD,提交或回滚事务,统统发生在本地事务中,由资源管理器(RM)直接提供事务支持。数据的一致性在一个本地事务中得到保证。
Spring中有7中的事务传播级别, 用@Transactional(propagation = value) 注解进行标注, 这个标注记号可以使用在类或者方法上.
- REQUIRED(没有事务创建事务, 有事务使用当前事务, 默认, 被设计成这个级别时, 会为每一个被调用的方法创建一个逻辑事务域. 如果前面的方法已经创建了事务,后面的方法支持当前的事务,如果当前没有事务会重新建立事务)
- SUPPORTS (支持当前事务, 当前没有事务,就以非事务方式执行)
- MANDATORY(该方法必须运行在一个事务中.如果当前没有事务正在发生,将抛出一个异常)
- REQUIRES_NEW(新建事务, 如果当前存在事务,把当前事务挂起)
- NOT_SUPPORTED(在没有事务的方式执行操作, 如果当前存在事务, 把当前事务挂起)
- NEVER (在没有事务的情况下执行.如果一个事务正在运行,则抛出一个异常)
- NESTED(支持当前事务, 新增SavePoint, 外层事务失败的时候全部回滚,内层事务失败的时候并不会发生回滚)
分布式事务
该部分内容全部抄袭自分布式系统理论基础 - 一致性、2PC和3PC
一致性
假设一个具有N个节点的分布式系统,当其满足以下条件时,我们说这个系统满足一致性:
全认同(agreement): 所有N个节点都认同一个结果
值合法(validity): 该结果必须由N个节点中的节点提出
可结束(termination): 决议过程在一定时间内结束,不会无休止地进行下去
但就这样看似简单的事情,分布式系统实现起来并不轻松,因为它面临着这些问题:
- 消息传递异步无序(asynchronous): 现实网络不是一个可靠的信道,存在消息延时、丢失,节点间消息传递做不到同步有序(synchronous)
- 节点宕机(fail-stop): 节点持续宕机,不会恢复
- 节点宕机恢复(fail-recover): 节点宕机一段时间后恢复,在分布式系统中最常见
- 网络分化(network partition): 网络链路出现问题,将N个节点隔离成多个部分
- 拜占庭将军问题(byzantine failure): 节点或宕机或逻辑失败,甚至不按套路出牌抛出干扰决议的### 两阶段提交(2PC)
对照现实中的问题
我: 老王,今晚7点老地方,搓够48圈不见不散!
……
(第二天凌晨3点) 隔壁老王: 没问题! // 消息延迟
我: ……
我: 小张,今晚7点老地方,搓够48圈不见不散!
小张: No ……
(两小时后……)
小张: No problem! // 宕机节点恢复
我: ……
我: 老李头,今晚7点老地方,搓够48圈不见不散!
老李: 必须的,大保健走起! // 拜占庭将军
(这是要打麻将呢?还是要大保健?还是一边打麻将一边大保健……)
我: 老李头们,今晚7点老地方,搓够48圈不见不散!
老李头A们: 联系不上 一致采纳A的意见去西门
老李头B们: 联系不上 一致采纳B的意见去东门
我们把以上所列的问题称为系统模型(system model),讨论分布式系统理论和工程实践的时候,必先划定模型。例如有以下两种模型:
异步环境(asynchronous)下,节点宕机(fail-stop)
异步环境(asynchronous)下,节点宕机恢复(fail-recover)、网络分化(network partition)
2比1多了节点恢复、网络分化的考量,因而对这两种模型的理论研究和工程解决方案必定是不同的,在还没有明晰所要解决的问题前谈解决方案都是一本正经地耍流氓。
一致性还具备两个属性,一个是强一致(safety),它要求所有节点状态一致、共进退;一个是可用(liveness),它要求分布式系统24*7无间断对外服务。FLP定理(FLP impossibility) 已经证明在一个收窄的模型中(异步环境并只存在节点宕机),不能同时满足 safety 和 liveness。
2 Pharse Commit
2PC(tow phase commit)两阶段提交顾名思义它分成两个阶段,先由一方进行提议(propose)并收集其他节点的反馈(vote),再根据反馈决定提交(commit)或中止(abort)事务。我们将提议的节点称为协调者(coordinator),其他参与决议节点称为参与者(participants, 或cohorts):
在异步环境(asynchronous)并且没有节点宕机(fail-stop)的模型下,2PC可以满足全认同、值合法、可结束,是解决一致性问题的一种协议。但如果再加上节点宕机(fail-recover)的考虑,2PC是否还能解决一致性问题呢?
coordinator如果在发起提议后宕机,那么participant将进入阻塞(block)状态、一直等待coordinator回应以完成该次决议。这时需要另一角色把系统从不可结束的状态中带出来,我们把新增的这一角色叫协调者备份(coordinator watchdog)。coordinator宕机一定时间后,watchdog接替原coordinator工作,通过问询(query) 各participant的状态,决定阶段2是提交还是中止。这也要求 coordinator/participant 记录(logging)历史状态,以备coordinator宕机后watchdog对participant查询、coordinator宕机恢复后重新找回状态。
从coordinator接收到一次事务请求、发起提议到事务完成,经过2PC协议后增加了2次RTT(propose+commit),带来的时延(latency)增加相对较少。
3 Pharse Commit
3PC(three phase commit)即三阶段提交,既然2PC可以在异步网络+节点宕机恢复的模型下实现一致性,那还需要3PC做什么,3PC是什么鬼?
在2PC中一个participant的状态只有它自己和coordinator知晓,假如coordinator提议后自身宕机,在watchdog启用前一个participant又宕机,其他participant就会进入既不能回滚、又不能强制commit的阻塞状态,直到participant宕机恢复。这引出两个疑问:
能不能去掉阻塞,使系统可以在commit/abort前回滚(rollback)到决议发起前的初始状态
当次决议中,participant间能不能相互知道对方的状态,又或者participant间根本不依赖对方的状态
相比2PC,3PC增加了一个准备提交(prepare to commit)阶段来解决以上问题:
coordinator接收完participant的反馈(vote)之后,进入阶段2,给各个participant发送准备提交(prepare to commit)指令。participant接到准备提交指令后可以锁资源,但要求相关操作必须可回滚。coordinator接收完确认(ACK)后进入阶段3、进行commit/abort,3PC的阶段3与2PC的阶段2无异。协调者备份(coordinator watchdog)、状态记录(logging)同样应用在3PC。
participant如果在不同阶段宕机,我们来看看3PC如何应对:
- 阶段1: coordinator或watchdog未收到宕机participant的vote,直接中止事务;宕机的participant恢复后,读取logging发现未发出赞成vote,自行中止该次事务
- 阶段2: coordinator未收到宕机participant的precommit ACK,但因为之前已经收到了宕机participant的赞成反馈(不然也不会进入到阶段2),coordinator进行commit;watchdog可以通过问询其他participant获得这些信息,过程同理;宕机的participant恢复后发现收到precommit或已经发出赞成vote,则自行commit该次事务
- 阶段3: 即便coordinator或watchdog未收到宕机participant的commit ACK,也结束该次事务;宕机的participant恢复后发现收到commit或者precommit,也将自行commit该次事务
因为有了准备提交(prepare to commit)阶段,3PC的事务处理延时也增加了1个RTT,变为3个RTT(propose+precommit+commit),但是它防止participant宕机后整个系统进入阻塞态,增强了系统的可用性,对一些现实业务场景是非常值得的。微服务事务管理
- 由于微服务间无法直接进行数据访问,微服务间互相调用通常通过RPC(dubbo)或Http API(SpringCloud)进行,所以已经无法使用TM统一管理微服务的RM。
- 不同的微服务使用的数据源类型可能完全不同,如果微服务使用了NoSQL之类不支持事务的数据库,则事务根本无从谈起。
- 即使微服务使用的数据源都支持事务,那么如果使用一个大事务将许多微服务的事务管理起来,这个大事务维持的时间,将比本地事务长几个数量级。如此长时间的事务及跨服务的事务,将为产生很多锁及数据不可用,严重影响系统性能。
BASE理论由eBay的架构师Dan Pritchett提出,BASE理论是对CAP理论的延伸,核心思想是即使无法做到强一致性,应用应该可以采用合适的方式达到最终一致性。BASE是指基本可用(Basically Available)、软状态( Soft State)、最终一致性( Eventual Consistency)。
BASE中的最终一致性是对于微服务下的事务管理的根本要求,既基于微服务的事务管理无法达到强一致性,但必须保证最重一致性。那么,有哪些方法可以保证微服务下的事务管理的最终一致性呢,按照实现原理分主要有两类,事件通知型和补偿型,其中事件通知型又可分为可靠事件通知模式及最大努力通知模式,而补偿型又可分为TCC模式、和业务补偿模式两种。这四种模式都可以达到微服务下的数据最终一致性。
本地事件服务
- 当业务执行时,在同一个本地事务中将事件写入本地事件表,同时投递该事件,如果事件投递成功,则将该事件从事件表中删除。
- 如果投递失败,则使用事件服务定时地异步统一处理投递失败的事件,进行重新投递,直到事件被正确投递,并将事件从事件表中删除。这种方式最大可能地保证了事件投递的实效性,并且当第一次投递失败后,也能使用异步事件服务保证事件至少被投递一次。
外部事件服务
业务服务在提交前,向事件服务发送事件,事件服务只记录事件,并不发送。业务服务在提交或回滚后通知事件服务,事件服务发送事件或者删除事件。不用担心业务系统在提交或者会滚后宕机而无法发送确认事件给事件服务,因为事件服务会定时获取所有仍未发送的事件并且向业务系统查询,根据业务系统的返回来决定发送或者删除该事件。
可靠事件模式需要注意的有两点,
- 事件的正确发送;
- 事件的重复消费。
通过异步消息服务可以确保事件的正确发送,然而事件是有可能重复发送的,那么就需要消费端保证同一条事件不会重复被消费,简而言之就是保证事件消费的幂等性。
如果事件本身是具备幂等性的状态型事件,如订单状态的通知(已下单、已支付、已发货等),则需要判断事件的顺序。
一般通过时间戳来判断,既消费过了新的消息后,当接受到老的消息直接丢弃不予消费。如果无法提供全局时间戳,则应考虑使用全局统一的序列号。
对于不具备幂等性的事件,一般是动作行为事件,如扣款100,存款200,则应该将事件id及事件结果持久化,在消费事件前查询事件id,若已经消费则直接返回执行结果;若是新消息,则执行,并存储执行结果。
最大努力通知模式
最大努力通知型的特点是,业务服务在提交事务后,进行有限次数(设置最大次数限制)的消息发送,比如发送三次消息,若三次消息发送都失败,则不予继续发送。所以有可能导致消息的丢失。
业务补偿模式
补偿模式比起事件通知模式最大的不同是,补偿模式的上游服务依赖于下游服务的运行结果,而事件通知模式上游服务不依赖于下游服务的运行结果。首先介绍业务补偿模式,业务补偿模式是一种纯补偿模式,其设计理念为,业务在调用的时候正常提交,当一个服务失败的时候,所有其依赖的上游服务都进行业务补偿操作
小明从杭州出发,去往美国纽约出差,现在他需要定从杭州去往上海的火车票,以及从上海飞往纽约的飞机票。如果小明成功购买了火车票之后发现那天的飞机票已经售空了,那么与其在上海再多待一天,小明还不如取消去上海的火车票,选择飞往北京再转机纽约,所以小明就取消了去上海的火车票。这个例子中购买杭州到上海的火车票是服务a,购买上海到纽约的飞机票是服务b,业务补偿模式就是在服务b失败的时候,对服务a进行补偿操作,在例子中就是取消杭州到上海的火车票。
补偿模式要求每个服务都提供补偿借口,且这种补偿一般来说是不完全补偿,既即使进行了补偿操作,那条取消的火车票记录还是一直存在数据库中可以被追踪(一般是有相信的状态字段“已取消”作为标记),毕竟已经提交的线上数据一般是不能进行物理删除的。
补偿模式要求每个服务都提供补偿借口,且这种补偿一般来说是不完全补偿,既即使进行了补偿操作,那条取消的火车票记录还是一直存在数据库中可以被追踪(一般是有相信的状态字段“已取消”作为标记),毕竟已经提交的线上数据一般是不能进行物理删除的。
TCC/Try Confirm Cancel模式
TCC模式是一种优化了的业务补偿模式,它可以做到完全补偿,既进行补偿后不留下补偿的纪录,就好像什么事情都没有发生过一样。同时,TCC的软状态时间很短,原因是因为TCC是一种两阶段型模式(已经忘了两阶段概念的可以回顾一下1.2.1),只有在所有的服务的第一阶段(try)都成功的时候才进行第二阶段确认(Confirm)操作,否则进行补偿(Cancel)操作,而在try阶段是不会进行真正的业务处理的。
和2PC很像
- Try,业务服务完成所有的业务检查,预留必需的业务资源
- 如果Try在所有服务中都成功,那么执行Confirm操作,Confirm操作不做任何的业务检查(因为try中已经做过),只是用Try阶段预留的业务资源进行业务处理;否则进行Cancel操作,Cancel操作释放Try阶段预留的业务资源.
example:
服务a(小明从招行转出100元):
try: update cmb_account set balance=balance-100, freeze=freeze+100 where acc_id=1 and balance>100;
confirm: update cmb_account set freeze=freeze-100 where acc_id=1;
cancel: update cmb_account set balance=balance+100, freeze=freeze-100 where acc_id=1;
服务b(小明往广发银行汇入100元):
try: update cgb_account set freeze=freeze+100 where acc_id=1;
confirm: update cgb_account set balance=balance+100, freeze=freeze-100 where acc_id=1;
cancel: update cgb_account set freeze=freeze-100 where acc_id=1;
具体说明:
a的try阶段,服务做了两件事,1:业务检查,这里是检查小明的帐户里的钱是否多余100元;2:预留资源,将100元从余额中划入冻结资金。
a的confirm阶段,这里不再进行业务检查,因为try阶段已经做过了,同时由于转账已经成功,将冻结资金扣除。
a的cancel阶段,释放预留资源,既100元冻结资金,并恢复到余额。
b的try阶段进行,预留资源,将100元冻结。
b的confirm阶段,使用try阶段预留的资源,将100元冻结资金划入余额。
b的cancel阶段,释放try阶段的预留资源,将100元从冻结资金中减去。
从上面的简单例子可以看出,TCC模式比纯业务补偿模式更加复杂,所以在实现上每个服务都需要实现Cofirm和Cancel两个接口。