分库分表会带来哪些问题如何解决

分库分表会带来哪些问题如何解决

分库分表是解决单库单表性能瓶颈的核心方案,但它本质是 "用复杂度换性能",会引入 7 大类核心问题,我结合实际项目经验逐一说明:

核心问题与解决方案对照表 📊

问题分类 具体表现 主流解决方案 推荐指数
分布式事务 跨库操作无法保证 ACID,数据不一致 1. 柔性事务(Seata AT 模式)2. 最终一致性(本地消息表 + MQ)3. TCC 模式(高并发核心场景) ⭐⭐⭐⭐⭐
跨库关联查询 JOIN 操作无法跨库执行,多表查询困难 1. 字段冗余(避免 JOIN)2. 数据同步到 ES 做宽表查询3. 应用层内存 JOIN(小表场景) ⭐⭐⭐⭐
分页排序聚合 跨表 LIMIT、ORDER BY、GROUP BY 结果错误 1. 全局排序:先查各分片再归并2. 深度分页:用游标分页替代 offset3. 聚合统计:预计算 + 定时更新 ⭐⭐⭐⭐
分布式 ID 生成 自增 ID 在多表间重复,主键冲突 1. 雪花算法(Snowflake)2. 美团 Leaf(双 buffer 优化)3. 数据库号段模式 ⭐⭐⭐⭐⭐
数据迁移扩容 扩容时数据重分布,业务停服 1. 双写迁移法(不停服)2. 按时间 / 范围分片(天然支持扩容)3. 一致性哈希(减少数据迁移量) ⭐⭐⭐⭐
多数据源管理 数据源切换复杂,连接池资源浪费 1. Sharding-JDBC(客户端分片)2. MyCat(服务端分片)3. 动态数据源路由 ⭐⭐⭐⭐
读写一致性 主从延迟导致读写不一致 1. 强一致性读(强制走主库)2. 延迟容忍度分级3. 读写分离 + 本地缓存 ⭐⭐⭐

关键问题深度解析 🔍

1. 分布式事务:最头疼的问题 😫

  • 避坑点:绝对不要用 2PC(两阶段提交),性能极差且阻塞严重
  • 项目首选:Seata AT 模式,无侵入性,适合 90% 业务场景
  • 高并发场景:用本地消息表 + RocketMQ 事务消息,保证最终一致性

2. 跨库查询:最常见的坑 🕳️

  • 最佳实践:设计阶段就避免跨库 JOIN,通过字段冗余解决
  • 复杂查询:将数据同步到 Elasticsearch,所有复杂查询走 ES
  • 反例:应用层循环查库,会导致 N+1 问题,性能灾难

3. 数据迁移:最容易出事故的环节 ⚠️

不停服双写迁移流程图:

分库分表设计原则 ✅

  1. 能不分就不分:单表 500 万行以下,优先优化索引和 SQL
  2. 分片键选择:优先用高频查询字段(如用户 ID、订单 ID)
  3. 分片策略:优先用范围分片 + 哈希分片结合
  4. 提前规划:一次分够,避免频繁扩容(建议一次分 16/32/64 表)

面试官追问高频点 💡

  • 问:雪花算法有什么问题?怎么解决?

    • 答:时钟回拨问题。解决方案:记录最后生成 ID 的时间戳,回拨时等待或抛出异常;美团 Leaf 用 ZooKeeper 做时钟校验。
  • 问:Sharding-JDBC 和 MyCat 怎么选?

    • 答:中小团队首选 Sharding-JDBC,无中间件,运维简单;大型团队可考虑 MyCat,集中式管理更方便。

核心代码实现(面试必背,带技术亮点)💻

1. 雪花算法(带时钟回拨防护,面试必写)✨

java 复制代码
public class SnowflakeIdGenerator {
    // 起始时间戳 (2020-01-01)
    private static final long START_TIMESTAMP = 1577836800000L;
    // 机器ID位数
    private static final long WORKER_ID_BITS = 5L;
    // 数据中心ID位数
    private static final long DATACENTER_ID_BITS = 5L;
    // 序列号位数
    private static final long SEQUENCE_BITS = 12L;

    // 最大值
    private static final long MAX_WORKER_ID = ~(-1L << WORKER_ID_BITS);
    private static final long MAX_DATACENTER_ID = ~(-1L << DATACENTER_ID_BITS);

