网站首页 > 教程文章 正文
业务背景
在业务上有很多需要防止重复提交的场景,例如大部分的创建方法要求同样的数据不能创建两次。对于此种业务处理一般可以分为前端处理和后端处理。前端可以在点击后将按钮置灰1s,做防抖处理,1s后才可以再次调用接口。后端这里需要在业务上做处理,我们在做入库操作时,需要校验:
待插入数据在数据库中是否存在?
存在则不能插入
不存在则可插入
重复提交的场景一般是同一个用户连续的点击按钮2次以上,那么这里出现重复提交的条件为:
同一用户
短时间内操作多次
那么为什么短时间多次操作就能出现多次插入呢,我们在插入时后端不是先查数据库做校验了么。
原来我们在短时间操作同一接口,虽然会先查询数据库,但是可能操作1还没有完成,操作2就开始了。操作1和操作2查询的数据就可能是一样的。
这个问题在面试时也经常会被问到:
如何实现接口的幂等性?
幂等要求我们多次操作,其产生的结果要跟一次操作一样。防重复提交就属于幂等问题。
对于保证幂等性,解决方案有很多。比如采用数据库的唯一索引,Redis相同Key是否有值,在查库时使用锁,使用Semaphore限流等等。
Redis实现
今天我们采用Redis限流操作来控制实现接口幂等。主要操作为:
相同key调用的接口,给对应值+1
在指定范围内,值小于指定数,则接口可调用
说干就干,我们先定义一个注解RateLimiter,用在需要防重复提交的方法上。RateLimiter定义如下:
这个注解我们要注意几个元素:
needUserLimit() //key设定为 接口名称 + userId
limit()//单位时间限制通过的请求数
expire()//过期时间,单位s
这里我们利用Redis的过期时间,在过期时间内请求数不超过指定的limit()数,则接口可以执行,否则接口执行前会被拦截。我们使用接口全路径名称+登录用户的id作为Redis的key。limit()和expire()可以使用默认值,即1秒内只能执行一次接口。
来看看如何实现这个注解:
我们写一个RateLimiterHandler类,在注入时加载Lua脚本
@PostConstruct
public void init() {
getRedisScript = new DefaultRedisScript<>();
getRedisScript.setResultType(String.class);
getRedisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("rateLimiter.lua")));
log.info(">>>>>>>>>>>>>>>>RateLimiterHandler[分布式限流处理器] lua脚本加载完成");
}
rateLimiter.lua脚本如下:
这个lua脚本主要做自增操作,当自增的值操作指定次数时,返回0,也就是false。否则返回1。
在RateLimiterHandler中如果我们按用户限流。needUserLimit需要设定为true。用于存Redis的key为:
固定前缀 + 方法全路径 + 登录用户id
代码如下:
boolean needUserLimit = rateLimiter.needUserLimit();
if (needUserLimit) {
//获取目标方法名(目标类型+方法名)
String targetClsName = targetCls.getName();
String targetObjectMethodName = targetClsName + "." + signature.getName();
Long userId = getCurrentUserId();
Preconditions.checkNotNull(userId);
limitKey = "redis:limit:".concat(targetObjectMethodName).concat(":").concat(String.valueOf(userId));
}
然后之心lua脚本:
String resultStr = stringRedisTemplate.execute(getRedisScript, Collections.singletonList(limitKey), String.valueOf(expireTimes), String.valueOf(limitTimes));
long result = resultStr == null ? 0 : Long.parseLong(resultStr);
StringBuilder sb = new StringBuilder();
if (result == 0) {
String msg = sb.append("超过单位时间=").append(expireTimes).append("允许的请求次数=").append(limitTimes).append("[触发限流]").toString();
log.info("key:[{}],{}", limitKey, msg);
throw new BusinessException(String.format("您的操作过于频繁,请在%s秒后再进行操作", expireTimes));
}
如果执行脚本返回0,我们给出提示:
您的操作过于频繁,请在%s秒后再进行操作
单元测试
代码到这里就结束了,其实思路也比较简单,我们写一个单元测试试试:
@ResponseBody
@RequestMapping("ratelimiter")
@RateLimiter(needUserLimit = true)
public String testLimit() {
return "限流注解测试专用";
}
运行上面的方法:
@Test
public void testPage() throws InterruptedException {
payCommonController.testLimit();
payCommonController.testLimit();
payCommonController.testLimit();
}
我们连续执行3次目标方法,发现控制台已有提示。
Redis上我们也看到了对应的key。
我们将调用时间间隔为:2s
@Test
public void testPage() throws InterruptedException {
payCommonController.testLimit();
Thread.sleep(2000);
payCommonController.testLimit();
Thread.sleep(2000);
payCommonController.testLimit();
}
测试通过
至此,我们用限流处理器来防止重复提交的需求达成。
- 上一篇: HP惠普各种打印机脱机自检方法
- 下一篇: Landing Page技巧太多,我们只拿实战说话
猜你喜欢
- 2024-12-06 玩转单表查询--JPA版
- 2024-12-06 ncat命令使用实例
- 2024-12-06 Mybatis分页助手PageHelper
- 2024-12-06 《MySQL数据库》分页查询及其优化、视图、索引
- 2024-12-06 Basic认证登录设定
- 2024-12-06 三分钟学会使用Mybatis-Plus——笔记
- 2024-12-06 我采访了一位 Pornhub 工程师,聊了这些纯纯的话题
- 2024-12-06 盘点最能延长(缩短)笔记本电脑寿命的浏览器
- 2024-12-06 Landing Page技巧太多,我们只拿实战说话
- 2024-12-06 HP惠普各种打印机脱机自检方法
- 最近发表
- 标签列表
-
- location.href (44)
- document.ready (36)
- git checkout -b (34)
- 跃点数 (35)
- 阿里云镜像地址 (33)
- qt qmessagebox (36)
- md5 sha1 (32)
- mybatis plus page (35)
- semaphore 使用详解 (32)
- update from 语句 (32)
- vue @scroll (38)
- 堆栈区别 (33)
- 在线子域名爆破 (32)
- 什么是容器 (33)
- sha1 md5 (33)
- navicat导出数据 (34)
- 阿里云acp考试 (33)
- 阿里云 nacos (34)
- redhat官网下载镜像 (36)
- srs服务器 (33)
- pico开发者 (33)
- https的端口号 (34)
- vscode更改主题 (35)
- 阿里云资源池 (34)
- os.path.join (33)