说明
实现幂等性的方法有很多种,本次仅基于redisson锁进行处理
本次开发基于自行封装的redis开发组件,有兴趣的可以看下redis组件
代码编写
pom.xml引入redisson
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.39.0</version>
</dependency>
redisson相关配置
- CusRedissonConfiguration(此次仅单体)
@Configuration
public class CusRedissonConfiguration {
private final static String REDIS_ADDRESS_PATTERN = "%s://%s:%s";
@Bean
public RedissonClient singleRedissonClient(SingleProperties singleProperties) {
Config config = new Config();
config.useSingleServer().setAddress(String.format(REDIS_ADDRESS_PATTERN,
BooleanUtils.isTrue(singleProperties.getEncryptEnabled()) ?
RedisConstants.redisProtocol.ENCRYPT_REDIS : RedisConstants.redisProtocol.REDIS,
singleProperties.getHost(), singleProperties.getPort()))
.setUsername(singleProperties.getUsername()).setPassword(singleProperties.getPassword());
return Redisson.create(config);
}
@Bean
@ConditionalOnProperty(prefix = "cus.redisson", name = "idempotent-default", havingValue = "true", matchIfMissing = true)
public Idempotent idempotent(){
return new DefaultIdempotentUtil();
}
}
- CusRedissonConfiguration
@Configuration
public class CusRedissonConfiguration {
private final static String REDIS_ADDRESS_PATTERN = "%s://%s:%s";
@Bean
public RedissonClient singleRedissonClient(SingleProperties singleProperties) {
Config config = new Config();
config.useSingleServer().setAddress(String.format(REDIS_ADDRESS_PATTERN,
BooleanUtils.isTrue(singleProperties.getEncryptEnabled()) ?
RedisConstants.redisProtocol.ENCRYPT_REDIS : RedisConstants.redisProtocol.REDIS,
singleProperties.getHost(), singleProperties.getPort()))
.setUsername(singleProperties.getUsername()).setPassword(singleProperties.getPassword());
return Redisson.create(config);
}
@Bean
@ConditionalOnProperty(prefix = "cus.redisson", name = "idempotent-default", havingValue = "true", matchIfMissing = true)
public Idempotent idempotent(){
return new DefaultIdempotentUtil();
}
}
- SingleProperties
@Component
@ConfigurationProperties(prefix = "cus.redisson.single-properties")
public class SingleProperties {
private String host = RedisConstants.defaultRedisInfo.DEFAULT_IP;
private String port = RedisConstants.defaultRedisInfo.DEFAULT_PORT;
private String username;
private String password;
private Integer database = RedisConstants.defaultRedisInfo.DEFAULT_DATABASE;
private Boolean encryptEnabled = false;
public String getHost() {
return host;
}
public void setHost(String host) {
this.host = host;
}
public String getPort() {
return port;
}
public void setPort(String port) {
this.port = port;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public Boolean getEncryptEnabled() {
return encryptEnabled;
}
public void setEncryptEnabled(Boolean encryptEnabled) {
this.encryptEnabled = encryptEnabled;
}
public Integer getDatabase() {
return database;
}
public void setDatabase(Integer database) {
this.database = database;
}
}
lock代码编写
- Lock
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Lock {
//锁键
String lockKey();
//等待时间
long waitTime() default 60L;
//自动释放时间
long leaseTime();
//时间单位
TimeUnit timeUnit() default TimeUnit.SECONDS;
//锁类型
String lockType() default "FAIR";
//校验lockKey是否存在
boolean lockKeyExistFlag() default false;
}
- LockAspect
此处为核心处理代码,如果需要检验lockKey是否是被伪造的,可以将lockKeyExistFlag设置为true,由idempotent执行相关校验逻辑
getLockKey可以视为通用方法,后续考虑放到core中
@Aspect
@Component
public class LockAspect {
private final static Logger LOGGER = LoggerFactory.getLogger(LockAspect.class);
private LockServiceFactory lockServiceFactory;
private Idempotent idempotent;
@Autowired
public void setLockServiceFactory(LockServiceFactory lockServiceFactory) {
this.lockServiceFactory = lockServiceFactory;
}
@Autowired
public void setIdempotent(Idempotent idempotent) {
this.idempotent = idempotent;
}
@Around("@annotation(lock)")
public Object around(ProceedingJoinPoint joinPoint, Lock lock) throws Throwable {
String lockKey = this.getLockKey(joinPoint, lock);
if (StringUtils.isEmpty(lockKey)) {
LOGGER.error("lockKey is empty");
return null;
}
//校验lockKey是否是被伪造的,lockKey的生成和校验逻辑可以自定义
if (lock.lockKeyExistFlag() && !idempotent.lockKeyExist(lockKey)) {
LOGGER.error("the lockKey was forged");
return null;
}
LockService lockService = lockServiceFactory.getLockServiceByType(lock.lockType());
CusLockInfo cusLockInfo = new CusLockInfo();
cusLockInfo.setLockKey(lockKey);
cusLockInfo.setWaitTime(lock.waitTime());
cusLockInfo.setLeaseTime(lock.leaseTime());
cusLockInfo.setTimeUnit(lock.timeUnit());
try {
lockService.lock(cusLockInfo);
if (!lockService.lock(cusLockInfo)) {
throw new RuntimeException("Failed to acquire lock");
}
return joinPoint.proceed();
} finally {
//开启了伪造校验,结束后需进行清除
if (lock.lockKeyExistFlag() && !idempotent.clear(lockKey)) {
LOGGER.error("Verification resource lockKey clearance failed, lockKey value is{}", lockKey);
}
lockService.unLock(cusLockInfo);
}
}
/**
* 结合el表达式解析lockKey
*
* @param joinPoint 切点
* @param lock 锁信息
* @return lockKey
*/
private String getLockKey(ProceedingJoinPoint joinPoint, Lock lock) {
// 获取方法签名
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
// 创建表达式解析器
ExpressionParser parser = new SpelExpressionParser();
// 创建评估上下文
EvaluationContext context = new StandardEvaluationContext();
// 将方法参数绑定到上下文中
String[] paramNames = signature.getParameterNames();
Object[] paramValues = joinPoint.getArgs();
for (int i = 0; i < paramNames.length; i++) {
context.setVariable(paramNames[i], paramValues[i]);
}
// 解析lockKey的值
return parser.parseExpression(lock.lockKey()).getValue(context, String.class);
}
}
lockKey相关逻辑
- Idempotent
实现类可自定义,yml中指定idempotent-default为false,自行注册Idempotent的自定义实现类即可
public interface Idempotent {
/**
* 创建唯一性lockKey
*
* @param args 构建lockKey的参数
* @return lockKey
*/
String createUniqueLockKey(String... args);
/**
* 校验lockKey是否存在(是否是伪造的)
*
* @param lockKey lockKey
* @return lockKey是否存在
*/
Boolean lockKeyExist(String lockKey);
/**
* 清除校验数据
* @param lockKey lockKey
* @return 是否清除成功
*/
Boolean clear(String lockKey);
}
- DefaultIdempotentUtil
@Component
public class DefaultIdempotentUtil implements Idempotent {
private final static Logger logger = LoggerFactory.getLogger(DefaultIdempotentUtil.class);
private final static String PREFIX = "token_%s_%s";
public final static String DEFAULT_SERVERNAME = "default";
public final static String UNIQUE_FLAG = "true";
public RedisHelper redisHelper;
@Autowired
public void setRedisHelper(RedisHelper redisHelper) {
this.redisHelper = redisHelper;
}
/**
* 获取token的接口需要防止疯狂获取导致redis暴库
*
* @param serverName 服务名
* @return token
*/
public String createUniqueTokenWithServerName(String serverName) {
serverName = Optional.ofNullable(serverName).orElse(DEFAULT_SERVERNAME);
String uuid = UUID.randomUUID().toString();
String uniqueToken = String.format(PREFIX, serverName, uuid);
logger.debug("{} generate unique_token {}", serverName, uniqueToken);
redisHelper.strSet(uniqueToken, UNIQUE_FLAG);
return uniqueToken;
}
@Override
public String createUniqueLockKey(String... args) {
// 确保至少有一个参数被提供
if (args == null || args.length == 0) {
throw new IllegalArgumentException("At least one argument is required.");
}
// 使用第一个参数作为serverName
return createUniqueTokenWithServerName(args[0]);
}
@Override
public Boolean lockKeyExist(String lockKey){
return StringUtils.isNotEmpty(redisHelper.strGet(lockKey));
}
@Override
public Boolean clear(String lockKey) {
return redisHelper.del(lockKey);
}
}
yml相关配置
spring:
redis:
host: ip
port: port
database: 1
username: root
password: root
jedis:
pool:
# 资源池中最大连接数
# 默认8,-1表示无限制;可根据服务并发redis情况及服务端的支持上限调整
max-active: ${SPRING_REDIS_POOL_MAX_ACTIVE:50}
# 资源池运行最大空闲的连接数
# 默认8,-1表示无限制;可根据服务并发redis情况及服务端的支持上限调整,一般建议和max-active保持一致,避免资源伸缩带来的开销
max-idle: ${SPRING_REDIS_POOL_MAX_IDLE:50}
# 当资源池连接用尽后,调用者的最大等待时间(单位为毫秒)
# 默认 -1 表示永不超时,设置5秒
max-wait: ${SPRING_REDIS_POOL_MAX_WAIT:5000}
cus:
redisson:
single-properties:
host: ip
port: port
username: root
password: root
idempotent-default: true
参考资料
[1].redis代码