当前位置: 首页 > news >正文

Sentinel入门篇【流量治理】

流量治理基础篇–Sentinel

本文示例代码见GITEE

地址:https://gitee.com/quercus-sp204/sourcecode-and-demos/tree/master/protect-sentinel

该项目的protect-sentinel模块

1. 基本介绍

阿里巴巴开源的 Sentinel 是面向分布式系统的轻量级流量治理组件,专注于保障微服务架构的高可用性。它以流量为核心切入点,提供多维度的防护能力,支撑了阿里巴巴“双十一”等超大规模流量场景。

它的主要功能:


【①流量控制】

在一个系统中,任意时间到来的请求往往是随机不可控的,而系统的处理能力是有限的,由于这种随机不可控的性质,可能在某个时间点或者时间段到达系统的请求超过了系统处理的能力,就可能会对系统业务造成损失。Sentinel支持 QPS(每秒请求数)和并发线程数限流,防止突发流量压垮系统。

它的流控模式:

  • 直接拒绝:默认策略,超阈值立即拒绝请求。
  • Warm Up(冷启动):系统冷启动时缓慢提升流量阈值,避免瞬时压力
  • 匀速排队:通过漏桶算法平滑流量,实现削峰填谷(如消息队列场景)

【②熔断降级】

什么是熔断降级?

在分布式服务部署的架构中,服务雪崩是一种常见的风险。当某个服务出现故障,由于依赖关系,可能会引发一系列的服务不可用,最终导致整个系统瘫痪。为了应对这种风险,熔断降级机制应运而生。熔断降级是一种保护机制,用于在分布式系统中防止服务雪崩。其核心思想是在某个服务出现故障时,通过熔断机制快速切断故障源,避免故障扩散;同时,通过降级机制提供简化或备用的服务,保证系统的整体可用性

熔断机制是指当某个服务的错误率达到一定阈值时,自动触发熔断,直接返回一个错误响应,不再继续调用该服务。这样可以避免大量的请求涌入故障服务,导致系统资源耗尽。

降级可以是功能上的降级,也可以是性能上的降级。例如,在电商系统中,当搜索服务出现故障时,可以降级为展示默认商品列表,而不是完全无法搜索。降级机制需要根据实际业务场景进行设计和实现,以确保在故障发生时,能够快速切换到降级方案。

总的来说,降级和熔断其实就是服务安全中的2个不同的流程,在服务发生故障时,肯定是先断开(熔断)与服务的连接,然后在执行降级逻辑;

那么,回到Sentinel,当调用链路中某个资源出现不稳定,例如,表现为 timeout,异常比例升高的时候,则对这个资源的调用进行限制,并让请求快速失败,避免影响到其它的资源,最终产生雪崩的效果。他可以基于 响应时间(RT)异常比例异常数 触发熔断,自动阻断不稳定资源的调用(例如,订单服务依赖的库存服务超时,Sentinel 会快速失败并返回降级结果),降级后进入时间窗口期,窗口结束后自动恢复,避免级联故障。


在正式使用Sentinel之前,我们还需要了解一下其涉及到的一些基本概念

**资源:**资源是 Sentinel 的关键概念。它可以是 Java 应用程序中的任何内容,例如,由应用程序提供的服务,或由应用程序调用的其它应用提供的服务,甚至可以是一段代码。只要通过 Sentinel API 定义的代码,就是资源,能够被 Sentinel 保护起来。大部分情况下,可以使用方法签名,URL,甚至服务名称作为资源名来标示资源。

规则:围绕资源的实时状态设定的规则,可以包括流量控制规则熔断降级规则以及系统保护规则。所有规则可以动态实时调整

2. 使用示例

2.1 控制台

Sentinel 控制台是流量控制、熔断降级规则统一配置和管理的入口,它为用户提供了机器自发现、簇点链路自发现、监控、规则配置等功能。在 Sentinel 控制台上,我们可以配置规则并实时查看流量控制效果。

下载地址:https://github.com/alibaba/Sentinel/tree/1.8/sentinel-dashboard

下载到本地,然后用mvn打包为jar包,就可以运行下面的命令运行了:(控制台就是一个SpringBoot项目)-- 我这里把端口改成了10000

java -Dserver.port=10000 -Dcsp.sentinel.dashboard.server=localhost:10000 -Dproject.name=sentinel-dashboard -jar sentinel-dashboard.jar

