黑马点评项目笔记(5)

秒杀优化

原先的秒杀功能虽然完善,但性能不是最佳,并发能力不强。如上图,所有业务逻辑串行执行,那总的执行时间就是每一个功能的执行时间,但是其中还有四个对数据库的操作并且有两个写操作,我们知道数据库的性能其实不好,所以这整个秒杀功能的性能固然不好。

但是整个秒杀的业务逻辑可以分为两部分,查询资格和减库存创订单。我们可以把查询资格和创订单分开。就好像饭店的前台和后厨一样,分工合作。一个线程查询有没有资格,另一个线程创建订单,这样异步执行,提高性能。

而查询资格是查数据库,性能不高。我们可以把数据放缓存里,查缓存来提高性能。

那问题就是怎么把数据存进redis里面,选什么类型的数据。查询优惠券库存用string就行,查是否下单要求一个key能存多个value,且里面的value唯一。所以使用set。业务流程使用lua脚本保证原子性,如下图:

秒杀优化实现

第一步功能实现:

    @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
        redisTemplate.opsForValue().set(SECKILL_STOCK_KEY + voucher.getId(), String.valueOf(voucher.getStock()));
    }

第二步实现:lua脚本

---
--- Generated by EmmyLua(https://github.com/EmmyLua)
--- Created by Mayn.
--- DateTime: 2025/2/28 上午10:45
---

--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.订单key
local orderKey = 'seckill:order' .. userId
--3.判断库存是否充足
if (tonumber(redis.call('get', stockKey)) <= 0) then
    --3.1.库存不充足返回1
    return 1
end
--4.库存充足,判断用户是否下单
if (redis.call('sismember', orderKey, userId) == 1)then
    --4.1.已下单,返回2
    return 2
end
--5.扣库存
redis.call('incrby',stockKey,-1)
--6.创建订单
redis.call('sadd',orderKey,userId)
return 0

java里面执行lua脚本

 //LUA脚本
    private static final DefaultRedisScript<Long> SECKILL_SCRIPT;
    //静态代码块初始化lua脚本
    static {
        SECKILL_SCRIPT = new DefaultRedisScript<>();
        SECKILL_SCRIPT.setLocation(new ClassPathResource("seckill.lua"));//找位置
        SECKILL_SCRIPT.setResultType(Long.class);//设置返回值类型
    }


    @Override
    public Result seckillVoucher(Long voucherId) {
        Long userId = UserHolder.getUser().getId();
        //1、执行lua脚本
        Long result = redisTemplate.execute(
                SECKILL_SCRIPT,
                Collections.emptyList(),
                voucherId.toString(),userId.toString()
        );
        //2、判断返回结果
        int r = result.intValue();
        if (r != 0) {
            //2.1、返回结果不为0,返回异常信息
            return r == 1? Result.fail("库存不充足"):Result.fail("一人只能买一单!");
        }
        //2.2、返回结果为0,TODO 存入阻塞队列
        Long orderId = redisIdWorker.nextId("order");
        //3、返回订单id
        return Result.ok(orderId);
    }

第三步:存入阻塞队列

 //创建阻塞队列
    BlockingQueue<VoucherOrder> orderTask = new ArrayBlockingQueue<>(1024 * 1024);



//2.2、返回结果为0,存入阻塞队列
        VoucherOrder voucherOrder = new VoucherOrder();
        Long orderId = redisIdWorker.nextId("order");
        //2.3、设置订单的各个id
        //2.4、代金券id
        voucherOrder.setVoucherId(voucherId);
        //2.5、用户id
        voucherOrder.setUserId(userId);
        //2.6、订单id
        voucherOrder.setId(orderId);
        //2.7、将订单存入阻塞队列
        orderTask.add(voucherOrder);

第四步:创建异步线程,完成异步下单

创建线程池,创建线程(像下面这样写) 

先创建一个线程池,然后创建一个内部类继承Runnable接口,里面重写run方法,run方法里面写业务逻辑。

由于我们这个下单操作是spring服务器启动就要开始的,所以服务启动后就要开启这个线程执行业务。用到注解@PostConstruct,如下,在里面用线程池submit实现Runnable的类。

//创建线程池
    ExecutorService executorService = Executors.newSingleThreadExecutor();
    @PostConstruct
    public void init() {
        executorService.submit(new voucherOrderHandler());
    }
    private class voucherOrderHandler implements Runnable{

        @Override
        public void run() {
            while (true) {
                try {
                    //1、获取阻塞队列中的订单
                    VoucherOrder order = orderTask.take();
                    //2、创建订单
                    handleVoucherOrder(voucherOrder);
                } catch (InterruptedException e) {
                    log.error("下单异常:{}",e.getMessage());
                }
            }

        }
    }

 创建订单

private void handleVoucherOrder(VoucherOrder voucherOrder) {
        //userId要从线程池里取
        Long userId = voucherOrder.getUserId();
        //创建锁对象
        RLock lock = redissonClient.getLock("lock:order:" + userId);
        boolean isLock = lock.tryLock();
        if (!isLock) {
            //获取锁失败
            log.error("获取锁失败。");
            return;
        }
        try {
            proxy.createVoucherOrder(voucherOrder);
        } finally {
            lock.unlock();
        }
    }
IVoucherOrderService proxy;//代理对象要在外面获取

    @Transactional
    public void createVoucherOrder(VoucherOrder voucherOrder) {
        //5、确保一人一单
        Long userId = voucherOrder.getUserId();

        int count = query().eq("voucher_id", voucherOrder.getVoucherId()).eq("user_id", userId).count();
        if (count > 0) {
            log.error("一人只能一单!");
        }
        //6、库存充足,扣减库存
        boolean success = seckillVoucherService.update().setSql("stock = stock - 1")
                .eq("voucher_id", voucherOrder.getVoucherId())
                .gt("stock", 0)
                .update();
        if (!success) {
            log.error("库存不足!");
        }

        //7.4、保存订单
        save(voucherOrder);
    }

userID不能从Userholder里面拿了,要从线程池里拿。//代理对象要在外面获取。

基于jvm的阻塞队列完成优化秒杀存在问题,如上图。因为jvm的阻塞队列是在内存上工作的。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值