🚀 从"数据小作坊"到"数据航母"——百亿级物联网数据平台设计全攻略

58 阅读25分钟

作者:某不愿透露姓名的10年老Java程序员 👨‍💻
难度系数:⭐⭐⭐⭐ (别怕,包教包会!)
适合人群:技术小白 + 架构师 + 面试求职者


📖 序章:一个让人头大的面试题

面试官:(推了推眼镜) "小王啊,假如你要为一个大型物联网平台设计数据存储,每天有数千万设备上报状态数据(温度、位置等),数据量达到TB级别,需要支持百亿级数据查询,你会怎么设计?"

我:(内心OS:这是要我命啊!) 😱

别慌!今天这篇文章就是你的救命稻草。我将用10年的Java开发经验,结合最新的技术方案,带你从零开始设计一个能扛住百亿级数据的物联网平台。保证你看完后,不仅能回答面试题,还能吹上三天三夜!🎉


🎯 第一章:需求分析——知己知彼,百战不殆

1.1 场景描述

想象一下这个场景:

  • 数千万台设备:全国各地的智能温度计、GPS定位器、环境监测仪...
  • 每天TB级数据:每个设备每分钟上报一次数据,一天下来就是天文数字
  • 累计百亿条记录:随着时间推移,数据像滚雪球一样越滚越大

生活类比:就像全国每个人每分钟都要给你发一条微信,告诉你他们现在的体温和位置。一开始还能应付,时间长了,你的手机直接炸了!💥

1.2 核心需求拆解

需求类型具体要求技术挑战难度等级
📊 数据写入每天数千万次写入,峰值可能更高高并发写入、数据不能丢⭐⭐⭐⭐
🔍 查询1查询某个设备最近一年的数据时序查询、数据量大⭐⭐⭐
📈 查询2查询某地区某时间段平均温度聚合查询、多维度⭐⭐⭐⭐⭐
⚡ 实时性查询响应时间 < 3秒查询优化、索引设计⭐⭐⭐⭐
💪 可扩展性设备数量持续增长分布式架构、水平扩展⭐⭐⭐⭐⭐

🏗️ 第二章:整体架构设计——搭建数据航母

2.1 架构全景图

┌─────────────────────────────────────────────────────────────────┐
│                        物联网设备层                               │
│  🌡️ 温度传感器  📍 GPS设备  💨 风速仪  💡 智能灯 ...          │
└────────────────────────┬────────────────────────────────────────┘
                         │ MQTT/HTTP/CoAP
                         ↓
┌─────────────────────────────────────────────────────────────────┐
│                    数据接入层 (Gateway)                           │
│  ● 协议适配  ● 数据校验  ● 限流熔断  ● 身份认证                 │
└────────────────────────┬────────────────────────────────────────┘
                         │
                         ↓
┌─────────────────────────────────────────────────────────────────┐
│                    消息队列层 (Kafka)                             │
│  Topic分区: device-data-01 ~ device-data-100                    │
│  ● 削峰填谷  ● 解耦  ● 持久化  ● 高吞吐                         │
└────────────────┬────────────────────────────────────────────────┘
                 │
         ┌───────┴───────┐
         ↓               ↓
┌──────────────┐  ┌──────────────────┐
│  实时计算层   │  │   批量存储通道    │
│ (Flink/Storm)│  │  (Consumer Group) │
│ ● 实时聚合   │  │  ● 批量写入       │
│ ● 告警监控   │  │  ● 数据清洗       │
└──────┬───────┘  └────────┬─────────┘
       │                   │
       ↓                   ↓
┌─────────────────────────────────────────────────────────────────┐
│                    存储层 (多数据库组合)                          │
│  ┌─────────────────┐  ┌─────────────────┐  ┌────────────────┐  │
│  │  时序数据库      │  │   分析数据库     │  │   缓存层       │  │
│  │  (InfluxDB/     │  │  (ClickHouse/   │  │   (Redis)      │  │
│  │   TDengine)     │  │   Doris)        │  │  ● 热点数据    │  │
│  │  ● 原始数据     │  │  ● 预聚合数据   │  │  ● 查询缓存    │  │
│  │  ● 高压缩比     │  │  ● 快速分析     │  │  ● 过期淘汰    │  │
│  └─────────────────┘  └─────────────────┘  └────────────────┘  │
└─────────────────────────────────────────────────────────────────┘
                         ↑
                         │ 读取
                         │
┌─────────────────────────────────────────────────────────────────┐
│                    查询服务层 (Java)                              │
│  ● SpringBoot  ● MyBatis  ● 分库分表路由  ● 查询优化           │
└────────────────────────┬────────────────────────────────────────┘
                         │ REST API
                         ↓
┌─────────────────────────────────────────────────────────────────┐
│                    应用层 (前端/其他系统)                         │
│  📱 Dashboard  🖥️ 监控中心  📊 数据分析平台                    │
└─────────────────────────────────────────────────────────────────┘

生活类比:这就像一个超级快递物流系统!

  • 设备 = 全国各地的寄件人
  • 接入层 = 快递收件点(检查包裹是否合格)
  • Kafka = 大型分拣中心(暂存、分类)
  • Flink = 实时监控系统(统计今天收了多少件)
  • 存储层 = 各地仓库(长期保管)
  • 查询服务 = 快递查询系统(你想查啥都行)

2.2 为什么这样设计?

🤔 疑问1:为什么要用Kafka?

错误做法:设备直接写数据库

// ❌ 错误示范:扛不住并发!
@PostMapping("/device/report")
public void reportData(@RequestBody DeviceData data) {
    deviceDataMapper.insert(data); // 数据库直接崩溃!
}

正确做法:设备 → Kafka → 数据库

// ✅ 正确姿势:削峰填谷
@PostMapping("/device/report")
public void reportData(@RequestBody DeviceData data) {
    // 先发到Kafka,异步写入
    kafkaTemplate.send("device-data", data);
    return "OK"; // 秒级响应
}

原因

  • Kafka能撑住每秒百万级写入
  • 数据库慢慢消费,不会被打死
  • 就像把汹涌的洪水引入水库,慢慢发电 💧⚡

🤔 疑问2:为什么用时序数据库而不是MySQL?

对比项MySQL时序数据库(InfluxDB)优势倍数
写入速度1万/秒100万/秒100倍
压缩比1:31:207倍
时序查询需要索引优化原生支持10倍
存储成本100TB5TB省95%

生活类比

  • MySQL = 普通记事本(啥都能记,但不专业)
  • 时序数据库 = 专业日记本(按时间排列,带目录,查起来贼快)

💾 第三章:存储方案详解——数据住在哪?

3.1 冷热数据分离策略

概念:不是所有数据都需要住"市中心"的!

┌─────────────────────────────────────────┐
│          热数据 (Hot Data)               │  
│  ● 最近7天的数据                         │
│  ● 存储在 SSD + 时序数据库               │
│  ● 查询速度:<100ms                      │
│  ● 占比:5% 数据量                       │
│  ● 查询量:95% 的请求                    │
└─────────────────────────────────────────┘
                  ↓ 7天后自动归档
┌─────────────────────────────────────────┐
│          温数据 (Warm Data)              │
│  ● 7天-3个月的数据                       │
│  ● 存储在 HDD + 时序数据库               │
│  ● 查询速度:<1s                         │
│  ● 占比:15% 数据量                      │
│  ● 查询量:4% 的请求                     │
└─────────────────────────────────────────┘
                  ↓ 3个月后压缩归档
┌─────────────────────────────────────────┐
│          冷数据 (Cold Data)              │
│  ● 3个月以上的历史数据                   │
│  ● 存储在 对象存储(OSS/S3) + Parquet格式 │
│  ● 查询速度:<5s                         │
│  ● 占比:80% 数据量                      │
│  ● 查询量:1% 的请求                     │
└─────────────────────────────────────────┘

代码实现

/**
 * 智能路由:根据查询时间范围选择存储
 */
@Service
public class DataQueryRouter {
    
    @Autowired
    private InfluxDBTemplate influxDB;  // 热数据
    
    @Autowired
    private ClickHouseTemplate clickHouse;  // 温数据
    
    @Autowired
    private S3ParquetReader s3Reader;  // 冷数据
    
    /**
     * 查询设备历史数据
     */
    public List<DeviceData> queryDeviceData(
            String deviceId, 
            LocalDateTime startTime, 
            LocalDateTime endTime) {
        
        List<DeviceData> result = new ArrayList<>();
        LocalDateTime now = LocalDateTime.now();
        
        // 智能路由逻辑
        if (startTime.isAfter(now.minusDays(7))) {
            // 全部在热数据范围
            return queryFromInfluxDB(deviceId, startTime, endTime);
            
        } else if (startTime.isAfter(now.minusMonths(3))) {
            // 可能跨热、温数据
            result.addAll(queryFromInfluxDB(deviceId, 
                Math.max(startTime, now.minusDays(7)), endTime));
            result.addAll(queryFromClickHouse(deviceId, 
                startTime, now.minusDays(7)));
                
        } else {
            // 需要查询所有层
            // 热数据
            if (endTime.isAfter(now.minusDays(7))) {
                result.addAll(queryFromInfluxDB(deviceId, 
                    now.minusDays(7), endTime));
            }
            // 温数据
            if (startTime.isBefore(now.minusDays(7)) && 
                endTime.isAfter(now.minusMonths(3))) {
                result.addAll(queryFromClickHouse(deviceId,
                    Math.max(startTime, now.minusMonths(3)), 
                    Math.min(endTime, now.minusDays(7))));
            }
            // 冷数据
            if (startTime.isBefore(now.minusMonths(3))) {
                result.addAll(queryFromS3(deviceId, 
                    startTime, 
                    Math.min(endTime, now.minusMonths(3))));
            }
        }
        
        // 合并排序
        return result.stream()
            .sorted(Comparator.comparing(DeviceData::getTimestamp))
            .collect(Collectors.toList());
    }
}

生活类比

  • 热数据 = 放在书桌上的今天要用的文件(拿起来就能看)
  • 温数据 = 放在抽屉里的最近几个月的文件(翻一翻就找到)
  • 冷数据 = 放在地下室的多年前的文件(得费点劲,但能找到)

3.2 分库分表策略

为什么要分库分表?

单表极限

  • MySQL单表建议 < 2000万行
  • 我们每天产生上亿条数据
  • 不分表 = 等死 💀

分表策略:按设备ID + 时间

/**
 * 分表规则:
 * - 按设备ID的hash值分1024张表(解决单表数据量问题)
 * - 按月份分区(解决时序查询问题)
 * 
 * 表名示例:device_data_0512_202410
 *          device_data_{hash}_{YYYYMM}
 */
@Component
public class ShardingStrategy {
    
    private static final int TABLE_COUNT = 1024;  // 2^10,方便取模
    
    /**
     * 计算设备应该落在哪张表
     */
    public String getTableName(String deviceId, LocalDateTime time) {
        // 1. 根据设备ID计算hash
        int hash = Math.abs(deviceId.hashCode() % TABLE_COUNT);
        String hashStr = String.format("%04d", hash);  // 补零到4位
        
        // 2. 根据时间确定月份分区
        String monthStr = time.format(DateTimeFormatter.ofPattern("yyyyMM"));
        
        // 3. 组合表名
        return String.format("device_data_%s_%s", hashStr, monthStr);
    }
    
    /**
     * 查询最近一年数据需要扫描的表
     */
    public List<String> getTableNames(String deviceId, 
                                      LocalDateTime startTime, 
                                      LocalDateTime endTime) {
        List<String> tables = new ArrayList<>();
        int hash = Math.abs(deviceId.hashCode() % TABLE_COUNT);
        String hashStr = String.format("%04d", hash);
        
        // 遍历时间范围内的所有月份
        LocalDateTime current = startTime.withDayOfMonth(1);
        while (current.isBefore(endTime) || current.equals(endTime)) {
            String monthStr = current.format(
                DateTimeFormatter.ofPattern("yyyyMM"));
            tables.add(String.format("device_data_%s_%s", hashStr, monthStr));
            current = current.plusMonths(1);
        }
        
        return tables;
    }
}

示例:查询设备"ABC123"在2024年的数据

// 设备ID: ABC123
// hash("ABC123") % 1024 = 512

// 需要查询的表:
// device_data_0512_202401
// device_data_0512_202402
// device_data_0512_202403
// ...
// device_data_0512_202412

// 只需要查12张表,而不是1024 * 12 = 12288张表!

分表收益

维度单表分1024表效果
单表数据量100亿1000万✅ 降低1000倍
查询扫描范围全表单表单月✅ 缩小10000倍
并发写入能力1万/秒100万/秒✅ 提升100倍
索引大小500GB500MB✅ 内存放得下

3.3 索引设计

-- InfluxDB 的索引策略(Tag自动建索引)
-- Measurement: device_data
-- Tags (索引字段):
--   - device_id: 设备ID
--   - region: 地区
--   - device_type: 设备类型
-- Fields (值字段):
--   - temperature: 温度
--   - longitude: 经度
--   - latitude: 纬度
-- Timestamp: 时间戳(自动索引)

