Redis

初识Redis

Redis是一种键值型的NoSQL数据库,这里有两个关键字

  • 键值型
  • NoSQL 其中键值型是指Redis中存储的数据都是以Key-Value键值对的形式存储,而Value的形式多种多样,可以使字符串、数值甚至Json

而NoSQL则是相对于传统关系型数据库而言,有很大差异的一种数据库

认识NoSQL

NoSql可以翻译做Not Only Sql(不仅仅是SQL),或者是No Sql(非Sql的)数据库。是相对于传统关系型数据库而言,有很大差异的一种特殊的数据库,因此也称之为非关系型数据库。

结构化与非结构化

传统关系型数据库是结构化数据,每张表在创建的时候都有严格的约束信息,如字段名、字段数据类型、字段约束等,插入的数据必须遵循这些约束

而NoSQL则对数据库格式没有约束,可以是键值型,也可以是文档型,甚至是图格式

关联与非关联

传统数据库的表与表之间往往存在关联,例如外键约束

而非关系型数据库不存在关联关系,要维护关系要么靠代码中的业务逻辑,要么靠数据之间的耦合

{
  id: 1,
  name: "张三",
  orders: [
    {
       id: 1,
       item: {
	 id: 10, title: "荣耀6", price: 4999
       }
    },
    {
       id: 2,
       item: {
	 id: 20, title: "小米11", price: 3999
       }
    }
  ]
}

例如此处要维护张三与两个手机订单的关系,不得不冗余的将这两个商品保存在张三的订单文档中,不够优雅,所以建议使用业务逻辑来维护关联关系

查询方式

传统关系型数据库会基于Sql语句做查询,语法有统一的标准

SELECT id, age FROM tb_user WHERE id = 1

而不同的非关系型数据库查询语法差异极大

Redis:  get user:1
MongoDB: db.user.find({_id: 1})
elasticsearch:  GET http://localhost:9200/users/1

事务

传统关系型数据库能满足事务的ACID原则(原子性、一致性、独立性及持久性)

而非关系型数据库汪汪不支持事务,或者不能要个保证ACID的特性,只能实现计本的一致性

总结

特性 SQL NoSQL
数据结构 结构化 (Structured) 非结构化 (Unstructured)
数据关联 关联的 (Relational) 无关联的 (Non-relational)
查询方式 SQL 查询 非 SQL (Non-SQL)
事务特性 ACID BASE
存储方式 磁盘 (Disk-based) 内存 (Memory-based)
扩展性 垂直扩展 (Vertical scalability) 水平扩展 (Horizontal scalability)
使用场景 1) 数据结构固定 1) 数据结构不固定
2) 对一致性、安全性要求高 2) 相关业务对数据安全性、一致性要求较高
3) 对性能要求高
  • 存储方式
    • 关系型数据库基于磁盘进行存储,会有大量的磁盘IO,对性能有一定影响
    • 非关系型数据库,他们的操作更多的是依赖于内存来操作,内存的读写速度会非常快,性能自然会好一些
  • 扩展性
    • 关系型数据库集群模式一般是主从,主从数据一致,起到数据备份的作用,称为垂直扩展。
    • 非关系型数据库可以将数据拆分,存储在不同机器上,可以保存海量数据,解决内存大小有限的问题。称为水平扩展。
    • 关系型数据库因为表之间存在关联关系,如果做水平扩展会给数据查询带来很多麻烦

认识Redis

Redis诞生于2009年,全称是Remote Dictionary Server远程词典服务器,是一个基于内存的键值型NoSQL数据库。

特征:

  • 键值(Key-Value)型,Value支持多种不同的数据结构,功能丰富
  • 单线程,每个命令具有原子性
  • 低延迟,速度快(基于内存、IO多路复用、良好的编码)
  • 支持数据持久化
  • 支持主从集群、分片集群
  • 支持多语言客户端

Redis常用命令

Redis是典型的key-value数据库,key一般是字符串,而value包含很多不同的数据类型

1

Redis通用命令

命令 描述
KEYS pattern 查找所有符合给定模式 (pattern) 的 key
EXISTS key 检查给定 key 是否存在
TYPE key 返回 key 所储存的值的类型
TTL key 返回给定 key 的剩余生存时间 (TTL, time to live),以秒为单位
DEL key 该命令用于在 key 存在时删除 key
  • KEYS:查看符合模板的所有key
    • 不建议在生产环境设备上使用,因为Redis是单线程的,执行查询的时候会阻塞其他命令,当数据量很大的时候,使用KEYS进行模糊查询,效率很差
  • DEL:删除一个指定的key
    • 也可以删除多个key,DEL name age,会将name和age都删掉
  • EXISTS:判断key是否存在
    • EXISTS name,如果存在返回1,不存在返回0
  • EXPIRE:给一个key设置有效期,有效期到期时该key会被自动删除
    • EXPIRE name 20,给name设置20秒有效期,到期自动删除
  • TTL:查看一个key的剩余有效期(Time-To-Live)
    • TTL name,查看name的剩余有效期,如果未设置有效期,则返回-1

String类型

String类型,也就是字符串类型,是Redis中最简单的存储类型

其value是字符串,不过根据字符串的格式不同,又可以分为3类

  • string:普通字符串
  • int:整数类型,可以做自增、自减操作
  • float:浮点类型,可以做自增、自减操作

不管是哪种格式,底层都是字节数组形式存储,只不过是编码方式不同,字符串类型的最大空间不能超过512M

String的常用命令

String的常用命令有

命令 描述
SET 添加或者修改一个已经存在的 String 类型的键值对
GET 根据 key 获取 String 类型的 value
MSET 批量添加多个 String 类型的键值对
MGET 根据多个 key 获取多个 String 类型的 value
INCR 让一个整型的 key 自增 1
INCRBY 让一个整型的 key 自增并指定步长值,例如:INCRBY num 2num 值自增 2
INCRBYFLOAT 让一个浮点类型的数字自增并指定步长值
SETNX 添加一个 String 类型的键值对,前提是这个 key 不存在,否则不执行,可以理解为真正的新增
SETEX 添加一个 String 类型的键值对,并指定有效期

Key结构

Redis没有类似MySQL中Table的概念,那么我们该如何区分不同类型的Key呢?

例如:需要存储用户、商品信息到Redis,有一个用户的id是1,有一个商品的id恰好也是1,如果此时使用id作为key,那么就回冲突,该怎么办?

我们可以通过给key添加前缀加以区分,不过这个前缀不是随便加的,有一定的规范

Redis的key允许有多个单词形成层级结构,多个单词之间用:隔开,格式如下

项目名:业务名:类型:id

这个格式也并非是固定的,可以根据自己的需求来删除/添加词条,这样我们就可以把不同数据类型的数据区分开了,从而避免了key的冲突问题

例如我们的项目名叫reggie,有user和dish两种不同类型的数据,我们可以这样定义key

  • user相关的key:reggie:user:1
  • dish相关的key:reggie:dish:1

如果value是一个Java对象,例如一个User对象,则可以将对象序列化为JSON字符串后存储

KEY VALUE
reggie:user:1 {“id”:1, “name”: “Jack”, “age”: 21}
reggie:dish:1 {“id”:1, “name”: “鲟鱼火锅”, “price”: 4999}

并且在Redis的桌面客户端中,也会以相同前缀作为层次结构,让数据看起来层次分明,关系清晰

Hash类型

Hash类型,也叫散列,其中value是一个无序字典,类似于Java中的HashMap结构

String结构是将对象序列化为JSON字符串后存储,当我们要修改对象的某个属性值的时候很不方便

Hash结构可以将对象中的每个字段独立存储,可以针对单个字段做CRUD

KEY VALUE
field value
reggie:user:1 name Jack
age 21
reggie:user:2 name Rose
age 18
Hash的常用命令有
命令 描述
HSET key field value 添加或者修改 hash 类型 key 的 field 的值
HGET key field 获取一个 hash 类型 key 的 field 的值
HMSET 批量添加多个 hash 类型 key 的 field 的值
HMGET 批量获取多个 hash 类型 key 的 field 的值
HGETALL 获取一个 hash 类型的 key 中的所有的 field 和 value
HKEYS 获取一个 hash 类型的 key 中的所有的 field
HINCRBY 让一个 hash 类型 key 的字段值自增并指定步长
HSETNX 添加一个 hash 类型的 key 的 field 值,前提是这个 field 不存在,否则不执行

List类型

