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脚本确保解锁操作的原子性
使用场景
String 是 Redis 中最简单同时也是最常用的一个数据类型。它是一种二进制安全的数据类型,可以用来存储任何类型的数据比如字符串、整数、浮点数、图片(图片的 base64 编码或者解码或者图片的路径)、序列化后的对象。
- 由于购物车中的商品频繁修改和变动,购物车信息建议使用 Hash 存储
Sorted Set
(有序集合)的数据类型经常被用在各种排行榜的场景,比如直播间送礼物的排行榜、朋友圈的微信步数排行榜、王者荣耀中的段位排行榜、话题热度排行榜等等。Set
- 存放的数据不能重复的场景:网站 UV 统计(数据量巨大的场景还是HyperLogLog
更适合一些)、文章点赞、动态点赞等等。获取多个数据源交集、并集和差集的场景:共同好友(交集)、共同粉丝(交集)、共同关注(交集)、好友推荐(差集)、抽奖系统、随机点名等等。- Bitmap 存储的是连续的二进制数字(0 和 1)统计活跃用户
Redisson实现分布式锁
Redisson是操作redis的Java框架
设置分布式锁的过期时间,避免锁一直被占用导致死锁
为每个锁关联一个线程id和重入次数作为锁value的一部分存储在redis中,避免误删和不可重入
redisson自动续期,提高定时任务延长锁的有效期
redis缓存的基本思想
空间换时间 ,也就是用更多的存储空间来存储一些可能重复使用或计算的数据,从而减少数据的重新获取或计算的时间。
持久化
如何保证数据不丢失
持久化
将数据从内存中存储到持久化存储介质中的过程,以便在程序重启或者系统崩溃能够从存储介质中恢复数据
RDB,RedisDataBase持久化:快照方式持久化,将某一时刻的内存数据,以二进制的方式写入磁盘
- 将某一时刻的内存快照,以二进制方式写入磁盘的持久化机制。
- 优点: 速度快,相对于AOF,RDB持久化速度更快,只需要在指定的时间间隔内将数据从内存写入到磁盘 空间占用小,数据压缩在二进制文件中 恢复速度快,可靠性高
- 缺点:指定时间间隔保存,会丢失一些数据 实时性差:定期执行
AOF,Append Only File持久化:文件追加持久化,记录所有非查询操作命令,并以文本形式追加到文件中
将Red的每个非查询操作命令都追加到文件中
优点:数据不易丢失、实时性好,数据可读性强(纯文本的形式)
缺点:写入性能低、占用磁盘大、文件可能出现损坏
混合持久化,二者混合。写入时,当前数据以RDB形式写入文件的开头,再将后续的操作命令以AOF的格式存入文件。既保证重启速度,又降低数据丢失
- 复杂度高、可读性差、兼容性差
集群运行
将单个redis服务器换成集群。
主从同步
- 主要存储数据的节点作为主节点,其他节点复制主节点的数据
哨兵模式
- 主节点崩溃需要手动恢复,于是哨兵模式自动恢复。哨兵模式监控主节点
Redis Cluster
- 拥有多个主节点
Redis 线程模型(重要)
对于读写命令来说,Redis 一直是单线程模型。Redis 6.0 版本之后引入了多线程来处理网络请求(提高网络 IO 读写性能)。
Redis 通过 IO 多路复用程序 来监听来自客户端的大量连接(或者说是监听多个 socket),它会将感兴趣的事件及类型(读、写)注册到内核中并监听每个事件是否发生。
这样的好处非常明显:I/O 多路复用技术的使用让 Redis 不需要额外创建多余的线程来监听客户端的大量连接,降低了资源的消耗(和 NIO 中的 Selector
组件很像)。
Redis 内存管理
内存是有限且珍贵的,如果不对缓存数据设置过期时间,那内存占用就会一直增长,最终可能会导致 OOM 问题。通过设置合理的过期时间,Redis 会自动删除暂时不需要的数据,为新的缓存数据腾出空间。
Redis 是如何判断数据是否过期的呢?
Redis 通过一个叫做过期字典(可以看作是 hash 表)来保存数据过期的时间。过期字典的键指向 Redis 数据库中的某个 key(键),过期字典的值是一个 long long 类型的整数,这个整数保存了 key 所指向的数据库键的过期时间(毫秒精度的 UNIX 时间戳)。
在查询一个 key 的时候,Redis 首先检查该 key 是否存在于过期字典中(时间复杂度为 O(1)),如果不在就直接返回,在的话需要判断一下这个 key 是否过期,过期直接删除 key 然后返回 null。
Redis 过期 key 删除策略
- 惰性删除:只会在取出/查询 key 的时候才对数据进行过期检查。这种方式对 CPU 最友好,但是可能会造成太多过期 key 没有被删除。
- 定期删除:周期性地随机从设置了过期时间的 key 中抽查一批,然后逐个检查这些 key 是否过期,过期就删除 key。相比于惰性删除,定期删除对内存更友好,对 CPU 不太友好。
- 延迟队列:把设置过期时间的 key 放到一个延迟队列里,到期之后就删除 key。这种方式可以保证每个过期 key 都能被删除,但维护延迟队列太麻烦,队列本身也要占用资源。
- 定时删除:每个设置了过期时间的 key 都会在设置的时间到达时立即被删除。这种方法可以确保内存中不会有过期的键,但是它对 CPU 的压力最大,因为它需要为每个键都设置一个定时器。
Redis 采用的是 定期删除+惰性/懒汉式删除 结合的策略
大量Key集中过期
定期删除执行中,突然遇到大量的key过期,客户端请求需要等待定期清理过期key任务线程执行完成。因为定期任务线程在主线程中执行,导致客户端请求没办法及时处理,相应变慢
解决方式
给key设置随机过期时间
开启延迟释放,redis采用异步的方式延迟释放key使用的内存,将该操作交给单独的子线程处理,避免阻塞主线程
Redis性能优化(big、hot)
big Key
一个key对应的value占用内存过大。
例如String类型的value超过1M,复合类型包含的元素超过5000个
产生的原因
程序设计不当:直接使用String类型存储较大的文件对应的二进制数据
业务数据规模考虑不当:使用集合类型未考虑数据量的快速增长
未及时清理数据:哈希中存在大量的冗余键值对
如何发现 bigkey?
1、使用 Redis 自带的 --bigkeys
参数来查找。
2、使用 Redis 自带的 SCAN 命令
3、借助开源工具分析 RDB 文件。
4、借助公有云的 Redis 分析服务。
如何处理 bigkey?
- 分割 bigkey:将一个 bigkey 分割为多个小 key。例如,将一个含有上万字段数量的 Hash 按照一定策略(比如二次哈希)拆分为多个 Hash。
- 手动清理:Redis 4.0+ 可以使用
UNLINK
命令来异步删除一个或多个指定的 key。Redis 4.0 以下可以考虑使用SCAN
命令结合DEL
命令来分批次删除。 - 采用合适的数据结构:例如,文件二进制数据不使用 String 保存、使用 HyperLogLog 统计页面 UV、Bitmap 保存状态信息(0/1)。
- 开启 lazy-free(惰性删除/延迟释放) :lazy-free 特性是 Redis 4.0 开始引入的,指的是让 Redis 采用异步方式延迟释放 key 使用的内存,将该操作交给单独的子线程处理,避免阻塞主线程。
hot Key
一个key的访问次数明显多于其他key。redis实例的每秒请求处理达到5000,每个key每秒访问达2000
出现原因:某个热点数据访问量突然暴增
危害:占用大量的cpu、带宽,影响redis实例对其他请求的正常处理
如何发现 hotkey?
1、使用 Redis 自带的 --hotkeys
参数来查找。
2、使用MONITOR
命令。
3、借助开源项目。
4、根据业务情况提前预估。
5、业务代码中记录分析。
6、借助公有云的 Redis 分析服务。
如何解决 hotkey?
- 读写分离:主节点处理写请求,从节点处理读请求。
- 使用 Redis Cluster:将热点数据分散存储在多个 Redis 节点上。
- 二级缓存:hotkey 采用二级缓存的方式进行处理,将 hotkey 存放一份到 JVM 本地内存中(可以用 Caffeine)。
性能优化
- 使用批量操作减少网络传输,降低往返时间
- Redis命令执行分为四步:
- 发送命令
- 命令排队
- 命令执行
返回结果
使用原生批量操作
- MGET获取一个或多个指定的key,对应MSET HMGET获取哈希表中一个或者多个指定字段的值,对应HMSET
- 对于不支持批量操作的命令,使用pipeline将一批Redis命令封装成一组,一次性提交到Redis服务器,只需要一次网络传输。
优化方案
缩短键值对的存储长度
使用延迟删除特性
设置合理的过期时间
禁用长耗时的查询命令
使用slowlog优化耗时命令
使用pipeline批量操作数据
避免大量数据同时失效
对于不重要的数据关闭持久化功能
雪崩、击穿、穿透(生产问题)
缓存雪崩
大量缓存数据同一时间过期,或者Redis服务器宕机,此时有大量的用户请求,无法在Redis中处理,于是全部请求数据库,导致数据库压力骤增。
解决方式
- 设置随机过期时间,避免大量的键同时过期
- 实现缓存预热:系统启动或缓存失效前,提前将热门数据加载到缓存中,避免在关键时刻大量请求直接访问后端服务
- 使用分布式缓存:将缓存数据分布在多个缓存节点上,分散请求负载
- 设置熔断机制:缓存失效,直接返回默认值或者默认值,避免直接访问后端
- 实时监控:监控缓存状态,异常则报警
缓存击穿
热点数据过期或者失效,大量请求访问该数据,导致请求直接访问数据库
解决方式
- 设置热点数据永不过期,或者设置较长的过期时间
- 加互斥锁、分布式锁:访问热点数据,保证只有一个线程访问后端,其他线程等待结果。当第一个线程获取到数据,其他线程可以直接从缓存中获取
- 限制并发访问:控制请求的流量
缓存穿透
大量请求查询不存在缓存或者数据库中的数据,导致直接访问后端
- 原因:
- 恶意请求,故意查询不存在的数据
- 高并发请求,缓存无法名字
解决方式
- 布隆过滤器:用于快速判断一个元素是否存在集合中。查询请求到达时,使用布隆过滤器判断请求对应的数据是否存在缓存或者数据库中,避免无效查询
- 缓存空值处理:将空结果也缓存起来,设置一个短过期时间,避免频繁查询。
- 异步加载缓存:缓存未命中,异步加载数据到缓存中,通过互斥锁或者分布式锁保证一个线程加载数据
- 热点数据永不过期
- 限制恶意请求:加验证码,控制访问频率
Redis、MySQL数据缓存一致性
读数据,选择旁路缓存策略,cache命中则直接从db加载数据到cache;
写数据,先更新db,再删除cache
布隆过滤器
一种空间效率搞得概率性数据结构,用于判断元素是否存在集合中。基于位数组和哈希函数
缓存淘汰和过期删除
内存淘汰
- 当内存满了,redis触发内存淘汰策略,淘汰掉不必要的内存
过期删除
- 将已过期的键值对进行删除,惰性删除+定期删除