1. 分布式數(shù)據(jù)庫的CAP原理
- Consistency:強(qiáng)一致性
- Availability:可用性
- Partitition tolerance:分區(qū)容錯(cuò)性
- 只能三選二:
CA:傳統(tǒng)關(guān)系型數(shù)據(jù)庫
AP:大型網(wǎng)站
CP: Redis、Mongodb
2. BASE
- Basically Available基本可用
- Soft state 軟狀態(tài)
- Eventually consistent 最終一致性
3. Redis
??Remote dictionary server(遠(yuǎn)程字典服務(wù)器)是一個(gè)高性能的(key/value)分布式內(nèi)存數(shù)據(jù)庫,基于內(nèi)存運(yùn)行,并支持持久化的NoSQL數(shù)據(jù)庫。具有如下特點(diǎn):
- redis支持?jǐn)?shù)據(jù)的持久化,可以將內(nèi)存中的數(shù)據(jù)保持在磁盤中,重啟時(shí)可以再次加載進(jìn)行使用;
- redis不僅支持key/value類型的數(shù)據(jù),還提供list,set,zset,String等數(shù)據(jù)結(jié)構(gòu)的存儲;
- redis支持?jǐn)?shù)據(jù)的備份,即master-salve模式的數(shù)據(jù)備份。
4. Redis五大數(shù)據(jù)類型及應(yīng)用場景
- String 最多512M,以一種純字符串作為value的形式存在的。value可以存儲json格式、數(shù)值型等。
- string使用場景一般是存儲簡單的鍵值類型。比如用戶信息,登錄信息,配置信息等。
- string的incr/decr操作,即自減/自增操作。調(diào)用它是原子性的,無論調(diào)用多少次,都一一計(jì)算成功。例如需要增減庫存的操作。
- List 底層是一個(gè)鏈表,在redis中,插入list中的值,只需要找到list的key即可,而不需要像hash一樣插入兩層的key。list是一種有序的、可重復(fù)的集合。
- list可以使用左推、左拉、右推、右拉的方式。所以你可以使用list作為集合存儲,比如存儲某寶商鋪里面的所有商品。
- 也可以用作輕量級別的隊(duì)列來使用。左推左拉、右推右拉。需要注意的是盡管redis可以使用推拉的隊(duì)列模式,但是一定要注意場景。因?yàn)閞edis的隊(duì)列是一種輕量級別的,沒有隊(duì)列重試、隊(duì)列重放機(jī)制。消費(fèi)完隊(duì)列消息在redis代表已經(jīng)刪除了。
- Hash String類型的field和value的映射表,hash特別適合適用于存儲對象。在redis中,hash因?yàn)槭且粋€(gè)集合,所以有兩層。第一層是key:hash集合value,第二層是hashkey:string value。所以判斷是否采用hash的時(shí)候可以參照有兩層key的設(shè)計(jì)來做參考。并且注意的是,設(shè)置過期時(shí)間只能在第一層的key上面設(shè)置。
- 使用hash,一般是有那種需要兩層key的應(yīng)用場景,也可以是‘刪除一個(gè)key可以刪除所有內(nèi)容’的場景。例如一個(gè)商品有很多規(guī)格,規(guī)格里面有不同的值。
- 如果需要?jiǎng)h除商品時(shí),可以一次性刪除‘商品id’的key,則商品里面的所有規(guī)格也會刪除,而不需要找到對應(yīng)的規(guī)格再做處理。如果查找商品id與規(guī)格id1的商品時(shí),則通過兩個(gè)key查找即可。
- 或者查找所有商品的規(guī)格,查找商品id即可。
- 需要注意的是,經(jīng)過測試,在性能上來說一般hash里面的第二層key,不要超過200個(gè)為佳。盡管hash里面的key-value能達(dá)到500多MB的存儲容量。
- Set 是一種無序的,不能重復(fù)的集合。并且在redis中,只有一個(gè)key。
- 如保存一些標(biāo)簽的名字。標(biāo)簽的名字不可以重復(fù),順序是可以無序的。
- ZSet(Sorted Set:有序集合) 每個(gè)元素都會關(guān)聯(lián)一個(gè)double類型的分?jǐn)?shù),分?jǐn)?shù)允許重復(fù)
- 排行榜
5. Redis String的實(shí)現(xiàn)
??Redis雖然是用C語言寫的,但卻沒有直接用C語言的字符串,而是自己實(shí)現(xiàn)了一套字符串。目的就是為了提升速度,提升性能。Redis構(gòu)建了一個(gè)叫做簡單動(dòng)態(tài)字符串(Simple Dynamic String),簡稱SDS
struct sdshdr{
// 記錄已使用長度
int len;
// 記錄空閑未使用的長度
int free;
// 字符數(shù)組
char[] buf;
};
Redis的字符串也會遵守C語言的字符串的實(shí)現(xiàn)規(guī)則,即最后一個(gè)字符為空字符。然而這個(gè)空字符不會被計(jì)算在len里頭。
- Redis動(dòng)態(tài)擴(kuò)展步驟:
- 計(jì)算出大小是否足夠
- 開辟空間至滿足所需大小
- 開辟與已使用大小len相同長度的空閑free空間(如果len < 1M),開辟1M長度的空閑free空間(如果len >= 1M)
- Redis字符串的性能優(yōu)勢
- 快速獲取字符串長度:直接返回len
- 避免緩沖區(qū)溢出:每次追加字符串時(shí)都會檢查空間是否夠用
- 降低空間分配次數(shù)提升內(nèi)存使用效率:(1)空間預(yù)分配;(2)惰性空間回收
6. Redis持久化
- RDB(redis database)在指定時(shí)間間隔內(nèi)將內(nèi)存中的數(shù)據(jù)集快照寫入磁盤,也就是snapshot快照,恢復(fù)時(shí)是將快照文件直接讀到內(nèi)存里。Redis會單獨(dú)創(chuàng)建(fork:復(fù)制一個(gè)與當(dāng)前進(jìn)程一樣的進(jìn)程)一個(gè)子進(jìn)程來進(jìn)行持久化,會先將數(shù)據(jù)寫入到臨時(shí)文件,待持久化過程結(jié)束,再替換上次持久化好的文件(dump.rdb)。主進(jìn)程不進(jìn)行IO操作。如果需要進(jìn)行大規(guī)模數(shù)據(jù)的恢復(fù),且對數(shù)據(jù)完整性不敏感,那么RDB比AOF更高效。缺點(diǎn)就是最后一次持久化的數(shù)據(jù)可能丟失。
- 默認(rèn):1分鐘改了一萬次,5分鐘改了10次,15分鐘改了一次
- AOF以日志的形式來記錄每個(gè)寫操作,將redis執(zhí)行過的所有寫的指令記錄下來(讀操作不記錄),只需追加文件當(dāng)不可以改寫文件,redis重啟的話就根據(jù)日志文件的內(nèi)容將寫指令從前到后執(zhí)行一次以完成數(shù)據(jù)的恢復(fù)工作。
- 同步策略:always一直同步、everysec每秒同步、no不同步
- AOF的優(yōu)點(diǎn):
- 備份機(jī)制更穩(wěn)健,丟失數(shù)據(jù)概率低
- 可讀的日志文本,可以處理誤操作
- AOF的缺點(diǎn):
- 比RDB占用更多的磁盤空間
- 恢復(fù)備份速度慢
- 每次讀寫同步的話有一定的性能壓力
- 存在個(gè)別的bug,造成不能恢復(fù)
- AOF重寫機(jī)制:當(dāng)aof文件的大小超過所設(shè)定的閾值時(shí),redis就會啟動(dòng)AOF文件的內(nèi)容壓縮,只保留可以恢復(fù)數(shù)據(jù)的最小指令集,可以使用命令bgrewriteaof,fork出一條新進(jìn)程來將文件重寫,redis會記錄上次重寫時(shí)的AOF大小,默認(rèn)配置是當(dāng)AOF文件大小是上次rewrite后大小的1倍且文件大于64M時(shí)觸發(fā)。
7. Redis的事務(wù)
- 定義:可以一次執(zhí)行多個(gè)命令,部分支持事務(wù)
- 命令:MULTI開啟事務(wù)、EXEC執(zhí)行事務(wù)、DISCARD放棄事務(wù)、WATCH監(jiān)視一個(gè)或多個(gè)key,如果在事務(wù)執(zhí)行之前這個(gè)key被其他命令所改動(dòng),那么事務(wù)將被打斷、UNWACTH一旦執(zhí)行了EXEC之前加的所有的監(jiān)控鎖都會被取消。
- 特性:
- 單獨(dú)的隔離操作:事務(wù)中所有的命令都會序列化、按順序地執(zhí)行。事務(wù)執(zhí)行過程中,不會被其他客戶端發(fā)送過來的命令請求所打斷。
- 沒有隔離級別的概念
- 不保證原子性:只要有一條命令執(zhí)行失敗,其他的命令仍然會執(zhí)行,不支持回滾
8. LUA腳本
- 定義:LUA是一種小巧的腳本語言,可以很容易地被C/C++調(diào)用,也可以調(diào)用C/C++函數(shù),一個(gè)完整的LUA解釋器不超過200k,適合作為嵌入式腳本語言。
- 在redis中的優(yōu)勢
- 將復(fù)雜或者多步的redis操作,寫為一個(gè)腳本,一次性提交給redis執(zhí)行,減少反復(fù)連接redis的次數(shù),提升性能。
- LUA腳本類似redis事務(wù),有一定的原子性,不會被其他命令插隊(duì)
9. Redis內(nèi)存淘汰策略
- 這八種大體上可以分為4種,lru、lfu、random、ttl。
1) volatile-lru:從已設(shè)置過期時(shí)間的數(shù)據(jù)集中挑選最近最少使用的數(shù)據(jù)淘汰。
2) volatile-ttl:從已設(shè)置過期時(shí)間的數(shù)據(jù)集中挑選將要過期的數(shù)據(jù)淘汰。
3) volatile-random:從已設(shè)置過期時(shí)間的數(shù)據(jù)集中任意選擇數(shù)據(jù)淘汰。
4) volatile-lfu:從已設(shè)置過期時(shí)間的數(shù)據(jù)集挑選使用頻率最低的數(shù)據(jù)淘汰。
5) allkeys-lru:從數(shù)據(jù)集中挑選最近最少使用的數(shù)據(jù)淘汰
6) allkeys-lfu:從數(shù)據(jù)集中挑選使用頻率最低的數(shù)據(jù)淘汰。
7) allkeys-random:從數(shù)據(jù)集(server.db[i].dict)中任意選擇數(shù)據(jù)淘汰
8) no-enviction(驅(qū)逐):禁止驅(qū)逐數(shù)據(jù),這也是默認(rèn)策略。意思是當(dāng)內(nèi)存不足以容納新入數(shù)據(jù)時(shí),新寫入操作就會報(bào)錯(cuò),請求可以繼續(xù)進(jìn)行,線上任務(wù)也不能持續(xù)進(jìn)行,采用no-enviction策略可以保證數(shù)據(jù)不被丟失。
10. 秒殺常見問題
- 連接超時(shí)
- 使用連接池
- 超賣問題
- 使用事務(wù)
- 庫存遺留
- 使用LUA腳本
11. Redis發(fā)布訂閱
- SUBSCRIBE c1 c2 c3
- PUBLISH c2 hello-redis
12. Redis主從復(fù)制
- 配從不配主:slaveof 主庫IP 主庫端口,每次與master斷開之后,都需要重新連接,除非配置redis.conf文件。
- 配置文件細(xì)節(jié)操作:
- 拷貝多個(gè)redis.conf文件
- 開啟daemonize yes
- Pid文件名字
- 指定端口
- Log文件名字
- Dump.rdb名字
- 常用招式
- 一主二仆
- Info replication:查看信息
-
SLAVEOF 127.0.0.1 6379:配置從庫
image.png
13. 哨兵模式
- 定義:反客為主(slaveof no one)自動(dòng)化,能夠監(jiān)控主機(jī)是否故障,如果故障根據(jù)投票數(shù)自動(dòng)將從庫轉(zhuǎn)為主庫
- 使用步驟:
- 調(diào)整結(jié)構(gòu),6379帶著80、81
- 自定義的/myredis目錄下新建sentinel.conf文件
- 配置哨兵,填寫內(nèi)容:Sentinel monitor host637(被監(jiān)控?cái)?shù)據(jù)庫名字(自己起名字))127.0.0.1 6379 1(多于1票則設(shè)為主機(jī))
- 啟動(dòng)哨兵:Redis-sentinel /myredis/sentinel.conf
14. Java使用redis
- 連接:Jedis jedis = new Jedis(“127.0.0.1”,6379);
- 插入:jedis.set(“k1”,”v1”);
- 事務(wù):
Transaction transaction = jedis.multi();
transaction.set(“k2”,”v2”);
transaction.set(“k3”,”v3”);
transaction.exec();
- 加鎖:
public class TestTransaction {
public boolean transMethod() {
Jedis jedis = new Jedis("127.0.0.1", 6379);
int balance;// 可用余額
int debt;// 欠額
int amtToSubtract = 10;// 實(shí)刷額度
jedis.watch("balance");
//jedis.set("balance","5");//此句不該出現(xiàn)。模擬其他程序已經(jīng)修改了該條目
balance = Integer.parseInt(jedis.get("balance"));
if (balance < amtToSubtract) {
jedis.unwatch();
System.out.println("modify");
return false;
} else {
System.out.println("***********transaction");
Transaction transaction = jedis.multi();
transaction.decrBy("balance", amtToSubtract);
transaction.incrBy("debt", amtToSubtract);
transaction.exec();
balance = Integer.parseInt(jedis.get("balance"));
debt = Integer.parseInt(jedis.get("debt"));
System.out.println("*******" + balance);
System.out.println("*******" + debt);
return true;
}
}
/**
* 通俗點(diǎn)講,watch命令就是標(biāo)記一個(gè)鍵,如果標(biāo)記了一個(gè)鍵, 在提交事務(wù)前如果該鍵被別人修改過,那事務(wù)就會失敗,這種情況通常可以在程序中重新再嘗試一次。
* 首先標(biāo)記了鍵balance,然后檢查余額是否足夠,不足就取消標(biāo)記,并不做扣減; 足夠的話,就啟動(dòng)事務(wù)進(jìn)行更新操作,
* 如果在此期間鍵balance被其它人修改, 那在提交事務(wù)(執(zhí)行exec)時(shí)就會報(bào)錯(cuò), 程序中通常可以捕獲這類錯(cuò)誤再重新執(zhí)行一次,直到成功。
*/
public static void main(String[] args) {
TestTransaction test = new TestTransaction();
boolean retValue = test.transMethod();
System.out.println("main retValue-------: " + retValue);
}
}
- 主從復(fù)制
- 配置從庫:Jedis jedis_s = new Jedis(“127.0.0.1”,6380);
- Jedis_s.slaveof(“127.0.0.1”,6379);
- JedisPool
JedisPoolConfig poolConfig = new JedisPoolConfig( );
poolConfig.setMaxActive ( 1000);
poolconfig.setMaxIdle ( 32);
poolconfig. setMaxwait (100*1000);poolconfig.setTestOnBorrow(true);
jedisPool = new JedisPool(poolConfig, "127.0.0.1",6379);
- maxActive:控制一個(gè)pool可分配多少個(gè)jedis實(shí)例,通過pool.getResource()來獲取;如果賦值為-1,則表示不限制;如果pool已經(jīng)分配了maxActive個(gè)jedis實(shí)例,則此時(shí)pool的狀態(tài)為exhausted。
- maxIdle:控制一個(gè)pool最多有多少個(gè)狀態(tài)為idle(空閑)的jedis實(shí)例;
- whenExhaustedAction:表示當(dāng)pool中的jedis實(shí)例都被allocated完時(shí),pool要采取的操作:默認(rèn)有三種。
- WHEN_EXHAUSTED_FAIL -->表示無jedis實(shí)例時(shí),直接拋出NoSuchElementException;
- WHEN_EXHAUSTED_BLOCK -->則表示阻塞住,或者達(dá)到maxWait時(shí)拋出JedisConnectionException;
- WHEN_EXHAUSTED_GRoW -->則表示新建一個(gè)jedis實(shí)例,也就說設(shè)置的maxActive無用;
- maxWait:表示當(dāng)borrow一個(gè)jedis實(shí)例時(shí),最大的等待時(shí)間,如果超過等待時(shí)間,則直接拋JedisConnectionException;
- testOnBorrow:獲得一個(gè)jedis實(shí)例的時(shí)候是否檢查連接可用性(ping());如果為true,則得到的jedis實(shí)例均是可用的:
15. 解決session存儲問題
- 方案一:存在cookie里
- 不安全
- 網(wǎng)絡(luò)負(fù)擔(dān)效率低
- 方案二:存在文件服務(wù)器或數(shù)據(jù)庫里
- 大量的IO效率問題
- 方案三:session復(fù)制
- Session數(shù)據(jù)冗余
- 節(jié)點(diǎn)越多浪費(fèi)越大
- 方案四:緩存數(shù)據(jù)庫
- 完全存在內(nèi)存中,速度快數(shù)據(jù)結(jié)構(gòu)簡單
16. 單線程+多路IO復(fù)用
??多路復(fù)用是指用一個(gè)線程來檢查多個(gè)文件描述符(socket)的就緒狀態(tài),比如調(diào)用select、poll、epoll函數(shù)進(jìn)行監(jiān)視,傳入多個(gè)文件描述符,如果有一個(gè)文件描述符就緒,則返回,否則阻塞直到超時(shí)。得到就緒狀態(tài)后進(jìn)行真正的操作可以在同一個(gè)線程里執(zhí)行,也可以啟動(dòng)線程執(zhí)行(比如使用線程池)。
??多路I/O復(fù)用模型是利用 select、poll、epoll 可以同時(shí)監(jiān)察多個(gè)流的 I/O 事件的能力,在空閑的時(shí)候,會把當(dāng)前線程阻塞掉,當(dāng)有一個(gè)或多個(gè)流有 I/O 事件時(shí),就從阻塞態(tài)中喚醒,于是程序就會輪詢一遍所有的流(epoll 是只輪詢那些真正發(fā)出了事件的流),并且只依次順序的處理就緒的流,這種做法就避免了大量的無用操作。這里“多路”指的是多個(gè)網(wǎng)絡(luò)連接,“復(fù)用”指的是復(fù)用同一個(gè)線程。
- Select:每一個(gè)請求都進(jìn)行詢問,最多1024個(gè)
- Poll:每一個(gè)請求都進(jìn)行詢問不限制數(shù)量
- Epoll:監(jiān)視請求時(shí)為每個(gè)請求設(shè)置標(biāo)識符,不需要一一詢問
17. Select、poll、epoll
- select的幾大缺點(diǎn):
1)每次調(diào)用select,都需要把fd集合從用戶態(tài)拷貝到內(nèi)核態(tài),這個(gè)開銷在fd很多時(shí)會很大
2)同時(shí)每次調(diào)用select都需要在內(nèi)核遍歷傳遞進(jìn)來的所有fd,這個(gè)開銷在fd很多時(shí)也很大
3)select支持的文件描述符數(shù)量太小了,默認(rèn)是1024 - 為什么epoll比select和poll更高效?
1)減少了用戶態(tài)和內(nèi)核態(tài)之間文件描述符的拷貝
2)減少了對就緒文件描述符的遍歷
3)select和poll只支持LT模式,而epoll支持高效的ET模式,并且epoll還支持EPOLLONESHOT事件。 - 無論哪種情況下,epoll都比select和poll高效嗎?
- epoll適用于連接較多,活動(dòng)數(shù)量較少的情況。
1)epoll為了實(shí)現(xiàn)返回就緒的文件描述符,維護(hù)了一個(gè)紅黑樹和好多個(gè)等待隊(duì)列,內(nèi)核開銷很大。如果此時(shí)監(jiān)聽了很少的文件描述符,底層的開銷會得不償失;
2)epoll中注冊了回調(diào)函數(shù),當(dāng)有事件發(fā)生時(shí),服務(wù)器設(shè)備驅(qū)動(dòng)調(diào)用回調(diào)函數(shù)將就緒的fd掛在rdllist上,如果有很多的活動(dòng),同一時(shí)間需要調(diào)用的回調(diào)函數(shù)數(shù)量太多,服務(wù)器壓力太大。 - select和poll適用于連接較少的情況。
1)當(dāng)select和poll上監(jiān)聽的fd數(shù)量較少,內(nèi)核通知用戶現(xiàn)在有就緒事件發(fā)生,應(yīng)用程序判斷當(dāng)前是哪個(gè)fd就緒所消耗的時(shí)間復(fù)雜度就會大大減小。
- epoll適用于連接較多,活動(dòng)數(shù)量較少的情況。
18. REDIS緩存穿透,緩存擊穿,緩存雪崩原因+解決方案
- 緩存穿透:key對應(yīng)的數(shù)據(jù)在數(shù)據(jù)庫和緩存并不存在,每次針對此key的請求從緩存獲取不到,請求都會到數(shù)據(jù)庫,從而可能壓垮數(shù)據(jù)庫。比如用一個(gè)不存在的用戶id獲取用戶信息,不論緩存還是數(shù)據(jù)庫都沒有,若黑客利用此漏洞進(jìn)行攻擊可能壓垮數(shù)據(jù)庫。
- 解決方案:
1)最常見的則是采用布隆過濾器,在寫入數(shù)據(jù)庫時(shí),將數(shù)據(jù)哈希到一個(gè)足夠大的bitmap中,一個(gè)一定不存在的數(shù)據(jù)會被這個(gè)bitmap攔截掉,從而避免了對底層存儲系統(tǒng)的查詢壓力。
2)簡單粗暴的方法:如果一個(gè)查詢返回的數(shù)據(jù)為空(不管是數(shù)據(jù)不存在,還是系統(tǒng)故障),我們?nèi)匀话堰@個(gè)空結(jié)果進(jìn)行緩存,但它的過期時(shí)間會很短,最長不超過五分鐘。
- 解決方案:
- 緩存擊穿:是指一個(gè)key非常熱點(diǎn),在不停的扛著大并發(fā),大并發(fā)集中對這一個(gè)點(diǎn)進(jìn)行訪問,當(dāng)這個(gè)key在失效的瞬間,持續(xù)的大并發(fā)就穿破緩存,直接請求數(shù)據(jù)庫,就像在一個(gè)屏障上鑿開了一個(gè)洞。
- 解決方案
1)使用互斥鎖:就是在緩存失效的時(shí)候(判斷拿出來的值為空),不是立即去load db,而是先使用緩存工具的某些帶成功操作返回值的操作(比如Redis的SETNX或者M(jìn)emcache的ADD)去set一個(gè)mutex key,當(dāng)操作返回成功時(shí),再進(jìn)行l(wèi)oad db的操作并回設(shè)緩存;否則,就重試整個(gè)get緩存的方法。
2)設(shè)置永不過期
- 解決方案
- 緩存雪崩:當(dāng)緩存服務(wù)器重啟或者大量緩存集中在某一個(gè)時(shí)間段失效,這樣在失效的時(shí)候,也會給后端系統(tǒng)(比如DB)帶來很大壓力。
- 大量數(shù)據(jù)同時(shí)過期解決方案
1)用加鎖或者隊(duì)列的方式保證來保證不會有大量的線程對數(shù)據(jù)庫一次性進(jìn)行讀寫,從而避免失效時(shí)大量的并發(fā)請求落到底層存儲系統(tǒng)上。加鎖排隊(duì)只是為了減輕數(shù)據(jù)庫的壓力,并沒有提高系統(tǒng)吞吐量。
2)將緩存失效時(shí)間分散開,比如我們可以在原有的失效時(shí)間基礎(chǔ)上增加一個(gè)隨機(jī)值,比如1-5分鐘隨機(jī),這樣每一個(gè)緩存的過期時(shí)間的重復(fù)率就會降低,就很難引發(fā)集體失效的事件。
3)雙key策略,主key設(shè)置過期時(shí)間,備key設(shè)置永久,主key過期時(shí),返回備key內(nèi)容
4)后臺緩存更新,定時(shí)更新、消息隊(duì)列通知更新 -
服務(wù)器宕機(jī)解決方案
1)服務(wù)熔斷:在分布式系統(tǒng)中,我們往往需要依賴下游服務(wù),不管是內(nèi)部系統(tǒng)還是第三方服務(wù),如果下游出現(xiàn)問題,我們不在盲目地去請求,在一個(gè)周期內(nèi)失敗達(dá)到一定次數(shù),不在請求,及時(shí)失敗。過一段時(shí)間,在逐步放開請求,這樣既能防止不斷的調(diào)用,使下游服務(wù)更壞,保護(hù)了下游方,還能降低自己的執(zhí)行成本,快速的響應(yīng),減少延遲,增加吞吐量。
2)服務(wù)降級:降級就是為了解決資源不足和訪問量增加的矛盾,在有限的資源情況下,為了能抗住大量的請求,就需要對系統(tǒng)做出一些犧牲,有點(diǎn)“棄卒保帥”的意思。放棄一些功能,保證整個(gè)系統(tǒng)能平穩(wěn)運(yùn)行。比如:搶購可以占時(shí)限流評論,將流量讓給秒殺業(yè)務(wù)
3)請求限流:通過對并發(fā)訪問進(jìn)行限速。最簡單的方式,把多余的請求直接拒絕掉,可以根據(jù)一定的用戶規(guī)則進(jìn)行拒絕策略
4)構(gòu)建redis高可靠集群
image.png
- 大量數(shù)據(jù)同時(shí)過期解決方案
19. 布隆過濾器
??布隆過濾器由「初始值都為 0 的位圖數(shù)組」和「 N 個(gè)哈希函數(shù)」兩部分組成。當(dāng)我們在寫入數(shù)據(jù)庫數(shù)據(jù)時(shí),在布隆過濾器里做個(gè)標(biāo)記,這樣下次查詢數(shù)據(jù)是否在數(shù)據(jù)庫時(shí),只需要查詢布隆過濾器,如果查詢到數(shù)據(jù)沒有被標(biāo)記,說明不在數(shù)據(jù)庫中。
布隆過濾器會通過 3 個(gè)操作完成標(biāo)記:
- 第一步,使用 N 個(gè)哈希函數(shù)分別對數(shù)據(jù)做哈希計(jì)算,得到 N 個(gè)哈希值;
- 第二步,將第一步得到的 N 個(gè)哈希值對位圖數(shù)組的長度取模,得到每個(gè)哈希值在位圖數(shù)組的對應(yīng)位置。
-
第三步,將每個(gè)哈希值在位圖數(shù)組的對應(yīng)位置的值設(shè)置為 1;
舉個(gè)例子,假設(shè)有一個(gè)位圖數(shù)組長度為 8,哈希函數(shù) 3 個(gè)的布隆過濾器。
image.png
??在數(shù)據(jù)庫寫入數(shù)據(jù) x 后,把數(shù)據(jù) x 標(biāo)記在布隆過濾器時(shí),數(shù)據(jù) x 會被 3 個(gè)哈希函數(shù)分別計(jì)算出 3 個(gè)哈希值,然后在對這 3 個(gè)哈希值對 8 取模,假設(shè)取模的結(jié)果為 1、4、6,然后把位圖數(shù)組的第 1、4、6 位置的值設(shè)置為 1。當(dāng)應(yīng)用要查詢數(shù)據(jù) x 是否數(shù)據(jù)庫時(shí),通過布隆過濾器只要查到位圖數(shù)組的第 1、4、6 位置的值是否全為 1,只要有一個(gè)為 0,就認(rèn)為數(shù)據(jù) x 不在數(shù)據(jù)庫中。
??布隆過濾器由于是基于哈希函數(shù)實(shí)現(xiàn)查找的,高效查找的同時(shí)存在哈希沖突的可能性,比如數(shù)據(jù) x 和數(shù)據(jù) y 可能都落在第 1、4、6 位置,而事實(shí)上,可能數(shù)據(jù)庫中并不存在數(shù)據(jù) y,存在誤判的情況。
??所以,查詢布隆過濾器說數(shù)據(jù)存在,并不一定證明數(shù)據(jù)庫中存在這個(gè)數(shù)據(jù),但是查詢到數(shù)據(jù)不存在,數(shù)據(jù)庫中一定就不存在這個(gè)數(shù)據(jù)。
20. 為什么要用redis而不用map做緩存?
- Redis 可以用幾十 G 內(nèi)存來做緩存,Map 不行,一般 JVM 也就分幾個(gè) G 數(shù)據(jù)就夠大了
- Redis 的緩存可以持久化,Map 是內(nèi)存對象,程序一重啟數(shù)據(jù)就沒了
- Redis 可以實(shí)現(xiàn)分布式的緩存,Map 只能存在創(chuàng)建它的程序里
- Redis 可以處理每秒百萬級的并發(fā),是專業(yè)的緩存服務(wù),Map 只是一個(gè)普通的對象
- Redis 緩存有過期機(jī)制,Map 本身無此功能
- Redis 有豐富的 API,Map 就簡單太多了
21. 如何保持緩存和數(shù)據(jù)庫的一致性?
- 淘汰緩存還是更新緩存?
選擇淘汰緩存,原因:數(shù)據(jù)可能為簡單數(shù)據(jù),也可能為較復(fù)雜的數(shù)據(jù),復(fù)雜數(shù)據(jù)進(jìn)行緩存的更新操作,成本較高,因此一般推薦淘汰緩存 - 先淘汰緩存還是先更新數(shù)據(jù)庫?
選擇先淘汰緩存,再更新數(shù)據(jù)庫,原因:假如先更新數(shù)據(jù)庫,再淘汰緩存,如果緩存淘汰失敗,那么后面的請求都會得到臟數(shù)據(jù),直至緩存過期。假如先淘汰緩存再更新數(shù)據(jù)庫,如果數(shù)據(jù)庫更新失敗,只會產(chǎn)生一次緩存miss,相比較而言,后者對業(yè)務(wù)影響更小一點(diǎn)。 - 延時(shí)雙刪策略:解決數(shù)據(jù)庫讀寫分離
public void write(String key,Object data){
redisUtils.del(key);
db.update(data);
Thread.Sleep(100);
redisUtils.del(key);
}
22. Redis分布式鎖的實(shí)現(xiàn)
- 加鎖:使用setnx key value命令,如果key不存在,設(shè)置value(加鎖成功)。如果已經(jīng)存在lock(也就是有客戶端持有鎖了),則設(shè)置失敗(加鎖失敗)。
- 解鎖:使用del命令,通過刪除鍵值釋放鎖。釋放鎖之后,其他客戶端可以通過setnx命令進(jìn)行加鎖。
23. Redis集群
- Redis集群解決內(nèi)存壓力,實(shí)現(xiàn)了對redis的水平擴(kuò)容,即啟動(dòng)N個(gè)redis節(jié)點(diǎn),將整個(gè)數(shù)據(jù)庫分布在N個(gè)節(jié)點(diǎn)中,每個(gè)節(jié)點(diǎn)存儲數(shù)據(jù)的1/N。
- Redis集群通過分區(qū)來提供一定程度的可用性,即使集群中有一部分節(jié)點(diǎn)失效或者無法進(jìn)行通訊,集群也可以基礎(chǔ)處理命令請求。
- Redis部署模型
-
模式一:單實(shí)例
image.png -
模式二:一主一從
image.png -
模式三:一主多從
image.png -
模式四:多主多從
image.png -
模式五:集群
image.png
- Redis緩存預(yù)熱
- 緩存預(yù)熱的思路
(1) 提前給redis中嵌入部分?jǐn)?shù)據(jù),再提供服務(wù),肯定不可能將所有數(shù)據(jù)都寫入redis,因?yàn)閿?shù)據(jù)量太大了,第一耗費(fèi)的時(shí)間太長了,第二redis根本就容納不下所有的數(shù)據(jù)
(2) 需要更具當(dāng)天的具體訪問情況,試試統(tǒng)計(jì)出頻率較高的熱數(shù)據(jù)
(3) 然后將訪問頻率較高的熱數(shù)據(jù)寫入到redis,肯定是熱數(shù)據(jù)也比較多,我們也得多個(gè)服務(wù)并行的讀取數(shù)據(jù)去寫,并行的分布式的緩存預(yù)熱
(4) 然后將嵌入的熱數(shù)據(jù)的redis對外提供服務(wù),這樣就不至于冷啟動(dòng),直接讓數(shù)據(jù)庫奔潰了 - 具體的實(shí)時(shí)方案:
(1) nginx+lua將訪問量上報(bào)到kafka中要統(tǒng)計(jì)出來當(dāng)前最新的實(shí)時(shí)的熱數(shù)據(jù)是哪些,我們就得將商品詳情頁訪問的請求對應(yīng)的流量,日志,實(shí)時(shí)上報(bào)到kafka中,
(2) storm從kafka中消費(fèi)數(shù)據(jù),實(shí)時(shí)統(tǒng)計(jì)出每個(gè)商品的訪問次數(shù),訪問次數(shù)基于LRU內(nèi)存數(shù)據(jù)結(jié)構(gòu)的存儲方案。
a) 優(yōu)先用內(nèi)存中的一個(gè)LRUMap去存放,性能高,而且沒有外部依賴。否則的話,依賴redis,我們就是要防止reids掛掉數(shù)據(jù)丟失的情況,就不合適了;用mysql,扛不住高并發(fā)讀寫;用hbase,hadoop生態(tài)系統(tǒng),維護(hù)麻煩,太重了,其實(shí)我們只要統(tǒng)計(jì)出一段時(shí)間訪問最頻繁的商品,然后對它們進(jìn)行訪問計(jì)數(shù),同時(shí)維護(hù)出一個(gè)前N個(gè)訪問最多的商品list即可。計(jì)算好每個(gè)task大致要存放的商品訪問次數(shù)的數(shù)量,計(jì)算出大小,然后構(gòu)建一個(gè)LURMap,apache commons collections有開源的實(shí)現(xiàn),設(shè)定好map的最大大小,就會自動(dòng)根據(jù)LRU算法去剔除多余的數(shù)據(jù),保證內(nèi)存使用限制,即使有部分?jǐn)?shù)據(jù)被干掉了,然后下次來重新開始技術(shù),也沒什么關(guān)系,因?yàn)槿绻籐RU算法干掉,那么它就不是熱數(shù)據(jù),說明最近一段時(shí)間很少訪問,
(3) 每個(gè)storm task啟動(dòng)的時(shí)候,基于zk分布式鎖,將自己的id寫入zk的一個(gè)節(jié)點(diǎn)中
(4) 每個(gè)storm task負(fù)責(zé)完成自己這里的熱數(shù)據(jù)的統(tǒng)計(jì),比如每次計(jì)數(shù)過后,維護(hù)一個(gè)錢1000個(gè)商品的list,每次計(jì)算完都更新這個(gè)list
(5) 寫一個(gè)后臺線程,每個(gè)一段時(shí)間,比如一分鐘,將排名錢1000的熱數(shù)據(jù)list,同步到zk中