Redis中的List类型与Java中的LinkedList类似,可以看做是一个双向链表结构。既可以支持正向检索和也可以支持反向检索。

特征也与LinkedList类似:

  • 有序
  • 元素可以重复
  • 插入和删除快
  • 查询速度一般
  • 常用来存储一个有序数据,例如:朋友圈点赞列表,评论列表等。

List的常见命令有:

命令 描述
LPUSH key element ... 向列表左侧插入一个或多个元素
LPOP key 移除并返回列表左侧的第一个元素,没有则返回 nil
RPUSH key element ... 向列表右侧插入一个或多个元素
RPOP key 移除并返回列表右侧的第一个元素
LRANGE key start end 返回一段索引范围内的所有元素
BLPOPBRPOP LPOPRPOP 类似,只不过在没有元素时等待指定时间,而不是直接返回 nil

Set类型

Redis的Set结构与Java中的HashSet类似,可以看做是一个value为null的HashMap。因为也是一个hash表,因此具备与HashSet类似的特征:

  • 无序
  • 元素不可重复
  • 查找快
  • 支持交集、并集、差集等功能

Set的常见命令有:

命令 描述
SADD key member ... 向 set 中添加一个或多个元素
SREM key member ... 移除 set 中的指定元素
SCARD key 返回 set 中元素的个数
SISMEMBER key member 判断一个元素是否存在于 set 中
SMEMBERS key 获取 set 中的所有元素
SINTER key1 key2 ... 求 key1 与 key2 的交集
SUNION key1 key2 ... 求 key1 与 key2 的并集
SDIFF key1 key2 ... 求 key1 与 key2 的差集

SortedSet类型

Redis的SortedSet是一个可排序的set集合,与Java中的TreeSet有些类似,但底层数据结构却差别很大。SortedSet中的每一个元素都带有一个score属性,可以基于score属性对元素排序,底层的实现是一个跳表(SkipList)加 hash表。

SortedSet具备下列特性:

  • 可排序
  • 元素不重复
  • 查询速度快 因为SortedSet的可排序特性,经常被用来实现排行榜这样的功能。

SortedSet的常见命令有:

命令 描述
ZADD key score member 添加一个或多个元素到 sorted set ,如果已经存在则更新其 score 值
ZREM key member 删除 sorted set 中的一个指定元素
ZSCORE key member 获取 sorted set 中的指定元素的 score 值
ZRANK key member 获取 sorted set 中的指定元素的排名
ZCARD key 获取 sorted set 中的元素个数
ZCOUNT key min max 统计 score 值在给定范围内的所有元素的个数
ZINCRBY key increment member 让 sorted set 中的指定元素自增,步长为指定的 increment 值
ZRANGE key min max 按照 score 排序后,获取指定排名范围内的元素
ZRANGEBYSCORE key min max 按照 score 排序后,获取指定 score 范围内的元素
ZDIFFZINTERZUNION 求差集、交集、并集

Redis的Java客户端

目前主流的Redis的Java客户端有三种

  • Jedis和Lettuce:这两个主要是提供了Redis命令对应的API,方便我们操作Redis,而SpringDataRedis又对这两种做了抽象和封装,因此我们后期会直接以SpringDataRedis来学习。
  • Redisson:是在Redis基础上实现了分布式的可伸缩的java数据结构,例如Map、Queue等,而且支持跨进程的同步机制:Lock、Semaphore等待,比较适合用来实现特殊的功能需求。

Jedis客户端

快速入门

  1. 导入Jedis的maven坐标
<!--jedis-->
<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
    <version>3.7.0</version>
</dependency>
<!--单元测试-->
<dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter</artifactId>
    <version>5.7.0</version>
    <scope>test</scope>
</dependency>
  1. 建立连接 新建一个单元测试类
private Jedis jedis;

@BeforeEach
void setUp() {
    //1. 建立连接
    jedis = new Jedis("101.42.225.160", 6379);
    //2. 设置密码
    jedis.auth("root");
    //3. 选择库
    jedis.select(0);
}
  1. 测试
@Test
void testString(){
    jedis.set("name","Kyle");
    String name = jedis.get("name");
    System.out.println("name = " + name);
}

@Test
void testHash(){
    jedis.hset("reggie:user:1","name","Jack");
    jedis.hset("reggie:user:2","name","Rose");
    jedis.hset("reggie:user:1","age","21");
    jedis.hset("reggie:user:2","age","18");
    Map<String, String> map = jedis.hgetAll("reggie:user:1");
    System.out.println(map);
}
  1. 释放资源
