Redis高級進階(一)

1、redis中的事務java

在關係型數據庫中事務是必不可少的一個核心功能,生活中也是到處可見,好比咱們去銀行轉帳,首先須要將A帳戶的錢划走,而後存到B帳戶上,這兩個步驟必須在同一事務中,要麼都執行,要麼都不執行,否則錢憑空消失了,換了誰也沒法接受。redis

一樣,redis中也爲咱們提供了事務,原理是:先把一組同一事務中的命令發送給redis,而後redis進行依次執行。數據庫

一、事務的語法:緩存

multi
命令1
命令2
...
exec

解釋下語法:首先經過multi命令告訴redis:「下面我發給你的命令是屬於同一事務的,你呢,先不要執行,能夠把它們先存儲起來」。redis回答:「okay啦」。然後咱們就發送銀行轉帳的命令1和命令2,這時redis將聽從約定不會執行命令,而是返回queued,表示把這兩條命令存儲到等待執行的事務隊列中了。最後咱們發送exec命令,redis開始依次執行在等待隊列中的命令,完成一個事務的操做。性能優化

redis保證事務中全部命令要麼全執行,要麼都不執行。若是在發送exec前客戶端斷線了,redis會清空等待的事務隊列,全部命令都不會執行。而一旦發送了exec,即便在執行過程當中客戶端斷線了也沒有關係,由於redis早已存儲了命令。session

下面咱們模擬下銀行轉帳的業務,從銀行A轉帳5000到銀行B,中間企圖修改銀行A的餘額,這時看看可否轉帳成功並保證金額正確?post

  • 先給銀行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

二、事務錯誤處理性能

若是在執行一個事務時,裏面某個命令出錯了,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中的事務不像關係型數據庫有回滾機制,爲此如出現這樣的問題,開發者必須本身收拾形成這樣的爛攤子了。

爲了保證儘可能不出現命令和數據類型不匹配的運行錯誤,事前規劃數據庫(如保證鍵名規範)是尤其重要的。

三、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>

2、生存時間

 在實際開發中常常會遇到一些有時效的數據,好比限時優惠活動、緩存或驗證碼等,過一段時間須要刪除這些數據。在關係數據庫中通常須要維護一個額外的字段來存儲過時時間,而後按期檢測刪除過時數據。而在redis中命令就能夠搞定。

一、命令

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  精確到毫秒

3、排序

 在咱們實際的開發中,不少地方用到排序這個功能,上節我們說過有序集合就能夠實現排序功能,是經過它分數,redis除了有序集合外還有sort命令,sort命令很複雜,能用好它不是很容易,並且一不當心就可能致使性能問題,下面就說說這些排序。

一、有序集合排序

有序集合經常使用的場景是大數據排序,如遊戲玩家的排行榜,通常不多須要得到鍵中的全部數據。

二、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"

三、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

四、性能優化

sort命令是redis中最強大複雜的命令之一,若是用很差很容易出現性能問題,sort命令的時間複雜度爲:O(N+MlogM),其中N表示須要排序的元素個數,M表示返回的元素個數,當N數值很大時sort排序性能較低,而且redis在排序前會創建一個長度爲N的容器來存儲排序的元素,雖然是臨時的,可是若是遇到大數據量的排序則會成爲性能瓶頸。因此在開發中須要注意如下幾個方面:

  • 儘量減小待排序鍵中的元素數量(下降N值)
  • 使用limit參數只得到須要的值(減小M值)
  • 若是排序的數據量較大,儘量使用store將結果緩存
相關文章
相關標籤/搜索