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?请读者先考虑伸缩性设计。后续作专题介绍。

    欢迎转载,转载请务必注明出处
  • 相关阅读:
    [转]Android Uri Intent 用法汇总
    [书目20120607]编写高质量代码:改善C#程序的157个建议
    [转]Android多媒体:实现图像的编辑和合成
    [转]Android IPC进程通信——Messager方式
    [转]Android中程序与Service交互的方式——交互方式
    [书目20120605]人力资源管理 余凯成
    [转]SurfaceView horizontal scrolling
    住房乃生活所需
    [转]android service 学习(上) 音乐播放
    [转]Android实现获取本机中所有图片
  • 原文地址:https://www.cnblogs.com/mzsg/p/11977842.html
Copyright © 2011-2022 走看看