概述
互聯(lián)網架構設計的五大要素:高性能禽笑、高可用入录、可伸縮性、可擴展性佳镜、安全纷跛。
如何做到高性能、高可用邀杏,緩存是一大助力贫奠。我們知道,絕大部分的時候望蜡,讀數據寫數據符合二八定律唤崭。并且讀數據中,百分之二十是數據被經常讀取(熱數據)脖律。那么我們解決這百分之二十的數據的方法就可以取得很好的一個性能谢肾。
以一句比較戲謔的話說說本質的東西:
互聯(lián)網架構設計中沒有什么不能通過一層抽象層(代理)解決的,如果有小泉,那就兩層芦疏。
他山之石
- 互聯(lián)網架構的三板斧:https://yq.aliyun.com/articles/54449
- 一致性哈希算法:http://www.reibang.com/p/e8fb89bb3a61
-如何設計實現一個LRU Cache:http://www.importnew.com/18758.html - [Google Guava] 3-緩存:http://ifeve.com/google-guava-cachesexplained/
- Java集合-ConcurrentHashMap原理分析:https://www.cnblogs.com/ITtangtang/p/3948786.html
-ehcache基本原理:http://tommy-lu.iteye.com/blog/2237109
-Spring+EhCache緩存實例:https://www.kancloud.cn/digest/javaframe/125582
緩存分類
從很多互聯(lián)網架構設計中可以看到冕杠,從用戶在瀏覽器上輸入網址開始,經歷了太多的緩存酸茴。我大概列舉一下:
- 輸入網址后分预,查詢?yōu)g覽器緩存
- 查詢?yōu)g覽器dns緩存
- 查詢操作系統(tǒng)dns緩存
- 請求dns服務器,查詢dns服務器緩存
- 獲得ip薪捍,靜態(tài)資源走cdn緩存笼痹。動態(tài)數據走服務器
- 如果配置了頁面緩存,走頁面緩存
- 如果配置了本地緩存(localcache)酪穿,走本地緩存
- 如果配置了分布式緩存(如redis等等)凳干,走分布式緩存
- 數據庫操作,數據庫緩存
這里被济,我想主要講講2種localcache的選擇救赐,為什么要引入localcache,可以用《互聯(lián)網架構的三板斧》中的一句話來說明:
數據訪問熱點只磷,比如Detail中對某些熱點商品的訪問度非常高经磅,即使是Tair緩存這種Cache本身也有瓶頸問題,一旦請求量達到單機極限也會存在熱點保護問題喳瓣。有時看起來好像很容易解決,比如說做好限流就行赞别,但你想想一旦某個熱點觸發(fā)了一臺機器的限流閥值畏陕,那么這臺機器Cache的數據都將無效,進而間接導致Cache被擊穿仿滔,請求落地應用層數據庫出現雪崩現象惠毁。這類問題需要與具體Cache產品結合才能有比較好的解決方案,這里提供一個通用的解決思路崎页,就是在Cache的client端做本地Localcache鞠绰,當發(fā)現熱點數據時直接Cache在client里,而不要請求到Cache的Server飒焦。
還有一個原因是蜈膨,在做秒殺的時候,我可以在每一臺應用服務器中設置一個有失效時間的商品剩余數量的計數器牺荠,以達到盡可能在調用鏈前面攔截非有效請求翁巍。
分布式緩存
如何部署分布式緩存,這里不細說休雌。列一下我司的部署方式:
這種方式是不太好擴機器。有一種比較好的方式是:一致性哈希算法杈曲。
可以參考這篇文章:他山之石中的《一致性哈希算法》
Localcache
在java中驰凛,localcache本質上是一個map胸懈,對應map中的每一個鍵值對,可以設置過期時間恰响,也可以通過如LRU(Least Recently Used 最近最少使用)做淘汰策略趣钱。
LRU算法實現原理參見他山之石中的《如何設計實現一個LRU Cache》。本質上是一個hashmap+雙向鏈表渔隶,每次訪問操作都將節(jié)點放在鏈表頭部羔挡,尾部自然就是最舊的節(jié)點了。
我對比了兩種LocalCache,google的Guava庫中的cache模塊
和Ehcache
间唉。
Guava可以參考文章:他山之石中的《 [Google Guava] 3-緩存》
其基本原理為:ConcurrentMap(利用分段鎖降低鎖粒度) + LRU算法绞灼。
ConcurrentMap分段鎖原理參考《Java集合-ConcurrentHashMap原理分析》
分治思想。
Ehcache相關呈野,可以參考《ehcache基本原理》與《Spring+EhCache緩存實例》低矮。該緩存是一個比較重的localcache。有一個比較有意思的點是:支持磁盤緩存被冒,這樣媽媽再也不用擔心內存不夠用了>->军掂。
我的需求是:可以不用擔心內存不足的問題,做一些配置或不需要強一致性數據緩存昨悼。甚至可以做偽熱加載配置蝗锥。
因此,我選擇使用了Ehcache率触。如果只是做內存緩存终议,建議使用guava,有很多有意思的東西葱蝗,比如緩存失效穴张,自動從數據源加載數據等等。
Ehcache工具類
根據自身需要两曼,封裝了一個只走磁盤的無容量限制的localcache工具類皂甘,僅供參考,考慮到可以放在多個地方悼凑,因此沒有走配置文件
package com.fenqile.creditcard.appgatewaysale.provider.util;
import com.alibaba.fastjson.JSONObject;
import net.sf.ehcache.Cache;
import net.sf.ehcache.CacheManager;
import net.sf.ehcache.Element;
import net.sf.ehcache.config.CacheConfiguration;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.security.MessageDigest;
/**
* User: Rudy Tan
* Date: 2018/3/28
*
* 本地緩存工具偿枕,基于ehcache,磁盤存儲
*
* 可以運用于配置文件户辫、接口數據等等益老,
*
*/
public class LocalCacheUtil {
private static final CacheManager cacheManager = CacheManager.create();
private static Logger LOG = LoggerFactory.getLogger(LocalCacheUtil.class);
private static String md5(String str) {
try {
MessageDigest md = MessageDigest.getInstance("MD5");
byte[] bytes = md.digest(str.getBytes("utf-8"));
final char[] HEX_DIGITS = "0123456789ABCDEF".toCharArray();
StringBuilder ret = new StringBuilder(bytes.length * 2);
for (int i=0; i<bytes.length; i++) {
ret.append(HEX_DIGITS[(bytes[i] >> 4) & 0x0f]);
ret.append(HEX_DIGITS[bytes[i] & 0x0f]);
}
return ret.toString();
}
catch (Exception e) {
throw new RuntimeException(e);
}
}
private static Cache getCacheInstance(){
String cacheKey = "local_cache_"+ md5(Thread.currentThread().getStackTrace()[1].getClassName());
if (!cacheManager.cacheExists(cacheKey)){
synchronized (cacheManager){
if (!cacheManager.cacheExists(cacheKey)){
CacheConfiguration cacheConfiguration = new CacheConfiguration();
cacheConfiguration.setTimeToIdleSeconds(60);
cacheConfiguration.setTimeToLiveSeconds(60);
cacheConfiguration.setName(cacheKey);
cacheConfiguration.setMaxEntriesLocalHeap(1);
cacheConfiguration.setMaxEntriesLocalDisk(100000);
cacheConfiguration.setEternal(false);
cacheConfiguration.setOverflowToDisk(true);
cacheConfiguration.setMaxElementsInMemory(1);
cacheConfiguration.setCopyOnRead(true);
cacheConfiguration.setCopyOnWrite(true);
Cache cache = new Cache(cacheConfiguration);
cacheManager.addCache(cache);
}
}
}
return cacheManager.getCache(cacheKey);
}
private static Element serialization(String key, Object value, Integer expireTime){
if (StringUtils.isEmpty(key)
|| null == expireTime
|| 0 == expireTime){
return null;
}
String clazz = "";
String content = "";
if (null == value){
clazz = "null";
content = clazz + "_class&data_null";
}else {
clazz = value.getClass().getName();
content = clazz + "_class&data_"+ JSONObject.toJSONString(value);
}
return new Element(key, content, expireTime, expireTime);
}
private static Object unSerialization(Element element){
if (null == element){ return null; }
String content = (String) element.getObjectValue();
String[] data = content.split("_class&data_");
Object response = null;
try {
if ("null".equalsIgnoreCase(data[0])){
return null;
}
response = JSONObject.parseObject(data[1], Class.forName(data[0]));
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
return response;
}
/**
* 設置本地緩存
*/
public static boolean setCache(String key, Object value, Integer expireTime){
Cache cache = getCacheInstance();
if (null == cache){
LOG.info("setCache:cache is null, {}, {}, {}", key, value, expireTime);
return false;
}
if (StringUtils.isEmpty(key)
|| null == expireTime
|| 0 == expireTime){
LOG.info("setCache:params is not ok, {}, {}, {}", key, value, expireTime);
return false;
}
synchronized (cache){
cache.put(serialization(key, value, expireTime));
cache.flush();
}
return true;
}
/**
* 獲取本地緩存
*/
public static Object getCache(String key){
Cache cache = getCacheInstance();
if (null == cache
|| StringUtils.isEmpty(key)){
LOG.info("getCache:params is not ok, {}", key);
return null;
}
Element element = cache.get(key);
return unSerialization(element);
}
/**
* 清理本地緩存
*/
public static boolean delCache(String key){
Cache cache = getCacheInstance();
if (null == cache){
LOG.info("delCache:cache is null, {}", key);
return true;
}
if (StringUtils.isEmpty(key)){
LOG.info("delCache:params is not ok, {}", key);
return true;
}
synchronized (cache){
cache.put(serialization(key, null, 0));
cache.flush();
}
return true;
}
}
good luck.