SpringCache学习笔记

SpringCache

一、简介

1、缓存的概念

我们可以将缓存定义为一种存储机制,它将数据保存在某个地方,并以一种更快的方式提供服务。

理解缓存,我们先了解下基本概念

缓存命中率

即从缓存中读取数据的次数与总读取次数的比率。 一般来讲,命中率越高也好。

命中率 = 从缓存中读取的次数  / (总读取次数[从缓存中读取的次数+从慢速设备上读取的次数])
Miss率 = 没从缓存中读取的次数/ (总读取次数[从缓存中读取的次数+从慢速设备上读取的次数])

这是一个非常重要的监控指标,如果要做缓存,就一定要监控这个指标,来看缓存是否工作良好。

2、缓存介绍

​ Spring 从 3.1 开始就引入了对 Cache 的支持。定义了 org.springframework.cache.Cacheorg.springframework.cache.CacheManager 接口来统一不同的缓存技术。并支持使用 JCache(JSR-107)注解简化我们的开发。

JCache简介

​ JCache是JSR-107规范中定义了Java对象临时缓存在内存中的API和语义,包括对象的创建(object creation)、共享访问(shared access)、失效(invalidation)和跨JVM的一致性(consitency across jvm’s)。

简单说就是:JCache是Java提供的标准缓存API。

​ 其使用方法和原理都类似于 Spring 对事务管理的支持。Spring Cache 是作用在方法上的,其核心思想是,当我们在调用一个缓存方法时会把该方法参数和返回结果作为一个键值对存在缓存中。

3、SpringCache常用注解

https://www.jianshu.com/p/33c019de9115

@Cacheable
public @interface Cacheable {  
    String[] value();              //缓存的名字,可以把数据写到多个缓存  
    String key() default "";       //缓存key,如果不指定将使用默认的KeyGenerator生成,后边介绍  
    String condition() default ""; //满足缓存条件的数据才会放入缓存,condition在调用方法之前和之后都会判断  
    String unless() default "";    //用于否决缓存更新的,不像condition,该表达只在方法执行之后判断,此时可以拿到返回值result进行判断了 
    String keyGenerator() default ""; //key生成器,和key二选一使用,支持自定义
}  

应用到读取数据的方法上,即可缓存的方法,如查找方法:先从缓存中读取,如果没有再调用方法获取数据,然后把数据添加到缓存中(select)

@CachePut
public @interface CachePut {  
    String[] value();              //缓存的名字,可以把数据写到多个缓存  
    String key() default "";       //缓存key,如果不指定将使用默认的KeyGenerator生成,后边介绍  
    String condition() default ""; //满足缓存条件的数据才会放入缓存,condition在调用方法之前和之后都会判断  
    String unless() default "";    //用于否决缓存更新的,不像condition,该表达只在方法执行之后判断,此时可以拿到返回值result进行判断了  
}  

