自我介绍
## redis用了哪些数据结构
**五种基本类型**

|类型|底层编码|典型场景|
|---|---|---|
|String|int / embstr / raw|计数、缓存对象|
|Hash|listpack / hashtable|商户信息、用户属性|
|List|listpack / quicklist|消息队列、最新动态|
|Set|listpack / hashtable|去重、共同好友|
|ZSet|listpack / skiplist+hashtable|排行榜、延迟队列|

**重点说几个底层**

**ZSet 的跳表**：为什么不用红黑树？跳表范围查询更直观，实现简单，内存局部性也不比红黑树差，Redis 作者自己说的。我们用 ZSet 做过排行榜和延迟队列，`ZRANGEBYSCORE` 很好用。

**Hash 的 listpack**：元素少于 128 个且 value 不大时用 listpack（老版本叫 ziplist），连续内存，省空间但查找 O(n)——元素少时无所谓。

**三种特殊类型**

- **Bitmap**：本质是 String，按位操作，做签到、用户标签很省内存
- **HyperLogLog**：统计 UV，误差 ~0.81%，固定 12KB，亿级数据也不怕
- **Geo**：底层是 ZSet，经纬度编码成 score，支持附近商家查询，我们项目里用过

实际项目里用得最多的还是 String 和 Hash，ZSet 在排行榜场景真的很顺手。

## redis String zSet底层实现
**String 底层**

三种编码，根据内容自动选择：

- **int**：值是整数且在 long 范围内，直接存数值，`INCR` 操作就是在这层做的
- **embstr**：长度 ≤ 44 字节，redisObject 和 SDS **连续分配**，一次 malloc/free，只读不可修改（修改直接升级为 raw）
- **raw**：> 44 字节，redisObject 和 SDS 分开分配

SDS（Simple Dynamic String）相比 C 字符串的优势：

- 头部存 len 和 alloc，O(1) 获取长度
- 预分配 + 惰性释放，减少 realloc 次数
- 二进制安全，不以 `\0` 判断结束

**ZSet 底层**

两种编码：

**listpack**（元素数 < 128 且单个元素 < 64 字节）

- 连续内存，紧凑存储，省内存
- 查找 O(n)，小数据量无所谓

**skiplist + hashtable** 同时存在，这个设计值得展开说：

```
skiplist：负责范围查询，ZRANGEBYSCORE / ZRANK
hashtable：负责单点查询，ZSCORE O(1) 直接拿
```

两者共同维护，空间换时间。

**跳表结构**

```
level4: [head] ──────────────────→ [50] → nil
level3: [head] ──────→ [20] ──────→ [50] → nil  
level2: [head] → [10] → [20] → [30] → [50] → nil
level1: [head] → [10] → [20] → [30] → [40] → [50] → nil
```

- 每个节点随机层数，期望 O(log n) 查找
- 相比红黑树：范围查询更自然，实现简单，并发场景下锁粒度更好控制

踩过的坑：ZSet 元素超过阈值从 listpack 升级到 skiplist 是**不可逆**的，如果 value 设计不合理导致提前升级，内存会涨不少，需要注意 field 大小控制。

## 跳表时间复杂度 
**核心操作都是 O(log n)**

- **查找**：O(log n)
- **插入**：O(log n)——先找到位置，再更新各层指针
- **删除**：O(log n)——同上
- **范围查询**：O(log n + k)，k 是结果集大小

**为什么是 O(log n)**

跳表本质是**多层索引**，每层节点数是下层的 1/2（概率意义上），类似二分。

最高层跨度最大，从顶层开始往下逐层收窄，期望经过 O(log n) 个节点就能定位。

**空间复杂度**

O(n)——每个节点平均层数是常数（等比级数求和），所以总节点数仍是 O(n)。

**和红黑树对比**

||跳表|红黑树|
|---|---|---|
|查找|O(log n)|O(log n)|
|插入|O(log n)|O(log n)|
|范围查询|更自然，链表顺序遍历|需要中序遍历|
|实现复杂度|简单|旋转逻辑复杂|

最坏情况跳表会退化，但这依赖随机层数极端不均匀，概率极低，工程上可以接受。

## redis过期策略都有什么 
**两种过期策略组合使用**

