Redis系列八 抢红包

来都来了 2020-12-08 16:27:05 ⋅ 819 阅读

 

本文概述

  1. 掌握红包的两种常见生成算法
  2. 掌握lua+redis 实现原子性抢红包
  3. 项目中还有mysql相关内容
  4. 了解jmeter的基本用法
  5. 遗留问题
    1. redis同步DB时机问题

红包生成算法

普通随机方法

该方法的原理是:每次都以 [最小值,剩余金额值] 之间进行随机取值。
假设红包金额为 88.88,红包数量为 8 个

  1. 第一个人领取金额将从 [0.01, 88.88] 之间进行取值,假设取值为 20.20,那么剩余的金额为 68.68。
  2. 第二个领取金额将从 [0,01, 68.68] 之间进行取值,
  3. 以此类推…

这里可以明显看出此方法的弊端,前面领取红包的金额区间更大,也就更容易获取更大的红包金额。下面看二倍均值法的原理。

二倍均值法 — 公平版

原理:每次以 [最小值,红包剩余金额 / 人数 * 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元。

该方法也不是完美的,上述是非常理想情况下红包的领取金额,同时每个人获取金额区间相对公平。但是当其中一个人在区间取值接近最小值或者最大值都会对后面的区间造成影响。当取到接近最小值时,后面领取红包金额区间将会变大;反之,则变小。这也是该方法的弊端。

截线段法 — 拼手速版

假设

  1. 问题:十人分一段十米长绳子,先到先截取。
  2. 前提:人人理性、利己
  3. 分析:
    1. 第一个人可以直接拿走,问题结束。这样太没意思了,就算拿5m;
    2. 第二个人最多可以拿5m;
    3. 一次类推,越往后,可选择越短,直到没了绳子。

代码部分

随机整数 【含最大值,含最小值】

function randomInt(min: number, max: number) {
min = Math.ceil(min);
max = Math.floor(max);
return Math.floor(Math.random() * (max - min + 1)) + min;
}
 

二倍均值法

/**
* 1. 二倍均值法
*
* !核心:红包 = 随机([最小值,(红包剩余金额 / 人数 * 2)])
*
*
* @param totalAmount 总金额 单位:分
* @param redPacketNum 总人数
* @return 小红包值【单位:分】
*
*
*/
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;
}
 

拼手速版本

/**
* 拼手速版本
* @param totalAmount 总金额; 单位分
* @param redPacketNum 红包个数
*/
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) {
// 如果红包发完了,就直接返回0
if (begin === end) {
result.push(0);
} else {
// 如果 起止间隔1,得特殊补充一下,否则会少1分,《原因在于math.randomInt(99, 100),总是返回99》
if (end === begin + 1) {
begin++;
result.push(1);
} else {
// 起止位置中 挑个点
const randomPoint = randomInt(begin, end);
// console.log("begin", begin);
// console.log("end", end);
// console.log("randomPoint", randomPoint);
// 亮点之间的距离作为 红包金额
const amount = randomPoint - begin;
// 更改起点
begin += amount;
result.push(amount);
}
}
_redPacketNum--;
}
// 最后一个兜底
result.push(end - begin);
return result;
}
 

测试结果

// !结果
/**
拼手速红包: [ 66, 8, 13, 0, 10, 0, 2, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ] -- 100 - 20
拼手速红包: [ 52, 2, 42, 2, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ] -- 100 - 20
拼手速红包: [ 50, 0, 45, 3, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ] -- 100 - 20
拼手速红包: [ 90, 2, 3, 4, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ] -- 100 - 20
拼手速红包: [ 61, 2, 25, 6, 0, 1, 1, 1, 2, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ] -- 100 - 20

公平红包 : [ 6, 7, 2, 7, 4, 1, 7, 1, 6, 1, 9, 4, 10, 6, 8, 3, 8, 5, 1, 4 ] -- 100 - 20
公平红包 : [ 7, 1, 1, 1, 6, 6, 11, 8, 6, 4, 5, 5, 2, 9, 1, 1, 1, 9, 8, 8 ] -- 100 - 20
公平红包 : [ 6, 5, 8, 5, 6, 7, 7, 3, 7, 6, 4, 7, 1, 1, 1, 5, 1, 11, 4, 5 ] -- 100 - 20
公平红包 : [ 4, 7, 4, 8, 7, 5, 4, 8, 8, 1, 7, 2, 2, 6, 3, 4, 3, 3, 2, 12 ] -- 100 - 20
公平红包 : [ 9, 3, 2, 6, 3, 8, 6, 5, 2, 4, 10, 7, 5, 1, 6, 5, 8, 1, 7, 2 ] -- 100 - 20
*/
 

redis+lua 抢红包实现

有了就红包算法,结合 redis+lua, 就可以实现一个抢红包功能了

整体步骤

采用事先生成小红包的方式。

  1. 生成红包数据
    1. 插入红包表 DB
    2. 生成红包算法
    3. 生成红包&用户表 DB
    4. 插入待消费队列 redis
  2. redis相关
    1. 待消费队列: 生成红包时插入redis
    2. 集合 set: 存储抢红包的用户ID;
    3. 消费队列: 从待消费队列pop消费队列
  3. 最后同步到:红包&用户表

代码部分

支持三种类型的红包生成方式

  1. 公平版
  2. 手速版
  3. 固定版

lua

写代码离不开debug, lua也不例外,不会的左转上篇文章有调试的教程。

