http://xuyangyang.club/articles/2018/09/03/1535981938478.html

作者:三分之一程序员

一、分布式锁

  分布式锁是在分布式场景下一种常见技术,通常通过基于redis和zookeeper来实现,本文主要介绍redis分布式锁和zookeeper分布式锁的实现方案和对比:

(1)基于redis的普通实现

  这个方案的加锁主要实现是基于redis的”SET key 随机值 NX PX 过期时间(毫秒)”指令,NX代表只有key不存在时才设置成功,PX代表在过期时间后会自动释放。

  这个方案的释放锁是通过lua脚本删除key的方式,判断value一样则删除key。

  使用随机值的原因是如果某个获取到锁的客户端阻塞了很长时间,导致了它获取到的锁已经自动释放,此时可能有其他客户端已经获取到了锁,如果直接删除是有问题的,所以要通过随机值加上lua脚本去判断如果value相等时再删除。

  这个方案存在一个问题就是,如果采用redis单实例可能会存在单点故障问题,但如果采用普通主从方式,如果主节点挂了key还没来得及同步到从节点,此时从节点被切换到了主节点,由于没有同步到数据别人就会拿到锁。

(2)redis的RedLock算法

  这个方案是redis官方推荐的分布式锁的解决方案,假设有5个redis master实例,然后执行如下步骤去获取一把锁:

  1)获取当前时间戳,单位是毫秒
  2)跟上面类似,轮流尝试在每个master节点上创建锁,过期时间较短,一般就几十毫秒
  3)尝试在大多数节点上建立一个锁,比如5个节点就要求是3个节点(n / 2 +1)
  4)客户端计算建立好锁的时间,如果建立锁的时间小于超时时间,就算建立成功了
  5)要是锁建立失败了,那么就依次删除这个锁
  6)只要别人建立了一把分布式锁,你就得不断轮询去尝试获取锁
  这个方案实现起来较为麻烦,不过貌似有很多开源的插件封装了这个算法,但是这个算法好像在国外是有争议的。

(3) zookeeper的临时节点

  这个方案的实现是通过在获取锁时在zookeeper上创建临时节点(常用为临时顺序节点),如果创建成功(如果是临时顺序节点则为当前最小),就代表获取了到了这个锁;

如果创建创建失败(如果是临时顺序节点,当前自己节点不是最小),则注册监听器监听这个锁。释放锁时会删除这个临时节点,测试由于注册了监听器,其他等待锁的线程则会收到释放锁的消息,然后再去尝试获取锁。

(4)redis分布式锁和zookeeper分布式锁的对比

  通过对比redis分布式锁和zk分布式锁可以发现,redis分布式锁类似于自旋锁的方式需要自己不断尝试去获取锁,这个是比较耗性能的。zk获取不到锁的话则可以注册监听器,不需要不断尝试,这样的活性能开销较小;

其次,redis锁有一个问题就是,如果获取到锁的客户端崩溃了或者没有正常释放锁则会导致只能等到过期时间完了才能获取到锁,而zk建立的由于是临时节点,客户端崩溃了或者挂了,临时节点会自动删除,此时会自动释放锁;

最后,这个redis的实现方式如果采用RedLock算法的话较为复杂并且还存在争议,普通的算法存在单点故障和主从同步的问题,所以一般来说,个人认为zk分布式锁要比redis分布式锁更可靠并且易用。

二、分布式session

  在传统的单体应用下,我们可以通过session去存储一些数据,但是在分布式和为微服务结构下,如果需要使用session就需要采取一些手段去维护,以下提供2个方案去实现分布式session:

(1)tomcat+redis

  由于我们传统的应用基本都是通过tomcat去部署的,我们可以利用tomcat的RedisSessionManager来将session的数据都存在redis中。

  但这种方案现在不怎么使用了,因为移植性很差,如果要换web容器就尴尬了。

(2)spring session + redis

  给sping session配置基于redis来存储session数据,然后配置一个spring session的过滤器,这样的话,session相关操作都会交给spring session来管了。接着在代码中,就用原生的session操作,就是直接基于spring sesion从redis中获取数据了。

  这个方案现在还是比较主流的,因为现在主流的开发基本都是基于spring开源的一些框架,所以说如果换技术栈的影响不会很大。

三、分布式事务

  现在分布式系统成为了主流,但使用分布式也随之带来了一些问题和痛点,分布式事务就是最常见的一个问题,本文主要介绍分布式事务的一些常见解决方案。

(1)两阶段提交方案/XA方案

  两阶段提交,有一个事务管理器的概念,负责协调多个数据库(资源管理器)的事务,事务管理器先问问各个数据库你准备好了吗?如果每个数据库都回复ok,那么就正式提交事务,在各个数据库上执行操作;如果任何一个数据库回答不ok,那么就回滚事务。
  这种分布式事务方案,比较适合单块应用里,跨多个库的分布式事务,而且因为严重依赖于数据库层面来搞定复杂的事务,效率很低,绝对不适合高并发的场景。如果要玩儿,那么基于spring + JTA就可以搞定。
  这个方案很少用,一般来说某个系统内部如果出现跨多个库的这么一个操作,是不合规的。我可以给大家介绍一下, 现在微服务,一个大的系统分成几百个服务,几十个服务。一般来说,我们的规定和规范,是要求说每个服务只能操作自己对应的一个数据库。如果你要操作别的服务对应的库,不允许直连别的服务的库,违反微服务架构的规范,如果你要操作别人的服务的库,你必须是通过调用别的服务的接口来实现,绝对不允许你交叉访问别人的数据库!

