Redis

 

Redis

1.说说什么是 Redis?

Redis 是 Remote Dictionary Service 三个单词中加粗字母的组合,是一种基于键值对(key-value)的 NoSQL 数据库。

但比一般的键值对,比如 HashMap 强大的多,Redis 中的 value 支持 string(字符串)、hash(哈希)、 list(列表)、set(集合)、zset(有序集合)、Bitmaps(位图)、 HyperLogLog(基数估算)、GEO(地理信息定位)等多种数据结构。

而且因为 Redis 的所有数据都存放在内存当中,所以它的读写性能非常出色。

不仅如此,Redis 还可以将内存数据持久化到硬盘上,这样在发生类似断电或者机器故障的时候,内存中的数据并不会“丢失”。

除此之外,Redis 还提供了键过期、发布订阅、事务、流水线、Lua 脚本等附加功能,是互联网技术领域中使用最广泛的缓存中间件。

Redis 和 MySQL 的区别?

  • Redis:数据存储在内存中的 NoSQL 数据库,读写性能非常好,是互联网技术领域中使用最广泛的缓存中间件。
  • MySQL:数据存储在硬盘中的关系型数据库,适用于需要事务支持和复杂查询的场景。

2.Redis 可以用来干什么?

Redis 可以用来实现多种功能,以下是一些常见的使用场景:

1. 缓存

  • 数据库查询缓存:将频繁访问的数据库查询结果缓存到 Redis 中,减少数据库的压力,提高查询速度。
  • 页面缓存:缓存整个页面或页面片段,减少服务器渲染压力,提高页面加载速度。
  • 对象缓存:缓存复杂的对象或计算结果,减少重复计算,提高系统性能。

2. 会话管理

  • 分布式会话:将用户会话数据存储在 Redis 中,实现分布式会话管理,适用于多节点部署的应用。
  • 购物车:将用户的购物车数据存储在 Redis 中,确保数据的快速读写和持久化。

3. 分布式锁

  • 分布式锁:使用 Redis 实现分布式锁,确保在分布式系统中只有一个实例在同一时间内执行某个操作,防止数据竞争和重复操作。

4. 消息队列

  • 简单消息队列:使用 Redis 的列表或发布/订阅功能实现简单的消息队列,适用于任务调度、异步处理等场景。
  • 事件通知:通过发布/订阅模式实现事件通知和实时消息系统。

5. 计数器和限流

  • 计数器:使用 Redis 实现计数器,统计网站访问量、点赞数、评论数等。
  • 限流器:使用 Redis 实现限流器,控制接口调用频率,防止系统过载。

6. 数据分析

  • 实时数据分析:使用 Redis 的位图(Bitmap)、HyperLogLog 等数据结构进行实时数据分析和统计。
  • 排行榜:使用 Redis 的有序集合(Sorted Set)实现排行榜功能,适用于积分排名、热度排名等场景。

7. 地理信息

  • 地理位置存储和查询:使用 Redis 的 GEO 数据结构存储和查询地理位置信息,适用于附近的人、附近的店等场景。

8. 任务调度

  • 延时任务:使用 Redis 实现延时任务调度,适用于订单超时处理、定时邮件发送等场景。
  • 任务队列:使用 Redis 的列表实现任务队列,适用于任务调度和异步处理。

以下是一个使用 Redis 实现简单缓存的示例:

1. 添加依赖

pom.xml 文件中添加 Redis 依赖:

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

2. 配置 Redis

application.properties 文件中配置 Redis 连接信息:

spring.redis.host=localhost
spring.redis.port=6379

3. 使用 Redis 缓存数据库查询结果

创建一个服务类,使用 RedisTemplate 进行 Redis 操作:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;

import java.util.concurrent.TimeUnit;

@Service
public class UserService {

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    @Autowired
    private UserRepository userRepository;

    public User getUserById(Long userId) {
        String key = "user:" + userId;
        // 尝试从缓存中获取用户信息
        User user = (User) redisTemplate.opsForValue().get(key);
        if (user == null) {
            // 如果缓存中没有,则从数据库中查询
            user = userRepository.findById(userId).orElse(null);
            if (user != null) {
                // 将查询结果存入缓存,并设置过期时间
                redisTemplate.opsForValue().set(key, user, 10, TimeUnit.MINUTES);
            }
        }
        return user;
    }
}

总结- 2

Redis 可以用于多种场景,包括缓存、会话管理、分布式锁、消息队列、计数器和限流、数据分析、地理信息和任务调度等。通过理解和利用 Redis 的这些功能,可以显著提高系统的性能和可扩展性。

3.Redis 有哪些数据类型?

Redis 支持多种数据类型,每种数据类型都有其特定的用途和操作。以下是 Redis 支持的主要数据类型及其常见操作:

1. 字符串(String)

  • 描述:最基本的数据类型,可以存储任何形式的字符串,包括二进制数据。
  • 常见操作
    • SET key value:设置指定 key 的值。
    • GET key:获取指定 key 的值。
    • INCR key:将 key 的值加1(适用于整数)。
    • DECR key:将 key 的值减1(适用于整数)。
    • APPEND key value:将 value 追加到 key 原来的值之后。
  • 使用场景
    • 缓存功能
    • 计数
    • 共享 Session
    • 限速

2. 哈希(Hash)

  • 描述:键值对集合,适用于存储对象。
  • 常见操作
    • HSET key field value:设置哈希表 key 中的字段 field 的值为 value。
    • HGET key field:获取哈希表 key 中的字段 field 的值。
    • HGETALL key:获取哈希表 key 中的所有字段和值。
    • HDEL key field:删除哈希表 key 中的一个或多个指定字段。
  • 使用场景
    • 缓存用户信息
    • 缓存对象

3. 列表(List)

  • 描述:有序的字符串列表,可以从两端插入和删除元素。
  • 常见操作
    • LPUSH key value:将一个值插入到列表头部。
    • RPUSH key value:将一个值插入到列表尾部。
    • LPOP key:移除并返回列表的头元素。
    • RPOP key:移除并返回列表的尾元素。
    • LRANGE key start stop:获取列表指定范围内的元素。
  • 使用场景
    • 消息队列
    • 文章列表

4. 集合(Set)

  • 描述:无序的字符串集合,集合成员是唯一的。
  • 常见操作
    • SADD key member:向集合添加一个或多个成员。
    • SREM key member:移除集合中的一个或多个成员。
    • SMEMBERS key:返回集合中的所有成员。
    • SISMEMBER key member:判断 member 元素是否是集合 key 的成员。
  • 使用场景
    • 标签
    • 共同关注

5. 有序集合(Sorted Set)

  • 描述:类似集合,但每个元素都会关联一个分数,Redis 会根据分数自动排序。
  • 常见操作
    • ZADD key score member:向有序集合添加一个成员,并设置分数。
    • ZREM key member:移除有序集合中的一个或多个成员。
    • ZRANGE key start stop [WITHSCORES]:返回有序集合中指定范围内的成员。
    • ZSCORE key member:返回有序集合中,成员的分数值。
  • 使用场景
    • 用户点赞统计
    • 用户排序

6. 位图(Bitmap)

  • 描述:位数组,可以对字符串进行位操作。
  • 常见操作
    • SETBIT key offset value:对 key 所储存的字符串值,设置或清除指定偏移量上的位。
    • GETBIT key offset:对 key 所储存的字符串值,获取指定偏移量上的位。
    • BITCOUNT key [start end]:计算字符串中被设置为 1 的位的数量。

7. HyperLogLog

  • 描述:用于基数统计的概率性数据结构,可以估算集合中唯一元素的数量。
  • 常见操作
    • PFADD key element [element ...]:添加指定元素到 HyperLogLog 中。
    • PFCOUNT key [key ...]:返回给定 HyperLogLog 的基数估算值。
    • PFMERGE destkey sourcekey [sourcekey ...]:将多个 HyperLogLog 合并为一个。

8. 地理空间(GEO)

  • 描述:用于存储地理位置信息并进行操作。
  • 常见操作
    • GEOADD key longitude latitude member:将地理空间位置(经度、纬度、名称)添加到指定 key 中。
    • GEODIST key member1 member2 [unit]:返回两个给定位置之间的距离。
    • GEORADIUS key longitude latitude radius m|km|ft|mi:以给定的经纬度为中心,返回指定范围内的地理位置集合。

以下是一个使用 Redis 的哈希数据类型的示例:

pom.xml 文件中添加 Redis 依赖:

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

application.properties 文件中配置 Redis 连接信息:

spring.redis.host=localhost
spring.redis.port=6379

3. 使用 Redis 哈希数据类型

创建一个服务类,使用 RedisTemplate 进行 Redis 操作:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;

@Service
public class UserService {

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    public void saveUser(String userId, String name, int age) {
        String key = "user:" + userId;
        redisTemplate.opsForHash().put(key, "name", name);
        redisTemplate.opsForHash().put(key, "age", age);
    }

    public Object getUser(String userId) {
        String key = "user:" + userId;
        return redisTemplate.opsForHash().entries(key);
    }
}

总结

Redis 支持多种数据类型,包括字符串、哈希、列表、集合、有序集合、位图、HyperLogLog 和地理空间。每种数据类型都有其特定的用途和操作,通过理解和利用这些数据类型,可以更高效地使用 Redis 进行数据存储和操作。

4.Redis 为什么快呢?

Redis 的速度非常快,单机的 Redis 就可以支撑每秒十几万的并发,性能是 MySQL 的几十倍。速度快的原因主要有几点:

①、基于内存的数据存储,Redis 将数据存储在内存当中,使得数据的读写操作避开了磁盘 I/O。而内存的访问速度远超硬盘,这是 Redis 读写速度快的根本原因。

②、单线程模型,Redis 使用单线程模型来处理客户端的请求,这意味着在任何时刻只有一个命令在执行。这样就避免了线程切换和锁竞争带来的消耗。

③、IO 多路复⽤,基于 Linux 的 select/epoll 机制。该机制允许内核中同时存在多个监听套接字和已连接套接字,内核会一直监听这些套接字上的连接请求或者数据请求,一旦有请求到达,就会交给 Redis 处理,就实现了所谓的 Redis 单个线程处理多个 IO 读写的请求。

④、高效的数据结构,Redis 提供了多种高效的数据结构,如字符串(String)、列表(List)、集合(Set)、有序集合(Sorted Set)等,这些数据结构经过了高度优化,能够支持快速的数据操作。

5.能说一下 I/O 多路复用吗?

I/O 多路复用(I/O Multiplexing)是一种高效的 I/O 模型,允许一个线程同时监视多个文件描述符(如套接字),一旦某个文件描述符准备好进行 I/O 操作(如读或写),线程就可以对其进行相应的操作。I/O 多路复用在高并发网络服务器中非常常用,因为它可以有效地管理大量的并发连接。

I/O 多路复用的工作原理

I/O 多路复用的核心思想是通过一个系统调用(如 selectpollepoll)来监视多个文件描述符,并在其中任何一个文件描述符准备好进行 I/O 操作时通知应用程序。这样,应用程序可以在一个线程中高效地处理多个 I/O 事件。

常见的 I/O 多路复用机制

select:

  • select 是最早的 I/O 多路复用机制,几乎在所有操作系统上都可用。
  • 它使用一个固定大小的数组来存储文件描述符,并在每次调用时遍历整个数组,检查哪些文件描述符准备好进行 I/O 操作。
  • 缺点是每次调用都需要遍历整个数组,效率较低,且支持的文件描述符数量有限。

poll:

  • poll 是 select 的改进版,使用一个链表来存储文件描述符,避免了 select 的固定大小限制。
  • 它的工作原理与 select 类似,但在处理大量文件描述符时效率仍然不高。

epoll:

  • epoll 是 Linux 特有的 I/O 多路复用机制,专为高并发场景设计。
  • 它使用一个红黑树和一个双向链表来管理文件描述符,支持动态添加和删除文件描述符。
  • epoll 提供了 epoll_create、epoll_ctl 和 epoll_wait 三个系统调用,分别用于创建 epoll 实例、控制文件描述符和等待 I/O 事件。
  • epoll 的效率非常高,因为它只在文件描述符状态发生变化时通知应用程序,而不是每次都遍历所有文件描述符。

Redis 中的 I/O 多路复用

Redis 使用 I/O 多路复用来处理客户端的并发连接。具体来说,Redis 使用 epoll(在 Linux 上)、kqueue(在 macOS 上)或 select(在其他操作系统上)来实现 I/O 多路复用。

Redis 的 I/O 多路复用机制使得它可以在单线程模型下高效地处理大量并发连接,避免了多线程上下文切换和锁竞争的问题。

6.Redis 为什么早期选择单线程?

官方 FAQ 表示,因为 Redis 是基于内存的操作,CPU 成为 Redis 的瓶颈的情况很少见,Redis 的瓶颈最有可能是内存的大小或者网络限制。

如果想要最大程度利用 CPU,可以在一台机器上启动多个 Redis 实例。

同时 FAQ 里还提到了, Redis 4.0 之后开始变成多线程,除了主线程外,它也有后台线程在处理一些较为缓慢的操作,例如清理脏数据、无用连接的释放、大 Key 的删除等等。

7.Redis6.0 使用多线程是怎么回事?

Redis 6.0 引入了多线程支持,以提高某些操作的性能,特别是网络 I/O 和命令处理。以下是 Redis 6.0 多线程的详细介绍:

1. 多线程的引入背景

  • 性能瓶颈:虽然 Redis 早期版本使用单线程模型处理请求,但在高并发场景下,网络 I/O 和命令处理可能成为性能瓶颈。
  • 硬件发展:随着多核 CPU 的普及,单线程模型无法充分利用多核 CPU 的计算能力。

2. 多线程的实现

