原本流程
修改后
如何在Redis中判断秒杀库存和校验一人一单
将信息存入Redis中
Lua脚本
--1.参数列表
-- 1.1 优惠券id
local voucherId = ARGV[1]
-- 1.2 用户id
local userId = ARGV[2]
--2.数据key
-- 2.1 库存key
local stockKey = "seckill:stock:" .. voucherId
-- 2.2 订单id
local orderKey = "seckill:order:" .. voucherId
--脚本业务
-- 3.1 判断库存是否充足
if (tonumber(redis.call("get", stockKey) )<= 0) then
-- 库存不足
return 1
end
-- 3.2判断用户是否下单
if(redis.call("sismember", orderKey, userId) == 1) then
-- 已经下单
return 2
end
-- 3.3 库存减一 incrby stockKey -1
redis.call("incrby", stockKey, -1)
-- 3.4 下单 sadd orderKey userId
redis.call("sadd", orderKey, userId)
return 0
完整实现流程
private static final DefaultRedisScript<Long> SECKILL_SCRIPT;
static{
SECKILL_SCRIPT = new DefaultRedisScript<>();
SECKILL_SCRIPT.setLocation(new ClassPathResource("seckill.lua"));
SECKILL_SCRIPT.setResultType(Long.class);
}
private IVoucherOrderService proxy;
@Override
public Result seckillVoucher(Long voucherId) {
Long userId = UserHolder.getUser().getId();
// 1.执行lua脚本
Long result = stringRedisTemplate.execute(
SECKILL_SCRIPT,
Collections.emptyList(),
voucherId.toString(),
userId.toString()
);
// 2.判断返回值是否为0
int r = result.intValue();
if (r != 0) {
// 2.1 不为0 没有购买资格
return Result.fail(r==1?"库存不足!":"已经抢过券了!");
}
// 2.2 为0 有购买资格 把下单信息保存到阻塞队列
long orderId = redisIdWorker.nextId("order");
VoucherOrder voucherorder = new VoucherOrder();
voucherorder.setId(orderId);
voucherorder.setVoucherId(voucherId);
voucherorder.setUserId(userId);
// 2.3放入阻塞队列
orderTasks.add(voucherorder);
// 3 获取代理对象
proxy = (IVoucherOrderService) AopContext.currentProxy();
return Result.ok(orderId);
}
线程中新增一个线程用来处理阻塞队列中的订单
@PostConstruct注解init方法在 类构造时开始执行
// 阻塞队列
private BlockingQueue<VoucherOrder> orderTasks = new ArrayBlockingQueue<>(1024*1024);
// 线程池
private static final ExecutorService SECKILL_ORDER_EXECUTOR = Executors.newSingleThreadExecutor();
@PostConstruct
private void init(){
SECKILL_ORDER_EXECUTOR.submit(new VoucherOrderHandler());
}
// 线程任务
private class VoucherOrderHandler implements Runnable{
@Override
public void run() {
while(true){
try {
// 获取队列中的订单信息
VoucherOrder voucherOrder = orderTasks.take();
// 创建订单
handleVoucherOrder(voucherOrder);
}catch (Exception e){
log.error("处理订单异常", e);
}
}
}
}
private void handleVoucherOrder(VoucherOrder voucherOrder) {
Long userId = voucherOrder.getUserId();
RLock lock = redissonClient.getLock("lock:order:" + userId);
// 获取锁
boolean isLock = lock.tryLock();
if(!isLock) {
// 获取锁失败 返回错误信息或重试
log.info("获取锁失败");
return;
}
try {
// 创建代理对象 解决事务问题
proxy.createVoucherOrder(voucherOrder);
}finally {
lock.unlock();
}
}
@Transactional
public void createVoucherOrder(VoucherOrder voucherOrder) {
// 一人一单
Long userId = voucherOrder.getUserId();
int count = query().eq("user_id", userId).eq("voucher_id", voucherOrder.getVoucherId()).count();
// 判断是否存在
if (count > 0) {
log.error("重复下单!");
}
// 5.扣减库存
boolean success = seckillVoucherService.update()
.setSql("stock = stock - 1")
.eq("voucher_id", voucherOrder.getVoucherId()).gt("stock", 0)
.update();
if(!success) {
log.error("库存不足!");
}
save(voucherOrder);
}
秒杀的优化思路:
先利用Redis完成库存余量、一人一单判断,完成抢单业务
再将下单业务放入阻塞队列,利用独立线程异步下单
还存在的问题:
可能内存溢出 阻塞队列用的是JVM的内存