    // 偏移量
    private static final long WORKER_ID_SHIFT = SEQUENCE_BITS;
    private static final long DATACENTER_ID_SHIFT = SEQUENCE_BITS + WORKER_ID_BITS;
    private static final long TIMESTAMP_SHIFT = SEQUENCE_BITS + WORKER_ID_BITS + DATACENTER_ID_BITS;

    // 序列号掩码
    private static final long SEQUENCE_MASK = ~(-1L << SEQUENCE_BITS);

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

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

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

        // 核心亮点:时钟回拨处理
        if (currentTimestamp < lastTimestamp) {
            long offset = lastTimestamp - currentTimestamp;
            // 回拨小于5ms,等待恢复
            if (offset <= 5) {
                try {
                    wait(offset << 1);
                    currentTimestamp = System.currentTimeMillis();
                    if (currentTimestamp < lastTimestamp) {
                        throw new RuntimeException("时钟回拨严重,无法生成ID");
                    }
                } catch (InterruptedException e) {
                    throw new RuntimeException("线程中断", e);
                }
            } else {
                throw new RuntimeException("时钟回拨超过5ms,无法生成ID");
            }
        }

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

        lastTimestamp = currentTimestamp;

        // 拼接64位ID
        return ((currentTimestamp - START_TIMESTAMP) << TIMESTAMP_SHIFT)
                | (datacenterId << DATACENTER_ID_SHIFT)
                | (workerId << WORKER_ID_SHIFT)
                | sequence;
    }

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

技术亮点:

  • ✅ 解决了雪花算法最致命的时钟回拨问题,支持小范围回拨等待
  • ✅ 使用synchronized保证单实例线程安全,性能优于ReentrantLock
  • ✅ 支持自定义机器 ID 和数据中心 ID,适配分布式部署
  • ✅ 序列号溢出自动等待下一毫秒,避免 ID 重复

2. Sharding-JDBC 自定义复合分片策略(哈希 + 时间)🔥

java 复制代码
/**
 * 复合分片策略:按用户ID哈希分表 + 按创建时间分库
 * 优点:数据分布均匀,支持按时间快速查询历史数据
 */
public class UserOrderComplexShardingStrategy implements ComplexKeysShardingAlgorithm<Long> {
    // 分库数量
    private static final int DB_COUNT = 4;
    // 每个库的分表数量
    private static final int TABLE_COUNT_PER_DB = 8;

    @Override
    public Collection<String> doSharding(Collection<String> availableTargetNames, ComplexKeysShardingValue<Long> shardingValue) {
        // 获取分片键值
        Map<String, Collection<Long>> valueMap = shardingValue.getColumnNameAndShardingValuesMap();
        Long userId = valueMap.get("user_id").iterator().next();
        Long createTime = valueMap.get("create_time").iterator().next();

        // 按时间分库:每半年一个库
        LocalDateTime time = LocalDateTime.ofInstant(Instant.ofEpochMilli(createTime), ZoneId.systemDefault());
        int year = time.getYear() - 2020;
        int month = time.getMonthValue();
        int dbIndex = (year * 2 + (month > 6 ? 1 : 0)) % DB_COUNT;

        // 按用户ID哈希分表
        int tableIndex = Math.abs(userId.hashCode()) % TABLE_COUNT_PER_DB;

        // 拼接实际表名
        String actualTableName = String.format("order_db_%d.order_%d", dbIndex, tableIndex);
        return Collections.singletonList(actualTableName);
    }

    @Override
    public void init(Properties properties) {}

    @Override
    public String getType() {
        return "USER_ORDER_COMPLEX_SHARDING";
    }
}

技术亮点:

  • ✅ 复合分片策略,同时利用两个分片键的优势
  • ✅ 按时间分库,天然支持历史数据归档和删除
  • ✅ 按用户 ID 哈希分表,数据分布均匀,避免数据倾斜
  • ✅ 支持动态调整分库分表数量,无需修改业务代码

3. 游标分页实现(解决深度分页性能问题)🚀

java 复制代码
@Service
public class OrderPaginationService {
    @Autowired
    private OrderMapper orderMapper;

