Spring Boot中使用緩存


在程序中可以使用緩存的技術來節省對數據庫的開銷。Spring Boot對緩存提供了很好的支持,我們幾乎不用做過多的配置即可使用各種緩存實現。這里主要介紹平日里個人接觸較多的Ehcache和Redis緩存實現。

 

准備工作

可根據Spring-Boot中使用Mybatis.html搭建一個Spring Boot項目,然后yml中配置日志輸出級別以觀察SQL的執行情況:

logging:
level:
  com:
    springboot:
      mapper: debug

 

其中com.spring.mapper為MyBatis的Mapper接口路徑。

然后編寫如下測試方法:

@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest(classes = Application.class)
public class ApplicationTest {

  @Autowired
  private StudentService studentService;
   
  @Test
  public void test() throws Exception {
      Student student1 = this.studentService.queryStudentBySno("001");
      System.out.println("學號" + student1.getSno() + "的學生姓名為:" + student1.getName());
       
      Student student2 = this.studentService.queryStudentBySno("001");
      System.out.println("學號" + student2.getSno() + "的學生姓名為:" + student2.getName());
  }
}

 

右鍵run as junit test:

2017-11-17 16:34:26.535 DEBUG 9932 --- [main] c.s.m.StudentMapper.queryStudentBySno    : ==>  Preparing: select * from student where sno=? 
2017-11-17 16:34:26.688 DEBUG 9932 --- [main] c.s.m.StudentMapper.queryStudentBySno   : ==> Parameters: 001(String)
2017-11-17 16:34:26.716 DEBUG 9932 --- [main] c.s.m.StudentMapper.queryStudentBySno   : <==     Total: 1
學號001的學生姓名為:KangKang
2017-11-17 16:34:26.720 DEBUG 9932 --- [main] c.s.m.StudentMapper.queryStudentBySno   : ==> Preparing: select * from student where sno=?
2017-11-17 16:34:26.720 DEBUG 9932 --- [main] c.s.m.StudentMapper.queryStudentBySno   : ==> Parameters: 001(String)
2017-11-17 16:34:26.721 DEBUG 9932 --- [main] c.s.m.StudentMapper.queryStudentBySno   : <==     Total: 1
學號001的學生姓名為:KangKang

 

可發現第二個查詢雖然和第一個查詢完全一樣,但其還是對數據庫進行了查詢。接下來引入緩存來改善這個結果。

使用緩存

要開啟Spring Boot的緩存功能,需要在pom中引入spring-boot-starter-cache

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

 

接着在Spring Boot入口類中加入@EnableCaching注解開啟緩存功能:

@SpringBootApplication
@EnableCaching
public class Application {
  public static void main(String[] args) {
      SpringApplication.run(Application.class,args);
  }
}

 

在StudentService接口中加入緩存注解:

@CacheConfig(cacheNames = "student")
@Repository
public interface StudentService {
  @CachePut(key = "#p0.sno")
  Student update(Student student);
   
  @CacheEvict(key = "#p0", allEntries = true)
  void deleteStudentBySno(String sno);
   
  @Cacheable(key = "#p0")
  Student queryStudentBySno(String sno);
}

 

我們在StudentService接口中加入了@CacheConfig注解,queryStudentBySno方法使用了注解@Cacheable(key="#p0"),即將id作為redis中的key值。當我們更新數據的時候,應該使用@CachePut(key="#p0.sno")進行緩存數據的更新,否則將查詢到臟數據,因為該注解保存的是方法的返回值,所以這里應該返回Student。

其實現類:

@Repository("studentService")
public class StudentServiceImpl implements StudentService{
  @Autowired
  private StudentMapper studentMapper;
   
  @Override
  public Student update(Student student) {
      this.studentMapper.update(student);
      return this.studentMapper.queryStudentBySno(student.getSno());
  }
   
  @Override
  public void deleteStudentBySno(String sno) {
      this.studentMapper.deleteStudentBySno(sno);
  }
   
  @Override
  public Student queryStudentBySno(String sno) {
      return this.studentMapper.queryStudentBySno(sno);
  }
}

 

在Spring Boot中可使用的緩存注解有:

緩存注解

  1. @CacheConfig:主要用於配置該類中會用到的一些共用的緩存配置。在這里@CacheConfig(cacheNames = "student"):配置了該數據訪問對象中返回的內容將存儲於名為student的緩存對象中,我們也可以不使用該注解,直接通過@Cacheable自己配置緩存集的名字來定義;

  2. @Cacheable:配置了queryStudentBySno函數的返回值將被加入緩存。同時在查詢時,會先從緩存中獲取,若不存在才再發起對數據庫的訪問。該注解主要有下面幾個參數:

    • valuecacheNames:兩個等同的參數(cacheNames為Spring 4新增,作為value的別名),用於指定緩存存儲的集合名。由於Spring 4中新增了@CacheConfig,因此在Spring 3中原本必須有的value屬性,也成為非必需項了;

    • key:緩存對象存儲在Map集合中的key值,非必需,缺省按照函數的所有參數組合作為key值,若自己配置需使用SpEL表達式,比如:@Cacheable(key = "#p0"):使用函數第一個參數作為緩存的key值,更多關於SpEL表達式的詳細內容可參考https://docs.spring.io/spring/docs/current/spring-framework-reference/integration.html#cache

    • condition:緩存對象的條件,非必需,也需使用SpEL表達式,只有滿足表達式條件的內容才會被緩存,比如:@Cacheable(key = "#p0", condition = "#p0.length() < 3"),表示只有當第一個參數的長度小於3的時候才會被緩存;

    • unless:另外一個緩存條件參數,非必需,需使用SpEL表達式。它不同於condition參數的地方在於它的判斷時機,該條件是在函數被調用之后才做判斷的,所以它可以通過對result進行判斷;

    • keyGenerator:用於指定key生成器,非必需。若需要指定一個自定義的key生成器,我們需要去實現org.springframework.cache.interceptor.KeyGenerator接口,並使用該參數來指定;

    • cacheManager:用於指定使用哪個緩存管理器,非必需。只有當有多個時才需要使用;

    • cacheResolver:用於指定使用那個緩存解析器,非必需。需通過org.springframework.cache.interceptor.CacheResolver接口來實現自己的緩存解析器,並用該參數指定;

  3. @CachePut:配置於函數上,能夠根據參數定義條件來進行緩存,其緩存的是方法的返回值,它與@Cacheable不同的是,它每次都會真實調用函數,所以主要用於數據新增和修改操作上。它的參數與@Cacheable類似,具體功能可參考上面對@Cacheable參數的解析;

  4. @CacheEvict:配置於函數上,通常用在刪除方法上,用來從緩存中移除相應數據。除了同@Cacheable一樣的參數之外,它還有下面兩個參數:

    • allEntries:非必需,默認為false。當為true時,會移除所有數據;

    • beforeInvocation:非必需,默認為false,會在調用方法之后移除數據。當為true時,會在調用方法之前移除數據。

緩存實現

要使用上Spring Boot的緩存功能,還需要提供一個緩存的具體實現。Spring Boot根據下面的順序去偵測緩存實現:

  • Generic

  • JCache (JSR-107)

  • EhCache 2.x

  • Hazelcast

  • Infinispan

  • Redis

  • Guava

  • Simple

除了按順序偵測外,我們也可以通過配置屬性spring.cache.type來強制指定。

接下來主要介紹基於Redis和Ehcache的緩存實現。

Redis

Redis的下載地址為https://github.com/MicrosoftArchive/redis/releases,Redis 支持 32 位和 64 位。這個需要根據你系統平台的實際情況選擇,這里我們下載 Redis-x64-xxx.zip壓縮包到C盤。打開一個CMD窗口,輸入如下命令:

C:\Users\Administrator>cd c:\Redis-x64-3.2.100

c:\Redis-x64-3.2.100>redis-server.exe redis.windows.conf
              _._
          _.-``__ ''-._
    _.-``   `. `_. ''-._           Redis 3.2.100 (00000000/0) 64 bit
