专栏首页moon聊技术锁┃你们要的分布式锁,moon给你们肝出来了!!!

锁┃你们要的分布式锁,moon给你们肝出来了!!!

前言

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

什么是分布式锁?

为什么会出现分布式锁?

分布式锁有哪些特点?

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

怎么用redis实现分布式锁?

怎么用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 应该再次尝试获取锁,如果获取到锁,则就可以开始业务的处理

结语

今天的文章内容属实有点多,大家看起来可能比较累,慢慢消化,内容还是比较丰富的,本来想着只写redis实现分布式锁的方式,但是想了想,还是都写了,所以redis系列的文章就暂时画上一个句号了,其实redis中还有很多细节上的点可以讲,比如字典,sds内存预分配,zset跳跃链表等等,但是其实这些都属于小细节,当然也是很重要的,所以我也会在后续慢慢和大家聊到。

那么下期就开始新的篇章mysql,让我想想第一篇聊什么好呢?

END

本文分享自微信公众号 - moon聊技术(onetraveller_llxz),作者:moon聊技术

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2020-10-13

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • mysql 的三种锁

    锁的重要性想必不用多说了吧,作为面试造火箭中最重要的一个点之一,可谓是不得不会,说出来都是一把辛酸泪,什么悲观锁,乐观锁,自旋锁,偏向锁等等等等,虽然说在我们平...

    Java宝典
  • 面试官问我:Object o = new Object() 占用了多少个字节?

    最近马师傅火的不要不要的,虽然没有抢到耗子尾汁的商标注册权,但是必须得蹭一波马师傅的热度,下面就是闪电五连鞭的教学环节,你准备好了吗!

    Java宝典
  • 接口的幂等性的多重考虑,你会了吗?

    今天的主题:接口幂等性的解决方案。本来是想把对象的存储过程和内存布局肝出来的,但是临时产生了变化,哈哈,这部分内容我们留在下一期吧,有句话说的好,好事多...

    moon聊技术
  • 面试官问我:Object o = new Object() 占用了多少个字节?

    这张图其实就能完整的说明一个对象的创建过程到底发生了什么,很多朋友可能一下看不懂,那么我们就跟着左上角的一步一步来:

    程序IT圈
  • 他写的东西,你们喜欢么?

    你可能经常会从各种公众号看到《程序员必知的基础知识》和 《操作系统总结》,没错这就是我写的。具体的链接如下

    cxuan
  • mysql┃多个角度说明sql优化,让你吊打面试官!

    mysql的优化是我们经常都会提到的一个话题,也是重中之重,在很多大厂中会有专门的DBA来做这件事情,甚至更过分的是连应届生的招聘岗位要求上都写了需要懂...

    moon聊技术
  • 还有比Redis更骚的分布式锁的实现方式吗?有,etcd!

    关于为什么要有「分布式锁」这个东西,欢迎阅读我的zk分布式锁的实现,介绍了单机高并发、分布式高并发的解决方案:

    行百里er
  • 高并发下的接口幂等性解决方案!

    来源:blog.csdn.net/u011635492/article/details/81058153

    芋道源码
  • Synchronized和 ReentrantLock到底怎么选,我蒙了。。。

    Synchronized 和 ReentrantLock 大家应该都不陌生了,作为java中最常用的本地锁,最初版本中 ReentrantLock 的性能是远...

    Java宝典
  • 《孤岛惊魂:新曙光》Steam版解锁 首发获特别好评

    《孤岛惊魂:新曙光》Steam版今日正式解锁,尽管本作的媒体评分不算好,但Steam版首发评价看起来却非常不错。截止到目前,Steam上评价为“特别好评”,好评...

    Zip
  • 【A】兼容Core3.0后 Natasha 的隔离域与热编译操作。

    深度克隆:https://github.com/night-moon-studio/DeepClone

    心莱科技雪雁
  • ceRNA研究思路!

    ceRNA(竞争性内源RNA)是近几年的研究热点,已知microRNA可以通过结合mRNA导致基因沉默,而ceRNA可以通过竞争性地结合microRNA来调节基...

    生信交流平台
  • 从人到电子人的科技之路|洞见

    刘彩红 ThoughtWorks 2016年5月底,ThoughtWorks在纽约办公室再次倾听并访问了艺术家Neil Harbisson和Moon Riba...

    ThoughtWorks
  • 【基因革命】自然选择已经失效,智能进化是未来么?

    作者:Peter Diamandis 奇点大学执行主席,和库兹韦尔共同创立了奇点大学(Singuarity University),致力于培养面向未来的、有“全...

    新智元
  • RocketMQ实战(三)分布式事务

    接 RocketMQ实战(一),RocketMQ实战(二),本篇博客主要讨论的话题是:顺序消费、RMQ在分布式事务中的应用等。

    Java旅途
  • moon不讲武德!!!一个类加载机制给面试官说蒙了!!

    距离上次发表文章已经一周了,本来是打算早点肝出来的,但是由于不可抗力因素,年终了,需求急剧增加,再加上moon得给自己留出点学习时间,这篇文章也就拖到了...

    moon聊技术
  • 背完这套Java面试八股文,自动解锁面试牛逼症被动技能

    国内的互联网面试,恐怕是现存的、最接近科举考试的制度。很多人对八股文都嗤之以鼻,认为无法衡量出一个程序员的真是水平。还有一部分人则是深恶痛绝,因为实在太难背了。

    北游
  • RocketMQ实战(三):分布式事务关于多Master多Slave的说明Queue in Topic初步认识RocketMQ的核心模块 Order MessageTransaction Messag

    由于在之前的博客中已经搭建了双Master,其实多Master多Slave大同小异,因此这里并不会一步步的演示搭建多Master多Slave,而是从思路上,分析...

    用户2890438
  • 2018-07-10 如何用消息系统避免分布式事务?1 本地事务2 分布式事务—两阶段提交协议3 使用消息队列来避免分布式事务参考文献

    前阵子从支付宝转账1万块钱到余额宝,这是日常生活的一件普通小事,但作为互联网研发人员的职业病,我就思考支付宝扣除1万之后,如果系统挂掉怎么办,这时余额宝账户并没...

    Albert陈凯

扫码关注云+社区

领取腾讯云代金券