Redis
Redis
认识Redis
什么是Redis?
Redis 是一种基于内存的数据库,对数据的读写操作都是在内存中完成,因此读写速度非常快,常用于缓存,消息队列、分布式锁等场景。
Redis 和 Memcached 有什么区别?
- Redis 支持的数据类型更丰富(String、Hash、List、Set、ZSet),而 Memcached 只支持最简单的 key-value 数据类型;
- Redis 支持数据的持久化,可以将内存中的数据保持在磁盘中,重启的时候可以再次加载进行使用,而 Memcached 没有持久化功能,数据全部存在内存之中,Memcached 重启或者挂掉后,数据就没了;
- Redis 原生支持集群模式,Memcached 没有原生的集群模式,需要依靠客户端来实现往集群中分片写入数据;
- Redis 支持发布订阅模型、Lua 脚本、事务等功能,而 Memcached 不支持;
为什么用 Redis 作为 MySQL 的缓存?
主要是因为 Redis 具备「高性能」和「高并发」两种特性。
Redis数据结构
Redis 数据类型?
常见的有五种数据类型:String(字符串),Hash(哈希),List(列表),Set(集合)、Zset(有序集合)。
随着 Redis 版本的更新,后面又支持了四种数据类型: BitMap(2.2 版新增)、HyperLogLog(2.8 版新增)、GEO(3.2 版新增)、Stream(5.0 版新增)
各个数据类型使用场景?
- String 类型的应用场景:缓存对象、常规计数、分布式锁、共享 session 信息等。底层数据结构是由int和SDS简单动态字符串
- List 类型的应用场景:消息队列(但是有两个问题:1. 生产者需要自行实现全局唯一 ID;2. 不能以消费组形式消费数据)等。底层数据结构是双向链表和压缩列表实现,3.2之后用quicklist。
- Hash 类型:缓存对象、购物车等。用压缩列表和哈希表实现,7.0后用listpack和实现。
- Set 类型:聚合计算(并集、交集、差集)场景,比如点赞、共同关注、抽奖活动等。使用哈希表或整数集合实现。
- Zset 类型:排序场景,比如排行榜、电话和姓名排序等。底层:哈希表+跳表,7.0后转为listpack或跳表。
- BitMap(2.2 版新增):二值状态统计的场景,比如签到、判断用户登陆状态、连续签到用户总数等;
- HyperLogLog(2.8 版新增):海量数据基数统计的场景,比如百万级网页 UV 计数等;
- GEO(3.2 版新增):存储地理位置信息的场景,比如滴滴叫车;
- Stream(5.0 版新增):消息队列,相比于基于 List 类型实现的消息队列,有这两个特有的特性:自动生成全局唯一消息ID,支持以消费组形式消费数据。
五种常见的 Redis 数据类型是怎么实现?
Redis线程模型
Redis 是单线程吗?
Redis 单线程指的是「接收客户端请求->解析请求 ->进行数据读写等操作->发送数据给客户端」这个过程是由一个线程(主线程)来完成的,这也是我们常说 Redis 是单线程的原因。
但是,Redis 程序并不是单线程的,Redis 在启动的时候,是会启动后台线程(BIO)来完成「关闭文件、AOF 刷盘、释放内存」这些任务。
Redis 单线程模式是怎样的?
Redis 采用单线程为什么还这么快?
- Redis 的大部分操作都在内存中完成,并且采用了高效的数据结构。
- Redis 采用单线程模型可以避免了多线程之间的竞争,省去了多线程切换带来的时间和性能上的开销,而且也不会导致死锁问题。
- Redis 采用了 I/O 多路复用机制处理大量的客户端 Socket 请求,IO 多路复用机制是指一个线程处理多个 IO 流,就是我们经常听到的 select/epoll 机制。
Redis 6.0 之前为什么使用单线程?
CPU 并不是制约 Redis 性能表现的瓶颈所在,更多情况下是受到内存大小和网络I/O的限制
多线程模型虽然在某些方面表现优异,但是它却引入了程序执行顺序的不确定性,带来了并发读写的一系列问题,增加了系统复杂度、同时可能存在线程切换、甚至加锁解锁、死锁造成的性能损耗
Redis 6.0 之后为什么引入了多线程?
在 Redis 6.0 版本之后,也采用了多个 I/O 线程来处理网络请求,这是因为随着网络硬件的性能提升,Redis 的性能瓶颈有时会出现在网络 I/O 的处理上。
所以为了提高网络 I/O 的并行度,Redis 6.0 对于网络 I/O 采用多线程来处理。但是对于命令的执行,Redis 仍然使用单线程来处理,所以大家不要误解 Redis 有多线程同时执行命令。
Redis持久化
Redis 如何实现数据不丢失?
Redis 共有三种数据持久化的方式:
- AOF 日志:每执行一条写操作命令,就把该命令以追加的方式写入到一个文件里;
- RDB 快照:将某一时刻的内存数据,以二进制的方式写入磁盘;
- 混合持久化方式:Redis 4.0 新增的方式,集成了 AOF 和 RBD 的优点;
RDB全称Redis Database Backup file (Redis数据备份文件),也被叫做Redis数据快照。简单来说就是把内存中的所有数据都记录到磁盘中。当Redis实例故障重启后,从磁盘读取快照文件,恢复数据
AOF全称为Append Only File (追加文件)。Redis处理的每一个写命令都会记录在AOF文件,可以看做是命令日志文件。
AOF 日志是如何实现的?
为什么先执行命令,再把数据写入日志呢?
避免额外的检查开销
不会阻塞当前写操作命令的执行
产生问题
- 数据可能会丢失
- 可能阻塞其他操作
AOF 写回策略有几种?
AOF 日志过大,会触发什么机制?
Redis 为了避免 AOF 文件越写越大,提供了 AOF 重写机制,当 AOF 文件的大小超过所设定的阈值后,Redis 就会启用 AOF 重写机制,来压缩 AOF 文件。
但是在使用重写机制后,就会读取 name 最新的 value(键值对) ,然后用一条命令记录到新的 AOF 文件。
RDB 快照是如何实现的呢?
RDB 快照就是记录某一个瞬间的内存数据,记录的是实际数据,而 AOF 文件记录的是命令操作的日志,而不是实际的数据。
因此在 Redis 恢复数据时, RDB 恢复数据的效率会比 AOF 高些,因为直接将 RDB 文件读入内存就可以,不需要像 AOF 那样还需要额外执行操作命令的步骤才能恢复数据。
RDB 做快照时会阻塞线程吗?
分别是 save 和 bgsave,他们的区别就在于是否在「主线程」里执行:
- 执行了 save 命令,就会在主线程生成 RDB 文件,由于和执行操作命令在同一个线程,所以如果写入 RDB 文件的时间太长,会阻塞主线程;
- 执行了 bgsave 命令,会创建一个子进程来生成 RDB 文件,这样可以避免主线程的阻塞;
RDB 在执行快照的时候,数据能修改吗?
可以的,执行 bgsave 过程中,Redis 依然可以继续处理操作命令的,也就是数据是能被修改的,关键的技术就在于写时复制技术(Copy-On-Write, COW)。
执行 bgsave 命令的时候,会通过 fork() 创建子进程,此时子进程和父进程是共享同一片内存数据的,因为创建子进程的时候,会复制父进程的页表,但是页表指向的物理内存还是一个,此时如果主线程执行读操作,则主线程和 bgsave 子进程互相不影响。
为什么会有混合持久化?
RDB 优点是数据恢复速度快,但是快照的频率不好把握。频率太低,丢失的数据就会比较多,频率太高,就会影响性能。
AOF 优点是丢失数据少,但是数据恢复不快。
混合持久化,AOF 文件的前半部分是 RDB 格式的全量数据,后半部分是 AOF 格式的增量数据。
Redis集群
Redis 如何实现服务高可用?
主从复制
单节点Redis的并发能力是有上限的,要进一步提高Redis的并发能力,就需要搭建主从集群,实现读写分离。般都是一主多从,主节点
负责写数据,从节点负责读数据。
全量同步:
1.从节点请求主节点同步数据(replication id、offset)
2.主节点判断是否是第一次请求,是第一次就与从节点同步版本信息(replication id和offset
3.主节点执行bgsave,生成rdb文件后,发送给从节点去执
4.在rdb生成执行期间,主节点会以命令的方式记录到缓冲区(一个日志文件
5.把生成之后的命令日志文件发送给从节点进行同步。
增量同步: 1.从节点请求主节点同步数据,主节点判断不是第一次请求,不是第一次就获取从节点的offset值
2.主节点从命令日志中获取offset值之后的数据,发送给从节点进行数据同步
哨兵模式
Redis提供了哨兵(Sentinel)机制来实现主从集群的自动故障恢复。哨兵的结构和作用如下:
监控:Sentinel会不断检查您的master和slave是否按预期工作
自动故障恢复:如果master故障,Sentinel会将一个slave提升为master。当故障实例恢复后也以新的master为主
通知:Sentinel充当Redis客户端的服务发现来源,当集群发生故障转移时,会将最新信息推送给Redis的客户端
哨兵监测方式
Sentinel基于心跳机制监测服务状态,每隔1秒向集群的每个实例发送ping命令:
主观下线:如果某sentinel节点发现某实例未在规定时间响应,则认为该实例主观下线
客观下线:若超过指定数量(quorum)的sentinel都认为该实例主观下线,则该实例客观下线。quorum值最好超过Sentinel实例数量的一半。
哨兵选主规则
首先判断主与从节点断开时间长短,如超过指定值就排该从节点
然后判断从节点的slave-priority值,越小优先级越高
如果slave-prority一样,则判断slave节点的offset值,越大优先级越高
最后是判断slave节点的运行id大小,越小优先级越高
分片集群
用多个redis作为写的主节点,每个写的节点可以有多个读的从节点。
使用分片集群可以解决上述问题,分片集群特征
集群中有多个master,每个master保存不同数据
每个master都可以有多个slave节点
master之间通过ping监测彼此健康状态
客户端请求可以访问集群任意节点,最终都会被转发到正确节点
分片集群的写入原理:
Redis分片集群引入了哈希槽的概念,Redis 集群有 16384个哈希槽每个key通过CRC16校验后对16384取模来决定放置哪个槽,集群的每个节点负责一部分hash槽。
集群脑裂导致数据丢失怎么办?
什么是脑裂?
由于网络问题,集群节点之间失去联系。主从数据不同步;重新平衡选举,产生两个主服务。等网络恢复,旧主节点会降级为从节点,再与新主节点进行同步复制的时候,由于会从节点会清空自己的缓冲区,所以导致之前客户端写入的数据丢失了。
解决方案
当主节点发现从节点下线或者通信超时的总数量小于阈值时,那么禁止主节点进行写数据,直接把错误返回给客户端。
在 Redis 的配置文件中有两个参数我们可以设置:
- min-slaves-to-write x,主节点必须要有至少 x 个从节点连接,如果小于这个数,主节点会禁止写数据。
- min-slaves-max-lag x,主从数据复制和同步的延迟不能超过 x 秒,如果超过,主节点会禁止写数据。
Redis 过期删除与内存淘汰
Redis 使用的过期删除策略是什么?
Redis 使用的过期删除策略是「惰性删除+定期删除」这两种策略配和使用。
什么是惰性删除策略?
不主动删除过期键,每次从数据库访问 key 时,都检测 key 是否过期,如果过期则删除该 key。
惰性删除策略的优点:
- 此策略只会使用很少的系统资源,因此,惰性删除策略对 CPU 时间最友好。
惰性删除策略的缺点:
- 它所占用的内存就不会释放,造成了一定的内存空间浪费。所以,惰性删除策略对内存不友好。
什么是定期删除策略?
每隔一段时间「随机」从数据库中取出一定数量的 key 进行检查,并删除其中的过期key。
Redis 的定期删除的流程:
- 从过期字典中随机抽取 20 个 key;
- 检查这 20 个 key 是否过期,并删除已过期的 key;
- 如果本轮检查的已过期 key 的数量,超过 5 个(20/4),也就是「已过期 key 的数量」占比「随机抽取 key 的数量」大于 25%,则继续重复步骤 1;如果已过期的 key 比例小于 25%,则停止继续删除过期 key,然后等待下一轮再检查。
Redis 持久化时,对过期键会如何处理的?
RDB 文件分为两个阶段,RDB 文件生成阶段和加载阶段。
- RDB 文件生成阶段:从内存状态持久化成 RDB(文件)的时候,会对 key 进行过期检查,过期的键「不会」被保存到新的 RDB 文件中,因此 Redis 中的过期键不会对生成新 RDB 文件产生任何影响。
- RDB 加载阶段:RDB 加载阶段时,要看服务器是主服务器还是从服务器,分别对应以下两种情况:
- 如果 Redis 是「主服务器」运行模式的话,在载入 RDB 文件时,程序会对文件中保存的键进行检查,过期键「不会」被载入到数据库中。
- 如果 Redis 是「从服务器」运行模式的话,在载入 RDB 文件时,不论键是否过期都会被载入到数据库中。
AOF 文件分为两个阶段,AOF 文件写入阶段和 AOF 重写阶段。
- AOF 文件写入阶段:当 Redis 以 AOF 模式持久化时,如果数据库某个过期键还没被删除,那么 AOF 文件会保留此过期键,当此过期键被删除后,Redis 会向 AOF 文件追加一条 DEL 命令来显式地删除该键值。
- AOF 重写阶段:执行 AOF 重写时,会对 Redis 中的键值对进行检查,已过期的键不会被保存到重写后的 AOF 文件中。
Redis 主从模式中,对过期键会如何处理?
从库不会进行过期扫描,从库对过期的处理是被动的。主库在 key 到期时,会在 AOF 文件里增加一条 del 指令,同步到所有的从库,从库通过执行这条 del 指令来删除过期的 key。
Redis 内存满了,会发生什么?
在 Redis 的运行内存达到了某个阀值,就会触发内存淘汰机制,这个阀值就是我们设置的最大运行内存,此值在 Redis 的配置文件中可以找到,配置项为 maxmemory。
Redis 内存淘汰策略有哪些?
在设置了过期时间的数据中进行淘汰:
- volatile-random:随机淘汰设置了过期时间的任意键值;
- volatile-ttl:优先淘汰更早过期的键值。
- volatile-lru(Redis3.0 之前,默认的内存淘汰策略):淘汰所有设置了过期时间的键值中,最久未使用的键值;
- volatile-lfu(Redis 4.0 后新增的内存淘汰策略):淘汰所有设置了过期时间的键值中,最少使用的键值;
在所有数据范围内进行淘汰:
- allkeys-random:随机淘汰任意键值;
- allkeys-lru:淘汰整个键值中最久未使用的键值;
- allkeys-lfu(Redis 4.0 后新增的内存淘汰策略):淘汰整个键值中最少使用的键值
LRU 算法和 LFU 算法有什么区别?
什么是 LRU 算法?
LRU 全称是 Least Recently Used 翻译为最近最少使用,会选择淘汰最近最少使用的数据。
传统 LRU 算法的实现是基于「链表」结构,链表中的元素按照操作顺序从前往后排列,最新操作的键会被移动到表头,当需要内存淘汰时,只需要删除链表尾部的元素即可,因为链表尾部的元素就代表最久未被使用的元素。
Redis 是如何实现 LRU 算法的?
Redis 实现的是一种近似 LRU 算法,目的是为了更好的节约内存,它的实现方式是在 Redis 的对象结构体中添加一个额外的字段,用于记录此数据的最后一次访问时间。
什么是 LFU 算法?
LFU 全称是 Least Frequently Used 翻译为最近最不常用的,LFU 算法是根据数据访问次数来淘汰数据的,它的核心思想是“如果数据过去被访问多次,那么将来被访问的频率也更高”。
Redis 是如何实现 LFU 算法的?
LFU 算法相比于 LRU 算法的实现,多记录了「数据的访问频次」的信息。Redis 对象的结构如下:
高 16bit 存储 ,用来记录 key 的访问时间戳;低 8bit 存储 ,用来记录 key 的访问频次。
Redis缓存
如何避免缓存雪崩、缓存击穿、缓存穿透?
如何避免缓存雪崩?
缓存雪崩是指在同一时段大量的缓存key同时失效或者Redis服务宕机,导致大量请求到达数据库,带来巨大压力。
解决方案:
- 给不同的Key的TTL添加随机值
- 利用Redis集群提高服务 的可用性哨兵模式、集群模式
- 给缓存业务添加降级限流策略 ngxin或spring cloud gateway
- 给业务添加多级缓存
如何避免缓存击穿?
缓存击穿:给某一个key设置了过期时间,当key过期的时候,恰好这时间点对这个key有大量的并发请求过来,这些并发的请求可能会瞬间把DB压垮。
解决方案一:互斥锁,强一致,性能差 解决方案二:不给热点数据设置过期时间,由后台异步更新缓存,或者在热点数据准备要过期前,提前通知后台线程更新缓存以及重新设置过期时间;高可用,性能优,不能保证数据绝对一致。
如何避免缓存穿透?
缓存穿透:查询一个不存在的数据,mysql查询不到数据也不会直接写入缓存,就会导致每次请求都查数据库。
缓存空数据,优点:简单,缺点:消耗内存,可能发生不一致。
布隆过滤器,优点:占用内存少,没有多余key,缺点:实现复杂,存在误判。
非法请求的限制:API 入口处我们要判断求请求参数是否合理,请求参数是否含有非法值、请求字段是否存在。
缓存空数据
布隆过滤器
如何设计一个缓存策略,可以动态缓存热点数据呢?
热点数据动态缓存的策略总体思路:通过数据最新访问时间来做排名,并过滤掉不常访问的数据,只留下经常访问的数据。
说说常见的缓存更新策略?
常见的缓存更新策略共有3种:
- Cache Aside(旁路缓存)策略;
- Read/Write Through(读穿 / 写穿)策略;
- Write Back(写回)策略;
Cache Aside(旁路缓存)策略
Cache Aside(旁路缓存)策略是最常用的,应用程序直接与「数据库、缓存」交互,并负责对缓存的维护,该策略又可以细分为「读策略」和「写策略」。
Cache Aside 策略适合读多写少的场景,不适合写多的场景
Redis实战
Redis 如何实现延迟队列?
延迟队列是指把当前要做的事情,往后推迟一段时间再做。延迟队列的常见使用场景有以下几种:
- 在淘宝、京东等购物平台上下单,超过一定时间未付款,订单会自动取消;
- 打车的时候,在规定时间没有车主接单,平台会取消你的单并提醒你暂时没有车主接单;
- 点外卖的时候,如果商家在10分钟还没接单,就会自动取消订单;
在 Redis 可以使用有序集合(ZSet)的方式来实现延迟消息队列的,ZSet 有一个 Score 属性可以用来存储延迟执行的时间。
使用 zadd score1 value1 命令就可以一直往内存中生产消息。再利用 zrangebysocre 查询符合条件的所有待处理的任务, 通过循环执行队列任务即可。
Redis 的大 key 如何处理?
如何找到大 key ?
1、redis-cli --bigkeys 查找大key
redis-cli -h 127.0.0.1 -p6379 -a "password" -- bigkeys
注意事项:
- 主节点上执行时,会阻塞主节点;
- 没有从节点,那么可以选择在 Redis 实例业务压力的低峰阶段进行扫描查询
不足之处:
- 只能返回每种类型中最大的那个 bigkey,无法得到大小排在前 N 位的 bigkey;
- 对于集合类型来说,只统计集合元素个数的多少,而不是实际占用的内存量
2、使用 SCAN 命令查找大 key
使用 SCAN 命令对数据库扫描,然后用 TYPE 命令获取返回的每一个 key 的类型。
3、使用 RdbTools 工具查找大 key
rdb dump.rdb -c memory --bytes 10240 -f redis.csv
如何删除大 key?
在应用程序释放内存时,操作系统需要把释放掉的内存块插入一个空闲内存块的链表。
所以,如果一下子释放了大量内存,空闲内存块链表操作时间就会增加,相应地就会造成 Redis 主线程的阻塞。
1、分批次删除
对于删除大 Hash,使用 hscan
命令,每次获取 100 个字段,再用 hdel
命令,每次删除 1 个字段。
对于删除大 List,通过 ltrim
命令,每次删除少量元素。
对于删除大 Set,使用 sscan
命令,每次扫描集合中 100 个元素,再用 srem
命令每次删除一个键。
对于删除大 ZSet,使用 zremrangebyrank
命令,每次删除 top 100个元素。
2、异步删除
可以采用异步删除法,用 unlink 命令代替 del 来删除。
这样 Redis 会将这个 key 放入到一个异步线程中进行删除,这样不会阻塞主线程。
除了主动调用 unlink 命令实现异步删除之外,我们还可以通过配置参数,达到某些条件的时候自动进行异步删除。
Redis 管道有什么用?
管道技术是客户端提供的一种批处理技术,用于一次处理多个 Redis 命令,从而提高整个交互的性能。
管道技术可以解决多个命令执行时的网络等待,它是把多个命令整合到一起发送给服务器端处理之后统一返回给客户端,这样就免去了每条命令执行后都要等待的情况,从而有效地提高了程序的执行效率。
Redis 事务支持回滚吗?
Redis 中并没有提供回滚机制,虽然 Redis 提供了 DISCARD 命令,但是这个命令只能用来主动放弃事务执行,把暂存的命令队列清空。
如何用 Redis 实现分布式锁的?
Redis 的 SET 命令有个 NX 参数可以实现「key不存在才插入」,所以可以用它来实现分布式锁:
- 如果 key 不存在,则显示插入成功,可以用来表示加锁成功;
- 如果 key 存在,则会显示插入失败,可以用来表示加锁失败。
双写一致性
1.先删除缓存,再更新数据库
延时双删:先删缓存->更新数据库->睡眠->再删缓存
2.先更新数据库,再删缓存
- MQ作为消息队列,更新数据库之后,通知缓存删除,删除失败继续重试
- 利用canal组件,伪装从节点,从dump线程读取binlog日志删除缓存。
如果要求强一致性:
需要用分布式锁,在更新数据库和删除缓存前,加锁,更新缓存后释放锁。
延迟双删
分布式锁
内存碎片问题
- 内部原因:内存分配器的分配策略决定操作系统无法做到“按需分配”。
- Redis使用libc、jemalloc、tcmalloc多种内存分配器来分配内存,默认使用jemalloc。
- 内存分配器是按照固定大小来分配内存空间,不是完全按照应用程序申请的内存大小来分配。
- 以jemalloc为例,是按照一系列固定的大小划分内存空间,例如8字节、16字节、32字节、...、2KB、4KB等。当程序申请的内存最接近某个固定值时,jemalloc就会给它分配相应大小的空间。
- 外部原因:键值对大小不一样,并且键值对可以被修改和删除。
- Redis申请内存空间分配时,对于大小不一的内存空间需求,内存分配器按照固定大小分配内存空间,分配的内存空间一般都会比申请的内存空间大一些,这会产生一定的内存碎片。
- 键值对会被修改和删除,会导致空间的扩容和释放。
如何判断Redis是否有内存碎片?
Redis提供的INFO命令,查询内存使用的详细信息
INFO memory
# Memory
used_memory:350458970752
used_memory_human:326.39G
used_memory_rss:349066919936
used_memory_rss_human:325.09G
…
mem_fragmentation_ratio:1.00
- used_memory:表示Redis为了保存数据实际申请使用的内存空间。
- used_memory_rss:表示操作系统实际分配给Redis的物理内存空间,其中包含了内存空间碎片。
- mem_fragmentation_ratio:表示Redis当前的内存碎片率。计算公式:mem_fragmentation_ratio=used_memory_rss/used_memory
- mem_fragmentation_ratio大于等于1但小于等于1.5,这种情况是合理的。
- mem_fragmentation_ratio大于1.5,表明内存碎片率已经超过了50%。
- mem_fragmentation_ratio小于1,表名内存不足。
如何清理内存碎片?
Redis专门为自动内存碎片清理机制提供参数设置。可以通过设置参数,来控制碎片清理的开始和结束时机,以及占用的CPU比例,从而减少碎片清理对Redis请求处理的性能影响。
首先,开启自动内存碎片清理:
config set activedefrag yes 然后,设置触发内存清理的条件:
active-defrag-ignore-bytes 100mb:表示内存碎片的字节数达到100MB时,开始清理;
active-defrag-threshold-lower 10:表示内存碎片空间占操作系统分配给Redis的总空间比例达到10%时,开始清理。
最后,控制清理操作占用CPU时间比例的上、下限:
- active-defrag-cycle-min 25: 表示自动清理过程所用CPU时间的比例不低于25%,保证清理能正常开展;
- active-defrag-cycle-max 75:表示自动清理过程所用CPU时间的比例不高于75%,一旦超过,就停止清理,从而避免在清理时,大量的内存拷贝阻塞Redis,导致响应延迟升高。