Redis 6.0 引入了多线程来处理网络 I/O 和命令解析,但数据操作仍然在单线程中进行。这种设计确保了 Redis 的线程安全性,同时提高了并发处理能力。

多线程处理的部分

  • 网络 I/O:多线程用于处理客户端的读写请求,减少了网络 I/O 的瓶颈。
  • 命令解析:多线程用于解析客户端发送的命令,提高了命令处理的并发能力。

单线程处理的部分

  • 数据操作:所有的数据操作仍然在单线程中进行,确保了数据的一致性和线程安全性。
  • 事务:事务操作仍然在单线程中进行,避免了多线程带来的复杂性。

3. 多线程的配置

Redis 6.0 的多线程功能是可配置的,可以通过配置文件或命令行参数进行设置。

配置文件

在 Redis 配置文件 redis.conf 中,可以通过 io-threads 参数设置 I/O 线程的数量:

// 设置 I/O 线程的数量,默认为 1(即单线程)
io-threads 4

命令行参数

启动 Redis 时,可以通过 --io-threads 参数设置 I/O 线程的数量:

redis-server --io-threads 4

4. 多线程的优点

  • 提高并发处理能力:多线程处理网络 I/O 和命令解析,提高了 Redis 的并发处理能力。
  • 充分利用多核 CPU:多线程模型可以充分利用多核 CPU 的计算能力,提高系统性能。
  • 保持数据一致性:数据操作仍然在单线程中进行,确保了数据的一致性和线程安全性。

以下是一个简单的 Redis 配置示例,展示了如何启用多线程:

配置文件 redis.conf

// 启用多线程,并设置 I/O 线程的数量为 4
io-threads 4

启动 Redis 服务器

redis-server redis.conf

6. 总结

Redis 6.0 引入了多线程支持,以提高网络 I/O 和命令处理的性能。多线程用于处理客户端的读写请求和命令解析,而数据操作仍然在单线程中进行,确保了数据的一致性和线程安全性。通过配置文件或命令行参数,可以灵活地设置 I/O 线程的数量,以充分利用多核 CPU 的计算能力,提高 Redis 的并发处理能力。

8.Redis 常用命令

Redis 提供了丰富的命令集,用于操作各种数据类型和管理 Redis 实例。以下是一些常用的 Redis 命令,按数据类型和功能分类:

1. 字符串(String)命令

  • SET key value:设置指定 key 的值。
  • GET key:获取指定 key 的值。
  • INCR key:将 key 的值加1(适用于整数)。
  • DECR key:将 key 的值减1(适用于整数)。
  • APPEND key value:将 value 追加到 key 原来的值之后。
  • MSET key1 value1 key2 value2 ...:同时设置多个 key-value 对。
  • MGET key1 key2 ...:获取多个 key 的值。

2. 哈希(Hash)命令

  • HSET key field value:设置哈希表 key 中的字段 field 的值为 value。
  • HGET key field:获取哈希表 key 中的字段 field 的值。
  • HGETALL key:获取哈希表 key 中的所有字段和值。
  • HDEL key field:删除哈希表 key 中的一个或多个指定字段。
  • HEXISTS key field:检查哈希表 key 中是否存在指定字段。
  • HINCRBY key field increment:将哈希表 key 中的字段 field 的值加上指定增量(适用于整数)。

3. 列表(List)命令

  • LPUSH key value:将一个值插入到列表头部。
  • RPUSH key value:将一个值插入到列表尾部。
  • LPOP key:移除并返回列表的头元素。
  • RPOP key:移除并返回列表的尾元素。
  • LRANGE key start stop:获取列表指定范围内的元素。
  • LLEN key:获取列表的长度。

4. 集合(Set)命令

  • SADD key member:向集合添加一个或多个成员。
  • SREM key member:移除集合中的一个或多个成员。
  • SMEMBERS key:返回集合中的所有成员。
  • SISMEMBER key member:判断 member 元素是否是集合 key 的成员。
  • SCARD key:获取集合的成员数。

5. 有序集合(Sorted Set)命令

  • ZADD key score member:向有序集合添加一个成员,并设置分数。
  • ZREM key member:移除有序集合中的一个或多个成员。
  • ZRANGE key start stop [WITHSCORES]:返回有序集合中指定范围内的成员。
  • ZSCORE key member:返回有序集合中,成员的分数值。
  • ZCARD key:获取有序集合的成员数。

6. 位图(Bitmap)命令

  • SETBIT key offset value:对 key 所储存的字符串值,设置或清除指定偏移量上的位。
  • GETBIT key offset:对 key 所储存的字符串值,获取指定偏移量上的位。
  • BITCOUNT key [start end]:计算字符串中被设置为 1 的位的数量。

7. HyperLogLog 命令

  • PFADD key element [element ...]:添加指定元素到 HyperLogLog 中。
  • PFCOUNT key [key ...]:返回给定 HyperLogLog 的基数估算值。
  • PFMERGE destkey sourcekey [sourcekey ...]:将多个 HyperLogLog 合并为一个。

8. 地理空间(GEO)命令

  • GEOADD key longitude latitude member:将地理空间位置(经度、纬度、名称)添加到指定 key 中。
  • GEODIST key member1 member2 [unit]:返回两个给定位置之间的距离。
  • GEORADIUS key longitude latitude radius m|km|ft|mi:以给定的经纬度为中心,返回指定范围内的地理位置集合。

9. 发布/订阅(Pub/Sub)命令

  • PUBLISH channel message:将信息发送到指定的频道。
  • SUBSCRIBE channel [channel ...]:订阅一个或多个频道。
  • UNSUBSCRIBE [channel ...]:退订一个或多个频道。

10. 事务(Transaction)命令

  • MULTI:标记一个事务块的开始。
  • EXEC:执行所有事务块内的命令。
  • DISCARD:取消事务块内的所有命令。
  • WATCH key [key ...]:监视一个或多个 key,如果在事务执行之前这些 key 被修改,事务将被中止。

11. 脚本(Scripting)命令

  • EVAL script numkeys key [key ...] arg [arg ...]:执行 Lua 脚本。
  • EVALSHA sha1 numkeys key [key ...] arg [arg ...]:执行缓存的 Lua 脚本。

12. 服务器(Server)命令

  • INFO:获取 Redis 服务器的各种信息和统计数据。
  • CONFIG GET parameter:获取 Redis 配置参数的值。
  • CONFIG SET parameter value:修改 Redis 配置参数的值。
  • CLIENT LIST:获取连接到 Redis 服务器的客户端列表。
  • MONITOR:实时打印出 Redis 服务器接收到的命令。

以下是一些常用 Redis 命令的示例代码:

// 字符串操作
SET mykey "Hello"
GET mykey

// 哈希操作
HSET myhash field1 "value1"
HGET myhash field1

// 列表操作
LPUSH mylist "World"
RPUSH mylist "Hello"
LRANGE mylist 0 -1

// 集合操作
SADD myset "one"
SADD myset "two"
SMEMBERS myset

// 有序集合操作
ZADD myzset 1 "one"
ZADD myzset 2 "two"
ZRANGE myzset 0 -1 WITHSCORES

// 发布/订阅操作
PUBLISH mychannel "Hello, World!"
SUBSCRIBE mychannel

9.单线程 Redis 的 QPS 是多少?

Redis 的 QPS(Queries Per Second,每秒查询数)取决于多种因素,包括硬件配置、操作类型、数据量和网络延迟等。在理想条件下,单线程 Redis 的 QPS 可以达到非常高的水平。

理想条件下的 QPS

在高性能硬件和优化的网络环境下,单线程 Redis 的 QPS 可以达到以下水平:

  • 简单操作:对于简单的读写操作(如 GETSET),Redis 的 QPS 可以达到 10 万到 20 万。
  • 复杂操作:对于复杂的操作(如 LRANGEZADD 等),QPS 会有所降低,但仍然可以达到数万。

影响 QPS 的因素

  1. 硬件配置
    • CPU:Redis 是 CPU 密集型应用,CPU 的性能对 QPS 有直接影响。高主频、多核 CPU 可以显著提高 Redis 的性能。
    • 内存:充足的内存可以确保数据全部存储在内存中,避免磁盘 I/O 的瓶颈。
    • 网络:低延迟、高带宽的网络环境可以减少网络传输的开销,提高 QPS。
  2. 操作类型
    • 简单操作:如 GETSET 等简单的键值操作,处理速度非常快,QPS 较高。
    • 复杂操作:如 LRANGEZADD 等涉及数据结构操作的命令,处理速度相对较慢,QPS 较低。
  3. 数据量
    • 小数据量:数据量较小时,Redis 可以更快地处理请求,QPS 较高。
    • 大数据量:数据量较大时,Redis 需要更多的时间进行数据操作,QPS 较低。
  4. 客户端数量
    • 并发客户端:更多的并发客户端可以提高 Redis 的 QPS,但也会增加 CPU 和网络的负载。
    • 单客户端:单客户端的 QPS 受限于网络延迟和客户端的处理能力。

实际测试

在实际测试中,Redis 的 QPS 可能会受到上述因素的影响。以下是一些常见的测试工具和方法:

  1. redis-benchmark
    • Redis 自带的性能测试工具,可以模拟多种操作类型和并发客户端,测试 Redis 的 QPS。
    • 示例命令:

      redis-benchmark -t get,set -n 100000 -c 50
      

      该命令测试 GETSET 操作,执行 10 万次请求,使用 50 个并发客户端。

  2. 自定义测试
    • 使用编程语言(如 Python、Java 等)编写自定义测试脚本,模拟实际应用场景,测试 Redis 的 QPS。
    • 示例 Python 脚本:

      import redis
      import time
      
      r = redis.Redis(host='localhost', port=6379, db=0)
      start = time.time()
      for i in range(100000):
          r.set(f'key{i}', f'value{i}')
      end = time.time()
      print(f'QPS: {100000 / (end - start)}')
      

总结- 9

在理想条件下,单线程 Redis 的 QPS 可以达到 10 万到 20 万,具体取决于硬件配置、操作类型、数据量和网络延迟等因素。通过使用 redis-benchmark 等工具,可以测试 Redis 在不同条件下的 QPS,评估其性能表现。

10.Redis 持久化⽅式有哪些?有什么区别?

Redis 提供了两种主要的持久化方式:RDB(Redis Database)和 AOF(Append-Only File)。这两种持久化方式各有优缺点,可以根据具体需求选择使用。

1. RDB(Redis Database)

RDB工作原理
  • RDB 是 Redis 的默认持久化方式,它会在指定的时间间隔内生成数据快照(snapshot),并将快照保存到磁盘上。
  • RDB 文件是一个紧凑的二进制文件,包含了某个时间点上 Redis 数据库的所有数据。
RDB配置示例

redis.conf 文件中,可以通过以下参数配置 RDB 持久化:

// 配置在指定时间间隔内生成快照
save 900 1   // 900 秒内如果至少有 1 个键发生变化,则生成快照
save 300 10  // 300 秒内如果至少有 10 个键发生变化,则生成快照
save 60 10000 // 60 秒内如果至少有 10000 个键发生变化,则生成快照

// 指定 RDB 文件的保存路径
dir /var/lib/redis

// 指定 RDB 文件的名称
dbfilename dump.rdb

②、当 Redis 服务器通过 SHUTDOWN 命令正常关闭时,如果没有禁用 RDB 持久化,Redis 会自动执行一次 RDB 持久化,以确保数据在下次启动时能够恢复。

③、在 Redis 复制场景中,当一个 Redis 实例被配置为从节点并且与主节点建立连接时,它可能会根据配置接收主节点的 RDB 文件来初始化数据集。这个过程中,主节点会在后台自动触发 RDB 持久化,然后将生成的 RDB 文件发送给从节点。

手动RDB

①、save 命令:会同步地将 Redis 的所有数据保存到磁盘上的一个 RDB 文件中。这个操作会阻塞所有客户端请求直到 RDB 文件被完全写入磁盘。

当 Redis 数据集较大时,使用 SAVE 命令会导致 Redis 服务器停止响应客户端的请求。

不推荐在生产环境中使用,除非数据集非常小,或者可以接受服务暂时的不可用状态。

②、bgsave 命令:会在后台异步地创建 Redis 的数据快照,并将快照保存到磁盘上的 RDB 文件中。这个命令会立即返回,Redis 服务器可以继续处理客户端请求。

在 BGSAVE 命令执行期间,Redis 会继续响应客户端的请求,对服务的可用性影响较小。快照的创建过程是由一个子进程完成的,主进程不会被阻塞。是在生产环境中执行 RDB 持久化的推荐方式。

RDB优点
  • 性能高:RDB 文件是紧凑的二进制文件,生成快照的过程对 Redis 的性能影响较小。
  • 恢复速度快:RDB 文件包含了某个时间点上 Redis 数据库的所有数据,恢复速度较快。
  • 适合备份:RDB 文件是一个完整的快照,适合用于定期备份。
RDB缺点
  • 数据丢失风险:由于 RDB 是在指定时间间隔内生成快照,因此在快照之间的数据变更可能会丢失。
  • 生成快照开销大:生成快照的过程需要复制整个数据集,数据量较大时可能会占用较多的内存和 CPU 资源。

2. AOF(Append-Only File)

AOF工作原理
  • AOF 是通过记录每次写操作的日志来实现持久化的。每次写操作都会被追加到 AOF 文件的末尾。
  • Redis 会定期对 AOF 文件进行重写(rewrite),以压缩文件大小。
  • AOF 的工作流程操作有四个步骤:命令写入 (append)、文件同步(sync)、文件重写(rewrite)、重启加载(load)
AOF配置示例

redis.conf 文件中,可以通过以下参数配置 AOF 持久化:

// 启用 AOF 持久化
appendonly yes

// 指定 AOF 文件的名称
appendfilename "appendonly.aof"

// 配置 AOF 文件的同步策略
// always: 每次写操作后都同步到 AOF 文件,性能较低但数据最安全
// everysec: 每秒同步一次,性能和数据安全性折中
// no: 由操作系统决定何时同步,性能最高但数据安全性最低
appendfsync everysec

// 启用 AOF 文件重写
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb

1)当 AOF 持久化功能被启用时(通过在配置文件中设置 appendonly 参数为 yes 来启用),Redis 服务器会将接收到的所有写命令(比如 SET, LPUSH, SADD 等修改数据的命令)追加到 AOF 缓冲区(buffer)的末尾。

2)为了将缓冲区中的命令持久化到磁盘中的 AOF 文件,Redis 提供了几种不同的同步策略:

  • always:每次写命令都会同步到 AOF 文件,这提供了最高的数据安全性,但可能因为磁盘 I/O 的延迟而影响性能。
  • everysec(默认):每秒同步一次,这是一种折衷方案,提供了较好的性能和数据安全性。如果系统崩溃,最多可能丢失最后一秒的数据。
  • no:只会在 AOF 关闭或 Redis 关闭时执行, 或由操作系统内核触发。在这种模式下,如果发生宕机,那么丢失的数据量由操作系统内核的缓存冲洗策略决定。

3)随着操作的不断执行,AOF 文件会不断增长,为了减小 AOF 文件大小,Redis 可以重写 AOF 文件:

  • 重写过程不会解析原始的 AOF 文件,而是将当前内存中的数据库状态转换为一系列写命令,然后保存到一个新的 AOF 文件中。
  • AOF 重写操作由 BGREWRITEAOF 命令触发,它会创建一个子进程来执行重写操作,因此不会阻塞主进程。
  • 重写过程中,新的写命令会继续追加到旧的 AOF 文件中,同时也会被记录到一个缓冲区中。一旦重写完成,Redis 会将这个缓冲区中的命令追加到新的 AOF 文件中,然后切换到新的 AOF 文件上,以确保数据的完整性。

4)当 Redis 服务器启动时,如果配置为使用 AOF 持久化方式,它会读取 AOF 文件中的所有命令并重新执行它们,以恢复数据库的状态。

AOF优点
  • 数据安全性高:AOF 记录每次写操作,数据丢失风险较低。
  • 可读性好:AOF 文件是一个日志文件,包含了所有写操作的记录,便于分析和调试。
  • 灵活性高:可以通过配置不同的同步策略来平衡性能和数据安全性。
AOF缺点
  • 文件大小大:AOF 文件会随着写操作的增加而不断增大,需要定期进行重写以压缩文件大小。
  • 恢复速度慢:AOF 文件记录了所有写操作,恢复时需要逐条执行这些操作,恢复速度较慢。
  • 性能开销大:每次写操作都需要记录到 AOF 文件,性能开销较大。

3. 混合持久化

Redis 4.0 引入了混合持久化(Hybrid Persistence),结合了 RDB 和 AOF 的优点。

工作原理
  • 在进行持久化时,Redis 会先生成一个 RDB 快照,然后将最近的写操作追加到 AOF 文件中。
  • 这样既可以利用 RDB 快照的高效性,又可以保证 AOF 的数据安全性。
配置示例

redis.conf 文件中,可以通过以下参数启用混合持久化:

// 启用混合持久化
aof-use-rdb-preamble yes

总结- 10

  • RDB:适合用于定期备份,生成快照的过程对性能影响较小,但在快照之间的数据变更可能会丢失。
  • AOF:适合用于需要高数据安全性的场景,记录每次写操作,数据丢失风险较低,但文件大小较大,恢复速度较慢。
  • 混合持久化:结合了 RDB 和 AOF 的优点,既可以利用 RDB 快照的高效性,又可以保证 AOF 的数据安全性。

根据具体需求,可以选择合适的持久化方式,或者结合使用 RDB 和 AOF,以实现最佳的性能和数据安全性。

11.RDB 和 AOF 如何选择?

选择 RDB(Redis Database)和 AOF(Append-Only File)持久化方式时,需要根据具体的应用场景和需求来决定。以下是一些选择建议:

选择 RDB 的场景

  1. 数据备份
    • 如果主要需求是定期备份数据,RDB 是一个很好的选择。RDB 生成的数据快照文件紧凑,适合用于备份和恢复。
  2. 快速恢复
    • 如果需要快速恢复数据,RDB 是一个不错的选择。RDB 文件包含了某个时间点上 Redis 数据库的所有数据,恢复速度较快。
  3. 性能优先
    • 如果对性能要求较高,且可以接受一定的数据丢失,RDB 是一个合适的选择。RDB 生成快照的过程对 Redis 的性能影响较小。

选择 AOF 的场景

  1. 数据安全性
    • 如果数据安全性非常重要,不能接受数据丢失,AOF 是一个更好的选择。AOF 记录每次写操作,数据丢失风险较低。
  2. 数据可追溯性
    • 如果需要追溯数据的变化历史,AOF 是一个合适的选择。AOF 文件是一个日志文件,包含了所有写操作的记录,便于分析和调试。
  3. 灵活的同步策略
    • 如果需要灵活地平衡性能和数据安全性,AOF 提供了多种同步策略(always、everysec、no),可以根据具体需求进行配置。

选择混合持久化的场景

  1. 综合需求
    • 如果既需要 RDB 的高效性,又需要 AOF 的数据安全性,混合持久化是一个理想的选择。混合持久化结合了 RDB 和 AOF 的优点,既可以利用 RDB 快照的高效性,又可以保证 AOF 的数据安全性。

总结- 11

  • RDB:适合用于定期备份和快速恢复,性能影响较小,但可能会丢失快照之间的数据。
  • AOF:适合需要高数据安全性和数据可追溯性的场景,数据丢失风险较低,但文件较大,恢复速度较慢。
  • 混合持久化:结合了 RDB 和 AOF 的优点,适合需要综合考虑性能和数据安全性的场景。

根据具体需求,可以选择合适的持久化方式,或者结合使用 RDB 和 AOF,以实现最佳的性能和数据安全性。

12.Redis 的数据恢复?

当 Redis 中的数据丢失时,可以从 RDB 或者 AOF 中恢复数据。

可以将 RDB 文件或者 AOF 文件复制到 Redis 的数据目录下,然后重启 Redis 服务,Redis 会自动加载数据文件并恢复数据。

Redis数据恢复

Redis 启动时加载数据的流程:

  • AOF 开启且存在 AOF 文件时,优先加载 AOF 文件。
  • AOF 关闭或者 AOF 文件不存在时,加载 RDB 文件。

13.Redis主从复制

主从复制(Master-Slave Replication)是指将一台 Redis 服务器的数据,复制到其他的 Redis 服务器。

前者称为主节点(master),后者称为从节点(slave)。且数据的复制是单向的,只能由主节点到从节点。

在 Redis 主从架构中,主节点负责处理所有的写操作,并将这些操作异步复制到从节点。从节点主要用于读取操作,以分担主节点的压力和提高读性能。

主从复制主要的作用是什么?

①、数据冗余: 主从复制实现了数据的热备份,是持久化之外的一种数据冗余方式。

②、故障恢复: 如果主节点挂掉了,可以将一个从节点提升为主节点,从而实现故障的快速恢复。

通常会使用 Sentinel 哨兵来实现自动故障转移,当主节点挂掉时,Sentinel 会自动将一个从节点升级为主节点,保证系统的可用性。

// sentinel.conf

port 26379
sentinel monitor mymaster 192.168.1.1 6379 2
sentinel down-after-milliseconds mymaster 5000
sentinel failover-timeout mymaster 60000
sentinel parallel-syncs mymaster 1

假如是从节点挂掉了,主节点不受影响,但应该尽快修复并重启挂掉的从节点,使其重新加入集群并从主节点同步数据。

③、负载均衡: 在主从复制的基础上,配合读写分离,可以由主节点提供写服务,由从节点提供读服务 (即写 Redis 时连接主节点,读 Redis 时连接从节点),分担服务器负载。尤其是在写少读多的场景下,通过多个从节点分担读负载,可以大大提高 Redis 服务器的并发量。

④、高可用基石: 除了上述作用以外,主从复制还是哨兵和集群能够实施的 基础

主从复制出现数据不一致怎么办?

Redis 的主从复制是异步进行的,这意味着主节点在执行完写操作后,会立即返回给客户端,而不是等待从节点完成数据同步。

在主节点将数据同步到从节点的过程中,可能会出现网络延迟或中断,从而导致从节点的数据滞后于主节点。

为了解决数据不一致的问题,应该尽量保证主从节点之间的网络连接状况良好,比如说避免在不同机房之间部署主从节点,以减少网络延迟。但可能会带来新的问题,就是整个机房都挂掉的情况。

此外,Redis 本身也提供了一些机制来解决数据不一致的问题,比如说通过 Redis 的 INFO replication 命令监控主从节点的复制进度,及时发现和处理复制延迟。

具体做法是获取主节点的 master_repl_offset 和从节点的 slave_repl_offset,计算两者的差值。如果差值超过预设的阈值,采取措施(如停止从节点的数据读取)以减少读到不一致数据的情况。

主从复制

Redis解决单点故障主要靠什么?

主从复制,当主节点发生故障时,可以通过手动或自动方式将某个从节点提升为新的主节点,继续对外提供服务,从而避免单点故障。

Redis 的哨兵机制(Sentinel)可以实现自动化的故障转移,当主节点宕机时,哨兵会自动将一个从节点升级为新的主节点。

另外,集群模式下,当某个节点发生故障时,Redis Cluster 会自动将请求路由到其他节点,并通过从节点进行故障恢复。

14.Redis 主从有几种常见的拓扑结构?

Redis 主从复制有几种常见的拓扑结构,每种结构适用于不同的应用场景和需求。以下是几种常见的 Redis 主从拓扑结构:

1. 单主多从结构

描述
  • 单主节点:一个主节点负责处理所有的写操作。
  • 多从节点:多个从节点负责处理读操作,从主节点同步数据。
优点
  • 读写分离:主节点处理写操作,从节点处理读操作,提高系统的读性能。
  • 数据冗余:多个从节点提供数据冗余,提高数据的可靠性。
  • 负载均衡:多个从节点分担读负载,提高系统的并发能力。
缺点
  • 单点故障:主节点故障会导致写操作不可用,需要手动或自动进行故障转移。
示例
      Master
        |
  ---------------
  |      |      |
Slave1  Slave2  Slave3

2. 菊花链结构(链式复制)

描述
  • 链式复制:从节点不仅从主节点同步数据,还可以从其他从节点同步数据,形成链式结构。
优点
  • 减少主节点压力:从节点可以从其他从节点同步数据,减轻主节点的同步压力。
  • 灵活性高:可以根据需要调整链的长度和结构。
缺点
  • 数据延迟:链式结构可能会导致数据同步延迟,从节点的数据可能滞后于主节点。
  • 复杂性高:链式结构的管理和维护较为复杂。
示例
Master
  |
Slave1
  |
Slave2
  |
Slave3

3. 哨兵模式(Sentinel)

描述
  • 哨兵监控:使用哨兵(Sentinel)监控主节点和从节点的状态,自动进行故障转移。
  • 自动故障转移:当主节点故障时,哨兵会自动将一个从节点提升为新的主节点。
优点
  • 高可用性:哨兵模式提供自动故障转移,保证系统的高可用性。
  • 自动化管理:哨兵模式自动监控和管理主从节点,减少人工干预。
缺点
  • 配置复杂:哨兵模式的配置和管理较为复杂,需要额外的哨兵节点。
示例
      Sentinel
        |
      Master
        |
  ---------------
  |      |      |
Slave1  Slave2  Slave3

4. 集群模式(Cluster)

描述
  • 分片存储:Redis 集群将数据分片存储在多个主节点上,每个主节点负责一部分数据。
  • 多主多从:每个主节点都有一个或多个从节点,提供数据冗余和高可用性。
优点
  • 水平扩展:集群模式支持水平扩展,可以通过增加节点来扩展存储容量和处理能力。
  • 高可用性:集群模式提供自动故障转移和数据冗余,保证系统的高可用性。
缺点
  • 复杂性高:集群模式的配置和管理较为复杂,需要额外的集群管理工具。
示例
      Master1 ---- Master2 ---- Master3
        |            |            |
  ---------------  ---------------  ---------------
  |      |      |  |      |      |  |      |      |
Slave1 Slave2 Slave3 Slave4 Slave5 Slave6 Slave7 Slave8 Slave9

5.一主一从结构

一主一从结构是最简单的复制拓扑结构,用于主节点出现宕机时从节点提供故障转移支持。

15.Redis 的主从复制原理了解吗?

  • 1.保存主节点(master)信息 这一步只是保存主节点信息,保存主节点的 ip 和 port。
  • 2.主从建立连接 从节点(slave)发现新的主节点后,会尝试和主节点建立网络连接。
  • 3.发送 ping 命令 连接建立成功后从节点发送 ping 请求进行首次通信,主要是检测主从之间网络套接字是否可用、主节点当前是否可接受处理命令。
  • 4.权限验证 如果主节点要求密码验证,从节点必须正确的密码才能通过验证。
  • 5.同步数据集 主从复制连接正常通信后,主节点会把持有的数据全部发送给从节点。
  • 6.命令持续复制 接下来主节点会持续地把写命令发送给从节点,保证主从数据一致性。

16.说说主从数据同步的方式?

Redis 在 2.8 及以上版本使用 psync 命令完成主从数据同步,同步过程分为:全量复制和部分复制。

