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

2021-12-02  本文已影响0人  萤火架构

问题

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

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

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

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

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

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

select count(*) from table where mobile = 'xxx'

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

我们再来看两个例子:

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

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

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

方案

问题归纳

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

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

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

008i3skNly1gwxji89c4fj30m00jqdgh.jpg

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

008i3skNly1gwz5vw9g2lj30ow0h0gm9.jpg

限流方案

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

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

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

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

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

报名重复提交问题

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

员工提交报销问题

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

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

限流用于重复提交的限制

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

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

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

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

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

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

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

实现

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

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

这里还是使用 FireflySoft.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 上访问到最新的代码。


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

本文由博客一文多发平台 OpenWrite 发布!

上一篇 下一篇

猜你喜欢

热点阅读