抢红包的需求分析

抢红包的场景有点像秒杀,但是要比秒杀简单点。
因为秒杀通常要和库存相关。而抢红包则可以允许有些红包没有被抢到,因为发红包的人不会有损失,没抢完的钱再退回给发红包的人即可。
另外像小米这样的抢购也要比淘宝的要简单,也是因为像小米这样是一个公司的,如果有少量没有抢到,则下次再抢,人工修复下数据是很简单的事。而像淘宝这么多商品,要是每一个都存在着修复数据的风险,那如果出故障了则很麻烦。

淘宝的专家丁奇有个文章有写到淘宝是如何应对秒杀的:《秒杀场景下MySQL的低效–原因和改进》

http://blog.nosqlfan.com/html/4209.html

基于redis的抢红包方案

下面介绍一种基于Redis的抢红包方案。

把原始的红包称为大红包,拆分后的红包称为小红包。

1.小红包预先生成,插到数据库里,红包对应的用户ID是null。生成算法见另一篇blog:http://blog.csdn.NET/hengyunabc/article/details/19177877

2.每个大红包对应两个redis队列,一个是未消费红包队列,另一个是已消费红包队列。开始时,把未抢的小红包全放到未消费红包队列里。

未消费红包队列里是json字符串,如{userId:'789', money:'300'}。

3.在redis中用一个map来过滤已抢到红包的用户。

4.抢红包时,先判断用户是否抢过红包,如果没有,则从未消费红包队列中取出一个小红包,再push到另一个已消费队列中,最后把用户ID放入去重的map中。

5.用一个单线程批量把已消费队列里的红包取出来,再批量update红包的用户ID到数据库里。

上面的流程是很清楚的,但是在第4步时,如果是用户快速点了两次,或者开了两个浏览器来抢红包,会不会有可能用户抢到了两个红包?

为了解决这个问题,采用了lua脚本方式,让第4步整个过程是原子性地执行。

下面是在redis上执行的Lua脚本:

  1. -- 函数:尝试获得红包,如果成功,则返回json字符串,如果不成功,则返回空
  2. -- 参数:红包队列名, 已消费的队列名,去重的Map名,用户ID
  3. -- 返回值:nil 或者 json字符串,包含用户ID:userId,红包ID:id,红包金额:money
  4. -- 如果用户已抢过红包,则返回nil
  5. if redis.call('hexists', KEYS[3], KEYS[4]) ~= 0 then
  6. return nil
  7. else
  8. -- 先取出一个小红包
  9. local hongBao = redis.call('rpop', KEYS[1]);
  10. if hongBao then
  11. local x = cjson.decode(hongBao);
  12. -- 加入用户ID信息
  13. x['userId'] = KEYS[4];
  14. local re = cjson.encode(x);
  15. -- 把用户ID放到去重的set里
  16. redis.call('hset', KEYS[3], KEYS[4], KEYS[4]);
  17. -- 把红包放到已消费队列里
  18. redis.call('lpush', KEYS[2], re);
  19. return re;
  20. end
  21. end
  22. return nil

