Welcome to Hexo! This is your very first post. Check documentation for more info. If you get any problems when using Hexo, you can find the answer in troubleshooting or you can ask me on GitHub.

Quick Start

Create a new post

1
$ hexo new "My New Post"

More info: Writing

Run server

1
$ hexo server

More info: Server

Generate static files

1
$ hexo generate

More info: Generating

Deploy to remote sites

1
$ hexo deploy

More info: Deployment

本interview.md用于面试,目前出于内容收集期

self.logger: 失去的“权力”与应对策略

  1. 静态方法与类方法的日志权
    self.logger是实例属性,@staticmethod@classmethod内部没有self
    约束:未来如果Service层需要写工具性质的静态方法或工厂类方法,这些方法无法使用self.logger
    当前项目的风险评估:极低。目前Service层设计为纯实例方法,没有静态方法的需求。私有方法也是实例方法,可以直接用self.logger
    应对策略:如果未来真的出现这种场景,在同一个类里混用self.logger和模块级logger会违反宪法一致性。届时更合理的做法可能是将该静态功能独立成一个工具函数,并将 logger作为参数显式传入。

  2. 模块级函数与工具函数的日志权
    约束:如果未来从Service中提取出独立的工具函数(如复杂的格式校验、数据清洗),它们脱离了Service实例,无法使用self.logger
    当前项目的风险评估:中等。但这是逻辑解耦的必然代价。
    应对策略:这正是“显式优于隐式”原则的体现。对于独立函数,应该通过参数传入logger或返回错误由调用方记录日志。这避免了独立函数对特定logger的硬依赖,反而更灵活。

  3. 跨模块通用逻辑的日志权
    约束:如果开发一个通用的校验器基类或Mixin,供多个Service继承,该如何在基类中打日志?是用self.logger还是模块级?如果用self.logger,则强制子类必须设置该属性;如果用模块级,基类日志就绑定了自己的模块路径,子类日志上下文会混乱。
    当前项目的风险评估:低。根据架构,我们目前不计划使用复杂的继承体系,而是通过组合(私有方法)来实现代码复用。
    应对策略:如果真的需要复用,可以使用组合模式,让通用组件在初始化时接收一个logger实例,这是标准的依赖注入,保持了日志来源的清晰可控。

  4. 异步编程下的上下文传递
    self.logger本身是线程安全的,但关键在于我们手动传递的extra字典。
    约束:未来引入asyncio时,当前通过extra={"method": "xxx", "sku": sku}手动传递上下文的方式,在一个async/await交错的环境中会变得脆弱且易出错,因为你必须手动确保在异步切换点前后,extra里的值仍然是正确的。
    当前项目的风险评估:中等。
    应对策略:业界最佳实践是使用contextvars自动记录和传递上下文。届时,我们需要写一个轻量级的适配器,让日志调用从contextvars中自动抓取trace_idmethod等字段。self.logger依然是那个logger实例,只是extra的组装方式从“手动拼接”进化成了“自动填充”。

  5. 与可观测性系统的集成成本
    约束:未来对接OpenTelemetry等自动注入trace_id的工具时,业界标准做法是猴子补丁修改模块级logging模块的行为。我们使用self.logger,它本身也是标准的logging设施,所以不存在兼容性问题。
    当前项目的风险评估:极低。
    应对策略:self.logger完全兼容标准logging生态,只是可能需要在配置自动注入时进行一些额外的适配,但不存在“无法集成”或“失去权力”的问题。

Q & A

Q: 项目中用了实例级logger,而不是模块级的,能解释一下吗?
A:
(第一层,战术需要):
为了可测试性。在这个项目里,我们视日志为系统契约的一部分,而不仅仅是排障的副产品。我们必须能在单元测试里精确断言logger.error没有被意外调用、或者logger.warning恰好被调用了一次。模块级logger的mock是脆弱且依赖实现的,实例级self.logger让我们能一行替换,实现稳定、干净的日志行为验证。
(第二层,战略高度):
这背后,是我们项目的核心设计原则——我们主动选择了将日志行为上升为业务契约来对待。当一条INFO日志被定义为‘金融审计的唯一证据’,一条WARNING日志被定义为‘业务异常监控的触发信号’时,它们的‘有无’和‘次数’就和数据库里的数据一样,是不可或缺的、必须被精确验证的系统状态。我们是用‘非主流写法的微小代价’,换来了‘系统可观测性可验证’的巨大收益。
(第三层,终极反杀):
当然,这个选择仅限于Service层的业务逻辑编排。我们非常清楚它在静态方法、序列化等方面的局限性。但在当前架构下,这些代价都不会被触发。这是在有明确上下文边界下的刻意的、有纪律的技术取舍,而不是一个无知的错误。