.-`` .-```. ```\/   _.,_ ''-._
(   '     ,       .-` | `,   )     Running in standalone mode
|`-._`-...-` __...-.``-._|'` _.-'|     Port: 6379
|   `-._   `._   /     _.-'   |     PID: 6404
`-._   `-._ `-./ _.-'   _.-'
|`-._`-._   `-.__.-'   _.-'_.-'|
|   `-._`-._       _.-'_.-'   |           http://redis.io
`-._   `-._`-.__.-'_.-'   _.-'
|`-._`-._   `-.__.-'   _.-'_.-'|
|   `-._`-._       _.-'_.-'   |
`-._   `-._`-.__.-'_.-'   _.-'
    `-._   `-.__.-'   _.-'
        `-._       _.-'
            `-.__.-'

[6404] 25 Dec 09:47:58.890 # Server started, Redis version 3.2.100
[6404] 25 Dec 09:47:58.898 * DB loaded from disk: 0.007 seconds
[6404] 25 Dec 09:47:58.898 * The server is now ready to accept connections on port 6379

 

然后打開另外一個CMD終端,輸入:

C:\Users\Administrator>cd c:\Redis-x64-3.2.100

c:\Redis-x64-3.2.100>redis-cli.exe -p 6379
127.0.0.1:6379>

 

准備工作做完后,接下來開始在Spring Boot項目里引入Redis:

<!-- spring-boot redis -->
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

 

在application.yml中配置Redis:

spring:
redis:
  # Redis數據庫索引(默認為0)
  database: 0
  # Redis服務器地址
  host: localhost
  # Redis服務器連接端口
  port: 6379
  pool:
    # 連接池最大連接數(使用負值表示沒有限制)
    max-active: 8
    # 連接池最大阻塞等待時間(使用負值表示沒有限制)
    max-wait: -1
    # 連接池中的最大空閑連接
    max-idle: 8
    # 連接池中的最小空閑連接
    min-idle: 0
  # 連接超時時間(毫秒)
  timeout: 0

 

更多關於Spring Boot Redis配置可參考:https://docs.spring.io/spring-boot/docs/current/reference/html/common-application-properties.html# REDIS

接着創建一個Redis配置類:

@Configuration
public class RedisConfig extends CachingConfigurerSupport {

  // 自定義緩存key生成策略
  @Bean
  public KeyGenerator keyGenerator() {
      return new KeyGenerator() {
          @Override
          public Object generate(Object target, java.lang.reflect.Method method, Object... params) {
              StringBuffer sb = new StringBuffer();
              sb.append(target.getClass().getName());
              sb.append(method.getName());
              for (Object obj : params) {
                  sb.append(obj.toString());
              }
              return sb.toString();
          }
      };
  }

  // 緩存管理器
  @Bean
  public CacheManager cacheManager(@SuppressWarnings("rawtypes") RedisTemplate redisTemplate) {
      RedisCacheManager cacheManager = new RedisCacheManager(redisTemplate);
      // 設置緩存過期時間(秒)
      cacheManager.setDefaultExpiration(3600);
      return cacheManager;
  }

  @Bean
  public RedisTemplate<String, String> redisTemplate(RedisConnectionFactory factory) {
      StringRedisTemplate template = new StringRedisTemplate(factory);
      setSerializer(template);// 設置序列化工具
      template.afterPropertiesSet();
      return template;
  }

  private void setSerializer(StringRedisTemplate template) {
      @SuppressWarnings({ "rawtypes", "unchecked" })
      Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
      ObjectMapper om = new ObjectMapper();
      om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
      om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
      jackson2JsonRedisSerializer.setObjectMapper(om);
      template.setValueSerializer(jackson2JsonRedisSerializer);
  }
}

 

運行測試,控制台輸出:

2017-11-17 18:17:06.995 DEBUG 8836 --- [main] c.s.m.StudentMapper.queryStudentBySno    : ==>  Preparing: select * from student where sno=? 
2017-11-17 18:17:07.128 DEBUG 8836 --- [main] c.s.m.StudentMapper.queryStudentBySno   : ==> Parameters: 001(String)
2017-11-17 18:17:07.152 DEBUG 8836 --- [main] c.s.m.StudentMapper.queryStudentBySno   : <==     Total: 1
學號001的學生姓名為:KangKang
學號001的學生姓名為:KangKang

 

第二次查詢沒有訪問數據庫,而是從緩存中獲取的,在redis中查看該值:

127.0.0.1:6379>   keys *
1) "student~keys"
2) "001"
127.0.0.1:6379> get 001
"[\"com.springboot.bean.Student\",{\"sno\":\"001\",\"name\":\"KangKang\",\"sex\":\"M \"}]"

 

在測試方法中測試更新:

@Test
public void test() throws Exception {
  Student student1 = this.studentService.queryStudentBySno("001");
  System.out.println("學號" + student1.getSno() + "的學生姓名為:" + student1.getName());
   
  student1.setName("康康");
  this.studentService.update(student1);
   
  Student student2 = this.studentService.queryStudentBySno("001");
  System.out.println("學號" + student2.getSno() + "的學生姓名為:" + student2.getName());
}

 

控制台輸出:

學號001的學生姓名為:KangKang
2017-11-17 19:30:05.813 INFO 11244 --- [main] com.alibaba.druid.pool.DruidDataSource   : {dataSource-1} inited
2017-11-17 19:30:05.823 DEBUG 11244 --- [main] c.s.mapper.StudentMapper.update         : ==> Preparing: update student set sname=?,ssex=? where sno=?
2017-11-17 19:30:05.941 DEBUG 11244 --- [main] c.s.mapper.StudentMapper.update         : ==> Parameters: 康康(String), M (String), 001(String)
2017-11-17 19:30:05.953 DEBUG 11244 --- [main] c.s.mapper.StudentMapper.update         : <==   Updates: 1
2017-11-17 19:30:05.957 DEBUG 11244 --- [main] c.s.m.StudentMapper.queryStudentBySno   : ==> Preparing: select * from student where sno=?
2017-11-17 19:30:05.959 DEBUG 11244 --- [main] c.s.m.StudentMapper.queryStudentBySno   : ==> Parameters: 001(String)
2017-11-17 19:30:05.976 DEBUG 11244 --- [main] c.s.m.StudentMapper.queryStudentBySno   : <==     Total: 1
學號001的學生姓名為:康康

 

在redis中查看:

127.0.0.1:6379> get 001
"[\"com.springboot.bean.Student\",{\"sno\":\"001\",\"name\":\"\xe5\xba\xb7\xe5\xba\xb7\",\"sex\":\"M \"}]"

 

可見更新數據庫的同時,緩存也得到了更新。

源碼鏈接:https://github.com/wuyouzhuguli/Spring-Boot-Demos/tree/master/09.Spring-Boot-Redis-Cache

Ehcache

引入Ehcache依賴:

<!-- ehcache -->
<dependency>
  <groupId>net.sf.ehcache</groupId>
  <artifactId>ehcache</artifactId>
</dependency>

 

在src/main/resources目錄下新建ehcache.xml:

<?xml version="1.0" encoding="UTF-8"?>
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      xsi:noNamespaceSchemaLocation="ehcache.xsd">
  <defaultCache
      maxElementsInMemory="10000"
      eternal="false"
      timeToIdleSeconds="3600"
      timeToLiveSeconds="0"
      overflowToDisk="false"
      diskPersistent="false"
      diskExpiryThreadIntervalSeconds="120" />

  <cache
      name="student"
      maxEntriesLocalHeap="2000"
      eternal="false"
      timeToIdleSeconds="3600"
      timeToLiveSeconds="0"
      overflowToDisk="false"
      statistics="true"/>
</ehcache>

 

關於Ehcahe的一些說明:

  • name:緩存名稱。

  • maxElementsInMemory:緩存最大數目

  • maxElementsOnDisk:硬盤最大緩存個數。

  • eternal:對象是否永久有效,一但設置了,timeout將不起作用。

  • overflowToDisk:是否保存到磁盤。

  • timeToIdleSeconds:設置對象在失效前的允許閑置時間(單位:秒)。僅當eternal=false對象不是永久有效時使用,可選屬性,默認值是0,也就是可閑置時間無窮大。

  • timeToLiveSeconds:設置對象在失效前允許存活時間(單位:秒)。最大時間介於創建時間和失效時間之間。僅當eternal=false對象不是永久有效時使用,默認是0,也就是對象存活時間無窮大。

  • diskPersistent:是否緩存虛擬機重啟期數據,默認值為false。

  • diskSpoolBufferSizeMB:這個參數設置DiskStore(磁盤緩存)的緩存區大小。默認是30MB。每個Cache都應該有自己的一個緩沖區。

  • diskExpiryThreadIntervalSeconds:磁盤失效線程運行時間間隔,默認是120秒。

  • memoryStoreEvictionPolicy:當達到maxElementsInMemory限制時,Ehcache將會根據指定的策略去清理內存。默認策略是LRU(最近最少使用)。你可以設置為FIFO(先進先出)或是LFU(較少使用)。

  • clearOnFlush:內存數量最大時是否清除。

  • memoryStoreEvictionPolicy:Ehcache的三種清空策略:FIFO,first in first out,這個是大家最熟的,先進先出。LFU, Less Frequently Used,就是上面例子中使用的策略,直白一點就是講一直以來最少被使用的。如上面所講,緩存的元素有一個hit屬性,hit值最小的將會被清出緩存。LRU,Least Recently Used,最近最少使用的,緩存的元素有一個時間戳,當緩存容量滿了,而又需要騰出地方來緩存新的元素的時候,那么現有緩存元素中時間戳離當前時間最遠的元素將被清出緩存。

接着在application.yml中指定ehcache配置的路徑:

spring:
cache:
  ehcache:
    config: 'classpath:ehcache.xml'

 

這樣就可以開始使用ehcache了,運行測試類,觀察控制台:

2017-11-18 09:10:40.201 DEBUG 3364 --- [main] c.s.m.StudentMapper.queryStudentBySno    : ==>  Preparing: select * from student where sno=? 
2017-11-18 09:10:40.343 DEBUG 3364 --- [main] c.s.m.StudentMapper.queryStudentBySno   : ==> Parameters: 001(String)
2017-11-18 09:10:40.364 DEBUG 3364 --- [main] c.s.m.StudentMapper.queryStudentBySno   : <==     Total: 1
學號001的學生姓名為:KangKang
學號001的學生姓名為:KangKang

 

可看到第二次是從緩存中獲取的。

測試更新:

2017-11-18 09:18:04.230 DEBUG 11556 --- [main] c.s.m.StudentMapper.queryStudentBySno    : ==>  Preparing: select * from student where sno=? 
2017-11-18 09:18:04.397 DEBUG 11556 --- [main] c.s.m.StudentMapper.queryStudentBySno   : ==> Parameters: 001(String)
2017-11-18 09:18:04.427 DEBUG 11556 --- [main] c.s.m.StudentMapper.queryStudentBySno   : <==     Total: 1
學號001的學生姓名為:KangKang
2017-11-18 09:18:04.433 DEBUG 11556 --- [main] c.s.mapper.StudentMapper.update         : ==> Preparing: update student set sname=?,ssex=? where sno=?
2017-11-18 09:18:04.438 DEBUG 11556 --- [main] c.s.mapper.StudentMapper.update         : ==> Parameters: 康康(String), M (String), 001(String)
2017-11-18 09:18:04.440 DEBUG 11556 --- [main] c.s.mapper.StudentMapper.update         : <==   Updates: 1
2017-11-18 09:18:04.440 DEBUG 11556 --- [main] c.s.m.StudentMapper.queryStudentBySno   : ==> Preparing: select * from student where sno=?
2017-11-18 09:18:04.441 DEBUG 11556 --- [main] c.s.m.StudentMapper.queryStudentBySno   : ==> Parameters: 001(String)
2017-11-18 09:18:04.442 DEBUG 11556 --- [main] c.s.m.StudentMapper.queryStudentBySno   : <==     Total: 1
學號001的學生姓名為:康康

 

可見,即使更新方法加了@CachePut注解,第二次查詢因為Student對象更新了,其是從數據庫獲取數據的,所以對於Ehcache來說,更新方法加不加@CachePut注解,結果都一樣。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM