1.快手二面：
1.1全程拷打项目
1.2手撕代码：二叉树之字形遍历

2.字节二面（已约三面）:
2.1聊天；谈谈大模型
## 2.2redis雪崩以及如何解决
**缓存雪崩**：大量 key 同时过期，或 Redis 实例直接挂掉，导致请求全部打到 DB，DB 扛不住崩溃。
**解决思路：**
**过期时间打散**
```go
expiry := baseExpiry + rand.Intn(300) // 加随机抖动，避免集中失效
```
**熔断 + 限流**
- 在缓存层加熔断（如 sentinel），DB 压力超阈值直接降级返回兜底数据
- 我们之前用 `golang.org/x/time/rate` 在服务层做令牌桶限流
**多级缓存**
- 本地缓存（如 ristretto/bigcache）兜底，即使 Redis 挂了也能扛一波
- 但要注意本地缓存一致性问题，我们当时选择允许短暂脏读
**Redis 高可用**
- Sentinel 或 Cluster 模式，避免单点
- 挂掉时的 failover 时间窗口内，本地缓存是关键
**和缓存击穿的区别**：雪崩是批量 key 问题，击穿是单个热点 key 失效被大量并发穿透——后者用 singleflight 更合适。
实际踩过的坑：定时任务批量刷新缓存时没加抖动，凌晨集中过期，DB CPU 一下飙到 90%+，后来强制加了随机偏移才解决。

## 2.3热key问题
**热 key**：少数 key 请求量极高，单个 Redis 节点 CPU/带宽打满，其他 key 也受影响。
**识别方式**
- `redis-cli --hotkeys`（需开启 LFU 策略）
- 实时监控：在客户端埋点统计 key 访问频次，超阈值上报
- 我们用的是在中间件层滑动窗口计数，异步上报到监控平台
**解决方案**
**本地缓存兜底**（最有效）
```go
// 热key直接走进程内缓存，不打Redis
if val, ok := localCache.Get(key); ok {
    return val
}
```
用 ristretto 或 bigcache，TTL 设短一点（几秒），接受轻微不一致
**key 拆分**
```
hot_key → hot_key_0, hot_key_1, ... hot_key_N
// 读时随机或hash取一个副本
```
写时同步更新所有副本，N 一般取 10~50
**客户端读写分离**
- 读打 slave，但 slave 同样可能成为瓶颈，治标不治本
**我的判断**：
- 热 key + 数据不敏感 → 本地缓存优先，简单有效
- 热 key + 需要强一致 → 基本没法纯靠缓存解决，得从业务侧限流或异步化
踩过的坑：key 拆分后，某次活动更新数据只更新了部分副本，导致读到旧数据持续了几分钟——写操作要确保原子性地更新所有副本，或者接受最终一致。

2.3手撕代码：求单向链表倒数第K个节点

3.腾讯三面
3.1拷打项目（被问烂）
## 3.2linux操作系统是基于什么数据结构来管理
这个问题范围比较广，我理解你问的是几个核心子系统各自用了什么数据结构？
**进程管理**
- 进程描述符 `task_struct` 组织成**红黑树**（CFS 调度器按 vruntime 排序）
- 同时维护双向链表用于遍历所有进程
**内存管理**
- 虚拟地址空间 `vm_area_struct` 用**红黑树** + 链表管理
- 物理页用 buddy system（**位图 + 链表**）管理
- slab allocator 内部是**链表 + 缓存池**
**文件系统**
- VFS 的 dentry 缓存用**哈希表 + LRU 链表**
- ext4 目录项索引用 **htree（hash tree）**
**网络**
- socket 查找用**哈希表**
- TCP 连接管理也是哈希表，TIME_WAIT 优化是个经典话题
**总结规律**：
- 需要快速查找 → 红黑树或哈希表
- 需要顺序遍历 → 链表
- 两者都要 → 红黑树 + 链表组合（内核里非常常见）
说实话内核这块我更多是概念层理解，没有深入读过源码。如果你要深挖某个子系统我可以展开，但会说明哪些是我确定的，哪些是推断。

文件的
## 3.3删除一个文件的命令；rm返回成功能代表这个文件真的被删掉了吗
**删除命令**

```bash
rm file.txt
rm -rf dir/   # 递归删除目录
```

**rm 成功 ≠ 文件真的被删掉**

