高并发下 如何安全、高效扣减库存? 有更好的方案?

参考文档

干货分享:五分钟教你解决高并发场景下的订单和库存处理方案
聊聊高并发下库存加减那些事儿——“如何实现异步扣减库存”
Redis如何实现高并发分布式锁?
如何利用Redis分布式锁处理高并发?
每秒上千订单场景下的分布式锁高并发优化实践

方案1 :redis或Redisson 作分布式锁 + mq + mysql

特点: 强制加锁, 串行执行,能支持的并发量不高

@Autowired
private StringRedisTemplate stringRedisTemplate;
@PutMapping(value = "/subtractStock")
public boolean subtractStock(String productId, int num) throws Exception {
	//用uuid来标记是哪个线程加的锁
    String clientUuid = UUID.randomUUID().toString();
	Long start = System.currentTimeMillis();
	try {
		boolean isLock = false;
		for(int i=0; i<3; i++){
			//尝试3次加锁, 同时设置过期时间
			isLock = stringRedisTmplate.opsForValue()
				.setIfAbsent("lock_"+productId, clientUuid, 10, TimeUnit.SECONDS);
			if(isLock){
				break;
			}
			Thread.sleep(100);
		}		
		// 判断是否获得锁
		if (!isLock) { return false; }
		Object value = stringRedisTemplate.opsForValue().get("stock_"+productId);
		if(value == null){
			//前提 提前将商品库存放入缓存 ,如果缓存不存在,视为没有该商品
			return false;
		}
        int newStock = stringRedisTemplate.opsForValue().increment("stock_"+productId, -num);
		//库存充足
		if (newStock >= 0) {	
			LogUtil.info("成功抢购");	
			//TODO 向MQ 发消息,  对mysql 进行减库存,减少响应时间
		} else {
			//库存不足,把减掉的库存 加回来
			stringRedisTemplate.opsForValue().increment("stock_"+productId, num);
			LogUtil.info("库存不足,并发");
			return false;
		}
		return true;
    } finally {
		Long end = System.currentTimeMillis();
		Long costTime = end - start;
		if(costTime < 10){
			//锁还未超时, 业务就已经处理完, 需要手动释放锁
			// 删除锁的时候判断是不是自己的锁
		    if(clientUuid.equals(stringRedisTemplate.opsForValue().get("lock_"+productId))){
				stringRedisTemplate.delete("lock_"+productId);   
			}
		}
        //否则锁已经超时, Redis会自动释放锁
    }
    return true;
}

2. 方案2 不加锁 使用redis的increment原子操作 + 补偿机制 + mq + MySQL

public boolean subtractStock(String orderCode,String skuCode, Integer num) {
	String key = "shop-product-stock" + skuCode;
	Object value = redis.get(key);
	if (value == null) {
		//前提 提前将商品库存放入缓存 ,如果缓存不存在,视为没有该商品
		return false;
	}
	//第一次检查库存, 如果不足, 则无需继续后面的操作
	Integer stock = (Integer) value;
	if (stock < num) {
		LogUtil.info("库存不足");
		return false;
	} 	
    //不可在这里直接操作数据库减库存,否则导致数据不安全
    //因为此时可能有其他线程已经将redis的key修改了
	//redis 减少库存,然后才能操作数据库
	Long newStock = redis.increment(key, -num.longValue());
	
	//减库存后, 再次检查库存
	if (newStock >= 0) {	
		LogUtil.info("成功抢购");
		//TODO 向MQ 发消息,  对mysql 进行减库存,减少响应时间
	} else {
		//库存不足,把减掉的库存 加回来
		redis.increment(key, num.longValue());
		LogUtil.info("库存不足,并发");
		return false;
	}
	return true;
}

3. 方案3 redis分库 + mq(RocketMQ、kafka) + mysql分库、分表

