分布式事务,分布式锁,分布式系统幂等处理解决方案

分布式锁

什么是锁?对所有线程可见,原子性改变锁状态

synchronize的锁是对象,靠这个锁对象锁住代码块,只要拿到对象的线程执行锁住的代码块时,这个对象对所有线程可见,别的线程就不能执行该代码块,lock的锁是一个volatile修饰的int型变量,对所有线程可见,和原子性修改。

除了利用内存做锁,其他任何互斥的都能做锁(只考虑互斥情况),如流水表中的流水号与时间结合做幂等校验可以看做是一个不会释放的锁,或者使用某个文件是否存在作为锁。只要满足对标记进行修改能保证原子性和内存可见性即可。

锁需要满足的条件

锁在的空间需要被全部进程访问(单机的堆内存被所有线程共享)
唯一的标识,不同的共享资源,需要不同的锁
至少两种状态。让线程知道,是可以直接访问,还是要阻塞等待。如state=1/0

考虑情况:

什么是分布式? 单机:多线程访问共享变量,锁可以是内存标记量,因为线程共享堆内存;分布式:,多进程访问共享变量,需要将标记存储在一个所有进程都看到的地方

分布式CAP理论告诉我们,任何一个分布式系统都无法同时满足一致性(Consistency),可用性(Avaliability)和分区容错性(Partion tolerance),最多只能同时满足两项。

分布式部署数据一致性问题很重要,在绝大场景都需要牺牲强一致性来换取高可用性,系统只需要保证最终一致性问题。

分布式场景
此处主要指集群模式下,多个相同的服务器同时开启。

在许多场景中,为了保证数据的最终一致性,需要很多技术来支撑,比如分布式事务,分布式锁等。很多时候要保证一个方法在一个时间段只能被同一个线程执行,单机环境下,java提供的并发API可以解决。但是在分布式环境下,就没有那么简单了

分布式与单机情况下,最大的不同在于其不是多线程而是多进程。

多线程由于可以共享堆内存,因此可以简单的采取内存作为标记存储位置。而进程之间甚至可能都不在同一台物理机上,因此需要将标记存储在一个所有进程都能看到的地方。

什么是分布式锁?所有进程可见,控制同时对共享变量修改的进程数

当在分布式模型下,数据只要一份(共享变量),此时需要一种锁控制某一时刻修改数据的进程数。

不仅需要保持进程可见,还需要考虑进程与锁之间的网络问题(分布式情况下之所以变得复杂,主要就是需要考虑网络的延时和不可靠)

分布式锁也可以标记在公共内存中,所有进程可见,如redis,Memchache。至于利用数据库,文件做锁与单机的实现是一样的,只要能保证标记互斥就行。

我们需要怎样的分布式锁

同一方法在同一时间只能被一台机器山的一个线程执行

这把锁要是一把可重入锁(避免死锁)
最好试一把阻塞锁(根据业务需求考虑要不要这条,如读多写少)
最好是一把公平锁(根据业务需求考虑要不要这条)
有高可用的获取锁和释放锁功能
获取锁和释放锁的性能要好

分布式锁的几种方案

基于数据库做分布式锁 锁对象是数据库中的行记录,插入成功,获取锁,删除行释放锁

创建一张锁表,因为数据库对所有进程可见,所有进程都可以操作该表改变锁的状态。
当我们锁住某个方法或资源时,我们在表中增加一条记录,释放锁的时候,删除该记录。其他进程看到锁表这行记录为空,就说明没有进程在执行方法,反之则有线程在执行方法。

具体操作:在数据库中创建一张表,方法名为索引,线程想要执行该方法时,就使用方法名向表中插入数据。成功插入则获取锁,执行完后删除行释放锁。

几个问题:

这张表强依赖于数据库的可用性,数据库是一个单点,一旦挂掉,就没有锁了。线程无法判断该方法是否有别的线程在执行

解决:搞两个数据库,数据之前双向同步,一旦挂掉切换到备份。

这把锁没有失效时间,一旦删除行失败,其他线程就没办法获得锁

解决:做个定时任务,每个一段时间把数据库中的超时数据清理一遍。

这把锁是非阻塞的,因为一旦插入失败,直接报错。没有获得锁的线程不会进入排队队列,想要再次获得锁,再次触发获得锁操作。理想:获得锁失败阻塞,等待被唤醒,现在获得锁失败,再次触发获得锁操作。(?)

解决:while循环直到insert成功。

