数据看板从15s优化到54ms,如何做到?

业务场景

电销系统的数据看板,展示各维度的销售数据:

  • 今日通话量、成单量、成单金额
  • 按团队、按人员的业绩排名
  • 趋势图(最近7天/30天)

问题:页面加载需要15秒,用户体验极差。


性能分析

原始架构

前端请求 → 后端服务 → 实时查询MySQL → 返回结果
                           │
                     多个复杂聚合查询
                     每个查询2-5秒

慢查询分析

-- 今日成单统计(3秒)
SELECT team_id, COUNT(*), SUM(amount)
FROM orders
WHERE create_time >= CURDATE()
GROUP BY team_id;

-- 人员排名(5秒)
SELECT user_id, SUM(amount) as total
FROM orders
WHERE create_time >= CURDATE()
GROUP BY user_id
ORDER BY total DESC
LIMIT 10;

-- 趋势数据(7秒)
SELECT DATE(create_time), SUM(amount)
FROM orders
WHERE create_time >= DATE_SUB(CURDATE(), INTERVAL 30 DAY)
GROUP BY DATE(create_time);

优化策略

策略一:预计算 + 缓存

将复杂聚合提前计算好,存入汇总表或缓存。

// 定时任务:每5分钟预计算
@Scheduled(cron = "0 */5 * * * ?")
public void preComputeDashboard() {
    // 计算各维度数据
    DashboardData data = new DashboardData();
    data.setTodayCalls(orderMapper.countTodayCalls());
    data.setTodayOrders(orderMapper.countTodayOrders());
    data.setTeamRanking(orderMapper.getTeamRanking());
    data.setUserRanking(orderMapper.getUserRanking());
    data.setTrend(orderMapper.getTrendData());
    
    // 存入Redis
    redisTemplate.opsForValue().set("dashboard:data", 
        JSON.toJSONString(data), 6, TimeUnit.MINUTES);
}

// 查询接口:直接读缓存
public DashboardData getDashboard() {
    String json = redisTemplate.opsForValue().get("dashboard:data");
    return JSON.parseObject(json, DashboardData.class);
}

效果:查询时间从15秒降到54ms(只是Redis读取)。

策略二:增量更新

不必每次全量计算,可以增量更新。

// 订单创建时增量更新
@TransactionalEventListener
public void onOrderCreated(OrderCreatedEvent event) {
    Order order = event.getOrder();
    
    // 更新今日统计
    String key = "dashboard:today:" + LocalDate.now();
    redisTemplate.opsForHash().increment(key, "orderCount", 1);
    redisTemplate.opsForHash().increment(key, "totalAmount", order.getAmount());
    
    // 更新团队统计
    String teamKey = "dashboard:team:" + order.getTeamId();
    redisTemplate.opsForHash().increment(teamKey, "amount", order.getAmount());
}

策略三:分层加载

首屏快速展示关键指标,详细数据异步加载。

// 前端分层加载
async function loadDashboard() {
    // 第一层:核心指标(优先加载)
    const summary = await api.getSummary();
    renderSummary(summary);
    
    // 第二层:排行榜(次优先)
    const ranking = await api.getRanking();
    renderRanking(ranking);
    
    // 第三层:趋势图(可延迟)
    const trend = await api.getTrend();
    renderTrend(trend);
}

策略四:物化视图

对于复杂查询,可以使用物化视图。

-- 创建汇总表(每日定时刷新)
CREATE TABLE daily_order_summary (
    stat_date DATE PRIMARY KEY,
    order_count INT,
    total_amount DECIMAL(15,2),
    call_count INT,
    update_time DATETIME
);

-- 定时刷新
INSERT INTO daily_order_summary (stat_date, order_count, total_amount, call_count)
SELECT DATE(create_time), COUNT(*), SUM(amount), SUM(call_count)
FROM orders
WHERE DATE(create_time) = CURDATE()
ON DUPLICATE KEY UPDATE
    order_count = VALUES(order_count),
    total_amount = VALUES(total_amount);

策略五:ES聚合查询

对于维度多、查询灵活的场景,使用ES。

public DashboardData getDashboardFromES() {
    SearchRequest request = new SearchRequest("orders");
    
    SearchSourceBuilder source = new SearchSourceBuilder();
    source.size(0);  // 不需要文档,只要聚合结果
    
    // 按团队聚合
    source.aggregation(AggregationBuilders.terms("by_team")
        .field("team_id")
        .subAggregation(AggregationBuilders.sum("amount").field("amount")));
    
    // 按日期聚合
    source.aggregation(AggregationBuilders.dateHistogram("by_date")
        .field("create_time")
        .calendarInterval(DateHistogramInterval.DAY));
    
    request.source(source);
    return parseResponse(client.search(request));
}

最终架构

前端请求
    │
    ├── 核心指标 → Redis(预计算结果)→ 54ms
    │
    ├── 排行榜 → Redis(增量更新)→ 30ms
    │
    └── 趋势图 → ES聚合查询 → 200ms
    
定时任务
    │
    ├── 每5分钟 → 预计算核心指标
    │
    └── 每天凌晨 → 同步数据到ES

优化效果

指标 优化前 优化后 提升
首屏加载 15秒 54ms 278倍
排行榜 5秒 30ms 167倍
趋势图 7秒 200ms 35倍

面试答题框架

原始问题:实时聚合查询,15秒加载

优化思路:
1. 预计算:定时任务提前算好,存Redis
2. 增量更新:订单创建时实时更新统计
3. 分层加载:首屏优先,详情异步
4. ES聚合:灵活查询走ES

技术选型:
- 核心指标:Redis预计算
- 排行榜:Redis ZSet
- 趋势图:ES或物化视图

效果:15秒 → 54ms

总结

策略 适用场景 效果
预计算缓存 数据变化不频繁 极快
增量更新 实时性要求高 快+实时
物化视图 固定维度报表
ES聚合 灵活多维分析 快+灵活