How to Prevent Coupon Over‑Issuing in High‑Concurrency Scenarios
The article analyzes why a coupon‑distribution feature can issue more coupons than available under heavy load, explains the root cause of concurrent stock deductions, and presents four practical solutions—including Java synchronized blocks, SQL row‑level locking, Redis distributed locks, and Redisson—to reliably prevent over‑issuance.
Problem Description
In a recent project we implemented a coupon‑receiving feature where each coupon has a total issuance quantity and a per‑user limit. When a user successfully claims a coupon, a record is written to a separate table (Table B).
Under high concurrency the stock can become negative, leading to over‑issuance.
Root Cause
When two threads simultaneously pass the availability check, both may decrement the stock, causing the stock to drop below zero.
Solutions
Solution 1 – Java synchronized
Wrap the entire coupon‑claiming logic in a synchronized(this) block to ensure only one thread executes the method at a time.
synchronized (this) {
// business logic
// check coupon, create record, reduce stock, insert record
}Drawbacks: works only within a single JVM, introduces serialization, and can become a bottleneck in a clustered environment.
Solution 2 – SQL row‑level lock
Update the stock with a condition that stock > 0, letting InnoDB lock the row.
UPDATE coupon
SET stock = stock - 1
WHERE id = #{coupon_id} AND stock > 0;Alternatively use an optimistic‑lock pattern with a version column to avoid ABA problems.
UPDATE product
SET stock = stock - 1, version = version + 1
WHERE id = 1 AND stock > 0 AND version = #{last_version};Solution 3 – Redis distributed lock (SETNX)
Use SETNX to acquire a lock key, set an expiration, execute the business logic, then delete the key. To avoid accidental deletion, store the thread ID as the lock value and verify it before deletion.
String key = "lock:coupon:" + couponId;
String threadId = Thread.currentThread().getId() + "";
if (redis.setnx(key, threadId)) {
redis.expire(key, 30, TimeUnit.MILLISECONDS);
try {
// business logic
} finally {
if (redis.get(key).equals(threadId)) {
redis.del(key);
}
}
} else {
// retry or spin
}Use a Lua script for atomic check‑and‑delete:
if redis.call('get', KEYS[1]) == ARGV[1] then
return redis.call('del', KEYS[1])
else
return 0
endSolution 4 – Redisson client
Configure Redisson and obtain an RLock for the coupon key. Redisson’s watchdog automatically renews the lock lease, eliminating manual expiration handling.
@Configuration
public class AppConfig {
@Value("${spring.redis.host}") private String redisHost;
@Value("${spring.redis.port}") private String redisPort;
@Bean
public RedissonClient redisson() {
Config config = new Config();
config.useSingleServer().setAddress("redis://" + redisHost + ":" + redisPort);
return Redisson.create(config);
}
}
public JsonData addCoupon(long couponId, CouponCategoryEnum category) {
String key = "lock:coupon:" + couponId;
RLock lock = redisson.getLock(key);
lock.lock();
try {
// business logic
} finally {
lock.unlock();
}
return JsonData.buildSuccess();
}All four approaches can prevent coupon over‑issuance; the choice depends on the system architecture and performance requirements.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Java Backend Technology
Focus on Java-related technologies: SSM, Spring ecosystem, microservices, MySQL, MyCat, clustering, distributed systems, middleware, Linux, networking, multithreading. Occasionally cover DevOps tools like Jenkins, Nexus, Docker, and ELK. Also share technical insights from time to time, committed to Java full-stack development!
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.
