對(duì)于緩存大家都不會(huì)陌生,但如何正確和合理的使用緩存還是需要一定的思考,本文將基于Java技術(shù)棧對(duì)緩存做一個(gè)相對(duì)詳細(xì)的介紹,內(nèi)容分為基本概念、本地緩存、遠(yuǎn)程緩存和分布式緩存集群幾個(gè)部分,重點(diǎn)在于理解緩存的相關(guān)概念,愿合理的使用Cache如下圖的妹子一樣美好。
電腦培訓(xùn),計(jì)算機(jī)培訓(xùn),平面設(shè)計(jì)培訓(xùn),網(wǎng)頁(yè)設(shè)計(jì)培訓(xùn),美工培訓(xùn),Web培訓(xùn),Web前端開發(fā)培訓(xùn)

基本概念

緩存是計(jì)算機(jī)系統(tǒng)中必不可少的一種解決性能問(wèn)題的方法,常見的應(yīng)用包括CPU緩存、操作系統(tǒng)緩存、本地緩存、分布式緩存、HTTP緩存、數(shù)據(jù)庫(kù)緩存等。其核心就是用空間換時(shí)間,通過(guò)分配一塊高速存儲(chǔ)區(qū)域(一般來(lái)說(shuō)是內(nèi)存)來(lái)提高數(shù)據(jù)的讀寫效率,實(shí)現(xiàn)的難點(diǎn)就在于清空策略的實(shí)現(xiàn),比較合理的思路就是定時(shí)回收與即時(shí)判斷數(shù)據(jù)是否過(guò)期相結(jié)合。

緩存相關(guān)概念

  1. 命中率:命中率指請(qǐng)求次數(shù)與正確返回結(jié)果次數(shù)的比例,其影響因素包括數(shù)據(jù)實(shí)時(shí)性,如果股票類實(shí)時(shí)性要求很高的數(shù)據(jù),緩存的命中率會(huì)很低;緩存粒度問(wèn)題, 如果KEY值包含的條件太多,會(huì)出現(xiàn)緩存命中率特別低的情況。通常來(lái)說(shuō),提高緩存命中率的方法包括增大緩存空間的大小的;對(duì)熱點(diǎn)數(shù)據(jù)進(jìn)行實(shí)時(shí)更新;調(diào)整緩存KEY的算法,保證緩存KEY的細(xì)粒度,如key-value;根據(jù)業(yè)務(wù)需要合理調(diào)整緩存的過(guò)期策略。

  2. 最大元素:緩存中可以存放的元素的最大數(shù)量。

  3. 清空策略包括FIFO,最先進(jìn)入緩存的數(shù)據(jù)在空間不夠時(shí)會(huì)被優(yōu)先清理;LFU一直以來(lái)最少被使用的元素會(huì)被清理,可以給緩存元素設(shè)置一個(gè)計(jì)數(shù)器實(shí)現(xiàn);LRU最近最少使用的緩存元素會(huì)被清理,可以通過(guò)一個(gè)時(shí)間戳來(lái)講最近未使用數(shù)據(jù)清除。

  4. 預(yù)熱策略包括全量預(yù)熱,一開始就加載全部數(shù)據(jù),適用于不怎么變化的數(shù)據(jù)(地區(qū)數(shù)據(jù));增量預(yù)熱,查詢不到時(shí)從數(shù)據(jù)源取出放入緩存。

Tip:
緩存在高并發(fā)場(chǎng)景下的常見問(wèn)題

