库存保卫战:电商系统防超卖的5把利刃与Java实战

简介: 在电商系统中,超卖是指商品库存不足以满足所有购买请求时,系统仍然接受了超过库存数量的订单。这会导致:商家无法正常发货,用户体验受损,平台信誉下降。

大家好,我是小悟。

目录

  • 问题背景
  • 解决方案概览
  • 数据库悲观锁方案
  • 数据库乐观锁方案
  • Redis原子操作方案
  • 分布式锁方案
  • 消息队列方案
  • 总结与对比

问题背景

在电商系统中,超卖是指商品库存不足以满足所有购买请求时,系统仍然接受了超过库存数量的订单。这会导致:

  • 商家无法正常发货
  • 用户体验受损
  • 平台信誉下降

解决方案概览

方案 适用场景 优点 缺点
数据库悲观锁 并发量不高 实现简单,强一致性 性能瓶颈,死锁风险
数据库乐观锁 读多写少 性能较好 需要重试机制
Redis原子操作 高并发场景 高性能 数据一致性需注意
分布式锁 分布式系统 强一致性 实现复杂
消息队列 流量削峰 系统解耦 实时性较差

数据库悲观锁方案

实现原理

使用数据库的排他锁(SELECT … FOR UPDATE)锁定库存记录,确保同一时间只有一个事务可以修改库存。

Java代码实现

@Service
@Transactional
public class PessimisticLockInventoryService {
    
    @Autowired
    private ProductRepository productRepository;
    
    @Autowired
    private OrderRepository orderRepository;
    
    public boolean purchaseWithPessimisticLock(Long productId, Integer quantity) {
        // 1. 查询商品并加锁
        Product product = productRepository.findWithLock(productId);
        
        if (product == null) {
            throw new RuntimeException("商品不存在");
        }
        
        // 2. 检查库存
        if (product.getStock() < quantity) {
            throw new RuntimeException("库存不足");
        }
        
        // 3. 扣减库存
        int updatedRows = productRepository.decreaseStock(productId, quantity);
        
        if (updatedRows == 0) {
            throw new RuntimeException("库存扣减失败");
        }
        
        // 4. 创建订单
        Order order = new Order();
        order.setProductId(productId);
        order.setQuantity(quantity);
        order.setStatus(1);
        orderRepository.save(order);
        
        return true;
    }
}
// Repository 层实现
@Repository
public interface ProductRepository extends JpaRepository<Product, Long> {
    
    @Query(value = "SELECT * FROM product WHERE id = ?1 FOR UPDATE", nativeQuery = true)
    Product findWithLock(Long id);
    
    @Modifying
    @Query("UPDATE Product p SET p.stock = p.stock - ?2 WHERE p.id = ?1 AND p.stock >= ?2")
    int decreaseStock(Long productId, Integer quantity);
}

数据库乐观锁方案

实现原理

通过版本号机制,在更新时检查数据是否被其他事务修改过,如果版本号不匹配则更新失败。

Java代码实现

@Service
public class OptimisticLockInventoryService {
    
    @Autowired
    private ProductRepository productRepository;
    
    @Autowired
    private OrderRepository orderRepository;
    
    private static final int MAX_RETRY_TIMES = 3;
    
    public boolean purchaseWithOptimisticLock(Long productId, Integer quantity) {
        int retryTimes = 0;
        
        while (retryTimes < MAX_RETRY_TIMES) {
            // 1. 查询商品(不加锁)
            Product product = productRepository.findById(productId)
                    .orElseThrow(() -> new RuntimeException("商品不存在"));
            
            // 2. 检查库存
            if (product.getStock() < quantity) {
                throw new RuntimeException("库存不足");
            }
            
            // 3. 扣减库存(带版本号检查)
            int updatedRows = productRepository.decreaseStockWithVersion(
                    productId, quantity, product.getVersion());
            
            if (updatedRows > 0) {
                // 4. 创建订单
                Order order = new Order();
                order.setProductId(productId);
                order.setQuantity(quantity);
                order.setStatus(1);
                orderRepository.save(order);
                return true;
            }
            
            retryTimes++;
            
            // 重试前短暂休眠
            try {
                Thread.sleep(50);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                throw new RuntimeException("线程中断", e);
            }
        }
        
        throw new RuntimeException("并发更新失败,请重试");
    }
}
// 实体类
@Entity
@Table(name = "product")
public class Product {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    private String name;
    
    private Integer stock;
    
    @Version
    private Integer version;
    
    // getter和setter方法
}
// Repository 层实现
@Repository
public interface ProductRepository extends JpaRepository<Product, Long> {
    
