秒杀库存解决方案

电商系统中秒杀是一种常见的业务场景需求,其中核心设计之一就是如何扣减库存。本篇主要分享一些常见库存扣减技术方案,库存扣减设计选择并非一味追求性能更佳,更多的应该考虑根据实际情况来进行架构取舍。在商品购买的过程中,库存的抵扣过程通常包括以下步骤:

  1. 开启事务:在开始进行库存抵扣操作前,开启一个事务。
  2. 查询库存:根据商品ID,使用SELECT语句从库存表中查询该商品的当前库存数量。
  3. 检查库存是否足够:将查询到的库存数量与用户购买数量进行比较。如果库存数量大于或等于用户购买数量,则库存足够,可以继续下单。如果库存不足,需要采取相应的处理措施,例如提示用户库存不足或进行库存预订等。
  4. 扣减库存:如果库存足够,根据用户购买数量,使用UPDATE语句将库存表中对应商品的库存数量减去购买数量,得到最新的库存剩余值。
  5. 记录交易明细:在购买过程中,通常需要记录交易明细,例如生成订单记录或交易日志,以便后续查询和跟踪。
  6. 提交事务:以上操作通常会在一个事务中进行,确保操作的原子性。如果所有步骤都成功执行,则提交事务,库存扣减过程完成。如果在任何步骤中出现错误或异常,事务会回滚,恢复到操作前的状态,确保数据的完整性和一致性。

由于涉及到 SELECT后进行UPDATE,以上步骤中存在多事务并发时写覆盖的问题。

悲观锁更新库存

在数据库并发控制中,防止写覆盖是一个重要的问题,特别是在多个会话(事务)同时尝试修改同一行数据时。如果不进行适当的并发控制,可能会导致数据的不一致性和丢失更新。

为了解决这个问题,可以使用 SELECT FOR UPDATE语句。在使用SELECT FOR UPDATE时,数据库会将目标行的数据加上写锁,阻止其他事务在当前事务完成之前修改被锁定的数据。这样,其他会话无法同时修改同一行数据,从而避免了并发写入冲突。

需要注意的是,使用SELECT FOR UPDATE可能会引起一些并发性能问题,因为其他会话需要等待锁释放才能继续执行。因此,在设计并发控制策略时,需要综合考虑并发性能和数据一致性之间的平衡。

在上述流程中,步骤3 改为:

3. 查询库存并锁定:使用SELECT ... FOR UPDATE语句查询指定商品的库存,并将其锁定。这将确保其他并发事务在当前事务提交或回滚之前无法修改该商品的库存。

    # 开始事务connection.begin()# 加锁(FOR UPDATE)并读取当前库存记录cursor.execute("SELECT quantity FROM inventory WHERE id = ? FOR UPDATE", item_id)current_quantity = cursor.fetchone()['quantity']# 检查库存是否足够if current_quantity >= requested_quantity:# 计算更新后的库存数量new_quantity = current_quantity - requested_quantity# 更新库存cursor.execute("UPDATE inventory SET quantity = ? WHERE id = ?", (new_quantity, item_id))#{... 记录明细等操作}# 提交事务connection.commit()return Trueelse:# 库存不足,回滚事务connection.rollback()return False 

乐观锁更新库存

除开悲观锁,自然也可以想到使用乐观锁的方式来进行更新;最常见的设计就是CAS + 版本号的更新来实现库存更新,在库存表中新加一个 version的字段。

# 开始事务
connection.begin()# 读取当前库存记录和版本号
cursor.execute("SELECT quantity, version FROM inventory WHERE id = ?", item_id)
result = cursor.fetchone()
current_quantity = result['quantity']
current_version = result['version']# 检查版本号是否匹配
if current_quantity >= requested_quantity:# 计算更新后的库存数量和版本号new_quantity = current_quantity - requested_quantitynew_version = current_version + 1# 更新库存和版本号cursor.execute("UPDATE inventory SET quantity=%s,version=%s WHERE id=%s AND version=%s",(new_quantity, new_version, item_id, current_version))#{... 记录明细等操作}# 检查是否有更新行数if cursor.rowcount == 1:# 提交事务connection.commit()return Trueelse:# 更新失败,可能是由于版本号不匹配导致的并发操作问题connection.rollback()return False
else:# 库存不足,回滚事务connection.rollback()return False

