4. Redis常见面试问题与API使用

1 面试常见问题

1.1 击穿

  1. 服务常见架构

    1. 通常请求需要查询数据,先尝试从redis中查询,如果能取到直接返回
    2. 如果取不到,改为从数据库中取,取到数据后更新缓存并返回结果
    3. 如果数据库中也取不到,直接返回空结果

4. Redis常见面试问题与API使用_第1张图片

  1. redis作为缓存时,其key可能会由于过期、lru、lfu算法而被清理。高并发地访问恰好被清理掉的某个key对应的数据,就会导致高并发访问数据库,这就是缓存的击穿

  2. 解决缓存击穿的客户端代码

    1. get k1:先尝试从缓存中查找k1
    2. setnx k1:如果缓存中没有k1,设置k1的值,且该命令会为k1上锁,且redis是单进程单实例,因此一定会有一个客户端先获取到k1的锁
      1. 上锁成功:访问数据库,将结果更新缓存
      2. 上锁失败:后续的客户端会上锁失败,上锁失败后sleep一段时间,再重复整个流程。先get k1,此时能从缓存获取到k1,就不会再访问数据库
  3. 如果第一个进程在获取到锁后,挂掉,会导致锁永远无法释放,因此可以为锁设置过期时间

  4. 假设过期时间1s,如果确实第一个客户端获取锁后,访问后段数据库需要10s,此时锁被自动释放,后续的客户端进程再次击穿缓存,访问到后端数据库。因此可以考虑为获取到锁的客户端启动一个守护进程,守护进程去监控客户端状态,如果发现客户端并没有死,只是访问数据库时间较长,可以延长锁的过期时间

1.2 穿透

  1. 可以使用布隆过滤器
    1. client包含布隆算法和数据
    2. 客户端持有算法,redis存放数据
    3. redis集成布隆(redis bloom)
  2. 布隆过滤器无法删除
    1. 可以使用布谷鸟这种可以删除的过滤器替代
    2. 从数据库删除后,在redis中存放一个value为空的key,客户端发现该产品的value为空,就知道根本没有该产品信息

1.3 雪崩

  1. 大量的key同时失效,从而间接导致高并发到达数据库,称为雪崩
  2. 解决方案
    1. key必须在某时间点过期(例如0点必须过期)
      1. 参考击穿的解决方案
      2. 业务层增加判断,当0点时,服务器上进行延时,比如先sleep一会再继续访问
    2. key的过期与时点无关:将过期时间设置为随机

1.4 分布式锁

