Redis八股总结
Redis
数据结构
Redis底层的数据结构
String
可以是字符串、整数或浮点数
对整个字符串或字符串的一部分进行操作; 对整数、浮点数进行自增或者自减操作。
List
一个链表,链表上的每个节点都包含一个字符串
对链表的两端进行push、pop,读取单个或者多个元素;根据值查找或者删除元素
Set
包含字符串的无序集合
包含基础的方法,增加获取删除、计算交并查集
Hash
包含键值对的无序散列表
包含添加、删除、获取单个元素
Zset有序集合
和散列一样,用于存储键值对
字符串成员与浮点数之间的有序映射; 元素排序由分数的大小决定; 增加、获取、删除单个元素以及根据分支范围或成员来获取元素
BitMap、HyperLogLog、GEO、Stream
数据结构的常用场景
String
缓存对象
常规计数
分布式锁
共享session信息
List
- 消息队列。生产者需要自行实现全局唯一id 不能以消费组形式消费数据
Hash
缓存对象
购物车
Set
聚合计算
点赞、共同关注、抽奖活动
Zset
排序场景
排行榜
电话
姓名排序
BitMap
二值状态
签到
是否登录
连续签到用户总数
HyperLogLog
海量数据基数统计
百万级网页UV计数
GEO
- 存储地理位置信息
Stream
消息队列
自动生成全局唯一id
支持以消费组形式消费数据
Zset 底层实现
结合哈希表和跳表,实现支持唯一性和有序性的集合。每个元素包含一个唯一的成员和一个可以重复的分数,根据分数进行排序
底层使用跳表和HashTable。跳表用于按分数进行排序存储;字典用于快速查找元素
跳表,在链表的基础上建立多层索引,增删查复杂度logN
- 特点:
- 元素按分数排序:跳表中每个节点都按分数递增排列
- 多层索引:跳表有多层链表,每一层链表上的元素都是下一层链表元素的子集。更高的层数意味着更少的元素,查找时可以快速跳过较多的元素,从而加快速度
时间复杂度LogN 空间复杂度N
- HashTable,将成员作为键,分数作为值
跳表如何实现
Redis如何实现消息队列
使用Redis的发布订阅功能
- Redis发布订阅。生产者将消息发布到指定的channel,消费者订阅此channel,接受并处理消息、 该模式支持一对多,也可以实现分组订阅,将不同的消费者分为不同的组,实现广播或者点对点的消息传递。
List数据结构
- 实现消息队列的入队、出队操作。生产者将消息插入List尾部,消费者从List头部获取消息。使用阻塞时的POP操作实现消费者等待新消息到达,也可以使用定时轮询来获取新消息
Redis实现分布式锁
锁的核心在于互斥
SETNX (SET If Not Exists) 如果key不存在,才会设置key的值 key已经存在,SETNX啥也不做
释放锁,直接DEL对应的key
使用EXPIRE设置过期时间,避免锁无法被释放 需要
防止误删到其他锁,使用lua脚本确保解锁操作的原子性
Redisson实现分布式锁
Redisson是操作redis的Java框架
设置分布式锁的过期时间,避免锁一直被占用导致死锁
为每个锁关联一个线程id和重入次数作为锁value的一部分存储在redis中,避免误删和不可重入
redisson自动续期,提高定时任务延长锁的有效期
大量Key集中过期
定期删除执行中,突然遇到大量的key过期,客户端请求需要等待定期清理过期key任务线程执行完成。因为定期任务线程在主线程中执行,导致客户端请求没办法及时处理,相应变慢
解决方式
给key设置随机过期时间
开启延迟释放,redis采用异步的方式延迟释放key使用的内存,将该操作交给单独的子线程处理,避免阻塞主线程
大Key
big Key
一个key对应的value占用内存过大。 例如String类型的value超过1M,复合类型包含的元素超过5000个
产生的原因
程序设计不当:直接使用String类型存储较大的文件对应的二进制数据
业务数据规模考虑不当:使用集合类型未考虑数据量的快速增长
未及时清理数据:哈希中存在大量的冗余键值对
hot Key
一个key的访问次数明显多于其他key。redis实例的每秒请求处理达到5000,每个key每秒访问达2000
出现原因:某个热点数据访问量突然暴增
危害
- 占用大量的cpu、带宽,影响redis实例对其他请求的正常处理
性能优化
使用批量操作减少网络传输,降低往返时间
- Redis命令执行分为四步:
- 发送命令
- 命令排队
- 命令执行
- 返回结果
使用原生批量操作
- MGET获取一个或多个指定的key,对应MSET HMGET获取哈希表中一个或者多个指定字段的值,对应HMSET
对于不支持批量操作的命令,使用pipeline将一批Redis命令封装成一组,一次性提交到Redis服务器,只需要一次网络传输。
优化方案
缩短键值对的存储长度
使用延迟删除特性
设置合理的过期时间
金庸长耗时的查询命令
使用slowlog优化耗时命令
使用pipeline批量操作数据
避免大量数据同时失效
对于不重要的数据关闭持久化功能
Redis、MySQL数据缓存一致性
读数据,选择旁路缓存策略,cache命中则直接从db加载数据到cache;
写数据,先更新db,再删除cache
布隆过滤器
一种空间效率搞得概率性数据结构,用于判断元素是否存在集合中。基于位数组和哈希函数
雪崩、击穿、穿透
缓存雪崩
大量缓存数据同一时间过期,或者Redis服务器宕机,此时有大量的用户请求,无法在Redis中处理,于是全部请求数据库,导致数据库压力骤增。
解决方式
- 设置随机过期时间,避免大量的键同时过期
- 实现缓存预热:系统启动或缓存失效前,提前将热门数据加载到缓存中,避免在关键时刻大量请求直接访问后端服务
- 使用分布式缓存:将缓存数据分布在多个缓存节点上,分散请求负载
- 设置熔断机制:缓存失效,直接返回默认值或者默认值,避免直接访问后端
- 实时监控:监控缓存状态,异常则报警
缓存击穿
热点数据过期或者失效,大量请求访问该数据,导致请求直接访问数据库
解决方式
- 设置热点数据永不过期,或者设置较长的过期时间
- 加互斥锁、分布式锁:访问热点数据,保证只有一个线程访问后端,其他线程等待结果。当第一个线程获取到数据,其他线程可以直接从缓存中获取
- 限制并发访问:控制请求的流量
缓存穿透
大量请求查询不存在缓存或者数据库中的数据,导致直接访问后端
- 原因:
- 恶意请求,故意查询不存在的数据
- 高并发请求,缓存无法名字
解决方式
- 布隆过滤器:用于快速判断一个元素是否存在集合中。查询请求到达时,使用布隆过滤器判断请求对应的数据是否存在缓存或者数据库中,避免无效查询
- 缓存空值处理:将空结果也缓存起来,设置一个短过期时间,避免频繁查询。
- 异步加载缓存:缓存未命中,异步加载数据到缓存中,通过互斥锁或者分布式锁保证一个线程加载数据
- 热点数据永不过期
- 限制恶意请求:加验证码,控制访问频率
缓存淘汰和过期删除
内存淘汰
- 当内存满了,redis触发内存淘汰策略,淘汰掉不必要的内存
过期删除
- 将已过期的键值对进行删除,惰性删除+定期删除
持久化
如何保证数据不丢失
持久化
将数据从内存中存储到持久化存储介质中的过程,以便在程序重启或者系统崩溃能够从存储介质中恢复数据
RDB,RedisDataBase持久化:快照方式持久化,将某一时刻的内存数据,以二进制的方式写入磁盘
将某一时刻的内存快照,以二进制方式写入磁盘的持久化机制。
优点: 速度快,相对于AOF,RDB持久化速度更快,只需要在指定的时间间隔内将数据从内存写入到磁盘 空间占用小,数据压缩在二进制文件中 恢复速度快,可靠性高
缺点:指定时间间隔保存,会丢失一些数据 实时性差:定期执行
AOF,Append Only File持久化:文件追加持久化,记录所有非查询操作命令,并以文本形式追加到文件中
将Red的每个非查询操作命令都追加到文件中
优点:数据不易丢失、实时性好,数据可读性强(纯文本的形式)
缺点:写入性能低、占用磁盘大、文件可能出现损坏
混合持久化,二者混合。写入时,当前数据以RDB形式写入文件的开头,再将后续的操作命令以AOF的格式存入文件。既保证重启速度,又降低数据丢失
- 复杂度高、可读性差、兼容性差
集群运行
将单个redis服务器换成集群。
主从同步
- 主要存储数据的节点作为主节点,其他节点复制主节点的数据
哨兵模式
- 主节点崩溃需要手动恢复,于是哨兵模式自动恢复。哨兵模式监控主节点
Redis Cluster
- 拥有多个主节点