DSP 计价系统

手动竞价模式

手动竞价模式下,商家需要向DSP平台提交广告投放的详细参数,以确保广告能够按照预期进行竞价和投放。通常,商家提交的数据结构会包括广告基本信息、出价信息、投放策略和素材内容等。

广告数据结构(JSON 示例)

{
  "campaign_id": "123456",  // 广告系列ID
  "ad_group_id": "654321",  // 广告组ID
  "ad_id": "987654",        // 广告ID
  "advertiser_id": "10001", // 广告主ID
  
  "bidding_mode": "manual",  // 竞价模式(manual: 手动竞价, auto: 自动竞价)
  "bid_strategy": "fixed",    // 竞价策略(fixed: 固定出价, tiered: 阶梯竞价)
  "currency": "CNY",          // 货币类型(CNY: 人民币, USD: 美元)

  "bid_price": {              
    "cpc": 2.5,   // 每次点击成本(Cost Per Click)
    "cpm": 20.0,  // 每千次展示成本(Cost Per Mille)
    "cpa": null   // 每次转化成本(Cost Per Acquisition),可为空
  },

  "budget": {
    "type": "daily",   // 预算类型(daily: 每日预算, total: 总预算)
    "min_amount": 500, // 最低预算金额
    "max_amount": 1000 // 最高预算金额
  },

  "targeting": {  
    "location": ["北京", "上海"],  // 目标投放地域
    "age_range": ["18-24", "25-34"], // 目标投放年龄段
    "gender": "all",  // 目标性别(male: 男性, female: 女性, all: 所有人)
    "interests": {
      "match_type": "broad",  // 兴趣匹配模式(exact: 精准匹配, broad: 模糊匹配)
      "values": ["游戏", "运动"] // 目标兴趣分类
    }
  },

  "exclude_audiences": {  
    "location": ["新疆", "西藏"], // 排除的地域
    "interests": ["博彩", "成人内容"] // 排除的兴趣
  },

  "ad_creative": {
    "format": "image",  // 广告素材格式(image: 图片, video: 视频, text: 文字)
    "media_url": "https://example.com/ad_image.jpg",  // 广告素材URL
    "width": 1080,  // 广告素材宽度(像素)
    "height": 1920, // 广告素材高度(像素)
    "cta_text": "立即购买" // CTA(Call To Action)按钮文字,如“立即购买”,“查看更多”
  },

  "schedule": {
    "start_time": "2025-03-15T00:00:00Z",  // 广告投放开始时间(UTC格式)
    "end_time": "2025-03-20T23:59:59Z",    // 广告投放结束时间(UTC格式)
    "timezone": "Asia/Shanghai"            // 时区(例如 "Asia/Shanghai")
  },

  "tracking": {
    "click_url": "https://track.example.com/click?adid=987654&device={device}&location={location}", 
    // 点击监测链接,支持动态参数(如 {device}, {location})
    
    "impression_url": "https://track.example.com/impression?adid=987654", 
    // 曝光监测链接
    
    "conversion_url": "https://track.example.com/conversion?adid=987654" 
    // 转化监测链接
  }
}

数据字段解析

  1. 基本信息
    • campaign_id:广告活动的唯一ID。
    • ad_group_id:广告组ID(一个广告活动下可有多个广告组)。
    • ad_id:广告ID,唯一标识某个具体广告。
    • advertiser_id:商家/广告主的唯一标识。
  2. 竞价信息
    • bid_type:竞价方式,手动竞价模式设为 manual
    • bid_strategy:竞价策略,手动竞价通常是 fixed(固定出价)。
    • bid_price
      • cpc(Cost Per Click):按点击计费的单价。
      • cpm(Cost Per Mille):按千次曝光计费的单价。
      • cpa(Cost Per Action):如果选择按转化计费,可提供CPA出价。
  3. 预算
    • budget.type:预算类型:
      • daily:每日预算。
      • total:整个广告活动的总预算。
    • budget.amount:具体的预算金额。
  4. 定向策略
    • location:投放地域(省、市或国家)。
    • age_range:年龄段定向,如 “18-24”。
    • gender:性别定向,可选 male(男性)、female(女性)或 all(全部)。
    • interests:兴趣标签,如 “游戏”、"运动"等。
    • device:设备定向,如 “iOS”、“Android”。
  5. 广告素材
    • format:广告格式,如 image(图片)、video(视频)。
    • title & description:广告标题和描述。
    • media_url:广告素材地址(图片或视频)。
    • landing_page:落地页(用户点击广告后跳转的目标URL)。
  6. 投放时间
    • start_time & end_time:广告投放的起止时间(UTC 时间)。
    • time_slots:每日投放时间段,如 “08:00-12:00”。
  7. 追踪和监测
    • click_url:点击监测URL。
    • impression_url:曝光监测URL。
    • conversion_url:转化监测URL。

