我们可以通过 分页 + 缓存 + 异步统计 来完美解决。
非常好 👍 你这个需求(200w 用户列表页面卡顿)是大多数中大型系统后台的通病,我们可以通过 分页 + 缓存 + 异步统计 来完美解决。
下面我给出完整方案,分为 架构思路 → SQL 优化 → 缓存方案 → ThinkPHP 示例代码 四个部分,拿去直接可落地用。👇
一、架构思路(核心目标)
| 模块 | 优化目标 | 说明 |
|---|---|---|
| 分页查询 | 秒级打开 | 只查询 20 条数据,不做复杂统计 |
| 总数统计 | 缓存或异步更新 | 避免每次执行 COUNT(200w) |
| 搜索 / 筛选 | 可索引字段查询 | 避免模糊 LIKE + JOIN |
| JOIN 数据 | 最小化 | 尽量只在必要字段时再 JOIN |
二、数据库设计与索引优化
1️⃣ 关键索引
ALTER TABLE eb_user ADD PRIMARY KEY (uid);
ALTER TABLE eb_user ADD INDEX idx_created_at(created_at);
ALTER TABLE eb_wechat_user ADD INDEX idx_uid(uid);
uid 是主键; created_at 用于分页排序; JOIN 用的字段必须有索引。
三、分页 + 异步统计方案
方案目标
- 列表页加载快(1 秒内)
- 总数、搜索、统计延迟可接受(几分钟内更新)
1. 用户列表分页 SQL(高效版)
SELECT
u.uid,
u.nickname,
u.phone,
w.openid,
w.subscribe
FROM eb_user u
LEFT JOIN eb_wechat_user w ON u.uid = w.uid
ORDER BY u.created_at DESC
LIMIT 0, 20;
- 避免在分页时使用 COUNT(*)。
- 用索引字段排序(created_at 或 uid)。
2. 用户总数统计缓存表
新建一个缓存表:
CREATE TABLE sys_stat_cache (
id INT PRIMARY KEY AUTO_INCREMENT,
stat_key VARCHAR(64) UNIQUE,
stat_value BIGINT,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
初始化:
INSERT INTO sys_stat_cache (stat_key, stat_value) VALUES ('user_total', 0);
3. 定时任务更新用户总数
用一个后端定时脚本(如 ThinkPHP 命令行任务):
<?php
namespace app\command;
use think\console\Command;
use think\console\Input;
use think\console\Output;
use think\facade\Db;
class UpdateUserCount extends Command
{
protected function configure()
{
$this->setName('update:usercount')
->setDescription('更新用户总数缓存');
}
protected function execute(Input $input, Output $output)
{
$count = Db::name('eb_user')->count();
Db::name('sys_stat_cache')
->where('stat_key', 'user_total')
->update(['stat_value' => $count]);
$output->writeln("User total updated: {$count}");
}
}
建议执行周期:
- 每 5 分钟一次(用 crontab 或队列触发)
- 对 200w 用户表,这种 COUNT 操作耗时 2~5 秒,完全可以后台运行。
4. 后台控制器读取缓存统计
// app/admin/controller/UserController.php
use think\facade\Db;
public function index()
{
// 分页参数
$page = input('page', 1);
$limit = input('limit', 20);
$offset = ($page - 1) * $limit;
// 快速分页查询
$list = Db::name('eb_user')
->alias('u')
->leftJoin('eb_wechat_user w', 'u.uid=w.uid')
->field('u.uid,u.nickname,u.phone,w.openid,w.subscribe')
->order('u.created_at desc')
->limit($offset, $limit)
->select();
// 从缓存表读取总数
$total = Db::name('sys_stat_cache')
->where('stat_key', 'user_total')
->value('stat_value');
return json([
'code' => 0,
'data' => [
'list' => $list,
'total' => $total
]
]);
}
效果:
- 查询 20 条记录:< 0.5 秒
- 总数从缓存表读取:< 1 毫秒 ✅ 后台页面可秒开
5. 可选 Redis 缓存(更快)
如果系统已经使用 Redis,可以直接:
// 写缓存
$count = Db::name('eb_user')->count();
cache('user_total', $count, 300); // 缓存 5 分钟
// 读缓存
$total = cache('user_total');
Redis 读取速度约 0.2ms,比数据库缓存表还快。
四、整体流程图
┌────────────┐
│ 前端请求用户列表 │
└──────┬─────┘
│
▼
┌─────────────────────────────┐
│ 控制器从 sys_stat_cache 取总数 │ ← (0.001s)
│ 并分页查询用户表 + 关联表 │ ← (0.5s)
└─────────────────────────────┘
│
▼
┌──────────────────────────┐
│ 定时任务每5分钟更新缓存 │ ← (后台执行)
└──────────────────────────┘
五、最终效果与性能提升对比
| 操作 | 原始方案 | 优化后 |
|---|---|---|
| 后台列表打开 | 30+ 秒 | < 1 秒 |
| 总数统计 | 实时 COUNT(200w) | 缓存读取 |
| 系统负载 | 高 | 稳定 |
| 可扩展性 | 差 | 优秀,可扩展到千万级 |
可扩展方向(后续升级)
- 用户表分表:按注册时间或地区水平分表。
- 搜索索引引擎:用 Elasticsearch / Sphinx 替代复杂 LIKE 查询。
- 数据异步聚合:用消息队列异步写入统计缓存。
- MySQL 8.0 并行查询优化:利用窗口函数或 JSON 聚合简化多表 JOIN。

