任务队列 P133

通过将待执行任务的相关信息放入队列里面,并在之后对队列进行处理,可以推迟执行那些耗时对操作,这种将工作交给任务处理器来执行对做法被称为任务队列 (task queue) 。 P133

先进先出队列 P133

可以 Redis 的列表结构存储任务的相关信息,并使用 RPUSH 将待执行任务的相关信息推入列表右端,使用阻塞版本的弹出命令 BLPOP 从队列中弹出待执行任务的相关信息(因为任务处理器除了执行任务不需要执行其他工作)。 P134

发送任务

// 将任务参数推入指定任务对应的列表右端
func SendTask(conn redis.Conn, queueName string, param string) (bool, error) {
count, err := redis.Int(conn.Do("RPUSH", queueName, param))
if err != nil {
return false, nil
}
// 只有成功推入 1 个才算成功发送
return count == 1, nil
}

执行任务

// 不断从任务对应的列表中获取任务参数,并执行任务
func RunTask(conn redis.Conn, queueName string, taskHandler func(param string)) {
for ; ; {
result, err := redis.Strings(conn.Do("BLPOP", queueName, 10))
// 如果成功获取任务信息,则执行任务
if err != nil && len(result) == 2 {
taskHandler(result[1])
}
}
}

以上代码是任务队列与 Redis 交互的通用版本,使用方式简单,只需要将入参信息序列化成字符串传入即可发送一个任务,提供一个处理任务的方法回调即可执行任务。

任务优先级 P136

在此基础上可以讲原有的先进先出任务队列改为具有优先级的任务队列,即高优先级的任务需要在低优先级的任务之前执行。 BLPOP 将弹出第一个非空列表的第一个元素,所以我们只需要将所有任务队列名数组按照优先级降序排序,让任务队列名数组作为 BLPOP 的入参即可实现上述功能(当然这种如果高优先级任务的生成速率大于消费速率,那么低优先级的任务就永远不会执行)。 P136

优先执行高优先级任务

// 不断从任务对应的列表中获取任务参数,并执行任务
// queueNames 从前往后的优先级依次降低
func RunTasks(conn redis.Conn, queueNames []string, queueNameToTaskHandler map[string]func(param string)) {
// 校验是否所有任务都有对应的处理方法
for _, queueName := range queueNames {
if _, exists := queueNameToTaskHandler[queueName]; !exists {
panic(fmt.Sprintf("queueName(%v) not in queueNameToTaskHandler", queueName))
}
}
// 将所有入参放入同一个数组
length := len(queueNames)
args := make([]interface{}, length + 1)
for i := 0; i < length; i++ {
args[i] = queueNames[i]
}
args[length] = 10
for ; ; {
result, err := redis.Strings(conn.Do("BLPOP", args...))
// 如果成功获取任务信息,则执行任务
if err != nil && len(result) == 2 {
// 找到对应的处理方法并执行
taskHandler := queueNameToTaskHandler[result[0]]
taskHandler(result[1])
}
}
}
延迟任务 P136

实际业务场景中还存在某些任务需要在指定时间进行操作,例如:邮件定时发送等。此时还需要存储任务执行的时间,并将可以执行的任务放入刚刚的任务队列中。可以使用有序集合进行存储,时间戳作为分值,任务相关信息及队列名等信息的 json 串作为键。

发送延迟任务

// 存储延迟任务的相关信息,用于序列化和反序列化
type delayedTaskInfo struct {
UnixNano int64 `json:"unixNano"`
QueueName string `json:"queueName"`
Param string `json:"param"`
}
// 发送一个延迟任务
func SendDelayedTask(conn redis.Conn, queueName string, param string, executeAt time.Time) (bool, error) {
// 如果已到执行时间,则直接发送到任务队列
if executeAt.UnixNano() <= time.Now().UnixNano() {
return SendTask(conn, queueName, param)
}
// 还未到执行时间,需要放入有序集合
// 序列化相关信息
infoJson, err := json.Marshal(delayedTaskInfo{
UnixNano: time.Now().UnixNano(),
QueueName:queueName,
Param:param,
})
if err != nil {
return false, err
}
// 放入有序集合
count, err := redis.Int(conn.Do("ZADD", "delayed_tasks", infoJson, executeAt.UnixNano()))
if err != nil {
return false, err
}
// 只有成功加入 1 个才算成功
return count == 1, nil
}

拉取可执行的延迟任务,放入任务队列

