Redis为什么这么快
- 完全基于内存,绝大部分请求是纯粹的内存操作;
- 数据结构简单,Redis中的数据结构是专门进行设计的;
- 采用单线程,避免了不必要的上下文切换和竞争条件;
- 使用多路I/O复用模型,非阻塞IO;
- Redis构建了自己的VM机制,因为一般的系统调用系统函数的话,会浪费一定的时间去移动和请求;
Redis和Memcached的区别
Redis支持复杂的数据结构,并且原生支持集群模式;
MC处理请求时使用多线程异步IO的方式,合理利用CPU多核的优势,缺点是,key不能超过250个字节,value不能超过1M字节,key的最大失效时间是30天,只支持K-V结构,不提供持久化和主从同步功能;
Redis的线程模型
Redis内部使用文件事件处理器 file event handler,它采用IO多路复用机制同时监听多个Socket,根据Socket上的事件来选择对应的事件处理器进行处理。
文件事件处理器由四部分组成:
多个Socket;
IO多路复用程序;
文件事件分派器;
事件处理器(连接应答处理器、命令请求处理器、命令回复处理器);
多个Socket并发产生不同的操作,每个操作对应不同的文件事件,IO多路复用程序监听多个Socket,并将其产生的事件放入队列中排队,事件分派器每次从队列中取出一个事件,将其交给对应的事件处理器进行处理。
基础
五种基本的数据结构
字符串String
底层结构
底层类似Java中的ArrayList,从源码的 sds.h/sdshdr 文件中可以看到 Redis 底层对于字符串的定义 SDS,即 Simple Dynamic String 结构。在源码中同样一组结构Redis使用泛型定义了好多次,为什么不直接使用int类型?
因为当字符串比较短时,len
和alloc
可以使用byte
和short
来表示,故为了优化内存,不同长度的字符串使用不同的结构体来表示。
SDS与C语言中字符串的区别
SDS:
/* Note: sdshdr5 is never used, we just access the flags byte directly. |
C语言中使用长度为N+1的字符数组表示长度为N的字符串,且字符数组最后一个元素总是’\0’,会造成如下问题:
- 获取字符串长度的时间复杂度总是O(N),因为C语言的实现中没有保存数组的长度,每次都要遍历整个数组;
- 在拼接或截取字符串时,若操作不当,容易发生缓冲区溢出/内存泄漏的问题,原因同上;
- 只能保存文本数据,因为C语言中的字符串必须符合某种编码(如ASCII)
追加字符串,Redis源码如下:(Redis规定字符串不得超过512MB)
/* Append the specified binary-safe string pointed by 't' of 'len' bytes to the |
基本操作/命令
设置和获取键值对(key存在时,SET命令会直接覆盖旧的值)
批量设置键值对
过期和SET命令扩展
计数:如果 value 是一个整数,还可以对它使用 INCR
命令进行 原子性 的自增操作,这意味着及时多个客户端对同一个 key 进行操作,也决不会导致竞争的情况。
SET counter 100 |
返回原值的GETSET命令
应用
- 缓存功能:String字符串是最常用的数据类型,不仅仅是Redis,各个语言都是最基本类型,因此,利用Redis作为缓存,配合其它数据库作为存储层,利用Redis支持高并发的特点,可以大大加快系统的读写速度、以及降低后端数据库的压力。
- 计数器:许多系统都会使用Redis作为系统的实时计数器,可以快速实现计数和查询的功能。而且最终的数据结果可以按照特定的时间落地到数据库或者其它存储介质当中进行永久保存。
- 共享用户Session:用户重新刷新一次界面,可能需要访问一下数据进行重新登录,或者访问页面缓存Cookie,但是可以利用Redis将用户的Session集中管理,在这种模式只需要保证Redis的高可用,每次用户Session的更新和获取都可以快速完成。大大提高效率。
列表list
底层类似于Java中的LinkedList,故删除/插入极快O(1),索引定位慢O(n)。
基本操作
LPUSH
和RPUSH
分别可以向 list 的左边(头部)和右边(尾部)添加一个新元素;LRANGE
命令可以从 list 中取出一定范围的元素;LINDEX
命令可以从 list 中取出指定下表的元素,相当于 Java 链表操作中的get(int index)
操作;
list实现队列
RPUSH books python java golang |
list实现栈
RPUSH books python java golang |
应用
- 消息队列:Redis的链表结构,可以轻松实现阻塞队列,可以使用左进右出的命令组成来完成队列的设计。比如:数据的生产者可以通过Lpush命令从左边插入数据,多个数据消费者,可以使用BRpop命令阻塞的“抢”列表尾部的数据。
- 文章列表或者数据分页展示的应用。比如,常用的博客网站的文章列表,当用户量越来越多时,而且每一个用户都有自己的文章列表,而且当文章多时,都需要分页展示,这时可以考虑使用Redis的列表,列表不但有序同时还支持按照范围内获取元素,可以完美解决分页查询功能。大大提高查询效率。(lrange命令)
字典hash
底层结构
类似于Java中的HashMap,内部实现也类似,通过”数组 + 链表”的链地址法来解决部分哈希冲突,同时这样的结构也吸收了两种不同数据结构的优点。
typedefstruct dictht { |
table
属性是一个数组,数组中的每个元素都是一个指向 dict.h/dictEntry
结构的指针,而每个 dictEntry
结构保存着一个键值对:
typedefstruct dictEntry { |
从源码中可以看到dict中包含两个dicht,通常情况下只有一个dicht是有值的,但在字典扩容缩容时需要分配新的dicht,进行渐进式搬迁(会在rehash的同时保留新旧两个hash结构,在后续的定时任务以及hash操作指令中,逐渐把旧字典的内容迁移到新字典中,当搬迁完成,使用新的hash结构取而代之。)
扩容条件
条件:hash表中元素的个数等于第一维数组的长度时。
容量:扩容的新数组是原数组大小的2倍。
特殊情况:Redis正在执行bgsave时(持久化命令),Redis尽量不扩容,当hash表达到第一维数组长度5倍时,会执行强制扩容。
缩容条件
条件:元素个数低于数组长度的10%
缩容不会考虑Redis是否在做bgsave。
字典的基本操作
HSET books java "think in java" # 命令行的字符串如果包含空格则需要使用引号包裹 |
应用
这个是类似Map的一种结构,一般可以将结构化的数据,比如一个对象(前提是这个对象没嵌套其他的对象)给缓存在Redis里,然后每次读写缓存的时候,可以就操作Hash里的某个字段。
集合set
类似于Java中的HashSet,内部的键值对是无序的,唯一的。内部实现相当于一个特殊的字典,字典中所有的value都是NULL。
基本操作
HSET books java "think in java" # 命令行的字符串如果包含空格则需要使用引号包裹 |
有序列表zset
底层实现基于跳跃表
基本操作
ZADD books 9.0 "think in java" |
应用
有序集合的使用场景与集合类似,但是set集合不是自动有序的,而Sorted set可以利用分数进行成员间的排序,而且是插入时就排序好。所以当需要一个有序且不重复的集合列表时,就可以选择Sorted set数据结构作为选择方案。
- 排行榜:有序集合经典使用场景。例如视频网站需要对用户上传的视频做排行榜,榜单维护可能是多方面:按照时间、按照播放量、按照获得的赞数等。例如微博热搜榜。
- 用Sorted Sets来做带权重的队列,比如普通消息的score为1,重要消息的score为2,然后工作线程可以选择按score的倒序来获取工作任务。让重要的任务优先执行。
不基本的数据结构
BitMap
通过一个bit位来表示某个元素对应的值或者状态,key就是对应元素本身。可以将其想象成以位为单位的数组,数组的每个单元只能存储0和1,数组的下标叫做偏移量。
优势:极大的节省储存空间。
基本操作
SETBIT key offset value |
返回值:指定偏移量上原来存储的bit
GETBIT key offset |
说明:获取指定偏移量上的bit,当offset比字符串值的长度大,或者key不存在时,返回0。
BITCOUNT key start end |
说明:计算给定字符串中,指定的偏移量上,被设置为1的位的数量。
应用
用户签到
统计活跃用户
HyperLogLog
供不精确的去重技术功能,适合做大规模数据的去重统计。
Redis Module
BloomFilter
底层基于BitMap,由一个位数组和K个哈希函数组成。
当一个元素加入布隆过滤器时,会经历如下步骤:
1)使用K个哈希函数对元素进行K次计算,得到K个哈希值;
2)在位数组中根据得到的哈希值,把对应的下标的值置为1;
优点:空间效率高,占用空间小,查询效率高;
存在的问题:当插入的元素越来越多,即位数组中被置为1的位置也越多,当一个不在布隆过滤器中的元素,经过哈希计算之后,得到的值在位数组中查询,有可能这些位置也都被置为1,即导致误判。
主要命令:
bf.add
添加元素到布隆过滤器中;bf.exists
判断某个元素是否在过滤器中;
在redis中有两个值决定布隆过滤器的准确率:
error_rate
:允许布隆过滤器的错误率,这个值越低过滤器的位数组的大小越大,占用空间也就越大;initial_size
:布隆过滤器可以储存的元素个数,当实际存储的元素个数超过这个值之后,过滤器的准确率会下降;
redis 中有一个命令可以来设置这两个值:
bf.reserve urls 0.01 100 |
- 第一个参数是过滤器的名字;
- 第二个参数是
error_rate
的值; - 第三个参数是
initial_size
的值;
Redis Module,如BloomFilter,RedisSearch,RedisML
Redis的持久化
因为Redis数据全部保存在内存中,如果突然宕机,数据就会全部丢失,所以Redis持久化机制应运而生,即将内存中的数据保存到磁盘中。
持久化的过程
客户端向数据库发出写命令,数据库接收到客户端的写请求后,调用系统API将数据写入磁盘,操作系统将写缓冲区传输到磁盘控制器,然后由磁盘控制器将数据写入实际的物理媒介中。
数据流向:
客户端的内存 -> 服务器的内存 -> 内核缓冲区 -> 磁盘缓存 -> 磁盘
方式一:快照 RDB(Redis Database)
触发时机:
- save的规则满足的情况下;
- 执行flushall命令;
- 退出Redis;
如何恢复rdb文件:
只需要将rdb文件放到redis启动目录即可,redis启动时会自动检查dump.rdb恢复其中的数据。
原理:系统多进程Copy On Write机制 + fork函数
快照将生成一个包含整个数据集的.rdb
文件。
fork创建子进程过程:
分配新的内存块和内核数据结构给子进程;
将父进程部分数据结构内容拷贝给子进程;
添加子进程到系统进程列表中;
fork返回,开始调度器调度;
Redis在持久化时会调用glibc的函数fork产生一个子进程(共享代码块和数据段),将快照持久化交给子进程处理,父进程则继续处理客户端请求。
父进程对内存数据结构进行修改,会对子进程造成影响么?
不会,因为此时使用操作系统的COW机制进行数据段页面的分离,数据段是由很多操作系统的页面组成,当父进程对其中一个页面的数据进行修改时,会将被共享的页面复制一份分离出来,对这个复制的页面进行修改。这时子进程 相应的页面并没有变化,还是进程产生时的数据。
缺点:如果快照保存完成前宕机,这段时间写入Redis的最新数据将会丢失;fork进程会占用一定的内存空间;在生成数据快照时,如果文件很大,客户端可能会暂停几毫秒甚至几秒。
方式二:AOF
Append Only File,每次执行修改内存中数据集的写操作时,都会记录该操作。有灵活的同步策略,no,always,every seconds。
原理:AOF日志是以文件的形式存在的,当程序对AOF日志文件进行写操作时,实际上是将内容写到了内核为文件描述符分配的一个内存缓存中,然后内核会通过glibc提供的fsync(int fd)函数,异步将指定文件内容强制从内核缓存刷到磁盘。
如果aof文件有错位,这时Redis无法connect,可以使用redis-check-aof –fix修复。
缺点:相同规模的数据集,AOF大于RDB;运行效率低于RDB。
rewrite重写规则
优化
Redis提供了bgrewriteaof指令用于对AOF日志进行瘦身。
原理:开辟一个子进程对内存进行遍历转换成一系列Redis的操作指令,序列化到一个新的AOF日志文件中。序列化完成后再将操作期间发生的增量AOF日志追加到这个新的AOF日志文件中,追加完毕后就立即替代旧的AOF日志文件了,瘦身工作就完成了。
方式三:混合持久化
Redis4.0开始提供。将rdb文件的内容和增量的AOF日志文件存在一起。这里的 AOF日志不再是全量的日志,而是自持久化开始到持久化结束的这段时间发生的增量AOF日志,通常这部分AOF日志很小。于是重启Redis时,可以先加载rdb的内容,然后再重放增量AOF日志,重启效率大幅提升。
常见场景
缓存雪崩
原因:缓存服务器重启或key同时失效,请求全部落到数据库。
方案:
1)在Redis中批量存数据时,将key的失效时间设为随机值,让缓存失效时间尽量均匀;
2)设置热点数据永不过期,有更新操作就手动更新缓存;
3)使用快速失败的熔断策略,减少DB瞬间压力;
缓存穿透
原因:缓存和数据库中都没有的数据,用户不断发起请求。(比如用一个不存在的用户id获取用户信息,不论缓存还是数据库都没有,若黑客利用此漏洞进行攻击可能压垮数据库。)
方案:
1)如果一个查询返回的数据为空,将这个空结果进行缓存,但过期时间需设置很短;
2)把所有可能存在的请求的值都存放在布隆过滤器中,当用户请求过来,会先判断用户发来的请求的值是否存在布隆过滤器中。不存在的话,直接返回请求参数错误信息给客户端,从而避免了对数据库的查询压力,存在的话才会走下面的流程。
3)增加参数校验;
缓存击穿
原因:缓存中没有数据但数据库中有数据(缓存时间到期)
方案:
1)设置热点数据永远不过期;
2)加上互斥锁;
public static String getData(String Key) throws InterruptedException { |
六种Key的淘汰策略
Redis中通过maxmemory参数来设定内存的使用上限,当Redis使用内存达到设定的最大值时,会根据配置文件中的策略删除key,从而给新的键值留出空间。
目前Redis提供了6种的淘汰策略(默认的是noeviction):
volatile-lru:在设置了过期时间的键空间中,移除最近最少使用的key;
allkeys-lru:移除最近最少使用的key;
volatile-random,在设置了过期时间的键空间中,随机移除一个key;
allkeys-random,随机移除一个key;
volatile-ttl,在设置了过期时间的键空间中,优先回收存活时间(TTL)较短的键,即移除将要过期的key;
noeviction:当内存使用达到阀值的时候,所有引起申请内存的命令会报错;
三种删除过期key策略
定时删除
在设置某个key的过期时间同时,创建一个定时器,让定时器在该过期时间到来时,立即执行删除操作。
缺点:在过期键比较多时,删除过期键会占用一部分CPU时间,对服务器的响应时间和吞吐量造成影响。
惰性删除
放任键过期不管,每次获取键时,都检查取得的键是否过期,若过期,删除即可。
缺点:如果多个键都已经过期,而这些键又恰好没有被访问,那么这部分的内存就都不会被释放。
定期删除
Redis会周期性的随机检查设置了过期时间的key,删除里面过期的key。
缺点:难以确定操作执行的频率。
应用
Redis集群
主从之间的数据同步
master负责写,将数据同步给slave,slave负责读,分发掉大量请求,并且可以实现水平扩容。复制只能是单向的。
启动一台slave时,发送psync命令给master,如果这个slave是第一次连接到master,则会触发一个全量复制,即master启动一个线程生成RDB快照,并将新的写请求缓存在内存中,RDB文件生成后,master将其发送给slave,slave拿到后写进本地磁盘,然后加载进内存,之后master会把内存中缓存的新命令发给slave。
从节点可以执行写命令么?
可以,修改redis.conf中slave-read-only 设置为no,即可执行写命令,但从节点写命令的数据,其他从节点或主节点是不能获取的。
至少需要三台,一主二从。
作用:
数据冗余:主从复制实现了数据的热备份,是持久化之外的一种数据冗余方式;
故障恢复:当主节点出现问题时,可以由从节点提供服务,实现快速的故障恢复(实际上是一种服务的冗余);
负载均衡:在主从复制的基础上,配合读写分离,可以由主节点提供写服务,从节点提供读服务,分担服务器负载;(尤其在写少读多场景下,可以大大提高Redis服务器的并发量)
高可用(集群)基石:是哨兵和集群能够实施的基础;
info replication # 查看 |
默认情况下 每台Redis服务器都是master,只需配置slave。使用命令SLAVEOF是暂时的,修改配置文件是永久的。
master断开,slave依旧连接到master;当master启动后,slave可以直接获取到master写的信息
复制原理
Slave启动成功连接到master后会发送一个sync同步命令,Master接到命令,启动后台的存盘进程,同时收集所有接收到的用于修改数据集命令,在后台进程执行完毕后,master将传送整个数据文件到slave,并完成一次完全同步。
全量复制:slave服务在接收到数据库文件数据后,将其存盘并加载到内存中;
增量复制:master继续将新的所有收集到的修改命令一次传给slave,完成同步;
宕机后手动配置主机
SLAVEOF no one |
哨兵模式
基于主从模式的优化:当主节点挂掉后,从节点能够自动变成主节点。
工作方式
后台监控主机是否故障,如果故障了根据投票数自动将从库转换为主库。然而一个哨兵进程对Redis服务器进行监控,可能会出现问题,因此 可以使用多个哨兵进行监控,各个哨兵之间还会进行监控,这样就形成了多哨兵模式。
配置哨兵配置文件 sentinel.conf
Example sentinel.conf |
如果master恢复,只能归并到新的master下当做slave。
三个定时任务:
1)每10秒,每个sentinel对master和slave执行info命令:用来发现slave节点;确定主从关系。
2)每2秒,每个sentinel通过master节点的channel(名称为sentinel:hello)交换信息(pub/sub):用来交互对节点的看法以及自身信息。
3)每1秒,每个sentinel对其他sentinel和redis执行ping命令,用于心跳检测,作为节点存活的判断依据。
如果一个实例(instance)距离最后一次有效回复PING命令的时间超过down-after-milliseconds选项所指定的值, 则这个实例会被Sentinel进程标记为主观下线SDOWN;
当有足够数量的Sentinel进程(大于等于配置文件指定的值)在指定时间范围内确认Master进入了主观下线状态SDOWN, 则Master会被标记为客观下线ODOWN;(此时开启故障转移机制)
当Master被Sentinel进程标记为ODOWN后,Sentinel进程向下线的Master的所有Slave发送INFO命令的频率会从10秒一次改为1秒一次;
哨兵组件的主要功能:
集群监控:负责监控Redis master和slave进程是否正常工作;
消息通知:如果某个Redis实例有故障,那么哨兵负责发送消息作为报警通知给管理员;
故障转移:如果 master node 挂掉了,会自动转移到slave node上;
例子:
当master出现故障,此时3个Sentinel节点共同选举了Sentinel3作为领导,负载处理主节点的故障转移:
将slave-1脱离原从节点,升级为master;
将从节点slave-2指向新的主节点;
通知客户端master已更换;
将原主节点(oldMaster)变成slave,指向新的master;
配置中心:如果故障转移发生了,通知client客户端新的master地址;
缺点:较难支持在线扩容,在集群容量达到上限时在线扩容会很复杂;每台redis存储相同数据,浪费内存;
Redis-Cluster
Redis3.0,实现了redis的分布式存储,即每台redis上存储不同的内容。
Redis-Cluster采用无中心结构,特点如下:
所有的节点彼此互联(PING-PONG机制),内部使用二进制协议优化传输速度和带宽;
节点的fail是通过集群中超过半数的节点检测失效时才生效;
客户端与redis节点直连,不需要中间代理层.客户端不需要连接集群所有节点,连接集群中任何一个可用节点即可;
工作方式
每一个节点上,都有两个部分:插槽(slot),它的的取值范围是:0-16383;cluster,可以理解为是一个集群管理的插件。当存取的key到达时,redis会根据crc16的算法得出一个结果,然后把结果对16384求余数,这样每个key都会对应一个编号在0-16383之间的哈希槽,通过这个值,找到对应的插槽所对应的节点,然后直接自动跳转到这个对应的节点上进行存取操作。
从redis中没有slot,不会参与集群投票,仅仅作为主机的备份。
为了保证高可用,redis-cluster集群引入了主从模式,一个主节点对应一个或者多个从节点,当主节点宕机的时候,就会启用从节点。当其它主节点ping一个主节点A时,如果半数以上的主节点与A通信超时,那么认为主节点A宕机了。
什么情况下集群不可用?
- 任意主节点和它的从节点都宕机了;
- 集群中超过半数的master挂掉,无论是否有slave,集群都进入fail状态;
KV,DB读写模式
Cache Aside Pattern
- 读的时候,先取缓存,如果缓存中没有,就读数据库,取出数据后放入缓存,同时返回响应。
- 更新的时候,先更新数据库,然后删除缓存。
为什么删除缓存,而不是更新缓存?
- 可能对应的缓存数据需要综合其他数据进行计算;
- 假如数据在 1 分钟内修改了 20 次,或 100 次,那么缓存就更新 20 次、100 次;但是这个缓存在 1 分钟内只被读取了 1 次,存在大量的冷数据;
- Lazy加载思想;
Redis分布式锁
一致性hash算法
将整个哈希值空间按顺时针方向组织成一个虚拟的圆环;
根据hash函数计算出的hash值,将对象key映射到环形空间;
使用相同的hash算法将cache也映射到这个环形空间;
每个key顺时针找到的第一个cache节点就是存储位置;
优势:假如有一台服务器不可用,受影响的仅仅是从此服务器逆时针方向的前一台服务器之间的数据,其他的不会受影响;
数据倾斜:当服务节点较少时,容易因为节点分布不均匀而造成数据倾斜现象,此时可以运用一致性哈希的虚拟节点机制,即对每一个服务节点计算多个哈希,每个计算结果位置都放置一个此服务节点,称为虚拟节点。
Redis实现消息队列
Redis提供了两种方式来做消息队列,生产消费模式,和发布订阅模式。
生产消费模式
Redis数据结构的列表
List
提供了push
和pup
命令,遵循着先入先出FIFO
的原则。使用push/pop
方式的优点在于消息可以持久化,缺点是一条消息只能被一个消费者接收,是一种比较简陋的消息队列。如果队列空了,消费者会陷入pop死循环,即使没有数据也不会停止,影响Redis性能,所以可以使用brpop和blpop实现阻塞读取,阻塞读在队列没有数据时会立即进入休眠状态,一旦数据到来则立即被唤醒,消息的延迟几乎为零。需要注意的是如果线程一直阻塞在那里,连接就会被服务器主动断开来减少资源占用,这时
blpop/brpop
会抛出异常,所以编写消费段时需要注意异常的处理。可以设置超时时间,如果列表中没有消息则一直阻塞直到超时,减小Redis的压力。发布订阅模式
原理:通过subscribe命令订阅某频道后,redis-server里维护了一个字典,字典的键就是一个个channel,字典的值则是一个链表,链表中保存了所有订阅这个channel的客户端。subscribe命令的关键,就是将客户端添加到给定channel的订阅链表中。
Redis自带
pub/sub
机制即发布订阅模式,此模式中生产者producer
和消费者consumer
之间的关系是一对多的,也就是一条消息会被多个消费者所消费,当只有一个消费者时可视为一对一的消息队列。发布订阅模式常见命令:
psubscribe
订阅一个或多个符合给定模式的频道publish
将消息发布到指定的频道pubsub
查看订阅与发布系统状态pubsub channels pattern
列出当前的活跃频道pubsub numsub channel-1 channel-n
获取给定频道的订阅者数量pubsub numpat
获取订阅模式的数量punsubscribe
指示客户端退订所有给定模式subscribe
订阅给定的一个或多个频道的消息unsubscribe
指示客户端退订给定的频道场景:实时消息系统;实时聊天,将消息回显给所有人即可;订阅/关注系统;
Redis实现延时消息队列
使用zset,消息作为value,时间作为score
Hot Key和Big Key
Redis脑裂
注意:较新版本的redis.conf文件中的参数变成了
min-replicas-to-write 3 |
按照上面的配置,要求至少3个slave节点,且数据复制和同步的延迟不能超过10秒,否则的话master就会拒绝写请求,配置了这两个参数之后,如果发生集群脑裂,原先的master节点接收到客户端的写入请求会拒绝,就可以减少数据同步之后的数据丢失。
redis中的异步复制情况下的数据丢失问题也能使用这两个参数