私有方法代替DTO完成业务逻辑校验,以保证架构的纯洁性

未来演进路径

  • 起点:模块化单体
  • 第一步:拆分库存并用GO重写
  • 第二步:Redis缓存
  • 第三步:MySQL主从/读写分离
  • 第四步:异步/消息队列
  • 第五步:拆分订单并重写 (Python)。刚需流式处理时用Java重写订单

引入Redis。基于当前系统、单人开发的条件,引入Redis意味着需要面对

  • 缓存Key的命名规范
  • 缓存与数据库的双写一致性
  • 缓存穿透、击穿、雪崩的防护
  • 单元测试中,Mock掉所有Redis调用
  • 集成测试中,管理Redis容器的生命周期

引入主从,这意味着需要面对

  • 读写分离的路由规则(怎么判断一条SQL该走主库还是从库)
  • 主从延迟导致的写后读不到问题
  • 从库挂掉时的降级策略

项目亮点描述(草案)

  • 不依赖框架隐式能力,手动实现Service层的“防御-异常-观测”三位一体保障体系,对数据完整性和系统可观测性实施白盒级控制
  • 精准微框架 + 显式工程纪律
  • 在AI辅助编程时代,ModM 选择了一条“反碎片化”的道路。我们不依赖框架的隐式行为来加速功能交付,而是通过一套严格的工程宪法,确保每一行由人机协作生成的代码,都具有完全的透明度与可解释性。这降低了系统的长期认知负债,使开发者始终保持对代码行为的知情权

更贴近简历项目描述的项目亮点

电商数据管道(模块化单体架构)

  • 从零设计并落地了三层架构的施工规范(CONVENTIONS.md),覆盖分层职责、事务边界、日志体系、并发控制、测试策略等全部环节
  • 实现了基于乐观锁的库存防超卖机制,通过原子化SQL + 版本号实现并发扣减的零超卖保障
  • 建立了完整的分层防御体系:Repo层原子操作、Service层事务编排、API层决策,每层边界清晰、职责单一
  • 全路径测试覆盖,每种异常分支、每次事务回滚、每条日志输出均被精确断言,测试即文档

八股收集

什么是乐观锁?和悲观锁有什么区别?

  • 乐观锁不是‘数据库技巧’,而是一种并发控制策略。在代码里,乐观锁本质是带着一个版本号去写,写的时候检查版本号是否还是之前读到的那个。悲观锁则用SELECT ... FOR UPDATE锁行。我在项目里,价格更新用乐观锁(version字段),库存扣减用条件更新(WHERE stock >= quantity),后者是乐观锁思想在业务字段上的应用。

乐观锁具体怎么实现?SQL怎么写?

  • 在SQLAlchemy里,我用Core风格写:update(table).where(version == old_version).values(version=table.c.version + 1)。关键点是版本自增必须在数据库侧做(version + 1),不能在应用层算好再传,避免并发脏读。如果rowcount == 0,我在Service 层抛一个自定义的ProductConcurrentUpdateError,而不是让数据库异常直接透出。

乐观锁的冲突检测放在哪一层?Service还是Repo?

  • 我采用显式分层。Repo层只做原子操作:UPDATE ... WHERE version = ?,冲突了返回None,不抛异常。Service层拿到None后,根据业务上下文判断为并发冲突,抛出业务异常ProductConcurrentUpdateError。这样Repo层不感知业务语义,Service层掌握事务控制权和异常决策权。测试时,分别验证Repo返回None和Service抛异常。

乐观锁有什么坑?有没有什么场景不适合用?

  • 第一个坑是审计日志带来的额外读。如果业务要求记录旧值(比如价格变更审计),乐观锁就得先SELECT再UPDATE,打开一个并发窗口。这个窗口虽然被version保护,但代码复杂度上去了。对比我们项目的库存扣减,它不需要旧值,直接用WHERE stock >= quantity一步完成,就不需要这个窗口。
  • 第二个坑是版本号必须从事实源取。我遇到过用业务字段(old_price)充当版本号的方案,它只能检测该字段的变更,其他字段被改了检测不到。后来升级到专用version字段才解决。
  • 第三个坑是重试策略。如果冲突了,是直接抛异常让用户重试,还是自旋重试?这要看业务——价格更新我们直接抛异常,因为后台管理系统操作频率低,用户手动重试即可;秒杀库存就不能这么干,得用消息队列削峰。

