前言
在实际的开发项目中,一个对外暴露的接口往往会面临很多次请求,我们来解释一下幂等的概念:任意多次执行所产生的影响均与一次执行的影响相同。按照这个含义,最终的含义就是 对数据库的影响只能是一次性的,不能重复处理。如何保证其幂等性,通常有以下手段:
1、数据库建立唯一性索引,可以保证最终插入数据库的只有一条数据。
2、token机制,每次接口请求前先获取一个token,然后再下次请求的时候在请求的header体中加上这个token,后台进行验证,如果验证通过删除token,下次请求再次判断token。
3、悲观锁或者乐观锁,悲观锁可以保证每次for update的时候其他sql无法update数据(在数据库引擎是innodb的时候,select的条件必须是唯一索引,防止锁全表)
4、先查询后判断,首先通过查询数据库是否存在数据,如果存在证明已经请求过了,直接拒绝该请求,如果没有存在,就证明是第一次进来,直接放行。
redis 实现自动幂等的原理图:
搭建 Redis 服务 API
1、首先是搭建redis服务器。
2、引入springboot中到的redis的stater,或者Spring封装的jedis也可以,后面主要用到的api就是它的set方法和exists方法,这里我们使用springboot的封装好的redisTemplate。
推荐一个 Spring Boot 基础教程及实战示例:
/**java项目 fhadmin.cn
* redis工具类
*/
@Component
public class RedisService {
@Autowired
private RedisTemplate redisTemplate;
/**
* 写入缓存
* @param key
* @param value
* @return
*/
public boolean set(finalString 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(finalString 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(finalString key) {
return redisTemplate.hasKey(key);
}
/**
* 读取缓存
* @param key
* @return
*/
public Objectget(finalString key) {
Object result = null;
ValueOperations<Serializable, Object> operations = redisTemplate.opsForValue();
result = operations.get(key);
return result;
}
/**
* 删除对应的value
* @param key
*/
public boolean remove(finalString key) {
if (exists(key)) {
Boolean delete = redisTemplate.delete(key);
return delete;
}
returnfalse;
}
}
自定义注解 AutoIdempotent
自定义一个注解,定义此注解的主要目的是把它添加在需要实现幂等的方法上,凡是某个方法注解了它,都会实现自动幂等。后台利用反射如果扫描到这个注解,就会处理这个方法实现自动幂等,使用元注解ElementType.METHOD表示它只能放在方法上,etentionPolicy.RUNTIME表示它在运行时。
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface AutoIdempotent {
}
token 创建和检验
token服务接口:我们新建一个接口,创建token服务,里面主要是两个方法,一个用来创建token,一个用来验证token。创建token主要产生的是一个字符串,检验token的话主要是传达request对象,为什么要传request对象呢?主要作用就是获取header里面的token,然后检验,通过抛出的Exception来获取具体的报错信息返回给前端。
publicinterface TokenService {
/**java项目 fhadmin.cn
* 创建token
* @return
*/
public String createToken();
/**
* 检验token
* @param request
* @return
*/
public boolean checkToken(HttpServletRequest request) throws Exception;
}
token的服务实现类:token引用了redis服务,创建token采用随机算法工具类生成随机uuid字符串,然后放入到redis中(为了防止数据的冗余保留,这里设置过期时间为10000秒,具体可视业务而定),如果放入成功,最后返回这个token值。checkToken方法就是从header中获取token到值(如果header中拿不到,就从paramter中获取),如若不存在,直接抛出异常。这个异常信息可以被拦截器捕捉到,然后返回给前端。
@Service
publicclass TokenServiceImpl implements TokenService {
@Autowired
private RedisService redisService;
/**
* 创建token
* java fhadmin.cn
* @return
*/
@Override
public String createToken() {
String str = RandomUtil.randomUUID();
StrBuilder token = new StrBuilder();
try {
token.append(Constant.Redis.TOKEN_PREFIX).append(str);
redisService.setEx(token.toString(), token.toString(),10000L);
boolean notEmpty = StrUtil.isNotEmpty(token.toString());
if (notEmpty) {
return token.toString();
}
}catch (Exception ex){
ex.printStackTrace();
}
returnnull;
}
/**
* 检验token
*
* @param request
* @return
*/
@Override
public boolean checkToken(HttpServletRequest request) throws Exception {
String token = request.getHeader(Constant.TOKEN_NAME);
if (StrUtil.isBlank(token)) {// header中不存在token
token = request.getParameter(Constant.TOKEN_NAME);
if (StrUtil.isBlank(token)) {// parameter中也不存在token
thrownew ServiceException(Constant.ResponseCode.ILLEGAL_ARGUMENT, 100);
}
}
if (!redisService.exists(token)) {
thrownew ServiceException(Constant.ResponseCode.REPETITIVE_OPERATION, 200);
}
boolean remove = redisService.remove(token);
if (!remove) {
thrownew ServiceException(Constant.ResponseCode.REPETITIVE_OPERATION, 200);
}
returntrue;
}
}
拦截器的配置
web配置类,实现WebMvcConfigurerAdapter,主要作用就是添加autoIdempotentInterceptor到配置类中,这样我们到拦截器才能生效,注意使用@Configuration注解,这样在容器启动是时候就可以添加进入context中。
@Configuration
publicclass WebConfiguration extends WebMvcConfigurerAdapter {
@Resource
private AutoIdempotentInterceptor autoIdempotentInterceptor;
/**
* 添加拦截器
* @param registry
*/
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(autoIdempotentInterceptor);
super.addInterceptors(registry);
}
}
拦截处理器:主要的功能是拦截扫描到AutoIdempotent到注解到方法,然后调用tokenService的checkToken()方法校验token是否正确,如果捕捉到异常就将异常信息渲染成json返回给前端。
/** fhadmin.cn
* 拦截器
*/
@Component
publicclass AutoIdempotentInterceptor implements HandlerInterceptor {
@Autowired
private TokenService 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)) {
returntrue;
}
HandlerMethod handlerMethod = (HandlerMethod) handler;
Method method = handlerMethod.getMethod();
//被ApiIdempotment标记的扫描
AutoIdempotent methodAnnotation = method.getAnnotation(AutoIdempotent.class);
if (methodAnnotation != null) {
try {
return tokenService.checkToken(request);// 幂等性校验, 校验通过则放行, 校验失败则抛出异常, 并通过统一异常处理返回友好提示
}catch (Exception ex){
ResultVo failedResult = ResultVo.getFailedResult(101, ex.getMessage());
writeReturnJson(response, JSONUtil.toJsonStr(failedResult));
throw ex;
}
}
//必须返回true,否则会被拦截一切请求
returntrue;
}
@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();
}
}
}
测试用例
模拟业务请求类,首先我们需要通过/get/token路径通过getToken()方法去获取具体的token,然后我们调用testIdempotence方法,这个方法上面注解了@AutoIdempotent,拦截器会拦截所有的请求,当判断到处理的方法上面有该注解的时候,就会调用TokenService中的checkToken()方法,如果捕获到异常会将异常抛出调用者,下面我们来模拟请求一下:
@RestController
publicclass BusinessController {
@Resource
private TokenService tokenService;
@Resource
private TestService testService;
@PostMapping("/get/token")
public String getToken(){
String token = tokenService.createToken();
if (StrUtil.isNotEmpty(token)) {
ResultVo resultVo = new ResultVo();
resultVo.setCode(Constant.code_success);
resultVo.setMessage(Constant.SUCCESS);
resultVo.setData(token);
return JSONUtil.toJsonStr(resultVo);
}
return StrUtil.EMPTY;
}
@AutoIdempotent
@PostMapping("/test/Idempotence")
public String testIdempotence() {
String businessResult = testService.testIdempotence();
if (StrUtil.isNotEmpty(businessResult)) {
ResultVo successResult = ResultVo.getSuccessResult(businessResult);
return JSONUtil.toJsonStr(successResult);
}
return StrUtil.EMPTY;
}
}
使用postman请求,首先访问get/token路径获取到具体到token:
利用获取到到token,然后放到具体请求到header中,可以看到第一次请求成功,接着我们请求第二次:
第二次请求,返回到是重复性操作,可见重复性验证通过,再多次请求到时候我们只让其第一次成功,第二次就是失败:
相关推荐
首先,我们需要理解表单重复提交的问题。当用户点击提交按钮多次或者由于网络延迟导致的重复提交,服务器可能会接收到相同的数据请求多次,这可能会对业务逻辑产生不良影响。例如,在电商网站中,如果用户多次点击...
在现代Web应用中,防止表单...总的来说,结合Redis和Spring Boot可以有效地解决Web应用中的表单重复提交问题,提供了健壮性和可扩展性。在实际项目中,可以根据具体需求进行优化和调整,以满足更高的性能和安全性要求。
基于 SpringBoot 实现表单重复提交解决方案 表单重复提交是指在一次请求完成之前防止重复提交,解决表单重复提交有多种形式,以下以 Aop+自定义注解+Redis 为例来介绍。 解决方案的详细流程 1. 当页面加载时,...
总结起来,结合Spring Boot、Redis和AOP,我们可以构建出一个优雅的解决方案,防止表单的重复提交,保障系统的稳定性和数据一致性。这种方法充分利用了Spring Boot的便利性、Redis的高速缓存能力和AOP的代码组织优势...
Spring Boot 防止重复提交是指在用户提交表单或请求时,防止同一客户端在短时间内对同一 URL 的重复提交,从而避免服务器端的处理压力和数据的一致性问题。下面将详细介绍 Spring Boot 防止重复提交实现方法的相关...
为了解决这一问题,我们可以采用自定义注解结合Redis来实现一个防止表单重复提交的解决方案。 首先,让我们理解自定义注解的核心思想。注解是一种元数据,它提供了在代码中添加信息的方式,这些信息可以被编译器或...
接口上面加上@NoRepeatSubmitAspect这个注解即可轻松完美解决重复提交问题,这个是Redis版本,性能最好,RedisUtils静态工具类也一并打包在内。如果项目不用redis,可以自行改成数据库查存校验!
这个名为"springboot防重复提交工具包"的资源很可能提供了一种解决方案,帮助开发者防止用户因网络延迟或其他原因导致的多次点击提交,从而避免数据库中的数据异常。 Spring Boot是基于Spring框架的轻量级开发工具...
在日常开发中,我们经常会遇到需要防止重复提交和操作幂等的问题,本文将记录 SpringBoot 实现简单防重放与幂等的方法。 防重放是指防止数据重复提交,例如用户多次点击提交按钮或接口短时间内被多次调用。操作幂等...
系统的特点 高性能:秒杀涉及大量的并发读和并发写,因此支持高并发访问这点非常关键 一致性:秒杀商品减库存的实现方式同样关键,有限数量的商品在同一时刻被很多倍的请求同时来减...禁止重复提交:限定每个用户发起
本项目"基于注解+redis实现表单防重复提交.zip"提供了一种利用SpringBoot框架来解决这一问题的方法。这里我们将深入探讨如何通过注解和Redis缓存技术来防止表单的重复提交。 首先,SpringBoot是一个快速开发框架,...
标题中的"springboot和redis以及通用mapper等的一个结合框架.zip"指的是一个整合了Spring Boot、Redis和通用Mapper的Java开发框架。这个框架旨在简化开发流程,提高开发效率,特别是对于处理数据存储和缓存操作的...
springboot一个注解防重复提交,实现原理是使用spring的aop功能,允许用户使用SPEL表达式设置防重key,支持自定义超时时间,结合redis实现分布式防重复
为了解决这个问题,Spring Boot 提供了一些方法来防止重复提交。 首先,从数据库方面考虑,可以通过设置唯一索引来避免脏数据的产生。例如,在用户表中,可以设置用户名或邮箱为唯一索引,以防止重复提交相同的用户...
Spring Boot 使用 AOP 防止重复提交的方法示例 在传统的 Web 项目中,防止重复提交的方法通常是:后端生成一个唯一的提交令牌(uuid),并存储在服务端。页面提交请求携带这个提交令牌,后端验证并在第一次验证后...
总的来说,"avoidRepeatSubmit.rar"提供的组件结合了自定义注解、AOP拦截和Redis的setNX功能,为SpringBoot应用提供了一套完整的防重复提交解决方案。开发者可以参考其中的代码和示例,理解其工作原理,并将其应用于...
如何设计一个秒杀系统系统的特点高效:秒杀涉及大量的并发读和并发写,因此支持高并发访问这点非常关键一致...前端答题或验证码,来分散用户的请求禁止重复提交:限定每个用户发起一次秒杀后,需等待才可以发起另一次请
案例3:自定义注解+拦截器解决表单重复提交 博客地址: : 案例4:SpringBoot利用@Scheduled创建定时任务 博客地址: : 案例5:自定义注解+拦截器?秒防刷新 博客地址: : 案例6:发送文本邮件,html邮件,带附件...
SpringBoot配置详解 SpringBoot日志配置 SpringBoot整合Thymeleaf模板 使用JdbcTemplate访问数据库 ...重复提交(分布式锁) 重复提交(本地锁) WebSocket 安全框架(Shiro) 分布式限流 集成hadoop、hive、oozie
在`RepeatSubmitAspect`的`around`方法中,我们可以实现具体的防止重复提交的逻辑,比如利用Redis存储请求信息并判断是否在指定时间内重复提交。这种设计模式在Spring Boot应用中非常常见,因为它提供了很好的灵活性...