非重入:同一个线程在没有释放锁之前无法再次获得锁‘
解决:在数据库表中加个字段,记录线程信息及其机器信息。下次再次获取锁的时候,查询数据库,如果信息匹配,则直接获取锁。

优点:借助数据库,方案简单

缺点:实现过程中遇到各种问题,为解决这些问题,实现方式会越来越复杂。依赖数据库需要开销,数据库优化有可能造成表锁。

基于Redis实现分布式锁 锁对象是缓存中存储的key-value元素,set成功获取锁(失效时间),del或到了过期时间释放锁

Redis2.6.12版本之前,使用setnx命令设置key-value,使用expire命令设置key的过期时间,获取分布式锁,使用del删除分布式锁:

问题:设置key-value后,还没来得及设置过期时间,线程就挂掉了,那么这个key-value一直在缓存中,别的线程无法获取锁。
解决:将两个操作合成一个原子操作。(SET(keyName, lockvalue, ‘NX’, ‘EX’, expireSecond))

set成功获取锁,否则失败。设置value要有唯一性,来确保不会被误删,value=系统时间戳+UUID

客户端通过del释放锁或者锁到了失效时间,自动释放。

只有当key的value和传入的value相同时,才会被删除。

优点:高性能,借助redis方便。
缺点:失效时间不好确定,设置少了,提前失效,还是会产生并发问题,设置长了,会让其他线程白白等待。

基于Zookeeper实现分布式锁

Zookper是一个为分布式应用提供一致性服务的开源组件。内部是一个分层文件系统目录树结构,每个目录下只能有唯一个文件名。

临时节点:由客户端创建,客户端与ZK连接断开,临时节点被删除,可以防止创建了锁但突然故障造成锁没有被删除导致其他线程无法获得锁而造成的死锁。

基于Zookeeper实现分布式步骤:
创建一个目录mylock

线程A想要获取锁就在mylock目录下创建临时顺序节点;
创建完毕后,获取mylock目录下的所有子节点,看自己是不是最小的那个,是则获取锁。不是则监视比自己小的那个最大节点,进入等待,直到监听的节点状态变更时,再获取子节点,判断是否获取锁。
释放锁就直接删除自己创建的那个子节点。

优点:高可用,可重入,阻塞锁特性,可以解决失效死锁问题。

解决锁无法释放的问题:客户端为获取锁在ZK中创建的是临时节点,一旦客户端获取锁之后突然挂掉(和ZK的session连接断开),那么临时节点就会自动删除。其他客户端可以再次获得。

阻塞锁特性:别的线程因为自己是最小的节点获取到锁了,那么其他线程创建的不是最小节点,会阻塞,此时他会在比他小的节点中绑定一个监听器,一旦比他小的节点释放锁了,这个监听到了,就会再次判断自己是不是最小的节点来获取锁。

可重入:客户端在创建节点的时候,把当前客户端的主机信息和线程信息写到节点中,下次想要获取锁的时候,就判断最小的节点中的数据对比一下就好了,如果一样直接获取锁,不一样则在创建一个新的临时顺序节点参与排序。

单点问题:ZK是集群部署的,只要集群中有半数以上的机器存活,就可以对外提供服务了。·

用分布式锁来防止库存超卖。


分布式事务 一个事务中的模块在不同的系统中,要保证这些处在同一个事务中的模块要么全部执行,要么全都不执行。由于模块分布在不同系统中,要想这些系统直到对方是否执行成功,就使用MQ消息队列作为中间商,告诉另一个系统对方系统是否执行成功。如:A系统执行成功,发送确认消息成功到MQ,B收到确定消息,会不断重试保证自己一定执行成功,如果不成功,则人工回滚或人工干完B的操作。A系统执行不成功,MQ是不会发送消息给B系统的。

工程领域主要讨论的是强一致性和最终一致性的解决方案。典型方案包括:
1.两阶段提交,(2PC, Two-Phase_Commit)方案。
2.eBay事件队列方案
3.TCC补偿模式
4.缓存数据最终一致性。

一, 致性理论:

分布式事务目的是保证分库数据一致性,垮裤事务会遇到各种不可控制的问题,如单个节点永久性宕机。

两阶段协议是实现分布式较经典的方案,但2PC的可扩展性很差,分布式架构下应用较大。
eBay架构师提出BASE理论,解决大规模分布式系统下的数据一致性问题。BASE理论:可以放弃系统在每个时刻的强一致性来换取系统的可扩展性

1.CAP理论
Consistency:一致性,分布式环境下多个节点的数据是否强一致
Avalability: 可用性, 分布式服务能一直保证可用状态,在有限时间内返回结果。
Partition Tolerance:分区容忍性,对网络分区的容忍性,分布式是不可或缺的。

