限流的非常规用途 - 解决重复提交问题

本文涉及的产品
云数据库 Tair(兼容Redis),内存型 2GB
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
简介: 限流的非常规用途 - 解决重复提交问题

问题


在业务应用程序开发中,经常遇到用户重复提交的问题。

比如有一个报名的表单,如果用户不小心连续点击了提交按钮多次,数据库中就可能产生多条报名记录;再或者正常提交后,因为网络或者服务端的原因,前端没有及时收到提交结果,则用户可能认为自己没有提交成功,然后再次甚至多次提交,数据库中也可能产生多条此用户的报名记录。

这个例子中的情况还不会对业务造成多大影响,如果是涉及到资源增减的场景,比如账户、库存等的操作,可能就比较麻烦了。

对于解决问题的办法,你可能会说,前端提交后锁住按钮不就可以了嘛!确实能够解决一部分问题。为什么是一部分呢?因为调用者可能绕过前端界面,直接访问后端服务。那你也可能会说,在服务端加上判断不就可以了嘛!

好的,我们先来看一下怎么判断:

在上面报名的例子中,我们假设用户的身份是用手机号来区分的,那么服务端判断是否重复提交的时候,可以用一条SQL查一下:

csharp

复制代码

selectcount(*) from table where mobile = 'xxx'

如果查询出的数量大于0,我们就认为已经报名过,从而中断程序的执行,返回错误。

我们再来看两个例子:

  • 员工提交报销,一不小心提交了两次,这时服务端或许可以通过 “用户Id+提交时间” 的方式来判断。
  • 给用户发放积分的处理,第一次请求超时了,又重试了一次,这时服务端或许可以通过 “用户Id+事务Id” 的方式判断是否重复提交。

还有很多的重复提交的场景,我们也都可以通过在服务端增加类似重复判断的方式来解决。但是每次都要编写这些大体类似的业务判断逻辑,软件开发不是经常说: Don't repeat yourself 吗?

再者增加判断也不能完美解决问题,为什么呢?因为后端服务一般是多线程处理的,甚至可能是分布式的,只是写个判断的逻辑还不够,还要处理数据一致的问题,这个就有点技术含量了。

有什么通用的简单办法吗?


方案


问题归纳


这里先来看下服务端可能收到重复请求的场景,我归纳如下:

  • 前端把关不严,用户 “提交中” 时没有禁用提交按钮,导致用户多次点击,向服务端发起多次请求。
  • 调用者直接访问服务,因程序错误(如死循环)或者攻击行为(如重放攻击),导致对同一业务多次发起服务请求。
  • 程序重试,可能会在前端或者后端的代码中使用重试逻辑,发生某些异常或者超时的时候自动重试,导致一次业务多次请求服务。
  • 多线程或分布式环境下,加了重复判断,但是因为数据一致性问题导致判断失效,业务被重复处理。

前两种场景比较好理解,这里不做过多说明。重点说明一下后两种是如何发生的。

先来看重试导致的重复提交,客户端第一次请求后没有正常收到返回,判断超时后,再次发起第二次业务请求,此时服务端执行了两次相同的业务处理。

再来看多线程环境下的重复提交,线程1访问数据库查询数据,然后判断没有提交过,在线程1写入数据前,线程2也来访问数据库查询数据,然后判断也没有提交过,于是线程1和线程2都向数据库写入相同的数据。


限流方案


现在到重点了,限流为何能够应用到解决重复提交的问题?


重复提交满足限流的基本要素


关于限流,这里定义如下几个基本要素(个人总结):

  • 限流有一个针对的目标,比如限制IP、限制用户等。
  • 限流有一个时间周期,比如1秒之内、1分钟之内等。
  • 限流有一个对应时间周期的阈值,比如每秒10次、每分钟100次等。

