天天看點

Redis應用于商城緩存的設計與思考

Redis應用于商城緩存的設計與思考

題記:關于使用redis的一些問題是不得不考慮的,那就是使用時存儲(使用何種存儲結構),重新整理(如何設定過期時間),清理(是否需要定期清理)的設計,和擊穿,穿透,雪崩的容災設計。

項目背景:現需要對在運作的商城進行二次開發,随着商城人數的增加,網站通路壓力變大,特别是活動商品和特價商品的通路量比較大。為了解決這些問題,降低資料庫的負載,決定使用redis對項目進行二次開發。使用redis對使用者的登入,浏覽記錄,購物車進行重寫,并緩存活動商品的資料行。

一,redis使用位置

redis在php中使用,需要安裝redis,和對應版本的phpredis。

以下關于redis的所有使用都需要提前執行個體化,這裡給出執行個體化的代碼,在以下代碼中省略

//redis的執行個體化
$redis = new \redis();    
$redis->connect('127.0.0.1', 6379);    
           
二,使用redis實作購物車

以前的購物車都是儲存在資料庫的表中,結構如下:

Redis應用于商城緩存的設計與思考

商城使用者數比較多,且購物車添加與删除在整個項目中應該屬于對資料庫插入,删除操作最多,是以決定将購物車相關功能使用redis實作。

主要有會員id,商品規格id,數量,店鋪id。我們決定使用散清單的形式存儲: cart:memberId , formId => num。

散清單的形式 hSet(鍵名,成員,值)

//原版
$selRes = db('shopcart')->where('memberid',$memberId)->where('formid',$formId)->find();//購物車中是否有該商品
if($selRes){   
    $number = $number+$selRes['num'];    
    $res = db('shopcart')->where('memberid',$memberId)->where('formid',$formId)->update(['num'=>$number]);}else{    
    $data = [      
    'num' => $number,      
    'formid' => $formId,     
    'memberid' => $memberId,    
    ];    
    $res = db('shopcart')->insert($data);
}
           
//使用redis實作
public function addCart($memberId,$formId,$num){    
    if($num <= 0){//移除指定商品        
        $redis->hDel('cart:'.$memberId,$formId);    
    }else{//更新記錄       
        $redis->hSet('cart:'.$memberId,$formId,$num);   
    }   
 }
           
三,使用redis實作儲存使用者浏覽記錄

儲存使用者浏覽記錄是用于在每周或者每月做商品的分析,看哪種商品使用者浏覽最多,進而對銷售政策進行調整。

但是出現了一個問題。使用者浏覽記錄太多了,我們并不能一一儲存,是以需要維護一個合适定值的資料集。(假設為10萬)

存貯的過程是發生在每個使用者的請求中,如果在每個使用者處設定限制,顯然是不合理的,因為這樣操作太頻繁,而且容易出錯。是以我們需要完成一個可以定時處理并重新整理其中資料的功能。我們需要的是最接近統計時間的10萬條資料,是以我們使用有序集合,score值就設定為經過處理的時間戳,數值越大證明此條資料越“新”。

我們使用有序集合的形式存儲: recent , timestr , goodsId.":".formId 。

reid集合 zadd(鍵名,分值,成員):分值不能重複,成員名重複則會替換舊值
//浏覽記錄添加
public function addRecent($memberId,$goodsId){    
    $timeStr = substr(time(),-6);       
    $redis->zAdd('recent',$timeStr,$goodsId.':'.$memberId);   
 }
           

接下來就需要完成浏覽記錄的更新了。邏輯是超出限定值則剔除最“舊”的100個,

注意根據score剔除成員函數的用法 :

zRemRangeByRank(key, start, end); 移除key對應的有序集合中rank值介于start和stop之間的所有元素。 start和stop均是從0開始的,并且兩者均可以是負值。當索引值為負值時,表明偏移值從有序集合中score值最高的元素開始例如:-1表示具有最高score的元素,而-2表示具有次高score的元素,以此類推。

未超出時,休眠十分鐘後繼續執行。是以使用了while true,這個程式最好是以守護程序的形式一直運作,也可以根據系統的情況,調整合适的限定值,或不想用休眠的話改為定時任務執行

