一篇文章吃透分布式锁

549 阅读7分钟

前言

锁其实在Java中就已经存在,synchronized关键字和ReentrantLock可重入锁在我们日常写代码时,就会时常用到,一般我们的应用场景,都是在对统一资源进行并发访问,随着互联网行业日渐发展,分布式和微服务生态架构的高速发展,单体服务中的加锁已经不再能满足我们的需求,在分布式环境中单体服务加锁的会失去应有的作用。所以为了解决在分布式场景下需要加锁的需求,分布式锁的解决方案也就应景出现了。

分布式锁出现的意义

在分布式场景下,多个节点中执行同一个任务,但是这个任务一次只能被一条线程给执行,这个时候java自带的锁已经做不到了,所以分布式锁解决方案也就开始出现。我们举个常用的例子,比如抢购场景下,我们仓库有100件商品,但是大家哄抢上来肯定是不行的,因为业务的执行流程是 查询库存 -> 库存是否大于0 -> 库存大于0 -> 当前库存减去购买数 一旦在多个用户同时查询库存,都获取到大于0然后去减少库存,最后就有可能会导致超卖的情况。还有一种情况就是例如发送邮件这种功能,假设有三个节点不可能每个节点都去发送邮件,所以在发送邮件的时候就需要一个分布式锁,只要有一个人去发送就好了,其他人发现锁已经在的情况下,直接放弃发送邮件。

分布式锁需要具备的一些特性

  1. 互斥性 相同服务不同节点中的不同线程的互斥。
  2. 可重入性 同一个节点上的同一个线程如果获取了锁之后,同一线程在外层方法获取锁的时候,再进入内层方法会自动获取锁,也就是说,同一线程可以进入任何一个它已经拥有的锁的代码块。
  3. 超时解锁 支持超时自动解锁,防止意外造成锁无法释放导致死锁。
  4. 高可用 加锁和解锁需要高效需要保证锁的稳定性,不能因为意外发生锁的失效导致出现脏数据,也不能因为意外发生导致锁无法释放。

常用的分布式锁解决方案

MySQL行级锁

  1. 修改 用刚才秒杀的示例来说,我们如果在将整个流程合成一步,且对行数据进行加锁,那么就可以避免出现超卖的情况,实现SQL:
UPDATE STOCK - 1 FROM PRODUCT_STOCK WHERE PRODUCT_ID = 1 AND STOCK > 0

这样就同时做到了整个流程在一个SQL中执行,众所周知,UPDATE语句是自带行级锁的,所以每次对WHERE条件中指定的数据,每次只会执行一次UPDATE,这样就不会造成超卖的情况了。

  1. 查询 我们有时候经常需要去实时查询展示库存,那么为了拿到最新的库存需要去锁定这条数据,让它等待查询完成后再去修改,这时候可以用 FOR UPDATE 进行加锁,SQL:
SELECT STOCK FROM PRODUCT_STOCK WHERE PRODUCT_ID = 1 FOR UPDATE

SQL语句中的FOR UPDATE就会对这行数据进行上锁。

  1. 原理 其实UPDATE语句,和FOR UPDATE原理是相同的,FOR UPDATE就是为了模拟UPDATE语句,去获取锁。行级锁的本质是阻塞锁,会在找到WHERE条件的指定数据后去申请一把锁,如果申请到了,就去执行前半段语句,如果未申请到,就会一直阻塞循环获取锁,直到锁被获取到。所以 如果同一资源访问并发量较大的情况下是会导致阻塞获取锁,蝴蝶效应造成资源占用过高

Redis

  1. 实现 现在大家搜索分布式锁解决方案,网上实现方案最多的,应该就是Redis了,Redis因为其性能优异,实现分布式锁便捷等特点,在分布式锁解决方案上收到广大开发者热捧。
    接下来我们说说如何实现,对Redis经常使用或者学习过的同学,应该都是知道 setNx 的,它的全称是 set if not exist 如果不存在则设置,所以,加锁的时候我们只需要进行操作
setNx key value

这样就会去获取锁,如果存在则返回false,如果不存在,则会加上一个锁并返回true。文章最初有提到,分布式锁一定需要做超时解锁,而这条语句并没有超时参数,所以我们重新写一条语句

setNx key value ex second 

second是锁的存在时间,单位是秒。这样上的锁就会有过期时间了。

  1. 为什么优先选用Redis做分布式锁 Redis做分布式锁难度不高,性能对比Zookeeper和MySQL却强太多了。同时Redis还能兼顾缓存层的责任,只需要使用一个中间件就能完成两个任务,性能还优异,何乐而不为呢。

利用ZooKeeper特性

  1. 实现 Zookeeper有一个很重要的特性,不能重复创建一个节点,我们可以利用这个特性来实现一个分布式锁。实现流程 查看目标节点是否已经创建 -> 已经创建 -> 循环等待查询节点 -> 创建节点成功 -> 执行业务 -> 删除节点

  2. 惊群效应 举一个比较容易理解的例子,当你往一群动物中间进行投食,一次只扔一块食物,这样的话,最终抢到食物的动物只会有一只,但是所有动物都会来抢夺这个食物,最终没有抢到食物的动物只好继续等待下一块食物到来。这样的话,当你每扔一块食物,都会惊动所有的动物,这种现象,我们称之为惊群,在高并发场景下,所有线程都在等待获取锁,一旦当锁被释放,那所有的线程都会过来争抢这把锁,造成了服务器资源一瞬间的巨大消耗,网络在一瞬间也会造成冲击,最终结果甚至可能直接导致服务器宕机。

  3. 如何解决惊群效应 惊群效应的解决实现方式有很多种,但是归根到底都是在排队,让所有线程进入队列中去排队,一个一个的去获取锁执行业务,执行完再把锁给释放,让下一条线程重复操作,这样就不会造成一瞬间的资源消耗巨大。

分布式锁几个注意事项

  1. 惊群效应 在做分布式锁的时候,一定要注意惊群效应的产生,这个问题是极其容易出现的,一旦服务器处理能力不上请求速度,线程积累过多,一瞬间惊群就可能直接让服务器宕机。

  2. GC的STW 之前我们提到过惊群效应是可以用排队来解决的,但是这中间也是存在问题的,如果队伍过长,服务器资源不够,那么必定一直触发JVM的GC回收,GC的回收是会造成STW(STOP THE WORLD),频繁的GC会导致服务锁死,一直在停顿状态,无法继续执行任务,所以在排队的过程中也需要去限制队列长度,超过长度就告诉他们不用排队了,服务器承受不住了。

  3. 网络IO 分布式锁的实现都是通过第三方中间件来实现的,那么必然存在网络消耗,如果阻塞的线程过多,都在循环获取锁,线程阻塞越多,网络压力就会越大。所以在设计锁的时候也要考虑网络性能和业务场景,设置合适的队列长度,合适的循环时间和合适的超时时间

结语

这期文章讲解了分布式锁的多个实现方案,也提到了其中需要注意的一些问题,同时也方便大家在现有中间件和业务场景的考虑下,选用适合自己的分布式锁,避免无端的资源浪费。

感谢大家的收看,我的公众号如下欢迎关注

公众号