本文将讲述使用redis实现抢红包功能,采用发红包时将红包拆好存储,解决红包金额平衡问题(两种算法)、解决超发现象、将数据通过消息队列传递给另一个服务写入数据库,现阶段不考虑redis宕机的情况。

--新增余额处理。

框架为:springboot2.x,环境搭建、maven配置略。

一个简单的前端页面模拟并发量:

两个功能:一个发红包和一个抢红包

<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><title>Title</title>
</head>
<body>
<script type="text/javascript" src="/js/jquery.min.js"></script>
<script type="text/javascript">
function  aaa(){var redId=1;var num=document.getElementById("userNum").value;for (var i = 1; i<=num;i++){$.post("http://localhost:8081/rushToBuy/redPaper",{"userId":i,"redId":redId},function (result) {});}
}
function  bbb(){var redId=1;var amount=document.getElementById("amount").value*100;var num=document.getElementById("num").value;if (amount<num){alert("每个红包最少一分钱")return;}$.post("http://localhost:8081/rushToBuy/sendRedPaperLine",{"redId":redId,"amount":amount,"num":num},function (result) {});
}
</script>抢红包 <button id="but1" onclick="aaa()">启动</button>  |抢红包人数:<input id="userNum"><br>
发红包 <button id="but2" onclick="bbb()">发送</button>  |红包金额:<input id="amount"> 红包个数:<input id="num">
</body>
</html>

一、我们先来实现redis功能

实现发红包时将红包拆分存储到redis

使用的算法1:线性切割法。

中心思想:将总金额想象成一条那么长的线段,需要分割成num份,随机num-1次,将每次的随机值映射到该线段上。这样的好处是将随机交给程序,缺点是有小概率造成某个人分配过多(抢红包嘛,只要不是太离谱就可以接受)。

