实现创意轮播

广告创意轮播实现:从 Redis 到 HBase 的方案详解

在广告投放中,创意轮播是一种常见策略,通过让同一广告单元下的多个创意按顺序或比例交替展示,帮助广告主对比不同创意的效果(如点击率、转化率),优化投放策略。本文将详细介绍基于 Redis 和 HBase 的创意轮播实现方案,分析其适用场景与优化思路。

创意轮播的核心需求

创意轮播的核心目标是让多个创意 “雨露均沾”,确保各创意的曝光量尽可能均衡。具体需求包括:

  • 顺序轮播:按固定顺序循环展示创意(如创意 A→创意 B→创意 C→创意 A…)。
  • 状态记录:记住用户最后一次看到的创意,确保下次展示下一个。
  • 高并发支持:在百万级 QPS 的广告请求中,快速判断并返回下一个创意。
  • 数据持久化:长期保存用户的创意展示记录,支持历史数据分析。

基于 Redis 的创意轮播实现(高并发场景)

Redis 凭借内存存储的高效性,适合作为创意轮播的实时存储方案,尤其适用于对响应速度要求高的场景(如信息流广告、开屏广告)。

数据结构设计

  • Key:采用 用户ID:广告单元IDuid:dealId)的格式,唯一标识 “用户 - 广告单元” 组合。
  • Value:使用 Redis List 存储用户对该广告单元的创意曝光记录,按时间倒序排列(最新记录在最前),或仅存储最后一次曝光的创意 ID(简化版)。

简化方案选择:仅存储最后一次曝光的创意 ID,可大幅减少内存占用,且能满足轮播需求(只需知道上一个创意即可推导下一个)。

核心实现逻辑

(1)记录最后一次曝光的创意

每次曝光后,更新用户对该广告单元的最后一次创意 ID:

/**
 * 记录用户最后一次看到的创意
 * @param uid 用户ID
 * @param dealId 广告单元ID
 * @param creativeId 创意ID
 */
private void recordLastCreative(String uid, String dealId, long creativeId) {
    String key = "carousel:" + uid + ":" + dealId; // 键格式:carousel:用户ID:广告单元ID
    // 存储最后一次创意ID(覆盖旧值)
    jedis.set(key, String.valueOf(creativeId));
    // 设置过期时间(如30天,用户长期未活跃则重置)
    jedis.expire(key, 30 * 24 * 3600);
}
(2)获取下一个轮播的创意

根据广告单元的创意列表和用户最后一次看到的创意,计算下一个展示的创意:

/**
 * 获取下一个轮播的创意
 * @param uid 用户ID
 * @param dealId 广告单元ID
 * @param creativeList 该广告单元的创意列表(如 [100, 101, 102])
 * @return 下一个创意ID
 */
private long getNextCreative(String uid, String dealId, List<Long> creativeList) {
    String key = "carousel:" + uid + ":" + dealId;
    String lastCreativeStr = jedis.get(key);
    
    // 首次曝光:返回第一个创意
    if (lastCreativeStr == null) {
        return creativeList.get(0);
    }
    
    // 非首次曝光:找到最后一次创意的位置,返回下一个(循环)
    long lastCreative = Long.parseLong(lastCreativeStr);
    int lastIndex = creativeList.indexOf(lastCreative);
    // 若最后一次创意不在列表中(如已下架),默认从第一个开始
    if (lastIndex == -1) {
        return creativeList.get(0);
    }
    
    // 计算下一个索引(循环:最后一个的下一个是第一个)
    int nextIndex = (lastIndex + 1) % creativeList.size();
    return creativeList.get(nextIndex);
}
(3)完整轮播流程
// 广告单元的创意列表(假设从数据库或配置中心获取)
List<Long> creativeList = Arrays.asList(100L, 101L, 102L);
String uid = "user123";
String dealId = "deal456";

// 步骤1:获取下一个要展示的创意
long nextCreative = getNextCreative(uid, dealId, creativeList);

// 步骤2:展示创意(如返回给前端渲染)
showCreative(nextCreative);

// 步骤3:记录本次曝光的创意,作为下次轮播的依据
recordLastCreative(uid, dealId, nextCreative);

方案优势与局限

优势 局限
内存操作,响应速度快(微秒级),支持高并发 内存成本高,不适合存储海量用户数据(如 10 亿用户 ×10 个广告单元 = 100 亿条记录)
实现简单,无需复杂查询 数据易丢失(需开启 Redis 持久化)
适合短期轮播(如用户当天的多次曝光) 不便于长期数据分析(如查看用户 1 个月内的创意偏好)

基于 HBase 的创意轮播实现(海量数据场景)

当用户规模达到亿级,Redis 的内存成本过高,此时需采用 HBase 等分布式存储方案,适合长期保存用户的创意轮播记录。

HBase 表结构设计

  • 表名ad_carousel(创意轮播记录表)
  • RowKeyuid(用户 ID,便于按用户聚合查询)
  • 列族f(存储用户的广告单元 - 创意映射)
  • 列名dealId(广告单元 ID,每个广告单元对应一列)
  • :最后一次曝光的创意 ID(Long 类型)
  • TTL:90 天(自动清理过期数据)

核心实现逻辑

