Implementing a Red Packet System with Redis, Lua Scripts, and Spring Async

This article explains how to build a high‑performance red‑packet (hong‑bao) service by configuring a RedisTemplate in Spring, writing an atomic Lua script for stock management, persisting results to a MySQL database via JDBC batch processing, and using @Async to off‑load long‑running tasks, complete with code examples and deployment steps.

Java Captain
Java Captain
Java Captain
Implementing a Red Packet System with Redis, Lua Scripts, and Spring Async

Previous Articles

Red Packet Case Study and Code (Part 1)

Red Packet Case Study and Code (Part 2)

Red Packet Case Study and Code (Part 3)

These three posts originally stored data in MySQL, but this article switches to Redis for in‑memory speed while still persisting final results to disk.

Redis offers faster access than disk‑based databases, but it lacks full transactional guarantees; therefore data correctness is ensured through strict validation and the atomic nature of Lua scripts.

Because Redis is primarily a cache, the red‑packet data is eventually written back to MySQL when the amount reaches zero or the packet expires.

Implementation Steps

Configure RedisTemplate via Annotations

Define a RedisTemplate bean in a @Configuration class and register it in the Spring IoC container.

/**
 * 创建一个 RedisTemplate 对象
 */
@Bean(name = "redisTemplate")
public RedisTemplate initRedisTemplate() {
    JedisPoolConfig poolConfig = new JedisPoolConfig();
    // 最大空闲数
    poolConfig.setMaxIdle(50);
    // 最大连接数
    poolConfig.setMaxTotal(100);
    // 最大等待毫秒数
    poolConfig.setMaxWaitMillis(20000);
    // 创建Jedis链接工厂
    JedisConnectionFactory connectionFactory = new JedisConnectionFactory(poolConfig);
    connectionFactory.setHostName("192.168.31.66");
    connectionFactory.setPort(6379);
    // 调用后初始化方法,没有它将抛出异常
    connectionFactory.afterPropertiesSet();
    // 自定Redis序列化器
    RedisSerializer jdkSerializationRedisSerializer = new JdkSerializationRedisSerializer();
    RedisSerializer stringRedisSerializer = new StringRedisSerializer();
    // 定义RedisTemplate,并设置连接工厂
    RedisTemplate redisTemplate = new RedisTemplate();
    redisTemplate.setConnectionFactory(connectionFactory);
    // 设置序列化器
    redisTemplate.setDefaultSerializer(stringRedisSerializer);
    redisTemplate.setKeySerializer(stringRedisSerializer);
    redisTemplate.setValueSerializer(stringRedisSerializer);
    redisTemplate.setHashKeySerializer(stringRedisSerializer);
    redisTemplate.setHashValueSerializer(stringRedisSerializer);
    return redisTemplate;
}

Note that when the bean is created manually, afterPropertiesSet() must be called to avoid initialization errors.

Lua Script and Asynchronous Persistence

Redis does not provide strict transactions, so a Lua script is used to guarantee atomic stock decrement and list insertion.

--缓存抢红包列表信息列表 key
local listKey = 'red_packet_list_'..KEYS[1]
--当前被抢红包 key
local redPacket = 'red_packet_'..KEYS[1]
--获取当前红包库存
local stock = tonumber(redis.call('hget', redPacket, 'stock'))
--没有库存,返回为 0 
if stock <= 0 then 
  return 0 
end 
--库存减 1
stock = stock-1
--保存当前库存
redis.call('hset', redPacket, 'stock', tostring(stock))
--往链表中加入当前红包信息
redis.call('rpush', listKey, ARGV[1])
--如果是最后一个红包,则返回 2 ,表示抢红包已经结束,需要将列表中的数据保存到数据库中
if stock == 0 then 
  return 2 
end 
--如果并非最后一个红包,则返回 1 ,表示抢红包成功
return 1

The script returns:

0 – no stock, stop.

1 – stock decremented, data stored in Redis list.

2 – last packet taken, trigger asynchronous persistence to MySQL.

Service Layer: Persisting Redis Data to MySQL

When the script returns 2, a new thread (enabled by @Async) reads the Redis list in batches of 1,000 entries and writes them to the database using JDBC batch execution.

package com.artisan.redpacket.service;

public interface RedisRedPacketService {
    /**
     * 保存redis抢红包列表
     * @param redPacketId --抢红包编号
     * @param unitAmount -- 红包金额
     */
    public void saveUserRedPacketByRedis(Long redPacketId, Double unitAmount);
}
package com.artisan.redpacket.service.impl;

