DSP 核心实现技术方案

候选池

候选池的实现通常不是单一中间件能够完成的,而是需要多种中间件结合,根据业务需求进行选择和组合。高并发低延迟实时更新常见的最佳实践是缓存+消息队列+流计算的组合架构,确保查询速度快,同时候选池数据始终是最新的。

结合 Redis(缓存)+ Kafka(消息队列)+ Flink(流计算)+ Elasticsearch(精准查询),具体方案如下:

1. Redis + Elasticsearch(高并发低延迟查询)

  • Redis(第一层快速筛选):
    • 采用 Set / SortedSet(ZSet) 存储商家 ID,快速获取符合基本条件的候选池。
    • 热门商家数据存入 Redis,保证高并发下的极低延迟(<1ms)。
  • Elasticsearch(第二层精准查询):
    • 适用于需要复杂筛选(如按地域、评分、历史点击率等条件过滤商家)。
    • 采用倒排索引,支持多维度查询,降低数据库压力。

2. Kafka + Flink(实时更新候选池)

  • Kafka(数据实时流入):
    • 采集商家数据、广告投放效果、用户行为等信息。
    • 生产者(Producer)不断将商家状态变化推送到 Kafka Topic。
  • Flink(实时计算 & 规则更新):
    • 监听 Kafka 的数据流,计算商家投放质量评分、活跃度、转化率等关键指标。
    • 计算结果存入 Redis / Elasticsearch,实时更新候选池。
    • 例如,如果某商家点击率下降,Flink 计算后可自动降低该商家排名剔除候选池

架构流程

  1. 商家投放数据变化(广告效果、用户点击、转化率等)→ 写入 Kafka
  2. Flink 监听 Kafka 数据流,计算商家最新评分、排名、投放效果等信息
  3. Flink 计算完成后,更新 Redis / Elasticsearch
    • Redis:存储高频查询的热门候选商家
    • Elasticsearch:存储全量商家数据,提供复杂查询能力
  4. DSP(广告系统)查询候选池时
    • 先查 Redis,如果命中直接返回,保证高并发低延迟
    • 未命中则查询 Elasticsearch,获取更精准候选池

优化方案

异步更新 Elasticsearch

在DMP模块中进行异步更新Elasticsearch(ES),主要是为了避免同步写入时的性能瓶颈,提升系统的吞吐量和实时性。

✅ 方案:基于 Kafka 进行异步更新
  • 流程

    1. DMP 计算出新的商家数据后,先将数据写入 Redis 作为缓存。
    2. 同时,将更新消息(商家ID、最新状态、得分等)异步推送到 Kafka
    3. 独立的消费端(ES Writer) 从 Kafka 读取消息,并批量写入 Elasticsearch。
  • 优点

    • 高吞吐量:Kafka 可以承载大量数据,并提供顺序消费、重试机制
    • 解耦合:DMP 只需写入 Kafka,不需要关心 ES 的更新速度,ES 处理慢不会影响 Redis。
    • 批量更新:消费端可以批量写入 ES,减少 IO 负担,提高写入效率。

数据一致性

Redis 和 Elasticsearch 更新数据不需要做成事务

不建议使用分布式事务(如 2PC)来保证 Redis 和 Elasticsearch 的数据一致性,而应使用 最终一致性方案。

1. 为什么不建议使用事务?

在 Redis 和 Elasticsearch 之间执行 强一致性事务(如 2PC),有以下缺点:

  • 性能损耗:分布式事务(XA/2PC)会严重影响 Redis 和 ES 的吞吐量,导致 DSP 查询和 DMP 更新变慢。
  • 事务支持问题
    • Redis 没有原生的分布式事务支持,只能通过 Lua 脚本或 MULTI 机制实现本地事务。
    • Elasticsearch不支持事务,其索引更新是幂等的,通常采用版本控制(_version)或者写入时序保证数据一致性。
  • 分布式架构原则:在大规模数据流(高并发、高吞吐)场景下,强一致性不现实,最终一致性是更好的选择。
2. 采用“最终一致性”解决方案
✅ 方案 1:Kafka + 双写,ES 异步更新

流程

  1. 先更新 Redis(保证查询实时性)。
  2. 再发送更新事件到 Kafka,让异步消费者更新 Elasticsearch。
  3. ES 消费端监听 Kafka 事件批量更新 Elasticsearch,避免影响 Redis 的高并发查询能力。

Kafka 发送和 Redis 更新解耦
如果 Kafka 发送是独立的,即使 Redis 更新失败,Kafka 仍然可以收到数据,ES 仍然会更新。

public void updateMerchantData(Merchant merchant) {
    try {
        // 1. 更新 Redis
        redisTemplate.opsForHash().put("merchant_info", merchant.getId(), merchant);
    } catch (Exception e) {
        log.error("Redis 更新失败: {}", e.getMessage());
        // 继续执行 Kafka 发送,确保 ES 仍然可以更新
    }
    
    // 2. 发送 Kafka 消息(无论 Redis 是否成功)
    kafkaTemplate.send("merchant-update-topic", merchant.getId(), merchant);
}

Kafka 消费端异步更新 ES,补偿机制(Redis 失败时,Kafka 仍发送消息,并让 Kafka 消费端尝试补偿 Redis)

