## 1.介绍rabbit MQ在多级缓存架构中的使用 
在多级缓存（本地缓存 + Redis + DB）架构里，RabbitMQ 主要解决**缓存一致性**和**异步更新**两个问题。

### 核心用法

**1. 缓存失效广播**

数据变更时，通过 RabbitMQ fanout exchange 广播失效消息，各节点收到后删除本地缓存（Caffeine/Guava）。比直接用 Redis pub/sub 的好处是有消息持久化，节点重启后能消费积压消息补偿。

**2. 异步 DB → 缓存同步**

写操作先更新 DB，发一条消息到队列，消费者异步刷新 Redis。这样写链路不阻塞，但要接受短暂的不一致窗口。

### 踩过的坑

- **重复消费导致缓存写脏**：消费者要做幂等，通常带上数据版本号，只更新版本更新的数据
- **消息顺序问题**：同一个 key 的多次变更如果乱序消费，可能写入旧值。解法是按 key hash 到同一个 queue，或消费时比较时间戳/版本
- **本地缓存更新延迟**：fanout 消息到达有延迟，这段时间内各节点本地缓存不一致是正常的，业务上要能接受

### 我的判断

RabbitMQ 在这个场景里适合**写频率不极高**的业务。如果是高频写场景（比如计数器），引入 MQ 反而加重链路复杂度，直接 TTL 过期 + 旁路缓存更简单。

你们这边缓存一致性要求是强一致还是最终一致？

## 2.多级缓存先删redis还是本地缓存 
**先删本地缓存，再删 Redis。**

### 原因

多级缓存的读取顺序是：本地缓存 → Redis → DB

如果先删 Redis：

- 其他节点的本地缓存还存着旧数据
- 并发请求打过来，直接从本地缓存返回旧值，Redis 已经没了也不会回源
- 本地缓存的脏数据会一直存到 TTL 过期

先删本地缓存的好处：

- 本地缓存 miss 后会去查 Redis，Redis 此时还有数据，不会击穿到 DB
- 再删 Redis 后，下次请求会回源 DB 重建缓存，数据是最新的

### 但这还不够

单机删自己的本地缓存没问题，**其他节点的本地缓存怎么办**？

这就是上一个问题里 RabbitMQ fanout 的价值——广播失效消息，让所有节点同步删本地缓存。

顺序就变成：

1. 删当前节点本地缓存
2. 发 MQ 消息 → 其他节点消费后删各自本地缓存
3. 删 Redis（或者更新 Redis）

### 一个细节

步骤 2 和 3 之间有个时间窗口，其他节点本地缓存还没删完，Redis 已经删了，这时候并发读会击穿到 DB。**高并发场景要加分布式锁或者 singleflight 防击穿。**

## 3.热门票库存缓存一致性如何保证？分布式架构如何完成票务扣减？ 
这是个经典的热点库存问题，核心矛盾是**高并发读写 + 不能超卖 + 性能不能崩**。

### 库存缓存一致性

直接用 Redis 作为库存的**主存储**，DB 异步对账，不走传统的"缓存 + DB 双写"模式。

原因：热门票瞬间 QPS 可能十万级，DB 完全扛不住，Redis 原子操作才是核心防线。

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

Lua 脚本保证**check-and-decrement 原子性**，避免 ABA 问题。

### 分布式扣减流程

1. **预扣减**：Redis Lua 脚本扣库存，成功则生成订单流水写 MQ
2. **异步落库**：消费 MQ，写 DB 订单记录 + 更新 DB 库存
3. **超时回滚**：订单 15 分钟未支付，MQ 延迟消息触发回滚，Redis 库存 +1

### 踩过的坑

- **Redis 单 key 热点**：单个 key 的 QPS 上限约 10w，超过会成瓶颈。解法是**库存分片**，把 1000 张票拆成 10 个 key 各存 100，随机路由，最后汇总
- **Redis 宕机库存丢失**：开启 AOF + 主从，且 DB 是最终兜底，定期对账补偿
- **MQ 消费失败导致库存不一致**：消费端做幂等（订单号去重），失败重试，最终靠对账任务兜底

### 我的判断

强一致在这个场景基本做不到，**目标是最终一致 + 不超卖**。允许极短暂的库存数字不准，但绝不能多卖。对账任务是最后的安全网，不能省。

## 4.为什么使用rabbit MQ，你的技术考量是什么？ 
坦白说，**RabbitMQ 不一定是最优选**，我来说说我的判断依据。

### 选 RabbitMQ 的理由

