集群库存问题
如果是单机情况下(单JVM),线程之间共享内存,只要使用线程锁就可以解决并发问题
如果是分布式情况下(多JVM),线程A和线程B很可能不是在同一JVM中,这样线程锁就无法起到作用了,这时候就要用到分布式锁来解决
分布式锁实现机制
分布式锁一般有三种实现方式:1. 数据库乐观锁;2. 基于Redis的分布式锁;3. 基于ZooKeeper的分布式锁
基于Redis的分布式锁
SETNX lock:168 1
命令来自于SET if Not eXists的缩写,意思是:如果 key 不存在,则设置 value 给这个key,否则啥都不做
命令的返回值:
1:设置成功;
0:key 没有设置成功
这个方案存在一个存在造成锁无法释放的问题,造成该问题的场景如下:
客户端所在节点崩溃,无法正确释放锁;
业务逻辑异常,无法执行 DEL指令。
解决方法:
1.设置一个「超时时间」,到点后锁自动释放
「加锁」、「设置超时」是两个命令,他们不是原子操作。
如果出现只执行了第一条,第二条没机会执行就会出现「超时时间」设置失败,依然出现锁无法释放
2.SET resource_name random_value NX PX 30000
NX:表示只有 resource_name 不存在的时候才能 SET 成功,从而保证只有一个客户端可以获得锁;
PX 30000:表示这个锁有一个 30 秒自动过期时间。
拓展了 SET 命令的参数,满足了当 key 不存在则设置 value,同时设置超时时间的语义,并且满足原子性。
正确设置锁超时-看门狗
让获得锁的线程开启一个守护线程 ,用来给快要过期的锁「续航」。
加锁的时候设置一个过期时间,同时客户端开启一个「守护线程」,定时去检测这个锁的失效时间。
如果快要过期,但是业务逻辑还没执行完成,自动对这个锁进行续期,重新设置过期时间。
基于Redis的分布式锁-正确操作
通过 SET lock_resource_name random_value NX PX expire_time,同时启动守护线程为快要过期但还没执行完的客户端的锁续命;
客户端执行业务逻辑操作共享资源;
通过 Lua 脚本释放锁,先 get 判断锁是否是自己加的,再执行 DEL。
实现可重入锁
使用 Redis hash 结构实现,key 表示被锁的共享资源, hash 结构的 fieldKey 的 value 则保存加锁的次数。
当线程拥有锁之后,往后再遇到加锁方法,直接将加锁次数加 1,然后再执行方法逻辑。
退出加锁方法之后,加锁次数再减 1,当加锁次数为 0 时,锁才被真正的释放。
可以看到可重入锁最大特性就是计数,计算加锁的次数。
主从架构带来的问题
Redlock 红锁是为了解决主从架构中当出现主从切换导致多个客户端持有同一个锁而提出的一种算法。
Redisson
依赖:
<dependency>
<groupId>org.redisson</groupId>
<!-- for Spring Data Redis v.2.5.x -->
<artifactId>redisson-spring-data-25</artifactId>
<version>3.16.4</version>
</dependency>
添加配置文件
spring:
redis:
database:
host:
port:
password:
ssl:
timeout:
# 根据实际情况配置 cluster 或者哨兵
cluster:
nodes:
sentinel:
master:
nodes:
就这样在 Spring 容器中我们拥有以下几个 Bean 可以使用:
RedissonClient
RedissonRxClient
RedissonReactiveClient
RedisTemplate
ReactiveRedisTemplate
使用:
1.失败无限重试。拿锁失败时会不停的重试,具有 Watch Dog 自动延期机制,默认续 30s 每隔 30/3=10 秒续到 30s。
RLock lock = redisson.getLock("码哥字节");
try {
// 1.最常用的第一种写法
lock.lock();
// 执行业务逻辑
.....
} finally {
lock.unlock();
}
2.失败超时重试,自动续命
// 尝试拿锁10s后停止重试,获取失败返回false,具有Watch Dog 自动延期机制, 默认续30s
boolean flag = lock.tryLock(10, TimeUnit.SECONDS);
3.超时自动释放锁
// 没有Watch Dog ,10s后自动释放,不需要调用 unlock 释放锁。
lock.lock(10, TimeUnit.SECONDS);
4.超时重试,自动解锁
// 尝试加锁,最多等待100秒,上锁以后10秒自动解锁,没有 Watch dog
boolean res = lock.tryLock(100, 10, TimeUnit.SECONDS);
if (res) {
try {
...
} finally {
lock.unlock();
}
}
源码解读
在调用 lock 方法时,会最终调用到 tryAcquireAsync
调用链为:lock()->tryAcquire->tryAcquireAsync
加锁最终执行的就是这段 lua 脚本语言
if (redis.call('exists', KEYS[1]) == 0) then
redis.call('hset', KEYS[1], ARGV[2], 1);
redis.call('pexpire', KEYS[1], ARGV[1]);
return nil;
end;
脚本的主要逻辑为:
exists 判断 key 是否存在
当判断不存在则设置 key
然后给设置的key追加过期时间
性能提升
参考ConcurrentHashMap的锁分段技术的思想,例如我们代码的库存量当前为1000,那我们可以分为10段,每段100,然后对每段分别加锁,这样就可以同时执行10个请求的加锁与处理,当然有要求的同学还可以继续细分。但其实Redis的Qps已经达到10W+了,没有特别高并发量的场景下也是完全够用的。