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
- 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`).
- Lower the purchase amount in the order request so it is within the current balance.
- 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
- Seed demo accounts with a large balance before each test run.
- Keep the conditional UPDATE guard in SQL — it is the concurrency-safe source of truth, not the pre-check.
- Reset the lab database (re-run SQL scripts) between tutorial sessions.
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.