全量复制 一般用于初次复制场景,Redis 早期支持的复制功能只有全量复制,它会把主节点全部数据一次性发送给从节点,当数据量较大时,会对主从节点和网络造成很大的开销。

全量复制的流程如下:

全量复制

  • 1.发送 psync 命令进行数据同步,由于是第一次进行复制,从节点没有复制偏移量和主节点的运行 ID,所以发送 psync-1。
  • 2.主节点根据 psync-1 解析出当前为全量复制,回复+FULLRESYNC 响应。
  • 3.从节点接收主节点的响应数据保存运行 ID 和偏移量 offset
  • 4.主节点执行 bgsave 保存 RDB 文件到本地
  • 5.主节点发送 RDB 文件给从节点,从节点把接收的 RDB 文件保存在本地并直接作为从节点的数据文件
  • 6.对于从节点开始接收 RDB 快照到接收完成期间,主节点仍然响应读写命令,因此主节点会把这期间写命令数据保存在复制客户端缓冲区内,当从节点加载完 RDB 文件后,主节点再把缓冲区内的数据发送给从节点,保证主从之间数据一致性。
  • 7.从节点接收完主节点传送来的全部数据后会清空自身旧数据
  • 8.从节点清空数据后开始加载 RDB 文件
  • 9.从节点成功加载完 RDB 后,如果当前节点开启了 AOF 持久化功能, 它会立刻做 bgrewriteaof 操作,为了保证全量复制后 AOF 持久化文件立刻可用。

部分复制 部分复制主要是 Redis 针对全量复制的过高开销做出的一种优化措施, 使用 psync{runId}{offset}命令实现。当从节点(slave)正在复制主节点 (master)时,如果出现网络闪断或者命令丢失等异常情况时,从节点会向 主节点要求补发丢失的命令数据,如果主节点的复制积压缓冲区内存在这部分数据则直接发送给从节点,这样就可以保持主从节点复制的一致性。

部分复制的流程如下:

部分复制

  • 1.当主从节点之间网络出现中断时,如果超过 repl-timeout 时间,主节点会认为从节点故障并中断复制连接
  • 2.主从连接中断期间主节点依然响应命令,但因复制连接中断命令无法发送给从节点,不过主节点内部存在的复制积压缓冲区,依然可以保存最近一段时间的写命令数据,默认最大缓存 1MB。
  • 3.当主从节点网络恢复后,从节点会再次连上主节点
  • 4.当主从连接恢复后,由于从节点之前保存了自身已复制的偏移量和主节点的运行 ID。因此会把它们当作 psync 参数发送给主节点,要求进行部分复制操作。
  • 5.主节点接到 psync 命令后首先核对参数 runId 是否与自身一致,如果一 致,说明之前复制的是当前主节点;之后根据参数 offset 在自身复制积压缓冲区查找,如果偏移量之后的数据存在缓冲区中,则对从节点发送+CONTINUE 响应,表示可以进行部分复制。
  • 6.主节点根据偏移量把复制积压缓冲区里的数据发送给从节点,保证主从复制进入正常状态。

17.主从复制存在哪些问题呢?

主从复制虽好,但也存在一些问题:

  • 一旦主节点出现故障,需要手动将一个从节点晋升为主节点,同时需要修改应用方的主节点地址,还需要命令其他从节点去复制新的主节点,整个过程都需要人工干预。
  • 主节点的写能力受到单机的限制。
  • 主节点的存储能力受到单机的限制。 第一个问题是 Redis 的高可用问题,第二、三个问题属于 Redis 的分布式问题。

18.Redis 哨兵了解吗?

哨兵机制是 Redis 提供的一个高可用性解决方案,用于监控 Redis 的主从复制,以自动完成故障转移和通知管理员。

哨兵

哨兵主要由两部分组成:

  • 哨兵节点: 哨兵节点是特殊的 Redis 节点,不存储数据,只对数据节点进行监控。
  • 数据节点: 主节点和从节点都是数据节点。

哨兵的主要功能有:

  • 监控(Monitoring): 哨兵 Sentinel 会不断检查主节点和从节点是否正常工作。
  • 通知(Notification): Sentinel 可以向管理员或其他应用程序发送通知,告知 Redis 实例的状态变化。
  • 自动故障转移(Automatic failover): 当 Sentinel 检测到主节点不可用时,会自动将一个从节点提升为新的主节点,并让其他从节点开始复制新的主节点。
  • 配置提供者(Configuration provider): Sentinel 客户端可以从 Sentinel 集群获取当前的主节点地址,以实现动态配置。

19.Redis 哨兵实现原理知道吗?

哨兵模式是通过哨兵节点完成对数据节点的监控、下线、故障转移。

哨兵模式

定时监控

Redis Sentinel 通过三个定时监控任务完成对各个节点发现和监控:

  • 1.每隔 10 秒,每个 Sentinel 节点会向主节点和从节点发送 info 命令获取最新的拓扑结构
  • 2.每隔 2 秒,每个 Sentinel 节点会向 Redis 数据节点的sentinel:hello 频道上发送该 Sentinel 节点对于主节点的判断以及当前 Sentinel 节点的信息
  • 3.每隔 1 秒,每个 Sentinel 节点会向主节点、从节点、其余 Sentinel 节点发送一条 ping 命令做一次心跳检测,来确认这些节点当前是否可达

定时监控任务

主观下线和客观下线

主观下线就是哨兵节点认为某个节点有问题,客观下线就是超过一定数量的哨兵节点认为主节点有问题。

  • 主观下线 每个 Sentinel 节点会每隔 1 秒对主节点、从节点、其他 Sentinel 节点发送 ping 命令做心跳检测,当这些节点超过 down-after-milliseconds 没有进行有效回复,Sentinel 节点就会对该节点做失败判定,这个行为叫做主观下线。

  • 客观下线 当 Sentinel 主观下线的节点是主节点时,该 Sentinel 节点会通过 sentinel is- master-down-by-addr 命令向其他 Sentinel 节点询问对主节点的判断,当超过 <quorum> 个数,Sentinel 节点认为主节点确实有问题,这时该 Sentinel 节点会做出客观下线的决定

主客观下线

领导者 Sentinel 节点选举

Sentinel 节点之间会做一个领导者选举的工作,选出一个 Sentinel 节点作为领导者进行故障转移的工作。Redis 使用了 Raft 算法实现领导者选举。

Redis 使用 Raft 算法实现领导者选举的:当主节点挂掉后,新的主节点是由剩余的从节点发起选举后晋升的。

领导者Sentinel节点选举

①、每个在线的 Sentinel 节点都有资格成为领导者,当它确认主节点下线时候,会向其他哨兵节点发送命令,表明希望由自己来执行主从切换,并让所有其他哨兵进行投票。

这个投票过程称为“Leader 选举”。候选者会给自己先投 1 票,然后向其他 Sentinel 节点发送投票的请求。

②、收到请求的 Sentinel 节点会进行判断,如果候选者的日志与自己的日志一样新,任期号也小于自己,且之前没有投票过,就会同意投票,回复 Y。否则回复 N。

③、候选者收到投票后会统计支持自己的得票数,如果候选者获得了集群中超过半数节点的投票支持(即多数原则),它将成为新的主节点。

新的主节点在确立后,会向其他从节点发送心跳信号,告诉它们自己已经成为主节点,并将其他节点的状态重置为从节点。

④、如果多个节点同时成为候选者,并且都有可能获得足够的票数,这种情况下可能会出现选票分裂。也就是没有候选者获得超过半数的选票,那么这次选举就会失败,所有候选者都会再次发起选举。

为了防止无限制的选举失败,每个节点都会有一个选举超时时间,且是随机的。

故障转移

领导者选举出的 Sentinel 节点负责故障转移,过程如下:

故障转移

  • 1.在从节点列表中选出一个节点作为新的主节点,这一步是相对复杂一些的一步
  • 2.Sentinel 领导者节点会对第一步选出来的从节点执行 slaveof no one 命令让其成为主节点
  • 3.Sentinel 领导者节点会向剩余的从节点发送命令,让它们成为新主节点的从节点
  • 4.Sentinel 节点集合会将原来的主节点更新为从节点,并保持着对其关注,当其恢复后命令它去复制新的主节点

20.新的主节点是怎样被挑选出来的?

选出新的主节点,大概分为这么几步:

主节点选取

  • 1.过滤:“不健康”(主观下线、断线)、5 秒内没有回复过 Sentinel 节 点 ping 响应、与主节点失联超过 down-after-milliseconds*10 秒。
  • 2.选择 slave-priority(从节点优先级)最高的从节点列表,如果存在则返回,不存在则继续。
  • 3.选择复制偏移量最大的从节点(复制的最完整),如果存在则返 回,不存在则继续。
  • 4.选择 runid 最小的从节点。

21.Redis 集群了解吗?

前面说到了主从存在高可用和分布式的问题,哨兵解决了高可用的问题,而集群就是终极方案,一举解决高可用和分布式问题。

集群

数据分区: 数据分区 (或称数据分片) 是集群最核心的功能。集群将数据分散到多个节点,一方面 突破了 Redis 单机内存大小的限制,存储容量大大增加;另一方面 每个主节点都可以对外提供读服务和写服务,大大提高了集群的响应能力

高可用: 集群支持主从复制和主节点的 自动故障转移 (与哨兵类似),当任一节点发生故障时,集群仍然可以对外提供服务。

21.切片集群了解吗?

切片集群是一种将数据分片存储在多个 Redis 实例上的集群架构,每个 Redis 实例负责存储部分数据。

数据和实例之间如何映射呢?

在 Redis 3.0 之前,官方并没有针对切片集群提供具体的解决方案;但是在 Redis 3.0 之后,官方提供了 Redis Cluster,它是 Redis 官方推荐的分布式解决方案。

在 Redis Cluster 中,数据和实例之间的映射是通过哈希槽(hash slot)来实现的。Redis Cluster 有 16384 个哈希槽,每个键根据其名字的 CRC16 值被映射到这些哈希槽上。然后,这些哈希槽会被均匀地分配到所有的 Redis 实例上。

CRC16 是一种哈希算法,它可以将任意长度的输入数据映射为一个 16 位的哈希值。

哈希槽

例如,如果我们有 3 个 Redis 实例,那么每个实例可能会负责大约 5461 个哈希槽。

当需要存储或检索一个键值对时,Redis Cluster 会先计算这个键的哈希槽,然后找到负责这个哈希槽的 Redis 实例,最后在这个实例上进行操作。

22.集群中的数据如何分区

在 Redis 集群中,数据分区是通过将数据分散到不同的节点来实现的,常见的数据分区规则有三种:节点取余分区、一致性哈希分区、虚拟槽分区。

节点取余分区

节点取余分区是一种简单的分区策略,其中数据项通过对某个值(通常是键的哈希值)进行取余操作来分配到不同的节点。

类似 HashMap 中的取余操作,数据项的键经过哈希函数计算后,对节点数量取余,然后将数据项分配到余数对应的节点上。

缺点是扩缩容时,大多数数据需要重新分配,因为节点总数的改变会影响取余结果,这可能导致大量数据迁移。

节点取余分区

一致性哈希分区

一致性哈希分区的原理是:将哈希值空间组织成一个环,数据项和节点都映射到这个环上。数据项由其哈希值直接映射到环上,然后顺时针分配到遇到的第一个节点。

从而来减少节点变动时数据迁移的量。

一致性哈希分区

Key 1 和 Key 2 会落入到 Node 1 中,Key 3、Key 4 会落入到 Node 2 中,Key 5 落入到 Node 3 中,Key 6 落入到 Node 4 中。

这种方式相比节点取余最大的好处在于加入和删除节点只影响哈希环中相邻的节点,对其他节点无影响。

但它还是存在问题:

  • 节点在圆环上分布不平均,会造成部分缓存节点的压力较大
  • 当某个节点故障时,这个节点所要承担的所有访问都会被顺移到另一个节点上,会对后面这个节点造成压力。

虚拟槽分区?

在虚拟槽(也叫哈希槽)分区中,槽位的数量是固定的(例如 Redis Cluster 有 16384 个槽),每个键通过哈希算法(比如 CRC16)映射到这些槽上,每个集群节点负责管理一定范围内的槽。

这种分区可以灵活地将槽(以及槽中的数据)从一个节点迁移到另一个节点,从而实现平滑扩容和缩容;数据分布也更加均匀,Redis Cluster 采用的正是这种分区方式。

虚拟槽分配

假设系统中有 4 个实际节点,假设为其分配了 16 个槽(0-15);

  • 槽 0-3 位于节点 node1;
  • 槽 4-7 位于节点 node2;
  • 槽 8-11 位于节点 node3;
  • 槽 12-15 位于节点 node4。

如果此时删除 node2,只需要将槽 4-7 重新分配即可,例如将槽 4-5 分配给 node1,槽 6 分配给 node3,槽 7 分配给 node4,数据在节点上的分布仍然较为均衡。

如果此时增加 node5,也只需要将一部分槽分配给 node5 即可,比如说将槽 3、槽 7、槽 11、槽 15 迁移给 node5,节点上的其他槽位保留。

当然了,这取决于 CRC16(key) % 槽的个数 的具体结果。因为在 Redis Cluster 中,槽的个数刚好是 2 的 14 次方,这和 HashMap 中数组的长度必须是 2 的幂次方有着异曲同工之妙。

它能保证扩容后,大部分数据停留在扩容前的位置,只有少部分数据需要迁移到新的槽上。

23.能说说 Redis 集群的原理吗?

Redis 集群通过数据分区来实现数据的分布式存储,通过自动故障转移实现高可用。

集群创建

数据分区是在集群创建的时候完成的。

集群创建

