欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

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

程序员文章站 2022-06-23 22:30:42
前言 微信红包业务,发红包之后如果24小时之内没有被领取完就自动过期失效。 架构设计 业务流程 老板发红包,此时缓存初始化红包个数,红包金额(单位分),并异步入库。 红包数据入延迟队列,唯一标识+失效时间 红包数据出延迟队列,根据唯一标识清空红包缓存数据、异步更新数据库、异步退回红包金额 代码案例 ......

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

前言

微信红包业务,发红包之后如果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