本文目录导读:

- 方案一:SQL 暴力筛选(适合小数据量,如 1 万以内)
- 方案二:Redis 有序集合(ZSET)匹配(适合抢单、高频实时)
- 方案三:匈牙利算法 / 贪心算法(适合一对多最优分配)
- 方案四:基于 Elasticsearch 的复杂匹配(适合全文检索+地理位置+权重)
- 方案五:死锁避免与超时处理(工程化细节)
- 总结与建议
在 PHP 中实现“供需匹配”是一个非常宽泛且高频的需求(常见于打车、兼职、二手交易、外卖、工单分配等场景),它并没有一个固定的函数,而是根据业务规则、数据规模和实时性要求来设计算法。
下面我按照从简单到复杂的思路,给出几种常见的 PHP 供需匹配实现方案,并附上核心代码示例。
SQL 暴力筛选(适合小数据量,如 1 万以内)
如果数据量不大,且匹配规则简单(如按地区、价格区间),直接用 MySQL 查询是最快的。
场景:用户发布一个“求购”需求,系统在“供应”表中找匹配。
<?php
// 用户的需求参数
$userDemand = [
'city' => '上海',
'district' => '浦东',
'price_max' => 100,
'category' => '家电'
];
// PDO 预处理查询
$pdo = new PDO('mysql:host=localhost;dbname=test', 'root', '');
$sql = "SELECT * FROM supplies
WHERE city = :city
AND district = :district
AND price <= :price_max
AND category = :category
AND status = 'available'
ORDER BY distance ASC, rating DESC
LIMIT 20";
$stmt = $pdo->prepare($sql);
$stmt->execute($userDemand);
$matches = $stmt->fetchAll(PDO::FETCH_ASSOC);
// 如果没找到,可以放宽条件(降级匹配)
if (empty($matches)) {
$sql = "SELECT * FROM supplies
WHERE city = :city
AND category = :category
AND price <= :price_max * 1.2 -- 放宽价格
AND status = 'available'
ORDER BY rating DESC
LIMIT 20";
// ... 重新执行
}
?>
缺点:无法处理复杂的权重排序(如“距离近”与“信誉好”的权衡),也无法处理实时地理位置计算。
Redis 有序集合(ZSET)匹配(适合抢单、高频实时)
类似滴滴抢单模式:供应方(司机)在 Redis 里维护自己的状态,需求方(乘客)发起请求时,从 Redis 中快速取出符合条件的人。
核心逻辑:使用 GEOADD 存储位置,使用 ZSET 存储优先级(如评分、距离分数)。
<?php
$redis = new Redis();
// 1. 司机上线,将自己的坐标存入 GEO,并设置一个“可用状态”ZSET
$redis->geoAdd('drivers:available', 121.47, 31.23, 'driver_001'); // 经度,纬度,ID
// 2. 乘客叫车,搜索附近 3 公里内的司机,并按距离排序
$distance = 3; // 单位:公里
$nearbyDrivers = $redis->geoRadius('drivers:available', 121.50, 31.20, $distance, 'km', ['WITHDIST' => true, 'ASC' => true]);
foreach ($nearbyDrivers as $driver) {
// $driver = [0 => 'driver_001', 1 => '0.5'] // 0是ID,1是距离
// 3. 二次过滤:检查司机评分是否达标
$score = $redis->zScore('driver:rating', $driver[0]);
if ($score >= 4.5) {
// 4. 分配订单,并将司机从可用集合移除(防止重复接单)
$redis->zRem('drivers:ready', $driver[0]); // 移出准备队列
$redis->geoRemove('drivers:available', $driver[0]);
echo "匹配成功:司机 {$driver[0]},距离 {$driver[1]} 公里,评分 {$score}";
break; // 只匹配第一个
}
}
?>
优点:毫秒级响应,非常适合实时性要求高的场景。
匈牙利算法 / 贪心算法(适合一对多最优分配)
比如有 10 个外卖员,20 个订单,如何分配让总配送距离最短?这就是线性分配问题。
思路:使用 Kuhn-Munkres 算法(匈牙利算法),虽然没有内置函数,但可以通过 PHP 实现或使用第三方库(如 rubix/ml)。
简单贪心示例(如果不想引入复杂算法):
<?php
// 需求列表
$tasks = [
['id' => 't1', 'x' => 10, 'y' => 10],
['id' => 't2', 'x' => 30, 'y' => 40],
['id' => 't3', 'x' => 50, 'y' => 60],
];
// 供给列表(工人)
$workers = [
['id' => 'w1', 'x' => 15, 'y' => 15],
['id' => 'w2', 'x' => 50, 'y' => 50],
];
// 计算成本矩阵(欧氏距离)
$cost = [];
foreach ($workers as $wi => $worker) {
foreach ($tasks as $ti => $task) {
$cost[$wi][$ti] = sqrt(pow($worker['x'] - $task['x'], 2) + pow($worker['y'] - $task['y'], 2));
}
}
// 贪心匹配:每次找距离最小的组合
$matchedTasks = [];
$assignments = [];
while (count($assignments) < min(count($workers), count($tasks))) {
$minCost = INF;
$minW = null;
$minT = null;
foreach ($cost as $wi => $taskCosts) {
foreach ($taskCosts as $ti => $c) {
if ($c < $minCost && !in_array($ti, $matchedTasks) && !isset($assignments[$wi])) {
$minCost = $c;
$minW = $wi;
$minT = $ti;
}
}
}
if ($minW !== null) {
$assignments[$workers[$minW]['id']] = $tasks[$minT]['id'];
$matchedTasks[] = $minT;
}
}
print_r($assignments); // 输出匹配结果
?>
注意:贪心算法不是全局最优,但如果数据量小且要求不高(如派单给最近的空闲人),这个就够了。
基于 Elasticsearch 的复杂匹配(适合全文检索+地理位置+权重)
如果匹配规则复杂(如“找保姆”不只要看距离,还要看价格、星级、服务类型、是否住家),且数据量在百万级。
核心思路:将供需双方的数据同步到 Elasticsearch,利用其 function_score 查询做多条件加权排序。
<?php
// 使用 Elasticsearch PHP Client
$client = ClientBuilder::create()->build();
$params = [
'index' => 'service_providers',
'body' => [
'query' => [
'bool' => [
'must' => [
['term' => ['city' => '上海']],
['term' => ['is_available' => true]],
],
'filter' => [
['geo_distance' => ['distance' => '5km', 'location' => ['lat' => 31.20, 'lon' => 121.50]]]
]
]
],
'sort' => [
'_geo_distance' => [ // 按距离排序
'location' => ['lat' => 31.20, 'lon' => 121.50],
'order' => 'asc',
'unit' => 'km'
]
],
// 也可以加上 script_score 计算加权分
'size' => 10
]
];
$response = $client->search($params);
// 处理返回结果...
?>
死锁避免与超时处理(工程化细节)
在 PHP 中做匹配,最大的坑是并发,比如两个用户同时抢了同一个订单。
解决方案:
-
Redis 分布式锁:在匹配前对
supply_id加锁。$lockKey = 'lock:supply:' . $supplyId; $locked = $redis->set($lockKey, 1, ['NX', 'EX' => 5]); // 5秒锁 if (!$locked) { return response('该订单已被锁定,请稍后重试', 409); } // 执行匹配逻辑... // 完成后删除锁 $redis->del($lockKey); -
状态机(乐观锁):在 SQL 中更新时带上条件。
UPDATE supplies SET status = 'matched', user_id = :userId WHERE id = :supplyId AND status = 'available' -- 如果影响行数为0,说明已经被别人抢了
-
消息队列(延迟匹配):如果匹配算法耗时较长(如 5 秒),应该放入 RabbitMQ/Beanstalkd 队列异步处理,用户点击“找车”后先显示“寻找中...”,匹配完成后通过 WebSocket 推送结果。
总结与建议
如果你是初学者,建议先做 方案一(SQL) 和 方案三(贪心) 来理解业务逻辑。
如果你是做中大型项目,需要根据核心痛点选型:
- 需求:外卖/打车匹配 -> 方案二(Redis GEO)
- 需求:简历与职位推荐 -> 方案四(Elasticsearch)
- 需求:资源最优分配(最大化利润/最小化路程) -> 方案三(匈牙利算法)
PHP 8+ 的性能已经足以支撑此类计算,瓶颈通常在于 I/O(数据库/网络),因此尽量将热数据放入 Redis,并使用连接池或 Swoole 常驻内存来提升吞吐量。