## 简介
顾名思义,延迟队列就是进入该队列的消息会被延迟消费的队列。而一般的队列,消息一旦入队了之后就会被消费者马上消费。
## 和定时任务区别
>延时任务有别于定时任务,定时任务往往是固定周期的,有明确的触发时间。
>[warning] 而延时任务一般没有固定的开始时间,它常常是由一个事件触发的,而在这个事件触发之后的一段时间内触发另一个事件。
> 任务事件生成时并不想让消费者立即拿到,而是延迟一定时间后才接收到该事件进行消费。
## 业务场景
- 订单超时,用户下单后进入支付页面(通常会有超时限制)超过15分钟没有进行操作,那么这个订单就需要作废处理。
- 如何定期检查处于退款状态的订单是否已经退款成功?
- 注册后到现在已经一周的用户,如何发短信撩动。
- 交易信息双重效验防止因系统级/应用级/用户级等各种异常情况发生后导致的全部/部分丢失的订单信息。
- 实现重复通知,默认失败连续通知10次(通知间隔为`n*2+1/min`),直到消费方正确响应,超出推送上限次数后标记为异常状态,可进行恢复!
## 使用场景
> 延迟队列多用于需要延迟工作的场景。
最常见的是以下两种场景:
### 1、延迟消费
1. 用户生成订单之后,需要过一段时间校验订单的支付状态,如果订单仍未支付则需要及时地关闭订单。
2. 用户注册成功之后,需要过一段时间比如一周后校验用户的使用情况,如果发现用户活跃度较低,则发送邮件或者短信来提醒用户使用。
### 2、延迟重试
比如消费者从队列里消费消息时失败了,但是想要延迟一段时间后自动重试。
>[warning] 如果不使用延迟队列,那么我们只能通过一个轮询扫描程序去完成。
### 扫表存在的问题是
- 扫表与数据库长时间连接,在数量量大的情况容易出现连接异常中断,需要更多的异常处理,对程序健壮性要求高
- 在数据量大的情况下延时较高,规定内处理不完,影响业务,虽然可以启动多个进程来处理,这样会带来额外的维护成本,不能从根本上解决。
- 每个业务都要维护一个自己的扫表逻辑。 当业务越来越多时,发现扫表部分的逻辑会重复开发,但是非常类似
## 缓存队列设计
![](https://img.kancloud.cn/9e/bd/9ebdf012c01e3f385b8cac8c9a218cfa_1392x742.png)
## 场景设计
实际的生产场景是笔者负责的某个系统需要对接一个外部的资金方,每一笔资金下单后需要延时30分钟推送对应的附件。
这里简化为一个订单信息数据延迟处理的场景,就是每一笔下单记录一条订单消息(暂时叫做`OrderMessage`),订单消息需要延迟5到15秒后进行异步处理。
![](https://img.kancloud.cn/08/68/08683eb976b047dac0d1a0b72f7ad4d8_1021x173.png)
## 延时队列的实现
选用了基于`Redis`的有序集合`Sorted Set`和`Crontab`短轮询进行实现。
### 具体方案是:
1. 订单创建的时候,订单ID和当前时间戳分别作为`Sorted Set`的`member`和`score`添加到订单队列`Sorted Set`中。
2. 订单创建的时候,订单ID和推送内容`JSON`字符串分别作为`field`和`value`添加到订单队列内容`Hash`中。
3. 第1步和第2步操作的时候用`Lua`脚本保证原子性。
4. 使用一个异步线程通过`Sorted Set`的命令`ZREVRANGEBYSCORE`弹出指定数量的`订单ID`对应的订单队列内容`Hash`中的订单推送内容数据进行处理。
### 对于第4点处理有两种方案:
> 处理方案一
弹出订单内容数据的同时进行数据删除,也就是`ZREVRANGEBYSCORE`、`ZREM`和`HDEL`命令要在同一个`Lua`脚本中执行,这样的话`Lua`脚本的编写难度大,并且由于弹出数据已经在`Redis`中删除,如果数据处理失败则可能需要从数据库重新查询补偿。
> 处理方案二
弹出订单内容数据之后,在数据处理完成的时候再主动删除订单队列`Sorted Set`和订单队列内容`Hash`中对应的数据,这样的话需要控制并发,有重复执行的可能性。
>[warning] 选用了方案一,也就是从`Sorted Set`弹出订单ID并且从Hash中获取完推送数据之后马上删除这两个集合中对应的数据。
方案的流程图大概是这样:
![](https://img.kancloud.cn/c9/0a/c90afd71fb917ba12b8878138cd578d4_1094x565.png)
## 相关Redis命令
### Sorted Set相关命令
>[success] `ZADD`命令 - 将一个或多个成员元素及其分数值加入到有序集当中。
```
ZADD KEY SCORE1 VALUE1.. SCOREN VALUEN
```
>[success] `ZREVRANGEBYSCORE`命令 - 返回有序集中指定分数区间内的所有的成员。有序集成员按分数值递减(从大到小)的次序排列。
```
ZREVRANGEBYSCORE key max min [WITHSCORES] [LIMIT offset count]
```
- max:分数区间 - 最大分数。
- min:分数区间 - 最小分数。
- WITHSCORES:可选参数,是否返回分数值,指定则会返回得分值。
- LIMIT:可选参数,offset和count原理和`MySQL`的`LIMIT offset,size`一致,如果不指定此参数则返回整个集合的数据。
>[success] `ZREM`命令 - 用于移除有序集中的一个或多个成员,不存在的成员将被忽略。
```
ZREM key member [member ...]
```
### Hash相关命令
>[success] `HMSET`命令 - 同时将多个field-value(字段-值)对设置到哈希表中。
```
HMSET KEY_NAME FIELD1 VALUE1 ...FIELDN VALUEN
```
>[success] `HDEL`命令 - 删除哈希表key中的一个或多个指定字段,不存在的字段将被忽略。
```
HDEL KEY_NAME FIELD1.. FIELDN
```
### Lua 语法
* 加载`Lua`脚本并且返回脚本的`SHA-1`字符串:`SCRIPT LOAD script`。
* 执行已经加载的`Lua`脚本:`EVALSHA sha1 numkeys key [key ...] arg [arg ...]`。
* `unpack`函数可以把`table`类型的参数转化为可变参数,不过需要注意的是`unpack`函数必须使用在非变量定义的函数调用的最后一个参数,否则会失效,详细见`Stackoverflow`的提问[table.unpack() only returns the first element](https://stackoverflow.com/questions/32439689/table-unpack-only-returns-the-first-element)。
>[warning] 如果不熟悉Lua语言,建议系统学习一下,因为想用好Redis,一定离不开Lua。
## Lua 脚本
### 入队` enqueue.lua`
```lua
local zset_key = KEYS[1]
local hash_key = KEYS[2]
local zset_value = ARGV[1]
local zset_score = ARGV[2]
local hash_field = ARGV[3]
local hash_value = ARGV[4]
redis.call('ZADD', zset_key, zset_score, zset_value)
redis.call('HSET', hash_key, hash_field, hash_value)
return nil
```
> 将任务的执行时间作为score,要执行的任务数据作为value,存放在zset中
### 出队 `dequeue.lua`
```lua
local zset_key = KEYS[1]
local hash_key = KEYS[2]
local min_score = ARGV[1]
local max_score = ARGV[2]
local offset = ARGV[3]
local limit = ARGV[4]
-- TYPE命令的返回结果是{'ok':'zset'}这样子,这里利用next做一轮迭代
local status, type = next(redis.call('TYPE', zset_key))
if status ~= nil and status == 'ok' then
if type == 'zset' then
local list = redis.call('ZREVRANGEBYSCORE', zset_key, max_score, min_score, 'LIMIT', offset, limit)
if list ~= nil and #list > 0 then
-- unpack函数能把table转化为可变参数
redis.call('ZREM', zset_key, unpack(list))
local result = redis.call('HMGET', hash_key, unpack(list))
redis.call('HDEL', hash_key, unpack(list))
return result
end
end
end
return nil
```
> 如果最小的分数小于等于当前时间戳,就将该任务取出来执行,否则休眠一段时间后再查询。
>[danger] 注意:这里其实有一个性能隐患,命令`ZREVRANGEBYSCORE`的时间复杂度可以视为为O(N),N是集合的元素个数,由于这里把所有的订单信息都放进了同一个Sorted Set(ORDER_QUEUE)中,所以在一直有新增数据的时候,`dequeue`脚本的时间复杂度一直比较高,后续订单量升高之后会此处一定会成为性能瓶颈,后面会给出解决的方案
这里的出队使用`Crontab` 作为轮训去查询消费
## 业务核心代码
### 延迟队列类 RedisDelayQueue.php
```php
<?php
/**
* @desc Redis 延迟任务队列
* @author Tinywan(ShaoBo Wan)
* @date 2021/03/02 11:36
*/
declare(strict_types=1);
namespace redis;
class RedisDelayQueue
{
// 生产者 脚本sha值
const DELAY_QUEUE_PRODUCER_SCRIPT_SHA = 'DELAY:QUEUE:PRODUCER:SCRIPT:SHA';
// 消费者 脚本sha值
const DELAY_QUEUE_CONSUMER_SCRIPT_SHA = 'DELAY:QUEUE:CONSUMER:SCRIPT:SHA';
// 订单关闭
const DELAY_QUEUE_ORDER_CLOSE = 'DELAY:QUEUE:ORDER:CLOSE';
// 订单关闭详情哈希
const DELAY_QUEUE_ORDER_CLOSE_HASH = 'DELAY:QUEUE:ORDER:CLOSE:HASH';
/**
* Redis 静态实例
* @return \Redis
*/
private static function _redis()
{
$redis = \redis\BaseRedis::server();
$redis->select(3);
return $redis;
}
/**
* @desc: 延迟队列 生产者
* @param string $keys1
* @param string $keys2
* @param string $member
* @param int $score
* @param array $message
* @return mixed
*/
public static function producer(string $keys1, string $keys2, string $member, int $score, array $message)
{
$redis = self::_redis();
$scriptSha = $redis->get(self::DELAY_QUEUE_PRODUCER_SCRIPT_SHA);
if (!$scriptSha) {
$script = <<<luascript
redis.call('ZADD', KEYS[1], ARGV[1], ARGV[2])
redis.call('HSET', KEYS[2], ARGV[2], ARGV[3])
return 1
luascript;
$scriptSha = $redis->script('load', $script);
$redis->set(self::DELAY_QUEUE_PRODUCER_SCRIPT_SHA, $scriptSha);
}
$hashValue = json_encode($message, JSON_UNESCAPED_UNICODE);
return $redis->evalSha($scriptSha, [$keys1, $keys2, $score, $member, $hashValue], 2);
}
/**
* @desc: 延迟队列 消费者
* @param string $keys1
* @param string $keys2
* @param int $maxScore
* @return mixed
*/
public static function consumer(string $keys1, string $keys2, int $maxScore)
{
$redis = self::_redis();
$scriptSha = $redis->get(self::DELAY_QUEUE_CONSUMER_SCRIPT_SHA);
if (!$scriptSha) {
$script = <<<luascript
local status, type = next(redis.call('TYPE', KEYS[1]))
if status ~= nil and status == 'ok' then
if type == 'zset' then
local list = redis.call('ZREVRANGEBYSCORE', KEYS[1], ARGV[1], ARGV[2], 'LIMIT', ARGV[3], ARGV[4])
if list ~= nil and #list > 0 then
redis.call('ZREM', KEYS[1], unpack(list))
local result = redis.call('HMGET', KEYS[2], unpack(list))
redis.call('HDEL', KEYS[2], unpack(list))
return result
end
end
end
luascript;
$scriptSha = $redis->script('load', $script);
$redis->set(self::DELAY_QUEUE_CONSUMER_SCRIPT_SHA, $scriptSha);
}
return $redis->evalSha($scriptSha, [$keys1, $keys2, $maxScore, 0, 0, 10], 2);
}
}
```
> 用redis来实现可以依赖于redis自身的持久化来实现持久化,redis的集群来支持高并发和高可用。因此开发成本很小,可以做到很实时。
## 脚本命令行
#### 生产者消息
```php
private function delayQueueOrderClose()
{
$orderId = time();
$keys1 = RedisDelayQueue::DELAY_QUEUE_ORDER_CLOSE;
$keys2 = RedisDelayQueue::DELAY_QUEUE_ORDER_CLOSE_HASH;
$score = time() + 60; // 延迟60秒执行
$message = [
'event' => RedisDelayQueue::EVENT_ORDER_CLOSE,
'order_id' => $orderId,
'create_time' => time()
];
$res = RedisDelayQueue::producer($keys1, $keys2, (string) $orderId, $score, $message);
var_dump($res);
}
```
> 如果是ThinkPHP6 框架,执行该命令则可以生产消息,`php think crontab delay-queue-order-producer`
循环
```php
private function delayOrderProducer()
{
$keys1 = DelayQueue::KEY_ORDER_CLOSE;
$keys2 = DelayQueue::KEY_ORDER_CLOSE_HASH;
for ($i = 1; $i <= 10; $i++) {
$orderId = 'S' . $i;
$score = time(); // 延迟60秒执行
$message = [
'event' => DelayQueue::EVENT_ORDER_CLOSE,
'order_id' => $orderId,
'create_time' => time()
];
$res = DelayQueue::producer($keys1, $keys2, (string) $orderId, $score, $message);
var_dump($res);
}
}
```
#### 消费者消息
>1、通过Crontab 轮询执行
```php
private function delayQueueOrderConsumer()
{
$keys1 = RedisDelayQueue::DELAY_QUEUE_ORDER_CLOSE;
$keys2 = RedisDelayQueue::DELAY_QUEUE_ORDER_CLOSE_HASH;
$maxScore = time();
$queueList = RedisDelayQueue::consumer($keys1, $keys2, $maxScore);
if (false === $queueList) {
echo ' [x] Message List is Empty, Try Again ', "\n";
return;
}
var_dump($queueList);
}
```
>[warning] 说明:如果最小的分数小于等于当前时间戳,就将该任务取出来执行,否则休眠一段时间后再查询
> 2、阻塞执行
```php
private function delayQueueOrderConsumerWhile()
{
$keys1 = RedisDelayQueue::DELAY_QUEUE_ORDER_CLOSE;
$keys2 = RedisDelayQueue::DELAY_QUEUE_ORDER_CLOSE_HASH;
while (true) {
$maxScore = time();
$queueList = RedisDelayQueue::consumer($keys1, $keys2, $maxScore);
if (false === $queueList) {
echo ' [x] Message List is Empty, Try Again ', "\n";
sleep(1);
continue;
}
// 处理业务
foreach ($queueList as $queue) {
$messageArray = json_decode($queue, true);
}
}
}
```
## 数据删除为处理问题
>[danger] 方案一:弹出订单内容数据的同时进行数据删除,也就是ZREVRANGEBYSCORE、ZREM和HDEL命令要在同一个Lua脚本中执行,这样的话Lua脚本的编写难度大,并且由于弹出数据已经在Redis中删除,如果数据处理失败则可能需要从数据库重新查询补偿。
针对以上的解决方案就是:**消息进入到延迟队列后,保证至少被消费一次。**
- 消费延迟队列消息后(zset结构中扫描到期的消息),不及时消费
- 把读取的消息放入一个 redis stream 队列,同时加入消费组
- 通过消费组消费 redis stream 消费,处理业务逻辑
- Redis Stream 消费组,读取消息处理并且 `ACK(将消息标记为"已处理")`
- 如果消息读取但是没处理,则进入XPENDING 列表,进行二次消费并且 `ACK(将消息标记为"已处理")`
## Redis Stream
- 序言
- 专题一 PHP基础教程
- 1、empty、isset、is_null的用法
- 2、线程安全与非线程
- 3、大文件上传需要修改的配置
- 10、魔术方法
- 4、编译安装PHP7
- 5、编译安装PHP7.4
- 6、PECL 安装 PHP 扩展库
- 专题二 PHP高级教程
- 1、类和对象
- 2、继承
- 3、魔术方法
- 4、抽象类
- 5、接口
- 6、反射机制实现自动依赖注入
- 7、服务容器与依赖注入的思想
- 8、并发解决方案之opcache
- 9、Composer自动加载原理
- 1、安装与使用
- 10、抽象类和接口的区别
- 11、self和static的区别
- 12、PHP7 变量
- 13、PHP8.3 错误 Error 和 异常 Exception 树列表
- 专题三 ThinkPHP6专题
- 1、DI容器
- 2、AUTH权限认证
- 3、Nginx URI重写方式
- 4、并发锁问题
- 5、自定义全局异常
- 6、CLI 模式跨模块查询数据库
- 7、数据库优化方案
- 附录一 常见错误
- 附录二 自定义分页类
- 附录三 cropper.js图片上传和裁剪
- 附录四 数据库和模型源码解读
- 1、Db类
- 2、查询构造器
- 3、模型
- 附录五 权限认证Auth类
- 附录六 ThinkPHP5.1 源码分析
- (一)类的自动加载
- (二)配置文件Config类
- (三)容器Container类
- (四)门面Facade
- (五)框架执行流程
- (六)路由解析
- 附录七 官方扩展
- 1、think-queue 队列
- 附录八 易错整理
- 附录九 问题列表
- 附录十 任务队列异步通过视图导出PDF
- 专题四 Docker教程
- 1、Docker安装
- 2、如何在本地构建镜像
- 3、镜像、容器以及命令操作
- 4、容器进入的4种方式
- 5、Dockerfile常用指令详解
- 6、发布自己的镜像
- 7、数据卷管理
- 8、docker-compose概念
- 9、docker-compose入门
- 10、如何构建docker-compose
- 11、Docker网络
- 12、搭建私有仓库
- 13、Docker部署方式
- 14、推送到Github仓库
- 附录一 PHPStrom 调试XDebug
- 附录二 安装RabbitMQ
- 附录 日常使用笔记
- 附录四 Docker 调试XDebug
- 专题五 Redis教程
- 1、编译安装
- 2、配置文件详解
- 3、Lua 脚本的应用和实践
- 4、Redis实现分布式锁(集群版)
- 5、Redis键空间通知
- 6、Redis5.0 搭建集群
- (1)、创建和使用Redis群集
- (2)、新增节点
- 7、限流器的实现
- 8、Redis5.0 新特性
- 8.1、注意要点
- 8.2 xreadgroup 命令
- 9、延迟任务队列
- 10 Stream 消息队列
- 11、基于 Redis 的 Stream 类型的完美消息队列解决方案
- 12、Streams 实现延迟消息队列
- 13、Stream流三种ACK机制
- 14、read error on connection排查
- 附录一 常见问题
- 附录二 Redis面试大全
- 附录三 有序集合使用场景
- 附录四 Lua脚本调试
- 附录五 高性能、高可扩展关键技术
- 专题六 MySQL教程
- 1-1、二进制安装
- 1-2、安装包安装(推荐)
- 2、索引、锁、事务
- 3、字符集
- 4、导出导入数据
- 5、5.7版本兼容性
- 6、数据库自动备份
- 7、如何重置MySQL 5.7 root密码
- 8、MySQL自动完成和语法突出
- 9、普通索引和唯一索引的区别
- 10、深入了解行锁、表锁、索引
- 11、索引数据结构
- 12、MySQL规范
- 13、开发高频面试题精选(重要)
- 14、锁专题
- 1、可重复读(REPEATABLE_READ)
- 2、事务隔离级别概述
- 15、MySQL外键约束
- 16、left join 查询
- 17、 MySQL设计三范式和反范式
- 18、性能分析-Profiling
- 19、查询好慢,除了索引,还能因为什么?(重要)
- 20、常用日期字段
- MYSQL 5.7 VARCHAR 类型详解
- 专题七 Nginx教程
- 1、什么是Nginx?
- 2、编译安装
- 3、日志配置和模块讲解
- 4、静态资源和缓存服务
- 5、正向和反向服务
- 6、Rewrite规则
- 7、HTTP负载均衡(七层)
- 8、TCP负载均衡(四层)
- 9、如何配置HTTPS服务
- 10、Nginx的负载均衡算法
- 11、如何配置http和https同时访问
- 12、灰度发布
- 13、常见负载均衡算法
- 14、Openresty 专题
- 15、如何改进 NGINX 配置文件节省带宽?
- 16、谈谈基于 OpenResty 的接口网关设计
- 附录一 阿里云负载均衡配置
- 附录二、基础配置文件
- nginx.conf
- 附录三、Nginx+lua+Memcache 实现灰度发布
- 附录四 视频监控RTSP转HLS解决方案
- 附录五 Openresty 编译
- 附录六 Vod模块
- 1、本地模式
- 2、映射模式
- 专题八 Git版本管理
- 1、Git 基础知识
- 2、团队分支模型
- 3、储藏与清理
- 4、如何同步Fork
- 5、多Git账户id_rsa私钥
- 6、高效规范使用Git
- 7、远程分支的创建
- 8、GitFlow工作流
- 9、Git撤销&回滚操作(git reset 和 get revert)
- 10、合并时 --no-ff 的作用
- 11、 删除本地、远程、缓存分支
- 13、Git和Windows的大小写不敏感产生的问题
- 附录一 、一次记录
- 附录二、常用工作流程
- 附录三、每次更新代码都要输入用户名密码
- 附录四、OEM版本控制
- 附录五 常用记录
- 1、查看某一个文件修改的具体内容
- 2、强制推送到远程分支
- 3、生产环境代码回滚
- 附录六 三年 Git 使用心得 & 常见问题整理
- 附录七 Git 忽略文件,不提交文件 清空缓存
- 12/找回历史删除分支
- 专题九 WorkerMan服务
- 3、SocketIO消息推送
- 4、master和worker模型
- 5、GatewayWorker
- 6、使用systemd管理workerman
- 7、TCP长连接应用GatewayWorker心跳检测
- 附录一 运行问题
- 附录二 问题与解决方法
- 专题十 MQ消息中间件
- 1、为什么要使用消息队列
- 2、RabbitMQ
- 『1』AMQP核心概念
- 『2』交换机模式讲解
- 『3』RabbitMQ高级特性
- (1)hello
- 3、NSQ
- 4、RabbitMQ延迟队列
- 附件一 RabbitMQ 注意要点
- 5、RocketMQ PHP 生产端和消费端代码优雅实现
- 专题十一 PHP函数整理
- 1、系统函数
- 2、自定义函数
- 3、回调函数
- 4、匿名函数
- 5、递归函数
- 6、常用函数库
- 7、call_user_func函数
- 8、preg_replace_callback函数
- 专题十二 常用设计模式
- 1、创建型模式
- (1)单例模式
- (2)工厂模式
- (3)抽象工厂模式
- (4)建造者模式(Builder)
- (5)原型模式(Prototype)
- 2、结构型模式
- (1)适配器模式(Adapter)
- (2)桥接模式(Bridge)
- (3)合成模式(Composite)
- (4)装饰器模式(Decorator)
- (5)代理模式(Proxy)
- (6)享元模式(Flyweight)
- 3、行为型模式
- 2、策略模式( Strategy)
- 4、六大原则
- 1、依赖注入
- 5、其他
- 6、Presenter模式
- 4、Service 模式
- 5、Repository模式
- 外观设计模式示例
- 专题十三 实时通信
- 1、pusher 入门教程
- 2、pusher 演示与频道实时通信
- 3、pusher 如何使用私有频道
- 4、pusher 实时图表展示
- 11、webman插件push入门教程
- 12、webman插件push如何使用私有频道
- 13、webman插件push私有频道客户端推送
- 14、webman插件push的webhooks
- 15、webman插件push的实时动态图表
- 专题十四 PHP异常处理
- 1、Exception 类
- 2、如何自定义异常?
- 3、处理PHP重错误
- 4、自定义错误处理器
- 专题十五 Shell脚本案例
- 1、crontab任务脚本无法执行问题
- 专题十六 Jenkins自动化部署
- 1、Jenkins安装
- 2、Pipeline插件
- 3、BlueOcean
- 4、OPENSSH PRIVATE KEY转换为RSA PRIVATE KEY
- 专题十七 常用工具整理
- 1、证书在线打印
- 2、密码生成规则
- 3、vscode插件
- 专题十八 常用功能列表
- 1、frp 内网穿透工具
- (1)如何做成一个服务
- (2)代理Websocket服务
- (3)代理N个Web服务
- (4)设置为系统服务
- (5)Https 配置
- 附录一 常见问题排查
- 2、如何美化文档
- 3、如何提高访问github的速度?
- 4、Vultr搭建SS教程
- 5、PPH编译安装
- 6、Supervisor进程管理工具
- 7、Umeditor 上传文件阿里云和本地
- 8、scp 远程上传或下载 文件/文件夹
- 9、安装和使用守护进程Supervisor
- 10、人脸识别
- 专题十九 流媒体直播实战
- 1、什么是视频直播?
- 2、如何使用推流软件OBS?
- 3、基于Nginx 的RTMP模块搭建系统
- 4、直播流程
- 附录一 阿里云直播
- 5、典型业务场景
- 8、直播回调授权观看
- 9、视频直播源如何加密
- 10、如何实现视频在线云剪辑
- 11、视频点播以及加密技术实现
- 12、FFmpeg 入门教程
- 13、HLS 直播加密播放
- 14、nginx-vod-module 模块
- 15、车辆维修直播系统
- 『16』HLS-m3u8专题
- 附件一 FFmpeg 命令
- 附件二 阿里云点播
- 专题二十 微信
- 附录一 遇到的坑
- 专题二十一 支付专题
- 『1』支付宝支付
- 『2』微信支付
- 『3』支付宝直付通
- 「1」什么是直付通?
- 「2」二级商户进件
- 「3」统一交易收款
- 「4」资金结算
- 「5」分账
- 「6」支付接入流程
- 「?」问题列表
- 附录一 return_url和notify_url的区别
- 附录二 常见错误信息
- 附录三 电脑端支付案例
- 附录四 其他问题
- 1、购买了线上课程后不能退款 这样的现象合法吗?
- 专题二十二 Vue3笔记
- 1、开发环境搭建
- 专题二十三 开放API专题
- 1、错误码
- 2、OAuth2流的简单说明
- 『3』HTTP API 身份验证和授权
- 专题二十四 测试专题
- 1、并发测试
- 专题二十五 DevOps专题
- 『1』PHP代码质量实战
- 专题二十六 前后端分离
- 『1』前后端分离介绍
- 『2』控制权限管理
- 专题二十七 微服务专题
- 1、服务发现 Nacos
- 1.1 服务发现
- 专题二十八 Casbin权限专题
- 1、设置超级管理员的三种方法
- 2、多租户权限和基本设置
- 3、casbin简化策略数据
- 4、多个RBAC
- 5、身份验证和基于角色的RBAC授权
- 6、Casbin在RESTful及中间件使用
- 7、Casbin 中 ABAC 的使用方法
- 8、Model语法和策略存储
- 9、Casbin的Model和Policy
- 10、RBAC的RESTful完全匹配访问模型
- 11、自定义函数使用
- 12、webman中使用
- 13、分布式服务中如何使用Watcher
- 14、Casbin 项目实战ABAC模型策略
- 附录一 源码解读
- 附录二 常见问题
- 专题二十九 PHP 常见错误处理
- 专题三十 ELK日志系统
- 1、docker-elk
- 专题三十一 Swoole专题
- 1、ThinkPHP6中RPC服务
- 专题三十二 Webman框架
- 1、自定义进程执行异步任务
- 2、实现WebRTC信令服务器
- 3、实现一个RPC服务
- 4、对象和资源的持久化
- 5、ThinkORM持久化连接
- 6、ThinkORM悲观锁解决商品超卖问题的实现
- 7、monolog日志神器
- 附录一 为什么?
- 附录二 编码规范
- 附录一 游戏
- 1、初级程序员常犯的错误
- 附录三 设计模式
- 1、单例模式
- 2、工厂方法模式
- 3、抽象工厂模式
- 4、装饰器模式
- 附录四 Docker安装SqlServer
- 专题二十一 Layui
- 我的技术栈【重要】
- 附录四 Linux 日常运维
- 1、sudo 权限
- 2、用户和用户组管理
- 3、grep 多条件查询
- 其他系列
- 1、fnm:基于Rust开发的高效Node版本管理工
- 样式
- Mall商城
- 1、系统架构
- 1.1 mall整合ThinkPHP+ThinkORM搭建基本骨架
- 1.2 mall整合Elasticsearch实现商品搜索
- 1.3 mall整合OSS实现文件上传
- 2、业务篇