Fading Coder

One Final Commit for the Last Sprint

Home > Tech > Content

Implementing a Red Packet Rain System with Redis and Lua

Tech May 13 1

In 2018, live quiz shows like Wang Sicong's "Chongding Dahui," Xigua Video's "Million Heroes," and Inke's "Cheese Superman" became wildly popular. An e-commerce company I worked for joined this trend, and the tech team developed a live quiz feature. After a quiz ended, red packets would fall as a "red packet rain." Users could click on these falling packets, and if they managed to grab one, the cash amount would be credited to their account.

A red packet rain is a classic high-concurrency scenario, with a massive number of requests hitting the server in a short time. To ensure smooth system operation, the tech team designed the red packet grabbing mechanism based on Redis + Lua scripts.

1. Overall Process

Let's analyze the overall flow of grabbing a red packet:

  1. The operations system configures the total amount and the number of packets for the rain event, pre-calculates individual packet amounts, and stores them in Redis.
  2. On the red packet rain interface, users tap on falling packets to initiate a grab request.
  3. A TCP gateway receives the request and invokes the red packet grabbing dubbo service (from the quiz system). This service essentially executes a Lua script and returns the result via the TCP gateway to the frontend.
  4. If a user successfully grabs a packet, an asynchronous task retrieves the packet details from Redis, calls the balance system, and deposits the amount into the user's account.

2. Redis Data Design for Red Packets

The red packet grabbing rules are:

  • A user can only grab one packet per event.
  • The number of packets is limited, and each packet can only be grabbed by one user.

Three data structures are designed:

  1. Pre-allocated red packet list (operational): A list containing pre-generated red packet data. Each element is a JSON object:

    {
        "redPacketId": "365628617880842241",
        "amount": "12.21"
    }
    
  2. User red packet claim record list: A list storing the claim history. Each element is:

    {
        "redPacketId": "365628617880842241",
        "amount": "12.21",
        "userId": "265628617882842248"
    }
    
  3. User red packet deduplication Hash table: A hash table used to prevent duplicate claims. Key: user ID, value: red packet ID.

Redis operations for grabbing a packet:

  1. Use HEXISTS to check if the user has already claimed a packet. If not, proceed.
  2. Use RPOP to pop a red packet entry from the pre-allocated list.
  3. Use HSET to store the user's claim record in the deduplication hash.
  4. Use LPUSH to add the claim record to the user claim history list.

Key concerns during this process:

  • Atomicity: executing multiple commands must be atomic. If one command fails, can we roll back?
  • Isolation: In high concurrency, can we maintain isolation between operations?
  • The subsequent steps depend on the results of previous steps.

Redis supports two modes for this: transactions and Lua scripts. Let's explore both.

3. Transaction Mechanism

Redis transactions use the following commands:

Command Description
MULTI Marks the start of a transaction block.
EXEC Executes all commands in the transaction block.
DISCARD Cancels the transaction and discards all queued commands.
WATCH Monitors one or more keys. If any monitored key is modified before EXEC, the transaction is interrupted.
UNWATCH Cancels the monitoring of all keys.

A transaction has three phases:

  1. Begin: MULTI switches the client to transaction state.
  2. Queue: After MULTI, commands are not executed immediately but are queued.
  3. Execute or Discard: EXEC executes the queue; DISCARD discards it.

Example:

redis> MULTI
OK
redis> SET msg "hello world"
QUEUED
redis> GET msg
QUEUED
redis> EXEC
1) OK
2) "hello world"

A critical point: Redis keys can be modified before EXEC is called. To achieve an optimistic locking effect, use WATCH. If a watched key is modified before EXEC, the transaction will fail.

Example with WATCH:

redis> WATCH msg
OK
redis> MULTI
OK
redis> SET msg "new value"
QUEUED
redis> EXEC
(nil)  -- because msg was changed by another client before EXEC

4. ACID Properties of Redis Transactions

4.1 Atomicity

Atomicity means that all operations in a transaction are completed successfully, or none are. Errors cause a rollback to the initial state.

Case 1: Syntax error before EXEC

If a client sends a command with a syntax error or an unknown command before EXEC, the entire transaction fails.

redis> MULTI
OK
redis> SET msg "other msg"
QUEUED
redis> wrongcommand  --- intentional error
(error) ERR unknown command 'wrongcommand'
redis> EXEC
(error) EXECABORT Transaction discarded because of previous errors.
redis> GET msg
"hello world"

In this case, atomicity is preserved – the transaction is discarded.

Case 2: Type error during EXEC

If commands are queued successfully but a type error occurs during EXEC (e.g., using HMSET on a string), Redis does not roll back. It continues executing the remaining commands.