緩存相關(guān)問(wèn)題

  1. 緩存穿透:一般的緩存系統(tǒng),都是按照key去緩存查詢,如果不存在對(duì)應(yīng)的value,就應(yīng)該去后端系統(tǒng)查找(比如DB)。如果key對(duì)應(yīng)的value是一定不存在的,并且對(duì)該key并發(fā)請(qǐng)求量很大,就會(huì)對(duì)后端系統(tǒng)造成很大的壓力。這就叫做緩存穿透。解決方法包括將查詢結(jié)果為空的情況也進(jìn)行緩存,緩存時(shí)間設(shè)置短一點(diǎn),并在該key對(duì)應(yīng)的數(shù)據(jù)insert之后清理緩存;對(duì)一定不存在的key進(jìn)行過(guò)濾。

  2. 緩存雪崩
    當(dāng)緩存服務(wù)器重啟或者大量緩存集中在某一個(gè)時(shí)間段失效,這時(shí)會(huì)給后端系統(tǒng)(比如DB)帶來(lái)很大壓力。解決方案包括在緩存失效后,通過(guò)加鎖或者隊(duì)列來(lái)控制讀數(shù)據(jù)庫(kù)寫緩存的線程數(shù)量,比如對(duì)某個(gè)key只允許一個(gè)線程查詢數(shù)據(jù)和寫緩存,其他線程等待;不同的key設(shè)置不同的過(guò)期時(shí)間,讓緩存失效的時(shí)間點(diǎn)盡量均勻;做二級(jí)緩存,A1為原始緩存,A2為拷貝緩存,A1失效時(shí),可以訪問(wèn)A2,A1緩存失效時(shí)間設(shè)置為短期,A2設(shè)置為長(zhǎng)期。

分布式緩存系統(tǒng)的需要注意緩存一致性、緩存穿透和雪崩、緩存數(shù)據(jù)的清理等問(wèn)題??梢?strong style="margin: 0px; padding: 0px;">通過(guò)鎖解決一致性問(wèn)題;為了提高緩存命中率,可以對(duì)緩存分層,分為全局緩存,二級(jí)緩存,他們是存在繼承關(guān)系的,全局緩存可以有二級(jí)緩存來(lái)組成。為了保證系統(tǒng)的HA,緩存系統(tǒng)可以組合使用兩套存儲(chǔ)系統(tǒng)(memcache,redis)。緩存淘汰的策略包括定時(shí)去清理過(guò)期的緩存、判斷過(guò)期時(shí)間來(lái)決定是否重新獲取數(shù)據(jù)。

在java應(yīng)用中通常由兩類緩存,一類是進(jìn)程內(nèi)緩存,就是使用java應(yīng)用虛擬機(jī)內(nèi)存的緩存;另一個(gè)是進(jìn)程外緩存,現(xiàn)在我們常用的各種分布式緩存。前者比較簡(jiǎn)單,而且在一個(gè)JVM中,快速且可用性高,但會(huì)存在多態(tài)負(fù)載均衡主機(jī)數(shù)據(jù)不一致的問(wèn)題,因此適合最常用且不易變的數(shù)據(jù)。后者擴(kuò)展性強(qiáng),而且相關(guān)的方案多,比如Redis Cluster等。通常來(lái)說(shuō),從數(shù)據(jù)庫(kù)讀取一條數(shù)據(jù)需要10ms,從分布式緩存讀取則只需要0.5ms左右,而本地緩存則只需要10μs,因此需要根據(jù)具體場(chǎng)景選出合適的方案。

Local緩存

Java的本地緩存很早就有了相關(guān)標(biāo)準(zhǔn)javax.cache,要求的特性包括原子操作、緩存讀寫、緩存事件監(jiān)聽器、數(shù)據(jù)統(tǒng)計(jì)等內(nèi)容。實(shí)際工作中本地緩存主要用于特別頻繁的穩(wěn)定數(shù)據(jù),不然的話帶來(lái)的數(shù)據(jù)不一致會(huì)得不償失。實(shí)踐中,常使用Guava Cache,以及與Spring結(jié)合良好的EhCache.

Guava Cache

