Design and Implementation of a Flash Sale (SecKill) System with Concurrency Control
This article presents a complete design and implementation of a flash‑sale (SecKill) system, detailing entity models, DAO methods, service and controller layers, a high‑concurrency test, and an improved solution using row‑level locking to prevent overselling under heavy load.
This article describes the end‑to‑end design of a flash‑sale (SecKill) system built with Spring, JPA and Java, focusing on handling high concurrency and preventing overselling.
Initial scheme
Two entity classes are defined: @Entity public class SecKillGoods implements Serializable { @Id private String id; /** remaining stock */ private Integer remainNum; /** product name */ private String goodsName; } and @Entity public class SecKillOrder implements Serializable { @Id @GeneratedValue @Column(length = 36) private String id; private String consumer; private String goodsId; private Integer num; }
The DAO for goods provides a custom update method to reduce stock: public interface SecKillGoodsDao extends JpaRepository { @Query("update SecKillGoods g set g.remainNum = g.remainNum - ?2 where g.id=?1") @Modifying @Transactional int reduceStock(String id, Integer remainNum); }
The service layer initializes the product and offers a method to generate an order: @Service public class SecKillService { @Autowired SecKillGoodsDao secKillGoodsDao; @Autowired SecKillOrderDao secKillOrderDao; @PostConstruct public void initSecKillEntity(){ secKillGoodsDao.deleteAll(); secKillOrderDao.deleteAll(); SecKillGoods g = new SecKillGoods(); g.setId("123456"); g.setGoodsName("秒杀产品"); g.setRemainNum(10); secKillGoodsDao.save(g); } public void generateOrder(String consumer,String goodsId,Integer num){ secKillOrderDao.save(new SecKillOrder(consumer,goodsId,num)); } }
The controller exposes the seckill endpoint: @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){ Thread.sleep(1000); // simulate network delay secKillGoodsDao.reduceStock(num); secKillService.generateOrder(consumer,goodsId,num); return "购买成功"; } return "购买失败,库存不足"; } }
To simulate high concurrency, a second controller launches 50 threads, each sending a POST request to the seckill URL: @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#=1"); ClientHttpRequest request = httpRequestFactory.createRequest(uri, HttpMethod.POST); BufferedReader br = new BufferedReader(new InputStreamReader(request.execute().getBody())); String line, result=""; while((line=br.readLine())!=null){ result+=line; } System.out.println(consumerName+":"+result); }catch(Exception e){ e.printStackTrace(); } }).start(); } return "simulationCocurrentTakeOrder"; } }
Running the simulation reveals an over‑stock problem: multiple threads read the same remaining quantity before it is decremented, leading to dirty reads and more orders than available stock.
A naïve fix is to synchronize the controller method, but this serialises all requests and kills performance.
Improved scheme
The better solution leverages row‑level locking in the database. The DAO update query is changed to include a condition that the stock must be greater than zero, and the method returns the number of affected rows: public interface SecKillGoodsDao extends JpaRepository { @Query("update SecKillGoods g set g.remainNum = g.remainNum - ?2 where g.id=?1 and g.remainNum>0") @Modifying @Transactional int reduceStock(String id,Integer remainNum); }
The controller now checks the returned integer to decide success: @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); if(goods.getRemainNum()>0){ int affected = secKillGoodsDao.reduceStock(goodsId,num); if(affected!=0){ secKillService.generateOrder(consumer,goodsId,num); return "购买成功"; } else { return "购买失败,库存不足"; } } else { return "购买失败,库存不足"; } } return "购买失败,库存不足"; }
After applying this change, the high‑concurrency test shows that the stock correctly drops to zero and only ten orders are recorded, confirming that row‑level locking prevents overselling even with simulated network delays.
Source: CSDN article . The content is reproduced with attribution to the original author.
Architecture Digest
Focusing on Java backend development, covering application architecture from top-tier internet companies (high availability, high performance, high stability), big data, machine learning, Java architecture, and other popular fields.
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.