如何使用Redis实现抢红包功能

news2024/11/27 7:38:02

【内容摘要】

在这篇文章中,我们将探讨如何使用Redis来设计和实现一个抢红包的业务场景。从业务场景、需求分析、技术选型、代码实现,痛点问题等进行多维分析和思考。

【业务场景】

下面引入一个实际的使用案例,如微信群中常用的红包功能。应用redis的相关知识做些思考和总结。
在这里插入图片描述

根据上图我们思考几个问题:

  1. 新人入群,发红包+抢红包,属于高并发业务要求,不能用mysql来做,尝试用redis实现
  2. 一个总的大红包,会有可能拆分成多个小红包,总金额= 分金额1+分金额2+分金额3…分金额N
  3. 每个人只能抢一次,需要有记录,比如100块钱,被拆分成10个红包发出去,总计有10个红包,抢一个少一个,总数显示(10/6)直到抢完,需要记录哪些人抢到了红包。
  4. 有可能还需要你计时,从发出全部抢完,耗时多少?
  5. 红包过期,没人抢红包,需在24小时内退回发红包主账户下。
  6. 虽说是随机红包,但是红包金额如何设置才能显得相对公平?
  7. 高并发下如何保证数据一致性?

【需求分析】

基本业务流程如下:

在这里插入图片描述

【技术选型】

抢红包属于高并发场景,为避免频繁IO导致的性能瓶颈,故选用redis实现。

【落地实现】

Redis如何支持抢红包场景的基本操作,不包括完整的业务逻辑和异常处理。要在命令行中使用Redis实现一个简单的抢红包场景,可以通过以下步骤使用redis-cli工具来执行Redis命令。 以下是生成红包池、发红包、抢红包和红包记录的命令示例:

  1. 生成红包池:

# 使用RPUSH命令向名为"red_packet_pool"的列表中添加红包金额,此处示例为10个红包,总金额100127.0.0.1:6379> RPUSH red_packet_pool 10 20 30 40 50 60 70 80 90 100
  1. 发红包:

# 使用LPUSH命令将红包ID推送到名为"red_packet_ids"的列表中,同时也将红包金额从"red_packet_pool"中弹出
127.0.0.1:6379> LPUSH red_packet_ids RP_1
127.0.0.1:6379> LPOP red_packet_pool
  1. 抢红包:

# 使用RPOP命令从"red_packet_ids"列表中获取一个红包ID
127.0.0.1:6379> RPOP red_packet_ids
  1. 红包记录:
# 使用LPUSH命令将抢到的红包金额和用户ID记录到名为"red_packet_records"的列表中
127.0.0.1:6379> LPUSH red_packet_records "User1 抢到了 10元"

这只是一个简单的演示,在真实应用中,这些命令通常会由后端应用程序执行。以下是代码实现:

首先,确保你的Spring Boot项目中已正确配置了Redis连接。在application.properties或application.yml中添加Redis连接配置:


spring.redis.host=localhost
spring.redis.port=6379

接下来,创建一个Spring Boot服务类来处理抢红包逻辑:



import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import java.util.List;

@Service
public class RedPacketService {
    @Autowired
    private RedisTemplate<String, String> redisTemplate;

    public void sendRedPacket(String redPacketId, double totalAmount, int totalPeople) {
        double remainingAmount = totalAmount;
        for (int i = 1; i < totalPeople; i++) {
            double randomAmount = Math.random() * remainingAmount / (totalPeople - i);
            redisTemplate.opsForList().leftPush(redPacketId, String.format("%.2f", randomAmount));
            remainingAmount -= randomAmount;
        }
        redisTemplate.opsForList().leftPush(redPacketId, String.format("%.2f", remainingAmount));
    }

    public String grabRedPacket(String redPacketId) {
        String amount = redisTemplate.opsForList().rightPop(redPacketId);
        if (amount != null) {
            double grabbedAmount = Double.parseDouble(amount);
            String userId = "User" + System.nanoTime();
            String grabInfo = userId + " 抢到了 " + String.format("%.2f", grabbedAmount) + " 元";
            redisTemplate.opsForList().leftPush("grabbed:" + redPacketId, grabInfo);
            return grabInfo;
        } else {
            return "红包已抢完";
        }
    }