是一個(gè)全內(nèi)存的本地緩存實(shí)現(xiàn),它提供了線程安全的實(shí)現(xiàn)機(jī)制,簡(jiǎn)單易用,性能好。其創(chuàng)建方式包括cacheLoadercallable callback兩種,前者針對(duì)整個(gè)cache,而后者比較靈活可以在get時(shí)指定。
CacheBuilder.newBuilder()方法創(chuàng)建cache時(shí)重要的幾個(gè)方法如下所示,之后是一個(gè)簡(jiǎn)單的使用示例。
maximumSize(long):設(shè)置容量大小,超過(guò)就開始回收。
expireAfterAccess(long, TimeUnit):在這個(gè)時(shí)間段內(nèi)沒有被讀/寫訪問(wèn),就會(huì)被回收。
expireAfterWrite(long, TimeUnit):在這個(gè)時(shí)間段內(nèi)沒有被寫訪問(wèn),就會(huì)被回收 。
removalListener(RemovalListener):監(jiān)聽事件,在元素被刪除時(shí),進(jìn)行監(jiān)聽。

@Servicepublic class ConfigCenterServiceImpl implements ConfigCenterService {    private final static long maximumSize = 20;    /**
     * 最大20個(gè),過(guò)期時(shí)間為1天
     */
    private Cache<String, Map<String, ConfigAppSettingDto>> cache = CacheBuilder.newBuilder().maximumSize(maximumSize)
            .expireAfterWrite(1, TimeUnit.DAYS).build();
    @Autowired    private ConfigAppSettingDAO configAppSettingDAO;

    @Override    public ConfigAppSettingDto getByTypeNameAndKey(String configType, String appID, String key) {
        Map<String, ConfigAppSettingDto> map = getByType(configType, appID);        return map.get(key);
    }    /************************** 輔助方法 ******************************/
    private Map<String, ConfigAppSettingDto> getByType(String configType, String appID) {        try {            return cache.get(configType, new Callable<Map<String, ConfigAppSettingDto>>() {
                @Override                public Map<String, ConfigAppSettingDto> call() throws Exception {
                    Map<String, ConfigAppSettingDto> result = Maps.newConcurrentMap();
                    List<ConfigAppSetting> list = configAppSettingDAO.getByTypeName(configType, appID);                    if (null != list && !list.isEmpty()) {                        for (ConfigAppSetting item : list) {
                            result.put(item.getAppkey(), new ConfigAppSettingDto(item.getAppkey(), item.getAppvalue(),
                                    item.getDescription()));
                        }
                    }                    return result;
                }
            });
        } catch (ExecutionException ex) {            throw new BizException(300, "獲取ConfigAppSetting配置信息失敗");
        }
    }
}

EHCache

EHCache也是一個(gè)全內(nèi)存的本地緩存實(shí)現(xiàn),符合javax.cache JSR-107規(guī)范,被應(yīng)用在Hibernate中,過(guò)去存在過(guò)期失效的緩存元素?zé)o法被GC掉,造成內(nèi)存泄露的問(wèn)題,其主要類型及使用示例如下所示。
Element:緩存的元素,它維護(hù)著一個(gè)鍵值對(duì)。
Cache:它是Ehcache的核心類,它有多個(gè)Element,并被CacheManager管理,實(shí)現(xiàn)了對(duì)緩存的邏輯操作行為。
CacheManager:Cache的容器對(duì)象,并管理著Cache的生命周期。

Spring Boot整合Ehcache示例

//maven配置<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<dependency>
    <groupId>net.sf.ehcache</groupId>
    <artifactId>ehcache</artifactId>
</dependency>//開啟Cache@SpringBootApplication
@EnableCachingpublic class Application {    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}//方式1@CacheConfig(cacheNames = "users")public interface UserRepository extends JpaRepository<User, Long> {
    @Cacheable
    User findByName(String name);
}//方式2@Servicepublic class CacheUserServiceImpl implements CacheUserService {
    @Autowired    private UserMapper userMapper;
    @Override    public List<User> getUsers() {        return userMapper.findAll();
    }    // Cacheable表示獲取緩存,內(nèi)容會(huì)存儲(chǔ)在people中,包含兩個(gè)Key-Value
    @Override
    @Cacheable(value = "people", key = "#name")    public User getUser(String name) {        return userMapper.findUserByName(name);
    }    //put是存儲(chǔ)
    @CachePut(value = "people", key = "#user.userid")    public User save(User user) {
        User finalUser = userMapper.insert(user);        return finalUser;
    }    //Evict是刪除
    @CacheEvict(value = "people")    public void remove(Long id) {
        userMapper.delete(id);
    }
}//在application.properties指定spring.cache.type=ehcache即可//在src/main/resources中創(chuàng)建ehcache.xml<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:noNamespaceSchemaLocation="ehcache.xsd">
    <cache name="users"
           maxEntriesLocalHeap="100"
           timeToLiveSeconds="1200">
    </cache>
