字节一面
时间：2026-2-5 14:00面试、2026-2-523:00通过
项目
视频feed流系统
## jwt认证具体实现方式
我理解你问的是 **JWT 认证**？

**JWT 结构**

```
Header.Payload.Signature
```

- Header：算法类型（HS256 / RS256）
- Payload：用户信息 + 过期时间
- Signature：防篡改签名

**典型流程**

```
登录 → 服务端签发 token → 客户端存储（localStorage/cookie）
→ 每次请求带上 → 服务端验签 + 解析
```

**两种签名算法选择**

- `HS256`：对称加密，简单，适合单服务
- `RS256`：非对称，私钥签发公钥验证，适合微服务/多服务共享认证

**关键实现细节**

**token 刷新**：access token 短期（15min），refresh token 长期，过期用 refresh token 换新的

**token 吊销**：JWT 无状态，这是痛点

```
方案1：黑名单存 Redis，验证时查一次（引入了状态）
方案2：短期 token + 强制过期
```

**Go 实现关键片段**

```go
claims := jwt.MapClaims{
    "uid": userID,
    "exp": time.Now().Add(15 * time.Minute).Unix(),
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
signed, _ := token.SignedString(secretKey)
```

**踩过的坑**：没校验 `alg` 字段，被攻击者把算法改成 `none` 绕过验签——验证时要强制指定期望的算法，不能信任 Header 里的 alg。

## Feed流的稳定分页是如何保证的
**问题本质**

普通分页用 `OFFSET`，Feed 流内容实时更新（插入/删除），导致翻页时数据重复或丢失。

**核心方案：游标分页（Cursor-based Pagination）**

不用 offset，用上一页最后一条数据的**唯一游标**作为下一页起点。

```sql
-- 普通分页，有问题
SELECT * FROM feed ORDER BY created_at DESC LIMIT 20 OFFSET 40

-- 游标分页，稳定
SELECT * FROM feed 
WHERE created_at < :last_cursor 
ORDER BY created_at DESC LIMIT 20
```

客户端每次请求带上 `cursor`，服务端返回下一页的 `next_cursor`。

**游标选择**

- 纯时间戳：同一秒多条数据会有问题
- **时间戳 + ID 组合**：更稳定

```go
cursor = fmt.Sprintf("%d_%d", createdAt.UnixMilli(), lastID)
```

**推拉模式下的分页**

- **拉模式**：直接查 DB/ES，游标分页够用
- **推模式**：内容写入用户收件箱（Redis List/Sorted Set）

```
ZREVRANGEBYSCORE inbox:uid +inf (lastScore LIMIT 0 20
```

用 score（时间戳）做游标，天然稳定

**踩过的坑**：游标用纯时间戳，同毫秒写入多条时翻页会漏数据，后来改成 `timestamp_id` 拼接游标解决。还有就是用户长时间不刷新再回来，游标对应的数据已被清理，要做游标失效兜底逻辑。

## 项目中RabbitMQ的作用
**我们项目里 RabbitMQ 主要用在这几个场景**

**1. 异步解耦** 下单后需要通知库存、积分、营销多个系统，不用同步调用：

```
订单服务 → MQ → 库存服务
                → 积分服务  
                → 营销服务
```

主流程只管发消息，下游各自消费，互不影响。

**2. 削峰填谷** 促销活动瞬间流量大，直接打 DB 扛不住，先写 MQ 排队消费。

**3. 延迟队列** 订单 30 分钟未支付自动取消，用死信队列实现：

```
消息 TTL 到期 → 死信交换机 → 延迟消费队列
```

RabbitMQ 插件 `rabbitmq-delayed-message-exchange` 更优雅，但我们当时用的死信方案。

**关键可靠性保障**

- **生产者**：开启 Confirm 模式，失败重试 + 本地消息表兜底
- **消费者**：手动 ACK，业务处理成功再确认
- **幂等**：消息可能重投，消费端用唯一 messageID 做去重

**和 Kafka 的选择**

RabbitMQ 适合：需要灵活路由、延迟队列、消息优先级

Kafka 适合：超高吞吐、日志收集、消息回放

我们业务消息量不算特别大，RabbitMQ 的管理界面和路由灵活性更符合需求，就没换 Kafka。

## Redis满了怎么办
**Redis 内存满了会触发淘汰策略**

先确认配置的 `maxmemory-policy`，默认是 `noeviction`——直接报错，写操作全部失败，这是最危险的。

**8种淘汰策略**

- `noeviction`：不淘汰，写入报错
- `allkeys-lru`：淘汰最近最少使用的 key
- `allkeys-lfu`：淘汰访问频率最低的 key
- `volatile-lru`：只在设了过期时间的 key 里做 LRU
- `volatile-ttl`：优先淘汰快过期的 key
- `allkeys-random`：随机淘汰

**实践选择**

- 缓存场景 → `allkeys-lru` 或 `allkeys-lfu`（LFU 更精准，Redis 4.0+）
- 混用缓存+持久数据 → `volatile-lru`，保护不带过期时间的重要 key

**治本方案**

**扩容**：最直接，加内存或升实例

**数据清理**：

```bash
# 找大key
redis-cli --bigkeys
# 扫描分析
redis-cli --memkeys
```

大 key 往往是少数，清掉效果明显

**架构优化**：

- value 压缩存储
- 冷热分离，冷数据不放 Redis
- 集群分片，横向扩展

**踩过的坑**：线上用的 `noeviction`，某次数据量暴涨直接写失败，服务大面积报错。后来统一规范，缓存集群强制用 `allkeys-lru`，持久化数据单独一个实例。

热榜是如何实现的
分布式账本
## 讲一下Raft共识算法
**Raft 解决什么问题**

分布式系统多节点如何就某个值达成一致，即使部分节点挂掉也能正常工作。

**三种角色**

- **Leader**：唯一处理写请求，负责日志复制
- **Follower**：被动接收，响应 Leader
- **Candidate**：选举中间态

**核心流程**

**Leader 选举**

```
Follower 超时未收到心跳 → 变为 Candidate → 发起投票
获得多数票（n/2+1）→ 成为 Leader
```

每个任期（term）只能有一个 Leader，term 单调递增。

**日志复制**

```
客户端写请求 → Leader 写本地日志 
→ 并行发给所有 Follower 
→ 多数派确认 → Leader commit 
→ 返回客户端成功
```

**脑裂处理**：网络分区时少数派 Leader 无法获得多数确认，写入不会成功，保证一致性。

**关键细节**

- 选举有随机超时（150~300ms），避免同时发起选举
- 只有日志最新的节点才能当选 Leader
- 已 commit 的日志不会丢失

**和 Paxos 对比**

Raft 是工程化的 Paxos，可理解性强得多，etcd、TiKV 都用 Raft。Paxos 理论上更灵活但实现复杂。

这块我主要是理论理解 + 读过 etcd 部分源码，没有自己实现过。

项目是如何防止篡改的
ECDSA加密
讲一下每笔交易如何签名和验签的流程

八股
##  简单介绍一下Go
**为什么选 Go**

简洁、并发模型好、编译快、部署简单——后端服务很合适，这也是我主力语言。

**核心特性**

**goroutine + channel**

```go
go func() { ... }()  // 轻量级协程，初始栈 2KB 可动态扩缩
```

M:N 调度模型，GMP 三层结构：

- G：goroutine
- M：系统线程
- P：逻辑处理器，持有本地队列

**GC** 三色标记 + 混合写屏障，STW 时间控制在毫秒以内。 GC 调优主要靠 `GOGC` 和 `GOMEMLIMIT`，高并发场景下 GC 压力是个真实问题。

**接口是隐式实现**

```go
// 不需要显式声明 implements
type Writer interface { Write([]byte) (int, error) }
```

duck typing，组合优于继承。

**没有的东西也值得说**

