微信搶紅包過期失效實戰案例

来源:https://www.cnblogs.com/smallSevens/archive/2020/02/11/12296225.html

前言 微信紅包業務,發紅包之後如果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


您的分享是我們最大的動力!

更多相關文章
  • 一、定寬高 1、絕對定位和負margin值: <section class="absolute"> <div></div> </section> <style> section{ display:block; } section.absolute { width: 100px; height: 10 ...
  • 最近正在學習node.js,就像搞一些東西來玩玩,於是這個簡單的爬蟲就誕生了。 ...
  • 先看let和var: 1. console.log(a); // undefined var a = 3; console.log(a); // Uncaught ReferenceError: Cannot access 'a' before initialization let a = 3; 在 ...
  • 報錯信息: 1 Access to XMLHttpRequest at 'http://192.168.37.130:5050/socket.io/?EIO=3&transport=polling&t=N0oqNsW' from origin 'http://localhost:8080' has ...
  • 全局作用域和局部作用域 全局作用域 局部作用域:函數作用域 全局作用域在全局和局部都可以訪問到,局部作用域只能在局部被訪問到 var name="cyy"; function fn(){ var age=25; console.log(name);//cyy console.log(age);//2 ...
  • 什麼是網頁?html文檔經過瀏覽器內核渲染後展示出來的頁面(五大主流瀏覽器及四大內核) html文檔文件名尾碼是.html,之前存在的.htm是為支持DOM系統(目前織夢還是用.htm文件名結尾文件) 編碼是我們通過代碼的形式把想要展示的頁面寫到html文檔裡面,接著渲染作為一個動作,主要是ht... ...
  • 最初使用回調函數 ​ 由於最初j s官方沒有明確的規範,各種第三方庫中封裝的非同步函數中傳的回調函數中的參數沒有明確的規範, 沒有明確各個參數的意義, 不便於使用。 ​ 但是node中有明確的規範 ​ node中的的回調模式: 1. 所有回調函數必須有兩個參數,第一個參數表示錯誤,第二個參數表示結果 ...
  • 前言 準確的說eval處理過的代碼應該叫做壓縮代碼,不過效果上算是加密過了一樣!很多小伙伴不想直接讓別人看到自己的js代碼往往就會採取這樣的處理措施。不過,其實這樣的方法只能防禦那些小白。對於真正的大佬來說,破解就是秒秒鐘的事!真的就是秒秒鐘!話不多說,我們直接進入正題! 演示代碼: 操作步驟 1. ...
一周排行
  • 微信公眾號dotnet跨平臺2020年初做的一個關於中國.NET開發者調查收到了開發者近 1400 條回覆。這份調查報告涵蓋了開發者工具鏈的所有部分,包括編程語言、應用架構、應用伺服器、運行時平臺、框架技術、框架配置、IDE、.NET/.NET Core 發行版部署模式、構建工具和Kubernete... ...
  • Winform控制項的雙緩衝。控制項的雙緩衝屬性是隱藏的,可以通過反射改變其屬性值。 lv.GetType().GetProperty("DoubleBuffered", BindingFlags.Instance | BindingFlags.NonPublic).SetValue(lv, true, ...
  • 1. 需求 上圖這種包含多選(CheckBox)和單選(RadioButton)的菜單十分常見,可是在WPF中只提供了多選的MenuItem。順便一提,要使MenuItem可以多選,只需要將MenuItem的 屬性設置為True: 不知出於何種考慮,WPF沒有為MenuItem提供單選的功能。為了在 ...
  • gRPC的結構 在我們搭建gRPC通信系統之前,首先需要知道gRPC的結構組成。 首先,需要一個server(伺服器),它用來接收和處理請求,然後返迴響應。 既然有server,那麼肯定有client(客戶端),client的作用就是向server發送請求,具體就是生成一個請求,然後把它發送到ser ...
  • 區別 OpenId: Authentication :認證 Oauth: Aurhorize :授權 輸入賬號密碼,QQ確認輸入了正確的賬號密碼可以登錄 認證 下麵需要勾選的覆選框(獲取昵稱、頭像、性別) 授權 OpenID 當你需要訪問A網站的時候,A網站要求你輸入你的OpenId,即可跳轉到你的 ...
  • 前言 預計是通過三篇來將清楚asp.net core 3.x中的授權:1、基本概念介紹;2、asp.net core 3.x中授權的預設流程;3、擴展。 在完全沒有概念的情況下無論是看官方文檔還是源碼都暈乎乎的,希望本文能幫到你。不過我也是看源碼結合官方文檔看的,可能有些地方理解不對,所以只作為參考 ...
  • 簡介 基於生產者消費者模式,我們可以開發出線程安全的非同步消息隊列。 知識儲備 什麼是生產者消費者模式? 為了方便理解,我們暫時將它理解為垃圾的產生到結束的過程。 簡單來說,多住戶產生垃圾(生產者)將垃圾投遞到全小區唯一一個垃圾桶(單隊列),環衛將垃圾桶中的垃圾進行處理(消費者)。就是一個生產者消費者 ...
  • 很多時候,需要對類中的方法進行一些測試,來判斷是否能按要求輸出預期的結果。 C#提供了快速創建單元測試的方法,但單元測試不僅速度慢不方便,大量的單元測試還會拖慢項目的啟動速度。 所以決定自己搞個方便的測試用例。 控制台一句話調用。 測試用例.註冊並Print(EnumEx.Name); 結果畫面: ...
  • 常成員函數不能改變數據成員的值,例如定義坐標類Coordinate,成員函數changeX():void Coordinate::changeX(){ x = 10;}雖然changeX()沒有參數,但是它隱含一個參數——this指針:void Coordinate::changeX(Coordin... ...
  • 因為新冠肺炎疫情,診所還沒復工。這是在家用手機敲的,代碼顯示有問題。等復工以後在電腦上改,各位先湊和看吧。 支持向量機(Support Vector Machine, SVM)是一種基於統計學習的模式識別的分類方法,主要用於模式識別。所謂支持向量指的是在分割區域邊緣的訓練樣本點,機是指演算法。就是要找 ...
x