【案例】分库分表企业应用实例

15 篇文章 0 订阅
2 篇文章 0 订阅

前言

今天分享一下关于分库分表的案例
在面试时,经常会问到一个问题!你公司有没有分表分库的案例?详细说说
如果说,你之前没有遇到过的话,建议看看这篇文章,提供了分库分表的几种案例,看完这个问题就知道如何回答了。
分库分表分为四种,垂直分库、垂直分表、水平分库、水平分表
若不理解分库分表概念,可看这篇:
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号会将文章地址贴出来,正在加紧写)

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
下面给出一个使用Druid连接池实现分表分库的示例: 1. 添加Druid连接池依赖,例如在Maven项目中添加以下依赖: ```xml <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.2.3</version> </dependency> ``` 2. 配置Druid连接池,例如在Spring Boot项目中可以在application.properties文件中添加以下配置: ```properties spring.datasource.driver-class-name=com.mysql.jdbc.Driver spring.datasource.url=jdbc:mysql://localhost:3306/testdb?useUnicode=true&characterEncoding=utf8&autoReconnect=true&useSSL=false spring.datasource.username=root spring.datasource.password=root spring.datasource.type=com.alibaba.druid.pool.DruidDataSource spring.datasource.initialSize=5 spring.datasource.minIdle=5 spring.datasource.maxActive=20 spring.datasource.maxWait=60000 spring.datasource.validationQuery=SELECT 1 FROM DUAL spring.datasource.testOnBorrow=false spring.datasource.testOnReturn=false spring.datasource.testWhileIdle=true spring.datasource.timeBetweenEvictionRunsMillis=60000 spring.datasource.minEvictableIdleTimeMillis=25200000 spring.datasource.filters=stat,wall,log4j spring.datasource.connectionProperties=druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000 ``` 其中,spring.datasource.url中的testdb是数据库名,可以根据实际情况修改为自己的数据库名。 3. 实现分表分库功能,例如可以在代码中使用Sharding-JDBC来实现分表分库功能,具体实现方式可以参考以下示例: ```java @Configuration public class DataSourceConfiguration { @Bean public DataSource dataSource() throws SQLException { Map<String, DataSource> dataSourceMap = new HashMap<>(); dataSourceMap.put("ds0", createDataSource("ds0")); dataSourceMap.put("ds1", createDataSource("ds1")); ShardingRuleConfiguration shardingRuleConfig = new ShardingRuleConfiguration(); shardingRuleConfig.getTableRuleConfigs().add(getOrderTableRuleConfiguration()); shardingRuleConfig.getBindingTableGroups().add("t_order"); return ShardingDataSourceFactory.createDataSource(dataSourceMap, shardingRuleConfig, new Properties()); } private TableRuleConfiguration getOrderTableRuleConfiguration() { TableRuleConfiguration result = new TableRuleConfiguration("t_order", "ds${0..1}.t_order_${0..1}"); result.setDatabaseShardingStrategyConfig(new InlineShardingStrategyConfiguration("user_id", "ds${user_id % 2}")); result.setTableShardingStrategyConfig(new InlineShardingStrategyConfiguration("order_id", "t_order_${order_id % 2}")); return result; } private DataSource createDataSource(final String dataSourceName) throws SQLException { DruidDataSource result = new DruidDataSource(); result.setDriverClassName("com.mysql.jdbc.Driver"); result.setUrl(String.format("jdbc:mysql://localhost:3306/%s?useUnicode=true&characterEncoding=utf8&useSSL=false", dataSourceName)); result.setUsername("root"); result.setPassword("root"); return result; } } ``` 以上示例中,创建了两个数据源ds0和ds1,分别对应了两个数据库,使用Sharding-JDBC实现了分表分库功能,其中按照user_id对数据进行分库,按照order_id对表进行分表。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值