使用Redis来实现消息队列(stream)

redis消息队列分3种
1.List : 不支持消息确认机制,不支持消息回朔
2.pubSub :不支持消息确认机制,不支持消息回朔,不支持消息持久化
3.stream :支持消息确认机制,支持消息回朔,支持消息持久化,支持消息阻塞

因此我们采用stream来处理消息队列

STREAM类型消息队列的XREADGOUP命令特点:

  • 消息可回朔
  • 可以多消费者争抢消息,加快消费速度
  • 可以阻塞读取
  • 没有消息漏读风险
  • 有消息确认机制,保证消息至少被消费一次

伪代码

1.提前创建我们的消费者组

//需提前创建我们的消费者组  或者执行我们的redis指令为 : 	 XGROP  CREATE stream.orders g1 0  MKSTREAM
stringRedisTemplate.opsForStream().createGroup("stream.orders",ReadOffset.from("0"),"g1");

2.发送消息,可以采用我们的lua脚本。或者我们的java代码都可以。这里使用lua脚本,使我们的redis操作原子性

--把发消息的步骤放到我们的lua脚本里面  也可以通过java代码发送   这边需要处理该lua脚本  
--发送消息 XADD stream.orders * k1 v1 k2 v2 ...
--1.1 优惠卷id
local voucherId = ARGV[1]
-- 1.2 用户id
local userId = ARGV[2]

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 判断库存是否充足  get stockKey
if (tonumber(redis.call('get', stockKey)) <=  0 ) then
  -- 3.2 库存不足返回-1tonumber
    return 1
end

-- 3.2 判断用户是否重复下单 SISMEMBER orderKey
if (redis.call('sismember',orderKey,userId) == 1) then
    --3.3 存在说明是重复下单 返回2
    return 2
end

--4  扣减库存 incrby  stockKey   -1
    redis.call('incrby',stockKey,-1)

-- 5 下单保存用户 sadd orderKey userId

redis.call('sadd',orderKey , userId)
redis.call('XADD','stream.orders','*','userId',userId,'voucherId',voucherId,'id',orderId)
return 0

3.由于我们使用的是lua脚本,因此我们需要解析该lua脚本


    private static final DefaultRedisScript<Long> SECKILL_SCRIPT ;
    //提前加载lua脚本的信息  避免平凡io操作
    static {
        SECKILL_SCRIPT = new DefaultRedisScript<>();
        SECKILL_SCRIPT.setLocation(new ClassPathResource("seckill.lua"));
        SECKILL_SCRIPT.setResultType(Long.class);
    }
    
  private IVoucherOrderService proxy ;

    @Override //改造  redis队列消息
    public Result seckillVoucher(Long voucherId) {

        
        UserDTO user = UserHolder.getUser();
        //6.1 订单id
        long orderId = redisIdWorker.nextId("order");

		//1 执行lua脚本
        Long result = stringRedisTemplate.execute(SECKILL_SCRIPT,
                Collections.emptyList(),
                voucherId.toString(),
                user.getId().toString(),
                String.valueOf(orderId));
        //2 判断结果为0
        int r = result.intValue();
        if (r != 0){
            //3 不为0,代表没有购买资格
            return Result.fail(r == 1 ?"库存不足":"该用户不能重复下单");
        }

        //获取代理对象   这部操作主要获取我们的代理类的对象去执行我们的事务方法子线程
        proxy = (IVoucherOrderService) AopContext.currentProxy();

        return Result.ok(orderId);
    }

4.开启一个线程,去不断的获取我们的消息,进行消费。