@AfterEach
void tearDown(){
    if (jedis != null){
        jedis.close();
    }
}

连接池

Jedis本身是线程不安全的,并且频繁的创建和销毁连接会有性能损耗,因此我们推荐大家使用Jedis连接池代替Jedis的直连方式。

新建一个util,用于存放我们编写的工具类

后面我们使用SpringDataRedis的时候,可以直接在yml配置文件里配置这些内容

public class JedisConnectionFactory {

    private static JedisPool jedisPool;

    static {
        // 配置连接池
        JedisPoolConfig poolConfig = new JedisPoolConfig();
        poolConfig.setMaxTotal(8);
        poolConfig.setMaxIdle(8);
        poolConfig.setMinIdle(0);
        poolConfig.setMaxWaitMillis(1000);
        // 创建连接池对象,参数:连接池配置、服务端ip、服务端端口、超时时间、密码
        jedisPool = new JedisPool(poolConfig, "101.42.225.160", 6379, 1000, "root");
    }

    public static Jedis getJedis(){
        return jedisPool.getResource();
    }
}

之后我们的测试类就可以修改为如下

@SpringBootTest
class RedisTestApplicationTests {

    private Jedis jedis = JedisConnectionFactory.getJedis();

    @Test
    void testString(){
        jedis.set("name","Kyle");
        String name = jedis.get("name");
        System.out.println("name = " + name);
    }

    @Test
    void testHash(){
        jedis.hset("reggie:user:1","name","Jack");
        jedis.hset("reggie:user:2","name","Rose");
        jedis.hset("reggie:user:3","name","Kyle");
        jedis.hset("reggie:user:1","age","21");
        jedis.hset("reggie:user:2","age","18");
        jedis.hset("reggie:user:3","age","18");
        Map<String, String> map = jedis.hgetAll("reggie:user:1");
        System.out.println(map);
    }

    @AfterEach
    void tearDown(){
        if (jedis != null){
            jedis.close();
        }
    }
}

SpringDataRedis客户端

SpringData是Spring中数据操作的模块,包含对各种数据库的集成,其中对Redis的集成模块就叫做SpringDataRedis

官网地址:https://spring.io/projects/spring-data-redis

  • 提供了对不同Redis客户端的整合(Lettuce和Jedis)
  • 提供了RedisTemplate统一API来操作Redis
  • 支持Redis的发布订阅模型
  • 支持Redis哨兵和Redis集群
  • 支持基于Lettuce的响应式编程
  • 支持基于JDK、JSON、字符串、Spring对象的数据序列化及反序列化
  • 支持基于Redis的JDKCollection实现

SpringDataRedis中提供了RedisTemplate工具类,其中封装了各种对Redis的操作。并且将不同数据类型的操作API封装到了不同的类型中:

API 返回值类型 说明
redisTemplate.opsForValue() ValueOperations 操作 String 类型数据
redisTemplate.opsForHash() HashOperations 操作 Hash 类型数据
redisTemplate.opsForList() ListOperations 操作 List 类型数据
redisTemplate.opsForSet() SetOperations 操作 Set 类型数据
redisTemplate.opsForZSet() ZSetOperations 操作 SortedSet 类型数据
redisTemplate 通用的命令

快速入门

  1. 导入依赖
<!--redis依赖-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!--common-pool-->
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-pool2</artifactId>
</dependency>
<!--Jackson依赖-->
<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
</dependency>
<!--lombok-->
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <optional>true</optional>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
</dependency>
  1. 配置Redis
spring:
  redis:
    host: 101.42.225.160
    port: 6379
    password: root
    lettuce:
      pool:
        max-active: 8
        max-idle: 8
        min-idle: 0
        max-wait: 100ms
  1. 注入RedisTemplate
@Autowired
private RedisTemplate redisTemplate;
  1. 编写测试方法
@Test
void stringTest(){
    redisTemplate.opsForValue().set("username","David");
    String username = (String) redisTemplate.opsForValue().get("username");
    System.out.println(username);
}

自定义序列化

RedisTemplate可以接收任意Object作为值写入Redis

