简介
我们都知道秒杀是一个高并发,大量请求的场景,如果每次秒杀,我们都直接去操作数据库,校验库存,扣减库存,大量请求的话,数据库肯定扛不住,会出现各种问题。那怎么办?数据库虽然扛不住,但是redis能抗,所以我们可以使用定时任务,提前将秒杀商品的库存同步到redis中,每次秒杀请求,扣减redis中的库存,然后异步修改数据库的库存。到这里,大量请求又会出现一个问题,假如redis中某秒杀库存为1,这时候有两台服务器查询,发现都是1,都执行了redis库存扣减操作,这个时候,redis中的商品库存会变成-1,产生了超卖问题。这个如何解决呢?可以配合lua脚本来解决,众所周知,redis保证lua脚本以原子性的方式执行:当某个脚本正在运行的时候,不会有其他脚本或 redis命令被执行。扣减库存操作写在lua脚本中,即可防止超卖。
完整流程:首先使用定时任务,将数据库秒杀商品库存同步到redis中,秒杀时,使用lua脚本扣减redis中的库存,然后将订单信息打入阻塞队列中,之后异步处理阻塞队列中的订单信息,进行数据库的库存扣减,订单信息生成等等操作。
流程图
下单流程
异步处理订单流程
实现
lua脚本
-- 存活时间
local expire = tonumber(ARGV[2])
local value = tonumber(ARGV[1])
local key = KEYS[1]
-- 加锁
local ret = redis.call("SET", key, value, "NX", "EX", expire)
local strret = tostring(ret)
if strret == 'false' then
return 0
else
return 1
end
if redis.call('get', KEYS[1]) == ARGV[1] then
return redis.call('del', KEYS[1])
else
return 0
end
--商品id
local goodsId = tostring(ARGV[1])
--用户id
local userId = ARGV[2]
--购买数量
local num = ARGV[3]
--商品库存
local stock = "cache_seckill_goods:"..goodsId
--商品订单列表
local order = "cache_seckill:order:"..goodsId
-- 查询商品库存
local stockNum = redis.call('get',stock)
if tonumber(stockNum) ~= nil and tonumber(stockNum) >= tonumber(num) then
-- 判断用户是否下过单
if redis.call('sismember',order,tonumber(userId)) == 0 then
-- 扣除库存
redis.call('incrby',stock,-tonumber(num))
redis.call('sadd',order,tonumber(userId))
return 1 -- 下单成功
else
return 0 -- 下单失败:用户限购
end
else
return -1 -- 库存不足
end
Java代码
redis运行lua脚本的工具类
/**
* 运行lua脚本
* @param fileName lua文件名
* @param keyList key
* @param args value
*/
public Object runRedisScript(String fileName,List<String> keyList,Object... args){
killLuaScript.setLocation(new ClassPathResource("/lua/"+fileName)); // 指定脚本文件路径
killLuaScript.setResultType(Long.class); // 指定脚本返回值类型
log.info("准备运行/lua/{}脚本,key为:{},参数为:{}",fileName,keyList.toArray(),args);
Object result = redisTemplate.opsForValue().getOperations().execute(killLuaScript,keyList,args);
log.info("运行/lua/{}脚本结束,结果为:{}",fileName,result);
return result;
}
/**
* 加锁
* @param key
* @param seconds
* @return
*/
public Object lock(String key,Integer seconds){
List<String> keyList = new ArrayList<>();
keyList.add(key);
return runRedisScript("lock.lua",keyList,1,seconds);
}
/**
* 解锁
* @param key
* @return
*/
public Object unlock(String key){
List<String> keyList = new ArrayList<>();
keyList.add(key);
return runRedisScript("unlock.lua",keyList,1);
}
订单服务类
/**
* 订单服务类
* @author gcp
*/
@Service
@Slf4j
public class OrderService extends ServiceImpl<OrderMapper, Order> {
/**
* 等待处理订单
*/
private static BlockingQueue<HashMap<String,Object>> orders = new ArrayBlockingQueue<>(1024);
/**
* 处理失败订单
*/
private static BlockingQueue<HashMap<String,Object>> loseOrders = new ArrayBlockingQueue<>(1024);
/**
* 处理完成的订单
*/
private static final String ORDER_DEAL_KEY = "cache_order_deal:";
/**
* 定义线程池
*/
private static final ThreadPoolExecutor POOL_EXECUTOR = new ThreadPoolExecutor(2,4, 30,TimeUnit.SECONDS, new ArrayBlockingQueue<>(10),new TestThreadFactory());
/**
* 自定义线程工厂
*/
private static class TestThreadFactory implements ThreadFactory{
private AtomicInteger count = new AtomicInteger(0);
@Override
public Thread newThread(Runnable r) {
Thread t = new Thread(r);
t.setName("线程"+count.addAndGet(1));
return t;
}
}
@Resource
RedisCache redisCache;
@Resource
GoodsService goodsService;
@Resource
OrderGoodsService orderGoodsService;
/**
* spring容器初始化的时候执行
*/
@PostConstruct
private void orderhandle() {
//处理等待队列
POOL_EXECUTOR.submit(new Runnable() {
@SneakyThrows
@Override
public void run() {
while(true) {
//队列中取出商品id
HashMap<String,Object> take = orders.take();
if(!take.isEmpty()) {
OrderService orderService = ReflectUtil.getBean(OrderService.class);
try {
orderService.createOrder(take);
} catch (Exception e) {
e.printStackTrace();
// 处理失败的订单进入失败订单队列
loseOrders.put(take);
}
}
}
}
});
//处理失败队列
POOL_EXECUTOR.submit(new Runnable() {
@SneakyThrows
@Override
public void run() {
while(true) {
//队列中取出商品id
HashMap<String,Object> take = loseOrders.take();
if(!take.isEmpty()) {
log.info("秒杀请求失败,请重新下单:{}",take.get("orderId"));
}
}
}
});
}
/**
* 创单逻辑
* @param take
*/
@Transactional(rollbackFor = Exception.class)
public void createOrder(HashMap<String,Object> take){
log.info("开始处理订单:{}",take.get("orderId"));
SecKillReqDto secKillReqDto = JSON.parseObject(take.get("orderId").toString(),SecKillReqDto.class);
Goods goods = goodsService.getById(secKillReqDto.getGoodsId());
//更新商品库存
goodsService.update(Wrappers.lambdaUpdate(Goods.class).set(Goods::getStockNum,goods.getStockNum()-secKillReqDto.getNum()).set(Goods::getFreezeNum,goods.getFreezeNum()+secKillReqDto.getNum()).set(Goods::getSaleNum,goods.getSaleNum()+secKillReqDto.getNum()).eq(Goods::getId,secKillReqDto.getGoodsId()));
//订单信息
Order order = new Order().setOrderPrice(goods.getPrice()).setCreateTime(LocalDateTime.now()).setUserId(secKillReqDto.getUserId()).setStatus(0).setExpiration_time(LocalDateTime.now().plusMinutes(10));
this.save(order);
//订单商品信息
OrderGoods orderGoods = new OrderGoods().setUserId(secKillReqDto.getUserId())
.setGoodsId(secKillReqDto.getGoodsId())
.setNum(secKillReqDto.getNum())
.setCreateTime(LocalDateTime.now())
.setPrice(goods.getPrice().multiply(BigDecimal.valueOf(secKillReqDto.getNum())))
.setOrderId(order.getId()).setStatus(0);
orderGoodsService.save(orderGoods);
//保存处理完成信息,保存10s
redisCache.setCacheObject(ORDER_DEAL_KEY+secKillReqDto.getUserId()+":"+secKillReqDto.getGoodsId(),1,10,TimeUnit.SECONDS);
log.info("处理订单:{}结束",order.getId());
}
/**
* 秒杀
* @param secKillReqDto 秒杀入参
*/
public Boolean secKill(SecKillReqDto secKillReqDto) throws InterruptedException {
List<String> list = new ArrayList<>();
Object executeRes;
try {
//运行lua脚本
executeRes = redisCache.runRedisScript("secKill.lua",list , secKillReqDto.getGoodsId(),secKillReqDto.getUserId(),secKillReqDto.getNum());
} catch (Exception e) {
e.printStackTrace();
throw new CommonException("秒杀请求失败!!!!");
}
if ((Long) executeRes == 1L) {
// 将用户下单信息保存到阻塞队列
HashMap<String, Object> hashMap = new HashMap<>();
hashMap.put("orderId", JSON.toJSONString(secKillReqDto));
orders.put(hashMap);
log.info("用户[{}]秒杀请求成功",secKillReqDto.getUserId());
return true;
} else if((Long) executeRes == -1L){
throw new CommonException("库存不足");
}else{
throw new CommonException("限制购买");
}
}
}