面試總結(分佈式系統相關)

RabbitMQ的消費模式就是兼具Push和Pull。

一、消息隊列技術選型

爲什麼使用消息隊列啊?
解耦、異步、削峯
結合實際項目解釋:機臺參數每個模塊都需要,並且會經常變動,所以,多個模塊之間調用越來越複雜,維護起來越來越麻煩。就可以通過MQ來異步調用解耦;用戶點擊後,系統中有耗時的操作,使用戶等待的時間太長,所以引入MQ異步調用;秒殺的時候,Mysql承受不住大量的併發處理,引入MQ來削峯。

削峯:
在這裏插入圖片描述

消息隊列有什麼優點和缺點啊?
系統的可用性降低:MQ中心化
系統的複雜性提高:消息丟失、重複消費等
一致性問題:調用過程中某個子系統調用失敗引起數據不一致

kafka、activemq、rabbitmq、rocketmq都有什麼區別以及適合哪些場景?

  • ActiveMQ:萬級,延時毫秒級,主從架構保證高可用,消息可靠性低可能丟失消息,MQ領域的功能極其完備
    非常成熟,功能強大,在業內大量的公司以及項目中都有應用偶爾會有較低概率丟失消息而且現在社區以及國內應用都越來越少,官方社區現在對ActiveMQ 5.x維護越來越少,幾個月才發佈一個版本而且確實主要是基於解耦和異步來用的,較少在大規模吞吐的場景中使用
  • RabbitMQ:萬級,延時微妙級(最低),主從架構保證高可用,可靠性一般,基於erlang開發併發性能很強性能很好
    erlang語言開發,性能極其好,延時很低;吞吐量到萬級,MQ功能比較完備而且開源提供的管理界面非常棒,用起來很好用社區相對比較活躍,幾乎每個月都發布幾個版本,在國內一些互聯網公司近幾年用rabbitmq也比較多,但是一些問題也是顯而易見的,RabbitMQ確實吞吐量會低一些,這是因爲他做的實現機制比較重。而且erlang開發,國內有幾個公司有實力做erlang源碼級別的研究和定製?如果說你沒這個實力的話,確實偶爾會有一些問題,你很難去看懂源碼,你公司對這個東西的掌控很弱,基本職能依賴於開源社區的快速維護和修復bug。而且rabbitmq集羣動態擴展會很麻煩,不過這個我覺得還好。其實主要是erlang語言本身帶來的問題。很難讀源碼,很難定製和掌控。
  • RocketMQ:十萬級,延時毫秒級,分佈式架構保證高可用,0丟失,MQ領域內的功能較爲完善
    接口簡單易用,而且畢竟在阿里大規模應用過,有阿里品牌保障日處理消息上百億之多,可以做到大規模吞吐,性能也非常好,分佈式擴展也很方便,社區維護還可以,可靠性和可用性都是ok的,還可以支撐大規模的topic數量,支持複雜MQ業務場景而且一個很大的優勢在於,阿里出品都是java系的,我們可以自己閱讀源碼,定製自己公司的MQ,可以掌控社區活躍度相對較爲一般,不過也還可以,文檔相對來說簡單一些,然後接口這塊不是按照標準JMS規範走的有些系統要遷移需要修改大量代碼還有就是阿里出臺的技術,你得做好這個技術萬一被拋棄,社區黃掉的風險,那如果你們公司有技術實力我覺得用RocketMQ挺好的
  • Kafka:十萬級,延時毫秒級,分佈式架構高可用性最好,0丟失,MQ領域內的功能較爲完善
    kafka的特點其實很明顯,就是僅僅提供較少的核心功能,但是提供超高的吞吐量,ms級的延遲,極高的可用性以及可靠性,而且分佈式可以任意擴展同時kafka最好是支撐較少的topic數量即可,保證其超高吞吐量而且kafka唯一的一點劣勢是有可能消息重複消費,那麼對數據準確性會造成極其輕微的影響,在大數據領域中以及日誌採集中,這點輕微影響可以忽略這個特性天然適合大數據實時計算以及日誌收集

技術選型總結:
一般的業務系統要引入MQ,最早大家都用ActiveMQ,但是現在確實大家用的不多了,沒經過大規模吞吐量場景的驗證,社區也不是很活躍,所以大家還是算了吧,我個人不推薦用這個了;
後來大家開始用RabbitMQ,但是確實erlang語言阻止了大量的java工程師去深入研究和掌控他,對公司而言,幾乎處於不可控的狀態,但是確實是開源的,有比較穩定的支持,活躍度也高;
不過現在確實越來越多的公司,會去用RocketMQ,確實很不錯,但是有社區黃掉的風險,對自己公司技術實力有絕對自信的,我推薦用RocketMQ,否則回去老老實實用RabbitMQ吧,人是活躍開源社區,絕對不會黃
所以中小型公司,技術實力較爲一般,技術挑戰不是特別高,用RabbitMQ是不錯的選擇;大型公司,基礎架構研發實力較強,用RocketMQ是很好的選擇
如果是大數據領域的實時計算、日誌採集等場景,用Kafka是業內標準的,絕對沒問題,社區活躍度很高,絕對不會黃,何況幾乎是全世界這個領域的事實性規範

二、如何保證消息隊列的高可用?

高可用:通過設計減少系統不能提供服務的時間。

1、RabbitMQ的高可用性?(可集羣,但不是分佈式的
三種部署模式:單機模式(本地Demo),普通集羣模式,鏡像集羣模式

  • 普通集羣模式:在多臺機器上啓動多個rabbitmq實例,每個機器啓動一個。但是你創建的queue,只會放在一個rabbtimq實例上,但是每個實例都同步queue的元數據。完了你消費的時候,實際上如果連接到了另外一個實例,那麼那個實例會從queue所在實例上拉取數據過來。
    沒做到所謂的分佈式,就是普通的集羣,有queue的節點掛了就完了,所以這種方式沒有所謂的高可用,但是可以提高吞吐量。 在這裏插入圖片描述

  • 鏡像集羣模式:這纔是高可用模式,跟普通的集羣模式不一樣的是,創建的queue,無論元數據還是queue會同步到每一個節點上的。
    這樣的話,好處在於,你任何一個機器宕機了,沒事兒,別的機器都可以用。壞處在於,第一,這個性能開銷也太大了吧,消息同步所有機器,導致網絡帶寬壓力和消耗很重!第二,這麼玩兒,就沒有擴展性可言了,如果某個queue負載很重,你加機器,新增的機器也包含了這個queue的所有數據,並沒有辦法線性擴展你的queue

那麼怎麼開啓這個鏡像集羣模式呢?
rabbitmq有很好的管理控制檯,就是在後臺新增一個策略,這個策略是鏡像集羣模式的策略,指定的時候可以要求數據同步到所有節點的,也可以要求就同步到指定數量的節點,然後你再次創建queue的時候,應用這個策略,就會自動將數據同步到其他的節點上去了。

2、Kafka的高可用性?(是分佈式的)
broker進程就是Kafka在每臺機器上啓動的自己的一個進程,每臺機器+機器上的broker進程就是集羣中的一個節點。

kafka一個最基本的架構認識:多個broker組成,每個broker是一個節點;你創建一個topic,這個topic可以劃分爲多個partition,每個partition可以存在於不同的broker上,每個partition就放一部分數據。

這就是天然的分佈式消息隊列,就是說一個topic的數據,是分散放在多個機器上的,每個機器就放一部分數據。

實際上rabbitmq之類的,並不是分佈式消息隊列,他就是傳統的消息隊列,只不過提供了一些集羣、HA的機制而已,因爲無論怎麼玩兒,rabbitmq一個queue的數據都是放在一個節點裏的,鏡像集羣下,也是每個節點都放這個queue的完整數據。

kafka 0.8以前,是沒有HA機制的,就是任何一個broker宕機了,那個broker上的partition就廢了,沒法寫也沒法讀,沒有什麼高可用性可言。

kafka 0.8以後,提供了HA機制,就是replica副本機制。每個partition的數據都會同步到其他機器上,形成自己的多個replica副本。然後所有replica會選舉一個leader出來,那麼生產和消費都跟這個leader打交道,然後其他replica就是follower。寫的時候,leader會負責把數據同步到所有follower上去,讀的時候就直接讀leader上數據即可。只能讀寫leader?很簡單,要是你可以隨意讀寫每個follower,那麼就要care數據一致性的問題,系統複雜度太高,很容易出問題。kafka會均勻的將一個partition的所有replica分佈在不同的機器上,這樣纔可以提高容錯性。

三、如何保證消息的冪等性,也就是如何處理重複消息,如何保證消息不被重複消費?

首先rabbitmq、rocketmq、kafka等,都是有可能會出現重複消費的問題,正常。因爲這問題通常不是mq自己保證的,是給你保證的。然後我們挑一個kafka來舉個例子,說說怎麼重複消費吧。

kafka實際上有個offset的概念,就是每個消息寫進去,Kafka都會分配一個offset,代表他的序號,消費者就是根據offset的順序去消費的,然後consumer消費了數據之後,每隔一段時間,會把自己消費過的消息的offset提交一下,代表我已經消費過了,下次我要是重啓啥的,你就讓我繼續從上次消費到的offset來繼續消費吧。

但是凡事總有意外,比如我們之前生產經常遇到的,就是你有時候重啓系統,看你怎麼重啓了,如果碰到點着急的,直接kill進程了,再重啓。這會導致consumer有些消息處理了,但是沒來得及提交offset,尷尬了。重啓之後,少數消息會再次消費一次。

其實重複消費不可怕,可怕的是你沒考慮到重複消費之後,怎麼保證冪等性。
(冪等性,通俗點說,就是一個數據,或者一個請求,給你重複來多次,你得確保對應的數據是不會改變的,不能出錯。)

Kafka或者RabbitMQ如何保證消息不被重複消費的解決思路?
保證消息不被重複消費的關鍵是保證消息隊列的冪等性,這個問題針對業務場景來答分以下幾點:

  • 1.比如,你拿到這個消息做數據庫的insert操作。那就容易了,給這個消息做一個唯一主鍵,那麼就算出現重複消費的情況,就會導致主鍵衝突,避免數據庫出現髒數據。或者直接update一下。
  • 2.再比如,你拿到這個消息做redis的set的操作,那就容易了,不用解決,因爲你無論set幾次結果都是一樣的,set操作本來就算冪等操作。
  • 3.如果上面兩種情況還不行,上大招。準備一個第三方介質,來做消費記錄。你需要讓生產者發送每條數據的時候,裏面加一個全局唯一的id,類似訂單id之類的東西,然後你這裏消費到了之後,先根據這個id去比如redis裏查一下,之前消費過嗎?如果沒有消費過,你就處理,然後這個id寫redis。如果消費過了,那你就別處理了,保證別重複處理相同的消息即可。以redis爲例,給消息分配一個全局id,只要消費過該消息,將<id,message>以K-V形式寫入redis。那消費者開始消費前,先去redis中查詢有沒消費記錄即可。

四、RabbitMQ如何解決丟數據的問題?如何保證消息的可靠性傳輸?

1、生產者消息丟失
生產者的消息沒有投遞到MQ中怎麼辦,在發消息時網絡問題引發消息發送失敗等?從生產者弄丟數據這個角度來看,RabbitMQ提供transaction和confirm模式來確保生產者不丟消息。
第一種解決方案:Transaction
rabbitmq提供的事務功能,就是生產者發送數據之前開啓rabbitmq事務(channel.txSelect),然後發送消息,如果消息沒有成功被rabbitmq接收到,那麼生產者會收到異常報錯,此時就可以回滾事務(channel.txRollback),然後重試發送消息;如果收到了消息,那麼可以提交事務(channel.txCommit)。但是問題是,rabbitmq事務機制一搞,基本上吞吐量會下來,因爲太耗性能。事務縱然可以保證生產者消息到達服務端,然而這是以性能爲代價的。事務會阻塞發送方,直到RabbitMQ迴應後,纔可以繼續發送消息,大量的使用事務機制會嚴重拖垮服務端的性能。

transaction機制就是說,發送消息前,開啓事物(channel.txSelect()),然後發送消息,如果發送過程中出現什麼異常,事物就會回滾(channel.txRollback()),如果發送成功則提交事物(channel.txCommit())。

channel.txSelect() //將信道置爲信道模式,開啓事務
channel.txCommit() //提交事務
channel.txRollback() //回滾事務

使用方式如下:

channel.txSelect(); //開始事務
try {
    channel.basicPublish("txExchange","",null,"m3".getBytes()); //發送一條或多條消息
    //...
    channel.txCommit(); //提交事務
}catch (Exception e){
    e.printStackTrace();
    channel.txRollback(); //回滾事務
}

第二種解決方案:開啓Confirm模式
如果你要確保說寫rabbitmq的消息別丟,可以開啓confirm模式,在生產者那裏設置開啓confirm模式之後,你每次寫的消息都會分配一個唯一的id,然後如果寫入了rabbitmq中,rabbitmq會給你回傳一個ack消息,告訴你說這個消息ok了。如果rabbitmq沒能處理這個消息,會回調你一個nack接口,告訴你這個消息接收失敗,你可以重試。而且你可以結合這個機制自己在內存裏維護每個消息id的狀態,如果超過一定時間還沒接收到這個消息的回調,那麼你可以重發。(一般就是用這個解決生產者端消息丟失問題,而且有個好處是異步不會阻塞,吞吐量高)

注意:一旦開啓來發送方確認機制,信道上發送的消息將被從1編號,每條消息都將擁有一個唯一的編號,之後服務端響應時,使用deliveryTag來告訴發送方,它響應都是哪一條消息。需要注意的是,編號是Channel級別的,這樣做能保證消息編號唯一性的關鍵在於,channel不是多線程共享的,發送方應該使用單一線程在channel發送消息來保證消息編號的唯一性,之後再在該channel中處理服務端的相應。
儘管消息被在channel上自動編號,但這只是RabbitMQ服務端和發送方確定消息唯一性的手段。對於業務而言,如果收到一條服務端的nack響應,告訴發送方eliveryTag=5,發送方如何處理呢?也許它需要重新發送消息,但它只知道deliveryTag=5,5號消息是什麼消息呢?也就是說,仍然需要客戶端維護消息狀態,使用發送方確認機制時,發送方仍然可能需要維護一個消息的集合,記錄已經被髮送的消息,之後收到服務端的ack後,再從集合中刪除消息,或者收到nack時,決定重新發送或是別的處理,總之,發送方維護了消息集合,之後纔有可能根據服務端返回deliveryTag,從集合中獲得具體的消息。
使用方法

// 首先,調用channel.confirmSelect將開啓發送方確認
channel.confirmSelect()

// 此後,信息被設置成confirm模式,發送方開始發送消息
// 發送方有兩種方式來處理服務端的響應:
// (1)調用channel.waitForConfirms()等待服務端響應
// 該調用會一直等待,直到服務端響應,如果發送的消息被服務端ack則返回true,否則返回false。(如果再使用之前沒有設置成confirm模式,則調用waitForConfirms時就會拋出異常)
try {
    channel.confirmSelect();
    channel.basicPublish("cfmExchange","",null,"msg".getBytes());
    if(channel.waitForConfirms()) {
        System.out.println("send success");
    } else {
        System.out.println("send fail");
    }
 } catch (InterruptedException e) {
    e.printStackTrace();
 }
 
// (2)定義監聽回調函數,處理服務端響應
// handleAck和handleNack分別被服務端的ack和nack消息處理;
// deliveryTag爲消息編號
// multiple設置爲true是,一次性處理多條消息,即編號消息deliveryTag的消息
channel.addConfirmListener(new ConfirmListener() {
    public void handleAck(long deliveryTag, boolean multiple) throws IOException {

    }

    public void handleNack(long deliveryTag, boolean multiple) throws IOException {

    }
});

總結:事務機制和cnofirm機制最大的不同在於,事務機制是同步的,你提交一個事務之後會阻塞在那兒,但是confirm機制是異步的,你發送個消息之後就可以發送下一個消息,然後那個消息rabbitmq接收了之後會異步回調你一個接口通知你這個消息接收到了。所以一般在生產者這塊避免數據丟失,都是用confirm機制的。

2、RabbitMq本身消息丟失
就是rabbitmq自己弄丟了數據,這個你必須開啓rabbitmq的持久化,就是消息寫入之後會持久化到磁盤,哪怕是rabbitmq自己掛了,恢復之後會自動讀取之前存儲的數據,一般數據不會丟。除非極其罕見的是,rabbitmq還沒持久化,自己就掛了,可能導致少量數據會丟失的,但是這個概率較小。

設置持久化有兩個步驟:

  • 第一個是創建queue的時候將其設置爲持久化的,這樣就可以保證rabbitmq持久化queue的元數據,但是不會持久化queue裏的數據;(將queue的持久化標識durable設置爲true,則代表是一個持久的隊列
  • 第二個是發送消息的時候將消息的deliveryMode設置爲2,就是將消息設置爲持久化的,此時rabbitmq就會將消息持久化到磁盤上去。必須要同時設置這兩個持久化纔行,rabbitmq哪怕是掛了,再次重啓,也會從磁盤上重啓恢復queue,恢復這個queue裏的數據。(發送消息的時候將deliveryMode=2)

而且持久化可以跟生產者那邊的confirm機制配合起來,只有消息被持久化到磁盤之後,纔會通知生產者ack了,所以哪怕是在持久化到磁盤之前,rabbitmq掛了,數據丟了,生產者收不到ack,你也是可以自己重發的。

關於持久化多說一點

  • RabbitMQ的消息什麼時候需要持久化?
    一般有兩種情況下會將消息寫入磁盤,一種情況是消息本身在publish的時候要求持久化;另一種情況是當內存緊張時,需要將部分內存中的消息數據轉移到磁盤中。
        channel.queueDeclare(queue_name, durable, false, false, null); //聲明消息隊列,且爲可持久化的
         String message="Hello world"+Math.random();
         //將隊列設置爲持久化之後,還需要將消息也設爲可持久化的,MessageProperties.PERSISTENT_TEXT_PLAIN
         channel.basicPublish("", queue_name, MessageProperties.PERSISTENT_TEXT_PLAIN,message.getBytes());
  • 消息什麼時候會刷到磁盤?
    a. 寫入文件前會有一個Buffer,大小爲1M(1048576),數據在寫入文件時,首先會寫入到這個Buffer,如果Buffer已滿,則會將Buffer寫入到文件(未必刷到磁盤);
    b. 有個固定的刷盤時間:25ms,也就是不管Buffer滿不滿,每隔25ms,Buffer裏的數據及未刷新到磁盤的文件內容必定會刷到磁盤;
    c. 每次消息寫入後,如果沒有後續寫入請求,則會直接將已寫入的消息刷到磁盤:使用Erlang的receive x after 0來實現,只要進程的信箱裏沒有消息,則產生一個timeout消息,而timeout會觸發刷盤操作。
  • 消息在磁盤文件中的格式
    消息保存於$MNESIA/msg_store_persistent/x.rdq文件中,其中x爲數字編號,從1開始,每個文件最大爲16M(16777216),超過這個大小會生成新的文件,文件編號加1。消息以以下格式存在於文件中<<Size:64, MsgId:16/binary, MsgBody>>,MsgId爲RabbitMQ通過rabbit_guid:gen()每一個消息生成的GUID,MsgBody會包含消息對應的exchange,routing_keys,消息的內容,消息對應的協議版本,消息內容格式(二進制還是其它)等等。
  • 文件何時刪除?
    當所有文件中的垃圾消息(已經被刪除的消息)比例大於閾值(GARBAGE_FRACTION = 0.5)時,會觸發文件合併操作(至少有三個文件存在的情況下),以提高磁盤利用率。
    publish消息時寫入內容,ack消息時刪除內容(更新該文件的有用數據大小),當一個文件的有用數據等於0時,刪除該文件。

持久化代碼:

// 生產者
import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.MessageProperties;
public class ClientSend1 {
    public static final String queue_name="my_queue";
    public static final boolean durable=true; //消息隊列持久化
    public static void main(String[] args)
    throws java.io.IOException{
        ConnectionFactory factory=new ConnectionFactory(); //創建連接工廠
        factory.setHost("localhost");
        factory.setVirtualHost("my_mq");
        factory.setUsername("zhxia");
        factory.setPassword("123456");
        Connection connection=factory.newConnection(); //創建連接
        Channel channel=connection.createChannel();//創建信道
        channel.queueDeclare(queue_name, durable, false, false, null); //聲明消息隊列,且爲可持久化的
        String message="Hello world"+Math.random();
        //將隊列設置爲持久化之後,還需要將消息也設爲可持久化的,MessageProperties.PERSISTENT_TEXT_PLAIN
        channel.basicPublish("", queue_name, MessageProperties.PERSISTENT_TEXT_PLAIN,message.getBytes());
        System.out.println("Send message:"+message);
        channel.close();
        connection.close();
    }

}
// 消費者
import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.QueueingConsumer;
public class ClientReceive1 {
    public static final String queue_name="my_queue";
    public static final boolean autoAck=false;
    public static final boolean durable=true;
    public static void main(String[] args)
    throws java.io.IOException,java.lang.InterruptedException{
        ConnectionFactory factory=new ConnectionFactory();
        factory.setHost("localhost");
        factory.setVirtualHost("my_mq");
        factory.setUsername("zhxia");
        factory.setPassword("123456");
        Connection connection=factory.newConnection();
        Channel channel=connection.createChannel();
        channel.queueDeclare(queue_name, durable, false, false, null);
        System.out.println("Wait for message");
        channel.basicQos(1); //消息分發處理
        QueueingConsumer consumer=new QueueingConsumer(channel);
        channel.basicConsume(queue_name, autoAck, consumer);
        while(true){
            Thread.sleep(500);
            QueueingConsumer.Delivery deliver=consumer.nextDelivery();
            String message=new String(deliver.getBody());
            System.out.println("Message received:"+message);
            channel.basicAck(deliver.getEnvelope().getDeliveryTag(), false);
        }
    }
}

3、消費端消息丟失
如果你打開了autoAck機制,消費端消費數據後立即通知RabbitMQ,已經OK了,而真實時還沒來得及處理,結果進程掛了,比如重啓了,那麼就尷尬了,rabbitmq認爲你都消費了,這數據就丟了。

這個時候得用rabbitmq提供的ack機制,簡單來說,就是你關閉rabbitmq自動ack,可以通過一個api來調用就行,然後每次你自己代碼裏確保處理完的時候,在程序裏ack一把。這樣的話,如果你還沒處理完,不就沒有ack?那rabbitmq就認爲你還沒處理完,這個時候rabbitmq會把這個消費分配給別的consumer去處理,消息是不會丟的。

五、kafka如何解決丟數據的問題?

1)消費端弄丟了數據
唯一可能導致消費者弄丟數據的情況,就是說,消費到了這個消息,然後消費者那邊自動提交了offset,讓kafka以爲你已經消費好了這個消息,其實你剛準備處理這個消息,你還沒處理,你自己就掛了,此時這條消息就丟咯。

這不是一樣麼,大家都知道kafka會自動提交offset,那麼只要關閉自動提交offset,在處理完之後自己手動提交offset,就可以保證數據不會丟。但是此時確實還是會重複消費,比如你剛處理完,還沒提交offset,結果自己掛了,此時肯定會重複消費一次,自己保證冪等性就好了。

生產環境碰到的一個問題,就是說我們的kafka消費者消費到了數據之後是寫到一個內存的queue裏先緩衝一下,結果有的時候,你剛把消息寫入內存queue,然後消費者會自動提交offset。

然後此時我們重啓了系統,就會導致內存queue裏還沒來得及處理的數據就丟失了

2)kafka弄丟了數據
這塊比較常見的一個場景,就是kafka某個broker宕機,然後重新選舉partiton的leader時。大家想想,要是此時其他的follower剛好還有些數據沒有同步,結果此時leader掛了,然後選舉某個follower成leader之後,他不就少了一些數據?這就丟了一些數據啊。