    @Modifying
    @Query("UPDATE Product p SET p.stock = p.stock - ?2, p.version = p.version + 1 " +
           "WHERE p.id = ?1 AND p.stock >= ?2 AND p.version = ?3")
    int decreaseStockWithVersion(Long productId, Integer quantity, Integer version);
}

Redis原子操作方案

实现原理

利用Redis的原子操作(如DECR、LUA脚本)来保证库存扣减的原子性。

Java代码实现

@Service
public class RedisInventoryService {
    
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    
    @Autowired
    private ProductRepository productRepository;
    
    @Autowired
    private OrderRepository orderRepository;
    
    private static final String STOCK_KEY_PREFIX = "product:stock:";
    private static final String LUA_SCRIPT = 
            "if redis.call('exists', KEYS[1]) == 1 then " +
            "local stock = tonumber(redis.call('get', KEYS[1])); " +
            "local num = tonumber(ARGV[1]); " +
            "if stock >= num then " +
            "return redis.call('decrby', KEYS[1], num) " +
            "else " +
            "return -1 " +
            "end " +
            "else " +
            "return -2 " +
            "end";
    
    public boolean purchaseWithRedis(Long productId, Integer quantity) {
        String stockKey = STOCK_KEY_PREFIX + productId;
        
        // 使用LUA脚本保证原子性
        Long result = (Long) redisTemplate.execute(
                new DefaultRedisScript<>(LUA_SCRIPT, Long.class),
                Collections.singletonList(stockKey),
                quantity);
        
        if (result == null) {
            throw new RuntimeException("Redis操作失败");
        }
        
        if (result == -1) {
            throw new RuntimeException("库存不足");
        }
        
        if (result == -2) {
            throw new RuntimeException("商品不存在");
        }
        
        // 异步更新数据库
        asyncUpdateDatabase(productId, quantity);
        
        return true;
    }
    
    @Async
    public void asyncUpdateDatabase(Long productId, Integer quantity) {
        try {
            // 更新数据库库存
            productRepository.decreaseStock(productId, quantity);
            
            // 创建订单
            Order order = new Order();
            order.setProductId(productId);
            order.setQuantity(quantity);
            order.setStatus(1);
            orderRepository.save(order);
        } catch (Exception e) {
            // 如果数据库更新失败,需要回滚Redis库存
            rollbackRedisStock(productId, quantity);
            throw new RuntimeException("数据库更新失败", e);
        }
    }
    
    private void rollbackRedisStock(Long productId, Integer quantity) {
        String stockKey = STOCK_KEY_PREFIX + productId;
        redisTemplate.opsForValue().increment(stockKey, quantity);
    }
    
    /**
     * 初始化Redis库存
     */
    public void initRedisStock(Long productId, Integer stock) {
        String stockKey = STOCK_KEY_PREFIX + productId;
        redisTemplate.opsForValue().set(stockKey, stock);
    }
}

分布式锁方案

实现原理

使用Redis或Zookeeper实现分布式锁,确保在分布式环境下同一时间只有一个服务实例可以操作库存。

Java代码实现

@Service
public class DistributedLockInventoryService {
    
    @Autowired
    private RedissonClient redissonClient;
    
    @Autowired
    private ProductRepository productRepository;
    
    @Autowired
    private OrderRepository orderRepository;
    
    private static final String LOCK_KEY_PREFIX = "inventory_lock:";
    
    public boolean purchaseWithDistributedLock(Long productId, Integer quantity) {
        String lockKey = LOCK_KEY_PREFIX + productId;
        RLock lock = redissonClient.getLock(lockKey);
        
        try {
            // 尝试加锁,最多等待5秒,锁过期时间为10秒
            boolean locked = lock.tryLock(5, 10, TimeUnit.SECONDS);
            
            if (!locked) {
                throw new RuntimeException("系统繁忙,请稍后重试");
            }
            
            // 查询商品库存
            Product product = productRepository.findById(productId)
                    .orElseThrow(() -> new RuntimeException("商品不存在"));
            
            // 检查库存
            if (product.getStock() < quantity) {
                throw new RuntimeException("库存不足");
            }
            
            // 扣减库存
            int updatedRows = productRepository.decreaseStock(productId, quantity);
            
            if (updatedRows == 0) {
                throw new RuntimeException("库存扣减失败");
            }
            
            // 创建订单
            Order order = new Order();
            order.setProductId(productId);
            order.setQuantity(quantity);
            order.setStatus(1);
            orderRepository.save(order);
            
            return true;
            
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            throw new RuntimeException("线程中断", e);
        } finally {
            // 释放锁
            if (lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
        }
    }
}
// Redisson配置类
@Configuration
public class RedissonConfig {
    