Linux 删除文件的本质是：**减少 inode 的硬链接计数**，只有计数降为 0 且没有进程持有该文件的 fd，数据块才会被真正释放。

`rm` 只做了：unlink 系统调用，把目录项删掉，inode 引用计数 -1。

**两种没真正删掉的场景：**

**1. 硬链接存在**

```bash
ln file.txt file_hard.txt
rm file.txt   # inode 引用计数还是 1，数据还在
```

**2. 进程还持有 fd（更常见）**

```bash
# 某进程 open 了这个文件，rm 之后
ls /proc/<pid>/fd   # 还能看到该文件，显示 (deleted)
```

数据不会释放，直到进程关闭 fd 或退出。

**实际踩过的坑**：日志文件被 rm 了但磁盘空间没释放，排查半天发现是日志进程还在往里写。解决方式是 `> /proc/<pid>/fd/<n>` truncate 掉，或者重启进程。

这也是为什么删除大文件后 `df` 显示空间没变的经典原因。

## 3.4linux启动一个进程可能会使用哪些资源;这些资源有什么限制
**启动进程会申请的资源**

- **内存**：虚拟地址空间、栈、堆、代码段映射
- **文件描述符**：stdin/stdout/stderr 默认就占 3 个
- **进程 ID（PID）**
- **线程**：即使单线程也有主线程
- **CPU 时间片**：调度器分配
- **信号处理表、页表**等内核结构

**限制机制：ulimit**

```bash
ulimit -a          # 查看当前所有限制
ulimit -n 65535    # 设置最大 fd 数
```

常见限制项：

|资源|参数|典型默认值|
|---|---|---|
|打开文件数|`-n`|1024|
|栈大小|`-s`|8MB|
|进程数|`-u`|视系统|
|内存大小|`-v`|unlimited|

**系统级限制**

- `/proc/sys/kernel/pid_max`：最大 PID 数
- `/proc/sys/fs/file-max`：系统级 fd 上限
- cgroup：容器场景下限制 CPU、内存、IO

**踩过的坑**：服务上线后偶发 `too many open files`，排查发现是 ulimit `-n` 还是默认 1024，连接池 + 日志 fd 加起来超了。后来统一在启动脚本里设置，或者用 systemd 的 `LimitNOFILE`。

cgroup 这块在 K8s 环境用得更多，limits/requests 本质就是在操作 cgroup。

## 3.5在使用数据库事务的时候需要关注什么
**核心关注点**

**1. 事务边界要尽量小**

- 事务内只放必要的 DB 操作，HTTP 调用、MQ 发送绝对不能放里面
- 长事务持锁时间长，容易造成锁等待甚至死锁

**2. 隔离级别选对**

- 大多数场景 `Read Committed` 够用
- 需要可重复读才上 `Repeatable Read`（MySQL 默认）
- 用 RR 要注意幻读，以及 MVCC 快照时间点的问题

**3. 锁的意识**

- `SELECT ... FOR UPDATE` 会加行锁，范围查询可能升级为间隙锁
- 多个事务操作多张表，注意加锁顺序一致，避免死锁

**4. 事务与外部操作的一致性**

```go
// 典型错误：事务提交前发了MQ，事务回滚了但消息出去了
tx.Begin()
db.Insert(order)
mq.Send(orderEvent)  // ❌ 不该在这里
tx.Commit()
```

解决方案：事务表 + 本地消息，提交后再发

**5. 异常处理要显式回滚**

```go
defer func() {
    if err != nil {
        tx.Rollback()
    }
}()
```

**踩过的坑**：RR 隔离级别下，事务开启后读到的是快照数据，另一个事务已提交的更新对当前不可见，导致超卖——最终加了 `SELECT FOR UPDATE` 强制读最新值才解决。

## 3.6多个节点搭建数据库，数据库的主从复制
**主从复制核心流程（以 MySQL 为例）**

1. 主库写操作记录到 **binlog**
2. 从库 IO thread 拉取 binlog 写入 **relay log**
3. 从库 SQL thread 回放 relay log

**三种 binlog 格式**

- `STATEMENT`：记 SQL，省空间，但有不确定函数问题（now()、uuid()）
- `ROW`：记行变更，数据准确，但日志量大
- `MIXED`：自动切换，实践中用得多

**同步模式**

- **异步**：默认，主库不等从库确认，性能好但主库宕机可能丢数据
- **半同步**：至少一个从库写入 relay log 再返回，延迟略高
- **全同步**：所有从库确认，基本不用

