为了防止分布式系统中的多个进程之间相互干扰,我们需要一种分布式协调技术来对这些进程进行调度。
而这个分布式协调技术的核心就是来实现这个分布式锁。
分布式锁应该具备条件
互斥性 防止死锁 可重入 非阻塞 锁的力度
目前我所知道的有3种方式
Redisson分布式锁的实现是基于实现RLock接口
1、加锁机制
线程去获取锁,获取成功: 执行lua脚本,保存数据到redis数据库。
线程去获取锁,获取失败: 一直通过while循环尝试获取锁,获取成功后,执行lua脚本,保存数据到redis数据库。
2、watch dog自动延期机制(性能较差)
在一个分布式环境下,假如一个线程获得锁后,突然服务器宕机了,那么这个时候在一定时间后这个锁会自动释放,你也可以设置锁的有效时间(不设置默认30秒),这样的目的主要是防止死锁的发生。
3、使用lua脚本
通过封装在lua脚本中发送给redis,而且redis是单线程的,这样就保证这段复杂业务逻辑执行的原子性。
Redis分布式锁会有个缺陷,就是在Redis哨兵模式下:
客户端1
对某个master节点
写入了redisson锁,此时会异步复制给对应的 slave节点。但是这个过程中一旦发生 master节点宕机,主备切换,slave节点从变为了 master节点。
这时客户端2
来尝试加锁的时候,在新的master节点上也能加锁,此时就会导致多个客户端对同一个分布式锁完成了加锁。
这时系统在业务语义上一定会出现问题,导致各种脏数据的产生。
缺陷
在哨兵模式或者主从模式下,如果 master实例宕机的时候,可能导致多个客户端同时完成加锁。
自定义starter 封装Redission 结构如下 打成jar包
源码地址:https://gitee.com/love_yu_0698/distributed-redission-spring-boot-starter.git
新建项目测试(引入jar包)
application.yml
yu:
redisson:
lock:
server:
address: 127.0.0.1:6379
password: redis
type: standalone
database: 0
/**
* 不基于注解方式锁操作
*/
@RestController
@Slf4j
public class LockController {
@Autowired
RedissonLock redissonLock;
/**
* 模拟这个是商品库存
*/
public static volatile Integer TOTAL = 10;
@GetMapping("lock-decrease-stock")
public String lockDecreaseStock() throws InterruptedException {
redissonLock.lock("lock", 10L);
if (TOTAL > 0) {
TOTAL--;
}
Thread.sleep(50);
log.info("===lock===减完库存后,当前库存===" + TOTAL);
//如果该线程还持有该锁,那么释放该锁。如果该线程不持有该锁,说明该线程的锁已到过期时间,自动释放锁
if (redissonLock.isHeldByCurrentThread("lock")) {
redissonLock.unlock("lock");
}
return "=================================";
}
@GetMapping("trylock-decrease-stock")
public String trylockDecreaseStock() throws InterruptedException {
if (redissonLock.tryLock("trylock", 5L, 200L)) {
if (TOTAL > 0) {
TOTAL--;
}
Thread.sleep(50);
redissonLock.unlock("trylock");
log.info("====tryLock===减完库存后,当前库存===" + TOTAL);
} else {
log.info("[ExecutorRedisson]获取锁失败");
}
return "===================================";
}
@GetMapping("annotatin-lock-decrease-stock")
@DistributedLock(value="goods", leaseTime=5)
public String lockDecreaseStock() throws InterruptedException {
if (TOTAL > 0) {
TOTAL--;
}
log.info("===注解模式=== 减完库存后,当前库存===" + TOTAL);
return "=================================";
}
}
使用 jmeter 开启1000个线程 测试 是否会有超卖的问题
PS:大部分情况 都是使用 lock
1、tryLock锁是可能会等待的,因为当过了等待时间还没有获取锁,就会返回false,对于性能来说,这显然很致命!
2、注解锁只能用于方法上,颗粒度太大,满足不了方法内加锁。
在使用RedissonLock锁时,很容易报这类异常,比如如下操作
//设置锁1秒过去
redissonLock.lock("redisson", 1);
//业务逻辑需要咨询2秒
redissonLock.release("redisson");
原因:
线程1 进来获得锁后,但它的业务逻辑需要执行2秒,在 线程1 执行1秒后,这个锁就自动过期了,那么这个时候
线程2 进来了获得了锁。在线程1去解锁就会抛上面这个异常(因为解锁和当前锁已经不是同一线程了)
解决:
//如果为false就说明该线程的锁已经自动释放,无需解锁
if (redissonLock.isHeldByCurrentThread("lock")) {
redissonLock.unlock("lock");
}
参考大牛的文章:https://www.cnblogs.com/qdhxhz/p/11046905.html