拋出,問題
最近項目碰到這么一個技術(shù)上的需求:
前端通過長輪詢的機制(http long polling)绞灼,獲取服務(wù)端的消息數(shù)據(jù)呈野。而服務(wù)端是需要訂閱所有業(yè)務(wù)方的業(yè)務(wù)消息,再通知到給前端商佛。
長輪詢姆打,其實簡單來說,就是前端發(fā)起一個http請求幔戏,服務(wù)端把當(dāng)前的請求 hang 住税课,直到超時或者有需要返回的內(nèi)容,才return韩玩。 Apollo 配置中心就是使用這個機制實現(xiàn)配置的更新通知。
但有這么一種情況合愈,假如服務(wù)端消費到消息击狮,但此時前端與服務(wù)端的連接剛好斷開了,那這個消息就沒法通知到前端彪蓬。
所以,我們得需要把服務(wù)端消費的消息保存下來膘茎,保證前端的每次發(fā)起長輪詢的時候,都能拿到消息數(shù)據(jù)辽狈。
如果說,前端能直接訂閱業(yè)務(wù)方的消息隊列的話驮配,那其實就沒服務(wù)端什么關(guān)系了着茸。當(dāng)然,這是不允許的涮阔,前端不能連接咱們的消息中間件,并且業(yè)務(wù)方的消息數(shù)據(jù)也需要清洗處理后才能給到前端掰邢。
思考伟阔,方案
Apollo 的實現(xiàn)機制是,所有的配置都會寫入數(shù)據(jù)庫皱炉。每次請求過來,會去數(shù)據(jù)庫獲取是否有數(shù)據(jù)變更合搅。
考慮到我們實際的業(yè)務(wù)場景,我們的業(yè)務(wù)消息其實時候時效性的康铭,也就是說消息如果過期了赌髓,那其實也沒用了。
要考慮輕量春弥,我第一想到的就是 Redis Lists,利用其可以實現(xiàn)隊列的特性扫责,所以綜合考慮最終采用 Redis 作為消息的存儲模型逃呼。
優(yōu)化者娱,代碼
Redis 的 Lists 數(shù)據(jù)結(jié)構(gòu)苏揣,是簡單的字符串鏈表,按插入順序排序框沟≡鎏浚可以在頭部(Left)或者尾部(Right)添元素。
利用這個特性隙姿,我們可以實現(xiàn)隊列(先進先出)的數(shù)據(jù)結(jié)構(gòu),搭配命令 rpush + lpop 或者 lpush + rpop 队丝。
但是不管是 lpop 或是 rpop 欲鹏,列表都只會彈出一個數(shù)據(jù),沒法達到我們的需求一次獲取多個貌虾。
網(wǎng)上搜索了一下相關(guān)的解決方案裙犹,再結(jié)合官網(wǎng)的命令文檔。
得出一種比較可行的方案: lrange + ltrim + pipeline
lrange 和 ltrim 是Redis Lists 的指令
lrange mylist 0 5
表示從隊列頭部(Left)開始取袄膏,下標(biāo)為0掺冠,到下標(biāo)為5的元素。如果是負(fù)數(shù)的話德崭,表示隊列尾部(Right)開始取。
ltrim mylist 0 5
ltrim 的含義是只保留指定范圍的元素锌奴,上面的意思是憾股,只保留列表下標(biāo)為 0 - 5 的元素箕慧,其他的都會被刪掉茴恰。
那如果要刪掉前6條數(shù)據(jù)颠焦,就可能需要這么寫:
ltrim mylist 5 -1
負(fù)數(shù)表示隊列尾部(Right)開始取往枣。
按實現(xiàn)來說, lrange + ltrim 就能實現(xiàn)從 List 獲取多個元素的效果似忧,為什么還需要用上 pipeline 呢丈秩?
其實這里有一個很顯然的并發(fā)問題,這兩個命令對于redis來說饺著, 并不是原子操作 肠牲,假如有其他線程在執(zhí)行這兩個命令的線程之間,刪除了隊列的一部分?jǐn)?shù)據(jù)缀雳,那么第二個命令執(zhí)行的時候,其實是list里面的數(shù)據(jù)已經(jīng)是不對了肥印。
而 Redis 的 pipeline 功能,可以解決我們上面說的這個問題腹鹉。
大部分人可能對pipeline 比較陌生敷硅,因為平時業(yè)務(wù)上也很少用到。
先來看看 Redis Pipelining 的定義
A Request/Response server can be implemented so that it is able to process new requests even if the client hasn't already read the old responses. This way it is possible to send multiple commands to the server without waiting for the replies at all, and finally read the replies in a single step.
This is called pipelining, and is a technique widely in use for many decades. For instance many POP3 protocol implementations already support this feature, dramatically speeding up the process of downloading new emails from the server.
簡單來說力奋,它能夠支持支持客戶端一次發(fā)送多個命令幽七,服務(wù)端接收到這些命令后,會統(tǒng)一按順序處理,并且是在一個原子事務(wù)里藕届。
顯然亭饵,Pipelining 最明顯的優(yōu)勢在于提高 client 與 server 的交互響應(yīng)時間。將幾個命令放在同一個請求中辜羊,和每個命令作為一個請求相比,效率是肯定提高的碱妆。
PS:這種方式昔驱,跟我為什么要獲取多個list 數(shù)據(jù)的思路類似的。前端長輪詢獲取數(shù)據(jù)結(jié)果時骤肛,要返回多個數(shù)據(jù)。
話不多說腋颠,直接貼上示例代碼:
@Autowired
RedisTemplate<String, Object> redisTemplate;
public void run() throws Exception {
System.out.println("start......");
String key = "key:list";
Integer number = 5;
// 每次獲取5個
List<Object> objects = redisTemplate.executePipelined(new RedisCallback<Object>() {
@Override
public Object doInRedis(RedisConnection connection) throws DataAccessException {
connection.lRange(key.getBytes(StandardCharsets.UTF_8), 0, number - 1);
connection.lTrim(key.getBytes(StandardCharsets.UTF_8), number, -1);
return null;
}
});
System.out.println(Arrays.toString(objects.toArray()));
System.out.println("end......");
}
redisTemplate 提供了兩種類型的方法:
- executePipelined(RedisCallback action)
- executePipelined(SessionCallback session)
這兩者實現(xiàn)的功能都差不多,只不過 SessionCallback 比 RedisCallck 封裝的更友好一下巾腕,后者的話更加底層絮蒿,用法更加貼近與原生命令。