增加数据库慢查询观测与索引防线并收窄打手拼单分页查询
This commit is contained in:
@@ -2,12 +2,13 @@ import { Pool } from 'pg'
|
||||
import type { PoolClient, QueryResult, QueryResultRow } from 'pg'
|
||||
|
||||
import { runtimeConfig } from '../config/runtime.js'
|
||||
import { logWarn } from '../utils/logger.js'
|
||||
|
||||
let poolInstance: Pool | null = null
|
||||
|
||||
export function getDb(): Pool {
|
||||
if (!poolInstance) {
|
||||
poolInstance = new Pool({
|
||||
const pool = new Pool({
|
||||
connectionString: String(runtimeConfig.database?.url || '').trim(),
|
||||
ssl: runtimeConfig.database?.ssl ? { rejectUnauthorized: false } : false,
|
||||
max: Number(runtimeConfig.database?.maxConnections || 10),
|
||||
@@ -15,6 +16,12 @@ export function getDb(): Pool {
|
||||
connectionTimeoutMillis: Number(runtimeConfig.database?.connectionTimeoutMs || 5_000),
|
||||
statement_timeout: Number(runtimeConfig.database?.statementTimeoutMs || 15_000),
|
||||
})
|
||||
pool.on('error', (error) => {
|
||||
void logWarn('[db/pool]', '数据库连接池出现空闲连接错误', {
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
})
|
||||
})
|
||||
poolInstance = pool
|
||||
}
|
||||
|
||||
return poolInstance
|
||||
@@ -30,7 +37,40 @@ export async function query<T extends QueryResultRow>(
|
||||
params: unknown[] = [],
|
||||
): Promise<QueryResult<T>> {
|
||||
const pool = getDb()
|
||||
return pool.query<T>(text, params)
|
||||
const startedAt = process.hrtime.bigint()
|
||||
try {
|
||||
const result = await pool.query<T>(text, params)
|
||||
logSlowQuery(text, startedAt, result.rowCount ?? 0)
|
||||
return result
|
||||
} catch (error) {
|
||||
logSlowQuery(text, startedAt, 0, error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
function logSlowQuery(text: string, startedAt: bigint, rowCount: number, error?: unknown) {
|
||||
const thresholdMs = Number(runtimeConfig.database?.slowQueryThresholdMs ?? 500)
|
||||
if (thresholdMs <= 0) return
|
||||
const durationMs = Number(process.hrtime.bigint() - startedAt) / 1_000_000
|
||||
if (durationMs < thresholdMs && !error) return
|
||||
void logWarn('[db/query]', error ? '数据库 SQL 执行失败' : '数据库慢查询', {
|
||||
durationMs: Math.round(durationMs * 100) / 100,
|
||||
thresholdMs,
|
||||
rowCount,
|
||||
sql: fingerprintSql(text),
|
||||
error: error instanceof Error ? error.message : undefined,
|
||||
})
|
||||
}
|
||||
|
||||
/** 只保留可聚合的 SQL 形状,参数值始终不写入日志。 */
|
||||
function fingerprintSql(text: string) {
|
||||
return String(text || '')
|
||||
.replace(/--[^\r\n]*/g, ' ')
|
||||
.replace(/\/\*[\s\S]*?\*\//g, ' ')
|
||||
.replace(/\$\d+/g, '?')
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim()
|
||||
.slice(0, 500)
|
||||
}
|
||||
|
||||
export async function withTransaction<T>(fn: (client: PoolClient) => Promise<T>): Promise<T> {
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
-- 数据库可观测性与高频查询索引保护。
|
||||
-- PostgreSQL 容器通过 shared_preload_libraries 预加载;托管数据库若无权限,跳过扩展创建并由平台侧开启。
|
||||
DO $$
|
||||
BEGIN
|
||||
CREATE EXTENSION IF NOT EXISTS pg_stat_statements;
|
||||
EXCEPTION
|
||||
WHEN insufficient_privilege OR undefined_file THEN
|
||||
RAISE NOTICE '无法创建 pg_stat_statements,请在数据库平台侧启用该扩展';
|
||||
END
|
||||
$$;
|
||||
|
||||
-- 打手订单状态计算中的 pending 撤单/反馈 EXISTS。
|
||||
CREATE INDEX IF NOT EXISTS idx_worker_cancel_requests_pending_worker_order
|
||||
ON worker_order_cancel_requests(worker_id, work_order_id)
|
||||
WHERE status = 'pending';
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_worker_feedbacks_pending_worker_order
|
||||
ON worker_order_feedbacks(worker_id, work_order_id)
|
||||
WHERE status = 'pending';
|
||||
|
||||
-- 工单公共详情和资料更新角标按工单、事件类型取最新事件。
|
||||
CREATE INDEX IF NOT EXISTS idx_work_order_events_order_type_id
|
||||
ON work_order_events(work_order_id, event_type, id DESC);
|
||||
|
||||
-- 排行榜按打手和验收时间筛选;全部周期仍需汇总表或缓存,索引不能替代聚合。
|
||||
CREATE INDEX IF NOT EXISTS idx_work_orders_accepted_worker_at
|
||||
ON work_orders(assigned_worker_id, accepted_at DESC)
|
||||
WHERE status = 'accepted' AND assigned_worker_id IS NOT NULL;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_work_order_shares_accepted_worker_at
|
||||
ON work_order_shares(worker_id, accepted_at DESC)
|
||||
WHERE status = 'accepted';
|
||||
Reference in New Issue
Block a user