.1事務 redis中事務是一組命令的集合。 事務同命令一樣都是redis的最小執行單位,Redis保證一個事務中的命令要麼都執行,要麼都不執行。如果redisClient在發送EXEC命令前掉線,則redis會清空事務隊列,事務中的所有命令都不會執行;如果redisClient在發送EXEC命令後 ...
.1事務
redis中事務是一組命令的集合。
事務同命令一樣都是redis的最小執行單位,Redis保證一個事務中的命令要麼都執行,要麼都不執行。如果redisClient在發送EXEC命令前掉線,則redis會清空事務隊列,事務中的所有命令都不會執行;如果redisClient在發送EXEC命令後掉線,所有的命令依然會被執行,因為redis中已經記錄了所有要執行的命令了。
另外,Redis保證一個事務從開始執行到執行結束期間,不會穿插的執行其它的命令或事務。
redis事務舉例 |
4.1.2 redis事務的錯誤處理
命令執行出錯的兩種情況介紹: |
註意:redis事務沒有回滾功能。對於剛纔提到的會導致事務執行失敗的兩種錯誤,語法錯誤完全可以在開發時找出並解決;運行錯誤可以通過做好規劃鍵名規範等來杜絕(這樣就不會出現命令與鍵類型不匹配的情況了)。
4.1.3 Watch與UnWatch
Watch場景 |
|
場景:在事務中,有時候需要先獲得一條命令的返回值,然後再根據這個值執行下一條命令;例如用事務來實現incr函數以防止竟態條件。 思路:要保證從get獲得鍵值開始到incr函數執行結束為止,該鍵值不會被其它客戶端修改,這樣也可以防止竟態條件。 WATCH命令:用於監控一個或多個鍵,一旦其中有一個鍵被修改或者刪除,之後的事務就不會執行(EXEC返回空結果nil);監控一直持續到EXEC命令(因為事務中的命令是在EXEC之後執行的,所以在事務中可以修改WATCH監控的鍵值)。
|
|
存在竟態條件的原始偽碼 |
用事務改造以防止竟態條件 |
def incr($key) $value = GET $key if not $value $value = 0 $value = $value + 1 set $key, $value return $value |
def incr($key) WATCH $key $value = GET $key if not $value $value = 0 $value = $value + 1 MULTI set $key, $value result = EXEC // TODO result為nil則遞歸調用incr,否則返回結果 return result[0] |
UnWatch場景 |
|
4.2 過期時間
expire key seconds |
>> 設置(更新)一個鍵的過期時間,到期後redis會自動刪除。 1表示設置成功; 0表示鍵不存在或設置失敗。 |
pexpire key mseconds |
毫秒 |
ttl key |
>> 返回一個鍵還有多久到期(秒); 如果鍵不存在則返回-2; 如果鍵是永久的(即沒有為鍵設置過期時間)則返回-1。 |
pttl key |
毫秒 |
persist key |
>> 取消鍵的過期時間設置; 1表示過期時間被成功清除; 否則返回0(鍵不存在或鍵本身就是永久的) 註意:set和getset為鍵賦值也會同時清除鍵的過期時間。 |
expireAt key utcSeconds pexpireAt key utlMSeconds |
兩個不常用的命令,設置鍵的過期時刻 |
實踐 |
1、實現訪問頻率限制之方案1
此方案的問題: 極端情況下,一個ip在第1分鐘的第1秒訪問了1次博客,建了一個鍵並設置60秒到期時間,然後在第1分鐘最後一秒又訪問80次,這樣第一分鐘內總共訪問81次,是可以通過訪問頻率限制的,此時該鍵到期被刪除。然後在第2分鐘第一秒又訪問了1次,會再建同名鍵並設置60秒的到期時間,第二分鐘第2秒又訪問了80次,依然可以通過訪問頻率限制。但是問題是從第1分鐘最後1秒到第2分鐘第2秒這3秒時間內訪問次數是超過了100次的,我們需要粒度更小的控制方案。 |
2、實現訪問頻率限制之方案2(解決方案1的問題) 如果要精確的保證每個分鐘最多訪問100次,需要記錄下用戶每次訪問的時間。對每個ip,使用一個列表類型鍵來保存他最近100次訪問博客的時間。
|
3、實現緩存 當伺服器記憶體有限時,如果大量使用緩存鍵且過期時間設置過長就會導致redis占滿記憶體,而如果擔心redis占用記憶體過大就將緩存鍵的過期時間設置過短就會導致緩存命中率過低。實際開發中一般會限制redis的最大記憶體,並讓redis按照一定規則淘汰不需要的鍵。 具體設置方法為,修改配置文件的maxmemory參數,以限制redis最大可用記憶體大小(位元組),當超過這個限制時,redis會根據maxmemory-policy參數指定的策略來刪除不需要的鍵直到redis占用記憶體小於指定記憶體大小。
|
4.3 排序
問題:在3.5中我們提到,我們是使用集合類型來存儲一個標簽下所有文章id的,由於集合類型是無序的,所以查看一個標簽下的文章列表時,文章不是按照時間排序的。考慮換成有序集合類型,但是zset支持的集合操作又不如set類型強大,比如說實現獲取同屬於多個標簽下的文章列表,由於zset沒有zInter命令,只有zInterStore命令,用zInterStore來實現zInter的效果就有點麻煩,偽代碼如下。
MULTI zInterStore tempKey … zRange tempKey … del tempKey EXEC |
至於為何,zset不提供zInter,zUnion命令,解釋如下圖:
4.3.2-5 SORT命令
sort key [ALPHA] [DESC] [LIMIT offset count] |
1、sort命令可用於對列表類型鍵、集合類型鍵、有序集合類型鍵進行排序,並且可以完成類似連接查詢的功能。 2、在對有序集合類型排序時會忽略元素的分數,只針對元素自身的值進行排序。 3、sort命令預設是對數值元素進行排序的,它會嘗試將所有元素轉換成double來比較,如果不能轉換則報錯,通過加ALPHA參數可以實現按照字典順序排列非數字元素。 4、預設是從小到大排序,指定DESC參數就是從大到小排序。 5、如果需要分頁還可以指定[LIMIT offset count]參數。 |
BY參數 |
格式為[BY 參考鍵] ,參考鍵可以是字元串類型鍵,或者散列類型鍵的某個欄位(散列鍵名->欄位名)。 如果提供了BY參數,SORT命令將不再按照元素自身值來進行排序,而是對每個元素依次獲取相應參考鍵的值,然後依據這些參考鍵的值來進行排序。對每個元素,通過使用元素的值替換參考鍵中的第一個”*”來決定相應的參考鍵是什麼。 如果幾個元素的對應參考鍵值相同,則SORT命令會再比較元素本身的值來決定元素的順序。當某個元素對應的參考鍵不存在時,會預設參考鍵的值為0。 若參考鍵名不包含”*”(即常量參考鍵名),則所有要比較的值都是一樣的,SORT命令將不會執行排序。在不需要排序,但需要藉助SORT命令獲得與元素相關聯的數據時,常量參考鍵名很有用。 ======================================================================== 例如,可通過“sort TAG:java:articleIds BY article:*->articleTime DESC”來對java標簽下的文章按文章發佈時間進行排序(這裡假設每篇文章都對應了一個散列類型鍵article:articleId用於存儲該文章對象含有的欄位,包括發佈時間欄位articleTime)。 再例如,可通過“sort TAG:java:articleIds BY article:*: articleTime DESC”來對java標簽下的文章按照文章發佈時間進行排序(這裡假設每篇文章都對應了一個字元串類型鍵article:articleId:articleTime用於存儲該文章的發佈時間);
|
GET參數 |
GET參數不影響排序,用於使SORT命令的返回結果不再是元素自身的值,而是GET參數中指定的參數鍵值。 格式為[GET 參數鍵],GET參數的規則和上面介紹的BY參數一樣。要註意:在一個SORT命令中可以有多個GET參數,但只能由一個BY參數。 ======================================================================== 要實現在排序後直接返迴文章標題以及發佈時間列表而不是文章id列表,可通過在之前命令的基礎上加上GET參數,例如”sort TAG:java:articleIds BY article:*->articleTime DESC GET article:*->articletITLE GET article:*->articletIME GET #”,其中”GET #”用於返回元素自身的值。 |
STORE參數 |
預設情況下,SORT命令會直接返回排序結果,如果希望保存排序結果,可以使用STORE參數,格式為[STORE 目標鍵名],保存後的鍵的類型為列表類型,如果目標鍵已存在則會被覆蓋,加上STORE參數後SORT命令的返回值為目標列表類型鍵中元素個數。例如”sort TAG:java:articleIds BY article:*->articleTime DESC STORE resultkey”。
|
4.3.6 SORT性能分析
4.4 消息通知
解決方案:定義一個任務隊列,作為生產者的頁面進程負責添加任務到隊列中,而作為消費者的郵件發送進程負責不斷的從隊列中獲取任務進行處理。
任務隊列的好處: |
1、 松耦合。生產者和消費者無需知道彼此的實現細節,只需要約定好任務對象的描述格式。 2、 易於擴展。可增加多個分佈在不同伺服器上的消費者來降低單台伺服器的負載。 |
4.4.2 通過redis列表類型實現任務隊列
思路:定義一個列表類型鍵作為任務隊列,生產者通過LPush命令添加任務到隊列中,消費者不斷的使用Rpop命令從隊列中取出任務來處理。消費者偽碼如下:
BRPop list [list2 …] timeout BLPop …… |
描述:同時檢測多個列表類型鍵,移出並獲取列表的最後一個元素,如果列表中沒有元素則會阻塞列表直到等待超時或發現可彈出元素為止;如果多個鍵都有元素則按照從左到右的順序取第一個鍵中的第一個元素。timeout設置為”0”表示不限制等待時間。 返回值:假如在指定時間內沒有任何元素被彈出,則返回一個 nil 和等待時長。 反之,返回一個含有兩個元素的列表,第一個元素是被彈出元素所屬的 key ,第二個元素是被彈出元素的值。 |
4.4.3 優先順序隊列
需求:博客系統存在兩種任務,即新郵箱請求訂閱時發送確認郵件的任務,以及發佈新文章後發送通知郵件給所有已訂閱郵箱的任務。現在需要實現優先順序隊列,當發送確認郵件任務和發送通知郵件任務同時存在時,優先執行前者。
4.4.4 “發佈/訂閱”模式
publish channel msg |
描述:發佈者給指定頻道發佈消息,發出去的消息不會被持久化,客戶端訂閱一個頻道後只能收到後續發佈到該頻道的消息,之前發送的就收不到了。 返回值:接收到這條消息的訂閱者數量 |
subscribe channel [channel2 …] |
描述:subscribe用於訂閱者訂閱指定頻道,執行subscribe命令後客戶端會進入訂閱狀態,處於此狀態下的客戶端不能使用除了subscribe、unsubscribe、psubscribe、unsubscribe這4個屬於”發佈/訂閱”模式的命令之外的命令,否則會報錯。
|
unsubscribe [channel channel2 …] |
用於訂閱者取消訂閱指定頻道,如果不指定則會取消訂閱所有頻道。 |
4.4.5 按照規則訂閱
PSUBSCRIBE pattern [pattern ...] |
PSUBSCRIBE命令用於訂閱者訂閱一個或多個符合給定模式的頻道。用psubscribe命令進入訂閱模式後,如果收到消息,返回值會包含4個值;第一個值為”pmessge”,表示這條消息是通過psubscribe命令訂閱頻道而收到的;第二個值表示訂閱時使用的pattern;第三個值表示實際收到消息的頻道名稱,第四個值即為消息內容。
|
pUNsubscribe [pattern pattern2 …] |
PSUBSCRIBE用於退訂指定的規則,如果沒有指定參數,則會退訂所有規則。
|
4.5管道
往返時延:redisClient向redis發送命令耗時 + redis向redisClient返回命令執行結果耗時 |
redis底層對管道提供了支持,通過管道可以一次性發送多條命令併在執行完後一次性將結果返回,管道通過減少redisClient與redis的通信次數來實現降低往返時延累計值的目的。當一組命令中所有命令都不依賴於之前命令的執行結果時,就可以將這組命令一起通過管道發出。
|