(反问)贵司的乐观锁在哪些场景落地?版本号是数据库字段还是分布式版本向量?冲突后的重试策略是自旋、退避还是直接抛异常?有没有遇到过审计日志驱动下‘先读后写’带来的窗口问题?

  • 由对方回答。

乐观锁具体怎么实现?SQL怎么写?

  • 在库存表增加version整数字段。
  • Repo层使用Core风格写原子化SQL:
  • UPDATE inventory SET reserved_quantity = reserved_quantity + ?, version = version + 1
  • WHERE product_id = ? AND version = ? AND quantity - reserved_quantity >= ?;
  • 关键点:版本自增在数据库侧完成(version + 1),不在应用层算好再传,避免并发脏读。
  • 如果rowcount == 0,Repo 返回None。Service层根据上下文判断为版本冲突或库存不足,分别抛出StockConcurrencyError或透传InsufficientStockError

什么是CAS(Compare And Swap)?

  • CAS是一种无锁的并发控制原子操作,是乐观锁的底层核心思想。
  • 涉及三个操作数:内存位置(要修改的数据)、期望值V_old(你认为数据应该是什么)、新值V_new(你希望更新成什么)。
  • 原子逻辑:如果当前位置的值等于V_old,则更新为V_new,返回成功;否则什么也不做,返回失败。
  • 在我们项目中的体现:
  • Compare:WHERE version = ?(检查版本是否还是旧值)
  • Swap:SET version = version + 1(更新为新版本)
  • 原子性:两个动作在同一条SQL中完成,数据库保证不被中断

如何防止库存超卖?

  • 三层联动构成“完全体”:
  • 数据库层(Repo):通过带version字段的原子化SQL,将“判断库存是否充足”和“执行扣减”合并为一个原子操作,保证基于最新数据做决策
  • 业务层(Service):检测Repo返回的影响行数。为0时区分两种情况:库存不足抛InsufficientStockError,版本冲突抛StockConcurrencyError,同时记录WARNING日志
  • 接口层(API):决定冲突处理策略。库存不足直接返回给客户端;版本冲突可自动重试或返回“系统繁忙,请重试”

库存释放操作为什么也需要乐观锁?

  • 防止基于过期库存快照执行释放,导致数据不一致
  • 更重要的是防止重复释放:超时取消任务因网络抖动或重试被执行两次时,version 乐观锁能让第二次操作因版本不匹配而静默失败,天然提供接口幂等性的雏形

并发策略

悲观锁

  • 假设冲突一定会发生,所以在动手前,先把资源锁住,不让别人碰。
  • [核心机制]
  • 数据库悲观锁 (Pessimistic Locking):使用 SELECT ... FOR UPDATE 显式锁定目标数据行。在事务提交前,其他事务无法读取或修改这些行
  • 应用级互斥锁 (Mutex/Lock):在编程语言层面(如 Python 的 threading.Lock)控制,同一时刻只有一个线程能执行某段代码
  • [适用场景]
  • 冲突激烈
  • 需保护“读-思考-写”这个漫长过程的完整性,如银行转账
  • [代价]会阻塞其他操作,降低系统吞吐量,甚至可能导致死锁

乐观锁

  • 假设冲突是小概率事件,先大胆地做,提交时再检查有没有人捣乱
  • [核心机制]
  • 数据版本控制:给数据加一个版本号或时间戳,更新时作为条件
  • 冲突检测:UPDATE ... SET version = version + 1 WHERE version = :my_read_version。如果 rowcount 为 0,说明冲突了
  • 失败处理:立即失败,还是原地“自旋”重试,或是把任务丢到消息队列延后处理
  • [适用场景]
  • 读多写少,冲突概率低
  • 不希望锁阻塞其他读操作
  • [代价]一旦冲突,操作就失败了,需要上层有合适的重试或降级策略

原子操作

  • 设计一个操作,在数据库引擎里一步到位,物理上不可分割
  • [核心机制]
  • 数据库原子更新:一条SQL搞定一切,比如 UPDATE inventory SET stock = stock - ? WHERE sku = ? AND stock >= ?
  • CPU 原子指令 (CAS):底层硬件提供的“比较并交换”指令,是很多高级并发工具的地基。应用层的“自旋”就是反复调用 CAS 指令直到成功
  • [适用场景]
  • 逻辑很简单,一个条件就能判断(比如库存还够不够)
  • [代价]只适用于简单的业务逻辑,复杂场景下很难用一条SQL表达

