Java技术债务Java技术债务

  •  首页
  •  分类
  •  归档
  •  标签
  • 博客日志
  • 资源分享
  •  友链
  •  关于本站
注册
登录

三种实现分布式锁的实现与区别

Redis,分布式

文章目录

  • 思想
  • 一、基于Zookeeper实现
  • 二、基于缓存(redis实现)
  • 三、基于数据库实现方式
  • 四、三种分布式锁优缺点

思想

分布式锁,是一种思想,它的实现方式有很多。比如,我们将沙滩当做分布式锁的组件,那么它看起来应该是这样的


  • 加锁 在沙滩上踩一脚,留下自己的脚印,就对应了加锁操作。其他进程或者线程,看到沙滩上已经有脚印,证明锁已被别人持有,则等待。
  • 解锁

把脚印从沙滩上抹去,就是解锁的过程。

  • 锁超时 为了避免死锁,我们可以设置一阵风,在单位时间后刮起,将脚印自动抹去。

分布式锁的实现有很多,比如基于数据库、memcached、Redis、系统文件、zookeeper等。它们的核心的理念跟上面的过程大致相同。具备的条件:

1、一个方法同一时间只能被一个机器一个线程执行

2、高可用的获取锁和释放锁

3、高性能的获取锁和释放锁

4、具备可重入性

5、具备锁失效机制,防止死锁

6、具备非阻塞锁特性,即没有获取锁直接放回获取锁失败。

那么如何实现分布式锁呢,有如下几种方式:

  1. 基于Zookeeper实现
  2. 基于缓存(redis实现)
  3. 基于数据库实现方式

一、基于Zookeeper实现

Zookeeper数据存储结构是一颗树,树由节点组成,节点叫ZNode

Znode分四种类型:

1、持久节点(persistent)

默认节点类型,创建节点的客户端和Zookeeper断开链接后,节点依旧存在

2、持久节点顺序节点(persistent_sequential)

创建节点时,根据创建时间给节点编号。

3、临时节点

断开链接后,节点被删除

4、临时顺序节点

Zookeeper分布式锁的原理:

获取锁:

  1. 在Zookeeper创建一个持久节点ParentLock,当客户端想要获取锁时,在ParentLock节点下创建临时顺序节点。
  2. 然后客户端再去获取临时节点是否是最靠前的一个,如果是则获取锁。
  3. 另外一个客户端先创建临时节点,然后获取临时节点是靠前,如果不是靠前的,并且不是最小的序号,此时向前面的节点注册Watcher,用于监听前一个节点的锁是否存在。

释放锁:

  1. 任务完成删除临时节点
  2. 由于节点都是相互监听,当前一个节点消失,下一个节点被置顶
  3. 如果机器宕机了,会自动删除临时节点。

缺点:

  1. 性能上不如缓存服务高,创建锁和释放锁过程上,都动态创建、销毁临时节点实现锁功能,Zk中创建和删除节点只能通过leader服务器执行,然后将数据同步到所有follower机器上。

  2. 可能会因为网络抖动导致链接中断,删除临时节点,但是ZK有多种重试策略,重试之后才会删除临时节点。

二、基于缓存(redis实现)

1、加锁

加锁实际上就是在redis中,给Key键设置一个值,为避免死锁,并给定一个过期时间。

SET lock_key random_value NX PX 5000

值得注意的是:

random_value 是客户端生成的唯一的字符串。
NX 代表只在键不存在时,才对键进行设置操作。
PX 5000 设置键的过期时间为5000毫秒。
这样,如果上面的命令执行成功,则证明客户端获取到了锁。

2、解锁

解锁的过程就是将Key键删除。但也不能乱删,不能说客户端1的请求将客户端2的锁给删除掉。这时候random_value的作用就体现出来。

为了保证解锁操作的原子性,我们用LUA脚本完成这一操作。先判断当前锁的字符串是否与传入的值相等,是的话就删除Key,解锁成功。

 if redis.call('get',KEYS[1]) == ARGV[1] then
	 return redis.call('del',KEYS[1])
 else
 	return 0
 end

3、使用

首先,我们在pom文件中,引入Redis包。在这里,笔者用的是最新版本,注意由于版本的不同,API可能有所差异。

<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

加锁的过程很简单,就是通过SET指令来设置值,成功则返回;否则就循环等待,在timeout时间内仍未获取到锁,则获取失败。

缺点:

客户端A从master获取锁,master将锁同步到slave钱,master 宕机,slave节点晋升master节点,客户端B取得了被客户端A锁定的同一个资源。安全失效。

三、基于数据库实现方式

数据库拍他锁

1、根据名字获取锁信息

2、更新锁信息(比如版本,状态等)占有锁

缺点:

1、数据库的强依赖性,数据库不可用会导致业务系统不可用。

2、锁没有失效时间,解锁失败,会使锁一直存在,其他线程无法获取锁。

