是否想過PHP使用redis作為緩存時,如何能:
- 前后台模塊共用Model層;
- 但是,不能每個Model類都進行緩存,這樣太浪費Redis資源;
- 前后台模塊可以自由決定從數據庫還是從緩存讀數據;
- 沒有冗余代碼;
- 使用方便。
這里我們先展示實現的最終效果。
最終的代碼和使用說明請移步Github:https://github.com/yeszao/php-redis-cache。
馬上安裝使用命令:
$ composer install yeszao/cache
經過簡單配置就可以使用,請參看Github的README說明。
1 最終效果
假設在MVC框架中,model層有一個Book類和一個getById方法,如下:
class Book { public function getById($id) { return $id; } }
加入緩存技術之后,原來方法的調用方式和返回的數據結構都不應該改變。
所以,我們希望,最后的效果應該是這樣的:
1 (new Book)->getById(100); // 原始的、不用緩存的調用方式,還是原來的方式,一般是讀取數據庫的數據。 2 (new Book)->getByIdCache(100); // 使用緩存的調用方式,緩存鍵名為:app_models_book:getbyid: + md5(參數列表) 3 (new Book)->getByIdClear(100); // 刪除這個緩存 4 (new Book)->getByIdFlush(); // 刪除 getById() 方法對應的所有緩存,即刪除 app_models_book:getbyid:*。這個方法不需要參數。
這樣我們可以很清楚的明白自己在做什么,同時又知道數據的來源函數,並且被引用方式完全統一,可謂一箭三雕。
其實實現起來也比較簡單,就是使用PHP的魔術方法__call()方法。
2 __call()方法
這里簡單說明一下__call方法的作用。
在PHP中,當我們訪問一個不存在的類方法時,就會調用這個類的__call()方法。
(如果類方法不存在,又沒有寫__call()方法,PHP會直接報錯)
假設我們有一個Book類:
1 class Book 2 { 3 public function __call($name, $arguments) 4 { 5 echo '類Book不存在方法', $name, PHP_EOL; 6 } 7 8 public function getById($id) 9 { 10 echo '我的ID是', $id, PHP_EOL; 11 } 12 }
當調用存在的getById(50)方法時,程序打印:我的ID是50。
而如果調用不存在的getAge()方法時,程序就會執行到A類的__call()方法里面,這里會打印:類Book不存在方法getAge。
這就是__call的原理。
3 實現細節
接下來我們就利用__call()方法的這種特性,來實現緩存策略。
從上面的例子,我們看到,__call()方法被調用時,會傳入兩個參數。
name:想要調用的方法名arguments:參數列表
我們就可以在參數上面做文章。
還是以Book類為例,我們假設其原本結構如下:
1 class Book 2 { 3 public function __call($name, $arguments) 4 { 5 // 待填充內容 6 } 7 8 public function getById($id) 9 { 10 return ['id' => $id, 'title' => 'PHP緩存技術' . $id]; 11 } 12 }
開始之前,我們還確認Redis的連接,這是緩存必須用到的,這里我們寫個簡單的單例類:
1 class Common 2 { 3 private static $redis = null; 4 5 public static function redis() 6 { 7 if (self::$redis === null) { 8 self::$redis = new \Redis('127.0.0.1'); 9 self::$redis->connect('redis'); 10 } 11 return self::$redis; 12 }
然后,我們開始填充__call()方法代碼,具體說明請看注釋:
1 class Book 2 { 3 public function __call($name, $arguments) 4 { 5 // 因為我們主要是根據方法名的后綴決定具體操作, 6 // 所以如果傳入的 $name 長度小於5,可以直接報錯 7 if (strlen($name) < 5) { 8 exit('Method does not exist.'); 9 } 10 11 // 接着,我們截取 $name,獲取原方法和要執行的動作, 12 // 是cache、clear還是flush,這里我們取了個巧,動作 13 // 的名稱都是5個字符,這樣截取就非常高效。 14 $method = substr($name, 0, -5); 15 $action = substr($name, -5); 16 17 // 當前調用的類名稱,包括命名空間的名稱 18 $class = get_class(); 19 20 // 生成緩存鍵名,$arguments稍后再加上 21 $key = sprintf('%s:%s:', str_replace('\\', '_', $class), $method); 22 // 都用小寫好看點 23 $key = strtolower($key); 24 25 switch ($action) { 26 case 'Cache': 27 // 緩存鍵名加上$arguments 28 $key = $key . md5(json_encode($arguments)); 29 30 // 從Redis中讀取數據 31 $data = Common::redis()->get($key); 32 33 // 如果Redis中有數據 34 if ($data !== false) { 35 $decodeData = json_decode($data, JSON_UNESCAPED_UNICODE); 36 // 如果不是JSON格式的數據,直接返回,否則返回json解析后的數據 37 return $decodeData === null ? $data : $decodeData; 38 } 39 40 // 如果Redis中沒有數據則繼續往下執行 41 42 // 如果原方法不存在 43 if (method_exists($this, $method) === false) { 44 exit('Method does not exist.'); 45 } 46 47 // 調用原方法獲取數據 48 $data = call_user_func_array([$this, $method], $arguments); 49 50 // 保存數據到Redis中以便下次使用 51 Common::redis()->set($key, json_encode($data), 3600); 52 53 // 結束執行並返回數據 54 return $data; 55 break; 56 57 case 'Clear': 58 // 緩存鍵名加上$arguments 59 $key = $key . md5(json_encode($arguments)); 60 return Common::redis()->del($key); 61 break; 62 63 case 'Flush': 64 $key = $key . '*'; 65 66 // 獲取所有符合 $class:$method:* 規則的緩存鍵名 67 $keys = Common::redis()->keys($key); 68 return Common::redis()->del($keys); 69 break; 70 71 default: 72 exit('Method does not exist.'); 73 } 74 } 75 76 // 其他方法 77 }
這樣就實現了我們開始時的效果。
4 實際使用時
在實際使用中,我們需要做一些改變,把這一段代碼歸入一個類中,
然后在model層的基類中引用這個類,再傳入Redis句柄、類對象、方法名和參數,
這樣可以降低代碼的耦合,使用起來也更靈活。
完整的代碼已經放在Github上,請參考文章開頭的參考地址。
推薦閱讀: