📝 本文记录了一些面试问题和知识点总结。
📚 知识点总结
1. Redux 的发布订阅模式 🔄
Redux 的 store 通过 <Provider> 注入 React Context。connect 函数将组件与 store 连接:
- 它订阅 store 的变化,在每次 state 更新时重新计算
mapStateToProps的结果 - 通过浅比较决定是否触发组件重渲染
- 尽管所有监听器都会在每次 dispatch 后被调用,但只有依赖了已变更 state 片段的组件才会实际更新
connect不负责绑定 reducer 或 effect,它只负责 state 选择 和 action 分发
2. Raft 算法是如何复制日志的 📋
流程概述:
- 领导者接收到客户端请求后,创建日志项并追加到本地日志
- 通过
AppendEntries RPC复制给 Follower - 当领导者收到大多数 Follower 的成功响应后,将该日志项标记为已提交(committed)
- 将其 apply 到本地状态机(如 etcd 的 KV 存储),随后返回成功给客户端
- 领导者在后续的
AppendEntries(包括心跳)中携带当前的commitIndex - Follower 收到后,更新自己的
commitIndex,并将所有已提交但尚未 apply 的日志按序 apply 到本地状态机
Q: 如果来自领导者的追加日志和本地日志不一样,如何解决?
📌 此时,跟随者会给主节点返回错误,主节点收到后会回退日志索引重新发送,直到和 follower 的本地日志的索引和任期号相同,领导者将强制覆盖之后不一致的日志。
3. 手写防抖节流 ⏱️
防抖(Debounce)实现:
1 | function debounce(delay, fn) { |
节流(Throttle)实现:
1 | function throttle(fn, wait) { |
4. MySQL 多对多是怎么实现的 🔗
- ORM 层:只需要定义
many2many关键字即可 - SQL 层:得显式声明关联表
5. ReAct Agent 是如何设计的 🤖
ReAct Agent 分为四个模块:
- Thought:分析当前状态与目标
- Action:基于判断调用工具或执行操作
- Observation:观察行动结果
- Thought:根据新信息再次推理
工作原理:
简单来说就是一个带中止循环结构,user 把 tools 信息注册到 query 的提示词里,由 LLM 输出:
Thought:当前状态Action:调用哪个函数arg:传入哪个参数finish:是否结束
再将 Action 和 arg 交给 tools 去调用,再把获取到的 result 交给下一轮循环。
示例代码:
1 | while True: |
6. Redis 的缓存降级策略如何设计 🛡️
本地缓存回退策略:
在 Redis 缓存层之外,增加一个应用内的本地缓存层(如 Caffeine、Guava Cache 等)。
- 当 Redis 不可用时,本地缓存未命中才从数据库拿数据
- 这样可以有效减少对数据库的压力,提高系统的可用性
7. Redis 和 MySQL 的双写一致性 🔄
常规做法:
- 写请求:先改 MySQL 再删 Redis 缓存
- 读请求:若 Redis 未命中,查 MySQL 并更新缓存
Q: 如果是先写缓存呢,怎么保证一致性?
📌这种情况下,如果数据库写入失败可能会获取脏数据,所以,必须对写请求本身做持久化,以便后续恢复。做法是可以在缓存和数据库之间加一个mq,让mq持久化消息并尝试更新数据库。但是大部分情况不推荐。
8. JS 原型链 🔗
工作原理:
- 当我们声明一个类或者一个构造函数时,JS 引擎会自动创建一个空的原型对象
prototype - 调用这个构造函数时,会将该对象的
__proto__指向构造函数的原型对象 - 当执行一个对象某个方法时,如果这个对象没有该方法,则会去
__proto__指向的原型对象上找 - 沿着原型链向上查找,直到找到
Object.prototype为止
原型链查找路径:
1 | 对象 → __proto__ → 构造函数.prototype → ... → Object.prototype → null |
9. golang的内存逃逸
在Go语言中,内存分配有两种方式:栈分配和堆分配。栈分配是在函数调用时为局部变量分配内存,当函数返回时,这些内存会自动释放。而堆分配则是通过 new 或者 make 函数动态分配内存,需要手动进行释放。内存逃逸是指原本应该在栈上分配的内存被分配到了堆上。这意味着即使函数返回后,这部分内存也不会被自动释放,需要等待垃圾回收器来回收。
📌 Go 编译器会在编译时进行逃逸分析,其核心逻辑是:如果一个变量的生命周期超出了当前函数的作用域,或者其地址被外部引用,那么它就必须分配在堆上,否则分配在栈上。