惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

T
Tenable Blog
博客园_首页
Vercel News
Vercel News
WordPress大学
WordPress大学
美团技术团队
G
Google Developers Blog
大猫的无限游戏
大猫的无限游戏
小众软件
小众软件
Y
Y Combinator Blog
博客园 - 【当耐特】
量子位
酷 壳 – CoolShell
酷 壳 – CoolShell
The Cloudflare Blog
T
The Blog of Author Tim Ferriss
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
Google DeepMind News
Google DeepMind News
云风的 BLOG
云风的 BLOG
腾讯CDC
M
MIT News - Artificial intelligence
爱范儿
爱范儿
Recent Announcements
Recent Announcements
雷峰网
雷峰网
Last Week in AI
Last Week in AI
宝玉的分享
宝玉的分享
The Register - Security
The Register - Security
Jina AI
Jina AI
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
Hugging Face - Blog
Hugging Face - Blog
P
Privacy & Cybersecurity Law Blog
Recorded Future
Recorded Future
Help Net Security
Help Net Security
N
News and Events Feed by Topic
博客园 - Franky
P
Proofpoint News Feed
L
LINUX DO - 热门话题
S
SegmentFault 最新的问题
The GitHub Blog
The GitHub Blog
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
月光博客
月光博客
D
Docker
Google DeepMind News
Google DeepMind News
有赞技术团队
有赞技术团队
IT之家
IT之家
Security Latest
Security Latest
L
LangChain Blog
V
V2EX
阮一峰的网络日志
阮一峰的网络日志
J
Java Code Geeks

文章列表 - 她和她的猫

使用小鹤双拼一个月 折腾 Hugo PaperMod 主题 去长鹿旅游休博园 1Password 自动化登录堡垒机 深入 Hyperf:Inject 注解是如何工作的? 译|理解容器镜像层 使用 Rust 实现高性能预写日志(Write Ahead Log) 译|使用 Linux 命名空间隔离系统 深入 Hyperf:HTTP 服务是如何处理请求的? Linux 文件权限学习笔记 译|了解 Bitcask:基于日志结构的 KV 存储引擎 还原 SM2 压缩公钥的几种方法 深入 Hyperf:HTTP 服务启动时发生了什么? 一次 Hyperf 注解失效问题分析 将博客迁移到又拍云 译|PHP 7 新的 Hashtable 实现 排查 ES 查询问题:深入了解 json_encode() 函数 浅析 Redlock 分布式锁实现原理 Yar 源码阅读笔记:RPC 服务端 Yar 源码阅读笔记:客户端的并行调用 Yar 源码阅读笔记:客户端的同步调用 Yar 源码阅读笔记:数据传输模块 Yar 源码阅读笔记:消息编码模块 Yar 源码阅读笔记:RPC 通信协议 Yar 源码阅读笔记:开篇 PHP 8 新特性介绍 基于 OpenSSL 实现国密 SM4 加解密 PHP 源码阅读笔记:编译与调试 PHP 什么是惊群问题 大端模式和小端模式 基于 GitHub Actions 定时推送网址到百度站长平台 聊聊五种 I/O 模型 Workerman 源码分析:文件上传 浅入浅出 HTTP 协议 PHP 网络编程:构建 MySQL 蜜罐获取攻击者微信 ID 使用 Workerman 接入 Bilibili 直播弹幕协议 使用 GitHub Actions 自动部署 Hexo 什么是二进制安全 【转载】PHP 程序员进阶之路 优化 Workerman 检查主进程是否存活的逻辑 基于 Redis 实现分布式锁 《PHP 实现 Base64 编码/解码》笔记 PHP 多进程下载必应壁纸 【转载】PHP 实现 Base64 编码/解码 使用 MySQL FIELD() 函数自定义排序规则 Laravel 的 Facades 实现原理 C语言单链表实现约瑟夫环 数据结构学习笔记:顺序栈和链栈(C语言) 使用 QueryList + Redis 下载壁纸 在 Laravel 登录/注册中使用 mews/captcha 扩展包 使用 mews/captcha 扩展包为 Laravel 应用添加图片验证码 给 YOURLS 短网址系统编写插件《批量生成短网址》 给 YOURLS 短网址系统编写插件《Hello World!》 PHP 7.1 使用 json_encode 函数造成浮点类型数据出现精度问题 Laravel 授权策略(Policy)的基本使用 Ubuntu 系统下 WiFi 频繁掉线解决方法 Laravel 应用部署到 Nginx 服务器上的第一个坑 Ubuntu 16.04 LST 安装 Redis 和 Composer PHP 采集逐浪小说章节列表 Nginx 环境 SSL 强制 HTTP 301 跳转到 HTTPS 【工作&生活】第一周 ServiceStack.Redis 与多线程 Count the number of Duplicates C# Base64 加密及解密 ASP.NET MVC 使用动态类型传递数据 Django 使用 order_by() 对数据进行排序操作 ASP.NET + jQuery + AJAX 实现用户登录 ASP.NET 从客户端中检测到有潜在危险的 Request.Form 值 ASP.NET 使用 FormData 实现 AJAX 上传图片 Python 使用 BeautifulSoup 抓取网页 贪心算法之字符串的完美度 一言不和就造轮子之 CookieHelper ASP.NET 上传图片 DBHelper 类 C# 捕获屏幕源码 用 JavaScript 做一些有趣的事情 第一篇文章 好久不见。 如何防止非法调用 PHP 文件 清明小记。 PHP 生成随机字符串 Z-Blog PHP 程序做 301 跳转 PHP 处理数组常用的几个函数 Sublime Text 3 快捷键总结 PHP 使用 file_get_contents() 函数实现采集网页 使用 C# 编写学生信息管理系统 终于到了。 zblog报错: magic_quotes_gpc 和 safe_mode 世事无常。 如何使用 PHP 上传文件 安卓手机编写和运行 PHP 程序的软件 使用 AJAX 刷新、验证 PHP 图片验证码 PHP 生成图片验证码 PHP 之 session 的使用方法 PHP 之 cookie 的使用方法 PHP 实现分页原理详解 Thinkphp 框架中系统常量在 Js 文件中不解析的解决方法
基于 Redis 实现延迟队列
她和她的猫 · 2020-01-18 · via 文章列表 - 她和她的猫