举例:Cassandra、Dynamo 等,默认优先选择AP,弱化C;HBase、MongoDB 等,默认优先选择CP,弱化A。

2.BASE理论
核心思想:
基本可用(BasicallyAvailable): 指分布式系统在出现故障时,允许部分损失的可用性来保证核心可用。
软状态(Soft State):指允许分布式系统存在中间状态,该中间状态不会影响到系统的整体可用性。
最终一致性(Eventual Consistency):指分布式系统中的所有副本数据经过一段时间后,最终能够达到一致的状态。

二,一致性模型

数据一致性模型可分为3类:
1.强一致性: 数据更新成功后,任意时刻所有副本中的数据都是一致的,一般采用同步的方式实现。
2.弱一致性:数据更新成功后,系统不承诺立即可以读到最新写入的值,也不承认诺具体多久之后可以读到。
3.最终一致性:弱一致性的一种形式,数据更新成功后,系统不承诺若立即返回最新写入的值,但是保证最终会返回上一次更新操作的值。

分布式解决方案

1.2PC方案(XA两阶段提交方案)——强一致性
2.eBay事件队列解决方案——最终一致性
3.TCC(Try-Confirm_cancle) 补偿式最终一致性
4.缓存数据最终一致性(可靠消息最终一致性方案)
5.最大努力通知方案

1.2PC方案(XA两阶段提交方案)
两阶段提交,有一个事务管理器的概念,负责协调多个数据库(资源管理器)的事务,事务管理器先问问各个数据库你准备好了吗?如果每个数据库都恢回复OK,那就正式提交事务,在各个数据库上执行操作;如果其中任何一个数据库回答不OK,那么就回滚事务。

适合单块应用里跨多个哭的分布式事务,严重依赖于数据库层面搞定复杂问题,效率很低,绝不适合高并发场景。很少用。这个方案系统内部出现跨多个库是不合规的。微服务,一个大的系统分成几十个甚至几百个服务,每个服务只能操作自己对应的一个数据库。

如果你要操作别人的服务的库,你必须是通过调用别的服务的接口来实现,绝对不允许直接交叉访问别人的数据库。
在这里插入图片描述
TCC方案 Try、Confirm、Cancel

Try:先把两个银行账户中的资金给它冻结住就不让操作了;
Confirm:执行实际的转账操作,A 银行账户的资金扣减,B 银行账户的资金增加;
Cancel:任何一个服务的业务方法执行出错,回滚。

几乎很少人使用,事务回滚实际上是严重依赖于你自己写代码来回滚和补偿了,会造成补偿代码巨大。

跟钱相关的,跟钱打交道的,支付、交易相关的场景,我们会用 TCC,严格保证分布式事务要么全部成功,要么全部自动回滚。
在这里插入图片描述
本地消息表
A系统操作本地的一个事务时,插入一条数据到消息表,并将消息放入到MQ中。
B系统接收到消息后,在一个事务里,往自己本地消息表插入一条数据,同时其他业务,如果这个消息已经被处理过了,那么此时事务会回滚,保证不会处理消息。
B系统执行成功后,跟新自己本地消息和A的消息表的状态。
如果B失败了,就 不会更新消息表状态,A会定时扫描自己的消息表,如果有未处理,在添加到MQ中。保证了最终一致性,严重依赖于数据库的消息表来管理实务。如果是高并发场景,扩展咋办,很少使用。
在这里插入图片描述
可靠性消息最终一致性方案:
干脆不用本地消息表,直接基于MQ实现事务。

A系统处理处理事务之前,发送一个prepared消息到MQ,如果这个prepared消息发送失败,直接取消事务操作。如果消息发送成功,A再执行事务,执行事务失败,告诉MQ回滚消息,如果成功告诉MQ确认消息。A发送确认消息如果失败了呢,消息停留在prepared状态?MQ会自动轮询所有prepared消息,回调系统A的接口,这个消息是不是本地事务处理失败了,所以没发送确定消息,是继续重试还是回滚。这就避免了本地事务成功了,而没发送确认消息。

如果发送了确认消息,那么此时 B 系统会接收到确认消息,然后执行本地的事务;

这个方案里,要是系统 B 的事务失败了咋办?重试咯,MQ自动不断重试直到成功,如果实在是不行,要么就是针对重要的资金类业务进行回滚,比如 B 系统本地回滚后,想办法通知系统 A 也回滚;或者是发送报警由人工来手工回滚和补偿。
在这里插入图片描述
适用场景: 这个方案的使用还是比较广,目前国内互联网公司大都是基于这种思路玩儿的。

最大努力通知方案
在这里插入图片描述
1.系统 A 本地事务执行完之后,发送个消息到 MQ。
2.这里会有个专门消费 MQ 的最大努力通知服务,这个服务会消费 MQ,然后写入数据库中记录下来,或者是放入个内存队列。
3.接着调用系统 B 的接口。假如系统 B 执行成功就万事 ok 了,但是如果系统 B 执行失败了呢? 那么此时最大努力通知服务就定时尝试重新调用系统 B,反复 N 次,最后还是不行就放弃。

这套方案和上面的可靠消息最终一致性方案的区别:
可靠消息最终一致性方案可以保证的是只要系统 A 的事务完成,通过不停(无限次)重试来保证系统 B 的事务总会完成。

但是最大努力方案就不同,如果系统 B 本地事务执行失败了,那么它会重试 N 次后就不再重试,系统 B 的本地事务可能就不会完成了。

至于你想控制它究竟有“多努力”,这个需要结合自己的业务来配置。

比如对于电商系统,在下完订单后发短信通知用户下单成功的业务场景中,下单正常完成,但是到了发短信的这个环节由于短信服务暂时有点问题,导致重试了 3 次还是失败。

那么此时就不再尝试发送短信,因为在这个场景中我们认为 3 次就已经算是尽了“最大努力”了。

简单总结:就是在指定的重试次数内。

适用场景: 一般用在不太重要的业务操作中,就是那种完成的话是锦上添花,但失败的话对我也没有什么坏影响的场景。

什么事务
一系列操作要么成功,要么失败,蛮子ACID(原子性,一致性,隔离性,持久性)。记住转账操作,保证在任何情况下都可以安全正确的执行。多个操作只执行了一部分。

ACID:

java中的事务
service层的增删改方法上添加一个@Transactional注解。Spring会给组件生成一个Proxy代理,当执行add()时,会打开事务,执行完毕后,提交事务。

什么是分布式事务
先看单机事务: 事务中的模块在一个JVM中
员工模块、财务模块和请假模块全部在一个单机系统中,有个操作要按顺序调用3个模块的接口。这个操作包含在一个事务中,是一个整体,要么同时成功要么同时失败回滚。
分布式事务: 事务中的模块在多个JVM中
员工模块、财务模块和请假模块分别给拆分成员工系统、财务系统和请假系统。3个模块在3个系统中。他们之间调用会存在 http 或者 rpc通信 ,会分地落到数据库中,它们的操作都需要分别落地到数据库中。

这 3 个系统的一系列操作需要全部被包裹在同一个分布式事务中。

单块系统是运行在同一个 JVM 进程中的,但是分布式系统中的各个系统运行在各自的 JVM 进程中。因此你直接加@Transactional 注解是不行的,因为它只能控制同一个 JVM 进程中的事务。

为什么会有事务这个概念
事务是为了解决什么问题


*分布式系统幂等处理解决方案 * 保证重复操作和一次操作结果是一样的

点一次和点多次是一样的,避免重复下单。

接收消息的时候,如果不能保证一次推送和多次推送一样,那么消息重复推送。

所谓幂等,简单地说,就是对接口的多次调用所产生的结果和调用一次是一致的。对外发布的HTTP接口或者Thrift接口,也可以是接收消息的内部接口,甚至是一个内部方法或操作。

在App中下订单的时候,点击确认之后,没反应,就又点击了几次。在这种情况下,如果无法保证该接口的幂等性,那么将会出现重复下单问题。

在接收消息的时候,消息推送重复。如果处理消息的接口无法保证幂等,那么重复消费消息产生的影响可能会非常大。

GTIS提供了一套可靠的解决方法:依赖于存储引擎,通过对不同操作所对应的唯一的内容特性生成一个唯一的全局ID来防止操作重复。

在分布式环境中,网络环境更加复杂,因前端操作抖动、网络故障、消息重复、响应速度慢等原因,对接口的重复调用概率会比集中式环境下更大,尤其是重复消息在分布式环境中很难避免。

https://www.zhihu.com/search?type=content&q=%E5%88%86%E5%B8%83%E5%BC%8F%E7%B3%BB%E7%BB%9F%E5%B9%82%E7%AD%89%E5%A4%84%E7%90%86%E8%A7%A3%E5%86%B3%E6%96%B9%E6%A1%88

作者:欧阳丰
链接:https://zhuanlan.zhihu.com/p/79833740
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值