启用spring-boot-start-cache

以下所有内容均可在springboot-demo中找到。

Spring Cache是Spring针对Spring应用,给出的一整套应用缓存解决方案。
Spring Cache本身并不提供缓存实现,而是通过统一的接口和代码规范,配置、注解等使你可以在Spring应用中使用各种Cache,而不用太关心Cache的细节。通过Spring Cache,你可以方便的使用各种缓存实现,包括ConcurrentMap,Ehcache,JCache,Redis等。

所需依赖

在pom.xml引入maven依赖,注意是否存在依赖冲突。

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-cache</artifactId>
</dependency>

Spring中Cache的定义

Spring 中关于缓存的定义,包括在接口 org.springframework.cache.Cache 中,它主要提供了如下方法

// 根据指定key获取值
<T> T get(Object key, Class<T> type)
// 将指定的值,根据相应的key,保存到缓存中
void put(Object key, Object value);
// 根据键,回收指定的值
void evict(Object key)
从定义中不难看着,Cache事实上就是一个key-value的结构我们通过个指定的key来操作相应的value

Cache Manager

Cache是key-value的集合,但我们在项目中,可能会存在各种业务主题的不同的Cache,比如用户的cache,部门的Cache等,这些cache在逻辑上是分开的。为了区分这些Cache,提供了org.springframework.cache.CacheManager用来管理各种Cache.该接口只包含两个方法

// 根据名字获取对应主题的缓存
Cache getCache(String name);
// 获取所有主题的缓存
Collection<String> getCacheNames();

在该接口中,不允许对Cache进行增加、删除操作,这些操作应该在各种CacheManager实现的内部完成,而不应该公开出来。

简单使用

定义一个缓存的操作接口

public interface CacheService {

    class keyValue{
        public String key;
        public String value;
    }

    String get(String key)throws Exception;

    void set(String key,String value)throws Exception;

    void del(String key)throws Exception;

}

使用一个默认的缓存主题,实现get,set,del方法。

import com.zph.programmer.springboot.cache.CacheService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager;
import org.springframework.stereotype.Service;

@Service
public class CacheServiceImpl implements CacheService {

    private final static String cacheName="spring-boot-demo-cache";
    @Autowired
    private CacheManager cacheManager;

    @Override
    public String get(String key)throws Exception {
        Cache cache = cacheManager.getCache(cacheName);
        if(cache!=null) {
            return cache.get(key, String.class);
        }
        else{
            throw new Exception("缓存"+cacheName+"异常!");
        }
    }

    @Override
    public void set(String key, String value)throws Exception {
        Cache cache = cacheManager.getCache(cacheName);
        if(cache!=null) {
            cache.put(key, value);
        }
        else{
            throw new Exception("缓存"+cacheName+"异常!");
        }
    }

    @Override
    public void del(String key) throws Exception{
        Cache cache = cacheManager.getCache(cacheName);
        if(cache!=null) {
            cache.evict(key);
        }
        else{
            throw new Exception("缓存"+cacheName+"异常!");
        }
    }
}

最后在启动类或者配置类里添加注解@EnableCaching即可。

其他

spring-boot默认的缓存是存储在ConcurrentHashMap里的,对于分布式应用应该引入redis等缓存来实现CacheManager。此外,spring-boot的缓存抽象还支持基于注解的使用方式,可参考 官方文档——Cache Abstraction
个人并不喜欢滥用复杂的缓存方式,因为这样很容易导致数据一致性,缓存击穿,缓存穿透,缓存雪崩等一系列问题,而且还不便于查错,只用当出现性能问题时,才考虑能否用缓存解决,并且缓存的数据尽量简单,常用,一定要设置过期时间,因为大量的低使用率数据积累在缓存中,必然导致缓存太大影响程序的运行。

整合EhCache

Ehcache 是一个纯Java开源缓存框架,配置简单、结构清晰、功能强大,非常轻量级的缓存实现。

官网地址Ehcache

所需依赖

在pom.xml引入maven依赖