-- 函数:尝试获得红包,如果成功,则返回json字符串,如果不成功,则返回空
---
-- 参数:
--- KEYS[1-3] 未消费队列名、已消费的队列名、hset key <集合: 去重用户>
--- ARGV[1] 用户ID
-- 返回值:nil 或者 json字符串
---- {userId: xxx, packetId: xxx, amount: 11} : 用户ID:userId,红包ID:packetId,红包金额:money

-- 用户是否抢过
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] -- 加入用户ID信息
local newItem = cjson.encode(_item)
redis.call("SADD", KEYS[3], ARGV[1]) -- 把用户ID放到去重的set里
redis.call("LPUSH", KEYS[2], newItem) -- 把红包放到已消费队列里
return newItem
else
return 200 -- 红包队列已经为空
end
end
return nil
 

抢红包

/**
* redis+lua抢红包
* @param packetId
* @param userId
*/
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}`;
// lua
const filePath = path.join(__dirname, '../bin/redpacket.lua');
const luaScript = fs.readFileSync(filePath, "utf8");
const result = await this.app.redis.;
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解决抢红包高并发的问题


全部评论: 0

    我有话说:

    Redis系列四 锁

      本文目标 1. 熟悉乐观锁ABA概念 2. 理解掌握redis事务以及watch回滚; 3. 实战redis锁 乐观锁 乐观锁是一种不会阻塞其他线程并发的机制,它不会使用数据库的

    Redis系列六 Lua

      本文目标 学习lua基本语法 能够采用redis+lua lua 基本语法 Lua 是一种轻量小巧的脚本语言,用标准C语言编写并以源代码形式开放, 其设计目的是为了嵌入应用

    Redis系列七 Debug Lua

      调试redis+lua 学了lua的基本语法,了解了redis+lua的配套用法,但是却不知道怎么断点调试。学就学全面点, 官网中有dubug相关说明。地址:Redis Lua

    Redis系列一 基本用法&应用场景

        说明 redis的最基本使用方法以及使用场景。 字符串 // stringasync function stringFun() { const [key

    Redis系列二:位图实战,实现打卡签到

    前言 如果要统计一篇文章的阅读量,可以直接使用 Redis 的 incr 指令来完成。 如果要求阅读量必须按用户去重,那就可以使用 set 来记录阅读了这篇文章的所有用户 id,获取

    Redis系列九 推荐系统-布隆过滤器

      布隆过滤器 概念 布隆过滤器是一种空间利用率较高的概率型数据结构,用来测试一个元素是否在集合中。但是存在一定可能,导致结果误判。当布隆过滤器说某个值存在时,这个值可能不存在;当它说不存在时,那就肯定不存在. 特性: 与哈希表不同,...

    Redis系列四 GEO附近的人

    GEO算法 GeoHash是一种地址编码方法。将二维的空间经纬度数据编码成一个字符串; 地球上的经度范围:[-180, 180],纬度范围:[-90,90]。如果以本初子午线、赤道为界,地球可以分成4个部分。 我们先将平面切割成四个正方形,然...

    为什么单线程的Redis能够达到百万级的QPS?

    作者:在江湖中coding链接:https://juejin.im/post/5e6097846fb9a07c9f3fe744 性能测试报告 查看了下阿里云 Redis 的性能测试报告如下,能够

    Redisson 3.13.6 发布,官方推荐的 Redis 客户端

    Redisson 3.13.6 已发布,这是一个 Java 编写的 Redis 客户端,具备驻内存数据网格(In-Memory Data Grid)功能,并获得了 Redis 的官方推荐

    红旗 Linux 桌面系统发布 v11 预览版,1月10日开放下载

    近日,国产操作系统红旗 Linux 官网上线最新的红旗 Linux 桌面操作系统 v11 社区预览版,更新内容包括多项组件更新和全新的主题界面等。 据悉,最新的红旗 Linux 桌面系统 v11

    商城系统 DBShop V3.0 Beta 发布

    全新重构,首次亮相。 系统简介 DBShop企业级商城系统,使用PHP语言基于Laminas(Zendframework 3) + Doctrine 2 组合框架开发完成。可定制、多终端、多场景、多

    BAT大牛Redis客户端与服务端交互原理

    Redis实例运行在单独的进程中,应用系统Redis客户端)通过Redis协议和Redis Server

    Redis 5.0.11、6.0.11、6.2 发布,修复 32 位系统上的整数溢出

    Redis 同时发布了 5.0.11、6.0.11 和 6.2 版本。对于使用 32 位 Redis 的用户来说,此次更新解决了一个重要的安全问题,即 32 位系统上的整数溢出((CVE-2021

    Martian框架发布 3.0.3 版本,Redis分布式锁

    项目简介 Martian 是一个声明式 API 编程(DAP)框架,可以帮助你快速开发后端服务。 以HttpServer作为 http服务,彻底脱离Tomcat这一类的Web容器和Servlet,同时也让项目减少了几个依赖 声明式API,让Co...

    Redis多线程演进

    Redis作为一个基于内存的缓存系统,一直以高性能著称,因没有上下文切换以及无锁操作,即使在单线程处理情况下,读速度仍可达到11万次/s,写速度达到8.1万次/s。但是,单线程的设计也给Redis

    精品推荐:Redis主从复制

    持久化保证了即使 redis 服务重启也会丢失数据,因为 redis 服务重启后会将硬盘上持久化的数据恢复到内存中,但是当 redis 服务器的硬盘损坏了可能会导致数据丢失,如果通过 redis

    Redis 6.2.2 发布

    Redis 6.2.2 现已发布,该版本升级迫切性程度为高。对于那些使用 ACL 和 pub/sub,CONFIG REWRITE,或遭受性能下降影响的用户来说,详见下文: 修复了