生產環境也遇到過,我們也是,之前kafka的leader機器宕機了,將follower切換爲leader之後,就會發現說這個數據就丟了

所以此時一般是要求起碼設置如下4個參數:

  • 給這個topic設置replication.factor參數:這個值必須大於1,要求每個partition必須有至少2個副本
  • 在kafka服務端設置min.insync.replicas參數:這個值必須大於1,這個是要求一個leader至少感知到有至少一個follower還跟自己保持聯繫,沒掉隊,這樣才能確保leader掛了還有一個follower吧
  • 在producer端設置acks=all:這個是要求每條數據,必須是寫入所有replica之後,才能認爲是寫成功了
  • 在producer端設置retries=MAX(很大很大很大的一個值,無限次重試的意思):這個是要求一旦寫入失敗,就無限重試,卡在這裏了

我們生產環境就是按照上述要求配置的,這樣配置之後,至少在kafka broker端就可以保證在leader所在broker發生故障,進行leader切換時,數據不會丟失

3)生產者會不會弄丟數據
如果按照上述的思路設置了ack=all,一定不會丟,要求是,你的leader接收到消息,所有的follower都同步到了消息之後,才認爲本次寫成功了。如果沒滿足這個條件,生產者會自動不斷的重試,重試無限次。

六、如何保證消息的順序性?

在這裏插入圖片描述
如果像上圖中,本來有執行順序的數據被多個Consumer拿到,每個Consumer的先後執行順序是不確定的,就會導致出錯。

(1)rabbitmq:拆分多個queue,每個queue一個consumer,就是多一些queue而已,確實是麻煩點;或者就一個queue但是對應一個consumer,然後這個consumer內部用內存隊列做排隊,然後分發給底層不同的線程(或者worker)來處理
(2)kafka:一個topic,一個partition(寫入到partition中的消息時有順序的),一個consumer,內部單線程消費,寫N個內存queue,然後N個線程分別消費一個內存queue即可。

七、如何解決消息隊列的延時以及過期失效問題?消息隊列滿了以後該怎麼處理?有幾百萬消息持續積壓幾小時,說說怎麼解決?

如果你積壓了幾百萬到上千萬的數據,即使消費者恢復了,也需要大概1小時的時間才能恢復過來。
(1)大量消息在mq裏積壓了幾個小時了還沒解決

  • 1)先修復consumer的問題,確保其恢復消費速度,將消息取出後重新寫到新建的topic的partition中,然後將現有cnosumer都停掉
  • 2)新建一個topic,partition是原來的10倍,臨時建立好原先10倍或者20倍的queue數量
  • 3)然後寫一個臨時的分發數據的consumer程序,這個程序部署上去消費積壓的數據,消費之後不做耗時的處理,直接均勻輪詢寫入臨時建立好的10倍數量的queue
  • 4)接着臨時徵用10倍的機器來部署consumer,每一批consumer消費一個臨時queue的數據
  • 5)這種做法相當於是臨時將queue資源和consumer資源擴大10倍,以正常的10倍速度來消費數據
  • 6)等快速消費完積壓數據之後,得恢復原先部署架構,重新用原先的consumer機器來消費消息

(2)這裏我們假設再來第二個坑
假設你用的是rabbitmq,rabbitmq是可以設置過期時間的,就是TTL,如果消息在queue中積壓超過一定的時間就會被rabbitmq給清理掉,這個數據就沒了。那這就是第二個坑了。這就不是說數據會大量積壓在mq裏,而是大量的數據會直接搞丟。

這個情況下,就不是說要增加consumer消費積壓的消息,因爲實際上沒啥積壓,而是丟了大量的消息。我們可以採取一個方案,就是批量重導,這個我們之前線上也有類似的場景幹過。就是大量積壓的時候,我們當時就直接丟棄數據了,然後等過了高峯期以後,比如大家一起喝咖啡熬夜到晚上12點以後,用戶都睡覺了。

這個時候我們就開始寫程序,將丟失的那批數據,寫個臨時程序,一點一點的查出來,然後重新灌入mq裏面去,把白天丟的數據給他補回來。也只能是這樣了。

假設1萬個訂單積壓在mq裏面,沒有處理,其中1000個訂單都丟了,你只能手動寫程序把那1000個訂單給查出來,手動發到mq裏去再補一次

(3)然後我們再來假設第三個坑
如果走的方式是消息積壓在mq裏,那麼如果你很長時間都沒處理掉,此時導致mq都快寫滿了,咋辦?這個還有別的辦法嗎?沒有,誰讓你第一個方案執行的太慢了,你臨時寫程序,接入數據來消費,消費一個丟棄一個,都不要了,快速消費掉所有的消息。然後走第二個方案,到了晚上再補數據吧。

八、如果讓你寫一個消息隊列,該如何進行架構設計啊?說一下你的思路

比如說這個消息隊列系統,我們來從以下幾個角度來考慮一下

(1)首先這個mq得支持可伸縮性吧,就是需要的時候快速擴容,就可以增加吞吐量和容量,那怎麼搞?設計個分佈式的系統唄,參照一下kafka的設計理念,broker -> topic -> partition,每個partition放一個機器,就存一部分數據。如果現在資源不夠了,簡單啊,給topic增加partition,然後做數據遷移,增加機器,不就可以存放更多數據,提供更高的吞吐量了?

(2)其次你得考慮一下這個mq的數據要不要落地磁盤吧?那肯定要了,落磁盤,才能保證別進程掛了數據就丟了。那落磁盤的時候怎麼落啊?順序寫,這樣就沒有磁盤隨機讀寫的尋址開銷,磁盤順序讀寫的性能是很高的,這就是kafka的思路。

(3)其次你考慮一下你的mq的可用性啊?這個事兒,具體參考我們之前可用性那個環節講解的kafka的高可用保障機制。多副本 -> leader & follower -> broker掛了重新選舉leader即可對外服務。

(4)能不能支持數據0丟失啊?可以的,參考我們之前說的那個kafka數據零丟失方案

其實一個mq肯定是很複雜的,面試官問你這個問題,其實是個開放題,他就是看看你有沒有從架構角度整體構思和設計的思維以及能力。確實這個問題可以刷掉一大批人,因爲大部分人平時不思考這些東西。

九、分佈式搜索引擎

lucene底層的原理是倒排索引。核心思想就是在多臺機器上啓動多個es進程實例,組成了一個es集羣。

1、es的分佈式架構原理,如何實現分佈式?
es中存儲數據的基本單位是索引。index -> type -> mapping -> document -> field
type:沒法跟mysql裏去對比,一個index裏可以有多個type,每個type的字段都是差不多的,但是有一些略微的差別。

好比說,有一個index,是訂單index,裏面專門是放訂單數據的。就好比說你在mysql中建表,有些訂單是實物商品的訂單,就好比說一件衣服,一雙鞋子;有些訂單是虛擬商品的訂單,就好比說遊戲點卡,話費充值。就兩種訂單大部分字段是一樣的,但是少部分字段可能有略微的一些差別。

所以就會在訂單index裏,建兩個type,一個是實物商品訂單type,一個是虛擬商品訂單type,這兩個type大部分字段是一樣的,少部分字段是不一樣的。

很多情況下,一個index裏可能就一個type,但是確實如果說是一個index裏有多個type的情況,你可以認爲index是一個類別的表,具體的每個type代表了具體的一個mysql中的表

每個type有一個mapping,如果你認爲一個type是一個具體的一個表,index代表了多個type的同屬於的一個類型,mapping就是這個type的表結構定義,你在mysql中創建一個表,肯定是要定義表結構的,裏面有哪些字段,每個字段是什麼類型。。。

mapping就代表了這個type的表結構的定義,定義了這個type中每個字段名稱,字段是什麼類型的,然後還有這個字段的各種配置

實際上你往index裏的一個type裏面寫的一條數據,叫做一條document,一條document就代表了mysql中某個表裏的一行給,每個document有多個field,每個field就代表了這個document中的一個字段的值

接着你搞一個索引,這個索引可以拆分成多個shard,每個shard存儲部分數據。

接着就是這個shard的數據實際是有多個備份,就是說每個shard都有一個primary shard,負責寫入數據,但是還有幾個replica shard。primary shard寫入數據之後,會將數據同步到其他幾個replica shard上去。

通過這個replica的方案,每個shard的數據都有多個備份,如果某個機器宕機了,沒關係啊,還有別的數據副本在別的機器上呢。高可用了吧。

es集羣多個節點,會自動選舉一個節點爲master節點,這個master節點其實就是幹一些管理的工作的,比如維護索引元數據拉,負責切換primary shard和replica shard身份拉,之類的。

要是master節點宕機了,那麼會重新選舉一個節點爲master節點。

如果是非master節點宕機了,那麼會由master節點,讓那個宕機節點上的primary shard的身份轉移到其他機器上的replica shard。急着你要是修復了那個宕機機器,重啓了之後,master節點會控制將缺失的replica shard分配過去,同步後續修改的數據之類的,讓集羣恢復正常。

其實上述就是elasticsearch作爲一個分佈式搜索引擎最基本的一個架構設計
在這裏插入圖片描述

2、es寫入數據的工作原理是什麼?
(1)es寫數據過程

  • 1)客戶端選擇一個node發送請求過去,這個node就是coordinating node(協調節點)
  • 2)coordinating node,對document進行路由,將請求轉發給對應的node(有primary shard)
  • 3)實際的node上的primary shard處理請求,然後將數據同步到replica node
  • 4)coordinating node,如果發現primary node和所有replica node都搞定之後,就返回響應結果給客戶端

寫數據底層原理
1)先寫入buffer,在buffer裏的時候數據是搜索不到的;同時將數據寫入translog日誌文件
2)如果buffer快滿了,或者到一定時間,就會將buffer數據refresh到一個新的segment file中,但是此時數據不是直接進入segment file的磁盤文件的,而是先進入os cache的。這個過程就是refresh。
每隔1秒鐘,es將buffer中的數據寫入一個新的segment file,每秒鐘會產生一個新的磁盤文件,segment file,這個segment file中就存儲最近1秒內buffer中寫入的數據
3)只要數據進入os cache,此時就可以讓這個segment file的數據對外提供搜索了
4)重複1~3步驟,新的數據不斷進入buffer和translog,不斷將buffer數據寫入一個又一個新的segment file中去,每次refresh完buffer清空,translog保留。隨着這個過程推進,translog會變得越來越大。當translog達到一定長度的時候,就會觸發commit操作。
5)commit操作發生第一步,就是將buffer中現有數據refresh到os cache中去,清空buffer
6)將一個commit point寫入磁盤文件,裏面標識着這個commit point對應的所有segment file
7)強行將os cache中目前所有的數據都fsync到磁盤文件中去
8)將現有的translog清空,然後再次重啓啓用一個translog,此時commit操作完成。默認每隔30分鐘會自動執行一次commit,但是如果translog過大,也會觸發commit。整個commit的過程,叫做flush操作。我們可以手動執行flush操作,就是將所有os cache數據刷到磁盤文件中去。
9)translog其實也是先寫入os cache的,默認每隔5秒刷一次到磁盤中去,所以默認情況下,可能有5秒的數據會僅僅停留在buffer或者translog文件的os cache中,如果此時機器掛了,會丟失5秒鐘的數據。但是這樣性能比較好,最多丟5秒的數據。也可以將translog設置成每次寫操作必須是直接fsync到磁盤,但是性能會差很多。
10)如果是刪除操作,commit的時候會生成一個.del文件,裏面將某個doc標識爲deleted狀態,那麼搜索的時候根據.del文件就知道這個doc被刪除了
11)如果是更新操作,就是將原來的doc標識爲deleted狀態,然後新寫入一條數據
12)buffer每次refresh一次,就會產生一個segment file,所以默認情況下是1秒鐘一個segment file,segment file會越來越多,此時會定期執行merge
13)每次merge的時候,會將多個segment file合併成一個,同時這裏會將標識爲deleted的doc給物理刪除掉,然後將新的segment file寫入磁盤,這裏會寫一個commit point,標識所有新的segment file,然後打開segment file供搜索使用,同時刪除舊的segment file。

(2)es讀數據過程
查詢,GET某一條數據,寫入了某個document,這個document會自動給你分配一個全局唯一的id,doc id,同時也是根據doc id進行hash路由到對應的primary shard上面去。也可以手動指定doc id,比如用訂單id,用戶id。
你可以通過doc id來查詢,會根據doc id進行hash,判斷出來當時把doc id分配到了哪個shard上面去,從那個shard去查詢。

  • 1)客戶端發送請求到任意一個node,成爲coordinate node
  • 2)coordinate node對document進行路由,將請求轉發到對應的node,此時會使用round-robin隨機輪詢算法,在primary shard以及其所有replica中隨機選擇一個,讓讀請求負載均衡
  • 3)接收請求的node返回document給coordinate node
  • 4)coordinate node返回document給客戶端

(3)es搜索數據過程
1)客戶端發送請求到一個coordinate node
2)協調節點將搜索請求轉發到所有的shard對應的primary shard或replica shard也可以
3)query phase:每個shard將自己的搜索結果(其實就是一些doc id),返回給協調節點,由協調節點進行數據的合併、排序、分頁等操作,產出最終結果
4)fetch phase:接着由協調節點,根據doc id去各個節點上拉取實際的document數據,最終返回給客戶端

3、es在數據量很大的情況下(數十億級別)如何提高查詢性能啊?
(1)性能優化的殺手鐗——filesystem cache
在這裏插入圖片描述
os cache,操作系統的緩存。你往es裏寫的數據,實際上都寫到磁盤文件裏去了,磁盤文件裏的數據操作系統會自動將裏面的數據緩存到os cache裏面去。

歸根結底,你要讓es性能要好,最佳的情況下,就是你的機器的內存,至少可以容納你的總數據量的一半。

比如說你現在有一行數據:id name age …30個字段

但是你現在搜索,只需要根據id name age三個字段來搜索

如果你往es裏寫入一行數據所有的字段,就會導致說70%的數據是不用來搜索的,結果硬是佔據了es機器上的filesystem cache的空間,單條數據的數據量越大,就會導致filesystem cahce能緩存的數據就越少。

實際上,我們僅僅只是寫入es中要用來檢索的少數幾個字段就可以了,比如說,就寫入es id name age三個字段就可以了,然後你可以把其他的字段數據存在mysql裏面,我們一般是建議用es + hbase的這麼一個架構。(hbase的特點是適用於海量數據的在線存儲,就是對hbase可以寫入海量數據,不要做複雜的搜索,就是做很簡單的一些根據id或者範圍進行查詢的這麼一個操作就可以了),從es中根據name和age去搜索,拿到的結果可能就20個doc id,然後根據doc id到hbase裏去查詢每個doc id對應的完整的數據,給查出來,再返回給前端。

然後你從es檢索可能就花費20ms,然後再根據es返回的id去hbase裏查詢,查20條數據,可能也就耗費個30ms,可能你原來那麼玩兒,1T數據都放es,會每次查詢都是5~10秒,現在可能性能就會很高,每次查詢就是50ms。