- **消息可靠性**：支持持久化、confirm 机制、死信队列，适合"不能丢消息"的场景，比如库存回滚、订单状态流转
- **灵活的路由**：fanout/topic/direct exchange 原生支持，缓存失效广播用 fanout 很自然
- **成熟的生态**：运维工具完善，团队上手成本低

### 但它的缺点我很清楚

- **吞吐量天花板低**：单机约 10w msg/s，Kafka 能到百万级
- **不适合消息堆积**：设计上不是为海量堆积优化的，堆积多了性能会下降
- **不支持消息回溯**：消费完就没了，Kafka 可以按 offset 重放

### 票务场景我会这样选

|场景|我的选择|原因|
|---|---|---|
|库存扣减异步落库|RabbitMQ|消息量不大，可靠性优先|
|缓存失效广播|Redis pub/sub 或 RocketMQ|允许少量丢失，延迟更低|
|订单日志/埋点|Kafka|海量、需要回溯|

### 我的判断

选 MQ 核心看三点：**消息可靠性要求、吞吐量量级、是否需要回溯**。

票务核心链路我更倾向 **RocketMQ**——国内互联网场景更常见，延迟消息原生支持（订单超时回滚直接用），吞吐比 RabbitMQ 高，运维也比 Kafka 简单。

你们现在用的是哪个？

## 5.令牌前置过滤器怎么设计的呢？ 
这里说的"令牌前置过滤器"是指 **API Gateway 层的 Token 认证过滤器**吧？我按这个方向回答。

### 核心设计

```
请求 → Gateway Filter → Token验证 → 路由到下游服务
                ↓
         Redis 缓存 Token
                ↓
         必要时回源 Auth服务
```

过滤器做的事：提取 Token → 验签 → 鉴权 → 透传用户信息

### 关键实现点

**1. JWT 本地验签，不回源**

用公钥本地验 JWT 签名，避免每次请求都打 Auth 服务。只有 Token 在黑名单里才需要走 Redis 查一次。

**2. Token 黑名单**

退出登录、强制下线的 Token 写入 Redis，key 是 `blacklist:{jti}`，TTL 对齐 Token 过期时间。过滤器验签通过后，再查一次黑名单。

**3. 用户信息透传**

验证通过后把 userId、roles 等写入请求 Header，下游服务直接取，不用重复解析 Token。

### 踩过的坑

- **时钟偏移导致 Token 误判过期**：JWT exp 校验加 30s 容忍窗口
- **黑名单 Redis 挂了怎么办**：降级策略是只做本地验签，跳过黑名单检查，安全性降级但服务不中断
- **Filter 执行顺序**：认证 Filter 要在限流、日志 Filter 之前，否则匿名流量也被计入限流配额

### 性能考量

本地验签是纳秒级，Redis 黑名单查询是微秒级，整体加在 Gateway 的延迟 **< 1ms**，可以接受。

你们是 Spring Cloud Gateway 还是自研的？

## 6.redis的数据结构有哪些，适用场景？ 
### 核心数据结构 + 场景

**String**

- 最基础，计数器、分布式锁、缓存单个对象
- 库存扣减就是 `INCRBY` / `DECRBY`

**Hash**

- 存对象字段，比 String 序列化整个对象更节省内存，支持单字段更新
- 适合用户信息、商品详情，字段频繁部分更新的场景

**List**

- 有序、可重复，支持两端操作
- 消息队列（简单场景）、最新动态列表、评论分页

**Set**

- 无序不重复，支持交并差集
- 去重场景：UV 统计、抽奖已参与用户、共同好友

**ZSet（Sorted Set）**

- 带分数的有序集合，按 score 排序
- 排行榜、延迟队列（score 存时间戳）、热搜榜

**Bitmap**

- 本质是 String，按位操作
- 用户签到（365 天只占 46 字节）、活跃用户统计

**HyperLogLog**

- 概率统计基数，误差约 0.81%，内存固定 12KB
- UV 统计，不需要精确值时替代 Set

**Stream**（Redis 5.0+）

- 持久化消息队列，支持消费者组
- 轻量级替代 MQ，但功能比 RabbitMQ 弱很多

### 我常踩的坑

- **ZSet 排行榜**：成员数量大时 `ZRANGE` 全量拉取很慢，要分页
- **Hash vs String**：Hash 字段超过 128 个或 value 超过 64 字节会从 ziplist 转 hashtable，内存会涨，要注意

## 7.项目中最具有挑战的地方？是否还有别的方法？ 
结合我们前面聊的票务场景，说一个我觉得最有挑战的：**热点库存的分片一致性问题**。

### 挑战在哪

库存分片成 10 个 key 后，扣减是分散的，但有几个棘手问题：

