缓存-SpringCache

为什么要用缓存

我们一定听说过"缓存无敌"的话,特别是在大型互联网公司,"查多写少"的场景屡见不鲜。网络上查到的很多诸如系统吞吐量提升50%、接口耗时降低80%、一个分钟级别的程序优化到毫秒级别等,多多少少和缓存有关。

举个例子:在我们程序中,很多配置数据(例如一个商品信息、一个白名单、一个第三方客户的回调接口),这些数据存在我们的DB上,数据量比较少,但是程序访问很频繁,这种情况下,将数据放一份到我们的内存缓存中将大大提升我们系统的访问效率,因为减少了数据库访问,有可能减少了数据库建连时间、网络数据传输时间、数据库磁盘寻址时间……

总的来说,下面这些场景都可以考虑使用缓存优化性能:1、查数据库2、读取文件3、网络访问,特别是调用第三方服务查询接口

Spring Cache介绍

Spring Cache 是Spring 提供的一整套的缓存解决方案,它不是具体的缓存实现,它只提供一整套的接口和代码规范、配置、注解等,用于整合各种缓存方案,比如Caffeine、Guava Cache、Ehcache。

如果我们没有使用Spring Cache,而是直接使用Guava Cache,我们的代码可能得像下面这么写:

方便复制,我把源码贴出来:

import com.alibaba.fastjson.JSON;
import com.google.common.base.Stopwatch;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.tin.example.library.BookEntity;
import com.tin.example.library.BookService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
​
import java.util.concurrent.TimeUnit;
​
/**
 * title: CacheService
 * <p>
 * description:
 *
 * @author  @【林在闪闪发光】 
 */
@Service
public class MyGuavaCacheService implements InitializingBean {
    private static final Logger LOGGER = LoggerFactory.getLogger(MyGuavaCacheService.class);
    private static LoadingCache<String, BookEntity> CACHE;
​
    @Autowired
    private BookService bookService;
​
    public void query() {
        String bookNamePrefix = "00";
        String bookNameSuffix = "号藏书";
        for (int i = 0; i < 3; i++) {
            String bookName = bookNamePrefix + i + bookNameSuffix;
            for (int j = 0; j < 2; j++) {
                queryFromCache(bookName);
            }
        }
​
        //查看缓存状态
        LOGGER.info("cache stats:{}", CACHE.stats().toString());
    }
​
    public void queryFromCache(String bookName) {
        try {
            Stopwatch stopwatch = Stopwatch.createStarted();
            BookEntity bookEntity = CACHE.get(bookName);
            LOGGER.info("query:{},cost:{}ms,book:{}",
                    bookName, stopwatch.elapsed(TimeUnit.MILLISECONDS), JSON.toJSONString(bookEntity));
        } catch (Exception e) {
            LOGGER.error("cache read error. bookName:{}", bookName, e);
        }
    }
​
    /**
     * spring容器启动时初始化guava cache
     */
    @Override
    public void afterPropertiesSet() throws Exception {
        CACHE = CacheBuilder.newBuilder()
                //并发级别=8,并发级别表示可以同时写缓存的线程数
                .concurrencyLevel(8)
                //设置缓存容器的初始容量为50
                .initialCapacity(50)
                //设置缓存最大容量为100,超过100之后就会按照LRU最近最少使用移除缓存项
                .maximumSize(100)
                //设置写缓存后100毫秒后过期
                .expireAfterWrite(100, TimeUnit.MILLISECONDS)
                //统计缓存情况,生产环境慎重使用
                .recordStats()
                //build方法中可以指定CacheLoader,在缓存不存在时通过CacheLoader的实现自动加载缓存
                .build(new BookEntityCacheLoader());
    }
​
    /**
     * 缓存不存在或者过期时触发load方法回源更新缓存
     */
    public class BookEntityCacheLoader extends CacheLoader<String, BookEntity> {
        @Override
        public BookEntity load(String key) {
            try {
                return bookService.findByBookName(key);
            } catch (Exception e) {
                LOGGER.error("cache load error. key:{}", key);
                return null;
            }
        }
    }
}

只有一个缓存这么写看着也还行,但如果有很多缓存,每个缓存都这样写,使用起来就复杂了,和我们的业务代码严重耦合。

这个时候就用到了Spring Cache,Spring Cache并不是缓存的实现,而是缓存使用的一种方式,其基于注解和Spring高级特性提供缓存读写以及失效刷新等各种能力

Spring Cache默认支持几个缓存实现,如下图jar包(spring-context-support 5.3.14版本)所示:

这三个包只是Spring的support(类似于适配器),真正的缓存实现是需要手动依赖jar的,后文我会举例讲到。 

EhCache:纯Java进程内缓存框架,也是Hibernate、MyBatis默认的缓存提供。
Caffeine:使用Java8对Guava缓存的重写版本,从Spring5开始,Spring默认删除了Guava而使用Caffeine,支持多种缓存过期策略。
jcache:实现了JSR107规范的三方缓存都可以通过此包得到适配。

Spring Cache使用入门 

Spring Cache依赖Spring的天然优势——AOP,我们只需要显式地在代码中调用第三方接口,在方法上加上注解,就可以实现把获取到的结果后把结果插入缓存内,在下一次查询的时候优先从缓存中读取数据。

使用Spring Cache也比较简单,简单总结就是3步:加依赖,开启缓存、加注解。

