一、前言
在这个微服务多节点、多线程的环境中,多个任务可能会同时竞争访问共享资源,从而导致数据错误和不一致。一般的JVM层面的加锁显然无法满足多个节点的情况!分布式锁就出现了,在redis官网推荐Java使用Redisson去实现分布式锁!
这是基本api调用,今天我们使用自定义注解来完成,一劳永逸,减少出错!
二、Redisson简介
Redisson是一个用于Java应用程序的开源的、基于Redis的分布式和高性能数据结构服务库。它提供了一系列的分布式对象和服务,帮助开发人员更轻松地在分布式环境中使用Java编程语言。Redisson通过封装Redis的功能,使得开发者能够更方便地利用分布式特性,同时提供了许多额外的功能和工具。
比setnx简单的加锁机制,Redisson会提供更完善的加锁机制,比如:
「到期方法没有执行完成,引入看门狗机制自动续期,内部使用Lua脚本保证原子性!」
「提供众多的锁:」
- 可重入锁(Reentrant Lock)
- 公平锁(Fair Lock)
- 联锁(MultiLock)
- 红锁(RedLock)
- 读写锁(ReadWriteLock)
对于今天的注解形式,只能实现可重入锁、公平锁两种形式,不过也满足大部分业务场景!
今天以实战为主,这些信息可以去官网看一下详细的文档:
Redisson文档:https://github.com/redisson/redisson/wiki/1.-Overview。
三、实战
1、导入依赖
org.springframework.boot
spring-boot-starter-data-redis
org.redisson
redisson
3.12.0
2、配置文件
server:
port: 8087
spring:
redis:
password: 123456
# 一定要加redis://
address: redis://127.0.0.1:6379
datasource:
#使用阿里的Druid
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/test?serverTimeznotallow=UTC
username: root
password:
3、RedissonClient配置
@Configuration
public class MyRedissonConfig {
@Value("${spring.redis.password}")
private String password;
@Value("${spring.redis.address}")
private String address;
@Bean(destroyMethod="shutdown")
public RedissonClient redissonClient(){
// 1. 创建配置
Config config = new Config();
// 一定要加redis://
config.useSingleServer().setAddress(address);
config.useSingleServer().setPassword(password);
// 2. 根据config创建出redissonClient实例
RedissonClient redissonClient = Redisson.create(config);
return redissonClient;
}
}
4、Redis序列化配置
@Configuration
public class RedisConfig {
@Bean
@SuppressWarnings(value = { "unchecked", "rawtypes" })
public RedisTemplate
5、自定义注解
我们自定义注解,key支持el表达式!这里的参数可以再加一个key的前缀或者锁的类型,根据类型判断:可重入锁(RLock getLock(String name))、公平锁(RLock getFairLock(String name);)这两种的加锁!等待锁超时时间、自动解锁时间、时间单位这是可选择的,大家按需,需要看门狗的有的就不需要,现在是有两种加锁机制,后面也是看大家的选择!
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Documented
public @interface RedisLock {
String value();
long waitTime() default 30;
long leaseTime() default 100;
TimeUnit timeUnit() default TimeUnit.SECONDS;
}
6、定义切片
现在有两种加锁方式,我们来详细说一下区别,大家按需选择:
「lock.tryLock():」
这是一个非阻塞的方法。如果获取锁成功,会立即返回 true,如果获取锁失败,会立即返回 false。
当然你可以添加等待时间,超过这个时间仍然没有获取到锁才会返回false。tryLock(long time, TimeUnit unit)tryLock(long waitTime, long leaseTime, TimeUnit unit)
如果你想尝试获取锁,但「不希望在获取失败时被阻塞」,可以使用这个方法。
这个方法通常用于获取锁后执行一个短时间的任务,避免长时间的等待。
「lock.lock():」
这是一个阻塞的方法,如果获取锁失败,它会阻塞当前线程,直到获取到锁或超时。因此要确保你的锁的使用不会导致长时间的等待,避免影响系统性能。
也可以添加锁的过期时间,一旦获取锁成功,锁会在指定的时间后自动释放。如果在这段时间内任务未完成,锁会自动释放,避免长时间的占用。这个时间要考虑清除,如果执行时间不可控建议还是不要传过期时间,默认会有看门狗来自动续期,防止方法执行中锁被释放了!
lock(long leaseTime, TimeUnit unit)
如果你希望一定能够获取锁,而且「不希望在获取失败时立即返回」,可以使用这个方法。
这个方法通常用于获取锁后需要执行一个相对耗时的任务,以及希望避免锁被长时间占用而引发的问题。
小编这里建议使用第一种,有锁正在执行,应该返回信息给用户,不应该让用户长时间等待造成不好的影响!
如果是第一个方法,我们需要判断返回值,加锁失败返回给用户!异常大家可以专门定义一个加锁失败异常,小编这里就使用业务异常了!
@Slf4j
@Aspect
@RequiredArgsConstructor
@Component
public class RedisLockAspect {
private final RedissonClient redissonClient;
private final SpelExpressionParser spelExpressionParser = new SpelExpressionParser();
private final DefaultParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer();
@Around("@annotation(redisLock)")
public Object aroundRedisLock(ProceedingJoinPoint point, RedisLock redisLock) throws Throwable {
log.info("=====请求来排队尝试获取锁=====");
String value = redisLock.value();
Assert.hasText(value, "@RedisLock key不能为空!");
boolean el = redisLock.isEl();
String lockKey;
if (el) {
lockKey = evaluateExpression(value, point);
} else {
lockKey = value;
}
log.info("========解析后的lockKey :{}", lockKey);
long waitTime = redisLock.waitTime();
long leaseTime = redisLock.leaseTime();
TimeUnit timeUnit = redisLock.timeUnit();
RLock lock = redissonClient.getLock(lockKey);
// lock.tryLock(waitTime, leaseTime, timeUnit);
// lock.lock(leaseTime, timeUnit);
// lock.lock();
boolean tryLock = lock.tryLock();
if (!tryLock) {
throw new ServiceException("锁被占用,请稍后提交!");
}
try {
return point.proceed();
} catch (Throwable throwable) {
log.error("方法执行失败:", throwable.getMessage());
throw throwable;
} finally {
lock.unlock();
}
}
private String evaluateExpression(String expression, ProceedingJoinPoint point) {
// 获取目标对象
Object target = point.getTarget();
// 获取方法参数
Object[] args = point.getArgs();
MethodSignature methodSignature = (MethodSignature) point.getSignature();
Method method = methodSignature.getMethod();
EvaluationContext context = new MethodBasedEvaluationContext(target, method, args, parameterNameDiscoverer);
Expression exp = spelExpressionParser.parseExpression(expression);
return exp.getValue(context, String.class);
}
}
7、测试
我们测试一个el表达式的,模拟方法执行15s,方便我们测试!
@SneakyThrows
@RedisLock("#id")
@GetMapping("/listTest")
public Result listTest(@RequestParam("id") Long id){
System.out.println("=====方法执行中");
Thread.sleep(150000);
System.out.println("=====方法执行完成");
return Result.success("成功");
}
我们调用两次这个方法,看到控制台有报错信息,返回结果也是没有问题的!
四、总结
在本篇博客中,我们深入探讨了如何在Spring Boot应用中借助自定义注解来实现分布式锁,为分布式环境下的并发问题提供了优雅且高效的解决方案。通过自定义注解,我们成功地将分布式锁的复杂逻辑进行了封装,使得在业务代码中只需简单地使用注解,便能实现分布式锁的获取和释放。这不仅让代码更具可读性,还提升了开发效率,让开发人员能够更专注于业务逻辑的实现。
相信大家已经能够对Spring Boot中使用自定义注解实现分布式锁有一个清晰的理解,加锁的方式大家可以按需选择!