spring缓存注解@Cacheable等

1 简述

spring缓存注解,除常用的@Cacheable,还有@CachePut、@CacheEvict、@CacheConfig、@Caching等注解,组成了一个完整的缓存注解集。

缓存的重要性、地位就不说了,不能狭义理解,缓存就是对数据库的数据缓存,比如说CPU缓存、互联网CDN服务都有它的影子,把一些耗时的计算结果存储下来,形成直接可利用的产品数据,避免重复计算,也可以称为计算缓存。可以泛泛理解为,缓存就是为突破稀缺资源的性能瓶颈,而采取的一种方法、策略。这些资源是数据库、第三方接口、网络带宽、一段业务逻辑等。

有一个问题,在软件开发时,缓存经常用到,不管是本地缓存,还是redis缓存,直接用这些缓存类库提供工具类,已经很方便、很灵活,为什么说还要用spring提供的缓存注解,被束缚。说心里话spring提供的缓存注解,功能上很完整,也很灵活。但实际开发中用的最多的还是通过工具类方式,程序员多少都有些控制综合证,就是不放心委托给第三方去处理。

我的理解,采用spring缓存注解的价值:

(1) 业务逻辑、缓存逻辑二者进行分离,使其职责更清晰。

(2) 它是一种规范(个人认为是最重要的),大家都采用这种方式,而不是五花八门,更有利于对系统长期迭代、维护。 

2 定义

@Cacheable:创建、查询缓存
@CachePut:更新缓存
@CacheEvict:删除缓存
@CacheConfig:类级别共享配置
@Caching: 组合缓存配置

2.1 @Cacheable

该注解,可以使方法返回结果被缓存,再次通过相同参数调用时,会直接从缓存获取,而不再执行该方法逻辑。

参数:

cacheNames:缓存名称,用来划分不同的缓存区,避免相同key值互相影响。

key:缓存key值,格式为spring EL 表达式,从方法参数获取值,例如:"#id","#user.id"等。

keyGenerator:自定义key生成类,通过反射方式自己构建key,细节参考“4 自定义”部分内容,跟key参数不能同时赋值。

cacheManager:指定缓存管理器,不赋值时,为默认管理器,常用于多缓存源场景。

cacheResolver:自定义获取缓存源,跟keyGenerator类似,细节参考“4 自定义”部分内容。

condition:设置匹配条件,针对请求参数,满足条件的进行缓存,格式为spring EL 表达式。

unless:设置排除条件,针对返回值,满足条件的不进行缓存,格式为spring EL 表达式。

sync:是否开启同步底层方法的调用,如果开启,可避免相同key值,多个线程同时加载数据,默认为false没有开启。(开启后,会对参数有些限制,细节可参考Cacheable源码描述)

例子:

	@Cacheable(cacheNames="users", key="#id")
	public User get1(int id) {
		System.out.println("do get1: " + id);
		return new User(id);
	}
	
	@Cacheable(cacheNames="users", key="#user.id")
	public User get2(User user) {
		System.out.println("do get2: " + user.getId());
		return new User(user.getId());
	}	
	
	@Cacheable(cacheNames="users", key="#user.id", 
			condition="#user.id > 300", unless="#result.id > 500")
	public User get3(User user) {
		System.out.println("do get3: " + user.getId());
		return new User(user.getId());
	}
	
	@Cacheable(cacheNames="users", key="#user.id", sync=true)
	public User get4(User user) throws InterruptedException {
		System.out.println("do get4: " + user.getId());
		
		Thread.sleep(10000);		
		return new User(user.getId());
	}		
	
	@Cacheable(cacheNames="users", keyGenerator="myKeyGenerator", cacheManager="myCacheManager")
	public User get5(int id) {
		System.out.println("do get5: " + id);
		return new User(id);
	}


2.2 @CachePut

该注解,在功能上跟@Cacheable基本相同,不同之处就是,每次都会执行方法逻辑,更新缓存。

参数:

除不包含sync参数外,其它跟@Cacheable一致。

例子:

	@CachePut(cacheNames="users", key="#id")
	public User put(int id) {
		System.out.println("do put: " + id);
		return new User(id);
	}


2.3 @CacheEvict

该注解,对符合参数条件的缓存,作删除处理。

参数:

除跟@Cacheable类似的参数外,还包含另外allEntries,beforeInvocation两个参数。

allEntries:删除指定cacheNames区域内,所有的缓存。

