Redis案例-微信抢红包

news2024/11/15 19:48:50

Redis案例-微信抢红包

1、业务描述

​ 微信红包,一个人能发红包,红包的分发规则,红包能被几个人抢,超过24小时没有人领取自动退回原账户,红包详情页面有每个人的抢红包记录,包括金额大小和时间,当前已经抢了几个人剩余红包个数等信息。

2、需求分析

  1. 各种节假日,发红包+抢红包,不用说,100%高并发业务要求,不能用mysql来做;
  2. 一个总的大红包,会有可能拆分成多个小红包,总金额=分金额1+分金额2+分金额3…分金额N;
  3. 每个人只能抢一次,你需要有记录,比如100块钱,被拆分成10个红包发出去,总计有10个红包,抢一个少一个,总数显示(10/6)直到完,需要记录那些人抢到了红包,重复抢作弊不可以;
  4. 有可能还需要你计时,完整抢完,从发出到全部over,耗时多少;
  5. 红包过期,或者群主人品差,没人抢红包,原封不动退回;
  6. 红包过期,剩余金额可能需要回退到发红包主账户下;

由于是高并发不能用mysql来做,只能用redis,那需要redis的什么数据类型?

3、架构设计

  1. 拆分算法

    红包其实就是金额,拆分算法,100块钱,分成10个小红包,金额有可能小概率相同,可能有两个红包都是2.58,如何拆分随机金额设定每个红包里塞多少钱是个问题。

  2. 次数限制

    每个人只能抢一次,不能重复。

  3. 原子性

    每抢走一个红包就减少一个(类似库存减少),那就需要保证库存的原子性,不能加锁实现

关键点

  • 发红包

  • 抢红包

    抢,不加锁且原子性,还能支持高并发,每人一次且有抢红包记录

  • 记红包

    记录每个人抢了多少

  • 拆红包算法

    1. 所有人抢到的金额之和等于红包总金额,不能超过,也不能少于。
    2. 每个人至少抢到一分钱。
    3. 要保证所有人抢到的金额的几率相等。

抢红包业务通用算法,一般业内的红包通用算法

  • 二倍均值法

    剩余红包金额为M,剩余人数为N,那么有以下公式

    每次抢到的金额 = 随机区间 (0到 (剩余红包金额M ➗剩余人数N)X2)

    这个公式,保证了每次随机金额的平均值是相等的,不会因为抢红包的先后顺序而造成不公平。

举个例子:

假设有10人,红包总额为100元

  • 第一次:100➗10 X2 =20,所以第一个人的随机范围是(0,20),平均可以抢到10元。假设的一个人随机抢到了10元,那么剩余金额就是90元;
  • 第二次:90➗9 X2 =20,所以第一个人的随机范围是(0,20),平均可以抢到10元。假设的一个人随机抢到了10元,那么剩余金额就是80元;
  • 第三次:80➗10 X2 =20,所以第一个人的随机范围是(0,20),平均可以抢到10元。假设的一个人随机抢到了10元,以此类推…

分析一下RedPackageController

发红包抢红包记红包拆红包算法
设置K V
一个红包多个金额值使用list
如何保证高并发+多线程+不加锁且保证原子性
在redis中,每个命令就是 单线程天生的原子性
使用数据结构list,lpop出数据
盘点+汇总防止作弊,同一个用户不可以抢2次红包使用redis中的hset,它本来就不允许相同业内常用二倍均值法

4、编码实现

说明:此处的controller是基于前面学习的redis一系列的案例基础上添加的。

RedPackageController.java

package com.zm.controller;

import cn.hutool.core.util.IdUtil;
import com.google.common.primitives.Ints;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.Arrays;
import java.util.Random;
import java.util.concurrent.TimeUnit;

@RestController
public class RedPackageController {
    public static final String RED_PACKAGE_KET = "redpackage:";
    public static final String RED_PACKAGE_CONSUME_KET = "redpackage:consume";
    @Autowired
    private RedisTemplate redisTemplate;

