本文介紹如何在游戲業務中使用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一致。
方案概述
以下為示例代碼的概述,具體實現請參見下方的示例代碼。
連接Tair(企業版)實例。
創建名為
activity_popup
的Bloom Filter數據結構,具體實現可參見示例代碼中的createBloom
函數。本示例創建Bloom Filter預計存儲50000個元素,誤判率設置為1%(即0.01)。
在使用Bloom Filter時,誤判率是一個關鍵設計決策,誤判是指錯誤地認為某個不在集合中的元素是存在的。誤判率越低,過濾器的準確性越高,但占用的內存空間也越大。因此設置誤判率需要在精度和內存空間之間做權衡,誤判率的設置建議如下:
低誤判率(如0.01%):如果業務場景對誤判率非常敏感,如安全系統、金融系統等,則應該選擇非常低的誤判率(如 0.01% 或更低),但這會增加內存的開銷。
中誤判率(如0.1%到1%):對大多數場景來說,這個誤判率是合理的折中方案。既能保持較好的內存使用效率,又能保證較低的誤判率。
高誤判率(如1% 以上):在對準確性要求較低的場景(如緩存預熱、推薦系統等),可以選擇1%甚至更高的誤判率。這種情況下,內存需求較低,但誤判率較高。
當玩家登錄時,打算向該玩家進行推送,具體實現可參見示例代碼中的
handlePopup
函數。但推送前需要先檢查是否需要推送,具體實現可參見示例代碼中的
shouldShowPopup
函數。若玩家ID不在Bloom Filter中,表示未進行推送,此時需要向該玩家推送,并更新玩家的推送狀態(
updatePopupState
函數)。若玩家ID已在Bloom Filter中,表示可能已推送,則不進行推送。
示例代碼
Jedis的依賴如下:
本示例基于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 已推送過