真实的国产乱ⅩXXX66竹夫人,五月香六月婷婷激情综合,亚洲日本VA一区二区三区,亚洲精品一区二区三区麻豆

成都創(chuàng)新互聯(lián)網(wǎng)站制作重慶分公司

面試官:如何做到不停機(jī)分庫分表遷移?

面試官:如何做到不停機(jī)分庫分表遷移?

網(wǎng)站的建設(shè)創(chuàng)新互聯(lián)專注網(wǎng)站定制,經(jīng)驗(yàn)豐富,不做模板,主營網(wǎng)站定制開發(fā).小程序定制開發(fā),H5頁面制作!給你煥然一新的設(shè)計(jì)體驗(yàn)!已為成都石牌坊等企業(yè)提供專業(yè)服務(wù)。

需求說明

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

借鑒codis

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

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

分庫分表

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

1. 確定sharding column

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

2. 分庫分表方案

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

主動遷移就是一個(gè)獨(dú)立程序,遍歷需要分庫分表的installed_app表,將數(shù)據(jù)遷移到分庫分表后的目標(biāo)表中。

被動遷移就是與installed_app表相關(guān)的業(yè)務(wù)代碼自身將數(shù)據(jù)遷移到分庫分表后對應(yīng)的表中。

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

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

public void migrate(){
    // 查詢出當(dāng)前表的最大ID, 用于判斷是否遷移完成
    long maxId = execute("select max(id) from installed_app");
    long tempMinId = 0L;
    long stepSize = 1000;
    long tempMaxId = 0L;
    do{
        try {
            tempMaxId = tempMinId + stepSize;
            // 根據(jù)InnoDB索引特性, where id>=? and id=#{tempMinId} and id<#{tempMaxId}";
            List installedApps = executeSql(scanSql);
            Iterator iterator = installedApps.iterator();
            while (iterator.hasNext()) {
                InstalledApp installedApp = iterator.next();
                // help GC
                iterator.remove();
                long userId = installedApp.getUserId();
                String status = executeRedis("get MigrateStatus:${userId}");
                if ("COMPLETED".equals(status)) {
                    // migration finish, nothing to do
                    continue;
                }
                if ("MIGRATING".equals(status)) {
                    // "被動遷移" migrating, nothing to do
                    continue;
                }
                // 遷移前先獲取鎖: set MigrateStatus:18 MIGRATING ex 3600 nx
                String result = executeRedis("set MigrateStatus:${userId} MIGRATING ex 86400 nx");
                if ("OK".equals(result)) {
                    // 成功獲取鎖后, 先將這個(gè)用戶所有已安裝的app查詢出來[即遷移過程以用戶ID維度進(jìn)行遷移]
                    String sql = "select * from installed_app where user_id=#{user_id}";
                    List userInstalledApps = executeSql(sql);
                    // 將這個(gè)用戶所有已安裝的app遷移到分庫分表后的表中(有user_id就能得到分庫分表后的具體的表)
                    shardingInsertSql(userInstalledApps);
                    // 遷移完成后, 修改緩存狀態(tài)
                    executeRedis("setex MigrateStatus:${userId} 864000 COMPLETED");
                } else {
                    // 如果沒有獲取到鎖, 說明被動遷移已經(jīng)拿到了鎖, 那么遷移交給被動遷移即可[這種概率很低]
                    // 也可以加強(qiáng)這里的邏輯, "被動遷移"過程不可能持續(xù)很長時(shí)間, 可以嘗試循環(huán)幾次獲取狀態(tài)判斷是否遷移完
                    logger.info("Migration conflict. userId = {}", userId);
                }
            }
            if (tempMaxId >= maxId) {
                // 更新max(id),最終確認(rèn)是否遍歷完成
                maxId = execute("select max(id) from installed_app");
            }
            logger.info("Migration process id = {}", tempMaxId);
        }catch (Throwable e){
            // 如果執(zhí)行過程中有任何異常(這種異常只可能是redis和MySQL拋出來的), 那么退出, 修復(fù)問題后再遷移
            // 并且將tempMinId的值置為logger.info("Migration process id="+tempMaxId);日志最后一次記錄的id, 防止重復(fù)遷移
            System.exit(0);
        }
        tempMinId += stepSize;
    }while (tempMaxId < maxId);
}

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

  • 第一步查詢出max(id)是為了盡量減少max(id)的查詢次數(shù),假如第一次查詢max(id)為10000000,那么直到遍歷的id到10000000以前,都不需要再次查詢max(id);

  • 根據(jù)id>=? and id=? limit n或者limit m, n進(jìn)行遍歷,因?yàn)閘imit性能一般,且會隨著遍歷越往后,性能越差。而id>=? and id

  • 根據(jù)id區(qū)間范圍查詢出來的List要轉(zhuǎn)換為Iterator,每迭代處理完一個(gè)userId,要remove掉,否則可能導(dǎo)致GC異常,甚至OOM;

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

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

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

  • 如果值為COMPLETED,表示已經(jīng)遷移完成,那么將請求轉(zhuǎn)移到分庫分表后的表中進(jìn)行處理即可;

  • 如果值為MIGRATING,表示正在遷移中,可以循環(huán)等待直到值為COMPLETED即遷移完成后,再將請求轉(zhuǎn)移到分庫分表后的表中進(jìn)行處理處理;

  • 否則值為空,那么嘗試獲取鎖再進(jìn)行數(shù)據(jù)遷移。遷移完成后,將緩存值更新為COMPLETED,最后再將請求轉(zhuǎn)移到分庫分表后的表中進(jìn)行處理處理;
3.方案完善

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

4.遺留工作

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

回顧總結(jié)

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

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

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

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


當(dāng)前標(biāo)題:面試官:如何做到不停機(jī)分庫分表遷移?
當(dāng)前網(wǎng)址:http://weahome.cn/article/pisgji.html

其他資訊

在線咨詢

微信咨詢

電話咨詢

028-86922220(工作日)

18980820575(7×24)

提交需求

返回頂部