1.接口幂等介绍
接口幂等性是指同一个接口,多次发出同一个请求,必须保证操作只执行一次。即用户对于同一个接口发起的一次请求或者多次请求的结果是一致的,不会因为多次请求而产生不同的结果。
在应用中,如果一个接口没有设计成幂等的,那么每次请求可能会产生不同的结果,这可能会导致数据的不一致性。因此,在设计接口时,需要考虑接口的幂等性。
2.防止重复提交的几种方式
- 使用Redis记录请求信息:在用户提交请求时,将请求的唯一标识(如Token、接口名、请求参数等)存储到Redis中。如果Redis中已经存在相同的请求信息,说明该请求已经被处理过,可以直接返回结果或者给出相应的提示。
- 数据库表增加唯一索引约束:在数据库表中增加唯一索引约束,确保同一个请求的数据不会被重复插入。当有重复请求到达时,数据库会因为唯一索引约束而拒绝插入操作,从而防止重复提交。
- 请求唯一ID:为每个请求生成一个唯一的ID,并在处理请求时检查该ID是否已经存在。如果存在,说明该请求已经被处理过,直接返回结果或者给出相应的提示。
- 使用分布式锁:通过分布式锁机制,确保同一个请求在多个节点上不会被重复处理。当一个节点处理完请求后,会释放锁,其他节点在获取锁之前无法处理该请求,从而防止重复提交。
3.我采用的是 Redis
1.表单提交前获取Token
2.提交表单的时候参数或在请求头中带着Token就可以
4.集成Redis
SpringBoot配置文件
server: port: 9000 tomcat: basedir: /. spring: profiles: active: dev redis: host: 127.0.0.1 port: 6379 password:
pom.xml
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>provided</scope> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-configuration-processor</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-lang3</artifactId> <version>3.12.0</version> </dependency> <dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-all</artifactId> <version>5.8.16</version> </dependency>
5.Redis封装
package com.lp.redis; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.core.ValueOperations; import org.springframework.stereotype.Service; import javax.annotation.Resource; import java.io.Serializable; import java.util.concurrent.TimeUnit; /** * @author liu pei * @date 2023年12月18日 下午6:44 * @Description: 封装redis */ @Service public class RedisService { @Resource private RedisTemplate redisTemplate; /** * 写入缓存 * @param key * @param value * @return */ public boolean set(final String key, Object value) { boolean result = false; try { ValueOperations<Serializable, Object> operations = redisTemplate.opsForValue(); operations.set(key, value); result = true; } catch (Exception e) { e.printStackTrace(); } return result; } /** * 写入缓存设置时效时间 * @param key * @param value * @return */ public boolean setEx(final String key, Object value, Long expireTime) { boolean result = false; try { ValueOperations<Serializable, Object> operations = redisTemplate.opsForValue(); operations.set(key, value); redisTemplate.expire(key, expireTime, TimeUnit.SECONDS); result = true; } catch (Exception e) { e.printStackTrace(); } return result; } /** * 判断缓存中是否有对应的value * @param key * @return */ public boolean exists(final String key) { return redisTemplate.hasKey(key); } /** * 读取缓存 * @param key * @return */ public Object get(final String key) { Object result = null; ValueOperations<Serializable, Object> operations = redisTemplate.opsForValue(); result = operations.get(key); return result; } /** * 删除对应的value * @param key */ public boolean remove(final String key) { if (exists(key)) { Boolean delete = redisTemplate.delete(key); return delete; } return false; } }
6.自定义注解
它添加在需要实现幂等的方法上,凡是某个方法注解了它,都会实现自动幂等。后台利用反射如果扫描到这个注解,就会处理这个方法实现自动幂等,
package com.lp.ann; 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 FromAnnId { }
7.表单唯一ID,创建和唯一验证
package com.lp.service; import cn.hutool.core.util.StrUtil; import com.lp.redis.RedisService; import org.apache.commons.lang3.StringUtils; import org.springframework.stereotype.Service; import javax.annotation.Resource; import javax.servlet.http.HttpServletRequest; import java.util.UUID; /** * @author liu pei * @date 2023年12月18日 下午6:52 * @Description: */ @Service public class FromTokenService { @Resource private RedisService redisService; public static String FROM_KEY = "redis:from:token:"; public static String FROM_HEADER_KEY = "FROM-TOKEN-ID"; /** * 创建token * * @return */ public String createToken() { String str = UUID.randomUUID().toString(); try { String token = StringUtils.join(FROM_KEY,str); //设置默认过期时间:30分钟 redisService.setEx(token, str,30000L); boolean notEmpty = StrUtil.isNotEmpty(str); if (notEmpty) { return str; } }catch (Exception ex){ ex.printStackTrace(); } return null; } /** * 检验token * * @param request * @return */ public boolean checkToken(HttpServletRequest request) throws Exception { String formId = request.getHeader(FROM_HEADER_KEY); String token = StringUtils.join(FROM_KEY,formId); if (StrUtil.isBlank(formId)) {// header中不存在token formId = request.getParameter(FROM_HEADER_KEY); if (StrUtil.isBlank(formId)) {// parameter中也不存在token throw new RuntimeException("重复提交参数异常。"); } } if (!redisService.exists(token)) { throw new RuntimeException("提交已过期。"); } boolean remove = redisService.remove(token); if (!remove) { throw new RuntimeException("表单提交异常。"); } return true; } }
8.拦截器配置也可以用AOP(我用的是拦截器)
package com.lp.interceptor; import cn.hutool.json.JSONUtil; import com.lp.ann.FromAnnId; import com.lp.service.FromTokenService; import org.springframework.stereotype.Component; import org.springframework.web.method.HandlerMethod; import org.springframework.web.servlet.HandlerInterceptor; import org.springframework.web.servlet.ModelAndView; import javax.annotation.Resource; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.io.PrintWriter; import java.lang.reflect.Method; import java.util.HashMap; /** * @author liu pei * @date 2023年12月18日 下午7:12 * @Description: */ @Component public class AuthInterceptor implements HandlerInterceptor { @Resource private FromTokenService tokenService; /** * 预处理 * * @param request * @param response * @param handler * @return * @throws Exception */ @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(); //被ApiIdempotment标记的扫描 FromAnnId fromAnnId = method.getAnnotation(FromAnnId.class); if (fromAnnId != null) { try { return tokenService.checkToken(request);// 幂等性校验, 校验通过则放行, 校验失败则抛出异常, 并通过统一异常处理返回友好提示 }catch (Exception ex){ HashMap<String, Object> hashMap = new HashMap<>(); hashMap.put("msg",ex.getMessage()); hashMap.put("code",500); writeReturnJson(response, JSONUtil.toJsonStr(hashMap)); throw ex; } } //必须返回true,否则会被拦截一切请求 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 { } /** * 返回的json值 * @param response * @param json * @throws Exception */ private void writeReturnJson(HttpServletResponse response, String json) throws Exception{ PrintWriter writer = null; response.setCharacterEncoding("UTF-8"); response.setContentType("text/html; charset=utf-8"); try { writer = response.getWriter(); writer.print(json); } catch (IOException e) { } finally { if (writer != null) writer.close(); } } }
package com.lp.config; import com.lp.interceptor.AuthInterceptor; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter; import javax.annotation.Resource; /** * @author liu pei * @date 2023年12月18日 下午7:09 * @Description: */ @Configuration public class WebConfiguration extends WebMvcConfigurerAdapter { @Resource private AuthInterceptor authInterceptor; /** * 添加拦截器 * @param registry */ @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(authInterceptor); super.addInterceptors(registry); } }
9.测试
1.使用创建Token接口创建一个token
2.使用FROM-TOKEN-ID在save()方法中填写请求头参数。
例如:FROM-TOKEN-ID:token
package com.lp.controller; import com.lp.service.FromTokenService; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import javax.annotation.Resource; import java.util.Map; /** * @author liu pei * @date 2023年12月18日 下午7:18 * @Description: */ @RestController @RequestMapping("/redis") public class TestController { @Resource private FromTokenService tokenService; @RequestMapping("/token") public Object fromToken(){ return tokenService.createToken(); } @RequestMapping("/save") @FromAnnId public Object save(@RequestBody Map<String,Object> user){ System.out.println(user); return user; } }