Java Synchronisation面试场景:多并发银行交易防余额透支方案问询
面试官给的这个场景简直是分布式并发控制的经典考题啊!我当初面试也遇到过类似的,一开始提
synchronized被怼了,后来才明白为啥这个方案在真实银行场景里站不住脚。咱们一步步拆解:
为啥单纯的对象synchronization不被认可?
- 单JVM局限:银行系统肯定是分布式多节点部署的,
synchronized只能锁住当前JVM里的账户对象,其他节点的交易根本感知不到这个锁,照样能同时修改余额,透支问题还是会出现。 - 性能瓶颈:如果给整个账户对象加锁,所有针对这个账户的交易都得串行执行,银行每天几百万笔交易,这种粒度的锁会直接把系统拖垮。
- 事务一致性缺失:就算在单JVM里,
synchronized只能保证内存中操作的原子性,但如果余额存在数据库里,内存改了但数据库没提交,或者数据库提交失败,还是会出现数据不一致的情况,锁本身解决不了持久化层面的问题。
适合银行场景的管控方案
下面这些方案都是真实金融系统里常用的,面试官肯定更想听这些:
1. 数据库层面的锁机制
悲观锁(适合低并发、对一致性要求极高的场景)
直接在查询余额时锁定账户行,确保同一时间只有一个交易能修改:
SELECT balance FROM accounts WHERE id = ? FOR UPDATE; -- 检查余额足够后,执行扣减 UPDATE accounts SET balance = balance - ? WHERE id = ?;
这个锁会一直持有到事务结束,其他操作必须等待,能彻底杜绝并发修改,但高并发下可能导致锁等待队列过长。
乐观锁(高并发场景首选)
给账户表加一个version字段(或者用余额本身作为条件),更新时做校验:
UPDATE accounts SET balance = balance - ?, version = version + 1 WHERE id = ? AND version = ? AND balance >= ?;
如果更新返回的行数为0,说明有并发冲突(要么版本不对,要么余额不够),这时候可以选择重试交易,或者直接拒绝。乐观锁不会锁住数据,性能更好,适合银行这种高并发场景。
2. 分布式锁(多节点部署必备)
如果系统是分布式的,就得用跨节点的锁来锁定账户ID,比如基于Redis的Redisson、ZooKeeper实现的分布式锁:
- 交易发起时,先获取对应账户ID的分布式锁
- 拿到锁后再执行余额校验、扣减操作
- 操作完成后释放锁,注意设置合理的超时时间,避免锁泄漏
3. 预扣额度机制
很多银行会把账户余额分成可用余额和预扣余额:
- 交易发起时,先校验可用余额是否足够,足够就把对应金额从可用余额转到预扣余额
- 完成交易的后续流程(比如网购的商家确认、支票的审核通过)后,再从预扣余额里扣除,正式完成交易
- 如果交易失败(比如商家取消订单),就把预扣额度转回可用余额
这种方式能避免多个交易同时看到“可用余额足够”而重复扣减,同时也能处理交易中间状态的一致性问题。
4. 事务+锁的组合拳
把整个交易流程(查询余额、校验、扣减)放在一个数据库事务里,配合上面的锁机制,确保整个操作的原子性——要么全部成功,要么全部回滚,不会出现中间状态。比如用Spring的@Transactional注解包裹业务逻辑,再结合乐观锁或悲观锁,双重保障。
总结一下,面试官想要的不是“单进程里的同步”,而是能应对分布式、高并发、持久化场景的工业级解决方案,毕竟银行系统的资金安全容不得半点马虎。
内容的提问来源于stack exchange,提问作者ghostrider
相关产品推荐
相关产品推荐

