使用redis的increment()方法實現計數器功能案例
一直知道redis可以用來實現計數器功能,但是之前沒有實際使用過,昨天碰到一個需求:使用者掃碼當天達到20次即提示:當日掃碼次數達到上限!
當時就想到使用redis的遞增方法increment()來實現計數器功能,一定要注意redisTemplate和stringRedisTemplate的使用
首先設定key:
該key我使用了使用者id和當天日期作為key的一部分,date:xxxx-xx-xx格式,這樣一來該使用者在第二天掃碼的時候又是一個新key,因為日期不同了
設定key的過期時間:
實現計數器功能:
通過使用上面的方法,redis的計數器功能就可以實現了。
在使用過程過遇到的問題:
在使用的過程中,老是會拋錯:ERR value is not an integer or out of range
後來發現當時我使用的方法底層用的redisTemplate和stringRedisTemplate串了,當時setKey的時候用的方法底層是
stringRedisTemplate,後面我想get(key)的時候方法底層的模板使用的是redisTemplate,後面統一了一下模板的使用,然後計數
器功能正常執行不再拋錯。
看過很多文章說是序列化器的鍋,increment方法必須是stringRedisTemplate模板才能使用,但是我在實際使用的時候也確實是
用了redisTemplate,這個具體原因我還在看,此次使用中最主要的問題是setKey的時候使用的模板和取key的時候使用的模板不
一致導致的。寫個筆記記錄一下,一個坑不踩第二遍。大家如果遇到一樣的問題可以一起討論學習一下。
補充知識:認識redis:redis計數器與數量控制
這篇文章是我個人對redis的一些理解,可以幫助大家系統的認識redis。本文的目標讀者是使用過redis,但對redis瞭解不深的朋友。文章內容以redis為主,也會少量提到memcached。文章從redis的設計目的、工作模式、應用場景等方面闡述,最後會講解一些具體的應用場景,還會夾帶一些程式碼作為“乾貨”。
鑑於本人水平有限,文中如有不準確的內容,敬請斧正。
redis是什麼
redis是一種記憶體型的資料儲存器,使用場景
資料庫
快取
訊息代理(message broker)
同memcached對比
memcached是分散式的記憶體物件快取系統,設計意圖為通過緩解資料庫壓力來加快web應用的響應速度。
上面的描述分別被放在了redis和memcached的官網首頁最顯眼的位置,這也回答了redis和memcached的本質區別。
redis的工作模式
redis的工作模式為單程序,這意味著redis只能利用到一個cpu核心。 redis對請求的處理是序列的,對於同時湧進來的多個請求,redis首先把請求存入佇列,按請求到達的先後順序序列處理。
瞭解單程序和序列這兩個特點,有助於我們使用redis時“揚長避短”。
之前有同事提到過,為何redis不適合儲存大塊的資料?從redis的工作模式我們可以窺知一二:大塊的資料意味著需要較長的io時間,包括記憶體io和網路的io,cpu資源在io過程中是一直被佔用的,這會阻塞其它請求,從而影響redis的整體效能。
資料型別
大家對redis的資料型別已經比較熟悉了,主要有以下5種。
string
list
hash
set
sorted set
各種資料型別的常用操作
string
set,get,setnx,setex,psetex
拼接
增加、減少(整數型字串)
位操作
list
入列,出列(這兩個命令都有阻塞模式)
按排位獲取部分元素
hash
設定某個索引
獲取某個索引
增加/減少某個索引的值(整數型字串)
獲取所有索引的值
set
集合是一個數學概念,囉嗦提一下:集合中的元素都是唯一的
加入元素
刪除元素
檢查元素是否在集合
獲取集合中的元素數量
取差集/並集/交集
元素轉移(從集合a移至集合b)
zset
有序集合,每個元素都有一個分值,用於對元素進行排序
取交集/並集
獲取一個元素的rank
獲取分值在某個範圍內的元素數量
獲取分值在某個範圍內的元素
按rank範圍獲取元素
redis事務
redis事務和我們熟悉的mysql事務有所區別,它們的相同在於都是對一個或一組命令的打包執行,不同的地方在於redis事務不可回滾。
redis的事務具有原子性,一個事務的執行有兩種結果
完全執行
完全不執行
一些不可抗力因素除外,如服務掛掉,伺服器斷電。redis事務是一個獨立的請求,執行過程中會阻塞其它請求。
實現redis事務有以下兩種方式
multi-exec
lua指令碼
multi-exec
127.0.0.1:6380[1]> set counter1 1 OK 127.0.0.1:6380[1]> set counter2 2 OK 127.0.0.1:6380[1]> set counter3 3 OK 127.0.0.1:6380[1]> 127.0.0.1:6380[1]> 127.0.0.1:6380[1]> multi OK 127.0.0.1:6380[1]> incr counter1 QUEUED 127.0.0.1:6380[1]> sadd counter2 1 QUEUED 127.0.0.1:6380[1]> incr counter3 QUEUED 127.0.0.1:6380[1]> exec 1) (integer) 2 2) (error) WRONGTYPE Operation against a key holding the wrong kind of value 3) (integer) 4
從上面的事務執行輸出可以看到,我們的sadd執行出錯了,原因是操作的資料型別不正確,但redis沒有中止整個事務,而是繼續往下執行。redis這麼做是有道理的,見參考文獻1。
multi-exec和watch的組合可以實現類似於mysql事務的功能,如果被watch的key在事務執行前被修改了,redis會放棄執行事務。
lua指令碼
和multi-exec相比,lua指令碼的優勢在於靈活性,也可以減少一定的網路時間消耗(為什麼?)。
鑑於redis的工作模式,不建議用lua指令碼實現耗時較長的事務。
下面模擬了lua指令碼的執行阻塞其它請求的場景,大家可以親自試一下。
client 1
eval "local i=0; while(i<1000000) do redis.call('keys','*'); i=i+1; end" 0
client 2
incr counter
redis的實際應用
鎖
型別:string
命令:setnx name alice |set name alice NX
返回true則鎖定成功,否則鎖定失敗
瞭解了redis的工作模式,就知道為什麼用redis實現鎖是如此容易了。用memcache也可以實現鎖,但程式碼層面要複雜一些,參見memcached.cas。
事件佇列
型別:zset
命令:zadd,zrangebyscore,zrem
適合儲存一些需要順序處理的事件,將事件的score值設為時間戳或自增id即可。為什麼用list不可以?
計數器
型別:string
命令:incr
抽獎限額
型別:string
命令:incrby
某活動的現金紅包每天最多隻能傳送10000元。下面是這段邏輯的虛擬碼,如果能夠舉一反三,這段程式碼將大有用武之地。大家可以用併發測試工具測試這段程式碼,如果發現了bug,或者能有更好的實現方式,請不要告訴我 -_-
$key = 'max_amount'; $amountLimit = 10000; if (!$currAmount = Redis::get($key)) { $currAmount = 9990; // 從持久化資料庫獲取當前已發放金額 // 初始化 Redis::setnx($key,$currAmount); } if ($currAmount >= $amountLimit) { // 超出限額退出 } // 抽獎金額 $rewardAmount = 10; if ($rewardAmount > $amountLimit - $currAmount) { $rewardAmount = $amountLimit - $currAmount; } if (Redis::incrby($key,$rewardAmount) > $amountLimit) { // 超出限額退出 } else { // 成功抽獎 }
初始化為何用setnx,用set可以嗎?
最後為何使用incrby,不用可以嗎?
總結
文章內容不多,所謂的乾貨更少,這和作者的水平有關,也和文章的定位有關。細心的朋友可能發現了,文中有一些內容是帶有問號的,有興趣的朋友可以加以思考。希望能給大家一個參考,也希望大家多多支援我們