我們可以通過 分頁 + 緩存 + 異步統(tǒng)計(jì) 來完美解決。
非常好 ?? 你這個(gè)需求(200w 用戶列表頁面卡頓)是大多數(shù)中大型系統(tǒng)后臺的通病,我們可以通過 分頁 + 緩存 + 異步統(tǒng)計(jì) 來完美解決。
下面我給出完整方案,分為 架構(gòu)思路 → SQL 優(yōu)化 → 緩存方案 → ThinkPHP 示例代碼 四個(gè)部分,拿去直接可落地用。??
一、架構(gòu)思路(核心目標(biāo))
| 模塊 | 優(yōu)化目標(biāo) | 說明 |
|---|---|---|
| 分頁查詢 | 秒級打開 | 只查詢 20 條數(shù)據(jù),不做復(fù)雜統(tǒng)計(jì) |
| 總數(shù)統(tǒng)計(jì) | 緩存或異步更新 | 避免每次執(zhí)行 COUNT(200w) |
| 搜索 / 篩選 | 可索引字段查詢 | 避免模糊 LIKE + JOIN |
| JOIN 數(shù)據(jù) | 最小化 | 盡量只在必要字段時(shí)再 JOIN |
二、數(shù)據(jù)庫設(shè)計(jì)與索引優(yōu)化
1?? 關(guān)鍵索引
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 用的字段必須有索引。
三、分頁 + 異步統(tǒng)計(jì)方案
方案目標(biāo)
- 列表頁加載快(1 秒內(nèi))
- 總數(shù)、搜索、統(tǒng)計(jì)延遲可接受(幾分鐘內(nèi)更新)
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;
- 避免在分頁時(shí)使用 COUNT(*)。
- 用索引字段排序(created_at 或 uid)。
2. 用戶總數(shù)統(tǒng)計(jì)緩存表
新建一個(gè)緩存表:
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. 定時(shí)任務(wù)更新用戶總數(shù)
用一個(gè)后端定時(shí)腳本(如 ThinkPHP 命令行任務(wù)):
<?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('更新用戶總數(shù)緩存');
}
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}");
}
}
建議執(zhí)行周期:
- 每 5 分鐘一次(用 crontab 或隊(duì)列觸發(fā))
- 對 200w 用戶表,這種 COUNT 操作耗時(shí) 2~5 秒,完全可以后臺運(yùn)行。
4. 后臺控制器讀取緩存統(tǒng)計(jì)
// app/admin/controller/UserController.php
use think\facade\Db;
public function index()
{
// 分頁參數(shù)
$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();
// 從緩存表讀取總數(shù)
$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 秒
- 總數(shù)從緩存表讀?。?lt; 1 毫秒 ? 后臺頁面可秒開
5. 可選 Redis 緩存(更快)
如果系統(tǒng)已經(jīng)使用 Redis,可以直接:
// 寫緩存
$count = Db::name('eb_user')->count();
cache('user_total', $count, 300); // 緩存 5 分鐘
// 讀緩存
$total = cache('user_total');
Redis 讀取速度約 0.2ms,比數(shù)據(jù)庫緩存表還快。
四、整體流程圖
┌────────────┐
│ 前端請求用戶列表 │
└──────┬─────┘
│
▼
┌─────────────────────────────┐
│ 控制器從 sys_stat_cache 取總數(shù) │ ← (0.001s)
│ 并分頁查詢用戶表 + 關(guān)聯(lián)表 │ ← (0.5s)
└─────────────────────────────┘
│
▼
┌──────────────────────────┐
│ 定時(shí)任務(wù)每5分鐘更新緩存 │ ← (后臺執(zhí)行)
└──────────────────────────┘
五、最終效果與性能提升對比
| 操作 | 原始方案 | 優(yōu)化后 |
|---|---|---|
| 后臺列表打開 | 30+ 秒 | < 1 秒 |
| 總數(shù)統(tǒng)計(jì) | 實(shí)時(shí) COUNT(200w) | 緩存讀取 |
| 系統(tǒng)負(fù)載 | 高 | 穩(wěn)定 |
| 可擴(kuò)展性 | 差 | 優(yōu)秀,可擴(kuò)展到千萬級 |
可擴(kuò)展方向(后續(xù)升級)
- 用戶表分表:按注冊時(shí)間或地區(qū)水平分表。
- 搜索索引引擎:用 Elasticsearch / Sphinx 替代復(fù)雜 LIKE 查詢。
- 數(shù)據(jù)異步聚合:用消息隊(duì)列異步寫入統(tǒng)計(jì)緩存。
- MySQL 8.0 并行查詢優(yōu)化:利用窗口函數(shù)或 JSON 聚合簡化多表 JOIN。

