如何用redis做消息中间件


前言

最近发现公司内部很多同事利用redis搭配定时器做了一些类似消息中间件的功能。一面生产一面消费。不过每个人都有各自的做法,于是对针对每种做法做了一些总结。


提示:以下是本篇文章正文内容,下面案例可供参考

一、利用Hash数据结构,搭配xxljob方式?直接上代码

生产者

业务代码省略
stringRedisTemplate.opsForHash().put(com.kyexpress.vms.adapter.provider.constant.Constants.VIDEO_CUT_OUT_RECORD_REDIS_KEY, videoCutOutBO.getThirdNo(),
                    JsonUtils.deserializer(videoCutOutBO));

消费者 部分代码

@Scheduled(cron = "0/30 * * * * ?")
    public void execute() {
        if (RedisTemplateLockUtil.tryGetDistributedLock(stringRedisTemplate, VIDEO_CUT_OUT_STATUS_LOCK_KEY, VIDEO_CUT_OUT_STATUS_LOCK_VALUE, 60 * 1)) {
            logger.info("queryVideoCutOutStatus#execute获取锁成功");
            try {
                long beginTime = System.currentTimeMillis();
                executeQueryVideoCutOutStatus();
                logger.info("queryVideoCutOutStatus execute total time:{}", System.currentTimeMillis() - beginTime);
            } catch (Exception e) {
                logger.error("queryVideoCutOutStatus#execute出错:", e);
            } finally {
                RedisTemplateLockUtil.releaseDistributedLock(stringRedisTemplate, VIDEO_CUT_OUT_STATUS_LOCK_KEY, VIDEO_CUT_OUT_STATUS_LOCK_VALUE);
            }
        }
    }

    public void executeQueryVideoCutOutStatus() {
        List<Object> objectList = stringRedisTemplate.opsForHash().values(Constants.VIDEO_CUT_OUT_RECORD_REDIS_KEY);
        省略
stringRedisTemplate.opsForHash().delete(Constants.VIDEO_CUT_OUT_RECORD_REDIS_KEY, 
videoCutOutBO.getThirdNo());

从代码可以看出,这里就是通过Hash结构作为存储,搭配定时任务做消费。定时器的业务时间是30
秒,但是有眼尖的同学应该可以看到定时器的入口加了一把分布式锁。这是为什么呢?

思来想去,那可能就是怕30秒执行不完业务代码,带来重复消费的问题把,因为key是在最后被删除掉的。

二、利用List结构,搭配while死循环方式

生产者

stringRedisTemplate.opsForList().leftPush(Constants.VIDEO_NAV_UPLOAD_REDIS_KEY, JSON.toJSONString(videoNavBO));

消费者1

@Override
    public void run(String... args) throws Exception {
        String redisLstKey = com.kyexpress.vms.adapter.provider.constant.Constants.VIDEO_WAY_BILL_UPLOAD;
        while(true){
            try {
                //视频导航上传到腾讯云
                String videoNavRedisListKey = com.kyexpress.vms.adapter.provider.constant.Constants.VIDEO_NAV_UPLOAD_REDIS_KEY;
                Long videoNavRedisListSize = stringRedisTemplate.opsForList().size(videoNavRedisListKey);
                if(videoNavRedisListSize>0){
                    String videoNavString = stringRedisTemplate.opsForList().rightPop(videoNavRedisListKey);
                    logger.info("视频导航上传videoNavString:{}",videoNavString);
                    VideoNavBO videoNavBO  = JSONObject.parseObject(videoNavString, VideoNavBO.class);
                    videoNavExecutor.execute(new Runnable() {
                        @Override
                        public void run() {
                            try {
                                videoNavUploadService.uploadVideo(videoNavBO);
                            } catch (Exception e) {
                                logger.error("COSClient#uploadVideo#exception",e);
                            }
                        }
                    });
                }

//                logger.info("CosService#commandLine#run#redisLstSize="+redisSize);
                if (stringRedisTemplate.opsForList().size(redisLstKey) < 10) {
                    sleep(3000);
                } else {
                    sleep(1000);
                }
            } catch (Exception e) {
                logger.error("CosService#commandLine#runException="+e.getMessage());
            }

        }

这里通过while死循环的方式,去消费List中的数据,但是下面为什么加了一个睡眠呢 sleep(3000);
而且最长还睡了三秒,后来分析这是怕对cpu的资源造成消耗。值得注意的是,这里用到消费的命令是,stringRedisTemplate.opsForList().rightPop(videoNavRedisListKey)。

针对以上的写法,我稍微做了些修改,虽然不是很完美,但是可能对cpu的消耗减少很多:

ScheduledExecutorService scheduler =  Executors.newScheduledThreadPool(1);
        //视频导航上传到腾讯云
        String videoNavRedisListKey = com.kyexpress.vms.adapter.provider.constant.Constants.VIDEO_NAV_UPLOAD_REDIS_KEY;
        scheduler.scheduleWithFixedDelay(() -> {
            Long videoNavSize = stringRedisTemplate.opsForList().size(videoNavRedisListKey);
            if(videoNavSize > 0) {
                String videoNavString = stringRedisTemplate.opsForList().rightPop(videoNavRedisListKey, 3, TimeUnit.SECONDS);
                logger.info("视频导航上传videoNavString:{}", videoNavString);
                VideoNavBO videoNavBO = JSONObject.parseObject(videoNavString, VideoNavBO.class);
                try {
                    videoNavUploadService.uploadVideo(videoNavBO);
                } catch (Exception e) {
                    logger.error("COSClient#uploadVideo#exception", e);
                }
            }

        },1,1,TimeUnit.SECONDS);

这里就是利用了延时任务,以及阻塞消费的方式,无数据,挂起3秒钟。

总结

针对以上两种方式我稍微做了下总结,这两种方式,可能是我们优先想到的方式,但是仔细思考,都存在着一定的缺陷。
1.针对第一种方式,利用hash作为存储,搭配xxljob,并且融入了分布式锁。使用上依赖了很多的技术,两个字概括麻烦,本身一个很简单的功能,却做的这么麻烦,如果生产者一旦速度很快,30秒消费一次,是否会使我们的Hash造成大key,都存在一定的可能。
2.针对第二种方式,虽然去除掉了很多技术上的依赖,但是一旦消费失败,数据可能存在丢失的问题。虽然针对List结构给了我们一个消费并存储的到另一个数据结构的命令,但是实现太复杂。

如果利用上面两种方式去实现,反而不如直接利用rabbitmq。哈哈哈

接下来继续尝试redis是否有更好,更简单的命令呢

三、利用redis发布订阅模式

生产者:

redisTemplate.convertAndSend(channel, message);

消费者

public class RedisMessageListener implements MessageListener {
    @Autowired
    private RedisTemplate redisTemplate;

    @Override
    public void onMessage(Message message, byte[] pattern) {

        redisTemplate.getValueSerializer().deserialize(message.getBody());
        log.info(messageDto.getData()+","+messageDto.getContent());
    }
}

以上消费简单,快速,并且不需要介入定时器。
内部实现原理,其实就是存储了一个 以channel为key,以订阅者为链表的数据结构,虽然也会出现数据丢失,但是如果扩展起来方便。

四、利用redis stream

生产者


            String result = redisUtil.addMap(redisKey, map);

消费者


@Slf4j
@Component
public class ConsumeListener1 implements StreamListener<String, MapRecord<String, String, String>> {
 
    @Autowired
    private RedisUtil redisUtil;
 
    private static ConsumeListener1 consumeListener1;
 
    @PostConstruct
    public void init(){
        consumeListener1 = this;
        consumeListener1.redisUtil=this.redisUtil;
    }
 
    @Override
    public void onMessage(MapRecord<String, String, String> message) {
        String stream = message.getStream();
        RecordId id = message.getId();
        Map<String, String> map = message.getValue();
        log.info("[不自动ack] group:[group-a] consumerName:[{}] 接收到一个消息 stream:[{}],id:[{}],value:[{}]", stream, id, map);
        consumeListener1.redisUtil.ack(stream, "group-a", id.getValue());
        consumeListener1.redisUtil.del(stream, id.getValue());
    }

————————————————

redis stream 是redis5.0之后出来的,增加了消费者组,ack,数据持久化等概念,简直跟kafka太像了。这里就不一一介绍了

针对以上的各个方式,你觉得哪个更合理呢?

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
Redis作为消息中间件的原因是因为它可以作为缓存,并且在互联网项目中被广泛使用。在引入新的组件时,我们需要考虑其必要性。如果Redis本身的消息队列功能已经能够满足大多数需求,为什么还要引入消息队列(MQ)呢?\[1\] Redis虽然没有像MQ一样支持丰富的消息传播能力,但在大多数场景中已经足够使用了。Redis提供了list数据类型和pop、push方法,允许我们像使用队列一样操作list。另外,Redis还支持发布订阅模式,类似于其他消息队列的实现方式\[3\]。 在使用Redis作为消息中间件时,我们可以编写消费者类来监听消息,并在接收到消息时进行相应的处理。例如,可以创建一个RedisMessageListener类实现MessageListener接口,通过RedisTemplate来处理消息\[2\]。 总之,Redis作为消息中间件的选择是因为它的缓存功能和在互联网项目中的广泛应用。虽然它的消息队列功能相对简单,但在大多数场景中已经足够使用了\[1\]\[3\]。 #### 引用[.reference_title] - *1* *3* [异步消息中间件的门面组件easy-asyn(二):进阶篇(Redis消息中间件的使用)](https://blog.csdn.net/qq_28802119/article/details/100608257)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^insert_down28v1,239^v3^insert_chatgpt"}} ] [.reference_item] - *2* [如何用redis消息中间件](https://blog.csdn.net/qq_41435009/article/details/127995482)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^insert_down28v1,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值