智柴网的一次缓存问题排查:Redis `usernames` 键丢失全记录

智柴网的一次缓存问题排查: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 映射缓存");
    
    // ... 清除其他缓存 ...
}

问题分析

  1. 业务逻辑缺陷:管理页面每次访问都清空全量缓存
  2. 过度删除clearAllUserRelatedCache() 删除整个 usernames Hash
  3. 无副作用机制:清空后不会自动重建 usernames
  4. 影响范围过大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:1 user: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 架构层面

  1. CQRS 架构下的缓存策略

- Command(写操作):清空相关缓存 ✅ - Query(读操作):不应清空基础缓存 ❌ - 管理后台是 Query 场景,不应调用 clearAllUserRelatedCache()

  1. 监控与告警缺失

- 没有监控 usernames 的完整率 - 没有告警机制,导致问题持续多天才发现 - 应该添加:if (hlen(usernames) < db_count * 0.9) alert()

  1. 恢复机制缺失

- 没有自动化重建脚本 - 没有管理后台手动重建功能 - 没有预填充逻辑包含用户缓存

9.3 运维层面

  1. 日志的重要性

- 通过 php_errors.log 快速定位到 UserAdminController - 日志显示了问题的频率(多次/小时)和来源

  1. 快速验证脚本的必要性

- 一键诊断工具能快速确认问题范围 - 一键重建工具能快速恢复服务

  1. 文档化的重要性

- 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 作者:智柴网技术团队 状态:已修复 ✅

← 返回目录