- 没有泛型（1.18之前）——老代码里大量 `interface{}` + 类型断言，很丑
- 没有异常，只有 error + panic，强制处理错误是好习惯也是啰嗦的地方
- 没有继承，靠组合和接口

**实际使用感受**

并发写起来确实顺手，但 goroutine 泄漏是最常见的坑——channel 没人消费、context 没传导致 goroutine 永远阻塞。我们代码里会定期用 `runtime.NumGoroutine()` 监控协程数。

1.18 泛型出来后用得还不多，观望中。

## 简单介绍一下GMP模型
**GMP 是什么**

Go 运行时的调度模型，解决如何用少量系统线程高效调度大量 goroutine。

**三个角色**

- **G（Goroutine）**：用户态协程，初始栈 2KB，可动态扩缩
- **M（Machine）**：系统线程，真正执行代码，数量有上限（默认10000）
- **P（Processor）**：逻辑处理器，持有本地 G 队列，M 必须绑定 P 才能执行 G

**核心结构**

```
P0: [G1,G2,G3...] ← M0 绑定执行
P1: [G4,G5,G6...] ← M1 绑定执行
全局队列: [G7,G8...]
```

P 的数量默认等于 `GOMAXPROCS`，一般设为 CPU 核数。

**调度关键机制**

**work stealing**：P 本地队列空了，随机从其他 P 偷一半 G 来执行，保证 CPU 利用率。

**handoff**：M 执行 G 发生系统调用阻塞时，P 会脱离该 M，绑定新的 M 继续执行其他 G，阻塞的 M 恢复后重新竞争 P。

**抢占调度**：Go 1.14+ 引入信号抢占，解决了之前纯协作式调度下死循环 goroutine 占用 P 不释放的问题。

**为什么不直接用线程**

线程栈默认 1~8MB，切换有内核态开销。G 初始 2KB，切换在用户态，百万 goroutine 是可行的。

踩过的坑：`GOMAXPROCS` 在容器里默认读宿主机核数，导致 P 过多线程竞争激烈，要用 `automaxprocs` 库自动适配 cgroup 限制。

## 协程和线程
**本质区别：调度者不同**

- 线程：OS 内核调度，抢占式
- 协程：用户态调度，协作式（Go 1.14+ 加了信号抢占）

**几个维度对比**

**内存开销**

- 线程：栈固定 1~8MB
- goroutine：初始 2KB，按需扩缩，百万级可行

**切换开销**

- 线程：用户态→内核态切换，保存寄存器、TLB刷新，开销大
- 协程：用户态切换，只保存少量寄存器，快很多

**创建销毁**

- 线程：系统调用，慢
- goroutine：runtime 管理，有对象池复用，极快

**阻塞行为**

- 线程阻塞：该线程挂起，OS 调度其他线程
- goroutine 阻塞：
    - 网络 IO → netpoller 接管，G 挂起，M 继续跑其他 G
    - 系统调用阻塞 → M 和 P 解绑，P 找新 M（handoff 机制）

**Go 协程的代价**

不是完全免费：

- goroutine 泄漏比线程泄漏更隐蔽
- 栈动态扩缩有 copy 开销，热点函数频繁分配大对象会触发
- 调度本身也有开销，goroutine 数量过多反而适得其反

**实际经验**：goroutine 适合 IO 密集型，CPU 密集型该用线程池控制并发度，无脑开 goroutine 反而会因调度开销下降。

Human i/o多路复用

os
进程、协程、线程
死锁
## 分页中断
应该是**缺页中断（Page Fault）**，上面虚拟内存里提到过，展开说一下。

**触发时机**

访问虚拟地址时，MMU 发现对应物理页不存在或无权限，触发缺页异常，陷入内核。

**三种情况**

**1. 页不在内存（最常见）**

```
进程访问地址 → 页表项无效 → 缺页中断
→ OS 从磁盘/swap 加载页 → 更新页表 → 重新执行
```

**2. 写时复制（COW）**

```
fork() 后父子进程共享物理页，标记只读
子进程写入 → 触发缺页 → OS 复制一份新页 → 各自修改
```

