基于redis stream + spring boot的消息队列机制简单实现

1.通知结构整体设计

spring redis视频教程 spring redis stream_spring boot

2.流程解析

  1. 在各个不同的功能点都可能触发消息通知,借助LxNoticeStreamUtil#addNoticeMessage(Integer noticeAction,String actionParam)方法将消息推送到指定队列
  2. 在程序启动时就会由StreamConsumerRunner启动MQ的监听器StreamMessageListener,一旦有消息产生,监听者自动接收消息,并解析执行
  3. 监听器接收到消息后委托给通知发送封装类NoticeMediator#sendNotice(Map<String,String> messageBody)处理
  4. NoticeMediator接收到消息体后根据携带的消息内容解析出需要发送什么通知,从而由NoticeParameterFactory#getStringParameterStrategy(Integer strategyCode)构建出用于获取被通知对象以及消息内容的NoticeParameterStrategy
  5. 每个具体的消息通知需求都可以抽象为一个NoticeParameterStrategy,在这个基础结构中提供了三种方法
  • 反序列化请求发起时的必要参数DTO
    void init(String body)
  • 获取通知的消息体
    T getMessage()
  • 获取被通知者列表
    List<S> listNotifiedPersons()
  1. 构建出消息通知必要的内容后交给Notifier#sendNotice(String message, List<String> noticedPersons)发送通知
  2. 通知完成后对特定消息发起ACK,确认消息消费完成

3.消息补偿机制

目标:在消息一次因为网络或其他可恢复的原因造成消息未消费完成的情况提供一种补偿机制,让消息尽可能消费完成

  1. 使用特定消费者组的特定消费者监听消息队列中挂在消费者下的PendingListPendingList存储着被消费过,但是没有被ACK的消息
  2. 遍历PendingList消费这些未成功处理的消息,若补偿消费成功发送ACK确认
  3. 在3次遍历后还存在未消费的消息则继续存放等待下次补偿机制触发
  4. 判断MQ容量,超过限制自动缩容1/2

4.spring redis connection

@ConditionalOnProperty(name = "spring.redis.client-type", havingValue = "lettuce", matchIfMissing = true)

通过这个注解可知,在我使用的版本已经将lettuce置为默认redis连接池

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.4.4</version>
    <relativePath/> <!-- lookup parent from repository -->
</parent>

4.1自定义redis 连接(standalone)

  1. 构造连接池配置对象
@Bean
    public GenericObjectPoolConfig redisPool() {
        GenericObjectPoolConfig<Object> genericObjectPoolConfig = new GenericObjectPoolConfig<>();
        genericObjectPoolConfig.setMaxWaitMillis(noticeRedisConfiguration.getMaxWait());
        genericObjectPoolConfig.setMaxTotal(noticeRedisConfiguration.getMaxActive());
        genericObjectPoolConfig.setMaxIdle(noticeRedisConfiguration.getMaxIdle());
        genericObjectPoolConfig.setMinIdle(noticeRedisConfiguration.getMinIdle());
        return new GenericObjectPoolConfig<>();
    }
  1. 构造redis连接配置对象
@Bean("redisConfigMaster1")
    public RedisStandaloneConfiguration redisConfigMaster() {
        RedisStandaloneConfiguration redisStandaloneConfiguration = new RedisStandaloneConfiguration(noticeRedisConfiguration.getHost(), noticeRedisConfiguration.getPort());
        redisStandaloneConfiguration.setDatabase(noticeRedisConfiguration.getDatabase());
        redisStandaloneConfiguration.setPassword(RedisPassword.of(noticeRedisConfiguration.getPassword()));
        return redisStandaloneConfiguration;
    }
  1. 配置连接工厂,用于生产与redis的连接
@Bean("factory1")
    public LettuceConnectionFactory factory(@Qualifier("redisConfigMaster1") RedisStandaloneConfiguration redisConfigMaster) {
        LettuceClientConfiguration clientConfiguration = LettucePoolingClientConfiguration.builder().poolConfig(redisPool()).build();
        return new LettuceConnectionFactory(redisConfigMaster, clientConfiguration);
    }
  1. 生成redisTemplate操作对象
@Bean("noticeStringRedisTemplate")
public StringRedisTemplate redisTemplate(@Qualifier("factory1") RedisConnectionFactory factory) {
    return getStringStringRedisTemplate(factory);
}

/**
 * 设置序列化方式 (这一步不是必须的)
 *
 * @param factory
 * @return
 */
private StringRedisTemplate getStringStringRedisTemplate(RedisConnectionFactory factory) {
    StringRedisTemplate template = new StringRedisTemplate(factory);
    GenericJackson2JsonRedisSerializer jackson2JsonRedisSerializer = new GenericJackson2JsonRedisSerializer();
    template.setValueSerializer(jackson2JsonRedisSerializer);
    template.afterPropertiesSet();
    return template;
}

5.小结

  1. 本文设定了一种基于redis stream构建的队列机制,包含队列的消息自动生产与消费,可通过配置与变动StreamConsumerRunner中相关代码实现自定义的扩容。
  2. 本文简单使用单点下的redis作为数据源,读者可根据自己需求构造出集群/哨兵模式下的redis连接配置
  3. spring boot 在高版本使用Lettuce作为redis的默认连接工具
  4. 借助工厂模式+枚举类来屏蔽大量的具体策略类,借助spring的IOC机制来辅助工厂模式生产合适的策略对象.
  5. ApplicationContext可通过实现ApplicationContextAware接口获取
  6. 可通过往期博客了解stream基础支持

github地址 欢迎 star fork