Preventing Coupon Over‑Issuance in High‑Concurrency Java Applications

This article examines why coupon stock can become negative under concurrent requests and presents four practical solutions—including Java synchronized blocks, SQL conditional updates, Redis distributed locks, and Redisson client usage—to reliably prevent coupon over‑issuance in high‑traffic systems.

Java High-Performance Architecture
Java High-Performance Architecture
Java High-Performance Architecture
Preventing Coupon Over‑Issuance in High‑Concurrency Java Applications

Problem Statement

In a recent project we need a coupon‑claim feature. Each coupon has a total stock and a per‑user limit. When a user successfully claims a coupon we write a record to another table (table B).

Under load the stock can become negative, indicating over‑issuance.

Root Cause

When two threads check the stock simultaneously, both may pass the validation and then both decrement the stock, causing the stock to go below zero.

Solution 1 – Java synchronized

Wrap the whole claim logic in a synchronized(this) block so only one thread can execute it in a single JVM.

synchronized (this){
    LoginUser loginUser = LoginInterceptor.threadLocal.get();
    CouponDO couponDO = couponMapper.selectOne(new QueryWrapper<CouponDO>()
            .eq("id", couponId)
            .eq("category", categoryEnum.name()));
    if(couponDO == null){
        throw new BizException(BizCodeEnum.COUPON_NO_EXITS);
    }
    this.checkCoupon(couponDO, loginUser.getId());
    // build coupon record
    CouponRecordDO couponRecordDO = new CouponRecordDO();
    BeanUtils.copyProperties(couponDO, couponRecordDO);
    couponRecordDO.setCreateTime(new Date());
    couponRecordDO.setUseState(CouponStateEnum.NEW.name());
    couponRecordDO.setUserId(loginUser.getId());
    couponRecordDO.setUserName(loginUser.getName());
    couponRecordDO.setCouponId(couponDO.getId());
    couponRecordDO.setId(null);
    int row = couponMapper.reduceStock(couponId);
    if(row == 1){
        couponRecordMapper.insert(couponRecordDO);
    }else{
        log.info("发送优惠券失败:{},用户:{}", couponDO, loginUser);
    }
}

This passes JMeter stress testing, but it is unsuitable for clustered deployments because the lock is JVM‑local and creates a serialization bottleneck.

synchronized only works on a single JVM instance.

It introduces thread contention, causing other requests to wait.

Solution 2 – SQL‑Level Protection

Update the stock only when it is greater than zero.

update coupon set stock = stock - 1 where id = #{coupon_id} and stock > 0

MySQL InnoDB locks the row during the update, preventing concurrent modifications.

Alternatively, use an optimistic‑lock style update with a version column to avoid ABA problems.

update product set stock = stock - 1 where stock = #{previous_stock} and id = 1 and stock > 0
update product set stock = stock - 1, version = version + 1 where id = 1 and stock > 0 and version = #{previous_version}

Solution 3 – Redis Distributed Lock

Use Redis SETNX to acquire a lock before touching the database, set an expiration, and release the lock in a finally block.

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

To avoid deleting a lock owned by another thread, store the thread identifier and check it before deletion, or execute the check‑and‑delete atomically with a Lua script.

String key = "lock:coupon:" + couponId;
String threadId = Thread.currentThread().getId();
try{
    if(setnx(key, threadId)){
        exp(key, 30, TimeUnit.MILLISECONDS);
        try{
            // business logic
        }finally{
            if(get(key).equals(threadId)){
                del(key);
            }
        }
    }else{
        // retry
    }
}
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 Client

Redisson provides a high‑level API that handles lock acquisition, automatic watchdog renewal, and safe release, working across a cluster.

@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 rLock = redisson.getLock(key);
    LoginUser loginUser = LoginInterceptor.threadLocal.get();
    rLock.lock();
    try{
        // business logic
    }finally{
        rLock.unlock();
    }
    return JsonData.buildSuccess();
}

Redisson’s watchdog automatically extends the lock’s TTL, eliminating manual expiration handling.

These approaches collectively ensure that coupon stock never drops below zero, even under heavy concurrent traffic.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

JavaSQLconcurrencyredisdistributed-lockredissonCoupon
Java High-Performance Architecture
Written by

Java High-Performance Architecture

Sharing Java development articles and resources, including SSM architecture and the Spring ecosystem (Spring Boot, Spring Cloud, MyBatis, Dubbo, Docker), Zookeeper, Redis, architecture design, microservices, message queues, Git, etc.

0 followers
Reader feedback

How this landed with the community

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.