【Redis消息队列实现异步秒杀】--Redis学习笔记08

前言

秒杀业务的优化思路:

  1. 先利用Redis完成库存余量、一人一单判断,完成抢单业务
  2. 再将下单业务放入队列中(阻塞队列,消息队列),利用独立线程异步下单

基于阻塞队列的异步秒杀存在哪些问题?

  1. 内存限制问题
  2. 数据安全问题

一、基于阻塞队列实现异步秒杀

1.秒杀流程图

【Redis消息队列实现异步秒杀】--Redis学习笔记08_第1张图片

2.新增秒杀优惠券的同时,将优惠券信息保存到Redis中

    @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);
        stringRedisTemplate.opsForValue()
                .set(RedisConstants.SECKILL_STOCK_KEY+voucher.getId(),voucher.getStock().toString());
    }

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

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

4.如果抢购成功,将优惠券id和用户id封装后存入阻塞队列

	import java.util.concurrent.ArrayBlockingQueue;
	import java.util.concurrent.BlockingQueue;
	
	private  IVoucherOrderService proxy;
	//创建阻塞队列
    private BlockingQueue<VoucherOrder> orderTask=new ArrayBlockingQueue<>(1024*1024);
    
    public Result seckillVoucher(Long voucherId) {
        // 1.获取用户
        Long userId = UserHolder.getUser().getId();
        // 2.执行Lua脚本
        Long result = stringRedisTemplate.execute(SECKILL_SCRIPT,// 执行的Lua脚本
                Collections.emptyList(),// 生成空集合
                voucherId.toString(), userId.toString());// 脚本中需要获取的ARGV参数
        // 3.判断结果不为0
        if (result.intValue() !=0){
            return Result.fail(1==result.intValue() ? "库存不足":"不能重复下单");
        }
        //4.为0,有购买资格,把下单信息保存到阻塞队列中
        VoucherOrder voucherOrder = new VoucherOrder();
        long orderId = redisIdworker.nextId("order");
        voucherOrder.setId(orderId);
        voucherOrder.setVoucherId(orderId);
        voucherOrder.setUserId(userId);
        voucherOrder.setVoucherId(voucherId);
        orderTask.add(voucherOrder);
        // 获取线程的代理对象
        proxy=(IVoucherOrderService) AopContext.currentProxy();
        return Result.ok(orderId);
    }

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

import java.util.concurrent.ExecutorService;

//创建线程池
    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 {
                    // 获取队列中的订单信息orderTask.take()获取队列中的第一个元素,如果队列中没有元素,则一直等待。
                    VoucherOrder voucherOrder = orderTask.take();
                    //创建订单
                    handleVoucherOrder(voucherOrder);
                } catch (Exception e) {
                    log.error("处理订单异常",e);
                }
            }
        }
    }

 private void handleVoucherOrder(VoucherOrder voucherOrder) {
        //1.获取用户
        Long userId = voucherOrder.getUserId();
        //2.创建锁对象
        RLock lock = redissonClient.getLock("lock:order" + userId);
        //3.获取锁
        boolean isLock = lock.tryLock();
        // 4.判断是否获取锁成功
        if (!isLock){
            log.error("不允许重复下单");
            return;
        }
        try {
            proxy.createVoucherOrder(voucherOrder);
        } catch (Exception e) {
           log.error("创建订单异常",e);
        }

    }

 	@Override
    public void createVoucherOrder(VoucherOrder voucherOrder) {
        // 1.查询用户
        Long userId = voucherOrder.getUserId();
        // 2 查询订单
        int count = query().eq("user_id", userId).eq("voucher_id", voucherOrder.getVoucherId()).count();
        // 3 判断订单是否存在
        if (count > 0) {
            log.error("用户已购买过一次!");
        }
        // 4.扣减库存
        boolean success = seckillVoucherService.update()
                .setSql("stock = stock - 1") // set stock=stock -1
                .eq("voucher_id", voucherOrder.getVoucherId()).gt("stock", 0)//where id=? and stock > 0
                .update();
        if (!success){
            log.error("库存不足");
            return;
        }
        // 5.保存信息
        save(voucherOrder);

    }

二、基于Redis消息队列实现异步秒杀

1.基本概念

消息队列(Message Queue),字面意思就是存放消息的队列。最简单的消息队列模型包括3个角色:
消息队列:存储和管理消息,也被称为消息代理(Message Broker)
生产者:发送消息到消息队列
消费者:从消息队列获取消息并处理消息