可以进一步思考,是否需要版本的概念;扣减库存流程中,如果将 SELECT 查询 作为库存超卖前置检查的(保障扣减成功率,减少不必要的写操作)是视角看待,其实需要保障的是扣减后的库存是否大于等于零。

如何理解前置检查视角?
用个卖西瓜的例子来说明,假如你今天微信问到楼下水果店老板有特价5毛一斤西瓜还有10个,这时你立刻下楼去购买。那么可能两种结果,结果一 你买到了特价西瓜;结果二 买的人太多,你到店的时候已经卖光了。从结果看,微信询问的消息只是决定你下不下楼购买,而并非决定真正买到(不影响库存);这种询问作用在于减少直接下楼购买花费体力。

			# 读取当前库存记录cursor.execute("SELECT quantity FROM inventory WHERE id = ? ", item_id)current_quantity = cursor.fetchone()['quantity']# 检查库存是否足够if current_quantity >= requested_quantity:# 开始事务connection.begin()# 更新库存cursor.execute("UPDATE inventory SET quantity = quantity-? WHERE id = ? and quantity - ?>= 0", (requested_quantity, item_id,requested_quantity))#{... 记录明细等操作}# 检查是否有更新行数if cursor.rowcount == 1:# 提交事务connection.commit()return Trueelse:# 更新失败connection.rollback()return Falseelse:return False 

库存读写分离

再考虑一个极端的例子:假设有一个最新款的 iPhone 秒杀活动,库存只有 100 件,活动期间预估峰值每秒查询请求量(QPS)为 10 万次。在活动结束后,流水表最终只会插入 100 条记录,但是查询的 QPS 却接近 10 万次,导致读取的压力非常大。

在这种情况下,查询压力主要是由于活动期间大量的用户查询商品的秒杀状态和库存数量所导致的。虽然流水表最终只插入了 100 条记录,但是查询请求却非常频繁,可能会导致数据库性能问题。

优化首先可以想到是采用读写分离架构,通过新增一套从库来实现。借助MySQL自带的数据同步能力,可以将主库的数据同步到从库,从而在读取库存时可以直接查询从数据库。这样可以将读取请求分散到从库,减轻主库的查询压力。

虽然读写分离可以提高查询性能,但需要注意从库的数据同步可能会有一定的时间延迟,导致从库的数据新鲜度(实时性)有一定的滞后性。(前置检查视角)在进行库存校验时,从库的数据并不一定完全准确,但可以拦截大部分无效流量,起到了一定的作用。

最终的购买决策仍然由主库的UPATE SQL语句来控制,以确保最终扣减的准确性。虽然从库的数据可能有一定的滞后,但并不会影响最终扣减的结果,因为购买操作仍然在主库上执行,确保了数据的一致性和准确性。

优点:1. 借助数据库的 ACID 特性,确保事务的原子性、一致性、隔离性和持久性,避免了超卖和少买等业务问题。
2. 实现简单,适用于项目工期紧张或开发资源有限的情况。

不足: 如果参与秒杀的 SKU(库存量单位)非常多,最终的写操作都是基于库存主库,可能会导致主库的性能压力较大。

这里 牺牲数据实时性(新鲜度) 来提升性能 是一种典型的 技术架构选型的 取舍方向。

库存分库分表

为了解决上述存在的容量和性能上限问题,库存分库分表会是一种优化选择。

  • 将库存扣减表和扣减明细表根据商品ID进行水平拆分,将不同商品的记录存储在不同的分片中。这样可以将高并发的请求路由到不同的数据库实例上,分摊数据库负载。

  • 在水平拆分的基础上,进一步考虑将不同商品的记录分布在不同的数据库实例中,每个实例称为一个库。对于每个库,可以再将表进行分表,将不同商品的记录分开存储。例如,可以按照商品ID的哈希值进行分表,或者按照一定的范围将商品ID进行分段,确保每个表的数据量均衡。

