日增量500万的表结构如何设计

设计一个日数据量超500万的数据库结构

开场:需求边界确认(面试必做!)

在正式设计前,我先明确几个核心边界(避免答非所问):

  • 假设场景:用户行为日志系统(互联网最常见的高吞吐场景)
  • 核心指标:日增 500 万条 → 月增 1.5 亿 → 年增 18 亿
  • 查询模式:90% 是「用户 ID + 时间范围」查询,少量全局统计
  • 读写比例:1:3(写多读少,典型日志特征)
  • 数据保留:6 个月热数据,超过归档到冷存储
  • 一致性要求:最终一致即可,允许秒级延迟

核心架构选型 ✅

架构说明:

  • Kafka 削峰填谷,应对突发流量
  • MySQL 主从分离,读写分离
  • Redis 缓存热点数据(最近 7 天)
  • ES 支持复杂全文检索和统计
  • 定时任务负责数据归档和过期删除

整理了面试真题、每日技术知识点、系统学习路线,都汇总在个人网站 www.javadashen.com,有需要的同学自取。

公众号「Rain的Java大神之路」每天拆一个知识点,陪你悄悄变强,有空来坐坐。


核心表结构设计 💡

3.1 基础表结构(user_behavior_log)

字段名 类型 长度 说明 索引
id bigint 20 雪花算法主键 PRIMARY KEY
user_id bigint 20 用户 ID 联合索引 (user_id, event_time)
event_type tinyint 1 事件类型(1 = 点击,2 = 曝光,3 = 下单)
event_time datetime(3) 事件时间(精确到毫秒) 联合索引 (user_id, event_time)
device_id varchar 32 设备 ID
ext_info json 扩展字段(非结构化数据)
create_time datetime 入库时间

设计要点:

  1. 主键用雪花算法:避免分库分表后自增 ID 冲突,同时自带时间戳属性
  2. 字段最小化:event_type 用 tinyint 代替 varchar,节省存储空间
  3. JSON 存扩展字段:避免频繁改表,适配业务快速迭代
  4. 联合索引最左匹配:(user_id, event_time) 完美覆盖 90% 的查询场景

3.2 索引设计原则 ⚠️

  • 只建必要索引:单表索引不超过 5 个,避免写入性能下降
  • 避免冗余索引:如果有 (a,b) 联合索引,就不需要单独建 a 索引
  • 定期清理无效索引:用pt-index-usage工具分析索引使用情况

分库分表策略(核心中的核心!)

4.1 分库策略:按 user_id 哈希分库

  • 分库数量:8 个库(2 的幂次,方便后续扩容)
  • 路由规则:库号 = user_id % 8
  • 优势:数据均匀分布,避免单库热点,按用户 ID 查询性能最优

4.2 分表策略:时间 + user_id 哈希二级分表

  • 分表周期:每月一张表(月增 1.5 亿,单表控制在 5000 万以内)
  • 分表数量:每张月表再分4 张子表
  • 路由规则:表号 = (user_id % 4) + 月份偏移量 * 4
  • 示例:2026 年 6 月的表 → user_behavior_log_202606_0 ~ user_behavior_log_202606_3

4.3 分库分表示意图

数据量计算验证:

  • 日增 500 万 → 月增 1.5 亿
  • 8 库 ×4 表 = 32 张表 / 月
  • 单表月增:1.5 亿 / 32 ≈ 468 万条
  • 6 个月热数据:单表≈2800 万条(远低于 MySQL 单表 5000 万的最佳实践上限)✅

性能优化关键点 🚀

  1. 批量写入:Kafka 消费后攒批 1000 条再批量插入 MySQL,写入性能提升 10 倍以上
  2. 读写分离:主库只写,从库负责所有查询请求,主库压力降低 70%
  3. 热点缓存:最近 7 天的活跃用户行为缓存到 Redis,缓存命中率≥80%
  4. 数据压缩:JSON 字段用 Snappy 压缩,存储空间节省 50% 以上
  5. 避免大事务:单条 SQL 操作不超过 1000 行,防止锁表和主从延迟

数据生命周期管理 📅

  • 自动建表:每月 25 号提前创建下个月的分表,避免月底凌晨流量高峰时建表失败
  • 自动归档:每月 1 号自动将 6 个月前的热数据归档到 HDFS 冷存储
  • 自动删除:归档完成后删除 MySQL 中的过期表,释放存储空间
  • 归档数据查询:通过 Presto 查询 HDFS 中的冷数据,满足历史数据分析需求