    @RequestMapping("/send")
    //totalMoney红包总金额,redPackageNumber红包个数
    public String sendRedPackage(int totalMoney,int redPackageNumber){
        //1、拆红包,将总金额totalMoney拆分成redPackageNumber个子红包
        Integer[] splitRedPacks = splitRedPackageAlgorithm(totalMoney, redPackageNumber);//通过拆分红包算法分好的多个子红包数组
        //2、拆完红包开始发红包,发红包保存进list结构里面
        String key = RED_PACKAGE_KET + IdUtil.simpleUUID();
        redisTemplate.opsForList().leftPushAll(key,splitRedPacks);
        //还需要设置24小时过期
        redisTemplate.expire(key, 1,TimeUnit.DAYS);
        //3、将发红包信息返回前台显示
        return key + "\t" + Ints.asList(Arrays.stream(splitRedPacks).mapToInt(Integer::valueOf).toArray());
    }
    //抢红包
    @RequestMapping("/rob")
    public String robRedPackages(String redPackageKey,String userID){
        //1、验证某个用户是否抢过红包,不可以多抢
        Object redPackage =redisTemplate.opsForHash().get(RED_PACKAGE_CONSUME_KET+redPackageKey,userID);
        if (redPackage == null){//如果为空就是没抢过
            //2、从红包里(list)里出队一个作为客户抢的红包,抢一个红包
            Object partRedPackage = redisTemplate.opsForList().leftPop(RED_PACKAGE_KET + redPackageKey);
            if (partRedPackage != null){
                //抢到红包后需要做记录,进入hash结构,表示谁抢了多少钱红包
                redisTemplate.opsForHash().put(RED_PACKAGE_CONSUME_KET+redPackageKey,userID,partRedPackage);
                System.out.println("用户:"+userID+"\t 抢到了:"+partRedPackage+"元");
                //后续可以有的操作,异步进mysql或者MQ进一步做统计,做个汇总
                return "恭喜你抢到了:"+String.valueOf(partRedPackage)+"元";
            }
            //抢完了
            return "手慢无,你来晚了,红包没了";
        }
        //3、某个用户抢过了,不可以作弊抢多次
        return userID+"你已经抢过了,不能重复抢!";
    }

    //拆分红包算法----二倍均值算法
    private Integer[] splitRedPackageAlgorithm(int totalMoney,int redPackageNumber){
        Integer[] redPackageNumbers = new Integer[redPackageNumber];//拆开好的红包数组,当前是空的
        int userMoney = 0;//已经被抢的红包金额
        for (int i = 0; i < redPackageNumber; i++) {
            //先判断当前循环次数是不是最后一个,如果是最后一个就不需要使用二倍均值算法了
            if (i == redPackageNumber -1){
                redPackageNumbers[i] = totalMoney - userMoney;
            }else {
                //再使用二倍均值算法
                //拆分金额 = 随机区间内(0,(剩余红包金额M ➗ 剩余红包个数N) * 2)
                int avgMoney = ((totalMoney - userMoney) / (redPackageNumber - i)) * 2;
                redPackageNumbers[i] = 1 + new Random().nextInt(avgMoney -1);
            }
            userMoney = userMoney + redPackageNumbers[i];
        }
        return redPackageNumbers;
    }
}

测试

启动程序之后先塞红包:localhost:7777/send?totalMoney=100&redPackageNumber=5

image-20240127175647964

到redis查看红包拆分情况

image-20240127175845135

然后抢红包:localhost:7777/rob?redPackageKey=e0798e46601944d6b9bae951f0baf4c3&userID=1

image-20240127181016620

到redis中查看记录情况

image-20240127181117204

没有错 1号用户抢了50元,接下来还用1号抢试试

image-20240127181210606

提示出来了,不能重复抢

接下来全抢完

image-20240127181400770

然后再看redis中还有没有了

image-20240127181454430

只剩下记录了,红包已经没有了,所以红包的key也没有了。这个时候再来一个6号抢红包,就会发现没有了。

image-20240127181544929

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1414554.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