你最好是寫入es的數據小於等於,或者是略微大於es的filesystem cache的內存容量。

(2)數據預熱
對於那些你覺得比較熱的,經常會有人訪問的數據,最好做一個專門的緩存預熱子系統,就是對熱數據,每隔一段時間,你就提前訪問一下,讓數據進入filesystem cache裏面去。這樣期待下次別人訪問的時候,一定性能會好一些。
(3)冷熱分離
es可以做類似於mysql的水平拆分,就是說將大量的訪問很少,頻率很低的數據,單獨寫一個索引,然後將訪問很頻繁的熱數據單獨寫一個索引.
你最好是將冷數據寫入一個索引中,然後熱數據寫入另外一個索引中,這樣可以確保熱數據在被預熱之後,儘量都讓他們留在filesystem os cache裏,別讓冷數據給沖刷掉。
對於冷數據而言,是在別的index裏的,跟熱數據index都不再相同的機器上,大家互相之間都沒什麼聯繫了。如果有人訪問冷數據,可能大量數據是在磁盤上的,此時性能差點,就10%的人去訪問冷數據;90%的人在訪問熱數據。

(4)document模型設計
es裏面的複雜的關聯查詢,複雜的查詢語法,儘量別用,一旦用了性能一般都不太好,對於這些需求就要儘量改善document模型設計。

寫入es的java系統裏,就完成關聯,將關聯好的數據直接寫入es中,搜索的時候,就不需要利用es的搜索語法去完成join來搜索了

document模型設計是非常重要的,很多操作,不要在搜索的時候纔想去執行各種複雜的亂七八糟的操作。es能支持的操作就是那麼多,不要考慮用es做一些它不好操作的事情。如果真的有那種操作,儘量在document模型設計的時候,寫入的時候就完成。另外對於一些太複雜的操作,比如join,nested,parent-child搜索都要儘量避免,性能都很差的。

很多複雜的亂七八糟的一些操作,如何執行?
兩個思路,在搜索/查詢的時候,要執行一些業務強相關的特別複雜的操作:

  • 1)在寫入數據的時候,就設計好模型,加幾個字段,把處理好的數據寫入加的字段裏面
  • 2)自己用java程序封裝,es能做的,用es來做,搜索出來的數據,在java程序裏面去做,比如說我們,基於es,用java封裝一些特別複雜的操作

(5)分頁性能優化
es在做分頁查詢時,是要在每一個shard上都先查出這些頁的數據來,然後在合併到一個協調節點上,再排序,最後找出目標頁的數據。也就是說,你要查的頁越大,從每個shard上查詢的數據量就越大,所要查詢的總數據就越大,時間就越長。
所以,兩個解決方案:

  • 1)不允許深度分頁(默認深度分頁性能很慘)
  • 2)類似於app裏的推薦商品不斷下拉出來一頁一頁的
    scroll會一次性給你生成所有數據的一個快照,然後每次翻頁就是通過遊標移動,獲取下一頁下一頁這樣子,性能會比上面說的那種分頁性能也高很多很多。針對這個問題,你可以考慮用scroll來進行處理,scroll的原理實際上是保留一個數據快照,然後在一定時間內,你如果不斷的滑動往後翻頁的時候,類似於你現在在瀏覽微博,不斷往下刷新翻頁。那麼就用scroll不斷通過遊標獲取下一頁數據,這個性能是很高的,比es實際翻頁要好的多的多。但是唯一的一點就是,這個適合於那種類似微博下拉翻頁的,不能隨意跳到任何一頁的場景。同時這個scroll是要保留一段時間內的數據快照的,你需要確保用戶不會持續不斷翻頁翻幾個小時。
    無論翻多少頁,性能基本上都是毫秒級的。因爲scroll api是隻能一頁一頁往後翻的,是不能說,先進入第10頁,然後去120頁,回到58頁,不能隨意亂跳頁。所以現在很多產品,都是不允許你隨意翻頁的,app,也有一些網站,做的就是你只能往下拉,一頁一頁的翻

4、es生產集羣的部署架構是什麼?每個索引的數據量大概有多少?每個索引大概有多少個分片?
es生產集羣的部署架構是什麼?每個索引的數據量大概有多少?每個索引大概有多少個分片?
幾個基本的版本如下:

  • (1)es生產集羣我們部署了5臺機器,每臺機器是6核64G的,集羣總內存是320G
  • (2)我們es集羣的日增量數據大概是2000萬條,每天日增量數據大概是500MB,每月增量數據大概是6億,15G。目前系統已經運行了幾個月,現在es集羣裏數據總量大概是100G左右。
  • (3)目前線上有5個索引(這個結合你們自己業務來,看看自己有哪些數據可以放es的),每個索引的數據量大概是20G,所以這個數據量之內,我們每個索引分配的是8個shard,比默認的5個shard多了3個shard。

深度面試題
kafka複製的底層原理,leader選舉的算法,增加partition以後的rebalance算法
es底層的相關度評分算法(TF/IDF算法)、deep paging、上千萬數據批處理、跨機房多集羣同步、搜索效果優化

十、分佈式緩存

用緩存,主要是倆用途,高性能和高併發
用緩存不良後果:
1)緩存與數據庫雙寫不一致
2)緩存雪崩
3)緩存穿透
4)緩存併發競爭

1、redis和memcached有什麼區別?redis的線程模型是什麼?爲什麼單線程的redis比多線程的memcached效率要高得多(爲什麼redis是單線程的但是還可以支撐高併發)?

(1)redis和memcached有啥區別:Redis支持的數據類型更多;Redis支持集羣,memcached不支持集羣。

(2)Redis的線程模型
1)文件事件處理器
redis基於reactor模式開發了網絡事件處理器,這個處理器叫做文件事件處理器,file event handler。這個文件事件處理器,是單線程的,redis才叫做單線程的模型,採用IO多路複用機制同時監聽多個socket,根據socket上的事件來選擇對應的事件處理器來處理這個事件。

如果被監聽的socket準備好執行accept、read、write、close等操作的時候,跟操作對應的文件事件就會產生,這個時候文件事件處理器就會調用之前關聯好的事件處理器來處理這個事件。

文件事件處理器是單線程模式運行的,但是通過IO多路複用機制監聽多個socket,可以實現高性能的網絡通信模型,又可以跟內部其他單線程的模塊進行對接,保證了redis內部的線程模型的簡單性。

文件事件處理器的結構包含4個部分:多個socket,IO多路複用程序,文件事件分派器,事件處理器(命令請求處理器、命令回覆處理器、連接應答處理器,等等)。

多個socket可能併發的產生不同的操作,每個操作對應不同的文件事件,但是IO多路複用程序會監聽多個socket,但是會將socket放入一個隊列中排隊,每次從隊列中取出一個socket給事件分派器,事件分派器把socket給對應的事件處理器。

然後一個socket的事件處理完之後,IO多路複用程序纔會將隊列中的下一個socket給事件分派器。文件事件分派器會根據每個socket當前產生的事件,來選擇對應的事件處理器來處理。

2)文件事件

當socket變得可讀時(比如客戶端對redis執行write操作,或者close操作),或者有新的可以應答的sccket出現時(客戶端對redis執行connect操作),socket就會產生一個AE_READABLE事件。

當socket變得可寫的時候(客戶端對redis執行read操作),socket會產生一個AE_WRITABLE事件。

IO多路複用程序可以同時監聽AE_REABLE和AE_WRITABLE兩種事件,要是一個socket同時產生了AE_READABLE和AE_WRITABLE兩種事件,那麼文件事件分派器優先處理AE_REABLE事件,然後纔是AE_WRITABLE事件。

3)文件事件處理器

如果是客戶端要連接redis,那麼會爲socket關聯連接應答處理器
如果是客戶端要寫數據到redis,那麼會爲socket關聯命令請求處理器
如果是客戶端要從redis讀數據,那麼會爲socket關聯命令回覆處理器

4)客戶端與redis通信的一次流程

在redis啓動初始化的時候,redis會將連接應答處理器跟AE_READABLE事件關聯起來,接着如果一個客戶端跟redis發起連接,此時會產生一個AE_READABLE事件,然後由連接應答處理器來處理跟客戶端建立連接,創建客戶端對應的socket,同時將這個socket的AE_READABLE事件跟命令請求處理器關聯起來。

當客戶端向redis發起請求的時候(不管是讀請求還是寫請求,都一樣),首先就會在socket產生一個AE_READABLE事件,然後由對應的命令請求處理器來處理。這個命令請求處理器就會從socket中讀取請求相關數據,然後進行執行和處理。

接着redis這邊準備好了給客戶端的響應數據之後,就會將socket的AE_WRITABLE事件跟命令回覆處理器關聯起來,當客戶端這邊準備好讀取響應數據時,就會在socket上產生一個AE_WRITABLE事件,會由對應的命令回覆處理器來處理,就是將準備好的響應數據寫入socket,供客戶端來讀取。

命令回覆處理器寫完之後,就會刪除這個socket的AE_WRITABLE事件和命令回覆處理器的關聯關係。

在這裏插入圖片描述

(3)爲啥redis單線程模型也能效率這麼高?

1)純內存操作
2)核心是基於非阻塞的IO多路複用機制
3)單線程反而避免了多線程的頻繁上下文切換問題(百度)
在這裏插入圖片描述
補充:多線程上下文切換
CPU時間片即CPU分配給每個線程的執行時間段,稱作它的時間片。CPU時間片一般爲幾十毫秒(ms)。

上下文切換:CPU通過時間片段的算法來循環執行線程任務,而循環執行即每個線程允許運行的時間後的切換,而這種循環的切換使各個程序從表面上看是同時進行的。而切換時會保存之前的線程任務狀態,當切換到該線程任務的時候,會重新加載該線程的任務狀態。而這個從保存到加載的過程稱之爲上下文切換。

  • 若當前線程還在運行而時間片結束後,CPU將被別的線程剝奪並分配給另一個線程。
  • 若線程在時間片結束前阻塞或結束,CPU進行線程切換。而不會造成CPU資源浪費。

2、redis都有哪些數據類型?分別在哪些場景下使用比較合適?

hash類的數據結構,主要是用來存放一些對象,把一些簡單的對象給緩存起來,後續操作的時候,你可以直接僅僅修改這個對象中的某個字段的值

list有序列表,這個是可以玩兒出很多花樣的,微博,某個大v的粉絲,就可以以list的格式放在redis裏去緩存

set無序集合,自動去重

sorted set:排序的set,去重但是可以排序,寫進去的時候給一個分數,自動根據分數排序,這個可以玩兒很多的花樣,最大的特點是有個分數可以自定義排序規則,排行榜

3、redis的過期策略都有哪些?內存淘汰機制都有哪些?手寫一下LRU代碼實現?
(1)設置過期時間
我們set key的時候,都可以給一個expire time,就是過期時間,指定這個key比如說只能存活1個小時?10分鐘?這個很有用,我們自己可以指定緩存到期就失效。
如果假設你設置一個一批key只能存活1個小時,那麼接下來1小時後,redis是怎麼對這批key進行刪除的?

答案是:定期刪除+惰性刪除

所謂定期刪除,指的是redis默認是每隔100ms就隨機抽取一些設置了過期時間的key,檢查其是否過期,如果過期就刪除。假設redis裏放了10萬個key,都設置了過期時間,你每隔幾百毫秒,就檢查10萬個key,那redis基本上就死了,cpu負載會很高的,消耗在你的檢查過期key上了。注意,這裏可不是每隔100ms就遍歷所有的設置過期時間的key,那樣就是一場性能上的災難。實際上redis是每隔100ms隨機抽取一些key來檢查和刪除的。

但是問題是,定期刪除可能會導致很多過期key到了時間並沒有被刪除掉,那咋整呢?所以就是惰性刪除了。這就是說,在你獲取某個key的時候,redis會檢查一下 ,這個key如果設置了過期時間那麼是否過期了?如果過期了此時就會刪除,不會給你返回任何東西。

並不是key到時間就被刪除掉,而是你查詢這個key的時候,redis再懶惰的檢查一下

通過上述兩種手段結合起來,保證過期的key一定會被幹掉。

很簡單,就是說,你的過期key,靠定期刪除沒有被刪除掉,還停留在內存裏,佔用着你的內存呢,除非你的系統去查一下那個key,纔會被redis給刪除掉。

但是實際上這還是有問題的,如果定期刪除漏掉了很多過期key,然後你也沒及時去查,也就沒走惰性刪除,此時會怎麼樣?如果大量過期key堆積在內存裏,導致redis內存塊耗盡了,咋整?

答案是:走內存淘汰機制。

(2)內存淘汰

如果redis的內存佔用過多的時候,此時會進行內存淘汰,有如下一些策略:

redis 10個key,現在已經滿了,redis需要刪除掉5個key

1個key,最近1分鐘被查詢了100次
1個key,最近10分鐘被查詢了50次
1個key,最近1個小時倍查詢了1次

1)noeviction:當內存不足以容納新寫入數據時,新寫入操作會報錯,這個一般沒人用吧,實在是太噁心了
2)allkeys-lru:當內存不足以容納新寫入數據時,在鍵空間中,移除最近最少使用的key(這個是最常用的)
3)allkeys-random:當內存不足以容納新寫入數據時,在鍵空間中,隨機移除某個key,這個一般沒人用吧,爲啥要隨機,肯定是把最近最少使用的key給幹掉啊
4)volatile-lru:當內存不足以容納新寫入數據時,在設置了過期時間的鍵空間中,移除最近最少使用的key(這個一般不太合適)
5)volatile-random:當內存不足以容納新寫入數據時,在設置了過期時間的鍵空間中,隨機移除某個key
6)volatile-ttl:當內存不足以容納新寫入數據時,在設置了過期時間的鍵空間中,有更早過期時間的key優先移除
(3)要不你手寫一個LRU算法?

你可以現場手寫最原始的LRU算法,那個代碼量太大了,我覺得不太現實:

public class LRUCache<K, V> extends LinkedHashMap<K, V> {
    
	private final int CACHE_SIZE;

    // 這裏就是傳遞進來最多能緩存多少數據
    public LRUCache(int cacheSize) {
    	// 調用LinkedHashMap中的構造函數進行初始化,其中有三個參數:初始容量、加載因子和排序模式
        super((int) Math.ceil(cacheSize / 0.75) + 1, 0.75f, true); // 這塊就是設置一個hashmap的初始大小,同時最後一個true指的是讓linkedhashmap按照訪問順序來進行排序,最近訪問的放在頭,最老訪問的就在尾
        // Math.ceil大於或等於一個給定數字的最小整數
        CACHE_SIZE = cacheSize;
    }

    @Override
    protected boolean removeEldestEntry(Map.Entry eldest) {
        return size() > CACHE_SIZE; // 這個意思就是說當map中的數據量大於指定的緩存個數的時候,就自動刪除最老的數據
    }

}

我給你看上面的代碼,是告訴你最起碼你也得寫出來上面那種代碼,不求自己純手工從底層開始打造出自己的LRU,但是起碼知道如何利用已有的jdk數據結構實現一個java版的LRU

具體LinkedHashMap原理參考:https://blog.csdn.net/varyall/article/details/82319461
在這裏插入圖片描述
在這裏插入圖片描述

4、如何通過讀寫分離來承載讀請求QPS超過10萬+?
redis不能支持高併發的瓶頸在單機。單機也就在幾萬。
支持高併發:

  • 讀寫分離
  • 主從架構 -> 讀寫分離 -> 支撐10萬+讀QPS的架構

redis replication -> 主從架構 -> 讀寫分離 -> 水平擴容支撐讀高併發

redis replication的核心機制
(1)redis採用異步方式複製數據到slave節點, 不過redis 2.8開始,slave node會週期性地確認自己每次複製的數據量
(2)一個master node是可以配置多個slave node的
(3)slave node也可以連接其他的slave node
(4)slave node做複製的時候,是不會block master node的正常工作的
(5)slave node在做複製的時候,也不會block對自己的查詢操作,它會用舊的數據集來提供服務; 但是複製完成的時候,需要刪除舊數據集,加載新數據集,這個時候就會暫停對外服務了
(6)slave node主要用來進行橫向擴容,做讀寫分離,擴容的slave node可以提高讀的吞吐量

master持久化對於主從架構的安全保障的意義
如果採用了主從架構,那麼建議必須開啓master node的持久化!
不建議用slave node作爲master node的數據熱備,因爲那樣的話,如果你關掉master的持久化,可能在master宕機重啓的時候數據是空的,然後可能一經過複製,salve node數據也丟了

主從複製原理、斷點續傳、無磁盤化複製、過期key處理
(1)主從複製的核心原理

  • 當啓動一個slave node的時候,它會發送一個PSYNC命令給master node
  • 如果這是slave node重新連接master node,那麼master node僅僅會複製給slave部分缺少的數據; 否則如果是slave node第一次連接master node,那麼會觸發一次full resynchronization
  • 開始full resynchronization的時候,master會啓動一個後臺線程,開始生成一份RDB快照文件,同時還會將從客戶端收到的所有寫命令緩存在內存中。RDB文件生成完畢之後,master會將這個RDB發送給slave,slave會先寫入本地磁盤,然後再從本地磁盤加載到內存中。然後master會將內存中緩存的寫命令發送給slave,slave也會同步這些數據。
  • slave node如果跟master node有網絡故障,斷開了連接,會自動重連。master如果發現有多個slave node都來重新連接,僅僅會啓動一個rdb save操作,用一份數據服務所有slave node。

(2)主從複製的斷點續傳

  • 從redis2.8開始,就支持主從複製的斷點續傳,如果主從複製過程中,網路連接斷掉了,那麼當網絡恢復後就可以接着上次複製的地方,繼續複製下去,而不是從頭開始複製一份。
  • master node會在內存中創建一個backlog,master和slave都會保存一個replica offset還有一個master id , offset就是保存在backlog中的。如果master和slave網絡連接斷掉了,slave會讓master從上次的replica offset開始繼續複製。
  • 如果沒有找到對應的offset,那麼就會執行一次resynchronization。

(3)無磁盤化複製:master在內存中直接創建rdb,然後發送給slave,不會在自己本地落地磁盤了

  • repl-diskless-sync yes (在redis的配置文件中)
  • repl-diskless-sync-delay 5(單位爲秒,等待一定時長再開始複製,因爲要等更多slave重新連接過來)

(4)過期key處理
slave不會過期key,只會等待master過期key。如果master過期了一個key,或者通過LRU淘汰了一個key,那麼會模擬一條del命令發送給slave。

1、複製的完整流程

(1)slave node啓動,僅僅保存master node的信息,包括master node的host和ip,但是複製流程沒開始

master host和ip是從哪兒來的,redis.conf裏面的slaveof配置的

(2)slave node內部有個定時任務,每秒檢查是否有新的master node要連接和複製,如果發現,就跟master node建立socket網絡連接
(3)slave node發送ping命令給master node
(4)口令認證,如果master設置了requirepass,那麼salve node必鬚髮送masterauth的口令過去進行認證
(5)master node第一次執行全量複製,將所有數據發給slave node
(6)master node後續持續將寫命令,異步複製給slave node

2、數據同步相關的核心機制

指的就是第一次slave連接msater的時候,執行的全量複製,那個過程裏面你的一些細節的機制

(1)master和slave都會維護一個offset

master會在自身不斷累加offset,slave也會在自身不斷累加offset
slave每秒都會上報自己的offset給master,同時master也會保存每個slave的offset

這個倒不是說特定就用在全量複製的,主要是master和slave都要知道各自的數據的offset,才能知道互相之間的數據不一致的情況

