架构深度学习

3.10:分布式锁

2020-05-17  本文已影响0人  今年花开正美

本文将梳理微服务架构下,分布式锁的常用方案。整体包含以下三部分:

分布式锁的提出

Java锁概念

一说到锁,我们可能一下会有很多锁的知识往外涌。以Java为例,有JVM内置的锁机制synchroized,也有基于队列同步器实现的Lock(可重入锁和读写锁),甚至我们可以基于队列同步器自己定制一把锁。

那锁到底是什么呢,本质功能是什么呢,个人认为,锁的作用是控制在多线程场景下对同一资源的访问(读写)顺序。锁是每个线程的访问凭证,只有持有锁的线程才可以对共享资源进行访问。

再简单点来说,锁就是将多线程并发访问共享资源串行化

要实现串行化,个人觉得需要先了解锁的三个基本要素:

进程内的锁大部分都是通过上述三个基本要素来实现锁功能的,关于Java锁更详细的内容将在后续文章中单独深入了解。

分布式锁

分布式锁提出的根本原因就是,分布式架构下应用单元集群部署了。用户的请求分散到不同的进程中,这时Java锁这种进程内的锁机制就无法满足并发资源的串行化了。

了解上述原因后,我们参考Java锁的知识可以知道,要实现分布式锁主要技术点有

  1. 锁集中存储,分布式锁要可以被所有部署在不同单元的线程访问。
  2. 锁要有状态,参与竞争的线程可以通过锁的状态来确定是申请到了还是继续等待竞争。

根据上面的两点,我们就可以从了解到的已有系统哪些可以用来实现分布式锁,以及如何实现。

分布式锁主流方案

分布式锁方案

目前经常用来实现分布式锁的服务有MySQL、Redis、Zookeeper、Etcd等。考虑Mysql一般都是系统的性能瓶颈点,基本上很少有公司会使用了。

分布式锁功能

我们首先从分布式锁设计上来分析,一个分布式锁要包含哪些基本功能。

分布式锁只有支持上述最基本的三个功能,才能算是健壮的。下面我们就分别使用Redis、Etcd来实现分布式锁。

Redis分布式锁实现


在用Redis来实现分布式锁过程中,一共经历了以下几个阶段,可以理解成Redis的进化过程。

第一阶段

最原始简单实现:

  1. 申请锁:调用setnx,原理是利用Redis的setnx能在key不存在时才保存数据,设置成功则返回true,若key已经存在则返回false。这样业务调用时根据返回结果就可以知道是否申请锁成功。
  2. 设置过期时间:调用expirt,申请锁成功后再调用expirt给锁设置过期时间。
  3. 释放锁:调用del,业务执行完后,调用del来主动删除Key,也就是释放了锁。

第一阶段的锁最大的问题就是申请锁和设置过期时间不是原子的。若申请锁成功后,服务故障或网络异常导致没有设置过期时间,这样锁就永远不会释放了。

第二阶段

基于上面方案的非原子性问题,又引入了lua脚本+Redis事务来解决原子问题。

  1. 编写Redis命令:开启事务->setnx命令->expirt命令->提交事务。
  2. 将上述Redis命令放到lua脚本里面。
  3. 在业务调用时通过执行Redis的EVAL来执行lua脚本来实现申请锁+设置过期时间的原子性。

这种方案相较第一种来说有一定的改进,但是因为Redis事务是不保证一致性的,也就是说lua脚本中存在部分成功部分失败。因此这种方案其实也是有较大缺点。

第三阶段

Redis在2.6以后扩展了setnx命令,支持原来的setnx功能之外同时可以设定过期时间。因此到第三阶段,基于Redis实现分布式锁就只有以下两步:

  1. 申请锁:调用setnx,设置成功后,命令同时提供了过期时间的参数。
  2. 释放锁:调用del,业务执行完后,调用del来主动删除Key,也就是释放了锁。

上面的三个阶段的实现都只是满足锁申请和锁释放两个最基本功能。在锁申请失败时,需要业务实现不断的重试功能。因此开源框架中又出现了基于上述命令的封装组件Redisson和RedLock

Redisson

Redisson在实现时,主要增加了以下改进

  1. 申请锁失败后,申请锁的lua脚本会返回当前锁的过期时间。
  2. 申请锁失败后,一先订阅锁对于的Channel,等待锁释放后通知消息;二会根据上述收到的过期时间,在到时间后再次申请锁,只要没有申请锁成功将一直重复。
  3. 申请锁成功后,将发布一条锁释放消息,通知所有正在申请当前锁的线程。
  4. Redisson还增加了可重入性,根据锁上的线程id来判断释放当前线程持有锁。

Redisson的调用还是比较简单的,操作和Jvm的Lock类似就不再累述。

RedLock

在上述所有方案中,使用的都是单点的Redis,就算是主从或者是Cluster部署,最终锁都是只存在一个节点中,在节点故障时系统可用性得不到保证。同时主从切换时若锁未同步到从库,将导致出现多个业务持有锁,造成数据不一致性。

RedLock主要解决的就是Redis单点的问题,假设部署的Redis集群数量未N,需要在(N/2)+1上加锁成功,即半数以上加锁成功,才算申请成功。相当于RedLock实现了一套基本的分布式数据一致性算法。

RedLock解决的问题,由于RedLock保证了(N/2)+1工作即可加锁成功,因此解决单点以及在主从切换时多个客户端获取锁问题。

<font color=red>
总结一下Redis实现分布式锁的问题和适用场景。基于CAP原则来分析的话,Redis本质其实是为了满足AP。因为Redis的集群模式是主从模型,在网络分区(脑裂)情况下,Redis也能保证服务的可用性(主从切换)。但是未同步到从库的数据是会丢失的,也就是数据不一致了。</font>

因此Redis锁适用于数据一致性没有强制要求的场景,其实大部分的业务场景都是满足AP即可,基于Redis的高性能,Redis锁是大部分锁的首选。

Etcd分布式锁实现


Etcd是使用分布式协议raft作为一致性算法,目标是提供高可用的分布式键值(Key-Value)数据库。相较Redis最大的优点就是Etcd是强一致性的,同时提供简单的API功能,应用可以直接通过http协议来访问。

使用Etcd实现分布式锁和Redis是基本类似的

后续再上传使用Etcd实现分布式锁的代码。

分布式锁选择

在实际项目中,我们该怎么选择使用哪种分布式锁呢?个人总结主要从以下两个方面考虑:

  1. 场景是CP还是AP的,比如在支付类需要强一致性的业务就是CP场景,这时候我们应该选择使用Etcd或Zookeeper强一致性的存储系统来实现。若是AP类的场景,比如社交等,就可以采用Redis实现即可,比较Redis组件在大部分项目中已经使用,就不需要再引入额外的系统了。
  2. Etcd还是Zookeeper,这个就看公司选择即可,从性能上来说,Etcd会有一定的优势,单也不是量级的优势,若对Zookeeper比较熟悉了,而且技术生态已经有Zookeeper了就直接使用Zookeeper即可。
上一篇下一篇

猜你喜欢

热点阅读