    /**
     * 游标分页查询(性能比offset分页高100倍以上)
     * 对比:offset分页需要扫描前N条数据,游标分页直接定位到起始位置
     */
    public PageResult<Order> getOrdersByCursor(Long userId, Long cursor, int pageSize) {
        List<Order> orders;
        if (cursor == null) {
            // 第一页,没有游标
            orders = orderMapper.selectFirstPage(userId, pageSize);
        } else {
            // 后续页,使用游标(上一页最后一条记录的ID)
            orders = orderMapper.selectByCursor(userId, cursor, pageSize);
        }

        // 计算是否有下一页
        boolean hasNext = orders.size() == pageSize;
        // 获取下一页的游标
        Long nextCursor = hasNext ? orders.get(orders.size() - 1).getId() : null;

        return new PageResult<>(orders, hasNext, nextCursor);
    }
}

// Mapper接口
public interface OrderMapper {
    @Select("SELECT * FROM order WHERE user_id = #{userId} ORDER BY id DESC LIMIT #{pageSize}")
    List<Order> selectFirstPage(@Param("userId") Long userId, @Param("pageSize") int pageSize);

    @Select("SELECT * FROM order WHERE user_id = #{userId} AND id < #{cursor} ORDER BY id DESC LIMIT #{pageSize}")
    List<Order> selectByCursor(@Param("userId") Long userId, @Param("cursor") Long cursor, @Param("pageSize") int pageSize);
}

// 分页结果类
@Data
@AllArgsConstructor
public class PageResult<T> {
    private List<T> data;
    private boolean hasNext;
    private Long nextCursor;
}

技术亮点:

  • ✅ 彻底解决 offset 分页的深度分页性能问题,查询时间稳定
  • ✅ 实现简单,无需修改数据库结构
  • ✅ 支持无限分页,没有数据量限制
  • ✅ 避免了全表扫描,数据库压力小

分库分表核心技术难点与深度解决方案 🚩

技术难点 根本原因 最优解决方案 技术亮点 避坑指南
数据倾斜 分片键选择不当,部分分片数据量远超其他 1. 热点分片拆分2. 一致性哈希 + 虚拟节点3. 动态分片路由 热点数据自动识别与分流,支持在线调整分片权重 ❌ 绝对不要用状态字段(如订单状态)作为分片键
跨库 COUNT/SUM 聚合 无法直接执行 SQL 聚合,结果不准确 1. 预计算 + 定时任务更新2. 实时聚合引擎(ClickHouse)3. 应用层归并(小数据量) 冷热数据分离,热数据实时聚合,冷数据预计算 ❌ 不要在高并发场景下做跨库实时聚合
平滑扩容(不停服) 数据重分布需要迁移大量数据,影响业务 1. 一致性哈希(减少迁移量)2. 按范围分片(天然支持扩容)3. 双写迁移法(通用方案) 增量数据同步 + 全量校验,业务无感知 ❌ 扩容前必须做压测,避免迁移过程中数据库压力过大
读写一致性 主从延迟导致刚写入的数据读不到 1. 强制读主库(强一致性)2. 读自己写的走主库3. 延迟容忍度分级 基于业务场景动态选择读策略,平衡性能与一致性 ❌ 不要盲目开启读写分离,核心交易场景必须强制读主
分布式事务回滚失败 部分分支事务失败,无法全局回滚 1. Seata AT 模式自动回滚2. TCC 模式手动补偿3. 定时任务兜底校验 事务日志持久化,失败自动重试,人工干预接口 ❌ 避免长事务,事务超时时间设置合理
分库分表后的运维 多数据源管理复杂,备份恢复困难 1. 统一数据源管理平台2. 按分片备份恢复3. 自动化运维工具 一键扩容、一键迁移、一键备份,减少人工操作 ❌ 不要手动修改分片数据,所有操作通过统一平台执行

真实模拟面试

真实模拟面试

面试官(身子微微前倾,笑着看向你):

"咱们聊个实在的。你现在负责的订单系统,单库单表已经到瓶颈了,你决定分库分表。那你踩过哪些实打实的坑?怎么填的?一个一个说。" 😏

候选人(搓了搓手,记忆犹新):

"好,这一拆,真是捅了马蜂窝。我先给您画个痛点的全景图,您一看就明白👇"

"这四个玩意儿,一个比一个让人头秃。我按顺序跟您讲讲当时我们流的血和泪。" 😅

面试官:

"行,那先说说 ID 那块,自增主键冲突你们怎么搞的?"

候选人:

"原来 AUTO_INCREMENT 爽得很,拆成 32 张表后直接傻眼。UUID 太长、索引不友好,我们毫不犹豫上了雪花算法 Snowflake 的魔改版。

