日报表定时任务优化历程

报表需求背景

报表是一个很常见的需求,在项目中后期往往会需要加多种维度的一些统计信息,今天就来谈谈上线近10个月后的一次报表优化优化之路(从一天报表跑需要五分钟,优化至秒级)
需求:对代理商进行日统计
统计数据:门店数量、设备总数、当日订单数/金额/退款/收益、门店七日新增数、30日0订单门店数量
前置约束:未明确标明指定主库操作 以及 事务,则默认代表走 从库 以及 默认事务

先来看看这一版的流程:

// 以下所有查询/统计 均为从MySQL中获取

按天 开始 循环(任务调度时可指定日期补偿重跑,防止后续定时任务中断,默认跑昨日数据)
    1. 获取所有代理商(大几千个)
        代理商列表 循环开始
    2. 门店统计
        2.1    获取代理名下所有门店列表
        2.2    查询代理近三十天内有订单的门店ID,对比门店列表 得到:30日0订单门店数量
        2.3    获取代理名下七日新增门店
    3. 设备总数统计
    4. 订单统计
        4.1    统计代理昨日订单数/订单金额/退款(订单/收益 均是千万级表)
        4.2    统计代理昨日收益
        代理商列表 循环结束
    5. 新开事务 且 指定主库
        5.1    清理对应日期的统计数据
        5.2    对统计数据进行分批提交(mybatis拼接SQL,千条为一个批次,防止后续当日统计数据过多,导致SQL长度超限)
        5.3    事务提交
按天 结束 循环

以上流程跑当日耗时大约在4-5分钟,乍一看其实并不慢,但此时距离上线已有九月有余,乍一算这个任务得跑20+小时
不管了,能跑就行,先上线再优化

after a long time
午夜惊醒,这玩意得优化哇,这也太不好用了
-_- 还债的时刻到了

第二版

思考:报表任务里都是一些MySQL查询 以及 内存循环对比,且门店统计那块是嵌套循环查询,订单的查询时间也有点长
带着这些思路去排查,发现几个问题:

  1. 每个代理都需要去查询一遍门店统计信息,这里网络IO次数 = 总代理数量
    若每次50ms * 几千,emm,怎么这么多…
  2. 订单的查询某些代理耗时很高,去看了下索引,emm,1 2 3 4 …8 9 10个索引
    了解到MySQL8.0是基于成本模型来生成执行计划的,那么有可能是索引不完全匹配 或 执行计划偏移,下面贴一下SQL与表当前索引
# 订单统计SQL
SELECT
    count( * ) orderTotal,
    sum( pay_amount ) AS orderAmount,
    sum( refund_amount ) AS refundTotal
FROM
    order 
WHERE
    agent_id = #{groupId}
    AND pay_rev_time BETWEEN #{startDate} and #{endDate}    # 这个时间可能会有跨度
    
# 贴下部分索引
uk_order_no            `order_no` ASC
idx_agent_id            `agent_id` ASC
idx_pay_rev_time    `pay_rev_time` ASC
idex_emp            `empower_time` ASC

发现问题,那么就开始一个个尝试改造优化下:

问题一流程优化

1. 分组查询所有代理 门店总数
2. 分组查询所有代理 7 日新增门店数
3. 分组查询所有代理 名下门店总数
4. 分组查询所有代理 近三十天内有订单的门店ID
5. 分组查询所有代理 设备总数
6. 分组查询所有代理 昨日收益金额
按天 开始 循环(任务调度时可指定日期补偿重跑,防止后续定时任务中断,默认跑昨日数据)
    7. 获取所有的代理
        代理商列表 循环开始
            8. 门店统计
                8.1    内存中 获取代理名下所有门店列表(时间复杂度O(1))
                8.2    内存中 查询代理近三十天内有订单的门店ID,对比门店列表 得到:30日0订单门店数量(时间复杂度O(1))
                8.3    内存中 获取代理名下七日新增门店(时间复杂度O(M+N) 代理门店列表 与 有订单门店列表求交集)
            9. 订单统计
                9.1    MySQL 统计代理昨日订单数/订单金额/退款
                9.2    内存中 统计代理昨日收益(时间复杂度O(1))
            10. 内存中 获取设备总数统计(时间复杂度O(1))
            11. 新开事务 且 指定主库
                11.1    清理对应日期的统计数据
                11.2    对统计数据进行分批提交(mybatis拼接SQL,千条为一个批次,防止后续当日统计数据过多,导致SQL长度超限)
                11.3    事务提交
          
        代理商列表 循环结束
按天 结束 循环

至此重跑,发现统计一天的数据已经达到秒级,这里给到一段真实执行时间

问题二SQL优化

看到这里就会有小伙伴有疑问了,为什么上面 9.1流程 中不采用预先一次性统计所有代理数据呢?
这里是为了引出第二个优化方向,不然这不就结束了嘛~~~

修改后打补丁继续执行,又又又失败了…

# 回顾上面的 订单统计SQL,有两个条件,分别是:agent_id、pay_rev_time
# 而这两个字段也分别有自己的独立索引,分别是:idx_agent_id、idx_pay_rev_time

# 那么对于优化器就大概以下几个策略来进行查询:
#     1. 根据 idx_pay_rev_time索引来找到一段时间内数据,然后再根据agent_id 筛选出最终的结果
#     2. 根据 agent_id索引来找到具体代理商的数据,然后再根据pay_rev_time 筛选出最终的结果
#     3. 全表 扫

# 在业务中,使用上述几种方式去查询都将不是最优解,而 agent_id、pay_rev_time又是此SQL的必填条件,
# 此时可以为他们创建一个联合索引:ALTER TABLE order ADD INDEX idx_agentid_paytime (agent_id,pay_rev_time);
# 并且在SQL上强制使用此索引,防止执行计划偏移

SELECT
    count( * ) orderTotal,
    sum( pay_amount ) AS orderAmount,
    sum( refund_amount ) AS refundTotal
FROM
    order force index(idx_agentid_paytime)
WHERE
    agent_id = #{groupId}
    AND pay_rev_time BETWEEN #{startDate} and #{endDate}

后记

问题一流程优化解释

此解题思路实际上是避免了循环查询MySQL,以 一次慢查询 来 优化后续的 多次快查询

但事无绝对,在某些情景下,一次统计的慢查询可能会令系统负载很高,甚至影响到实时业务,那么保持现状:多次快查询 可能会更优

少量多次 与 一次解决,需要根据业务以及系统现状来衡量,有时候快并不是唯一的追求

参考资料

https://dev.mysql.com/doc/refman/8.0/en/cost-model.html
https://www.cnblogs.com/wcwen1990/p/6656611.html

  • 26
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值