设置节点 Redis 集群一般由多个节点组成,节点数量至少为 6 个才能保证组成完整高可用的集群。每个节点需要开启配置 cluster-enabled yes,让 Redis 运行在集群模式下。

节点握手 节点握手是指一批运行在集群模式下的节点通过 Gossip 协议彼此通信, 达到感知对方的过程。节点握手是集群彼此通信的第一步,由客户端发起命 令:cluster meet{ip}{port}。完成节点握手之后,一个个的 Redis 节点就组成了一个多节点的集群。

分配槽(slot) Redis 集群把所有的数据映射到 16384 个槽中。每个节点对应若干个槽,只有当节点分配了槽,才能响应和这些槽关联的键命令。通过 cluster addslots 命令为节点分配槽。

故障转移

Redis 集群的故障转移和哨兵的故障转移类似,但是 Redis 集群中所有的节点都要承担状态维护的任务。

故障发现 Redis 集群内节点通过 ping/pong 消息实现节点通信,集群中每个节点都会定期向其他节点发送 ping 消息,接收节点回复 pong 消息作为响应。如果在 cluster-node-timeout 时间内通信一直失败,则发送节 点会认为接收节点存在故障,把接收节点标记为主观下线(pfail)状态。

主观下线

当某个节点判断另一个节点主观下线后,相应的节点状态会跟随消息在集群内传播。通过 Gossip 消息传播,集群内节点不断收集到故障节点的下线报告。当半数以上持有槽的主节点都标记某个节点是主观下线时。触发客观下线流程。

客观下线

故障恢复

故障节点变为客观下线后,如果下线节点是持有槽的主节点则需要在它 的从节点中选出一个替换它,从而保证集群的高可用。

故障恢复

  • 1.资格检查 每个从节点都要检查最后与主节点断线时间,判断是否有资格替换故障 的主节点。
  • 2.准备选举时间 当从节点符合故障转移资格后,更新触发故障选举的时间,只有到达该 时间后才能执行后续流程。
  • 3.发起选举 当从节点定时任务检测到达故障选举时间(failover_auth_time)到达后,发起选举流程。
  • 4.选举投票 持有槽的主节点处理故障选举消息。投票过程其实是一个领导者选举的过程,如集群内有 N 个持有槽的主节 点代表有 N 张选票。由于在每个配置纪元内持有槽的主节点只能投票给一个 从节点,因此只能有一个从节点获得 N/2+1 的选票,保证能够找出唯一的从节点。
  • 5.替换主节点 当从节点收集到足够的选票之后,触发替换主节点操作。

选举

24.部署 Redis 集群至少需要几个物理节点?

在投票选举的环节,故障主节点也算在投票数内,假设集群内节点规模是 3 主 3 从,其中有 2 个主节点部署在一台机器上,当这台机器宕机时,由于从节点无法收集到 3/2+1 个主节点选票将导致故障转移失败。这个问题也适用于故障发现环节。因此部署集群时所有主节点最少需要部署在 3 台物理机上才能避免单点问题。

25.说说集群的伸缩?

Redis 集群提供了灵活的节点扩容和收缩方案,可以在不影响集群对外服务的情况下,为集群添加节点进行扩容也可以下线部分节点进行缩容。

集群伸缩

其实,集群扩容和缩容的关键点,就在于槽和节点的对应关系,扩容和缩容就是将一部分槽和数据迁移给新节点。

例如下面一个集群,每个节点对应若干个槽,每个槽对应一定的数据,如果希望加入 1 个节点希望实现集群扩容时,需要通过相关命令把一部分槽和内容迁移给新节点。

error

缩容也是类似,先把槽和数据迁移到其它节点,再把对应的节点下线。

26.什么是缓存击穿、缓存穿透、缓存雪崩?

缓存穿透、缓存击穿和缓存雪崩是指在使用 Redis 做为缓存时可能遇到的三种问题。

缓存击穿(Cache Breakdown)

缓存击穿是指某一个或少数几个数据被高频访问,当这些数据在缓存中过期的那一刻,大量请求就会直接到达数据库,导致数据库瞬间压力过大。

缓存击穿

解决方案

①、加锁更新,比如请求查询 A,发现缓存中没有,对 A 这个 key 加锁,同时去数据库查询数据,写⼊缓存,再返回给用户,这样后面的请求就可以从缓存中拿到数据了。

加锁更新

②、将过期时间组合写在 value 中,通过异步的方式不断刷新过期时间,防止此类现象

缓存穿透(Cache Penetration)

缓存穿透是指查询不存在的数据,由于缓存没有命中(因为数据根本就不存在),请求每次都会穿过缓存去查询数据库。如果这种查询非常频繁,就会给数据库造成很大的压力。

缓存穿透

缓存穿透意味着缓存失去了减轻数据压力的意义。缓存穿透可能有两种原因:

1.自身业务代码问题

2.恶意攻击,爬虫造成空命中

解决方案

①、缓存空值/默认值

在数据库无法命中之后,把一个空对象或者默认值保存到缓存,之后再访问这个数据,就会从缓存中获取,这样就保护了数据库。

缓存空值

缓存空值有两大问题:

  • 1.空值做了缓存,意味着缓存层中存了更多的键,需要更多的内存空间(如果是攻击,问题更严重),比较有效的方法是针对这类数据设置一个较短的过期时间,让其自动剔除。
  • 2.缓存层和存储层的数据会有一段时间窗口的不一致,可能会对业务有一定影响。 例如过期时间设置为 5 分钟,如果此时存储层添加了这个数据,那此段时间就会出现缓存层和存储层数据的不一致。

这时候可以利用消息队列或者其它异步方式清理缓存中的空对象。

②、布隆过滤器

除了缓存空对象,我们还可以在存储和缓存之前,加一个布隆过滤器,做一层过滤。

布隆过滤器里会保存数据是否存在,如果判断数据不存在,就不会访问存储。

布隆过滤器

两种解决方案的对比:

two plans

缓存雪崩(Cache Avalanche)

缓存雪崩是指在某个时间段内,大量缓存同时失效,导致大量请求直接访问数据库,造成数据库压力过大甚至崩溃。

缓存雪崩

解决方案

1、提高缓存可用性

集群部署:采用分布式缓存而不是单一缓存服务器,可以降低单点故障的风险。即使某个缓存节点发生故障,其他节点仍然可以提供服务,从而避免对数据库的大量直接访问。

可以利用 Redis Cluster,或者第三方集群方案 Codis。

备份缓存:对于关键数据,除了在主缓存中存储,还可以在备用缓存中保存一份。当主缓存不可用时,可以快速切换到备用缓存,确保系统的稳定性和可用性。

2、过期时间

对于缓存数据,设置不同的过期时间,避免大量缓存数据同时过期。可以通过在原有过期时间的基础上添加一个随机值来实现,这样可以分散缓存过期时间,减少同一时间对数据库的访问压力。

3、限流和降级

通过设置合理的系统限流策略,如令牌桶或漏斗算法,来控制访问流量,防止在缓存失效时数据库被打垮。

此外,系统可以实现降级策略,在缓存雪崩或系统压力过大时,暂时关闭一些非核心服务,确保核心服务的正常运行。

27.能说说布隆过滤器吗?

布隆过滤器(Bloom Filter)是一种空间效率极高的概率型数据结构,用于快速检查一个元素是否存在于一个集合中。

布隆过滤器由一个长度为 m 的位数组和 k 个哈希函数组成。

  • 开始时,布隆过滤器的每个位都被设置为 0。
  • 当一个元素被添加到过滤器中时,它会被 k 个哈希函数分别计算得到 k 个位置,然后将位数组中对应的位设置为 1。
  • 当检查一个元素是否存在于过滤器中时,同样使用 k 个哈希函数计算位置,如果任一位置的位为 0,则该元素肯定不在过滤器中;如果所有位置的位都为 1,则该元素可能在过滤器中。

布隆过滤器

因为布隆过滤器占用的内存空间非常小,所以查询效率也非常高,所以在 Redis 缓存中,使用布隆过滤器可以快速判断请求的数据是否在缓存中。

但是布隆过滤器也有一定的缺点,因为是通过哈希函数计算的,所以存在哈希冲突的问题,可能会导致误判

28.如何保证缓存和数据库的数据⼀致性?

采用先写 MySQL,再删除 Redis 的方式来保证缓存和数据库的数据一致性。

为什么要删除缓存而不是更新缓存

因为相对而言,删除缓存的速度比更新缓存的速度要快得多。举个例子:假设商品 product_123 的当前库存是 10,现在有一次购买操作,库存减 1,我们需要更新 Redis 中的库存信息。

product_id = "product_123"
// 假设这是购买操作后的新库存值
new_stock = 9

// 更新Redis中的库存信息
redis.set(product_id, new_stock)

更新操作至少涉及到两个步骤:计算新的库存值和更新 Redis 中的库存值。

假如是直接删除操作,直接就一步到位了:

product_id = "product_123"

// 删除Redis中的库存缓存
redis.del(product_id)

假如是更新缓存,那么可能请求 A 更新完 MySQL 后在更新 Redis 中,请求 B 已经读取到 Redis 中的旧值返回了,又一次导致了缓存和数据库不一致。

为什么要先更新数据库,再删除缓存?

因为更新数据库的速度比删除缓存的速度要慢得多。因为更新 MySQL 是磁盘 IO 操作,而 Redis 是内存操作。内存操作比磁盘 IO 快得多(这是硬件层面的天然差距)。

那假如是先删除缓存,再更新数据库,就会造成这样的情况:

缓存中不存在,数据库又没有完成更新,此时有请求进来读取数据,并写入到缓存,那么在更新完缓存后,缓存中这个 key 就成了一个脏数据。

先更数据库还是先删缓存

目前最流行的缓存读写策略 Cache Aside Pattern(旁路缓存模式)就是采用的先写数据库,再删缓存的方式。

  • 失效:应用程序先从缓存读取数据,如果数据不存在,再从数据库中读取数据,成功后,放入缓存。
  • 命中:应用程序从缓存读取数据,如果数据存在,直接返回。
  • 更新:先把数据写入数据库,成功后,再让缓存失效。

那假如对一致性要求很高,该怎么办呢

缓存和数据库数据不一致的原因,常见的有两种:

  • 缓存删除失败
  • 并发导致写入了脏数据

那通常有四种方案可以解决。

①、引入消息队列保证缓存被删除

使用消息队列(如 Kafka、RabbitMQ)保证数据库更新和缓存更新之间的最终一致性。当数据库更新完成后,将更新事件发送到消息队列。有专门的服务监听这些事件并负责更新或删除缓存。

消息队列保证key被删除

这种方案很不错,缺点是对业务代码有一定的侵入,引入了消息队列。

②、数据库订阅+消息队列保证缓存被删除

可以专门起一个服务(比如 Canal,阿里巴巴 MySQL binlog 增量订阅&消费组件)去监听 MySQL 的 binlog,获取需要操作的数据。

canal

然后用一个公共的服务获取订阅程序传来的信息,进行缓存删除。

数据库订阅+消息队列保证key被删除

这种方式虽然降低了对业务的侵入,但增加了整个系统的复杂度,适合基建完善的大厂。

③、延时双删防止脏数据

简单说,就是在第一次删除缓存之后,过一段时间之后,再次删除缓存。

主要针对缓存不存在,但写入了脏数据的情况。在先删缓存,再写数据库的更新策略下发生的比较多。

延时双删

这种方式的延时时间需要仔细考量和测试。

④:设置缓存过期时间兜底

这是一个朴素但有用的兜底策略,给缓存设置一个合理的过期时间,即使发生了缓存和数据库的数据不一致问题,也不会永远不一致下去,缓存过期后,自然就一致了。

29.如何保证本地缓存和分布式缓存的一致?

为了保证本地缓存和 Redis 缓存的一致性,我们可以采用的策略有:

①、设置本地缓存的过期时间,这是最简单也是最直接的方法,当本地缓存过期时,就从 Redis 缓存中去同步。

②、使用 Redis 的 Pub/Sub 机制,当 Redis 缓存发生变化时,发布一个消息,本地缓存订阅这个消息,然后删除对应的本地缓存。

③、Redis 缓存发生变化时,引入消息队列,比如 RocketMQ、RabbitMQ 去更新本地缓存。

如果在项目中多个地方都要使用到二级缓存的逻辑,如何设计这一块?

在设计时,应该清楚地区分何时使用一级缓存和何时使用二级缓存。通常情况下,对于频繁访问但不经常更改的数据,可以放在本地缓存中以提供最快的访问速度。而对于需要共享或者一致性要求较高的数据,应当放在一级缓存中。

本地缓存和 Redis 缓存的区别和效率对比?

Redis 可以部署在多个节点上,支持数据分片,适用于跨服务器的缓存共享。而本地缓存只能在单个服务器上使用。

Redis 还可以持久化数据,支持数据备份和恢复,适用于对数据安全性要求较高的场景。并且支持发布/订阅、事务、Lua 脚本等高级功能。

效率上,Redis 和本地缓存都是存储在内存中,读写速度都非常快。

30.怎么处理热 key?

所谓的热 key,就是指在很短时间内被频繁访问的键。

比如,热门新闻或热门商品,这类 key 通常会有大流量的访问,对存储这类信息的 Redis 来说,是不小的压力。

某天某流量明星突然爆出一个大瓜,微博突然就崩了,这就是热 key 的压力。

再比如说 Redis 是集群部署,热 key 可能会造成整体流量的不均衡(网络带宽、CPU 和内存资源),个别节点出现 OPS 过大的情况,极端情况下热点 key 甚至会超过 Redis 本身能够承受的 OPS。

OPS(Operations Per Second)是 Redis 的一个重要指标,表示 Redis 每秒钟能够处理的命令数。

