perf: V0.9.1-0.9.3 性能优化(count/featured 全表→聚合、请求内缓存、二级索引、PSI 未读铃铛)

- V0.9.1 (P0): Model::count() 改 SELECT COUNT(*);新增请求内 identity-map 缓存;
  Product::featured() 改 WHERE+ LIMIT 避免全表
- V0.9.2 (P1): schema.sql 内联二级索引 + install/upgrades/009_perf_indexes.sql 幂等迁移
- V0.9.3 (P1): psi_unread_events() 改 COUNT + JSON_CONTAINS,消除每页 all()+循环
This commit is contained in:
2026-08-08 19:47:35 +08:00
parent a2dac3b095
commit 4d36a284ff
5 changed files with 193 additions and 25 deletions
+13
View File
@@ -167,6 +167,19 @@ if (!function_exists('site_url')) {
if (!subsys_user('psi')) return 0;
$uid = (int) ($_SESSION['admin_uid'] ?? 0);
if ($uid <= 0) return 0;
// V0.9.3MySQL 下改用 COUNT + JSON_CONTAINS 聚合,避免每个后台页都 SELECT * 全表拉回再循环计数。
// 语义与旧逻辑一致:统计「当前用户不在 read_by 已读列表」的事件数。
if (\Core\Db::driver() === 'mysql') {
try {
return (int) \Core\Db::query(
"SELECT COUNT(*) FROM psi_events WHERE JSON_CONTAINS(COALESCE(read_by,'[]'), CAST(? AS JSON)) = 0",
[$uid]
)->fetchColumn();
} catch (\Throwable $e) {
// 极端情况(如 read_by 含非法 JSON)回退旧逻辑,保证铃铛数字不报错
}
}
// 文件模式 / MySQL 兜底:无 JSON 函数或异常,保留全表循环(数据量小,可接受)
try {
$events = (new \App\Models\PSI\Event())->all();
} catch (\Throwable $e) {
+70 -4
View File
@@ -11,6 +11,28 @@ class Model
protected $pk = 'id';
protected $orderBy = 'id';
/* ---------- 请求内缓存(同一请求内重复读取同一查询只走一次 DB/文件) ---------- */
private static $reqCache = [];
private function cacheKey(string $method, ...$args): string
{
return $this->table . '|' . $method . '|' . md5(serialize($args));
}
/** 读取缓存;未命中返回 null(用 array_key_exists 区分「未缓存」与「缓存了空数组」) */
private function cacheGet(string $k)
{
return array_key_exists($k, self::$reqCache) ? self::$reqCache[$k] : null;
}
private function cachePut(string $k, $v): void
{
self::$reqCache[$k] = $v;
}
/** 任意写操作后清空请求内缓存,避免同请求内读到脏数据(CMS 读多写少,刷新成本可忽略) */
private function cacheFlush(): void
{
self::$reqCache = [];
}
/* ---------- 文件模式 ---------- */
private function file(): string
{
@@ -71,10 +93,16 @@ class Model
/* ---------- 通用 CRUD ---------- */
public function all(): array
{
$k = $this->cacheKey('all');
$hit = $this->cacheGet($k);
if ($hit !== null) return $hit;
if (Db::driver() === 'mysql') {
return Db::query("SELECT * FROM `{$this->table}` ORDER BY `{$this->orderBy}` ASC")->fetchAll();
$rows = Db::query("SELECT * FROM `{$this->table}` ORDER BY `{$this->orderBy}` ASC")->fetchAll();
} else {
$rows = $this->read(); $this->sort($rows);
}
$rows = $this->read(); $this->sort($rows); return $rows;
$this->cachePut($k, $rows);
return $rows;
}
public function find($id)
@@ -106,6 +134,7 @@ class Model
public function insert(array $data)
{
$this->cacheFlush();
if (Db::driver() === 'mysql') {
$cols = array_keys($data);
$sql = "INSERT INTO `{$this->table}` (`" . implode('`,`', $cols) . "`) VALUES (" . implode(',', array_fill(0, count($cols), '?')) . ")";
@@ -121,6 +150,7 @@ class Model
public function update($id, array $data): void
{
$this->cacheFlush();
if (Db::driver() === 'mysql') {
$cols = array_keys($data);
$sql = "UPDATE `{$this->table}` SET `" . implode('`=?,`', $cols) . "`=? WHERE `{$this->pk}`=?";
@@ -136,6 +166,7 @@ class Model
public function delete($id): void
{
$this->cacheFlush();
if (Db::driver() === 'mysql') {
Db::query("DELETE FROM `{$this->table}` WHERE `{$this->pk}`=?", [$id]);
return;
@@ -147,6 +178,7 @@ class Model
/** 按任意列批量删除(用于主从表级联删除从表) */
public function deleteRaw(string $col, $val): void
{
$this->cacheFlush();
if (Db::driver() === 'mysql') {
Db::query("DELETE FROM `{$this->table}` WHERE `{$col}`=?", [$val]);
return;
@@ -155,8 +187,42 @@ class Model
$this->write(array_values($rows));
}
public function count(): int
/**
* 计数:MySQL 下走 COUNT(*) 聚合(避免 SELECT * 全表拉回再 count);
* 可选按某列过滤。文件模式下回退为本地计数。
* @param string|null $col 过滤列(null 表示全表计数)
* @param mixed $val 过滤值
*/
public function count(string $col = null, $val = null): int
{
return count($this->all());
if (Db::driver() === 'mysql') {
if ($col === null) {
return (int) Db::query("SELECT COUNT(*) FROM `{$this->table}`")->fetchColumn();
}
return (int) Db::query("SELECT COUNT(*) FROM `{$this->table}` WHERE `{$col}`=?", [$val])->fetchColumn();
}
$rows = $this->all();
if ($col === null) return count($rows);
$n = 0;
foreach ($rows as $r) {
if (($r[$col] ?? null) == $val) $n++;
}
return $n;
}
/**
* 取某列等于 $val 的前 $limit 条。
* MySQL 下带 LIMIT(索引友好);文件模式用 whereAll + array_slice 兜底。
* 用于首页精选 / 列表分页等高频只读场景。
*/
public function whereLimit(string $col, $val, int $limit): array
{
if (Db::driver() === 'mysql') {
return Db::query(
"SELECT * FROM `{$this->table}` WHERE `{$col}`=? ORDER BY `{$this->orderBy}` ASC LIMIT ?",
[$val, $limit]
)->fetchAll();
}
return array_slice($this->whereAll($col, $val), 0, $limit);
}
}
+3 -1
View File
@@ -14,7 +14,9 @@ class Product extends Model
}
public function featured(int $limit = 6): array
{
return array_slice(array_filter($this->all(), fn($p) => ($p['status'] ?? 1) == 1), 0, $limit);
// V0.9.1:改用 whereLimit('status',1,$limit)MySQL 下走带 LIMIT 的索引查询,
// 不再 SELECT * 全表拉回再 array_filter(原写法每次请求都全表扫描)。
return $this->whereLimit('status', 1, $limit);
}
public function specsArray($p): array
{