结论
在手动竞价模式下,商家提交的广告数据需要包含出价信息、预算、投放策略、广告素材和监测信息。与智能竞价模式不同,手动竞价需要广告主自己设定价格,并无法利用DSP的自动优化算法。

DSP 手动竞价

如果 DSP 从 Elasticsearch 查询出一条手动竞价的广告数据,它应该按照广告主提交的手动出价策略来计算竞价价格,而不是通过智能竞价算法动态调整价格。以下是计算流程:

1. 确定竞价模式

在广告数据中:

"bidding_mode": "manual"

意味着该广告是手动竞价,DSP 不会使用自动竞价优化,而是直接依据广告主提交的出价策略。

2. 确定竞价方式

广告数据中可能包含:

"bid_strategy": "fixed"

"bid_strategy": "tiered"

不同竞价方式的计算方法不同。

(1) 固定出价 (fixed)

如果 bid_strategy 设定为 fixed(固定出价),DSP 直接采用广告主提交的固定价格:

"bid_price": {
  "cpc": 2.5,  
  "cpm": 20.0, 
  "cpa": null  
}
  • 如果 DSP 采用 CPC(按点击付费)模式,那么广告的价格就是 2.5 元/点击
  • 如果 DSP 采用 CPM(按千次曝光付费)模式,那么广告的价格就是 20.0 元/千次展示
  • 如果 DSP 采用 CPA(按转化付费)模式,但 cpanull,说明该广告不支持按转化计费。

📌 最终价格计算

  • CPC 模式最终竞价价格 = bid_price.cpc
  • CPM 模式最终竞价价格 = bid_price.cpm
  • CPA 模式:如果 bid_price.cpa 有值,则 最终竞价价格 = bid_price.cpa

(2) 阶梯竞价 (tiered)

如果 bid_strategy 设定为 tiered(阶梯竞价),则 DSP 需要根据广告的曝光情况或竞争情况,选择适合的出价档位:

"bid_price": {
  "tiered": [
    { "threshold": 1000, "cpc": 2.0, "cpm": 18.0 },
    { "threshold": 5000, "cpc": 1.8, "cpm": 16.0 },
    { "threshold": 10000, "cpc": 1.5, "cpm": 14.0 }
  ]
}
  • 如果曝光量 < 1000,则采用第一档 cpc = 2.0, cpm = 18.0
  • 如果曝光量在 1000~5000 之间,则采用第二档 cpc = 1.8, cpm = 16.0
  • 如果曝光量超过 10000,则采用第三档 cpc = 1.5, cpm = 14.0

📌 最终价格计算

  1. 查询广告当前曝光量 current_impressions,确定匹配的 threshold
  2. 选择对应的 cpccpm 作为最终出价。

3. 计算竞价得分

DSP 竞价排名通常采用 eCPM(有效千次曝光成本) 计算广告排名:
eCPM=CPC×CTR×1000
eCPM=CPM
其中:

  • CPC:手动竞价的 CPC 出价
  • CTR(点击率):DSP 预估该广告的点击率
  • CPM:手动竞价的 CPM 出价

DSP 会计算广告的 eCPM,与其他广告进行排序,决定广告是否能竞价成功以及展示的顺序。

4. 竞价成功后,实际支付价格

在手动竞价模式下,DSP 仍然可能采用二价拍卖(Second-Price Auction) 机制:

  • 第一名广告的出价bid_price.cpc = 2.5
  • 第二名广告的出价bid_price.cpc = 2.0
  • 实际支付价格:广告主只需支付比第二名稍高的价格,例如 2.01 元/点击

如果采用CPM 模式,则类似:

  • 第一名广告的出价bid_price.cpm = 20
  • 第二名广告的出价bid_price.cpm = 18
  • 最终支付价格:广告主可能只支付 18.01 元/千次展示

总结

如果 DSP 发现广告是手动竞价

  1. 读取竞价策略
    • fixed 模式:直接使用 bid_price 里的 cpccpm
    • tiered 模式:根据曝光情况,选择对应的 cpccpm
  2. 计算 eCPM,用于排名:
    • eCPM = CPC × CTR × 1000
    • eCPM = CPM
  3. 使用二价拍卖
    • 实际支付的价格通常是比第二名稍高的价格。

