解决商品超卖的方案和思路

文章讨论了电商系统中防止商品超卖的两种主要解决方案:悲观锁和乐观锁。悲观锁通过同步关键字synchronized限制并发,但可能导致用户体验下降;乐观锁则利用Redis消息队列和事务处理,保证数据一致性且性能较高。文章提供了使用Redis实现乐观锁防止超卖的Java代码示例。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1. 确认需求和技术方案

一般在电商系统和=或者秒杀系统中都有出现一种商品超卖的问题存在,原因就是再大量并发请求的时候导致了数据库的脏读和不可重复读,从而造成了商品的下单数量大于了商品的库存数量。
一般来说常用的解决超卖的方案有两种:

  • 方案一:悲观锁(不推荐)
  • 方案二:乐观锁

2. 使用两种方案来解决问题

方案一:悲观锁
  对于方案一的解决方法有很多,比如在对要加锁的方法上加入synchronized同步字段,使接口被访问时至多同时只能被一个线程访问,其他的请求都会处于阻塞状态,直至上一个请求处理结束,线程被释放,下一个请求再进来,使每一次进来的请求都需要排队,一次一个。
 这种方案可以有效的解决超卖问题,每一次请求都可以保证数据库数据的一致性,但是也有缺点,用户的体验就不是不好了,后面的的用户在等待很久之后可能还是会提示请求失败,商品已经买完了。

    @PutMapping("")
    public Object doorder(@ApiParam(name = "DTO" @RequestBody DTO dto){
        try {
            //悲观锁
            synchronized (this){
                //业务层的减库存方法和下单方法
                ...
                return;
            }
        }catch (Exception e){   
        	//这里也可以使用自定义的方式捕获异常并返回想要异常
            e.printStackTrace();
            return ...;
        }
    }

 但需要强调的一点是synchronized不能和@Transactional一起使用,也不要在Service层的方法上加synchronized,因为Service层的业务方法大多加有事务控制,和悲观锁联合使用的时候,悲观锁解锁的时间比事务提交的时间早,可能会导致少量请求在上一次事务未完全提交就进来,最终还是会导致少量的超卖出现。所以尽量在接口中加上同步代码块来控制业务的访问。

方案二:乐观锁
 对于方案二来说,并发数较大的情况下就需要使用消息队列来保证数据的一致性。常用的有Redis消息队列来防止超卖,下面给出的是用Redis的消息队列来进行商品超卖的控制。

  • 一:获取商品库存。
  • 二:判断库存是否充足,如果充足则继续执行,否则返回错误信息。
  • 三:使用Redis作为消息队列,将购买请求放入队列中。
  • 四:开启一个线程来消费队列中的购买请求,对于每一个请求,按照以下步骤处理。
    • a:获取商品库存。
    • b:判断库存是否充足,如果充足则继续执行,否则返回错误信息。
    • c:将购买请求的处理结果返回给用户。

下面是完整的Java代码实现。

<dependency>
	<groupId>redis.clients</groupId>
	<artifactId>jedis</artifactId>
	<version>3.0.0</version>
</dependency>
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;
import redis.clients.jedis.Transaction;

public class RedisDemo {

    public static void main(String[] args) {

        // 创建redis连接池
        JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
        jedisPoolConfig.setMaxTotal(8);
        jedisPoolConfig.setMaxIdle(8);
        JedisPool jedisPool = new JedisPool(jedisPoolConfig, "localhost", 6379, 3000, null);

        // 乐观锁实现防止超卖
        for (int i = 0; i &lt; 100; i++) {
            new Thread(() -&gt; {
                String watchKey = "goods:001";
                String userKey = "user:001";
                String requestId = "request:001";
                int buyNum = 1;

                Jedis jedis = null;
                try {
                    jedis = jedisPool.getResource();
                    // 监视watchKey
                    jedis.watch(watchKey);
                    // 获取商品库存
                    int stock = Integer.parseInt(jedis.get(watchKey));
                    if (stock &lt; buyNum) {
                        jedis.unwatch();
                        System.out.println("库存不足!");
                        return;
                    }

                    // 开始事务
                    Transaction transaction = jedis.multi();
                    // 减少库存
                    transaction.decrBy(watchKey, buyNum);
                    // 成功的订单数
                    int successCount = Integer.parseInt(jedis.get(userKey));
                    // 提交事务
                    transaction.exec();
                    // 增加成功的订单数
                    successCount++;
                    jedis.set(userKey, String.valueOf(successCount));

                    System.out.println(Thread.currentThread().getName() + " 抢购成功!");
                } finally {
                    if (jedis != null) {
                        jedis.close();
                    }
                }
            }, "用户" + (i + 1)).start();
        }

        // 消费消息队列,处理购买请求
        new Thread(() -&gt; {
            Jedis jedis = null;
            try {
                jedis = jedisPool.getResource();
                while (true) {
                    // 从消息队列中获取购买请求
                    String request = jedis.rpop(requestId);
                    if (request == null) {
                        continue;
                    }

                    // 开始事务
                    Transaction transaction = jedis.multi();
                    // 获取商品库存
                    int stock = Integer.parseInt(jedis.get(watchKey));
                    if (stock &lt; buyNum) {
                        continue;
                    }
                    // 减少库存
                    transaction.decrBy(watchKey, buyNum);
                    // 提交事务
                    transaction.exec();
                    // 返回结果给用户
                    jedis.set(request, "success");
                }
            } finally {
                if (jedis != null) {
                    jedis.close();
                }
            }
        }, "消费者").start();
    }
}

 使用Redis防止超卖的一种很常见的做法,其主要是通过Redis的原子操作incr或decr实现。其特点有如下几点:

  1. 快速高效:Redis是内存级别的缓存,读写速度非常快,可以满足高并发场景下的业务需求,提升系统性能。

  2. 可靠性高:Redis支持事务和CAS命令,防止并发操作导致数据不一致的问题,保证数据的一致性和可靠性。

  3. 简单易用:Redis有丰富的数据结构和详细的API文章,易上手和维护。

  4. 运维成本低:Redis有非常好的集群和监控方案,也支持自动化运维,能减少运维成本和复杂度。

  5. 灵活可扩展:Redis支持主从复制和分片,可以根据业务需求快速扩展集群,以应对高并发场景的需求。

3.总结

 悲观锁和乐观锁的方式相比较,悲观锁是在对库存表进行操作时预先加锁,确保同一时刻只有一个线程能够访问和修改库存数据。但是因为加锁的原因,就会导致其他线程需要等待锁释放才能进行操作,影响并发性能,并且给用户的体验感非常差。
 而使用Redis,基于Redis的原子操作能够更好地保证数据的安全性,并且性能更高,在大并发的场景下会有很好的性能表现。

<think>首先,用户的问题是基于参考站内引用的内容,但引用并不是用户真正的需求。用户的问题是:“我想解决在并发环境下库存问题 请问如何在并发场景下避免库存问题”。所以,我需要专注于回答这个问题。 参考的引用提供了背景信息: - 引用[1]:解释了问题的本质是并发数据修改冲突,并提到了四种解决方案。 - 引用[2]:讨论了高并发下的库存抢购问题,包括多库存秒杀。 - 引用[3]:提到了使用数据库技术改造或Redis+lua脚本的方案。 这些引用是站内的,所以我需要在回答中自然地引用它们,在段落末尾添加引用标识,如[^1]。 现在,用户的问题是关于在并发场景下避免库存问题。我需要确保回答结构清晰,帮助用户逐步解决问题,并保证真实可靠。 回答结构: 1. **解释问题**:简要解释什么是库存问题,为什么在并发环境下发生。 2. **核心原因**:分析并发冲突的根本原因。 3. **解决方案**:提供多种解决方案,每种方案包括原理、实现思路、适用场景优缺点。参考引用中提到的方案。 4. **总结建议**:给出推荐方案或根据场景选择建议。 5. **生成相关问题**:在末尾添加相关问题,以帮助用户深入学习。 从引用中提取的解决方案: - 引用[1]:提到了四种解决方案(未详细列出,但可以推断)。 - 引用[2]:讨论了多库存秒杀锁机制。 - 引用[3]:提到了数据库技术改造(如inventory hint)Redis+lua脚本。 常见的解决方案包括: 1. 悲观锁(如数据库行锁)。 2. 乐观锁(如版本控制)。 3. 使用缓存(如Redis)。 4. 分布式锁。 5. 队列机制。 我需要覆盖这些,并基于引用组织内容。 系统级指令: - 所有行内数学表达式用$...$格式。 - 独立公式用$$...$$格式。 - LaTeX语法正确。 - 使用中文回答。 - 生成相关问题。 - 回答中引用的段落末尾自然地添加引用标识。 在回答中,数学表达式可能不多,但如果有计算或公式,要正确格式化。 回答步骤: 1. **引言**:定义问题。 2. **原因分析**:解释并发冲突。 3. **解决方案列表**: - 方案1:悲观锁(数据库行锁)。 - 方案2:乐观锁(版本控制)。 - 方案3:使用RedisLua脚本。 - 方案4:分布式锁。 - 方案5:队列机制(如消息队列)。 4. **优缺点比较**:简要总结。 5. **推荐方案**:根据场景建议。 6. **相关问题**:生成3-5个相关问题。 在结尾添加相关问题,如指令所示。 确保语言中文,专业但易懂。 最终输出格式: - 回答内容。 - 末尾有“相关问题”部分。</think>### 在并发场景下避免库存问题解决方案 库存问题是指在并发环境下,多个用户同时请求扣减库存时,由于数据修改冲突导致实际库存不足却仍被成功扣减的现象。例如,在高并发抢购活动中,如果初始库存为100件,但多个请求同时处理时,系统可能错误地过100件商品,引发用户投诉或系统异常。的本质是并发数据修改冲突:当多个事务同时读取库存、计算新值并写回时,由于数据库隔离级别或网络延迟,可能导致写操作覆盖旧值,而非基于最新数据更新[^1]。在本回答中,我将逐步解释问题原因、提供多种解决方案(包括原理、实现思路、适用场景优缺点),并给出推荐建议,确保内容真实可靠,基于工程实践。 #### 1. 问题原因分析 库存的核心是并发环境下的数据一致性问题。在数据库系统中,库存扣减通常涉及以下步骤: 1. 读取当前库存值 $S$。 2. 判断 $S \geq$ 请求数量 $Q$(其中 $Q$ 是用户请求扣减的数量)。 3. 更新库存为 $S - Q$。 在并发场景下,如果多个事务同时执行这些步骤,可能导致: - **读取过时数据**:事务A读取 $S=100$,事务B也读取 $S=100$,两者都判断 $S \geq Q$ 并更新,导致库存被多次扣减。 - **写操作冲突**:数据库的ACID特性(如隔离级别)可能无法完全阻止并发写,尤其在分布式系统中。 数学上,这可以用并发事务模型描述:设库存变量为 $S$,事务 $T_i$ 执行 $S \leftarrow S - Q_i$。如果多个事务同时执行,且无锁机制,则可能违反 $S \geq 0$ 的约束。例如,两个事务 $T_1$ $T_2$ 同时操作,最终库存可能为负值: $$ S_{\text{final}} = S_{\text{initial}} - Q_1 - Q_2 $$ 但实际执行中,由于顺序问题,$S_{\text{final}}$ 可能小于0[^1]。 #### 2. 解决方案 以下是五种常见解决方案,每种都经过实践验证,适用于不同并发规模。实现时需结合具体场景(如电商系统、秒杀活动),并优先考虑简单方案。 ##### 方案1: 悲观锁(数据库行锁) - **原理**:在事务开始时对库存行加锁(如SELECT FOR UPDATE),确保同一时间只有一个事务能修改该库存。这属于悲观策略,假设冲突会发生。 - **实现思路**: - SQL示例:在事务中使用 `SELECT ... FOR UPDATE` 锁定库存行,然后更新。 - Java伪代码: ```java try (Connection conn = dataSource.getConnection()) { conn.setAutoCommit(false); // 加锁查询库存 PreparedStatement stmt = conn.prepareStatement("SELECT stock FROM inventory WHERE product_id = ? FOR UPDATE"); stmt.setInt(1, productId); ResultSet rs = stmt.executeQuery(); if (rs.next() && rs.getInt("stock") >= quantity) { // 更新库存 PreparedStatement updateStmt = conn.prepareStatement("UPDATE inventory SET stock = stock - ? WHERE product_id = ?"); updateStmt.setInt(1, quantity); updateStmt.setInt(2, productId); updateStmt.executeUpdate(); conn.commit(); // 提交事务,释放锁 } else { conn.rollback(); // 库存不足,回滚 } } ``` - **适用场景**:中小型并发系统(如日活用户数万),数据库负载不高时。适合单个商品库存扣减。 - **优点**:实现简单,数据库原生支持,保证强一致性。 - **缺点**:高并发下锁竞争激烈,可能导致性能瓶颈(如连接时)。不适用于分布式系统或秒杀场景[^1][^2]。 ##### 方案2: 乐观锁(版本控制) - **原理**:基于版本号或时间戳机制,在更新时检查数据是否被修改。如果版本不匹配,则重试或失败。这属于乐观策略,假设冲突较少。 - **实现思路**: - 在库存表中添加版本字段(如 `version`)。 - SQL示例:更新时检查版本。 - Java伪代码(使用重试机制): ```java int retryCount = 0; while (retryCount < MAX_RETRY) { // 读取库存版本 Inventory inv = inventoryDao.selectById(productId); if (inv.getStock() < quantity) { throw new InsufficientStockException(); } // 尝试更新 int rowsUpdated = inventoryDao.updateStock(productId, quantity, inv.getVersion()); if (rowsUpdated > 0) { break; // 更新成功 } retryCount++; // 版本冲突,重试 } ``` - **适用场景**:冲突频率低的系统(如普通电商),或读多写少场景。支持多库存扣减(如一次秒杀多个商品)。 - **优点**:避免锁竞争,提高并发性能;易于在分布式环境实现。 - **缺点**:重试机制可能导致延迟,高冲突时性能下降(如秒杀活动)。需处理重试上限,避免无限循环[^1][^3]。 ##### 方案3: Redis + Lua脚本(原子操作) - **原理**:利用Redis的内存数据库Lua脚本的原子性,在缓存层执行扣减逻辑。Lua脚本在Redis中单线程执行,确保操作不可中断。 - **实现思路**: - 将库存预加载到Redis(如 `SET stock:1001 100`)。 - Lua脚本示例(扣减库存): ```lua local key = KEYS[1] local quantity = tonumber(ARGV[1]) local stock = tonumber(redis.call('GET', key)) if stock >= quantity then redis.call('DECRBY', key, quantity) return 1 -- 成功 else return 0 -- 失败 end ``` - Java调用示例(使用Jedis): ```java String script = "lua脚本内容"; Object result = jedis.eval(script, Collections.singletonList("stock:1001"), Collections.singletonList("2")); if ((Long)result == 1) { // 扣减成功,异步同步到数据库 } else { // 库存不足 } ``` - **注意**:需定期将Redis数据同步到数据库,保证最终一致性。 - **适用场景**:高并发秒杀(如百万QPS),Redis性能高,能缓解数据库压力。 - **优点**:极高吞吐量,Lua脚本原子性避免;支持复杂逻辑(如多库存扣减)。 - **缺点**:增加系统复杂度(需维护缓存一致性);Redis故障可能导致数据丢失。需结合数据库持久化[^3]。 ##### 方案4: 分布式锁 - **原理**:使用分布式锁(如Redis的RedLock或ZooKeeper)协调多个实例,确保同一时间只有一个实例能修改库存。 - **实现思路**: - 基于Redis实现分布式锁(SETNX命令)。 - Java伪代码: ```java try { boolean locked = redis.setnx("lock:product_1001", "locked", 10); // 设置10秒时 if (locked) { // 执行库存扣减(可结合方案1或2) updateStockInDB(productId, quantity); } else { // 获取锁失败,重试或返回错误 } } finally { redis.del("lock:product_1001"); // 释放锁 } ``` - **适用场景**:分布式系统(如微服务架构),需要跨多个节点协调。 - **优点**:扩展性好,适用于大规模集群。 - **缺点**:锁管理复杂,可能引入死锁或网络延迟问题;性能低于Redis Lua方案[^2][^3]。 ##### 方案5: 队列机制(消息队列) - **原理**:将扣减请求放入队列(如Kafka或RabbitMQ),由消费者顺序处理,避免并发冲突。 - **实现思路**: - 用户请求先入队,消费者单线程处理队列。 - 架构示例: - 生产者:接收请求,发送消息到队列。 - 消费者:从队列取出消息,执行库存扣减(使用简单SQL更新)。 - Java伪代码: ```java // 生产者 messageQueue.send(new InventoryMessage(productId, quantity)); // 消费者 while (true) { InventoryMessage msg = messageQueue.poll(); if (inventoryService.updateStock(msg.getProductId(), msg.getQuantity())) { // 成功 } } ``` - **适用场景**:高并发(如秒杀活动),能削峰填谷,保护数据库。 - **优点**:解耦系统,提高可靠性;支持批量处理。 - **缺点**:引入延迟,不适合实时性要求高的场景;需处理队列积压[^1]。 #### 3. 方案比较与推荐 - **比较总结**: | 方案 | 适用并发规模 | 一致性 | 性能 | 复杂度 | 支持多库存 | |---|---|---|---|---|---| | 悲观锁 | 低至中 | 强 | 低(高并发下差) | 低 | 否 | | 乐观锁 | 中 | 最终 | 中 | 中 | 是[^2] | | Redis + Lua | 高 | 强(缓存层) | 高 | 高 | 是 | | 分布式锁 | 中至高 | 强 | 中 | 高 | 是 | | 队列机制 | 高 | 最终 | 高(异步) | 中 | 是 | - **推荐建议**: - **中小系统**:优先使用乐观锁,简单且高效。 - **高并发秒杀**:首选Redis + Lua脚本,结合异步数据库同步(如引用[3]所述),能处理百万级QPS。 - **分布式环境**:结合分布式锁队列,确保全局一致性。 - **通用原则**:无论采用哪种方案,都需添加监控重试机制。测试时,使用工具(如JMeter)模拟并发请求验证方案。 在实践中,问题可通过数据库优化(如引用[3]提到的inventory hint)提升并发能力,但缓存方案更通用[^1][^3]。最终,选择方案需权衡业务需求、系统架构团队经验。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值