首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >介绍一下分库分表

介绍一下分库分表

原创
作者头像
Java大神之路
发布2026-07-22 09:36:59
发布2026-07-22 09:36:59
210
举报
文章被收录于专栏:场景设计题场景设计题

介绍一下分库分表

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

为什么需要分库分表? 🤔

当 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();
}

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

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

问题

解决方案

最佳实践

分布式事务

最终一致性(MQ + 本地事务表) 2. Seata AT 模式

优先使用最终一致性,强一致性场景用 Seata

跨库关联查询

字段冗余 2. 数据同步到 ES 做聚合查询 3. 应用层多次查询

避免跨库 JOIN,用 ES 做复杂查询

分页排序

业务层禁止深分页 2. 用游标分页代替 offset 分页

SELECT * FROM order WHERE id > ? LIMIT 10

全局唯一 ID

雪花算法(Snowflake) 2. 美团 Leaf 3. 数据库自增序列

优先使用雪花算法,注意时钟回拨问题

扩容问题

提前规划分片数量(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 冲突

雪花算法(推荐) 2. 美团 Leaf 3. 数据库号段模式

优先使用改进版雪花算法;提前规划机器 ID 分配;监控时钟偏移

数据平滑迁移

业务不能停服,数据不能丢失

双写顺序错误导致数据不一致;数据校验不全面

双写 + 数据校验 + 流量切换 2. 全量迁移 + 增量同步

严格按照 "双写→校验→切读→停写" 四步走;灰度切换流量;保留回滚能力

分布式事务

跨库操作无法保证 ACID

强一致性方案性能差;最终一致性数据不一致

最终一致性(MQ + 本地事务表) 2. Seata AT 模式 3. TCC 模式

90% 场景用最终一致性;强一致性场景用 Seata AT;避免使用 TCC

跨库关联查询

无法执行跨库 JOIN 操作

应用层多次查询导致 N+1 问题;数据冗余导致不一致

字段冗余 2. 数据同步到 ES 做聚合 3. 应用层组装

绝对禁止跨库 JOIN;用 ES 做复杂查询和统计;冗余字段只保留不变的

分页排序

深分页 offset 过大导致全表扫描

传统分页在大数据量下超时;跨库排序不准确

游标分页(推荐) 2. 禁止深分页 3. 业务层限制分页深度

所有列表接口必须用游标分页;前端禁止跳页;最大分页深度不超过 100 页

热点数据问题

数据倾斜导致部分分片压力过大

某个用户 / 商家订单量占比超过 50%

热点数据单独分片 2. 读写分离 3. 缓存热点数据

提前识别热点数据;对超级用户做特殊处理;热点数据优先走缓存

扩容问题

分片数量不足需要扩容

数据重分布导致业务中断;扩容复杂度高

提前规划分片数量(2 的幂次) 2. 双倍扩容法 3. 一致性哈希

初始分 16 个库 64 个表;用双倍扩容法减少数据迁移量;扩容前做好备份

分布式主键查询

非分片键查询需要遍历所有分片

非分片键查询性能差;全表扫描

建立映射表 2. 数据同步到 ES 3. 业务层避免非分片键查询

所有查询必须带分片键;非分片键查询走 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 同路人共勉。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 介绍一下分库分表
    • 为什么需要分库分表? 🤔
    • 分库分表的两种核心方式 📊
      • 1. 垂直拆分(按业务维度)
      • 2. 水平拆分(按数据维度)
    • 核心分片策略 🎲
      • 核心代码示例(哈希分片路由):
    • 分库分表带来的问题与解决方案 ⚠️
    • 主流中间件对比 🛠️
    • 面试加分项 ✨
    • 核心代码实现(技术亮点)💻
      • 1. 改进版雪花算法(解决时钟回拨问题)✅
      • 2. 双写数据迁移核心逻辑(平滑无感知)✅
      • 3. 游标分页通用实现(解决深分页问题)✅
    • 核心技术难点深度解析 ⚠️
  • 真实现场模拟面试
    • 真实现场模拟面试
      • 面试官 😊:
      • 候选人 🧑‍💻:
      • 面试官 👍:
      • 候选人 🗂️:
      • 面试官 👏:
      • 候选人 🎯:
      • 面试官 🤔:
      • 候选人 🔑:
      • 面试官 ⚠️:
      • 候选人 🚫:
      • 面试官 📄:
      • 候选人 📑:
      • 面试官 ⚡:
      • 候选人 🔄:
      • 面试官 🛠:
      • 候选人 🔍:
      • 面试官 💡:
      • 候选人 🧩:
      • 面试官 💻:
      • 候选人 👨‍💻:
      • 面试官 🔍:
      • 候选人 🛡️:
      • 面试官 🧠:
      • 候选人 📋:
      • 面试官 🎉:
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档