@KafkaListener(topics = "merchant-update-topic", groupId = "es-writer-group")
public void consumeMerchantUpdate(Merchant merchant) {
    try {
        // 1. 先补偿 Redis 更新(如果之前失败)
        redisTemplate.opsForHash().put("merchant_info", merchant.getId(), merchant);
    } catch (Exception e) {
        log.error("Kafka 消费端补偿 Redis 失败: {}", e.getMessage());
    }
    
    // 2. 更新 Elasticsearch
    bulkUpdateElasticsearch(Collections.singletonList(merchant));
}

优点

  • 高并发,低延迟:Redis 更新是同步的,Kafka+ES 更新是异步的,保证查询速度。
  • 支持批量更新:Kafka 消费端可以批量写入 ES,减少 ES 的写入负担。
  • 数据最终一致:即使 ES 更新失败,Kafka 也可以重试,不会丢数据。

缺点

  • ES 更新稍有延迟(通常 <1s)。

Redis 同步更新和 Kafka+ES 的异步更新做到解耦,Redis 更新失败不会影响 Kafka+ES 的异步更新结果。

如果 Redis 同步更新成功,Kafka 消息发送失败,指数级重试 3 次,如果依然失败,使用 Prometheus + Grafana 监控,页面告警,邮件告警,把消息存储到本地消息表中 Mysql。

这样,当 Kafka 消息发送失败次数超过设定阈值(如 5 次),Prometheus 会触发 Grafana 告警,Alertmanager 负责发送邮件通知。🚀

使用 Elastic-Job 定时任务,扫描本地消息表中的数据进行发送,发送成功后进行逻辑标记删除,一定时期对消息表中已发送成功的数据进行归档,归档后进行物理删除。

归档数据

归档数据可以存储在不同的地方,取决于业务需求、查询频率和存储成本。以下是几种常见的归档方案:

方案 1:归档到 MySQL 的历史表(最常见)

适用场景:

  • 需要保留消息历史记录,但查询频率不高。
  • 依然希望能通过 SQL 快速查询归档数据。
  • 适用于结构化数据的存储。

实现方法:

  1. 创建归档表(结构与原表一致)
    CREATE TABLE local_message_queue_archive LIKE local_message_queue;
    
  2. 定期归档已处理的消息
    INSERT INTO local_message_queue_archive 
    SELECT * FROM local_message_queue WHERE status = 'PROCESSED' AND created_at < NOW() - INTERVAL 30 DAY;
    
  3. 删除主表中已归档的数据
    DELETE FROM local_message_queue WHERE status = 'PROCESSED' AND created_at < NOW() - INTERVAL 30 DAY;
    
  4. 设置定时任务(如 MySQL Event 或 Spring Task 定期执行)
    @Scheduled(cron = "0 0 2 * * ?")  // 每天凌晨2点执行
    public void archiveAndCleanMessages() {
        jdbcTemplate.update("INSERT INTO local_message_queue_archive SELECT * FROM local_message_queue WHERE status = 'PROCESSED' AND created_at < NOW() - INTERVAL 30 DAY");
        jdbcTemplate.update("DELETE FROM local_message_queue WHERE status = 'PROCESSED' AND created_at < NOW() - INTERVAL 30 DAY");
    }
    

优点:
✅ 结构化存储,查询方便,可直接用 SQL 查找特定消息。
✅ 归档数据仍然可以通过数据库管理工具查看,方便运维。
✅ 适用于查询量较小的场景,存储和维护成本较低。

缺点:
❌ MySQL 表数据量仍然会增长,虽然影响较小,但可能需要定期清理更老的数据(如保留 6 个月)。
❌ 如果数据量特别大,查询可能会慢(可以考虑分库分表或迁移到更适合 OLAP 查询的存储)。

方案 2:归档到 Elasticsearch(适用于高效检索)

适用场景:

  • 需要对历史消息进行全文检索或复杂查询。
  • 需要高效的查询性能,能快速查找某个消息的状态。
  • 日志、交易记录等需要较快查询的场景。

实现方法:

  1. 创建 Elasticsearch 索引
    PUT local_message_archive
    {
      "mappings": {
        "properties": {
          "message_id": { "type": "keyword" },
          "status": { "type": "keyword" },
          "payload": { "type": "text" },
          "created_at": { "type": "date" }
        }
      }
    }
    
  2. 写入归档数据
    • 使用 Kafka 消费归档队列,将消息写入 Elasticsearch:
    @KafkaListener(topics = "message-archive")
    public void archiveMessage(String messageJson) {
        IndexRequest request = new IndexRequest("local_message_archive")
                .source(messageJson, XContentType.JSON);
        elasticsearchClient.index(request, RequestOptions.DEFAULT);
    }
    
  3. 定期清理 MySQL 主表
    DELETE FROM local_message_queue WHERE status = 'PROCESSED' AND created_at < NOW() - INTERVAL 30 DAY;
    

优点:
✅ 查询速度快,适用于高并发、高速检索的场景。
✅ 可以进行全文搜索,查询条件更灵活(如按 message_id 或 payload 关键词搜索)。
✅ 适合大数据量的归档,比 MySQL 存储效率更高。

缺点:
❌ 需要维护 Elasticsearch 集群,运维成本比 MySQL 高。
❌ 存储成本较高,ES 消耗的磁盘空间比 MySQL 多。