未来扩展方案 🔮

如果未来日增数据量突破 1000 万,可以按以下顺序升级:

  1. 分库扩容:从 8 个库扩展到 16 个库(2 倍扩容,无需数据迁移)
  2. 缩短分表周期:从每月一张表改成每两周一张表
  3. 引入时序数据库:将 MySQL 替换为 TDengine/InfluxDB,时序数据写入性能提升 10 倍以上
  4. 引入数据湖:用 Iceberg/Hudi 构建数据湖,支持更灵活的数据分析

核心代码实现(面试必写亮点💻)

1 分库分表路由算法(核心)

java 复制代码
/**
 * 分库分表路由工具类
 * 技术亮点:位运算代替取模,性能提升3倍;支持2倍平滑扩容
 */
public class ShardingRouter {
    // 分库数量(必须是2的幂次)
    private static final int DB_COUNT = 8;
    // 每个月的分表数量(必须是2的幂次)
    private static final int TABLE_PER_MONTH = 4;
    // 基准年份(用于计算月份偏移量)
    private static final int BASE_YEAR = 2020;

    /**
     * 计算库号
     */
    public static int getDbNo(long userId) {
        // 位运算代替取模:a % 2^n = a & (2^n - 1)
        return (int) (userId & (DB_COUNT - 1));
    }

    /**
     * 计算表号
     */
    public static int getTableNo(long userId, LocalDateTime eventTime) {
        // 计算月份偏移量:(年-基准年)*12 + 月
        int monthOffset = (eventTime.getYear() - BASE_YEAR) * 12 + eventTime.getMonthValue();
        // 表号 = 月份偏移量 * 每月表数 + 用户哈希
        return monthOffset * TABLE_PER_MONTH + (int) (userId & (TABLE_PER_MONTH - 1));
    }

    /**
     * 获取实际表名
     */
    public static String getTableName(long userId, LocalDateTime eventTime) {
        int tableNo = getTableNo(userId, eventTime);
        return String.format("user_behavior_log_%d", tableNo);
    }
}

2 雪花算法主键生成(解决时钟回拨)

java 复制代码
/**
 * 雪花算法ID生成器
 * 技术亮点:解决时钟回拨问题,支持最大100ms的回拨容忍
 */
public class SnowflakeIdGenerator {
    // 起始时间戳(2020-01-01)
    private static final long START_TIMESTAMP = 1577808000000L;
    // 机器ID位数
    private static final long WORKER_ID_BITS = 5L;
    // 序列号位数
    private static final long SEQUENCE_BITS = 12L;
    // 机器ID最大值
    private static final long MAX_WORKER_ID = ~(-1L << WORKER_ID_BITS);
    // 序列号最大值
    private static final long MAX_SEQUENCE = ~(-1L << SEQUENCE_BITS);

    private final long workerId;
    private long lastTimestamp = -1L;
    private long sequence = 0L;

    public SnowflakeIdGenerator(long workerId) {
        if (workerId < 0 || workerId > MAX_WORKER_ID) {
            throw new IllegalArgumentException("Worker ID超出范围");
        }
        this.workerId = workerId;
    }

    public synchronized long nextId() {
        long currentTimestamp = System.currentTimeMillis();

        // 处理时钟回拨
        if (currentTimestamp < lastTimestamp) {
            long offset = lastTimestamp - currentTimestamp;
            // 回拨小于100ms,等待
            if (offset <= 100) {
                try {
                    wait(offset << 1);
                    currentTimestamp = System.currentTimeMillis();
                    if (currentTimestamp < lastTimestamp) {
                        throw new RuntimeException("时钟回拨超过100ms,无法生成ID");
                    }
                } catch (InterruptedException e) {
                    throw new RuntimeException("等待时钟恢复失败", e);
                }
            } else {
                throw new RuntimeException("时钟回拨超过100ms,无法生成ID");
            }
        }

        // 同一毫秒内,序列号自增
        if (currentTimestamp == lastTimestamp) {
            sequence = (sequence + 1) & MAX_SEQUENCE;
            // 序列号用完,等待下一毫秒
            if (sequence == 0) {
                currentTimestamp = waitNextMillis(lastTimestamp);
            }
        } else {
            sequence = 0L;
        }

        lastTimestamp = currentTimestamp;

        // 拼接ID
        return ((currentTimestamp - START_TIMESTAMP) << (WORKER_ID_BITS + SEQUENCE_BITS))
                | (workerId << SEQUENCE_BITS)
                | sequence;
    }

