Redis:优化秒杀

一、Redis优化秒杀:异步秒杀

原先的步骤:
1、查询优惠券
2、判断秒杀库存;
3、查询订单;
4、校验一人一单;
5、减库存、创建订单;
这些 涉及到大量数据库的操作,所以在高并发 的情况下,性能并不是很好;

解决方案:通过redis,先将优惠券库存信息和订单信息存入redis,用lua脚本执行,保证原子性;将用户id用set集合存到redis中,对于同一张优惠券,如果userId存在,用户不能重复下单,以此判断用户是否有资格购买,然后异步开启独立线程执行,创建订单,放入阻塞队列,线程读取队列中的信息,完成秒杀操作,异步创建订单,与同步创建相比,在高并发情况下,大大提高响应时间
在这里插入图片描述

在这里插入图片描述

二、基于redis完成异步秒杀资格判断

在这里插入图片描述

秒杀业务的优化思路?
1、先利用Redis完成库存余量、一人一单的判断,完成抢单业务
2、再将下单业务放到阻塞队列,利用独立线程异步下单;

基于阻塞队列的异步秒杀存在的问题?
1、内存限制问题;如果服务宕机,会导致内存数据丢失,任务丢失;
2、数据安全问题;

 /**
     * 创建阻塞队列,1024 * 1024 指定队列大小,避免cpu过多消耗,将订单信息放入阻塞队列中
     */
    private BlockingQueue<VoucherOrder> voucherOrderBlockingQueue = new ArrayBlockingQueue<>(1024 * 1024);

    /**
     * 异步线程,执行保存订单到数据库
     */
    private static final ExecutorService VOUCHER_ORDER_EXECUTOR = Executors.newSingleThreadExecutor();

    /**
     * PostConstruct注解,什么时候执行?在用户秒杀执行之前去执行,这个任务在这个类初始化之后就来执行submit
     */
    @PostConstruct
    private void init() {
        VOUCHER_ORDER_EXECUTOR.submit(new VoucherOrderHandler());
    }

    /**
     * 异步线程,从阻塞队列中取出订单信息,执行保存订单到数据库
     */
    public class VoucherOrderHandler implements Runnable{

        @Override
        public void run() {
            while (true){
                // 只要队列中有,就不断去取,不用担心陷入死循环,因为take()还有队列中有才会取,没有就不会拿
                try {
                    VoucherOrder voucherOrder = voucherOrderBlockingQueue.take();
                    // 创建订单
                    createVoucherOrder(voucherOrder);
                } catch (Exception e) {
                    log.error("订单异常信息",e);
                }
            }

        }


    }

    /**
     * 用于异步秒杀哦,创建订单,不会影响秒杀业务的执行;兜底方案
     * @param voucherOrder
     */
    private void createVoucherOrder(VoucherOrder voucherOrder) {
        Long voucherId = voucherOrder.getVoucherId();
        Long userId = voucherOrder.getUserId();
        String name = "order:" + userId;
        // 拿到锁对象锁
        RLock clientLock = redissonClient.getLock(RedisConstants.LOCK + name);
        // 不传参,失败了直接结束,默认值 ong time = 0L, TimeUnit unit 30s,超时30s之后直接是否
        boolean tryLock = clientLock.tryLock();
        if (!tryLock){
            log.error("不能重复领取优惠券");
            return;
        }
        try {
            int count = this
                    .query()
                    .eq("user_id", userId)
                    .eq("voucher_id", voucherId).count();
            if (count > 0) {
                log.error("你已下过单");
                return;
            }
            boolean sucess = iSeckillVoucherService.update()
                    // CAS 法 Compare and Switch**:比较修改。在版本号的基础上,
                    // 既然用version字段前后可以比较得出这条数据是否发生变化,那同样,
                    // 直接用stock库存本身来比较,stock前后是否发生了变化;
                    .setSql("stock = stock -1") // set stock = stock -1
                    .eq("voucher_id", voucherId).gt("stock", 0)
                    // 乐观锁的缺点:**   成功率低,由于多个线程同时对优惠券进行操作,如果有一个线程拿到了锁,
                    // 其他线程可能就会直接取消抢购,没有不断的重试,造成优惠券大量富余,库存大量富余,最后库存没有卖完。
                    // 所以这里这样判断stock > 0即可
                    .update();
            if (!sucess) {
                log.error("库存不足");
                return;
            }
            // 6、将数据存入优惠券订单表
            save(voucherOrder);
        } finally {
            // 判断线程是不是当前线程
            // 2、最后一定要释放锁
            clientLock.unlock();
        }
    }

三、Redis消息队列实现异步秒杀

消息队列:包含
1、消息队列:存储和管理消息,也被称为消息代理M(Message Broker)
2、生产者:发送消息到消息队列;
3、消费者:从消息队列获取消息并处理消息;

