介绍一下分库分表

介绍一下分库分表

分库分表本质上是数据库水平扩展的核心方案,当单库单表的数据量和并发量达到瓶颈时,通过 "化整为零" 的方式将数据分散到多个库和表中,解决单机存储和性能问题。

为什么需要分库分表? 🤔

当 MySQL 单表数据量超过500 万行2GB ,单库 QPS 超过1000时,查询性能会急剧下降。分库分表就是为了解决以下三个核心瓶颈:

瓶颈类型 具体表现 临界点
存储瓶颈 单库磁盘空间不足,备份恢复慢 单库 > 100GB
性能瓶颈 查询、写入延迟飙升,索引失效 单表 > 500 万行
并发瓶颈 连接数耗尽,数据库锁竞争激烈 单库 QPS>1000

分库分表的两种核心方式 📊

1. 垂直拆分(按业务维度)

  • 垂直分库:将一个大库按业务模块拆分为多个独立的库,比如用户库、订单库、商品库。

  • 垂直分表:将一个大表按字段冷热程度拆分为多个表,比如订单主表(常用字段)和订单详情表(大字段)。

  • 优点:业务清晰,便于团队维护,冷热数据分离。

  • 缺点:无法解决单表数据量过大的问题。

2. 水平拆分(按数据维度)

  • 水平分库:将同一个表的数据按规则分散到多个库中。

  • 水平分表:将同一个表的数据按规则分散到同一个库的多个表中。

  • 优点:彻底解决单表数据量和并发问题。

  • 缺点:引入了分布式事务、跨库关联等复杂问题。

核心分片策略 🎲

最常用的是哈希分片范围分片,各有优劣:

分片策略 实现方式 优点 缺点 适用场景
哈希分片 库索引=user_id%库数量``表索引=user_id%表数量 数据分布均匀,热点分散 扩容麻烦,需要数据迁移 用户中心、订单系统
范围分片 按时间、ID 范围分片 扩容简单,便于按范围查询 容易产生热点数据 日志系统、历史数据
一致性哈希 虚拟节点环 扩容时只迁移少量数据 实现复杂 缓存系统、分布式存储

核心代码示例(哈希分片路由):

java 复制代码
// 计算分片索引
public ShardingResult sharding(Long userId, int dbCount, int tableCount) {
    // 先分库
    long dbIndex = userId % dbCount;
    // 再分表
    long tableIndex = (userId / dbCount) % tableCount;
    
    return ShardingResult.builder()
            .dbName("order_db_" + dbIndex)
            .tableName("order_" + tableIndex)
            .build();
}

分库分表带来的问题与解决方案 ⚠️

这是面试官最关心的部分,也是分库分表的核心难点:

问题 解决方案 最佳实践
分布式事务 1. 最终一致性(MQ + 本地事务表)2. Seata AT 模式 优先使用最终一致性,强一致性场景用 Seata
跨库关联查询 1. 字段冗余2. 数据同步到 ES 做聚合查询3. 应用层多次查询 避免跨库 JOIN,用 ES 做复杂查询
分页排序 1. 业务层禁止深分页2. 用游标分页代替 offset 分页 SELECT * FROM order WHERE id > ? LIMIT 10
全局唯一 ID 1. 雪花算法(Snowflake)2. 美团 Leaf3. 数据库自增序列 优先使用雪花算法,注意时钟回拨问题
扩容问题 1. 提前规划分片数量(2 的幂次)2. 双倍扩容法 初始分 16 个库 64 个表,足够支撑百亿级数据

主流中间件对比 🛠️

中间件 架构 优点 缺点 推荐指数
Sharding-JDBC 客户端层 轻量,无代理,性能高 不支持跨库事务 ⭐⭐⭐⭐⭐
MyCat 服务端代理 支持多种数据库,功能全 性能损耗大,运维复杂 ⭐⭐⭐
Sharding-Proxy 服务端代理 透明化接入,支持多语言 有性能损耗 ⭐⭐⭐⭐