<dependency>
            <groupId>net.sf.ehcache</groupId>
            <artifactId>ehcache</artifactId>
            <version>2.10.9.2</version>
        </dependency>

配置文件

在resource目录下添加ehcache.xml。

<?xml version="1.0" encoding="UTF-8"?>
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:noNamespaceSchemaLocation="http://ehcache.org/ehcache.xsd">
    <!--
           diskStore:为缓存路径,ehcache分为内存和磁盘 2级,此属性定义磁盘的缓存位置
           user.home - 用户主目录
           user.dir - 用户当前工作目录
           java.io.tmpdir - 默认临时文件路径
       -->
    <!-- 磁盘缓存位置 -->
    <diskStore path="java.io.tmpdir/ehcache"/>

    <!-- 默认缓存 -->
    <defaultCache
            maxEntriesLocalHeap="10000"
            eternal="false"
            timeToIdleSeconds="120"
            timeToLiveSeconds="120"
            maxEntriesLocalDisk="10000000"
            diskExpiryThreadIntervalSeconds="120"
            memoryStoreEvictionPolicy="LRU">
        <persistence strategy="localTempSwap"/>
    </defaultCache>

    <cache name="ehcache" maxEntriesLocalHeap="10000"
           eternal="false"
           timeToIdleSeconds="120"
           timeToLiveSeconds="120"
           maxEntriesLocalDisk="10000000"
           diskExpiryThreadIntervalSeconds="120"
           memoryStoreEvictionPolicy="LRU">
        <persistence strategy="localTempSwap"/>
    </cache>
    <!--
            name:缓存名称。
            maxElementsInMemory:缓存最大数目
            maxElementsOnDisk:硬盘最大缓存个数。
            eternal:对象是否永久有效,一但设置了,timeout将不起作用。
            overflowToDisk:是否保存到磁盘,当系统宕机时
            timeToIdleSeconds:设置对象在失效前的允许闲置时间(单位:秒)。仅当eternal=false对象不是永久有效时使用,可选属性,默认值是0,也就是可闲置时间无穷大。
            timeToLiveSeconds:设置对象在失效前允许存活时间(单位:秒)。最大时间介于创建时间和失效时间之间。仅当eternal=false对象不是永久有效时使用,默认是0.,也就是对象存活时间无穷大。
            diskPersistent:是否缓存虚拟机重启期数据 Whether the disk store persists between restarts of the Virtual Machine. The default value is false.
            diskSpoolBufferSizeMB:这个参数设置DiskStore(磁盘缓存)的缓存区大小。默认是30MB。每个Cache都应该有自己的一个缓冲区。
            diskExpiryThreadIntervalSeconds:磁盘失效线程运行时间间隔,默认是120秒。
            memoryStoreEvictionPolicy:当达到maxElementsInMemory限制时,Ehcache将会根据指定的策略去清理内存。默认策略是LRU(最近最少使用)。你可以设置为FIFO(先进先出)或是LFU(较少使用)。
            clearOnFlush:内存数量最大时是否清除。
            memoryStoreEvictionPolicy:可选策略有:LRU(最近最少使用,默认策略)、FIFO(先进先出)、LFU(最少访问次数)。
                FIFO,first in first out,这个是大家最熟的,先进先出。
                LFU, Less Frequently Used,就是上面例子中使用的策略,直白一点就是讲一直以来最少被使用的。如上面所讲,缓存的元素有一个hit属性,hit值最小的将会被清出缓存。
                LRU,Least Recently Used,最近最少使用的,缓存的元素有一个时间戳,当缓存容量满了,而又需要腾出地方来缓存新的元素的时候,那么现有缓存元素中时间戳离当前时间最远的元素将被清出缓存。
    -->

</ehcache>

整合springboot-cache

@Configuration
public class EhCacheConf {
    @Bean
    public EhCacheManagerFactoryBean ehCacheManagerFactoryBean() {
        EhCacheManagerFactoryBean cacheManagerFactoryBean = new EhCacheManagerFactoryBean();
        cacheManagerFactoryBean.setConfigLocation(new ClassPathResource("ehcache.xml"));
        cacheManagerFactoryBean.setShared(true);
        return cacheManagerFactoryBean;
    }

