怎样优化消息服务,提升5倍的消费速率?


前言

在生产中,消息服务往往会封装统一的对外提供的接口,来让各个微服务进行统一的调用,随着我们各个服务的新增,消息服务面临的压力越来越大,怎样优化消息消费的速度往往成为了企业急需面临解决的问题,下面我结合我们公司的实际案例,来全面的分析一下解决的方案。


一、整体架构设计

1.1 设计思路:

消息服务对外统一的消息发送的接口,将接收到待发送的消息,存入到redis队列中,然后开启10条线程去并发的消费需要待发送的消息。

二、 方案设计

2.1 控制线程的并发数:

在这里插入图片描述
我是利用从令牌算法汲取的经验,利用redis来实现。有人说为啥不直接用线程池来设计最大的线程数来控制。主要是考虑我们的分布式环境。这样不管线上多少台机器可以达到限流的目的,当然这样写死的线程数设计不利于后来随着业务的增加,线程机器的水平扩展,后期优化的目标可以根据机器的数量进行灵活的拓展。

2.2 为啥不在每台机器上单独的使用线程池?

好的,我们来分析一下线程池的特点,在我们初始化一个线程之后,线程池是没有任何线程数的,随着请求数的新增,我们会开启一个线程,两个线程…,当然一个请求结束了会将线程还回线程池,不需要在开启一个新的线程,核心线程数的大小决定了我们开启的线程数的多少,再来了请求会加入到阻塞队列中,阻塞队列满了,判断如果设计的最大线程数大于核心线程数,那么继续开启新的线程数去消费,直到达到了最大线程数,然后再来的请求就开启了拒绝策略,下面是线程池的4钟拒绝策略:
在这里插入图片描述
我的目标是建立10条消费数据的通道,每一个线程消费完了,立马去消费另一条消息,所以用线程池的方式不能满足我的需求。

2.3 利用计数器来防止"CPU空转"

在这里插入图片描述
我们在Spring启动的时候会往redis队列放入10个数字,用来控制线程的数量,相当于令牌。在消费的时候,会先去该队列中获取令牌,获取到令牌然后去消息队列中消费消息,试想如果我们没有一个计数的数据,我们会频繁的从redis中拿取令牌,然后去pop一条消息来消费,如果消息队列中没有数据,我们就重复的做这个获取令牌,还回令牌的动作,相当于CPU的空转,虽然redis的读写能力非常优秀,但是我们可以通过计数来进行优化。

2.4 利用定时线程池定时的去消费消息

在这里插入图片描述
我们这里设计的是每个10毫秒就循环一次,访问redis看时候有需要消费的数据,首先是通过计数器来判断,是否有需要消费的消息。
=== > 重点来了!!!
计数器的新增我们可以利用redis有个原子性加一的操作轻易的实现,那么这里如何通过计数器去判断需要消费的数据呢?如果我们这样写代码:

if(计数器 > 0){   <=(1)
 计数器 - 1;       <=(2)
  //消费消息
}

那么在第一步,还有第二步之间这里很容易出现并发的问题,我通过lua的方式(redis操作通过lua脚本执行,为原子性操作)进行解决:

    /**
     * @param keys
     * @param args
     * @return
     */
    public Object decrIfGreaterThanZero(List<String> keys, List<String> args) {
        String arDecrStr = "local count = redis.call('get',KEYS[1]);"
                + "if count and tonumber(count)==0 then "
                + "count=-1;"
                +"end;"
                + "if count and tonumber(count)>0 then "
                + "count=redis.call('incrby',KEYS[1],-1);"
                + "end;"
                + "return tonumber(count)";
        return jedis.eval(arDecrStr, keys, args);
    }

我先判断存的计数器的大小是否等于0,等于0的话就返回-1,大于0再减一,然后更新redis计数器的数值,为啥我需要先判断等于0,是因为如果等于0的话再去减一更新计数器的话,计数器的值就会成为负数,那么我们直接判断大于0减一更新计数器就好了,
比如:

                String arDecrStr = "if count and tonumber(count)>0 then "
                + "count=redis.call('incrby',KEYS[1],-1);"
                + "end;"
                + "return tonumber(count)";

还存在一个问题,如果只有一条消息,我们返回的结果是0,没有消息计数器为0的时候,我们返回的也是0,我们无法通过返回的数值来判断是否做了减一的操作,来决定是否需要去开启线程去消费。所以,这里先判断了等于0,置为-1。
(当然这里还存在可能小于0的情况,虽然这种情况逻辑上不会发生,但代码需要考虑到各种各样可能会发生的情况,健全的代码,我们需要做一些补偿的处理)
我们通过返回值大于等于0就可以判断是否需要去从我们的redis来获取令牌去消费数据,伪代码如下:

 if(decrIfGreaterThanZero() >= 0){
    //计数线程队列blpop一个数
    //记录当前的时间
    starttime = System.currentTimeMillis();
    弹出的数据 = redisService.blpop(5秒超时);
    if(弹出的数据 != null){
     需要消费的数据 =redisServie.rpop();
     if(需要消费的数据 != null){
        //开启异步去消费
        //最后还回令牌,catch到异常也需要还回令牌
     }
  }else{
    if(System.currentTimeMillis() > 5){
       //计数器加1
    }
  }
}

请注意我们这里的blpop()方法是阻塞式的,是为了在这里让线程等待令牌池有数据,让其他线程执行完,返回池子中。但是我们这里不能无限等待,我在这里设计了5秒的超时时间,如果没有取到值的话,我判断等待的时间是大于5秒钟的话,我们在上一步进行了计数器减一的操作了,这里我们将计数器原子性加一。
压力测试,亲测结果,可提升五倍的消费速率!!

纯手打,原创,如果喜欢的话 多多点赞,支持,感谢各位大佬!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值