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

  1. Reseed/top up the account table balance for the test user.
  2. Retry with a smaller order amount.
  3. 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

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.