// 轮询延迟任务,将可执行的任务放入任务队列
func PollDelayedTask(conn redis.Conn) {
for ; ; {
// 获取最早需要执行的任务
infoMap, err := redis.StringMap(conn.Do("ZRANGE", "delayed_tasks", 0, 0, "WITHSCORES"))
if err != nil || len(infoMap) != 1 {
// 睡 1ms 再继续
time.Sleep(time.Millisecond)
continue
}
for infoJson, unixNano := range infoMap {
// 已到时间,放入任务队列
executeAt, err := strconv.Atoi(unixNano)
if err != nil {
log.Errorf("#PollDelayedTask -> convert unixNano to int error, infoJson: %v, unixNano: %v", infoJson, unixNano)
// 做一些后续处理,例如:删除该条信息,防止耽误其他延迟任务
}
if int64(executeAt) <= time.Now().UnixNano() {
// 反序列化
info := new(delayedTaskInfo)
err := json.Unmarshal([]byte(infoJson), info)
if err != nil {
log.Errorf("#PollDelayedTask -> infoJson unmarshal error, infoJson: %v, unixNano: %v", infoJson, unixNano)
// 做一些后续处理,例如:删除该条信息,防止耽误其他延迟任务
}
// 从有序集合删除该信息,并放入任务队列
count, err := redis.Int(conn.Do("ZREM", "delayed_tasks", infoJson))
if err != nil && count == 1 {
_, _ = SendTask(conn, info.QueueName, info.Param)
}
} else {
// 未到时间,睡 1ms 再继续
time.Sleep(time.Millisecond)
}
}
}
}

有序集合不具备列表的阻塞弹出机制,所以程序需要不断循环,并尝试从队列中获取要被执行的任务,这一操作会增大网络和处理器的负载。可以通过在函数里面增加一个自适应方法 (adaptive method) ,让函数在一段时间内都没有发现可执行的任务时,自动延长休眠时间,或者根据下一个任务的执行时间来决定休眠的时长,并将休眠时长的最大值限制为 100ms ,从而确保任务可以被及时执行。 P138

消息拉取 P139

两个或多个客户端在互相发送和接收消息的时候,通常会使用以下两种方法来传递信息: P139

  • 消息推送 (push messaging) :即由发送者来确保所有接受者已经成功接收到了消息。 Redis 内置了用于进行消息推送的 PUBLISH 命令和 SUBSCRIBE 命令(05. Redis 其他命令简介 介绍了这两个命令的用法和缺陷)
  • 消息拉取 (pull messaging) :即由接受者自己去获取存储的信息
单个接受者 P140

单个接受者时,只需要将发送的信息保存至每个接收者对应的列表中即可,使用 RPUSH 可以向执行接受者发送消息,使用 LTRIM 可以移除列表中的前几个元素来获取收到的消息。 P140

多个接受者 P141

多个接受者的情况类似群组,即群组内的人发消息,其他人都可以收到。我们可以使用以下几个数据结构存储所需数据,以便实现我们的所需的功能:

  • STRING: 群组的消息自增 id

    • INCR: 实现 id 自增并获取
  • ZSET: 存储该群组中的每一条信息,分值为当前群组内的消息自增 id
    • ZRANGEBYSCORE: 获得未获取的消息
  • ZSET: 存储该群组中每个人获得的最新一条消息的 id ,所有消息均未获取时为 0
    • ZCARD: 获取群组人数
    • ZRANGE: 经过处理后,可实现哪些消息成功被哪些人接收了的功能
    • ZRANGE: 获取 id 最小数据,可实现删除被所有人获取过的消息的功能
  • ZSET: 存储一个人所有群组获得的最新一条消息的 id ,离开群组时自动删除,加入群组时初始化为 0
    • ZCARD: 获取所在的群组个数
    • ZRANGE: 经过处理后,可实现批量拉取所有群组的未获取的消息的功能

文件分发 P145

根据地理位置聚合用户数据 P146

现在拥有每个 ip 每天进行活动的时间和具体操作,现需要计算每天每个城市的人操作数量(类似于统计日活)。

原始数据十分巨大,所以需要分批读入内存进行聚合统计,而聚合后的数据相对来说很小,所以完全可以在内存中进行聚合统计,完成后再将结果写入 Redis 中,可以有效减少程序与 Redis 服务的通信次数,缩短任务时间。

日志分发及处理

现在有一台机器的本地日志需要交给多个日志处理器进行不同的分析。

这种场景类似群组,所以我们可以复用上面提到的支持多个接受者的消息拉取组件。

本地机器:

  1. 将所有日志发送至群组,最后再发送一条结束消息
  2. 等待所有日志处理器处理完(群组对应的完成标识 = 群组内的成员数 - 1)
  3. 清理本次发送的所有日志

日志处理器:

  1. 不断从群组中拉取消息,并进入相关处理,直至拉取到结束消息
  2. 对群组对应的完成标识进行 INCR ,表示当前日志处理器已完成处理

本文首发于公众号:满赋诸机(点击查看原文) 开源在 GitHub :reading-notes/redis-in-action

