springboot整合redis redis中常见的面试题

1.springboot整合redis。

springboot整合redis时提供了两个模板工具类,StringRedisTemplate和RedisTemplate.

StringRedisTemplate

(1) 引入相关的依赖

  
            org.springframework.boot
            spring-boot-starter-data-redis
        

(2)注入StringRedisTemplate该类对象

 @Autowired
 private StringRedisTemplate redisTemplate;

(3)使用StringRedisTemplate

该类把对每种数据类型的操作,单独封了相应的内部类。

package springbootredis0802.demo;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.redis.core.HashOperations;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.ValueOperations;

import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;

@SpringBootTest
class DemoApplicationTests {
    @Autowired
    private StringRedisTemplate redisTemplate;
    @Test
    public void test02(){
        //对hash类型的操作。
        HashOperations forhash = redisTemplate.opsForHash();
        forhash.put("k1","name","张三");
        forhash.put("k1","age","15");
        Map map=new HashMap<>();
        map.put("name","李四");
        map.put("age","25");
        forhash.putAll("k2",map );
        Object o = forhash.get("k1", "name");
        System.out.println(o);
        Set k1 = forhash.keys("k1");
        System.out.println(k1);
        List k11 = forhash.values("k1");
        System.out.println(k11);

        //获取k1对于的所有的field和value
        Map k12 = forhash.entries("k1");
        System.out.println(k12);
    }




    @Test
    void contextLoads(){
        //删除指定的key
//        redisTemplate.delete();
        //查看所有的key
//        redisTemplate.keys()
        //是否存在指定的key
//         redisTemplate.hasKey()

        //对字符串数据类型的操作ValueOperations
        ValueOperations forValue = redisTemplate.opsForValue();
        //存储字符串类型
        forValue.set("k1","张三",30, TimeUnit.SECONDS);
//等价于setnx  存入成功返回true,失败返回false
        Boolean absent = forValue.setIfAbsent("k11", "李四", 30, TimeUnit.SECONDS);
        System.out.println(absent);
        Integer append = forValue.append("k11", "真丑");
        String k11 = forValue.get("k11");
        System.out.println(k11);


    }

}
 
  

RedisTemplate

package springbootredis0802.demo;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.redis.core.HashOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;

@SpringBootTest
class DemoApplicationTests02 {
    @Autowired
    private RedisTemplate redisTemplate;
    @Test
    public void test01(){
        //必须认为指定序列化方式   自己创建了config类 就不用下面两行代码
       // redisTemplate.setKeySerializer(new StringRedisSerializer());
        //redisTemplate.setValueSerializer(new Jackson2JsonRedisSerializer(Object.class));

        //对string类型操作
        ValueOperations forValue = redisTemplate.opsForValue();
        //redis中key和value都成了乱码
        //key和value都没有指定序列话方式 默认采用jdk的序列化方式
        forValue.set("k1","张三");

        //value 默认采用jdk 类必须实现序列化接口
        forValue.set("k2",new User(1,"王五",23));
    }


}
 
  

上面的RedisTemplate需要每次都指定key value以及field的序列化方式,搞一个配置类,已经为RedisTemplate指定好序列化。以后再用就无需指定。

@Configuration
public class RedisConfig {

    @Bean
    public RedisTemplate redisTemplate(RedisConnectionFactory factory) {
        RedisTemplate template = new RedisTemplate<>();
        RedisSerializer redisSerializer = new StringRedisSerializer();
        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);
        template.setConnectionFactory(factory);
        //key序列化方式
        template.setKeySerializer(redisSerializer);
        //value序列化
        template.setValueSerializer(jackson2JsonRedisSerializer);
        //value hashmap序列化  filed value
        template.setHashValueSerializer(jackson2JsonRedisSerializer);
        template.setHashKeySerializer(redisSerializer);
        return template;
    }
}

2. redis的使用场景

2.1 作为缓存

(1)数据存储在内存中,数据查询速度快。可以分摊数据库压力。

springboot整合redis redis中常见的面试题_第1张图片

 

(2)什么样的数据适合放入缓存

查询频率比较高,修改频率比较低。

安全系数低的数据

(3)使用redis作为缓存

@Service
public class DeptService {

    @Autowired
    private DeptMapper deptMapper;

    @Autowired
    private RedisTemplate redisTemplate;

    //业务代码
    public Dept findById(Integer id){
        ValueOperations forValue = redisTemplate.opsForValue();
        //查询缓存
        Object o = forValue.get("dept::" + id);
        //缓存命中
        if(o!=null){
            return (Dept) o;
        }
        Dept dept = deptMapper.selectById(id);
        if(dept!=null){
            //存入缓存中
            forValue.set("dept::"+id,dept,2, TimeUnit.HOURS);
        }
        return dept;
    }

    public int deleteById(Integer id){
        redisTemplate.delete("dept::"+id);
        int row = deptMapper.deleteById(id);
        return row;
    }

    public Dept insert(Dept dept){
        int insert = deptMapper.insert(dept);
        return dept;
    }

    public Dept update(Dept dept){
        ValueOperations forValue = redisTemplate.opsForValue();
        forValue.set("dept::"+dept.getId(),dept,2, TimeUnit.HOURS);
        int insert = deptMapper.updateById(dept);
        return dept;
    }
}

查看的缓存: 前部分代码相同@before通知,后部分代码也相同后置通知。 我们可以AOP完成缓存代码和业务代码分离。

spring框架它应该也能想到。--使用注解即可完成。解析该注解。

(1)把缓存的配置类加入

@Bean
    public CacheManager cacheManager(RedisConnectionFactory factory) {
        RedisSerializer redisSerializer = new StringRedisSerializer();
        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);
        // 配置序列化(解决乱码的问题),过期时间600秒
        RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
                .entryTtl(Duration.ofSeconds(600)) //缓存过期10分钟 ---- 业务需求。
                .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(redisSerializer))//设置key的序列化方式
                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer)) //设置value的序列化
                .disableCachingNullValues();
        RedisCacheManager cacheManager = RedisCacheManager.builder(factory)
                .cacheDefaults(config)
                .build();
        return cacheManager;
    }

(2)使用开启缓存注解

springboot整合redis redis中常见的面试题_第2张图片

 (3)使用注解

//业务代码
    //使用查询注解:cacheNames表示缓存的名称 key:唯一标志---dept::key
    //先从缓存中查看key为(cacheNames::key)是否存在,如果存在则不会执行方法体,如果不存在则执行方法体并把方法的返回值存入缓存中
    @Cacheable(cacheNames = {"dept"},key="#id")
    public Dept findById(Integer id){
        Dept dept = deptMapper.selectById(id);
        return dept;
    }
//先删除缓存在执行方法体。
    @CacheEvict(cacheNames = {"dept"},key = "#id")
    public int deleteById(Integer id){
        int row = deptMapper.deleteById(id);
        return row;
    }

    //这个注释可以确保方法被执行,同时方法的返回值也被记录到缓存中,实现缓存与数据库的同步更新。
    @CachePut(cacheNames = "dept",key="#dept.id")
    public Dept update(Dept dept){
        int insert = deptMapper.updateById(dept);
        return dept;
    }

2.2 分布式锁

使用压测工具测试高并发下带来线程安全问题

springboot整合redis redis中常见的面试题_第3张图片

我们看到同一个库存被使用了n次。以及数据库中库存为负数。 线程安全问题导致。

1. 解决方案: 使用 synchronized 或者lock锁

@Service
public class ProductStockServiceImpl2 implements ProductStockService {
    @Autowired
    private ProductStockDao productStockDao;

    @Override
    public  String decreaseStock(Integer productId) {
              synchronized (this) {
                  //查看该商品的库存数量
                  Integer stock = productStockDao.findStockByProductId(productId);
                  if (stock > 0) {
                      //修改库存每次-1
                      productStockDao.updateStockByProductId(productId);
                      System.out.println("扣减成功!剩余库存数:" + (stock - 1));
                      return "success";
                  } else {
                      System.out.println("扣减失败!库存不足!");
                      return "fail";
                  }
              }

    }
}

使用synchronized 或者lock锁 如果我们搭建了项目集群,那么该锁无效。

springboot整合redis redis中常见的面试题_第4张图片

 使用idea开集群项目

springboot整合redis redis中常见的面试题_第5张图片

 springboot整合redis redis中常见的面试题_第6张图片

 springboot整合redis redis中常见的面试题_第7张图片

 

3. redis的解决分布式锁的bug。

springboot整合redis redis中常见的面试题_第8张图片

 

为持有锁的线程开启一个守护线程,守护线程会每隔10秒检查当前线程是否还持有锁,如果持有则延迟生存时间。

使用:


            org.redisson
            redisson
            3.13.4
        
        
         //获取redisson对象并交于spring容器管理
    @Bean
    public Redisson redisson(){
        Config config =new Config();
        config.useSingleServer().
                setAddress("redis://localhost:6379").
                //redis默认有16个数据库
                setDatabase(0);
        return (Redisson) Redisson.create(config);
    }
@Override
    public String decreaseStock(Integer productId) {
        //获取锁对象
        RLock lock = redisson.getLock("aaa::" + productId);
        try {
            lock.lock(30, TimeUnit.SECONDS);
            //查看该商品的库存数量
            Integer stock = productStockDao.findStockByProductId(productId);
            if (stock > 0) {
                //修改库存每次-1
                productStockDao.updateStockByProductId(productId);
                System.out.println("扣减成功!剩余库存数:" + (stock - 1));
                return "success";
            } else {
                System.out.println("扣减失败!库存不足!");
                return "fail";
            }
        } finally {
            lock.unlock();
        }


    }

4. redis中常见的面试题

4.1 什么是缓存穿透?怎么解决?

1. 数据库中没有该记录,缓存中也没有该记录,这时由人恶意大量访问这样的数据。这样就会导致该请求绕过缓存,直接访问数据,从而造成数据库压力过大。

2.解决办法:
   [1]在controller加数据校验。
   [2]我们可以在redis中存入一个空对象,而且要设置过期时间不能太长。超过5分钟
   [3]我们使用布隆过滤器。底层:有一个bitmap数组,里面存储了该表的所有id.



//伪代码
String get(String key) { //布隆过滤器钟存储的是数据库表钟对应的id
    String value = redis.get(key);  //先从缓存获取。  
    if (value  == null) { //缓存没有命中
        if(!bloomfilter.mightContain(key)){//查看布隆过滤器钟是否存在
            return null; 
        }else{
            value = db.get(key); //查询数据库
            redis.set(key, value); 
        }    
    }
    return value;
}

4.2 什么是缓存雪崩?如何解决?

缓存雪崩是指缓存中数据大批量到过期时间,而查询数据量巨大,引起数据库压力过大甚至down机。和缓存击穿不同的是, 缓存击穿指并发查同一条数据,缓存雪崩是不同数据都过期了,很多数据都查不到从而查数据库。
1.什么下会发生缓存雪崩:
  [1]项目刚上线,缓存中没有任何数据
  [2]缓存出现大量过期。
  [3]redis宕机
  
2.解决办法: 
   1.上线前预先把一些热点数据放入缓存。
   2.设置过期时间为散列值
   3.搭建redis集群

4.4 什么是缓存击穿?如何解决?

缓存击穿是指缓存中没有但数据库中有的数据(一般是缓存时间到期),这时由于并发用户特别多,同时读缓存没读到数据,又同时去数据库去取数据,引起数据库压力瞬间增大,造成过大压力。

缓存击穿解决方案:
1.设置永久不过期。【这种只适合内存】
2.使用互斥锁(mutex key)业界比较常用的做法。

springboot整合redis redis中常见的面试题_第9张图片

 

4.5 Redis 淘汰策略有哪些?

volatile-lru :从已设置过期时间的数据集( server. db[i].
expires )中挑 选最近最少使用的数据淘汰。
volatile-ttl :从已设置过期时间的数据集( server. db[i].
expires )中挑 选将要过期的数据淘汰。
volatile-random :从已设置过期时间的数据集( server. db[i].
expires )中 任意选择数据淘汰。
allkeys-lru :从数据集( server. db[i]. dict )中挑选最近最少使
用的数据 淘汰。
allkeys-random :从数据集( server. db[i]. dict )中任意选择数
据淘汰。
no-enviction (驱逐):禁止驱逐数据。

你可能感兴趣的:(redis,spring,boot,数据库)