基于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,搭建略。

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

   @Autowired
    private JmsMessagingTemplate jmsMessagingTemplate;
    @Value("${activemq.name}")
    private String name;   
@Override
    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);
            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 {
    @Autowired
    private 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 {
    @Bean
    RedisMessageListenerContainer 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 {
    @Autowired
    private RedisTemplate redisTemplate;

    public RedisKeyExpirationListener(RedisMessageListenerContainer listenerContainer) {
        super(listenerContainer);
    }

    /**
     * 针对redis数据失效事件,进行数据处理
     * @param message
     * @param pattern
     */
    @Async
    @Override
    public 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+"元未领取,返回给用户");
        }

    }
}

猜你喜欢

转载自blog.csdn.net/qq_39404258/article/details/107491620