摘要:版权声明:本文为CSDN博主「码拉松」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。原文链接:https://blog.csdn.net/CSDN_WYL2016/article/details/122993086
版权声明:本文为CSDN博主「码拉松」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。原文链接:https://blog.csdn.net/CSDN_WYL2016/article/details/122993086
简单来说,就是对一个接口执行重复的多次请求,与一次请求所产生的结果是相同的,听起来非常容易理解,但要真正的在系统中要始终保持这个目标,是需要很严谨的设计的,在实际的生产环境下,我们应该保证任何接口都是幂等的,而如何正确的实现幂等,就是本文要讨论的内容。
首先,我们要知道查询类的请求一般都是天然幂等的,除此之外,删除请求在大多数情况下也是幂等的,但是ABA场景下除外。
比如,先请求了一次删除A的操作,但由于响应超时,又自动请求了一次删除A的操作,如果在两次请求之间,又插入了一次A,而实际上新插入的这一次A,是不应该被删除的,这就是ABA问题,不过,在大多数业务场景中,ABA问题都是可以忽略的。
除了查询和删除之外,还有更新操作,同样的更新操作在大多数场景下也是天然幂等的,其例外是也会存在ABA的问题,更重要的是,比如执行update table set a = a + 1 where v = 1这样的更新就非幂等了。
最后,就还剩插入了,插入大多数情况下都是非幂等的,除非是利用数据库唯一索引来保证数据不会重复产生。插播一条:如果你近期准备面试跳槽,建议在Java面试库小程序在线刷题,涵盖 2000+ 道 java 面试题,几乎覆盖了所有主流技术面试题。
当发起一次RPC请求时,难免会因为网络不稳定而导致请求失败,一般遇到这样的问题我们希望能够重新请求一次,正常情况下没有问题,但有时请求实际上已经发出去了,只是在请求响应时网络异常或者超时,此时,请求方如果再重新发起一次请求,那被请求方就需要保证幂等了。
异步回调是提升系统接口吞吐量的一种常用方式,很明显,此类接口一定是需要保证幂等性的。
现在常用的消息队列框架,比如:Kafka、RocketMQ、RabbitMQ在消息传递时都会采取At least once原则(也就是至少一次原则,在消息传递时,不允许丢消息,但是允许有重复的消息),既然消息队列不保证不会出现重复的消息,那消费者自然要保证处理逻辑的幂等性了。
四、实现幂等的关键因素关键因素1幂等唯一标识,可以叫它幂等号或者幂等令牌或者全局ID,总之就是客户端与服务端一次请求时的唯一标识,一般情况下由客户端来生成,也可以让第三方来统一分配。
关键因素2有了唯一标识以后,服务端只需要确保这个唯一标识只被使用一次即可,一种常见的方式就是利用数据库的唯一索引。
插播一条:如果你近期准备面试跳槽,建议在Java面试库小程序在线刷题,涵盖 2000+ 道 Java 面试题,几乎覆盖了所有主流技术面试题。
下面演示一种利用Redis来实现的方式。
推荐一个开源免费的 Spring Boot 实战项目:
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;}@Datapublic class RequestData {private Header header;private T body;}@Datapublic class Header {private StringToken;}@Datapublic class Order {String orderNo;}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@Componentpublic class IdempotentAspect {@Resourceprivate 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 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 "重复请求";}}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 getParamValue(ProceedingJoinPoint joinPoint) {Object paramValues = joinPoint.getArgs;String paramNames = ((CodeSignature) joinPoint.getSignature).getParameterNames;Map param = new HashMap(paramNames.length);for (int i = 0; i 插播一条:如果你近期准备面试跳槽,建议在Java面试库小程序在线刷题,涵盖 2000+ 道 Java 面试题,几乎覆盖了所有主流技术面试题。4.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 {@Resourceprivate 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;@Componentpublic class RedisIdempotentStorage implements IdempotentStorage {@Resourceprivate RedisTemplate redisTemplate;@Overridepublic void save(String idempotentId) {redisTemplate.opsForValue.set(idempotentId, idempotentId, 10, TimeUnit.MINUTES);}@Overridepublic boolean delete(String idempotentId) {return redisTemplate.delete(idempotentId);}}import java.util.UUID;public class IdGeneratorUtil {public static String generateId {return UUID.randomUUID.toString;}}调用接口之前,先申请一个token,然后带着服务端返回的token值,再去请求。
import com.springboot.micrometer.annotation.Idempotent;import com.springboot.micrometer.entity.Order;import com.springboot.micrometer.entity.RequestData;import org.springframework.web.bind.annotation.RequestBody;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RestController;@RestController@RequestMapping("/order")public class OrderController {@RequestMapping("/saveOrder")@Idempotent(name = "requestData", type = RequestData.class, field = "token")public String saveOrder(@RequestBody RequestData requestData) {return "success";}}请求获取token值。
带着token值,第一次请求成功。
第二次请求失败。
来源:涵涵课堂