Redis
Redis是C语言开发的一个开源的(遵从BSD协议)高性能键值对(key-value)的内存数据库,可以用作数据库、缓存、消息中间件等。它是一种NoSQL(not-only sql,泛指非关系型数据库)的数据库。
- 1、性能优秀,数据在内存中,读写速度非常快,支持并发10W QPS;
- 2、单进程单线程,是线程安全的,采用IO多路复用机制;
- 3、丰富的数据类型,支持字符串(strings)、散列(hashes)、列表(lists)、集合(sets)、有序集合(sorted sets)等;4、支持数据持久化。可以将内存中数据保存在磁盘中,重启时加载;5、主从复制,哨兵,高可用;6、可以用作分布式锁;7、可以作为消息中间件使用,支持发布订阅
Redis内部内存管理
数据类型
Redis的数据类型是根据 Value来划分的,Key默认全都是String类型.redis一共有五种数据类型包括String,Set,Zset,List,hash
- 1、string是redis最基本的类型,一个key对应一个value。value不仅是string,也可以是数字。string类型是二进制安全的,意思是redis的string类型可以包含任何数据,比如jpg图片或者序列化的对象。string类型的值最大能存储512M。
- 2、Hash是一个键值(key-value)的集合。redis的hash是一个string的key和value的映射表,Hash特别适合存储对象。常用命令:hget,hset,hgetall等。
- 3、list列表是简单的字符串列表,按照插入顺序排序。可以添加一个元素到列表的头部(左边)或者尾部(右边) 常用命令:lpush、rpush、lpop、rpop、lrange(获取列表片段)等。redis提供了List的push和pop操作,还提供了操作某一段的api,可以直接查询或者删除某一段的元素。实现方式:redis list的是实现是一个双向链表,既可以支持反向查找和遍历,更方便操作,不过带来了额外的内存开销。
- 4、set是string类型的无序集合。集合是通过hashtable实现的。set中的元素是没有顺序的,而且是没有重复的。常用命令:sdd、spop、smembers、sunion等。应用场景:redis set对外提供的功能和list一样是一个列表,特殊之处在于set是自动去重的,而且set提供了判断某个成员是否在一个set集合中。
- 5、zset和set一样是string类型元素的集合,且不允许重复的元素。常用命令:zadd、zrange、zrem、zcard等。使用场景:sorted set可以通过用户额外提供一个优先级(score)的参数来为成员排序,并且是插入有序的,即自动排序。当你需要一个有序的并且不重复的集合列表,那么可以选择sorted set结构。和set相比,sorted set关联了一个double类型权重的参数score,使得集合中的元素能够按照score进行有序排列,redis正是通过分数来为集合中的成员进行从小到大的排序。实现方式:Redis sorted set的内部使用HashMap和跳跃表(skipList)来保证数据的存储和有序,HashMap里放的是成员到score的映射,而跳跃表里存放的是所有的成员,排序依据是HashMap里存的score,使用跳跃表的结构可以获得比较高的查找效率,并且在实现上比较简单。
类型 | 简介 | 特性 | 场景 |
---|---|---|---|
string(字符串) | 二进制安全 | 可以包含任何数据,比如jpg图片或者序列化对象 | — |
Hash(字典) | 键值对集合,即编程语言中的map类型 | 适合存储对象,并且可以像数据库中的update一个属性一样只修改某一项属性值 | 存储、读取、修改用户属性 |
List(列表) | 链表(双向链表) | 增删快,提供了操作某一元素的api | 最新消息排行;消息队列 |
set(集合) | hash表实现,元素不重复 | 添加、删除、查找的复杂度都是O(1),提供了求交集、并集、差集的操作 | 共同好友;利用唯一性,统计访问网站的所有Ip |
sorted set(有序集合) | 将set中的元素增加一个权重参数score,元素按score有序排列 | 数据插入集合时,已经进行了天然排序 | 排行榜;带权重的消息队列 |
编码方式
编码方式 | 对应的底层数据结构 |
---|---|
INT | long 类型的整数 |
EMBSTR | embstr 编码的简单动态字符串 |
RAW | 简单动态字符串 |
HT | 字典 |
LINKEDLIST | 双端链表 |
ZIPLIST | 压缩列表 |
INTSET | 整数集合 |
SKIPLIST | 跳跃表和字典 |
类型 | 编码方式 | 对象 |
---|---|---|
String | INT | 使用整数值实现的字符串对象。 |
String | EMBSTR | 使用 embstr 编码的简单动态字符串实现的字符串对象。 |
String | RAW | 使用简单动态字符串实现的字符串对象 |
List | ZIPLIST | 使用压缩列表实现的列表对象 |
List | LINKEDLIST | 使用双端链表实现的列表对象 |
Hash | ZIPLIST | 使用压缩列表实现的哈希对象 |
Hash | HT | 使用字典实现的哈希对象 |
Set | INTSET | 使用整数集合实现的对象 |
Set | HT | 使用字典实现的集合对象 |
ZSET | ZIPLIST | 使用压缩列表实现的有序集合对象。 |
ZSET | SKIPLIST | 使用跳跃表和字典实现的有序集合对象 |
String类型
String类型的数据结构存储方式有三种int、raw、embstr,
- Redis中规定假如存储的是**「整数型值」,比如
set num 123
这样的类型,就会使用 int的存储方式进行存储,在redisObject的「ptr属性」**中就会保存该值 - 假如存储的**「字符串是一个字符串值并且长度大于32个字节」就会使用
SDS(simple dynamic string)
方式进行存储,并且encoding设置为raw;若是「字符串长度小于等于32个字节」**就会将encoding改为embstr来保存字符串。
SDS称为**「简单动态字符串」**,对于SDS中的定义在Redis的源码中有的三个属性int len、int free、char buf[]
。len保存了字符串的长度,free表示buf数组中未使用的字节数量,buf数组则是保存字符串的每一个字符元素。因此当你在Redsi中存储一个字符串Hello时,根据Redis的源代码的描述可以画出SDS的形式的redisObject结构图如下图所示:
c语言字符串 | SDS |
---|---|
获取长度的时间复杂度为O(n) | 获取长度的时间复杂度为O(1) |
不是二进制安全的 | 是二进制安全的 |
只能保存字符串 | 还可以保存二进制数据 |
n次增长字符串必然会带来n次的内存分配 | n次增长字符串内存分配的次数<=n |
- c语言中两个字符串拼接,若是没有分配足够长度的内存空间就会出现缓冲区溢出的情况;而SDS会先根据len属性判断空间是否满足要求,若是空间不够,就会进行相应的空间扩展,所以不会出现缓冲区溢出的情况。
- SDS还提供**「空间预分配」和「惰性空间释放」两种策略。在为字符串分配空间时,分配的空间比实际要多,这样就能「减少连续的执行字符串增长带来内存重新分配的次数」**。
- 当字符串被缩短的时候,SDS也不会立即回收不适用的空间,而是通过
free
属性将不使用的空间记录下来,等后面使用的时候再释放。 - 具体的空间预分配原则是:「当修改字符串后的长度len小于1MB,就会预分配和len一样长度的空间,即len=free;若是len大于1MB,free分配的空间大小就为1MB」。
Hash类型
Hash对象的实现方式有两种分别是ziplist、hashtable
,其中hashtable的存储方式key是String类型的,value也是以key value
的形式进行存储。
字典类型的底层就是hashtable实现的,明白了字典的底层实现原理也就是明白了hashtable的实现原理,hashtable的实现原理可以与HashMap的是底层原理相类比。
ziplist
压缩列表(ziplist)
是一组连续内存块组成的顺序的数据结构,压缩列表能够节省空间,压缩列表中使用多个节点来存储数据。
压缩列表是列表键和哈希键底层实现的原理之一,「压缩列表并不是以某种压缩算法进行压缩存储数据,而是它表示一组连续的内存空间的使用,节省空间」,压缩列表的内存结构图如下
zlbytes
:4个字节的大小,记录压缩列表占用内存的字节数。zltail
:4个字节大小,记录表尾节点距离起始地址的偏移量,用于快速定位到尾节点的地址。zllen
:2个字节的大小,记录压缩列表中的节点数。entry
:表示列表中的每一个节点。zlend
:表示压缩列表的特殊结束符号'0xFF'
。
再压缩列表中每一个entry节点又有三部分组成,包括previous_entry_ength、encoding、content
。
-
previous_entry_ength
表示前一个节点entry的长度,可用于计算前一个节点的其实地址,因为他们的地址是连续的。 -
encoding:这里保存的是content的内容类型和长度。
-
content:content保存的是每一个节点的内容。
-
zlbytes
:4个字节的大小,记录压缩列表占用内存的字节数。 -
zltail
:4个字节大小,记录表尾节点距离起始地址的偏移量,用于快速定位到尾节点的地址。 -
zllen
:2个字节的大小,记录压缩列表中的节点数。 -
entry
:表示列表中的每一个节点。 -
zlend
:表示压缩列表的特殊结束符号'0xFF'
。
再压缩列表中每一个entry节点又有三部分组成,包括previous_entry_ength、encoding、content
。
previous_entry_ength
表示前一个节点entry的长度,可用于计算前一个节点的其实地址,因为他们的地址是连续的。- encoding:这里保存的是content的内容类型和长度。
- content:content保存的是每一个节点的内容。
List类型
Redis中的列表在3.2之前的版本是使用ziplist
和linkedlist
进行实现的。在3.2之后的版本就是引入了quicklist
。
ziplist压缩列表上面已经讲过了,我们来看看linkedlist和quicklist的结构是怎么样的。
linkedlist是一个双向链表,他和普通的链表一样都是由指向前后节点的指针。插入、修改、更新的时间复杂度尾O(1),但是查询的时间复杂度确实O(n)。
linkedlist和quicklist的底层实现是采用链表进行实现,在c语言中并没有内置的链表这种数据结构,Redis实现了自己的链表结构
Redis中链表的特性:
- 每一个节点都有指向前一个节点和后一个节点的指针。
- 头节点和尾节点的prev和next指针指向为null,所以链表是无环的。
- 链表有自己长度的信息,获取长度的时间复杂度为O(1)。
Set集合
Redis中列表和集合都可以用来存储字符串,但是**「Set是不可重复的集合,而List列表可以存储相同的字符串」**,Set集合是无序的这个和后面讲的ZSet有序集合相对。
Set的底层实现是**「ht和intset」**,ht(哈希表)前面已经详细了解过,下面我们来看看inset类型的存储结构。
inset也叫做整数集合,用于保存整数值的数据结构类型,它可以保存int16_t
、int32_t
或者int64_t
的整数值。
在整数集合中,有三个属性值encoding、length、contents[]
,分别表示编码方式、整数集合的长度、以及元素内容,length就是记录contents里面的大小。
在整数集合新增元素的时候,若是超出了原集合的长度大小,就会对集合进行升级,具体的升级过程如下:
- 首先扩展底层数组的大小,并且数组的类型为新元素的类型。
- 然后将原来的数组中的元素转为新元素的类型,并放到扩展后数组对应的位置。
- 整数集合升级后就不会再降级,编码会一直保持升级后的状态。
ZSet集合
ZSet是有序集合,从上面的图中可以看到ZSet的底层实现是ziplist
和skiplist
实现的,ziplist上面已经详细讲过,这里来讲解skiplist的结构实现。
skiplist
也叫做**「跳跃表」**,跳跃表是一种有序的数据结构,它通过每一个节点维持多个指向其它节点的指针,从而达到快速访问的目的。
skiplist有如下几个特点:
- 有很多层组成,由上到下节点数逐渐密集,最上层的节点最稀疏,跨度也最大。
- 每一层都是一个有序链表,至少包含两个节点,头节点和尾节点。
- 每一层的每一个每一个节点都含有指向同一层下一个节点和下一层同一个位置节点的指针。
- 如果一个节点在某一层出现,那么该以下的所有链表同一个位置都会出现该节点。
Redis事务操作
Redis事务是一组命令的集合,将多个命令进行打包,然后这些命令会被顺序的添加到队列中,并且按顺序的执行这些命令。
「Redis事务中没有像Mysql关系型数据库事务隔离级别的概念,不能保证原子性操作,也没有像Mysql那样执行事务失败会进行回滚操作」。
这个与Redis的特点:「快速、高效」有着密切的关联,「因为一些列回滚操作、像事务隔离级别那这样加锁、解锁,是非常消耗性能的」。所以,Redis中执行事务的流程只需要简单的下面三个步骤:
- 开始事务(MULTI)
- 命令入队
- 执行事务(EXEC)、撤销事务(DISCARD )
在Redis中事务的实现主要是通过如下的命令实现的:
命令 | 功能描述 |
---|---|
MULTI | 「事务开始的命令」,执行该命令后,后面执行的对Redis数据类型的**「操作命令都会顺序的放进队列中」**,等待执行EXEC命令后队列中的命令才会被执行 |
DISCARD | 「放弃执行队列中的命令」,你可以理解为Mysql的回滚操作,「并且将当前的状态从事务状态改为非事务状态」。 |
EXEC | 执行该命令后**「表示顺序执行队列中的命令」,执行完后并将结果显示在客户端,「将当前状态从事务状态改为非事务状态」**。若是执行该命令之前有key被执行WATCH命令并且又被其它客户端修改,那么就会放弃执行队列中的所有命令,在客户端显示报错信息,若是没有修改就会执行队列中的所有命令。 |
WATCH key | 表示指定监视某个key,「该命令只能在MULTI命令之前执行」,如果监视的key被其他客户端修改,「EXEC将会放弃执行队列中的所有命令」 |
UNWATCH | 「取消监视之前通过WATCH 命令监视的key」,通过执行EXEC 、DISCARD 两个命令之前监视的key也会被取消监视 |
Redis客户端的命令执行后若是当前状态处于事务状态命令就会进入队列中,并且返回QUEUED
字符串,表示该命令已经进入了命令队列中,并且**「事务队列是以先进先出(FIFO)的方式保存入队的命令」**的。
当客户端执行EXEC命令的时候,上面的命令队列就会被按照先进先出的顺序被执行,当然执行的结果有成功有失败,这个后面分析。
上面说到当客户端处于非事务的状态命令发送到服务端会被立即执行,若是客户端处于事务状态命令就会被放进命令队列。
命令入队的时候,会按照顺序进入队列,队列以先进先出的特点来执行队列中的命令。
事务的命令队列中有三个参数分别是:「要执行的命令」、「命令的参数」、「参数的个数」。
若是客户端处于事务状态,执行的是EXEC、DISCARD、UNWATCH
这些操作事务的命令,也会被立即执行。
WATCH
命令是在MULTI命令之前执行的,表示监视任意数量的key,与它对应的命令就是UNWATCH
命令,取消监视的key。
WATCH
命令有点**「类似于乐观锁机制」,在事务执行的时候,若是被监视的任意一个key被更改,则队列中的命令不会被执行,直接向客户端返回(nil)表示事务执行失败。WATCH命令的底层实现中保存了watched_keys
字典,「字典的键保存的是监视的key,值是一个链表,链表中的每个节点值保存的是监视该key的客户端」**。
持久化操作
Redis
是一个基于内存的非关系型的数据库,数据保存在内存中,但是内存中的数据也容易发生丢失。这里Redis就为我们提供了持久化的机制,分别是RDB(Redis DataBase)
和AOF(Append Only File)
。
RDB持久化机制
RDB持久化就是将当前进程的数据以生成快照的形式持久化到磁盘中。对于快照的理解,我们可以理解为将当前线程的数据以拍照的形式保存下来。
RDB持久化的时候会单独fork一个与当前进程一摸一样的子进程来进行持久化,因此RDB持久化有如下特点:
- 开机恢复数据快。
- 写入持久化文件快。
RDB的持久化也是Redis默认的持久化机制,它会把内存中的数据以快照的形式写入默认文件名为dump.rdb
中保存
持久化触发时机
在RDB机制中触发内存中的数据进行持久化,有以下三种方式:
(1)save命令:
save命令不会fork子进程,通过阻塞当前Redis服务器,直到RDB完成为止,所以该命令在生产中一般不会使用。save命令执行原理图如下:
(2)bgsave命令:
bgsave
命令会在后台fork一个与Redis主线程一模一样的子线程,由子线程负责内存中的数据持久化。
这样fork与主线程一样的子线程消耗了内存,但是不会阻塞主线程处理客户端请求,是以空间换时间的方式快照内存中的数据到到文件中。
bgsave
命令阻塞只会发生在fork子线程的时候,这段时间发生的非常短,可以忽略不计
(3)自动化
除了上面在命令行使用save和bgsave命令触发持久化,也可以在redis.conf
配置文件中,完成配置,如下图所示:在新安装的redis中由默认的以上三个save配置,save 900 1
表示900秒内如果至少有1个key值变化,则进行持久化保存数据;
save 300 10
则表示300秒内如果至少有10个key值发生变化,则进行持久化,save 60 10000
以此类推。
通过以上的分析可以得出以下save和bgsave的对比区别:
- save是同步持久化数据,而bgsave是异步持久化数据。
save
不会fork子进程,通过主进程持久化数据,会阻塞处理客户端的请求,而bdsave
会fork
子进程持久化数据,同时还可以处理客户端请求,高效。- save不会消耗内存,而bgsave会消耗内存。
RDB的优缺点
缺点: RDB持久化后的文件是紧凑的二进制文件,适合于备份、全量复制、大规模数据恢复的场景,对数据完整性和一致性要求不高,RDB会丢失最后一次快照的数据。
优点: 开机的恢复数据快,写入持久化文件快。
AOF持久化机制
AOF持久化机制是以日志的形式记录Redis中的每一次的增删改操作,不会记录查询操作,以文本的形式记录,打开记录的日志文件就可以查看操作记录。
AOF是默认不开启的,若是想开启AOF,在如下图的配置修改即可只需要把appendonly no
修改为appendonly yes
即可开启,在AOF中通过appendfilename
配置生成的文件名,该文件名默认为appendonly.aof
,路径也是通过dir配置的,这个与RDB的一样
AOF触发机制
AOF带来的持久化更加安全可靠,默认提供三种触发机制,如下所示:
no
:表示等操作系统等数据缓存同步到磁盘中(快、持久化没保证)。always
:同步持久化,每次发生数据变更时,就会立即记录到磁盘中(慢,安全)。everysec
:表示每秒同步一次(默认值,很快,但是会丢失一秒内的数据)。
AOF中每秒同步也是异步完成的,效率是非常高的,由于该机制对日志文件的写入操作是采用append
的形式。
因此在写入的过程即使宕机,也不会丢失已经存入日志文件的数据,数据的完整性是非常高的。
AOF重写机制
但是,在写入所有的操作到日志文件中时,就会出现日志文件很多重复的操作,甚至是无效的操作,导致日志文件越来越大。
所谓的无效的的操作,举个例子,比如某一时刻对一个k++,然后后面的某一时刻k–,这样k的值是保持不变的,那么这两次的操作就是无效的。
如果像这样的无效操作很多,记录的文件臃肿,就浪费了资源空间,所以在Redis中出现了rewrite
机制。
redis提供了bgrewriteaof命令。将内存中的数据以命令的方式保存到临时文件中,同时会fork出一条新进程来将文件重写。
重写AOF的日志文件不是读取旧的日志文件瘦身,而是将内存中的数据用命令的方式重写一个AOF文件,重新保存替换原来旧的日志文件,因此内存中的数据才是最新的。
重写操作也会fork
一个子进程来处理重写操作,重写以内存中的数据作为重写的源,避免了操作的冗余性,保证了数据的最新。
在Redis以append的形式将修改的数据写入老的磁盘中 ,同时Redis也会创建一个新的文件用于记录此期间有哪些命令被执行。
当AOF的日志文件增长到一定大小的时候Redis就能够bgrewriteaof对日志文件进行重写瘦身。当AOF配置文件大于改配置项时自动开启重写(这里指超过原大小的100%)
AOF的优缺点
优点: AOF更好保证数据不会被丢失,最多只丢失一秒内的数据,通过fork一个子进程处理持久化操作,保证了主进程不会进程io操作,能高效的处理客户端的请求。
另外重写操作保证了数据的有效性,即使日志文件过大也会进行重写。
AOF的日志文件的记录可读性非常的高,即使某一时刻有人执行flushall
清空了所有数据,只需要拿到aof的日志文件,然后把最后一条的flushall给删除掉,就可以恢复数据。
缺点: 对于相同数量的数据集而言,AOF文件通常要大于RDB文件。RDB 在恢复大数据集时的速度比 AOF 的恢复速度要快。AOF在运行效率上往往会慢于RDB。
混合持久化
混合持久化也是通过bgrewriteaof
来完成的,不同的是当开启混合持久化时,fork出的子进程先将共享内存的数据以RDB方式写入aof文件中,然后再将重写缓冲区的增量命令以AOF方式写入文件中。
写入完成后通知主进程统计信息,并将新的含有RDB格式和AOF格式的AOF文件替换旧的AOF文件。简单的说:新的AOF文件前半段是以RDB格式的全量数据后半段是AOF格式的增量数据。
优点: 混合持久化结合RDB持久化和AOF持久化的优点,由于绝大部分的格式是RDB格式,加载速度快,增量数据以AOF方式保存,数据更少的丢失。
RDB和AOF优势和劣势
rdb适合大规模的数据恢复,由于rdb是以快照的形式持久化数据,恢复的数据快,在一定的时间备份一次,而aof的保证数据更加完整,损失的数据只在秒内。
具体哪种更适合生产,在官方的建议中两种持久化机制同时开启,如果两种机制同时开启,优先使用aof持久化机制。
过期策略
Redis是key-value数据库,我们可以设置Redis中缓存的key的过期时间。Redis的过期策略就是指当Redis中缓存的key过期了,Redis如何处理。
过期策略通常有以下三种:
- 定时过期:每个设置过期时间的key都需要创建一个定时器,到过期时间就会立即清除。该策略可以立即清除过期的数据,对内存很友好;但是会占用大量的CPU资源去处理过期的数据,从而影响缓存的响应时间和吞吐量。
- 惰性过期:只有当访问一个key时,才会判断该key是否已过期,过期则清除。该策略可以最大化地节省CPU资源,却对内存非常不友好。极端情况可能出现大量的过期key没有再次被访问,从而不会被清除,占用大量内存。
- 定期过期:每隔一定的时间,会扫描一定数量的数据库的expires字典中一定数量的key,并清除其中已过期的key。该策略是前两者的一个折中方案。通过调整定时扫描的时间间隔和每次扫描的限定耗时,可以在不同情况下使得CPU和内存资源达到最优的平衡效果。(expires字典会保存所有设置了过期时间的key的过期时间数据,其中,key是指向键空间中的某个键的指针,value是该键的毫秒精度的UNIX时间戳表示的过期时间。键空间是指该Redis集群中保存的所有键。)
- Redis中同时使用了惰性过期和定期过期两种过期策略。
除了缓存服务器自带的缓存失效策略之外(Redis默认的有6中策略可供选择),我们还可以根据具体的业务需求进行自定义的缓存淘汰,常见的策略有两种:
- 定时去清理过期的缓存;
- 当有用户请求过来时,再判断这个请求所用到的缓存是否过期,过期的话就去底层系统得到新数据并更新缓存。
淘汰策略
全局的键空间选择性移除
-
noeviction:当内存不足以容纳新写入数据时,新写入操作会报错。
-
allkeys-lru:当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的key。(这个是最常用的)
-
allkeys-random:当内存不足以容纳新写入数据时,在键空间中,随机移除某个key
设置过期时间的键空间选择性移除
-
volatile-lru:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,移除最近最少使用的key。
-
volatile-random:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,随机移除某个key。
-
volatile-ttl:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,有更早过期时间的key优先移除。
Redis的内存淘汰策略的选取并不会影响过期的key的处理。内存淘汰策略用于处理内存不足时的需要申请额外空间的数据;过期策略用于处理过期的缓存数据。
LRU指淘汰最近访问时间久远的值:redis在redisOject中会存入一个lru的值大小为24bit记录最近访问的时间,原始的lru算法是将所有的key集合在一起以lru值的大小形成一个链表两端分别为LRU和MRU,淘汰LRU的一端,由于在计算时有大量的数据移动和删除所以在redis中lru的淘汰策略可以分为以下步骤
- 第一次选取时在redis中随机选取N(maxmemory_sample)个样本,比较样本的LRU的值
- 第二次以第一次选取的最小的LRU的值,挑选比当前LRU的值更小的值放入集合,当达到N(maxmemory_sample )时淘汰最小的
LFU是指淘汰使用次数最小的值:LFU与LRU原理相同,只是在24位记录中分离出8位以存储最近使用的次数,这样可以防止热点数据被删除而导致的数据库压力
在淘汰中会先比较访问次数,如果淘汰的次数相同,则比较两个KEY的最近的访问时间。但8比特最大数只有255redis使用了增长速度在更慢的计数规则
集群
主从复制
主从配置结合哨兵模式能解决单点故障问题,提高redis可用性。从节点仅提供读操作,主节点提供写操作。对于读多写少的状况,可给主节点配置多个从节点,从而提高响应效率。
从服务器挂掉之后重启之后并不会直接变成从服务器,会直接成为主服务器,重新加入后,会重头复制主服务器中的数据,主服务器挂掉之后重启之后,从服务器仍然识别主服务器。
复制过程:
1、从节点执行slave of[masterIP] [masterPort],保存主节点信息
2、从节点中的定时任务发现主节点信息,建立和主节点的socket连接
3、从节点发送Ping信号,主节点返回Pong,两边能互相通信
4、连接建立后,主节点将所有数据发送给从节点(数据同步)
5、主节点把当前的数据同步给从节点后,便完成了复制的建立过程。接下来,主节点就会持续的把写命令发送给从节点,保证主从数据一致性。
**高可用:**redis在集群设置中实现高可用的方式时为一致性哈希算法
数据同步过程:
redis2.8之前使用sync[runId] [offset]同步命令,redis2.8之后使用psync[runId] [offset]命令。两者不同在于,sync命令仅支持全量复制过程,psync支持全量和部分复制。介绍同步之前,先介绍几个概念:runId:每个redis节点启动都会生成唯一的uuid,每次redis重启后,runId都会发生变化。offset:主节点和从节点都各自维护自己的主从复制偏移量offset,当主节点有写入命令时,offset=offset+命令的字节长度。从节点在收到主节点发送的命令后,也会增加自己的offset,并把自己的offset发送给主节点。这样,主节点同时保存自己的offset和从节点的offset,通过对比offset来判断主从节点数据是否一致。repl_backlog_size:保存在主节点上的一个固定长度的先进先出队列,默认大小是1MB。
(1)主节点发送数据给从节点过程中,主节点还会进行一些写操作,这时候的数据存储在复制缓冲区中。从节点同步主节点数据完成后,主节点将缓冲区的数据继续发送给从节点,用于部分复制。
(2)主节点响应写命令时,不但会把命名发送给从节点,还会写入复制积压缓冲区,用于复制命令丢失的数据补救。
-
- 上面是psync的执行流程:从节点发送psync[runId] [offset]命令,主节点有三种响应:(1)FULLRESYNC:第一次连接,进行全量复制 (2)CONTINUE:进行部分复制 (3)ERR:不支持psync命令,进行全量复制
sentinel,中文名是哨兵。哨兵是 redis 集群机构中非常重要的一个组件,主要有以下功能:
- 集群监控:负责监控 redis master 和 slave 进程是否正常工作。
- 消息通知:如果某个 redis 实例有故障,那么哨兵负责发送消息作为报警通知给管理员。
- 故障转移:如果 master node 挂掉了,会自动转移到 slave node 上。
- 配置中心:如果故障转移发生了,通知 client 客户端新的 master
选取规则:slave -piroty值越小优先选取,偏移量最大的优先选取(获取主机数据最全的),runid谁最小
哨兵用于实现 redis 集群的高可用,本身也是分布式的,作为一个哨兵集群去运行,互相协同工作。
- 故障转移时,判断一个 master node 是否宕机了,需要大部分的哨兵都同意才行,涉及到了分布式选举的问题。
- 即使部分哨兵节点挂掉了,哨兵集群还是能正常工作的,因为如果一个作为高可用机制重要组成部分的故障转移系统本身是单点的,那就很坑爹了。
- 哨兵至少需要 3 个实例,来保证自己的健壮性。
- 哨兵 + redis 主从的部署架构,是不保证数据零丢失的,只能保证 redis 集群的高可用性。
- 对于哨兵 + redis 主从这种复杂的部署架构,尽量在测试环境和生产环境,都进行充足的测试和演练。
哨兵工作原理
1、每个Sentinel节点都需要定期执行以下任务:每个Sentinel以每秒一次的频率,向它所知的主服务器、从服务器以及其他的Sentinel实例发送一个PING命令。
2、如果一个实例距离最后一次有效回复PING命令的时间超过down-after-milliseconds所指定的值,那么这个实例会被Sentinel标记为主观下线。
3、如果一个主服务器被标记为主观下线,那么正在监视这个服务器的所有Sentinel节点,要以每秒一次的频率确认主服务器的确进入了主观下线状态。
4、如果一个主服务器被标记为主观下线,并且有足够数量的Sentinel(至少要达到配置文件指定的数量)在指定的时间范围内同意这一判断,那么这个主服务器被标记为客观下线。
5、一般情况下,每个Sentinel会以每10秒一次的频率向它已知的所有主服务器和从服务器发送INFO命令,当一个主服务器被标记为客观下线时,Sentinel向下线主服务器的所有从服务器发送INFO命令的频率,会从10秒一次改为每秒一次。
6、Sentinel和其他Sentinel协商客观下线的主节点的状态,如果处于SDOWN状态,则投票自动选出新的主节点,将剩余从节点指向新的主节点进行数据复制。
7、当没有足够数量的Sentinel同意主服务器下线时,主服务器的客观下线状态就会被移除。当主服务器重新向Sentinel的PING命令返回有效回复时,主服务器的主观下线状态就会被移除。
缓存
缓存穿透
缓存穿透是指查询一条数据库和缓存都没有的一条数据,就会一直查询数据库,对数据库的访问压力就会增大,缓存穿透的解决方案,有以下两种:
- 缓存空对象:代码维护较简单,但是效果不好。
- 布隆过滤器:代码维护复杂,效果很好。
缓存空对象
缓存空对象是指当一个请求过来缓存中和数据库中都不存在该请求的数据,第一次请求就会跳过缓存进行数据库的访问,并且访问数据库后返回为空,此时也将该空对象进行缓存。缓存空对象的实现代码很简单,但是缓存空对象会带来比较大的问题,就是缓存中会存在很多空对象,占用内存的空间,浪费资源,一个解决的办法就是设置空对象的较短的过期时间
布隆过滤器
布隆过滤器是一种基于概率的数据结构,主要用来判断某个元素是否在集合内,它具有运行速度快(时间效率),占用内存小的优点(空间效率),但是有一定的误识别率和删除困难的问题。它只能告诉你某个元素一定不在集合内或可能在集合内。
在布隆过滤器中引用了一个误判率的概念,即它可能会把不属于这个集合的元素认为可能属于这个集合,但是不会把属于这个集合的认为不属于这个集合,布隆过滤器的特点如下:
- 一个非常大的二进制位数组 (数组里只有0和1)
- 若干个哈希函数
- 空间效率和查询效率高
- 不存在漏报(False Negative):某个元素在某个集合中,肯定能报出来。
- 可能存在误报(False Positive):某个元素不在某个集合中,可能也被爆出来。
- 不提供删除方法,代码维护困难。
- 位数组初始化都为0,它不存元素的具体值,当元素经过哈希函数哈希后的值(也就是数组下标)对应的数组位置值改为1。
具体布隆过布隆过滤的判断的准确率和一下两个因素有关:
- 布隆过滤器大小:越大,误判率就越小,所以说布隆过滤器一般长度都是非常大的。
- 哈希函数的个数:哈希函数的个数越多,那么误判率就越小
布隆过滤器不能删除元素的原因很简单,因为删除元素后,将对应元素的下标设置为零,可能别的元素的下标也引用改下标,这样别的元素的判断就会收到影响
缓存击穿
缓存击穿是指一个key
非常热点,在不停的扛着大并发,大并发集中对这一个点进行访问,当这个key在失效的瞬间,持续的大并发就穿破缓存,直接请求数据库,瞬间对数据库的访问压力增大。
缓存击穿这里强调的是并发,造成缓存击穿的原因有以下两个:
- 该数据没有人查询过 ,第一次就大并发的访问。(冷门数据)
- 添加到了缓存,reids有设置数据失效的时间 ,这条数据刚好失效,大并发访问(热点数据)
对于缓存击穿的解决方案就是加锁,具体实现的原理图如下
当用户出现大并发访问的时候,在查询缓存的时候和查询数据库的过程加锁,只能第一个进来的请求进行执行,当第一个请求把该数据放进缓存中,接下来的访问就会直接集中缓存,防止了缓存击穿。
缓存雪崩
缓存雪崩 是指在某一个时间段,缓存集中过期失效。此刻无数的请求直接绕开缓存,直接请求数据库。
造成缓存雪崩的原因,有以下两种:
- reids宕机
- 大部分数据失效
对于缓存雪崩的解决方案有以下两种:
- 搭建高可用的集群,防止单机的redis宕机。
- 设置不同的过期时间,防止同一时间内大量的key失效
分布式锁
想要实现分布式锁,必须要求 Redis 有「互斥」的能力,我们可以使用 SETNX 命令,这个命令表示SET if Not eXists,即如果 key 不存在,才会设置它的值,否则什么也不做。
两个客户端进程可以执行这个命令,达到互斥,就可以实现一个分布式锁。客户端 1 申请加锁,加锁成功,客户端 2 申请加锁,因为它后到达,加锁失败此时,加锁成功的客户端,就可以去操作「共享资源」,例如,修改 MySQL 的某一行数据,或者调用一个 API 请求。操作完成后,还要及时释放锁,给后来者让出操作共享资源的机会。
解决线程死锁的方式:
- 加锁:SET lock_key $unique_id EX $expire_time NX
- 操作共享资源
- 释放锁:Lua 脚本,先 GET 判断锁是否归属自己,再 DEL 释放锁
为了解决不好评估过期时间的策略,Redisson 是一个 Java 语言实现的 Redis SDK 客户端,在使用分布式锁时,它就采用了「自动续期」的方案来避免锁过期,这个守护线程我们一般也把它叫做「看门狗」线程。
SDK 还封装了很多易用的功能:
- 可重入锁
- 乐观锁
- 公平锁
- 读写锁
- Redlock
Redlock 的方案基于 2 个前提:
- 不再需要部署从库和哨兵实例,只部署主库
- 但主库要部署多个,官方推荐至少 5 个实例
也就是说,想用使用 Redlock,你至少要部署 5 个 Redis 实例,而且都是主库,它们之间没有任何关系,都是一个个孤立的实例。
Redlock整体的流程是这样的,一共分为 5 步:
- 客户端先获取「当前时间戳T1」
- 客户端依次向这 5 个 Redis 实例发起加锁请求(用前面讲到的 SET 命令),且每个请求会设置超时时间(毫秒级,要远小于锁的有效时间),如果某一个实例加锁失败(包括网络超时、锁被其它人持有等各种异常情况),就立即向下一个 Redis 实例申请加锁
- 如果客户端从 >=3 个(大多数)以上 Redis 实例加锁成功,则再次获取「当前时间戳T2」,如果 T2 - T1 < 锁的过期时间,此时,认为客户端加锁成功,否则认为加锁失败
- 加锁成功,去操作共享资源(例如修改 MySQL 某一行,或发起一个 API 请求)
:
- 可重入锁
- 乐观锁
- 公平锁
- 读写锁
- Redlock
Redlock 的方案基于 2 个前提:
- 不再需要部署从库和哨兵实例,只部署主库
- 但主库要部署多个,官方推荐至少 5 个实例
也就是说,想用使用 Redlock,你至少要部署 5 个 Redis 实例,而且都是主库,它们之间没有任何关系,都是一个个孤立的实例。
Redlock整体的流程是这样的,一共分为 5 步:
- 客户端先获取「当前时间戳T1」
- 客户端依次向这 5 个 Redis 实例发起加锁请求(用前面讲到的 SET 命令),且每个请求会设置超时时间(毫秒级,要远小于锁的有效时间),如果某一个实例加锁失败(包括网络超时、锁被其它人持有等各种异常情况),就立即向下一个 Redis 实例申请加锁
- 如果客户端从 >=3 个(大多数)以上 Redis 实例加锁成功,则再次获取「当前时间戳T2」,如果 T2 - T1 < 锁的过期时间,此时,认为客户端加锁成功,否则认为加锁失败
- 加锁成功,去操作共享资源(例如修改 MySQL 某一行,或发起一个 API 请求)
- 加锁失败,向「全部节点」发起释放锁请求(前面讲到的 Lua 脚本释放锁)