單張億級大表分表方案


1、前言

生產環境使用的是postgresql數據庫,其中有一張角色表t_role_right,包含了公司各產品的角色和權限項,目前有大約5億數據,好在建表初期建立了比較合理的索引,查詢起來走索引的話速度還是挺快的,目前運行良好。但是單表5億數據實在是太大了,雖然不知道postgresql單表數據量的極限在哪,估計已經快逼近極限了,一旦此表造成數據庫崩潰,將會影響公司所有產品線,這將是災難性的后果,所以分表迫在眉睫。

表結構如下:

字段說明:

fcid 公司ID
froleid 角色ID
ftype 產品類型
fobjectid 模塊ID
faccess 各權限項之和
fmodifytime 修改時間

其中一個公司下面有多個角色,一個角色擁有多個產品下面多個模塊的權限,聯合主鍵為(fcid,froleid,ftype,fobjectid)

2、分表方案

首先想到的分表方案就是采用中間件,目前比較流行的中間件有MyCat和當當網的sharding-jdbc

1、MyCat

MyCat是一個真正意義上的中間件,它需要單獨安裝,並且啟動一個獨立的服務

2、Sharding-Jdbc

Sharding-Jdbc是一個第三方jar包,可以直接植入到項目中,但是它對表之間的left join支持不是很好

因此,以上兩種方案均被否定,最后采用的是Mybatis攔截器的分表方案

3、Mybatis

分表方案為:利用公司ID對40取模,將表分到40個新表中(新表和原來的大表結構一模一樣),t_role_right_0,t_role_right_1,t_role_right_2 ... t_role_right_39

配置文件:

代碼:

@Intercepts({  
	@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})
})  
public class ShardingInterceptor implements Interceptor {

	private static final String TABLE = "t_role_right";
	private static final String EXCLUDE_TABLE = "t_role_right_4upgrade";
	private static final int SHARDING_NUM = 40;//分表數量
			
	@Override
	public Object intercept(Invocation invocation) throws Throwable {
		StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
        BoundSql boundSql = statementHandler.getBoundSql(); 
		String sql = boundSql.getSql();
		
		if(sql.contains(TABLE) && !sql.contains(EXCLUDE_TABLE)){

                        //獲取SQL語句參數中的公司ID
			ParameterHandler parameterHandler = statementHandler.getParameterHandler();
			Object parameter = parameterHandler.getParameterObject();
			long cid = 0L;
			if(parameter instanceof RoleRight){
				RoleRight rr = (RoleRight)parameter;
				cid = rr.getCid();
			}else if(parameter instanceof Long){
				cid = (Long)parameter;
			}else{
				Map<String,Object> args = (HashMap<String,Object>)parameter;
				if(args.containsKey("list")){
					List<RoleRight> rrList = (List<RoleRight>)args.get("list");
					for(RoleRight rr : rrList){
						cid = rr.getCid();
						break;
					}
				}else{
					cid = Long.parseLong(String.valueOf(args.get("cid")));
				}
			}

                        //公司ID對40取模,得到該公司ID對應的新表
			String shardingTable = TABLE + "_" + cid % SHARDING_NUM;

                        //將原SQL語句中的t_role_right替換成新表
			String newSql = sql.replace(TABLE, shardingTable);
			
                        //通過反射修改sql語句
			Field field = boundSql.getClass().getDeclaredField("sql");
			field.setAccessible(true);
			field.set(boundSql, newSql);
		}
		return invocation.proceed();
	}

	@Override
	public Object plugin(Object target) {
		return Plugin.wrap(target, this);
	}

	@Override
	public void setProperties(Properties properties) {
		//此處可以接收到配置文件中的property參數
	}
}

可以看到,上述程序不需要修改xml文件中的SQL語句,即可動態的實現CRUD操作到分表后的表。

3、遷移數據

程序處理完之后,接下來就是要遷移數據了,遷移數據嘗試了以下幾種方案:

1、postgresql存儲過程

先查出所有的公司ID,再循環依次以公司ID為單位insert into select from

create or replace function sharding() 
returns integer as $$
declare 	
		cidCur cursor for select distinct fcid from t_role_right;
		v_tbl varchar;--由於表名不能使用變量,所以需要動態生成sql,再執行
		v_sql varchar;
		v_cid numeric;
		i integer; 
begin
		open cidCur;
		i:=0;
		fetch cidCur into v_cid;--必須先fetch一條,否則found為false
		while found loop
				v_tbl :=  't_role_right_' || v_cid % 40;
				v_sql := concat('insert into ',v_tbl,' select * from t_role_right where fcid = ',v_cid);
					execute(v_sql);		
					i:= i+1;	
					raise notice 'cid=%成功遷移到表%',v_cid,v_tbl;
			fetch cidCur into v_cid;	
		end loop;
    close cidCur;
		return i;
END;
$$ 
LANGUAGE plpgsql;

