1. 程式人生 > >資料庫資料定期同步實現

資料庫資料定期同步實現

實現這個需求,首先想到的是直接通過sql 進行同步,表之間資料同步無非是三種操作:更新,刪除,插入,假設兩個表 dst,src,dst中有id,name,auth三個欄位,src中有id,name,dsc三個欄位,需要將src 中的id,name同步到dst中去,如圖所示:
在dst和src中都存在的資料,只需要按照src中的資料,批量更新dst中的資料即可,sql語句可能是這樣:

update dst, src set dst.id=src.id,dst.name=src.name where src.id=dst.id;
1
更新完成之後,需要向dst表中插入在src存在,而dst中不存在的資料,簡單的sql可能是這樣:

insert dst (id,name,auth) select src.id,src.name,’1′ from src where not exists(select dst.id from dst where src.id=dst.id);
1
再接著是刪除,將dst中存在src中不存在的資料稱從dst中刪除,sql如下:

delete from dst where not exists(select src.id from src where src.id=dst.id );
1
注意:如果是用的mysql上述同步刪除語句中的dst表明不能簡寫

有了這三條語句,寫個定時任務,依次執行即可,如果是用的spring,通常會用@Scheduled具體使用可以google一下,非常方便。如果需要事務可以使用@Transactional,這些是spring通過aop整合好的,可以宣告式使用,但是提供的粒度不夠靈活,使用也會有些限制,如果想更靈活點話,可以使用程式設計式事務。而如果資料庫開啟了autocommit功能,其本身就會有事務,不需要邏輯程式碼中再加事務(不是絕對的,當然要看自己需要,要記住autocommit只保證每一條sql是一個事務)。如果要看看自己的資料庫是否開啟了autocommit,可以用下面的sql:

show variables like ‘autocommit’
1
這種方式的特點就是思路很簡單。不需要寫太多的java程式碼(定時任務都可以直接用spring封裝好的註解,只需要寫個類,寫個函式,如果使用orm的話,然後實現mybatis或hibernate相關的dao和service)。而這種實現的問題也很明顯,就是你對整個同步過程可控的東西很有限,最多通過事務保證如果同步失敗了,整體回滾。而且當同步邏輯比較複雜的時候,比如說表中欄位比較多,而且同步部分欄位,同步的欄位需要join其他表才能決定需不需要同步,這些邏輯全部寫在sql中會導致sql很臃腫,而且更容易出錯,更嚴重的是出錯了你卻什麼都做不了,也不知道具體哪裡同步出錯了。腫麼辦?那就一條一條的來唄。

要想對同步過程擁有足夠的控制,就只能將需要同步的資料全部load到記憶體,然後通過寫程式進行遍歷。具體過程應該是這樣的:
1. 將src中的資料全部load到記憶體中,如果資料量比較大(通常都是這樣,記憶體一次性放不下),就進行分頁load,sql語句如下:

select * from src limit ‘pageSize’ offset ‘offset’
1
其中’pageSize’通過程式設定,而’offset’就是pageSize*pageNo,比如說每頁100條資料,取第一頁的資料就是select * from src limit 100 offset 100*1

對取出來的資料進行迴圈遍歷,java程式簡寫如下:

private void syncData(){
Date curTime = new Date();
List<MyData> datas = myDataDao.getDatas(pageNo,pageSize);//訪問的是src表
for(MyData data : datas){
if(needToUpdate(data))//src表中的資料在dst中已經存在就update,否則就insert
update(data);
else insert(data);
}
deleteDatasFromNow(curTime); //刪除當前同步時間之前的資料
}
1
2
3
4
5
6
7
8
9
10
那麼needToUpdate做的就是判斷一下data是否在dst中存在,這裡需要一個唯一標識來確定當前data,通常是一個欄位或幾個聯合確定唯一的data。所以needToUpdate可能如下:

private boolean needToUpdate(MyData data){
Optional<MyData> myData = myDataDao.findByName(data.getName);//這裡訪問的是dst表
if(myData.isPresent()){
return true;
}
else return false;
}
1
2
3
4
5
6
7
update和insert都會改變dst中的updateTime,所以在刪除的時候就可以通過updateTime是否晚於curTime來判斷當前資料是否更新過或新插入的,如果不是,那就是需要刪除的資料,所以deleteDatasFromNow()如下:

private void deleteDatasFromNow(Date curTime){
List<MyData> datasNeedToDel = myDataDao.getDatasNeedToDel(curTime);
for(MyData data : datasNeedToDel){
delete(data);
}
}
1
2
3
4
5
6
這樣整個同步過程就完成了,如果想要列印同步日誌或將同步過程記錄下來,就可以在update(),insert(),delete()中插入日誌操作就行了,就拿update()來說的話,可能像下面的情況:

private void update(MyData data){
try{
myDataDao.update(data);
}
catch(Exception e){
logger.error(“資料”+data.getName()+”同步出錯”);//這裡是列印日誌,如果需要也可以儲存到資料庫
return;
}
logger.info(“資料”+data.getName()+”同步成功”);
}
1
2
3
4
5
6
7
8
9
10
通過日誌檔案分析(如果將操作儲存到資料庫的話也可以直接查詢資料庫),可以清晰的知道哪些資料進行了更新,哪些資料是新插入的,哪些是刪除了的,然後還可以進行統計,共更新了多少資料,多少成功了,多少失敗了。業務層面就可以瞭解更多有關資料同步的資訊。而且這種操作使sql非常簡單,也不太容易出錯。但是一個很明顯的問題也暴露出來了,那就是效率問題,如果通過這種方式,勢必要遍歷每一條資料,對需要update,insert,delete的資料需要一個一個地進行訪問資料庫,而且對於needToUpdate(MyData data)中也額外訪問了一次資料庫,親測這個效率真是低驚人。而且這種實現中會導致,即便什麼資料都不更新,也會完全遍歷一遍資料,訪問同樣多的資料庫,所需的時間還是那麼久,這是在業務層面無法容忍的。於是就有了思路三。

既然上述方法的主要問題就是訪問資料庫過多導致效率底下,那麼就必須儘可能減少資料庫的訪問和遍歷的次數。那就需要緊貼業務需求,針對具體的需求進行改善。我遇到的需求就是每次資料同步的過程中,大部分資料都是不變的,只有少部分新增和刪除,針對這個需求進行了下面的優化:
* 不再每次將所有的src中的資料取出來,而是將需要刪除的和需要更新的,以及需要新插入的分別取出來,這樣資料取來之後就可以直接進行update,insert,delete了,不需要再進行額外的比較判斷的了
* 大部分資料不變,因此進行update的時候不再單條進行更新,直接進行分頁批量更新,比如說每次更新100條或者更多,以提高更新的效率。
通過這些優化大副提高了同步的效率,其中需要注意的是當寫稍微複雜點的sql的時候一定要注意,雖然都能得到相同的查詢結果,可能效率相差十萬八千里,在實現這個資料同步是,就因為join位置放的不對,導致查詢需要插入的資料時灰常慢,所以好好學學sql優化還是很有必要的,網上有很多大牛介紹,可以多看看,或者用的時候再學(我通常是這樣。。。)