前言

之前在一些博客上看到过讲如何实现延迟队列,但是平时没用上也没有动手实现过。

在上次面试的时候,面试官也问过我有没有用过延迟队列,最后凭借着记忆讲了下如何用 Redis 的有序集合实现延迟队列,以及有什么缺点。

纸上得来终觉浅,绝知此事要躬行。

这句诗就是本文的主要目的。

原理

主要用到了 Redis 的三个命令,ZADDZREMZRANGEBYSCORE

ZADD key_name score value

ZRANGEBYSCORE key min max [WITHSCORES] [LIMIT offset count]

ZREM key value [value ...]

ZADD 用于向有序集合中添加一条或多条数据,score 作为数据处理时间,value 存放数据。

ZRANGEBYSCORE 用于获取有序集合中指定时间范围内的数据,按照时间戳升序排列。

ZREM 用于将一条或多条数据从有序集合中移除。

举个例子:

# 添加测试数据
127.0.0.1:6379> ZADD test 1 a 2 b 3 c 4 d 5 e
(integer) 5

# 获取 2 到 4 之间的数据
127.0.0.1:6379> ZRANGEBYSCORE test 2 4
1) "b"
2) "c"
3) "d"

# 获取一条 2 到 4 之间的数据
127.0.0.1:6379> ZRANGEBYSCORE test 2 4 LIMIT 0 1
1) "b"

# 移除数据 b
127.0.0.1:6379> ZREM test b
(integer) 1

127.0.0.1:6379> ZRANGEBYSCORE test 2 4 LIMIT 0 1
1) "c"

在 Redis 2.4 版本前,ZADD、ZREM 每次只能添加/删除一条数据。

实现

用的是 predis/predis 扩展包操作 Redis。

use Predis\Client;

class RedisDelayQueue
{
    /**
     * @var Client
     */
    private $client = null;

    /**
     * RedisDelayQueue constructor.
     * @param Client $client
     */
    public function __construct(Client $client = null)
    {
        $this->client = $client ?: $this->createClient();
    }

    private function createClient()
    {
        $params = [
            'scheme' => 'tcp',
            'host'   => '127.0.0.1',
            'port'   => 6379,
        ];

        return new Client($params);
    }

    /**
     * 添加数据
     * 
     * @param $queueName
     * @param $data
     * @return int
     */
    public function push($queueName, $data)
    {
        $members = [];
        $data = isset($data['content']) ? [$data] : $data;
        
        foreach($data as $datum) {
            $members[$datum['content']] = $datum['time'];
        }

        return $this->client->zadd($queueName, $members);
    }