通常以 Key 被请求的频率来判定,比如:

  • QPS 集中在特定的 Key:总的 QPS(每秒查询率)为 10000,其中一个 Key 的 QPS 飙到了 8000。
  • 带宽使用率集中在特定的 Key:一个拥有上千成员且总大小为 1M 的哈希 Key,每秒发送大量的 HGETALL 请求。
  • CPU 使用率集中在特定的 Key:一个拥有数万个成员的 ZSET Key,每秒发送大量的 ZRANGE 请求。

  • HGETALL 命令用于返回哈希表中,所有的字段和值。
  • ZRANGE 命令用于返回有序集中,指定区间内的成员。

捕捉热key

对热 key 的处理,最关键的是对热 key 的监控:

①、客户端

客户端其实是距离 key“最近”的地方,因为 Redis 命令就是从客户端发出的,例如在客户端设置全局字典(key 和调用次数),每次调用 Redis 命令时,使用这个字典进行记录。

②、代理端

像 Twemproxy、Codis 这些基于代理的 Redis 分布式架构,所有客户端的请求都是通过代理端完成的,可以在代理端进行监控。

③、Redis 服务端

使用 monitor 命令统计热点 key 是很多开发和运维人员首先想到的方案,monitor 命令可以监控到 Redis 执行的所有命令。

monitor 命令的使用:redis-cli monitor

还可以通过 bigkeys 参数来分析热 Key。

bigkeys 命令的使用:redis-cli --bigkeys

处理热key

只要监控到了热 key,对热 key 的处理就简单了:

①、把热 key 打散到不同的服务器,降低压⼒。

基本思路就是给热 Key 加上前缀或者后缀,见下例:

// N 为 Redis 实例个数,M 为 N 的 2倍
const M = N * 2
//生成随机数
random = GenRandom(0, M)
//构造备份新 Key
bakHotKey = hotKey + "_" + random
data = redis.GET(bakHotKey)
if data == NULL {
    data = redis.GET(hotKey)
    if data == NULL {
        data = GetFromDB()
        // 可以利用原子锁来写入数据保证数据一致性
        redis.SET(hotKey, data, expireTime)
        redis.SET(bakHotKey, data, expireTime + GenRandom(0, 5))
    } else {
        redis.SET(bakHotKey, data, expireTime + GenRandom(0, 5))
    }
}

②、加⼊二级缓存,当出现热 Key 后,把热 Key 加载到 JVM 中,后续针对这些热 Key 的请求,直接从 JVM 中读取。

这些本地的缓存工具有很多,比如 Caffeine、Guava 等,或者直接使用 HashMap 作为本地缓存都是可以的。

注意,如果对热 Key 进行本地缓存,需要防止本地缓存过大。

31.缓存预热怎么做呢?

缓存预热是指在系统启动时,提前将一些预定义的数据加载到缓存中,以避免在系统运行初期由于缓存未命中(cache miss)导致的性能问题。

通过缓存预热,可以确保系统在上线后能够立即提供高效的服务,减少首次访问时的延迟。

可以使用项目启动时自动加载和定时预热两种方式,比如说每天定时更新站点地图到 Redis 缓存中。

/**
 * 采用定时器方案,每天5:15分刷新站点地图,确保数据的一致性
 */
@Scheduled(cron = "0 15 5 * * ?")
public void autoRefreshCache() {
    log.info("开始刷新sitemap.xml的url地址,避免出现数据不一致问题!");
    refreshSitemap();
    log.info("刷新完成!");
}

@Override
public void refreshSitemap() {
    initSiteMap();
}

private synchronized void initSiteMap() {
    long lastId = 0L;
    RedisClient.del(SITE_MAP_CACHE_KEY);
    while (true) {
        List<SimpleArticleDTO> list = articleDao.getBaseMapper().listArticlesOrderById(lastId, SCAN_SIZE);

        // 刷新站点地图信息
        Map<String, Long> map = list.stream().collect(Collectors.toMap(s -> String.valueOf(s.getId()), s -> s.getCreateTime().getTime(), (a, b) -> a));
        RedisClient.hMSet(SITE_MAP_CACHE_KEY, map);
        if (list.size() < SCAN_SIZE) {
            break;
        }
        lastId = list.get(list.size() - 1).getId();
    }
}

32.热点 key 重建?问题?解决?

开发的时候一般使用“缓存+过期时间”的策略,既可以加速数据读写,又保证数据的定期更新,这种模式基本能够满足绝大部分需求。

但是有两个问题如果同时出现,可能就会出现比较大的问题:

当前 key 是一个热点 key(例如一个热门的娱乐新闻),并发量非常大。

重建缓存不能在短时间完成,可能是一个复杂计算,例如复杂的 SQL、多次 IO、多个依赖等。 在缓存失效的瞬间,有大量线程来重建缓存,造成后端负载加大,甚至可能会让应用崩溃。

怎么处理呢?

要解决这个问题也不是很复杂,解决问题的要点在于:

  • 减少重建缓存的次数。
  • 数据尽可能一致。
  • 较少的潜在危险。

所以一般采用如下方式:

  • 互斥锁(mutex key) 这种方法只允许一个线程重建缓存,其他线程等待重建缓存的线程执行完,重新从缓存获取数据即可。
  • 永远不过期 “永远不过期”包含两层意思:
    • 从缓存层面来看,确实没有设置过期时间,所以不会出现热点 key 过期后产生的问题,也就是“物理”不过期。
    • 从功能层面来看,为每个 value 设置一个逻辑过期时间,当发现超过逻辑过期时间后,会使用单独的线程去构建缓存。

33.知道无底洞问题吗?如何解决?

什么是无底洞问题?

2010 年,Facebook 的 Memcache 节点已经达到了 3000 个,承载着 TB 级别的缓存数据。但开发和运维人员发现了一个问题,为了满足业务要求添加了大量新 Memcache 节点,但是发现性能不但没有好转反而下降了,当时将这 种现象称为缓存的“无底洞”现象。

那么为什么会产生这种现象呢?

通常来说添加节点使得 Memcache 集群 性能应该更强了,但事实并非如此。键值数据库由于通常采用哈希函数将 key 映射到各个节点上,造成 key 的分布与业务无关,但是由于数据量和访问量的持续增长,造成需要添加大量节点做水平扩容,导致键值分布到更多的 节点上,所以无论是 Memcache 还是 Redis 的分布式,批量操作通常需要从不同节点上获取,相比于单机批量操作只涉及一次网络操作,分布式批量操作会涉及多次网络时间。

无底洞问题如何优化呢

先分析一下无底洞问题:

客户端一次批量操作会涉及多次网络操作,也就意味着批量操作会随着节点的增多,耗时会不断增大。

网络连接数变多,对节点的性能也有一定影响。

常见的优化思路如下:

命令本身的优化,例如优化操作语句等。

减少网络通信次数。

降低接入成本,例如客户端使用长连/连接池、NIO 等。

34.Redis 报内存不足怎么处理?

Redis 内存不足有这么几种处理方式:

  • 修改配置文件 redis.conf 的 maxmemory 参数,增加 Redis 可用内存
  • 也可以通过命令 set maxmemory 动态设置内存上限
  • 修改内存淘汰策略,及时释放内存空间
  • 使用 Redis 集群模式,进行横向扩容。

35.Redis 的过期数据回收策略有哪些?

Redis 支持为键设置过期时间,当键的过期时间到达后,Redis 会自动删除这些键。过期回收策略主要有两种:惰性删除和定期删除。

什么是惰性删除?

当某个键被访问时,如果发现它已经过期,Redis 会立即删除该键。这意味着如果一个已过期的键从未被访问,它不会被自动删除,可能会占用额外的内存。

什么是定期删除?

Redis 会定期随机测试一些键,并删除其中已过期的键。这个过程是 Redis 内部自动执行的,旨在减少过期键对内存的占用。可以通过 config get hz 命令查看当前的 hz 值。

shell

结果显示 hz 的值为 “10”。这意味着 Redis 服务器每秒执行其内部定时任务(如过期键的清理)的频率是 10 次。可以通过 CONFIG SET hz 20 进行调整,或者直接通过配置文件中的 hz 设置。

36.Redis 有哪些内存淘汰策略?

Redis 提供了多种内存淘汰策略,用于在内存达到上限时自动删除一些数据,以释放内存。以下是 Redis 支持的内存淘汰策略:

1. noeviction

  • 描述:当内存不足时,不再接受写入操作,直接返回错误。
  • 适用场景:适用于对数据完整性要求较高的场景,不希望任何数据被淘汰。

2. allkeys-lru

  • 描述:在所有键中使用 LRU(最近最少使用)算法进行淘汰。
  • 适用场景:适用于希望优先淘汰不常用数据的场景。

3. volatile-lru

  • 描述:在设置了过期时间的键中使用 LRU 算法进行淘汰。
  • 适用场景:适用于希望优先淘汰不常用且设置了过期时间的数据的场景。

4. allkeys-random

  • 描述:在所有键中随机淘汰。
  • 适用场景:适用于数据访问模式不确定的场景。

5. volatile-random

  • 描述:在设置了过期时间的键中随机淘汰。
  • 适用场景:适用于希望优先淘汰设置了过期时间的数据的场景。

6. volatile-ttl

  • 描述:在设置了过期时间的键中,优先淘汰存活时间(TTL)最短的键。
  • 适用场景:适用于希望优先淘汰即将过期的数据的场景。

7. volatile-lfu

  • 描述:在设置了过期时间的键中使用 LFU(最少使用频率)算法进行淘汰。
  • 适用场景:适用于希望优先淘汰使用频率较低且设置了过期时间的数据的场景。

8. allkeys-lfu

  • 描述:在所有键中使用 LFU 算法进行淘汰。
  • 适用场景:适用于希望优先淘汰使用频率较低的数据的场景。

配置示例

在 Redis 配置文件(redis.conf)中设置内存淘汰策略:

maxmemory 2gb
maxmemory-policy allkeys-lru

也可以通过命令动态设置内存淘汰策略:

redis-cli config set maxmemory 2gb
redis-cli config set maxmemory-policy allkeys-lru

示例代码

以下是一个简单的示例代码,展示了如何设置 Redis 的内存淘汰策略:

import redis.clients.jedis.Jedis;

public class RedisEvictionPolicyExample {

    public static void main(String[] args) {
        Jedis jedis = new Jedis("localhost");

        // 设置最大内存为 2GB
        jedis.configSet("maxmemory", "2gb");

        // 设置内存淘汰策略为 allkeys-lru
        jedis.configSet("maxmemory-policy", "allkeys-lru");

        // 添加一些数据
        for (int i = 0; i < 100000; i++) {
            jedis.set("key" + i, "value" + i);
        }

        jedis.close();
    }
}

总结

Redis 提供了多种内存淘汰策略,包括 noevictionallkeys-lruvolatile-lruallkeys-randomvolatile-randomvolatile-ttlvolatile-lfuallkeys-lfu。通过合理选择和配置内存淘汰策略,可以有效管理 Redis 的内存使用,确保系统的稳定性和性能。

37.Redis 阻塞?怎么解决?

API 或数据结构使用不合理

通常 Redis 执行命令速度非常快,但是不合理地使用命令,可能会导致执行速度很慢,导致阻塞,对于高并发的场景,应该尽量避免在大对象上执行算法复杂 度超过 O(n)的命令。

对慢查询的处理分为两步:

  • 1.发现慢查询: slowlog get{n}命令可以获取最近 的 n 条慢查询命令;
  • 2.发现慢查询后,可以从两个方向去优化慢查询:
    • 1)修改为低算法复杂度的命令,如 hgetall 改为 hmget 等,禁用 keys、sort 等命令
    • 2)调整大对象:缩减大对象数据或把大对象拆分为多个小对象,防止一次命令操作过多的数据。

CPU 饱和的问题

单线程的 Redis 处理命令时只能使用一个 CPU。而 CPU 饱和是指 Redis 单核 CPU 使用率跑到接近 100%。

针对这种情况,处理步骤一般如下:

  • 1.判断当前 Redis 并发量是否已经达到极限,可以使用统计命令 redis-cli-h{ip}-p{port}–stat 获取当前 Redis 使用情况
  • 2.如果 Redis 的请求几万+,那么大概就是 Redis 的 OPS 已经到了极限,应该做集群化水品扩展来分摊 OPS 压力
  • 3.如果只有几百几千,那么就得排查命令和内存的使用

持久化相关的阻塞

对于开启了持久化功能的 Redis 节点,需要排查是否是持久化导致的阻塞。

  • 1.fork 阻塞 fork 操作发生在 RDB 和 AOF 重写时,Redis 主线程调用 fork 操作产生共享 内存的子进程,由子进程完成持久化文件重写工作。如果 fork 操作本身耗时过长,必然会导致主线程的阻塞。
  • 2.AOF 刷盘阻塞 当我们开启 AOF 持久化功能时,文件刷盘的方式一般采用每秒一次,后台线程每秒对 AOF 文件做 fsync 操作。当硬盘压力过大时,fsync 操作需要等待,直到写入完成。如果主线程发现距离上一次的 fsync 成功超过 2 秒,为了 数据安全性它会阻塞直到后台线程执行 fsync 操作完成。
  • 3.HugePage 写操作阻塞 对于开启 Transparent HugePages 的 操作系统,每次写命令引起的复制内存页单位由 4K 变为 2MB,放大了 512 倍,会拖慢写操作的执行时间,导致大量写操作慢查询。

38.大 key 问题了解吗?

大 key 指的是存储了大量数据的键,比如:

  • 单个简单的 key 存储的 value 很大,size 超过 10KB
  • hash,set,zset,list 中存储过多的元素(以万为单位)

如何找到大 key?

