基本实现
借助于redis中的命令setnx(key, value),key不存在就新增,存在就什么都不做。同时有多个客户端发送setnx命令,只有一个客户端可以成功,返回1(true);其他的客户端返回0(false)。
改造StockService方法
@Service
public class StockService {
@Autowired
private StockMapper stockMapper;
@Autowired
private StringRedisTemplate redisTemplate;
/**
* 数据库分布式锁
*/
public void checkAndLock() {
// 加锁,获取锁失败重试
while (!this.redisTemplate.opsForValue().setIfAbsent("lock", "xxx")){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 先查询库存是否充足
Stock stock = this.stockMapper.selectById(1L);
// 再减库存
if (stock != null && stock.getCount() > 0){
stock.setCount(stock.getCount() - 1);
this.stockMapper.updateById(stock);
}
// 释放锁
this.redisTemplate.delete("lock");
}
}
其中,加锁
// 加锁,获取锁失败重试
while (!this.redisTemplate.opsForValue().setIfAbsent("lock", "xxx")){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
解锁
// 释放锁
this.redisTemplate.delete("lock");
使用Jmeter压力测试如下
查看mysql数据库
防死锁
解决:给锁设置过期时间,自动释放锁。
设置过期时间两种方式:
压力测试肯定也没有问题
问题:可能会释放其他服务器的锁。
场景:如果业务逻辑的执行时间是7s。执行流程如下
最终等于没锁的情况。
解决:setnx获取锁时,设置一个指定的唯一值(例如:uuid);释放前获取这个值,判断是否自己的锁。
防误删
实现如下
问题:删除操作缺乏原子性。
场景:
解决方案:没有一个命令可以同时做到判断 + 删除,所有只能通过其他方式实现(LUA脚本)
redis中的lua脚本
现实问题
redis采用单线程架构,可以保证单个命令的原子性,但是无法保证一组命令在高并发场景下的原子性。
在串行场景下:A和B的值肯定都是3
在并发场景下:A和B的值可能在0-6之间。
极限情况下1:
则A的结果是0,B的结果是3
极限情况下2:
则A和B的结果都是6
如果redis客户端通过lua脚本把3个命令一次性发送给redis服务器,那么这三个指令就不会被其他客户端指令打断。Redis 也保证脚本会以原子性(atomic)的方式执行: 当某个脚本正在运行的时候,不会有其他脚本或 Redis 命令被执行。 这和使用 MULTI/ EXEC 包围的事务很类似。
但是MULTI/ EXEC方法来使用事务功能,将一组命令打包执行,无法进行业务逻辑的操作。这期间有某一条命令执行报错(例如给字符串自增),其他的命令还是会执行,并不会回滚。
lua介绍
Lua 是一种轻量小巧的脚本语言,用标准C语言编写并以源代码形式开放, 其设计目的是为了嵌入应用程序中,从而为应用程序提供灵活的扩展和定制功能。
设计目的
其设计目的是为了嵌入应用程序中,从而为应用程序提供灵活的扩展和定制功能。
Lua 特性
lua基本语法
对lua脚本感兴趣的,可以到官方教程或者《菜鸟教程》。这里仅以redis中可能会用到的部分语法作介绍。
a = 5 -- 全局变量
local b = 5 -- 局部变量, redis只支持局部变量
a, b = 10, 2*x -- 等价于 a=10; b=2*x
流程控制
if( 布尔表达式 1)
then
--[ 在布尔表达式 1 为 true 时执行该语句块 --]
elseif( 布尔表达式 2)
then
--[ 在布尔表达式 2 为 true 时执行该语句块 --]
else
--[ 如果以上布尔表达式都不为 true 则执行该语句块 --]
end
redis执行lua脚本 - EVAL指令
在redis中需要通过eval命令执行lua脚本。
格式
EVAL script numkeys key [key ...] arg [arg ...]
script:lua脚本字符串,这段Lua脚本不需要(也不应该)定义函数。
numkeys:lua脚本中KEYS数组的大小
key [key ...]:KEYS数组中的元素
arg [art ...]:ARGV数组中的元素
案例1:基本案例
EVAL "return 10" 0
案例2:动态传参
EVAL "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}" 5 10 20 30 40 50 60 70 80 90
# 输出:10 20 60 70
EVAL "if KEYS[1] > ARGV[1] then return 1 else return 0 end" 1 10 20
# 输出:0
EVAL "if KEYS[1] > ARGV[1] then return 1 else return 0 end" 1 20 10
# 输出:1
传入了两个参数10和20,KEYS的长度是1,所以KEYS中有一个元素10,剩余的一个20就是ARGV数组的元素。
redis.call()中的redis是redis中提供的lua脚本类库,仅在redis环境中可以使用该类库。
案例3:执行redis类库方法
set aaa 10 -- 设置一个aaa值为10
EVAL "return redis.call('get', 'aaa')" 0
## 通过return把call方法返回给redis客户端,打印:"10"
注意:脚本里使用的所有键都应该由 KEYS 数组来传递。但并不是强制性的,代价是这样写出的脚本不能被 Redis 集群所兼容。
案例4:给redis类库方法动态传参
EVAL "return redis.call('set', KEYS[1], ARGV[1])" 1 bbb 20 1
学到这里基本可以应付redis分布式锁所需要的脚本知识了。
案例5:pcall函数的使用(了解)
-- 当call() 在执行命令的过程中发生错误时,脚本会停止执行,并返回一个脚本错误,输出错误信息
EVAL "return redis.call('sets', KEYS[1], ARGV[1]), redis.call('set', KEYS[2],
ARGV[2])" 2 bbb ccc 20 30
-- pcall函数不影响后续指令的执行
EVAL "return redis.pcall('sets', KEYS[1], ARGV[1]), redis.pcall('set',
KEYS[2], ARGV[2])" 2 bbb ccc 20 30
注意:set方法写成了sets,肯定会报错。
性能优化 - EVALSHA指令
EVAL 命令要求你在每次执行脚本的时候都发送一次脚本主体(script body)。Redis 有一个内部的缓存机制,因此它不会每次都重新编译脚本,不过在很多场合,付出无谓的带宽来传送脚本主体并不是最佳选择。
为了减少带宽的消耗, Redis 实现了 EVALSHA命令,它的作用和 EVAL 一样,都用于执行lua脚
本,但它接受的第一个参数不是脚本,而是脚本的 SHA1 编码。
EVALSHA 命令的表现如下:
如果服务器存在SHA1编码对应的的脚本,那么就会执行这个脚本;如果服务器不存在SHA1编码对应的脚本,那么会返回一个特殊的错误,提醒用户使用 EVAL 代替 EVALSHA。
-- script load会对redis脚本进行sha1加密生成加密字符串,无论脚本多长,密文长度固定,会以密文为key缓存lua基本
SCRIPT LOAD "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}"
-- 通过密文方式执行缓存的lua脚本
EVALSHA a42059b356c875f0717db19a51f6aaca9ae659ea 2 aa bb cc dd
-- 判断缓存中是否存在某个lua脚本,有返回1,无返回0
SCRIPT EXISTS a42059b356c875f0717db19a51f6aaca9ae659ea
-- 删除缓存中的lua脚本
SCRIPT FLUSH
测试
使用lua保证删除原子性
删除LUA脚本
if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del',
KEYS[1]) else return 0 end
代码实现
@Service
public class StockService {
@Autowired
private StockMapper stockMapper;
@Autowired
private StringRedisTemplate redisTemplate;
/**
* 数据库分布式锁
*/
public void checkAndLock() {
// 加锁,获取锁失败重试
String uuid = UUID.randomUUID().toString();
while (!this.redisTemplate.opsForValue().setIfAbsent("lock", uuid, 3, TimeUnit.SECONDS)) {
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 先查询库存是否充足
Stock stock = this.stockMapper.selectById(1L);
// 再减库存
if (stock != null && stock.getCount() > 0) {
stock.setCount(stock.getCount() - 1);
this.stockMapper.updateById(stock);
}
// 释放锁
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
this.redisTemplate.execute(new DefaultRedisScript<>(script,
Long.class), Arrays.asList("lock"), uuid);
}
}
压力测试
库存量也没有问题
可重入锁
由于上述加锁命令使用了 SETNX ,一旦键存在就无法再设置成功,这就导致后续同一线程内继续加锁,将会加锁失败。当一个线程执行一段代码成功获取锁之后,继续执行时,又遇到加锁的子任务代码,可重入性就保证线程能继续执行,而不可重入就是需要等待锁释放之后,再次获取锁成功,才能继续往下执行 。
用一段 Java 代码解释可重入
public synchronized void a() {
b();
}
public synchronized void b() {
// pass
}
假设 X 线程在 a 方法获取锁之后,继续执行 b 方法,如果此时不可重入,线程就必须等待锁释放,再次争抢锁。
锁明明是被 X 线程拥有,却还需要等待自己释放锁,然后再去抢锁,这看起来就很奇怪,我释放我自己~。
可重入性就可以解决这个尴尬的问题,当线程拥有锁之后,往后再遇到加锁方法,直接将加锁次数加1,然后再执行方法逻辑。退出加锁方法之后,加锁次数再减 1,当加锁次数为 0 时,锁才被真正的释放。
可以看到可重入锁最大特性就是计数,计算加锁的次数。所以当可重入锁需要在分布式环境实现时,我们也就需要统计加锁次数。
解决方案:redis + Hash
加锁脚本
Redis 提供了 Hash (哈希表)这种可以存储键值对数据结构。所以我们可以使用 Redis Hash 存储的锁的重入次数,然后利用 lua 脚本判断逻辑。
if (redis.call('exists', KEYS[1]) == 0 or redis.call('hexists', KEYS[1],
ARGV[1]) == 1)
then
redis.call('hincrby', KEYS[1], ARGV[1], 1);
redis.call('expire', KEYS[1], ARGV[2]);
return 1;
else
return 0;
end
假设值为:KEYS:[lock], ARGV[uuid, expire]
如果锁不存在或者这是自己的锁,就通过hincrby(不存在就新增并加1,存在就加1)获取锁或者锁次数加1。
解锁脚本
-- 判断 hash set 可重入 key 的值是否等于 0
-- 如果为 nil 代表 自己的锁已不存在,在尝试解其他线程的锁,解锁失败
-- 如果为 0 代表 可重入次数被减 1
-- 如果为 1 代表 该可重入 key 解锁成功
if(redis.call('hexists', KEYS[1], ARGV[1]) == 0) then
return nil;
elseif(redis.call('hincrby', KEYS[1], ARGV[1], -1) > 0) then
return 0;
else
redis.call('del', KEYS[1]);
return 1;
end;
这里之所以没有跟加锁一样使用 Boolean ,这是因为解锁 lua 脚本中,三个返回值含义如下:
代码实现
由于加解锁代码量相对较多,这里可以封装成一个工具类
具体实现
public class RedisDistributeLock {
private final StringRedisTemplate redisTemplate;
/**
* 线程局部变量,可以在线程内共享参数
*/
private final String lockName;
private String uuid;
private Integer expire = 30;
private static final ThreadLocal THREAD_LOCAL = new ThreadLocal<>();
public RedisDistributeLock(StringRedisTemplate redisTemplate, String lockName) {
this.redisTemplate = redisTemplate;
this.lockName = lockName;
this.uuid = THREAD_LOCAL.get();
if (StringUtils.isBlank(uuid)) {
this.uuid = UUID.randomUUID().toString();
THREAD_LOCAL.set(uuid);
}
}
public void lock() {
this.lock(expire);
}
public void lock(Integer expire) {
this.expire = expire;
String script = "if (redis.call('exists', KEYS[1]) == 0 or redis.call('hexists', KEYS[1], ARGV[1]) == 1)" +
"then" +
" redis.call('hincrby', KEYS[1], ARGV[1], 1);" +
" redis.call('expire', KEYS[1], ARGV[2]);" +
" return 1;" +
"else" +
" return 0;" +
"end";
if (Boolean.FALSE.equals(this.redisTemplate.execute(new DefaultRedisScript<>(script,
Boolean.class), Collections.singletonList(lockName), uuid, expire.toString()))) {
try {
// 没有获取到锁,重试
Thread.sleep(60);
lock(expire);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public void unlock() {
String script = "if(redis.call('hexists', KEYS[1], ARGV[1]) == 0) then " +
" return nil; " +
"elseif(redis.call('hincrby', KEYS[1], ARGV[1], -1) > 0) then " +
" return 0; " +
"else " +
" redis.call('del', KEYS[1]); " +
" return 1; " +
"end;";
// 如果返回值没有使用Boolean,Spring-data-redis 进行类型转换时将会把 null转为 false,这就会影响我们逻辑判断
// 所以返回类型只好使用 Long:null-解锁失败;0-重入次数减1;1-解锁成功。
Long result = this.redisTemplate.execute(new DefaultRedisScript<>
(script, Long.class), Collections.singletonList(lockName), uuid);
// 如果未返回值,代表尝试解其他线程的锁
if (result == null) {
throw new IllegalMonitorStateException("attempt to unlock lock, not locked by lockName:"
+ lockName + " with request: " + uuid);
} else if (result == 1) {
THREAD_LOCAL.remove();
}
}
}
使用及测试
在业务代码中使用
public void checkAndLock() {
// 加锁,获取锁失败重试
RedisDistributeLock lock = new RedisDistributeLock(this.redisTemplate, "lock");
lock.lock();
// 先查询库存是否充足
Stock stock = this.stockMapper.selectById(1L);
// 再减库存
if (stock != null && stock.getCount() > 0){
stock.setCount(stock.getCount() - 1);
this.stockMapper.updateById(stock);
}
// this.testSubLock();
// 释放锁
lock.unlock();
}
测试
测试可重入性
自动续期
lua脚本
if(redis.call('hexists', KEYS[1], ARGV[1]) == 1) then
redis.call('expire', KEYS[1], ARGV[2]);
return 1;
else
return 0;
end
在RedisDistributeLock中添加renewExpire方法
public class RedisDistributeLock {
private final StringRedisTemplate redisTemplate;
/**
* 线程局部变量,可以在线程内共享参数
*/
private final String lockName;
private String uuid;
private Integer expire = 30;
private static final ThreadLocal THREAD_LOCAL = new ThreadLocal<>();
/**
* 这里也可以换成线程池
* private static final ScheduledExecutorService EXECUTOR_SERVICE = new ScheduledThreadPoolExecutor(1, new BasicThreadFactory.Builder().namingPattern("example-schedule-pool-%d").daemon(true).build());
* EXECUTOR_SERVICE.schedule();
*/
private static final Timer TIMER = new Timer();
public RedisDistributeLock(StringRedisTemplate redisTemplate, String lockName) {
this.redisTemplate = redisTemplate;
this.lockName = lockName;
this.uuid = THREAD_LOCAL.get();
if (StringUtils.isBlank(uuid)) {
this.uuid = UUID.randomUUID().toString();
System.out.println("---------:" + uuid);
THREAD_LOCAL.set(uuid);
}
}
public void lock() {
this.lock(expire);
}
public void lock(Integer expire) {
this.expire = expire;
String script = "if (redis.call('exists', KEYS[1]) == 0 or redis.call('hexists', KEYS[1], ARGV[1]) == 1)" +
"then" +
" redis.call('hincrby', KEYS[1], ARGV[1], 1);" +
" redis.call('expire', KEYS[1], ARGV[2]);" +
" return 1;" +
"else" +
" return 0;" +
"end";
if (Boolean.FALSE.equals(this.redisTemplate.execute(new DefaultRedisScript<>(script,
Boolean.class), Collections.singletonList(lockName), uuid, expire.toString()))) {
try {
// 没有获取到锁,重试
Thread.sleep(60);
lock(expire);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//获取锁成功后自动续期
renewExpire();
}
public void unlock() {
String script = "if(redis.call('hexists', KEYS[1], ARGV[1]) == 0) then " +
" return nil; " +
"elseif(redis.call('hincrby', KEYS[1], ARGV[1], -1) > 0) then " +
" return 0; " +
"else " +
" redis.call('del', KEYS[1]); " +
" return 1; " +
"end;";
// 如果返回值没有使用Boolean,Spring-data-redis 进行类型转换时将会把 null转为 false,这就会影响我们逻辑判断
// 所以返回类型只好使用 Long:null-解锁失败;0-重入次数减1;1-解锁成功。
Long result = this.redisTemplate.execute(new DefaultRedisScript<>
(script, Long.class), Collections.singletonList(lockName), uuid);
// 如果未返回值,代表尝试解其他线程的锁
if (result == null) {
throw new IllegalMonitorStateException("attempt to unlock lock, not locked by lockName:"
+ lockName + " with request: " + uuid);
} else if (result == 1) {
THREAD_LOCAL.remove();
}
//设置锁之后,把uuid置未空,停止定时任务
this.uuid = null;
}
/**
* 开启定时器,自动续期
*/
private void renewExpire() {
String script = "if(redis.call('hexists', KEYS[1], ARGV[1]) == 1) then redis.call('expire', KEYS[1], ARGV[2]); return 1; else return 0; end";
TIMER.schedule(new TimerTask() {
@Override
public void run() {
// 如果uuid为空,则终止定时任务
if (StringUtils.isNotBlank(uuid)) {
redisTemplate.execute(new DefaultRedisScript<>(script,
Boolean.class), Collections.singletonList(lockName), RedisDistributeLock.this.uuid,
expire.toString());
renewExpire();
}
}
}, expire * 1000 / 3);
}
}
在lock方法中使用
在unlock方法中添加红框中的代码
红锁算法
redis集群状态下的问题:
安全失效!
解决集群下锁失效,参照redis官方网站针对redlock文档:https://redis.io/topics/distlock,在算法的分布式版本中,我们假设有N个Redis服务器。这些节点是完全独立的,因此我们不使用复制或
任何其他隐式协调系统。前几节已经描述了如何在单个实例中安全地获取和释放锁,在分布式锁算法中,将使用相同的方法在单个实例中获取和释放锁。将N设置为5是一个合理的值,因此需要在不同的计算机或虚拟机上运行5个Redis主服务器,确保它们以独立的方式发生故障。
为了获取锁,客户端执行以下操作:
每台计算机都有一个本地时钟,我们通常可以依靠不同的计算机来产生很小的时钟漂移。只有在拥有锁的客户端将在锁有效时间内(如步骤3中获得的)减去一段时间(仅几毫秒)的情况下终止工作,才能保证这一点。以补偿进程之间的时钟漂移。当客户端无法获取锁时,它应该在随机延迟后重试,以避免同时获取同一资源的多个客户端之间不同步(这可能会导致脑裂的情况:没人胜)。同样,客户端在大多数Redis实例中尝试获取锁的速度越快,出现裂脑情况(以及需要重试)的窗口就越小,因此理想情况下,客户端应尝试将SET命令发送到N个实例同时使用多路复用。
值得强调的是,对于未能获得大多数锁的客户端,尽快释放(部分)获得的锁有多么重要,这样就不必等待锁定期满才能再次获得锁(但是,如果发生了网络分区,并且客户端不再能够与Redis实例进行通信,则在等待密钥到期时需要付出可用性损失)。
redisson中的分布式锁
Redisson是一个在Redis的基础上实现的Java驻内存数据网格(In-Memory Data Grid)。它不仅
提供了一系列的分布式的Java常用对象,还提供了许多分布式服务。其中包括(BitSet, Set, Multimap,SortedSet, Map, List, Queue, BlockingQueue, Deque, BlockingDeque, Semaphore, Lock,AtomicLong, CountDownLatch, Publish / Subscribe, Bloom filter, Remote service, Spring cache,Executor service, Live Object service, Scheduler service) Redisson提供了使用Redis的最简单和最便捷的方法。Redisson的宗旨是促进使用者对Redis的关注分离(Separation of Concern),从而让使用者能够将精力更集中地放在处理业务逻辑上。
官方文档地址:https://github.com/redisson/redisson/wiki
可重入锁(Reentrant Lock)
基于Redis的Redisson分布式可重入锁 RLock Java对象实现了 java.util.concurrent.locks.Lock
接口。
大家都知道,如果负责储存这个分布式锁的Redisson节点宕机以后,而且这个锁正好处于锁住的状态时,这个锁会出现锁死的状态。为了避免这种情况的发生,Redisson内部提供了一个监控锁的看门狗,它的作用是在Redisson实例被关闭前,不断的延长锁的有效期。默认情况下,看门狗检查锁的超时时间是30秒钟,也可以通过修改 Config.lockWatchdogTimeout 来另行指定。
RLock 对象完全符合Java的Lock规范。也就是说只有拥有锁的进程才能解锁,其他进程解锁则会抛出IllegalMonitorStateException 错误。
另外Redisson还通过加锁的方法提供了 leaseTime 的参数来指定加锁的时间。超过这个时间后锁便自动解开了。
RLock lock = redisson.getLock("anyLock");
// 最常见的使用方法
lock.lock();
// 加锁以后10秒钟自动解锁
// 无需调用unlock方法手动解锁
lock.lock(10, TimeUnit.SECONDS);
// 尝试加锁,最多等待100秒,上锁以后10秒自动解锁
boolean res = lock.tryLock(100, 10, TimeUnit.SECONDS);
if (res) {
try {
...
} finally {
lock.unlock();
}
}
1. 引入依赖
org.redisson
redisson
3.11.2
2. 添加配置
@Configuration
public class RedissonConfig {
@Bean
public RedissonClient redissonClient(){
Config config = new Config();
// 可以用"rediss://"来启用SSL连接
config.useSingleServer().setAddress("redis://172.16.116.100:6379");
return Redisson.create(config);
}
}
3. 代码中使用
@Autowired
private RedissonClient redissonClient;
public void checkAndLock() {
// 加锁,获取锁失败重试
RLock lock = this.redissonClient.getLock("lock");
lock.lock();
// 先查询库存是否充足
Stock stock = this.stockMapper.selectById(1L);
// 再减库存
if (stock != null && stock.getCount() > 0){
stock.setCount(stock.getCount() - 1);
this.stockMapper.updateById(stock);
}
// 释放锁
lock.unlock();
}
4. 压力测试
性能跟我们手写的区别不大。
数据库也没有问题
公平锁(Fair Lock)
基于Redis的Redisson分布式可重入公平锁也是实现了 java.util.concurrent.locks.Lock 接口的一
种 RLock 对象。同时还提供了异步(Async)、反射式(Reactive)和RxJava2标准的接口。它保证了当多个Redisson客户端线程同时请求加锁时,优先分配给先发出请求的线程。所有请求线程会在一个队列中排队,当某个线程出现宕机时,Redisson会等待5秒后继续下一个线程,也就是说如果前面有5个线程都处于等待状态,那么后面的线程会等待至少25秒。
RLock fairLock = redisson.getFairLock("anyLock");
// 最常见的使用方法
fairLock.lock();
// 10秒钟以后自动解锁
// 无需调用unlock方法手动解锁
fairLock.lock(10, TimeUnit.SECONDS);
// 尝试加锁,最多等待100秒,上锁以后10秒自动解锁
boolean res = fairLock.tryLock(100, 10, TimeUnit.SECONDS);
fairLock.unlock();
联锁(MultiLock)
基于Redis的Redisson分布式联锁 RedissonMultiLock 对象可以将多个 RLock 对象关联为一个联锁,每个 RLock 对象实例可以来自于不同的Redisson实例
RLock lock1 = redissonInstance1.getLock("lock1");
RLock lock2 = redissonInstance2.getLock("lock2");
RLock lock3 = redissonInstance3.getLock("lock3");
RedissonMultiLock lock = new RedissonMultiLock(lock1, lock2, lock3);
// 同时加锁:lock1 lock2 lock3
// 所有的锁都上锁成功才算成功。
lock.lock();
...
lock.unlock();
红锁(RedLock)
基于Redis的Redisson红锁 RedissonRedLock 对象实现了Redlock介绍的加锁算法。该对象也可以用来将多个 RLock 对象关联为一个红锁,每个 RLock 对象实例可以来自于不同的Redisson实例。
RLock lock1 = redissonInstance1.getLock("lock1");
RLock lock2 = redissonInstance2.getLock("lock2");
RLock lock3 = redissonInstance3.getLock("lock3");
RedissonRedLock lock = new RedissonRedLock(lock1, lock2, lock3);
// 同时加锁:lock1 lock2 lock3
// 红锁在大部分节点上加锁成功就算成功。
lock.lock();
...
lock.unlock();
读写锁(ReadWriteLock)
基于Redis的Redisson分布式可重入读写锁 RReadWriteLock Java对象实java.util.concurrent.locks.ReadWriteLock 接口。其中读锁和写锁都继承了RLock接口。
分布式可重入读写锁允许同时有多个读锁和一个写锁处于加锁状态。
RReadWriteLock rwlock = redisson.getReadWriteLock("anyRWLock");
// 最常见的使用方法
rwlock.readLock().lock();
// 或
rwlock.writeLock().lock();
// 10秒钟以后自动解锁
// 无需调用unlock方法手动解锁
rwlock.readLock().lock(10, TimeUnit.SECONDS);
// 或
rwlock.writeLock().lock(10, TimeUnit.SECONDS);
// 尝试加锁,最多等待100秒,上锁以后10秒自动解锁
boolean res = rwlock.readLock().tryLock(100, 10, TimeUnit.SECONDS);
// 或
boolean res = rwlock.writeLock().tryLock(100, 10, TimeUnit.SECONDS);
...
lock.unlock();
添加StockController方法
@GetMapping("/test/read")
public String testRead(){
String msg = stockService.testRead();
return "测试读";
}
@GetMapping("/test/write")
public String testWrite(){
String msg = stockService.testWrite();
return "测试写";
}
添加StockService方法
public String testRead() {
RReadWriteLock rwLock = this.redissonClient.getReadWriteLock("rwLock");
rwLock.readLock().lock(10, TimeUnit.SECONDS);
System.out.println("测试读锁。。。。");
// rwLock.readLock().unlock();
return null;
}
public String testWrite() {
RReadWriteLock rwLock = this.redissonClient.getReadWriteLock("rwLock");
rwLock.writeLock().lock(10, TimeUnit.SECONDS);
System.out.println("测试写锁。。。。");
// rwLock.writeLock().unlock();
return null;
}
打开开两个浏览器窗口测试:
信号量(Semaphore)
基于Redis的Redisson的分布式信号量(Semaphore)Java对象 RSemaphore 采用了与java.util.concurrent.Semaphore 相似的接口和用法。同时还提供了异步(Async)、反射式 (Reactive)和RxJava2标准的接口。
RSemaphore semaphore = redisson.getSemaphore("semaphore");
semaphore.acquire();
semaphore.release();
在StockController添加方法
@GetMapping("test/semaphore")
public String testSemaphore(){
this.stockService.testSemaphore();
return "测试信号量";
}
在StockService添加方法
public void testSemaphore() {
RSemaphore semaphore = this.redissonClient.getSemaphore("semaphore");
semaphore.trySetPermits(3);
try {
semaphore.acquire();
TimeUnit.SECONDS.sleep(5);
System.out.println(System.currentTimeMillis());
semaphore.release();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
添加测试用例:并发10次,循环一次
控制台效果
由此可知:
1606960790秒有3次请求进来:每个控制台各1次
1606960795秒有3次请求进来:控制台2有1次,控制台3有2次
1606960800秒有3次请求进来:控制台1有2次,控制台2有1次
1606960805秒有1次请求进来:控制台1有1次
闭锁(CountDownLatch)
基于Redisson的Redisson分布式闭锁(CountDownLatch)Java对象 RCountDownLatch 采用了与
java.util.concurrent.CountDownLatch 相似的接口和用法。
RCountDownLatch latch = redisson.getCountDownLatch("anyCountDownLatch");
latch.trySetCount(1);
latch.await();
// 在其他线程或其他JVM里
RCountDownLatch latch = redisson.getCountDownLatch("anyCountDownLatch");
latch.countDown();
需要两个方法:一个等待,一个计数countDown
给StockController添加测试方法
@GetMapping("test/latch")
public String testLatch(){
this.stockService.testLatch();
return "班长锁门。。。";
}
@GetMapping("test/countdown")
public String testCountDown(){
this.stockService.testCountDown();
return "出来了一位同学";
}
给StockService添加测试方法
public void testLatch() {
RCountDownLatch latch = this.redissonClient.getCountDownLatch("latch");
latch.trySetCount(6);
try {
latch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public void testCountDown() {
RCountDownLatch latch = this.redissonClient.getCountDownLatch("latch");
latch.trySetCount(6);
latch.countDown();
}
重启测试,打开两个页面:当第二个请求执行6次之后,第一个请求才会执行。