-- 查询1:某设备最近一年数据(命中索引)
SELECT * FROM device_data 
WHERE device_id = 'ABC123' 
  AND time > now() - 365d;
-- 索引路径:device_id索引 → 时间范围过滤 → 高效!

-- 查询2:某地区时间段平均温度(命中索引)
SELECT MEAN(temperature) FROM device_data
WHERE region = 'Beijing'
  AND time >= '2024-01-01'
  AND time < '2025-01-01'
GROUP BY time(1d);
-- 索引路径:region索引 → 时间范围过滤 → 按天聚合 → 高效!

⚡ 第四章:查询优化——让查询飞起来

4.1 预聚合:空间换时间的艺术

问题:每次都实时计算"北京市2024年每天的平均温度"太慢了!

解决方案:提前算好,存起来!

/**
 * 使用Flink实时计算预聚合
 */
@Component
public class PreAggregationJob {
    
    public static void main(String[] args) throws Exception {
        StreamExecutionEnvironment env = 
            StreamExecutionEnvironment.getExecutionEnvironment();
        
        // 1. 从Kafka读取原始数据
        DataStream<DeviceData> rawData = env
            .addSource(new FlinkKafkaConsumer<>("device-data", ...))
            .assignTimestampsAndWatermarks(...);  // 处理乱序
        
        // 2. 按地区、天维度聚合
        DataStream<RegionDailyStats> dailyStats = rawData
            .keyBy(data -> data.getRegion())  // 按地区分组
            .window(TumblingEventTimeWindows.of(Time.days(1)))  // 按天窗口
            .aggregate(new AggregateFunction<DeviceData, Accumulator, RegionDailyStats>() {
                @Override
                public Accumulator createAccumulator() {
                    return new Accumulator();  // 累加器
                }
                
                @Override
                public Accumulator add(DeviceData value, Accumulator acc) {
                    acc.sum += value.getTemperature();
                    acc.count++;
                    acc.min = Math.min(acc.min, value.getTemperature());
                    acc.max = Math.max(acc.max, value.getTemperature());
                    return acc;
                }
                
                @Override
                public RegionDailyStats getResult(Accumulator acc) {
                    return new RegionDailyStats(
                        acc.region,
                        acc.date,
                        acc.sum / acc.count,  // 平均值
                        acc.min,
                        acc.max,
                        acc.count
                    );
                }
                
                @Override
                public Accumulator merge(Accumulator a, Accumulator b) {
                    // 合并两个累加器
                    a.sum += b.sum;
                    a.count += b.count;
                    a.min = Math.min(a.min, b.min);
                    a.max = Math.max(a.max, b.max);
                    return a;
                }
            });
        
        // 3. 写入ClickHouse(OLAP数据库)
        dailyStats.addSink(new ClickHouseSink());
        
        env.execute("Pre-Aggregation Job");
    }
}

效果对比

场景不预聚合预聚合提升
查询北京年平均温度扫描10亿行,耗时30秒查询365行,耗时0.1秒300倍 🚀
存储空间10TB10TB + 100MB几乎无额外成本

生活类比

  • 不预聚合 = 每次都重新数全班同学的成绩求平均分
  • 预聚合 = 老师已经算好了每科的平均分,写在黑板上

4.2 多级缓存策略

┌─────────────────────────────────────┐
         客户端请求                   
└──────────────┬──────────────────────┘
               
┌──────────────────────────────────────┐
  L1: 本地缓存 (Caffeine)              
   容量:10000条                      
   TTL:5分钟                         
   命中率:60%                        
   响应时间:<1ms                     
└─────────┬──────────────────┬─────────┘
           Miss             Hit (返回)
┌──────────────────────────────────────┐
  L2: 分布式缓存 (Redis)               
   容量:100万条                      
   TTL:1小时                         
   命中率:30%                        
   响应时间:<10ms                    
└─────────┬──────────────────┬─────────┘
           Miss             Hit (返回)
┌──────────────────────────────────────┐
  L3: 数据库查询                       
   命中率:10%                        
   响应时间:<500ms                   
   查询后写入L1、L2缓存               
└──────────────────────────────────────┘

代码实现

@Service
public class CachedQueryService {
    
    // L1: 本地缓存
    private final Cache<String, List<DeviceData>> localCache = 
        Caffeine.newBuilder()
            .maximumSize(10000)
            .expireAfterWrite(5, TimeUnit.MINUTES)
            .recordStats()  // 记录命中率
            .build();
    
    // L2: Redis缓存
    @Autowired
    private RedisTemplate<String, List<DeviceData>> redisTemplate;
    
    // L3: 数据库
    @Autowired
    private DeviceDataRepository repository;
    
    /**
     * 查询设备数据(三级缓存)
     */
    public List<DeviceData> queryDeviceData(String deviceId, 
                                            LocalDateTime start, 
                                            LocalDateTime end) {
        String cacheKey = buildCacheKey(deviceId, start, end);
        
        // L1: 本地缓存
        List<DeviceData> data = localCache.getIfPresent(cacheKey);
        if (data != null) {
            log.info("L1 Cache Hit: {}", cacheKey);
            return data;
        }
        
        // L2: Redis缓存
        data = redisTemplate.opsForValue().get(cacheKey);
        if (data != null) {
            log.info("L2 Cache Hit: {}", cacheKey);
            // 回写L1
            localCache.put(cacheKey, data);
            return data;
        }
        
        // L3: 数据库查询
        log.info("Cache Miss, Query from DB: {}", cacheKey);
        data = repository.findByDeviceIdAndTimeRange(deviceId, start, end);
        
        // 写入缓存(异步,不阻塞主流程)
        CompletableFuture.runAsync(() -> {
            localCache.put(cacheKey, data);
            redisTemplate.opsForValue().set(cacheKey, data, 
                1, TimeUnit.HOURS);
        });
        
        return data;
    }
    
    private String buildCacheKey(String deviceId, 
                                 LocalDateTime start, 
                                 LocalDateTime end) {
        return String.format("device:%s:%d:%d", 
            deviceId, 
            start.toEpochSecond(ZoneOffset.UTC),
            end.toEpochSecond(ZoneOffset.UTC));
    }
}

性能对比

缓存层级QPSP99延迟成本
无缓存(直接查DB)1000800ms💰💰💰
只有Redis缓存1000050ms💰💰
本地+Redis两级缓存1000005ms💰

4.3 查询改写与优化

原始查询(慢):

// ❌ 问题:没有限制返回数量,可能查出百万条数据
public List<DeviceData> getDeviceHistory(String deviceId) {
    return deviceDataMapper.selectByDeviceId(deviceId);
}

优化后(快):