4. Redis常见面试问题与API使用_第2张图片

  1. 同一个客户端用户,同时发出了多个请求,分布式环境下,多台机器上部署的多个service进行了并发操作,导致插入了冗余数据

  2. 修改逻辑为service需要先去抢锁,抢到锁的service,才去数据库操作,这个锁就是分布式锁

  3. 分布式锁的实现

    1. Redis
    2. Zookeeper
    3. Memcached
    4. Chubby
  4. redis实现分布式锁

    //1. 加锁
    //a. 不使用setnx(lock_sale_商品ID,1)+expire(lock_sale_商品ID, 30)是为了防止加锁后,还未设置过期时间这段时间内进程挂掉,导致的死锁
    //b. 如果线程A执行了30s没执行完,但此时由于key到期,线程B进入,而此时A处理完成,del时,可能误将B的锁删除,因此需要将value设置为线程id,且当当前线程id和加锁时的线程id相同,才允许删除
    //c. 但无法解决B可能会和A访问同一段代码的问题
    //d. 可以为A开启一个守护线程,守护线程从A进行了29s后开始执行,之后每20s执行一次,如果发现A未处理完成,使用expire命令为A的锁延长20s,当A结束后,关闭其守护进程。如果节点1忽然断电,由于线程A和守护线程在同一个进程,守护线程也会停下。这把锁到了超时的时候,没人给它续命,也就自动释放了
    String threadId = Thread.currentThread().getId()
    set(key,threadId ,30,NX)
    
    //2. 解锁
    if(threadId .equals(redisClient.get(key)){
        del(key)
    }
    

2 API

  1. spring官网–Projects–Spring Data–Spring Data Redis–LEARN–Reference Doc.
  2. java中可以使用lettuce、Jedis、Redisson等开源api访问redis,spring中也集成了它们,但现在默认使用lettuce对redis进行连接
  3. Jedis和lettuce的简单使用:可以参考各自github中的文档,都是需要先通过maven引入,然后输入ip和port来创建Jedis或lettuce实例,最后使用具体方法发送命令给redis
  4. Jedis线程不安全,多线程使用同一个Jedis实例会有问题,通常采用一个称为JedisPool的连接池,为每个线程分配一个连接,但这样会导致大量socket连接在服务端上,成本较高
  5. RedisConnectionFactory:用于建立与Redis的连接
  6. RedisTemplate:用于真正发送指令给redis,由RedisConnectionFactory创建,可以使用RedisTemplate这种高阶的API来操作redis,也可以通过RedisTemplate来获取一个RedisConnection对象,从而调用lettuce、Jedis、Redisson等API的原生方法(低阶别API)
  7. Serializers:由于Redis是二进制安全的,该类可以用于指定如何将数据转为二进制码,比如是直接通过java序列化转为二进制码,还是先转为一个jason格式的字符串,再转为二进制码。如果写和读使用的二进制转换的机制不同(例如写入使用java序列化,读取使用python的逻辑),可能会导致客户端无法正确解析读取到的二进制码

2.1 使用spring boot连接redis

  1. File–New–Project–Spring Initializr–com.msb.spring.redis–NoSQL–Spring Data Redis(Access+Driver)

  2. application.properties

    spring.redis.host=172.128.246.128
    spring.redis.port=6379
    
  3. TestRedis

    package com.msb.spring.redis.demo;
    
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.data.redis.core.RedisTemplate;
    import org.springframework.stereotype.Component;
    
    @Component
    public class TestRedis {
        //1. spring boot会自动根据application.properties中配置的内容,创建RedisConnectionFactory,并通过RedisConnectionFactory创建RedisTemplate
        //2. 之后根据Autowired注释,将容器中的RedisTemplate对象自动注入到redisTemplate中
        @Autowired
        RedisTemplate redisTemplate;
        public void testRedis(){
            //3. 表示获取redis的string类型的命令
            redisTemplate.opsForValue().set("hello","china");
            System.out.println(redisTemplate.opsForValue().get("hello"));
        }
    }
    
  4. DemoApplication

    package com.msb.spring.redis.demo;
    
    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    import org.springframework.context.ConfigurableApplicationContext;
    
    @SpringBootApplication
    public class DemoApplication {
        public static void main(String[] args) {
            //1. 用于初始化spring容器
            ConfigurableApplicationContext ctx = SpringApplication.run(DemoApplication.class, args);
            TestRedis redis = ctx.getBean(TestRedis.class);
            redis.testRedis();
        }
    
    }
    
  5. redis服务默认的安全策略是禁止远端访问,且绑定必须通过127.0.0.1访问,即telnet 192.168.246.128 6379不通。需要通过修改配置文件protected-mode no开启远端访问,#bind 127.0.0.1取消ip绑定。也可以通过本地客户端连接后执行config set protected-mode no命令临时开启远端访问。config get *可以查看当前redis服务的所有配置参数

  6. 执行DemoApplication后,发现执行成功,正确打印china,但使用redis-cli直接连接redis服务时,发现存放的key和value并不是hello和china,而是一堆乱码。这是因为在使用redisTemplate将key和value转为二进制码时,默认使用的是java的序列化机制,也就是将一个String对象序列到了redis中,而不是将字符串本身直接转为二进制码

2.2 不使用java序列化

  1. TestRedis

    //StringRedisTemplate要求key和value都必须为string类型,同时会将String对象,当作字符串转为二进制码,这样存放到redis中的key和value就不再是乱码
    @Autowired
    StringRedisTemplate stringRedisTemplate;
    public void testRedis(){
      stringRedisTemplate.opsForValue().set("hello","china");
      System.out.println(stringRedisTemplate.opsForValue().get("hello"));
    }
    
  2. 使用低阶API操作redis

    //redis中存放的key和value也是正常的
    RedisConnection connection = redisTemplate.getConnectionFactory().getConnection();
    connection.set("hello02".getBytes(),"mashibing".getBytes());
    System.out.println(new String(connection.get("hello02".getBytes())));
    

2.3 hash操作

  1. 操作hash

    HashOperations<String, Object, Object> hash = stringRedisTemplate.opsForHash();
    hash.put("sean","name","zhouzhilei");
    //由于使用了stringRedisTemplate,因此无法使用12这个整型作为value,必须使用string类型作为value
    hash.put("sean","age","12");
    //打印:{name=zhouzhilei, age=12}
    System.out.println(hash.entries("sean"));
    
  2. 使用Jackson2HashMapper将java对象转为Map类型,从而存放到redis的hash类型变量中

    1. 引入pom依赖

      <dependency>
        <groupId>org.springframework.bootgroupId>
        <artifactId>spring-boot-starter-jsonartifactId>
      dependency>
      
    2. Person

      package com.msb.spring.redis.demo;
      
      public class Person {
          private String name;
          private Integer age;
      
          public String getName() {
              return name;
          }
      
          public void setName(String name) {
              this.name = name;
          }
      
          public Integer getAge() {
              return age;
          }
      
          public void setAge(Integer age) {
              this.age = age;
          }
      }
      
    3. TestRedis

      Person p = new Person();
      p.setName("zhangsan");
      p.setAge(16);
      //1. 选择true时,会采用扁平化的方式转为jason
      //2. 不采用扁平化时,jason内容为
      //{"firstname" : "Jon", "lastname" : "Snow", "address" : { "city" : "Castle Black", "country" : "The North" }, "date" : "1561543964015", "localDateTime" : "2018-01-02T12:13:14"}
      //3. 采用扁平化处理时,jason内容为
      //{"firstname" : "Jon", "lastname" : "Snow", "address.city" : "Castle Black", "address.country" : "The North", "date" : "1561543964015", "localDateTime" : "2018-01-02T12:13:14"}
      //public class Person {
      //    String firstname;
      //    String lastname;
      //    Address address;
      //    Date date;
      //    LocalDateTime localDateTime;
      //}
      //
      //public class Address {
      //    String city;
      //    String country;
      //}
      Person p = new Person();
      p.setName("zhangsan");
      p.setAge(16);
      Jackson2HashMapper jm = new Jackson2HashMapper(objectMapper, false);
      redisTemplate.opsForHash().putAll("sean01",jm.toHash(p));
      Map map = redisTemplate.opsForHash().entries("sean01");
      Person per = objectMapper.convertValue(map,Person.class);
      System.out.println(per.getName());
      
  3. 由于使用了RedisTemplate,所以会对Key和Value进行java序列化,导致存入redis的字节码只能用java程序读取。但如果改为使用StringRedisTemplate,由于Person中存在Integer类型的age,Person转为的Map中的value为Integer类型,会导致放入redis失败

  4. 可以为RedisTemplate或StringRedisTemplate设置hash中value的序列化器,StringRedisTemplate默认使用的就是StringRedisSerializer,因此可以将String对象以字符串的方式转为二进制码

  5. MyTemplate

    package com.msb.spring.redis.demo;
    
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.data.redis.connection.RedisConnectionFactory;
    import org.springframework.data.redis.core.StringRedisTemplate;
    import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
    
    @Configuration
    public class MyTemplate {
      	//1. 之前spring管理了一个StringRedisTemplate类型的bean,id为stringRedisTemplate,现在加上一个id为ooxx的StringRedisTemplate类型的bean,下面StringRedisTemplate类型的变量使用新的bean进行注入
        @Bean
        public StringRedisTemplate ooxx(RedisConnectionFactory fc){
            StringRedisTemplate tp = new StringRedisTemplate(fc);
          	//2. 可以分别为key、value、hash中的key、hash中的value设置序列化器,由于stringRedisTemplate已经规定了key、value、hash中的key都以字符串的形式转为二进制码,因此只需要对hash的value进行特殊设置即可
            tp.setHashValueSerializer(new Jackson2JsonRedisSerializer<Object>(Object.class));
            return tp;
        }
    }
    
  6. TestRedis

    @Autowired
    @Qualifier("ooxx")
    StringRedisTemplate stringRedisTemplate;
    
    ...
      
    Person p = new Person();
    p.setName("zhangsan");
    p.setAge(16);
    Jackson2HashMapper jm = new Jackson2HashMapper(objectMapper, false);
    stringRedisTemplate.opsForHash().putAll("sean01",jm.toHash(p));
    Map map = stringRedisTemplate.opsForHash().entries("sean01");
    Person per = objectMapper.convertValue(map,Person.class);
    System.out.println(per.getName());
    

2.4 实现聊天室

RedisConnection cc = stringRedisTemplate.getConnectionFactory().getConnection();
//	订阅频道,此时使用redis-cli连接一个客户端,执行publish ooxx "xiaohong: Hello",订阅中就会收到该消息,收到后触发onMessage,就会打印消息
cc.subscribe(new MessageListener() {
  @Override
  public void onMessage(Message message, byte[] pattern) {
    byte[] body = message.getBody();
    System.out.println(new String(body));
  }
}, "ooxx".getBytes());

while(true){
  // 向频道发布消息
  stringRedisTemplate.convertAndSend("ooxx","我: hello  from wo zi ji ");
  try {
    Thread.sleep(3000);
  } catch (InterruptedException e) {
    e.printStackTrace();
  }
}

你可能感兴趣的:(Redis)