⭐⭐⭐ Spring Boot 项目实战 ⭐⭐⭐ Spring Cloud 项目实战
《Dubbo 实现原理与源码解析 —— 精品合集》 《Netty 实现原理与源码解析 —— 精品合集》
《Spring 实现原理与源码解析 —— 精品合集》 《MyBatis 实现原理与源码解析 —— 精品合集》
《Spring MVC 实现原理与源码解析 —— 精品合集》 《数据库实体设计合集》
《Spring Boot 实现原理与源码解析 —— 精品合集》 《Java 面试题 + Java 学习指南》

摘要: 原创出处 blog.csdn.net/CSDN_WYL2016/article/details/122993086 「码拉松」欢迎转载,保留摘要,谢谢!


🙂🙂🙂关注微信公众号:【芋道源码】有福利:

  1. RocketMQ / MyCAT / Sharding-JDBC 所有源码分析文章列表
  2. RocketMQ / MyCAT / Sharding-JDBC 中文注释源码 GitHub 地址
  3. 您对于源码的疑问每条留言都将得到认真回复。甚至不知道如何读源码也可以请教噢。
  4. 新的源码解析文章实时收到通知。每周更新一篇左右。
  5. 认真的源码交流微信群。

一、什么是幂等性?

简单来说,就是对一个接口执行重复的多次请求,与一次请求所产生的结果是相同的,听起来非常容易理解,但要真正的在系统中要始终保持这个目标,是需要很严谨的设计的,在实际的生产环境下,我们应该保证任何接口都是幂等的,而如何正确的实现幂等,就是本文要讨论的内容。

二、哪些请求天生就是幂等的?

首先,我们要知道查询类的请求一般都是天然幂等的,除此之外,删除请求在大多数情况下也是幂等的,但是ABA场景下除外。

举一个简单的例子

比如,先请求了一次删除A的操作,但由于响应超时,又自动请求了一次删除A的操作,如果在两次请求之间,又插入了一次A,而实际上新插入的这一次A,是不应该被删除的,这就是ABA问题,不过,在大多数业务场景中,ABA问题都是可以忽略的。

除了查询和删除之外,还有更新操作,同样的更新操作在大多数场景下也是天然幂等的,其例外是也会存在ABA的问题,更重要的是,比如执行update table set a = a + 1 where v = 1这样的更新就非幂等了。

最后,就还剩插入了,插入大多数情况下都是非幂等的,除非是利用数据库唯一索引来保证数据不会重复产生。

三、为什么需要幂等

1.超时重试

当发起一次RPC请求时,难免会因为网络不稳定而导致请求失败,一般遇到这样的问题我们希望能够重新请求一次,正常情况下没有问题,但有时请求实际上已经发出去了,只是在请求响应时网络异常或者超时,此时,请求方如果再重新发起一次请求,那被请求方就需要保证幂等了。

2.异步回调

异步回调是提升系统接口吞吐量的一种常用方式,很明显,此类接口一定是需要保证幂等性的。

3.消息队列

现在常用的消息队列框架,比如:Kafka、RocketMQ、RabbitMQ在消息传递时都会采取At least once原则(也就是至少一次原则,在消息传递时,不允许丢消息,但是允许有重复的消息),既然消息队列不保证不会出现重复的消息,那消费者自然要保证处理逻辑的幂等性了。

四、实现幂等的关键因素

关键因素1

幂等唯一标识,可以叫它幂等号或者幂等令牌或者全局ID,总之就是客户端与服务端一次请求时的唯一标识,一般情况下由客户端来生成,也可以让第三方来统一分配。

关键因素2

有了唯一标识以后,服务端只需要确保这个唯一标识只被使用一次即可,一种常见的方式就是利用数据库的唯一索引。

五、注解实现幂等性

下面演示一种利用Redis来实现的方式。

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.统一的请求入参对象

@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 "重复请求";
}
}
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;
}
}

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 {

@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();
}

}

5. 请求示例

调用接口之前,先申请一个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<Order> requestData) {
return "success";
}

}

请求获取token值。

带着token值,第一次请求成功。

第二次请求失败。

文章目录
  1. 1. 一、什么是幂等性?
  2. 2. 二、哪些请求天生就是幂等的?
    1. 2.0.1. 举一个简单的例子
  • 3. 三、为什么需要幂等
    1. 3.1. 1.超时重试
    2. 3.2. 2.异步回调
    3. 3.3. 3.消息队列
  • 4. 四、实现幂等的关键因素
    1. 4.0.1. 关键因素1
    2. 4.0.2. 关键因素2
  • 5. 五、注解实现幂等性
    1. 5.0.1. 1.自定义注解
    2. 5.0.2. 2.统一的请求入参对象
    3. 5.0.3. 3.AOP处理
    4. 5.0.4. 4.Token值生成
    5. 5.0.5. 5. 请求示例