目录
1、分布式锁理解及手动实现
2、Redisson介绍
Redisson是架设在Redis基础上的一个Java驻内存数据网格(In-Memory Data Grid),是一款具有诸多高性能功能的综合类开源中间件,提供的功能特性及其在项目中所起的作用远大于原生Redis所提供的各种功能,让开发者对Redis的关注进行分离,可以将更多的精力放在处理业务逻辑上。
Redisson支持Redis多种连接方式(单机、集群、主从、哨兵等),包含分布式锁、布隆过滤器、分布式对象、分布式集合等多种工具。
3、引入依赖
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.16.0</version>
</dependency>
4、Redis配置
redis:
host: 192.168.xx.xx
port: xxx
password: xxx
database: 1
timeout: 7200s
jedis:
pool:
max-idle: 500
min-idle: 50
max-wait: -1
max-active: -1
5、RedissonConf配置
package com.example.learningexpansion.conf;
import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
@EnableCaching
public class RedissonConf {
@Value("${spring.redis.host}")
private String host;
@Value("${spring.redis.port}")
private String port;
@Value("${spring.redis.password}")
private String password;
@Bean(destroyMethod = "shutdown")
public RedissonClient redissonClient(){
Config config = new Config();
/**
* 连接哨兵:config.useSentinelServers().setMasterName("myMaster").addSentinelAddress()
* 连接集群:config.useClusterServers().addNodeAddress()
* 连接主从:config.useMasterSlaveServers().setMasterAddress("xxx").addSlaveAddress("xxx")
*/
// 连接单机
config.useSingleServer()
.setAddress("redis://"+host+":"+port)
.setPassword(password);
RedissonClient client = Redisson.create(config);
return client;
}
}
6、API介绍
@Api(tags = "Redis")
@RestController
@RequestMapping("/testRedis")
@Slf4j
public class TestRedisController {
@Resource
private RedissonClient redissonClient;
@GetMapping("/testRedisson")
@ApiOperation("Redisson")
public ResultVO<Object> testRedisson(@RequestParam Long goodsId) {
RLock lock = redissonClient.getLock("lock_" + goodsId);
String threadName = Thread.currentThread().getName();
try {
// 注意:若设置了锁的过期时间则没有看门狗机制
// 阻塞,拿不到锁会一直尝试获取;锁的有效期默认30秒,有看门狗机制延长锁的有效期
lock.lock();
// 阻塞,加锁成功后设置指定的有效时间,时间到自动释放锁(无论拿到锁线程是否执行结束),前提是没有调用解锁方法;没有看门狗
lock.lock(10,TimeUnit.SECONDS);
// 尝试获取锁,加锁成功后启动看门狗;非阻塞,失败立马返回;注意释放锁时要判断是否存在及是否被当前线程保持
boolean tryLock = lock.tryLock();
if (!tryLock){
return ResultUtils.error("加锁失败,请稍后重试!");
}
// 在指定时间内尝试获取锁,失败立即返回;有看门狗
boolean tryLock2 = lock.tryLock(5, TimeUnit.SECONDS);
if (!tryLock2){
return ResultUtils.error("加锁失败,请稍后重试!");
}
// 指定时间内尝试获取锁,失败立即返回;成功后设置有效时间为指定值,无看门狗
boolean tryLock1 = lock.tryLock(5, 10, TimeUnit.SECONDS);
if (!tryLock1){
return ResultUtils.error("加锁失败,请稍后重试!");
}
// 注意:异步加锁需要调用get()方法使线程执行完成,否则会造成多个线程同时拿到锁
RFuture<Void> voidRFuture = lock.lockAsync();
voidRFuture.get();
// 同lock.tryLock();
RFuture<Boolean> booleanRFuture = lock.tryLockAsync();
Boolean aBoolean = booleanRFuture.get();
if (!aBoolean){
return ResultUtils.error("加锁失败,请稍后成重试!");
}
// 注意重载方法中只有一个long时,要传的是线程ID
RFuture<Boolean> booleanRFuture1 = lock.tryLockAsync(Thread.currentThread().getId());
Boolean aBoolean1 = booleanRFuture1.get();
if (!aBoolean1){
return ResultUtils.error("加锁失败,请稍后重试!");
}
// 同lock.tryLock(5, TimeUnit.SECONDS)
RFuture<Boolean> rFuture = lock.tryLockAsync(3, TimeUnit.SECONDS);
Boolean aBoolean3 = rFuture.get();
if (!aBoolean3){
return ResultUtils.error("加锁失败,请稍后重试!");
}
// 同lock.tryLock(5, 10, TimeUnit.SECONDS)
RFuture<Boolean> booleanRFuture4 = lock.tryLockAsync(3, 10, TimeUnit.SECONDS);
Boolean aBoolean4 = booleanRFuture4.get();
if (!aBoolean4){
return ResultUtils.error("加锁失败,请稍后重试!");
}
// 原理同lock.tryLockAsync(3, 10, TimeUnit.SECONDS),区别在于多个是线程ID的参数
RFuture<Boolean> booleanRFuture5 = lock.tryLockAsync(3, 10, TimeUnit.SECONDS, Thread.currentThread().getId());
Boolean aBoolean5 = booleanRFuture5.get();
if (!aBoolean5) {
return ResultUtils.error("加锁失败,请稍后重试!");
}
log.info("{}:获取到锁", threadName);
TimeUnit.SECONDS.sleep(5);
log.info("{}:业务执行结束", threadName);
} catch (Exception e) {
log.error("testRedisson exception:", e);
return ResultUtils.sysError();
} finally {
// 判断锁是否存在
boolean locked = lock.isLocked();
// 判断锁是否被当前线程保持
boolean heldByCurrentThread = lock.isHeldByCurrentThread();
log.info("{}:获取锁状态:{} 是否当前线程保留:{}", threadName, locked, heldByCurrentThread);
if (locked && heldByCurrentThread) {
lock.unlock();
log.info("{}:释放锁", threadName);
} else {
log.info("{}:未获得锁不用释放", threadName);
}
}
return ResultUtils.success();
}
}
需要注意的一点是只有在不指定锁的过期时间时,看门狗机制才会生效,从源码可知:
if (leaseTime != -1L) {
this.internalLockLeaseTime = unit.toMillis(leaseTime);
} else {
this.scheduleExpirationRenewal(threadId);
}
7、效果演示
7.1、lock()方法演示
使用8701、8702端口同时启动两个服务,传入相同的参数,快速向两个服务各调用一次
8701服务效果:
2022-12-30 11:10:12.111 INFO 4796 --- [nio-8701-exec-3] c.e.l.c.testRedis.TestRedisController : http-nio-8701-exec-3:获取到锁
2022-12-30 11:10:17.112 INFO 4796 --- [nio-8701-exec-3] c.e.l.c.testRedis.TestRedisController : http-nio-8701-exec-3:业务执行结束
2022-12-30 11:10:17.114 INFO 4796 --- [nio-8701-exec-3] c.e.l.c.testRedis.TestRedisController : http-nio-8701-exec-3:获取锁状态:true 是否当前线程保留:true
2022-12-30 11:10:17.115 INFO 4796 --- [nio-8701-exec-3] c.e.l.c.testRedis.TestRedisController : http-nio-8701-exec-3:释放锁
8702服务效果:
2022-12-30 11:10:17.122 INFO 10168 --- [nio-8702-exec-6] c.e.l.c.testRedis.TestRedisController : http-nio-8702-exec-6:获取到锁
2022-12-30 11:10:22.124 INFO 10168 --- [nio-8702-exec-6] c.e.l.c.testRedis.TestRedisController : http-nio-8702-exec-6:业务执行结束
2022-12-30 11:10:22.126 INFO 10168 --- [nio-8702-exec-6] c.e.l.c.testRedis.TestRedisController : http-nio-8702-exec-6:获取锁状态:true 是否当前线程保留:true
2022-12-30 11:10:22.128 INFO 10168 --- [nio-8702-exec-6] c.e.l.c.testRedis.TestRedisController : http-nio-8702-exec-6:释放锁
从上述日志可看出:8701服务先拿到锁,执行完业务释放锁后8702服务才能拿到锁,达到了分布式锁想要的效果
7.2、演示看门狗机制
同样使用lock()方法,不指定过期时间(默认30秒),睡眠40s模拟执行业务,看是否自动续期
8701服务效果:
2022-12-30 14:41:25.898 INFO 13804 --- [nio-8701-exec-1] c.e.l.c.testRedis.TestRedisController : http-nio-8701-exec-1:获取到锁
2022-12-30 14:42:05.899 INFO 13804 --- [nio-8701-exec-1] c.e.l.c.testRedis.TestRedisController : http-nio-8701-exec-1:业务执行结束
2022-12-30 14:42:05.901 INFO 13804 --- [nio-8701-exec-1] c.e.l.c.testRedis.TestRedisController : http-nio-8701-exec-1:获取锁状态:true 是否当前线程保留:true
2022-12-30 14:42:05.905 INFO 13804 --- [nio-8701-exec-1] c.e.l.c.testRedis.TestRedisController : http-nio-8701-exec-1:释放锁
8702服务效果:
2022-12-30 14:42:05.913 INFO 14320 --- [nio-8702-exec-1] c.e.l.c.testRedis.TestRedisController : http-nio-8702-exec-1:获取到锁
2022-12-30 14:42:45.914 INFO 14320 --- [nio-8702-exec-1] c.e.l.c.testRedis.TestRedisController : http-nio-8702-exec-1:业务执行结束
2022-12-30 14:42:45.917 INFO 14320 --- [nio-8702-exec-1] c.e.l.c.testRedis.TestRedisController : http-nio-8702-exec-1:获取锁状态:true 是否当前线程保留:true
2022-12-30 14:42:45.921 INFO 14320 --- [nio-8702-exec-1] c.e.l.c.testRedis.TestRedisController : http-nio-8702-exec-1:释放锁
从日志可看出每个拿到锁的线程都执行了40s,并且8702服务在8701服务执行到第30s的时候仍然没有拿到锁,说明自动续期生效啦
7.3、演示无看门狗情况
同样使用lock()方法,指定过期时间为5s,睡眠8s模拟执行业务,看是否自动续期
8701服务效果:
2022-12-30 14:33:49.840 INFO 12276 --- [nio-8701-exec-1] c.e.l.c.testRedis.TestRedisController : http-nio-8701-exec-1:获取到锁
2022-12-30 14:33:57.840 INFO 12276 --- [nio-8701-exec-1] c.e.l.c.testRedis.TestRedisController : http-nio-8701-exec-1:业务执行结束
2022-12-30 14:33:57.846 INFO 12276 --- [nio-8701-exec-1] c.e.l.c.testRedis.TestRedisController : http-nio-8701-exec-1:获取锁状态:true 是否当前线程保留:false
2022-12-30 14:33:57.846 INFO 12276 --- [nio-8701-exec-1] c.e.l.c.testRedis.TestRedisController : http-nio-8701-exec-1:未获得锁不用释放
8702服务效果:
2022-12-30 14:33:54.844 INFO 11932 --- [nio-8702-exec-5] c.e.l.c.testRedis.TestRedisController : http-nio-8702-exec-5:获取到锁
2022-12-30 14:34:02.845 INFO 11932 --- [nio-8702-exec-5] c.e.l.c.testRedis.TestRedisController : http-nio-8702-exec-5:业务执行结束
2022-12-30 14:34:02.848 INFO 11932 --- [nio-8702-exec-5] c.e.l.c.testRedis.TestRedisController : http-nio-8702-exec-5:获取锁状态:false 是否当前线程保留:false
2022-12-30 14:34:02.848 INFO 11932 --- [nio-8702-exec-5] c.e.l.c.testRedis.TestRedisController : http-nio-8702-exec-5:未获得锁不用释放
从日志可看出:8701服务先拿到锁执行,5s后锁自动失效被8702服务获取到锁,此时8701的业务仍未执行结束,因此可验证结论--指定过期时间时,不会自动续期
8、公平锁
Redisson分布式锁支持公平和非公平,上文中使用的是非公平锁
公平锁遵循先到先得的原则
@GetMapping("/testFairLock")
@ApiOperation("公平锁")
public ResultVO<Object> testFairLock(@RequestParam Long goodsId) {
RLock fairLock = redissonClient.getFairLock("fairLock_" + goodsId);
String threadName = Thread.currentThread().getName();
try {
fairLock.lock();
log.info("{}:获得锁,开始执行业务", threadName);
TimeUnit.SECONDS.sleep(3);
log.info("{}:执行结束", threadName);
return ResultUtils.success();
} catch (Exception e) {
log.error("testFairLock exception:", e);
return ResultUtils.sysError();
} finally {
boolean locked = fairLock.isLocked();
boolean heldByCurrentThread = fairLock.isHeldByCurrentThread();
log.info("{}:获取锁状态:{} 是否当前线程保留:{}", threadName, locked, heldByCurrentThread);
if (locked && heldByCurrentThread) {
fairLock.unlock();
log.info("{}:释放锁成功", threadName);
} else {
log.info("{}:未获得锁不用释放", threadName);
}
}
}
9、红锁
可以使用红锁来解决主从架构锁失效问题:就是说在主从架构系统中,线程A从master中获取到分布式锁,数据还未同步到slave中时master就挂掉了,slave成为新的master,其它线程从新的master获取锁也成功了,就会出现并发安全问题
红锁算法:
- 应用程序获取系统当前时间,毫秒级
- 应用程序使用相同的key、value值依次从多个Redis实例中获取锁,如果某一个节点超过一定时间仍然没有获取到锁则直接放弃,尽快尝试从下一个Redis节点获取锁,以避免被宕机的节点阻塞
- 计算获取锁的消耗时间=客户端程序当前时间-step1中的时间,获取锁的消耗时间小于总的锁定时间(例如30s)并且半数以上节点(假如有5个节点,则至少有3个节点)获取锁成功,才认为获取锁成功
- 计算剩余锁定时间=总的锁定时间-step3中的消耗时间
- 如果获取锁失败,对所有的Redis节点释放锁(无论加锁是否成功)
// 用于Redis集群架构下,这些节点是完全独立的,所以不使用复制或任何其他隐式协调系统
// 该对象可以用来将多个RLock对象关联为一个红锁,每个RLock对象实例可以来自于不同的Redisson实例
@GetMapping("/testRedLock")
@ApiOperation("红锁")
public ResultVO<Object> testRedLock(@RequestParam Long id) {
String threadName = Thread.currentThread().getName();
RLock one = redissonClient.getLock("one_" + id);
RLock two = redissonClient.getLock("two_" + id);
RLock three = redissonClient.getLock("three_" + id);
RedissonMultiLock redLock = new RedissonRedLock(one, two, three);
try {
redLock.lock();
log.info("{}:获得锁,开始执行业务", threadName);
TimeUnit.SECONDS.sleep(2);
log.info("{}:执行结束", threadName);
return ResultUtils.success();
} catch (Exception e) {
log.error("testRedLock exception:", e);
return ResultUtils.sysError();
} finally {
// 注意:不能使用isLocked()和isHeldByCurrentThread()方法,会抛出UnsupportedOperationException异常
redLock.unlock();
log.info("{}:释放锁成功", threadName);
}
}
10、联锁
联锁(RedissonMultiLock)对象可以将多个RLock对象关联为一个联锁,实现加锁和解锁功能。每个RLock对象实例可以来自于不同的Redisson实例。
@GetMapping("/testMultLock")
@ApiOperation("联锁")
public ResultVO<Object> testMultLock(@RequestParam Long id) {
String threadName = Thread.currentThread().getName();
RLock one = redissonClient.getLock("one_" + id);
RLock two = redissonClient.getLock("two_" + id);
RLock three = redissonClient.getLock("three_" + id);
RedissonMultiLock multiLock = new RedissonMultiLock(one, two, three);
try {
// 所有的锁都上锁成功才算成功
multiLock.lock();
log.info("{}:获得锁,开始执行业务", threadName);
TimeUnit.SECONDS.sleep(3);
log.info("{}:执行结束", threadName);
return ResultUtils.success();
} catch (Exception e) {
log.error("testMultLock exception:", e);
return ResultUtils.sysError();
} finally {
// 注意:不能使用isLocked()和isHeldByCurrentThread()方法,会抛出UnsupportedOperationException异常
multiLock.unlock();
log.info("{}:释放锁成功", threadName);
}
}
11、Lua脚本实现可重入分布式锁
Lua脚本实现可重入分布式锁_mlwsmqq的博客-CSDN博客提到分布式锁,那一定绕不开Redisson,在深入Redisson源码时发现它使用了大量的lua脚本,为什么要使用lua脚本呢?答案就是它能够保证Redis操作的原子性;受到Redisson的启发,本文将带领大家一步步的通过lua脚本实现可重入分布式锁,还有两篇关于分布式锁的博客供大家参考。https://blog.csdn.net/mlwsmqq/article/details/128472150
有任何错误,欢迎大家指正!
转载请注明出处!转载请注明出处!
若本文对大家有所启示,请动动小手点赞和收藏哦!!!