作者 | 微爱帮CTO
日期 | 2025年12月
一、数据库架构概述
1.1 设计原则
安全第一 → 性能稳定 → 易于维护 → 成本合理
1.2 数据库架构图
┌─────────────────────────────────────────────┐
│ 应用服务器集群 │
│ PHP + Nginx (8节点,负载均衡) │
└───────────────┬───────────────┬─────────────┘
│ │
┌───────▼───────┐ ┌─────▼─────┐
│ 主数据库 │ │ 从数据库 │
│ MySQL 8.0 │ │ (读写分离) │
│ 监狱核心数据 │ │ 家属数据 │
└───────┬───────┘ └─────┬─────┘
│ │
┌───────▼───────┐ ┌─────▼─────┐
│ 审计数据库 │ │ 缓存层 │
│ (只写) │ │ Redis集群 │
│ 操作日志 │ │ 高频查询 │
└───────────────┘ └───────────┘
二、核心优化策略
2.1 表设计优化
-- 原始设计 vs 优化设计
-- 原始设计(2025年9月)
CREATE TABLE letters (
id INT AUTO_INCREMENT PRIMARY KEY,
sender_id INT,
receiver_id INT,
content TEXT,
status VARCHAR(20),
created_at TIMESTAMP
);
-- 优化设计(2025年12月)
CREATE TABLE letters (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
sender_id INT UNSIGNED NOT NULL COMMENT '发送者ID',
receiver_id INT UNSIGNED NOT NULL COMMENT '接收者ID',
content MEDIUMTEXT NOT NULL COMMENT '信件内容(加密存储)',
content_hash CHAR(64) NOT NULL COMMENT 'SHA256内容哈希',
prison_code CHAR(10) NOT NULL COMMENT '监狱代码',
letter_type TINYINT NOT NULL DEFAULT 1 COMMENT '1:家属信 2:官方信 3:紧急信',
status TINYINT NOT NULL DEFAULT 0 COMMENT '0:待审核 1:已发送 2:已送达 3:已阅读',
priority TINYINT NOT NULL DEFAULT 1 COMMENT '优先级 1-5',
attachment_count TINYINT NOT NULL DEFAULT 0 COMMENT '附件数量',
word_count INT UNSIGNED NOT NULL COMMENT '字数统计',
encryption_key_id VARCHAR(32) COMMENT '加密密钥ID',
reviewed_by INT UNSIGNED COMMENT '审核人ID',
reviewed_at DATETIME COMMENT '审核时间',
sent_at DATETIME COMMENT '发送时间',
delivered_at DATETIME COMMENT '送达时间',
read_at DATETIME COMMENT '阅读时间',
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
deleted_at DATETIME COMMENT '软删除时间',
INDEX idx_sender_status (sender_id, status),
INDEX idx_receiver_status (receiver_id, status),
INDEX idx_prison_status (prison_code, status, created_at),
INDEX idx_created_status (created_at, status),
UNIQUE INDEX uniq_content_hash (content_hash) COMMENT '防止重复内容'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
COMMENT='信件表'
ROW_FORMAT=COMPRESSED
KEY_BLOCK_SIZE=8;
2.2 分表分库策略
class DatabaseSharding {
/** 基于监狱代码的分表策略 */
private function getTableName($prisonCode, $yearMonth) {
// 格式: letters_{省份缩写}_{年月}
$province = $this->extractProvince($prisonCode);
$table = "letters_{$province}_{$yearMonth}";
// 每月自动创建新表
$this->createTableIfNotExists($table);
return $table;
}
/** 基于用户ID的分库策略 */
private function getDatabaseByUserId($userId) {
// 家属用户和服刑用户分离
$userType = $this->getUserType($userId);
if ($userType === 'FAMILY') {
$shardId = $userId % 4; // 家属库分4个
return "weiai_family_{$shardId}";
} else if ($userType === 'INMATE') {
$prisonCode = $this->getPrisonCode($userId);
$province = $this->extractProvince($prisonCode);
return "weiai_prison_{$province}"; // 按省份分库
}
return "weiai_common";
}
/** 自动创建分表 */
private function createTableIfNotExists($tableName) {
$sql = "CREATE TABLE IF NOT EXISTS {$tableName} LIKE letters_template";
$this->execute($sql);
}
}
三、查询优化实战
3.1 高频查询优化
class QueryOptimizer {
/** 优化前:N+1查询问题 */
public function getInmateLettersBad($inmateId) {
$letters = $this->db->query(
"SELECT * FROM letters WHERE receiver_id = ?",
[$inmateId]
);
foreach ($letters as &$letter) {
// 每次循环都查数据库
$letter['sender_info'] = $this->db->query(
"SELECT name, relation FROM family WHERE id = ?",
[$letter['sender_id']]
);
}
return $letters;
}
/** 优化后:JOIN + 缓存 */
public function getInmateLettersOptimized($inmateId) {
// 使用JOIN一次性获取
$sql = "SELECT
l.*,
f.name as sender_name,
f.relation as sender_relation,
f.mobile as sender_mobile
FROM letters l
LEFT JOIN family f ON l.sender_id = f.id
WHERE l.receiver_id = ?
AND l.status IN (1, 2, 3) -- 只查有效状态
AND l.deleted_at IS NULL
ORDER BY l.priority DESC, l.created_at DESC
LIMIT 100";
$letters = $this->db->query($sql, [$inmateId]);
// 监狱信息批量查询
$prisonCodes = array_unique(array_column($letters, 'prison_code'));
$prisons = $this->batchGetPrisons($prisonCodes);
// 合并结果
foreach ($letters as &$letter) {
$letter['prison_info'] = $prisons[$letter['prison_code']] ?? null;
}
return $letters;
}
/** 批量查询优化 */
private function batchGetPrisons($prisonCodes) {
if (empty($prisonCodes)) return [];
// 使用IN查询而不是循环
$placeholders = implode(',', array_fill(0, count($prisonCodes), '?'));
$sql = "SELECT * FROM prisons WHERE code IN ({$placeholders})";
$result = $this->db->query($sql, $prisonCodes);
// 转换为code为key的数组
return array_column($result, null, 'code');
}
}
3.2 复杂报表查询优化、
-- 月度信件统计报表(优化前)
EXPLAIN SELECT
DATE(created_at) as day,
prison_code,
COUNT(*) as total,
SUM(CASE WHEN status = 1 THEN 1 ELSE 0 END) as sent,
SUM(CASE WHEN status = 2 THEN 1 ELSE 0 END) as delivered,
SUM(CASE WHEN status = 3 THEN 1 ELSE 0 END) as read
FROM letters
WHERE created_at BETWEEN '2025-12-01' AND '2025-12-31'
GROUP BY DATE(created_at), prison_code
ORDER BY day DESC, total DESC;
-- 优化后:使用汇总表
CREATE TABLE letter_daily_stats (
stat_date DATE NOT NULL COMMENT '统计日期',
prison_code CHAR(10) NOT NULL COMMENT '监狱代码',
total_count INT NOT NULL DEFAULT 0 COMMENT '总信件数',
sent_count INT NOT NULL DEFAULT 0 COMMENT '已发送',
delivered_count INT NOT NULL DEFAULT 0 COMMENT '已送达',
read_count INT NOT NULL DEFAULT 0 COMMENT '已阅读',
avg_process_time INT COMMENT '平均处理时间(秒)',
PRIMARY KEY (stat_date, prison_code),
INDEX idx_prison_date (prison_code, stat_date)
) ENGINE=InnoDB COMMENT='信件每日统计表';
-- 定时任务更新汇总表
CREATE EVENT update_letter_stats
ON SCHEDULE EVERY 1 HOUR
DO
BEGIN
INSERT INTO letter_daily_stats (
stat_date, prison_code, total_count,
sent_count, delivered_count, read_count
)
SELECT
DATE(created_at) as stat_date,
prison_code,
COUNT(*) as total_count,
SUM(status = 1) as sent_count,
SUM(status = 2) as delivered_count,
SUM(status = 3) as read_count
FROM letters
WHERE created_at >= DATE_SUB(NOW(), INTERVAL 2 DAY)
AND created_at < CURDATE() -- 只处理昨天之前的数据
GROUP BY stat_date, prison_code
ON DUPLICATE KEY UPDATE
total_count = VALUES(total_count),
sent_count = VALUES(sent_count),
delivered_count = VALUES(delivered_count),
read_count = VALUES(read_count);
END;
四、索引优化策略
4.1 智能索引管理
class IndexManager {
/** 自动分析并创建索引 */
public function optimizeIndexes($table) {
// 1. 分析现有索引
$existingIndexes = $this->getTableIndexes($table);
// 2. 分析慢查询日志
$slowQueries = $this->analyzeSlowLog($table);
// 3. 生成优化建议
$recommendations = [];
foreach ($slowQueries as $query) {
$whereColumns = $this->extractWhereColumns($query);
$orderColumns = $this->extractOrderColumns($query);
// 推荐组合索引
if (!empty($whereColumns)) {
$indexName = "idx_" . implode('_', $whereColumns);
if (!isset($existingIndexes[$indexName])) {
// 添加排序字段
if (!empty($orderColumns)) {
$indexColumns = array_merge($whereColumns, $orderColumns);
} else {
$indexColumns = $whereColumns;
}
$recommendations[] = [
'type' => 'ADD',
'name' => $indexName,
'columns' => $indexColumns,
'estimated_improvement' => $this->estimateImprovement($query)
];
}
}
}
// 4. 删除无用索引
$unusedIndexes = $this->findUnusedIndexes($table);
foreach ($unusedIndexes as $index) {
if ($index['name'] !== 'PRIMARY') {
$recommendations[] = [
'type' => 'DROP',
'name' => $index['name'],
'reason' => '超过30天未使用'
];
}
}
return $recommendations;
}
/** 为监狱信件查询优化的索引策略 */
public function createPrisonLetterIndexes() {
// 高频查询模式1:家属查发给服刑人员的信
$this->db->execute(
"CREATE INDEX idx_family_inmate ON letters (sender_id, receiver_id, created_at DESC)"
);
// 高频查询模式2:按监狱+状态+时间查询
$this->db->execute(
"CREATE INDEX idx_prison_status_time ON letters (prison_code, status, created_at)"
);
// 高频查询模式3:审核队列查询
$this->db->execute(
"CREATE INDEX idx_review_queue ON letters (status, priority DESC, created_at) WHERE status = 0"
);
// 覆盖索引:避免回表
$this->db->execute(
"CREATE INDEX idx_covering_sender ON letters (sender_id, status, created_at) INCLUDE (receiver_id, prison_code, content_hash)"
);
}
}
4.2 分区表策略
-- 按时间范围分区(适用于信件表)
ALTER TABLE letters
PARTITION BY RANGE (YEAR(created_at) * 100 + MONTH(created_at)) (
PARTITION p202509 VALUES LESS THAN (202510),
PARTITION p202510 VALUES LESS THAN (202511),
PARTITION p202511 VALUES LESS THAN (202512),
PARTITION p202512 VALUES LESS THAN (202601),
PARTITION p_future VALUES LESS THAN MAXVALUE
);
-- 按监狱代码哈希分区(适用于监狱关联表)
ALTER TABLE prison_relationships
PARTITION BY HASH(MOD(prison_code, 8))
PARTITIONS 8;
五、缓存策略优化
5.1 多层缓存架构
class CacheManager {
private $redis;
private $localCache = [];
/** 四级缓存策略 */
public function getWithCache($key, $callback, $ttl = 300) {
// 1. PHP进程内存缓存(最快)
if (isset($this->localCache[$key])) {
return $this->localCache[$key];
}
// 2. Redis缓存(分布式)
$cached = $this->redis->get($key);
if ($cached !== false) {
$this->localCache[$key] = $cached;
return $cached;
}
// 3. MySQL查询(带缓存标记)
$lockKey = "lock:{$key}";
if ($this->redis->setnx($lockKey, 1, 5)) { // 获取分布式锁
try {
$data = $callback(); // 执行数据库查询
// 4. 写入缓存
$this->redis->setex($key, $ttl, $data);
$this->localCache[$key] = $data;
return $data;
} finally {
$this->redis->del($lockKey);
}
} else {
// 等待其他进程加载数据
usleep(100000); // 100ms
return $this->getWithCache($key, $callback, $ttl);
}
}
/** 监狱信息缓存策略 */
public function getPrisonInfo($prisonCode) {
$key = "prison:{$prisonCode}";
return $this->getWithCache($key, function() use ($prisonCode) {
// 查询数据库
$sql = "SELECT * FROM prisons WHERE code = ?";
$result = $this->db->query($sql, [$prisonCode]);
if ($result) {
// 关联查询监狱规则
$rules = $this->db->query(
"SELECT * FROM prison_rules WHERE prison_code = ?",
[$prisonCode]
);
$result['rules'] = $rules;
}
return $result;
}, 3600); // 缓存1小时
}
/** 批量缓存预热 */
public function warmUpCache() {
// 预加载热点数据
$hotPrisons = $this->getHotPrisons();
foreach ($hotPrisons as $prison) {
$this->getPrisonInfo($prison['code']);
}
// 预加载家属常用数据
$activeFamilies = $this->getActiveFamilies(1000);
foreach ($activeFamilies as $family) {
$key = "family:{$family['id']}:inmates";
$this->getWithCache($key, function() use ($family) {
return $this->getFamilyInmates($family['id']);
}, 1800);
}
}
}
性能指标与SLA
6.1 性能基准
| 指标 | 目标值 | 监控频率 | 报警阈值 |
|---|---|---|---|
| 查询平均响应时间 | < 50ms | 实时 | > 100ms |
| 写入平均响应时间 | < 100ms | 实时 | > 200ms |
| 连接池使用率 | < 80% | 每分钟 | > 90% |
| 缓存命中率 | > 95% | 每分钟 | < 90% |
| 慢查询数量 | < 10/分钟 | 每分钟 | > 50/分钟 |
| 锁等待时间 | < 1秒 | 实时 | > 5秒 |
6.2 优化成果(实施后对比)
| 优化项 | 优化前 | 优化后 | 提升 |
|---|---|---|---|
| 信件查询平均时间 | 320ms | 45ms | 7.1倍 |
| 监狱信息查询 | 180ms | 缓存命中 2ms | 90倍 |
| 月度统计报表 | 12秒 | 0.8秒 | 15倍 |
| 数据库连接数峰值 | 850 | 120 | 减少86% |
| 存储空间 | 2.1TB | 1.3TB | 节省38% |
七、最佳实践总结
7.1 微爱帮数据库优化黄金法则
-
查询第一原则
先优化查询,再考虑加硬件
-
索引适量原则
每个表索引不超过5个,联合索引不超过4列
-
缓存友好原则
能缓存的尽量缓存,缓存时间业务化
-
分区适时原则
单表超过1000万行考虑分区
-
监控先行原则
没有监控就没有优化
7.2 特殊业务考虑
由于微爱帮业务的特殊性,我们特别注意:
-
监狱数据隔离:不同监狱数据物理隔离
-
审计日志完整:所有操作都有迹可循
-
敏感信息加密:信件内容加密存储
-
合规性优先:优化不得违反监管要求
文档版本 :v2.0
最后更新 :2025年12月
适用环境 :微爱帮生产数据库集群
负责人:数据库团队(
监控面板 :保密
紧急联系人:保密
优化不是一次性的工作,而是持续的实践。在微爱帮,我们相信:
最好的优化,是用户感受不到的流畅体验