如何在ThinkPHP 8.0.2中高效查询并展示几十万条地图数据?

PHP如何设计个一次查询几十万条数据的接口??

框架:thinkphp 8.0.2
数据库: mysql 8.0
百度地图:mapv.DataSet()mapv.baiduMapLayer()

目前正在重构一个地图坐标相关的功能(百度地图坐标标点),遇到了一些问题,在project_data数据表中有近百万条数据,存放的是每个施工项目的数据,其中包含项目id,和施工记录的经纬度等字段。
现在需要开发一个后台功能,将这些项目的施工记录通过经纬度在百度地图上标点,但是有些项目的施工记录数据达到了20万条左右。有什么方案可以更好的实现这个功能?

原功能实现的逻辑是,前端通过传参proj_id循环调接口获取对应项目的数据(每次请求3万条),但是这样也非常耗时,从请求接口到渲染数据,整个流程大概耗时大概是15秒-20秒。字段proj_id加了索引。

目前不需要考虑实时推送最新施工记录的问题,也不需要考虑并发问题,后台没几个人用,也就是请求接口拿数据,然后渲染就完事了。

原功能界面截图(图2是图1缩放后的效果,会聚合在一起并显示坐标点的数量):

想尝试使用Redis来存储数据,但是本人对Redis的知识尚少,不知道能否实现,目前也在考虑这个方案。希望有这方面经验的老哥,可以指点一下,谢谢。

阅读 538
avatarAI BotBETA

回答

在ThinkPHP 8.0.2中高效查询并展示几十万条地图数据,可以考虑以下方案:

1. 分页查询与前端聚合

  • 后端分页查询:不要一次性查询几十万条数据,而是采用分页查询的方式,每次查询固定数量的数据(如每次查询几千条)。这可以通过数据库的分页功能(如MySQL的LIMITOFFSET)来实现。
  • 前端聚合:前端在接收到分页数据后,逐步聚合这些数据并渲染到地图上。这可以通过JavaScript的数组操作或者使用像mapv这样的库来实现。

2. 使用Redis缓存

  • 数据缓存:考虑到查询大量数据对数据库的压力,可以将查询结果缓存到Redis中。当需要展示数据时,先从Redis中读取,如果Redis中没有,再从数据库中查询并缓存到Redis。
  • 过期策略:为缓存的数据设置合理的过期时间,以确保数据的实时性。

3. 优化数据库查询

  • 索引优化:确保proj_id字段上有索引,这可以显著提高查询性能。
  • 查询优化:检查查询语句,确保没有不必要的字段被查询,同时避免使用复杂的JOIN操作。

4. 使用WebSocket实时推送(可选)

  • 如果未来需要考虑实时推送最新施工记录,可以考虑使用WebSocket技术。这样前端可以实时接收到新的数据并更新地图。

5. 示例代码

以下是一个简单的示例,展示如何在ThinkPHP中实现分页查询:

// 在控制器中
public function getProjectData($proj_id, $page = 1, $limit = 5000)
{
    $data = Db::name('project_data')
        ->where('proj_id', $proj_id)
        ->limit(($page - 1) * $limit, $limit)
        ->select();

    return json($data);
}

前端JavaScript代码示例(使用mapv):

let dataSet = new mapv.DataSet();
let page = 1;
let limit = 5000;

function fetchData(proj_id) {
    fetch(`/getProjectData?proj_id=${proj_id}&page=${page}&limit=${limit}`)
        .then(response => response.json())
        .then(data => {
            dataSet.add(data);
            renderMap(dataSet);

            // 如果还有数据,继续请求下一页
            if (data.length === limit) {
                page++;
                fetchData(proj_id);
            }
        });
}

function renderMap(dataSet) {
    let mapLayer = new mapv.baiduMapLayer(dataSet, {
        // 配置项
        draw: 'scatter',
        size: 10,
        color: '#a6c84c',
        style: {
            shadowBlur: 20,
            shadowColor: '#aaa'
        }
    });

    // 将图层添加到百度地图上
    BMap.Map.addLayer(mapLayer);
}

// 调用函数获取数据并渲染地图
fetchData(yourProjectId);

注意:上述代码是一个简化的示例,实际使用时需要根据具体需求进行调整。特别是前端部分,需要确保百度地图已经正确初始化,并且mapv库已经正确引入。

通过以上方案,可以显著提高查询和展示大量地图数据的效率。

3 个回答

我提供一个示例给你参考下

1. 连接 Redis 并插入数据

首先,连接到 Redis 并批量插入施工记录数据:

<?php
// 连接 Redis
$redis = new Redis();
$redis->connect('127.0.0.1', 6379);

// 示例数据
$data = [
    ['project_id' => 1, 'record_id' => 101, 'latitude' => 39.915, 'longitude' => 116.404],
    ['project_id' => 1, 'record_id' => 102, 'latitude' => 39.916, 'longitude' => 116.405],
    // 更多数据...
];

// 批量插入数据
$pipeline = $redis->multi(Redis::PIPELINE);
foreach ($data as $record) {
    $key = "project:{$record['project_id']}:{$record['record_id']}";
    $pipeline->hmset($key, $record);
}
$pipeline->exec();
?>

2. 查询数据

根据项目 ID 查询所有施工记录数据:

<?php
// 查询数据
$project_id = 1;
$keys = $redis->keys("project:$project_id:*");
$pipeline = $redis->multi(Redis::PIPELINE);
foreach ($keys as $key) {
    $pipeline->hgetall($key);
}
$results = $pipeline->exec();

// 处理查询结果
$points = [];
foreach ($results as $result) {
    $points[] = [
        'lng' => $result['longitude'],
        'lat' => $result['latitude']
    ];
}
?>

3. 在百度地图上标点

使用百度地图 API 将查询到的点标记在地图上:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>百度地图标点示例</title>
    <script src="https://api.map.baidu.com/api?v=3.0&ak=你的百度地图API密钥"></script>
    <script src="https://mapv.baidu.com/build/mapv.min.js"></script>
</head>
<body>
    <div id="map" style="width: 100%; height: 500px;"></div>
    <script>
        var map = new BMap.Map("map");
        map.centerAndZoom(new BMap.Point(116.404, 39.915), 11);
        map.enableScrollWheelZoom(true);

        var data = <?php echo json_encode($points); ?>;
        var points = data.map(function(item) {
            return new BMap.Point(item.lng, item.lat);
        });

        var options = {
            size: BMAP_POINT_SIZE_SMALL,
            shape: BMAP_POINT_SHAPE_CIRCLE,
            color: '#d340c3'
        };
        var pointCollection = new BMap.PointCollection(points, options);
        map.addOverlay(pointCollection);
    </script>
</body>
</html>

后续优化

4. 优化和扩展

  • 数据分片:如果数据量非常大,可以将数据分片存储在多个 Redis 实例中。
  • 可以使用集群,这里使用手动数据分片参考代码

    <?php
    // 连接多个 Redis 实例
    $redis1 = new Redis();
    $redis1->connect('127.0.0.1', 6379);
    
    $redis2 = new Redis();
    $redis2->connect('127.0.0.1', 6380);
    
    // 根据 project_id 分片存储数据
    function getRedisInstance($project_id) {
      global $redis1, $redis2;
      return ($project_id % 2 == 0) ? $redis1 : $redis2;
    }
    
    // 示例数据
    $data = [
      ['project_id' => 1, 'record_id' => 101, 'latitude' => 39.915, 'longitude' => 116.404],
      ['project_id' => 1, 'record_id' => 102, 'latitude' => 39.916, 'longitude' => 116.405],
      // 更多数据...
    ];
    
    // 插入数据
    foreach ($data as $record) {
      $redis = getRedisInstance($record['project_id']);
      $key = "project:{$record['project_id']}:{$record['record_id']}";
      $redis->hmset($key, $record);
    }
    ?>
    
  • 缓存更新策略:确保数据一致性,可以使用写通过(Write-Through)或缓存失效(Cache Invalidation)策略。
  • 写通过参考代码
<?php
// 更新数据库和缓存
function updateRecord($project_id, $record_id, $new_data) {
    global $db;
    // 更新数据库
    $db->update('project_data', $new_data, ['project_id' => $project_id, 'record_id' => $record_id]);

    // 更新缓存
    $redis = getRedisInstance($project_id);
    $key = "project:{$project_id}:{$record_id}";
    $redis->hmset($key, $new_data);
}
?>
  • 缓存失效策略参考代码

    <?php
    // 更新数据库和使缓存失效
    function updateRecord($project_id, $record_id, $new_data) {
      global $db;
      // 更新数据库
      $db->update('project_data', $new_data, ['project_id' => $project_id, 'record_id' => $record_id]);
    
      // 使缓存失效
      $redis = getRedisInstance($project_id);
      $key = "project:{$project_id}:{$record_id}";
      $redis->del($key);
    }
    
    // 查询数据时重新加载缓存
    function getRecord($project_id, $record_id) {
      $redis = getRedisInstance($project_id);
      $key = "project:{$project_id}:{$record_id}";
      $record = $redis->hgetall($key);
    
      if (empty($record)) {
          // 缓存中没有数据,从数据库中查询并重新加载缓存
          global $db;
          $record = $db->select('project_data', '*', ['project_id' => $project_id, 'record_id' => $record_id]);
          if ($record) {
              $redis->hmset($key, $record);
          }
      }
    
      return $record;
    }
    ?>
    
  • 分页查询:避免一次性加载大量数据,使用分页技术分批次加载。
  • 分页查询参考代码