    @Value("${spring.redis.host:localhost}")
    private String redisHost;
    
    @Value("${spring.redis.port:6379}")
    private String redisPort;
    
    @Bean
    public RedissonClient redissonClient() {
        Config config = new Config();
        config.useSingleServer()
                .setAddress("redis://" + redisHost + ":" + redisPort)
                .setDatabase(0);
        return Redisson.create(config);
    }
}

消息队列方案

实现原理

通过消息队列进行流量削峰,将订单请求放入队列中顺序处理,避免瞬时高并发。

Java代码实现

@Service
public class MQInventoryService {
    
    @Autowired
    private RabbitTemplate rabbitTemplate;
    
    @Autowired
    private ProductRepository productRepository;
    
    @Autowired
    private OrderRepository orderRepository;
    
    private static final String ORDER_QUEUE = "order.queue";
    private static final String ORDER_EXCHANGE = "order.exchange";
    private static final String ORDER_ROUTING_KEY = "order.create";
    
    /**
     * 接收订单请求,发送到消息队列
     */
    public boolean submitOrder(Long productId, Integer quantity, Long userId) {
        OrderRequest orderRequest = new OrderRequest();
        orderRequest.setProductId(productId);
        orderRequest.setQuantity(quantity);
        orderRequest.setUserId(userId);
        orderRequest.setRequestId(UUID.randomUUID().toString());
        orderRequest.setCreateTime(System.currentTimeMillis());
        
        // 发送消息到队列
        rabbitTemplate.convertAndSend(ORDER_EXCHANGE, ORDER_ROUTING_KEY, orderRequest);
        
        return true;
    }
    
    /**
     * 消费订单消息,处理库存扣减
     */
    @RabbitListener(queues = ORDER_QUEUE)
    public void processOrder(OrderRequest orderRequest) {
        Long productId = orderRequest.getProductId();
        Integer quantity = orderRequest.getQuantity();
        
        try {
            // 查询商品库存
            Product product = productRepository.findById(productId)
                    .orElseThrow(() -> new RuntimeException("商品不存在"));
            
            // 检查库存
            if (product.getStock() < quantity) {
                // 库存不足,记录日志或发送通知
                handleInsufficientStock(orderRequest);
                return;
            }
            
            // 扣减库存
            int updatedRows = productRepository.decreaseStock(productId, quantity);
            
            if (updatedRows == 0) {
                handleInsufficientStock(orderRequest);
                return;
            }
            
            // 创建订单
            Order order = new Order();
            order.setProductId(productId);
            order.setQuantity(quantity);
            order.setUserId(orderRequest.getUserId());
            order.setStatus(1);
            orderRepository.save(order);
            
            // 发送订单创建成功通知
            sendOrderSuccessNotification(orderRequest);
            
        } catch (Exception e) {
            // 处理异常,可以重试或记录错误
            handleProcessError(orderRequest, e);
        }
    }
    
    private void handleInsufficientStock(OrderRequest orderRequest) {
        // 记录库存不足日志
        // 发送库存不足通知给用户
        System.err.println("库存不足,订单请求: " + orderRequest);
    }
    
    private void sendOrderSuccessNotification(OrderRequest orderRequest) {
        // 发送订单创建成功通知
        System.out.println("订单创建成功: " + orderRequest);
    }
    
    private void handleProcessError(OrderRequest orderRequest, Exception e) {
        // 处理异常,可以重试或记录错误
        System.err.println("处理订单异常: " + orderRequest + ", 错误: " + e.getMessage());
    }
}
// 订单请求DTO
@Data
public class OrderRequest {
    private String requestId;
    private Long productId;
    private Integer quantity;
    private Long userId;
    private Long createTime;
}
// RabbitMQ配置类
@Configuration
public class RabbitMQConfig {
    
    @Bean
    public Queue orderQueue() {
        return new Queue("order.queue", true);
    }
    
    @Bean
    public DirectExchange orderExchange() {
        return new DirectExchange("order.exchange");
    }
    
    @Bean
    public Binding orderBinding(Queue orderQueue, DirectExchange orderExchange) {
        return BindingBuilder.bind(orderQueue).to(orderExchange).with("order.create");
    }
}

总结与对比

方案选择建议

  1. 小型系统:推荐使用数据库乐观锁,实现简单且性能较好
  2. 中型系统:推荐使用Redis原子操作,性能优秀且实现相对简单
  3. 大型分布式系统:推荐使用分布式锁+消息队列组合方案,保证强一致性和高可用性

最佳实践

