分布式锁注解最佳实践

This commit is contained in:
zhongzb
2023-04-22 22:49:11 +08:00
parent 3ea4bb9953
commit 0d2ba4e70a
10 changed files with 248 additions and 29 deletions

View File

@@ -109,6 +109,11 @@
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
</dependency>
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.17.1</version>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,40 @@
package com.abin.mallchat.common.common.annotation;
import java.lang.annotation.*;
import java.util.concurrent.TimeUnit;
/**
* 分布式锁注解
*/
@Retention(RetentionPolicy.RUNTIME)//运行时生效
@Target(ElementType.METHOD)//作用在方法上
public @interface RedissonLock {
/**
* key的前缀,默认取方法全限定名,除非我们在不同方法上对同一个资源做分布式锁,就自己指定
*
* @return key的前缀
*/
String prefixKey() default "";
/**
* springEl 表达式
*
* @return 表达式
*/
String key();
/**
* 等待锁的时间,默认-1不等待直接失败,redisson默认也是-1
*
* @return 单位秒
*/
int waitTime() default -1;
/**
* 等待锁的时间单位,默认毫秒
*
* @return 单位
*/
TimeUnit unit() default TimeUnit.MILLISECONDS;
}

View File

@@ -6,6 +6,7 @@ import com.abin.mallchat.common.common.exception.BusinessException;
import com.abin.mallchat.common.common.exception.CommonErrorEnum;
import com.abin.mallchat.common.common.utils.RedisUtils;
import com.abin.mallchat.common.common.utils.RequestHolder;
import com.abin.mallchat.common.common.utils.SpElUtils;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
@@ -33,8 +34,6 @@ import java.util.*;
public class FrequencyControlAspect {
@Autowired
private RedisUtils redisUtils;
private final ExpressionParser parser = new SpelExpressionParser();
private final DefaultParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer();
@Around("@annotation(com.abin.mallchat.common.common.annotation.FrequencyControl)||@annotation(com.abin.mallchat.common.common.annotation.FrequencyControlContainer)")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
@@ -43,11 +42,11 @@ public class FrequencyControlAspect {
Map<String, FrequencyControl> keyMap = new HashMap<>();
for (int i = 0; i < annotationsByType.length; i++) {
FrequencyControl frequencyControl = annotationsByType[i];
String prefix = StrUtil.isBlank(frequencyControl.prefixKey()) ? method.toGenericString() + ":index:" + i : frequencyControl.prefixKey();//默认方法限定名+注解排名(可能多个)
String prefix = StrUtil.isBlank(frequencyControl.prefixKey()) ? SpElUtils.getMethodKey(method) + ":index:" + i : frequencyControl.prefixKey();//默认方法限定名+注解排名(可能多个)
String key = "";
switch (frequencyControl.target()) {
case EL:
key = parseSpEl(method, joinPoint.getArgs(), frequencyControl.spEl());
key = SpElUtils.parseSpEl(method, joinPoint.getArgs(), frequencyControl.spEl());
break;
case IP:
key = RequestHolder.get().getIp();
@@ -79,13 +78,5 @@ public class FrequencyControlAspect {
}
}
private String parseSpEl(Method method, Object[] args, String spEl) {
String[] params = parameterNameDiscoverer.getParameterNames(method);//解析参数名
EvaluationContext context = new StandardEvaluationContext();//el解析需要的上下文对象
for (int i = 0; i < params.length; i++) {
context.setVariable(params[i], args[i]);//所有参数都作为原材料扔进去
}
Expression expression = parser.parseExpression(spEl);
return expression.getValue(context, String.class);
}
}

View File

@@ -0,0 +1,50 @@
package com.abin.mallchat.common.common.aspect;
import cn.hutool.core.util.StrUtil;
import com.abin.mallchat.common.common.annotation.FrequencyControl;
import com.abin.mallchat.common.common.annotation.RedissonLock;
import com.abin.mallchat.common.common.exception.BusinessException;
import com.abin.mallchat.common.common.exception.CommonErrorEnum;
import com.abin.mallchat.common.common.service.LockService;
import com.abin.mallchat.common.common.utils.RedisUtils;
import com.abin.mallchat.common.common.utils.RequestHolder;
import com.abin.mallchat.common.common.utils.SpElUtils;
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.reflect.MethodSignature;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
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;
import java.util.*;
/**
* Description: 分布式锁切面
* Author: <a href="https://github.com/zongzibinbin">abin</a>
* Date: 2023-04-20
*/
@Slf4j
@Aspect
@Component
public class RedissonLockAspect {
@Autowired
private LockService lockService;
@Around("@annotation(com.abin.mallchat.common.common.annotation.RedissonLock)")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
RedissonLock redissonLock = method.getAnnotation(RedissonLock.class);
String prefix = StrUtil.isBlank(redissonLock.prefixKey()) ? SpElUtils.getMethodKey(method) : redissonLock.prefixKey();//默认方法限定名+注解排名(可能多个)
String key = SpElUtils.parseSpEl(method, joinPoint.getArgs(), redissonLock.key());
return lockService.executeWithLockThrows(prefix + ":" + key, redissonLock.waitTime(), redissonLock.unit(), joinPoint::proceed);
}
}

View File

@@ -0,0 +1,30 @@
package com.abin.mallchat.common.common.config;
import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.data.redis.RedisProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* Description:
* Author: <a href="https://github.com/zongzibinbin">abin</a>
* Date: 2023-04-22
*/
@Configuration
public class RedissonConfig {
@Autowired
private RedisProperties redisProperties;
@Bean
public RedissonClient redissonClient() {
Config config = new Config();
config.useSingleServer()
.setAddress("redis://" + redisProperties.getHost() + ":" + redisProperties.getPort())
.setPassword(redisProperties.getPassword())
.setDatabase(redisProperties.getDatabase());
return Redisson.create(config);
}
}

View File

@@ -15,6 +15,7 @@ public enum CommonErrorEnum implements ErrorEnum {
SYSTEM_ERROR(-1, "系统出小差了,请稍后再试哦~~"),
PARAM_VALID(-2, "参数校验失败"),
FREQUENCY_LIMIT(-3, "请求太频繁了,请稍后再试哦~~"),
LOCK_LIMIT(-4, "请求太频繁了,请稍后再试哦~~"),
;
private Integer code;
private String msg;

View File

@@ -0,0 +1,51 @@
package com.abin.mallchat.common.common.service;
import com.abin.mallchat.common.common.exception.BusinessException;
import com.abin.mallchat.common.common.exception.CommonErrorEnum;
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.Service;
import java.util.concurrent.Callable;
import java.util.concurrent.TimeUnit;
import java.util.function.Supplier;
@Service
@Slf4j
public class LockService {
@Autowired
private RedissonClient redissonClient;
public <T> T executeWithLockThrows(String key, int waitTime, TimeUnit unit, SupplierThrow<T> supplier) throws Throwable {
RLock lock = redissonClient.getLock(key);
boolean lockSuccess = lock.tryLock(waitTime, unit);
if (!lockSuccess) {
throw new BusinessException(CommonErrorEnum.LOCK_LIMIT);
}
try {
return supplier.get();//执行锁内的代码逻辑
} finally {
lock.unlock();
}
}
@SneakyThrows
public <T> T executeWithLock(String key, int waitTime, TimeUnit unit, Supplier<T> supplier) {
return executeWithLockThrows(key, waitTime, unit, supplier::get);
}
@FunctionalInterface
public interface SupplierThrow<T> {
/**
* Gets a result.
*
* @return a result
*/
T get() throws Throwable;
}
}

View File

@@ -0,0 +1,35 @@
package com.abin.mallchat.common.common.utils;
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;
/**
* Description: spring el表达式解析
* Author: <a href="https://github.com/zongzibinbin">abin</a>
* Date: 2023-04-22
*/
public class SpElUtils {
private static final ExpressionParser parser = new SpelExpressionParser();
private static final DefaultParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer();
public static String parseSpEl(Method method, Object[] args, String spEl) {
String[] params = parameterNameDiscoverer.getParameterNames(method);//解析参数名
EvaluationContext context = new StandardEvaluationContext();//el解析需要的上下文对象
for (int i = 0; i < params.length; i++) {
context.setVariable(params[i], args[i]);//所有参数都作为原材料扔进去
}
Expression expression = parser.parseExpression(spEl);
return expression.getValue(context, String.class);
}
public static String getMethodKey(Method method){
return method.getDeclaringClass()+"#"+method.getName();
}
}

View File

@@ -1,5 +1,6 @@
package com.abin.mallchat.common.user.service.impl;
import com.abin.mallchat.common.common.annotation.RedissonLock;
import com.abin.mallchat.common.common.domain.enums.IdempotentEnum;
import com.abin.mallchat.common.common.domain.enums.YesOrNoEnum;
import com.abin.mallchat.common.user.dao.ItemConfigDao;
@@ -32,6 +33,7 @@ public class UserBackpackServiceImpl implements IUserBackpackService {
private ItemCache itemCache;
@Override
@RedissonLock(key = "#uid")
public void acquireItem(Long uid, Long itemId, IdempotentEnum idempotentEnum, String businessId) {//todo 分布式锁
String idempotent = getIdempotent(itemId, idempotentEnum, businessId);
UserBackpack userBackpack = userBackpackDao.getByIdp(idempotent);