mirror of
https://github.com/zongzibinbin/MallChat.git
synced 2026-03-13 21:53:41 +08:00
分布式锁注解最佳实践
This commit is contained in:
@@ -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>
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -1,10 +1,19 @@
|
||||
package com.abin.mallchat.custom.common.config;
|
||||
|
||||
import org.springframework.boot.actuate.autoconfigure.endpoint.web.CorsEndpointProperties;
|
||||
import org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointProperties;
|
||||
import org.springframework.boot.actuate.autoconfigure.web.server.ManagementPortType;
|
||||
import org.springframework.boot.actuate.endpoint.ExposableEndpoint;
|
||||
import org.springframework.boot.actuate.endpoint.web.*;
|
||||
import org.springframework.boot.actuate.endpoint.web.annotation.ControllerEndpointsSupplier;
|
||||
import org.springframework.boot.actuate.endpoint.web.annotation.ServletEndpointsSupplier;
|
||||
import org.springframework.boot.actuate.endpoint.web.servlet.WebMvcEndpointHandlerMapping;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.core.env.Environment;
|
||||
import org.springframework.core.env.Profiles;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
|
||||
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
|
||||
@@ -17,6 +26,10 @@ import springfox.documentation.spi.DocumentationType;
|
||||
import springfox.documentation.spring.web.plugins.Docket;
|
||||
import springfox.documentation.swagger2.annotations.EnableSwagger2WebMvc;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Description:
|
||||
* Author: <a href="https://github.com/zongzibinbin">abin</a>
|
||||
@@ -47,21 +60,22 @@ public class SwaggerConfig {
|
||||
.build();
|
||||
}
|
||||
/**
|
||||
* swagger 配置
|
||||
* @param environment 环境
|
||||
*/
|
||||
// @Bean
|
||||
// public Docket docket(Environment environment) {
|
||||
//
|
||||
// // 设置环境范围
|
||||
// Profiles profiles = Profiles.of("dev","test");
|
||||
// // 如果在该环境返回内则返回:true,反之返回 false
|
||||
// boolean flag = environment.acceptsProfiles(profiles);
|
||||
//
|
||||
// // 创建一个 swagger 的 bean 实例
|
||||
// return new Docket(DocumentationType.SWAGGER_2)
|
||||
// .enable(flag) // 是否开启 swagger:true -> 开启,false -> 关闭
|
||||
// ;
|
||||
// }
|
||||
* 增加如下配置可解决Spring Boot 6.x 与Swagger 3.0.0 不兼容问题
|
||||
**/
|
||||
@Bean
|
||||
public WebMvcEndpointHandlerMapping webEndpointServletHandlerMapping(WebEndpointsSupplier webEndpointsSupplier, ServletEndpointsSupplier servletEndpointsSupplier, ControllerEndpointsSupplier controllerEndpointsSupplier, EndpointMediaTypes endpointMediaTypes, CorsEndpointProperties corsProperties, WebEndpointProperties webEndpointProperties, Environment environment) {
|
||||
List<ExposableEndpoint<?>> allEndpoints = new ArrayList();
|
||||
Collection<ExposableWebEndpoint> webEndpoints = webEndpointsSupplier.getEndpoints();
|
||||
allEndpoints.addAll(webEndpoints);
|
||||
allEndpoints.addAll(servletEndpointsSupplier.getEndpoints());
|
||||
allEndpoints.addAll(controllerEndpointsSupplier.getEndpoints());
|
||||
String basePath = webEndpointProperties.getBasePath();
|
||||
EndpointMapping endpointMapping = new EndpointMapping(basePath);
|
||||
boolean shouldRegisterLinksMapping = this.shouldRegisterLinksMapping(webEndpointProperties, environment, basePath);
|
||||
return new WebMvcEndpointHandlerMapping(endpointMapping, webEndpoints, endpointMediaTypes, corsProperties.toCorsConfiguration(), new EndpointLinksResolver(allEndpoints, basePath), shouldRegisterLinksMapping, null);
|
||||
}
|
||||
private boolean shouldRegisterLinksMapping(WebEndpointProperties webEndpointProperties, Environment environment, String basePath) {
|
||||
return webEndpointProperties.getDiscovery().isEnabled() && (StringUtils.hasText(basePath) || ManagementPortType.get(environment).equals(ManagementPortType.DIFFERENT));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user