限流的非常规用途 - 缓解抢购压力

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

这两年因为疫情,节假日都不怎么外出了,以前每逢节假日都要提前一个月或者半个月抢火车票,人太多的时候会把12306整崩溃。当时很多技术人员指点江山,激扬想法,粪土当年铁科院。

前几年小米手机还很火爆,特别抢手,每每新机发布,人山人海、万人空巷,却往往都在米兔排队的身影后收获失望的表情。

俱往矣!数风流套路,还看今朝!

几句笑谈,引出抢购这回事儿,下边开始正文。


怎么理解抢购?


抢购,大部分人应该都经历过,没抢过火车票,还没抢过口罩吗?没抢过食盐,还没抢过米面粮油吗?没抢过618,难道没抢过双11?总有一款抢购适合你。

“购”大家都能够理解,就是花钱买东西。“抢”就很有意思了,首先是因为商品数量有限(这里不讲套路了),然后需要的人特别多,所以怎么分配就成了一个问题,也就是人们常说的僧多粥少。

不知道大家是否见过这样的场景,一群人围在商店门口,大门还没完全打开,就咆哮着挤了进去,抢着拿货架上的商品,谁跑得快挤得凶,谁就能拿到,谁拿到谁就可以去付款带走,这就是抢的最真实写照了。如果你不曾体验过,那么挤公交也可以代替,要么挤上去,刷卡走人,要么被挤下来,等待下一辆车。虽然体验不好,但也确实解决了分配问题。

这里的所谓“抢”就是看谁先获得了有利位置,谁先锁定了商品。那么锁定了商品就一定会购买吗?也不见得,说不定你突然发现囊中羞涩,大家应该听过很多弃购的新闻。所以对于抢购来说,抢的是“购”的机会,谁先到了谁就有 “购” 的机会。至于会不会真的买回家,还真不好下定论。


抢购程序的问题


理解了抢购,再来看看程序怎么实现它?

根据前面的理解,我们可以将抢购分为两个阶段,“抢”映射为下单,“购”映射为支付订单。下单的操作就是锁定商品,又可以分为锁定库存、创建订单两个阶段,锁定库存就相当于你在商店中把商品拿到了手里,创建订单就相当于你和商家关于商品的价格达成协议。

1689131792529.png

操作库存有一个很有名的“超售”问题,说的是扣减库存时出错了,库存数量扣成了负数,那么这个问题是如何发生的呢?

后端服务处理请求时一般都是多线程的,为了高可用,还可能是分布式的,这个问题就是多线程和分布式环境带来的并行处理造成的。扣减库存时,一般有两个步骤,先检查库存是否足够,然后再从库存中减去相应的数量。这两个步骤如果不是原子的,那么在多线程或分布式环境下,就会存在多个线程同时查询库存,单从每个请求处理的上下文看库存是足够,但是库存总量不能满足所有这些扣减加在一起,结果就扣成了负数。

那怎么解决呢?方法很多,比如把查询和更新放到同一个数据库事务中就可以了,或者使用一个锁(分布式部署时为分布式锁)来锁定对某件商品库存的查询+扣减同时只能有一个在处理。

这样是不是就没有问题了呢?还是有的。需要注意这里说的是抢购,意味着会有很多的人来尝试,在现实生活中,如果人特别多,商店中承载不了这么多人,很可能会出现安全事故。在计算机程序中也可能会出现耗尽资源导致崩溃的问题,特别是数据库,IO请求突然爆发,很可能就会被压垮,上文提到的数据库事务无疑会加重这一负担。而锁也会导致吞吐量的下降,请求堆积无法得到及时处理,一样会加重服务器负担,出现频繁超时甚至无法服务的问题。

还有什么办法呢?以前写程序的时候有一个观点,大概是不用太关注代码运行效率,增加硬件就好了,比如增加内存、升级CPU、换固态硬盘等等。不过在抢购这里不太可行,还是因为并发量可能太大了,不得不考虑计算资源的成本,而动态资源调配的速度也是个问题。

