SpringBoot结合Redis处理重复请求
数据重复提交导致多次请求服务、入库,产生脏数据、冗余数据等情况。禁止重复提交使我们保证数据准确性及安全性的必要操作。
实际上,造成这种情况的场景不少:
1.网络波动:因为网络波动,造成重复请求。
2.用户的重复性操作:用户误操作,或者因为接口响应慢,而导致用户耐性消失,有意多次触发请求。
3.重试机制:这种情况,经常出现在调用三方接口的时候。对可能出现的异常情况抛弃,然后进行固定次数的接口重复调用,直到接口返回正常结果。
4.分布式消息消费:任务发布后,使用分布式消息服务来进行消费。
接口幂等性
接口幂等性就是用户对于同一操作发起的一次请求或者多次请求的结果是一致的,不会因为多次点击而产生了副作用。
应用场景:用户多次点击新增按钮,服务器会执行很多相同的请求,虽然前端可以将按钮制灰也可以实现,但是有些人知道接口地址后会对某个接口用程序频繁发请求导致服务器崩溃。为了解决这个问题就要实现接口的幂等性。
旧的笔记
方案
方案:同一客户端2s内请求同样的url,即视为重复提交。
在请求进入业务方法之前,进入切面。以sessionId+url作为key,在redis中查询,看是否存在。存在即为重复提交。
若重复提交则抛出异常。否则正常处理业务逻辑。
AOP
本文面向切面拦截的是类的元数据(包、类、方法名、参数等)
相对于拦截器更加细致,而且非常灵活,拦截器只能针对URL做拦截,而AOP针对具体的代码,能够实现更加复杂的业务逻辑。
AOP(Aspect Oriented Programming) 面向切面编程,通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术。AOP是Spring框架中的一个重要内容,它通过对既有程序定义一个切入点,然后在其前后切入不同的执行内容,比如常见的有:打开数据库连接/关闭数据库连接、打开事务/关闭事务、记录日志等。基于AOP不会破坏原来程序逻辑,因此它可以很好的对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。
自定义注解
自定义一个注解用于拦截标识(作用到方法上的注解)
/**
* 防止重复提交注解
*/
@Target(ElementType.METHOD) //作用到方法上
@Retention(RetentionPolicy.RUNTIME)
public @interface NoRepeatSubmit
{
String value() default "";
}
切面
AOP的默认配置属性,其中spring.aop.auto属性默认是开启的,也就是说只要引入了AOP依赖后,默认已经增加了@EnableAspectJAutoProxy。
@Component
@Aspect
public class NoRepeatSubmitAop {
@Autowired
TokenService tokenService;
@Pointcut("execution(* com.example.demo9.*.*(..)) && @annotation(com.example.demo9.NoRepeatSubmit) && !execution(public * com.example.demo9.TestController.*(..))")
public void verifyRequestToken()
{
}
/***
* 校验是否为重复提交
* @param joinPoint
* @throws Exception
*/
@Before("verifyRequestToken()")
public void executeVerify(JoinPoint joinPoint) throws Exception
{
ServletRequestAttributes attrubutes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
String sessionId = RequestContextHolder.getRequestAttributes().getSessionId();
HttpServletRequest request = attrubutes.getRequest();
// 判断是否重复提交(使用sessionId+url机制)
String key = sessionId + "-" + request.getServletPath();
tokenService.checkTokenBySessionAndUrl(key);
}
}
Controller
@RestController
public class TestController {
@Autowired
TokenService tokenService;
@RequestMapping("/test")
public String test(String token) throws Exception
{
tokenService.checkTokenBySessionAndUrl(token);
return "test duplicate request";
}
}
TokenServiceImpl
@Service
public class TokenServiceImpl implements TokenService {
@Autowired
StringRedisTemplate redisTemplate;
@Override
public void checkTokenBySessionAndUrl(String key) throws Exception
{
/**
* 提交的session+url已经存在redis 视为重复提交
* @param key
*/
if(StringUtils.isEmpty(key)){
System.out.println("key 为空");
}
if(redisTemplate.opsForValue().get(key) == null)
{
//放入redis缓存,设置超时时间3s
redisTemplate.opsForValue().setIfAbsent(key, key, 3, TimeUnit.SECONDS);
System.out.println("key 放入redis缓存");
}else {
System.out.println("throw new Exception 重复提交");
//throw new Exception("[Exception]重复提交");
}
}
}
Redis Cluster查询
[root@localhost ~]# redis-cli -c -h 192.168.236.128 -p 6379 -a 123456
192.168.236.128:6379> keys *
1) "token123"
192.168.236.128:6379> keys * (3秒后)
(empty list or set)
[root@localhost ~]# redis-cli -c -h 192.168.236.128 -p 6380 -a 123456
192.168.236.128:6380> keys *
1) "token456"
192.168.236.128:6380> keys * (3秒后)
(empty list or set)
参考
Spring AOP中pointcut expression表达式解析 及匹配多个条件
https://www.cnblogs.com/rainy-shurun/p/5195439.html
RequestContextHolder简析
https://www.jianshu.com/p/80165b7743cf