  1. 库存预热:活动开始前将库存加载到Redis中
  2. 限流降级:在网关层实现限流,防止系统过载
  3. 库存预警:设置库存阈值,及时提醒补货
  4. 数据一致性:定期同步Redis和数据库的库存数据
  5. 监控告警:实时监控库存和订单状态,及时发现问题

注意事项

  • 根据业务场景选择合适的方案
  • 考虑系统复杂度和维护成本
  • 做好异常处理和数据补偿
  • 进行充分的压力测试


库存保卫战:电商系统防超卖的5把利刃与Java实战.png


谢谢你看我的文章,既然看到这里了,如果觉得不错,随手点个赞、转发、在看三连吧,感谢感谢。那我们,下次再见。


您的一键三连,是我更新的最大动力,谢谢

山水有相逢,来日皆可期,谢谢阅读,我们再会

我手中的金箍棒,上能通天,下能探海

相关文章
|
18天前
|
监控 Java Nacos
手把手搭建Java微服务:从技术选型到生产部署
每个服务边界的确立,都是在回答一个基本问题:"什么应该在一起,什么应该分离?"这与我们人生中关于人际关系、职业发展的决策何其相似。
71 0
|
17天前
|
监控 数据可视化 Java
Spring Boot 整合 Elasticsearch 及实战应用
这次内容详细介绍如何使用 Spring Boot 整合 Elasticsearch,并提供几个实际应用案例。内容涵盖 Elasticsearch 的基本概念、Spring Boot 整合步骤、实战应用示例以及优化建议。
132 0
|
19天前
|
NoSQL 前端开发 Java
宝塔面板部署Java项目全指南:JDK+Nginx+MySQL+Redis一站式配置
使用宝塔面板部署Java项目确实能省去不少配置环境的时间。下面整理了一份详细的部署步骤,并汇总了关键信息以方便实践。
237 0
|
25天前
|
安全 关系型数据库 网络安全
Navicat通过SSH隧道连接数据库,详细步骤
通过SSH隧道连接数据库的本质是建立一个加密的安全通道。你的Navicat会先通过SSH协议登录到你的远程服务器,然后再通过这个加密的隧道连接服务器上的数据库服务。
122 1
|
23天前
|
Java 关系型数据库 Docker
Docker + Spring Boot:天生一对的完美部署
使用Docker部署Spring Boot项目能极大简化环境配置和应用分发。要将Spring Boot项目部署到Docker,主要流程是:准备项目、创建Docker镜像、运行Docker容器。
107 0
|
存储 监控 数据可视化
Java网络编程:下载进度监控实现详解
文件下载是许多应用程序的重要功能,而下载进度监控是提高用户体验的关键。在本文中,我们将详细介绍如何使用Java实现文件下载进度监控,以便用户可以实时了解文件下载的进度。
393 0
|
2月前
|
存储 SQL 搜索推荐
货拉拉用户画像基于 Apache Doris 的数据模型设计与实践
货拉拉基于Apache Doris构建高效用户画像系统,实现标签管理、人群圈选与行为分析的统一计算引擎,支持秒级响应与大规模数据导入,显著提升查询效率与系统稳定性,助力实时化、智能化运营升级。
243 14
货拉拉用户画像基于 Apache Doris 的数据模型设计与实践
|
2月前
|
消息中间件 存储 Kafka
流、表与“二元性”的幻象
本文探讨流与表的“二元性”本质,指出实现该特性需具备主键、变更日志语义和物化能力。强调Kafka与Iceberg因缺乏更新语义和主键支持,无法真正实现二元性,唯有统一系统如Flink、Paimon或Fluss才能无缝融合流与表。
243 7
流、表与“二元性”的幻象
|
2月前
|
缓存 监控 Java
用 Spring Boot 3 构建高性能 RESTful API 的 10 个关键技巧
本文介绍使用 Spring Boot 3 构建高性能 RESTful API 的 10 大关键技巧,涵盖启动优化、数据库连接池、缓存策略、异步处理、分页查询、限流熔断、日志监控等方面。通过合理配置与代码优化,显著提升响应速度、并发能力与系统稳定性,助力打造高效云原生应用。
468 3
|
2月前
|
机器学习/深度学习 SQL 关系型数据库
TRUNCATE、DELETE、DROP 的区别?
MySQL中DELETE、TRUNCATE和DROP均用于删除数据,但作用不同:DELETE删除行记录,支持WHERE条件和事务回滚,速度慢;TRUNCATE快速清空表并重置自增ID,不可回滚;DROP则彻底删除表结构与数据,操作不可逆。三者在日志记录、速度及功能上有显著差异。
399 0