> **Redis 場景使用**
[TOC]
## 說明 ##
**最近在整理Redis的一些使用場景, 包含我在工作當中的例子和網上看到比較好的文章。**
----------
##參考文章##
> 平凡希:https://www.cnblogs.com/xiaoxi/p/7007695.html
## String ##
<h5>介紹</h5>
String 數據結構是簡單的key-value類型,value其實不僅是String,也可以是數字。
常用命令:get、set、incr、decr、mget等。
應用場景:String是最常用的一種數據類型,普通的key/ value 存儲都可以歸為此類,即可以完全實現目前 Memcached 的功能,并且效率更高。還可以享受Redis的定時持久化,操作日志及 Replication等功能。除了提供與 Memcached 一樣的get、set、incr、decr 等操作外,Redis還提供了下面一些操作:
獲取字符串長度
往字符串append內容
設置和獲取字符串的某一段內容
設置及獲取字符串的某一位(bit)
批量設置一系列字符串的內容
使用場景:常規key-value緩存應用。常規計數: 微博數, 粉絲數。
實現方式:String在redis內部存儲默認就是一個字符串,被redisObject所引用,當遇到incr,decr等操作時會轉成數值型進行計算,此時redisObject的encoding字段為int。
<h5>存儲信息1</h5>
用戶ID為查找的key,存儲的value用戶對象包含姓名,年齡,生日等信息,如果用普通的key/value結構來存儲,主要有以下2種存儲方式:
![請輸入圖片描述][1]
將用戶相關的信息轉換為JSON字符串,存儲在string類型中
$userInfo=['name'=>'depp', 'age'=>'25','sex'=>'age',];
Redis::set("user:1",json_encode($userInfo));
dd(json_decode(Redis::get("user:1")));
第一種方式將用戶ID作為查找key,把其他信息封裝成一個對象以序列化的方式存儲,這種方式的缺點是,增加了序列化/反序列化的開銷,并且在需要修改其中一項信息時,需要把整個對象取回,并且修改操作需要對并發進行保護,引入CAS等復雜問題。
<h5>存儲信息2</h5>
第二種方法是這個用戶信息對象有多少成員就存成多少個key-value對兒,用用戶ID+對應屬性的名稱作為唯一標識來取得對應屬性的值,雖然省去了序列化開銷和并發問題,但是用戶ID為重復存儲,如果存在大量這樣的數據,內存浪費還是非常可觀的
![請輸入圖片描述][2]
$shopConfing =[
's:1:mset'=>1, //是否開啟會員體系
's:1:svset'=>1, //是否開啟短視頻功能
's:1:lset'=>1, //是否開始直播功能
];
Redis::mset($shopConfing);
dd(Redis::keys("shop:1:*"));
<h5>計數器</h5>
Redis string提供的incr 跟 decr 方法也可以實現簡單的計數器功能。
Redis::set("v:z:1",1); //設置點贊
//增加點贊數
Redis::incr("v:z:1",1);
//得到點贊數
dd(Redis::get("v:z:1"));
> 其他計數器,統計數類似場景:微博的評論數、點贊數、分享數,抖音作品的收藏數,京東商品的銷售量、評價數等
<h5>短信驗證碼小例子</h5>
//發送短信證碼
$code=rand(1000,9999);
// 發送短信處理
/** send message **/
//存儲驗證碼
Redis::setex("code:17600128033","120",$code);
// 接收驗證碼
$inCode="7580";
$mobileNumber="17600128033";
if($inCode == Redis::get("code:".$mobileNumber)){
dd ("驗證碼正確");
}else{
dd ("驗證碼錯誤,驗證碼為:".Redis::get("code:".$mobileNumber));
}
## Hash ##
Redis hash 是一個 string 類型的 field(字段) 和 value(值) 的映射表,hash 特別適合用于存儲對象。
常用命令:hget,hset,hgetall 等。
<h5>存儲信息3</h5>
除了上述string 存儲信息的方式外,我們還可以用Hash類型來存儲。
Redis的Hash實際是內部存儲的Value為一個HashMap,并提供了直接存取這個Map成員的接口,使得Hash既沒有序列化開銷和并發問題,用戶ID也不會重復存儲,非常適合存儲對象。
![請輸入圖片描述][3]
Redis::hset('user:1','name','zhangsan');
Redis::hset('user:1','sex','男');
Redis::hset('user:1','age','20');
dd(Redis::hgetall('user:1'));
<h5>注意點</h5>
這里同時需要注意,Redis提供了接口(hgetall)可以直接取到全部的屬性數據,但是如果內部Map的成員很多,那么涉及到遍歷整個內部Map的操作,由于Redis單線程模型的緣故,這個遍歷操作可能會比較耗時,而另其它客戶端的請求完全不響應,這點需要格外注意。
使用場景:存儲部分變更數據,如用戶信息等。
實現方式:
上面已經說到Redis Hash對應Value內部實際就是一個HashMap,實際這里會有2種不同實現,這個Hash的成員比較少時Redis為了節省內存會采用類似一維數組的方式來緊湊存儲,而不會采用真正的HashMap結構,對應的value redisObject的encoding為zipmap,當成員數量增大時會自動轉成真正的HashMap,此時encoding為ht。
<h5>計數、統計</h5>
同時Hash類型靈活的結構也適合給某一類事物進行各種維度的計數。
例如:電商類:商品維度有各種計數(點贊數,評論數,瀏覽數)
例如:直播類:主播維度有各種計數(動態數、關注數、粉絲數)
Redis::hset('video:1','likes',1); //視頻點贊數
Redis::hset('video:1','collections',1); //視頻收藏
//增加視頻、點贊
Redis::hIncrBy('video:1','likes',1);
Redis::hIncrBy('video:1','collections',1);
//減少點贊數
Redis::hIncrBy('video:1','likes',-1);
//獲取點贊數
Redis::hget('video:1','goods:4');
<h5>簡單購物車</h5>
簡單的購物車功能就可以使用Hash結構快速實現。以用戶id為key,商品id為field,商品數量為value,恰好構成了購物車的3個要素,如下圖所示。
![請輸入圖片描述][4]
Redis::hset('card:user:1','goods:1','1'); //用戶1 增加1個商品1到購物車
Redis::hset('card:user:1','goods:2','2'); //用戶2 增加2個商品2到購物車
Redis::hset('card:user:2','goods:1','2'); //用戶2 增加2個商品1到購物車
//添加商品購物車
Redis::hset('card:user:1','goods:4','2');
//獲取購物車內容
Redis::hgetall('card:user:1');
//增加數量
Redis::hIncrBy('card:user:1','goods:4','2');
//減少數量
Redis::hIncrBy('card:user:1','goods:4','-2');
//刪除一個商品
Redis::hdel('card:user:1','goods:4');
//清空購物車
Redis::del('card:user:1');
<h5>hash 和 string的選擇</h5>
string 和 hash 兩種類型都可以用作對象存儲。以下的思路可以供大家參考。
當一個對象的屬性相對整體而且而且不易變化時,比較適合用string存儲
比如:
用戶:姓名、年齡、地址、愛好、民族、已婚等等
主播:房間號、姓名、年齡、直播領域
----------
當對象的某個屬性需要頻繁修改,且屬性比較零散時,比較適合用hash存儲
比如:
用戶:喜歡的視頻數、喜歡的商品數、點贊數
主播:粉絲數、訂閱數
## List ##
<h5>介紹</h5>
常用命令:lpush,rpush,lpop,rpop,lrange等。
應用場景:
Redis list的應用場景非常多,也是Redis最重要的數據結構之一,比如twitter的關注列表,粉絲列表等都可以用Redis的list結構來實現。
List 就是鏈表,相信略有數據結構知識的人都應該能理解其結構。使用List結構,我們可以輕松地實現最新消息排行等功能。List的另一個應用就是消息隊列,
可以利用List的PUSH操作,將任務存在List中,然后工作線程再用POP操作將任務取出進行執行。Redis還提供了操作List中某一段的api,你可以直接查詢,刪除List中某一段的元素。
實現方式:
Redis list的實現為一個雙向鏈表,即可以支持反向查找和遍歷,更方便操作,不過帶來了部分額外的內存開銷,Redis內部的很多實現,包括發送緩沖隊列等也都是用的這個數據結構。
Redis的list是每個子元素都是String類型的雙向鏈表,可以通過push和pop操作從列表的頭部或者尾部添加或者刪除元素,這樣List即可以作為棧,也可以作為隊列。
<h5>消息隊列系統</h5>
使用list可以構建隊列系統,使用sorted set甚至可以構建有優先級的隊列系統。
比如:將Redis用作日志收集器
實際上還是一個隊列,多個端點將日志信息寫入Redis,然后一個worker統一將所有日志寫到磁盤。
取最新N個數據的操作
記錄前N個最新登陸的用戶Id列表,超出的范圍可以從數據庫中獲得。
//把當前登錄人添加到鏈表里
for ($a=1;$a<20;$a++){
$ret = Redis::lpush("l:uid", $a); //login:user_id
}
//保持鏈表只有10位
$ret = Redis::ltrim("l:uid", 0, (10-1));
//獲得前10個最新登陸的用戶Id列表
dd(Redis::lrange("l:uid", 0, (10-1)));
結果:
array:10 [▼
0 => "19"
1 => "18"
2 => "17"
3 => "16"
4 => "15"
5 => "14"
6 => "13"
7 => "12"
8 => "11"
9 => "10"
]
<h5>sina微博熱數據:</h5>
在Redis中我們的最新微博ID使用了常駐緩存,這是一直更新的。但是我們做了限制不能超過5000個ID,因此我們的獲取ID函數會一直詢問Redis。只有在start/count參數超出了這個范圍的時候,才需要去訪問數據庫。
我們的系統不會像傳統方式那樣“刷新”緩存,Redis實例中的信息永遠是一致的。SQL數據庫(或是硬盤上的其他類型數據庫)只是在用戶需要獲取“很遠”的數據時才會被觸發,而主頁或第一個評論頁是不會麻煩到硬盤上的數據庫了。
<h5>秒殺場景的簡單實現</h5>
隊列的特性在日常開發中還可以用于流量削鋒跟解耦。這里做流量削鋒的秒殺搶購場景的簡單示例。
//監聽已搶購的數量
Redis::watch("sk:1:num"); //已經秒殺完的商品數量
$skNum = Redis::hget("sk:h:1",'gum'); //秒殺商品Hash信息
$isSkNum = (int)Redis::get("sk:1:num");
if($isSkNum < $skNum ){
$uid = $this->rand(5);//隨機生成用戶id
// 暫時用setnx 跟 expire 處理限購 問題是并發情況下會不止10個人進來,但是不影響限購啊
// 沒有考慮到更好的解決方案,先這么處理吧
// Redis::set("sk:su:1", 1 , 'NX', 'EX', "1000"); 不清楚predis下set 同時設置 NX和EX為什么老是不生效,暫時用下邊的方法處理
if(Redis::setnx("sk:su:".$uid,$uid)){
Redis::expire("sk:su:".$uid,10); //設置過期時間,保證10秒內一個用戶只能秒殺成功一次
}else{
Rddis::incr('fail');
echo "10秒內允許搶購一次。";
}
//上述代碼不能放在multi之內。 否則if(Redis::setnx("sk:su:".$uid,$uid)) 會報錯
//放在multi當中,相當于未執行,結果不會返回,所以會一直報錯
Redis::multi();
Redis::incr('sk:1:num');
Redis::lpush("sk:l:1",$uid);
Redis::exec();
}else{
Redis::incr('fail');
echo "不好意思,秒殺已經結束了。";
}
----------
ab 1000請求 100并發的請求結果:
127.0.0.1:6379> get sk:1:num
"10"
127.0.0.1:6379> lrange sk:l:1 0 -1
1) "t106A0502910239"
2) "160A50HR21k23R9"
3) "160i7O502f12Y39"
4) "160j5021W23UzZ9"
5) "Wd160c5i0212539"
6) "1Pw60I502Ae1239"
7) "146050RgG213239"
8) "1605p0aL212p3u9"
9) "16050wyAa2123Y9"
10) "16050fi2012f3j9"
<h5>熱點數據更新</h5>
場景描述:后臺在更新咨詢。前臺用戶在觀看數據。
比如說數據A B C D E F G,一次取兩條數據的, 用戶的進來是A B兩條數據,此時后臺運營人員在后臺插入H F兩條數據,此時鏈表變成了H F A B C D E F G,此時用戶往上滑如何保證是C D
記錄用戶后一個值,傳到后臺,不能下標后移處理。
## 結尾 ##
<p style="background-image: -webkit-linear-gradient(left, #3498db, #f47920 10%, #d71345 20%, #f7acbc 30%,#ffd400 40%, #3498db 50%, #f47920 60%, #d71345 70%, #f7acbc 80%, #ffd400 90%, #3498db);color: transparent;-webkit-text-fill-color: transparent;-webkit-background-clip: text;text-align:center;">
腹有詩書氣自華,最是書香能致遠。
</p>
[1]: https://blog.zxliu.cn/usr/uploads/2020/11/217422969.png
[2]: https://blog.zxliu.cn/usr/uploads/2020/11/1945224260.png
[3]: https://blog.zxliu.cn/usr/uploads/2020/11/1321091741.png
[4]: https://blog.zxliu.cn/usr/uploads/2020/11/2167948208.png