面试加分项 ✨

  1. 先优化再拆分:先做 SQL 优化、索引优化、读写分离,实在不行再分库分表
  2. 避免过度拆分:拆分越细,复杂度越高,维护成本越大
  3. 数据迁移方案:双写 + 数据校验 + 流量切换的平滑迁移方案
  4. 监控告警:对各分片的数据量、QPS、延迟进行实时监控

核心代码实现(技术亮点)💻

1. 改进版雪花算法(解决时钟回拨问题)✅

技术亮点:增加时钟回拨检测与等待机制,支持自定义机器 ID,生成 64 位有序唯一 ID

java 复制代码
public class SnowflakeIdGenerator {
    // 起始时间戳 (2020-01-01)
    private static final long START_TIMESTAMP = 1577808000000L;
    
    // 各部分位数
    private static final long WORKER_ID_BITS = 5L;
    private static final long DATA_CENTER_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_DATA_CENTER_ID = ~(-1L << DATA_CENTER_ID_BITS);
    
    // 偏移量
    private static final long WORKER_ID_SHIFT = SEQUENCE_BITS;
    private static final long DATA_CENTER_ID_SHIFT = SEQUENCE_BITS + WORKER_ID_BITS;
    private static final long TIMESTAMP_SHIFT = SEQUENCE_BITS + WORKER_ID_BITS + DATA_CENTER_ID_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_DATA_CENTER_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) & ~(-1L << SEQUENCE_BITS);
            // 序列号溢出,等待下一毫秒
            if (sequence == 0) {
                currentTimestamp = waitNextMillis(lastTimestamp);
            }
        } else {
            sequence = 0L;
        }
        
        lastTimestamp = currentTimestamp;
        
        // 拼接ID
        return ((currentTimestamp - START_TIMESTAMP) << TIMESTAMP_SHIFT)
                | (dataCenterId << DATA_CENTER_ID_SHIFT)
                | (workerId << WORKER_ID_SHIFT)
                | sequence;
    }

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

2. 双写数据迁移核心逻辑(平滑无感知)✅

技术亮点:状态机控制迁移流程,保证数据一致性,支持回滚

java 复制代码
@Service
public class DataMigrationService {
    // 迁移状态枚举
    public enum MigrationStatus {
        INIT,       // 初始状态
        DOUBLE_WRITE, // 双写阶段
        DATA_CHECK,   // 数据校验阶段
        SWITCH_READ,  // 切换读流量
        FINISH        // 迁移完成
    }

    @Autowired
    private OldOrderMapper oldOrderMapper;
    @Autowired
    private NewOrderMapper newOrderMapper;
    @Autowired
    private RedisTemplate<String, String> redisTemplate;

    // 技术亮点:根据状态自动路由写入
    public void createOrder(Order order) {
        MigrationStatus status = getMigrationStatus();
        
        // 先写旧库
        oldOrderMapper.insert(order);
        
        // 双写阶段:再写新库
        if (status == MigrationStatus.DOUBLE_WRITE || 
            status == MigrationStatus.DATA_CHECK ||
            status == MigrationStatus.SWITCH_READ) {
            try {
                newOrderMapper.insert(order);
            } catch (Exception e) {
                // 记录失败日志,后续补偿
                log.error("双写新库失败,orderId:{}", order.getId(), e);
                compensateWrite(order);
            }
        }
    }

    // 技术亮点:根据状态自动路由查询
    public Order getOrderById(Long orderId) {
        MigrationStatus status = getMigrationStatus();
        
        if (status == MigrationStatus.SWITCH_READ || status == MigrationStatus.FINISH) {
            return newOrderMapper.selectById(orderId);
        } else {
            return oldOrderMapper.selectById(orderId);
        }
    }

