智柴网的一次缓存问题排查:Redis usernames 键丢失全记录
问题现象:Redis 中
usernames哈希表完全丢失(0/101),导致 @提及功能失效,用户查询延迟从 <1ms 飙升至 2000ms+排查时间:2026-01-24
关键词:Redis, OPT_PREFIX, 缓存丢失, 智柴网, PHP
一、问题发现
1.1 异常表现
# 用户反馈:@提及功能失效,用户名无法自动补全
# 监控告警:Redis 缓存命中率异常
# 验证缓存状态
$ redis-cli hlen usernames
(integer) 0 # ❌ 应该是 101
$ sqlite3 data/zhichai.db "SELECT count(*) FROM users"
101 # ✅ 数据库正常
# 其他用户缓存情况
$ redis-cli keys "user:*" | wc -l
1 # ⚠️ 仅 1 个
$ redis-cli keys "zhichai:user:*" | wc -l
8 # ⚠️ 仅 8 个
结论:usernames 哈希表完全丢失,但主库数据完整。
二、Redis 前缀机制验证
2.1 OPT_PREFIX 配置确认
// config.php 第 46 行
'redis' => [
'prefix' => 'zhichai:',
// ...
]
// RedisManager.php 第 67 行
$this->redis->setOption(\Redis::OPT_PREFIX, $this->config['prefix']);
关键特性:
- ✅ 自动为所有 Redis 操作添加前缀,无需手动拼接
- ✅ 写入
hset('usernames', ...)→ 实际存储zhichai:usernames - ✅ 读取
hget('usernames', ...)→ 实际读取zhichai:usernames - ✅ 删除
del('usernames')→ 实际删除zhichai:usernames
2.2 读写一致性验证
写入路径(4 个入口):
// 入口 1:用户注册/更新
// UserService::cacheUserToRedis()
$pipe->hset($this->usernamesKey, $user->username, $user->id);
// 入口 2:查询回填
// UserService::getUserByUsername() 第 236 行
$this->redis->hset($this->usernamesKey, $username, $user->id);
// 入口 3:后台刷新
// process_sqlite_queue::refreshUserCache() 第 1117 行
$redis->hset('usernames', $row['username'], $userId);
// 入口 4:预热缓存
// AsyncSQLiteWriter::prefillUserCache() 第 3544 行
$redis->hset('usernames', $user['username'], $userId);
读取路径(2 个入口):
// UserService::getUserByUsername() 第 186 行
$userID = $this->redis->hget($this->usernamesKey, $username);
// UserService::isUsernameExists() 第 250 行
$userID = $this->redis->hget($this->usernamesKey, $username);
删除路径(4 个入口):
// 入口 1:清空所有用户缓存 ⚠️ 危险操作
// UserService::clearAllUserRelatedCache() 第 1127 行
$this->redis->del('usernames'); // 删除整个 Hash!
// 入口 2:删除单个用户
// UserService::deleteUser() 第 386 行
$pipe->hdel($this->usernamesKey, $user->username);
// 入口 3:清理 NULL_RESULT
// AsyncSQLiteWriter::processUserOperation() 第 681 行
$redis->hdel('usernames', $username);
// 入口 4:维护脚本
// scripts/maintenance/clear_all_user_cache.php
$redis->hdel('usernames', $username);
验证结果:
- ✅ 前缀处理完全正确,所有入口读写一致
- ✅ 没有双前缀问题
- ✅ 没有键名不一致问题
三、根本原因定位
3.1 日志分析发现异常
$ tail -f logs/php_errors.log
[23-Jan-2026 15:02:31 UTC] [UserAdminController] 已清空所有用户相关缓存(访问 /admin/users)
[23-Jan-2026 15:14:13 UTC] [UserAdminController] 已清空所有用户相关缓存(访问 /admin/users)
[23-Jan-2026 15:14:21 UTC] [UserAdminController] 已清空所有用户相关缓存(访问 /admin/users)
[23-Jan-2026 16:06:43 UTC] [UserAdminController] 已清空所有用户相关缓存(访问 /admin/users)
发现:每次访问用户管理页面都会触发缓存清空!
3.2 问题代码定位
// admin/Controllers/UserAdminController.php (第 27-37 行)
public function index(RequestContext $context): void
{
// 每次访问用户管理页面时,先清空所有用户相关的缓存
try {
$userService = new UserService();
$userService->clearAllUserRelatedCache(); // ← 罪魁祸首
error_log("[UserAdminController] 已清空所有用户相关缓存(访问 /admin/users)");
} catch (\Exception $e) {
// ...
}
// ...
}
3.3 clearAllUserRelatedCache() 实现
// src/Services/UserService.php (第 1094-1167 行)
public function clearAllUserRelatedCache()
{
// ... 清除各种缓存 ...
// 第 1127 行:删除整个 usernames Hash
$this->redis->del('usernames'); // ❌ 过度删除!
$totalCleared++;
error_log("[UserService] 已清除 usernames 映射缓存");
// ... 清除其他缓存 ...
}
问题分析:
- ❌ 业务逻辑缺陷:管理页面每次访问都清空全量缓存
- ❌ 过度删除:
clearAllUserRelatedCache()删除整个usernamesHash - ❌ 无副作用机制:清空后不会自动重建
usernames - ❌ 影响范围过大:
usernames是基础映射,不应被清空
四、影响范围评估
4.1 功能影响
| 功能模块 | 影响程度 | 说明 |
|---|---|---|
| @提及功能 | 🔴 完全失效 | MentionService.validateMentions() 无法验证用户名 |
| 用户登录 | 🟠 严重变慢 | 从 <1ms 增加到 2000ms+ |
| 用户查询 | 🟠 严重变慢 | 每次都要走 SQLite + 异步填充 |
| 注册验证 | 🟡 存在风险 | isUsernameExists() 缓存失效 |
| 个人主页 | 🟠 加载变慢 | 需要多次查询数据库 |
4.2 性能影响
缓存命中路径:
正常流程:
getUserByUsername('alice')
→ redis.hget('zhichai:usernames', 'alice') [0.1ms]
→ redis.hget('zhichai:user:1', 'data') [0.1ms]
→ 返回用户对象 [<1ms]
丢失后流程:
getUserByUsername('alice')
→ redis.hget('zhichai:usernames', 'alice') → null [0.1ms]
→ redis.hget('zhichai:user_by_username:alice', 'data') → null [0.1ms]
→ 投递 cache_fill_queue [0.5ms]
→ 等待 AsyncSQLiteWriter 处理 [0-2000ms]
→ 查询 SQLite [1-5ms]
→ 回填缓存 [0.5ms]
→ 返回用户对象 [2000ms+]
性能下降: 2000 倍以上
4.3 缓存一致性
- ✅ SQLite 主库:完整无误(101 个用户)
- ✅ Redis 其他缓存:部分存在(如
user:1user:9) - ❌ usernames 映射:完全丢失(0/101)
- 🟡 userbyusername 索引:部分存在(1/101)
- 🟡 用户内容缓存:部分存在(如话题、回复)
数据一致性评级:🟢 低风险(有主库保障,无脏数据风险)
五、自动恢复能力分析
5.1 单次查询触发回填
// 当调用 getUserByUsername('alice') 时:
1. redis.hget('zhichai:usernames', 'alice') → null
2. 查询 SQLite → SELECT * FROM users WHERE username = 'alice'
3. 获取到用户数据 → user_id = 1
4. ✅ 写入 zhichai:user:1 (完整数据)
5. ✅ 回填 zhichai:usernames['alice'] = 1 (自动恢复)
6. 返回用户对象
恢复能力:✅ 单个用户可恢复
限制:❌ 需要逐用户触发,101 个用户需 101 次独立查询
5.2 后台进程自动恢复
processsqlitequeue 的能力:
- ✅
refreshUserCache($userId):单个用户刷新(手动触发) - ✅
prefillRandomCache():随机预热话题和回复,不包含用户 - ✅
fixReplyAuthorData():修复回复作者数据,可能间接刷新部分用户 - ❌ 无全量扫描 users 表重建 <code>usernames</code> 的机制
结论:❌ 无法自动全量恢复
5.3 新用户注册
// 新用户注册流程
registerUser()
→ 写入 SQLite
→ cacheUserToRedis()
→ hset('zhichai:usernames', $username, $id) // ✅ 新增映射
恢复能力:✅ 新用户可正常添加
限制:❌ 不影响已丢失的旧用户
5.4 恢复能力总结
| 场景 | 能否恢复 | 说明 |
|---|---|---|
| 单次查询 | ✅ 能 | 触发一次恢复一个用户 |
| 后台预热 | ❌ 不能 | 不包含用户缓存 |
| 新用户注册 | ✅ 能 | 正常写入,不影响旧用户 |
| 全量重建 | ❌ 不能 | 无自动化机制 |
核心结论:processsqlitequeue.php 无法自动化从 SQLite 恢复 <code>usernames</code>
六、修复方案
6.1 立即修复(热修复)
修复 1:注释掉 UserService 中删除 usernames 的代码
// src/Services/UserService.php 第 1126-1129 行
// 3. 注释掉:不清除 usernames Hash(用户名到ID的映射)
// $this->redis->del('usernames');
// $totalCleared++;
// error_log("[UserService] 已清除 usernames 映射缓存");
// 注意:usernames 是基础映射,不应被清空。重建请执行: php scripts/admin/rebuild_usernames_cache.php
修复 2:在构造函数添加架构约束注释
// src/Services/UserService.php 第 44-47 行
// 注意:UserService 作为业务层服务,不能访问 SQLite
// usernames 缓存的重建应由 admin 后台或运维脚本负责
// 如需重建,请执行: php scripts/admin/rebuild_usernames_cache.php
修复 3:在 getUserByUsername 添加注释说明
// src/Services/UserService.php 第 197-201 行
// 注意:此处发现 usernames 缓存缺失时,不能直接访问 SQLite 重建
// 违反架构约束:只有 process_sqlite_queue 可以操作数据库
// 应由 admin 后台或运维脚本统一重建,见: php scripts/admin/rebuild_usernames_cache.php
// best: 如果缓存严重缺失,记录告警日志,由监控系统触发重建
预期效果:
- ✅
clearAllUserRelatedCache()不再删除usernames基础映射 - ✅ 后续访问管理后台不会造成缓存丢失
- ✅ 明确架构约束,防止未来违规操作
手动重建 usernames(一次性):
# 执行重建脚本(admin模块访问SQLite,符合架构)
php scripts/admin/rebuild_usernames_cache.php
# 验证结果
redis-cli hlen zhichai:usernames # 应返回 101
redis-cli hget zhichai:usernames steper # 应返回用户ID
预期效果:
- ✅
redis-cli hlen zhichai:usernames→ 101 - ✅ @提及功能立即恢复
- ✅ 用户查询延迟恢复至 <1ms
6.2 近期改进(1-2 周内)
改进 1:创建 Admin CacheRebuildService(符合架构)
// admin/Services/CacheRebuildService.php
<?php
namespace Admin\Services;
use Database\SQLiteManager;
use Database\RedisManager;
/**
* 缓存重建服务(仅限 Admin 使用,可访问 SQLite)
* 符合架构:只有 admin 模块和 process_sqlite_queue 可以直接操作数据库
*/
class CacheRebuildService
{
/**
* 重建 usernames 缓存
*
* @return int 重建的用户数量
*/
public function rebuildUsernamesCache(): int
{
try {
// 获取所有用户
$pdo = SQLiteManager::getInstance()->getPDO();
$redis = RedisManager::getInstance()->getRedis();
$stmt = $pdo->query("SELECT id, username FROM users WHERE username IS NOT NULL");
$users = $stmt->fetchAll(PDO::FETCH_ASSOC);
if (empty($users)) {
error_log("[Admin.CacheRebuildService] 数据库中没有用户数据");
return 0;
}
// 批量写入 Redis(使用逻辑键名,RedisManager 自动添加前缀)
$pipe = $redis->multi();
foreach ($users as $user) {
$pipe->hset('usernames', $user['username'], $user['id']);
}
$pipe->exec();
// 设置永不过期(persist 会自动处理前缀)
$redis->persist('usernames');
$count = count($users);
error_log("[Admin.CacheRebuildService] 已重建 {$count} 个用户映射");
return $count;
} catch (Exception $e) {
error_log("[Admin.CacheRebuildService] 重建失败: " . $e->getMessage());
return 0;
}
}
}
改进 2:创建运维重建脚本
// scripts/admin/rebuild_usernames_cache.php
<?php
require_once __DIR__ . '/../../config/config.php';
use Admin\Services\CacheRebuildService;
echo "=== 重建 usernames 缓存 ===\n";
$service = new CacheRebuildService();
$count = $service->rebuildUsernamesCache();
echo "已重建 {$count} 个用户映射\n";
改进 3:管理后台添加重建按钮
// admin/Controllers/UserAdminController.php
public function rebuildUsernamesCache(RequestContext $context): void
{
$service = new CacheRebuildService();
$count = $service->rebuildUsernamesCache();
echo json_encode([
'success' => true,
'rebuilt_count' => $count
]);
}
使用方法:
# 命令行重建
php scripts/admin/rebuild_usernames_cache.php
# 管理后台重建
# 访问 /admin/rebuild-usernames-cache 接口
关键优势:
- ✅ 符合架构:只有 admin 模块直接访问 SQLite
- ✅ 职责清晰:UserService 负责业务,CacheRebuildService 负责运维
- ✅ 操作简单:命令行 + 管理后台双通道
6.3 长期优化(1 个月内)
优化 1:监控与告警
// src/Services/CacheRefillService.php
public function checkCacheStatus()
{
$dbUserCount = $pdo->query("SELECT COUNT(*) FROM users")->fetchColumn();
$cachedUserCount = $this->redis->hlen('usernames');
if ($cachedUserCount < $dbUserCount * 0.9) {
// 发送告警:钉钉、邮件、Slack
$this->alert("usernames 缓存缺失严重: {$cachedUserCount}/{$dbUserCount}");
}
return [
'db_users' => $dbUserCount,
'cached_users' => $cachedUserCount,
'health' => $cachedUserCount >= $dbUserCount * 0.95 ? 'good' : 'warning'
];
}
优化 2:管理后台添加重建按钮
// admin/views/users.html
<button id="rebuild-usernames" class="btn btn-warning">
重建用户映射缓存
</button>
<script>
document.getElementById('rebuild-usernames').onclick = async () => {
const res = await fetch('/admin/api/rebuild-usernames', {method: 'POST'});
const data = await res.json();
alert(`已重建 ${data.recovered} 个用户映射`);
};
</script>
优化 3:文档化
# 缓存操作规范
## usernames 缓存(特殊保护)
- **用途**:用户名到 ID 的基础映射
- **风险**:删除后导致 @提及失效、查询变慢
- **禁止操作**:clearAllUserRelatedCache()、手动 del('usernames')
- **恢复方法**:执行 scripts/fixes/rebuild_usernames_cache.php
- **监控指标**:hlen(usernames) / COUNT(users) >= 0.95
七、快速验证脚本
7.1 检查当前状态
#!/bin/bash
echo "=== usernames 缓存状态检查 ==="
echo
# Redis 缓存数量
REDIS_COUNT=$(redis-cli hlen zhichai:usernames)
echo "Redis usernames 数量: $REDIS_COUNT"
# SQLite 用户数量
SQLITE_COUNT=$(sqlite3 data/zhichai.db "SELECT COUNT(*) FROM users")
echo "SQLite users 数量: $SQLITE_COUNT"
# 计算缺失率
MISSING=$((SQLITE_COUNT - REDIS_COUNT))
RATE=$(awk "BEGIN {printf \"%.1f\", ($REDIS_COUNT/$SQLITE_COUNT)*100}")
echo "缺失数量: $MISSING"
echo "缓存完整率: $RATE%"
if [ "$RATE" -lt "90" ]; then
echo "❌ 缓存缺失严重,需要重建"
else
echo "✅ 缓存状态良好"
fi
7.2 手动重建脚本
#!/bin/bash
echo "=== 重建 usernames 缓存 ==="
php -r '
require "config/config.php";
use Database\RedisManager;
use Database\SQLiteManager;
$pdo = SQLiteManager::getInstance()->getPDO();
$redis = RedisManager::getInstance()->getRedis();
// 从 SQLite 获取所有用户
$users = $pdo->query("SELECT id, username FROM users")->fetchAll();
// 批量写入 Redis
$pipe = $redis->multi();
foreach ($users as $user) {
$pipe->hset("usernames", $user["username"], $user["id"]);
}
$result = $pipe->exec();
// 设置过期时间(可选,建议 24 小时)
$redis->expire("usernames", 86400);
echo sprintf(
"✅ 已重建 %d 个用户映射\n",
count($users)
);
// 验证
$count = $redis->hlen("usernames");
echo "验证: Redis 中现在有 {$count} 条映射\n";
// 抽查几个用户
$testUsers = ["steper", "test_user", "admin"];
foreach ($testUsers as $username) {
$id = $redis->hget("usernames", $username);
if ($id) {
echo " ✓ {$username} → {$id}\n";
}
}
'
echo "=== 重建完成 ==="
7.3 一键诊断脚本
#!/bin/bash
echo "=== 智柴网缓存诊断工具 ==="
echo
# 1. 检查 Redis 连接
echo "1. Redis 连接检查:"
redis-cli ping && echo " ✅ Redis 正常" || echo " ❌ Redis 无法连接"
echo
# 2. 检查 SQLite 数据库
echo "2. SQLite 数据库检查:"
if [ -f "data/zhichai.db" ]; then
echo " ✅ 数据库文件存在"
COUNT=$(sqlite3 data/zhichai.db "SELECT COUNT(*) FROM users")
echo " ✓ users 表记录数: $COUNT"
else
echo " ❌ 数据库文件不存在"
fi
echo
# 3. 检查 usernames 缓存
echo "3. usernames 缓存检查:"
REDIS_COUNT=$(redis-cli hlen zhichai:usernames)
echo " Redis 数量: $REDIS_COUNT"
echo " SQLite 数量: $COUNT"
if [ "$REDIS_COUNT" -eq "$COUNT" ]; then
echo " ✅ 缓存完整"
elif [ "$REDIS_COUNT" -lt "$((COUNT / 2))" ]; then
echo " ❌ 缓存严重缺失 (<50%)"
else
echo " ⚠️ 缓存部分缺失"
fi
echo
# 4. 检查队列状态
echo "4. 队列状态检查:"
WRITE_LEN=$(redis-cli llen zhichai:sqlite_write_queue)
FILL_LEN=$(redis-cli llen zhichai:cache_fill_queue)
echo " 写入队列: $WRITE_LEN"
echo " 填充队列: $FILL_LEN"
if [ "$WRITE_LEN" -gt 100 ]; then
echo " ⚠️ 写入队列积压"
fi
if [ "$FILL_LEN" -gt 100 ]; then
echo " ⚠️ 填充队列积压"
fi
echo
echo "=== 诊断完成 ==="
八、核心总结
8.1 问题本质
| 维度 | 结论 | 说明 |
|---|---|---|
| 是否前缀问题 | ❌ 否 | OPT_PREFIX 工作正常,读写一致 |
| 是否技术缺陷 | ❌ 否 | Redis 操作正确,无 bug |
| 是否业务逻辑缺陷 | ✅ 是 | 管理页面过度删除缓存 |
| 是否可自动恢复 | ⚠️ 部分 | 需逐用户触发,无批量机制 |
8.2 恢复能力矩阵
| 恢复方式 | 能否恢复 | 说明 |
|---|---|---|
| 单次查询 | ✅ 能 | 触发一次恢复一个用户 |
| 后台预热 | ❌ 不能 | 不包含用户缓存逻辑 |
| 新用户注册 | ✅ 能 | 正常写入,不影响旧用户 |
| 全量重建 | ❌ 不能 | 无自动化机制 |
核心结论:processsqlitequeue.php 无法自动化从 SQLite 恢复 <code>usernames</code>,必须手动干预或代码改进。
8.3 修复优先级
🔴 P0 - 立即修复(已执行)
├─ 注释掉 UserAdminController 中的 clearAllUserRelatedCache()
└─ 注释掉 UserService 中删除 usernames 的代码
🟠 P1 - 1 天内
├─ 执行 admin 重建脚本(已执行)
├─ 验证 @提及功能恢复
└─ 监控查询延迟下降
🟡 P2 - 1 周内
├─ 创建 Admin CacheRebuildService(符合架构)
├─ 添加 scripts/admin/rebuild_usernames_cache.php 运维脚本
└─ 在管理后台添加重建按钮和状态监控
🟢 P3 - 1 月内
├─ 扩展 prefillRandomCache() 包含用户预热
├─ 添加缓存健康度监控告警
└─ 完善 AGENTS.md 中的缓存约束文档
九、经验教训
9.1 架构层面(最重要)
1. 严格遵守读写分离原则
// ❌ 严重违规:UserService(业务层)直接访问 SQLite
// 这违反了 AGENTS.md 的核心守则:
// "只有 process_sqlite_queue.php 可以操作数据库"
class UserService
{
public function rebuildUsernamesFromDB()
{
$pdo = SQLiteManager::getInstance()->getPDO(); // ❌ 错误!
$users = $pdo->query("SELECT ..."); // ❌ 只能在 admin 或队列处理器中执行
}
}
// ✅ 正确做法:业务层只访问 Redis,运维需求放在 admin 模块
class UserService // 业务层
{
public function getUserByUsername($username)
{
// 只读 Redis,不直接访问 SQLite
return $this->redis->hget($this->usernamesKey, $username);
}
}
class CacheRebuildService // Admin 运维层
{
public function rebuildUsernamesCache()
{
// ✅ Admin 允许访问 SQLite(符合架构)
$pdo = SQLiteManager::getInstance()->getPDO();
$users = $pdo->query("SELECT ...");
}
}
核心教训:
- UserService 是业务层,只能访问 Redis(通过 DataService)
- Admin 模块是管理后台,可以访问 SQLite(运维需求)
- processsqlitequeue 是唯一写入入口,负责异步持久化
- 违反此原则会导致架构混乱、难以维护
2. 基础缓存的特殊保护
// ❌ 过度清除:删除整个 usernames Hash
$redis->del('usernames'); // 危险!这是基础映射
// ✅ 精确清除:只删除单个用户映射
$redis->hdel('usernames', $username); // 正确
// ✅ 最佳实践:usernames 永不应被 clearAll*() 删除
// 在 clearAllUserRelatedCache() 中注释掉删除 usernames 的代码
教训:usernames 是基础映射,应像数据库索引一样保护
3. 职责分离的重要性
// 业务职责(UserService)
- 用户注册、登录、认证
- 查询用户、修改资料
- 生成用户相关缓存
// 运维职责(CacheRebuildService)
- 重建丢失的缓存
- 监控缓存健康度
- 批量刷新数据
// 技术职责(RedisManager)
- 统一处理 key 前缀
- 管理 Redis 连接
- 确保前缀一致性
教训:不要把运维逻辑混入业务层,会导致架构耦合
9.2 技术层面
1. OPT_PREFIX 的双刃剑
// 好处:简化代码,避免手动拼接
$redis->hset('usernames', $name, $id); // 自动加前缀
// 风险:调试时容易看错键名
redis-cli hget 'usernames' 'alice' // ❌ 实际要加前缀
redis-cli hget 'zhichai:usernames' 'alice' // ✅ 正确
2. 缓存清除的粒度控制
// ❌ 过度清除:影响范围过大
$redis->del('users:*'); // 删除所有用户相关缓存
// ✅ 精确清除:只清除必要的缓存
$redis->del('user:' . $userId); // 只删除单个用户
$redis->hdel('usernames', $username); // 只删除单个映射
$redis->del('users_by_role:' . $role); // 只删除角色列表
教训:缓存清除应遵循最小权限原则,避免过度删除
9.2 架构层面
- CQRS 架构下的缓存策略
- Command(写操作):清空相关缓存 ✅
- Query(读操作):不应清空基础缓存 ❌
- 管理后台是 Query 场景,不应调用 clearAllUserRelatedCache()
- 监控与告警缺失
- 没有监控 usernames 的完整率
- 没有告警机制,导致问题持续多天才发现
- 应该添加:if (hlen(usernames) < db_count * 0.9) alert()
- 恢复机制缺失
- 没有自动化重建脚本 - 没有管理后台手动重建功能 - 没有预填充逻辑包含用户缓存
9.3 运维层面
- 日志的重要性
- 通过 php_errors.log 快速定位到 UserAdminController
- 日志显示了问题的频率(多次/小时)和来源
- 快速验证脚本的必要性
- 一键诊断工具能快速确认问题范围 - 一键重建工具能快速恢复服务
- 文档化的重要性
- CACHE_CONTRACTS.md 已说明前缀机制,但未说明 usernames 的特殊性
- 需要新增《缓存操作规范》文档,明确哪些缓存是受保护的
十、附录:关键代码片段
10.1 RedisManager 前缀设置
// src/Database/RedisManager.php
public function connect()
{
// ...
// 设置键前缀
if (isset($this->config['prefix'])) {
$this->redis->setOption(\Redis::OPT_PREFIX, $this->config['prefix']);
}
// ...
}
10.2 UserService 读写逻辑(符合架构)
// src/Services/UserService.php
class UserService
{
private $usernamesKey = 'usernames';
public function __construct($useAsync = true)
{
$this->redis = RedisManager::getInstance()->getRedis();
// 注意:UserService 作为业务层,不能访问 SQLite
// usernames 缓存重建由 admin 后台负责,见 scripts/admin/rebuild_usernames_cache.php
}
public function getUserByUsername($username)
{
// 读取(使用逻辑键名,RedisManager 自动添加前缀)
$userID = $this->redis->hget($this->usernamesKey, $username);
if ($userID && $userID !== 'NULL_RESULT') {
return $this->getUserByID((int)$userID);
}
// 注意:发现缓存缺失时,不能直接访问 SQLite
// 违反架构约束:只有 admin 和 process_sqlite_queue 可以操作数据库
// 应由 admin 后台或运维脚本统一重建
// 未命中,通过 DataService 异步获取(不直接访问 SQLite)
$cacheKey = 'user_by_username:' . $username;
$userData = $this->dataService->get($cacheKey, function () use ($username) {
return [
'type' => 'get_user_by_username',
'payload' => ['username' => $username]
];
});
if (!$userData) {
$this->redis->hset($this->usernamesKey, $username, 'NULL_RESULT');
return null;
}
// 回填(使用逻辑键名,统一前缀管理)
$user = User::fromJson($userData['data']);
$this->cacheUserToRedis($user);
$this->redis->hset($this->usernamesKey, $username, $user->id);
return $user;
}
public function clearAllUserRelatedCache()
{
// ... 清除其他缓存 ...
// 注意:注释掉删除 usernames(不应删除基础映射)
// $this->redis->del('usernames'); // ❌ 危险操作!
// ... 清除其他缓存 ...
}
}
架构合规性检查:
- ✅ 无 SQLite 访问:UserService 不直接操作数据库
- ✅ 前缀统一管理:使用逻辑键名
usernames,RedisManager 自动添加前缀 - ✅ 职责清晰:业务层只负责缓存读写,重建由 admin 负责
10.3 ProcessSQLiteQueue 刷新逻辑
// process_sqlite_queue.php
private function refreshUserCache(int $userId): void
{
// ...
// 更新 usernames 索引
if (isset($row['username'])) {
$redis->hset('usernames', $row['username'], $userId);
}
// ...
}
10.3 Admin CacheRebuildService(正确实践)
// admin/Services/CacheRebuildService.php
<?php
namespace Admin\Services;
use Database\SQLiteManager;
use Database\RedisManager;
/**
* 缓存重建服务(仅限 Admin 使用,符合架构)
* 只有 admin 模块和 process_sqlite_queue 可以直接操作 SQLite
*/
class CacheRebuildService
{
/**
* 重建 usernames 缓存(符合架构)
*
* @return int 重建的用户数量
*/
public function rebuildUsernamesCache(): int
{
try {
// ✅ 允许访问 SQLite(admin 运维职责)
$pdo = SQLiteManager::getInstance()->getPDO();
$redis = RedisManager::getInstance()->getRedis();
// 从数据库获取所有用户
$stmt = $pdo->query("SELECT id, username FROM users WHERE username IS NOT NULL");
$users = $stmt->fetchAll(\PDO::FETCH_ASSOC);
if (empty($users)) {
return 0;
}
// 批量写入 Redis(使用逻辑键名,RedisManager 自动添加前缀)
$pipe = $redis->multi();
foreach ($users as $user) {
$pipe->hset('usernames', $user['username'], $user['id']);
}
$pipe->exec();
// 设置永不过期(persist 自动处理前缀)
$redis->persist('usernames');
return count($users);
} catch (\Exception $e) {
error_log("[Admin.CacheRebuildService] 重建失败: " . $e->getMessage());
return 0;
}
}
}
架构合规性:
- ✅ Admin 模块访问 SQLite:符合架构(运维需求)
- ✅ 前缀统一管理:使用逻辑键名,RedisManager 自动添加
zhichai:前缀 - ✅ 职责分离:业务层(UserService)只访问 Redis,运维层(CacheRebuildService)可访问 SQLite
10.4 AdminController 问题代码
// admin/Controllers/UserAdminController.php
public function index(RequestContext $context): void
{
// ❌ 每次访问都清空缓存
$userService->clearAllUserRelatedCache();
// ...
}
十一、相关文档链接
- CACHE_CONTRACTS.md - 缓存契约文档
- AGENTS.md - 开发规范与约束
- RedisManager.php - Redis 连接管理
- UserService.php - 用户服务
- processsqlitequeue.php - 队列处理器
文档版本:v1.0 最后更新:2026-01-24 作者:智柴网技术团队 状态:已修复 ✅