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.
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 update T_RED_PACKET set stock = stock - 1, version = version + 1 where id = #{id} and version = #{version} 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 grapRedPacketForVersion(Long redPacketId, Long userId) { int result = userRedPacketService.grapRedPacketForVersion(redPacketId, userId); Map 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" %> 参数 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.
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.
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.