3、锁只能是非阻塞,插入失败报错,线程不会进入排队队列中,会再次出发获取锁的操作。

4、锁是非重入,同一个线程没有释放锁,无法再次获得该锁,因为数据库中锁的数据已经存在。

解决方案:

1、主从复制

2、定时任务,或者添加上次更新的时间

3、死循环insert

4、数据库存入当前线程的信息。

四、三种分布式锁优缺点

分布式锁 优点 缺点
Zookeeper 1、封装好的框架,容易实现
2、有等待锁的队列,提升抢锁的概率 添加和删除节点性能低
添加和删除节点性能低
Redis set和del指令性能较高 1、实现复杂:需要考虑超时、原子性、误删等情况
2、没有等待锁的队列,需要在客户端自旋等待锁,效率低
数据库 容易理解 复杂度较高,需要设计表、策略等如果获取锁失败,需要不断的链接数据库,查库操作。

思想

分布式锁,是一种思想,它的实现方式有很多。比如,我们将沙滩当做分布式锁的组件,那么它看起来应该是这样的


  • 加锁 在沙滩上踩一脚,留下自己的脚印,就对应了加锁操作。其他进程或者线程,看到沙滩上已经有脚印,证明锁已被别人持有,则等待。
  • 解锁

把脚印从沙滩上抹去,就是解锁的过程。

  • 锁超时 为了避免死锁,我们可以设置一阵风,在单位时间后刮起,将脚印自动抹去。

分布式锁的实现有很多,比如基于数据库、memcached、Redis、系统文件、zookeeper等。它们的核心的理念跟上面的过程大致相同。具备的条件:

1、一个方法同一时间只能被一个机器一个线程执行

2、高可用的获取锁和释放锁

3、高性能的获取锁和释放锁

4、具备可重入性

5、具备锁失效机制,防止死锁

6、具备非阻塞锁特性,即没有获取锁直接放回获取锁失败。

那么如何实现分布式锁呢,有如下几种方式:

  1. 基于Zookeeper实现
  2. 基于缓存(redis实现)
  3. 基于数据库实现方式

一、基于Zookeeper实现

Zookeeper数据存储结构是一颗树,树由节点组成,节点叫ZNode

Znode分四种类型:

1、持久节点(persistent)

默认节点类型,创建节点的客户端和Zookeeper断开链接后,节点依旧存在

2、持久节点顺序节点(persistent_sequential)

创建节点时,根据创建时间给节点编号。

3、临时节点

断开链接后,节点被删除

4、临时顺序节点

Zookeeper分布式锁的原理:

获取锁:

  1. 在Zookeeper创建一个持久节点ParentLock,当客户端想要获取锁时,在ParentLock节点下创建临时顺序节点。
  2. 然后客户端再去获取临时节点是否是最靠前的一个,如果是则获取锁。
  3. 另外一个客户端先创建临时节点,然后获取临时节点是靠前,如果不是靠前的,并且不是最小的序号,此时向前面的节点注册Watcher,用于监听前一个节点的锁是否存在。

释放锁:

  1. 任务完成删除临时节点
  2. 由于节点都是相互监听,当前一个节点消失,下一个节点被置顶
  3. 如果机器宕机了,会自动删除临时节点。

缺点:

  1. 性能上不如缓存服务高,创建锁和释放锁过程上,都动态创建、销毁临时节点实现锁功能,Zk中创建和删除节点只能通过leader服务器执行,然后将数据同步到所有follower机器上。

  2. 可能会因为网络抖动导致链接中断,删除临时节点,但是ZK有多种重试策略,重试之后才会删除临时节点。

二、基于缓存(redis实现)

1、加锁

加锁实际上就是在redis中,给Key键设置一个值,为避免死锁,并给定一个过期时间。

SET lock_key random_value NX PX 5000

值得注意的是:

random_value 是客户端生成的唯一的字符串。
NX 代表只在键不存在时,才对键进行设置操作。
PX 5000 设置键的过期时间为5000毫秒。
这样,如果上面的命令执行成功,则证明客户端获取到了锁。

2、解锁

解锁的过程就是将Key键删除。但也不能乱删,不能说客户端1的请求将客户端2的锁给删除掉。这时候random_value的作用就体现出来。

为了保证解锁操作的原子性,我们用LUA脚本完成这一操作。先判断当前锁的字符串是否与传入的值相等,是的话就删除Key,解锁成功。

 if redis.call('get',KEYS[1]) == ARGV[1] then
	 return redis.call('del',KEYS[1])
 else
 	return 0
 end

3、使用

首先,我们在pom文件中,引入Redis包。在这里,笔者用的是最新版本,注意由于版本的不同,API可能有所差异。

<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

加锁的过程很简单,就是通过SET指令来设置值,成功则返回;否则就循环等待,在timeout时间内仍未获取到锁,则获取失败。

