PHP 怎么供需匹配

wen PHP项目 1

本文目录导读:

PHP 怎么供需匹配

  1. 方案一:SQL 暴力筛选(适合小数据量,如 1 万以内)
  2. 方案二:Redis 有序集合(ZSET)匹配(适合抢单、高频实时)
  3. 方案三:匈牙利算法 / 贪心算法(适合一对多最优分配)
  4. 方案四:基于 Elasticsearch 的复杂匹配(适合全文检索+地理位置+权重)
  5. 方案五:死锁避免与超时处理(工程化细节)
  6. 总结与建议

在 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 中做匹配,最大的坑是并发,比如两个用户同时抢了同一个订单。

解决方案

  1. Redis 分布式锁:在匹配前对 supply_id 加锁。

    $lockKey = 'lock:supply:' . $supplyId;
    $locked = $redis->set($lockKey, 1, ['NX', 'EX' => 5]); // 5秒锁
    if (!$locked) {
        return response('该订单已被锁定,请稍后重试', 409);
    }
    // 执行匹配逻辑...
    // 完成后删除锁
    $redis->del($lockKey);
  2. 状态机(乐观锁):在 SQL 中更新时带上条件。

    UPDATE supplies 
    SET status = 'matched', user_id = :userId 
    WHERE id = :supplyId AND status = 'available' 
    -- 如果影响行数为0,说明已经被别人抢了
  3. 消息队列(延迟匹配):如果匹配算法耗时较长(如 5 秒),应该放入 RabbitMQ/Beanstalkd 队列异步处理,用户点击“找车”后先显示“寻找中...”,匹配完成后通过 WebSocket 推送结果。


总结与建议

如果你是初学者,建议先做 方案一(SQL)方案三(贪心) 来理解业务逻辑。

如果你是做中大型项目,需要根据核心痛点选型:

  • 需求:外卖/打车匹配 -> 方案二(Redis GEO)
  • 需求:简历与职位推荐 -> 方案四(Elasticsearch)
  • 需求:资源最优分配(最大化利润/最小化路程) -> 方案三(匈牙利算法)

PHP 8+ 的性能已经足以支撑此类计算,瓶颈通常在于 I/O(数据库/网络),因此尽量将热数据放入 Redis,并使用连接池或 Swoole 常驻内存来提升吞吐量。

抱歉,评论功能暂时关闭!