实现创意轮播
广告创意轮播实现:从 Redis 到 HBase 的方案详解
在广告投放中,创意轮播是一种常见策略,通过让同一广告单元下的多个创意按顺序或比例交替展示,帮助广告主对比不同创意的效果(如点击率、转化率),优化投放策略。本文将详细介绍基于 Redis 和 HBase 的创意轮播实现方案,分析其适用场景与优化思路。
创意轮播的核心需求
创意轮播的核心目标是让多个创意 “雨露均沾”,确保各创意的曝光量尽可能均衡。具体需求包括:
- 顺序轮播:按固定顺序循环展示创意(如创意 A→创意 B→创意 C→创意 A…)。
- 状态记录:记住用户最后一次看到的创意,确保下次展示下一个。
- 高并发支持:在百万级 QPS 的广告请求中,快速判断并返回下一个创意。
- 数据持久化:长期保存用户的创意展示记录,支持历史数据分析。
基于 Redis 的创意轮播实现(高并发场景)
Redis 凭借内存存储的高效性,适合作为创意轮播的实时存储方案,尤其适用于对响应速度要求高的场景(如信息流广告、开屏广告)。
数据结构设计
- Key:采用
用户ID:广告单元ID(uid: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(创意轮播记录表) - RowKey:
uid(用户 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 协同优化
实际生产环境中,通常采用 “缓存 + 存储” 的混合方案,兼顾性能与成本:
- Redis 作为缓存:存储近期(如 7 天内)活跃用户的最后一次创意记录,满足高并发实时查询。
- HBase 作为存储:存储全量用户的历史记录(90 天),用于冷数据查询和离线分析。
- 数据同步:
- 实时写入:曝光时同时写入 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,减少冷启动耗时