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.

macrozheng
macrozheng
macrozheng
Mastering Token Bucket Rate Limiting and Lock Strategies in Flash Sale Systems

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

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.

javaspringoptimistic lockrate limitingpessimistic-lockToken Bucket
macrozheng
Written by

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.

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.