形而上者谓之道;形而下者谓之器。(《周易·系辞上》)
本文概述
- 掌握红包的两种常见生成算法
- 掌握lua+redis 实现原子性抢红包
- 项目中还有mysql相关内容
- 了解jmeter的基本用法
- 遗留问题
- redis同步DB时机问题
红包生成算法
普通随机方法
该方法的原理是:每次都以 [最小值,剩余金额值] 之间进行随机取值。
假设红包金额为 88.88,红包数量为 8 个
- 第一个人领取金额将从 [0.01, 88.88] 之间进行取值,假设取值为 20.20,那么剩余的金额为 68.68。
- 第二个领取金额将从 [0,01, 68.68] 之间进行取值,
- 以此类推…
这里可以明显看出此方法的弊端,前面领取红包的金额区间更大,也就更容易获取更大的红包金额。下面看二倍均值法的原理。
二倍均值法 — 公平版
原理:每次以 [最小值,红包剩余金额 / 人数 * 2] 的区间进行取值。
假设100元红包发10个人,那么合理的做法应该是每个人领到10元的概率相同。
第一个人随机金额的范围为[0,100/10×2] ,也就是[0,20],这样平均可以领到10元,此时剩余金额为100-10=90。
第二个人随机金额的范围为[0,90/9×2] ,也就是[0,20],这样平均也可以领到10元,此时剩余金额为90-10=80。
第三个人随机金额的范围为[0,80/8×2] ,也就是[0,20],这样平均也可以领到10元。
该方法也不是完美的,上述是非常理想情况下红包的领取金额,同时每个人获取金额区间相对公平。但是当其中一个人在区间取值接近最小值或者最大值都会对后面的区间造成影响。当取到接近最小值时,后面领取红包金额区间将会变大;反之,则变小。这也是该方法的弊端。
截线段法 — 拼手速版
假设
- 问题:十人分一段十米长绳子,先到先截取。
- 前提:人人理性、利己
- 分析:
- 第一个人可以直接拿走,问题结束。这样太没意思了,就算拿5m;
- 第二个人最多可以拿5m;
- 一次类推,越往后,可选择越短,直到没了绳子。
代码部分
随机整数 【含最大值,含最小值】
function randomInt(min: number, max: number) { min = Math.ceil(min); max = Math.floor(max); return Math.floor(Math.random() * (max - min + 1)) + min; }
|
二倍均值法
export function fairMethod(totalAmount: number, redPacketNum: number) { if (totalAmount < 0 && redPacketNum < 0) throw '金额错误'; let curTotalAmount = totalAmount; if (curTotalAmount / redPacketNum < 1) throw '最低每人0.1元'; const min = 1; let _redPacketNum = redPacketNum; const reslut: number[] = []; while (_redPacketNum > 1) { const max = (curTotalAmount / _redPacketNum) * 2; const amount = randomInt(min, max); curTotalAmount -= amount; _redPacketNum--; reslut.push(amount); } reslut.push(curTotalAmount); return reslut; }
|
拼手速版本
export function speedMethod(totalAmount: number, redPacketNum: number) { if (totalAmount < 0 && redPacketNum < 0) throw '金额错误'; const curTotalAmount = totalAmount; if (curTotalAmount / redPacketNum < 1) throw '最低每人0.1元'; let [ begin, end ] = [ 0, curTotalAmount ]; let _redPacketNum = redPacketNum; const result: number[] = []; while (_redPacketNum > 1) { if (begin === end) { result.push(0); } else { if (end === begin + 1) { begin++; result.push(1); } else { const randomPoint = randomInt(begin, end); const amount = randomPoint - begin; begin += amount; result.push(amount); } } _redPacketNum--; } result.push(end - begin); return result; }
|
测试结果
redis+lua 抢红包实现
有了就红包算法,结合 redis+lua, 就可以实现一个抢红包功能了
整体步骤
- 生成红包数据
- 插入红包表
DB
- 生成红包算法
- 生成红包&用户表
DB
- 插入待消费队列
redis
- redis相关
- 待消费队列: 生成红包时插入redis
- 集合 set: 存储抢红包的用户ID;
- 消费队列: 从
待消费队列
pop消费队列
- 最后同步到:红包&用户表
代码部分
支持三种类型的红包生成方式
- 公平版
- 手速版
- 固定版
lua
写代码离不开debug, lua也不例外,不会的左转上篇文章有调试的教程。
local judge = redis.call("SISMEMBER", KEYS[3], ARGV[1]) if judge ~= 0 then return 100 else local item = redis.call("RPOP", KEYS[1]) if item then local _item = cjson.decode(item) _item["userId"] = ARGV[1] local newItem = cjson.encode(_item) redis.call("SADD", KEYS[3], ARGV[1]) redis.call("LPUSH", KEYS[2], newItem) return newItem else return 200 end end return nil
|
抢红包
public async getRedPacket_redis_lua(packetId: number, userId: number) { const redisDefalueKey = `${RP_DEFALUT_LIST}${packetId}`; const redisConsumeKey = `${RP_CONSUME_LIST}${packetId}`; const redisSetKey = `${RP_USER_SET}${packetId}`; const filePath = path.join(__dirname, '../bin/redpacket.lua'); const luaScript = fs.readFileSync(filePath, "utf8"); const result = await this.app.redis.eval(luaScript, 3, redisDefalueKey, redisConsumeKey, redisSetKey, userId); if(result === 100){ console.log('已经抢过了~', userId) } if(result === 200){ console.log('红包空了', userId) } if(typeof result === 'string') { console.log('抢红包成功', result) } return result; }
|
测试
redis 数据结构
jmeter 测试
抢红包成功 {"id":71,"userId":"16","rpId":9,"amount":17} 抢红包成功 {"id":72,"userId":"5","rpId":9,"amount":14} 抢红包成功 {"id":73,"userId":"18","rpId":9,"amount":11} 已经抢过了~ 18 抢红包成功 {"id":74,"userId":"19","rpId":9,"amount":16} 抢红包成功 {"id":75,"userId":"13","rpId":9,"amount":10} 抢红包成功 {"id":76,"userId":"1","rpId":9,"amount":1} 抢红包成功 {"id":77,"userId":"4","rpId":9,"amount":15} 抢红包成功 {"id":78,"userId":"14","rpId":9,"amount":2} 抢红包成功 {"id":79,"userId":"3","rpId":9,"amount":6} 已经抢过了~ 13 已经抢过了~ 13 已经抢过了~ 19 抢红包成功 {"id":80,"userId":"8","rpId":9,"amount":8} 红包空了 15 已经抢过了~ 8 已经抢过了~ 1 已经抢过了~ 19 已经抢过了~ 4 红包空了 6 红包空了 7
|
jmeter的强大功能后续再摸索
源码
因为后续还会往这个项目中加其他redis业务,采用 egg+ts
https://github.com/simuty/Integration/blob/main/Redis/
参考
并发 - 利用redis + lua解决抢红包高并发的问题
[高并发]抢红包设计(使用redis)
抢红包算法–四种抢红包算法对比
抢红包的红包生成算法
微信红包生成算法
抢红包算法(公平版和手速版)
红包分配算法