Redis知识复习笔记(上)

**

**

Redis

一、基础

1 说说什么是 Redis?【*】

Redis是一种基于键值对的NoSQl数据库。

特点:

数据存储:它主要的特点是把数据放在内存当中,相比直接访问磁盘的关系型数据库,读写速度会快很多,基本上能达到微秒级的响应。

应用:

高性能场景:所以在一些对性能要求很高的场景,比如缓存热点数据、防止接口爆刷,都会用到 Redis。

持久化:不仅如此,Redis 还支持持久化,可以将内存中的数据异步落盘,以便服务宕机重启后能恢复数据

(1)Redis 和 MySQL 的区别?

Redis是非关系数据库,Mysql是关系型数据库

Redis 属于非关系型数据库,数据是通过键值对的形式放在内存当中的;MySQL 属于关系型数据库,数据以行和列的形式存储在磁盘当中。

实际开发二者配合使用:实际开发中,会将 MySQL 作为主存储,Redis 作为缓存,通过先查 Redis,未命中再查 MySQL 并写回Redis 的方式来提高系统的整体性能。

(2)项目里哪里用到了 Redis?

拼团项目中自定义注解基于Redis实现了动态数据修改、基于此实现白名单、切量控制、活跃用户等

有很多地方都用到了 Redis,比如说用户活跃排行榜用到了 zset,作者白名单用到了 set。

还有用户登录后的 Session、站点地图 SiteMap,分别用到了 Redis 的字符串和哈希表两种数据类型。

其中比较有挑战性的一个应用是,通过 Lua 脚本封装 Redis 的 setnex 命令来实现分布式锁,以保证在高并发场景下,热点文章在短时间内的高频访问不会击穿 MySQL。

(3)部署过 Redis 吗?

使用过两种方式部署Redis:

1.本地单机:本地部署过单机版,下载 Redis 的安装包,解压后运行 redis-server 命令即可。

2.云服务器等Linux环境下:我有使用 Docker 拉取 Redis 镜像后进行容器化部署。

java 复制代码
docker run -d --name redis -p 6379:6379 redis:7.0-alpine

(4)Redis 的高可用方案有部署过吗?

哨兵机制。

(5)用过哪些缓存数据库,除redis以外?

使用过谷歌推出的Guava Cache可以作为本地缓存。Guava Cache 适合小规模缓存。

其他的还有Caffeine,Caffeine 性能更好,支持更多高级特性。Caffeine 通常用来作为二级缓存来使用,主要用于存储一些不经常变动的数据,以减轻 Redis 的压力。

2 Redis 可以用来干什么?

1.主要用来做缓存。比如说把高频访问的文章详情、商品信息、用户信息放入 Redis 当中,并通过设置过期时间来保证数据一致性,这样就可以减轻数据库的访问压力。

2.实现TOPN榜单的功能:使用Redis的ZSet,Redis 的 Zset 还可以用来实现积分榜、热搜榜,通过 score 字段进行排序,然后取前 N 个元素,就能实现 TOPN 的榜单功能。

3.实现分布式锁:利用 Redis 的 SETNX 命令或者 Redisson 还可以实现分布式锁,**确保同一时间只有一个节点可以持有锁,**为了防止出现死锁,可以给锁设置一个超时时间,到期后自动释放;并且最好开启一个监听线程,当任务尚未完成时给锁自动续期。

4.Lua脚本:如果是秒杀接口,还可以使用 Lua 脚本来实现令牌桶算法,限制每秒只能处理 N 个请求。

lua 复制代码
-- KEYS[1]: 令牌桶的唯一标识(例如:rate_limit:user:1001)
-- ARGV[1]: 桶的最大容量(Capacity),即突发流量的最大值
-- ARGV[2]: 令牌生成速率(Rate),每秒生成多少个令牌
-- ARGV[3]: 当前时间戳(秒),通常由应用层传入,避免Redis时钟不同步问题

-- 1. 获取当前桶的状态
-- 使用 HMGET 获取当前桶中的 'tokens' (剩余令牌数) 和 'timestamp' (上次刷新时间)
local bucket = redis.call('HMGET', KEYS[1], 'tokens', 'timestamp')

-- 2. 初始化变量
-- 如果 bucket[1] 为空(第一次访问),则默认令牌数为桶容量 ARGV[1](让第一次请求能通过)
local tokens = tonumber(bucket[1]) or ARGV[1]
-- 如果 bucket[2] 为空,则默认上次时间为当前时间 ARGV[3]
local last_time = tonumber(bucket[2]) or ARGV[3]

-- 将参数转为数字类型,方便计算
local rate = tonumber(ARGV[2])
local capacity = tonumber(ARGV[1])
local now = tonumber(ARGV[3])

-- 3. 【核心逻辑】计算并补充令牌(惰性计算)
-- 计算时间间隔:当前时间 - 上次更新时间
-- math.max(0, ...) 是为了防止时钟回拨导致出现负数
local delta = math.max(0, now - last_time)

-- 计算这段时间内应该生成的令牌数 = 时间间隔 * 速率
local add_tokens = delta * rate

-- 更新当前令牌数:不能超过桶的容量 (capacity)
tokens = math.min(capacity, tokens + add_tokens)

-- 更新刷新时间为当前时间
last_time = now

-- 4. 判断是否允许通过(消费令牌)
local allowed = 0 -- 默认为 0 (被限流)
if tokens >= 1 then
    -- 如果令牌数 >= 1,说明足够消费
    tokens = tokens - 1 -- 扣减一个令牌
    allowed = 1         -- 设置为 1 (通过)
end

-- 5. 保存状态回 Redis
-- 将更新后的令牌数和时间戳写回 Hash 结构
redis.call('HMSET', KEYS[1], 'tokens', tokens, 'timestamp', last_time)

-- 6. 设置过期时间
-- 给这个 Key 设置 3600 秒过期,防止长时间不活跃的 Key 占用 Redis 内存
redis.call('EXPIRE', KEYS[1], 3600)

-- 7. 返回结果
-- 返回 1 表示通过,0 表示限流
return allowed

在 Java 中调用 Lua 脚本:

java 复制代码
// 令牌桶参数
int capacity = 10; // 桶容量
int rate = 2;      // 每秒2个令牌
long now = System.currentTimeMillis() / 1000;
String key = "token_bucket:user:123";

// 调用 Lua 脚本,返回 1 表示通过,0 表示被限流
Long allowed = (Long) redis.eval(luaScript, 1, key, String.valueOf(capacity), String.valueOf(rate), String.valueOf(now));

(1)redis做缓存要考虑哪些问题,在业务方面呢

一类是经典的缓存系统设计问题(穿透、击穿、雪崩) ,另一类是与业务逻辑紧密相关的业务缓存问题(数据一致性、缓存粒度等)。

当修改了数据库的数据后,如何保证缓存里的数据也同步更新?如果处理不好,用户就会看到"脏数据"。

另外就是我们应该缓存一个完整的、包含各种关联信息的复杂对象,还是只缓存那些最常用的基础字段?

3 Redis有哪些数据类型?【*】

有5种基本数据类型:字符串、列表、哈希、集合、有序集合。3种扩展的数据类型:用于位操作的Bitmap、用于基数估计的HyperLogLog、支持存储和查询地理坐标的GEO。(这里说的类型就是键值对中值Value的类型,其中key的类型永远都是String)

(1)字符串String

字符串是最基本的数据类型,可以存储文本、数字或者二进制数据,最大容量是 512 MB。 对应,设置key,设置value为字符串

适合缓存单个对象,比如验证码、token、计数器等。

String情况:

最基本的数据类型,二进制安全(Binary Safe),意味着它可以包含任何数据(如图片、序列化对象)。

  • 结构:Key-Value。
  • 最大限制:512MB。
  • 常用命令
  • SET key value / GET key
  • INCR key / DECR key (自增/自减,原子操作)
  • SETNX key value (仅当 key 不存在时设置,用于分布式锁)
  • MGET k1 k2 (批量获取)
  • 应用场景
  • 缓存:存储 JSON、HTML 片段。
  • 计数器:点赞数、访问量(利用 INCR 的原子性)。
  • 分布式锁:利用 SETNX 实现。
  • 共享 Session:Spring Session Redis。

(2)列表List

列表是一个有序的元素集合 ,支持从头部或尾部插入/删除元素 ,常用于消息队列或任务列表

List列表

底层是双向链表(Doubly Linked List)。意味着头部或尾部插入/删除极快,但随机访问(通过索引访问)较慢。

  • 结构:Key -> [ v1, v2, v3 ... ] (有序,可重复)
  • 常用命令
  • LPUSH key value / RPUSH key value (左/右插入)
  • LPOP key / RPOP key (左/右弹出)
  • LRANGE key start stop (查看范围)
  • 应用场景
  • 消息队列:LPUSH 生产消息,RPOP 消费消息。
  • 最新列表:比如朋友圈的时间线(Timeline),新消息总是插入头部。

(3)哈希Hash

哈希是一个键值对集合,适合存储对象,如商品信息、用户信息 等。比如说 value = {name: '沉默王二', age: 18}

Hash哈希

类似 Java 的 HashMap 或 Python 的 dict。是一个 String 类型的 Field 和 Value 的映射表。

  • 结构:Key -> { Field1: Value1, Field2: Value2 ... }
  • 优势:适合存储对象,比 String 更节省内存(特定条件下),且方便修改对象的某个字段。
  • 常用命令
  • HSET key field value / HGET key field
  • HMGET key f1 f2 (批量获取字段,你刚才的 Lua 脚本就用的这个)
  • HGETALL key (获取所有键值)
  • HINCRBY key field increment (给某个字段累加)
  • 应用场景
  • 存储对象:用户信息(ID为Key,Name/Age/Email为Field)。
  • 购物车:用户ID为Key,商品ID为Field,数量为Value。

(4)集合Set

集合是无序且不重复的 ,支持交集、并集操作,查询效率能达到 O(1) 级别,主要用于去重、标签、共同好友等场景。