// ✅ 优化1:分页查询
public Page<DeviceData> getDeviceHistory(String deviceId, int page, int size) {
    // 限制每页最多1000条
    size = Math.min(size, 1000);
    return deviceDataMapper.selectByDeviceIdWithPage(deviceId, page, size);
}

// ✅ 优化2:只查询必要的字段
public List<DeviceDataVO> getDeviceHistorySimple(String deviceId) {
    // 不要 SELECT *,只查需要的字段
    return deviceDataMapper.selectDeviceDataSimple(deviceId);
}

// ✅ 优化3:使用游标查询大数据量
public void exportDeviceHistory(String deviceId, OutputStream output) {
    // 使用游标,避免一次性加载所有数据到内存
    try (Cursor<DeviceData> cursor = 
            deviceDataMapper.selectByDeviceIdCursor(deviceId)) {
        cursor.forEach(data -> {
            // 逐条写入文件,内存占用恒定
            writeToOutput(data, output);
        });
    }
}

🔥 第五章:实战案例——两个核心查询的实现

5.1 查询1:某设备最近一年的数据

需求分析

  • 数据量预估:1设备 × 365天 × 24小时 × 60分钟 = 52万条记录
  • 查询频率:高频(用户经常查看设备历史)
  • 优化策略:分页 + 缓存 + 索引

完整实现

/**
 * Controller层:提供RESTful API
 */
@RestController
@RequestMapping("/api/device")
public class DeviceDataController {
    
    @Autowired
    private DeviceQueryService queryService;
    
    /**
     * 查询设备历史数据
     * GET /api/device/ABC123/history?page=1&size=100
     */
    @GetMapping("/{deviceId}/history")
    public Result<PageData<DeviceDataVO>> getDeviceHistory(
            @PathVariable String deviceId,
            @RequestParam(defaultValue = "1") int page,
            @RequestParam(defaultValue = "100") int size,
            @RequestParam(required = false) Long startTime,
            @RequestParam(required = false) Long endTime) {
        
        // 参数校验
        if (size > 1000) {
            return Result.error("单页最多返回1000条数据");
        }
        
        // 默认查询最近一年
        LocalDateTime end = endTime != null 
            ? LocalDateTime.ofEpochSecond(endTime, 0, ZoneOffset.UTC)
            : LocalDateTime.now();
        LocalDateTime start = startTime != null
            ? LocalDateTime.ofEpochSecond(startTime, 0, ZoneOffset.UTC)
            : end.minusYears(1);
        
        // 查询数据
        PageData<DeviceDataVO> data = queryService.queryDeviceHistory(
            deviceId, start, end, page, size);
        
        return Result.success(data);
    }
}

/**
 * Service层:业务逻辑
 */
@Service
@Slf4j
public class DeviceQueryService {
    
    @Autowired
    private InfluxDBTemplate influxDB;
    
    @Autowired
    private RedisTemplate<String, PageData> redisTemplate;
    
    /**
     * 查询设备历史数据(带缓存)
     */
    public PageData<DeviceDataVO> queryDeviceHistory(
            String deviceId, 
            LocalDateTime start, 
            LocalDateTime end,
            int page, 
            int size) {
        
        // 1. 构建缓存Key
        String cacheKey = String.format("device:history:%s:%d:%d:%d:%d",
            deviceId, 
            start.toEpochSecond(ZoneOffset.UTC),
            end.toEpochSecond(ZoneOffset.UTC),
            page, size);
        
        // 2. 查询缓存
        PageData<DeviceDataVO> cached = redisTemplate.opsForValue().get(cacheKey);
        if (cached != null) {
            log.info("Cache hit for device: {}", deviceId);
            return cached;
        }
        
        // 3. 查询InfluxDB
        String query = String.format(
            "SELECT * FROM device_data " +
            "WHERE device_id = '%s' " +
            "AND time >= '%s' " +
            "AND time < '%s' " +
            "ORDER BY time DESC " +
            "LIMIT %d OFFSET %d",
            deviceId,
            start.format(DateTimeFormatter.ISO_DATE_TIME),
            end.format(DateTimeFormatter.ISO_DATE_TIME),
            size,
            (page - 1) * size
        );
        
        QueryResult result = influxDB.query(new Query(query, "iot_db"));
        
        // 4. 转换数据
        List<DeviceDataVO> dataList = convertToVO(result);
        
        // 5. 查询总数(用于分页)
        long total = queryTotalCount(deviceId, start, end);
        
        PageData<DeviceDataVO> pageData = new PageData<>(
            dataList, total, page, size);
        
        // 6. 写入缓存(5分钟过期)
        redisTemplate.opsForValue().set(cacheKey, pageData, 
            5, TimeUnit.MINUTES);
        
        return pageData;
    }
    
    /**
     * 查询总数(带缓存)
     */
    private long queryTotalCount(String deviceId, 
                                  LocalDateTime start, 
                                  LocalDateTime end) {
        String countKey = String.format("device:count:%s:%d:%d",
            deviceId,
            start.toEpochSecond(ZoneOffset.UTC),
            end.toEpochSecond(ZoneOffset.UTC));
        
        Long cached = redisTemplate.opsForValue().get(countKey);
        if (cached != null) {
            return cached;
        }
        
        String countQuery = String.format(
            "SELECT COUNT(*) FROM device_data " +
            "WHERE device_id = '%s' " +
            "AND time >= '%s' " +
            "AND time < '%s'",
            deviceId,
            start.format(DateTimeFormatter.ISO_DATE_TIME),
            end.format(DateTimeFormatter.ISO_DATE_TIME)
        );
        
        QueryResult result = influxDB.query(new Query(countQuery, "iot_db"));
        long total = extractCount(result);
        
        // 缓存1小时
        redisTemplate.opsForValue().set(countKey, total, 
            1, TimeUnit.HOURS);
        
        return total;
    }
}

优化效果

指标优化前优化后提升
响应时间2000ms50ms40倍
数据库QPS500100减轻5倍压力
可支持并发5002000040倍

5.2 查询2:某地区某时间段平均温度

需求分析

  • 数据量预估:1个地区 × 365天 × 10万设备 = 36.5亿条记录
  • 查询类型:聚合查询(计算平均值)
  • 优化策略:预聚合 + OLAP数据库

完整实现

Step1:实时预聚合(Flink)

/**
 * Flink实时聚合任务
 */
public class RegionTemperatureAggregation {
    
