yudaocode/SpringBoot-Labs · error · java.lang.Exception
余额不足
Error message
余额不足
What it means
Thrown by the account service of the Seata AT + OpenFeign demo (labx-17, Spring Cloud version). reduceBalance performs a conditional UPDATE that only decrements when the balance covers the price; 0 affected rows means the guard failed and the service throws a generic Exception to mark the branch as failed. Under Seata AT this exception propagates through the Feign call to the order service (TM), which marks the global transaction rollback-only so every branch's before-image is restored.
Source
Thrown at labx-17/labx-17-sc-seata-at-feign-demo/labx-17-sc-seata-at-feign-demo-account-service/src/main/java/cn/iocoder/springcloud/labx17/accountservice/service/impl/AccountServiceImpl.java:34
@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
- Reseed/top up the account table balance for the test user.
- Retry with a smaller order amount.
- Map this exception at the order-service boundary to an 'insufficient balance' business response instead of surfacing a raw 500.
Example fix
// before
int updateCount = accountDao.reduceBalance(price);
if (updateCount == 0) {
throw new Exception("余额不足");
}
// after — pass userId into the guarded SQL and throw a domain exception
int updateCount = accountDao.reduceBalance(userId, price); // WHERE user_id=? AND balance >= ?
if (updateCount == 0) {
throw new InsufficientBalanceException(userId, price);
} Defensive patterns
Strategy: validation
Validate before calling
// Order service, before starting the purchase
Integer balance = accountFeign.getBalance(userId);
if (balance == null || balance < totalPrice) {
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");
}
log.error("global tx rolled back, XID={}", RootContext.getXID(), e);
throw e;
} Prevention
- Pre-validate balance via a read RPC before opening the global transaction.
- Top up demo accounts before each run; reseed the DB.
- Keep the SQL-level balance guard; treat 0-row updates as the definitive signal.
When it happens
Trigger: Order flow invoked with order amount exceeding the user's balance such that checkBalance passed but the guarded UPDATE matched 0 rows (e.g. concurrent deductions), or directly calling account-service's reduceBalance with price > balance.
Common situations: Shared demo DB drained by earlier runs; two concurrent orders for the same user both passing the pre-check and the second getting 0 update count; amount/price unit mismatch between order and account services.
Related errors
AI-assisted analysis of yudaocode/SpringBoot-Labs@6c12efaed0 (2026-08-14).
Data as JSON: /api/errors/3d7badd1baf12913.
Report an issue: GitHub.