基于AOP实现Redis分布式锁
基于AOP实现Redis分布式锁
0.场景描述
随着业务发展的需要,原单体单机部署的系统被演化成分布式集群系统后,由于分布式系统多线程、多进程并且分布在不同机器上,这将使原单机部署情况下的并发控制锁策略失效,单纯的Java API并不能提供分布式锁的能力。为了解决这个问题就需要一种跨JVM的互斥机制来控制共享资源的访问,这就是分布式锁要解决的问题!
1.大体思路
基于注解和aop环绕的方式实现,
1.自定义一个注解:定义key的生成规则、key的过期时间、重试次数、重试间隔等
- 首先我们需要确定redis的key值,可以时直接指定的字符串也可以是通过SpringEl表达式获取参数的值做key
- 考虑不通业务场景需要执行的时间并不固定,key的过期时间作为注解的配置项,更灵活
- 添加重试策略
2.AOP环绕拦截目标方法,执行目标方法之前,我们需要先获取锁
3.获取锁成功执行目标方法,反之则不执行目标方法
4.执行完目标方法之后,释放锁(手动释放(推荐),自动释放:redis key过期)
2.准备工作
1.引入相关依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.github.xingfudeshi</groupId>
<artifactId>knife4j-openapi3-jakarta-spring-boot-starter</artifactId>
<version>4.6.0</version>
</dependency>
2.redis配置
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import java.time.Duration;
@EnableCaching
@Configuration
public class RedisConfig {
@Bean
@ConditionalOnMissingBean(name = "redisTemplate")
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory);
// 配置键序列化器
RedisSerializer<String> stringSerializer = new StringRedisSerializer();
redisTemplate.setKeySerializer(stringSerializer);
redisTemplate.setHashKeySerializer(stringSerializer);
// 配置值序列化器(在构造器中指定 ObjectMapper,替代 setObjectMapper 方法)
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
objectMapper.activateDefaultTyping(
LaissezFaireSubTypeValidator.instance,
ObjectMapper.DefaultTyping.NON_FINAL,
JsonTypeInfo.As.WRAPPER_ARRAY
);
// 关键修改:通过构造器传入 ObjectMapper,而非使用 setObjectMapper()
Jackson2JsonRedisSerializer<Object> jacksonSerializer = new Jackson2JsonRedisSerializer<>(objectMapper, Object.class);
redisTemplate.setValueSerializer(jacksonSerializer);
redisTemplate.setHashValueSerializer(jacksonSerializer);
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
@Bean
@ConditionalOnMissingBean(CacheManager.class)
public CacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) {
// 配置值序列化器(同样通过构造器传入 ObjectMapper)
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
objectMapper.activateDefaultTyping(
LaissezFaireSubTypeValidator.instance,
ObjectMapper.DefaultTyping.NON_FINAL,
JsonTypeInfo.As.WRAPPER_ARRAY
);
Jackson2JsonRedisSerializer<Object> jacksonSerializer = new Jackson2JsonRedisSerializer<>(objectMapper, Object.class);
// 缓存配置
RedisCacheConfiguration cacheConfig = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofSeconds(600))
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jacksonSerializer))
.disableCachingNullValues();
return RedisCacheManager.builder(redisConnectionFactory)
.cacheDefaults(cacheConfig)
.build();
}
}
3.application.yaml配置
server:
port: 8080
servlet:
context-path: /api
spring:
application:
name: distributedLock
data:
redis:
host: localhost
port: 6379
# springdoc-openapi项目配置
springdoc:
swagger-ui:
path: /swagger-ui.html
tags-sorter: alpha
operations-sorter: alpha
api-docs:
path: /v3/api-docs
group-configs:
- group: 'default'
paths-to-match: '/**'
packages-to-scan: com.llp.llpdistributedlock
# knife4j增强配置
knife4j:
enable: true
setting:
language: zh_cn
3.代码实现
1.自定义注解
import com.llp.llpdistributedlock.keyresolver.DistributedLockKeyResolver;
import com.llp.llpdistributedlock.keyresolver.impl.DefaultDistributedLockKeyResolver;
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;
/**
* 分布式锁注解
* 基于Redis实现的分布式锁,支持自动加锁和释放锁
* 使用Lua脚本保证原子性操作
*
* @author llp
* @date 2025-09-25
*/
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface DistributedLock {
/**
* 锁的key,支持SpEL表达式
* 例如:
* - "'product:' + #productId"
* - "user:operation:' + #userId"
* - "'complex:' + #request.userId + '_' + #request.resourceType + '_' + #request.resourceId"
*/
String key();
/**
* 锁key的前缀,默认为"distributed:lock"
*/
String prefix() default "distributed:lock";
/**
* 等待获取锁的时间,默认3秒
* 超过这个时间还没获取到锁就放弃
*/
long waitTime() default 3L;
/**
* 锁的租期时间,默认30秒
* 到期后自动释放锁,防止死锁
*/
long leaseTime() default 30L;
/**
* 时间单位,默认为秒
*/
TimeUnit timeUnit() default TimeUnit.SECONDS;
/**
* 获取锁失败时的提示信息
*/
String message() default "获取分布式锁失败,请稍后重试";
/**
* 是否自动释放锁,默认为true
* 如果为false,需要手动调用unlock方法释放锁
*/
boolean autoUnlock() default true;
/**
* 使用的Key解析器
*/
Class<? extends DistributedLockKeyResolver> keyResolver() default DefaultDistributedLockKeyResolver.class;
/**
* 锁的重试策略
* FAIL_FAST: 快速失败,不重试
* RETRY: 重试获取锁
*/
RetryStrategy retryStrategy() default RetryStrategy.FAIL_FAST;
/**
* 重试次数,当retryStrategy为RETRY时生效,默认3次
*/
int retryCount() default 3;
/**
* 重试间隔时间(毫秒),默认100ms
*/
long retryInterval() default 100L;
/**
* 重试策略枚举
*/
enum RetryStrategy {
/**
* 快速失败,不重试
*/
FAIL_FAST,
/**
* 重试获取锁
*/
RETRY
}
}
2.AOP环绕
import com.llp.llpdistributedlock.annotation.DistributedLock;
import com.llp.llpdistributedlock.aspect.distributedLock.DistributedLockService;
import com.llp.llpdistributedlock.keyresolver.DistributedLockKeyResolver;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
/**
* 分布式锁切面
* <p>
* 拦截声明了 {@link DistributedLock} 注解的方法,实现分布式锁功能
* <p>
* 功能特性:
* 1. 自动获取和释放锁
* 2. 支持自定义锁key解析器
* 3. 支持重试策略
* 4. 异常情况下自动释放锁
* 5. 支持可配置的超时时间
*
* @author llp
* @date 2025-09-25
*/
@Component
@Aspect
@Slf4j
public class DistributedLockAspect {
private final DistributedLockService distributedLockService;
/**
* DistributedLockKeyResolver 集合
*/
private final Map<Class<? extends DistributedLockKeyResolver>, DistributedLockKeyResolver> keyResolvers;
@Autowired
public DistributedLockAspect(DistributedLockService distributedLockService,
List<DistributedLockKeyResolver> keyResolvers) {
this.distributedLockService = distributedLockService;
// 构建key解析器映射
Map<Class<? extends DistributedLockKeyResolver>, DistributedLockKeyResolver> resolverMap = new HashMap<>();
for (DistributedLockKeyResolver keyResolver : keyResolvers) {
resolverMap.put(keyResolver.getClass(), keyResolver);
}
this.keyResolvers = resolverMap;
}
/**
* 环绕通知:处理分布式锁逻辑
*
* @param joinPoint 切点
* @param distributedLock 分布式锁注解
* @return 方法执行结果
* @throws Throwable 可能抛出的异常
*/
@Around("@annotation(distributedLock)")
public Object around(ProceedingJoinPoint joinPoint, DistributedLock distributedLock) throws Throwable {
// 解析锁的key
String lockKey = resolveLockKey(joinPoint, distributedLock);
if (!StringUtils.hasText(lockKey)) {
throw new RuntimeException("分布式锁key不能为空");
}
// 完整的锁key(加上前缀)
String fullLockKey = distributedLock.prefix() + ":" + lockKey;
boolean lockAcquired = false;
int retryCount = 0;
int maxRetries = distributedLock.retryStrategy() == DistributedLock.RetryStrategy.RETRY ?
distributedLock.retryCount() : 0;
String lockValue = generateLockValue();
try {
// 尝试获取锁(支持重试)
do {
lockAcquired = distributedLockService.tryLock(
fullLockKey,
lockValue,
distributedLock.waitTime(),
distributedLock.leaseTime(),
distributedLock.timeUnit()
);
if (lockAcquired) {
log.info("获取分布式锁成功: key={}, retryCount={}", fullLockKey, retryCount);
break;
}
// 重试策略处理
if (distributedLock.retryStrategy() == DistributedLock.RetryStrategy.RETRY && retryCount < maxRetries) {
retryCount++;
log.info("获取分布式锁失败,准备重试: key={}, retryCount={}/{}", fullLockKey, retryCount, maxRetries);
// 重试间隔
if (distributedLock.retryInterval() > 0) {
Thread.sleep(distributedLock.retryInterval());
}
} else {
break;
}
} while (retryCount <= maxRetries);
// 获取锁失败
if (!lockAcquired) {
String errorMsg = String.format("获取分布式锁失败: key=%s, 重试次数=%d, 消息=%s",
fullLockKey, retryCount, distributedLock.message());
log.error(errorMsg);
throw new RuntimeException(distributedLock.message());
}
// 执行目标方法
log.info("开始执行业务方法: {}", joinPoint.getSignature().toShortString());
Object result = joinPoint.proceed();
log.info("业务方法执行完成: {}", joinPoint.getSignature().toShortString());
return result;
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
log.error("获取分布式锁时被中断: key={}", fullLockKey, e);
throw new RuntimeException("获取分布式锁时被中断", e);
} catch (RuntimeException e) {
// 分布式锁异常直接抛出
throw e;
} catch (Throwable e) {
log.error("执行业务方法时发生异常: key={}, method={}", fullLockKey, joinPoint.getSignature().toShortString(), e);
throw e;
} finally {
// 释放锁
if (lockAcquired && distributedLock.autoUnlock()) {
try {
boolean unlocked = distributedLockService.unlock(fullLockKey, lockValue);
if (unlocked) {
log.info("释放分布式锁成功: key={}", fullLockKey);
} else {
log.warn("释放分布式锁失败,锁可能已过期: key={}", fullLockKey);
}
} catch (Exception e) {
log.error("释放分布式锁时发生异常: key={}", fullLockKey, e);
}
}
}
}
/**
* 解析锁的key
*
* @param joinPoint 切点
* @param distributedLock 分布式锁注解
* @return 解析后的key
*/
private String resolveLockKey(ProceedingJoinPoint joinPoint, DistributedLock distributedLock) {
try {
// 获取key解析器
DistributedLockKeyResolver keyResolver = keyResolvers.get(distributedLock.keyResolver());
if (keyResolver == null) {
throw new RuntimeException("找不到对应的 DistributedLockKeyResolver: " + distributedLock.keyResolver().getName());
}
// 解析key
String resolvedKey = keyResolver.resolver(joinPoint, distributedLock);
if (!StringUtils.hasText(resolvedKey)) {
throw new RuntimeException("解析的锁key不能为空");
}
return resolvedKey;
} catch (Exception e) {
log.error("解析分布式锁key失败: annotation={}", distributedLock, e);
throw new RuntimeException("解析分布式锁key失败: " + e.getMessage(), e);
}
}
/**
* 生成锁值(UUID)
*
* @return 锁值
*/
private String generateLockValue() {
return UUID.randomUUID().toString();
}
}
3.分布式锁Key解析器接口
import com.llp.llpdistributedlock.annotation.DistributedLock;
import org.aspectj.lang.JoinPoint;
/**
* 分布式锁Key解析器接口
*
* 用于解析分布式锁的key,支持SpEL表达式
*
* @author llp
* @date 2025-09-25
*/
public interface DistributedLockKeyResolver {
/**
* 解析锁的key
*
* @param joinPoint 切点信息
* @param distributedLock 分布式锁注解
* @return 解析后的key
*/
String resolver(JoinPoint joinPoint, DistributedLock distributedLock);
}
import com.llp.llpdistributedlock.annotation.DistributedLock;
import com.llp.llpdistributedlock.keyresolver.DistributedLockKeyResolver;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.core.DefaultParameterNameDiscoverer;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.Expression;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
/**
* 默认分布式锁Key解析器
*
* 支持SpEL表达式解析,可以使用方法参数、对象属性等
*
* 支持的表达式示例:
* - "#{#userId}" - 直接使用参数
* - "#{#user.id}" - 使用对象属性
* - "#{T(System).currentTimeMillis()}" - 使用静态方法
*
* @author llp
* @date 2025-09-25
*/
@Component
@Slf4j
public class DefaultDistributedLockKeyResolver implements DistributedLockKeyResolver {
/**
* SpEL表达式解析器
*/
private final ExpressionParser parser = new SpelExpressionParser();
/**
* 参数名发现器
*/
private final DefaultParameterNameDiscoverer nameDiscoverer = new DefaultParameterNameDiscoverer();
@Override
public String resolver(JoinPoint joinPoint, DistributedLock distributedLock) {
String key = distributedLock.key();
// 如果key不包含SpEL表达式,直接返回
if (!key.contains("#")) {
return key;
}
try {
// 获取方法信息
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
Method method = methodSignature.getMethod();
// 获取参数名和参数值
String[] parameterNames = nameDiscoverer.getParameterNames(method);
Object[] args = joinPoint.getArgs();
// 创建SpEL上下文
EvaluationContext context = new StandardEvaluationContext();
// 设置参数到上下文中
if (parameterNames != null && args != null) {
for (int i = 0; i < parameterNames.length; i++) {
context.setVariable(parameterNames[i], args[i]);
}
}
// 解析表达式
Expression expression = parser.parseExpression(key);
Object value = expression.getValue(context);
return value != null ? value.toString() : "";
} catch (Exception e) {
log.error("解析分布式锁key失败,key: {}, error: {}", key, e.getMessage(), e);
// 如果解析失败,返回方法名+参数值的组合作为fallback
return generateFallbackKey(joinPoint);
}
}
/**
* 生成备用key
*
* @param joinPoint 切点信息
* @return 备用key
*/
private String generateFallbackKey(JoinPoint joinPoint) {
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
String className = methodSignature.getDeclaringTypeName();
String methodName = methodSignature.getName();
StringBuilder keyBuilder = new StringBuilder();
keyBuilder.append(className).append(".").append(methodName);
Object[] args = joinPoint.getArgs();
if (args != null && args.length > 0) {
keyBuilder.append(":");
for (int i = 0; i < args.length; i++) {
if (i > 0) {
keyBuilder.append(",");
}
keyBuilder.append(args[i] != null ? args[i].toString() : "null");
}
}
return keyBuilder.toString();
}
}
4.分布式锁接口
/**
* 分布式锁服务接口
*
* 基于Redis实现的分布式锁服务,支持自动过期、可重入等特性
*
* @author llp
* @date 2025-09-25
*/
public interface DistributedLockService {
/**
* 尝试获取锁(不等待)
*
* @param key 锁的key
* @param leaseTime 锁的租期时间
* @param timeUnit 时间单位
* @return 是否获取成功
*/
boolean tryLock(String key, String value, long leaseTime, TimeUnit timeUnit);
/**
* 尝试获取锁(等待指定时间)
*
* @param key 锁的key
* @param waitTime 等待时间
* @param leaseTime 锁的租期时间
* @param timeUnit 时间单位
* @return 是否获取成功
*/
boolean tryLock(String key,String value, long waitTime, long leaseTime, TimeUnit timeUnit);
/**
* 释放锁
*
* @param key 锁的key
* @return 是否释放成功
*/
boolean unlock(String key,String lockValue);
/**
* 获取锁的剩余过期时间
*
* @param key 锁的key
* @param timeUnit 时间单位
* @return 剩余过期时间,-1表示永不过期,-2表示key不存在
*/
long getExpireTime(String key, TimeUnit timeUnit);
}
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Service;
import java.util.Collections;
import java.util.concurrent.TimeUnit;
/**
* 分布式锁服务实现
* 基于Redis和Lua脚本实现的分布式锁,特性:
* 1. 原子性:使用Lua脚本保证获取锁和设置过期时间的原子性
* 2. 防误删:使用UUID作为锁值,只能删除自己持有的锁
* 3. 自动过期:支持设置锁的租期,防止死锁
* 4. 可重入性:通过线程本地存储实现简单的可重入
*
* @author llp
* @date 2025-09-25
*/
@Service
@Slf4j
@AllArgsConstructor
public class DistributedLockServiceImpl implements DistributedLockService {
private final StringRedisTemplate stringRedisTemplate;
/**
* 获取锁的Lua脚本
* 如果key不存在,则设置key和过期时间;如果key存在且值相同(可重入),则续期
*/
private static final String LOCK_LUA_SCRIPT =
"if redis.call('get', KEYS[1]) == ARGV[1] then " +
" return redis.call('expire', KEYS[1], ARGV[2]) " +
"else " +
" return redis.call('set', KEYS[1], ARGV[1], 'EX', ARGV[2], 'NX') " +
"end";
/**
* 释放锁的Lua脚本
* 只有锁的值与传入的值相同时才删除,防止误删其他线程的锁
*/
private static final String UNLOCK_LUA_SCRIPT =
"if redis.call('get', KEYS[1]) == ARGV[1] then " +
" return redis.call('del', KEYS[1]) " +
"else " +
" return 0 " +
"end";
@Override
public boolean tryLock(String key, String value, long leaseTime, TimeUnit timeUnit) {
return tryLock(key, value, 0, leaseTime, timeUnit);
}
@Override
public boolean tryLock(String key, String lockValue, long waitTime, long leaseTime, TimeUnit timeUnit) {
// 转换为秒
long leaseSeconds = timeUnit.toSeconds(leaseTime);
long waitMillis = timeUnit.toMillis(waitTime);
long startTime = System.currentTimeMillis();
try {
do {
// 尝试获取锁
if (doTryLock(key, lockValue, leaseSeconds)) {
log.info("获取分布式锁成功: key={}, value={}, leaseTime={}s", key, lockValue, leaseSeconds);
return true;
}
// 如果需要等待,则休眠一段时间后重试
if (waitMillis > 0) {
long elapsed = System.currentTimeMillis() - startTime;
if (elapsed >= waitMillis) {
break;
}
// 计算休眠时间,最多休眠100ms
long sleepTime = Math.min(100, waitMillis - elapsed);
Thread.sleep(sleepTime);
}
} while (waitMillis > 0);
log.info("获取分布式锁失败: key={}, waitTime={}ms", key, waitMillis);
return false;
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
log.error("获取分布式锁被中断: key={}", key, e);
return false;
} catch (Exception e) {
log.error("获取分布式锁异常: key={}", key, e);
return false;
}
}
@Override
public boolean unlock(String key,String lockValue) {
try {
// 执行释放锁的Lua脚本
DefaultRedisScript<Long> script = new DefaultRedisScript<>();
script.setScriptText(UNLOCK_LUA_SCRIPT);
script.setResultType(Long.class);
Long result = stringRedisTemplate.execute(script, Collections.singletonList(key),lockValue);
return result == 1;
} catch (Exception e) {
log.error("释放分布式锁异常: key={}", key, e);
return false;
}
}
@Override
public long getExpireTime(String key, TimeUnit timeUnit) {
try {
long expireSeconds = stringRedisTemplate.getExpire(key, TimeUnit.SECONDS);
return timeUnit.convert(expireSeconds, TimeUnit.SECONDS);
} catch (Exception e) {
log.error("获取锁过期时间异常: key={}", key, e);
return -2;
}
}
/**
* 执行获取锁操作
*
* @param key 锁key
* @param lockValue 锁值
* @param leaseSeconds 租期(秒)
* @return 是否获取成功
*/
private boolean doTryLock(String key, String lockValue, long leaseSeconds) {
DefaultRedisScript<String> script = new DefaultRedisScript<>();
script.setScriptText(LOCK_LUA_SCRIPT);
script.setResultType(String.class);
String result = stringRedisTemplate.execute(script, Collections.singletonList(key), lockValue, String.valueOf(leaseSeconds));
// 对于SET命令,成功返回"OK";对于EXPIRE命令,成功返回1
return "OK".equals(result) || "1".equals(result);
}
}
4.编写测试
import com.llp.llpdistributedlock.annotation.DistributedLock;
import com.llp.llpdistributedlock.model.Result;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.concurrent.TimeUnit;
/**
* 分布式锁测试
*/
@RestController
@RequestMapping("/test/distributed-lock")
@Tag(name = "分布式锁测试", description = "分布式锁功能测试接口")
@Slf4j
public class DistributedLockTestController {
@Autowired
private StringRedisTemplate stringRedisTemplate;
/**
* 测试基础分布式锁功能
*
* 这个接口演示了最基本的分布式锁使用方式
*/
@GetMapping("/basic/{productId}")
@Operation(summary = "基础分布式锁测试", description = "测试基础的分布式锁功能,锁定特定商品的操作")
@DistributedLock(
key = "'product:' + #productId",
waitTime = 3,
leaseTime = 10,
timeUnit = TimeUnit.SECONDS,
message = "商品操作繁忙,请稍后重试"
)
public Result<String> testBasicLock(@Parameter(description = "商品ID") @PathVariable Long productId) {
log.info("开始执行商品{}的业务逻辑", productId);
try {
// 模拟业务处理时间
Thread.sleep(5000);
// 模拟库存操作
String key = "stock:product:" + productId;
String currentStock = stringRedisTemplate.opsForValue().get(key);
int stock = currentStock != null ? Integer.parseInt(currentStock) : 100;
if (stock > 0) {
stringRedisTemplate.opsForValue().set(key, String.valueOf(stock - 1));
log.info("商品{}库存扣减成功,剩余库存:{}", productId, stock - 1);
return Result.success("库存扣减成功,剩余库存:" + (stock - 1));
} else {
log.warn("商品{}库存不足", productId);
return Result.failed("库存不足");
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
log.error("业务处理被中断", e);
return Result.failed("处理被中断");
}
}
/**
* 测试用户级别的分布式锁
*
* 演示如何基于用户ID进行锁定
*/
@PostMapping("/user/{userId}/operation")
@Operation(summary = "用户级分布式锁测试", description = "测试基于用户ID的分布式锁")
@DistributedLock(
key = "'user:operation:' + #userId",
prefix = "business:lock",
waitTime = 1,
leaseTime = 15,
retryStrategy = DistributedLock.RetryStrategy.RETRY,
retryCount = 1,
timeUnit = TimeUnit.SECONDS,
retryInterval = 500,
message = "用户操作冲突,请稍后重试"
)
public Result<String> testUserLock(@Parameter(description = "用户ID") @PathVariable Long userId,
@Parameter(description = "操作类型") @RequestParam String operation) {
log.info("用户{}执行{}操作", userId, operation);
try {
// 模拟复杂的业务处理
Thread.sleep(5000);
// 记录操作日志
String logKey = "user:log:" + userId;
String currentTime = String.valueOf(System.currentTimeMillis());
stringRedisTemplate.opsForValue().set(logKey, operation + ":" + currentTime, 1, TimeUnit.HOURS);
log.info("用户{}的{}操作执行完成", userId, operation);
return Result.success("操作执行成功");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
log.error("用户操作被中断", e);
return Result.failed("操作被中断");
}
}
/**
* 测试复杂参数的分布式锁
*
* 演示如何使用对象属性作为锁的key
*/
@PostMapping("/complex")
@Operation(summary = "复杂参数分布式锁测试", description = "测试使用对象属性作为锁key的分布式锁")
@DistributedLock(
key = "'complex:' + #request.userId + '_' + #request.resourceType + '_' + #request.resourceId",
waitTime = 2,
leaseTime = 20,
message = "资源访问冲突,请稍后重试"
)
public Result<String> testComplexLock(@RequestBody ComplexRequest request) {
log.info("处理复杂请求: userId={}, resourceType={}, resourceId={}",
request.getUserId(), request.getResourceType(), request.getResourceId());
try {
// 模拟复杂业务逻辑
Thread.sleep(5500);
String result = String.format("处理完成: 用户%d对%s类型的资源%d执行了操作",
request.getUserId(), request.getResourceType(), request.getResourceId());
return Result.success(result);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return Result.failed("处理被中断");
}
}
/**
* 测试不自动释放锁的场景
*
* 演示手动控制锁释放的用法
*/
@PostMapping("/manual-unlock/{taskId}")
@Operation(summary = "手动释放锁测试", description = "测试手动控制锁释放的分布式锁")
@DistributedLock(
key = "'manual:task:' + #taskId",
waitTime = 1,
leaseTime = 60,
autoUnlock = false, // 不自动释放锁
message = "任务正在处理中,请稍后重试"
)
public Result<String> testManualUnlock(@Parameter(description = "任务ID") @PathVariable String taskId) {
log.info("开始处理任务: {}", taskId);
try {
// 模拟长时间任务处理
Thread.sleep(2000);
// 在某些条件下,可能需要保持锁不释放
// 实际场景中,可以通过 DistributedLockService.unlock() 手动释放
return Result.success("任务处理完成,锁仍保持(需要手动释放)");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return Result.failed("任务处理被中断");
}
}
/**
* 复杂请求对象
*/
@Setter
@Getter
public static class ComplexRequest {
private Long userId;
private String resourceType;
private Long resourceId;
private String action;
}
}
post请求: http://127.0.0.1:8080/api/test/distributed-lock/complex
请求示例:
{
"userId": 0,
"resourceType": "123",
"resourceId": 0,
"action": "123"
}
响应示例:
{
"code": 201,
"msg": "资源访问冲突,请稍后重试",
"data": null,
"ok": false
}