前言:
实现分布式锁的几种方案
1.Redis实现 (推荐)
2.Zookeeper实现
3.数据库实现
项目中使用Eureka注册中心,所以未研究Zookeeper实现.
什么是分布式锁
分布式锁的特征:
-
「互斥性」: 任意时刻,只有一个客户端能持有锁。
-
「锁超时释放」:持有锁超时,可以释放,防止不必要的资源浪费,也可以防止死锁。
-
「可重入性」:一个线程如果获取了锁之后,可以再次对其请求加锁。
-
「高性能和高可用」:加锁和解锁需要开销尽可能低,同时也要保证高可用,避免分布式锁失效。
-
「安全性」:锁只能被持有的客户端删除,不能被其他客户端删除
数据库实现分布式和redis实现的缺点
数据库实现分布式缺点:
缺点:
1、这把锁强依赖数据库的可用性,数据库是一个单点,一旦数据库挂掉,会导致业务系统不可用。
2、这把锁没有失效时间,一旦解锁操作失败,就会导致锁记录一直在数据库中,其他线程无法再获得到锁。
3、这把锁只能是非阻塞的,因为数据的insert操作,一旦插入失败就会直接报错。没有获得锁的线程并不会进入排队队列,要想再次获得锁就要再次触发获得锁操作。
4、这把锁是非重入的,同一个线程在没有释放锁之前无法再次获得该锁。因为数据中数据已经存在了。
Redis实现布式锁缺点:
缺点: 使用 Redis 实现分布式锁方案最大的问题就是如果你对某个 Redis Master 实例完成了加锁,此时 Master 会异步复制给其对应的 slave 实例。但是这个过程中一旦 Master 宕机,主备切换,slave 变为了 Master。接着就会导致,客户端 2 来尝试加锁的时候,在新的 Master 上完成了加锁,而客户端 1 也以为自己成功加了锁,此时就会导致多个客户端对一个分布式锁完成了加锁,这时系统在业务语义上一定会出现问题,导致各种脏数据的产生。所以这个就是 Redis Cluster 或者说是 Redis Master-Slave 架构的主从异步复制导致的 Redis 分布式锁的最大缺陷(在 Redis Master 实例宕机的时候,可能导致多个客户端同时完成加锁)。
使用Redis官方推荐的Redlock实现分布式锁
官网文档地址如下:Distributed locks with Redis – Redis
redlock算法是为了解决什么问题呢?
在单redis实例实现分布式锁时,可能会出现线程A设置完锁后,master挂掉,slave提升为master,因为异步复制的特性,线程A设置的锁丢失了,这时候线程B设置锁也能够成功,导致线程A和B同时拥有锁
然后redis作者提出了redlock算法
算法描述
-
获得当前时间(ms)
-
首先设置一个锁有效时间valid_time,也就是超过这个时间后锁自动释放,使用相同的key和value对所有redis实例进行设置,每次链接redis实例时设置一个小于valid_time的超时时间,比如valid_time时10s,那超时时间可以设置成50ms,如果这个实例不行,那么换下一个设置
-
计算获取锁总共占用的时间,再加上时钟偏移,如果这个总时间小于valid_time,并且成功设置锁的实例数>= N/2 + 1,那么加锁成功
-
如果加锁成功了,那么这个锁的有效时间就是valid_time - 获取锁占用的时间 - 时钟偏移
-
如果加锁失败,解锁所有实例(每个redis实例都运行del key)
单节点下模拟
配置
依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.7.0</version>
</dependency>
相关代码
Controller使用类
测试
启动两个userservice服务,分别不同端口号,userservice01中途线程沉睡五秒。02不沉睡。
开启debug模式
先查看redis中的所有key
断点至
再次查看redis
会在redis多出我们加的锁
在执行玩业务后,需要手动释放掉锁。
继续执行,再次查看 ,锁会被释放
同时像gateway发起两次请求,根据负载均衡,会各发一次。发现userservice01会沉睡五秒,02无沉睡。正常结果是01五秒后返回值,02秒返回。
因为有个分布式锁后,01和02会获取同一个lock :"lock:user",所以快速发两次请求,会发现02并不会秒返回,因为被01占用了锁,需要等01释放锁,所以两个服务会同时返回结果.
02并不会秒返回!