如何在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加了索引。

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

原功能界面截图:

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

阅读 406
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库已经正确引入。

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

2 个回答

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

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 等压缩数据返回数据

前端数据处理的时间:

撰写回答
你尚未登录,登录后可以
  • 和开发者交流问题的细节
  • 关注并接收问题和回答的更新提醒
  • 参与内容的编辑和改进,让解决方法与时俱进
宣传栏