单张亿级大表分表方案
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表重命名,防止有数据写进来,启动数据库服务
应用服务恢复之后,通知各产品线的测试,结果一切正常。
至此,分表方案完美成功!