Redis高階進階(一)
一、redis中的事務
在關係型資料庫中事務是必不可少的一個核心功能,生活中也是處處可見,比如我們去銀行轉賬,首先需要將A賬戶的錢划走,然後存到B賬戶上,這兩個步驟必須在同一事務中,要麼都執行,要麼都不執行,不然錢憑空消失了,換了誰也無法接受。
同樣,redis中也為我們提供了事務,原理是:先把一組同一事務中的命令傳送給redis,然後redis進行依次執行。
1、事務的語法:
multi 命令1 命令2 ... exec
解釋下語法:首先通過multi命令告訴redis:“下面我發給你的命令是屬於同一事務的,你呢,先不要執行,可以把它們先儲存起來”。redis回答:“okay啦”。而後我們就傳送銀行轉賬的命令1和命令2,這時redis將遵從約定不會執行命令,而是返回queued,表示把這兩條命令儲存到等待執行的事務佇列中了。最後我們傳送exec命令,redis開始依次執行在等待佇列中的命令,完成一個事務的操作。
redis保證事務中所有命令要麼全執行,要麼都不執行。如果在傳送exec前客戶端斷線了,redis會清空等待的事務佇列,所有命令都不會執行。而一旦傳送了exec,即使在執行過程中客戶端斷線了也沒有關係,因為redis早已儲存了命令。
下面我們模擬下銀行轉賬的業務,從銀行A轉賬5000到銀行B,中間企圖修改銀行A的餘額,這時看看能否轉賬成功並保證金額正確?
- 先給銀行A 和B分別初始化10000元。
127.0.0.1:6379> set bankA 10000 OK 127.0.0.1:6379> get bankA "10000" 127.0.0.1:6379> set bankB 10000 OK 127.0.0.1:6379> get bankB "10000"
- 進行轉賬業務操作:
127.0.0.1:6379> multi OK 127.0.0.1:6379> decrby bankA 5000 QUEUED 127.0.0.1:6379> incrby bankB 5000 QUEUED 127.0.0.1:6379>
- 重新開啟一個session,模擬修改銀行A的餘額,比如取10000元,這時錢被取了,餘額為0
127.0.0.1:6379> decrby bankA 10000 (integer) 0 127.0.0.1:6379> get bankA "0"
- 到第一個session中執行事務
127.0.0.1:6379> exec 1) (integer) -5000 #這裡假設餘額可以透支,哈哈。變成﹣5000,而不是原來想的5000,如果實際業務,這時是無法進行轉賬的。事務保證了餘額正確 2) (integer) 15000
2、事務錯誤處理
如果在執行一個事務時,裡面某個命令出錯了,redis怎麼處理呢?在redis中,需要分析導致命令錯誤的原因,不同的原因會有不同的處理方式。
1)語法錯誤
語法錯誤是指該命令不存在或者引數個數不正確。對於這類錯誤,redis(2.6.5之後的版本)的處理方式是直接返回錯誤,全部不執行,即使裡面有正確的命令。
127.0.0.1:6379> multi OK 127.0.0.1:6379> set key value QUEUED 127.0.0.1:6379> set key (error) ERR wrong number of arguments for 'set' command 127.0.0.1:6379> iiiget key (error) ERR unknown command 'iiiget' 127.0.0.1:6379> exec (error) EXECABORT Transaction discarded because of previous errors. 127.0.0.1:6379> get key (nil) #事務中有語法錯誤的命令,即使有一個命令正確也不會被執行 127.0.0.1:6379>
2)執行錯誤
執行錯誤是指命令在執行的時候報錯,比如用雜湊的命令操作集合型別的鍵。這類錯誤在執行前redis是無法發現的,故事務中如出現這樣錯誤的命令,其他正確的命令會依然被執行,即使是在錯誤命令之後的。需要小心為上,避免此類錯誤。
127.0.0.1:6379> multi OK 127.0.0.1:6379> set key 1 QUEUED 127.0.0.1:6379> sadd key 2 QUEUED 127.0.0.1:6379> set key 3 QUEUED 127.0.0.1:6379> exec 1) OK 2) (error) WRONGTYPE Operation against a key holding the wrong kind of value 3) OK 127.0.0.1:6379> get key "3"
可見sadd key 2出錯了,但是set key 3依然被執行了。
redis中的事務不像關係型資料庫有回滾機制,為此如出現這樣的問題,開發者必須自己收拾造成這樣的爛攤子了。
為了保證儘量不出現命令和資料型別不匹配的執行錯誤,事前規劃資料庫(如保證鍵名規範)是尤為重要的。
3、watch命令
watch命令可以監控一個和多個鍵,一旦被監控鍵的值被修改,阻止之後的一個事務執行(即執行exec時返回nil,但這時watch監控也會失效),還記得上面轉賬嗎?當在轉賬事務過程中,bankA被取走了10000,餘額變成0,這時操作轉賬時應該提示餘額不足,無法轉賬。可以使用watch命令來阻止轉賬事務的執行。下面優化一下上面的轉賬業務:
127.0.0.1:6379> watch bankA #監控銀行A賬號 OK
127.0.0.1:6379> decrby bankA 10000
(integer) 0
127.0.0.1:6379> multi
OK 127.0.0.1:6379> decrby bankA 5000 QUEUED 127.0.0.1:6379> incrby bankB 5000 QUEUED 127.0.0.1:6379> exec (nil) 127.0.0.1:6379> get bankA "0" 127.0.0.1:6379> get bankB "10000" 127.0.0.1:6379>
二、生存時間
在實際開發中經常會遇到一些有時效的資料,比如限時優惠活動、快取或驗證碼等,過一段時間需要刪除這些資料。在關係資料庫中一般需要維護一個額外的欄位來儲存過期時間,然後定期檢測刪除過期資料。而在redis中命令就可以搞定。
1、命令
expire key seconds:返回1表示設定成功,0表示設定失敗或該鍵不存在;
127.0.0.1:6379> set lifecycle 'test life cycle' OK 127.0.0.1:6379> get lifecycle "test life cycle" 127.0.0.1:6379> expire lifecycle 30 #設定生存時間為30秒,最小單位是秒 (integer) 1 127.0.0.1:6379> ttl lifecycle #ttl檢視還剩多久 (integer) 13 127.0.0.1:6379> ttl lifecycle (integer) 11 127.0.0.1:6379> ttl lifecycle (integer) 9 127.0.0.1:6379> ttl lifecycle (integer) 8 127.0.0.1:6379> ttl lifecycle #當時間到了刪除後會返回-2,不存在的鍵也返回-2 (integer) -2
取消設定時間:persist key
除了專用的取消命令,set,getset命令也會清除key的生存時間。
pexpire key mileseconds 精確到毫秒
三、排序
在我們實際的開發中,很多地方用到排序這個功能,上節咱們說過有序集合就可以實現排序功能,是通過它分數,redis除了有序集合外還有sort命令,sort命令很複雜,能用好它不是很容易,而且一不小心就可能導致效能問題,下面就說說這些排序。
1、有序集合排序
有序集合常用的場景是大資料排序,如遊戲玩家的排行榜,一般很少需要獲得鍵中的所有資料。
2、sort命令
除了上面的有序集合,redis還提供了sort命令,它可以對列表、集合、有序集合型別鍵進行排序,並且完成如關係資料庫中關聯查詢類似的任務。
- 對集合型別排序:
127.0.0.1:6379> sadd set_sort 5 2 1 0 7 9 (integer) 6
#不是說集合是無序的嘛,明明上面新增時無序的,這裡打印出來怎麼排序了呢?是這樣的,集合常常用來儲存物件的id,一般都是整數,對於這種情況,進行了優化,所以這裡是排序了
127.0.0.1:6379> smembers set_sort 1) "0" 2) "1" 3) "2" 4) "5" 5) "7" 6) "9" 127.0.0.1:6379> sort set_sort desc #desc是按降序排序,當然這裡的排序並不影響原始的資料 1) "9" 2) "7" 3) "5" 4) "2" 5) "1" 6) "0"
- 對列表排序:
127.0.0.1:6379> lpush mylist 2 -1 3 44 5 (integer) 5 127.0.0.1:6379> lrange mylist 0 -1 1) "5" 2) "44" 3) "3" 4) "-1" 5) "2" 127.0.0.1:6379> sort mylist 1) "-1" 2) "2" 3) "3" 4) "5" 5) "44"
- 對有序集合排序
127.0.0.1:6379> zadd myzset 0 tom 1 helen 2 allen 3 jack (integer) 4 127.0.0.1:6379> zrange myzset 0 -1 #這是按照分數排序取得結果 1) "tom" 2) "helen" 3) "allen" 4) "jack" 127.0.0.1:6379> sort myzset (error) ERR One or more scores can't be converted into double 127.0.0.1:6379> sort myzset alpha #通過sort按照字母字典排序後的結果,這時忽略有序集合的分數了,按照元素的字典排序。 1) "allen" 2) "helen" 3) "jack" 4) "tom"
當然,如果結果集資料太多,需要分頁顯示,這時可以用limit引數搞定:
limit offset count 表示從第offset起,共取得count個數據
127.0.0.1:6379> sort myzset alpha desc limit 2 2 #表示從第二條取2條記錄 1) "helen" 2) "allen"
3、sort命令的引數
1)By引數
有時我們經常遇到對一個散列表中某個欄位進行排序,比如寫部落格時按照文章的釋出時間進行排序,取最新的文章放到首頁,這個時候sort命令的by引數就可以派上用場了。
By引數的語法是:“by 參考鍵”,其中參考鍵可以是字串型別或者雜湊型別鍵的某個欄位(散列表示為:鍵名->欄位名)如果提供了by引數,sort將不會再按照元素自身值進行排序,而是對每個元素使用元素值替換參考鍵中的第一個‘*’並獲取其值,然後依據該值進行排序。
有點繞啊,下面舉例說明:假如java類別下有4篇部落格文章,可以按照下圖建立文章類別和文章的資料模型。具體類別和文章的結構圖如下:
Tag:java:posts:表示文章的類別java,我們用集合型別表示,裡面只儲存文章的ID
Post:id:表示文章,有三個欄位,我們用雜湊型別儲存,一個id對應一個雜湊
這樣模型可以這樣建立了:
127.0.0.1:6379> hmset Post:1 title 'java study' content 'java is a good programming language' date '201509122110' OK 127.0.0.1:6379> hmset Post:22 title 'java study2' content 'java is a good programming language' date '201409121221' OK 127.0.0.1:6379> hmset Post:26 title 'java study3' content 'java is a good programming language' date '201709121221' OK 127.0.0.1:6379> hmset Post:60 title 'java study4' content 'java is a good programming language' date '201510221221' OK 127.0.0.1:6379> sadd Tag:java:posts 1 22 26 60 (integer) 4
這個時候我想對Tag:java:posts類別下的文章按照發布日期進行排序,可以這樣來:
127.0.0.1:6379> smembers Tag:java:posts #排序之前直接取得的文章 1) "1" 2) "22" 3) "26" 4) "60" 127.0.0.1:6379> sort Tag:java:posts by Post:*->date desc #按照日期排序後的結果,這裡by後面引數是雜湊型別的 1) "26" 2) "60" 3) "1" 4) "22"
當然了,by後面還可以跟上字串型別,如下:
127.0.0.1:6379> lpush sortbylist 2 1 3 (integer) 3 127.0.0.1:6379> set item:1 50 OK 127.0.0.1:6379> set item:2 90 OK 127.0.0.1:6379> set item:3 20 OK 127.0.0.1:6379> sort sortbylist by item:* desc 1) "2" 2) "1" 3) "3"
2)get引數
上面事例中的文章排序後,如果我想獲取文章標題,需要針對每個文章id進行hget,有沒有覺得很麻煩呢?其實sort還提供了get引數,可以很輕鬆的獲得文章的標題
get引數不影響排序,它的作用是返回get引數指定的鍵值。get引數和by引數一樣,也支援雜湊和字串型別的鍵,並使用*作為佔位符,要實現排序後直接返回文章的標題,可以這樣做:
127.0.0.1:6379> sort Tag:java:posts by Post:*->date desc get Post:*->title 1) "java study3" 2) "java study4" 3) "java study" 4) "java study2" 127.0.0.1:6379>
在一個sort中可以有多個get引數,但是by只能有一個,get #表示返回元素本身的值。還可以這樣:
127.0.0.1:6379> sort Tag:java:posts by Post:*->date desc get Post:*->title get Post:*->date get #
1) "java study3"
2) "201709121221"
3) "26"
4) "java study4"
5) "201510221221"
6) "60"
7) "java study"
8) "201509122110"
9) "1"
10) "java study2"
11) "201409121221"
12) "22"
3)store引數
如果希望儲存排序後的結果集,可以使用store引數,預設儲存後的資料型別是列表型別,如果該鍵已存在,則覆蓋它。
127.0.0.1:6379> sort Tag:java:posts by Post:*->date desc get Post:*->title get Post:*->date get # store sort.result (integer) 12 127.0.0.1:6379> lrange sort.result 0 -1 1) "java study3" 2) "201709121221" 3) "26" 4) "java study4" 5) "201510221221" 6) "60" 7) "java study" 8) "201509122110" 9) "1" 10) "java study2" 11) "201409121221" 12) "22" 127.0.0.1:6379>
store引數常常用來跟exprie命令實現排序結果的快取功能,如上面提到的遊戲排行榜資料,實現的虛擬碼如下
#判斷是否存在排序結果的快取 $isExistsCache = Exists cache.sort if ($isExistsCache = 1) #如果存在快取,直接返回值 return lrange cache.sort 0 -1 else #如果不存在快取,使用sort命令排序並將結果存入cache.sort快取中 $sortResult=sort some.list store cache.sort #設定快取的生存時間為10分鐘 expire cache.sort 600 return $sortResult
4、效能優化
sort命令是redis中最強大複雜的命令之一,如果用不好很容易出現效能問題,sort命令的時間複雜度為:O(N+MlogM),其中N表示需要排序的元素個數,M表示返回的元素個數,當N數值很大時sort排序效能較低,並且redis在排序前會建立一個長度為N的容器來儲存排序的元素,雖然是臨時的,但是如果遇到大資料量的排序則會成為效能瓶頸。所以在開發中需要注意以下幾個方面:
- 儘可能減少待排序鍵中的元素數量(降低N值)
- 使用limit引數只獲得需要的值(減少M值)
- 如果排序的資料量較大,儘可能使用store將結果快取