• 微信红包实现原理


    接口开发说明

    发红包功能接口开发

    • 新增一条红包记录
    • 往 mysql 里面添加一条红包记录
    • 往 redis 里面添加一条红包数量记录
    • 往 redis 里面添加一条红包金额记录

    抢红包功能接口开发

    • 在抢红包这里并不能保证用户已经能领到这个红包
    • 抢红包只是做了一个判断,判断当前是否还有红包
    • 有红包则返回可以领
    • 没红包则返回不可以领

    拆红包功能接口开发

    • 拆红包才是用户能领到红包
    • 这时候要先减 redis 里面的金额和红包数量 decr  decreby
    • 减完金额再入库

    微信红包设计算法分析

    玩法:微信金额是拆的时候实时算出来,不是预先分配的,采用的是纯内存计算,不需要预算空间存储

    分配:

    • 发100块钱,总共10个红包,那么平均值是10块钱一个,那么发出来的红包的额度在0.01元~20元之间波动
    • 当前面4个红包总共被领了30块钱时,剩下70块钱,总共6个红包,那么这7个红包的额度在:0.01~(70➗6✖️2)=23.33之间波动
    • 这样算下去,可能会超过最开始的全部金额,因此到了最后面如果不够这么算,那么会采取如下算法:保证剩余用户能拿到最低1分钱即可

    存储:数据库会累加已经领取的个数与金额,插入一条领取记录。入账则是后台异步操作

    转账:通过财付通往红包所得者账户转账,过程通过是异步操作

    数据表设计

    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='抢红包记录表,抢一个红包插入一条记录';
    红包业务涉及到的表结构

    代码实现

    @RestController
    public class RedPacketController {
    
    
        @Autowired
        private RedisService redisService;
    
        @Autowired
        private RedPacketInfoMapper redPacketInfoMapper;
    
        @Autowired
        private RedPacketRecordMapper redPacketRecordMapper;
    
        private static final String TOTAL_NUM = "_totalNum";
        private static final String TOTAL_AMOUNT = "_totalAmount";
    
        /***
         * 发红包
         * @param uid
         * @param totalNum
         * @return
         */
        @ResponseBody
        @RequestMapping("/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);
            long redPacketId = System.currentTimeMillis();   //此时无法保证红包id唯一,最好是用雪花算法进行生成分布式系统唯一键
            record.setRedPacketId(redPacketId);
            redPacketInfoMapper.insert(record);
            redisService.set(redPacketId + "_totalNum", totalNum + "");
            redisService.set(redPacketId + "_totalAmount", totalAmount + "");
            return "success";
        }
    
    
        /**
         * 抢红包
         *
         * @param redPacketId
         * @return
         */
        @ResponseBody
        @RequestMapping("/getPacket")
        public Integer getRedPacket(long redPacketId) {
            String redPacketName = redPacketId + TOTAL_NUM;
            String num = (String) redisService.get(redPacketName);
            if (StringUtils.isNotBlank(num)) {
                return Integer.parseInt(num);
            }
            return 0;
        }
    
    
        /**
         * 拆红包
         *
         * @param redPacketId
         * @return
         */
        @ResponseBody
        @RequestMapping("/getRedPacketMoney")
        public String getRedPacketMoney(int uid, long redPacketId) {
            Integer randomAmount = 0;
            String redPacketName = redPacketId + TOTAL_NUM;
            String totalAmountName = redPacketId + TOTAL_AMOUNT;
            String num = (String) redisService.get(redPacketName);
            if (StringUtils.isBlank(num) || Integer.parseInt(num) == 0) {
                return "抱歉!红包已经抢完了";
            }
            String totalAmount = (String) redisService.get(totalAmountName);
            if (StringUtils.isNotBlank(totalAmount)) {
                Integer totalAmountInt = Integer.parseInt(totalAmount);
                Integer totalNumInt = Integer.parseInt(num);
                Integer maxMoney = totalAmountInt / totalNumInt * 2;
                Random random = new Random();
                randomAmount = random.nextInt(maxMoney);
            }
            //课堂作业:lua脚本将这两个命令一起请求
            redisService.decr(redPacketName, 1);
            redisService.decr(totalAmountName,randomAmount);  //redis decreby功能
            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(1111);
            redPacketRecord.setCreateTime(new Date());
            redPacketRecordMapper.insertSelective(redPacketRecord);
            //这里应该查出RedPacketInfo的数量,将总数量和总金额减去
        }
    
    
    }
    @Service
    public class RedisService {
    
        @Autowired
        private RedisTemplate redisTemplate;
    
        private static double size = Math.pow(2, 32);
    
    
        /**
         * 写入缓存
         *
         * @param key
         * @param offset   位 8Bit=1Byte
         * @return
         */
        public boolean setBit(String key, long offset, boolean isShow) {
            boolean result = false;
            try {
                ValueOperations<Serializable, Object> operations = redisTemplate.opsForValue();
                operations.setBit(key, offset, isShow);
                result = true;
            } catch (Exception e) {
                e.printStackTrace();
            }
            return result;
        }
    
        /**
         * 写入缓存
         *
         * @param key
         * @param offset
         * @return
         */
        public boolean getBit(String key, long offset) {
            boolean result = false;
            try {
                ValueOperations<Serializable, Object> operations = redisTemplate.opsForValue();
                result = operations.getBit(key, offset);
            } catch (Exception e) {
                e.printStackTrace();
            }
            return result;
        }
    
    
        /**
         * 写入缓存
         *
         * @param key
         * @param value
         * @return
         */
        public boolean set(final String key, Object value) {
            boolean result = false;
            try {
                ValueOperations<Serializable, Object> operations = redisTemplate.opsForValue();
                operations.set(key, value);
                result = true;
            } catch (Exception e) {
                e.printStackTrace();
            }
            return result;
        }
    
    
        /**
         * 写入缓存
         *
         * @param key
         * @return
         */
        public Object get(final String key) {
            boolean result = false;
            try {
                ValueOperations<Serializable, Object> operations = redisTemplate.opsForValue();
                return operations.get(key);
            } catch (Exception e) {
                e.printStackTrace();
                return null;
            }
        }
    
    
        /**
         * 写入缓存
         *
         * @param key
         * @param value
         * @return
         */
        public boolean decr(final String key, int value) {
            boolean result = false;
            try {
                ValueOperations<Serializable, Object> operations = redisTemplate.opsForValue();
                operations.increment(key,-value);
                result = true;
            } catch (Exception e) {
                e.printStackTrace();
            }
            return result;
        }
    
        /**
         * 写入缓存设置时效时间
         *
         * @param key
         * @param value
         * @return
         */
        public boolean set(final String key, Object value, Long expireTime) {
            boolean result = false;
            try {
                ValueOperations<Serializable, Object> operations = redisTemplate.opsForValue();
                operations.set(key, value);
                redisTemplate.expire(key, expireTime, TimeUnit.SECONDS);
                result = true;
            } catch (Exception e) {
                e.printStackTrace();
            }
            return result;
        }
    
        /**
         * 批量删除对应的value
         *
         * @param keys
         */
        public void remove(final String... keys) {
            for (String key : keys) {
                remove(key);
            }
        }
    
    
        /**
         * 删除对应的value
         *
         * @param key
         */
        public void remove(final String key) {
            if (exists(key)) {
                redisTemplate.delete(key);
            }
        }
    
        /**
         * 判断缓存中是否有对应的value
         *
         * @param key
         * @return
         */
        public boolean exists(final String key) {
            return redisTemplate.hasKey(key);
        }
    
        /**
         * 读取缓存
         *
         * @param key
         * @return
         */
        public Object genValue(final String key) {
            Object result = null;
            ValueOperations<String, String> operations = redisTemplate.opsForValue();
            result = operations.get(key);
            return result;
        }
    
        /**
         * 哈希 添加
         *
         * @param key
         * @param hashKey
         * @param value
         */
        public void hmSet(String key, Object hashKey, Object value) {
            HashOperations<String, Object, Object> hash = redisTemplate.opsForHash();
            hash.put(key, hashKey, value);
        }
    
        /**
         * 哈希获取数据
         *
         * @param key
         * @param hashKey
         * @return
         */
        public Object hmGet(String key, Object hashKey) {
            HashOperations<String, Object, Object> hash = redisTemplate.opsForHash();
            return hash.get(key, hashKey);
        }
    
        /**
         * 列表添加
         *
         * @param k
         * @param v
         */
        public void lPush(String k, Object v) {
            ListOperations<String, Object> list = redisTemplate.opsForList();
            list.rightPush(k, v);
        }
    
        /**
         * 列表获取
         *
         * @param k
         * @param l
         * @param l1
         * @return
         */
        public List<Object> lRange(String k, long l, long l1) {
            ListOperations<String, Object> list = redisTemplate.opsForList();
            return list.range(k, l, l1);
        }
    
        /**
         * 集合添加
         *
         * @param key
         * @param value
         */
        public void add(String key, Object value) {
            SetOperations<String, Object> set = redisTemplate.opsForSet();
            set.add(key, value);
        }
    
        /**
         * 集合获取
         *
         * @param key
         * @return
         */
        public Set<Object> setMembers(String key) {
            SetOperations<String, Object> set = redisTemplate.opsForSet();
            return set.members(key);
        }
    
        /**
         * 有序集合添加
         *
         * @param key
         * @param value
         * @param scoure
         */
        public void zAdd(String key, Object value, double scoure) {
            ZSetOperations<String, Object> zset = redisTemplate.opsForZSet();
            zset.add(key, value, scoure);
        }
    
        /**
         * 有序集合获取
         *
         * @param key
         * @param scoure
         * @param scoure1
         * @return
         */
        public Set<Object> rangeByScore(String key, double scoure, double scoure1) {
            ZSetOperations<String, Object> zset = redisTemplate.opsForZSet();
            redisTemplate.opsForValue();
            return zset.rangeByScore(key, scoure, scoure1);
        }
    
    
        //第一次加载的时候将数据加载到redis中
        public void saveDataToRedis(String name) {
            double index = Math.abs(name.hashCode() % size);
            long indexLong = new Double(index).longValue();
            boolean availableUsers = setBit("availableUsers", indexLong, true);
        }
    
        //第一次加载的时候将数据加载到redis中
        public boolean getDataToRedis(String name) {
    
            double index = Math.abs(name.hashCode() % size);
            long indexLong = new Double(index).longValue();
            return getBit("availableUsers", indexLong);
        }
    
        /**
         * 有序集合获取排名
         *
         * @param key 集合名称
         * @param value 值
         */
        public Long zRank(String key, Object value) {
            ZSetOperations<String, Object> zset = redisTemplate.opsForZSet();
            return zset.rank(key,value);
        }
    
    
        /**
         * 有序集合获取排名
         *
         * @param key
         */
        public Set<ZSetOperations.TypedTuple<Object>> zRankWithScore(String key, long start,long end) {
            ZSetOperations<String, Object> zset = redisTemplate.opsForZSet();
            Set<ZSetOperations.TypedTuple<Object>> ret = zset.rangeWithScores(key,start,end);
            return ret;
        }
    
        /**
         * 有序集合添加
         *
         * @param key
         * @param value
         */
        public Double zSetScore(String key, Object value) {
            ZSetOperations<String, Object> zset = redisTemplate.opsForZSet();
            return zset.score(key,value);
        }
    
    
        /**
         * 有序集合添加分数
         *
         * @param key
         * @param value
         * @param scoure
         */
        public void incrementScore(String key, Object value, double scoure) {
            ZSetOperations<String, Object> zset = redisTemplate.opsForZSet();
            zset.incrementScore(key, value, scoure);
        }
    
    
        /**
         * 有序集合获取排名
         *
         * @param key
         */
        public Set<ZSetOperations.TypedTuple<Object>> reverseZRankWithScore(String key, long start,long end) {
            ZSetOperations<String, Object> zset = redisTemplate.opsForZSet();
            Set<ZSetOperations.TypedTuple<Object>> ret = zset.reverseRangeByScoreWithScores(key,start,end);
            return ret;
        }
    
        /**
         * 有序集合获取排名
         *
         * @param key
         */
        public Set<ZSetOperations.TypedTuple<Object>> reverseZRankWithRank(String key, long start, long end) {
            ZSetOperations<String, Object> zset = redisTemplate.opsForZSet();
            Set<ZSetOperations.TypedTuple<Object>> ret = zset.reverseRangeWithScores(key, start, end);
            return ret;
        }
    
    }
    RedisService 类似工具类

    抢红包功能扩展设计

    • 将红包 ID 的请求放入请求队列中,如果发现超过红包的个数,直接返回
    • 类推出 token 令牌和秒杀设计原理

    注意点

    • 抢到红包不代表能拆成功
    • 2014 年的红包一点开就知道金额,分两次操作,先抢到金额,然后再转账。2015 年后的红包的拆和抢是分离的,需要点两次,因此会出现抢到红包了,但点开后告知红包已经被领完的状况。进入到第一个页面不代表抢到,只表示当时红包还有。
  • 相关阅读:
    前端经典书籍
    D3基本概念
    Array.map和parseInt的用法
    首屏和白屏时间计算
    css换肤总结
    文件上传总结
    js的uuid
    html5 drag事件用法
    shell脚本中的逻辑判断 文件目录属性判断 if特殊用法 case判断
    Mac vim“装逼”配置
  • 原文地址:https://www.cnblogs.com/jwen1994/p/12264359.html
Copyright © 2020-2023  润新知