机器 ID 这块,因为我们跑在 k8s 上,直接用 Pod IP 最后两个字节异或一下当 workerId,部署时自动分配,不用人工配。

核心噩梦是时钟回拨。有一次 NTP 同步把时间往回拨了 3 秒,差点没把我们搞死。😱 后来我们在 ID 生成器里加了个硬防御:如果当前时间比上次生成时间小,就自旋等待差值加倍时间;要是回拨超过 500ms,直接抛异常、告警,那台机器熔断不接流量,等时钟追平再恢复。稳得很。"

面试官(点头):

"跨分片查询呢?按 user_id 分片,运营非要按时间范围查全部,还要求分页,这你们怎么'伺候'的?" 🤔

候选人:

"这就是最隐蔽的雷!我们当时差点跟产品打起来。最后形成了三套组合拳:

  • 联机查询 :强迫前端改成游标分页 (传 last_id),每次并行查所有分片 WHERE id > last_id ORDER BY id LIMIT N,内存里归并取最小 N 条,这样不分页,只有上下滑动。敢点"跳转到第 100 页"就直接报错 ------ 因为真实数据量下内存归并 100 页就是找死。🤯
  • 后台复杂检索 :把订单数据异构到 Elasticsearch,按时间、金额各种条件搜,拿到订单 ID 列表,再回分片 WHERE id IN(...) 查详细。延迟秒级,够用。
  • 报表分析:干脆全量甩到 ClickHouse,跟在线库物理隔离,爱怎么 group by 都行。

我拿 B+ 树推算说服了产品:分片下 LIMIT 100,20 这种偏移量越大性能越烂,产品听完也认了。" ✌️

面试官:

"说得好,那分布式事务呢?@Transactional 肯定废了,你们对一致性要求高的扣款怎么搞的?"

候选人:

"我们铁律就一条:能不用强分布式事务就不用。扣款这种场景用了 TCC 模式。

比如扣库存,Try 阶段用 Redis DECR 预占,同时把冻结流水写进一张 frozen_record 表(和业务库同分片,本地事务保证)。Confirm 阶段真正扣减数据库库存并删冻结记录,Cancel 则把 Redis 库存加回去。

这块最忌讳空谈理论。我们还做了双重兜底:一是所有 TCC 接口必须幂等,靠唯一流水号防重;二是每天凌晨跑一套对账脚本,对比支付单和库存流水,差一分钱马上报警。金融业务没有对账就是裸奔。" 💰

对于非核心链路,比如发送优惠券,我们直接用本地消息表 + RocketMQ 事务消息,保证最终一致性就完事了。

面试官:

"那最后说扩容吧,假如你开始是 8 库 * 16 表,现在要扩到 16 库 * 32 表,数据要重分布,怎么弄不丢数据还不长时间停机?" ⏳

候选人:

"扩容绝对是终极大考,我们用的双写 + 灰度切流的方案,分四步走:

  1. 升级路由:让所有分片路由支持新旧两组分片键算法(我们用的是一致性哈希,150 个虚拟节点,比取模好扩容)。
  2. 开启双写:所有新写入同时发到新旧两个分片集群,读仍走老集群。
  3. 历史数据回流:凌晨用离线任务扫描老表,把数据按新算法 insert 到新表,忽略重复。同时校验工具不停抽 rows 比对 MD5,不一致就重导。
  4. 切读:校验一致后,灰度把读流量切到新集群,观察无报错,最后把老集群双写下线。

有一次迁移了 2 亿条数据,干了一整晚,好在全自动脚本稳住了,第二天早上只公告停机了 20 分钟。😌 如果不追求秒级无损,停服搬家最稳妥,不丢人。"

面试官(微笑):

"总结得不错,能看出你是真刀真枪干过的。那最后一个开放点,做完这些你架构上最大的体会是什么?"

候选人:

"分库分表本质是把压力分散,但把查询复杂度集中了。所以不能光拆,要同步做'减熵':联机库只保留最核心的主键查询和小范围扫描,把复杂分析、搜索甩给 ES/列存。代码里杜绝跨分片 join,宁肯多查一次。这样架构才不会腐化成一坨没人敢动的屎山。" 💩

💻 硬核时刻:代码实现与难点总结

面试官(双手交叉,饶有兴趣):

"别光说方案,把你刚才提到的 雪花算法防时钟回拨、跨分片游标分页、TCC 幂等扣库存 这几个最见功底的代码,挑关键部分给我看看。再把你整个分库分表面临的技术难点,一表给我汇总。" ⌨️