</ehcache>

ehcache官方文檔

Remote緩存

常見的分布式緩存組件包括memcached,redis等。前者性能高效,使用方便,但功能相對(duì)單一,只支持字符串類型的數(shù)據(jù),需要結(jié)合序列化協(xié)議,只能用作緩存。后者是目前最流行的緩存服務(wù)器,具有高效的存取速度,高并發(fā)的吞吐量,并且有豐富的數(shù)據(jù)類型,支持持久化。因此,應(yīng)用場(chǎng)景非常多,包括數(shù)據(jù)緩存、分布式隊(duì)列、分布式鎖、消息中間件等。

Redis

Redis支持更豐富的數(shù)據(jù)結(jié)構(gòu), 例如 字符串(strings), 散列(hashes), 列表(lists), 集合(sets), 有序集合(sorted sets) 與范圍查詢, bitmaps, hyperloglogs 和 地理空間(geospatial) 索引半徑查詢。 此外,Redis 內(nèi)置了 復(fù)制(replication),LUA腳本(Lua scripting), LRU驅(qū)動(dòng)事件(LRU eviction),事務(wù)(transactions) 和不同級(jí)別的 磁盤持久化(persistence), 并通過(guò) Redis哨兵(Sentinel)和自動(dòng) 分區(qū)(Cluster)提供高可用性(high availability)??梢哉f(shuō)Redis兼具了緩存系統(tǒng)和數(shù)據(jù)庫(kù)的一些特性,因此有著豐富的應(yīng)用場(chǎng)景。本文介紹Redis在Spring Boot中兩個(gè)典型的應(yīng)用場(chǎng)景。
場(chǎng)景1:數(shù)據(jù)緩存

//maven配置<dependency>  
    <groupId>org.springframework.boot</groupId>  
    <artifactId>spring-boot-starter-redis</artifactId>  
</dependency>//application.properties配置# Redis數(shù)據(jù)庫(kù)索引(默認(rèn)為0)spring.redis.database=0  # Redis服務(wù)器地址spring.redis.host=localhost# Redis服務(wù)器連接端口spring.redis.port=6379  # Redis服務(wù)器連接密碼(默認(rèn)為空)spring.redis.password=  
# 連接池最大連接數(shù)spring.redis.pool.max-active=8  # 連接池最大阻塞等待時(shí)間(使用負(fù)值表示沒有限制)spring.redis.pool.max-wait=-1  # 連接池中的最大空閑連接spring.redis.pool.max-idle=8  # 連接池中的最小空閑連接spring.redis.pool.min-idle=0//方法1@Configuration
@EnableCachingpublic class CacheConfig {
    @Autowired    private JedisConnectionFactory jedisConnectionFactory;
    @Bean    public RedisCacheManager cacheManager() {
        RedisCacheManager redisCacheManager = new RedisCacheManager(redisTemplate());        return redisCacheManager;
    }
    @Bean    public RedisTemplate<Object, Object> redisTemplate() {
        RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<Object, Object>();
        redisTemplate.setConnectionFactory(jedisConnectionFactory);        // 開啟事務(wù)支持
        redisTemplate.setEnableTransactionSupport(true);        // 使用String格式序列化緩存鍵
        StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
        redisTemplate.setKeySerializer(stringRedisSerializer);
        redisTemplate.setHashKeySerializer(stringRedisSerializer);        return redisTemplate;
    }
}//方法2,和之前Ehcache方式一致