一、加依赖

maven:

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

二、开启缓存

需要在启动类加上@EnableCaching注解才能启动使用Spring Cache,比如:

import com.tin.example.service.MyGuavaCacheService;
import com.tin.example.service.SpringCacheService;
import com.tin.example.util.SpringContextUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;
​
/**
 * title: Application
 * <p>
 * description:
 *
 * @author tin 林在闪闪发光 
 */
@SpringBootApplication
@EnableCaching
public class Application {
    private static final Logger LOGGER = LoggerFactory.getLogger(Application.class);
​
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
        LOGGER.info("容器启动成功... ");
​
        SpringCacheService springCacheService = SpringContextUtil.getBean(SpringCacheService.class);
        springCacheService.query();
​
//        MyGuavaCacheService myGuavaCacheService = SpringContextUtil.getBean(MyGuavaCacheService.class);
//        myGuavaCacheService.query();
    }
}

三、加注解

在需要缓存返回结果的方法上加上注解@Cacheable即可,比如:

我们做个测试,假设查询一本书籍耗时100毫秒

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
​
import java.util.List;
​
/**
 * title: LibraryService
 * <p>
 * description:
 *
 * @author tin 林在闪闪发光
 */
@Service
public class BookService extends AbstractLibraryService {
​
    @Autowired
    private BookStore bookStore;
​
    @Override
    public BookEntity findByBookName(String bookName) {
        //模拟查询耗时100毫秒
        sleep4Millis(100);
​
        if (!bookStore.hasBook()) {
            return null;
        }
        List<BookEntity> allBook = bookStore.getBookStore();
        for (BookEntity bookEntity : allBook) {
            if (bookEntity == null) {
                continue;
            }
            if (bookEntity.getBookName() != null && bookEntity.getBookName().contains(bookName)) {
                return bookEntity;
            }
        }
        return null;
    }
​
    @Cacheable("library")
    @Override
    public BookEntity findByBookNameWithSpringCache(String bookName) {
        return findByBookName(bookName);
    }
}

加上Spirng Cache缓存后,可以明显地发现第二次查询同一本书耗时0ms 

这说明缓存已经生效了。

接入Caffeine缓存实现框架

上文讲到了Spring Cache支持三种缓存实现,在使用我们上面所说"三步"引入使用Spring Cache,我们究竟使用的是哪种缓存实现呢?

通过CacheManager打印看一下:

 ConcurrentMapCache是Spring 内置默认的缓存实现。如果需要使用CaffeineCache,需要额外引入CaffeineCache包,同时生成一个CaffeineCacheManager的bean。

maven依赖:

CaffeineCacheManager生成:

import com.github.benmanes.caffeine.cache.Caffeine;
import org.springframework.cache.CacheManager;
import org.springframework.cache.caffeine.CaffeineCacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
​
import java.util.concurrent.TimeUnit;
​
/**
 * title: CaffeineCacheConfig
 * <p>
 * description:
 *
 * @author tin @林在闪闪发光
 */
@Configuration
public class CaffeineCacheConfig {
    @Bean
    public CacheManager cacheManager() {
        CaffeineCacheManager cacheManager = new CaffeineCacheManager();
        cacheManager.setCaffeine(Caffeine.newBuilder()
                .expireAfterWrite(10, TimeUnit.MINUTES)
                .initialCapacity(100)
                .maximumSize(10000))
        ;
        return cacheManager;
    }
}

常用注解

Spring Cache比较常用的几个注解:@Cacheable、 @CacheConfig、@CacheEvict、@CachePut、@Caching、@EnableCaching。spring-context依赖包下也能看到注解的定义。

除了CacheConfig只能用于类上,其余的都可以用在类或者方法上,用在方法上好理解,缓存方法结果,如果用在类上,就相当于对该类的所有可以缓存的方法(需要是public方法)加上注解。

@Cacheable

@Cacheble注解表示这个方法的结果可以被缓存,调用该方法前,会先检查对应的缓存key在缓存中是否已经有值,如果有就直接返回,不调用方法,如果没有,就会调用方法,同时把结果缓存起来。

@CacheConfig

有些配置可能又是一个类通用的,这种情况就可以使用@CacheConfig了,它是一个类级别的注解,可以在类级别上配置cacheNames、keyGenerator、cacheManager、cacheResolver等。

@CachePut

@CachePut注解修饰的方法,会把方法的返回值put到缓存里面缓存起来,它只是触发put的动作,和@Cacheable不同,不会读取缓存,put到缓存的值进程内其他场景的使用者就可以使用了。

@CacheEvict

@CacheEvict注解修饰的方法,会触发缓存的evict操作,清空缓存中指定key的值。

@Caching

@Caching能够支持多个缓存注解生效。

因为Java方法上相同类型注解只能有一个有效,在我们有些场景下需要多个注解操作,特别是CacheEvict删除缓存,我们可能需要同时删除多份缓存值,这个后@Ocaching就有用途了。 

我是林,一个在努力让自己变得更优秀的普通人。自己阅历有限、学识浅薄,如有发现文章不妥之处,非常欢迎向我提出,我一定细心推敲加以修改。

坚持创作不容易,你的反馈是我坚持输出的最强大动力,谢谢!

猜你喜欢

转载自blog.csdn.net/weixin_68829137/article/details/127164634