1伊滋、String 類型使用場(chǎng)景
- 場(chǎng)景一:商品庫(kù)存數(shù)
從業(yè)務(wù)上,商品庫(kù)存數(shù)據(jù)是熱點(diǎn)數(shù)據(jù)队秩,交易行為會(huì)直接影響庫(kù)存笑旺,而redis自身String類型提供了下面這幾個(gè)命令
incr key && decr key 遞增/遞減
incrby key increment && decrby key decrement 增加/減少指定值
set goods_id 10; 設(shè)置id為good_id的商品的庫(kù)存初始值為10;
decr goods_id ; 當(dāng)商品被購(gòu)買(mǎi)時(shí)馍资,庫(kù)存數(shù)據(jù)減1
依次類推同樣的場(chǎng)景筒主,商品的瀏覽次數(shù),問(wèn)題或者回復(fù)的點(diǎn)贊次數(shù)等鸟蟹,這種計(jì)數(shù)的場(chǎng)景都可以考慮利用Redis 來(lái)實(shí)現(xiàn)
場(chǎng)景二:時(shí)效信息存儲(chǔ)
這個(gè)也就是我們平時(shí)用的最多的場(chǎng)景乌妙,存儲(chǔ)value的時(shí)候設(shè)置過(guò)期時(shí)間,時(shí)間一到建钥,自動(dòng)刪除實(shí)現(xiàn)方式
String在redis內(nèi)部存儲(chǔ)默認(rèn)就是一個(gè)字符串(SDS)藤韵,被redisObject所引用,當(dāng)遇到incr熊经,decr等操作時(shí)會(huì)轉(zhuǎn)成數(shù)值型進(jìn)行計(jì)算泽艘,此時(shí)redisObject的encoding字段為int。
2镐依、List類型使用場(chǎng)景
List是按照插入數(shù)據(jù)排序的字符串鏈表匹涮,可以在頭部和尾部插入新的元素(雙向鏈表實(shí)現(xiàn),兩端添加元素的時(shí)間復(fù)雜度為O(1))
- 場(chǎng)景一:最新上架商品
在交易網(wǎng)站首頁(yè)槐壳,經(jīng)常會(huì)有推薦新上架產(chǎn)品的模塊然低,這個(gè)模塊是存儲(chǔ)了最新上架前100名
這時(shí)候使用Redis的list數(shù)據(jù)結(jié)構(gòu),來(lái)進(jìn)行 TOP 100新上架產(chǎn)品的存儲(chǔ)务唐,下面以偽代碼演示:
// 1雳攘、把新上架的商品添加到鏈表中
ret = r.lpush("new:goods",goodsId)
// 2、通過(guò)ltrim裁減list鏈表绍哎,使之包含 指定范圍內(nèi)的指定元素
r.ltrim("new:goods",0,99)
// 3来农、獲取前100個(gè)最新上架的商品 id 列表
new_goods_list = r.lrange("new:goods",0,99)
- 場(chǎng)景二:消息隊(duì)列實(shí)現(xiàn)
根據(jù)List 鏈表的特性,可以實(shí)現(xiàn)消息隊(duì)列的要求崇堰,當(dāng)然,目前有很多專業(yè)的消息隊(duì)列組件,kafka海诲、rabbitMQ等繁莹;
List 存儲(chǔ)就是一個(gè)隊(duì)列的存儲(chǔ)形式:
1、lpush key value特幔;在key對(duì)應(yīng)list的頭部添加字符串元素咨演;
2、rpop key蚯斯;移除key對(duì)應(yīng)list列表的最后一個(gè)元素薄风,返回值為移除的元素
-
實(shí)現(xiàn)方式
redis 的列表相當(dāng)于 Java 語(yǔ)言里面的 LinkedList,注意它是鏈表而不是數(shù)組拍嵌。這意味著 list 的插入和刪除操作非吃饴福快,時(shí)間復(fù)雜度為 O(1)横辆,但是索引定位很慢撇他,時(shí)間復(fù)雜度為 O(n)。如果再深入一點(diǎn)狈蚤,你會(huì)發(fā)現(xiàn) Redis 底層存儲(chǔ)的還不是一個(gè)簡(jiǎn)單的 linkedlist困肩,而是稱之為快速鏈表 quicklist 的一個(gè)結(jié)構(gòu)。
首先在列表元素較少的情況下會(huì)使用一塊連續(xù)的內(nèi)存存儲(chǔ)脆侮,這個(gè)結(jié)構(gòu)是 ziplist锌畸,也即是壓縮列表。它將所有的元素緊挨著一起存儲(chǔ)靖避,分配的是一塊連續(xù)的內(nèi)存潭枣。
當(dāng)數(shù)據(jù)量比較多的時(shí)候才會(huì)改成 quicklist。因?yàn)槠胀ǖ逆湵硇枰母郊又羔樋臻g太大,會(huì)比較浪費(fèi)空間筋蓖,而且會(huì)加重內(nèi)存的碎片化卸耘。比如這個(gè)列表里存的只是 int 類型的數(shù)據(jù),結(jié)構(gòu)上還需要兩個(gè)額外的指針 prev 和 next 粘咖。所以 Redis 將鏈表和 ziplist 結(jié)合起來(lái)組成了 quicklist蚣抗。也就是將多個(gè) ziplist 使用雙向指針串起來(lái)使用。這樣既滿足了快速的插入刪除性能瓮下,又不會(huì)出現(xiàn)太大的空間冗余翰铡。
3、set類型使用場(chǎng)景
Set也是存儲(chǔ)了一個(gè)集合列表的功能讽坏,但是锭魔,和list不同,set具備去重功能路呜。當(dāng)需要存儲(chǔ)一個(gè)列表信息迷捧,同時(shí)要求列表內(nèi)的元素不能有重復(fù)织咧,這時(shí)候使用set比較適合。與此同時(shí)漠秋,set還可以實(shí)現(xiàn)交集笙蒙、并集、差集庆锦。
例如捅位,在交易網(wǎng)站,我們會(huì)存儲(chǔ)用戶感興趣的商品信息搂抒,在進(jìn)行相似性用戶分析的時(shí)候艇搀,可以通過(guò)計(jì)算兩個(gè)不同用戶之間感興趣商品的數(shù)量來(lái)提供一些依據(jù)。
下面以偽代碼演示:
//userId為用戶id求晶,goodid為感興趣的商品id
sadd "user:userId" goodId;
sadd "user:101" 1;
sadd "user:101" 2;
sadd "user:102" 1;
sadd "user:102" 3;
interResult = sinter "user:101" "user:102" 返回直接定集合的交集焰雕;1
unionResult = sinter "user:101" "user:102" 返回直接定集合的并集;1,2,3
diffResult = sinter "user:101" "user:102" 返回直接定集合的差集誉帅;2,3
獲取到兩個(gè)用戶相似的產(chǎn)品淀散,然后確定相似產(chǎn)品的類目就可以進(jìn)行用戶分析。
類似的場(chǎng)景還有蚜锨,社交場(chǎng)景下共同關(guān)注好友档插,相似興趣tag 等場(chǎng)景的支持。
- 實(shí)現(xiàn)方式
Redis 的集合相當(dāng)于 Java 語(yǔ)言里面的 HashSet亚再,它內(nèi)部的鍵值對(duì)是無(wú)序的唯一的郭膛。它的內(nèi)部實(shí)現(xiàn)相當(dāng)于一個(gè)特殊的字典,字典中所有的 value 都是一個(gè)值NULL氛悬。
4则剃、Hash類型使用場(chǎng)景
Redis在存儲(chǔ)對(duì)象(例如,用戶信息)的時(shí)候如捅,需要對(duì)對(duì)象進(jìn)行系列化轉(zhuǎn)換然后進(jìn)行存儲(chǔ)棍现。
還有一種形式,就是將對(duì)象數(shù)據(jù)轉(zhuǎn)換為JSON結(jié)構(gòu)數(shù)據(jù)镜遣,然后存儲(chǔ)json字符串到Redis己肮。
其實(shí),對(duì)于一些對(duì)象類型悲关,還有一種比較方便的類型谎僻,那就是按照Redis的Hash類型進(jìn)行存儲(chǔ)
例如,我們存儲(chǔ)一些網(wǎng)站用戶的基本信息寓辱,我們可以使用
hset key field value
hset user101 name "小明"
hset user101 sex "男"
hset user101 phone "123456"
這樣我們就存儲(chǔ)了一個(gè)用戶的基本信息艘绍,存儲(chǔ)信息有{name : 小明,sex : 男,phone : 123456}
當(dāng)然這種類似場(chǎng)景還非常多,比如存儲(chǔ)訂單的數(shù)據(jù),產(chǎn)品的數(shù)據(jù)志于,商家基本信息等桐臊。
- 實(shí)現(xiàn)方式
Redis 的字典相當(dāng)于 Java 語(yǔ)言里面的 HashMap鸳址,它是無(wú)序字典。內(nèi)部實(shí)現(xiàn)結(jié)構(gòu)上同 Java 的 HashMap 也是一致的传轰,同樣的數(shù)組 + 鏈表二維結(jié)構(gòu)绳慎。第一維 hash 的數(shù)組位置碰撞時(shí),就會(huì)將碰撞的元素使用鏈表串接起來(lái)敷存。
5、Sorted Set 類型使用場(chǎng)景
Redis sorted set的使用場(chǎng)景與set類似堪伍,區(qū)別是set不是自動(dòng)有序锚烦,而sorted set可以通過(guò)提供一個(gè)score參數(shù)來(lái)為存儲(chǔ)數(shù)據(jù)排序,并且是自動(dòng)排序帝雇,插入即有序涮俄。
業(yè)務(wù)中,如果需要一個(gè)有序且不重復(fù)的集合列表尸闸,就可以選擇sorted set這種數(shù)據(jù)結(jié)構(gòu)
比如彻亲,商品的購(gòu)買(mǎi)熱度可以將購(gòu)買(mǎi)總量num當(dāng)做商品列表的score,這樣獲取最熱門(mén)的商品時(shí)吮廉,就可以自動(dòng)按售賣(mài)總量排好序苞尝。
sorted set適合有排序需求的集合存儲(chǔ)場(chǎng)景。
- 實(shí)現(xiàn)方式
有序集合的編碼可以是ziplist和skiplist之一宦芦。
有序集合對(duì)象使用ziplist編碼需要滿足兩個(gè)條件:一是所有元素長(zhǎng)度小于64字節(jié)宙址;二是元素個(gè)數(shù)小于128個(gè);不滿足任意一條件將使用skiplist編碼调卑。
以上兩個(gè)條件可以在Redis配置文件中修改zset-max-ziplist-entries選項(xiàng)和zset-max-ziplist-value選項(xiàng)抡砂。ziplist編碼結(jié)構(gòu)如下
skiplist編碼的有序集合對(duì)象底層實(shí)現(xiàn)是跳躍表和字典兩種:
一個(gè)是 dict(字典),key是成員恬涧,value是分值注益,用于支持 O(1) 復(fù)雜度的按成員取分值操作;
一個(gè)是 skiplist(跳躍表)溯捆,按分值排序成員丑搔,用于支持平均復(fù)雜度為O(log N)的按分值定位成員的操作,以及范圍操作现使;
備注:上面提到的ziplist quicklist skiplist數(shù)據(jù)結(jié)構(gòu)詳解見(jiàn)Redis(七):Redis底層數(shù)據(jù)類型
6低匙、分布式環(huán)境下常見(jiàn)的應(yīng)用場(chǎng)景之分布式鎖
當(dāng)多個(gè)進(jìn)程不在同一個(gè)系統(tǒng)中時(shí),用分布式鎖控制多個(gè)進(jìn)程對(duì)資源的操作或者訪問(wèn)碳锈。
分布式鎖可以避免不同進(jìn)程重復(fù)相同的工作顽冶,減少資源浪費(fèi)。同時(shí)分布式鎖可以避免破壞數(shù)據(jù)正確性的發(fā)生售碳,例如多個(gè)進(jìn)程對(duì)同一個(gè)訂單操作强重,可以導(dǎo)致訂單狀態(tài)錯(cuò)誤覆蓋绞呈。。间景。
-
定時(shí)任務(wù)重復(fù)執(zhí)行
隨著業(yè)務(wù)的發(fā)展佃声,業(yè)務(wù)系統(tǒng)勢(shì)必發(fā)展為集群分布式模式,如果我們需要一個(gè)定時(shí)任務(wù)來(lái)進(jìn)行訂單狀態(tài)的統(tǒng)計(jì)倘要,比如圾亏,每15分鐘統(tǒng)計(jì)一下所有未支付的訂單數(shù)量,那么我們啟動(dòng)定時(shí)任務(wù)的時(shí)候封拧,肯定不能同一時(shí)刻多個(gè)業(yè)務(wù)后臺(tái)服務(wù)都去執(zhí)行定時(shí)任務(wù)志鹃,這樣就會(huì)帶來(lái)重復(fù)計(jì)算以及業(yè)務(wù)邏輯混亂的問(wèn)題。這時(shí)候泽西,就需要使用分布式鎖曹铃,進(jìn)行資源的鎖定。那么在執(zhí)行定時(shí)任務(wù)的函數(shù)中捧杉,首先進(jìn)行分布式鎖的獲取陕见,如果可以獲取的到,那么這臺(tái)機(jī)器就執(zhí)行正常的業(yè)務(wù)數(shù)據(jù)統(tǒng)計(jì)邏輯計(jì)算味抖,如果獲取不到則證明目前已有其他的服務(wù)進(jìn)程執(zhí)行這個(gè)定時(shí)任務(wù)评甜,就不用自己操作執(zhí)行了,只需要返回就行了非竿,如下圖所示:
上面的這種業(yè)務(wù)場(chǎng)景蜕着,也可以使用分布式任務(wù)調(diào)度框架xxjob來(lái)實(shí)現(xiàn)。
- 避免用戶重復(fù)下單
分布式鎖的實(shí)現(xiàn)方式有很多種:
1红柱、數(shù)據(jù)庫(kù)樂(lè)觀鎖方式(數(shù)據(jù)庫(kù)加一個(gè)版本號(hào))
2承匣、基于Redis的分布式鎖
3、基于ZK的分布式鎖(Zookeeper基礎(chǔ)(五):分布式鎖)
分布式鎖的實(shí)現(xiàn)要保證幾個(gè)基本點(diǎn):
1锤悄、互斥性:任意時(shí)刻韧骗,只有一個(gè)資源能夠獲取到鎖
2、容災(zāi)性:能夠在未成功釋放鎖的情況下零聚,一定時(shí)限內(nèi)能夠恢復(fù)鎖的正常功能
3袍暴、統(tǒng)一性:加鎖和解鎖保證同一資源來(lái)進(jìn)行操作
7、分布式環(huán)境下常見(jiàn)的應(yīng)用場(chǎng)景之分布式自增id
隨著用戶以及交易量的增加隶症,我們可能會(huì)針對(duì)用戶數(shù)據(jù)政模,商品數(shù)據(jù),以及訂單數(shù)據(jù)進(jìn)行分庫(kù)發(fā)表的操作蚂会,這時(shí)候由于進(jìn)行了分庫(kù)分表的行為淋样,所以mysql自增id的形式來(lái)唯一表示一行數(shù)據(jù)的方案不可行了,因此需要一個(gè)分布式id生成器胁住,來(lái)提供唯一id的信息趁猴。
通常對(duì)于分布式自增id的實(shí)現(xiàn)方式有下面幾種:
1刊咳、利用數(shù)據(jù)庫(kù)自增id的屬性
2、通過(guò)uuid來(lái)實(shí)現(xiàn)唯一id生產(chǎn)
3儡司、Twitter的SnowFlake算法
4娱挨、利用Redis生成唯一id
我們使用redis的incr命令來(lái)實(shí)現(xiàn)唯一id,因?yàn)镽edis是單進(jìn)程單線程架構(gòu)捕犬,不會(huì)因?yàn)槎鄠€(gè)取號(hào)方的incr命令導(dǎo)致取號(hào)重復(fù)跷坝,因此,基于Redis的incr命令實(shí)現(xiàn)序列號(hào)的生成基本能滿足全局唯一與單調(diào)自增的特性
8或听、Redis發(fā)布訂閱使用應(yīng)用場(chǎng)景
Redis有一個(gè)發(fā)布訂閱的通信方式探孝,發(fā)送者publish發(fā)送消息,訂閱者subscribe接收消息誉裆。
下圖展示了頻道 channel1 , 以及訂閱這個(gè)頻道的三個(gè)客戶端 —— client2 缸濒、 client5 和 client1 之間的關(guān)系:
當(dāng)有新消息通過(guò) PUBLISH 命令發(fā)送給頻道 channel1 時(shí)足丢, 這個(gè)消息就會(huì)被發(fā)送給訂閱它的三個(gè)客戶端:
監(jiān)聽(tīng)
/**
* 訂閱者
*/
public class RedisSubTest {
@Test
public void subjava() {
System.out.println("訂閱者 ");
Jedis jr = null;
try {
jr = new Jedis("127.0.0.1", 6379, 0);// redis服務(wù)地址和端口號(hào)
// redis發(fā)布訂閱消息監(jiān)聽(tīng)器 需要繼承JedisPubSub;當(dāng)然也可以像下面這種寫(xiě)法
JedisPubSub jedisPubSub = new JedisPubSub(){
@Override //收到消息會(huì)調(diào)用
public void onMessage(String channel, String message) {
System.out.println(String.format("receive redis published message, channel %s, message %s", channel, message));
}
@Override //訂閱了頻道會(huì)調(diào)用
public void onSubscribe(String channel, int subscribedChannels) {
System.out.println(String.format("subscribe redis channel success, channel %s, subscribedChannels %d",
channel, subscribedChannels));
}
@Override //取消訂閱 會(huì)調(diào)用
public void onUnsubscribe(String channel, int subscribedChannels) {
System.out.println(String.format("unsubscribe redis channel, channel %s, subscribedChannels %d",
channel, subscribedChannels));
}
};
// jr客戶端配置監(jiān)聽(tīng)兩個(gè)channel
jr.subscribe(jedisPubSub, "news.share", "news.blog");
} catch (Exception e) {
e.printStackTrace();
} finally {
if (jr != null) {
jr.disconnect();
}
}
}
}
/**
* 發(fā)布者
*/
public class RedisPubTest {
@Test
public void pubjava() {
System.out.println("發(fā)布者 ");
Jedis jr = null;
try {
jr = new Jedis("127.0.0.1", 6379, 0);// redis服務(wù)地址和端口號(hào)
// jr客戶端配置監(jiān)聽(tīng)兩個(gè)channel
jr.publish( "news.share", "新聞分享");
jr.publish( "news.blog", "新聞博客");
} catch (Exception e) {
e.printStackTrace();
} finally {
if (jr != null) {
jr.disconnect();
}
}
}
}
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.9.0</version>
</dependency>
9庇配、key設(shè)計(jì)原則
9.1 key名設(shè)計(jì)
- 可讀性和可管理性【建議】
以業(yè)務(wù)名(或數(shù)據(jù)庫(kù)名)為前綴(防止key沖突)斩跌,用冒號(hào)分隔,比如業(yè)務(wù)名:表名:id
ugc:video:1
用冒號(hào)作為分割是設(shè)計(jì)key的一種不成文的原則捞慌,遵循這種格式設(shè)計(jì)出的key在某些redis客戶端下可以有效的識(shí)別
- 簡(jiǎn)潔性【建議】
保證語(yǔ)義的前提下耀鸦,控制key的長(zhǎng)度,當(dāng)key較多時(shí)啸澡,內(nèi)存占用也不容忽視袖订,例如:
user:{uid}:friends:messages:{mid}
簡(jiǎn)化為
u:{uid}:fr:m:{mid}。
- 不要包含特殊字符【強(qiáng)制】
反例:包含空格嗅虏、換行洛姑、單雙引號(hào)以及其他轉(zhuǎn)義字符
9.2 value設(shè)計(jì)
-
拒絕bigkey(防止網(wǎng)卡流量、慢查詢)
string類型控制在10KB以內(nèi)皮服,hash楞艾、list、set龄广、zset元素個(gè)數(shù)不要超過(guò)5000硫眯。反例:一個(gè)包含200萬(wàn)個(gè)元素的list。
非字符串的bigkey择同,不要使用del刪除两入,使用hscan、sscan奠衔、zscan方式漸進(jìn)式刪除谆刨。
注意: 當(dāng)bigkey過(guò)期后塘娶,會(huì)被刪除,如果沒(méi)有使用Redis 4.0版本以上的過(guò)期異步刪除(lazyfree-lazy-expire yes)痊夭,就會(huì)存在阻塞Redis的可能性刁岸。
因?yàn)椴辉O(shè)置為異步刪除的話,會(huì)同步刪除她我,觸發(fā)del操作虹曙,造成阻塞,而且該操作不會(huì)不出現(xiàn)在慢查詢中(latency可查)
對(duì)于4.0以上的版本番舆,默認(rèn)是開(kāi)啟異步刪除的酝碳,即lazyfree-lazy-expire=yes
//1. Hash刪除: hscan + hdel
public void delBigHash(String host, int port, String password, String bigHashKey) {
Jedis jedis = new Jedis(host, port);
if (password != null && !"".equals(password)) {
jedis.auth(password);
}
ScanParams scanParams = new ScanParams().count(100);
String cursor = "0";
do {
ScanResult<Entry<String, String>> scanResult = jedis.hscan(bigHashKey, cursor, scanParams);
List<Entry<String, String>> entryList = scanResult.getResult();
if (entryList != null && !entryList.isEmpty()) {
for (Entry<String, String> entry : entryList) {
jedis.hdel(bigHashKey, entry.getKey());
}
}
cursor = scanResult.getStringCursor();
} while (!"0".equals(cursor));
//刪除bigkey
jedis.del(bigHashKey);
}
//2. List刪除: ltrim
public void delBigList(String host, int port, String password, String bigListKey) {
Jedis jedis = new Jedis(host, port);
if (password != null && !"".equals(password)) {
jedis.auth(password);
}
long llen = jedis.llen(bigListKey);
int counter = 0;
int left = 100;
while (counter < llen) {
//每次從左側(cè)截掉100個(gè)
jedis.ltrim(bigListKey, left, llen);
counter += left;
}
//最終刪除key
jedis.del(bigListKey);
}
//3. Set刪除: sscan + srem
public void delBigSet(String host, int port, String password, String bigSetKey) {
Jedis jedis = new Jedis(host, port);
if (password != null && !"".equals(password)) {
jedis.auth(password);
}
ScanParams scanParams = new ScanParams().count(100);
String cursor = "0";
do {
ScanResult<String> scanResult = jedis.sscan(bigSetKey, cursor, scanParams);
List<String> memberList = scanResult.getResult();
if (memberList != null && !memberList.isEmpty()) {
for (String member : memberList) {
jedis.srem(bigSetKey, member);
}
}
cursor = scanResult.getStringCursor();
} while (!"0".equals(cursor));
//刪除bigkey
jedis.del(bigSetKey);
}
//4. SortedSet刪除: zscan + zrem
public void delBigZset(String host, int port, String password, String bigZsetKey) {
Jedis jedis = new Jedis(host, port);
if (password != null && !"".equals(password)) {
jedis.auth(password);
}
ScanParams scanParams = new ScanParams().count(100);
String cursor = "0";
do {
ScanResult<Tuple> scanResult = jedis.zscan(bigZsetKey, cursor, scanParams);
List<Tuple> tupleList = scanResult.getResult();
if (tupleList != null && !tupleList.isEmpty()) {
for (Tuple tuple : tupleList) {
jedis.zrem(bigZsetKey, tuple.getElement());
}
}
cursor = scanResult.getStringCursor();
} while (!"0".equals(cursor));
//刪除bigkey
jedis.del(bigZsetKey);
}
- 選擇適合的數(shù)據(jù)類型【推薦】
例如:實(shí)體類型(要合理控制和使用數(shù)據(jù)結(jié)構(gòu)內(nèi)存編碼優(yōu)化配置,例如ziplist,但也要注意節(jié)省內(nèi)存和性能之間的平衡)
//反例:
set user:1:name tom
set user:1:age 19
set user:1:favor football
//正例:
hmset user:1 name tom age 19 favor football
- 控制key的生命周期恨狈,redis不是垃圾桶【推薦】
建議使用expire設(shè)置過(guò)期時(shí)間(條件允許可以打散過(guò)期時(shí)間疏哗,防止集中過(guò)期),不過(guò)期的數(shù)據(jù)重點(diǎn)關(guān)注idletime禾怠。