千万级数据量系统优化实践

注:文中的“Doctor”是精分对话者,另一个我,道德约束者,话唠,请无视他。

博主最近在做公司一个很重要的项目,有关于汽车物联网金融的,作为刚工作一年的新人来说,这是一个极好的学习机会,而且被信任的感觉很好啊有木有。简单说博主在为某实力国产汽车公司卖命,这个系统涉及到的用户有几百万,每个用户都会有很多的金融数据,金融总是要结算的,所以每个月到结算日会有千万条数据需要操作,累计有几十亿次的计算,那么面对这庞大的数据集,我是瑟瑟发抖的,之前对于优化只是建立在理论上的,这次虽然是好的实践机会,但是作为核心项目,一点也马虎不得。

Doctor:你的废话太多了。。。

咳咳,那么废话不说了,现在开始进入正题,本文章将会跟大家分享一下个人在优化中的一些实践和心得,不足的地方希望大家指正。

关于针对该系统的优化,我主要做了以下几点:

1、数据库SQL语句调优,mybatis批量插入,建立索引

2、适当使用Redis存储不常修改但是常用的数据

3、使用异步处理,校验完核心数据即可返回前台操作完成,并异步跑后续操作

4、多线程处理海量运算


该系统的架构是基于SpringCloud的微服务架构,Springboot+Mybatis+Maven+MySql整合,同时使用到Redis、kafka。

1、数据库SQL语句调优,mybatis批量插入,建立索引

关于SQL语句调优我想最基本的就是在项目开始阶段,根据业务需求抽象成数据库表的时候要请专门的DBA来分析建表,本系统是由我跟另一位高级开发建的表,其实后期发现并不够完美,也许跟业务变更有些大导致的吧,我认为数据库表的设计和代码一样也需要健壮性,我们的系统使用的是基于SpringCloud的微服务架构,其实更像是把单体服务拆成多个springboot小项目,既然使用敏捷迭代开发模式(我们现在的模式也可以勉强说是devops,只不过devops里面运维要做的很多工作都由我们开发做了),就要针对变动频繁的需求设计出合理健壮的数据库表结构。

首先,要按照三范式来建表,但是既然是范式就未必一定遵守,其实有些时候,比如某张表里面有很多需要频繁加起来的字段,假设我们现在有一张结算表account,里面有三个字段:account_a、account_b、account_c,每次系统结算的时候都会从这个表里面取出这三个字段累加起来使用,那么我们可以违背范式规则,增加一个冗余字段:account_d,存放前三个字段的和,这样使用的时候就不需要取出前三个再计算,直接取最后一个字段即可。