下面是测试代码:

  1. public class TestEval {
  2. static String host = "localhost";
  3. static int honBaoCount = 1_0_0000;
  4. static int threadCount = 20;
  5. static String hongBaoList = "hongBaoList";
  6. static String hongBaoConsumedList = "hongBaoConsumedList";
  7. static String hongBaoConsumedMap = "hongBaoConsumedMap";
  8. static Random random = new Random();
  9. //  -- 函数:尝试获得红包,如果成功,则返回json字符串,如果不成功,则返回空
  10. //  -- 参数:红包队列名, 已消费的队列名,去重的Map名,用户ID
  11. //  -- 返回值:nil 或者 json字符串,包含用户ID:userId,红包ID:id,红包金额:money
  12. static String tryGetHongBaoScript =
  13. //          "local bConsumed = redis.call('hexists', KEYS[3], KEYS[4]);\n"
  14. //          + "print('bConsumed:' ,bConsumed);\n"
  15. "if redis.call('hexists', KEYS[3], KEYS[4]) ~= 0 then\n"
  16. + "return nil\n"
  17. + "else\n"
  18. + "local hongBao = redis.call('rpop', KEYS[1]);\n"
  19. //          + "print('hongBao:', hongBao);\n"
  20. + "if hongBao then\n"
  21. + "local x = cjson.decode(hongBao);\n"
  22. + "x['userId'] = KEYS[4];\n"
  23. + "local re = cjson.encode(x);\n"
  24. + "redis.call('hset', KEYS[3], KEYS[4], KEYS[4]);\n"
  25. + "redis.call('lpush', KEYS[2], re);\n"
  26. + "return re;\n"
  27. + "end\n"
  28. + "end\n"
  29. + "return nil";
  30. static StopWatch watch = new StopWatch();
  31. public static void main(String[] args) throws InterruptedException {
  32. //      testEval();
  33. generateTestData();
  34. testTryGetHongBao();
  35. }
  36. static public void generateTestData() throws InterruptedException {
  37. Jedis jedis = new Jedis(host);
  38. jedis.flushAll();
  39. final CountDownLatch latch = new CountDownLatch(threadCount);
  40. for(int i = 0; i < threadCount; ++i) {
  41. final int temp = i;
  42. Thread thread = new Thread() {
  43. public void run() {
  44. Jedis jedis = new Jedis(host);
  45. int per = honBaoCount/threadCount;
  46. JSONObject object = new JSONObject();
  47. for(int j = temp * per; j < (temp+1) * per; j++) {
  48. object.put("id", j);
  49. object.put("money", j);
  50. jedis.lpush(hongBaoList, object.toJSONString());
  51. }
  52. latch.countDown();
  53. }
  54. };
  55. thread.start();
  56. }
  57. latch.await();
  58. }
  59. static public void testTryGetHongBao() throws InterruptedException {
  60. final CountDownLatch latch = new CountDownLatch(threadCount);
  61. System.err.println("start:" + System.currentTimeMillis()/1000);
  62. watch.start();
  63. for(int i = 0; i < threadCount; ++i) {
  64. final int temp = i;
  65. Thread thread = new Thread() {
  66. public void run() {
  67. Jedis jedis = new Jedis(host);
  68. String sha = jedis.scriptLoad(tryGetHongBaoScript);
  69. int j = honBaoCount/threadCount * temp;
  70. while(true) {
  71. Object object = jedis.eval(tryGetHongBaoScript, 4, hongBaoList, hongBaoConsumedList, hongBaoConsumedMap, "" + j);
  72. j++;
  73. if (object != null) {
  74. //                          System.out.println("get hongBao:" + object);
  75. }else {
  76. //已经取完了
  77. if(jedis.llen(hongBaoList) == 0)
  78. break;
  79. }
  80. }
  81. latch.countDown();
  82. }
  83. };
  84. thread.start();
  85. }
  86. latch.await();
  87. watch.stop();
  88. System.err.println("time:" + watch.getTotalTimeSeconds());
  89. System.err.println("speed:" + honBaoCount/watch.getTotalTimeSeconds());
  90. System.err.println("end:" + System.currentTimeMillis()/1000);
  91. }
  92. }

测试结果20个线程,每秒可以抢2.5万个,足以应付绝大部分的抢红包场景。

如果是真的应付不了,拆分到几个redis集群里,或者改为批量抢红包,也足够应付。

总结:

redis的抢红包方案,虽然在极端情况下(即redis挂掉)会丢失一秒的数据,但是却是一个扩展性很强,足以应付高并发的抢红包方案。

抢红包算法 java的更多相关文章

  1. java Random 抢红包算法

    红包有一个总金额和总数量,领的时候随机分配金额. 维护一个剩余总金额和总数量,分配时,如果数量等于1,直接返回总金额,如果大于1,则计算平均值,并设定随机最大值为平均值的两倍,然后取一个随机值,如果随 ...

  2. 归并排序算法 java 实现

    归并排序算法 java 实现 可视化对比十多种排序算法(C#版) [直观学习排序算法] 视觉直观感受若干常用排序算法 算法概念 归并排序是建立在归并操作上的一种有效的排序算法,该算法是采用分治法(Di ...

  3. 快速排序算法 java 实现

    快速排序算法 java 实现 快速排序算法Java实现 白话经典算法系列之六 快速排序 快速搞定 各种排序算法的分析及java实现 算法概念 快速排序是C.R.A.Hoare于1962年提出的一种划分 ...

  4. 堆排序算法 java 实现

    堆排序算法 java 实现 白话经典算法系列之七 堆与堆排序 Java排序算法(三):堆排序 算法概念 堆排序(HeapSort)是指利用堆积树(堆)这种数据结构所设计的一种排序算法,可以利用数组的特 ...

  5. Atitit 电子商务订单号码算法(java c# php js 微信

    Atitit 电子商务订单号码算法(java c# php js  微信 1.1. Js版本的居然钱三爷里面没有..只好自己实现了. 1.2. 订单号标准化...长度16位 1.3. 订单号的结构 前 ...

  6. 无向图的最短路径算法JAVA实现

    一,问题描述 给出一个无向图,指定无向图中某个顶点作为源点.求出图中所有顶点到源点的最短路径. 无向图的最短路径其实是源点到该顶点的最少边的数目. 本文假设图的信息保存在文件中,通过读取文件来构造图. ...

  7. 无向图的最短路径算法JAVA实现(转)

    一,问题描述 给出一个无向图,指定无向图中某个顶点作为源点.求出图中所有顶点到源点的最短路径. 无向图的最短路径其实是源点到该顶点的最少边的数目. 本文假设图的信息保存在文件中,通过读取文件来构造图. ...

  8. 基于FP-Tree的关联规则FP-Growth推荐算法Java实现

    基于FP-Tree的关联规则FP-Growth推荐算法Java实现 package edu.test.ch8; import java.util.ArrayList; import java.util ...

  9. 双色球机选算法java实现

    双色球机选算法java实现 一.代码 package com.hdwang; import java.util.Random; /** * Created by admin on 2017/1/10. ...

随机推荐

  1. MyEclipse导入Maven项目pom文件第一行报错,运行Tomcat报Log4j错误--解决方法

    问题描述: 前一段时间电脑第一次导入Maven项目,又是pom文件错,改好后又是运行Tomcat报Log4j错误,一直倒腾了近一个月程序才成功跑起来,太不容易. 也上网查了很长时间,没一个方法能解决我 ...

  2. Python第一天——入门Python(1)数据定义

    数据类型: 什么是数据? 在计算机科学中,数据是指所有能输入到计算机并被计算机程序处理的符号的介质的总称,是用于输入电子计算机进行处理,具有一定意义的数字字母.符号和模拟量等的统称.现在计算机存储和处 ...

  3. MiniMetro Items

    圈代表居民区三角写字楼等工作区方块是商业区钻石是金融中心五角星是政府 十字是医院 扁扁的旅游景区

  4. hdu 5833 Zhu and 772002 异或方程组高斯消元

    ccpc网赛卡住的一道题 蓝书上的原题 但是当时没看过蓝书 今天又找出来看看 其实也不是特别懂 但比以前是了解了一点了 主要还是要想到构造异或方程组 异或方程组的消元只需要xor就好搞了 数学真的是硬 ...

  5. javascript 事件委托 和jQuery事件绑定on、off 和one

    一. 事件委托什么是事件委托?用现实中的理解就是:有100 个学生同时在某天中午收到快递,但这100 个学生不可能同时站在学校门口等,那么都会委托门卫去收取,然后再逐个交给学生.而在jQuery 中, ...

  6. 傲梅分区助手专业版 v6.2 中文免费版

    软件名称: 傲梅分区助手专业版 软件语言: 简体中文 授权方式: 免费软件 运行环境: Win7 / Vista / Win2003 / WinXP / Win2008 软件大小: 9.1MB 图片预 ...

  7. centos 6.5 安装openssl

    1.下载wget https://www.openssl.org/source/openssl-1.0.2h.tar.gz 2.解压tar zxf openssl-1.0.2h.tar.gzcd op ...

  8. HTML5学习之Web存储

    在客户端存储数据 HTML5 提供了两种在客户端存储数据的新方法: localStorage - 没有时间限制的数据存储 sessionStorage - 针对一个 session 的数据存储 之前, ...

  9. HDU 5904 LCIS

    $dp$. 这题的突破口在于要求数字是连续的. 可以分别记录两个串以某个数字为结尾的最长上升长度,然后枚举一下以哪个数字为结尾就可以得到答案了. 因为$case$有点多,不能每次$memset$,额外 ...

  10. 如何查看centos系统版本

    [root@LAMP1 config]# cat /proc/version Linux version 2.6.32-279.el6.x86_64 (mockbuild@c6b9.bsys.dev. ...