    public List<String> getRedPacketRecords(String redPacketId) {
        return redisTemplate.opsForList().range("grabbed:" + redPacketId, 0, -1);
    }
}

然后,创建一个Spring Boot控制器来处理HTTP请求:


import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RestController
@RequestMapping("/redpacket")
public class RedPacketController {
    @Autowired
    private RedPacketService redPacketService;

    @PostMapping("/send")
    public void sendRedPacket(@RequestParam String redPacketId, @RequestParam double totalAmount, @RequestParam int totalPeople) {
        redPacketService.sendRedPacket(redPacketId, totalAmount, totalPeople);
    }

    @PostMapping("/grab")
    public String grabRedPacket(@RequestParam String redPacketId) {
        return redPacketService.grabRedPacket(redPacketId);
    }

    @GetMapping("/records")
    public List<String> getRedPacketRecords(@RequestParam String redPacketId) {
        return redPacketService.getRedPacketRecords(redPacketId);
    }
}

最后,假设你的Spring Boot应用程序已经在主机 127.0.0.1 的端口 8080 上运行。
1、发红包操作:

  • URL:http://114.116.85.56:8080/redpacket/send
  • 参数:
  • redPacketId:红包的唯一标识符。
  • totalAmount:红包的总金额。
  • totalPeople:红包的总领取人数。
    示例请求:

http://114.116.85.56:8080/redpacket/send?redPacketId=1&totalAmount=100.0&totalPeople=10

2、抢红包操作:

  • URL:http://114.116.85.56:8080/redpacket/grab
  • 参数:
  • redPacketId:要抢的红包的唯一标识符。

URL: http://114.116.85.56:8080/redpacket/grab?redPacketId=1

3、获取红包记录操作:

  • URL:http://114.116.85.56:8080/redpacket/records
  • 参数:
  • redPacketId:要获取记录的红包的唯一标识符。

http://114.116.85.56:8080/redpacket/records?redPacketId=1redPacketId:要获取记录的红包的唯一标识符。
br

【痛点问题】

在抢红包过程中,可能存在一些痛点问题,这些问题需要在系统设计和实现中仔细考虑和解决。以下是一些可能存在的痛点问题:

  1. 高并发问题:抢红包场景通常伴随着高并发操作,多个用户同时尝试抢夺同一个红包。这可能导致竞态条件和数据一致性问题。

  2. 数据一致性问题:在高并发情况下,多个用户同时修改Redis中的数据,可能导致数据一致性问题。例如,多个用户同时写入抢红包记录,可能导致数据的混乱或丢失。

  3. 性能问题:处理高并发抢红包请求可能对系统的性能产生挑战。需要考虑系统的扩展性和负载均衡。

  4. 作弊问题:用户可能尝试通过不正当手段多次抢夺同一个红包。需要考虑如何检测和防止作弊行为。

  5. 红包池管理:红包池的管理和维护也是一个问题,包括红包的生成、过期处理和数据清理。

  6. 数据安全性:红包金额的安全性也是一个关键问题。需要确保用户不能通过恶意请求或攻击来窃取或篡改红包金额。

  7. 用户体验:最终用户的体验也是关键因素。抢红包的过程应该是流畅的,用户不应该感到等待时间过长或遇到错误。
    解决这些痛点问题需要综合考虑多个因素,包括并发控制、事务处理、分布式锁、数据模型设计、性能优化、安全性等。在设计抢红包系统时,需要仔细权衡这些因素,以确保系统的可伸缩性、稳定性和用户体验。

    我们就高并发问题可能导致竞态条件和数据一致性问题给出解决方案。

方案一:分布式锁

使用分布式锁来解决高并发问题的代码示例:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;

import java.util.concurrent.TimeUnit;