应用到写数据的方法上,如新增/修改方法,调用方法时会自动把相应的数据放入缓存(insert/update

@CacheEvict
public @interface CacheEvict {  
    String[] value();              //缓存的名字,可以把数据写到多个缓存  
    String key() default "";       //缓存key,如果不指定将使用默认的KeyGenerator生成,后边介绍  
    String condition() default ""; //满足缓存条件的数据才会放入缓存,condition在调用方法之前和之后都会判断  
    boolean allEntries() default false;      //是否移除所有数据  
    boolean beforeInvocation() default false;//是调用方法之前移除/还是调用之后移除
}

应用于删除数据的方法之上(delete)

3、SpringCache的优缺点

优点

  • 支持开箱即用(Out Of The Box),并提供基本的Cache抽象,方便切换各种底层Cache
  • 通过Cache注解即可实现缓存逻辑透明化,让开发者关注业务逻辑
  • 当事务回滚时,缓存也会自动回滚
  • 支持比较复杂的缓存逻辑
  • 提供缓存编程的一致性抽象,方便代码维护。

缺点

  • Spring Cache并不针对多进程的应用环境进行专门的处理。
  • 另外Spring Cache抽象的操作中没有锁的概念,当多线程并发操作(更新或者删除)同一个缓存项时,有可能读取到过期的数据。
3、Cache 和 CacheManager 接口说明
  • Cache 接口包含缓存的各种操作集合,你操作缓存就是通过这个接口来操作的。
  • Cache 接口下 Spring 提供了各种 xxxCache 的实现,比如:RedisCache、EhCache、ConcurrentMapCache
  • CacheManager 定义了创建、配置、获取、管理和控制多个唯一命名的 Cache。这些 Cache 存在于 CacheManager 的上下文中。

小结:

每次调用需要缓存功能的方法时,Spring 会检查指定参数的指定目标方法是否已经被调用过,如果有就直接从缓存中获取方法调用后的结果,如果没有就调用方法并缓存结果后返回给用户。下次调用直接从缓存中获取。

二、SpringBoot集成SpringCache

sync = true 可以有效的避免缓存击穿的问题。

1. 添加maven依赖
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-cache</artifactId>
</dependency>
2.在启动类上添加**@EnableCaching,开始缓存**
@SpringBootApplication
@MapperScan("com.springcache.Mapper")
@EnableCaching
public class DemoApplication {
    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }
}
3.编写实体类
@Data
@NoArgsConstructor
@AllArgsConstructor
public class User {
    private Integer userId;
    private String password;
    private String username;
}
5、编写mapper层实现对数据库的操作
@Repository
@Mapper
public interface UsersMapper  {
   /**
     * 根据id查询
     * @param userId userId
     * @return User
     */
    User queryUser(Integer userId);
}
6、编写service,使用缓存注解
@Service
public class UserImpl implements UserService {

    @Resource
    private UsersMapper usersMapper;

    @Override
    public User queryNo(Integer userId) {
        System.out.println("进入无缓存方法");
        User user = usersMapper.queryUser(userId);
        return user;
    }
    
 	 /**
     * 查找缓存
     * @param userId userID
     * @return user
     */
    @Override
    @Cacheable(cacheNames ="user",key = "#userId)
    public User query(Integer userId) {
        System.out.println("进入有缓存方法");
        return usersMapper.queryUser(userId);
    }       
}

三、SpringCache整合Redis

1、导入redis依赖

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

2、配置yaml文件

 redis:
    host: 主机ip地址
    port: 6379
    password: '密码'
    jedis:
      pool:
        max-active: 8
        max-wait: -1
        max-idle: 500
        min-idle: 0
    lettuce:
      shutdown-timeout: 30000

3、配置Redis整合SpringCache文件

3.1需要用到序列化工具,所以导入maven依赖
		<dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.70</version>
        </dependency>
3.2配置缓存管理器
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.ParserConfig;
import com.alibaba.fastjson.serializer.SerializerFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.data.redis.RedisProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.CachingConfigurerSupport;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.interceptor.CacheErrorHandler;
import org.springframework.cache.interceptor.KeyGenerator;
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.*;

import java.lang.reflect.Method;
import java.nio.charset.Charset;
import java.time.Duration;

@Slf4j
@Configuration
@EnableCaching
@EnableConfigurationProperties(RedisProperties.class)
public class CacheConfig extends CachingConfigurerSupport {
    private static final FastJsonRedisSerializer<Object> fastJsonRedisSerializer = new FastJsonRedisSerializer<>(Object.class);


    //缓存管理器。可以管理多个缓存
    //只有CacheManger才能扫描到cacheable注解
    //spring提供了缓存支持Cache接口,实现了很多个缓存类,其中包括RedisCache。但是我们需要对其进行配置,这里就是配置RedisCache

