JAVA高并发订单系统库存扣减出现并发超卖的完整解决路径

Java 高并发订单系统库存扣减并发超卖完整解决方案

大家好,今天我们来聊聊 Java 高并发订单系统中库存扣减时可能出现的并发超卖问题,以及如何完整地解决这个问题。这是一个非常常见但又至关重要的问题,处理不好会导致严重的业务损失。

一、问题背景:并发超卖

在高并发场景下,多个用户同时下单购买同一件商品,如果库存扣减逻辑没有做好并发控制,就会出现超卖现象,即卖出的商品数量超过实际库存。

举个简单的例子:

  • 假设商品 A 的库存是 10 件。
  • 用户 1 和 用户 2 同时下单,都购买 5 件商品 A。
  • 如果库存扣减逻辑没有做好同步,可能出现以下情况:

    1. 用户 1 读取到库存为 10。
    2. 用户 2 读取到库存也为 10。
    3. 用户 1 扣减库存,将库存更新为 5。
    4. 用户 2 扣减库存,也将库存更新为 5。

    这样,实际上卖出了 10 件商品,但库存却被扣减成了 5,导致超卖。

二、导致超卖的根本原因:竞态条件

超卖的根本原因在于竞态条件(Race Condition)。多个线程并发地访问和修改共享资源(库存),而最终的结果依赖于线程执行的顺序。由于线程执行顺序的不确定性,导致了数据不一致。

三、解决并发超卖的方案

解决并发超卖问题,核心在于控制对共享资源(库存)的并发访问,保证在同一时刻只有一个线程能够修改库存。 常用的方法包括:

  1. 悲观锁(Pessimistic Locking)
  2. 乐观锁(Optimistic Locking)
  3. 分布式锁(Distributed Locking)
  4. 队列(Queue)
  5. Redis 原子操作
  6. Lua 脚本

下面我们分别介绍这些方案,并给出代码示例。

1. 悲观锁

悲观锁认为并发冲突发生的概率很高,因此在访问共享资源之前,先获取锁,确保在锁释放之前,其他线程无法访问该资源。 在Java中,可以使用 synchronized 关键字或者 ReentrantLock 实现悲观锁。

  • synchronized 关键字

    synchronized 关键字可以修饰方法或代码块,当一个线程进入 synchronized 修饰的方法或代码块时,会自动获取锁,其他线程必须等待该线程释放锁才能进入。

    public class InventoryService {
        private int stock = 10;
    
        // 使用 synchronized 关键字修饰方法
        public synchronized boolean decreaseStock(int quantity) {
            if (stock >= quantity) {
                stock -= quantity;
                System.out.println("扣减库存成功,剩余库存:" + stock);
                return true;
            } else {
                System.out.println("库存不足");
                return false;
            }
        }
    }

    或者使用 synchronized 代码块:

    public class InventoryService {
        private int stock = 10;
        private final Object lock = new Object();
    
        public boolean decreaseStock(int quantity) {
            synchronized (lock) {
                if (stock >= quantity) {
                    stock -= quantity;
                    System.out.println("扣减库存成功,剩余库存:" + stock);
                    return true;
                } else {
                    System.out.println("库存不足");
                    return false;
                }
            }
        }
    }
  • ReentrantLock

    ReentrantLock 是一个可重入的互斥锁,提供了比 synchronized 更强大的功能,例如可以设置公平锁、超时等待等。

    import java.util.concurrent.locks.Lock;
    import java.util.concurrent.locks.ReentrantLock;
    
    public class InventoryService {
        private int stock = 10;
        private final Lock lock = new ReentrantLock();
    
        public boolean decreaseStock(int quantity) {
            lock.lock(); // 获取锁
            try {
                if (stock >= quantity) {
                    stock -= quantity;
                    System.out.println("扣减库存成功,剩余库存:" + stock);
                    return true;
                } else {
                    System.out.println("库存不足");
                    return false;
                }
            } finally {
                lock.unlock(); // 释放锁
            }
        }
    }

    优点: 实现简单,保证数据一致性。

    缺点: 并发度低,性能较差。所有请求都需要排队等待锁,在高并发场景下会成为性能瓶颈。