**主要问题**

**主从延迟**：

- 从库单线程回放是历史痛点，MySQL 5.7+ 支持并行回放
- 监控 `Seconds_Behind_Master`，延迟高时读从库会读到旧数据

**实践处理方案**：

```
写后立即读 → 强制走主库
对延迟不敏感的查询 → 走从库
```

**数据一致性**：

- 主库宕机 failover 时，异步模式下从库可能少数据
- 用 GTID 模式可以精确知道哪些事务没同步到

踩过的坑：业务写完订单立刻查列表走了从库，偶发查不到刚创建的订单，加了路由策略强制写后读主库才解决。

## 3.7在go语言调用方法时，传递参数时是值传递还是引用传递
**Go 只有值传递，没有引用传递。**

所有参数传递都是拷贝，区别在于拷贝的是什么。

**传基本类型 / struct**

```go
func modify(n int) { n = 10 }  // 外部不变
```

拷贝的是值本身。

**传指针**

```go
func modify(p *User) { p.Name = "x" }  // 外部会变
```

拷贝的是指针值（地址），但通过地址能修改原始数据。看起来像引用传递，本质还是值传递。

**slice / map / channel 要单独说**

- 它们本身是含指针的结构体（slice 是 `{ptr, len, cap}`）
- 传递时拷贝的是这个结构体
- 所以修改元素会影响原始数据，但 **对 slice 做 append 触发扩容后不会影响原始**

```go
func modify(s []int) {
    s[0] = 99      // 影响原始
    s = append(s, 1, 2, 3...)  // 扩容后s是新底层数组，不影响原始
}
```

这是个经典坑，面试和生产都踩过——函数内 append 以为改了外面的 slice，结果没生效，最后还是得传指针或者返回新 slice。

**结论**：想在函数内修改外部数据，传指针是最明确的方式。

## 3.8defer命令原理
**defer 基本语义**

函数返回前按 **LIFO 顺序**执行，常用于资源释放、解锁、recover。

**底层原理**

Go 1.13 之前：每个 defer 在堆上分配 `_defer` 结构体，开销较大。

Go 1.13+：编译器优化，简单场景直接在**栈上分配**，性能大幅提升。

Go 1.14+：引入**开放编码（open-coded defer）**，直接展开成普通函数调用，几乎零开销。

**关键细节**

**参数在注册时求值**

```go
x := 1
defer fmt.Println(x)  // 打印 1，不是 2
x = 2
```

**与返回值的交互**

```go
func f() (result int) {
    defer func() { result++ }()  // 能修改命名返回值
    return 1  // 实际返回 2
}
```

匿名返回值就没这个效果，这块容易踩坑。

**recover 必须在 defer 里**

```go
defer func() {
    if r := recover(); r != nil {
        // 捕获 panic
    }
}()
```

**踩过的坑**：循环里大量注册 defer，以为函数结束才执行没关系，结果持有锁/fd 时间过长导致资源泄漏——循环内资源释放应该封装成独立函数，不要依赖 defer。

## 3.9怎么在1000亿数据（格式为url）中找到重复次数最多的一个
**核心思路：分治 + 哈希**

1000亿条数据内存装不下，直接统计不现实。

**第一步：哈希分片**

```
hash(url) % N → 写入对应文件 file_0, file_1, ... file_N
```

相同 url 一定落在同一个文件，N 取 1000 左右，每个文件可以装进内存。

**第二步：单文件统计** 对每个分片文件用 HashMap 统计频次：

```go
map[string]int  // url -> count
```

记录每个文件中频次最高的 url 及其 count。

**第三步：全局归并** N 个文件各自的 top1，做一次比较取最大值。

**工程细节**

- 分片时 hash 函数要稳定，推荐 murmurhash，比 md5 快
- 单文件如果还是太大（hash 冲突集中），可以二次分片
- 多机并行：每台机器处理部分分片，最后汇总

**时间复杂度**：O(N)，每条数据读两次（分片一次，统计一次）

**如果只有一台机器内存有限**，可以用 **BloomFilter 去重 + 分片**，但 BloomFilter 只能判断存在性，不能计数，这题还是老老实实分片。

这类题本质都是：**内存装不下 → 哈希分治 → 各自统计 → 归并结果**，换成 TopK 也是同样套路。