**惰性删除**

- key 被访问时才检查是否过期，过期就删除
- 优点：对 CPU 友好，不主动消耗资源
- 缺点：过期 key 如果一直不访问，永远不会被清理，内存泄漏

**定期删除**

- 每隔一定时间（默认 100ms）随机抽取一批设了过期时间的 key，删除其中已过期的
- 抽取比例有阈值控制，避免扫描时间过长阻塞主线程
- 两种模式：`SLOW`（定时任务）和 `FAST`（事件循环间隙执行）

两者配合：定期删除兜底内存，惰性删除补漏网之鱼。

**内存淘汰策略（逐出策略）**

光靠上面两个还不够，内存打满时触发：

```
noeviction      # 不淘汰，直接报错（默认）
allkeys-lru     # 全部 key 中 LRU 淘汰
volatile-lru    # 只淘汰设了过期时间的 key，LRU
allkeys-lfu     # LFU，按访问频率
volatile-ttl    # 优先淘汰快过期的
allkeys-random  # 随机淘汰
```

生产上一般用 `allkeys-lru`，缓存场景下所有 key 都可以淘汰，符合直觉。

踩过的坑：用默认 `noeviction`，内存打满后写操作直接报错，线上炸过一次，之后统一改成 `allkeys-lru` 并加了内存使用率告警。

## 了解TreeMap吗 
了解，Java 里用得比较多。

**底层实现**

红黑树，key 有序，增删查都是 O(log n)。

红黑树性质：

- 节点红或黑，根和叶（nil）是黑
- 红节点的子节点必须是黑
- 任意节点到叶的路径，黑节点数相同

保证树高 ≤ 2log(n+1)，避免退化。

**和 HashMap 对比**

||TreeMap|HashMap|
|---|---|---|
|底层|红黑树|数组+链表/红黑树|
|有序性|按 key 排序|无序|
|增删查|O(log n)|O(1) 均摊|
|适用场景|范围查询、有序遍历|随机访问|

**TreeMap 独有的 API 很实用**

```java
treeMap.floorKey(key)    // 小于等于 key 的最大键
treeMap.ceilingKey(key)  // 大于等于 key 的最小键
treeMap.subMap(k1, k2)   // 范围截取
```

实际场景比如：时间窗口限流、区间覆盖查询、按分数段统计，用 TreeMap 比自己维护有序结构省事很多。

**和跳表的关系**

功能上高度重叠，都是有序 + O(log n)。Redis 选跳表是因为范围操作实现更直观，Java 标准库选红黑树是因为最坏情况更可控。并发版本 `ConcurrentSkipListMap` 反而用了跳表，因为跳表的 CAS 并发控制比红黑树旋转操作更容易实现无锁。

## TreeMap和HashMap的区别 
刚才其实已经带过一次，简单总结一下：

**核心区别**

- **底层结构**：TreeMap 红黑树，HashMap 数组+链表/红黑树（链表长度超8转红黑树）
- **有序性**：TreeMap 按 key 自然排序或自定义 Comparator，HashMap 无序
- **性能**：HashMap O(1) 均摊，TreeMap O(log n)
- **key 限制**：TreeMap 的 key 必须可比较（实现 Comparable 或传 Comparator），HashMap 只需要 hashCode + equals

**什么时候选 TreeMap**

- 需要范围查询：`subMap`、`headMap`、`tailMap`
- 需要找最近邻：`floorKey`、`ceilingKey`
- 需要有序遍历

纯随机访问场景没理由用 TreeMap，O(log n) 比 O(1) 差不少。

**HashMap 的一个细节**

Java 8 之前链表过长查找退化到 O(n)，8 之后超阈值转红黑树，最坏 O(log n)。但转换有成本，实际上 hash 函数设计合理的话基本不会触发。

这两个没什么特别深的 trade-off，选择标准很清晰——**需不需要有序**，需要就 TreeMap，不需要就 HashMap。

给出一串代码，优化一下 
手撕：
## 输出字符串能组成的所有的IP地址 
经典回溯，LeetCode 93，做过。

**思路**

字符串切成 4 段，每段 1~3 位，满足：

- 无前导零
- 数值 ≤ 255
- 四段用完恰好消耗完整个字符串