2. 乐观锁

乐观锁认为并发冲突发生的概率较低,因此不会在访问共享资源之前加锁,而是在更新数据时检查是否有其他线程修改过数据。常用的实现方式是使用版本号(Version)。

  • 基于版本号的乐观锁

    在数据库表中增加一个版本号字段,每次更新数据时,版本号加 1。在更新数据时,比较当前版本号和读取时的版本号是否一致,如果一致则更新成功,否则更新失败。

    // 数据库表结构:商品表 (product)
    // id  | name | stock | version
    // --- | ---- | ----- | -------
    // 1   | 商品A | 10    | 1
    
    public class InventoryService {
        // 模拟数据库操作
        public boolean decreaseStock(int productId, int quantity) {
            // 1. 查询商品信息,包括库存和版本号
            Product product = getProductById(productId);
            if (product == null) {
                System.out.println("商品不存在");
                return false;
            }
    
            int currentStock = product.getStock();
            int version = product.getVersion();
    
            if (currentStock < quantity) {
                System.out.println("库存不足");
                return false;
            }
    
            // 2. 尝试更新库存,并增加版本号
            int rows = updateStock(productId, currentStock - quantity, version);
    
            // 3. 判断更新是否成功
            if (rows > 0) {
                System.out.println("扣减库存成功,剩余库存:" + (currentStock - quantity));
                return true;
            } else {
                System.out.println("扣减库存失败,可能已被其他线程修改");
                return false;
            }
        }
    
        // 模拟数据库查询
        private Product getProductById(int productId) {
            // 实际项目中从数据库查询
            // 这里为了演示,直接创建对象
            Product product = new Product();
            product.setId(1);
            product.setName("商品A");
            product.setStock(10);
            product.setVersion(1);
            return product;
        }
    
        // 模拟数据库更新
        private int updateStock(int productId, int newStock, int version) {
            // 实际项目中使用 SQL 的 WHERE 子句进行版本号的比较
            // UPDATE product SET stock = #{newStock}, version = version + 1 WHERE id = #{productId} AND version = #{version}
            // 这里为了演示,直接返回 1 或 0
            // 模拟更新成功或失败
            if (Math.random() > 0.5) {
                System.out.println("productId = " + productId + ", newStock = " + newStock + ", version = " + version);
                return 1; // 更新成功
            } else {
                return 0; // 更新失败
            }
        }
    }
    
    class Product {
        private int id;
        private String name;
        private int stock;
        private int version;
    
        public int getId() {
            return id;
        }
    
        public void setId(int id) {
            this.id = id;
        }
    
        public String getName() {
            return name;
        }
    
        public void setName(String name) {
            this.name = name;
        }
    
        public int getStock() {
            return stock;
        }
    
        public void setStock(int stock) {
            this.stock = stock;
        }
    
        public int getVersion() {
            return version;
        }
    
        public void setVersion(int version) {
            this.version = version;
        }
    }
    

    优点: 并发度高,性能较好。

    缺点: 需要重试机制,如果并发冲突频繁,重试次数会增加,影响性能。 同时也需要数据库的支持。

3. 分布式锁

