在實際開發中我們經常會遇到需要長時間等待後臺事件的情況,例如較為常見的掃碼登入功能,二維碼介面需等待後臺掃碼登入成功的事件,再如匯入匯出等需要較長時間才能處理完成的任務,此時需要把任務放到後臺由非同步任務進行處理,完成後再給前臺介面推送完成事件,以上需求我們需要用長連線才能完成推送,但長連線推送狀態管理複雜,且需要部署獨立系統,系統流程複雜且橫向水平擴充套件困難,此時選擇更簡單long polling等待是一個更好的選擇,http請求直接等待返回,顯然邏輯更簡單,可用性可維護性也會更高。
openresty是一個構建在nginx上的高效能能系統,一般情況下我們也需要在自身服務前部署nginx作為閘道器,那麼選擇openresty來構建一個高效能的long polling服務顯然是一個好選擇。slock是高效能的狀態及原子運算元據庫,redis則是高效能的記憶體快取資料庫,使用下邊nginx配置檔案即可快速基於slock和redis構建一個高效能高可用long polling服務。同時構建的此long polling服務是一個通用服務,即可用於掃碼登入這樣的需求完成狀態推送,也可用於像訊息系統、私信系統等的訊息推送。
slock專案地址:github.com/snower/slock
slock簡介可看:segmentfault.com/a/119000004114862...
首先需在安裝好的openresty服務中安裝slock的lua client包。
專案地址:github.com/snower/slock-lua-nginx
安裝方式即把slock-lua-nginx中slock.lua複製到openresty目錄中的lualib/中,然後新增以下nginx配置檔案修改相關引數即可。
init_worker_by_lua_block {
local slock = require "slock"
slock:connect("server1", "127.0.0.1", 5658)
}
server {
listen 8081;
default_type application/json;
location /poll/event {
content_by_lua_block {
local cjson = require "cjson"
local slock = require "slock"
local default_type = ngx.var.arg_default_type or "clear"
local wait_type = ngx.var.arg_wait_type or ""
local event_key = ngx.var.arg_event or ""
local wait_timeout = tonumber(ngx.var.arg_timeout) or 60
local sendResult = function(err_code, err_message)
ngx.say(cjson.encode({
err_code = err_code,
err_message = err_message,
}))
end
if event_key == "" then
return sendResult(400, "event key is empty")
end
local slock_client = slock:get("server1")
local event = nil
if default_type == "set" then
event = slock_client:newDefaultSetEvent(event_key, 5, wait_timeout * 2)
else
event = slock_client:newDefaultClearEvent(event_key, 5, wait_timeout * 2)
end
if wait_type == "reset" then
local ok, err = event:waitAndTimeoutRetryClear(wait_timeout)
if not ok then
return sendResult(504, "wait event timeout")
end
return sendResult(0, "succed")
end
local ok, err = event:wait(wait_timeout)
if not ok then
return sendResult(504, "wait event timeout")
end
return sendResult(0, "succed")
}
}
location /poll/message {
content_by_lua_block {
local cjson = require "cjson"
local redis = require "resty.redis"
local slock = require "slock"
local default_type = ngx.var.arg_default_type or "clear"
local wait_type = ngx.var.arg_wait_type or ""
local event_key = ngx.var.arg_event or ""
local wait_timeout = tonumber(ngx.var.arg_timeout) or 60
local sendResult = function(err_code, err_message, data)
ngx.say(cjson.encode({
err_code = err_code,
err_message = err_message,
data = data,
}))
end
if event_key == "" then
return sendResult(400, "event key is empty")
end
local redis_client = redis:new()
redis_client:set_timeouts(5000, wait_timeout * 500, wait_timeout * 500)
local ok, err = redis_client:connect("10.10.10.251", 6379)
if not ok then
return sendResult(502, "redis connect fail")
end
local message, err = redis_client:lpop(event_key)
if err ~= nil then
return sendResult(500, "redis lpop fail")
end
redis_client:set_keepalive(7200000, 16)
if message ~= ngx.null then
return sendResult(0, "", message)
end
local slock_client = slock:get("server1")
local event = nil
if default_type == "set" then
event = slock_client:newDefaultSetEvent(event_key, 5, wait_timeout * 2)
else
event = slock_client:newDefaultClearEvent(event_key, 5, wait_timeout * 2)
end
if wait_type == "reset" then
local ok, err = event:waitAndTimeoutRetryClear(wait_timeout)
if not ok then
return sendResult(504, "wait timeout")
end
redis_client = redis:new()
redis_client:set_timeouts(5000, wait_timeout * 500, wait_timeout * 500)
local ok, err = redis_client:connect("10.10.10.251", 6379)
if not ok then
return sendResult(502, "redis connect fail")
end
local message, err = redis_client:lpop(event_key)
if err ~= nil then
return sendResult(500, "redis lpop fail")
end
redis_client:set_keepalive(7200000, 16)
return sendResult(0, "succed", message)
end
local ok, err = event:wait(wait_timeout)
if not ok then
return sendResult(504, "wait timeout")
end
redis_client = redis:new()
redis_client:set_timeouts(5000, wait_timeout * 500, wait_timeout * 500)
local ok, err = redis_client:connect("10.10.10.251", 6379)
if not ok then
return sendResult(502, "redis connect fail")
end
local message, err = redis_client:lpop(event_key)
if err ~= nil then
return sendResult(500, "redis lpop fail")
end
redis_client:set_keepalive(7200000, 16)
return sendResult(0, "succed", message)
}
}
}
/poll/event 介面只等待事件觸發,不返回資料。
/poll/message 則是先從redis中獲取資料,成功則返回,否則等待事件觸發,再從redis獲取資料返回。
介面Query String引數:
- default_type 建立Event的初始狀態是seted還是cleared,wait等待seted狀態觸發,即如果使用初始是seted則需其它系統先執行clear操作,可選值:set、clear,預設值clear
- wait_type 事件觸發後是否重置Event狀態,不重置可保證在過期時間內可重入,重置則可用於私信系統的迴圈獲取訊息或事件,設定為reset為重置,預設值空字元不重置
- event 等待的事件key,不可為空,redis也使用該key在List資料結構中儲存訊息
- timeout 數字,等待超時時間,單位秒,預設等待60秒
特別注意:
openresty使用單一連線到slock的tcp連線處理所有請求,nginx只有init_worker_by_lua_block建立的socket才可在整個worker生命週期中保持存在,所以slock connect需在init_worker_by_lua_block完成,第一個引數為連線名稱,後續可用該名稱獲取該連線使用。
slock配置為replset模式時,可用replset方式連線,例:slock:connectReplset(“server1”, {{“127.0.0.1”, 5658}, {“127.0.0.1”, 5659}}),使用該模式連線時,nginx會自動跟蹤可用節點,保持高可用。
配置完成後執行下方shell會處於等待返回狀態:
curl "http://localhost:8081/poll/message?event=test&default_type=clear"
使用slock java client推送事件
java client專案地址:github.com/snower/jaslock
package main;
import io.github.snower.jaslock.Client;
import io.github.snower.jaslock.Event;
import io.github.snower.jaslock.exceptions.SlockException;
import redis.clients.jedis.Jedis;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
public class App {
public static void main(String[] args) {
Client slock = new Client("localhost", 5658);
Jedis jedis = new Jedis("10.10.10.251", 6379);
try {
byte[] eventKey = "test".getBytes(StandardCharsets.UTF_8);
slock.open();
jedis.rpush(eventKey, "hello".getBytes(StandardCharsets.UTF_8));
jedis.expire(eventKey, 120);
Event event = slock.newEvent(eventKey, 5, 120, false);
event.set();
} catch (IOException | SlockException e) {
e.printStackTrace();
} finally {
slock.close();
jedis.close();
}
}
}
newEvent引數:
- eventKey 事件名稱,和前端請求一致
- timeout set操作超時事件
- expried 如果初始狀態時cleared,則表示set之後狀態保持時間,如果初始狀態是seted,則表示clear之後狀態保持時間,超過改時間都將被自動回收
- defaultSeted ture表示初始是seted狀態,false為cleared狀態,需和前端傳參一致
注:只推送事件時去除redis操作即可。
php、python、golang操作類似。
php client專案地址:github.com/snower/pyslock
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Redis;
use Snower\Phslock\Laravel\Facades\Phslock;
class TestSlockEvent extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'test:slock-event';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Command description';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return int
*/
public function handle()
{
Redis::rpush("test", "hello"); //需禁用prefix
Redis::expire("test", 120);
$event = Phslock::Event("test", 5, 120, false);
$event->set();
return 0;
}
}
python client專案地址:github.com/snower/phslock
import redis
import pyslock
redis_client = redis.Redis("10.10.10.251")
slock_client = pyslock.Client("localhost")
redis_client.rpush("test", "hello")
redis_client.expire("test", 120)
event = slock_client.Event("test", 5, 120, False)
event.set()
使用redis自定義命令推送事件
也可用redis自定義命令來執行slock Event的set和clear操作完成事件觸發。
初始是seted時:
#clear操作
lock ${EVENT_KEY} lock_id ${EVENT_KEY} flag 2 timeout ${TIMEOUT} expried ${EXPRIED}
#如 lock test lock_id test flag 2 timeout 5 expried 120
#set操作
unlock ${EVENT_KEY} lock_id ${EVENT_KEY}
#如 unlock test lock_id test
初始是cleared時:
#clear操作
unlock ${EVENT_KEY} lock_id ${EVENT_KEY}
#如 unlock test lock_id test
#set操作
lock ${EVENT_KEY} lock_id ${EVENT_KEY} flag 2 timeout ${TIMEOUT} expried ${EXPRIED} count 2
#如 lock test lock_id test flag 2 timeout 5 expried 120 count 2
#用redis-cli -p 5658連線slock後執行該示例命令,即可看到上方等待curl命令成功返回。
關於高可用,slock支援配置為叢集模式,在集權模式執行時主節點異常時可自動選擇新主節點,此時如果openresty使用resplset模式連線時,可自動使用新可用節點,保證高可用。
關於水平擴充套件,slock良好的多核支援,百萬級qps,保證了無需過多考慮水平擴充套件問題,而openresty則依然保持了web服務常規無狀態特性,可按照web常規水平擴充套件方式擴容即可不斷提高系統承載效能。
本作品採用《CC 協議》,轉載必須註明作者和本文連結