• 微信抢红包过期失效实战案例


    前言

    微信红包业务,发红包之后如果24小时之内没有被领取完就自动过期失效。

    架构设计

    业务流程

    • 老板发红包,此时缓存初始化红包个数,红包金额(单位分),并异步入库。

    • 红包数据入延迟队列,唯一标识+失效时间

    • 红包数据出延迟队列,根据唯一标识清空红包缓存数据、异步更新数据库、异步退回红包金额

    代码案例

    这里我们使用Java内置的DelayQueue来实现,DelayQueue是一个无界的BlockingQueue,用于放置实现了Delayed接口的对象,其中的对象只能在其到期时才能从队列中取走。这种队列是有序的,即队头对象的延迟到期时间最长。

    老板发了10个红包一共200人民币,假装只有9个人抢红包。

    发红包,缓存数据进入延迟队列:

       /**
         * 有人没抢 红包发多了
         * 红包进入延迟队列
         * 实现过期失效
         * @param redPacketId
         * @return
         */
        @ApiOperation(value="抢红包三",nickname="爪哇笔记")
        @PostMapping("/startThree")
        public Result startThree(long redPacketId){
            int skillNum = 9;
            final CountDownLatch latch = new CountDownLatch(skillNum);//N个抢红包
            /**
             * 初始化红包数据,抢红包拦截
             */
            redisUtil.cacheValue(redPacketId+"-num",10);
            /**
             * 初始化红包金额,单位为分
             */
            redisUtil.cacheValue(redPacketId+"-money",20000);
            /**
             * 加入延迟队列 24s秒过期
             */
            RedPacketMessage message = new RedPacketMessage(redPacketId,24);
            RedPacketQueue.getQueue().produce(message);
            /**
             * 模拟 9个用户抢10个红包
             */
            for(int i=1;i<=skillNum;i++){
                int userId = i;
                Runnable task = () -> {
                    /**
                     * 抢红包 判断剩余金额
                     */
                    Integer money = (Integer) redisUtil.getValue(redPacketId+"-money");
                    if(money>0){
                        Result result = redPacketService.startTwoSeckil(redPacketId,userId);
                        if(result.get("code").toString().equals("500")){
                            LOGGER.info("用户{}手慢了,红包派完了",userId);
                        }else{
                            Double amount = DoubleUtil.divide(Double.parseDouble(result.get("msg").toString()), (double) 100);
                            LOGGER.info("用户{}抢红包成功,金额:{}", userId,amount);
                        }
                    }
                    latch.countDown();
                };
                executor.execute(task);
            }
            try {
                latch.await();
                Integer restMoney = Integer.parseInt(redisUtil.getValue(redPacketId+"-money").toString());
                LOGGER.info("剩余金额:{}",restMoney);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return Result.ok();
        }
    

    红包队列消息:

    /**
     * 红包队列消息
     */
    public class RedPacketMessage implements Delayed {
    
        private static final DateTimeFormatter F = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
    
        /**
         * 默认延迟3秒
         */
        private static final long DELAY_MS = 1000L * 3;
    
        /**
         * 红包 ID
         */
        private final long redPacketId;
    
        /**
         * 创建时间戳
         */
        private final long timestamp;
    
        /**
         * 过期时间
         */
        private final long expire;
    
        /**
         * 描述信息
         */
        private final String description;
    
        public RedPacketMessage(long redPacketId, long expireSeconds) {
            this.redPacketId = redPacketId;
            this.timestamp = System.currentTimeMillis();
            this.expire = this.timestamp + expireSeconds * 1000L;
            this.description = String.format("红包[%s]-创建时间为:%s,超时时间为:%s", redPacketId,
                    LocalDateTime.ofInstant(Instant.ofEpochMilli(timestamp), ZoneId.systemDefault()).format(F),
                    LocalDateTime.ofInstant(Instant.ofEpochMilli(expire), ZoneId.systemDefault()).format(F));
        }
    
        public RedPacketMessage(long redPacketId) {
            this.redPacketId = redPacketId;
            this.timestamp = System.currentTimeMillis();
            this.expire = this.timestamp + DELAY_MS;
            this.description = String.format("红包[%s]-创建时间为:%s,超时时间为:%s", redPacketId,
                    LocalDateTime.ofInstant(Instant.ofEpochMilli(timestamp), ZoneId.systemDefault()).format(F),
                    LocalDateTime.ofInstant(Instant.ofEpochMilli(expire), ZoneId.systemDefault()).format(F));
        }
    
        public long getRedPacketId() {
            return redPacketId;
        }
    
        public long getTimestamp() {
            return timestamp;
        }
    
        public long getExpire() {
            return expire;
        }
    
        public String getDescription() {
            return description;
        }
    
        @Override
        public long getDelay(TimeUnit unit) {
            return unit.convert(this.expire - System.currentTimeMillis(), TimeUnit.MILLISECONDS);
        }
    
        @Override
        public int compareTo(Delayed o) {
            return (int) (this.getDelay(TimeUnit.MILLISECONDS) - o.getDelay(TimeUnit.MILLISECONDS));
        }
    }
    

    红包延迟队列:

    /**
     * 红包延迟队列
     */
    public class RedPacketQueue {
    
        /** 用于多线程间下单的队列 */
        private static DelayQueue<RedPacketMessage> queue = new DelayQueue<>();
    
        /**
         * 私有的默认构造子,保证外界无法直接实例化
         */
        private RedPacketQueue(){}
        /**
         * 类级的内部类,也就是静态的成员式内部类,该内部类的实例与外部类的实例
         * 没有绑定关系,而且只有被调用到才会装载,从而实现了延迟加载
         */
        private static class SingletonHolder{
            /**
             * 静态初始化器,由JVM来保证线程安全
             */
            private  static RedPacketQueue queue = new RedPacketQueue();
        }
        //单例队列
        public static RedPacketQueue getQueue(){
            return SingletonHolder.queue;
        }
        /**
         * 生产入队
         * 1、执行加锁操作
         * 2、把元素添加到优先级队列中
         * 3、查看元素是否为队首
         * 4、如果是队首的话,设置leader为空,唤醒所有等待的队列
         * 5、释放锁
         */
        public  Boolean  produce(RedPacketMessage message){
            return queue.add(message);
        }
        /**
         * 消费出队
         * 1、执行加锁操作
         * 2、取出优先级队列元素q的队首
         * 3、如果元素q的队首/队列为空,阻塞请求
         * 4、如果元素q的队首(first)不为空,获得这个元素的delay时间值
         * 5、如果first的延迟delay时间值为0的话,说明该元素已经到了可以使用的时间,调用poll方法弹出该元素,跳出方法
         * 6、如果first的延迟delay时间值不为0的话,释放元素first的引用,避免内存泄露
         * 7、判断leader元素是否为空,不为空的话阻塞当前线程
         * 8、如果leader元素为空的话,把当前线程赋值给leader元素,然后阻塞delay的时间,即等待队首到达可以出队的时间,在finally块中释放leader元素的引用
         * 9、循环执行从1~8的步骤
         * 10、如果leader为空并且优先级队列不为空的情况下(判断还有没有其他后续节点),调用signal通知其他的线程
         * 11、执行解锁操作
         */
        public  RedPacketMessage consume() throws InterruptedException {
            return queue.take();
        }
    }
    

    红包延迟队列过期消费,监听任务:

    /**
     * 红包延迟队列过期消费
     */
    @Component("redPacket")
    public class TaskRunner implements ApplicationRunner {
    
        private final static Logger LOGGER = LoggerFactory.getLogger(TaskRunner.class);
    
        @Autowired
        private RedisUtil redisUtil;
    
        ExecutorService executorService = Executors.newSingleThreadExecutor(r -> {
            Thread thread = new Thread(r);
            thread.setName("RedPacketDelayWorker");
            thread.setDaemon(true);
            return thread;
        });
    
        @Override
        public void run(ApplicationArguments var){
            executorService.execute(() -> {
                while (true) {
                    try {
                        RedPacketMessage message = RedPacketQueue.getQueue().consume();
                        if(message!=null){
                            long redPacketId = message.getRedPacketId();
                            LOGGER.info("红包{}过期了",redPacketId);
                            /**
                             * 获取剩余红包个数以及金额
                             */
                            int num = (int) redisUtil.getValue(redPacketId+"-num");
                            int restMoney = (int) redisUtil.getValue(redPacketId+"-money");
                            LOGGER.info("剩余红包个数{},剩余红包金额{}",num,restMoney);
                            /**
                             * 清空红包数据
                             */
                            redisUtil.removeValue(redPacketId+"-num");
                            redisUtil.removeValue(redPacketId+"-money");
                            /**
                             * 异步更新数据库、异步退回红包金额
                             */
                        }
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            });
        }
    }
    

    适用场景

    淘宝订单到期,下单成功后60s之后给用户发送短信通知,限时支付、缓存系统等等。

    演示

    Application中有接口演示说明,你可以在抢红包 Red Packet Controller接口中输入任何参数进行测试,也可以配合数据库稍加修改即可作为生产环境的抢红包功能模块。

    源码

    https://gitee.com/52itstyle/spring-boot-seckill

  • 相关阅读:
    基于Netty实现高性能通信程序之传输协议编码与解码
    博客园停止文章更新,最新文章请访问 www.zhaoyafei.cn,多谢您的支持!
    再谈PHP错误与异常处理
    C语言之预处理
    【转】linux sort 命令详解
    GO语言之channel
    浅谈Yii-admin的权限控制
    【转】搞清FastCgi与PHP-fpm之间的关系
    网站添加第三方登陆(PHP版)
    【转】PHP的Trait 特性
  • 原文地址:https://www.cnblogs.com/smallSevens/p/12296225.html
Copyright © 2020-2023  润新知