统计学-R语言-7.3

文章目录 前言总体方差的检验一个总体方差的检验两个总体方差比的检验 非参数检验总体分布的检验正态性检验的图示法Shapiro-Wilk和K-S正态性检验总体位置参数的检验 练习 前言 本篇文章继续对总体方差的检验进行介绍。 总体方差的检验 一个总体方差的检验 在生产和生活的许多…

使用BootStrapBlazor组件搭建Bootstarp风格的Winform界面

项目地址https://gitee.com/zhang_jie_sc/my-blazor-winforms 1.安装Bootstrap.Blazor.Templates 模板 在power shell中输入dotnet new install Bootstrap.Blazor.Templates::7.6.1&#xff0c;安装7.6.1是因为版本8以后就要强制使用net8.0了&#xff0c;很多语法不一样&…

了解WPF控件:ToggleButton和Separator常用属性与用法(十三)

掌握WPF控件&#xff1a;熟练ToggleButton和Separator常用属性&#xff08;十三&#xff09; ToggleButton 一个按钮类UI元素&#xff0c;它的特点是拥有两种状态&#xff1a;选中&#xff08;Checked&#xff09;和未选中&#xff08;Unchecked&#xff09;。当用户单击Togg…

pve8.1 安装、创建centos7虚拟机及配置

之前创建虚拟机centos7时&#xff0c;硬盘分配太大了&#xff0c;做成模板后无法进行修改了&#xff0c;安装完pve8.1后&#xff0c;强迫症犯了重新创建一下顺便记录一下配置过程。由于目前centos7还是生产用的比较多的版本所以本次还是安装centos7.9版本。 一、下载镜像 下载…

使用 CDC MinIO 汇入端为 CockroachDB 保持持久数据

CockroachDB 数据库迅速崭露头角&#xff0c;作为一个坚韧且可扩展的分布式 SQL 数据库。它从其昆虫名字的坚持不懈中汲取灵感&#xff0c;即使面对硬件故障&#xff0c;CockroachDB 也能保证高可用性。其分布式架构横跨多个节点&#xff0c;类似于其昆虫原型的适应性。 凭借强…

dos攻击与ddos攻击的区别

①DOS攻击&#xff1a; DOS&#xff1a;中文名称是拒绝服务&#xff0c;一切能引起DOS行为的攻击都被称为dos攻击。该攻击的效果是使得计算机或网络无法提供正常的服务。常见的DOS攻击有针对计算机网络带宽和连通性的攻击。 DOS是单机于单机之间的攻击。 DOS攻击的原理&#…

JavaScript学习-原型和原型链

原型和原型链 示例代码 //创建一个Person类 class Person {constructor(name) {this.name name;}drink() {console.log(喝水);} } //创建一个Teacher类&#xff0c;继承Person class Teacher extends Person {constructor(name, subject) {super(name);this.subject subjec…

详细解读vcruntime140_1.dll修复的手段,如何快速解决vcruntime140_1.dll丢失问题

当出现“无法找到vcruntime140_1.dll”或程序“未能正常启动”时&#xff0c;这通常指示系统中缺失了一个关键文件&#xff1a;vcruntime140_1.dll。作为Visual C Redistributable组件的一部分&#xff0c;这个小文件在很多用Visual Studio编译的C程序运行时发挥着重要作用。解…

go语言(十八)---- goroutine

