Global ID Generator: Redis Auto-increment
Disadvantages of database auto-increment IDs:
- May expose business metrics: users could infer daily coupon sales.
- Large coupon volumes lead to oversized IDs; sharding causes ID conflicts.

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
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
| Solution | Pros | Cons | Use Case |
|---|
| Method synchronized | Simple | Poor performance | Not recommended |
| User ID lock (JVM) | Fine-grained, better perf | Fails in cluster | Single deployment |
| Redis distributed lock | Cluster support, fine control | Complex implementation | Production |
Redis Lock Issues & Solutions
| Issue Type | Solution |
|---|
| Mistaken deletion | Lua atomic unlock |
| Lock timeout concurrency | Redisson watchdog |