写点什么

✅日活 3kw 的实际库存业务场景中的超卖到底怎么解决的

作者:派大星
  • 2024-03-08
    辽宁
  • 本文字数:4075 字

    阅读完需:约 13 分钟

这个问题其实可以说是随便一百度几乎可以出来全是解决方案,其实超卖问题再实际业务场景中是十分复杂的。没有什么绝对的解决方案。都是因人而异的。


"超卖"是指商品售出数量超过实际库存量的情况。通常在处理商品库存扣减时,我们会先检查库存是否充足,如果足够则进行扣减,否则直接返回下单失败。


然而,在高并发环境下,可能出现以下情形:



在高并发情况下,当两个并发线程同时查询库存时,假设数据库中库存仅剩 1 个,两个线程都获得了 1 的库存量。在经过库存校验后,它们分别开始执行库存扣减操作,最终导致库存变成负数。


这种情况是高并发环境下典型的超卖问题。


超卖问题的根源在于并发操作,因此解决超卖问题实质上就是解决并发问题。在上述情况中,关键在于确保库存扣减过程的原子性和有序性


  • 原子性指的是库存查询、库存判断和库存扣减这一系列操作作为一个不可分割的整体,不会被中断,也不会被其他线程同时执行。这确保了操作的完整性和一致性。

  • 有序性则要求多个并发操作按照一定的顺序执行,避免出现竞争条件,从而保证数据的准确性和正确性。


通过确保库存扣减操作的原子性和有序性,可以有效解决高并发环境下的超卖问题,保障系统的稳定性和可靠性。

实现方案:数据库

从三个角度考虑实现:


  • 数据库层面的悲观锁

  • 数据库层面的乐观锁

  • 依赖数据库执行引擎的顺序执行机制


以上三个角度简单来说:在处理库存扣减时,常见的方法是通过数据库操作实现。确保操作的原子性和有序性通常可以通过加锁实现,无论是悲观锁还是乐观锁都可以达到这个目的。

悲观锁的实现方式

-- 开始事务BEGIN;
-- 查询商品信息并加锁SELECT quantity FROM items WHERE id = 1 FOR UPDATE;
-- 修改商品数量为2UPDATE items SET quantity = 2 WHERE id = 1;
-- 提交事务COMMIT;
复制代码


注意:


在前述讨论中,我们提到了使用SELECT...FOR UPDATE会对数据进行锁定,但需要注意锁的级别。在 MySQL InnoDB 中,默认使用行级锁。行级锁是基于索引的,如果一条 SQL 语句没有使用索引,那么不会使用行级锁,而会使用表级锁将整个表锁定。因此,这一点需要引起注意。


然而,使用悲观锁可能会导致请求阻塞和排队,在高并发情况下可能对数据库造成负担。乐观锁则通过版本号等方式控制顺序执行,但在高并发环境下可能会出现大量失败操作,不适合高并发场景,因为在更新过程中也需要加行级锁,可能会导致阻塞。

乐观锁的实现方式

在 MySQL 中,乐观锁主要通过 CAS(Compare and Swap)机制来实现,通常通过版本号来实现。CAS 是一种乐观锁技术,当多个线程尝试使用 CAS 同时更新同一个变量时,只有其中一个线程能成功更新变量的值,而其他线程会失败。失败的线程不会被挂起,而是会被告知在这次竞争中失败,并可以再次尝试。


举例来说,对于之前提到的库存扣减问题,通过乐观锁可以实现以下操作:


//查询出商品信息,quantity = 3select quantity from items where id=1
//根据商品信息生成订单//修改商品quantity为2update items set quantity=2 where id=1 and quantity = 3;
复制代码


尽管如此,即使不使用锁也是可行的。可以依赖数据库执行引擎的顺序执行机制,只需确保库存不会变为负数。这种情况下,可以通过巧妙设计的 SQL 语句来实现操作的原子性和有序性。

数据库执行引擎的实现方式

举例来说,假设有一张名为"inventory"的表,其中包含"product_id"和"stock"字段,可以通过以下 SQL 语句来实现库存扣减:


UPDATE inventory SET stock = stock - 1 WHERE product_id = 'your_product_id' AND stock > 0;
复制代码


这样的 SQL 语句能够确保在库存大于 0 的情况下进行扣减,避免库存变为负数。通过这种方式,可以在不加锁的情况下有效地管理库存扣减操作。


有人可能会觉得数据库执行引擎的实现方式挺好的。然而,这种解决方案并不理想。实际上,这种方式与乐观锁方案的缺点相同,都完全依赖于数据库。在高并发情况下,多个线程同时更新库存时可能会导致阻塞。这不仅会导致操作速度变慢,还可能给数据库带来压力。


通常情况下,MySQL 的热点行更新最多也只能承受 200-300 个并发更新。如果需要更高的并发处理能力,一种方法是提升硬件水平,另一种方法是进行一些技术改造,比如采用 inventory hint 的方式。


关于 inventory hint 后续可以单独出一片文章聊一聊。其实就是热点数据如何高效更新


说到这里,数据库层面的超卖的解决实现方案也就聊的差不多了。

实现方式:Redis

我们可以利用 Redis 的单线程执行特性,结合 Lua 脚本执行过程中的原子性保障,实现库存扣减操作。通过在 Redis 中使用如下 Lua 脚本:




