基于redis实现抢红包功能(包括余额退回处理)
本文將講述使用redis實(shí)現(xiàn)搶紅包功能,采用發(fā)紅包時(shí)將紅包拆好存儲(chǔ),解決紅包金額平衡問(wèn)題(兩種算法)、解決超發(fā)現(xiàn)象、將數(shù)據(jù)通過(guò)消息隊(duì)列傳遞給另一個(gè)服務(wù)寫(xiě)入數(shù)據(jù)庫(kù),現(xiàn)階段不考慮redis宕機(jī)的情況。
--新增余額處理。
框架為:springboot2.x,環(huán)境搭建、maven配置略。
一個(gè)簡(jiǎn)單的前端頁(yè)面模擬并發(fā)量:
兩個(gè)功能:一個(gè)發(fā)紅包和一個(gè)搶紅包
<%@ 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("每個(gè)紅包最少一分錢(qián)")return;}$.post("http://localhost:8081/rushToBuy/sendRedPaperLine",{"redId":redId,"amount":amount,"num":num},function (result) {}); } </script>搶紅包 <button id="but1" onclick="aaa()">啟動(dòng)</button> |搶紅包人數(shù):<input id="userNum"><br> 發(fā)紅包 <button id="but2" onclick="bbb()">發(fā)送</button> |紅包金額:<input id="amount"> 紅包個(gè)數(shù):<input id="num"> </body> </html>一、我們先來(lái)實(shí)現(xiàn)redis功能
實(shí)現(xiàn)發(fā)紅包時(shí)將紅包拆分存儲(chǔ)到redis
使用的算法1:線(xiàn)性切割法。
中心思想:將總金額想象成一條那么長(zhǎng)的線(xiàn)段,需要分割成num份,隨機(jī)num-1次,將每次的隨機(jī)值映射到該線(xiàn)段上。這樣的好處是將隨機(jī)交給程序,缺點(diǎn)是有小概率造成某個(gè)人分配過(guò)多(搶紅包嘛,只要不是太離譜就可以接受)。
代碼思路:獲取(0,max)的隨機(jī)數(shù)(防止有人搶到紅包但金額為0),使用Treeset進(jìn)行排序去重(去重是為了防止隨機(jī)到相同大小,會(huì)導(dǎo)致一個(gè)人搶到紅包但金額為0),然后循環(huán)將兩個(gè)區(qū)間內(nèi)的差值作為紅包金額存入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)區(qū)間for (int i =0;i<(num-1);i++){int randInt = random.nextInt(n-m-1)+(m+1); //將區(qū)間控制在(0,max) ,不能出現(xiàn)為0和最大的情況sets.add(randInt);}while (sets.size()<(num-1)){int randInt = random.nextInt(n-m-1)+(m+1);sets.add(randInt);}int cur=0; //做為當(dāng)前set循環(huán)中的上一參數(shù)for (Integer i:sets){redisTemplate.opsForList().leftPush("redId:"+redId,(double)(i-cur)/100);cur=i;}redisTemplate.opsForList().leftPush("redId:"+redId,(double)(amount-cur)/100);}因?yàn)樗惴?使用的redis中的list,所以取走一個(gè)少一個(gè),不會(huì)存在多人拿到同一個(gè)的情況,所以可以忽略超發(fā)問(wèn)題。
那么不需要考慮超發(fā)問(wèn)題,搶紅包時(shí)的代碼就非常簡(jiǎn)單。
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("用戶(hù)id:"+userId+"搶到了"+Double.valueOf(amount)+"元");}}測(cè)試一下:100元紅包,20個(gè),100人搶。
輸出結(jié)果:讓我們恭喜一個(gè)4號(hào)倒霉蛋。
用戶(hù)id:6搶到了2.29元 用戶(hù)id:4搶到了0.09元 用戶(hù)id:3搶到了13.56元 用戶(hù)id:2搶到了7.86元 用戶(hù)id:1搶到了5.18元 用戶(hù)id:5搶到了5.36元 用戶(hù)id:11搶到了2.68元 用戶(hù)id:7搶到了11.6元 用戶(hù)id:9搶到了1.16元 用戶(hù)id:8搶到了6.48元 用戶(hù)id:10搶到了1.82元 用戶(hù)id:12搶到了0.47元 用戶(hù)id:13搶到了5.72元 用戶(hù)id:16搶到了3.72元 用戶(hù)id:17搶到了17.56元 用戶(hù)id:14搶到了5.89元 用戶(hù)id:18搶到了5.55元 用戶(hù)id:15搶到了1.18元 用戶(hù)id:20搶到了0.93元 用戶(hù)id:19搶到了0.9元算法2.兩倍均值法
中心思想:剩余紅包金額為M,剩余人數(shù)為N,每次搶到的金額 = 隨機(jī)區(qū)間 (0, M/N *2)
代碼實(shí)現(xiàn):搶紅包代碼不變,只改變發(fā)紅包時(shí)的代碼,需要注意的是最后一個(gè)人要把剩余的所有金額拿走。
/*算法2 二倍均值法 紅包算法*/public void sendRedPaperTwo(int redId,int amount,int num){Random random=new Random();//剩余紅包金額為M,剩余人數(shù)為N,每次搶到的金額 = 隨機(jī)區(qū)間 (0, M/N *2)for (;num>1;num--){int randInt = random.nextInt(amount/num*2-1)+1; //將區(qū)間控制在(0, M/N *2) ,不能出現(xiàn)為0和最大的情況amount -= randInt;redisTemplate.opsForList().leftPush("redId:"+redId,(double)randInt/100);}//最后一個(gè)將剩余所有金額拿走redisTemplate.opsForList().leftPush("redId:"+redId,(double)amount/100);}測(cè)試一下:100元紅包,20個(gè),100人搶。
輸出結(jié)果:
用戶(hù)id:1搶到了1.18元 用戶(hù)id:2搶到了15.22元 用戶(hù)id:3搶到了0.02元 用戶(hù)id:4搶到了2.7元 用戶(hù)id:7搶到了5.79元 用戶(hù)id:6搶到了3.02元 用戶(hù)id:5搶到了12.11元 用戶(hù)id:8搶到了2.53元 用戶(hù)id:9搶到了5.79元 用戶(hù)id:10搶到了9.82元 用戶(hù)id:11搶到了9.08元 用戶(hù)id:12搶到了2.2元 用戶(hù)id:13搶到了9.67元 用戶(hù)id:14搶到了0.45元 用戶(hù)id:15搶到了6.45元 用戶(hù)id:16搶到了2.42元 用戶(hù)id:17搶到了1.31元 用戶(hù)id:19搶到了5.1元 用戶(hù)id:18搶到了2.44元 用戶(hù)id:20搶到了2.7元二、通過(guò)消息隊(duì)列異步實(shí)現(xiàn)持久化
使用的消息隊(duì)列為activeMQ,搭建略。
在搶紅包的方法中進(jìn)行修改:
@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("用戶(hù)id:"+userId+"搶到了"+Double.valueOf(amount)+"元");}}避免包的信任問(wèn)題,改由json字符串傳遞。
將消息發(fā)送到消息隊(duì)列后,由監(jiān)聽(tīng)器異步監(jiān)聽(tīng)接收消息,寫(xiě)入到mysql進(jìn)行持久化操作。
?
@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();}}} }三、紅包余額退回
本來(lái)沒(méi)寫(xiě)這塊內(nèi)容,后來(lái)發(fā)現(xiàn)這個(gè)余額退回也不是那么直白,畢竟設(shè)置了過(guò)期時(shí)間的key一失效便拿不到紅包的信息,在網(wǎng)上找了一些解決方案,比如quartz框架,但這個(gè)框架暫時(shí)還沒(méi)學(xué)習(xí),后面可能會(huì)有補(bǔ)充。于是通過(guò)邏輯去解決這個(gè)問(wèn)題。
過(guò)期退回思路:在拆紅包時(shí)向redis存兩條數(shù)據(jù),一條隊(duì)列存小紅包的信息,一條字符串存該紅包的過(guò)期時(shí)間。當(dāng)紅包過(guò)期觸發(fā)監(jiān)聽(tīng)事件,讀取隊(duì)列里紅包的信息,使用完刪除(既然使用了隊(duì)列,只需要把里面的內(nèi)容都取走即可)。避免監(jiān)聽(tīng)不及時(shí),在領(lǐng)取紅包的內(nèi)容也增加了判斷。
代碼實(shí)現(xiàn):
先配置redis,打開(kāi)監(jiān)聽(tīng)。放開(kāi)這一條notify-keyspace-events Ex
重啟redis,在項(xiàng)目里增加配置。
@Configuration public class RedisListenerConfig {@BeanRedisMessageListenerContainer container(RedisConnectionFactory connectionFactory) {RedisMessageListenerContainer container = new RedisMessageListenerContainer();container.setConnectionFactory(connectionFactory);return container;} }修改之前的發(fā)紅包邏輯,以二倍均值法為例。
僅僅增加了一句代碼:redisTemplate.opsForValue().set("copyredId:"+redId,"0",30, TimeUnit.SECONDS);
public void sendRedPaperTwo(int redId,int amount,int num){//每次發(fā)紅包將兩條數(shù)據(jù)放入redis中,一條存數(shù)據(jù),一條存過(guò)期時(shí)間redisTemplate.opsForValue().set("copyredId:"+redId,"0",30, TimeUnit.SECONDS);Random random=new Random();//剩余紅包金額為M,剩余人數(shù)為N,每次搶到的金額 = 隨機(jī)區(qū)間 (0, M/N *2)for (;num>1;num--){int randInt = random.nextInt(amount/num*2-1)+1; //將區(qū)間控制在(0, M/N *2) ,不能出現(xiàn)為0和最大的情況amount -= randInt;redisTemplate.opsForList().leftPush("redId:"+redId,(double)randInt/100);}//最后一個(gè)將剩余所有金額拿走redisTemplate.opsForList().leftPush("redId:"+redId,(double)amount/100);}修改搶紅包代碼。增加了紅包過(guò)期判斷,當(dāng)用戶(hù)點(diǎn)擊紅包,如果過(guò)期則去返還這個(gè)紅包余額(為了防止監(jiān)聽(tīng)器來(lái)不及處理)。
public void RushRedPaper(int redId, int userId) {Object copyred= redisTemplate.opsForValue().get("copyredId:"+redId);//為空則已過(guò)期,當(dāng)用戶(hù)再次點(diǎn)擊時(shí)清空紅包if (copyred==null){Double stockMoney = 0.0;while (true){Double obj = (Double)redisTemplate.opsForList().leftPop("redId:"+redId);if (obj==null){System.out.println("該紅包已過(guò)期");break;}stockMoney+=obj;}if (Math.abs(stockMoney) > 0.000001){System.out.println("還有"+stockMoney+"元未領(lǐng)取,返回給用戶(hù)");}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("用戶(hù)id:"+userId+"搶到了"+Double.valueOf(amount)+"元");}}最后設(shè)置監(jiān)聽(tīng)器,用來(lái)通知紅包過(guò)期,返還紅包余額。
@Component public class RedisKeyExpirationListener extends KeyExpirationEventMessageListener {@Autowiredprivate RedisTemplate redisTemplate;public RedisKeyExpirationListener(RedisMessageListenerContainer listenerContainer) {super(listenerContainer);}/*** 針對(duì)redis數(shù)據(jù)失效事件,進(jìn)行數(shù)據(jù)處理* @param message* @param pattern*/@Async@Overridepublic void onMessage(Message message, byte[] pattern) {//監(jiān)聽(tīng)失效key,將余額返回給用戶(hù)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+"元未領(lǐng)取,返回給用戶(hù)");}} }?
總結(jié)
以上是生活随笔為你收集整理的基于redis实现抢红包功能(包括余额退回处理)的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 加贺电子发表手掌大小的小型轻量DLP放映
- 下一篇: 微信支付可能改变的六大行业