(2)TCC方案

  TCC的全程是:Try、Confirm、Cancel。

  这个其实是用到了补偿的概念,分为了三个阶段:
  1)Try阶段:这个阶段说的是对各个服务的资源做检测以及对资源进行锁定或者预留
  2)Confirm阶段:这个阶段说的是在各个服务中执行实际的操作
  3)Cancel阶段:如果任何一个服务的业务方法执行出错,那么这里就需要进行补偿,就是执行已经执行成功的业务逻辑的回滚操作。

  这种方案说实话几乎很少用人使用,因为这个事务回滚实际上是严重依赖于你自己写代码来回滚和补偿了,会造成补偿代码巨大,非常之恶心。

  比较适合的场景:这个就是除非你是真的一致性要求太高,是你系统中核心之核心的场景,比如常见的就是资金类的场景,那你可以用TCC方案了,自己编写大量的业务逻辑,自己判断一个事务中的各个环节是否ok,不ok就执行补偿/回滚代码。

(3)本地消息表

  本地消息表示国外的ebay搞出来的这么一套思想
  本地消息表来实现分布式事务的思路大致如下:
  1)A系统在自己本地一个事务里操作同时,插入一条数据到消息表
  2)接着A系统将这个消息发送到MQ中去
  3)B系统接收到消息之后,在一个事务里,往自己本地消息表里插入一条数据,同时执行其他的业务操作,如果这个消息已经被处理过了,那么此时这个事务会回滚,这样保证不会重复处理消息
  4)B系统执行成功之后,就会更新自己本地消息表的状态以及A系统消息表的状态
  5)如果B系统处理失败了,那么就不会更新消息表状态,那么此时A系统会定时扫描自己的消息表,如果有没处理的消息,会再次发送到MQ中去,让B再次处理
  6)这个方案保证了最终一致性,哪怕B事务失败了,但是A会不断重发消息,直到B那边成功为止。
  这个方案最大的弊端在于依赖于数据库消息表来保证事务,但是在高并发场景下,数据库就成了瓶颈。
  (4)可靠消息最终一致性方案

  这个方案的大致思路为:
  1)A系统先发送一个prepared消息到mq,如果这个prepared消息发送失败那么就直接取消操作别执行了
  2)如果这个消息发送成功过了,那么接着执行本地事务,如果成功就告诉mq发送确认消息,如果失败就告诉mq回滚消息
  3)如果发送了确认消息,那么此时B系统会接收到确认消息,然后执行本地的事务
  4)mq会自动定时轮询所有prepared消息回调你的接口,问你,这个消息是不是本地事务处理失败了,所有没发送确认消息?那是继续重试还是回滚?一般来说这里你就可以查下数据库看之前本地事务是否执行,如果回滚了,那么这里也回滚吧。这个就是避免可能本地事务执行成功了,别确认消息发送失败了。
  5)这个方案里,要是系统B的事务失败了咋办?重试咯,自动不断重试直到成功,如果实在是不行,要么就是针对重要的资金类业务进行回滚,比如B系统本地回滚后,想办法通知系统A也回滚;或者是发送报警由人工来手工回滚和补偿。

  这个方案是目前国内公司采用较多的一种方案。

(5)最大努力通知方案

  思路:
  1)系统A本地事务执行完之后,发送个消息到MQ
  2)这里会有个专门消费MQ的最大努力通知服务,这个服务会消费MQ然后写入数据库中记录下来,或者是放入个内存队列也可以,接着调用系统B的接口
  3)要是系统B执行成功就ok了;要是系统B执行失败了,那么最大努力通知服务就定时尝试重新调用系统B,反复N次,最后还是不行就放弃。

(6)究竟如何来使用分布式事务?

  咨询过一些互联网公司的大佬,在他们的业务场景下起码几百个服务,复杂的分布式大型系统,里面其实也没几个分布式事务。

  其实用任何一个分布式事务的这么一个方案,都会导致你那块儿代码会复杂10倍。很多情况下,系统A调用系统B、系统C、系统D,我们可能根本就不做分布式事务。如果调用报错会打印异常日志或者通过返回值判断是否需要回滚。每个月也就那么几个bug,很多bug是功能性的,体验性的,真的是涉及到数据层面的一些bug,一个月就几个,两三个?如果你为了确保系统自动保证数据100%不能错,上了几十个分布式事务,代码太复杂;性能太差,系统吞吐量、性能大幅度下跌。

  99%的分布式接口调用,一些大佬给的建议不要做分布式事务,直接就是监控(发邮件、发短信)、记录日志(一旦出错,完整的日志)、事后快速的定位、排查和出解决方案、修复数据。这样比你做50个分布式事务,成本要来的低上百倍,低几十倍。

  所以在要用分布式事务的时候要权衡,分布式事务一定是有成本,代码会很复杂,开发很长时间,性能和吞吐量下跌,系统更加复杂更加脆弱反而更加容易出bug;但是好处就是如果做好了,TCC、可靠消息最终一致性方案,一定可以100%保证你那快数据不会出错。