在实际应用中,系统需要根据商品ID来决定将请求路由到哪个数据库实例上。可以使用一致性哈希算法、分段路由规则等方式来实现请求的正确路由。这样可以确保同一商品的库存扣减和明细记录在同一个数据库实例上进行,保证事务的原子性和数据的一致性。

总体来说,通过水平拆分和分库分表的设计,可以有效地提高系统的吞吐量和性能,并减轻单一实例的容量限制。但是在实际应用中,需要仔细考虑数据库的选择、数据路由策略、数据一致性等问题,以确保系统的可用性和性能。同时,还需要合理评估业务需求和数据增长趋势,以选择合适的分片和数据库配置,避免出现过度分片或数据倾斜等问题。

缓存扣减库存

读写分离、分库分表确实能分摊主库很大一部分压力,但是如果面对是 单品万级QPS 的秒杀流量,MySQL 的千级 TPS 同样也支撑不了,需要进一步升级性能。

(读 改为 Redis)此时引入缓存中间件,将 MySQL 的数据定时同步到缓存中(可能存在延迟,库存显示不准确)。库存超卖前置检查,从 Redis 中查询剩余的库存数据,写入操作在数据库校验不准也不会超卖。 由于缓存基于内存操作,性能比数据库高出几个数量级,单台 Redis 实例可以达到 10W QPS 的读性能。

(读/写库存都为 Redis)对于扣减库存的操作,如果直接执行多个 Redis 命令,无法保证原子性。为了确保原子性,可以采用 Lua 脚本的形式,将多个 Redis 命令打包到一个脚本中,作为一个命令发送给 Redis 执行,从而保证了操作的原子性。

具体步骤如下:

  • 使用 Lua 脚本:将扣减库存的多个 Redis 命令封装在一个 Lua 脚本中。这样可以确保这些命令在 Redis 中以原子方式执行,避免并发问题。
  • 执行 Lua 脚本:将封装了扣减库存逻辑的 Lua 脚本作为一个整体命令发送给 Redis 执行。这样在 Redis 中执行脚本时,将按照脚本中的逻辑一次性执行多个命令。
  • 异步保存到数据库:Redis 扣减库存成功后,将此次扣减操作异步化保存到数据库中进行持久化存储。

单品分桶扣减

在更大规模,针对单一商品的超高并发扣减的库存集群中,可能基于数据库内核的改造优化还无法满足业务需求。单一商品的超高并发扣减可能会影响到同一数据库实例上的其他商品扣减,同一个数据库实例上也可能存在多个热点商品造成互相影响,这时就考虑引入基于缓存的分桶扣减方案。

将商品ID按照一定的规则分成多个桶(Bucket),每个桶对应一个缓存项。例如,可以根据商品ID的哈希值或者取模运算的结果来分桶。分桶的目的是将不同商品的库存信息均匀地存储在不同的缓存项中,避免单个缓存项过大导致性能问题。

在进行库存扣减时,首先根据商品ID找到对应的缓存项。然后,在缓存中读取当前库存数量,并进行判断是否足够进行扣减操作。如果足够,更新缓存中的库存数量,并将扣减后的值存回缓存。如果不足,直接返回扣减失败。

其他解决方案

  1. 针对单品较多场景,也可以考虑批量扣减库存,批量处理库存的更新操作,这样可以大量的减少数据库事务。

  2. 基于消息的库存,下单完成后发生订单相关消息,库存通过消息消费的方式进行更新;优势在于库存的更新速率可控。

  3. 令牌库存,可控的时间内进行秒杀库存,提升用户秒杀感知。

以上综述

可以看到库存扣减方案场景多样,更多的 应该根据业务要求 以及 具体的流量进行选择,仅追求性能非好的选择;性能高的同时 往往意味 着其他方面的取舍,比如:代码复杂性、库存精准性、部署复杂性等等。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/66624.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Redis——String类型详解

概述 Redis中的字符串直接按照二进制的数据存储,不会有任何的编码转换,因此存放什么样,取出来的时候就什么样。而MySQL默认的字符集是拉丁文,如果插入中文就会失败 Redis中的字符串类型不仅可以存放文本数据,还可以存…

