多級快取架構(六)

童話述說我的結局發表於2021-07-04

在資訊暴炸的時代,為了在專案中提高資料載入效率,快取技術是必不可以少的,快取技術存在於應用場景的方方面面。從瀏覽器請求,到反向代理伺服器,從程式內快取到分散式快取。其中快取策略,演算法也是層出不窮,下面要說的就是一套如何實現一套可以對後端伺服器形成最小壓力的架構。

一、快取的解析

 

 

 借一下上一篇文章中的圖,其實現在問很多人怎麼提高資料的訪問速度和效率,很多人都能回答出做快取處理,這句話可以說對也可以說不對,對是因為沒錯,快取是可以提高效率,但並不是做了快取處理他的併發量就一定能提升的上來,就以上圖為例,如果說前端請求很高,達到了2000K,如果所有的資料載入在後端redis中做了快取,如果這時所有使用者請求全部從前端傳到後端,伺服器照樣承受不了這麼高併發,所以說只在後端提升了載入速度是沒有用的,正確的做法是把快取在到Nginx中而不是存到後臺伺服器,原因就是Nginx能承受的併發比tomcat要高。

 

 

 有了這個思路後,架構就要換一下了,如上圖,當第一次請求過來時走後臺去後臺Redis中查詢,並存在Nginx下面的Redis,之後的所有請求就不用走後臺查詢,這樣效能就提升上來了,這時還要考慮一個問題,那就是如果Nginx下的Redis本身沒有快取資料又怎麼搞,那就要去Nginx快取中去找,因為Nginx本身是帶有快取功能的,如果Nginx中存在要的快取就直接從Nginx中返回給使用者,這樣一設計後可以幾十萬個請求就只有幾十個落在了後臺伺服器上了。這種設計除了可以有效提高載入速度、降低後端服務負載之外,還可以防止快取雪崩,這也就是電商中用的多級快取架構體系。

二、應用

我們在看購物平臺時有很多商品優先推薦展示,這些其實都是推廣商品,並非真正意義上的熱門商品,首頁展示這些商品資料需要載入效率極高,並且商城首頁訪問頻率也是極高,所以需要對首頁資料做快取處理,首先想到的就是Redis快取。

 

 接下來要作的東西就是設計推廣商品的表結構及邏輯設計,其實這推廣產品不一定只在首頁出現,可能在別的地方也有,所以可以設計一張表存放這玩意,表結構如下

DROP TABLE IF EXISTS `ad_items`;
CREATE TABLE `ad_items` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(100) DEFAULT NULL,
  `type` int(3) DEFAULT NULL COMMENT '分類,1首頁推廣,2列表頁推廣',
  `sku_id` varchar(60) DEFAULT NULL COMMENT '展示的產品',
  `sort` int(11) DEFAULT NULL COMMENT '排序',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=123 DEFAULT CHARSET=utf8;

建立實體類:

@Data
@AllArgsConstructor
@NoArgsConstructor
//MyBatisPlus表對映註解
@TableName(value = "ad_items")
public class AdItems implements Serializable{

    @TableId(type = IdType.AUTO)
    private Integer id;
    private String name;
    private Integer type;
    private String skuId;
    private Integer sort;
}
public interface AdItemsMapper extends BaseMapper<AdItems> {
}
public interface SkuService extends IService<Sku> {
    /**
     * 根據推廣產品分類ID查詢Sku列表
     * @param id
     * @return
     */
    List<Sku> typeSkuItems(Integer id);
}
@Service
public class SKuServiceImpl extends ServiceImpl<SkuMapper, Sku> implements SkuService {

    @Resource
    private AdItemsMapper adItemsMapper;

    @Resource
    private SkuMapper skuMapper;


    @Override
    public List<Sku> typeSkuItems(Integer id) {
        //1.查詢當前分類下的所有列表資訊
        QueryWrapper<AdItems> adItemsQueryWrapper = new QueryWrapper<AdItems>();
        adItemsQueryWrapper.eq("type",id);
        List<AdItems> adItems = adItemsMapper.selectList(adItemsQueryWrapper);

        //2.根據推廣列表查詢產品列表資訊
        List<String> skuids = adItems.stream().map(adItem->adItem.getSkuId()).collect(Collectors.toList());
        return skuids==null || skuids.size()<=0? null : skuMapper.selectBatchIds(skuids);
    }
}
@RestController
@RequestMapping(value = "/sku")
public class SkuController {
    @Autowired
    private SkuService skuService;

    /****
     * 根據推廣分類查詢推廣產品列表
     *
     */
    @GetMappingpublic List<Sku> typeItems(@RequestParam(value = "id")Integer id){
        //查詢
        List<Sku> skus = skuService.typeSkuItems(id);
        return skus;
    }
}

經過上面的程式碼就可以實現熱門商品推廣操作,但是有個問題,因為前面說過熱點資料訪問量大,是要放在快取裡面進行快取處理的,下面就來完成這最後一步

三、快取處理

在改程式碼前先要說明幾個常用的註解:

@EnableCaching :開關性註解,在專案啟動類或某個配置類上使用此註解後,則表示允許使用註解的方式進行快取操作。
@Cacheable :可用於類或方法上;在目標方法執行前,會根據key先去快取中查詢看是否有資料,有就直接返回快取中的key對應的value值。不再執行目標方法;無則執行目標方法,並將方法的返回值作為value,並以鍵值對的形式存入快取。
@CacheEvict :可用於類或方法上;在執行完目標方法後,清除快取中對應key的資料(如果快取中有對應key的資料快取的話)。
@CachePut :可用於類或方法上;在執行完目標方法後,並將方法的返回值作為value,並以鍵值對的形式存入快取中。
@Caching :此註解即可作為@Cacheable、@CacheEvict、@CachePut三種註解中的的任何一種或幾種來使用。
@CacheConfig :可以用於配置@Cacheable、@CacheEvict、@CachePut這三個註解的一些公共屬性,例如cacheNames、keyGenerator。
說明完註解接下來就是來實現快取功能了,首先在配置檔案中配置Redis快取
  #Redis配置
  redis:
    host: 192.168.32.135
    port: 6379

然後在啟動類上開啟快取

 

 然後修改SKuServiceImpl類,加上紅框內內容

 

 完成這一步快取就做好了,但是有個問題,前面說過熱點商品可能在多個地方都存被呼叫的情況,針對這個情況就要寫feigin介面,在寫feigin介面同時我也順便把修改和刪除操作給寫了,後面就懶著改這個類了

在spring-cloud-api的pom檔案中先引用以下包

        <!--工具包-->
        <dependency>
            <groupId>com.ghy</groupId>
            <artifactId>spring-cloud-common</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </dependency>
        <!--openfeign-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
            <version>2.2.5.RELEASE</version>
        </dependency>

然後在spring-cloud-goods-api伺服器寫feigin介面

@FeignClient(value = "spring-cloud-goods-service")   
public interface SkuFeign {


/****
* 根據推廣分類查詢推廣產品列表
*/
@GetMapping(value = "/sku")
List<Sku> typeItems(@RequestParam(value = "id")Integer id);

/****
* 根據推廣分類刪除推廣產品列表
*
*/
@DeleteMapping(value = "/sku")
RespResult delTypeItems(@RequestParam(value = "id")Integer id);

/****
* 根據推廣分類修改推廣產品列表
*/
@PutMapping(value = "/sku")
RespResult updateTypeItems(@RequestParam(value = "id")Integer id);

}

四、Lua+Redis實現多級快取

 

 前面說過Lua和多級快取,接下來就把這兩個東西結合起來實現;前面說過前端請求過來後第一次會通過Nginx會分發到tomact進行查詢,然後將查詢的資料放到nginx中去;現在要做的操作就是通過Lua指令碼完善後面的操作,操作流程是這樣,當請求過來

nginx會先執行lua指令碼資料先從redis中去查詢先看下redis有沒有對應的資料,如果有則直接把資料響應給使用者,沒有再經過tomact查詢;這時有的人會問,如果一不小心把Redis伺服器中的資料全清空了怎麼辦,其實問題也不大,前面也提過Nginx本身也帶有快取,當Redis沒查詢資料時,他會去Nginx自身查下,看能不能找到資料;

下面先寫一個lua指令碼;

進入/usr/local/openresty/nginx/lua,在裡面建立一個aditem.lua指令碼

--資料響應型別JSON 
ngx.header.content_type="application/json;charset=utf8"
--Redis庫依賴 
local redis = require("resty.redis");
local cjson = require("cjson"); 
--獲取id引數(type) 
local id = ngx.req.get_uri_args()["id"]; 
--key組裝 
local key = "ad-items-skus::"..id 
--建立連結物件 
local red = redis:new() 
--設定超時時間 red:set_timeout(2000) 
--設定伺服器連結資訊 
red:connect("192.168.32.135", 6379) 
--查詢指定key的資料 
local result=red:get(key); 
--關閉Redis連結 
red:close()
if result==nil or result==null or result==ngx.null then 
return true 
else
--輸出資料 
ngx.say(result) 
end
修改 nginx.conf 新增如下配置:
#推廣產品查詢 
location /sku{ 
content_by_lua_file /usr/local/openresty/nginx/lua/aditem.lua;
 }

 

 

 

然後儲存修改,打入nginx -s reload命令重啟下再訪問 http://www.ljx.com/sku?id=1 就可以拿到資料,這個請求跟你後臺伺服器開沒開都沒有關係了。

 

 

五、nginx代理快取

前面有提過,當Redis快取沒有資料時,請求會去nginx快取找資料;在這時就要提到一個東西,那就是proxy_cache ;proxy_cache 是用於 proxy 模式的快取功能,proxy_cache 在 Nginx 配置的 http 段、server 段中分別寫入不同的配置。http 段中的配置用於定義 proxy_cache 空間,server 段中的配置用於呼叫 http 段中的定義,啟用對server 的快取功能。要想使用它就要完成兩個步驟:第一個步驟是定義快取空間;第二個步驟是在指定地方使用定義的快取 。

1、開啟Proxy_Cache快取,這個需要在nginx.conf中配置才能開啟快取:

proxy_cache_path /usr/local/openresty/nginx/cache levels=1:2 keys_zone=proxy_cache:10m max_size=1g inactive=60m use_temp_path=off;
引數說明:
【proxy_cache_path】指定快取儲存的路徑,快取儲存在/usr/local/openresty/nginx/cache目錄 
【levels=1:2】設定一個兩級目錄層次結構儲存快取,在單個目錄中包含大量檔案會降低檔案訪問速度,因此我們建議對大多數部署使用兩級目錄層次結構。如果 levels 未包含該引數,Nginx 會將所有檔案放在同一目錄中。 
【keys_zone=proxy_cache:10m】設定共享記憶體區域,用於儲存快取鍵和後設資料,例如使用計時器。擁有記憶體中的金鑰副本,Nginx 可以快速確定請求是否是一個 HIT 或 MISS 不必轉到磁碟,從而大大加快了檢查速度。1 MB 區域可以儲存大約 8,000 個金鑰的資料,因此示例中配置的 10 MB 區域可以儲存大約80,000 個金鑰的資料。
【max_size=1g】設定快取大小的上限。它是可選的; 不指定值允許快取增長以使用所有可用磁碟空間。當快取大小達到限制時,一個稱為快取管理器的程式將刪除最近最少使用的快取,將大小恢復到限制之下的檔案。
【inactive=60m】指定專案在未被訪問的情況下可以保留在快取中的時間長度。在此示例中,快取管理器程式會自動從快取中刪除 60 分鐘未請求的檔案,無論其是否已過期。預設值為 10 分鐘(10m)。非活動內容與過期內容不同。Nginx 不會自動刪除快取 header 定義為已過期內容(例如 Cache-Control:max-age=120)。過期(陳舊)內容僅在指定時間內未被訪問時被刪除。訪問過期內容時,Nginx 會從原始伺服器重新整理它並重置 inactive 計時器。
【use_temp_path=off】表示NGINX會將臨時檔案儲存在快取資料的同一目錄中。這是為了避免在更新快取時,磁碟之間互相複製響應資料,我們一般關閉該功能。
Proxy_Cache屬性:
proxy_cache:設定是否開啟對後端響應的快取,如果開啟的話,引數值就是zone的名稱,比如:proxy_cache。
proxy_cache_valid:針對不同的response code設定不同的快取時間,如果不設定code,預設為200,301,302,也可以用any指定所有code。
proxy_cache_min_uses:指定在多少次請求之後才快取響應內容,這裡表示將快取內容寫入到磁碟。
proxy_cache_lock:預設不開啟,開啟的話則每次只能有一個請求更新相同的快取,其他請求要麼等待快取有資料要麼限時等待鎖釋放;nginx 1.1.12才開始有。配套著proxy_cache_lock_timeout一起使用。
proxy_cache_key:快取檔案的唯一key,可以根據它實現對快取檔案的清理操作。 
2、開啟Proxy_Cache快取後修改 nginx.conf;在location /sku中加入以下配置
 #先找Nginx快取 
 rewrite_by_lua_file /usr/local/openresty/nginx/lua/aditem.lua; 
 #啟用快取openresty_cache 
 proxy_cache proxy_cache; 
 #針對指定請求快取 
 #proxy_cache_methods GET; 
 #設定指定請求會快取 
 proxy_cache_valid 200 304 60s; 
 #最少請求1次才會快取 
 proxy_cache_min_uses 1; 
 #如果併發請求,只有第1個請求會去伺服器獲取資料 
 #proxy_cache_lock on; 
 #唯一的key 
 proxy_cache_key $host$uri$is_args$args; 
 #動態代理,這是在快取中都沒查到的情況 
proxy_pass http:
//192.168.32.32:8081;

 

 

重啟nginx或者重新載入配置檔案 nginx -s reload ,再次測試,可以發現下面個規律:
 
1:先查詢Redis快取 
2:Redis快取沒資料,直接找Nginx快取 
3:Nginx快取沒資料,則找真實伺服器

這時還可以發現在cache目錄下多了目錄個檔案,這個東西就是Nginx快取;這裡補充下,如果你的linux是布在雲上而你的專案是內外,那麼動態代理配置的就是專案的公網地址;

六、Cache_Purge代理快取清理

上面講了Nginx快取的儲存,但是在很多時候我們如果不想等待快取的過期,想要主動清除快取,可以採用第三方的快取清除模組清除快取nginx_ngx_cache_purge 。安裝nginx的時候,需要新增 purge 模組purge 模組我們已經下載了,這一個步驟我在安裝 OpenRestry 的時候已經實現了。在OpenRestry文章中也有說明,安裝好了後,我們配置一個清理快取的地址:http://192.168.32.32/purge/sku?id=1 
 
#清理快取 
location ~ /purge(/.*) {
#清理快取
proxy_cache_purge proxy_cache $host$1$is_args$args;
}

 

 

此時訪問http://www.ljx.com/purge/sku?id=1,表示清除快取,如果出現如下
效果表示清理成功: 

 

 


 

 

相關文章