《JAVA分布式事务解决方案.docx》由会员分享,可在线阅读,更多相关《JAVA分布式事务解决方案.docx(26页珍藏版)》请在三一办公上搜索。
1、JAVA分布式事务解决方案目录1. 基础知识22. mysql如何保证持久性和原子性42.1 undo 日志42.2 redo log52.3总结73. 分布式事务83.1应用场景83.2 CAP 理论83.3 BASE 理论93.4二阶段提交103.5TCC 模式123.6可靠消息服务153.7AT 模式201.基础知识1)事务事务由一组操作构成,我们希望这组操作能够全部正确执行,如果这一 组操作中的任意一个步骤发生错误,那么就需要回滚之前已经完成的操作。 也就是同一个事务中的所有操作,要么全都正确执行,要么全都不要执行。2)事务的四大特性ACID原子性:事务是一个不可分割的执行单元,事务中
2、的所有操作那么全 部执行,要么全部不执行;隔离性:事务的执行是相互独立的,它们不会相互干扰,一个事务不 会看到另一个正在运行过程中的事务的数据;持久性:持久性要求,一个事务完成之后,事务的执行结果必须是持 久化保存的。即使数据库发生崩溃,在数据库恢复后事务提交的结果仍然不 会丢失; 一致性:事务在开始前和结束后,数据库的完整性约束没有被破坏。3)脏读、幻读、虚读及不可重复读脏读:如果一个事务中对数据进行了更新,但事务还没有提交,另一 个事务可以“看到”该事务没有提交的更新结果,这样造成的问题就是,如果 第一个事务回滚,那么,第二个事务在此之前所 “看到”的数据就是一笔脏数 据。不可重复读:包括
3、幻读和虚读两种情况幻读:事务1在两次查询的过程中,事务2对该表进行了插入、删除 操作,从而事务1第二次查询的结果发生了变化。虚读:在事务1两次读取同一记录的过程中,事务 2对该记录进行了 修改,从而事务1第二次读到了不一样的记录。4)数据库的四种隔离级别读未提交(readuncommitted): 在该级别下,一个事务对一行数据修改 的过程中,不允许另一个事务对该行数据进行修改,但是允许另一个事务对该行数据读。因此在本级别下,不会出现更新丢失,但会出现脏读,不可重 复读。读提交(ReadCommitted):在该隔离级别下,不允许两个未提交的事务 之间并行执行,但它允许在一个事务执行的过程中,
4、另外一个事务得到执行 并提交。这样,会出现一种情况,第一个事务前后两次select出来的某行数据,值可能不一样。值改变的原因是,穿插执行的事务2对该行数据进行了update操作。在同一个事务中,两次select出来的值不相同的问题称为不可 重复读问题。要解决不可重复读问题,需要把数据的隔离级别设置为可重复 读。重复读(Repeatableread):在该隔离级别下,在一个事务使用某行数据 的过程中,不允许别的事务再对该行数据进行操作。可重复读应该是给数据 库的行加上了锁。这种隔离级别下,依旧允许别的事务在该表中插入和删除 数据,于是就会出现,在事务1执行的过程中,如果先后两次select出符合
5、 某个条件的行,如果在这两次select过程中另一个事务得到了执行,insert 或者delete 了某些行,就会出现先后两次select出来的符合同一条件的结果 不一样,第一次select好像出现了幻觉一样,因此这个问题也被称为幻读。要解决幻读问题,需要将数据库的隔离级别设置为串行化。序列化(serializable):该级别要求所有事务都必须串行执行,因此 能避免一切因并发引起的问题,但效率很低。注:mysql默认的隔离级别是重复读级别,oracle是读提交5) 乐观锁和悲观锁乐观锁:总是假设最好的情况,每次去拿数据的时候都认为别人不会 修改,所以不会上锁,但是在更新的时候会判断一下在此期
6、间别人有没有去 更新这个数据,可以使用版本号机制和CAS算法实现。乐观锁适用于多读的应用类型,这样可以提高吞吐量。悲观锁:总是假设最坏的情况,每次去拿数据的时候都认为别人会修 改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直 到它拿到锁(共享资源每次只给一个线程使用,其它线程阻塞,用完后再把 资源转让给其它线程)。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。Java中synchronized和ReentrantLock等独占锁就是悲观锁思想的实现。2. mysql如何保证持久性和原子性在数据库系统中,既有存放数据的文件
7、,也有存放日志的文件。日志在内存中也是有缓存Log buffer,也有磁盘文件log fiLeMySQL 中的日志文件,有这么两种与事务有关:undo日志与redo日志。2.1 undo 日志数据库事务具备原子性(atomicity)如果事务执行失败,需要把数据回滚。事务同时还具备持久性(durability)事务对数据所做的变更需要保存到硬 盘,不能因为故障而丢失。事务的原子性可以利用undo日志来实现。undo log的原理很简单,为了满足事务的原子性,在操作任何数据之前,首先将数据备份到undo log,然后进行数据的修改。如果出现了错误或 者用户执行了 rollback语句,系统可以利
8、用undo log中的备份将数据恢复 到事务开始之前的状态。数据库写入数据到磁盘之前,会把数据先缓存到内存中,事务提交时才 会写入磁盘中。用undo log实现原子性和持久化的事务的简化过程如下:假设有A、B两个数据,值分别为1、2:1) 事务开始2) 记录 A=1 到 undo log buffer3) 修改A=34) 记录 B=2 到 undo log buffer5) 修改B=46) 将undo log写到磁盘如何保证持久性?事务提交前,会把修改数据刷到磁盘,也就是说只要事务提交了,数据 肯定持久化了。如何保证原子性?每次对数据库修改,都会把修改前数据记录在undo log中,那么需要回
9、滚时,可以读取undo log,恢复数据。若系统在7)和8)之间崩溃,如何处理?此时事务并未提交,需要回滚。而 undo log已经被持久化,可以根据 undo log来恢复数据。若系统在7)之前崩溃,如何处理?此时数据并未持久化到硬盘,依然保持在事务之前的状态。缺陷:每个事务提交前将数据和undo log写入磁盘,这样会导致大量的 磁盘IO,因此性能很低。如果能够将数据缓存一段时间,就能减少IO提高性能,但是这样就会丧失事务的持久性,因此引入了另外一种机制来实现持久化,即redo log。2.2 redo log和undo log相反,redo log记录的是新数据的备份。在事务提交前,只
10、要将redo log持久化即可,不需要将数据持久化,减少了 IO的次数。先来看一下基本原理,undo + redo事务的简化过程:2.2.1安全性和性能问题如何保证原子性?如果在事务提交前故障,通过 undo log日志恢复数据。如果undo log都 还没写入,那么数据就尚未持久化,无需回滚。如何保证持久化?大家会发现,这里并没有出现数据的持久化。因为数据已经写入redolog,而redo log持久化到了硬盘,因此只要到了步骤 9)以后,事务是可以 提交的。内存中的数据库数据何时持久化到磁盘?因为redo log已经持久化,因此数据库数据写入磁盘与否影响不大,不 过为了避免出现脏数据(内存
11、中与磁盘不一致),事务提交后也会将内存数 据刷入磁盘(也可以按照设定的频率刷新内存数据到磁盘中)。 redolog何时写入磁盘?redo log会在事务提交之前,或者redo log buffer 了的时候写入磁 盘。2.2.2存在的问题这里存在两个问题:1) 问题1:之前是写undo和数据库数据到硬盘,现在是写 undo和 redo到磁盘,似乎没有减少IO次数数据库数据写入是随机IO,性能很差; redolog在初始化时会开辟一段连续的空间,写入是顺序IO,性能很好;实际上undolog并不是直接写入磁盘,而是先写入到 undologbuffer 中,当redolog持久化时,undolog
12、就同时持久化到硬盘了。因此事务提交前,只需要对redo log持久化即可。另外,redo log并不是写入一次就持久化一次,redo log在内存中也有 自己的缓冲池redo log buffer。每次写redo log都是写入到buffer,在提交时一次性持久化到磁盘,减少IO次数。2)问题2: redolog数据是写入内存buffer中,当buffer满或者事务 提交时,将buffer数据写入磁盘。redolog中记录的数据,有可能包含尚未 提交的事务,如果此时数据库崩溃,那么如何完成数据恢复?数据恢复有两种策略:恢复时,只重做已经提交了的事务恢复时,重做所有事务,包括未提交的事务和回滚了
13、的事务。然后通过undolog回滚那些未提交的事务。InnoDB引擎采用的是第二种方案,因此 undo log要在redo log前持久 化。2.3总结 undolog记录更新前数据,用于保证事务原子性 redolog记录更新后数据,用于保证事务的持久性 redolog有自己的内存buffer,先写入到buffer,事务提交时写入磁盘 redolog持久化之后,意味着事务是可提交的3.分布式事务3.1应用场景当我们的系统采用了微服务架构后,一个电商系统往往被拆分成如下几 个子系统:商品系统、订单系统、支付系统、积分系统等。整个下单的过程 如下:1)用户通过商品系统浏览商品,他看中了某一项商品,
14、便点击下单2)此时订单系统会生成一条订单3)订单创建成功后,支付系统提供支付功能4)当支付完成后,由积分系统为该用户增加积分上述2)、3)、4)需要在一个事务中完成。对于传统单体应用而言, 实现事务非常简单,只需将这三个步骤放在一个方法A中,再用spring的Transactional注解标识该方法即可。Spring通过数据库的事务支持,保证 这些步骤要么全部执行完成,要么全都不执行。但在这个微服务架构中,这 三个步骤涉及三个系统,涉及三个数据库,此时我们必须在数据库和应用系 统之间,通过某项黑科技,实现分布式事务的支持。3.2 CAP理论在一个分布式系统中,最多只能满足C、A、P中的两个需求
15、: C-Consistency: 一致性,同一数据的多个副本是否实时相同 A-Availability:可用性,一定时间内,系统返回一个明确的结果,则 称为该系统可用 P-Partitiontolerance:分区容错性,将同一服务分布在多个系统中,从而保证某一个系统宕机,仍然有其他系统提供相同的服务。3.3 BASE 理论BASE是三个单词的缩写: BasicallyAvailable(基本可用) Softstate(软状态) Eventuallyconsistent(最终一致性)如下图所示,订单服务、库存服务、用户服务及他们对应的数据库就是 分布式应用中的三个部分。 CP方式:现在如果要满
16、足事务的强一致性,就必须在订单服务数据库 锁定的同时,对库存服务、用户服务数据资源同时锁定。等待三个服务业务 全部处理完成,才可以释放资源。此时如果有其他请求想要操作被锁定的资 源就会被阻塞,这样就是满足了 CP。(这就是强一致性,弱可用) AP方式:三个服务的对应数据库各自独立执行自己的业务,执行本地事务,不要求相互锁定资源。但是这个中间状态下,我们去访问数据库,可 能遇到数据不一致的情况,不过我们需要做一些后补措施,保证在经过一段 时间后,数据最终满足一致性(这就是高可用,但弱一致:最终一致性)。由上面的两种思想,延伸出了很多的分布式事务解决方案: XA TCC可靠消息最终一致性 AT3.
17、4二阶段提交1)正常情况如上图所示,正常情况下可分为两阶段:投票阶段协调组询问各个事务参与者,是否可以执行事务。每个事务参与者执行 事务,写入redo和undo日志,然后反馈事务执行成功的信息(agree)。提交阶段协调组发现每个参与者都可以执行事务(agree),于是向各个事务参与 者发出commit指令,各个事务参与者提交事务。Propose PhaseCommit Phase2)异常情况propose 1valuevoterlagreevoter2disagreevoter3abortabortedAbortedabortedcoordinator如上图所示,异常情况的处理方式为:投票阶
18、段:协调组询问各个事务参与者,是否可以执行事务。每个事务参与者执行事务,写入redo和undo日志,然后反馈事务执行结果。但只要有一个参与者返回的是Disagree,则说明执行失败。提交阶段:协调组发现一个或多个参与者返回的是Disagree,认为执行失败。于是向各个事务参与者发出abort指令,各个事务参与者回滚事 务。3)缺点2PC的缺点在于不能处理Fail-stop形式的节点failure比如下图这种情假设 cordinator和 voter3 都在 commit 这个阶段 crash 了,而 voterl 和 voter2没有收到commit消息。这时候voter1和voter2就陷入
19、了一个困境。 因为他们并不能判断现在是两个场景中的哪一种:上轮全票通过,然后voter3第一个收到了 commit消息并在commit操 作之后crash 了上轮voter3反对,所以干脆没有通过;4)阻塞问题在准备阶段、提交阶段,每个事物参与者都会锁定本地资源,并等待其 它事务的执行结果,阻塞时间较长,资源锁定时间太久,因此执行的效率就 比较低了。3.5 TCC模式TCC模式可以解决2PC中的资源锁定和阻塞问题,减少资源锁定时间。它本质是一种补偿的思路,事务运行过程包括三个方法: Try:资源的检测和预留 Confirm:执行的业务操作提交。要求 Try成功,Confirm 一定要成 功;
20、Cancel:预留资源释放执行分两个阶段:准备阶段(try):资源的检测和预留执行阶段(confirm/cancel):根据上一步结果,判断下面的执行方法。如果上一步中所有事务参与者都成功,则这里执行confirm ;反之,执行cancle粗看似乎与两阶段提交没什么区别,但其实差别很大: try、confirm、cancel都是独立的事务,不受其他参与者的影响,不会阻塞等待他人;try、confirm、cancel由程序员在业务层编写,锁力度由代码控制;以下单业务中的扣减余额为例来看下怎么编写,假设账户A原来余额是100,需要余额扣减30元。如图:1) 一阶段(try)-余额检查,并冻结用户部
21、分金额,此盼段执行完毕/事务已经提交-检查用户余额是否充足,如果充足,冻结部分余额-在账户表中添加冻结金额字段值为3S余额不芟2)二阶段-提交(Confirm):喜正的扣款,把冻结金额从余额中扣除,冻绐金额清空-修改冻结金额为跖修改余额为1阳-3。= 了3元-补僧(Cancel):释放之前冻结的金额,并非回滚-余额不变,修改账户冻结金额为03.5.1 TCC优缺点及使用场景1)优势TCC执行的每一个阶段都会提交本地事务并释放锁,并不需要等待其它 事务的执行结果。而如果其它事务执行失败,最后不是回滚,而是执行补偿 操作。这样就避免了资源的长期锁定和阻塞等待,执行效率比较高,属于性 能比较好的分布
22、式事务方式。2)缺点代码侵入:需要人为编写代码实现try、confirm、cancel,代码侵入较多开发成本高:一个业务需要拆分成 3个步骤,分别编写业务实现,业 务编写比较复杂安全性考虑:cancel动作如果执行失败,资源就无法释放,需要引入 重试机制,而重试可能导致重复执行,还要考虑重试时的幂等问题3)使用场景对事务有一定的一致性要求(最终一致)对性能要求较高开发人员具备较高的编码能力和幂等处理经验3.6可靠消息服务一般分为事务的发起者A和事务的其它参与者B :事务发起者A执行本地事务事务发起者A通过MQ将需要执行的事务信息发送给事务参与者B这个过程有点像你去学校食堂吃饭:事务参与者B接收
23、到消息后执行本地事务L执行本地事务服和3.接畋消恩4.执行本地业务拿着钱去收银处,点一份红烧牛肉面,付钱收银处给你发一个小票,还有一个号牌,你别把票弄丢!你凭小票和号牌一定能领到一份红烧牛肉面,不管需要多久几个注意事项:事务发起者A必须确保本地事务成功后,消息一定发送成功MQ必须保证消息正确投递和持久化保存事务参与者B必须确保消息最终一定能消费,如果失败需要多次重试事务B执行失败,会重试,但不会导致事务 A回滚那么问题来了,我们如何保证消息发送一定成功?如何保证消费者一定能收到消息?3.6.1本地消息表参看如下简化图:事务发起者开启本地事务执行事务相关业务发送消息到MQ把消息持久化到数据库,标
24、记为已发送提交本地事务事务接收者接收消息开启本地事务处理事务相关业务修改数据库消息状态为已消费提交本地事务额外的定时任务定时扫描表中超时未消费的消息,重新发送优点与tcc相比,实现方式较为简单,开发成本低。缺点数据一致性完全依赖于消息服务,因此消息服务必须是可靠的。需要处理被动业务方的幂等问题被动业务失败不会导致主动业务的回滚,而是重试被动的业务事务业务与消息发送业务耦合、业务数据与消息表要在一起3.6.2独立消息服务为了解决上述问题,我们会引入一个独立的消息服务,来完成对消息的 持久化、发送、确认、失败重试等一系列行为,大概的模型如下:一次消息发送时序图:事务发起者A的基本执行步骤:开启本地
25、事务通知消息服务,准备发送消息(消息服务将消息持久化,标记为准备 发送)执行本地业务:执行失败则终止,通知消息服务,取消发送(消息服 务修改订单状态);执行成功则继续,通知消息服务,确认发送(消息服务 发送消息、修改订单状态)提交本地事务消息服务本身提供下面的接口 :准备发送:把消息持久化到数据库,并标记状态为准备发送取消发送:把数据库消息状态修改为取消确认发送:把数据库消息状态修改为确认发送。尝试发送消息,成功后修改状态为已发送确认消费:消费者已经接收并处理消息,把数据库消息状态修改为已 消费定时任务:定时扫描数据库中状态为确认发送的消息,然后询问对应 的事务发起者,事务业务执行是否成功,结
26、果:业务执行成功,则尝试发送 消息,成功后修改状态为已发送;业务执行失败,则把数据库消息状态修改 为取消事务参与者B的基本步骤:接收消息开启本地事务执行业务通知消息服务,消息已经接收和处理提交事务优点:解除了事务业务与消息相关业务的耦合缺点:实现起来比较复杂3.6.3 RabbitMQ的消息确认RabbitMQ确保消息不丢失的思路比较奇特,并没有使用传统的本地表, 而是利用了消息的确认机制:生产者确认机制:确保消息从生产者到达 MQ不会有问题消息生产者发送消息到RabbitMQ时,可以设置一个异步的监听器,监 听来自MQ的ACK MQ接收到消息后,会返回一个回执给生产者:消息到达交换机后路由失
27、败,会返回失败 ACK消息路由成功,持久化失败,会返回失败 ACK消息路由成功,持久化成功,会返回成功 ACK生产者提前编写好不同回执的处理方式失败回执:等待一定时间后重新发送成功回执:记录日志等行为消费者确认机制:确保消息能够被消费者正确消费消费者需要在监听队列的时候指定手动 ACK模式 RabbitMQ把消息投递给消费者后,会等待消费者 ACK,接收到ACK 后才删除消息,如果没有接收到 ACK消息会一直保留在服务端,如果消费者 断开连接或异常后,消息会投递给其它消费者。消费者处理完消息,提交事务后,手动 ACK。如果执行过程中抛出异 常,则不会ACK,业务处理失败,等待下一条消息经过上面
28、的两种确认机制,可以确保从消息生产者到消费者的消息安 全,再结合生产者和消费者两端的本地事务,即可保证一个分布式事务的最 终一致性。3.7 AT模式基本原理:有没有感觉跟TCC的执行很像,都是分两个阶段: 一阶段:执行本地事务,并返回执行结果二阶段:根据一阶段的结果,判断二阶段做法:提交或回滚但AT模式底层做的事情可完全不同,而且第二阶段根本不需要我们编 写,全部有Seata自己实现了。也就是说:我们写的代码与本地事务时代码 一样,无需手动处理分布式事务。3.7.1详细处理流程1) 一阶段在一阶段,Seata会拦截“业务SQL”,首先解析SQL语义,找到“业务SQL”要更新的业务数据,在业务数
29、据被更新前,将其保存成“beforeimage”,然后执行“业务SQL”更新业务数据,在业务数据更新之后,再将其保存成“after image”,最后获取全局行锁,提交事务。以上操作全部在一个数据库事务内完成,这样保证了一阶段操作的原子性。这里的before image和after image类似于数据库的undo和redo日志,但其实是用数据库模拟的2)二阶段二阶段如果是提交的话,因为“业务SQL”在一阶段已经提交至数据库, 所以Seata框架只需将一阶段保存的快照数据和行锁删掉,完成数据清理即 可。二阶段如果是回滚的话,Seata就需要回滚一阶段已经执行的“业务 SQL”,还原业务数据。回滚方式便是用“before image”还原业务数据;但在 还原前要首先要校验脏写,对比“数据库当前业务数据”和“after image”,如 果两份数据完全一致就说明没有脏写,可以还原业务数据,如果不一致就说明有脏写,出现脏写就需要转人工处理。不过因为有全局锁机制,所以可以降低出现 脏写的概率。AT模式的一阶段、二阶段提交和回滚均由 Seata框架自动生成,用户 只需编写“业务SQL”,便能轻松接入分布式事务,AT模式是一种对业务无 任何侵入的分布式事务解决方案。