代码思路:获取(0,max)的随机数(防止有人抢到红包但金额为0),使用Treeset进行排序去重(去重是为了防止随机到相同大小,会导致一个人抢到红包但金额为0),然后循环将两个区间内的差值作为红包金额存入redis。

 /*算法1 分段切割法 红包算法*/public void sendRedPaperLine(int redId,int amount,int num){Random random=new Random();int m=0,n=amount;Set<Integer> sets = new TreeSet<>();//(m,n)区间for (int i =0;i<(num-1);i++){int randInt = random.nextInt(n-m-1)+(m+1);  //将区间控制在(0,max) ,不能出现为0和最大的情况sets.add(randInt);}while (sets.size()<(num-1)){int randInt = random.nextInt(n-m-1)+(m+1);sets.add(randInt);}int cur=0; //做为当前set循环中的上一参数for (Integer i:sets){redisTemplate.opsForList().leftPush("redId:"+redId,(double)(i-cur)/100);cur=i;}redisTemplate.opsForList().leftPush("redId:"+redId,(double)(amount-cur)/100);}

因为算法1使用的redis中的list,所以取走一个少一个,不会存在多人拿到同一个的情况,所以可以忽略超发问题。

那么不需要考虑超发问题,抢红包时的代码就非常简单。

  public void RushRedPaper(int redId, int userId) {Double amount = (Double) redisTemplate.opsForList().leftPop("redId:"+redId);if (amount!=null){RedPaperUserInfo userInfo = new RedPaperUserInfo();userInfo.setRedId(redId);userInfo.setCreateTime(LocalDateTime.now());userInfo.setUserId(userId);userInfo.setRushAmount(Double.valueOf(amount));redisTemplate.opsForHash().put("redInfo:"+redId,"user:"+userId,userInfo);System.out.println("用户id:"+userId+"抢到了"+Double.valueOf(amount)+"元");}}

测试一下:100元红包,20个,100人抢。

输出结果:让我们恭喜一个4号倒霉蛋。

用户id:6抢到了2.29元
用户id:4抢到了0.09元
用户id:3抢到了13.56元
用户id:2抢到了7.86元
用户id:1抢到了5.18元
用户id:5抢到了5.36元
用户id:11抢到了2.68元
用户id:7抢到了11.6元
用户id:9抢到了1.16元
用户id:8抢到了6.48元
用户id:10抢到了1.82元
用户id:12抢到了0.47元
用户id:13抢到了5.72元
用户id:16抢到了3.72元
用户id:17抢到了17.56元
用户id:14抢到了5.89元
用户id:18抢到了5.55元
用户id:15抢到了1.18元
用户id:20抢到了0.93元
用户id:19抢到了0.9元

算法2.两倍均值法

中心思想:剩余红包金额为M,剩余人数为N,每次抢到的金额 = 随机区间 (0, M/N *2)

代码实现:抢红包代码不变,只改变发红包时的代码,需要注意的是最后一个人要把剩余的所有金额拿走。

  /*算法2 二倍均值法 红包算法*/public void sendRedPaperTwo(int redId,int amount,int num){Random random=new Random();//剩余红包金额为M,剩余人数为N,每次抢到的金额 = 随机区间 (0, M/N *2)for (;num>1;num--){int randInt = random.nextInt(amount/num*2-1)+1;  //将区间控制在(0, M/N *2) ,不能出现为0和最大的情况amount -= randInt;redisTemplate.opsForList().leftPush("redId:"+redId,(double)randInt/100);}//最后一个将剩余所有金额拿走redisTemplate.opsForList().leftPush("redId:"+redId,(double)amount/100);}

测试一下:100元红包,20个,100人抢。

输出结果:

用户id:1抢到了1.18元
用户id:2抢到了15.22元
用户id:3抢到了0.02元
用户id:4抢到了2.7元
用户id:7抢到了5.79元
用户id:6抢到了3.02元
用户id:5抢到了12.11元
用户id:8抢到了2.53元
用户id:9抢到了5.79元
用户id:10抢到了9.82元
用户id:11抢到了9.08元
用户id:12抢到了2.2元
用户id:13抢到了9.67元
用户id:14抢到了0.45元
用户id:15抢到了6.45元
用户id:16抢到了2.42元
用户id:17抢到了1.31元
用户id:19抢到了5.1元
用户id:18抢到了2.44元
用户id:20抢到了2.7元

二、通过消息队列异步实现持久化

使用的消息队列为activeMQ,搭建略。

在抢红包的方法中进行修改:

   @Autowiredprivate JmsMessagingTemplate jmsMessagingTemplate;@Value("${activemq.name}")private String name;
@Overridepublic void RushRedPaper(int redId, int userId) {Double amount = (Double) redisTemplate.opsForList().leftPop("redId:"+redId);if (amount!=null){RedPaperUserInfo userInfo = new RedPaperUserInfo();userInfo.setRedId(redId);DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");String localdataString = LocalDateTime.now().format(dtf);userInfo.setCreateTime(localdataString);userInfo.setUserId(userId);userInfo.setRushAmount(Double.valueOf(amount));redisTemplate.opsForHash().put("redInfo:"+redId,"user:"+userId,userInfo);jmsMessagingTemplate.convertAndSend(name,JSONObject.fromObject(userInfo).toString());System.out.println("用户id:"+userId+"抢到了"+Double.valueOf(amount)+"元");}}

避免包的信任问题,改由json字符串传递。

将消息发送到消息队列后,由监听器异步监听接收消息,写入到mysql进行持久化操作。

@Component
public class RedPaperListener {@Autowiredprivate RedPaperUserInfoDao redPaperUserInfoDao;@Async@JmsListener(destination = "${activemq.name}")public void getRedInfo(Message message){if (message instanceof TextMessage) {TextMessage textMessage = (TextMessage) message;try {String s = textMessage.getText();RedPaperUserInfo redPaperUserInfo=(RedPaperUserInfo)  JSONObject.toBean(JSONObject.fromObject(s), RedPaperUserInfo.class);redPaperUserInfoDao.insert(redPaperUserInfo);} catch (Exception e) {e.printStackTrace();}}}
}

三、红包余额退回

本来没写这块内容,后来发现这个余额退回也不是那么直白,毕竟设置了过期时间的key一失效便拿不到红包的信息,在网上找了一些解决方案,比如quartz框架,但这个框架暂时还没学习,后面可能会有补充。于是通过逻辑去解决这个问题。

过期退回思路:在拆红包时向redis存两条数据,一条队列存小红包的信息,一条字符串存该红包的过期时间。当红包过期触发监听事件,读取队列里红包的信息,使用完删除(既然使用了队列,只需要把里面的内容都取走即可)。避免监听不及时,在领取红包的内容也增加了判断。

代码实现:

先配置redis,打开监听。放开这一条notify-keyspace-events Ex

重启redis,在项目里增加配置。

@Configuration
public class RedisListenerConfig {@BeanRedisMessageListenerContainer container(RedisConnectionFactory connectionFactory) {RedisMessageListenerContainer container = new RedisMessageListenerContainer();container.setConnectionFactory(connectionFactory);return container;}
}

修改之前的发红包逻辑,以二倍均值法为例。

仅仅增加了一句代码:redisTemplate.opsForValue().set("copyredId:"+redId,"0",30, TimeUnit.SECONDS);

public void sendRedPaperTwo(int redId,int amount,int num){//每次发红包将两条数据放入redis中,一条存数据,一条存过期时间redisTemplate.opsForValue().set("copyredId:"+redId,"0",30, TimeUnit.SECONDS);Random random=new Random();//剩余红包金额为M,剩余人数为N,每次抢到的金额 = 随机区间 (0, M/N *2)for (;num>1;num--){int randInt = random.nextInt(amount/num*2-1)+1;  //将区间控制在(0, M/N *2) ,不能出现为0和最大的情况amount -= randInt;redisTemplate.opsForList().leftPush("redId:"+redId,(double)randInt/100);}//最后一个将剩余所有金额拿走redisTemplate.opsForList().leftPush("redId:"+redId,(double)amount/100);}

修改抢红包代码。增加了红包过期判断,当用户点击红包,如果过期则去返还这个红包余额(为了防止监听器来不及处理)。

public void RushRedPaper(int redId, int userId) {Object copyred= redisTemplate.opsForValue().get("copyredId:"+redId);//为空则已过期,当用户再次点击时清空红包if (copyred==null){Double stockMoney = 0.0;while (true){Double obj =  (Double)redisTemplate.opsForList().leftPop("redId:"+redId);if (obj==null){System.out.println("该红包已过期");break;}stockMoney+=obj;}if (Math.abs(stockMoney) > 0.000001){System.out.println("还有"+stockMoney+"元未领取,返回给用户");}return;}Double amount = (Double) redisTemplate.opsForList().leftPop("redId:"+redId);if (amount!=null){RedPaperUserInfo userInfo = new RedPaperUserInfo();userInfo.setRedId(redId);DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");String localdataString = LocalDateTime.now().format(dtf);userInfo.setCreateTime(localdataString);userInfo.setUserId(userId);userInfo.setRushAmount(Double.valueOf(amount));redisTemplate.opsForHash().put("redInfo:"+redId,"user:"+userId,userInfo);jmsMessagingTemplate.convertAndSend(name,JSONObject.fromObject(userInfo).toString());System.out.println("用户id:"+userId+"抢到了"+Double.valueOf(amount)+"元");}}

最后设置监听器,用来通知红包过期,返还红包余额。

@Component
public class RedisKeyExpirationListener extends KeyExpirationEventMessageListener {@Autowiredprivate RedisTemplate redisTemplate;public RedisKeyExpirationListener(RedisMessageListenerContainer listenerContainer) {super(listenerContainer);}/*** 针对redis数据失效事件,进行数据处理* @param message* @param pattern*/@Async@Overridepublic void onMessage(Message message, byte[] pattern) {//监听失效key,将余额返回给用户String expiredCopyKey = message.toString();String expiredKey=expiredCopyKey.substring(4);Double stockMoney=0.0;while (true){Double obj = (Double) redisTemplate.opsForList().leftPop(expiredKey);if (obj==null){break;}stockMoney+=obj;}//有余额if (Math.abs(stockMoney)>0.000001){System.out.println("还有"+stockMoney+"元未领取,返回给用户");}}
}

基于redis实现抢红包功能(包括余额退回处理)相关推荐

  1. redis实现抢红包功能

    转自:http://blog.csdn.net/hengyunabc/article/details/19433779/ 抢红包的需求分析 抢红包的场景有点像秒杀,但是要比秒杀简单点. 因为秒杀通常要 ...

  2. java基于Redis实现排行榜功能-附源码

    java基于Redis Zset实现排行榜功能 前言 做之前要思考的问题? Zset怎么存储需要的多个字段? 话不多说先上效果图 数据存储格式 代码 源码下载 闲暇之余,整理了一下之前利用Redis ...

  3. 【Redis学习02】基于session和基于redis实现登录功能

    文章目录 前言 1. 基于session实现短信登录功能 1.1 发送短信验证码 1.2 短信验证登录 1.3 登录校验功能 2. 集群的Session问题 3. 基于redis实现共享session ...

  4. java抢红包_Java 用Redis 实现抢红包功能

    红包功能描述 红包功能现在已经很普遍了,主要功能发红包和抢红包. 一发一抢或一发多抢,主要难点还是一发多择抢高并发场景. 红包功能分析 首先我们假设会员体系和金额账号体系都已经存在,我们本功能只是一个 ...

  5. java发红包redis_基于Redis实现类似微信抢红包

    一个简单的基于Redis实现抢红包功能,分为两个步骤: 1.拆分红包 /** * 拆红包 1.红包金额要被全部拆分完 2.红包金额不能差的太离谱 * * @param total * @param c ...

  6. 同程旅行王晓波:同程凤凰缓存系统在基于 Redis 方面的设计与实践(上篇)

    王晓波 同程旅行机票事业群 CTO 读完需要 12 分钟 速读仅需 4 分钟 本章和大家分享一下同程凤凰缓存系统在基于 Redis 方面的设计与实践.在本章中除了会列举我们工作过程中遇到各种问题和误区 ...

  7. 案例:同程凤凰缓存系统基于Redis的设计与实践。

    本文和大家分享一下同程凤凰缓存系统在基于Redis方面的设计与实践.在本文中除了会列举我们工作过程中遇到各种问题和误区外,还会给出我们相应的解决办法,希望能够抛砖引玉为大家带来一定的启示. 同程凤凰缓 ...

  8. 【一图流思维导图】Redis设计与实现 包括( 数据类型-数据结构) 及应用场景(登录次数校验,在线人数统计,分布式session,redis分页,判断重复注册,社交领域共同喜好,排行榜 ,topN)

    参照 Redis详解(一)------ redis的简介与安装 Redis详解(二)------ redis的配置文件介绍 Redis详解(三)------ redis的六大数据类型详细用法 Redi ...

  9. 基于Redis实现微信抢红包功能

    使用Redis和Lua的原子性实现抢红包功能 安装Lua(可选) 编写lua脚本 lua脚本学习可以参考 [https://www.runoob.com/lua/lua-basic-syntax.ht ...

最新文章

  1. JSP第二次作业_6小题
  2. matlab版本的cnn代码,Deep Learning学习 之 CNN代码解析(MATLAB)
  3. 一些与oracle相关的关于查询锁的信息
  4. 你会因为什么原因而离职
  5. 『数据库』数据库编程(概念性的东西,应用一般,甚至有点过时,用来考试)
  6. Dex文件格式扫描器:特征API的检测和扫描-小工具一枚(转载)
  7. 用FlyMcu和USB转TTL给stm32中烧录程序(stm32C8/C6)
  8. java基于SSM的宠物医院信息管理系统
  9. 我从零开始学黑莓开发的过程
  10. 在计算机硬件系统中 cache是,CPU中Cache是什么
  11. java基础[多态基础,多态数组,多态参数方法]
  12. (附源码)springboot学生宿舍管理系统 毕业设计 211955
  13. Python监控股价并通过微信提醒
  14. 案例分析: 全球顶尖的物流公司Schenker采用SAPERION
  15. c语言打出的王字图形图形,C语言编程宝典(王大刚) 3 关键字和标识符
  16. js base64解码JWT失败:VM273:1 Uncaught DOMException: Failed to execute 'atob' on 'Window': The string to
  17. Vue 6. 列表渲染
  18. zigbee3.0 BDB 介绍(一)
  19. HTMLCSS 高级表格 合并单元格
  20. 备份介质介绍之一:磁带

热门文章

  1. PS学习笔记------运用脚本及自动化批量处理
  2. Hbase2.4.1集群安装:HMaster自动挂掉问题终于解决了
  3. 中国科学家或揭开生物第六感之谜
  4. 汇编语言里 eax, ebx, ecx, edx, esi, edi, ebp, esp,eip
  5. Oracle官文,明确20c不能用于生产环境
  6. 2021年彬州范公中学高考成绩查询,彬州市拟奖励81人名单公布!快看有你认识的吗?...
  7. habor-deploy docker https 私有仓库搭建
  8. DQN_Continuous_Action
  9. python自学软件-学习python用什么软件
  10. imx6ull 14*14 平台使用mfgtools下载代码出现UTP Waiting for device to appear错误