How to Build a 100k QPS Seckill System with Spring Boot, Redis, and Lua
This article provides a production‑grade, step‑by‑step engineering guide for designing a high‑concurrency seckill (flash‑sale) system that can sustain 100,000 QPS using Spring Boot, Redis with Lua scripts, asynchronous messaging, and comprehensive fault‑tolerance, monitoring, and scalability techniques.
1. Conclusion: A Seckill System Is a High‑Concurrency State Machine
Many teams first think the problem is simply to make the order‑creation API faster, but when QPS grows from hundreds to tens of thousands the core challenges shift to handling traffic spikes, hot SKUs, duplicate clicks, malicious bots, downstream jitter, and ensuring no oversell while keeping the main site stable and observable.
Key insight: The system must prevent the database from becoming the real‑time competition entry point.
2. Target Scenario and Constraints
Typical e‑commerce flash‑sale scenario:
300 000 users reserve the activity
80 000 concurrent users in the 10 seconds before start
Peak inbound traffic: 100 000 QPS
Effective purchase requests: 20 000–40 000 QPS
3 hot SKUs, each with 10 000 units
Per‑user limit: 1 unit
Interface P99 latency < 80 ms
Non‑functional goals (correctness, performance, availability 99.99%, isolation, scalability, recoverability, observability) are listed in a table in the original article.
3. Architecture Design: From Synchronous Order to "Qualification + Asynchronous Confirmation"
The system defines a successful seckill as obtaining a qualification first, then completing the order asynchronously. Users poll or receive push notifications for the final result.
"User first gets the purchase qualification, then the backend asynchronously creates the order; the user checks the final status via polling or push."
This trade‑off sacrifices immediate strong consistency for stability, scalability, and correctness.
3.1 Overall Production Architecture
Components (from client to result query): Client → CDN+WAF → Nginx/Gateway → Seckill Service Cluster → Redis Cluster → Risk Service → Kafka/RocketMQ → Order Consumer Cluster → MySQL/Sharding → Result Query API → Outbox/Compensation Job → Config Center → Metrics/Logs/Tracing.
3.2 Layer Responsibilities
CDN/WAF : static asset distribution, basic anti‑scraping, blacklist blocking
Gateway : routing, authentication, rate‑limiting, signature verification, gray release, circuit breaking
Seckill Service : qualification check, token validation, Lua atomic stock deduction, message dispatch
Redis : activity metadata, stock, user purchase flags, result cache
MQ : peak‑shaving, async decoupling, retry, replay
Order Consumer : idempotent consumption, order creation, stock persistence, result write‑back
MySQL : final order data, stock ledger, audit, reconciliation
Compensation Job : message replay, stock rollback, dirty‑data repair
Observability Platform : metrics, logs, tracing, alerts, capacity review
3.3 Why Redis + Lua Is Core
Seckill requires three steps: (1) check if the user has already purchased, (2) verify stock, (3) deduct stock and mark the user. If these steps are separate Redis commands, race conditions cause oversell. Lua scripts run single‑threaded and atomically, eliminating the need for Java‑level locks or DB pessimistic locks.
4. Deep Dive into Design Principles
4.1 Redis Atomicity Is Not a "Lock" but "Serial Execution"
Redis serialises command execution phases.
Lua script execution is never interrupted by other commands.
Thus multiple read‑write steps become an atomic operation inside Redis.
This allows correct stock deduction without Java‑side locking.
4.2 Three‑Layer Correctness Guarantees
Cache Layer Correctness : Lua guarantees no duplicate deduction in Redis.
Message Layer Correctness : Consumers must be idempotent because messages may be redelivered.
Database Layer Correctness : Unique constraints on order/qualification tables prevent duplicate final orders.
4.3 "Deduct in Redis, then Async Order" Trade‑off
Redis may succeed while the subsequent message or order fails, leading to temporary inconsistency. The system accepts this and focuses on strong final consistency via compensation, idempotency, and reconciliation.
Goal: Strong final result, not strong immediate consistency.
4.4 Why Direct DB Deduction Is Unsuitable for 100k QPS
SQL like
UPDATE seckill_goods SET available_stock = available_stock - 1 WHERE sku_id = ? AND available_stock > 0;works at low concurrency but becomes a bottleneck under high QPS due to row lock contention, connection‑pool exhaustion, and retry storms.
5. Core Business Flow: From Warm‑up to Order Completion
5.1 Pre‑heat Activities
Load activity config, product info, stock, purchase limits into Redis.
Push seckill pages, countdowns, and assets to CDN.
Pre‑warm hot product details, qualification data, and risk rules.
Scale up Gateway, Seckill Service, and MQ consumers.
Check Redis cluster hot‑key distribution, MQ partition load, DB master‑slave lag.
Dry‑run degradation switches, sold‑out switches, read‑only mode, and traffic replay.
5.2 In‑flight Core Link
1. User clicks purchase
2. Gateway performs auth, rate‑limit, signature check
3. Seckill Service validates activity status, user qualification, idempotent token
4. Redis Lua atomically checks repeat purchase and stock, then deducts
5. On success, writes result cache and publishes MQ message
6. Consumer asynchronously creates order; on DB success updates result cache
7. User polls result API for final status5.3 Post‑activity Cleanup
Reconcile Redis pre‑deduction with actual DB order count.
Clean up abnormal, dead‑letter, and compensation records.
Export peak traffic, limit‑hit rate, sell‑out time, failure‑reason distribution.
Recycle activity‑level caches, tokens, purchase flags, temporary result states.
Review hot keys, consumer lag, DB response time, gateway reject rate.
6. Production‑Level Data Model
6.1 Core Tables (SQL)
CREATE TABLE seckill_activity (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
activity_id BIGINT NOT NULL UNIQUE,
activity_name VARCHAR(128) NOT NULL,
start_time DATETIME NOT NULL,
end_time DATETIME NOT NULL,
status TINYINT NOT NULL,
created_at DATETIME NOT NULL,
updated_at DATETIME NOT NULL
);
CREATE TABLE seckill_sku (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
activity_id BIGINT NOT NULL,
sku_id BIGINT NOT NULL,
total_stock INT NOT NULL,
available_stock INT NOT NULL,
limit_per_user INT NOT NULL DEFAULT 1,
version INT NOT NULL DEFAULT 0,
created_at DATETIME NOT NULL,
updated_at DATETIME NOT NULL,
UNIQUE KEY uk_activity_sku (activity_id, sku_id)
);
CREATE TABLE seckill_order (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
order_no VARCHAR(64) NOT NULL UNIQUE,
activity_id BIGINT NOT NULL,
sku_id BIGINT NOT NULL,
user_id BIGINT NOT NULL,
status TINYINT NOT NULL,
amount DECIMAL(10,2) NOT NULL,
request_id VARCHAR(64) NOT NULL UNIQUE,
created_at DATETIME NOT NULL,
updated_at DATETIME NOT NULL,
UNIQUE KEY uk_activity_user_sku (activity_id, user_id, sku_id)
);
CREATE TABLE seckill_task_log (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
request_id VARCHAR(64) NOT NULL,
activity_id BIGINT NOT NULL,
sku_id BIGINT NOT NULL,
user_id BIGINT NOT NULL,
message_status VARCHAR(32) NOT NULL,
biz_status VARCHAR(32) NOT NULL,
remark VARCHAR(256),
created_at DATETIME NOT NULL,
updated_at DATETIME NOT NULL,
UNIQUE KEY uk_request_id (request_id)
);6.2 Why Keep Both request_id and Unique Indexes
request_idlinks request, message, consumption, and logs (technical idempotency). uk_activity_user_sku prevents duplicate purchases at the business level. uk_request_id prevents duplicate consumption and duplicate message delivery.
Both dimensions are required; they cannot replace each other.
7. Redis Key Design (Hot‑Key Architecture)
seckill:activity:{activityId}:status -> activity status
seckill:stock:{activityId}:{skuId} -> remaining stock
seckill:user:{activityId}:{skuId}:{userId} -> whether user has purchased
seckill:req:{requestId} -> request idempotent marker
seckill:result:{requestId} -> purchase result
seckill:token:{activityId}:{userId} -> purchase token
seckill:soldout:{activityId}:{skuId} -> sold‑out flag7.1 Key Design Tips
Stock keys must be short and stable to reduce network overhead.
Purchase flags should expire after the activity ends to avoid unbounded growth.
Request idempotent markers need a TTL long enough to survive client retries.
Hot SKUs may need sharding or a proxy layer to disperse hotspot traffic.
7.2 Result‑State Cache Importance
Many systems return "queued" after a successful stock deduction but lack a result‑state cache, causing the front‑end to hit the DB repeatedly. Caching the result state (INIT, ACCEPTED, SUCCESS, FAILED) in Redis lets the front‑end poll Redis instead of the DB.
8. Core Code Implementation (Spring Boot + Redis + Lua + MQ)
8.1 Maven Dependencies
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.kafka</groupId>
<artifactId>spring-kafka</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>8.2 Request and Response DTOs
@Data
public class SeckillRequest {
@NotNull private Long activityId;
@NotNull private Long skuId;
@NotBlank private String requestId;
}
@Builder
@Data
public class SeckillResponse {
private String requestId;
private String status;
private String message;
}8.3 Controller (Entry Orchestration Only)
@RestController
@RequestMapping("/api/seckill")
@RequiredArgsConstructor
public class SeckillController {
private final SeckillApplicationService seckillApplicationService;
@PostMapping("/execute")
public ResponseEntity<SeckillResponse> execute(@RequestBody @Valid SeckillRequest request,
@RequestHeader("X-USER-ID") Long userId) {
SeckillResponse response = seckillApplicationService.execute(userId, request);
return ResponseEntity.ok(response);
}
@GetMapping("/result/{requestId}")
public ResponseEntity<SeckillResponse> result(@PathVariable String requestId,
@RequestHeader("X-USER-ID") Long userId) {
return ResponseEntity.ok(seckillApplicationService.queryResult(userId, requestId));
}
}8.4 Lua Script (Atomic Idempotent Stock Deduction)
-- KEYS[1] = requestId key
-- KEYS[2] = user purchase key
-- KEYS[3] = stock key
-- KEYS[4] = result key
-- ARGV[1] = requestId
-- ARGV[2] = userId
-- ARGV[3] = result ttl seconds
-- ARGV[4] = user marker ttl seconds
local requestKey = KEYS[1]
local userKey = KEYS[2]
local stockKey = KEYS[3]
local resultKey = KEYS[4]
local requestId = ARGV[1]
local userId = ARGV[2]
local resultTtl = tonumber(ARGV[3])
local userTtl = tonumber(ARGV[4])
if redis.call('EXISTS', requestKey) == 1 then
return 0
end
if redis.call('EXISTS', userKey) == 1 then
redis.call('SETEX', resultKey, resultTtl, 'FAILED:REPEAT_BUY')
return -1
end
local stock = tonumber(redis.call('GET', stockKey) or '0')
if stock <= 0 then
redis.call('SETEX', resultKey, resultTtl, 'FAILED:SOLD_OUT')
return -2
end
redis.call('SETEX', requestKey, resultTtl, requestId)
redis.call('DECR', stockKey)
redis.call('SETEX', userKey, userTtl, userId)
redis.call('SETEX', resultKey, resultTtl, 'ACCEPTED')
return 18.5 Application Service (Entry Validation & Message Dispatch)
@Service
@RequiredArgsConstructor
public class SeckillApplicationService {
private final StringRedisTemplate stringRedisTemplate;
private final DefaultRedisScript<Long> seckillScript;
private final KafkaTemplate<String, SeckillOrderMessage> kafkaTemplate;
public SeckillResponse execute(Long userId, SeckillRequest request) {
String requestKey = "seckill:req:" + request.getRequestId();
String userKey = "seckill:user:" + request.getActivityId() + ":" + request.getSkuId() + ":" + userId;
String stockKey = "seckill:stock:" + request.getActivityId() + ":" + request.getSkuId();
String resultKey = "seckill:result:" + request.getRequestId();
Long result = stringRedisTemplate.execute(seckillScript,
List.of(requestKey, userKey, stockKey, resultKey),
request.getRequestId(), String.valueOf(userId), "1800", "86400");
if (result == null) {
throw new IllegalStateException("lua execute failed");
}
if (result == 0L) {
return SeckillResponse.builder()
.requestId(request.getRequestId())
.status("ACCEPTED")
.message("duplicate request ignored")
.build();
}
if (result == -1L) {
return SeckillResponse.builder()
.requestId(request.getRequestId())
.status("FAILED")
.message("repeat buy")
.build();
}
if (result == -2L) {
return SeckillResponse.builder()
.requestId(request.getRequestId())
.status("FAILED")
.message("sold out")
.build();
}
// enqueue order creation
SeckillOrderMessage message = SeckillOrderMessage.builder()
.requestId(request.getRequestId())
.activityId(request.getActivityId())
.skuId(request.getSkuId())
.userId(userId)
.build();
kafkaTemplate.send("seckill-order-topic", request.getRequestId(), message);
return SeckillResponse.builder()
.requestId(request.getRequestId())
.status("ACCEPTED")
.message("queued")
.build();
}
public SeckillResponse queryResult(Long userId, String requestId) {
String value = stringRedisTemplate.opsForValue().get("seckill:result:" + requestId);
if (value == null) {
return SeckillResponse.builder()
.requestId(requestId)
.status("UNKNOWN")
.message("result expired or not found")
.build();
}
String[] parts = value.split(":", 2);
return SeckillResponse.builder()
.requestId(requestId)
.status(parts[0])
.message(parts.length > 1 ? parts[1] : value)
.build();
}
}8.6 Consumer (The Real Production Difficulty Lies Here)
@Component
@RequiredArgsConstructor
public class SeckillOrderConsumer {
private final OrderDomainService orderDomainService;
private final StringRedisTemplate stringRedisTemplate;
@KafkaListener(topics = "seckill-order-topic", groupId = "seckill-order-consumer")
public void consume(SeckillOrderMessage message, Acknowledgment acknowledgment) {
try {
orderDomainService.createOrder(message);
stringRedisTemplate.opsForValue().set(
"seckill:result:" + message.getRequestId(),
"SUCCESS:" + message.getRequestId(),
Duration.ofMinutes(30));
acknowledgment.acknowledge();
} catch (DuplicateKeyException ex) {
stringRedisTemplate.opsForValue().set(
"seckill:result:" + message.getRequestId(),
"SUCCESS:DUPLICATE_IGNORED",
Duration.ofMinutes(30));
acknowledgment.acknowledge();
} catch (Exception ex) {
stringRedisTemplate.opsForValue().set(
"seckill:result:" + message.getRequestId(),
"FAILED:ORDER_CREATE_ERROR",
Duration.ofMinutes(30));
throw ex;
}
}
}8.7 Order Domain Service (Idempotent DB Write + Unique Constraints)
@Service
@RequiredArgsConstructor
public class OrderDomainService {
private final SeckillOrderRepository seckillOrderRepository;
@Transactional(rollbackFor = Exception.class)
public void createOrder(SeckillOrderMessage message) {
if (seckillOrderRepository.existsByRequestId(message.getRequestId())) {
return; // already processed
}
SeckillOrder order = new SeckillOrder();
order.setOrderNo("SK" + System.currentTimeMillis() + message.getUserId());
order.setRequestId(message.getRequestId());
order.setActivityId(message.getActivityId());
order.setSkuId(message.getSkuId());
order.setUserId(message.getUserId());
order.setStatus(1);
order.setAmount(BigDecimal.valueOf(99.00));
seckillOrderRepository.save(order);
}
}8.8 Why Not Deduct Stock Again in Consumer
Stock has already been atomically deducted in the Redis Lua phase. The consumer's responsibility is to ensure the order is persisted, guarantee idempotent consumption, and handle compensation if failures occur. Re‑deducting would re‑introduce race conditions.
9. Engineering Upgrade: Ten Must‑Have Capabilities for High‑Concurrency
Gateway Rate Limiting : multi‑layer limits (IP, user, activity, SKU).
Qualification Tokens : pre‑issue tokens to eligible users to filter traffic early.
Local Hotspot Cache : Caffeine for read‑heavy config data.
Message Partition Design : partition by skuId or requestId to avoid hot‑partition bottlenecks.
Idempotent Design : client requestId, Redis idempotent marker, MQ consumer check, DB unique index.
Failure Compensation : log‑based replay, timeout reconciliation, stock rollback, result cache repair.
Result Query API : fast return of requestId and status; front‑end polls Redis.
Resource Isolation : separate thread pools, connection pools, and circuit‑breaker rules for seckill vs. main site.
Observability : monitor QPS, RT, error rate, Redis command latency, MQ lag, DB RT, sell‑out speed, repeat‑request ratio.
Switch Governance : dynamic switches for activity, SKU, rate limits, degradation, sell‑out short‑circuit, qualification‑only mode.
10. Scalable Design: From Single Activity to Multi‑Activity, Multi‑SKU, Multi‑Region
Isolate by activityId in Redis keys, MQ topics, and metrics.
Hot‑SKU isolation: separate rate limits, dedicated consumer groups, possible Redis sharding.
Database sharding (by user or time) for massive order volumes; keep unique indexes compatible with sharding.
Unit‑based routing for multi‑region deployments; keep local loops closed and avoid cross‑region strong dependencies.
11. Production Fault Scenarios and Mitigation
Redis succeeded but MQ failed : log pending status, async compensation scans and retries, stock rollback on timeout.
MQ consumer backlog : scale consumer instances, throttle upstream traffic, tighten SKU‑level limits, show "queueing" UI.
Database jitter : consumer degrades to log‑only, delay order creation, return only qualification success.
Redis hot‑key saturation : pre‑identify hot SKUs, add proxy‑layer hot‑key dispersion, local cache short‑circuit, possible stock sharding.
User sees success but no order : clarify status semantics (ACCEPTED = qualification, SUCCESS = order persisted) and ensure front‑end messages match.
12. Process Knowledge: Seckill Is Not a Single API
Pre‑activity preparation (rules, config, stock preload, CDN push, risk rules, capacity checks, dry‑run).
During activity (open static pages, gradual ramp‑up, real‑time monitoring, dynamic limit adjustments).
Post‑activity cleanup (freeze config, reconcile stock/orders, generate reports, clean caches, review metrics).
13. Real‑World Case: 100k QPS Hot‑SKU Seckill
Scenario: brand‑collaboration launch, 10‑minute window, 1 hot SKU with 10 000 units, 1.2 M reserved users, estimated 100 k QPS peak.
Initial naive pipeline (Gateway → Spring Boot → Redis check → MySQL deduct → MySQL write) failed at 15 k QPS (DB jitter) and 20 k QPS (connection pool exhaustion) with massive duplicate writes.
Upgraded pipeline (Gateway limit → qualification → Redis Lua → Kafka async order → consumer write → Redis result cache) achieved:
~55 % invalid traffic filtered at entry.
~35 % duplicate/sold‑out filtered by Redis Lua.
Effective MQ traffic 8‑12 % of total requests.
P99 latency < 60 ms.
DB load smoothed by async consumption.
14. Load Testing and Capacity Estimation
Test QPS, RT, Redis Lua latency, MQ throughput, consumer lag, DB write throughput, limit hit distribution, hot‑SKU sell‑out path.
Suggested test scenarios: normal traffic, repeat clicks, malicious traffic, fault injection (MQ delay, Redis jitter, DB slowdown), sell‑out short‑circuit.
Simple capacity estimate example: 100 k total QPS → 35 % survive entry filtering → 35 k reach Redis → 10 % survive Lua → 10 k enter MQ → final DB writes equal to actual stock (10 k), not the full 100 k.
15. Common Pitfalls
Only Redis stock without idempotency → duplicate orders.
Defining success too early (stock deducted = order success) → user complaints.
No result‑state cache → DB overload from polling.
Only test normal flow, ignore retry storms and post‑sell‑out traffic.
Deploy seckill services in the same resource pool as main site → cascade failures.
16. Evolution Roadmap
Stage 1 – Minimal Viable : Spring Boot, Redis preload, Lua deduction, MySQL unique index.
Stage 2 – Standard Production : Gateway rate limiting, qualification tokens, MQ async order, result cache, compensation, monitoring.
Stage 3 – Large‑Scale Enhancement : Hot‑SKU isolation, multi‑level cache, unit routing, multi‑region disaster recovery, auto‑scaling, fault‑injection platform.
17. Full‑Solution Summary
Seckill is not a "fast interface" but a high‑concurrency state‑flow system built around correctness, isolation, peak‑shaving, and eventual consistency.
Front‑end filters invalid traffic.
Redis Lua atomically handles stock deduction and duplicate checks.
MQ decouples the peak from the DB.
Idempotent consumer + DB unique indexes prevent duplicate orders.
Result‑state cache protects the query path.
Compensation, reconciliation, and dynamic switches guarantee final consistency.
Monitoring, load testing, and rehearsal ensure controllable launch.
18. Final Advice for Implementation Teams
Standardise success semantics: distinguish ACCEPTED (qualification) from SUCCESS (order persisted).
Implement request‑level idempotency, consumer‑level idempotency, and DB unique constraints first.
Deploy a result‑state cache before allowing front‑end to query the DB.
Build compensation jobs and activity reconciliation before scaling architecture.
Conduct full‑stack load tests and fault‑injection rehearsals before claiming 100 k QPS capability.
The most fragile part of a production system is not missing features, but assuming readiness without the full chain of architecture, process, governance, and failure‑handling in place.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Cloud Architecture
Focuses on cloud‑native and distributed architecture engineering, sharing practical solutions and lessons learned. Covers microservice governance, Kubernetes, observability, and stability engineering to help your systems run stable, fast, and cost‑effectively.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
