参考文档

干货分享:五分钟教你解决高并发场景下的订单和库存处理方案每秒上千订单场景下的分布式锁高并发优化实践

方案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库存 redis库存扣减高并发_redis库存


redis库存 redis库存扣减高并发_java_02

如何把库存数据放入到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("添加库存失败");
	}
}