自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

如何做到不停機(jī)分庫(kù)分表遷移?

運(yùn)維 數(shù)據(jù)庫(kù)運(yùn)維
隨著業(yè)務(wù)的發(fā)展,單表容量超過(guò)千萬(wàn)甚至達(dá)到億級(jí)別以上,這時(shí)候就需要考慮分庫(kù)分表這個(gè)問(wèn)題了,而不停機(jī)分庫(kù)分表遷移,這應(yīng)該是分庫(kù)分表最基本的需求,畢竟互聯(lián)網(wǎng)項(xiàng)目不可能掛個(gè)廣告牌"今晚10:00~次日10:00系統(tǒng)停機(jī)維護(hù)",這得多l(xiāng)ow呀,以后跳槽面試,你跟面試官說(shuō)這個(gè)遷移方案,面試官怎么想呀?

[[222412]]

需求說(shuō)明

類似訂單表,用戶表這種未來(lái)規(guī)模上億甚至上十億百億的海量數(shù)據(jù)表,在項(xiàng)目初期為了快速上線,一般只是單表設(shè)計(jì),不需要考慮分庫(kù)分表。隨著業(yè)務(wù)的發(fā)展,單表容量超過(guò)千萬(wàn)甚至達(dá)到億級(jí)別以上,這時(shí)候就需要考慮分庫(kù)分表這個(gè)問(wèn)題了,而不停機(jī)分庫(kù)分表遷移,這應(yīng)該是分庫(kù)分表最基本的需求,畢竟互聯(lián)網(wǎng)項(xiàng)目不可能掛個(gè)廣告牌"今晚10:00~次日10:00系統(tǒng)停機(jī)維護(hù)",這得多l(xiāng)ow呀,以后跳槽面試,你跟面試官說(shuō)這個(gè)遷移方案,面試官怎么想呀?

借鑒codis

筆者正好曾經(jīng)碰到過(guò)這個(gè)問(wèn)題,并借鑒了codis一些思想實(shí)現(xiàn)了不停機(jī)分庫(kù)分表遷移方案;codis不是這篇文章的重點(diǎn),這里只提及借鑒codis的地方--rebalance:

當(dāng)遷移過(guò)程中發(fā)生數(shù)據(jù)訪問(wèn)時(shí),Proxy會(huì)發(fā)送“SLOTSMGRTTAGSLOT”遷移命令給Redis,強(qiáng)制將客戶端要訪問(wèn)的Key立刻遷移,然后再處理客戶端的請(qǐng)求。( SLOTSMGRTTAGSLOT 是codis基于redis定制的)

分庫(kù)分表

明白這個(gè)方案后,了解不停機(jī)分庫(kù)分表遷移就比較容易了,接下來(lái)詳細(xì)介紹筆者當(dāng)初對(duì)installed_app表的實(shí)施方案;即用戶已安裝的APP信息表;

1. 確定sharding column

確定sharding column絕對(duì)是分庫(kù)分表最最最重要的環(huán)節(jié),沒(méi)有之一。sharding column直接決定整個(gè)分庫(kù)分表方案最終是否能成功落地;一個(gè)合適的sharding column的選取,基本上能讓與這個(gè)表相關(guān)的絕大部分流量接口都能通過(guò)這個(gè)sharding column訪問(wèn)分庫(kù)分表后的單表,而不需要跨庫(kù)跨表,最常見(jiàn)的sharding column就是user_id,筆記這里選取的也是user_id;

2. 分庫(kù)分表方案

根據(jù)自身的業(yè)務(wù)選取最合適的sharding column后,就要確定分庫(kù)分表方案了。筆者采用主動(dòng)遷移與被動(dòng)遷移相結(jié)合的方案:

  1. 主動(dòng)遷移就是一個(gè)獨(dú)立程序,遍歷需要分庫(kù)分表的installed_app表,將數(shù)據(jù)遷移到分庫(kù)分表后的目標(biāo)表中。
  2. 被動(dòng)遷移就是與installed_app表相關(guān)的業(yè)務(wù)代碼自身將數(shù)據(jù)遷移到分庫(kù)分表后對(duì)應(yīng)的表中。

接下來(lái)詳細(xì)介紹這兩個(gè)方案;

2.1 主動(dòng)遷移

