Backend Development 11 min read

Preventing Coupon Over‑Issuance in High‑Concurrency Scenarios: Java Locks, SQL Constraints, and Redis Distributed Locks

This article analyzes the coupon over‑issuance issue caused by concurrent requests, demonstrates how simple SQL updates can fail under load, and presents four solutions—including Java synchronized blocks, SQL row‑level locking, optimistic locking, and Redis‑based distributed locks with Redisson—to ensure atomic coupon allocation.

IT Architects Alliance
IT Architects Alliance
IT Architects Alliance
Preventing Coupon Over‑Issuance in High‑Concurrency Scenarios: Java Locks, SQL Constraints, and Redis Distributed Locks

In a recent project a coupon‑claiming feature exhibited over‑issuance when subjected to high concurrency. Each coupon has a total stock (e.g., 120) and a per‑user limit, but under load the stock counter could become negative, indicating that more coupons were issued than available.

The root cause is a race condition: multiple threads pass the availability check simultaneously and then decrement the stock, allowing the stock to drop below zero.

Solution 1 – Java synchronized lock

Wrap the entire coupon‑claiming method in a synchronized(this) block so that only one thread can execute the critical section at a time.

synchronized (this) {
    // business logic: check coupon, create record, reduce stock
    int row = couponMapper.reduceStock(couponId);
    if (row == 1) {
        couponRecordMapper.insert(couponRecordDO);
    } else {
        log.info("发送优惠券失败:{},用户:{}", couponDO, loginUser);
    }
}

This works for a single‑JVM deployment but fails in a clustered environment and can cause thread contention.

Solution 2 – SQL level protection

Use an UPDATE statement that only decrements stock when the current stock is greater than zero, leveraging InnoDB row‑level locking.

<update id="reduceStock">
    UPDATE coupon SET stock = stock - 1 
    WHERE id = #{coupon_id} AND stock > 0;
</update>

Alternatively, apply optimistic locking by checking the previous stock (or a version column) in the WHERE clause to avoid the ABA problem.

<update id="reduceStock">
    UPDATE product SET stock = stock - 1, version = version + 1 
    WHERE id = 1 AND stock > 0 AND version = #{lastVersion};
</update>

Solution 3 – Redis distributed lock (setnx)

Implement a lock using Redis SETNX so that only one instance can modify the coupon stock at a time.

String key = "lock:coupon:" + couponId;
if (setnx(key, "1")) {
    exp(key, 30, TimeUnit.MILLISECONDS); // set expiration
    try {
        // business logic
    } finally {
        del(key);
    }
} else {
    // retry or spin
}

Store the thread ID (or a UUID) as the lock value and delete the key only if the stored value matches, preventing accidental unlocks.

String threadId = Thread.currentThread().getId();
if (setnx(key, threadId)) {
    exp(key, 30, TimeUnit.MILLISECONDS);
    try {
        // business logic
    } finally {
        if (get(key).equals(threadId)) {
            del(key);
        }
    }
}

For atomic check‑and‑delete, use a Lua script:

String script = "if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end";
redisTemplate.execute(new DefaultRedisScript<>(script, Integer.class), Arrays.asList(key), threadId);

Solution 4 – Redisson (Redis recommended lock)

Configure Redisson and obtain a distributed lock via RLock . Redisson’s watchdog automatically extends 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 categoryEnum){
    String key = "lock:coupon:" + couponId;
    RLock lock = redisson.getLock(key);
    lock.lock();
    try {
        // business logic
    } finally {
        lock.unlock();
    }
    return JsonData.buildSuccess();
}

This approach works across multiple JVMs, handles lock renewal automatically, and avoids the pitfalls of manual SETNX implementations.

All four methods can prevent coupon over‑issuance; the choice depends on the system architecture, performance requirements, and deployment topology.

JavaSQLconcurrencyRedisdistributed lockcoupon
IT Architects Alliance
Written by

IT Architects Alliance

Discussion and exchange on system, internet, large‑scale distributed, high‑availability, and high‑performance architectures, as well as big data, machine learning, AI, and architecture adjustments with internet technologies. Includes real‑world large‑scale architecture case studies. Open to architects who have ideas and enjoy sharing.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.