String 类型的无序集合。自动去重 ,且提供了集合运算(交集、并集、差集)

  • 结构:Key -> { v1, v2, v3 } (无序,不重复)
  • 常用命令
  • SADD key member (添加)
  • SREM key member (删除)
  • SMEMBERS key (查看所有)
  • SISMEMBER key member (判断是否存在)
  • SINTER k1 k2 (求交集)
  • 应用场景
  • 标签(Tag):给用户打标签。
  • 共同好友:利用 SINTER 计算两个用户集合的交集。
  • 抽奖:利用 SRANDMEMBER 随机抽取元素。
  • UV统计:统计网站每天的独立 IP(因为 Set 自动去重)。

(5)有序集合Sorted Set(ZSet)

有序集合的元素按分数进行排序 ,支持范围查询 ,适用于排行榜或优先级队列

Redis 最有特色的数据结构。它和 Set 一样不可重复,但每个元素都会关联一个 double 类型的分数(Score)。Redis 根据分数对元素进行从小到大的排序。

  • 结构:Key -> { (Score1, Member1), (Score2, Member2) ... }
  • 常用命令
  • ZADD key score member
  • ZRANGE key start stop (按分数从小到大排)
  • ZREVRANGE key start stop (从大到小排)
  • ZRANK key member (获取排名)
  • 应用场景
  • 排行榜:游戏积分榜、热搜榜(Score 是积分/热度)。
  • 延迟队列:Score 存储执行时间戳,消费者轮询获取 Score <= 当前时间的任务。
  • 带权重的消息:高优先级的消息 Score 设置得更高。

(6)Bitmap位图

Bitmap 可以把一组二进制位紧凑地存储在一块连续内存中 ,每一位代表一个对象的状态,比如是否签到、是否活跃 等。比如用户 0 的已签到 1、用户 1 未签到 0、用户 2 已签到,Redis 就会把这些状态放进一个连续的二进制串 101,1 亿用户签到仅需 100,000,000 / 8 / 1024 ≈ 12MB 的空间,真的省到离谱。(非常节省空间)

bitmap**"用一个 bit(0 或 1)来标记状态"**

核心原理

Bitmap 不是一种新的数据结构,它本质上就是 String

Redis 的 String 最大支持 512MB,而一个字节有 8 个 bit。所以,你可以把 String 看作是一个巨大的位数组(Bit Array),最大长度可达232232个 bit(约 40 亿)。

常用命令

  • SETBIT key offset value:设置第 offset 位的值(0 或 1)。
  • GETBIT key offset:获取第 offset 位的值。
  • BITCOUNT key:统计有多少个 1。
  • BITOP AND/OR/XOR dest key1 key2:对两个 Bitmap 做位运算。

经典场景:用户签到 / 在线状态

假设你有 1 亿个用户,需要统计今天的登录情况。

  • 用 Set:存 1 亿个 ID(假设 ID 是 Long),需要几百 MB 甚至 G 级内存。
  • 用 Bitmap
  • key = login:20231024
  • 用户 ID 作为 offset(偏移量)。例如用户 ID 1001 登录了,就 SETBIT login:20231024 1001 1。
  • 占用空间 :1 亿个用户只需要10^8bits≈≈12MB。内存节省了几十倍!

(7)HyperLogLog基数统计

HyperLogLog 是一种用于基数统计的概率性数据结构,可以在仅有 12KB 的内存空间下,统计海量数据集中不重复元素的个数,误差率仅 0.81%。(用于基数统计,一下图片简单示例)

底层基于 LogLog 算法改进 ,先把每个元素哈希成一个二进制串,然后取前 14 位进行分组放到 16384 个桶中记录每组最大的前导零数量,最后用一个近似公式推算出总体的基数。

示例:

举个超简单的例子,假设有一个神奇的哈希函数,可以把元素散列成一个二进制数,比如:

元素 哈希值 前导零个数
userA 000100101... 3
userB 001010011... 2
userC 000000101... 6

可以发现,哈希值越长前导零越多,也就说明集合里的元素越多。

大型网站 UV 统计系统示例:(UV就是User Visit 用户访问量的统计)

java 复制代码
// UV统计的简单示例
public class UVCounter {
    private Jedis jedis;
    
    public void recordVisit(String date, String userId) {
        String key = "uv:" + date;
        jedis.pfadd(key, userId);
    }
    
    public long getUV(String date) {
        return jedis.pfcount("uv:" + date);
    }
    
    public long getUVBetween(String startDate, String endDate) {
        List<String> keys = getDateKeys(startDate, endDate);
        return jedis.pfcount(keys.toArray(new String[0]));
    }
}

HyperLogLog基数统计

"在允许极小误差的情况下,统计海量去重数据"

核心原理

它是为了解决 基数统计(Cardinality Counting) 问题的。也就是统计"集合中不重复元素的个数"。

它的神奇之处在于:不管你存入多少数据(几千或几亿),它占用的内存是固定的,仅需 12KB!

  • 代价 :它不是 100% 精确的,有 0.81% 的标准误差。

常用命令

  • PFADD key element:添加元素(类似 SADD)。
  • PFCOUNT key:统计不重复元素的个数(类似 SCARD)。
  • PFMERGE dest key1 key2:合并多个 HLL。

经典场景:百万级 UV 统计

你需要统计网站每天的 UV(独立访客)

  • 用 Set:如果有 1000 万 UV,内存消耗巨大。
  • 用 HyperLogLog:仅需 12KB。对于 UV 统计来说,0.81% 的误差(1000万里的8万误差)通常是完全可以接受的。

(8)GEO地理位置

GEO 用于存储和查询地理位置信息 ,可以用来计算两点之间的距离,查找某位置半径内的其他元素

常见的应用场景包括:附近的人或者商家、计算外卖员和商家的距离、判断用户是否进入某个区域等。

底层基于 ZSet 实现,通过 Geohash 算法把经纬度编码成 score。

比如说查询附近的商家时,Redis 会根据中心点经纬度反推可能的 Geohash 范围, 在 ZSet 上做范围查询,拿到候选点后,用 Haversine 公式精确计算球面距离,筛选出最终符合要求的位置。

java 复制代码
// GEO的使用示例
public class NearbyShopService {
    private Jedis jedis;
    private static final String SHOP_KEY = "shops:geo";
    
    // 添加商铺
    public void addShop(String shopId, double longitude, double latitude) {
        jedis.geoadd(SHOP_KEY, longitude, latitude, shopId);
    }
    
    // 查询附近的商铺
    public List<GeoRadiusResponse> getNearbyShops(
            double longitude, 
            double latitude, 
            double radiusKm) {
        return jedis.georadius(SHOP_KEY, 
                             longitude, 
                             latitude, 
                             radiusKm, 
                             GeoUnit.KM, 
                             GeoRadiusParam.geoRadiusParam()
                                         .withCoord()
                                         .withDist()
                                         .sortAscending()
                                         .count(20));
    }
    
    // 计算两个商铺之间的距离
    public double getShopDistance(String shop1Id, String shop2Id) {
        return jedis.geodist(SHOP_KEY, 
                           shop1Id, 
                           shop2Id, 
                           GeoUnit.KILOMETERS);
    }
}

GEO地理位置

"存储经纬度,计算距离和附近的人"

核心原理

Redis 3.2 引入。底层其实是 ZSet(有序集合)

它利用 GeoHash 算法将二维的经纬度编码成一个 52 位的整数,作为 ZSet 的 Score(分值),元素本身是 Member。因为 ZSet 是有序的,所以可以很高效地查找"附近的点"。

常用命令

  • GEOADD key longitude latitude member:添加坐标。
  • GEOPOS key member:获取坐标。
  • GEODIST key m1 m2 unit:计算两个点之间的距离(支持 km, m, mi)。
  • GEORADIUS / GEOSEARCH:核心功能,搜索半径内有哪些成员(例如:查找我周围 5km 内的车辆)。

经典场景

  • 附近的人:微信/陌陌的"附近的人"功能。
  • 外卖配送:计算用户和商家的距离。
  • 网约车:查找附近的司机。

(9)为什么使用 hash 类型而不使用 string 类型序列化存储

Hash表的结构可以只读取或者修改某一个字段、但是String类型需要一次性把整个对象取出来,做更改再存进去。

比如说有一个用户对象 user = {name: '沉默王二', age: 18},如果使用 Hash 存储,可以直接修改 age 字段:

bash 复制代码
redis.hset("user:1", "age", 19);

如果使用 String 存储,需要先取出整个对象,修改后再存回去:

java 复制代码
String userJson = redis.get("user:1");
User user = JSON.parseObject(userJson, User.class);
user.setAge(19);
redis.set("user:1", JSON.toJSONString(user));

(10)Redis中的Key

Key(键) ,在 Redis 中永远都是 String(字符串) 类型,Key的开发最佳实践:

这是 Redis 中最通用的约定。使用冒号可以将 Key 像"目录"一样分层管理。

很多 Redis 可视化工具(如 Redis Desktop Manager, Another Redis Desktop Manager)会自动根据冒号将 Key 折叠成文件夹的形式,非常方便管理。

通用格式:

业务名:对象名:ID:属性

示例:

  • Bad: user1001name (看不懂,无法分层)
  • Good: app:user:1001:name
  • app:项目或系统名称(防止不同项目共用一个 Redis 实例时冲突)
  • user:模块或表名
  • 1001:主键 ID
  • name:具体字段

为了防止 Key 冲突(尤其是在微服务架构中),通常会加上服务前缀:

  • 订单服务:order-service:order:20230101
  • 支付服务:pay-service:payment:883712

总结

一个优秀的 Redis Key 应该是:

  1. 可读:user:1001:cart 而不是 u1c。
  2. 分层:用 : 隔开。
  3. 唯一:通过加入 ID 确保不重复。
  4. 紧凑:不要写成句子,单词尽量缩写(如 message -> msg, count -> cnt)。

4 Redis 为什么快呢?

三方面的原因:基于内存进行读写、IO多路复用、高效的数据结构

(1)基于内存的读写RAM

第一,Redis 的所有数据都放在内存中,而内存的读写速度本身就比磁盘快几个数量级。

(2)IO多路复用