(2)backlog

master node有一個backlog,默認是1MB大小
master node給slave node複製數據時,也會將數據在backlog中同步寫一份
backlog主要是用來做全量複製中斷時候的增量複製的

(3)master run id

info server,可以看到master run id
如果根據host+ip定位master node,是不靠譜的,如果master node重啓或者數據出現了變化,那麼slave node應該根據不同的run id區分,run id不同就做全量複製
如果需要不更改run id重啓redis,可以使用redis-cli debug reload命令

(4)psync

從節點使用psync從master node進行復制,psync runid offset
master node會根據自身的情況返回響應信息,可能是FULLRESYNC runid offset觸發全量複製,可能是CONTINUE觸發增量複製

3、全量複製

(1)master執行bgsave,在本地生成一份rdb快照文件
(2)master node將rdb快照文件發送給salve node,如果rdb複製時間超過60秒(repl-timeout),那麼slave node就會認爲複製失敗,可以適當調節大這個參數
(3)對於千兆網卡的機器,一般每秒傳輸100MB,6G文件,很可能超過60s
(4)master node在生成rdb時,會將所有新的寫命令緩存在內存中,在salve node保存了rdb之後,再將新的寫命令複製給salve node
(5)client-output-buffer-limit slave 256MB 64MB 60,如果在複製期間,內存緩衝區持續消耗超過64MB,或者一次性超過256MB,那麼停止複製,複製失敗
(6)slave node接收到rdb之後,清空自己的舊數據,然後重新加載rdb到自己的內存中,同時基於舊的數據版本對外提供服務
(7)如果slave node開啓了AOF,那麼會立即執行BGREWRITEAOF,重寫AOF

rdb生成、rdb通過網絡拷貝、slave舊數據的清理、slave aof rewrite,很耗費時間

如果複製的數據量在4G~6G之間,那麼很可能全量複製時間消耗到1分半到2分鐘

4、增量複製

(1)如果全量複製過程中,master-slave網絡連接斷掉,那麼salve重新連接master時,會觸發增量複製
(2)master直接從自己的backlog中獲取部分丟失的數據,發送給slave node,默認backlog就是1MB
(3)msater就是根據slave發送的psync中的offset來從backlog中獲取數據的

5、heartbeat

主從節點互相都會發送heartbeat信息

master默認每隔10秒發送一次heartbeat,salve node每隔1秒發送一個heartbeat

6、異步複製

master每次接收到寫命令之後,先在內部寫入數據,然後異步發送給slave node

在這裏插入圖片描述
在這裏插入圖片描述
幾個概念的澄清:
1、什麼是99.99%高可用?
架構上,高可用性,99.99%的高可用性

2、redis不可用是什麼?單實例不可用?主從架構不可用?不可用的後果是什麼?

3、redis怎麼才能做到高可用?哨兵節點實現了Redis的高可用性。
主備快速切換。
由誰來監控這些Redis的故障檢測?由誰來進行主備切換?——哨兵節點(Sentinal Node)


1、哨兵的介紹

sentinal,中文名是哨兵

哨兵是redis集羣架構中非常重要的一個組件,主要功能如下

(1)集羣監控,負責監控redis master和slave進程是否正常工作
(2)消息通知,如果某個redis實例有故障,那麼哨兵負責發送消息作爲報警通知給管理員
(3)故障轉移,如果master node掛掉了,會自動轉移到slave node上
(4)配置中心,如果故障轉移發生了,通知client客戶端新的master地址

----> 哨兵本身也是分佈式的,作爲一個哨兵集羣去運行,互相協同工作 <-----
如何工作:
(1)故障轉移時,判斷一個master node是宕機了,需要大部分的哨兵都同意纔行,涉及到了分佈式選舉的問題
(2)即使部分哨兵節點掛掉了,哨兵集羣還是能正常工作的,因爲如果一個作爲高可用機制重要組成部分的故障轉移系統本身是單點的,那就很坑爹了

--> 目前採用的是sentinal 2版本,sentinal 2相對於sentinal 1來說,重寫了很多代碼,主要是讓故障轉移的機制和算法變得更加健壯和簡單 <--

2、哨兵的核心知識

(1)哨兵至少需要3個實例,來保證自己的健壯性
(2)哨兵 + redis主從的部署架構,是不會保證數據零丟失的,只能保證redis集羣的高可用性
(3)對於哨兵 + redis主從這種複雜的部署架構,儘量在測試環境和生產環境,都進行充足的測試和演練

3、爲什麼redis哨兵集羣只有2個節點無法正常工作?

哨兵集羣必須部署2個以上節點

如果哨兵集羣僅僅部署了個2個哨兵實例,quorum=1

+----+         +----+
| M1 |---------| R1 |
| S1 |         | S2 |
+----+         +----+

Configuration: quorum = 1

master宕機,s1和s2中只要有1個哨兵認爲master宕機就可以還行切換,同時s1和s2中會選舉出一個哨兵來執行故障轉移

同時這個時候,需要majority,也就是大多數哨兵都是運行的,2個哨兵的majority就是2(2的majority=2,3的majority=2,5的majority=3,4的majority=2),2個哨兵都運行着,就可以允許執行故障轉移

但是如果整個M1和S1運行的機器宕機了,那麼哨兵只有1個了,此時就沒有majority來允許執行故障轉移,雖然另外一臺機器還有一個R1,但是故障轉移不會執行

4、經典的3節點哨兵集羣

       +----+
       | M1 |
       | S1 |
       +----+
          |
+----+    |    +----+
| R2 |----+----| R3 |
| S2 |         | S3 |
+----+         +----+

Configuration: quorum = 2,majority

如果M1所在機器宕機了,那麼三個哨兵還剩下2個,S2和S3可以一致認爲master宕機,然後選舉出一個來執行故障轉移

同時3個哨兵的majority是2,所以還剩下的2個哨兵運行着,就可以允許執行故障轉移

兩種主備切換過程中數據丟失的情況? 解決異步複製和腦裂導致的數據丟失?
主備切換過程中數據丟失情況一異步複製導致的數據丟失。因爲master -> slave的複製是異步的,所以可能有部分數據還沒複製到slave,master就宕機了,此時這些部分數據就丟失了。
主備切換過程中數據丟失情況二腦裂導致的數據丟失。腦裂,也就是說,某個master所在機器突然脫離了正常的網絡,跟其他slave機器不能連接,但是實際上master還運行着。此時哨兵可能就會認爲master宕機了,然後開啓選舉,將其他slave切換成了master。這個時候,集羣裏就會有兩個master,也就是所謂的腦裂。此時雖然某個slave被切換成了master,但是可能client還沒來得及切換到新的master,還繼續寫向舊master的數據可能也丟失了。因此舊master再次恢復的時候,會被作爲一個slave掛到新的master上去,自己的數據會清空,重新從新的master複製數據

這兩個問題的解決方法是通過兩個Redis中配置參數解決如下:

  • min-slaves-to-write 1

  • min-slaves-max-lag 10
    兩個參數的意義:至少有1個slave,數據複製和同步的延遲不能超過10秒。如果說一旦所有的slave,數據複製和同步的延遲都超過了10秒鐘,那麼這個時候,master就不會再接收任何請求了。上面兩個配置可以減少異步複製和腦裂導致的數據丟失。

  • [ 1] 減少異步複製的數據丟失
    有了min-slaves-max-lag這個配置,就可以確保說,一旦slave複製數據和ack延時太長,就認爲可能master宕機後損失的數據太多了,那麼就拒絕寫請求,這樣可以把master宕機時由於部分數據未同步到slave導致的數據丟失降低的可控範圍內
    在這裏插入圖片描述

  • [ 2] 減少腦裂的數據丟失
    如果一個master出現了腦裂,跟其他slave丟了連接,那麼上面兩個配置可以確保說,如果不能繼續給指定數量的slave發送數據,而且slave超過10秒沒有給自己ack消息,那麼就直接拒絕客戶端的寫請求。這樣腦裂後的舊master就不會接受client的新數據,也就避免了數據丟失。上面的配置就確保了,如果跟任何一個slave丟了連接,在10秒後發現沒有slave給自己ack,那麼就拒絕新的寫請求。因此在腦裂場景下,最多就丟失10秒的數據。(Client端就要進行服務降級處理)
    在這裏插入圖片描述

Redis哨兵的多個核心底層原理的深入解析(包含slave選舉算法)

1、sdown和odown轉換機制

sdown和odown兩種失敗狀態

sdown是主觀宕機,就一個哨兵如果自己覺得一個master宕機了,那麼就是主觀宕機

odown是客觀宕機,如果quorum數量的哨兵都覺得一個master宕機了,那麼就是客觀宕機

sdown達成的條件很簡單,如果一個哨兵ping一個master,超過了is-master-down-after-milliseconds指定的毫秒數之後,就主觀認爲master宕機

sdown到odown轉換的條件很簡單,如果一個哨兵在指定時間內,收到了quorum指定數量的其他哨兵也認爲那個master是sdown了,那麼就認爲是odown了,客觀認爲master宕機

2、哨兵集羣的自動發現機制

哨兵互相之間的發現,是通過redis的pub/sub系統實現的,每個哨兵都會往__sentinel__:hello這個channel裏發送一個消息,這時候所有其他哨兵都可以消費到這個消息,並感知到其他的哨兵的存在

每隔兩秒鐘,每個哨兵都會往自己監控的某個master+slaves對應的__sentinel__:hello channel裏發送一個消息,內容是自己的host、ip和runid還有對這個master的監控配置

每個哨兵也會去監聽自己監控的每個master+slaves對應的__sentinel__:hello channel,然後去感知到同樣在監聽這個master+slaves的其他哨兵的存在

每個哨兵還會跟其他哨兵交換對master的監控配置,互相進行監控配置的同步

3、slave配置的自動糾正

哨兵會負責自動糾正slave的一些配置,比如slave如果要成爲潛在的master候選人,哨兵會確保slave在複製現有master的數據; 如果slave連接到了一個錯誤的master上,比如故障轉移之後,那麼哨兵會確保它們連接到正確的master上

4、slave->master選舉算法

如果一個master被認爲odown了,而且majority哨兵都允許了主備切換,那麼某個哨兵就會執行主備切換操作,此時首先要選舉一個slave來

會考慮slave的一些信息

(1)跟master斷開連接的時長
(2)slave優先級
(3)複製offset
(4)run id

如果一個slave跟master斷開連接已經超過了down-after-milliseconds的10倍,外加master宕機的時長,那麼slave就被認爲不適合選舉爲master

(down-after-milliseconds * 10) + milliseconds_since_master_is_in_SDOWN_state

接下來會對slave進行排序

(1)按照slave優先級進行排序,slave priority越低,優先級就越高
(2)如果slave priority相同,那麼看replica offset,哪個slave複製了越多的數據,offset越靠後,優先級就越高
(3)如果上面兩個條件都相同,那麼選擇一個run id比較小的那個slave

5、quorum和majority

每次一個哨兵要做主備切換,首先需要quorum數量的哨兵認爲odown,然後選舉出一個哨兵來做切換,這個哨兵還得得到majority哨兵的授權,才能正式執行切換

如果quorum < majority,比如5個哨兵,majority就是3,quorum設置爲2,那麼就3個哨兵授權就可以執行切換

但是如果quorum >= majority,那麼必須quorum數量的哨兵都授權,比如5個哨兵,quorum是5,那麼必須5個哨兵都同意授權,才能執行切換

6、configuration epoch

哨兵會對一套redis master+slave進行監控,有相應的監控的配置

執行切換的那個哨兵,會從要切換到的新master(salve->master)那裏得到一個configuration epoch,這就是一個version號,每次切換的version號都必須是唯一的

如果第一個選舉出的哨兵切換失敗了,那麼其他哨兵,會等待failover-timeout時間,然後接替繼續執行切換,此時會重新獲取一個新的configuration epoch,作爲新的version號

7、configuraiton傳播

哨兵完成切換之後,會在自己本地更新生成最新的master配置,然後同步給其他的哨兵,就是通過之前說的pub/sub消息機制

這裏之前的version號就很重要了,因爲各種消息都是通過一個channel去發佈和監聽的,所以一個哨兵完成一次新的切換之後,新的master配置是跟着新的version號的

其他的哨兵都是根據版本號的大小來更新自己的master配置的

5、如何保證redis掛掉之後再重啓數據可以進行恢復?
redis的持久化有哪幾種方式?不同的持久化機制都有什麼優缺點?持久化機制具體底層是如何實現的?

redis的持久化,RDB,AOF,區別,各自的特點是什麼,適合什麼場景?redis的企業級的持久化方案是什麼,是用來跟哪些企業級的場景結合起來使用的???

redis持久化的意義,在於故障恢復,如果通過持久化將數據搞一份兒在磁盤上去,然後定期比如說同步和備份到一些雲存儲服務上去,那麼就可以保證數據不丟失全部,還是可以恢復一部分數據回來的。

如果你把redis的持久化做好,備份和恢復方案做到企業級的程度,那麼即使你的redis故障了,也可以通過備份數據,快速恢復,一旦恢復立即對外提供服務,保證了高可用。

1、RDB和AOF兩種持久化機制的介紹
RDB持久化機制,對Redis中的數據執行週期性的持久化。

AOF機制對每條寫入命令作爲日誌,以append-only的模式寫入一個日誌文件中,在redis重啓的時候,可以通過回放AOF日誌中寫入的指令來重新構建整個數據集。
需要注意的是:redis中的數據,是有一定限量的,不可能說redis內存中的數據無限增長,所以不可能導致AOF文件會無限的增長。內存大小是一定的,到一定程度,redis會利用內存淘汰算法、LRU自動將內存中的一部分數據刪除。AOF是存放沒條寫命令的,所以會不斷的膨脹,當增大到一定程度時,會做rewirte操作。

rewirte操作,就會基於當時redis內存中的數據,來重新構造一個更小的AOF文件,然後,將舊的膨脹到一定程度的AOF文件刪除。
在這裏插入圖片描述
在這裏插入圖片描述
如果redis掛了,服務器上的內存和磁盤上的數據都丟了,可以從雲服務上拷貝回來之前的數據,放到指定的目錄中,然後重新啓動redis,redis就會自動根據持久化數據文件中的數據,去恢復內存中的數據,繼續對外提供服務。

如果同時使用RDB和AOF兩種持久化機制,那麼在redis重啓的時候,會==優先使用AOF來重新構建數據==,因爲AOF中的數據更加完整

2、RDB持久化機制的優點
(1)RDB會生成多個數據文件,每個數據文件都代表了某一個時刻中redis的數據,這種多個數據文件的方式,非常適合做冷備,可以將這種完整的數據文件發送到一些遠程的安全存儲上去,比如說Amazon的S3雲服務上去,在國內可以是阿里雲的ODPS分佈式存儲上,以預定好的備份策略來定期備份redis中的數據

AOF也可以做冷備份,只有一個文件,每隔一段時間copy一份。

(2)RDB對redis對外提供的讀寫服務,影響非常小,可以讓redis保持高性能,因爲redis主進程只需要fork一個子進程,讓子進程執行磁盤IO操作來進行RDB持久化即可。

(3)相對於AOF持久化機制來說,直接基於RDB數據文件來重啓和恢復redis進程,更加快速。
RDB做冷備份的優勢在於兩點:RDB是由Redis去控制固定時長生成快照文件,AOF是需要用戶自己處理;RDB做數據恢復時速度更快
AOF是需要執行指令的,所以比較慢。RDB直接加載即可。

3、RDB持久化機制的缺點
(1)如果想要在redis故障時,儘可能少的丟失數據,那麼RDB沒有AOF好。一般來說,RDB數據快照文件,都是每隔5分鐘,或者更長時間生成一次,這個時候就得接受一旦redis進程宕機,那麼會丟失最近5分鐘的數據,這是最大的缺點。

(2)RDB每次在fork子進程來執行RDB快照數據文件生成的時候,如果數據文件特別大,可能會導致對客戶端提供的服務暫停數毫秒,或者甚至數秒。一般不要RDB的間隔太大。

4、AOF持久化機制的優點
(1)AOF可以更好的保護數據不丟失,一般AOF會每隔1秒,通過一個後臺線程執行一次fsync操作,最多丟失1秒鐘的數據

(2)AOF日誌文件以append-only模式寫入,所以沒有任何磁盤尋址的開銷,寫入性能非常高,而且文件不容易破損,即使文件尾部破損,也很容易修復。

(3)AOF日誌文件即使過大的時候,出現後臺重寫操作,也不會影響客戶端的讀寫。因爲在rewrite log的時候,會對其中的指導進行壓縮,創建出一份需要恢復數據的最小日誌出來。再創建新日誌文件的時候,老的日誌文件還是照常寫入。當新的merge後的日誌文件ready的時候,再交換新老日誌文件即可。

(4)AOF日誌文件的命令通過非常可讀的方式進行記錄,這個特性非常適合做災難性的誤刪除的緊急恢復。比如某人不小心用flushall命令清空了所有數據,只要這個時候後臺rewrite還沒有發生,那麼就可以立即拷貝AOF文件,將最後一條flushall命令給刪了,然後再將該AOF文件放回去,就可以通過恢復機制,自動恢復所有數據。

5、AOF持久化機制的缺點
(1)對於同一份數據來說,AOF日誌文件通常比RDB數據快照文件更大。

(2)AOF開啓後,支持的寫QPS會比RDB支持的寫QPS低,因爲AOF一般會配置成每秒fsync一次日誌文件,當然,每秒一次fsync,性能也還是很高的。
(如果要保證一條數據都不丟,則可以設置每寫入一條數據就,fsync一次,但是這樣就會導致併發性能大大降低)
(3)以前AOF發生過bug,就是通過AOF記錄的日誌,進行數據恢復的時候,沒有恢復一模一樣的數據出來。所以說,類似AOF這種較爲複雜的基於命令日誌/merge/回放的方式,比基於RDB每次持久化一份完整的數據快照文件的方式,更加脆弱一些,容易有bug。不過AOF就是爲了避免rewrite過程導致的bug,因此每次rewrite並不是基於舊的指令日誌進行merge的,而是基於當時內存中的數據進行指令的重新構建,這樣健壯性會好很多。

(4)唯一比較大的缺點就在於數據恢復時比較慢,冷備不方便。

6、RDB和AOF到底該如何選擇

(1)不要僅僅使用RDB,因爲那樣會導致你丟失很多數據

(2)也不要僅僅使用AOF,因爲那樣有兩個問題,第一,你通過AOF做冷備,沒有RDB做冷備,來的恢復速度更快; 第二,RDB每次簡單粗暴生成數據快照,更加健壯,可以避免AOF這種複雜的備份和恢復機制的bug

(3)綜合使用AOF和RDB兩種持久化機制,用AOF來保證數據不丟失,作爲數據恢復的第一選擇; 用RDB來做不同程度的冷備,在AOF文件都丟失或損壞不可用的時候,還可以使用RDB來進行快速的數據恢復

5、redis cluster集羣模式的原理嗎?
Redis突破單機瓶頸,支撐海量數據的途徑是利用Redis Cluster。

redis cluster = 多master + 讀寫分離 + 高可用

我們只需要基於Redis Cluster去搭建Redis集羣即可,不需要手動去搭建replication複製 + 主從架構 + 讀寫分離 + 哨兵模式 + 高可用。

具體應用場景總結如下:

(1)如果你的數據量很少,主要是承載高併發高性能的場景,比如你的緩存一般就幾個G,單機足夠了。replication,一個mater,多個slave,要幾個slave跟你的要求的讀吞吐量有關係,然後自己搭建一個sentinal集羣,去保證redis主從架構的高可用性,就可以了。
(2)如果業務是:海量數據+高併發+高可用的場景,海量數據,如果你的數據量很大,那麼建議就用redis cluster。

分佈式數據存儲的核心算法:(在多個master節點的時候,數據如何分佈到這些節點上去)

hash算法 -> 一致性hash算法(memcached) -> 最終進化出了:redis cluster,hash slot算法

在redis cluster架構下,每個redis要放開兩個端口號,比如一個是6379,另外一個就是加10000的端口號,比如16379, 16379端口號是用來進行節點間通信的,也就是cluster bus的東西,集羣總線。cluster bus的通信,用來進行故障檢測,配置更新,故障轉移授權。cluster bus用了另外一種二進制的協議,主要用於節點間進行高效的數據交換,佔用更少的網絡帶寬和處理時間

redis cluster的hash slot算法:redis cluster有固定的16384個hash slot,對每個key計算CRC16值,然後對16384取模,可以獲取key對應的hash slot。

redis cluster中每個master都會持有部分slot,比如有3個master,那麼可能每個master持有5000多個hash slot。

hash slot讓node的增加和移除很簡單,增加一個master,就將其他master的hash slot移動部分過去,減少一個master,就將它的hash slot移動到其他master上去。

移動hash slot的成本是非常低的,客戶端的api,可以對指定的數據,讓他們走同一個hash slot,通過hash tag來實現。

採用Redis Cluster後,節點間的內部通信機制:redis cluster節點間採取gossip協議進行通信。應用gossip的好處在於,元數據的更新比較分散,不是集中在一個地方,更新請求會陸陸續續,打到所有節點上去更新,有一定的延時,降低了壓力; 缺點,元數據更新有延時,可能導致集羣的一些操作會有一些滯後。

gossip協議:gossip協議包含多種消息,包括ping,pong,meet,fail,等等。

6、面向集羣的jedis內部實現原理
開發過程中都是利用的Jedis,這是Redis的Java Client。如果使用Redis cluster架構,則使用jedis cluster api進行開發。

jedis cluster api與redis cluster集羣交互的一些基本原理:
(1)基於重定向的客戶端(網絡開銷太大,所以不用此模式)
redis-cli -c,自動重定向
( 1.1 )請求重定向
客戶端可能會挑選任意一個redis實例去發送命令,每個redis實例接收到命令,都會計算key對應的hash slot。如果在本地就在本地處理,否則返回moved給客戶端,讓客戶端進行重定向,cluster keyslot mykey,可以查看一個key對應的hash slot是什麼,用redis-cli的時候,可以加入-c參數,支持自動的請求重定向,redis-cli接收到moved之後,會自動重定向到對應的節點執行命令。
( 1.2 )計算hash slot
計算hash slot的算法,就是根據key計算CRC16值,然後對16384取模,拿到對應的hash slot。用hash tag可以手動指定key對應的slot,同一個hash tag下的key,都會在一個hash slot中,比如set mykey1:{100}和set mykey2:{100}。
( 1.3 )hash slot查找
節點間通過gossip協議進行數據交換,就知道每個hash slot在哪個節點上

(2)smart jedis
基於重定向的客戶端,很消耗網絡IO,因爲大部分情況下,可能都會出現一次請求重定向,才能找到正確的節點。所以大部分的客戶端,比如java redis客戶端,就是jedis,都是smart的
本地維護一份hashslot -> node的映射表,緩存,大部分情況下,直接走本地緩存就可以找到hashslot -> node,不需要通過節點進行moved重定向。

JedisCluster的工作原理:在JedisCluster初始化的時候,就會隨機選擇一個node,初始化hashslot -> node映射表,同時爲每個節點創建一個JedisPool連接池。每次基於JedisCluster執行操作,首先JedisCluster都會在本地計算key的hashslot,然後在本地映射表找到對應的節點。

  • 如果那個node正好還是持有那個hashslot,那麼就ok;
    如果說進行了reshard這樣的操作,可能hashslot已經不在那個node上了,就會返回moved

  • 如果JedisCluter API發現對應的節點返回moved,那麼利用該節點的元數據,更新本地的hashslot -> node映射表緩存

重複上面幾個步驟,直到找到對應的節點,如果重試超過5次,那麼就報錯,JedisClusterMaxRedirectionException。jedis老版本,可能會出現在集羣某個節點故障還沒完成自動切換恢復時,頻繁更新hash slot,頻繁ping節點檢查活躍,導致大量網絡IO開銷。jedis最新版本,對於這些過度的hash slot更新和ping,都進行了優化,避免了類似問題。

hashslot遷移和ask重定向:如果hash slot正在遷移,那麼會返回ask重定向給jedis,jedis接收到ask重定向之後,會重新定位到目標節點去執行,但是因爲ask發生在hash slot遷移過程中,所以JedisCluster API收到ask是不會更新hashslot本地緩存。已經可以確定說,hashslot已經遷移完了,moved是會更新本地hashslot->node映射表緩存的。

(3)高可用性與主備切換原理
redis cluster的高可用的原理,幾乎跟哨兵是類似的

  • 判斷節點宕機
  • 從節點過濾
  • 從節點選舉
  • 與哨兵比較:整個流程跟哨兵相比,非常類似,所以說,redis cluster功能強大,直接集成了replication和sentinal的功能。

7、如何應對緩存雪崩以及穿透問題?
(1)緩存雪崩
發生的現象:
在這裏插入圖片描述
緩存雪崩的事前事中事後的解決方案:

  • 事前:redis高可用,主從+哨兵,redis cluster,避免全盤崩潰
  • 事中:本地ehcache緩存 + hystrix限流&降級,避免MySQL被打死
  • 事後:redis持久化,快速恢復緩存數據
    在這裏插入圖片描述
    (2)緩存穿透
    緩存穿透的現象和解決方法:
    在這裏插入圖片描述

8、如何保證緩存與數據庫雙寫時的數據一致性?
最經典的緩存+數據庫讀寫的模式,cache aside pattern

Cache Aside Pattern
(1)讀的時候,先讀緩存,緩存沒有的話,那麼就讀數據庫,然後取出數據後放入緩存,同時返回響應
(2)更新的時候,先刪除緩存,然後再更新數據庫

爲什麼是刪除緩存,而不是更新緩存呢?
更新緩存的代價太高,其實刪除緩存,而不是更新緩存,就是一個lazy計算的思想,不要每次都重新做複雜的計算,不管它會不會用到,而是讓它到需要被使用的時候再重新計算。

數據庫與緩存雙寫不一致,很常見的問題,大型的緩存架構中,第一個解決方案

(1)最初級的緩存不一致問題以及解決方案
問題:先修改數據庫,再刪除緩存,如果刪除緩存失敗了,那麼會導致數據庫中是新數據,緩存中是舊數據,數據出現不一致.
解決思路:先刪除緩存,再修改數據庫,如果刪除緩存成功了,如果修改數據庫失敗了,那麼數據庫中是舊數據,緩存中是空的,那麼數據不會不一致。但是這個解決方案只是解決了對於更新DB失敗的情況有用,因爲更新DB成功的情況下依然會出現數據不一樣,例如下面將要講的(2)。

(2)比較複雜的數據不一致問題分析
問題:數據發生了變更,先刪除了緩存,然後要去修改數據庫,此時還沒修改DB,一個請求過來,去讀緩存,發現緩存空了,去查詢數據庫,查到了修改前的舊數據,放到了緩存中,然後數據變更的程序完成了數據庫的修改。此時數據庫和緩存中的不一樣了。
這種情況下是上億流量高併發場景下,緩存會出現這個問題。
解決方案:通過新建一個內存隊列,首先對這個數據有唯一一個標識,如果讀請求過來,查詢到該數據正在更新操作,則進隊列等待(注意等待超時時間和處理,如果時間較長則返回一定的數據),等更新操作完後,使得讀請求讀緩存,如果緩存沒有則讀DB並更新到緩存。

總結:
一般來說,就是如果你的系統不是嚴格要求緩存+數據庫必須一致性的話,緩存可以稍微的跟數據庫偶爾有不一致的情況,最好不要做這個方案,讀請求和寫請求串行化,串到一個內存隊列裏去,這樣就可以保證一定不會出現不一致的情況。

串行化之後,就會導致系統的吞吐量會大幅度的降低,用比正常情況下多幾倍的機器去支撐線上的一個請求。

9、redis併發競爭問題以及解決方案?
redis的併發競爭問題是什麼?如何解決這個問題?瞭解Redis事務的CAS方案嗎?
併發競爭問題是指:多客戶端同時併發寫一個key,如下圖:
在這裏插入圖片描述
解決方案
通過分佈式鎖+數據版本
在這裏插入圖片描述
而且redis自己就有天然解決這個問題的CAS類的樂觀鎖方案。

10、生產環境中的redis是怎麼部署的?
redis是主從架構?集羣架構?用了哪種集羣方案?有沒有做高可用保證?有沒有開啓持久化機制確保可以進行數據恢復?線上redis給幾個G的內存?設置了哪些參數?壓測後你們redis集羣承載多少QPS?

redis cluster,10臺機器,5臺機器部署了redis主實例,另外5臺機器部署了redis的從實例,每個主實例掛了一個從實例,5個節點對外提供讀寫服務,每個節點的讀寫高峯qps可能可以達到每秒5萬,5臺機器最多是25萬讀寫請求/s。

機器是什麼配置?32G內存+8核CPU+1T磁盤,但是分配給redis進程的是10g內存,一般線上生產環境,redis的內存儘量不要超過10g,超過10g可能會有問題。

5臺機器對外提供讀寫,一共有50g內存。

因爲每個主實例都掛了一個從實例,所以是高可用的,任何一個主實例宕機,都會自動故障遷移,redis從實例會自動變成主實例繼續提供讀寫服務。

你往內存裏寫的是什麼數據?每條數據的大小是多少?商品數據,每條數據是10kb。100條數據是1mb,10萬條數據是1g。常駐內存的是200萬條商品數據,佔用內存是20g,僅僅不到總內存的50%。

目前高峯期每秒就是3500左右的請求量。

十一、分佈式事務

(1)兩階段提交方案/XA方案
也叫做兩階段提交事務方案,這個舉個例子,比如說咱們公司裏經常tb是吧(就是團建),然後一般會有個tb主席(就是負責組織團建的那個人)。(tb,team building,團建)

第一個階段,一般tb主席會提前一週問一下團隊裏的每個人,說,大傢伙,下週六我們去滑雪+燒烤,去嗎?這個時候tb主席開始等待每個人的回答,如果所有人都說ok,那麼就可以決定一起去這次tb。如果這個階段裏,任何一個人回答說,我有事不去了,那麼tb主席就會取消這次活動。

第二個階段,那下週六大家就一起去滑雪+燒烤了

所以這個就是所謂的XA事務,兩階段提交,有一個事務管理器的概念,負責協調多個數據庫(資源管理器)的事務,事務管理器先問問各個數據庫你準備好了嗎?如果每個數據庫都回復ok,那麼就正式提交事務,在各個數據庫上執行操作;如果任何一個數據庫回答不ok,那麼就回滾事務。

這種分佈式事務方案,比較適合單塊應用裏,跨多個庫的分佈式事務,而且因爲嚴重依賴於數據庫層面來搞定複雜的事務,效率很低,絕對不適合高併發的場景。如果要玩兒,那麼基於spring + JTA就可以搞定,自己隨便搜個demo看看就知道了。

這個方案,我們很少用,一般來說某個系統內部如果出現跨多個庫的這麼一個操作,是不合規的。我可以給大家介紹一下, 現在微服務,一個大的系統分成幾百個服務,幾十個服務。一般來說,我們的規定和規範,是要求說每個服務只能操作自己對應的一個數據庫。

如果你要操作別的服務對應的庫,不允許直連別的服務的庫,違反微服務架構的規範,你隨便交叉胡亂訪問,幾百個服務的話,全體亂套,這樣的一套服務是沒法管理的,沒法治理的,經常數據被別人改錯,自己的庫被別人寫掛。

如果你要操作別人的服務的庫,你必須是通過調用別的服務的接口來實現,絕對不允許你交叉訪問別人的數據庫!

(2)TCC方案

TCC的全程是:Try、Confirm、Cancel。

這個其實是用到了補償的概念,分爲了三個階段:

1)Try階段:這個階段說的是對各個服務的資源做檢測以及對資源進行鎖定或者預留
2)Confirm階段:這個階段說的是在各個服務中執行實際的操作
3)Cancel階段:如果任何一個服務的業務方法執行出錯,那麼這裏就需要進行補償,就是執行已經執行成功的業務邏輯的回滾操作

給大家舉個例子吧,比如說跨銀行轉賬的時候,要涉及到兩個銀行的分佈式事務,如果用TCC方案來實現,思路是這樣的:

1)Try階段:先把兩個銀行賬戶中的資金給它凍結住就不讓操作了
2)Confirm階段:執行實際的轉賬操作,A銀行賬戶的資金扣減,B銀行賬戶的資金增加
3)Cancel階段:如果任何一個銀行的操作執行失敗,那麼就需要回滾進行補償,就是比如A銀行賬戶如果已經扣減了,但是B銀行賬戶資金增加失敗了,那麼就得把A銀行賬戶資金給加回去

這種方案說實話幾乎很少用人使用,我們用的也比較少,但是也有使用的場景。因爲這個事務回滾實際上是嚴重依賴於你自己寫代碼來回滾和補償了,會造成補償代碼巨大,非常之噁心。

比如說我們,一般來說跟錢相關的,跟錢打交道的,支付、交易相關的場景,我們會用TCC,嚴格嚴格保證分佈式事務要麼全部成功,要麼全部自動回滾,嚴格保證資金的正確性,在資金上出現問題

比較適合的場景:這個就是除非你是真的一致性要求太高,是你係統中核心之核心的場景,比如常見的就是資金類的場景,那你可以用TCC方案了,自己編寫大量的業務邏輯,自己判斷一個事務中的各個環節是否ok,不ok就執行補償/回滾代碼。

而且最好是你的各個業務執行的時間都比較短。

但是說實話,一般儘量別這麼搞,自己手寫回滾邏輯,或者是補償邏輯,實在太噁心了,那個業務代碼很難維護。

(3)本地消息表

國外的ebay搞出來的這麼一套思想

這個大概意思是這樣的

1)A系統在自己本地一個事務裏操作同時,插入一條數據到消息表
2)接着A系統將這個消息發送到MQ中去
3)B系統接收到消息之後,在一個事務裏,往自己本地消息表裏插入一條數據,同時執行其他的業務操作,如果這個消息已經被處理過了,那麼此時這個事務會回滾,這樣保證不會重複處理消息
4)B系統執行成功之後,就會更新自己本地消息表的狀態以及A系統消息表的狀態
5)如果B系統處理失敗了,那麼就不會更新消息表狀態,那麼此時A系統會定時掃描自己的消息表,如果有沒處理的消息,會再次發送到MQ中去,讓B再次處理
6)這個方案保證了最終一致性,哪怕B事務失敗了,但是A會不斷重發消息,直到B那邊成功爲止

這個方案說實話最大的問題就在於嚴重依賴於數據庫的消息表來管理事務啥的???這個會導致如果是高併發場景咋辦呢?咋擴展呢?所以一般確實很少用

(4)可靠消息最終一致性方案

這個的意思,就是乾脆不要用本地的消息表了,直接基於MQ來實現事務。比如阿里的RocketMQ就支持消息事務。

大概的意思就是:
1)A系統先發送一個prepared消息到mq,如果這個prepared消息發送失敗那麼就直接取消操作別執行了
2)如果這個消息發送成功過了,那麼接着執行本地事務,如果成功就告訴mq發送確認消息,如果失敗就告訴mq回滾消息
3)如果發送了確認消息,那麼此時B系統會接收到確認消息,然後執行本地的事務
4)mq會自動定時輪詢所有prepared消息回調你的接口,問你,這個消息是不是本地事務處理失敗了,所有沒發送確認消息?那是繼續重試還是回滾?一般來說這裏你就可以查下數據庫看之前本地事務是否執行,如果回滾了,那麼這裏也回滾吧。這個就是避免可能本地事務執行成功了,別確認消息發送失敗了。
5)這個方案裏,要是系統B的事務失敗了咋辦?重試咯,自動不斷重試直到成功,如果實在是不行,要麼就是針對重要的資金類業務進行回滾,比如B系統本地回滾後,想辦法通知系統A也回滾;或者是發送報警由人工來手工回滾和補償

這個還是比較合適的,目前國內互聯網公司大都是這麼玩兒的,要不你舉用RocketMQ支持的,要不你就自己基於類似ActiveMQ?RabbitMQ?自己封裝一套類似的邏輯出來,總之思路就是這樣子的

(5)最大努力通知方案

這個方案的大致意思就是:

1)系統A本地事務執行完之後,發送個消息到MQ
2)這裏會有個專門消費MQ的最大努力通知服務,這個服務會消費MQ然後寫入數據庫中記錄下來,或者是放入一個內存隊列也可以,接着調用系統B的接口
3)要是系統B執行成功就ok了;要是系統B執行失敗了,那麼最大努力通知服務就定時嘗試重新調用系統B,反覆N次,最後還是不行就放棄

(6)你們公司是如何處理分佈式事務的?

一般情況下,使用TCC來保證強一致性;然後其他的一些場景基於了阿里的RocketMQ來實現了分佈式事務。

你找一個嚴格資金要求絕對不能錯的場景,你可以說你是用的TCC方案;如果是一般的分佈式事務場景,訂單插入之後要調用庫存服務更新庫存,庫存數據沒有資金那麼的敏感,可以用可靠消息最終一致性方案

友情提示一下,rocketmq 3.2.6之前的版本,是可以按照上面的思路來的,但是之後接口做了一些改變,我這裏不再贅述了。

當然如果你願意,你可以參考可靠消息最終一致性方案來自己實現一套分佈式事務,比如基於rabbitmq來玩兒。

4、昨天學員給我提的一個問題

老師,我們現在想保證我們的某個系統非常的可靠,任何一個數據都不能錯,我們用的是微服務架構,幾十個服務。結果我們一盤點,發現,如果到處都要搞的話,一個系統要做幾十個分佈式事務出來。

我們的經驗,我帶幾十人的team,最大的一個項目,起碼幾百個服務,複雜的分佈式大型系統,裏面其實也沒幾個分佈式事務。

你其實用任何一個分佈式事務的這麼一個方案,都會導致你那塊兒代碼會複雜10倍。很多情況下,系統A調用系統B、系統C、系統D,我們可能根本就不做分佈式事務。如果調用報錯會打印異常日誌。

每個月也就那麼幾個bug,很多bug是功能性的,體驗性的,真的是涉及到數據層面的一些bug,一個月就幾個,兩三個?如果你爲了確保系統自動保證數據100%不能錯,上了幾十個分佈式事務,代碼太複雜;性能太差,系統吞吐量、性能大幅度下跌。

99%的分佈式接口調用,不要做分佈式事務,直接就是監控(發郵件、發短信)、記錄日誌(一旦出錯,完整的日誌)、事後快速的定位、排查和出解決方案、修復數據。
每個月,每隔幾個月,都會對少量的因爲代碼bug,導致出錯的數據,進行人工的修復數據,自己臨時動手寫個程序,可能要補一些數據,可能要刪除一些數據,可能要修改一些字段的值。

比你做50個分佈式事務,成本要來的低上百倍,低幾十倍