方案 3:归档到 Kafka(适用于流式计算 + 冷存储)

适用场景:

  • 归档数据本身不需要经常查询,但可能用于大数据分析或定期批量处理。
  • 适用于离线数据分析机器学习等场景。
  • 适用于数据流式消费,如后续再写入 HDFS、ClickHouse、Doris 等数据仓库。

实现方法:

  1. 创建 Kafka 归档主题
    kafka-topics.sh --create --topic message-archive --bootstrap-server kafka:9092 --partitions 3 --replication-factor 2
    
  2. 将归档消息推送到 Kafka
    kafkaTemplate.send("message-archive", messageJson);
    
  3. 消费 Kafka 归档数据,并存入 HDFS/ClickHouse
    • 以 Flink、Spark Streaming 或 Kafka Consumer 方式消费 Kafka 消息:
    @KafkaListener(topics = "message-archive")
    public void archiveToHDFS(String messageJson) {
        hdfsClient.write("/archive/messages/" + System.currentTimeMillis() + ".json", messageJson);
    }
    

优点:
✅ 适用于数据分析场景,可以结合大数据生态(Flink、Spark、Hive)。
✅ Kafka 处理吞吐量大,适用于超大规模数据归档
✅ 结合 HDFS,可以存储 PB 级别的历史数据。

缺点:
❌ Kafka 只是消息存储,仍然需要落地到 HDFS、ClickHouse 等持久化存储,否则 Kafka 日志会过期。
❌ 直接查询 Kafka 数据比较困难,不如 Elasticsearch 方便。

最终推荐

方案适用场景存储方式查询性能成本维护难度
MySQL 归档表中小型项目,查询量不高结构化存储一般
Elasticsearch 归档高速检索,日志分析文档存储
Kafka + HDFS批量归档,离线计算流式存储
  • 如果只是存储历史消息,且查询不频繁,推荐 MySQL 历史表(方案 1)。
  • 如果要高效检索归档消息(如搜索某个 message_id),推荐 Elasticsearch(方案 2)。
  • 如果主要用于数据分析,不需要频繁查询,推荐 Kafka + HDFS(方案 3)。

异步批量消费 kafka 消息进行更新 Elasticsearch,手动提交 offset,如果消费失败,进行重试,同时使用 grafana+prometheus 进行重试次数记录,超过三次后则进行页面告警,同时发送告警邮件,进行人为介入。

商家竞价策略

📌 常见竞价方式

竞价模式全称计费方式适用场景
CPMCost Per Mille每千次曝光 付费品牌推广,提升曝光量
CPCCost Per Click每次点击 付费关注点击转化,提高流量
CPACost Per Action每次转化(如购买、注册) 付费目标是提高转化率
CPSCost Per Sale每次销售 付费适合电商,按成交量计费
CPICost Per Install每次应用安装 付费适合 App 下载推广
CPVCost Per View每次完整观看 付费适用于视频广告
ROASReturn On Ad Spend广告支出回报比 竞价适合效果导向型广告,优化收益

📌 更智能的竞价策略

除了以上常见竞价方式,一些 智能竞价 策略也广泛应用,例如:

  • oCPM(优化千次展示成本):DSP 会自动优化投放,提升转化率。
  • oCPC(优化点击成本):DSP 预测哪些用户更可能点击,自动优化出价。
  • tCPA(目标转化成本):广告主设定目标 CPA,DSP 自动调整出价,以实现目标转化成本。
  • tROAS(目标广告支出回报比):广告主设定目标 ROAS,DSP 根据预估收益调整出价。

📌 结论

商家设定的竞价方式不止 CPM、CPC、CPA,还包括 CPS、CPI、CPV、ROAS 等多种模式。
DSP 计算 eCPM 时,会将各种竞价方式转换为统一标准进行比较,以保证公平竞价
智能竞价策略(如 oCPC、tCPA、tROAS)能帮助广告主更高效地优化投放效果 🚀。

eCPM

eCPM (Effective Cost Per Mille,有效千次展示成本) 是广告投放中的一个关键指标,全称 Effective Cost Per Mille,意思是 “有效千次展示成本”。它表示 每 1000 次广告展示,广告主愿意支付的费用,用于衡量广告的价值和竞争力。

eCPM 是 所有广告格式(CPC、CPA、CPM)都可以转换到的统一竞价标准,这样 DSP 在竞价时可以公平比较不同类型的广告。

📌 eCPM 计算公式

eCPM =(总广告收益 / 总展示次数)× 1000

或者,对不同的竞价模式进行转换:

  • CPM(千次曝光出价)广告

    eCPM = 直接的 CPM 出价

  • CPC(按点击付费)广告

    eCPM = CPC × CTR × 1000

    • CPC(Cost Per Click):广告主愿意为每次点击支付的费用
    • CTR(Click Through Rate):广告点击率(点击数 / 曝光数)
  • CPA(按转化付费)广告

    eCPM = CPA × CVR × CTR × 1000

    • CPA(Cost Per Acquisition):广告主愿意为每次转化(如购买、注册)支付的费用
    • CVR(Conversion Rate):转化率(转化数 / 点击数)

📌 eCPM 在广告竞价中的作用