    public static void main(String[] args) throws Exception {
        StreamExecutionEnvironment env = 
            StreamExecutionEnvironment.getExecutionEnvironment();
        
        // 1. 从Kafka读取原始数据
        FlinkKafkaConsumer<DeviceData> consumer = new FlinkKafkaConsumer<>(
            "device-data",
            new DeviceDataSchema(),
            kafkaProps);
        
        DataStream<DeviceData> dataStream = env
            .addSource(consumer)
            .assignTimestampsAndWatermarks(
                WatermarkStrategy
                    .<DeviceData>forBoundedOutOfOrderness(Duration.ofMinutes(1))
                    .withTimestampAssigner((event, timestamp) -> 
                        event.getTimestamp().toEpochMilli())
            );
        
        // 2. 按地区、小时聚合
        DataStream<RegionHourlyStats> hourlyStats = dataStream
            .filter(data -> data.getTemperature() != null)  // 过滤无效数据
            .keyBy(DeviceData::getRegion)  // 按地区分组
            .window(TumblingEventTimeWindows.of(Time.hours(1)))  // 小时窗口
            .aggregate(new HourlyTemperatureAggregator());
        
        // 3. 写入ClickHouse
        hourlyStats.addSink(
            JdbcSink.sink(
                "INSERT INTO region_hourly_stats " +
                "(region, hour, avg_temp, min_temp, max_temp, sample_count) " +
                "VALUES (?, ?, ?, ?, ?, ?)",
                (ps, stats) -> {
                    ps.setString(1, stats.getRegion());
                    ps.setTimestamp(2, Timestamp.valueOf(stats.getHour()));
                    ps.setDouble(3, stats.getAvgTemp());
                    ps.setDouble(4, stats.getMinTemp());
                    ps.setDouble(5, stats.getMaxTemp());
                    ps.setLong(6, stats.getSampleCount());
                },
                JdbcExecutionOptions.builder()
                    .withBatchSize(1000)
                    .withBatchIntervalMs(5000)
                    .build(),
                new JdbcConnectionOptions.JdbcConnectionOptionsBuilder()
                    .withUrl("jdbc:clickhouse://localhost:8123/iot")
                    .withDriverName("ru.yandex.clickhouse.ClickHouseDriver")
                    .build()
            )
        );
        
        env.execute("Region Temperature Aggregation");
    }
    
    /**
     * 温度聚合器
     */
    static class HourlyTemperatureAggregator 
            implements AggregateFunction<DeviceData, TempAccumulator, RegionHourlyStats> {
        
        @Override
        public TempAccumulator createAccumulator() {
            return new TempAccumulator();
        }
        
        @Override
        public TempAccumulator add(DeviceData value, TempAccumulator acc) {
            acc.sum += value.getTemperature();
            acc.count++;
            acc.min = Math.min(acc.min, value.getTemperature());
            acc.max = Math.max(acc.max, value.getTemperature());
            acc.region = value.getRegion();
            acc.hour = value.getTimestamp().truncatedTo(ChronoUnit.HOURS);
            return acc;
        }
        
        @Override
        public RegionHourlyStats getResult(TempAccumulator acc) {
            return new RegionHourlyStats(
                acc.region,
                acc.hour,
                acc.sum / acc.count,  // 平均温度
                acc.min,
                acc.max,
                acc.count
            );
        }
        
        @Override
        public TempAccumulator merge(TempAccumulator a, TempAccumulator b) {
            a.sum += b.sum;
            a.count += b.count;
            a.min = Math.min(a.min, b.min);
            a.max = Math.max(a.max, b.max);
            return a;
        }
    }
}

Step2:ClickHouse表结构

-- 创建预聚合表
CREATE TABLE region_hourly_stats (
    region String,              -- 地区
    hour DateTime,              -- 小时(2024-10-20 15:00:00)
    avg_temp Float64,           -- 平均温度
    min_temp Float64,           -- 最低温度
    max_temp Float64,           -- 最高温度
    sample_count UInt64,        -- 样本数量
    update_time DateTime DEFAULT now()
) ENGINE = MergeTree()
PARTITION BY toYYYYMM(hour)    -- 按月分区
ORDER BY (region, hour)         -- 排序键(自动索引)
SETTINGS index_granularity = 8192;

-- 创建物化视图:自动聚合到天级别
CREATE MATERIALIZED VIEW region_daily_stats_mv
ENGINE = SummingMergeTree()
PARTITION BY toYYYYMM(day)
ORDER BY (region, day)
AS SELECT
    region,
    toDate(hour) as day,
    sum(avg_temp * sample_count) / sum(sample_count) as avg_temp,
    min(min_temp) as min_temp,
    max(max_temp) as max_temp,
    sum(sample_count) as sample_count
FROM region_hourly_stats
GROUP BY region, day;

Step3:查询服务

/**
 * 地区温度查询服务
 */
@Service
public class RegionTemperatureService {
    
    @Autowired
    private JdbcTemplate clickHouseTemplate;
    
    @Autowired
    private RedisTemplate<String, RegionTempStats> redisTemplate;
    
    /**
     * 查询地区平均温度(智能路由)
     */
    public RegionTempStats queryRegionAvgTemperature(
            String region,
            LocalDateTime startTime,
            LocalDateTime endTime) {
        
        // 1. 检查缓存
        String cacheKey = String.format("region:temp:%s:%d:%d",
            region,
            startTime.toEpochSecond(ZoneOffset.UTC),
            endTime.toEpochSecond(ZoneOffset.UTC));
        
        RegionTempStats cached = redisTemplate.opsForValue().get(cacheKey);
        if (cached != null) {
            return cached;
        }
        
        // 2. 选择查询粒度
        Duration duration = Duration.between(startTime, endTime);
        String sql;
        
        if (duration.toDays() <= 7) {
            // 一周内:查小时级数据
            sql = "SELECT " +
                  "  region, " +
                  "  sum(avg_temp * sample_count) / sum(sample_count) as avg_temp, " +
                  "  min(min_temp) as min_temp, " +
                  "  max(max_temp) as max_temp, " +
                  "  sum(sample_count) as total_samples " +
                  "FROM region_hourly_stats " +
                  "WHERE region = ? " +
                  "  AND hour >= ? " +
                  "  AND hour < ? " +
                  "GROUP BY region";
        } else {
            // 超过一周:查天级预聚合数据
            sql = "SELECT " +
                  "  region, " +
                  "  avg(avg_temp) as avg_temp, " +
                  "  min(min_temp) as min_temp, " +
                  "  max(max_temp) as max_temp, " +
                  "  sum(sample_count) as total_samples " +
                  "FROM region_daily_stats_mv " +
                  "WHERE region = ? " +
                  "  AND day >= ? " +
                  "  AND day < ? " +
                  "GROUP BY region";
        }
        
        // 3. 执行查询
        RegionTempStats result = clickHouseTemplate.queryForObject(
            sql,
            new Object[]{region, startTime, endTime},
            (rs, rowNum) -> new RegionTempStats(
                rs.getString("region"),
                rs.getDouble("avg_temp"),
                rs.getDouble("min_temp"),
                rs.getDouble("max_temp"),
                rs.getLong("total_samples")
            )
        );
        
        // 4. 写入缓存
        long ttl = duration.toDays() > 30 ? 24 : 1;  // 历史数据缓存更久
        redisTemplate.opsForValue().set(cacheKey, result, 
            ttl, TimeUnit.HOURS);
        
        return result;
    }
    
