Build a Flash‑Sale System with Optimistic Locking, Redis Limiting, and Kafka
This article walks through the step‑by‑step implementation of a high‑throughput flash‑sale (seckill) service in Java, covering request flow, database schema, optimistic‑locking updates, Redis‑based rate limiting, CI scripts, and optional Kafka asynchronous order processing to achieve fast, reliable, and scalable sales handling.
Introduction
The article demonstrates a practical implementation of a flash‑sale (seckill) system based on a previously discussed Java interview design. It starts from a simple architecture diagram and progressively adds performance optimizations.
Architecture Overview
Request flow:
Web layer (Spring MVC) receives the request and forwards it to the Service layer via Dubbo RPC.
Service layer validates stock, updates inventory, and creates an order.
Architecture diagram:
Basic Order Process
Even without any optimization, a normal order flow consists of four steps:
Validate stock
Deduct stock
Create order
Pay
Project structure:
Web Layer (Controller)
@Autowired
private StockService stockService;
@Autowired
private OrderService orderService;
@RequestMapping("/createWrongOrder/{sid}")
@ResponseBody
public String createWrongOrder(@PathVariable int sid) {
logger.info("sid=[{}]", sid);
int id = 0;
try {
id = orderService.createWrongOrder(sid);
} catch (Exception e) {
logger.error("Exception", e);
}
return String.valueOf(id);
}Service Layer (API)
@Service("orderService")
public class OrderServiceImpl implements OrderService {
@Resource(name = "DBOrderService")
private DBOrderService orderService;
@Override
public int createWrongOrder(int sid) throws Exception {
// 1. Validate stock
Stock stock = checkStock(sid);
// 2. Deduct stock
saleStock(stock);
// 3. Create order
int id = createOrder(stock);
return id;
}
private Stock checkStock(int sid) {
return stockService.getStockById(sid);
}
private void saleStock(Stock stock) {
stock.setSale(stock.getSale() + 1);
stockService.updateStockById(stock);
}
private int createOrder(Stock stock) {
StockOrder order = new StockOrder();
order.setSid(stock.getId());
order.setName(stock.getName());
int id = orderMapper.insertSelective(order);
return id;
}
}Database Schema
CREATE TABLE `stock` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(50) NOT NULL DEFAULT '' COMMENT '名称',
`count` int(11) NOT NULL COMMENT '库存',
`sale` int(11) NOT NULL COMMENT '已售',
`version` int(11) NOT NULL COMMENT '乐观锁,版本号',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;
CREATE TABLE `stock_order` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`sid` int(11) NOT NULL COMMENT '库存ID',
`name` varchar(30) NOT NULL DEFAULT '' COMMENT '商品名称',
`create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=55 DEFAULT CHARSET=utf8;Problem: Overselling
When the endpoint is stress‑tested with JMeter (300 concurrent threads), the stock is correctly reduced to zero, but the order table contains 124 rows – a classic oversell scenario caused by race conditions.
Solution 1 – Optimistic Locking
Optimistic locking is introduced by adding a version column and updating it together with the stock count. The Service method checks the affected row count; if it is zero, a RuntimeException is thrown.
@Override
public int createOptimisticOrder(int sid) throws Exception {
Stock stock = checkStock(sid);
saleStockOptimistic(stock);
int id = createOrder(stock);
return id;
}
private void saleStockOptimistic(Stock stock) {
int count = stockService.updateStockByOptimistic(stock);
if (count == 0) {
throw new RuntimeException("Concurrent stock update failed");
}
} <update id="updateByOptimistic" parameterType="com.crossoverJie.seconds.kill.pojo.Stock">
UPDATE stock
<set>
sale = sale + 1,
version = version + 1
</set>
WHERE id = #{id}
AND version = #{version}
</update>After applying optimistic locking, failed requests are returned immediately and the order count matches the remaining stock.
Solution 2 – Horizontal Scaling
Both the web and service layers are scaled horizontally behind Nginx load balancers.
Solution 3 – Simple CI Scripts
Bash scripts stop the old process, pull the latest code, rebuild the WAR, and restart Tomcat for both the consumer (web) and provider (service) applications.
# Build web consumer
#!/bin/bash
appname="consumer"
PID=$(ps -ef | grep $appname | grep -v grep | awk '{print $2}')
for var in ${PID[@]}; do
echo "loop pid= $var"
kill -9 $var
done
echo "kill $appname success"
cd ..
git pull
cd SSM-SECONDS-KILL-WEB
mvn -Dmaven.test.skip=true clean package
cp target/SSM-SECONDS-KILL-WEB-2.2.0-SNAPSHOT.war /home/crossoverJie/tomcat-dubbo-consumer-8083/webapps
sh /home/crossoverJie/tomcat-dubbo-consumer-8083/bin/startup.sh # Build service provider
#!/bin/bash
appname="provider"
PID=$(ps -ef | grep $appname | grep -v grep | awk '{print $2}')
for var in ${PID[@]}; do
echo "loop pid= $var"
kill -9 $var
done
echo "kill $appname success"
cd ..
git pull
cd SSM-SECONDS-KILL-PROVIDER
mvn -Dmaven.test.skip=true clean package
cp target/SSM-SECONDS-KILL-PROVIDER-2.2.0-SNAPSHOT.war /home/crossoverJie/tomcat-dubbo-provider-8080/webapps
sh /home/crossoverJie/tomcat-dubbo-provider-8080/bin/startup.shSolution 4 – Distributed Rate Limiting with Redis
A Redis‑based rate limiter uses a Lua script to atomically increment a key and reject requests when the limit is exceeded.
@Configuration
public class RedisLimitConfig {
@Value("${redis.limit}")
private int limit;
@Autowired
private JedisConnectionFactory jedisConnectionFactory;
@Bean
public RedisLimit build() {
return new RedisLimit.Builder(jedisConnectionFactory, RedisToolsConstant.SINGLE)
.limit(limit)
.build();
}
}Controller usage:
@SpringControllerLimit(errorCode = 200)
@RequestMapping("/createOptimisticLimitOrder/{sid}")
@ResponseBody
public String createOptimisticLimitOrder(@PathVariable int sid) {
try {
int id = orderService.createOptimisticOrder(sid);
return String.valueOf(id);
} catch (Exception e) {
// handle exception, return failure code
return "0";
}
}Performance tests show a significant reduction in database connections and request latency when the limiter is active.
Solution 5 – Caching Stock Data in Redis
Repeated stock‑validation queries are moved to Redis. The authoritative data remains in MySQL, but reads are served from the cache and updates are synchronized.
public int createOptimisticOrderUseRedis(int sid) throws Exception {
Stock stock = checkStockByRedis(sid);
saleStockOptimisticByRedis(stock);
int id = createOrder(stock);
return id;
}
private Stock checkStockByRedis(int sid) throws Exception {
Integer count = Integer.parseInt(redisTemplate.opsForValue()
.get(RedisKeysConstant.STOCK_COUNT + sid));
Integer sale = Integer.parseInt(redisTemplate.opsForValue()
.get(RedisKeysConstant.STOCK_SALE + sid));
if (count.equals(sale)) {
throw new RuntimeException("Redis stock insufficient, currentCount=" + sale);
}
Integer version = Integer.parseInt(redisTemplate.opsForValue()
.get(RedisKeysConstant.STOCK_VERSION + sid));
Stock stock = new Stock();
stock.setId(sid);
stock.setCount(count);
stock.setSale(sale);
stock.setVersion(version);
return stock;
}
private void saleStockOptimisticByRedis(Stock stock) {
int count = stockService.updateStockByOptimistic(stock);
if (count == 0) {
throw new RuntimeException("Concurrent stock update failed");
}
redisTemplate.opsForValue().increment(RedisKeysConstant.STOCK_SALE + stock.getId(), 1);
redisTemplate.opsForValue().increment(RedisKeysConstant.STOCK_VERSION + stock.getId(), 1);
}Load tests confirm that database access drops dramatically while the order count remains correct.
Solution 6 – Asynchronous Order Creation with Kafka
After passing rate limiting and stock validation, order data is published to a Kafka topic. A separate Spring Boot consumer reads the topic and persists the order, allowing the API to return immediately. This decouples the order pipeline and further improves throughput.
Full source code is available in the GitHub repository.
Takeaways
Intercept invalid traffic as early as possible (rate limiting, UID‑based limits).
Cache hot data in Redis to minimize database hits.
Apply optimistic locking to avoid overselling.
Scale horizontally with load balancers.
Convert synchronous operations to asynchronous pipelines (Kafka) for higher throughput.
Fail fast to protect downstream services.
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.
Programmer DD
A tinkering programmer and author of "Spring Cloud Microservices in Action"
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.
