前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >如何实现接口限流,接口幂等功能

如何实现接口限流,接口幂等功能

作者头像
Lvshen
发布2022-12-05 13:49:10
5020
发布2022-12-05 13:49:10
举报

业务背景

在业务上有很多需要防止重复提交的场景,例如大部分的创建方法要求同样的数据不能创建两次。对于此种业务处理一般可以分为前端处理和后端处理。前端可以在点击后将按钮置灰1s,做防抖处理,1s后才可以再次调用接口。后端这里需要在业务上做处理,我们在做入库操作时,需要校验:

  • 待插入数据在数据库中是否存在?
  • 存在则不能插入
  • 不存在则可插入

常规插入

重复提交的场景一般是同一个用户连续的点击按钮2次以上,那么这里出现重复提交的条件为:

  • 同一用户
  • 短时间内操作多次

那么为什么短时间多次操作就能出现多次插入呢,我们在插入时后端不是先查数据库做校验了么。

原来我们在短时间操作同一接口,虽然会先查询数据库,但是可能操作1还没有完成,操作2就开始了。操作1和操作2查询的数据就可能是一样的。

并发插入

这个问题在面试时也经常会被问到:

❝如何实现接口的幂等性? ❞

幂等要求我们多次操作,其产生的结果要跟一次操作一样。防重复提交就属于幂等问题。

对于保证幂等性,解决方案有很多。比如采用数据库的唯一索引,Redis相同Key是否有值,在查库时使用锁,使用Semaphore限流等等。

Redis实现

今天我们采用Redis限流操作来控制实现接口幂等。主要操作为:

❝相同key调用的接口,给对应值+1 在指定范围内,值小于指定数,则接口可调用 ❞

说干就干,我们先定义一个注解RateLimiter,用在需要防重复提交的方法上。RateLimiter定义如下:

这个注解我们要注意几个元素:

代码语言:javascript
复制
needUserLimit() //key设定为  接口名称 + userId
limit()//单位时间限制通过的请求数
expire()//过期时间,单位s

这里我们利用Redis的过期时间,在过期时间内请求数不超过指定的limit()数,则接口可以执行,否则接口执行前会被拦截。我们使用接口全路径名称+登录用户的id作为Redis的key。limit()expire()可以使用默认值,即1秒内只能执行一次接口。

来看看如何实现这个注解:

我们写一个RateLimiterHandler类,在注入时加载Lua脚本

代码语言:javascript
复制
@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为:

代码语言:javascript
复制
固定前缀 + 方法全路径 + 登录用户id

代码如下:

代码语言:javascript
复制
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脚本:

代码语言:javascript
复制
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秒后再进行操作 ❞

单元测试

代码到这里就结束了,其实思路也比较简单,我们写一个单元测试试试:

代码语言:javascript
复制
@ResponseBody
@RequestMapping("ratelimiter")
@RateLimiter(needUserLimit = true)
public String testLimit() {
    return "限流注解测试专用";
}

运行上面的方法:

代码语言:javascript
复制
@Test
public void testPage() throws InterruptedException {
    payCommonController.testLimit();
    payCommonController.testLimit();
    payCommonController.testLimit();
}

我们连续执行3次目标方法,发现控制台已有提示。

Redis上我们也看到了对应的key。

我们将调用时间间隔为:2s

代码语言:javascript
复制
@Test
public void testPage() throws InterruptedException {
    payCommonController.testLimit();
    Thread.sleep(2000);
    payCommonController.testLimit();
    Thread.sleep(2000);
    payCommonController.testLimit();
}

测试通过

至此,我们用限流处理器来防止重复提交的需求达成。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2022-08-21,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 Lvshen的技术小屋 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 业务背景
  • Redis实现
  • 单元测试
相关产品与服务
云数据库 Redis
腾讯云数据库 Redis(TencentDB for Redis)是腾讯云打造的兼容 Redis 协议的缓存和存储服务。丰富的数据结构能帮助您完成不同类型的业务场景开发。支持主从热备,提供自动容灾切换、数据备份、故障迁移、实例监控、在线扩容、数据回档等全套的数据库服务。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档