redis面试题
说一下redis
redis是用C语言开发的开源的键值对类型的nosql数据库,可以用于缓存、消息订阅/发布、队列等
记忆方法:是什么,可以用来做什么
redis有什么特点
键值对类型的nosql数据库
支持的数据类型多样
读写速度快
可是设置键的生存时间
支持持久化功能
提供主从复制(支持集群中多个服务器之间的数据同步)
redis的所有操作是原子性的(之所以是原子性的,是因为Redis是单线程的。)
redis处理网络请求是单线程的
redis会周期性的把更新的数据写入磁盘或者把修改操作写入追加的记录文件,并在此基础上实现了主从同步
Redis配置文件:redis.conf
redis默认端口:6379
记忆方法:用法上的特点,原理上的特点
redis的缺点
不支持对内容做条件查询、关联查询
读写速度快的原因:
直接操作内存:内存读写速度非常快
采用单线程,避免了不必要的上下文切换和线程竞争,不用去考虑加锁释放锁的问题;
多路复用IO:多路”指的是多个网络连接,“复用”指的是复用一个或少量线程。是一种非阻塞IO,采用多路 I/O 复用技术可以让单个线程高效的处理多个连接请求(尽量减少网络IO的时间消耗)。
多路复用 I/O 模型
它允许单个线程同时处理多个IO任务,这种机制的核心在于利用操作系统提供的系统调用,如select、poll、epoll等,来监听多个IO事件的状态变化。通过让线程一次性的监听多个socket,在数据到达时通过系统调用得到通知,进行下一步处理。
I/O就是指的我们网络I/O,多路指多个TCP连接(或多个Channel),复用指复用一个或少量线程。
使用多路复用 I/O 可以用少量的线程处理大量的链接。
多路复用 I/O 就是通过一种机制,选择器进行监视多个连接,如果数据到达就通知线程进行处理。
select, poll, epoll 都是I/O多路复用的具体的实现,在现在的Linux内核里有都能够支持
选择器:
负责监听多个通道的事件(比如:连接打开,数据到达)
单线程好处
代码更清晰,处理逻辑更简单
不用去考虑各种锁的问题,不存在加锁释放锁操作,没有因为可能出现死锁而导致的性能消耗
不存在多进程或者多线程导致的切换而消耗CPU
多路复用io用在什么地方
使用多路复用I/O来监听和接受来自客户端的连接请求。当有新的连接请求到达时,服务器会将其加入到监听队列中,并在适当的时候进行处理。
服务器使用多路复用I/O来同时处理多个客户端的请求,而无需为每个请求创建单独的线程或进程。这样,Redis可以高效地处理大量的并发请求。
Redis内部实现了一个事件驱动框架,用于处理各种事件,如客户端连接、断开连接、请求读取和写入等。这个框架利用多路复用I/O来监听相关的事件,并在事件发生时触发相应的处理逻辑。
通过多路复用I/O,Redis能够在单个进程或线程中处理多个客户端连接和请求,从而避免了为每个连接创建单独进程或线程的开销。这不仅提高了Redis的并发处理能力,还降低了上下文切换的成本,使得Redis能够更高效地处理大量的客户端请求。
在Redis使用多路复用I/O的过程中,主要涉及到的是网络I/O操作,而不是磁盘I/O操作
Redis单线程
单线程指的是网络请求模块使用了一个线程(所以不需考虑并发安全性),即一个线程处理所有网络请求,其他模块仍用了多个线程。
当一个客户端请求到达Redis服务器时,Redis会将这个请求放入一个队列中。然后,这个单线程会按照顺序从队列中取出请求进行处理。
选用单线程的原因:
因为Redis是基于内存的操作,CPU不是Redis的瓶颈,Redis的瓶颈最有可能是机器内存的大小或者网络带宽。所以就选择了单线程
单线程处理请求的原理:
对于并发访问,redis采用多路复用I/O 技术可以让单个线程高效的处理多个连接请求,
I/O多路复用不需要切换线/进程效率很高,I/O多路复用不会因为一个连接的I/O阻塞造成线程阻塞,线程还可以处理其他I/O
单线程和多线程对比:
单线程优点:
避免了多线程的线程切换带来的资源消耗和时间消耗
避免了多线程状态下为了线程安全对锁的处理
多线程优点:
可以充分利用多核cpu的优势
但是如果CPU成为Redis瓶颈,或者不想让服务器其他CUP核闲置,那怎么办?
可以考虑多起几个Redis进程,Redis是key-value数据库,不是关系数据库,数据之间没有约束。只要客户端分清哪些key放在哪个Redis进程上就可以了。
redis全局哈希表
对于每个redis的数据库,都会有一个全局哈希表存储缓存key与缓存value的对应关系,内部实现也是维护了一个数组。对于hash冲突的问题也是用链表的方式进行处理的。当键值数量达到一定的阀值会新建一个更大的数组并移动数据。
redis写数据过程
redis根据提供的键进行计算哈希值在内存中的哈希表中找到对应的位置,之后把数据写入到具体的数据结构中,写完之后就会返回操作结果。持久化的过程是异步进行的(默认是快照方式)。
如果写操作已经有值了,会覆盖原值,如果原来设置有过期时间,会把原过期时间清除。写入成功会返回OK
如果是操作日志(aof)的持久化方式:
每收到一个写的命令,就会将这个命令追加到 AOF 缓冲区中,之后按照配置的磁盘同步策略(每个写命令都同步、每秒同步一次、由操作系统决定何时同步)写到磁盘上的aof文件中;当aof文件大到一定的阈值的时候,会对aof进行重写(创建一个新的 AOF 文件,其中只包含恢复当前数据集所需的最小命令集合。重写完成后,Redis 会用新的 AOF 文件替换旧的 AOF 文件。)
如果是镜像文件的持久化方式:
每收到一个写的命令,会首先在内存中执行相应的写操作,更新数据集,根据配置的持久化条件(在n秒内如果超过m个key)触发持久化,会将内存中的数据序列化成一个镜像文件,之后替换老的镜像文件。
redis读数据过程
当接收到读请求时,它会根据key在哈希表中查找对应的value。
redis根据提供的键进行计算哈希值在内存中的哈希表中找到对应的值,因为redis支持多种数据类型,可能还要在这个值中继续找到用户要查询的内容,之后返回给用户。
Redis在读取数据的过程中,通常不会直接涉及到读取镜像文件(RDB文件)或AOF文件。这两个文件主要用于数据的持久化,即在Redis服务器重启后能够恢复数据。但在正常的读取操作中,Redis主要依赖内存中的数据。
Redis线程安全
Redis本身是单线程线程安全的内存数据库,redis的单个操作是线程安全的
redis支持的数据类型
string(字符串)、list(列表)、hash(哈希)、set(集合)、SortedSet(有序集合)、bitmap、HyperLogLog 等
String
一个key对应一个value,string可以包含任何数据,如jpg图片或者序列化的对象
hash
一个key对应一个string类型的field和value的映射表
list
一个key对应一个字符串列表,按照插入顺序排序
set
一个key对应一个string类型的无序集合,不能够有重复的元素
zset(sorted set)
一个key对应一个string类型的有序集合,不允许重复的成员,每个元素都会关联一个double类型的分数,通过分数排序(默认正序),分数可以重复
Bitmap(位图)
一个key对应一个Bitmap,Bitmap是一串连续的2进制数字,通过一个bit位来表示某个元素对应的值或者状态,可以给指定偏移量上设置0或者1,可以获取到总共1或0的数量等,可以用极小的空间来进行统计,如统计用户签到,月活人数等
offset 参数必须大于或等于 0 ,小于 2^32 (bit 映射被限制在 512 MB 之内)。因为 Redis 字符串的大小被限制在 512 兆内,redis以一个字符串大小来存储这些值
HyperLogLog
HyperLogLog用来做基数统计的,HyperLogLog 的优点是,在输入元素的数量或者体积非常非常大时,计算基数所需的空间总是固定 的、并且是很小的。它和Set类似,维护一个不可重复的String集合,但是HyperLogLogs并不维护具体的member内容,只维护member的个数。也就是说,HyperLogLogs只能用于计算一个集合中不重复的元素数量,所以它比Set要节省很多内存空间。
bitmap如何实现的:
为什么使用这么多数据类型
我们可以针对不同的场景,为对象设置多种不同的数据结构,从而优化对象在不同场景下的使用效率。
丰富了redis的应用场景,优化了不同场景下的效率
各种数据类型的应用场景
string:
数据缓存,session共享,分布式锁,
利用incr做计数器,做全局id
list
做队列
hash:
可以存储有关联的数据比如一个对象的属性(可以直接取到属性的值,删除key就全部删除)
配合发布订阅作为注册中心
redisson使用hash结构存储分布式锁信息(key是我们指定的字符串也就是锁的名字,field为线程相关的字符串,value是递增的数字,每次锁重入加1,锁重入解锁减一,通过设置key的有效期自动解锁)
set
可以存储标签的名字,不能重复
zset
做排行榜
Bitmap
统计签到,统计月活,布隆过滤器
HyperLogLog
基数统计
redis存储原理
redis常用的5种数据类型都是通过redisObject 来存储的
typedef struct redisObject {
unsigned type:4; /* 对象的类型 */
unsigned encoding:4; /* 具体的数据结构*/
unsigned lru:LRU_BITS; /* 24 位,对象最后一次被命令程序访问的时间,与内存回收有关*/
int refcount; /* 引用计数。当refcount 为0 的时候,表示该对象已经不被任何对象引用,则可以进行垃圾回收了*/
void *ptr; /* 数据指针:指向对象实际的数据结构*/
} robj;
type属性定义了存储对象的数据类型:
包括:OBJ_STRING、OBJ_LIST、OBJ_HASH、OBJ_SET、OBJ_ZSET
encoding定义了存储对象的数据结构(编码方式):
OBJ_ENCODING_RAW
OBJ_ENCODING_INT
OBJ_ENCODING_HT
OBJ_ENCODING_ZIPMAP
OBJ_ENCODING_LINKEDLIST
OBJ_ENCODING_ZIPLIST
OBJ_ENCODING_INTSET
OBJ_ENCODING_SKIPLIST
OBJ_ENCODING_EMBSTR
OBJ_ENCODING_QUICKLIST
int就代表的整数,长度是长整形的长度
raw就代表的简单动态字符串
ht就代表的哈希结构(或叫字典表)
ziplist就代表的压缩列表
linkedlist就代表的链表
intset代表的整数集合
skiplist代表的跳表
string数据类型使用的编码:embstr ,raw,int
hash数据类型使用的编码:ht,ziplist
list数据类型使用的编码:linkedlist,ziplist
set数据类型使用的编码:ht,intset
zset数据类型使用的编码:ziplist,skiplist
各数据类型的数据结构
String
对应的实现方式:整型,动态字符串
字符串的长度不得超过 512M (应该是出于效率和性能的考虑)
一定数值范围内的整数使用整型结构,字符串和浮点数使用动态字符串结构
动态字符串:是Redis为了高效地处理字符串而自定义的一种数据结构,内部使用了数组来存储字符,还包含了其他元数据和管理机制
String在数据结构中被称为串,java中使用数组来实现存储,数组是顺序存储结构,元素逻辑上相邻,存储位置也相邻,存储的空间连续;
redis中String类型有3中编码方式
int:如果设置的值为整数值,而且值大小小于2^63-1就会存储为int编码
embstr:如果是一个字符串值,并且长度小于等于 44 字节,那么将使用 embstr 编码的简单动态字符串方式来保存这个字符串。
raw:如果是一个字符串值,并且长度大于 44字节,那么将使用SDS来保存这个字符串值,并将对象的编码设置为 raw。
如果整数的值超过2^63-1或者不在为整数就会转变为字符串
在对embstr对象进行修改时,都会先转化为raw再进行修改,无论是否达到了44个字节。
对于浮点数的存储也是使用的embstr或raw编码格式
embstr与raw都使用redisObject和sds保存数据,区别在于,embstr的使用只分配一次内存空间(因此redisObject和sds是连续的),而raw需要分配两次内存空间(分别为redisObject和sds分配空间)。因此与raw相比,embstr的好处在于创建时少分配一次空间,删除时少释放一次空间,以及对象的所有数据连在一起,寻找方便。而embstr的坏处也很明显,如果字符串的长度增加需要重新分配内存时,整个redisObject和sds都需要重新分配空间,因此redis中的embstr实现为只读。
SDS(simple dynamic string)简单动态字符串的结构实现
struct sdshdr {
int len;// buf 中已占用空间的长度
int free;// buf 中剩余可用空间的长度
char buf[]; // 数据空间
};
数据保存在数组中
为什么这样设计:
为了更有效率的存取数据,及存储空间大小的考虑
buf的大小等于len+free+1,其中多余的1个字节是用来存储’\0’的。
通过空间预分配和空间惰性释放 减少内存分配问题
当给sds的值追加一个字符串,而当前的剩余空间不够时,就会触发sds的扩容机制。扩容采用了空间预分配的优化策略,即分配空间的时候:如果sds 值大小< 1M ,则增加一倍; 反之如果>1M , 则当前空间加1M作为新的空间。
当sds的字符串缩短了,sds的buf内会多出来一些空间,这个空间并不会马上被回收,而是暂时留着以防再用的时候进行多余的内存分配。这个是惰性空间释放的策略,SDS也提供直接释放未使用空间的API,在需要的时候,也能真正的释放掉多余的空间。
List
对应两种实现方法,一种是压缩列表(ziplist),另一种是双向循环链表。3.2版本以后都采用quicklist(快速列表)
Redis倾向于使用压缩列表进行存储,因为压缩列表占用内存更少,而且比双向链表可以更快载入;当列表对象元素较多时,压缩列表就会转化为更适合存储大量元素的双向链表。
列表中存储的数据量比较小的时候,列表就可以采用压缩列表的方式实现。具体需要同时满足以下两个条件:
- 列表中保存的单个数据小于64字节;
- 列表中数据个数少于512个
压缩列表:不是基础数据结构,而是Redis自己设计的一种数据存储结构。它有点类似数组,通过一片连续的内存空间,来存储数据。不过,它允许存储的数据大小不同。每个元素都保存了元素的数据类型前个位置的字节长度、编码(编码部分记录了节点内容所保存数据的类型和长度)、实际值
压缩列表的特点:
内存连续,没有内存碎片,添加删除效率低因为需要移动数据
双向链表特点:
内存不连续,会产生内存碎片,内存开销大(因为还要存储前驱后驱加上内存碎片),但便于数据的添加和删除,从头和尾都可以遍历(查询效率更高)
quicklist:
快速列表的数据结构为 双向链表+zipList压缩列表,能够在内存使用和访问速度之间达到一个平衡。
quicklist是由ziplist组成的双向链表,链表中的每一个节点都以压缩列表ziplist的结构保存着数据,这样就就降低了双向链表的长度,提高内存利用率。链表允许在列表两端进行快速插入和删除操作,而压缩列表则提供了紧凑的存储和快速的元素访问。
默认单个 ziplist 长度为 8k 字节,超出了设置的阀值,就会新起一个ziplist。Redis 会根据一定的策略来决定何时创建新的压缩列表节点,这个策略通常基于当前节点的长度和配置的阈值。
为什么使用快速列表代替压缩列表和双向链表实现List
考虑到双向链表的内存占用太大,就改造成为快速列表
为什么要用双向链表:
因为redis的list提供了头插入,尾插入,头读取,尾读取,头删除,尾删除等功能,双向链表执行效率更高。
压缩列表怎么计算内存位置:
压缩列表每个元素都带有前一个节点的长度信息,我们根据偏移量查询元素时,需要拿到头节点位置之后遍历元素相加长度,到达指定偏移量后就计算好了内存地址了。
为什么要用压缩列表不用数组:
压缩列表更节省空间,因为redis数据是存在内存中使用的,每个元素根据大小分配空间大小更省空间。
Redis的List实际是设计来用于实现队列
hash
字典类型也有两种实现方式。一种是压缩列表zipList,另一种是散列表ht。
同样,只有当存储数据量比较小的情况下,Redis 才使用压缩列表来实现字典类型。具体需要满足两个条件:
- 字典中保存的键值的大小都要小于64字节
- 字典中的键值对的个数要小于512个
当不能同时满足上面两个条件时,Redis 就使用散列表来实现。
如何在压缩列表中存储:
压缩列表数据结构类似数组,采用key|value|key|value这样的方式即一个键值对占用两个位置来采用顺序存储,新增的键值对是保存到压缩列表的表尾
这样方式适用用数据较小的情况,优点是节省内存,没有内存碎片。
hash结构:
这里的hash结构的实现时数组+链表实现,冲突之后放入链表,容量不够也会进行扩容,扩容的条件:是使用的大小达到总hash表长度指定比例就会触发
扩容要做大量数据搬移和哈希值的重新计算,所以比较耗时。针对这个问题,Redis使用渐进式扩容策略,将数据的搬移分批进行,避免大量数据一次性搬移的服务停顿。
set
实现方法有两种,一种是基于整数集合intset,另一种是基于散列表ht。
当要存储的数据,同时满足下面两个条件时候,Redis 就采用整数集合。
- 存储数据都是整数;
- 存储的数据元素个数不超过512个。
当不能同时满足这两个条件的时候,Redis 就使用散列表来存储集合中的数据。
整数集合的特点:
节省内存空间,数据量大时效率不高
typedef struct intset {
uint32_t encoding; // 编码类型 int16_t、int32_t、int64_t
uint32_t length; // 长度 最大长度:2^32
int8_t contents[]; // 整型数组
} intset;
可以看出内部使用的数组进行保存数据
使用hash结构实现时:
要存储的值作为key,value存的null
sortedset
数据量较小的时候,Redis 会使用压缩列表来实现有序集合。具体点说,使用压缩列表实现的有序集合的前提有两个:
- 所有数据的大小都要小于64字节;
- 元素个数要小于128个。
压缩列表如何存储数据:
每个集合元素使用两个紧挨在一起的压缩列表节点来保存,第一个节点保存元素的成员,第二个节点保存元素的分值。并且压缩列表内的集合元素按分值从小到大的顺序进行排列
不满足的时候使用zset来实现,zset是由skiplist跳表+hash表来共同实现
哈希表用来根据数据查分数(score)的对应关系,而skiplist用来根据分数查询数据(可能是范围查找)。
字典的键保存元素的值,字典的值则保存元素的分值;跳跃表节点的 object 属性保存元素的成员,跳跃表节点的 score 属性保存元素的分值。
这两种数据结构会通过指针来共享相同元素的成员和分值,所以不会产生重复成员和分值,造成内存的浪费。
说明:其实有序集合单独使用字典或跳跃表其中一种数据结构都可以实现,但是这里使用两种数据结构组合起来,原因是假如我们单独使用 字典,虽然能以 O(1) 的时间复杂度查找成员的分值,但是因为字典是以无序的方式来保存集合元素,所以每次进行范围操作的时候都要进行排序;假如我们单独使用跳跃表来实现,虽然能执行范围操作,但是查找操作有 O(1)的复杂度变为了O(logN)。因此Redis使用了两种数据结构来共同实现有序集合。
跳表说明:
跳表是在有序的链表基础上每个节点增加一个或多个指向后续节点的指针,链表变为多层链表结构,插入元素时会随机一个层数然后插入到这个层数中,查询时从最高层逐步寻找到元素避免了遍历的复杂度
跳表是一种多层的有序链表,通过在不同层级上维护有序链表的结构,能够在O(log n)的时间复杂度内完成查找、插入和删除操作。
当执行zset的相关操作时,Redis会利用跳表的有序性进行高效处理。例如,查找某个元素时,Redis可以从跳表的顶层开始,依次向下层遍历,通过比较节点的score值来快速定位到目标元素。这种分层的结构使得每次比较都能排除掉一部分元素,从而大大减少了比较次数。
为什么要随机层数:
如果按照每一层链表的节点个数,是下面一层的节点个数的一半,查找的时间复杂度可以降低到O(log n),问题:新插入一个节点之后,就会打乱上下相邻两层链表上节点个数严格的2:1的对应关系。如果要维持这种对应关系,就必须把新插入的节点后面的所有节点(也包括新插入的节点)重新进行调整,这会让时间复杂度重新蜕化成O(n)。删除数据也有同样的问题。
随机的话插入操作只需要修改插入节点前后的指针,而不需要对很多节点都进行调整。这就降低了插入操作的复杂度。
多层链表节点的约束:
每个节点肯定都有第1层指针(每个节点都在第1层链表里)。
节点最大的层数不允许超过一个最大值,redis里面取值是32