第二,Redis 采用了基于 IO 多路复用技术的事件驱动模型来处理客户端请求和执行 Redis 命令。

其中的 IO 多路复用技术可以在只有一个线程的情况下,同时监听成千上万个客户端连接,解决传统 IO 模型中每个连接都需要一个独立线程带来的性能开销。

IO 多路复用会持续监听请求,然后把准备好的请求压入到一个队列当中,并将其有序地传递给文件事件分派器,最后由事件处理器来执行对应的 accept、read 和 write 请求

Redis 会根据操作系统选择最优的 IO 多路复用技术,比如 Linux 下使用 epoll,macOS 下使用 kqueue 等。

Redis 线程模型(Threading Model)的演进:6.0之前和6.0

Redis6.0之前

包括连接建立、请求读取、响应发送,以及命令执行都是在主线程中顺序执行的,这样可以避免多线程环境下的锁竞争和上下文切换,因为 Redis 的绝大部分操作都是在内存中进行的,性能瓶颈主要是内存操作和网络通信,而不是 CPU。(也就上单线程顺序执行)

Redis6.0

将网络IO剥离出来,为了进一步解决网络 IO 的性能瓶颈,Redis 6.0 引入了多线程机制,把网络 IO 和命令执行分开,网络 IO 交给线程池来处理,而命令执行仍然在主线程中进行,这样就可以充分利用多核 CPU 的性能。

  • 工作流程
    Redis 6.0 引入了 Redis IO Thread(IO 线程池)。虽然"命令执行"依然在主线程,但"脏活累活"(网络读取和发送)分给了其他线程去做。
  1. Read(多线程) :主线程利用 epoll 监听到有数据来了,把 Socket 分发给 IO 线程池。多个 IO 线程并行读取并解析数据
  2. Execute(单线程) :数据解析完后,主线程 依然亲自执行命令(操作内存)。
    • 注意:图中的紫色方块 c3 set, c2 get, c1 incr 依然是在主线程上串行执行的。
  3. Write(多线程) :主线程执行完拿到结果,再把结果交给 IO 线程池,多个 IO 线程并行将数据写回客户端
  • 核心优势
  • 打破瓶颈:利用了多核 CPU 的能力来处理网络数据的读写(这是最耗时的部分)。
  • 保持简单 :因为执行命令依然是单线程的,所以 Redis 依然不需要处理复杂的锁机制(Lock-free),不存在多线程并发修改数据的安全问题,保留了 Redis 原有的高性能和简单性。

这张图解释了 Redis 6.0 性能提升的关键原因:
将"网络 IO"(瓶颈)剥离给多线程处理,而"核心计算"(内存操作)依然保留单线程。

它回答了一个经典的面试题:"Redis 是单线程的吗?"

  • 准确回答 :Redis 的核心业务逻辑(命令执行)是单线程的 ,但在 Redis 6.0 之后,网络 IO 层变成了多线程

(3)高效的数据结构

第三,Redis 对底层数据结构做了极致的优化,比如说 String 的底层数据结构动态字符串支持动态扩容、预分配冗余空间,能够减少内存碎片和内存分配的开销。

5 能详细说一下IO多路复用吗?

IO多路复用技术就是允许当进程同时监控多个文件描述符(这是Linux\Unix中的说法,指的就是网络连接Socket )的技术,使得程序能够高效处理多个并发连接而无需创建大量线程。Redis 仅仅使用单线程,就能每秒处理数万次请求

  • 多路:指多个网络连接
  • 复用:只使用一个线程来监听网络连接

发生两次阻塞:阻塞在 select 上,等待"数据到达内核";阻塞在 read 上,等待"数据从内核拷贝到用户进程"。

IO 多路复用的核心思想是:让单个线程可以等待多个文件描述符就绪,然后对就绪的描述符进行操作。这样可以在不使用多线程或多进程的情况下处理并发连接。

具体的主要实现机制是select、poll、epoll(这3个是Linux/Unix的实现)、kqueue(MacOS实现)和IOCP(Windows)的实现。

1.用户态与内存态?为什么要把文件描述符从从用户态拷贝进内存态?

操作系统(OS)为了系统的安全和稳定,把内存划分为两个区域,并定义了两种运行级别:内核态和用户态

内核态:权限最高,只有内核态才能直接操控硬件。

用户态:权限较低。我们平时跑的程序(Redis, Nginx, Chrome, Python脚本)都运行在这里。

当你的 Redis 需要从网卡读取数据(Read)时,它自己是没权限的。

  1. 切换:Redis(用户态)发起一个"系统调用"(System Call),请求操作系统帮忙。
  2. 执行 :CPU 暂停用户态代码,切换到内核态,由内核驱动去网卡拿数据,把数据从硬件缓冲区拷贝到内存。
  3. 返回 :内核拿完数据,再切换回用户态,把数据交给 Redis。

关键点从"用户态"切换到"内核态"(Context Switch)是有开销的,虽然很快,但在高并发下,频繁切换会消耗大量 CPU 资源。

2.解释下为什么要说事件描述符?

在 Linux/Unix 系统中,一切皆文件

当你用 Redis 连接到一个客户端时,操作系统并不把这个连接看作什么神圣的"网络通道",它觉得这和读写一个硬盘上的文本文件没啥区别:

  • 读文件 = 从网络接收数据
  • 写文件 = 向网络发送数据
  • 关闭文件 = 断开连接

所以,既然网络连接被当成文件处理,那么标识它的 ID 自然就叫"文件"描述符。

(1)请说说 select、poll、epoll、kqueue 和 IOCP 的区别?

  • select

select 的缺点是单个进程能监视的文件描述符数量有限,一般为 1024 个 ,且每次调用都需要将文件描述符集合从用户态复制到内核态,然后遍历找出就绪的描述符,性能较差。

select:拷贝->内核遍历->拷贝回->用户再遍历

  • 实现机制
  1. 位图(Bitmap):select 使用一个固定长度的位图(fd_set)来表示要监视的文件描述符。
  2. 拷贝 :调用 select 时,需要把这个位图从用户态拷贝到内核态
  3. 轮询(Polling) :内核遍历这 1024个bit,检查对应的 Socket 是否有数据。如果有,修改位图状态
  4. 再拷贝:内核把修改后的位图拷贝回用户态。
  5. 再遍历 :用户态代码不知道具体是哪个 Socket 有数据,只能再遍历一遍位图,找到被标记的 FD。
  • 缺点
  • 数量限制 :默认只能监视 1024 个连接(由 FD_SETSIZE 宏决定,改这个需要重新编译内核)。
  • 性能开销:两次内存拷贝(用户<->内核),两次 O(N) 遍历。并发越高,性能越差。
  • poll

poll 的优点是没有最大文件描述符数量的限制,但是每次调用仍然需要将文件描述符集合从用户态复制到内核态(缺点),依然需要遍历,性能仍然较差。(比较select只是没有数量限制了)

  • 实现机制
  • 它不再使用固定长度的位图,而是使用一个 动态数组(链表) (struct pollfd) 来存储文件描述符。
  • 除此之外,流程和 select 几乎一样:拷贝 -> 内核遍历 -> 拷贝回 -> 用户遍历
  • 优点
  • 解决了 1024 连接数的限制(只要内存够,想连多少连多少)。
  • 缺点
  • 性能痛点依旧:依然是 O(N) 线性遍历。如果你监视 10 万个连接,只有 1 个活跃,内核和用户程序依然要扫描完这 10 万个元素才能找到那 1 个。
  • epoll

epoll是Linux特有的IO多路复用机制 ,支持大规模并发连接,使用事件驱动模型,性能更高。其工作原理是将文件描述符注册到内核中,然后通过事件通知机制来处理就绪的文件描述符,不需要轮询,也不需要数据拷贝,更没有数量限制,所以性能非常高。

  • 实现机制(三大核心)
  1. epoll_create :在内核中创建一个对象,底层由 红黑树(RB-Tree)双向链表(Ready List) 组成。
  2. epoll_ctl :程序告诉内核:"我要监视这个 Socket"。内核把它挂到红黑树上。
    • 优势:不需要像 select/poll 那样每次调用都把所有 Socket 传给内核,这里只传一次。
  3. epoll_wait :程序问内核:"谁有数据?"
    • 关键点(回调机制) :当网卡收到数据,通过中断触发回调,内核直接把该 Socket 从红黑树里拿出来,扔到"就绪链表"里
    • 返回:epoll_wait 只需要把这个"就绪链表"里的数据返回给用户即可。
  • kqueue

kqueue 是 BSD/macOS 系统下的 IO 多路复用机制,类似于 epoll,支持大规模并发连接,使用事件驱动模型。

FreeBSD、macOS、iOS 系统的实现,地位等同于 Linux 的 epoll。

  • 实现机制
  • 原理与 epoll 高度相似,也是基于事件驱动的 O(1) 复杂度。
  • 它注册的是 kevent 结构体。
  • 相比 epoll,kqueue 的 API 设计甚至更通用一些,不仅能监听 Socket,还能监听文件修改(类似 Linux 的 inotify)、进程信号等。
  • 总结:在 Unix 世界里,Linux 用 epoll,BSD 系(包括苹果)用 kqueue。Redis 源码里会通过宏定义自动选择。
  • IOCP

IOCP 是 Windows 系统下的 IO 多路复用机制,使用使用完成端口模型而非事件通知。

(2)举个例子说一下 IO 多路复用?

数学老师检查作业,保安看监控。

比如说我是一名数学老师,上课时提出了一个问题:"今天谁来证明一下勾股定律?"同学小王举手,我就让小王回答;小李举手,我就让小李回答;小张举手,我就让小张回答。这种模式就是 IO 多路复用,我只需要在讲台上等,谁举手谁回答,不需要一个一个去问。

Redis就是使用epoll这样的IO多路复用机制,在单线程模型下实现高效的网络 IO,从而支持高并发的请求处理。

(3)举例子说一下阻塞IO和IO多路复用的差别?

阻塞IO就是一个线程只能处理一个网络连接请求,依次处理,在没有处理完这个IO之前,但是其他请求就阻塞。