trade off,權衡,要用分佈式事務的時候,一定是有成本,代碼會很複雜,開發很長時間,性能和吞吐量下跌,系統更加複雜更加脆弱反而更加容易出bug;好處,如果做好了,TCC、可靠消息最終一致性方案,一定可以100%保證你那快數據不會出錯。

1%,0.1%,0.01%的業務,資金、交易、訂單,我們會用分佈式事務方案來保證,會員積分、優惠券、商品信息,其實不要這麼搞了
在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述

十二、分佈式鎖

(1)redis分佈式鎖

官方叫做RedLock算法,是redis官方支持的分佈式鎖算法。

這個分佈式鎖有3個重要的考量點,互斥(只能有一個客戶端獲取鎖),不能死鎖,容錯(大部分redis節點或者這個鎖就可以加可以釋放)

第一個最普通的實現方式,如果就是在redis裏創建一個key算加鎖

SET my:lock 隨機值 NX PX 30000,這個命令就ok,這個的NX的意思就是隻有key不存在的時候纔會設置成功,PX 30000的意思是30秒後鎖自動釋放。別人創建的時候如果發現已經有了就不能加鎖了。
在這裏插入圖片描述
釋放鎖就是刪除key,但是一般可以用lua腳本刪除,判斷value一樣才刪除:

關於redis如何執行lua腳本,自行百度

if redis.call("get",KEYS[1]) == ARGV[1] then
return redis.call("del",KEYS[1])
else
    return 0
end

爲啥要用隨機值呢?因爲如果某個客戶端獲取到了鎖,但是阻塞了很長時間才執行完,此時可能已經自動釋放鎖了,此時可能別的客戶端已經獲取到了這個鎖,要是你這個時候直接刪除key的話會有問題,所以得用隨機值加上面的lua腳本來釋放鎖。

但是這樣是肯定不行的。因爲如果是普通的redis單實例,那就是單點故障。或者是redis普通主從,那redis主從異步複製,如果主節點掛了,key還沒同步到從節點,此時從節點切換爲主節點,別人就會拿到鎖。

第二個實現方式:官方推薦的Redis鎖機制:RedLock算法

這個場景是假設有一個redis cluster,有5個redis master實例。然後執行如下步驟獲取一把鎖:

1)獲取當前時間戳,單位是毫秒
2)跟上面類似,輪流嘗試在每個master節點上創建鎖,過期時間較短,一般就幾十毫秒
3)嘗試在大多數節點上建立一個鎖,比如5個節點就要求是3個節點(n / 2 +1)
4)客戶端計算建立好鎖的時間,如果建立鎖的時間小於超時時間,就算建立成功了
5)要是鎖建立失敗了,那麼就依次刪除這個鎖
6)只要別人建立了一把分佈式鎖,你就得不斷輪詢去嘗試獲取鎖
集羣后,可以防止單點故障。
在這裏插入圖片描述
雖然基於Redis的兩個分佈式鎖可能能夠實現,但是並不推薦使用。

(2)zk分佈式鎖

zk分佈式鎖,其實可以做的比較簡單,就是某個節點嘗試創建臨時znode,此時創建成功了就獲取了這個鎖;這個時候別的客戶端來創建鎖會失敗,只能註冊個監聽器監聽這個鎖。釋放鎖就是刪除這個znode,一旦釋放掉就會通知客戶端,然後有一個等待着的客戶端就可以再次重新枷鎖。
在這裏插入圖片描述

/**
 * ZooKeeperSession
 * @author Administrator
 *
 */
public class ZooKeeperSession {
	
	private static CountDownLatch connectedSemaphore = new CountDownLatch(1);
	
	private ZooKeeper zookeeper;
private CountDownLatch latch;

	public ZooKeeperSession() {
		try {
			this.zookeeper = new ZooKeeper(
					"192.168.31.187:2181,192.168.31.19:2181,192.168.31.227:2181", 
					50000, 
					new ZooKeeperWatcher());			
			try {
				connectedSemaphore.await();
			} catch(InterruptedException e) {
				e.printStackTrace();
			}

			System.out.println("ZooKeeper session established......");
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
	
	/**
	 * 獲取分佈式鎖
	 * @param productId
	 */
	public Boolean acquireDistributedLock(Long productId) {
		String path = "/product-lock-" + productId;
	
		try {
			zookeeper.create(path, "".getBytes(), 
					Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL);
return true;
		} catch (Exception e) {
while(true) {
				try {
Stat stat = zk.exists(path, true); // 相當於是給node註冊一個監聽器,去看看這個監聽器是否存在
if(stat != null) {
this.latch = new CountDownLatch(1);
this.latch.await(waitTime, TimeUnit.MILLISECONDS);
this.latch = null;
}
zookeeper.create(path, "".getBytes(), 
						Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL);
return true;
} catch(Exception e) {
continue;
}
}

// 很不優雅,我呢就是給大家來演示這麼一個思路
// 比較通用的,我們公司裏我們自己封裝的基於zookeeper的分佈式鎖,我們基於zookeeper的臨時順序節點去實現的,比較優雅的
		}
return true;
	}
	
	/**
	 * 釋放掉一個分佈式鎖
	 * @param productId
	 */
	public void releaseDistributedLock(Long productId) {
		String path = "/product-lock-" + productId;
		try {
			zookeeper.delete(path, -1); 
			System.out.println("release the lock for product[id=" + productId + "]......");  
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
	
	/**
	 * 建立zk session的watcher
	 * @author Administrator
	 *
	 */
	private class ZooKeeperWatcher implements Watcher {

		public void process(WatchedEvent event) {
			System.out.println("Receive watched event: " + event.getState());

			if(KeeperState.SyncConnected == event.getState()) {
				connectedSemaphore.countDown();
			} 

if(this.latch != null) {  
this.latch.countDown();  
}
		}
		
	}
	
	/**
	 * 封裝單例的靜態內部類
	 * @author Administrator
	 *
	 */
	private static class Singleton {
		
		private static ZooKeeperSession instance;
		
		static {
			instance = new ZooKeeperSession();
		}
		
		public static ZooKeeperSession getInstance() {
			return instance;
		}
		
	}
	
	/**
	 * 獲取單例
	 * @return
	 */
	public static ZooKeeperSession getInstance() {
		return Singleton.getInstance();
	}
	
	/**
	 * 初始化單例的便捷方法
	 */
	public static void init() {
		getInstance();
	}
	
}

(3)redis分佈式鎖和zk分佈式鎖的對比

redis分佈式鎖,其實需要自己不斷去嘗試獲取鎖,比較消耗性能

zk分佈式鎖,獲取不到鎖,註冊個監聽器即可,不需要不斷主動嘗試獲取鎖,性能開銷較小

另外一點就是,如果是redis獲取鎖的那個客戶端bug了或者掛了,那麼只能等待超時時間之後才能釋放鎖;而zk的話,因爲創建的是臨時znode,只要客戶端掛了,znode就沒了,此時就自動釋放鎖

redis分佈式鎖大家每發現好麻煩嗎?遍歷上鎖,計算時間等等。。。zk的分佈式鎖語義清晰實現簡單

所以先不分析太多的東西,就說這兩點,我個人實踐認爲zk的分佈式鎖比redis的分佈式鎖牢靠、而且模型簡單易用

public class ZooKeeperDistributedLock implements Watcher{
	
    private ZooKeeper zk;
    private String locksRoot= "/locks";
    private String productId;
    private String waitNode;
    private String lockNode;
    private CountDownLatch latch;
    private CountDownLatch connectedLatch = new CountDownLatch(1);
private int sessionTimeout = 30000; 

    public ZooKeeperDistributedLock(String productId){
        this.productId = productId;
         try {
	   String address = "192.168.31.187:2181,192.168.31.19:2181,192.168.31.227:2181";
            zk = new ZooKeeper(address, sessionTimeout, this);
            connectedLatch.await();
        } catch (IOException e) {
            throw new LockException(e);
        } catch (KeeperException e) {
            throw new LockException(e);
        } catch (InterruptedException e) {
            throw new LockException(e);
        }
    }

    public void process(WatchedEvent event) {
        if(event.getState()==KeeperState.SyncConnected){
            connectedLatch.countDown();
            return;
        }

        if(this.latch != null) {  
            this.latch.countDown(); 
        }
    }

    public void acquireDistributedLock() {   
        try {
            if(this.tryLock()){
                return;
            }
            else{
                waitForLock(waitNode, sessionTimeout);
            }
        } catch (KeeperException e) {
            throw new LockException(e);
        } catch (InterruptedException e) {
            throw new LockException(e);
        } 
}

    public boolean tryLock() {
        try {
 		// 傳入進去的locksRoot + “/” + productId
		// 假設productId代表了一個商品id,比如說1
		// locksRoot = locks
		// /locks/10000000000,/locks/10000000001,/locks/10000000002
            lockNode = zk.create(locksRoot + "/" + productId, new byte[0], ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);
   
            // 看看剛創建的節點是不是最小的節點
	 	// locks:10000000000,10000000001,10000000002
            List<String> locks = zk.getChildren(locksRoot, false);
            Collections.sort(locks);
	
            if(lockNode.equals(locksRoot+"/"+ locks.get(0))){
                //如果是最小的節點,則表示取得鎖
                return true;
            }
	
            //如果不是最小的節點,找到比自己小1的節點
	  int previousLockIndex = -1;
            for(int i = 0; i < locks.size(); i++) {
		if(lockNode.equals(locksRoot + “/” + locks.get(i))) {
	         	    previousLockIndex = i - 1;
		    break;
		}
	   }
	   
	   this.waitNode = locks.get(previousLockIndex);
        } catch (KeeperException e) {
            throw new LockException(e);
        } catch (InterruptedException e) {
            throw new LockException(e);
        }
        return false;
    }
     
    private boolean waitForLock(String waitNode, long waitTime) throws InterruptedException, KeeperException {
        Stat stat = zk.exists(locksRoot + "/" + waitNode, true);
        if(stat != null){
            this.latch = new CountDownLatch(1);
            this.latch.await(waitTime, TimeUnit.MILLISECONDS);            	   this.latch = null;
        }
        return true;
}

    public void unlock() {
        try {
		// 刪除/locks/10000000000節點
		// 刪除/locks/10000000001節點
            System.out.println("unlock " + lockNode);
            zk.delete(lockNode,-1);
            lockNode = null;
            zk.close();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (KeeperException e) {
            e.printStackTrace();
        }
}

    public class LockException extends RuntimeException {
        private static final long serialVersionUID = 1L;
        public LockException(String e){
            super(e);
        }
        public LockException(Exception e){
            super(e);
        }
}

// 如果有一把鎖,被多個人給競爭,此時多個人會排隊,第一個拿到鎖的人會執行,然後釋放鎖,後面的每個人都會去監聽排在自己前面的那個人創建的node上,一旦某個人釋放了鎖,排在自己後面的人就會被zookeeper給通知,一旦被通知了之後,就ok了,自己就獲取到了鎖,就可以執行代碼了
}  

十三、ZK(最常用的分佈式鎖就是zookeeper來做分佈式鎖)

大致來說,zk的使用場景如下,我就舉幾個簡單的,大家能說幾個就好了:

(1)分佈式協調:這個其實是zk很經典的一個用法,簡單來說,就好比,你A系統發送個請求到mq,然後B消息消費之後處理了。那A系統如何知道B系統的處理結果?用zk就可以實現分佈式系統之間的協調工作。A系統發送請求之後可以在zk上對某個節點的值註冊個監聽器,一旦B系統處理完了就修改zk那個節點的值,A立馬就可以收到通知,完美解決。
在這裏插入圖片描述
(2)分佈式鎖:對某一個數據連續發出兩個修改操作,兩臺機器同時收到了請求,但是隻能一臺機器先執行另外一個機器再執行。那麼此時就可以使用zk分佈式鎖,一個機器接收到了請求之後先獲取zk上的一把分佈式鎖,就是可以去創建一個znode,接着執行操作;然後另外一個機器也嘗試去創建那個znode,結果發現自己創建不了,因爲被別人創建了。。。。那隻能等着,等第一個機器執行完了自己再執行。
在這裏插入圖片描述
(3)元數據/配置信息管理:zk可以用作很多系統的配置信息的管理,比如kafka、storm等等很多分佈式系統都會選用zk來做一些元數據、配置信息的管理,包括dubbo註冊中心不也支持zk麼
在這裏插入圖片描述
(4)HA高可用性:這個應該是很常見的,比如hadoop、hdfs、yarn等很多大數據系統,都選擇基於zk來開發HA高可用機制,就是一個重要進程一般會做主備兩個,主進程掛了立馬通過zk感知到切換到備用進程
在這裏插入圖片描述

十四、Dubbo

1、Dubbo的工作原理?註冊中心掛了可以繼續通信嗎?

Dubbo原理:

  • 第一層:service層,接口層,給服務提供者和消費者來實現的
  • 第二層:config層,配置層,主要是對dubbo進行各種配置的
  • 第三層:proxy層,服務代理層,透明生成客戶端的stub和服務單的skeleton
  • 第四層:registry層,服務註冊層,負責服務的註冊與發現
  • 第五層:cluster層,集羣層,封裝多個服務提供者的路由以及負載均衡,將多個實例組合成一個服務
  • 第六層:monitor層,監控層,對rpc接口的調用次數和調用時間進行監控 第七層:protocol層,遠程調用層,封裝rpc調用
  • 第八層:exchange層,信息交換層,封裝請求響應模式,同步轉異步
  • 第九層:transport層,網絡傳輸層,抽象mina和netty爲統一接口 第十層:serialize層,數據序列化層

工作流程:

  • 第一步,provider向註冊中心去註冊
  • 第二步,consumer從註冊中心訂閱服務,註冊中心會通知consumer註冊好的服務
  • 第三步,consumer調用provider
  • 第四步,consumer和provider都異步的通知監控中心
    在這裏插入圖片描述
    註冊中心掛了可以繼續通通信,因爲剛開始初始化的時候,消費者會將提供者的地址等信息拉取到本地緩存,所以註冊中心掛了可以繼續通信。

2、dubbo都支持哪些通信協議以及序列化協議?
1)dubbo協議
dubbo://192.168.0.1:20188
默認就是走dubbo協議的,單一長連接,NIO異步通信,基於hessian作爲序列化協議
適用的場景就是:傳輸數據量很小(每次請求在100kb以內),但是併發量很高

爲了要支持高併發場景,一般是服務提供者就幾臺機器,但是服務消費者有上百臺,可能每天調用量達到上億次!此時用長連接是最合適的,就是跟每個服務消費者維持一個長連接就可以,可能總共就100個連接。然後後面直接基於長連接NIO異步通信,可以支撐高併發請求。

否則如果上億次請求每次都是短連接的話,服務提供者會扛不住。

而且因爲走的是單一長連接,所以傳輸數據量太大的話,會導致併發能力降低。所以一般建議是傳輸數據量很小,支撐高併發訪問。

2)rmi協議
走java二進制序列化,多個短連接,適合消費者和提供者數量差不多,適用於文件的傳輸,一般較少用

3)hessian協議
走hessian序列化協議,多個短連接,適用於提供者數量比消費者數量還多,適用於文件的傳輸,一般較少用

4)http協議
走json序列化

5)webservice
走SOAP文本序列化

dubbo支持的序列化協議:dubbo實際基於不同的通信協議,支持hessian、java二進制序列化、json、SOAP文本序列化多種序列化協議。但是hessian是其默認的序列化協議。
常見序列化框架的性能對比:
https://www.cnblogs.com/lonelywolfmoutain/p/5563985.html

3、dubbo支持哪些負載均衡、高可用以及動態代理的策略?

負載均衡策略:

  • random loadbalance隨機
    默認情況下,dubbo是random load balance隨機調用實現負載均衡,可以對provider不同實例設置不同的權重,會按照權重來負載均衡,權重越大分配流量越高,一般就用這個默認的就可以了。
  • roundrobin loadbalance輪詢
    這個的話默認就是均勻地將流量打到各個機器上去,但是如果各個機器的性能不一樣,容易導致性能差的機器負載過高。所以此時需要調整權重,讓性能差的機器承載權重小一些,流量少一些。
  • leastactive loadbalance最少活躍
    這個就是自動感知一下,如果某個機器性能越差,那麼接收的請求越少,越不活躍,此時就會給不活躍的性能差的機器更少的請求
  • consistanthash loadbalance一致性哈希
    一致性Hash算法,相同參數的請求一定分發到一個provider上去,provider掛掉的時候,會基於虛擬節點均勻分配剩餘的流量,抖動不會太大。如果你需要的不是隨機負載均衡,是要一類請求都到一個節點,那就走這個一致性hash策略。

dubbo集羣容錯策略:

  • failover cluster模式
    失敗自動切換,自動重試其他機器,默認就是這個,常見於讀操作
  • failfast cluster模式
    一次調用失敗就立即失敗,常見於寫操作
  • failsafe cluster模式
    出現異常時忽略掉,常用於不重要的接口調用,比如記錄日誌
  • failbackc cluster模式
    失敗了後臺自動記錄請求,然後定時重發,比較適合於寫消息隊列這種
  • forking cluster
    並行調用多個provider,只要一個成功就立即返回
  • broadcacst cluster
    逐個調用所有的provider

dubbo動態代理策略:
默認使用javassist動態字節碼生成,創建代理類;但是可以通過spi擴展機制配置自己的動態代理策略;

4、SPI Dubbo

spi,簡單來說,就是service provider interface,說白了是什麼意思呢,比如你有個接口,現在這個接口有3個實現類,那麼在系統運行的時候對這個接口到底選擇哪個實現類呢?這就需要spi了,需要根據指定的配置或者是默認的配置,去找到對應的實現類加載進來,然後用這個實現類的實例對象。

接口A -> 實現A1,實現A2,實現A3

配置一下,接口A = 實現A2

在系統實際運行的時候,會加載你的配置,用實現A2實例化一個對象來提供服務

比如說你要通過jar包的方式給某個接口提供實現,然後你就在自己jar包的META-INF/services/目錄下放一個跟接口同名的文件,裏面指定接口的實現裏是自己這個jar包裏的某個類。ok了,別人用了一個接口,然後用了你的jar包,就會在運行的時候通過你的jar包的那個文件找到這個接口該用哪個實現類。

這是jdk提供的一個功能。

比如說你有個工程A,有個接口A,接口A在工程A裏是沒有實現類的 -> 系統在運行的時候,怎麼給接口A選擇一個實現類呢?

你就可以自己搞一個jar包,META-INF/services/,放上一個文件,文件名就是接口名,接口A,接口A的實現類=com.zhss.service.實現類A2。讓工程A來依賴你的這個jar包,然後呢在系統運行的時候,工程A跑起來,對接口A,就會掃描自己依賴的所有的jar包,在每個jar裏找找,有沒有META-INF/services文件夾,如果有,在裏面找找,有沒有接口A這個名字的文件,如果有在裏面找一下你指定的接口A的實現是你的jar包裏的哪個類?

SPI機制,一般來說用在哪兒?插件擴展的場景,比如說你開發的是一個給別人使用的開源框架,如果你想讓別人自己寫個插件,插到你的開源框架裏面來,擴展某個功能。

經典的思想體現,大家平時都在用,比如說jdbc,java定義了一套jdbc的接口,但是java是沒有提供jdbc的實現類。但是實際上項目跑的時候,要使用jdbc接口的哪些實現類呢?一般來說,我們要根據自己使用的數據庫,比如msyql,你就將mysql-jdbc-connector.jar,引入進來;oracle,你就將oracle-jdbc-connector.jar,引入進來。在系統跑的時候,碰到你使用jdbc的接口,他會在底層使用你引入的那個jar中提供的實現類。
在這裏插入圖片描述
dubbo也用了spi思想,不過沒有用jdk的spi機制,是自己實現的一套spi機制。

Protocol protocol = ExtensionLoader.getExtensionLoader(Protocol.class).getAdaptiveExtension();

Protocol是一個接口,在運行時dubbo要判斷一下,在系統運行的時候,應該選用這個Protocol接口的哪個實現類來實例化對象來使用呢?

上面代碼的右邊的意思是:他會去找一個你配置的Protocol,他就會將你配置的Protocol實現類,加載到jvm中來,然後實例化對象,就用你的那個Protocol實現類就可以了

微內核,可插拔,大量的組件,Protocol負責rpc調用的東西,你可以實現自己的rpc調用組件,實現Protocol接口,給自己的一個實現類即可。

這行代碼就是dubbo裏大量使用的,就是對很多組件,都是保留一個接口和多個實現,然後在系統運行的時候動態根據配置去找到對應的實現類。如果你沒配置,那就走默認的實現好了,沒問題。

@SPI("dubbo")  	// 默認就是dubbo,就直接去/META_INF/dubbo/internal/com.alibaba.dubbo.rpc.Protocol文件中讀取key是dubbo的value
public interface Protocol {  
      
    int getDefaultPort();  
  
    @Adaptive  	// 動態加載用戶自定義的
    <T> Exporter<T> export(Invoker<T> invoker) throws RpcException;  
  
    @Adaptive  
    <T> Invoker<T> refer(Class<T> type, URL url) throws RpcException;  

    void destroy();  
  
}  

在dubbo自己的jar裏,在/META_INF/dubbo/internal/com.alibaba.dubbo.rpc.Protocol文件中:

dubbo=com.alibaba.dubbo.rpc.protocol.dubbo.DubboProtocol
http=com.alibaba.dubbo.rpc.protocol.http.HttpProtocol
hessian=com.alibaba.dubbo.rpc.protocol.hessian.HessianProtocol

所以說,這就看到了dubbo的spi機制默認是怎麼玩兒的了,其實就是Protocol接口,@SPI(“dubbo”)說的是,通過SPI機制來提供實現類,實現類是通過dubbo作爲默認key去配置文件裏找到的,配置文件名稱與接口全限定名一樣的,通過dubbo作爲key可以找到默認的實現了就是com.alibaba.dubbo.rpc.protocol.dubbo.DubboProtocol。

dubbo的默認網絡通信協議,就是dubbo協議,用的DubboProtocol

如果想要動態替換掉默認的實現類,需要使用@Adaptive接口,Protocol接口中,有兩個方法加了@Adaptive註解,就是說那倆接口會被代理實現。

啥意思呢?

比如這個Protocol接口搞了倆@Adaptive註解標註了方法,在運行的時候會針對Protocol生成代理類,這個代理類的那倆方法裏面會有代理代碼,代理代碼會在運行的時候動態根據url中的protocol來獲取那個key,默認是dubbo,你也可以自己指定,你如果指定了別的key,那麼就會獲取別的實現類的實例了。

通過這個url中的參數,就可以控制動態使用不同的組件實現類

好吧,那下面來說說怎麼來自己擴展dubbo中的組件

自己寫個工程,要是那種可以打成jar包的,裏面的src/main/resources目錄下,搞一個META-INF/services,裏面放個文件叫:com.alibaba.dubbo.rpc.Protocol,文件裏搞一個my=com.zhss.MyProtocol。自己把jar弄到nexus私服裏去。

然後自己搞一個dubbo provider工程,在這個工程裏面依賴你自己搞的那個jar,然後在spring配置文件裏給個配置:

<dubbo:protocol name=”my” port=”20000” />

這個時候provider啓動的時候,就會加載到我們jar包裏的my=com.zhss.MyProtocol這行配置裏,接着會根據你的配置使用你定義好的MyProtocol了,這個就是簡單說明一下,你通過上述方式,可以替換掉大量的dubbo內部的組件,就是扔個你自己的jar包,然後配置一下即可。

dubbo裏面提供了大量的類似上面的擴展點,就是說,你如果要擴展一個東西,只要自己寫個jar,讓你的consumer或者是provider工程,依賴你的那個jar,在你的jar裏指定目錄下配置好接口名稱對應的文件,裏面通過key=實現類。

然後對對應的組件,用類似dubbo:protocol用你的哪個key對應的實現類來實現某個接口,你可以自己去擴展dubbo的各種功能,提供你自己的實現。

在這裏插入圖片描述
5、基於dubbo如何做服務治理、服務降級以及重試?
(1)服務治理
調用鏈路自動生成:需要基於dubbo做的分佈式系統中,對各個服務之間的調用自動記錄下來,然後自動將各個服務之間的依賴關係和調用鏈路生成出來,做成一張圖,顯示出來。
服務訪問壓力以及時長統計:需要自動統計各個接口和服務之間的調用次數以及訪問延時,而且要分成兩個級別。一個級別是接口粒度,就是每個服務的每個接口每天被調用多少次;第二個級別是從源頭入口開始,一個完整的請求鏈路經過幾十個服務之後,完成一次請求,每天全鏈路走多少次。
其他的治理:服務分層(避免循環依賴),調用鏈路失敗監控和報警,服務鑑權,每個服務的可用性的監控

(2)服務降級 —— dubbo提供了mock機制。
比如說服務A調用服務B,結果服務B掛掉了,服務A重試幾次調用服務B,還是不行,直接降級,走一個備用的邏輯,給用戶返回響應。
例如:

public interface HelloService {

   void sayHello();

}

public class HelloServiceImpl implements HelloService {

    public void sayHello() {
        System.out.println("hello world......");
    }
	
}

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:dubbo="http://code.alibabatech.com/schema/dubbo"
    xsi:schemaLocation="http://www.springframework.org/schema/beans        http://www.springframework.org/schema/beans/spring-beans.xsd        http://code.alibabatech.com/schema/dubbo        http://code.alibabatech.com/schema/dubbo/dubbo.xsd">

    <dubbo:application name="dubbo-provider" />
    <dubbo:registry address="zookeeper://127.0.0.1:2181" />
    <dubbo:protocol name="dubbo" port="20880" />
    <dubbo:service interface="com.zhss.service.HelloService" ref="helloServiceImpl" timeout="10000" />
    <bean id="helloServiceImpl" class="com.zhss.service.HelloServiceImpl" />

</beans>

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:dubbo="http://code.alibabatech.com/schema/dubbo"
    xsi:schemaLocation="http://www.springframework.org/schema/beans        http://www.springframework.org/schema/beans/spring-beans.xsd        http://code.alibabatech.com/schema/dubbo        http://code.alibabatech.com/schema/dubbo/dubbo.xsd">

    <dubbo:application name="dubbo-consumer"  />

    <dubbo:registry address="zookeeper://127.0.0.1:2181" />

    <dubbo:reference id="fooService" interface="com.test.service.FooService"  timeout="10000" check="false" mock="return null">
    </dubbo:reference>

</beans>

現在就是mock,如果調用失敗統一返回null

但是可以將mock修改爲true,然後在跟接口同一個路徑下實現一個Mock類,命名規則是接口名稱加Mock後綴。然後在Mock類裏實現自己的降級邏輯。

public class HelloServiceMock implements HelloService {
	public void sayHello() {
		// 降級邏輯
	}
}

(3)失敗重試和超時重試
所謂失敗重試,就是consumer調用provider要是失敗了,比如拋異常了,此時應該是可以重試的,或者調用超時了也可以重試。

<dubbo:reference id="xxxx" interface="xx" check="true" async="false" retries="3" timeout="2000"/>

某個服務的接口,要耗費5s,你這邊不能幹等着,你這邊配置了timeout之後,我等待2s,還沒返回,我直接就撤了,不能幹等着。
如果是超時了,timeout就會設置超時時間;如果是調用失敗了自動就會重試指定的次數。
你就結合你們公司的具體的場景來說說你是怎麼設置這些參數的,timeout,一般設置爲200ms,我們認爲不能超過200ms還沒返回。
retries,3次,設置retries,還一般是在讀請求的時候,比如你要查詢個數據,你可以設置個retries,如果第一次沒讀到,報錯,重試指定的次數,嘗試再次讀取2次。

6、分佈式系統中接口的冪等性該如何保證?比如不能重複扣款?
實際例子:訂單系統調用支付系統進行支付,結果不小心因爲網絡超時了,然後訂單系統走了前面我們看到的那個重試機制,咔嚓給你重試了一把,好,支付系統收到一個支付請求兩次,而且因爲負載均衡算法落在了不同的機器上,尷尬了。。。
網絡問題很常見,100次請求,都ok;1萬次,可能1次是超時會重試;10萬,10次;100萬,100次;如果有100個請求重複了,你沒處理,導致訂單扣款2次,100個訂單都扣錯了;

所謂冪等性,就是說一個接口,多次發起同一個請求,你這個接口得保證結果是準確的,比如不能多扣款,不能多插入一條數據,不能將統計值多加了1。這就是冪等性。

這個不是技術問題,這個沒有通用的一個方法,這個是結合業務來看應該如何保證冪等性的,你的經驗。其實保證冪等性主要是三點:

  • (1)對於每個請求必須有一個唯一的標識,舉個例子:訂單支付請求,肯定得包含訂單id,一個訂單id最多支付一次。
  • (2)每次處理完請求之後,必須有一個記錄標識這個請求處理過了,比如說常見的方案是在mysql中記錄個狀態等。
  • (3)每次接收請求需要進行判斷之前是否處理過的邏輯處理,比如說,如果有一個訂單已經支付了,就已經有了一條支付流水,那麼如果重複發送這個請求,則此時先插入支付流水,orderId已經存在了,唯一鍵約束生效,報錯插入不進去的。然後你就不用再扣款了。

上面只是給大家舉個例子,實際運作過程中,你要結合自己的業務來,比如說用redis用orderId作爲唯一鍵。只有成功插入這個支付流水,纔可以執行實際的支付扣款。

7、分佈式系統中的接口調用如何保證順序性?
其實分佈式系統接口的調用順序,也是個問題,一般來說是不用保證順序的。但是有的時候可能確實是需要嚴格的順序保證。給大家舉個例子,你服務A調用服務B,先插入再刪除。好,結果倆請求過去了,落在不同機器上,可能插入請求因爲某些原因執行慢了一些,導致刪除請求先執行了,此時因爲沒數據所以啥效果也沒有;結果這個時候插入請求過來了,好,數據插入進去了,那就錯了。

首先,一般來說,我個人給你的建議是,你們從業務邏輯上最好設計的這個系統不需要這種順序性的保證,因爲一旦引入順序性保障,會導致系統複雜度上升,而且會帶來效率低下,熱點數據壓力過大,等問題。

一個解決方案:
首先你得用dubbo的一致性hash負載均衡策略,將比如某一個訂單id對應的請求都給分發到某個機器上去,接着就是在那個機器上因爲可能還是多線程併發執行的,你可能得立即將某個訂單id對應的請求扔一個內存隊列裏去,強制排隊,這樣來確保他們的順序性

8、如何設計一個類似dubbo的rpc框架?架構上該如何考慮?
簡單的回答:
(1)上來你的服務就得去註冊中心註冊吧,你是不是得有個註冊中心,保留各個服務的信心,可以用zookeeper來做。
(2)然後你的消費者需要去註冊中心拿對應的服務信息吧,而且每個服務可能會存在於多臺機器上。
(3)接着你就該發起一次請求了,咋發起?蒙圈了是吧。當然是基於動態代理了,你面向接口獲取到一個動態代理,這個動態代理就是接口在本地的一個代理,然後這個代理會找到服務對應的機器地址。
(4)然後找哪個機器發送請求?那肯定得有個負載均衡算法了,比如最簡單的可以隨機輪詢是不是。
(5)接着找到一臺機器,就可以跟他發送請求了,第一個問題咋發送?你可以說用netty了,nio方式;第二個問題發送啥格式數據?你可以說用hessian序列化協議了,或者是別的,然後請求過去了。
(6)服務器那邊一樣的,需要針對你自己的服務生成一個動態代理,監聽某個網絡端口了,然後代理你本地的服務代碼。接收到請求的時候,就調用對應的服務代碼。

降級、治理、擴展等。

十五、分佈式session

集羣部署時的分佈式session如何實現?

方案(1)tomcat + redis
這個其實還挺方便的,就是使用session的代碼跟以前一樣,還是基於tomcat原生的session支持即可,然後就是用一個叫做Tomcat RedisSessionManager的東西,讓所有我們部署的tomcat都將session數據存儲到redis即可。在tomcat的配置文件中,配置一下:

<Valve className="com.orangefunction.tomcat.redissessions.RedisSessionHandlerValve" />

<Manager className="com.orangefunction.tomcat.redissessions.RedisSessionManager"
         host="{redis.host}"
         port="{redis.port}"
         database="{redis.dbnum}"
         maxInactiveInterval="60"/>

搞一個類似上面的配置即可,你看是不是就是用了RedisSessionManager,然後指定了redis的host和 port就ok了。

<Valve className="com.orangefunction.tomcat.redissessions.RedisSessionHandlerValve" />
<Manager className="com.orangefunction.tomcat.redissessions.RedisSessionManager"
	 sentinelMaster="mymaster"
	 sentinels="<sentinel1-ip>:26379,<sentinel2-ip>:26379,<sentinel3-ip>:26379"
	 maxInactiveInterval="60"/>

還可以用上面這種方式基於redis哨兵支持的redis高可用集羣來保存session數據,都是ok的

方案(2)spring session + redis
分佈式會話的這個東西重耦合在tomcat中,如果我要將web容器遷移成jetty,難道你重新把jetty都配置一遍嗎?
因爲上面那種tomcat + redis的方式好用,但是會嚴重依賴於web容器,不好將代碼移植到其他web容器上去,尤其是你要是換了技術棧咋整?比如換成了spring cloud或者是spring boot之類的。還得好好思忖思忖。
所以現在比較好的還是基於java一站式解決方案,spring了。人家spring基本上包掉了大部分的我們需要使用的框架了,spirng cloud做微服務了,spring boot做腳手架了,所以用sping session是一個很好的選擇。

1、pom.xml:
<dependency>
  <groupId>org.springframework.session</groupId>
  <artifactId>spring-session-data-redis</artifactId>
  <version>1.2.1.RELEASE</version>
</dependency>
<dependency>
  <groupId>redis.clients</groupId>
  <artifactId>jedis</artifactId>
  <version>2.8.1</version>
</dependency>

2、spring配置文件中:
<bean id="redisHttpSessionConfiguration"
     class="org.springframework.session.data.redis.config.annotation.web.http.RedisHttpSessionConfiguration">
    <property name="maxInactiveIntervalInSeconds" value="600"/>
</bean>

<bean id="jedisPoolConfig" class="redis.clients.jedis.JedisPoolConfig">
    <property name="maxTotal" value="100" />
    <property name="maxIdle" value="10" />
</bean>

<bean id="jedisConnectionFactory"
      class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory" destroy-method="destroy">
    <property name="hostName" value="${redis_hostname}"/>
    <property name="port" value="${redis_port}"/>
    <property name="password" value="${redis_pwd}" />
    <property name="timeout" value="3000"/>
    <property name="usePool" value="true"/>
    <property name="poolConfig" ref="jedisPoolConfig"/>
</bean>

web.xml

<filter>
    <filter-name>springSessionRepositoryFilter</filter-name>
    <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>
<filter-mapping>
    <filter-name>springSessionRepositoryFilter</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>

示例代碼:
@Controller
@RequestMapping("/test")
public class TestController {

@RequestMapping("/putIntoSession")
@ResponseBody
    public String putIntoSession(HttpServletRequest request, String username){
        request.getSession().setAttribute("name",  “leo”);

        return "ok";
    }

@RequestMapping("/getFromSession")
@ResponseBody
    public String getFromSession(HttpServletRequest request, Model model){
        String name = request.getSession().getAttribute("name");
        return name;
    }
}

上面的代碼就是ok的,給sping session配置基於redis來存儲session數據,然後配置了一個spring session的過濾器,這樣的話,session相關操作都會交給spring session來管了。接着在代碼中,就用原生的session操作,就是直接基於spring sesion從redis中獲取數據了。

實現分佈式的會話,有很多種很多種方式,我說的只不過比較常見的兩種方式,tomcat + redis早期比較常用;近些年,重耦合到tomcat中去,通過spring session來實現。

十六、高併發的系統架構

(1)系統拆分,將一個系統拆分爲多個子系統,用dubbo來搞。然後每個系統連一個數據庫,這樣本來就一個庫,現在多個數據庫,不也可以抗高併發麼。

(2)緩存,必須得用緩存。大部分的高併發場景,都是讀多寫少,那你完全可以在數據庫和緩存裏都寫一份,然後讀的時候大量走緩存不就得了。畢竟人家redis輕輕鬆鬆單機幾萬的併發啊。沒問題的。所以你可以考慮考慮你的項目裏,那些承載主要請求的讀場景,怎麼用緩存來抗高併發。

(3)MQ,必須得用MQ。可能你還是會出現高併發寫的場景,比如說一個業務操作裏要頻繁搞數據庫幾十次,增刪改增刪改,瘋了。那高併發絕對搞掛你的系統,你要是用redis來承載寫那肯定不行,人家是緩存,數據隨時就被LRU了,數據格式還無比簡單,沒有事務支持。所以該用mysql還得用mysql啊。那你咋辦?用MQ吧,大量的寫請求灌入MQ裏,排隊慢慢玩兒,後邊系統消費後慢慢寫,控制在mysql承載範圍之內。所以你得考慮考慮你的項目裏,那些承載複雜寫業務邏輯的場景裏,如何用MQ來異步寫,提升併發性。MQ單機抗幾萬併發也是ok的,這個之前還特意說過。

(4)分庫分表,可能到了最後數據庫層面還是免不了抗高併發的要求,好吧,那麼就將一個數據庫拆分爲多個庫,多個庫來抗更高的併發;然後將一個表拆分爲多個表,每個表的數據量保持少一點,提高sql跑的性能。

(5)讀寫分離,這個就是說大部分時候數據庫可能也是讀多寫少,沒必要所有請求都集中在一個庫上吧,可以搞個主從架構,主庫寫入,從庫讀取,搞一個讀寫分離。讀流量太多的時候,還可以加更多的從庫。

(6)Elasticsearch,可以考慮用es。es是分佈式的,可以隨便擴容,分佈式天然就可以支撐高併發,因爲動不動就可以擴容加機器來抗更高的併發。那麼一些比較簡單的查詢、統計類的操作,可以考慮用es來承載,還有一些全文搜索類的操作,也可以考慮用es來承載。

在這裏插入圖片描述

十七、分庫分表—讀寫分離

1、爲什麼要分庫分表?(設計高併發系統的時候,數據庫層面該如何設計?)
單表數據量太大,會極大影響你的sql執行的性能,到了後面你的sql可能就跑的很慢了。一般來說,就以我的經驗來看,單表到幾百萬的時候,性能就會相對差一些了,你就得分表了。

分表是啥意思?就是把一個表的數據放到多個表中,然後查詢的時候你就查一個表。比如按照用戶id來分表,將一個用戶的數據就放在一個表中。然後操作的時候你對一個用戶就操作那個表就好了。這樣可以控制每個表的數據量在可控的範圍內,比如每個表就固定在200萬以內。