Redis提供了三种不同的方式来实现消息队列:
list结构:基于List结构模拟消息队列
PubSub:基本的点对点消息模型
Stream:比较完善的消息队列模型

消息队列和阻塞队列的区别:

  1. 消息队列是在JVM以外的独立服务,不受JVM内存限制。
  2. 存进消息队列中的数据,会进行持久化,保证宕机时消息不会丢失,同时会有消费者确认机制,消费者未确认,则消息队列中数据依然存在,会继续让消费者消费,确保消息不会丢失。

因list结构和PubSub都无法解决消丢失的问题,本文主要使用Redis 5.0引入的Stream
【Redis消息队列实现异步秒杀】--Redis学习笔记08_第2张图片
【Redis消息队列实现异步秒杀】--Redis学习笔记08_第3张图片
【Redis消息队列实现异步秒杀】--Redis学习笔记08_第4张图片

2.修改之前的秒杀下单Lua脚本,在认定有抢购资格后,直接向stream.orders中添加消息,内容包含voucherId、userId、orderId

---1.参数列表
--- 1.1 优惠券id
local voucherId=ARGV[1];
--- 1.2 用户ID
local userId=ARGV[2];
--- 1.3 订单id
local orderId=ARGV[3];

--- 2数据key
--- 2.1库存key ..表示拼接
local stockKey ='seckill:stock:'.. voucherId
--- 2.2 订单key
local orderKey ='seckill:order:'.. voucherId

--- 3.业务脚本
--- 3.1 判断库存是否充足
if (tonumber(redis.call('get',stockKey)) <=0) then
    --- 3.2 库存不足
    return 1
end
--- 3.2 判断用户是否已经下过单.如果存在就会返回1
if (redis.call('sismember',orderKey,userId)== 1)then
    --- 3.3 存在即为重复下单,返回2
    return 2
end
--- 3.4 扣减库存
redis.call('incrby',stockKey,-1)
--- 3.5 下单保存用户
redis.call('sadd',orderKey,userId)
--- 3.6发送消息到队列中 XADD stream.order * k1 v1 k2 v2 ...
redis.call('xadd','stream.orders','*','userId',userId,'voucherId',voucherId,'id',orderId)
return 0

3.项目启动时,开启一个线程任务,尝试获取stream.orders中的消息,完成下单

import cn.hutool.core.bean.BeanUtil;
import com.hmdp.dto.Result;
import com.hmdp.entity.SeckillVoucher;
import com.hmdp.entity.VoucherOrder;
import com.hmdp.mapper.VoucherOrderMapper;
import com.hmdp.service.ISeckillVoucherService;
import com.hmdp.service.IVoucherOrderService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.hmdp.utils.RedisIdworker;
import com.hmdp.utils.UserHolder;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.aop.framework.AopContext;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.redis.connection.stream.*;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import java.time.Duration;
import java.time.LocalDateTime;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

  	@PostConstruct // 当前类初始化完成开始执行
    private void init(){
        SECKILL_ORDER_EXECUTOR.submit(new VoucherOrderHandler());
    }

    private class VoucherOrderHandler implements Runnable {
        String queueName = "stream.orders";

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

        private void handlePendingList() {
            while (true) {
                try {
                    //1.从消息队列中获取订单信息 XREADGROUP GROUP g1 c1 COUNT 1  STREAM STREAMS stream.order 0
                    List<MapRecord<String, Object, Object>> list = stringRedisTemplate.opsForStream().read(
                            Consumer.from("g1", "c1"),
                            StreamReadOptions.empty().count(1),
                            StreamOffset.create(queueName, ReadOffset.from("0"))
                    );
                    // 2.判断消息获取是否成功
                    if (list == null || list.isEmpty()) {
                        //如果为空,则说明PendLing-list没有消息,则结束循环
                        break;
                    }
                    // 3.解析消息
                    MapRecord<String, Object, Object> entries = list.get(0);
                    Map<Object, Object> value = entries.getValue();
                    VoucherOrder voucherOrder = BeanUtil.fillBeanWithMap(value, new VoucherOrder(), true);
                    //创建订单
                    handleVoucherOrder(voucherOrder);
                    // ACK 确认
                    stringRedisTemplate.opsForStream().acknowledge(queueName, "g1", entries.getId());
                } catch (Exception e) {
                    log.error("处理Pending-list订单异常", e);
                    try {
                        Thread.sleep(50);
                    } catch (InterruptedException e1) {
                        e1.printStackTrace();
                    }
                }
            }
        }

    }

你可能感兴趣的:(Redis,redis,学习,lua)