redis配合lua脚本
redis是可以使用 外部的lua脚本 从而完成一系列操作的原子性
- 在做秒杀的时候,需要把 商品 的id 和 库存余量存入redis ,并且还需要建立一张表,表里面存入有谁已经买过了这个商品(一人一单需要用到) 实际上需要的最重要的参数就是:用户id 和 商品 id
- 基本的流程如下:
- 首先判断库存是否充足
- 不充足就直接返回失败,充足就去看这个人是不是买过了
- 买过了则返回失败,没买过就扣减库存,并且将这个用户id存入redis的表中,代表这个人已经买过了
- 至于最终的存入mysql这样的数据库,完全可以有一定的延时性,比如使用 mq 来做。
local goodsId = ARG[1]
local userId = ARG[2]
local goodsIdMatchStock='seckill:stock:'..goodsId
local goodsIdMatchUserId='seckill:order:'..userId
--在redis中有两个key 一个是 goodsIdMatchStock value 是 库存,假如是100
-- 一个是 goodsIdMatchUserId value 是 set集合,里面存储的是用户的id
--首先是看库存是否充足
if (tonumber(redis.call('get',goodsIdMatchStock))<=0) then
return 1
end
--到这里代表 库存充足 接下来判断这个用户是不是已经买过了
if redis.call('sismember',goodsIdMatchUserId,userId)==1 then
return 2
end
--到这里代表 库存充足 并且 用户没买过 所以要 扣除库存
redis.call('incrby',goodsIdMatchStock,-1)
redis.call('sadd',goodsIdMatchUserId,userId)
return 0
调用lua脚本
@Autowired
private StringRedisTemplate stringRedisTemplate;
private static final DefaultRedisScript<Long> SECKILL_SCRIPT;
/*阻塞队列*/
private BlockingQueue<VoucherOrder> orderTask = new ArrayBlockingQueue<>(1024);
/*线程池*/
private static final ExecutorService SECKILL_ORDER_EXECUTOR = new ThreadPoolExecutor(
4,
8,
10,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(1024)
);
private class VoucherOrderHandler implements Runnable{
@Override
public void run() {
try {
VoucherOrder voucherOrder = orderTask.take();
/*接下来就是处理订单,往数据库插入数据*/
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
@PostConstruct //类初始化完成后,就执行这个方法
private void init(){
SECKILL_ORDER_EXECUTOR.submit(new VoucherOrderHandler());
}
//这里是加载资源文件
static {
SECKILL_SCRIPT = new DefaultRedisScript<>();
SECKILL_SCRIPT.setLocation(new ClassPathResource("seckill.lua"));
SECKILL_SCRIPT.setResultType(Long.class);
}
//这里是redis调用 lua 文件
public Integer seckillByLuaScript(Long goodsId, Long userId) throws Exception {
Long execute = stringRedisTemplate.execute(
SECKILL_SCRIPT,
Collections.emptyList(),
goodsId, userId
);
int i = execute.intValue();
if (i != 0) {
/*代表没有购买资格*/
throw new Exception("没有购买资格");
}
/*有购买资格,这个时候其实库存已经减了,下面只需要将订单放入mysql中就行,而且这个步骤也不着急
* 对于实时性没有那么的高,所以这里只需要把一些信息放入阻塞队列中,并由一个线程去处理这个队列中的信息
* ,后期可以使用mq优化*/
VoucherOrder order=new VoucherOrder();
order.setVoucherId(goodsId);
order.setUserId(userId);
orderTask.add(order);
return null;
}
整体的流程为: 类加载的时候, lua脚本也被加载,并且线程池由于 @PostConstruct 注解的原因,也会开始执行 run方法(方法体的内容是,从阻塞队列中取出数据,插入数据库)。每当用户开始下单的时候,相关参数就会被穿入redis中并执行lua脚本,保证原子性,redis 执行完成后,将相关参数放入阻塞队列,而由于run方法一直在监视着阻塞队列,所以一旦阻塞队列中有元素,就会被取出并执行插入数据库的操作。
但是这样做有一个问题:阻塞队列用的是 JVM 的内存。如果阻塞队列满了怎么办,或者是,由于用户下单量过大,内存溢出了怎么办。