(1)记录最后一次曝光的创意
/**
 * 写入HBase:记录用户最后一次看到的创意
 */
public void recordLastCreativeHBase(String uid, String dealId, long creativeId) {
    Table table = null;
    try {
        table = connection.getTable(TableName.valueOf("ad_carousel"));
        Put put = new Put(Bytes.toBytes(uid));
        // 列名:dealId,值:创意ID(转为字节数组)
        put.addColumn(
            Bytes.toBytes("f"),
            Bytes.toBytes(dealId),
            Bytes.toBytes(String.valueOf(creativeId))
        );
        put.setTTL(90 * 24 * 3600 * 1000); // 90天过期
        table.put(put);
    } catch (IOException e) {
        log.error("HBase写入失败", e);
    } finally {
        if (table != null) try { table.close(); } catch (IOException e) {}
    }
}
(2)获取下一个轮播的创意
/**
 * 从HBase读取最后一次创意,计算下一个轮播创意
 */
public long getNextCreativeHBase(String uid, String dealId, List<Long> creativeList) {
    Table table = null;
    try {
        table = connection.getTable(TableName.valueOf("ad_carousel"));
        Get get = new Get(Bytes.toBytes(uid));
        get.addColumn(Bytes.toBytes("f"), Bytes.toBytes(dealId));
        
        Result result = table.get(get);
        if (result.isEmpty()) {
            // 首次曝光:返回第一个创意
            return creativeList.get(0);
        }
        
        // 解析最后一次创意ID
        byte[] value = result.getValue(Bytes.toBytes("f"), Bytes.toBytes(dealId));
        long lastCreative = Long.parseLong(new String(value));
        
        // 计算下一个创意(逻辑同Redis方案)
        int lastIndex = creativeList.indexOf(lastCreative);
        if (lastIndex == -1) {
            return creativeList.get(0);
        }
        int nextIndex = (lastIndex + 1) % creativeList.size();
        return creativeList.get(nextIndex);
    } catch (IOException e) {
        log.error("HBase读取失败", e);
        return creativeList.get(0); // 异常时默认返回第一个创意
    } finally {
        if (table != null) try { table.close(); } catch (IOException e) {}
    }
}
(3)完整轮播流程

与 Redis 方案一致,仅将读写操作替换为 HBase 实现:

List<Long> creativeList = Arrays.asList(100L, 101L, 102L);
String uid = "user123";
String dealId = "deal456";

long nextCreative = getNextCreativeHBase(uid, dealId, creativeList);
showCreative(nextCreative);
recordLastCreativeHBase(uid, dealId, nextCreative);

方案优势与局限

优势 局限
分布式存储,支持海量数据(PB 级),适合亿级用户 磁盘 IO 操作,响应速度较慢(毫秒级,比 Redis 慢 10-100 倍)
支持长期存储(90 天 +),便于离线分析 高并发场景下需配合缓存(如 Redis)使用
按用户 ID 聚合,适合批量查询用户的创意偏好 写入需处理 HBase 连接池、RegionServer 负载等问题

混合方案:Redis + HBase 协同优化

实际生产环境中,通常采用 “缓存 + 存储” 的混合方案,兼顾性能与成本:

  1. Redis 作为缓存:存储近期(如 7 天内)活跃用户的最后一次创意记录,满足高并发实时查询。
  2. HBase 作为存储:存储全量用户的历史记录(90 天),用于冷数据查询和离线分析。
  3. 数据同步:
    • 实时写入:曝光时同时写入 Redis 和 HBase(或通过消息队列异步同步到 HBase)。
    • 缓存回填:当 Redis 中无记录时,从 HBase 读取并写入 Redis,避免重复查询 HBase。

扩展场景与优化思路

1. 非顺序轮播:按比例分配曝光量

若广告主需要按比例轮播(如创意 A 占 60%,创意 B 占 40%),可基于权重随机实现:

// 创意权重列表(与creativeList顺序对应)
List<Integer> weights = Arrays.asList(6, 4); // 总权重10,A占60%,B占40%

// 按权重随机选择创意(替代顺序轮播的nextIndex逻辑)
private long getWeightedCreative(List<Long> creativeList, List<Integer> weights) {
    int total = weights.stream().mapToInt(Integer::intValue).sum();
    int random = new Random().nextInt(total);
    int sum = 0;
    for (int i = 0; i < weights.size(); i++) {
        sum += weights.get(i);
        if (random < sum) {
            return creativeList.get(i);
        }
    }
    return creativeList.get(0);
}

2. 创意下架的兼容处理

当轮播列表中的某个创意下架(如被审核驳回),需跳过该创意继续轮播:

// 过滤下架创意后的有效列表
List<Long> validCreatives = creativeList.stream()
    .filter(c -> isCreativeValid(c)) // 检查创意是否有效
    .collect(Collectors.toList());

// 若有效列表为空,返回默认创意或拒绝曝光
if (validCreatives.isEmpty()) {
    return defaultCreativeId; // 或抛出异常
}

3. 性能优化

  • 批量操作:HBase 可通过 PutList 批量写入多个用户的记录,减少 RPC 次数。
  • Redis 集群:使用 Redis Cluster 分片存储,避免单节点内存瓶颈。
  • 预热缓存:对高活跃用户的轮播记录提前加载到 Redis,减少冷启动耗时