大家好,我是小趴菜,最近接了一个需求,需求得流程如下
- 1:管理员在后台发布一个任务,并且可以指定哪些用户可以接收这个任务
- 2:管理员在发布任务得时候,会创建一个导入数据得Excel模板
- 3:接收任务的用户按照这个模板来上传数据
- 4:上传的数据是保存到MySql中的
其实整个流程下来其实很简单,也就是一个上传数据的需求。但是通过了解需求背景的原因,可以明确的是整个数据量以后会很大,所以我们在一开始就决定使用分表的方式来存储
既然要分表存储,那么以下几个问题是不得不考虑的
- 1:如何分表,分片建如何选择
- 2:如果分表以后,单表的数据量还是很大,就要重新进行分表,这时候如何做数据迁移
- 3:如何满足多维度的条件查询
- 4:.........
接下来看看我是如何来实现整个数据的分表设计的
任务ID取模
当时的第一反应是想按照任务ID来作为分片键,首先管理员会创建一个任务,这个任务就信息会保存在任务表中,这时候可以根据任务表的主键来进行分片,用任务ID进行取模,来确定这个任务的数据最终是保存到哪张表中
js
spring.shardingsphere.sharding.tables.traffic.table-strategy.inline.algorithm-expression=data_$->{ task_id % 2 }
spring.shardingsphere.sharding.tables.traffic.table-strategy.inline.sharding-column=task_id
现在来看看以任务ID为分片键会有什么问题
1:数据扩容
比如一开始我们是分了8张表,如果时间久了,这8张表数据都很大了,这时候我们就要分16张表存储,这时候,以前的数据就要做迁移了,将以前的数据重新进行路由存储
2:数据倾斜
可能任务ID=1这个任务的数据只有几百条,但是任务ID=2的这个任务可能有几十万,这都是有可能的,举个例子,任务ID=1的这个任务只是收集整个省的某次考试的监考老师信息,但是任务ID=2的这个任务呢是收集这次参加考试的所有考生的信息,那么这二次任务收集到的数据的量差距是很大的
所以就会导致你分的几张表中,可能表_1的数据量只有几千,但是表_2可能已经是几百万了
3:多维度的查询
从管理员角度来看
我需要知道自己发布了哪些任务,并且能查看到某个任务的数据,首先我们有一张任务的主表,也就是存这个任务的一些基本信息,比如谁创建的,任务名称,创建的时间,使用的模板是多少等等,其实是完全满足管理员的查询
至于要查询某个任务的数据,就可以根据任务ID主键进行取模来确定数据具体是在哪一张表中
从用户角度来看
作为一个用户,我需要知道我接收到哪些任务,所以我们有一张单独的表记录每一个任务都有哪些用户接收。这样就可以根据用户的ID来找到这个用户的所有接收到的任务
至于要查询某个任务的数据,就可以根据任务ID主键进行取模来确定数据具体是在哪一张表中
4:方案总结
以任务ID进行取模的方案虽然简单,并且添加两张中间表就可以满足管理员和普通用户的多维度查询,但是缺点就是会存在数据倾斜还需要数据迁移
以任务ID范围分片
比如任务ID在 1-100范围内的任务数据都存在一张表,任务ID在101-200的数据存在一张表
现在来看看以任务ID范围分片会有什么问题
1:数据扩容
现在不会存在数据迁移的问题了,后续的任务根据范围存在不同的表中即可,即使任务数量起来了,也只是新增几个表而已
2:数据倾斜
依然还会存在数据倾斜的问题,比如任务ID从1-100这100个任务的数据量很小,但是下一个任务的数据量可能抵得上前面100个任务的数据了,如果某一张表中的100个任务数据量都很大,那么这张表的数据量就会非常非常大
3:多维度的查询
从管理员角度来看
我需要知道自己发布了哪些任务,并且能查看到某个任务的数据,首先我们有一张任务的主表,也就是存这个任务的一些基本信息,比如谁创建的,任务名称,创建的时间,使用的模板是多少等等,其实是完全满足管理员的查询
至于要查询某个任务的数据,就可以根据任务ID主键进行取模来确定数据具体是在哪一张表中
从用户角度来看
作为一个用户,我需要知道我接收到哪些任务,所以我们有一张单独的表记录每一个任务都有哪些用户接收。这样就可以根据用户的ID来找到这个用户的所有接收到的任务
至于要查询某个任务的数据,就可以根据任务ID主键进行取模来确定数据具体是在哪一张表中
4:方案总结
以任务ID进行范围分片方案虽然简单,并且添加两张中间表就可以满足管理员和普通用户的多维度查询,但是缺点还是会存在数据倾斜,而且倾斜的概率会比ID取模更大
有些人可能会说那么就每10个任务一张表,如果你后续任务多,比如有10万个任务,那么就有1万张数据表,维护起来就太麻烦了
以时间范围分片
其实时间范围分片跟以ID范围分片的效果是一样的,这里就不再赘述
自定义分片键
我们以任务ID,或者其它字段来作为分片键大都都是取模来选择最终要落得表,也就是说在一定程度上你这个任务的数据数据落到哪张表上是系统上决定的
举个例子,比如我们有10张表,然后以任务ID取模,这时候任务ID=11的这个任务的数据是落在表_1这张表,但是我们发现其实表_1的数据量已经很大了,我们想让这个任务的数据落在数据量最小的那张表中,这时候我们以任务ID取模的就做不到了
一般来说为了防止数据倾斜,我们都会把最新的一个任务的数据放到数据量最小的那张表中,所以为了满足这个需求,我们需要自己定义分片建的值,而不是使用像ID这种数据库帮我们生成的
自定义分片键生成需要注意哪几个点呢?
- 1:重复问题
- 2:尽可能短,太长的话占数据库磁盘
其实对于我们这个系统而言,这个分片值没有什么特殊作用,只是用来分片而已,所以为了满足不重复的要求,我们这个分片是使用任务ID_表后缀的形式来实现的
什么是表后缀呢? 我们的表是这种格式的: data_1,data_2,data_3,data_4,这种格式的,表后缀就是表名最后的数字,这个任务ID是数据库的自增主键,所以不会重复,所以整个自定义的分片键就不会重复,所以这个任务的数据最终会落到哪张表中就是根据分片键最后的表后缀来决定的
至于根据分片键来决定最终落在那张表的实现,ShardingJdbc是提供了自定义的实现的,这个可以去网上搜索相关的代码。
最关键的是我们的分片键要如何生成,并且如何满足数据扩容如何做到无迁移,数据不会倾斜,并且可以满足多维度查询 的要求
- 1:在配置类配置表信息
js
public class DbConfig{
public static static List<DbInfo> dbList = new ArrayList<>();
//DbInfo第一个参数是值这个表的后缀
//DbInfo第二个参数表示的这张表现有的数据量,后续有使用到
static {
DbInfo dbInfo_1 = new DbInfo(1,0);
dbList.add(dnInfo_1);
DbInfo dbInfo_2 = new DbInfo(2,0);
dbList.add(dnInfo_2);
//继续添加剩余的表,比如我们有10张表,这里就把10张表的信息都配置进去
}
//通过轮询的方式进行选择表
publci static Long getDbNum(Long taskId) {
//拿到dbList的大小
int size = dnList.size();
return taskId%size;
}
}
- 2:用户创建任务,生成分片键
js
public void createTask(TaskInfoDto taskInfoDto) {
//管理员创建任务,并保存到数据库
taskMapper.insert(taskInfoDto);
//获取这个任务的主键ID
Long taskId = taskInfoDto.getId();
//构造这个任务的分片键
Long dbNum = DbConfig.getDbNum(taskId);
String key = taskId + "_" + dbNum;
//将这个分片键保存到数据库
taskInfoDto.setKey(key);
taskMapper.updateById(taskInfoDto);
}
- 3:后续用户上传数据只需要根据自定义的分片键来确认是落在哪张表即可
现在我们来看看,自定义分片键是否解决了之前出现的问题
数据扩容无迁移
比如开始我们分了10张表,现在要扩容了,这时候又加了10张表,那么我们需要做数据迁移嘛?
答案是不用,为什么呢?
因为我们的分片键是包含了表的后缀的,即使你现在扩容加了表,后续的分片键只需要加上你新扩容的表名的后缀即可,至于历史的任务数据还在原来的表不动,所以扩容的时候我们根本不需要做数据迁移
数据倾斜
上面我们是使用轮询的方式来决定这个任务的数据最终落到哪一张表中,但是轮询是会造成数据倾斜的,所以为了避免数据倾斜,我们需要你使用到另外一种算法:加权轮询算法
每一张表都有一个DbInfo对应的一个对象数据,这个对象的第二个参数就是这张表此时的数据量 那么我们在getDbNum方法的时候,是不是可以根据这个值来获取到数据量最小的那张表呢?
肯定是可以的,我们可以根据数据量这个参数来进行排序,拿到数据量最小的那张表的数据,然后将这个任务的数据都保存到数据量最小的那张表里,所以我们在一定程度上是可以避免数据倾斜的问题的
但是这里有一个注意点,就是用户每次上传了数据都要更新下数据库的数据量
js
public class DbConfig{
public static static List<DbInfo> dbList = new ArrayList<>();
//DbInfo第一个参数是值这个表的后缀
//DbInfo第二个参数表示的这张表现有的数据量,后续有使用到
static {
DbInfo dbInfo_1 = new DbInfo(1,0);
dbList.add(dnInfo_1);
DbInfo dbInfo_2 = new DbInfo(2,0);
dbList.add(dnInfo_2);
//继续添加剩余的表,比如我们有10张表,这里就把10张表的信息都配置进去
}
//通过轮询的方式进行选择表
publci static Long getDbNum(Long taskId) {
//拿到dbList的大小
int size = dnList.size();
return taskId%size;
}
}
为什么说是一定程度避免了数据倾斜呢????
比如你创建一个任务,有10个人需要上传数据,但是这10个人上传的时间都是不一样的,所以可能第一个人上传了数据,但是后面几个人还没上传,此时这张表的数据依然是最少的,所以下个任务还是会把数据保存到这张表中,后续另外9个人把数据上传以后,这张表的数据可能就非常大了
但是在后续的时间内,由于这张表的数据量很大,所以会有很多的任务的数据都不会保存到这张表,数据倾斜也就会慢慢恢复
3:多维度的查询
从管理员角度来看
我需要知道自己发布了哪些任务,并且能查看到某个任务的数据,首先我们有一张任务的主表,也就是存这个任务的一些基本信息,比如谁创建的,任务名称,创建的时间,使用的模板是多少等等,其实是完全满足管理员的查询
至于要查询某个任务的数据,就可以根据自定义分片键的表后缀来判断这个任务具体是在哪中了
从用户角度来看
作为一个用户,我需要知道我接收到哪些任务,所以我们有一张单独的表记录每一个任务都有哪些用户接收。这样就可以根据用户的ID来找到这个用户的所有接收到的任务
至于要查询某个任务的数据,就可以根据自定义分片键的表后缀来判断这个任务具体是在哪中了
写在最后
我们当前业务只有分表,没有分库,如果后续分库,那么就要涉及到一个数据迁移,以及包括分片键的改造,所以这就是当前方案后续可能会存在的隐患
其实这个方案可能还是会存在一些隐患,这里欢迎大家提出,后续持续改进