🚀 这样 DSP 能在保证广告主手动竞价逻辑的前提下,让广告进入竞价市场,并合理计算最终的支付价格。

代码实现

import java.util.*;

class BidPrice {
    Double cpc;  // 每次点击成本 (Cost Per Click)
    Double cpm;  // 千次曝光成本 (Cost Per Mille)
    Double cpa;  // 每次转化成本 (Cost Per Acquisition)
    List<TieredBid> tiered; // 阶梯竞价
}

class TieredBid {
    int threshold;  // 曝光量阈值
    double cpc;  
    double cpm;
}

class AdBid {
    String biddingMode;  // "manual" 或 "auto"
    String bidStrategy;  // "fixed" 或 "tiered"
    BidPrice bidPrice;
    int currentImpressions;  // 当前广告曝光量
    double estimatedCTR; // DSP 预估点击率(可为空)

	// 获取广告点击率,如果为空则使用默认值
    public double getCTR() {
        return (estimatedCTR != null) ? estimatedCTR : 0.01;  // 默认 CTR 设为 1%
    }
}

public class DSPBidCalculator {
    // 计算出 DSP 竞价价格
    public static double calculateBid(AdBid adBid) {
        if (!"manual".equals(adBid.biddingMode)) {
            throw new IllegalArgumentException("Only manual bidding is supported.");
        }

        if ("fixed".equals(adBid.bidStrategy)) {
            return adBid.bidPrice.cpc != null ? adBid.bidPrice.cpc : adBid.bidPrice.cpm;
        } else if ("tiered".equals(adBid.bidStrategy)) {
            for (TieredBid tier : adBid.bidPrice.tiered) {
                if (adBid.currentImpressions < tier.threshold) {
                    return tier.cpc != 0 ? tier.cpc : tier.cpm;
                }
            }
            // 超过最高阈值,使用最后一档价格
            TieredBid lastTier = adBid.bidPrice.tiered.get(adBid.bidPrice.tiered.size() - 1);
            return lastTier.cpc != 0 ? lastTier.cpc : lastTier.cpm;
        }
        throw new IllegalArgumentException("Unsupported bid strategy: " + adBid.bidStrategy);
    }

    // 计算 eCPM,DSP 计算出的结果会提交给 Ad Exchange
    public static double calculateECPM(AdBid adBid, double bidPrice) {
        if (adBid.bidPrice.cpc != null) {
            return bidPrice * adBid.estimatedCTR * 1000; // eCPM = CPC * 预估CTR * 1000
        } else if (adBid.bidPrice.cpm != null) {
            return bidPrice; // CPM 直接作为 eCPM
        }
        return 0;
    }

    public static void main(String[] args) {
        AdBid adBid = new AdBid();
        adBid.biddingMode = "manual";
        adBid.bidStrategy = "tiered";
        adBid.currentImpressions = 4500;
        adBid.estimatedCTR = 0.02; // 预估点击率 2%

        BidPrice bidPrice = new BidPrice();
        bidPrice.tiered = Arrays.asList(
            new TieredBid() {{ threshold = 1000; cpc = 2.0; cpm = 18.0; }},
            new TieredBid() {{ threshold = 5000; cpc = 1.8; cpm = 16.0; }},
            new TieredBid() {{ threshold = 10000; cpc = 1.5; cpm = 14.0; }}
        );
        adBid.bidPrice = bidPrice;

        // DSP 计算 eCPM 并提交给 Ad Exchange
        double bid = calculateBid(adBid);
        double eCPM = calculateECPM(adBid, bid);

        System.out.println("DSP 计算出的 eCPM: " + eCPM);
    }
}

优化点

  1. DSP 只计算 eCPM,不决定最终广告投放
  2. 不涉及二价拍卖逻辑,因为 二价拍卖(Second-Price Auction) 是 Ad Exchange 层的逻辑。
  3. 分离出 calculateECPM() 逻辑,DSP 计算 eCPM,然后提交给 Ad Exchange 进行最终竞价

并行加速计算

对于 DSP 计算竞价 这种高吞吐的任务,并行计算 可以大幅提升处理速度。

并行计算方案
由于 DSP 需要对 50~200 条广告数据 计算竞价价格,我们可以:

  1. 使用 Java 并行流(ParallelStream)
    • Java 8+ 提供了 parallelStream(),可以自动利用 CPU 多核并行计算。
  2. 使用线程池(ExecutorService)
    • 适用于更大规模的并发计算,DSP 可以创建一个 固定大小的线程池,提交竞价计算任务,并行处理后收集结果。
  3. 使用 CompletableFuture 进行异步计算
    • 适用于 DSP 需要在计算价格时,还要 查询历史CTR数据计算更复杂的 eCPM 公式

