話說數據庫主從複製,讀寫分離,數據一致性

一致性:
1.強一致性
這種一致性級別是最符合用戶直覺的,它要求系統寫入什麼,讀出來的也會是什麼,用戶體驗好,但實現起來往往對系統的性能影響大
2.弱一致性
這種一致性級別約束了系統在寫入成功後,不承諾立即可以讀到寫入的值,也不久承諾多久之後數據能夠達到一致,但會盡可能地保證到某個時間級別(比如秒級別)後,數據能夠達到一致狀態
3.最終一致性
最終一致性是弱一致性的一個特例,系統會保證在一定時間內,能夠達到一個數據一致的狀態。這裏之所以將最終一致性單獨提出來,是因爲它是弱一致性中非常推崇的一種一致性模型,也是業界在大型分佈式系統的數據一致性上比較推崇的模型

數據庫爲什麼要讀寫分離?

  1. 現在很多大型互聯網業務,往往讀多寫少,那麼數據庫的讀會首先成爲數據庫的瓶頸,我們希望提升數據庫的讀性能。消除讀寫鎖衝突從而提升數據庫的寫性能,那麼讀寫分離架構。主從只負責各自的寫和讀,極大程度的緩解X鎖和S鎖爭用。
    解釋:排它鎖(X鎖)和共享鎖(S鎖)。
    X鎖,是事務T對數據A加上X鎖時,只允許事務T讀取和修改數據A
    S鎖,是事務T對數據A加上S鎖時,其他事務只能再對數據A加S鎖,而不能加X鎖,直到T釋放A上的S鎖
    若事務T對數據對象A加了S鎖,則T就可以對A進行讀取,但不能進行更新(S鎖因此又稱爲讀鎖),在T釋放A上的S鎖以前,其他事務可以再對A加S鎖,但不能加X鎖,從而可以讀取A,但不能更新A。
  2. 假如我們的從庫是多臺的,可以分攤讀取。假如原來每分鐘150條讀數據,分攤給3臺服務器,每臺服務器也就處理50條,同時增加冗餘,提高可用性,當一臺數據庫服務器宕機後能通過調整另外一臺從庫來以最快的速度恢復服務,保證業務正常運行。

這裏以Mysql爲例:

怎麼做到主從複製?

master配置:

配置my.cnf文件

#在[mysqld]中添加:
server-id=1                      #server-id 服務器唯一標識
log_bin=master-bin               #啓動MySQL二進制日誌,即數據同步語句。
log_bin_index=master-bin.index
binlog_do_db=hxg                 #指定記錄二進制日誌的數據庫,即需要複製的數據庫名

創建從服務器的用戶和權限

mysql> grant replication slave on *.* to masterbackup@'你的從主機地址' identified by '從庫連接密碼';
#如果有多個用正則匹配 地址。

配置完後,重啓一下。可以用show master status查看一下。

slave配置:

#在[mysqld]中添加:
server-id=2                    #server-id 服務器唯一標識,如果有多個從服務器,每個服務器的server-id不能重複,跟IP一樣是唯一標識
relay-log=slave-relay-bin      #relay-log 啓動MySQL二進制日誌,可以用來做數據備份和崩潰恢復,或主服務器掛掉了,將此從服務器作爲其他從服務器的主服務器。
relay-log-index=slave-relay-bin.index
#replicate-do-db=hxg           #replicate-do-db 指定同步的數據庫,如果複製多個數據庫,重複設置這個選項即可。若在master端不指定binlog-do-db,則在slave端可用replication-do-db來過濾。

同樣需要重新啓動。
連接master主服務器

mysql> change master to master_host='主機IP',master_port=3306,master_user='masterbackup',master_password='連接密碼',master_log_file='File',master_log_pos=Position;
#master_log_file、master_log_pos分別對應show master status顯示的File列和Position列。

怎麼讀寫分離呢?:

基於中間代理層實現:代理一般位於客戶端和服務器之間,代理服務器接到客戶段的請求通過判斷後轉發到後端數據庫。
使用Mysql-proxy實現mysql的讀寫分離,Mysql-proxy實際上是作爲後端mysql主從服務器的代理,它直接接受客戶端的請求,對SQL語句進行分析,判斷出是讀操作還是寫操作,然後分發至對應的Mysql服務器上。
  Mysql-proxy是官方提供的Mysql中間件產品可以實現負載平衡,讀寫分離,failover等
  MySQL Proxy就是這麼一箇中間層代理,簡單的說,MySQL Proxy就是一個連接池,負責將前臺應用的連接請求轉發給後臺的數據庫,並且通過使用lua腳本,可以實現複雜的連接控制和過濾,
  從而實現讀寫分離和負載平衡。對於應用來說,MySQL Proxy是完全透明的,應用則只需要連接到MySQL Proxy的監聽端口即可。
  當然,這樣Proxy機器可能成爲單點失效,但完全可以使用多個proxy機器做爲冗餘,在應用服務器的連接池配置中配置到多 個proxy的連接參數即可。
 
在這裏插入圖片描述
從上圖很清晰的看到,主庫提供寫,多個從庫提供讀。

主從同步的詳細步驟:

在這裏插入圖片描述
mysql主從複製需要三個線程,master(binlog dump thread)、slave(I/O thread 、SQL thread)。
master:
binlog dump線程:當主庫中有數據更新時,那麼主庫就會根據按照設置的binlog格式,將此次更新的事件類型寫入到主庫的binlog文件中,此時主庫會創建log dump線程通知slave有數據更新,當I/O線程請求日誌內容時,會將此時的binlog名稱和當前更新的位置同時傳給slave的I/O線程。
slave:

  1. I/O線程:該線程會連接到master,向log dump線程請求一份指定binlog文件位置的副本,並將請求回來的binlog存到本地的relay log(中繼日誌)中,relay log和binlog日誌一樣也是記錄了數據更新的事件,它也是按照遞增後綴名的方式,產生多個relay log( host_name-relay-bin.000001)文件,slave會使用一個index文件( host_name-relay-bin.index)來追蹤當前正在使用的relay log文件。
  2. SQL線程:該線程檢測到relay log有更新後,會讀取並在本地做redo操作,將發生在主庫的事件在本地重新執行一遍,來保證主從數據同步。此外,如果一個relay log文件中的全部事件都執行完畢,那麼SQL線程會自動將該relay log 文件刪除掉

同步方式

異步複製

  1. MySQL默認的複製即是異步的,主庫在執行完客戶端提交的事務後會立即將結果返給給客戶端,並不關心從庫是否已經接收並處理,這樣就會有一個問題,主如果crash掉了,此時主上已經提交的事務可能並沒有傳到從庫上,如果此時,強行將從提升爲主,可能導致新主上的數據不完整
  2. 主庫將事務 Binlog 事件寫入到 Binlog 文件中,此時主庫只會通知一下 Dump 線程發送這些新的 Binlog,然後主庫就會繼續處理提交操作,而此時不會保證這些 Binlog 傳到任何一個從庫節點上。

全局同步複製

  1. 指當主庫執行完一個事務,所有的從庫都執行了該事務才返回給客戶端。因爲需要等待所有從庫執行完該事務才能返回,所以全同步複製的性能必然會收到嚴重的影響。
  2. 當主庫提交事務之後,所有的從庫節點必須收到、APPLY並且提交這些事務,然後主庫線程才能繼續做後續操作。但缺點是,主庫完成一個事務的時間會被拉長,性能降低。

半同步複製

  1. 是介於全同步複製與全異步複製之間的一種,主庫只需要等待至少一個從庫節點收到並且 Flush Binlog 到 Relay Log 文件即可,主庫不需要等待所有從庫給主庫反饋。同時,這裏只是一個收到的反饋,而不是已經完全完成並且提交的反饋,如此,節省了很多時間。
  2. 介於異步複製和全同步複製之間,主庫在執行完客戶端提交的事務後不是立刻返回給客戶端,而是等待至少一個從庫接收到並寫到relay log中才返回給客戶端。相對於異步複製,半同步複製提高了數據的安全性,同時它也造成了一定程度的延遲,這個延遲最少是一個TCP/IP往返的時間。所以,半同步複製最好在低延時的網絡中使用。