①、bigkeys 参数:使用 bigkeys 命令以遍历的方式分析 Redis 实例中的所有 Key,并返回整体统计信息与每个数据类型中 Top1 的大 Key

bigkeys 命令的使用:redis-cli –bigkeys

②、redis-rdb-tools:redis-rdb-tools 是由 Python 语言编写的用来分析 Redis 中 rdb 快照文件的工具。

如何处理大 key?

大key处理

①、删除大 key

  • 当 Redis 版本大于 4.0 时,可使用 UNLINK 命令安全地删除大 Key,该命令能够以非阻塞的方式,逐步地清理传入的大 Key。
  • 当 Redis 版本小于 4.0 时,建议通过 SCAN 命令执行增量迭代扫描 key,然后判断进行删除。

②、压缩和拆分 key

  • 当 vaule 是 string 时,比较难拆分,则使用序列化、压缩算法将 key 的大小控制在合理范围内,但是序列化和反序列化都会带来额外的性能消耗。
  • 当 value 是 string,压缩之后仍然是大 key 时,则需要进行拆分,将一个大 key 分为不同的部分,记录每个部分的 key,使用 multiget 等操作实现事务读取。
  • 当 value 是 list/set 等集合类型时,根据预估的数据规模来进行分片,不同的元素计算后分到不同的片。

39.Redis 常见性能问题和解决方案?

  • 1.Master 最好不要做任何持久化工作,包括内存快照和 AOF 日志文件,特别是不要启用内存快照做持久化。
  • 2.如果数据比较关键,某个 Slave 开启 AOF 备份数据,策略为每秒同步一次。
  • 3.为了主从复制的速度和连接的稳定性,Slave 和 Master 最好在同一个局域网内。
  • 4.尽量避免在压力较大的主库上增加从库。
  • 5.Master 调用 BGREWRITEAOF 重写 AOF 文件,AOF 在重写的时候会占大量的 CPU 和内存资源,导致服务 load 过高,出现短暂服务暂停现象。
  • 6.为了 Master 的稳定性,主从复制不要用图状结构,用单向链表结构更稳定,即主从关为:Master<–Slave1<–Slave2<–Slave3…,这样的结构也方便解决单点故障问题,实现 Slave 对 Master 的替换,也即,如果 Master 挂了,可以立马启用 Slave1 做 Master,其他不变。

40.使用 Redis 如何实现异步队列?

Redis作为异步队列通常有以下几种情况:

使用 list 作为队列,lpush 生产消息,rpop 消费消息

这种方式,消费者死循环 rpop 从队列中消费消息。但是这样,即使队列里没有消息,也会进行 rpop,会导致 Redis CPU 的消耗。

list

可以通过让消费者休眠的方式的方式来处理,但是这样又会又消息的延迟问题。

使用 list 作为队列,lpush 生产消息,brpop 消费消息

brpop 是 rpop 的阻塞版本,list 为空的时候,它会一直阻塞,直到 list 中有值或者超时。

blist

这种方式只能实现一对一的消息队列。

使用 Redis 的 pub/sub 来进行消息的发布/订阅

发布/订阅模式可以 1:N 的消息发布/订阅。发布者将消息发布到指定的频道频道(channel),订阅相应频道的客户端都能收到消息。

订阅

但是这种方式不是可靠的,它不保证订阅者一定能收到消息,也不进行消息的存储。

所以,一般的异步队列的实现还是交给专业的消息队列。

41.Redis 如何实现延时队列?

可以使用 Redis 的 zset(有序集合)来实现延时队列。

zset实现延时队列

第一步,将任务添加到 zset 中,score 为任务的执行时间戳,value 为任务的内容。

ZADD delay_queue 1617024000 task1

第二步,定期(例如每秒)从 zset 中获取 score 小于当前时间戳的任务,然后执行任务。

ZREMRANGEBYSCORE delay_queue -inf 1617024000

第三步,任务执行后,从 zset 中删除任务。

ZREM delay_queue task1

41.Redis 支持事务吗?

Redis 支持简单的事务,可以将多个命令打包,然后一次性的,按照顺序执行。主要通过 multi、exec、discard、watch 等命令来实现:

  • multi:标记一个事务块的开始
  • exec:执行所有事务块内的命令
  • discard:取消事务,放弃执行事务块内的所有命令
  • watch:监视一个或多个 key,如果在事务执行之前这个 key 被其他命令所改动,那么事务将被打断

事务的原理

事务

  • 使用 MULTI 命令开始一个事务。从这个命令执行之后开始,所有的后续命令都不会立即执行,而是被放入一个队列中。在这个阶段,Redis 只是记录下了这些命令。
  • 使用 EXEC 命令触发事务的执行。一旦执行了 EXEC,之前 MULTI 后队列中的所有命令会被原子地(atomic)执行。这里的“原子”意味着这些命令要么全部执行,要么(在出现错误时)全部不执行。
  • 如果在执行 EXEC 之前决定不执行事务,可以使用 DISCARD 命令来取消事务。这会清空事务队列并退出事务状态。
  • WATCH 命令用于实现乐观锁。WATCH 命令可以监视一个或多个键,如果在执行事务的过程中(即在执行 MULTI 之后,执行 EXEC 之前),被监视的键被其他命令改变了,那么当执行 EXEC 时,事务将被取消,并且返回一个错误。

Redis 事务的注意点有哪些?

Redis 事务是不支持回滚的,不像 MySQL 的事务一样,要么都执行要么都不执行;一旦 EXEC 命令被调用,所有命令都会被执行,即使有些命令可能执行失败。失败的命令不会影响到其他命令的执行。

Redis 事务为什么不支持回滚?

引入事务回滚机制会大大增加 Redis 的复杂性,因为需要跟踪事务中每个命令的状态,并在发生错误时逆向执行命令以恢复原始状态。

Redis 是一个基于内存的数据存储系统,其设计重点是实现高性能。事务回滚需要额外的资源和时间来管理和执行,这与 Redis 的设计目标相违背。因此,Redis 选择不支持事务回滚。

Redis 事务的 ACID 特性如何体现?

ACID 是 MySQL 事务中常见的四个属性:原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)、持久性(Durability)。虽然 Redis 提供了事务的支持,但它在 ACID 属性上的表现与 MySQL 有所不同。

Redis 事务中,所有命令会依次执行,但并不支持部分失败后的自动回滚。因此 Redis 的事务原子性有限。

Redis 在事务层面并不强制保证一致性。用户必须通过正确的命令顺序和逻辑来确保在事务执行后数据的一致性。

Redis 事务在一定程度上提供了隔离性,事务中的命令会按顺序执行,不会被其他客户端的命令插入。

Redis 的持久性依赖于其持久化机制(如 RDB 和 AOF),而不是事务本身。

42.Redis 和 Lua 脚本的使用了解吗?

Redis 的事务功能比较简单,平时的开发中,可以利用 Lua 脚本来增强 Redis 的命令。

Lua 脚本能给开发人员带来这些好处:

  • Lua 脚本在 Redis 中是原子执行的,执行过程中间不会插入其他命令。
  • Lua 脚本可以帮助开发和运维人员创造出自己定制的命令,并可以将这 些命令常驻在 Redis 内存中,实现复用的效果。
  • Lua 脚本可以将多条命令一次性打包,有效地减少网络开销。
-- 尝试获取锁
if redis.call("setnx", KEYS[1], ARGV[1]) == 1 then
    redis.call("expire", KEYS[1], ARGV[2])
    return 1
else
    return 0
end
import redis.clients.jedis.Jedis;

public class RedisDistributedLock {

    private static final String LOCK_SCRIPT = 
        "if redis.call('setnx', KEYS[1], ARGV[1]) == 1 then " +
        "redis.call('expire', KEYS[1], ARGV[2]) " +
        "return 1 " +
        "else " +
        "return 0 " +
        "end";

    public static void main(String[] args) {
        Jedis jedis = new Jedis("localhost");

        String lockKey = "lock:key";
        String lockValue = "unique_value";
        int lockExpire = 10; // 锁的过期时间,单位:秒

        // 尝试获取锁
        Object result = jedis.eval(LOCK_SCRIPT, 1, lockKey, lockValue, String.valueOf(lockExpire));
        if ("1".equals(result.toString())) {
            System.out.println("Lock acquired");
            // 执行需要加锁的操作
            // ...
        } else {
            System.out.println("Failed to acquire lock");
        }

        jedis.close();
    }
}

43.Redis 的管道Pipeline了解吗?

Pipeline 是 Redis 提供的一种优化手段,允许客户端一次性向服务器发送多个命令,而不必等待每个命令的响应,从而减少网络延迟。它的工作原理类似于批量操作,即多个命令一次性打包发送,Redis 服务器依次执行后再将结果一次性返回给客户端。

通常在 Redis 中,每个请求都会遵循以下流程:

  • 1.客户端发送命令到服务器。
  • 2.服务器执行命令并将结果返回给客户端。
  • 3.客户端接收返回结果。

每一个请求和响应之间存在一次网络通信的往返时间(RTT,Round-Trip Time),如果大量请求依次发送,网络延迟会显著增加请求的总执行时间。

有了 Pipeline 后,流程变为:

发送命令1、命令2、命令3…… -> 服务器处理 -> 一次性返回所有结果。

例如,批量写入大量数据或执行一系列查询时,可以将这些操作打包通过 Pipeline 执行。

Pipelining

在 Pipeline 模式下,客户端不会在每条命令发送后立即等待 Redis 的响应,而是将多个命令依次写入 TCP 缓冲区,所有命令一起发送到 Redis 服务器。

Redis 服务器接收到批量命令后,依次执行每个命令。

Redis 服务器执行完所有命令后,将每条命令的结果一次性打包通过 TCP 返回给客户端。

客户端一次性接收所有返回结果,并解析每个命令的执行结果。

43.Redis 实现分布式锁了解吗?

分布式锁是一种用于在分布式系统中协调对共享资源的访问的机制。Redis 提供了一种简单而有效的方式来实现分布式锁,通常使用 SETNX 命令和 EXPIRE 命令来实现。

可以使用 Redis 的 SET 命令实现分布式锁。SET 命令支持设置键值对的同时添加过期时间,这样可以防止死锁的发生。

set原子命令

SET key value NX PX 30000
  • key 是锁名。
  • value 是锁的持有者标识,可以使用 UUID 作为 value。
  • NX 只在键不存在时设置。
  • PX 30000:设置键的过期时间为 30 秒(防止死锁)。

上面这段命令其实是 setnx 和 expire 组合在一起的原子命令,算是比较完善的一个分布式锁了。

当然,实际的开发中,没人会去自己写分布式锁的命令,因为有专业的轮子——Redisson。(戳链接跳转至悟空聊架构:分布式锁中的王者方案 - Redisson)

Redisson

Redisson 是一个基于 Redis 的 Java 驻内存数据网格(In-Memory Data Grid),提供了一系列 API 用来操作 Redis,其中最常用的功能就是分布式锁。

RLock lock = redisson.getLock("lock");
lock.lock();
try {
    // do something
} finally {
    lock.unlock();
}

普通锁的实现源码是在 RedissonLock 类中,也是通过 Lua 脚本封装一些 Redis 命令来实现的的,比如说 tryLockInnerAsync 源码:

RedissonLock

其中 hincrby 命令用于对哈希表中的字段值执行自增操作,pexpire 命令用于设置键的过期时间。比 SETNX 更优雅。

Redlock

Redlock 是 Redis 作者提出的一种分布式锁实现方案,用于确保在分布式环境下安全可靠地获取锁。它的目标是在分布式系统中提供一种高可用、高容错的锁机制,确保在同一时刻,只有一个客户端能够成功获得锁,从而实现对共享资源的互斥访问。

Redisson 中的 RedLock 是基于 RedissonMultiLock(联锁)实现的。

Redlock

实现原理

RedissonMultiLock 的 tryLock 方法会在指定的 Redis 实例上逐一尝试获取锁。

在获取锁的过程中,Redlock 会根据配置的 waitTime(最大等待时间)和 leaseTime(锁的持有时间)进行灵活控制。比如,如果获取锁的时间小于锁的有效期(通过TTL命令获取锁的剩余时间),则表示获取锁成功。

通常,至少需要多数(如 5 个实例中的 3 个)实例成功获取锁,才能认为整个锁获取成功。

如果指定了锁的持有时间(leaseTime),在成功获取锁后,Redlock 会为锁进行续期,以防止锁在操作完成之前意外失效。

红锁能不能保证百分百上锁

Redlock 不能保证百分百上锁,因为在分布式系统中,网络延迟、时钟漂移、Redis 实例宕机等因素都可能导致锁的获取失败。

44.Redis 底层数据结构?

Redis 的底层数据结构有动态字符串(sds)链表(list)字典(ht)跳跃表(skiplist)整数集合(intset)压缩列表(ziplist) 等。

Redis Object对应的映射

比如说 string 是通过 SDS 实现的,list 是通过链表实现的,hash 是通过字典实现的,set 是通过字典实现的,zset 是通过跳跃表实现的。

类型-编码-结构

动态字符串 SDS

Redis 是通过 C 语言实现的,但 Redis 并没有直接使用 C 语言的字符串,而是自己实现了一种叫做动态字符串 SDS 的类型。

struct sdshdr {
    int len; // buf 中已使用的长度
    int free; // buf 中未使用的长度
    char buf[]; // 数据空间
};

因为C语言的字符串不记录自身的长度信息,当需要获取字符串长度时,需要遍历整个字符串,时间复杂度为O(N)。

而 SDS 保存了长度信息,这样就将获取字符串长度的时间由 O(N) 降低到了 O(1)。

链表 linkedlist

Redis 的链表是⼀个双向无环链表结构,和 Java 中的 LinkedList 类似。