再回到重复提交上,我们可以分析得出:

  • 重复提交可以通过某些数据进行识别,这个就可以看作是限流目标。
  • 重复提交天然的存在一个时间纬度,可以对应到限流的时间周期上。
  • 重复提交即提交一次之后继续提交,可以使用限流的阈值进行控制,并固定阈值为1。

看着有戏,再通过两个例子验证下:

报名重复提交问题

  • 限流目标:手机号
  • 限流周期:从用户首次提交到报名结束

手机号可以从报名信息中提取,用户第一次提交时会使用手机号创建一条限流计数记录,报名结束之前,用户再次提交时,限流计数超过1,从而触发限流逻辑,向调用方返回错误;用户报名结束后再次提交,服务可能已经关停,或者前端已关停入口,后端也有报名截止时间的判断,即使还可以提交,已经没有什么意义,对业务没有影响。

员工提交报销问题

  • 限流目标:员工Id + 提交分钟数
  • 限流周期:从用户提交 到 其后的1分钟之内

员工Id可以从会话中提取到,提交分钟数可以用 yyyyMMddHHmm 表达,用户第一次提交报销时会创建 “用户Id + 提交分钟数” 的限流计数记录,用户在1分钟内再次提交时,限流计数会超过1,触发限流处理逻辑,向调用方返回错误;用户1分钟后再次提交时,会创建新的限流计数记录,同时不会触发限流处理逻辑,可以正常提交。

从以上分析不难看出,重复提交可以满足限流的几个基本要素,那限流可以解决所有重复提交的问题吗?


限流用于重复提交的限制


不过你也许已经发现,这里有一个隐含的假设:所有第一次业务提交都得到了正确的处理。所以限流计数1才能代表已经提交过一次。这在实际运行中很难保证,因为限流计数和业务处理往往不在一个事务中,限流计数一般更靠前一些,所以限流计数可能没问题,但是业务处理并没有成功,比如超时、断网、宕机等基础设施问题,甚至是业务条件不满足等业务逻辑问题。那么限流又要被一棍子打死了吗?

在遇到比较棘手的问题的时候,我经常想之前是否出现过呢?

在网络论坛比较流行的年代,发帖或者回帖后,都会先进入到一个数秒的倒计时跳转页面,倒计时结束后再跳转到正常的页面。

这不就是一种限流并有效防止了重复提交的方式嘛!这个设计给到的一个启示就是:系统可以在很短的一个时间之内,通过限流这种低成本的方式,限制用户的重复操作,正常用户可能不会感觉到或者只有轻微的影响,但却很大程度上能够避免重复提交带来的数据问题,也可以屏蔽某些恶意行为。

基于这个认识,我们再来看下前文提到的几个重复请求场景:

  • 前端把关不严,导致用户多次点击,向服务端发起多次请求。
    服务端可以对某一个用户的提交使用短时间跨度的限流,比如5秒1次,正常用户填写1个表单耗费的时间应在5秒以上,假如用户在5s内又提交了,则前端可以根据服务端返回的错误码提示用户,并跳转到提交结果查询页面,用户可以看到自己的提交结果。如果第一次提交真的没有处理成功,则用户可以再重新填写提交表单,因为这时距离第一次提交超过了5s,因此用户不会被限流。因为绝大部分提交都应该是正常的,所以这种概率比较小,但是也给了补救的机会,用户可能会抱怨几句。
    这里也可能出现服务端处理过慢,查询结果的时候查不到的问题,解决这个问题或许可以在服务端设置一个尽可能短的超时时间,在前端多查询几次,其出现的概率一般不高,而且也可以通过技术手段降低。
  • 直接访问接口时,因程序错误或者攻击行为,导致同一业务多次发起服务请求。
    服务端可以对同一个访问者的提交使用短时间跨度的限流,比如5秒1次,如果触发限流,同时给予一个限流惩罚,30秒内都不能提交,还可以对这个限流惩罚时间采用指数递增的方式。这样可以尽量降低外部程序异常行为对服务的影响,同时调用方正常处理后又能自动恢复正常。
    在某些接口中可能会定义时间戳、验证码、SessionId之类的参数,也可以把它们加到限流目标中,用以准确识别重复提交。
  • 程序重试导致重复提交,发生某些异常或者超时的时候自动重试,导致一次业务多次请求服务。
    这可能是个设计问题。应该避免在中间服务发起提交行为的重试操作,因为很多的业务处理可能都不是幂等的,中间服务的重试行为因为访问者看不到,所以很可能被忽略掉,从而导致数据问题。如果需要重试,应该仅在业务的发起处进行重试,发起者应该清楚重试逻辑可能导致的问题,并尽量降低影响。
    可以在最上层服务引入限流处理,选择合适的限流目标,限流时间跨度和限流阈值,内部服务一般认为相对可靠,没必要引入限流。
  • 多线程或分布式环境下,加了重复判断,但是因为数据一致性问题导致判断失效,业务被重复处理。
    通过选择合适的限流目标,使用分布式一致性的限流算法,比如使用Redis,也可以实现提交操作在某个时间范围内只能被执行一次,从而让重复判断的结果有效,避免业务重复处理。

