Redis
什么是Redis?
nosql
noSQL = not only sql !!! (不仅仅是SQL)
关系与非关系型数据库
关系型数据库:行+列,同一个表下数据的结构是一样的。
非关系型数据库:数据存储没有固定的形式,并且可以横向拓展。
RDBMS(关系型数据库)和NoSQL
RDBMS
- 组织化结构
- 固定sql
- 数据和关系都存在单独的表中
- ACID
- 基本的事务
NoSQL
- key-value的存储行形式(Redis)
- 列存储(Hbase)
- 文档存储(MangoDB)
- 图形存储(Neo4j)
- 最终一致性(BASE) 基本可用,软状态 最终一致性
Redis整体结构
整体的存储结构是一个大的hashmap
dictEntry由key-value组成,其中的value指的是redisObject
redisObject:具体指向redis数据类型 ptr-数据结构的地址 type-对象的类型
Redis五种数据类型
- string
- list
- set
- hashSet
- hash
Redis底层八种数据结构
- REDIS_ENCODING_INT(long 类型的整数)
- REDIS_ENCODING_EMBSTR embstr (编码的简单动态字符串)
- REDIS_ENCODING_RAW (简单动态字符串)
- REDIS_ENCODING_HT (字典)
- REDIS_ENCODING_LINKEDLIST (双端链表)
- REDIS_ENCODING_ZIPLIST (压缩列表)
- REDIS_ENCODING_INTSET (整数集合)
- REDIS_ENCODING_SKIPLIST (跳跃表和字典)
数据类型对应的数据结构
String存储结构
int,embstr,raw
在int数据结构范围内使用long
小于44字节使用embstr,redisObject和sds存储在一起,形成一个连续的内存块
大于44字节使用raw,redisObject和sds分别分配内存
sds
简单动态字符串,是redis数据库中用于存储字符串数据的自定义数据结构。embstr和raw都为sds编码。
- 优化获取字符串长度:sdsO(1) C语言O(N)
- 防止缓冲区溢出: sds记录了字符串长度,因此不会溢出
- 空间预分配:字符串需要拓展的时候,预分配多的空间,防止多次连续拓展
- 惰性释放:字符串缩小时,不会立即回收多余空间。
为什么小于 44 字节用 embstr 编码呢
embstr(短字符串)使用最小的一个sdshdr8
redisObject=16字节 sdshdr8=4字节
初始最小分配为64字节 所以 使用embstr的时候64-16-4=44 要小于44字节,如果大于44使用raw(长字符串)
List存储结构
3.2之前使用ziplist+linkedlist:满足以下两个条件时 使用ziplist
list中保存的元素小于64字节
列表中数据小于512
3.2之后使用quicklist:基于ziplist的双向链表
为什么不直接使用likedlist?
附加空间相对太高,通常每个指针占用8个字节,prev(前节点)和next(后节点)就占用16个字节,而且每一个节点都是单独分配,会加剧内存碎片化,影响内存管理效率。
ziplist
压缩列表,存储在连续的内存区域中,当列表元素不大,每个元素也不大时,采用ziplist存储。
当数据量过大时ziplist就不好用了,ziplist是连续的内存存储,新增或更新元素时,分配不足容纳新数据,整个列表可能需要重新内存分配,这在大数据量操作成本较高且可能导致频繁的内存碎片
quickList
快速列表。结合了ziplist和linkedlist的优点。为列表类型提供更好的空间效率和性能。
基本思想是将多个小的ziplist片段通过双向链表链接起来。每个ziplist片段存储一部分连续的数据元素。
Hash存储结构
数据项比较少的情况下,使用ziplist,随着数据的增加,底层的ziplist可能会转成dict(hashtable-ht)!
dict结构有两个hashtable,但是通常情况下只有一个是有指的。在dict扩容或缩容的时候,需要重新分配hashtable。然后进行rehash(渐进式搬迁)搬迁结束后旧的删除,新的代替。
rehash(渐进式)
主要应用在扩容和缩容操作,当哈希表需要进行大小调整时,如果一次性将所有键值对重新计算哈希并移动到新的哈希表,消耗大量的CPU。
在渐进式Rehash过程中:
- 当Redis检测到当前哈希表已达到预设的负载因子或者需要调整哈希表大小时,它会创建一个新的、更大或更小的哈希表。
- 新旧两个哈希表同时存在,在接下来的一段时间内,每次执行写入、删除或查找等操作时,除了执行原操作外,还会将操作涉及到的键值对从旧哈希表迁移到新哈希表中。
- 这个迁移过程是在单次命令执行的过程中顺带完成的,分散在整个服务周期中,避免了集中式的重哈希操作带来的性能冲击。
- 渐进式Rehash持续进行,直到旧哈希表中的所有键值对都被迁移至新哈希表,此时Redis将废弃旧哈希表,完全使用新的哈希表进行后续操作
Set存储结构
内部实现相当于一个特殊的字典,字典中所有的value都是一个值null。底层编码包括hashtable和intset
满足以下条件时,使用intset,否则使用hashtable
- 存储的数据都是整数
- 存储的数据元素个数小于512个
inset
一个有序集合(升序)查找元素O(logN)二分法,但插入时不一定是O(logN),因为有可能涉及到升级操作,比如当集合里全是int16_t型整数,此时插入一个int32_t,那么为了维持集合中数据类型的 一直,所有的数据都会转换成32类型,涉及到内存的重新分配,这时插入的复杂度为O(N)。intset不支持降级操作
Zset存储结构
给每个元素设置了一个分数,作为排序的依据。
数据结构使用了ziplist和skiplist
ziplist排序
每个集合元素使用两个紧挨在一起的压缩列表节点来保存,第一个节点保存元素的成员(member),而第二个元素则保存元素的分值(score)
skiplist跳表
跳跃表是一种随机化的数据,跳跃表以有序的方式在层次化的链表中保存元素。
在了解跳跃表之前,我们先了解一下有序链表。有序链表是所有元素以递增或递减方式有序排列的数据结构,其中每个节点都指向下个节点的next指针,最后一个节点的next指针指向NULL。
使用单有序链表,如果要查询31的元素,需要从第一个元素开始依次向后查询、比较才可以找到,找到顺序为1->8->11->12->26->31,共6次比较,时间复杂度为O(N)
使用分层有序链表,比如我们查找值为31的节点时,查找步骤如下:
- 从最高层第2层开始查找,1节点比31值要小,继续向后比较。
- 11节点比31节点要小,继续向后比较,这时会发现第2层11节点的next指针是指向NULL,所以在11节点就开始需要下降一层到第1层并继续向后查找节点进行比较。
- 在下降到第1层中,11节点的值比31要小,继续向后比较,第1层11节点的next指针指向26,26比31要小,继续向后比较,第1层26节点的next指针指向61,61比31要大,需要下降一层继续向后比较。
- 最后下降到了第0层,第0层的26节点的next指针指向31,31为我们要找的节点,节点被找到。
跳表的结构体zskiplist
header:指向跳跃表头节点,头节点时跳跃表的特殊节点,他的level是固定数组元素个数未32个
tail:指向跳跃表尾节点
length:跳跃表长度,表示第0层除头节点以外的所有节点总数
level:跳跃表高度
三大特殊数据类型
- geospatial(地理位置)应用场景: 查看附近的人,微信位置共享,地图上直线距离的展示
- Hyperloglog(基数)就是不重复的元素。应用场景:同一天统计一个ip访问次数
- Bitmaps(位存储)可以实现对位的操作,表示0或1。应用场景:登录与非登录状态等。
Redis事务
事务本质:一组命令的事务
数据库事务-通过ACID(原子性,一致性,隔离性,持久性)来完成。
Redis事务-将多个命令打包,然后一次性按顺序地执行的机制,并且事务在执行的期间不会主动中断,服务器在执行完事务中的所有命令之后 才会继续处理其他客户端的其他命令
Redis持久化
Redis 是一种内存型数据库,一旦服务器进程退出,数据库的数据就会丢失,为了解决这个问题 Redis 供了两种持久化的方案,将内存中的数据保存到磁盘中,避免数据的丢失
RDB(快照)
经过压缩的二进制文件,这个文件保存到磁盘,可以手动执行,也可以定时执行
- Redis 调用 forks。同时拥有父进程和子进程;
- 子进程将数据集写入到一个临时 RDB 文件中;
- 当子进程完成对新 RDB 文件的写入时,Redis 用新 RDB 文件替换原来的 RDB 文件,并删除旧的 RDB 文件。
手动触发的分为save(同步),bgsave(异步)
AOF(追加)
已日志的方式只记录写操作,不会记录读操作。只允许追加文件不可修改文件。redis在启动的时候会调用aof文件执行指令记录
缓存穿透、击穿、雪崩问题
缓存穿透
缓存穿透是指缓存和数据库中都没有数据,而用户不断发送请求,在流量大的时候容易导致宕机。
解决方案:
在接口层校验访问的key是否合法,例如校验id>0
缓存和数据库都找不到值的情况下,把value设置null值
使用布隆过滤器类似于hahs set 用于快速判断某个元素是否存在于集合中,如果没有直接返回。
缓存击穿
缓存中没有数据,但是数据库中有(一般大量数据到期)这时并发用户特别多,同时读缓存没有导致读大量请求数据库,导致数据库压力剧增.(并发查同一数据)
解决方案:
- 设置热点数据永不过期
- 接口限流和熔断,重要的接口一定要做好限流策略,防止用户恶意刷接口,同时要降级准备,当接口中的某些服务不可用时候,进行熔断,快速返回机制
- 加互斥锁
缓存雪崩
大量数据同时到期,而查询量巨大,引起数据库数量过大或宕机。和缓存击穿不同的时,缓存击穿是并发查同一条数据,而缓存雪崩是不同的数据都过期了
解决方案:
- 缓存数据的过期时间设置随机,防止大量的key同一时间过期
- 将热点数据散列在不同的缓存数据库中(分部署)
- 设置热点数据永不过期
过期策略(不是淘汰策略)
- 定期删除策略(定时任务方式):redis会定期地(根据最新redis文档)随机抽取一部分设置了过期时间的键,检查他们是否过期。如果过期则删除。
- 惰性删除策略(懒汉式方式):当访问一个键时,redis会检查改建是否过期,如果过期则删除。
- 主动清除:当redis实例配置了maxmemory限制,并且当前使用的内存达到了这个上限,忽略以上两点过期策略主动清除策略,通过LRU。LFU等淘汰算法清除。以保证内存不会超过设定最大值。
LRU和LFU有什么区别?
LRU(Least Recently Used):侧重于最近的历史访问记录 适用于大多数情况下通用缓存
LFU(Least Frequently Used):侧重于历史访问的总频率 使用于访问模式与访问频次紧密相关的场景
Redis集群
主从集群
主从复制,是指将redis一台服务器复制到另一台redis服务器上,前者为主节点(master)后者为从节点 (slave);数据的复制是单向的,只能主节点到从节点,默认通常情况下每一台redis服务器都是一个主节点,且一个主节点可以有多个从节点或没有,一个从节点只能有一个主节点。
主从复制的作用:
- 数据冗余:主从复制实现了数据的热备份(在运行时实现数据备份,不会影响正常使用)。
- 故障恢复:当主节点出现故障时,可以由从节点提供服务
- 负载均衡:在主从复制的基础上,实现读写分离,主节点实现写操作,从节点实现读操作,分担服务器负载,尤其在读多写上的情况下。
读写分离的原理
全量复制:首次创建从节点并连接主节点时,会触发全量复制,主节点生成当前所有数据的rdb文件,让从节点执行。这个过程会清除原从节点的数据。
增量复制:是在主从节点已经建立了复制关系且保持更新的情况下进行数据同步的方式。redis在完成初始化全量同步之后,会切换到增量复制模式。主节点在接受到命令后会将这些命令按照顺序发送给从节点执行。
全量复制的三个阶段
1.主从库建立连接与协商同步:新的从节点加入集群时,主动向主节点发送PSYNC命令。提供自己的runid和偏移量(作用数据是否同步完整)首次没有的话执行全量复制。
2.主节点生成并发送RDB快照给从节点:创建RDB文件,这个文件包含了全部的数据集并发送给从节点。
3.主节点发送缓冲区中的命令流:在主节点实现节点2的时候,可能会产生新增的数据,这个阶段已增量命令的形式发送从节点来保证完整数据。
哨兵集群
哨兵的核心功能是主节点的自动故障迁移。
哨兵实现功能:
- 监控:每个哨兵节点会对数据节点(主/从)节点和其余的哨兵节点进行监控
- 通知:哨兵会将故障转移的结果通知给应用方
- 故障转移:实现从节点晋升主节点,并维护后续主仆关系
- 配置中心:在哨兵模式中,客户端在初始化时候连接的是哨兵节点,从中获取主从信息
Cluster
RMQ
RMQ是一个二方库,基于Redis实现的消息队列的功能。
RMQ有消息合并,消息优先级,定时消息等特性。
可以用于异步解耦,削峰填谷,亿级数据堆积。
区间重复合并消息
发送消息时需要定义一个时间区间,消息延迟改时间区间长度后会消费消息,在改时间区间内如果发送消息,重复消息将会被合并。如果消息在Redis服务端发生推积,重复到来的消息依然会被合并处理。改类型消息适用于消息重复率较高且希望重复消息合并处理的场景,对重复消息进行合并可以减少下游消费系统的压力,减少不必要的资源消耗,将有限的资源最大化的利用,提升消费效率。
优先级消息
支持给消息设置任意等级的优先级,优先级高的消息会被优先消费,相同优先级的消息被随机消费,如果消息在redis服务端发生堆积,重复的消息将合并处理,合并后消息的优先级等于最后存储的消息的优先级,该类型消息适用于希望重复消息合并处理且设置优先级的场景,下游消费者资源有限时,合并重复消息且优先处理优先级高的消息将可以合理利用有限资源。
定时消息
支持给消息设置任意消费时间,只有消费时间到了之后消息才被消费,消费时间可精确到秒,消息到期后没有及时被消费,消费之将按照时间由远及近进行消费。如果消息在redis服务端发生堆积,重复的消息将被合并处理,合并后消息的消费时间等于最存储的消费的消费时间,该类型消息适用于希望重复合并并处理且需要定时消费的 场景定时消息应用场景非常丰富,比如定时打标去标,活动结束后清理动作,订单超时关闭等。
Redis7.0优化
Multi-part AOF(MP-AOF)
基于持久化aop的优化方式,也称为aof分块持久化或AOF子部分持久化。
优化后,不在对整个文件进行追加,而是将AOF文件分割成多个部分(part),每次只重写其中一部分,并且可以并行执行重写和追加新的命令操作。
费。如果消息在redis服务端发生堆积,重复的消息将被合并处理,合并后消息的消费时间等于最存储的消费的消费时间,该类型消息适用于希望重复合并并处理且需要定时消费的 场景定时消息应用场景非常丰富,比如定时打标去标,活动结束后清理动作,订单超时关闭等。
Redis7.0优化
Multi-part AOF(MP-AOF)
基于持久化aop的优化方式,也称为aof分块持久化或AOF子部分持久化。
优化后,不在对整个文件进行追加,而是将AOF文件分割成多个部分(part),每次只重写其中一部分,并且可以并行执行重写和追加新的命令操作。