假设每个减库存操作的响应时间优化到50毫秒,并发2000,按照常规做法加全局锁那第2000个人的响应时间便是前面1999个用户的响应时间加他自己的50毫秒之和为100秒。100秒的响应可能用户早就心里默默诅咒你了。而且这已经是非常理想化的单次响应时间了。如果有人说可以优化到2毫秒就不会超时了。。麻烦带上键盘去微博杠吧。。
假设三个用户请求减库存操作,完全可以让三个请求进三个不同的锁去扣减各自的库存数,此时三人没有排队可以保证他们同时减库存,而又不影响库存总数的准确性,因为三个请求操作的是各自锁所维护的库存数。随着业务增长,库存总数的分割可以不断细分直到缩短响应时间到合理范围,而这个库存总数的分割很好的保证了不会遇到瓶颈。但是由于这种业务架构的设计,导致业务不得不变得复杂,可以看到我们在进入分布式锁之前有一个称为库存总数协调器的模块.

在这里插入图片描述
在这里插入图片描述

如何把库存数据放入到Redis缓存中?

在把库存添加到MySQL的方法里 同时添加到缓存

@Transactional(isolation = Isolation.REPEATABLE_READ,
			propagation = Propagation.REQUIRED, rollbackFor = Exception.class)
public void addProduct(String productId, int num){
	try{
		//向MySQL或Oracle等添加商品库存
		dao.save(productId, num);
		Object value = stringRedisTemplate.opsForValue().get("stock_"+productId);
		if(value == null){
			//第一次把库存 放入Redis
			stringRedisTemplate.opsForValue().set("stock_"+productId, num);
		}else{			
			//increment 是一个原子操作, 类似于AtomicInteger的 getAndAdd(), addAndGet();
			stringRedisTemplate.opsForValue().increment("stock_"+productId, num);
		}
	}catch(){
		log.error("添加库存失败");
	}
}
  • 7
    点赞
  • 40
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
好的,这个问题可以回答。对于高并发订单扣减库存的问题,需要考虑多线程并发和数据库事务的管理。以下是一个示例代码: ```java public class OrderService { private final InventoryDao inventoryDao; public OrderService(InventoryDao inventoryDao) { this.inventoryDao = inventoryDao; } // 扣减库存,返回值为扣减是否成功 public boolean reduceStock(int itemId, int quantity) { int stock = inventoryDao.getStock(itemId); if (stock < quantity) { return false; } return inventoryDao.reduceStock(itemId, quantity); } } public class InventoryDao { // 获取库存 public int getStock(int itemId) { String sql = "SELECT quantity FROM inventory WHERE item_id = ?"; try (Connection conn = getConnection(); PreparedStatement statement = conn.prepareStatement(sql)) { statement.setInt(1, itemId); try (ResultSet rs = statement.executeQuery()) { if (rs.next()) { return rs.getInt("quantity"); } } } catch (SQLException e) { throw new RuntimeException(e); } return 0; } // 扣减库存,返回值为扣减是否成功 public boolean reduceStock(int itemId, int quantity) { String sql = "UPDATE inventory SET quantity = quantity - ? WHERE item_id = ?"; try (Connection conn = getConnection()) { conn.setAutoCommit(false); try (PreparedStatement statement = conn.prepareStatement(sql)) { statement.setInt(1, quantity); statement.setInt(2, itemId); int affectedRows = statement.executeUpdate(); if (affectedRows == 1) { conn.commit(); return true; } } conn.rollback(); } catch (SQLException e) { throw new RuntimeException(e); } return false; } private Connection getConnection() throws SQLException { // 获取数据库连接 } } ``` 在这个示例代码中,OrderService 是对外提供的服务类,它依赖于 InventoryDao 来管理库存。reduceStock 方法是扣减库存的核心方法,它首先从数据库中获取当前库存数量,然后检查是否足够扣减。如果足够扣减,就开启一个数据库事务,更新库存数量,最后提交事务。如果不足够扣减,就直接返回 false 表示扣减失败。 在实际使用中,需要根据具体场景进行优化,比如使用连接池来提高数据库连接的性能,采用分布式锁来控制并发等等。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值