项目概述
本项目使用Redis实现一个简易的微信抢红包系统,模拟真实微信红包的核心功能:创建红包、抢红包、查看红包记录等。
![图片[1]_Redis迷你版微信抢红包实战_知途无界](https://zhituwujie.com/wp-content/uploads/2025/05/d2b5ca33bd20250526091401.png)
核心技术点
- 使用Redis的List结构存储红包
- 使用Redis的Hash结构存储用户抢到的红包记录
- 使用Lua脚本保证抢红包操作的原子性
- 实现红包金额的随机分配算法
系统设计
数据结构设计
- 红包信息存储
- Key:
redpacket:{红包ID} - Value: Hash结构
total_amount: 总金额(分)total_count: 总个数remaining_amount: 剩余金额(分)remaining_count: 剩余个数creator: 创建者IDcreate_time: 创建时间type: 红包类型(普通/拼手气)
- 红包队列
- Key:
redpacket:queue:{红包ID} - Value: List结构(仅用于普通红包,存储固定金额)
- 用户抢到的红包记录
- Key:
user:redpacket:{用户ID} - Value: Hash结构
{红包ID}: 抢到的金额(分)
抢红包流程
- 用户请求抢红包
- 检查红包是否存在且还有剩余
- 使用Lua脚本保证原子性操作:
- 检查剩余数量
- 计算本次抢到的金额(拼手气红包)
- 更新剩余金额和数量
- 将抢到的红包记录到用户Hash中
- 返回抢红包结果
代码实现
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()
优化与扩展
- 金额精度处理:使用分为单位存储,避免浮点数精度问题
- 并发控制:Lua脚本保证原子性操作
- 红包过期处理:设置过期时间,自动清理未抢完的红包
- 消息通知:抢到红包后通知用户
- 日志记录:记录抢红包的操作日志
- 性能优化:对于拼手气红包,可以考虑预分配金额范围
部署建议
- 使用Redis集群提高可用性
- 对高频操作(抢红包)进行缓存优化
- 考虑使用Redis事务或Lua脚本保证数据一致性
- 监控Redis内存使用情况
这个简易版微信抢红包系统展示了如何使用Redis实现高并发场景下的红包功能,实际生产环境还需要考虑更多细节和优化。
© 版权声明
文中内容均来源于公开资料,受限于信息的时效性和复杂性,可能存在误差或遗漏。我们已尽力确保内容的准确性,但对于因信息变更或错误导致的任何后果,本站不承担任何责任。如需引用本文内容,请注明出处并尊重原作者的版权。
THE END

























暂无评论内容