Code Review of Coupon Claim and Approval Workflow: Issues and Optimization Strategies
This article reviews Java Spring code for a coupon claim and approval workflow, identifies concurrency and transaction issues such as missing locks and inconsistent update order, and proposes optimizations including user‑level locking, simplifying lock management, and aligning transactional update sequences to prevent deadlocks.
In this technical review, the author examines a Java Spring implementation for a coupon claim feature and an approval business process, highlighting several concurrency and transactional problems. The discussion begins with a festive preface and proceeds to the problematic code sections, including the controller and service layers.
Original Controller Logic
@RedisRateLimiter(value = 200, limit = 1)
@PostMapping(value = "/claim")
public Object claim(@RequestBody EquityClaimReqEx claimReqEx) {
// 1. User validation
Result
result = claimService.check(claimReqEx);
if (!result.isSuccess()) {
return result;
}
// 2. Claim coupon – separate transaction
Result
claimCore = claimService.claimTran(claimReqEx);
if (!claimCore.isSuccess()) {
return claimCore;
}
// 3. Record claim details
return claimService.record(claimReqEx);
}The controller lacks any locking mechanism, which may lead to duplicate claims under concurrent requests.
Original Service Core Logic
@Transactional(propagation = Propagation.REQUIRED, isolation = Isolation.READ_COMMITTED, rollbackFor = RuntimeException.class)
public Result
claimCoreTransactional2(ActivityEquityClaimReqEx claimReq) {
long startTimeCore = System.currentTimeMillis();
// Step 1: Randomly fetch an unissued coupon code
EquityGoods equityGoods = equityGoodsDao.findByClaim(claimReq.getActivityId(), claimReq.getEquityId());
if (equityGoods == null) {
return Result.error("权益商品异常:无可领取权益商品!");
}
// Step 2: Acquire lock on the coupon code using Redis setnx
Boolean acquired = redisTemplate.opsForValue().setIfAbsent(equityGoods.getRedeemCode(), "Lock", Duration.ofSeconds(60 * 5));
if (acquired) {
try {
// Step 3: Lock inventory via custom lock manager
ClainReentrantLock claimLock = claimLockManager.getLockForClaim(getLockKey(claimReq), 60 * 24, TimeUnit.MINUTES);
claimLock.lock();
// Step 4: Query and decrement inventory
Inventory inventory = inventoryDao.findByconfirm(claimReq);
boolean isOk = inventoryDao.reduceInventory(inventory.getId(), isCompleted);
if (!isOk) {
throw new RuntimeException("更新配置权益核销状态失败!");
}
// Step 5: Update coupon status and user record
Boolean ret = updateUserRecordAndRedeemCode();
if (!ret) {
throw new RuntimeException("对不起,系统繁忙,稍后再试试吧!");
}
} finally {
claimLock.unlock();
redisTemplate.delete(equityGoods.getRedeemCode());
long endTimeCore = System.currentTimeMillis();
logger.info("================领取逻辑耗时测试. Timed :{}", endTimeCore - startTimeCore);
}
return Result.ok();
} else {
return Result.error("对不起,系统繁忙,稍后再试试吧!");
}
}The service layer mixes custom lock management with database transactions, leading to potential deadlocks and race conditions, especially because the lock is released before the transaction commits.
Identified Problems
The rate‑limiter annotation implementation is flawed.
The controller performs user validation without any locking, risking duplicate claims.
Using limit 1 in SQL to fetch a coupon can cause many users to receive the same code.
Custom lock utilities are hard to maintain and may be released prematurely.
Nested try blocks increase complexity and error‑handling difficulty.
Locks inside a transaction can be released before the transaction finishes, creating concurrency gaps.
Optimization Proposals for Coupon Claim
Apply a user‑level lock in the controller (e.g., using Redisson) and fail fast if the lock cannot be acquired.
Rely on the database's row‑level locking for inventory decrement and coupon status updates, removing custom lock code.
Fetch coupon codes from a thread‑safe queue or Redis set instead of using limit 1 queries.
Ensure the update order of tables is consistent across all transactional operations to avoid deadlocks.
Revised controller snippet:
@RedisRateLimiter(value = 200, limit = 1)
@PostMapping(value = "/claim")
public Object claim(@RequestBody EquityClaimReqEx claimReqEx) {
Boolean lock = redissonLockClient.tryLock(RedisKeys.COUPON_RECEIVE_LOCK + user.getId(), 20);
if (!lock) {
return "服务繁忙.....";
}
try {
Result
result = claimService.check(claimReqEx);
if (!result.isSuccess()) {
return result;
}
Result
claimCore = claimService.claimTran(claimReqEx);
if (!claimCore.isSuccess()) {
return claimCore;
}
return claimService.record(claimReqEx);
} finally {
redissonLockClient.unlock(RedisKeys.COUPON_RECEIVE_LOCK + user.getId());
}
}Approval Business Review
The approval workflow consists of three operations: submit approval, approve, and cancel approval. The original code updates the log table first and then the main table during approval, while the cancel operation updates the main table before the log table, leading to inconsistent lock acquisition order.
Potential deadlock scenario:
Two concurrent threads invoke approve and approveCancel on the same business record.
Each thread holds a lock on a different table and waits for the other, causing a deadlock.
Optimization for Approval Workflow
Standardize the update order across all transactional methods (e.g., always update the main table first, then the log table) to keep lock acquisition consistent.
Consider reducing the scope of transactions or using optimistic locking where appropriate.
Conclusion
The article presented two case studies—coupon claim and approval/cancel business—demonstrating that improper lock usage and inconsistent transactional update sequences can cause concurrency bugs and deadlocks. By applying user‑level locking, simplifying lock management, and aligning update orders, the reliability and performance of backend services can be significantly improved.
Rare Earth Juejin Tech Community
Juejin, a tech community that helps developers grow.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.