springboot项目如何进行缓存配置-cacheManager详解

3,346 阅读4分钟

本文主要讲解springboot项目如何进行缓存配置

  • 分布式缓存
  • 本地缓存
  • 自定义缓存

其实更重要的是这三种缓存的使用场景,我先详细说下

  • 分布式缓存 redis 基本上算是最常见的缓存模式了,一般项目都会接入 redis 缓存 ,在登陆授权,菜单鉴权,数据缓存如商品详情缓存等场景,一般都是分布式redis 解决不了的问题 才会考虑本地缓存 和自定义缓存
  • 本地缓存caffieine ,使用场景 无疑是高并发 场景, redis 虽然解决了分布式数据一致性问题,但是 通过tcp网络通信,在高并发场景,响应速度不及本地缓存,bigKey 对 redis本身性能影响很大,同时占用了大量的网络带宽,还会影响其他服务的通信质量,很容易成为性能瓶颈,如商品详情,店铺自定义页,系统参数查询 这种高频访问,不经常修改 或占用很多内存大小的数据, 本地缓存无疑是最好的,简单一句就是 hotkey bigKey 都可以考虑使用本地缓存 对系统优化有奇效
  • 接下来要说的是 自定义缓存, 自定义缓存其实就是为了解决分布式缓存和本地缓存 各自的缺点,同时又想使用各自的优点,怎么办那就自定义实现,实现本地缓存的一致性通信,解决本地缓存的一致性问题,以及分布式缓存的性能问题,同时还可以加入hotkey热点探测 能功能,选择性的将hotkey放在本地,以及 对缓存访问进行指标收集,告警通知,已经自定义接口实现,屏蔽掉一些 危险命令 如 keys等,能做的事情很多,所以自定义缓存无疑是最好的,更灵活高效,同时也更考验程序员的能力

分布式缓存-RedisCacheManager 配置

一般来说 平时的springboot项目都是接入接入redis 来作为CacheManager 实现 ,正常的配置文件如下: 配置 一个RedisTemplate 和 CacheManager ,回头你再搞个redisUtil 封装下RedisTemplate的方法就行了

package com.vyibc.boot.test.j2cache.service.config;

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.StringRedisSerializer;

@Configuration
@EnableCaching
public class RedisConfig {

    @Bean
    public CacheManager initRedisCacheManager(RedisConnectionFactory connectionFactory)
    {
        RedisCacheManager.RedisCacheManagerBuilder builder = RedisCacheManager
                .RedisCacheManagerBuilder.fromConnectionFactory(connectionFactory);
        return builder.build();
    }
 
        @Bean
        public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
            RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
            redisTemplate.setConnectionFactory(factory);
            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);
            //序列化设置 ,这样为了存储操作对象时正常显示的数据,也能正常存储和获取
            redisTemplate.setKeySerializer(new StringRedisSerializer());
            redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
            redisTemplate.setHashKeySerializer(new StringRedisSerializer());
            redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);
            return redisTemplate;
        }
    @Bean
    public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory factory) {
        StringRedisTemplate stringRedisTemplate = new StringRedisTemplate();
        stringRedisTemplate.setConnectionFactory(factory);
        return stringRedisTemplate;
    }
}

这时候 CacheManager的实现类 是RedisCacheManager,因此你搞一个含有@Cacheable 注解的缓存类RedisCacheService,以及缓存测试方法 ReidisCacheServiceTest

package com.vyibc.boot.test.j2cache.service;

import com.vyibc.boot.test.j2cache.service.model.TestBean;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;

import java.util.concurrent.atomic.AtomicInteger;

@Service
public class RedisCacheService {
   private final AtomicInteger num = new AtomicInteger(0);

   @Cacheable(cacheNames="test" ,key = "'a_num'")
   public Integer getNum() {
      return num.incrementAndGet();
   }
   
   @Cacheable(cacheNames="testBean",key = "'a_bean'")
   public TestBean testBean() {
      TestBean bean = new TestBean();
      bean.setNum(num.incrementAndGet());
      return bean;
   }
   
   @CacheEvict(cacheNames={"test","testBean"},key = "'a_num'")
   public void evict() {
      System.out.println("num = " + num.get());
      System.out.println("模拟的 db删除 " );
   }
   public void reset() {
      num.set(0);
   }
}
package com.vyibc.boot.test.j2cache.service;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.util.Assert;

import java.io.IOException;

/**
 * <p>
 *
 * </p>
 *
 * @author huchangfeng
 * @since 2023/7/3
 */
@SpringBootTest
class ReidisCacheServiceTest {
    @Autowired
    private J2CacheService j2CacheService;

    @Test
    public void testCache() throws IOException {
        j2CacheService.reset();
        j2CacheService.evict();
        j2CacheService.getNum();
        j2CacheService.getNum();
        j2CacheService.getNum();
        j2CacheService.getNum();
        j2CacheService.getNum();
        j2CacheService.getNum();
        Integer n = j2CacheService.getNum();
        Assert.isTrue(n == 1, "缓存未生效!");
        System.out.println("缓存生效!");
    }
    
}

执行下 testCache 可以看到 缓存生效了

image.png

当然 我自己的配置 肯定要优化成 LettuceConnectionFactory 的 具体配置如下

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

package com.vyibc.boot.test.j2cache.service.config;

import com.alibaba.fastjson.support.spring.FastJsonRedisSerializer;
import com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ObjectMapper.DefaultTyping;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.CachingConfigurerSupport;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.cache.RedisCacheWriter;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.RedisStandaloneConfiguration;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.listener.ChannelTopic;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
import org.springframework.data.redis.listener.adapter.MessageListenerAdapter;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext.SerializationPair;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

import java.time.Duration;
import java.util.Collections;

@EnableCaching
@Configuration
public class RedisConfig extends CachingConfigurerSupport {
    private static final Logger log = LoggerFactory.getLogger(RedisConfig.class);

    @Value("${spring.redis.host}")
    private String host;

    @Value("${spring.redis.port}")
    private int port;

    @Value("${spring.redis.password}")
    private String password;

    @Bean("redisConnectionFactory")
    public LettuceConnectionFactory redisConnectionFactory() {
        RedisStandaloneConfiguration config = new RedisStandaloneConfiguration(host, port);
        config.setPassword(password);
        LettuceConnectionFactory lettuceConnectionFactory = new LettuceConnectionFactory(config);
        lettuceConnectionFactory.afterPropertiesSet(); // 初始化连接工厂
        return lettuceConnectionFactory;
    }
    public RedisConfig() {
    }


    @Bean
    public StringRedisSerializer stringRedisSerializer() {
        return new StringRedisSerializer();
    }


    @Bean
    public RedisTemplate<String, Object> redisTemplate(@Qualifier("redisConnectionFactory")LettuceConnectionFactory lettuceConnectionFactory) {
        log.info(" --- redis config init --- ");
        Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = this.jacksonSerializer();
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate();
        redisTemplate.setConnectionFactory(lettuceConnectionFactory);
        RedisSerializer<String> stringSerializer = new StringRedisSerializer();
        redisTemplate.setKeySerializer(stringSerializer);
        redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
        redisTemplate.setHashKeySerializer(stringSerializer);
        redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);
        redisTemplate.afterPropertiesSet();
        return redisTemplate;
    }

    @Bean
    public CacheManager initRedisCacheManager(RedisConnectionFactory connectionFactory)
    {
        RedisCacheManager.RedisCacheManagerBuilder builder = RedisCacheManager
                .RedisCacheManagerBuilder.fromConnectionFactory(connectionFactory);
        return builder.build();
    }


    private Jackson2JsonRedisSerializer jacksonSerializer() {
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.setVisibility(PropertyAccessor.ALL, Visibility.ANY);
        objectMapper.enableDefaultTyping(DefaultTyping.NON_FINAL);
        jackson2JsonRedisSerializer.setObjectMapper(objectMapper);
        return jackson2JsonRedisSerializer;
    }
}

本地缓存-CaffeineCacheManager 配置

接下来要说的是 如果我想使用本地缓存作为cacheManager实现 如 CaffeineCacheManager,这个场景其实在高并发场景 很常见,高并发场景本地缓存是最有效的 解决方案,目前本地缓存综合性能最好的无疑是 Caffeine ,当然 怎么保证本地缓存一致性,热可以探测等 是另外必须要做的事情,后续文章会详细说明怎么实现。

如何自定义一个

SpringBoot系列教程之内存缓存Caffiene自定义CacheManager

<dependencies>
    <dependency>
        <groupId>com.github.ben-manes.caffeine</groupId>
        <artifactId>caffeine</artifactId>
    </dependency>

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-cache</artifactId>
    </dependency>
</dependencies>
package com.vyibc.boot.test.j2cache.service;

import com.github.benmanes.caffeine.cache.Caffeine;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.caffeine.CaffeineCacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.concurrent.TimeUnit;

@Configuration
@EnableCaching
public class CacheConfiguration {
    @Bean(name = "oneHourCacheManager")
    public CacheManager oneHourCacheManager(){
        Caffeine caffeine = Caffeine.newBuilder()
                .initialCapacity(10) //初始大小
                .maximumSize(11)  //最大大小
                .expireAfterWrite(1, TimeUnit.HOURS); //写入/更新之后1小时过期

        CaffeineCacheManager caffeineCacheManager = new CaffeineCacheManager();
        caffeineCacheManager.setAllowNullValues(true);
        caffeineCacheManager.setCaffeine(caffeine);
        return caffeineCacheManager;
    }
}

此时 就完成了使用本地缓存作为cacheManager实现 这个也是比较简单的,毕竟CaffeineCacheManager 已经被写好了

自定义缓存-自定义CacheManager 实现

接下来是 如何完全自定义自己的缓存实现类 其实也很简单 我们可以参考 spring-context-support 是如何做的 仿着做一下就行了,基本上我们在查看中间价的时候

image.png

主要分两步

  • 写一个cache类 实现 AbstractValueAdaptingCache接口
    • 大概逻辑:里面注入你自己的缓存容器如CaffeineCache 的 cache属性 并实现 接口逻辑 就是缓存的增删改查 逻辑
  • 写一个cacheManager类 实现接口CacheManager 或者 AbstractTransactionSupportingCacheManager
    • 大概逻辑: 构造一个 缓存命名空间-->cache 的 map容器 实现接口逻辑, 注入你上面写的 cache

这样我们就可以获取缓存的逻辑就是, 缓存命名空间 获取的cache ,然后执行cache 的查找逻辑,查不到就去执行 真正的方法

多说无益 ,直接看CaffeineCache 怎么做的 照着写就好了,然后自定义自己的逻辑 如多级缓存的实现