    // 数据校验:对比新旧库数据一致性
    public boolean checkDataConsistency(Long startId, Long endId) {
        List<Order> oldOrders = oldOrderMapper.selectByIdRange(startId, endId);
        List<Order> newOrders = newOrderMapper.selectByIdRange(startId, endId);
        
        // 技术亮点:用MD5对比数据内容,避免逐字段比较
        Map<Long, String> oldOrderMap = oldOrders.stream()
                .collect(Collectors.toMap(Order::getId, this::calculateOrderMd5));
        Map<Long, String> newOrderMap = newOrders.stream()
                .collect(Collectors.toMap(Order::getId, this::calculateOrderMd5));
        
        return oldOrderMap.equals(newOrderMap);
    }

    private String calculateOrderMd5(Order order) {
        // 将订单所有字段拼接后计算MD5
        String content = JSON.toJSONString(order);
        return DigestUtils.md5Hex(content);
    }

    private MigrationStatus getMigrationStatus() {
        String status = redisTemplate.opsForValue().get("migration:status");
        return status == null ? MigrationStatus.INIT : MigrationStatus.valueOf(status);
    }
}

3. 游标分页通用实现(解决深分页问题)✅

技术亮点:避免 offset 深分页性能问题,支持任意字段排序

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

    // 技术亮点:游标分页,性能稳定,不受数据量影响
    public PageResult<Order> queryOrdersByCursor(Long lastId, int pageSize, String status) {
        // 查询条件:id > lastId(游标)
        List<Order> orders = orderMapper.selectByCursor(lastId, pageSize, status);
        
        // 计算是否有下一页
        boolean hasNext = orders.size() == pageSize;
        // 获取下一页的游标
        Long nextCursor = hasNext ? orders.get(orders.size() - 1).getId() : null;
        
        return PageResult.<Order>builder()
                .data(orders)
                .hasNext(hasNext)
                .nextCursor(nextCursor)
                .build();
    }
}

// 对应的SQL
@Select("SELECT * FROM order_${tableIndex} " +
        "WHERE id > #{lastId} AND status = #{status} " +
        "ORDER BY id ASC LIMIT #{pageSize}")
List<Order> selectByCursor(@Param("lastId") Long lastId, 
                          @Param("pageSize") int pageSize, 
                          @Param("status") String status);

核心技术难点深度解析 ⚠️

这是面试官最关心的部分,也是分库分表的核心价值所在:

技术难点 问题本质 常见坑 解决方案 最佳实践
全局唯一 ID 生成 分布式环境下无法使用数据库自增 ID 时钟回拨导致 ID 重复;机器 ID 冲突 1. 雪花算法(推荐)2. 美团 Leaf3. 数据库号段模式 优先使用改进版雪花算法;提前规划机器 ID 分配;监控时钟偏移
数据平滑迁移 业务不能停服,数据不能丢失 双写顺序错误导致数据不一致;数据校验不全面 1. 双写 + 数据校验 + 流量切换2. 全量迁移 + 增量同步 严格按照 "双写→校验→切读→停写" 四步走;灰度切换流量;保留回滚能力
分布式事务 跨库操作无法保证 ACID 强一致性方案性能差;最终一致性数据不一致 1. 最终一致性(MQ + 本地事务表)2. Seata AT 模式3. TCC 模式 90% 场景用最终一致性;强一致性场景用 Seata AT;避免使用 TCC
跨库关联查询 无法执行跨库 JOIN 操作 应用层多次查询导致 N+1 问题;数据冗余导致不一致 1. 字段冗余2. 数据同步到 ES 做聚合3. 应用层组装 绝对禁止跨库 JOIN;用 ES 做复杂查询和统计;冗余字段只保留不变的
分页排序 深分页 offset 过大导致全表扫描 传统分页在大数据量下超时;跨库排序不准确 1. 游标分页(推荐)2. 禁止深分页3. 业务层限制分页深度 所有列表接口必须用游标分页;前端禁止跳页;最大分页深度不超过 100 页
热点数据问题 数据倾斜导致部分分片压力过大 某个用户 / 商家订单量占比超过 50% 1. 热点数据单独分片2. 读写分离3. 缓存热点数据 提前识别热点数据;对超级用户做特殊处理;热点数据优先走缓存
扩容问题 分片数量不足需要扩容 数据重分布导致业务中断;扩容复杂度高 1. 提前规划分片数量(2 的幂次)2. 双倍扩容法3. 一致性哈希 初始分 16 个库 64 个表;用双倍扩容法减少数据迁移量;扩容前做好备份
分布式主键查询 非分片键查询需要遍历所有分片 非分片键查询性能差;全表扫描 1. 建立映射表2. 数据同步到 ES3. 业务层避免非分片键查询 所有查询必须带分片键;非分片键查询走 ES;映射表只存主键和分片键

真实现场模拟面试

真实现场模拟面试

面试官 😊:

看你简历里写了海量数据优化经验,那咱先聊个场景:假设现在有个电商订单表,数据量干到 2 亿了,查询越来越慢,数据库连接池动不动就满了。你会怎么办?

候选人 🧑‍💻:

这个问题其实很典型。单表到 2 亿,光加索引、上缓存已经很难压住了。因为 InnoDB 的 B+ 树可能已经 4~5 层高,磁盘 IO 剧增,再加上主从延迟,很容易拖垮整个库。所以这种量级,我肯定会考虑分库分表

简单说,就是把原本一个库一张表的数据,按一定规则拆分到多个库多张表里,把存储和访问压力分散开。

面试官 👍:

嗯,方向对。那具体怎么拆?你能说说你理解的拆分策略吗?

候选人 🗂️:

好,我画个图可能更清楚点:

核心就两大维度:

  • 垂直拆分:按业务模块拆库(用户库、订单库),或按字段冷热拆表(把大字段拆到扩展表)。
  • 水平拆分:按数据行拆分,比如把订单表按用户 ID 拆成 16 个库,每个库 16 张表。

生产上一般先垂直拆分,再把核心业务的大表做水平拆分,组合着来。

面试官 👏:

思路很清晰。那水平拆分时,最关键的是什么?分片键和路由算法怎么选?

候选人 🎯:

最关键的是分片键,后续所有查询都得带着它,否则就得全分片扫描,直接禁用。

常见的路由算法我对比一下:

算法 特点 典型用法
哈希取模 分布均匀,但扩容迁移痛苦 db_idx = user_id % 4
一致性哈希 扩容平滑,可用虚拟节点避免数据倾斜 环形哈希空间
范围路由 易归档,但易产生写热点 order_db_2025

前期业务量不大时哈希取模够用,但后期扩容一般会配合双写 + 历史数据迁移,或者直接用像 ShardingSphere 这种支持在线扩缩容的中间件。

面试官 🤔:

分完表之后,原来单库的主键自增不能用了,分布式 ID 你怎么设计?

候选人 🔑:

对,这是个经典坑。我会这么选:

  • 雪花算法:趋势递增,纯内存生成,性能高,但依赖机器时钟,可能回拨。
  • 号段模式:比如美团 Leaf,从 DB 批量拉号段,解决时钟回拨,且能支撑更高并发。
  • Redis 自增:简单但持久化有风险,一般不作为主方案。

目前我们用雪花算法比较多,做好时钟回拨兜底(比如记录上一次时间戳,拒绝回拨并稍等重试)。

面试官 ⚠️:

继续深挖,分完后如果有个查询要关联用户表查用户名,怎么办?跨库 JOIN 怎么处理?

候选人 🚫:

原则就一条:严禁跨库 JOIN。我的解法:

  • 数据异构:通过 Canal 监听 binlog,把订单和用户数据异构到 Elasticsearch 构建宽表,查询走 ES。
  • 应用层拼接:先查用户服务拿到 user_id 列表,再带 id 去订单分片查,但要注意 in 元素不要太多。
  • 全局字典表:变动极少的基础数据,可以在每个分片冗余一份。

面试官 📄:

那分页呢?比如订单列表按时间倒序,还要翻页,分片后怎么处理?