<?php
// 分页查询数据
function getRecords($project_id, $page, $limit) {
    $redis = getRedisInstance($project_id);
    $start = ($page - 1) * $limit;
    $keys = $redis->keys("project:$project_id:*");
    $paged_keys = array_slice($keys, $start, $limit);

    $pipeline = $redis->multi(Redis::PIPELINE);
    foreach ($paged_keys as $key) {
        $pipeline->hgetall($key);
    }
    return $pipeline->exec();
}

// 示例:获取第1页,每页10条记录
$page = 1;
$limit = 10;
$records = getRecords(1, $page, $limit);
?>

补充使用 Redis 实现分页查询并根据 created_at 字段排序,同时在百度地图上标点

1. 插入数据到有序集合

首先,将每条记录的 created_at 时间戳作为分数(score),记录的唯一标识(如 record_id)作为成员(member)插入有序集合:

<?php
// 连接 Redis
$redis = new Redis();
$redis->connect('127.0.0.1', 6379);

// 示例数据
$data = [
    ['record_id' => 101, 'created_at' => strtotime('2024-11-01 10:00:00')],
    ['record_id' => 102, 'created_at' => strtotime('2024-11-01 11:00:00')],
    // 更多数据...
];

// 插入数据到有序集合
foreach ($data as $record) {
    $redis->zadd('records_by_created_at', $record['created_at'], $record['record_id']);
}
?>

2. 分页查询数据

使用 ZRANGEZREVRANGE 命令根据分数范围分页查询数据:

<?php
// 分页查询数据
function getRecordsByPage($project_id, $page, $limit) {
    global $redis;
    $start = ($page - 1) * $limit;
    $end = $start + $limit - 1;
    return $redis->zrevrange("project:$project_id:records_by_created_at", $start, $end);
}

// 示例:获取第1页,每页20,000条记录
$page = 1;
$limit = 20000;
$project_id = 1;
$record_ids = getRecordsByPage($project_id, $page, $limit);
?>

3. 批量获取详细数据每次处理1000条

根据查询到的 record_id 从Redis中取:

<?php
// 批量获取详细数据
function getRecordDetailsInBatches($record_ids, $batch_size = 1000) {
    global $redis;
    $results = [];
    $total = count($record_ids);
    for ($i = 0; $i < $total; $i += $batch_size) {
        $batch_ids = array_slice($record_ids, $i, $batch_size);
        $pipeline = $redis->multi(Redis::PIPELINE);
        foreach ($batch_ids as $record_id) {
            $pipeline->hgetall($record_id);
        }
        $results = array_merge($results, $pipeline->exec());
    }
    return $results;
}

$records = getRecordDetailsInBatches($record_ids, 1000); // 每批次处理 1,000 条记录
?>

4. 在百度地图上标点

使用百度地图 API 将查询到的点标记在地图上:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>百度地图标点示例</title>
    <script src="https://api.map.baidu.com/api?v=3.0&ak=你的百度地图API密钥"></script>
    <script src="https://mapv.baidu.com/build/mapv.min.js"></script>
</head>
<body>
    <div id="map" style="width: 100%; height: 500px;"></div>
    <script>
        var map = new BMap.Map("map");
        map.centerAndZoom(new BMap.Point(116.404, 39.915), 11);
        map.enableScrollWheelZoom(true);

        var data = <?php echo json_encode($records); ?>;
        var points = data.map(function(item) {
            return new BMap.Point(item.longitude, item.latitude);
        });

        var options = {
            size: BMAP_POINT_SIZE_SMALL,
            shape: BMAP_POINT_SHAPE_CIRCLE,
            color: '#d340c3'
        };
        var pointCollection = new BMap.PointCollection(points, options);
        map.addOverlay(pointCollection);
    </script>
</body>
</html>

可以先分析下 数据响应时间 和 前端数据处理的时间 分别多少, 每个环节的时间

数据响应时间优化建议:

  • reids 等 优化查询处理 (mysql 可以加 proj_id + 需要的数据 的索引 避免回表)
  • 减少返回无用字段 ,或内容结构压缩调整
  • gzip 等压缩数据返回数据

前端数据处理的时间:

看着上面那位高手服务端的方案已经给的很详细了,前端在给点建议。
前端还可以使用浏览器端的缓存做进一步优化,更短时间内拿到数据渲染页面,页面的需求方案和渲染的程序在做一下优化调整达到一个最优的平衡,更快的展示页面。

宣传栏