其中 -Dserver.port=10000 是 Spring Boot 的参数, 用于指定 Spring Boot 服务端启动端口为 10000。然后http://127.0.0.1:10000,账号密码都是sentinel,然后就可以进入控制台了。

2.2 SpringBoot整合

首先是引入maven依赖:

<dependencies><dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-sentinel</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency>
</dependencies><dependencyManagement><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-dependencies</artifactId><version>2.7.18</version><type>pom</type><scope>import</scope></dependency><dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-alibaba-dependencies</artifactId><version>2021.0.5.0</version><type>pom</type><scope>import</scope></dependency></dependencies>
</dependencyManagement>

然后在配置文件中:

spring:application:name: SentinelApplication1cloud:sentinel:transport:# 控制台的地址dashboard: localhost:10000# 这里的 spring.cloud.sentinel.transport.port 端口配置会在# 应用对应的机器上启动一个 Http Server,该 Server 会与 Sentinel 控制台做交互。port: 8719eager: trueenabled: true

接下来随便写写Controller,如下:

@RestController
@RequestMapping("/hello")
public class HelloController {@GetMapping("/heihei/{name}")// 这个接口被标识为一个资源@SentinelResource(value = "hello", blockHandlerClass = {HelloBlockHandler.class}, blockHandler = "helloBlockHandler") public String hello(@PathVariable("name") String name){System.out.println("hello: heihei: " + name);return "hello," + name ;}
}// HelloBlockHandler.java
// 对应处理 BlockException 的函数名称,可选项。blockHandler 函数访问范围需要是 public,返回类型需要与原方法相匹配,
// 参数类型需要和原方法相匹配并且最后加一个额外的参数,类型为 BlockException
// 可以指定 blockHandlerClass 为对应的类的 Class 对象,注意对应的函数必需为 static 函数,否则无法解析。
public class HelloBlockHandler {public static String helloBlockHandler(String name, BlockException ex) {System.out.println("当前访问人数过多!" +  name);return "限流了.....当前访问人数过多";}
}

启动好项目之后,我们可以在控制台添加流控规则:如下图所示

然后发送请求,请求该接口,会发现如果请求超过了设置的qps阈值,会返回“限流了…当前访问人数过多”。此外,从上图可以看到,新增流控规则的时候,有很多选项,这些选项分别表示什么意思呢?

下面就介绍一下用的比较多的两种规则了,剩下的就交由读者自行探索了。

2.3 流控规则介绍

选择不同的选项,界面中可供选择是不同的:选择示意图如下:【先不探讨集群的】

QPS:
--单机阈值
--流控模式:直接 | 关联 | 链路
--[多一个input框] 如果流控模式选择【关联】:(关联资源) | 如果流控模式选择【链路】:(入口资源) 
--流控效果:快速失败 | Warm Up[选择这个会多一个input框:预热时长] | 排队等待[选择这个会多一个input框:超时时间]并发线程数:
--单机阈值
--流控模式:直接 | 关联 | 链路
--如果流控模式选择【关联】:(关联资源) | 如果流控模式选择【链路】:(入口资源) 

大致的选择分支就是上面这个样子的。QPS快速失败就如上所示,直接走到我们自定的降级逻辑里面去了。我们从下往上看


现在看一下Warm Up(预热)效果:刚开始把 阈值调低,不要让过多的请求访问服务器,导致冲垮服务器,先让服务器一点一点处理,再慢慢加量,也就是说对超出阈值的请求同样是拒绝并抛出异常。但这种模式阈值会动态变化,从一个较小值逐渐增加到最大阈值。,用在什么场景呢?就比如说系统才启动不久,首次操作数据库需要初始化连接之类的。

请求初始阈值是 单机阈值 / coldFactor,coldFactor默认值是三,如果我设置单机阈值为3,预热时长也为3,初始时qps最大支持就是1,然后qps在3秒内缓慢增加到3。

请看下面例子:

同样是hello这个资源,快速访问它

QPS是一个缓慢上升的过程。这三秒确实qps最大为1. 同时呢,我们也可以发现快速失败和warm up 会拒绝新的请求并抛出异常。

接下来看排队等待:排队等待则是让所有请求进入一个队列中,然后按照阈值允许的时间间隔依次执行。后来的请求必须等待前面执行完成,如果请求预期的等待时间超出最大时长,则会被拒绝。就是说请求超过当时的qps阈值,就会进入等待队列等待,并且有一个等待的超时时间。等待超时了,直接异常了。

这就是QPS三种流控效果的大致介绍了。


往上看流控模式的其他两个 “关联”“链路”

