SpringBoot學習小結之緩存

前提

使用緩存的好處有:

  • 在一個系統中,數據庫中經常會有一些不怎麼變動的數據,舉個栗子:省市信息,不會經常變動。還有一些經過數據庫耗時計算得到的結果,也可以存入緩存。使用緩存的主要好處就是減少數據庫操作,減輕了數據庫壓力,提升系統性能。
  • 由於用戶請求和數據庫之間增加了緩存這一層,而緩存數據處於內存中,相比較而言數據庫是讀取磁盤文件,速度自然比緩存慢。使用緩存大大提高了系統對請求的響應速度,提升用戶感知。
  • 緩存是支持高併發的主要策略

壞處有:

  • 緩存是以空間換時間的策略,大量使用會佔用大量內存
  • 高併發情況下存在緩存和數據庫數據不一致
  • 內存容量小,而且比硬盤貴

下面以SpringBoot2.1.6.RELEASE爲例,學習緩存的使用

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

註解

SpringBoot 提供幾個註解實現緩存功能, 通過AOP方式實現

序號 註解名 含義
1 @EnableCaching 開啓緩存註解
2 @Cacheable 如果能從緩存拿到數據,直接用緩存的
如果拿不到,執行方法,並將結果進行緩存
3 @CachePut 執行方法,並將結果存入緩存,緩存已存在則更新
4 @CacheEvict 執行方法後觸發刪除緩存
5 @Caching 用於組合多種緩存策略
6 @CacheConfig 設置類級別緩存通用屬性

Cacheable提供以下幾個屬性更細粒度控制緩存

