阿里面试:分库分表怎么做的?分片键怎么选的?数据迁移怎么保证一致性?
公众号名称:Fox爱分享
作者名称:Fox爱分享
发布时间:2026-07-07 07:00
有个读者找我,说他面阿里后端岗,项目环节聊得还行,结果面试官突然话锋一转,甩了三个连环问:
分库分表怎么做的?分片键怎么选的?数据迁移怎么保证一致性?
他说前两题还能答一点,但都是”按用户 ID 取模""用 ShardingSphere”这种关键词级别的回答。到第三题数据迁移一致性,直接卡死,说了一句”双写”,面试官追了一句”双写过程中老库写成功新库写失败怎么办”,他整个人就傻了。
这三题其实是面试官精心设计的递进陷阱。第一题考你做没做过,第二题考你想没想清楚,第三题考你踩没踩过坑。你第一题答得虚,第二题就接不住,第三题直接露馅。
太多人的分库分表经验是简历上写一句话——“参与订单系统分库分表改造”,面试官一追问,发现就是用了个中间件配了个分片规则,既不知道为什么这么选,也没经历过数据迁移的生死线。
今天我把这三题拆开聊,每一层都给你讲到落地细节,不是给你背概念。
如果你也在准备后端面试,这篇建议存下来反复看。
第一问:分库分表怎么做的?
很多人听到这道题,条件反射就是一句”水平分库,按用户 ID 取模”。
面试官点点头,然后问:“为什么是水平分库不是垂直分库?你的分库分表是分了库还是分了表?单表数据量到多少你决定分的?”
你就卡住了。
因为”分库分表”这四个字,其实包含了四种完全不同的操作。
先搞清楚:你在分什么
垂直分表:把一张宽表拆成多张窄表。比如订单表有 40 个字段,其中 20 个是核心交易字段(订单号、金额、状态),20 个是扩展字段(备注、标签、附加信息)。拆成 order_main 和 order_ext 两张表,用订单号关联。目的是减少单行数据大小,提升 Buffer Pool 命中率。
垂直分库:按业务域拆库。订单库、用户库、商品库各自独立。目的是解耦业务,不同域的数据库可以独立扩容、独立维护。这个事跟数据量关系不大,跟系统架构关系大。
水平分表:同一个库里,把一张大表拆成多张结构相同的小表。比如 order_0、order_1、order_2、order_3。目的是降低单表数据量,提升查询效率。
水平分库:把数据分散到多个数据库实例上。比如 4 个 MySQL 实例,每个实例里再分 4 张表,总共 16 张分表。目的是分摊单库的连接数、IO、CPU 压力。
面试官问”分库分表怎么做的”,他真正想听的是你能不能讲清楚:你面临的是什么瓶颈,所以你选了哪种分法,分了多少库多少表,为什么是这个数量。