当应用是分布式部署时,单机的锁(如 synchronizedReentrantLock)无法保证多个应用实例之间的同步。这时需要使用分布式锁。 常用的分布式锁实现方式有:

  • 基于数据库的分布式锁

    通过在数据库中创建一张锁表,当需要获取锁时,向锁表中插入一条记录。释放锁时,删除该记录。

  • 基于 Redis 的分布式锁

    利用 Redis 的 SETNX 命令实现原子性的锁操作。

    import redis.clients.jedis.Jedis;
    
    public class RedisLock {
        private static final String LOCK_KEY = "product_stock_lock";
        private static final String LOCK_VALUE = "lock"; // 可以使用UUID
        private static final int LOCK_EXPIRE_TIME = 10; // 锁的过期时间,单位:秒
    
        private Jedis jedis;
    
        public RedisLock(Jedis jedis) {
            this.jedis = jedis;
        }
    
        // 获取锁
        public boolean tryLock() {
            String result = jedis.set(LOCK_KEY, LOCK_VALUE, "NX", "EX", LOCK_EXPIRE_TIME);
            return "OK".equals(result);
        }
    
        // 释放锁
        public void unlock() {
            jedis.del(LOCK_KEY);
        }
    
        // 释放锁的 lua 脚本, 保证原子性
        public void unlockLua() {
            String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
            Object result = jedis.eval(script, 1, LOCK_KEY);
            if ("1".equals(result.toString())) {
                System.out.println("unlock success");
            } else {
                System.out.println("unlock fail");
            }
        }
    }
    
    public class InventoryService {
        private int stock = 10;
        private RedisLock redisLock;
    
        public InventoryService(RedisLock redisLock) {
            this.redisLock = redisLock;
        }
    
        public boolean decreaseStock(int quantity) {
            try {
                // 尝试获取锁
                if (redisLock.tryLock()) {
                    if (stock >= quantity) {
                        stock -= quantity;
                        System.out.println("扣减库存成功,剩余库存:" + stock);
                        return true;
                    } else {
                        System.out.println("库存不足");
                        return false;
                    }
                } else {
                    System.out.println("获取锁失败,请稍后重试");
                    return false;
                }
            } finally {
                // 释放锁
                redisLock.unlockLua();
            }
        }
    }

    优点: 适用于分布式环境,并发度较高。

    缺点: 实现相对复杂,需要引入第三方组件(如 Redis)。需要考虑锁的过期时间,防止死锁。

4. 队列

将库存扣减操作放入队列中,利用队列的先进先出特性,保证库存扣减操作的顺序性。 可以使用 Kafka、RabbitMQ 等消息队列。

import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;

public class InventoryService {
    private int stock = 10;
    private BlockingQueue<Order> orderQueue = new LinkedBlockingQueue<>();