RTB(实时竞价) 中,广告主可能使用不同的出价方式(CPC、CPM、CPA)。为了 公平比较,DSP 会将所有广告的出价转换为 eCPM,然后选取 eCPM 最高 的广告进行竞价。

高 eCPM = 高收益 = 更有可能胜出竞价

📌 举例:不同广告竞价方式的 eCPM 计算

假设某广告位上有 3 个商家竞价:

广告商家出价方式出价预估 CTR预估 CVR计算 eCPM
商家 ACPM¥8 / 千次曝光--¥8
商家 BCPC¥2 / 点击5%-¥2 × 0.05 × 1000 = ¥10
商家 CCPA¥100 / 转化4%2%¥100 × 0.02 × 0.04 × 1000 = ¥8

结果:

  • 商家 B 的 eCPM = ¥10,最高,最终胜出!
  • 即使商家 A 的 CPM 出价直接是 ¥8,仍然输给了商家 B,因为按点击计费的 B 在该广告位的收益更高。

📌 结论

eCPM 是衡量广告收益的标准指标,能将 CPC、CPM、CPA 转换为统一标准,确保公平竞价。
在 RTB 竞价中,eCPM 最高的广告胜出,获得该广告位的曝光机会。
即使是 CPC 或 CPA 竞价,也可能比 CPM 竞价更有竞争力,如果其 eCPM 计算后更高的话。

eCPM 是 DSP 计算的! eCPM 是 DSP 计算最优出价、提高广告收益 的核心指标 🚀。

📌 计算主体

RTB(实时竞价) 过程中,eCPM 的计算由 DSP(需求方平台) 完成,而 Ad Exchange(广告交易平台) 只是负责接收 DSP 提供的出价,并选出最高的广告进行展示。

1️⃣ DSP 计算 eCPM

  1. 商家在 DSP 端设置竞价策略(CPM、CPC、CPA 等)。
  2. DSP 计算 eCPM:将不同的竞价方式(CPC、CPA)转换为 eCPM 统一衡量,方便与 CPM 直接竞价的广告比较。
    • CPM 出价:eCPM = CPM 出价
    • CPC 出价:eCPM = CPC × 预估 CTR × 1000
    • CPA 出价:eCPM = CPA × 预估 CVR × 预估 CTR × 1000
  3. DSP 计算最终出价,然后将结果提交给 Ad Exchange。

2️⃣ Ad Exchange 只负责筛选最高出价

  1. Ad Exchange 接收多个 DSP 提供的 eCPM 出价
  2. Ad Exchange 选出 eCPM 最高的广告,并将广告展示给用户。

📌 为什么是 DSP 计算 eCPM?

DSP 更了解广告主的策略和目标(如转化率、点击率等),能够根据商家需求 优化出价
Ad Exchange 主要负责竞价撮合,并不会深入计算不同竞价方式的换算。
DSP 需要通过 eCPM 让不同竞价方式(CPC、CPA、CPM)公平竞争,确保商家能以最优方式获得广告曝光。

📌 结论

eCPM 是 DSP 计算的,而 Ad Exchange 只是比较 DSP 提供的出价,选出最高者
DSP 计算 eCPM 的目的是为了确保不同竞价方式的广告能够公平竞争,提高竞价效率和广告收益 🚀。

广告位

某一个固定的广告位来说,最终被选上的通常只有一个商家,因为 Ad Exchange 经过竞价后,会选择出价最高且符合条件的商家来展示广告。

但是如果是多个广告位(例如电商平台搜索结果页中的多个广告栏位),那么这些广告位是并行的,每个广告位的竞价是独立进行的,最终会有多个不同商家的广告同时被选中展示。

📌 具体竞价流程(以单个广告位为例)

1️⃣ DSP 从 Elasticsearch 查询候选广告(如 50~100 条)。
2️⃣ 筛选掉不符合要求的广告(如预算不足、质量低、定向不匹配)。
3️⃣ 如果是自动竞价,计算每个广告的 eCPM 出价
4️⃣ DSP 提交候选广告到 Ad Exchange 进行 RTB 竞价
5️⃣ Ad Exchange 在所有 DSP 竞价广告中,选出 eCPM 最高的广告
6️⃣ 最终胜出的广告将被展示在该广告位上

📌 多广告位是并行的

在一个电商平台的搜索页面中,可能有 多个广告位(例如 5~10 个广告展示位)。这些广告位的竞价是独立进行的,即:

  • 每个广告位都会有自己的竞价过程。
  • 多个广告位可能会被同一家商家拿下多个位置,但通常不同广告位会有不同的商家。
  • 每个广告位的竞价策略可能不同(例如:搜索页顶部广告 vs 右侧推荐广告)。

📌 举例

假设用户在某电商平台搜索 “智能手表”,页面有 3 个广告位:

广告位竞价商家eCPM(预估出价)选中的商家
广告位 1(顶部)商家 A, B, CA: ¥20, B: ¥18, C: ¥15商家 A
广告位 2(侧边栏)商家 D, E, FD: ¥12, E: ¥14, F: ¥10商家 E
广告位 3(搜索结果中部)商家 G, H, IG: ¥16, H: ¥17, I: ¥13商家 H
  • 广告位 1 的最终胜出者是 商家 A,因为 A 的 eCPM 最高(¥20)。
  • 广告位 2商家 E 胜出(¥14)。
  • 广告位 3商家 H 胜出(¥17)。
  • 这 3 个广告位是 并行竞价的,相互之间没有直接影响,每个位置选出一个最优广告