方案 1:使用 ParallelStream(简单高效)

适用于 计算量适中,且没有额外外部 IO 操作(如数据库查询)的场景。

import java.util.*;
import java.util.stream.Collectors;

public class DSPBidParallelCalculator {
    public static List<Double> calculateBidsParallel(List<AdBid> adBids) {
        return adBids.parallelStream()
                     .map(DSPBidParallelCalculator::calculateBid)
                     .collect(Collectors.toList());
    }

    public static double calculateBid(AdBid adBid) {
        double ctr = adBid.getCTR();
        return adBid.bidPrice.cpc * ctr * 1000; // eCPM = CPC * 预估CTR * 1000
    }

    public static void main(String[] args) {
        List<AdBid> adBids = new ArrayList<>();
        for (int i = 0; i < 100; i++) {
            AdBid ad = new AdBid();
            ad.estimatedCTR = Math.random() * 0.1 + 0.01; // 模拟 1% ~ 10% 的 CTR
            BidPrice bidPrice = new BidPrice();
            bidPrice.cpc = Math.random() * 2 + 1; // 随机 CPC 1~3 元
            ad.bidPrice = bidPrice;
            adBids.add(ad);
        }

        long start = System.currentTimeMillis();
        List<Double> results = calculateBidsParallel(adBids);
        long end = System.currentTimeMillis();

        System.out.println("计算完成,耗时: " + (end - start) + " ms");
        System.out.println("示例 eCPM 价格: " + results.subList(0, 5)); // 输出前 5 条
    }
}

优点

  • parallelStream() 自动分配线程,不需要手动管理线程池。
  • 适用于 CPU 密集型任务(计算 eCPM)。
  • 代码简洁,易于维护。

🚫 缺点

  • 不能控制线程池大小,如果计算量过大,可能会造成 CPU 过载
  • 不能做 异步操作(如数据库查询)。

方案 2:使用线程池 ExecutorService(适用于大规模计算)

如果 DSP 需要并发计算 200+ 条广告数据,同时还要从数据库查询 CTR,建议使用 固定线程池 + Future 进行批量计算。

import java.util.*;
import java.util.concurrent.*;
import java.util.stream.Collectors;

public class DSPBidExecutorService {
    private static final int THREAD_POOL_SIZE = Runtime.getRuntime().availableProcessors(); // CPU 核心数

    public static List<Double> calculateBidsThreadPool(List<AdBid> adBids) throws InterruptedException {
        ExecutorService executor = Executors.newFixedThreadPool(THREAD_POOL_SIZE);
        List<Future<Double>> futures = new ArrayList<>();

        for (AdBid adBid : adBids) {
            futures.add(executor.submit(() -> calculateBid(adBid)));
        }

        List<Double> results = new ArrayList<>();
        for (Future<Double> future : futures) {
            try {
                results.add(future.get()); // 获取计算结果
            } catch (ExecutionException e) {
                e.printStackTrace();
            }
        }

        executor.shutdown(); // 关闭线程池
        executor.awaitTermination(10, TimeUnit.SECONDS); // 等待线程池任务完成
        return results;
    }

    public static double calculateBid(AdBid adBid) {
        double ctr = adBid.getCTR();
        return adBid.bidPrice.cpc * ctr * 1000; // eCPM = CPC * 预估CTR * 1000
    }

    public static void main(String[] args) throws InterruptedException {
        List<AdBid> adBids = new ArrayList<>();
        for (int i = 0; i < 200; i++) {
            AdBid ad = new AdBid();
            ad.estimatedCTR = Math.random() * 0.1 + 0.01;
            BidPrice bidPrice = new BidPrice();
            bidPrice.cpc = Math.random() * 2 + 1;
            ad.bidPrice = bidPrice;
            adBids.add(ad);
        }

        long start = System.currentTimeMillis();
        List<Double> results = calculateBidsThreadPool(adBids);
        long end = System.currentTimeMillis();

        System.out.println("计算完成,耗时: " + (end - start) + " ms");
        System.out.println("示例 eCPM 价格: " + results.subList(0, 5));
    }
}

优点

  • 手动控制线程池大小,避免过载。
  • 适用于更大规模的广告竞价计算(200+ 条)。
  • 可以进行数据库查询(比如查询 CTR)。

🚫 缺点

  • Future.get()阻塞的,需要等待所有任务完成。

方案 3:使用 CompletableFuture(适用于异步查询 + 计算)

如果 DSP 需要在计算 eCPM 之前,先从数据库查询 CTR,可以用 CompletableFuture 进行异步计算:

import java.util.*;
import java.util.concurrent.*;
import java.util.stream.Collectors;

public class DSPBidCompletableFuture {
    private static final ExecutorService executor = Executors.newFixedThreadPool(10);

    public static CompletableFuture<Double> calculateBidAsync(AdBid adBid) {
        return CompletableFuture.supplyAsync(() -> {
            double ctr = fetchCTRFromDB(adBid);
            return adBid.bidPrice.cpc * ctr * 1000;
        }, executor);
    }

    public static double fetchCTRFromDB(AdBid adBid) {
        try {
            Thread.sleep(50); // 模拟数据库查询
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return adBid.estimatedCTR != null ? adBid.estimatedCTR : 0.01; // 模拟查询 CTR
    }

    public static void main(String[] args) throws InterruptedException, ExecutionException {
        List<AdBid> adBids = new ArrayList<>();
        for (int i = 0; i < 200; i++) {
            AdBid ad = new AdBid();
            ad.estimatedCTR = Math.random() * 0.1 + 0.01;
            BidPrice bidPrice = new BidPrice();
            bidPrice.cpc = Math.random() * 2 + 1;
            ad.bidPrice = bidPrice;
            adBids.add(ad);
        }

        long start = System.currentTimeMillis();
        List<CompletableFuture<Double>> futures = adBids.stream()
            .map(DSPBidCompletableFuture::calculateBidAsync)
            .collect(Collectors.toList());

        List<Double> results = futures.stream().map(CompletableFuture::join).collect(Collectors.toList());
        long end = System.currentTimeMillis();

        System.out.println("计算完成,耗时: " + (end - start) + " ms");
        System.out.println("示例 eCPM 价格: " + results.subList(0, 5));
    }
}

优点

  • 适用于异步数据库查询(如从 Redis / MySQL 读取 CTR)。
  • 更快的响应时间(多个广告竞价可同时查询 CTR 并计算 eCPM)。

最终选择

  • 计算量小(50~100 条):✅ ParallelStream
  • 计算量大(200+ 条):✅ ExecutorService
  • 需要数据库查询:✅ CompletableFuture

自动竞价

数据结构

{
  "ad_id": "123456",  // 广告唯一 ID
  "advertiser_id": "7890",  // 广告主 ID
  "campaign_id": "5678",  // 广告活动 ID
  "creative_id": "2468",  // 广告创意 ID
  "targeting": {  
    "geo": ["US", "UK"],  // 目标投放地域,如美国、英国
    "age_range": [18, 45],  // 目标年龄范围,表示 18-45 岁
    "interests": ["technology", "gaming"],  // 目标用户兴趣,例如 "科技"、"游戏"
    "device_type": ["mobile", "desktop"],  // 设备类型,如移动端、桌面端
    "platform": ["iOS", "Android", "Windows"],  // 目标操作系统,如 iOS、Android
    "time_range": ["08:00-12:00", "18:00-22:00"]  // 允许广告投放的时间段
  },  
  "auto_bidding": {  
    "budget": 5000,  // 广告预算(单位:美元)
    "bid_strategy": "TARGET_ROAS",  // 竞价策略(TARGET_ROAS: 目标广告投资回报率)
    "target_roas": 3.5,  // 目标 ROAS(投资回报率),1 美元广告支出希望带来 3.5 美元回报
    "max_cpc": 2.0,  // 最高 CPC(每次点击最高出价)
    "max_cpm": 10.0,  // 最高 CPM(每千次曝光最高出价)
    "min_cpc": 0.5,  // 最低 CPC,防止出价过低影响投放
    "min_cpm": 2.0,  // 最低 CPM,避免广告出价过低
    "optimization_goal": "CONVERSION",  // 竞价优化目标,如 "CONVERSION"(转化率优化)
    "conversion_rate": 0.02,  // 预估转化率(2%)
    "historical_performance": {  
      "ctr": 0.05,  // 历史点击率(5%)
      "conversion_rate": 0.015,  // 历史转化率(1.5%)
      "cpa": 8.5  // 每次转化的平均成本(8.5 美元)
    }  
  },  
  "ad_creative": {  
    "title": "Best Gaming Laptop",  // 广告标题
    "description": "Buy now and get 20% off",  // 广告描述
    "image_url": "https://example.com/ad-image.jpg",  // 广告图片 URL
    "video_url": "https://example.com/ad-video.mp4",  // 广告视频 URL(可选)
    "landing_page": "https://example.com/product"  // 用户点击广告后跳转的目标页面
  },  
  "tracking": {  
    "click_url": "https://example.com/click-tracking",  // 点击追踪链接
    "impression_url": "https://example.com/impression-tracking",  // 曝光追踪链接
    "conversion_url": "https://example.com/conversion-tracking"  // 转化追踪链接
  },  
  "status": "ACTIVE",  // 广告状态(ACTIVE: 运行中, PAUSED: 暂停, DELETED: 已删除)
  "created_at": "2025-03-14T10:00:00Z",  // 广告创建时间(ISO 8601 格式)
  "updated_at": "2025-03-14T12:30:00Z"  // 广告最近更新时间(ISO 8601 格式)
}

代码实现

下面是 DSP 自动竞价 逻辑的 Java 实现,采用并行计算来提高性能,并基于广告历史表现数据(CTR、转化率、ROAS 目标)计算最优出价。

💡 核心逻辑

  1. 查询广告数据:从数据库或 Elasticsearch 获取符合条件的广告数据。
  2. 并行计算竞价:使用 ForkJoinPoolCompletableFuture 并行计算广告出价,提升计算效率。
  3. 计算竞价:基于广告的 目标 ROAS、历史点击率、转化率 计算出 CPC/CPM 出价。
  4. 返回计算结果:将所有广告的出价结果返回,供 Ad Exchange 竞价。

📌 完整 Java 代码

import java.util.*;
import java.util.concurrent.*;
import java.util.stream.Collectors;

class Ad {
    String adId;
    double targetRoas; // 目标 ROAS
    double maxCpc; // 最高 CPC
    double minCpc; // 最低 CPC
    double maxCpm; // 最高 CPM
    double minCpm; // 最低 CPM
    double ctr; // 历史点击率
    double conversionRate; // 历史转化率

    public Ad(String adId, double targetRoas, double maxCpc, double minCpc, double maxCpm, double minCpm, double ctr, double conversionRate) {
        this.adId = adId;
        this.targetRoas = targetRoas;
        this.maxCpc = maxCpc;
        this.minCpc = minCpc;
        this.maxCpm = maxCpm;
        this.minCpm = minCpm;
        this.ctr = ctr;
        this.conversionRate = conversionRate;
    }
}

class BidPrice {
    String adId;
    double cpcBid;
    double cpmBid;

    public BidPrice(String adId, double cpcBid, double cpmBid) {
        this.adId = adId;
        this.cpcBid = cpcBid;
        this.cpmBid = cpmBid;
    }

    @Override
    public String toString() {
        return "Ad ID: " + adId + ", CPC: " + cpcBid + ", CPM: " + cpmBid;
    }
}

public class DspAutoBidding {

    private static final int THREAD_POOL_SIZE = 10; // 线程池大小
    private static final ExecutorService executor = Executors.newFixedThreadPool(THREAD_POOL_SIZE);

    /**
     * 计算广告的最优竞价
     */
    public static BidPrice calculateBid(Ad ad) {
        // 计算出价逻辑:
        // 1. 目标 ROAS = 目标收入 / 广告花费
        // 2. 计算 CPC:目标 ROAS = (CTR * CPC) / ConversionRate
        //    => CPC = (targetRoas * conversionRate) / ctr
        // 3. 计算 CPM = CPC * CTR * 1000

        double estimatedCpc = (ad.targetRoas * ad.conversionRate) / ad.ctr;
        double estimatedCpm = estimatedCpc * ad.ctr * 1000;

        // 限制 CPC/CPM 在广告主设定的 min-max 范围内
        double finalCpc = Math.min(Math.max(estimatedCpc, ad.minCpc), ad.maxCpc);
        double finalCpm = Math.min(Math.max(estimatedCpm, ad.minCpm), ad.maxCpm);

        return new BidPrice(ad.adId, finalCpc, finalCpm);
    }

    /**
     * 并行计算所有广告的竞价
     */
    public static List<BidPrice> calculateBidsParallel(List<Ad> ads) {
        List<CompletableFuture<BidPrice>> futures = ads.stream()
                .map(ad -> CompletableFuture.supplyAsync(() -> calculateBid(ad), executor))
                .collect(Collectors.toList());

        return futures.stream()
                .map(CompletableFuture::join) // 等待所有任务完成
                .collect(Collectors.toList());
    }