    @Bean
    public CacheManager cacheManager(RedisConnectionFactory connectionFactory) {
        RedisCacheManager cacheManager = RedisCacheManager.RedisCacheManagerBuilder
                //Redis链接工厂
                .fromConnectionFactory(connectionFactory)
                //缓存配置 通用配置  默认存储一小时
                .cacheDefaults(getCacheConfigurationWithTtl(Duration.ofHours(1)))
                //配置同步修改或删除  put/evict
                .transactionAware()
                //对于不同的cacheName我们可以设置不同的过期时间
//                .withCacheConfiguration("app:",getCacheConfigurationWithTtl(Duration.ofHours(5)))
                .withCacheConfiguration("user:",getCacheConfigurationWithTtl(Duration.ofHours(2)))
                .build();
        return cacheManager;
    }
    //缓存的基本配置对象
    private   RedisCacheConfiguration getCacheConfigurationWithTtl(Duration duration) {
        return RedisCacheConfiguration
                .defaultCacheConfig()
                //设置key value的序列化方式
                // 设置key为String
                .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
                // 设置value 为自动转Json的Object
                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(fastJsonRedisSerializer))
                // 不缓存null
                .disableCachingNullValues()
                // 设置缓存的过期时间
                .entryTtl(duration);
    }

    //缓存的异常处理
    @Bean
    @Override
    public CacheErrorHandler errorHandler() {
        // 异常处理,当Redis发生异常时,打印日志,但是程序正常走
        log.info("初始化 -> [{}]", "Redis CacheErrorHandler");
        return new CacheErrorHandler() {
            @Override
            public void handleCacheGetError(RuntimeException e, Cache cache, Object key) {
                log.error("Redis occur handleCacheGetError:key -> [{}]", key, e);
            }
            @Override
            public void handleCachePutError(RuntimeException e, Cache cache, Object key, Object value) {
                log.error("Redis occur handleCachePutError:key -> [{}];value -> [{}]", key, value, e);
            }

            @Override
            public void handleCacheEvictError(RuntimeException e, Cache cache, Object key) {
                log.error("Redis occur handleCacheEvictError:key -> [{}]", key, e);
            }

            @Override
            public void handleCacheClearError(RuntimeException e, Cache cache) {
                log.error("Redis occur handleCacheClearError:", e);
            }
        };
    }

    @Override
    @Bean("myKeyGenerator")
    public KeyGenerator keyGenerator() {
        return new KeyGenerator() {
            @Override
            public Object generate(Object target, 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
    @ConditionalOnMissingBean(name = "redisTemplate") //保证仅有这一个bean可以被注册,否则报错
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
        System.out.println("redisTemplate");
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setValueSerializer(fastJsonRedisSerializer);
        redisTemplate.setHashValueSerializer(fastJsonRedisSerializer);
        redisTemplate.setConnectionFactory(factory);
        return redisTemplate;
    }
    //操纵缓存的模板
    @Bean
    public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory factory) {
        System.out.println("stringTemplate");
        StringRedisTemplate stringRedisTemplate = new StringRedisTemplate();
        stringRedisTemplate.setKeySerializer(new StringRedisSerializer());
        stringRedisTemplate.setValueSerializer(fastJsonRedisSerializer);
        stringRedisTemplate.setConnectionFactory(factory);
        stringRedisTemplate.setHashValueSerializer(fastJsonRedisSerializer);
        return stringRedisTemplate;
    }
}

class FastJsonRedisSerializer<T> implements RedisSerializer<T> {
    private final ObjectMapper objectMapper = new ObjectMapper();
    public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");


    private final Class<T> clazz;

    static {
        // 全局开启AutoType,这里方便开发,使用全局的方式
        ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
        // 建议使用这种方式,小范围指定白名单
        // ParserConfig.getGlobalInstance().addAccept("me.zhengjie.domain");
        // key的序列化采用StringRedisSerializer
    }

    public FastJsonRedisSerializer(Class<T> clazz) {
        super();
        this.clazz = clazz;
    }

    //序列化 我们存储时,存储的是json对象,而默认存储的是byte类型的,所以在可视化窗口上显示时,看到的是乱码
    @Override
    public byte[] serialize(T t) throws SerializationException {
        System.out.println("进行序列化");
        if (t == null) {
            return new byte[0];
        }
        System.out.println("序列化:::::");
        return JSON.toJSONString(t, SerializerFeature.WriteClassName).getBytes(DEFAULT_CHARSET);
    }

    //反序列化
    @Override
    public T deserialize(byte[] bytes) throws SerializationException {
        if (bytes == null || bytes.length <= 0) {
            return null;
        }
        String str = new String(bytes, DEFAULT_CHARSET);
        return JSON.parseObject(str, clazz);
    }
}

其中,CachingConfigurerSupport接口定义如下

public class CachingConfigurerSupport implements CachingConfigurer {