候选人(打开笔记本,认真翻出代码片段):

"没问题,我直接上 Java 伪代码,去掉业务敏感词,保留骨骼,您一看就懂。"

1. 分布式 ID 生成:时钟回拨熔断版 Snowflake

java 复制代码
public class SnowflakeIdGenerator {
    private final long epoch = 1577836800000L; // 2020-01-01
    private final long workerIdBits = 10L;
    private final long sequenceBits = 12L;
    private final long maxWorkerId = ~(-1L << workerIdBits);
    private final long sequenceMask = ~(-1L << sequenceBits);
    private final long workerIdShift = sequenceBits;
    private final long timestampShift = sequenceBits + workerIdBits;

    private long workerId;
    private long sequence = 0L;
    private long lastTimestamp = -1L;
    private final long maxBackwardMs = 500; // 允许最大回拨 500ms

    public SnowflakeIdGenerator(long workerId) {
        if (workerId > maxWorkerId || workerId < 0) throw new IllegalArgumentException();
        this.workerId = workerId;
    }

    public synchronized long nextId() {
        long timestamp = System.currentTimeMillis();
        // 🧨 时钟回拨防御
        if (timestamp < lastTimestamp) {
            long offset = lastTimestamp - timestamp;
            if (offset <= maxBackwardMs) {
                try { Thread.sleep(offset << 1); } // 等两倍差值
                catch (InterruptedException e) { Thread.currentThread().interrupt(); }
                timestamp = System.currentTimeMillis();
                if (timestamp < lastTimestamp) throw new RuntimeException("Clock moved backwards!");
            } else {
                throw new RuntimeException("Clock moved backwards too much!");
            }
        }
        if (timestamp == lastTimestamp) {
            sequence = (sequence + 1) & sequenceMask;
            if (sequence == 0) timestamp = tilNextMillis(lastTimestamp);
        } else {
            sequence = 0L;
        }
        lastTimestamp = timestamp;
        return ((timestamp - epoch) << timestampShift) | (workerId << workerIdShift) | sequence;
    }

    private long tilNextMillis(long lastTimestamp) {
        long ts = System.currentTimeMillis();
        while (ts <= lastTimestamp) ts = System.currentTimeMillis();
        return ts;
    }
}

技术亮点:内置两级时钟回拨处理 ------ 小回拨自旋等待,超限直接熔断抛异常,配合监控保证 ID 绝对递增。

2. 跨分片游标分页:内存归并 + 防 OOM

java 复制代码
public List<Order> scrollOrders(long lastId, int limit, List<Shard> shards) {
    int shardCount = shards.size();
    List<CompletableFuture<List<Order>>> futures = new ArrayList<>();
    
    // 并行查询每个分片
    for (Shard shard : shards) {
        futures.add(CompletableFuture.supplyAsync(() ->
            shard.query("SELECT * FROM order WHERE id > ? ORDER BY id LIMIT ?", lastId, limit)
        ));
    }
    
    // 内存归并,使用最小堆取全局最小的 N 条
    PriorityQueue<Order> minHeap = new PriorityQueue<>(Comparator.comparingLong(Order::getId));
    for (CompletableFuture<List<Order>> future : futures) {
        List<Order> part = future.join();
        if (part.size() > limit) part = part.subList(0, limit); // 防爆
        minHeap.addAll(part);
    }
    
    List<Order> result = new ArrayList<>(limit);
    while (!minHeap.isEmpty() && result.size() < limit) {
        result.add(minHeap.poll());
    }
    return result;
}

✨ 技术亮点:CompletableFuture 并行,PriorityQueue 归并取 Top N,强制截断单分片结果集防 OOM,天然适配瀑布流分页。

3. TCC 库存扣减:幂等 + Redis 预占 + 本地事务

java 复制代码
// Try 阶段
@Transactional
public boolean tryFreeze(String orderId, long skuId, int count) {
    // 幂等:先查流水
    if (frozenRecordDao.exists(orderId)) return true;
    // Redis 预占库存,原子操作
    Long remain = redis.decrBy("stock:sku:" + skuId, count);
    if (remain == null || remain < 0) {
        redis.incrBy("stock:sku:" + skuId, count); // 回滚 Redis
        return false;
    }
    // 写冻结流水(和订单同库,本地事务)
    frozenRecordDao.insert(new FrozenRecord(orderId, skuId, count));
    return true;
}