序號 屬性名 含義
1 value/cacheNames 緩存名字
2 key 緩存的key生成規則,spel表達式
3 keyGenerator 自定義key生成策略
4 cacheManager 自定義緩存管理器,默認ConcurrentMapCacheManager
5 cacheResolver 自定義緩存解析器, 默認SimpleCacheResolver
6 condition 緩存條件,傳入參數經過condition 判斷爲true緩存,spel表達式
7 unless 和condition類似,不過unless是在方法執行完畢後對結果進行判斷,是否加入緩存, spel表達式(常用#result)
8 sync 應用多線程環境下,多個線程key相同,只會有一個線程對緩存讀寫,其他阻塞

StringBoot @Cacheable key針對方法參數默認生成策略爲:

  • 無參數,key爲SimpleKey.EMPTY
  • 單個參數,key就是參數
  • 多個參數,key是SimpleKey 實例,參數注入SimpleKey的params屬性

key和keyGenerator只能指定一個,互斥

例子

@SpringBootApplication
@EnableCaching
public class SpringbootCacheApplication {
    public static void main(String[] args) {
        SpringApplication.run(SpringbootCacheApplication.class, args);
    }
}
@Data
public class User implements Serializable {
    private String name;
    private String password;
    private Integer age;
}
@RestController
public class BaseController {
    private static final Logger logger = LoggerFactory.getLogger(BaseController.class);
    
    @GetMapping("/test")
    @Cacheable(value = "cache-test")
	public Object test() {
        logger.info("進入test");
        User user = new User();
        user.setName("張三");
        user.setAge(12);
        user.setPassword("11111");
        users.add(user);
        return user;
    }
    @GetMapping("/cacheEvict")
    @CacheEvict(value = "cache-test")
    public Object cacheEvict() {
        logger.info("----進入CacheEvict------");
        return new ArrayList<>();
    }
    @GetMapping("/cachePut")
    @CachePut(value = "cache-test")
    public Object cachePut() {
       logger.info("----進入cachePut------");
        User user = new User();
        user.setName("王五");
        user.setAge(19);
        user.setPassword("119811");
        return user;
    }
    
    @GetMapping("/testKey")
    // 將參數name和key拼接作爲key
    @Cacheable(value = "cache-test", key = "#name + #age")
    public Object cacheKey(String name, Integer age) {
        logger.info("-----進入cacheKey------");
        User user = new User();
        user.setName(name);
        user.setAge(age);
        user.setPassword("11111");
        return user;
    }
    @GetMapping("/testCondition")
    // 當age > 10,將結果加入緩存
    @Cacheable(value = "cache-test", condition = "#age > 10")
    public Object cacheCondition(String name, Integer age) {
        logger.info("----進入cacheCondition-----");
        User user = new User();
        user.setName(name);
        user.setAge(age);
        user.setPassword("11111");
        return user;
    }
    
    @GetMapping("/testUnless")
    // 如果方法執行結果User.age > 10就不緩存
    @Cacheable(value = "cache-test", unless = "#result.age > 10")
    public Object cacheUnless(String name, Integer age) {
        logger.info("-----進入cacheUnless------");
        User user = new User();
        user.setName(name);
        user.setAge(age);
        user.setPassword("11111");
        return user;
    }
    
    @GetMapping("/testSync")
	// 當sync = true,三個線程key相同,只有一個對緩存讀寫,讀寫完後其他線程從緩存取,所以只會打印一次-----進入cacheSync----
    // 當sync = false時,三個線程key相同,判斷緩存爲空,同時進入方法,會打印三次-----進入cacheSync----
    @Cacheable(value = "cache-test", sync = true)
    public Object cacheSync(String name, Integer age) {
        logger.info("----進入cacheSync----");
        User user = new User();
        user.setName(name);
        user.setAge(age);
        user.setPassword("11111");
        return user;
    }
}
@RunWith(SpringRunner.class)
@SpringBootTest
public class SpringbootCacheApplicationTests {

    private static final Logger logger = LoggerFactory.getLogger(SpringbootCacheApplicationTests.class);

    @Test
    public void test() {
        // 訪問url,根據開啓端口,需要修改端口號
        String requestUrl = "http://localhost:8088/testSync?name=1&age=2";
        RestTemplate rt = new RestTemplate();
        new Thread(() -> {
            ResponseEntity<User> forEntity = rt.getForEntity(requestUrl, User.class);
            logger.info("Thread1:{}",forEntity);
        }).start();
        new Thread(() -> {
            ResponseEntity<User> forEntity = rt.getForEntity(requestUrl, User.class);
            logger.info("Thread2:{}",forEntity);
        }).start();
        new Thread(() -> {
            ResponseEntity<User> forEntity = rt.getForEntity(requestUrl, User.class);
            logger.info("Thread3:{}",forEntity);
        }).start();

        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

默認支持

SpringBoot 內置對一些緩存的默認支持,SpringBoot也是按照以下順序對緩存的提供者進行檢測

  1. Generic
  2. JCache (JSR-107) (EhCache 3, Hazelcast, Infinispan, and others)
  3. EhCache 2.x
  4. Hazelcast
  5. Infinispan
  6. Couchbase
  7. Redis
  8. Caffeine
  9. Simple

ConcurrenMap

  • ConcurrentMap屬於上面順序的第9位Simple,是SpringBoot對緩存的默認實現
  • 實現類是org.springframework.cache.concurrent.ConcurrentMapCache

Ehcache

  • pom依賴

    <dependency>
       <groupId>net.sf.ehcache</groupId>
       <artifactId>ehcache</artifactId>
       <version>2.10.6</version>
    </dependency>
    
  • 以ehcache 2.10.6爲例,默認需要將配置文件ehcache.xml放在resource目錄下

    <?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 path="java.io.tmpdir/ehcache"/>
    
    <!-- maxEntriesLocalHeap 當前緩存在堆內存上所能保存的最大元素數量 -->
    <!-- eternal 是否永遠不過期 -->
    <!-- timeToIdleSeconds 對象空閒時,指對象在多長時間沒有被訪問就會失效。只對eternal爲false的有效。默認值爲0。(單位:秒) -->
    <!-- timeToLiveSeconds 對象存活時間,指對象從創建到失效所需要的時間。只對eternal爲false的有效。默認值爲0,表示一直可以訪問。(單位:秒)-->
    <!-- maxEntriesLocalDisk 在磁盤上所能保存的元素的最大數量-->
    <!-- diskExpiryThreadIntervalSeconds 對象檢測線程運行時間間隔。標識對象狀態的線程多長時間運行一次。默認是120秒。(單位:秒)-->
    <!-- memoryStoreEvictionPolicy 如果內存中數據超過內存限制,向磁盤緩存定時的策略,默認值爲LRU -->
    <!-- overflowToDisk 如果內存中數據超過內存限制,是否要緩存到磁盤上 -->
    <!-- 默認緩存 -->
    <defaultCache
          maxEntriesLocalHeap="10000" 
          eternal="false" 
          timeToIdleSeconds="120"
          timeToLiveSeconds="120"
          maxEntriesLocalDisk="10000000"
          diskExpiryThreadIntervalSeconds="120"
          memoryStoreEvictionPolicy="LRU">
    <persistence strategy="localTempSwap"/>
    </defaultCache>
    <!-- 自定義緩存 -->
    <cache name="cache-test"
         maxElementsInMemory="1000"
         eternal="false"
         timeToIdleSeconds="5"
         timeToLiveSeconds="5"
         overflowToDisk="false"
         memoryStoreEvictionPolicy="LRU"/>
    </ehcache>
    
  • 也可以自己配置文件名和路徑,需要配置spring.cache.ehcache.config=classpath:config/another-config.xml

  • 注意配置文件中cache name需要和@cacheablevalue名字保持一致

  • ehcache比ConcurrenMap的優勢有:

    • 可以配置緩存過期時間
    • 持久化緩存數據到硬盤
    • 等等

Redis

  • pom依賴

    <dependency>
    	<groupId>org.springframework.boot</groupId>
    	<artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
    
  • SpringBoot支持Redis配置

    # Redis服務器地址
    spring.redis.host=127.0.0.1
    # Redis服務器連接端口
    spring.redis.port=6379
    # Redis服務器連接密碼
    spring.redis.password=
    # 連接池最大連接數(使用負值表示沒有限制)
    spring.redis.jedis.pool.max-active=20
    # 連接池最大阻塞等待時間(使用負值表示沒有限制)
    spring.redis.jedis.pool.max-wait=-1
    # 連接池中的最大空閒連接
    spring.redis.jedis.pool.max-idle=10
    # 連接池中的最小空閒連接
    spring.redis.jedis.pool.min-idle=0
    # 連接超時時間(毫秒)
    spring.redis.timeout=1000
    
  • 默認緩存到Redis的類需要實現Serializable接口

  • 更細粒度的配置可以通過@Configuration @Bean配置RedisCacheManagerRedisCacheConfiguration實現

    @Configuration
    public class RedisConfig {
    
    	// 自定義redis中key生成策略
        @Bean
        public KeyGenerator simpleKeyGenerator() {
            return (o, method, objects) -> {
                StringBuilder stringBuilder = new StringBuilder();
                stringBuilder.append(o.getClass().getSimpleName());
                stringBuilder.append(".");
                stringBuilder.append(method.getName());
                stringBuilder.append("[");
                for (Object obj : objects) {
                    stringBuilder.append(obj.toString());
                }
                stringBuilder.append("]");
    
                return stringBuilder.toString();
            };
        }
    	// 配置redisCacheManager
        @Bean
        public CacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) {
            return new RedisCacheManager(
                    RedisCacheWriter.nonLockingRedisCacheWriter(redisConnectionFactory),
                    this.getRedisCacheConfigurationWithTtl(600), // 默認策略,未配置的 key 會使用這個
                    this.getRedisCacheConfigurationMap() // 指定 key 策略
            );
        }
    	// 配置不同的緩存名字,不同的策略
        private Map<String, RedisCacheConfiguration> getRedisCacheConfigurationMap() {
            Map<String, RedisCacheConfiguration> redisCacheConfigurationMap = new HashMap<>();
            
            redisCacheConfigurationMap.put("redis-test", this.getRedisCacheConfigurationWithTtl(3000));
            redisCacheConfigurationMap.put("redis-test-1", this.getRedisCacheConfigurationWithTtl(18000));
    
            return redisCacheConfigurationMap;
        }
    	// 使用jackson配置redis 序列化策略
        private RedisCacheConfiguration getRedisCacheConfigurationWithTtl(Integer seconds) {
            Jackson2JsonRedisSerializer<Object> 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);
    
            RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig();
            redisCacheConfiguration = redisCacheConfiguration.serializeValuesWith(
                    RedisSerializationContext
                            .SerializationPair
                            .fromSerializer(jackson2JsonRedisSerializer)
            ).entryTtl(Duration.ofSeconds(seconds));
    
            return redisCacheConfiguration;
        }
    

    參考

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章