1、幂等的必要性
用户不可靠:用户重复点击、暴力攻击
网络不可靠:网络抖动、投递超时
服务不可靠:调用下游服务超时、异常
2、幂等实现的方案
1、唯一索引:要求表添加单独记录幂等号的唯一索引字段
2、token机制:一般用于前端的表单提交,在访问表单页前,请求后台获取token值。
3、悲观锁机制:在更新前查询对应数据,SQL语句中添加 select for update 锁住当前行
// 1. 开启事务
begin;
// 2. 基于幂等号查询
record = select * from tbl_xxx where out_biz_no = 'xxx' for update;
// 3. 根据状态进行决策
if(record.getStatus() != 预期状态){
return;
}
// 4. 更新记录
update tbl_xxx set status = '目标状态' where out_biz_no = 'xxx';
// 5. 提交事务
commit;
4、乐观锁机制:使用 version、count等字段,带条件更新
update set xx=xx, version=#{version}+1 where id = ? and version = #{version}
5、分布式锁机制:
本质与悲观锁一致,在请求前获取锁,实现串行化
6、状态机机制:
在有限数量的状态,状态之前存在流转顺序。配合乐观锁机制
update tableName set sq=sq-#{quantity},status=#{udpate_status}
where id =#{id} and status=#{status}
3、自定义幂等注解
1、自定义注解
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(value = ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Idempotent {
/**
* 参数名,表示将从哪个参数中获取属性值。
* 获取到的属性值将作为KEY。
* @return
*/
String name() default "";
/**
* 属性,表示将获取哪个属性的值。
* @return
*/
String field() default "";
/**
* 参数类型
* @return
*/
Class type();
}
2、统一请求参数封装
要求在head中必须传递一次性token
@Data
public class RequestData<T> {
private Header header;
private T body;
}
@Data
public class Header {
private String token;
}
@Data
public class Order {
String orderNo;
}
3、AOP切面处理
import com.springboot.micrometer.annotation.Idempotent;
import com.springboot.micrometer.entity.RequestData;
import com.springboot.micrometer.idempotent.RedisIdempotentStorage;
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.stereotype.Component;
import javax.annotation.Resource;
import java.lang.reflect.Method;
import java.util.Map;
@Aspect
@Component
public class IdempotentAspect {
@Resource
private RedisIdempotentStorage redisIdempotentStorage;
@Pointcut("@annotation(com.springboot.micrometer.annotation.Idempotent)")
public void idempotent() {
}
@Around("idempotent()")
public Object methodAround(ProceedingJoinPoint joinPoint) throws Throwable {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
Idempotent idempotent = method.getAnnotation(Idempotent.class);
String field = idempotent.field();
String name = idempotent.name();
Class clazzType = idempotent.type();
String token = "";
Object object = clazzType.newInstance();
Map<String, Object> paramValue = AopUtils.getParamValue(joinPoint);
if (object instanceof RequestData) {
RequestData idempotentEntity = (RequestData) paramValue.get(name);
token = String.valueOf(AopUtils.getFieldValue(idempotentEntity.getHeader(), field));
}
if (redisIdempotentStorage.delete(token)) {
return joinPoint.proceed();
}
return "重复请求";
}
}
4、反射工具类
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.reflect.CodeSignature;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;
public class AopUtils {
public static Object getFieldValue(Object obj, String name) throws Exception {
Field[] fields = obj.getClass().getDeclaredFields();
Object object = null;
for (Field field : fields) {
field.setAccessible(true);
if (field.getName().toUpperCase().equals(name.toUpperCase())) {
object = field.get(obj);
break;
}
}
return object;
}
public static Map<String, Object> getParamValue(ProceedingJoinPoint joinPoint) {
Object[] paramValues = joinPoint.getArgs();
String[] paramNames = ((CodeSignature) joinPoint.getSignature()).getParameterNames();
Map<String, Object> param = new HashMap<>(paramNames.length);
for (int i = 0; i < paramNames.length; i++) {
param.put(paramNames[i], paramValues[i]);
}
return param;
}
}
5、token生成接口
import com.springboot.micrometer.idempotent.RedisIdempotentStorage;
import com.springboot.micrometer.util.IdGeneratorUtil;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
@RestController
@RequestMapping("/idGenerator")
public class IdGeneratorController {
@Resource
private RedisIdempotentStorage redisIdempotentStorage;
@RequestMapping("/getIdGeneratorToken")
public String getIdGeneratorToken() {
String generateId = IdGeneratorUtil.generateId();
redisIdempotentStorage.save(generateId);
return generateId;
}
}
public interface IdempotentStorage {
void save(String idempotentId);
boolean delete(String idempotentId);
}
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.io.Serializable;
import java.util.concurrent.TimeUnit;
@Component
public class RedisIdempotentStorage implements IdempotentStorage {
@Resource
private RedisTemplate<String, Serializable> redisTemplate;
@Override
public void save(String idempotentId) {
redisTemplate.opsForValue().set(idempotentId, idempotentId, 10, TimeUnit.MINUTES);
}
@Override
public boolean delete(String idempotentId) {
return redisTemplate.delete(idempotentId);
}
}
import java.util.UUID;
public class IdGeneratorUtil {
public static String generateId() {
return UUID.randomUUID().toString();
}
}
本文含有隐藏内容,请 开通VIP 后查看