分庫是啥意思?就是你一個庫一般我們經驗而言,最多支撐到併發2000,一定要擴容了,而且一個健康的單庫併發值你最好保持在每秒1000左右,不要太大。那麼你可以將一個庫的數據拆分到多個庫中,訪問的時候就訪問一個庫好了。

2、用過哪些分庫分表中間件?不同的分庫分表中間件都有什麼優點和缺點?
比較常見的包括:cobar、TDDL、atlas、sharding-jdbc、mycat。

  • cobar:阿里b2b團隊開發和開源的,屬於proxy層方案。早些年還可以用,但是最近幾年都沒更新了,基本沒啥人用,差不多算是被拋棄的狀態吧。而且不支持讀寫分離、存儲過程、跨庫join和分頁等操作。
  • TDDL:淘寶團隊開發的,屬於client層方案。不支持join、多表查詢等語法,就是基本的crud語法是ok,但是支持讀寫分離。目前使用的也不多,因爲還依賴淘寶的diamond配置管理系統。
  • atlas:360開源的,屬於proxy層方案,以前是有一些公司在用的,但是確實有一個很大的問題就是社區最新的維護都在5年前了。所以,現在用的公司基本也很少了。
  • sharding-jdbc:噹噹開源的,屬於client層方案。確實之前用的還比較多一些,因爲SQL語法支持也比較多,沒有太多限制,而且目前推出到了2.0版本,支持分庫分表、讀寫分離、分佈式id生成、柔性事務(最大努力送達型事務、TCC事務)。而且確實之前使用的公司會比較多一些(這個在官網有登記使用的公司,可以看到從2017年一直到現在,是不少公司在用的),目前社區也還一直在開發和維護,還算是比較活躍,個人認爲算是一個現在也可以選擇的方案。
  • mycat:基於cobar改造的,屬於proxy層方案,支持的功能非常完善,而且目前應該是非常火的而且不斷流行的數據庫中間件,社區很活躍,也有一些公司開始在用了。但是確實相比於sharding jdbc來說,年輕一些,經歷的錘鍊少一些。

所以綜上所述,現在其實建議考量的,就是sharding-jdbc和mycat,這兩個都可以去考慮使用。

sharding-jdbc這種client層方案的優點在於不用部署,運維成本低,不需要代理層的二次轉發請求,性能很高,但是如果遇到升級啥的需要各個系統都重新升級版本再發布,各個系統都需要耦合sharding-jdbc的依賴;

mycat這種proxy層方案的缺點在於需要部署,自己及運維一套中間件,運維成本高,但是好處在於對於各個項目是透明的,如果遇到升級之類的都是自己中間件那裏搞就行了。

通常來說,這兩個方案其實都可以選用,但是我個人建議中小型公司選用sharding-jdbc,client層方案輕便,而且維護成本低,不需要額外增派人手,而且中小型公司系統複雜度會低一些,項目也沒那麼多;

但是中大型公司最好還是選用mycat這類proxy層方案,因爲可能大公司系統和項目非常多,團隊很大,人員充足,那麼最好是專門弄個人來研究和維護mycat,然後大量項目直接透明使用即可。

3、你們具體是如何對數據庫如何進行垂直拆分或水平拆分的?
水平拆分的意思,就是把一個表的數據給弄到多個庫的多個表裏去,但是每個庫的表結構都一樣,只不過每個庫表放的數據是不同的,所有庫表的數據加起來就是全部數據。水平拆分的意義,就是將數據均勻放更多的庫裏,然後用多個庫來抗更高的併發,還有就是用多個庫的存儲容量來進行擴容。

垂直拆分的意思,就是把一個有很多字段的表給拆分成多個表,或者是多個庫上去。每個庫表的結構都不一樣,每個庫表都包含部分字段。一般來說,會將較少的訪問頻率很高的字段放到一個表裏去,然後將較多的訪問頻率很低的字段放到另外一個表裏去。因爲數據庫是有緩存的,你訪問頻率高的行字段越少,就可以在緩存裏緩存更多的行,性能就越好。這個一般在表層面做的較多一些。

4、如何把系統不停機遷移到分庫分表的?
(1)停機遷移方案

(2)雙寫遷移方案(常用方案,不用停機)
簡單來說,就是在線上系統裏面,之前所有寫庫的地方,增刪改操作,都除了對老庫增刪改,都加上對新庫的增刪改,這就是所謂雙寫,同時寫倆庫,老庫和新庫。

然後系統部署之後,新庫數據差太遠,用之前說的導數工具,跑起來讀老庫數據寫新庫,寫的時候要根據gmt_modified這類字段判斷這條數據最後修改的時間,除非是讀出來的數據在新庫裏沒有,或者是比新庫的數據新纔會寫。

接着導一輪之後,有可能數據還是存在不一致,那麼就程序自動做一輪校驗,比對新老庫每個表的每條數據,接着如果有不一樣的,就針對那些不一樣的,從老庫讀數據再次寫。反覆循環,直到兩個庫每個表的數據都完全一致爲止。

接着當數據完全一致了,就ok了,基於僅僅使用分庫分表的最新代碼,重新部署一次,不就僅僅基於分庫分表在操作了麼,還沒有幾個小時的停機時間,很穩。所以現在基本玩兒數據遷移之類的,都是這麼幹了。

5、如何設計可以動態擴容縮容的分庫分表方案?
(1)停機擴容

(2)優化後的方案
一開始上來就是32個庫,每個庫32個表,1024張表
我可以告訴各位同學說,這個分法,第一,基本上國內的互聯網肯定都是夠用了,第二,無論是併發支撐還是數據量支撐都沒問題。
每個庫正常承載的寫入併發量是1000,那麼32個庫就可以承載32 * 1000 = 32000的寫併發,如果每個庫承載1500的寫併發,32 * 1500 = 48000的寫併發,接近5萬/s的寫入併發,前面再加一個MQ,削峯,每秒寫入MQ 8萬條數據,每秒消費5萬條數據。

  • 設定好幾臺數據庫服務器,每臺服務器上幾個庫,每個庫多少個表,推薦是32庫 * 32表,對於大部分公司來說,可能幾年都夠了
  • 路由的規則,orderId 模 32 = 庫,orderId / 32 模 32 = 表
  • 擴容的時候,申請增加更多的數據庫服務器,裝好mysql,倍數擴容,4臺服務器,擴到8臺服務器,16臺服務器
  • 由dba負責將原先數據庫服務器的庫,遷移到新的數據庫服務器上去,很多工具,庫遷移,比較便捷
  • 我們這邊就是修改一下配置,調整遷移的庫所在數據庫服務器的地址
  • 重新發布系統,上線,原先的路由規則變都不用變,直接可以基於2倍的數據庫服務器的資源,繼續進行線上系統的提供服務
    在這裏插入圖片描述

6、分庫分表之後,id主鍵如何處理?
(1)數據庫自增id
這個方案的好處就是方便簡單,誰都會用;
缺點就是單庫生成自增id,要是高併發的話,就會有瓶頸的;如果你硬是要改進一下,那麼就專門開一個服務出來,這個服務每次就拿到當前id最大值,然後自己遞增幾個id,一次性返回一批id,然後再把當前最大id值修改成遞增幾個id之後的一個值;但是無論怎麼說都是基於單個數據庫。

適合的場景:**併發很低,數據量大。**你分庫分表就兩個原因,要不就是單庫併發太高,要不就是單庫數據量太大;除非是你併發不高,但是數據量太大導致的分庫分表擴容,你可以用這個方案,因爲可能每秒最高併發最多就幾百,那麼就走單獨的一個庫和表生成自增主鍵即可。

(2)uuid
好處就是本地生成,不要基於數據庫來了;不好之處就是,uuid太長了,作爲主鍵性能太差了,不適合用於主鍵。

適合的場景:如果你是要隨機生成個什麼文件名了,編號之類的,你可以用uuid,但是作爲主鍵是不能用uuid的。

UUID.randomUUID().toString().replace(“-”, “”) -> sfsdf23423rr234sfdaf

(3)獲取系統當前時間
這個就是獲取當前時間即可,但是問題是,併發很高的時候,比如一秒併發幾千,會有重複的情況,這個是肯定不合適的。基本就不用考慮了。

(4)snowflake算法
twitter開源的分佈式id生成算法,就是把一個64位的long型的id,1個bit是不用的,用其中的41 bit作爲毫秒數,用10 bit作爲工作機器id,12 bit作爲序列號。
1 bit:不用,爲啥呢?因爲二進制裏第一個bit爲如果是1,那麼都是負數,但是我們生成的id都是正數,所以第一個bit統一都是0

41 bit:表示的是時間戳,單位是毫秒。41 bit可以表示的數字多達2^41 - 1,也就是可以標識2 ^ 41 - 1個毫秒值,換算成年就是表示69年的時間。

10 bit:記錄工作機器id,代表的是這個服務最多可以部署在2^10臺機器上哪,也就是1024臺機器。但是10 bit裏5個bit代表機房id,5個bit代表機器id。意思就是最多代表2 ^ 5個機房(32個機房),每個機房裏可以代表2 ^ 5個機器(32臺機器)。

12 bit:這個是用來記錄同一個毫秒內產生的不同id,12 bit可以代表的最大正整數是2 ^ 12 - 1 = 4096,也就是說可以用這個12bit代表的數字來區分同一個毫秒內的4096個不同的id

64位的long型的id,64位的long -> 二進制

public class IdWorker{

    private long workerId;
    private long datacenterId;
    private long sequence;

    public IdWorker(long workerId, long datacenterId, long sequence){
        // sanity check for workerId
// 這兒不就檢查了一下,要求就是你傳遞進來的機房id和機器id不能超過32,不能小於0
        if (workerId > maxWorkerId || workerId < 0) {
            throw new IllegalArgumentException(String.format("worker Id can't be greater than %d or less than 0",maxWorkerId));
        }
        if (datacenterId > maxDatacenterId || datacenterId < 0) {
            throw new IllegalArgumentException(String.format("datacenter Id can't be greater than %d or less than 0",maxDatacenterId));
        }
        System.out.printf("worker starting. timestamp left shift %d, datacenter id bits %d, worker id bits %d, sequence bits %d, workerid %d",
                timestampLeftShift, datacenterIdBits, workerIdBits, sequenceBits, workerId);

        this.workerId = workerId;
        this.datacenterId = datacenterId;
        this.sequence = sequence;
    }

    private long twepoch = 1288834974657L;

    private long workerIdBits = 5L;
    private long datacenterIdBits = 5L;
    private long maxWorkerId = -1L ^ (-1L << workerIdBits); // 這個是二進制運算,就是5 bit最多隻能有31個數字,也就是說機器id最多隻能是32以內
    private long maxDatacenterId = -1L ^ (-1L << datacenterIdBits); // 這個是一個意思,就是5 bit最多隻能有31個數字,機房id最多隻能是32以內
    private long sequenceBits = 12L;

    private long workerIdShift = sequenceBits;
    private long datacenterIdShift = sequenceBits + workerIdBits;
    private long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits;
    private long sequenceMask = -1L ^ (-1L << sequenceBits);

    private long lastTimestamp = -1L;

    public long getWorkerId(){
        return workerId;
    }

    public long getDatacenterId(){
        return datacenterId;
    }

    public long getTimestamp(){
        return System.currentTimeMillis();
    }

public synchronized long nextId() {
// 這兒就是獲取當前時間戳,單位是毫秒
        long timestamp = timeGen();

        if (timestamp < lastTimestamp) {
            System.err.printf("clock is moving backwards.  Rejecting requests until %d.", lastTimestamp);
            throw new RuntimeException(String.format("Clock moved backwards.  Refusing to generate id for %d milliseconds",
                    lastTimestamp - timestamp));
        }

// 0
// 在同一個毫秒內,又發送了一個請求生成一個id,0 -> 1

        if (lastTimestamp == timestamp) {
            sequence = (sequence + 1) & sequenceMask; // 這個意思是說一個毫秒內最多隻能有4096個數字,無論你傳遞多少進來,這個位運算保證始終就是在4096這個範圍內,避免你自己傳遞個sequence超過了4096這個範圍
            if (sequence == 0) {
                timestamp = tilNextMillis(lastTimestamp);
            }
        } else {
            sequence = 0;
        }

// 這兒記錄一下最近一次生成id的時間戳,單位是毫秒
        lastTimestamp = timestamp;

// 這兒就是將時間戳左移,放到41 bit那兒;將機房id左移放到5 bit那兒;將機器id左移放到5 bit那兒;將序號放最後10 bit;最後拼接起來成一個64 bit的二進制數字,轉換成10進制就是個long型
        return ((timestamp - twepoch) << timestampLeftShift) |
                (datacenterId << datacenterIdShift) |
                (workerId << workerIdShift) |
                sequence;
    }

0 | 0001100 10100010 10111110 10001001 01011100 00 | 10001 | 1 1001 | 0000 00000000


    private long tilNextMillis(long lastTimestamp) {
        long timestamp = timeGen();
        while (timestamp <= lastTimestamp) {
            timestamp = timeGen();
        }
        return timestamp;
    }

    private long timeGen(){
        return System.currentTimeMillis();
    }

    //---------------測試---------------
    public static void main(String[] args) {
        IdWorker worker = new IdWorker(1,1,1);
        for (int i = 0; i < 30; i++) {
            System.out.println(worker.nextId());
        }
    }

}

怎麼說呢,大概這個意思吧,就是說41 bit,就是當前毫秒單位的一個時間戳,就這意思;然後5 bit是你傳遞進來的一個機房id(但是最大隻能是32以內),5 bit是你傳遞進來的機器id(但是最大隻能是32以內),剩下的那個10 bit序列號,就是如果跟你上次生成id的時間還在一個毫秒內,那麼會把順序給你累加,最多在4096個序號以內。

所以你自己利用這個工具類,自己搞一個服務,然後對每個機房的每個機器都初始化這麼一個東西,剛開始這個機房的這個機器的序號就是0。然後每次接收到一個請求,說這個機房的這個機器要生成一個id,你就找到對應的Worker,生成。

他這個算法生成的時候,會把當前毫秒放到41 bit中,然後5 bit是機房id,5 bit是機器id,接着就是判斷上一次生成id的時間如果跟這次不一樣,序號就自動從0開始;要是上次的時間跟現在還是在一個毫秒內,他就把seq累加1,就是自動生成一個毫秒的不同的序號。

這個算法那,可以確保說每個機房每個機器每一毫秒,最多生成4096個不重複的id。

利用這個snowflake算法,你可以開發自己公司的服務,甚至對於機房id和機器id,反正給你預留了5 bit + 5 bit,你換成別的有業務含義的東西也可以的。

這個snowflake算法相對來說還是比較靠譜的,所以你要真是搞分佈式id生成,如果是高併發啥的,那麼用這個應該性能比較好,一般每秒幾萬併發的場景,也足夠你用了。

7、MySQL讀寫分離的原理?主從同步延時咋解決?
(1)如何實現mysql的讀寫分離?
在這裏插入圖片描述
其實很簡單,就是基於主從複製架構,簡單來說,就搞一個主庫,掛多個從庫,然後我們就單單只是寫主庫,然後主庫會自動把數據給同步到從庫上去。

(2)MySQL主從複製原理的是啥?
在這裏插入圖片描述
主庫將變更寫binlog日誌,然後從庫連接到主庫之後,從庫有一個IO線程,將主庫的binlog日誌拷貝到自己本地,寫入一箇中繼日誌中。接着從庫中有一個SQL線程會從中繼日誌讀取binlog,然後執行binlog日誌中的內容,也就是在自己本地再次執行一遍SQL,這樣就可以保證自己跟主庫的數據是一樣的。

這裏有一個非常重要的一點,就是從庫同步主庫數據的過程是串行化的,也就是說主庫上並行的操作,在從庫上會串行執行。所以這就是一個非常重要的點了,由於從庫從主庫拷貝日誌以及串行執行SQL的特點,在高併發場景下,從庫的數據一定會比主庫慢一些,是有延時的。所以經常出現,剛寫入主庫的數據可能是讀不到的,要過幾十毫秒,甚至幾百毫秒才能讀取到。
而且這裏還有另外一個問題,就是
如果主庫突然宕機,然後恰好數據還沒同步到從庫,那麼有些數據可能在從庫上是沒有的,有些數據可能就丟失了。

所以mysql實際上在這一塊有兩個機制,一個是半同步複製,用來解決主庫數據丟失問題;一個是並行複製,用來解決主從同步延時問題。

這個所謂半同步複製,semi-sync複製,指的就是主庫寫入binlog日誌之後,就會將強制此時立即將數據同步到從庫,從庫將日誌寫入自己本地的relay log之後,接着會返回一個ack給主庫,主庫接收到至少一個從庫的ack之後纔會認爲寫操作完成了。

所謂並行複製,指的是從庫開啓多個線程,並行讀取relay log中不同庫的日誌,然後並行重放不同庫的日誌,這是庫級別的並行。

(3)mysql主從同步延時問題(精華)
線上確實處理過因爲主從同步延時問題,導致的線上的bug,小型的生產事故:

show status,Seconds_Behind_Master,你可以看到從庫複製主庫的數據落後了幾ms

其實這塊東西我們經常會碰到,就比如說用了mysql主從架構之後,可能會發現,剛寫入庫的數據結果沒查到,所以實際上你要考慮好應該在什麼場景下來用這個mysql主從同步,建議是一般在讀遠遠多於寫,而且讀的時候一般對數據時效性要求沒那麼高的時候,用mysql主從同步。所以這個時候,我們可以考慮的一個事情就是,你可以用mysql的並行複製,但是問題是那是庫級別的並行,所以有時候作用不是很大。所以這個時候。。通常來說,我們會對於那種寫了之後立馬就要保證可以查到的場景,採用強制讀主庫的方式,這樣就可以保證你肯定的可以讀到數據了吧。其實用一些數據庫中間件是沒問題的。

一般來說,如果主從延遲較爲嚴重,則需要進行:

  • 分庫,將一個主庫拆分爲4個主庫,每個主庫的寫併發就500/s,此時主從延遲可以忽略不計
  • 打開mysql支持的並行複製,多個庫並行複製,如果說某個庫的寫入併發就是特別高,單庫寫併發達到了2000/s,並行複製還是沒意義。28法則,很多時候比如說,就是少數的幾個訂單表,寫入了2000/s,其他幾十個表10/s。
  • 重寫代碼,寫代碼的同學,要慎重,當時我們其實短期是讓那個同學重寫了一下代碼,插入數據之後,直接就更新,不要查詢
  • 如果確實是存在必須先插入,立馬要求就查詢到,然後立馬就要反過來執行一些操作,對這個查詢設置直連主庫。不推薦這種方法,你這麼搞導致讀寫分離的意義就喪失了
    在這裏插入圖片描述

十八、高可用系統架構

hystrix是國外的netflix開源的,netflix是國外很大的視頻網站,系統非常複雜,微服務架構,多達幾千個服務,爲自己的場景,經過大量的工業驗證,線上生產環境的實踐,產出和開源了高可用相關的一個框架,熔斷框架,hystrix。
hystrix未來會成爲國內的高可用的限流、熔斷和降級這一塊的事實上的標準,spring cloud微服務框架,就是集成了hystrix來做微服務架構中的限流、降級和熔斷的。

限流:如何限流?在工作中是怎麼做的?說一下具體的實現?
熔斷:如何進行熔斷?熔斷框架都有哪些?具體實現原理知道嗎?
降級:如何進行降級?

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章