工作经验总结:RH850中SP、LP、PC寄存器间联系与入栈出栈操作简单整理

一、RH850系列中SP、LP、PC寄存器简介 SP:栈顶寄存器,保存最新栈顶的地址 LP:链接寄存器,保存函数跳转的地址(当没发生调用子函数的操作时候,你可以通过单步调试发现,该寄存器的值不变&#x…

Apache Maven简介安装及系统坏境配置eclipse配置Apache Maven---详细介绍

一,简介 Maven可以简化项目的构建和依赖管理,并提供了一种规范化和可复用的方式来管理Java项目。它广泛应用于Java开发领域,简单来说:它提供了一个简单而强大的方式来管理项目的构建、依赖关系和文档在企业级项目中被广泛采用。 1…

【状态模式】拯救if-else堆出来的屎山代码

前言 我想大家平时都在开发重都遇见过屎山代码,这些屎山代码一般都是由于复杂且庞大的if-else造成的,状态模式,是一种很好的优化屎山代码的设计模式,本文将采用两个业务场景的示例来讲解如何使用状态模式拯救屎山代码。 目录 前…

【Android Framework系列】第10章 PMS之Hook实现广播的调用

1 前言 前面章节我们学习了【Android Framework系列】第4章 PMS原理我们了解了PMS原理,【Android Framework系列】第9章 AMS之Hook实现登录页跳转我们知道AMS可以Hook拦截下来实现未注册Activity页面的跳转,本章节我们来尝试一下HookPMS实现广播的发送。…

2023上半年京东手机行业品牌销售排行榜(京东数据平台)

后疫情时代,不少行业都迎来消费复苏,我国智能手机市场在今年上半年也实现温和的复苏,手机市场的出货量回暖。 根据鲸参谋平台的数据显示,2023年上半年,京东平台上手机的销量为2830万,环比增长约4%&#xf…

ORACLE行转列、列转行实现方式及案例

ORACLE行转列、列转行实现方式及案例 行转列案例方式1.PIVOT方式2.MAX和DECODE方式3.CASE WHEN和GROUP BY 列转行案例方式1.UNPIVOT方式2.UNION ALL 行转列 案例 假设我们有一个名为sales的表,其中包含了产品销售数据。表中有三列:product(…

六轴机械臂码垛货物堆叠仿真

六轴机械臂码垛货物堆叠仿真 1、建立模型与仿真 clear,clc,close all addpath(genpath(.)) %建立模型参数如下: L(1) Link( d, 0.122, a , 0 , alpha, pi/2,offset,0); L(2) Link( d, 0.019 , a ,0.408 , alpha, 0,offset,pi/2); L(3) Link( d, …

汇编指令练习

1.大小比较(循环) start: /*mov r0,#0x9mov r1,#0xfb LoopLoop:cmp r0,r1beq stopsubhi r0,r0,r1subcc r1,r1,r0b Loop stop:b stop.end 仿真图 2. 1到100之和 start:mov r0,#0x1mov r1,#0x0b sum sum:add r1,r1,r0add r0,r0,#0x1cmp r0,#0x65beq sto…

HarmonyOS NEXT新能力,一站式高效开发HarmonyOS应用

2023年8月6日华为开发者大会2023(HDC.Together)圆满收官,伴随着HarmonyOS 4的发布,华为向开发者发布了汇聚所有最新开发能力的HarmonyOS NEXT开发者预览版,并分享了围绕“一次开发,多端部署” “可分可合&a…

k8s service

1、认识Service 程序在容器中、容器在Pod中,可以通过pod的ip来访问应用程序,但是podIP会随着创建销毁而改变。由此,Service出现: Service会对提供同一个服务的多个pod进行聚合,并且提供一个统一的入口地址。通过访问…

new BigDecimal(double val)注意事项 / JWT解析BigDecimal类型数据

前言: 公司项目中有一个板块需要解析JWT令牌获取载荷里面封装的数据,遇到要解析一个BigDecimal类型的数据 问题发现过程: 正常来说,我们解析一个JWT令牌的步骤如下: public static Claims getDataFromToken(String tok…