@Service
public class RedPacketService {

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    public String grabRedPacket(String redPacketId, String userId) {
        String redPacketKey = "red_packet:" + redPacketId;
        String userKey = "user:" + userId;

        try {
            // 使用分布式锁
            boolean isLocked = stringRedisTemplate.opsForValue().setIfAbsent(userKey, "1", 10, TimeUnit.SECONDS);

            if (isLocked) {
                // 获取到锁,可以继续抢红包

                if (stringRedisTemplate.opsForList().size(redPacketKey) > 0) {
                    // 红包池还有红包,可以继续抢
                    String redPacket = stringRedisTemplate.opsForList().leftPop(redPacketKey);
                    // 记录抢红包信息
                    String record = userId + " 抢到了 " + redPacket + " 元";
                    stringRedisTemplate.opsForList().leftPush("red_packet_records:" + redPacketId, record);

                    // 释放用户锁
                    stringRedisTemplate.delete(userKey);

                    return record;
                } else {
                    // 红包池已空
                    stringRedisTemplate.delete(userKey);
                    return "红包已抢光";
                }
            } else {
                // 用户未成功获取锁,表示用户已经抢过红包
                return "你已经抢过红包了";
            }
        } catch (InterruptedException e) {
            // 处理异常
            e.printStackTrace();
            return "抢红包出现异常";
        }
    }
}

在这个示例中,我们使用了Spring Boot和Redis的String类型来模拟用户抢红包的操作。关键是使用setIfAbsent方法来获取用户的分布式锁,以确保同一个用户不会重复抢红包。如果用户成功获取锁,就可以继续抢红包。抢红包操作包括检查红包池是否还有红包,抢夺红包,记录抢红包信息,然后释放用户锁。
这个示例中的分布式锁是通过Redis的String类型实现的,但实际上可以使用更强大的分布式锁库,如Redisson。

方案二:Redis事务

使用Redis的事务机制来确保操作的原子性。Redis的事务允许一组操作(一系列命令)在一个单一的、原子的事务中执行,这意味着它们要么全部成功,要么全部失败。在抢红包的情况下,你可以使用Redis的MULTI、EXEC和WATCH命令来创建一个事务块。


# 开始一个事务
127.0.0.1:6379> MULTI


# 监视红包池的变化
127.0.0.1:6379> WATCH red_packet_pool


# 检查红包池中是否还有红包
127.0.0.1:6379> LLEN red_packet_pool
(integer) 3


# 如果红包池中还有红包,则继续操作
127.0.0.1:6379> LPUSH red_packet_ids RP_1
127.0.0.1:6379> LPOP red_packet_pool


# 提交事务
127.0.0.1:6379> EXEC

在上述事务中,我们首先使用WATCH命令监视红包池,以确保在执行事务期间没有其他人修改了红包池。然后,我们在事务块中使用LPOP命令弹出一个红包金额,并使用LPUSH命令将抢红包的信息记录下来。最后,使用EXEC命令提交事务。如果在事务执行期间没有其他人修改了红包池,事务将成功执行。

这个示例演示了如何在Redis命令行中使用事务来处理抢红包操作,以确保抢红包的原子性。在实际应用中,你可以使用Spring Data Redis或其他Redis客户端库来以编程方式执行事务,而不是手动执行Redis命令。

首先,确保在Spring Boot项目中配置了Spring Data Redis依赖:


<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

然后,在Spring Boot应用中创建一个RedPacketService类,该类包含了处理抢红包操作的方法:


import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.SessionCallback;
import org.springframework.stereotype.Service;

@Service
public class RedPacketService {

    @Autowired
    private RedisTemplate<String, String> redisTemplate;

    public String grabRedPacket(String redPacketId, String userId) {
        String redPacketKey = "red_packet:" + redPacketId;
        String userKey = "user:" + userId;

        SessionCallback<String> sessionCallback = operations -> {
            operations.watch(redPacketKey);
            String redPacket = operations.opsForList().leftPop(redPacketKey);
            if (redPacket != null) {
                operations.multi();
                operations.opsForList().leftPush("red_packet_records:" + redPacketId, userId + " 抢到了 " + redPacket + " 元");
                operations.exec();
            }
            operations.unwatch();
            return redPacket;
        };

        String result = redisTemplate.execute(sessionCallback);

        if (result == null) {
            return "红包已抢光";
        } else if (result.equals("")) {
            return "你已经抢过红包了";
        } else {
            return result;
        }
    }
}

在这个示例中,我们使用SessionCallback接口来执行事务。在sessionCallback中,我们首先调用watch方法来监视红包池的变化。然后,我们执行一系列操作,包括弹出红包、记录抢红包信息,并使用multi和exec方法来提交事务。最后,我们使用unwatch来取消监视。

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

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

相关文章

计算机网络复习2