📌 结论

单个广告位最终只会选出一个商家(即 eCPM 最高的广告)。
多个广告位之间是并行关系,每个广告位独立竞价,最终会由多个不同商家的广告被选中展示。
同一个商家可能拿下多个广告位,但这取决于他们的竞价策略和预算。

这样设计可以最大化广告收益,同时确保广告投放的公平性和多样性 🚀。

DSP 查询 Elasticsearch 的条件

在 DSP 查询 Elasticsearch 时,通常会使用一些关键条件来筛选出符合要求的广告,确保从候选池中找到与广告位匹配的广告。主要的查询条件包括:

  1. 广告位信息(Ad Placement)
    • 广告位的类型(比如:横幅广告、视频广告、推荐位等)。
    • 广告位的尺寸(如:300x250、728x90 等)。
    • 广告位的目标(如:品牌曝光、流量引导、转化等)。
  2. 用户信息(User Data)
    • 用户画像:例如,年龄、性别、地域、兴趣等(可能通过 DMP 或第一方数据传递给 DSP)。
    • 用户行为:历史浏览、购买记录、互动记录等。
  3. 商家广告定向(Advertiser Targeting)
    • 商家定向条件:例如广告主设置的目标用户群体,特定的地理位置、设备类型、时间等。
    • 广告类型:广告主设置的广告类型(如 CPC、CPM、CPA 等),以匹配特定的广告位和目标。
  4. 竞价信息(Bidding Information)
    • 商家的竞价策略(例如:CPM、CPC、CPA 等),确保广告的出价符合要求。
    • 是否符合广告主的最大出价范围或预算限制。
  5. 广告内容与创意(Creative Content)
    • 广告的创意类型,如图像、视频、文字等,匹配广告位的要求。
    • 创意是否已通过审核,符合平台规范。

查询之后,DSP 通常还需要进一步过滤掉不符合条件的广告。这些过滤可能包括以下几种:

  1. 预算和竞价过滤
    • 如果商家的广告预算已用完或出价过低,DSP 会过滤掉这些广告,避免提交不符合竞价要求的广告。
  2. 定向条件过滤
    • 根据广告主的定向条件(如地域、设备类型、兴趣等),去除不符合定向规则的广告。例如,某广告仅面向移动设备用户,其他设备的广告将被过滤。
  3. 创意过滤
    • 如果广告创意不符合广告位的格式要求(如尺寸不匹配),或者广告创意内容违反平台规范,则会被过滤掉。
  4. 频次过滤
    • 为了避免广告的重复展示,DSP 可能会基于用户历史曝光频次(Frequency Cap)进行过滤,确保广告不会频繁展示给同一用户。
  5. 效能过滤
    • DSP 可能会根据广告的历史表现(如 CTR、CVR)来进行过滤,优先展示历史表现较好的广告,提高广告效果。

Elasticsearch 广告数据结构

Elasticsearch (ES) 中,完整的商家广告数据应该包含广告基本信息、投放规则、定向策略、竞价信息、广告素材等内容。以下是 ES 中存储的一份完整广告数据示例,适用于DSP、Ad Exchange、推荐系统等场景。

{
  "ad_id": "ad_123456",
  "merchant_id": "m_7890",
  "ad_name": "iPhone 15 Pro 限时折扣",
  "ad_type": "商品推广",
  "ad_category": ["手机", "苹果", "电子产品"],
  "status": "active",
  "budget": {
    "total_budget": 50000,
    "daily_budget": 2000
  },
  "bidding": {
    "strategy": "CPC",
    "max_bid": 1.5,
    "current_bid": 1.2
  },
  "targeting": {
    "geo": ["Shanghai", "Beijing"],
    "device": ["mobile", "tablet"],
    "age_range": ["18-25", "26-35"],
    "gender": ["male", "female"],
    "interests": ["科技", "数码", "苹果粉丝"],
    "keywords": ["iPhone 15", "智能手机", "苹果官网"],
    "user_behavior": {
      "viewed_products": ["iPhone 14", "AirPods Pro"],
      "purchased_categories": ["智能手机", "数码配件"]
    }
  },
  "schedule": {
    "start_time": "2024-03-10T08:00:00Z",
    "end_time": "2024-03-31T23:59:59Z",
    "time_slots": ["08:00-12:00", "18:00-22:00"]
  },
  "ad_creatives": [
    {
      "creative_id": "c_001",
      "format": "image",
      "url": "https://cdn.example.com/ads/iphone15.jpg",
      "width": 1080,
      "height": 1920
    },
    {
      "creative_id": "c_002",
      "format": "video",
      "url": "https://cdn.example.com/ads/iphone15.mp4",
      "duration": 15
    }
  ],
  "landing_page": "https://www.apple.com/cn/iphone-15-pro/",
  "metrics": {
    "impressions": 15000,
    "clicks": 1200,
    "ctr": 0.08,
    "conversions": 320,
    "cvr": 0.026,
    "spent": 1800
  },
  "created_at": "2024-03-01T10:00:00Z",
  "updated_at": "2024-03-09T15:30:00Z"
}

Redis 存储结构设计

