0%

实现创意轮播

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

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

创意轮播的核心需求

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

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

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

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

数据结构设计

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

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

核心实现逻辑

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* 记录用户最后一次看到的创意
* @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)获取下一个轮播的创意

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
/**
* 获取下一个轮播的创意
* @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)完整轮播流程
1
2
3
4
5
6
7
8
9
10
11
12
13
// 广告单元的创意列表(假设从数据库或配置中心获取)
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)记录最后一次曝光的创意
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
* 写入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)获取下一个轮播的创意
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
/**
* 从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 实现:

1
2
3
4
5
6
7
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%),可基于权重随机实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 创意权重列表(与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. 创意下架的兼容处理

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

1
2
3
4
5
6
7
8
9
// 过滤下架创意后的有效列表
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,减少冷启动耗时

欢迎关注我的其它发布渠道

表情 | 预览
快来做第一个评论的人吧~
Powered By Valine
v1.3.10