只不过写入前会把Object序列化为字节形式,默认是采用JDK序列化,得到的结果是这样的

\xAC\xED\x00\x05t\x00\x06\xE5\xBC\xA0\xE4\xB8\x89

缺点:

  • 可读性差
  • 内存占用较大

我们可以自定义RedisTemplate的序列化方式,代码如下 在config包下编写对应的配置类

@Configuration
public class RedisConfig {

    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
        // 创建RedisTemplate对象
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        // 设置连接工厂
        template.setConnectionFactory(connectionFactory);
        // 创建JSON序列化工具
        GenericJackson2JsonRedisSerializer jsonRedisSerializer =
                new GenericJackson2JsonRedisSerializer();
        // 设置Key的序列化
        template.setKeySerializer(RedisSerializer.string());
        template.setHashKeySerializer(RedisSerializer.string());
        // 设置Value的序列化
        template.setValueSerializer(jsonRedisSerializer);
        template.setHashValueSerializer(jsonRedisSerializer);
        // 返回
        return template;
    }
}

我们编写一个User类,并尝试将其创建的对象存入Redis,看看是什么效果

@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
    private String name;
    private Integer age;
}

测试方法


@Test
void stringTest(){
    redisTemplate.opsForValue().set("userdata",new User("张三",18));
}

这里采用了JSON序列化来代替默认的JDK序列化方式。最终结果如下:

{
  "@class": "com.blog.entity.User",
  "name": "张三",
  "age": 18
}

整体可读性有了很大提升,并且能将Java对象自动的序列化为JSON字符串,并且查询时能自动把JSON反序列化为Java对象。不过,其中记录了序列化时对应的class名称,目的是为了查询时实现自动反序列化。这会带来额外的内存开销。

所以肯定会有更好的方法

StringRedisTemplate

为了节省内存空间,我们可以不使用JSON序列化器来处理value,而是统一使用String序列化器,要求只能存储String类型的key和value。当需要存储Java对象时,手动完成对象的序列化和反序列化。

因为存入和读取时的序列化及反序列化都是我们自己实现的,SpringDataRedis就不会将class信息写入Redis了

这种用法比较普遍,因此SpringDataRedis就提供了RedisTemplate的子类:StringRedisTemplate,它的key和value的序列化方式默认就是String方式。源码如下

public class StringRedisTemplate extends RedisTemplate<String, String> {
    public StringRedisTemplate() {
        this.setKeySerializer(RedisSerializer.string());
        this.setValueSerializer(RedisSerializer.string());
        this.setHashKeySerializer(RedisSerializer.string());
        this.setHashValueSerializer(RedisSerializer.string());
    }

省去了我们自定义RedisTemplate的序列化方式的步骤(可以将之前配置的RedisConfig删除掉),而是直接使用:

@Test
void stringTest() throws JsonProcessingException {
    //创建对象
    User user = new User("张三", 18);
    //手动序列化
    String json = mapper.writeValueAsString(user);
    //写入数据
    stringRedisTemplate.opsForValue().set("userdata", json);
    //获取数据
    String userdata = stringRedisTemplate.opsForValue().get("userdata");
    //手动反序列化
    User readValue = mapper.readValue(userdata, User.class);
    System.out.println(readValue);
}

存入Redis中是这样的

{
  "name": "张三",
  "age": 18
}


Redis面试题

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

缓存穿透是指查询一个不存在的数据,如果从存储层查询不到数据,则不会写入缓存,此时就会导致每次请求这个不存在的数据,都会到DB里去查询,可能会导致DB挂掉,这种情况大概率是遭到了攻击。

解决方案的话,一般可以缓存空数据,即缓存这个不存在的数据。另外一种解决方案就是使用布隆过滤器

好的,那你能介绍一下布隆过滤器吗?

布隆过滤器主要是用于检索一个元素绝对不在集合中或可能在集合中。它的底层主要是先去初始化一个比较大的数组,里面存放二进制的0或1,在一开始都是0,当一个key来了之后经过3个哈希函数的计算,模于数组长度找到数据的下标,然后把下标位置的0改为1,这样的话,三个数组下标的位置就能标明一个key的存在,查找的过程也是一样的。

当然这个也是有缺点的,布隆过滤器可能存在一些误判,我们一般是可以设置这个误判率的,大概不会超过5%,因为哈希冲突不可能避免,所以这个误判是必然存在的,要不然就增加数组长度。但其实5%的误判率一般项目也是能接受的,不至于高并发下压倒数据库。

1

用上图来举个例子