beforeInvocation:如果true时,在执行方法之前做删除缓存处理,false时,在执行方法之后做删除处理,默认为false。

例子:

	@CacheEvict(cacheNames="users", key="#id")
	public void delete(int id) {
		System.out.println("do delete: " + id);
	}
	
	@CacheEvict(cacheNames="users", allEntries=true)
	public void clear() {
		System.out.println("do clear");
	}

2.4 @CacheConfig

@CacheConfig是一个类级别的注解,类下所有被缓存注解的方法都会继承所配置的参数,避免方法上相同参数重复配置。

参数:

只包含cacheNames,keyGenerator,cacheManager,cacheResolver四个参数。

例子:

@Service
@CacheConfig(cacheNames="users", cacheManager="myCacheManager")
public class UserService2 {

	@Cacheable(key="#id")
	public User get(int id) {
		System.out.println("do get: " + id);
		return new User(id);
	}	

	@CachePut(key="#id")
	public User put(int id) {
		System.out.println("do put: " + id);
		return new User(id);
	}
	
	@CacheEvict(key="#id")
	public void delete(int id) {
		System.out.println("do delete: " + id);
	}
	
	@CacheEvict(allEntries=true)
	public void clear() {
		System.out.println("do clear");
	}
}

2.5 @Caching

该注解,可同时组合、配置多个@Cacheable, @CachePut, @CacheEvict注解。

例子:

	@Caching(evict={
			@CacheEvict(cacheNames="users", key="#id"),
			@CacheEvict(cacheNames="roles", key="#id")
	})
	public void delete2(int id) {
		System.out.println("do clear");
	}


3 配置


3.1 开启

开启缓存功能,需要先添加使能注解@EnableCaching,通常习惯在启动类配置,否则缓存注解@Cacheable等不起作用。

3.2 依赖

		
		
		    org.springframework.boot
		    spring-boot-starter-cache
				
		
		
        
            org.springframework.boot
            spring-boot-starter-data-redis
        
        
        
		
		    com.github.ben-manes.caffeine
		    caffeine
		


3.3 缓存源

既然是对数据进行缓存,就会涉及数据缓存到哪里问题,是进程本地内存,还是进程外远程存储,就需要配置缓存源,spring提供了丰富的缓存源种类,以下是常用的几种。

3.3.1 不配置(默认)

如果项目没有第三方缓存源依赖时,spring boot 会默认配置ConcurrentMapCacheManager缓存管理器,其内部由ConcurrentHashMap存储缓存数据,如果有第三方缓存依赖,例如:caffeine、redis时,就会相应的配置CaffeineCacheManager或RedisCacheManager做默认缓存管理器,但只配置一个,具体优先级,推测是按CacheType枚举顺序,没有细查。

其中ConcurrentMapCacheManager无法设置缓存时间,通常不建议使用。

3.3.2 caffeine缓存

下面是java配置方式(配置文件方式可参考通用配置类CacheProperties定义)

	@Bean(name = "simpleCacheManager")
	public SimpleCacheManager simpleCacheManager() {
		SimpleCacheManager result = new SimpleCacheManager();
		
		CaffeineCache users = new CaffeineCache("users",
				Caffeine.newBuilder()
				.expireAfterWrite(600, TimeUnit.SECONDS)
				.maximumSize(10000L).build());
		
		CaffeineCache roles = new CaffeineCache("roles",
				Caffeine.newBuilder()
				.expireAfterWrite(600, TimeUnit.SECONDS)
				.maximumSize(10000L).build());			
		
		result.setCaches(Arrays.asList(users, roles));		
		return result ;
	}

3.3.3 redis缓存

    @Bean(name = "redisCacheManager")
    public RedisCacheManager cacheManager(RedisConnectionFactory factory) {
		
        RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
                .entryTtl(Duration.ofSeconds(600))
                .serializeKeysWith(SerializationPair.fromSerializer(new StringRedisSerializer()))
                .serializeValuesWith(SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()))
                
                .disableCachingNullValues()
                .prefixCacheNameWith("mtr");

        RedisCacheManager redisCacheManager = RedisCacheManager.builder(factory)
                .cacheDefaults(config)
                .transactionAware()
                .build();
        return redisCacheManager;
    }

3.3.4 多缓存源

@Configuration
public class CacheConfig {	
	
