黑马点评项目学习笔记--(5)秒杀优化

1.分析优惠券秒杀的优化方案

之前的优惠券秒杀存在什么问题?

黑马点评项目学习笔记--(5)秒杀优化_第1张图片
前段接收的情况经过nginx负载均衡到每个tomcat上,而我们的业务流程中,由单个线程对业务进行串行执行,存在许多需要查询数据库的操作,而查询数据库操作本身是耗时的,而且我们还用到了分布式锁,其实大大降低了执行性能。

如何进行秒杀优化?

黑马点评项目学习笔记--(5)秒杀优化_第2张图片
我们先在redis中做能否成功下单的验证,验证通过后,开启一个独立的异步线程,去完成下单的操作。
这样一来,减少了数据库的查询操作,将本来串行执行的业务分给了两个线程取执行,一个负责验证用户是否能够下单,一个负责完成下单动作。

选择怎样的数据结构?

redis中需要保存某个优惠券的库存,这个用string就可以实现,而对于一人一单的校验,就需要一对多的结构了,这个时候需要用到set

基本的业务流程

黑马点评项目学习笔记--(5)秒杀优化_第3张图片

2.异步秒杀实现

1)在添加优惠券到数据库后,将优惠券库存添加到redis中

@Service
public class VoucherServiceImpl extends ServiceImpl<VoucherMapper, Voucher> implements IVoucherService {

    @Resource
    private ISeckillVoucherService seckillVoucherService;

    @Resource
    private StringRedisTemplate stringRedisTemplate;

    @Override
    public Result queryVoucherOfShop(Long shopId) {
        // 查询优惠券信息
        List<Voucher> vouchers = getBaseMapper().queryVoucherOfShop(shopId);
        // 返回结果
        return Result.ok(vouchers);
    }

    @Override
    @Transactional
    public void addSeckillVoucher(Voucher voucher) {
        // 保存优惠券
        save(voucher);
        // 保存秒杀信息
        SeckillVoucher seckillVoucher = new SeckillVoucher();
        seckillVoucher.setVoucherId(voucher.getId());
        seckillVoucher.setStock(voucher.getStock());
        seckillVoucher.setBeginTime(voucher.getBeginTime());
        seckillVoucher.setEndTime(voucher.getEndTime());
        seckillVoucherService.save(seckillVoucher);
        //保存优惠券库存到redis中
        stringRedisTemplate.opsForValue().set(RedisConstants.SECKILL_STOCK_KEY + voucher.getId(),voucher.getStock().toString());
    }
}

2)基于Lua脚本,判断秒杀库存、一人一单,决定用户是否秒杀成功

-- 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订单key
local orderKey = 'seckill:order' .. voucherId

-- 3.脚本业务
-- 3.1.判断库存是否充足 get stockKey
if(tonumber(redis.call('get',stockKey)) <= 0) then
    -- 3.2.库存不足,返回1
    return 1
end
-- 3.2.判断用户是否下单 SISMEMBER orderKey userId
if(redis.call('sismember',orderKey,userId) == 1) then
    -- 3.3.存在,说明重复下单,返回2
    return 2
end
-- 3.4.扣库存 incrby stockKey -1return
redis.call('incrby',stockKey,-1)
-- 3.5.下单(保存用户) sadd orderKey userId
redis.call('sadd',orderKey,userId)
return 0

判断用户是否有购买资格的业务代码:

 @Resource
    private ISeckillVoucherService seckillVoucherService;

    @Resource
    private StringRedisTemplate stringRedisTemplate;

    @Resource
    private RedissonClient redissonClient;

    @Resource
    private RedisIdWorker redisIdWorker;

    //在类加载的时候就将脚本读取好方便直接使用
    private static final DefaultRedisScript<Long> SECKILL_SCRIPT;
    static {
        SECKILL_SCRIPT = new DefaultRedisScript<>();
        SECKILL_SCRIPT.setLocation(new ClassPathResource("seckill.lua"));
        SECKILL_SCRIPT.setResultType(Long.class);
    }

    @Override
    public Result seckillVoucher(Long voucherId) throws InterruptedException {
        //获取用户
        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");
        //TODO 保存阻塞队列
        //3.返回订单id
        return Result.ok(0);
    }

3)抢购成功时,将优惠券订单信息存入阻塞队列

4)开启线程任务,不断从阻塞队列中获取信息,实现异步下单