    @Bean
    public EhCacheCacheManager eCacheCacheManager(EhCacheManagerFactoryBean bean) {
        return new EhCacheCacheManager(bean.getObject());
    }
}

简单使用

@Service("ehcache")
public class EhcacheServiceImpl implements CacheService {
    private final static String ehcacheName = "ehcache";
    @Autowired
    private EhCacheCacheManager ehCacheCacheManager;

    @Override
    public String get(String key) throws CacheException {
        Cache cache = ehCacheCacheManager.getCacheManager().getCache(ehcacheName);
        if (cache != null) {
            Element element = cache.get(key);
            return element == null ? null : String.valueOf(element.getObjectValue());
        } else {
            throw new CacheException("缓存" + ehcacheName + "异常!");

        }
    }

    @Override
    public void set(String key, String value, int second) throws CacheException {
        Cache cache = ehCacheCacheManager.getCacheManager().getCache(ehcacheName);
        if (cache != null) {

            Element element = new Element(key, value, second, second);

            /**
             * element
             * timeToLiveSeconds=x:缓存自创建日期起至失效时的间隔时间x;
             *
             * timeToIdleSeconds=y:缓存创建以后,最后一次访问缓存的日期至失效之时的时间间隔y;
             * 若自创建缓存后一直都没有访问缓存,那么间隔x后失效,若自创建缓存后有N次访问缓存,那么计算(最后一次访问缓存时间+y ) 即:按照timeToIdleSeconds计算,但总存活时间不超过 y;举个例子:
             *
             * timeToIdleSeconds=120;
             *
             * timeToLiveSeconds=180;
             *
             * 上面的表示此缓存最多可以存活3分钟,如果期间超过2分钟未访问 那么此缓存失效!
             */
            cache.put(element);
        } else {
            throw new CacheException("缓存" + ehcacheName + "异常!");
        }
    }

    @Override
    public void del(String key) throws CacheException {
        Cache cache = ehCacheCacheManager.getCacheManager().getCache(ehcacheName);
        if (cache != null) {
            cache.remove(key);
        } else {
            throw new CacheException("缓存" + ehcacheName + "异常!");

        }
    }
}

参考

一、缓存处理流程

前台请求,后台先从缓存中取数据,取到直接返回结果,取不到时从数据库中取,数据库取到更新缓存,并返回结果,数据库也没取到,那直接返回空结果。

二、缓存穿透

描述:

缓存穿透是指缓存和数据库中都没有的数据,而用户不断发起请求,如发起为id为“-1”的数据或id为特别大不存在的数据。这时的用户很可能是攻击者,攻击会导致数据库压力过大。

解决方案:

接口层增加校验,如用户鉴权校验,id做基础校验,id<=0的直接拦截;
从缓存取不到的数据,在数据库中也没有取到,这时也可以将key-value对写为key-null,缓存有效时间可以设置短点,如30秒(设置太长会导致正常情况也没法使用)。这样可以防止攻击用户反复用同一个id暴力攻击。

三、缓存击穿

描述:

缓存击穿是指缓存中没有但数据库中有的数据(一般是缓存时间到期),这时由于并发用户特别多,同时读缓存没读到数据,又同时去数据库去取数据,引起数据库压力瞬间增大,造成过大压力

解决方案:

设置热点数据长时间有效。
从数据库获取数据时加互斥锁。

四、缓存雪崩

描述:

缓存雪崩是指缓存中数据大批量到过期时间,而查询数据量巨大,引起数据库压力过大甚至down机。和缓存击穿不同的是,缓存击穿指并发查同一条数据,缓存雪崩是不同数据都过期了,很多数据都查不到从而查数据库。

解决方案:

缓存数据的过期时间设置随机,防止同一时间大量数据过期现象发生。
如果缓存数据库是分布式部署,将热点数据均匀分布在不同搞得缓存数据库中。
设置热点数据长时间有效。

留下评论