一、goroutine package mainimport ("fmt""time" )func main() {//用go创建承载一个形参为空&#xff0c;返回值为空的一个函数go func() {defer fmt.Println("A.defer")func() {defer fmt.Println("B.defer")//退出当前goroutinefmt…

P1042 [NOIP2003 普及组] 乒乓球 Java版最简单题解!

为什么说最简单&#xff0c;因为本人就是一个算法小白&#xff0c;只学过一点数据结构&#xff0c;打算备战蓝桥杯的&#xff0c;网上说备战蓝桥杯就去刷洛谷&#xff0c;早有听闻洛谷很难&#xff0c;今天一看算是真的被打醒了&#xff0c;对于小白是真的太难了。(;༎ຶД༎ຶ…

在Idea中使用git查看历史版本

idea查git历史 背景查看步骤总结 背景 有好几次同事到我电脑用idea查看git管理的历史记录&#xff0c;每次都说我的idea看不了历史版本&#xff0c;叫我到他电脑上去看&#xff0c;很晕&#xff0c;为什么,原来是我自己把显示历史文件的视图覆盖了&#xff0c;下面我们来一起学…

Python open函数

在Python编程中&#xff0c;open()函数是一个重要的文件操作函数&#xff0c;用于打开文件并进行读取、写入、追加等操作。本文将深入探讨open()函数的用法、语法、文件模式、示例代码&#xff0c;并探讨其在实际编程中的应用场景。 什么是open()函数&#xff1f; open()函数…

【阻塞队列】阻塞队列的模拟实现及在生产者和消费者模型上的应用

文章目录 &#x1f4c4;前言一. 阻塞队列初了解&#x1f346;1. 什么是阻塞队列&#xff1f;&#x1f345;2. 为什么使用阻塞队列&#xff1f;&#x1f966;3. Java标准库中阻塞队列的实现 二. 阻塞队列的模拟实现&#x1f35a;1. 实现普通队列&#x1f365;2. 实现队列的阻塞功…

Python.五.文件

1.文件读取的操作 1.文件的打开 open(name,mode,encoding) name:是要打开目标文件名的字符串&#xff0c;可以包含文件所在的具体路径 mode:设置打开文件的模式&#xff1a;只读 r 、写入 w 、追加 a encoding&#xff1a;编码格式 UTF-8 fopen("C:/test.txt"…

XSS_Labs靶场通关笔记

每一关的方法不唯一&#xff1b;可以结合源码进行分析后构造payload&#xff1b; 通关技巧&#xff08;四步&#xff09;&#xff1a; 1.输入内容看源码变化&#xff1b; 2.找到内容插入点&#xff1b; 3.测试是否有过滤&#xff1b; 4.构造payload绕过 第一关 构造paylo…

怎么获取二维码的链接?二维码转链接只需3步

怎么从二维码中提取内容呢&#xff1f;现在很多内容都会用二维码方式来存储&#xff0c;但是有些场景下二维码是无法使用的时候&#xff0c;想要查看二维码中的内容&#xff0c;就需要分解二维码成链接后使用。那么二维码分解成链接具体该怎么做呢&#xff1f;今天就将在线二维…

Hammer.js中文教程

一、什么是hammer.js hammerJS是一个开源的&#xff0c;轻量级的触屏设备javascript手势库&#xff0c;它可以在不需要依赖其他东西的情况下识别触摸&#xff0c;鼠标事件。允许同时监听多个手势、自定义识别器&#xff0c;也可以识别滑动方向。 优点&#xff1a; 为移动端网…

[已解决]504 Gateway Time-out 网关超时

文章目录 问题&#xff1a;504 Gateway Time-out 504 Gateway Time-out 网关超时思路解决 问题&#xff1a;504 Gateway Time-out 504 Gateway Time-out 网关超时 思路 网上的常规思路是修改nginx配置文件,增加请求执行时间,试过没有用 keepalive_timeout 600; fastcgi_con…

一文读懂: AIGC基本原理及应用领域

AIGC是利用人工智能技术来生成内容的一种新型技术。随着人工智能技术的不断发展&#xff0c;AIGC技术也得到了越来越广泛的应用。未来&#xff0c;AIGC技术将会对我们的生活和工作产生巨大的影响。 一、AIGC技术的基本原理 AIGC技术的基本原理是利用人工智能技术中…

JAVA学习笔记三

1.java执行流程分析 2.什么是编译 javac Hello.java 1.有了java源文件&#xff0c;通过编译器将其编译成JVM可以识别的字节码文件 2.在该源文件目录下&#xff0c;通过javac编译工具对Hello.java文件进行编译 3.如果程序没有错误&#xff0c;没有任何提示&#xff0c;但在…