	@Override
	@Nullable
	public CacheManager cacheManager() {      //创建一个CacheManager
		return null;
	}

	@Override
	@Nullable
	public CacheResolver cacheResolver() {    //创建一个cachaeResolver,和cacheManager创建其中一个即可
		return null;
	}

	@Override
	@Nullable
	public KeyGenerator keyGenerator() {      //自定义主键生成策略
		return null;
	}

	@Override
	@Nullable
	public CacheErrorHandler errorHandler() { //缓存错误处理
		return null;
	}

}

3.3测试使用

3.3.1普通缓存存入
@SpringBootTest
class DemoApplicationTests {

    @Autowired
    UserService userService;

    /**
     *  Cacheable 测试
     */
    @Test
    void contextLoads() {
        //第一次调用无缓存方法
        System.out.println("第一次加载");
        User query = userService.query(2);
        System.out.println(query);
        System.out.println("-----------------------");
        User user1 = userService.queryNo(2);
        System.out.println(user1);
        System.out.println("-----------------------");
        System.out.println("第二次加载");
        User query1 = userService.query(2);
        System.out.println(query1);
        System.out.println("-----------------------");
        User user2 = userService.queryNo(2);
        System.out.println(user2);
        System.out.println("-----------------------");
    }
  }

运行结果如图:

在这里插入图片描述

redis中:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PCgZxAdL-1648795578588)(C:\Users\admin\AppData\Roaming\Typora\typora-user-images\image-20220401091819007.png)]

3.3.2更新/插入:
 /**
     * CachePut 测试
     */
    @Test
    void update(){
        User user = new User(15,"123","testSave");
        userService.save(user);

        User query = userService.query(15);
        System.out.println(query);

        System.out.println("----------------");
        User user1 = new User(15,"123123213131","testSave");
        userService.update(user1);

        User query1 = userService.query(15);
        System.out.println(query1);
    }

运行结果:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4Xt2qorm-1648795578589)(C:\Users\admin\AppData\Roaming\Typora\typora-user-images\image-20220401091931395.png)]

redis:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-W55dVBgm-1648795578590)(C:\Users\admin\AppData\Roaming\Typora\typora-user-images\image-20220401091955349.png)]

并且是修改之后的密码,所以可见,进行修改和插入的时候,会自动生成缓存,如果以前有就自动删除并且替换掉,很方便

3.3.3使用自定义Key生成策略:
@Service
@CacheConfig(cacheNames = "user")
public class UserImpl implements UserService {
    @Override
    @Cacheable(key = "#userId")   //默认key方法
    public User query(Integer userId) {
        System.out.println("进入有缓存方法");
        System.out.println("userId--->"+userId);
        return usersMapper.queryUser(userId);
    }

    @Override
    @Cacheable(keyGenerator = "myKeyGenerator")  //使用自定义key生成策略
    public User queryMy(Integer userId) {
        System.out.println("进入有缓存方法");
        System.out.println("userId--->"+userId);
        return usersMapper.queryUser(userId);
    }
}

测试代码如下:

@SpringBootTest
class DemoApplicationTests { 
    @Autowired
    UserService userService;
    
	@Test
    void contextLoads() {
        //第一次调用无缓存方法
        System.out.println("第一次加载");
        User query = userService.query(2);
        System.out.println(query);
    } 

	/**
     * myKey
     */
    @Test
    void my(){
        User user = userService.queryMy(3);
        System.out.println(user);
    }
}

RDM中结果如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3B8lwxeU-1648795578590)(C:\Users\admin\AppData\Roaming\Typora\typora-user-images\image-20220401091015363.png)]

可以看到,使用自定义的成功了。

四、使用缓存应该注意到的问题

原文地址

4.1缓存雪崩

什么是缓存雪崩?

当某一个时刻出现大规模的缓存失效的情况,那么就会导致大量的请求直接打在数据库上面,导致数据库压力巨大,如果在高并发的情况下,可能瞬间就会导致数据库宕机。这时候如果运维马上又重启数据库,马上又会有新的流量把数据库打死。这就是缓存雪崩。

