Redis
1.什么是redis?他是用来做什么的?
Redis(Remote Dictionary Server远程字典服务),是一个c语言编写的nosql的数据库,支持网络,可基于内存也可持久化的,key-value类型的数据库。
与一般数据库不同的是,redis是存储在内存中的,他的读写速度非常快,常常被广泛应用到缓存之中,另外,redis还可以用来做分布式锁。
1.1redis除了用作缓存,还可以干什么?
1、最新列表
例如新闻列表页面最新的新闻列表,如果总数量很大的情况下,尽量不要使用select a from A limit 10,尝试redis的 LPUSH命令构建List,一个个顺序都塞进去就可以啦。不过万一内存清掉了咋办?也简单,查询不到存储key的话,用mysql查询并且初始化一个List到redis中就好了。
2、排行榜应用
实现这个功能主要用到的redis数据类型是redis的有序集合zset。zset 是set 类型的一个扩展,比原有的类型多了一个顺序属性,此属性在每次插入数据时会自动调整顺序值,保证value值按照一定顺序连续排列。
我们假设是一个游戏经验值排行榜,那主要的实现思路是:
1、在一个新的玩家参与到游戏中时,在redis中的zset中新增一条记录(记录内容看具体的需求)score为0
2、当玩家的经验值发生变化时,修改该玩家的score值
3、使用redis的ZREVRANGE方法获取排行榜
3、计数器应用
Redis的命令都是原子性的,你可以轻松地利用INCR、DECR命令进行原子性操作,来构建计数系统。由于单线程,可以避免并发问题,保证不会出错,而且100%毫秒级性能。
比如在一个 web 应用程序中,如果想知道用户在一年中每天的点击量,那么只要将用户 ID 以及相关的日期信息作为键,并在每次用户点击页面时,执行一次自增操作即可。
4、数据排重
Redis set是可以自动排重的,当你需要存储一个列表数据,又不希望出现重复数据时,set是一个很好的选择,并且set提供了判断某个成员是否在一个set集合内的重要接口。
实现方案:
set 的内部实现是一个 value永远为null的HashMap,实际就是通过计算hash的方式来快速排重的,这也是set能提供判断一个成员是否在集合内的原因。
5、实时的反垃圾系统
反垃圾系统通常都是基于关键词的,使用Redis储存关系词,能够利用Redis的高性能,为监控系统提供稳定及精确的实时监控功能,典型的案例如,邮件系统、评论系统等。
6、可以发布、订阅的实时消息系统
Redis中Pub/Sub系统可以构建实时的消息系统,比如,很多使用Pub/Sub构建的实时聊天应用。
设计思路:
服务端发送消息(含标题,内容),标题按照一定规则存入redis,同时标题(以最少的信息量)推送到客户端,客户点击标题时,获取相应的内容阅读.
如果未读取,可以提示多少条未读,redis能够很快记数
根据一定时间清理缓存
技术实现:
需要redis数据库,客户端websocket,服务器端websocket
7、队列应用
队列在现在程序中应用十分广泛,比如日志推送、任务处理等等。以往通常使用http sqs实现队列,其实,使用redis的list类型,也可以实现队列。
1.redis有哪些数据结构,特点?使用场景?
redis有5种数据结构和6种底层数据结构,这五种数据结构的底层数据结构是这6个底层数据结构
5种数据结构为:string,Hash,List,set,zset
6种底层数据结构为:简单动态字符串,双向链表,压缩列表,哈希表,调表和整数数组。
redis是键值对类型的,键都是string,值可以是任意类型的
redis的存储的底层结构是hash结构,他有一个全局哈希表,可以通过key计算出位置,然后将kev-value都存储在此位置的。
reids解决哈希冲突:提供两块内存空间,相当于扩容,将原来的映射渐进式的复制到扩容后的哈希表中,然后释放之前的空间。
1.string :string是redis最基本的数据类型,是key-value类型的,一个key对应一个value;redis的类型是二进制安全的,redis的值可以包含各种数据比如jpg图片或者序列化之后的对象等。一个key对应的value最大能存储512mb.
可以进行单值缓存,比如说存储手机号和验证码,
可以进行对象缓存,建议是不再改变值的对象,存储方式为json存储
还可以使用计数器,incr key 给某个key值自增1,decr key 给某个key值自减1,del,get,set等。
string基于sds实现的.
2.Hash::hash结构适合存储需要改变对象属性的值的场景,是一个键值(key=>value)对集合;是一个 string 类型的 field 和 value 的映射表。可以进行单个存储或者多行存储,hset,hget,(单个)hmset,hmget(多个)
del,hgetall,hlen,hincr by key v给某个key的某个属性值加v或者加-v,-v就是值减少的意思
hash结构可以存储个用户的个人信息,对象信息。
3.List:列表是简单的字符串列表,按照插入顺序排序。你可以添加一个元素到列表的头部(左边)或者尾部(右边)
常用命令:lpush、rpush、lpop、rpop、lrange等。
场景:list可以实现列表,栈等数据结构,经常会被用于消息队列的服务,以完成多程序之间的消息交换。
底层是使用跳表来实现的。
4.set:Set是string类型的无序集合。和列表一样,在执行插入和删除和判断是否存在某元素时,效率是很高的。集合最大的优势在于可以进行交集并集差集操作。集合是通过hash表实现的,所以添加,删除查找的时间复杂度都是0(1)。
常用命令:sadd,srem,s’members(某个key中的所有键值),scard(某个key中的元素的个数)
sunion key1 key2(并集)sdiff key1 key2(差集)sinter key1 key2(交集)
场景:利用交集求共同好友,利用唯一性,可以统计访问网站的所有独立IP,好友推荐的时候根据tag求交集,大于某个临界值的就可以推荐。
底层采用的是字典也就是哈希表,当存储的都是数字时用的是intset整数数组。
5.zset:zset 和 set 一样也是string类型元素的集合,且不允许重复的成员,不同的是每个元素都会关联一个double类型的分数。redis正是通过分数来为集合中的成员进行从小到大的排序。
zset中key不可以重复,这个分数是可以重复的。sorted set是插入有序的,会自动排序。
常用命令:add,zrange,zrem,zcard
场景:需要一个有序的不重复的列表可以用zset,比如说微信步数排行榜。
例如存储全班同学的成绩,其集合value可以是同学的学号,而score就可以是成绩。
2.redis设置失效时间
有些数据是不需要一直存在的,比如说验证码,过几分钟就没有用了,一直存储这会浪费内存,于是我们可以设置key的有效时间。在set的适合可以设置,set name jim ex 20即就设置了一个20秒有效的键值,20秒后key会自动过期的。 ex是秒的单位,还可以是px是毫秒的单位。ttl key 可以查看还有多次时间过期,是查看秒,pttl是看毫秒。
如果我们在设置值的适合没给失效时间,默认是永久不过期的,我们可以设置后再给他加上失效时间,可以使用expire key time 来设置 比如说 expire name 20是设置20秒过期,pexpire是设置毫秒
3.reids的线程模型
3.1redis的单线程模型
redis内部使用文件事件处理器file event handler,这个文件处理器是单线程的,所以Redis才叫单线程模型,采用IO多路复用机制同时监听多个socket,将产生事件的socket压入内存队列中,事件分派器根据socket上的事件类型来选择对于的事件处理器进行处理。
文件事件处理器的结构包含4个部分,
1.多个socket
2.io多路复用
3.文件事件分派器
4.事件处理器包括命令请求处理器,命令回复处理器,连接应答处理器等。
文件事件处理器的流程:1.当被监听的套接字准备好执行连接应答,读取,写入,关闭等操作时,与操作相对应的文件事件就会产生,文件处理器会调用套接字管理好的事件处理器来处理事件。
2.事件处理器是单线程运行的,但是通过io多路复用机制监听多个socket,根据socket目前执行的任务为套接字关联不同的事件处理器。
3.2如何开启redis多线程
io线程默认是管理的,想要开启的化,在redis.conf里面可以配置
io-threads 后面写的数字一般要小于cpu的核数
io-threads-do-reads yes 表示启用多线程
3.3redis是单线程还是多线程
不同的版本是有区别的,在redis6.x以前是真正意义上的单线程,也就是说 客户端连接,命令的执行都是由单个线程来完成的。在redis6.x以后引入了多线程来处理网络请求,而对键和值的操作依旧是单线程的,也就是说数据操作是单线程的,其他的像是网络请求,持久化,数据同步等操作是由其他线程来额外执行的。
3.4为什么单线程速度还这么快
1.数据存储在内存中,读取速度快,cpu不是性能瓶颈
2.结构简单,key-value底层是hash结构的,查询操作的时间复杂度都是0(1)
3.采用多路复用和阻塞式io模型,提高了连接访问的效率
4.单线程执行命令,减少了线程切换之间的开销,而且是线程安全的。
3.3为什么选择单线程
抛开持久化不谈,redis是纯内存操作,执行速度非常快,他的性能瓶颈是网络延迟而不是执行速度,多线程不会带来巨大的性能提升。
多线程会导致线程上下文切换,造成开销。
引入多线程会面临线程安全问题,解决这些问题会造成其他很大的开销。性能也会降低。
4.redis的持久化
io对象序列化和反序列化是指,把对象信息持久保存在硬盘上
redis是如何进行持久化的?
redis进行持久化有两种方式,一种是RDB(Redis DataBase)一种是AOP(Append onlyFile)
这两种方式都可以在配置文件中配置,redis默认的持久化方式是RDB
1.RDB是直接将内存中的数据快照,然后生成一个.rdb的压缩后的二进制文件。这个文件里面存的就是快照的信息,也就是redis的键值对信息。触发rdb有3中行为,
1.使用save或者bgsave命令可以出发快照,
2是在配置文件里面可以配置 save m n n秒内最少有m个数据被操作,会自动触发bgsave命令,
3.退出redis客户端时,最后可以选择是否save,
4.执行flushall命令在redis客户端。
bgsave的基本流程,fork主进程得到一个子进程 共享内存空间;子进程读写内存数据并写入新的rdb文件;用新的rdb文件替换旧的rdb文件。
优点,生成压缩后的rdb文件,体积小,恢复数据快,对数据的完整性要求不高。
缺点,bgsave命令每次执行都要执行fork操作创建子进程,开销很大,不宜频繁执行。也就是说实时性不太好。
AOF
aof方式就和数据库里面的日志一样。aof会记录每次的set命令。恢复数据的时候把这些命令重新执行即可。默认是不开启的。
aof有两种同步方式,一种是appendfsync always 每次的写入命令都要记录一下。
另一种是appendfsync everysec 每秒记录一次,这一秒内的数据可能会丢失。
aof优点,数据的一致性和完整性更好
缺点,aof记录的越多,数据恢复的时间就越长。
5.redis事务
redis在执行单条命令的时候是原子性的(单线程的一次只能由一个线程执行命令)有时候一次操作需要执行多条命令,如何保证多条命令整体执行,可以通过redis事务实现。
开启事务multi--------->添加命令,多组命令,注意命令不会立刻被执行,而是存入一个队列------->exec执行命令,才会将队列中的多条命令依次执行,执行一个事务的多条命令时,其他客户端会被隔离,不会交替执行。
但是事务不保证多条命令执行的原子性,假如执行了3条命令,第二条命令有语法错误,则第一条和第三条会执行成功,第二条不会执行成功。
6.主从复制
主从指的是主机和从机------>集群的架构 为什么要用集群?
如果只有一台redis服务,如果服务宕机了,则所有的请求都会到达数据库,导致数据库宕机,可以搭建多个redis服务器,如果其中一台出现故障,其他服务也可以正常使用。
主机负责写数据将数据同步到从机,一般的读数据都是从从机查询,从而实现读写分离,写命令由主机执行,读命令由从机执行。
主从复制包括全量复制,增量复制两种。
全量复制:一般用于初次复制场景,Redis早期支持的复制功能只有全量复制,它会把主节点全部数据一次性发送给从节点,当数据量较大时,会对主从节点和网络造成很大的开销。
流程:1.从机节点请求增量同步
2.主机节点判断replid,发现不一致,拒绝增量同步,这里的replid是数据集的id,保证这个从机是属于这个主机的从机。
3.主机将完整内存文件生成rdb文件,发送给从机,
4.从机清空本地内存,加载rdb文件
5.主机将rdb期间的命令记录在repl_baklog中,并持续将log中的命令发送给从机。
6.从机执行接受到的命令,保持与主机直接到同步。
增量复制:用于处理在主从复制中因网络闪断等原因造成的数据丢失场景,当从节点再次连上主节点后,如果条件允许,主节点会补发丢失数据给从节点。因为补发的数据远远小于全量数据,可以有效避免全量复制的过高开销。
流程:1.判断replid是否正确。
2.去repl_baklog中获取offset后的数据,主机发送命令。从机执行得到同步。
offset是偏移量,从机也有偏移量,主机也有偏移量,不一样则从机需要更新主机的数据了。
区别:全量是发送整个rdb文件和后续的命令。
增量是根据从机的offset找主机对应的offset,执行对应的offset后面的代码。
7.哨兵机制
有一个单独的线程对集群中的多台服务进行监听,给每个服务发送请求,如果没有相应。表面已经出现了故障,比如主机宕机了,会在从机中选取一台提升为主机,当原来的主机恢复之后,又可以当作主机使用。
哨兵的作用
监控,故障转移,通知 。
哨兵怎么判断一个机器是否健康,也就是有没有宕机?
每隔一秒发送一个ping命令,长时间没回复,则他就会认为服务坏了。如果多个哨兵都认为某个服务坏了,则它就真的坏了。
故障转移的步骤。
首先选定一个从机作为新的主机,提升他为主机 。然后给别的从机执行命令就是把别的从机连接到这个主机上,把旧主机的配置修改,让他变为从机。
8.key的过期策略
为key设置过期时间,时间到期后,redis如何处理过期的key。redis中同时使用了惰性删除和定期删除,既避免了惰性删除所带来的大量空间的占用,也避免了数据库频繁操作key,对cpu的压力。
1.立即删除:到期立即执行回调函数,立即释放内存,对redis的性能有影响
2.惰性删除:到期后不会立即删除,到下次使用改键的时候,根据状态(设置会会记录的)来决定是删除还是继续使用,很占用内存。
3.定期删除,每隔一段时间对所有到期的key进行删除,类似于java的垃圾回收线程。
9.缓存穿透,击穿,雪崩
查询数据的流程
发起查询请求—>缓存—>没有—>数据库查询
**缓存穿透:**所查询的数据,在数据库中没有,查询后也不在缓存中存,每次还是会去访问数据库,大量的请求压垮数据库。总的来说就是值数据库没有,缓存中也没有。
解决办法:
1.本来数据库没有值,将查询出来的空值放入缓存中,key—null,然后查询空值的时候就去访问缓存。
2.对查询的参数进行验证,比如说参数的格式不对就会拦截,减少后端的压力。
**缓存击穿 **:数据库中有数据,只是某个热点key在某个时间段内刚好过期了,此时有大量查询请求到达(查询是不加锁的)查询缓存没有,大量请求就会到达数据库,从而压垮数据库。
解决办法:
1.可以设置热点key的过期时间,合理设置。比如说秒杀活动8点开启,key的过期时间可以设置到10点。
2.查询缓存没有后,访问数据库时,可以加锁,第一个数据访问数据库后把值存入缓存之中,其他请求后面就会访问缓存,减少数据库压力。
缓存雪崩:大量的热点key过期或者redis服务故障,导致大量请求到达数据库,压垮数据库
解决办法:
1.随机设置key的过期时间,防止大量key同一时间过期。
2.把不同的热点key放在不同的redis服务,这样一个过期了还可以访问另一个服务(集群下实现)
3.设置较长的有效时间
4.在java中设置定时任务,去检测key是否过期。
10.Redis的内存淘汰策略
Redis有8中内存淘汰策略
1.volatile-lru:当内存不足以容纳新写入的数据时,从设置了过期时间的key中使用LRU(最近最近未使用)算法进行淘汰
**2.allkeys-lru:**当内存不足以容纳新写入的数据时,从所有的key中使用LRU(最近最近未使用)算法进行淘汰
**3.volatile-lfu:**当内存不足以容纳新写入的数据时,在过期的key中,使用LFU(最少访问)算法进行删除key
**4.allkeys-lfu:**当内存不足以容纳新写入的数据时,从所有的key中使用LFU(最少访问)算法进行删除key
**5.volatile-random:**当内存不足以容纳新写入的数据时,从设置了过期时间的key中,随机淘汰数据
6.allkeys-random:当内存不足以容纳新写入数据时,从所有key中随机淘汰数据。
**7.volatile-ttl:**当内存不足以容纳新写入数据时,从设置了过期时间的key中,,根据过期时间进行淘汰,越早过期的优先被淘汰;
8.noeviction:默认策略,当内存不足以容纳新写入数据时,新写入操作会报错。
11. Redis的跳跃表![preview](https://i-blog.csdnimg.cn/blog_migrate/77077088ff724e91e1866f2e2d4e2aeb.png)
跳表是zset的底层实现之一,
调表支持平均o(logn),最坏O(n)复杂度的节点查找,还可以通过顺序性批量操作节点。
调表实现由zskiplist和zskiplistNode两个结构组成,其中zskiplist用于保存跳表信息(表头,长度等)而node表示节点
跳表是在链表的基础上,增加多级索引提升查询效率
12. MySQL与Redis 如何保证双写一致性
1.先更新数据库,再更新缓存,如果Redis出现异常,会出现缓存读到脏数据的问题。
2.先删除缓存,再更新数据库,每次查询的时候将数据添加到缓存中。高并发下性能较低,而且仍然可能会有数据不一致的情况,比如线程a删除了redis缓存数据,正在更新mysql。此时线程b在查询的时候,就会吧MySQL中的旧数据又更新到redis里 。
3.延时双删,先删除redis缓存数据,再更新mysql,延迟几百毫秒再删出缓存数据,这样就算在更新MySQL时,其他线程吧旧数据又读进来,也会被删掉,从而保持数据一致。
13. 在生成 RDB期间,Redis 可以同时处理写请求么?
可以,redis提供两个指令生成rdb,分别是save和bgsave,如果是save,则会阻塞,因为是主线程执行的
如果是bgsave,是fork一个子进程来写入rdb文件,快照持久化交给子进程执行,主进程可以处理其他请求。
14. Redis底层,使用的什么协议?
RESP主要有实现简单、解析速度快、可读性好等优点。
15.redis底层数据结构
动态字符串sds
sds之所以叫做动态字符串,是因为他可以动态扩容的。扩容机制,底层是字符串数组来实现的,例如我们要给sds追加一个字符串amy,这里首先会申请新的内存 如果新的字符串小于1mb,则新空间为拓展后字符串的长度的二倍+1。这个+1是字符串结束的时候有个/0结束标志。每一个都有。
如果新字符串大于1mb,则新空间是拓展后的字符串的长度+1mb+1,这也成为内存预分配。
优点,获取字符串长度的时间复杂度为o1,因为数组里面会存他的长度的。
支持动态扩容,减少内存分配次数,是二进制安全的。
整数集合intset
intset可以看做是特殊的整数数组,1.redis会确保intset中的元素唯一,有序
2.具备类型升级机制,可以节省空间。这里的升级是元素可以保存为int16,int8,int32等类型数据。如果开始都是int8的数据,然后又插入了一个int16的数据,则会升级类型,先会吧最后一个数放进去,然后倒序这把所有值都放进去。正序会导致下标乱了。
3.底层采用二分查找来查询。
字典也叫哈希表
字典底层和hash表差不多,都是数组加链表的。他也是可以动态扩容和缩容的。每一次扩容或者缩容之后都会进行rehash。因为改变容量后,对应的下标就变了。
在数据很大的情况下,每次rehash会造成很大的开销,所以redis采用了渐进式rehash。过程大概如下,1.计算新的hash表的size。2.安装新的size申请空间,顺便创建一个新的空的hash数组,并赋值给这个数组。3.设置rehashid=0,表面开始rehash。4每次执行增删查改等操作时,都检查一下rehashid是否大于-1,如果是则将对应的entry链表rehash到新创建点数组中,id++,直到原来数组的值全部都rehash到新建的数组。5.讲新建的数组赋值给原数组 ,吧指针指到新的数组u。释放原来数组的空间。将rehashid设为-1,rehash结束。6.在rehash过程中,新增操作则直接写入新建的数组 查询和修改会在2个数组中依次查找并执行。
**ziplist压缩列表:**是特殊的双端列表,有一系列特殊编码的连续内存块,可以在任意一端进行压入,弹出操作。并且复杂度都为0(1)
压缩列表可以看作是一种连续内存空间的双向链表,
链表的节点之间不是通过指针连接的,而是记录上一节点和本节点长度来寻址,占用内存较低。
如果链表数据过多,导致链表过长,可能影响查询性能。
增或删叫大数据时有可能发生连续更新问题。连锁更新是指在压缩链表某处增加元素,导致后面有些元素的previous_entry_length属性由一字节扩展为5字节。新增和删除都可能导致连锁更新的发生。
quickList双端列表,他是一个双端列表,只不过链表的每个节点都是一个ziplist,我们可以通过配置来限制每个ziplist的大小,-1是不超过4kb,-2是不超过8k,一直到-5。
通过list-max-ziplist-size来限制,如果值是正数,则代表ziplist允许的entry的最大个数。
负数就是设置大小,默认值是-2.
特点:是一个节点位ziplist的双端链表,节点采用了ziplist,解决了传统链表的内存占用问题,控制ziplist的大小,解决了连续内存空间申请的效率问题。中间节点可以压缩,进一步节省了内存。
siplist跳表:元素按照升序排列存储,节点可能包含多个指针,每个指针的跨度不同。ele是元素,score是ele对应的分数,用来排序用的。
跳表是一个双向链表,每个节点都包含score和ele值,
节点按照score值排序,score值一样则按照ele字典排序
每个节点都可能包含多层指针,层数是1到32之间的随机数。
不同层指针到下一个节点的跨度不同,层级越高,跨度越大,
增删查改效率与红黑树基本一致,实现却更简单。
RedisObject:reids中任意类型的键和值都会被封装成一个RedisObject,也叫做redis对象,
16redis的网络模型
为了避免用户应用导致冲突甚至内核崩溃,用户应用与内核是分开的,进程的寻址空间会划分为两部分内核空间和用户空间。
内核空间可以执行所有命令,调用一些资源。用户空间只能调用用户可以使用的命令。而且不能直接调用系统资源,必须通过内核提供的接口来调用。
linux为了提高io效率,会在用户空间和内核空间都加入缓冲区。
在写数据时,要把用户缓存数据拷贝到内核缓冲区,然后写入设备,读数据时,要从设备读取数据到内核缓冲区,然后拷贝到用户缓冲区。
IO模型有5种:阻塞IO,非阻塞IO,IO多路复用,信号驱动IO,异步IO
阻塞IO(BIO):就是进程阻塞等待数据。比如说用户应用去读一个数据,内核没有数据,内核有两种处理方案,1.是返回没有数据,2.是等着,等到有数据为止。阻塞io就是不会返回失败,一种等有数据 ,直到我们将磁盘或者其他地方的数据读到缓冲区,然后将内核数据拷贝到用户空间,然后会返回ok,
典型的阻塞IO模型的例子为:data=socket.read();如果没有数据则会一直在等待。
**非阻塞IO(NIO)😗*当用户线程发起一个read操作后,并不需要等待,而是马上就得到了一个结果。如果结果是一个error时,它就知道数据还没有准备好,于是它可以再次发送read操作。一旦内核中的数据准备好了,并且又再次收到了用户线程的请求,那么它马上就将数据拷贝到了用户线程,然后返回。
在NIO中用户线程需要不断地询问内核数据是否就绪,也就说非阻塞IO不会交出CPU,而会一直占用CPU。
IO多路复用:
文件描述符FD,是一个从0开始递增的无符号整数,用来关联linux中的一个文件,linux中的任何东西都可以看作是文件,也包括socket。
IO多路复用是指利用单个线程来同时监听多个FD,并在某个FD可读,可写时得到通知,从而避免无效的等待,提高cpu利用效率。
监听FD,通知的方式有很多,常用的有select,poll,epoll。select和poll只会通知用户进程有FD可以操作,但不确定是那个,需要用户进程逐个遍历FD来确认。而epoll则会在通知用户进程FD就绪时,把就绪的FD写入用户空间。
select过程:用户空间创建一个数组,把监听的参数给select函数,用户空间会把这个数组拷贝到内核空间,由内核空间执行监听,遍历数组里面的FD,符合要求返回成功,不符合的返回失败。然后把找到的数组重新拷贝到用户空间。得到结果。
poll过程:创建数组,并给其中添加关注的FD信息,调用poll函数,将数组拷贝到内核空间转化为链表,内核遍历FD,判断是否就绪,数据就绪或者超时之后,拷贝到用户空间,返回就绪fd的数量,用户进程判断n是否大于0,大于0则遍历数组找到就绪的FD。这里把数组转化为链表,提高了监听查询的数量。
epoll过程:创建epoll实例,添加要监听的FD,关联callback,回调函数一旦触发了就把就绪的FD记录下来。epoll_wait函数,检测epoll树中是否有就绪的文件描述符。有则返回。
select存在3个问题:能监听的FD不超过1024个,每次select都要进行拷贝,每次都要遍历来寻找就绪的FD
poll的问题:遍历寻找FD,监听过多,性能下降。
epoll如何解决这些问题:
用红黑树保存监听的FD,理论上无线,而且效率很高,性能也不会下降。
callback自动记录就绪的FD,不需要遍历。
事件通知机制:
有两种LT和ET,LT是有数据可读的时候,会重复通知多次,直到数据处理完成,也是默认的。
ET是只通知一次。不管数据是否处理。
**信号驱动IO:**是于内核建立的SIGIO的信号关联并设置回调,当FD就绪时,会发出SIGIO信号通知用户,期间用户可以执行别的操作,无需等待。
当有大量IO操作的时候,信号较多,SIGIO处理函数不能及时处理可能导致信号队列溢出,而且内核空间与用户空间频繁的信号交流会减低性能,
异步IO(AIO):整个过程都是异步的,用户进程调用完异步API后可以去干其他事,内核数据等待数据并拷贝到用户空间之后才会递交信号,通知用户进程。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PHUYZpk6-1654094303168)(file:///C:\Users\15592026941\AppData\Roaming\Tencent\Users\2424835266\QQ\WinTemp\RichOle@ZZHBT{QGL{UHA2KA(G)]%{2.png)
LT和ET,LT是有数据可读的时候,会重复通知多次,直到数据处理完成,也是默认的。
ET是只通知一次。不管数据是否处理。
**信号驱动IO:**是于内核建立的SIGIO的信号关联并设置回调,当FD就绪时,会发出SIGIO信号通知用户,期间用户可以执行别的操作,无需等待。
当有大量IO操作的时候,信号较多,SIGIO处理函数不能及时处理可能导致信号队列溢出,而且内核空间与用户空间频繁的信号交流会减低性能,
异步IO(AIO):整个过程都是异步的,用户进程调用完异步API后可以去干其他事,内核数据等待数据并拷贝到用户空间之后才会递交信号,通知用户进程。
[外链图片转存中…(img-PHUYZpk6-1654094303168)]%{2.png)
IO是同步还是异步,主要看第二阶段是同步还是异步
17…rediskey过期了,为什么释放
在这样一种场景下,你先 set name jim 20px。然后修改了一下他的值,set name jim1 如何没有设置过期时间,redia会自动擦除这个key的过期时间。
还有一种情况就是,key的过期策略采用懒惰删除的话,key过期是不会马上删掉的,再下一次使用的时候,才会去检测这个key是否过期,如果过期就删除了。
在定时删除的时候,也是分批删除的,可能这次没轮到删除这个过期的key。
这些都会导致key过期了,没有被删除
17.redis实现消息队列
redis中有三种方式可以实现消息队列
1.使用list结构,基于list的数据结构来模拟消息队列。
list的数据结构是一个双向链表,我们可以在一端当消费者,另一端当生产者。可以用lpush,rpop或者rpush,lpop 但是这不是阻塞的,我们在消费消息队时候,没有消息应该阻塞等待,直到消息来了才继续消费消息。所以可以使用brpop和blpush来实现阻塞效果。
缺点,无法避免消息丢失,只能支持单个消费者。
2.基于pubsub的消息队列
pubsub是发布订阅的消息模型。消费者可以预订一个或者多个channel,生产者向对应的channel发送对应点消息后,所有订阅者都能收到相关的消息。
subscriem channel 订阅一个或者多个频道
publish channel msg 向一个频道发送多个消息
psubscribe pattern 订阅与pattern格式匹配的所有频道。
缺点,不支持持久化
3.基于stream的消息队列,redis5.0引入的,使用xadd添加队列,xread读队列
这种方式的消息是可回溯的,一个消息可以被多个消费者读取,可以阻塞读取,有消息漏读的风险。后面的参数是一次可以选择读多个或者1个,例如一次读1个,但是一次来了4个数据,他就只会默认读最新的一个,前面点就漏了。
18.redis实现分布式锁
1.实现方法
实现分布式锁需要实现获取锁和释放锁。
获取锁的话 ,是要保证互斥的,即保证一次只能有一个线程获得锁,可以使用 set lock thread1 nx ex 20,顺便给锁加上超时自动释放。避免线程阻塞锁不能正常释放,造成死锁的问题。
释放锁的话就是手动释放锁。也就是删除锁。
整个逻辑过程就是:首先尝试获取锁,如果获取成功了,则执行业务代码,然后释放锁,如果获取锁失败,则返回失败的消息。在释放锁的时候还要判断他要释放的锁是不是自己的,例如线程a拿到锁之后,阻塞了,redis超时释放锁,线程b进来执行完了之后,把线程a的锁释放了,线程a被唤醒后,继续执行,就存在两个线程执行一个东西了。这里是采用lua脚本来解决释放锁的原子性的。
还有就是使用redisson来实现分布式锁。
使用setnx实现的分布式锁是不可重入的,不可重试,超时释放的。
20.zset是怎么实现的
zset是有两种数据结构来实现的,分别是压缩表ziplist和跳表skiplist 来实现。
当节点的数量小于128并且节点的长度小于64时,会采用压缩表,其他情况下会采用跳表
跳表本质上是多级链表,采用空间换时间的思想来实现logn的世界复杂度。
他的最底层拥有所有的元素,其他层都是索引,由下往上分别是一级索引,二级索引等等。
redis中跳表一个节点最多可以达到64层,一个跳表中最多可以存储2的64次方个节点,节点通过hash表来关联对应的分数。
跳表的多级索引,采用随机的方式来构建,也就是说每次添加进来一个与阿奴,要不要对这个元素建立索引,建立几级索引,都要通过随机数的方式来决定。
为什么不用平衡二叉树用跳表
1.跳表更省内存,25%概率的随机层数,使得每个节点的指针数平均为1.33,二叉树树是2个
2.跳表遍历更友好,跳表找到大于目标元素后向后面遍历即可,平衡树需要中序遍历,实现也稍微复杂
3.跳表实现简单,维护简单,平衡树维护困难,需要旋转。