    public static void main(String[] args) {
        // 模拟广告数据
        List<Ad> ads = Arrays.asList(
                new Ad("A001", 3.5, 2.0, 0.5, 10.0, 2.0, 0.05, 0.02),
                new Ad("A002", 2.8, 1.8, 0.4, 8.0, 1.5, 0.04, 0.015),
                new Ad("A003", 4.0, 2.5, 0.6, 12.0, 2.5, 0.06, 0.025)
        );

        // 计算竞价
        List<BidPrice> bidPrices = calculateBidsParallel(ads);

        // 输出计算结果
        bidPrices.forEach(System.out::println);

        // 关闭线程池
        executor.shutdown();
    }
}

🚀 代码优化点

  1. 并行计算

    • 采用 CompletableFuture 并行计算,相比传统循环 for 提高了性能,适合 50-200 条广告数据并发计算
    • 线程池 Executors.newFixedThreadPool(THREAD_POOL_SIZE) 处理广告出价计算,避免 CPU 过载。
  2. 智能竞价逻辑

    • CPC 出价计算
      在这里插入图片描述
    • CPM 出价计算
      在这里插入图片描述
    • 确保出价范围
      • CPC 必须在 minCpc - maxCpc 之间
      • CPM 必须在 minCpm - maxCpm 之间
  3. 灵活扩展

    • 可加入 机器学习模型 进一步优化 CTR / 转化率预测,提升出价精准度。

📌 计算示例

假设广告 A001 数据如下:

  • targetRoas = 3.5
  • conversionRate = 0.02
  • ctr = 0.05
  • maxCpc = 2.0
  • minCpc = 0.5
  • maxCpm = 10.0
  • minCpm = 2.0

计算过程
在这里插入图片描述
最终出价:

  • CPC = 1.4(符合 min-max 范围)
  • CPM = 10.0(超出 maxCpm,调整为 10.0)

输出:

Ad ID: A001, CPC: 1.4, CPM: 10.0
Ad ID: A002, CPC: 1.05, CPM: 8.0
Ad ID: A003, CPC: 1.67, CPM: 10.0

✅ 结论

  • 自动竞价逻辑智能化,基于广告历史表现(CTR/转化率)计算最优出价。
  • 并行计算提高效率,适用于大规模广告数据处理。
  • 结果符合 min-max 约束,确保广告预算可控。

计算竞价所需的参数(CTR、转化率、ROAS 目标)都是由 DMP(数据管理平台)统计分析得出的,并且是动态变化的。这些数据会随着用户行为、市场情况、广告投放表现等因素的变化而不断更新。

📌 DMP 统计的数据参数

DMP 主要基于用户行为数据、广告历史数据、竞价成功率等,实时计算出以下核心指标:

参数数据来源作用是否实时变化
CTR(点击率)历史点击数据影响 CPC 计算✅ 实时变化
Conversion Rate(转化率)购买/注册等转化数据影响 ROAS 计算✅ 实时变化
ROAS(广告投资回报率)营销 ROI 计算决定广告出价策略✅ 实时变化
竞争广告出价竞价历史影响出价策略✅ 实时变化

📌 DMP 如何更新数据

DMP 通过数据流处理管道,不断更新这些指标,例如:

  1. CTR 计算(过去 7 天或 30 天点击数据)
    在这里插入图片描述
    • 如果某广告的 CTR 在过去 24 小时下降,DSP 可能会降低出价。
  2. 转化率计算(转化事件统计)
    - ( ConversionRate = \frac{\text{转化次数}}{\text{广告点击次数}} )
    • 例如,如果某广告最近 1000 次点击中有 30 次转化,则转化率 = 3%。
  3. ROAS 计算(广告收入/广告花费)
    - ( ROAS = \frac{\text{转化后收入}}{\text{广告投放成本}} )
    • 如果某广告 1000 美元投放带来 4000 美元销售额,则 ROAS = 4.0。

📌 实际应用

  1. DMP 通过 Kafka、Flink、Spark Streaming 实时更新数据。
    • 计算 CTR、转化率等数据并存入 Redis 或 ClickHouse 提供 DSP 查询。
  2. DSP 竞价时会调用最新的数据进行计算。
    • 例如,DSP 竞价前调用 Redis 读取最新 CTR,确保出价精准。

✅ 结论

