最近工作中需要实现一个防止重复提交的任务,其本质就是需要保证接口的幂等性。接口幂等性就是用户对于同一操作发起的一次请求或者多次请求的结果是一致的,不会因为多次点击而产生了副作用。
采用的方案包括了两部分,一部分核心的业务逻辑使用token校验,另一部分使用数据库的唯一索引来防止重复更改数据库。
先来说说使用数据库唯一索引的方案,使用数据库唯一索引的方案可以防止数据库表中在唯一索引字段相同的情况下去更改数据库,而更改数据库的操作,其中只有新增操作(INSERT语句)和计算式的更新操作(类似 UPDATE table SET a = a + 1 WHERE a = 0)会产生幂等性的问题,其余的情况都是具有天然幂等性的,我们不需要考虑。当我们在数据库表中加入了唯一索引之后,使用INSERT IGNORE INTO 语句就可以实现如果唯一索引冲突,则该条记录不会被插入到数据库中。
既然是通过数据库去做防重校验,那么在高并发的场景下必然会产生数据库压力较大的问题,这种情况下就需要对请求进行限流。做法是在网关(我这里使用的网关是Spring Cloud Gateway)中实现GlobalFilter接口,匹配满足特殊关键字的Url或是在配置文件中的Url,将IP与Url拼接成key存入到Redis中,并设置一个较短的过期时间,在过期时间内重复请求的话就提示,进行限流操作,代码如下:
@Component
public class RateLimiterFilter implements GlobalFilter, Ordered {
private Logger logger = LoggerFactory.getLogger(RateLimiterFilter.class);
private static final Integer RETRY_CODE = 604;
private static final Integer FAILED_CODE = 608;
@Autowired
RedisUtils redisUtils;
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
//获取请求机器IP地址
String address = exchange.getRequest().getRemoteAddress().getAddress().toString();
logger.info("请求机器IP地址:{}", address);
//获取请求路径
String path = exchange.getRequest().getPath().toString();
logger.info("请求路径:{}" , path);
//请求路径中是否模糊匹配save、update、insert(不区分大小写)
String pattern = ".*save.*|.*update.*|.*insert.*";
Pattern r = Pattern.compile(pattern,Pattern.CASE_INSENSITIVE);
Matcher m = r.matcher(path);
boolean isExist = false;
//判断请求路径在配置限流的文件中是否存在
for (String url : readFileAsList()) {
if (url.equals(path)) {
isExist = true;
break;
}
}
//若请求路径匹配到了正则表达式(包含save、update、insert关键字)或在配置限流的文件中存在
if(m.matches() || isExist){
ServerHttpResponse response = exchange.getResponse();
StringBuffer sb = new StringBuffer();
String redisKey