5.1假期结束就有组织内部友友分享他在腾讯金融科技后台方向的部分凉经,主要涉及数据库、Redis、计算机网络、性能优化等知识点,问题和知识点我都整理在下面喽
1.有用别的数据库吗?MySQL存了多少数据?如果存了很多数据会有性能问题,怎么办?
2.根据用户ID取模水平分表,但要查城市ID下面的用户,怎么查?
3.那如果还要进行一个全局的排序呢?
4.后端的网络层是怎么实现的?有做过socket网络编程的程序吗?
5.Redis集群是如何进行主从同步的?
6.如果Redis集群发生了火灾等事故, 有没有办法恢复其中的数据?
7.如果Redis的一个节点挂了, 怎么让系统继续运行?
8.如果Redis中的数据过期了, 怎么恢复数据?
9.缓存一致性,如果redis操作失败了怎么办,如果mq操作又失败了怎么办?
10.场景设计:你现在要发放一批优惠券,券库存有 1000 张,现在很多用户要来领,每人每天限领两张。
11.如果券已经发出去了,但是数据库写失败了,会发生什么,怎么避免
面经详解
有用别的数据库吗?MySQL存了多少数据?如果存了很多数据会有性能问题,怎么办?
回答思路:
其他数据库: 常见的除了 MySQL 外,还有 PostgreSQL、Oracle、SQL Server、MongoDB 等。PostgreSQL 功能强大,支持复杂查询和数据类型;MongoDB 是 NoSQL 数据库,适合存储非结构化数据。
大量数据性能问题解决办法:
索引优化:
合理创建索引可以加快查询速度,但要避免过多索引影响写入性能。
使用SHOW INDEX分析索引使用率,删除冗余索引
采用覆盖索引策略:SELECT字段尽量包含在索引中
针对时间范围查询,建立(city_id, create_time)组合索引
分区:
将大表按一定规则(如范围、列表、哈希等)分成多个小表,减少每次查询的数据量。
使用ShardingSphere中间件实现自动路由配置
双写策略保障分片扩容时数据平滑迁移
设置分片键sharding_key时需满足高离散度要求
读写分离: 使用主从复制,主库负责写操作,从库负责读操作,分担负载。
数据库集群: 如 MySQL Cluster,提高系统的可用性和处理能力。
根据用户ID取模水平分表,但要查城市ID下面的用户,怎么查?
- 先说问题根源 假设用户表按用户ID取模分成3个表(user_0、user_1、user_2),但查询条件是用城市ID来找用户。这时候数据分散在多个表里,常规查询会漏数据。
2. 初级方案:暴力遍历法 直接查所有分表再合并结果,比如:
(SELECT * FROM user_0 WHERE city_id=123)
UNION ALL
(SELECT * FROM user_1 WHERE city_id=123)
UNION ALL
(SELECT * FROM user_2 WHERE city_id=123)
但分表越多性能越差,适合分表数量少的情况
3. 进阶方案:空间换时间 建立城市ID与用户分表的映射关系:
全局索引表: 单独建表存储城市ID->用户ID列表->所在分表,查询时先查索引表获取用户所在分表,再精准查询
冗余存储: 按城市ID再建一套分表(数据冗余存储),但需同步更新两份数据
4. 基因分片法 把城市ID的哈希值融入用户ID生成规则(如用户ID后4位=城市哈希值),这样同城市用户会集中在特定分表,天然支持按城市查询。
5. 终极方案:上中间件 使用ShardingSphere或MyCat等工具,自动解析SQL并分发到所有分表,合并结果返回。对代码侵入小,但需要额外运维。
那如果还要进行一个全局的排序呢?
实时排序场景解决方案
内存归并排序法 每个分库执行本地排序:SELECT * FROM user_0 ORDER BY score DESC LIMIT 1000
服务层汇总所有分库返回的排序结果
使用堆排序算法(优先级队列)实现多路归并
优点:实现简单,适合中小数据量
缺点:当分库数超过10个时,内存消耗增长500%+
全局索引表法
-- 建立全局排序索引表
CREATE TABLE global_sort_index (
score DECIMAL(18,2),
user_id BIGINT,
shard_id INT,
PRIMARY KEY(score DESC, user_id)
)
通过binlog同步各分库数据到索引表
查询时直接走索引表:SELECT * FROM global_sort_index LIMIT 1000
适用场景:排序维度固定的高频查询
后端的网络层是怎么实现的?
一、核心功能模块
协议栈支撑
网络层基于TCP/IP协议栈实现,负责处理数据包的分段、路由选择和流量控制。在Linux系统中通过Socket API暴露接口,支持流式套接字(SOCK_STREAM)和数据报套接字(SOCK_DGRAM)45。
连接管理
典型实现包含连接池模块,通过epoll实现I/O多路复用。例如Nginx使用红黑树管理10万+连接,单个线程可处理数千并发请求3。
数据编解码
采用TLV(类型-长度-值)二进制协议,相比JSON减少30%传输体积。Protobuf/Thrift等工具自动生成编解码器,提升序列化性能3。
二、典型实现方案
原生Socket实现
// Linux内核级实现示例
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(optval));
bind(sockfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr));
listen(sockfd, 1024); // 完成三次握手队列大小
需自行处理粘包、心跳等机制,适合定制化协议开发
高级网络框架
Netty:基于事件驱动的Java框架,零拷贝技术实现10Gbps吞吐
Boost.Asio:C++跨平台异步框架,支持协程实现同步编程风格
Go net:原生支持百万级goroutine并发,通过goroutine-per-connection模型降低复杂度
Redis集群是如何进行主从同步的?
一、核心同步机制
Redis主从同步分为全量同步和增量同步两种模式,通过PSYNC协议实现:
全量同步(首次连接或无法增量同步时)
触发条件:当从节点首次连接主节点,或主从的replication ID不一致时
执行流程:
从节点发送PSYNC ? -1命令请求同步
主节点生成RDB快照(bgsave)并缓存期间的写命令
RDB文件传输完成后,主节点发送缓存命令(repl_backlog)
从节点清空旧数据,加载RDB文件后执行缓存命令
增量同步(断线重连恢复) 触发条件:主从replication ID一致,且从节点offset在复制积压缓冲区范围内
执行流程:
从节点发送PSYNC <replid> <offset>
主节点校验offset后,发送积压缓冲区内缺失的命令
从节点执行增量命令完成数据同步
二、技术实现
复制标识控制
replication ID:40位唯一字符串,标识主节点数据版本
offset偏移量:主从各自维护,记录已同步数据量(类似binlog位置) 复制积压缓冲区
环形缓冲区存储最近的写命令(默认1MB)
通过repl-backlog-size参数调整,建议设置为「网络延迟×写入流量」的2倍
异步复制特性
主节点不等待从节点ACK确认,性能高但存在秒级延迟
通过WAIT命令实现同步复制(牺牲性能保证强一致)
如果Redis集群发生了火灾等事故, 有没有办法恢复其中的数据?
一、核心恢复手段
持久化文件恢复
RDB快照恢复:从最后一次成功生成的.rdb文件恢复,通过config get dir定位备份文件路径,将其放置到新集群的dump.rdb 目录后重启服务。
AOF日志重放: 若启用AOF,将.aof文件复制到新节点,Redis启动时自动重放所有写命令,数据完整性优于RDB(最多丢失1秒数据)。
云平台灾备方案
阿里云等厂商提供自动备份能力,可通过控制台选择任意时间点的备份重建集群,支持秒级RPO(恢复点目标)和分钟级RTO(恢复时间目标)
二、容灾增强措施 跨机房双活架构
使用Redis-shake工具实现异地集群同步,配合CLUSTER FAILOVER TAKEOVER命令实现机房级故障切换。
多级备份策略
本地冷备: 每小时生成RDB快照,保留48小时历史
异地云存储: 每日全量备份上传至S3/OSS对象存储
如果Redis的一个节点挂了, 怎么让系统继续运行?
Sentinel 监控体系
哨兵节点每秒发送 PING 命令检测主节点状态
超过 down-after-milliseconds(默认30秒)无响应标记为 主观下线
半数以上哨兵确认后触发 客观下线
Raft 选举协议
从节点发起选举请求(携带 epoch 版本号)
其他主节点通过 CLUSTERMSG_TYPE_FAILOVER_AUTH_REQUEST 投票
获得多数票的从节点晋升为新主节点
数据同步保障
新主节点开启 slave-serve-stale-data=yes 允许旧数据服务
异步执行 PSYNC 命令同步最新数据
如果Redis中的数据过期了, 怎么恢复数据?
一、常规恢复方案
RDB 快照恢复
恢复条件:在数据过期前生成过 RDB 快照文件(默认保存路径 dump.rdb )
操作步骤:
# 停止 Redis 服务
redis-cli shutdown
# 用备份文件替换当前 dump.rdb
cp /backup/dump.rdb /var/lib/redis/dump.rdb
# 重启 Redis
redis-server /etc/redis/redis.conf
注意: 会丢失最后一次 RDB 快照之后的所有数据
AOF 日志重放 恢复条件:开启 AOF 持久化(appendonly yes)
核心原理:通过重放 AOF 日志中的写命令重建数据集
操作步骤:
# 修复 AOF 文件
redis-check-aof --fix appendonly.aof
# 启动时自动加载
redis-server --appendonly yes
优势:可恢复至最后一次命令执行后的状态,数据丢失量更少
二、企业级容灾方案
跨机房双活架构
部署异地 Redis 集群,通过 redis-shake 工具同步数据:
./redis-shake -type=sync -source=主库IP:6379 -target=备库IP:6380
数据恢复时切换 DNS 指向备库
混合持久化策略
配置 aof-use-rdb-preamble yes 启用 RDB+AOF 混合模式
恢复时优先加载 RDB 基础数据,再重放 AOF 增量命令
缓存一致性,如果redis操作失败了怎么办,如果mq操作又失败了怎么办?
一、Redis操作失败时的处理策略
事务机制
使用Redis事务(MULTI/EXEC)或Lua脚本保证操作的原子性,例如:
-- 扣减库存并记录用户领取的原子操作
if redis.call('DECR', 'coupon_stock') >= 0 then
redis.call('HINCRBY', 'user:'..user_id, 'coupon_count', 1)
return 1
else
return 0
end
风险点:事务中部分命令失败时无法回滚,需配合补偿机制
补偿型重试机制
记录失败操作日志,通过定时任务扫描重试(如每天凌晨2点扫描失败记录表)
采用指数退避算法重试(如首次5秒后重试,第二次10秒后,第三次20秒后)
异步数据校验
通过MySQL的Binlog订阅工具(如Canal)监听数据库变更,对比Redis数据差异并修复
每小时执行CHECKSUM TABLE校验核心表的数据完整性
二、MQ操作失败时的处理策略
生产者端保障
本地消息表方案:将消息与业务数据写入同一数据库事务,后台线程轮询发送未成功消息
/* 业务表和消息表在同一事务写入 */
INSERT INTO orders (...) VALUES (...);
INSERT INTO mq_pending_messages (topic, content) VALUES ('coupon', '...');
事务消息方案(如RocketMQ):采用两阶段提交,预发送消息->提交事务->二次确认
消费者端保障
幂等消费设计:通过唯一业务ID(如订单号+操作类型)判断是否重复处理
if(redis.setnx("lock:"+biz_id, 1)) {
// 处理业务
redis.expire("lock:"+biz_id, 300);
}
死信队列机制:配置3次重试失败后转入死信队列,触发人工干预告警
场景 | 应对方案 | 技术实现示例 |
---|---|---|
Redis写成功但MQ失败 | 异步补偿+数据核对 | Canal监听DB变化修复缓存 |
两者均失败 | 本地事务+定时任务 | Spring Retry + Quartz调度 |
网络分区场景 | 熔断降级+人工干预 | Hystrix熔断机制+钉钉告警 |
场景设计:你现在要发放一批优惠券,券库存有 1000 张,现在很多用户要来领,每人每天限领两张
一、核心痛点 库存防超卖:1000张券要精确扣减,不能发超
限领控制:每个用户每天最多领2张
高并发抢券:大量用户同时抢券,系统不能崩
二、技术方案
库存扣减用Redis原子操作
-- Lua脚本保证原子性
if redis.call('GET', 'coupon_stock') >= '1' then
if redis.call('HGET', 'user_limit:'..user_id, today) < 2 then
redis.call('DECR', 'coupon_stock')
redis.call('HINCRBY', 'user_limit:'..user_id, today, 1)
return 'SUCCESS'
end
end
return 'FAILED'
用Redis的DECR命令扣库存,哈希表存用户当天领券次数
双重校验兜底
前端点击后先查用户当天已领次数
后端操作前再查一次,防止绕过前端
异步落库 用消息队列把发放记录异步写入MySQL,防止数据库被打垮:
用户领券 -> Redis操作 -> 发MQ -> 消费者写DB
如果写DB失败,MQ会自动重试3次
三、容灾设计
库存超发补偿 每天凌晨跑对账脚本,对比Redis库存和DB发放记录,如果发现超发:
优先从未使用的券里回收
实在不行发短信补偿其他优惠
Redis挂了切降级方案
快速切换本地缓存+数据库行锁方案
提示用户"活动太火爆,稍后再试"
如果券已经发出去了,但是数据库写失败了,会发生什么,怎么避免
一、会发生什么? 用户视角
用户看到"领取成功",实际券没存到数据库 → 用户以为有券但用不了,投诉量爆炸
可能出现超发:假设库存1000,Redis扣减到800但数据库没存,后续用户继续领券,最终发券量超过库存
系统视角
Redis和数据库数据永久不一致 → 财务对账发现钱对不上,程序员背锅
二、怎么避免?
核心思路:宁可发券慢点,也不能让数据错
先写数据库再操作Redis
反直觉但有效!比如:
BEGIN;
UPDATE coupons SET stock=999 WHERE id=1; -- 先扣数据库库存
INSERT INTO user_coupons(user_id,coupon_id) VALUES(123,1); -- 记录领券
COMMIT;
数据库事务成功后,再操作Redis扣库存。即使Redis操作失败,至少数据库有完整记录,后续可补偿
消息队列兜底
用消息队列做"二次确认":
// 伪代码
if(redis扣库存成功){
发送MQ消息("用户123领券"); // 消息里带业务数据
}
// 消费者端
消费消息 → 写入数据库 → 若失败则自动重试3次
即使第一次写库失败,靠MQ重试能保证最终写入成功
对账脚本救场
每天凌晨跑个脚本:
-- 比对Redis剩余库存 vs 数据库剩余库存
SELECT * FROM coupons WHERE redis_stock != db_stock;
-- 检查用户领券记录
SELECT * FROM user_coupons
WHERE NOT EXISTS (SELECT 1 FROM redis_user_coupons WHERE user_id=xxx);
发现不一致就自动修复,相当于给系统上个保险
所以紧急用MQ重试补偿写入数据库
对多发出的券,给用户发短信道歉并赠送等额优惠
增加库存校验熔断机制:当Redis与数据库库存差异超过5%时自动停止发券