目的
为了保证数据的一致性、唯一性,最简单有效的方式就是数据库表增加唯一约束,但不是所有场景都支持,为了能够支持所有的业务场景,还是需要在接口上面做文章
场景
1,WEB页面重复提交
2,feign接口超时自动重试
3,防接口并发
并发锁
前后端都需要做防并发控制 (基本做法就是使用Redis的分布式锁)
自定义并发锁注解
/**
* 并发锁注解
*
* @author xiongyan
* @date 2020/3/14
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ConcurrentLock {
/**
* key
*
* @return
*/
String key();
/**
* 参数,多个用“,”隔开
*
* @return
*/
String param() default "";
/**
* 锁自动释放时间(单位秒)
*
* @return
*/
int lockTime() default 5;
}
并发锁切面
/**
* 并发锁切面
*
* @author xiongyan
* @date 2020/3/13
*/
@Aspect
@Slf4j
@Component
public class ConcurrentLockAspect {
@Pointcut(value = "@annotation(com.xxx.xxx.common.lock.ConcurrentLock)")
public void concurrentLock() {
}
@Around("concurrentLock()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
ConcurrentLock concurrentLock = signature.getMethod().getAnnotation(ConcurrentLock.class);
if (null == concurrentLock || StrUtil.isEmpty(concurrentLock.key())) {
return joinPoint.proceed();
}
// 并发锁key
String key = signature.getDeclaringTypeName() + "_" + signature.getName() + "_" + concurrentLock.key();
if (StrUtil.isNotEmpty(concurrentLock.param())) {
try {
Object[] spelValue = SpelUtil.parseToArray(concurrentLock.param().split(","), joinPoint);
if (null != spelValue) {
key = MessageFormat.format(key, spelValue);
}
} catch (Exception e) {
log.warn("并发锁注解解析失败, key:{}, param:{}", concurrentLock.key(), concurrentLock.param(), e);
return joinPoint.proceed();
}
}
long lockTimes = concurrentLock.lockTime();
RLock lock = RedisUtil.getLock(Md5Util.md5(key));
// 获取锁
if (lock.tryLock(0, lockTimes, TimeUnit.SECONDS)) {
try {
// 执行业务逻辑
return joinPoint.proceed();
} finally {
// 释放锁
if (lock.isLocked()) {
lock.forceUnlock();
}
}
} else {
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
log.warn("接口触发了并发锁,uri:{},params:{}", request.getRequestURI(), JacksonUtil.toJsonString(joinPoint.getArgs()));
throw new ScmBizException("请稍后再试");
}
}
}
幂等锁
如果自己实现幂等就需要加表,把请求的数据先存入表,存在则直接返回成功
实现通用的幂等组件(借助于Redis的分布式锁和Redis持久化缓存),大大降低了业务的复杂度
自定义幂等锁注解
/**
* 幂等锁注解
*
* @author xiongyan
* @date 2020/3/14
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface IdempotentLock {
/**
* key
*
* @return
*/
String key();
/**
* 参数,多个用“,”隔开
*
* @return
*/
String param() default "";
/**
* 锁自动释放时间(天)
*
* @return
*/
int lockTime() default 365;
}
幂等锁切面
/**
* 幂等锁切面
*
* @author xiongyan
* @date 2020/3/13
*/
@Aspect
@Slf4j
@Component
public class IdempotentLockAspect {
@Pointcut(value = "@annotation(com.xxx.xxx.common.lock.IdempotentLock)")
public void idempotentLock() {
}
@Around("idempotentLock()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
IdempotentLock idempotentLock = signature.getMethod().getAnnotation(IdempotentLock.class);
if (null == idempotentLock || StrUtil.isEmpty(idempotentLock.key())) {
return joinPoint.proceed();
}
// 幂等锁key
String key = signature.getDeclaringTypeName() + "_" + signature.getName() + "_" + idempotentLock.key();
if (StrUtil.isNotEmpty(idempotentLock.param())) {
try {
Object[] spelValue = SpelUtil.parseToArray(idempotentLock.param().split(","), joinPoint);
if (null != spelValue) {
key = MessageFormat.format(key, spelValue);
}
} catch (Exception e) {
log.warn("幂等锁注解解析失败, key:{}, param:{}", idempotentLock.key(), idempotentLock.param(), e);
return joinPoint.proceed();
}
}
key = Md5Util.md5(key);
RMapCache<String, Object> cache = RedisUtil.getMapCache("idempotent");
Object result = cache.get(key);
if (null != result) {
return result;
}
RLock lock = RedisUtil.getLock(key);
// 获取锁 (最大锁时间5秒后自动释放)
if (lock.tryLock(0, 5, TimeUnit.SECONDS)) {
try {
// 执行业务逻辑
result = joinPoint.proceed();
// 放入缓存
cache.put(key, result, idempotentLock.lockTime(), TimeUnit.DAYS);
return result;
} finally {
// 释放锁
if (lock.isLocked()) {
lock.forceUnlock();
}
}
} else {
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
log.warn("接口触发了并发锁,uri:{},params:{}", request.getRequestURI(), JacksonUtil.toJsonString(joinPoint.getArgs()));
throw new ScmBizException("请稍后再试");
}
}
}