    private long waitNextMillis(long lastTimestamp) {
        long timestamp = System.currentTimeMillis();
        while (timestamp <= lastTimestamp) {
            timestamp = System.currentTimeMillis();
        }
        return timestamp;
    }
}

3 Kafka 批量写入优化

java 复制代码
/**
 * Kafka消费者批量写入MySQL
 * 技术亮点:攒批+超时双重触发,兼顾吞吐量和延迟;失败重试机制
 */
@Component
public class UserBehaviorConsumer {
    @Autowired
    private UserBehaviorMapper userBehaviorMapper;
    // 批量大小
    private static final int BATCH_SIZE = 1000;
    // 最大等待时间(毫秒)
    private static final long MAX_WAIT_TIME = 500L;
    // 重试次数
    private static final int MAX_RETRY = 3;

    @KafkaListener(topics = "user_behavior_topic", groupId = "user_behavior_group")
    public void consume(List<ConsumerRecord<String, String>> records) {
        List<UserBehavior> batchList = new ArrayList<>(BATCH_SIZE);
        long lastFlushTime = System.currentTimeMillis();

        for (ConsumerRecord<String, String> record : records) {
            try {
                UserBehavior behavior = JSON.parseObject(record.value(), UserBehavior.class);
                behavior.setId(IdGenerator.nextId());
                batchList.add(behavior);

                // 达到批量大小或超时,触发写入
                if (batchList.size() >= BATCH_SIZE 
                        || System.currentTimeMillis() - lastFlushTime >= MAX_WAIT_TIME) {
                    batchInsertWithRetry(batchList);
                    batchList.clear();
                    lastFlushTime = System.currentTimeMillis();
                }
            } catch (Exception e) {
                log.error("解析消息失败: {}", record.value(), e);
            }
        }

        // 处理剩余数据
        if (!batchList.isEmpty()) {
            batchInsertWithRetry(batchList);
        }
    }

    private void batchInsertWithRetry(List<UserBehavior> list) {
        int retryCount = 0;
        while (retryCount < MAX_RETRY) {
            try {
                // MyBatis-Plus批量插入
                userBehaviorMapper.insertBatch(list);
                return;
            } catch (Exception e) {
                retryCount++;
                log.error("批量插入失败,重试第{}次", retryCount, e);
                if (retryCount >= MAX_RETRY) {
                    // 写入失败,发送到死信队列
                    sendToDlq(list);
                    throw new RuntimeException("批量插入最终失败", e);
                }
                // 指数退避重试
                try {
                    Thread.sleep(100L * (1 << retryCount));
                } catch (InterruptedException ie) {
                    Thread.currentThread().interrupt();
                }
            }
        }
    }
}

4 数据归档定时任务

java 复制代码
/**
 * 数据归档定时任务
 * 技术亮点:分批次迁移,避免大事务;幂等性设计,支持重复执行
 */
@Component
@EnableScheduling
public class DataArchiveTask {
    @Autowired
    private UserBehaviorMapper userBehaviorMapper;
    @Autowired
    private HdfsService hdfsService;
    // 每次迁移的批次大小
    private static final int BATCH_SIZE = 10000;
    // 数据保留时间(6个月)
    private static final int RETENTION_MONTHS = 6;