@Slf4j
@Service
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {

    @Resource
    private ISeckillVoucherService seckillVoucherService;

    @Resource
    private StringRedisTemplate stringRedisTemplate;

    @Resource
    private RedissonClient redissonClient;

    @Resource
    private RedisIdWorker redisIdWorker;

    //在类加载的时候就将脚本读取好方便直接使用
    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 BlockingQueue<VoucherOrder> orderTasks = new ArrayBlockingQueue<>(1024*1024);
    //创建线程池
    private static final ExecutorService SECKILL_ORDER_EXECUTOR = Executors.newSingleThreadExecutor();

    //PostConstruct注解的方法在所在类初始化的时候执行
    @PostConstruct
    private void init(){
        SECKILL_ORDER_EXECUTOR.submit(new VoucherOrderHandler());
    }

    private class VoucherOrderHandler implements Runnable{

        @Override
        public void run() {
            while(true){
                try {
                    //1.获取队列中的订单信息
                    //take()会检索并删除此队列的头,必要时等待,直到某个元素可用为止。
                    VoucherOrder voucherOrder = orderTasks.take();
                    //2.创建订单
                    handleVoucherOrder(voucherOrder);
                } catch (Exception e) {
                    log.error("处理订单异常",e);
                }
            }
        }
    }

    private void handleVoucherOrder(VoucherOrder voucherOrder) {
        //获取用户id
        Long userId = voucherOrder.getUserId();
        //创建锁对象
        RLock lock = redissonClient.getLock("lock:order:" + userId);
        //获取锁
        boolean isLock = lock.tryLock();
        if(!isLock){
            //获取锁失败,返回错误或者重试
            log.error("不允许重复下单");
        }
        try {
            proxy.createVoucherOrder(voucherOrder);
        }finally {
            //释放锁
            lock.unlock();
        }
    }

    private IVoucherOrderService proxy;

    @Override
    public Result seckillVoucher(Long voucherId) throws InterruptedException {
        //获取用户
        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,有购买资格,把下单信息保存到阻塞队列

        VoucherOrder voucherOrder = new VoucherOrder();
        //2.3订单id
        //redisIdWorker可以帮助生成id
        long orderId = redisIdWorker.nextId("order");
        voucherOrder.setId(orderId);
        //2.4用户id
        voucherOrder.setUserId(userId);
        //2.5代金券id
        voucherOrder.setVoucherId(voucherId);
        //2.6放入阻塞队列
        orderTasks.add(voucherOrder);
        //3.获取代理对象
        proxy = (IVoucherOrderService) AopContext.currentProxy();
        //4.返回订单id
        return Result.ok(orderId);
    }

    @Transactional
    public void createVoucherOrder(VoucherOrder voucherOrder) {
        //5.一人一单
        Long userId = UserHolder.getUser().getId();
        //userId.toString().intern()确保了相同的userId过来,锁住的是一个String对象
        //这里的toString()方法实际会创建一个新的字符串对象
        //intern()则会返回字符串常量池中与新创建的字符串对象equal的字符串对象的引用
        //从而保证相同的userId进行了toString().intern()返回的是同一个字符串对象的引用
        //5.1查询订单
        int count = query().eq("user_id", userId).eq("voucher_id", voucherOrder.getVoucherId()).count();
        //5.2判断是否存在该订单
        if (count > 0) {
            //用户已经购买过了
           log.error("用户已经购买过一次!");
        }
        //6.扣减库存
        boolean success = seckillVoucherService.update()
                .setSql("stock = stock - 1")
                .eq("voucher_id", voucherOrder.getVoucherId()).gt("stock", 0)//更新的条件:where id = ? stock > 0
                .update();
        if (!success) {
            //扣减失败
            log.error("库存不足!");
        }
        save(voucherOrder);
    }
}

将优惠券订单添加到阻塞队列中,并由异步线程不断从中取出优惠券订单并创建订单到数据库中。

总结:

秒杀业务的优化思路是什么?
  1. 先利用Redis完成库存余量、一人一单判断,完成抢单业务
  2. 在将下单业务放入阻塞队列,利用独立线程异步下单
基于阻塞队列的异步秒杀存在什么问题?
  • 内存限制问题:阻塞队列大小有限
  • 数据安全问题:可能出现redis中已经修改了库存但是服务突然宕机导致阻塞队列中的订单信息还没有来得及取出并创建到数据库中,从而造成数据不一致

你可能感兴趣的:(黑马点评项目,学习,笔记,java)