在分布式环境中,多个服务可能同时对同一个数据进行修改操作,这时候就需要考虑如何保证数据的幂等性,避免出现数据的重复修改或者数据修改的混乱情况。其中一个实现方案是使用redis作为分布式锁,实现对数据修改的幂等性注解。具体实现步骤如下:
1.在redis中创建一个哈希表,用于存储被修改的数据的主键和修改标识(可以是一个随机数或者时间戳)的对应关系。
2.在数据修改方法上添加注解,注解中包含待修改数据的主键和修改标识,如下所示:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Idempotent {
String key(); // 待修改数据的主键
String value(); // 修改标识
}
3.在注解处理器中,首先根据注解中的主键和修改标识从redis中获取对应的值。如果值存在,说明此次修改已经执行过,直接返回结果;如果值不存在,则使用redis分布式锁对该主键进行加锁,避免其他服务同时对该数据进行修改操作。加锁成功后,再次从redis中获取修改标识的值,如果值仍然存在,则说明此次修改已经执行过,直接返回结果。否则,执行实际的数据修改操作,并将修改标识写入redis中,以便下一次判断。最后解锁该主键,释放锁资源。
4.在注解处理器中,还需要添加异常处理逻辑,当redis连接异常或者redis分布式锁加锁失败时,需要抛出异常,避免数据修改操作失败。
以上就是基于redis实现数据幂等性注解的具体步骤。需要注意的是,使用redis实现幂等性注解可能会带来一定的性能开销,需要根据具体情况进行评估和调优。
源码:
/**
* 接口幂等性注解
*/
@Target({ElementType.METHOD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Idempotent {
/**
* 时间单位,默认为秒
*/
TimeUnit timeUnit() default TimeUnit.SECONDS;
/**
* 间隔时间,默认为5秒
*/
int interval() default 5;
}
实现
@Slf4j
@Aspect
@Component
public class IdempotentAspect {
@Resource
private RedisUtil redisUtil;
@Pointcut("@annotation(com.common.business.annotation.Idempotent)")
public void idempotentPointCut() {
}
@Around("idempotentPointCut()")
public Object doAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
Method method = currentMethod(proceedingJoinPoint);
//获取到方法的注解对象
Idempotent idempotent = method.getAnnotation(Idempotent.class);
//单位 秒
long interval = 60;
if (idempotent.interval() > 0) {
interval = idempotent.timeUnit().toSeconds(idempotent.interval());
}
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = null;
if (attributes != null) {
request = attributes.getRequest();
}
String params = argsToString(proceedingJoinPoint.getArgs());
String url = null;
String token = null;
if (request != null) {
// 请求地址(作为存放cache的key值)
url = request.getRequestURI();
// 用户的唯一标识
token = request.getHeader("Authorization");
}
// 唯一标识(url + token + params)
String submitKey = "INTERFACE:" + MD5Util.toMD5(url + "_" + token + ":" + params);
boolean flag = false;
//判断缓存中是否有此key
if (redisUtil.hasKey(submitKey)) {
log.info("key={},interval={},重复提交", submitKey, interval);
} else {
//如果没有表示不是重复提交并设置key存活的缓存时间
redisUtil.set(submitKey, "", interval);
flag = true;
System.out.println("非重复提交");
}
if (flag) {
Object result;
try {
result = proceedingJoinPoint.proceed();
} catch (Throwable e) {
/*异常通知方法*/
log.error("异常通知方法>目标方法名{},异常为:{}", method.getName(), e);
throw e;
} finally {
redisUtil.del(submitKey);
}
return result;
} else {
throw new ServiceException(ApiError.ERROR_1014);
}
}
/**
* 根据切入点获取执行的方法
*/
private Method currentMethod(JoinPoint joinPoint) {
String methodName = joinPoint.getSignature().getName();
//获取目标类的所有方法,找到当前要执行的方法
Method[] methods = joinPoint.getTarget().getClass().getMethods();
Method resultMethod = null;
for (Method method : methods) {
if (method.getName().equals(methodName)) {
resultMethod = method;
break;
}
}
return resultMethod;
}
/**
* 参数拼装
*/
private String argsToString(Object[] paramsArray) {
StringBuilder params = new StringBuilder();
if (paramsArray != null && paramsArray.length > 0) {
for (Object o : paramsArray) {
if (!ObjectUtils.isEmpty(o) && !isFilterObject(o)) {
try {
params.append(JSONObject.toJSONString(o)).append(" ");
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
return params.toString().trim();
}
/**
* 判断是否是需要过滤的对象
*/
@SuppressWarnings("rawtypes")
public boolean isFilterObject(final Object o) {
Class<?> clazz = o.getClass();
if (clazz.isArray()) {
return clazz.getComponentType().isAssignableFrom(MultipartFile.class);
} else if (Collection.class.isAssignableFrom(clazz)) {
Collection collection = (Collection) o;
for (Object value : collection) {
return value instanceof MultipartFile;
}
} else if (Map.class.isAssignableFrom(clazz)) {
Map map = (Map) o;
for (Object value : map.entrySet()) {
Map.Entry entry = (Map.Entry) value;
return entry.getValue() instanceof MultipartFile;
}
}
return o instanceof MultipartFile || o instanceof HttpServletRequest || o instanceof HttpServletResponse
|| o instanceof BindingResult;
}
}
数据幂等性注解实现(支持批量操作)
/**
* 数据幂等性注解 支持参数标记
*/
@Target({ElementType.METHOD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DataIdempotent {
/**
* 入参主键id的名称
*/
String keyIdName() default "";
/*** 上锁时长,默认设置时间 30秒
*** @return
**/
long leaseTime() default -1L;
/***
* 尝试时间,设置时间内通过自旋一致尝试获取锁,
* 默认 0秒
* 通常时间要小于 leaseTime 时间**
* @return
* */
long waitTime() default 0L;
/**
* 业务类型
*
* @return
*/
String businessType() default "";
}
实现
@Slf4j
@Aspect
@Component
public class DataIdempotentAspect {
@Resource
private RedissonClient redissonClient;
private static final ThreadLocal<List<RLock>> LOCK_THREAD = new ThreadLocal<>();
@Pointcut("@annotation(com.common.business.annotation.DataIdempotent)")
public void dataPointCut() {
}
@Around("dataPointCut()")
public Object doAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
Method method = currentMethod(proceedingJoinPoint);
//获取到方法的注解对象
DataIdempotent idempotent = method.getAnnotation(DataIdempotent.class);
//单位 秒
long leaseTime = idempotent.leaseTime();
long waitTime = idempotent.waitTime();
String businessType = idempotent.businessType();
//获取传参
Object obj = proceedingJoinPoint.getArgs()[0];
List<Object> objList = new ArrayList<>();
if (StringUtils.isNotBlank(idempotent.keyIdName())) {
if (obj instanceof String || obj instanceof Long || obj instanceof Integer) {
objList.add(String.valueOf(obj));
} else if (obj instanceof List) {
objList = (List<Object>) obj;
} else if (obj instanceof String[]) {
objList = Arrays.asList((String[]) obj);
} else if (Objects.nonNull(obj)) {
Map map = JSONObject.parseObject(JSONObject.toJSONString(obj), Map.class);
Object object = map.get(idempotent.keyIdName());
//单个参数 或 list
if (object instanceof String || object instanceof Long || object instanceof Integer) {
objList.add(String.valueOf(object));
} else if (object instanceof String[]) {
objList = Arrays.asList((String[]) object);
} else if (object instanceof List) {
objList = (List<Object>) object;
}
}
}
if (CollectionUtil.isNotEmpty(objList)) {
List<RLock> rLocks = new ArrayList<>();
objList.forEach(o -> {
try {
String submitKey = "BUSINESS:" + o + "_" + businessType;
log.info("分布式锁上锁,key:{},lockTime:{}", submitKey, leaseTime);
RLock clientLock = redissonClient.getLock(submitKey);
//不设置 lockTime watch dog会 默认 锁定30s 10s重试
boolean locked = clientLock.tryLock(waitTime,leaseTime, TimeUnit.SECONDS);
if (!locked) {
log.error("{}上锁失败", submitKey);
throw new ServiceException(ApiError.ERROR_1026);
}
// clientLock.lock(lockTime, TimeUnit.SECONDS);
rLocks.add(clientLock);
log.info("分布式锁上锁成功,key:{},lockTime:{}", submitKey, leaseTime);
} catch (Exception e) {
//存在不能上锁情况时 释放已上锁对象
if (CollectionUtil.isNotEmpty(rLocks)) {
// 无需判断锁是否存在,直接调用 unlock
rLocks.forEach(rLock -> {
if (rLock.isLocked()) {
rLock.unlock();
}
});
}
throw new ServiceException(ApiError.ERROR_1026);
}
});
if (CollectionUtil.isNotEmpty(rLocks)) {
LOCK_THREAD.set(rLocks);
}
}
// 调用目标方法
return proceedingJoinPoint.proceed();
}
/*** 处理完请求后执行
* @param joinPoint 切点
*/
@AfterReturning(value = "dataPointCut()", returning = "apiResult")
public void doAfterReturning(JoinPoint joinPoint, Object apiResult) {
handleData();
}
/*** 拦截异常操作
* ** @param joinPoint 切点
* * @param e 异常
* */
@AfterThrowing(value = "dataPointCut()", throwing = "e")
public void doAfterThrowing(JoinPoint joinPoint, Exception e) {
handleData();
}
/**
* 根据切入点获取执行的方法
*/
private Method currentMethod(JoinPoint joinPoint) {
String methodName = joinPoint.getSignature().getName();
//获取目标类的所有方法,找到当前要执行的方法
Method[] methods = joinPoint.getTarget().getClass().getMethods();
Method resultMethod = null;
for (Method method : methods) {
if (method.getName().equals(methodName)) {
resultMethod = method;
break;
}
}
return resultMethod;
}
private void handleData() {
List<RLock> rLocks = LOCK_THREAD.get();
if (CollectionUtil.isNotEmpty(rLocks)) {
try {
rLocks.forEach(rLock -> {
log.info("任务执行完成,当前锁状态:{}", rLock.isLocked());
// 无需判断锁是否存在,直接调用 unlock
if (rLock.isLocked()) {
rLock.unlock();
log.info("释放锁");
}
});
} catch (Exception exception) {
exception.printStackTrace();
} finally {
LOCK_THREAD.remove();
}
}
}
}