一、實現高性能數據庫集群
一般我們業務在讀多寫少的場景下,遇到的第一個瓶頸就是數據庫這塊,大量的讀請求會來到數據庫,這樣如果你初期部署的一個數據庫就會造成IO大量增加,使得請求變慢,甚至會卡死整個數據庫,到了這個階段,我們一般會將讀請求和寫請求進行分開數據處理,即采用主從讀寫分離的方式。
注:這里說的是主從并不是主備,主從中的從服務器是要承擔業務的,而主備中備份機器一般只作為備份存在。
我們采用主從讀寫分離的方式,目的是為了將更多的讀請求進行分發來緩解我們的大量讀請求。
讀寫分離架構原理
正如上面所說,讀寫分離是為了將請求流量分散到不同的數據庫節點上,將寫入數據的請求分發到主數據庫,讀取數據的請求分發到從數據庫,從數據可以有多臺,即一主多從。如下圖:
從上圖可看出,有個關鍵技術就是主從復制,每次寫入數據的時候,需要將主服務器數據復制到從服務器中,用來確保數據一致性。下面我們來單獨看看主從是怎么復制的,以我們互聯網中最熟悉的MySQL為例。
- 從服務器連接上主服務器,啟動復制的時候,則會自身創建一個IO線程去像主數據庫服務器拉取binlog的更新信息。
- 把拉過來的binlog信息寫到自己服務器的一個relay log日志文件中。
- 從數據庫服務器創建一個SQL線程,是為了將relay log的所有日志信息,進行sql回寫到自己的數據庫中,這樣就和主庫的數據一模一樣了。
- 當主數據庫有數據更新的時候,比如新插入了一條或者update了一條數據,這時候主庫會將這些數據更新到binlog二進制文件中,同時,主庫會創建一個binlog dump線程,這個線程將更新了的binlog信息發送到從庫的IO線程,需要注意的是,這個過程是異步的,如果等著從庫接受完成,是不是特別慢,且影響性能。
這樣的“一主多從”的主從復制方案做好之后,現在咱們就不怕當前這些大量的讀請求了,因為我們把這些大流量讀請求都分發到這些從數據庫中了,寫入數據的請求依然還是寫到主數據,一點不影響我們讀的業務,互不影響的。同時,從數據庫還可以作為備份數據庫來使用,萬一主庫突然故障了,它可以頂上去防止數據丟失。
但是,我們不能一味的加從數據庫,加個十七八個的,這樣做是無腦的操作啊,你想想看,你加一堆的的從數據庫連接到數據庫,復制他的數據,太多的IO線程會造成主數據不堪重負的,就會造成你寫入數據慢,還會卡死,這就悲劇了。所以,不能這么搞啊,一般生產環境一個主數據庫掛三個從數據就行了,最多不能超過5個以上,要是還是不滿足肯定就會有其他方案了啊,多級緩存方案啊,是不是,后面會講的。
主從延時
上面說了主從讀寫分離的那么多好處, 主從同步是有延遲的,當然,這個延時一般都在ms級別,但是如果到了秒級別,可能就會對有些業務造成影響,看我們能否接受。比如,我們在支付系統創建支付單后,風控系統會進入查詢作出相應的風控操作,如果查不到就會可能終止本次交易了。
主從延遲優化
- 我們可以在這些立馬需要查的業務,讓它直接查主數據庫,但是這種方案不推薦,因為量大怕會拖垮主數據
- 當從數據庫讀取不到,我們回去再讀主數據,這樣就能讀到數據了。但是,這種也是有風險的,量大也會拖垮主庫。
- 像我前面文章提到過,分重要性業務和非重要業務,將很核心的幾個業務查主數據,其他非核心,讀寫分離。
- 使用緩存,將新增的數據同時添加一份緩存,然后查緩存數據,這種建議新增的數據使用緩存較好。
- 使用消息隊列中間件進行消息冗余,將新的主數據內容,通過消息中間件MQ冗余一份當前數據,然后發到需要查詢的系統。
在消息體不大,推薦使用第 5 種優化方案,需要消息中間件;其次考慮緩存, redis,Memcache 都可以的,因為更新的操作,需要更新緩存,也需要解決一致性問題,所以,新增的數據,就首選緩存優化方案。最后,推薦重要性非重要性隔離方案。最好不要使用都查詢主庫的操作。
如何優雅使用讀寫分離
我們現在使用了數據庫讀寫分離的機制,但是我們代碼該怎么去友好的去訪問數據庫呢?以前我們一個數據源配置就可以了,現在有好多個數據源了,代碼里既要區分哪個地方使用寫數據庫的數據源,哪個地方需要使用讀數據的數據源。當然,肯定是有辦法的,業界大佬們都早于我們遇到了這些問題,下面我會分享出兩種方案:
1,程序代碼嵌入
代碼嵌入,是指通過在我們的代碼中開發出數據庫訪問中間層,由這個數據庫訪問中間層去訪問不同的數據源,以實現讀寫分離和數據源的管理。現在推薦使用淘寶開源的TDDL(Taobao Distributed Data Layer),使用方便,直接集成到我們代碼就可以了,它自己管理讀寫分離和數據庫配置。
特點是:
- 實現簡單,可以根據自己業務進行定制化開發
- 語言不同,就得開發不同語言版本的數據庫訪問層
2,部署獨立代理層
部署代理層是指,在我們的業務服務器和數據庫直接引入數據訪問代理層,并不用自己寫代碼。現在為代表的開源中間件有阿里的MyCat、360的Atlas、美團的DBProxy等。這些都是使用標準的MySql通信協議。
特點:
- 不用自己編寫多余代碼,使用方便。
- 支持對語言
- sql語句會跨兩層網絡,性能稍微低一點。
建議,在自己公司沒有比較成熟的中間件團隊的話就用程序代碼封裝的方案,雖然寫代碼麻煩點,但是自己可以把控;要是公司有成熟中間件團隊,就盡量考慮代理層部署的方案,因為需要有個團隊要研究和長期維護這個代理層,才能確保業務正常發展,現在我們公司大部分都用的是代理層方案,是有個專門團隊來維護。
總結,今天講到了當我們讀多寫少的場景下,采取數據庫讀寫分離的方式來分攤大流量。
二、大量并發寫入所帶來的性能問題優化
隨著運營部門的同事在不停的做出各種促銷或者拉新活動,我們注冊用戶越來越多,同時訂單量以及用戶行為數據等持續的增加,導致我們的系統現在出現了下面這些問題。
- 訂單量劇增,單表數據量已經達到了千萬的級別了,這個時候的索引查詢已經很慢了,所以現在我們的類似這些大數據表的查詢性能很差
- 數據量持續增加,現在我們的磁盤大部分空間都被使用,導致數據庫的復制備份操作很緩慢,所以,目前數據庫系統已不能滿足現在的數據量級。
- 我們整個系統的所有業務,訂單,用戶,優惠券、政策等等都在一個數據庫系統,耦合性太高,數據不隔離。
- 像每天大量的用戶關注、行為數據以及訂單數據的寫入,導致系統的寫入性能持續下降。
以上這些問題均是由于大并發的寫入操作導致目前的系統讀寫性能下降,并且系統可用性也在降低,這些都是現在階段需要解決的,需要將這些數據進行分片,也就是分散開,欄均攤我們整個數據庫的數據壓力,同時也是解決單機數據容量以及性能的解決方案,我們業界現在比較流行的方法就是“分庫分表”的方案。
提到分庫分表,大家應該都不陌生,但是怎么在自己項目中合理的使用分庫分表卻不是一件容易的事情,從今天開始,我們從分庫分表的策略到怎么部署上線以及怎么避坑等內容開始切入,幫助大家更好的掌握并使用分庫分表的技術
怎么做數據庫垂直拆分
垂直拆分是分庫分表方案中最為常見的一種方式,大的核心思想就是,將一堆的統一數據放到其他節點數據庫中或者表中進行存儲,不同于我們前面主從復制,主從復制是所有節點數據都是一樣,而垂直拆分后,是每個節點存儲一部分數據,就像Hadoop里面的一個個的Block一樣。
垂直拆分好處:
- 有效解決了單個數據庫或者表的數據存儲瓶頸。
- 有效提高數據查詢性能。
- 有效提高并發寫入性能,因為是可以寫到多個庫里面了。
我們公司的有個項目,你就理解我是用戶關注類的就行,這些數據已經到了億級別的了,之前是沒分庫分表的,到了億級別之后,不論是查詢還是寫入或者是報表之類的,反正就是各種痛苦,后來就是進行分庫分表,分到多個庫,才得以解決。
垂直拆分策略
我們現在知道了我們急需分庫分表的操作了,但是,我們該通過什么策略去將我們的數據庫進行垂直拆分呢?我這里建議是,我們最好按照我們的系統業務來進行垂直拆分,垂直拆分就是將數據庫豎著拆分,根據業務的不同將原有數據庫中的那些表分到不同的數據庫節點中。
不同的數據庫對應著不同的業務,比如,我們將用戶相關的表放在了用戶數據庫節點里,訂單相關的表放在了訂單數據庫節點上,關注相關的表放在了關注數據庫節點里。這樣還有個好處就是,我們的關注數據量龐大到了幾億的量級如果影響到了數據的操作,但是并不影響用戶的登錄和瀏覽、搜索下單操作。起到了很好的數據隔離的作用。
案例展示
以前,我們用戶表、關注表、訂單表都在同一個庫里面,也就是在我們的主庫中,后來經過拆分后,它們分別被拆到用戶庫、關注庫、訂單庫等。
垂直拆分這種方案,其實在我們中等規模的互聯網公司里都會首先使用到,但是,隨著數據的增加這種單庫里面的表還是會面臨瓶頸的,看上面我講的我們關注表數據,雖然拆到了關注庫里面,但是關注表數據都已經好幾億了,仍是會影響我們這塊業務的。所以說,垂直拆分只能暫緩我們的問題,但是,像那種單表數據驟增的情況還是需要采取另一種方法的,那就是我們下面要說的水平拆分。
怎么做數據庫水平拆分
水平拆分的核心思想是,將單一數據表數據按照我們約定的某種規則進行拆分到多個數據庫和數據表中,我們的關注點是在表數據本身上。
有哪些常用拆分規則
1,按照表中某一字段取哈希值進行拆分,例如我們的用戶表,如果將其拆分為16個庫,每個庫64張表,那么,我們就將UID哈希,為什么哈希大家知道吧,前面文章分析了hashmap,應該都懂吧。然后將哈希值對16取余,得到哪一個數據庫,然后對64取余就知道哪個表。這種規則比較適用于這種實體類的表。
2,按照某一個表字段的區間做拆分,這里最常使用就是日期字段了,比如我們關注表的創建時間,比如我們要查某個月的關注數據,就可以將月份數據放進對應月份表中,這樣我們就可以根據創建時間來定位到我們數據存儲在哪張表里面,然后在根據我們的查詢條件進行相應的查詢。
數據庫進行分庫分表后,我們代碼怎么去訪問,也會帶來一定的麻煩,之前只用訪問一個庫就行了,現在數據都被分到其他庫里面了,這個和我們前面的讀寫分離差不多,可以去看看()
現在數據庫的分庫分表解決了我們數據庫瓶頸、并發寫入和讀取等問題,也解決了我們擴展和數據隔離的問題,但是引入了分庫分表,也會給我們帶來一些問題:
怎么解決分庫分表帶來的問題
1,分區鍵
分區鍵就是我們用來進行分庫分表的字段,我們每次查詢的時候都必須得帶上這個字段,才能找到數據所在的庫和表,我們將上面用戶數據按照uid進行分庫分表的,那如果現在我想按照昵稱來查詢怎么辦呢?
- 這里我們一般建議,另外建立一張UID和昵稱的映射表。
- 然后通過昵稱查詢出UID
- 在通過UID就可以進行定位到庫和表了
2,多表JOIN
我們現在的單表數據都被分到多庫多表中了,然后有些程序員之前寫的那些連表join 操作怎么辦,跨庫了,就不能使用JOIN了。所以這塊我們需要將他們放到我們業務代碼中進行處理了,其實代碼中處理我們會更清晰一些
3,統計類
和上面JOIN類似,類似統計的count()就不能使用了,然后這塊我們建議是,通過另外建一張表活著放到redis緩存里面,最后再合并就行了,也很簡單。
最后建議,我們在我們的業務中不要一開始就去分庫分表,在沒必要的情況下不要去分庫分表;如果真的要分庫分表,在公司資源充足下,建議一次性分到位,會給你很爽的感覺,比如分16個庫64張表,資源緊缺的話,我們就根據業務來分,后續會將一些更過的方案。
總結,今天我們針對大并發的寫入造成的我們數據庫的瓶頸以及性能低下問題,我們就引入了分庫分表的方案,主要分為數據庫垂直拆分和水平拆分,也提到了拆分后給我們帶來了哪些挑戰并且給出相應的解決方案。
三、分庫分表后,保證ID全局唯一
上兩篇講到了我們的系統在面臨大并發讀取的時候,采用了讀寫分離主從復制(數據庫讀寫分離方案,實現高性能數據庫集群)的方案去應對,后來又面臨了大并發寫入的時候,系統數據庫采用了分庫分表的方案(數據庫分庫分表方案,優化大量并發寫入所帶來的性能問題),通過垂直拆分以及水平拆分的方式,將數據分到多個庫和多個表中去應對的,即現在是這樣的一套分布式存儲結構。
數據庫分庫分表那篇也講到了,使用了分庫分表勢必會帶來和我們之前使用不大相同的問題。今天,我將其中一個和我們開發息息相關的問題提出來進行講解,也就是我們開發中所使用的的主鍵的問題。我們知道,以前我們單庫的時候,主鍵唯一ID是自增的,現在好了,我們的數據被分到多個庫的多個表里面了,如果我們還是使用之前的主鍵自增策略,那么這樣就會出現兩個數據插入到了兩個不同的表會出現相同的ID值,這時我們該怎么去使用呢?
對于什么是主鍵,主鍵該怎么選,今天不做講解,我相信大家可能比我還精通,我們今天主要是講唯一主鍵ID在分布式存儲系統下怎么生成,保證ID的唯一性且符合我們業務需要,才是我們開發人員最關心的實戰。
UUID
這個時候,你可能會說,自增用不了,那我就是用UUID嘛,這個UUID生成出來的就是唯一的。的確,在我以前在一個公司中的確接觸到是使用UUID來生成唯一主鍵ID的,而且性能還可以。但是,我想提一點的就是,當這個ID和我們業務交集不相關的時候是可以使用UUID生成主鍵的。比如,一般我們業務是需要用來做查詢的,而且最好是單調遞增的,這樣我們的UUID就很不適合了。
主鍵ID單調遞增有什么好處呢?
1,就拿我們用戶關注航班這個模塊來說,我們查看某個航班關注用戶按照時間的先后進行排序。因為現在的ID是時間上有序的,所以現在我們就可以按照ID來進行排序了,同時這樣對于有些并不是要存儲時間的業務來說,會減少不少的存儲空間。
2,有序的ID可以提升數據寫入的性能
我們知道主鍵其實在數據庫中就是一種索引,而索引在MySql數據庫的B+數據結構中是順序存儲的,所以每次插入的時候就是遞增排序的,直接追加到后面就行。如果是無序的話,則每次插入數據之前還得查找它應該所在的位置,這無疑就會增加數據的異動等相關的開銷,如下圖:
如上圖所示,如果我們生成的ID是有序的,那這個 50 就直接插在尾部就行了,如果是無序的話,突然生成了一個 26,我們還得先找到 26 需要存放的位置,然后還要對其后面數據進行挪位置。
3,UUID不具備業務相關性
我們現在開發的項目都是依據公司業務開展的,而我們的唯一ID一般都是和業務有關系的,比如,有些訂單ID中帶上了時間的維度、機房的維度以及業務類型等維度。也就是為了我方便進行定位是那種業務的訂單,才會這么設計的,是不是。
而UUID是由32位的16進制數字組成的字符串,不僅在存儲空間上造成浪費,更不具備我們業務相關性。那我們該怎么解決呢?其實twitter提出來的Snowflake 算法就能很好滿足我們現在的要求,滿足了主鍵ID的全局唯一性、單調遞增性,也可以滿足我們的業務相關。所以,我們現在使用的唯一ID生成方式就是使用Snowflake算法,這個算法其實很簡單。下面我們來對其進行講解,并對其相應改造使其能用到我們的開發業務中來。
Snowflake 算法原理
Snowflake 是由 64 比特bit二進制數字組成的,一共分為4大部分:
- 1位默認不使用
- 41位時間戳
- 10位機器ID
- 12位序列號
- 我們從上圖中可以看出snowflake算法的第二部分的41位時間戳,大概可以支撐2^41/1000/60/60/24/365 年,也就是大約有69年。我們設計一個系統用69年應該是足夠了吧。
- 10位的機器ID我們可以怎么使用呢?我們可以劃分成大概2到3位IDC,也就是可以支撐4到8個IDC機房;然后劃分7到 8 位的機器ID,即可以支撐128~256臺機器。
- 12位的序號,就代表每個節點每毫秒可以生成4096個ID序號。
如何改造
我們現在已經知道了Snowflake 算法的核心原理,并且知道了其有64位的二進制數據,那我們就可以根據自己業務進行改造以更好的來為我們業務服務。一般不同的公司對其進行改造的方式都不盡相同,但是道理都是一樣的。我們可以這么做:
- 我們是減少序列號的位數,增加機器ID的位數,是為了用來支撐我們單IDC的更多機器。
- 將我們業務ID加入進去用來區分我們不同的業務。比如,1位0 + 41位時間戳 + 6位IDC(64個IDC) + 6位業務信息(支撐64種業務) + 10位自增序列(每毫秒1024個ID)
如此,我們就可以在單機房部署這么一個統一ID發號器,然后用Keeplive 保證高可用(對于高可用不熟悉的回去看看哈「高可用」你們服務器掛了怎么辦,我們是這樣做的)可以將不同的業務模塊ID加入進去,這樣的好處是即使哪個業務出問題了,我只看ID號我就分析出來,比如,我看到現在ID號有我的訂單ID業務,我就去看訂單模塊。
開發如何使用
現在我們知道Snowflake 算法原理了,還知道了我們可以進行改造了。那我們開發人員該怎么去使用,來為我們業務生成統一的唯一ID呢?
1,直接嵌入到業務代碼
嵌入業務代碼的意思就是,這個snowflake算法就部署在和我們業務相同的服務器上,這樣我們代碼使用的時候,就不用了跨網絡調用,性能相對比較好。但是也是有缺點的,因為我們的業務機器肯定是很多的,這就意味著我們發號器算法需要更多的機器ID位數。同時,太多的業務服務器我們會很難保證業務機器id的唯一性,這里就需要引用zookeeper一致性組件來保證每次機器重啟都能能獲得唯一的機器ID。
2,獨立部署成發號器服務
也就是說,我們將其作為單獨的服務部署到單獨的機器上,已對外提供服務。這樣就是多了網絡的傳輸,不過影響不大,比如,我可以將其部署成一個主備的方式對外提供發號服務,機器ID可以用作序列號使用,這樣也就是會有更多的自增序號,有部分大廠就是以這樣單獨的服務提供出來的。
開發中避坑大法
1,雖然snowflake很優秀,但是它是基于系統時間的,萬一我們系統的時間不準怎么辦,就會造成我們的ID會重復。那我們的做法就是,要利用系統的對時功能,一旦發現時間不一致,就暫停發號器,等到時鐘準了在啟用。
2,還有一個坑比較關鍵,也是常發生的,就是當我們的QPS并發不高的時候,比如每毫秒只生成一個ID號,這樣就是直接結果是,每次生成的ID末尾都是1,這樣我們分庫分表就會出現問題呀對吧,因為我們用這個ID去分庫分表呀,會造成數據不均勻,是吧,忘記了去復習哈(數據庫分庫分表方案,優化大量并發寫入所帶來的性能問題)那我們怎么解決呢?
我們可以將時間戳記錄從毫秒記錄改為秒記錄,這樣我一秒可以發好多個號了
生成的序列號起始號隨機啟動,比如這一秒起始號是10,我下一秒隨機了變成了28,這樣就更加分散開了。
總結,今天我們針對分庫分表之后帶來的第一個直接影響我們開發的問題,就是主鍵ID唯一性的問題,然后說到了使用Snowflake算法去解決,并且對其原理和使用進行了詳細的講解,同時,還將其在使用中遇到的坑給講出來了,也對其進行了填坑分析,讓大家直接避免遇到同樣的問題。當然生成唯一ID有多種,我們根據業務選擇合適我們自己的就好






