一、基础
1 什么是Redis
- redis是一个将数据存储在内存中的数据库,读写速度非常快
- redis支持数据的持久化,可以将内存中的数据保存到磁盘中,重启的时候可以再次加载进行使用
- redis不仅支持kv类型的数据,还支持list,set,zset,hash的存储
- redis支持多种数据备份,主从哨兵集群
2 Redis为什么这么快
- redis完全基于内存
- redis有高效的事件处理模型,主要是单线程事件循环和IO多路复用
- 单线程可以避免了上下文切换和竞争,不需要锁
- 是多路IO模型,不像阻塞IO会一直占用CPU,多路复用IO单个线程就可以同时处理多个IO请求(也就是select/epoll机制)
- redis内置了多种优化过后的数据结构实现,性能非常高
3 为什么要用Redis
Redis和,储存在磁盘中的传统数据库相比,完全基于内存,速度非常快
4C8G的MySQL只能达到四千QPS,
Redis单个分片就能达到十万QPS
3.1 redis适合的场景
- 缓存:减轻MySQL查询压力,提高性能
- 排行榜:利用redis的SortSet(有序集合)实现
- 计数器:利用Redis的自增操作,可以统计点赞数、页面访问数
- 限速器:
- 好友关系:利用集合的一些命令,求交集、并集、差集,解决共同好友、共同爱好问题
- 消息队列:用list完成异步处理,应用解耦,流量削峰和消息通讯,一般没人用
- session共享:客户请求到任何一台机器都可以获得session信息
- 分布式锁
- 支持事务,(不满足原子性和持久性)
二、内存
1 设置过期时间
- 设置过期时间减少内存消耗,绝大部分数据不需要一直保存
- 传统的数据库判断数据是否过期性能非常差
- 使用expire设置过期时间,字符串使用setex设置过期时间,persist移除一个键的过期时间,ttl查看还有多久过期
2 Redis如何判断数据是否过期
Redis通过【过期字典】判断数据是否过期
- 过期字典保存了数据库中所有键的过期时间
- 过期字典的key是指针,指向数据库的这个键
- value是long long类型,保存了键的过期时间
3 过期数据的删除策略
- 定时删除
- 在设置键的过期时间的同时,创建一个定时器,通过定时器执行对键的删除操作
- 内存友好,对CPU时间不友好
- 惰性删除
- 只有获取键的时候,才检查是否过期,过期就删除
- CPU时间友好,内存不友好
- 定期删除
- 每隔一段时间,就对数据库进行一次检查,删除其中的过期键
- 通过限制删除操作执行的时长和频率来减少删除操作对 CPU 时间的影响
4 内存淘汰机制
- volatile-lru:从已设置过期时间的数据中最少使用淘汰
- volatile-ttl:从已设置过期时间的数据中挑选将要过期的数据淘汰
- volatile-random:从已设置过期时间的数据中随机淘汰
- allkeys-lru:从全部数据中最少使用淘汰
- allkeys-random:从全部数据中任意淘汰
- 禁止驱逐数据
5 内存碎片
- redis存储数据的时候向操作系统申请的内存空间可能会大于数据实际需要的存储空间。
- 当redis中某个数据删除时,通常不会轻易释放内存给操作系统。
- info memory查到内存碎片率大于1.5需要清理碎片
- 碎片清理时,设置内存碎片清理所占用CPU时间的比例
三、性能优化
1 性能瓶颈
- 网络
- 内存
2 内存优化
- 降低key和value的大小
- 维护共享整数对象池
- 选择合适的底层存储结构
2.1 如何解决大Key的问题(影响,查看,解决)
- 定义:一般来讲string的value大于10KB的就是大key,其他类型field超过一万个
- 影响:
- 单线程,耗时增加会阻塞其他请求
- 大key导致分片内存不平衡,CPU使用率不平衡
- 查看和定位
- 看单分片监控,是否和平均一致
- bigkeys命令
- 解决:主要是业务角度做打散
- 删除机制
- 做拆分,大key变小key
- 是否key设计不合理,比如用的不是随机值而是固定值
2.2 设置ziplist和hashtable
- 区别
- ziplist是双向链表,占用空间比hashtable小,查询耗时比hashtable长
- hashtable是散列表,占用空间比ziplist大,查询耗时比hashtable短
- 使用ziplist的条件
- value字符串长度小于64(线上是8192)(并且)
- 并且field-value对的数量小于512个
- 查看底层存储的方法
OBJECT ENCODIN
3 网络优化
3.1 网络优化方法
核心:使用批量操作
- 原生命令:hmget、hmset
- 问题1:无法保证所有的 key 都在同一个hash slot,仍需要多次网络传输(总体来说还是优化)
- 问题2:批量操作使用的命令只能同一种,都是set或者都是get。
- 优点:可以保证原子操作
- pipeline:
- 问题一:需要控制一次批量操作的元素个数
- 问题二:也无法保证所有的 key 都在同一个hash slot
- 问题三:非原子操作
- 优点:可以使用多种命令
- lua:
- 问题:redis-cluster下无法保证原子性(也是哈希槽的问题)
- 优点一:支持简单逻辑处理
- 优点二:非redis-cluster可以保证原子性
- 并且一段lua执行过程中,不会有其他脚本或 Redis 命令同时执行,保证了操作不会被其他指令插入或打扰。
4 热Key
4.1 如何解决hotkey
- 读写分离:主节点处理写请求,从节点处理读请求
- 使用集群版redis:将热点数据分散到多个Redis节点
- 多级缓存:将热key存放一份到本地内存中
四、线程模型
1 单线程模型
1.1 文件事件处理器是什么
- Redis基于Reactor模式开发了自己的网络事件处理器。它使用I/O多路复用,来同时监听多个套接字,并且根据套接字目前执行的任务,来为套接字关联不同的事件处理器。
- 当被监听的套接字准备好执行连接、读取、写入、关闭的操作时,与操作相对应的文件事件就会产生。这时文件事件处理器就会调用套接字/之前关联好的事件处理器/来处理这些事件。
1.2 使用单线程的优点
- Redis的瓶颈是内存容量和网络耗时,cpu不是瓶颈
- 容易编程,且易于维护
- 多线程可能会存在死锁、线程上下文切换问题,会影响性能
1.3 使用IO多路复用的优点
- I/O多路复用/能让 Redis 不需要额外创建多余的线程来监听客户端的大量连接,降低了资源的消耗。
1.4 文件事件处理器的结构
- 有多个socket
- 有IO多路复用程序
- 文件事件分派器
- 事件处理器(连接应答处理器、命令请求处理器、命令回复处理器)
1.5 线程模型(如何使用IO多路复用)
- IO多路复用程序会监听多个socket,会将产生事件的socket放入队列中排队,事件分派器每次从队列中取出一个socket,根据socket类型交给对应的事件处理器进行处理。
2 Redis多线程情况
- Redis 单线程指的是「接收客户端请求->解析请求->进行数据读写等操作->发送数据给客户端」这个过程是由主线程这一个线程来完成的
- 但是Redis 在启动的时候,会启动后台线程
- Redis2.6,启动两个后台线程,分别处理关闭文件、AOF刷盘两个任务
- Redis4.0,新增lazyfree线程,例如执行unlink key、flushdb async、flushall async命令。因此如果要删除大key,不要用del删除,而是使用unlink来删除
- Redis6.0,采用了多个 I/O 线程来处理网络请求。因为随着网络硬件的性能提升,Redis 的性能瓶颈有时会出现在网络 I/O 的处理上。
- 这些任务使用多线程是因为很耗时,放在主线程会阻塞,无法处理后续请求。
- 但是执行命令,仍然是单线程顺序执行。
五、高并发
1 高并发场景下使用缓存需要注意哪些问题
- 缓存一致性问题
需要保证缓存中的数据与数据库中的一致,需要选择业务适合的缓存过期和更新策略。一般在数据发生更改的时候,先修改数据库,然后删除缓存。- 想避免删除缓存异常,达到最终一致性,可以选择:
- 消息队列做删除缓存,的重试
- 订阅mysql的binlog,再操作缓存(例如阿里的canal)
- 想避免删除缓存异常,达到最终一致性,可以选择:
- 缓存击穿问题
在某个高访问量的key过期时,大量的请求会直接落到数据库中。解决方法是加载缓存时采用互斥锁,保证只有一条请求落到数据库,其他请求先自旋,然后查询缓存,最后再申请锁。 - 缓存穿透问题
一些没有value的key被频繁查找,可以通过在缓存中缓存空对象来缓解。 - 缓存雪崩问题
大量的缓存同时失效或过期。解决方式:限流、降级、熔断、多级缓存。比如在设置过期时间时,合理增加随机性。
2 高并发下使用规范
2.1 键值设计
- key设计
- 有随机性,不要用固定值做key(防止哈希槽冲突)
- 以业务名为前缀,用冒号分隔
- 缩短key的长度
- 不包含特殊字符
- value设计
- 业务阻止大key(string类型10kb以内,其他的/元素个数小于5000)
- 选择合适的底层数据结构(比如hash里有ziplist和hashtable)
- 设置过期时间
2.2 命令使用
- 尽量使用批量操作降低网络耗时(比如pipeline或者hgetall)
- 使用批量操作,需要注意个数
- 禁止使用keys、flushall、flushdb,可以用rename进行禁用
- 尽量避免使用redis的事物(它不支持回滚、集群版key必须在同一个slot里)
2.3 使用规范
- 避免多个应用使用同一个Redis实例
- 使用连接池,控制连接数,提高效率
- 添加熔断机制
- 选择合适的内存淘汰策略(allkeys-lru),设置过期时间
六、持久化
作用
- 防止系统故障
- 重启机器之后数据还在
- 备份到其他位置
1 RDB
快照
1.1 什么是RDB
- 快照持久化是默认的持久化方式
- redis通过创建快照,来获得存储在内存里面的数据/在某个时间点上的副本。
1.2 RDB 创建快照时会阻塞主线程吗?
有两种命令
- bgsave是默认命令,会fork出一个子进程,子进程创建快照,不会阻塞主进程
- save是同步保存操作,会阻塞
2 AOF
只追加文件
2.1 什么是AOF
- 开启AOF持久化后,每执行一条更改数据的命令,会将这条命令写入到内存缓存,根据配置决定,什么时候将它同步到硬盘中的AOF文件
- 和RDB相比,AOF持久化的实时性更好
2.2 AOF是如何实现的
aof会在执行完命令之后才记录日志。
优点:
- 这样可以避免额外的检查开销
- 命令执行完之后再记录,不会阻塞当前的命令执行
缺点:
- 执行完命令还没有AOF就宕机,会导致对应的修改丢失
- 会阻塞后续其他命令的执行(AOF记录日志是在Redis主线程中进行的)
2.3 AOF重写
- 含义和条件
当 AOF 变得太大的时候,Redis 能够在后台自动重写 AOF 产生一个新的 AOF 文件,这个新的 AOF 文件和原有的 AOF 文件所保存的数据库状态一样,但体积更小。 - 实现方式
是通过读取当前的数据库状态来实现的 - 可能发生的问题和解决方式
-
问题1:创建新AOF时会出现进行大量写操作,主线程被长时间阻塞,无法处理其他命令
-
解决方案是:将AOF放到子线程执行
-
问题2:在后台AOF重写的时候,服务器会对当前数据进行修改,会导致数据不一致
-
解决方案是:使用AOF重写缓存。完成AOF文件重写后,将AOF重写缓存中的内容全部写入到新的AOF文件中,之后,改名覆盖旧的AOF文件
-
3 如何选择RDB和AOF
3.1 RDB优点
- RDB比AOF更小,适合做灾备
- 使用 RDB 文件恢复数据,直接解析还原数据即可,不需要一条一条地执行命令,速度非常快。
3.2 AOF优点
- AOF实时性比RDB更好
- RDB在产生的时候会对CPU和资源影响比较大
- AOF易于理解,可以轻松导出进行分析
4 混合持久化
- 结合 RDB 和 AOF 的优点, 快速加载,同时能避免丢失过多的数据。
- 它默认关闭,需要通过配置项开启
七、分布式高可用:主从/哨兵/集群
1 主从复制
Redis主从复制是一种数据同步机制,其中一个Redis实例作为主节点,将数据复制到多个从节点,实现数据的备份和读写分离。
- 作用
实现redis高可用性和冗余备份。一个Master可以有多个Slaves。 - 特点
- 主数据库可以进行读写操作,对主数据库写操作会自动将数据同步给从库。
- 从库只读,接收主库同步过来的数据
- 一个master可以有多个slave,一个slave只能有一个master
- master挂后不影响slave读,但是不提供写服务,master重启后才提供写服务
- master挂后,不会在slave选举master
- 优点
- 读写分离,可以提高服务器性能
- slave可以接受其他slave的连接和同步,能分担master同步的压力
- master是非阻塞的,master到slave同步期间,客户端仍然可以提交查询或修改请求到master服务器。
- slave是非阻塞的,同步期间,如果有客户端提交查询请求到从库,Redis则返回同步之前的数据。
- 缺点
- 不具备自动容错和恢复功能,master或slave宕机都会导致部分请求失败。
- 主机宕机,宕机前会有一些数据不能及时同步到从机。切换IP之后还会引入数据不一致的问题,降低了系统的可用性
- 多个slave宕机,不能同时重启,多个slave发送sync请求和主机全量同步时,可能会导致master IO剧增导致master宕机。
2 哨兵
Redis哨兵模式是一种自动监控和管理Redis主从复制架构的机制,它监控主节点的状态并在主节点故障时自动选举新的主节点,确保高可用性。
- 和主从比较的优缺点
- 优点:
- 哨兵模式基于主从,主从的优点哨兵都有
- 支持选举
- 支持自动发现节点,自动化故障转移
- 缺点:
- 心跳检测耗费性能
- 网络延迟可能会导致触发不必要的故障转移
- 优点:
- 主要功能
- 集群监控:监控redis的master和slave是否正常工作
- 消息通知:redis实例有故障,就发送报警给管理员
- 故障转移:master宕机,自动转移到slave上
- 配置中心:故障转移发生,通知client端新的master地址
- 主观下线和客观下线(高可用原理)
- 主观下线:一个哨兵节点判定主节点down是主观下线
- 客观下线:半数哨兵节点都主观判定主节点down掉,才会判断主节点客观下线。
客观下线才会切换主机。
3 集群
是一种分布式架构,将数据分散存储在多个节点上,实现数据的水平扩展和负载均衡,提高系统的性能和可伸缩性。
- 和主从、和哨兵的差别:
主从或者哨兵的每台服务器存储相同的数据,浪费内存,并且有木桶效应。集群版可以做到分布式存储。 - 原理:
- 计算key属于哪个哈希槽,集群中的每个节点负责处理一部分哈希槽。
- 使用的是数据分片而不是一致性哈希(数据分片划分是固定的)
- 一个主节点对应一个或者多个从节点,当主节点宕机的时候,就会启用从节点。
- 特点:
- 所有节点都是一主一从或一主多从,从节点不提供服务,仅作为备用
- 节点失败是超过半数节点检测失效才生效
- 客户端可以连接任何一个主节点进行读写
- 支持在线增加、在线删除节点
- 因为选举投票的机制,所以主库个数必须为奇数
- 优缺点:
- 优点
- 通过分片和虚拟槽解决分布式负载均衡的问题
- 可以实现动态扩容(数据分片)
- 是P2P模式,无中心化
- 缺点
- 从库在集群中充当冷备,不能缓解读压力
- 优点
- 其他:
如果接受/可能会读到旧数据,可以用开启从库的可读权限,用read only命令做到读写分离。
八、应用
1 分布式锁
- 分布式锁用于控制某个资源在同一时刻只能被一个分布式应用使用
- redis可以被多个客户端共享访问,而且单分片可以达到10万qps,可以应对高并发锁的场景
- redis的set命令可以设置nx来完成【key不存在才插入】,可以通过ex或者px来设置超时时间
- 如果key不存在,插入成功,加锁成功。key存在,插入失败,加锁失败
- 设置过期时间可以避免,客户端拿到锁后发生异常,导致锁没法释放
- 锁变量的值可以区分请求的不同客户端,防止当前程序释放了其他程序上的锁
- redis是单线程的,一条set nx px命令可以达到原子操作的效果
- 解锁的时候可以通过lua先判断value是否是自己加上的,然后再删除,保证原子性。
- (SET lock_key unique_value NX PX 10000)
1.1 分布式锁如果基于Redis可能会有什么问题
在redis是主从模式的时候,同步并不一致,可能会导致锁失效的情况
2 购物车
购物车信息一般用Hash存储,因为购物车中的商品频繁修改和变动
- 用户id为 key
- 商品id为field,商品数量为value 普通维护
- 用户添加商品就是往 Hash 里面增加新的 field 与 value;
- 查询购物车信息就是遍历对应的 Hash;
- 更改商品数量直接修改对应的 value 值(直接 set 或者做运算都可以);
- 删除商品就是删除 Hash 中对应的 field;
- 清空购物车直接删除对应的key。
3 排行榜
一般用sorted set。
常见的命令有:ZRANGE (从小到大排序) 、 (z reverse range)ZREVRANGE (从大到小排序)、ZREVRANK (指定元素排名)。
3.1 用zset实现排行榜先用分数排序再用时间排序怎么实现
- zset分值是double类型,一共64位,可以用高一些的22位表示分数,低一些的41位表示时间戳
- 分数在高位,所以优先是分数排,时间在低位,在相同的分数,时间是有序的
- 如果需要时间按升序排(时间越早,在排行榜越前)
- 如果是周榜,可以用下周的开始时间减去当前时间,随着时间推移变小,不会出现负数的情况
4 抽奖系统
一般使用set
SPOP key count: 随机移除并获取指定集合中一个或多个元素,适合不允许重复中奖的场景。- (s random member)
SRANDMEMBER key count: 随机获取指定集合中指定数量的元素,适合允许重复中奖的场景。
5 活跃用户
使用bitmap
使用日期(精确到天)作为 key,然后用户ID为offset,如果当日活跃过就设置为 1。
....
6 页面访问量
- 用
PFADD将访问指定页面的每个用户 ID 添加到HyperLogLog中。 - 用
PFCOUNT统计指定页面的 UV。
7 秒杀场景处理高并发和超卖
- 直接用redis进行incrby原子性,同步返回结果,异步写进数据库,但是可能数据不一致,或者异步线程死亡
- 在查询商品的时候加排他锁,但是可能导致并发效果不好
九、数据结构
1 有哪些数据结构
redis有五种基础数据结构
- String:字符串类型,可以存储字符串、整数和浮点数。
- List:有序列表类型,可以存储多个元素,每个元素可以是字符串或者数字。底层是双向链表
- Set:集合类型,可以存储多个元素,每个元素必须是唯一的。底层是哈希表,可以轻易实现交集、并集、差集的操作。
- Hash:哈希类型,可以存储多个字段和对应的值,每个字段和值都是字符串类型。key field value
- Zset:有序集合类型,可以存储多个元素,每个元素可以关联一个分数,用于排序。底层实现是跳跃表和哈希表
redis还有三种特殊数据结构
- HyperLogLogs(基数统计)
用概率统计数组中不重复的元素个数。用非常少的内存,存储非常多的数据,误差低于1%。PFADD将value存进key中PFCOUNT返回该key的近似基数
- Bitmap (位存储)
- Geospatial (地理位置)。
2 String(简单动态字符串)
Redis没有直接使用C语言传统的字符串,而是自己构建了一种:简单动态字符串(SDS)作为默认字符串。
SDS等同于cpp的char *,但使用了长度字段,所以是二进制安全的(不用\0标识字符串结束)。
- 字段:
- int len:字符串长度
- int free:数组中未使用字节的数量
- char buf[]:字节数组用来保存字符串
- 优点:
- 能直接获取字符串长度
- 能避免缓冲区溢出
- 减少修改字符串长度时,所需要的内存重分配次数
- 是二进制安全的
- 能兼容部分C++字符串的函数
3 Hash
- 底层结构
dictEntry **table:哈希表数组unsigned long size:哈希表大小unsigned long sizemark:哈希表大小掩码,用于计算索引值,总是等于size-1unsigned long used:哈希表已有节点的数量
- Hashtable和ziplist
4 ZSet(有序集合)
4.1 跳跃表
-
通过在每个节点中维持多个指向其他节点的指针,从而达到快速访问节点的目的。
-
跳跃表在链表的基础上增加了多级索引,用来提升查找的效率,是一个空间换时间的方案。
-
当节点本身比较大或者元素数量比较多的时候,空间的缺点可以忽略。
-
Redis使用跳跃表作为有序集合键的底层实现之一,如果一个有序集合包含的元素数量比较多,又或者有序集合中元素的成员是比较长的字符串时,Redis就会使用跳跃表来作为有序集合的底层实现。
4.2 为什么使用跳表,而不是用树,例如红黑树
- 跳表比红黑树存储空间的利用率更高,红黑树需要额外指针来维护红黑树的性质
- 跳表比红黑树扩展更简单,只需要增加层级索引,红黑树需要进行平衡操作,可能需要重构整个树的结构
5 List(有序链表)
- 节点(ListNode)底层结构
struct listNode *prev:前置节点struct listNode *next:后置节点void *value:节点的值
- 链表(List)底层结构
listNode *head:表头节点listNode *tail:表尾节点unsigned long len:链表所包含的节点数量void *(*dup)(void *ptr);:节点值复制void (*free)(void *ptr);:节点值释放int(*match)(void *ptr, void *key);:节点值对比
- 链表特性
- 每个节点存储前后两个节点,是双端链表
- 不是循环链表
- 可以保存不同类型的值
6 Set
7 布隆过滤器
- 布隆过滤器说数据不存在,一定不存在。
- 布隆过滤器说数据存在,实际可能不存在,取决于哈希冲突情况。
- 存放在布隆过滤器的数据不容易删除
十、事物
- Redis事务提供了一种将多个命令请求打包的功能。然后,再按顺序执行打包的所有命令。
- Redis事务不支持回滚,不满足原子性
- lua比redis事务减小了网络开销,但也不支持回滚