现实生活中我们还有一个文明的处理方式:排队,先到先得,售完为止。程序先把请求接下来,保存到队列中,然后再按照先后顺序一个个处理,处理完一个回复一个,这就是队列的处理方式。这样做的优点是不用再去协调那些跨线程跨进程的资源访问冲突,计算资源需求也会大幅降低;缺点是用户要多等一会才能看到结果,体验略差,不过本来就是很紧俏的东西。


限流之于抢购


上边已经分析了抢购可能会遇到的问题,那么限流能干什么呢?

在软件系统中库存就是一个数字,限流呢也有一个数字,我们可以把限流的阈值作为库存的数量,请求过来的时候,先用限流处理,能够通过的就进入下一步,如果被限流了,则说明库存已经耗尽了,返回错误就行了。示意图如下:

1689131819140.png

这样做的好处是什么呢?减轻后续其它处理的压力。如果库存已经耗尽,也再无必要去查询数据库,白白浪费宝贵的数据库资源,甚至分布式锁、显式的数据库事务都不需要了,因为能够通过限流检查的就是可以扣减库存的,直接使用Update就可以了;不过此时扣减库存和下订单时的压力仍旧存在,比如3W个请求同时进来,限流可以拦截其中的27000,剩下的3000会进入下一环节,影响还是需要仔细考虑的。再看使用队列的方式,加了限流,队列中也只接收能够下单的请求,队列压力小,后续处理队列时的数据库操作同样也减少很多。

这里可能还会出现一些问题,比如限流检查通过了,但是后续的处理因为某种原因失败了,库存不能回收,实际上就浪费了一次机会,不过这不是抢购的主要矛盾,一般会忽略这个问题。

当然对于限购来说还有很多其它问题,限流就很难发挥作用了,比如前端高并发查询库存数,这里就不多讲了。


技术实现


这里以ASP.NET Core Web API为例,限流组件选择 FireflySoft.RateLimit ,算法选择简单的固定窗口也就是计数器方式,进程内计数。这里没有选择Redis,是因为进程内计数比较简单,不需要外部依赖,演示方便;即使在分布式环境下,一般也可以应对,比如总的限流阈值是 1000/小时,部署了5份服务实例,只需要在程序中使用 200/小时 的阈值就可以了;但是如果负载均衡不均匀,某些情况下可能不太合适,此时可以选择Redis全局一致限流。


安装 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,这里假设它是从Url参数中传递过来的
                    // 实际使用可能还需要很多的安全检查,这里为了演示简单处理
                    return  (context as HttpContext).Request.Query["GoodsId"].ToString();;
                },
                CheckRuleMatching = context =>
                {
                    // 在这里判断当前请求是否 “抢购下单”,抢购下单才进行限流处理
                    // 实际使用可能还需要很多的检查,这里为了演示简单处理
                    var path = (context as HttpContext).Request.Path.Value;
                    if(path == "/Order/PanicBuying"){
                        return true;
                    }
                    return false;
                },
                Name = "缓解抢购压力限流",
                LimitNumber = 1000, // 限流阈值,等于库存数量
                StatWindow = TimeSpan.FromSeconds(3600), //限流的时间窗口,这里是3600秒
                StartTimeType = StartTimeType.FromNaturalPeriodBeign
            }
        }),
        new HttpErrorResponse()
        {
            BuildHttpContent = (context, ruleCheckResult) =>
            {
                return "同学!你来晚了,已经售罄!";
            }
        },
    );
    ...
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    ...
    app.UseRateLimit();
    ...
}

只需要上边这些简单的代码就可以跑起来了,你可以用Postman测试一下。

如果你不想在Web程序中使用,或者需要更多自定义的设置,也可以直接安装 FireflySoft.RateLimit.Core 这个包,它可以用于各种.NET程序。点击查看示例: bosima/FireflySoft.RateLimit(github.com)

