[记录点滴]Redis实现简单消息队列

0x00 摘要

本文提出了一种用Redis实现简单消息队列的方案,适合在资源不足的条件下临时使用。

0x01 缘由

一个兄弟创业,资源严重不足,但是还希望搭建一个消息队列,于是就咨询我。我恰好有些相关经验,就和他分享。他的需求如下:

  • 主要目的是为了解耦,消息先存入队列,然后再从队列转存到数据库。
  • 对消息可靠性要求不高,使用场景是消息容忍丢失,或者说对性能的渴求大于可靠性。
  • 不考虑分组消费,重复消费和广播。
  • 不考虑消息序列顺序。
  • 系统现在已经有Redis做缓存。
  • 人力和财力资源不足以再使用专业的Queue。

在这种情况下,我建议他在Redis上构建消息队列,暂时渡过难关。

0x02 背景概念

2.1 Redis是否适合做消息队列

首先说结论:Redis肯定是不适合做消息队列的,因为这个本身就不是Redis设计的初衷。

但是如果确实资源受限,为了降低系统的维护成本和实现复杂度,还是可以考虑使用Redis的。

2.1.1 Redis的问题

因为Redis就不是为消息队列设计的,所以它没有考虑一些消息队列的基本问题:

  • 队列丢东西怎么办?
  • 如果队列暂时无法被插入新数据,有没有办法把新数据暂时存储在临时存储上等队列恢复时候再重新插入?
  • 消费者读取数据时候是否需要一个“commit”的语义?是否需要确认已经读取处理完毕?
  • 队列长度是否有限制?如果达到最大长度怎么办?......

如果按照是否容忍错误来区分,可以分为两种队列,但是这两种都不适合Redis。

2.1.2 不容忍错误

这种队列的要求是:不允许丢失消息,要保证一致性。比如下单操作。

在这个需求下,用Redis是不实际的,因为你需要考虑如何在Redis基础上做一次性和异步幂等,保证exactly once。那样就应该使用常见的MQ,比如RabbitMQ, Kafka....

2.1.3 容忍错误

比如日志收集。这种允许一定程度的数据丢失,这种其实也不适合Redis,而且现成的方案有很多,比如fluentd,logstash……

2.2 Redis做消息队列的方案

一般来说有四种方式

  • 基于List的 LPUSH+BRPOP 的实现
  • 基于PUB/SUB,订阅/发布模式
  • 基于Sorted-Set的实现
  • 基于Stream类型的实现Redis在本实例中的应用

或者可以考虑基于Redis作者写的disque来做开发?

2.4 本文采取的方案

本文采用Redis的List作为队列可以用来在不同程序之间交换消息。生成者使用LPUSH或者RPUSH将一个消息放入队列。消费者使用RPOP或者LPOP命令取出队列中等待时间最长的消息。List支持多个生产者和消费者并发进出消息,每个消费者拿到都是不同的列表元素。

但是这样有两个问题:

  • 队列为空时候,LPOP或者RPOP会一直轮训,这样极大消耗资源。
  • 如果客户端在消费一个消息时候崩溃,则未处理完的消息也就因此丢失。

因此需要

  • 使用RPOPLPUSH命令(或者它的阻塞版本BRPOPLPUSH)。
  • 或者引入阻塞读blpop和brpop(b代表blocking),阻塞读 在队列没有数据的时候进入休眠状态。

2.4.1 RPOPLPUSH

RPOPLPUSH好处在于:它不仅返回一个消息,同时还将这个消息添加到另一个备份列表当中。如果一切正常的话,当一个客户端完成某个消息的处理之后,可以用LREM命令将这个消息从备份列表删除。

最后,还可以添加一个客户端专门用于监视备份表,它自动地将超过一定处理时限的消息重新放入队列中去(负责处理该消息的客户端可能已经崩溃),这样就不会丢失任何消息了。

需要注意的问题 :RPOPLPUSH重新入队,即把备份列表右侧元素(表尾)重新入队,可能会出现消息被重复消费的情况。因此消费操作要实现幂等性,即保证重复消费结果一致.

2.4.2 BLPOP和BRPOP

好处在于 :阻塞读在队列没有数据的时候进入休眠状态,一旦数据到来则立刻醒过来,消息延迟几乎为零。

需要注意的问题 :空闲连接的问题。如果线程一直阻塞在那里,Redis客户端的连接就成了闲置连接,闲置过久,服务器一般会主动断开连接,减少闲置资源占用,这个时候blpop和brpop或抛出异常,所以在编写客户端消费者的时候要小心,如果捕获到异常,还有重试。

0x03 生产者LUA

他的数据是由LUA产生的,由Openresty运行。

具体代码摘要如下:

local REDIS = require "redis_iresty"

local REDIS_STORE = REDIS:new(CONF)

REDIS_STORE:lpush(LOG_LIST, log)

0x04 消费者 JAVA

因为生产者是 LPUSH,所以消费者使用 RPOPLPUSH。

4.1 数据变量

因为RPOPLPUSH不仅返回一个消息,同时还将这个消息添加到另一个备份列表当中,所以mKey是消息列表,mKeyRollback是备份列表。从Redis读出消息后临时存储在mActionList。

protected List<String> mActionList = new CopyOnWriteArrayList<String>();

@Value("${key.list}")
private String mKey; @Value("${key.rollback.list}")
private String mKeyRollback;

4.2 消费函数

consume是消费函数。当出现异常时候,会从备份列表中把消息再写回到消息队列。

public boolean consume() {
rollbackLastLaunch(); //上次同步失败的,这次先弄回去 while(true) { try {
if (schedulejob) {
timerecord = System.currentTimeMillis();
schedulejob = false;
} // 从消息队列取出消息,同时Redis操作会自动把取出的消息放入备份队列。
String action = mRedisStore.listRightPopAndLeftPush(mKey, mKeyRollback, mWaitTimeLimit, TimeUnit.SECONDS);
if(action != null) {
mActionList.add(action);
} currentTimeStamp = System.currentTimeMillis();
if (mActionList.size() >= mBatchSize ||
(currentTimeStamp - timerecord >= mTimeElapsedLimit && mActionList.size() > 0)) {
schedulejob = true;
boolean res = sync2MySql();
if (res == true) {
clearRollback(); //清除备份列表
} else {
rollbackLastLaunch(); //rollback();
}
mActionList.clear();
}
} catch (Exception e) {
//发生了网络异常,需要把processing中的id再放回到waiting queue中
//如果redis, mysql异常,都会在这里被catch
rollbackLastLaunch();
} finally {
}
}
}
}

具体Redis操作是StringRedisTemplate.opsForList().rightPopAndLeftPush函数。

public String listRightPopAndLeftPush(String sourceKey, String destinationKey, long timeout, TimeUnit unit) {
getTemplate().setDefaultSerializer(new StringRedisSerializer());
return getTemplate().opsForList().rightPopAndLeftPush(sourceKey, destinationKey, timeout, unit);
}

使用这个是因为它支持配置超时时间。

V rightPopAndLeftPush(K var1, K var2, long var3, TimeUnit var5);

4.3 删除备份消息

clearRollback函数是当消息被成功处理之后,从备份队列中删除备份消息。

protected void clearRollback() {
Long count = mRedisStore.getListSize(mKeyRollback);
while(count > 0 ) {
mRedisStore.listRightPop(mKeyRollback);
count--;
}
}

4.4 处理异常

当出现问题时候,会调用rollbackLastLaunch函数,从备份列表中把消息再写回到消息队列。

因为我们需要在一个Redis操作中执行lpop和rpush两个操作,必须把这两个操作构建成一个原子序列,所以这里涉及到了Lua脚本的使用。通过内嵌对 Lua 环境的支持, Redis 解决了长久以来不能高效地处理 CAS (check-and-set)命令的缺点, 并且可以通过组合使用多个命令, 轻松实现以前很难实现或者不能高效实现的模式。

void rollbackLastLaunch() {
try {
Long count = mRedisStore.getListSize(mKeyRollback);
Long dbsize = 0l;
while(count > 0 ) {
List<String> keys = new ArrayList<String>();
keys.add(mKeyRollback);
keys.add(mKey); DefaultRedisScript<Long> script = new DefaultRedisScript<Long>();
script.setScriptText("local action = redis.call('lpop', KEYS[1]); local result = redis.call('rpush', KEYS[2], action); return result;");
script.setResultType(Long.class); dbsize += mRedisStore.executeScript(script, keys, null);
count--;
}
} catch (Exception e) {
}
}

0x05 参考

你对Redis的使用靠谱吗?

Lua 脚本

Redis实现消息队列的方案

Redis 怎么做消息队列?

Redis 阻塞、安全队列 BLPOP / BRPOP / LPUSH

[记录点滴]Redis实现简单消息队列的更多相关文章

  1. Redis实现简单消息队列

    http://www.jianshu.com/p/9c04890615ba 任务异步化 打开浏览器,输入地址,按下回车,打开了页面.于是一个HTTP请求(request)就由客户端发送到服务器,服务器 ...

  2. Redis+php-resque实现消息队列

      服务器硬件配置 Dell PowerEdge R310英特尔单路机架式服务器 Intel Xeon Processor X3430 2.4GHz, 8MB Cache 8GB内存(2 x 4GB) ...

  3. Delayer 基于 Redis 的延迟消息队列中间件

    Delayer 基于 Redis 的延迟消息队列中间件,采用 Golang 开发,支持 PHP.Golang 等多种语言客户端. 参考 有赞延迟队列设计 中的部分设计,优化后实现. 项目链接:http ...

  4. Redis 学习笔记(六)Redis 如何实现消息队列

    一.消息队列 消息队列(Messeage Queue,MQ)是在分布式系统架构中常用的一种中间件技术,从字面表述看,是一个存储消息的队列,所以它一般用于给 MQ 中间的两个组件提供通信服务. 1.1 ...

  5. 如何使用NODEJS+REDIS开发一个消息队列

    作者: RobanLee 原创文章,转载请注明: 萝卜李 http://www.robanlee.com MQ全称为Message Queue, 消息队列(MQ)是一种应用程序对应用程序的通信方法.应 ...

  6. simple简单消息队列

    一:介绍 1.优缺点 简单,但是耦合性较高. 这种模式是生产者与消费者一一对应,就是一个产生者,有一个消费者来消费. 如果,多个消费者想消费一个队列中的消息就不适合了.这种情况在后面会接着介绍. 2. ...

  7. Spring Cloud(7):事件驱动(Stream)分布式缓存(Redis)及消息队列(Kafka)

    分布式缓存(Redis)及消息队列(Kafka) 设想一种情况,服务A频繁的调用服务B的数据,但是服务B的数据更新的并不频繁. 实际上,这种情况并不少见,大多数情况,用户的操作更多的是查询.如果我们缓 ...

  8. php和redis怎么实现消息队列

    把瞬间服务器的请求处理换成异步处理,缓解服务器的压力,实现数据顺序排列获取.本文主要和大家分享php和redis如何实现消息队列,希望能帮助到大家. redis实现消息队列步骤如下: 1).redis ...

  9. 用 Redis 实现 PHP 的简单消息队列

    参考:PHP高级编程之消息队列 消息队列就是在消息的传输过程中,可以保存消息的容器. 常见用途: 存储转发:异步处理耗时的任务 分布式事务:多个消费者消费同一个消息队列 应对高并发:通过消息队列保存任 ...

  10. redis(五)---- 简单消息队列

    消息队列一个消息的链表,是一个异步处理的数据处理引擎.不仅能够提高系统的负荷,还能够改善因网络阻塞导致的数据缺失.一般用于邮件发送.手机短信发送,数据表单提交.图片生成.视频转换.日志储存等. red ...

随机推荐

  1. Apache 门户项目组介绍

    本文将快速浏览 Apache 门户项目组的所有项目,并着重介绍门户项目组中的核心项目-Jetspeed-2. 0 评论: 廖 健, 首席实施顾问 2006 年 11 月 02 日 内容 引言 JEE作 ...

  2. 什么是静态(static)?什么是静态方法,静态变量,静态块和静态类?

    本文由 ImportNew - 唐小娟 翻译自 Journaldev.如需转载本文,请先参见文章末尾处的转载要求. static是Java中的一个关键字,我们不能声明普通外层类或者包为静态的.stat ...

  3. Java 并发编程实战学习笔记——串行任务转并行任务

    package net.jcip.examples; import java.util.Collection; import java.util.List; import java.util.Queu ...

  4. 工具篇-FinalShell

    转载:https://www.toutiao.com/i6694563184428188171?wid=1625538368131 FinalShell是一款免费的国产的集SSH工具.服务器管理.远程 ...

  5. SFE人才需要具备哪些能力

    SFE(销售队伍效力)人才在企业中扮演着至关重要的角色,他们需要具备一系列的能力来确保销售队伍的高效运作和业绩提升.关于SFE的角色和能力,可以从业务理解.数据洞察.向上管理以及效率提升等几个方面来通 ...

  6. 怎么实时更新echarts图标数据?

    function getData(){ var request . nem XPHLHttpRequest () ; request . open("get",'http://lo ...

  7. shp转featureclass

    public void ConvertShapefileToFeatureClass() { // Create a name object for the source (shapefile) wo ...

  8. LSTM学习三维轨迹的Python实现

    一.引言 长短期记忆网络(LSTM)是一种强大的递归神经网络(RNN),广泛应用于时间序列预测.自然语言处理等任务.在处理具有时间序列特征的数据时,LSTM通过引入记忆单元和门控机制,能够更有效地捕捉 ...

  9. docker安装指定版本的gitlab并配置ssl证书

    安装gitlab,指定14.5.2版本,为便于与下篇文章:gitlab恢复做准备.如果不需要恢复,或不需要配置ssl证书,请自行精简以下操作. 1. 开启gitlab sh gitlab.sh doc ...

  10. 使用 VS Code 徒手构建 PDF 文件

    使用 VS Code 徒手构建 PDF 文件 PDF 文件是广泛应用的页面描述文件格式,从本质上讲,文件内部的结构混合使用了文本格式描述和二进制格式描述,对于简单的文件,比如说我们今天要创建的第一个 ...