物理层 文章目录 物理层通讯基础奈奎斯特定理香农定理编码与调制交换传输介质&#xff08;了解&#xff09;物理层设备 通讯基础 数据信号码元信源信道信宿单工通道&#xff1a;只有一个方向半双工通道&#xff1a;不能同时发送和接收全双工通道 奈奎斯特定理 规定&#xff…

裕泰微YT8521SH PHY芯片在uboot下的代码适配(二)

裕泰微YT8521SH PHY芯片在uboot下的代码适配&#xff08;一&#xff09; 文章目录 代码搜索移植步骤具体代码备注 本文主要是介绍uboot下的适配代码来源和具体修改。 代码搜索 https://github.com/starfive-tech/u-boot/blob/JH7110_VisionFive2_devel/drivers/net/phy/motorc…

Linux中的gcc\g++使用

文章目录 gcc\g的使用预处理编译汇编链接函数库gcc选项 gcc\g的使用 这里我们需要知道gcc和g实际上是对应的c语言和c编译器&#xff0c;而其他的Java&#xff08;半解释型&#xff09;&#xff0c;PHP&#xff0c;Python等语言实际上是解释型语言&#xff0c;因此我们经常能听…

Ps:混合颜色带 - 基础篇

混合颜色带 Blend If是“图层样式”对话框中的一个高级功能&#xff0c;允许根据下方图层或当前图层的色彩信息来混合图层&#xff0c;无需进行复杂的选区或蒙版操作。 混合颜色带是基于亮度&#xff08;灰色&#xff09;或颜色通道的特定范围来显示或隐藏图层的特定区域。 “当…

Android : 使用GestureDetector 进行手势识别—简单应用

示例图&#xff1a; GestureDetector 介绍&#xff1a; GestureDetector 是 Android 开发中用于识别和处理手势的一个类。它允许开发者检测用户在触摸屏上的各种手势&#xff0c;如滑动、长按、双击等。通过使用 GestureDetector&#xff0c;您可以轻松地为应用程序添加手势识…

GLTF 编辑器实现逼真3D动物毛发效果

在线工具推荐&#xff1a; 3D数字孪生场景编辑器 - GLTF/GLB材质纹理编辑器 - 3D模型在线转换 - Three.js AI自动纹理开发包 - YOLO 虚幻合成数据生成器 - 三维模型预览图生成器 - 3D模型语义搜索引擎 要实现逼真的3D动物毛发效果&#xff0c;可以采用以下技术和方法&…

AI数字人克隆系统源代码克隆系统开发--本地源码部署

随着人工智能技术的不断发展&#xff0c;AI数字人克隆系统逐渐成为现实。这一系统通过克隆人的外貌和行为模式&#xff0c;可以创建具有自我认知、学习和情感的数字化人类。而为了更好地开发AI数字人克隆系统&#xff0c;本地源码部署是一项关键步骤。 在开始介绍本地源码部署…

低功耗蓝牙模块:促进智慧城市发展的关键技术

在科技快速发展的时代&#xff0c;智慧城市的概念正引领着城市管理的革新。为实现城市更高效、可持续和智能化的管理&#xff0c;低功耗蓝牙模块成为推动智慧城市发展的关键技术之一。本文将探讨低功耗蓝牙模块在智慧城市中的作用&#xff0c;以及其在城市基础设施、公共服务等…

6、LLaVA

简介 LLaVA官网 LLaVA使用Vicuna(LLaMA-2)作为LLM f ϕ ( ⋅ ) f_\phi() fϕ​(⋅)&#xff0c;使用预训练的CLIP图像编码器 ViT-L/14 g ( X v ) g(X_v) g(Xv​)。 输入图像 X v X_v Xv​&#xff0c;首先获取feature Z v g ( X v ) Z_vg(X_v) Zv​g(Xv​)。考虑到最后一…

ROS-ID®活性氧/活性氮检测试剂盒

自由基和其他活性种在许多心理和病理生理过程中发挥着重要作用。自由基一旦在细胞内产生&#xff0c;就会破坏多种细胞成分&#xff0c;包括蛋白质、脂质和DNA。然而&#xff0c;在较低浓度下&#xff0c;自由基可以作为细胞信号传导中的第二信使。 Enzo Life Sciences的ROS-I…

svn外网打不开url地址怎么解决

