程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> 網頁編程 >> PHP編程 >> 關於PHP編程 >> 緩存的使用,緩存使用

緩存的使用,緩存使用

編輯:關於PHP編程

緩存的使用,緩存使用


  緩存,提高訪問速度的利器。工作中主要用到的是memcache和redis,它們是B/S軟件,類似練習時裝在機子上的Apache,它也會監聽端口,可以在客戶端(如在cmd上通過telnet操作Memcache)直接敲各自對應的命令來存取值,自學時可以通過這樣的方式熟悉下原生命令,看看效果。它們常駐內存,得到數據後寫入內存(安裝軟件後會占用一片內存區域),設定數據的過期時間,用到時直接從內存中讀出來,因為是內存所以訪問速度上有數量級的提高。lz曾今寫了個非常簡單的後台,完全不用緩存那種,那速度我自己看了都忍不住要咆哮:這麼慢tm也能叫網頁?!可那時我年幼無知,不會用緩存,當然現在也不見得好到哪兒去,但願那時前端妹子沒在心裡暗自唾罵>3<......

  memcache的使用較簡單,至少從命令上來看是這樣。在memcache中,存儲的方式是key->value,鍵-值對應的方式存儲,類似聯接數組的元素形式,可存儲的類型可以是數字、字符串、數組、對象等,項目中一般會將非字符串(數字可看成是數字字符串)類型的先進行json編碼或者序列化再存入緩存,取出來時則解碼或者反序列化。Memcache是php專門處理memcache緩存的類,封裝的主要方法是get、set、delete、flush、connect、close,即一個緩存必備的獲取、設置、刪除、清空、連接、關閉等。(中括號為可傳參數)

