1、前言
生產環境使用的是postgresql數據庫,其中有一張角色表t_role_right,包含了公司各產品的角色和權限項,目前有大約5億數據,好在建表初期建立了比較合理的索引,查詢起來走索引的話速度還是挺快的,目前運行良好。但是單表5億數據實在是太大了,雖然不知道postgresql單表數據量的極限在哪,估計已經快逼近極限了,一旦此表造成數據庫崩潰,將會影響公司所有產品線,這將是災難性的后果,所以分表迫在眉睫。表結構如下:
字段說明:
fcid 公司ID
froleid 角色ID
ftype 產品類型
fobjectid 模塊ID
faccess 各權限項之和
fmodifytime 修改時間
其中一個公司下面有多個角色,一個角色擁有多個產品下面多個模塊的權限,聯合主鍵為(fcid,froleid,ftype,fobjectid)
2、分表方案
首先想到的分表方案就是采用中間件,目前比較流行的中間件有MyCat和當當網的sharding-jdbc1、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表重命名,防止有數據寫進來,啟動數據庫服務
應用服務恢復之后,通知各產品線的測試,結果一切正常。
至此,分表方案完美成功!