跳表的平均复杂度是O(log n)。这种效率的提升是有代价的,需要建立很多索引。这是典型的空间换时间的设计思路。
为什么不用红黑树:
sortedset的核心操作是插入、删除、查找、迭代输出和区间查找
插入、删除、查找以及迭代输出有序序列这见个操作,红黑树可以完成,时间复杂度和跳表是一样的。但是,按照区间来查找数据这个操作,红黑树的效率没有跳表高。而且跳表实现更简单灵活等。
bitmap
BitMap 底层是基于 Redis 的字符串类型实现,其长度可以根据需要动态扩展。但最大长度受限于String结果的长度(512M,由于字符串末尾需要存储一位分隔符,实际上BitMap的偏移量offset值上限是2^32-1)
刚创建的bitMap所有位为 0。我们写数据时要指定偏移量(从0开始)和要设置的值如:SETBIT key 10 1;其中10为偏移量,1为值。
对String类型的value进行基于二进制位的置位操作。
HyperLogLog
redis数据类型的时间复杂度
基本上单个key的操作的时间复杂度为O(1)
多个key的操作为O(n)
redis布隆过滤器
布隆算法可以是用来判断某个元素(key)是否在某个集合中。如果存的1可能存在于集合中(但有一定的误判率);如果存的0,则可以确定元素一定不存在于集合中。
布隆算法是一个以牺牲一定的准确率来换取低内存消耗的过滤算法,可以实现大量数据的过滤、去重等操作。
和一般的hash set不同的是,这个算法无需存储key的值,就能判断key是否在集合中。
布隆过滤器应用场景:
用于缓存穿透防护、垃圾邮件过滤、URL去重等。
优点:
无须存key,节省内存
缺点:
- 有一定的概率误判:算法判断key在集合中时,key其实不在集合中(多报)
- 无法删除
- 需要我们自己计算偏移量
值为1时误判的原因:
由于哈希函数的设计特性,不同的输入可能会产生相同的哈希值,即哈希碰撞。产生hash碰撞后会计算到同一个位置,而且位置上不存储元素的值,如果为1就会无法判断是否真的存储过这个元素。
原理:
初始化时,需要一个长度为n的位数组(bit数组)(以个比特位占用一个index),每个比特位初始化为0
某个key加入集合时,用k个hash函数计算出k个散列值,并把数组中对应的比特位置设置为1
判断某个key是否在集合时,用k个hash函数计算出k个散列值,并查询数组中对应的比特位,如果所有的比特位都是1,认为在集合中。
redis的布隆过滤器模块
Redis 提供的 bitMap 可以实现布隆过滤器,但是需要自己设计映射函数和一些细节,这和我们自定义没啥区别。
Redis布隆过滤器模块(如RedisBloom(使用bitmap实现的))是一个扩展模块,它为Redis提供了原生的布隆过滤器支持。
用户可以直接在Redis中创建、查询和管理布隆过滤器,而无需手动实现布隆过滤器的算法和逻辑。
也会存在误判(即错误地将不存在的元素判断为存在)
由于这些模块是扩展模块,可能需要单独安装和配置,以确保与Redis的兼容性和稳定性。
可以设置误判率,值越低,数组长度就越大,占用空间就越大
redis处理缓存穿透问题的方式:
我们后台程序可以把要使用布隆过滤器的key放入到布隆过滤器中,之后使用这样的逻辑去使用
String get(String key) {
String value = redis.get(key);
if (value == null) {
if(!bloomfilter.mightContain(key)){
return null;
}else{
value = db.get(key);
redis.set(key, value);
}
}
return value;
}
redisson有提供API,也可以引入jrebloom依赖来使用
缓存穿透、缓存雪崩、缓存击穿
缓存穿透:
请求不存在的数据,造成大量请求访问数据库
解决方案:
- 当没有数据时,也将查询结果放入到缓存中,缺点:会多存储很多没有数据的结果,我们可以把它的有效期设置的短一点
- 使用redis提供的布隆过滤器
缓存击穿:
指热点key在某个时间点过期的时候,而恰好在这个时间点对这个Key有大量的并发请求过来,从而大量的请求打到db。
解决方案:
多级缓存比如首先访问ehcache,没有再访问redis
使用互斥锁,当缓存没有命中时,先加锁,之后db取数据放入缓存,再解锁,其他线程过来有锁的情况下先sleep一会,再获取
布隆过滤器
缓存雪崩:
是指缓存大量失效,导致大量的请求都直接向数据库获取数据,造成数据库的压力。缓存大量失效的原因可能是缓存服务器宏机,或者大量Redis的键设置的过期时间相同。
解决缓存雪崩我们也有两种解决方案:
- 在设置Redis键的过期时间时,加上一个随机数,这样可以避免。
- 部署分布式的Redis服务,当一个Redis服务器挂掉了之后,进行故障转移。
redis的事务
一个事务中的所有命令都会序列化,按顺序地串行化执行,并且在事务执行期间,服务器不会中断事务而去执行其他客户端的命令请求,它会将事务中的所有命令都执行完毕才去处理其他客户端的命令请求。
redis对事务的支持目前还比较简单。
redis事务特点:
redis同一个事务中如果有一条命令执行失败,其后的命令仍然会被执行,不回滚
redis事务相关的命令:
MULTI:标记事务块的开始
EXEC:执行事务块中的命令
DISCARD:清除事务队列,然后退出事务,如果客户端在执行事务的时候watch了一些键,则discard会取消所有键的watch
WATCH:是一个乐观锁,它可以在EXEC命令执行之前,监视任意数量的数据库键,并在执行EXEC命令时判断是否至少有一个被watch的键值被修改如果被修改就放弃事务的执行,如果没有被修改就清空watch的信息,执行事务列表里的命令。
unwatch: 顾名思义可以看出它的功能是与watch相反的,是取消对一个键值的“监听”的功能
jedis中
multi()
exec()
watch(…)
discard()
事务中某个操作失败,并不会回滚其他操作
redis事务为什么不支持回滚
Redis 命令只会因为错误的语法而失败(并且这些问题不能在入队时发现),或是命令用在了错误类型的键上面:这也就是说,从实用性的角度来说,失败的命令是由编程错误造成的,而这些错误应该在开发的过程中被发现,而不应该出现在生产环境中。
因为不需要对回滚进行支持,所以 Redis 的内部可以保持简单且快速。
redis消息队列
方式一:
使用list数据类型实现消息队列
使用lpush插入数据,使用rpop弹出消费;或者使用rpush插入数据,使用lpop弹出数据消费;或者使用blpop和brpop是阻塞读取。
特点是:只能有一个消费端消费数据,消息通过拉的方式消费
方式二:
使用redis的发布订阅,可以有多个消费端同时消费数据,消息通过推的方式消费
Redis发布订阅
发布订阅(pub/sub)是一种消息通知模式,主要的目的是解除消息发布者和消息订阅者之间的耦合
典型的广播模式,一个消息可以发布到多个消费者多信道订阅,消费者可以同时订阅多个信道,从而接收多类消息
功能说明:Redis 的 SUBSCRIBE 命令可以让客户端订阅任意数量的频道, 每当有新信息发送到被订阅的频道时, 信息就会被发送给所有订阅指定频道的客户端。当有新消息通过 PUBLISH 命令发送给频道这个消息就会被发送给订阅它的客户端,使用 UNSUBSCRIBE 命令可以退订指定的频道
redis发布订阅原理
当客户端调用 SUBSCRIBE channel1 channel2 channel3命令时,redis会把通过一个哈希表(由数组+链表实现)来关联订阅的频道和客户端,哈希表的键就是订阅的频道,链表的内容就是订阅该频道的客户端
当调用PUBLISH channel message 命令, 程序首先根据 channel 定位到哈希表的键, 然后将信息发送给字典值链表中的所有客户端。
redis过期机制
定期删除+惰性删除
定期删除:指的是redis默认是每隔100ms就随机抽取一些设置了过期时间的key,检查其是否过期,如果过期就删除。如果过期的 key 比率超过 1/4,还会再抽取。
惰性删除:在你获取某个key的时候,redis会检查一下 ,这个key如果设置了过期时间那么是否过期了?如果过期了此时就会删除,不会给你返回任何东西。
为什么采取这种策略:
定期删除可以避免每次查询的大概率删除操作消耗性能及不再使用的设置了过期时间的可以被删除,惰性删除可以确保获取到的值是没有过期的。
一个可以保证性能,一个保证准确性
redis和lua脚本
Redis中内嵌了对Lua环境的支持,允许开发者使用Lua语言编写脚本传到Redis中执行,Redis客户端可以使用Lua脚本,直接在服务端原子的执行多个Redis命令。
Lua是一个轻量级脚本语言,在Lua脚本中调用Redis命令,可以使用redis.call函数调用。比如我们调用string类型的命令
redis.call(‘set’,’hello’,’world’)
redis.call 函数的返回值就是redis命令的执行结果。
redis虚拟内存
redis属于内存数据库,内存总是不够用,redis会暂时把不经常访问的数据从内存交换到磁盘中,从而腾出宝贵的内存空间用于其他需要访问的数据。为了保证key的查找速度只会把value交换到磁盘中
在redis使用的内存没超过设置的vm-max-memory之前是不会交换任何value的。当超过最大内存限制后,redis会选择较老的对象。如果两个对象一样老会优先交换比较大的对象
可以设置vm-max-threads(用于执行value对象换入换出的工作线程数量)设为0时
主线程定期检查发现内存超出最大上限后,会直接以阻塞的方式,将选中的对象交换到磁盘,并释放其内存,获取交换过的key的值时,会阻塞所有client获取到值后返回,并恢复响应其他请求
vm-max-threads大于0时
当主线程检测到使用内存超过最大上限,会将选中的要交换的对象信息放到一个队列中交由工作线程后台处理,主线程会继续处理client请求。
如果有client请求的key被换出了,主线程先阻塞发出命令的client,然后将加载对象的信息放到一个队列中,让工作线程去加载。加载完毕后工作线程通知主线程。主线程再执行client的命令。这种方式只阻塞请求value被换出key的client
总的来说blocking vm的方式总的性能会好一些,因为不需要线程同步,创建线程和恢复被阻塞的client等开销。但是也相应的牺牲了响应性。threaded vm的方式主线程不会阻塞在磁盘io上,所以响应性更好。如果我们的应用不太经常发生换入换出,而且也不太在意有点延迟的话则推荐使用blocking vm的方式。
redis淘汰策略
通过配置文件的maxmemory-policy配合maxmemory来设置淘汰策略
最大内存设置32位系统最大为3G
Redis提供的数据淘汰策略:
noeviction: 不删除策略, 达到最大内存限制时, 如果需要更多内存, 直接返回错误信息。(默认的策略)
volatile-lru:使用LRU算法进行有效期数据淘汰(淘汰上次使用时间最早的,且使用次数最少的key),只淘汰设定了有效期的key
volatile-random:随机淘汰有效期数据,只淘汰设定了有效期的key
allkeys-lru:使用LRU算法进行所有数据淘汰,所有的key都可以被淘汰
allkeys-random:随机淘汰所有数据,所有的key都可以被淘汰
volatile-ttl:淘汰剩余有效期最短的key
最好为Redis指定一种有效的数据淘汰策略以配合maxmemory设置,避免在内存使用满后发生写入失败的情况。
lru淘汰策略:
LRU 是 Least Recently Used 缩写,即最近最少使用,选择最近最久未使用的数据进行淘汰。出于对内存占用和性能的考虑,Redis 没有实现严格的 LRU 缓存淘汰算法,而是提供了一种近似实现。Redis 会在 RedisObject 用 24 Bit 记录 LRU 时间戳,当内存使用量超出限制时,Redis 将执行缓存淘汰。根据配置的淘汰策略,在全局哈希表或设置了过期时间的 Key 的哈希表里随机采样一批键值对,然后根据空闲时间排序,最后把空闲时间最久的 Key 给淘汰掉。
一般来说,推荐使用的策略是volatile-lru,并辨识Redis中保存的数据的重要性。对于那些重要的,绝对不能丢弃的数据(如配置类数据等),应不设置有效期,这样Redis就永远不会淘汰这些数据。对于那些相对不是那么重要的,并且能够热加载的数据(比如缓存最近登录的用户信息,当在Redis中找不到时,程序会去DB中读取),可以设置上有效期,这样在内存不够时Redis就会淘汰这部分数据。
如果采用不删除策略就要做好redis监控,避免出现问题
LRU 时间戳是通过 LRU 时钟获取的,方法是getLRUClock() 。Redis 会以 100ms/次 的频率记录 Unix 时间戳,然后除以 LRU 时钟单位 1000,即以秒为单位,保留低 24 位时间戳。获取系统时间要发起一次系统调用,有一定的开销。而读取 LRU 时间戳又是非常频繁的操作,出于对性能的考虑,Redis 选择周期性的读取一次系统时间。
淘汰数据对镜像文件和操作日志文件的影响:
镜像文件:已经淘汰的key,生成镜像文件的时候就不会被包含到镜像文件中了
操作日志:删除操作也会被记录到AOF的缓存区,并最终写入到AOF文件中
虚拟内存和淘汰策略:
两种处理内存不够的策略,生产环境一般不开启虚拟内存,而是用淘汰策略
内存的数据会开始和磁盘产生频繁的交换(swap)。交换会让redis的性能急剧下降,对于访问量较大的redis来说,这样的龟速存取效率基本上等于不可用。
使用redis需要注意什么
使用高耗时的Redis命令是很危险的,会占用唯一的一个线程的大量处理时间,导致所有的请求都被拖慢。(例如时间复杂度为O(N)的KEYS命令,严格禁止在生产环境中使用)
redis的客户端
Redisson,Jedis,lettuce等等
jedis对比redisson
jedis轻量简介,其API提供了比较全面的Redis命令的支持,不支持读写分离,需要自己实现,Jedis使用阻塞的I/O,且其方法调用都是同步的,程序流需要等到sockets处理完I/O才能执行,不支持异步。Jedis客户端实例不是线程安全的,所以需要通过连接池来使用Jedis。
redisson提供很多分布式相关操作服务,例如,分布式锁,分布式集合,为我们提供了一系列具有分布式特性的工具类。Redisson使用非阻塞的I/O和基于Netty框架的事件驱动的通信层,其方法调用是异步的。Redisson的API是线程安全的,所以可以操作单个Redisson连接来完成各种操作。
如:
RList 是 Java 的 List 集合的分布式并发实现。
RMap,它是 Java 的Map 集合的分布式并发实现
Lettuce:高级Redis客户端,用于线程安全同步,异步和响应使用,支持集群,Sentinel,管道和编码器。目前springboot默认使用的客户端。
如何选择:
对于基本的redis的命令操作选择jedis
对于需要分布式工具类使用redisson,redisson还提供了本地缓存功能(RLocalCachedMap)可以把数据缓存到jvm
redis如何作为注册中心
利用redis的hash数据结构,配合redis的发布订阅
hash结构存储服务提供方和服务消费方的地址
当服务提供方变更就会发布消息,消费方启动时会订阅消息
这样就能够及时通知消费方注册的提供方地址了
Redis的部署方案
单机模式:
即所有服务连接一台redis服务,该模式不适用生产。如果发生宕机,内存爆炸,就可能导致所有连接改redis的服务发生缓存失效引起雪崩。
缺点:
1、内存容量有限
2、处理能力有限
3、无法高可用
主从复制:
主数据库可以进行读写操作,当读写操作导致数据变化时会自动将数据同步给从数据库
从数据库一般都是只读的,并且接收主数据库同步过来的数据
一个master可以拥有多个slave,但是一个slave只能对应一个master
从服务器也可以有自己的从服务器
复制功能不会阻塞主服务器: 即使有一个或多个从服务器正在进行初次同步, 主服务器也可以继续处理命令请求。
从redis 2.8开始,就支持主从复制的断点续传,如果主从复制过程中,网络连接断掉了,那么可以接着上次复制的地方,继续复制下去,而不是从头开始复制一份
优点:
1、解决数据备份问题
2、做到读写分离,提高服务器性能
缺点:
1、无法高可用,进行故障转移
2、不能解决master写的压力
3、无法实现动态扩容
主从复制的方式:
主从刚刚连接的时候,进行全量同步;全同步结束后,进行增量同步。当然,如果有需要,slave 在任何时候都可以发起全量同步。redis 策略是无论如何,首先会尝试进行增量同步,如不成功,要求从机进行全量同步。
全量通过是主会把一个数据快照发给从进行同步;增量是具体的写数据命令
哨兵模式:
使用一个或者多个哨兵(Sentinel)实例组成的系统,对redis节点进行监控
Redis官方推荐的高可用性解决方案,哨兵能监控多个master-slave集群,发现master宕机后能进行切换从服务器为主服务器
哨兵模式的功能:
监控主从:监控主从数据库是否正常运行
故障转移:master出现故障时,自动将slave转化为master,(根据配置的优先级及复制偏移量来选择slave为master)
监控哨兵:多哨兵配置的时候,哨兵之间也会自动监控
配置中心:客户端通过哨兵获取redis地址,并在故障转移时更新地址
注:
多个哨兵可以监控同一个redis
故障转移的概念:即当活动的服务或应用意外终止时,快速启用冗余或备用的服务器、系统、硬件或者网络接替它们工作。
优点:
1、能够自动进行故障转移
2、能够监控各个节点
缺点:
1、主从模式,切换需要时间丢数据
2、没有解决master写的压力
3、无法实现动态扩容
集群模式
Redis3.0推出cluster分布式集群方案,cluster方案主要解决分片问题,即把整个数据按照规则分成多个子集存储在多个不同节点上,每个节点负责自己整个数据的一部分。
一般集群建议搭建三主三从架构,三主提供服务,三从提供备份功能。每个集群中至少需要三个主数据库才能正常运行
特点:
每一个节点都存有这个集群所有主节点以及从节点的信息。
至少需要三台是跟redis的投票机制有关:
它们之间通过互相的ping-pong判断是否节点可以连接上。如果有一半以上的主节点去ping一个主节点的时候没有回应,集群就认为这个节点宕机了,然后去连接它的备用节点(故障转移)。如果某个节点和所有从节点全部挂掉,我们集群就进入fail状态。还有就是如果有一半以上的主节点宕机,那么我们集群同样进入fail状态。这就是我们的redis的投票机制
fail状态就是不可用状态,当集群不可用时,所有对集群的操作做都不可用,收到((error) CLUSTERDOWN The cluster is down)错误
Redis集群如何选举主节点(故障转移过程):
首先判断每个从节点与主节点断线时间超过设定值就没有资格,如果有资格复制偏移量越大优先级越高,一个从节点获得N/2+1的选票就可以成为主节点,选票是持有槽的主节点的选票
主观下线:当cluster-node-timeout时间内某节点无法与另一个节点顺利完成ping消息通信时,则将该节点标记另一个节点为主观下线状态。
客观下线:当大多数持有槽的主节点认为一个节点下线,那么该节点会标记为客观下线
未持有槽的主节点:比如新增的主几点,还没给他分匹配槽,要删除的主节点,撤销掉了他的槽
Redis集群如何分配数据:
哈希函数把所有的数据映射到一个固定范围内的整数集合,整数定义为槽(slot)。Redis Cluster槽的范围是0 ~ 16383。每个节点负责一定数量的槽。所有的键根据哈希函数映射到0 ~ 16383
优点:
1、可实现动态扩容
2、自动故障转移
3、负载均衡
引用场景:
数据量比较大
集群模式和哨兵模式的区别:
哨兵模式监控权交给了哨兵系统,集群模式中是工作节点自己做监控
哨兵模式发起选举是选举一个leader哨兵节点来处理故障转移,集群模式是在从节点中选举一个新的主节点,来处理故障的转移
redis做session共享
为什么做session共享
如果系统有多个点,用户在server1上存入数据到session中,用户在server2上就拿不到该存储的数据,为了解决这个问题就可以做session共享
怎么做session共享
把session数据存入redis中,各个点去redis中获取session数据来实现共享
redis常用命令
选择数据库select 0 ,选择id为0的数据库,默认选择的数据库就是id为0的数据库
对key的命令
DEL keyName 删除名为keyName键下的所有内容
DEL name type website 删除多个键,删除成功几个就返回几
EXISTS phone 判断名为phone的键是否存在
EXPIRE cache_page 30 #设置过期时间为 30 秒,再次执行刷新生存时间,过期后键被删除,单位是秒
EXPIREAT cache 1355292000 # 这个 key 将在 2012.12.12 过期,根据时间戳设置生存时间
PEXPIRE mykey 1500 设置生存时间1500毫秒
PEXPIREAT 以毫秒为单位设置 key 的过期 unix 时间戳
TTL cache_page # 查看剩余生存时间,秒
PTTL mykey # 查看剩余生存时间,毫秒
PERSIST mykey # 移除 key 的生存时间
KEYS pattern 查找当前数据库中所有符合给定模式 pattern 的 key 。特殊符号用 \ 隔开
KEYS * 匹配数据库中所有 key 。
KEYS h?llo 匹配 hello , hallo 和 hxllo 等。
KEYS h*llo 匹配 hllo 和 heeeeello 等。
KEYS h[ae]llo 匹配 hello 和 hallo ,但不匹配 hillo 。
MOVE song 1 # 将 song 移动到数据库 1,如果当前库和目标库都有song键或当前库没有song键move没有任何效果
RENAME message greeting改名 当已经存在时, RENAME 命令将覆盖旧值
RENAMENX key newkey 当且仅当 newkey 不存在时,将 key 改名为 newkey
TYPE key 返回 key 所储存的值的类型。
SCAN 命令用于迭代当前数据库中的数据库键。
SSCAN 命令用于迭代集合键中的元素。
HSCAN 命令用于迭代哈希键中的键值对。
ZSCAN 命令用于迭代有序集合中的元素(包括元素成员和元素分值)。
SCAN 命令、 SSCAN 命令、 HSCAN 命令和 ZSCAN 命令都返回一个包含两个元素的 multi-bulk 回复: 回复的第一个元素是字符串表示的无符号 64 位整数(游标), 回复的第二个元素是另一个 multi-bulk 回复, 这个 multi-bulk 回复包含了本次被迭代的元素。
sscan myset 0 match f* 从游标0开始查找键为myset中符合f*的数据
flushdb //删除当前数据库中的所有Key
flushall//删除所有数据库中的key
DBSIZE 返回当前数据库的 key 的数量。
对string结构数据
SET key value [EX seconds] [PX milliseconds] [NX|XX]
时间复杂度O(1)
将字符串值 value 关联到 key 。持有其他值, SET 就覆写旧值,无视类型。
对于某个原本带有生存时间(TTL)的键来说, 当 SET 命令成功在这个键上执行时, 这个键原有的 TTL 将被清除。
NX :只在键不存在时,才对键进行设置操作。XX :只在键已经存在时,才对键进行设置操作。
如:SET key value NX PX 30000
SETNX key value
时间复杂度O(1)
将 key 的值设为 value ,当且仅当 key 不存在。
若给定的 key 已经存在,则 SETNX 不做任何动作。设置成功,返回1;设置失败,返回0。
APPEND key value key 已存在并且是字符串, 将追加。不存在,和 SET key value 一样。
DECR key 时间复杂度O(1),将 key 中储存的数字值减一。不存在,那么 key 的值会先被初始化为 0 ,然后再执行 DECR 操作。如果值包含错误的类型,或字符串类型的值不能表示为数字,那么返回一个错误。
DECRBY key decrement
时间复杂度O(1)
将 key 所储存的值减去减量 decrement 。不存在,那么 key 的值会先被初始化为 0 ,然后再执行 DECRBY 操作。如果值包含错误的类型,或字符串类型的值不能表示为数字,那么返回一个错误。
GET key
返回 key 所关联的字符串值。不存在返回特殊值 nil 。不是字符串类型,返回一个错误。
GETSET key value
将给定 key 的值设为 value ,并返回 key 的旧值(old value)。
当 key 存在但不是字符串类型时,返回一个错误。没有旧值,返回 nil
INCR key
将 key 中储存的数字值增一。
如果 key 不存在,那么 key 的值会先被初始化为 0 ,然后再执行 INCR 操作。
如果值包含错误的类型,或字符串类型的值不能表示为数字,那么返回一个错误。
INCRBY key increment
将 key 所储存的值加上增量 increment 。
如果 key 不存在,那么 key 的值会先被初始化为 0 ,然后再执行 INCRBY 命令。
如果值包含错误的类型,或字符串类型的值不能表示为数字,那么返回一个错误。
INCRBYFLOAT key increment
为 key 中所储存的值加上浮点数增量 increment 。
如果 key 不存在,那么 INCRBYFLOAT 会先将 key 的值设为 0 ,再执行加法操作。
如果命令执行成功,那么 key 的值会被更新为(执行加法之后的)新值,并且新值会以字符串的形式返回给调用者。
MGET key [key ...]
时间复杂度O(N)
返回所有(一个或多个)给定 key 的值。
如果给定的 key 里面,有某个 key 不存在,那么这个 key 返回特殊值 nil 。因此,该命令永不失败。如:MGET redis mongodb mysql
MSET key value [key value ...]
时间复杂度O(N)
同时设置一个或多个 key-value 对。
如果某个给定 key 已经存在,那么 MSET 会用新值覆盖原来的旧值,如果这不是你所希望的效果,请考虑使用 MSETNX 命令:它只会在所有给定 key 都不存在的情况下进行设置操作。
MSET 时间复杂度O(N),是一个原子性(atomic)操作,所有给定 key 都会在同一时间内被设置,某些给定 key 被更新而另一些给定 key 没有改变的情况,不可能发生。
MSETNX key value [key value ...]
同时设置一个或多个 key-value 对,当且仅当所有给定 key 都不存在。
即使只有一个给定 key 已存在, MSETNX 也会拒绝执行所有给定 key 的设置操作。
MSETNX 是原子性的,因此它可以用作设置多个不同 key 表示不同字段(field)的唯一性逻辑对象(unique logic object),所有字段要么全被设置,要么全不被设置。
SETEX key seconds value
将值 value 关联到 key ,并将 key 的生存时间设为 seconds (以秒为单位)。
如果 key 已经存在, SETEX 命令将覆写旧值。
PSETEX key milliseconds value
这个命令和 SETEX 命令相似,但它以毫秒为单位设置 key 的生存时间,而不是像 SETEX 命令那样,以秒为单位。
STRLEN key
返回 key 所储存的字符串值的长度。时间复杂度O(1)
当 key 储存的不是字符串值时,返回一个错误。
对Hash结构数据
HSET key field value
时间复杂度O(1)
将哈希表 key 中的域 field 的值设为 value 。
如果 key 不存在,一个新的哈希表被创建并进行 HSET 操作。
如果域 field 已经存在于哈希表中,旧值将被覆盖。
HMSET key field value [field value ...]
同时将多个 field-value (域-值)对设置到哈希表 key 中。
此命令会覆盖哈希表中已存在的域。
如果 key 不存在,一个空哈希表被创建并执行 HMSET 操作。
HSETNX key field value
将哈希表 key 中的域 field 的值设置为 value ,当且仅当域 field 不存在。
若域 field 已经存在,该操作无效。
如果 key 不存在,一个新哈希表被创建并执行 HSETNX 命令。
HGET key field
时间复杂度O(1)
返回哈希表 key 中给定域 field 的值。
HMGET key field [field ...]
返回哈希表 key 中,一个或多个给定域的值。
如果给定的域不存在于哈希表,那么返回一个 nil 值。
因为不存在的 key 被当作一个空哈希表来处理,所以对一个不存在的 key 进行 HMGET 操作将返回一个只带有 nil 值的表。
HEXISTS key field
查看哈希表 key 中,给定域 field 是否存在。
HGETALL key
返回哈希表 key 中,所有的域和值。
HINCRBY key field increment
为哈希表 key 中的域 field 的值加上增量 increment 。
增量也可以为负数,相当于对给定域进行减法操作。
如果 key 不存在,一个新的哈希表被创建并执行 HINCRBY 命令。
如果域 field 不存在,那么在执行命令前,域的值被初始化为 0 。
对一个储存字符串值的域 field 执行 HINCRBY 命令将造成一个错误。
HINCRBYFLOAT key field increment
为哈希表 key 中的域 field 加上浮点数增量 increment 。
如果哈希表中没有域 field ,那么 HINCRBYFLOAT 会先将域 field 的值设为 0 ,然后再执行加法操作。
如果键 key 不存在,那么 HINCRBYFLOAT 会先创建一个哈希表,再创建域 field ,最后再执行加法操作。
HKEYS key
返回哈希表 key 中的所有域。
HVALS key
返回哈希表 key 中所有域的值。
HLEN key
返回哈希表 key 中域的数量。
对list结构数据
LSET key index value
- 时间复杂度O(N),如果操作的是头/尾部的元素,则时间复杂度为O(1)
- 将列表 key 下标为 index 的元素的值设置为 value 。
- 当 index 参数超出范围,或对一个空列表( key 不存在)进行 LSET 时,返回一个错误。
LINDEX key index
- 时间复杂度O(N)
- 返回列表 key 中,下标为 index 的元素。
- 下标(index)参数 start 和 stop 都以 0 为底,也就是说,以 0 表示列表的第一个元素,以 1 表示列表的第二个元素,以此类推。
- 你也可以使用负数下标,以 -1 表示列表的最后一个元素, -2 表示列表的倒数第二个元素,以此类推。
- 如果 key 不是列表类型,返回一个错误。
LLEN key
时间复杂度O(1)
返回列表 key 的长度。
如果 key 不存在,则 key 被解释为一个空列表,返回 0 .
如果 key 不是列表类型,返回一个错误。
RPUSH key value [value ...]
将一个或多个值 value 插入到列表 key 的表尾(最右边)。
如果有多个 value 值,那么各个 value 值按从左到右的顺序依次插入到表尾:比如对一个空列表 mylist 执行 RPUSH mylist a b c ,得出的结果列表为 a b c
LPOP key
时间复杂度O(1)
移除并返回列表 key 的头元素。
RPOP key
移除并返回列表 key 的尾元素。
LPUSH key value [value ...]
时间复杂度O(N),N为插入元素的数量
将一个或多个值 value 插入到列表 key 的表头,原子性地执行
如果有多个 value 值,那么各个 value 值按从左到右的顺序依次插入到表头
如果 key 不存在,一个空列表会被创建并执行 LPUSH 操作。
当 key 存在但不是列表类型时,返回一个错误。
LPUSHX key value
将值 value 插入到列表 key 的表头,当且仅当 key 存在并且是一个列表。
和 LPUSH 命令相反,当 key 不存在时, LPUSHX 命令什么也不做。
LRANGE key start stop
返回列表 key 中指定区间内的元素,区间以偏移量 start 和 stop 指定。包括 stop 下标的元素
LREM key count value
根据参数 count 的值,移除列表中与参数 value 相等的元素。
count 的值可以是以下几种:
count > 0 : 从表头开始向表尾搜索,移除与 value 相等的元素,数量为 count 。
count < 0 : 从表尾开始向表头搜索,移除与 value 相等的元素,数量为 count 的绝对值。
count = 0 : 移除表中所有与 value 相等的值。
LTRIM key start stop
对一个列表进行修剪(trim),就是说,让列表只保留指定区间内的元素,不在指定区间之内的元素都将被删除。
举个例子,执行命令 LTRIM list 0 2 ,表示只保留列表 list 的前三个元素,其余元素全部删除。
RPOPLPUSH source destination
命令 RPOPLPUSH 在一个原子时间内,执行以下两个动作:
将列表 source 中的最后一个元素(尾元素)弹出,并返回给客户端。
将 source 弹出的元素插入到列表 destination ,作为 destination 列表的的头元素。
RPUSHX key value
将值 value 插入到列表 key 的表尾,当且仅当 key 存在并且是一个列表。
和 RPUSH 命令相反,当 key 不存在时, RPUSHX 命令什么也不做。
BLPOP key [key ...] timeout
如果所有给定 key 都不存在或包含空列表,那么 BLPOP 命令将阻塞连接,直到等待超时,或有另一个客户端对给定 key 的任意一个执行 LPUSH 或 RPUSH 命令为止。
当存在多个给定 key 时, BLPOP 按给定 key 参数排列的先后顺序,依次检查各个列表。
timeout 接受一个以秒为单位的数字作为值。超时参数设为 0 表示阻塞时间无限长
相同的 key 可以被多个客户端同时阻塞。
不同的客户端被放进一个队列中,按『先阻塞先服务』
返回值:key和value都返回;如果超时返回nil
BRPOP key [key ...] timeout
类似BLPOP从右边弹出阻塞
RPOPLPUSH source destination timeout
将列表 source 中的最后一个元素(尾元素)弹出,并返回给客户端。
将 source 弹出的元素插入到列表 destination ,作为 destination 列表的的头元素。两个列表可以是同一个
返回值:成功返回弹出的元素,超时返回nil
对set结构数据
SADD key member [member ...]
时间复杂度O(N),N为添加的member个数
将一个或多个 member 元素加入到集合 key 当中,已经存在于集合的 member 元素将被忽略。
假如 key 不存在,则创建一个只包含 member 元素作成员的集合。
当 key 不是集合类型时,返回一个错误。
SCARD key
返回集合 key 的基数(集合中元素的数量)。 key 不存在时,返回 0 。
SDIFF key [key ...]
返回一个集合的全部成员,该集合是所有给定集合之间的差集。
不存在的 key 被视为空集。
SDIFFSTORE destination key [key ...]
这个命令的作用和 SDIFF 类似,但它将结果保存到 destination 集合,而不是简单地返回结果集。
如果 destination 集合已经存在,则将其覆盖。
destination 可以是 key 本身。
SINTER key [key ...]
返回一个集合的全部成员,该集合是所有给定集合的交集。
不存在的 key 被视为空集。
当给定集合当中有一个空集时,结果也为空集(根据集合运算定律)。
SINTERSTORE destination key [key ...]
这个命令类似于 SINTER 命令,但它将结果保存到 destination 集合,而不是简单地返回结果集。
如果 destination 集合已经存在,则将其覆盖。
destination 可以是 key 本身。
SUNION key [key ...]
返回一个集合的全部成员,该集合是所有给定集合的并集。
不存在的 key 被视为空集。
SUNIONSTORE destination key [key ...]
这个命令类似于 SUNION 命令,但它将结果保存到 destination 集合,而不是简单地返回结果集。
如果 destination 已经存在,则将其覆盖。
destination 可以是 key 本身。
SISMEMBER key member
判断 member 元素是否集合 key 的成员。
SMOVE source destination member
将 member 元素从 source 集合移动到 destination 集合。
SMOVE 是原子性操作。
SPOP key
移除并返回集合中的一个随机元素。
SRANDMEMBER key [count]
如果命令执行时,只提供了 key 参数,那么返回集合中的一个随机元素。
从 Redis 2.6 版本开始, SRANDMEMBER 命令接受可选的 count 参数:
如果 count 为正数,且小于集合基数,那么命令返回一个包含 count 个元素的数组,数组中的元素各不相同。如果 count 大于等于集合基数,那么返回整个集合。
如果 count 为负数,那么命令返回一个数组,数组中的元素可能会重复出现多次,而数组的长度为 count 的绝对值。
SREM key member [member ...]
移除集合 key 中的一个或多个 member 元素,不存在的 member 元素会被忽略。
对SortedSet结构数据
ZADD key score member [[score member] [score member] ...]
- 时间复杂度O(Mlog(N)),M为添加的member数量,N为Sorted Set中的member数量
- 将一个或多个 member 元素及其 score 值加入到有序集 key 当中。
- 如果某个 member 已经是有序集的成员,那么更新这个 member 的 score 值,并通过重新插入这个 member 元素,来保证该 member 在正确的位置上。
- score 值可以是整数值或双精度浮点数。
ZCARD key
- 返回有序集 key 的成员数量。
- ZCOUNT key min max
- 时间复杂度O(log(N))
- 返回有序集 key 中, score 值在 min 和 max 之间(默认包括 score 值等于 min 或 max )的成员的数量。
ZINCRBY key increment member
- 为有序集 key 的成员 member 的 score 值加上增量 increment 。
- 可以通过传递一个负数值,
ZRANGE key start stop [WITHSCORES]
- 返回有序集 key 中,指定下标区间内的成员。
- 其中成员的位置按 score 值递增(从小到大)来排序。
ZRANGEBYSCORE key min max [WITHSCORES] [LIMIT offset count]
- 返回有序集 key 中,所有 score 值介于 min 和 max 之间(包括等于 min 或 max )的成员。有序集成员按 score 值递增(从小到大)次序排列。
- 具有相同 score 值的成员按字典序
- 可选的 WITHSCORES 参数决定结果集是单单返回有序集的成员,还是将有序集成员及其 score 值一起返回。
- 可选的 LIMIT 参数指定返回结果的数量及区间(就像SQL中的 SELECT LIMIT offset, count ),注意当 offset 很大时,定位 offset 的操作可能需要遍历整个有序集,此过程最坏复杂度为 O(N) 时间。
ZRANK key member
返回有序集 key 中成员 member 的排名。其中有序集成员按 score 值递增(从小到大)顺序排列。
排名以 0 为底,也就是说, score 值最小的成员排名为 0 。
ZREM key member [member ...]
移除有序集 key 中的一个或多个成员,不存在的成员将被忽略。
ZREMRANGEBYRANK key start stop
移除有序集 key 中,指定排名(rank)区间内的所有成员。
区间分别以下标参数 start 和 stop 指出,包含 start 和 stop 在内。
ZREMRANGEBYSCORE key min max
移除有序集 key 中,所有 score 值介于 min 和 max 之间(包括等于 min 或 max )的成员。
ZREVRANGE key start stop [WITHSCORES]
返回有序集 key 中,指定区间内的成员。
其中成员的位置按 score 值递减(从大到小)来排列。
具有相同 score 值的成员按字典序的逆序
ZREVRANGEBYSCORE key max min [WITHSCORES] [LIMIT offset count]
返回有序集 key 中, score 值介于 max 和 min 之间(默认包括等于 max 或 min )的所有的成员。有序集成员按 score 值递减(从大到小)的次序排列。
具有相同 score 值的成员按字典序的逆序
ZREVRANK key member
返回有序集 key 中成员 member 的排名。其中有序集成员按 score 值递减(从大到小)排序。(Reverse 反转颠倒)
ZSCORE key member
返回有序集 key 中,成员 member 的 score 值。
如果 member 元素不是有序集 key 的成员,或 key 不存在,返回 nil 。
ZUNIONSTORE destination numkeys key [key ...] [WEIGHTS weight [weight ...]] [AGGREGATE SUM|MIN|MAX]
计算给定的一个或多个有序集的并集,其中给定 key 的数量必须以 numkeys 参数指定,并将该并集(结果集)储存到 destination 。
默认情况下,结果集中某个成员的 score 值是所有给定集下该成员 score 值之 和 。
WEIGHTS
使用 WEIGHTS 选项,你可以为 每个 给定有序集 分别 指定一个乘法因子(multiplication factor),每个给定有序集的所有成员的 score 值在传递给聚合函数(aggregation function)之前都要先乘以该有序集的因子。
如果没有指定 WEIGHTS 选项,乘法因子默认设置为 1 。
AGGREGATE
使用 AGGREGATE 选项,你可以指定并集的结果集的聚合方式。
默认使用的参数 SUM ,可以将所有集合中某个成员的 score 值之 和 作为结果集中该成员的 score 值;使用参数 MIN ,可以将所有集合中某个成员的 最小 score 值作为结果集中该成员的 score 值;而参数 MAX 则是将所有集合中某个成员的 最大 score 值作为结果集中该成员的 score 值。
ZINTERSTORE destination numkeys key [key ...] [WEIGHTS weight [weight ...]] [AGGREGATE SUM|MIN|MAX]
计算给定的一个或多个有序集的交集,其中给定 key 的数量必须以 numkeys 参数指定,并将该交集(结果集)储存到 destination 。
默认情况下,结果集中某个成员的 score 值是所有给定集下该成员 score 值之和.
对bitMap数据结构
SETBIT key offset value
对 key 所储存的字符串值,设置或清除指定偏移量上的位(bit)。
offset 范围:[0 ,2^32-1]只能是0或者正整数;value只能是0或者1(bit位上默认初始化值是0)
返回指定偏移量原来储存的位。不存在的默认返回0
GETBIT key offset
返回值字符串值指定偏移量上的位(bit)。当偏移量 OFFSET 比字符串值的长度大,或者 key 不存在时,返回 0 。
BITCOUNT key [start] [end]
计算给定字符串中,返回被设置为 1 的比特位的数量。时间复杂度O(N)
start 和 end 参数都可以使用负数值:比如 -1 表示最后一个位,而 -2 表示倒数第二个位,以此类推。
bitMap如何统计签到
根据日期
offset =今天是一年中的第几天;
key = 年份#用户id
setbit 2018#user1 1 1
setbit 2018#user1 2 1
bitcount 2018#user1 返回2
bitMap计算月活
offset=user_id
key=月份
play:yyyy - mm, user_id , 1
setbit key offset 1;
对HyperLogLog
PFADD key element [element ...]
添加指定元素到 HyperLogLog 中。
PFCOUNT key [key ...]
返回给定 HyperLogLog 的基数估算值。
PFMERGE destkey sourcekey [sourcekey ...]
将多个 HyperLogLog 合并为一个 HyperLogLog
实现:通过存储元素的hash值的第一个1的位置,来计算元素数量。
计算基数所需的空间总是固定 的、并且是很小的。
每个 HyperLogLog 键只需要花费 12 KB 内存,就可以计算接近 2^64 个不同元素的基 数。
计算基数具有一定的误差,标准误差是0.81%,对结果的精确度要求不高,那么HyperLoglog将是一个不错的选择。
高级不精确去重的数据结构.(一般是超过一百个就开始不准确了)
占用空间小(一个键最多12k,可以计算2^64个元素)
发布订阅
PUBSUB CHANNELS 查看活跃频道列表
PUBSUB NUMSUB [channel-1 ... channel-N] 查看频道的订阅者数量, 订阅模式的客户端不计算在内。
如:PUBSUB NUMSUB mychannel-1 mychannel-2
Psubscribe 订阅一个或多个符合给定模式的频道。
每个模式以 * 作为匹配符,比如 it* 匹配所有以 it 开头的频道
如:PSUBSCRIBE my*
Subscribe 命令用于订阅给定的一个或多个频道的信息
如:SUBSCRIBE mychannel
Publish 将信息发送到指定的频道,返回接收到消息的订阅者数量
如:PUBLISH mychannel “message”
monitor
Redis Monitor 命令用于实时打印出 Redis 服务器接收到的命令,调试用。
redis持久化
持久化就是把内存的数据写到磁盘中去,防止服务宕机了内存数据丢失
Redis 提供了两种持久化方式:RDB(默认)(快照方式)(即在指定目录下生成一个dump.rdb文件) 和AOF(操作日志方式)
使用redis时可以启用一种,或两种方式都启用。
快照方式:
这种方式是就是将内存中数据以快照的方式写入到二进制文件中,默认的文件名为dump.rdb
可以配置redis在n秒内如果超过m个key被修改就自动做快照,所以如果redis意外down掉的话,就会丢失最后一次快照后的所有修改。如果应用要求不能丢失任何修改的话,可以采用aof持久化方式。
操作日志方式:
在使用aof持久化方式时,redis会将每一个收到的写命令都通过write函数追加到文件中(默认是appendonly.aof)。当redis重启时会通过重新执行文件中保存的写命令来在内存中重建整个数据库的内容。(aof可以通过配置来选择是收到命令就持久化,还是每秒钟持久化一次,也可以依赖操作系统由操作系统判断什么时候持久化)
aof也有丢失数据的可能如每秒钟持久化一次,就有可能丢失1s的数据,如果配置的每收到命令就记录日志持久化就不会丢失数据,即使出现了某条日志只写入了一半的情况,也可以使用redis-check-aof工具轻松修复。如果由操作系统判断进行持久化就有可能丢失数据
比较:
1、aof文件比rdb更新频率高,优先使用aof还原数据。(因为rdb是一定时间间隔执行一次)
2、aof比rdb更安全文件也更大
3、rdb性能比aof好
4、如果两个都配了优先加载AOF
|
命令 |
RDB |
AOF |
说明 |
|
启动优先级 |
低 |
高 |
RDB和AOF都开启的情况下,Redis重启后,选择AOF进行恢复。大部分情况下它保存了比RDB更新的数据 |
|
体积 |
小 |
大 |
RDB二进制模式存储,而且做了压缩。AOF虽然有AOF重写,但是体积相对还是大很多,毕竟它是记日志形式 |
|
恢复速度 |
快 |
慢 |
RDB体积小,恢复速度快。AOF体积大,恢复速度慢 |
|
数据安全 |
丢数据 |
根据策略决定 |
RDB丢上次快照后的数据,AOF根据always、everysec、no策略决定是否丢数据 |
|
轻重 |
重 |
轻 |
AOF是追加日志,所以比较轻的操作。而RDB是CPU密集型操作,对磁盘,以及fork时对内存的消耗都比较大 |
持久化配置方法
RDB是Redis的默认持久化方式。
在redis.conf文件中配置
redis 优化经验
- 不使用占用线程时间长的命令,如keys命令(因为redis是单线程,影响redis的使用)
- 根据业务选择合适的数据类型
- 当业务场景不需要数据持久化时,关闭所有的持久化方式
- 设置的key尽量都设置失效时间
Redis分布式锁
分布式锁主要是对集群或分布式环境下多线程访问共享资源的线程安全保障机制
分布式锁要点
1、互斥
在分布式高并发的条件下,我们最需要保证,同一时刻只能有一个线程获得锁,这是最基本的一点。
2、防止死锁
在分布式高并发的条件下,比如有个线程获得锁的同时,还没有来得及去释放锁,就因为系统故障或者其它原因使它无法执行释放锁的命令,导致其它线程都无法获得锁,造成死锁。
所以分布式非常有必要设置锁的有效时间,确保系统出现故障后,在一定时间内能够主动去释放锁,避免造成死锁的情况。
3、性能
对于访问量大的共享资源,需要考虑减少锁等待的时间,避免导致大量线程阻塞。
所以在锁的设计时,需要考虑两点。
1、锁的颗粒度要尽量小。比如你要通过锁来减库存,那这个锁的名称你可以设置成是商品的ID,而不是任取名称。这样这个锁只对当前商品有效,锁的颗粒度小。
2、锁的范围尽量要小。比如只要锁2行代码就可以解决问题的,那就不要去锁10行代码了。
4、重入
我们知道ReentrantLock是可重入锁,那它的特点就是:同一个线程可以重复拿到同一个资源的锁。重入锁非常有利于资源的高效利用。
针对以上Redisson都能很好的满足
分布式锁是解决什么问题
我们通常使用的synchronized或者Lock都是线程锁,对同一个JVM进程内的多个线程有效。
访问资源(存储的数据)在分布式或集群访问时的线程安全问题:
当应用的同一段代码要保证多个节点线程安全时要用到分布式锁
当应用的不同位置的代码要保证访问同一个资源线程安全时要用到分布式锁
常用的分布式锁方式
一、数据库锁
- 基于MySQL锁表
依靠数据库唯一索引来实现,当想要获得锁时,即向数据库中插入一条记录,释放锁时就删除这条记录。
- 采用乐观锁增加版本号
根据版本号来判断更新之前有没有其他线程更新过,如果被更新过,则获取锁失败。
优点:直接使用数据库,使用简单。
缺点:分布式系统大多数瓶颈都在数据库,使用数据库锁会增加数据库负担。
二、缓存锁
- 使用redis基于setnx、expire两个命令来实现
基于setnx(set if not exist)的特点,当缓存里key不存在时,才会去set,否则直接返回false。如果返回true则获取到锁,否则获取锁失败,为了防止死锁,我们再用expire命令对这个key设置一个超时时间来避免。
缺点:当我们setnx成功后,线程发生异常中断,expire还没来的及设置,那么就会产生死锁。
- 使用redis基于set命令同时设置nx和过期时间
如:SET key value NX PX 30000,可以同时满足互斥性(加锁)、死锁(自动过期)
- 3. RedLock算法
redlock算法是redis作者推荐的一种分布式锁实现方式
4.使用redisson编写好的分布式锁
对于程序时间超过锁时间,redisson提供了自动续期的功能,如果自己调用加锁的api没有传入过期时间,redisson会设置默认的过期时间(默认30s),如果锁到期还没有执行完就会续期,每个10s检查一次;如果自己传入了过期时间就不会自动续期;自动续期功能利用的看门狗
优点:性能高,实现起来较为方便
缺点:通过锁超时机制不是十分可靠,当线程获得锁后,处理时间过长导致锁超时,就失去了锁的作用。(可以通过续期锁时间来弥补)
- zookeeper分布式锁
首先创建一个持久父节点,然后每个要获得锁的线程都会在这个节点下创建个临时顺序节点(唯一的有序的),由于序号的递增性,可以规定排号最小的那个获得锁。所以,每个线程在尝试占用锁之前,首先判断自己是排号是不是当前最小,如果是,则获取锁。
当处理完删除自己的节点代表解锁,zookeeper监听器负责监听文件节点的变化,通过监听器通知下一个线程来获取锁
优点:不依靠超时时间释放锁(依靠zookeeper的高可用特点);可靠性高;系统要求高可靠性时,建议采用zookeeper锁。
缺点:性能比不上缓存锁,因为要频繁的创建节点删除节点。
Setnx实现分布式锁
SETNX key value
将 key 的值设为 value ,当且仅当 key 不存在。设置成功返回1,失败返回0,value设置成当前时间+超时时间,防止获取锁后系统出现问题,没有释放锁的情况
通过SETNX来设置锁,如果设置成功说明获取到了锁
没有获取到锁的get获取value的值判断锁是否过期,如果过期就GETSET key 设置值value是当前时间+超时时间,GETSET如果返回的时间小于现在的时间说明加锁成功
主动释放锁就是del命令删除设置的值
从SETNX开始就在一个while循环中
Redisson实现的分布式锁
流程:
首先我们一个锁对象
RLock lock=redissonClient.getLock(‘lockName’);
在需要加锁的地方调用
boolean getLock = false;
try{
//尝试加锁,最多等待2秒,上锁以后10秒如果没有解锁进行自动解锁
getLock = lock.tryLock(2, 10, TimeUnit.SECONDS);
}catch(){
logger.error("抢购流程出错", e);
throw e;
}finally{
if (getLock) {
lock.unlock();
}
}
tryLock方法说明:
等待时间:如果超过等待时间还没有获取到锁,就返回false;是在每次尝试获取锁后进行判断。
redisson分布式锁支持所有的redis部署方式
redisson实现的分布式锁支持锁的重入,锁的自动释放
Redisson对分布式锁的操作都是通过Lua脚本进行的,redis是单线程的,这样保证这段复杂业务逻辑执行的原子性。
加锁(整个内容在一个lua脚本):
exists命令判断redis中是否有存在指定的key(就是我们加锁时指定的锁key),如果没有就可以加锁,加锁会通过hash结构存储锁信息,key为字符串类型的指定内容,field是uuid和线程id组成的字符串,value是数字,刚开始是1,保存内容后会通过pexpire命令设置key的有效时间;
如果exists判断为已存在,就判断当前线程是否在hash结构中,如果不在,就调用pttl命令返回key的剩余生存时间,就会订阅锁消息,之后进行while循环去获取锁,如果循环获取锁过程中获取到了锁,就不在循环了,并取消订阅;如果没有获取到锁,这个while并不会一直空消耗cpu空跑,而是会被阻塞等待锁释放的消息,锁释放消息一出,马上去争抢锁;
重入:
如果exists判断为已存在,但当前线程在hash结构中,就不需要争抢锁,要进行锁重入,重入value数字会+1,重新设置有效时间
解锁:
如果key已经不存在了,说明已经解锁,会发布解锁消息
如果该线程不是获取锁的线程(通过hash中的field判断)不能解锁
对value进行-1,如果还大于0,说明该线程是重入锁,还不能释放锁,重新设置锁的失效时间
所有验证都通过了就说明能够释放锁,删除key,发布锁释放消息
看门狗说明:
看门狗是一个后台进程,他会在获取锁之后默认每隔10s检查一下是否需要续期;如果设置了锁的有效期是不会用看门狗的;使用默认的有效期(30s)才会用到看门狗,默认每次续期30s
redis挂了怎么办:
redis使用集群,在master上操作,slave进行同步数据,当然会有锁从master复制到slave的时候挂了,也是会出现同一资源被多个client加锁的情况。这种情况就通过记录的日志来确定情况,对需要处理的进行人工手动处理
总结:
锁的信息是存放在hash的数据结构中,hash的key为用户指定值,field是和线程id相关的值,value是数字重入会incr增加
加锁:是往hash结构中存放线程相关的数据
释放锁:是删除hash的key
锁重入:是增加value的值
锁等待:获取不到锁,订阅消息,线程阻塞,等待锁释放消息,锁释放消息后重新争抢,抢不到继续阻塞
锁续期:后台看门狗每个10s检查是否需要续期(使用默认有效期起作用,自定义锁释放时间无效)
延时队列、阻塞队列
Redisson的延迟队列和阻塞队列
延迟队列:
延迟队列是队列中的元素可以指定多少时间后进行进行取出
redisson的延迟队列实现:
使用了redis中的list数据类型,配合zset数据类型;zset的scroe保存着数据要取出的时间点,配合发布订阅和定时器来对比时间来取出数据;逻辑采用lua脚本来编写避免线程安全问题
redisson的阻塞队列实现:
基于redis的list数据结构,使用blpop命令来获取数据,获取不到就会阻塞
(
这里使用了三个结构来存储,一个是目标队列list;一个是原生队列list,添加的是带有延时信息的结构体;一个是zset,其score为要过期的时间点
刚开始数据在原生队列中,zset存有与list中元素关联的信息,zset的分数存的是要过期的时间点,这个元素如果对于zset是一个新的元素就会通过发布订阅,发布其要过期的时间点方便定时器对该元素进行过期检查
之后定时任务根据zset的分数判断是否过期,然后将到期的元素转移到目标list
使用lua脚本操作redis的list和zset使用发布订阅及很多异步回调等来实现
阻塞队列
Redisson的阻塞队列基于java.util.concurrent.BlockingQueue接口
阻塞队列的功能:
如果线程尝试去从一个空的队列中提取对象的话,这个线程将会处于阻塞之中,直到队列中加入一个元素。
如果线程尝试向一个队列插入数据,队列已经满了,该线程会被阻塞,知道队列中可以加入元素
Redisson会使用redis的blpop命令去获取redis中list中的数据,获取到后阻塞的线程发现阻塞队列中有数据了就能够消费了
)
blpop命令:
BLPOP LIST1 LIST2 .. LISTN TIMEOUT
Blpop 移出并获取列表的第一个元素, 如果列表没有元素会阻塞列表直到等待超时或发现可弹出元素为止。如果TIMEOUT为0代表永不超时
blpop和单线程
blpop的实现是通过链表配合redis的循环访问来实现的,如果blpop对应的list有数据直接返回,如果没有数据就会把list对应的key放到blocking_keys哈希表中,哈希表的key为list的key,value是执行blpop命令的客户端组成的链表,当下次push命令发出时,服务器检查blocking_keys中是否有对应的key,如果存在就往ready_keys这个哈希表中键存为该key值存为null;同时将value插入到对应的list中,遍历ready_keys中就绪的key然后根据blocking_keys找到对应的客户端取出一个,响应给客户端。
过程中redis的线程并没有被阻塞,索引还是单线程的,客户端线程是被阻塞了
总结:
redisson是把进行blpop的客户端的信息保存起来了,保存到一个hash结构中(key存的要访问的list的key,value是被阻塞的客户端的信息组成的链表),redis的线程就可以做其他事情了,当list中有数据的时候,redis的线程再进行处理

浙公网安备 33010602011771号