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.

Programmer DD
Programmer DD
Programmer DD
Build a Flash‑Sale System with Optimistic Locking, Redis Limiting, and Kafka

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:

Architecture diagram
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:

Project structure
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.

Web scaling with Nginx
Web scaling with Nginx
Service scaling
Service scaling

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.sh

Solution 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.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

JavaMicroservicesRateLimitingSeckillOptimisticLocking
Programmer DD
Written by

Programmer DD

A tinkering programmer and author of "Spring Cloud Microservices in Action"

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.