关联模式:通过监控关联资源(如支付接口)的QPS或线程数,当其超过阈值时,直接限制当前资源(如订单查询接口)。例如:当支付接口QPS > 5时,限流订单查询接口。在资源竞争的场景下,例如“订单查询”与“订单支付”共享数据库连接池,当支付接口压力激增时,通过限流查询接口,确保支付业务优先处理;再比如说,扣减库存的位置过载时,限流商品详情页,避免级联雪崩。

链路模式:仅统计从特定入口资源(如/order/query)访问目标资源(如common()方法)的流量,并针对该入口限流。需结合@SentinelResource标记目标资源,并关闭Context整合(web-context-unify=false)。在多入口共享的场景下,例如订单查询(/order/query)和订单创建(/order/save)均调用GoodsService.queryGoods()方法,但只需限制查询入口的访问频率。

关联模式是“兄弟连坐”(关联资源超限,当前资源被限流);链路模式是“溯源追责”(仅限制特定入口的调用链路)


下面来看一下并发线程数:【也就是最开始不选QPS,而是选了这个】

采用基于线程数的限流模式后,我们不需要再显式地去进行线程池隔离,Sentinel 会控制访问该资源的线程数,超出的请求直接拒绝,直到堆积的线程处理完成。相当于是以资源为维度, 隔离出了每一种资源对应的不同线程数。这样乍一看,好像和QPS没啥区别啊

假设这样一种情况,数据库连接池设置的最大连接数假如是50,有这样一个接口/getUserOrder/{id},获取历史订单列表的接口,如果说平时这个接口查询的很快,在200ms左右,突然有用户订单列表超大,向下面翻了超多超多页,然后你还没有做过这样的优化,就可能会导致这个查询很慢,假如要10秒钟,也就是说这10秒钟是被这一个线程给占住了的,如果说一瞬间来了40个这样慢的接口,也就是说留给其他业务的数据库连接只有区区10个了,后续所有需要访问数据库的请求(即使是那些简单的、快的请求)在尝试获取数据库连接时都会超时或失败。这种情况,基于QPS是无法控制的。因为就算QPS设置为5,第1、2、3、4、5、6秒分别来了5个请求,三十个连接照样被消耗了。

所以说,这个可以基于并发线程数,来针对此接口限流处理,来限制同时执行该订单查询逻辑(特别是执行SQL部分)的线程数量,确保即使出现慢查询,也不会耗尽数据库连接池和应用服务器线程池,保护其他正常请求的处理能力。

所以说,这个阈值类型要与QPS来比较一下了:

QPS规则只关心请求进入的速率,完全不关心每个请求执行需要多长时间。它不知道一个请求是1ms完成还是10s完成,QPS规则在响应时间恶化时,仍在放行远超系统实际处理能力的请求量,直接导致资源(连接池、线程池)被慢查询占满。它没有根据资源实际饱和度进行调整。

QPS统计是基于时间窗口(如1秒)的。突发流量可能在窗口开头瞬间涌入大量请求(比如前100ms涌入50个慢查询),即使QPS没超阈值(整个1秒可能就50个请求),但这50个慢查询已经瞬间耗尽了50个连接池连接!QPS规则对此无能为力,因为它看的是1秒的总量,不是瞬间并发。此外呢, QPS规则对所有请求一视同仁。它无法优先让快请求通过,而阻止慢请求(虽然慢请求可能更耗资源)。在QPS限制下,宝贵的请求配额可能被慢查询消耗掉。

哦吼,那你这么说,我只用并发线程数就好了呗!

那肯定是不行了,比如说仅设置并发线程数的阈值,假设设置为20,但是这个接口响应挺快的,一秒钟有30个请求过来了,却有10个收到了访问失败,这对于该接口甚至是用户来说,体验是完全不好的,因为这样一个响应快的接口QPS居然约等于20了,所以说这种情况使用QPS阈值显然是更合理的。

特性QPS 限流线程数限流说明
控制目标请求进入速率同时执行的请求数 (并发度)目标不同
与资源关系间接 (需依赖正确估算处理能力)直接 (阈值=资源容量 x 缓冲系数)线程数规则与资源池容量天然绑定
是否感知处理时间❌ 不感知✅ 天然免疫慢调用影响线程数规则只看“是否在执行”,不关心“执行多久”
应对慢调用效果可能失效 (阈值需动态下调,难实现)始终有效 (只要阈值≤资源容量)慢调用时,QPS规则需调低阈值才有效,但响应时间恶化往往是突发的、不可预知的
突发流量防御依赖窗口统计,有滞后风险瞬时判断,实时防御线程数规则对瞬间涌来的慢查询有更强拦截力
阈值设定依据系统正常吞吐量 (压测结果)下游资源容量 (连接池/线程池大小)线程数规则的设定依据更稳定、更直观
保护本质防止系统总请求量过载防止并发竞争导致稀缺资源耗尽线程数规则直击慢调用雪崩的核心痛点

