Spring Boot 3+MongoDB销售系统库存一致性保障方案咨询
针对你遇到的库存超卖问题,结合Spring Boot 3 + MongoDB的非响应式架构,给你几个实用的解决方案:
1. 利用MongoDB单文档原子更新(最推荐)
MongoDB的单文档读写操作本身是原子性的,无需依赖事务就能保证并发下的库存一致性。核心思路是把「校验库存充足」和「扣减库存」合并成一个原子操作,从根源避免并发冲突。
代码示例:
先定义商品实体:
@Document(collection = "products") public class Product { @Id private String id; private String name; private int stock; // 库存数量 // getter、setter、构造方法 }
在Service层实现原子扣减逻辑:
@Service public class StockService { private final MongoTemplate mongoTemplate; public StockService(MongoTemplate mongoTemplate) { this.mongoTemplate = mongoTemplate; } // 尝试扣减指定商品库存,返回操作是否成功 public boolean decreaseStock(String productId) { // 查询条件:商品ID匹配,且库存 > 0 Query query = Query.query(Criteria.where("id").is(productId).and("stock").gt(0)); // 更新操作:库存减1 Update update = Update.update("stock", new IncOperator(-1)); // 执行原子更新,返回更新后的文档(更新成功则非空) Product updatedProduct = mongoTemplate.findAndModify(query, update, Product.class); return updatedProduct != null; } }
下单逻辑中调用:
@Service public class OrderService { private final StockService stockService; public OrderService(StockService stockService) { this.stockService = stockService; } public String createOrder(String productId) { if (stockService.decreaseStock(productId)) { // 库存扣减成功,执行创建订单逻辑 return "订单创建成功"; } else { return "库存不足,下单失败"; } } }
这个方案完全依托MongoDB原生特性,无需额外依赖,性能优异,适合绝大多数场景。
2. 乐观锁机制
如果你的业务允许少量冲突重试,可以用乐观锁实现。给商品实体添加版本号字段,每次更新时校验版本号,只有版本匹配才执行更新,冲突时抛出异常,业务层可选择重试或返回失败。
代码示例:
修改商品实体,添加@Version注解:
@Document(collection = "products") public class Product { @Id private String id; private String name; private int stock; @Version private Long version; // 乐观锁版本号 // getter、setter、构造方法 }
Service层扣减逻辑:
@Service public class StockService { private final ProductRepository productRepository; public StockService(ProductRepository productRepository) { this.productRepository = productRepository; } public boolean decreaseStock(String productId) { Optional<Product> productOpt = productRepository.findById(productId); if (productOpt.isEmpty() || productOpt.get().getStock() <= 0) { return false; } Product product = productOpt.get(); product.setStock(product.getStock() - 1); try { productRepository.save(product); return true; } catch (OptimisticLockingFailureException e) { // 版本冲突,说明有其他请求先更新了库存 return false; } } }
若并发较高,可在业务层添加重试逻辑(比如用Spring的@Retryable):
@Retryable(value = OptimisticLockingFailureException.class, maxAttempts = 3) public boolean decreaseStockWithRetry(String productId) { return decreaseStock(productId); }
这个方案实现简单,无需额外中间件,适合并发不是特别高的场景。
3. 应用层分布式锁(多实例部署场景)
如果你的应用是多实例部署,想要更严格的并发控制(虽然单文档原子更新已足够,但部分场景可能需要),可以引入分布式锁(比如基于Redis),确保同一时间只有一个请求能操作某个商品的库存。
代码示例(基于Spring Data Redis):
先实现分布式锁工具类:
@Service public class RedisDistributedLock { private final StringRedisTemplate stringRedisTemplate; private static final String LOCK_PREFIX = "stock:lock:"; private static final long LOCK_EXPIRE = 30000; // 锁过期时间30秒 public RedisDistributedLock(StringRedisTemplate stringRedisTemplate) { this.stringRedisTemplate = stringRedisTemplate; } // 尝试获取锁 public boolean tryLock(String productId) { String lockKey = LOCK_PREFIX + productId; // 用SETNX原子命令获取锁 Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, "locked", LOCK_EXPIRE, TimeUnit.MILLISECONDS); return Boolean.TRUE.equals(success); } // 释放锁 public void unlock(String productId) { String lockKey = LOCK_PREFIX + productId; stringRedisTemplate.delete(lockKey); } }
结合分布式锁和原子更新逻辑:
@Service public class StockService { private final MongoTemplate mongoTemplate; private final RedisDistributedLock redisDistributedLock; public StockService(MongoTemplate mongoTemplate, RedisDistributedLock redisDistributedLock) { this.mongoTemplate = mongoTemplate; this.redisDistributedLock = redisDistributedLock; } public boolean decreaseStock(String productId) { // 先获取分布式锁 if (!redisDistributedLock.tryLock(productId)) { // 获取锁失败,直接返回库存不足或触发重试 return false; } try { // 执行原子更新库存操作 Query query = Query.query(Criteria.where("id").is(productId).and("stock").gt(0)); Update update = Update.update("stock", new IncOperator(-1)); Product updatedProduct = mongoTemplate.findAndModify(query, update, Product.class); return updatedProduct != null; } finally { // 无论成功失败,释放锁 redisDistributedLock.unlock(productId); } } }
注意:分布式锁要设置合理的过期时间避免死锁,且锁的粒度要细(按商品ID加锁),不要用全局锁影响性能。
补充说明
你提到MongoDB不支持事务,其实MongoDB 4.0及以上版本支持多文档事务,但需要部署副本集或分片集群。不过对于库存扣减这种单文档操作,完全不需要事务,单文档原子更新已经足够,且性能更好。只有涉及多文档操作(比如扣减库存同时生成订单记录)时,才需要用到事务,前提是你的MongoDB环境支持。
内容的提问来源于stack exchange,提问作者Carlos C.M