如果你想要使用Redis,只需将 InProcessFixedWindowAlgorithm 换成 RedisFixedWindowAlgorithm ,除了多传递一个Redis连接对象,其它的代码都是一样的。


好了,这就是本文的主要内容了。对于使用限流缓解抢购压力,你有什么想说的呢?

FireflySoft.RateLimit 是一个开源的.NET Standard限流类库,其使用灵活轻巧,可以在 GitHub 上访问到最新的代码。其主要特点包括:

  • 多种限流算法:内置固定窗口、滑动窗口、漏桶、令牌桶四种算法,还可自定义扩展。
  • 多种计数存储:目前支持内存、Redis两种存储方式。
  • 分布式友好:通过Redis存储支持分布式程序统一计数。
  • 限流目标灵活:可以从请求中提取各种数据用于设置限流目标。
  • 支持限流惩罚:可以在客户端触发限流后锁定一段时间不允许其访问。
  • 动态更改规则:支持程序运行时动态更改限流规则。
  • 自定义错误:可以自定义触发限流后的错误码和错误消息。
  • 普适性:原则上可以满足任何需要限流的场景。


相关实践学习
基于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
相关文章
|
7月前
|
Oracle 数据库 UED
后台查询接口影响响应时间最大的因素:用空间换时间的优缺点及解决方案
1.当数据库的一个表记录很多显然查询数据很慢。 2.当数据库的一个表记录不大,但是数据很大也可能很慢。 我们的一个用户表中一个building很大,当查询100条数据就会把服务器的内存搞爆掉。 当然查询时要查询筛选有用字段,不可以直接把记录的所有字段都查拆来。这样能减少内存消耗和提高查询速度。 3.在经常查询字段上建立索引。据说oracle上用索查询和不用索引查询在超多记录的情况下相差1000倍。 4.若出现嵌套查询显然会大大增加相应查询时间。要先预处理用管道操作把能合并的查询合并到一个查询中,然后生成map,然后再处理。这是标准的用空间换时间的方案。
102 8
|
7月前
|
消息中间件 监控 Java
接口请求重试策略:保障稳定性的必杀技
接口请求重试策略:保障稳定性的必杀技
375 0
|
7月前
|
前端开发 Shell 开发工具
一个瞬间让你的代码量暴增的脚本
一个瞬间让你的代码量暴增的脚本
|
存储 算法 NoSQL
关于高并发限流的详谈
高并发限流是在面对大量请求并发访问时,为了保护系统的稳定性和可用性,对请求进行限制和控制的一种技术手段。下面将详细介绍高并发限流的原理、常见的限流算法以及实现方法。
113 0
|
开发框架 前端开发 NoSQL
限流的非常规用途 - 解决重复提交问题
限流的非常规用途 - 解决重复提交问题
82 0
间歇性宏图大志,持续性混吃等死...
间歇性宏图大志,持续性混吃等死...
83 0
|
缓存 数据挖掘 BI
面试官问你:日亿万级请求日志收集如何不影响主业务?你怎么回复
数据收集 上篇详细讨论了写缓存的架构解决方案,它虽然可以减少数据库写操作的压力,但也存在一些不足。比如需要长期高频插入数据时,这个方案就无法满足,接下来将围绕这个问题逐步提出解决方案。
|
SQL 运维 监控
redis瞬时查询返回量过多导致出口流量打满,影响系统整体响应时间
redis瞬时查询返回量过多导致出口流量打满,影响系统整体响应时间
456 0
redis瞬时查询返回量过多导致出口流量打满,影响系统整体响应时间
|
弹性计算 Java 测试技术
热点和秒杀来临前要做的5件事
热点和秒杀来临前要做的5件事
热点和秒杀来临前要做的5件事
|
算法 Java 数据库
【高并发】不可不说的几种限流算法
常见的限流算法有:令牌桶、漏桶,计数器也可以用来进行粗暴限流实现。
231 0