    /**
     * 查询地区温度趋势(按天)
     */
    public List<DailyTempPoint> queryRegionTempTrend(
            String region,
            LocalDateTime startTime,
            LocalDateTime endTime) {
        
        String sql = 
            "SELECT " +
            "  day, " +
            "  avg_temp, " +
            "  min_temp, " +
            "  max_temp " +
            "FROM region_daily_stats_mv " +
            "WHERE region = ? " +
            "  AND day >= ? " +
            "  AND day < ? " +
            "ORDER BY day";
        
        return clickHouseTemplate.query(
            sql,
            new Object[]{region, startTime.toLocalDate(), endTime.toLocalDate()},
            (rs, rowNum) -> new DailyTempPoint(
                rs.getDate("day").toLocalDate(),
                rs.getDouble("avg_temp"),
                rs.getDouble("min_temp"),
                rs.getDouble("max_temp")
            )
        );
    }
}

性能对比

场景方案扫描数据量查询时间备注
查询北京市1月平均温度直接查原始数据3.1亿行30秒❌ 太慢
查询北京市1月平均温度小时级预聚合744行50ms✅ 快!
查询北京市全年平均温度小时级预聚合8760行100ms✅ 还行
查询北京市全年平均温度天级预聚合365行20ms✅ 更快!

生活类比

  • 不预聚合 = 每次都从头计算全班同学的平均分
  • 小时级预聚合 = 每节课后老师算好这节课的平均分
  • 天级预聚合 = 每天放学后算好当天所有课的平均分
  • 查询 = 直接看黑板上的统计结果

🛡️ 第六章:高可用与容灾

6.1 分布式部署架构

                    ┌─────────────────┐
                    │   负载均衡器     │
                    │   (Nginx/LVS)   │
                    └────────┬────────┘
                             │
        ┌────────────────────┼────────────────────┐
        │                    │                    │
        ↓                    ↓                    ↓
┌──────────────┐    ┌──────────────┐    ┌──────────────┐
│ 查询服务节点1 │    │ 查询服务节点2 │    │ 查询服务节点3 │
│ (无状态)     │    │ (无状态)     │    │ (无状态)     │
└──────┬───────┘    └──────┬───────┘    └──────┬───────┘
       │                   │                    │
       └───────────────────┼────────────────────┘
                           ↓
              ┌────────────────────────┐
              │   Redis Cluster        │
              │   (主从+哨兵模式)      │
              │   33从              │
              └────────────────────────┘
                           ↓
       ┌───────────────────┼────────────────────┐
       │                   │                    │
       ↓                   ↓                    ↓
┌─────────────┐    ┌─────────────┐    ┌─────────────┐
│ InfluxDB 1  │    │ InfluxDB 2  │    │ InfluxDB 3  │
│ (主)        │───→│ (从)        │    │ (从)        │
│ 北京机房    │    │ 上海机房    │    │ 深圳机房    │
└─────────────┘    └─────────────┘    └─────────────┘

┌──────────────────────────────────────────────────────┐
│             ClickHouse 集群 (3分片2副本)              │
│                                                       │
│  Shard1-Replica1  Shard2-Replica1  Shard3-Replica1  │
│  Shard1-Replica2  Shard2-Replica2  Shard3-Replica2  │
└──────────────────────────────────────────────────────┘

6.2 故障处理策略

/**
 * 容错查询服务
 */
@Service
public class FaultTolerantQueryService {
    
    @Autowired
    private InfluxDBTemplate primaryDB;
    
    @Autowired
    private InfluxDBTemplate secondaryDB;
    
    @Autowired
    private CircuitBreakerRegistry circuitBreakerRegistry;
    
    /**
     * 查询设备数据(带熔断和降级)
     */
    public List<DeviceData> queryWithFaultTolerance(
            String deviceId,
            LocalDateTime start,
            LocalDateTime end) {
        
        // 获取熔断器
        CircuitBreaker breaker = circuitBreakerRegistry.circuitBreaker("influxdb");
        
        try {
            // 主库查询(带熔断保护)
            return breaker.executeSupplier(() -> 
                primaryDB.query(deviceId, start, end));
                
        } catch (CallNotPermittedException e) {
            // 熔断器打开,直接走降级
            log.warn("Circuit breaker is OPEN, using fallback");
            return queryFromSecondary(deviceId, start, end);
            
        } catch (Exception e) {
            // 主库查询失败,尝试从备库查询
            log.error("Primary DB query failed, trying secondary", e);
            return queryFromSecondary(deviceId, start, end);
        }
    }
    
    /**
     * 从备库查询(降级方案)
     */
    private List<DeviceData> queryFromSecondary(
            String deviceId,
            LocalDateTime start,
            LocalDateTime end) {
        try {
            return secondaryDB.query(deviceId, start, end);
        } catch (Exception e) {
            log.error("Secondary DB also failed, returning empty", e);
            // 所有数据库都挂了,返回空数据+告警
            alertService.sendAlert("All databases are down!");
            return Collections.emptyList();
        }
    }
}

/**
 * 熔断器配置
 */
@Configuration
public class ResilienceConfig {
    
    @Bean
    public CircuitBreakerRegistry circuitBreakerRegistry() {
        CircuitBreakerConfig config = CircuitBreakerConfig.custom()
            .failureRateThreshold(50)  // 失败率超过50%打开熔断器
            .waitDurationInOpenState(Duration.ofSeconds(30))  // 30秒后尝试恢复
            .slidingWindowSize(10)  // 滑动窗口大小
            .minimumNumberOfCalls(5)  // 最少调用5次才开始统计
            .build();
        
        return CircuitBreakerRegistry.of(config);
    }
}

6.3 数据备份策略

数据类型备份方式备份频率保留时间存储位置
热数据实时同步实时7天同城双活
温数据增量备份每小时3个月异地机房
冷数据全量备份每天永久对象存储(OSS)
预聚合数据快照备份每天1年对象存储

📊 第七章:监控与运维

7.1 关键指标监控

/**
 * 监控指标采集
 */
@Component
public class MetricsCollector {
    
    @Autowired
    private MeterRegistry meterRegistry;
    
