之前面试的时候有人问我怎么保证秒杀系统中库存的准确性,我当时想的是通过mysql的for update加锁的方式来实现,这种方式虽然能一定程度保证库存的准确性,但是并不适用于高并发下的秒杀系统
下面我来讲下怎么用Redis的increment方法和Redisson分布式锁来更加快速高效的保证库存准确性
首先放上代码
// 先去查询商品库存信息
// 优先去Redis里面去查,通过increment保证线程安全,如果Redis里面的库存显示不足则去刷新数据库库存
long stockNum = redisUtil.decr("stock-num" + addOrderParam.getProductNo(), addOrderParam.getNum());
// 如果Redis库存为空则去数据库获取新的库存信息
if (stockNum < 0) {
// 通过Redisson配置分布式全局锁保证数据安全性
RLock stockLock = redissonClient.getLock("GetProductNum" + addOrderParam.getProductNo());
try {
//尝试加锁, 最多等待3秒, 10秒后自动解锁
boolean getStockLock = stockLock.tryLock(3, 10, TimeUnit.SECONDS);
if (getStockLock) {
ApiResult<Integer> apiResultNum = stockFeignService.getProductNum(addOrderParam.getProductNo());
log.info("商品库存数量:" + apiResultNum.toString());
if (apiResultNum.getCode() != 200 || apiResultNum.getData() < 1 || apiResultNum.getData() < addOrderParam.getNum()) {
throw new BusinessException(500, "商品库存不足");
}
redisUtil.set("stock-num" + addOrderParam.getProductNo(), apiResultNum.getData() - addOrderParam.getNum());
}
} catch (Exception e) {
log.error("新增订单查询商品库存异常:" + e.getMessage(), e);
throw new BusinessException(500, "查询商品库存信息异常");
} finally {
//解锁
if (stockLock.isLocked() && stockLock.isHeldByCurrentThread()) {
stockLock.unlock();
}
}
}
// 通过MQ异步去处理扣减库存操作,加快下单响应速度
rocketMQTemplate.asyncSend("reduce_stock_mq", JSON.toJSONString(addOrderParam), new SendCallback() {
@Override
public void onSuccess(SendResult sendResult) {
log.info("推送扣减库存消息成功:" + JSON.toJSONString(addOrderParam));
}
@Override
public void onException(Throwable throwable) {
log.error("推送扣减库存消息失败:" + JSON.toJSONString(addOrderParam) + "------>" + throwable.getMessage());
}
});
redisUtil.decr里面调用的是increment方法
/**
* 递减
*
* @param key 键
* @param delta 要减少几(小于0)
* @return long
*/
public long decr(String key, long delta) {
if (delta < 0) {
throw new RuntimeException("递减因子必须大于0");
}
return redisTemplate.opsForValue().increment(key, -delta);
}
对这个方法的解释如下:
Redis的increment方法的实现原理非常简单,它实际上是对key对应的值进行加1或减1操作,并返回操作后的值。具体实现方式如下:
- 如果key不存在,则将其值设置为0。
- 如果key对应的值可以被解释为数字,则将其进行加1或减1操作,并返回操作后的值。
- 如果key对应的值不能被解释为数字,则返回错误信息。
需要注意的是,Redis的increment方法是原子操作,也就是说在高并发的环境下可以保证其正确性。
如果多个客户端同时对同一个key进行自增或自减操作,Redis会按照客户端的请求顺序进行处理,并保证最终结果是正确的。
可以看到这个方法是线程安全的,我们根据商品ID或者编号将库存放到Redis中,然后通过increment来计算库存保证准确性
当Redis里面库存stockNum不足时,我们再去库存系统获取新的库存信息,为了保证获取库存这个操作的安全性,我们需要用到Redisson分布式锁
这个Redisson使用非常简单
引入jar包,再加上一个配置就可以了
<!--引入redisson作为所有分布式锁,分布式对象等功能-->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.12.0</version>
</dependency>
package com.sakura.common.config;
import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* @author Sakura
* @date 2023/8/12 10:56
*/
@Configuration
public class RedissonConfig {
@Bean
public RedissonClient getRedisson() {
//1.创建配置
Config config = new Config();
config.useSingleServer()
.setAddress("redis://192.168.43.118:6379")
//.setAddress("redis://192.168.43.128:6379") // 如果是集群模式这里可以填多个Redis地址
.setPassword("px123456"); // 如果有密码就需要加上这个
//2.根据config创建处RedissonClient实例
RedissonClient redisson = Redisson.create(config);
return redisson;
}
}
在要使用的地方通过redissonClient获取一个锁就可以
@Autowired
private RedissonClient redissonClient;
RLock lock = redissonClient.getLock("myLock");
lock.lock();
try {
// 执行需要保护的操作
} finally {
lock.unlock();
}
最后我们将扣减库存的操作交给MQ
package com.sakura.stock.mq;
import com.alibaba.fastjson.JSONObject;
import com.sakura.common.constant.RocketMqConstant;
import com.sakura.stock.mapper.StockMapper;
import com.sakura.stock.param.ReduceStockParam;
import lombok.extern.slf4j.Slf4j;
import org.apache.rocketmq.spring.annotation.RocketMQMessageListener;
import org.apache.rocketmq.spring.core.RocketMQListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* @author Sakura
* @date 2023/8/12 16:48
*/
@Component
@Slf4j
@RocketMQMessageListener(topic = RocketMqConstant.REDUCE_STOCK_TOPIC, consumerGroup = RocketMqConstant.REDUCE_STOCK_CONSUMER_GROUP)
public class ReduceStockMQListener implements RocketMQListener<ReduceStockParam> {
@Autowired
private StockMapper stockMapper;
@Override
public void onMessage(ReduceStockParam message) {
log.info("MQ收到扣减库存消息:" + JSONObject.toJSONString(message));
int num = stockMapper.decreaseStock(message.getProductNo(), message.getNum());
if (num < 1) {
log.info("MQ扣减库存信息异常:" + JSONObject.toJSONString(message));
}
}
}
这里我用的是乐观锁的方式,要是想更安全可以用悲观锁,但是对应的性能肯定会有所下降
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.sakura.stock.mapper.StockMapper">
<update id="decreaseStock">
update t_stock
set product_num = product_num - #{num}
where product_no = #{productNo} and product_num - #{num} >= 0
</update>
</mapper>