广告创意轮播实现:从 Redis 到 HBase 的方案详解 在广告投放中,创意轮播 是一种常见策略,通过让同一广告单元下的多个创意按顺序或比例交替展示,帮助广告主对比不同创意的效果(如点击率、转化率),优化投放策略。本文将详细介绍基于 Redis 和 HBase 的创意轮播实现方案,分析其适用场景与优化思路。
创意轮播的核心需求 创意轮播的核心目标是让多个创意 “雨露均沾” ,确保各创意的曝光量尽可能均衡。具体需求包括:
顺序轮播 :按固定顺序循环展示创意(如创意 A→创意 B→创意 C→创意 A…)。
状态记录 :记住用户最后一次看到的创意,确保下次展示下一个。
高并发支持 :在百万级 QPS 的广告请求中,快速判断并返回下一个创意。
数据持久化 :长期保存用户的创意展示记录,支持历史数据分析。
基于 Redis 的创意轮播实现(高并发场景) Redis 凭借内存存储的高效性,适合作为创意轮播的实时存储方案,尤其适用于对响应速度要求高的场景(如信息流广告、开屏广告)。
数据结构设计
Key :采用 用户ID:广告单元ID
(uid:dealId
)的格式,唯一标识 “用户 - 广告单元” 组合。
Value :使用 Redis List 存储用户对该广告单元的创意曝光记录,按时间倒序排列(最新记录在最前),或仅存储最后一次曝光的创意 ID (简化版)。
简化方案选择 :仅存储最后一次曝光的创意 ID,可大幅减少内存占用,且能满足轮播需求(只需知道上一个创意即可推导下一个)。
核心实现逻辑 (1)记录最后一次曝光的创意 每次曝光后,更新用户对该广告单元的最后一次创意 ID:
1 2 3 4 5 6 7 8 9 10 11 12 13 private void recordLastCreative (String uid, String dealId, long creativeId) { String key = "carousel:" + uid + ":" + dealId; jedis.set(key, String.valueOf(creativeId)); 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 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" ; long nextCreative = getNextCreative(uid, dealId, creativeList);showCreative(nextCreative); 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)记录最后一次曝光的创意 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 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)); put.addColumn( Bytes.toBytes("f" ), Bytes.toBytes(dealId), Bytes.toBytes(String.valueOf(creativeId)) ); put.setTTL(90 * 24 * 3600 * 1000 ); 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 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 ); } byte [] value = result.getValue(Bytes.toBytes("f" ), Bytes.toBytes(dealId)); long lastCreative = Long.parseLong(new String(value)); 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 协同优化 实际生产环境中,通常采用 “缓存 + 存储” 的混合方案,兼顾性能与成本:
Redis 作为缓存 :存储近期(如 7 天内)活跃用户的最后一次创意记录,满足高并发实时查询。
HBase 作为存储 :存储全量用户的历史记录(90 天),用于冷数据查询和离线分析。
数据同步:
实时写入:曝光时同时写入 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 List<Integer> weights = Arrays.asList(6 , 4 ); 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,减少冷启动耗时
v1.3.10