📖 本篇简介:腾讯系(社交/游戏/云/内容)面试的 MySQL 题偏"海量数据怎么存"——好友关系、消息流、聊天记录、游戏资产、日志埋点都是它的高频场景。本篇精选 20 道公开高频题型,覆盖关系表设计、Feed 流、资产并发扣减、冷热分离、写后读一致性、大字段、不停机迁移、跨机房容灾等。题目均为公开面经常见类型,解析讲通用原理,不涉及任何公司未公开信息。前置知识:第 9、15、17、19 章;本系列示例基于 MySQL 8.0。
目录
- 1 本篇考情与知识地图
- 题目 1:海量好友关注关系表怎么设计
- 题目 2:社交消息Feed时间线怎么存
- 题目 3:游戏道具资产怎么设计与并发扣减
- 题目 4:海量聊天记录存储方案
- 题目 5:大表水平拆分后历史数据怎么归档
- 题目 6:高并发下自增主键有什么瓶颈
- 题目 7:redo undo binlog在高并发下怎么调
- 题目 8:有了Redis缓存为什么还要MySQL
- 题目 9:海量日志埋点数据怎么存
- 题目 10:冷热数据如何分层存储
- 题目 11:写后立刻读主从延迟怎么解决
- 题目 12:TEXT BLOB大字段怎么存
- 题目 13:登录会话放DB还是缓存
- 题目 14:分表后数据怎么迁移不停机
- 题目 15:MySQL80比57在工程上收益在哪
- 题目 16:游戏排行榜用ZSET还是DB
- 题目 17:长连接服务和数据库连接池什么关系
- 题目 18:数据量大时count慢怎么优化
- 题目 19:软删除和唯一索引冲突怎么办
- 题目 20:跨机房异地多活数据怎么复制
- 速查速记
- 常见坑与注意事项
- 复习清单
- 练习题
1 本篇考情与知识地图
腾讯系面试 MySQL 喜欢从"社交/游戏数据模型"切入,核心是量级大、写多读多、关系复杂。先看这张分工表:
| 数据类型 | 典型场景 | 存储选择 |
|---|---|---|
| 关系数据 | 好友、关注、拉黑 | MySQL 关系表 + 缓存 |
| 时间线 Feed | 好友动态、群消息 | 推/拉模式 + 存储分离 |
| 资产数据 | 游戏道具、金币 | MySQL 事务 + 流水 |
| 海量消息 | 聊天记录 | 分表 + 冷数据归档 |
| 计数排行 | 点赞、排行 | Redis ZSET |
| 日志埋点 | 行为上报 | 不进 MySQL,走数仓 |
一句话主线:关系和资产进 MySQL,热度和计数进缓存,海量日志进数仓,别什么都塞 MySQL。
题目 1:海量好友关注关系表怎么设计
考点
- 关系表一行一条、双向索引;
- 双向关系冗余 vs 反查;
- 分页与去重。
参考答案
1. 关系表一行一条:(user_id, followee_id, created_at),主键或唯一索引 (user_id, followee_id);
2. 查"我关注谁":按 user_id 索引查;查"谁关注我":按 followee_id 建反向索引;
3. 好友(双向):两条关注记录互相命中,或单独维护好友表;
4. 分页:按 created_at 或 ID 游标翻页,避免 OFFSET 深翻;
5. 数据量大:按 user_id 分表,关系落个人分片。
-- 关注关系表,本示例基于 MySQL 8.0
CREATE TABLE t_follow (
user_id BIGINT NOT NULL, -- 关注者
followee_id BIGINT NOT NULL, -- 被关注者
created_at DATETIME DEFAULT NOW(),
PRIMARY KEY (user_id, followee_id), -- 防重复关注
KEY idx_followee(followee_id) -- 反向查粉丝
);
追问
- 问:要不要冗余一份粉丝表?答:小体量用反向索引查即可,大体量高频可冗余粉丝表,接受异步同步。
- 问:怎么判断是否互相关注?答:查两条记录是否同时存在,或加
is_friend标记位。
题目 2:社交消息Feed时间线怎么存
考点
- 推模式 / 拉模式 / 推拉结合;
- 存储分离。
参考答案
1. 推模式(写扩散):发动态时主动写进每个粉丝的收件箱,读快但大V粉丝多写放大;
2. 拉模式(读扩散):发动态只存自己发件箱,读时拉关注列表再聚合,读慢;
3. 推拉结合:普通用户推、大V拉,平衡写放大与读延迟;
4. 存储:收件箱/发件箱用缓存 + 分表,动态正文存 MySQL/对象存储;
5. 时序:按时间倒序游标分页。
追问
- 问:大V发一条要写几百万粉丝怎么办?答:大V走拉模式,粉丝读时实时聚合,或分组批量推。
- 问:已读未读怎么处理?答:不存逐条已读,存"用户上次拉取时间"即可。
题目 3:游戏道具资产怎么设计与并发扣减
考点
- 事务一致性;流水与余额。
参考答案
1. 资产表 + 流水表:资产表存余额,流水表记录每笔变动,便于对账;
2. 扣减原子化:UPDATE asset SET count=count-? WHERE uid=? AND item=? AND count>=?;
3. 同事务:扣减与写流水在一个本地事务里,要么都成功;
4. 防并发超扣:靠条件 count>=? 或乐观锁版本;
5. 异步发奖:活动发奖先写流水异步入账,削峰。
-- 资产扣减与流水同事务,本示例基于 MySQL 8.0
START TRANSACTION;
UPDATE t_asset SET item_cnt = item_cnt - 1
WHERE uid = 1001 AND item_id = 88 AND item_cnt >= 1; -- 不足则0行
INSERT INTO t_asset_log(uid, item_id, change_cnt, balance, created_at)
VALUES (1001, 88, -1, (SELECT item_cnt FROM t_asset WHERE uid=1001 AND item_id=88), NOW());
COMMIT;
追问
- 问:流水表会很大吗?答:会,按时间分表/归档;余额是最终态,流水只对账用。
- 问:发奖失败要回滚吗?答:活动类用异步补偿,保证最终入账。
题目 4:海量聊天记录存储方案
考点
- 分表键;冷热分离;归档。
参考答案
1. 按会话分表:会话 ID 哈希分表,单会话消息落固定分片;
2. 冷热分离:近期消息热存,历史消息归档;
3. 消息 ID 时序化:消息 ID 含时间戳,支持按时间拉取;
4. 读多:群消息热点可挡 Redis;
5. 合规留存:按法规保留期限,到期归档/删除。
追问
- 问:群消息和单聊表结构一样吗?答:可统一成"会话消息表",会话类型区分;群聊广播是另一回事。
- 问:历史消息怎么查?答:归档库只读查询,别让在线库扛历史扫描。
题目 5:大表水平拆分后历史数据怎么归档
考点
- 归档流程;可恢复。
参考答案
1. 按时间窗口归档:如只保留近 6 个月在线,更早的迁归档库;
2. 迁移 + 校验 + 删除:先搬、校验行数/checksum 一致再删在线数据;
3. 按分区 DROP:分区表直接 DROP 老分区,比 DELETE 快;
4. 归档库可查:提供只读入口;
5. 备份兜底:归档前再备份一次。
追问
- 问:DELETE 删历史数据行不行?答:大事务、慢、产生大量 undo/ binlog,优先 DROP 分区或分批删除。
- 问:归档删错怎么办?答:先迁移到归档库验证无误,删前备份,保留回滚窗口。
题目 6:高并发下自增主键有什么瓶颈
考点
- 自增锁;主从切换。
参考答案
1. 5.7 及以前:自增用表级自增锁,并发插入有竞争;8.0 改为轻量 mutex,大幅缓解;
2. 主从切换:新主自增起点若没处理可能冲突,靠 auto_increment_increment/offset 协调;
3. 批量插入:INSERT ... VALUES (...),(...) 一次分配一批自增值,减少竞争;
4. 分库后:自增失去全局唯一性,需分布式 ID;
5. 有序写入:自增主键顺序插入,B+ 树尾部写入,性能好。
追问
- 问:8.0 自增怎么改的?答:把表锁改成内存 mutex,且自增值持久化到 redo log,重启后不复用。
- 问:自增会用完吗?答:BIGINT 足够,INT 才要担心。
题目 7:redo undo binlog在高并发下怎么调
考点
- 三种日志职责;参数方向。
参考答案
1. redo log:保证 crash-safe,innodb_log_file_size 适中——太小导致频繁 checkpoint 刷盘,太大恢复慢;
2. undo log:保证回滚与 MVCC,长事务不提交会让 undo 膨胀、purge 跟不上;
3. binlog:主从复制依据,sync_binlog=1 最安全但略慢;
4. 关键:缩短事务、别长事务占着 undo;
5. 刷盘策略:按可靠性要求平衡 innodb_flush_log_at_trx_commit 与 sync_binlog。
追问
- 问:双 1 是什么?答:
innodb_flush_log_at_trx_commit=1+sync_binlog=1,最安全。 - 问:undo 膨胀症状?答:表空间暴涨、purge 延迟、历史读视图堆积。
题目 8:有了Redis缓存为什么还要MySQL
考点
- 缓存与存储边界。
参考答案
1. MySQL 持久化 + 事务 + 复杂查询,Redis 是内存易失、不适合做权威存储;
2. Redis 会宕机/被清,重启后必须从 MySQL 重建;
3. MySQL 支持复杂条件查询、Join、事务约束,Redis 数据结构有限;
4. 定位:Redis 是"加速层",MySQL 是"数据源";
5. 成本:全量数据放内存太贵,MySQL 承担大容量持久数据。
追问
- 问:什么数据可以只在 Redis?答:可重建的临时态(会话、计数),权威数据必须回 MySQL。
- 问:Redis 缓存预热?答:上线/大促前把热点数据刷进缓存。
题目 9:海量日志埋点数据怎么存
考点
- MySQL 不适合日志;
- 数据流选型。
参考答案
1. 别进 MySQL:埋点写 QPS 极高、分析型查询多,MySQL 扛不住也不擅长;
2. 链路:埋点 → 消息队列(Kafka)→ 流式计算/数仓(ClickHouse/Hive)→ 大屏/报表;
3. MySQL 只存:需要事务、点查、订单态的核心业务数据;
4. 实时与离线分离:实时入 ClickHouse,离线落 Hive;
5. 成本:日志保留周期短,按冷热分层降本。
追问
- 问:为什么不能直接写 MySQL?答:写吞吐、聚合分析、存储成本都不匹配。
- 问:少量关键行为要回查业务库?答:业务库只存必要状态,明细走数仓关联。
题目 10:冷热数据如何分层存储
考点
- 分层策略;查询路由。
参考答案
1. 热数据:在线库 + 缓存,低延迟点查;
2. 温数据:略便宜的存储/只读副本;
3. 冷数据:归档库/列存/对象存储,按需查询;
4. 路由:按时间自动路由到对应层,业务无感;
5. 生命周期:自动迁移、到期销毁。
追问
- 问:冷数据还要支持随机查吗?答:低频即可,慢一点可接受;高频就还得留在线。
- 问:怎么判定冷热?答:按访问时间/频率统计,自动下沉。
题目 11:写后立刻读主从延迟怎么解决
考点
- 一致性读策略。
参考答案
1. 写后读强制走主库:如刚发动态立刻看自己动态;
2. 基于写请求的会话粘滞:写后短时间内该请求/用户走主;
3. 半同步复制:主库提交等从库 ACK;
4. 缓存写后更新:读走缓存绕开从库;
5. 延迟监控切流:从库延迟大时读切回主。
追问
- 问:全走主库不就好?答:主库扛不住所有读,只对"必须强一致"的读走主。
- 问:怎么知道该走主?答:业务上标注写后读接口,路由层识别。
题目 12:TEXT BLOB大字段怎么存
考点
- 行格式与溢出页;
- 大字段拆分。
参考答案
1. 大字段单独表:主表存常用字段,TEXT/BLOB 拆扩展表,按需加载,避免拖慢主表扫描;
2. 用覆盖索引:主表小列适合索引和缓存;
3. 正文存对象存储:长文本/图片放 OSS,DB 只存 URL;
4. 避免 SELECT *:别把大字段带出来;
5. 行格式:DYNAMIC 格式大字段存溢出页,行内只留指针。
追问
- 问:TEXT 占行空间吗?答:DYNAMIC 下只存前 768 字节指针,主体溢出页,不影响行内。
- 问:大字段影响索引吗?答:不直接,但宽表回表代价大,拆表后覆盖索引更高效。
题目 13:登录会话放DB还是缓存
考点
- 会话存储选择。
参考答案
1. 会话状态放 Redis:高频读写、可过期、易失可接受;
2. DB 只存:账号、凭证、需要持久化的授权记录;
3. JWT:无状态 token,服务端不存会话;
4. 别把会话强塞 MySQL:高频读写下 MySQL 压力大,且过期清理麻烦;
5. 登录态过期:Redis TTL 天然支持。
追问
- 问:Redis 丢了会话怎么办?答:会话可重建(重新登录),不是权威数据。
- 问:踢下线怎么做?答:删 Redis key 或 token 版本号校验。
题目 14:分表后数据怎么迁移不停机
考点
- 双写 + 追平 + 校验。
参考答案
1. 新表建好多分片;
2. 双写:新写同时写新旧表;
3. 存量迁移:离线工具分批搬运老表数据;
4. 增量追平:订阅老表 binlog 回放新表;
5. 校验 + 切读切写:行数/checksum 一致后灰度切流,老表只读过渡。
追问
- 问:迁移期间数据不一致?答:以老表为准,binlog 持续追平,切流前全量校验。
- 问:分批迁移多大一批?答:每批几千到一万,避开高峰,限速。
题目 15:MySQL80比57在工程上收益在哪
考点
- 8.0 关键特性。
参考答案
1. 原子 DDL:DDL 要么全成功要么回滚,不再中途崩溃留残表;
2. 窗口函数 / CTE:复杂查询不用写一堆自连接;
3. 不可见索引、函数索引:灰度验证索引更安全;
4. 隐藏列、降序索引:排序方向更灵活;
5. 性能优化:内部临时表、JSON、优化器改进;
6. 自增持久化:重启不复用自增值。
追问
- 问:5.7 升 8.0 要注意什么?答:字符集默认 utf8mb4、sql_mode 更严、保留字变化,先在测试环境全量回归。
- 问:8.0 备份用什么?答:mysqldump/xtrabackup,注意数据字典变更。
题目 16:游戏排行榜用ZSET还是DB
考点
- 排行榜数据结构;实时性。
参考答案
1. Redis ZSET:天然支持分数排序、取 TopN、查名次,O(logN),适合实时榜;
2. DB 只存:历史快照、最终结算;
3. 定期落库:榜单结束后把结果刷 MySQL 持久化;
4. 量大:大榜分片/拆榜,别一个 ZSET 塞全服;
5. 持久化兜底:Redis 开启 RDB/AOF,宕机可恢复。
追问
- 问:实时更新分数频繁写 ZSET 扛得住吗?答:Redis 单线程内存操作很快,扛得住高频。
- 问:并列名次怎么算?答:ZSET 按 score,名次用
ZREVRANK,并列需业务层处理。
题目 17:长连接服务和数据库连接池什么关系
考点
- 连接复用;别把长连接和 DB 连接混淆。
参考答案
1. 长连接是客户端与网关之间的 TCP 连接,目的是省握手、实时推送;
2. DB 连接是网关/应用到 MySQL 的连接,数量受 max_connections 限制;
3. 二者独立:百万长连接不能变成百万 DB 连接;
4. 应用侧连接池复用:少量 DB 连接服务大量长连接请求;
5. 异步非阻塞:IO 多路复用让少量 DB 连接扛住大量业务请求。
追问
- 问:长连接越多 DB 连接要越多吗?答:不要,请求是排队复用连接池的。
- 问:DB 连接池满了?答:调大上限 + 缩短事务 + 排队超时。
题目 18:数据量大时count慢怎么优化
考点
- count 原理;估算与替代。
参考答案
1. InnoDB 的 `count(*)` 要扫数据(无 where 时),越大越慢;
2. `count(*)` 带 where 也走索引/全扫;
3. 优化:用 COUNT(*) 走最小索引、近似值用 SHOW TABLE STATUS 估算;
4. 计数表:单独维护计数表,增删时同步更新;
5. 大数:把计数放 Redis,定期回写 DB。
-- 走最小二级索引减少回表,本示例基于 MySQL 8.0
SELECT COUNT(*) FROM t_order WHERE status = 1; -- 保证(status)有索引
追问
- 问:MyISAM 的 count 为什么快?答:它内部存了行数,InnoDB 因为 MVCC 要数可见行。
- 问:业务要精确总数怎么办?答:维护计数表,别每次 count 大表。
题目 19:软删除和唯一索引冲突怎么办
考点
- 唯一约束与删除标记。
参考答案
1. 问题:用户名要求唯一,软删除后留了 is_deleted=1,再建同名用户冲突;
2. 方案一:唯一索引带上删除标记 (username, is_deleted)——但多次删除会重复;
3. 方案二:软删除时把 username 拼接删除时间戳/ID,释放原名;
4. 方案三:唯一索引列里存"删除后的 tombstone",未删除时用真名;
5. 权衡:接受"删除后名字不可复用"也可,看业务。
追问
- 问:最常用哪种?答:唯一索引 =
(username, deleted_at),未删时 deleted_at 为 NULL,删除时填时间。 - 问:NULL 在唯一索引里?答:MySQL 允许多个 NULL,所以未删时不冲突。
题目 20:跨机房异地多活数据怎么复制
考点
- 同步 vs 异步复制;冲突处理。
参考答案
1. 主从异步复制:跨机房延迟大,不能强一致;
2. 单元化:按用户单元路由,请求落在本单元,跨单元访问走中心;
3. 双向复制:多主会冲突,要靠单元化避免同一条数据两边写;
4. 容灾 RPO:异步复制有数据丢失窗口,关键数据半同步;
5. 一致性:多活最难的是冲突,原则是"同一份数据只在一个单元写"。
追问
- 问:异地双活读怎么保证一致?答:写后读在本单元,别跨单元读从库。
- 问:跨城复制延迟多少?答:几十到上百毫秒级,不能当强一致用。
速查速记
| 场景 | 推荐方案 | 备选 |
|---|---|---|
| 关注关系 | 关系表双向索引 | 冗余粉丝表 |
| Feed 流 | 推拉结合 | 全推/全拉 |
| 资产扣减 | 原子更新+流水 | 乐观锁 |
| 排行榜 | Redis ZSET | DB 排序 |
| 日志埋点 | Kafka+数仓 | 不进 MySQL |
| 大字段 | 拆扩展表/OSS | 行内存 |
| count 大表 | 计数表/估算 | 走最小索引 |
| 软删唯一 | (列, deleted_at) | 拼接释放 |
常见坑与注意事项
- 坑 1:把日志埋点写进 MySQL——写吞吐和分析场景完全不匹配,应走 Kafka + 数仓;
- 坑 2:所有大表直接 DELETE 历史数据——大事务、慢、产生大量日志,优先分区 DROP;
- 坑 3:大字段和主表混存——宽表回表代价高,长文本拆扩展表或存 OSS;
- 坑 4:count(*) 大表当常规接口——要精确总数用计数表,别实时扫;
- 坑 5:软删除 + 唯一索引不处理——删了的用户占着唯一键,新用户无法注册;
- 注意:本篇均为公开题型与通用原理,不代表任何公司真实架构。
复习清单
| 题目 | 核心结论 | 易错点 |
|---|---|---|
| 关系表 | 双向索引/冗余 | 别只建单向 |
| Feed流 | 推拉结合 | 大V写放大 |
| 资产扣减 | 原子+流水 | 别先查后扣 |
| 聊天记录 | 会话分表归档 | 在线别扛历史 |
| 冷热分层 | 自动路由下沉 | 冷数据仍要可查 |
| 写后读 | 写后读走主 | 别全走从库 |
| 大字段 | 拆表/OSS | 别 SELECT * |
| count | 计数表/估算 | 别硬扫大表 |
| 软删唯一 | (列,deleted_at) | 别留死键冲突 |
练习题
练 1:设计一个关注关系表,要求能高效查"我关注谁"和"谁关注我",并防止重复关注。
<details>
<summary>参考答案</summary>
CREATE TABLE t_follow (
user_id BIGINT, followee_id BIGINT, created_at DATETIME DEFAULT NOW(),
PRIMARY KEY (user_id, followee_id), -- 主键防重复关注
KEY idx_followee(followee_id) -- 反查粉丝
);
我关注谁按主键前缀 user_id 查;谁关注我按 idx_followee 查;主键唯一约束天然防重复。
</details>
练 2:聊天记录表每天新增千万行,在线库已千万级,给出存储与归档方案。
<details>
<summary>参考答案</summary>
按会话 ID 哈希分表打散;近期 3 个月消息热存在线库并挡 Redis;更早数据夜间批量迁到归档库/列存,按时间分区 DROP 在线老分区;归档库提供只读查询入口,删除前校验行数并备份。
</details>
下一篇预告:第 31 章 字节系MySQL真题精讲
文章回复
0 条公开回复