    /**
     * 记录查询延迟
     */
    public void recordQueryLatency(String queryType, long latencyMs) {
        Timer.builder("query.latency")
            .tag("type", queryType)
            .register(meterRegistry)
            .record(latencyMs, TimeUnit.MILLISECONDS);
    }
    
    /**
     * 记录写入QPS
     */
    public void recordWriteQPS(long count) {
        Counter.builder("write.qps")
            .register(meterRegistry)
            .increment(count);
    }
    
    /**
     * 记录缓存命中率
     */
    public void recordCacheHitRate(boolean hit) {
        Counter.builder("cache.requests")
            .tag("result", hit ? "hit" : "miss")
            .register(meterRegistry)
            .increment();
    }
}

监控大盘

┌───────────────────────────────────────────────────────┐
│            物联网平台监控大盘                          │
├───────────────────────────────────────────────────────┤
│ 📊 实时指标                                            │
│   ● 在线设备数:28,537,621 台                          │
│   ● 每秒写入:847,329 条/秒                            │
│   ● 每秒查询:12,458 次/秒                             │
│   ● 查询P99延迟:156 ms                                │
│   ● 缓存命中率:94.3%                                  │
├───────────────────────────────────────────────────────┤
│ 💾 存储状态                                            │
│   ● 热数据:8.7 TB (InfluxDB)                         │
│   ● 温数据:127 TB (ClickHouse)                       │
│   ● 冷数据:2.3 PB (OSS)                              │
│   ● 磁盘使用率:67%                                    │
├───────────────────────────────────────────────────────┤
│ ⚠️  告警 (最近24小时)                                  │
│   ● [WARN] 北京机房InfluxDB磁盘使用率超过80%           │
│   ● [INFO] 定时任务"数据归档"执行成功                  │
└───────────────────────────────────────────────────────┘

💡 第八章:成本优化

8.1 成本构成分析

总成本:约 50万/月

┌────────────────────────────────────┐
│  服务器成本:30万 (60%)            │
│  ├─ InfluxDB集群:8台高配 (12万)  │
│  ├─ ClickHouse集群:6台 (10万)    │
│  ├─ Kafka集群:4台 (4万)          │
│  └─ 应用服务器:20台 (4万)        │
├────────────────────────────────────┤
│  存储成本:15万 (30%)              │
│  ├─ SSD存储(热数据):5万        │
│  ├─ HDD存储(温数据):3万        │
│  └─ OSS存储(冷数据):7万        │
├────────────────────────────────────┤
│  带宽成本:3万 (6%)                │
├────────────────────────────────────┤
│  其他(监控、备份等):2万 (4%)    │
└────────────────────────────────────┘

8.2 优化策略

优化1:数据压缩

// InfluxDB 使用 SNAPPY 压缩
// 压缩比:1:10
// 10TB 原始数据 → 1TB 存储

// ClickHouse 使用 LZ4 压缩
// 压缩比:1:7
// 100TB 原始数据 → 14TB 存储

// 节省成本:每月约 8万

优化2:冷数据归档

/**
 * 定时任务:归档3个月以上的数据到OSS
 */
@Scheduled(cron = "0 0 2 * * ?")  // 每天凌晨2点执行
public void archiveColdData() {
    LocalDateTime threeMonthsAgo = LocalDateTime.now().minusMonths(3);
    
    // 1. 从InfluxDB导出数据
    List<DeviceData> coldData = influxDB.query(
        "SELECT * FROM device_data WHERE time < '" + 
        threeMonthsAgo + "'");
    
    // 2. 转换为Parquet格式(列式存储,压缩比更高)
    String parquetFile = convertToParquet(coldData);
    
    // 3. 上传到OSS
    ossClient.putObject("cold-data-bucket", parquetFile);
    
    // 4. 删除InfluxDB中的冷数据
    influxDB.delete("DELETE FROM device_data WHERE time < '" + 
        threeMonthsAgo + "'");
    
    log.info("Archived {} records to OSS", coldData.size());
}

// OSS存储成本:0.12元/GB/月
// SSD存储成本:2元/GB/月
// 节省成本:每TB每月节省 1880元

优化3:采样降精度

/**
 * 对于历史数据,降低采样精度
 */
// 原始数据:每分钟一条
// 1年 = 525,600 条

// 优化后:
// - 最近7天:保留原始精度(每分钟)
// - 7天-1个月:降采样到每5分钟
// - 1个月-3个月:降采样到每30分钟
// - 3个月以上:降采样到每小时

// 数据量减少:90%
// 查询速度提升:10倍
// 节省存储成本:80%

🎓 第九章:面试加分项

9.1 高级话题

话题1:如何处理时序数据的乱序问题?

问题:网络延迟导致数据晚到,时间戳错乱

解决方案

// Flink Watermark机制
WatermarkStrategy
    .<DeviceData>forBoundedOutOfOrderness(Duration.ofMinutes(5))
    .withTimestampAssigner((event, timestamp) -> 
        event.getTimestamp().toEpochMilli());

// 允许5分钟的乱序,超过的数据会被丢弃或单独处理

话题2:如何保证数据不丢失?

方案

  1. Kafka持久化:数据先写Kafka(副本机制,不会丢)
  2. At-Least-Once语义:宁可重复,不可丢失
  3. 定期对账:比对Kafka和数据库的数据量
@Scheduled(cron = "0 */10 * * * ?")  // 每10分钟检查
public void checkDataIntegrity() {
    long kafkaCount = kafkaTemplate.getRecordCount("device-data");
    long dbCount = influxDB.count("device_data");
    
    if (Math.abs(kafkaCount - dbCount) > 1000) {
        alertService.sendAlert("数据不一致!差异:" + 
            (kafkaCount - dbCount));
    }
}

话题3:如何优雅地扩容?

步骤

  1. 添加新节点(不影响现有服务)
  2. 数据迁移(后台异步进行)
  3. 流量切换(渐进式,如10% → 50% → 100%)
  4. 验证(监控指标,确认无问题)
  5. 下线老节点
// 一致性Hash,方便扩容
ConsistentHash<String> hashRing = new ConsistentHash<>(
    Hashing.murmur3_128(), 
    nodes, 
    virtualNodes);

String node = hashRing.get(deviceId);  // 路由到节点

9.2 面试回答模板

面试官:"如何设计百亿级物联网数据平台?"

你的回答(4分钟,层层递进):

1. 需求分析 (30秒)
"首先,我会明确几个关键指标:每天写入量(TB级)、查询QPS、延迟要求(P99<3秒)、数据保留时长。这决定了架构选型。"

