基础篇
1.关系/非关系型数据库
关系型数据库最典型的数据结构是表,由二维表及其之间的联系所组成的一个数据组织
非关系型数据库不是数据库,而是数据结构化存储方法的集合,可以是文档或键值对等
2.什么是Redis
Redis 是一种基于内存的数据库,对数据的读写操作都是在内存中完成,因此**读写速度非常快**, 常用于**缓存,消息队列、分布式锁等场景**。并且对数据类型的操作都是**原子性**的,因为执行命令由单线程负责的, 不存在并发竞争的问题。Redis 还支持**事务 、持久化、Lua 脚本、多种集群方案(主从复制模式、哨兵模式、切片机群模式)、发布/订阅模式,内存淘汰机制、过期删除机制**等等。
3.redis数据类型
类型 | 用途 | 底层实现 |
String(字符串) 可存储字符串、整数或浮点数 | 计数器 分布式锁 存储序列化对象 | int(如果存的全数字) SDS 简单动态字符串 |
Hash(哈希) 可存储多个键值对 | 缓存对象 购物车 | ZipList 压缩列表 (连续占用内存,因此存储的元素不能太多) 一旦元素存储太多,会转为 Dict 哈希表 Redis 7.0 引入了 listpack 取代 ZipList 压缩列表 |
List(列表) 可重复、(插入和取出)有序 | 朋友圈点赞/评论列表 | QuickList 快速列表:一种双向链表,每个节点是一个压缩列表 结合了压缩列表和双向链表的优点 |
Set(集合) 不可重复,无序,求交集、差集等 | 求共同关注 去重 | 若集合中的元素都是整数且元素个数小于 512 : IntSet 整数集合 否则:Dict 哈希表(Set是单列集合,Dict的每个节点是存键值对的,那么其实是只用了键来存元素,值都设为null) |
ZSet(有序集合) 不可重复,每个元素都关联一个分数 自动按照分数排序元素 | 排行榜 | 当元素数量小于128,每个元素小于64字节:ZipList 压缩列表 否则:Dict + SkipList,存储两份数据,Dict 和 SkipList 各一份 为什么要存储两份数据?因为这是它的特性需求
Redis 7.0 引入了 listpack 取代 ZipList 压缩列表 |
Redis 的 String 类型底层实现是怎样的?为什么会有多种编码?
String 类型底层由
redisObject
表示,其实际编码可能是int
、embstr
或raw
,目的是根据数据大小和特性优化内存和性能:
- 小整数 →
int
编码,节省内存;- 小字符串(≤44字节) →
embstr
,一次性分配,创建销毁更快;- 大字符串 →
raw
,更灵活但开销大一些。
4.Redis单线程
Redis是单线程的;网络I/O和数据读写操作都是由单个线程来完成的。
但Redis的其他功能,如持久化,异步删除,集群数据同步等,是由额外线程来执行的。
因此,严格来说Redis并不是全面单线程。
5.Redis数据不丢失(持久化机制)
AOF 日志(完整):每执行一条写操作命令,就把该命令以追加的方式写入到一个文件里;
RDB 快照(恢复快):将某一时刻的内存数据,以二进制的形式写入磁盘;
RDB 快照就是记录某一个瞬间的内存数据,记录的是**实际数据**,而 AOF 文件记录的是**命令操作的日志,**而不是实际的数据。
因此在 Redis 恢复数据时, **RDB 恢复数据的效率会比 AOF 高些**,因为直接将 RDB 文件读入内存就可以,不需要像 AOF 那样还需要额外执行操作命令的步骤才能恢复数据
RDB快照
一篇文章彻底理解Redis持久化:RDB和AOF_rdb和aof存得是什么-CSDN博客
自动触发(主流)
save
命令来设置 自动快照的触发条件,当满足特定的“时间 + 修改次数”条件
save 300 10 # 如果300秒内至少有10个key发生变化,则触发保存
- 在配置文件中设置了save的相关配置,如sava m n,它表示在m秒内数据被修改过n次时,自动触发bgsave操作。
- 当从节点做全量复制时,主节点会自动执行bgsave操作,并且把生成的RDB文件发送给从节点。
- 执行debug reload命令时,也会自动触发bgsave操作。
- 执行shutdown命令时,如果没有开启AOF持久化也会自动触发bgsave操作。
手动触发==>阻塞与非阻塞保存
方式 | 命令 | 阻塞主线程 | 描述 |
---|---|---|---|
阻塞保存 | SAVE | 是 | 阻塞当前 Redis 服务器,直到 RDB 过程完成为止,对于内存比较大的实例造成长时间阻塞,基本不采用。 |
非阻塞保存 | BGSAVE | 否 | Redis 进程执行fork 操作创建子进程,RDB 持久化过程由子进程负责,完成后自动结束。阻塞只发生在 fork 阶段,一般时间很短。 |
Redis进程会执行fork操作创建子进程,RDB持久化由子进程负责,不会阻塞Redis服务进程。Redis服务的阻塞只发生在fork阶段,一般情况时间很短。
- 执行bgsave命令,Redis进程先判断当前是否存在正在执行的RDB或AOF子线程,如果存在就是直接结束。
- Redis进程执行fork操作创建子线程,在fork操作的过程中Redis进程会被阻塞。
- Redis进程fork完成后,bgsave命令就结束了,自此Redis进程不会被阻塞,可以响应其他命令。
- 子进程根据Redis进程的内存生成快照文件,并替换原有的RDB文件。
- 子进程通过信号量通知Redis进程已完成。
思考:如果当前Redis服务器中存储的数据特别多,内存消耗特别大(比如100GB),此时,进行上述的fork操作,是否会有很大的性能开销?
答:此时的性能开销,其实是挺小的。fork在进行内存拷贝的时候,不是简单无脑的直接把所有的数据都拷贝一遍,而是“写时拷贝”的机制来完成的!
具体来说,fork只会在内存被修改时才实际复制数据,而在未修改时,父子进程共享同一份物理内存。因此,即使有大量数据,内存的实际拷贝成本也能得到控制,从而在处理高内存使用情况下的性能影响降到最低。这使得Redis在高负载时仍然能够有效运行。
AOF日志
【Redis】持久化机制--RDB和AOF_redis rdb和aof-CSDN博客
AOF(Append Only File)持久化: 以独立日志的方式记录每次写命令,重启时再重新执行 AOF文件中的命令达到恢复数据的目的。AOF的主要作用是解决了数据持久化的实时性,目前已经是
Redis 持久化的主流方式。(当开启AOF后,RDB就不再生效)
AOF的工作流程
1. 所有的写⼊命令会追加到 aof_buf(缓冲区)中。
2. AOF 缓冲区根据对应的策略向硬盘做 同步操作 。 这样可以减少写磁盘的次数。【缓冲区还是为了追求高的效率,如果 突然断电 ,缓冲区中的数据还没有来得及写磁盘,这时候还是会出现 数据丢失 的情况的】
3. 随着 AOF ⽂件越来越⼤,需要定期对 AOF ⽂件进⾏重写 ,达到压缩的⽬的。
4. 当 Redis 服务器启动时,可以加载 AOF ⽂件进⾏数据恢复。
文件同步(缓冲区刷新策略)==3种策略
AOF 同步(写磁盘)发生在 Redis 执行完写操作之后,它有 3 种策略:
策略 | 含义 | 特点 |
---|---|---|
always | 每次写命令都同步到磁盘 | 最安全但最慢 |
everysec | 每秒同步一次 | 折中方案,默认选项 |
no | 由操作系统控制(缓冲区写满再刷盘) | 性能高但最不安全 |
重写机制
AOF文件重写是把 Redis 进程内的数据转化为写命令同步到新的 AOF文件。重写后的 AOF为什么可以变小?有如下原因:
- 进程内已超时的数据不再写入文件。
- 旧的 AOF 中的无效命令,例如 del、hdel、srem 等重写后将会删除
- 多条写操作合并为一条
Redis启动流程
优缺点对比
1. RDB快照
优点:1、性能影响较小(子进程完成) 2、数据恢复速度快 3、适用于定期备份
缺点:1、数据丢失风险较高:RDB是定期执行的,因此在两次快照之间的数据将会丢失。如果Redis在快照完成之前崩溃,会导致最新的数据丢失。2、不灵活:RDB持久化只能定期执行,无法根据实际业务需求灵活地设置持久化频率。
2. AOF
优点: 1、数据丢失风险低(同步策略)2、更可控的持久化机制 3、文件内容可读
缺点:1、文件体积大:由于AOF会记录每一条写操作指令,随着时间的推移,文件会比RDB大得多,导致持久化文件占用大量磁盘空间。2、数据恢复速度较慢:AOF文件在恢复时需要逐条执行日志中的命令,因此恢复速度通常比RDB慢。3、对性能影响更大:频繁的写操作会导致性能开销,尤其是在设置为“每次写操作”同步时,对Redis的性能影响较大。
总结
- RDB适用于:希望最小化对Redis性能影响、对数据持久化频率要求不高,或者需要定期备份的场景。
- AOF适用于:对数据持久性要求高,不能接受较多数据丢失的场景,但需要注意性能和磁盘空间的占用。
6.什么要用Redis/为什么要用缓存
主要从高性能和高并发这两个角度来看待问题
高性能:假如用户第一次访问数据库中的某些数据,因为从磁盘上读取的,过程比较慢。将该用户访问的数据存储在缓存中,这样下次再次访问这些数据的时候,就可以从缓存中拿了。操作缓存就是直接操作内存,所以速度特别快。
高并发:直接操作缓存所能承受的请求是远远大于直接访问数据库
7.为什么Redis这么快
(1)完全基于内存,绝大部分请求是纯粹的内存操作,非常快速。数据存在内存中,类似于 HashMap,HashMap 的优势就是查找和操作的时间复杂度都是 O(1);
(2)数据结构简单,对数据操作也简单,Redis 中的数据结构是专门进行设计的
(3)采用单线程,避免了不必要的上下文切换、多进程或者多线程切换而消耗 CPU,不用去考虑各种锁的问题,不存在加锁释放锁操作,没有因为可能出现死锁而导致的性能消耗;
(4)使用多路 I/O 复用模型
8.Redis的过期键和删除策略
Redis是key-value数据库,我们可以设置Redis中缓存的key的过期时间。
Redis的过期策略就是指当Redis中缓存的key过期了,Redis如何处理。
Redis 使用的过期删除策略是「**惰性删除+定期删除**」。
过期策略通常有以下三种
(1)定时删除:每个Key都需要一个定时器,到过期期间就会立即删除。
该策略可以立即清楚过期的key,对内存很友好;但是会占用大量CPU去处理过期的数据,从而影响缓存的响应时间和吞吐量。
(2)惰性删除:不主动删除过期键,每次从数据库访问 key 时,都检测 key 是否过期,如果过期则删除该 key。
该策略可以最大化的节省CPU资源,但对内存十分不友好,极端情况可能出现大量的过期key没有再次被访问,从而不会被清除,占用大量内存
(3)定期删除:每隔一段时间「随机」从数据库中取出一定数量的 key 进行检查,并删除其中的过期key
通过调整定时扫描的时间间隔和每次扫描的限定耗时,可以在不同情况下使得CPU和内存资源达到最优的平衡效果。
9.Redis 持久化时,对过期键会如何处理的?
Redis 持久化文件有两种格式: AOF和RDB
1.AOF 文件分为两个阶段:AOF 文件写入阶段(不检查)和 AOF 重写阶段(检查)。
**AOF 文件写入阶段**:如果数据库过期键还没被删除, AOF 文件会保留此过期键,当此过期键被删除后,Redis 会向 AOF 文件追加一条 DEL 命令来 显式地删除该键值**。
- **AOF 重写阶段**:执行 AOF 重写时,会对 Redis 中的键值对进行检查,**已过期的键不会被保 存到重写后的 AOF 文件中**,因此不会对 AOF 重写造成任何影响
2.RDB 文件分为两个阶段:RDB 文件生成阶段(检查)和加载阶段(主库检查、从库不检查)。
- **RDB 文件生成阶段**:从内存状态持久化成 RDB(文件)的时候,会对 key 进行过期检查,** 过期的键「不会」被保存到新的 RDB 文件中**,因此 Redis 中的过期键不会对生成新 RDB 文件产 生任何影响。
- RDB 加载阶段 :RDB 加载阶段时,要看服务器是主服务器还是从服务器,分别对应以下两种情况:- **如果 Redis 是「主服务器」运行模式的话,在载入 RDB 文件时,程序会对文件中保存的键进行 检查,过期键「不会」被载入到数据库中**。所以过期键不会对载入 RDB 文件的主服务器造成影 响;- **如果 Redis 是「从服务器」运行模式的话,在载入 RDB 文件时,不论键是否过期都会被载入到 数据库中**。
但由于主从服务器在进行数据同步时,从服务器的数据会被清空。所以一般来说,过期键对载入 RDB 文件的从服务器也不会造成影响。
10.Redis 主从模式中,对过期键会如何处理?
当 Redis 运行在主从模式下时,**从库不会进行过期扫描,从库对过期的处理是被动的**。也就是即 使从库中的 key 过期了,如果有客户端访问从库时,依然可以得到 key 对应的值,像未过期的键值 对一样返回。 从库的过期键处理依靠主服务器控制,**主库在 key 到期时,会在 AOF 文件里增加一条 del 指令, 同步到所有的从库**,从库通过执行这条 del 指令来删除过期的 key。
11.Redis 内存淘汰策略/内存中的cache淘汰
Redis 内存淘汰策略共有八种,这八种策略大体分为「不进行数据淘汰」和「进行数据淘汰」两类策 略。
1、不进行数据淘汰的策略 :当运行内存超过最大设置内存时, 不淘汰任何数据,但不再提供服务,直接返回错误。
2、进行数据淘汰的策略,细分为「在设置了过期时间的数据中进行淘汰」和「在所有数据范围内进行淘汰」这两类策略。
2-1、在设置了过期时间的键值中进行淘汰:
- **volatile-random**:随机淘汰任意过期键值;
- **volatile-ttl**:优先淘汰更早过期的键值。
- **volatile-lru**(Redis3.0 之前,默认的内存淘汰策略):淘汰最久未使用的过期键值;
- **volatile-lfu**(Redis 4.0 后新增的内存淘汰策略):淘汰最少使用的过期键值;
2-2、在所有数据范围内进行淘汰:
- **allkeys-random**:随机淘汰任意键值;
- **allkeys-lru**:淘汰最久未使用的键值;
- **allkeys-lfu**(Redis 4.0 后新增的内存淘汰策略):淘汰最少使用的键值
12.LRU 算法和 LFU 算法有什么区别?
什么是 LRU 算法?
**LRU** 全称是 Least Recently Used 翻译为**最近最少使用**,会选择淘汰最近最少使用的数据。
传统 LRU 算法的实现是基于**「链表」结构**,链表中的元素按照操作顺序从前往后排列,最新操 作的键会被移动到表头,当需要内存淘汰时,只需要删除链表尾部的元素即可,因为链表尾部的元素 就代表最久未被使用的元素。
Redis 并没有使用这样的方式实现 LRU 算法,因为传统的 LRU 算法存在两个问题:
- 需要用链表管理所有的缓存数据,这会带来额外的空间开销;-
- 当有数据被访问时,需要在链表上把该数据移动到头端,如果有大量数据被访问,就会带来很多链 表移动操作,会很耗时,进而会降低 Redis 缓存性能。
Redis 是如何实现 LRU 算法的?
`Redis 实现的是一种近似 LRU 算法`,目的是为了更好的节约内存,实现方式是在 Redis 的对象结构体中添加一个额外的字段,用于记录此数据的最后一次访问时间。---时间戳
当 Redis 进行内存淘汰时,会使用**随机采样的方式来淘汰数据**,它是随机取值,然后**淘汰最久没有使用的那个**。
Redis 实现的 LRU 算法的优点:
- 不用为所有的数据维护一个大链表,节省了空间占用;
- 不用在每次数据访问时都移动链表项,提升了缓存的性能;
但是 LRU 算法有一个问题,**无法解决缓存污染问题**,比如应用一次读取了大量的数据,而这些 数据只会被读取这一次,那么这些数据会留存在 Redis 缓存中很长一段时间,造成缓存污染。
因此,在 Redis 4.0 之后引入了 LFU 算法来解决这个问题。
> 什么是 LFU 算法?
LFU 全称是 Least Frequently Used 翻译为**最近最不常用的**,LFU 算法是根据数据访问次数来 淘汰数据的,它的核心思想是“如果数据过去被访问多次,那么将来被访问的频率也更高”。
所以, LFU 算法会记录每个数据的访问次数。当一个数据被再次访问时,就会**增加该数据的访问 次数**。这样就解决了偶尔被访问一次之后,数据留存在缓存中很长一段时间的问题,相比于 LRU 算法也更合理一些。
13.缓存雪崩、击穿、穿透
一、如何避免缓存雪崩?(大量数据无缓存,在数据库)
**大量缓存数据在同一时间过期(失效)时,如果此时有大量的用户请求,都无法在 Redis 中处 理,于是全部请求都直接访问数据库,从而导致数据库的压力骤增,造成整个系统崩溃。
对于缓存雪崩问题,我们可以采用两种方案解决。
- - 将缓存失效时间随机打散: 在原有的失效时间基础上增加一个随机值(0-1)这样每个缓存的过期时间都不重复了,也就降低了缓存集体失效的概率。
- - 设置缓存不过期:我们可以通过后台服务来更新缓存数据,从而避免因为缓存失效造成的缓存 雪崩,也可以在一定程度上避免缓存并发问题。
二、 如何避免缓存击穿?(热点数据无缓存,在数据库)
如果缓存中的**某个热点数据过期**了,此时大量的请求访问了该热点数据,就无法从缓存中读取, 直接访问数据库,数据库很容易就被高并发的请求冲垮,这就是**缓存击穿**的问题。
可以发现缓存击穿跟缓存雪崩很相似,你可以认为缓存击穿是缓存雪崩的一个子集。
应对缓存击穿 可以采取前面说到两种方案:
- - 互斥锁方案:保证同一时间只有一个业务线程请求缓存,未能获取互斥锁的请求,要么等待锁释放后重新读取缓存,要么就返回空值或者默认值。
- - 不给热点数据设置过期时间:由后台异步更新缓存,或者在热点数据准备要过期前,提前通知后台线程更新缓存以及重新设置过期时间;
三、如何避免缓存穿透?(缓存和数据库都没有)
当用户访问的数据,**既不在缓存中,也不在数据库中**, 那么当有大量这样的请求到来时,数据库的压力骤增,这就是**缓存穿透**的问题。
缓存穿透的发生一般有这两种情况:
- - 业务误操作:缓存中的数据和数据库中的数据都被误删除了,所以导致缓存和数据库中都没有数 据;
- - 黑客恶意攻击:故意大量访问某些读取不存在数据的业务;
应对缓存穿透的方案,常见的方案有三种。
- - 限制非法字段的请求:当有大量恶意请求访问不存在的数据的时候,也会发生缓存穿透,因此在 API 入口处我们要判断求请求参数是否合理,请求参数是否含有非法值、请求字段是否存在,如果判 断出是恶意请求就直接返回错误,避免进一步访问缓存和数据库。
- - 设置空值或者默认值:当我们线上业务发现缓存穿透的现象时,可以针对查询的数据,在缓存 中设置一个空值或者默认值,这样后续请求就可以从缓存中读取到空值或者默认值,返回给应用,而 不会继续查询数据库。
- - 使用布隆过滤器:用户请求到来时,业务线程确认缓存失效后,可以通过查询布隆过滤器快速判断数据是否存在,如果不存在,就不用通过查询数据库来判断数据是否存在,即使发生了缓存穿透,大量请求只会查询 Redis 和布隆过滤器,而不会查询数据库,保证了数据库能正常运行,Redis 自身也是支持布隆过滤器的。
14.Redis主从复制/同步
主从复制是 Redis 高可用服务的最基础的保证,实现方案就是将一台 Redis 服务器,同步数据到多台从 Redis 服务器上,即一主多从的模式,且主从服务器之间采用的是「读写分离」的方式。
1、从节点发送 SYNC 命令给主节点。
2、主节点生成 RDB 快照文件,并将后续的写命令缓存到缓冲区。
3、主节点发送 RDB 文件给从节点,从节点加载 RDB 文件。
4、主节点将缓冲区中的写命令发送给从节点,从节点执行这些命令以保持数据同步。
15.Redis哨兵模式(监控、选主、通知)
Redis 主从服务的时候,存在问题:当 Redis 的主从服务器出现故障宕机时,需要 动进行恢复。 为了解决这个问题,Redis 增加了哨兵模式,因为哨兵模式做到了可以监控主从服务器,并且提供主从节点故障转移的功能。
哨兵模式选取主节点
1、故障节点主观下线
2、故障节点客观下线
3、哨兵集群选取leader
4、哨兵leader选取主节点
16.Redis切片集群
当 Redis 缓存数据量大到一台服务器无法缓存时,就需要使用 **Redis 切片集群**方案,它将数据分布在不同的服务器上,以此来降低系统对单主节点的依赖,从而提高 Redis 服 务的读写性能。
17.Redis与MySQL数据一致性?
1.先写缓存,再写数据库
先写缓存,当我们写入缓存成功的时候,突然网络出现异常,导致写数据库失败,这时候数据库没有新的数据,缓存有新的数据,此时缓存中的数据就变成了脏数据
2.先写数据库,再写缓存
如果先更新缓存成功,但是数据库更新失败,则肯定会造成数据不一致
3.先删缓存,再更新数据库---**Cache Aside 策略**
在删除缓存与更新数据库期间,请求刚好查询,缓存中没有,从数据库读取。造成数据不一致
延时双删
删除缓存-->更新数据库--->一定时间间隔---->再删除缓存
防止旧缓存还存在,这样就不会从mysql更新
4.先更新数据库 ,再删除缓存---订阅MySQL binlog,后操作缓存
第一步是更新数据库,那么更新数据库成功,就会产生一 条变更日志,记录在 binlog 里。 于是我们就可以通过订阅 binlog 日志,拿到具体要操作的数据,然后再执行缓存删除,
18.大 key 如何处理?
什么是 Redis 大 key?
大 key 并不是指 key 的值很大,而是 key 对应的 **value 很大**。
一般而言,下面这两种情况被称为大 key:
- - String 类型的值大于 10 KB;
- - Hash、List、Set、ZSet 类型的元素的个数超过 5000个;
大 key 会造成什么问题?
大 key 会带来以下四种影响:
- **客户端超时响应**:由于 Redis 执行命令是单线程处理,然后在操作大 key 时会比较耗时,那么就会阻塞 Redis,从客户端这一视角看,就是很久都没有响应。
- **引发网络阻塞**:每次获取大 key 产生的网络流量较大
- **阻塞工作线程**:如果使用 del 删除大 key 时,会阻塞工作线程
- **内存分布不均**:集群模型在 slot 分片均匀情况下,会出现数据和查询倾斜情况,部分有大 key 的 Redis 节点占用内存多,QPS 也会比较大。
> 如何处理?
-**识别大key**:使用`redis-cli`工具的`SCAN`命令结合`KEYS`或`HGETALL`、`LRANGE`等命令来 定位哪些key占用过多的内存或哪些操作可能引起性能问题。-
**优化数据结构**:使用更高效的数据结构,例如用`Set`、`Sorted Set`或`Hash`替换`String`类型,当数据适合这些结构时。对于大型列表,考虑使用`List`的`LPUSH`和`RPUSH`来限制列表的长度,或者使用`ZADD`和`ZREM`在有序集合中维护固定大小的滑动窗口。使用`Bitmaps`来存储大量二进制数据,尤其是当数据是稀疏的或需要进行位操作时
**数据拆分**:最好在设计阶段,就把大 key 拆分成一个一个小 key。或者,定时检查 Redis 是否 存在大 key ,如果该大 key 是可以删除的,不要使用 DEL 命令删除,因为该命令删除过程会阻塞主 线程,而是用 unlink 命令(Redis 4.0+)删除大 key,因为该命令的删除过程是异步的,不会阻塞 主线程。
**避免全量读取**:对于大key,尽量避免一次性读取全部数据,而是使用范围查询如`HGET`、 `LPOP`、`RPOP`等命令来分批次读取数据。
19.热key如何处理
热key:访问率高、读写频繁的键
- 本地缓存:例如Java的Caffeine地本地缓存,减少Redis的压力,注意设置过期时间
- Key分片,将热Key拆分成多个子Key,分散到不同的Redis实例上
- 读写分离(含主从架构),读请求分流到多个从节点
20.事务与Redis
Redis的事务总是具有ACID中的一致性和隔离性,其他特性是不支持的。
Redis事务支持隔离性吗
Redis 是单进程程序,并且它保证在执行事务时,不会对事务进行中断,事务可以运行直到执行完所有事务队列中的命令为止。因此,Redis 的事务是总是带有隔离性的。
Redis事务保证原子性吗,支持回滚吗
Redis中单条命令是原子性执行的,但事务不保证原子性,且没有回滚。事务 中任意命令执行失败,其余的命令仍会被执行。
Redis事务其他实现
- 基于Lua脚本,Redis可以保证脚本内的命令一次性、按顺序地执行, 其同时也不提供事务运行错误的回滚,执行过程中如果部分命令运行错误,剩下的命令还是会继续运行完
- 基于中间标记变量,通过另外的标记变量来标识事务是否执行完成,读取数据时 先读取该标记变量判断是否事务执行完成。但这样会需要额外写代码实现,比较繁琐
21.跳表和字典
ZSet 使用了跳表和字典的组合
- 跳表:在创建节点时候,会生成范围为[0-1]的一个随机数,如果这个随机数小于 0.25,那么层数就增加1层,然后继续生成下一个随机数,直到随机数的结果大于 0.25 结束,最终确定该节点的层数(最高为64层)。
- 字典(哈希表):用来存储键值对,并且提供了高效的查找、插入和删除操作。在内部,字典通常由两个哈希表组成,使得在哈希冲突较多或者哈希表负载因子较高时,能够平滑地进行重新哈希,从而保持操作的高效性。
**为什么需要两个哈希表**-->针对重新哈希期间
1. **平滑重新哈希**:
- 一个哈希表会保留原有的键值对,而另一个哈希表则用于新插入的键值对。
- 避免了在单次操作中完成整个哈希表 的重新计算,从而减少了单次操作的延迟和对性能的影响。
2. **避免停顿时间**:
- 旧的哈希表仍然可以被读取,新的写操作会被导向到新的哈希表。
- 避免了在重新哈希过程中可能出现的长时间停顿,提高了系统的响应时间和并发能力。
3. **渐进式重新哈希**:
- 重新哈希操作可以被分割成多个步骤,每个 步骤只移动一部分键值对到新的哈希表,直到所有的键值对都完成了迁移。
- 这种策略可以确保在高负载下系统的稳定性和性能。
4. **节省内存**:
- 旧的哈希表不会立即被释放,而是等到所有的键值对都迁移到的哈希表后才被回收。
- 这种机制可以避免在短时间内大量的内存分配和释放,有助于减少内存碎片和提高内存管理的效率。
22.跳表、红黑树、B+树
23.Redis I/O多路复用
首先,Redis 是跑在单线程中的,所有的操作都是按照顺序线性执行的,但是由于读写操作等待用户输入或输出都是阻塞的,所以 I/O 操作在一般情况下往往不能直接返回,这会导致某一文件的 I/O 阻塞导致整个进程无法对其它客户提供服务,而 I/O 多路复用就是为了解决这个问题而出现的。
I/O 多路复用其实是在单个线程中通过记录跟踪每一个sock(I/O流) 的状态来管理多个I/O流。
24.Redis 管道有什么用?
管道技术(Pipeline)是客户端提供的一种批处理技术,用于一次处理多个 Redis 命令,从而提高整 个交互的性能。
使用**管道技术可以解决多个命令执行时的网络等待**,它是把多个命令整合到一起发送给服务器端 处理之后统一返回给客户端,这样就免去了每条命令执行后都要等待的情况,从而有效地提高了程序 的执行效率。 但使用管道技术也要注意避免发送的命令过大,或管道内的数据太多而导致的网络阻塞。
要注意的是,管道技术本质上是客户端提供的功能,而非 Redis 服务器端的功能
进阶篇 场景题
redis如何实现分布锁
1.通过 SET
命令的 NX
和 PX
选项可以实现分布式锁
SET lock_key unique_value NX PX 30000(30s)
释放锁时,使用 Lua 脚本确保只有持有锁的客户端才能释放
-
脚本先检查锁的值是否匹配,匹配则删除锁。
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
else
return 0
end
锁自动过期导致误释放
如果业务执行时间超过 EX 10
设置的时间,锁会自动释放,可能导致其他进程获取锁,产生数据竞争问题。
✅ 解决方案:可以使用 看门狗机制 续约锁(Redisson 解决方案)
lua脚本:确保获取锁和设置过期时间的原子性,适用于需要更高可靠性的场景
原子性:Lua 脚本在 Redis 中原子执行,无需担心并发问题。
减少网络开销:将多个操作合并为一个脚本,减少客户端与服务器之间的通信次数。
复杂逻辑支持:支持条件判断、循环等复杂逻辑。
2.使用 Redlock 算法,适用于多节点 Redis 集群
- 获取当前时间(T1)。
- 依次向 N 个 Redis 节点请求锁,使用相同的
lock_key
和unique_value
,并设置相同的过期时间。 - 计算获取锁的时间(T2 - T1),如果大多数节点(N/2 + 1)成功获取锁,且总时间小于锁的过期时间,则认为锁获取成功。
- 如果获取失败,向所有节点发送释放锁的请求。
3.使用Redisson库
Redisson可重入锁原理:即一个线程可以多次获取锁。利用Hash结构,记录获取锁的线程和获取锁的次数
获取锁的逻辑:判断 key(unique_value一般是业务唯一标识) 是否存在,即判断是否有线程持有锁
- 如果key不存在,代表没有线程持有锁,则获取锁,即设置一个Hash结构的 (field,value),field是机器码+线程ID,value是 次数 1。
- 如果key存在,代表有线程持有锁,此时判断持有锁的线程是否是当前线程,如果是当前线程,则重入次数+1。
释放锁的逻辑:重入次数-1,如果重入次数为0,删除key,释放锁
超时续约机制(看门狗机制)
为了解决TTL不好设置,如果分布式锁的key设置了TTL,又因为业务阻塞原因,导致当前线程的业务未完成,TTL到期了,导致被迫释放锁,此时就会有其他线程抢占锁,导致有线程并行执行,违背了加分布式锁的初心。
当 Redisson 获取锁没有指定锁的TTL时,默认会使用超时续约机制(看门狗WatchDog)机制。
续期规则:
- 默认 TTL 为 30 秒,后台线程(定时任务)每隔 10 秒 检查业务是否完成。
- 若未完成 → 通过 Lua 脚本重置锁 TTL(续期至 30 秒),保证锁不被释放,等到业务执行完。
Redisson分布式锁的数据未同步导致线程安全
主从问题:
- 主节点宕机时,若从节点未同步 锁 数据 → 新主节点可能允许其他线程重复加锁 → 锁失效。
解决方案:
- 此时需要部署多个 Redis 主节点,多主多从架构,向多个主节点都去获取锁
- MultiLock 联锁方案:要求 所有主节点加锁成功,否则立即失败并释放已获取的锁
- RedLock 红锁方案:要求 半数主节点以上(如 3/5)加锁成功,否则立即失败并释放已获取的锁。
布隆过滤器
布隆过滤器主要用于判断一个元素是否可能属于某个集合,而不支持直接获取集合中的所有元素。其基本结构是一个固定长度的位数组和一组(多个)哈希函数。
- 位数组:长度为 m,初始时所有位都设为 0。注意:不是二进制数组,是排序数组
- k 个哈希函数:每个哈希函数 hi(x) 计算元素 x 的哈希值,并将结果映射到 [0, m-1] 之间的某个位置。
原理实现
插入示例(m=10,k=3,插入 A):
- 初始状态: [0 0 0 0 0 0 0 0 0 0]===>对应0,1,2,3,4--..--9
- 哈希计算: h1(A) = 2, h2(A) = 5, h3(A) = 7
- 插入后: [0 0 1 0 0 1 0 1 0 0]
查询示例(查询B)
- 当前位数组: [0 0 1 0 0 1 0 1 0 0]
- 哈希计算: h1(B) = 3, h2(B) = 6, h3(B) = 9
- 索引位置: 3, 6, 9 (0, 0, 0) -> B一定不存在
如果所有位置都是
1
,说明 B 可能存在(但不一定)。如果有任意一个位置是
0
,说明 B 一定不存在。
删除元素
标准布隆过滤器不支持删除,因为多个元素可能共享相同的位,无法安全地清除某个元素的标记。
改进方法:
-
计数布隆过滤器(Counting Bloom Filter):用整数数组代替位数组,每个位置的值是一个计数器,插入时
+1
,删除时-1
删除A元素
- 当前计数器: [0 0 1 1 0 1 0 1 0 0]
- 哈希计算: h1(A) = 2, h2(A) = 5, h3(A) = 7
- 哈希计算: h1(B) = 3, h2(B) = 5, h3(B) = 7
- 删除后: [0 0 0 1 0 1 0 1 0 0 0] (如果 A 是唯一的)
会误判不漏判
为什么会误判,主要是哈希碰撞和无法删除元素。
- 误判:可能会错误地认为一个不存在的元素存在(假阳性)。
- 不漏判:如果布隆过滤器认为元素不存在,则一定不存在。
Redis扩容
1.哈希表扩容问题
1. 阻塞 / 延迟增加
- Redis 在进行 rehash(扩容或缩容)时会将键从旧表搬运到新表。
- 虽然 Redis 使用 渐进式 rehash,但如果数据量大、QPS 高,仍可能出现瞬时延迟。
2. 内存瞬时飙升
- 扩容时 Redis 会 同时维护两个哈希表(ht[0], ht[1]),在未完全 rehash 前占用双倍内存。
- 如果内存紧张,容易触发 OOM。
3. 并发操作复杂度上升
- 在 rehash 过程中,所有读写操作必须同时查询
ht[0]
和ht[1]
。 - 会增加 CPU 消耗和代码复杂度
2.Redis集群扩容问题
1. 数据迁移导致阻塞
- 使用
reshard
或cluster addslots
时,需要从旧节点迁移部分数据到新节点。 - 如果量大,会阻塞或降低性能。
2. 槽(slot)不均衡
- Redis Cluster 采用 16384 个哈希槽分片,扩容后槽分配不合理会导致负载不均衡。
3. 迁移失败/数据丢失
- 网络异常、节点故障可能导致
migrate
命令失败,甚至数据不一致。
Rehash
Rehash 是 Redis 在 哈希结构(Hash)扩容或缩容 时的一种机制,本质上是为了保持高性能的数据访问。它适用于 Redis 的底层数据结构,比如 dict
(哈希表),尤其是在存储大量键值对时。
哈希表的 负载因子(元素个数 / 哈希桶个数)超过一定阈值时,会触发扩容:
- 默认情况:负载因子 > 1,且不在执行 bgsave/bgrewriteaof
- 强制情况:负载因子 > 5 时立即扩容(即使正在 bgsave)
1. 有一个初始容量为 4 的哈希表
2. 插入 30 个 key,Redis 开始渐进 rehash
3. 但是请求量暴增,根本来不及迁移
4. 这时 ht[0] 仍保有 30 个元素,数组长度为 4
5. 负载因子 = 30 / 4 = 7.5
- 创建一个 更大(或更小)容量的哈希表
ht[1]
。 - 开始将
ht[0]
中的键值对迁移到ht[1]
。 - Redis 并不一次性迁移全部键值对,而是采用 渐进式 Rehash:
rehashidx
表示已经迁移到的索引位置:每次操作,搬运ht[0][rehashidx]
位置上的所有键值对到ht[1]
,然后rehashidx++
- 所有 key 搬完后,
ht[1]
替代ht[0]
,rehash 完成。
一致性哈希
是一种分布式系统中的哈希算法,用于解决数据在多台机器间分布存储时的负载均衡问题,尤其在节点频繁增删的场景下也能保持数据尽可能稳定。
为什么需要一致性哈希
传统哈希方式的问题是:
- 节点数量 N 变化时,几乎所有 key 的映射都会改变
- 比如从 3 个节点增加到 4 个,会造成约 75% 的 key 映射位置变化 → 数据迁移代价大!
一致性哈希解决的就是这个问题:
✅ 当节点数发生变化时,只有少量 key 的映射会受到影响,从而减少数据迁移。
核心思想
1. 将 key 和 节点都映射到一个哈希环(0 ~ 2³²)上
2. key 沿顺时针方向找最近的节点处理
- 比如
hash(key1)
落在节点 A 和 B 之间,则交给 B 处理 - 如果 B 挂了,key1 会被 A 处理,迁移量小
虚拟节点
一致性哈希可能会因节点分布不均导致某些节点负载过高。为了解决这个问题,引入了虚拟节点:
- 每个物理节点映射多个“虚拟节点”在环上
- key 还是通过 hash 找到“最近的虚拟节点”
- 虚拟节点再映射回原始物理节点
✅ 平衡负载
✅ 减少某个节点突然成为热点的问题