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;
        }
    

    参考

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