    // 每月1号凌晨2点执行
    @Scheduled(cron = "0 0 2 1 * ?")
    public void archiveExpiredData() {
        LocalDateTime expireTime = LocalDateTime.now().minusMonths(RETENTION_MONTHS);
        log.info("开始归档{}之前的数据", expireTime);

        // 遍历所有需要归档的表
        List<String> tablesToArchive = getTablesBefore(expireTime);
        for (String tableName : tablesToArchive) {
            // 幂等性检查:如果已经归档过,跳过
            if (hdfsService.exists("/archive/" + tableName)) {
                log.info("表{}已经归档,跳过", tableName);
                continue;
            }

            // 分批次迁移数据
            long lastId = 0L;
            int count;
            do {
                List<UserBehavior> list = userBehaviorMapper.selectByTableAndId(tableName, lastId, BATCH_SIZE);
                count = list.size();
                if (count > 0) {
                    // 写入HDFS
                    hdfsService.write("/archive/" + tableName + "/" + lastId, list);
                    // 删除已迁移的数据
                    userBehaviorMapper.deleteByTableAndId(tableName, list.get(list.size() - 1).getId());
                    lastId = list.get(list.size() - 1).getId();
                    log.info("表{}已归档{}条数据", tableName, count);
                }
            } while (count == BATCH_SIZE);

            // 归档完成,删除空表
            userBehaviorMapper.dropTable(tableName);
            log.info("表{}归档完成并删除", tableName);
        }

        log.info("数据归档任务完成");
    }
}

技术难点与解决方案(面试加分项✨)

技术难点 影响 解决方案 技术亮点
⚠️ 分库分表后跨库分页查询 性能极差,结果不准确,无法支持全局排序 1. 业务层强制限制:所有查询必须带 user_id2. 全局统计查询走 Elasticsearch3. 禁止无限制的跨库分页 从业务层面规避技术难题,用合适的工具做合适的事
⚠️ 热点数据问题(大 V 用户) 单库单表压力过大,成为系统瓶颈 1. 热点用户单独分库(白名单机制)2. 热点数据全量缓存到 Redis3. 读写分离 + 从库负载均衡 热点隔离,避免一颗老鼠屎坏了一锅粥
⚠️ 数据倾斜 部分库表数据量远超其他,导致性能不均 1. 哈希取模前加盐(userId + 随机数)2. 定期监控数据分布,动态调整路由3. 引入一致性哈希算法 数据均匀分布,充分利用集群资源
⚠️ 分库分表扩容 传统扩容需要全量数据迁移,停机时间长 1. 采用 2 的幂次分库分表2. 双写方案:先写新库,再同步旧数据3. 灰度切流量,验证无误后下线旧库 零停机平滑扩容,对业务无感知
⚠️ 分布式事务 跨库操作无法保证原子性 1. 日志场景不需要强一致性,最终一致即可2. 采用本地消息表 + 定时任务补偿3. 必要时使用 Seata AT 模式 降低一致性要求,换取性能提升
⚠️ 主从延迟 写入后立即查询不到数据 1. 核心查询强制走主库(白名单)2. 缓存更新采用先删后更模式3. 监控主从延迟,超过阈值自动降级 平衡性能和一致性,避免业务异常
⚠️ 数据丢失风险 Kafka 或 MySQL 宕机导致数据丢失 1. Kafka 开启 acks=all + 副本机制2. MySQL 开启 binlog + 主从备份3. 写入失败的数据发送到死信队列人工处理 多层数据保障,数据零丢失

真实面试模拟

真实面试模拟

面试官 👨‍💼:

好,聊个开放题。假设你正在设计一个物联网系统,每天要存入超过 500 万条 设备上报数据。业务上既要能查单设备的历史,又要能看全量趋势。你会怎么设计整体数据库架构?不用讲得太泛,抓住几个核心决策点。

候选人 🤔:

好的,我先分析一下量级。日均500万,写入平均每秒不到60条,压力主要在总量膨胀查询上。

我的核心思路是:把写入、在线查询、分析查询拆开,数据按时间冷热分层。

整体架构用一张简图表示就是:

text 复制代码
设备 → Kafka(削峰)→ 流计算(Flink) → 实时告警/聚合
                    → 批量写入服务 → MySQL分库分表(热数据)
                                    → ClickHouse(温数据,分析)
                                    → OSS(冷数据归档)

线上业务只碰热数据,分析全走 OLAP,互不干扰。

面试官 👍:

思路清晰。那聚焦到 MySQL 这块热数据,日增500万的表,你具体怎么建?注意索引和字段设计。

候选人 📝:

我追求字段精简、索引克制。假设存的是设备指标,建表大概这样:

sql 复制代码
CREATE TABLE device_data_20260604 (
    id BIGINT UNSIGNED NOT NULL,             -- 雪花ID,避免自增冲突
    device_id VARCHAR(32) NOT NULL,
    report_time DATETIME(3) NOT NULL,        -- 毫秒精度
    metric_code SMALLINT NOT NULL,           -- 枚举,省空间
    metric_value DECIMAL(12,4) NOT NULL,
    create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    PRIMARY KEY (id),
    KEY idx_device_time (device_id, report_time), -- 核心联合索引
    KEY idx_time_metric (report_time, metric_code)
) ENGINE=InnoDB
PARTITION BY RANGE (TO_DAYS(report_time)) (...);  -- 按天分区

关键点:

  • 主键用雪花算法,分库分表时不会冲突。
  • 只建了两个最必要的联合索引,覆盖查询,绝不多建,以免拖慢写入。
  • 按天分区,方便30天后直接 truncate 冷数据。

面试官 💡:

索引上很克制。那分库分表策略呢?每天500万,一个月就1.5亿,单表肯定不行吧?

候选人 🎯:

对。我采用 两级切分

  • 库维度 :按 device_id 哈希分16个库,路由算法 库序号 = hash(device_id) % 16
  • 表维度 :每个库内再按 分表,表名 device_data_YYYYMMDD

这样做的好处是:查单设备历史数据时,能直接定位到某一个库的某一张表,完全没有跨库事务和聚合的压力。每天定时清理旧表也只需要删分区或删表。

面试官 🔥:

好的。那写入链路怎么压榨性能?每秒虽不高,但你要预留增长空间。

候选人 ⚡:

我会上几个组合拳:

  1. 批量写入 :从 Kafka 消费后攒批,每 1000 条或 1 秒刷一次,用 INSERT INTO ... VALUES (...),(...)...
  2. 连接池隔离:16个分库每个一个独立的 HikariCP 连接池,避免争抢。
  3. 异步并行:线程池批量分发到不同分库,Future 异步等待结果。
  4. 精简 binlog:部分库可设为 ROW 格式且不做实时同步时,关掉不必要日志。

压测下来单库轻松到 8000+ TPS,16 库整体扛下 10 倍当前流量完全没问题 🚀。

面试官 🔍:

数据存好了,查询场景怎么办?比如"查某个设备最新一条数据","查某个设备过去7天曲线","查所有设备昨天每小时平均值"。

候选人 💬:

这三类正好对应不同方案,我分开处理:

查询场景 解决方案
单设备最新数据 每次上报异步写 Redis,用 ZSET 按时间排序,直接 O(1) 拿最新一条
单设备某时段历史 根据 device_id 路由到指定库表,走联合索引 idx_device_time,覆盖返回,毫秒级
全设备/全维度聚合 实时数仓那路 Flink 清洗后同步到 ClickHouse,聚合查询全走 OLAP,绝不拖 MySQL

这样 TP 和 AP 彻底分离 ,MySQL 只做点查,分析引擎专做聚合。

面试官 🧐:

容灾和扩展性方面,怎么保证高可用?如果以后设备量翻10倍,怎么扩?

候选人 🛠️:

每个分库都配 主从 + 读写分离,从库承接报表类读请求,主库只写。

分库路由上,用 ShardingSphere-Proxy 或自研轻量SDK,动态感知节点。

扩容时,通过 一致性哈希 减少迁移量,或者用"双写新库 + 数据逐批迁移"方案,平滑过渡,基本不停服。

冷数据方面,超过30天的数据自动迁移到 Parquet 格式存 OSS,用 Presto/Trino 做离线分析,成本很低。

面试官 😊:

很不错,整个链路都覆盖到了。最后用几句话总结一下你的设计哲学吧。

候选人 🌟:

就三点:

  • 库表不贪大,索引要吝啬------字段精简,只为关键查询建索引。
  • 写入走缓冲,查询做分离------MySQL 做在线点查,OLAP 做聚合,谁也不碍谁。
  • 时间分冷热,哈希拆孤表------按时间快速淘汰,按哈希均匀分摊压力。

这套架构可以伴随业务线性增长,每一步都有现成的中间件和开源方案支撑。

面试官 👨‍💼:

整体设计我很满意。这样,你再深入一下:挑几个你刚才提到的关键点,贴一下核心代码 ,让我看看你代码层面的实现功力。另外,你觉得这个场景下最大的技术难点是什么?怎么解的?

候选人 🧑‍💻:

没问题,我挑三个最体现技术深度的点来写伪代码,然后总结四个核心难点和我的解法。

🧬 核心代码片段(有技术亮点)

1. 高性能雪花 ID 生成(解决时钟回拨 + ShardingSphere 集成)

传统雪花算法怕时钟回拨,我内置了环形缓冲区预生成 + 回拨自旋等待

java 复制代码
// 雪花算法核心:workerId = 库索引,datacenterId = 机器IP hash
public class SnowflakeIdGenerator {
    // 环形缓冲区,预先产生ID,减少锁竞争
    private final RingBuffer<Long> ringBuffer;
    
    public SnowflakeIdGenerator(int workerBits, int datacenterBits) {
        this.ringBuffer = new RingBuffer<>(1024, this::nextIdInternal);
    }
    
    public long nextId() {
        Long id = ringBuffer.poll(10, TimeUnit.MILLISECONDS);
        if (id != null) return id;
        // 兜底:时钟严重回拨,等待追赶,超过阈值抛异常
        long currentMillis = System.currentTimeMillis();
        if (currentMillis < lastTimestamp) {
            long offset = lastTimestamp - currentMillis;
            if (offset <= 5) {
                LockSupport.parkNanos(offset * 1_000_000);
            } else {
                throw new RuntimeException("Clock moved backwards. Refusing to generate id");
            }
        }
        return nextIdInternal();
    }
    // 原始雪花算法逻辑...
}

亮点:RingBuffer 消除锁竞争,TPS 可达 1500万+/s,还能柔性应对最多 5ms 回拨。

2. 分库分表路由切面(自研轻量 SDK,无需 Proxy)

基于注解 + AOP,做到业务代码零侵入:

java 复制代码
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ShardingRoute {
    String shardKey();       // 方法参数名
    String tablePrefix();    // 表前缀
    ShardStrategy strategy(); // 哈希或日期
}

@Aspect
@Component
public class ShardingAspect {
    // 数据源持有器,存储当前线程选定的数据源和表名
    private final ThreadLocal<ShardingContext> contextHolder = new ThreadLocal<>();
    
    @Around("@annotation(route)")
    public Object route(ProceedingJoinPoint pjp, ShardingRoute route) throws Throwable {
        Object[] args = pjp.getArgs();
        String shardValue = (String) args[getArgIndex(pjp, route.shardKey())];
        // 一致性哈希确定库
        int dbIndex = ConsistentHash.getDb(shardValue, 16);
        // 今天日期确定表
        String table = route.tablePrefix() + DateUtil.today();
        
        DataSource dataSource = DataSourceManager.get(dbIndex);
        try (ShardingContext ctx = ShardingContext.set(dataSource, table)) {
            return pjp.proceed(); // SQL 中 ${tableName} 自动替换
        }
    }
}

亮点:线程级数据源切换 + 注解声明式,切库逻辑只在切面,业务代码只关心业务。

3. 批量写入 + 多线程异步并行(最大榨取磁盘IO)

java 复制代码
public class BatchWriter {
    private final List<DeviceData> batchQueue = new ArrayList<>(1000);
    
    @Scheduled(fixedDelay = 1000) // 每秒刷一次
    public void flushAll() {
        if (batchQueue.isEmpty()) return;
        // 按分库分表分组
        Map<ShardKey, List<DeviceData>> grouped = batchQueue.stream()
            .collect(Collectors.groupingBy(this::computeShardKey));
        
        List<CompletableFuture<Void>> futures = new ArrayList<>();
        grouped.forEach((shard, list) -> {
            futures.add(CompletableFuture.runAsync(() -> {
                String table = "device_data_" + DateUtil.today();
                jdbcTemplate.batchUpdate(
                    "INSERT INTO " + table + " (...) VALUES (...)", 
                    list,
                    1000,
                    (ps, row) -> { /* 参数设置 */ }
                );
            }, writeExecutor)); // 独立线程池
        });
        
        CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
        batchQueue.clear();
    }
}

亮点:按分库分组并行写入,每个分库一个连接池,线程数 = 分库数,避免上下文切换开销。

4. 冷数据自动归档(分区交换 + OSS 上传)

MySQL 端使用分区交换,瞬间清理:

java 复制代码
public void archiveDaily(Date date) {
    String partitionName = "p" + DateUtil.format(date, "yyyyMMdd");
    // 1. 交换到归档表(空表),瞬间完成,不锁业务读
    jdbcTemplate.execute("ALTER TABLE device_data EXCHANGE PARTITION " 
        + partitionName + " WITH TABLE device_data_archive");
    // 2. 从归档表导出为 Parquet 压缩文件(Spark 或直接流式读取)
    exportToParquet("device_data_archive", "oss://archive/" + partitionName + ".parquet");
    // 3. 清空归档表
    jdbcTemplate.execute("TRUNCATE TABLE device_data_archive");
}

亮点:分区交换是 DDL 秒级操作,不影响线上查询;Parquet 列存节省 90% 存储成本。

🔥 技术难点与解决方案

我梳理了这个场景下四个棘手的点,以及我的解决方案:

难点 风险描述 解决方案
分布式主键全局唯一 分库分表后自增ID会冲突,UUID无序导致页分裂 🌟 雪花算法 + RingBuffer:高位时间戳保证趋势递增,环形缓冲提升性能,WorkerID 映射库索引,零碰撞。
热点设备写入倾斜 某些设备上报极频繁,哈希后单库压力大 🌟 一致性哈希 + 虚拟节点:避免简单取模导致的数据迁移雪崩,虚拟节点分散热点;写入端本地预聚合(每秒同设备合并一次)再落库。
冷热数据无缝切换 业务按时间查询时可能跨冷热边界,性能断崖 🌟 查询路由中间件:解析时间范围,≤30天走MySQL,>30天自动路由到ClickHouse,通过 UNION ALL 合并结果,应用层无感。
分库扩容数据迁移 从16库扩到32库时,历史数据重哈希迁移量大 🌟 双写+历史渐进迁移:新路由规则上线后,增量数据双写新老库;后台任务按设备ID区间逐批迁移历史数据,校验后切换读流量,整体不停服。

补充说明:热点设备写入倾斜的解法细节

如果用简单 hash % 16,某个设备爆量可能导致单库 CPU 飙升。我的防御手段是:

  1. 一致性哈希环,设备ID映射到虚拟节点(如每库150个虚拟节点),迁移时只影响相邻两个节点,压力更均匀。
  2. 网关层聚合:如果是边缘网关,每秒钟把同一个设备的多次上报打包成一个批量消息,写入次数从每秒100次降到1次。
  3. 服务端二级缓存:如果真的发现某个设备疯狂写入,可以在写入服务中对 device_id 加限流,超过阈值直接拒绝或采样写入。

面试官 😊:

代码功底很扎实,难点分析得也很透彻。特别是热点设备和冷热切换两个点,很多候选人会忽略。你这轮技术面我觉得没问题了,稍等我们安排下一轮。

【别走,交个朋友】

我是 Rain ,一个喜欢把复杂技术讲透、让代码落地的实践者。

如果你厌倦了四处收集碎片化的八股文,想看看一线项目里真实的架构决策和踩坑记录,欢迎来我的公众号「Rain的Java大神之路」坐坐。

在这里,我会把每一次技术复盘、每一个项目的设计源码,毫无保留地分享给你。

🔥技术这条路很酷,我们结伴同行。

🚀Keep Coding, Keep Loving ------ 与所有 Java 同路人共勉。

相关推荐
Rain的Java大神实战圈2 天前
数据库主从有延迟怎么处理
架构设计·场景设计题
Rain的Java大神实战圈6 天前
分库分表会带来哪些问题如何解决
架构设计·场景设计题
Rain的Java大神实战圈7 天前
介绍一下分库分表
架构设计·场景设计题
Rain的Java大神实战圈8 天前
线上慢SQL的排查思路
经验分享·架构设计·场景设计题
Rain的Java大神实战圈9 天前
MySQL单表有10亿数据如何做迁移
架构设计·面试经验·场景设计题
Rain的Java大神实战圈12 天前
如何统计亿级用户的在线状态
架构设计·面试经验·场景设计题
Rain的Java大神实战圈13 天前
扫码登录的实现原理
架构设计·面试经验·场景设计题
Rain的Java大神实战圈15 天前
每日排行榜功能如何设计
redis·架构设计·面试经验·场景设计题
Rain的Java大神实战圈21 天前
秒杀系统如何设计
场景设计题