这是 fork 高效的核心原因，不会立刻复制全部内存。

**3. 权限违规** 访问没有映射的地址或越权访问 → `SIGSEGV`，进程直接挂掉。

**性能影响**

- 缺页一次涉及磁盘 IO，延迟从纳秒级跳到毫秒级
- 内存不足频繁换页（thrashing）是性能杀手

**Go runtime 的利用**

Go 申请内存用 `mmap`，只分配虚拟地址，首次访问才触发缺页真正分配物理页——这叫**按需分配**，也是 VSZ 远大于 RSS 的原因。

实际排查过一次服务启动慢的问题，根因是大量内存首次访问集中触发缺页，预热后恢复正常。

## 虚拟内存
**为什么需要虚拟内存**

直接用物理内存有几个问题：进程间内存隔离难、内存不够用、地址空间碎片化。虚拟内存是 OS 在进程和物理内存之间加的一层抽象。

**核心机制**

每个进程有独立的虚拟地址空间，通过**页表**映射到物理内存：

```
虚拟地址 → MMU + 页表 → 物理地址
```

页表存在内核，MMU 硬件做地址翻译，TLB 缓存热点映射加速。

**解决了什么问题**

- **隔离**：进程只能访问自己的虚拟空间，越界直接 segfault
- **超量使用**：虚拟空间可以大于物理内存，冷数据换出到磁盘（swap）
- **共享**：多进程可以映射同一块物理内存（共享库、mmap）

**缺页中断**

访问的虚拟页不在物理内存中：

```
触发缺页中断 → OS 从磁盘加载 → 更新页表 → 重新执行指令
```

频繁缺页（thrashing）会导致性能急剧下降。

**和 Go 的关联**

goroutine 栈动态扩缩就依赖虚拟内存，`runtime.mmap` 直接向 OS 申请虚拟地址空间，不代表真的用了那么多物理内存——这也是为什么 Go 进程 VSZ 看起来很大但 RSS 不高。

踩过的坑：容器里看到进程 VSZ 几个 GB 报警，实际 RSS 只有几百 MB，误报了，理解虚拟内存后才没再被这个坑到。

CN
## 浏览器输入URL后会发生什么
**完整流程**

**1. DNS 解析**

```
浏览器缓存 → 系统hosts → 本地DNS服务器 → 递归查询根域/权威DNS
```

拿到目标 IP

**2. TCP 三次握手**

- 建立连接，HTTPS 还要加 TLS 握手（1~2个RTT）
- HTTP/2 + TLS1.3 可以优化到 1RTT

**3. 发送 HTTP 请求**

```
GET / HTTP/1.1
Host: example.com
```

**4. 服务端处理**

- 负载均衡 → 业务服务器 → 可能查 DB/缓存
- 返回响应

**5. 浏览器渲染**

```
解析 HTML → 构建 DOM树
解析 CSS → 构建 CSSOM
合并 → Render Tree → Layout → Paint
```

遇到 JS 会阻塞解析（除非 async/defer）

**几个值得深挖的点**

- **DNS 预解析**：`<link rel="dns-prefetch">` 提前解析
- **连接复用**：HTTP/1.1 Keep-Alive，HTTP/2 多路复用
- **CDN**：DNS 解析时就近返回 CDN 节点 IP
- **缓存**：强缓存（Cache-Control）命中直接不发请求

后端视角更关注的是：请求到达后经过几层代理、LB 策略是什么、服务端如何处理高并发连接。TLS 握手优化也是性能调优的常见点。

HTTPS
TCP三次握手和四次挥手
TCP拥塞控制
DNS
HTTP1/HTTP2/HTTP3区别

MySQL
B+树的优势
事务的四个隔离级别
运行Update会发生什么
MVCC的实现及如何回滚

Redis
内存过期删除方式
数据持久化AOF和RDB
Redis和数据库如何保证一致性

算法
61.旋转链表-力扣（LeetCode）