字節(jié)終面:為什么在系統(tǒng)中不推薦雙寫?
圖片來(lái)自 包圖網(wǎng)
某日,阿雄跑去面試!于是有如下情形:
- 面試官:"阿雄是吧,做做自我介紹!"
- 阿雄:"我叫阿雄,來(lái)自某 a 國(guó)際電商公司!"
- 面試官:"我看你項(xiàng)目里用了 Elasticsearch,你是怎么同步數(shù)據(jù)的呢?"
- 阿雄:"在代碼里寫入數(shù)據(jù)庫(kù)的時(shí)候,同時(shí)再寫入 Elasticsearch!"
- 面試官:"那你如何保證寫入數(shù)據(jù)庫(kù),和寫入 Elasticsearch 原子性問題呢?萬(wàn)一寫入數(shù)據(jù)庫(kù)成功了,寫入 Elasticsearch 失敗了怎么處理?"
- 阿雄:"我還是回去等通知吧!"
其實(shí)這篇文章所探討的數(shù)據(jù)同步策略并不限于某兩種固定的存儲(chǔ)系統(tǒng)之間,而想去探討一種通用的數(shù)據(jù)同步策略。
主要分為以下三個(gè)部分:
- 背景介紹
- 雙寫缺點(diǎn)
- 改良方案
背景介紹
話說阿雄在加入某 a 國(guó)際電商公司的時(shí)候,業(yè)務(wù)系統(tǒng)十分簡(jiǎn)單,一個(gè) DataBase 就能搞定一切!
可是某 a 國(guó)際電商公司在產(chǎn)品韓的領(lǐng)導(dǎo)下,業(yè)務(wù)增長(zhǎng)迅速,阿雄發(fā)現(xiàn)了數(shù)據(jù)庫(kù)越來(lái)越慢,于是乎阿雄加入了一些緩存,如 Redis 來(lái)緩存一些數(shù)據(jù),提高系統(tǒng)的響應(yīng)能力。
又過了一段時(shí)間,產(chǎn)品韓發(fā)現(xiàn)搜索的速度灰常慢,讓阿雄去改。阿雄在網(wǎng)上發(fā)現(xiàn),現(xiàn)在業(yè)內(nèi)都用一些 Elasticsearch 做一些全文檢索的操作,于是乎阿雄將一些需要全文檢索的數(shù)據(jù)放入 Elasticsearch,提高了系統(tǒng)的搜索能力!
隨著數(shù)據(jù)的膨脹,阿雄慢慢的發(fā)現(xiàn)了,對(duì)數(shù)據(jù)庫(kù)做一些數(shù)據(jù)分析操作,性能明顯的跟不上了。于是乎阿雄將數(shù)據(jù)庫(kù)里的數(shù)據(jù),導(dǎo)入 Hadoop,然后進(jìn)行數(shù)據(jù)分析。
省略一萬(wàn)字….最后,阿雄和產(chǎn)品韓幸福的在一起了。OK,好,現(xiàn)在分析上面的場(chǎng)景!思考第一個(gè)問題。
①在 DataBase,Redis,Elasticsearch,Hadoop 中的數(shù)據(jù)是有關(guān)系的,還是彼此獨(dú)立的?
顯然是有關(guān)系的,在這幾個(gè)數(shù)據(jù)源中的數(shù)據(jù)都是相關(guān)的。只是格式不一樣而已!
例如,對(duì)于一條 Product 數(shù)據(jù),在數(shù)據(jù)庫(kù)里是:
在 Redis 里就是 key 為 product:pId:1,value 是:
- { "pId": "1",
- "productName": "macbook"
- }
如上所示,只是數(shù)據(jù)格式不一樣而已!那好,現(xiàn)在思考第二個(gè)問題。
②既然這些數(shù)據(jù)源之間數(shù)據(jù)是相關(guān)的,如何保證這幾個(gè)數(shù)據(jù)源之間數(shù)據(jù)一致性?
一種比較簡(jiǎn)單且容易想到的方案是,hardcode 在程序中。例如現(xiàn)在有兩個(gè)數(shù)據(jù)源 DataSouce1 和 DataSource2,我們往里頭寫數(shù)據(jù)。
代碼如下:
- ProductService{
- \\省略
- public void syncData(){
- x1. writeDataSource1();
- x2. writeDataSource2();
- }
- }
這就是我們標(biāo)題中所提到的雙寫!那么,雙寫會(huì)帶來(lái)什么壞處呢?OK,繼續(xù)往下看!
雙寫缺點(diǎn)
①一致性問題
打個(gè)比方我們現(xiàn)在有兩個(gè) client,同時(shí)往兩個(gè) DataSouce 寫數(shù)據(jù):
- 一個(gè) client 往里頭入 X 為 1
- 一個(gè) client 往里頭入 X 為 5
那么會(huì)有如下情形出現(xiàn):
如圖所示,兩個(gè) DataSouce 的數(shù)據(jù)就不一致了,一個(gè)為 1,一個(gè)為 5。除非接下來(lái)有一個(gè)新的請(qǐng)求,對(duì) x 數(shù)據(jù)發(fā)生了變更,才能修正這種現(xiàn)象!否則,你可能永遠(yuǎn)都發(fā)現(xiàn)不了。
②原子性問題
因?yàn)槲覀冃枰瑫r(shí)往 DataSource1 和 DataSource2 一起寫數(shù)據(jù),你需要保證:
- x1. writeDataSource1();
- x2. writeDataSource2();
這兩個(gè)操作一起成功,或者一起失敗!如果采用雙寫的方法,是避不開這個(gè)問題的!
那么有沒有通用的辦法來(lái)解決這些問題呢?有的,只要能按順序記錄數(shù)據(jù)的變更即可!那具體怎么做呢,我們繼續(xù)往下看!
改良方案
假設(shè),如果我們能將數(shù)據(jù)按順序記錄,寫入某個(gè)消息隊(duì)列,然后其他系統(tǒng)按消息順序恢復(fù)數(shù)據(jù),看看 what happen?
此時(shí)架構(gòu)圖如下:
在該架構(gòu)下,所有的數(shù)據(jù)變更寫入一個(gè)消息隊(duì)列里去。其他各數(shù)據(jù)源從消息隊(duì)列里恢復(fù)數(shù)據(jù)即可!
那么,此時(shí)還有一致性問題,和原子性問題么?
①一致性問題
OK,這種情況下,各個(gè)數(shù)據(jù)源之間數(shù)據(jù)肯定是一致的。因?yàn)閷懭腠樞蛞呀?jīng)在消息隊(duì)列中定義好,各數(shù)據(jù)源按照消息隊(duì)列中的消息順序,恢復(fù)數(shù)據(jù)即可,并不存在競(jìng)爭(zhēng)現(xiàn)象。因此,不會(huì)出現(xiàn)不一致的問題!
②原子性問題
OK,這種情況下,如果寫入 DataSource 失敗會(huì)怎么樣?例如出現(xiàn)了網(wǎng)絡(luò)問題,這條消息恢復(fù)失敗了。
這個(gè)問題其實(shí)好解決,一般我們?cè)陧樞蚋鶕?jù)消息恢復(fù)數(shù)據(jù)的時(shí)候,會(huì)記錄下坐標(biāo)。如果寫入失敗,停止恢復(fù)數(shù)據(jù)。下次從該坐標(biāo)處恢復(fù)數(shù)據(jù)即可。
但是在上面那張圖中,寫入 DataBase 是異步寫入的。這樣就不符合很多業(yè)務(wù)場(chǎng)景的"寫后即讀"的要求,因此,在實(shí)際落地中,做了一些變更!通用做法是去提取數(shù)據(jù)庫(kù)的變化!
如下圖所示:
在該圖中的中間件,例如 Oracle 中的 oracle golden gate 可以提取數(shù)據(jù)變化。
MySQL 中的 Canal 能提取數(shù)據(jù)的變化。至于消息隊(duì)列,可以選用 Kafka。直接提取數(shù)據(jù)變化到 Kafka 中,其他數(shù)據(jù)源從 Kafka 中獲取數(shù)據(jù),避免了直接雙寫從而導(dǎo)致一致性和原子性問題。
總結(jié)
本文討論了在項(xiàng)目中常見的數(shù)據(jù)同步問題,希望大家有所收獲。
作者:孤獨(dú)煙
編輯:陶家龍
出處:轉(zhuǎn)載自公眾號(hào)孤獨(dú)煙(ID:zrj_guduyan)