IO多路复用则是基于事件通知模型,监控等待多个IO请求,那个请求准备就绪就处理那个请求

阻塞IO

假设我是一名老师,让学生解答一道题目。

我的第一种选择:按顺序逐个检查,先检查 A同学,然后是 B,之后是 C、D。。。这中间如果有一个学生卡住,全班都会被耽误。

这种就是阻塞 IO,不具有并发能力。

IO多路复用

我的第二种选择,我站在讲台上等,谁举手我去检查谁。C、D 举手,我去检查 C、D 的答案,然后继续回到讲台上等。此时 E、A 又举手,然后去处理 E 和 A。

(4)select、poll 和 epoll 的实现原理?

select与poll都是将所有的文件描述符传递给内核,由内核遍历来判断那些文件描述符准备就绪。

1.select与poll

  • select使用位图Bitmaps将文件描述符传入内核之中,轮询所有文件描述符,通过调用file->poll函数查询是否由对应事件,没有就将 task 加入 FD 对应 file 的待唤醒队列,等待事件来临被唤醒,存在连接上限的问题1024
  • poll使用了动态数组pollfd,改进了连接上限数的问题。但本质上仍是线性遍历,性能没有提升太多。

select和poll的模式都是,一次将参数拷贝到内核空间,等有结果了再一次拷贝出去。

2.epoll

epoll是基于事件通知模型 的,epoll 将监听的 FD 注册进内核的红黑树 ,由内**核在事件触发时将就绪的 FD 放入 ready list。**应用程序通过 epoll_wait 获取就绪的 FD,从而避免遍历所有连接的开销。

epoll 最大的优点是:支持事件驱动 + 边缘触发,ADD 时拷贝一次,epoll_wait 时利用 MMAP 和用户共享空间,直接拷贝数据到用户空间,因此在高并发场景下性能远高于 select 和 poll。

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

三方面原因:避免锁开销和并发竞争问题、本身是IO密集型任务、保证命令执行原子性

第一,单线程模型不需要考虑复杂的锁机制,不存在多线程环境下的死锁、竞态条件等问题,开发起来更快,也更容易维护。

第二,Redis 是IO 密集型而非 CPU 密集型,主要受内存和网络 IO 限制,而非 CPU 的计算能力,单线程可以避免线程上下文切换的开销。

第三,单线程可以保证命令执行的原子性,无需额外的同步机制。

但是后期Redis 虽然最初采用了单线程设计,但后续的版本中也在特定方面引入了多线程,比如说 Redis 4.0 就引异步多线程,用于清理脏数据、释放无用连接、删除大 Key 等。

7 Redis 6.0 使用多线程是怎么回事?

命令执行还是单线程,只是网络IO变成了多线程,包括网络数据的读取、写入,以及请求解析。

复制代码
│ 单线程执行命令 │
                  │    ↑    ↓     │
┌─────────┐     ┌─┴────────────┴──┐
│ I/O线程1 │ ←→ │                 │
├─────────┤     │                 │
│ I/O线程2 │ ←→ │    主线程       │
├─────────┤     │                 │
│ I/O线程3 │ ←→ │                 │
└─────────┘     └─────────────────┘

而命令的执行依然是单线程,这种设计被称为"IO 线程化",能够在高负载的情况下,最大限度地提升 Redis 的响应速度。

8 说说 Redis 的常用命令

Redis 支持多种数据结构,常用的命令也比较多,比如说操作字符串可以用 SET/GET/INCR,操作哈希可以用 HSET/HGET/HGETALL,操作列表可以用 LPUSH/LPOP/LRANGE,操作集合可以用 SADD/SISMEMBER,操作有序集合可以用 ZADD/ZRANGE/ZINCRBY等,通用命令有 EXPIRE/DEL/KEYS 等。

(1)详细说说 set 命令?

SET 命令用于设置字符串的 key,支持过期时间和条件写入,常用于设置缓存、实现分布式锁、延长 Session 等场景。

bash 复制代码
SET key value [EX seconds | PX milliseconds | EXAT timestamp | PXAT timestamp-milliseconds | KEEPTTL] [NX | XX] [GET]

默认情况下,SET 会覆盖键已有的值。

支持多种设置过期时间的方式,比如说 EX 设置秒级过期时间,PX 设置毫秒过期时间。

支持条件写入,使其可以实现原子性操作,比如说 NX 仅在键不存在时设置值,XX 仅在键存在时设置值。

缓存实现:

复制代码
SET user:profile:{userid} {JSON数据} EX 3600  # 存储用户资料,并设置1小时过期

实现分布式锁:

复制代码
SET lock:resource_name {random_value} EX 10 NX  # 获取锁,10秒后自动释放

存储 Session:

复制代码
SET session:{sessionid} {session_data} EX 1800  # 存储用户会话,30分钟过期

(2)sadd 命令的时间复杂度是多少?

SADD 支持一次添加多个元素,返回值为实际添加成功的元素数量,时间复杂度为 O(N)

bash 复制代码
redis-cli SADD myset "apple" "banana" "orange"

(3)incr命令了解吗?

自增INCR 是一个原子命令,可以将指定键的值加 1,如果 key 不存在,会先将其设置为 0,再执行加 1 操作。

9 单线程的Redis QPS 能到多少?

根据官方的基准测试,一个普通服务器的 Redis 实例通常可以达到每秒十万左右的 QPS。

QPS

QPS (Queries Per Second) ,即 "每秒查询率"

它是衡量一个特定查询服务器(如 Redis, MySQL, Nginx)在规定时间内所处理流量多少的标准指标。

  • 通俗理解 :QPS 就是服务器的 "手速"
  • 计算公式:QPS = 总请求数 / 秒数
  • 例子:如果你的 Redis 在 1 秒钟内成功处理了 10,000 个 GET 或 SET 请求,那么当前的 QPS 就是 10,000。

QPS 越高,代表服务器的吞吐量(Throughput)越大,并发处理能力越强。

二、持久化

10 Redis的持久化方式有哪些?【*】

两种方式RDB(Redis DataBase,类似照相机)与AOF(Append Only File,类似记账本)。RDB通过创建时间点快照 的方式来持久化,AOF通过记录每一个写操作来持久化

两种方式可以单独使用也可以混合使用。保证Redis服务器重启的时候不丢失数据。这样就可以保证 Redis 服务器在重启后不丢失数据,通过 RDB 和 AOF 文件来恢复内存中原有的数据。

(1)详细说一下RDB?(记数据)

RDB 持久化机制可以在指定的时间间隔内将 Redis 某一时刻的数据保存到磁盘上的 RDB 文件中,当 Redis 重启时,可以通过加载这个 RDB 文件来恢复数据。

RDB持久化可以手动触发也可以自动触发,RDB 持久化可以通过 save 和 bgsave 命令手动触发也可以通过配置文件中的 save 指令自动触发。

  • save:阻塞主线程,直到保存完成(生产环境禁止使用,会卡死 Redis)。
  • bgsave:后台保存。Redis 会 fork 一个子进程在后台进行快照操作,主进程继续处理请求(这是默认方式)
  • 自动保存设置:在配置文件 redis.conf 中设置规则,例如:save 900 1(900秒内至少有1个key变化,就触发一次快照)

(2)什么情况下会自动触发RDB持久化?

三种情况回自动触发RDB持久化:在配置文件中设置了自动保存;主从复制;没有开启AOF时,执行shutdown命令。

  • 第一种,在 Redis 配置文件中设置 RDB 持久化参数 save <seconds> <changes>,表示在指定时间间隔内,如果有指定数量的键发生变化,就会自动触发 RDB 持久化。

    save 900 1 # 900 秒(15 分钟)内有 1 个 key 发生变化,触发快照
    save 300 10 # 300 秒(5 分钟)内有 10 个 key 发生变化,触发快照
    save 60 10000 # 60 秒内有 10000 个 key 发生变化,触发快照

  • 第二种,主从复制时,当从节点第一次连接到主节点时,主节点会自动执行 bgsave 生成 RDB 文件,并将其发送给从节点。

  • 第三种,如果没有开启 AOF,执行 shutdown 命令时,Redis 会自动保存一次 RDB 文件,以确保数据不会丢失。

(3)详细说一下AOF?(记命令)

AOF通过记录每一个写命令操作,并且将写命令追加到AOF文件来实现持久化,Redis宕机后就可以通过重新执行这些命令恢复数据。

当Redis执行写操作的时候,会将命令写入AOF缓冲区;Redis会根据同步刷盘策略将缓冲区的数据写入到AOF文件中。

当 AOF 文件过大时,Redis 会自动进行 AOF 重写,剔除多余的命令,比如说多次对同一个 key 的 set 和 del,生成一个新的 AOF 文件;当 Redis 重启时,读取 AOF 文件中的命令并重新执行,以恢复数据。

(4)AOF的刷盘策略了解吗?

Redis 将 AOF 缓冲区的数据写入到 AOF 文件时,涉及两个系统调用:write和fsync

  • write 将数据写入到操作系统的缓冲区
  • fsync 将 OS 缓冲区的数据刷新到磁盘

这里的刷盘涉及到三种策略:always、everysec 和 no,这个比较像Mysql日志的刷盘机制

  • always:每次写命令执行完,立即调用 fsync 同步到磁盘,这样可以保证数据不丢失,但性能较差。
  • everysec:每秒调用一次 fsync ,将多条命令一次性同步到磁盘,性能较好,数据丢失的时间窗口为 1 秒
  • no:不主动调用 fsync ,由操作系统决定,性能最好,但数据丢失的时间窗口不确定,依赖于操作系统的缓存策略,可能会丢失大量数据。

可以通过配置文件中的 appendfsync 参数进行设置。

复制代码
appendfsync everysec  # 每秒 fsync 一次

(5)说说AOF的重写机制?

问题:由于 AOF 文件会随着写操作的增加而不断增长;为了解决这个问题, Redis 提供了重写机制来对 AOF 文件进行压缩和优化。(根据结果去压缩命令)