    /**
     * 消费数据
     * @param $queueName
     * @param \Closure $callback
     * @param \Closure $catch
     */
    public function consume($queueName, \Closure $callback, \Closure $catch = null)
    {
        $options = [
            'limit' => [
                'offset' => 0,
                'count' => 1,   // 只取一条数据
            ]
        ];

        while (true) {
            // 从集合中获取一条小于当前时间的数据
            $result = $this->client->zrangebyscore($queueName, 0, time(), $options);
            // 没获取到数据就休息一秒
            if (empty($result)) {
                sleep(1);
                continue;
            }

            // 将数据从集合中移除
            if (!$this->client->zrem($queueName, $result[0])) {
                continue;
            }

            try {
                $callback($result[0]);
            } catch(\Exception $e) {
                if (!$catch instanceof \Closure) {
                    echo 'ERROR:' . $e->getMessage().PHP_EOL;
                    continue;
                }

                $catch($e, $result);
            }
        }
    }
}

因为存在多个进程处理同一个队列的情况,就会出现一条数据被多个进程获取到,所以只有当 ZREM 命令移除数据成功时,才算是真正的获取到了数据。

使用

添加数据

require_once './vendor/autoload.php';
require_once './RedisDelayQueue.php';

$queue = new RedisDelayQueue();

// 添加一条数据
$data = [
    'content' => '投递数据的时间是:'.date('Y-m-d H:i:s'),
    'time' => time() + 10,  // 十秒后处理
];

// 添加多条数据
// $data = [
//     [
//         'content' => '投递数据的时间是:'.date('Y-m-d H:i:s'),
//         'time' => time() + 10,  // 十秒后处理
//     ],
// ];

echo $queue->push('test', $data);

消费数据

require_once './vendor/autoload.php';
require_once './RedisDelayQueue.php';

$queue = new RedisDelayQueue();

$queue->consume('test', function ($value) {
    printf("[%s] %s \r\n", date('Y-m-d H:i:s'), $value);
});

// 处理异常情况
$queue->consume('test', function ($value) {
    printf("[%s] %s \r\n", date('Y-m-d H:i:s'), $value);
    // 假装出现异常
    throw new \Exception('数据库连接超时');
}, function ($e, $value) {
    printf("发生异常:%s \r\n", $e->getMessage());
    printf("获取到的数据:%s \r\n", $value);
});

运行结果

php index.php

[2020-01-19 02:29:47] 投递数据的时间是:2020-01-19 02:29:37
[2020-01-19 02:30:19] 投递数据的时间是:2020-01-19 02:30:09

使用 Lua 脚本进行优化

上面提到了,一条数据可能会被多个进程获取到,然后通过 ZREM 移除数据判断是否抢占数据成功,执行 ZRANGEBYSCOREZREM 命令会发出两次请求,那些没有抢到数据的进程就相当于这次数据白获取了。

使用 Lua 脚本进行优化,只发出一次请求,在服务端进行数据抢占操作。

Redis 中执行 Lua 脚本的命令是 EVAL

EVAL script numkeys key [key ...] arg [arg ...]
  • script:Lua 脚本的内容。
  • numkeys:命令的个数(用于区分 key 和 arg)。
  • key:从 EVAL 的第三个参数开始算起,表示在脚本中所用到的那些 Redis 键(key),在 Lua 中通过全局变量 KEYS 数组访问。
  • arg:附加参数,全局变量 ARGV 数组访问。

Redis 版本 >= 2.6.0 才能使用 Lua 脚本。

修改如下:

    public function consume($queueName, \Closure $callback, \Closure $catch = null)
    {
        $script = $this->getLuaScript();

        while (true) {
            // 使用 eval 执行 Lua 脚本
            $result = $this->client->eval($script, 2, $queueName, time());
            // 没获取到数据就休息一秒
            if (empty($result)) {
                sleep(1);
                continue;
            }

            try {
                $callback($result);
            } catch(\Exception $e) {
                if (!$catch instanceof \Closure) {
                    echo 'ERROR:' . $e->getMessage().PHP_EOL;
                    continue;
                }

                $catch($e, $result);
            }
        }
    }

    /**
     * 获取 lua 脚本
     * @return string
     */
    public function getLuaScript()
    {
        return <<<LUA
            local result = redis.call('zrangebyscore', KEYS[1], 0, KEYS[2], 'limit', 0, 1);
            if (table.getn(result) == 0)
            then
                return false;
            end
            if (redis.call('zrem', KEYS[1], result[1]) > 0)
            then
                return result[1];
            else
                return false;
            end
LUA;
    }

最后

需要注意是:基于 Redis 的延迟队列不能 100% 保证可靠性

如果在使用 ZREM 命令将数据从集合中移除后,处理数据时发生了异常,那么这条数据就丢失了,也就是缺少了 ACK 机制,所以在使用时候需要进行权衡,或者使用 RabbitMQ 这些专业的消息中间件。

写到这已经3点50了…溜了溜了


2020.01.20 01:28 更新

增加处理异常的回调参数,可自定义处理抛出的异常。