实际上二者是搭配使用为最好:第一道防线使用QPS限流防止总量超出系统的最大理论处理能力(即使所有请求都很快),设置在入口资源(如Web API端点);

第二道防线使用并发线程数控制, 防止慢调用资源竞争导致关键下游资源(数据库连接池、内部线程池、外部服务连接)被耗尽。

2.4 熔断规则介绍

除了流量控制以外,对调用链路中不稳定的资源进行熔断降级也是保障高可用的重要措施之一。一个服务常常会调用别的模块,可能是另外的一个远程服务、数据库,或者第三方 API 等。例如,支付的时候,可能需要远程调用银联提供的 API;查询某个商品的价格,可能需要进行数据库查询。然而,这个被依赖服务的稳定性是不能保证的。如果依赖的服务出现了不稳定的情况,请求的响应时间变长,那么调用服务的方法的响应时间也会变长,线程会产生堆积,最终可能耗尽业务自身的线程池,服务本身也变得不可用。

需要对不稳定的弱依赖服务调用进行熔断降级,暂时切断不稳定调用,避免局部不稳定因素导致整体的雪崩。

Sentinel 提供以下几种熔断策略:

  • 慢调用比例 (SLOW_REQUEST_RATIO):选择以慢调用比例作为阈值,需要设置允许的慢调用 RT(即最大的响应时间),请求的响应时间大于该值则统计为慢调用。当单位统计时长(statIntervalMs)内请求数目大于设置的最小请求数目,并且慢调用的比例大于阈值,则接下来的熔断时长内请求会自动被熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求响应时间小于设置的慢调用 RT 则结束熔断,若大于设置的慢调用 RT 则会再次被熔断。
  • 异常比例 (ERROR_RATIO):当单位统计时长(statIntervalMs)内请求数目大于设置的最小请求数目,并且异常的比例大于阈值,则接下来的熔断时长内请求会自动被熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求成功完成(没有错误)则结束熔断,否则会再次被熔断。异常比率的阈值范围是 [0.0, 1.0],代表 0% - 100%。
  • 异常数 (ERROR_COUNT):当单位统计时长内的异常数目超过阈值之后,并且请求数目大于设置的最小请求数目会自动进行熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求成功完成(没有错误)则结束熔断,否则会再次被熔断。
@RestController
@RequestMapping("/hystrix")
public class HystrixController {public static final AtomicInteger count = new AtomicInteger(1);@RequestMapping("/test")@SentinelResource(value = "hystrix", fallback = "testFallback")public String test() {if ( count.get() <= 2 ) {count.incrementAndGet();throw new RuntimeException();}count.incrementAndGet();return "hello world";}public String testFallback(Throwable e) {System.out.println("降级了");return "降级了";}
}

当我们设置如下规则:【这里只演示一下异常数】

我们使用jmeter每秒钟发送3个看看。持续7秒,按照猜想,应该是只有最后那两秒左右的请求是返回的正常值。

剩下两种就交由读者自行探索了。

2.5 规则扩展–持久化

上面的例子都是基于内存存储规则的,就是说我们的SpringBoot项目每次重启的时候,我们先前已经配置好的规则就会被清除掉,就木有咯,每次都要重新配置的话那是相当繁琐哇。

Sentinel 提供两种方式修改规则:

  • 通过 API 直接修改 (loadRules)
  • 通过 DataSource 适配不同数据源修改

我们可以将规则存储在文件、数据库或者配置中心当中。DataSource 接口给我们提供了对接任意配置源的能力。相比直接通过 API 修改规则,实现 DataSource 接口是更加可靠的做法。

这里贴上官网的示意图:

就是控制台修改规则后,推送到“规则中心”去,然后“规则中心”将规则推送到应用中去。【这种方式是官方建议的方式,也叫做推模式:规则中心统一推送,客户端通过注册监听器的方式时刻监听变化,比如使用 Nacos、Zookeeper 等配置中心。这种方式有更好的实时性和一致性保证。】

