<一>CAP(分布式事务)
一、事务
事务简单理解就是更新数据库中各种数据的一个程序执行单元(unit),一个事务严格上必须具备原子性、一致性、隔离性和持久性,简称 ACID。
- 原子性(Atomicity):一个事务内的所有操作要么都执行,要么都不执行。
- 一致性(Consistency):一致性是指事务必须使数据库从一个一致性状态变换到另一个一致性状态,即数据库处理前后结果应与根据业务规则执行后的结果保持一致。
- 隔离性(Isolation):指的是多个事务并发执行的时候不会互相干扰。
- 持久性(Durability):指的是一个事务完成了之后数据就被永远保存下来,之后的其他操作或故障都不会对事务的结果产生影响。
二、分布式事务
1、概念:分布式事务顾名思义就是要在分布式系统中实现事务,它由多个本地事务组合而成。
2、使用分布式事务的根源:高并发,当一台服务器忙不过来,需要增加多服务器来帮忙响应请求。这时候就会出现一个问题,就是数据只有一份,如何保证分布式环境下,每个事物执行过后,数据都是正确的,比如库存服务器,订单服务器,支付服务器,他们分别部署,这个时候做一次购买完成就需要这三个服务都需要完成相应的操作,要么一起失败,要么一起成功。这就引出了分布式事务的解决问题。
三、CAP理论
1、理论基础:一个分布式系统,不可能同时做到下面这三点。
- P:分区容错:分布式系统一定会存在延迟、错误、断网等问题,这里要做容错处理,不会轻易让服务挂掉。
- C:一致性:数据事务执行完后结果是正确的。
- A:可用性:就是服务能够及时响应请求
2、为什么不能做到这三点
首先P一定是存在的,为什么呢,因为客户端在调用多个服务的时候,有可能会因为延迟,出现错误,断网等网络原因导致一个多个服务中的一个服务执行不成功,这是客观存在的,比如突然断网啥的。
既然p一定存在,那么所谓的CAP理论重点就在于C和A二选一,要么一致性,要么可用性。
举个例子来体现他们的区别:比如银行转账,储户有两张卡,一张余额3000的建行卡假设服务器在上海,一张余额1000的广发卡假设服务器在广州,假设用户在建行客户端往广发卡转1000块钱。在点击确认转账后,如果要保证服务可用性,那么建行卡余额会直接减掉1000块,然后通知广发接口,那边卡的余额要新增1000。那在通知广发接口的时候网络发生了延迟,比如3s,那么在3秒内会出现建行客户端显示的余额是2000,广发客户端显示的余额还是1000,这3秒内这1000块钱丢失了。这样显然不行,不能让用户看到1000块钱不翼而飞的情况。那么就要保证数据的一致性,也就是说,在用户点击确认转账后,建行客户端必须先停止掉服务并等待广发接口执行完新增才能将服务重新恢复可用。
3、一致性和可用性该怎么选呢?以下有几种方案。
- 强一致性/2pc(two-pass commit protocol)/3pc(three-pass commit protocol)
牺牲可用性,只采用一致性。如下图:第一阶段,事务管理器用于事务的管理,先不管。事务管理器会先去问db1,如果返回就绪再去问db2,也返回数据,这个时候会将相关资源锁住。不让其他进程用呢,然后执行第二阶段的提交,直到两个事物提交成功了,再释放相关数据资源。
正常情况下都成功那就很好了,第一阶段因为没提交事物,那么不涉及数据改动,如果中间发生错误,那么直接反回了。如果第二阶段提交事物一到数据库1的时候失败了,由于没有对数据库2提交事务,那么db2的数据没有更改,也可以直接返回。那么如果数据库1的事务提交失败了,数据库2的事务提交出错了,这个时候就应该要给事务1发一条回滚的消息通知事务1执行回滚,保证数据一致性。这个时候有可能事务1在执行回滚的时候也失败了,这个时候就没办法啊了,就只能人工介入了。人工怎么知道哪个地方出错了呢,这个时候就要想到日志了,也就是说每一个事务操作都需要有日志记录,而且发生错误的时候系统要发出警报通知管理员,管理员再手动恢复数据。
强一致性在一般的场景下,可以以用用,但是在真正的分布式微服务环境下,会出现大问题。因为在分布式微服务下,单个节点的可用性会影响到其他节点,导致别的节点挂掉,进而引发雪崩效应。所以在分布式微服务中,可用性比一致性相对要重要一些。那么就要放弃一致性么?也不是,编程理念并不是非黑即白的东西,既然有强一致性,那么是不是有弱一致性?
弱一致性呢,指的是在保证可用性的情况下,尽量短时间的将数据恢复一致性。
- 弱一致性:TCC(Try-Confirm-Cancel)
如下图:将每个服务分成try,confirm,cancel的方法,try执行的是保存临时流水,confirm将流水确认到正式流水, cancel是根据临时流水撤销正式流水。比如库存和订单之间的关系,当用户下订单后,客户端先启动事务,然后去调用一个服务1(库存服务)的try方法预扣库存,这样会产生一个临时库存扣除流水(状态为预扣),为保证可用性,那么用户可以实时看到这个库存减少了,然后再调用服务2(订单服务)的try方法预新增订单,这样也会产生一个临时订单创建流水(状态为预新增)。当两个try执行完成,客户端都能实时显示订单和库存的数据。这时候客户端需要调用事务管理器进行提交事务,即将临时流水转成正式流水并改变临时流水状态为提交状态。如果中间出现问题,那么事务管理器会自动调用cancel回滚操作,即将临时流水作废,将扣除的库存还原,将订单置为无效。
弱一致性感觉挺好的,但是缺点也明显,就是成本比较高,如果节点太多了,服务器的响应也会变慢,回滚困难等问题。高并发情况下也是困难重重。客户端卡顿会造成不好的用户体验。又有另一种方案别提出来,就是先只管可用性,只需要数据能够最终能达到一致性就可以了。
- 最终一致性:优先保证可用性,执行过程中忽略一致性,最终数据保持一致性。
如下图:客户端创建了一个订单,然后去操作了订单DB。其他的操作,比如支付过期处理,库存等等都扔进队列去执行,一个操作直到队列操作完毕,数据最终一致。
那么如何保证每一个操作的原子性呢?每一个操作过程都有可能出现失败的情况,一旦中间有操作失败,那数据就无法达到最终一致性了。
- 保证消息队列的数据不丢:做好消息队列的数据备份
- 处理业务表的同时通过本地事务将数据写入publish任务表。
- cap定时任务:1、读取publish任务表。2、将任务写入消息队列。3、删除publish任务(这里会存在2成功,3失败导致cap写多条相同任务到消息队列,重复执行。解决方案是给任务订个唯一标志,只要是你这个标志,不管向消息队列传递了多少次,消息队列只执行一条。其他任务放弃并写入日志)。这是幂等性处理。