2. 整体架构 (1分钟)
"我会采用Lambda架构:设备通过MQTT上报到Kafka,然后分两路:一路走Flink实时计算做预聚合;另一路批量写入时序数据库(如InfluxDB)存储原始数据。查询层会根据请求特点,智能路由到InfluxDB(原始数据)或ClickHouse(预聚合数据)。"

3. 存储设计 (1分钟)
"采用冷热分离:热数据(7天内)放SSD+InfluxDB,查询快;温数据(3个月)放HDD+ClickHouse;冷数据(历史)压缩后存OSS。同时做分库分表,按设备ID哈希分1024张表,按月份分区,这样单表数据量控制在千万级,查询时只扫描必要的分区。"

4. 查询优化 (1分钟)
"两个核心优化:一是预聚合,用Flink实时计算好地区、小时维度的统计数据,查询时直接读结果表,速度提升100倍;二是多级缓存,本地Caffeine缓存+Redis分布式缓存,命中率可达95%,P99延迟降到50ms以内。"

5. 高可用 (30秒)
"采用多机房部署,数据库主从同步,应用层无状态可水平扩展。Kafka、InfluxDB、ClickHouse都是集群模式,单点故障不影响整体服务。"

6. 成本优化 (30秒)
"通过数据压缩(压缩比1:10)、冷数据归档OSS、历史数据降采样,把存储成本降低80%。同时用预聚合减少计算量,整体成本控制在合理范围。"

面试加分项

  • 💯 能画出架构图
  • 💯 说出具体的数据库选型理由
  • 💯 提到成本优化
  • 💯 有实际经验(哪怕是学习项目)
  • 💯 能回答"为什么不用XX方案"

📚 第十章:学习资源与实战

10.1 快速上手Demo

GitHub项目iot-data-platform-demo

# 1. 克隆项目
git clone https://github.com/xxx/iot-data-platform-demo.git

# 2. 启动Docker Compose(包含Kafka、InfluxDB、Redis等)
cd iot-data-platform-demo
docker-compose up -d

# 3. 启动数据模拟器(模拟1000台设备上报数据)
java -jar device-simulator.jar

# 4. 启动查询服务
cd query-service
mvn spring-boot:run

# 5. 访问Dashboard
open http://localhost:8080

10.2 推荐学习路径

阶段学习内容时间资源
入门时序数据库基础(InfluxDB)1周官方文档
进阶Kafka原理与实战2周《Kafka权威指南》
高级Flink流式计算3周Flink官方教程
实战搭建完整项目4周本文档+ GitHub Demo

10.3 常见坑与解决方案

坑1:InfluxDB写入慢

症状:写入QPS只有1万,达不到预期

原因

  • 没有批量写入(单条写入太慢)
  • TSM引擎还没刷盘(内存积压)

解决

// ❌ 错误:单条写入
for (DeviceData data : dataList) {
    influxDB.write(data);  // 慢!
}

// ✅ 正确:批量写入
influxDB.write(dataList);  // 快100倍!

// ✅ 更好:异步批量写入
influxDB.enableBatch(BatchOptions.DEFAULTS
    .actions(5000)  // 积累5000条
    .flushDuration(1000)  // 或1秒
);

坑2:查询某个地区数据特别慢

症状:查询北京地区数据要30秒,查上海只要1秒

原因:数据倾斜!北京设备特别多

解决

// 热点地区单独处理
if ("Beijing".equals(region) || "Shanghai".equals(region)) {
    // 大城市:强制走预聚合表
    return queryFromPreAggregation(region, start, end);
} else {
    // 小城市:可以直接查原始数据
    return queryFromRawData(region, start, end);
}

坑3:缓存穿透导致数据库打死

症状:突然有大量查询不存在的设备ID,Redis没有,数据库被打死

解决:布隆过滤器

@Bean
public BloomFilter<String> deviceIdFilter() {
    // 预计1亿台设备,误判率0.01%
    BloomFilter<String> filter = BloomFilter.create(
        Funnels.stringFunnel(Charset.defaultCharset()),
        100_000_000,
        0.0001
    );
    
    // 加载所有设备ID到布隆过滤器
    List<String> allDeviceIds = deviceService.getAllDeviceIds();
    allDeviceIds.forEach(filter::put);
    
    return filter;
}

// 查询前先判断
if (!deviceIdFilter.mightContain(deviceId)) {
    return Result.error("设备不存在");
}

🎉 总结:你已经是高级工程师了!

恭喜你读到这里!🎊 现在你已经掌握了:

  • 架构设计:Lambda架构、冷热分离、分库分表
  • 技术选型:Kafka、InfluxDB、ClickHouse、Flink、Redis
  • 性能优化:预聚合、多级缓存、索引优化
  • 工程实践:监控、容灾、成本优化
  • 面试技巧:如何回答、加分项、常见坑

最后的建议

  1. 不要死记硬背,理解原理最重要
  2. 画出架构图,一图胜千言
  3. 动手实践,跑跑Demo,改改代码
  4. 关注成本,体现工程师的业务sense

彩蛋:面试官可能追问的问题

问题快速回答要点
"如果数据量增长10倍怎么办?""水平扩展:加节点(Kafka加分区、数据库加分片);成本优化(更激进的冷数据归档)"
"如何保证数据一致性?""弱一致性模型(最终一致性),用Kafka保证不丢,定期对账"
"为什么不用MySQL?""MySQL不擅长时序数据:写入慢、压缩比低、时序查询需要复杂索引。时序数据库有10倍以上优势"
"能说说具体的成本吗?""百亿数据,约50万/月。主要是服务器(30万)和存储(15万)。优化后可降至30万"

📞 联系作者

如果你有任何问题,欢迎交流!

最后送你一句话

"架构设计没有银弹,只有权衡(Trade-off)。
根据业务场景选择最合适的方案,才是高级工程师的智慧。"


附录:技术栈清单

类别技术作用可替代方案
消息队列Kafka削峰填谷、解耦Pulsar、RabbitMQ
时序数据库InfluxDB存储原始时序数据TDengine、TimescaleDB
OLAP数据库ClickHouse存储预聚合数据、快速分析Apache Druid、Apache Doris
流计算Flink实时预聚合Spark Streaming、Storm
缓存Redis提升查询性能Memcached、Hazelcast
对象存储OSS/S3冷数据归档MinIO、Ceph
监控Prometheus+Grafana系统监控InfluxDB+Chronograf
应用框架Spring Boot业务逻辑Quarkus、Micronaut

版本历史

  • v1.0 (2024-10-20):初始版本
  • 预计v1.1:增加Kubernetes部署章节

License

本文档采用 CC BY-NC-SA 4.0 协议
欢迎分享,禁止商用


"愿你早日成为别人口中的'大佬'!" 💪

—— 一位写了10年Java、踩了无数坑的老程序员