主動(dòng)遷移就是一個(gè)獨(dú)立的外掛遷移程序,其作用是遍歷需要分庫(kù)分表的installed_app表,將這里的數(shù)據(jù)復(fù)制到分庫(kù)分表后的目標(biāo)表中,由于主動(dòng)遷移和被動(dòng)遷移會(huì)一起運(yùn)行,所以需要處理主動(dòng)遷移和被動(dòng)遷移碰撞的問(wèn)題,筆者的主動(dòng)遷移偽代碼如下:

 

  1. public void migrate(){  
  2.     // 查詢出當(dāng)前表的***ID, 用于判斷是否遷移完成  
  3.     long maxId = execute("select max(id) from installed_app");  
  4.     long tempMinId = 0L;  
  5.     long stepSize = 1000;  
  6.     long tempMaxId = 0L;  
  7.     do{  
  8.         try {  
  9.             tempMaxId = tempMinId + stepSize;  
  10.             // 根據(jù)InnoDB索引特性, where id>=? and id<?這種SQL性能***  
  11.             String scanSql = "select * from installed_app where id>=#{tempMinId} and id<#{tempMaxId}" 
  12.             List<InstalledApp> installedApps = executeSql(scanSql);  
  13.             Iterator<InstalledApp> iterator = installedApps.iterator();  
  14.             while (iterator.hasNext()) {  
  15.                 InstalledApp installedApp = iterator.next();  
  16.                 // help GC  
  17.                 iterator.remove();  
  18.                 long userId = installedApp.getUserId();  
  19.                 String status = executeRedis("get MigrateStatus:${userId}");  
  20.                 if ("COMPLETED".equals(status)) {  
  21.                     // migration finish, nothing to do  
  22.                     continue 
  23.                 }  
  24.                 if ("MIGRATING".equals(status)) {  
  25.                     // "被動(dòng)遷移" migrating, nothing to do  
  26.                     continue 
  27.                 }  
  28.                 // 遷移前先獲取鎖: set MigrateStatus:18 MIGRATING ex 3600 nx  
  29.                 String result = executeRedis("set MigrateStatus:${userId} MIGRATING ex 86400 nx");  
  30.                 if ("OK".equals(result)) {  
  31.                     // 成功獲取鎖后, 先將這個(gè)用戶所有已安裝的app查詢出來(lái)[即遷移過(guò)程以用戶ID維度進(jìn)行遷移]  
  32.                     String sql = "select * from installed_app where user_id=#{user_id}" 
  33.                     List<InstalledApp> userInstalledApps = executeSql(sql);  
  34.                     // 將這個(gè)用戶所有已安裝的app遷移到分庫(kù)分表后的表中(有user_id就能得到分庫(kù)分表后的具體的表)  
  35.                     shardingInsertSql(userInstalledApps);  
  36.                     // 遷移完成后, 修改緩存狀態(tài)  
  37.                     executeRedis("setex MigrateStatus:${userId} 864000 COMPLETED");  
  38.                 } else {  
  39.                     // 如果沒(méi)有獲取到鎖, 說(shuō)明被動(dòng)遷移已經(jīng)拿到了鎖, 那么遷移交給被動(dòng)遷移即可[這種概率很低]  
  40.                     // 也可以加強(qiáng)這里的邏輯, "被動(dòng)遷移"過(guò)程不可能持續(xù)很長(zhǎng)時(shí)間, 可以嘗試循環(huán)幾次獲取狀態(tài)判斷是否遷移完  
  41.                     logger.info("Migration conflict. userId = {}", userId);  
  42.                 }  
  43.             }  
  44.             if (tempMaxId >= maxId) {  
  45.                 // 更新max(id),最終確認(rèn)是否遍歷完成  
  46.                 maxId = execute("select max(id) from installed_app");  
  47.             }  
  48.             logger.info("Migration process id = {}", tempMaxId);  
  49.         }catch (Throwable e){  
  50.             // 如果執(zhí)行過(guò)程中有任何異常(這種異常只可能是redis和mysql拋出來(lái)的), 那么退出, 修復(fù)問(wèn)題后再遷移  
  51.             // 并且將tempMinId的值置為logger.info("Migration process id="+tempMaxId);日志***一次記錄的id, 防止重復(fù)遷移 
  52.             System.exit(0);  
  53.         }  
  54.         tempMinId += stepSize;  
  55.     }while (tempMaxId < maxId);  

 

這里有幾點(diǎn)需要注意:

  1. ***步查詢出max(id)是為了盡量減少max(id)的查詢次數(shù),假如***次查詢max(id)為10000000,那么直到遍歷的id到10000000以前,都不需要再次查詢max(id);
  2. 根據(jù)id>=? and id<?遍歷,而不要根據(jù)id>=? limit n或者limit m, n進(jìn)行遍歷,因?yàn)閘imit性能一般,且會(huì)隨著遍歷越往后,性能越差。而id>=? and id<?這種遍歷方式即使會(huì)有一些踩空,也沒(méi)有任何影響,且整個(gè)性能曲線非常平順,不會(huì)有任何抖動(dòng);遷移程序畢竟是輔助程序,不能對(duì)業(yè)務(wù)程序有過(guò)多的影響;
  3. 根據(jù)id區(qū)間范圍查詢出來(lái)的List<InstalledApp>要轉(zhuǎn)換為Iterator<InstalledApp>,每迭代處理完一個(gè)userId,要remove掉,否則可能導(dǎo)致GC異常,甚至OOM;

