## 簡介
顧名思義,延遲隊列就是進入該隊列的消息會被延遲消費的隊列。而一般的隊列,消息一旦入隊了之后就會被消費者馬上消費。
## 和定時任務區別
>延時任務有別于定時任務,定時任務往往是固定周期的,有明確的觸發時間。
>[warning] 而延時任務一般沒有固定的開始時間,它常常是由一個事件觸發的,而在這個事件觸發之后的一段時間內觸發另一個事件。
> 任務事件生成時并不想讓消費者立即拿到,而是延遲一定時間后才接收到該事件進行消費。
## 業務場景
- 訂單超時,用戶下單后進入支付頁面(通常會有超時限制)超過15分鐘沒有進行操作,那么這個訂單就需要作廢處理。
- 如何定期檢查處于退款狀態的訂單是否已經退款成功?
- 注冊后到現在已經一周的用戶,如何發短信撩動。
- 交易信息雙重效驗防止因系統級/應用級/用戶級等各種異常情況發生后導致的全部/部分丟失的訂單信息。
- 實現重復通知,默認失敗連續通知10次(通知間隔為`n*2+1/min`),直到消費方正確響應,超出推送上限次數后標記為異常狀態,可進行恢復!
## 使用場景
> 延遲隊列多用于需要延遲工作的場景。
最常見的是以下兩種場景:
### 1、延遲消費
1. 用戶生成訂單之后,需要過一段時間校驗訂單的支付狀態,如果訂單仍未支付則需要及時地關閉訂單。
2. 用戶注冊成功之后,需要過一段時間比如一周后校驗用戶的使用情況,如果發現用戶活躍度較低,則發送郵件或者短信來提醒用戶使用。
### 2、延遲重試
比如消費者從隊列里消費消息時失敗了,但是想要延遲一段時間后自動重試。
>[warning] 如果不使用延遲隊列,那么我們只能通過一個輪詢掃描程序去完成。
### 掃表存在的問題是
- 掃表與數據庫長時間連接,在數量量大的情況容易出現連接異常中斷,需要更多的異常處理,對程序健壯性要求高
- 在數據量大的情況下延時較高,規定內處理不完,影響業務,雖然可以啟動多個進程來處理,這樣會帶來額外的維護成本,不能從根本上解決。
- 每個業務都要維護一個自己的掃表邏輯。 當業務越來越多時,發現掃表部分的邏輯會重復開發,但是非常類似
## 緩存隊列設計

## 場景設計
實際的生產場景是筆者負責的某個系統需要對接一個外部的資金方,每一筆資金下單后需要延時30分鐘推送對應的附件。
這里簡化為一個訂單信息數據延遲處理的場景,就是每一筆下單記錄一條訂單消息(暫時叫做`OrderMessage`),訂單消息需要延遲5到15秒后進行異步處理。

## 延時隊列的實現
選用了基于`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中獲取完推送數據之后馬上刪除這兩個集合中對應的數據。
方案的流程圖大概是這樣:

## 相關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、O??Auth2流的簡單說明
- 『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、業務篇