消息队列与阻塞队列的区别:
1、消息队列是在JVM以外的独立服务,不受jvm内存的限制;解决了内存限制问题
2、消息队列的数据要做持久化,如果服务宕机了,数据也不会丢失;而且,消息队列要消费者做消息的确认,确保消息至少被消费一次;
3、市面上的rabbitMQ、kafka等等,但是redis可以利用list结构模拟消息队列;
在这里插入图片描述

四、基于List结构模拟消息队列

在这里插入图片描述
在这里插入图片描述

五、基于Pubsub的消息队列

发布订阅:是redis2.0版本引入的消息传递模型,消费者可以订阅一个或者多个频道,向对应的频道发送消息之后,所有订阅者都能收到消息;

在这里插入图片描述

在这里插入图片描述

六、基于Stream的消息队列

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

七、Stream的消费者组

消费者组:将多个消费者划分到一个小组中,监听同一个队列;具备以下特点:
1、消息分流:队列中的消息分流给组内的不同消费者,而不是重复性消费,避免消息堆积,加快消息处理速度;
2、消息标示:消费者组会维护一个标示,记录最后一个被处理的消息,哪怕消费者宕机重启,会从标志之后读取消息,确保每一个消息都会被消费;
3、消息确认:消费者获取消息之后,消息处于pending(待处理)状态,并存入一个penging-list列表。处理完成之后需要通过XACK来确认消息,标记消息为已处理,并从pending-list中移除;

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

八、基于Stream消息队列实现异步秒杀

/**
     * 新的玩法:异步线程,从消息队列中取出订单信息,执行保存订单到数据库
     */
    public class VoucherOrderHandler implements Runnable{

        @Override
        public void run() {
            while (true) {
                try {
                    // 1.获取消息队列中的订单信息 XREADGROUP GROUP g1 c1 COUNT 1 BLOCK 2000 STREAMS s1 >
                    List<MapRecord<String, Object, Object>> list = stringRedisTemplate.opsForStream().read(
                            Consumer.from("g1", "c1"),
                            StreamReadOptions.empty().count(1).block(Duration.ofSeconds(2)),
                            StreamOffset.create("stream.orders", ReadOffset.lastConsumed())
                    );
                    // 2.判断订单信息是否为空
                    if (list == null || list.isEmpty()) {
                        // 如果为null,说明没有消息,继续下一次循环
                        continue;
                    }
                    // 解析数据
                    MapRecord<String, Object, Object> record = list.get(0);
                    Map<Object, Object> value = record.getValue();
                    VoucherOrder voucherOrder = BeanUtil.fillBeanWithMap(value, new VoucherOrder(), true);
                    // 3.创建订单
                    createVoucherOrder(voucherOrder);
                    // 4.确认消息 XACK
                    stringRedisTemplate.opsForStream().acknowledge("s1", "g1", record.getId());
                } catch (Exception e) {
                    log.error("处理订单异常", e);
                    handlePendingList();
                }
            }
        }

        private void handlePendingList() {
            while (true) {
                try {
                    // 1.获取pending-list中的订单信息 XREADGROUP GROUP g1 c1 COUNT 1 BLOCK 2000 STREAMS s1 0
                    List<MapRecord<String, Object, Object>> list = stringRedisTemplate.opsForStream().read(
                            Consumer.from("g1", "c1"),
                            StreamReadOptions.empty().count(1),
                            StreamOffset.create("stream.orders", ReadOffset.from("0"))
                    );
                    // 2.判断订单信息是否为空
                    if (list == null || list.isEmpty()) {
                        // 如果为null,说明没有异常消息,结束循环
                        break;
                    }
                    // 解析数据
                    MapRecord<String, Object, Object> record = list.get(0);
                    Map<Object, Object> value = record.getValue();
                    VoucherOrder voucherOrder = BeanUtil.fillBeanWithMap(value, new VoucherOrder(), true);
                    // 3.创建订单
                    createVoucherOrder(voucherOrder);
                    // 4.确认消息 XACK
                    stringRedisTemplate.opsForStream().acknowledge("s1", "g1", record.getId());
                } catch (Exception e) {
                    log.error("处理订单异常", e);
                }
            }
        }
    }

    @Override
    public Result seckillVoucher(Long voucherId) {
        // 获取用户
        Long userId = UserHolder.getUser().getId();
        // 执行seckill.lua脚本
        Long result = stringRedisTemplate.execute(SECKILL_LUA, Collections.emptyList(), voucherId.toString(), userId.toString());
        // 拆箱
        int i = result.intValue();
        // 判断是否为0,不是0,为1的话说明是库存不足,为2说明重复下单
        if (i != 0){
            return Result.fail(i == 1 ? "库存不足" : "不能重复下单");
        }
        // todo 将创建的订单信息保存到消息队列
        long orderId = redisIdWorker.generateOnlyId("order");
        // 返回订单id
        return Result.ok(orderId);
    }
  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值