  1. 初始化布隆过滤器为 16 位,每一位初始值都为 0。
  2. 将 Fredy 录入布隆过滤器,经过三个哈希函数的计算,将第 1、3、7 位设为 1。
  3. 将 Eli 录入布隆过滤器,经过三个哈希函数的计算,将第 10、12、15 位设为 1。
  4. 查询 Tom,经过三个哈希函数的计算,得到第 0、2、5 位,这三个位置上的数字都是 0。根据布隆过滤器的规则,可以判断 Tom 绝对不在数据库中。
  5. 查询 Lily,经过三个哈希函数的计算,得到第 7、12、15 位,这三个位置上的数字都是 1。这里需要澄清一点:虽然这三个位置上的数字都是 1,但布隆过滤器只能判断 Lily 可能存在于数据库中,而不能确定 Lily 真的在数据库中。因为这三个位上的 1 可能是由 Fredy 和 Eli 共同组成的,所以存在一定的误判率。
  6. 当布隆过滤器判断数据一定不存在,就不用查询数据库,直接返回不存在的结果。当布隆过滤器判断可能存在,那么查询数据库,以确认元素是否真的存在。
什么是缓存击穿?怎么解决?
  • 缓存击穿是指在缓存中设置了过期时间的某个key,在某个时间点这个key正好过期,导致缓存失效,恰好此时有大量病发情趣同时访问该key,这些请求会直接访问后端数据库,造成数据库压力骤增,严重影响系统性能,甚至直接压垮数据库。

  • 解决方案的话,我了解的有两种

    • 方案一:使用互斥锁
    1. 当缓存失效时,不立即去加载数据库,而是先使用Redis的SETNX(Set if Not Exists)命令去设置一个互斥锁,只有一个请求能够成功设置互斥锁,其他请求会在这一步被阻塞
    2. 成功设置互斥锁的请求,再去加载数据库,并将加载的数据回设到缓存中
    • 方案二:逻辑过期
    1. 在设置缓存key的同时,额外存储一个过期时间字段到缓存中,但是不给当前key设置过期时间
    2. 当查询请求到达时,首先从缓存中取出数据,并且额外判断一下存储key的过期时间字段,若过期则认为缓存失效
    3. 当缓存失效时,开启另外一个线程进行数据的异步加载和缓存更新,当前请求直接返回缓存中的旧数据,但这部分数据可能不是最新
    4. 这种方案一定程度上保证了高可用性,避免了大量请求直接打到数据库。
  • 当然两种方案各有利弊
    • 方案一使用了互斥锁,保证了数据的强一致性,到哪性能可能会受到锁的竞争影响,而且需要考虑死锁的问题
    • 方案二优先考虑的是高可用和性能,但不能保证强一致性,有可能会出现缓存和数据库数据不一致的情况。
    • 实际使用中根据我们的需求来选择要保证一致性还是可用性。
什么是缓存雪崩?怎么解决?
  • 缓存雪崩是指换成那种设置了相同的过期时间,导致大量缓存在同一时刻同时失效,进而导致所有的请求直接转发到了后端数据库,导致数据库瞬时压力过大,可能直接压垮数据库。它与缓存击穿的区别在于,缓存雪崩是很多key同时失效,而缓存击穿是某一个key缓存失效

  • 解决方案主要是在设置缓存的时候,可以给每个缓存的过期时间加上一个随机值,例如在原有的失效时间基础上,再加上一个1~5分钟的随机值。

Redis作为缓存,MySQL的数据如何与Redis进行同步呢?(双写一致性问题)
  • 嗯,这个需要分为两种情况。
    • 保证强一致性:使用读写锁。
      • 如果是需要保证强一致性的话,可以采用Redisson实现的读写锁,在读的时候添加共享锁,可以保证读读不互斥,读写互斥。当我们更新数据的时候,添加排他锁,它是读写、读读都互斥,这样能保证在写数据的同时不会让其他线程来读数据,避免其他线程读到脏数据。这里需要注意读方法和写方法上需要使用同一把锁。
      • 排他锁的底层使用的也是setnx,保证同时只有一个线程操作被锁住的方法
    • 然后不追求强一致性,数据同步可以有一定的延时。
      • 可以采用阿里的canal组件来实现数据同步:不需要更改业务代码,部署一个canal服务,canal服务会把自己伪装成MySQL的一个从节点,当MySQL数据更新以后,canal会读取binlog数据,然后通过canal的客户端获取到数据,更新缓存即可。
Redis作为缓存,数据的持久化是怎么做的呢?

在Redis中提供了两种数据持久化的方法