add key, var, [flag], [expire] 添加一個鍵為key、值為var到服務端,可指定過期時間expire,0則永不過期,最長30天 get key, [flag] 從服務端獲取某一或某些元素 delete key, [timeout] 在指定timeout時間內刪除key對應元素,默認timeout為0立即刪除 flush 無 清空,即刪除所有元素 connect host, [port], [timeout] 連接到memcache服務器host主機的port端口,它會在腳本執行完自動關閉,也可主動關閉 close 無 主動關閉memcache服務端連接,但不會關閉pconnect生成的連接 set key, var, [flag], [expire] 存儲鍵為key、值為var的元素到服務端,key存在則覆蓋其值,不存在新add一個 replace key, var, [flag], [expire] 查找鍵為key的元素,以值var替換,查不到會報錯 increment key, [value] 在鍵為key的元素上增加值value,value默認為1,可能改變原值 decrement key, [value] 在鍵為key的元素上減少值value,value默認為1,可能改變原值 addServer host, [port], [persistent], [weight], [timeout], ...... 添加一個memcache服務到連接池(保存多個memcache連接的地方)中,即建立一個到host的連接,port為端口 pconnect host, [port], [timeout] 建立到位於host主機的memcache服務器的持久連接,port為端口,腳本執行完或調用close也無法關閉這種連接

  其中有幾點需要留意下:

  1. set和replace的用法區別:set幾乎任何情況下,當然不是連接都失敗、內存不夠用等硬性原因,都是成功的,replace只是在已經存在的鍵的基礎上進行修改,鍵若不存在不成功;

  2. connect方法建立的連接,要麼腳本運行結束自己斷開,要麼調用close()方法被斷開掉;

  3. increment或者的decrement方法得到的數永遠是大於或等於0,如果確實計算後得到的值是小於0的則進行轉化,比如int類型,32位上占4字節,則返回的值是(232-1),計算後大於或等於0的直接返回;而元素之類型為非數字(或數字字符串)的,先轉為數字再計算;

  4. 當我們new一個Memcache對象並進行連接時,可以有兩種方式,一是connect方法:$mem =new Memcache; $mem->connect('127.0.0.1', 11211); 或者使用addserver方法:$mem->addServer('127.0.0.1', 11211); 後者以向已有的連接池中添加一個Memcache服務器連接的方式建立,效果一樣。

  Redis,一款強大的緩存工具,得益於在國內門戶網站中的廣泛使用,功能上在不斷完善,現在有穩定的版本(貌似微軟github上最新64位的release 2.8版本,在我工作的機子上運行不起來...),方法比memcache多得多,作為開發使用到的主要是數據的存取,而其他的事物、通信協議、集群,在一般開發個後台,不是對速度追求到極致的情況下,還是基本用不到。redis中有按操作分為以下幾種類型:

  1. String:也是key-value對,主要也是對它的鍵對應的元素進行get、set、求長度、自增等操作,redis將添加的類型全存為字符串,即便純數字;

  

  2. Hash:哈希表(散列表),就是數據結構中的字典,采用特定的散列算法,比如手機通訊錄有幾百人,以姓的第一個字母作為關鍵碼進行排序,這樣我們可以快速找到聯系人,這是通過特定的映射方式,將一個字母與姓名聯系起來。在redis中,創建hash表時,首先指定表名,你存在哪張表裡邊,然後指定一個域(field,實際就是一個變量),和它對應的值,單用cmd操作就像下面這樣(在hash表hash1中存儲一個域username對應值Jack,redis會自動建立username到Jack的映射),一張hash表裡邊可以存儲多個域和對應的變量;

  

  3. List:列表,類似於相當於數據結構的鏈表,通過它還可實現棧和隊列,前面的string(這名字取得很怪異)存儲一一對應的單個鍵值對,它則存多個值(沒有鍵),重點是作為一個列表結構,它可以分別從左右(表頭和表尾)進行插入和彈出變量值,求長度等一系列操作,這也是為什麼稱為列表,下面lpush、rpush命令就是從表左邊和右邊插入變量hello和world;

  

  4. Set:集合,顧名思義也可以存儲多個變量值(沒有鍵),只是符合數學上定義的集合的特性,比如集合中元素不重復,因此set中各個數據對的鍵名也不一樣,它也可以進行並、交、差等運算(下面命令是向集合mySet添加一個值var);

  

     5. SortedSet:有序集合,可看做Set的特殊方式,在每一個SortedSet類型集合中,添加一個值(沒有鍵)時,都要給它們指定一個稱為score的變量值,有序有序,得有個字段來判定它的順序是不,就是這個玩意兒,下面命令創建一個有序集合stset,添加一個值google.com,它的score是10。其實猜也猜得到,既然叫有序集合,既然給定了score,基本確定可以用這個來進行某些排序操作,事實也是這樣;

  

  6. Key:單獨對鍵進行操作,當然對其對應的元素值也有影響,如查看鍵對應元素的數據類型、鍵對應元素的存活時間、重置存活時間、返回所有鍵、正則形式查看相關鍵名、刪除鍵(元素也跟著沒了)等等。它不是數據類型,實際上,Redis把Hash表表名、List列表表名、集合Set名都稱為鍵,整個集合或者表是它的值。下面是type命令查看它存儲值得類型,它存儲的是List列表。

  

  上面這些命令,找個參考文檔走一遍,很快就能玩熟,個人在這兒也只是泛泛而談。那麼在實際項目中,通常是怎樣使用緩存呢?不往大的談,比如微博的緩存設計,這可能涉及到架構層面的東西了,單說作為一個有一定訪問量的後台,又想使用緩存來提高速度的情況下。

  一個重要的原則(個人目前碰得多的情況)是,以傳入的參數來拼鍵名,以這個鍵名來存取值。比如現在Model模型類裡面有個方法:getUserInfoByUid($params),傳入包含uid字段的數組查詢這個用戶的信息,在這兒使用緩存,先取緩存數據,取不到則去查數據庫,然後重新加入緩存,最後返回結果數據,這也是使用緩存的一個通用流程。

  以Memcache為例,先看代碼,定義一個Cache類:

    /**
     * Memcache緩存類
     */
    define('CACHE_HOST', '127.0.0.1');
    define('CACHE_PORT', 11211);
    
    class Cache{
        private static $instance = null;  
        
        private $_cache = null;  // 緩存操作對象
        
        const prefix = 'APP|';  // 緩存鍵值的前綴,為該項目名稱 
        
        private function __construct(){
            if($this->_cache === null){
                try{
                    $this->_cache = new Memcache;
                    if(!$this->_cache->connect(CACHE_HOST, CACHE_PORT)){
                        exit('connect failed');
                    }
                    
                }
                catch(Exception $e){
                    echo 'ERROR: '.$e->getMessage();
                }
                
            }
        }
        /** 
         * 返回單例
         */
        public static function getInstance(){
            if(self::$instance === null){
                self::$instance = new self();
            }
            return self::$instance;
        }
        /**
         * 生成鍵名
         */
        private function genKey($key){
            return Cache::prefix.$key;
        }
        
        public function __destruct(){
            if($this->_cache !== null){
                return $this->_cache->close();
            }
        }
        /**
         * 添加元素,設置過期時間
         */
        public function add($key, $val, $expire = 3600){
            return $this->_cache->add($this->genKey($key), $val, 0, $expire);
        }
        /**
         * 重置元素
         */
        public function set($key, $val, $expire = 3600){
            echo 'cache key: '.$this->genKey($key).'<br/>';  // test
            return $this->_cache->set($this->genKey($key), $val, 0, $expire);
        }
        /**
         * 獲取元素
         */
        public function get($key){
            return $this->_cache->get($this->genKey($key));
        }
        /**
         * 自增
         */
        public function increment($key, $val = 1){
            return $this->_cache->increment($this->genKey($key), $val);
        }
        /**
         * 自減
         */
        public function decrement($key, $val = 1){
            return $this->_cache->decrement($this->genKey($key), $val);
        }
        /**
         * 刪除元素
         */
        public function delete($key, $timeout = 0){
            return $this->_cache->delete($this->genKey($key), $timeout);
        }
        /**
         * 刪除所有元素
         */
        public function flush(){
            return $this->_cache->flush();
        }    
    }

   然後定義一個操作數據庫操作類:

    /**
     * 數據庫操作
     */
    define('DB_NAME', 'test');
    define('DB_HOST', '127.0.0.1');
    define('DB_PORT', 3306);
    define('DB_USER', 'root');
    define('DB_PASS', '1234');
    
    class Dal{
        private static $instance = null;
        
        public $pdo = null;        
        
        private function __construct(){
            try{
                $dsn = 'mysql:dbname='.DB_NAME.';host='.DB_HOST.':'.DB_PORT;
                $this->pdo = new PDO($dsn, DB_USER, DB_PASS);
            }
            catch(PDOException $e){
                echo 'Error: '.$e->getMessage();
                return false;
            }
        }
        //獲取實例
        public static function getInstance(){
            if(self::$instance === null){
                self::$instance = new self();
            }
            return self::$instance;
        }
        // 獲取用戶信息
        public function getUserInfoByUid($uid){
            $sql = sprintf('select * from tab1 where uid = %s limit 1', $uid);
            $stmt = $this->pdo->query($sql);
            return $stmt->fetch(PDO::FETCH_ASSOC);
        }
    }

  而一般來說,通常是在Model模型類中才對數據進行讀寫,因此再定義一個UserModel類,盡量從簡,不再去寫可能會有的Model公共公共基類。

    include 'Dal.php';
    include 'Cache.php';
    
    class UserModel{
        private static $instance = null;
        
        public $cache = null;
        
        public $db = null;
        
        // 緩存鍵名部分,通過函數名稱及參數拼接
        const cache_get_userinfo_uid = 'GET|USER|INFO|BY|UID|%s';
        
        private function __construct(){
            // 獲取對應類實例
            $this->cache = Cache::getInstance();
            $this->db = Dal::getInstance();
        }
        
        public static function getInstance(){
            if(self::$instance === null){
                self::$instance = new self();
            }
            return self::$instance;
        }
        
        /**
         根據uid獲取用戶信息
         */
        public function getUserInfoByUid($params, $timeout = 3600){
            // 缺少必要參數uid,返回
            if(empty($params['uid'])){
                return null;
            }
    
            // 拼接緩存鍵名
            $key = sprintf(self::cache_get_userinfo_uid, $params['uid']);
                    
            // 獲取緩存數據
            $data = json_decode($this->cache->get($key), true);
            
            echo 'cache=><pre>';
            var_dump($data);
            
            // 緩存為空
            if(!$data){
                $data = $this->db->getUserInfoByUid($params['uid']);
                if(empty($data)){
                    return null;
                }
                // 在數據庫中獲取到數據後,重新寫入緩存
                $this->cache->set($key, json_encode($data), $timeout);
            }
            else{
                
            }
            return $data;
        }
    }
    // 測試
    $data = UserModel::getInstance()->getUserInfoByUid(array('uid'=>17653), 5);
    echo 'UserModel=><pre>';
    var_dump($data);

  這裡,我們要通過用戶的uid獲取一個用戶的信息,這個查詢條件是uid,傳過來的必要參數是uid,本來我們就是要緩存每個用戶的數據,以期在重復查詢時速度更快,因此在定義緩存的鍵時,就可以考慮使用uid。但是單單使用這個uid不行,因為假如其他的表中也有根據uid查詢的,就會出現鍵名重復,造成了數據混亂,好我們使用的是getUserInfoByUid這個方法,可以把方法名也拼進去,因此在UserModel類中,定義了一個常量cache_get_userinfo_uid,它最後的%s就是為了拼接參數uid。但是,這樣可能還是不行,現在公司啟動了另外一個項目,也要用到這個表,某人寫的方法跟這個方法名完全一樣,畢竟函數名字又沒有專利,而且運維一般單獨拿服務器出來作為緩存使用,幾個項目公用一台服務器,數據寫在一個緩存池裡也正常,此時可以考慮再加個前綴---工程名稱(或說項目),因此,在Cache類中,又定義了一個常量prefix,給這個項目叫APP,這樣基本就可以保證不會沖突了。當然如果這是一個組的子項目,這個組還有其他項目,這個組叫Star的話,可以再將項目組名稱加在前面。

  看看效果,第一次(左邊)運行讀取cache是空的,但是可以看到打印的緩存鍵名,

           

  第二次(右邊)cache就有數據了,因為我把打印緩存鍵名的地方放在Cache類的set方法中,第二次直接讀到了緩存數據,沒有去連數據庫,自然也沒有重置緩存,所以沒打印cache key。

  扯了半天就是要保證鍵名不沖突,也夾雜了一般在一個項目中怎樣使用緩存,有幾個供參考的方式:

  1. 當前參數,可以有幾個拼接幾個,這裡只舉了個uid,假如還有name, age等等,可以都放在後面,具體怎麼弄看個人喜好或者代碼規范;

  2. 當前方法名稱,盡量接近當前方法名稱,拼在緩存key中間;

  3. 當前項目名稱作為前綴(可選),更安全;

  4. 所在項目組名稱,再次作為前綴拼進來(一般用不到)。

  然後就是,在Model模型類中,先取緩存,取不到則讀數據庫,讀到後不要忘了寫入緩存,設置好過期時間等細節,就這麼一個流程。

  當然你還可以做得更加細致,比如讀取到緩存數據後,再檢測下它的過期時間,發現還有5秒就要過期了,於是我們重新給它設置為3000秒,這樣下次訪問時又能取到了,不至於下次又讀庫,頻繁地連接數據庫是很耗時的一件事。

  不過我還是覺得Redis好用,Redis中的Hash表最好的啦>-_-<,但恰好機子上還沒配。

  唔......不早了,洗洗睡~=_=

 

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved