一、概述
Redis
是互聯網技術領域使用最爲廣泛的存儲中間件,它是Remote Dictionary Service(遠程字典服務)
的首字母縮寫,Redis
以其超高的性能、活躍的社區、詳細的文檔以及豐富的客戶端庫支持在開源中間件領域廣受好評,國內外很多大型互聯網都在使用Redis
,比如:Github
、新浪微博、阿里巴巴、京東、Stack Overflow
等,可以說,深入瞭解Redis
應用和實踐,已成爲如今中高級後端加法繞不開的必備技能。
二、Redis常見應用場景
三、Redis有哪些數據結構
3.1 String字符串
🔥字符串典型的使用場景:
- 單值緩存
- 對象緩存
- 計數器
- 分佈式鎖
單值緩存
127.0.0.1:6379> set num 1
OK
127.0.0.1:6379> get num
"1"
127.0.0.1:6379>
單值緩存
SET user:1 value(json格式數據)
計數器
文章閱讀量、點贊量、評論量
127.0.0.1:6379> incr article:read:id1
(integer) 1
127.0.0.1:6379> incr article:read:id1
(integer) 2
127.0.0.1:6379> incr article:up:id1
(integer) 1
127.0.0.1:6379> incr article:up:id2
(integer) 1
127.0.0.1:6379> incr article:comment:id1
(integer) 1
127.0.0.1:6379> incr article:comment:id1
(integer) 2
127.0.0.1:6379>
分佈式鎖
- setnx
定時任務防止同一時刻重複執行,可以在業務執行代碼前使用分佈式鎖控制。
127.0.0.1:6379> setnx job GlobalNotifyJob
(integer) 1
127.0.0.1:6379> get job
"GlobalNotifyJob"
127.0.0.1:6379> ttl job
(integer) -1
127.0.0.1:6379>
僞代碼如下:
@Slf4j
@Component
public class GlobalNotifyJob {
private static final String LOCK_KEY = "redis_notify_lock";
/**
* 每小時執行一次
*/
@Scheduled(cron = "0 0 0/1 * * ?")
public void notify() {
if (!lockService.grabLock(LOCK_KEY)) {
log.info("[GlobalNotifyJob] 沒有拿到鎖, 停止操作......");
return;
}
// 拿到鎖,開始執行業務...
}
}
- setex + 過期時間【SETNX KEY_NAME TIMEOUT VALUE】
127.0.0.1:6379> setex key1 60 value1
OK
127.0.0.1:6379> ttl key1
(integer) 53
127.0.0.1:6379> get key1
"value1"
127.0.0.1:6379>
hash哈希
🔥哈希典型應用場景:
- 緩存對象信息(帖子標題、摘要、作者信息)
- 記錄帖子的點贊數、評論數和點擊數
- 電商購物車
命令 | 描述 |
---|---|
HSET key field value | 存儲一個哈希表key的鍵值 |
HSETNX key field value | 存儲一個不存儲的哈希表key的鍵值 |
HMSET key field value [field value...] | 在一個哈希表key中存儲多個鍵值對 |
HGET key field value | 獲取哈希表key對應的field鍵值 |
HMGET key field value | 批量獲取哈希表key中多個field鍵值 |
HDEL key field [field ...] | 刪除哈希表key中多個field的鍵值 |
HLEN key | 返回哈希表key中field的數量 |
HGETALL key | 返回哈希表key中所有的鍵值 |
127.0.0.1:6379> hmset user:1 name austin age 25 address guangzhou balance 6888
OK
127.0.0.1:6379> hget user:1 name
"austin"
127.0.0.1:6379> hget user:1 balance
"6888"
127.0.0.1:6379> hmget user:1 age address
1) "25"
2) "guangzhou"
127.0.0.1:6379> hlen user:1
(integer) 4
127.0.0.1:6379> hgetall user:1
1) "name"
2) "austin"
3) "age"
4) "25"
5) "address"
6) "guangzhou"
7) "balance"
8) "6888"
127.0.0.1:6379>
list列表
🔥列表的典型應用場景:
- 文章列表
- 微博和微信公衆號消息
Stack(棧FILO) = LPUSH + LPOP
Queue(隊列FIFO)= LPUSH + RPOP
Blocking MQ(阻塞隊列)= LPUSH + BRPOP
LPUSH key value [value ...] // 將一個或多個值value插入到key列表的表頭(最左邊)
RPUSH key value [value ...] // 將一個或多個值value插入到key列表的表尾(最右邊)
LPOP key // 移除並返回key列表的頭元素
RPOP key // 移除並返回key列表的尾元素
LRANGE key start stop // 返回列表key中指定區間內的元素,區間以偏移量start和stop指定
LINSERT key BEFORE|AFTER pivot element // 在元素element前後插入pivot
LREM key count element //根據參數 COUNT 的值,移除列表中與參數 VALUE 相等的元素 count > 0 : 從表頭開始向表尾搜索,移除與 VALUE 相等的元素,數量爲 COUNT
BLPOP key [key ...] timeout //從key列表表頭彈出一個元素,若列表中沒有元素,阻塞等待 timeout秒,如果timeout=0,一直阻塞等待
BRPOP key [key ...] timeout //從key列表表尾彈出一個元素,若列表中沒有元素,阻塞等待 timeout秒,如果timeout=0,一直阻塞等待
set集合
🔥列表的典型應用場景:
- 抽獎
- 微博點贊,收藏,標籤
- 共同好友
抽獎場景:
- 用戶參與抽獎
# 將用戶10001加入商品a的參與池子中
SADD luckdraw:product:a 10001
- 查看參與商品a抽獎的所有用戶
SMEMBERS luckdraw:product:a
- 抽取1名幸運中獎者
SPOP luckdraw:product:a 1
共同好友場景:
用戶1的好友爲:3,4,8
用戶2的好友爲:4,5,11
取交集,獲取用戶1和用戶2的共同好友,爲用戶4。
127.0.0.1:6379> sadd user_1 2 3 4
(integer) 3
127.0.0.1:6379> sadd user_2 4 5 7
(integer) 3
127.0.0.1:6379> sinter user_1 user_2
1) "4"
127.0.0.1:6379>
sorted set有序集合
🔥列表的典型應用場景:
- 微博熱搜榜
- 刷禮物實時排行榜
- 博客社區本週熱議
Redis
有序集合和集合一樣也是string
類型元素的集合,且不允許重複的成員。不同的是每個元素都會關聯一個 double
類型的分數,Redis
正是通過分數來爲集合中的成員進行從小到大的排序。有序集合的成員是唯一的,但分數score
卻可以重複。下面使用redis-cli
實踐Redis
有序集合命令:
zset幾個基本命令:
命令 | 說明 |
---|---|
zrange key start stop [WITHSCORES] | 將集合元素依照順序值升序排序再輸出,start 和stop 限制遍歷的限制範圍 |
zincrby key increment member | 有序集key 的成員member 的score 值加上增量increment
|
ZUNIONSTORE destination numkeys key [key ...] [WEIGHTS weight [weight ...]] | 計算給定的一個或多個有序集的並集,其中給定key 的數量必須以numkeys 參數指定,並將該並集 (結果集) 儲存到destination
|
127.0.0.1:6379[3]> zadd zsetofpost 89 post:1
(integer) 1
127.0.0.1:6379[3]> zadd zsetofpost 123 post:2
(integer) 1
127.0.0.1:6379[3]> zadd zsetofpost 32 post:3
(integer) 1
127.0.0.1:6379[3]> zadd zsetofpost 432 post:4
(integer) 1
127.0.0.1:6379[3]> zadd zsetofpost 128 post:5
(integer) 1
#升序排序
127.0.0.1:6379[3]> zrange zsetofpost 0 -1 withscores
1) "post:3"
2) "32"
3) "post:1"
4) "89"
5) "post:2"
6) "123"
7) "post:5"
8) "128"
9) "post:4"
10) "432"
#降序排序
127.0.0.1:6379[3]> zrevrange zsetofpost 0 -1 withscores
1) "post:4"
2) "432"
3) "post:5"
4) "128"
5) "post:2"
6) "123"
7) "post:1"
8) "89"
9) "post:3"
10) "32"
#有序集合某個元素的score值加上對應的增量
127.0.0.1:6379[3]> zincrby zsetofpost 40 post:1
"129"
127.0.0.1:6379[3]> zincrby zsetofpost 500 post:3
"532"
127.0.0.1:6379[3]> zrange zsetofpost 0 -1 withscores
1) "post:2"
2) "123"
3) "post:5"
4) "128"
5) "post:1"
6) "129"
7) "post:4"
8) "432"
9) "post:3"
10) "532"
簡單認識了Redis
有序集合和對應的命令之後,我們來實現本週熱議排行榜功能,博客的本週熱議主要的實現思路是:
- 庫獲取最近 7 天的所有文章(或者加多一個條件:評論數量大於 0)。
- 把文章的評論數量作爲有序集合的分數
score
,文章的ID作爲key
存儲到zset
中,當有人發表評論的時候,直接使用命令加一,並重新計算得到排行榜。 - 本週熱議上有標題和評論數量,因此,我們還需要把文章的基本信息存儲到
Redis
中,這樣得到文章的ID之後,我們再從緩存中得到標題等信息,這裏我們可以使用hash
的結構來存儲文章的信息。 - 因爲是本週熱議,如果文章發表超過 7 天了之後就會失效,所以我們可以給文章的有序集合一個有效時間。超過 7 天之後就自動刪除緩存。
畫圖分析:
最終實現效果:
Bitmaps位圖
🔥位圖的典型應用場景:
- 用戶連續簽到功能
很多社區、博客平臺其實都有每日簽到模塊,一開始看到這個模塊需求的時候,很多人第一反應是利用MySQL
來實現,創建一個簽到表,記錄用戶ID和簽到時間,然後統計的時候從數據庫中取出來然後聚合計算,這樣設計其實存在弊端,如我們想要做一些複雜的功能就不是太方便了,或者說不是太高性能了,比如,今天是連續簽到的第幾天,在一定時間內連續簽到了多少天。另外一方面,如果按 100 萬用戶量級來計算,一個用戶每年可以產生 365 條記錄,100 萬用戶的所有簽到記錄那就有點恐怖了,查詢計算速度也會越來越慢。其實Redis
的Bitmaps
位圖操作非常適合處理每日簽到功能場景,因爲Bit的值爲0或者1,位圖的每一位代表一天的簽到,1表示已籤,0表示未籤。 考慮到每月初需要重置連續簽到次數,最簡單的方式是按用戶每月存一條簽到數據(也可以每年存一條數據)。Key的格式爲u:sign:uid:yyyyMM
,Value則採用長度爲4個字節(32位)的位圖(最大月份只有31天)。
Redis位圖命令基本命令
命令 | 說明 |
---|---|
SETBIT key offset value | 對key所儲存的字符串值,設置或清除指定偏移量上的位(bit) |
BITPOS key bit [start] [end] | 查詢指定字節區間第一個被設置成1的bit位的位置 |
GETBIT key offset | 查詢指定偏移位置的bit值 |
BITCOUNT key [start end] | 統計指定字節區間bit爲1的數量 |
GETBIT key offset | 查詢指定偏移位置的bit值 |
BITFIELD key offset | 查詢指定偏移位置的bit值 |
這裏的offset,大家姑且當做用戶ID來看就可以了,那麼究竟如何去實現用戶打卡功能呢,我們可以利用上面的setbit
命令來實現,setbit
的作用說的直白就是:在你想要的位置操作字節值,比如說u:sign:1000:202302
表示ID=1000
的用戶在2023年2月7號
簽到記錄。
# 用戶1000在2023年2月7號簽到
SETBIT u:sign:1000:202302 6 1 # 偏移量是從0開始,所以要把7減1
# 檢查用戶1000在2023年2月7號是否簽到
GETBIT u:sign:1000:202302 6 # 偏移量是從0開始,所以要把7減1
# 統計用戶1000在2月份簽到次數
BITCOUNT u:sign:1000:202302
# 獲取2月份前28天的簽到數據
BITFIELD u:sign:1000:202302 get u28 0
# 獲取2月份首次簽到日期
BITPOS u:sign:1000:202302 1 # 返回的首次簽到的偏移量,加上1即爲當月的某一天
示例代碼:
/**
* 基於Redis位圖的用戶簽到功能工具實現類
*
* @author: austin
* @since: 2023/2/7 1:50
*/
public class UserSignKit {
private Jedis jedis = new Jedis();
/**
* 用戶簽到
*
* @param uid 用戶ID
* @param date 日期
* @return 之前的簽到狀態
*/
public boolean doSign(int uid, LocalDate date) {
int offset = date.getDayOfMonth() - 1;
return jedis.setbit(buildSignKey(uid, date), offset, true);
}
/**
* 檢查用戶是否簽到
*
* @param uid 用戶ID
* @param date 日期
* @return 當前的簽到狀態
*/
public boolean checkSign(int uid, LocalDate date) {
int offset = date.getDayOfMonth() - 1;
return jedis.getbit(buildSignKey(uid, date), offset);
}
/**
* 獲取用戶簽到次數
*
* @param uid 用戶ID
* @param date 日期
* @return 當前的簽到次數
*/
public long getSignCount(int uid, LocalDate date) {
return jedis.bitcount(buildSignKey(uid, date));
}
/**
* 獲取當月連續簽到次數
*
* @param uid 用戶ID
* @param date 日期
* @return 當月連續簽到次數
*/
public long getContinuousSignCount(int uid, LocalDate date) {
int signCount = 0;
String type = String.format("u%d", date.getDayOfMonth());
List<Long> list = jedis.bitfield(buildSignKey(uid, date), "GET", type, "0");
if (list != null && list.size() > 0) {
// 取低位連續不爲0的個數即爲連續簽到次數,需考慮當天尚未簽到的情況
long v = list.get(0) == null ? 0 : list.get(0);
for (int i = 0; i < date.getDayOfMonth(); i++) {
if (v >> 1 << 1 == v) {
// 低位爲0且非當天說明連續簽到中斷了
if (i > 0) {
break;
}
} else {
signCount += 1;
}
v >>= 1;
}
}
return signCount;
}
/**
* 獲取當月首次簽到日期
*
* @param uid 用戶ID
* @param date 日期
* @return 首次簽到日期
*/
public LocalDate getFirstSignDate(int uid, LocalDate date) {
long pos = jedis.bitpos(buildSignKey(uid, date), true);
return pos < 0 ? null : date.withDayOfMonth((int) (pos + 1));
}
/**
* 獲取當月簽到情況
*
* @param uid 用戶ID
* @param date 日期
* @return Key爲簽到日期,Value爲簽到狀態的Map
*/
public Map<String, Boolean> getSignInfo(int uid, LocalDate date) {
Map<String, Boolean> signMap = new HashMap<>(date.getDayOfMonth());
String type = String.format("u%d", date.lengthOfMonth());
List<Long> list = jedis.bitfield(buildSignKey(uid, date), "GET", type, "0");
if (list != null && list.size() > 0) {
// 由低位到高位,爲0表示未籤,爲1表示已籤
long v = list.get(0) == null ? 0 : list.get(0);
for (int i = date.lengthOfMonth(); i > 0; i--) {
LocalDate d = date.withDayOfMonth(i);
signMap.put(formatDate(d, "yyyy-MM-dd"), v >> 1 << 1 != v);
v >>= 1;
}
}
return signMap;
}
private static String formatDate(LocalDate date) {
return formatDate(date, "yyyyMM");
}
private static String formatDate(LocalDate date, String pattern) {
return date.format(DateTimeFormatter.ofPattern(pattern));
}
private static String buildSignKey(int uid, LocalDate date) {
return String.format("u:sign:%d:%s", uid, formatDate(date));
}
public static void main(String[] args) {
UserSignKit kit = new UserSignKit();
LocalDate today = LocalDate.now();
{ // doSign
boolean signed = kit.doSign(1000, today);
if (signed) {
System.out.println("您已簽到:" + formatDate(today, "yyyy-MM-dd"));
} else {
System.out.println("簽到完成:" + formatDate(today, "yyyy-MM-dd"));
}
}
{ // checkSign
boolean signed = kit.checkSign(1000, today);
if (signed) {
System.out.println("您已簽到:" + formatDate(today, "yyyy-MM-dd"));
} else {
System.out.println("尚未簽到:" + formatDate(today, "yyyy-MM-dd"));
}
}
{ // getSignCount
long count = kit.getSignCount(1000, today);
System.out.println("本月簽到次數:" + count);
}
{ // getContinuousSignCount
long count = kit.getContinuousSignCount(1000, today);
System.out.println("連續簽到次數:" + count);
}
{ // getFirstSignDate
LocalDate date = kit.getFirstSignDate(1000, today);
System.out.println("本月首次簽到:" + formatDate(date, "yyyy-MM-dd"));
}
{ // getSignInfo
System.out.println("當月簽到情況:");
Map<String, Boolean> signInfo = new TreeMap<>(kit.getSignInfo(1000, today));
for (Map.Entry<String, Boolean> entry : signInfo.entrySet()) {
System.out.println(entry.getKey() + ": " + (entry.getValue() ? "√" : "-"));
}
}
}
}
運行結果:
您已簽到:2023-02-07
您已簽到:2023-02-07
本月簽到次數:5
連續簽到次數:3
本月首次簽到:2023-02-02
當月簽到情況:
2023-02-01: -
2023-02-02: √
2023-02-03: √
2023-02-04: √
2023-02-05: -
2023-02-06: √
2023-02-07: √
2023-02-08: -
2023-02-09: -
2023-02-10: -
2023-02-11: -
2023-02-12: -
2023-02-13: -
2023-02-14: -
2023-02-15: -
2023-02-16: -
2023-02-17: -
2023-02-18: -
2023-02-19: -
2023-02-20: -
2023-02-21: -
2023-02-22: -
2023-02-23: -
2023-02-24: -
2023-02-25: -
2023-02-26: -
2023-02-27: -
2023-02-28: -
Redis發佈訂閱
Redis提供了發佈訂閱功能,可以用於消息的傳輸,Redis的發佈訂閱機制包括三個部分:發佈者
、訂閱者
和Channel
。發佈者和訂閱者都是Redis客戶端,Channel則爲Redis服務器端,發佈者將消息發送到某個的頻道,訂閱了這個頻道的訂閱者就能接收到這條消息。Redis的這種發佈訂閱機制與基於主題的發佈訂閱類似,Channel相當於主題。