  • RDB
    • RDB是一个快照文件,它是把Redis内存存储的数据写到磁盘上,当Redis实例宕机恢复数据的时候,方便从RDB的快照文件中恢复数据。
  • AOF
    • AOF的含义是追加文件,当Redis操作写命令的时候,都会存储到这个文件中,当Redis实例宕机恢复数据的时候,会从这个文件中再次执行一遍命令来恢复数据。
那这两种方式,哪种恢复的比较快呢?
  • RDB因为是二进制文件,在保存的时候体积也是比较小的,所以恢复的比较快,但是它有可能会丢失数据,因为Redis内部设置了触发RDB的机制,所以可能会丢失一段时间的缓存
save 900 1      ## 900秒内,如果至少有1个key发生了变化,则触发一次持久化操作
save 300 10     ## 300秒内,10个key变化
save 60 10000   ## 60秒内,10000个key变化
  • AOF恢复的速度慢一些,但是它丢失数据的风险要小很多,在Redis的配置文件中也可以配置AOF的刷盘策略,采用everysec的话,最多丢失一秒的数据
    配置项 刷盘时机 优点 缺点
    Always 同步刷盘 可靠性高,几乎不丢数据 性能影响大
    everysec 每秒刷盘 性能适中 最多丢失1秒数据
    no 操作系统控制 性能最好 可靠性较差,可能丢失大量数据
  • 实际环境中,都是结合使用RDB和AOF来使用的。
Redis的数据过期策略有哪些?
  • Redis提供了两种数据过期删除策略
    • 第一种是惰性删除,在设置key过期时间后,我们不去管它,当我们需要使用该key时,我们再检查其是否过期,如果过期,我们就删除它,没过期,我们就返回该key。这种方法的缺点是,如果有大量冷数据,长时间不会被使用到,会占用内存空间,不会被及时清理掉。

    • 第二种是定期删除,每隔一段时间,我们就对一些key进行检查,删除里面已经过期的key。定期清理的两种模式

    1. SLOW模式是定时任务,执行频率默认为10HZ,每次不超过25ms,不过也可以通过Redis的配置文件来手动配置
    2. FAST模式执行频率不固定,每次事件循环会尝试执行,但两次间隔不低于2ms,每次耗时不超过1ms
    • Redis的过期删除策略,一般都是结合这二者一起来使用的。
Redis的数据淘汰策略有哪些?
  • Redis支持八种不同的策略来选择要删除的key
  1. noeviction: 不淘汰任何key,但是内存满时不允许写入新数据,默认就是这种策略
  2. volatile-ttl: 对设置了TTL的key,比较key的剩余TTL值,TTL越小越先被淘汰
  3. allkeys-random:对全体key ,随机进行淘汰
  4. volatile-random:对设置了TTL的key ,随机进行淘汰
  5. allkeys-lru: 对全体key,基于LRU算法进行淘汰,LRU的意思是最近最少使用
  6. volatile-lru: 对设置了TTL的key,基于LRU算法进行淘汰
  7. allkeys-lfu: 对全体key,基于LFU算法进行淘汰,LFU的意思是最少频率使用
  8. volatile-lfu: 对设置了TTL的key,基于LFU算法进行淘汰
假如数据库中有1000w数据,Redis中只能缓存20w条数据,如何保证Redis中的数据都是热点数据?

这个问题可以从Redis的数据淘汰策略来考虑,如果需要保留热点数据,那么可以根据最近最少使用来淘汰数据,即LRU算法,这样剩下的就是热点数据了。

Redis内存用完了会发生什么?

这个问题得看配置的Redis数据淘汰策略是什么,如果是默认的配置,Redis内存用完以后直接报错,可以根据自己的需求来设置数据淘汰策略,我一般都是使用的LRU来淘汰数据。

Redis的分布式锁如何实现

Redis中提供了一个SETNX命令,即Set if Not Exists

由于Redis是单线程的,所以使用该命令后,只能有一个客户端对某一个key设置值,在没有过期或删除该key之前,其他客户端是不能设置这个key的

那你是如何控制Redis实现分布式锁的有效时长呢?

采用Redis的SETNX命令确实不太好控制这个时长,但是可以采用Redis的一个框架,Redisson来实现。

在Redisson中需要手动加锁,并且可以控制锁的失效时间和等待时间,当锁住的一个业务还没有执行完成时,Redisson的看门狗机制会每隔一段时间来检查当前业务是否还持有锁,如果持有锁,就增加锁的持久时间,当业务执行完成之后手动释放锁即可。

Redis集群有哪些方案?

Redis提供的集群方案有三种:主从复制、哨兵模式、Redis分片集群

那你来介绍一下主从同步

单节点的Redis并发能力有限,要进一步提高Redis的并发能力,可以搭建主从集群,实现读写分离。一般都是一主多从,主节点负责写入数据,从节点负责读数据,主节点写入数据之后,需要将数据同步到从节点中。

那你继续说一下主从同步的流程吧
  • 主从同步分为两个阶段,一个是全量同步,一个是增量同步

