全部產品
Search
文件中心

Tair (Redis® OSS-Compatible):使用Bloom Filter高效管理遊戲活動推送

更新時間:Nov 26, 2024

本文介紹如何在遊戲業務中使用Bloom Filter來實現營運活動的推送控制,避免向同一玩家重複推送。本文將結合代碼(以Jedis用戶端為例),展示如何使用Jedis串連Tair(企業版)並操作Bloom Filter資料。

背景資訊

在現代的遊戲營運中,遊戲開發人員和營運團隊常常會推出各種的活動以提高使用者的活躍度、參與度或付費率。通常以彈窗、站內信、NPC任務等形式將活動推送給玩家。然而,在一個複雜的遊戲環境中,開發人員需要確保這些活動資訊的發送頻率能夠被控制,並避免重複推送相同資訊而帶來使用者體驗下降的問題,同時又需要保持整個系統的高效性。

在上述情境中,使用Bloom Filter資料結構是一種高效的解決方案,可以高效地實現對彈窗的重複控制。Bloom Filter是一種空間效率極高的機率型資料結構,用於判斷某個元素是否在集合中。它可以很快地返回某個元素可能在集合中或者一定不在集合中,優點是具有較低的空間複雜度和查詢時間複雜度,適合處理大量資料的情境,缺點是可能存在誤判(在該情境中,誤判為漏推送給某使用者,不會重複推送)。

在該情境中使用Bloom Filter的優勢:

  • 高效性:由於Bloom Filter使用的是位元組,操作極為快速,並且其空間複雜度較低,非常適合儲存大量的使用者資料。

  • 低記憶體佔用:與傳統的集合結構相比,Bloom Filter佔用的空間要少得多,尤其在儲存數百萬個玩家的彈窗狀態時,這一優勢更加明顯。

  • 可擴充性:由於Bloom Filter的擴充性非常好,它適用於大規模的分布式情境,比如Redis叢集。

Tair(企業版)提供的Bloom資料結構相容Redis Bloom Filter資料結構,使用方式也與Redis Bloom Filter一致。

方案概述

以下為範例程式碼的概述,具體實現請參見下方的範例程式碼。

  1. 串連Tair(企業版)執行個體。

  2. 建立名為activity_popup的Bloom Filter資料結構,具體實現可參見範例程式碼中的createBloom函數。

    本樣本建立Bloom Filter預計儲存50000個元素,誤判率設定為1%(即0.01)。

    誤判率設定建議

    在使用Bloom Filter時,誤判率是一個關鍵設計決策,誤判是指錯誤地認為某個不在集合中的元素是存在的。誤判率越低,過濾器的準確性越高,但佔用的記憶體空間也越大。因此設定誤判率需要在精度和記憶體空間之間做權衡,誤判率的設定建議如下:

    • 低誤判率(如0.01%):如果業務情境對誤判率非常敏感,如安全系統、金融系統等,則應該選擇非常低的誤判率(如 0.01% 或更低),但這會增加記憶體的開銷。

    • 中誤判率(如0.1%到1%):對大多數情境來說,這個誤判率是合理的折中方案。既能保持較好的記憶體使用量效率,又能保證較低的誤判率。

    • 高誤判率(如1% 以上):在對準確性要求較低的情境(如緩衝預熱、推薦系統等),可以選擇1%甚至更高的誤判率。這種情況下,記憶體需求較低,但誤判率較高。

  3. 當玩家登入時,打算向該玩家進行推送,具體實現可參見範例程式碼中的handlePopup函數。

    但推送前需要先檢查是否需要推送,具體實現可參見範例程式碼中的shouldShowPopup函數。

    • 若玩家ID不在Bloom Filter中,表示未進行推送,此時需要向該玩家推送,並更新玩家的推送狀態(updatePopupState函數)。

    • 若玩家ID已在Bloom Filter中,表示可能已推送,則不進行推送。

樣本代碼

Jedis的依賴如下:

pom.xml檔案的Maven依賴

本樣本基於Jedis 5.1.0版本,您可以在pom.xml檔案中添加以下Maven依賴:

<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
    <version>5.1.0</version>
</dependency>

完整程式碼範例如下:

import redis.clients.jedis.*;
import redis.clients.jedis.UnifiedJedis;


public class TairBloomFilterDemo {
    static HostAndPort hostAndPort = new HostAndPort("r-bp1y****svonly41srpd.redis.rds.aliyuncs.com", 6379);  // 您可以在控制台擷取執行個體串連地址與連接埠號碼。
    static JedisClientConfig config = DefaultJedisClientConfig.builder().password("tw:Da***3").build(); // 執行個體帳號密碼。
    static UnifiedJedis unifiedJedis = new UnifiedJedis(hostAndPort, config);
    private static final String BLOOM_KEY = "activity_popup";

    /**
    * 建立一個Bloom Filter Key。
    */
    public static void createBloom() {
        try {
            unifiedJedis.bfReserve(BLOOM_KEY, 0.01, 50000);
        } catch (Exception e) {
            e.printStackTrace(); // 逾時等異常情況
        }
    }

    /**
    * 查詢Bloom Filter中是否已經存在指定的玩家ID。
    */
    public static boolean shouldShowPopup(String playerId) {
        try {
            return !unifiedJedis.bfExists(BLOOM_KEY, playerId);
        } catch (Exception e) {
            e.printStackTrace(); // 逾時等異常情況
            return true;
        }
    }

    /**
    * 將玩家ID添加到Bloom Filter Key中。
    */
    public static void updatePopupState(String playerId) {
        try {
            unifiedJedis.bfAdd(BLOOM_KEY, playerId);
        } catch (Exception e) {
            e.printStackTrace(); // 逾時等異常情況。
        }
    }

    /**
    * 向指定玩家ID進行推送。
    */
    public static void handlePopup(String playerId) {
        if (shouldShowPopup(playerId)) {
            // 進行推送。
            System.out.println("推送給玩家: " + playerId);
            // 更新推送狀態。
            updatePopupState(playerId);
        } else {
            System.out.println("玩家 " + playerId + " 已推送過");
        }
    }

    public static void main(String[] args) {
        createBloom();
        // 假設玩家ID為player123。
        String playerId = "player123";

        // 第一次調用時,應該推送。
        handlePopup(playerId);

        // 第二次調用時,不應該再推送。
        handlePopup(playerId);
    }
}

本樣本的正確執行結果如下:

推送給玩家: player123
玩家 player123 已推送過