候选人 📑:

分片下每个表都是局部有序,全局分页就变成"归并排序"。

简单做法:从所有分片各取前 N 条,内存排序后返回,但深分页性能差。

我会要求产品:禁止跳页,改用滚动分页(传最后一条的时间戳)。如果是后台管理系统必须要跳页,就用 ES 做搜索页,或者采用"二次查询法"保精准度,但复杂度很高。

面试官 ⚡:

分布式事务呢?比如下单跨库扣库存、生成订单,怎么保证一致性?

候选人 🔄:

首先尽量设计避免跨库事务,实在避免不了就用柔性事务

  • Seata AT 模式:对业务侵入小,但性能损耗要评估。
  • TCC 模式:自己实现 Try-Confirm-Cancel,性能好但代码侵入大。
  • 本地消息表 + 可靠消息:最终一致,适合对实时性要求不极高的场景。

一般内部系统用 AT 模式快速落地,高并发走 TCC 或消息最终一致。

面试官 🛠:

你之前用哪些中间件?选型时会考虑什么?

候选人 🔍:

我比较喜欢 ShardingSphere-JDBC,客户端 SDK 模式,性能高没代理层损耗,但只绑定 Java。如果是多语言环境,可能会考虑 Mycat 或 DBLE 这类代理。云原生方向 Vitess 做得很好,但运维成本高。

选型主要看:团队语言栈、是否接受客户端升级、运维复杂度、以及是否有在线扩缩容需求。

面试官 💡:

最后来个开放题:如果让你设计一个透明分库分表方案,你会怎么设计?

候选人 🧩:

我会从几个核心点考虑:

  • 封装统一数据源,应用层无感知,SQL 解析时自动获取分片键路由。
  • 强制分片键校验:如果 SQL 里没带分片键,直接拒绝执行,防止全表扫。
  • 读写分离联动:主从结构,写走主、读走从,分片内再做读写分离。
  • 冷热分离:热数据留 MySQL 分片,冷数据归档到 HBase / ES,低成本保留全量查询。
  • 平滑扩容:一致性哈希 + 双写 + 灰度切流 + 增量数据同步,最后再清冗余。

面试官 💻:

理论聊得挺透,那来点实际的。如果让你写一个雪花算法分布式ID生成器,并处理时钟回拨,你会怎么写?展示一段核心代码吧。

候选人 👨‍💻:

好的,我写一个生产可用的精简版:

java 复制代码
public class SnowflakeIdWorker {
    // 起始时间戳 (2020-01-01)
    private final long twepoch = 1577808000000L;
    // 机器ID所占位数
    private final long workerIdBits = 5L;
    private final long datacenterIdBits = 5L;
    // 支持的最大机器ID (31)
    private final long maxWorkerId = ~(-1L << workerIdBits);
    private final long maxDatacenterId = ~(-1L << datacenterIdBits);
    // 序列在id中占的位数
    private final long sequenceBits = 12L;
    // 机器ID左移12位
    private final long workerIdShift = sequenceBits;
    // 数据中心ID左移17位
    private final long datacenterIdShift = sequenceBits + workerIdBits;
    // 时间戳左移22位
    private final long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits;
    // 序列掩码 (4095)
    private final long sequenceMask = ~(-1L << sequenceBits);

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

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

    public synchronized long nextId() {
        long timestamp = timeGen();
        // 🔥 时钟回拨处理:如果当前时间小于上次生成ID的时间戳,说明时钟回拨了
        if (timestamp < lastTimestamp) {
            long offset = lastTimestamp - timestamp;
            if (offset <= 5) {
                // 回拨5ms以内,等待两倍时间
                try { Thread.sleep(offset << 1); } catch (InterruptedException e) {}
                timestamp = timeGen();
                if (timestamp < lastTimestamp) {
                    throw new RuntimeException("Clock moved backwards, refusing to generate id");
                }
            } else {
                throw new RuntimeException("Clock moved backwards too much");
            }
        }
        // 同一毫秒内,序列递增
        if (lastTimestamp == timestamp) {
            sequence = (sequence + 1) & sequenceMask;
            if (sequence == 0) {
                // 当前毫秒序列已满,等待下一毫秒
                timestamp = tilNextMillis(lastTimestamp);
            }
        } else {
            sequence = 0L;
        }
        lastTimestamp = timestamp;
        return ((timestamp - twepoch) << timestampLeftShift)
                | (datacenterId << datacenterIdShift)
                | (workerId << workerIdShift)
                | sequence;
    }