//浏覽記錄清理
public function cleanRecent(){   
    $limit = 10000;   
    $size = $redis->zSize('recent');   
    while(true){       
        if($size <= $limit){            
            sleep(600);//每隔十分鐘執行            
            continue;       
        }else{           
            $redis->zRemRangeByRank('recent', 0, 100);//超出後,剔除最舊的100個    
        }   
    }
}
           
四,使用redis緩存活動商品資料行

大多數自營商城都需要做活動,限時限量售賣一些特價商品,一般此類商品銷售的都比較快,上架後通路量會特别大,比如此商城在某活動日幾小時内達到了4000訂單,通路量達到了十萬的級别,這時候某幾個活動商品對資料庫的讀壓力就會很大,是以我們決定将活動商品的資料儲存在redis中進行完成。(應用在商品詳情頁面)

//原版查詢代碼
//商品資訊商品狀态為上架,店鋪為稽核通過
$goodsArr = Db::table('goods')        
                ->alias('g')       
                ->where('g.id',$goodsId)        
                ->where('g.goodsStatus',1)        
                ->join('goodsform f','f.goodid = g.id','LEFT')        
                ->join('store s','s.id = g.storeid','LEFT')        
                ->field('s.storename,f.formname,f.num,f.realprice,f.priceRMB,f.priceVirtual,f.salenum,f.id,
                f.goodid,g.goodsName,g.time,g.description,g.pic,g.storeid')
                ->select();
           

主要使用兩個函數來完成活動商品資料行的緩存。分别為延時函數和排程函數,其中是用了redis 的三種結構,兩個有序集合(延時有序集合和排程有序集合)和一個字元串機構(用來存儲商品資訊的json資料)。

基本原理是:

  • 1,店鋪添加活動商品時,先将商品添加至資料庫商品表,再調用延時函數添加至redis中,延時函數主要添加延時有序集合和處理有序集合,(延時有序集合參數為商品id和系統設定的活動商品的重新整理的頻率,處理有序集合參數主要為目前時間戳和商品id)
  • 2,系統以守護程序方式運作的排程函數,排程函數作用,根據現在的時間-排程集合中的時間的結果,與延時集合的時間進行對比,判斷是否需要更新。
  • 3,使用者讀取商品頁面直接從redis中讀取,購買商品減少庫存還是需要操作資料庫

    資料類型:

    排程有序集合,分值為時間戳,成員為資料行id。

    使用有序集合存儲: deal , timestr , goodsRowID 。

    延時有序集合,分值為指定資料行需要每隔多少秒更新一次,成員為資料行id。

    使用有序集合存儲: delay , delayTime , goodsRowID 。

//延時函數
public function delayShop($memberId,$delay){    
    $redis->zAdd('delay:',$delay,$memberId);//延時有序集合    
    $redis->zAdd('deal:',time(),$memberId);//排程有序集合
}
           
//排程集合
public function dealShop(){    
while(true){
           
            $next = $redis->zRange('deal:',0,0,true);//根據時間戳取最先添加進入的商品資訊
            $now = time();
            if(empty($next)||reset($next)>$now){//将val中存放的時間戳與現在進行比較,為空或者超出現在時間則程式休息
                sleep(5);
                continue;
            }
            $memberId = key($next);//取出key,存放的商品id
            $delay = $redis->zscore('delay:',$memberId);
            if($delay <= 0){//延時時間小于等于0,表示不再緩存,商品下架
                $redis->delete('inv:'.$memberId);
                $redis->zDelete('delay:',$memberId);
                $redis->zDelete('deal:',$memberId);
                continue;
            }
            $row = db('goods')->where('id','=',$memberId)->field('inform')->find();//從資料庫中擷取商品的json資料
            $redis->zAdd('deal:',$memberId,$now+$delay);//增加時間後,排在了隊列之後
            $redis->set('inv:'.$memberId,$row);
        }
}
           

注意zRange的用法,zRange(鍵名,開始位置,結束為止,withscore)。

取得特定範圍内的排序元素,0代表第一個元素,1代表第二個以此類推。-1代表最後一個,-2代表倒數第二個。最後一個參數表示是否在結果中展示score的值

五,查詢商品的緩存

每一個商城都有商品分類,特别是自營的商城,都有自己“主打”的商品類型,這類商品往往是經常被查詢或者點選的,我們想要将查詢的結果資訊進行緩存。(應用在商品查詢或者是首頁)

//原版
    $goodsArr = Db::field('*')
    ->table('goods')
    ->where('goodstypeNo',$type)
    ->where('goodsStatus',1)
    ->order('time desc')
    ->page($page,$number)
    ->select();
           

在開始使用redis完成此功能之前,我們必須考慮一些問題。在前幾個功能的設計中,關于存儲,重新整理,記憶體清理的問題已經考慮進去。完成基本功能後,還需要考慮容災問題就是使用者在某一時刻大規模通路網站,而redis卻沒有命中,導緻大規模通路資料庫的問題。

關于擊穿,穿透,雪崩《redis設計與實作》中的解釋
  1. 緩存擊穿:對于一些設定了過期時間的key, 剛好過期的時候,這時候有個高并發的請求,會導緻直接通路資料庫,危險.

    解決方法:(批量放入時這麼用的)先把緩存更新,再更新資料庫。單個的時候先更新資料庫,再更新緩存

  1. 緩存穿透:查詢一個一定不存在的資料,導緻直接通路資料庫。 

    解決方法:如果一個查詢傳回的資料為空,我們仍然把這個空結果進行緩存,但它的過期時間會很短,最長不超過五分鐘。

  1. 緩存雪崩是指在我們設定緩存時采用了相同的過期時間,導緻緩存在某一時刻同時失效。

    解決方法:随機設定過期時間。

  • 關于浏覽記錄,我們沒有設定過期時間,而是在使用者浏覽記錄達到某個門檻值之後進行清理。
  • 對于購物車,我們也沒有設定過期時間,而是限定了每個使用者添加購物車商品數量。

    這兩個功能都是資訊寫在redis中,和資料庫沒有關系,故沒有擊穿,穿透,雪崩的問題

  • 關于商品資料行,我們也沒有設定過期時間,資料直接從redis中讀取,有一個持續運作的函數不斷更新資料,保證使用者從redis中讀取的資料是較新的資料

那什麼時候可能會出現這種問題,就是設定過期時間時。我們使用redis來緩存經常被查詢的商品的資訊時,因為記憶體的限制,我們并不能一直儲存這些資料,是以我們必須為這些資料設定過期時間。流程如下:

使用者先查詢redis中,是否有該商品資訊,沒有則查詢資料庫,資料庫也沒有,則将該為空的查詢結果也寫入redis(避免穿透,過期時間可以短一些),資料庫中有,也寫入redis;如果redis中有,則直接從redis中傳回,注意寫入redis中時随機設定合理的過期時間。(避免雪崩)。若此時有大量商品資料更新了,則将商品資料先寫入redis,再寫入資料庫(避免穿透)

//緩存查詢商品
public function searchType($type){    
    $res = $redis->get('goods:'.$type);//查詢redis    
    if($res){       
        return $res;    
    }else{        
        $goodsArr = Db::field('*')->table('goods')->where('goodstypeNo',$type)->where('goodsStatus',1)->order('time desc')->select();//查詢資料庫        
        $redis->set('goods:'.$type,$goodsArr);//存入redis        
        if(empty($goodsArr)){//設定不同的過期時間            
            $expireTime = 60*5;        
        }else{            
            $expireTime = 60*60*2+random_int(1,300);//random_int是php7中的函數        
        }        
        $redis->expireAt('goods:'.$type, $expireTime);        
        return $goodsArr;    
    }
}
           
六,設計與思考

開題中提到,關于使用redis的一些問題是不得不考慮的,存儲,重新整理,清理的設計,和擊穿,穿透,雪崩的容災設計。

合理利用redis來解決商城項目中的性能瓶頸,就需要首先找出項目中大量讀寫資料庫,或者大量通路伺服器的點,然後結合redis各資料結構的特點,替換掉原有程式,提供一個穩定合理的解決方案。

redis作為一個記憶體上的資料庫,有極快的查詢速度,是以應用比較廣泛,這篇文章分享了redis在項目中使用的位置和結合的形式,希望能對你有所幫助,筆者也是第一次使用redis,有很多不熟悉的點,歡迎大家評論,指正,轉載請标明出處。