Java高手进阶:秒杀场景下的库存一致性解决方案

秒杀活动作为电商平台的重要营销手段,对库存管理的精确性提出了极高要求。防止超卖,即确保商品在秒杀过程中库存不会被过度消耗,是秒杀功能实现的关键。本文将探讨几种防止超卖的经典方案。

1.悲观锁机制

悲观锁机制通过锁定数据库中的某行数据,确保在高并发情况下只有一个用户可以修改库存。在用户请求秒杀时,数据库会锁定库存行,直到操作完成后才释放锁。

优缺点

优点:强一致性保障,确保在高并发下不会出现超卖问题。缺点:锁的开销较大,容易导致数据库性能瓶颈。在高并发场景下,过多的悲观锁可能导致锁等待和死锁问题。

使用场景适用于对一致性要求极高,但并发量相对较小的场景。

Demo
复制
@Mapper public interface ProductMapper { @Select("SELECT stock FROM products WHERE product_id = #{productId} FOR UPDATE") Integer selectStockForUpdate(@Param("productId") int productId); @Update("UPDATE products SET stock = stock - 1 WHERE product_id = #{productId}") int updateStock(@Param("productId") int productId); } @Service public class ProductService { @Autowired private ProductMapper productMapper; @Transactional public boolean seckill(int productId) { Integer stock = productMapper.selectStockForUpdate(productId); if (stock != null && stock > 0) { productMapper.updateStock(productId); return true; // 秒杀成功 } else { return false; // 库存不足 } } }1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.

2.乐观锁机制

乐观锁通常通过“版本号”机制来实现。在库存表中增加一个version字段,每次更新库存时,检查version是否与上次读取的一致,如果一致则更新库存和version,如果不一致则说明库存已经被其他用户修改过,需要重新尝试。

优缺点优点:无需锁表,对数据库性能影响较小,适合中小规模并发。缺点:并发过高时可能导致更新失败频繁,用户体验下降。在高并发场景下,乐观锁可能导致大量重试,增加系统负担。

使用场景适合于高并发但冲突不频繁的场景。

Demo
复制
// 假设有一个Product实体类,包含stock和version字段 // 在Service层进行库存更新操作 @Service public class ProductService { @Autowired private ProductMapper productMapper; @Transactional public boolean updateStock(int productId, int version) { Product product = productMapper.selectByPrimaryKey(productId); if (product.getVersion() != version) { return false; // 版本不匹配,更新失败 } product.setStock(product.getStock() - 1); product.setVersion(product.getVersion() + 1); productMapper.updateByPrimaryKey(product); return true; // 更新成功 } }1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.

3.分布式锁

分布式锁可以确保在多台服务器上并发处理库存时不会导致超卖。常用Redis来实现分布式锁。当用户请求秒杀时,先尝试通过Redis获得锁,如果获得锁则执行扣减库存操作,并释放锁;如果未获得锁则等待或重试。

优缺点优点:适合大规模并发场景,锁机制能够确保多台服务器在并发情况下安全修改库存。缺点:如果Redis出现故障,可能会影响锁的管理和库存的正确性。锁的粒度要控制好,锁的过大可能影响性能。在高并发场景下,分布式锁可能导致网络延迟和锁竞争问题。

使用场景适用于高并发场景,特别是多台服务器分布式部署时,对一致性要求较高。

Demo
复制
@Service public class SeckillService { @Autowired private RedisTemplate<String, Object> redisTemplate; public boolean seckill(int productId) { String lockKey = "lock:product:" + productId; boolean lock = redisTemplate.opsForValue().setIfAbsent(lockKey, "locked", 5, TimeUnit.SECONDS); if (lock) { try { String stockKey = "stock:" + productId; Integer stock = (Integer) redisTemplate.opsForValue().get(stockKey); if (stock != null && stock > 0) { redisTemplate.opsForValue().decrement(stockKey); return true; // 秒杀成功 } else { return false; // 库存不足 } } finally { redisTemplate.delete(lockKey); // 释放锁 } } else { return false; // 秒杀失败,未获得锁 } } }1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24.25.

4.库存预减+异步处理

在用户请求秒杀时,使用缓存进行库存预减(即在用户下单前就先减少库存),然后通过异步队列(如Kafka或RabbitMQ)将订单请求发往后端进行异步处理。商品秒杀开始前,将商品库存预先加载到Redis。当用户请求秒杀时,先从Redis中预减库存,将请求通过消息队列发送到后端进行订单处理和库存的最终确认。如果订单处理失败(如支付失败等),则通过异步任务将Redis中的库存回补。

优缺点优点:使用缓存大幅减少数据库压力,适合大规模并发场景。削峰填谷,利用消息队列将订单处理异步化,缓解高并发对数据库的冲击。缺点:需要处理订单失败后的库存回补,增加了系统复杂性。在极端情况下可能出现Redis库存与数据库库存不一致的问题,需要通过补偿机制来解决。同时,对缓存的可靠性和一致性要求较高。

使用场景适用于高并发场景,特别是需要减轻数据库压力时,对性能要求较高,但对一致性要求可以容忍一定延迟的场景。

Demo(仅展示库存预减部分,异步处理部分需结合消息队列实现):

复制
@Service public class SeckillService { @Autowired private RedisTemplate<String, Object> redisTemplate; public boolean preDecreaseStock(int productId) { String stockKey = "stock:" + productId; Integer stock = (Integer) redisTemplate.opsForValue().get(stockKey); if (stock != null && stock > 0) { redisTemplate.opsForValue().decrement(stockKey); return true; // 库存预减成功 } else { return false; // 库存不足 } } }1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.

5.小结

以上四种方案各有优劣,选择合适的方法取决于业务需求和技术栈。在实际应用中,可以根据并发量、系统性能要求、一致性需求等因素综合考虑选择合适的方案。

THE END