在家或者出差在外经常会有连接到公司内部SVN服务器的需求&#xff0c; 但是 svn外网打不开url地址怎么解决&#xff1f;如何才能连接到公司内部SVN服务器&#xff1f;今天小编教你一招&#xff0c;在本地SVN服务的内网IP端口用快解析软件添加映射&#xff0c;一步就可以提供让公…

PC9095高性能可调限流OVP过压过流保护 软启动 抗浪涌 集成功率FET开关

特点 •输入电压范围&#xff1a; •PC9095A、PC9095KA:2.5伏~13.5伏 •PC9095B&#xff0c;PC9095KB:2.5伏~10伏 •PC9095C&#xff0c;PC9095KC:2.5伏~5.5伏 •28V绝对最大额定电压VOUT •带外部电阻器的可调限流器 •集成功率FET开关&#xff0c;53mΩRds&#xff08…

【Vue2+3入门到实战】(14)路由入门之单页应用程序、路由 、 VueRouter的基本使用 详细示例

目录 一、学习目标1.路由入门 二、单页应用程序介绍1.概念2.具体示例3.单页应用 VS 多页面应用4.总结 三、路由介绍1.思考2.路由的介绍3.总结 四、路由的基本使用1.目标2.作用3.说明4.官网5.VueRouter的使用&#xff08;52&#xff09;6.代码示例7.两个核心步骤8.总结 五、组件…

关于MybatisPlus自动转化驼峰命名规则配置mapUnderscoreToCamelCase的个人测试和总结

关于MybatisPlus自动转化驼峰命名规则配置mapUnderscoreToCamelCase的个人测试和总结 测试一&#xff1a;没有添加 自动转化的配置&#xff0c;且domain中的属性名称和数据库的字段名称一致测试二&#xff1a;没有添加自动转化配置i&#xff0c;domain属性名userPassword和数据…

泽攸科技PECVD设备助力开发新型石墨烯生物传感器

近日&#xff0c;松山湖材料实验室许智团队与清华大学符汪洋合作在纳米领域头部期刊《Small》上发表了一项引人注目的研究成果&#xff0c;题为“Ultrasensitive biochemical sensing platform enabled by directly grown graphene on insulator”&#xff08;硅晶圆上直接生长…

计算机网络——应用层与网络安全(六)

前言&#xff1a; 前几章我们已经对TCP/IP协议的下四层已经有了一个简单的认识与了解&#xff0c;下面让我们对它的最顶层&#xff0c;应用层进行一个简单的学习与认识&#xff0c;由于计算机网络多样的连接形式、不均匀的终端分布&#xff0c;以及网络的开放性和互联性等特征&…

使用NTC负温度系数热敏电阻控制温度

鱼缸原来的加热棒使用的是NTC负温度系数的热敏电阻测温&#xff0c;负温度系数是指随着温度的升高&#xff0c;电阻是不断按照指数形式减小的&#xff0c;在22度的情况下实测电阻是10K多&#xff0c;可以断定使用了10K&#xff08;25度下是10K&#xff09;的电阻&#xff0c;为…

configure: error: no acceptable C compiler found in $PATH解决方案

大家好,我是爱编程的喵喵。双985硕士毕业,现担任全栈工程师一职,热衷于将数据思维应用到工作与生活中。从事机器学习以及相关的前后端开发工作。曾在阿里云、科大讯飞、CCF等比赛获得多次Top名次。现为CSDN博客专家、人工智能领域优质创作者。喜欢通过博客创作的方式对所学的…

10-1,ZZ004 新型电力系统运行与维护赛题第十套

2023年全国职业院校技能大赛中职组 “新型电力系统运行与维护” 竞 赛 任 务 书 10 第一部分 竞赛须知 竞赛总分为100分&#xff0c;完成时间为7小时。 一、竞赛纪律要求 &#xff08;一&#xff09;正确使用设备与工具&#xff0c;严格遵守操作安全规范。 &#xff08;二&…

如何文件从电脑传到iPhone,这里提供几个方法

本文介绍了如何使用Finder应用程序、iTunes for Windows、iCloud和谷歌照片将照片从Mac或PC传输到iPhone。 如何将照片从Mac传输到iPhone 如果你有一台Mac电脑&#xff0c;里面装满了你想转移到iPhone的照片&#xff0c;这是一件非常简单的事情。只需遵循以下步骤&#xff1a…