在內網環境測試了一下,內網t_role_right表中有50多萬條數據,執行存儲過程只需要7-9秒,速度還是很快的
但是postgresql這個版本(9.6.1,select version()可查看版本號)有一個很大的問題:就是存儲過程是一個整個的事務,無法拆分成多個事務,也就是說insert into select from這條語句執行完之后不會提交,要等所有數據執行完畢之后,一次性提交5億數據,這種結果無疑是很慢的,一旦發生異常,5億數據要全部回滾,具有不可預料的風險。

2、多線程

上面的存儲過程無法做到一個insert into select from作為一個事務提交,那我就用程序來實現它,多線程。
上網搜了一下,對於IO密集型的應用,則線程池大小設置為2N+1,N是CPU核數,很明顯,我們的應用就屬於這種,所以線程池大小設置為9

將程序打成jar包放到內網服務器上執行,發現執行時間很慢,同樣是內網50多萬條數據,用多線程跑完,發現需要20多秒,這個更無法接受了
我分析了一下原因,可能就是應用程序的內存和數據庫服務器的IO比較慢,還有網絡傳輸等因素影響了執行時間。

怎么辦?上面兩種方案都不合理,但是當天晚上就要停機發布,已經提前發布停機公告了,從0點到3點,只有3個小時的時間。

越是到緊要關頭,越能想出點子,我靈機一動,不就是要實現把insert into select from作為一個事務提交嘛,為什么不簡單粗暴一點呢?

3、SQL

insert into t_role_right_0 select * from t_role_right where mod(fcid,40) = 0;
insert into t_role_right_1 select * from t_role_right where mod(fcid,40) = 1;
insert into t_role_right_2 select * from t_role_right where mod(fcid,40) = 2;
insert into t_role_right_3 select * from t_role_right where mod(fcid,40) = 3;
.....
insert into t_role_right_39 select * from t_role_right where mod(fcid,40) = 39;

直接寫出40條SQL語句,分別在不同的窗口執行,這不就相當於多線程嗎?並且每個insert into select from還是獨立的事務提交,就是工作量大了點,需要點40次執行,但是至少達到了我們的目的。

確定了這個方案,說干就干,馬上拉到內網測試,取一個公司ID最多的數據,大約15000條數據,4秒左右就執行完了,大功告成!

后面DBA想到了建一個條件索引的辦法,索引建好之后,還是取公司ID最多的數據,執行insert into select from,2秒左右就完成了,效率提升了一倍。

CREATE INDEX index_01_t_role_right ON t_role_right (fcid) WHERE  fcid % 40=0;
CREATE INDEX index_01_t_role_right ON t_role_right (fcid) WHERE  fcid % 40=1;
CREATE INDEX index_01_t_role_right ON t_role_right (fcid) WHERE  fcid % 40=2;
...
CREATE INDEX index_01_t_role_right ON t_role_right (fcid) WHERE  fcid % 40=39;

此處建索引還有優化的空間,生產有5億數據,建一個索引會花很長時間,建40個那就更費時間了,所以,把上面的40條索引合並為1條

CREATE INDEX index_mod_t_role_right ON t_role_right (mod(fcid, 40)) ;

OK!到此,所有的准備工作都做好了,坐等發布和遷移數據了

4、發布生產

運維停機之后,DBA就開始操作了,先把40張表和各自的索引建好,然后重啟數據庫,清空所有連接,保證沒有新的數據寫入t_role_right表中 在遷移之前,我先統計了一下t_role_right表中的數據,方便和后面分表后的數據對比

理想很豐滿,現實很骨感

我們數據庫部署在騰訊雲上,之前有在內網測試過,5億數據建索引大約需要32秒左右,但是在騰訊雲上建索引快半個小時了,還是沒有完,不知道是不是騰訊雲的問題,后來DBA找騰訊雲的客服,客服找對應的技術人員,鼓搗了一個小時才建好索引,
建好索引之后,就開始多個窗口執行insert into select from了,過程很順利,一個小時左右就全部執行完了
我寫了一個存儲過程,專門用來統計分表之后各個新表的數據總和,統計結果和之前記錄的t_role_right表中的數據一致

create or replace function count_sharding() 
returns integer as $$
declare 	
		v_tbl varchar;--由於表名不能使用變量,所以需要動態生成sql,再執行
		v_sql varchar;
		v_count numeric;
		v_temp_count numeric;
		i integer; 
begin
		i:=0;
		v_count :=0;
		while i<40 loop
				v_tbl := 't_role_right_' || i;
				v_sql := concat('select count(1) from ',v_tbl);
				execute(v_sql) into v_temp_count;
				raise notice '表%數量=%',v_tbl,v_temp_count;
				v_count := v_count + v_temp_count;
				i:= i+1;	
		end loop;
		return v_count;
	END;
$$ 
LANGUAGE plpgsql;

然后將原來的t_role_right表重命名,防止有數據寫進來,啟動數據庫服務
應用服務恢復之后,通知各產品線的測試,結果一切正常。

至此,分表方案完美成功!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM