Redis的简单理解

355 阅读9分钟

Redis缓存

什么是缓存

​ 以空间换时间,将数据保存到内存中,读写操作更快,减小数据库压力,提高效能

哪些数据适合缓存

  • 经常查询的人热点数据

  • 不经常变的数据(数据变化会导致缓存中的数据跟着变,如果变化频繁,性能开销很大)

缓存的流程

  1. 请求查询时,先去缓存中查询,如果有直接返回
  2. 如果缓存中没有,到数据库查询
  3. 将数据库查询的数据同步到缓存中
  4. 返回查询数据

传统缓存方案和分布式缓存方案的区别

  • 传统缓存方案将我们的数据保存到本地
  • 当我们集群的时候,假如A,B微服务中的缓存数据都是100,如果此时A服务修改了数据库中的数据并且更新了缓存,B却没有修改,此时就出现了缓存不同步的问题
  • 使用分布式缓存方法,使用redis作为共享缓存,就解决了缓存不同步的问题,还不占应用内存
  • 同时微服务各自也可以将私有的数据保存在本地缓存中,读写更快

微服务集成redis

直接从微服务中调用redis服务器

基础搭建

导入redis缓存依赖

<!--整合Redis , 底层可以用jedis-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
            <exclusions>
                <exclusion>
                    <groupId>io.lettuce</groupId>
                    <artifactId>lettuce-core</artifactId>
                </exclusion>
            </exclusions>
        </dependency>

        <dependency>
            <groupId>redis.clients</groupId>
            <artifactId>jedis</artifactId>
        </dependency>

配置redis配置

package cn.hxyjyz.hrm.config;

import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.CachingConfigurerSupport;
import org.springframework.cache.interceptor.*;
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.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.StringRedisSerializer;//缓存的配置

import javax.annotation.Resource;

@Configuration
public class CacheConfig extends CachingConfigurerSupport {

    @Resource
    private RedisConnectionFactory factory;

    /**
     * 自定义生成redis-key , 类名.方法名
     * @return
     */
    @Override
    @Bean
    public KeyGenerator keyGenerator() {
        return (o, method, objects) -> {
            StringBuilder sb = new StringBuilder();
            sb.append(o.getClass().getName()).append(".");
            sb.append(method.getName()).append(".");
            for (Object obj : objects) {
                sb.append(obj.toString());
            }
            return sb.toString();
        };
    }

    //使用JSON进行序列化
    @Bean
    public RedisTemplate<Object, Object> redisTemplate() {
        RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(factory);
        //JSON格式序列化
        GenericJackson2JsonRedisSerializer genericJackson2JsonRedisSerializer = new GenericJackson2JsonRedisSerializer();
         //key的序列化
        redisTemplate.setKeySerializer(genericJackson2JsonRedisSerializer);
        //value的序列化
        redisTemplate.setValueSerializer(genericJackson2JsonRedisSerializer);
        //hash结构key的虚拟化
        redisTemplate.setHashKeySerializer(new StringRedisSerializer());
        //hash结构value的虚拟化
        redisTemplate.setHashValueSerializer(genericJackson2JsonRedisSerializer);
        return redisTemplate;
    }

    @Bean
    @Override
    public CacheResolver cacheResolver() {
        return new SimpleCacheResolver(cacheManager());
    }

    @Bean
    @Override
    public CacheErrorHandler errorHandler() {
        // 用于捕获从Cache中进行CRUD时的异常的回调处理器。
        return new SimpleCacheErrorHandler();
    }
    //缓存管理器
    @Bean
    @Override
    public CacheManager cacheManager() {
        RedisCacheConfiguration cacheConfiguration = RedisCacheConfiguration.defaultCacheConfig()
                        .disableCachingNullValues() //不允许空值
                        .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()));//值使用JSON虚拟化
        return RedisCacheManager.builder(factory).cacheDefaults(cacheConfiguration).build();
    }
} 

手动缓存

  1. 从redis缓存中获取数据
  2. 如果有就返回数据
  3. 如果没有就查询数据库,然后放入缓存中,最后返回数据
@Override
    public List<CourseType> list() {
        List<CourseType> courseTypes = (List<CourseType>)redisTemplate.opsForValue()
                .get(RedisCode.KEY_REDIS_CODE);
        if (courseTypes == null) {
            //表明缓存中没有,就从数据库中查询
            List<CourseType> courseTypeList = getCourseTypes2treeData();
            //将数据库中查询出来的放入缓存中
            redisTemplate.opsForValue().set(RedisCode.KEY_REDIS_CODE, courseTypeList);
            //返回查询的课程树
            return courseTypeList;
        } else {
            //返回缓存中的查询结果
            return courseTypes;
        }
    }

删除缓存

  • 复写方法
  • 注意删除缓存是先修改数据库,再删除缓存
  • 当我们使用注解时,对应的方法必须有接口(才能使用动态代理的方式)

注解缓存:集成SpringCache

  1. 基础搭建

  2. 启动类添加注解@EnableCaching开启缓存

  3. 通过在方法上添加注解@Cacheable进行缓存或查询缓存

    当发送请求时,首先通过注解在缓存中查询,如果缓存中没有,到数据库查询,并且将返回值放入缓存中

    • 里面有参数cachenames,缓存的名字,以及key,当我们保存到缓存中时,就是cachename::key拼接作为key值保存在redis中
    • condition,里面跟我们获取数据的条件
  4. 常用注解

    ² @Cacheable:触发缓存写入。

    ² @CacheEvict:触发缓存清除。

    ² @CachePut:更新缓存(不会影响到方法的运行)。

    ² @Caching:重新组合要应用于方法的多个缓存操作。

    ² @CacheConfig:设置类级别上共享的一些常见缓存设置。

常见问题

你们如何保证mysql和redis数据一致性的

首先,我们将数据库的不一致情况分为三种情况:

  1. 数据库有数据,缓存没有数据;

  2. 数据有数据,缓存也有数据,数据不相等;

  3. 数据库没有数据,缓存有数据;

我们一般使用Cache Aside Pattern的缓存策略来解决不一致问题,简而言之:

​ 1.首先从缓存中读取数据,如果未命中,就从数据库中读取,同步到缓存中并返回

​ 2.需要更新数据库,先更新数据库,在将缓存中的数据清除

解决逻辑:

​ 对于第一种,在读数据的时候会自动将数据库数据同步到缓存中

​ 对于第二种情况,数据变成了不相等,但是在之前某一个时刻,他们一定是相等的,这种不一致,一定是因为更新数据库导致的,而我们的更新策略是先更新数据库,再删除缓存,所以不一致的原因就是删除缓存失败了

​ 对于第三种情况,和第二种情况相似,删除缓存的时候失败了

解决方案:

​ 1.重试删除缓存,要求一致性越高,重试越频繁

​ 2.定期全量更新,比如在每天凌晨一点将所有缓存清空,然后全部重新加载

​ 3.给所有的缓存一个失效期(这个方法非常有效,失效期越短,数据一致性就越高,但是查询数据库加载缓存的频率也就越高,所以根据业务需要来定)

先删除缓存再改数据库和先改数据库后删缓存有什么区别

1.当我们先删除缓存时,此时如果请求来了,就去库读取旧数据,并保存到缓存中

2.然后在修改数据库,此时缓存中就存在的是脏数据

3.以后拿取的就都是脏数据

​ 解决方法:修改完数据库之后,我们再删一次缓存,下次请求来时就会去数据库中拿去数据并保存到缓存中

​ 这里又面临一个问题 (先删数据库后删缓存)

1.当我们修改数据库成功时,还没有删除缓存

2.请求来了,从缓存中拿到脏数据

3.我们删除缓存,下一次请求来的时候,就会去读取数据库更新缓存

​ 所以我们直接选择第二种

说一下SpringCache的常用注解和作用

@Cacheable 进行缓存

@CacheEvict 清除缓存

@CachePut 更新缓存

@Caching 组合多个缓存操作

@CacheConfig 配置一些共有的缓存配置

你们项目中缓存穿透,缓存击穿,缓存雪崩解决方案

缓存穿透

缓存穿透是指当我们请求去查询一个不存在的数据时,缓存中没有,就去数据库中查找,也没有也就无法同步到缓存,下次相同的请求依旧去查询数据库,缓存失去了意义,当并发高时,数据库可能就会挂掉

解决方案

一.将数据库中的空值也放入缓存中,当查询时返回查询的空值,降低访问数据(但是存在一个问题,如果缓存的空值太多,占用更多的空间,我们可以通过设置一个较短的过期时间)

二.将数据库中的数据放入布隆过滤器中,当请求过来时,我们先经过布隆过滤器进行查询,如果存在继续查返回数据,不存在直接丢弃

缓存雪崩

缓存雪崩是指在我们设置缓存时采用了相同的过期时间,导致缓存在某一时刻同时失效,请求全部转发到DB,DB瞬时压力过重雪崩。

解决方案

这里分享一个简单方案就时讲缓存失效时间分散开,比如我们可以在原有的失效时间基础上增加一个随机值,比如1-5分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件。

缓存失效时的雪崩效应对底层系统的冲击非常可怕。大多数系统设计者考虑用加锁或者队列的方式保证缓存的单线 程(进程)写,从而避免失效时大量的并发请求落到底层存储系统上。

缓存击穿

对于一些设置了过期时间的key,如果这些key可能会在某些时间点被超高并发地访问,是一种非常“热点”的数据。这个时候,需要考虑一个问题:缓存被“击穿”的问题,这个和缓存雪崩的区别在于这里针对某一key缓存,前者则是很多key。

缓存在某个时间点过期的时候,恰好在这个时间点对这个Key有大量的并发请求过来,这些请求发现缓存过期一般都会从后端DB加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把后端DB压垮。

解决方案

1.使用互斥锁(mutex key)

业界比较常用的做法,是使用mutex。简单地来说,就是在缓存失效的时候(判断拿出来的值为空),不是立即去load db,而是先使用缓存工具的某些带成功操作返回值的操作(比如Redis的SETNX或者Memcache的ADD)去set一个mutex key,当操作返回成功时,再进行load db的操作并回设缓存;否则,就重试整个get缓存的方法。

2. "提前"使用互斥锁(mutex key):

在value内部设置1个超时值(timeout1), timeout1比实际的memcache timeout(timeout2)小。当从cache读取到timeout1发现它已经过期时候,马上延长timeout1并重新设置到cache。然后再从数据库加载数据并设置到cache中。

3. "永远不过期": 最为实用

这里的“永远不过期”包含两层意思:

(1) 从redis上看,确实没有设置过期时间,这就保证了,不会出现热点key过期问题,也就是“物理”不过期。

(2) 从功能上看,如果不过期,那不就成静态的了吗?所以我们把过期时间存在key对应的value里,如果发现要过期了,通过一个后台的异步线程进行缓存的构建,也就是“逻辑”过期

​ 从实战看,这种方法对于性能非常友好,唯一不足的就是构建缓存时候,其余线程(非构建缓存的线程)可能访问的是老数据,但是对于一般的互联网功能来说这个还是可以忍受。

4. 资源保护:

采用netflix的hystrix,可以做资源的隔离保护主线程池,如果把这个应用到缓存的构建也未尝不可