业务开发中,经常会遇到重复提交的情况,无论是由于网络问题无法收到请求结果而重新发起请求,或是前端的操作抖动而造成重复提交情况。 在交易系统,支付系统这种重复提交造成的问题有尤其明显,比如:
提示:以下是本篇文章正文内容,下面案例可供参考
用户对于同一操作,无论是发起一次请求还是多次请求,最终的执行结果是一致的,不会因为多次点击而产生副作用。
update直接更新某个值时,幂等;
update更新累加操作的的结果,非幂等;
insert操作会每次都新增一条,非幂等;
连续点击提交两次按钮;
点击刷新按钮;
使用浏览器后退按钮重复之前的操作,导致重复提交表单;
使用浏览器历史记录重复提交表单;
浏览器重复地HTTP请求等。
在提交后执行页面重定向,这就是所谓的Post-Redirect-Get (PRG)模式。简言之,当用户提交了表单后,你去执行一个客户端的重定向,转到提交成功信息页面。
这能避免用户按F5导致的重复提交,而其也不会出现浏览器表单重复提交的警告,也能消除按浏览器前进和后退按导致的同样问题。
引入spring-boot-starter-data-redis依赖,通过RedisTemplate操作Redis缓存。
代码如下(示例):
@Slf4j
@Component
public class RedisService {
@Resource
private RedisTemplate redisTemplate;
/**
* 写入缓存
* @param key
* @param value
*/
public void set(final String key, Object value){
try {
ValueOperations operations = redisTemplate.opsForValue();
operations.set(key, value);
} catch (Exception e) {
log.error(e.getMessage(), e);
throw new RuntimeException("数据缓存至redis失败");
}
}
/**
* 写入缓存,设置超时时间
* @param key
* @param value
* @param expireTime
*/
public void setEx(final String key, Object value, Long expireTime){
try{
redisTemplate.opsForValue().set(key, value, expireTime, TimeUnit.SECONDS);
}catch (Exception e) {
log.error(e.getMessage(), e);
throw new RuntimeException("数据缓存至redis失败");
}
}
/**
* 判断缓存中是否存在key
* @param key
* @return
*/
public boolean exists(final String key){
return redisTemplate.hasKey(key);
}
/**
* 读取缓存
* @param key
* @return
*/
public Object get(final String key){
return redisTemplate.opsForValue().get(key);
}
public boolean remove(final String key){
return redisTemplate.delete(key);
}
}
创建token接口,用来创建和校验token。
代码如下(示例):
public interface TokenService {
/**
* 创建token
* @return
*/
public String createToken();
/**
* 校验token
* @param request
* @return
* @throws Exception
*/
public boolean checkToken(HttpServletRequest request) throws Exception;
}
代码如下(示例):
@Service
public class TokenServiceImpl implements TokenService {
private final RedisService redisService;
public TokenServiceImpl(RedisService redisService){
this.redisService = redisService;
}
@Override
public String createToken() {
try{
String token = RedisConst.REDIS_AUTO_IDEMPOTENT_PREFIX+RandomUtil.randomBigDecimal().toString();
redisService.setEx(token, token, 1000L);
return token;
}catch (Exception e){
e.printStackTrace();
}
return null;
}
@Override
public boolean checkToken(HttpServletRequest request) throws Exception {
String token = request.getHeader(RedisConst.AUTO_IDEMPOTENT_TOKEN_NAME);
if(StrUtil.isBlank(token)){
token = request.getParameter(RedisConst.AUTO_IDEMPOTENT_TOKEN_NAME);
}
if(StrUtil.isNotBlank(token) && redisService.remove(token)){
return true;
}
return false;
}
}
自定义一个注解,定义此注解的主要目的是把它添加在需要实现幂等的方法上,凡是某个方法注解了它,都会实现自动幂等。
代码如下(示例):
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface AutoIdempotent {
}
@Target——表示该注解用于什么地方。默认值为任何元素,表示该注解用于什么地方。可用的ElementType 参数包括(1:N,一个target可以包含多个ElementType):
ElementType.CONSTRUCTOR
: 用于描述构造器ElementType.FIELD
: 成员变量、对象、属性(包括enum实例)ElementType.LOCAL_VARIABLE
: 用于描述局部变量ElementType.METHOD
: 用于描述方法ElementType.PACKAGE
: 用于描述包ElementType.PARAMETER
: 用于描述参数ElementType.TYPE
: 用于描述类、接口(包括注解类型) 或enum声明@Retention——定义该注解的生命周期
RetentionPolicy.SOURCE
: 在编译阶段丢弃。这些注解在编译结束之后就不再有任何意义,所以它们不会写入字节码。@Override
,@SuppressWarnings
都属于这类注解。RetentionPolicy.CLASS
: 在类加载的时候丢弃。在字节码文件的处理中有用。注解默认使用这种方式RetentionPolicy.RUNTIME
: 始终不会丢弃,运行期也保留该注解,因此可以使用反射机制读取该注解的信息。我们自定义的注解通常使用这种方式。
主要的功能是拦截扫描AutoIdempotent注解到的方法,然后调用tokenService的checkToken()方法校验token是否正确,进行业务接口的放行及拦截。
代码如下(示例):
@Slf4j
@Component
public class AutoIdempotentInterceptor implements HandlerInterceptor {
private final TokenService tokenService;
public AutoIdempotentInterceptor(TokenService tokenService){
this.tokenService = tokenService;
}
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
if(!(handler instanceof HandlerMethod)){
return true;
}
HandlerMethod handlerMethod = (HandlerMethod)handler;
Method method = handlerMethod.getMethod();
//扫描包含AutoIdempotent注解的方法
AutoIdempotent autoIdempotent = method.getAnnotation(AutoIdempotent.class);
if(null != autoIdempotent){
return tokenService.checkToken(request);
}
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
}
}
通过实现WebMvcConfigurer,实现对拦截器的加载及设置拦截器的过滤路径规则。
代码如下(示例):
@Configuration
public class WebConfiguration implements WebMvcConfigurer {
@Resource
private AutoIdempotentInterceptor autoIdempotentInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(autoIdempotentInterceptor).addPathPatterns("/doudou/**");
}
}
代码如下(示例):
@Slf4j
@RestController
@RequestMapping("doudou")
public class IdempotentController {
private final TokenService tokenService;
public IdempotentController(TokenService tokenService){
this.tokenService = tokenService;
}
@ApiOperation(value = "获取Idempotent token", notes = "获取Idempotent token")
@GetMapping("/getToken")
public ResponseEntity>> getToken() {
log.info("获取token IdempotentController getToken。");
Map resultMap = Maps.newHashMap();
resultMap.put("idempToken", tokenService.createToken());
return ResponseEntity.ok(Response.ok(resultMap));
}
}
代码如下(示例):
@Slf4j
@RestController
@RequestMapping("doudou")
public class TestController {
private final RedisService redisService;
public TestController(RedisService redisService){
this.redisService = redisService;
}
@ApiOperation(value = "首个测试接口", notes = "首个测试接口")
@AutoIdempotent
@GetMapping("/test-1")
public ResponseEntity>> graphSearch(@ApiParam(value = "节点类型") @RequestParam String label) {
log.info("我被调用了:{}", label);
Map resultMap = Maps.newHashMap();
redisService.setEx(label, label, 100L);
return ResponseEntity.ok(Response.ok(resultMap));
}
}
首先访问/getToken路径获取到具体到token:
利用获取到到token,然后放到具体请求到header中,可以看到第一次请求成功:
第二次请求,返回到是重复性操作,可见重复性验证通过,再多次请求到时候我们只让其第一次成功,第二次就是失败:
暂未对异常进行捕获
总结
本文对幂等性的概念以及常见的解决接口幂等性的方式进行了介绍,同时对通过spring boot、拦截器、自定义注解、Redis优雅的实现了Redis Token接口幂等方案。