通过对这几种重复提交场景的分析,可以看到:限流并不能完美的避免重复提交,但是它可以提供一种通用的机制很大程度的降低重复提交,而且这种机制的成本可以很低,相比每个方法中硬编码重复数据的判断、查询数据库、使用分布式锁等带来的成本可能都要低不少。当然为了处理的更好,还可能需要前后端的一些其它配合。

其实也可以在业务处理中增加对重复数据的判断,因为前边已经被限流拦截了一道,重复执行的机会可以大为减少,重复数据判断的逻辑带来的影响也将很低。特别是一些关键业务中,重复提交导致的麻烦可能比较大,不过这时候可能要解决数据库的查询性能、分布式的数据一致问题。两害相权取其轻。


实现


分析清楚了限流对于限制重复提交的意义,就可以在合适的场景来应用它。

比如存在一个前后端分离的系统,用户都通过一个前端界面来处理业务,用户同时一般只能操作一个界面,为了尽可能避免重复提交问题,我们在后端API中增加对用户提交行为的限流操作,对于每个独立的用户限制5秒之内只能提交1次。

这里还是使用 Fir

eflySoft.RateLimit 来做限流,后端API基于ASP.NET Core WebAPI实现。


安装 Nuget 包


使用包管理器控制台:

Install-Package FireflySoft.RateLimit.AspNetCore

或者使用 .NET CLI:

dotnet add package FireflySoft.RateLimit.AspNetCore

或者直接添加到项目文件中:

<ItemGroup>
<PackageReference Include="FireflySoft.RateLimit.AspNetCore" Version="2.*" />
</ItemGroup>


编写限流规则


在Startup.cs中注册限流服务并使用限流中间件。

public void ConfigureServices(IServiceCollection services)
{
    ...
    services.AddRateLimit(new InProcessFixedWindowAlgorithm(
        new[] {
            new FixedWindowRule()
            {
                Id = "1",
                ExtractTarget = context =>
                {
                    // 限流的目标:用户Id,这里假设它是从HTTP Header中传递过来的
                    return (context as HttpContext).Request.GetTypedHeaders().Get<string>("userId");
                },
                CheckRuleMatching = context =>
                {
                    // 在这里判断当前请求是否 “提交行为”,提交行为才进行限流处理
                    var path = (context as HttpContext).Request.Path.Value;
                    if(path == "/Comapny/Add"
                      ||path == "/Comapny/Update"
                      ||path == "/Goods/Purchase"
                      ||path == "/Goods/ChangePrice"
                      ||path == "/Order/Pay"
                      ||path == "/Order/Cancel"){
                        return true;
                    }
                    return false;
                },
                Name = "用户提交行为限流",
                LimitNumber = 1, // 限流阈值
                StatWindow = TimeSpan.FromSeconds(5), //限流的时间窗口,这里是5秒
                StartTimeType = StartTimeType.FromNaturalPeriodBeign
            }
        })
    );
    ...
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    ...
    app.UseRateLimit();
    ...
}