- **路由不均**：随机路由在瞬间高并发下，某个分片可能先扣完，其他分片还有余量，导致用户看到"无票"但实际有票
- **汇总库存不准**：各分片单独扣，总库存数字是聚合计算的，实时性差
- **回滚对齐**：超时回滚时要还回原来扣的那个分片，订单必须记录扣了哪个分片

### 我的解法

订单创建时记录 `shard_id`，回滚精准还回对应分片。查询总库存用 Lua 脚本聚合所有分片，允许短暂不精确。

路由策略从随机改成**轮询 + 分片余量感知**，定期探活各分片剩余量，优先路由到余量多的。

### 还有别的方法吗

**方案二：Redis Cluster 自动分片**

不手动分片，直接用 Cluster 让 Redis 自己做 slot 分布。好处是运维简单，坏处是 Lua 脚本跨 slot 不支持，原子性保证变难。

**方案三：放弃 Redis，用数据库行锁**

SELECT FOR UPDATE 直接锁库存行，强一致，但 QPS 撑不住，只适合低并发场景。

**方案四：本地内存扣减 + 异步同步**

每个节点预分配一批库存到本地，扣完再申请。延迟最低，但节点宕机会丢已分配未消费的库存，需要心跳 + 回收机制。

### 我的判断

方案四在极致性能场景下值得考虑，但工程复杂度高，**容错逻辑写起来很脏**。大多数场景 Redis 分片已经够用，过度优化没必要。

## 9.字符编码 
这个问题范围比较广，我说几个后端开发中真正重要的点。

### 核心概念

- **ASCII**：128 个字符，单字节，英文够用
- **Unicode**：字符集，定义了全球字符的**码点**（如 `U+4E2D` = '中'）
- **UTF-8**：Unicode 的**编码方式**，变长 1-4 字节，兼容 ASCII，是互联网事实标准
- **UTF-16**：Java/Go 内部字符串用的，`char` 是 2 字节，处理 emoji 等增补字符需要代理对
- **GBK**：国标，双字节，老系统常见，和 UTF-8 不兼容

### 后端常踩的坑

**1. MySQL 的 utf8 不是真 UTF-8**

MySQL 的 `utf8` 最多 3 字节，存不了 emoji（需要 4 字节）。要用 `utf8mb4`，这个坑踩过一次就忘不了。

**2. Go 的 string 和 rune**

```go
s := "中文"
len(s)        // 6，字节数
len([]rune(s)) // 2，字符数
```

按字节切字符串会截断多字节字符，中文场景要用 `[]rune`。

**3. 服务间编解码不一致**

老系统用 GBK，新系统用 UTF-8，HTTP 接口对接时乱码。解法是在边界层统一转码，内部全用 UTF-8。

### 一个细节

UTF-8 的**BOM**（`EF BB BF`）在 Linux 下会导致脚本执行异常，Windows 记事本默认带 BOM，跨平台协作时要注意。

有具体想深入的方向吗？比如 Go 的字符串底层或者 MySQL 编码问题？

10.如何学习 
## 11.手撕：查询字符数组中与目标数组匹配的起始位置
经典字符串匹配，直接上 **KMP**，暴力 O(nm) 在面试里说得过去但不够亮眼。

### 思路

KMP 核心是复用已匹配信息，通过 `next` 数组避免回溯，时间复杂度 **O(n+m)**。

### 代码（Go）

```go
func search(source, target []byte) []int {
    n, m := len(source), len(target)
    result := []int{}
    if m == 0 || n < m {
        return result
    }

    // 构建 next 数组
    next := make([]int, m)
    next[0] = 0
    for i, j := 1, 0; i < m; i++ {
        for j > 0 && target[i] != target[j] {
            j = next[j-1]
        }
        if target[i] == target[j] {
            j++
        }
        next[i] = j
    }

    // 匹配
    for i, j := 0, 0; i < n; i++ {
        for j > 0 && source[i] != target[j] {
            j = next[j-1]  // 回退但不回溯 i
        }
        if source[i] == target[j] {
            j++
        }
        if j == m {
            result = append(result, i-m+1)
            j = next[j-1]  // 继续找下一个匹配
        }
    }
    return result
}
```

### 关键点

- `next[i]` 表示 target 前 i+1 个字符中，**最长公共前后缀长度**
- 匹配失败时 `j = next[j-1]`，i 不回退，这是 KMP 的精髓
- 找到一次匹配后 `j = next[j-1]` 而不是 `j = 0`，支持**重叠匹配**

### 验证

```
source: a b a b c a b a b
target: a b a b
output: [0, 4]  ✓
```

暴力解我也能写，但 KMP 在 target 有大量重复前缀时优势明显，比如 `aaaaaab` 这种模式，暴力退化严重。