目录
秒杀案例
我们来看一个秒杀小案例,每有一个用户下单库存就-1 ,在单环境部署情况下,我们添加了synchronized锁,锁住扣除库存代码块,这样不会出现多线程并发导致库存数量出现重复或负数问题。

但分布式部署,synchronized就无法解决此问题了,多个项目就有多个synchronized锁,锁之间会覆盖冲突,导致库存数量无法onebyone的减少,直至清空。
我们需要想一个策略,在分布式环境下共用一把锁,保证无论同时请求多少,库存都在一件一件扣,直到扣完为止~
分布式锁应具备的条件
多个机器在同一时间只让一个机器的一个线程执行
高性能、高可用的获取锁和释放锁
具备锁失效机制,防止死锁
具备非阻塞,没有获取到锁将直接返回获取失败
三种方案的分布式锁
分布式锁是控制分布式系统之间同步访问共享资源的一种方式。分布式锁常用的实现方式有三种:
基于数据库实现分布式锁、基于缓存实现分布式锁、基于Zookeeper实现分布式锁
三种方式都有一个前提:必须都是集群架构~
注意:分布式锁也同样存在CAP问题,因此具体使用哪种方案要综合考虑实际需求
基于数据库实现分布式锁
基于数据库其实很简单,我们直接用乐观锁就可以。这里有一篇乐观锁应用可参考
https://blog.youkuaiyun.com/Delicious_Life/article/details/107334456
需要思考的是,乐观锁把锁放在了数据库中,数据库中的锁是没有失效时间的,如果一旦锁操作失败,该记录就会一直记录在数据库中,其他线程无法获得该锁。
基于缓存实现分布式锁
拿redis举例,有两种思路:一种是用redis原生api SETNX;另一种是使用Redisson框架+Redlock
SETNX函数是只要key不存在,就把key的值设置给value,若key存在则SETNX不做任何操作。这不就是用来当锁的吗?哈哈
但SETNX并不全面,你需要填一些坑,例如设置key为锁,value必须为获取到锁的客户ID,这样来保证只有获取锁的用户或超时后才会释放锁,不能由B释放A的锁。
SETNX还有一个关键的问题,当你通过setnx+expire上锁并设置锁过期时间时,
如果setnx和expire的中间出现了Redis崩溃或服务器宕机的问题。就会出现死锁,解决办法是Reddison通过lua脚本让上锁和设置锁时间为一条原子指令执行
使用SETNX总结一个字:不安全

我们接下来看看Redisson
Redisson是redis的java客户端之一,它有很多特性,它包含了大量JUC操作、除此之外对集群支持也更强大。当然它也能做分布式锁,源码主要在RedissonLock类,源码中封装了基于lua脚本的“加锁/释放锁操作”,开箱即用。
类似于JDK提供的ReentrantLock,非常简便。
Redisson总结一个字:丝滑
最后来说说RedLock
为什么有了Redisson还要搞一个RedLock,RedLock是redis官方提供的一种分布式锁的算法,对应的方法为getRedLock
上面我们讨论分布式问题避开了一种情况,Redis主从集群,假如客户端刚向Redis主节点申请了一个锁,此时锁还没有通知给客户端,主节点挂掉了。此时主节点保存的客户端获取到锁的信息还没有同步到从节点,从节点被选为主节点后竟然不知道在这之前已经有一个客户端获取到锁了。
可想而知,倒霉的客户端并不知道自己中了500万但没公布出来,新的redis节点又重新摇奖了~
RedLock的意思是:一个客户端请求redis服务器,这个客户端需要顺序向每一个redis节点请求加锁,根据一定的超时时间来判断是否跳过该节点,当有超过半数节点收到请求加锁并且总的花费时间小于锁的有效期时,代表加锁成功。
假如锁30秒过期,5个redis,3个redis加锁时间共耗时29秒,那么加锁成功
RedLock在Redisson的基础上,尽可能的保证所有redis节点数据一致。
RedLock的问题
假设一共有5个Redis节点:A, B, C, D, E。设想发生了如下的事件序列:
- 客户端1成功锁住了A, B, C,获取锁成功(但D和E没有锁住)。
- 节点C崩溃重启了,但客户端1在C上加的锁没有持久化下来,丢失了。
- 节点C重启后,客户端2锁住了C, D, E,获取锁成功。
这样,客户端1和客户端2同时获得了锁(针对同一资源)。
虽然RedLock也并不是百分百安全的,但出问题的概率已经被大大降低。
总结:并没有绝对安全的一把锁!
基于Zookeeper实现分布式锁
获取锁
首先在zk中创建一个持久节点ParentLock。当第一个客户端想要获取锁时,需要在ParentLock节点下创建一个临时顺序节点Lock1

Client1查找ParentLock下面所有临时顺序节点并排序,判断自己创建的节点Lock1是不是顺序最靠前的一个,如果是则成功获取锁

此时,如果再有一个客户端Clinet2前来获取锁,则在ParentLock下再创建一个临时顺序节点Lock2

Client2查找ParentLock下面所有临时顺序节点并排序,判断自己所创建的节点Lock2是不是顺序最靠前的一个,结果发现节点Lock2并不是,因此Client2向排序仅在它前面的Lock1注册Watcher,用于监听Lock1节点是否存在。意味着Clinet2抢锁失败,进入等待状态

如果又来一个Clinet3,则新创建一个Lock3临时顺序节点,同理监听Lock2

这样一来Clinet1得到锁、Clinet2监听Lock1、Client3监听Lock2,类似于一个等待队列。
释放锁
当任务正常完成,lock1释放锁

当Clinet1崩溃了,zk的心跳检测机制检测不到Client1时会断开与zk的连接,根据临时节点的特性,Lock1会随之删除。

看到这里已经很明白了,无论Lock1是因为什么原因被删除了,Lock2都会上位获得锁

同理Lock2删除,Lock3上位

三种方案比较
从理解的难易程度角度(从低到高)
数据库 > 缓存 > Zookeeper
从实现的复杂性角度(从低到高)
Zookeeper >= 缓存 > 数据库
从性能角度(从高到低)
缓存 > Zookeeper >= 数据库
从可靠性角度(从高到低)
Zookeeper > 缓存 > 数据库

本文探讨了分布式环境下的秒杀场景,指出传统的synchronized无法解决多项目并发问题。接着,文章详细介绍了分布式锁的三个实现方案:基于数据库、缓存(如Redis)和Zookeeper,并分析了它们的优缺点。每种方案都需要考虑高可用、高性能、防止死锁等问题。分布式锁在数据库实现中可能面临死锁风险,Redisson作为Redis的客户端提供了便利的分布式锁功能,而RedLock通过多节点投票机制提高了锁的安全性。Zookeeper通过临时顺序节点实现锁机制,具有良好的可靠性。最后,文章对比了三种方案在理解难度、实现复杂性、性能和可靠性的差异。
6246

被折叠的 条评论
为什么被折叠?



