PHP实现按访问量占比分配的多设计A/B测试流量重定向咨询
一、核心疑问解答
1. 「按总访问量而非用户数占比分配」的具体含义
按总访问量占比分配的统计维度是单次访问请求,而非独立用户/独立设备:
- 例如总访问量为1000次时,严格按照配置比例分配500次到Design A、250次到Design B、250次到Design C
- 即使用户重复访问,每次访问都会被计入总访问量统计,不会因为用户之前看过某版本就固定分配,和基于用户标识固定版本的用户维度分配逻辑完全不同,更适合需要按流量比例精准切分的场景。
2. 所需数据库表结构设计
我们设计两张核心表,完全支持多活动、多变体的扩展需求:
ab_test_campaigns 活动主表
| 字段名 | 类型 | 说明 |
|---|---|---|
id | int(11) 主键 | 活动唯一ID |
campaign_name | varchar(100) | 活动名称 |
is_active | tinyint(1) | 是否启用 0=停用 1=启用 |
entry_url | varchar(255) | 活动入口访问地址 |
total_visits | bigint(20) | 活动累计总访问量 |
created_at | datetime | 创建时间 |
updated_at | datetime | 更新时间 |
ab_test_variants 变体配置表
| 字段名 | 类型 | 说明 |
|---|---|---|
id | int(11) 主键 | 变体唯一ID |
campaign_id | int(11) 外键 | 关联活动ID |
variant_name | varchar(50) | 变体名称 如Design A |
redirect_url | varchar(255) | 变体跳转目标地址 |
weight | int(3) | 分配占比 单位% 总和为100 |
current_visits | bigint(20) | 该变体累计访问量 |
is_enabled | tinyint(1) | 是否启用 0=停用 1=启用 |
上述结构已经支持单个活动无限新增变体,只需要调整各变体的weight占比即可,无需额外冗余字段。
3. 高效实现方案
为了避免每次请求都查库、锁表的性能问题,采用以下逻辑:
- 先从缓存读取当前活动的总访问量、各变体占比和访问量配置,不用每次请求查库
- 计算当前总访问量模100的余数,匹配余数落在哪个变体的占比区间,算法复杂度极低
- 异步更新总访问量和对应变体的访问量到数据库,不阻塞用户重定向
- 缓存1分钟自动刷新一次配置,并发场景下的误差在可接受范围内,完全满足业务需求
二、PHP代码实现示例
<?php // 数据库连接 可替换为项目自身的DB类 $pdo = new PDO('mysql:host=127.0.0.1;dbname=test', 'root', 'your_db_password'); // 匹配当前请求的入口路径 $currentUrl = $_SERVER['REQUEST_URI']; $cacheKey = 'ab_test_active_campaigns'; // 先读缓存 这里用Redis示例 可替换为项目自身缓存组件 $redis = new Redis(); $redis->connect('127.0.0.1', 6379); $campaigns = $redis->get($cacheKey); if (!$campaigns) { // 缓存过期 从DB读取所有启用的活动和变体 $stmt = $pdo->query("SELECT c.*, v.id as variant_id, v.redirect_url, v.weight FROM ab_test_campaigns c LEFT JOIN ab_test_variants v ON c.id = v.campaign_id WHERE c.is_active = 1 AND v.is_enabled = 1"); $rawData = $stmt->fetchAll(PDO::FETCH_ASSOC); // 整理数据结构 $campaigns = []; foreach ($rawData as $item) { if (!isset($campaigns[$item['entry_url']])) { $campaigns[$item['entry_url']] = [ 'id' => $item['id'], 'total_visits' => $item['total_visits'], 'variants' => [] ]; } $campaigns[$item['entry_url']]['variants'][] = [ 'id' => $item['variant_id'], 'redirect_url' => $item['redirect_url'], 'weight' => $item['weight'] ]; } // 写入缓存 有效期60秒 $redis->setex($cacheKey, 60, json_encode($campaigns)); } else { $campaigns = json_decode($campaigns, true); } // 匹配当前请求是否属于A/B测试活动 if (isset($campaigns[$currentUrl])) { $campaign = $campaigns[$currentUrl]; $totalVisits = $campaign['total_visits']; $remainder = $totalVisits % 100; // 匹配变体区间 $currentRange = 0; $targetVariant = null; foreach ($campaign['variants'] as $variant) { $currentRange += $variant['weight']; if ($remainder < $currentRange) { $targetVariant = $variant; break; } } if ($targetVariant) { // 生产环境建议用异步任务/消息队列更新访问量,避免阻塞重定向 $pdo->exec("UPDATE ab_test_campaigns SET total_visits = total_visits + 1 WHERE id = {$campaign['id']}"); $pdo->exec("UPDATE ab_test_variants SET current_visits = current_visits + 1 WHERE id = {$targetVariant['id']}"); // 执行重定向 header("Location: {$targetVariant['redirect_url']}"); exit; } } // 没有匹配到活动 走原有业务逻辑
内容的提问来源于stack exchange,提问作者Alex
相关产品推荐
相关产品推荐