在往数据库批量插入数据的时候,有两种方式,一种是通过在接口实现类里对集合遍历调用mapper,这样每有一条数据就会调一次mapper插一次数据库;另一种是使用mybatis的动态语句foreach,直接把一个集合插入,这样只需要一次mybatis动态代理,速度会比前一种快很多,但是需要注意的是mysql默认接受sql的大小是1M,如果集合过大就会报异常,可以调整MySQL安装目录下的my.ini文件[mysqld段的"max_allowed_packet = 1M"来改变默认接受的大小。

关于索引的建立,这里的实践真的是惊艳到我了,我测试将10万条数据去和一张170万条数据的表校验,对比出这10万条数据中哪些不在这表中,粗略估计最差的情况就要计算10万*170万次,也就是1700亿次,当然实际不可能这么多次,我们只是假设每次校验都校验到最后一条数据才能找到。我第一次跑的时候走了70多秒:

然后我给where语句后面的字段加了索引,使用的是B+树算法,建立索引的过程也等了一小段时间,毕竟170万条数据,当我建立完索引之后一运行,直接把我吓到了:

不得不感叹,这索引的强大,不实践真的没有体会,以前只是停留在理论上,这里博主还发现了一件奇怪的事情,就是我测试完一遍得到上一张图片的结果后又跑了一遍:

这里比上一次运行又快了十倍,不知道这次的优化是怎么出来的,是一个小谜题,如果有大神知道请评论里指教。

2、适当使用Redis存储不常修改但是常用的数据

我们可以把常用且不常修改的信息暂存到Redis里面,比如系统维护的数据字典表里面的信息,这样校验的时候直接从内存中取,避免了数据库层的访问,可以大大提高响应速度,如果是大量信息校验的系统里面,这种写法将会很大限度的提升接口响应速度。

下面是redis的部分配置内容:

redis:
    connect-timeout: 20s
    database: 7        
    host: 127.0.0.1    #本地地址,实际项目看项目的地址
    jedis:
      pool:
        max-active: 200
    read-timeout: 20s

下面是具体使用的代码:

/**
 * <Description> 常用缓存键类 <br>
 *
 * @author Coder_gasenwell<br>
 * @version 1.0<br>
 * @createDate 2020/5/4 <br>
 */
public interface CacheKey {

    /**
     * 字典缓存前缀
     */
    String PREFIX_TEST_SYS_DICT_TYPECODE = "test:sys_dict:typeCode:";

    /**
     * 字典缓存前缀
     */
    String PREFIX_TEST_SYS_DEPT_USERS_TREE = "test:sys_dept_users:tree:";
}
@Autowired
    private RedisUtils<String, Object> redisUtils;
//摘选一个方法作为参考,只关注实现语句逻辑,每次先检索redis里面是不是有缓存,如果没有就去数据库取数据然后存缓存,如果有的话就直接使用缓存。
    @Override
    public List<SysDict> getDictSetCache(String typeCode) {
        List<SysDict> dictList = null;
        // 确认缓存中是否有字典,如果没有有可能是第一次创建缓存
        Set<String> redisKeys = redisUtils.keys(CacheKey.PREFIX_TEST_SYS_DICT_TYPECODE + "*");
        if (null != redisKeys && redisKeys.size() > 0) {
            dictList = sysDictMapper.listByTypeCode(typeCode);
            if (null != dictList && dictList.size() > 0) {
                dictList.sort(new Comparator<SysDict>() {
                    @Override
                    //这里是对数据进行一个排序,可以忽略
                    public int compare(SysDict o1, SysDict o2) {
                        return o1.getSort() - o2.getSort();
                    }
                });
                redisUtils.set(CacheKey.PREFIX_TEST_SYS_DICT_TYPECODE + dictList.get(0).getTypeCode(), dictList);
            }

 

3、使用异步处理,校验完核心数据即可返回前台操作完成,并异步跑后续操作

虽然校验数据这里优化速度很快了,但是校验完要把海量数据取出来进行计算处理,所以为了避免从前端调一个接口让用户等太久,就把校验和后续计算分开,校验完成后把不存在的数据生产Excel表格导出,存在的数据开始走异步,此时返回前台接口调用完成。这时候前台会告诉用户校验完成,并下载到用户本地一个Excel文件。

Spring里面内置了@Async,所以这里开启异步处理只需要在方法体上打上该注解即可,然后由主要方法调用该方法。

4、多线程处理海量运算

 Spring是通过TaskExecutor来实现多线程的,使用Spring为我们提供的ThreadPoolTaskExecutor来实例化线程池,因为我们很多时候都是调用异步任务才会用到多线程,所以往往都会配合在启动类上打上@EnableAsync注解,在实际方法上打@Async注解,就可以开启多线程异步任务。

@SpringBootApplication(scanBasePackages = "cn.com.test")
@EnableFeignClients(basePackages = "cn.com.test")
@EnableAsync
@EnableDiscoveryClient
@EnableScheduling
@MapperScan("cn.com.test.mapper")
public class ThreadConfig implements AsyncConfigurer{
   @Override
   public Executor getAsyncExecutor() {
       ThreadPoolTaskExecutor threadPool = new ThreadPoolTaskExecutor();
       //核心线程数
       threadPool.setCorePoolSize(8);
       //最大线程数
       threadPool.setMaxPoolSize(50);
       //缓冲队列
       threadPool.setQueueCapacity(10);
       threadPool.setAwaitTerminationSeconds(60);
       threadPool.setThreadNamePrefix("TestThread:");
       threadPool.initialize();
       return threadPool;
   }
@Override
  public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
    return null;
  }

}

Doctor:我今天是不是很安静,我的台词都没了?

emmm,写的太专注,忘了给Doctor加戏了,下次一定。

Doctor: :)

好了,以上就是我的优化实践,这次实践确实体会到性能优化的强大之处,对于企业级开发的理解也更深了一步,也颇有成就感,想起来我们毕业前院长说的一句话:希望你们随着时间沉淀技术沉淀人格。技术和人格对于一个工程师来说,都很重要。

欢迎批评指正。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值