Redian新闻
>
如何使用Redis实现抢红包功能

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

公众号新闻

内容摘要

在这篇文章中,我们将探讨如何使用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个红包,总金额100元
127.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/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来取消监视。

结尾

感谢大家认真审阅,也欢迎大佬们批评指正。如果您觉得对日常工作或学习有帮助,欢迎点赞,在看,转发和评论。

·················END·················

微信扫码关注该文公众号作者

戳这里提交新闻线索和高质量文章给我们。
相关阅读
如何使用华夫饼机视频号带货:都想抢红利,急迫找方法拒绝红包攀比!把「100国100张外币」塞进红包,隔壁小孩羡慕哭了!来不及看标题了,快抢红包封面!李飞飞对话英伟达首席科学家:人类不仅是AI的创造者,也是如何使用的决策者红包界的“劳斯*斯”!把「100国100张外币」塞进红包,隔壁小孩都羡慕哭了!比VS Code快得多!用Rust重写,支持OpenAI、Copilot 的Zed编辑器开源了再次"出圈"!百亿量化私募2023年表现抢眼,但量化和主观却拉开了差距……张郎郎:\'血统\'鬼魅始终笼罩中国1折速抢红酒中的劳斯来斯!法国进口、拿奖到手软,过年送礼太有面!产品推荐 | 拒绝红包攀比!把「100国100张外币」塞进红包,隔壁小孩羡慕哭了!红包界的“劳斯*斯”!把「100国100张外币」塞进红包,有内涵有体面!最让我满意的客服,台湾的”问讲“们如何使用python发送日志易告警年终最后一波hfp劲爆福利,低至4.8折​!【进来抢红包封面】2024红包封图鉴:金龙迎新年,新加坡有哪些漂亮红包?恭喜发财,红包拿来!“红包”说成“Red Bag”可不对,正确的英文该怎么说呢?如何使用爽肤水警务人员新年不许收红包,发红包可不可以GPT、Etsy又在大面积封号!在国内如何使用美国电话?一招搞定《跌宕起伏心灵煎熬的14天》(6) 【自证清白】四季度公募REITs实录:5只收入过亿 | 另类投资气象第456期红包界“劳斯*斯”!「100国100张外币」塞进红包,隔壁小孩都羡慕哭了!工作中常用Redis的10种场景,太经典了!【赠送GPT账号】如何使用ChatGPT完成科研、程序开发、论文写作等,看看这篇!过去5年墨尔本这些郊区房价涨最猛!中郊和远郊表现抢眼券商场外业务数据出炉!跨境业务表现抢眼,这三家券商"赢了"别送现金红包了!这个「百国红包」有100张外币!!还送“龙钞”哦~别送现金红包了!这个「红包」有52张外币!!还是真钞哦~红包界的“劳斯赖斯”!把「100国100张外币」塞进红包,小孩都羡慕哭了!毛主席《水调歌头,才饮长沙水..》读着玩如何使用占卜板1折速抢红酒中的“爱马仕”!法国进口、拿奖到手软,过年送礼太有面!童年故事(10):玩冲锑招人嫉恨1折速抢红酒中的爱马仕!法国进口、拿奖到手软,过年送礼太有面!
logo
联系我们隐私协议©2024 redian.news
Redian新闻
Redian.news刊载任何文章,不代表同意其说法或描述,仅为提供更多信息,也不构成任何建议。文章信息的合法性及真实性由其作者负责,与Redian.news及其运营公司无关。欢迎投稿,如发现稿件侵权,或作者不愿在本网发表文章,请版权拥有者通知本网处理。