如果要在 Redis 中存一份完整的广告数据,主要目的是 加速广告匹配,避免每次都查询 Elasticsearch。由于 Redis 是键值存储,我们需要合理设计 数据结构,以 高效存取、快速查询 为目标。

Redis 适合的存储方式

  • 方式 1(适合广告 ID 查询):使用 Hash 存储完整的广告数据(适合存储单条广告信息)
  • 方式 2(适合筛选广告):使用 Sorted Set、Set、Bitmap、HyperLogLog 等结构进行广告索引

方式 1:使用 Redis Hash 存储广告完整数据

✅ 通过 ad_id 直接获取广告详情
✅ 适用于高频查询

HSET ad:123456 ad_id "123456"
HSET ad:123456 merchant_id "7890"
HSET ad:123456 ad_name "iPhone 15 Pro 限时折扣"
HSET ad:123456 ad_type "商品推广"
HSET ad:123456 ad_category "手机,苹果,电子产品"
HSET ad:123456 status "active"
HSET ad:123456 budget_total 50000
HSET ad:123456 budget_daily 2000
HSET ad:123456 bidding_strategy "CPC"
HSET ad:123456 bidding_max 1.5
HSET ad:123456 bidding_current 1.2
HSET ad:123456 targeting_geo "Shanghai,Beijing"
HSET ad:123456 targeting_device "mobile,tablet"
HSET ad:123456 targeting_age "18-25,26-35"
HSET ad:123456 targeting_gender "male,female"
HSET ad:123456 targeting_interests "科技,数码,苹果粉丝"
HSET ad:123456 targeting_keywords "iPhone 15,智能手机,苹果官网"
HSET ad:123456 landing_page "https://www.apple.com/cn/iphone-15-pro/"
HSET ad:123456 impressions 15000
HSET ad:123456 clicks 1200
HSET ad:123456 ctr 0.08
HSET ad:123456 conversions 320
HSET ad:123456 cvr 0.026
HSET ad:123456 spent 1800
HSET ad:123456 created_at "2024-03-01T10:00:00Z"
HSET ad:123456 updated_at "2024-03-09T15:30:00Z"

方式 2:构建广告索引,加速定向查询

✅ 需要根据 地域、设备、兴趣标签 快速筛选广告
避免全量遍历,提高查询效率

1. 地域索引

SADD targeting:geo:Shanghai ad_123456 ad_789012
SADD targeting:geo:Beijing ad_123456 ad_654321

查询:查找投放到上海的所有广告

SMEMBERS targeting:geo:Shanghai

2. 设备索引

SADD targeting:device:mobile ad_123456 ad_654321
SADD targeting:device:tablet ad_123456

查询:查找投放到 Mobile 设备的所有广告

SMEMBERS targeting:device:mobile

3. 年龄段索引

SADD targeting:age:18-25 ad_123456 ad_654321
SADD targeting:age:26-35 ad_123456 ad_789012

查询:查找投放给 18-25 岁用户的广告

SMEMBERS targeting:age:18-25

4. 兴趣标签索引

SADD targeting:interests:科技 ad_123456 ad_789012
SADD targeting:interests:数码 ad_123456 ad_654321
SADD targeting:interests:苹果粉丝 ad_123456

查询:查找兴趣是“科技”的广告

SMEMBERS targeting:interests:科技

5. 关键词索引

SADD targeting:keywords:iPhone15 ad_123456 ad_654321
SADD targeting:keywords:智能手机 ad_123456

查询:查找关键词包含“iPhone 15”的广告

SMEMBERS targeting:keywords:iPhone15

6. 活跃广告集合

SADD active_ads ad_123456 ad_654321 ad_789012

查询:获取所有活跃广告

SMEMBERS active_ads

如何进行广告检索

假设用户 设备:Mobile,地域:上海,兴趣:科技,查询步骤如下:

  1. 查询 targeting:device:mobile{ad_123456, ad_654321}
  2. 查询 targeting:geo:Shanghai{ad_123456, ad_789012}
  3. 查询 targeting:interests:科技{ad_123456, ad_789012}
  4. 取交集 SINTER targeting:device:mobile targeting:geo:Shanghai targeting:interests:科技
SINTER targeting:device:mobile targeting:geo:Shanghai targeting:interests:科技

返回 {ad_123456},最终广告 ID = ad_123456

然后可以:

HGETALL ad:123456

拿到完整广告数据

最终实现:广告查询速度更快,广告匹配精准度更高! 🚀

Elasticsearch 用户画像数据

Elasticsearch 中存储 用户画像数据,我们需要设计一个 结构化且易于查询的数据模型,以便支持高效的查询和筛选。用户画像数据通常包括用户的基本信息、行为信息、兴趣、偏好、历史活动等,通常是一个 文档型 存储,每个用户对应一个文档。