```go
func restoreIpAddresses(s string) []string {
    res := []string{}
    backtrack(s, 0, []string{}, &res)
    return res
}

func backtrack(s string, start int, parts []string, res *[]string) {
    if len(parts) == 4 {
        if start == len(s) {
            *res = append(*res, strings.Join(parts, "."))
        }
        return
    }

    for l := 1; l <= 3; l++ {
        if start+l > len(s) {
            break
        }
        seg := s[start : start+l]
        // 前导零剪枝
        if l > 1 && seg[0] == '0' {
            break
        }
        // 数值剪枝
        val, _ := strconv.Atoi(seg)
        if val > 255 {
            break
        }
        parts = append(parts, seg)
        backtrack(s, start+l, parts, res)
        parts = parts[:len(parts)-1]
    }
}
```

**几个剪枝关键点**

- 前导零：`l > 1 && seg[0] == '0'` 直接 break，不是 continue，后面更长的也不合法
- 超 255：同理 break
- 剩余字符兜底：可以加 `len(s)-start > (4-len(parts))*3` 提前剪，但一般输入不长，影响不大

时间复杂度 O(1)——IP 地址组合数有上限，最多 2^32 种，实际搜索空间很小。

## 设计电影院的票务系统，数据库有哪些表 
这道题偏系统设计，先梳理核心实体。

**核心表**

**影院 & 场馆**

```
Cinema: id, name, address, city
Hall: id, cinema_id, name, total_rows, total_cols
```

**电影**

```
Movie: id, title, duration, rating, genre, release_date
```

**排片（场次）**

```
Screening: id, movie_id, hall_id, start_time, end_time, price, status
```

**座位**

```
Seat: id, hall_id, row, col, type(normal/vip)
```

**订单 & 票**

```
Order: id, user_id, screening_id, total_price, status, created_at
Ticket: id, order_id, seat_id, screening_id, price, status(available/locked/sold)
```

**用户**

```
User: id, phone, nickname
```

**几个设计重点**

座位状态放在 `Ticket` 表而不是 `Seat` 表——`Seat` 是静态物理结构，`Ticket` 是每场次动态状态，否则跨场次查询很麻烦。

**并发选座怎么防超卖**

```sql
UPDATE ticket SET status='locked', order_id=?
WHERE screening_id=? AND seat_id=? AND status='available'
```

乐观锁思路，影响行数为 0 说明已被抢占，直接返回失败。

**锁座超时释放**

用户选座未支付需要释放，两种方案：

- 定时任务扫描 `locked` 超时的票
- 写入时记 `lock_expire_time`，查询可用座位时过滤掉未过期的锁定座位

生产上我倾向后者，不依赖定时任务的精度，查询时顺手过滤即可。

还需要考虑分库分表吗？要展开说说吗？

最近在看什么书籍、学习了什么新知识 
## 项目中有调研过其他消息队列的选型吗 
调研过，我们主要在 **Kafka、RocketMQ、Pulsar** 之间权衡过。

**Kafka**

优点：吞吐量天花板最高，生态最成熟，日志场景无敌 缺点：

- 不支持延迟消息，要自己实现很麻烦
- Topic 数量多时 partition 文件膨胀，元数据压力大
- 消息回溯方便，但 exactly-once 配置复杂

我们日志采集、埋点这类高吞吐场景用 Kafka。

**RocketMQ**

- 原生支持延迟消息、事务消息，这两个在业务场景很刚需
- 消息轨迹、死信队列开箱即用，运维友好
- 吞吐比 Kafka 低一档，但业务场景够用

秒杀、订单这类业务消息我们选 RocketMQ，事务消息解决了前面说的 Redis 扣库存后消息丢失的问题。

**Pulsar**

调研过但没上生产：

- 存算分离架构，BookKeeper 存储层理论上扩展性更好
- 原生多租户、跨地域复制
- 但运维复杂度高，组件多，团队维护成本扛不住

**选型结论**

```
日志/埋点/高吞吐    → Kafka
业务消息/事务/延迟  → RocketMQ
```

没必要统一成一套，按场景选。团队之前想用 Kafka 统一所有场景，延迟消息自己用时间轮实现，后来维护成本太高还是拆开了。

反问