总结一下Redis的相关知识点。
Redis是一个数据库,不过与传统数据库不同的是Redis的数据库是存在内存中,所以读写速度非常快,因此 Redis被广泛应用于缓存方向。除此之外,Redis也经常用来做分布式锁,Redis提供了多种数据类型来支持不同的业务场景。除此之外,Redis 支持事务持久化、LUA脚本、LRU驱动事件、多种集群方案。主要应用于缓存、分布式锁、排行榜(zset)、计数(incrby)、消息队列(stream)、地理位置(geo)、访客统计(hyperloglog)等。
1、Redis是纯内存的数据库
2、Redis是单线程的数据库,利用队列将并发访问转换成串行访问,有效避免了频繁的上下文切换
3、Redis采用多路I/O复用技术,“多路”指多个网络连接;"复用"指复用一个线程;多路复用IO技术可以让单线程高效的处理多个连接请求。
Redis使用单线程的原因:
Redis是基于内存的操作,CPU不是Redis的瓶颈,Redis的瓶颈最有可能是机器内存的大小或者网络带宽。既然单线程容易实现,而且CPU不会成为瓶颈,那就顺理成章地采用单线程的方案了(毕竟采用多线程会有很多麻烦!)
其底层的数据结构包括:简单的动态字符串 SDS、链表、字典、跳表、整数集合、压缩列表、对象。压缩列表是一种为了节约内存而开发的且经过特殊编码之后的连续内存块顺序型数据结构。
redisObject是Redis类型系统的核心,数据库中的每个键、值,以及Redis本身处理的参数,都表示为这种数据类型。
Redis 对象
typedef struct redisObject {
类型,记录了对象所保存的值的类型
unsigned type:4
对齐位
unsigned notused:2;
编码方式,记录了对象所保存的值的编码
unsigned encoding:4;
LRU 时间(相对于 server.lruclock)
unsigned lru:22;
引用计数
int refcount;
指向对象的值,指向实际保存值的数据结构, 这个数据结构由type属性和encoding属性决定
void *ptr;
} robj;
Redis没有直接使用C语言传统的字符串,而是自己构建了一种名为简单动态字符串(Simple dynamic string,SDS)的抽象类型,并将SDS用作Redis的默认字符串表示。其实SDS等同于C语言中的char * ,但它可以存储任意二进制数据,不能像C语言字符串那样以字符’\0’
来标识字符串的结束,因此它必然有个长度字段。
struct sdshdr {
记录buf数组中已使用字节的数量
等于sds所保存字符串的长度
int len;
记录buf数组中未使用字节的数量
int free;
字节数组,用于保存字符串
char buf[];
}
C语言字符串 | SDS |
---|---|
获取字符串长度的复杂度为O(N) | 获取字符串长度的复杂度为O(1) |
API是不安全的,可能会造成缓冲区溢出 | API是安全的,不会造成缓冲区溢出 |
修改字符串长度N次必然需要执行N次内存重分配 | 修改字符串长度N次最多需要执行N次内存重分配 |
只能保存文本数据 | 可以保存文本数据或者二进制数据 |
可以使用所有的 |
可以使用一部分 |
优点:
当有一个列表键包含了数量比较多的元素,又或者列表中包含的元素都是比较长的字符串时,Redis就会使用链表作为列表建的底层实现。
typedef struct listNode {
前置节点
struct listNode *prev;
后置节点
struct listNode *next;
节点的值
void *value;
} listNode;
链表的特点:
List的数据结构
typedef struct list {
表头节点
listNode *head;
表尾节点
listNode *tail;
链表所包含的节点数量
unsigned long len;
节点值复制函数
void *(*dup)(void *ptr);
节点值释放函数
void (*free)(void *ptr);
节点值对比函数
int(*match)(void *ptr, void *key);
} list;
字典的底层是哈希表,类似 C++中的 map ,也就是键值对。
hash的数据结构
typedef struct dictht {
哈希表数组
dictEntry **table;
哈希表大小
unsigned long size;
哈希表大小掩码,用于计算索引值,总是等于size-1
unsigned long sizemark;
该哈希表已有节点的数量
unsigned long used;
} dichht;
哈希算法
当字典被用作数据库的底层实现,或者哈希键的底层实现时,Redis使用MurmurHash算法(非加密型哈希函数)。这种算法的优点在于即使输入的键是规律的,算法仍能给出一个个很好的随机分布性,并且算法的计算速度非常快。
哈希冲突的解决方式
Redis的哈希表使用链地址法来解决键冲突,每个哈希表节点都有一个next指针,多个哈希表节点可以用这个单向链表连接起来,这就解决了键冲突的问题。
特性
HashMap在进行扩容时,会调用resize()函数,重新计算HashMap所需的新容量,然后重新定义一个新容器,将原数组数据进行Hash,放入新的容器中。这个过程就是rehash。Redis的rehash是渐进式的。
如图上面的普通的单链表,查找元素的时间复杂度为O(N)。下面的就是跳跃表,查找的步骤是从头节点的顶层开始,查到第一个大于指定元素的节点时,退回上一节点,在下一层继续查找,查找效率会相对提高。
为了避免插入操作的时间复杂度是O(N),skiplist每层的数量不会严格按照(两层节点数量:一层节点数量)2:1的比例,而是对每个要插入的元素随机一个层数。随机层数的计算过程如下:
一般来说,层的数量越多,访问其他节点的速度越快。
zskiplistNode
typedef struct zskiplistNode {
后退指针
struct zskiplistNode *backward;
分值 权重
double score;
成员对象
robj *obj;
层
struct zskiplistLevel {
// 前进指针
struct zskiplistNode *forward;
// 跨度
unsigned int span;
} leval[];
} zskiplistNode;
zskipList
typedef struct zskiplist {
表头节点和表尾节点
struct zskiplistNode *header, *tail;
表中节点的数量
unsigned long length;
表中层数最大的节点的层数
int leval;
} zskiplist;
特性:
压缩列表(ziplist)是列表键和哈希键的底层实现之一,是为了节省内存造的列表结构。当一个列表键只包含少量列表项,并且每个列表项要么就是小整数值,要么就是长度比较短的字符串,那么Redis就会使用压缩列表来做列表键的底层实现。
Redis常用的五种数据结构包括:字符串(String)、列表(List)、有序集合(Sorted Set)、集合(Set)、哈希表(Hash)。下图展示了 redisObject 、Redis 所有数据类型、以及 Redis 所有编码方式(底层实现)三者之间的关系:
String
String数据结构是简单的key-value类型,value其实不仅可以是String,也可以是数字。 常规key-value缓存应用; 常规计数:微博数,粉丝数等。
Hash
Hash 是一个 string 类型的 field 和 value 的映射表,hash 特别适合用于存储对象,后续操作的时候,你可以直接仅仅修改这个对象中的某个字段的值。 比如我们可以Hash数据结构来存储用户信息,商品信息等。
List
list 就是链表,Redis list 的应用场景非常多,也是Redis最重要的数据结构之一,比如微博的关注列表,粉丝列表, 消息列表等功能都可以用Redis的 list 结构来实现。
Redis list 的实现为一个双向链表,即可以支持反向查找和遍历,更方便操作,不过带来了部分额外的内存开销。
另外可以通过 lrange 命令,就是从某个元素开始读取多少个元素,实现分页查询,这个很棒的一个功 能,基于 Redis 实现简单的高性能分页,可以做类似微博那种下拉不断分页的东西(一页一页的往下走),性能高。
Set
set 对外提供的功能与list类似是一个列表的功能,特殊之处在于 set 是可以自动排重的。
当你需要存储一个列表数据,又不希望出现重复数据时,set是一个很好的选择,并且set提供了判断某个成员是否在一个set集合内的重要接口
,这个也是list所不能提供的。可以基于 set 轻易实现交集、并集、差集的操作
。
比如:在微博应用中,可以将一个用户所有的关注人存在一个集合中,将其所有粉丝存在一个集合。Redis可以非常方便的实现如共同关注、共同粉丝、共同喜好等功能。这个过程也就是求交集的过程,具体命令如下:sinterstore key1 key2 key3将交集存在key1内。
Sorted Set
和set相比,sorted set增加了一个权重参数score,使得集合中的元素能够按score进行有序排列。
举例: 在直播系统中,实时排行信息包含直播间在线用户列表,各种礼物排行榜,弹幕消息(可以理解为按消息维度的消息排行榜)等信息,适合使用 Redis 中的 SortedSet 结构进行存储。
HyperLogLog: 通常用于基数统计。使用少量固定大小的内存,来统计集合中唯一元素的数据。统计结果不是精确值,而是一个带有0.81%标准差(standard error)的近似值。所以,HyperLogLog适用于一些对于统计结果精确度要求不是特别高的场景,例如网站的UV统计(访客统计)。
Geo: Redis 3.2版本的新特性。可以将用户给定的地理位置信息储存起来,并对这些信息进行操作。获取2个位置的距离,根据给定地地理位置坐标获取指定范围内的地址位置集合。
BitMap: 位图。
Stream: 主要用于消息队列,类似于 Kafka,可以认为是 pub/sub 的进阶版。提供了消息的持久化和主从复制功能,可以让任何客户端访问任何时刻的数据,并且能记住每一个客户端的访问位置,还能保证消息不丢失。
一般使用list结构作为队列,rpush生产消息,lpop消费消息。
缺点:在消费者下线的情况下,生产的消息会丢失,得使用专业的消息队列如rabbitmq等
问题:能不能生产一次消费多次呢?
使用pub/sub主题订阅者模式,可以实现1:N的消息队列。
系统不是严格要求缓存+数据库必须一致性的话,最好将读请求和写请求串行化,串到一个内存队列里去,这样就可以保证一定不会出现不一致的情况。数据库与Redis双写一致性
缓存+数据库读写的模式,就是 预留缓存模式
(读取)
(可能没删除成功)
MySQL binlog 增量发布订阅消费+消息队列+增量数据更新到Redis
由于Redis是一种内存型数据库,即服务器在运行时,系统为其分配了一部分内存存储数据,一旦服务器挂了或宕机了,那么数据库里面的数据将会丢失,为了使服务器即使突然关机也能保存数据,必须通过持久化的方式将数据从内存保存到磁盘中。持久化就是把内存的数据写到磁盘中,防止服务器宕机了,导致内存数据丢失。
Redis提供两种持久化机制,分别是RDB和AOF。Redis服务器默认开启RDB,关闭AOF;
RDB持久化
RDB(Redis DataBase):快照。按照一定的时间将内存的数据以快照的形式保存到硬盘中,对应产生的数据文件为dump.rdb(二进制文件)。通过配置文件中的save参数来定义快照的周期。
优点:RDB文件紧凑,体积小,网络传输快,适合全量复制;恢复速度比AOF快。与AOF相比,RDB最重要的优点之一是对性能的影响相对较小。
缺点:RDB文件的致命缺点在于其数据快照的持久化方式决定了必然做不到实时持久化,而在数据越来越重要的今天,数据的大量丢失很多时候是无法接受的,因此AOF持久化成为主流。此外,RDB文件需要满足特定格式,兼容性差(如老版本的Redis不兼容新版本的RDB文件)。
AOF持久化
AOF(Append Only File):将Redis执行的每次写命令记录到单独的日志文件(文本协议数据)中,当重启Redis会重新从持久化的日志文件中恢复数据。与RDB持久化相对应,AOF的优点在于支持秒级持久化、兼容性好,缺点是文件大、恢复速度慢、对性能影响大。
持久化策略选择
(数据库运行时备份,实时的)
,也可以进行读写分离分担Redis读请求,以及在master宕掉后继续提供服务。当Redis的内存(maxmemory参数配置)已满时,它会根据淘汰策略(maxmemory-policy参数配置)进行相应的操作。
不删除策略(no-eviction)
no-eviction:不删除策略。Redis默认策略。达到最大内存限制时,若需要更多内存,直接返回错误信息。
最近最少使用策略(lru)
allkeys-lru:所有key通用;优先删除最近最少使用的key
volatile-lru:只限于设置了 expire 过期时间的部分;优先删除最近最少使用的key
随机策略(random)
allkeys-random:所有key通用;随机删除一部分key。
volatile-random:只限于设置 expire 的部分;随机删除一部分key。
剩余时间短策略(ttl)
volatile-ttl:只限于设置 expire 的部分;优先删除剩余时间(Time to live)
短的key。
最不经常使用策略(lfu)
volatile-lfu:只限于设置 expire 的部分;优先删除最不经常使用的key。
allkeys-lfu:所有key通用;优先删除最不经常使用的key。
volatile-:从已过期时间的数据集中淘汰key。
allkeys-:所有key。
Redis是 key-value 数据库,可以设置Redis缓存的key的过期时间。Redis的过期删除策略就是指当Redis中的key过期了,Redis是如何进行处理的。Redis默认是惰性+定期。
若过期删除该键,不返回任何东西
;若没过期,就返回值。热数据就是访问次数较多的数据,冷数据就是访问很少或者从不访问的数据。
需要注意的是只有热点数据,缓存才有价值;对于冷数据而言,大部分数据可能还没有再次访问到就已经被挤出内存,不仅占用内存,而且价值不大。
数据更新前至少读取两次,缓存才有意义。这个是最基本的策略,如果缓存还没有起作用就失效了,那就没有太大价值了。
Redis基于 Reactor 模式
开发了自己网络事件处理器,它由四部分组成,分别是套接字、IO多路复用程序、文件事件分派器、事件处理器。因为文件事件分派器队列的消费是单线程的,所以Redis才叫单线程模型。它采用 IO 多路复用机制同时监听多个socket,根据 socket 上的事件来选择对应的事件处理器进行处理。
使用 I/O 多路复用程序来同时监听多个套接字, 并根据套接字目前执行的任务来为套接字关联不同的事件处理器。当被监听的套接字准备好执行连接应答(accept)、读取(read)、写入(write)、关闭(close)等操作时, 与操作相对应的文件事件就会产生, 这时文件事件处理器就会调用套接字之前关联好的事件处理器来处理这些事件。
多个 socket 可能会并发产生不同的操作,每个操作对应不同的文件事件,但是 I/O 多路复用程序会监听多个 socket,会将 socket 产生的事件放入队列中排队,事件分派器每次从队列中取出一个事件,把该事件交给对应的事件处理器进行处理。
一句话总结就是:“I/O 多路复用程序负责监听多个套接字, 并向文件事件分派器传送那些产生了事件的套接字。
注: I/O多路复用 参考我之前写的文中,C++ 网络编程(第四篇)
定义: 同一时间内大量键过期(失效),后面的请求都会落到数据库上,造成数据库短时间内承受大量请求而崩掉。
如何解决缓存雪崩
解决思路
哨兵
或者 Redis集群),发现机器宕机尽快补上,选择合适的内存淘汰策略,尽量避免Redis挂掉。设置本地缓存(ehcache)+限流(hystrix)
,通过加锁或者队列来控制读数据库写缓存的线程数量。比如对某个key只允许一个线程查询数据和写缓存,其他线程等待。尽量避免MySQL崩掉,保证服务能正常工作。定义: 一般是黑客故意去请求缓存中不存在的数据,导致所有的请求都落到数据库上,造成数据库短时间内承受大量请求而崩掉。
解决办法
布隆过滤器是引入了k(k>1)个相互独立的哈希函数,保证在给定的空间、误判率下,完成元素判重的过程。 它的优点是空间效率和查询时间都远远超过一般的算法,缺点是有一定的误识别率和删除困难。
缓存空对象
当存储层不命中后,即使返回的空对象也将其缓存起来,同时会设置一个过期时间,之后再访问这个数据将会从缓存中获取,保护了后端数据源
;如果一个查询返回的数据为空(不管是数据不存 在,还是系统故障),我们仍然把这个空结果进行缓存,但它的过期时间会很短,最长不超过五分钟。
这种方法会存在两个问题:
1. 如果空值能够被缓存起来,这就意味着缓存需要更多的空间存储更多的键,因为这当中可能会有很多的空值的键;
2. 即使对空值设置了过期时间,还是会存在缓存层和存储层的数据会有一段时间窗口的不一致,这对于需要保持一致性的业务会有影响。
缓存空对象 | 布隆过滤器 | |
---|---|---|
适用场景 | 数据命中不高;据频繁变化且实时性较高 | 数据命中不高 ;数据相对固定即实时性较低 |
维护成本 | 代码维护简单 ;需要较多的缓存空间 ;数据会出现不一致的现象 | 代码维护较复杂 ;缓存空间要少一些 |
定义: 缓存预热是指系统上线后,将相关的缓存数据直接加载到缓存系统。这样就可以避免在用户请求的时候,先查询数据库,然后再将数据缓存的问题。用户会直接查询事先被预热的缓存数据!
解决思路
除了缓存服务器自带的缓存失效(淘汰)策略
之外(Redis默认的有6中策略可供选择),我们还可以根据具体的业务需求进行自定义的缓存删除策略,常见的策略有两种:
(1)定期去清理过期的缓存;定期删除和惰性删除 (Redis默认的删除策略)
(2)当有用户请求过来时,再判断这个请求所用到的缓存是否过期,过期的话就去底层系统得到新数据并更新缓存。
两者各有优劣,第一种的缺点是维护大量缓存的key是比较麻烦的,第二种的缺点就是每次用户请求过来都要判断缓存失效,逻辑相对比较复杂!具体用哪种方案,大家可以根据自己的应用场景来权衡。
缓存更新的设计模式有四种(以及上面的双写一致性):
Cache aside:查询:先查缓存,缓存没有就查数据库,然后加载至缓存内;更新:先更新数据库,然后让缓存失效;或者先失效缓存然后更新数据库;
Read through:在查询操作中更新缓存,即当缓存失效时,Cache Aside 模式是由调用方负责把数据加载入缓存,而 Read Through 则用缓存服务自己来加载;
Write through:在更新数据时发生。当有数据更新的时候,如果没有命中缓存,直接更新数据库,然后返回。如果命中了缓存,则更新缓存,然后由缓存自己更新数据库;
Write behind caching:俗称write back,在更新数据的时候,只更新缓存,不更新数据库,缓存会异步地定时批量更新数据库;
定义: 指缓存中没有但数据库中有的数据,用户请求并发读缓存数据没有读到,导致所有请求都落在数据库,造成过大压力。比如大并发集中对某个 key 进行访问,当这个key在失效的瞬间,持续的大并发就穿破缓存,直接请求数据库,就像在一个屏障上凿开了一个洞。
解决思路:
定义: 当访问量剧增、服务出现问题(如响应时间慢或不响应)或非核心服务影响到核心流程的性能时,仍然需要保证服务还是可用的,即使是有损服务。系统可以根据一些关键数据进行自动降级,也可以配置开关实现人工降级。
缓存降级的最终目的是保证核心服务可用,即使是(核心服务的功能无法完全保障)有损的。而且有些服务是无法降级的(如加入购物车、结算)。
在进行降级之前要对系统进行梳理,看看系统是不是可以丢卒保帅;从而梳理出哪些必须誓死保护,哪些可降级;比如可以参考日志级别设置预案:
服务降级的目的,是为了防止Redis服务故障,导致数据库跟着一起发生雪崩问题。因此,对于不重要的缓存数据,可以采取服务降级策略,例如一个比较常见的做法就是,Redis出现问题,不去数据库查询,而是直接返回默认值给用户。再比如去年双十一的时候淘宝购物车无法修改地址只能使用默认地址。
事务间相互独立:事务中的所有命令都会序列化,按顺序执行。事务在执行过程中,不会被其他客户端请求的命令中断。事务中的命令要么都执行,要么都不执行。
Redis事务功能是通过 MULTI、EXEC、DISCARD、WATCH
命令实现的。通过MULTI开启事务,然后将请求的命令入队,最后通过EXEC命令依次执行队列中所有的命令。
Redis会将一个事务所有的命令序列化,然后按顺序执行。
1. Redis不支持回滚。Redis在事务失败时不进行回滚,而是继续执行剩下的命令。
2. 若在一个事务中的命令出现错误,那么所有命令都不会执行。
3. 若在一个事务中出现运行错误,那么正确的命令会被执行。
WATCH命令: 是一个乐观锁,可以为Redis提供CAS操作。可以监控一个或多个键,一旦其中有一个键被修改或删除,之后的事务就不执行,监控一直持续到EXEC命令。
MULTI命令: 用于开启事务
。MULTI执行后,Client可以继续向服务器发送任意多条命令,这些命令会存放到一个队列中,当EXEC命令调用后,所有队列中的命令才会被执行。
EXEC命令: 执行所有事务块的命令,可以理解为提交事务。按命令的执行顺序,返回事务中所有命令的返回值。当操作被打断时,返回空值(nil)。
DISCARD命令: 用于清空事务队列,并放弃执行事务。Client从事务状态中退出。
UNWATCH命令: 用于取消WATCH命令对所有key的监控。
注: 事务执行过程中,若服务器收到有EXEC、DISCARD、WATCH、MULTI之外的请求,将会把请求放入队列中。
Redis事务的特性
Redis是单进程模式,队列技术将并发访问变为串行访问,且多个客户端对Redis的连接并不存在竞争关系,Redis可以使用setnx
命令实现分布式锁。
获取锁时调用setnx(setnx若设置值成功,返回1;设置失败,返回0)
。锁的value值会随机生成一个UUID,在释放锁时,会通过UUID进行判断是否为对应的锁,若是该锁,则释放该锁;可以使用 expire 命令为锁添加一个超时时间
,超过该时间则自动释放锁。使用delete
删除锁。
setnx key value
:只有在 key 不存在时,才将key设置为value值。
多个系统同时对一个key进行操作,最后执行的顺序和我们期望的顺序不同,导致结果不同。
如何解决并发竞争问题
可以通过Redis或Zookeeper实现分布式锁。
Redis 官方提出了一种权威的基于 Redis 实现分布式锁的方式名叫 Redlock,此种方式比原先的单节点的方法更安全。
特性
设置锁的方法(和setnx差不多)
SET key_name my_random_value NX PX 30000
# NX 表示if not exist 就设置并返回True,否则不设置并返回False
# PX 表示过期时间用毫秒级, 30000 表示这些毫秒时间后此key过期
这篇文章提出了一个有趣的问题,针对 RedLock 失效的问题,大家看一看Redlock(redis分布式锁)原理分析
哨兵(Sentinel) 是 Redis 的高可用性解决方案:由一个或多个 Sentinel 实例组成的 Sentinel 系统可以监视任意多个主服务器,以及这些主服务器属下的所有从服务器。
哨兵的作用:
哨兵的核心知识
不保证数据零丢失的
,只能保证 redis 集群的高可用性
。当从数据库启动时,会向主数据库发送 sync 命令,主数据库接收到 sync 后开始在后台保存快照rdb,在保存快照期间收到的命令缓存起来,当快照完成时,主数据库会将快照和缓存的命令一块发送给从数据库。复制初始化结束。之后,主数据库每收到1个命令就同步发送给从数据库。 当出现断开重连后,2.8之后的版本会将断线期间的命令传给从数据库。增量复制。
主从复制是乐观复制,当客户端发送写执行给主数据库,主数据库执行完立即将结果返回客户端,并异步的把命令发送给从数据库,从而不影响性能。
参考文章:
数据库和redis双写一致性
Redis面经汇总
菜鸟教程:Redis 列表(List)
Redis系列 | 缓存穿透、击穿、雪崩、预热、更新、降级
Redlock(redis分布式锁)原理分析