本文目录导读:

- 最基础版(Cookie + 随机分流)
- 进阶版(Redis 存储 + 加权分流 + 数据埋点)
- 纯前端方案(JavaScript + 服务端 API)
- 生产级架构建议(PHP + 数据库 + 消息队列)
- 最佳实践与踩坑指南
- 开源工具推荐(可直接集成)
在 PHP 中实现 A/B 测试,核心是分流(决定用户进入哪个版本)和数据收集(记录转化),下面是一套从基础到进阶的设计方案,包含代码示例。
最基础版(Cookie + 随机分流)
适用场景:简单的页面样式或文案测试,不依赖用户系统。
<?php
function get_variant($test_name, $variants = ['A', 'B'], $expire_days = 30) {
// 1. 检查是否已有分配结果
$cookie_name = 'ab_test_' . $test_name;
if (isset($_COOKIE[$cookie_name])) {
return $_COOKIE[$cookie_name];
}
// 2. 随机分配(50/50)
$variant = $variants[array_rand($variants)];
// 3. 写入 Cookie(稳定分组 + 过期时间)
setcookie($cookie_name, $variant, time() + ($expire_days * 86400), '/');
return $variant;
}
// 使用
$layout = get_variant('homepage_layout');
if ($layout === 'B') {
// 显示新版布局
} else {
// 显示旧版布局
}
?>
缺点:
- 无法精细控制流量比例(如 10% 流量进入测试组)
- 无法知道用户真实身份,无法做用户分层
- 数据统计需要自行埋点上报
进阶版(Redis 存储 + 加权分流 + 数据埋点)
适用场景:后端渲染、有用户系统、需要精确控制流量比例。
<?php
class ABTestService {
private $redis;
private $user_id;
public function __construct($redis, $user_id) {
$this->redis = $redis;
$this->user_id = $user_id;
}
/**
* 获取实验分组(保证同用户始终同组)
*/
public function assign($experiment_key, $groups = ['A' => 50, 'B' => 50]) {
// 1. 检查是否已分配
$cache_key = "abtest:{$experiment_key}:user:{$this->user_id}";
$cached = $this->redis->get($cache_key);
if ($cached) return $cached;
// 2. 哈希算法:同一 user_id 永远映射到同一组(也可使用 random 结合 DB 存储)
$hash = crc32($this->user_id . $experiment_key);
$total_weight = array_sum($groups);
$threshold = $hash % $total_weight;
$cumulative = 0;
foreach ($groups as $group => $weight) {
$cumulative += $weight;
if ($threshold < $cumulative) {
// 3. 存储结果(通常带过期时间)
$this->redis->setex($cache_key, 86400 * 30, $group);
return $group;
}
}
return array_key_first($groups); // 兜底
}
/**
* 上报转化事件
*/
public function track($experiment_key, $event_name, $user_id = null) {
$uid = $user_id ?: $this->user_id;
$group = $this->redis->get("abtest:{$experiment_key}:user:{$uid}");
// 写入统计队列(异步消费)
$this->redis->lpush('abtest:events', json_encode([
'experiment' => $experiment_key,
'group' => $group,
'event' => $event_name,
'user_id' => $uid,
'timestamp' => time()
]));
}
}
// 使用示例
$ab = new ABTestService($redis, $user->id);
$variant = $ab->assign('checkout_button', ['A' => 90, 'B' => 10]); // 10% 流量看到新按钮
if ($variant === 'B') {
// 渲染新按钮
} else {
// 渲染旧按钮
}
// 用户点击“购买”时
$ab->track('checkout_button', 'purchase_click');
?>
优点:
- 可控流量比例(90/10)
- 用户分组持久稳定
- 支持事件埋点
纯前端方案(JavaScript + 服务端 API)
适用场景:SPA(单页应用)或前端渲染的项目,后端只需提供配置和数据接口。
// PHP 后端 API:返回实验配置
// /api/abtest/config.php
<?php
header('Content-Type: application/json');
$experiments = [
'pricing_page' => [
'groups' => ['A' => 70, 'B' => 20, 'C' => 10],
'active' => true
],
'signup_form' => [
'groups' => ['A' => 50, 'B' => 50],
'active' => false
]
];
// 如果用户已登录,可获取 user_id 做一致性分组
$user_id = $_GET['user_id'] ?? $_COOKIE['user_id'] ?? null;
echo json_encode([
'experiments' => $experiments,
'user_id' => $user_id
]);
?>
// 前端 JavaScript
async function getABConfig() {
const res = await fetch('/api/abtest/config.php');
const data = await res.json();
return data;
}
// 简单分组函数
function assignGroup(userId, experimentKey, groups) {
const hash = hashCode(userId + experimentKey);
const total = Object.values(groups).reduce((a, b) => a + b, 0);
const threshold = hash % total;
let cumulative = 0;
for (const [group, weight] of Object.entries(groups)) {
cumulative += weight;
if (threshold < cumulative) return group;
}
return Object.keys(groups)[0];
}
// 应用实验
const config = await getABConfig();
if (localStorage.getItem('uid') === null) {
localStorage.setItem('uid', Math.random().toString(36).substring(2, 15));
}
const uid = localStorage.getItem('uid');
if (config.experiments.pricing_page.active) {
const variant = assignGroup(uid, 'pricing_page', config.experiments.pricing_page.groups);
if (variant === 'B') {
// 渲染B版本价格页
} else if (variant === 'C') {
// 渲染C版本价格页
}
}
// 发送事件
function trackABEvent(experiment, event) {
fetch('/api/abtest/track.php', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
experiment,
event,
user_id: uid,
group: localStorage.getItem('ab_' + experiment)
})
});
}
生产级架构建议(PHP + 数据库 + 消息队列)
数据模型设计
-- 实验表
CREATE TABLE ab_experiments (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(100) UNIQUE NOT NULL,
description TEXT,
status ENUM('draft', 'running', 'paused', 'completed') DEFAULT 'draft',
traffic_percent TINYINT DEFAULT 100, -- 参与流量百分比
start_date DATETIME,
end_date DATETIME,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 变体表
CREATE TABLE ab_variants (
id INT AUTO_INCREMENT PRIMARY KEY,
experiment_id INT NOT NULL,
variant_key VARCHAR(50) NOT NULL, -- A/B/C
weight INT DEFAULT 50, -- 权重
config JSON, -- 该变体的具体配置(如文案、样式)
FOREIGN KEY (experiment_id) REFERENCES ab_experiments(id)
);
-- 用户分组表(记录用户被分配到哪个实验的哪个组)
CREATE TABLE ab_user_assignments (
id INT AUTO_INCREMENT PRIMARY KEY,
experiment_id INT NOT NULL,
user_id INT NOT NULL, -- 或使用设备ID
variant_key VARCHAR(50) NOT NULL,
assigned_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY (experiment_id, user_id)
);
-- 事件表
CREATE TABLE ab_events (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
experiment_id INT NOT NULL,
user_id INT NOT NULL,
variant_key VARCHAR(50) NOT NULL,
event_name VARCHAR(100) NOT NULL, -- 'view', 'click', 'purchase'
event_value DECIMAL(10,2) DEFAULT 0, -- 如金额
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
INDEX idx_exp_event (experiment_id, event_name)
);
关键操作流程
<?php
// 1. 初始化实验(管理后台)
function create_experiment($name, $variants) {
$pdo->beginTransaction();
$pdo->exec("INSERT INTO ab_experiments (name, status) VALUES ('$name', 'draft')");
$exp_id = $pdo->lastInsertId();
foreach ($variants as $variant) {
$pdo->exec("INSERT INTO ab_variants (experiment_id, variant_key, weight, config)
VALUES ($exp_id, '{$variant['key']}', {$variant['weight']}, '{$variant['config']}')");
}
$pdo->commit();
}
// 2. 获取用户分组
function get_user_variant($user_id, $experiment_name) {
$exp = $pdo->query("SELECT id FROM ab_experiments WHERE name = '$experiment_name'")->fetch();
// 查找已有分配
$assigned = $pdo->query("SELECT variant_key FROM ab_user_assignments
WHERE experiment_id = {$exp['id']} AND user_id = $user_id")->fetch();
if ($assigned) return $assigned['variant_key'];
// 随机分配(加锁防止并发)
$variants = $pdo->query("SELECT * FROM ab_variants
WHERE experiment_id = {$exp['id']}")->fetchAll();
$pool = [];
foreach ($variants as $v) {
for ($i = 0; $i < $v['weight']; $i++) $pool[] = $v['variant_key'];
}
shuffle($pool);
$variant = $pool[array_rand($pool)];
// 记录分配
$pdo->exec("INSERT INTO ab_user_assignments (experiment_id, user_id, variant_key)
VALUES ({$exp['id']}, $user_id, '$variant')");
return $variant;
}
// 3. 数据统计(后台报表)
// 示例:点击率对比
SELECT
variant_key,
COUNT(DISTINCT CASE WHEN event_name = 'view' THEN user_id END) AS views,
COUNT(DISTINCT CASE WHEN event_name = 'purchase' THEN user_id END) AS purchases,
(COUNT(DISTINCT CASE WHEN event_name = 'purchase' THEN user_id END) * 100.0 /
NULLIF(COUNT(DISTINCT CASE WHEN event_name = 'view' THEN user_id END), 0)) AS conversion_rate
FROM ab_events
WHERE experiment_id = 1
GROUP BY variant_key;
最佳实践与踩坑指南
✅ 好的做法:
- 统一分流逻辑:所有实验通过同一个类/服务管理,确保同一用户每次请求拿到相同分组。
- 预注册实验:在生产环境发布前,先在数据库中注册实验,不要动态新增实验。
- 容量预留:实验开始前估算样本量(可使用统计功效分析工具)。
- 监控实验进度:设置最小样本量、显著性水平(如 P < 0.05),达到后停止实验。
- 多端一致性:如果同时有 Web 和 App,需要统一用户标识(如手机号/邮箱/设备ID映射)。
❌ 常见坑:
- 拿不到统计数据:忘记在关键转化点(如下单、注册)调用
track()方法。 - 分组不稳定:使用
rand()而不使用 Cookie/DB 持久化,导致用户刷新页面就换组。 - 多重检验问题:同时跑 10 个实验,总会有“假阳性”结果,需要做 Bonferroni 校正或整体显著性判断。
- 无视用户体验:实验可能导致页面布局闪跳(新旧版本交替渲染),需要前端配合做防闪烁加载。
- 数据量不足就下结论:至少等转化事件达到几百条再分析,避免小样本随机波动骗你。
开源工具推荐(可直接集成)
| 工具 | 特点 |
|---|---|
| Google Optimize | 免费(已停服,可参考),前端装载 |
| Vanity | 基于 Rails,但有 PHP 版本,简单易用 |
| GrowthBook | 开源,支持 PHP SDK,带可视化实验管理后台 |
| AWS CloudWatch Evidently | AWS 服务,支持 PHP SDK |
| 自研 | 灵活,适合复杂业务场景 |
对于 PHP 项目,推荐方案:
- 中小型项目:Redis + 用户 ID 哈希分组 + 异步事件埋点(方案二)
- 大型或复杂业务:数据库存储实验配置 + 独立统计服务 + BI 报表(方案四)
- 快速验证:本地 Cookie 随机分组(方案一),但要意识到数据质量不高
核心关键是:保证用户分组稳定 + 埋点覆盖完整漏斗 + 统计显著性判断,三者缺一不可。
版权声明:NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播