Spring Boot 基于Redisson实现注解式分布式锁
2023-12-30 05:13:47
依赖版本
- JDK 17
- Spring Boot 3.2.0
- Redisson 3.25.0
源码地址:Gitee
导入依赖
<properties>
<redisson.version>3.25.0</redisson.version>
</properties>
<dependencies>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>${redisson.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
</dependencies>
配置文件
# application.yml
server:
port: 8080
spring:
# ======== Redis配置 ========
redis:
redisson:
file: classpath:redisson.yaml
# redisson.yaml
# 编码。默认值: org.redisson.codec.JsonJacksonCodec
codec: !<org.redisson.codec.Kryo5Codec> {}
# 线程池数量。默认值: 当前处理核数量 * 2
threads: 16
# Netty线程池数量。默认值: 当前处理核数量 * 2
nettyThreads: 32
# 传输模式。默认值: NIO
transportMode: "NIO"
# 监控锁的看门狗超时,单位:毫秒。默认值: 30000
lockWatchdogTimeout: 30000
# 是否保持订阅发布顺序。默认值: true
keepPubSubOrder: true
# Redisson 单实例配置
singleServerConfig:
# 节点地址。格式:redis://host:port
address: "redis://127.0.0.1:6379"
# 密码。默认值: null
password: null
# 数据库编号。默认值: 0
database: 0
# 客户端名称(在Redis节点里显示的客户端名称)。默认值: null
clientName: null
# 连接超时,单位:毫秒。默认值: 10000
connectTimeout: 10000
# 命令等待超时,单位:毫秒。默认值: 3000
timeout: 3000
# 命令失败重试次数。默认值: 3
retryAttempts: 3
# 命令重试发送时间间隔,单位:毫秒。默认值: 1500
retryInterval: 1500
# 最小空闲连接数。默认值: 32
connectionMinimumIdleSize: 24
# 连接池大小。默认值: 64
connectionPoolSize: 64
# 单个连接最大订阅数量。默认值: 5
subscriptionsPerConnection: 5
# 发布和订阅连接的最小空闲连接数。默认值: 1
subscriptionConnectionMinimumIdleSize: 1
# 发布和订阅连接池大小。默认值: 50
subscriptionConnectionPoolSize: 50
# DNS监测时间间隔,单位:毫秒。默认值: 5000
dnsMonitoringInterval: 5000
# 连接空闲超时,单位:毫秒。默认值: 10000
idleConnectionTimeout: 10000
Redisson 锁简单使用
public void lock(String key) throws InterruptedException {
RLock lock = redissonClient.getLock(key);
log.info("[Redisson 分布式] 获取锁 KEY :{}", key);
boolean lockSuccess = lock.tryLock(500, TimeUnit.MILLISECONDS);
if (!lockSuccess) {
throw new RuntimeException("获取锁失败");
}
try {
//执行锁内的代码逻辑
} finally {
if (lock.isLocked() && lock.isHeldByCurrentThread()) {
lock.unlock();
log.info("[Redisson 分布式锁] 释放锁 KEY :{}", key);
}
}
}
Redisson锁的使用很方便,提供了很多的便携方法。但是在每个需要使用锁的地方都去写这样的模板代码有点“麻烦”,所以对Redisson锁的使用进行一个简单的封装,让在开发中使用更顺手
Redisson 锁工具类封装
RedisLockService 锁工具类
采用函数式接口,可在使用时对业务代码精准落锁,减少被锁的时间,提升系统性能。
package com.yiyan.study.utils.redis;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.concurrent.TimeUnit;
import java.util.function.Supplier;
/**
* 基于Redis Redisson 分布式锁工具类
*
* @createDate 2022-12-21
*/
@Slf4j
@Component
@RequiredArgsConstructor(onConstructor_ = @Autowired)
public class RedisLockService {
private final RedissonClient redissonClient;
// 编程式Redisson锁
public <T> T executeWithLockThrows(String key, int waitTime, TimeUnit unit, SupplierThrow<T> supplier) throws Throwable {
RLock lock = redissonClient.getLock(key);
log.info("[Redisson 分布式] 获取锁 KEY :{}", key);
boolean lockSuccess = lock.tryLock(waitTime, unit);
if (!lockSuccess) {
throw new RuntimeException("获取锁失败");
}
try {
return supplier.execute();//执行锁内的代码逻辑
} finally {
if (lock.isLocked() && lock.isHeldByCurrentThread()) {
lock.unlock();
log.info("[Redisson 分布式锁] 释放锁 KEY :{}", key);
}
}
}
@SneakyThrows
public <T> T executeWithLock(String key, int waitTime, TimeUnit unit, Supplier<T> supplier) {
return executeWithLockThrows(key, waitTime, unit, supplier::get);
}
public <T> T executeWithLock(String key, Supplier<T> supplier) {
return executeWithLock(key, -1, TimeUnit.MILLISECONDS, supplier);
}
/**
* 函数式接口,用于执行锁内的代码逻辑
*/
@FunctionalInterface
public interface SupplierThrow<T> {
T execute() throws Throwable;
}
}
使用示例
public void lockLine() {
// 模拟查询等不需要锁的操作
ThreadUtil.sleep(2000L);
redisLockService.executeWithLock("lockLine", 500, TimeUnit.MILLISECONDS,
() -> {
// 模拟上锁的数据操作
ThreadUtil.sleep(200L);
return null;
});
}
Redisson 锁注解
在开发中,有些方法的功能就是原子性的,比如订单状态更新这样方法,此时方法内的代码都需要被锁住,所以可以采用注解的方式,来对需要加锁的业务进行上锁,避免编写重复冗余的代码。
RedissonLock注解
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.concurrent.TimeUnit;
/**
* Redisson 分布式锁注解
*
* @createDate 2023-09-18 07:17
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RedissonLock {
/**
* key的前缀,默认取方法全限定名,除非我们在不同方法上对同一个资源做分布式锁,就自己指定
*
* @return key的前缀
*/
String prefixKey() default "";
/**
* springEl 表达式
*
* @return 表达式
*/
String key() default "";
/**
* 等待锁的时间,默认-1,不等待直接失败,redisson默认也是-1
*
* @return 单位秒
*/
int waitTime() default -1;
/**
* 等待锁的时间单位,默认毫秒
*
* @return 单位
*/
TimeUnit unit() default TimeUnit.MILLISECONDS;
}
注解切面
import com.yiyan.study.utils.SpElUtils;
import com.yiyan.study.utils.redis.RedisLockService;
import com.yiyan.study.utils.redis.annotation.RedissonLock;
import jakarta.annotation.Resource;
import jodd.util.StringUtil;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
import java.util.concurrent.TimeUnit;
/**
* Redisson 分布式锁切面
*
* @createDate 2023-09-18 07:17
*/
@Slf4j
@Aspect
@Component
// 确保比事务注解先执行,分布式锁在事务外
@Order(0)
public class RedissonLockAspect {
@Resource
private RedisLockService redisLockService;
@Pointcut("@annotation(com.yiyan.study.utils.redis.annotation.RedissonLock)")
public void lockPointcut() {
}
@Around("lockPointcut()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
RedissonLock redissonLock = method.getAnnotation(RedissonLock.class);
// 默认方法限定名+注解排名(可能多个)
String prefix = StringUtil.isBlank(redissonLock.prefixKey()) ? SpElUtils.getMethodKey(method) : redissonLock.prefixKey();
String key = prefix + ":" + SpElUtils.parseSpEl(method, joinPoint.getArgs(), redissonLock.key());
int waitTime = redissonLock.waitTime();
TimeUnit timeUnit = redissonLock.unit();
return redisLockService.executeWithLockThrows(key, waitTime, timeUnit, joinPoint::proceed);
}
}
使用示例
@RedissonLock(prefixKey = "lockFunc", waitTime = 500)
public void lockFunc() {
// 模拟查询等不需要锁的操作
ThreadUtil.sleep(2000L);
// 模拟上锁的数据操作
ThreadUtil.sleep(200L);
}
Redisson 锁测试
编写测试接口
import com.yiyan.study.utils.redis.RedisLockService;
import com.yiyan.study.utils.redis.annotation.RedissonLock;
import jakarta.annotation.Resource;
import jodd.util.ThreadUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.concurrent.TimeUnit;
@RestController
@RequestMapping("/")
@Slf4j
public class RedisLockController {
@Resource
private RedisLockService redisLockService;
@GetMapping("/lock_func")
@RedissonLock(prefixKey = "lockFunc", waitTime = 500)
public void lockFunc() {
// 模拟查询等不需要锁的操作
ThreadUtil.sleep(2000L);
// 模拟上锁的数据操作
ThreadUtil.sleep(200L);
}
@GetMapping("/lock_line")
public void lockLine() {
// 模拟查询等不需要锁的操作
ThreadUtil.sleep(2000L);
redisLockService.executeWithLock("lockLine", 500, TimeUnit.MILLISECONDS,
() -> {
// 模拟上锁的数据操作
ThreadUtil.sleep(200L);
return null;
});
}
}
测试
使用AB测试工具,10个请求,并发为5,模拟总业务时常2.5s:
文章来源:https://blog.csdn.net/m0_55712478/article/details/135300831
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。 如若内容造成侵权/违法违规/事实不符,请联系我的编程经验分享网邮箱:veading@qq.com进行投诉反馈,一经查实,立即删除!
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。 如若内容造成侵权/违法违规/事实不符,请联系我的编程经验分享网邮箱:veading@qq.com进行投诉反馈,一经查实,立即删除!