Redis概述
介紹
- redis是一個(gè)開源的key-value存儲(chǔ)系統(tǒng)
- 和Memcached類似,它支持的value類型相對(duì)更多,包括String(字符串)、List(鏈表)、Hash(哈希)、Set(無需不重復(fù)集合)、ZSet(sorted set有一定順序的集合)
- 與memecached一樣,redis數(shù)據(jù)都緩存在內(nèi)存中
- 區(qū)別是redis會(huì)周期性的把更新的數(shù)據(jù)寫入到磁盤或者把修改操作追加到記錄文件
- 并且再次基礎(chǔ)上實(shí)現(xiàn)了master-slaver(主從同步)
應(yīng)用場(chǎng)景
配合關(guān)系型數(shù)據(jù)庫(kù)做告訴緩存,減少數(shù)據(jù)庫(kù)IO
分布式架構(gòu),做session共享
多樣的數(shù)據(jù)結(jié)構(gòu)存儲(chǔ)持久化數(shù)據(jù)
相關(guān)技術(shù)
- redis是單線程 + 多路IO復(fù)用的技術(shù)
- 與memcache三點(diǎn)不同:支持多種數(shù)據(jù)類型,支持持久化,單線程+ 多路IO復(fù)用
- 多路IO復(fù)用是一種同步IO模型,實(shí)現(xiàn)一個(gè)線程可以處理多個(gè)IO請(qǐng)求。也就是在redis單線程的基礎(chǔ)上,實(shí)現(xiàn)了單線程redis同時(shí)處理多個(gè)IO請(qǐng)求。多路指的值多個(gè)網(wǎng)絡(luò)連接,復(fù)用是指的是復(fù)用一個(gè)redis線程。其實(shí)這里也不是同時(shí)處理,只是處理請(qǐng)求的耗時(shí)特別短,所以時(shí)間拉長(zhǎng)看的話,就是一個(gè)線程處理了多個(gè)IO請(qǐng)求。比如說redis實(shí)現(xiàn)了1s可以對(duì)11萬次。
redis的數(shù)據(jù)類型
String(字符串)
- 概述
- String是最基本的數(shù)據(jù)類型,可以理解為和memcached一樣,一個(gè)key對(duì)應(yīng)一個(gè)value。
- String類型是二進(jìn)制安全的。意味著redis的String類型可以包含任何數(shù)據(jù)。比如jpg圖片和序列化對(duì)象。
- String是redis最基本的數(shù)據(jù)類型,一個(gè)字符串的value最大可以是512M。
- 數(shù)據(jù)結(jié)構(gòu)
- String的數(shù)據(jù)結(jié)構(gòu)是動(dòng)態(tài)字符串, 是可以修改的字符串,內(nèi)部結(jié)構(gòu)類似Java的ArrayList,采用預(yù)分配冗余空間的方式減少內(nèi)存的頻繁分配。
List(列表)
- 概述
- 單鍵多值,redis的list列表是最簡(jiǎn)單的字符串列表,按照插入順序排序。可以添加一個(gè)元素在列表的頭部或者尾部。
- 它的底層其實(shí)是一個(gè)雙向鏈表,對(duì)兩端的操作性能很高,通過索引下標(biāo)操作中間節(jié)點(diǎn)的性能較差。
- 數(shù)據(jù)結(jié)構(gòu)
- List的數(shù)據(jù)結(jié)構(gòu)為快速鏈表,也就是quickList。
- 首先在鏈表元素較少的情況下使用一段連續(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)空間。
- redis將鏈表和ziplist結(jié)合起來組成了quicklist。也就是將多個(gè)ziplist使用雙向指針串起來使用。這樣既滿足了快速的插入刪除性能,又不會(huì)浪費(fèi)太大的空間。
Set(無須不重復(fù)集合)
- 概述
- redis的set集合對(duì)外提供的功能和list類似,是一個(gè)列表的功能,特殊之處是set可以自動(dòng)排重,當(dāng)你需要存儲(chǔ)一個(gè)列表數(shù)據(jù),又不希望有重復(fù)數(shù)據(jù)的時(shí)候,set集合是一個(gè)很好的選擇。并且set集合提供了判斷某個(gè)成員是否在set集合內(nèi)的重要接口,這也是list所不能提供的。
- redis的set是String類型的無序集合。它底層是一個(gè)value為null的哈希表,所以添加、查找、刪除的時(shí)間復(fù)雜度都是o(1)。
- 一個(gè)算法,隨著數(shù)據(jù)的增加,如果時(shí)間復(fù)雜度是o(1),查找數(shù)據(jù)的時(shí)間不變。
- 數(shù)據(jù)結(jié)構(gòu)
- set的數(shù)據(jù)結(jié)構(gòu)是dict字典,字典是用哈希表實(shí)現(xiàn)的。
- Java中的HashSet內(nèi)部也是會(huì)用HashMap實(shí)現(xiàn)的,只不過所有的value都指向同一個(gè)對(duì)象。redis的set也是一樣的,它的內(nèi)部也是hash結(jié)構(gòu),所有的數(shù)據(jù)都指向同一個(gè)內(nèi)部值。(意思是HashSet所有的key都有一個(gè)默認(rèn)的value,你是HashSet是不允許key重復(fù)的,所以只允許有一個(gè)為null的key)
Hash(哈希)
- 概述
- redis的hash是一個(gè)鍵值對(duì)集合(類似Java中的Map<String, Object>)。
- redis的hash是一個(gè)String類型的key和value的映射表,hash特別適合存儲(chǔ)對(duì)象。
- 數(shù)據(jù)結(jié)構(gòu)
- Hash對(duì)應(yīng)的數(shù)據(jù)結(jié)構(gòu)是兩種,一種是ziplist(壓縮列表),hashtable(哈希表)。當(dāng)key-value長(zhǎng)度較短且個(gè)數(shù)較少的時(shí)候,使用ziplist,否則使用hashtable。
Zset(Sorted set)
- 概述
- redis的zset集合和set集合非常的相似,是一個(gè)沒有重復(fù)元素的字符串集合。
- 不同之處是zset每個(gè)成員都關(guān)聯(lián)了一個(gè)score(評(píng)分),這個(gè)score(評(píng)分)是被用來按照從最低到最高排序的標(biāo)準(zhǔn),集合中的成員是唯一的,但是score是可以重復(fù)的。
- 因?yàn)樵厥怯行虻模砸部梢院芸旄鶕?jù)score或者position來獲取一個(gè)范圍的元素。
- 訪問一個(gè)有序集合也是非常快的,因此你能用有序集合作為一個(gè)沒有重復(fù)成員的智能列表。
- 數(shù)據(jù)結(jié)構(gòu)
- zset(sorted set)是redis提供的一個(gè)非常特別的數(shù)據(jù)結(jié)構(gòu),一方面他等價(jià)于Java的Map<String, double>,可以給每一個(gè)成員賦值一個(gè)權(quán)重score,另一方面它有類似treeSet,內(nèi)部元素會(huì)按照score權(quán)重進(jìn)行排序,可得到每個(gè)成員的名次,還可以通過score的范圍獲取元素列表。
- zset使用了兩個(gè)數(shù)據(jù)結(jié)構(gòu):
- hash,hash的作用就是關(guān)聯(lián)value和權(quán)重score,保障元素value的唯一性,可以通過value找到對(duì)應(yīng)的score值。
- 跳躍表,跳躍表的目的在于給元素value排序,根據(jù)score的范圍獲取元素列表。
Bitmaps
-
概述
- Redis提供了Bitmaps這個(gè)“數(shù)據(jù)類型”可以實(shí)現(xiàn)對(duì)位的操作:
Bitmaps本身不是一種數(shù)據(jù)類型, 實(shí)際上它就是字符串(key-value) , 但是它可以對(duì)字符串的位進(jìn)行操作。
Bitmaps單獨(dú)提供了一套命令, 所以在Redis中使用Bitmaps和使用字符串的方法不太相同。 可以把Bitmaps想象成一個(gè)以位為單位的數(shù)組, 數(shù)組的每個(gè)單元只能存儲(chǔ)0和1, 數(shù)組的下標(biāo)在Bitmaps中叫做偏移量。
- Redis提供了Bitmaps這個(gè)“數(shù)據(jù)類型”可以實(shí)現(xiàn)對(duì)位的操作:
-
Bitmaps與set對(duì)比
- 假設(shè)網(wǎng)站有1億用戶, 每天獨(dú)立訪問的用戶有5千萬, 如果每天用集合類型和Bitmaps分別存儲(chǔ)活躍用戶可以得到表:
set和Bitmaps存儲(chǔ)一天活躍用戶對(duì)比 數(shù)據(jù)類型 每個(gè)用戶id占用空間 需要存儲(chǔ)的用戶量 全部?jī)?nèi)存量 集合 64位 50000000 64位*50000000 = 400MB Bitmaps 1位 100000000 1位*100000000 = 12.5MB 很明顯, 這種情況下使用Bitmaps能節(jié)省很多的內(nèi)存空間, 尤其是隨著時(shí)間推移節(jié)省的內(nèi)存還是非常可觀的。
set和Bitmaps存儲(chǔ)獨(dú)立用戶空間對(duì)比 數(shù)據(jù)類型 一天 一個(gè)月 一年 集合 400MB 12GB 144GB Bitmaps 12.5MB 375MB 4.5GB 但Bitmaps并不是萬金油, 假如該網(wǎng)站每天的獨(dú)立訪問用戶很少, 例如只有10萬(大量的僵尸用戶) , 那么兩者的對(duì)比如下表所示, 很顯然, 這時(shí)候使用Bitmaps就不太合適了, 因?yàn)榛旧洗蟛糠治欢际?。
set和Bitmaps存儲(chǔ)一天活躍用戶對(duì)比(用戶比較少) 數(shù)據(jù)類型 每個(gè)userid占用空間 需要存儲(chǔ)的用戶量 全部?jī)?nèi)存量 集合 64位 100000 64位*100000 = 800KB Bitmaps 1位 100000000 1位*100000000 = 12.5MB HyperLogLog
在工作當(dāng)中,我們經(jīng)常會(huì)遇到與統(tǒng)計(jì)相關(guān)的功能需求,比如統(tǒng)計(jì)網(wǎng)站PV(PageView頁(yè)面訪問量),可以使用Redis的incr、incrby輕松實(shí)現(xiàn)。但像UV(UniqueVisitor獨(dú)立訪客)、獨(dú)立IP數(shù)、搜索記錄數(shù)等需要去重和計(jì)數(shù)的問題如何解決?這種求集合中不重復(fù)元素個(gè)數(shù)的問題稱為基數(shù)問題。
解決基數(shù)問題有很多種方案:
1.數(shù)據(jù)存儲(chǔ)在MySQL表中,使用distinct count計(jì)算不重復(fù)個(gè)數(shù)。
2.使用Redis提供的hash、set、bitmaps等數(shù)據(jù)結(jié)構(gòu)來處理。
以上的方案結(jié)果精確,但隨著數(shù)據(jù)不斷增加,導(dǎo)致占用空間越來越大,對(duì)于非常大的數(shù)據(jù)集是不切實(shí)際的。能否能夠降低一定的精度來平衡存儲(chǔ)空間?Redis推出了HyperLogLog。
Redis HyperLogLog 是用來做基數(shù)統(tǒng)計(jì)的算法,HyperLogLog 的優(yōu)點(diǎn)是:在輸入元素的數(shù)量或者體積非常非常大時(shí),計(jì)算基數(shù)所需的空間總是固定的、并且是很小的。
在 Redis 里面,每個(gè) HyperLogLog 鍵只需要花費(fèi) 12 KB 內(nèi)存,就可以計(jì)算接近 2^64 個(gè)不同元素的基數(shù)。這和計(jì)算基數(shù)時(shí),元素越多耗費(fèi)內(nèi)存就越多的集合形成鮮明對(duì)比。
但是,因?yàn)?HyperLogLog 只會(huì)根據(jù)輸入元素來計(jì)算基數(shù),而不會(huì)儲(chǔ)存輸入元素本身,所以 HyperLogLog 不能像集合那樣,返回輸入的各個(gè)元素。
Geospatial
Redis 3.2 中增加了對(duì)GEO類型的支持。GEO,Geographic,地理信息的縮寫。該類型,就是元素的2維坐標(biāo),在地圖上就是經(jīng)緯度。redis基于該類型,提供了經(jīng)緯度設(shè)置,查詢,范圍查詢,距離查詢,經(jīng)緯度Hash等常見操作。
Redis的發(fā)布和訂閱
什么是發(fā)布和訂閱
- Redis 發(fā)布訂閱 (pub/sub) 是一種消息通信模式:發(fā)送者 (pub) 發(fā)送消息,訂閱者 (sub) 接收消息。
- Redis 客戶端可以訂閱任意數(shù)量的頻道。
Redis事務(wù)、鎖機(jī)制秒殺
Redis事務(wù)定義
- Redis事務(wù)是一個(gè)單獨(dú)的隔離操作:事務(wù)中的所有命令都會(huì)序列化、按順序地執(zhí)行。事務(wù)在執(zhí)行的過程中,不會(huì)被其他客戶端發(fā)送來的命令請(qǐng)求所打斷。
- Redis事務(wù)的主要作用就是串聯(lián)多個(gè)命令防止別的命令插隊(duì)。
Multi、Exec、discard
Redis事務(wù)中有Multi、Exec和discard三個(gè)指令,在Redis中,從輸入Multi命令開始,輸入的命令都會(huì)依次進(jìn)入命令隊(duì)列中,但不會(huì)執(zhí)行,直到輸入Exec后,Redis會(huì)將之前的命令隊(duì)列中的命令依次執(zhí)行。而組隊(duì)的過程中可以通過discard來放棄組隊(duì)。
為什么要做成事務(wù)
想想一個(gè)場(chǎng)景:有很多人有你的賬戶,同時(shí)去參加雙十一搶購(gòu)。
事務(wù)沖突的問題
- 例子
一個(gè)請(qǐng)求想給金額減8000;
一個(gè)請(qǐng)求想給金額減5000;
-
一個(gè)請(qǐng)求想給金額減1000。
最終我們可以發(fā)現(xiàn),總共金額是10000,如果請(qǐng)求全部執(zhí)行,那最后的金額變?yōu)?4000,很明顯不合理。
悲觀鎖
悲觀鎖(Pessimistic Lock),顧名思義,就是很悲觀,每次去拿數(shù)據(jù)的時(shí)候都認(rèn)為別人會(huì)修改,所以每次在拿數(shù)據(jù)的時(shí)候都會(huì)上鎖,這樣別人想拿這個(gè)數(shù)據(jù)就會(huì)block直到它拿到鎖。傳統(tǒng)的關(guān)系型數(shù)據(jù)庫(kù)里邊就用到了很多這種鎖機(jī)制,比如行鎖,表鎖等,讀鎖,寫鎖等,都是在做操作之前先上鎖。
樂觀鎖
樂觀鎖(Optimistic Lock),顧名思義,就是很樂觀,每次去拿數(shù)據(jù)的時(shí)候都認(rèn)為別人不會(huì)修改,所以不會(huì)上鎖,但是在更新的時(shí)候會(huì)判斷一下在此期間別人有沒有去更新這個(gè)數(shù)據(jù),可以使用版本號(hào)等機(jī)制。樂觀鎖適用于多讀的應(yīng)用類型,這樣可以提高吞吐量。Redis就是利用這種check-and-set機(jī)制實(shí)現(xiàn)事務(wù)的。
- WATCH key [key …]
- 在執(zhí)行multi之前,先執(zhí)行watch key1 [key2],可以監(jiān)視一個(gè)(或多個(gè)) key ,如果在事務(wù)執(zhí)行之前這個(gè)(或這些) key被其他命令所改動(dòng),那么事務(wù)將被打斷。
- unwatch
- 取消 WATCH 命令對(duì)所有 key 的監(jiān)視。如果在執(zhí)行 WATCH 命令之后,EXEC 命令或DISCARD 命令先被執(zhí)行了的話,那么就不需要再執(zhí)行UNWATCH 了。
Redis事務(wù)三特性
單獨(dú)的隔離操作 :事務(wù)中的所有命令都會(huì)序列化、按順序地執(zhí)行。事務(wù)在執(zhí)行的過程中,不會(huì)被其他客戶端發(fā)送來的命令請(qǐng)求所打斷。
沒有隔離級(jí)別的概念 :隊(duì)列中的命令沒有提交之前都不會(huì)實(shí)際被執(zhí)行,因?yàn)槭聞?wù)提交前任何指令都不會(huì)被實(shí)際執(zhí)行。
不保證原子性 :事務(wù)中如果有一條命令執(zhí)行失敗,其后的命令仍然會(huì)被執(zhí)行,沒有回滾 。
redis事務(wù)案例
/**
* redis事務(wù)
*/
@Test
public void test6() {
//返回值o是由SessionCallback內(nèi)部方法execute方法返回
Object o = redisTemplate.execute(new SessionCallback<Object>() {
@Override
public Object execute(RedisOperations redisOperations) throws DataAccessException {
redisOperations.multi();
//1、設(shè)置key
redisOperations.opsForValue().set("vKey", "hi redis.");
//2、獲取key
redisOperations.opsForValue().get("vKey");
//3、刪除key
redisOperations.delete("vKey");
final List<Object> result = redisOperations.exec();
//返回vKey的值
return result.get(1).toString();
}
});
//輸出"hi redis."
System.out.println(o.toString());
}
/**
redis樂觀鎖使用
*/
@Test
public void test() {
Object o = redisTemplate.execute(new SessionCallback<Object>() {
int qua = 0;
/**
* Executes all the given operations inside the same session.
*
* @param operations Redis operations
* @return return value
*/
@Override
public Object execute(RedisOperations operations) throws DataAccessException {
try {
operations.opsForValue().set("quality", 20);
operations.watch("quality");
operations.multi();
operations.opsForValue().increment("quality", -30);
operations.exec();//執(zhí)行事務(wù)
} catch (Exception e) {
operations.unwatch();//解鎖
} finally {
qua = (int)operations.opsForValue().get("quality");
if (qua < 0) {
operations.opsForValue().increment("quality", 30);//還原庫(kù)存
}
qua = (int)operations.opsForValue().get("quality");
}
return qua;
}
});
System.out.println((Integer) o);
}
//通常我們并不使用這種方式加鎖,我們常使用分布式鎖解決此問題
連接超時(shí),通過連接池解決
節(jié)省每次連接redis服務(wù)帶來的消耗,把連接好的實(shí)例反復(fù)利用。
-
連接池參數(shù):
MaxTotal:控制一個(gè)pool可分配多少個(gè)jedis實(shí)例,通過pool.getResource()來獲取;如果賦值為-1,則表示不限制;如果pool已經(jīng)分配了MaxTotal個(gè)jedis實(shí)例,則此時(shí)pool的狀態(tài)為exhausted。
maxIdle:控制一個(gè)pool最多有多少個(gè)狀態(tài)為idle(空閑)的jedis實(shí)例;
MaxWaitMillis:表示當(dāng)borrow一個(gè)jedis實(shí)例時(shí),最大的等待毫秒數(shù),如果超過等待時(shí)間,則直接拋JedisConnectionException;
testOnBorrow:獲得一個(gè)jedis實(shí)例的時(shí)候是否檢查連接可用性(ping());如果為true,則得到的jedis實(shí)例均是可用的。
-
解決庫(kù)存遺留問題
- 將復(fù)雜的或者多步的redis操作,寫為一個(gè)腳本,一次提交給redis執(zhí)行,減少反復(fù)連接redis的次數(shù),提升性能。
- LUA腳本是類似redis事務(wù),有一定的原子性,不會(huì)被其他命令插隊(duì),可以完成一些redis事務(wù)性的操作。
- 但是注意redis的lua腳本功能,只有在Redis 2.6以上的版本才可以使用。
- 利用lua腳本淘汰用戶,解決超賣問題,redis 2.6版本以后,通過lua腳本解決爭(zhēng)搶問題,實(shí)際上是redis 利用其單線程的特性,用任務(wù)隊(duì)列的方式解決多任務(wù)并發(fā)問題。
Redis持久化之RDB
Redis 提供了2個(gè)不同形式的持久化方式:
- RDB(Redis DataBase)
- AOF(Append Of File)
RDB
-
簡(jiǎn)介
- 在指定的時(shí)間間隔內(nèi)將內(nèi)存中的數(shù)據(jù)集快照寫入磁盤, 也就是行話講的Snapshot快照,它恢復(fù)時(shí)是將快照文件直接讀到內(nèi)存里。
-
備份是如何執(zhí)行的
- Redis會(huì)單獨(dú)創(chuàng)建(fork)一個(gè)子進(jìn)程來進(jìn)行持久化,首先會(huì)將數(shù)據(jù)寫入到一個(gè)臨時(shí)文件中,待持久化過程都結(jié)束了,再用這個(gè)臨時(shí)文件替換上次持久化好的文件。整個(gè)過程中,主進(jìn)程是不進(jìn)行任何IO操作的,這就確保了極高的性能。如果需要進(jìn)行大規(guī)模數(shù)據(jù)的恢復(fù),且對(duì)于數(shù)據(jù)恢復(fù)的完整性不是非常敏感,那RDB方式要比AOF方式更加的高效。RDB的缺點(diǎn)是最后一次持久化后的數(shù)據(jù)可能丟失。
-
Fork
- Fork的作用是復(fù)制一個(gè)與當(dāng)前進(jìn)程一樣的進(jìn)程。新進(jìn)程的所有數(shù)據(jù)(變量、環(huán)境變量、程序計(jì)數(shù)器等) 數(shù)值都和原進(jìn)程一致,但是是一個(gè)全新的進(jìn)程,并作為原進(jìn)程的子進(jìn)程。
- 在Linux程序中,fork()會(huì)產(chǎn)生一個(gè)和父進(jìn)程完全相同的子進(jìn)程,但子進(jìn)程在此后多會(huì)exec系統(tǒng)調(diào)用,出于效率考慮,Linux中引入了“寫時(shí)復(fù)制技術(shù)”。
- 一般情況父進(jìn)程和子進(jìn)程會(huì)共用同一段物理內(nèi)存,只有進(jìn)程空間的各段的內(nèi)容要發(fā)生變化時(shí),才會(huì)將父進(jìn)程的內(nèi)容復(fù)制一份給子進(jìn)程。
-
dump.rdb文件
- 在redis.conf中配置文件名稱,默認(rèn)為dump.rdb。
- rdb文件的保存路徑,也可以修改。默認(rèn)為Redis啟動(dòng)時(shí)命令行所在的目錄下 “dir ./”
-
如何觸發(fā)RDB快照;保持策略
- 配置文件中默認(rèn)的快照配置
-
命令save VS bgsave
- save :save時(shí)只管保存,其它不管,全部阻塞。手動(dòng)保存,不建議。
- bgsave:Redis會(huì)在后臺(tái)異步進(jìn)行快照操作, 快照同時(shí)還可以響應(yīng)客戶端請(qǐng)求。
- 可以通過lastsave 命令獲取最后一次成功執(zhí)行快照的時(shí)間。
-
flushall命令
- 執(zhí)行flushall命令,也會(huì)產(chǎn)生dump.rdb文件,但里面是空的,無意義。
-
優(yōu)勢(shì)
- 適合大規(guī)模的數(shù)據(jù)恢復(fù)
- 對(duì)數(shù)據(jù)完整性和一致性要求不高更適合使用
- 節(jié)省磁盤空間
- 恢復(fù)速度快
-
劣勢(shì)
- Fork的時(shí)候,內(nèi)存中的數(shù)據(jù)被克隆了一份,大致2倍的膨脹性需要考慮。
- 雖然Redis在fork時(shí)使用了寫時(shí)拷貝技術(shù),但是如果數(shù)據(jù)龐大時(shí)還是比較消耗性能。
- 在備份周期在一定間隔時(shí)間做一次備份,所以如果Redis意外down掉的話,就會(huì)丟失最后一次快照后的所有修改。
-
如何停止
- 動(dòng)態(tài)停止RDB:redis-cli config set save “”#save后給空值,表示禁用保存策略。
Redis持久化之AOF
AOF(Append Only File)
以日志的形式來記錄每個(gè)寫操作(增量保存),將Redis執(zhí)行過的所有寫指令記錄下來(讀操作不記錄), 只許追加文件但不可以改寫文件,redis啟動(dòng)之初會(huì)讀取該文件重新構(gòu)建數(shù)據(jù),換言之,redis 重啟的話就根據(jù)日志文件的內(nèi)容將寫指令從前到后執(zhí)行一次以完成數(shù)據(jù)的恢復(fù)工作。
-
AOF持久化流程
- 客戶端的請(qǐng)求寫命令會(huì)被append追加到AOF緩沖區(qū)內(nèi);
- AOF緩沖區(qū)根據(jù)AOF持久化策略[always,everysec,no]將操作sync同步到磁盤的AOF文件中;
- AOF文件大小超過重寫策略或手動(dòng)重寫時(shí),會(huì)對(duì)AOF文件rewrite重寫,壓縮AOF文件容量;
- Redis服務(wù)重啟時(shí),會(huì)重新load加載AOF文件中的寫操作達(dá)到數(shù)據(jù)恢復(fù)的目的。
-
AOF默認(rèn)不開啟
- 可以在redis.conf中配置文件名稱默認(rèn)為 appendonly.aof文件中開啟,AOF文件的保存路徑,同RDB的路徑一致。
-
AOF和RDB同時(shí)開啟,redis聽誰(shuí)的?
- AOF和RDB同時(shí)開啟,系統(tǒng)默認(rèn)取AOF的數(shù)據(jù)(數(shù)據(jù)不會(huì)存在丟失)。這里的意思是redis重啟的時(shí)候,優(yōu)先加載AOF文件,因?yàn)橥ǔG闆r下AOF比RDB的數(shù)據(jù)完整性更好。
-
AOF啟動(dòng)、修復(fù)、恢復(fù)
- AOF的備份機(jī)制和性能雖然和RDB不同,但是備份和恢復(fù)的操作同RDB一樣,都是拷貝備份文件,需要恢復(fù)時(shí)再拷貝到Redis工作目錄下,啟動(dòng)系統(tǒng)即加載。
- 正常恢復(fù)
- 修改默認(rèn)的appendonly no,改為yes。
- 將有數(shù)據(jù)的aof文件復(fù)制一份保存到對(duì)應(yīng)目錄(查看目錄:config get dir)。
- 恢復(fù):重啟redis然后重新加載。
- 異常恢復(fù)
- 修改默認(rèn)的appendonly no,改為yes。
- 如遇到AOF文件損壞,通過/usr/local/bin/redis-check-aof–fix appendonly.aof進(jìn)行恢復(fù)。
- 備份被寫壞的AOF文件。
- 恢復(fù):重啟redis,然后重新加載。
-
AOF同步頻率設(shè)置
- appendfsync always:始終同步,每次Redis的寫入都會(huì)立刻記入日志;性能較差但數(shù)據(jù)完整性比較好。
- appendfsync everysec:每秒同步,每秒記入日志一次,如果宕機(jī),本秒的數(shù)據(jù)可能丟失。
- appendfsync no:redis不主動(dòng)進(jìn)行同步,把同步時(shí)機(jī)交給操作系統(tǒng)。
Rewrite壓縮
-
Rewrite壓縮是什么
- AOF采用文件追加方式,文件會(huì)越來越大為避免出現(xiàn)此種情況,新增了重寫機(jī)制,當(dāng)AOF文件的大小超過所設(shè)定的閾值時(shí),Redis就會(huì)啟動(dòng)AOF文件的內(nèi)容壓縮,只保留可以恢復(fù)數(shù)據(jù)的最小指令集,可以使用命令bgrewriteaof。
-
重寫原理,如何實(shí)現(xiàn)重寫
- AOF文件持續(xù)增長(zhǎng)而過大時(shí),會(huì)fork出一條新進(jìn)程來將文件重寫(也是先寫臨時(shí)文件最后再rename),redis4.0版本后的重寫,是指把rdb 的快照,以二進(jìn)制的形式附在新的aof頭部,作為已有的歷史數(shù)據(jù),替換掉原來的流水賬操作。
-
no-appendfsync-on-rewrite:
- 如果 no-appendfsync-on-rewrite=yes ,不寫入aof文件只寫入緩存,用戶請(qǐng)求不會(huì)阻塞,但是在這段時(shí)間如果宕機(jī)會(huì)丟失這段時(shí)間的緩存數(shù)據(jù)。(降低數(shù)據(jù)安全性,提高性能)
- 如果 no-appendfsync-on-rewrite=no,還是會(huì)把數(shù)據(jù)往磁盤里刷,但是遇到重寫操作,可能會(huì)發(fā)生阻塞。(數(shù)據(jù)安全,但是性能降低)
觸發(fā)機(jī)制,何時(shí)重寫
Redis會(huì)記錄上次重寫時(shí)的AOF大小,默認(rèn)配置是當(dāng)AOF文件大小是上次rewrite后大小的一倍且文件大于64M時(shí)觸發(fā)。
重寫雖然可以節(jié)約大量磁盤空間,減少恢復(fù)時(shí)間。但是每次重寫還是有一定的負(fù)擔(dān)的,因此設(shè)定Redis要滿足一定條件才會(huì)進(jìn)行重寫。
- auto-aof-rewrite-percentage:設(shè)置重寫的基準(zhǔn)值,文件達(dá)到100%時(shí)開始重寫(文件是原來重寫后文件的2倍時(shí)觸發(fā))。
- auto-aof-rewrite-min-size:設(shè)置重寫的基準(zhǔn)值,最小文件64MB。達(dá)到這個(gè)值開始重寫。
- 系統(tǒng)載入時(shí)或者上次重寫完畢時(shí),Redis會(huì)記錄此時(shí)AOF大小,設(shè)為base_size
- 如果Redis的AOF當(dāng)前大小>= base_size +base_size*100% (默認(rèn))且當(dāng)前大小>=64mb(默認(rèn))的情況下,Redis會(huì)對(duì)AOF進(jìn)行重寫。
- 例如:文件達(dá)到70MB開始重寫,降到50MB,下次什么時(shí)候開始重寫?100MB
重寫流程
- bgrewriteaof觸發(fā)重寫,判斷是否當(dāng)前有bgsave或bgrewriteaof在運(yùn)行,如果有,則等待該命令結(jié)束后再繼續(xù)執(zhí)行;
- 主進(jìn)程fork出子進(jìn)程執(zhí)行重寫操作,保證主進(jìn)程不會(huì)阻塞;
- 子進(jìn)程遍歷redis內(nèi)存中數(shù)據(jù)到臨時(shí)文件,客戶端的寫請(qǐng)求同時(shí)寫入aof_buf緩沖區(qū)和aof_rewrite_buf重寫緩沖區(qū),保證原AOF文件完整以及新AOF文件生成期間的新的數(shù)據(jù)修改動(dòng)作不會(huì)丟失;
- 子進(jìn)程寫完新的AOF文件后,向主進(jìn)程發(fā)信號(hào),父進(jìn)程更新統(tǒng)計(jì)信息。主進(jìn)程把a(bǔ)of_rewrite_buf中的數(shù)據(jù)寫入到新的AOF文件;
- 使用新的AOF文件覆蓋舊的AOF文件,完成AOF重寫。
優(yōu)勢(shì)
- 備份機(jī)制更穩(wěn)健,丟失數(shù)據(jù)概率更低。
- 可讀的日志文本,通過操作AOF穩(wěn)健,可以處理誤操作。
劣勢(shì)
- 比起RDB占用更多的磁盤空間。
- 恢復(fù)備份速度要慢。
- 每次讀寫都同步的話,有一定的性能壓力。
- 存在個(gè)別Bug,造成恢復(fù)不能。
總結(jié)(Which one)
用哪個(gè)好
-
官方推薦兩個(gè)都啟用:
- 如果對(duì)數(shù)據(jù)不敏感,可以選單獨(dú)用RDB。
- 不建議單獨(dú)用 AOF,因?yàn)榭赡軙?huì)出現(xiàn)Bug。
- 如果只是做純內(nèi)存緩存,可以都不用。
-
官網(wǎng)建議
RDB持久化方式能夠在指定的時(shí)間間隔能對(duì)你的數(shù)據(jù)進(jìn)行快照存儲(chǔ)。
AOF持久化方式記錄每次對(duì)服務(wù)器寫的操作,當(dāng)服務(wù)器重啟的時(shí)候會(huì)重新執(zhí)行這些命令來恢復(fù)原始的數(shù)據(jù),AOF命令以redis協(xié)議追加保存每次寫的操作到文件末尾。
Redis還能對(duì)AOF文件進(jìn)行后臺(tái)重寫,使得AOF文件的體積不至于過大。
只做緩存:如果你只希望你的數(shù)據(jù)在服務(wù)器運(yùn)行的時(shí)候存在,你也可以不使用任何持久化方式。
同時(shí)開啟兩種持久化方式:在這種情況下,當(dāng)redis重啟的時(shí)候會(huì)優(yōu)先載入AOF文件來恢復(fù)原始的數(shù)據(jù),因?yàn)樵谕ǔG闆r下AOF文件保存的數(shù)據(jù)集要比RDB文件保存的數(shù)據(jù)集要完整。
RDB的數(shù)據(jù)不實(shí)時(shí),同時(shí)使用兩者時(shí)服務(wù)器重啟也只會(huì)找AOF文件。那要不要只使用AOF呢?
建議不要,因?yàn)镽DB更適合用于備份數(shù)據(jù)庫(kù)(AOF在不斷變化不好備份),快速重啟,而且不會(huì)有AOF可能潛在的bug,留著作為一個(gè)萬一的手段。
-
性能建議:
- 因?yàn)镽DB文件只用作后備用途,建議只在Slave上持久化RDB文件,而且只要15分鐘備份一次就夠了,只保留save 9001這條規(guī)則。
- 如果使用AOF,好處是在最惡劣情況下也只會(huì)丟失不超過兩秒數(shù)據(jù),啟動(dòng)腳本較簡(jiǎn)單,只load自己的AOF文件就可以了。
aof代價(jià):一是帶來了持續(xù)的IO,二是AOF rewrite的最后,將rewrite過程中產(chǎn)生的新數(shù)據(jù)寫到新文件造成的阻塞幾乎是不可避免的。 - 只要硬盤許可,應(yīng)該盡量減少AOF rewrite的頻率,AOF重寫的基礎(chǔ)大小默認(rèn)值64M太小了,可以設(shè)到5G以上。默認(rèn)超過原大小100%大小時(shí)重寫可以改到適當(dāng)?shù)臄?shù)值。
Redis主從復(fù)制
主機(jī)數(shù)據(jù)更新后根據(jù)配置和策略, 自動(dòng)同步到備機(jī)的master/slaver機(jī)制,Master以寫為主,Slave以讀為主,主從復(fù)制節(jié)點(diǎn)間數(shù)據(jù)是全量的。
作用:
讀寫分離,性能擴(kuò)展
容災(zāi)快速恢復(fù)
復(fù)制原理
Slave啟動(dòng)成功連接到master后會(huì)發(fā)送一個(gè)sync命令;
Master接到命令啟動(dòng)后臺(tái)的存盤進(jìn)程,同時(shí)收集所有接收到的用于修改數(shù)據(jù)集命令,在后臺(tái)進(jìn)程執(zhí)行完畢之后,master將傳送整個(gè)數(shù)據(jù)文件到slave,以完成一次完全同步。
全量復(fù)制:slave服務(wù)器在接收到數(shù)據(jù)庫(kù)文件數(shù)據(jù)后,將其存盤并加載到內(nèi)存中。
增量復(fù)制:Master繼續(xù)將新的所有收集到的修改命令依次傳給slave,完成同步。
但是只要是重新連接master,一次完全同步(全量復(fù)制)將被自動(dòng)執(zhí)行。
哨兵模式(sentinel)
反客為主:當(dāng)一個(gè)master宕機(jī)后,后面的slave可以立刻升為master,其后面的slave不用做任何修改。用 slaveof no one 指令將從機(jī)變?yōu)橹鳈C(jī)。而哨兵模式是反客為主的自動(dòng)版,能夠后臺(tái)監(jiān)控主機(jī)是否故障,如果故障了根據(jù)投票數(shù)自動(dòng)將從庫(kù)轉(zhuǎn)換為主庫(kù)。
當(dāng)主機(jī)掛掉,從機(jī)選舉產(chǎn)生新的主機(jī)
- 哪個(gè)從機(jī)會(huì)被選舉為主機(jī)呢?根據(jù)優(yōu)先級(jí)別:slave-priority 。
- 原主機(jī)重啟后會(huì)變?yōu)閺臋C(jī)。
復(fù)制延時(shí)
由于所有的寫操作都是先在Master上操作,然后同步更新到Slave上,所以從Master同步到Slave機(jī)器有一定的延遲,當(dāng)系統(tǒng)很繁忙的時(shí)
候,延遲問題會(huì)更加嚴(yán)重,Slave機(jī)器數(shù)量的增加也會(huì)使這個(gè)問題更加嚴(yán)重。
故障恢復(fù)
優(yōu)先級(jí):在redis.conf中默認(rèn) slave-priority 100,值越小優(yōu)先級(jí)越高。
偏移量:指獲得原主機(jī)數(shù)據(jù)最全的概率。
runid:每個(gè)redis實(shí)例啟動(dòng)后都會(huì)隨機(jī)生成一個(gè)40位的runid。
Redis集群(cluster模式)
Redis 集群(包括很多小集群)實(shí)現(xiàn)了對(duì)Redis的水平擴(kuò)容,即啟動(dòng)N個(gè)redis節(jié)點(diǎn),將整個(gè)數(shù)據(jù)庫(kù)分布存儲(chǔ)在這N個(gè)節(jié)點(diǎn)中,每個(gè)節(jié)點(diǎn)存儲(chǔ)總數(shù)據(jù)的1/N,即一個(gè)小集群存儲(chǔ)1/N的數(shù)據(jù),每個(gè)小集群里面維護(hù)好自己的1/N的數(shù)據(jù)。
Redis 集群通過分區(qū)(partition)來提供一定程度的可用性(availability): 即使集群中有一部分節(jié)點(diǎn)失效或者無法進(jìn)行通訊, 集群也可以繼續(xù)處理命令請(qǐng)求。
該模式的redis集群特點(diǎn)是:分治、分片。
問題
容量不夠,redis如何進(jìn)行擴(kuò)容?
并發(fā)寫操作, redis如何分?jǐn)偅?/p>
另外,主從模式,薪火相傳模式,主機(jī)宕機(jī),導(dǎo)致ip地址發(fā)生變化,應(yīng)用程序中配置需要修改對(duì)應(yīng)的主機(jī)地址、端口等信息。
之前通過代理主機(jī)來解決,但是redis3.0中提供了解決方案。就是無中心化集群配置。
集群連接
普通方式登錄:可能直接進(jìn)入讀主機(jī),存儲(chǔ)數(shù)據(jù)時(shí),會(huì)出現(xiàn)MOVED重定向操作,所以,應(yīng)該以集群方式登錄。
集群登錄:redis-cli -c -p 6379 采用集群策略連接,設(shè)置數(shù)據(jù)會(huì)自動(dòng)切換到相應(yīng)的寫主機(jī).
redis cluster 如何分配這六個(gè)節(jié)點(diǎn)?
- 一個(gè)集群至少要有三個(gè)主節(jié)點(diǎn)。
- 選項(xiàng) –cluster-replicas 1 :表示我們希望為集群中的每個(gè)主節(jié)點(diǎn)創(chuàng)建一個(gè)從節(jié)點(diǎn)。
- 分配原則盡量保證每個(gè)主數(shù)據(jù)庫(kù)運(yùn)行在不同的IP地址,每個(gè)從庫(kù)和主庫(kù)不在一個(gè)IP地址上。
什么是slots
一個(gè) Redis 集群包含 16384 個(gè)插槽(hash slot),數(shù)據(jù)庫(kù)中的每個(gè)鍵都屬于這 16384 個(gè)插槽的其中一個(gè)。集群使用公式 CRC16(key) % 16384 來計(jì)算鍵 key 屬于哪個(gè)槽, 其中 CRC16(key) 語(yǔ)句用于計(jì)算鍵 key 的 CRC16 校驗(yàn)和 。
集群中的每個(gè)節(jié)點(diǎn)負(fù)責(zé)處理一部分插槽。 舉個(gè)例子, 如果一個(gè)集群可以有主節(jié)點(diǎn), 其中:
- 節(jié)點(diǎn) A 負(fù)責(zé)處理 0 號(hào)至 5460 號(hào)插槽。
- 節(jié)點(diǎn) B 負(fù)責(zé)處理 5461 號(hào)至 10922 號(hào)插槽。
- 節(jié)點(diǎn) C 負(fù)責(zé)處理 10923 號(hào)至 16383 號(hào)插槽。
在集群中錄入值
在redis-cli每次錄入、查詢鍵值,redis都會(huì)計(jì)算出該key應(yīng)該送往的插槽,如果不是該客戶端對(duì)應(yīng)服務(wù)器的插槽,redis會(huì)報(bào)錯(cuò),并告知應(yīng)前往的redis實(shí)例地址和端口。
redis-cli客戶端提供了 –c 參數(shù)實(shí)現(xiàn)自動(dòng)重定向。如 redis-cli -c –p 6379 登入后,再錄入、查詢鍵值對(duì)可以自動(dòng)重定向。不在一個(gè)slot下的鍵值,是不能使用mget,mset等多鍵操作。
故障恢復(fù)
如果主節(jié)點(diǎn)下線?從節(jié)點(diǎn)能否自動(dòng)升為主節(jié)點(diǎn)?注意:15秒超時(shí)
主節(jié)點(diǎn)恢復(fù)后,主從關(guān)系會(huì)如何?主節(jié)點(diǎn)回來變成從機(jī)。
如果所有某一段插槽的主從節(jié)點(diǎn)都宕掉,redis服務(wù)是否還能繼續(xù)?
如果某一段插槽的主從都掛掉,而cluster-require-full-coverage 為yes ,那么整個(gè)集群都掛掉。
如果某一段插槽的主從都掛掉,而cluster-require-full-coverage 為no ,那么,該插槽數(shù)據(jù)全都不能使用,也無法存儲(chǔ)。
Redis 集群優(yōu)點(diǎn)
實(shí)現(xiàn)擴(kuò)容
分?jǐn)倝毫?/p>
無中心配置相對(duì)簡(jiǎn)單
Redis 集群不足
多鍵操作是不被支持的。
多鍵的Redis事務(wù)是不被支持的,lua腳本不被支持。
由于集群方案出現(xiàn)較晚,很多公司已經(jīng)采用了其他的集群方案,而代理或者客戶端分片的方案想要遷移至redis cluster,需要整體遷移而不是逐步過渡,復(fù)雜度較大。
Redis應(yīng)用問題解決
緩存穿透
-
問題描述
- key對(duì)應(yīng)的數(shù)據(jù)在數(shù)據(jù)源并不存在,每次針對(duì)此key的請(qǐng)求從緩存獲取不到,請(qǐng)求都會(huì)壓到數(shù)據(jù)源(數(shù)據(jù)庫(kù)),從而可能壓垮數(shù)據(jù)源。比如用一個(gè)不存在的用戶id獲取用戶信息,不論緩存還是數(shù)據(jù)庫(kù)都沒有,若黑客利用此漏洞進(jìn)行攻擊可能壓垮數(shù)據(jù)庫(kù)。
-
緩存穿透發(fā)生的條件:
應(yīng)用服務(wù)器壓力變大
redis命中率降低
-
一直查詢數(shù)據(jù)庫(kù),使得數(shù)據(jù)庫(kù)壓力太大而壓垮
其實(shí)redis在這個(gè)過程中一直平穩(wěn)運(yùn)行,崩潰的是我們的數(shù)據(jù)庫(kù)(如MySQL)。
緩存穿透發(fā)生的原因:黑客或者其他非正常用戶頻繁進(jìn)行很多非正常的url訪問,使得redis查詢不到數(shù)據(jù)庫(kù)。
-
解決方案
- 對(duì)空值緩存:如果一個(gè)查詢返回的數(shù)據(jù)為空(不管是數(shù)據(jù)是否不存在),我們?nèi)匀话堰@個(gè)空結(jié)果(null)進(jìn)行緩存,設(shè)置空結(jié)果的過期時(shí)間會(huì)很短,最長(zhǎng)不超過五分鐘。
- 設(shè)置可訪問的名單(白名單):使用bitmaps類型定義一個(gè)可以訪問的名單,名單id作為bitmaps的偏移量,每次訪問和bitmap里面的id進(jìn)行比較,如果訪問id不在bitmaps里面,進(jìn)行攔截,不允許訪問。
- 采用布隆過濾器:布隆過濾器(Bloom Filter)是1970年由布隆提出的。它實(shí)際上是一個(gè)很長(zhǎng)的二進(jìn)制向量(位圖)和一系列隨機(jī)映射函數(shù)(哈希函數(shù))。布隆過濾器可以用于檢索一個(gè)元素是否在一個(gè)集合中。它的優(yōu)點(diǎn)是空間效率和查詢時(shí)間都遠(yuǎn)遠(yuǎn)超過一般的算法,缺點(diǎn)是有一定的誤識(shí)別率和刪除困難。
- 進(jìn)行實(shí)時(shí)監(jiān)控:當(dāng)發(fā)現(xiàn)Redis的命中率開始急速降低,需要排查訪問對(duì)象和訪問的數(shù)據(jù),和運(yùn)維人員配合,可以設(shè)置黑名單限制服務(wù)。
緩存擊穿
問題描述
key對(duì)應(yīng)的數(shù)據(jù)存在,但在redis中過期,此時(shí)若有大量并發(fā)請(qǐng)求過來,這些請(qǐng)求發(fā)現(xiàn)緩存過期一般都會(huì)從后端數(shù)據(jù)庫(kù)加載數(shù)據(jù)并回設(shè)到緩存,這個(gè)時(shí)候大并發(fā)的請(qǐng)求可能會(huì)瞬間把后端數(shù)據(jù)庫(kù)壓垮。
-
緩存擊穿的現(xiàn)象:
- 數(shù)據(jù)庫(kù)訪問壓力瞬時(shí)增加,數(shù)據(jù)庫(kù)崩潰
- redis里面沒有出現(xiàn)大量key過期
- redis正常運(yùn)行
緩存擊穿發(fā)生的原因:redis某個(gè)key過期了,大量訪問使用這個(gè)key(熱門key)。
-
解決方案
key可能會(huì)在某些時(shí)間點(diǎn)被超高并發(fā)地訪問,是一種非常“熱點(diǎn)”的數(shù)據(jù)。
- 預(yù)先設(shè)置熱門數(shù)據(jù):在redis高峰訪問之前,把一些熱門數(shù)據(jù)提前存入到redis里面,加大這些熱門數(shù)據(jù)key的時(shí)長(zhǎng)。
- 實(shí)時(shí)調(diào)整:現(xiàn)場(chǎng)監(jiān)控哪些數(shù)據(jù)熱門,實(shí)時(shí)調(diào)整key的過期時(shí)長(zhǎng)。
- 使用鎖:
- 就是在緩存失效的時(shí)候(判斷拿出來的值為空),不是立即去load db。
- 先使用緩存工具的某些帶成功操作返回值的操作(比如Redis的SETNX)去set一個(gè)mutex key。
- 當(dāng)操作返回成功時(shí),再進(jìn)行l(wèi)oad db的操作,并回設(shè)緩存,最后刪除mutex key;
- 當(dāng)操作返回失敗,證明有線程在load db,當(dāng)前線程睡眠一段時(shí)間再重試整個(gè)get緩存的方法。
緩存雪崩
- 問題描述
- key對(duì)應(yīng)的數(shù)據(jù)存在,但在redis中過期,此時(shí)若有大量并發(fā)請(qǐng)求過來,這些請(qǐng)求發(fā)現(xiàn)緩存過期一般都會(huì)從后端數(shù)據(jù)庫(kù)加載數(shù)據(jù)并回設(shè)到緩存,這個(gè)時(shí)候大并發(fā)的請(qǐng)求可能會(huì)瞬間把后端數(shù)據(jù)庫(kù)壓垮。
- 緩存雪崩與緩存擊穿的區(qū)別在于這里針對(duì)很多key緩存,前者則是某一個(gè)key正常訪問。
- 解決方案
- 構(gòu)建多級(jí)緩存架構(gòu):nginx緩存 + redis緩存 +其他緩存(ehcache等)。
- 使用鎖或隊(duì)列:用加鎖或者隊(duì)列的方式來保證不會(huì)有大量的線程對(duì)數(shù)據(jù)庫(kù)一次性進(jìn)行讀寫,從而避免失效時(shí)大量的并發(fā)請(qǐng)求落到底層存儲(chǔ)系統(tǒng)上,該方法不適用高并發(fā)情況。
- 設(shè)置過期標(biāo)志更新緩存:記錄緩存數(shù)據(jù)是否過期(設(shè)置提前量),如果過期會(huì)觸發(fā)通知另外的線程在后臺(tái)去更新實(shí)際key的緩存。
- 將緩存失效時(shí)間分散開:比如可以在原有的失效時(shí)間基礎(chǔ)上增加一個(gè)隨機(jī)值,比如1-5分鐘隨機(jī),這樣每一個(gè)緩存的過期時(shí)間的重復(fù)率就會(huì)降低,就很難引發(fā)集體失效的事件。
分布式鎖
-
問題描述
- 隨著業(yè)務(wù)發(fā)展的需要,原單體單機(jī)部署的系統(tǒng)被演化成分布式集群系統(tǒng)后,由于分布式系統(tǒng)多線程的特點(diǎn)以及分布在不同機(jī)器上,這將使原單機(jī)部署情況下的并發(fā)控制鎖策略失效,單純的Java API并不能提供分布式鎖的能力。為了解決這個(gè)問題就需要一種跨JVM的互斥機(jī)制來控制共享資源的訪問,這就是分布式鎖要解決的問題!
-
分布式鎖主流的實(shí)現(xiàn)方案:
- 基于數(shù)據(jù)庫(kù)實(shí)現(xiàn)分布式鎖
- 基于緩存(Redis等)
- 基于Zookeeper
-
根據(jù)實(shí)現(xiàn)方式,分布式鎖還可以分為類CAS自旋式分布式鎖以及event事件類型分布式鎖:
- 類CAS自旋式分布式鎖:詢問的方式,類似java并發(fā)編程中的線程獲詢問的方式嘗試加鎖,如mysql、redis。
- 另外一類是event事件通知進(jìn)程后續(xù)鎖的變化,輪詢向外的過程,如zookeeper、etcd。
-
每一種分布式鎖解決方案都有各自的優(yōu)缺點(diǎn):
- 性能:redis最高
- 可靠性:zookeeper最高
-
解決方案:使用redis實(shí)現(xiàn)分布式鎖
- setnx:通過該命令嘗試獲得鎖,沒有獲得鎖的線程會(huì)不斷等待嘗試。
- set key ex 3000nx:設(shè)置過期時(shí)間,自動(dòng)釋放鎖,解決當(dāng)某一個(gè)業(yè)務(wù)異常而導(dǎo)致鎖無法釋放的問題。但是當(dāng)業(yè)務(wù)運(yùn)行超過過期時(shí)間時(shí),開辟監(jiān)控線程增加該業(yè)務(wù)的運(yùn)行時(shí)間,直到運(yùn)行結(jié)束,釋放鎖。
- uuid:設(shè)置uuid,釋放前獲取這個(gè)值,判斷是否自己的鎖,防止誤刪鎖,造成沒鎖的情況。
RedLock
Redlock是一種算法,Redlock也就是 Redis Distributed Lock,可用實(shí)現(xiàn)多節(jié)點(diǎn)redis的分布式鎖。RedLock官方推薦,Redisson完成了對(duì)Redlock算法封裝。
-
此種方式具有以下特性:
- 互斥訪問:即永遠(yuǎn)只有一個(gè) client 能拿到鎖。
- 避免死鎖:最終 client 都可能拿到鎖,不會(huì)出現(xiàn)死鎖的情況,即使鎖定資源的服務(wù)崩潰或者分區(qū),仍然能釋放鎖。
- 容錯(cuò)性:只要大部分 Redis 節(jié)點(diǎn)存活(一半以上),就可以正常提供服務(wù)
redis分布式鎖實(shí)現(xiàn)
private int score = 100;
boolean b = false;
@Autowired
RedissonClient redissonClient;
@Test
public void test() {
for (int i = 0; i < 3; i++) {
new Thread(new Runnable() {
@Override
public void run() {
redisLock();
}
}).start();
}
}
private void redisLock() {
// 問題代碼
// while (true) {
// if (score > 0) {
// System.out.println(score);
// score -= 1;
// }
// }
// 分布式鎖解決庫(kù)存問題
RLock redLock = redissonClient.getLock("RED_LOCK_KEY");
System.out.println("redLock=" + redLock);
try {
// 嘗試加鎖,最多等待500ms,上鎖以后10s自動(dòng)解鎖
b = redLock.tryLock(5000, 10000, TimeUnit.MILLISECONDS);
System.out.println("b=" + b);
while (true) {
if (b) {
//獲取鎖成功
if (score > 0) {
System.out.println(score);
score -= 1;
}
} else {
break;
}
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
if (b) {
redLock.unlock();
}
}
}
redis緩存與數(shù)據(jù)庫(kù)雙寫一致性問題
其實(shí)從理論上來講,給緩存設(shè)置過期時(shí)間才是保證最終一致性的最終解決方案。
所有的寫操作我們以數(shù)據(jù)庫(kù)為準(zhǔn),對(duì)于緩存操作我們盡最大努力即可。
redis的數(shù)據(jù)過期清除策略和內(nèi)存淘汰策略
- 數(shù)據(jù)過期策略:定期刪除 + 惰性刪除
定期刪除:redis默認(rèn)每隔100s就隨機(jī)抽取一些設(shè)置了過期時(shí)間的數(shù)據(jù)的key,檢查其是否過期,如果過期,就刪除。
至于為什么是隨機(jī)抽取,是因?yàn)榧尤雛edis存儲(chǔ)了幾十萬了key,如果每隔100s就遍歷所有的key,是很消耗性能的。
為什么不用定時(shí)刪除呢?定時(shí)刪除時(shí)用一個(gè)定時(shí)器監(jiān)視key,過期則自動(dòng)刪除。雖然內(nèi)存得到及時(shí)釋放,
但是很消耗性能。
惰性刪除:定期刪除可能導(dǎo)致很多過期的key沒有刪除。這時(shí)候就需要惰性刪除,
惰性刪除時(shí)當(dāng)獲取某個(gè)key的時(shí)候,redis會(huì)檢查這個(gè)key是否過期,如果過期了,
就會(huì)被刪除。
- 定期刪除和惰性刪除存在的問題:
- 如果一個(gè)過期的key定期刪除沒有刪除,然后也沒有再次請(qǐng)求這個(gè)key,也就是惰性刪除也沒生效,這時(shí)候這個(gè)key就會(huì)一直留在內(nèi)存中,如果內(nèi)存中堆積了大量的這樣的key,就會(huì)導(dǎo)致內(nèi)存消耗殆盡,這時(shí)候就用到了內(nèi)存淘汰策略。
redis的內(nèi)存淘汰策略
noevicition:不進(jìn)行淘汰數(shù)據(jù)。一旦緩存被寫滿,再有寫的請(qǐng)求進(jìn)來,redis就不在提供寫的服務(wù),直接返回報(bào)錯(cuò)。
volatile-ttl:在設(shè)置了過期時(shí)間的鍵值對(duì)中,移除即將過期的鍵值對(duì)
volatile-random:在設(shè)置了過期時(shí)間的鍵值對(duì)中,隨機(jī)一處某個(gè)key
volatile-lru:在設(shè)置了過期時(shí)間的鍵值對(duì)中,移除最近很少使用的鍵值對(duì)
volatile-lfu:在設(shè)置了過期時(shí)間的鍵值對(duì)中,移除最近最不頻繁使用的鍵值對(duì)
allkeys-random:在所有鍵值對(duì)中,隨機(jī)移除某個(gè)key
allkeys-lru:在所有的鍵值對(duì)中,移除最近最少使用的鍵值對(duì)。
allkeys-lfu:在所有的鍵值對(duì)中移除最近最少使用的鍵值對(duì)
- 通常情況下默認(rèn)使用alkeys-lru策略,這樣可以充分利用LRU這一經(jīng)典緩存算法的優(yōu)勢(shì),把最近長(zhǎng)訪問的數(shù)據(jù)留在緩存中,提升應(yīng)用范文性能。
- 如果你的業(yè)務(wù)中有明顯的冷熱分區(qū),建議使用allkeys-lru策略
- 如果沒有明顯得冷熱分區(qū),建議使用allkeys-random策略,隨機(jī)淘汰數(shù)據(jù)就行
- 對(duì)于沒有設(shè)置過期時(shí)間的鍵值對(duì),volatile開頭的策略和noevicition是一樣的效果。