// Confirm 阶段
@Transactional
public void confirm(String orderId) {
    FrozenRecord record = frozenRecordDao.selectForUpdate(orderId);
    if (record == null || record.getStatus() == CONFIRMED) return; // 幂等
    // 正式扣减数据库库存
    stockDao.decrease(record.getSkuId(), record.getCount());
    // 标记完成
    record.setStatus(CONFIRMED);
    frozenRecordDao.update(record);
    // Redis 不需要管,预占时已经扣了
}

// Cancel 阶段
@Transactional
public void cancel(String orderId) {
    FrozenRecord record = frozenRecordDao.selectForUpdate(orderId);
    if (record == null || record.getStatus() == CANCELLED) return; // 幂等
    // 释放 Redis 库存
    redis.incrBy("stock:sku:" + record.getSkuId(), record.getCount());
    record.setStatus(CANCELLED);
    frozenRecordDao.update(record);
}

技术亮点 :所有操作靠流水状态做幂等,Redis 和 DB 不一致时依靠每天对账脚本修复,实现最终一致。

4. 一致性哈希路由 & 扩容双写

java 复制代码
public class ConsistentHashRouter {
    private final TreeMap<Integer, Shard> ring = new TreeMap<>();
    private final int virtualNodeCnt;
    private final List<Shard> newShardGroup = null; // 扩容时临时持有新集群

    public Shard route(long shardKey) {
        int hash = hash(shardKey);
        if (ring.ceilingEntry(hash) != null) return ring.ceilingEntry(hash).getValue();
        return ring.firstEntry().getValue();
    }

    // 双写:同时写入新旧集群
    public void insertWithDualWrite(Order order) {
        Shard oldShard = route(order.getUserId());
        oldShard.insert(order);
        if (newShardGroup != null) {
            Shard newShard = newConsistentHashRoute(order.getUserId());
            newShard.insert(order);
        }
    }
}

技术亮点TreeMap 实现环,虚拟节点均匀分布;扩容期间 newShardGroup 非空时开启双写,灰度切换读流量。

📊 技术难点 & 解决方案速查表

技术难点 问题表现 解决方案 关键手段
分布式 ID 冲突 多库自增主键重复 雪花算法 + workerId 自动分配 时钟回拨两级防御(自旋&熔断)
跨分片排序分页 全局分页需归并所有分片 游标分页 + 内存归并 + 异构索引 拒绝跳页,限制单分片结果集
分布式事务一致 跨库操作原子性丧失 TCC + 本地消息表 + 对账 幂等设计,Redis 预占,每日对账
扩容数据迁移 新分片加入需数据重分布 一致性哈希 + 双写 + 灰度切流 虚拟节点,MD5 校验,停服兜底
分片热点倾斜 个别分片负载过高 分片键打散(如尾部加随机数)或再分片 监控分区 QPS,动态路由感知

面试官(满意地合上笔记本):

"代码清爽,回拨处理、归并防 OOM、幂等设计都到位了,看起来是写过上万行核心代码的人。"

别走,交个朋友

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

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

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

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

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

相关推荐
Rain的Java大神实战圈1 天前
介绍一下分库分表
架构设计·场景设计题
doiito2 天前
【RUST AI】把 TTS 搬进浏览器:kokoroi-rs 的 WASM 实践
ai·rust·架构设计
Rain的Java大神实战圈2 天前
线上慢SQL的排查思路
经验分享·架构设计·场景设计题
Rain的Java大神实战圈3 天前
MySQL单表有10亿数据如何做迁移
架构设计·面试经验·场景设计题
2301_768103494 天前
HarmonyOS趣味相机实战第13篇:CameraPicker与CameraKit选型、系统拍照回退及架构边界
harmonyos·arkts·架构设计·camerapicker·camerakit
Rain的Java大神实战圈6 天前
如何统计亿级用户的在线状态
架构设计·面试经验·场景设计题
MateCloud微服务6 天前
从 prompt 直连反模式到企业 AI 中台:一次架构演进的完整拆解算力、模型、智能体、技能、MCP 五件套自持
人工智能·架构·prompt·架构设计·mcp·太一企业版·ai中台
Rain的Java大神实战圈7 天前
扫码登录的实现原理
架构设计·面试经验·场景设计题
Rain的Java大神实战圈8 天前
附近的人功能如何设计
架构设计·面试经验