分布式锁

为什么要使用分布式锁

使用分布式锁的目的,无外乎是为了保证共享资源在同一时间只被一个线程操作。

单机时代,通常我们会使用java并发相关API(Lock或synchronized)进行互斥实现。分布式情况下较为复杂,线程分配在不同的进程或机器中,这时候我们需要一种跨JVM的互斥机制来控制共享资源的访问,分布式锁应运而生。

需要具备哪些条件

  1. 在分布式系统环境下,一个方法在同一时间只能被一个线程执行;

  2. 高可用、高性能的获取锁和释放锁;

  3. 具备可重入性;

  4. 失效机制,防止死锁;

  5. 具备阻塞锁和非阻塞锁特性(根据业务需要考虑需要);

常见的分布式锁实现方案

  1. 基于数据库实现方案;

  2. 基于分布式缓存实现的锁服务:典型代表是使用Redis实现锁服务和基于Redis实现的RedLock方案;

  3. 基于分布式一致性算法实现锁服务:典型代表zookeeper和google的Chubby; 

具体实现

基于数据库

先来了解核心实现:

  1. 创建一张锁表,以其中一个字段为唯一索引

  2. 使用方法名向表中插入数据,插入成功则获取锁成功

  3. 方法结束后删除数据,释放锁

以上三步完成锁的基本要求,实现简单,但工作中为什么我们不使用这种方式呢?

有以下几个问题:

  • 数据库的可用性和性能影响分布式锁的可用性及性能

  • 锁没有失效时间,可能会导致死锁

  • 只能非阻塞,成功失败立刻返回

  • 非重入,同一个线程在没有释放锁之前无法再次获得该锁

当然我们也可以在程序上处理这几个问题,但考虑到性能与复杂度又会得不偿失。

基于Redis

先来了解锁核心实现:

  1. 加锁setnx(key,value)

  2. 锁超时expire(key,time)

  3. 解锁del(key)

以上三步满足锁的基本要求,细思量某些场景又会出问题:

场景1:A想获取锁,于是A操作setnx成功,这时A突然挂了,还未来得及expire; 此时锁就无超时时间。

解决:使用SET KEY VALUE [EX seconds] [PX milliseconds] [NX|XX] 替代setnx与expire,变成原子操作;

场景2:A获取锁成功,这时B来了,B获取锁失败,走之前顺便del一下,把锁释放了,这时C来了就和A撞一起了。 

解决:使用线程ID存储value,释放锁之前判断是否是自己的锁(不限于此)

场景3:A获取锁成功,但是A干活太慢所已经超时了,这时B来了

解决:启动守护线程重设A的超时时间(考虑程序优化,为什么执行这么慢呢)

 

基于zookeeper

了解Zookeeper的节点分为4种类型,分别为持久节点、持久顺序节点、时节点、临时顺序节点;这里我们重点关注 【临时顺序节点】,这是实现分布式锁的重要依据。这种节点有如下几个特性:

  1. 会话结束后节点自动删除,也可手动删除;

  2. 不能拥有子节点;

  3. 节点以10位数字序列号为后缀形式命名;(zk_lock_0000000001)

来看一张图:

先来分析排它锁(Exclusive_lock),核心过程如下:

  1. A、B、C竞争锁资源,创建临时节点lock,A创建成功;

  2. B、C设置watch lock节点;

  3. A执行操作完成,删除lock节点释放锁,或A挂了,lock节点自动删除;

  4. B、C收到zk的通知;

  5. B和C竞争创建临时节点lock,同上面4步,直至获取锁成功

排它锁实现简单易于理解,不再多说;下面来分析共享锁(Share_lock);

  1. A、B、C分别创建自己的顺序临时节点分别为R1、W2、W3

  2. 获取zk的 /Share_lock下的所有节点;

  3. 判断自己的节点是否是Share_lock下的最小节点,A最小获取锁成功,B、C置自身上一个节点的watcher;即B watch R1 , C watch W2;

  4. A操作完成,删除R1节点;

  5. zk通知B获取锁;

简述优缺点

从三个方面考虑:实现复杂度、性能、可靠性由高到低对三种实现方式比较

实现复杂度:zookeeper > 缓存 > 数据库

可靠性:zookeeper > 缓存 > 数据库

性能:缓存 > zookeeper > 数据库

原文地址:https://www.cnblogs.com/sjp007/p/10238718.html