缺点:

客户端A从master获取锁,master将锁同步到slave钱,master 宕机,slave节点晋升master节点,客户端B取得了被客户端A锁定的同一个资源。安全失效。

三、基于数据库实现方式

数据库拍他锁

1、根据名字获取锁信息

2、更新锁信息(比如版本,状态等)占有锁

缺点:

1、数据库的强依赖性,数据库不可用会导致业务系统不可用。

2、锁没有失效时间,解锁失败,会使锁一直存在,其他线程无法获取锁。

3、锁只能是非阻塞,插入失败报错,线程不会进入排队队列中,会再次出发获取锁的操作。

4、锁是非重入,同一个线程没有释放锁,无法再次获得该锁,因为数据库中锁的数据已经存在。

解决方案:

1、主从复制

2、定时任务,或者添加上次更新的时间

3、死循环insert

4、数据库存入当前线程的信息。

四、三种分布式锁优缺点

分布式锁 优点 缺点
Zookeeper 1、封装好的框架,容易实现
2、有等待锁的队列,提升抢锁的概率 添加和删除节点性能低
添加和删除节点性能低
Redis set和del指令性能较高 1、实现复杂:需要考虑超时、原子性、误删等情况
2、没有等待锁的队列,需要在客户端自旋等待锁,效率低
数据库 容易理解 复杂度较高,需要设计表、策略等如果获取锁失败,需要不断的链接数据库,查库操作。
完
  • 本文作者:Java技术债务
  • 原文链接: https://cuizb.top/myblog/article/1638283998
  • 版权声明: 本博客所有文章除特别声明外,均采用 CC BY 3.0 CN协议进行许可。转载请署名作者且注明文章出处。
阅读全文
Java技术债务

Java技术债务

Java技术债务
Java技术债务
热门文章
  1. ClickHouse使用过程中的一些查询优化(六)2003
  2. MySQL数据库被攻击,被删库勒索,逼迫我使出洪荒之力进行恢复数据764
  3. MySQL主从同步原理458
  4. 线程池的理解以及使用414
  5. Spring Cloud Gateway整合nacos实战(三)409
分类
  • Java
    30篇
  • 设计模式
    27篇
  • 数据库
    20篇
  • Spring
    18篇
  • MySQL
    13篇
  • ClickHouse
    11篇
  • Kubernetes
    10篇
  • Redis
    9篇
  • Docker
    8篇
  • SpringBoot
    7篇
  • JVM
    6篇
  • Linux
    5篇
  • Spring Cloud
    5篇
  • 多线程
    5篇
  • Netty
    4篇
  • Kafka
    4篇
  • 面经
    4篇
  • Nginx
    3篇
  • JUC
    3篇
  • 随笔
    2篇
  • 分布式
    1篇
  • MyBatis
    1篇
  • 报错合集
    1篇
  • 生活记录
    1篇
  • 源码
    1篇
  • 性能优化
    1篇

最新评论

  • MySQL数据库被攻击,被删库勒索,逼迫我使出洪荒之力进行恢复数据2022-05-06
    Java技术债务:@capture 一起探讨学习,服务器被黑很正常,及时做好备份以及做好防护
  • MySQL数据库被攻击,被删库勒索,逼迫我使出洪荒之力进行恢复数据2022-04-13
    capture:我的刚上线两天,网站里就两篇文章也被攻击了,纳闷
  • Java常用集合List、Map、Set介绍以及一些面试问题2022-01-18
    Java技术债务:HashSet和TreeSet 相同点:数据不能重复 不同点: 1、底层存储结构不同; HashSet底层使用HashMap哈希表存储 TreeSet底层使用TreeMap树结构存储 2、唯一性方式不同 HashSet底层使用hashcode()和equal()方法判断 TreeSet底层使用Comparable接口的compareTo判断的 3、HashSet无序,TreeSet有序
  • undefined2021-12-14
    Java技术债务:如果不指定线程池,CompletableFuture会默认使用ForkJoin线程池,如果同一时间出现大量请求的话,会出现线程等待问题,建议使用自定义线程池。。。
  • undefined2021-12-02
    you:很好,对于小白相当不错了,谢谢
  • CSDN
  • 博客园
  • 程序猿DD
  • 纯洁的微笑
  • spring4all
  • 廖雪峰的官方网站
  • 猿天地
  • 泥瓦匠BYSocket
  • crossoverJie
  • 张先森个人博客
  • 越加网

© 2021-2022 Java技术债务 - Java技术债务 版权所有
总访问量 0 次 您是本文第 0 位童鞋
豫ICP备2021034516号
Java技术债务 豫公网安备 51011402000164号

微信公众号

Java技术债务
Java技术债务

专注于Spring,SpringBoot等后端技术探索

以及MySql数据库开发和Netty等后端流行框架学习

日志
分类
标签
RSS

有不足之处也希望各位前辈指出