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

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

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

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

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

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

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

  • 原子性指的是库存查询、库存判断和库存扣减这一系列操作作为一个不可分割的整体,不会被中断,也不会被其他线程同时执行。这确保了操作的完整性和一致性。
  • 有序性则要求多个并发操作按照一定的顺序执行,避免出现竞争条件,从而保证数据的准确性和正确性。

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

实现方案:数据库

从三个角度考虑实现:

  • 数据库层面的悲观锁
  • 数据库层面的乐观锁
  • 依赖数据库执行引擎的顺序执行机制

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

悲观锁的实现方式

sql 复制代码
-- 开始事务
BEGIN;

-- 查询商品信息并加锁
SELECT quantity FROM items WHERE id = 1 FOR UPDATE;

-- 修改商品数量为2
UPDATE items SET quantity = 2 WHERE id = 1;

-- 提交事务
COMMIT;

注意:

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

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

乐观锁的实现方式

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

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

sql 复制代码
//查询出商品信息,quantity = 3
select quantity from items where id=1

//根据商品信息生成订单
//修改商品quantity为2
update items set quantity=2 where id=1 and quantity = 3;

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

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

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

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脚本:

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方式

java 复制代码
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();
        }
    }
}

消费者示例代码:

java 复制代码
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贝塔】进行体现,无限使用。早用早享受

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

相关推荐
源代码•宸8 分钟前
GoLang八股(Go语言基础)
开发语言·后端·golang·map·defer·recover·panic
czlczl200209258 分钟前
OAuth 2.0 解析:后端开发者视角的原理与流程讲解
java·spring boot·后端
颜淡慕潇16 分钟前
Spring Boot 3.3.x、3.4.x、3.5.x 深度对比与演进分析
java·后端·架构
布列瑟农的星空16 分钟前
WebAssembly入门(一)——Emscripten
前端·后端
小突突突2 小时前
Spring框架中的单例bean是线程安全的吗?
java·后端·spring
iso少年2 小时前
Go 语言并发编程核心与用法
开发语言·后端·golang
掘金码甲哥2 小时前
云原生算力平台的架构解读
后端
码事漫谈2 小时前
智谱AI从清华实验室到“全球大模型第一股”的六年征程
后端
码事漫谈2 小时前
现代软件开发中常用架构的系统梳理与实践指南
后端
Mr.Entropy2 小时前
JdbcTemplate 性能好,但 Hibernate 生产力高。 如何选择?
java·后端·hibernate