Implementing Optimistic Lock for Red Packet System in Java with Versioning and Retry Mechanisms

This article explains how to fix red‑packet over‑issuance bugs using optimistic locking with CAS, introduces version fields to prevent ABA problems, shows DAO, service and controller code updates, and demonstrates time‑based and count‑based retry strategies to improve success rates under high concurrency.

Java Captain
Java Captain
Java Captain
Implementing Optimistic Lock for Red Packet System in Java with Versioning and Retry Mechanisms

Review

Previous articles covered the red‑packet case analysis and code implementation (parts 1 and 2). The next step is to fix the over‑issuance bug using an optimistic lock.

Optimistic Lock

Optimistic locking is a non‑blocking concurrency control mechanism that relies on the CAS (Compare‑And‑Swap) principle instead of database locks, improving concurrent throughput.

CAS Principle

1. Save the current value (old value) of a shared resource, e.g., the remaining red‑packet count of 100.

2. When decreasing the count, compare the current database value with the saved old value; if they match, perform the update, otherwise abort.

The CAS flow is illustrated with a diagram (omitted).

ABA Problem

The ABA issue occurs when a value changes from A to B and back to A, causing a thread to mistakenly think the value is unchanged. Adding a monotonically increasing version field solves this problem.

Database Schema Modification

A new version column is added to the T_RED_PACKET table to record update attempts.

Code Refactoring

DAO Interface and Mapper

RedPacketDao.java

/**
 * @Description: Decrease red‑packet count using optimistic lock
 * @param id        -- red‑packet id
 * @param version   -- version marker
 * @return: number of rows updated
 */
public int decreaseRedPacketForVersion(@Param("id") Long id, @Param("version") Integer version);

RedPacket.xml

<!-- Decrease red‑packet with version check -->
<update id="decreaseRedPacketForVersion">
    update T_RED_PACKET
    set stock = stock - 1,
        version = version + 1
    where id = #{id}
      and version = #{version}
</update>

Service Layer

Implementation adds version checking and retry logic.

@Override
@Transactional(isolation = Isolation.READ_COMMITTED, propagation = Propagation.REQUIRED)
public int grapRedPacketForVersion(Long redPacketId, Long userId) {
    // Get red‑packet info
    RedPacket redPacket = redPacketDao.getRedPacket(redPacketId);
    if (redPacket.getStock() > 0) {
        int update = redPacketDao.decreaseRedPacketForVersion(redPacketId, redPacket.getVersion());
        if (update == 0) {
            return FAILED; // version conflict
        }
        UserRedPacket userRedPacket = new UserRedPacket();
        userRedPacket.setRedPacketId(redPacketId);
        userRedPacket.setUserId(userId);
        userRedPacket.setAmount(redPacket.getUnitAmount());
        userRedPacket.setNote("redpacket- " + redPacketId);
        int result = userRedPacketDao.grapRedPacket(userRedPacket);
        return result;
    }
    return FAILED; // out of stock
}

Controller Layer

Added a new endpoint to trigger the version‑based grab.

@RequestMapping(value = "/grapRedPacketForVersion")
@ResponseBody
public Map<String, Object> grapRedPacketForVersion(Long redPacketId, Long userId) {
    int result = userRedPacketService.grapRedPacketForVersion(redPacketId, userId);
    Map<String, Object> retMap = new HashMap<>();
    boolean flag = result > 0;
    retMap.put("success", flag);
    retMap.put("message", flag ? "抢红包成功" : "抢红包失败");
    return retMap;
}

View Layer

A simple JSP page (grapForVersion.jsp) is created to simulate 30,000 concurrent POST requests using jQuery.

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>
<!DOCTYPE html>
<html>
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    <title>参数</title>
    <script src="https://code.jquery.com/jquery-3.2.0.js"></script>
    <script type="text/javascript">
        $(document).ready(function () {
            var max = 30000;
            for (var i = 1; i <= max; i++) {
                $.post({
                    url: "./userRedPacket/grapRedPacketForVersion.do?redPacketId=1&userId=" + i,
                    success: function (result) {}
                });
            }
        });
    </script>
</head>
<body>
</body>
</html>

Test Results

After 30,000 attempts, 7,521 red packets were successfully grabbed, leaving 12,479 unclaimed, indicating many failures due to version mismatches.

Improving Success Rate with Retry Mechanisms

Two retry strategies are introduced:

Timestamp‑based retry: keep trying within a 100 ms window.

Count‑based retry: limit to 3 attempts.

Timestamp‑Based Retry Implementation

@Override
@Transactional(isolation = Isolation.READ_COMMITTED, propagation = Propagation.REQUIRED)
public int grapRedPacketForVersion(Long redPacketId, Long userId) {
    long start = System.currentTimeMillis();
    while (true) {
        long now = System.currentTimeMillis();
        if (now - start > 100) return FAILED;
        RedPacket redPacket = redPacketDao.getRedPacket(redPacketId);
        if (redPacket.getStock() > 0) {
            int update = redPacketDao.decreaseRedPacketForVersion(redPacketId, redPacket.getVersion());
            if (update == 0) continue; // version conflict, retry
            // create and insert UserRedPacket ...
            return result;
        } else {
            return FAILED;
        }
    }
}

Testing shows all red packets are grabbed without over‑issuance, and failure rate drops dramatically.

Count‑Based Retry Implementation

@Override
@Transactional(isolation = Isolation.READ_COMMITTED, propagation = Propagation.REQUIRED)
public int grapRedPacketForVersion(Long redPacketId, Long userId) {
    for (int i = 0; i < 3; i++) {
        RedPacket redPacket = redPacketDao.getRedPacket(redPacketId);
        if (redPacket.getStock() > 0) {
            int update = redPacketDao.decreaseRedPacketForVersion(redPacketId, redPacket.getVersion());
            if (update == 0) continue; // version conflict, retry
            // create and insert UserRedPacket ...
            return result;
        } else {
            return FAILED;
        }
    }
    return FAILED;
}

Again, 30,000 concurrent requests successfully consume all packets without over‑issuance.

Future Work

The article mentions exploring Redis + Lua scripts for even higher performance, with links to related Redis transaction tutorials.

Source code is available at https://github.com/yangshangwei/ssm_redpacket .

Feel free to like and share if you find this tutorial helpful.

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.

BackendJavaconcurrencyCASred packetoptimistic locking
Java Captain
Written by

Java Captain

Focused on Java technologies: SSM, the Spring ecosystem, microservices, MySQL, MyCat, clustering, distributed systems, middleware, Linux, networking, multithreading; occasionally covers DevOps tools like Jenkins, Nexus, Docker, ELK; shares practical tech insights and is dedicated to full‑stack Java development.

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.