前言
今天分享一下关于分库分表的案例
在面试时,经常会问到一个问题!你公司有没有分表分库的案例?详细说说
如果说,你之前没有遇到过的话,建议看看这篇文章,提供了分库分表的几种案例,看完这个问题就知道如何回答了。
分库分表分为四种,垂直分库、垂直分表、水平分库、水平分表
若不理解分库分表概念,可看这篇:
Click me
垂直分库
在分布式项目中,随处可见
先介绍一下公司项目结构,分布式项目,根据业务划分一个个单独的服务,每一个服务单独一个数据库
xz_user : 用户服务相关
xz_account : 账户服务相关
xz_trade : 交易服务相关
xz_order :订单相关服务
xz_job : 定时任务相关服务
xz_fee: 手续费相关服务
xz_product: 产品相关服务
…
基本就是每个服务去连单独的数据库,避免了在一个项目中,连接多个数据库,需要进行多数据源管理的问题。
每个服务与服务之间相互调用
【多嘴一句】若是在单应用架构中使用垂直分库,需要手动管理数据源。
分享管理的思路如下:
使用jdbc连接的时候,针对不同的数据库,设置不同的jdbc连接池
例如可以有:order_jdbc, user_jdbc 操作不同的库时去使用不同的jdbc连接。
第二种管理思路:使用shardingsphere数据库中间件进行管理
sharding-jdbc可以在配置文件中配置好分库的关系,然后进行操作
【以上第二种方法后面会有专门的文章分享shardingsphere如何使用,管理多数据源】
垂直分表
在单应用架构系统或分布式系统中,随处可见。取决于表结构设计的思路
举例:
用户:拥有用户基本信息、密钥信息、交易密码信息 user_info user_key user_password
登录:登录信息、登录授权信息 login_info login_authorize
店铺:店铺信息、店铺持有人信息 store_info store_person
渠道:渠道基本信息、渠道详情信息 bank_channel bank_channel_info
假设上述都是:1对1对1,则根据每个场景不一样,可以创建多张表,避免一张表的字段非常多,这样数据量一大,就会很影响CURD
因此:垂直分表的思想无处不在,设置一张表的时候就会考虑这些
水平分库
在CURD时,需要根据路由规则进行某种库路由选择
如果使用shardingsphere就可以非常简单,通过配置数据源及路由规则即可,上层代码对底层数据库操作是无感知多数据库的。
不可否认,在现有的项目中还没用到水平分库
个人建议:在面试时可说有过水平分库的案例,使用shardingsphere进行管理,配置两个数据源,然后分库策略,主键生成策略,根据取模算法路由到数据库,然后进行操作。
切记:新项目、新数据库,一开始设计的时候就做好了水平分库。若是在已有项目上进行水平分库的话,涉及的路由规则、以及数据迁移非常复杂,很容易露馅,就不建议大家在旧项目上说,有过水平分库的操作。
若是在一个新项目中,使用shardingsphere进行水平分库,详细看shardingsphere入门教学,
(20201118号会将文章地址贴出来,正在加紧写)
水平分表
同上
【多嘴一句】如果没有用水平分表,可以介绍另一种处理方案:
借鉴水平分表的思想,通过数据迁移到一张一摸一样的表数据,来缓解大数据量下CURD的压力
面试时,如果提到该问题,面试官主要是想了解一下你有没有处理过大数据量经验,有其他的类似方案也可以抛出来聊聊
数据迁移
思路:在同一个库中创建一张一模一样数据结构的历史数据表,每隔一定的时间将数据移入到历史数据表中,同时删除原表的数据,这样可减轻原表的数据压力,提高响应数据。
查询时可根据时间的维度去判断查询新表,抑或是先查询新表,如果新表不存在,则查旧表。这就要根据具体的实际场景去设计了。
若历史表的数据多的话,以2年维度为一张历史表,不断迭加也可以
分享部分源码
/**
* 启动数据迁移
*/
public void startDataMigration(){
logger.info("==>startDataMigration className={}<==", this.simpleClassName);
try{
this.processDataMigration();
}catch(Exception e){
logger.error("数据迁移过程中出现异常", e);
}
logger.info("==>endDataMigration className={}<==", this.simpleClassName);
}
/**
* 处理数据迁移
*/
private void processDataMigration(){
logger.info("==>processDataMigration className={}", this.simpleClassName);
//step1 计算,得到需要迁移数据的日期
Date migrationEndDate = this.calcMigrationEndDate(this.getDataKeepDays());
logger.info("==>migrationEndDate={} className={}", DateUtils.formatDate(migrationEndDate), this.simpleClassName);
List<Date> migrationDateList = this.listNeedDoMigrationDates(migrationEndDate);
if(migrationDateList == null || migrationDateList.isEmpty()){
logger.info(this.simpleClassName+" 没有需要进行迁移的数据");
return;
}
// 以每天的数据进行迁移,否则数据量一下子太大了,分批处理
for(Date migrationDate : migrationDateList){
int migrationCount = 0;
//调用account服务,传入交易日期,得到符合条件的 minId、maxId,如果没有符合条件的数据,则继续下一个日期
AccountMigrationVo migrationVo = this.getMigrationVo(migrationDate);
migrationVo.setMigrateNumPerTime(this.getMigrationNumEachTime());
logger.info("==>migrationDate={} AccountMigrationVo={} className={} ", DateUtils.formatDate(migrationDate), JSON.toJSONString(migrationVo, true), this.simpleClassName);
// 开始处理,看下面的方法
migrationCount = this.executeDataMigration(migrationVo);
logger.info("==>processDataMigration_migrationDate={} preMigrationNum={} realMigrationNum={} className={}",DateUtils.formatDate(migrationDate), migrationVo.getPreMigrationNum(), migrationCount, this.simpleClassName);
}
logger.info("==>processDataMigration className={}<==", this.simpleClassName);
}
migrationCount = this.executeDataMigration(migrationVo);
/**
* 执行数据迁移
* @param migrationVo
* @return
*/
private int executeDataMigration(AccountMigrationVo migrationVo){
logger.info("==>executeDataMigration className={} AccountMigrationVo={}", this.simpleClassName, JSON.toJSONString(migrationVo, true));
int totalCount = 0;
//step3 分批迁移,每批次5000条记录
long nextMinId = migrationVo.getMinId();
while(migrationVo.getMaxId() >= nextMinId){
migrationVo.setCurrentMinId(nextMinId);
migrationVo.setCurrentMaxId(migrationVo.getCurrentMinId() + migrationVo.getMigrateNumPerTime()-1);
if(migrationVo.getCurrentMaxId() > migrationVo.getMaxId()){
migrationVo.setCurrentMaxId(migrationVo.getMaxId());
}
nextMinId = migrationVo.getCurrentMaxId() + 1;
try{
logger.info("==>currentMinId={} currentMaxId={} className={} start", migrationVo.getCurrentMinId(), migrationVo.getCurrentMaxId(), this.simpleClassName);
// 进行迁移操作,看下面代码
int migrationNum = this.doDataMigration(migrationVo);
logger.info("==>currentMinId={} currentMaxId={} migrationNum={} className={}", migrationVo.getCurrentMinId(), migrationVo.getCurrentMaxId(), migrationNum, this.simpleClassName);
totalCount += migrationNum;
}catch(Exception e){
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append("==>执行数据迁移过程中发生异常 Exception_Happened, currentMinId=").append(migrationVo.getCurrentMinId())
.append(" currentMaxId=").append(migrationVo.getCurrentMaxId())
.append(" className=").append(this.simpleClassName);
logger.error(stringBuilder.toString(), e);
}
}
logger.info("==>executeDataMigration className={}<==", this.simpleClassName);
return totalCount;
}
int migrationNum = this.doDataMigration(migrationVo);
/**
* 从主账户明细迁移数据到主账户明细历史记录表,同时删除在主账户明细表中的记录
* 事务隔离级别调到最低,避免死锁,因为不会有程序再对这些历史记录做更新操作了
* @param migrationVo
* @return
*/
@Transactional(rollbackFor = Exception.class)
public int doAccountDetailMigration(AccountMigrationVo migrationVo){
log.info("==>doAccountDetailMigration_migrationDate={} curMixId={} curMaxId={}", DateUtil.formatDateTimeSafe(migrationVo.getMigrationDate()), migrationVo.getCurrentMinId(), migrationVo.getCurrentMaxId());
this.validateMigrationParam(migrationVo);
List<Long> detailIdList = accountDetailDao.listIdsForMigration(migrationVo);
log.info("==>doAdvanceDetailMigration_migrationDate={} detailIdList.size={} 获取id完成", DateUtil.formatDateTimeSafe(migrationVo.getMigrationDate()), detailIdList==null?0:detailIdList.size());
if(detailIdList == null || detailIdList.size() <= 0){
return 0;
}
int insertCount = accountDetailHistoryDao.migrationFromAccountDetailByIds(detailIdList);
log.info("==>doAccountDetailMigration_migrationDate={} 本批次插入数据完成,开始删除", DateUtil.formatDateTimeSafe(migrationVo.getMigrationDate()));
if(insertCount > 0){
int deleteCount = accountDetailDao.deleteDetailByIdList(migrationVo, detailIdList);
if(insertCount != deleteCount){
log.error("doAccountDetailMigration_error insertCount={} and deleteCount={} not match,Transactional rollback, AccountMigrationVo={}", insertCount, deleteCount, JSON.toJSONString(migrationVo, true));
throw new AccountMchBizException(AccountMchBizException.PARAM_INVALID, "主账户明细迁移的记录数跟删除记录数不一致");
}
}
log.info("==>doAccountDetailMigration_migrationDate={} <==", DateUtil.formatDateTimeSafe(migrationVo.getMigrationDate()));
return insertCount;
}
自此,学会了数据迁移解决数据量过大的问题
探讨旧项目水平分库分表问题
为什么上面说:若没有相关实际经验,不要跟面试官说,在旧项目中使用水平分库分表?
以水平分表为例,若原先的架构是单表,要进行水平分表,需要解决以下的问题。
1、 路由规则设定,
假如说是水平分表2,最简单的路由是偶数2表,奇数1表,但是目前所有的数据都在表1中,需要进行数据迁移。
抑或是路由规则为:表1为旧表,表2为新表,这样后续的数据基本都插入到2表,1表为旧数据。这种处理是很麻烦的
1)嵌套shardingsphere 2)使用分布式数据库解决
在这个过程有非常多的细节点和问题要解决,所以一般到最后一步实在不能优化的数量级别才会考虑水平分表,容易被某个点给问倒。
2、 多结点查询问题 ~ 多条件查询 + 统计都是难点
基本水平分表分库是最后一步考虑的
若是新项目, 不存在旧数据,一开始设计为水平分库分表就可以,使用shardingsphere中间件管理,这样就完美哩。
详细看shardingsphere入门教学,
(20201118号会将文章地址贴出来,正在加紧写)