本文目录导读:

我将为你提供一个完整的Spring Boot接口幂等性实现方案。
使用Token+Redis的方式实现接口幂等性,这是最常用的方案之一。
完整实现代码
添加依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
自定义注解
package com.example.idempotent.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 幂等性注解
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Idempotent {
/**
* 幂等标识参数名称
* 支持从请求参数、请求头、路径变量中获取
*/
String key() default "";
/**
* 过期时间(秒)
*/
long expireTime() default 60;
/**
* 提示信息
*/
String message() default "重复请求,请稍后再试";
}
创建响应类
package com.example.idempotent.common;
import lombok.Data;
@Data
public class Result<T> {
private Integer code;
private String message;
private T data;
public static <T> Result<T> success(T data) {
Result<T> result = new Result<>();
result.setCode(200);
result.setMessage("success");
result.setData(data);
return result;
}
public static <T> Result<T> error(Integer code, String message) {
Result<T> result = new Result<>();
result.setCode(code);
result.setMessage(message);
return result;
}
}
创建异常类
package com.example.idempotent.exception;
public class IdempotentException extends RuntimeException {
public IdempotentException(String message) {
super(message);
}
}
实现AOP切面
package com.example.idempotent.aspect;
import com.example.idempotent.annotation.Idempotent;
import com.example.idempotent.exception.IdempotentException;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
@Slf4j
@Aspect
@Component
public class IdempotentAspect {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Around("@annotation(com.example.idempotent.annotation.Idempotent)")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
Idempotent idempotent = method.getAnnotation(Idempotent.class);
// 获取请求上下文
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
// 生成幂等key
String idempotentKey = generateIdempotentKey(request, idempotent, joinPoint);
if (StringUtils.isBlank(idempotentKey)) {
throw new IdempotentException("无法生成幂等标识");
}
// 尝试获取分布式锁
boolean lockSuccess = tryLock(idempotentKey, idempotent.expireTime());
if (!lockSuccess) {
throw new IdempotentException(idempotent.message());
}
try {
// 执行目标方法
return joinPoint.proceed();
} finally {
// 执行完成后删除锁
// 注意:这里可以根据业务需求决定是否删除锁
// 如果需要在固定时间内防止重复提交,可以不删除锁使其在过期时间后自动失效
// releaseLock(idempotentKey);
}
}
/**
* 生成幂等key
*/
private String generateIdempotentKey(HttpServletRequest request, Idempotent idempotent, ProceedingJoinPoint joinPoint) {
StringBuilder keyBuilder = new StringBuilder("idempotent:");
keyBuilder.append(request.getRequestURI()).append(":");
// 1. 优先使用自定义key表达式
if (StringUtils.isNotBlank(idempotent.key())) {
String customKey = resolveCustomKey(idempotent.key(), request, joinPoint);
keyBuilder.append(customKey);
} else {
// 2. 默认使用token(从请求头或参数中获取)
String token = request.getHeader("token");
if (StringUtils.isBlank(token)) {
token = request.getParameter("token");
}
if (StringUtils.isBlank(token)) {
token = UUID.randomUUID().toString();
}
keyBuilder.append(token);
}
return keyBuilder.toString();
}
/**
* 解析自定义key
*/
private String resolveCustomKey(String keyExpression, HttpServletRequest request, ProceedingJoinPoint joinPoint) {
// 支持格式:#paramName、headerName、pathVariableName
if (keyExpression.startsWith("#")) {
// 从参数中获取
String paramName = keyExpression.substring(1);
Object[] args = joinPoint.getArgs();
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
String[] parameterNames = signature.getParameterNames();
if (parameterNames != null) {
for (int i = 0; i < parameterNames.length; i++) {
if (paramName.equals(parameterNames[i])) {
return String.valueOf(args[i]);
}
}
}
} else if (keyExpression.startsWith("header:")) {
// 从请求头获取
return request.getHeader(keyExpression.substring(7));
} else {
// 从请求参数获取
return request.getParameter(keyExpression);
}
return null;
}
/**
* 使用Redis SETNX实现分布式锁
*/
private boolean tryLock(String key, long expireTime) {
Boolean success = stringRedisTemplate.opsForValue()
.setIfAbsent(key, "1", expireTime, TimeUnit.SECONDS);
return Boolean.TRUE.equals(success);
}
/**
* 释放锁
*/
private void releaseLock(String key) {
stringRedisTemplate.delete(key);
}
}
全局异常处理
package com.example.idempotent.handler;
import com.example.idempotent.common.Result;
import com.example.idempotent.exception.IdempotentException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(IdempotentException.class)
public Result<?> handleIdempotentException(IdempotentException e) {
log.warn("幂等异常:{}", e.getMessage());
return Result.error(4004, e.getMessage());
}
@ExceptionHandler(Exception.class)
public Result<?> handleException(Exception e) {
log.error("系统异常", e);
return Result.error(500, "系统异常");
}
}
创建Controller示例
package com.example.idempotent.controller;
import com.example.idempotent.annotation.Idempotent;
import com.example.idempotent.common.Result;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
@Slf4j
@RestController
@RequestMapping("/api/order")
public class OrderController {
/**
* 创建订单 - 使用请求头token实现幂等
*/
@PostMapping("/create")
@Idempotent(key = "header:token", expireTime = 30, message = "订单创建中,请勿重复提交")
public Result<?> createOrder(@RequestBody OrderRequest request) {
// 模拟业务处理
log.info("创建订单:{}", request);
try {
TimeUnit.MILLISECONDS.sleep(100);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
Map<String, Object> result = new HashMap<>();
result.put("orderId", "ORDER" + System.currentTimeMillis());
result.put("orderNo", request.getOrderNo());
return Result.success(result);
}
/**
* 支付订单 - 使用订单号作为幂等标识
*/
@PostMapping("/pay")
@Idempotent(key = "#orderNo", expireTime = 60, message = "支付处理中,请勿重复操作")
public Result<?> payOrder(@RequestParam String orderNo) {
log.info("支付订单:{}", orderNo);
Map<String, Object> result = new HashMap<>();
result.put("orderNo", orderNo);
result.put("status", "PAID");
return Result.success(result);
}
/**
* 退款 - 使用用户ID和订单号组合
*/
@PostMapping("/refund")
@Idempotent(key = "requestId", expireTime = 30)
public Result<?> refund(@RequestParam String requestId,
@RequestParam String orderNo,
@RequestParam Double amount) {
log.info("退款请求:requestId={}, orderNo={}, amount={}", requestId, orderNo, amount);
Map<String, Object> result = new HashMap<>();
result.put("refundId", "REF" + System.currentTimeMillis());
result.put("orderNo", orderNo);
result.put("amount", amount);
return Result.success(result);
}
/**
* 测试并发场景
*/
@PostMapping("/concurrent")
@Idempotent(key = "header:requestId", expireTime = 10, message = "请求正在处理中")
public Result<?> concurrentTest(@RequestBody Map<String, Object> params) {
log.info("处理请求:{}", params);
Map<String, Object> result = new HashMap<>();
result.put("code", "SUCCESS");
result.put("data", params);
return Result.success(result);
}
// 内部类
public static class OrderRequest {
private String orderNo;
private String productId;
private Integer quantity;
// getter/setter省略...
}
}
Redis配置
package com.example.idempotent.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(factory);
// 使用String序列化器处理key
template.setKeySerializer(new StringRedisSerializer());
template.setHashKeySerializer(new StringRedisSerializer());
// 使用JSON序列化器处理value
GenericJackson2JsonRedisSerializer jsonSerializer = new GenericJackson2JsonRedisSerializer();
template.setValueSerializer(jsonSerializer);
template.setHashValueSerializer(jsonSerializer);
template.afterPropertiesSet();
return template;
}
}
自定义Token生成工具
package com.example.idempotent.util;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
@Component
public class TokenUtil {
@Autowired
private StringRedisTemplate stringRedisTemplate;
/**
* 生成幂等token并存储在Redis
*/
public String generateToken() {
String token = UUID.randomUUID().toString().replace("-", "");
// 存储token到Redis,设置30分钟过期
stringRedisTemplate.opsForValue().set(token, "1", 30, TimeUnit.MINUTES);
return token;
}
/**
* 校验并删除token(一次性使用)
*/
public boolean validateAndRemoveToken(String token) {
Boolean existed = stringRedisTemplate.hasKey(token);
if (Boolean.TRUE.equals(existed)) {
stringRedisTemplate.delete(token);
return true;
}
return false;
}
}
测试用例
package com.example.idempotent;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.http.*;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class IdempotentTest {
@Autowired
private TestRestTemplate restTemplate;
/**
* 测试相同token的幂等性
*/
@Test
public void testIdempotentWithSameToken() {
String url = "http://localhost:8080/api/order/create";
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
headers.set("token", "test-token-001");
Map<String, Object> body = new HashMap<>();
body.put("orderNo", "ORDER-TEST-001");
body.put("productId", "P001");
body.put("quantity", 1);
HttpEntity<Map<String, Object>> entity = new HttpEntity<>(body, headers);
// 第一次请求
ResponseEntity<String> firstResponse = restTemplate.exchange(url, HttpMethod.POST, entity, String.class);
System.out.println("第一次请求结果: " + firstResponse.getBody());
// 第二次相同请求(在过期时间内)
ResponseEntity<String> secondResponse = restTemplate.exchange(url, HttpMethod.POST, entity, String.class);
System.out.println("第二次请求结果: " + secondResponse.getBody());
}
/**
* 测试并发场景
*/
@Test
public void testConcurrentRequests() throws InterruptedException {
String url = "http://localhost:8080/api/order/concurrent";
int threadCount = 10;
CountDownLatch latch = new CountDownLatch(threadCount);
ExecutorService executor = Executors.newFixedThreadPool(threadCount);
for (int i = 0; i < threadCount; i++) {
final int threadNum = i;
executor.submit(() -> {
try {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
headers.set("requestId", "concurrent-test-001");
Map<String, Object> body = new HashMap<>();
body.put("thread", threadNum);
body.put("timestamp", System.currentTimeMillis());
HttpEntity<Map<String, Object>> entity = new HttpEntity<>(body, headers);
ResponseEntity<String> response = restTemplate.exchange(url, HttpMethod.POST, entity, String.class);
System.out.println("线程" + threadNum + "请求结果: " + response.getBody());
} finally {
latch.countDown();
}
});
}
latch.await();
executor.shutdown();
}
}
使用场景说明
前端调用示例
// 获取token
async function getToken() {
const response = await fetch('/api/token/generate');
const data = await response.json();
return data.data;
}
// 创建订单(携带token)
async function createOrder() {
const token = await getToken();
const response = await fetch('/api/order/create', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'token': token
},
body: JSON.stringify({
orderNo: 'ORDER123',
productId: 'P001',
quantity: 1
})
});
return response.json();
}
配置application.yml
spring:
redis:
host: localhost
port: 6379
database: 0
timeout: 10000ms
lettuce:
pool:
max-active: 8
max-wait: -1ms
max-idle: 8
min-idle: 0
注意事项
- Redis可用性:确保Redis服务稳定,避免单点故障
- 过期时间设置:根据业务场景合理设置过期时间
- Key设计:要保证key的唯一性和可读性
- 异常处理:处理好Redisson异常,避免业务中断
- 性能考虑:精简AOP逻辑,避免过多Redis交互
这个方案能有效防止用户重复提交、网络重试等情况导致的重复处理问题。
版权声明:NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
足球直播
足球直播
足球直播
篮球直播
篮球直播
篮球直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
亚冠直播
亚冠直播
亚冠直播
欧冠直播
欧冠直播
欧冠直播
英超直播
英超直播
英超直播
西甲直播
西甲直播
西甲直播
中超直播
中超直播
中超直播
足球直播
足球直播
足球直播
篮球直播
篮球直播
篮球直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
亚冠直播
亚冠直播
亚冠直播
欧冠直播
欧冠直播
欧冠直播
英超直播
英超直播
英超直播
西甲直播
西甲直播
西甲直播
中超直播
中超直播
中超直播
足球直播
足球直播
足球直播
篮球直播
篮球直播
篮球直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
亚冠直播
亚冠直播
亚冠直播
欧冠直播
欧冠直播
欧冠直播
英超直播
英超直播
英超直播
西甲直播
西甲直播
西甲直播
中超直播
中超直播
中超直播
足球直播
足球直播
足球直播
篮球直播
篮球直播
篮球直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
亚冠直播
亚冠直播
亚冠直播
欧冠直播
欧冠直播
欧冠直播
英超直播
英超直播
英超直播
西甲直播
西甲直播
西甲直播
中超直播
中超直播
中超直播
曼联直播
曼联直播
曼联直播
日职联直播
日职联直播
日职联直播
澳超直播
澳超直播
澳超直播
阿森纳直播
阿森纳直播
阿森纳直播
足球直播
足球直播
足球直播
NBA直播
NBA直播
NBA直播
曼联直播
曼联直播
曼联直播
日职联直播
日职联直播
日职联直播
澳超直播
澳超直播
澳超直播
阿森纳直播
阿森纳直播
阿森纳直播
足球直播
足球直播
足球直播
NBA直播
NBA直播
NBA直播
曼联直播
曼联直播
曼联直播
日职联直播
日职联直播
日职联直播
澳超直播
澳超直播
澳超直播
阿森纳直播
阿森纳直播
阿森纳直播
足球直播
足球直播
足球直播
NBA直播
NBA直播
NBA直播
曼联直播
曼联直播
曼联直播
日职联直播
日职联直播
日职联直播
澳超直播
澳超直播
澳超直播
阿森纳直播
阿森纳直播
阿森纳直播
足球直播
足球直播
足球直播
NBA直播
NBA直播
NBA直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
足球直播
足球直播
足球直播
足球直播
足球直播
英超直播
英超直播
英超直播
英超直播
英超直播
篮球直播
篮球直播
篮球直播
篮球直播
篮球直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
中超直播
中超直播
中超直播
中超直播
中超直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
足球直播
足球直播
足球直播
足球直播
足球直播
英超直播
英超直播
英超直播
英超直播
英超直播
篮球直播
篮球直播
篮球直播
篮球直播
篮球直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
中超直播
中超直播
中超直播
中超直播
中超直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播