Redis迷你版微信抢红包实战

项目概述

本项目使用Redis实现一个简易的微信抢红包系统,模拟真实微信红包的核心功能:创建红包、抢红包、查看红包记录等。

图片[1]_Redis迷你版微信抢红包实战_知途无界

核心技术点

  1. 使用Redis的List结构存储红包
  2. 使用Redis的Hash结构存储用户抢到的红包记录
  3. 使用Lua脚本保证抢红包操作的原子性
  4. 实现红包金额的随机分配算法

系统设计

数据结构设计

  1. 红包信息存储
  • Key: redpacket:{红包ID}
  • Value: Hash结构
    • total_amount: 总金额(分)
    • total_count: 总个数
    • remaining_amount: 剩余金额(分)
    • remaining_count: 剩余个数
    • creator: 创建者ID
    • create_time: 创建时间
    • type: 红包类型(普通/拼手气)
  1. 红包队列
  • Key: redpacket:queue:{红包ID}
  • Value: List结构(仅用于普通红包,存储固定金额)
  1. 用户抢到的红包记录
  • Key: user:redpacket:{用户ID}
  • Value: Hash结构
    • {红包ID}: 抢到的金额(分)

抢红包流程

  1. 用户请求抢红包
  2. 检查红包是否存在且还有剩余
  3. 使用Lua脚本保证原子性操作:
  • 检查剩余数量
  • 计算本次抢到的金额(拼手气红包)
  • 更新剩余金额和数量
  • 将抢到的红包记录到用户Hash中
  1. 返回抢红包结果

代码实现

1. 创建红包(Lua脚本)

-- KEYS[1]: redpacket:{红包ID}
-- ARGV[1]: 总金额(分)
-- ARGV[2]: 总个数
-- ARGV[3]: 创建者ID
-- ARGV[4]: 红包类型(0-普通,1-拼手气)
-- ARGV[5]: 当前时间戳

local redpacketKey = KEYS[1]
local totalAmount = tonumber(ARGV[1])
local totalCount = tonumber(ARGV[2])
local creator = ARGV[3]
local redpacketType = tonumber(ARGV[4])
local now = tonumber(ARGV[5])

if totalCount <= 0 or totalAmount <= 0 then
    return {err = "Invalid amount or count"}
end

local remainingAmount = totalAmount
local remainingCount = totalCount

if redpacketType == 0 then -- 普通红包
    local amountPerPacket = math.floor(totalAmount / totalCount)
    remainingAmount = amountPerPacket * totalCount -- 可能会有舍去
    if remainingAmount < totalAmount then
        remainingAmount = totalAmount -- 如果舍去后不足,还是按原金额
    end
end

redis.call('HMSET', redpacketKey, 
    'total_amount', totalAmount,
    'total_count', totalCount,
    'remaining_amount', remainingAmount,
    'remaining_count', remainingCount,
    'creator', creator,
    'create_time', now,
    'type', redpacketType
)

if redpacketType == 0 then -- 普通红包,预存金额到队列
    for i=1, totalCount do
        redis.call('LPUSH', 'redpacket:queue:'..redpacketKey, amountPerPacket)
    end
else -- 拼手气红包,只存元数据
    -- 不需要预存金额
end

return {success = true, redpacket_id = redpacketKey}

2. 抢红包(Lua脚本)

-- KEYS[1]: redpacket:{红包ID}
-- KEYS[2]: user:redpacket:{用户ID}
-- ARGV[1]: 用户ID
-- ARGV[2]: 当前时间戳

local redpacketKey = KEYS[1]
local userRedpacketKey = KEYS[2]
local userId = ARGV[1]
local now = tonumber(ARGV[2])

-- 检查红包是否存在
if not redis.call('EXISTS', redpacketKey) == 1 then
    return {err = "Red packet not exists"}
end

-- 获取红包信息
local redpacketInfo = redis.call('HMGET', redpacketKey, 
    'total_amount', 'total_count', 'remaining_amount', 'remaining_count', 'type')

local totalAmount = tonumber(redpacketInfo[1])
local totalCount = tonumber(redpacketInfo[2])
local remainingAmount = tonumber(redpacketInfo[3])
local remainingCount = tonumber(redpacketInfo[4])
local redpacketType = tonumber(redpacketInfo[5])

-- 检查红包是否已抢完
if remainingCount <= 0 then
    return {err = "Red packet is empty"}
end

-- 计算本次抢到的金额
local amount = 0
if redpacketType == 0 then -- 普通红包
    amount = tonumber(redis.call('RPOP', 'redpacket:queue:'..redpacketKey))
    if not amount then
        return {err = "Failed to get amount from queue"}
    end
