基于Redis 的高并发抢红包程序是如何实现的

2024-03-13 13:58

本文主要是介绍基于Redis 的高并发抢红包程序是如何实现的,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

下面介绍一种基于redis的抢红包方案。
把原始的红包称为大红包,拆分后的红包称为小红包。
1.小红包预先生成,插到数据库里,红包对应的用户ID是null,红包生成算法如下:
预先生成所有的红包还是一个请求随机生成一个红包
简单来说,就是把一个大整数m分解(直接以“分为单位,如1元即100)分解成n个小整数的过程,小整数的范围是[min, max]。
最简单的思路,先保底,每个小红包保证有min,然后每个请求都随机生成一个0到(max-min)范围的整数,再加上min就是红包的钱数。
这个算法虽然简单,但是有一个弊端:最后生成的红包可能都是min钱数的。也就是说可能最后的红包都是0.01元的。
另一种方式是预先生成所有红包,这样就比较容易控制了。我选择的是预先生成所有的红包。
理想的红包生成算法:
理想的红包生成结果是平均值附近的红包比较多,大红包和小红包的数量比较少。
可以想像下,生成红包的数量的分布有点像正态分布。
那么如何实现这种平均线附近值比较多的要求呢?
就是要找到一种算法,可以提高平均值附近的概率。那么利用一种”膨胀“再”收缩“的方式来达到这种效果。
先平方,再生成平方范围内的随机数,再开方,那么概率就不再是平均的了。
2.每个大红包对应两个redis队列,一个是未消费红包队列,另一个是已消费红包队列。开始时,把未抢的小红包全放到未消费红包队列里。
未消费红包队列里是json字符串,如{userId:’789′, money:’300′}。
3.在redis中用一个map来过滤已抢到红包的用户。
4.抢红包时,先判断用户是否抢过红包,如果没有,则从未消费红包队列中取出一个小红包,再push到另一个已消费队列中,最后把用户ID放入去重的map中。
5.用一个单线程批量把已消费队列里的红包取出来,再批量update红包的用户ID到数据库里。
上面的流程是很清楚的,但是在第4步时,如果是用户快速点了两次,或者开了两个浏览器来抢红包,会不会有可能用户抢到了两个红包?
为了解决这个问题,采用了lua脚本方式,让第4步整个过程是原子性地执行。
下面是在redis上执行的Lua脚本:
-- 函数:尝试获得红包,如果成功,则返回json字符串,如果不成功,则返回空
-- 参数:红包队列名, 已消费的队列名,去重的Map名,用户ID
-- 返回值:nil 或者 json字符串,包含用户ID:userId,红包ID:id,红包金额:money
-- 如果用户已抢过红包,则返回nil
if redis.call('hexists', KEYS[3], KEYS[4]) ~= 0 then
  return nil
else
  -- 先取出一个小红包
  local hongBao = redis.call('rpop', KEYS[1]);
  if hongBao then
    local x = cjson.decode(hongBao);
    -- 加入用户ID信息
    x['userId'] = KEYS[4];
    local re = cjson.encode(x);
    -- 把用户ID放到去重的set里
    redis.call('hset', KEYS[3], KEYS[4], KEYS[4]);
    -- 把红包放到已消费队列里
    redis.call('lpush', KEYS[2], re);
    return re;
  end
end
return nil

下面是测试代码:
public class TestEval {
    static String host = "localhost";
    static int honBaoCount = 1_0_0000;
    static int threadCount = 20;
    static String hongBaoList = "hongBaoList";
    static String hongBaoConsumedList = "hongBaoConsumedList";
    static String hongBaoConsumedMap = "hongBaoConsumedMap";
    static Random random = new Random();
//  -- 函数:尝试获得红包,如果成功,则返回json字符串,如果不成功,则返回空
//  -- 参数:红包队列名, 已消费的队列名,去重的Map名,用户ID
//  -- 返回值:nil 或者 json字符串,包含用户ID:userId,红包ID:id,红包金额:money
    static String tryGetHongBaoscript = 
//          "local bConsumed = redis.call('hexists', KEYS[3], KEYS[4]);\n"
//          + "print('bConsumed:' ,bConsumed);\n"
            "if redis.call('hexists', KEYS[3], KEYS[4]) ~= 0 then\n"
            + "return nil\n"
            + "else\n"
            + "local hongBao = redis.call('rpop', KEYS[1]);\n"
//          + "print('hongBao:', hongBao);\n"
            + "if hongBao then\n"
            + "local x = cjson.decode(hongBao);\n"
            + "x['userId'] = KEYS[4];\n"
            + "local re = cjson.encode(x);\n"
            + "redis.call('hset', KEYS[3], KEYS[4], KEYS[4]);\n"
            + "redis.call('lpush', KEYS[2], re);\n"
            + "return re;\n"
            + "end\n"
            + "end\n"
            + "return nil";
    static StopWatch watch = new StopWatch();
 
    public static void main(String[] args) throws InterruptedException {
//      testEval();
        generateTestData();
        testTryGetHongBao();
    }
 
    static public void generateTestData() throws InterruptedException {
        Jedis jedis = new Jedis(host);
        jedis.flushAll();
        final CountDownLatch latch = new CountDownLatch(threadCount);
        for(int i = 0; i < threadCount; ++i) {
            final int temp = i;
            Thread thread = new Thread() {
                public void run() {
                    Jedis jedis = new Jedis(host);
                    int per = honBaoCount/threadCount;
                    JSONObject object = new JSONObject();
                    for(int j = temp * per; j < (temp+1) * per; j++) {
                        object.put("id", j);
                        object.put("money", j);
                        jedis.lpush(hongBaoList, object.toJSONString());
                    }
                    latch.countDown();
                }
            };
            thread.start();
        }
        latch.await();
    }
 