場(chǎng)景2:共享Session

//maven配置<dependency>
    <groupId>org.springframework.session</groupId>
    <artifactId>spring-session-data-redis</artifactId>
</dependency>//Session配置@Configuration@EnableRedisHttpSession(maxInactiveIntervalInSeconds = 86400*)//public class SessionConfig {
}//示例@RequestMapping("/session")@RestControllerpublic class SessionController {    @Autowired
    private UserRepository userRepository;    @RequestMapping("/user/{id}")
    public User getUser(@PathVariable Long id, HttpSession session) {        User user = (User) session.getAttribute("user" + id);        if (null == user) {
            user = userRepository.findOne(id);
            session.setAttribute("user" + id, user);
        }        return user;
    }
}

tip:
Jedis的使用請(qǐng)見《大型分布式網(wǎng)站架構(gòu)》學(xué)習(xí)筆記--02基礎(chǔ)設(shè)施
Jedis的Github地址
Spring Redis默認(rèn)使用JDK進(jìn)行序列化和反序列化,因此被緩存對(duì)象需要實(shí)現(xiàn)java.io.Serializable接口,否則緩存出錯(cuò)。
過(guò)去寫過(guò)一篇Redis快速入門,現(xiàn)在來(lái)看理解上還差的比較多,有些麻瓜。

Redis集群

實(shí)現(xiàn)包括如下3種方式,相對(duì)于傳統(tǒng)的大客戶端分片和代理模式,路由查詢的方式比較新穎,具體解決方案推薦redis-cluster。
客戶端分片:包括jedis在內(nèi)的一些客戶端,都實(shí)現(xiàn)了客戶端分片機(jī)制。
基于代理的分片:Twemproxy、codis,客戶端發(fā)送請(qǐng)求到一個(gè)代理,代理解析客戶端的數(shù)據(jù),將請(qǐng)求轉(zhuǎn)發(fā)至正確的節(jié)點(diǎn),然后將結(jié)果回復(fù)給客戶端。
路由查詢:Redis-cluster,將請(qǐng)求發(fā)送到任意節(jié)點(diǎn),接收到請(qǐng)求的節(jié)點(diǎn)會(huì)將查詢請(qǐng)求發(fā)送到正確的節(jié)點(diǎn)上執(zhí)行,這個(gè)思路比較有意思。

Docker部署Redis主從

//1.獲取redis$docker pull  redis:3.2//2.主從啟動(dòng)$docker run -p 6379:6379 --name redis01 -v $PWD/data01:/data -v $PWD/config01:/usr/local/redis  -d redis:3.2 redis-server  --appendonly yes//$docker start redis01 /usr/local/redis/redis.conf //先建立容器,再做好配置,之后根據(jù)配置啟動(dòng)(這部分細(xì)節(jié)上掌握的有些問(wèn)題,比如需要先run,stop再start,如何合理的使用create?)//配置到官網(wǎng)下載,docker中只需要修改appendonly yes, port xxxx//修改從庫(kù)02,03的配置redis.conf,添加slaveof xxx.xxx.xxx.xxx 6379, docker exec -ti redis01 /bin/bash//docker run  -p 6380:6380  -v $PWD/config02:/usr/local/redis  -v $PWD/data02:/data --name redis02 -d redis:3.2 redis-server /usr/local/redis/redis.confdocker run  -p 6380:6379  -v $PWD/config02:/usr/local/redis  -v $PWD/data02:/data --name redis02 -d redis:3.2 redis-server --slaveof  xxx.xxx.xxx.xxx 6379 --appendonly yes


作  者:熊二哥 
出  處:http://www.cnblogs.com/wanliwang01/ 
版權(quán)聲明:本文版權(quán)歸作者和博客園共有,歡迎轉(zhuǎn)載,但未經(jīng)作者同意必須保留此段聲明,且在文章頁(yè)面明顯位置給出原文鏈接。 

http://www.cnblogs.com/wanliwang01/p/cache01.html