之前讲过如何用spring-cloud-gateway实现类nginx反向代理功能《spring-cloud-gateway 类nginx反向代理 无注册中心实现负载均衡》,但使用gateway肯定不只是替代nginx,而是要加入更多功能,因为spring-cloud-gateway为java开发,加功能比nginx要方便很多。
在网关上做防重复提交,可以让负责做业务开发的同事,尽可能的把心思放在业务上。举个例子,要做一个签到功能,但每天最多只能签到6次,业务同事在实现签到功能时,先查一下数据库里面已经签到多少次,如果有6次就不签到,没有6次就签到。但如果两次签到间隔特别的短,就有可能出现连续请求,第一次查询结果还没出,第二次的请求又来了,导致超签。
如果不考虑网关的话,可以使用 Redisson +redis 分布式锁来完成,
public void checkIn() {
RLock rLock = redissonClient.getLock("PATH"+"USERID");
rLock.lock(10, TimeUnit.SECONDS);//最多锁10秒,这个时间足够完成任务
/////////业务
rLock.unlock();//解锁
}
其中PATH代表当前的路径,USERID代表当前用户的id,通过 Redisson 内置的lock就可以实现分布锁,使用lock后,业务就好像排队一样,必须前一个执行完,后面才能执行,这样一来,重复签到的问题就没了,而现在要做的就是把这功能在网关里实现。
spring-cloud-gateway 有两个版本,一个是非阻塞,一个阻塞的,阻塞的只建议在jdk21及以上版本使用,因为有了虚拟线程,性能比非阻塞的也没差多少,下面介绍非阻塞是怎么写的。
首先引入 redisson-spring-boot-starter。 具体版本选择可以参考
redisson/redisson-spring-boot-starter at master · redisson/redisson 和 redisson/redisson-spring-data at master · redisson/redisson
然后在配置文件里面把 Redisson 要连接的redis地址配置好,就可以开始使用了。
先建一个防重复请求的工具类
public class RepeatedRequest {
public static GatewayFilter repeatedRequest(RedissonReactiveClient redissonReactiveClient) {
return (exchange, chain) -> {
ServerHttpRequest serverHttpRequest = exchange.getRequest();
String path = serverHttpRequest.getPath().value();//获取当前路径
String token = serverHttpRequest.getHeaders().getFirst("userToken"); //用户标识放在请求头里面
String key = path + "-" + token;
RLockReactive rlockReactive = redissonReactiveClient.getLock(key);
//锁一分钟
return rlockReactive.lock(1, TimeUnit.MINUTES).then(chain.filter(exchange)).doFinally((d) -> {
rlockReactive.forceUnlock().subscribe();//解锁
});
};
}
}
路由处代码为,这里以百度为例
@Autowired
private RedissonReactiveClient redissonReactiveClient;
@Bean
public RouteLocator routes2(RouteLocatorBuilder builder) {
//配置防重复提交逻辑
return builder.routes().route("",r->r.path("/baidu").filters(f->f.filter(RepeatedRequest.repeatedRequest(redissonReactiveClient))).uri("https://www.baidu.com")).build();
}
以上代码是根据用户ID以及路径做的锁,锁显得有些粗糙,其实可以做约定,有操作行为的采用POST提交,对于GET提交则直接放过,减小redis压力
HttpMethod httpMethod = exchange.getRequest().getMethod();
if(httpMethod==HttpMethod.GET){
return chain.filter(exchange);
}