redis> MULTI
OK
redis> SET mystring "I am a string"
QUEUED
redis> HMSET mystring name "test"
QUEUED
redis> SET msg "after"
QUEUED
redis> EXEC
1) OK
2) (error) WRONGTYPE Operation against a key holding the wrong kind of value
3) OK
redis> GET msg
"after"

In this case, atomicity is not guaranteed.

Summary: Redis transactions provide atomicity only under certain conditions (errors before EXEC). They do not support rollback.

4.2 Isolation

In database terms, isolation ensures that concurrent transactions do not interfere with eachother. Redis does not have transaction isolation levels, but we can consider isolation in the context of concurrent operations.

  • Before EXEC: Use WATCH to implement optimistic locking. If a watched key is changed, the transaction aborts.
  • After EXEC: Redis processes commands in a single thread. Once EXEC is called, all commands in the queue are executed sequentially. This ensures isolation during execution.

Conclusion: Redis transactions can provide isolation when used with WATCH.

4.3 Durability

Durability ensures that once a transaction completes, changes persist even in the event of a system failure.

  • No persistence (RDB/AOF disabled): No durability.
  • RDB mode: Data may be lost if a crash occurs between the last snapshot and the transaction.
  • AOF mode: always provides strong (but slow) durability; everysec and no can lose data.

Conclusion: Redis transactions do not guarantee durability.

4.4 Consistency

The definition of consistency in ACID is often debated.

Wikipedia Definition:

Consistency ensures that a transaction can only bring the database from one valid state to another, maintaining database invariants: any data written to the database must be valid according to all defined rules, including constraints, cascades, triggers, and any combination thereof.

Under this definition, consistency is about constraints. If constraints are maintained, the database is consistent. Redis transactions maintain Redis constraints (e.g., type constraints), but they do not handle application-level constraints. For example, a transfer that results in a negative balance is still "consistent" from the database perspective if no constraint prohibits negative values.

Application-defined Consistency:

Consistency (in the ACID sense) is a property of the application.

In practice, achieving application-level consistency requires atomicity, isolation, durability, and application logic. Redis transactions alone cannot guarantee application consistency.

Conclusion for our scenario: Redis transactions can maintain database-level consistency (constraints), but they are insufficient for complex business logic.

4.5 Summary

Redis transactions provide:

  • Isolation (with WATCH)
  • No durability
  • Limited atomicity (no rollback)
  • Consistency only at the database constraint level

For the red packet rain scenario, where each step depends on the previous result, we would need to use WATCH for optimistic locking, which is cumbersome. Lua scripts are a better fit.

5. Lua Scripting in Redis

5.1 Introduction

Lua is a lightweight, embeddable scripting language. Redis has included a Lua interpreter since version 2.6.0, allowing scripts to be executed atomically on the server.

Benefits of using Lua scripts:

  • Reduced network overhead: Multiple commands can be sent in one script.
  • Atomic execution: The entire script runs atomically without interference.
  • Reusability: Scripts can be cached (using SCRIPT LOAD) and executed by their SHA1 hash.

Common Lua commands in Redis:

Command Description
EVAL Execute a Lua script.
EVALSHA Execute a cached Lua script by its SHA1 hash.
SCRIPT EXISTS Check if a script exists in the cache.
SCRIPT FLUSH Remove all scripts from the cache.
SCRIPT KILL Kill a currently running Lua script.
SCRIPT LOAD Load a script into the cache without executing it.

5.2 EVAL Command

Syntax:

EVAL script numkeys key [key ...] arg [arg ...]
  • script: Lua 5.1 script.
  • numkeys: Number of keys.
  • key...: Keys, accessible as KEYS[1], KEYS[2], etc.
  • arg...: Arguments, accessible as ARGV[1], ARGV[2], etc.

Examples:

redis> EVAL "return ARGV[1]" 0 100
"100"
redis> EVAL "return {KEYS[1], ARGV[1]}" 1 mykey hello
1) "mykey"
2) "hello"

To execute Redis commands from Lua, use redis.call():

redis> SET mykey "hello"
OK
redis> EVAL "return redis.call('GET', KEYS[1])" 1 mykey
"hello"

5.3 EVALSHA Command

To avoid sending long scripts repeatedly, cache them with SCRIPT LOAD:

redis> SCRIPT LOAD "return 'hello world'"
"5332031c6b470dc5a0dd9b4bf2030dea6d65de91"
redis> EVALSHA 5332031c6b470dc5a0dd9b4bf2030dea6d65de91 0
"hello world"

5.4 Transactions vs. Lua Scripts

"From the point of view of Redis, a script is a transaction. Anything you can do with a transaction, you can do with a script. And in general, scripts are simpler and faster."

Lua scripts are an alternative way to achieve transactional behavior. They provide:

  • Atomicity (but no rollback on errors)
  • Isolation (script runs uninterrupted)
  • Ability to use results of previous commands

