How to Build a High‑Concurrency Flash‑Sale (SecKill) System in Java

This article explains how to design and implement a Java‑based flash‑sale (seckill) system that can handle tens of thousands of concurrent requests, covering entity modeling, DAO methods, service logic, controller handling, a concurrency simulation test, and an improved locking strategy to prevent overselling.

21CTO
21CTO
21CTO
How to Build a High‑Concurrency Flash‑Sale (SecKill) System in Java

In e‑commerce flash‑sale (seckill) scenarios, massive concurrent requests can overwhelm the system. This article demonstrates a Java‑based solution, including entity design, DAO interfaces, service logic, controller implementation, a concurrency simulation test, and an improved locking strategy to avoid overselling.

Initial Scheme

Entity for the seckill product with initial stock:

@Entity
public class SecKillGoods implements Serializable {
    @Id
    private String id;
    /** remaining stock */
    private Integer remainNum;
    /** product name */
    private String goodsName;
}

Entity for successful seckill orders:

@Entity
public class SecKillOrder implements Serializable {
    @Id
    @GenericGenerator(name = "PKUUID", strategy = "uuid2")
    @GeneratedValue(generator = "PKUUID")
    @Column(length = 36)
    private String id;
    // user name
    private String consumer;
    // product id
    private String goodsId;
    // purchase quantity
    private Integer num;
}

DAO for the product, providing a method to reduce stock:

public interface SecKillGoodsDao extends JpaRepository<SecKillGoods, String> {
    @Query("update SecKillGoods g set g.remainNum = g.remainNum - ?2 where g.id=?1")
    @Modifying(clearAutomatically = true)
    @Transactional
    int reduceStock(String id, Integer remainNum);
}

Service that initializes data and saves orders:

@Service
public class SecKillService {
    @Autowired
    SecKillGoodsDao secKillGoodsDao;
    @Autowired
    SecKillOrderDao secKillOrderDao;

    /**
     * Initialize data at startup: clear tables and add a product with 10 units.
     */
    @PostConstruct
    public void initSecKillEntity() {
        secKillGoodsDao.deleteAll();
        secKillOrderDao.deleteAll();
        SecKillGoods secKillGoods = new SecKillGoods();
        secKillGoods.setId("123456");
        secKillGoods.setGoodsName("秒杀产品");
        secKillGoods.setRemainNum(10);
        secKillGoodsDao.save(secKillGoods);
    }

    /** Save a successful order */
    public void generateOrder(String consumer, String goodsId, Integer num) {
        secKillOrderDao.save(new SecKillOrder(consumer, goodsId, num));
    }
}

Controller handling a seckill request (simplified version):

@Controller
public class SecKillController {
    @Autowired
    SecKillGoodsDao secKillGoodsDao;
    @Autowired
    SecKillService secKillService;

    @RequestMapping("/seckill.html")
    @ResponseBody
    public String SecKill(String consumer, String goodsId, Integer num) throws InterruptedException {
        SecKillGoods goods = secKillGoodsDao.findOne(goodsId);
        if (goods.getRemainNum() >= num) {
            // simulate network delay
            Thread.sleep(1000);
            // reduce stock
            secKillGoodsDao.reduceStock(goodsId, num);
            // save order
            secKillService.generateOrder(consumer, goodsId, num);
            return "购买成功";
        }
        return "购买失败,库存不足";
    }
}

Simulation controller that spawns multiple threads to mimic high concurrency:

@Controller
public class SecKillSimulationOpController {
    final String takeOrderUrl = "http://127.0.0.1:8080/seckill.html";

    @RequestMapping("/simulationCocurrentTakeOrder")
    @ResponseBody
    public String simulationCocurrentTakeOrder() {
        SimpleClientHttpRequestFactory httpRequestFactory = new SimpleClientHttpRequestFactory();
        for (int i = 0; i < 50; i++) {
            final String consumerName = "consumer" + i;
            new Thread(() -> {
                try {
                    URI uri = new URI(takeOrderUrl + "?consumer=" + consumerName + "&goodsId=123456&num=1");
                    ClientHttpRequest request = httpRequestFactory.createRequest(uri, HttpMethod.POST);
                    InputStream body = request.execute().getBody();
                    BufferedReader br = new BufferedReader(new InputStreamReader(body));
                    String line, result = "";
                    while ((line = br.readLine()) != null) {
                        result += line;
                    }
                    System.out.println(consumerName + ":" + result);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }).start();
        }
        return "simulationCocurrentTakeOrder";
    }
}

After running the simulation, the order table should contain only 10 records because the product was initialized with 10 units. The screenshots below show the actual results.

Order table after simulation
Order table after simulation

Product table after initialization:

Product table
Product table

Analysis of Over‑Selling

Multiple threads read the stock simultaneously, each sees enough inventory, but because the stock reduction is delayed, they all proceed, causing dirty reads and overselling.

Improved Scheme

Apply row‑level locking in the database so that only one thread can modify a row at a time. The DAO method is changed to include a condition that the remaining stock must be greater than zero, and the method returns the number of affected rows.

public interface SecKillGoodsDao extends JpaRepository<SecKillGoods, String> {
    @Query("update SecKillGoods g set g.remainNum = g.remainNum - ?2 where g.id=?1 and g.remainNum>0")
    @Modifying(clearAutomatically = true)
    @Transactional
    int reduceStock(String id, Integer remainNum);
}

The controller now checks the return value of reduceStock to decide whether the purchase succeeded:

@RequestMapping("/seckill.html")
@ResponseBody
public String SecKill(String consumer, String goodsId, Integer num) throws InterruptedException {
    SecKillGoods goods = secKillGoodsDao.findOne(goodsId);
    if (goods.getRemainNum() >= num) {
        Thread.sleep(1000);
        int affected = secKillGoodsDao.reduceStock(goodsId, num);
        if (affected != 0) {
            secKillService.generateOrder(consumer, goodsId, num);
            return "购买成功";
        } else {
            return "购买失败,库存不足";
        }
    } else {
        return "购买失败,库存不足";
    }
}

Running the improved version under the same high‑concurrency test shows that the stock never drops below zero and the order table contains exactly the initialized number of records, confirming that the row‑level lock prevents overselling even with network delays.

Simulation result
Simulation result
Final order table
Final order table

Thus, by leveraging database row‑level locking and checking the affected row count, the seckill system can safely handle massive concurrent requests without the risk of overselling.

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.

JavadatabaseconcurrencyspringSeckilljpa
21CTO
Written by

21CTO

21CTO (21CTO.com) offers developers community, training, and services, making it your go‑to learning and service platform.

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.