import java.sql.Connection;
import java.sql.SQLException;
import java.sql.Statement;
import java.sql.Timestamp;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.List;
import javax.sql.DataSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.BoundListOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;

@Service
public class RedisRedPacketServiceImpl implements RedisRedPacketService {
    private static final String PREFIX = "red_packet_list_";
    private static final int TIME_SIZE = 1000;
    @Autowired
    private RedisTemplate redisTemplate; // RedisTemplate
    @Autowired
    private DataSource dataSource; // 数据源

    @Async
    public void saveUserRedPacketByRedis(Long redPacketId, Double unitAmount) {
        System.err.println("开始保存数据");
        Long start = System.currentTimeMillis();
        BoundListOperations ops = redisTemplate.boundListOps(PREFIX + redPacketId);
        Long size = ops.size();
        Long times = size % TIME_SIZE == 0 ? size / TIME_SIZE : size / TIME_SIZE + 1;
        int count = 0;
        List<UserRedPacket> userRedPacketList = new ArrayList<UserRedPacket>(TIME_SIZE);
        for (int i = 0; i < times; i++) {
            List userIdList = null;
            if (i == 0) {
                userIdList = ops.range(i * TIME_SIZE, (i + 1) * TIME_SIZE);
            } else {
                userIdList = ops.range(i * TIME_SIZE + 1, (i + 1) * TIME_SIZE);
            }
            userRedPacketList.clear();
            for (int j = 0; j < userIdList.size(); j++) {
                String args = userIdList.get(j).toString();
                String[] arr = args.split("-");
                Long userId = Long.parseLong(arr[0]);
                Long time = Long.parseLong(arr[1]);
                UserRedPacket userRedPacket = new UserRedPacket();
                userRedPacket.setRedPacketId(redPacketId);
                userRedPacket.setUserId(userId);
                userRedPacket.setAmount(unitAmount);
                userRedPacket.setGrabTime(new Timestamp(time));
                userRedPacket.setNote("抢红包 " + redPacketId);
                userRedPacketList.add(userRedPacket);
            }
            count += executeBatch(userRedPacketList);
        }
        redisTemplate.delete(PREFIX + redPacketId);
        Long end = System.currentTimeMillis();
        System.err.println("保存数据结束,耗时" + (end - start) + "毫秒,共" + count + "条记录被保存。");
    }

    private int executeBatch(List<UserRedPacket> userRedPacketList) {
        Connection conn = null;
        Statement stmt = null;
        int[] count = null;
        try {
            conn = dataSource.getConnection();
            conn.setAutoCommit(false);
            stmt = conn.createStatement();
            for (UserRedPacket userRedPacket : userRedPacketList) {
                String sql1 = "update T_RED_PACKET set stock = stock-1 where id=" + userRedPacket.getRedPacketId();
                DateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
                String sql2 = "insert into T_USER_RED_PACKET(red_packet_id, user_id, amount, grab_time, note) values (" +
                        userRedPacket.getRedPacketId() + ", " + userRedPacket.getUserId() + ", " +
                        userRedPacket.getAmount() + ", '" + df.format(userRedPacket.getGrabTime()) + "', '" +
                        userRedPacket.getNote() + "')";
                stmt.addBatch(sql1);
                stmt.addBatch(sql2);
            }
            count = stmt.executeBatch();
            conn.commit();
        } catch (SQLException e) {
            throw new RuntimeException("抢红包批量执行程序错误");
        } finally {
            try {
                if (conn != null && !conn.isClosed()) {
                    conn.close();
                }
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
        return count.length / 2;
    }
}

Controller Endpoint

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

Testing the Whole Flow

Initialize a red‑packet in Redis:

127.0.0.1:6379> HMSET red_packet_1 stock 20000 unit_amount 10
OK

Deploy the web application and open http://localhost:8080/ssm_redpacket/grapByRedis.jsp. The page launches 30,000 concurrent AJAX POST requests to simulate massive users grabbing the packet.

Results show that the Redis‑based solution maintains data consistency while delivering far higher throughput than optimistic or pessimistic locking approaches.

Conclusion

The complete source code is available at https://github.com/yangshangwei/ssm_redpacket . This series demonstrates how to combine Redis atomic Lua scripts, Spring async execution, and JDBC batch writes to build a scalable red‑packet service.

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.

JavaredisspringAsyncLua
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.