隔离

  • 直接不给并发机会。每个操作处理自己的那份数据,最后再把大家的成果合并起来,有效规避冲突
  • [核心机制]
  • Actor 模型:每个“Actor”有自己的私有状态,只通过收发消息通信。同一时刻只处理一条消息,天然无竞争
  • 无冲突复制数据类型 (CRDT):为分布式系统设计的数据结构,保证在无锁的情况下,多个副本最终能达成一致。比如你和一个朋友同时编辑一个文档的不同行,两人的修改都能被接受,不会冲突
  • [适用场景]
  • 分布式、去中心化系统
  • 需要极高可用性和容忍网络分区的场景(如在线协作应用)
  • [代价]编程模型复杂,不是传统的“读-改-写”范式

项目问题收集

Modular Monolith中如何处理跨域数据访问?

  • 不允许在Repo层内部直接调用其他模块的 Repo 或JOIN其他模块的表。低层模块之间禁止形成横向依赖网
  • 正确做法:在Service 层通过依赖注入持有其他模块的Repo实例,按顺序编排多个领域的Repo方法,完成跨域业务流程。例如InventoryService注入ProductRepo,先通过ProductRepo.get_by_id()确认商品存在,再通过InventoryRepo查询库存
  • 原则:依赖倒置——高层模块依赖低层模块的接口,而非低层模块之间互相依赖

不同模块定义了同名异常(如ProductNotFoundError),如何处理?

  • 每个模块在src/exceptions/ 目录下独立管理自己的异常文件(如product_exceptions.py、inventory_exceptions.py)
  • 模块内部使用简洁的类名(如ProductNotFoundError),无需前缀
  • 当两个同名异常需要在同一文件中共存时,通过 Python的as关键字在导入侧重命名以消除歧义:
  • from src.exceptions.inventory_exceptions import ProductNotFoundError as InventoryProductNotFoundError
  • 原则:每个模块拥有自己的异常定义,不直接复用其他模块的异常类。将来拆分微服务时,各模块的异常文件直接属于各自的服务代码库,无需改动

Repo层为什么允许ORM和Core混用?怎么选择?

  • ORM风格:适合简单查询 + 对象映射,如select(Product).where(...),自动水合为领域对象,代码最简洁
  • Core风格:适合复杂查询、聚合统计、批量写操作,如select(product_table)update(table).where(...),精确控制SQL,性能更好
  • text() 原生SQL:适合复杂聚合或多表子查询,是Core工具箱的一部分,可在最小必要范围内使用
  • 同一个Repo文件中,每个方法只使用一种风格,不在同一方法内混搭
  • 原则:因地制宜,为每个查询选择最合适的工具。ProductRepo已经走了这条路,InventoryRepo继承同样的实践

业务异常和操作异常的区别?分别放在哪里?

类型 定义 抛出者 存放位置 示例
业务异常 业务逻辑不允许的情况 Service层 src/exceptions/目录 ProductNotFoundError、InventoryValidationError
操作异常 无法执行操作本身 Repo 层 Repo文件内部 InsufficientStockError

  • Repo层只抛出操作异常(物理约束不满足,如库存不足无法扣减)。
  • Service层只抛出业务异常(业务规则不允许,如商品不存在)。
  • 操作异常可被Service层捕获后透传或转换为业务异常再抛出

反问环节问题收集

  • 如果进面后,诊断对方的需求是人才熟识工程治理,那么反问环节的问题应跟权力结构紧密相关,工程治理需要权限下放
  • 以下问题旨在把握团队的本质:
  • [问技术负责人]如果我发现一个核心模块的代码需要重写才能支撑下个季度的需求,但不是这个迭代的计划内,我们有什么流程来处理这种事?之前有过成功案例吗?.
  • 如果对方开始谈“灰度发布”、“风险评估”,说明有戏。如果对方打哈哈说“我们很敏捷,到时候再说”,说明没有
  • [问未来的同事]我们现在的代码,有什么地方是你们特别想改但一直没机会改的吗?
  • 如果他们眼睛放光,开始吐槽某个“传奇屎山”,那说明团队有技术追求但被压制了。如果他们一脸茫然,说“都挺好的”,那说明这个团队对混乱已经麻木了
  • [问管理者(CTO/技术总监)]这个岗位,是希望我去解决具体的、明确的技术问题,还是去定义一个尚未明确的、关于‘我们如何写出更好代码’的长期路线图?
  • 这是最关键的“生态位”确认。如果他清晰地说后者,并且能说出他现在的痛点是“系统越来越脆弱、改不动”,那么权力是可以谈的。如果他只是说“我们缺一个能干活的资深开发”,那这个位置就很危险
0%