設計目標
- 統一報表資料快取存取介面
- 支援同步/非同步讀取 (阻塞等待快取生成)
- 自動過期、版本控制、資料實體封裝
- 鍵值命名規範化 (報表名 + 用戶標識)
核心類別架構
<?php
/**
* 入口門面:靜態方法快速存取
*/
class Rpt {
public static function read(string $key, string $nick): mixed {
return self::createFinder($key, $nick)->read();
}
public static function readSync(string $key, string $nick): mixed {
return self::createFinder($key, $nick)->readSync();
}
public static function save(string $key, string $nick, mixed $data, int $expired = 7200): bool {
return self::createStorage($key, $nick)->save($data, $expired);
}
public static function createFinder(string $key, string $nick): RptFinder {
return new RptFinder(RptGenerate::key($key, $nick));
}
public static function createStorage(string $key, string $nick): RptStorage {
return new RptStorage(RptGenerate::key($key, $nick));
}
private static function createRedis(): Redis {
$redis = new Redis();
$redis->connect(Yii::app()->params['RedisServerIP']); // 需根據實際框架調整
return $redis;
}
}
/**
* 鍵值生成器:統一命名規範
*/
class RptGenerate {
public static function key(string $key, string $nick): string {
return $key . '.' . md5($nick); // 如: diamond.account.a1b2c3d4...
}
}
/**
* 資料實體:封裝快取元資料
*/
class RptData {
public int $expired = 0; // TTL 秒數
public int $expiredTime = 0; // 寫入時間戳
public mixed $data = null; // 實際業務資料
public bool $isRead = false; // 是否有效資料
public function isExpired(): bool {
return (time() - $this->expiredTime) > $this->expired;
}
}
/**
* 讀取器:非同步/同步讀取策略
*/
class RptFinder {
private string $key;
public function __construct(string $key) { $this->key = $key; }
// 非阻塞讀取:快取存在且未過期即返回,否則 null
public function read(): mixed {
$data = $this->readData();
return ($data->isRead && !$data->isExpired()) ? $data->data : null;
}
// 阻塞讀取:輪詢等待快取生成 (適用於報表異步生成場景)
public function readSync(): mixed {
while (true) {
$data = $this->readData();
if ($data->isRead && !$data->isExpired()) {
return $data->data;
}
sleep(1); // 避免佔滿 CPU
}
}
private function readData(): RptData {
$redis = Rpt::createRedis();
$rptData = new RptData();
$raw = $redis->get($this->key);
if ($raw === false) { // 鍵不存在
$rptData->expiredTime = time();
$rptData->expired = 86400; // 預設 24h
return $rptData;
}
$obj = json_decode($raw);
$rptData->expired = $obj->expired ?? 0;
$rptData->isRead = $obj->isRead ?? false;
$rptData->expiredTime = $obj->expiredTime ?? time();
$rptData->data = $obj->data ?? null;
return $rptData;
}
}
/**
* 寫入器:原子性寫入 (SETEX)
*/
class RptStorage {
private string $key;
public function __construct(string $key) { $this->key = $key; }
public function save(mixed $data, int $expired = 7200): bool {
$rptData = new RptData();
$rptData->data = $data;
$rptData->expiredTime = time();
$rptData->isRead = true;
$rptData->expired = $expired;
$redis = Rpt::createRedis();
return $redis->setex($this->key, $rptData->expired, json_encode($rptData));
}
}
使用範例
<?php
// 1. 簡單儲存/讀取
Rpt::save('diamond.account', $userNick, $accountData, 3600); // 1 小時
$data = Rpt::read('diamond.account', $userNick);
// 2. 同步等待 (報表異步生成中)
$data = Rpt::readSync('diamond.account', $userNick); // 阻塞直到有效資料
// 3. 物件導向風格
$finder = Rpt::createFinder('diamond.account', $userNick);
$storage = Rpt::createStorage('diamond.account', $userNick);
$data = $finder->read();
if (!$data) {
$storage->save($freshData, 7200);
}
關鍵設計亮點
| 特性 | 實現方式 | 效益 |
| 原子性寫入 | Redis SETEX (SET + EXPIRE) | 避免競態條件,寫入與過期單操作 |
| 資料完整性 | RptData 實體封裝 isRead/expiredTime | 區分「無快取」vs「快取過期」vs「快取有效」 |
| 同步等待 | readSync() 輪詢 + sleep(1) | 異步報表生成完成前自動阻塞,上層無感 |
| 鍵值隔離 | md5(nick) 避免鍵衝突、長度可控 | 支援任意用戶標識,安全無注入風險 |
| 門面模式 | Rpt 靜態方法統一入口 | 業務代碼簡潔,底層儲存可替換 (Redis→Memcached) |
擴展建議
- 標籤式清除:引入
RptTag 類別,支援按報表類別批次失效 (如 Rpt::invalidateTag('diamond.*'))。
- 壓縮大資料:
json_encode 前對 $data 使用 gzcompress,讀取時 gzuncompress,節省記憶體/頻寬。
- 分散式鎖:快取未命中時,使用
SET NX EX 獲取鎖,僅一個進程生成報表,其餘 readSync 等待。
- 監控指標:埋點記錄 hit/miss 率、平均延遲、同步等待耗時,接入 Prometheus/Grafana。
- 框架解耦:將
createRedis() 抽象為介面,支援 Laravel Redis、Predis、phpredis 多驅動。