作者:某不愿透露姓名的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:3 | 1:20 | 7倍 |
| 时序查询 | 需要索引优化 | 原生支持 | 10倍 |
| 存储成本 | 100TB | 5TB | 省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倍 |
| 索引大小 | 500GB | 500MB | ✅ 内存放得下 |
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倍 🚀 |
| 存储空间 | 10TB | 10TB + 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));
}
}
性能对比:
| 缓存层级 | QPS | P99延迟 | 成本 |
|---|---|---|---|
| 无缓存(直接查DB) | 1000 | 800ms | 💰💰💰 |
| 只有Redis缓存 | 10000 | 50ms | 💰💰 |
| 本地+Redis两级缓存 | 100000 | 5ms | 💰 |
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;
}
}
优化效果:
| 指标 | 优化前 | 优化后 | 提升 |
|---|---|---|---|
| 响应时间 | 2000ms | 50ms | 40倍 |
| 数据库QPS | 500 | 100 | 减轻5倍压力 |
| 可支持并发 | 500 | 20000 | 40倍 |
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 │
│ (主从+哨兵模式) │
│ 3主3从 │
└────────────────────────┘
↓
┌───────────────────┼────────────────────┐
│ │ │
↓ ↓ ↓
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ 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:如何保证数据不丢失?
方案:
- Kafka持久化:数据先写Kafka(副本机制,不会丢)
- At-Least-Once语义:宁可重复,不可丢失
- 定期对账:比对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:如何优雅地扩容?
步骤:
- 添加新节点(不影响现有服务)
- 数据迁移(后台异步进行)
- 流量切换(渐进式,如10% → 50% → 100%)
- 验证(监控指标,确认无问题)
- 下线老节点
// 一致性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
- ✅ 性能优化:预聚合、多级缓存、索引优化
- ✅ 工程实践:监控、容灾、成本优化
- ✅ 面试技巧:如何回答、加分项、常见坑
最后的建议:
- 不要死记硬背,理解原理最重要
- 画出架构图,一图胜千言
- 动手实践,跑跑Demo,改改代码
- 关注成本,体现工程师的业务sense
彩蛋:面试官可能追问的问题
| 问题 | 快速回答要点 |
|---|---|
| "如果数据量增长10倍怎么办?" | "水平扩展:加节点(Kafka加分区、数据库加分片);成本优化(更激进的冷数据归档)" |
| "如何保证数据一致性?" | "弱一致性模型(最终一致性),用Kafka保证不丢,定期对账" |
| "为什么不用MySQL?" | "MySQL不擅长时序数据:写入慢、压缩比低、时序查询需要复杂索引。时序数据库有10倍以上优势" |
| "能说说具体的成本吗?" | "百亿数据,约50万/月。主要是服务器(30万)和存储(15万)。优化后可降至30万" |
📞 联系作者
如果你有任何问题,欢迎交流!
- 📧 Email: senior-java-dev@example.com
- 💬 微信:JavaArchitect10Years
- 🌐 博客:blog.example.com
最后送你一句话:
"架构设计没有银弹,只有权衡(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、踩了无数坑的老程序员