主從同步延遲問題

mysql的主從複製都是單線程的操作,主庫對所有DDL和DML產生binlog,binlog是順序寫,所以效率很高,slave的I/O線程到主庫取日誌,效率也比較高,但是,slave的SQL線程將主庫的DDL和DML操作在slave實施。DML和DDL的IO操作是隨即的,不是順序的,成本高很多,還可能存在slave上的其他查詢產生lock爭用的情況,由於SQL也是單線程的,所以一個DDL卡住了,需要執行很長一段事件,後續的DDL線程會等待這個DDL執行完畢之後才執行,這就導致了延時。當主庫的TPS(每秒事務量:(Com_commit + Com_rollback)/Uptime)併發較高時,產生的DDL數量超過slave一個sql線程所能承受的範圍,延時就產生了,除此之外,還有可能與slave的大型query語句產生了鎖等待導致。
解決方案

  1. 業務的持久化層的實現採用分庫架構,mysql服務可平行擴展,分散壓力。
  2. 服務的基礎架構在業務和mysql之間加入memcache或者Redis的cache層。降低mysql的讀壓力。
  3. 使用比主庫更好的硬件設備作爲slave。

怎麼保證數據一致性?

一致性指分佈式服務化系統之間的弱一致性,包括應用系統一致性和數據一致性。
無論是水平拆分還是垂直拆分,都解決了特定場景下的特定問題,凡事有好的一面,都會有壞的一面,拆分後的系統或者服務化的系統最大的問題就是一致性問題,這麼多個具有元功能的模塊,或者同一個功能池中的多個節點之間,如何保證他們的信息是一致的、工作步伐是一致的、狀態是一致的、互相協調有序的工作呢?
想想以下經典案例:

  1. 轉賬:扣除自己成功,別人增加失敗,那麼你就損失這筆錢。扣除自己失敗,增加別人成功,銀行就損失這筆錢。
  2. 下訂單和扣庫存: 下訂單和庫存,先下單,後扣庫存,那麼會導致超賣。如果下單成功,扣庫存成功,那麼會導致少賣。
  3. 同步超時:服務化的系統間調用常常因爲網絡問題導致系統間調用超時,系統A同步調用系統B超時,系統A可以明確得到超時反饋,但是無法確定系統B是否已經完成了預定的功能或者沒有完成預定的功能。於是,系統A就迷茫了,不知道應該繼續做什麼,如何反饋給使用方。
  4. 異步回調超時:統A同步調用系統B發起指令,系統B採用受理模式,受理後則返回受理成功,然後系統B異步通知系統A。在這個過程中,如果系統A由於某種原因遲遲沒有收到回調結果,那麼兩個系統間的狀態就不一致,互相認知不同會導致系統間發生錯誤,嚴重情況下會影響核心事務,甚至會導致資金損失。
  5. 掉單:分佈式系統中,兩個系統協作處理一個流程,分別爲對方的上下游,如果一個系統中存在一個請求,通常指訂單,另外一個系統不存在,則導致掉單,掉單的後果很嚴重,有時候也會導致資金損失。
  6. 系統間狀態不一致:兩個系統間都存在請求,但是請求的狀態不一致。
  7. 緩存和數據庫不一致:一些特殊的場景對讀的性能要求極高,服務於交易的數據庫難以抗住大規模的讀流量,通常需要在數據庫前墊緩存,那麼緩存和數據庫之間的數據如何保持一致性?是要保持強一致呢還是弱一致性呢?
  8. 緩存數據結構不一致:某系統需要種某一數據結構的緩存,這一數據結構有多個數據元素組成,其中,某個數據元素都需要從數據庫中或者服務中獲取,如果一部分數據元素獲取失敗,由於程序處理不正確,仍然將不完全的數據結構存入緩存,那麼緩存的消費者消費的時候很有可能因爲沒有合理處理異常情況而出錯。
  9. 本地緩存節點間不一致:一個服務池上的多個節點爲了滿足較高的性能需求,需要使用本地緩存,使用了本地緩存,每個節點都會有一份緩存數據的拷貝,如果這些數據是靜態的、不變的,那永遠都不會有問題,但是如果這些數據是半靜態的或者常被更新的,當被更新的時候,各個節點更新是有先後順序的,在更新的瞬間,各個節點的數據是不一致的,如果這些數據是爲某一個開關服務的,想象一下重複的請求走進了不同的節點。一個請求走了開關打開的邏輯,同時另外一個請求走了開關關閉的邏輯,這導致請求被處理兩次,最壞的情況下會導致災難性的後果,就是資金損失。
    酸鹼平衡理論(ACID在英文中的意思是“酸”)
    ACID
    A: Atomicity    原子性
    C: Consistency  一致性
    I: Isolation    隔離性
    D: Durability   持久性
    具有ACID的特性的數據庫支持強一致性,強一致性代表數據庫本身不會出現不一致,每個事務是原子的,或者成功或者失敗,事物間是隔離的,互相完全不影響,而且最終狀態是持久落盤的。
    CAP(帽子理論)
    C:Consistency,一致性, 數據一致更新,所有數據變動都是同步的
    A:Availability,可用性, 好的響應性能,完全的可用性指的是在任何故障模型下,服務都會在有限的時間處理響應
    P:Partition tolerance,分區容錯性,可靠性
    任何分佈式系統只可同時滿足二點,沒法三者兼顧。關係型數據庫由於關係型數據庫是單節點的,因此,不具有分區容錯性,但是具有一致性和可用性。
    BASE理論(鹼):
    BASE理論解決CAP理論提出了分佈式系統的一致性和可用性不能兼得的問題。滿足CAP理論,通過犧牲強一致性,獲得可用性,一般應用在服務化系統的應用層或者大數據處理系統,通過達到最終一致性
    BA:Basically Available,基本可用
    S:Soft State,軟狀態,狀態可以有一段時間不同步
    E:Eventually Consistent,最終一致,最終數據是一致的就可以了,而不是時時保持強一致
    以轉賬爲例:
    我們把用戶A給用戶B轉賬分成四個階段
    第一個階段用戶A準備轉賬
    第二個階段從用戶A賬戶扣減餘額
    第三個階段對用戶B增加餘額
    第四個階段完成轉賬。
    系統需要記錄操作過程中每一步驟的狀態,一旦系統出現故障,系統能夠自動發現沒有完成的任務,然後,根據任務所處的狀態,繼續執行任務,最終完成任務,達到一致的最終狀態。
    上面過程是通過持久化執行任務的狀態和環境信息,一旦出現問題,定時任務會撈取未執行完的任務,繼續未執行完的任務,直到執行完成爲止,或者取消已經完成的部分操作回到原始狀態。
    下面介紹一下保持數據一致性的方案:
    一、 semi-sync(半同步複製)
    之所以會讀取到舊數據,關鍵在於主從同步需要一個時間段,而讀取請求可能剛好就發生在同步階段。爲了讀取到最新的數據,需要等主從同步完成之後,主庫上的寫請求再返回 。
    在這裏插入圖片描述
    (1)系統先對DB-master進行了一個寫操作,寫主庫;
    (2)等主從同步完成,寫主庫的請求才返回;
    (3)讀從庫,讀到最新的數據(如果讀請求先完成,寫請求後完成,讀取到的是“當時”最新的數據)。
    顯然帶來的後果就是主庫的寫請求時延會增加,吞吐量會降低。
    二、 利用中間件
    藉助中間件的路由作用,對服務層的讀寫請求進行分發,從而避免出現不一致問題。
    在這裏插入圖片描述
    (1)所有的讀寫都走數據庫中間件,通常情況下,寫請求路由到主庫,讀請求路由到從庫。
    (2)記錄所有路由到主庫的key,在經驗主從同步時間窗口內(假設是500ms),如果有讀請求訪問中間件,此時有可能從庫還是舊數據,就把這個key上的讀請求路由到主庫。
    (3)經驗主從同步時間過完後,對應key的讀請求繼續路由到從庫。
    中間件帶來的好處就是能保證數據的絕對一致性,但同時也帶來成本上升的問題。
    三、利用緩存
    在這裏插入圖片描述
    (1)將某個庫上的某個key要發生寫操作,記錄在cache裏,並設置“經驗主從同步時間”的cache超時時間。
    (2)修改數據庫。
    而讀取請求發生的時候:
    在這裏插入圖片描述
    從圖中可以看出:
    (1)先到cache裏查看,對應庫的對應key有沒有相關數據;
    (2)如果cache hit,有相關數據,說明這個key上剛發生過寫操作,此時需要將請求路由到主庫讀最新的數據;
    (3)如果cache miss,說明這個key上近期沒有發生過寫操作,此時將請求路由到從庫,繼續讀寫分離。
    顯然,利用緩存,減少了中間件帶來的成本問題,但多了一個Cache組件,並且讀寫數據庫多了一步Cache操作,操作相對其他稍較繁瑣。
    其實用緩存策略,也存在很多問題。

