spring-cloud-gateway如何做防重复提交

653 阅读2分钟

之前讲过如何用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);
            }