local key = KEYS[1] -- 商品的键名-- 获取商品当前的库存量local remaining_stock = tonumber(redis.call("GET", key))
local quantity_to_reduce = tonumber(ARGV[1]) -- 扣减的数量
-- 如果库存足够,则减少库存并返回新的库存量if remaining_stock >= quantity_to_reduce then redis.call("DECRBY", key, quantity_to_reduce) return "Stock reduced successfully"else return "Insufficient stock"end
复制代码


通过先从 Redis 中获取当前剩余库存,然后进行足够性检查并执行扣减操作,可以有效避免并发问题。由于 Lua 脚本在执行过程中不会被中断,且 Redis 是单线程执行的,因此在脚本中进行这些操作可以确保原子性和有序性。这种方法结合了 Redis 的高性能和分布式缓存特性,使得使用 Lua 脚本扣减库存非常高效。

我们实际项目中如何处理超卖的

在实际应用中,通常会结合使用数据库和 Redis 两种方案来实现库存扣减操作。一种常见的做法是首先利用 Redis 进行扣减操作以应对高并发流量,然后将扣减结果同步到数据库中,实现扣减并进行持久化存储,以防止 Redis 宕机导致数据丢失。


具体流程如下:


  • 首先在 Redis 中进行库存扣减操作,然后发送一个消息到消息队列(MQ)。

  • 消费者接收到消息后,执行数据库中的真正库存扣减以及其他业务逻辑操作。


Redis 扣减操作示例代码:


可以使用上述提到的 Lua 脚本方式,或者采用如下 Redisson 方式


import org.redisson.Redisson;import org.redisson.api.RLock;import org.redisson.api.RedissonClient;import org.redisson.config.Config;
public class InventoryConsumer {
public static void main(String[] args) { Config config = new Config(); config.useSingleServer().setAddress("redis://localhost:6379"); RedissonClient redisson = Redisson.create(config);
RLock lock = redisson.getLock("inventory_lock");
try { lock.lock();
// 执行库存扣减及其他业务逻辑操作 // 例如:更新数据库中的库存信息 // 注意:在锁内执行扣减操作
} finally { lock.unlock(); redisson.shutdown(); } }}
复制代码


消费者示例代码:


public class InventoryConsumer {
public static void main(String[] args) { Jedis jedis = new Jedis("localhost", 6379); Connection conn = null;
try { conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/database", "user", "password");
jedis.subscribe(new JedisPubSub() { @Override public void onMessage(String channel, String message) { // 在收到消息时执行数据库操作,进行库存扣减及其他业务逻辑 try { Statement stmt = conn.createStatement(); stmt.executeUpdate("UPDATE inventory SET stock = stock - 1 WHERE product_id = '123'"); conn.commit(); } catch (SQLException e) { e.printStackTrace(); } } }, "inventory_update");
} catch (Exception e) { e.printStackTrace(); } finally { if (conn != null) { try { conn.close(); } catch (SQLException e) { e.printStackTrace(); } } jedis.close(); } }}
复制代码


通过结合使用 Redis 和数据库,可以充分发挥它们各自的优势,实现高效的库存管理并确保数据的一致性和持久性。


这种方法确实有助于确保 Redis 中的数据与数据库中的数据最终保持一致,同时也有助于避免超卖的情况发生。然而,存在一个潜在问题,即可能导致少卖的情况发生。

少卖的解决方案

在上述流程中,如果第一步成功执行,导致 Redis 中的库存成功扣减,但随后的第二步消息未能成功发送,或者在后续消费过程中消息丢失或失败,就可能出现 Redis 中库存减少而数据库库存未减少的情况,从而导致实际业务操作未能发生。这种情况会导致 Redis 中出现多扣的情况,进而引发少卖的问题。


为了解决这类问题,需要引入一种对账机制,实施准实时核对,及时发现并处理这类情况。如果发现存在较大的少卖问题,需要将这些库存重新添加回去。


在许多成熟的电商公司中,无论之前的方案多么完善,这种对账系统都是不可或缺的。及时进行核对,发现超卖、少卖等问题至关重要。


一个简单的示例是,在消费者处理消息时,记录每次库存变化的日志,包括扣减和增加操作,然后定期对比 Redis 中的库存和数据库中的库存,检查是否存在不一致的情况。如果发现多扣或少卖的情况,可以根据日志记录进行修正。


这种对账机制可以帮助保证系统的数据一致性,并及时发现并纠正潜在的问题,确保业务操作的准确性和稳定性。


综上所述可得没有完美的解决方案,引入新的中间件总会面临的的问题。这就需要根据实际业务进行权衡了。




如有问题,欢迎加微信交流:w714771310,备注- 技术交流  。或关注微信公众号【码上遇见你】。


免费的Chat GPT可关注公众号【AI贝塔】进行体现,无限使用。早用早享受


好了,本章节到此告一段落。希望对你有所帮助,祝学习顺利。

发布于: 刚刚阅读数: 4
用户头像

派大星

关注

微信搜索【码上遇见你】,获取更多精彩内容 2021-12-13 加入

微信搜索【码上遇见你】,获取更多精彩内容

评论

发布
暂无评论
✅日活3kw的实际库存业务场景中的超卖到底怎么解决的_电商超卖_派大星_InfoQ写作社区