mirror of
https://github.com/CyC2018/Interview-Notebook.git
synced 2026-10-31 19:13:42 +00:00
Update 系统设计基础.md
补充限流、熔断与降级的区别和熔断器的状态变化,新增降级、防止超卖的几种做法、Redis 与数据库的库存一致三节,补充压测的注意事项,附录 A18、A19。
This commit is contained in:
+252
-1
@@ -117,6 +117,8 @@
|
||||
- 降级:主动关闭部分非核心功能,从而保证核心功能可用;
|
||||
- 熔断:某个依赖的服务持续出错时,暂时不再调用它而是直接返回默认结果,避免故障沿着调用链扩散。
|
||||
|
||||
三者针对的对象不同:限流保护的是自己,按进入的请求量拒绝多余的请求;熔断针对的是下游依赖,按依赖的失败情况停止调用;降级是被限流、被熔断或主动关闭功能之后返回的替代结果。三者通常一起使用,例如熔断器打开时执行的就是降级逻辑。
|
||||
|
||||
#### 1. 限流算法
|
||||
|
||||
限流要判断的是:最近一段时间已经放行了多少请求,下一个请求还能不能放行。下面都以“每个用户每秒最多下单 5 次”为例。
|
||||
@@ -194,6 +196,58 @@ return allowed
|
||||
|
||||
<div align="center"> <img src="pics/circuit-breaker.png" width="520px"/> </div><br>
|
||||
|
||||
熔断要解决的问题是下游变慢时调用方被拖垮。例如库存服务卡住,订单服务调用它的读取超时为 2 秒、每秒调用 200 次,按 Little 定律,约有 200 × 2 = 400 个线程同时在等待。线程池耗尽后,不依赖库存的接口也无法响应,故障就沿调用链传到了上游。熔断器发现下游持续失败后直接拒绝调用,调用方立即执行降级逻辑,线程不再被占住,下游也得到了恢复的时间。
|
||||
|
||||
是否打开由最近一段时间的调用统计决定。以 Resilience4j(Spring Cloud Circuit Breaker 常用的实现)为例,主要的参数和默认值:
|
||||
|
||||
- 统计窗口:最近 100 次调用,也可以改为最近若干秒;
|
||||
- 最小调用数:窗口内至少有 100 次调用才计算失败率,避免刚启动时 1 次失败就算作 100% 的失败率;
|
||||
- 阈值:失败率达到 50% 时打开,也可以按慢调用的比例打开,下游没有报错、只是变慢时也能熔断;
|
||||
- 打开时长:60 秒,之后进入半开状态;
|
||||
- 半开时放行的请求数:10 个,这些试探请求的失败率仍达到阈值就重新打开,否则关闭,其余请求在试探完成前仍被拒绝。
|
||||
|
||||
Sentinel 的半开状态只放行一个请求,这个请求成功就结束熔断,失败则再次熔断。
|
||||
|
||||
例如把统计窗口改为最近 10 次、最小调用数 10、打开时长 2 秒、半开时放行 3 个,库存服务从第 5 次调用开始超时,每次调用的结果如下(Resilience4j 2.2 实测,见[附录 A18](#a18-熔断器的状态变化)):
|
||||
|
||||
```text
|
||||
#01~#04 库存 37
|
||||
#05~#10 失败(读取超时),降级:有货 第 10 次后失败率 60%,CLOSED → OPEN
|
||||
#11~#13 被熔断,降级:有货 不再调用库存服务
|
||||
(2 秒后,库存服务仍然超时)
|
||||
#14~#16 失败(读取超时),降级:有货 OPEN → HALF_OPEN,3 次试探都失败,回到 OPEN
|
||||
#17 被熔断,降级:有货
|
||||
(再过 2 秒,库存服务已恢复)
|
||||
#18~#20 库存 37 OPEN → HALF_OPEN,3 次试探都成功,HALF_OPEN → CLOSED
|
||||
#21 库存 37
|
||||
```
|
||||
|
||||
21 次调用中只有 17 次真正发给了库存服务。需要注意的是,打开时长过后要有调用到来才会转为半开,默认不启动后台线程检查状态,所以是第 14 次调用触发了 OPEN → HALF_OPEN。
|
||||
|
||||
熔断依赖超时:没有设置超时的调用只会一直等待,不会失败,熔断器也就无从统计,所以每个远程调用都要先设好超时,见 [OpenFeign](微服务.md#openfeign) 中的超时配置。
|
||||
|
||||
重试则要克制。只对幂等的操作、可能自行恢复的错误(例如超时、连接被拒绝)重试,次数要有上限,间隔按指数增长并加上随机抖动;参数错误、余额不足这类重试也不会成功的错误直接失败。还要避免每一层都重试:调用链上 3 层各重试 3 次,一次用户操作在最底层最多产生 4 × 4 × 4 = 64 次请求([Addressing Cascading Failures](https://sre.google/sre-book/addressing-cascading-failures/))。熔断器打开后,对这个依赖的重试也随之停止。
|
||||
|
||||
#### 4. 降级
|
||||
|
||||
降级指的是在资源不足或依赖故障时,用代价更低的结果代替完整的结果,以保住核心功能。限流和熔断决定的是“什么时候不再接收、不再调用”,降级决定的是“这时返回什么”。
|
||||
|
||||
常见的降级做法:
|
||||
|
||||
- 返回缓存或旧的数据:例如推荐服务超时时,返回前一天算好的热门商品;
|
||||
- 返回默认值或简化的结果:库存服务被熔断时,详情页显示“有货”,下单时仍以数据库为准;
|
||||
- 关闭非核心功能:大促时关闭评论、积分抵扣,把资源留给下单和支付;
|
||||
- 延后写操作:积分、通知这类操作先写入消息队列,下游恢复后再处理;
|
||||
- 快速失败:核心功能无法降级时,立即提示“稍后再试”,而不是让请求一直等待。
|
||||
|
||||
降级有两种触发方式。自动降级由超时、熔断和限流触发,兜底的逻辑(fallback)事先写在代码中,例如上面熔断时返回“有货”。手动降级由开关控制,开关放在[配置中心](微服务.md#六配置中心),大促前或故障时修改配置即可生效,不需要重新发布。
|
||||
|
||||
哪些功能可以降级、降级成什么样,要在设计时和业务方一起确定。例如商品详情页依赖商品、价格、库存、评论、推荐五个服务:商品和价格是核心,失败时返回错误;库存失败时显示“有货”;评论和推荐失败时不显示这一块。
|
||||
|
||||
大模型应用中也是同样的思路:模型接口超时或被限流时,切换到备用的模型或更小的模型,或者返回缓存的回答;Agent 调用工具超时时,把“工具不可用”作为工具的结果返回给模型,由模型换一种方式回答或如实告知用户,而不是让整个请求失败。
|
||||
|
||||
需要注意的是,降级逻辑平时很少执行,容易到真正需要时才发现它本身有问题,例如兜底的缓存早已过期、开关关闭后功能没有恢复。因此降级方案要和正常功能一样测试,并在故障演练中实际打开一次。
|
||||
|
||||
## 五、安全性
|
||||
|
||||
要求系统在应对各种攻击手段时能够有可靠的应对措施,常见的攻击手段和防范方法见 [攻击技术](攻击技术.md)。
|
||||
@@ -238,7 +292,47 @@ Redis 执行 Lua 脚本时不会穿插执行其它命令,判断是否已购、
|
||||
UPDATE goods SET stock = stock - 1 WHERE id = 1001 AND stock > 0;
|
||||
```
|
||||
|
||||
一人一单可以用订单表上(用户 ID,商品 ID)的唯一索引保证,消息被重复消费时插入失败,也就实现了幂等。Redis 中扣减成功、但订单最终创建失败时,需要把 Redis 中的库存和购买记录恢复,并定期与数据库对账。
|
||||
一人一单可以用订单表上(用户 ID,商品 ID)的唯一索引保证,消息被重复消费时插入失败,也就实现了幂等。Redis 中扣减成功、但订单最终创建失败时,需要把 Redis 中的库存和购买记录恢复,并定期与数据库对账,做法见下文。
|
||||
|
||||
### 防止超卖的几种做法
|
||||
|
||||
超卖的根源是“判断库存”和“扣减库存”不是一个原子操作:两个请求都读到库存为 1,都判断有货,都去扣减。所以防止超卖的做法都是让判断和扣减成为一个整体,区别在于在哪里做、代价是什么。
|
||||
|
||||
在数据库中有三种写法:
|
||||
|
||||
- 悲观锁:在事务中先用 SELECT ... FOR UPDATE 锁住库存行,再判断和扣减,其它请求在锁上排队;
|
||||
- 乐观锁:表中加一个版本号,UPDATE 时带上条件 version = 读到的值,影响行数为 0 说明这一行已被别人修改,放弃或重试;
|
||||
- 条件更新:把判断写进 UPDATE 的条件 stock > 0,判断和扣减由一条语句在行锁的保护下完成。
|
||||
|
||||
用 64 个线程模拟抢购库存为 100 的商品,每个用户请求 1 次,每种写法运行 3 轮(MySQL 8.0 实测,见[附录 A19](#a19-防止超卖的几种写法)):
|
||||
|
||||
```text
|
||||
写法 用户数 卖出 剩余库存
|
||||
先查询再扣减(对照) 1000 134~162 -62~-34
|
||||
悲观锁 FOR UPDATE 1000 100 0
|
||||
乐观锁,不重试 1000 70~83 17~30
|
||||
乐观锁,最多重试 3 次 1000 100 0
|
||||
乐观锁,最多重试 3 次 150 48~57 43~52
|
||||
条件更新 stock > 0 1000 100 0
|
||||
```
|
||||
|
||||
先查询再扣减多卖了 34~62 件。乐观锁不会超卖,但同一时刻读到同一个版本号的请求中只有一个能更新成功,其余请求即使还有库存也会失败:150 个用户抢 100 件,重试 3 次也只卖出了 48~57 件,出现了“少卖”。因此版本号适合冲突少的场景,例如修改个人资料;库存这类同一行被大量请求同时修改的场景用条件更新,它不需要重试,语句也最少。悲观锁同样正确,但锁要持有到事务提交,事务中的操作越多,其它请求等待得越久。
|
||||
|
||||
无论哪种写法,同一行的更新在数据库中都是串行执行的,单行能承受的写入量有上限,见[热点行](#1-热点行)。所以秒杀先用 Redis 挡住绝大部分请求,数据库的条件更新作为最后一道防线。
|
||||
|
||||
### Redis 与数据库的库存一致
|
||||
|
||||
库存同时保存在 Redis 和数据库中,两者无法在一个事务中同时修改,所以先要确定以谁为准:数据库中的库存和订单是最终结果,Redis 中的库存只是一道闸门,用来挡住注定失败的请求。因此可以接受 Redis 的库存暂时偏少(少卖,事后补回),但数据库不能超卖,数据库的条件更新始终保留。
|
||||
|
||||
按请求经过的步骤,可能出现的不一致和处理方法:
|
||||
|
||||
- Lua 脚本扣减成功,发送消息失败:发送失败时立即执行一段反向的脚本,把库存加回、把用户移出已购集合;应用恰好在两步之间宕机时,这次扣减只留在 Redis 中,由对账发现。也可以用 RocketMQ 的[事务消息](分布式.md#本地消息表),先发送半消息,执行 Lua 脚本后再提交,应用宕机时由 Broker 回查用户是否在已购集合中;
|
||||
- 消息已发出,订单服务创建订单失败:数据库库存不足、用户校验不通过这类业务上的失败,订单服务记录失败结果并回补 Redis;网络超时、数据库暂时不可用这类异常则依靠消息重试,订单表的唯一索引保证重试不会创建两个订单;
|
||||
- 订单创建后用户未支付:超时取消订单时,在同一个事务中修改订单状态并把数据库的库存加回,提交后再回补 Redis,超时取消可以用[延迟消息](消息队列.md#延迟消息)实现。
|
||||
|
||||
回补 Redis 时要防止重复执行,例如在脚本中先 SREM 已购集合,返回 1(用户确实在集合中)才把库存加 1,同一条回补消息执行两次也只加一次。
|
||||
|
||||
对账在活动结束后或定期进行:Redis 中的已购用户应当与数据库中的订单一一对应,只出现在 Redis 中的用户说明消息丢失了,补发消息或回补库存。需要注意的是,库存只在活动开始前从数据库一次性加载到 Redis,活动进行中不要再从数据库同步,否则还在队列中的消息会让两边的数字互相覆盖。
|
||||
|
||||
### 同类场景:抢券、抢红包与抽奖
|
||||
|
||||
@@ -569,6 +663,18 @@ Google SRE 对过载的建议是先返回降级的响应,即数据更少、计
|
||||
|
||||
数据库也有类似的拐点。MySQL 主键点查在 16 个连接时为 16.9 万 QPS,128 个连接时为 19.2 万 QPS,吞吐量只增加 14%,平均响应时间却从 0.095 ms 增至 0.668 ms(见[附录 A4](#a4-mysql-主键点查的并发扫描))。压测时客户端与服务端在同一台机器上,共用 10 个核心,吞吐量很快接近这台机器的上限,之后的并发只增加排队时间。热点行的更新则从一开始就没有并发收益。
|
||||
|
||||
压测的结果要能代表生产,容易出错的地方有:
|
||||
|
||||
- 环境:机器规格、实例数和数据量与生产一致,或者按比例缩小;表中只有一万行时,索引和缓冲池的表现与一亿行时完全不同;
|
||||
- 数据的分布:读接口如果总是请求同一个商品,所有请求都命中缓存,测出的只是缓存的上限;请求的参数要按生产的访问日志回放或者按热度分布生成,才能得到真实的命中率和回源量;
|
||||
- 读写分开:先单独压读接口和写接口,分别找到瓶颈(读接口通常是缓存和应用的 CPU,写接口通常是数据库的锁和连接池),再按生产中的比例混合压测;
|
||||
- 压测客户端:客户端的 CPU、连接数和带宽要有余量,不能先于服务端成为瓶颈;
|
||||
- 持续时间:每个并发级别至少运行几分钟,时间太短看不到连接池耗尽、GC 和缓存过期这类随时间出现的问题。
|
||||
|
||||
JMeter、wrk、k6 等工具负责按设定的并发或速率发出请求,并统计延迟分布;同时要看服务端各层的监控。压测的结论应该是“哪一层先到达拐点、原因是什么”,而不只是一个 QPS 数字。
|
||||
|
||||
大促前通常还要在生产环境做全链路压测,因为测试环境很难复现生产的规模和依赖关系。压测请求带上专门的标记(例如一个请求首部),每个服务识别这个标记并传给下游,传递方式与[链路追踪](微服务.md#八链路追踪)中的 traceId 相同,消息和异步任务中也要带上;带标记的请求写入影子库或影子表,不污染真实的订单和库存([阿里云 PTS:线上业务压测的核心要素](https://help.aliyun.com/zh/pts/performance-test-pts-2-0/product-overview/core-elements-for-online-business-stress-testing))。压测安排在业务低峰期,并准备好随时停止压测的开关。
|
||||
|
||||
并发数 = 吞吐量 × 响应时间,这一关系即 Little 定律(Little's law),见[性能指标](#性能指标)。它可以用来估算同时在处理的请求数。例如 10 万 QPS、平均响应时间 5 ms(假设)时,系统中约有 500 个请求同时在处理。单台实例按 1200 QPS 计算时,同时处理约 6 个请求,线程池和连接池要在此基础上留出余量。
|
||||
|
||||
实例数的估算方法是:所需实例数 = 峰值 QPS ÷(单实例拐点 QPS × 目标利用率),再按可用区向上取整。例如应用层的回源为 4 万 QPS,单实例拐点按 2000 QPS 计算(假设,需用服务自己的压测结果替换),目标利用率为 60%:4 万 ÷ 1200 ≈ 33.3,取 34 台,三个可用区各 12 台,共 36 台。一个可用区故障时剩余 24 台,每台约 1667 QPS,仍低于拐点。
|
||||
@@ -1577,6 +1683,148 @@ return redis.call('HINCRBY', KEYS[2], ARGV[1], 1) -- 返回第几张,落库
|
||||
订单 9001 取消,释放 70001(WHERE order_id = 9001 AND status = 1):影响 1 行
|
||||
```
|
||||
|
||||
### A18 熔断器的状态变化
|
||||
|
||||
环境:Resilience4j 2.2.0(resilience4j-circuitbreaker),JDK 21.0.13,Apple M1 Pro。库存服务用一个方法模拟,inventoryDown 为 true 时抛出 TimeoutException:
|
||||
|
||||
```java
|
||||
CircuitBreakerConfig cfg = CircuitBreakerConfig.custom()
|
||||
.slidingWindowType(CircuitBreakerConfig.SlidingWindowType.COUNT_BASED)
|
||||
.slidingWindowSize(10)
|
||||
.minimumNumberOfCalls(10)
|
||||
.failureRateThreshold(50)
|
||||
.waitDurationInOpenState(Duration.ofSeconds(2))
|
||||
.permittedNumberOfCallsInHalfOpenState(3)
|
||||
.build();
|
||||
CircuitBreaker cb = CircuitBreaker.of("inventory", cfg);
|
||||
|
||||
Supplier<String> call = () -> {
|
||||
try {
|
||||
return cb.executeCallable(() -> queryStock(10086));
|
||||
} catch (CallNotPermittedException e) {
|
||||
return "被熔断,降级:有货";
|
||||
} catch (Exception e) {
|
||||
return "失败(" + e.getMessage() + "),降级:有货";
|
||||
}
|
||||
};
|
||||
// 先调用 4 次;inventoryDown = true 后调用 9 次;等待 2.1 秒后调用 4 次;
|
||||
// inventoryDown = false,再等待 2.1 秒后调用 4 次。状态变化由 onStateTransition 事件打印
|
||||
```
|
||||
|
||||
CircuitBreakerConfig.ofDefaults() 的输出中,failureRateThreshold=50.0、slidingWindowSize=100、minimumNumberOfCalls=100、permittedNumberOfCallsInHalfOpenState=10、automaticTransitionFromOpenToHalfOpenEnabled=false、slowCallDurationThreshold=PT1M。运行输出(方括号中是距开始的时间):
|
||||
|
||||
```text
|
||||
#01 库存 37
|
||||
#02 库存 37
|
||||
#03 库存 37
|
||||
#04 库存 37
|
||||
-- 库存服务开始超时
|
||||
#05 失败(读取超时),降级:有货
|
||||
#06 失败(读取超时),降级:有货
|
||||
#07 失败(读取超时),降级:有货
|
||||
#08 失败(读取超时),降级:有货
|
||||
#09 失败(读取超时),降级:有货
|
||||
#10 失败(读取超时),降级:有货
|
||||
[0.01 s] State transition from CLOSED to OPEN
|
||||
#11 被熔断,降级:有货
|
||||
#12 被熔断,降级:有货
|
||||
#13 被熔断,降级:有货
|
||||
-- 等待 2 秒,库存服务仍然故障
|
||||
#14 失败(读取超时),降级:有货
|
||||
[2.12 s] State transition from OPEN to HALF_OPEN
|
||||
#15 失败(读取超时),降级:有货
|
||||
#16 失败(读取超时),降级:有货
|
||||
[2.12 s] State transition from HALF_OPEN to OPEN
|
||||
#17 被熔断,降级:有货
|
||||
-- 再等待 2 秒,库存服务已恢复
|
||||
#18 库存 37
|
||||
[4.22 s] State transition from OPEN to HALF_OPEN
|
||||
#19 库存 37
|
||||
#20 库存 37
|
||||
[4.22 s] State transition from HALF_OPEN to CLOSED
|
||||
#21 库存 37
|
||||
共调用 21 次,真正发给库存服务 17 次
|
||||
```
|
||||
|
||||
状态变化的事件在调用过程中触发,每次调用结束后打印,所以 OPEN → HALF_OPEN 出现在第 14 次和第 18 次调用的结果之后。
|
||||
|
||||
### A19 防止超卖的几种写法
|
||||
|
||||
环境:MySQL 8.0.44(默认的可重复读),Python 3 + PyMySQL,Apple M1 Pro。表 d5_goods(id, stock, version) 中商品 1001 的库存为 100,d5_order 的主键为 (user_id, goods_id)。每种写法用 64 个线程,每个用户请求 1 次,每个线程使用自己的连接(自动提交):
|
||||
|
||||
```python
|
||||
def naive(uid): # 先查询,大于 0 再扣减
|
||||
cur.execute("SELECT stock FROM d5_goods WHERE id = 1001")
|
||||
if cur.fetchone()[0] <= 0: return 'soldout'
|
||||
cur.execute("UPDATE d5_goods SET stock = stock - 1 WHERE id = 1001")
|
||||
cur.execute("INSERT INTO d5_order VALUES (%s, 1001)", (uid,))
|
||||
return 'ok'
|
||||
|
||||
def pessimistic(uid): # SELECT ... FOR UPDATE
|
||||
c.begin()
|
||||
cur.execute("SELECT stock FROM d5_goods WHERE id = 1001 FOR UPDATE")
|
||||
if cur.fetchone()[0] <= 0: c.rollback(); return 'soldout'
|
||||
cur.execute("UPDATE d5_goods SET stock = stock - 1 WHERE id = 1001")
|
||||
cur.execute("INSERT INTO d5_order VALUES (%s, 1001)", (uid,))
|
||||
c.commit(); return 'ok'
|
||||
|
||||
def optimistic(uid, retries=0): # 版本号,冲突时最多重试 retries 次
|
||||
for _ in range(retries + 1):
|
||||
cur.execute("SELECT stock, version FROM d5_goods WHERE id = 1001")
|
||||
stock, ver = cur.fetchone()
|
||||
if stock <= 0: return 'soldout'
|
||||
n = cur.execute("UPDATE d5_goods SET stock = stock - 1, version = version + 1 "
|
||||
"WHERE id = 1001 AND version = %s", (ver,))
|
||||
if n == 1:
|
||||
cur.execute("INSERT INTO d5_order VALUES (%s, 1001)", (uid,))
|
||||
return 'ok'
|
||||
return 'conflict'
|
||||
|
||||
def conditional(uid): # 条件更新
|
||||
n = cur.execute("UPDATE d5_goods SET stock = stock - 1 WHERE id = 1001 AND stock > 0")
|
||||
if n == 0: return 'soldout'
|
||||
cur.execute("INSERT INTO d5_order VALUES (%s, 1001)", (uid,))
|
||||
return 'ok'
|
||||
```
|
||||
|
||||
运行 3 轮的原始输出(耗时受机器负载影响,只作参考):
|
||||
|
||||
```text
|
||||
== 第 1 轮
|
||||
先查询后扣减 请求 1000 成功 162 售罄 838 冲突 0 剩余库存 -62 订单 162 122 ms
|
||||
悲观锁 FOR UPDATE 请求 1000 成功 100 售罄 900 冲突 0 剩余库存 0 订单 100 183 ms
|
||||
乐观锁(不重试) 请求 1000 成功 70 售罄 0 冲突 930 剩余库存 30 订单 70 134 ms
|
||||
乐观锁(重试 3 次) 请求 1000 成功 100 售罄 708 冲突 192 剩余库存 0 订单 100 147 ms
|
||||
条件更新 stock > 0 请求 1000 成功 100 售罄 900 冲突 0 剩余库存 0 订单 100 80 ms
|
||||
先查询后扣减 请求 150 成功 122 售罄 28 冲突 0 剩余库存 -22 订单 122 79 ms
|
||||
悲观锁 FOR UPDATE 请求 150 成功 100 售罄 50 冲突 0 剩余库存 0 订单 100 85 ms
|
||||
乐观锁(不重试) 请求 150 成功 29 售罄 0 冲突 121 剩余库存 71 订单 29 41 ms
|
||||
乐观锁(重试 3 次) 请求 150 成功 55 售罄 0 冲突 95 剩余库存 45 订单 55 64 ms
|
||||
条件更新 stock > 0 请求 150 成功 100 售罄 50 冲突 0 剩余库存 0 订单 100 60 ms
|
||||
== 第 2 轮
|
||||
先查询后扣减 请求 1000 成功 134 售罄 866 冲突 0 剩余库存 -34 订单 134 89 ms
|
||||
悲观锁 FOR UPDATE 请求 1000 成功 100 售罄 900 冲突 0 剩余库存 0 订单 100 162 ms
|
||||
乐观锁(不重试) 请求 1000 成功 83 售罄 0 冲突 917 剩余库存 17 订单 83 112 ms
|
||||
乐观锁(重试 3 次) 请求 1000 成功 100 售罄 634 冲突 266 剩余库存 0 订单 100 155 ms
|
||||
条件更新 stock > 0 请求 1000 成功 100 售罄 900 冲突 0 剩余库存 0 订单 100 83 ms
|
||||
先查询后扣减 请求 150 成功 115 售罄 35 冲突 0 剩余库存 -15 订单 115 39 ms
|
||||
悲观锁 FOR UPDATE 请求 150 成功 100 售罄 50 冲突 0 剩余库存 0 订单 100 81 ms
|
||||
乐观锁(不重试) 请求 150 成功 38 售罄 0 冲突 112 剩余库存 62 订单 38 58 ms
|
||||
乐观锁(重试 3 次) 请求 150 成功 48 售罄 0 冲突 102 剩余库存 52 订单 48 69 ms
|
||||
条件更新 stock > 0 请求 150 成功 100 售罄 50 冲突 0 剩余库存 0 订单 100 56 ms
|
||||
== 第 3 轮
|
||||
先查询后扣减 请求 1000 成功 147 售罄 853 冲突 0 剩余库存 -47 订单 147 87 ms
|
||||
悲观锁 FOR UPDATE 请求 1000 成功 100 售罄 900 冲突 0 剩余库存 0 订单 100 139 ms
|
||||
乐观锁(不重试) 请求 1000 成功 70 售罄 0 冲突 930 剩余库存 30 订单 70 93 ms
|
||||
乐观锁(重试 3 次) 请求 1000 成功 100 售罄 646 冲突 254 剩余库存 0 订单 100 135 ms
|
||||
条件更新 stock > 0 请求 1000 成功 100 售罄 900 冲突 0 剩余库存 0 订单 100 69 ms
|
||||
先查询后扣减 请求 150 成功 126 售罄 24 冲突 0 剩余库存 -26 订单 126 62 ms
|
||||
悲观锁 FOR UPDATE 请求 150 成功 100 售罄 50 冲突 0 剩余库存 0 订单 100 77 ms
|
||||
乐观锁(不重试) 请求 150 成功 31 售罄 0 冲突 119 剩余库存 69 订单 31 56 ms
|
||||
乐观锁(重试 3 次) 请求 150 成功 57 售罄 0 冲突 93 剩余库存 43 订单 57 69 ms
|
||||
条件更新 stock > 0 请求 150 成功 100 售罄 50 冲突 0 剩余库存 0 订单 100 66 ms
|
||||
```
|
||||
|
||||
## 参考资料
|
||||
|
||||
- [Redis: Scripting with Lua](https://redis.io/docs/latest/develop/programmability/eval-intro/)
|
||||
@@ -1597,6 +1845,9 @@ return redis.call('HINCRBY', KEYS[2], ARGV[1], 1) -- 返回第几张,落库
|
||||
- [Checking object integrity for data uploads in Amazon S3](https://docs.aws.amazon.com/AmazonS3/latest/userguide/checking-object-integrity-upload.html)
|
||||
- [CompleteMultipartUpload - Amazon S3 API Reference](https://docs.aws.amazon.com/AmazonS3/latest/API/API_CompleteMultipartUpload.html)
|
||||
- [UploadPart - Amazon S3 API Reference](https://docs.aws.amazon.com/AmazonS3/latest/API/API_UploadPart.html)
|
||||
- [Resilience4j: CircuitBreaker](https://resilience4j.readme.io/docs/circuitbreaker)
|
||||
- [Sentinel 熔断降级](https://sentinelguard.io/zh-cn/docs/circuit-breaking.html)
|
||||
- [阿里云 PTS:线上业务压测的核心要素](https://help.aliyun.com/zh/pts/performance-test-pts-2-0/product-overview/core-elements-for-online-business-stress-testing)
|
||||
- [ListParts - Amazon S3 API Reference](https://docs.aws.amazon.com/AmazonS3/latest/API/API_ListParts.html)
|
||||
- [Uploading objects in Amazon S3](https://docs.aws.amazon.com/AmazonS3/latest/userguide/upload-objects.html)
|
||||
- [MinIO 源码 cmd/api-errors.go、cmd/object-api-errors.go](https://github.com/minio/minio/tree/master/cmd)
|
||||
|
||||
Reference in New Issue
Block a user