  • CTR、转化率、ROAS 都是动态变化的,由 DMP 计算。
  • DSP 需要实时查询最新数据,以计算最优竞价。
  • 通常 DMP 通过实时流计算(Kafka + Flink/Spark)持续更新数据,存入 Redis 或 ClickHouse 供 DSP 访问。

这样就能保证竞价的实时性精准度,确保广告主的预算花在最有效的地方。

CTR、转化率、ROAS 等动态数据 存入 Elasticsearch (ES) 并与广告数据存储在一起,确实是一个可行的方案,但有一些需要权衡的点,具体来说可以对比存入 ES vs. 存入 Redis 的优缺点:

✅ 方案 1:将 CTR、转化率等数据存入 Elasticsearch

📌 方案思路

  • ES 作为广告数据的存储库,广告信息(广告 ID、广告主、预算、出价模式等)都存放在 ES。
  • CTR、转化率等动态数据也存入 ES,这样 DSP 竞价时查询广告数据的同时,也能拿到最新的指标。
    📌 优势
  1. 查询时可以直接获取完整广告数据
    • 避免 DSP 竞价时需要分别查询广告数据(ES)和指标数据(Redis)。
  2. 支持复杂查询(如广告筛选 + 排序)
    • 例如,可以按照 CTR 排序广告,或者筛选 转化率 > 3% 的广告进行竞价。
  3. 适合大规模数据分析
    • ES 本身支持大规模数据的查询和聚合,非常适合广告分析需求。

📌 可能的挑战

  1. 数据更新延迟
    • ES 的数据写入通常是批量同步异步刷新,相比 Redis 可能会有一定的延迟。
  2. 高频实时更新可能导致索引压力
    • CTR、转化率等数据每秒都会更新,如果直接存入 ES,可能会造成索引频繁变更,影响查询性能。
    • 解决方案:使用 ES 批量更新,减少索引写入压力。

✅ 方案 2:将 CTR、转化率等数据存入 Redis,广告数据存 ES

📌 方案思路

  • 广告的静态数据(如广告 ID、广告文案、出价模式)存入 ES。
  • 动态的 CTR、转化率数据 存入 Redis(数据源来自 DMP 实时计算)。
  • DSP 竞价时查询 ES 获取广告数据,同时查询 Redis 获取 CTR、转化率等指标

📌 优势

  1. Redis 支持高并发、超低延迟的查询
    • 适合高频动态数据查询,例如 CTR 每秒都在变化,而 Redis 读写速度极快。
  2. 降低 ES 的更新压力
    • 只需要在 ES 存储广告基础数据,减少索引变更,查询性能更稳定
  3. 支持超实时更新
    • 例如,Redis + Kafka/Flink 可以做到毫秒级数据更新,比 ES 更快

📌 可能的挑战

  1. DSP 竞价查询时需要两次查询(ES + Redis)
    • 可能比直接查询 ES 略微增加一次查询时间,但 Redis 读取速度极快,通常影响不大。
  2. 数据同步管理成本
    • 需要定期将 Redis 的 CTR 数据同步回 ES,确保 ES 也有较新的数据供分析。

🔎 方案对比

方案数据存储优点缺点适用场景
方案 1:CTR 直接存 ES广告 + CTR 统一存 ES查询时直接获取完整数据,支持复杂查询高频更新可能导致索引压力,更新有延迟适合中低频更新,查询广告数据时需要 CTR 参与排序
方案 2:CTR 存 Redis,广告存 ES广告数据存 ES,CTR 存 Redis读写速度快,适合超高并发查询,更新实时需要 DSP 查询 ES+Redis,数据同步成本适合高频更新的 CTR 数据,提高 DSP 竞价效率

✅ 推荐方案

  • 如果 DSP 竞价对实时性要求极高,CTR 每秒都在变化,建议:
    • 广告数据存 ES
    • CTR、转化率等动态数据存 Redis
    • DSP 竞价时同时查询 ES + Redis
  • 如果 CTR 数据更新频率较低(如 10 分钟更新一次),或者DSP 需要根据 CTR 排序筛选广告,可以:
    • 广告 + CTR 一起存入 ES
    • 避免高频索引更新,可以批量同步(如每 5~10 分钟更新一次)。

📌 你可以这样优化

如果你希望 兼顾查询效率 + 实时性

  1. CTR 主要存 Redis,DSP 竞价时从 Redis 读取
  2. 每天或每小时定期同步 CTR 数据到 ES,供后续广告分析用
  3. 使用 Kafka/Flink/ElasticSearch Bulk API 进行批量更新,降低索引压力

这样可以最大化查询性能,同时保证 DSP 竞价的实时性。😃

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值