    private long tilNextMillis(long lastTimestamp) {
        long timestamp = timeGen();
        while (timestamp <= lastTimestamp) {
            timestamp = timeGen();
        }
        return timestamp;
    }

    private long timeGen() {
        return System.currentTimeMillis();
    }
}

💡 亮点 :对时钟回拨做了分级容错,回拨5ms内用等待自动恢复,超过则快速失败或报警,保证ID全局唯一。

面试官 🔍:

不错,代码细节到位。那在实际项目里,你如何保证不带分片键的SQL直接拒绝?能写个拦截器吗?

候选人 🛡️:

我们用 ShardingSphere 的 Hint 强制路由 + 自定义 SQL 解析拦截:

java 复制代码
@Aspect
@Component
public class ShardingKeyCheckAspect {
    @Around("@annotation(com.example.annotation.ForceShardingKey)")
    public Object checkShardingKey(ProceedingJoinPoint joinPoint) throws Throwable {
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        // 获取参数中的分片键
        Object[] args = joinPoint.getArgs();
        Long shardingKey = null;
        for (Object arg : args) {
            if (arg instanceof ShardingKeyHolder) {
                shardingKey = ((ShardingKeyHolder) arg).getShardingKey();
            }
        }
        // 如果没带分片键,直接抛异常,避免全库扫描
        if (shardingKey == null) {
            throw new BizException("查询必须携带分片键");
        }
        // 使用 HintManager 强制路由到指定分片
        try (HintManager hintManager = HintManager.getInstance()) {
            hintManager.addDatabaseShardingValue("order", shardingKey);
            hintManager.addTableShardingValue("order", shardingKey);
            return joinPoint.proceed();
        }
    }
}

这样在 DAO 层方法上加 @ForceShardingKey,分片键缺失直接报错,防止线上出现慢查询拖垮所有分片。

面试官 🧠:

很实用。最后请你总结一下,整套分库分表方案的核心技术难点和解决方案

候选人 📋:

可以,我整理成表格,清晰明了 😎

🧩 技术难点 ⚡ 问题描述 ✅ 解决方案
分布式ID生成 自增主键不可用,需全局唯一且趋势递增 雪花算法(处理时钟回拨)、号段模式(Leaf)、Redis 自增(备选)
跨分片查询 JOIN、聚合、排序需要合并多分片数据 数据异构到 ES / 宽表、应用层拼接、禁止不带分片键查询
分页与排序 各分片局部有序,全局分页效率低 滚动分页(禁止跳页)、ES 搜索、二次查询法(精准但复杂)
分布式事务 跨库写操作无法使用单机ACID 尽量避免跨库事务;柔性事务:Seata AT/TCC/Saga + 本地消息表
扩容与数据迁移 哈希取模扩容需重新分布数据 一致性哈希+虚拟节点、双写+历史迁移+灰度切流、ShardingSphere在线扩缩容
SQL支持度 分库分表后复杂SQL语法受限制 提前梳理业务SQL,改造为简单查询;SQL解析拦截非法语句
运维监控 多数据源管理复杂,故障定位难 统一数据源监控、慢SQL采集、分片路由日志埋点

面试官 🎉:

总结得很清楚,既有理论深度,又有代码落地和坑点复盘。这套方案拿到大厂去扛高并发场景完全没问题,今天面试就聊到这儿,辛苦了!

别走,交个朋友

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

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

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

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

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

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