基于redis实现抢红包功能(包括余额退回处理)
本文將講述使用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实现抢红包功能(包括余额退回处理)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 加贺电子发表手掌大小的小型轻量DLP放映
- 下一篇: 使用pgAdmin把Excel文件导入P