Redis 实战 —— 09. 实现任务队列、消息拉取和文件分发的更多相关文章

  1. 【mq读书笔记】消息拉取

    疑问:PullRequest何时添加? PullMessageService提供延迟添加与立即添加2种方式 疑问:PullRequest是在什么时候创建的呢? 1.上上图中 PullRequest p ...

  2. RocketMQ中PullConsumer的消息拉取源码分析

    在PullConsumer中,有关消息的拉取RocketMQ提供了很多API,但总的来说分为两种,同步消息拉取和异步消息拉取 同步消息拉取以同步方式拉取消息都是通过DefaultMQPullConsu ...

  3. 源码分析Kafka 消息拉取流程

    目录 1.KafkaConsumer poll 详解 2.Fetcher 类详解 本节重点讨论 Kafka 的消息拉起流程. @(本节目录) 1.KafkaConsumer poll 详解 消息拉起主 ...

  4. 【mq读书笔记】消息拉取长轮训机制(Broker端)

    RocketMQ并没有真正实现推模式,而是消费者主动想消息服务器拉取消息,推模式是循环向消息服务端发送消息拉取请求. 如果消息消费者向RocketMQ发送消息拉取时,消息未到达消费队列: 如果不启用长 ...

  5. 小程序切换账户拉取仓库文件的appid提示

    小程序切换账户拉取仓库文件,拉取后appid会提示项目不是当前appid的项目,因为切换了账户,而每个小程序账户只有一个appid,所以会冲突 去project.config.json里吧appid改 ...

  6. k8s实战之从私有仓库拉取镜像 - kubernetes

    1.实战目的 从私有docker仓库拉取镜像,部署pod.上一篇中,我们搭建了私有的镜像仓库,这一篇我们将与k8s结合实战使用私有仓库. 2.登录docker 为了完成本次实战,需要登录docker, ...

  7. rsync拉取远程文件

    mkdir -p   /doc sshpass -p ''pwd" rsync -avz -e 'ssh -o UserKnownHostsFile=/dev/null -o StrictH ...

  8. Redis 实战 —— 10. 实现内容搜索、定向广告和职位搜索

    使用 Redis 进行搜索 P153 通过改变程序搜索数据的方式,并使用 Redis 来减少绝大部分基于单词或者关键字进行的内容搜索操作的执行时间. P154 基本搜索原理 P154 倒排索引 (in ...

  9. Redis 实战 —— 11. 实现简单的社交网站

    简介 前面介绍了广告定向的实现,它是一个查询密集型 (query-intensive) 程序,所以每个发给它的请求都会引起大量计算.本文将实现一个简单的社交网站,则会尽可能地减少用户在查看页面时系统所 ...

随机推荐

  1. Python 设计模式——单例模式

    单例模式即确保类有且只有一个特定类型的对象,并提供全局访问点.因此通常用于日志记录.数据库操作.打印机后台处理程序等.这些程序在运行过程中只生成一个实例,避免对同一资源产生相互冲突的请求. 特点: 确 ...

  2. inotifywait命令如何监控文件变化?

    转载自:https://segmentfault.com/a/1190000038351925 文件监控可以配合rsync实现文件自动同步,例如监听某个目录,当文件变化时,使用rsync命令将变化的文 ...

  3. alibaba-sentinel-1.8变化

    maven最新坐标 <dependencies> <dependency> <groupId>org.springframework.boot</groupI ...

  4. 为什么线程安全的List推荐使用CopyOnWriteArrayList,而不是Vector

    注:本系列文章中用到的jdk版本均为java8 相比很多同学在刚接触Java集合的时候,线程安全的List用的一定是Vector.但是现在用到的线程安全的List一般都会用CopyOnWriteArr ...

  5. 「每日一题」面试官问你对Promise的理解?可能是需要你能手动实现各个特性

    关注「松宝写代码」,精选好文,每日一题 加入我们一起学习,day day up 作者:saucxs | songEagle 来源:原创 一.前言 2020.12.23日刚立的flag,每日一题,题目类 ...

  6. c#——ToString()的各种用法

    ToString()的各种用法 string str = ""; str = 123456.ToString("N"); //生成 12,3456.00 str ...

  7. Java获取X509证书里的指纹(SHA-1)从pxf文件里面

    直接通过流去获取pxf后缀文件的内容,指纹通过X509才能获取.String keyStorefile = "pfx文件地址";String strPassword = " ...

  8. [Machine Learning] 逻辑回归 (Logistic Regression) -分类问题-逻辑回归-正则化

    在之前的问题讨论中,研究的都是连续值,即y的输出是一个连续的值.但是在分类问题中,要预测的值是离散的值,就是预测的结果是否属于某一个类.例如:判断一封电子邮件是否是垃圾邮件:判断一次金融交易是否是欺诈 ...

  9. WebSocket入门及使用指南

    最近在一个项目中,需要使用到websocket,于是就花了一点时间来熟悉websocket并总结写篇blog. 为何使用websocket 在浏览器与服务器通信间,传统的 HTTP 请求在某些场景下并 ...

  10. SonarQube学习(三)- 项目代码扫描

    一.前言 元旦三天假,两天半都在玩86版本DNF,不得不说,这个服真的粘度太高了,但是真的很良心. 说明: 注册账号上线100w点券,一身+15红字史诗装备以及+21强化新手武器.在线泡点一分钟888 ...