    • 全量同步是指从节点第一次与主节点建立连接的时候
    1. 从节点请求主节点同步数据,从节点会携带自己的replication id和offset偏移量
    2. 主节点判断是否是第一次请求,判断的依据是,主节点与从节点是否是同一个replication id,然后不是,那主节点就会把自己的replication id和offset发送给从节点,让主节点和从节点的信息保持一致
    3. 与此同时,主节点会执行bgsave,生成RDB文件后,发送给从节点去执行,从节点先把自己的数据清空,然后执行主节点发来的RDB文件,这样就保持了一致。
    4. 如果在从节点执行RDB文件的期间,仍有请求到了主节点,那么主节点会以命令的方式记录到缓冲区,缓冲区是一个日志文件,最后把这个日志文件发送给从节点,这样就保证了主节点和从节点数据完全一致,后期再进行同步数据的时候,都是依赖于这个日志文件。这个就是全量同步
    • 增量同步是指,当从节点服务重启之后,数据不一致了,这个时候,从节点会请求主节点同步数据,主节点还是先判断是不是第一次请求,判断的语句还是replication id,不是第一次请求的话,就获取从节点的offset值,然后主节点从命令日志中获取offset值之后的数据,发送给从节点进行数据同步。
怎么保证Redis的高并发高可用

首先可以搭建主从集群,再加上使用Redis中的哨兵模式,哨兵模式可以实现主从集群的自动故障恢复,里面就包含了对主从服务的监控、自动故障回复、通知;如果主节点故障,哨兵会将一个从节点提升为主节点,当故障实例恢复正常之后,也是以最新的主节点为主。同时哨兵也充当Redis客户端的服务发现来源,当集群发生故障转移的时候,会将最新消息推送给Redis客户端,所以一般项目都会采用哨兵模式来保证Redis的高并发高可用

Redis是单线程的,为什么还那么快?
  1. 首先Redis完全是基于内存的,内存的读写速度是非常快的,并且Redis是由C语言编写的

  2. 采用单线程,可以避免不必要的上下文切换的竞争条件

  3. 使用I/O多路复用模型,非阻塞IO

    I/O多路复用指的是利用单个线程来监听多个socket,并在某个socket就绪的时候,得到通知,从而避免无效的等待,充分利用CPU资源。目前I/O多路复用都是采用的epoll模式实现,它会通知用户进程socket就绪的同时,把已就绪的socket写入用户空间,不需要挨个遍历socket来判断是否就绪。提升了性能。

  4. Redis的网络模型使用I/O多路复用结合事件处理器来应对多个socket请求,例如,提供了连接应答处理器、命令回复处理器、命令请求处理器。

    Redis因为是基于内存的,内存读写速度非常快,所以性能瓶颈不在内存,而是在网络I/O,具体是在命令的解析这一块,假如很多的客户端都来读数据,那么他们都会携带自己的命令,Redis需要接收网络请求的数据,转化为Redis命令,那么Redis此时可能就忙不过来了。在Redis 6.0之后引入了多线程,用于解析网络请求。但是真正去执行命令的时候,还是使用的单线程。