1. 介绍项目 
## 2. 讲讲缓存优化。缓存空值造成的空间浪费怎么优化？还有其他解决缓存击穿的方案吗？核心问题在于缓存和DB的同步，你再想想其他办法？ 
**空值缓存的空间优化**

核心思路是降低单条成本 + 控制总量：

- TTL 设短一点，比正常 key 短 10 倍，空值本身就是临时状态
- Value 用最小表示，比如存 `"nil"` 字符串而不是序列化空对象
- 用 **Bloom Filter** 前置拦截——查缓存前先过一遍，不存在的直接返回，从源头不写空值进来。我们之前用 Redis 的 `BF.ADD`，内存占用降了大概 40%

**缓存击穿的其他方案**

除了常见的互斥锁（singleflight），还有：

- **永不过期**（逻辑过期）：value 里存一个过期时间戳，异步 goroutine 去刷，读永远命中缓存，代价是短暂读旧数据
- **热点数据预加载**：提前识别热 key，定时刷新，不依赖自然过期触发重建

singleflight 在 Go 里用得很顺手，但要注意一个问题——如果 DB 查询本身很慢，合并的请求全部等在那，一旦超时会一起失败，需要配合 fallback 兜底。

**缓存和 DB 同步的其他思路**

这是核心问题，几个方向：

- **CDC（Change Data Capture）**：监听 binlog（用 Canal 或 Debezium），异步更新缓存，解耦写路径，适合写多读多场景
- **Write-through**：写 DB 的同时同步写缓存，强一致但写延迟增加
- **消息队列异步刷新**：DB 写完发 MQ，消费端更新缓存，可重试，但有短暂不一致窗口

实际项目我更倾向 CDC 方案，因为业务代码不用感知缓存，维护成本低——踩过的坑是 Canal 消费延迟在大事务时会抖动，需要做监控报警。

## 2. 商户信息用的Redis的什么数据结构？Key是什么形式？string类型底层怎么编码？商铺信息有哪些字段？如果商家修改了地址信息，需要整个更新缓存吗？有没有优化办法？生产环境中主从同步都会有延迟问题吗？Redis集群部署是怎么实现的？ 
**商户信息用什么数据结构**

用 **Hash**，key 形式：`merchant:{merchant_id}`，field 对应商户的各个字段。

不用 String 序列化整个对象，原因就是下面要说的——支持部分字段更新。

**String 底层编码**

三种编码：

- `int`：纯整数，直接存 long
- `embstr`：≤44字节，SDS 和 redisObject 连续内存分配，一次 malloc
- `raw`：>44字节，分开分配

**商铺信息字段**

大概：`name`、`address`、`lat/lng`、`category`、`phone`、`status`、`rating`、`open_hours` 这类。

**地址修改是否要整体更新**

用 Hash 就不需要全量更新，直接 `HSET merchant:123 address "新地址" lat "39.9" lng "116.4"` 只改三个 field，其他字段不动。

用 String 就只能整体覆盖，反序列化→修改→再序列化写回，有并发覆盖风险。

**主从同步延迟**

生产环境**一定有**，只是大小问题。常见原因：

- 从节点单线程回放 replication buffer 跟不上主节点写速度
- 网络抖动、大 key 同步阻塞

应对：读操作允许旧数据的走从库，强一致场景走主库，监控 `master_repl_offset` 和 `slave_repl_offset` 的 lag。

**Redis 集群部署**

Redis Cluster 用**哈希槽**方案，共 16384 个 slot，key 经过 CRC16 取模分配到对应节点。

- 每个 master 负责一段 slot 范围，挂对应 slave 做高可用
- 节点间用 Gossip 协议互相感知状态
- 客户端拿到 MOVED 重定向时，更新本地 slot→node 映射

踩过的坑：批量操作（`MGET`/Pipeline）跨 slot 会报错，需要用 `{}` hash tag 强制同 slot，或者在业务层拆分请求。

## 3. 删缓存后更新DB，会不会有流量异常的情况，给DB带来压力？为什么不先更新缓存？ 
**先删缓存再更新 DB 的问题**

这个顺序有个经典的并发漏洞：