链表的节点由⼀个叫做 listNode 的结构来表示,每个节点都有指向其前置节点和后置节点的指针,同时头节点的前置和尾节点的后置均指向 null。

链表linkedlist

字典 dict

⽤于保存键值对的抽象数据结构。Redis 使用 hash 表作为底层实现,一个哈希表里可以有多个哈希表节点,而每个哈希表节点就保存了字典里中的一个键值对。

每个字典带有两个 hash 表,供平时使用和 rehash 时使用,hash 表使用链地址法来解决键冲突,被分配到同⼀个索引位置的多个键值对会形成⼀个单向链表,在对 hash 表进行扩容或者缩容的时候,为了服务的可用性,rehash 的过程不是⼀次性完成的,⽽是渐进式的。

字典

跳跃表 skiplist

跳跃表(也称跳表)是有序集合 Zset 的底层实现之⼀。在 Redis 7.0 之前,如果有序集合的元素个数小于 128 个,并且每个元素的值小于 64 字节时,Redis 会使用压缩列表作为 Zset 的底层实现,否则会使用跳表;在 Redis 7.0 之后,压缩列表已经废弃,交由 listpack 来替代。

跳表

跳表由 zskiplist 和 zskiplistNode 组成,zskiplist ⽤于保存跳表的基本信息(表头、表尾、长度、层高等)。

typedef struct zskiplist {
    struct zskiplistNode *header, *tail;
    unsigned long length;
    int level;
} zskiplist;

zskiplistNode ⽤于表示跳表节点,每个跳表节点的层高是不固定的,每个节点都有⼀个指向保存了当前节点的分值和成员对象的指针。

typedef struct zskiplistNode {
    sds ele;
    double score;
    struct zskiplistNode *backward;
    struct zskiplistLevel {
        struct zskiplistNode *forward;
        unsigned int span;
    } level[];
} zskiplistNode;

整数集合 intset

⽤于保存整数值的集合抽象数据结构,不会出现重复元素,底层实现为数组。

整数集合intset

压缩列表 ziplist

压缩列表是为节约内存而开发的顺序性数据结构,它可以包含任意多个节点,每个节点可以保存⼀个字节数组或者整数值。

压缩列表

紧凑列表 listpack

listpack 是 Redis 用来替代压缩列表(ziplist)的一种内存更加紧凑的数据结构。

listpack

为了避免 ziplist 引起的连锁更新问题,listpack 中的元素不再像 ziplist 那样,保存其前一个元素的长度,而是保存当前元素的编码类型、数据,以及编码类型和数据的长度。

listpack 的元素

listpack 每个元素项不再保存上一个元素的长度,而是优化元素内字段的顺序,来保证既可以从前也可以向后遍历。

但因为 List/Hash/Set/ZSet 都严重依赖 ziplist,所以这个替换之路很漫长。

45.Redis 的 SDS 和 C 中字符串相比有什么优势?

C 语言使用了一个长度为 N+1 的字符数组来表示长度为 N 的字符串,并且字符数组最后一个元素总是 \0,这种简单的字符串表示方式 不符合 Redis 对字符串在安全性、效率以及功能方面的要求。

C语言的字符串

C 语言的字符串可能有什么问题?

  • 获取字符串长度复杂度高 :因为 C 不保存数组的长度,每次都需要遍历一遍整个数组,时间复杂度为 O(n);
  • 不能杜绝缓冲区溢出/内存泄漏的问题 : C 字符串不记录自身长度带来的另外一个问题是容易造成缓存区溢出(buffer overflow)
  • C 字符串只能保存文本数据 → 因为 C 语言中的字符串必须符合某种编码(比如 ASCII),例如中间出现的 ‘\0’ 可能会被判定为提前结束的字符串而识别不了;

Redis 如何解决?优势?

Redis sds

  • 多增加 len 表示当前字符串的长度:这样就可以直接获取长度了,复杂度 O(1);
  • 自动扩展空间:当 SDS 需要对字符串进行修改时,首先借助于 len 和 alloc 检查空间是否满足修改所需的要求,如果空间不够的话,SDS 会自动扩展空间,避免了像 C 字符串操作中的溢出情况;
  • 有效降低内存分配次数:C 字符串在涉及增加或者清除操作时会改变底层数组的大小造成重新分配,SDS 使用了 空间预分配惰性空间释放 机制,简单理解就是每次在扩展时是成倍的多分配的,在缩容是也是先留着并不正式归还给 OS;
  • 二进制安全:C 语言字符串只能保存 ascii 码,对于图片、音频等信息无法保存,SDS 是二进制安全的,写入什么读取就是什么,不做任何过滤和限制;

46.字典是如何实现的?Rehash 了解吗?

字典是 Redis 服务器中出现最为频繁的复合型数据结构。除了 hash 结构的数据会用到字典外,整个 Redis 数据库的所有 key 和 value 也组成了一个 全局字典,还有带过期时间的 key 也是一个字典。(存储在 RedisDb 数据结构中)

字典结构是什么样的呢?

Redis 中的字典相当于 Java 中的 HashMap,内部实现也差不多类似,采用哈希与运算计算下标位置;通过 “数组 + 链表” 的链地址法 来解决哈希冲突,同时这样的结构也吸收了两种不同数据结构的优点。

Redis字典结构

字典是怎么扩容的?

字典结构内部包含 两个 hashtable,通常情况下只有一个哈希表 ht[0] 有值,在扩容的时候,把 ht[0]里的值 rehash 到 ht[1],然后进行 渐进式 rehash ——所谓渐进式 rehash,指的是这个 rehash 的动作并不是一次性、集中式地完成的,而是分多次、渐进式地完成的。

待搬迁结束后,h[1]就取代 h[0]存储字典的元素。

47.跳表是如何实现的?原理?

跳表(skiplist)是一种有序的数据结构,它通过在每个节点中维持多个指向其它节点的指针,从而达到快速访问节点的目的。

跳表

为什么使用跳表?

首先,因为 zset 要支持随机的插入和删除,所以它 不宜使用数组来实现,关于排序问题,我们也很容易就想到 红黑树/ 平衡树 这样的树形结构,为什么 Redis 不使用这样一些结构呢?

  • 性能考虑: 在高并发的情况下,树形结构需要执行一些类似于 rebalance 这样的可能涉及整棵树的操作,相对来说跳跃表的变化只涉及局部;
  • 实现考虑: 在复杂度与红黑树相同的情况下,跳跃表实现起来更简单,看起来也更加直观;

基于以上的一些考虑,Redis 基于 William Pugh 的论文做出一些改进后采用了 跳跃表 这样的结构。

跳跃表是怎么实现的?

①、

跳跃表节点的 level 数组可以包含多个元素,每个元素都包含一个指向其它节点的指针,程序可以通过这些层来加快访问其它节点的速度,一般来说,层的数量月多,访问其它节点的速度就越快。

每次创建一个新的跳跃表节点的时候,程序都根据幂次定律,随机生成一个介于 1 和 32 之间的值作为 level 数组的大小,这个大小就是层的“高度”

②、前进指针

每个层都有一个指向表尾的前进指针(level[i].forward 属性),用于从表头向表尾方向访问节点。

我们看一下跳跃表从表头到表尾,遍历所有节点的路径:

通过前进指针遍历

③、跨度

层的跨度用于记录两个节点之间的距离。跨度是用来计算排位(rank)的:在查找某个节点的过程中,将沿途访问过的所有层的跨度累计起来,得到的结果就是目标节点在跳跃表中的排位。

例如查找,分值为 3.0、成员对象为 o3 的节点时,沿途经历的层:查找的过程只经过了一个层,并且层的跨度为 3,所以目标节点在跳跃表中的排位为 3。

计算节点的排位

④、分值和成员

节点的分值(score 属性)是一个 double 类型的浮点数,跳跃表中所有的节点都按分值从小到大来排序。

节点的成员对象(obj 属性)是一个指针,它指向一个字符串对象,而字符串对象则保存这一个 SDS 值。

为什么 hash 表范围查询效率比跳表低?

哈希表是一种基于键值对的数据结构,主要用于快速查找、插入和删除操作。

哈希表通过计算键的哈希值来确定值的存储位置,这使得它在单个元素的访问上非常高效,时间复杂度为 O(1)。

然而,哈希表内的元素是无序的。因此,对于范围查询(如查找所有在某个范围内的元素),哈希表无法直接支持,必须遍历整个表来检查哪些元素满足条件,这使得其在范围查询上的效率低下,时间复杂度为 O(n)。

跳表是一种有序的数据结构,能够保持元素的排序顺序。

它通过多层的链表结构实现快速的插入、删除和查找操作,其中每一层都是下一层的一个子集,并且元素在每一层都是有序的。

当进行范围查询时,跳表可以从最高层开始,快速定位到范围的起始点,然后沿着下一层继续直到找到范围的结束点。这种分层的结构使得跳表在进行范围查询时非常高效,时间复杂度为 O(log n) 加上范围内元素的数量。

48.压缩列表了解吗?

压缩列表是 Redis 为了节约内存 而使用的一种数据结构,是由一系列特殊编码的连续内存快组成的顺序型数据结构。

一个压缩列表可以包含任意多个节点(entry),每个节点可以保存一个字节数组或者一个整数值。

压缩列表由这么几部分组成:

  • zlbyttes:记录整个压缩列表占用的内存字节数
  • zltail:记录压缩列表表尾节点距离压缩列表的起始地址有多少字节
  • zllen:记录压缩列表包含的节点数量
  • entryX:列表节点
  • zlend:用于标记压缩列表的末端

压缩列表示例

49.快速列表 quicklist 了解吗?

Redis 早期版本存储 list 列表数据结构使用的是压缩列表 ziplist 和普通的双向链表 linkedlist,也就是说当元素少时使用 ziplist,当元素多时用 linkedlist。

但考虑到链表的附加空间相对较高,prev 和 next 指针就要占去 16 个字节(64 位操作系统占用 8 个字节),另外每个节点的内存都是单独分配,会家具内存的碎片化,影响内存管理效率。

后来 Redis 新版本(3.2)对列表数据结构进行了改造,使用 quicklist 代替了 ziplist 和 linkedlist,quicklist 是综合考虑了时间效率与空间效率引入的新型数据结构。

quicklist 由 list 和 ziplist 结合而成,它是一个由 ziplist 充当节点的双向链表。

quicklist

50.假如 Redis 里面有 1 亿个 key,其中有 10w 个 key 是以某个固定的已知的前缀开头的,如何将它们全部找出来?

使用 keys 指令可以扫出指定模式的 key 列表。但是要注意 keys 指令会导致线程阻塞一段时间,线上服务会停顿,直到指令执行完毕,服务才能恢复。这个时候可以使用 scan 指令,scan 指令可以无阻塞的提取出指定模式的 key 列表,但是会有一定的重复概率,在客户端做一次去重就可以了,但是整体所花费的时间会比直接用 keys 指令长。

与 KEYS 命令不同,SCAN 命令是增量迭代的,不会一次性返回所有结果,从而避免了阻塞服务器的问题。

51.秒杀问题(错峰、削峰、前端、流量控制)

秒杀主要是指大量用户集中在短时间内对服务器进行访问,从而导致服务器负载剧增,可能出现系统响应缓慢甚至崩溃的情况。

解决这一问题的关键就在于错峰削峰和限流。当然了,前端页面的静态化、按钮防抖也能够有效的减轻服务器的压力。

  • 页面静态化:将商品详情等页面静态化,使用 CDN 分发。
  • 按钮防抖:避免用户因频繁点击造成的额外请求,比如设定间隔时间后才能再次点击。

如何实现错峰削峰呢?

在秒杀场景下,可以通过以下几种方式实现错峰削峰:

①、预热缓存:提前将热点数据加载到 Redis 缓存中,减少对数据库的访问压力。

②、消息队列:引入消息队列,将请求异步处理,减少瞬时请求压力。消息队列就像一个水库,可以削减上游的洪峰流量。

排队

③、多阶段多时间窗口:将秒杀活动分为多个阶段,每个阶段设置不同的时间窗口,让用户在不同的时间段内参与秒杀活动。

④、插入答题系统:在秒杀活动中加入答题环节,只有答对题目的用户才能参与秒杀活动,这样可以减少无效请求。

答题

如何限流呢?

采用令牌桶算法

在实际开发中,我们需要维护一个容器,按照固定的速率往容器中放令牌(token),当请求到来时,从容器中取出一个令牌,如果容器中没有令牌,则拒绝请求。

令牌桶

第一步,使用 Redis 初始化令牌桶:

redis-cli SET "token_bucket" "100"

第二步,使用 Lua 脚本实现令牌桶算法;假设每秒向桶中添加 10 个令牌,但不超过桶的最大容量。

-- Lua 脚本来添加令牌,并确保不超过最大容量
local bucket = KEYS[1]
local add_count = tonumber(ARGV[1])
local max_tokens = tonumber(ARGV[2])
local current = tonumber(redis.call('GET', bucket) or 0)
local new_count = math.min(current + add_count, max_tokens)
redis.call('SET', bucket, tostring(new_count))
return new_count

第三步,使用 Shell 脚本调用 Lua 脚本:

// !/bin/bash
while true; do
    redis-cli EVAL "$(cat add_tokens.lua)" 1 token_bucket 10 100
    sleep 1
done

第四步,当请求到达时,需要检查并消耗一个令牌。

-- Lua 脚本来消耗一个令牌
local bucket = KEYS[1]
local tokens = tonumber(redis.call('GET', bucket) or 0)
if tokens > 0 then
    redis.call('DECR', bucket)
    return 1  -- 成功消耗令牌
else
    return 0  -- 令牌不足
end

调用 Lua 脚本:

redis-cli EVAL "$(cat consume_token.lua)" 1 token_bucket