For the red packet rain, Lua scripts are the optimal solution.

Caveats:

  • Keep scripts simple and fast to avoid blocking Redis.
  • Test thoroughly, as there is no rollback.

6. Practical Preparation

We'll use Redisson 3.12.0 as the Redis client, with a thin wrapper.

Create a PlatformScriptCommand class to execute Lua scripts:

// Load a Lua script
String scriptLoad(String luaScript);

// Execute a script
Object eval(String shardingKey, 
            String luaScript, 
            ReturnType returnType,
            List<Object> keys, 
            Object... values);

// Execute a cached script by SHA1
Object evalSha(String shardingKey, 
               String shaDigest,
               List<Object> keys, 
               Object... values);

The shardingKey is necessary for Redis cluster mode to determine which node executes the script (based on hash slot).

7. Red Packet Grabbing Script

The Lua script returns a JSON string.

Success:

{
    "code": "0",
    "amount": "7.1",
    "redPacketId": "162339217730846210"
}

User already claimed:

{
    "code": "1"
}

No more packets (failure):

{
    "code": "-1"
}

Lua script using cjson:

-- KEYS[1]: deduplication hash key (user claim records)
local userHashKey = KEYS[1]
-- KEYS[2]: pre-allocated red packet list key
local redPacketOperatingKey = KEYS[2]
-- KEYS[3]: user claim history list key
local userAmountKey = KEYS[3]
-- KEYS[4]: user ID
local userId = KEYS[4]

local result = {}

-- Check if user already claimed
if redis.call('hexists', userHashKey, userId) == 1 then
    result['code'] = '1'
    return cjson.encode(result)
else
    -- Try to pop a red packet
    local redPacket = redis.call('rpop', redPacketOperatingKey)
    if redPacket then
        local data = cjson.decode(redPacket)
        data['userId'] = userId
        -- Add user to dedup hash
        redis.call('hset', userHashKey, userId, data['redPacketId'])
        -- Add to claim history list
        redis.call('lpush', userAmountKey, cjson.encode(data))
        -- Prepare success response
        result['redPacketId'] = data['redPacketId']
        result['code'] = '0'
        result['amount'] = data['amount']
        return cjson.encode(result)
    else
        -- No packets left
        result['code'] = '-1'
        return cjson.encode(result)
    end
end

Debugging tips:

  • Write JUnit tests for the script logic.
  • Use Redis Lua debugger (LDB) from version 3.2 onwards.

8. Asynchronous Tasks

After a user grabs a packet, an asynchronous task processes the claim and updates the balance. We can use a simple message queue pattern on top of Redis.

Consumer class:

String groupName = "userGroup";
String queueName = "userAmountQueue";
RedisMessageQueueBuilder builder = redisClient.getRedisMessageQueueBuilder();
RedisMessageConsumer consumer = new RedisMessageConsumer(groupName, builder);
consumer.subscribe(queueName, new UserAmountMessageListener());
consumer.start();

Listener class:

public class UserAmountMessageListener implements RedisMessageListener {
    @Override
    public RedisConsumeAction onMessage(RedisMessage redisMessage) {
        try {
            String message = (String) redisMessage.getData();
            // TODO: call user balance system
            // Return success
            return RedisConsumeAction.CommitMessage;
        } catch (Exception e) {
            logger.error("Failed to process message", e);
            // Retry later
            return RedisConsumeAction.ReconsumeLater;
        }
    }
}

9. Conclusion

"Learning without practice is shallow; practice without learning is dangerous."

Studying Redis Lua hands-on has taught me a lot. I was surprised to find that Redis transactions don't support rollback. It's humbling to realize how easy it is to make assumptions about technologies we think we know.

No technology is perfect. There are always trade-offs between design and implementation. Understanding these trade-offs is key to making good engineering decisions.

If this article has been helpful, feel free to share it with others. Your support motivates me to write higher-quality content. Thank you!

Tags: Redislua

Related Articles

Understanding Strong and Weak References in Java

Strong References Strong reference are the most prevalent type of object referencing in Java. When an object has a strong reference pointing to it, the garbage collector will not reclaim its memory. F...

Comprehensive Guide to SSTI Explained with Payload Bypass Techniques

Introduction Server-Side Template Injection (SSTI) is a vulnerability in web applications where user input is improper handled within the template engine and executed on the server. This exploit can r...

Implement Image Upload Functionality for Django Integrated TinyMCE Editor

Django’s Admin panel is highly user-friendly, and pairing it with TinyMCE, an effective rich text editor, simplifies content management significantly. Combining the two is particular useful for bloggi...

Leave a Comment

Anonymous

◎Feel free to join the discussion and share your thoughts.