837-学习分布式锁

学习分布式锁

从最初的单机部署,到我们现在的分布式,微服务,随着开发场景越来越复杂,也随之产生了很多很多以前没有遇到过的问题,比如今天我们今天的话题-----分布式锁,而这个话题也可以说是面试中必问的一个问题了,当然,实现分布式锁的方式有很多种,比如mysql,zookeeper等,读完这篇文章你将学到:

什么是分布式锁?

简而言之,就是控制我们现在分布式环境下某一个资源的唯一开关,它是控制分布式系统之间互斥访问共享资源的一种方式。

为什么会出现分布式锁?

说为何会分布式锁之前我们先给大家说下我们经常见的单机锁,我们看下下面这张图:
在这里插入图片描述
在传统的单机项目中,如果同时有很多请求请求同一个资源,我们可以使用synchronized关键字来锁住请求资源的代码块,那么请求就会如下图一样顺序执行(这里也可能是请求2在前面,关键是串行)。
在这里插入图片描述
因为synchronized是jvm锁,是针对当前jvm来完成这个功能的。

所以,问题来了,如果我们在另一台服务器部署了相同的项目,只用synchronized会有什么问题呢?
在这里插入图片描述
这就是比较简单的一个分布式的请求图,我们把它再精简下
在这里插入图片描述
我们可以发现,使用synchronized是无法解决业务需求的。
于是问题就产生了:
我们的业务场景不允许相同的资源同时请求数据库,而synchronized又是一个单机锁,无法锁住不同jvm上的资源,所以,为了解决这个问题,出现了分布式锁

在这里插入图片描述
上图就是我们真实的业务场景中一个简单的分布式锁示意图

分布式锁具有哪些特点?

① 互斥性: 同一时刻只能有一个线程持有锁
②可重入性: 同一节点上的同一个线程如果获取了锁之后能够再次获取锁
③锁超时:和J.U.C中的锁一样支持锁超时,防止死锁
④高性能和高可用: 加锁和解锁需要高效,同时也需要保证高可用,防止分布式锁失效
⑤具备阻塞和非阻塞性:能够及时从阻塞状态中被唤醒
⑥容错:当部分节点(redis节点等)宕机时,客户端仍然能够获取锁和释放锁。

分布式锁的实现方式有哪些?

①基于mysql实现分布式锁
②基于redis实现分布式锁
③基于zookeeper
④基于etcd的实现
⑤…

基于mysql实现分布式锁

① 利用数据库主键来实现分布式锁
利用主键唯一的特性,如果有多个请求同时提交到数据库的话,数据库会保证只有一个操作可以成功,那么我们就可以认为操作成功的那个线程获得了该方法的锁,当方法执行完毕之后,想要释放锁的话,删除这条数据库记录即可。

②基于表字段版本号做分布式锁
这个策略源于 mysql 的 mvcc 机制(这个后期在mysql篇会给大家讲到),使用这个策略其实本身没有什么问题,唯一的问题就是对数据表侵入较大,我们要为每个表设计一个版本号字段,然后写一条判断 sql 每次进行判断,增加了数据库操作的次数,在高并发的要求下,对数据库连接的开销也是无法忍受的。

③基于数据库排他锁做分布式锁
在查询语句后面增加for update,数据库会在查询过程中给数据库表增加排他锁 (注意:InnoDB 引擎在加锁的时候,只有通过索引进行检索的时候才会使用行级锁,否则会使用表级锁。这里我们希望使用行级锁,就要给要执行的方法字段名添加索引,值得注意的是,这个索引一定要创建成唯一索引,否则会出现多个重载方法之间无法同时被访问的问题。重载方法的话建议把参数类型也加上。)。当某条记录被加上排他锁之后,其他线程无法再在该行记录上增加排他锁。

基于redis实现分布式锁

①使用Lua脚本(包含setnx和expire两条指令,最常用)

使用步骤
1、setnx(lockkey, 1) 如果返回 0,则说明占位失败;如果返回 1,则说明占位成功
2、expire() 命令对 lockkey 设置超时时间,为的是避免死锁问题。
3、执行完业务代码后,可以通过 delete 命令删除 key。

如果在第一步 setnx 执行成功后,在 expire() 命令执行成功前,发生了宕机的现象,那么就依然会出现死锁的问题,所以我们一定要使用lua脚本去完成这两项操作,因为在lua脚本中的整段命令是具有原子性的,可以保证两个命令一起成功

②使用 set key value [EX seconds][PX milliseconds][NX|XX]命令
Redis在 2.6.12 版本开始,为 SET 命令增加一系列选项:

SET key value[EX seconds][PX milliseconds][NX|XX]br

EX seconds: 设定过期时间,单位为秒

PX milliseconds: 设定过期时间,单位为毫秒

NX: 仅当key不存在时设置值

XX: 仅当key存在时设置值

set命令的nx选项,就等同于setnx命令,代码过程如下:

public boolean tryLock_with_set(String key, String UniqueId, int seconds)
{ 
	return "OK".equals(jedis.set(key, UniqueId, "NX", "EX", seconds));
}

value必须要具有唯一性,我们可以用UUID来做,设置随机字符串保证唯一性,至于为什么要保证唯一性?假如value不是随机字符串,而是一个固定值,那么就可能存在下面的问题:

1、客户端1获取锁成功
2、客户端1在某个操作上阻塞了太长时间
3、设置的key过期了,锁自动释放了
4、客户端2获取到了对应同一个资源的锁
5、客户端1从阻塞中恢复过来,因为value值一样,所以执行释放锁操作时就会释放掉客户端2持有的锁,这样就会造成问题

所以通常来说,在释放锁时,我们需要对value进行验证,避免出现上述获取错误锁资源的情况

③基于 Redlock 做分布式锁
Redlock 是 Redis 的作者 antirez 给出的集群模式的 Redis 分布式锁,它基于 N 个完全独立的 Redis 节点(通常情况下 N 可以设置成 5)。

算法的步骤如下:
1、客户端获取当前时间,以毫秒为单位。

2、客户端尝试获取 N 个节点的锁,(每个节点获取锁的方式和前面说的缓存锁一样),N 个节点以相同的 key 和 value 获取锁。

客户端需要设置接口访问超时,接口超时时间需要远远小于锁超时时间,比如锁自动释放的时间是 10s,那么接口超时大概设置 5-50ms。

这样可以在有 redis 节点宕机后,访问该节点时能尽快超时,而减小锁的正常使用。

3、客户端计算在获得锁的时候花费了多少时间,方法是用当前时间减去在步骤一获取的时间,只有客户端获得了超过 3 个节点的锁,而且获取锁的时间小于锁的超时时间,客户端才获得了分布式锁。

4、客户端获取的锁的时间为设置的锁超时时间减去步骤三计算出的获取锁花费时间。

5、如果客户端获取锁失败了,客户端会依次删除所有的锁。使用 Redlock 算法,可以保证在挂掉最多 2 个节点的时候,分布式锁服务仍然能工作,这相比之前的数据库锁和缓存锁大大提高了可用性,由于 redis 的高效性能,分布式缓存锁性能并不比数据库锁差。

使用zookeeper来实现分布式锁

①临时节点
步骤
1、让多个进程(或线程)竞争性地去创建同一个临时节点,由于 ZooKeeper 不允许存在两个完全相同节点,因此必然只有一个进程能够抢先创建成功 ;

2、假设是进程 A 成功创建了节点,则它获得该分布式锁。此时其他进程需要在 parent_node 上注册监听,监听其下所有子节点的变化,并挂起当前线程;
3、当 parent_node 下有子节点发生变化时候,它会通知所有在其上注册了监听的进程。这些进程需要判断是否是对应的锁节点上的删除事件。如果是,则让挂起的线程继续执行,并尝试再次获取锁。
如下图
在这里插入图片描述
这里之所以使用临时节点是为了避免死锁:进程 A 正常执行完业务逻辑后,会主动地去删除该节点,释放锁。但如果进程 A 意外宕机了,由于声明的是临时节点,因此该节点也会被移除,进而避免死锁。

当然,这种方式的缺点也很明显

1、由于多个进程监听了同一个父节点,所以只要此父节点下的任意一个子节点发生变动,那么zookeeper都要去通知这多个进程,会带来极大的网络开销,一个释放的消息,就好像一个牧羊犬进入了羊群,所有的羊都四散而开,随时可能冲破围栏(干掉机器),会占用服务资源,网络带宽等等,这就是羊群效应。

2、这种方式是非公平锁,也就是说在进程 A 释放锁后,进程 B,C,D 发起重试的顺序与其收到通知的时间有关,而与其第一次尝试获取锁的时间无关,即与等待时间的长短无关。

②临时有序节点方案
步骤
1、每个进程(或线程)都会尝试在 parent_node 下创建临时有序节点 。

2、然后每个进程需要获取当前 parent_node 下该锁的所有临时节点的信息,并判断自己是否是最小的一个节点
如果是,则代表获得该锁。

如果不是,则挂起当前线程。并对其前一个节点注册监听(这里可以通过 exists 方法传入需要触发 Watch 事件)。

3、当进程 A 处理完成后,会触发进程 B 注册的 Watch 事件,此时进程 B 就知道自己获得了锁,从而可以将挂起的线程继续,并开始业务的处理

这里需要注意的是一种特殊的情况,其过程如下:

1、如果进程 B 创建了临时节点,并且通过比较后知道自己不是最小的一个节点,但还没有注册监听。

2、而 A 进程此时恰好处理完成并删除了 01 节点。

3、接着进程 B 再调用 exist 方法注册监听就会抛出 IllegalArgumentException 异常,通常代表前一个节点已经不存在了。

在这种情况下进程 B 应该再次尝试获取锁,如果获取到锁,则就可以开始业务的处理

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

林林林ZEYU

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值