什么时候该分
不是数据量一大就分。分库分表是有代价的——跨库 JOIN 没法做、分布式事务复杂、运维成本翻倍。所以在分之前,先确认你真的走到了瓶颈。
几个硬指标:
-
单表数据量超过 1000 万且持续增长,查询明显变慢,加索引也压不住。注意不是绝对的 1000 万,跟字段数量、行大小、查询模式有关,但这是一个经验阈值。
-
单库 QPS 打到 5000 以上,连接池开始紧张,主从延迟拉大。
-
单库磁盘 IO 接近饱和,iostat 看 util% 长期在 80% 以上。
-
DDL 变更影响线上,给一张几亿行的表加个字段,主库阻塞几十分钟,从库延迟更久。
如果只是单表数据量大但 QPS 不高,先试试分区表(MySQL 的 PARTITION BY),或者归档冷数据,把历史数据搬到归档表里。这俩方案比分库分表简单得多,代价小得多。
确认了必须分,再往下走。
怎么分:分片策略
分片策略解决一个问题:一条数据来了,它该落到哪个库哪个表。
常见的有四种。
第一种:取模分片。
最简单最常用。比如分了 4 个库,每个库 4 张表,总共 16 张分表。分片键是用户 ID,分表序号 = user_id % 16。
user_id = 1001 → 1001 % 16 = 9 → db_2, table_1(库序号 = 9 / 4 = 2,表序号 = 9 % 4 = 1)
user_id = 1002 → 1002 % 16 = 10 → db_2, table_2
user_id = 1003 → 1003 % 16 = 11 → db_2, table_3
优点是数据分布均匀,实现简单。缺点是扩容麻烦——你从 16 张表扩到 32 张表,几乎所有数据都要重新迁移,因为取模结果全变了。
第二种:范围分片。
按分片键的范围切。比如 user_id 0-1000万 在分表 0,1000万-2000万在分表 1,以此类推。
优点是扩容方便,新数据直接落到新分表,老数据不用动。缺点是数据分布不均匀——最新的分表可能数据量最大,热点集中。而且按范围查询虽然友好,但跨范围查询还是得扫多张表。
第三种:一致性哈希分片。
把分片节点映射到一个 0 到 2^32 的哈希环上,数据按分片键的哈希值顺时针找到最近的节点。扩容时只影响哈希环上相邻的一小段数据,迁移量小。
缺点是实现复杂,而且在节点较少时数据分布可能不均匀,需要引入虚拟节点。
第四种:路由表。
维护一张路由表,记录每条数据在哪个分片上。写入时查路由表决定落哪里,读取时也查路由表。
极其灵活,想怎么分怎么分,扩容时只改路由表不动数据。但路由表本身是个单点瓶颈,每次读写都要多一次查询,一般配合缓存用。
实际生产中最常见的是取模分片,简单可靠,配合合理的分片键设计,大部分场景够用。一致性哈希适合节点数经常变化的场景,路由表适合分片规则复杂且需要灵活调整的场景。
分多少库多少表
这是面试官特别爱追问的细节。很多人说”分了 8 个库 64 张表”,问他为什么是 8 和 64,说不出来。
分库分表的数量不是拍脑袋定的,要算。
分表数量:预估未来 3-5 年的数据总量,除以每张表期望的数据量。比如订单系统,预计 3 年后总订单量 10 亿,期望单表不超过 2000 万,那分表数量至少 50 张,往上取 2 的整数次幂,64 张。
分库数量:看 QPS 和连接数。假设峰值 QPS 5 万,单库能扛 5000 QPS,那至少 10 个库。再考虑冗余,往上取 16 个库。
总数据量:10亿
单表上限:2000万 → 分表数:10亿 / 2000万 = 50 → 取 64 张表
峰值QPS:5万
单库QPS:5000 → 分库数:5万 / 5000 = 10 → 取 16 个库
最终方案:16库 × 4表/库 = 64张分表
选 2 的整数次幂是为了取模运算高效,也能让分库和分表的路由计算更简洁——分表序号 = hash(分片键) % 64,分库序号 = 分表序号 / 4。
用什么中间件
Java 生态里主流的就两个:
ShardingSphere-JDBC:客户端模式,作为一个 JDBC 的增强层嵌入到应用里。不需要额外部署中间件,性能好,零额外网络开销。缺点是每个应用实例都要维护分片规则,多语言环境不友好。
ShardingSphere-Proxy:代理模式,独立部署一个中间件,应用连它就像连一个普通 MySQL。多语言友好,应用无感知。缺点是多了一层网络转发,性能有损耗,而且代理本身是个单点,需要做高可用。
如果技术栈纯 Java,团队规模不大,优先选 ShardingSphere-JDBC。如果有多语言需求,或者想让应用层完全不感知分片逻辑,选 Proxy。
第二问:分片键怎么选的?
这题是第一问的自然延伸。面试官已经知道你怎么分了,现在要听你选的分片键合不合理。
太多人在这题上翻车,翻车的方式高度一致——说”按用户 ID 分的”,然后讲不出为什么是用户 ID,不能用别的。
分片键的选择,直接决定了你的分库分表方案能不能用。选对了,大部分查询都能精准路由到单表;选错了,几乎所有查询都要跨库扫描,分了还不如不分。
选择原则:三个必须
第一,必须能覆盖核心查询场景。
你的系统里最高频的查询是什么?分片键必须能直接定位这些查询的数据。
比如订单系统,最核心的查询是”查某个用户的订单列表”。那分片键就应该是 user_id,查询时直接 user_id % 64 路由到对应分表,一次命中。
如果你选了 order_id 做分片键,那”查某个用户的订单”就完蛋了——你不知道这个用户的订单散落在哪些分表上,只能广播到所有 64 张表去查,这就是全表扫描的分布式版本,性能比分之前还差。
第二,必须分布均匀。
分片键的值要尽量均匀分布,不能有严重的数据倾斜。
user_id 一般没问题,因为它通常是自增的或雪花算法生成的,天然均匀。但如果你选了 create_time 的日期做分片键,那同一天的所有数据都落在同一个分表上,热点问题会比不分还严重。
第三,必须有明确的业务边界,不会频繁变更。
分片键一旦确定,数据落到了哪个分表就固定了。如果分片键的值会变,比如用户换了手机号、订单换了归属人,数据就得迁移到新分表,这个迁移成本极高且容易出问题。
所以分片键必须是业务上稳定不变的属性。
常见场景的分片键选择
| 业务场景 | 推荐分片键 | 核心查询 | 为什么 |
|---|---|---|---|
| 电商订单 | user_id | 查用户订单列表 | 用户维度查询最高频 |
| 社交 Feed | user_id | 查用户的时间线 | 每个用户的数据独立 |
| 即时通讯 | conversation_id | 查会话消息 | 会话维度查询,且会话 ID 天然分散 |
| 物流轨迹 | tracking_no | 查运单轨迹 | 运单号唯一且高频查询 |
| IoT 设备数据 | device_id | 查设备数据 | 设备维度查询,数据量大且分散 |
注意一个关键点:分片键不一定是主键,但分片键必须是大多数查询的过滤条件。
终极难题:跨片查询怎么办
这是面试官一定会追问的点。不管你分片键选得多好,总有查询不包含分片键的时候。
比如订单系统按 user_id 分片,但商家要查”我这个店铺最近的所有订单”,这个查询的过滤条件是 merchant_id,不是 user_id。同一个商家的订单散落在所有 64 张分表上,怎么办?
三种解法,各有代价。
解法一:广播查询。
把查询发到所有分片上,各自查完再合并结果。
简单粗暴,但代价巨大。64 张表全查一遍,数据库压力直接翻 64 倍。只能用于偶尔的管理后台查询,绝对不能用于 C 端高频场景。而且分页会变成噩梦——你需要在应用层做归并排序,第二页意味着每张分表都要查前两页的数据再合并去重,越往后翻越慢。
解法二:异构索引。
用另一套存储来维护非分片键到分片键的映射。比如用 Elasticsearch 建一个 merchant_id 到 order_id 的索引,查询时先从 ES 查出这个商家所有订单的 order_id 列表,再拿着 order_id 去分片表里精确查。
查询流程:
1. 商家查订单 → 先查 ES:SELECT order_id FROM es_index WHERE merchant_id = ?
2. 拿到 order_id 列表 → 路由到对应分表:SELECT * FROM order WHERE order_id IN (...)
3. 合并结果返回
这个方案是生产环境最常用的。代价是需要维护 ES 和 MySQL 之间的数据同步,多了一层存储和一致性保障的成本。
解法三:双写异构表。
换一个维度再存一份数据。比如订单按 user_id 分片存一份,再按 merchant_id 分片存一份,两份数据通过消息队列异步同步。
查询的时候,用户维度查 user_id 分片,商家维度查 merchant_id 分片,各自都能精准路由。
代价是存储成本翻倍,且要保证两份数据的最终一致性。适用于查询场景明确且数据量可控的场景。
分片键嵌入业务 ID:基因法
这是一个高级技巧,面试时说出来能加分。
问题场景:订单按 user_id 分片,但很多查询只带了 order_id,不知道 user_id,没法路由。
常规做法是建一张 order_id 到 user_id 的映射表,但映射表本身又是个瓶颈。
基因法的设计思路是:把 user_id 的分片信息”种”进 order_id 里。
具体做法是,生成 order_id 的时候,取 user_id 的低 N 位作为”基因”,嵌入到 order_id 中。
user_id = 1001234(二进制:...0001 1110 1010 0011 0010 0100 10)
取低 4 位作为基因:0010(即 2)
order_id 的结构:
[时间戳 41位] [机器ID 10位] [基因 4位] [序列号 8位]
↑
user_id 的低 4 位种在这里
最终 order_id = 7293847562102(其中低 4 位 = 2)
查询时,不管你手里拿的是 user_id 还是 order_id,都能算出分表位置:
-
有 user_id → user_id % 64 直接路由
-
有 order_id → 取 order_id 低 4 位,再 % 64 路由
因为 order_id 低 4 位就来自 user_id 的低 4 位,所以两者的分片结果一致。
基因位数取决于你的分表数量。16 张表取 4 位(2^4 = 16),64 张表取 6 位(2^6 = 64),256 张表取 8 位。
这个方案的好处是零额外存储,查询时不需要映射表。代价是 order_id 的生成逻辑变复杂了,且基因位数受限(雪花算法的 64 位里要挤进去)。
第三问:数据迁移怎么保证一致性?
这题是三连问的终章,也是淘汰率最高的一题。
前两题你答得再好,这题接不住,面试官会判定你”没真正做过分库分表”。因为只有亲手迁移过几亿行数据的人,才知道这中间有多少坑。
先理解问题:为什么迁移会不一致
你有一张老订单表,5000 万行数据,现在要把它拆到 16 张新分表里。
迁移过程中,老表还在正常提供服务,不断有新数据写入、旧数据更新。你的迁移程序把老表的数据读到新表,但读的那一刻和写的那一刻之间,可能已经有新数据写进老表了。
时间线:
T1: 迁移程序读到老表 user_id=1001 的记录,金额=100
T2: 业务系统更新老表 user_id=1001,金额=200
T3: 迁移程序把金额=100 写入新表
T4: 切流,业务系统开始读写新表
T5: 查询新表 user_id=1001,金额=100 ← 丢了 100 块
这就是迁移一致性的核心矛盾:数据在搬家的过程中,家里的东西还在变。
完整迁移方案:六步走
生产级别的数据迁移,不是写个脚本批量 INSERT 就完事的。它是一个有严格流程的工程,分六步。
第一步:全量同步
把老表的数据全量同步到新分表。这一步通常用离线任务跑,按主键范围分批 SELECT 出来,路由到对应分表后批量 INSERT。
关键细节:
-
分批处理,每批 1000-5000 条,避免大事务锁表。
-
按主键有序扫描,避免跳页和重复读取。
-
限流,控制读取速度,别把老库的 IO 打满影响线上业务。
全量同步伪代码:
long minId = 0;
long maxId = getMaxId(oldTable);
int batchSize = 2000;
while (minId <= maxId) {
// 按主键范围分批读取
List batch = oldDao.selectBatch(minId, minId + batchSize);
if (batch.isEmpty()) {
minId += batchSize;
continue;
}
// 按 user_id 路由到新分表
Map> routed = batch.stream()
.collect(Collectors.groupingBy(
o -> o.getUserId() % 64
));
// 批量写入新分表
for (Map.Entry> entry : routed.entrySet()) {
newDao.batchInsert(entry.getKey(), entry.getValue());
}
minId = batch.get(batch.size() - 1).getId() + 1;
// 限流
Thread.sleep(50);
}
全量同步跑完,新分表里有了老表的快照数据。但这只是起点,因为这个快照在生成的下一秒就过时了。
第二步:增量同步
全量同步过程中和完成后,老表还在不断写入新数据。这些增量数据怎么追上?
两种方案。
方案 A:基于 Binlog 的 CDC 同步。
用 Canal 或 Debezium 监听老库的 MySQL Binlog,解析出 INSERT/UPDATE/DELETE 事件,路由到对应新分表回放。
老库 MySQL → Canal → 解析Binlog → 路由计算 → 写入新分库
Binlog事件:
INSERT INTO old_order (id, user_id, amount) VALUES (1001, 888, 200)
→ user_id=888, 888%64=48 → 写入新分表 table_48
这是生产环境最常用的方案。优点是实时性好,延迟通常在秒级;对业务无侵入,不需要改代码。缺点是 Canal 本身需要保证高可用,且 Binlog 解析的顺序性必须严格保证。
方案 B:应用层双写。
改业务代码,在写老库的同时也写新库。双写可以同步做,也可以异步通过消息队列做。
同步双写的伪代码:
@Transactional
public void createOrder(Order order) {
// 写老库
oldDao.insert(order);
// 写新分库
int shardIndex = order.getUserId() % 64;
newDao.insert(shardIndex, order);
}
优点是可控性强,你可以精确控制写入逻辑。缺点是对业务有侵入,且双写的事务一致性是个大问题——老库写成功后新库写失败,怎么办?后面专门讲。
生产实践里通常是组合使用:先上 Binlog 增量同步追平数据,同时灰度开启双写作为兜底。 两套机制互相校验,任何一套出了问题另一套还能补。
第三步:数据校验
全量同步完了,增量也追上了,怎么确认新老数据一致?
不能跳过这一步。你以为同步完了,实际上可能丢了一批数据,或者有些字段映射错了。
校验方案:
全量对账:跑一个离线任务,按主键范围分批比对老表和新分表的每一条记录,逐字段比较。
对账伪代码:
long minId = 0;
int mismatchCount = 0;
while (minId <= maxId) {
List oldBatch = oldDao.selectBatch(minId, minId + 5000);
for (Order old : oldBatch) {
int shardIndex = old.getUserId() % 64;
Order newOrder = newDao.selectByPk(shardIndex, old.getId());
if (newOrder == null || !old.equals(newOrder)) {
mismatchCount++;
// 记录不一致的记录,后续修复
logMismatch(old, newOrder);
}
}
minId += 5000;
}
System.out.println("总不一致记录数:" + mismatchCount);
采样对账:全量对账太慢的话,可以按比例采样,比如随机抽 1% 的数据比对。不能完全替代全量对账,但能快速发现系统性问题。
实时对账:在增量同步运行期间,持续比对 Binlog 事件和新分表的实际状态。发现不一致立即告警。
对账发现的不一致记录,单独走修复流程——重新从老表读取最新数据,覆盖写入新分表。
第四步:灰度切流
数据对齐了,接下来开始把读流量从老库切到新库。不能一把切,必须灰度。
切流策略:
阶段1:0% 读新库,100% 读老库 → 影子模式,只读不切
阶段2:1% 读新库,99% 读老库 → 试探性切流
阶段3:10% 读新库 → 小范围验证
阶段4:50% 读新库 → 半量切流
阶段5:100% 读新库 → 全量切流
每一步之间观察一段时间(通常几小时到一天),看有没有报错、慢查询、数据不一致的告警。有问题立刻回退到上一阶段。
灰度的实现方式通常是网关层做流量比例控制,或者按用户 ID 取模来分流——比如 user_id % 100 < 1 的用户走新库,其余走老库。
// 切流判断逻辑
public Order queryOrder(Long userId, Long orderId) {
if (userId % 100 < grayPercent) {
// 走新库
int shardIndex = userId % 64;
return newDao.selectByPk(shardIndex, orderId);
} else {
// 走老库
return oldDao.selectByPk(orderId);
}
}
灰度期间,双写不能停。不管是 Binlog 同步还是应用层双写,必须保证老库和新库同时在写,否则一旦回退,回退期间写到新库的数据在老库里不存在,又会不一致。
第五步:写流量切换
读流量全量切到新库后,观察一段时间确认稳定,再切写流量。
写流量切换是整个迁移过程中风险最高的一步。因为一旦写切换到新库,老库就不再接收新的写入了,回退的代价会很高。
稳妥的做法是:
-
先停止双写中的”写老库”部分,只写新库。但老库的 Binlog 同步保留,让新库的变更继续同步到老库,作为反向兜底。
-
观察一段时间(通常 1-3 天),确认新库写入稳定,没有数据丢失。
-
如果一切正常,关闭 Binlog 同步,老库正式下线。如果发现问题,立刻切回双写模式。
写切换前: 写老库 + 双写新库 + Binlog同步 → 两个库都有完整数据
写切换中: 写新库 + 双写老库 + 反向Binlog → 两个库都有完整数据(反向兜底)
写切换后: 只写新库 → 老库可下线
注意这个”反向 Binlog”的细节。很多人切写流量的时候只想着”不再写老库了”,但如果新库出了问题要回退,回退期间写到新库的数据在老库里没有。反向 Binlog 就是解决这个问题的——新库的变更也同步回老库,保证任何时候回退都有数据。
第六步:老库下线
写流量切到新库且稳定运行一段时间后,老库可以下线。但别急着删数据,先离线归档,保留一段时间(通常 1-3 个月)以备万一。

