這一年以來(lái),寫(xiě)了太多的業(yè)務(wù)代碼。是時(shí)候要總結(jié)一下自己的積累了。本文是redis深度歷險(xiǎn)的讀書(shū)筆記,做個(gè)記錄以及分享給大家。
docker redis redis插件加載
數(shù)據(jù)結(jié)構(gòu)
字符串
字符串是一個(gè)字符數(shù)組
常見(jiàn)用途就是信息JSON序列化成為字符串之后,存入redis,取信息會(huì)經(jīng)過(guò)一次反序列化
字符串是動(dòng)態(tài)字符串,可以進(jìn)行擴(kuò)容;當(dāng)字符串長(zhǎng)度小于1m的時(shí)候,擴(kuò)容是加倍。當(dāng)大于1m,每次擴(kuò)容就增加1m。字符串最大的長(zhǎng)度為512m
使用字符串計(jì)數(shù)的時(shí)候,單個(gè)值的最大值為signed long,超過(guò)就會(huì)報(bào)錯(cuò)
-
常用命令
set name a get name exists a del a mset a 1 b 2 mget a b expire a 5 setex a 5 1 setnx a 1 incr a incrby a 10
列表
-
相當(dāng)于java中的LinkedList,一個(gè)雙向鏈表。插入和刪除操作快,但是索引定位很慢。
這里幫助大家回憶一下普通節(jié)點(diǎn)刪除操作 p.pre.next = p.next p.next.pre = p.pre p = null 不需要對(duì)節(jié)點(diǎn)進(jìn)行移動(dòng) -
列表可以作為隊(duì)列,右進(jìn)左出即可
rpush books java python golang kotlin 4 llen books 4 lpop books java -
列表可以作為棧,右進(jìn)右出即可。就是,調(diào)整了隊(duì)列使用的方向
rpush books java python golang kotlin 4 llen books 4 rpop books kotlin -
使用隊(duì)列會(huì)存在的慢操作有g(shù)et(index)方法,需要遍歷整個(gè)鏈表,性能為O(n)
rpush books java python golang kotlin 4 lindex 1 # 時(shí)間為O(n) lindex books 1 "python" lrange books 0 -1 "java" "python" "golang" "kotlin" ltrim books 1 -1 # 設(shè)置長(zhǎng)度為3的數(shù)組,區(qū)間外的全部被清除 OK lrange books 0 -1 # 獲取所有元素 "python" "golang" "kotlin" llen books 3 ltrim books 1 0 # 刪除所有元素 OK 快速列表:在列表元素較少的時(shí)候,會(huì)使用一塊連續(xù)的內(nèi)存,叫ziplist(壓縮列表)。當(dāng)數(shù)據(jù)量較多的時(shí)候,就要變成quicklist(快速列表),多個(gè)ziplist使用雙向指針串起來(lái)使用。
// 快速列表
struct quicklist {
quicklistNode* head;
quicklistNode* tail;
long count; // 元素總數(shù)
int nodes; // ziplist 節(jié)點(diǎn)的個(gè)數(shù)
int compressDepth; // LZF 算法壓縮深度
...
}
// 快速列表節(jié)點(diǎn)
struct quicklistNode {
quicklistNode* prev;
quicklistNode* next;
ziplist* zl; // 指向壓縮列表
int32 size; // ziplist 的字節(jié)總數(shù)
int16 count; // ziplist 中的元素?cái)?shù)量
int2 encoding; // 存儲(chǔ)形式 2bit,原生字節(jié)數(shù)組還是 LZF 壓縮存儲(chǔ)
}
struct ziplist_compressed {
int32 size;
byte[] compressed_data;
}
struct ziplist {
...
}
Hash字典
內(nèi)部實(shí)現(xiàn)結(jié)構(gòu)就是數(shù)組+鏈表的方式進(jìn)行實(shí)現(xiàn)。相同hash值的元素使用鏈表串聯(lián)起來(lái)。java的鏈表數(shù)量超過(guò)6個(gè)會(huì)變成紅黑樹(shù),不知道redis的會(huì)不會(huì)有這個(gè)查詢(xún)優(yōu)化。
redis里面的值就只能是字符串,便于比較。不像java的hashmap可以存儲(chǔ)對(duì)象,equals方法的編寫(xiě)可謂五花八門(mén)
-
java的hashmap在進(jìn)行rehash的時(shí)候,需要一次性進(jìn)行rehash,是個(gè)耗時(shí)操作,多線程還會(huì)出現(xiàn)環(huán)。redis的是漸進(jìn)式rehash。rehash的時(shí)候,會(huì)保留兩個(gè)hash結(jié)構(gòu),查詢(xún)時(shí)候會(huì)查詢(xún)兩個(gè)hash結(jié)構(gòu),在后續(xù)的定時(shí)任務(wù)和hash指令操作的時(shí)候,漸進(jìn)地將舊的內(nèi)容遷移到新的hash結(jié)構(gòu)中。當(dāng)內(nèi)容遷移完畢,刪除舊的hash結(jié)構(gòu)
hset books java "think in java" 1 hset books golang "concurrency in go" 1 hset books python "python cookbook" 1 hgetall books # 返回所有entryies,key和value間隔返回 "java" "think in java" "golang" "concurrency in go" "python" "python cookbook" hlen books 3 hget books java "think in java" hset books java "my java book" 0 hmset books java "java" golang "golang" ok hset user-lixinkun age 25 1 hincrby user-lixinkun 1 26
set
-
相當(dāng)于java的hashset,確保唯一性
sadd books java 1 sadd books java 0 sadd books python golang smembers books java golang python #順序可能被打亂,輸出結(jié)果是無(wú)序的
Zset 有序列表
類(lèi)似于java的sortedset和hashmap的結(jié)合體。具備set的唯一性原理,也可以給每個(gè)值賦一個(gè)score,代表它的排序權(quán)重。它的內(nèi)部實(shí)現(xiàn)是一個(gè)跳躍列表。貌似java也有這個(gè)數(shù)據(jù)結(jié)構(gòu)
內(nèi)部排序通過(guò)skiplist實(shí)現(xiàn)。zset要支持睡覺(jué)的插入和刪除,不能使用數(shù)組來(lái)表示。普通的鏈表數(shù)據(jù)結(jié)構(gòu),有新元素插入的時(shí)候,需要進(jìn)行定位,這樣才能保證是有序的。通常會(huì)通過(guò)二分查找來(lái)確定位置,但是只有數(shù)組才支持快速查找。這時(shí)候就需要一個(gè)多層級(jí)的列表結(jié)構(gòu),最下面一層的元素都是串聯(lián)起來(lái)的。某個(gè)元素存在于所有的層。定位插入點(diǎn)的時(shí)候,先在頂層進(jìn)行定位,然后下潛到下一層,然后一直到最底下一層。redia采用隨機(jī)策略來(lái)決定元素可以兼職到第幾層。L0層的概率是100%,L1的是50%。這樣去類(lèi)推
-
數(shù)據(jù)結(jié)構(gòu),具體介紹鏈接redis跳躍列表介紹
/* ZSETs use a specialized version of Skiplists */ typedef struct zskiplistNode { //成員對(duì)象 robj *obj; //分值 double score; //后退指針 struct zskiplistNode *backward; //層 struct zskiplistLevel { struct zskiplistNode *forward;//前進(jìn)指針 unsigned int span;//跨度 } level[]; } zskiplistNode; typedef struct zskiplist { //表頭節(jié)點(diǎn)和表尾節(jié)點(diǎn) struct zskiplistNode *header, *tail; //表中節(jié)點(diǎn)的的數(shù)量 unsigned long length; //表中層數(shù)最大的節(jié)點(diǎn)層數(shù) int level; } zskiplist;
容器的通用規(guī)則
create if not exists.如果容器不存在,則先創(chuàng)建容器。
drop if no elements.如果容器沒(méi)有元素了,就立即刪除該容器,釋放內(nèi)存。
容器的過(guò)期時(shí)間是針對(duì)整個(gè)對(duì)象的,hash的過(guò)期是整個(gè)進(jìn)行過(guò)期,而不是單個(gè)元素
分布式鎖
對(duì)于不是原子性的操作,就需要通過(guò)分布式鎖來(lái)保證只有一個(gè)線程可以獲取到鎖。具體使用的是setnx和del命令。鎖處理完了以后,就要被釋放。中間需要解決的問(wèn)題就是,如果業(yè)務(wù)邏輯執(zhí)行之間,出現(xiàn)了異常,可能導(dǎo)致del無(wú)法執(zhí)行,整個(gè)業(yè)務(wù)癱瘓。
或許,可以加一個(gè)過(guò)期時(shí)間,5s以后自動(dòng)釋放。但是邏輯還有問(wèn)題的,如果expire的時(shí)候,命令丟失了,也會(huì)死鎖。出現(xiàn)這種問(wèn)題的原因就是setnx和expire兩個(gè)命令不是原子性的。
在redis2.8的版本中,作者加入了set指令的擴(kuò)展餐廚,setnx和expire指令可以一起執(zhí)行了,指令是原子性的。
加鎖
set lock:book true ex 5 nx
//釋放鎖
del lock:book
超時(shí)問(wèn)題是redis分布式鎖不能解決的問(wèn)題,如果加鎖和釋放鎖之間的邏輯太長(zhǎng),超出了鎖的超時(shí)時(shí)限,那么第二個(gè)線程可能獲得redis分布式鎖。所以,redis分布式鎖不要用于較長(zhǎng)時(shí)間的任務(wù)
可重入性。java里面有一個(gè)ReentrantLock。Redis分布式鎖如果要支持重入,需要對(duì)客戶(hù)端的set方法進(jìn)行封裝,使用線程的ThreadLocal遍歷存儲(chǔ)當(dāng)前持有鎖的計(jì)數(shù)。
下面代碼redis分布式鎖的使用,這里鏈接是錯(cuò)誤案例介紹redis分布式鎖使用與錯(cuò)誤示例
//錯(cuò)誤使用就是使用setnx和expire兩個(gè)指令
/**
* 嘗試獲取分布式鎖
* @param jedis Redis客戶(hù)端
* @param lockKey 鎖
* @param requestId 請(qǐng)求標(biāo)識(shí)
* @param expireTime 超期時(shí)間
* @return 是否獲取成功
*/
public static boolean tryGetDistributedLock(Jedis jedis, String lockKey, String requestId, int expireTime) {
String result = jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);
if (LOCK_SUCCESS.equals(result)) {
return true;
}
return false;
}
- java版本的可重入鎖,核心就是用threadlocal去記錄每個(gè)線程,某個(gè)key的鎖計(jì)數(shù)
public class RedisWithReentrantLock{
//線程鎖計(jì)數(shù)器8
private ThreadLocal<Map<String,Integer>> lockers = new ThreadLocal<>();
private Jedis jedis;
//內(nèi)部使用的lock
private boolean _lock(String key) {
return jedis.set(key, "", "nx", "ex", 5L) != null;
}
//內(nèi)部使用的unlock
private void _unlock(String key) {
jedis.del(key);
}
//獲取計(jì)數(shù)器
private Map<String, Integer> currentLockers() {
Map<String, Integer> refs = lockers.get();
if (refs != null) {
return refs;
}
lockers.set(new HashMap<>());
return lockers.get();
}
//可重入加鎖
public boolean lock(String key) {
Map<String, Integer> refs = currentLockers();
Integer refCnt = refs.get(key);
if (refCnt != null) {
refs.put(key, refCnt + 1);
return true;
}
boolean ok = _lock(key);
if (!ok) {
return false;
}
refs.put(key, 1);
return true;
}
//可重入釋放鎖
public boolean unlock(String key) {
Map<String, Integer> refs = currentLockers();
Integer refCnt = refs.get(key);
if (refCnt == null) {
return false;
}
refCnt = -1;
if (refCnt > 0) {
refs.put(key, refCnt);
} else {
refs.remove(key);
_unlock(key);
}
return true;
}
}
redis的消息隊(duì)列,只做特性介紹,公司實(shí)際使用rocketmq
使用簡(jiǎn)單,只有一個(gè)消費(fèi)者
沒(méi)有ack保證,如果對(duì)消息可靠性有極高的要求,推薦使用rocketmq的事務(wù)消息
使用list作為異步消息的發(fā)送結(jié)構(gòu),rpush,rpop
延時(shí)隊(duì)列的實(shí)現(xiàn)
- 延時(shí)隊(duì)列可以使用zset實(shí)現(xiàn),將消息序列化成為一個(gè)字符串,消息的到期時(shí)間為score。然后使用多個(gè)線程去輪詢(xún)這個(gè)隊(duì)列。使用多線程是為了保障可用性。一個(gè)線程掛了,還有其他線程可以進(jìn)行處理
位圖,節(jié)約存儲(chǔ)空間
平時(shí)進(jìn)行開(kāi)發(fā)的時(shí)候,要對(duì)一些bool類(lèi)型的數(shù)據(jù)進(jìn)行存取,如用戶(hù)一年的簽到記錄。如果使用key/value方式進(jìn)行存取,就要記錄365個(gè),當(dāng)用戶(hù)量上億的時(shí)候,需要的存儲(chǔ)結(jié)構(gòu)是非常驚人的。
redis提供了位圖數(shù)據(jù)結(jié)構(gòu),每個(gè)用戶(hù)每天的簽到就只占據(jù)一個(gè)位。365天大概需要46個(gè)字節(jié),大大節(jié)約空間
位圖就是byte數(shù)組,可以使用getbit和setbit進(jìn)行存儲(chǔ),如果字節(jié)是不可以打印字符,redis會(huì)展示其16進(jìn)制。實(shí)際來(lái)操作一波
零存整取的方式,一個(gè)個(gè)bit去設(shè)置,然后一次性拿出值
通過(guò)python獲取字母`h`的hash值為0b1101000
setbit s 1 1
setbit s 2 1
setbit s 4 1
get s
"h" # 這樣就得到了h的值
零存零取的方式,就是對(duì)單個(gè)bit進(jìn)行操作,沒(méi)有設(shè)置的位就是默認(rèn)為0
set bit w 1 1
get bit w 0
0
整存零取
set w h
getbit w 1
1
不可打印字符
setbit x 0 1
setbit x 1 1
get x
"\xc0"
- 位圖的統(tǒng)計(jì)和查找:redis提供了對(duì)位圖的基本統(tǒng)計(jì)算
set w hello
ok
bitcount w
21
bitcount w 0 0
3
bitcount w 0 1 # 統(tǒng)計(jì)1的數(shù)量
7
bitpos w 0 # 第一個(gè)0
0
bitpos w 1 1 1 # 第二個(gè)字符算起,第一個(gè)1位
魔術(shù)指令bitfield
bitfield w get u4 0
高級(jí)數(shù)據(jù)結(jié)構(gòu)
HyberLogLog,提供不精確的統(tǒng)計(jì)數(shù)據(jù)
頁(yè)面的uv,不能使用一個(gè)key的incr,因?yàn)橐M(jìn)行去重
使用zset也可以做到這個(gè),但是消耗空間太大
HyperLogLog的標(biāo)準(zhǔn)誤差是0.81%。對(duì)于不精確的統(tǒng)計(jì),可以使用這個(gè)數(shù)據(jù)結(jié)構(gòu)
提供pdadd,pfcount兩個(gè)指令,pf是Philippe Flajolet教授的名字縮寫(xiě),這個(gè)數(shù)據(jù)結(jié)構(gòu)是他發(fā)明的。這個(gè)數(shù)據(jù)結(jié)構(gòu)占用的內(nèi)存是12k,使用了2^14個(gè)桶去進(jìn)行計(jì)算
pfadd book java
1
pfadd book java
0
pfadd book python
1
pfadd book python
pfcount book
2
布隆過(guò)濾器Bloom Filter
這個(gè)數(shù)據(jù)結(jié)構(gòu)主要實(shí)現(xiàn)去重的功能
場(chǎng)景使用:廣告推送的時(shí)候,如果我們看過(guò)了一條廣告,下一條推送的時(shí)候,要對(duì)看過(guò)的廣告進(jìn)行去重。如果使用關(guān)系型數(shù)據(jù)庫(kù),每次推送都要去數(shù)據(jù)庫(kù)查詢(xún),這樣數(shù)據(jù)庫(kù)很難抗住這個(gè)壓力。況且,瀏覽記錄會(huì)隨著時(shí)間線性增長(zhǎng),空間也跟不上
布隆過(guò)濾器會(huì)有一定的誤判概率。這可以比喻成為一個(gè)不精確的set結(jié)構(gòu),調(diào)用contains的時(shí)候,有可能誤判。如果說(shuō)這個(gè)值不存在,那就肯定不存在。如果說(shuō)這個(gè)值存在,優(yōu)肯是誤判,可能,兩者長(zhǎng)得相像?這樣的話(huà),對(duì)于實(shí)際業(yè)務(wù)也沒(méi)有影響,我們可以保證推送給用戶(hù)的都是最新的內(nèi)容,只有極少數(shù)用戶(hù)沒(méi)看過(guò)的內(nèi)容會(huì)被誤判。經(jīng)過(guò)測(cè)試,誤判率大概在1%左右。調(diào)整參數(shù)可以?xún)?yōu)化這個(gè)誤判率
布隆過(guò)濾器作為redis的一個(gè)插件,需要版本為4.0以后,才能使用。
基本指令為bf.add,bf.exists,bf.madd(批量),安裝docker,然后去重新安裝redis,帶這個(gè)插件的版本。centos7安裝docker方法,安裝好以后再安裝插件,使用docker啟動(dòng)redis
用戶(hù)可以通過(guò)bf.reserve命令顯式制定錯(cuò)誤率,key和初始空間。錯(cuò)誤率越低,占用空間越大。初始值越大,空間越大,所以,在使用的時(shí)候,需要準(zhǔn)確預(yù)估一下元素的大小。
布隆過(guò)濾器是一個(gè)大型的位數(shù)組。向布隆過(guò)濾器添加key的時(shí)候,會(huì)使用多個(gè)hash函數(shù)對(duì)key進(jìn)行hash,算得一個(gè)整數(shù)索引。然后對(duì)位數(shù)組的長(zhǎng)度取模運(yùn)算得到一個(gè)位置,每個(gè)hash函數(shù)都得到一個(gè)不同的位置,再把這幾個(gè)位置都設(shè)置為1,完成了add操作。巧妙地共享可多個(gè)key的存儲(chǔ)空間。
為何會(huì)出現(xiàn)誤判呢?就是由于判斷key是否存在的時(shí)候,如果某個(gè)位是0,那就一定不存在。但是,如果各個(gè)位置都是1的話(huà),也不能說(shuō)明是存在的,有可能是其他key存在導(dǎo)致。如果位數(shù)組比較稀疏,出現(xiàn)誤判的概率就低了。所以,使用的時(shí)候,不要讓實(shí)際的元素大于初始值。如果大于的時(shí)候,要對(duì)布隆過(guò)濾器進(jìn)行重建。
set存儲(chǔ)的是所有的元素,布隆過(guò)濾器是存儲(chǔ)元素的指紋(hash)??臻g節(jié)省還是比較明顯的.
docker加載插件的命令,docker run -p 6379:6379 --name redis-redisbloom redislabs/rebloom:latest
docker exec -it redis-redisbloom bash
bf.add book java
1
bf.add book java #再次加入java,就被排出了
0
bf.exists book python
0
redis限流
- 后端屏蔽用戶(hù)的過(guò)于頻繁的請(qǐng)求,比如1秒內(nèi)不能請(qǐng)求超過(guò)5次。這個(gè)限流存在一個(gè)滑動(dòng)時(shí)間窗口,zset的score圈出一個(gè)時(shí)間窗口,我們保留這個(gè)時(shí)間窗口,對(duì)于窗口外面的數(shù)據(jù),則可以統(tǒng)統(tǒng)砍掉。zset的value可以為毫秒時(shí)間戳。
public class SimpleRateLimiter{
private Jedis jedis;
//用戶(hù)請(qǐng)求是否允許
//使用樂(lè)觀鎖
//value保證唯一性就好
public boolean isActionAllow(String userId, String actionKey, int period, int maxCount) {
String key = String.format("hist:%s:%s", userId, actionKey);
long now = System.currentTimeMillis();
Pipeline pipe = jedis.pipelined();
pipe.multi();
pipe.zadd(key, now, "", now);
//截取
pipe.zremrangeByScore(key, 0, now - period * 1000);
Response<Long> count = pipe.zcard(key);
pipe.expire(key, period + 1);
pipe.exec();
pipe.close();
return count.get() <= maxCount;
}
}
- 漏斗限流,靈感來(lái)源于漏斗,就是漏斗有一個(gè)容量,也有一個(gè)消耗速率。當(dāng)灌水速率小于漏水速率,漏斗超過(guò)容量以后。灌水行為就要丟棄或者暫停
//單機(jī)版的漏斗限流
public class FunnelRateLimiter{
class Funnel{
//容量
int capacity;
//漏水速率
float leakingRate;
//剩余空間
int leftQuota;
//上次觸發(fā)時(shí)間
long leakingTs;
}
//....省略構(gòu)造方法
void makeSpace() {
long now = System.currentTimeMillis();
long deltaTs = nowTs - leakingTs;
int deltaQuota = (int)(deltaTs * leakingRate);
//如果時(shí)間間隔太久,可能會(huì)導(dǎo)致int溢出
if (deltaQuota < 0) {
//重設(shè)
this.leftQuota = capacity;
this.leakingTs = 0;
return;
}
if (deltaQuota < 1) {
return;
}
this.leftQuota += leftQuota;
this.leakingTs = nowTs;
if (this.leftQuota > this.capacity) {
this.leftQuota = this.capacity;
}
//是否可以接收請(qǐng)求
boolean watering(int quota) {
makeSpace();
if (leftQuota >= quota) {
this.leftQuota -= quota;
return true;
}
return false;
}
}
private Map<String, Funnel> funnels = new HashMap<>();
public boolean isActionAllow(String userId, String actionKey, int capacity, fload leakingRate) {
String key = String.format("funnels:%s:%s", userId, actionKey);
Funnel funnel = funnels.get(key);
if (funnel == null) {
funnel = new Funnel(capacity, leakingRate);
funnels.put(key, funnel);
}
return funnel.watering(1);
}
}
- 這里是redis提供的分布式限流,redis4.0提供了redis-cell。這是一個(gè)差價(jià),需要去github下載redis-cell的源碼解壓以后。進(jìn)入redis-cli后,執(zhí)行redis的 module load命令即可加載
wget https://github.com/brandur/redis-cell/releases/download/v0.2.4/redis-cell-v0.2.4-x86_64-unknown-linux-gnu.tar.gz
tar -xzvf redis-cell-v0.2.4-x86_64-unknown-linux-gnu.tar.gz
解壓后得到libredis_cess.so
redis-cli
module load /data/libredis_cell.so
我這里是放到了data目錄下面,運(yùn)行redis后加載就行了
//各個(gè)參數(shù) key 容量 計(jì)算漏斗速率 可選參數(shù)
cl.throttle <key> <max_burst> <count per period> <period> [<quantity>]
cl.throttle book 15 30 60 1 # 這個(gè)表示容量為15,2秒一個(gè)漏水速率(30/60)
0 # 0 允許 -1 拒絕
16 # 漏斗容量
15 # 剩余空間
-1 # 當(dāng)?shù)谝粋€(gè)參數(shù)為-1師,這里的值表示需要經(jīng)過(guò)多少時(shí)間以后才能再試
2 # 多久時(shí)間以后,漏斗可以完全空出來(lái)