Generating Distributed Unique Identifiers with Redis
In high-concurrency flash sale scenarios, traditional database auto-increment primary keys present several critical limitations that can compromise system integrity and performance.
Limitations of Database Auto-Increment IDs
When processing time-sensitive orders during promotional events, relying on database-generated sequential identifiers introduces two major concerns:
- Predictability: Sequential IDs expose business metrics such as order volume and growth patterns to competitors
- Scalability Bottleneck: Single-table auto-increment mechanisms create performance ceilings and hotspot issues in distributde architectures
Redis-Powered Distributed ID Generator
A robust solution employs Redis atomic operations combined with bitwise composition to generate 64-bit unique identifiers with temporal ordering capabilities.
The identifier structure consists of:
- Sign Bit (1 bit): Permanently set to 0 for future compatibility
- Timestamp Component (31 bits): Stores seconds elapsed since custom epoch, providing approximately 69 years of range (2³¹ seconds)
- Sequence Counter (32 bits): Tracks increments within the same second, supporting over 4 billion unique IDs per second
@Component
public class RedisDistributedIdGenerator {
@Autowired
private StringRedisTemplate redisTemplate;
// Custom epoch: January 1, 2022 00:00:00 UTC
private static final long EPOCH_START = 1640995200L;
private static final long SEQUENCE_BIT_LENGTH = 32L;
public long generateId(String businessKey) {
// Capture current timestamp
LocalDateTime currentTime = LocalDateTime.now(ZoneOffset.UTC);
long currentSecond = currentTime.toEpochSecond(ZoneOffset.UTC);
long timestampOffset = currentSecond - EPOCH_START;
// Generate atomic sequence number using Redis
String dateKey = currentTime.format(DateTimeFormatter.BASIC_ISO_DATE);
String counterKey = String.format("id:counter:%s:%s", businessKey, dateKey);
Long sequence = redisTemplate.opsForValue().increment(counterKey);
// Compose final ID: timestamp << 32 | sequence
return timestampOffset << SEQUENCE_BIT_LENGTH | sequence;
}
}
Building Overselling Protection for Flash Sales
The Race Condition Challenge
During peak traffic, thousands of concurrent requests may simultaneously query inventory levels before deduction. This check-then-act pattern creates a critical race window where multiple threads can oversell limited stock.
Optimistic Locking Strategy
The initial approach uses version comparison—ensuring the stock value hasn't changed between read and write operations:
// Initial flawed implementation
boolean deductionSuccess = couponService.update()
.setSql("available_quantity = available_quantity - 1")
.eq("coupon_id", couponId)
.eq("available_quantity", currentStock) // Version check
.update();
if (!deductionSuccess) {
return ApiResponse.error("Insufficient inventory");
}
This implementation suffers from high abort rates because all concurrent threads read the same stock value, but only one can successfully update it. The remaining transcations fail despite inventory still being available.
Enhanced Condition-Based Update
A more pragmatic approach eliminates the version comparison and simply verifies that inventory remains positive:
// Improved implementation
boolean updateSuccess = couponService.update()
.setSql("stock = stock - 1")
.eq("promotion_id", promoId)
.gt("stock", 0) // Ensure stock remains positive
.update();
if (!updateSuccess) {
return Result.failure("Inventory depleted");
}
This pattern allows all qualified requests to proceed as long as inventory exists, dramatically improving success rates while maintaining data consistency through atomic database operations.