2.2 被動(dòng)遷移

被動(dòng)遷移就是在正常與installed_app表相關(guān)的業(yè)務(wù)邏輯前插入了遷移邏輯,以新增用戶已安裝APP為例,其偽代碼如下:

 

  1. // 被動(dòng)遷移方法是公用邏輯,所以與`installed_app`表相關(guān)的業(yè)務(wù)邏輯前都需要調(diào)用這個(gè)方法;  
  2. public void migratePassive(long userId)throws Exception{  
  3.     String status = executeRedis("get MigrateStatus:${userId}");  
  4.     if ("COMPLETED".equals(status)) {  
  5.         // 該用戶數(shù)據(jù)已經(jīng)遷移完成, nothing to do  
  6.         logger.info("user's installed app migration completed. user_id = {}", userId);  
  7.     }else if ("MIGRATING".equals(status)) {  
  8.         // "被動(dòng)遷移" migrating, 等待直到遷移完成; 為了防止死循環(huán), 可以增加***等待時(shí)間邏輯  
  9.         do{  
  10.             Thread.sleep(10);  
  11.             status = executeRedis("get MigrateStatus:${userId}");  
  12.         }while ("COMPLETED".equals(status)); 
  13.     }else {  
  14.         // 準(zhǔn)備遷移  
  15.         String result = executeRedis("set MigrateStatus:${userId} MIGRATING ex 86400 nx");  
  16.         if ("OK".equals(result)) {  
  17.             // 成功獲取鎖后, 先將這個(gè)用戶所有已安裝的app查詢出來(lái)[即遷移過(guò)程以用戶ID維度進(jìn)行遷移]  
  18.             String sql = "select * from installed_app where user_id=#{user_id}" 
  19.             List<InstalledApp> userInstalledApps = executeSql(sql);  
  20.             // 將這個(gè)用戶所有已安裝的app遷移到分庫(kù)分表后的表中(有user_id就能得到分庫(kù)分表后的具體的表)  
  21.             shardingInsertSql(userInstalledApps);  
  22.             // 遷移完成后, 修改緩存狀態(tài)  
  23.             executeRedis("setex MigrateStatus:${userId} 864000 COMPLETED");  
  24.         }else {  
  25.             // 如果沒(méi)有獲取到鎖, 應(yīng)該是其他地方先獲取到了鎖并正在遷移, 可以嘗試等待, 直到遷移完成  
  26.         }  
  27.     }  
  28.  
  29. // 與`installed_app`表相關(guān)的業(yè)務(wù)--新增用戶已安裝的APP  
  30. public void addInstalledApp(InstalledApp installedApp) throws Exception{  
  31.     // 先嘗試被動(dòng)遷移  
  32.     migratePassive(installedApp.getUserId());  
  33.     // 將用戶已安裝app信息(installedApp)插入到分庫(kù)分表后的目標(biāo)表中  
  34.     shardingInsertSql(installedApp);  

 

無(wú)論是CRUD中哪種操作,先根據(jù)緩存中MigrateStatus:${userId}的值進(jìn)行判斷:

  1. 如果值為COMPLETED,表示已經(jīng)遷移完成,那么將請(qǐng)求轉(zhuǎn)移到分庫(kù)分表后的表中進(jìn)行處理即可;
  2. 如果值為MIGRATING,表示正在遷移中,可以循環(huán)等待直到值為COMPLETED即遷移完成后,再將請(qǐng)求轉(zhuǎn)移到分庫(kù)分表后的表中進(jìn)行處理處理;
  3. 否則值為空,那么嘗試獲取鎖再進(jìn)行數(shù)據(jù)遷移。遷移完成后,將緩存值更新為COMPLETED,***再將請(qǐng)求轉(zhuǎn)移到分庫(kù)分表后的表中進(jìn)行處理處理;

3.方案完善

當(dāng)所有數(shù)據(jù)遷移完成后,CRUD操作還是會(huì)先根據(jù)緩存中MigrateStatus:${userId}的值進(jìn)行判斷,數(shù)據(jù)遷移完成后這一步已經(jīng)是多余的。可以加個(gè)總開(kāi)關(guān),當(dāng)所有數(shù)據(jù)遷移完成后,將這個(gè)開(kāi)關(guān)的值通過(guò)類似TOPIC的方式發(fā)送,所有服務(wù)接收到TOPIC后將開(kāi)關(guān)local cache化。那么接下來(lái)服務(wù)的CRUD都不需要先根據(jù)緩存中MigrateStatus:${userId}的值進(jìn)行判斷;

4.遺留工作

遷移完成后,將主動(dòng)遷移程序下線,并將被動(dòng)遷移程序中對(duì)migratePassive()的調(diào)用全部去掉,并可以集成一些第三方分庫(kù)分表中間件,例如sharding-jdbc,可以參考sharding-jdbc集成實(shí)戰(zhàn)

回顧總結(jié)

回顧這個(gè)方案,***的缺點(diǎn)就是如果碰到sharding column(例如userId)的總記錄數(shù)比較多,且主動(dòng)遷移正在進(jìn)行中,被動(dòng)遷移與主動(dòng)遷移碰撞,那么被動(dòng)遷移可能需要等待較長(zhǎng)時(shí)間。

不過(guò)根據(jù)DB性能,一般批量插入1000條數(shù)據(jù)都是10ms級(jí)別,并且同一sharding column的記錄分庫(kù)分表后只屬于一張表,不涉及跨表。所以,只要在遷移前先通過(guò)sql統(tǒng)計(jì)待遷移表中沒(méi)有這類異常sharding column即可放心遷移;

筆者當(dāng)初遷移installed_app表時(shí),用戶最多也只擁有不超過(guò)200個(gè)APP,所以不需要過(guò)多考慮碰撞帶來(lái)的性能問(wèn)題;沒(méi)有***的方案,但是有適合自己的方案;

 

如果有那種上萬(wàn)條記錄的sharding column,可以把這些sharding column先緩存起來(lái),遷移程序在夜間上線,優(yōu)先遷移這些緩存的sharding column的數(shù)據(jù),就可以盡可能的降低遷移程序?qū)@些用戶的體驗(yàn)。當(dāng)然你也可以使用你想出來(lái)的更好的方案。 

責(zé)任編輯:龐桂玉 來(lái)源: 數(shù)據(jù)庫(kù)開(kāi)發(fā)
相關(guān)推薦

2020-05-06 13:47:42

ZooKeeperKubernetes遷移

2019-04-25 10:40:02

分庫(kù)分表MySQL數(shù)據(jù)庫(kù)

2018-01-12 15:17:40

數(shù)據(jù)庫(kù)水平分庫(kù)數(shù)據(jù)遷移

2024-10-25 10:00:00

云服務(wù)計(jì)算

2021-03-01 10:10:39

數(shù)據(jù)遷移擴(kuò)容

2021-06-26 08:09:21

MySQL不停機(jī)不鎖表

2019-01-02 16:40:13

MongoDBPostgres數(shù)據(jù)庫(kù)

2020-07-28 09:04:09

NewSQL分庫(kù)分表

2020-04-13 15:45:46

MySQL數(shù)據(jù)庫(kù)備份

2011-11-09 15:49:52

API

2020-07-30 17:59:34

分庫(kù)分表SQL數(shù)據(jù)庫(kù)

2022-02-23 08:55:06

數(shù)據(jù)遷移分庫(kù)分表數(shù)據(jù)庫(kù)

2022-07-11 08:16:47

NewSQL關(guān)系數(shù)據(jù)庫(kù)系統(tǒng)

2024-11-22 15:32:19

2024-06-06 11:26:03

2019-11-12 09:54:20

分庫(kù)分表數(shù)據(jù)

2022-10-08 09:33:00

平臺(tái)中間件

2023-08-18 12:17:03

Linode實(shí)時(shí)遷移云計(jì)算

2009-11-20 11:37:11

Oracle完全卸載

2021-08-31 20:21:11

VitessMySQL分庫(kù)
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)