一、概述
Redis是互聯(lián)網(wǎng)技術(shù)領(lǐng)域使用最為廣泛的存儲中間件,它是Remote Dictionary Service(遠程字典服務(wù))的首字母縮寫,Redis以其超高的性能、活躍的社區(qū)、詳細的文檔以及豐富的客戶端庫支持在開源中間件領(lǐng)域廣受好評,國內(nèi)外很多大型互聯(lián)網(wǎng)都在使用Redis,比如:Github、新浪微博、阿里巴巴、京東、Stack Overflow等,可以說,深入了解Redis應(yīng)用和實踐,已成為如今中高級后端加法繞不開的必備技能。
二、Redis常見應(yīng)用場景

三、Redis有哪些數(shù)據(jù)結(jié)構(gòu)

3.1 String字符串
??字符串典型的使用場景:
- 單值緩存
- 對象緩存
- 計數(shù)器
- 分布式鎖
單值緩存
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格式數(shù)據(jù))
計數(shù)器
文章閱讀量、點贊量、評論量

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
定時任務(wù)防止同一時刻重復(fù)執(zhí)行,可以在業(yè)務(wù)執(zhí)行代碼前使用分布式鎖控制。
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";
/**
* 每小時執(zhí)行一次
*/
@Scheduled(cron = "0 0 0/1 * * ?")
public void notify() {
if (!lockService.grabLock(LOCK_KEY)) {
log.info("[GlobalNotifyJob] 沒有拿到鎖, 停止操作......");
return;
}
// 拿到鎖,開始執(zhí)行業(yè)務(wù)...
}
}
- 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哈希
??哈希典型應(yīng)用場景:
- 緩存對象信息(帖子標題、摘要、作者信息)
- 記錄帖子的點贊數(shù)、評論數(shù)和點擊數(shù)
- 電商購物車
| 命令 | 描述 |
|---|---|
| HSET key field value | 存儲一個哈希表key的鍵值 |
| HSETNX key field value | 存儲一個不存儲的哈希表key的鍵值 |
| HMSET key field value [field value...] | 在一個哈希表key中存儲多個鍵值對 |
| HGET key field value | 獲取哈希表key對應(yīng)的field鍵值 |
| HMGET key field value | 批量獲取哈希表key中多個field鍵值 |
| HDEL key field [field ...] | 刪除哈希表key中多個field的鍵值 |
| HLEN key | 返回哈希表key中field的數(shù)量 |
| 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列表
??列表的典型應(yīng)用場景:
- 文章列表
- 微博和微信公眾號消息
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中指定區(qū)間內(nèi)的元素,區(qū)間以偏移量start和stop指定
LINSERT key BEFORE|AFTER pivot element // 在元素element前后插入pivot
LREM key count element //根據(jù)參數(shù) COUNT 的值,移除列表中與參數(shù) VALUE 相等的元素 count > 0 : 從表頭開始向表尾搜索,移除與 VALUE 相等的元素,數(shù)量為 COUNT
BLPOP key [key ...] timeout //從key列表表頭彈出一個元素,若列表中沒有元素,阻塞等待 timeout秒,如果timeout=0,一直阻塞等待
BRPOP key [key ...] timeout //從key列表表尾彈出一個元素,若列表中沒有元素,阻塞等待 timeout秒,如果timeout=0,一直阻塞等待
set集合
??列表的典型應(yīng)用場景:
- 抽獎
- 微博點贊,收藏,標簽
- 共同好友
抽獎場景:
- 用戶參與抽獎
# 將用戶10001加入商品a的參與池子中
SADD luckdraw:product:a 10001
- 查看參與商品a抽獎的所有用戶
SMEMBERS luckdraw:product:a
- 抽取1名幸運中獎?wù)?/li>
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有序集合
??列表的典型應(yīng)用場景:
- 微博熱搜榜
- 刷禮物實時排行榜
- 博客社區(qū)本周熱議
Redis有序集合和集合一樣也是string類型元素的集合,且不允許重復(fù)的成員。不同的是每個元素都會關(guān)聯(lián)一個 double類型的分數(shù),Redis正是通過分數(shù)來為集合中的成員進行從小到大的排序。有序集合的成員是唯一的,但分數(shù)score卻可以重復(fù)。下面使用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的數(shù)量必須以numkeys參數(shù)指定,并將該并集 (結(jié)果集) 儲存到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值加上對應(yīng)的增量
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有序集合和對應(yīng)的命令之后,我們來實現(xiàn)本周熱議排行榜功能,博客的本周熱議主要的實現(xiàn)思路是:
- 庫獲取最近 7 天的所有文章(或者加多一個條件:評論數(shù)量大于 0)。
- 把文章的評論數(shù)量作為有序集合的分數(shù)
score,文章的ID作為key存儲到zset中,當有人發(fā)表評論的時候,直接使用命令加一,并重新計算得到排行榜。 - 本周熱議上有標題和評論數(shù)量,因此,我們還需要把文章的基本信息存儲到
Redis中,這樣得到文章的ID之后,我們再從緩存中得到標題等信息,這里我們可以使用hash的結(jié)構(gòu)來存儲文章的信息。 - 因為是本周熱議,如果文章發(fā)表超過 7 天了之后就會失效,所以我們可以給文章的有序集合一個有效時間。超過 7 天之后就自動刪除緩存。
畫圖分析:

