Redis实战篇笔记(五)

Redis实战篇笔记——异步秒杀思路


前言

本系列文章是针对于黑马的Redis教学视频中的实战篇,本篇文章是实战篇的第五部分——异步秒杀思路,本篇内容相较于上几次的笔记稍微轻松一点,主要是异步思路的学习

异步秒杀思路

之前我们的秒杀下单都是同步进行的,也就是说我们的 查询、判断、下单都是在一个线程中做的,也就是说当一个用户他经历了 查询、判断、下单后,另一个用户才可以开始他的流程 。那这样用户数量多后,时间会有亿点点长。下面是1000个用户同时下单,我们可以看到平均值是 497 还是有点高的。
image.png

那我们接下来的思路就是去将这个秒杀变成异步的,具体来说就是 主线程来进行查询,判断,成功后返回给用户一个 订单 Id,让用户可以先拿着这个订单Id 去付款,然后一个子线程从 阻塞队列中拿到 这个订单的信息,然后去数据库中下单。这个时候下单的逻辑就和其他的业务是异步的了,就像饭店中,服务员负责点单,把小票给厨师,厨师根据小票做菜。

查询,判断的业务


因为查询,判断都是对数据库的读操作,对于这些操作,我们将其放在 Redis中处理会更快些。但是这些操作我们要保证他的原子性,对此我们可以加锁,也可以用 lua 脚本,这里我们为了方便,就用 lua脚本来操作

---
--- Generated by EmmyLua(https://github.com/EmmyLua)
--- Created by 86156.
--- DateTime: 2022/12/23 16:48
---
-- 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.脚本业务
-- 3.1 判断库存释放充足 get stockKey
-- 这里要注意一下,get命令返回的是字符串,要转成number
if(tonumber(redis.call('get', stockKey))<=0) then
  -- 3.2 库存不足,返回1
  return 1
end

-- sismember 是 redis 中 set的一个命令
-- 在这里用来判断 userId在不在 orderKey 这个 set里面
-- 3.2 判断用户是否下单 sismember orderKey userId
if(redis.call('sismember',orderKey,userId)==1) then
  --3.3 存在,已经下过单,说明这次是重复下单,返回2
  return 2
end
-- 3.4 扣库存 incrby stockKey -1
redis.call("incrby",stockKey,-1)
-- 3.5 下单 (保存用户) sadd orderKey userId
redis.call("sadd",orderKey,userId)
return 0

下面是Java代码

private static final DefaultRedisScript<Long> SECKILL_SCRIPT;
// 我们像调用 unlock那个lua脚本一样,在类中用一个静态代码块来初始化
// 因为这个类都需要这个 lua脚本。
static {
    
    
        SECKILL_SCRIPT=new DefaultRedisScript<>();
        SECKILL_SCRIPT.setLocation(new ClassPathResource("seckill.lua"));
        SECKILL_SCRIPT.setResultType(Long.class);
    }
 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
        if(result !=0){
    
    
            //2.1 不为0,代表没有购买资格
            return Result.fail(result==1 ? "库存不足" : "不能重复下单");
        }
        //2.2 为0,将来要把下单信息放到阻塞队列
        long orderId = redisIdWorker.nextId("order");
        // TODO 生成订单信息,放到阻塞队列

下单操作

之前我们说了查询,判断成功后,要将订单信息放到一个消息队列中,然后让一个线程去这个队列中取出信息去完成下单,下面我们就来实现。

  // ctrl+shift+u 转大写 创建一个线程池
    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 {
    
    
                   // 1.获取队列中的订单信息
                   VoucherOrder voucherOrder = orderTasks.take();
                   // 2.创建订单
                   handleVoucherOrder(voucherOrder);
               } catch (Exception e) {
    
    
                   log.error("处理订单异常",e);
               }
           }
        }


    }
    // 这里我们把这个 proxy 抽出来作为一个成员变量,是因为,我们在这个
    // handleVoucherOrder方法中也是要这个代理对象的,但是调用这个方法的
    // 是我们创建的线程池,而AopContext.currentProxy()是从threadlocal中
    // 获取proxy的,所以是获取不到的,所以我们这里把这个proxy抽出来
    private IVoucherOrderService proxy;
    private void handleVoucherOrder(VoucherOrder voucherOrder) {
    
    
        Long userId = voucherOrder.getUserId();
        RLock lock = redissonClient.getLock("lock:order" + userId);
//        SimpleRedisLock lock = new SimpleRedisLock("order", stringRedisTemplate);
        boolean isLock = lock.tryLock();
        if(!isLock){
    
    
            log.error("不允许重复下单");
        }
        try {
    
    
            proxy.createVoucherOrder(voucherOrder);
        } finally {
    
    
            lock.unlock();
        }
    }

 @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
        if(result !=0){
    
    
            //2.1 不为0,代表没有购买资格
            return Result.fail(result==1 ? "库存不足" : "不能重复下单");
        }
        //2.2 为0,将来要把下单信息放到阻塞队列
        long orderId = redisIdWorker.nextId("order");
        VoucherOrder voucherOrder = new VoucherOrder();
        // 添加订单id
        voucherOrder.setId(orderId);
        // 添加用户
        voucherOrder.setUserId(userId);
        // 添加优惠券id
        voucherOrder.setVoucherId(voucherId);
        // 获取代理对象
        proxy = (IVoucherOrderService) AopContext.currentProxy();
        // 放入阻塞队列
        orderTasks.add(voucherOrder);
       
        return Result.ok(orderId);
    }