緩存和數據庫一致性:

先更新數據庫,再更新緩存
存在線程安全問題,同時如果數據更新頻繁,讀行爲還沒到,緩存更新頻繁存在性能浪費。 同時如果寫高併發場景,壓力則直接壓到了DB上。
同時有請求A和請求B進行更新操作,那麼會出現
(1)線程A更新了數據庫
(2)線程B更新了數據庫
(3)線程B更新了緩存
(4)線程A更新了緩存
先刪緩存,再更新數據庫
(1)請求A進行寫操作,刪除緩存
(2)請求B查詢發現緩存不存在
(3)請求B去數據庫查詢得到舊值
(4)請求B將舊值寫入緩存
(5)請求A將新值寫入數據庫
解決方案:採用延時雙刪策略。刪兩次。
(1)先淘汰緩存
(2)再寫數據庫(這兩步和原來一樣)
(3)休眠1秒,再次淘汰緩存
這麼做,可以將1秒內所造成的緩存髒數據,再次刪除。
先更新數據庫,再刪緩存
(1)緩存剛好失效
(2)請求A查詢數據庫,得一箇舊值
(3)請求B將新值寫入數據庫
(4)請求B刪除緩存
(5)請求A將查到的舊值寫入緩存
發生上述情況有一個先天性條件,就是步驟(3)的寫數據庫操作比步驟(2)的讀數據庫操作耗時更短,纔有可能使得步驟(4)先於步驟(5)。可是,大家想想,數據庫的讀操作的速度遠快於寫操作的(不然做讀寫分離幹嘛,做讀寫分離的意義就是因爲讀操作比較快,耗資源少),因此步驟(3)耗時比步驟(2)更短,這一情形很難出現。
首先,給緩存設有效時間是一種方案。其次,採用策略(2)裏給出的異步延時刪除策略,保證讀請求完成以後,再進行刪除操作。
還有如果,刪除緩存失敗了怎麼辦?也會導致數據不一致。
那麼我們需要一個重試機制
方案一:
在這裏插入圖片描述
(1)更新數據庫數據
(2)緩存因爲種種問題刪除失敗
(3)將需要刪除的key發送至消息隊列
(4)自己消費消息,獲得需要刪除的key
(5)繼續重試刪除操作,直到成功
你會發現,,該方案有一個缺點,對業務線代碼造成大量的侵入。於是有了方案二,在方案二中,啓動一個訂閱程序去訂閱數據庫的binlog,獲得需要操作的數據。在應用程序中,另起一段程序,獲得這個訂閱程序傳來的信息,進行刪除緩存操作。
方案二:
在這裏插入圖片描述
(1)更新數據庫數據
(2)數據庫會將操作信息寫入binlog日誌當中
(3)訂閱程序提取出所需要的數據以及key
(4)另起一段非業務代碼,獲得該信息
(5)嘗試刪除緩存操作,發現刪除失敗
(6)將這些信息發送至消息隊列
(7)重新從消息隊列中獲得該數據,重試操作。
上述的訂閱binlog程序在mysql中有現成的中間件叫canal。

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