zoukankan      html  css  js  c++  java
  • 微信红包实现原理

      我们平时在用微信的时候,经常会用到‘抢红包’的功能。那么这样一个需求给我们的话,具体又应该怎么实现呢?

      

    需求分析

      1 发红包:在db、cache各新增一条记录
      2 抢红包:有人发红包之后,肯定很多人同时去抢,所以应该请求访问cache,剩余红包个数大于0就可以点击拆开红包;反之提醒红包已经被抢完了
      3 拆红包:总金额每次都是递减,可以用redis的decreby来做。
      4 查看红包记录:用户直接查db即可。
      这里面就会涉及到2个问题:
        我只发了100个红包,并发下如何保证抢到红包的人数不会超过100.
        红包总金额1w元,如何分配才能让金额不超出这个数,如何保证最后一个人一定能抢到钱.

      

    数据库表设计

    红包信息表主要字段:    谁发的红包,发红包时间,红包总个数、总金额、剩余红包信息、最后一次被抢红包时间

    CREATE TABLE `red_packet_info` (
      `id` int(11) NOT NULL AUTO_INCREMENT, 
      `red_packet_id` bigint(11) NOT NULL DEFAULT  0 COMMENT '红包id,采用timestamp+5位随机数', 
      `total_amount` int(11) NOT NULL DEFAULT 0 COMMENT '红包总金额,单位分',
      `total_packet` int(11) NOT NULL DEFAULT 0 COMMENT '红包总个数',
      `remaining_amount` int(11) NOT NULL DEFAULT 0 COMMENT '剩余红包金额,单位分',
      `remaining_packet` int(11) NOT NULL DEFAULT 0 COMMENT '剩余红包个数',
      `uid` int(20) NOT NULL DEFAULT 0 COMMENT '新建红包用户的用户标识',
      `create_time` timestamp  COMMENT '创建时间',
      `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
      PRIMARY KEY (`id`)
    ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='红包信息表,新建一个红包插入一条记录';

    抢红包记录主要字段:    红包信息、抢红包人信息,抢红包时间    (主要都是查看抢红包记录列表的那些字段)

    CREATE TABLE `red_packet_record` (
      `id` int(11) NOT NULL AUTO_INCREMENT, 
      `amount` int(11) NOT NULL DEFAULT '0' COMMENT '抢到红包的金额',
      `nick_name` varchar(32) NOT NULL DEFAULT '0' COMMENT '抢到红包的用户的用户名',
      `img_url` varchar(255) NOT NULL DEFAULT '0' COMMENT '抢到红包的用户的头像',
      `uid` int(20) NOT NULL DEFAULT '0' COMMENT '抢到红包用户的用户标识',
      `red_packet_id` bigint(11) NOT NULL DEFAULT '0' COMMENT '红包id,采用timestamp+5位随机数', 
      `create_time` timestamp  COMMENT '创建时间',
      `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
      PRIMARY KEY (`id`)
    ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='抢红包记录表,抢一个红包插入一条记录';

    编码实现

    发红包

    发红包之后,肯定立马会有很多人来抢,如果直接操作数据库会有很大的压力,所以我们把数据放到缓存里面去。

        /***
         * 发红包
         * @param uid 发红包的用户id
         * @param totalNum 红包金额
         * @param totalAmount 红包总个数
         * @return
         */
        @GetMapping("/addPacket")
        public String saveRedPacket(Integer uid, Integer totalNum, Integer totalAmount) {
            // 组装数据
            RedPacketInfo record = new RedPacketInfo();
            record.setUid(uid);
            record.setTotalAmount(totalAmount);
            record.setTotalPacket(totalNum);
            record.setCreateTime(new Date());
            record.setRemainingAmount(totalAmount);
            record.setRemainingPacket(totalNum);
            // 雪花算法生成唯一id
            long redPacketId = new SnowflakeDistributeId(0, 0).nextId();
            record.setRedPacketId(redPacketId);
            // 红包保存到数据库
            redPacketInfoMapper.insert(record);
            // 红包个数和总金额存入缓存
            redisService.set(redPacketId + "_totalNum", totalNum + "");
            redisService.set(redPacketId + "_totalAmount", totalAmount + "");
            return "success";
        }

    抢红包

    用户点击红包之后,就查看红包数量,如果为0的话,点击拆红包就提示红包被抢完了;反之获取到红包金额数量

        /**
         * 抢红包
         * @param redPacketId 红包id
         * @param uid 用户id
         * @return
         */
        @GetMapping("/getPacket")
        public String getRedPacket(long redPacketId, Integer uid) {
            Object record = redisService.get(uid + RECORD + redPacketId);
            // 如果用户已经抢过红包了,那点击抢红包就应该是查看抢红包的详细记录
            if (StringUtils.isNotBlank((String)record)){
                return "红包详细记录";
            }
            // 查询红包剩余个数
            String redPacketName = redPacketId + TOTAL_NUM;
            String num = (String) redisService.get(redPacketName);
            if (StringUtils.isNotBlank(num)) {
                return num;
            }
            return "0";
        }

    拆红包(核心)

    这是重点也是难点,我们要保证领取红包的人数不能超过设置的红包个数,还要保证每一个人的红包都能抢到钱、还不能超过总金额。这就会涉及到线程安全问题。现在我们就来来想想,如何合理的生成红包随机金额数量。
      1. 剩余总金额/剩余总个数 = 红包金额平均数
      2. 由于红包是随机金额,我们的红包金额可以在这个平均值左右浮动,总和不变即可
      这样设计,才能真正保证每个人拆开都能领到钱,而且总金额不会超支

        /**
         * 拆红包
         * @param redPacketId 红包id
         * @param uid 用户id
         * @return
         */
        @GetMapping("/getRedPacketMoney")
        public String getRedPacketMoney(int uid, long redPacketId) {
            // 抢到的红包金额
            Integer randomAmount = 0;
            String redPacketName = redPacketId + TOTAL_NUM;
            String totalAmountName = redPacketId + TOTAL_AMOUNT;
            // 预减获取红包剩余数量,decr原子减来防止领取人数超过红包个数
            long decr = redisService.decr(totalAmountName, 1);
            if (decr<0){
                System.out.println(uid+":  抱歉!红包已经抢完了");
                return "抱歉!红包已经抢完了";
            }
            // 下面就开始随机分配金额了,并发下可能领取人数的业务逻辑同时走到了这里,
            // 下面算法最后计算出来的金额就会和总金额有偏差,所以我们可以通过对红包
            // id进行路由,放入同一个队列里面,从而保证顺序消费,
            // 这样金额总和就和总金额不会有偏差
    
            // 剩余总金额(后面所有逻辑,都由下游业务去队列里面执行)
            Integer totalAmountInt = Integer.parseInt((String) redisService.get(redPacketName));
            // 剩余金额 / 剩余红包个数 * 2 = 最大红包金额
            Integer maxMoney = (int) (totalAmountInt / (decr + 1) * 2);
            Random random = new Random();
            // 红包取值随机数,不超过最大金额(如果是最后一个红包,金额就是剩下的所有钱)
            randomAmount = random.nextInt(maxMoney);
            System.out.println(uid+":  抢到了  "+randomAmount+"   分钱");
            // 红包剩余个数减1,同时剩余金额也要减少
            redisService.decr(redPacketName,randomAmount);  //redis decreby功能
            redisService.set(uid + RECORD + redPacketId,randomAmount.toString());
            // 数据库插入抢红包记录
            updateRacketInDB(uid, redPacketId,randomAmount);
            return randomAmount + "";
        }
    
        public void updateRacketInDB(int uid, long redPacketId, int amount) {
            // 数据库插入抢红包记录
            RedPacketRecord redPacketRecord = new RedPacketRecord();
            redPacketRecord.setUid(uid);
            redPacketRecord.setRedPacketId(redPacketId);
            redPacketRecord.setAmount(amount);
            redPacketRecord.setCreateTime(new Date());
            redPacketRecordMapper.insertSelective(redPacketRecord);
            // 查询到红包信息
            RedPacketInfoExample example = new RedPacketInfoExample ();
            RedPacketInfoExample.Criteria criteria = example.createCriteria();
            criteria.andRedPacketIdEqualTo(redPacketId);
            RedPacketInfo redPacketInfo = redPacketInfoMapper.selectByExample(example).get(0);
            // 修改红包剩余信息
            redPacketInfo.setRemainingPacket(redPacketInfo.getRemainingPacket()-amount);
            redPacketInfo.setRemainingAmount(redPacketInfo.getRemainingAmount()-1);
            redPacketInfo.setCreateTime(new Date());
            redPacketInfoMapper.updateByPrimaryKey(redPacketInfo);
        }
  • 相关阅读:
    java总结1
    immutable
    iconfont
    实战build-react(三)+ style-components
    PHP判断字符串的包含
    win7 64位操作系统中 Oracle 11g 安装教程(图解)
    我在博客园写博客的原因
    面向对象程序设计-C++_课时17函数重载和默认参数
    面向对象程序设计-C++_课时16子类父类关系
    面向对象程序设计-C++_课时14对象组合_课时15继承
  • 原文地址:https://www.cnblogs.com/wlwl/p/12521166.html
Copyright © 2011-2022 走看看