else -- 拼手气红包
    if remainingCount == remainingAmount then -- 第一个人抢到最大金额
        amount = math.floor(remainingAmount * 100) / 100 -- 避免浮点数精度问题
    else
        -- 随机金额算法: min(200分,剩余金额/剩余人数*2)
        local max = math.min(200, math.floor(remainingAmount / remainingCount * 2))
        amount = math.random(1, max)
        amount = math.floor(amount * 100) / 100 -- 避免浮点数精度问题
        -- 确保最后一个人能拿到剩余金额
        if remainingCount == 1 then
            amount = remainingAmount
        end
    end
    amount = math.floor(amount * 100) -- 转换为分

    -- 确保不会超发
    if amount > remainingAmount then
        amount = remainingAmount
    end

    -- 计算新的剩余金额
    remainingAmount = remainingAmount - amount
    if remainingAmount < 0 then
        remainingAmount = 0
    end
end

-- 减少剩余数量
remainingCount = remainingCount - 1

-- 更新红包信息
redis.call('HMSET', redpacketKey, 
    'remaining_amount', remainingAmount,
    'remaining_count', remainingCount
)

-- 记录用户抢到的红包
redis.call('HSET', userRedpacketKey, redpacketKey, math.floor(amount * 100)) -- 存储为分

-- 返回抢红包结果
return {
    success = true,
    amount = math.floor(amount * 100), -- 返回单位为分
    remaining_count = remainingCount
}

3. 获取红包详情

def get_redpacket_detail(redpacket_id):
    redpacket_key = f"redpacket:{redpacket_id}"
    if not redis.exists(redpacket_key):
        return {"error": "Red packet not exists"}

    redpacket_info = redis.hgetall(redpacket_key)
    return {
        "id": redpacket_id,
        "total_amount": int(redpacket_info.get(b'total_amount', 0)) / 100,
        "total_count": int(redpacket_info.get(b'total_count', 0)),
        "remaining_amount": int(redpacket_info.get(b'remaining_amount', 0)) / 100,
        "remaining_count": int(redpacket_info.get(b'remaining_count', 0)),
        "creator": redpacket_info.get(b'creator', b'').decode(),
        "create_time": int(redpacket_info.get(b'create_time', 0)),
        "type": int(redpacket_info.get(b'type', 0))
    }

4. 获取用户抢到的红包

def get_user_redpackets(user_id):
    user_redpacket_key = f"user:redpacket:{user_id}"
    if not redis.exists(user_redpacket_key):
        return []

    redpacket_ids = redis.hkeys(user_redpacket_key)
    result = []

    for rp_id in redpacket_ids:
        rp_id = rp_id.decode()
        amount = int(redis.hget(user_redpacket_key, rp_id) or 0)
        # 这里可以补充获取红包的其他信息
        result.append({
            "redpacket_id": rp_id,
            "amount": amount / 100  # 转换为元
        })

    return result

测试用例

import unittest
import redis
import time

class TestRedPacket(unittest.TestCase):
    def setUp(self):
        self.redis = redis.StrictRedis(host='localhost', port=6379, db=0)
        self.redpacket_id = "rp_123456"
        self.creator_id = "user_1"
        self.user_id = "user_2"

        # 创建红包
        now = int(time.time())
        script = """
        -- [上面的创建红包Lua脚本]
        """
        self.redis.eval(script, 1, f"redpacket:{self.redpacket_id}", 
                       1000, 3, self.creator_id, 0, now)  # 创建一个10元的普通红包(1000分)

    def test_grab_redpacket(self):
        # 抢红包
        script = """
        -- [上面的抢红包Lua脚本]
        """
        result = self.redis.eval(script, 2, 
                                f"redpacket:{self.redpacket_id}", 
                                f"user:redpacket:{self.user_id}",
                                self.user_id, 
                                int(time.time()))

        self.assertTrue(result.get("success"))
        self.assertEqual(result.get("remaining_count"), 2)

        # 再抢一次
        result2 = self.redis.eval(script, 2, 
                                f"redpacket:{self.redpacket_id}", 
                                f"user:redpacket:{self.user_id}_2",
                                "user_2", 
                                int(time.time()))

        self.assertEqual(result2.get("remaining_count"), 1)

    def tearDown(self):
        self.redis.delete(f"redpacket:{self.redpacket_id}")
        self.redis.delete(f"user:redpacket:{self.user_id}")
        self.redis.delete(f"user:redpacket:{self.user_id}_2")

if __name__ == "__main__":
    unittest.main()

优化与扩展

  1. 金额精度处理:使用分为单位存储,避免浮点数精度问题
  2. 并发控制:Lua脚本保证原子性操作
  3. 红包过期处理:设置过期时间,自动清理未抢完的红包
  4. 消息通知:抢到红包后通知用户
  5. 日志记录:记录抢红包的操作日志
  6. 性能优化:对于拼手气红包,可以考虑预分配金额范围

部署建议

  1. 使用Redis集群提高可用性
  2. 对高频操作(抢红包)进行缓存优化
  3. 考虑使用Redis事务或Lua脚本保证数据一致性
  4. 监控Redis内存使用情况

这个简易版微信抢红包系统展示了如何使用Redis实现高并发场景下的红包功能,实际生产环境还需要考虑更多细节和优化。

© 版权声明
THE END
喜欢就点个赞,支持一下吧!
点赞33 分享
评论 抢沙发
头像
欢迎您留下评论!
提交
头像

昵称

取消
昵称表情代码图片

    暂无评论内容