本文介绍如何在游戏业务中使用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)。
当玩家登录时,打算向该玩家进行推送,具体实现可参见示例代码中的
handlePopup函数。但推送前需要先检查是否需要推送,具体实现可参见示例代码中的
shouldShowPopup函数。若玩家ID不在Bloom Filter中,表示未进行推送,此时需要向该玩家推送,并更新玩家的推送状态(
updatePopupState函数)。若玩家ID已在Bloom Filter中,表示可能已推送,则不进行推送。
示例代码
Jedis的依赖如下:
完整代码示例如下:
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 已推送过