Mastering Token Bucket Rate Limiting and Lock Strategies in Flash Sale Systems
This article explains how to implement token‑bucket rate limiting with Guava's RateLimiter, compare it to leaky‑bucket algorithms, and combine it with optimistic and pessimistic locking techniques to prevent overselling in high‑concurrency flash‑sale applications.
Preface
This is the second part of the flash‑sale system series, focusing on practical code to help you quickly grasp the key points of a flash‑sale system and start a real project.
We mainly discuss interface rate‑limiting measures, which are also a form of system security protection. The following easy‑to‑understand methods are presented:
Token‑bucket rate limiting
Per‑user request frequency limiting
Hiding the flash‑sale interface
Many readers complained that optimistic locking could not sell all items under high concurrency, so this article also covers optimistic and pessimistic locks.
Flash‑Sale System Overview
Refer to the first article for background; the link is omitted here.
Interface Rate Limiting
Without limiting the interface under massive concurrent purchase requests, the backend could be overwhelmed, especially the order‑creation interface that puts heavy load on the database.
Therefore the flash‑sale service is deployed independently from other backend systems to avoid cascading failures.
Practical Token‑Bucket Rate Limiting
The token‑bucket algorithm, originally from computer networking, smooths traffic by allowing bursts while keeping the average rate under control.
The token bucket has a fixed size and generates tokens at a constant rate. Unused tokens accumulate up to the bucket’s capacity; excess tokens are discarded.
Token Bucket vs. Leaky Bucket
The leaky‑bucket algorithm forces a constant outflow rate; excess incoming requests overflow.
The token bucket differs from the leaky bucket in that it allows bursts while still limiting the average transmission rate.
Implementing Token‑Bucket with Guava RateLimiter
Guava provides a RateLimiter class that implements the token‑bucket algorithm.
We add the rate‑limiting code to the previously discussed optimistic‑lock purchase interface.
OrderController:
@Controller
public class OrderController {
private static final Logger LOGGER = LoggerFactory.getLogger(OrderController.class);
@Autowired
private StockService stockService;
@Autowired
private OrderService orderService;
// each second allow 10 requests
RateLimiter rateLimiter = RateLimiter.create(10);
@RequestMapping("/createWrongOrder/{sid}")
@ResponseBody
public String createWrongOrder(@PathVariable int sid) {
int id = 0;
try {
id = orderService.createWrongOrder(sid);
LOGGER.info("创建订单id: [{}]", id);
} catch (Exception e) {
LOGGER.error("Exception", e);
}
return String.valueOf(id);
}
/**
* Optimistic lock + token‑bucket rate limiting
*/
@RequestMapping("/createOptimisticOrder/{sid}")
@ResponseBody
public String createOptimisticOrder(@PathVariable int sid) {
// non‑blocking token acquisition
if (!rateLimiter.tryAcquire(1000, TimeUnit.MILLISECONDS)) {
LOGGER.warn("你被限流了,真不幸,直接返回失败");
return "购买失败,库存不足";
}
int id;
try {
id = orderService.createOptimisticOrder(sid);
LOGGER.info("购买成功,剩余库存为: [{}]", id);
} catch (Exception e) {
LOGGER.error("购买失败:[{}]", e.getMessage());
return "购买失败,库存不足";
}
return String.format("购买成功,剩余库存为:%d", id);
}
}The line RateLimiter rateLimiter = RateLimiter.create(10); initializes a token bucket that releases 10 permits per second.
Two acquisition methods are demonstrated:
Blocking acquisition: the request waits until a token becomes available.
Non‑blocking acquisition: the request tries to obtain a token within a timeout (1000 ms here); if it fails, the purchase is rejected.
Using JMeter with 200 concurrent threads against a stock of 100 iPhones, we isolate requests that receive the "you have been rate‑limited" response.
When using rateLimiter.tryAcquire(1000, TimeUnit.MILLISECONDS), the green bars in the result indicate requests blocked by the token bucket, while red bars represent successful purchases. Approximately 15 % of requests bypass the limit.
Because optimistic locking can still cause some database update failures, we further discuss optimistic vs. pessimistic locks.
Optimistic Lock Without Version Field
An SQL update that checks the current sale count avoids adding a version column:
<update id="updateByOptimistic" parameterType="cn.monitor4all.miaoshadao.dao.Stock">
update stock
<set>
sale = sale + 1,
</set>
WHERE id = #{id,jdbcType=INTEGER}
AND sale = #{sale,jdbcType=INTEGER}
</update>Pessimistic Lock Implementation
We add a new endpoint that uses a database row lock (FOR UPDATE) inside a Spring transaction.
@RequestMapping("/createPessimisticOrder/{sid}")
@ResponseBody
public String createPessimisticOrder(@PathVariable int sid) {
int id;
try {
id = orderService.createPessimisticOrder(sid);
LOGGER.info("购买成功,剩余库存为: [{}]", id);
} catch (Exception e) {
LOGGER.error("购买失败:[{}]", e.getMessage());
return "购买失败,库存不足";
}
return String.format("购买成功,剩余库存为:%d", id);
}Service layer uses @Transactional to lock the row during stock check and update.
@Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRED)
@Override
public int createPessimisticOrder(int sid) {
Stock stock = checkStockForUpdate(sid);
saleStock(stock);
int id = createOrder(stock);
return stock.getCount() - stock.getSale();
}
private Stock checkStockForUpdate(int sid) {
Stock stock = stockService.getStockByIdForUpdate(sid);
if (stock.getSale().equals(stock.getCount())) {
throw new RuntimeException("库存不足");
}
return stock;
}
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;
}Running JMeter with 200 concurrent requests against this endpoint yields 100 successful purchases and 100 failures, demonstrating that pessimistic locking guarantees ordered, reliable sales under heavy load, though it may cause longer wait times.
We also verified that the transaction truly holds a row lock by attempting to update the stock directly in MySQL while the transaction is paused; the command blocks until the transaction completes, confirming the lock’s effectiveness.
Conclusion
The project code is open‑source on GitHub: https://github.com/qqxx6661/miaosha
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.
macrozheng
Dedicated to Java tech sharing and dissecting top open-source projects. Topics include Spring Boot, Spring Cloud, Docker, Kubernetes and more. Author’s GitHub project “mall” has 50K+ stars.
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.