获取到的消息,需要ACK确认消息已经确认消费。如果没有进行ACK,那么就会进入我们的peding-list异常消息中。如果发生异常,那么就会去到我们的peding-list进行处理异常消息。

    //线程池
    private static final ExecutorService SECKILL_ORDER_EXECUTOR = Executors.newSingleThreadExecutor();

    @PostConstruct //在当前类初始化完毕后才去执行
    private void init(){
        SECKILL_ORDER_EXECUTOR.submit(new VoucherOrderHandler());
    }
       
 private class VoucherOrderHandler implements Runnable{
        private static final String queueName = "stream.orders";
        @Override
        public void run() {
            while (true){
                try {
                    //1 获取消息队列中的订单信息  XREADGROUP  GROUP g1 c1 COUNT 1 BLOCK 2000 STREAMS streams.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()){
                        //2.1 获取失败 说明没有消息继续下一次循环
                        continue;
                    }
                  //3 解析消息中的数据
                    MapRecord<String, Object, Object> entries = list.get(0);
                    Map<Object, Object> value = entries.getValue();//键值对
                    VoucherOrder voucherOrder = BeanUtil.fillBeanWithMap(value, new VoucherOrder(), true);//让其自动转换为我们需要的对象

                    //2.1 获取成功  可以处理我们的业务
                    dowork(voucherOrder);
                    //2 ACK确认消息已经确认消费  SACK stream.orders g1 id
                    stringRedisTemplate.opsForStream().acknowledge(queueName,"g1",entries.getId());

                } catch (Exception e) {
                    log.error("处理业务异常",e);
                    HandlePendingList();
                    e.printStackTrace();
                }
                //2 创建订单
            }
        }
		//处理异常消息  ReadOffset.from("0")
        private void HandlePendingList() {
            while (true) {
                try {
                    //1 获取pending-list中的消息队列中的业务信息  XREADGROUP  GROUP g1 c1 COUNT 1 BLOCK 2000 STREAMS streams.order 0
                    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.from("0"))
                    );
                    //2 判断消息是否获取成功
                    if (list == null || list.isEmpty()) {
                        //2.1 获取失败 说明pending-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);//让其自动转换为我们需要的对象

                    //2.1 获取成功  可以重新处理我们的业务
                    dowork(voucherOrder);
                    //2 ACK确认消息已经确认消费  SACK stream.orders g1 id
                    stringRedisTemplate.opsForStream().acknowledge(queueName, "g1", entries.getId());

                } catch (Exception e) {
                    log.error("处理pending-list业务异常", e);
                    try {
                        Thread.sleep(2000);
                    } catch (InterruptedException ex) {
                        ex.printStackTrace();
                    }
                    e.printStackTrace();
                }
            }
        }

Redis指令

  1. 添加消息指令
    xadd s1 * k1
    [s1:表示我们的队列,k1 :表示内容]

  2. 创建消息组指令
    XGROP CREATE s1 g1 0 MKSTREAM
    [s1:表示我们的队列名 g1:表示消费者的组名 0:表示我创建了这个组,如果队列里面有消息我们就消费,如果是$符号表示s1队列的消息,我们不要 MKSTREAM:表示队列不存在时自动创建]

  3. 从消费者组内读取消息指令
    XREADGROUP GROUP g1 c1 COUNT 1 BLOCK 2000 STREAMS s1 >
    [g1:表示组名 c1:表示消费者,如果没有会自动创建 1:表示一次获取一个消息查询最大数量 2000:当没有消息的时候等待2秒 s1:表示监听的队列 >:表示从下一个未消费的消息开始 ]

  4. 确认已消费的消息指令
    XACK s1 g1 1660668751543-0
    【s1: 队列 g1:组名 1660668751543-0 :需确认的消息】

  5. 查询未确认的消息指令
    XPENDING s1 g1 - + 10
    【s1:队列 g1:组名】

  6. XREADGROUP GROUP g1 c1 COUNT 1 BLOCK 2000 STREAMS s1 > 0【0:获取未确认的消息,出现异常的消息】

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
`redisTemplate.opsForStream().read()` 是 Redis Streams 数据结构中的一个读取操作。它用于从指定的 Stream 中读取数据,并返回最新一条未被读取的数据。 具体来说,`read()` 可以接受多个参数,其中最重要的是 `StreamOffset`,它指定了要读取的 Stream 的名称和读取位置。`read()` 还可以指定其他参数,比如读取的最大条目数、阻塞时间等等。 以下是一个使用 `read()` 读取 Stream 数据的示例代码: ```java // 创建 StreamOffset 对象 StreamOffset<String, String> offset = StreamOffset.create("mystream", ReadOffset.lastConsumed()); // 从 Redis 中读取数据,最多读取 10 条,阻塞时间为 1 秒 List<MapRecord<String, String, String>> records = redisTemplate.opsForStream().read( Consumer.from("mygroup", "myconsumer"), offset, Limit.limit().count(10), Duration.ofSeconds(1) ); // 处理读取到的数据 for (MapRecord<String, String, String> record : records) { System.out.println(record.getId() + ": " + record.getValue()); } // 更新消费者组的消费位置 redisTemplate.opsForStream().acknowledge("mygroup", "myconsumer", records.stream().map(MapRecord::getId).toArray(String[]::new)); ``` 在这个示例中,我们首先创建了一个 `StreamOffset` 对象,它表示了我们要从 `mystream` 这个 Stream 中读取最新未被读取的数据。然后,我们调用 `read()` 方法,指定了要使用的消费者组和消费者名称、读取位置、最大条目数和阻塞时间。`read()` 方法返回一个包含多个 `MapRecord` 对象的列表,每个 `MapRecord` 对象代表了一条 Stream 中的数据项。 最后,我们遍历读取到的数据,处理每一条数据,并使用 `acknowledge()` 方法将消费者组的消费位置更新到最新值。这样,Redis 就会知道哪些数据已经被消费,哪些数据还未被消费。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值