那就是说还有另一种方式了,拉模式:客户端主动向某个规则管理中心定期轮询拉取规则,这个规则中心可以是 RDBMS、文件,甚至是 VCS 等。这样做的方式是简单,缺点是无法及时获取变更;


现在来以Nacos为sentinel数据源,演示推模式:【本文以本地Nacos单机模式启动】

首先启动nacos,创建属于sentinel的命名空间,然后在该命名空间内创建两个文件,如下图所示:

测试controller

@RestController
@RequestMapping("/nacos")
public class NacosDatasourceController {// 测试流控@RequestMapping("/testFlow")@SentinelResource(value = "testFlow", fallback = "testFallback")public String testFlow() {return "testFlow";}// 测试熔断@RequestMapping("/testDegrade")@SentinelResource(value = "testDegrade", fallback = "testFallback")public String testDegrade() {Random random = new Random();boolean b = random.nextBoolean();if (b) {throw new RuntimeException("触发熔断");}return "testDegrade";}public String testFallback(Throwable e) {System.out.println("降级了 " + e.getMessage());return "访问这么频繁吗??";}
}

文件内容如下:

// 流控规则配置文件 sentinel-flow-rules
[{"resource": "testFlow", // 资源名"limitApp": "default", // 来源应用 若为default,则不区分调用来源"grade": 1, // 阈值类型,0表示并发线程数,1表示QPS"count": 5, // 单机阈值"strategy": 0, // 流控模式,0-表示直接,1-表示关联,2-表示链路"controlBehavior": 0, // 流控效果,0-表示快速失败,1-表示Warm Up(预热模式),2-表示排队等待"clusterMode": false // 是否集群/*"warmUpPeriodSec": 10, // 预热时间(秒,预热模式需要此参数)"maxQueueingTimeMs": 500, // 超时时间(排队等待模式需要此参数)"refResource": "test" // 关联资源、入口资源(关联、链路模式)*/}
]// 熔断规则配置文件 sentinel-degrade-rules
[{"resource": "testDegrade",  // 资源名"grade": 2, // 熔断策略,0-慢调用比例,1-异常比例,2-异常数"count": 1, // 慢调用比例下为最大RT,单位ms;异常比例下为比例阈值,异常数下为异常数"timeWindow": 5, // 熔断时长,单位:s"minRequestAmount": 5, // 最小请求数量"statIntervalMs": 1000 // 统计时长,单位ms}
]

然后我们需要将springboot配置文件修改一下:【bootstrap.yml】

引入之前需要引入这样一个依赖:

<!--bootstrap-->
<!--在SpringBoot 2.4.x的版本之后,对于bootstrap.properties/bootstrap.yaml配置文件(我们合起来成为Bootstrap配置文件)的支持,需要导入如下的依赖-->
<dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-bootstrap</artifactId><version>3.1.6</version>
</dependency>

bootstrap.yml如下:

spring:cloud:nacos:config:server-addr: 127.0.0.1:8848  # Nacos 地址namespace: 53ae4f59-f5c6-45f1-b0d1-fc27158f75ac # 命名空间group: DEFAULT_GROUPsentinel:transport:dashboard: localhost:10000   # Sentinel 控制台地址port: 8719eager: true                   # 立即初始化 Sentineldatasource:# 流控规则数据源flow-rule:nacos:server-addr: ${spring.cloud.nacos.config.server-addr}namespace: ${spring.cloud.nacos.config.namespace}dataId: sentinel-flow-rulesgroupId: DEFAULT_GROUPrule-type: flowdata-type: json# 熔断规则数据源degrade-rule:nacos:server-addr: ${spring.cloud.nacos.config.server-addr}namespace: ${spring.cloud.nacos.config.namespace}dataId: sentinel-degrade-rulesgroupId: DEFAULT_GROUPrule-type: degradedata-type: json #指定文件配置的是哪种规则 flow-流控,degrade-熔断,param-flow热点参数,system-系统保护,authority-授权,gw-flow gateway网关流控,gw-api-group

这样配置后,再启动项目,就可以在sentinel的控制台看到,流控规则和熔断规则有这俩东西了,不需要重新创建规则了。然后jmeter测试访问ok。

需要注意的事:

此时,只是在springboot启动的时候,将规则中心数据源里面的规则加载到客户端里面,然后Sentinel控制台也可以看得到(这样一来每次重启的时候就不用重新添加规则了),但是,我们在Sentinel控制台上修改的规则,并不会同步到Nacos的配置文件中去,但是会对客户端生效。如果在Nacos配置中心里面修改文件然后发布,Sentinel控制台是可以识别到的!所以,我们需要定制一下Sentinel-dashboard这个项目,才能达到在Sentinel控制台修改同步到Nacos该目的。本文就不深究了,给出一篇文章:【来自知乎】https://zhuanlan.zhihu.com/p/663149010

当然,还可以用其他方式,比如说文件,数据库,Redis等等。本文就不一 一列举了。

3. 案例

3.1 秒杀大促

本节示例见文章开头的代码仓库中的【seckill-promotion-demo】模块.

现在结合一个小案例,来看看我们的Sentinel可以发挥何种神威。【本节仅仅是抛砖引玉啊,欢迎各位在评论区探讨】

一个秒杀项目,我们可以从前端,网关(在3.2节),应用层,以及兜底措施去全链路分析,秒杀就是瞬时流量高峰的场景,我们需要围绕流量控制、库存安全、接口安全、性能优化、熔断降级等核心目标展开。

一个秒杀活动,大部分都是先从后台由平台运营人员制定,创建好活动信息以及时间之后,然后用户才能在手机上或者网站的秒杀模块上面可以看到。试想一下,各位读者看到淘宝或者京东等平台的大促活动,是不是会在这些版块疯狂查看相关信息呢(比如说618,双11等等)?这个时候可能会有大量的查询请求,如果全部由可怜的数据库来承受,虽然只是查询,但是压力也不小啊。

所以,我们可以对这些查询的数据事先放到缓存里面去(比如说Redis),来给数据库上一个保护,Redis承受这种能力比数据库可要强喔,实在怕Redis都扛不住的,可以搞Redis主从集群吧。反正就是给数据库上一个保护。

如果说,查询信息包含了很多秒杀商品的图片这种静态资源,我们可以考虑根据用户设备(手机、PC)返回不同尺寸(如手机用 400x400,PC 用 800x800),避免 “大图片在小屏幕上浪费带宽”,同时呢,还可以用 CDN 加速图片分发。实在不行的,我们把秒杀商品信息界面,做成静态界面HTML,配置通过Nginx直接访问,就不走后端接口了,只有库存等信息通过AJAX请求渲染。

前端其实对秒杀的过滤也会起到一定作用的,比如说,用户点击“立即购买”按钮的时候,立即禁用按钮,防止重复提交(前端JS控制,有一定作用,但是治标不治本),如果想恶心用户的话,我们还可以给提交请求加一个验证码(图片、滑块验证码等等,可以过滤一些机器人请求,哈哈哈)。

然后对于提交秒杀订单的接口,为了避免瞬时流量过大,我们可以结合sentinel,对该接口限流,直接返回“当前商品太火爆”等等。

当用户经过一系列校验,并且也没有被限流拦住,那么就会进入到扣减库存生成订单的环节了,这个环节我们需要保证的是:库存安全,既要保证性能的同时,又要保证秒杀商品不能超卖,由于创建秒杀活动的时候我们将库存数量存到了Redis中,固我们可以使用lua脚本来扣减库存,保证“校验数量”,“库存扣减”这两步操作的原子性。

紧接着到了生成订单的环节了,此时我们可以选择直接插入数据库生成订单数据,也可以考虑异步入库的形式,如果瞬时订单太大,使用MQ异步入库可以缓解数据库压力,既然引入了MQ,那么就需要注意MQ所带来的问题了【比如说,消息丢失,消息幂等性等经典问题】。

从上图可以看到从创建活动,到进入支付环节的大致流程了,可以看到引入了好几个中间件了,系统复杂度直线上升!但是可以承受的并发数量是明显要多了的。

现在看一下抢购那个接口的:

// 2.抢购活动中的某个商品
@PostMapping("/seckillGood")
@SentinelResource(value = "seckillGood", fallback = "seckillGoodFallback") // 添加sentinel限流
public R seckillGood(@RequestBody SeckillDto seckillDto) {//log.info("【秒杀开始】-- {}", seckillDto);String msg = orderTabService.seckillOrder(seckillDto);if ( "恭喜您抢到了!".equals( msg) ) {log.info("【秒杀成功】-- {}", seckillDto);}return R.success().setData("msg", msg);
}// service层
@Service("orderTabService")
@Slf4j
public class OrderTabServiceImpl extends ServiceImpl<OrderTabDao, OrderTab> implements OrderTabService {@Resourceprivate RedissonClient redissonClient;@Resourceprivate RedisTemplate<String, Object> redisTemplate;@Overridepublic String seckillOrder(SeckillDto seckillDto) {// TODO 校验参数 比如说时间范围啊等等基本参数 有无该活动、或者该商品是否在该活动里面[ 这些细节就交由读者自行实现了]Integer activityId = seckillDto.getActivityId();Integer goodsId = seckillDto.getGoodsId();// 尝试锁库存 -- 可以用分布式锁redissonString lockKey = buildDistributedLockKey(activityId, goodsId);RLock lock = redissonClient.getLock(lockKey);try {boolean tryLock = lock.tryLock(3, -1, TimeUnit.SECONDS); // leaseTime指定为-1,启动默认的看门狗if (!tryLock) {log.info("获取锁失败");return "当前活动人数较多,请重试!";}// 获取锁成功 判断并扣减库存String res = judgeStocksAndDec(activityId, goodsId, seckillDto.getNum());if ( !"ok".equals(res) ) return res; // 扣减库存失败-库存不足了// TODO 创建订单// 方式一:直接创建订单// 方式二:发送消息到消息队列-创建订单} catch (InterruptedException e) {log.info("获取锁出现异常");} finally {if ( lock != null && lock.isHeldByCurrentThread()) {lock.unlock();}}return "恭喜您抢到了!";}private String judgeStocksAndDec(Integer activityId, Integer goodsId, Integer goodsNum ) {// 2.lua脚本原子叛断库存,并扣减库存RedisScript<Long> redisScript = getScript(); // 定义Lua脚本String key = RedisKey.SECKILL_ACTIVITY_STOCK + activityId;Long result = redisTemplate.execute(redisScript, // 脚本Arrays.asList(key, String.valueOf(goodsId)), // keysgoodsNum // 参数);if ( result == 0 ) return "库存不足!";return "ok";// 1.普通方法//Integer goodsStock = (Integer) redisTemplate.opsForHash().get("seckillAct" + seckillId, String.valueOf(goodsId));//log.info("redis {}",goodsStock);//if ( goodsStock == null || goodsStock < goodsNum) return "库存不足!";//redisTemplate.opsForHash().increment("seckillAct" + seckillId, String.valueOf(goodsId), -goodsNum);//return "ok";}private static RedisScript<Long> getScript() {String script ="local current = redis.call('HGET', KEYS[1], KEYS[2])\n" +"if not current or tonumber(current) < tonumber(ARGV[1]) then\n" +"    return 0\n" +"else\n" +"    redis.call('HINCRBY', KEYS[1], KEYS[2], -tonumber(ARGV[1]))\n" +"    return 1\n" +"end";return new DefaultRedisScript<>(script, Long.class);}private String buildDistributedLockKey(Integer activityId, Integer goodsId) {// 同一个活动中的同一个商品只允许一个线程来扣库存return "seckill:activity:" + activityId + ":goods:" + goodsId;}
}// sentinel规则 -- 这里就用这种形式了,图简单嘛
@Configuration
public class SentinelRules {@PostConstructpublic void init() {initFlowRules();}public void initFlowRules() {List<FlowRule> rules = new ArrayList<>();FlowRule rule = new FlowRule();rule.setResource("seckillGood");         // 资源名称(与@SentinelResource的value对应)rule.setGrade(RuleConstant.FLOW_GRADE_QPS);  // 限流类型:QPSrule.setCount(700);                   // 阈值:每秒最多700次请求rule.setControlBehavior(RuleConstant.CONTROL_BEHAVIOR_DEFAULT); // 直接拒绝rules.add(rule);FlowRuleManager.loadRules(rules);}
}

现在有活动ID20,里面有一个商品ID1,秒杀个数是5个,然后我们使用jmeter启动1000个线程看看效果,参数如下

{"username": "jack","info": "少放辣椒","activityId": 20,"goodsId": 1,"num": 1
}

发现只卖出去五个,然后去redis里面看,发现该商品秒杀库存刚好是0。同时sentinel限流起到了一定作用,对于超出qps阈值的,迅速返回了。sentinel还可以这样,对查询接口的流控模式设置为关联模式,关联提交订单,当提交订单超过设置的阈值的时候,对查询接口做适应的限流操作。

示例代码中的案例比较简单,参数校验,时间判断等等方面都没有做,仅仅是将核心流程给展示了一遍。实际生产中,往往还有最后一步,那就是支付环节,采用第三方支付,往往都是在支付回调里面修改订单的支付状态的,同时还有超时支付,超过时间订单就自动关闭的。这样需要注意一个小问题,那就是订单过期的临界点问题:那就是假如 预计10:00:00订单过期,用户在9:59:59秒完成支付,回调是在10:00:03到达的,就可能会发生支付成功了,但是由于订单过期把状态改错了。

所以,订单预计过期时间可以稍微多一点儿,预留给支付回调。与此同时,假设进入了订单过期的逻辑里面,我们可以在该逻辑里面向第三方平台主动查询订单的状态,从而也可以避免把订单状态改错的情况。这里最后要说的就是这个小问题。

3.2 热点参数

不知各位读者是否还记得缓存中的hotkey问题,本文探讨的是流量治理,所以Sentinel有针对于热点参数的限流。Sentinel 的 热点参数限流 是针对请求中特定参数的高频访问场景设计的精细化流量控制策略。其核心是通过 参数级别的统计动态阈值调整,对高频参数值进行限流,防止热点数据引发系统过载

@RestController
@RequestMapping("/hotParams")
public class HotParamsController {@GetMapping("/get")@SentinelResource(value = "getRequest", fallback = "getRequestFallBack")public String get(String userId, String goodsId) {System.out.println("userId:" + userId + " goodsId:" + goodsId);return "success";}public String getRequestFallBack(String userId, String goodsId) {System.out.println("goodsId参数过于火爆了!!!");return "fallback";}
}@Configuration
public class SentinelHotParamsRules {@PostConstructpublic void init() {// 添加热点参数规则initHotParamsRules();}private void initHotParamsRules() {// 创建热点参数规则ParamFlowRule rule = new ParamFlowRule("hotResource").setParamIdx(1)          // 参数索引(从0开始).setCount(10)            // 全局默认阈值(QPS).setDurationInSec(1)     // 统计窗口时长(秒).setControlBehavior(RuleConstant.CONTROL_BEHAVIOR_DEFAULT); // 流控效果// 添加参数例外项(如特定商品ID放宽阈值)ParamFlowItem item = new ParamFlowItem().setObject("520")    // goodsId = 520 放宽.setCount(50);       // 例外阈值 qps50rule.setParamFlowItemList(Collections.singletonList(item));// 加载规则ParamFlowRuleManager.loadRules(Collections.singletonList(rule));}
}

从查看结果树可以看到,第二个请求的qps确实是放宽了。

参数索引就是@SentinelResource注解标注的方法的参数列表了,比如说参数索引是1,那么就是goodsId参数了。默认情况下,Sentinel 只能直接支持从 URL 查询参数或方法参数中提取热点参数(例如:String userId, String goodsId)。

end. 参考

  1. https://sentinelguard.io/zh-cn/docs/quick-start.html 【官网】
  2. https://blog.csdn.net/qq_41712271/article/details/118336309
http://www.dtcms.com/a/270406.html

相关文章:

  • 行业实践案例:医疗行业数据治理的挑战与突破
  • 【RAG知识库实践】数据源Data Source
  • ABP VNext + .NET Minimal API:极简微服务快速开发
  • B. Shrinking Array/缩小数组
  • Web后端实战:(部门管理)
  • 数据结构*搜索树
  • 二极管常见种类及基本原理
  • 【牛客刷题】小红的red字符串
  • MyBatis-Plus:提升数据库操作效率的利器
  • AB实验的长期影响
  • 【数据结构】复杂度分析
  • SpringBoot框架完整学习指南
  • [创业之路-489]:企业经营层 - 营销 - 如何将缺点转化为特点、再将特点转化为卖点
  • 钉钉企业应用开发技巧:在单聊会话中实现互动卡片功能
  • 学习日记-spring-day43-7.8
  • 基于物联网架构的温室环境温湿度传感器节点设计
  • 扣子Coze纯前端部署多Agents
  • WouoUI-Page移植
  • Java-Collections、Map
  • H3初识——入门介绍之常用中间件
  • 11款常用C++在线编译与运行平台推荐与对比
  • ffmpeg 中config 文件一些理解
  • Flutter基础(前端教程②-卡片列表)
  • study_WebView介绍
  • MYSQL进阶知识
  • 在keil中使用stlink下载程序报错Invalid ROM Table
  • Day07_C语言IO进程线程(重难点)
  • TensorFlow 和PyTorch的全方位对比和选择建议
  • Latex几种常用的花体
  • [2-02-02].第04节:环境搭建 - Linux搭建ES集群环境