只需要上边这些简单的代码就可以用来限制重复提交了。可以跑起来试试。

不过如果你要在分布式环境下使用,还需要准备一个Redis,将 InProcessFixedWindowAlgorithm 换成 RedisFixedWindowAlgorithm ,除了多传递一个Redis连接对象,其它的代码都是一样的。

FireflySoft.RateLimit 是一个开源的.NET Standard限流类库,其使用灵活轻巧,可以在 GitHub 或者 Gitte 上访问到最新的代码。


好了,这就是这篇文章的主要内容了。对于用限流解决重复提交的问题,你有什么想说的呢?



相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore &nbsp; &nbsp; ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库&nbsp;ECS 实例和一台目标数据库&nbsp;RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&amp;RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
相关文章
|
SQL 缓存 NoSQL
接口的幂等性设计和防重保证,详细分析幂等性的几种实现方法
本篇文章详细说明了幂等性,解释了什么是幂等性,幂等性的使用场景,讨论了幂等和防重的概念。分析了幂等性的情况以及如何设计幂等性服务。阐述了幂等性实现防重的几种策略,包括乐关锁,防重表,分布式锁,token令牌以及支付缓冲区。
6716 0
接口的幂等性设计和防重保证,详细分析幂等性的几种实现方法
|
2月前
|
存储 监控 Java
一种优雅的方式整合限流、幂等、防盗刷
【9月更文挑战第6天】在软件开发中,限流、幂等和防盗刷是保证系统稳定性和安全性的重要手段。本文介绍了一种优雅的整合方案:首先引入限流库并配置策略,在关键位置应用限流;其次设计幂等接口,记录请求状态并处理重复请求;接着识别异常请求,记录请求日志,并采取防范措施;最后通过切面或过滤器整合这些功能,并集中配置管理,提高系统的稳定性、安全性和可靠性。
|
3月前
|
负载均衡 调度
异步任务处理系统问题之任务流控的主要目的是什么
异步任务处理系统问题之任务流控的主要目的是什么
|
5月前
|
缓存 NoSQL 前端开发
《优化接口设计的思路》系列:第六篇—接口防抖(防重复提交)的一些方式
本文探讨了后端开发中的接口防抖策略,作者是一名有六年经验的Java开发者,分享了如何防止重复提交导致的问题。防抖主要用于避免用户误操作或网络波动引起的多次请求,作者提出理想防抖机制应具备正确性、响应速度、易集成和用户反馈。文章详细分析了哪些接口需要防抖(如用户输入、按钮点击、滚动加载)以及如何识别重复接口,提出了使用共享缓存和分布式锁两种实现方式,并展示了基于Redis的Java代码示例。作者通过注解实现请求锁,并提供了测试截图证明防抖效果。然而,实现完全幂等性还需要业务层面的补充措施。
417 7
|
6月前
|
消息中间件 监控 Java
接口请求重试策略:保障稳定性的必杀技
接口请求重试策略:保障稳定性的必杀技
323 0
|
6月前
|
缓存
什么情景与接口需要做幂等
什么情景与接口需要做幂等
67 0
|
数据采集 存储 数据挖掘
Django爬虫:如何处理超过重试次数的请求以保障数据完整性
Django爬虫:如何处理超过重试次数的请求以保障数据完整性
|
缓存 NoSQL JavaScript
8 种方案解决重复提交问题,还怕没有适合你的?
8 种方案解决重复提交问题,还怕没有适合你的?
|
存储 开发框架 负载均衡
限流的非常规用途 - 缓解抢购压力
限流的非常规用途 - 缓解抢购压力
115 0
|
安全 NoSQL Redis
优雅的接口防刷处理方案! 下
优雅的接口防刷处理方案! 下