分布式锁

一. 分布式锁 适用场景:

电商秒杀模块

抢优惠券 抢购场景

二. 分布式锁的实现方式:

基于数据库实现分布式锁

基于缓存(Redis等)实现分布式锁

基于Zookeeper实现分布式锁

三. 三种分布式锁的比较

从理解的难易程度角度(从低到高)

数据库 > 缓存 > Zookeeper

从实现的复杂性角度(从低到高)

Zookeeper >= 缓存 > 数据库

从性能角度(从高到低)

缓存 > Zookeeper >= 数据库

从可靠性角度(从高到低)

Zookeeper > 缓存 > 数据库

四. 分布式锁应该具备的条件:

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

2.高可用的获取锁与释放锁

3.高性能的获取锁与释放锁

4.具备可重入特性

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

6.具备非阻塞锁特性,即没有获取锁将直接返回获取锁失败

五.基于数据库实现的分布式锁

分布式锁实现方法:

悲观锁

悲观锁是在数据修改之前,把待修改的数据进行锁定,防止并发修改。通常采用for update加锁方式来实现,使用时需要注意以下两点:

  • 首先要开启事务
  • 在for update语句中where条件字段上创建索引,因为是通过索引来加锁的,否则会锁整个表。

在实际开发中,很少使用悲观锁防止并发操作,因为这种方式控制不当的话,经常会出现死锁情况。

乐观锁

乐观锁是相对悲观锁而言的,假设数据一般情况不会发生冲突的,所以在数据提交的时候,才会进行数据冲突校验,如果发送数据冲突,由用户决定如何操作。乐观锁通常是数据版本(version)机制实现的。何谓数据版本?数据版本就是在表中增加一个version字段。读取数据时,同时将version字段读取出来,数据每次更新,对此version值加1。当我们提交数据时,将对比表中对应记录的版本和取出来的版本是否一致,一致则更新成功,否则更新失败。

缺点

1、这把锁强依赖数据库的可用性,数据库是一个单点,一旦数据库挂掉,会导致业务系统不可用。

2、这把锁没有失效时间,一旦解锁操作失败,就会导致锁记录一直在数据库中,其他线程无法再获得到锁。

3、这把锁只能是非阻塞的,因为数据的insert操作,一旦插入失败就会直接报错。没有获得锁的线程并不会进入排队队列,要想再次获得锁就要再次触发获得锁操作。

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

六.基于缓存(Redis等)实现分布式锁

1.Redis实现原理

使用Redis实现分布式锁主要是运用了Redis的SETNX(SET if Not eXists) 指令和lua脚本来实现,那为什么要用到lua脚本呢?

原因只要是我们要给锁加一个过期时间并没有一个API能提供,一般都需要拆分成两个步骤,第一个将key存入Redis,第二部给key设置过期时间,这个时候就会导致线程不安全性。因为 Lua 脚本可以保证多个指令的原子性执行(单线程基于I\O多了复用和对列执行命令)。

锁超时解决方案:

1.尽量避免Redis 分布式锁不要用于较长时间的任务。如果真的偶尔出现了问题,造成的数据小错乱可能就需要人工的干预。

2.安全点的方式手动释放锁,将锁的 value 值设置为一个随机数,释放锁时先匹配随机数是否一致,然后再删除 key,这是为了确保当前线程占有的锁不会被其他线程释放,除非这个锁是因为过期了而被服务器自动释放的。这种方式也会存在问题 ,因为匹配value和删除key在 Redis 中并不是一个原子性的操作,所以也是不安全的,当然可以使用lua脚本来保证。

3.使用Redisson框架

2.Redisson实现原理

1.加锁机制:SETNX(SET if Not eXists)指令和lua脚本来实现

*Lua脚本属于一门胶水语言,也就是必须依附宿主语言起作用

2.watch dog自动延期机制,就是会启动一个后台线程,定时比如说10秒检测当前线程是否还持有了锁,是的话就延期。

3.可重入性,Redis存储锁的数据类型是 Hash类型并且Hash数据类型的key值包含了当前线程信息。

4.互斥性,通过Redis数据结构来保证分布式锁的唯一性

5.避免死锁,通过Redis的超时时间保证

Redisson机制:

超时时间:如果拿到分布式锁的节点宕机,且这个锁正好处于锁住的状态时,会出现锁死的状态,为了避免这种情况的发生,锁都会设置一个过期时间,分布式锁的超时时间默认是30秒。

设置超时时间:另外Redisson 还提供了可以指定leaseTime参数的加锁方法来指定加锁的时间。超过这个时间后锁便自动解开了,不会延长锁的有效期。

watch dog自动延期机制:这样也存在一个问题,如果一个线程拿到了锁设置了30s超时,在30s后这个线程还没有执行完毕,锁超时释放了,就会导致问题。Redisson提供了一个监控锁的看门狗,它的作用是在Redisson实例被关闭前,不断的延长锁的有效期,也就是说,如果一个拿到锁的线程一直没有完成逻辑,那么看门狗会帮助线程不断的延长锁超时时间,锁不会因为超时而被释放。默认情况下,看门狗的续期时间是30s,也可以通过修改Config.lockWatchdogTimeout来另行指定。

尝试获取锁时间:线程会尝试在一定时间内获取锁,如果超时则表示获取失败,返回false

宕机:如果宕机了定时任务跑不了,就续不了期,那自然30秒之后锁就解开了

注意:如果程序释放锁操作时因为异常没有被执行,那么锁无法被释放,所以释放锁操作一定要放到 finally {} 中;

Redisson的缺点

1.当Redis做的是Cluster集群的情况下,如果客户端对某个master节点写入了Redisson锁,此时会异步复制给对应的 slave节点。但是这个过程中一旦发生 master节点宕机,主备切换,slave节点从变为了 master节点。这时客户端2来尝试加锁的时候,由于客户端1已经宕机的master节点加锁成功,而客户端2在新的master节点上也能加锁,此时就会导致多个客户端对同一个分布式锁完成了加锁。

2.在哨兵模式或者主从模式下,如果 master实例宕机的时候,可能导致多个客户端同时完成加锁。

七.基于Zookeeper实现分布式锁(互斥锁)

Zookeeper

zookeeper实现分布式锁采用其提供的有序临时节点+监听来实现。临时节点只要客户端断开连接就会被删除,正好可以利用这一特性实现锁。

节点就是用于存储数据的,在zookeeper中,节点是类似于文件系统的目录的结构。

①永久节点。会进行持久化,重启不会丢失。

②临时节点。存储在内存中,不会持久化,重启服务或断开连接数据会丢失。

节点中存储的数据可以进行排序。

当节点中的数据有更新或是节点被删除时,会触发zookeeper的通知机制告知客户端,因为zookeeper一直在监听所有的节点。

1.zookeeper实现非公平锁

可以使用 持久化节点临时节点 来实现。推荐使用临时节点,因为持久化节点还需要手动删除,人工维护大量无用节点。临时节点session关闭就会过期,zookeeper内部线程自动删除!

2.zookeeper实现公平锁(互斥锁)

公平锁的实现需要保证节点的顺序性

可以使用zookeeper的 持久化顺序节点临时顺序节点

3.zookeeper实现读写锁(共享锁)

实现核心两点:

1.zookeeper的WatchedEvent监听事件

2.zookeeper保存节点数据的两个特点。有序和临时

订阅评论
提醒
0 评论
最旧
最新 最多投票
内联反馈
查看所有评论
滚动至顶部