AOF重写同样也是两种方式触发:手动执行和文件配置自动重写参数

  • 手动执行命令BGREWRITEAOF(后台重写AOF)命令,适合需要立即见效AOF文件大小的场景。

  • 在 Redis 配置文件中设置自动重写参数,比如说 auto-aof-rewrite-percentageauto-aof-rewrite-min-size,表示当 AOF 文件大小超过指定值时,自动触发重写。

    auto-aof-rewrite-percentage 100 # 默认值100,表示当前AOF文件大小相比上次重写后大小增长了多少百分比时触发重写
    auto-aof-rewrite-min-size 64mb # 默认值64MB,表示AOF文件至少要达到这个大小才会考虑重写

(6)AOF 重写的具体过程是怎样的?

他是后台重写AOF,不会阻塞主线程。Redis 在收到重写指令后,会创建一个子进程 ,并 fork 一份与父进程完全相同的数据副本,然后遍历内存中的所有键值对,生成重建它们所需的最少命令。

重写示例:

  • 比如说多个 RPUSH 命令可以合并为一个带有多个参数的 RPUSH;
  • 比如说一个键被设置后又被删除,这个键的所有操作都不会被写入新 AOF。
  • 比如说使用 SADD key member1 member2 member3 代替多个单独的 SADD key memberX

因为是后台重写,主线程不会阻塞,主线程可以继续处理来自客户端的命令。子进程在执行 AOF 重写的同时,主进程可以继续处理来自客户端的命令。

数据一致性的保障:

为了保证数据一致性,Redis 使用了 AOF 重写缓冲区机制,主进程在执行写操作时,会将命令同时写入旧的 AOF 文件和重写缓冲区。

等子进程完成重写后,会向主进程发送一个信号,主进程收到后将重写缓冲区中的命令追加到新的 AOF 文件中,然后调用操作系统的 rename,将旧的 AOF 文件替换为新的 AOF 文件。(重写完aof文件后,发信号给主线程重命名替换)。

OF 重写期间,Redis 服务器会处于特殊状态:

  • aof_child_pid 不为 0,表示有子进程在执行 AOF 重写
  • aof_rewrite_buf_blocks 链表不为空,存储 AOF 重写缓冲区内容

如果在配置文件中设置 no-appendfsync-on-rewrite 为 yes,那么重写期间可能会暂停 AOF 文件的 fsync 操作。

复制代码
appendonly yes                # 开启AOF
appendfilename "appendonly.aof"  # AOF文件名
appendfsync everysec          # 写入磁盘策略
no-appendfsync-on-rewrite no  # 重写期间是否临时关闭fsync
auto-aof-rewrite-percentage 100   # AOF文件增长到原来多少百分比时触发重写
auto-aof-rewrite-min-size 64mb    # AOF文件最小多大时才允许重写

(7)AOF 文件存储的是什么类型的数据?

AOF存储的Redis服务器接收到的写命令数据,以Redis协议格式进行保存。

这种格式的特点是,每个命令以*开头,后跟参数的数量,每个参数前用$符号,后跟参数字节长度,然后是参数的实际内容。

(8)AOF重写期间命令可能会写入两次,会造成什么影响?

不会造成数据重复和数据不一致问题,因为新旧文件是分离的。AOF 重写期间命令会同时写入现有AOF文件和重写缓冲区

因为新旧文件是分离的,现有命令写入当前 AOF 文件,重写缓冲区的命令最终写入新的 AOF 文件,完成后,新文件通过原子性的 rename 操作替换旧文件。两个文件是完全分离的,不会导致同一个 AOF 文件中出现重复命令。

11 RDB 和 AOF 各自有什么优缺点?

RDB 通过 fork 子进程在特定时间点对内存数据进行全量备份,生成二进制格式的快照文件。其最大优势在于备份恢复效率高,文件紧凑,恢复速度快,适合大规模数据的备份和迁移场景。缺点是可能丢失两次快照期间的所有数据变更。

RDB

优点

  1. 恢复速度极快:RDB 文件是紧凑的二进制文件,Redis 重启时直接加载到内存,速度非常快。
  2. 文件体积小:适合做冷备份(比如每天备份一次传到亚马逊 S3)。
  3. 性能最大化:主进程只需要 fork 一个子进程,之后的 IO 操作全由子进程处理,主进程不进行磁盘 IO。

缺点

  1. 数据安全性较低(会丢数据) :因为是每隔一段时间(比如 5 分钟)拍一张照,如果 Redis 在两张照片之间宕机,这 5 分钟内的数据就全丢了
  2. 资源消耗大:fork 子进程虽然是写时复制(Copy-on-Write),但在数据量巨大(几十 GB)时,fork 操作本身会阻塞主线程几百毫秒,甚至导致 CPU 飙升。

AOF 会记录每一条修改数据的写命令。这种日志追加的方式让 AOF 能够提供接近实时的数据备份,数据丢失风险可以控制在 1 秒内甚至完全避免。缺点是文件体积较大,恢复速度慢。

AOF

优点

  1. 数据更安全:默认每秒同步一次,最多只丢 1 秒数据。
  2. 可读性强:AOF 文件是纯文本(RESP 协议),如果手滑执行了 FLUSHALL(清空数据库),你可以赶紧关机,把 AOF 文件最后那行 FLUSHALL 删掉,重启就能恢复。

缺点

  1. 文件体积大:同等数据量下,AOF 文件通常比 RDB 大得多。
  2. 恢复速度慢:因为要一条条重放命令,重启速度远慢于 RDB。
  3. 性能稍弱:开启 AOF 后,QPS 会比纯 RDB 模式稍微低一些(因为要频繁写磁盘)
对比项 RDB(快照) AOF(命令日志)
数据完整性 ❌ 可能丢失几分钟数据 ✅ 最多丢 1 秒数据
恢复速度 ✅ 快(直接加载二进制快照) ❌ 慢(逐条 replay)
文件大小 ✅ 小(压缩后) ❌ 大(命令追加)
性能影响 ✅ 低(fork 后保存) ❌ 较高(每次写都记录)
写入方式 定期全量写 每次写命令就记录
适用场景 冷备份,灾难恢复 实时持久化,数据安全
默认状态 默认启用 Redis 7 默认也启用
重写机制 有(BGREWRITEAOF)
混合支持 Redis 4.0 后支持结合使用(aof-use-rdb-preamble)

12 RDB 和 AOF 如何选择?

RDB适合大规模备份和恢复;AOF适合实时备份。在选择 Redis 持久化方案时,我会从业务需求和技术特性两个维度来考虑。

RDB使用场景

可以接受一定程度的数据丢失场景,做缓存的时候就可以使用RDB甚至都不使用。这种追求性能,丢失不敏感

AOF使用场景

数据丢失敏感的场景选择AOF,但如果是处理订单或者支付这样的核心业务,数据丢失将造成严重后果,那么 AOF 就成为必然选择。通过配置每秒同步一次,可以将潜在的数据丢失风险限制在可接受范围内。

实际项目中应该使用RDB与AOF的混合模式

bash 复制代码
appendonly yes # 开启 AOF
appendfsync everysec # 每秒刷盘一次
aof-use-rdb-preamble yes # 开启混合持久化,重启时优先加载 RDB,RDB 作为冷备,AOF 作为实时同步

13 Redis如何恢复数据?

大体上来说先AOF再RDB,流程如下图

当 Redis 服务重启时,它会优先查找 AOF 文件,如果存在就通过重放其中的命令来恢复数据;如果不存在或未启用 AOF,则会尝试加载 RDB 文件,直接将二进制数据载入内存来恢复。

检查并恢复AOF文件 :如果 AOF 文件损坏的话,Redis 会尝试通过 redis-check-aof 工具来修复 AOF 文件,或者直接使用 --repair 参数来修复。

bash 复制代码
redis-check-aof --repair appendonly.aof

检查RDB文件完整性 :虽然 Redis 还提供了 redis-check-rdb 工具来检查 RDB 文件的完整性,但它并不支持修复 RDB 文件,只能用来验证文件的完整性。

bash 复制代码
redis-check-rdb dump.rdb

14 Redis 4.0 的混合持久化了解吗?【*】

RDB和AOF混合持久化结合了二者的优点。混合持久化结合了 RDB 和 AOF 两种方式的优点,解决了它们各自的不足。在 Redis 4.0 之前,我们要么面临 RDB 可能丢失数据的风险,要么承受 AOF 恢复慢的问题,很难两全其美。

混合持久化的工作原理非常巧妙:在 AOF 重写期间,先以 RDB 格式将内存中的数据快照保存到 AOF 文件的开头,再将重写期间的命令以 AOF 格式追加到文件末尾。

这样,当需要恢复数据时,Redis 先加载 RDB 格式的数据来快速恢复大部分的数据,然后通过重放命令恢复最近的数据,这样就能在保证数据完整性的同时,提升恢复速度。(结合二者性能优势和安全优势)

(1)如何设置持久化模式?

启用混合持久化命令很简单,在配置文件中设置aof-use-rdb-preamble yes就可以了。

bash 复制代码
aof-use-rdb-preamble yes

(2)你在开发中是怎么配置 RDB 和 AOF 的?

大多数场景使用混合持久化,结合二者优点,配置文件如下:

bash 复制代码
# 启用AOF
appendonly yes

# 使用混合持久化
aof-use-rdb-preamble yes

# 每秒同步一次AOF,平衡性能和安全性
appendfsync everysec

# AOF重写触发条件:文件增长100%且至少达到64MB
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb

# RDB备份策略
save 900 1    # 15分钟内有1个修改
save 300 10   # 5分钟内有10个修改
save 60 10000 # 1分钟内有10000个修改

简单缓存场景,使用RDB

bash 复制代码
# 禁用AOF
appendonly no

# 较宽松的RDB策略
save 3600 1    # 1小时内有1个修改
save 300 100   # 5分钟内有100个修改

数据敏感的金融场景使用AOF的always

bash 复制代码
# 启用AOF
appendonly yes

# 使用混合持久化
aof-use-rdb-preamble yes

# 每个命令都同步(谨慎使用,性能影响大)
# 通常我会在关键时间窗口动态修改为always
appendfsync always

# 更频繁的RDB快照
save 300 1     # 5分钟内有1个修改
save 60 100    # 1分钟内有100个修改