@Transactional
    // ctrl+alt+m,实现将代码块快速封装为一个函数
    public void createVoucherOrder(VoucherOrder voucherOrder) {
    
    
        Long userId = UserHolder.getUser().getId();

        // 判断一人一单
        int count = query().eq("user_id", userId).eq("voucher_id", voucherOrder.getId()).count();
        if (count > 0) {
    
    
            log.error("用户已经购买过一次");
        }
        // 5.扣除库存
        boolean success = seckillVoucherService.update()
                .setSql("stock=stock-1").eq("voucher_id", voucherOrder.getId()).gt("stock", 0).update();
        if (!success) {
    
    
            log.error("库存不足");
        }
        this.save(voucherOrder);
    }

看完代码后,我们再来回顾一下整个异步秒杀的流程

  1. 先是去 redis 中查询库存和判断是否已经秒杀过
  2. 查询和判断成功后,就生成订单信息,并且准备好 proxy代理对象给子线程用,然后将订单信息放到阻塞队列中,返回给用户订单信息,这样用户这就相当于拿到订单了
  3. 子线程从 阻塞队列中获取订单信息,并用代理对象调用创建订单对象,去数据库中添加订单。

最后我们再来看一下这个用阻塞队列来实现异步秒杀的缺点:

  1. 这个阻塞队列是 jdk 从内存中创建的,高并发的情况下会导致内存溢出,所以我们创建时给他加了一个限制,但是加限制后,当队列满后,就无法添加了
  2. 就像刚才说的,这个阻塞队列是从内存中创建的,一旦服务器宕机后,那数据就全部丢失了,数据问题很大,所以接下来我们学习一种全新的消息队列。

总结

本篇内容我们学习了关于秒杀业务一些异步化的思路,将生成订单和数据库处理分离,本质还是数据库写入数据较为生成订单缓慢,如果让用户一直等到数据库做完操作后再返回结果,效果较差,所以我们这里用异步的方式提升用户体验。但异步,我们需要用一个容器将消息存储后再分发,现在我们是在内存中创建的队列来存储,但是内存一断电数据就消失了,我们需要使用一个类似于数据库的容器进行存储消息,这就是我们接下来要了解的消息队列了,由于消息队列内容过多,我们留在下期再聊

最后,我是Mayphyr,从一点点到亿点点,我们下次再见

猜你喜欢

转载自blog.csdn.net/aaaaaaaa273216/article/details/133347843