	@Bean(name = "simpleCacheManager")
	public SimpleCacheManager simpleCacheManager() {
		SimpleCacheManager result = new SimpleCacheManager();
		
		CaffeineCache users = new CaffeineCache("users",
				Caffeine.newBuilder()
				.expireAfterWrite(600, TimeUnit.SECONDS)
				.maximumSize(10000L).build());
		
		CaffeineCache roles = new CaffeineCache("roles",
				Caffeine.newBuilder()
				.expireAfterWrite(600, TimeUnit.SECONDS)
				.maximumSize(10000L).build());			
		
		result.setCaches(Arrays.asList(users, roles));		
		return result ;
	}
	
    @Bean(name = "redisCacheManager")
    @Primary
    public RedisCacheManager cacheManager(RedisConnectionFactory factory) {
		
        RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
                .entryTtl(Duration.ofSeconds(600))
                .serializeKeysWith(SerializationPair.fromSerializer(new StringRedisSerializer()))
                .serializeValuesWith(SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()))
                
                .disableCachingNullValues()
                .prefixCacheNameWith("mtr");

        RedisCacheManager redisCacheManager = RedisCacheManager.builder(factory)
                .cacheDefaults(config)
                .transactionAware()
                .build();
        return redisCacheManager;
    }
}


4 自定义

4.1 keyGenerator

例子(不完善仅做参考):

@Component("keyGenerator")
public class CacheKeyGenerator implements KeyGenerator {
	
    public static final int NO_PARAM_KEY = 0;
    public static final int NULL_PARAM_KEY = 53;
 
    @Override
    public Object generate(Object target, Method method, Object... params) {
        StringBuilder key = new StringBuilder();
        key.append(target.getClass().getSimpleName()).append(".").append(method.getName()).append(":");
        if (params.length == 0) {
            key.append(NO_PARAM_KEY);
        } else {
            int count = 0;
            for (Object param : params) {
                if (0 != count) {
                    key.append(',');
                }
                if (param == null) {
                    key.append(NULL_PARAM_KEY);
                } else if (ClassUtils.isPrimitiveArray(param.getClass())) {
                    int length = Array.getLength(param);
                    for (int i = 0; i < length; i++) {
                        key.append(Array.get(param, i));
                        key.append(',');
                    }
                } else if (ClassUtils.isPrimitiveOrWrapper(param.getClass()) || param instanceof String) {
                    key.append(param);
                } else {
                	//Java一定要重写hashCode和eqauls
                    key.append(param.hashCode());
                }
                count++;
            }
        }
 
        String finalKey = key.toString();
        System.out.println("using cache key=" + finalKey);

        return finalKey;
    }
}


4.2 cacheResolver

SimpleCacheResolver,NamedCacheResolver 是spring内部的CacheResolver接口实现类,可根据实际情况参考实现,就不提供例子。

5 问题

5.1 经典问题

也就是大家常讨论的问题:雪崩、穿透、击穿,下面一一说一下:

(1) 雪崩

缓存雪崩,就是某一时刻发生大规模的缓存失效,这时,大量请求就会直接打到DB上,严重时会导致DB撑不住、挂掉。

方案:
a. 保证缓存服务的高可用。
b. 针对大量key同时过期的情况,解决起来比较简单,只需要将每个key的过期时间打散即可.
c. 本地缓存保底,以及限流&降级等措施。

(2) 穿透

当查询DB不存在的数据时,而缓存也不保存空值,就会导致请求每次都会打到数据库上面去。这种查询不存在数据,而导致直接访问DB的现象称为缓存穿透。

方案:
a. 缓存保存空值,常见的redis缓存都提供相关配置参数。
b. 采用布隆过滤器等,进行前端拦截等。

(3) 击穿

在高并发场景,大量请求同时查询一个 key 时,而此时该key正好失效,就会导致多个请求同时加载该key数据场景。这种现象我们称为缓存击穿。

方案:
a. 可以采用锁的方式,限制对相同key的数据同时加载,可尝试通过缓存注解@Cacheable中sync参数处理。

5.2 注意事项

(1)  配置缓存注解参数时,最好通过cacheManager明确缓存源,避免项目后期迭代,添加第三方依赖时,可能导致默认缓存管理器变动,发生不可预期的问题。

(2) 如果缓存涉及key、value序列化,当value是复杂类型时,不可避免会有属性变动,此时需要考虑已缓存数据,与新数据类型兼容问题,在多项目共享数据类型场景,尤其需要注意。

你可能感兴趣的:(spring,缓存,spring,java)