面灵AI→

袋鼠云 Node.js 后端一面面经

轮次
一面
结果
已挂
时间
2026-10
来源
牛客网

《面试题目》

  1. 了解回调地狱吗?
  2. Node.js 流式处理是什么?
  3. TypeORM 的实体关系定义是怎么做的?
  4. 库存流水表有哪些必填字段?如何防止超扣?
  5. 项目为什么使用 SQLite 和 MySQL 双数据库连接?
  6. 如何实现 SQLite 一键迁移 MySQL?
  7. 怎么用 SQLite 事务与定时任务代替 Redis 和 MQ 的异步实现逻辑?
  8. 迁移数据库的时候怎么确保写入的数据不丢失(双写)?

《参考解析》

回调地狱与异步演进。回调地狱是早期 Node.js 里多层嵌套回调导致的横向金字塔:错误处理分散在每个回调里、无法用 try/catch 捕获异步异常、代码无法顺序阅读。演进路径是 Promise(把嵌套变成 .then 链,错误集中到 .catch,但链式仍不直观)→ async/await(用同步写法表达异步流程,配合 try/catch)→ 并发控制(Promise.all 并行、Promise.allSettled 不中断、for await 串行消费)。要能说出 async/await 的坑:在 forEach 里 await 不会等待(要用 for...of 或 Promise.all(map))、忘记 await 会吞掉拒绝导致 unhandled rejection、串行 await 会放大延迟(能并行就并行)。流式处理解决的问题是内存与背压:用 stream 分块处理大文件、大响应、大结果集,而不是全部读进内存;pipe 或 pipeline 会自动传递背压(可写端消费不过来时暂停可读端),Transform 用来边读边转换,finished/error 事件与 pipeline 负责错误传播与资源释放。反问可以往「什么场景用流」答:文件上传下载、日志处理、数据库大结果集导出、代理转发。

TypeORM 实体关系。用装饰器声明:@Entity 定义表与列(@PrimaryGeneratedColumn、@Column、@CreateDateColumn),关系用 @ManyToOne/@OneToMany(一对多用 @JoinColumn 在多的一方存外键)、@ManyToMany(需要中间表 @JoinTable)、@OneToOne。实战要点:eager/lazy 与 relations 决定要不要联表查询,写不好会 N+1;级联(cascade)能省事但很容易误删数据,生产上要谨慎;synchronize: true 只适合开发环境,生产要用 migration 管理表结构变更;事务用 DataSource.transaction 或 QueryRunner,保证「扣库存 + 写流水」在一个事务里。

库存流水表的字段设计与防超扣。流水表(或称库存变更记录)至少要有的字段:自增或雪花主键、商品/SKU id、变更类型(入库、出库、锁定、释放、回滚)、变更数量(有符号更直观)、变更前余量、变更后余量、业务单据号(订单号,用于幂等)、操作人/来源(用户、系统、渠道)、时间戳、以及备注。索引要按业务查询建(SKU + 时间、订单号唯一)。防超扣有三层:数据库层用乐观锁或条件更新做原子扣减,例如 UPDATE stock SET qty = qty - ? WHERE sku = ? AND qty >= ?,看影响行数判断是否成功,或用 version 字段做乐观锁;应用层用唯一索引 + 幂等键防重复提交(同一订单号只扣一次),流水表在订单号 + 类型上建唯一索引;事务与锁保证扣库存与写流水原子,必要时 SELECT ... FOR UPDATE 锁行(注意锁范围与死锁,按固定顺序加锁)。Redis 预热库存加 Lua 是更常见的方案,用在本项目里则要看「SQLite 单机单写」的特性。

为什么 SQLite 与 MySQL 双数据库。合理的答案通常是场景分离:SQLite 用于本地或边缘端——零运维、单文件、部署简单、读性能好、适合单机或嵌入式场景的设备侧存储;MySQL 用于中心服务端——支持并发写、网络访问、多实例与备份、生态与运维工具成熟。这样架构的代价是数据同步与一致性:两端表结构可能不同(方言差异)、自增主键冲突、时间与时区、以及网络断连时的重试。回答时要主动把「为什么不用一套」讲清楚(成本、部署环境、网络条件、并发量),并说明边界:SQLite 不适合高并发写,SQLITE_BUSY 要靠 WAL 模式与串行化写来规避。

一键迁移与双写不丢数据。迁移实现思路:先从两端 schema 生成映射(同名字段直接映射、类型转换、时区统一),再分批读取源库(按主键或时间游标分页,避免一次性载入内存),写入目标库时用「批量 insert + 唯一键 upsert」保证可重跑,记录每批的游标与校验和(行数、主键范围、关键字段哈希),迁移后做全量对账。双写不丢数据的做法:先写主库再写从库(或先 MySQL 再 SQLite),每笔写带全局唯一 id 与时间戳;同步失败要进入重试队列并告警,而不是吞掉错误;用幂等写入(INSERT ... ON CONFLICT/upsert)让重放安全;定期或增量对账,用「源库有、目标库没有」的差集回补;切换阶段要明确单一写入口(避免双写变成双主),并保留回滚路径。要强调「双写期间的一致性是最终一致,不是强一致」,所以必须有对账任务作为兜底。

用 SQLite 事务加定时任务替代 Redis 与 MQ。思路是把「异步解耦」降级为「同库事务 + 轮询」。具体做法:把待处理的异步任务写进一张任务表(状态、重试次数、下次执行时间、payload),与业务写入放在同一个事务里提交——这样「业务成功但消息没发出去」的窗口就消失了;由一个定时任务按周期拉取 status = pending AND next_run_at <= now() 的记录,按行加锁或标记为 processing 后执行,失败则更新重试次数与下次执行时间,超过次数进死信状态。它能替代的是「可靠投递 + 延迟执行」这两类需求,替代不了的是高吞吐削峰与多消费者水平扩展:SQLite 单写者模型下并发能力有限,轮询有延迟(最小粒度取决于轮询周期),任务量大时表会膨胀需要归档。所以答这题的正确姿势是先说明「在什么规模下可以这么做」,再指出规模上去后应该换回 Redis Stream 或 MQ。