Java如何实现并发?银行转账并发余额更新错误如何处理?
银行转账并发更新丢失问题解决方案
问题根因
该场景属于典型的丢失更新并发问题:两个并发事务先读取同一版本的账户余额,在业务层完成数值计算后各自写回数据库,后提交的事务会直接覆盖先提交事务的更新,最终只生效了一次转账操作。
场景处理方案
数据库层面方案
- 原子更新SQL:不要将余额查询到业务层做计算后再写回,直接在SQL层面完成累加操作,示例SQL:
UPDATE account SET balance = balance + 100 WHERE user_id = 'B'。数据库的UPDATE操作本身自带原子性和行锁,是这类数值累加场景最高效、最常用的解决方案,完全避免了业务层计算的并发风险。 - 悲观行锁:查询账户余额时主动加排他锁,阻塞其他并发查询,示例SQL:
SELECT balance FROM account WHERE user_id = 'B' FOR UPDATE。只要第一个事务拿到B账户的行锁,其他查询B余额的事务就会进入阻塞状态,直到前一个事务提交释放锁,后续查询就能拿到更新后的余额,不会出现重复计算问题,适合并发量不高、一致性要求极高的金融场景。 - 乐观锁:给账户表新增
version版本号字段,每次更新时校验版本号,示例更新SQL:UPDATE account SET balance = balance + 100, version = version + 1 WHERE user_id = 'B' AND version = 【此前查询到的版本号】。如果更新返回的受影响行数为0,说明期间有其他事务修改了该账户数据,直接触发重试即可。性能优于悲观锁,适合并发量中等的场景。如果不想新增字段,也可以用余额本身作为校验条件,不过存在ABA问题风险。
分布式架构方案
如果是跨服务、跨数据库的分布式转账场景,可以引入分布式锁:以B账户的唯一标识作为锁Key,同一时间只有一个拿到锁的请求可以操作B账户的余额,操作完成后释放锁即可。常用的实现有Redis分布式锁、ZooKeeper分布式锁。
Java 实现并发控制的常用方案
分为单进程内并发控制和分布式场景并发控制两类:
单进程内并发控制方案
synchronized关键字:JVM内置的互斥锁,可修饰方法或代码块,保证同一时间只有一个线程执行被修饰的逻辑,使用简单无需手动释放锁,适合基础的进程内并发控制场景。java.util.concurrent.locks包下的锁工具:包括ReentrantLock可重入锁、ReentrantReadWriteLock读写锁、StampedLock等,比synchronized更灵活,支持超时获取锁、中断响应、读写分离等高级特性,适合复杂的进程内并发场景。- 原子类:
java.util.concurrent.atomic包下的AtomicInteger、AtomicLong、LongAdder等,基于CAS(比较并交换)实现无锁原子操作,性能远高于加锁方案,适合高并发下的数值计数、累加场景。 - 并发容器:包括
ConcurrentHashMap、CopyOnWriteArrayList、ArrayBlockingQueue等,这些容器自带并发安全封装,无需手动加锁即可直接使用,适合多线程下的数据存储、队列消费等场景。
分布式场景并发控制方案
- Redis分布式锁:通过
SET key value NX EX 过期时间命令实现,性能高、接入简单,是当前分布式系统最常用的分布式锁方案,也可以结合Redisson框架实现可重入锁、公平锁、读写锁等高级特性。 - ZooKeeper分布式锁:通过临时有序节点实现,天然支持锁自动释放、阻塞等待等特性,一致性高于Redis锁,适合对一致性要求极高、并发量不大的场景。
- 数据库锁:即前面提到的悲观锁、乐观锁、原子SQL等方案,只要是操作同一个数据库的场景都可使用。
内容的提问来源于stack exchange,提问作者Vaishnavi Sabnawis
相关产品推荐
相关产品推荐