    static public void testTryGetHongBao() throws InterruptedException {
        final CountDownLatch latch = new CountDownLatch(threadCount);
        System.err.println("start:" + System.currentTimeMillis()/1000);
        watch.start();
        for(int i = 0; i < threadCount; ++i) {
            final int temp = i;
            Thread thread = new Thread() {
                public void run() {
                    Jedis jedis = new Jedis(host);
                    String sha = jedis.scriptLoad(tryGetHongBaoscript);
                    int j = honBaoCount/threadCount * temp;
                    while(true) {
                        Object object = jedis.eval(tryGetHongBaoscript, 4, hongBaoList, hongBaoConsumedList, hongBaoConsumedMap, "" + j);
                        j++;
                        if (object != null) {
//                          System.out.println("get hongBao:" + object);
                        }else {
                            //已经取完了
                            if(jedis.llen(hongBaoList) == 0)
                                break;
                        }
                    }
                    latch.countDown();
                }
            };
            thread.start();
        }
 
        latch.await();
        watch.stop();
 
        System.err.println("time:" + watch.getTotalTimeSeconds());
        System.err.println("speed:" + honBaoCount/watch.getTotalTimeSeconds());
        System.err.println("end:" + System.currentTimeMillis()/1000);
    }
}

测试结果20个线程,每秒可以抢2.5万个,足以应付绝大部分的抢红包场景。
如果是真的应付不了,拆分到几个redis集群里,或者改为批量抢红包,也足够应付。
redis的抢红包方案,虽然在极端情况下(即redis挂掉)会丢失一秒的数据,但是却是一个扩展性很强,足以应付高并发的抢红包方案。

这篇关于基于Redis 的高并发抢红包程序是如何实现的的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/805108

相关文章

Spring Security自定义身份认证的实现方法

《SpringSecurity自定义身份认证的实现方法》:本文主要介绍SpringSecurity自定义身份认证的实现方法,下面对SpringSecurity的这三种自定义身份认证进行详细讲解,... 目录1.内存身份认证(1)创建配置类(2)验证内存身份认证2.JDBC身份认证(1)数据准备 (2)配置依

利用python实现对excel文件进行加密

《利用python实现对excel文件进行加密》由于文件内容的私密性,需要对Excel文件进行加密,保护文件以免给第三方看到,本文将以Python语言为例,和大家讲讲如何对Excel文件进行加密,感兴... 目录前言方法一:使用pywin32库(仅限Windows)方法二:使用msoffcrypto-too

C#使用StackExchange.Redis实现分布式锁的两种方式介绍

《C#使用StackExchange.Redis实现分布式锁的两种方式介绍》分布式锁在集群的架构中发挥着重要的作用,:本文主要介绍C#使用StackExchange.Redis实现分布式锁的... 目录自定义分布式锁获取锁释放锁自动续期StackExchange.Redis分布式锁获取锁释放锁自动续期分布式

springboot使用Scheduling实现动态增删启停定时任务教程

《springboot使用Scheduling实现动态增删启停定时任务教程》:本文主要介绍springboot使用Scheduling实现动态增删启停定时任务教程,具有很好的参考价值,希望对大家有... 目录1、配置定时任务需要的线程池2、创建ScheduledFuture的包装类3、注册定时任务,增加、删

SpringBoot整合mybatisPlus实现批量插入并获取ID详解

《SpringBoot整合mybatisPlus实现批量插入并获取ID详解》这篇文章主要为大家详细介绍了SpringBoot如何整合mybatisPlus实现批量插入并获取ID,文中的示例代码讲解详细... 目录【1】saveBATch(一万条数据总耗时:2478ms)【2】集合方式foreach(一万条数

使用Python实现矢量路径的压缩、解压与可视化

《使用Python实现矢量路径的压缩、解压与可视化》在图形设计和Web开发中,矢量路径数据的高效存储与传输至关重要,本文将通过一个Python示例,展示如何将复杂的矢量路径命令序列压缩为JSON格式,... 目录引言核心功能概述1. 路径命令解析2. 路径数据压缩3. 路径数据解压4. 可视化代码实现详解1

PyQt6/PySide6中QTableView类的实现

《PyQt6/PySide6中QTableView类的实现》本文主要介绍了PyQt6/PySide6中QTableView类的实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学... 目录1. 基本概念2. 创建 QTableView 实例3. QTableView 的常用属性和方法

PyQt6/PySide6中QTreeView类的实现

《PyQt6/PySide6中QTreeView类的实现》QTreeView是PyQt6或PySide6库中用于显示分层数据的控件,本文主要介绍了PyQt6/PySide6中QTreeView类的实现... 目录1. 基本概念2. 创建 QTreeView 实例3. QTreeView 的常用属性和方法属性

Android使用ImageView.ScaleType实现图片的缩放与裁剪功能

《Android使用ImageView.ScaleType实现图片的缩放与裁剪功能》ImageView是最常用的控件之一,它用于展示各种类型的图片,为了能够根据需求调整图片的显示效果,Android提... 目录什么是 ImageView.ScaleType?FIT_XYFIT_STARTFIT_CENTE

pandas中位数填充空值的实现示例

《pandas中位数填充空值的实现示例》中位数填充是一种简单而有效的方法,用于填充数据集中缺失的值,本文就来介绍一下pandas中位数填充空值的实现,具有一定的参考价值,感兴趣的可以了解一下... 目录什么是中位数填充?为什么选择中位数填充?示例数据结果分析完整代码总结在数据分析和机器学习过程中,处理缺失数