{
  "user_id": "123456",
  "user_name": "张三",
  "age": 29,
  "gender": "male",
  "location": {
    "city": "北京",
    "province": "北京",
    "country": "中国"
  },
  "device": {
    "os": "Android",
    "device_type": "手机",
    "device_id": "device12345"
  },
  "interests": [
    "科技",
    "数码",
    "篮球",
    "苹果产品"
  ],
  "browsing_history": [
    {
      "product_id": "987654",
      "category": "电子产品",
      "sub_category": "手机",
      "brand": "苹果",
      "timestamp": "2024-03-01T10:00:00Z"
    },
    {
      "product_id": "543210",
      "category": "电子产品",
      "sub_category": "耳机",
      "brand": "BOSE",
      "timestamp": "2024-02-25T12:00:00Z"
    }
  ],
  "purchase_history": [
    {
      "product_id": "123456",
      "category": "家居",
      "sub_category": "厨房用品",
      "amount": 150,
      "purchase_date": "2024-01-10T10:30:00Z"
    }
  ],
  "search_history": [
    {
      "query": "iPhone 15",
      "timestamp": "2024-03-01T09:00:00Z"
    },
    {
      "query": "智能手表",
      "timestamp": "2024-02-28T14:30:00Z"
    }
  ],
  "social_media_activity": {
    "facebook": {
      "followers": 350,
      "likes": 120,
      "shares": 35
    },
    "weibo": {
      "followers": 500,
      "likes": 200,
      "shares": 75
    }
  },
  "loyalty_program": {
    "points": 1200,
    "membership_level": "Gold"
  },
  "recommendations": [
    {
      "product_id": "987654",
      "category": "电子产品",
      "sub_category": "手机",
      "reason": "你最近浏览过苹果手机"
    },
    {
      "product_id": "654321",
      "category": "电子产品",
      "sub_category": "耳机",
      "reason": "根据你的购买历史推荐"
    }
  ],
  "preferences": {
    "payment_method": "信用卡",
    "shipping_address": "北京市朝阳区XXX小区",
    "communication_preferences": {
      "email": true,
      "sms": false,
      "push_notifications": true
    }
  },
  "updated_at": "2024-03-09T15:30:00Z",
  "created_at": "2023-10-01T10:00:00Z"
}

Redis 用户画像数据

如果要在 Redis 中存储用户画像数据,可以采取 缓存化存储 的策略,将用户画像数据以一种 高效且快速检索 的方式存储在 Redis 中。考虑到 Redis 主要是用于 快速访问高并发场景,数据的结构和存储方式要简化且能高效获取。

我们可以利用 Redis 提供的数据结构,如 哈希(Hash)字符串(String)列表(List) 等来存储用户画像数据。每个用户的画像可以被视为一个独立的对象,存储为 Redis 的 哈希表(Hash),其中字段可以是用户画像的各个属性,确保存储和读取效率。

下面是将上面的完整用户画像数据转化为 Redis 中存储的结构:

假设我们将每个用户的画像数据存储在 Redis 的哈希表中,键名为 user:<user_id>,哈希字段对应用户画像的不同属性。这样可以将用户的所有信息存储在一个哈希结构下,便于按字段访问。

// 用户画像的 Redis 键名
String userRedisKey = "user:123456";

// 使用 Hash 存储用户画像字段
Map<String, String> userProfile = new HashMap<>();
userProfile.put("user_name", "张三");
userProfile.put("age", "29");
userProfile.put("gender", "male");
userProfile.put("city", "北京");
userProfile.put("province", "北京");
userProfile.put("country", "中国");
userProfile.put("device_os", "Android");
userProfile.put("device_type", "手机");
userProfile.put("device_id", "device12345");
userProfile.put("interests", "科技,数码,篮球,苹果产品");
userProfile.put("loyalty_points", "1200");
userProfile.put("membership_level", "Gold");
userProfile.put("updated_at", "2024-03-09T15:30:00");
userProfile.put("created_at", "2023-10-01T10:00:00");

// 存储浏览历史信息(使用列表或者哈希结构存储)
String browsingHistoryKey = "user:123456:browsing_history";
List<String> browsingHistory = Arrays.asList(
    "987654|电子产品|手机|苹果|2024-03-01T10:00:00",
    "543210|电子产品|耳机|BOSE|2024-02-25T12:00:00"
);

// 存储购买历史信息(使用列表或者哈希结构存储)
String purchaseHistoryKey = "user:123456:purchase_history";
List<String> purchaseHistory = Arrays.asList(
    "123456|家居|厨房用品|150|2024-01-10T10:30:00"
);

// 存储推荐信息(使用哈希或者列表存储)
String recommendationsKey = "user:123456:recommendations";
List<String> recommendations = Arrays.asList(
    "987654|电子产品|手机|你最近浏览过苹果手机",
    "654321|电子产品|耳机|根据你的购买历史推荐"
);

// 将用户画像存储到 Redis
redisTemplate.opsForHash().putAll(userRedisKey, userProfile);

// 存储浏览历史和购买历史
redisTemplate.opsForList().rightPushAll(browsingHistoryKey, browsingHistory);
redisTemplate.opsForList().rightPushAll(purchaseHistoryKey, purchaseHistory);
redisTemplate.opsForList().rightPushAll(recommendationsKey, recommendations);

