yudaocode/SpringBoot-Labs · error · java.lang.Exception

余额不足

Error message

余额不足

What it means

Thrown by the account service in a Seata AT distributed-transaction demo (lab-53, Dubbo version). After a guarded UPDATE (`UPDATE account SET balance = balance - ? WHERE balance >= ?` style) the DAO returns 0 affected rows, meaning the WHERE clause matched nothing, so the service throws a plain Exception to signal insufficient balance. Because the method runs inside a Seata global transaction, this exception propagates to the TM (order service) and triggers a global rollback of all branch transactions (order, product, account).

Source

Thrown at lab-53/lab-53-seata-at-dubbo-demo/lab-53-seata-at-dubbo-demo-account-service/src/main/java/cn/iocoder/springboot/lab53/accountservice/service/AccountServiceImpl.java:33

    @Autowired
    private AccountDao accountDao;

    @Override
    @Transactional // 开启新事物
    public void reduceBalance(Long userId, Integer price) throws Exception {
        logger.info("[reduceBalance] 当前 XID: {}", RootContext.getXID());

        // 检查余额
        checkBalance(userId, price);

        logger.info("[reduceBalance] 开始扣减用户 {} 余额", userId);
        // 扣除余额
        int updateCount = accountDao.reduceBalance(price);
        // 扣除成功
        if (updateCount == 0) {
            logger.warn("[reduceBalance] 扣除用户 {} 余额失败", userId);
            throw new Exception("余额不足");
        }
        logger.info("[reduceBalance] 扣除用户 {} 余额成功", userId);
    }

    private void checkBalance(Long userId, Integer price) throws Exception {
        logger.info("[checkBalance] 检查用户 {} 余额", userId);
        Integer balance = accountDao.getBalance(userId);
        if (balance < price) {
            logger.warn("[checkBalance] 用户 {} 余额不足,当前余额:{}", userId, balance);
            throw new Exception("余额不足");
        }
    }

}

View on GitHub (pinned to 6c12efaed0)

Solutions

  1. Reset the demo data: re-run the SQL seed script so the test user's balance covers the order price (e.g. `UPDATE account SET balance = 100000 WHERE id = 1`).
  2. Lower the purchase amount in the order request so it is within the current balance.
  3. If concurrency is real, keep the guarded UPDATE (it is correct) and treat this exception as the business signal — catch it at the TM (order) side and return a friendly 'insufficient balance' response instead of letting it bubble as a 500.

Example fix

// before
int updateCount = accountDao.reduceBalance(price);
if (updateCount == 0) {
    throw new Exception("余额不足");
}

// after — keep the guarded update, but use a business exception the caller can branch on
int updateCount = accountDao.reduceBalance(userId, price); // guard in SQL: WHERE user_id = ? AND balance >= ?
if (updateCount == 0) {
    throw new InsufficientBalanceException(userId, price); // custom RuntimeException with code
Defensive patterns

Strategy: validation

Validate before calling

// Before placing the order, read the balance and compare
Integer balance = accountApi.getBalance(userId);
if (balance == null || balance < orderPrice) {
    return Result.error("余额不足,当前余额:" + balance);
}
// then call orderApi.create(...)

Try / catch

// Order service (TM) — catch business failure, Seata rolls back automatically
try {
    orderService.create(userId, productId, amount);
} catch (InsufficientBalanceException e) {
    return ResponseEntity.badRequest().body("Insufficient balance: " + e.getMessage());
} catch (Exception e) {
    // unexpected failure — global tx rolls back; log with XID for tracing
    log.error("order failed, XID={}", RootContext.getXID(), e);
    return ResponseEntity.status(500).body("order failed");
}

Prevention

When it happens

Trigger: Calling the order creation flow when the test user's account balance is smaller than the order price; the pre-check in checkBalance may pass, but a concurrent deduction (or a stale read) makes the conditional UPDATE affect 0 rows, hitting `updateCount == 0` at line 33.

Common situations: Demo/test data was consumed by previous runs and the account table was never reseeded; concurrent test requests race between the balance check and the UPDATE; the price computed by the order service is larger than expected (e.g. amount * price overflow or wrong unit).

Related errors


AI-assisted analysis of yudaocode/SpringBoot-Labs@6c12efaed0 (2026-08-14). Data as JSON: /api/errors/521f2d1258ccc671. Report an issue: GitHub.