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

余额不足

Error message

余额不足

What it means

Thrown by the account service of labx-17's Seata AT + Dubbo variant. reduceBalance executes a guarded UPDATE that decrements only when balance suffices; a 0 affected-row count means the WHERE guard rejected the deduction and the service throws Exception('余额不足') at line 33. Within the Seata global transaction this exception propagates over Dubbo to the order service and triggers a global rollback.

Source

Thrown at labx-17/labx-17-sca-seata-at-dubbo-demo/labx-17-sca-seata-at-dubbo-demo-account-service/src/main/java/cn/iocoder/springcloudalibaba/labx17/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. Top up / reset the account balance for the test user.
  2. Retry with a lower order amount.
  3. Catch this at the order service and convert to a user-facing business error, keeping Seata's rollback semantics intact.

Example fix

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

// after — guarded SQL keyed by user, domain exception
int updateCount = accountDao.reduceBalance(userId, price);
if (updateCount == 0) {
    throw new InsufficientBalanceException(userId, price);
}
Defensive patterns

Strategy: validation

Validate before calling

// Dubbo generic/pre-check before ordering
Integer balance = accountService.getBalance(userId); // if exposed
if (balance == null || balance < orderAmount) {
    return Result.error("余额不足");
}

Try / catch

try {
    orderService.create(userId, productId, amount);
} catch (Exception e) {
    if (e.getMessage() != null && e.getMessage().contains("余额不足")) {
        return ResponseEntity.badRequest().body("insufficient balance");
    }
    throw e; // Seata TM handles rollback
}

Prevention

When it happens

Trigger: Order price exceeds the user's remaining balance at UPDATE time — either the pre-check raced with a concurrent deduction, or the balance is simply below the price and the pre-check path (line 43) was bypassed/reordered.

Common situations: Demo account data not reseeded after earlier runs; parallel requests draining the same account; price calculation differences between order and account services.

Related errors


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