周煦辰 2016年8月31日
本文介紹了一下本人在開發過程中遇到“定時推送提醒”的需求的時候所思考的三種解決方案。
明確問題
首先明確一下這個需求可能包含的幾個“坑”:
- 系統內的用戶量是否很大?所涉及的提醒任務是否會很多?
- 該提醒是否是用戶自己設置的?中途是否會修改?
- 推送的時間是否固定(如每天固定時間推送或者每隔一個小時推送等)?還是用戶自定義推送時間?
所需工具
- Redis
- crontab
- 任何一種Linux上可以運行的腳本語言(Python、PHP等)
解決方案一:使用Redis隊列(普通入隊出隊)
針對第一個問題,我們可以將需要推送的任務作為一個消息隊列,這樣可以減輕數據庫的壓力。因此這就引出第一種解決思路:使用Redis的隊列命令實現一個簡單的消息隊列。
基本思路為,在一天中的某個時間(例如早上五點這種服務器不會遇到什么壓力的時間段),通過crontab運行腳本,將推送任務整理完成並逐條插入Redis的隊列中。基本的代碼思路如下:
import redis
import json
# 這里是你的數據庫查詢代碼
# TODO
# 這里將你需要推送的用戶ID、內容等整合為一個字典
reminder = {
'id': 27149,
'content': 'The meaning of life is 42'
}
# 將字典編碼為json字符串
reminder_str = json.dumps(reminder)
# 連接redis並將數據插入redis中
r = redis.StrictRedis(hostname='localhost', port=6379, db=0)
print r.lpush('test_list', reminder_str)
# 如果所有數據已入隊
# 可以在最后插入一個空數據作為結束的標志
r.lpush('test_list', '{}')
到需要推送消息的時間(例如早上十點),通過crontab運行如下的出隊命令,進行消息的推送。
import redis
import json
r = redis.StrictRedis(hostname='localhost', port=6379, db=0)
while True:
item = r.rpop('test_list')
data = json.loads(item)
if not data:
break
# 這里是自定義的推送代碼
以上這種方法的優點在於:
- 可以利用Redis實現不同服務端應用間的數據,例如你的服務端應用是用PHP寫的,而入隊的腳本希望使用Python,則可以采用這種方法。
- 可以減輕對數據庫的壓力,且Redis的查詢效率非常高,可以提升該功能上的性能。
不足之處:
- 假如推送的消息是用戶自定義的,且中間會有修改,那這種方案就會遇到推送時的消息錯誤。
針對不足,我們來看方案二。
解決方案二:還是使用Redis隊列,但是我們使用阻塞模式(Blocking)
這個方案我們還是會用到Redis的隊列,不同的是我們會使用到Redis提供的阻塞出隊接口(blpop、brpop)。阻塞出隊簡單來說,就是在出隊命令在沒有接收到隊列內的數據前,會掛起,直到在設置的阻塞時間內隊列中有新的數據入隊,則彈出數據,命令結束;如果在設置的阻塞時間內沒有數據入隊,則返回空;如果阻塞時間被設置為0,則進程將一直被掛起直到隊列中有數據入隊。
若使用阻塞模式,假設我們的推送時間為早上的十點,那么在9:58左右,可以運行入隊命令,將需要推送的數據加入Redis隊列中。代碼示例如方案一,這里就不放了。
到了十點整,運行如下代碼進行出隊。
import redis
import json
r = redis.StrictRedis(hostname='localhost', port=6379, db=0)
while True:
item = r.brpop('test_list', 0)
data = json.loads(item)
if not data:
break
# 這里是自定義的推送代碼
注意第6行代碼item = r.brpop('test_list', 0),第二個參數即為阻塞的等待時間。
使用這種方法基本可以保證推送消息的准確性,一邊生成消息塞進隊列,一邊從隊列里拿即可。但是似乎這種方式並沒有發揮到“緩存”的優勢。
解決方案三:偷雞摸狗,用Redis的Hash實現一個“隊列”
這個方案中我們會使用到Redis中的Hash功能。簡單來說Redis的Hash就是一個類似Python中的字典,不同的是Redis的一個Hash-Field只能對應一個字符串。
r.hset(hash_key, hask_field, value)
使用該方案可以在用戶修改計划任務時快速找到需要推送的消息並修改。出隊的時候使用HAVLS並且遍歷即可。代碼就不放了,畢竟是個偷雞摸狗的思路。
