START
Basic
Redis 和 Memcached 有什么区别?
Back:
Redis 与 Memcached 区别:
- Redis 支持的数据类型更丰富(String、Hash、List、Set、ZSet),而 Memcached 只支持最简单的 key-value 数据类型;
- Redis 支持数据的持久化,可以将内存中的数据保持在磁盘中,重启的时候可以再次加载进行使用,而 Memcached 没有持久化功能,数据全部存在内存之中,Memcached 重启或者挂掉后,数据就没了;
- Redis 原生支持集群模式,Memcached 没有原生的集群模式,需要依靠客户端来实现往集群中分片写入数据;
- Redis 支持发布订阅模型、Lua 脚本、事务等功能,而 Memcached 不支持;
END
START
Basic
五种常见的 Redis 数据类型是怎么实现的?
Back:
END
START
Basic
Redis 的单线程模型?
Back:
Redis 单线程指的是「接收客户端请求->解析请求 ->进行数据读写等操作->发送数据给客户端」这个过程是由一个线程(主线程)来完成的,这也是我们常说 Redis 是单线程的原因。
但是,Redis 程序并不是单线程的,Redis 在启动的时候,是会启动后台线程(BIO)的
- 关闭文件
- AOF 刷盘
- 释放内存
之所以 Redis 采用单线程(网络 I/O 和执行命令)那么快,有如下几个原因:
- Redis 的大部分操作都在内存中完成,并且采用了高效的数据结构,因此 Redis 瓶颈可能是机器的内存或者网络带宽,而并非 CPU,既然 CPU 不是瓶颈,那么自然就采用单线程的解决方案了;
- Redis 采用单线程模型可以避免了多线程之间的竞争,省去了多线程切换带来的时间和性能上的开销,而且也不会导致死锁问题。
- Redis 采用了 I/O 多路复用机制处理大量的客户端 Socket 请求,IO 多路复用机制是指一个线程处理多个 IO 流,就是我们经常听到的 select/epoll 机制。简单来说,在 Redis 只运行单线程的情况下,该机制允许内核中,同时存在多个监听 Socket 和已连接 Socket。内核会一直监听这些 Socket 上的连接请求或数据请求。一旦有请求到达,就会交给 Redis 线程处理,这就实现了一个 Redis 线程处理多个 IO 流的效果。
END
START
Basic
Redis 如何实现数据不丢失?
Back:
Redis 共有三种数据持久化的方式:
- AOF 日志:每执行一条写操作命令,就把该命令以追加的方式写入到一个文件里;
- Redis 是先执行写,然后才将该命令记录到 AOF 日志里的,这就不是传统意义上的【WAF】了
- Redis 有三种写回硬盘的策略:
- Always
- EverySec:先写到 Page Cache 隔一段时间写回硬盘
- No:交给操作系统控制写回的时机
- AOF 有【AOF 重写】机制,即整理 Compaction 后形成新的 AOF 文件(由后台子进程完成)
- 有一个 【AOF 重写缓冲区】来承接 AOF 重写阶段不一致的写,当 AOF 重写完成时,主进程再将重写缓冲区中的数据追加到新的 AOF 文件中
- RDB 快照:将某一时刻的内存数据,以二进制的方式写入磁盘;
- Redis 的快照是全量快照
- save 命令会阻塞主线程进行快照,bgsave 则会创建一个子线程来实现快照
- 混合持久化方式:Redis 4.0 新增的方式,集成了 AOF 和 RBD 的优点;
- AOF 文件的前半部分是 RDB 格式的全量数据,后半部分是 AOF 格式的增量数据
END
- AOF 文件的前半部分是 RDB 格式的全量数据,后半部分是 AOF 格式的增量数据
START
Basic
Redis 使用的过期删除策略是什么?
Back:
当我们查询一个 key 时,Redis 首先检查该 key 是否存在于过期字典中:
- 如果不在,则正常读取键值;
- 如果存在,则会获取该 key 的过期时间,然后与当前系统时间进行比对,如果比系统时间大,那就没有过期,否则判定该 key 已过期。
Redis 使用的过期删除策略是「惰性删除+定期删除」这两种策略配和使用。
- 惰性删除:当访问这个 key 的时候,如果这个 key 已过期则标记并返回 null
- 定期删除:每隔一段时间「随机」从数据库中取出一定数量的 key 进行检查,并删除其中的过期key。
END
START
Basic
Redis 是怎么实现 LRU 的
Back:
Redis 实现的是一种近似 LRU 算法,目的是为了更好的节约内存,它的实现方式是在 Redis 的对象结构体中添加一个额外的字段,用于记录此数据的最后一次访问时间。
当 Redis 进行内存淘汰时,会使用随机采样的方式来淘汰数据,它是随机取 5 个值(此值可配置),然后淘汰最久没有使用的那个。
Redis 实现的 LRU 算法的优点:
- 不用为所有的数据维护一个大链表,节省了空间占用;
- 不用在每次数据访问时都移动链表项,提升了缓存的性能;
END
START
Basic
Redis 如何避免缓存雪崩、缓存击穿、缓存穿透?
Back:
通常会给缓存中的数据项设置过期时间,过期时间一到无论如何都要去存储层取数据,如果过期时间过于相近,就会在某一个时刻同时出现大量缓存失效,这就是缓存雪崩
我们可以将过期时间打散来解决缓存雪崩的问题
缓存击穿是指某一热点数据的缓存失效了,无法从缓存中读取,直接访问数据库,数据库很容易就被高并发的请求冲垮
- 互斥锁方案,保证同一时间只有一个业务访问缓存
- 不给热点数据设置过期时间,由后台异步刷新缓存
缓存穿透是指大量访问既不在缓存,也不在数据库中的数据项,也会引起频繁的数据库访问
- 限制非法请求
- 设置空值或默认值
- 使用布隆过滤器
END
START
Basic
Redis 是否支持事物回滚?
Back:
Redis 中并没有提供回滚机制,虽然 Redis 提供了 DISCARD 命令,但是这个命令只能用来主动放弃事务执行,把暂存的命令队列清空,起不到回滚的效果,事务中执行时的错误会被忽略,但是正常执行了的语句无法回滚。
END
START
Basic
Redis 如何实现分布式锁?
Back:
Redis 的 SET
命令有个 NX
参数可以实现「key不存在才插入」,所以可以用它来实现分布式锁:
- 如果 key 不存在,则显示插入成功,可以用来表示加锁成功;
- 如果 key 存在,则会显示插入失败,可以用来表示加锁失败。
- 锁变量需要设置过期时间,以免客户端拿到锁后发生异常,导致锁一直无法释放,所以,我们在 SET 命令执行时加上
EX/PX
选项,设置其过期时间 - 解锁的过程就是将 lock_key 键删除(
del lock_key
),但不能乱删,要保证执行操作的客户端就是加锁的客户端。所以,解锁的时候,我们要先判断锁的 unique_value 是否为加锁客户端,是的话,才将 lock_key 键删除。
END
START
Basic
Redis 如何解决集群情况下分布式锁的可靠性?
Back:
Redis 主从复制模式中的数据是异步复制的,这样导致分布式锁的不可靠性。如果在 Redis 主节点获取到锁后,在没有同步到其他节点时,Redis 主节点宕机了,此时新的 Redis 主节点依然可以获取锁,所以多个应用服务就可以同时获取到锁。
为了保证集群环境下分布式锁的可靠性,Redis 官方已经设计了一个分布式锁算法 Redlock(红锁)。
Redlock 算法的基本思路,是让客户端和多个独立的 Redis 节点依次请求申请加锁,如果客户端能够和半数以上的节点成功地完成加锁操作,那么我们就认为,客户端成功地获得分布式锁,否则加锁失败。
Redlock 算法加锁三个过程(需要在锁过期的时间内获取到超半数的 Redis 节点上的锁):
- 第一步是,客户端获取当前时间(t1)。
- 第二步是,客户端按顺序依次向 N 个 Redis 节点执行加锁操作:
- 加锁操作使用 SET 命令,带上 NX,EX/PX 选项,以及带上客户端的唯一标识。
- 如果某个 Redis 节点发生故障了,为了保证在这种情况下,Redlock 算法能够继续运行,我们需要给「加锁操作」设置一个超时时间(不是对「锁」设置超时时间,而是对「加锁操作」设置超时时间),加锁操作的超时时间需要远远地小于锁的过期时间,一般也就是设置为几十毫秒。
- 第三步是,一旦客户端从超过半数(大于等于 N/2+1)的 Redis 节点上成功获取到了锁,就再次获取当前时间(t2),然后计算计算整个加锁过程的总耗时(t2-t1)。如果 t2-t1 < 锁的过期时间,此时,认为客户端加锁成功,否则认为加锁失败。
END
START
Basic
Redis 大 key 对系统的影响
Back:
当 AOF 写回策略配置了 Always 策略,如果写入是一个大 Key,主线程在执行 fsync() 函数的时候,阻塞的时间会比较久,因为当写入的数据量很大的时候,数据同步到硬盘这个过程是很耗时的。
AOF 重写机制和 RDB 快照(bgsave 命令)的过程,都会分别通过 fork()
函数创建一个子进程来处理任务。会有两个阶段会导致阻塞父进程(主线程):
- 创建子进程的途中,由于要复制父进程的页表等数据结构,阻塞的时间跟页表的大小有关,页表越大,阻塞的时间也越长;
- 创建完子进程后,如果父进程修改了共享数据中的大 Key,就会发生写时复制,这期间会拷贝物理内存,由于大 Key 占用的物理内存会很大,那么在复制物理内存这一过程,就会比较耗时,所以有可能会阻塞父进程。
大 key 除了会影响持久化之外,还会有以下的影响:
-
客户端超时阻塞。由于 Redis 执行命令是单线程处理,然后在操作大 key 时会比较耗时,那么就会阻塞 Redis,从客户端这一视角看,就是很久很久都没有响应。
-
引发网络阻塞。每次获取大 key 产生的网络流量较大,如果一个 key 的大小是 1 MB,每秒访问量为 1000,那么每秒会产生 1000MB 的流量,这对于普通千兆网卡的服务器来说是灾难性的。
-
阻塞工作线程。如果使用 del 删除大 key 时,会阻塞工作线程,这样就没办法处理后续的命令。
-
内存分布不均。集群模型在 slot 分片均匀情况下,会出现数据和查询倾斜情况,部分有大 key 的 Redis 节点占用内存多。
如何避免大 Key 呢?
最好在设计阶段,就把大 key 拆分成一个一个小 key。或者,定时检查 Redis 是否存在大 key ,如果该大 key 是可以删除的,不要使用 DEL 命令删除,因为该命令删除过程会阻塞主线程,而是用 unlink 命令(Redis 4.0+)删除大 key,因为该命令的删除过程是异步的,不会阻塞主线程。
END