zoukankan      html  css  js  c++  java
  • 记一次结算程序的性能优化过程

    背景:某项目结算程序,业务系统每日生成当天全量交易数据并上传至FTP,该结算程序从FTP中获取文件后解析交易数据,执行余额变更操作及登记资金流水。

    第一轮压测结果:TPS=3

    分析源码后发现,开发童鞋是串行单笔处理的,简化后的核心代码如下:

    //遍历每条交易数据
    foreach(){
        insertLogAndUpdateBalance();
    }
    
    //启用事务
    insertLogAndUpdateBalance(){
        //插入资金流水
        insertLog();
        //更新余额
        updateBalance();
    }
    

    第一轮改进方法:
    1、改串行单笔处理为串行多笔处理
    2、余额变更update转insert,即先记入余额流水表,再异步刷新回余额表
    3、这里引入了一个新问题:如何保证余额不被扣成负数?该问题后面另起专题介绍。
    简化后的核心代码

    //遍历每批交易数据,每500条一批
    foreachBatch(){
        insertLogAndUpdateBalanceBatch();
    }
    
    //启用事务
    insertLogAndUpdateBalance(){
        //插入资金流水
        insertLogBatch();
        //更新余额
        updateBalanceBatch();
    }
    

    第二轮压测结果:TPS很不稳定,15 - 45之间

    第二轮改进方法:
    经过排除后发现开发人员使用定时任务的方式为,(SpringBoot项目)

    @EnableScheduling
    public class ExceptionStatusChangeNotifyScheduler {
     @Scheduled(cron = "0 */1 * * * ?")
        public void doUpdateStatus() {
            //业务逻辑处理
        }
    }
    

    翻看EnableScheduling 源码可以看到类说明里面已经很清晰的描述了,默认情况下是使用单线程方式,而我们项目同时有多个定时任务,所以这也解释了为什么TPS会很不稳定的现象。

     * In all of the above scenarios, a default single-threaded task executor is used.
     * When more control is desired, a {@code @Configuration} class may implement
     * {@link SchedulingConfigurer}. This allows access to the underlying
     * {@link ScheduledTaskRegistrar} instance. For example, the following example
     * demonstrates how to customize the {@link Executor} used to execute scheduled
     * tasks:
    

    对于这个问题,我特意百度了一下也看了某些介绍SpringBoot定时任务的书及文章,都是一样的demo,但都没有提到这种方式是单线程的......
    所以这里我想说网络及书籍上的代码可以参考,但要有相对细致的了解,不致于出现问题时束手无策。
    解决方案很简单,就是实现SchedulingConfigurer接口,示例如下:

    public class XXX implements SchedulingConfigurer{
        @Override
        public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
            taskRegistrar.setScheduler(taskExecutor());
        }    
        @Bean(destroyMethod="shutdown")
        public Executor taskExecutor() {
            return Executors.newScheduledThreadPool(30);
        }
    }
    

    第三轮压测结果:TPS=30
    第三轮改进方法:
    已经有所进步了,不是嘛,至少比起第一轮结果,我们已经提高10倍了,但是,这合理吗?我们都是insert操作,经过排查加日志,最终定位批量插入4500条记录需要10S以上,我们的插入语句是这样的。(使用MyBatis 3.1.1)

    <insert id="insertBatch">
           insert into xxx (msg_id, balance_type,
               amount, account_id, user_id,
               is_handle, create_time
               ) values
         <foreach collection="list" item="item" index="index" separator=",">
           (#{item.msgId,jdbcType=VARCHAR}, #{item.balanceType,jdbcType=VARCHAR},
               #{item.amount,jdbcType=BIGINT}, #{item.accountId,jdbcType=BIGINT}, #{item.userId,jdbcType=VARCHAR},
               #{item.isHandle,jdbcType=INTEGER}, #{item.createTime,jdbcType=TIMESTAMP}
               )
         </foreach>
         </insert>
    

    使用的了MyBatis foreach语法,生成insert into xxx values (aa,bb), (cc, dd)这种sql语句,经测试,每条sql长度在500K,网络也基本排除延迟的情况下,不禁开始怀疑MyBatis的foreach语法问题,于是将以上SQL由代码生成,再经由MyBatis执行,示例代码如下:

    <insert id="insertBySql" parameterType="com.moext.SqlVo" >
        ${sql}
     </insert>
    

    第四轮压测结果:TPS=300,然后第二批开始下降到100左右
    第四轮改进方法:排除第一批数据与后续批次数据量有明显差距的情况,将问题定位到Mysql,经过定位,原来字段msg_id(值为UUID)存在惟一索引,使用UUID作为惟一索引存在两个问题:
    1、太长
    2、无序,每次insert操作时,索引重建效率不高
    这个字段是由于历史原因存在的,事实上已经用不上了,故删除之。

    第五轮压测结果:TPS=300+

    总结:

    本次性能优化的过程,首先从串行单笔操作转为串行批量操作,增加每次跨进程(服务到DB)交互的数据量;然后通过TPS不稳定发现了SpringBoot中EnableScheduling存在单线程问题;
    最后根据历史经验及测试对比又定位出使用Mybatis的foreache存在的性能问题; 最后根据TPS忽然下降定位出UUID作为惟一索引存在的问题,从而最终将单机串行处理TPS提高到300以上。至此只是了单机性能。如何利用多机进一步提升TPS?请读者先考虑伸缩性设计。后续作专题介绍。

    欢迎转载,转载请务必注明出处
  • 相关阅读:
    hlgoj 1766 Cubing
    Reverse Linked List
    String to Integer
    Bitwise AND of Numbers Range
    Best Time to Buy and Sell Stock III
    First Missing Positive
    Permutation Sequence
    Next Permutation
    Gray Code
    Number of Islands
  • 原文地址:https://www.cnblogs.com/mzsg/p/11977842.html
Copyright © 2011-2022 走看看