Coupon Seckill

Global ID Generator: Redis Auto-increment

Disadvantages of database auto-increment IDs:

  1. May expose business metrics: users could infer daily coupon sales.
  2. Large coupon volumes lead to oversized IDs; sharding causes ID conflicts.

Alt text

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// Sign bit 0 + 31-bit timestamp + 32-bit sequence number
public long nextId(String keyPrefix){
    // Generate timestamp
    LocalDateTime now=LocalDateTime.now();
    long nowSecond=now.toEpochSecond(ZoneOffset.UTC);
    long timestamp=nowSecond-BEGIN_TIMESTAMP;
    // Generate sequence number. Same key per day
    String date=now.format(DateTimeFormatter.ofPattern("yyyy:MM:dd"));
    long count=  stringRedisTemplate.opsForValue().increment("icr:"+keyPrefix+":"+date);
    // Combine and return
    return timestamp<<COUNT_BITS|count;
}

keyPrefix represents object type, e.g., coupon order.

Overselling Problem

200 QPS with only 100 coupons. JMeter shows 45% failure rate; database shows -9 stock, indicating overselling.

Optimistic Lock - Version Number

Alt text Read version A from DB, update only if current version matches A, then increment version.

Optimistic Lock - CAS

1
2
3
4
5
// Deduct stock
boolean success= seckillVoucherService.update().setSql("stock=stock-1")
        .eq("voucher_id",voucherId)
        .eq("stock",voucher.getStock()) // CAS method
        .update();
1
2
3
4
UPDATE seckill_voucher
SET stock = stock - 1
WHERE voucher_id = #{voucherId}
  AND stock = #{voucher.getStock()};

JMeter shows 89% failure rate; 79 coupons remain unsold due to race conditions between read and update.

Improved CAS

1
2
3
4
boolean success= seckillVoucherService.update().setSql("stock=stock-1")
        .eq("voucher_id",voucherId)
        .gt("stock", 0) // Let DB handle locking
        .update();
1
2
3
4
UPDATE seckill_voucher
SET stock = stock - 1
WHERE voucher_id = #{voucherId}
  AND stock > 0;

50% success rate; all coupons sold out. Uses database row locks.

One Order Per User

Initial Approach

Check DB for existing user order before creating new order. Race condition exists between check and insert.

synchronized Pessimistic Lock

Lock the entire order creation method:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
@Transactional
public synchronized Result createVoucherOrder(Long voucherId){
    Long userId=UserHolder.getUser().getId();
    int count=query().eq("user_id",userId).eq("voucher_id", voucherId).count();
    if(count>0) return Result.fail("User already purchased");
    
    boolean success= seckillVoucherService.update().setSql("stock=stock-1")
            .eq("voucher_id",voucherId).gt("stock", 0).update();
    if(!success) return Result.fail("Insufficient stock");
    
    VoucherOrder voucherOrder=new VoucherOrder();
    Long orderId= redisIdWorker.nextId("order");
    voucherOrder.setId(orderId);
    voucherOrder.setUserId(userId);
    voucherOrder.setVoucherId(voucherId);
    save(voucherOrder);
    return Result.ok(orderId);
}

Thread-safe with READ_COMMITTED isolation, but coarse-grained locking.

Fine-grained Locking

Lock per user ID:

1
2
3
4
5
6
7
@Transactional
public Result createVoucherOrder(Long voucherId){
    Long userId=UserHolder.getUser().getId();
    synchronized(userId.toString().intern()){
        // ... same logic
    }
}

Fails in cluster environments due to JVM-specific locks.

Redis Distributed Lock

1
2
3
4
5
6
7
8
9
SimpleRedisLock lock=new SimpleRedisLock("order:"+userId, stringRedisTemplate);
boolean isLock=lock.tryLock(1200);
if(!isLock) return Result.fail("No duplicate orders");
try{
    IVoucherOrderService proxy=(IVoucherOrderService) AopContext.currentProxy();
    return proxy.createVoucherOrder(voucherId);
}finally{
    lock.unlock();
}

Works across JVMs but has issues with lock expiration and mistaken deletion.

Redisson Lock

Usage

1
2
3
4
5
6
7
8
9
@Configuration
public class RedissonConfig {
    @Bean
    public RedissonClient redissonClient() {
        Config config=new Config();
        config.useSingleServer().setAddress("redis://localhost:6379");
        return Redisson.create(config);
    }
}
1
2
RLock lock=redissonClient.getLock("lock:order:"+userId);
boolean isLock=lock.tryLock();

Reentrant Lock

Uses Redis hash: key=lock name, field=thread ID, value=reentry count.

Retry Mechanism

1
boolean isLock=lock.tryLock(long waitTime, long leaseTime, TimeUnit unit);

Retries lock acquisition with timeout.

Watchdog for Lease Renewal

Automatically renews lock expiration if leaseTime not specified.

MultiLock for Master-Slave Consistency

Requires acquiring locks on multiple Redis instances.

Seckill Optimization

Design

Move stock check and user validation to Redis using Lua scripts for atomic operations.

Lua Script for Validation

1
2
-- Script logic for stock check and user validation
-- Returns 0 if eligible, 1 if out of stock, 2 if user already ordered

Blocking Queue Implementation

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
@PostConstruct
private void init(){
    SECKILL_ORDER_EXECUTOR.submit(new VoucherOrderHandler());
}
private class VoucherOrderHandler implements Runnable{
    public void run() {
        while(true){
            VoucherOrder voucherOrder=orderTasks.take();
            handleVoucherOrder(voucherOrder);
        }
    }
}

Message Queue Options

  • List: Simple but no persistence
  • Pub/Sub: Multi-consumer but no persistence
  • Streams: Persistent with consumer groups

Summary

Solution Comparison

SolutionProsConsUse Case
Method synchronizedSimplePoor performanceNot recommended
User ID lock (JVM)Fine-grained, better perfFails in clusterSingle deployment
Redis distributed lockCluster support, fine controlComplex implementationProduction

Redis Lock Issues & Solutions

Issue TypeSolution
Mistaken deletionLua atomic unlock
Lock timeout concurrencyRedisson watchdog