有部分排版问题,懒得改了,这是 Markdown 复制过来的。
Kafka & NATS
Kafka:高吞吐、持久化的消息系统
端口:9092
通俗来说干嘛的?把各种服务的消息丢进去的日志,然后提供接口给分析系统分析数据。(不是 Kafka 里的,是开发者写的)
Apache Kafka 是一个分布式、高吞吐、持久化日志系统,常用于日志收集、实时流处理、大数据管道。
Kafka 是什么?
- Kafka 是由 Apache 基金会开发的 Java 程序
- 运行 Kafka 需要:
- 启动 Kafka 服务器(
kafka-server-start.sh) - 依赖 ZooKeeper(新版已支持移除)
- 你的程序(用 Go、Java、Python 都可以)通过 Kafka 客户端 SDK 与 Kafka 服务交互
Kafka 是一个独立运行的服务进程
Go 程序只是客户端(通过 confluent-kafka-go 等包)
模块
| 模块 | 说明 |
|---|---|
| Producer | 负责向 Kafka 集群发送消息,支持异步、分区路由、压缩等 |
| Broker | Kafka 服务端组件,接收消息、保存日志、处理消费者请求 |
| Topic & Partition | 逻辑主题划分单位,Partition 是并发和负载均衡的核心 |
| Consumer | 订阅并消费消息,支持 Consumer Group 机制实现负载均衡 |
| Zookeeper / Kafka Controller | Kafka 的控制面组件(早期依赖 ZK,现在逐步用 Raft 替代) |
| Storage Layer (Log) | 顺序写入磁盘、段文件、索引文件、高效日志存储 |
| Replication | 分区副本机制,用于高可用,支持 ISR(in-sync replica)机制 |
特点
- 持久化强:数据写入磁盘,可保留几天或几周
- 高吞吐:适合处理大量数据(TB 级别)
- 可分区/可复制:天生支持分布式
- 消费者分组:支持一个 topic 多个消费者组并发消费
使用场景
- 日志收集(ELK)
- 实时指标分析
- 流式数据处理(如 Spark、Flink)
- 解耦微服务的数据依赖
Kafka 怎么保证消息不丢失
Kafka 消息可靠性保证主要依赖以下机制:
- 磁盘顺序写(持久化)
消息写入后立即追加到分区日志文件(log segment),并调用fsync或定期刷盘,防止进程崩溃或重启导致数据丢失。 - 多副本机制(Replication)
每个分区有多个副本(replica),分布在不同 broker 上,一个 leader 负责读写,follower 从 leader 拉取数据。 - ACK 确认机制
生产者发送消息时可设置acks参数:
acks=0:不等待确认(可能丢失)acks=1:leader 写入成功即返回(follower 可能还没同步)acks=-1/all:leader 和 ISR(in-sync replicas)中所有副本写入成功才返回,可靠性最高
- ISR(同步副本集合)
Kafka 维护一个与 leader 数据完全同步的 follower 列表,只有 ISR 中的副本才参与acks=-1的确认,保证数据一致性。
NATS:轻量、低延迟的消息系统
端口:4222
通俗来说干嘛的?微服务之间快速传递消息、实时反应。它可以把一个服务的消息转发给多个服务,例如下单服务把信息发给风控、通知、日志。
NATS 是一个轻量级、高性能、低延迟的消息系统,强调简洁与速度,使用 Go 实现。
NATS 是什么?
- NATS 是一个用 Go 写的消息服务器程序
- 核心组件是
nats-server,你需要像运行 Redis 一样运行它
nats-server -p 4222
- 客户端用各种语言 SDK 连接(如
github.com/nats-io/nats.go)
NATS 也是一个独立运行的服务进程
你写的 Go 程序用 SDK 与它通信,不嵌入服务逻辑。
特点:
- 低延迟:通信延迟非常小(微秒级)
- 轻量简洁:安装快,配置少
- 适合内网微服务通信:默认不做持久化(JetStream 扩展支持持久化)
- 支持 Pub/Sub、Request/Reply
使用场景:
- 微服务之间通信
- 实时事件广播(如游戏、IoT)
- 替代 RPC 通信
对比
| 特性 | Kafka | NATS |
|---|---|---|
| 持久化 | 强,磁盘存储,适合大数据 | 默认无(JetStream 才支持) |
| 吞吐 | 非常高 | 高,但主要强调低延迟 |
| 延迟 | 毫秒级 | 微秒级 |
| 安装复杂度 | 较复杂 | 非常轻量 |
| 编程模型 | Topic + 分区 + 消费者组 | Pub/Sub、Req/Reply、Queue |
| 使用语言 | Java 生态(客户端支持广) | 原生 Go 实现(跨语言支持) |
| 典型使用场景 | 日志处理、大数据管道 | 微服务通信、事件通知 |
Kafka 和 NATS 都是独立运行的程序(Server)
就像 MySQL 一样,它们是需要单独部署和运行的服务进程,而不是 Go 的普通库。
Redis – Remote Dictionary Server
端口:6379、6380(TLS)
Redis 是一个巨大的 Map,存储 key-value。
〇、为什么快
- 内存数据库
- 单线程避免上下文切换
- I/O 多路复用,同时监听大量连接,实现高并发处理。
- 高效的数据结构
- 避免磁盘随机 I/O。绝大部分操作在内存中完成,持久化是异步写磁盘,减少阻塞。
- 命令简单且优化到位
一、数据结构
核心类型
- String:最基础的类型,支持整数、浮点、位操作等。
- List:双向链表,支持队列、栈操作。
- Set:无序集合,元素唯一。
- Hash:键值对集合,适合表示对象。
- ZSet(Sorted Set):有序集合,基于分数排序。
- Bitmap:位图,节省空间进行布尔标记。
- HyperLogLog:用于基数估算,节省内存。
- Geo:地理空间索引。
- Stream:可持久化的消息队列结构。
二、底层实现
Set
- intset:元素全为整数,数量少时使用。
- hashtable:元素多或包含非整数时自动转换。
ZSet
- 跳表(skiplist):按分数有序,用于范围查询、排名。
- 哈希表:存储成员到分数的映射。
- ZSet 的使用场景:
- 实时排行榜(按分数排序)
- 延时队列(score 为时间戳)
- 社交平台点赞、活跃度排序
- 分布式优先级任务队列
- 电商按权重排序推荐
- 分数相同时的排序规则:
- Redis 在分数相同情况下,按成员名的字典序排序
三、跳表机制
跳表简介
跳表是一种分层链表结构,每一层都是下层的索引。它以概率方式构建索引,平均查找、插入、删除 时间复杂度为对数级别。跳表的节点指针较少,适合内存存储。
在 Redis 中,ZSet 的排序部分由跳表实现,支持按分数范围查询、按排名获取等操作。
复杂度
- 查找:O(log n)
- 插入:O(log n)
- 删除:O(log n)
- 区间范围查找:O(log n + m),m 为结果集大小
还有哪些查找数据结构复杂度为 O(log n)
- 平衡二叉搜索树(AVL 树、红黑树)
- B 树 / B+ 树(数据库索引结构)
- 二分查找(在有序数组上)
- 堆(heap)查找最大/最小值是 O(1),插入/删除是 O(log n)
- Treap(随机平衡树)
为什么不用二叉树而用跳表
- 二叉树需要复杂的旋转维持平衡,实现和维护难度大;跳表实现简单,插入删除无需全局重平衡。
- 链表结构天然支持范围扫描,跳表的多级索引能直接跳过不相关区间,非常适合范围查询(ZREVRANGEBYSCORE 等)。
- 内存利用率和局部性:跳表是分层链表结构,插入删除局部修改;二叉树在大量插入删除下可能退化(或需要频繁旋转)。
- Redis 压缩结构优化:跳表在节点数量小的时候可以用压缩列表节省空间。
最大高度
Redis 跳表默认最大高度是 32 层,由常量定义。实际构建过程中,大多数节点只参与低层索引。
建立层数算法
Redis 使用随机算法决定层数,采用几何分布(概率为 1/4)。具体算法是:
- 初始为 1 层
- 每次以概率 1/4 决定是否增加一层
- 最终不超过最大高度 32 层
这个设计保证了跳表整体平衡,同时插入开销较小。
四、持久化机制 AOF 与 RDB
AOF(Append Only File):每次写操作都追加一条命令到日志,恢复时可以按顺序重放。
RDB(Redis Database Backup):定时将内存快照保存为二进制文件。
| 项目 | RDB | AOF |
|---|---|---|
| 落盘机制 | 周期性快照 | 每次写操作记录为命令 |
| 文件格式 | 二进制压缩快照 | 文本命令日志(可读) |
| 典型内容 | 紧凑、序列化后的 key-value | 原始命令,例如:SET x 123 |
| 恢复速度 | 快,加载快照 | 慢,重放命令 |
| 数据安全 | 可能丢失几分钟 | 丢失几秒甚至 0 |
| 占用空间 | 小,可压缩 | 大,命令多,冗余 |
| 使用场景 | 快速恢复、冷备 | 实时恢复、热备 |
推荐方式
- AOF + RDB 混合模式
- 配置
appendfsync策略控制写频率
五、分布式与一致性问题
分布式 + 主从
Redis 是内存数据库,如果数据太大存不下怎么办?
分布式存储 + 哈希槽。
存不下就做多个 Redis 节点。数据通过哈希计算,落入某个哈希槽,每个分布式节点负责一部分 哈希槽。每个节点都记录了其它节点负责的哈希槽。
哈希槽在这里是一种中间件,负责逻辑上存储一个数据,然后被一个节点负责存储。当添加节点,我们只需要把部分哈希槽挪进新的节点即可。
当客户端查询 Redis,它可能访问了错误的节点,这个节点没有对应数据。但由于每个节点都存储了其它节点负责的哈希槽,所以它会返回一个 error,并告诉客户端,到哪里去找正确的数据。这叫 重定向。
如果某节点正在迁移入数据,此时客户端来查找数据,该节点并不知道这个数据是不存在还是未迁入。它通过重定向,让客户端访问数据迁出的节点,即可查到或知道数据不存在。
每个 Redis 节点可以设置多个从节点,当主节点失效,使用从节点替代。
主从的哨兵机制
Redis 的哨兵机制是 Redis 官方提供的高可用组件。它独立于 Redis 服务器进程,负责实时监控主从节点的健康状态。一旦主节点宕机,会自动完成主从切换,并通知客户端更新连接配置,从而实现自动故障转移和服务可用性保障。
哨兵的三个核心功能:
- 监控(Monitoring)
哨兵不断 ping 主从 Redis 节点,检测其是否宕机。 - 通知(Notification)
当发现某个 Redis 实例出现问题,会通知管理员或系统。 - 自动故障转移(Automatic failover)
当主节点不可用,哨兵会自动将某个从节点升级为主节点,并更新其他从节点的主节点指向。
注意事项
- 哨兵本身也是一个独立的进程,需要部署多个实例(通常奇数个)以实现多数投票机制。
- 客户端通常要支持哨兵协议,或借助中间件实现连接重定向。
一致性
- Redis 不是强一致性的,你下发通知可能会失败的;
- 修改全局内存涉及并发安全。
- 关于 Redis 不强一致性的问题:
- Redis 的 Pub/Sub 是 “至多送达”模型,如果订阅者挂了或重启、网络抖动,可能 漏掉消息。
- 所以可以采用如下改进方案:
- 使用 可靠消息队列 替代(如 Kafka、NATS)
- 或者在 Redis 里再维护一份 “修改版本号 + 数据快照”
- 每次订阅后拉取当前版本,与本地版本比对是否同步,不一致则全量拉取
Redis 的 Pub/Sub 不保证强一致
- 采用“至多送达”,可能丢失消息
- 改进方案:
- Kafka、NATS 等消息队列替代
- 增加版本号和快照机制进行补偿
替代中间件(配置中心)
- Nacos / Apollo / etcd / Consul
- 特性:
- Watch 监听变更
- 版本控制、回滚
- 多副本一致性(Raft)
六、MySQL 为主,Redis 为 cache 的正确做法:Cache Aside 模式
一般缓存的顺序:用户请求->HTTP 缓存->CDN 缓存->代理服务器缓存->进程内缓存->分布式缓存->数据库
本地缓存(Local Cache) 和 分布式缓存(Distributed Cache)。本地缓存更注重 访问速度,而分布式缓存则关注 数据一致性和扩展性。
写流程
- 客户端写数据 → 写入 MySQL
- 写成功后 → 删除对应 Redis 缓存
- 下次查询会从 MySQL 读,然后写入 Redis(懒加载)
读流程
- 查询先查 Redis(命中 → 返回)
- 未命中 → 查 MySQL → 回写 Redis
为什么写后要删 Redis?
- 避免读到旧值(写 MySQL 后 Redis 还留着旧缓存)
延迟双删策略
SET MySQL
DELETE Redis
sleep 50ms
DELETE Redis again
问题背景:并发写入场景下的数据不一致
我们来举一个具体的例子:
假设两个请求并发发生:
- 请求 A:查询数据
- 查 Redis,没有命中 → 查数据库 → 读到旧值 → 写入 Redis 缓存
- 请求 B:更新数据
- 写 MySQL 成功
- 立即删除 Redis 缓存
这时出现问题了:
- 请求 B 删除了缓存
- 但请求 A 随后写入了旧数据到 Redis
- 最终 Redis 缓存反而是“脏的”,与数据库不一致!
延迟双删就是为了解决这个时序问题。
缓存穿透
突发大流量查询,Redis 中没有缓存,MySQL 中也没有。造成大量无效查询。这是一种恶意攻击。可能造成数据库宕机。如果 key 变化不大,就把这个 key 放进 Redis,注意设置过期时间,避免大量占用 Redis。
其它方法:
- 接口验证筛掉明显不合理的查询
- 针对 IP 或者用户限流
- 布隆过滤器:用于判断:“某个元素可能在集合中”或“一定不在集合中”。
缓存击穿
大流量对小部分 key 查询,Redis 可以扛住,但如果 Redis 中的缓存突然失效(超时),大量流量就会进入数据库,造成宕机。
解决方案:
- 设置热点数据永不过期或者热点时期不过期。
- 加互斥锁,保证只有少量的请求到达数据库。
缓存雪崩
大流量对大量数据查询,由于某些原因,大量的 key 几乎同时过期。原因比如不合理的过期时间,或者 Redis 失效。这样会导致数据库压力大甚至死机。
解决方案:
- 保证 Redis 可用。使用 Redis 集群、主从。持久化,重启后自动加载数据。
- 多级缓存,使流量不会全部涌入数据库。
- 设置随机的失效时间,避免大量数据同时过期。
- 提前预热,热 key 提前加载。
- 数据库限流,限制大量流量进入数据库。
七、常见问题与优化建议
1. 避免大 key 和 big value
- 大 key 占内存高,迁移慢,删除慢,可能导致阻塞。
- big value 影响网络传输效率,压垮带宽。
- 建议 key 控制在几十字节以内,value 不超过几 KB。
2. 避免慢查询(slowlog)
- 单条命令执行时间超 1 毫秒可视为慢查询。
- 常见慢命令:KEYS、SCAN 大量遍历,SORT、ZRANGE 大范围排序。
- 使用 slowlog 命令排查问题。
3. 合理设置过期时间
- 防止缓存击穿、雪崩。
- 为所有缓存 key 设置 TTL,避免内存泄露。
- 使用随机过期时间,防止同时过期造成突发流量。
EXPIRE key $((300 + RANDOM % 60)) # 随机过期时间
4. 谨慎使用危险命令
KEYS:全库扫描,易阻塞FLUSHALL:清空库EVAL:脚本执行时间长可能阻塞
5. 连接数限制
- 默认支持的最大连接数有限(一般 10000 左右),建议使用连接池。
- 服务端设置 maxclients 限制连接总数。
6. 限制内存使用,防止 OOM
- 设置 maxmemory 和 maxmemory-policy(如 volatile-lru、allkeys-lru)
- 设置淘汰策略防止写满内存导致 Redis crash
7. 持久化配置建议
- AOF 可靠但体积大,写频繁时会影响性能。
- RDB 性能高但可能丢数据。
- 推荐 AOF + RDB 混合模式,设置合适的 appendfsync 策略。
8. 单线程阻塞问题
- Redis 单线程处理命令,阻塞命令会拖慢所有请求。
- 保证每条命令都轻量快速,必要时拆分任务逻辑。
9. 安全性建议
- Redis 默认无密码,不能暴露在公网。
- 使用 bind、requirepass、rename-command 等限制访问。
- 使用防火墙或内网隔离。
10. 事务和原子性
Redis 的事务采用 Lua 脚本来完成。
优点:
- 原子性强,天然保证“要么都成功,要么都失败”
- 脚本里可以自由组合对任意 key 和数据结构的操作(支持 string、hash、list、zset、set)
- 简单、高效、业务侧无感
适用场景:
- key 数量可控(Redis 限制 8000 key 内)
- 不依赖外部状态(Lua 脚本无法调用网络或持久化)
其实也用(MULTI / EXEC)语句来做事务,但
- 仅保证命令按顺序执行,不支持失败自动回滚
- 一旦某个命令失败,其他命令不会自动撤销
11. 扩展能力
- 使用 Redis Cluster 进行分片
- 使用 Codis、Twemproxy 做代理分布式扩展
12. Redis 热 key 如何优化?
热 key 问题指某个 key 被高频访问或更新,导致 Redis 单线程被拖慢或热点迁移时压力集中。
优化方案:
- 加本地缓存,如使用 Go 的 sync.Map 或 LRU 缓存,避免频繁请求 Redis
- 用 Nginx 或业务层加缓存 TTL 分散访问压力
- 使用分片 key(如给 key 加随机后缀),读时做合并处理
- 利用 Redis Cluster,将热 key 分散到不同 slot(但不能 hash 到同一 slot)
- 对频繁写的热 key,引入消息队列异步落入 Redis
- 使用 Lua 脚本或 pipeline 合并访问
八、大 key 删除方法
Redis 删除大 key(比如:包含百万元素的 list、set、hash、zset)时,不能直接使用 DEL key,否则可能阻塞 Redis 主线程,导致服务卡顿甚至宕机。
Redis 是单线程模型,任何慢操作都会阻塞整个服务,包括大 key 的删除。下面是应对这种情况的几种推荐做法。
为什么不能直接 DEL 大 key?
- DEL key 是同步删除
- 会立刻释放该 key 占用的内存
- 如果 key 很大(如 set 有 1 百万个元素),这个释放过程耗时可能达数百毫秒甚至几秒
- Redis 是单线程,这期间其他命令无法响应
正确做法:使用非阻塞删除机制
- 使用 Redis 4.0+ 的 UNLINK 命令(推荐)
UNLINK mybigkey
- **非阻塞删除**
- Redis 会将 key 从主线程移除,然后在后台线程中异步释放内存
- 非常适合删除大 key
- 和 DEL 命令相比,UNLINK 不会阻塞主线程
支持版本:Redis 4.0 及以上
- 分批删除 key(适用于集合类型)
如果你用的是老版本 Redis(不支持 UNLINK),或者你想更细致控制,可以手动分批删除 key 内的元素。
举例:删除一个大 hash
HSCAN myhash 0 COUNT 1000
HDEL myhash field1 field2 ... field1000
你可以通过脚本或后台服务批量扫描并删除字段,确保每次操作耗时短。
优点:控制删除粒度
缺点:逻辑复杂、过程长、删除期间 key 仍存在
- 设置过期时间(让 Redis 自己异步删除)
EXPIRE mybigkey 60
- 给大 key 设置过期时间,交由 Redis 定时删除
- Redis 的过期删除是**惰性 + 定期 + 分批策略**,不会一下子删完,也不会阻塞主线程
- 适用于可接受数据短时间“滞留”的场景
- 后台迁移到空数据库,再删除(高级操作)
- 将大 key 转移到 Redis 空闲的 DB(如 SELECT 15)然后对整个 DB 执行 FLUSHDB ASYNC(Redis 4.0+)
bash
MOVE mybigkey 15
SELECT 15
FLUSHDB ASYNC
- FLUSHDB ASYNC 会在后台清除整个数据库,不阻塞主线程
- 非常适合清理批量垃圾数据或灰度下线数据
---
## 九、分布式锁
Redis 分布式锁是基于 Redis 的原子操作实现的一种跨进程、跨服务的互斥机制,常用于分布式系统中协调资源访问、保证操作串行化。
1. **基本原理**
利用 Redis 提供的 `SET` 命令的原子性,在设置 key 的同时带上互斥标志和过期时间,达到加锁效果。
核心命令:
bash
SET lock_key lock_value NX PX 3000
含义:
- `NX`:只有当 key 不存在时才设置,确保互斥
- `PX 3000`:设置过期时间(毫秒),防止死锁
- `lock_value`:唯一标识当前持锁方(如 UUID)
释放锁:
bash
if redis.call(“GET”, KEYS[1]) == ARGV[1] then
return redis.call(“DEL”, KEYS[1])
else
return 0
end
避免误删:只有当前持锁者才能释放锁
2. **使用流程**
1. 客户端尝试加锁,使用 `SET NX PX`
2. 加锁成功:继续执行业务
3. 加锁失败:可以重试(带退避)或放弃
4. 业务执行完毕后,校验后释放锁
3. **Redlock 分布式锁协议(官方推荐)**
适用于多节点 Redis 的高可用场景,基本思想是:
- 同时向多个 Redis 实例写入锁(一般是 5 个)
- 成功获取过半实例的锁(如 3 个),视为加锁成功
- 设置统一过期时间,所有节点自动释放
- 释放锁时向所有 Redis 释放本客户端持有的锁
Redlock 优点:
- 容错性强,任意少数实例故障不影响锁可用性
- 避免主从不一致或节点抖动造成的锁误用
但也存在争议,要求系统时钟稳定、Redis 之间时延低
4. **常见应用场景**
- 秒杀场景:防止超卖,控制库存扣减操作的并发性
- 防止重复提交表单
- 分布式定时任务:控制定时任务只在一个节点执行
- 创建唯一资源(如订单号、快照 ID)
5. **实现时的注意事项**
1. 锁必须设置过期时间,防止程序异常死锁
2. 锁标识必须唯一,释放锁前必须比对持有者身份
3. 锁不可重入(默认),如需可重入需自行扩展
4. 尽量用 Lua 脚本执行解锁逻辑,避免非原子性
5. 不推荐使用 `SETNX` 和 `EXPIRE` 分开写,可能造成非原子加锁
6. **示例伪代码**(Go)
go
// 加锁
success := redisClient.Set(“lock:order”, uuid, “NX”, “PX”, 3000).Result()
// 解锁(Lua 脚本)
script := if redis.call("GET", KEYS[1]) == ARGV[1] then return redis.call("DEL", KEYS[1]) else return 0 end
redisClient.Eval(script, []string{“lock:order”}, uuid)
---
## 十、实战设计
1. 生成以小时为单位的前 100 首歌曲榜单,怎么设计?
- 每小时一个 zset,key 格式为 `rank:songs:2025080710`
- 成员为歌曲 ID,score 为播放次数
- 业务写入时使用 `ZINCRBY rank:songs:2025080710 1 song123`
- 使用 `ZREVRANGE key 0 99 WITHSCORES` 获取排行榜
2. 听歌业务和榜单业务如何通信?
- 听歌服务负责更新计数
- 榜单服务定时统计,可以使用:
- Redis Stream(推送播放事件)
- 发布订阅 PubSub(轻量级广播)
- Kafka 等异步日志(可跨服务)
3. 听歌用户量非常大,都写入一个 zset 会不会有问题?
是,会导致高并发写入冲突和大 key 问题。
解决方案:
- 按时间窗口分 key(小时或分钟)
- 按用户 ID 取模 hash 到多个 zset,汇总时用 `ZUNIONSTORE`
- 使用队列异步写入,分批更新
- 关键写操作用 Lua 脚本合并多操作
---
# MongoDB
端口号:27017
JSON-like 存储 → 和 Go 的 struct/json 转换天然兼容;无需严格表结构,开发快。
查询语言:BSON 文档查询语言--MongoDB Query Language
查找语句
mariadb
db.users.find({id:”123″})
由于 Json 不支持二进制,所以 Json + Binary = Bson。MySQL 的一行对应的就是 MongoDB 的一个 Bson 文档。一个表就是一个 `xxx.wt` 文件。
## 数据页
直接读取大的 wt 文件很慢,所以一个 wt 内部拆成多个页。读取时按页读取即可。一般每个页是 32 KB。
### 快速找到某数据的数据页
每个页有页号,每条记录有 ID。把每个 ID 的页号放在一个页里快查,这就是 B+ 树索引。
## 写时复制机制
MySQL 在更新 B+ 树索引时加锁,而 MongoDB 更新索引,把要写的复制出来写,而原数据则继续对外供读。后面再找机会合并。
## Cache 和 Journal
MongoDB 带有内存 Cache 机制,为防止 内存的 Cache 丢失,用 Journal Buffer 机制,把所有 写 操作同步到 内存的 Journal Buffer 里,再定时把这个 Journal Buffer 写入磁盘的 Journal 文件里。为什么不直接写入 DB?因为 Journal 文件顺序写入,MongoDB 分散,顺序写入性能高很多。所以这也是日志备份重放的一个重要原因。定时从内存 Cache 写入磁盘 MongoDB,就是 CheckPoint 机制,写入后可以安全删除写入前的 Journal 记录。
## WiredTiger
以上这些机制,加上对数据库增删查改的接口,就是一个存储引擎,称为 WiredTiger。
## Server 层
分为
1. 连接管理,管理外部对数据库的连接
2. 解析器,解析语句,检查语法
3. 优化器,选择索引生成执行计划
4. 执行器,执行计划
## 解耦合
存储引擎层和 Server 层通过接口函数进行解耦,只要实现了需要的接口,就可以替换。
## 扩展性
分库分表分节点。把数据分成多个节点存。要怎么找呢?加一层中间层“路由服务” `Mongos`。每个节点将自身资源信息上报给 `Config Server`,然后 Mongos 从中读取,选择适当的路由方法。
## 高可用
类似 Redis 主从和集群。为了保证节点掉了数据还能读,就做主从。当主节点挂了,就把副本节点通过选举机制选成主节点。
# MySQL
端口:3306
MySQL 是一个**多存储引擎框架**,最常用的是 **InnoDB**,其底层数据结构包括:
| 层次 | 描述 |
| -------------------- | ------------------------------------------------------------ |
| 页(Page) | InnoDB 的最小存储单位,默认 16KB,一个页存多个行记录 |
| 行(Row) | 表中一行数据,存储在页中(支持变长字段) |
| 表空间(Tablespace) | 存放多个页的集合,支持表独立文件(.ibd)或共享文件(ibdata) |
| 数据组织结构 | 使用 **B+ 树** 作为索引结构,包括主索引(聚簇索引)和二级索引(辅助索引) |
| 缓存结构 | 包括 buffer pool(缓存页)、redo log、undo log、change buffer 等 |
## InnoDB 简介
InnoDB 是 MySQL 默认且最常用的存储引擎,它具有以下特性:
| 特性 | 说明 |
| --------------------------- | ---------------------------------------- |
| 支持事务 | 使用 redo log、undo log 实现 ACID |
| 支持行级锁 | 并发性能更高 |
| 支持 MVCC | 多版本并发控制,提升读性能 |
| 聚簇索引(Clustered Index) | 主键索引即为数据本身,数据按主键排序存储 |
| 外键支持 | 可定义引用完整性约束 |
| 使用 B+ 树 作为索引结构 | 高效查找、范围查询、排序 |
## MyISAM 引擎对比
| 项目 | InnoDB | MyISAM |
| ---------- | ------------------------------- | -------------------------------- |
| 事务支持 | 支持 ACID,支持事务与回滚 | 不支持事务 |
| 锁粒度 | 行级锁,支持并发 | 表级锁,写时阻塞其他操作 |
| 外键支持 | 支持外键约束 | 不支持 |
| 数据缓存 | 缓存数据和索引 | 只缓存索引,不缓存数据 |
| 主索引结构 | 聚簇索引 | 非聚簇索引 |
| 崩溃恢复 | 支持崩溃恢复(redo、undo 日志) | 崩溃后需手动修复 |
| 适用场景 | 更新频繁、事务要求高的业务 | 查询为主、数据只读或更新少的业务 |
## 聚簇索引和非聚簇索引
- 聚簇索引:数据行存储在主键索引的叶子节点上,索引本身就是数据。例如 InnoDB 主键索引
- 非聚簇索引:索引的叶子节点存储的是指向数据行的地址或主键值,需要回表查询
总结:
- 聚簇索引读取主键快,但只能有一个
- 非聚簇索引可建多个,查询非主键字段时需要回表
## 缓存的机制
MySQL 缓存包括:
1. 查询缓存(早期版本,8.0 已移除)
- 相同 SQL 和结果可命中缓存
- 对动态数据支持差,命中率低
2. InnoDB Buffer Pool(主流缓存机制)
- 缓存数据页和索引页,减少磁盘 IO
- 默认占用内存 75%,支持冷热页淘汰策略
3. File System Cache(操作系统层)
- OS 页缓存也会提升数据访问效率
4. 内存临时表(如 GROUP BY 或 ORDER BY 中间结果)
- 超过内存限制后会转为磁盘临时表
## 事务的几种机制以及 MVCC 实现原理
InnoDB 实现事务的机制包括:
1. redo log(重做日志):保证已提交事务的持久性
2. undo log(回滚日志):支持事务回滚、MVCC 读快照
3. binlog(二进制日志):主从同步、恢复数据
4. MVCC(多版本并发控制):实现快照读,减少加锁冲突
MVCC 实现原理:
- 每行记录附带两个隐藏字段:创建版本号和删除版本号
- 读取时比较当前事务的版本号,只读取可见版本(读未提交、读提交、可重复读、串行化)
## 幻读是怎么解决的
幻读指同一事务中执行相同语句两次,第二次多了“幻影”记录(例如 insert)
- 可重复读(默认隔离级别)下,通过 MVCC 避免幻读
- 若需要防止 insert 造成的幻读,可使用 next-key lock(行锁 + 间隙锁)
总结:InnoDB 在可重复读隔离级别下,通过加锁和 MVCC 共同解决幻读
## 慢 SQL 排查与优化思路
1. 使用工具定位慢 SQL
- 开启 slow_query_log,查看日志
- 使用 `SHOW PROCESSLIST` 查看当前执行的 SQL
- 使用 `EXPLAIN` 分析执行计划
2. 优化方法
- 确认是否走了索引(extra 字段中是否有 Using index)
- 避免全表扫描、filesort、临时表
- 确保 WHERE 条件能命中合适索引
- 减少函数调用、类型转换、模糊查询
- 分库分表、缓存热点数据
3. 索引设计
- 用覆盖索引减少回表
- 联合索引设计考虑最左匹配原则
4. 结构优化
- 表太大时拆表或分区
- 热字段做单独表做冗余
## 超大表建表分表分页查询优化方法
问题:大分页如 offset 1000000,会造成极大的扫描负担
优化方案:
1. ID 设计
- 不建议用 MySQL 自增主键,分布式环境难以协调
- 推荐使用分布式 ID(如雪花算法、UUIDV4/Base62 编码等)
2. 避免 offset
- 使用“延续性分页”或“游标分页”
- 例如使用 `WHERE id > last_id LIMIT N` 替代 offset
3. 建索引
- 对分页字段建立合适索引(如时间戳、id)
- 避免冗余索引,只为高频查询字段建索引
- 联合索引遵循最左匹配原则
4. 覆盖索引
- 优先只查索引中的字段,避免回表
5. 延迟关联
- 第一步只查主键列表,第二步再根据主键查完整数据
- 分两步执行,减轻查询负担
6. 缓存热点分页
- 对前几页缓存(如第一页),避免频繁访问数据库
总结:越往后的分页效率越低,应尽量用主键位置替代 offset
## 主从
### 主从同步机制如何保证实时性与完整性
1. **MySQL 主从同步流程**
- 主库将写操作记录到 binlog(二进制日志)
- 从库 I/O 线程拉取主库 binlog,写入 relay log
- 从库 SQL 线程读取 relay log 并执行,重放数据变化
2. **如何确认同步完整且实时**
- **半同步复制(semi-sync replication)**:
- 主库写完 binlog 后,必须等待至少一个从库确认已收到,才返回客户端成功
- 提高了数据可靠性(不是强一致性)
- **GTID 模式(全局事务 ID)**:
- 可唯一标识每个事务,防止重复或丢失执行
- 从库可基于 GTID 精确知道同步到哪个事务点
- **延迟监控**:
- 查询 `Seconds_Behind_Master` 判断从库滞后
- 使用 `pt-heartbeat` 工具精确监测复制延迟
- **主从一致性校验**:
- `pt-table-checksum` 检查主从数据是否一致
- 定期做冷备 + 校验
3. **性能建议**
- 开启并行复制(多线程复制)
- 主库启用 binlog group commit
- 合理设置 innodb_flush_log_at_trx_commit,减少刷盘瓶颈
## 什么是最左匹配原则?
联合索引(如 `INDEX(col1, col2, col3)`)在使用时,只有**从最左列开始连续匹配**时,索引才会生效。
sql
— 联合索引 (a, b, c)
WHERE a = 1 — ✅ 使用索引
WHERE a = 1 AND b=2 — ✅ 使用索引
WHERE b = 2 — ❌ 不使用索引(跳过了 a)
WHERE a = 1 AND c=3 — ✅ 部分使用 (a),c 不参与索引
“`
为什么会这样?
- B+ 树索引是按从左到右依次排序的
- 索引条目按 (a, b, c) 排序,一旦跳过
a,就没法快速定位分支 - 所以必须“从左往右连续使用”
联合索引用于 ORDER BY 时,也要遵守最左匹配原则吗?
是的:排序使用联合索引,也需要最左匹配原则。
原因:
排序是否能用索引,依赖于“查询的 WHERE 子句 + ORDER BY 是否与索引方向一致”。
为什么 MySQL 不用单线程
- 并发需求
MySQL 需要同时处理多个连接和 SQL 语句,单线程会导致阻塞等待。 - 磁盘 I/O 等待
MySQL 有大量磁盘读写操作,如果是单线程,一个查询阻塞会影响所有请求。 - 事务隔离与并行执行
多线程可利用多核 CPU 提高吞吐,事务间可以并发执行,减少响应时间。 - 网络延迟影响
单线程容易被慢连接拖慢,影响整体处理速度。
索引的实现
MySQL 本身只是一个数据库框架,索引的具体实现由存储引擎决定。
- InnoDB(默认):使用 B+ 树 作为索引结构
- MyISAM:也用 B+ 树 作为索引结构,但组织方式和 InnoDB 不同
- Memory 引擎:默认用 哈希索引
B+ 树为什么适合做索引
- 有序性:范围查找快(支持最左匹配、区间查询)
- 磁盘友好:节点存多个 key,减少 IO 次数
- 链表结构:叶子节点之间有指针,方便范围遍历
与 B 树对比
| 特性 | B 树 | B+ 树 |
|---|---|---|
| 数据存储位置 | 内部节点 + 叶子节点 | 仅叶子节点 |
| 叶子节点连接 | 无连接 | 叶子节点通过链表连接 |
| 查询效率 | 中等 | 更高(尤其是范围查询) |
| 内存占用 | 较大 | 较小 |
| 插入/删除性能 | 较好 | 稍差(因移动数据到叶子节点) |
| 支持范围查询 | 不如 B+ 树 | 高效(叶子节点链表) |
为什么 B+ 树比 B 树更适合做数据库索引?
B+ 树的 所有数据都在叶子节点,并且 叶子节点通过链表连接,这使得 顺序查询和范围查询 比 B 树更高效,尤其是大规模的数据量中,能显著提高数据库性能。
B 树因为内部节点存储数据,增删时不仅涉及叶子节点,还需要修改内部节点,这使得增删操作较为复杂,性能也稍低。B+ 树因为内部节点只存索引,增删时只影响叶子节点,操作简单高效,性能较好。
B+ 树的优势是什么?
主要优势在于它能高效处理范围查询和顺序扫描,并且由于只在叶子节点存储数据,内存占用更小。
哈希索引(Memory 引擎默认)
- 基于哈希表,O(1) 查找
- 只适合等值查询(=、IN),不支持范围查找、排序
- InnoDB 的自适应哈希索引(AHI)也是类似的原理,但由引擎自动维护
字符集
MySQL 字符集
MySQL 字符集(Character Set)是一套符号和编码的规则。它告诉 MySQL 如何存储和处理文本数据。MySQL 中有几个关键的字符集设置:
- 数据库(Database)字符集:这是创建数据库时指定的默认字符集。
- 表(Table)字符集:这是创建表时指定的默认字符集。如果未指定,它会继承数据库的字符集。
- 列(Column)字符集:这是为表的每一列(例如
VARCHAR、TEXT)指定的字符集。如果未指定,它会继承表的字符集。 - 连接(Connection)字符集:这是客户端与 MySQL 服务器通信时使用的字符集。
UTF-8 和 UTF8mb4 的区别
这是 MySQL 中一个非常常见的问题。简而言之,utf8 和 utf8mb4 都是 UTF-8 编码,但它们支持的字符范围不同:
utf8(MySQL 的):这个字符集是 MySQL 在早期实现的 UTF-8 版本。它最多支持 3 个字节的 UTF-8 编码。这意味着它能存储大多数常用语言的字符,比如中文、英文等。然而,它无法存储一些需要 4 个字节的字符,比如:- 某些生僻字或特殊符号。
- Emoji 表情符号。
utf8mb4:这个字符集是 MySQL 对完整 UTF-8 编码的实现。mb4的意思是 “most bytes 4″,它最多支持 4 个字节的 UTF-8 编码。- 它能存储所有 3 个字节的字符。
- 它能存储 4 个字节的字符,因此完全支持 Emoji 表情和所有 Unicode 字符。
总结:为了避免潜在的字符集问题,强烈建议在所有新项目中默认使用 utf8mb4。它提供了更好的兼容性和未来扩展性,而性能开销几乎可以忽略不计。
表字符集和字段字符集的关系
表字符集是该表的默认字符集。当你为表指定了字符集后,任何没有单独指定字符集的新字段都会自动继承这个表的字符集。
字段字符集是针对具体字段设置的字符集。你可以为表中的某个字段指定一个不同于表字符集的字符集。
关系:字段字符集是表的字符集的一个子集,但它可以被独立地配置。表字符集提供了一个默认值,而字段字符集提供了更细粒度的控制。
字段字符集和表的字符集不一样会怎么样?
这完全是允许的,而且在某些情况下是有用的。
当你为某个字段指定了不同于表字符集的字符集时,MySQL 会只为这个字段使用你指定的字符集来存储和排序数据。
例子:假设你有一张名为 products 的表,其默认字符集是 latin1。但你有一个 description 字段需要存储中文,所以你为这个字段单独指定了 utf8mb4 字符集。
在这个例子中,name 字段只能存储 latin1 编码的字符,而 description 字段则可以存储中文和 Emoji 表情。
连接字符集和库字符集不一致会发生什么?
这是一个非常常见的问题,也是导致乱码的罪魁祸首。
连接字符集指的是客户端和服务器之间进行数据传输时使用的编码。当客户端发送 SQL 语句和数据给服务器时,它会使用连接字符集进行编码。当服务器返回查询结果给客户端时,也会使用连接字符集进行编码。
数据库字符集是数据在存储时使用的编码。
如果这两种字符集不一致,就会导致乱码。这个过程可以用一个简单的模型来理解:
- 客户端发送一条 SQL 语句,比如
INSERT INTO table (col) VALUES ('你好');。 - 客户端会用它的连接字符集(比如
latin1)将'你好'这两个字进行编码,然后发送给服务器。 - 服务器接收到数据后,它会根据客户端声明的连接字符集(
latin1)去解码。 - 然后,服务器会试图用表的字符集(比如
utf8mb4)对解码后的字符重新编码,并存储到数据库中。
如果连接字符集和数据库字符集不匹配,服务器可能无法正确地将数据从连接字符集转换到数据库字符集,从而导致存储的数据变成乱码。
如何避免?
最简单的办法是在建立连接后,立即通过以下命令设置连接字符集为你的数据库字符集(通常是 utf8mb4):SET NAMES utf8mb4;
这条命令会同时设置 character_set_client、character_set_connection 和 character_set_results 三个变量,确保客户端和服务器之间的所有通信都使用 utf8mb4 字符集,从而避免乱码问题。
MongoDB 对比 MySQL
| 对比维度 | MySQL(关系型数据库) | MongoDB(文档型数据库) |
|---|---|---|
| 数据模型 | 表(table)+ 行(row)+ 列(column) | 集合(collection)+ 文档(document) |
| 数据结构 | 固定字段,强类型 | 灵活字段,BSON 文档,可嵌套 |
| 查询语言 | SQL | JSON 风格的查询语法 |
| 事务支持 | 原生支持 ACID 事务(InnoDB 引擎) | 4.0+ 支持事务,但性能不如 MySQL |
| 索引机制 | 支持 B+ 树索引、多列索引 | 支持单字段、多字段、复合索引,查询灵活 |
| 扩展性 | 主从复制、分库分表较复杂 | 原生支持分片(Sharding)和副本集 |
| 一致性模型 | 强一致性 | 默认最终一致性,可配置强一致性 |
| 性能特点 | 结构化、高并发读写、聚合强 | 写入快、扩展性好、适合灵活查询和大数据量写入 |
| 适用场景 | 银行、电商、ERP 等结构固定、强一致业务 | CMS、日志系统、IoT、社交应用等灵活结构场景 |
| 模型适配性 | 面向关系建模,适合规范化数据 | 接近对象模型,适合非结构化、变化快的业务 |
| 部署难度 | 架构成熟但分布式扩展较重 | 天然支持分布式,易于横向扩展 |
Kubernetes(简称 K8s)
是什么
K8s 是一个容器编排平台,用来管理容器化应用的部署、扩缩容、负载均衡和更新。它的核心是通过 Pod、Service、Deployment 等资源,结合 API Server、Scheduler、kubelet 等组件,实现自动化运维和弹性伸缩。
Kubernetes(简称 K8s)是一个开源的容器编排平台,用于自动化部署、扩缩容、负载均衡以及管理容器化应用。它最初由 Google 开发,后来捐赠给 CNCF(Cloud Native Computing Foundation)维护。
- 容器编排:管理成百上千个容器的部署、更新和运行状态。
- 自动化运维:自动扩容/缩容、故障容器自动替换。
- 服务发现与负载均衡:容器的 IP 可能变化,K8s 自动提供稳定的访问入口。
- 资源调度:根据 CPU、内存等资源限制,把容器分配到合适的节点。
- 滚动更新与回滚:平滑更新应用,出问题可一键回滚。
工作流程(简化版)
- 编写配置文件(YAML/JSON),定义应用部署方式(副本数、镜像、端口等)。
- kubectl apply 提交到 API Server。
- Scheduler 选择合适的节点运行 Pod。
- kubelet 在节点上启动容器,并监控健康状态。
- Service 暴露服务,外部或集群内部可访问。
- 出现故障时,K8s 会自动重建 Pod,保证服务可用。
核心组件
| 组件 | 作用 |
|---|---|
| Pod | K8s 中最小的部署单元,封装一个或多个容器 |
| Node | 容器运行的主机(物理机或虚拟机) |
| Deployment | 定义应用的期望状态(副本数、镜像版本等),K8s 会自动维持它 |
| Service | 稳定的访问入口,屏蔽 Pod IP 变化 |
| Ingress | 提供 HTTP/HTTPS 访问规则(如域名路由) |
| ConfigMap / Secret | 配置管理和敏感信息管理 |
| Namespace | 资源隔离(如测试环境、生产环境分开) |
| kube-apiserver | 控制平面的核心,提供 API |
| etcd | 分布式键值存储,用于存储集群状态 |
| kube-scheduler | 负责调度 Pod 到合适的 Node |
| kubelet | 节点上运行的代理,负责启动容器、汇报状态 |
问题
背景
- a 服务有 10 个 Pod(IP1、IP2、…)
- b 服务有 10 个 Pod(IP21、IP22、…)
- Pod IP 是 临时的(Pod 删除重建后会变)
- a 服务要访问 b 服务中某一个 Pod
K8s 的访问原则
在 K8s 里,Pod 之间的直接 IP 调用是不推荐的,因为:
- Pod IP 会变,不稳定
- 无法做负载均衡
- 没有健康检查
所以 K8s 引入了 Service(ClusterIP / NodePort / LoadBalancer) 来作为稳定入口。
访问流程(默认情况:访问整个 b 服务,而不是指定某个 Pod)
- a 服务访问 b 服务时,用的是 Service 名字(比如
http://b-service:8080)。 - Pod 内的 kube-dns(CoreDNS) 会把
b-service解析为 ClusterIP(一个虚拟 IP)。 - ClusterIP 不是真实 Pod 的 IP,而是 kube-proxy 维护的虚拟地址。
- kube-proxy 在每个 Node 上用 iptables / IPVS 建立规则,把 ClusterIP 请求转发到 b 服务的 10 个 Pod 之一。
- 转发过程有负载均衡策略(轮询、随机、IP 哈希)。
- 最终,a 服务访问的实际上是 b 服务的某个 Pod 的 IP,但这个 IP 是由 kube-proxy 动态选择的。
如果要访问 b 服务的某个 特定 Pod
有几种方式:
- 直接用 Pod IP(需要 a 服务和 b 服务在同一网络,且 Pod 没被删除)
不推荐,因为 Pod 重建后 IP 会变。 - Headless Service(
ClusterIP: None) - DNS 不解析成一个 ClusterIP,而是直接解析成所有 Pod 的 IP 列表。
- a 服务解析到多个 IP 后,可以自己选择某个 Pod。
- Pod 直接暴露(HostPort / NodePort)
- 一般用于调试或特定场景,不常用。
- StatefulSet + 固定 Pod 名称
- 每个 Pod 都有稳定的 DNS,比如
b-0.b-service、b-1.b-service,这样可以精确访问某个 Pod。
简答版
在 K8s 中,a 服务访问 b 服务通常是通过 b 的 Service 名称解析成 ClusterIP,然后由 kube-proxy 根据负载均衡策略把流量转发到某个 Pod。如果需要访问 b 的特定 Pod,可以使用 Headless Service 或 StatefulSet 来获取固定的 Pod DNS 名称。
gRPC
gRPC 是 Google 开源的一种 高性能、通用的 RPC(Remote Procedure Call)框架,基于 HTTP/2 协议 和 Protocol Buffers(protobuf)序列化。
它的主要特点:
- 跨语言:支持 Go、Java、Python、C++、Node.js 等十几种语言。
- 高性能:HTTP/2 支持多路复用、头部压缩、二进制传输,提升效率。
- IDL(接口描述语言):用
.proto文件定义服务和消息结构,自动生成多语言客户端和服务端代码。 - 多种通信模式:
- 一元 RPC(普通请求-响应)
- 服务端流式 RPC(服务端连续推送数据)
- 客户端流式 RPC(客户端连续上传数据)
- 双向流式 RPC(两边同时流式通信)
- gRPC 和 REST 的区别是什么?
- 协议层:gRPC 基于 HTTP/2 + protobuf,REST 一般基于 HTTP/1.1 + JSON。
- 性能:gRPC 二进制更小、更快;REST 文本可读性更强但开销大。
- 接口约定:gRPC 通过
.proto强类型定义;REST 主要靠文档。 - 通信模式:REST 只能单次请求响应;gRPC 支持双向流。
- gRPC 为什么要用 HTTP/2?
- HTTP/2 支持 多路复用,避免队头阻塞。
- 支持 双向流,天然适合 gRPC 的流式调用。
- 支持 头部压缩,减少带宽开销。
- gRPC 为什么使用 protobuf 而不是 JSON?
- protobuf 是二进制序列化,体积小、解析快;
- JSON 可读性强但开销大,适合调试,不适合高性能场景。
- gRPC 的四种通信模式?
- Unary RPC:普通请求-响应
- Server streaming RPC:客户端一次请求,服务端多次响应
- Client streaming RPC:客户端多次请求,服务端一次响应
- Bidirectional streaming RPC:客户端和服务端都可以持续发送数据
- gRPC 的负载均衡是怎么做的?
- 客户端内置 轮询负载均衡;
- 结合 服务发现(如 etcd、Consul、k8s service)实现动态负载均衡。
- gRPC 的拦截器(Interceptor)是什么?
- 类似中间件,可以在 RPC 调用前后插入逻辑。
- 常见用途:日志、认证、限流、监控。
- gRPC 如何保证安全性?
- 基于 TLS(SSL)保证加密通信;
- 支持认证(如 token 校验、OAuth);
- 也可以结合 API Gateway 做额外安全控制。
- gRPC 的应用场景?
- 微服务之间的高性能通信(内部 RPC)
- 流式数据传输(日志、监控数据上报)
- 移动端与服务端之间的高效通信
Protocol Buffers(Protobuf)
Protocol Buffers 是 Google 开发的一种 语言无关、平台无关、可扩展的序列化协议,简称 Protobuf。
主要作用:
- 序列化:把结构化数据(对象)转成二进制,方便网络传输或持久化存储;
- 反序列化:从二进制还原出原始对象。
核心点:
- 通过
.proto文件定义数据结构和服务接口; - 编译
.proto文件,自动生成各种语言的类/结构体和序列化代码; - 支持前向和后向兼容(字段可新增、删除)。
- Protobuf 和 JSON、XML 的区别?
- 体积:Protobuf 是二进制,数据体积比 JSON、XML 小很多;
- 性能:Protobuf 序列化/反序列化速度比 JSON 快;
- 可读性:JSON/XML 可读性好,Protobuf 可读性差(需依赖
.proto)。 - 扩展性:Protobuf 有严格的字段编号和类型定义,支持版本兼容;JSON 没有强约束。
- Protobuf 的工作流程?
- 定义
.proto文件,描述数据结构和服务接口; - 用
protoc编译器生成对应语言的代码; - 在应用程序中使用生成的类进行序列化和反序列化。
- Protobuf 为什么要用字段编号(tag)?
- 每个字段都有一个唯一的 tag number(如
string name = 1;),二进制格式里只存 tag,不存字段名; - 这样能减少体积,同时确保版本兼容(新增字段不会影响老版本)。
- Protobuf 如何保证前向兼容和后向兼容?
- 新增字段:老版本会忽略未识别的字段,不会报错;
- 删除字段:不要复用旧的 tag number,可以标记为
reserved; - 修改字段类型:尽量保持兼容,比如
int32->int64是可以的,但int32->string会有问题。
- Protobuf 的数据类型有哪些?
- 标量类型:
int32,int64,uint32,bool,string,bytes - 复合类型:
message(嵌套结构)、enum(枚举) - 集合:
repeated表示数组/列表 - 映射:
map<key_type, value_type>
- Protobuf v2 和 v3 的区别?
- v2:字段必须加
optional或required; - v3:默认字段是
optional,取消了required(避免兼容性问题); - v3:增加了
map类型,更方便表示键值对。
- Protobuf 的使用场景?
- gRPC 通信(默认序列化方式就是 Protobuf);
- 分布式系统内部通信;
- 存储配置文件、日志数据等高性能场景;
- 移动端和服务端之间的高效通信。
- 为什么 gRPC 选择 Protobuf 而不是 JSON?
- 二进制传输,性能更高、体积更小;
.proto提供强类型约束,避免接口文档和代码不一致;- 跨语言自动生成代码,减少手写代码。
etcd
etcd 是一个 分布式键值存储系统,由 CoreOS 开发,用 Go 语言实现。
基于 Raft 共识算法 实现高可用,保证数据在集群中的一致性。
etcd 最主要的用途是:
- 服务发现:保存服务注册信息,让服务可以互相发现;
- 配置中心:存储分布式系统的配置,支持动态更新;
- 分布式协调:类似 ZooKeeper,提供锁、Leader 选举等能力;
- Kubernetes 的底层依赖:K8s 所有的资源对象(Pod、Service、ConfigMap 等)都存储在 etcd 中。
- etcd 的核心特点是什么?
- 强一致性:基于 Raft 算法,保证数据在多副本下的一致性;
- 高可用性:支持集群部署,少数节点宕机仍可用;
- 高性能:顺序写日志,支持高吞吐;
- Watch 机制:客户端可以实时订阅 key 变化;
- TTL / 租约机制:支持临时 key,用于服务注册和心跳检测。
- etcd 和 ZooKeeper 的区别?
- 实现语言:etcd 用 Go 写的,ZooKeeper 用 Java 写的;
- 协议:etcd 用 Raft,ZooKeeper 用 ZAB;
- API 友好度:etcd 提供 HTTP/gRPC API,更易用;ZooKeeper 使用 Java 客户端;
- 性能:etcd 在高并发下通常优于 ZooKeeper;
- 生态:etcd 是 Kubernetes 的核心组件,云原生领域更常用。
- etcd 是如何保证一致性的?
- 使用 Raft 算法:所有写请求先由 Leader 节点处理,然后复制到 Follower,超过半数确认后提交;
- 线性一致性读写:保证客户端看到的数据是最新的。
- etcd 的 Watch 机制是什么?
- 客户端可以 Watch 某个 key 或前缀,当 key 变化时,etcd 主动推送通知;
- 用于服务发现(监听服务上下线)、动态配置更新。
- etcd 的 Lease / TTL 有什么用?
- Lease(租约):key 可以绑定一个租约,租约过期时 key 自动删除;
用途: - 服务注册(自动过期代表服务下线);
- 分布式锁(锁超时自动释放);
- 心跳检测。
- etcd 在 Kubernetes 中的作用?
- Kubernetes 所有数据(Pod、Service、ConfigMap、Deployment 等)都存放在 etcd;
- API Server 负责与 etcd 交互,保证 Kubernetes 状态持久化;
- etcd 相当于 K8s 的数据库。
- etcd 集群节点数为什么建议是奇数?
- Raft 需要 超过半数节点 同意才能提交日志;
- 偶数节点和奇数节点在容错能力上是一样的,但奇数更节省资源。
例如: - 3 节点集群,允许 1 个宕机;
- 4 节点集群,也只允许 1 个宕机。
- etcd 的使用场景有哪些?
- Kubernetes(最典型应用);
- 微服务注册与发现(替代 Consul、ZooKeeper);
- 分布式配置中心;
- 分布式锁、Leader 选举。
- etcd 的性能优化方法?
- 合理设置集群节点数(3/5/7 个);
- 定期压缩历史版本(避免存储过大);
- 使用 SSD 磁盘,提升 IO 性能;
- 调整 gRPC KeepAlive,优化客户端连接。
