You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Data JPA并发测试:PostgreSQL竞态条件与ACID默认行为探究

问题:Data JPA + PostgreSQL 并发转账的竞态条件行为不符合预期

我想深入了解Data JPA与PostgreSQL在竞态条件和ACID特性方面的默认行为,于是搭建了账户表、JPA实体以及处理转账的AccountService。先通过同步测试验证了环境没问题,接着写了并发测试——我知道PostgreSQL默认隔离级别是READ COMMITTED,只能防止脏读,本以为会出现金额异常(比如凭空增加),但实际测试结果是只有1笔转账生效,最终两个账户余额都是50,和预期的失败情况不一样。

我用Docker部署了最新版PostgreSQL做集成测试环境,相关代码如下:

@Service
public class AccountService {
    @Autowired
    private AccountRepository accountRepository;

    @Transactional
    public void deposit(int from, int to, int amount) {
        Account f = accountRepository.findByOwner(from).orElseThrow(RuntimeException::new);
        Account t = accountRepository.findByOwner(to).orElseThrow(RuntimeException::new);
        if (f.getBalance() >= amount) {
            f.setBalance(f.getBalance() - amount);
            t.setBalance(t.getBalance() + amount);
            accountRepository.save(f);
            accountRepository.save(t);
        }
    }
}
@DataJpaTest
@Import(AccountService.class)
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
public class ConcurrencyTest {
    @Autowired
    private AccountRepository accountRepository;

    @Autowired
    private AccountService accountService;


    @BeforeEach
    void setUp() {
        Account from = Account
                .builder()
                .owner(1)
                .balance(100)
                .build();

        Account to = Account
                .builder()
                .owner(2)
                .balance(0)
                .build();

        accountRepository.save(from);
        accountRepository.save(to);
    }

    @Test
    public void testSync() {
        accountService.deposit(1, 2, 50);

        Account f = accountRepository.findByOwner(1).get();
        Account t = accountRepository.findByOwner(2).get();
        assertEquals(50, f.getBalance());
        assertEquals(50, t.getBalance());
    }

    @Test
    public void concurrencyTest() throws InterruptedException {
        int NUMBER_OF_THREADS = 5;
        ExecutorService executorService = Executors.newFixedThreadPool(NUMBER_OF_THREADS);
        CountDownLatch countDownLatch = new CountDownLatch(NUMBER_OF_THREADS);
        for (int i = 0; i < NUMBER_OF_THREADS; i++) {
            executorService.submit(() -> {
                try {
                    accountService.deposit(1, 2, 50);
                } catch (Exception e) {
                    e.printStackTrace();
                } finally {
                    countDownLatch.countDown();
                }
            });
        }

        countDownLatch.await();
        executorService.shutdown();


        Account f = accountRepository.findByOwner(1).get();
        Account t = accountRepository.findByOwner(2).get();
        assertEquals(0, f.getBalance()); // 实际结果为50
        assertEquals(100, t.getBalance()); // 实际结果为50

    }
}

内容的提问来源于stack exchange,提问作者Andrin F.

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.13 23:09:59