Redis 中的存储设计

  • 用户基本信息(Hash)
    • 键名:user:<user_id>
    • 字段:user_name, age, gender, city, device_os, device_type
    • 这些字段存储为 Redis 的哈希表,可以通过哈希操作快速获取。
  • 用户兴趣(String 或 List)
    • 键名:user:<user_id>:interests
    • 字段:将用户兴趣爱好存储为字符串或列表,以便快速获取和修改。
  • 用户浏览历史(List 或 Set)
    • 键名:user:<user_id>:browsing_history
    • 列表内容:每条浏览历史记录的商品 ID、类别、品牌和浏览时间。
    • 使用列表类型进行存储,以便按时间顺序存取浏览历史。
  • 用户购买历史(List 或 Set)
    • 键名:user:<user_id>:purchase_history
    • 列表内容:每条购买历史记录的商品 ID、类别、金额、购买时间。
    • 使用列表类型进行存储,支持用户的购买历史查询。
  • 用户推荐信息(List)
    • 键名:user:<user_id>:recommendations
    • 列表内容:商品 ID、类别、推荐理由等。
    • 使用列表类型存储,可以存储多条推荐商品信息。
  • 时间戳(String)
    • 存储字段如 updated_atcreated_at,记录用户画像的更新时间和创建时间。

Redis 存储广告数据

基于传统的 静态定向(如地域、设备、性别、年龄段)进行广告缓存,但随着 用户画像 的引入,广告推荐变得更加个性化。这意味着用户行为(例如,最近浏览的商品、购买记录等)将显著影响广告的投放。因此,Redis 和 Elasticsearch 在广告缓存和查询中的作用会发生一些变化。

传统的定向方案

  • 缓存广告分类:Redis 可以根据固定的属性(如地域、设备、性别、年龄段等)进行广告分类和缓存。
  • 使用场景:当用户的属性较为稳定且广告定向规则较为简单时(例如同一年龄段的用户通常对类似广告感兴趣),可以通过 Redis 直接缓存广告,避免每次都查询 Elasticsearch。

考虑用户画像后的方案

随着用户画像(例如,最近浏览过的商品类型、偏好、历史行为等)的加入,广告推荐的复杂度和个性化需求大大增加。这时,我们可以考虑 动态定向,Redis 的作用可能会有所不同,但依然能在广告推荐中发挥重要作用。

缓存用户行为信息

Redis 依然可以缓存 用户行为数据(例如,最近浏览的商品、最近互动的广告类型、购物车中的商品等)。不过,这些信息会根据时间和用户行为的变化频繁更新。

  • 哈希结构:可以使用 Redis 的哈希(Hash)数据结构来缓存用户最近的行为数据。
    • 用户最近浏览的商品user:{user_id}:recent_browsed_products,缓存最近浏览的商品 ID 或商品类别。
    • 用户购物车内容user:{user_id}:cart,缓存用户购物车中的商品。

这样,当用户访问广告时,Redis 可以快速查询用户的行为数据,以便为其推荐相关广告。

动态广告推荐和缓存

  • 定向广告缓存:通过结合 用户画像静态定向条件,Redis 可以缓存一些 用户个性化的广告推荐,但需要注意缓存的 动态性。例如:
    • 对于 商品类型广告:如果用户最近浏览了某类商品,那么可以在 Redis 中缓存该类商品的广告。
    • 对于 推荐系统:在广告缓存时,可以根据用户的最近行为筛选出最可能感兴趣的广告,并将这些广告缓存到 Redis 中。
    • user:{user_id}:ads_recommendations: 存储个性化广告推荐列表。

Redis 与 Elasticsearch 结合

  • Redis 缓存最近的个性化广告:当用户的广告需求是基于历史行为、实时兴趣等动态信息时,可以先在 Redis 中快速查找最近的广告推荐。
  • Elasticsearch 查询更丰富的广告信息:对于某些需要实时更新或者多维度查询的复杂广告信息,Redis 中的缓存可能无法完全满足需求。这时,可以在 Redis 查询不到结果时,向 Elasticsearch 查询更丰富的数据,例如:按关键词、商品类别、上下文信息等进行深度筛选。
  1. 广告定向缓存
    • Redis 用于缓存用户的 个性化广告推荐用户行为数据,以提高广告查询的响应速度。
    • 用户的基本属性(例如地域、性别、设备类型)仍然可以存储在 Redis 中。
  2. 动态更新缓存
    • 当用户行为(例如浏览商品)发生变化时,可以动态更新 Redis 缓存,重新计算广告推荐,并缓存更新后的推荐结果。
    • 通过定时清理过期的广告缓存,确保广告推荐的实时性和准确性。
  3. 缺失数据的回退机制
    • 如果 Redis 中没有命中某个广告数据,可以回退到 Elasticsearch 进行复杂的查询和深度筛选,确保最终选择到最适合用户的广告。

广告去重

在分布式集群生产环境下,采用 Redis + 共享 Key 方案,让 Ad Exchange 负责去重,前端兜底,而 DSP 只专注于竞价

🔥 方案设计

  1. Ad Exchange 采用 Redis 去重,确保同一广告不会被选中多次。
  2. 前端兜底去重,防止因网络延迟导致的重复广告。
方案负责去重适用场景
Ad Exchange 端去重(推荐)Redis 共享去重确保广告位唯一,低延迟,高性能
前端兜底去重浏览器前端防止极端情况导致的重复广告

🔥 方案优势

方案说明
按用户 ID 维度去重确保不同用户的广告不互相影响
短时间内防止广告重复60 秒 TTL 让广告不频繁重复
使用 Redis Set 结构去重高效,查询 O(1)

最终方案: Redis + 用户 ID 作为 Key,确保个性化去重,低延迟,高效率! 🚀

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值