    // 接收订单,放入队列
    public void placeOrder(Order order) {
        try {
            orderQueue.put(order);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    // 启动消费者线程,处理订单
    public void startConsumer() {
        new Thread(() -> {
            while (true) {
                try {
                    Order order = orderQueue.take();
                    decreaseStock(order.getProductId(), order.getQuantity());
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();
    }

    // 扣减库存
    private boolean decreaseStock(int productId, int quantity) {
        synchronized (this) {
            if (stock >= quantity) {
                stock -= quantity;
                System.out.println("扣减库存成功,剩余库存:" + stock);
                return true;
            } else {
                System.out.println("库存不足");
                return false;
            }
        }
    }

    public static void main(String[] args) {
        InventoryService inventoryService = new InventoryService();
        inventoryService.startConsumer();

        // 模拟多个用户下单
        for (int i = 0; i < 20; i++) {
            Order order = new Order(1, 1); // 商品ID为1,购买数量为1
            inventoryService.placeOrder(order);
        }
    }
}

class Order {
    private int productId;
    private int quantity;

    public Order(int productId, int quantity) {
        this.productId = productId;
        this.quantity = quantity;
    }

    public int getProductId() {
        return productId;
    }

    public int getQuantity() {
        return quantity;
    }
}

优点: 可以实现异步处理,提高系统吞吐量。可以削峰填谷,缓解高并发压力。

缺点: 引入了消息队列,增加了系统的复杂性。需要保证消息的可靠性,防止消息丢失。

5. Redis 原子操作

Redis 提供了原子操作,可以直接对库存进行原子性的加减操作,避免并发冲突。

import redis.clients.jedis.Jedis;

public class InventoryService {
    private static final String STOCK_KEY = "product_stock:1"; // 商品ID为1的库存

    private Jedis jedis;

    public InventoryService(Jedis jedis) {
        this.jedis = jedis;
    }

    public boolean decreaseStock(int quantity) {
        // 使用 Redis 的 DECRBY 命令原子性地减少库存
        Long stock = jedis.decrBy(STOCK_KEY, quantity);

        if (stock >= 0) {
            System.out.println("扣减库存成功,剩余库存:" + stock);
            return true;
        } else {
            // 库存不足,恢复库存
            jedis.incrBy(STOCK_KEY, quantity);
            System.out.println("库存不足");
            return false;
        }
    }

    public static void main(String[] args) {
        Jedis jedis = new Jedis("localhost", 6379);
        // 初始化库存
        jedis.set(STOCK_KEY, "10");

        InventoryService inventoryService = new InventoryService(jedis);

        // 模拟多个用户下单
        for (int i = 0; i < 20; i++) {
            inventoryService.decreaseStock(1); // 每次购买1件
        }

        jedis.close();
    }
}

优点: 实现简单,性能高。利用 Redis 的高性能特性。

缺点: 需要引入 Redis。需要考虑 Redis 的持久化和高可用。

6. Lua 脚本

可以将多个 Redis 操作封装成一个 Lua 脚本,利用 Redis 的原子性执行 Lua 脚本,保证多个操作的原子性。

import redis.clients.jedis.Jedis;

public class InventoryService {
    private static final String STOCK_KEY = "product_stock:1"; // 商品ID为1的库存

    private Jedis jedis;

    public InventoryService(Jedis jedis) {
        this.jedis = jedis;
    }

    public boolean decreaseStock(int quantity) {
        // Lua 脚本
        String script = "local stock = tonumber(redis.call('get', KEYS[1]))n" +
                "if stock >= tonumber(ARGV[1]) thenn" +
                "  redis.call('decrby', KEYS[1], ARGV[1])n" +
                "  return 1n" +
                "elsen" +
                "  return 0n" +
                "end";

        // 执行 Lua 脚本
        Object result = jedis.eval(script, 1, STOCK_KEY, String.valueOf(quantity));

        if ("1".equals(result.toString())) {
            System.out.println("扣减库存成功,剩余库存:" + jedis.get(STOCK_KEY));
            return true;
        } else {
            System.out.println("库存不足");
            return false;
        }
    }

    public static void main(String[] args) {
        Jedis jedis = new Jedis("localhost", 6379);
        // 初始化库存
        jedis.set(STOCK_KEY, "10");

        InventoryService inventoryService = new InventoryService(jedis);

        // 模拟多个用户下单
        for (int i = 0; i < 20; i++) {
            inventoryService.decreaseStock(1); // 每次购买1件
        }

        jedis.close();
    }
}

优点: 可以将多个 Redis 操作原子化,提高性能。

缺点: 需要编写 Lua 脚本,增加了复杂性。

四、方案选择建议

选择哪种方案取决于具体的业务场景和系统架构。

方案 适用场景 优点 缺点
悲观锁 并发量低,数据一致性要求高 实现简单,保证数据一致性 并发度低,性能差
乐观锁 并发量较高,允许一定程度的重试 并发度高,性能好 需要重试机制,并发冲突频繁时性能下降,需要数据库支持
分布式锁 分布式环境,需要跨应用实例同步 适用于分布式环境,并发度较高 实现相对复杂,需要引入第三方组件,需要考虑锁的过期时间
队列 高并发,需要异步处理,削峰填谷 实现异步处理,提高系统吞吐量,削峰填谷,缓解高并发压力 引入了消息队列,增加了系统的复杂性,需要保证消息的可靠性
Redis 原子操作 高并发,对性能要求高 实现简单,性能高,利用 Redis 的高性能特性 需要引入 Redis,需要考虑 Redis 的持久化和高可用
Redis Lua 脚本 需要原子性地执行多个 Redis 操作 可以将多个 Redis 操作原子化,提高性能 需要编写 Lua 脚本,增加了复杂性

五、总结

解决 Java 高并发订单系统中库存扣减的并发超卖问题,需要根据具体的业务场景和系统架构选择合适的方案。没有一种方案是万能的,需要综合考虑并发量、数据一致性要求、性能要求、系统复杂性等因素。 在实际项目中,通常会结合多种方案,例如使用乐观锁结合重试机制,或者使用队列结合 Redis 原子操作。只有充分理解各种方案的优缺点,才能选择最适合自己的解决方案,构建稳定可靠的高并发订单系统。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注