审查 Java 转账代码,识别并发与原子性问题。
未标记
考查重点:条件扣款、事务与重复转账。检查余额条件扣减与入账是否同事务,重复请求是否幂等,多账户锁顺序是否一致;用并发交错和失败回滚验证余额与流水是否一致
本题目录
参考答案
核心回答
先看代码里余额检查与扣款是否构成可靠的并发控制,扣款与入账是否同成同败,以及重复请求会不会再次转账。没有给出具体代码时,只能说明审查路径,不能断言某一行一定有漏洞。
并发:读到余额足够,不等于扣款时仍足够
以下是独立的数值推演,不是原题代码:余额 100,两个请求各转出 80。若两者都先读到 100、分别算出剩余 20,再各自把余额写成 20,同时给两个收款方各加 80,就发生了扣款覆盖:最终付款方是 20,收款方却一共增加 160。
一种处理是由数据库执行带余额条件的扣减,并检查受影响行数:
UPDATE account
SET balance = balance - :amount
WHERE id = :from_id AND balance >= :amount;
这里还需要先校验金额为正、账户和权限合法,并保证金额表示准确;受影响行数不是 1 时,不能继续入账。也可以在事务内锁定相关账户后检查和更新,涉及多个账户时保持一致的加锁顺序,降低死锁风险。单个 Java 进程里的 synchronized 不能自动保护其他服务实例。
原子性:不能只扣款成功
在同一事务资源内,扣款、收款方入账、转账记录与幂等记录应按业务设计一起提交或回滚。审查 Spring 事务时,核对是否真的经过事务代理、所用事务管理器和回滚规则是否覆盖失败;只看到 @Transactional 不足以证明生效。默认代理模式下的同类内部调用尤其需要检查。
若两个账户跨库,或还要调用外部支付服务,本地事务不能自动包含全部动作,需要另行设计跨系统一致性与重试流程。
重复请求与验证
用稳定的业务请求标识和唯一约束识别重复转账,并检查相同标识是否对应相同参数;不能每次重试都换一个新标识。已完成请求返回已有结果,不能再扣一次。
测试应覆盖两个并发扣款、相反方向转账、重复提交、扣款后入账前失败及响应丢失后重试。检查余额与流水等业务不变量,并在实际数据库与事务配置下验证锁和回滚。上面的条件扣款只解决一部分并发检查,不能单独当作完整转账实现。
参考资料
- 国内:JavaGuide:接口幂等性设计:采用稳定业务标识、唯一约束与业务事务共同约束重复执行。
- 国内:JavaGuide:乐观锁和悲观锁:借鉴独占锁和加锁顺序的解释,检查并发保护覆盖的资源与实例范围。
- 国内:JavaGuide:MySQL 事务隔离级别:借鉴多事务交错的分析方式,区分普通读取与后续写入保护。
- 国内:JavaGuide:Spring 事务详解:采用扣款与入账同成同败的事务边界,并检查回滚条件。
- 国外:MySQL 8.4:Locking Reads:依据锁定读与普通
SELECT的区别,解释事务内的并发保护和提交、回滚时的锁释放。 - 国外:Spring Framework:Using @Transactional:依据代理调用与运行时事务配置的条件,解释只看到注解为什么不能证明事务生效。
相关面经
1 篇字节跳动
后端开发工程师审查 Java 转账代码,有什么并发与原子性问题?
