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

本文涉及的产品
云数据库 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
相关文章
|
前端开发
多次请求同一数据接口造成数据混乱问题解决办法
在进行前端开发过程中,经常会遇到需要请求同一个数据接口但不同参数的需求,这种情况下当用户通过页面操作频繁请求该接口,而接口的不同参数响应时间差异较大时,容易引发数据渲染混乱的bug。
2559 0
|
2月前
|
存储 监控 Java
一种优雅的方式整合限流、幂等、防盗刷
【9月更文挑战第6天】在软件开发中,限流、幂等和防盗刷是保证系统稳定性和安全性的重要手段。本文介绍了一种优雅的整合方案:首先引入限流库并配置策略,在关键位置应用限流;其次设计幂等接口,记录请求状态并处理重复请求;接着识别异常请求,记录请求日志,并采取防范措施;最后通过切面或过滤器整合这些功能,并集中配置管理,提高系统的稳定性、安全性和可靠性。
|
2月前
|
数据采集 Java Python
优化数据的抓取规则:减少无效请求
本文详细介绍了一种高效抓取贝壳等二手房平台房价信息的方法,重点在于过滤无效链接和减少冗余请求。文章首先分析了目标数据和平台特点,然后提出了URL过滤、分页控制、动态设置User-Agent和Cookies、代理IP轮换及多线程优化等策略。最后,提供了一个结合代理IP技术的Python爬虫代码示例,展示了如何具体实现上述优化措施,从而显著提升数据抓取的稳定性和效率。
73 26
优化数据的抓取规则:减少无效请求
|
28天前
|
缓存 网络协议 API
【Azure 环境】请求经过应用程序网关,当响应内容大时遇见504超时报错
应用程序网关的响应缓冲区可以收集后端服务器发送的全部或部分响应数据包,然后再将它们发送给客户端。 默认在应用程序网关上启用响应缓冲,这对于适应缓慢的客户端很有用。
|
3月前
|
安全 数据安全/隐私保护
数据安全用户系统问题之需要限制验密错误次数以及冻结功能如何解决
数据安全用户系统问题之需要限制验密错误次数以及冻结功能如何解决
|
5月前
|
缓存 NoSQL 前端开发
《优化接口设计的思路》系列:第六篇—接口防抖(防重复提交)的一些方式
本文探讨了后端开发中的接口防抖策略,作者是一名有六年经验的Java开发者,分享了如何防止重复提交导致的问题。防抖主要用于避免用户误操作或网络波动引起的多次请求,作者提出理想防抖机制应具备正确性、响应速度、易集成和用户反馈。文章详细分析了哪些接口需要防抖(如用户输入、按钮点击、滚动加载)以及如何识别重复接口,提出了使用共享缓存和分布式锁两种实现方式,并展示了基于Redis的Java代码示例。作者通过注解实现请求锁,并提供了测试截图证明防抖效果。然而,实现完全幂等性还需要业务层面的补充措施。
394 7
|
6月前
|
消息中间件 监控 Java
接口请求重试策略:保障稳定性的必杀技
接口请求重试策略:保障稳定性的必杀技
316 0
|
6月前
|
数据采集 搜索推荐 API
python爬虫如何处理请求频率限制?
【2月更文挑战第21天】【2月更文挑战第64篇】python爬虫如何处理请求频率限制?
524 3
|
12月前
|
数据采集 存储 数据挖掘
Django爬虫:如何处理超过重试次数的请求以保障数据完整性
Django爬虫:如何处理超过重试次数的请求以保障数据完整性
|
定位技术
后端一次性返回几百万条数据怎样处理
后端一次性返回几百万条数据怎样处理