另外,对于高并发场景,应该设置no-appendfsync-on-rewrite yes,避免 AOF 重写影响主进程性能;对于大型实例,也应该设置 rdb-save-incremental-fsync yes 来减少大型 RDB 保存对性能的影响。

bash 复制代码
# AOF重写期间不fsync,AOF 重写期间,主进程不会对新写入的 AOF 缓冲区执行 fsync 操作(即不强制刷盘),而是等重写结束后再统一刷盘。
no-appendfsync-on-rewrite yes
# RDB 快照保存时采用增量 fsync,即每写入一定量的数据就执行一次 fsync,将数据分批同步到磁盘。
rdb-save-incremental-fsync yes

三、高可用

什么是高可用?

即使系统出现了硬件故障、网络中断或软件崩溃,服务依然能保持"在线",让用户几乎感觉不到中断

高可用(HA) 不是一种具体的软件,而是一种系统设计目标

  • 它的敌人:单点故障。
  • 它的武器冗余 (多部署几台)、自动故障转移(自动切换)。
  • 在 Redis 中
  • 持久化 (RDB/AOF) 保证机器重启后数据还在。
  • 主从 + 哨兵 (Sentinel) 保证机器挂了后,服务还能自动恢复。

只有同时具备了"数据不丢"和"服务不断",才算是一个真正健壮的 Redis 生产环境。

15 主从复制了解吗?

主从复制允许从节点维护主节点的数据副本。在这种架构中,一个主节点可以连接多个从节点,从而形成一主多从的结构。

主节点负责写操作,从节点负责读,从节点会同步主节点的数据变更,实现读写分离。

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

读写分离、保证高可用

读写分离:第一,主节点负责处理写请求,从节点负责处理读请求,从而实现读写分离,减轻主节点压力的同时提升系统的并发能力。

保证高可用:第二,**从节点可以作为主节点的数据备份,当主节点发生故障时,可以快速将从节点提升为新的主节点,**从而保证系统的高可用性。

哨兵模式

Redis Sentinel(哨兵模式)

这是 Redis 官方推荐的最基础的高可用方案。

  1. 架构:1 个 Master + N 个 Slave + M 个 Sentinel(哨兵进程)。
  2. 工作流程
  • Sentinel 集群监控 Master。
  • Master 挂了。
  • Sentinel 投票,选出一个 Slave 变成新的 Master。
  • Sentinel 通知客户端(Java/Go 代码):地址变了,连新的这个 IP。
  1. 优点:实现了自动化故障恢复。

(2)什么情况下会出现主从复制数据不一致?

因为主从复制是异步的,所以当主线程写好数据后发生宕机、网络波动或者高延迟时,会出现主从节点数据不一致的情况

比如主节点写入数据后宕机,但从节点还未来得及复制,就会出现数据不一致。

另一个原因是主节点内存压力。当主节点的内存接近上限启用淘汰策列,某些键可能被自动删除,**而这些删除操作如果未能及时同步,就会造成从节点保留了主节点已经不存在的数据。**下图未能及时同步

(3)主从复制数据不一致的解决方案有哪些?

  • **网络层面的优化,**主从节点应该部署在同一个网络区域以内,避免跨区域的网络延迟。
  • 配置调整,比如说适当增大复制积压缓冲区的大小和存活时间,以便从节点重连后进行增量同步而不是全量同步,以最大程度减少主从同步的延迟。
  • 引入监控和自动修复机制,定期检查主从节点的数据一致性

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

三种:一主一从、一主多从、树状主从结构

(1)一主一从:主节点负责写入,从节点负责读和数据备份。这种结构虽然简单,但维护成本低。

(2)一主多从:随着业务增多,读请求增多,拓展一主多从的模式,主节点负责写入,多个从节点还可以分摊压力。

(3)在跨地域部署场景中,树**状主从结构可以有效降低主节点负载和需要传送给从节点的数据量。**通过引入复制中间层,从节点不仅可以复制主节点数据,同时可以作为其他从节点的主节点继续向下层复制。

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

Redis主从复制的原理:通过异步复制 主节点的数据变更同步到从节点,从而实现数据备份和读写分离

主要分三个步骤:建立连接、数据同步和传播命令

(1)建立连接

建立连接阶段,从节点执行命令replicaof命令连接到主节点 。连接建立之后,从节点向主节点发送psyn命令,请求数据同步。这时主节点会为该从节点创建一个连接和复制缓冲区。

(2)数据同步

同步数据阶段分为全量同步和增量同步 。当从节点首次连接主节点时,会触发全量同步