```
线程A: 删缓存 → (还没更新DB)
线程B:          cache miss → 读DB旧值 → 写入缓存
线程A:                                          → 更新DB新值
结果: DB是新值，缓存是旧值，不一致
```

流量压力方面——删完缓存到 DB 更新完成这个窗口期，所有请求都打到 DB，如果这个操作耗时稍长或并发高，DB 压力会明显上升。

**为什么不先更新缓存**

- DB 写失败时，缓存已经是新值，回滚缓存不原子，脏数据难处理
- 并发写场景下，两个线程更新缓存的顺序可能和 DB 提交顺序不一致，最终缓存值是"中间态"

**正确姿势：先更新 DB，再删缓存**

这是 Cache Aside 的标准写法，即使删缓存失败，下次读也是 DB 最新值再回填，顶多短暂读旧缓存，不会持久脏数据。

删缓存失败兜底方案：

- **重试机制**：删失败写到重试队列，异步补偿
- **延迟双删**：更新 DB 后删一次，延迟几百毫秒再删一次，覆盖掉窗口期内被回填的旧缓存

生产上我一般用延迟双删 + 删失败重试队列组合，基本能覆盖大多数异常场景。

## 4. 秒杀方案如何保证避免超卖？如果Redis扣减库存后，服务重启等异常情况导致消息没发出去，会有问题吗？怎么处理？ 
**避免超卖**

核心：库存扣减必须原子操作

```lua
-- Lua 脚本保证原子性
if redis.call('get', KEYS[1]) >= ARGV[1] then
    return redis.call('decrby', KEYS[1], ARGV[1])
else
    return -1
end
```

扣减成功才进后续流程，失败直接返回"库存不足"。DB 层再加一道 `stock > 0` 的条件更新作为最终兜底。

**Redis 扣了库存但消息没发出去**

这是个典型的**两阶段不一致**问题，服务重启、OOM、网络抖动都可能触发。

几个处理思路：

**方案1：本地事务表 + 定时补偿**

- 扣库存前先写一条"待发送"记录到 DB（和业务在同一事务）
- 消息发成功后标记"已发送"
- 定时任务扫描超时未发送的记录，重新投递

**方案2：消息中间件事务消息（RocketMQ）**

- 发 half message → 执行本地逻辑 → commit/rollback
- Broker 会回查本地事务状态，天然解决这个问题
- 这是我更倾向的方案，逻辑清晰，不用自己维护补偿任务

**方案3：Redis 库存回滚**

- 检测到消息发送失败，把库存加回去
- 问题是"检测失败"本身不可靠，服务直接崩掉就没机会回滚了，不推荐作为主要手段

**实际踩坑**

用事务消息时要注意回查接口的幂等性，RocketMQ 可能多次回查，本地查询逻辑要稳定，不能有副作用。

## 5. 手撕：能说一下这个Lua脚本的具体实现吗？手写lua脚本 
## 6. 算法：二叉树的公共祖先，需要自己构造节点，每个节点有指向父节点的指针，输入两个节点，输出最近公共祖先，你之前刷到过吗？ 
刷到过类似的，标准 LCA 一般是没有父指针的，有父指针这个变种反而更简单。

**思路**

有父指针本质上变成了**两个链表找第一个公共节点**——每个节点往上走到 root 就是一条链。

方案一：HashSet，O(h) 空间

```go
func lca(p, q *Node) *Node {
    visited := map[*Node]bool{}
    for p != nil {
        visited[p] = true
        p = p.parent
    }
    for q != nil {
        if visited[q] { return q }
        q = q.parent
    }
    return nil
}
```

方案二：类似链表找交点，O(1) 空间

- 分别算两个节点的深度，深的先走差值步，再同步往上走，相遇即为答案

```go
// 伪代码
depthA, depthB := depth(p), depth(q)
// 深的先走
for depthA > depthB { p = p.parent; depthA-- }
for depthB > depthA { q = q.parent; depthB-- }
for p != q { p = p.parent; q = q.parent }
return p
```

**节点定义**

```go
type Node struct {
    Val    int
    Left   *Node
    Right  *Node
    Parent *Node
}
```

实际面试我会先写方案一，清晰好 debug，然后主动说"可以优化到 O(1) 空间"，再给方案二——这样节奏比较自然。

你们这道题是要手写构造测试用例吗？

7.反问