preview

分析:

造成缓存雪崩的关键在于在同一时间大规模的key失效。为什么会出现这个问题呢,有几种可能,第一种可能是Redis宕机,第二种可能是采用了相同的过期时间。搞清楚原因之后,那么有什么解决方案呢?

解决方案:

1、在原有的失效时间上加上一个随机值,比如1-5分钟随机。这样就避免了因为采用相同的过期时间导致的缓存雪崩。

如果真的发生了缓存雪崩,有没有什么兜底的措施?

2、使用熔断机制。当流量到达一定的阈值时,就直接返回“系统拥挤”之类的提示,防止过多的请求打在数据库上。至少能保证一部分用户是可以正常使用,其他用户多刷新几次也能得到结果。

3、提高数据库的容灾能力,可以使用分库分表,读写分离的策略。

4、为了防止Redis宕机导致缓存雪崩的问题,可以搭建Redis集群,提高Redis的容灾性。

4.2缓存击穿

什么是缓存击穿?

其实跟缓存雪崩有点类似,缓存雪崩是大规模的key失效,而缓存击穿是一个热点的Key,有大并发集中对其进行访问,突然间这个Key失效了,导致大并发全部打在数据库上,导致数据库压力剧增。这种现象就叫做缓存击穿。

分析:

关键在于某个热点的key失效了,导致大并发集中打在数据库上。所以要从两个方面解决,第一是否可以考虑热点key不设置过期时间,第二是否可以考虑降低打在数据库上的请求数量。

解决方案:

1、上面说过了,如果业务允许的话,对于热点的key可以设置永不过期的key。

2、使用互斥锁。如果缓存失效的情况,只有拿到锁才可以查询数据库,降低了在同一时刻打在数据库上的请求,防止数据库打死。当然这样会导致系统的性能变差。

4.3缓存穿透

什么是缓存穿透?

我们使用Redis大部分情况都是通过Key查询对应的值,假如发送的请求传进来的key是不存在Redis中的,那么就查不到缓存,查不到缓存就会去数据库查询。假如有大量这样的请求,这些请求像“穿透”了缓存一样直接打在数据库上,这种现象就叫做缓存穿透。

分析:

关键在于在Redis查不到key值,这和缓存击穿有根本的区别,区别在于缓存穿透的情况是传进来的key在Redis中是不存在的。假如有黑客传进大量的不存在的key,那么大量的请求打在数据库上是很致命的问题,所以在日常开发中要对参数做好校验,一些非法的参数,不可能存在的key就直接返回错误提示,要对调用方保持这种“不信任”的心态。

preview

解决方案:

1、把无效的Key存进Redis中。如果Redis查不到数据,数据库也查不到,我们把这个Key值保存进Redis,设置value=“null”,当下次再通过这个Key查询时就不需要再查询数据库。这种处理方式肯定是有问题的,假如传进来的这个不存在的Key值每次都是随机的,那存进Redis也没有意义。

2、使用布隆过滤器。布隆过滤器的作用是某个 key 不存在,那么就一定不存在,它说某个 key 存在,那么很大可能是存在(存在一定的误判率)。于是我们可以在缓存之前再加一层布隆过滤器,在查询的时候先去布隆过滤器查询 key 是否存在,如果不存在就直接返回。

preview

总结

这三个问题在使用Redis的时候是肯定会遇到的,而且是非常致命性的问题,所以在日常开发中一定要注意,每次使用Redis时,都要对其保持严谨的态度。还有一个需要注意的是要做好熔断,一旦出现缓存雪崩,击穿,穿透这种情况,至少还有熔断机制保护数据库不会被打死。

注: 仅作个人笔记使用

关于此的一些文档:

  • https://blog.csdn.net/zhang19903848257/article/details/115144049

  • https://xie.infoq.cn/article/001e0f5ab65fa7dd1484c51e5

  • https://cloud.tencent.com/developer/article/1862561

官方文档:

  • https://spring.io/

  • https://docs.spring.io/spring-framework/docs/current/reference/html/integration.html#cache

  • https://www.jianshu.com/p/d9ecd56710c4

SpEl

  • https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#expressions
  • 20
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值