最終實現(xiàn)效果:

Bitmaps位圖
??位圖的典型應(yīng)用場景:
- 用戶連續(xù)簽到功能

很多社區(qū)、博客平臺其實都有每日簽到模塊,一開始看到這個模塊需求的時候,很多人第一反應(yīng)是利用MySQL來實現(xiàn),創(chuàng)建一個簽到表,記錄用戶ID和簽到時間,然后統(tǒng)計的時候從數(shù)據(jù)庫中取出來然后聚合計算,這樣設(shè)計其實存在弊端,如我們想要做一些復(fù)雜的功能就不是太方便了,或者說不是太高性能了,比如,今天是連續(xù)簽到的第幾天,在一定時間內(nèi)連續(xù)簽到了多少天。另外一方面,如果按 100 萬用戶量級來計算,一個用戶每年可以產(chǎn)生 365 條記錄,100 萬用戶的所有簽到記錄那就有點恐怖了,查詢計算速度也會越來越慢。其實Redis的Bitmaps位圖操作非常適合處理每日簽到功能場景,因為Bit的值為0或者1,位圖的每一位代表一天的簽到,1表示已簽,0表示未簽。 考慮到每月初需要重置連續(xù)簽到次數(shù),最簡單的方式是按用戶每月存一條簽到數(shù)據(jù)(也可以每年存一條數(shù)據(jù))。Key的格式為u:sign:uid:yyyyMM,Value則采用長度為4個字節(jié)(32位)的位圖(最大月份只有31天)。
Redis位圖命令基本命令
| 命令 | 說明 |
|---|---|
| SETBIT key offset value | 對key所儲存的字符串值,設(shè)置或清除指定偏移量上的位(bit) |
| BITPOS key bit [start] [end] | 查詢指定字節(jié)區(qū)間第一個被設(shè)置成1的bit位的位置 |
| GETBIT key offset | 查詢指定偏移位置的bit值 |
| BITCOUNT key [start end] | 統(tǒng)計指定字節(jié)區(qū)間bit為1的數(shù)量 |
| GETBIT key offset | 查詢指定偏移位置的bit值 |
| BITFIELD key offset | 查詢指定偏移位置的bit值 |
這里的offset,大家姑且當做用戶ID來看就可以了,那么究竟如何去實現(xiàn)用戶打卡功能呢,我們可以利用上面的setbit命令來實現(xiàn),setbit的作用說的直白就是:在你想要的位置操作字節(jié)值,比如說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
# 統(tǒng)計用戶1000在2月份簽到次數(shù)
BITCOUNT u:sign:1000:202302
# 獲取2月份前28天的簽到數(shù)據(jù)
BITFIELD u:sign:1000:202302 get u28 0
# 獲取2月份首次簽到日期
BITPOS u:sign:1000:202302 1 # 返回的首次簽到的偏移量,加上1即為當月的某一天
示例代碼:
/**
* 基于Redis位圖的用戶簽到功能工具實現(xiàn)類
*
* @author: austin
* @since: 2023/2/7 1:50
*/
public class UserSignKit {
private Jedis jedis = new Jedis();
/**
* 用戶簽到
*
* @param uid 用戶ID
* @param date 日期
* @return 之前的簽到狀態(tài)
*/
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 當前的簽到狀態(tài)
*/
public boolean checkSign(int uid, LocalDate date) {
int offset = date.getDayOfMonth() - 1;
return jedis.getbit(buildSignKey(uid, date), offset);
}
/**
* 獲取用戶簽到次數(shù)
*
* @param uid 用戶ID
* @param date 日期
* @return 當前的簽到次數(shù)
*/
public long getSignCount(int uid, LocalDate date) {
return jedis.bitcount(buildSignKey(uid, date));
}
/**
* 獲取當月連續(xù)簽到次數(shù)
*
* @param uid 用戶ID
* @param date 日期
* @return 當月連續(xù)簽到次數(shù)
*/
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) {
// 取低位連續(xù)不為0的個數(shù)即為連續(xù)簽到次數(shù),需考慮當天尚未簽到的情況
long v = list.get(0) == null ? 0 : list.get(0);
for (int i = 0; i < date.getDayOfMonth(); i++) {
if (v >> 1 << 1 == v) {
// 低位為0且非當天說明連續(xù)簽到中斷了
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為簽到狀態(tài)的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("本月簽到次數(shù):" + count);
}
{ // getContinuousSignCount
long count = kit.getContinuousSignCount(1000, today);
System.out.println("連續(xù)簽到次數(shù):" + 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() ? "√" : "-"));
}
}
}
}
運行結(jié)果:
您已簽到:2023-02-07
您已簽到:2023-02-07
本月簽到次數(shù):5
連續(xù)簽到次數(shù):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發(fā)布訂閱
Redis提供了發(fā)布訂閱功能,可以用于消息的傳輸,Redis的發(fā)布訂閱機制包括三個部分:發(fā)布者、訂閱者和Channel。發(fā)布者和訂閱者都是Redis客戶端,Channel則為Redis服務(wù)器端,發(fā)布者將消息發(fā)送到某個的頻道,訂閱了這個頻道的訂閱者就能接收到這條消息。Redis的這種發(fā)布訂閱機制與基于主題的發(fā)布訂閱類似,Channel相當于主題。