双写一致性怎么保证
前面提到,应用层双写最大的问题是:老库写成功了,新库写失败了,怎么办?
这取决于你用的是同步双写还是异步双写。
同步双写的一致性方案:
同步双写意味着老库和新库的写入在同一个事务里。但这有个前提——两个库必须支持分布式事务。MySQL 的 XA 事务可以做到,但性能损耗极大,生产环境几乎没人用。
实际生产中,同步双写的一致性保障通常靠本地消息表 + 重试机制:
// 在老库的同一个事务里写入业务数据和消息记录
@Transactional("oldTx")
public void createOrder(Order order) {
oldDao.insert(order);
// 在老库的消息表中插入一条迁移消息
msgDao.insert(new MigrationMsg("ORDER_INSERT", order.getId(), JSON.toJSONString(order)));
}
// 异步任务扫描消息表,写入新库
@Scheduled(fixedDelay = 100)
public void syncToNewDb() {
List msgs = msgDao.selectPending(100);
for (MigrationMsg msg : msgs) {
try {
Order order = JSON.parseObject(msg.getData(), Order.class);
int shardIndex = order.getUserId() % 64;
newDao.insert(shardIndex, order);
msgDao.markDone(msg.getId());
} catch (Exception e) {
msgDao.incrementRetry(msg.getId());
if (msg.getRetryCount() > MAX_RETRY) {
alertService.send("迁移消息重试超限: " + msg.getId());
}
}
}
}
这个方案的本质是:把分布式问题降级为本地问题。 业务数据和迁移消息在老库的同一个本地事务里,要么都成功要么都失败。异步任务保证最终一致,失败有重试,重试超限有告警。
异步双写的一致性方案:
异步双写是通过消息队列做的。业务系统写完老库后,发一条消息到 MQ,消费者负责写入新库。
业务系统 → 写老库 → 发MQ消息 → 消费者 → 写新库
这个方案的一致性依赖 MQ 的可靠投递。关键点:
-
写老库和发 MQ 必须保证原子性。可以用本地消息表(同上),也可以用事务消息(RocketMQ 的事务消息)。
-
消费者必须幂等。因为 MQ 可能重复投递,消费者重复写入新库不能出问题。
-
消费失败要重试,重试超限要进死信队列人工处理。
迁移过程中最常见的三个坑
坑一:自增主键冲突。
老表的自增 ID 是连续的,迁移到新分表后,如果新分表也用自增 ID,不同分表之间会产生主键冲突。
解法:迁移前就把主键策略改成分布式 ID(雪花算法、号段模式等),新分表的主键由 ID 生成器统一分配,不依赖数据库自增。
坑二:Binlog 格式不对。
Canal 解析 Binlog 要求 MySQL 的 binlog_format 必须是 ROW 格式。如果你的老库用的是 STATEMENT 或 MIXED 格式,Canal 解析出来的 SQL 语句没法精准还原数据变更。
解法:迁移前先检查并修改 binlog_format = ROW,且 binlog_row_image = FULL。
坑三:迁移过程中老表 DDL。
迁移进行到一半,业务侧突然要给老表加个字段。老表结构变了,但新分表还是旧结构,增量同步的 Binlog 解析直接报错。
解法:迁移期间冻结老表的所有 DDL 操作。如果必须加字段,先在新分表上加,再在老表上加,中间保证 Binlog 同步的兼容性(新字段在老表 Binlog 里不存在时用默认值填充)。
回到面试:面试官到底在考什么
聊了这么多,回到开头那三个问题。
面试官问分库分表怎么做的,他不是在等你背出”水平分库取模分片”八个字。他想看的是你有没有想清楚瓶颈在哪、为什么选这个方案、分多少库多少表是算出来的还是拍脑袋的。
他问分片键怎么选的,不是在等你列几个候选键。他想看的是你有没有想过跨片查询怎么处理、数据倾斜怎么避免、业务 ID 和分片键的关系怎么设计。
他问数据迁移怎么保证一致性,更不是在等你一句”双写”。他想看的是你有没有经历过全量同步、增量追赶、数据校验、灰度切流这一整套流程,中间踩过哪些坑,怎么兜底的。
这三题串在一起,面试官真正在评估的是一件事:你做架构决策的时候,是想清楚了再做的,还是照着网上的教程抄了一遍。
分库分表不是一个技术点,是一套系统工程。从分片策略的选择到分片键的设计,从全量迁移到增量同步,从灰度切流到数据校验,每一步都有坑,每一步都有代价。你能说出这些代价,面试官就知道你是真做过的。
说不出来,那他只能判定你是简历上写了,实际没碰过。
这一步不能靠背,得靠做。
以上,既然看到这里了,如果觉得不错,随手点个赞、在看、转发三连吧,如果想第一时间收到推送,也可以给我个星标。
我是 Fox,持续分享后端面试硬核解析。我们,下次再见。
内容效果不满意?点此反馈