服务中经常会出现一些只允许一次提交的场景,幂等模块可快速实现此类场景。无论集群环境还是单机开发,都能灵活适配。
概述
幂等模块通过 @Idempotent 注解 + AOP 拦截,自动判定重复请求:
-
分布式推荐: 使用 Redis 做存储判定,实现应用级跨实例拦截
-
内存兜底: 使用本地内存存储,无需 Redis 也能运行,适合开发 / 测试环境
仓库坐标
Maven
<dependencies>
<dependency>
<groupId>com.luamas.thinkbridge</groupId>
<artifactId>idempotent-spring-boot-starter</artifactId>
</dependency>
<!-- 可选依赖:不加则默认使用内存实现 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
自动配置
引入 idempotent-spring-boot-starter 后,Spring Boot 自动完成以下配置,无需手动声明任何 Bean:
| 内部配置类 | 触发条件 | 说明 |
|---|---|---|
RedisIdempotentKeyStoreConfiguration |
存在 RedisConnectionFactory Bean |
使用 Redis SET NX EX 实现分布式幂等 |
InMemoryIdempotentKeyStoreConfiguration |
不存在任何 IdempotentKeyStore Bean |
使用 ConcurrentHashMap + TTL 定时清理实现本地幂等 |
IdempotentAspect Bean |
存在 IdempotentKeyStore Bean |
AOP 切面,拦截 @Idempotent 注解方法 |
MvcBindingEvaluationContextConfiguration |
Servlet Web 环境 |
将 #request 变量注入 SpEL 上下文 |
Key 结构
幂等 Key 由多层前缀和唯一标识拼接而成:
{全局前缀}:{注解前缀}:{唯一表达式}
注解前缀(业务级命名空间)
在 @Idempotent 中通过 prefix 属性指定,支持 SpEL 表达式动态解析:
// 静态前缀
@Idempotent(prefix = "payment", uniqueExpression = "#orderId")
// 完整 key: myapp:payment:{orderId}
// SpEL 动态前缀
@Idempotent(prefix = "'order:' + #tenantId", uniqueExpression = "#orderId")
// 完整 key: myapp:order:{tenantId}:{orderId}
| 全局前缀和注解前缀互不冲突,二者同时存在时以 : 拼接。均不配置时,使用默认前缀 thinkbridge:idem。 |
@Idempotent 注解
基本用法
@Idempotent(uniqueExpression = "#userDto.id", duration = 5)
public void createUser(UserDto userDto) {
// 业务逻辑
}
uniqueExpression 支持以下 SpEL 变量:
| 变量 | 说明 | 示例 |
|---|---|---|
#参数名 |
按方法参数名访问 |
#userDto.id |
#p0, #p1… |
按参数索引访问 |
#p0 |
#request |
当前 HTTP 请求(Web 环境) |
#request.getHeader('token') |
属性参考
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
prefix |
String |
thinkbridge:idem |
业务级 Key 前缀,支持 SpEL 动态解析 |
uniqueExpression |
String |
空 |
唯一标识的 EL 表达式,为空时仅用前缀作为 Key |
value |
String |
空 |
存入的值,用于值匹配防止误删 |
duration |
long |
300(5分钟) |
Key 的 TTL 过期时间,单位秒 |
message |
String |
idempotent error |
重复请求时的提示消息 |
removeKeyWhenFinished |
boolean |
true |
业务成功后立即清除 Key |
removeKeyWhenError |
boolean |
true |
业务异常时立即清除 Key |
BindingEvaluationContext SPI
如需在 SpEL 中注入自定义变量,实现 BindingEvaluationContext 接口并注册为 Spring Bean:
@FunctionalInterface
public interface BindingEvaluationContext {
void bind(@Nullable EvaluationContext evaluationContext);
}
示例:绑定自定义请求上下文
package com.luamas.thinkbridge.integration.demo.idempotent.evaluation;
import com.luamas.thinkbridge.common.idempotent.BindingEvaluationContext;
import org.springframework.context.annotation.Profile;
import org.springframework.expression.EvaluationContext;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
/**
* 绑定自定义表达式
*/
@Profile("idempotent")
@Component
public class RequestEvaluationContext implements BindingEvaluationContext {
@Override
public void bind(EvaluationContext evaluationContext) {
// 如果在 sevlet 环境下,则将 request 信息放入上下文,便于获取请求参数
ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder
.getRequestAttributes();
if (requestAttributes != null) {
evaluationContext.setVariable("request", requestAttributes.getRequest());
}
}
}
注册后即可在 @Idempotent 中使用:
@Idempotent(uniqueExpression = "#requestContext.getTraceId()")
public Result handle(Request req) { ... }
完整示例
基础配置:
package com.luamas.thinkbridge.integration.demo.idempotent.config;
import com.luamas.thinkbridge.common.idempotent.key.IdempotentKeyStore;
import com.luamas.thinkbridge.common.idempotent.key.RedisIdempotentKeyStore;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import org.springframework.data.redis.core.StringRedisTemplate;
/**
* 这里使用redis作为缓存
*/
@Profile("idempotent")
@Configuration
public class ThinkBridgeIdempotentConfiguration {
@Bean
public IdempotentKeyStore idempotentKeyStore(StringRedisTemplate stringRedisTemplate) {
return new RedisIdempotentKeyStore(stringRedisTemplate);
}
}
标准 SpEL 表达式使用:
package com.luamas.thinkbridge.integration.demo.idempotent.service;
import com.luamas.thinkbridge.common.idempotent.annotation.Idempotent;
import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Component;
@Profile("idempotent")
@Component
public class UserService {
/
* 模拟保存用户
* prefix 前缀
* uniqueExpression 为el表达式,这里可以直接使用http请求的request对象
* duration 幂等的控制时长(秒),必须大于业务的处理耗时
*
* @param userId
/
@Idempotent(prefix = "test:save1", uniqueExpression = "#userId", duration = 100, removeKeyWhenFinished = false)
public String save1(String userId) {
return "success";
}
/
* 模拟保存用户
* prefix 前缀
* uniqueExpression 为el表达式,这里可以直接使用http请求的request对象
* duration 幂等的控制时长(秒),必须大于业务的处理耗时
* removeKeyWhenFinished 完成后是否取消锁定
* @param userId
*/
@Idempotent(prefix = "test:save2", uniqueExpression = "#request.getParameter('test')", duration = 100)
public String save2(String userId) {
try {
// 延时10秒,模拟业务
Thread.sleep(10000L);
} catch (InterruptedException ignored) {
}
return "success";
}
/*
* 模拟保存用户
* prefix 前缀
* uniqueExpression 为el表达式,这里可以直接使用http请求的request对象
* duration 幂等的控制时长(秒),必须大于业务的处理耗时
* removeKeyWhenFinished 完成后是否取消锁定
* @param userId
*/
@Idempotent(prefix = "test:save3", uniqueExpression = "#userId", duration = 100)
public String save3(String userId) {
return "success";
}
}