这个过程:(生成主节点的RDB文件传给从节点,生成期间的写命令也通过缓冲区传送

  • 主节点会 fork 一个子进程生成 RDB 文件,同时将文件生成期间收到的写命令缓存到复制缓冲区。
  • 然后将 RDB 文件发送给从节点,从节点清空自己的数据并加载这个 RDB 文件。
  • 等 RDB 传输完成后,主节点再将缓存的写命令发送给从节点执行,确保数据完全一致。

下图主从复制

(3)传播命令

第一次全量主从同步,之后主要依靠传播命令阶段来保持数据的增量同步。主节点会将每次执行的写命令实时发送给所有从节点。

在RDB文件传过去的过程中,主节点也会进行写操作,Redis 2.8 版本后,主节点会为每个从节点维护一个复制积压缓冲区,用于存储最近的写命令。

增量复制时,主节点会把要同步的写命令暂存一份到复制积压缓冲区。这样当从节点和主节点发生网络断连,从节点重新连接后,可以从复制积压缓冲区中复制尚未同步的写命令。

18 详细说说全量同步和增量同步?

(1)全量同步

全量同步会将主节点的完整数据集 传输给从节点,通常发生在从节点首次连接主节点时

具体流程如下:

  1. 此时,从节点发送 psync ? -1 命令请求同步。? 表示从节点没有主节点 ID,-1 表示没有偏移量。
  2. 主节点收到后会回复 FULLRESYNC响应从节点。同时也会包含主库 runid 和复制偏移量 offset 两个参数。
  3. 然后 fork 一个子进程生成 RDB 文件,并将新的写命令存入复制缓冲区。
  4. 从库收到 RDB 文件后,清空旧数据并加载新的 RDB 文件。
  5. 加载完成后,从节点会向主节点回复确认消息,主节点再将复制缓冲区中的数据发送给从节点,确保从节点的数据与主节点一致。
  6. 全量同步的代价很高,因为完整的 RDB 文件在生成时会占用大量的 CPU 和磁盘 IO;在网络传输时还会消耗掉不少带宽。

(2)增量同步

于是 Redis 在 2.8 版本后引入了增量同步的概念,目的是在断线重连后避免全量同步

增量同步主要依赖3个关键因素:

  • 复制偏移量:主从节点分别维护一个复制偏移量,记录传输的字节数。主节点每传输 N 个字节数据,自身的复制偏移量就会增加 N;从节点每收到 N 个字节数据,也会相应增加自己的偏移量。offset
  • 主节点 ID**:每个主节点都有一个唯一 ID,即复制 ID,用于标识主节点的数据版本**。当主节点发生重启或者角色变化时,ID 会改变。runid
  • 复制积压缓冲区:主节点维护的一个固定长度的先进先出队列,默认大小为 1M。**主节点在向从节点发送命令的同时,也会将命令写入这个缓冲区。**存储主节点的写命令

当从节点与主节点断开重连后,会发送 psync{runId}{offset} 命令,带上之前记录的主节点 ID 和复制偏移量。

当主节点收到psync {offset} {runid}会使用runid进行匹配:

  • 如果主节点 ID 与从节点提供的 runId 不匹配,说明主节点已经变化,必须进行全量同步。
  • 如果 ID 匹配,主节点会查找从节点请求的偏移量之后的数据是否还在复制积压缓冲区。
  • 如果在,只发送从该偏移量开始的增量数据,这就是增量同步;否则说明断线时间太长,积压缓冲区已经覆盖了这部分数据,需要全量同步。

psync {offset} {runid}中{offset} {runid}一半是什么样子

当一个从节点(Slave)因为网络抖动等原因与主节点(Master)断开连接,过了一会儿又重连上时,它不希望重新把所有数据拷贝一遍(全量复制,开销极大),而是希望 "只把断线期间缺失的数据补回来"。这个时候就要使用增量复制。

runid:主节点(Master)的唯一标识符 ,Master 启动时自动生成;重启会改变。当 Slave 断线重连时,它必须确认:"我现在连的这个 Master,还是刚才断线前那个 Master 吗?"

offset:这是 复制偏移量 ,代表数据同步的进度,每写入 N 个字节的数据,自己的 master_repl_offset 就加 N。当 Slave 重连时,它需要告诉 Master:**"咱们上次同步到哪里了?"**Slave 发送 PSYNC ,这里的 是 Slave 自己最后收到的那个偏移量 + 1(或者理解为它想要请求的起始位置)。

增量同步的优势显而易见:只传输断线期间的命令数据,大大减少了网络传输量和主从节点的负载,从节点也不需要清空重载数据,能更快地跟上主节点状态。

对于写入频繁或网络不稳定的环境,应该增大复制积压缓冲区的大小,确保短时间断线后能进行增量同步而不是全量同步。

bash 复制代码
repl-backlog-size 1mb  # 默认值 1MB,表示主节点的复制缓冲区大小
repl-backlog-ttl 3600  # 默认值 3600 秒,表示主节点的复制缓冲区存活时间

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

主要存在两个问题:数据不一致的风险和全量同步的资源占用巨大

  1. Redis异步进行主从复制,Redis 主从复制的最大挑战来自于它的异步特性主节点处理完写命令后会立即响应客户端,而不会等待从节点确认,这就导致在某些情况下可能出现数据不一致。

  2. 全量同步占用大量CPU和IO资源,另一个常见问题是全量同步对系统的冲击。全量同步会占用大量的 CPU 和 IO 资源,尤其是在大数据量的情况下,会导致主节点的性能下降。

(1)脑裂问题了解吗?

脑裂问题主要原因就是主节点和哨兵、从节点之间网络出现故障,但与客户端连接正常出现的问题,出现两个"主节点"同时对外提供服务。

哨兵认为主节点已经下线了,于是会将一个从节点选举为新的主节点。但原主节点并不知情,仍然在继续处理客户端的请求。

等主节点网络恢复正常了,发现已经有新的主节点了,于是原主节点会自动降级为从节点。在降级过程中,它(原主节点)需要与新主节点进行全量同步,此时原主节点的数据会被清空。导致客户端在原主节点故障期间写入的数据全部丢失。

(2)脑裂问题解决

为了防止脑裂问题的发生,会设定最小的数据同步从节点数。Redis 提供了 min-slaves-to-writemin-slaves-max-lag 参数。

bash 复制代码
# 设置主节点能进行数据同步的最少从节点数量
min-slaves-to-write 1
# 设置主从节点间进行数据同步时,从节点给主节点发送 ACK 消息的最大延迟(以秒为单位)
min-slaves-max-lag 10
  • min-slaves-to-write 如果主节点连接不到指定数量的从节点,主节点会拒绝写入请求。
  • min-slaves-max-lag 从节点响应超时,主节点会拒绝写入请求。

怎么解决?当网络故障发生,主节点与从节点、哨兵之间的连接断开,但主节点与客户端的连接正常时,由于主节点无法再连接到任何从节点,或者延迟超过了设定值,比如说配置了min-slaves-to-write 1,主节点就会自动拒绝所有写请求。

同时在网络的另一侧,哨兵会检测到主节点"下线",选举一个从节点成为新的主节点。由于原主节点已经停止接受写入 ,所以不会产生新的数据变更 ,等网络恢复后,即使原主节点降级为从节点并进行全量同步,也不会丢失网络分区期间的写入数据,**因为根本就没有新的写入发生。**由此解决了脑裂问题,就是不让主节点写了

20 Redis哨兵机制了解吗?

哨兵(Sentinel)机制是高可用架构的最常用的一种,哨兵机制主要保证3方面能力:监控、通知和自动故障转换

Redis 中的哨兵用于监控主从集群的运行状态,并在主节点故障时自动进行故障转移。

核心功能包括监控、通知和自动故障转移。哨兵会定期检查主从节点是否按预期工作,当检测到主节点故障时,就在从节点中选举出一个新的主节点,并通知客户端连接到新的主节点。

bash 复制代码
# 1. 核心监控配置
# 格式: sentinel monitor <主节点别名> <IP> <端口> <法定票数>
# 含义: 告诉哨兵去监控 IP 为 127.0.0.1:6379 的主节点,给它起名叫 "mymaster"。
#      最后的 "2" 是 Quorum(法定人数),表示至少需要 2 个哨兵都同意"主节点挂了",
#      系统才会真正认为它挂了(客观下线),并开始故障转移。
sentinel monitor mymaster 127.0.0.1 6379 2

# 2. 判定"主观下线"的时间
# 含义: 哨兵会每秒发 PING 给主节点。如果主节点在 5000 毫秒(5秒)内
#      都没有有效回复(比如超时、返回错误),当前这个哨兵就会觉得
#      "我看它是挂了",但这只是我一个人的看法,所以叫"主观下线"(SDOWN)。
sentinel down-after-milliseconds mymaster 5000

# 3. 故障转移的超时时间
# 含义: 如果哨兵决定开始进行故障转移(选新主节点),但因为各种原因(比如网络卡顿、
#      脚本执行慢)导致在 60000 毫秒(1分钟)内还没完成整个切换过程,
#      那就认为这次转移失败。稍后会再次尝试。
sentinel failover-timeout mymaster 60000

# 4. 并行同步数(重要性能调优参数)
# 含义: 当新的主节点选出来后,剩下的从节点都要改旗易帜,去连新的老大同步数据。
#      "1" 表示每次只能有 1 个从节点去同步数据,其他排队。
#      如果设得太大,多个从节点同时复制,会把新主节点的网卡和磁盘 IO 打满,导致服务不可用;
#      如果设得太小(如 1),恢复整个集群的时间会变长。
sentinel parallel-syncs mymaster 1

21 Redis哨兵的工作原理知道吗?

哨兵模式的工作原理主要概括为4个方面:定时监控,主观下线、客观下线、领导者选举和故障转移。

  • 定时监控:首先,哨兵会定期向所有Redis节点发送PING命令来检测它们是否可达。如果在指定时间内没有收到回复,哨兵会将该节点标记为"主观下线"
  • **客观下线:**主观下线向客观下线的转变,当一个哨兵判断主节点主观下线后,会询问其他哨兵的意见,如果达到配置的法定人数,主节点会被标记为"客观下线"。(法定人数的设定一般是哨兵总数的一半以上)
  • 领导者选举: 然后开始故障转移,这个过程中,哨兵会先选举出一个领导者,领导者再从从节点中选择一个最适合的节点作为新的主节点,选择标准包括复制偏移量、优先级等因素。

Redis 哨兵选举的核心规则其实就三点:

  1. Raft 协议:基于 Raft 算法的领头选举。
  2. 先到先得:在一个任期(Epoch)内,每个哨兵只有一张票,谁先来要,我就给谁。
  3. 过半机制 :想当老大,必须获得超过半数哨兵的支持(N/2+1N/2+1)。这就是为什么哨兵集群通常建议部署 奇数台(3台、5台),方便凑出半数

图中:Sentinel 1 最先确认主节点挂了, 然后它向其他哨兵(S2, S3)发送一个命令(SENTINEL is-master-down-by-addr),你可以理解为它在喊:"主节点挂了!我要当 Leader 去处理,你们投我一票好不好?"(这就是图中的 票+1 请求)。

Raft算法:让一组计算机(集群)像一个整体一样工作,即使其中一部分挂掉了,剩下的只要超过半数,依然能正常对外提供服务,并且数据保持一致。Raft 完美解决了分布式系统的一致性问题(CAP 理论中的 CP)

  • 故障转移:确定新主节点后,哨兵会向其发送 SLAVEOF NO ONE 命令使其升级为主节点,然后向其他从节点发送 SLAVEOF 命令指向新主节点,最后通过发布/订阅机制通知客户端主节点已经发生变化。

在实际部署中,为了保证哨兵机制的可靠性,通常建议至少部署三个哨兵节点,并且这些节点应分布在不同的物理机器上,降低单点故障风险。同时,法定人数的设置也非常关键,一般建议设置为哨兵数量的一半加一,既能确保在少数哨兵故障时系统仍能正常工作,又能避免网络分区导致的脑裂问题。

22 Redis领导者选举了解吗?

基于分布式共识算法Raft算法进行领导者选举。目的是在主节点故障时,选出一个哨兵来负责执行故障转移操作。

选举流程如下:

1.当一个哨兵确认主节点客观下线之后会向其他哨兵节点发送请求,表明希望由自己来当leader执行主从切换,**并且让其他的哨兵进行投票。**候选者先给自己投一票,然后等待其他哨兵的投票结果。()

2.收到请求的哨兵节点进行判断,如果候选者的日志和自己的一样新,任期号也小于自己,且之前没有投票过,就会投同意票 Y。否则回复 N。(一定要Epoch是比较起来比较新的才会进行投票)

3.候选者收到投票后会统计自己的得票数,如果获得了集群中超过半数节点的投票,它就会当选为领导者

4.如果没有哨兵在这一轮投票中获得超过半数的选票,这次选举就会失败,然后进行下一轮的选举。为了防止无限制的选举失败,每个哨兵都会有一个选举超时时间,且是随机的。

所以按照流程来说:越先发现客观下线节点的哨兵越容易变成leader

Raft算法完美解决了分布式系统的一致性问题(CAP 理论中的 CP)。raft算法

推荐阅读:Raft算法的选主过程详解

  • 强一致性:写操作只要返回成功,读操作一定能读到最新数据。
  • 高可用性:只要挂掉的节点不超过一半(比如 3 台挂 1 台,5 台挂 2 台),集群就能继续工作。

所有操作采用类似两阶段提交的方式,Leader 在收到来自客户端的请求后并不会执行,只是将其写入自己的日志列表中,然后将该操作发送给所有的 Follower。Follower 在收到请求后也只是写入自己的日志列表中然后回复 Leader,当有超过半数的结点写入后 Leader 才会提交该操作并返回给客户端,同时通知所有其他结点提交该操作。

通过这一流程保证了只要提交过后的操作一定在多数结点上留有记录(在日志列表中),从而保证了该数据不会丢失。

Redis Sentinel 选举不需要比较"谁的数据更新"(那是选 Redis Master 时的事),选 Sentinel Leader 只是为了找个牵头人

所以它的投票判断极其简单粗暴:

  • 看年代(Epoch):旧的滚粗。
  • 拼手速(First-Come-First-Served):谁的请求先到我这,我就投谁,后面来的通通拒绝。
  • 看权重 (Config Epoch):如果 Epoch 一样且都没投过,可能会参考配置纪元,但在故障转移选举中,主要就是拼手速

这就保证了在一个选举周期内,只能有一个 Sentinel 拿到超过半数的票。

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

由leader哨兵来进行判断,主要三方面:slave-priority(从节点优先级)最大节点、偏移量最大、runid最小

  1. 首先会进行从节点的过滤,进行一轮基础筛选,排除那些不满足基本条件的节点。比如说已下线的节点、网络连接不稳定的节点,以及优先级设为 0 明确不参与挑选的节点。
java 复制代码
// 第一轮筛选:排除不满足基本条件的从节点
for (int i = 0; i < numslaves; i++) {
    sentinelRedisInstance *slave = slaves[i];
    
    // 排除已下线的从节点
    if (slave->flags & (SRI_S_DOWN|SRI_O_DOWN)) continue;
    // 排除断开连接的从节点
    if (slave->link->disconnected) continue;
    // 排除近期(5秒内)断过连的从节点
    if (mstime() - slave->link->last_avail_time > 5000) continue;
    // 排除未建立主从复制的节点
    if (slave->slave_priority == 0) continue;
    
    // 找到第一个满足条件的从节点
    selected = i;
    break;
}

2.将还存在的节点,哨兵会对剩下的从节点进行排序,选出最合适的主节点。按照3个维度来进行排序:

  • 从节点优先级: slave-priority 的值越小优先级越高,优先级为 0 的从节点不会被选中。
  • 复制偏移量: 偏移量越大意味着从节点的数据越新,复制的越完整。
  • 运行 ID: 如果优先级和偏移量都相同,就比较运行 ID 的字典序,字典序小的优先。

选出新主节点后,哨兵会向其发送 SLAVEOF NO ONE 命令将其提升为主节点。

c 复制代码
// sentinel.c中的compareSlaves函数
int compareSlaves(sentinelRedisInstance *a, sentinelRedisInstance *b) {
    // 1. 首先比较用户设置的优先级,值越小优先级越高
    if (a->slave_priority != b->slave_priority)
        return (a->slave_priority < b->slave_priority) ? 1 : 2;
        
    // 2. 如果优先级相同,比较复制偏移量,偏移量越大数据越新
    if (a->slave_repl_offset > b->slave_repl_offset) return 1;
    else if (a->slave_repl_offset < b->slave_repl_offset) return 2;
    
    // 3. 如果复制偏移量也相同,比较运行ID的字典序
    return (strcmp(a->runid, b->runid) < 0) ? 1 : 2;
}
  1. 选择好之后向其他从节点发送命令通知自己是新的主节点。之后,哨兵会等待新主节点的角色转换完成,通过发送 INFO 命令检查其角色是否已变为 master 来确认。确认成功后,会更新所有从节点的复制目标,指向新的主节点。
bash 复制代码
SLAVEOF new-master-ip new-master-port

24 Redis集群了解吗?

Redis Cluster(Redis 集群) 是 Redis 3.0 之后推出的官方分布式解决方案

主从复制实现了读写分离和数据备份 ,哨兵机制实现了主节点故障时自动进行故障转移。结合前两种的另一种优化

集群架构是对前两种方案的进一步扩展和完善,通过数据分片解决 Redis 单机内存大小的限制,当用户基数从百万增长到千万级别时,我们只需简单地向集群中添加节点,就能轻松应对不断增长的数据量和访问压力。

比如说我们可以将单实例模式下的数据平均分为 5 份,然后启动 5 个 Redis 实例,每个实例保存 5G 的数据,从而实现集群化。

为什么会有Redis Cluster?

但是,它们都有一个致命死穴:

无论你怎么做主从,所有的"写操作"都只能由 1 个 Master 承担

如果你的业务非常庞大,比如微博热搜,每秒有 50 万写入,或者内存数据达到了 500GB,单台机器(即使是顶级服务器)的内存和 CPU 也会被撑爆。

Redis Cluster 就是为了解决"容量"和"写性能"的扩展问题而生的。

25 请详细说一说Redis Cluster?

推荐阅读:Redis Cluster

Redis集群更加详细的说明。核心技术:数据切片的哈希槽,来了一个key:value我们要怎么存在Redis数据集群里

Redis Cluster核心理念是去中心化,采用P2P模式,没有中心节点的概念,每一个节点保留着数据和整个集群的状态,节点之间通过gossip协议交换信息

数据切片,数据如何存放,采用它的核心技术哈希槽 ,在数据分片方面,Redis Cluster 使用哈希槽机制将整个集群划分为 16384 个单元。(2^14)

例如,如果我们有 4 个 Redis 实例,那么每个实例会负责 4000 多个哈希槽。

在计算哈希槽编号时,Redis Cluster 会通过 CRC16 算法先计算出键的哈希值,再对这个哈希值进行取模运算,得到一个 0 到 16383 之间的整数。

复制代码
slot = CRC16(key) mod 16384

这种方式可以将数据均匀地分布到各个节点上,避免数据倾斜的问题。

当需要存储或查询一个键值对时,Redis Cluster 会先计算这个键的哈希槽编号,然后根据哈希槽编号找到对应的节点进行操作。

26 集群中数据如何分区?

集群数据的分区主要有三种方法:节点取余、一致性哈希、哈希槽

(1)节点取余

通过key去计算目标节点的索引。节点取余分区简单明了,通过计算键的哈希值,然后对节点数量取余,结果就是目标节点的索引。

c 复制代码
target_node = hash(key) % N  // N为节点数量

缺点:增加节点后会发生大规模数据迁移。

缺点是增加一个新节点后,节点数量从 N 变为 N+1,**几乎所有的取余结果都会改变,导致大部分缓存失效。**出现大规模的数据迁移。

扩缩容不仅麻烦,而且是灾难:

  • 假设现在加了一台机器,N 从 3 变成了 4。
  • 原来的公式 10%3=1 变成了 10%4=2。
  • 结果:user:1 从 Node 1 搬到了 Node 2。
  • 灾难 :一旦机器数量变化,几乎所有数据(约 80%-90%)的映射关系都会改变 。这会导致海量数据需要迁移,或者缓存瞬间全部失效(缓存雪崩),请求直接打垮数据库。

(2)一致性哈希

为了解决节点变化导致的大规模数据迁移问题,一致性哈希分区出现了:它将整个哈希值空间想象成一个环,节点和数据都映射到这个环上。数据被分配到顺时针方向上遇到的第一个节点。

这种设计的巧妙之处在于,当节点数量变化时,只有部分数据需要重新分配。比如说我们从 5 个节点扩容到 8 个节点,理论上只有约 3/8 的数据需要迁移,大大减轻了扩容时的系统压力。(只影响邻近节点的数据

但一致性哈希仍然有一个问题:数据分布不均匀。比如说在上面的例子中,节点 1 和节点 2 的数据量差不多,但节点 3 的数据量却远远小于它们。

想象一条闭合的环(Ring),在这个环上定义了 0 到 2^32−1个点。

  1. 节点落环:把机器的 IP 或 ID 进行哈希,落在环上的某个位置。
  2. 数据落环:把 key 进行哈希,也落在环上的某个位置。
  3. 顺时针查找 :数据落点之后,顺时针 找到的第一个机器节点,就是它的归宿。

(3)哈希槽(Redis Cluster使用)

它将整个哈希值空间划分为 16384 个槽位,每个节点负责一部分槽,数据通过 CRC16 算法计算后对 16384 取模,确定它属于哪个槽。

复制代码
slot = CRC16(key) % 16384

假设系统中有 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,节点上的其他槽位保留。(扩容移动的数据量还是比较少)

因为槽的个数刚好是 2 的 14 次方,和 HashMap 中数组的长度必须是 2 的幂次方有着异曲同工之妙。它能保证扩容后,大部分数据停留在扩容前的位置,只有少部分数据需要迁移到新的槽上。

27 能说说 Redis 集群的原理吗?

Redis集群的搭建开始于节点的添加和握手 。每一个节点需要设置cluster-enabled yes集群模式。然后通过命令CLUSTER MEET进行握手,将对方添加到格子的节点队列中。

节点之间会使用gossip协议进行通信连接。节点 A 发送 MEET 消息,节点 B 回复 PONG 并发送 PING,节点 A 回复 PONG,于是双向的通信链路就建立完成了。

由于采用了 Gossip 协议,我们不需要让每对节点都执行握手。在一个多节点集群的部署中,仅需要让第一个节点与其他节点握手,其余节点就能通过信息传播自动发现并连接彼此。

握手完成后,可以通过 CLUSTER ADDSLOTS 命令为主节点分配哈希槽。当 16384 个槽全部分配完毕,集群正式进入就绪状态。

关于故障检测

故障检测和恢复是保障 Redis 集群高可用的关键。每秒钟,节点会向一定数量的随机节点发送 PING 消息,当发现某个节点长时间未响应 PING 消息,就会将其标记为主观下线。

当半数以上的主节点都认为某节点主观下线时,这个节点就会被标记为"客观下线"。

如果下线的是主节点,它的从节点之一将被选举为新的主节点,接管原主节点负责的哈希槽。

(1)部署 Redis 集群至少需要几个物理节点?

部署一个生产环境可用的 Redis 集群,从技术角度来说,**至少需要 3 个物理节点。**这个最小节点数的设定并非 Redis 技术上的硬性要求,而是基于高可用原则的实践考量。

从实践角度看,**最经典的 Redis 集群配置是 3 主 3 从,共 6 个 Redis 实例。**考虑到需要 3 个主节点和 3 个从节点,并且每对主从不能在同一物理机上,那么至少需要 3 个物理节点,每个物理节点上运行 1 个主节点和另一个主节点的从节点。

  • 物理节点1:主节点A + 从节点B'
  • 物理节点2:主节点B + 从节点C'
  • 物理节点3:主节点C + 从节点A'

这种交错部署方式可以确保任何一个物理节点故障时,最多只影响一个主节点和一个不同主节点的从节点。

28 说说Redis集群的动态伸缩?

Redis集群的动态伸缩是通过哈希槽实现的,动态伸缩就是可以灵活的增减节点。

当需要扩容时,首先通过 CLUSTER MEET 命令将新节点加入集群;然后使用 reshard 命令将部分哈希槽重新分配给新节点。

相关推荐
小吴编程之路21 小时前
MySQL 索引核心特性深度解析:从底层原理到实操应用
数据库·mysql
~莫子21 小时前
MySQL集群技术
数据库·mysql
凤山老林1 天前
SpringBoot 使用 H2 文本数据库构建轻量级应用
java·数据库·spring boot·后端
就不掉头发1 天前
Linux与数据库进阶
数据库
与衫1 天前
Gudu SQL Omni 技术深度解析
数据库·sql
咖啡の猫1 天前
Redis桌面客户端
数据库·redis·缓存
oradh1 天前
Oracle 11g数据库软件和数据库静默安装
数据库·oracle
what丶k1 天前
如何保证 Redis 与 MySQL 数据一致性?后端必备实践指南
数据库·redis·mysql
_半夏曲1 天前
PostgreSQL 13、14、15 区别
数据库·postgresql
把你毕设抢过来1 天前
基于Spring Boot的社区智慧养老监护管理平台(源码+文档)
数据库·spring boot·后端