1、主要为了给自己项目中的代码留一个备份
2、给大家也分享一下自己写的小工具,同时也希望大家找到其中需要优化的地方从而让我的代码以及所在公司的业务有所优化。
业界主流的redis与db数据一致性的解决方式目前应该就是 改后删除策略,虽然这个策略也会有问题,但是用于中小型项目应该是够用了。有些blog讲解过为什么改后删除方案比改前删除方案要优一些,大家有兴趣可以去看看。
首先工具分两个部分,由于使用AOP机制,首先有自定义注解,其次就是一个切面控制器Aspect,具体直接上代码:
RedisToolAnnotation.java
package top.swzhao.project.workflow.common.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* @Discreption <> 处理rediskey缓存的注解
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Inherited
public @interface RedisToolAnnotation {
/**
* 处理的keys列表
*
* @return
*/
String[] keys() default "";
/**
* 对key的操作:默认为删除
*
* @return
*/
String opt() default "del";
/**
* dbIndex
*
* @return
*/
int dbIndex() default 0;
}
RedisToolAspect.java
package top.swzhao.project.workflow.common.annotation.aspect;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import top.swzhao.project.workflow.common.annotation.RedisToolAnnotation;
import top.swzhao.project.workflow.common.utils.RedisUtil;
import java.util.Map;
/**
* @Discreption <> 处理rediskey的前面
* {@link top.swzhao.project.workflow.common.annotation.RedisToolAnnotation}
*/
@Aspect
@Component
@Slf4j
public class RedisToolAspect {
/**
* 被注解的方法可以通过设置threadlocal的方式来进行额外的操作
*/
public static final ThreadLocal<Map<String, String>> dealKV = new ThreadLocal<>();
@AfterReturning("@annotation(RedisToolAnnotation)")
public void doAfter(JoinPoint joinpoint) {
try {
MethodSignature signature = (MethodSignature) joinpoint.getSignature();
RedisToolAnnotation annotation = signature.getMethod().getAnnotation(RedisToolAnnotation.class);
String[] keys = annotation.keys();
String opt = annotation.opt();
int dbIndex = annotation.dbIndex();
// 仅有删除功能,后面可自行拓展
switch (opt) {
case "del" :
RedisUtil.delList(keys, dbIndex);
default:
break;
}
}catch (Exception e) {
log.error(getClass().getSimpleName().concat(".").concat(Thread.currentThread().getStackTrace()[0].getClassName()).concat(" 异常, 原因:{}"), e.getMessage(), e);
}
}
}
DistributeLock.java
package top.swzhao.project.workflow.common.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* @Discreption <> 分布式锁注解
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface DistributeLock {
/**
* 临界区标识,支持SPEL表达式
* @return
*/
String key() default "";
/**
* 锁过期时间(s)
* @return
*/
int timeout() default 10;
/**
* 是否是互斥场景
* true:取号器场景,等待自旋
* false:互斥场景,执行后不再执行
* @return
*/
boolean loopWithLockFail() default false;
/**
* 自旋尝试次数,默认一百次
* 一次等待100ms
* @return
*/
int tryTime() default 100;
}
DistributeLockAspect.java
package top.swzhao.project.workflow.common.annotation.aspect;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
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.core.DefaultParameterNameDiscoverer;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.Expression;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.stereotype.Component;
import top.swzhao.project.workflow.common.annotation.DistributeLock;
import top.swzhao.project.workflow.common.utils.RedisUtil;
import java.util.Objects;
import java.util.UUID;
/**
* @Discreption <>
*/
@Aspect
@Component
@Slf4j
public class DistributeLockAspect {
/**
* 当前线程标识UUID
*/
private static final ThreadLocal<String> uniqueIdThreadLocal = new ThreadLocal<>();
@Pointcut(value = "@annotation(top.swzhao.project.workflow.common.annotation.DistributeLock)")
public void scheduleLock(){
}
@Around(value = "scheduleLock() && @annotation(lock)")
public Object around(ProceedingJoinPoint proceedingJoinPoint, DistributeLock lock) {
// 解析当前需要加锁的业务标识
String key = generateKeyBySpEL(lock.key(), proceedingJoinPoint);
// 锁过期的时间
int timeout = lock.timeout();
// 是否自旋
boolean isLoop = lock.loopWithLockFail();
// 自选尝试次数
int loopTime = lock.tryTime();
boolean setResult = false;
try {
int count = 0;
// 设置当前线程的标识
uniqueIdThreadLocal.set(UUID.randomUUID().toString());
// 先尝试一次
setResult = RedisUtil.setRedisLock(key, uniqueIdThreadLocal.get(), timeout);
if (!setResult) {
// 没成功则根据策略进行处理
if (!isLoop) {
// 互斥场景
return null;
}
// 取号器场景
while (!setResult && count++ < loopTime) {
setResult = RedisUtil.setRedisLock(key, uniqueIdThreadLocal.get(), timeout);
Thread.sleep(100);
}
// 结束后如果还是没获取到则直接异常退出
if (!setResult) {
log.warn("自旋获取锁超时,请查看lock :{} 是否未被释放", key);
throw new Exception("自旋获取锁超时");
}
}
return proceedingJoinPoint.proceed();
} catch (Exception e) {
// 这里是锁异常,正常业务异常需要提前捕获并抛出
log.error("分布式锁加锁异常:", e);
return null;
} catch (Throwable throwable) {
throwable.printStackTrace();
return null;
} finally {
// 释放分布式锁
if (setResult) {
String uuid = uniqueIdThreadLocal.get();
uniqueIdThreadLocal.remove();
// 仅释放当前线程加的锁,不要释放别人的锁
RedisUtil.luaExistKeyDel(key, uuid);
}
}
}
private SpelExpressionParser spelExpressionParser = new SpelExpressionParser();
private DefaultParameterNameDiscoverer defaultParameterNameDiscoverer = new DefaultParameterNameDiscoverer();
/**
* 将入参替换到key中的占位符
* @param key
* @param proceedingJoinPoint
* @return
*/
private String generateKeyBySpEL(String key, ProceedingJoinPoint proceedingJoinPoint) {
// 转换表达式
Expression expression = spelExpressionParser.parseExpression(key);
// 获取上下文
EvaluationContext context = new StandardEvaluationContext();
// 从当前方法中获取用户输入的参数
MethodSignature signature = (MethodSignature) proceedingJoinPoint.getSignature();
// 注解方法中的入参
Object[] args = proceedingJoinPoint.getArgs();
// 从方法中获取未被压缩过的参数名:正常来说压缩过的为arg1,arg2,这样找不到用户的真实参数名称
String[] parameterNames = defaultParameterNameDiscoverer.getParameterNames(signature.getMethod());
if (parameterNames == null || parameterNames.length == 0) {
return key;
}
// 判断是否存在spel表达式
boolean flag = false;
for (int i = 0; i < parameterNames.length; i++) {
if (StringUtils.isNotBlank(parameterNames[i]) && key.contains(parameterNames[i])) {
flag = true;
context.setVariable(parameterNames[i], args);
}
}
return !flag ? key : Objects.requireNonNull(expression.getValue(context).toString());
}
}
首先说一下删除缓存的工具,这个工具比较老,之前偷懒所以没有加spel表达式,之后分布式锁遇到了特殊业务场景所以才不得不加了spel表达式解析key
其次分布式锁这里没有续命机制其实是一个败笔,我觉得后面可以用redission来改造Aspect应该会比自己写要简单的多。