Redis重点知识整理 -奔波儿东
一、Redis简介:
1、什么是Redis?
- Redis(Remote Dictionary Server)是一个用c语言写的,开源的、高性能非关系型数据库。
NoSQL
①优点:
- 读写性能优异,单机redis读的速度能到110000次/s,写的速度能到81000次/s
- 支持数据的持久化和过期策略
- 丰富的数据类型。key为String类型,value常用类型:String、hash、List、set、zset等
- 支持事务,redis所有的操作是都是原子的,同时redis还支持对几个操作合并后的原子执行。
- 支持多种语言编程
- 支持主从复制,主机会自动将数据同步到从机,可以进行读写分离。
- 高可用、集群
②缺点:
- 数据库容量受物理内存的限制,不能作为海量数据的高性能读写。因此redis适合的场景主要局限在较小数据量的高性能操作和运算上。
- redis不具备自动容错和恢复功能,主从机的宕机都会导致前端部分读写请求的失败,需要等待机器重启或手动切换前端ip才能恢复。
- redis较难支持在线扩容,在集群达到上限时在线扩容变得很复杂。为避免这一问题,运维人员需要在系统上线时就必须确保足够的空间,这对资源造成浪费。
- 主机宕机,宕机前有部分数据未及时同步到从机,切换IP后会引入数据不一问题,降低系统可用性。
2、SQL与nosql
①关系型数据库的特点:
- 以表格的形式、基于行存储数据是一个二维的模式
- 存储是结构化的数据,数据存储有固定的模式(schema)数据需适应表结构
- 表与表之间存在关联
- 大部分关系型数据库都支持SQL的操作,支持复杂的关联查询
- 支持事务来提供严格或者实时的数据一致性。(ACID)
局限:
- 要实现扩容的话,只能垂直扩容,(磁盘限制数据的存储,就要扩磁盘容量,通过堆硬件的方式,不支持动态扩容缩容。水平扩容需要复杂的技术来实现,如分库分表)
- 表结构修改困难,因此存储数据格式也受到限制。
- 在高并发和大数据量的情况下,由于关系型数据库数据存储于磁盘中,加载时需要磁盘IO,访问速度较慢。
②非关系型数据库的特点:
- 存储非结构化的数据、如文本、图片、音频、视频
- 表与表之间没有关联,可扩展性强。
- 保证数据的最终一致性。遵循BASE理论。Basically Available(基本可用)、Soft-state(软状态)、Eventually Consistent(最终一致性)
- 支持海量数据的存储和高并发的高效读写。
- 支持分布式,能够对数据进行分片存储、扩容简单。
二、Redis在项目中的主要作用是是什么?怎么用的?(应用场景)
Redis作为一款性能优异的内存数据库,在互联网公司有着多种应用场景,主要有以下几个方面:
- 数据缓存
- 会话缓存
- 分布式session共享
- 当计数、统计、热数据
- 分布式锁
- 接口限流器
- 作为消息队列,生产一消费
- 作为订阅发布器,似聊天工具或推送
①应用场景:
总结1:
缓存
将热点数据放到内存中,设置内存的最大使用量以及淘汰策略来保证缓存的命中率。
会话缓存
可以使用 Redis 来统一存储多台应用服务器的会话信息。当应用服务器不再存储用户的会话信息,也就不再具有状态,一个用户可以请求任意一个应用服务器,从而更容易实现高可用性以及可伸缩性。
计数器:
可以对 String 进行自增自减运算,从而实现计数器功能。Redis 这种内存型数据库的读写性能非常高,很适合存储频繁读写的计数量。
全页缓存(FPC)
除基本的会话token之外,Redis还提供很简便的FPC平台。以Magento为例,Magento提供一个插件来使用Redis作为全页缓存后端。此外,对WordPress的用户来说,Pantheon有一个非常好的插件 wp-redis,这个插件能帮助你以最快速度加载你曾浏览过的页面。
查找表
例如 DNS 记录就很适合使用 Redis 进行存储。查找表和缓存类似,也是利用了 Redis 快速的查找特性。但是查找表的内容不能失效,而缓存的内容可以失效,因为缓存不作为可靠的数据来源。
消息队列(发布/订阅功能)
List 是一个双向链表,可以通过 lpush 和 rpop 写入和读取消息。不过最好使用 Kafka、RabbitMQ 等消息中间件。
分布式锁实现
在分布式场景下,无法使用单机环境下的锁来对多个节点上的进程进行同步。可以使用 Redis 自带的
SETNX
命令实现分布式锁(使用del key 命令释放锁),除此之外,还可以使用官方提供的 RedLock 分布式锁实现。其它
Set 可以实现交集、并集等操作,从而实现共同好友等功能。ZSet 可以实现有序性操作,从而实现排行榜等功能。
总结2:
String类型应用场景:
- 热点数据缓存(报表、明星出轨)、
- 对象缓存、
- 全页缓存、
- 数据共享分布式(分布式Session)
hash类型应用场景:(String可以做的hash都可以做。)
- 存储对象类型的数据(对象或一张表数据,比String节省更多的key空间,更便于集中管理)、
- 购物车(key:用户ID;field:商品id;value:商品数量 +1:hincr;-1:hdecr ; 删除:hdel; 全选:hgetall; 商品数:hlen)
List列表类型的应用场景:
- 用户消息时间线(timeline:微博的时间线)、
- 消息队列(lpush/rpop 或rpush/lpop)
set类型应用场景:抽奖(随机获取元素 spop)、点赞、签到、打卡
这条微博的id是t1001,用户ID是u2001
用like:t1001来维护t1001这条微博的所有的点赞用户
点赞:sadd like:t1001 u3001
取消点赞:srem like:t1001 u3001
是否点赞:sismember like:t1001 u3001
点赞的总人数: smembers like:t1001 u3001
点赞数:scard like:t1001
- 商品标签:(用tags:i5000来维护商品的所有标签)
sadd tags:i5000 画质清晰细腻
sadd tags:i5000 真彩清晰显示屏
sadd tags:i5000 性能一流
- 商品筛选
获取差集:sdiff set1 set2
获取交集:sinter set1 set2
获取并集: sunion set1 set2
- 用户关注、推荐模型(相互关注、我关注的人也关注了他、可能认识的人)
zset类型的应用场景
排行榜:ID为5000的新闻的点击数加1:zincrby hotnews:20210525 n5000
获取今天点击数最多的10条: zrevrange hotnews:20210525 10 withscores
三、Redis支持的数据类型
1、Redis五种常用数据类型:
- String字符串:字符串类型是 Redis 最基础的数据结构,首先键都是字符串类型,而且其他几种数据结构都是在字符串类型基础上构建的,我们常使用的 set key value 命令就是字符串。常用在缓存、计数、共享Session、限速等。
- Hash哈希:在Redis中,哈希类型是指值本身又是一个键值对结构,哈希可以用来存放用户信息,比如实现购物车。
- List列表(双向链表):列表(list)类型是用来存储多个有序的字符串。可以做简单的消息队列的功能。
- Set集合:集合(set)类型也是用来保存多个的字符串元素,但和列表类型不一 样的是,集合中不允许有重复元素,并且集合中的元素是无序的,不能通过索引下标获取元素。利用 Set 的交集、并集、差集等操作,可以计算共同喜好,全部的喜好,自己独有的喜好等功能。
- Sorted Set有序集合(跳表实现):Sorted Set 多了一个权重参数 Score,集合中的元素能够按 Score 进行排列。可以做排行榜应用,取 TOP N 操作。
2、redis不同对象的编码
- 字符串对象string: int,存储8个字节的长整型(long)、embstr编码的SDS(Simple Dynamic String)简单动态字符串(存储小于44个字节的字符串)、raw简单动态字符串(存储大于44个字节的字符串)
- 列表对象list:ziplist、linkedlist
- 哈希对象hash:ziplist、hashtable
- 集合对象set:intset、hsahtable
- 有序集合zset:ziplist、skiplist
四、Redis的持久化机制
①Redis持久化定义:
- Redis速度快,很大一部分原因是它所有的数据均存储在内存中。如果断电或者宕机都会导致内存中的数据丢失。为了实现重启后数据不丢失,Redis提供了两种持久化的方案,RDB快照(redis database)和 AOF(append only file)
②RDB:默认方案
- RDB:快照形式是直接把内存中的数据保存到一个dump.rdb的文件中,按照一定的时间将内存中的数据以快照的形式保存到硬盘中。 当Redis需要做持久化时,Redis会fork一个子进程,子进程将数据写到磁盘上一个临时RDB文件中。当子进程完成写临时文件后,将原来的RDB替换掉。
- 优点:
- 只有一个文件 dump.rdb,方便持久化。
- 容灾性好,一个文件可以保存到安全的磁盘。
- 性能最大化,fork 子进程来完成写操作,让主进程继续处理命令,所以是 IO 最大化。使用单独子进程来进行持久化,主进程不会进行任何 IO 操作,保证了 redis 的高性能
- 相对于数据集大时,比 AOF 的启动效率更高。
- 缺点:
- 数据安全性低。RDB 是间隔一段时间进行持久化,如果两个持久化之间 redis 发生故障,会发生数据丢失。所以这种方式更适合数据要求不严谨的时候。
③AOF:
- AOF:把所有的对Redis的服务器进行修改的命令都存到一个文件里,命令的集合。 使用AOF做持久化,每一个写命令都通过write函数追加到appendonly.aof中。aof的默认策略是每秒钟fsync一次,在这种配置下,就算发生故障停机,也最多丢失一秒钟的数据。 缺点是对于相同的数据集来说,AOF的文件体积通常要大于RDB文件的体积。根据所使用的fsync策略,AOF的速度可能会慢于RDB。
Redis默认是快照RDB的持久化方式。对于主从同步来说,主从刚刚连接的时候,进行全量同步(RDB);全同步结束后,进行增量同步(AOF)。
④RDB和AOF的优缺点:
- AOF文件比RDB更新频率高,优先使用AOF还原数据。
- AOF比RDB更安全,但也更大。
- RDB性能比AOF要好。
- 如果两个都配了,优先加载AOF
⑤如何选择合适的持久化方式
- 一般来说要较高的数据安全性,需要同时使用两种持久化,当redis重启后会优先加载AOF文件来恢复原始数据,因为AOF文件保存的数据集要不比RDB文件保存的数据集要完整。
- 如果可以忍受数分钟内的数据丢失,那么可以只是用RDB。
- 不建议只是用AOF,因为定时生成的RDB非常便于数据库备份,并且RDB恢复数据集的速度要比AOF要快。并且RDB还可以避免AOF程序的bug。
- 如果只是希望数据在服务器运行时候存在,可以不做持久化。
五、Redis的数据过期策略以及内存淘汰机制
①数据过期策略
Redis 中数据过期策略采用定期删除+惰性删除策略
结合起来,以及采用淘汰策略来兜底。
定时删除策略:
- 每个设置过期时间的key都创建一个定时器,到过期时间后立即删除。该策略可以立即清除过期的数据,对内存很友好。
- 缺点:每次都遍历内存中所有的数据,非常消耗 CPU 资源,影响缓存的响应时间和吞吐量;并且当 key 已过期,但是定时器还处于未唤起状态,这段时间内 key 仍然可以用。
惰性删除策略:
- 在获取 key 时,先判断 key 是否过期,如果过期则删除。节约CPU资源但是很耗内存。
- 缺点:如果这个 key 一直未被使用,那么它一直在内存中,其实它已经过期了,会浪费大量的空间。
- 定期删除
- 每隔一段时间,会扫描设置expires字典中一定数量的key,并清除其中已经过期的key。该策略是定时和惰性删除的折中方案。不在是每次扫描全部的 key 了,而是
随机抽取一部分 key 进行检查
,这样就降低了对 CPU 资源的损耗,惰性删除策略互补了为检查到的key,基本上满足了所有要求。
- 每隔一段时间,会扫描设置expires字典中一定数量的key,并清除其中已经过期的key。该策略是定时和惰性删除的折中方案。不在是每次扫描全部的 key 了,而是
定期删除+惰性删除都没有删除过期的key?
当内存不够用的时候,就会走到redis的内存淘汰机制。
内存淘汰机制就保证了在redis的内存占用过多的时候,去进行内存淘汰,也就是删除一部分key,保证redis的内存占用率不会过高。
在redis.conf中有一行配置
maxmemory-policy volatile-lru
②redis提供6种数据淘汰策略:
volatile-lru:
从已设置过期时间
的数据集(server.db[i].expires)中挑选最近最少使用的数据淘汰
volatile-ttl
:从已设置过期时间
的数据集(server.db[i].expires)中挑选将要过期的数据淘汰
volatile-random
:从已设置过期时间的数据集(server.db[i].expires)中随机移除key
allkeys-lru
:当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的key(这个是最常用的)
allkeys-random
:从数据集(server.db[i].dict)中任意选择数据淘汰
no-eviction
:当内存不足以容纳新写入数据时,新写入操作会报错,无法写入新数据,一般不采用
4.0版本后增加以下两种:
- volatile-lfu:从已设置过期时间的数据集(server.db[i].expires)中挑选最不经常使用的数据淘汰
- allkeys-lfu:当内存不足以容纳新写入数据时,在键空间中,移除最不经常使用的key
③Redis的LRU过期策略的具体实现
Redis每次按key获取一个值的时候,都会更新value中的lru字段为当前秒级别的时间戳。
Redis初始的实现算法很简单,随机从dict中取出五个key,淘汰一个lru字段值最小的。
在3.0的时候,又改进了一版算法:
- 首先第一次随机选取的key都会放入一个pool中(pool的大小为16),pool中的key是按lru大小顺序排列的。
- 接下来每次随机选取的key lru值必须小于pool中最小的lru才会继续放入,直到将pool放满。
- 放满之后,每次如果有新的key需要放入,需要将pool中lru最大的一个key取出。
- 淘汰的时候,直接从pool中选取一个lru最小的值然后将其淘汰。
六、如何解决Redis缓存雪崩,缓存穿透、缓存预热、缓存更新、缓存降级
1、Redis缓存雪崩
定义:redis缓存中大量的key同时失效,(缓存服务器宕机)此时又刚好有大量的请求打进来,直接打到数据库层,造成数据库阻塞甚至宕机。 场景: 把所有存入redis的所有数据设置相同过期的时间,过期时间失效后,就会大量请求数据库。 如何解决? 1、在缓存的时候我们给过期时间设置一个随机数,但是也要根据业务场景需求来设置 2、事发前:实现redis的高可用、主从架构+sentinel 或者 redis cluster ,避免全盘崩溃; 3、事发后:万一redis真的挂了,可以设置本地缓存ehcache+限流hystrix,避免数据库被打死; 4、事发后:redis持久化,重启后自动从磁盘上加载数据,快速恢复缓存数据
2、Redis缓存穿透
定义
:指查询一个缓存和数据库都不存在的数据,导致尽管数据不存在但是每次都会到数据库查询。在访问量大时可能DB就会挂掉。如果有人利用不存在的key频繁攻击,则这就形成了漏洞。
场景
:比如我们表的数据的id是从1开始的正数,如果在这里有黑客攻击我们的系统,会设置一些负数的id到数据库查询数据,查询出来返回的数据为null,在这里,由于缓存不命中,并且处于容错考虑,从数据库查询的数据为null就不写到redis,这将导致每次查询数据都会到数据库查询,失去了缓存的意义。这样数据库迟早也会挂掉
如何解决缓存穿透?
1、由于请求的参数是不合法(-1) 每次请求都是不存在的数据,于是我们可以使用布隆过滤器(BloomFilter) 或者 压缩filter提前拦截,不合法就不能访问数据库。
2、当我们从数据库查询出来的数据为null时,也把他设置到缓存redis中,下次请求的时候就到redis中查询了,在这里后台可以判断,如果为null,那么设置一个较短的过期时间,到期自动就失效,否则就是按正常操作。
3、布隆过滤器
- 什么是布隆过滤器?
- 布隆过滤器(Bloom Filter)是1970年由布隆提出的。它实际上是一个很长的
二进制向量
和一系列随机映射函数
。布隆过滤器可以用于检索一个元素是否在一个集合中。它的优点是空间效率和查询时间都比一般的算法要好的多,缺点是有一定的误识别率和删除困难。
- 布隆过滤器(Bloom Filter)是1970年由布隆提出的。它实际上是一个很长的
- 设计概念
- 如果判断一个元素是否在集合中,一般思路可以是将所有的元素保存起来,然后通过比较确定。但是随着数据量的增加我们需要的存储空间也越来越大,检索速度也越来越慢。此时可以使用到hash表的数据结构。可以通过hash函数将一个元素映射到位阵列(bit array)中的一个点。这样我们只要判断这个点是不是1就可以知道集合中存不存在某元素。
- 使用hash算法,免不了会有冲突,为了解决冲突就只能通过增加散列表的空间大小,导致空间利用率不高,此时还有一种方式就是增加hash函数的数量。通过多个hash函数,判断元素是否存在,如果有一个判断为0(即不存在),则判断此元素在集合中是不存在的。但是避免不了有误判的可能。
- 核心思想
- 多个hash函数,增加随机性,减少hash碰撞的概率。
- 扩大数组范围,使hash值均匀分布,减少hash碰撞的概率。
- 优点:
- 相比其他的数据结构,布隆过滤器的空间及时间方面的优势是巨大的。存储空间和插入、查询时间都是常数。
- hash函数之间是没有关系的,方便由硬件并行实现。
- 布隆过滤器本身不需要存储元素本身,数据安全性较高。
缺点:
- 存在一定的误判率,不能100%判断元素是否真的存在。
- 不能删除。(因为不知道某一个位置上代表的是否还有其他的元素)
BloomFilter的应用
- K-V系统快速判断某个key是否存在 典型的例子有Hbase,Hbase的每个Region中都包含一个BloomFilter,用于在查询时快速判断某个key在该region中是否存在,如果不存在,直接返回,节省掉后续的查询。
- 黑名单 比如邮件黑名单过滤器,判断邮件地址是否在黑名单中
- 排序(仅限于BitSet) 仔细想想,其实BitSet在set(int value)的时候,“顺便”把value也给排序了。
- 网络爬虫 判断某个URL是否已经被爬取过
- 缓存穿透
- 集合元素重复的判断
4、击穿:
击穿与雪崩的不同在于缓存key失效的量级上。击穿是对于单个key值的缓存失效过期,雪崩则是大面积key同时失效。
解决办法:
- 若缓存数据基本不会发生更新,则可尝试将热点数据设置为永不过期。
- 若缓存的数据更新不频繁,且缓存刷新的整个流程耗时较少的情况下,则可以采用基于 Redis、zookeeper 等分布式中间件的分布式互斥锁,或者本地互斥锁以保证仅少量的请求能请求数据库并重新构建缓存,其余线程则在锁释放后能访问到新缓存。
- 若缓存的数据更新频繁或者在缓存刷新的流程耗时较长的情况下,可以利用定时线程在缓存过期前主动地重新构建缓存或者延后缓存的过期时间,以保证所有的请求能一直访问到对应的缓存。
5、缓存预热:
缓存预热是在系统上线后,将相关缓存数据直接加载到缓存系统中。这样可以避免用户请求时先查数据库,再更新缓存的问题。
方案:
- 写缓存刷新页面,上线时候手动更新缓存
- 数据量不大的话可以在项目启动时自动加载
- 定时刷新缓存(每天凌晨去做)
6、缓存降级
当访问量剧增、服务出现问题(响应时间长或不响应)或非核心服务影响到核心流程的性能,仍然需要保证服务是可用的,即使有损服务。系统可以根据一些关键数据进行自动降级,也可以配置开关实现人工降级。
降级的目的:保证核心服务的可用性,即使是有损的,而且有些服务是不能降级的(购物车、支付等)
以参考日志级别设置预案:
(1)一般:比如有些服务偶尔因为网络抖动或者服务正在上线而超时,可以自动降级;
(2)警告:有些服务在一段时间内成功率有波动(如在95~100%之间),可以自动降级或人工降级,并发送告警;
(3)错误:比如可用率低于90%,或者数据库连接池被打爆了,或者访问量突然猛增到系统能承受的最大阀值,此时可以根据情况自动降级或者人工降级;
(4)严重错误:比如因为特殊原因数据错误了,此时需要紧急人工降级。
服务降级的目的,是为了防止Redis服务故障,导致数据库跟着一起发生雪崩问题。因此,对于不重要的缓存数据,可以采取服务降级策略,例如一个比较常见的做法就是,Redis出现问题,不去数据库查询,而是直接返回默认值给用户。
七、zset跳表的数据结构
跳跃表(skiplist)是一种有序数据链表结构, 它通过在每个节点中维持多个指向其他节点的指针, 从而达到快速访问节点的目的。查询平均性能为O(logN),最坏的情况会出现O(N)情况,而redis中的zset在数据较多的时候底层就是采用跳跃表去实现的,元素较少的时候会进行小对象压缩采用压缩列表实现。
从上述图我们可以看出跳跃表有以下几个特点:
- 跳跃表的每个节点都有多层构成。
- 跳跃表存在一个头结点,该头结点有64层结构,每层都包含指向下个节点的指针,指向本层下个节点中间所跨越的节点个数为跨度(span)。
- 除头结点以外,层高最高的节点为该跳跃表的level,图中的跳跃表level为3。 每层都是一个有序链表。
- 最底层的有序链表包含所有的节点数,也即是整个跳跃表的长度。 跳跃表每个节点都维护了多个指向其他节点的指针,所以在进行查询、更新、删除等操作的时候不需要进行整条链表的遍历,可以通过维护的指针过滤掉中间的很多节点,从而达到很快速的访问效果,一般情况来说跳跃表的性能能与平衡树相媲美的,而且跳跃表实现较为简单,所以这也是redis为什么采用跳跃表来作为zset底层的数据结构实现。
查找过程
跳跃表的查询,跳跃表有多层的情况下查询复杂度为O(logN),如果跳跃表就一层那么查询复杂度会上升为O(N),接下来我们就用图1的实例来模拟下查询score为70的节点的具体查询过程。
如图所示我们需要找到score为70的节点,查找首先从header开始,因为level为3我们先从L2开始往后开始遍历,查找到第一个节点,发现score比70小,继续往后遍历查找到第五个节点,发现score比70大,于是从当前节点往下一层进行查找,查找到节点3,以此类推,最终查询到score为70的节点。
插入以及更新过程
插入过程
:跳跃表插入节点的时候,首先需要通过score找到自己的位置,也就是需要先走一步查找过程,找到新节点所处的位置的时候就创建一个新节点,并对新节点分配一个层数(这里层数的分配redis采用的是random随机机制,分配层数从1开始,每次晋升为上一层的概率为0.25),层数分配完了之后将前后指针进行赋值将新节点与旧节点串起来,如果层数大于当前的level还需要进行level的更新操作。
更新过程
:更新过程会稍微复杂一些,更新其实就是插入,只不过插入的时候发现value已经存在了,只是需要调整一下score值,如果更新的score值不会带来位置上的改变,那么直接更新score就行不需要进行调整位置,但是如果新score会导致排序改变,那么就需要调整位置了,redis采用的方式比较直接就是先删除这个元素然后再插入这个元素即可,前后需要两次路径搜索。
补充问题:
Redis使用跳表不用B+树的原因?
Redis使用跳表不用B+树的原因是:redis是内存数据库,而B+树纯粹是为了mysql这种IO数据库准备的。B+树的每个节点的数量都是一个mysql分区页的大小。
九、Redis集群方案
① 主从模式
特点:
- 主数据库可以进行读写操作,当读写操作导致数据变化时会自动将数据同步给从数据库
- 从数据库一般都是只读的,并且接收主数据库同步过来的数据
- 一个master可以拥有多个slave,但是一个slave只能对应一个master
- slave挂了不影响其他slave的读和master的读和写,重新启动后会将数据从master同步过来
- master挂了以后,不影响slave的读,但redis不再提供写服务,master重启后redis将重新对外提供写服务
- master挂了以后,不会在slave节点中重新选一个master
工作机制
- 当slave启动后,主动向master发送SYNC命令,master接收到命令后在后台保存快照和缓存保存快照这段时间的操作,然后将保存的快照文件和缓存命令发送到slave。slave接收到快照文件和命令后加载到快照和缓存的执行命令。
- 在复制初始化后,master每次接收到写命令都会同步发送给slave,保证主从数据一致性。
缺点:
- master节点在主从模式中是唯一的,如果master挂掉,则redis无法对外提供写服务。
redis-cli -h 182.208.x.x -a 密码
②哨兵模式(Sentinel)
哨兵模式
- 集群监控:负责监控master和slave进程是否正常工作。
- 消息通知:如果某个redis实例出现故障,哨兵会负责发送消息作为警报通知给管理员
- 故障转移:如果master节点挂掉,会自动转移到slave节点上。
- 配置中心:如果故障转移发生了,通知client客户端新的master地址。
- 主从模式不具备高可用性,当master挂掉之后,redis不能再对外提供写操作。
哨兵模式特点:
- sentinel模式是建立在主从模式的基础上,如果只有一个Redis节点,sentinel就没有任何意义
- 当master挂了以后,sentinel会在slave中选择一个做为master,并修改它们的配置文件,其他slave的配置文件也会被修改,比如slaveof属性会指向新的master
- 当master重新启动后,它将不再是master而是做为slave接收新的master的同步数据
- sentinel因为也是一个进程有挂掉的可能,所以sentinel也会启动多个形成一个sentinel集群
- 多sentinel配置的时候,sentinel之间也会自动监控
- 当主从模式配置密码时,sentinel也会同步将配置信息修改到配置文件中,不需要担心
- 一个sentinel或sentinel集群可以管理多个主从Redis,多个sentinel也可以监控同一个redis
- sentinel最好不要和Redis部署在同一台机器,不然Redis的服务器挂了以后,sentinel也挂了
工作机制
- 每个sentinel以每秒钟一次的频率向它所知的master,slave以及其他sentinel实例发送一个 PING 命令
- 如果一个实例距离最后一次有效回复 PING 命令的时间超过 down-after-milliseconds 选项所指定的值, 则这个实例会被sentinel标记为主观下线。
- 如果一个master被标记为主观下线,则正在监视这个master的所有sentinel要以每秒一次的频率确认master的确进入了主观下线状态
- 当有足够数量的sentinel(大于等于配置文件指定的值)在指定的时间范围内确认master的确进入了主观下线状态, 则master会被标记为客观下线
- 在一般情况下, 每个sentinel会以每 10 秒一次的频率向它已知的所有master,slave发送 INFO 命令
- 当master被sentinel标记为客观下线时,sentinel向下线的master的所有slave发送 INFO 命令的频率会从 10 秒一次改为 1 秒一次
- 若没有足够数量的sentinel同意master已经下线,master的客观下线状态就会被移除;
- 若master重新向sentinel的 PING 命令返回有效回复,master的主观下线状态就会被移除
哨兵用于实现 redis 集群的高可用,本身也是分布式的,作为一个哨兵集群去运行,互相协同工作。
故障转移时,判断一个 master node 是否宕机了,需要大部分的哨兵都同意才行,涉及到了分布式选举的问题。
即使部分哨兵节点挂掉了,哨兵集群还是能正常工作的,因为如果一个作为高可用机制重要组成部分的故障转移系统本身是单点的,那就很坑爹了。
哨兵的核心知识
- 哨兵至少需要 3 个实例,来保证自己的健壮性。
- 哨兵 + redis 主从的部署架构,是不保证数据零丢失的,只能保证 redis 集群的高可用性。
- 对于哨兵 + redis 主从这种复杂的部署架构,尽量在测试环境和生产环境,都进行充足的测试和演练。
③官方Redis Cluster(服务端路由查询)
- sentinel 模式可以满足一般生产的需求,具备高可用性。但是当数据过大后一台服务器存放不下的情况时,主从模式和哨兵模式都不能满足需求。此时需要对存储的数据进行分片,将数据存储到多个实例中。cluster模式的出现就是为解决Redis容量有限的问题,将数据根据一定的规则分配到多台机器上。
cluster集群的特点
- 多个redis节点网络互联,数据共享
- 所有的节点都是一主一从(也可以是一主多从),其中从节点不提供服务,仅作为备用
- 不支持同时处理多个key(如MSET/MGET),因为redis需要把key均匀分布在各个节点上, 并发量很高的情况下同时创建key-value会降低性能并导致不可预测的行为
- 支持在线增加、删除节点
- 客户端可以连接任何一个主节点进行读写
方案说明
通过哈希的方式,将数据分片,每个节点均分存储一定哈希槽(哈希值)区间的数据,默认分配了16384 个槽位
每份数据分片会存储在多个互为主从的多节点上
数据写入先写主节点,再同步到从节点(支持配置为阻塞同步)
同一分片多个节点间的数据不保持一致性
读取数据时,当客户端操作的key没有分配在该节点上时,redis会返回转向指令,指向正确的节点
扩容时时需要需要把旧节点的数据迁移一部分到新节点
在 redis cluster 架构下,每个 redis 要放开两个端口号,比如一个是 6379,另外一个就是 加1w 的端口号,比如 16379。16379 端口号是用来进行节点间通信的,也就是 cluster bus 的东西,cluster bus 的通信,用来进行故障检测、配置更新、故障转移授权。cluster bus 用了另外一种二进制的协议,gossip 协议,用于节点间进行高效的数据交换,占用更少的网络带宽和处理时间。
节点间的内部通信机制
基本通信原理
集群元数据的维护有两种方式:集中式、Gossip 协议。redis cluster 节点间采用 gossip 协议进行通信。
分布式寻址算法
hash 算法(大量缓存重建) 一致性 hash 算法(自动缓存迁移)+ 虚拟节点(自动负载均衡) redis cluster 的 hash slot 算法 优点
无中心架构,支持动态扩容,对业务透明 具备Sentinel的监控和自动Failover(故障转移)能力 客户端不需要连接集群所有节点,连接集群中任何一个可用节点即可 高性能,客户端直连redis服务,免去了proxy代理的损耗 缺点
运维也很复杂,数据迁移需要人工干预 只能使用0号数据库 不支持批量操作(pipeline管道操作) 分布式逻辑和存储模块耦合等
基于客户端分配
简介
Redis Sharding是Redis Cluster出来之前,业界普遍使用的多Redis实例集群方法。其主要思想是采用哈希算法将Redis数据的key进行散列,通过hash函数,特定的key会映射到特定的Redis节点上。Java redis客户端驱动jedis,支持Redis Sharding功能,即ShardedJedis以及结合缓存池的ShardedJedisPool
优点
- 优势在于非常简单,服务端的Redis实例彼此独立,相互无关联,每个Redis实例像单服务器一样运行,非常容易线性扩展,系统的灵活性很强
缺点
- 由于sharding处理放到客户端,规模进一步扩大时给运维带来挑战。
- 客户端sharding不支持动态增删节点。服务端Redis实例群拓扑结构有变化时,每个客户端都需要更新调整。连接不能共享,当应用规模增大时,资源浪费制约优化
基于代理服务器分片
简介
客户端发送请求到一个代理组件,代理解析客户端的数据,并将请求转发至正确的节点,最后将结果回复给客户端
特征
透明接入,业务程序不用关心后端Redis实例,切换成本低 Proxy 的逻辑和存储的逻辑是隔离的 代理层多了一次转发,性能有所损耗 业界开源方案
Twtter开源的Twemproxy 豌豆荚开源的Codis
9.Redis集群容错机制是什么样的?
高可用首先需要解决集群部分失败的场景:当集群内少量节点出现故障时通过自动故障转移保证集群可以正常对外提供服务。主要针对Redis Cluster来分析:
详细的知识见:https://blog.csdn.net/xiaofeng10330111/article/details/90384502
故障发现
redis的各个节点通过ping/pong进行消息通信,不需要Sentinel,转播槽的信息和节点状态信息,故障发现也是通过这个动作发现的,跟Sentinel一样,有主观下线和客观下线。
- 主观下线:某个节点认为另一个节点不可用,“偏见”,但不是最终的故障判断。 客观下线:标志着一个节点真正的下线,即半数以上持有槽的主节点都标记某节点主观下线,如果是持有槽的主节点故障,需要为该节点进行故障转移。
- 尝试客观下线:通知集群内所有节点标记故障节点为客观下线,通过故障节点的从节点触发故障转移流程。
故障恢复
故障节点变为客观下线后,如果下线节点是持有槽的主节点则需要在它的从节点中选出一个替换它,从而保证集群的高可用。下线主节点的所有从节点承担故障恢复的义务,当从节点通过内部定时任务发现自身复制的主节 点进入客观下线时,将会触发故障恢复流程:资格检查-->准备选举时间-->发起选举-->选举投票-->替换主节点。
10.Redis集群中某个master节点挂了,后面流程怎么判断?
具体还是9上的回答,更为具体的见:https://blog.csdn.net/xiaofeng10330111/article/details/90384502
可以从以下三个方面分析:
redis集群如何判断一个主节点挂了:
- 集群中的每个节点都会顶起地向集群中的其他节点发送PING消息
- 默认每隔1s中发送一次PING
- 默认随机选择5个集群中的其他主节点,规则为最长时间未PING以及PONG时间超过timeout/2
- 如果发送PING的主节点1,在timeout时间内没收到主节点2的PONG消息,那么主节点1会将主节点2的状态标记为pfail
- 主节点1在后续发送PING消息时,会带上主节点2的pfail状态,收到这个消息的主节点会在clusterNode里下线报告fail_reports中将主节点2标记为pfail
- 当某个主节点被标记为pfail的个数大于集群总主节点个数的一半时,会被标记为fail, 并向整个集群广播一条PONG消息,说明该主节点下线。
- 其他主节点收到广播消息后,将该主节点状态标记为fail,
- 集群进入故障转移阶段
故障转移阶段流程:
当一个从节点发现自己正在复制的主节点进入已下线状态时,从节点将开始对下线主节点进行故障转移 复制下线主节点的所有从节点里面,会有一个从节点被选为新的主节点新的主节点从撤销所有已下线主节点的槽位指派,并将这些槽位指给自己新的主节点会向集群广播一条PONG消息,告知其他节点自己已由从节点转为主节点,并接管相应的槽位故障转移完成
选举新的主节点
集群的纪元时一个自增的计数器,初始值为0 当集群里的某个节点开始一次故障转移操作时,集群的纪元会加一 对于每个纪元,集群里每个复制处理槽位的主节点都有一次投票机会,而第一次向主节点要求投票的从节点将获得主节点的投票 什么时候开始投票? 当从节点发现自己正在复制的主节点进入已下线状态,从节点会向集群广播一条CLUSTERMSG_TYPE_FAILOVER_AUTH_REQUEST消息,收到该消息的主节点可以开始投票 主节点投票后返回CLUSTERMSG_TYPE_FAILOVER_AUTH_ACK消息 当某个从节点的票数大于投票总节点数的一半时,被选为新的主节点 若每个获得足够的票数,进入下一轮投票
11.Redis的缓存优化方向有哪些?你们怎么理解的?对热点键的注意事项设计什么?
缓存的主要优势和成本:
缓存收益:加速读写、降低后端存储负载; 缓存成本:缓存和存储数据不一致性、代码维护成本、运维成本;
优化方向:
- Redis 的数据过期策略:采用定期删除+惰性删除策略结合起来,以及采用淘汰策略来兜底。
- 缓存粒度控制:全部属性更好or部分重要属性更好?
- 缓存穿透优化:缓存空对象 + 布隆过滤器拦截(通过很小的内存来实现对数据的过滤。)
- 缓存雪崩优化:保证缓存高可用性,例如 Redis Cluster、Redis Sentinel、VIP;* 依赖隔离组件为后端限流;提前演练,例如压力测试。
- 无底洞问题优化:命令本身优化,例如慢查询 keys、hgetall bigkey;减少网络通信次数;降低接入成本,例如客户端使用长连接/连接池、NIO 等 。
- 热点key优化:避免 bigkey;热键不要用 hash_tag,因为 hash_tag 会落到一个节点上;如果真有热点 key 而且业务对一致性要求不高时,可以用本地缓存 + MQ 解决。
- 热点key重建优化:互斥锁(mutex key),查询数据源 -> 重建缓存 这个过程加互斥锁;永不过期,缓存层面不设置过期时间(没有用 expire),功能层面为每个 value 添加逻辑过期时间,但发现超过逻辑过期时间后,会使用单独的线程去构建缓存。
- 对热点键的注意事项,如上热点key优化和热点key重建优化。
十一、补充问题:
1、单线程的Redis为什么快?
- 纯内存操作
- 单线程操作,避免了频繁的上下文切换
- 合理高效的数据结构
- 采用了非阻塞I/O多路复用机制
2、Redis和memcached的区别?
- 存储方式上:memcache会把数据全部存在内存之中,断电后会挂掉,数据不能超过内存大小。redis有部分数据存在硬盘上,这样能保证数据的持久性。
- 数据支持类型上:memcache对数据类型的支持简单,只支持简单的key-value,,而redis支持常见的五种数据类型。(String、List、set、zset、hash)
- 用底层模型不同:它们之间底层实现方式以及与客户端之间通信的应用协议不一样。redis直接自己构建了VM机制,因为一般的系统调用系统函数的话,会浪费一定的时间去移动和请求。
- value的大小:redis可以达到512MB,而memcache只有1MB。
- redis支持数据的备份,即master-slave模式的数据备份。
3、redis的同步机制
- redis可以使用主从同步,从从同步第一次同步时,主节点做一次bgsave,并同时将后续的修改操作记录到内存缓存中,待完成后将RDB文件全量同步到复制节点,复制节点接收完成后将RDB镜像加载到内存。加载完成后,再通知主节点将期间修改的操作记录同步到复制节点进行重放完成同步过程。
4、Redis和DB数据一致性处理
对于读操作流程:先到redis缓存中查询数据,如果为null,那么再到数据库查询出来再设置到redis中去,最后将数据返回给请求。 定义: 如果只是简单查询,缓存数据和数据库数据没有什么问题,当我们更新的时候就可能导致数据库数据和缓存数据不一致了。 数据库库存为 999 缓存数据为1000 这就是更新导致的情况。
解决方案:
- 1、比如操作菜单的时候,当我们增加 、删除、修改菜单时,操作成功之后就应该立刻根据菜单的key从redis缓存中把数据给删除,第二次查询 的时候肯定为null,从数据库查询再设置到redis中。这是马上见效情况
- 2、不是马上见效的情况,就是设置过期时间来确定,比如我们商城中web页面根据店铺搜索出来的数据有最新的4张照片,当我们在商家后台添加一个商品时,就应该显示在最新添加的照片,此时就不能按照删除key来操作redis了,因为多个商家添加多个商品,就失去了缓存的意义,那么会根据用户需求来设置过期时间,这里的redis缓存就可能和数据库不一致,需要过期时间来控制数据。因为缓存时间到了,就会被删除,再到数据库查询设置到redis中去 。
5、Redis 常见性能问题和解决方案?
Master 最好不要做任何持久化工作,如 RDB 内存快照和 AOF 日志文件
如果数据比较重要,某个 Slave 开启 AOF 备份数据,策略设置为每秒同步一次
为了主从复制的速度和连接的稳定性, Master 和 Slave 最好在同一个局域网内
尽量避免在压力很大的主库上增加从库
主从复制不要用图状结构,用单向链表结构更为稳定,即:Master <- Slave1 <- Slave2 <-Slave3…
6、知道什么是热key问题?热key问题怎么解决?
- 热key:突然有十几万请求去访问redis中的某个特定key。那么这样造成流量过于集中,达到物理网卡上限,从而导致这台redis服务器宕机。接下来就是这个key的请求直接打到数据库服务器上导致整个服务不可用。(击穿)
发现热key:
- 根据经验评估热key。(某些商品在做秒杀,那么此商品的key可判断为热key,但不是所有的业务都能判断出来热key)
- 客户端收集。(在操作redis之前加代码进行统计,但是会造成代码的侵入)
- 代理层做收集。
- redis自带的命令
- monitor命令,该命令可以实时抓取出redis服务器接收到的命令,然后写代码统计出热key是啥。当然,也有现成的分析工具可以给你使用,比如redis-faina。但是该命令在高并发的条件下,有内存增暴增的隐患,还会降低redis的性能。
解决方案:
- 提前将热key打散到不同的服务器上降低压力。
- 加入二级缓存,提前加载热key数据到内存中,如果redis宕机走查内存。(可使用HashMap或ehcache,在发现热key后,把热key加载到系统的JVM中,针对此热key请求直接从JVM中取,不走redis)
7、假如 Redis 里面有 1 亿个 key,其中有 10w 个 key 是以某个固定的已知的前缀开头的,如果将它们全部找出来?
答:使用 keys 指令可以扫出指定模式的 key 列表。
对方接着追问:如果这个 redis 正在给线上的业务提供服务,那使用 keys 指令会有什么问题?
这个时候你要回答 redis 关键的一个特性:redis 的单线程的。keys 指令会导致线程阻塞一段时间,线上服务会停顿,直到指令执行完毕,服务才能恢复。这个时候可以使用 scan 指令,scan 指令可以无阻塞的提取出指定模式的 key 列表,但是会有一定的重复概率,在客户端做一次去重就可以了,但是整体所花费的时间会比直接用 keys 指令长。
8、那你使用过Redis分布式锁么,它是什么回事?
先拿setnx来争抢锁,抢到之后,再用expire给锁加一个过期时间防止锁忘记了释放。
这时候对方会告诉你说你回答得不错,然后接着问如果在setnx之后执行expire之前进程意外crash或者要重启维护了,那会怎么样? 这时候你要给予惊讶的反馈:唉,是喔,这个锁就永远得不到释放了。紧接着你需要抓一抓自己得脑袋,故作思考片刻,好像接下来的结果是你主动思考出来的,然后回答:我记得set指令有非常复杂的参数,这个应该是可以同时把setnx和expire合成一条指令来用的!
十二、Redis事务
①Redis事务:
Redis事务功能是通过MULTI、EXEC、DISCARD和WATCH
四个原语实现的
Redis会将一个事务中的所有命令序列化,然后按顺序执行。其他客户端提交的命令不会被插入到事务执行命令的序列中。(一次性、顺序性、排他性)
1、redis 不支持回滚 Redis 在事务失败时不进行回滚,而是继续执行余下的命令
, 所以 Redis 的内部可以保持简单且快速。
2、如果在一个事务中的命令出现错误,那么所有的命令都不会执行;
3、如果在一个事务中出现运行错误,那么正确的命令会被执行。
(1)MULTI命令用于开启一个事务,它总是返回OK。MULTI执行之后,客户端可以继续向服务器发送任意多条命令,这些命令不会立即被执行,而是被放到一个队列中,当EXEC命令被调用时,所有队列中的命令才会被执行。
(2)EXEC:执行所有事务块内的命令。返回事务块内所有命令的返回值,按命令执行的先后顺序排列。当操作被打断时,返回空值 nil 。
(3)通过调用DISCARD,客户端可以清空事务队列,并放弃执行事务, 并且客户端会从事务状态中退出。
(4)WATCH 命令可以为 Redis 事务提供 check-and-set (CAS)行为。可以监控一个或多个键,一旦其中有一个键被修改(或删除),之后的事务就不会执行,监控一直持续到EXEC命令。
②Redis事务支持隔离性吗
Redis 是单进程程序,并且它保证在执行事务时,不会对事务进行中断,事务可以运行直到执行完所有事务队列中的命令为止。因此,Redis 的事务是总是带有隔离性的。
Redis秒杀、事务、单线程模型?内存淘汰策略?超时时间?
十三、线程模型
Redis线程模型
Redis基于Reactor模式开发了网络事件处理器,这个处理器被称为文件事件处理器(file event handler)。它的组成结构为4部分:多个套接字、IO多路复用程序、文件事件分派器、事件处理器。因为文件事件分派器队列的消费是单线程的,所以Redis才叫单线程模型。
- 文件事件处理器使用 I/O 多路复用(multiplexing)程序来同时监听多个套接字, 并根据套接字目前执行的任务来为套接字关联不同的事件处理器。
- 当被监听的套接字准备好执行连接应答(accept)、读取(read)、写入(write)、关闭(close)等操作时, 与操作相对应的文件事件就会产生, 这时文件事件处理器就会调用套接字之前关联好的事件处理器来处理这些事件。
虽然文件事件处理器以单线程方式运行, 但通过使用 I/O 多路复用程序来监听多个套接字, 文件事件处理器既实现了高性能的网络通信模型, 又可以很好地与 redis 服务器中其他同样以单线程方式运行的模块进行对接, 这保持了 Redis 内部单线程设计的简单性。
深入学习:(具体参考王争的博客,讲的很详细!!!)跳表
一、如何理解跳表?
对于单链表来说,我们查找某个数据,只能从头到尾遍历链表,此时时间复杂度是 ○(n)。
单链表
那么怎么提高单链表的查找效率呢?看下图,对链表建立一级 索引
,每两个节点提取一个结点到上一级,被抽出来的这级叫做 索引
或 索引层
。
提高单链表的查找效率呢?对链表建立一级索引,每两个节点提取一个结点到上一级,被抽出来的这级叫做索引或索引层。 所以要找到13,就不需要将16前的结点全遍历一遍,只需要遍历索引,找到13,然后发现下一个结点是17,那么16一定是在 [13,17] 之间的,此时在13位置下降到原始链表层,找到16,加上一层索引后,查找一个结点需要遍历的结点个数减少了,也就是说查找效率提高了。
建立一级索引的方式相似,我们在第一级索引的基础上,每两个结点就抽出一个结点到第二级索引。此时再查找16,只需要遍历 6 个结点了,需要遍历的结点数量又减少了。
第一级索引
开发中经常会用到一种处理方式,hashmap 中存储的值类型是一个 list,这里就可以把索引当做 hashmap 中的键,将每 2 个结点看成每个键对应的值 list。
所以要找到13,就不需要将16前的结点全遍历一遍,只需要遍历索引,找到13,然后发现下一个结点是17,那么16一定是在 [13,17] 之间的,此时在13位置下降到原始链表层,找到16,加上一层索引后,查找一个结点需要遍历的结点个数减少了,也就是说查找效率提高了
那么我们再加一级索引呢? 跟前面建立一级索引的方式相似,我们在第一级索引的基础上,每两个结点就抽出一个结点到第二级索引。此时再查找16,只需要遍历 6 个结点了,需要遍历的结点数量又减少了。
第二级索引
当结点数量多的时候,这种添加索引的方式,会使查询效率提高的非常明显,这种链表加多级索引的结构,就是跳表。
二、用跳表查询到底有多快
在一个单链表中,查询某个数据的时间复杂度是 ○(n),那在一个具有多级索引的跳表中,查询某个数据的时间复杂度是多少呢?
按照上面的示例,每两个节点就抽出一个一级索引,每两个一级索引又抽出一个二级索引,所以第一级索引的结点个数大约就是 n/2
,第二级索引的结点个数就是 n/4
,第 k 级索引的结点个数就是 n/2^k
。
假设一共建立了 h 级索引,最高级的索引有两个节点(如果最高级索引只有一个结点,那么这一级索引起不到判断区间的作用,那么是没什么意义的),所以有:
在一个单链表中,查询某个数据的时间复杂度是 ○(n),那在一个具有多级索引的跳表中,查询某个数据的时间复杂度就是 ○(㏒n) 。
时间复杂度的分析
根据上图得知,每级遍历 3 个结点即可,而跳表的高度为 h ,所以每次查找一个结点时,需要遍历的结点数为 3*跳表高度 ,所以忽略低阶项和系数后的时间复杂度就是 ○(㏒n) 。
三、跳表是不是很浪费内存?
来分析一下跳表的空间复杂度为O(n)。
实际上,在实际开发中,我们不需要太在意索引占据的额外空间,在学习数据结构与算法时,我们习惯的将待处理数据看成整数,但是实际开发中,原始链表中存储的很可能是很大的对象,而索引结点只需要存储关键值(用来比较的值)和几个指针(找到下级索引的指针),并不需要存储原始链表中完整的对象,所以当对象比索引结点大很多时,那索引占用的额外空间就可以忽略了。
每层索引结点数
空间复杂度
所以如果将包含 n 个结点的单链表构造成跳表,我们需要额外再用接近 n 个结点的存储空间,那怎么才能降低索引占用的内存空间呢?
前面是每两个结点抽一个结点到上级索引,如果我们每三个,或每五个结点,抽一个结点到上级索引,是不是就不用那么多索引结点了呢?
每三个结点抽取一个上级索引
计算空间复杂度的过程与前面的一致,尽管最后空间复杂度依然是 ○(n),但我们知道,使用大○表示法忽略的低阶项或系数,实际上同样会产生影响,只不过我们为了关注高阶项而将它们忽略。
空间复杂度
实际上,在实际开发中,我们不需要太在意索引占据的额外空间,在学习数据结构与算法时,我们习惯的将待处理数据看成整数,但是实际开发中,原始链表中存储的很可能是很大的对象,而索引结点只需要存储关键值(用来比较的值)和几个指针(找到下级索引的指针),并不需要存储原始链表中完整的对象,所以当对象比索引结点大很多时,那索引占用的额外空间就可以忽略了。
四、高效的动态插入和删除
跳表这个动态数据结构,不仅支持查找操作,还支持动态的插入、删除操作,而且插入、删除操作的时间复杂度也是 ○(㏒n)。
对于单纯的单链表,需要遍历每个结点来找到插入的位置。但是对于跳表来说,因为其查找某个结点的时间复杂度是 ○(㏒n),所以这里查找某个数据应该插入的位置,时间复杂度也是 ○(㏒n)。
插入操作
那么删除操作呢?
删除操作。
五、跳表索引动态更新
当我们不停的往跳表中插入数据时,如果我们不更新索引,就可能出现某 2 个索引结点之间数据非常多的情况。极端情况下,跳表会退化成单链表。
作为一种动态数据结构,我们需要某种手段来维护索引与原始链表大小之间的平滑,也就是说如果链表中结点多了,索引结点就相应地增加一些,避免复杂度退化,以及查找、插入、删除操作性能下降。
跳表是通过随机函数来维护前面提到的平衡性。
我们往跳表中插入数据的时候,可以选择同时将这个数据插入到第几级索引中,比如随机函数生成了值 K,那我们就将这个结点添加到第一级到第 K 级这 K 级索引中。 随机函数可以保证跳表的索引大小和数据大小的平衡性,不至于性能过度退化。
Redis 中的有序集合是通过跳表来实现的,严格点讲,还用到了散列表(关于散列表),如果查看 Redis 开发手册,会发现 Redis 中的有序集合支持的核心操作主要有下面这几个:
- 插入一个数据
- 删除一个数据
- 查找一个数据
- 按照区间查找数据(比如查找在[100,356]之间的数据)
- 迭代输出有序序列
其中,插入、查找、删除以及迭代输出有序序列这几个操作,红黑树也能完成,时间复杂度和跳表是一样的,但是,按照区间来查找数据这个操作,红黑树的效率没有跳表高。
对于按照区间查找数据这个操作,跳表可以做到 ○(㏒n) 的时间复杂度定位区间的起点,然后在原始链表中顺序往后遍历就可以了。这样做非常高效。
当然,还有其他原因,比如,跳表代码更容易实现,可读性好不易出错。跳表更加灵活,可以通过改变索引构建策略,有效平衡执行效率和内存消耗。
不过跳表也不能完全替代红黑树。因为红黑树出现的更早一些。很多编程语言中的 Map 类型都是用红黑树来实现的。写业务的时候直接用就行,但是跳表没有现成的实现,开发中想用跳表,得自己实现。
Redis面试部分
1、redis中的过期策略
1)Redis项目中用来做什么
2) Redis分布式锁怎么实现的
3)Redisson的看门狗机制
4)Redis底层数据结构
5)Redis持久化机制
3.秒杀是怎么做的
1)怎么测试秒杀
2)Redis怎么库存预热
布隆过滤器:
Redis线程模型、单线程快的原因?
如何避免缓存穿透、缓存击穿、缓存雪崩?
总结
在技术面试的时候,不管是Redis还是什么问题,如果你能举出实际的例子,或者是直接说自己开发过程的问题和收获会给面试官的印象分会加很多,回答逻辑性也要强一点,不要东一点西一点,容易把自己都绕晕的。
还有一点就是我问你为啥用Redis你不要一上来就直接回答问题了,你可以这样回答:
帅气的面试官您好,首先我们的项目DB遇到了瓶颈,特别是秒杀和热点数据
这样的场景DB基本上就扛不住了,那就需要缓存中间件的加入了,目前市面上有的缓存中间件有 Redis 和 Memcached ,他们的优缺点……,综合这些然后再结合我们项目特点,最后我们在技术选型的时候选了谁。
如果你这样有条不紊,有理有据的回答了我的问题而且还说出这么多我问题外的知识点,我会觉得你不只是一个会写代码的人,你逻辑清晰,你对技术选型,对中间件对项目都有自己的理解和思考,说白了就是你的offer有戏了。
参考书籍、文献和资料
1、https://blog.csdn.net/xiaofeng10330111/article/details/105360939
这段时间收集了阿里、腾讯、百度、京东、美团、字节跳动等公司的Java面试题,总结了Redis系列的高频面试题:
1、Redis持久化机制
2、缓存雪崩、缓存穿透、缓存预热、缓存更新、缓存降级等问题
3、热点数据和冷数据是什么
4、Memcache与Redis的区别都有哪些?
5、单线程的redis为什么这么快
6、redis的数据类型,以及每种数据类型的使用场景
7、redis的过期策略以及内存淘汰机制
8、Redis 为什么是单线程的
9、Redis 常见性能问题和解决方案?
10、为什么Redis的操作是原子性的,怎么保证原子性的?
11、Redis事务
Redis系列高频面试题解析
Redis持久化机制
Redis是一个支持持久化的内存数据库,通过持久化机制把内存中的数据同步到硬盘文件来保证数据持久化。当Redis重启后通过把硬盘文件重新加载到内存,就能达到恢复数据的目的。
实现:单独创建fork()一个子进程,将当前父进程的数据库数据复制到子进程的内存中,然后由子进程写入到临时文件中,持久化的过程结束了,再用这个临时文件替换上次的快照文件,然后子进程退出,内存释放。
RDB是Redis默认的持久化方式。按照一定的时间周期策略把内存的数据以快照的形式保存到硬盘的二
进制文件。即Snapshot快照存储,对应产生的数据文件为dump.rdb,通过配置文件中的save参数来定义快照的周期。( 快照可以是其所表示的数据的一个副本,也可以是数据的一个复制品。)
AOF:Redis会将每一个收到的写命令都通过Write函数追加到文件最后,类似于MySQL的binlog。当Redis重启是会通过重新执行文件中保存的写命令来在内存中重建整个数据库的内容。
当两种方式同时开启时,数据恢复Redis会优先选择AOF恢复。
缓存雪崩、缓存穿透、缓存预热、缓存更新、缓存降级等问题
1、缓存雪崩
我们可以简单的理解为:由于原有缓存失效,新缓存未到期间
(例如:我们设置缓存时采用了相同的过期时间,在同一时刻出现大面积的缓存过期),所有原本应该访问缓存的请求都去查询数据库了,而对数据库CPU和内存造成巨大压力,严重的会造成数据库宕机。从而形成一系列连锁反应,造成整个系统崩溃。
解决办法:
大多数系统设计者考虑用加锁( 最多的解决方案)或者队列的方式保证来保证不会有大量的线程对数据库一次性进行读写,从而避免失效时大量的并发请求落到底层存储系统上。还有一个简单方案就时讲缓存失效时间分散开。
2、缓存穿透
缓存穿透是指用户查询数据,在数据库没有,自然在缓存中也不会有。这样就导致用户查询的时候,在缓存中找不到,每次都要去数据库再查询一遍,然后返回空(相当于进行了两次无用的查询)。这样请求就绕过缓存直接查数据库,这也是经常提的缓存命中率问题。
解决办法;
最常见的则是采用布隆过滤器,将所有可能存在的数据哈希到一个足够大的bitmap中,一个一定不存在的数据会被这个bitmap拦截掉,从而避免了对底层存储系统的查询压力。
另外也有一个更为简单粗暴的方法,如果一个查询返回的数据为空(不管是数据不存在,还是系统故障),我们仍然把这个空结果进行缓存,但它的过期时间会很短,最长不超过五分钟。通过这个直接设置的默认值存放到缓存,这样第二次到缓冲中获取就有值了,而不会继续访问数据库,这种办法最简单粗暴。
5TB的硬盘上放满了数据,请写一个算法将这些数据进行排重。如果这些数据是一些32bit大小的数据该如何解决?如果是64bit的呢?
对于空间的利用到达了一种极致,那就是Bitmap和布隆过滤器(Bloom Filter)。
Bitmap:典型的就是哈希表
缺点是,Bitmap对于每个元素只能记录1bit信息,如果还想完成额外的功能,恐怕只能靠牺牲更多的空
布隆过滤器(推荐)
就是引入了k(k>1)k(k>1)个相互独立的哈希函数,保证在给定的空间、误判率下,完成元素判重的过程。
它的优点是空间效率和查询时间都远远超过一般的算法,缺点是有一定的误识别率和删除困难。
Bloom-Filter算法的核心思想就是利用多个不同的Hash函数来解决“冲突”。
Hash存在一个冲突(碰撞)的问题,用同一个Hash得到的两个URL的值有可能相同。为了减少冲突,我们可以多引入几个Hash,如果通过其中的一个Hash值我们得出某元素不在集合中,那么该元素肯定不在集合中。只有在所有的Hash函数告诉我们该元素在集合中时,才能确定该元素存在于集合中。这便是Bloom-Filter的基本思想。
Bloom-Filter一般用于在大数据量的集合中判定某元素是否存在。
3、缓存预热
缓存预热这个应该是一个比较常见的概念,相信很多小伙伴都应该可以很容易的理解,缓存预热就是系统上线后,将相关的缓存数据直接加载到缓存系统。这样就可以避免在用户请求的时候,先查询数据库,然后再将数据缓存的问题!用户直接查询事先被预热的缓存数据!
解决思路:
1、直接写个缓存刷新页面,上线时手工操作下;
2、数据量不大,可以在项目启动的时候自动进行加载;
3、定时刷新缓存;
4、缓存更新
除了缓存服务器自带的缓存失效策略之外(Redis默认的有6中策略可供选择),我们还可以根据具体的业务需求进行自定义的缓存淘汰,常见的策略有两种:
(1)定时去清理过期的缓存;
(2)当有用户请求过来时,再判断这个请求所用到的缓存是否过期,过期的话就去底层系统得到新数据并更新缓存。
两者各有优劣,第一种的缺点是维护大量缓存的key是比较麻烦的,第二种的缺点就是每次用户请求过来都要判断缓存失效,逻辑相对比较复杂!具体用哪种方案,大家可以根据自己的应用场景来权衡。
5、缓存降级
当访问量剧增、服务出现问题(如响应时间慢或不响应)或非核心服务影响到核心流程的性能时,仍然需要保证服务还是可用的,即使是有损服务。系统可以根据一些关键数据进行自动降级,也可以配置开关实现人工降级。
降级的最终目的是保证核心服务可用,即使是有损的。而且有些服务是无法降级的(如加入购物车、结算)。
以参考日志级别设置预案:
(1)一般:比如有些服务偶尔因为网络抖动或者服务正在上线而超时,可以自动降级;
(2)警告:有些服务在一段时间内成功率有波动(如在95~100%之间),可以自动降级或人工降级,并发送告警;
(3)错误:比如可用率低于90%,或者数据库连接池被打爆了,或者访问量突然猛增到系统能承受的最大阀值,此时可以根据情况自动降级或者人工降级;
(4)严重错误:比如因为特殊原因数据错误了,此时需要紧急人工降级。
服务降级的目的,是为了防止Redis服务故障,导致数据库跟着一起发生雪崩问题。因此,对于不重要的缓存数据,可以采取服务降级策略,例如一个比较常见的做法就是,Redis出现问题,不去数据库查询,而是直接返回默认值给用户。
热点数据和冷数据是什么
热点数据,缓存才有价值
对于冷数据而言,大部分数据可能还没有再次访问到就已经被挤出内存,不仅占用内存,而且价值不场景。
对于热点数据,比如我们的某IM产品,生日祝福模块,当天的寿星列表,缓存以后可能读取数十万次。
再举个例子,某导航产品,我们将导航信息,缓存以后可能读取数百万次。
数据更新前至少读取两次,缓存才有意义。这个是最基本的策略,如果缓存还没有起作用就失效了,那就没有太大价值了。
那存不存在,修改频率很高,但是又不得不考虑缓存的场景呢?有!比如,这个读取接口对数据库的压力很大,但是又是热点数据,这个时候就需要考虑通过缓存手段,减少数据库的压力,比如我们的某助手产品的,点赞数,收藏数,分享数等是非常典型的热点数据,但是又不断变化,此时就需要将数据同步保存到Redis缓存,减少数据库压力。
Memcache与Redis的区别都有哪些?
1、存储方式 Memecache把数据全部存在内存之中,断电后会挂掉,数据不能超过内存大小。Redis有部份存在硬盘上,redis可以持久化其数据
2、数据支持类型 memcached所有的值均是简单的字符串,redis作为其替代者,支持更为丰富的数据类型 ,提供list,set,zset,hash等数据结构的存储
3、使用底层模型不同 它们之间底层实现方式 以及与客户端之间通信的应用协议不一样。Redis直接自己构建了VM 机制 ,因为一般的系统调用系统函数的话,会浪费一定的时间去移动和请求。
4、 value 值大小不同:Redis 最大可以达到 1gb;memcache 只有 1mb。
5、redis的速度比memcached快很多
6、Redis支持数据的备份,即master-slave模式的数据备份。
单线程的redis为什么这么快
1、纯内存操作
2、单线程操作,避免了频繁的上下文切换
3、采用了非阻塞I/O多路复用机制
redis的数据类型,以及每种数据类型的使用场景
回答:一共五种
1、String
这个其实没啥好说的,最常规的set/get操作,value可以是String也可以是数字。一般做一些复杂的计数功能的缓存。
2、hash
这里value存放的是结构化的对象,比较方便的就是操作其中的某个字段。博主在做单点登录的时候,就是用这种数据结构存储用户信息,以cookieId作为key,设置30分钟为缓存过期时间,能很好的模拟出类似session的效果。
3、list
使用List的数据结构,可以做简单的消息队列的功能。另外还有一个就是,可以利用lrange命令,做基于redis的分页功能,性能极佳,用户体验好。本人还用一个场景,很合适—取行情信息。就也是个生产者和消费者的场景。LIST可以很好的完成排队,先进先出的原则。
4、set
因为set堆放的是一堆不重复值的集合。所以可以做全局去重的功能。为什么不用JVM自带的Set进行去重?因为我们的系统一般都是集群部署,使用JVM自带的Set,比较麻烦,难道为了一个做一个全局去重,再起一个公共服务,太麻烦了。
另外,就是利用交集、并集、差集等操作,可以计算共同喜好,全部的喜好,自己独有的喜好等功能。
5、sorted set
redis的过期策略以及内存淘汰机制
redis采用的是定期删除+惰性删除策略。
为什么不用定时删除策略?
定时删除,用一个定时器来负责监视key,过期则自动删除。虽然内存及时释放,但是十分消耗CPU资源。
在大并发请求下,CPU要将时间应用在处理请求,而不是删除key,因此没有采用这一策略.
定期删除+惰性删除是如何工作的呢?
定期删除,redis默认每个100ms检查,是否有过期的key,有过期key则删除。需要说明的是,redis不是每个100ms将所有的key检查一次,而是随机抽取进行检查(如果每隔100ms,全部key进行检查,redis岂不是卡死)。因此,如果只采用定期删除策略,会导致很多key到时间没有删除。
于是,惰性删除派上用场。也就是说在你获取某个key的时候,redis会检查一下,这个key如果设置了过期时间那么是否过期了?如果过期了此时就会删除。
采用定期删除+惰性删除就没其他问题了么?
不是的,如果定期删除没删除key。然后你也没即时去请求key,也就是说惰性删除也没生效。这样,redis的内存会越来越高。那么就应该采用内存淘汰机制。
在redis.conf中有一行配置
maxmemory-policy volatile-lru
该配置就是配内存淘汰策略的(什么,你没配过?好好反省一下自己)
volatile-lru:从已设置过期时间的数据集(server.db[i].expires)中挑选最近最少使用的数据淘汰
volatile-ttl:从已设置过期时间的数据集(server.db[i].expires)中挑选将要过期的数据淘汰
volatile-random:从已设置过期时间的数据集(server.db[i].expires)中任意选择数据淘汰
allkeys-lru:从数据集(server.db[i].dict)中挑选最近最少使用的数据淘汰
allkeys-random:从数据集(server.db[i].dict)中任意选择数据淘汰
no-enviction(驱逐):禁止驱逐数据,新写入操作会报错
ps:如果没有设置 expire 的key, 不满足先决条件(prerequisites); 那么 volatile-lru, volatile-random 和volatile-ttl 策略的行为, 和 noeviction(不删除) 基本上一致。
Redis 为什么是单线程的
官方FAQ表示,因为Redis是基于内存的操作,CPU不是Redis的瓶颈,Redis的瓶颈最有可能是机器内存的大小或者网络带宽。既然单线程容易实现,而且CPU不会成为瓶颈,那就顺理成章地采用单线程的方案了(毕竟采用多线程会有很多麻烦!)Redis利用队列技术将并发访问变为串行访问
1、绝大部分请求是纯粹的内存操作(非常快速)
2、采用单线程,避免了不必要的上下文切换和竞争条件
3、非阻塞IO优点:
(1)速度快,因为数据存在内存中,类似于HashMap,HashMap的优势就是查找和操作的时间复杂度都是O(1)
(2)支持丰富数据类型,支持string,list,set,sorted set,hash
(3)支持事务,操作都是原子性,所谓的原子性就是对数据的更改要么全部执行,要么全部不执行
(4)丰富的特性:可用于缓存,消息,按key设置过期时间,过期后将会自动删除如何解决redis的并发竞争key问题同时有多个子系统去set一个key。这个时候要注意什么呢?不推荐使用redis的事务机制。因为我们的生产环境,基本都是redis集群环境,做了数据分片操作。你一个事务中有涉及到多个key操作的时候,这多个key不一定都存储在同一个redis-server上。因此,redis的事务机制,十分鸡肋。
(1)如果对这个key操作,不要求顺序:准备一个分布式锁,大家去抢锁,抢到锁就做set操作即可作了。以此类推。
(2) 利用队列,将set方法变成串行访问也可以redis遇到高并发,如果保证读写key的一致性对redis的操作都是具有原子性的,是线程安全的操作,你不用考虑并发问题,redis内部已经帮你处理好并发的问题了。
Redis 常见性能问题和解决方案?
1、Master 最好不要做任何持久化工作,如 RDB 内存快照和 AOF 日志文件
2、 如果数据比较重要,某个 Slave 开启 AOF 备份数据,策略设置为每秒同步一次
3、为了主从复制的速度和连接的稳定性, Master 和 Slave 最好在同一个局域网内
4、尽量避免在压力很大的主库上增加从库
5、主从复制不要用图状结构,用单向链表结构更为稳定,即:Master <- Slave1 <- Slave2 <-Slave3…
为什么Redis的操作是原子性的,怎么保证原子性的?
对于Redis而言,命令的原子性指的是:一个操作的不可以再分,操作要么执行,要么不执行。
Redis的操作之所以是原子性的,是因为Redis是单线程的。
Redis本身提供的所有API都是原子操作,Redis中的事务其实是要保证批量操作的原子性。
多个命令在并发中也是原子性的吗?
不一定, 将get和set改成单命令操作,incr 。使用Redis的事务,或者使用Redis+Lua==的方式实现.
Redis事务
Redis事务功能是通过MULTI、EXEC、DISCARD和WATCH 四个原语实现的
Redis会将一个事务中的所有命令序列化,然后按顺序执行。
1、redis 不支持回滚“Redis 在事务失败时不进行回滚,而是继续执行余下的命令”, 所以 Redis 的内部可以保持简单且快速。
2、如果在一个事务中的命令出现错误,那么所有的命令都不会执行;
3、如果在一个事务中出现运行错误,那么正确的命令会被执行。
(1)MULTI命令用于开启一个事务,它总是返回OK。MULTI执行之后,客户端可以继续向服务器发送任意多条命令,这些命令不会立即被执行,而是被放到一个队列中,当EXEC命令被调用时,所有队列中的命令才会被执行。
(2)EXEC:执行所有事务块内的命令。返回事务块内所有命令的返回值,按命令执行的先后顺序排列。当操作被打断时,返回空值 nil 。
(3)通过调用DISCARD,客户端可以清空事务队列,并放弃执行事务, 并且客户端会从事务状态中退出。
(4)WATCH 命令可以为 Redis 事务提供 check-and-set (CAS)行为。可以监控一个或多个键,一旦其中有一个键被修改(或删除),之后的事务就不会执行,监控一直持续到EXEC命令。
深入学习redis 的线程模型
一、redis 的线程模型
redis 内部使用文件事件处理器 file event handler,它是单线程的,所以redis才叫做单线程模型。它采用IO多路复用机制同时监听多个 socket,将产生事件的 socket 压入内存队列中,事件分派器根据 socket 上的事件类型来选择对应的事件处理器进行处理。
文件事件处理器的结构:
- 多个 socket
- IO 多路复用程序
- 文件事件分派器
- 事件处理器(连接应答处理器、命令请求处理器、命令回复处理器)
线程模型
多个 socket 可能会并发产生不同的操作,每个操作对应不同的文件事件,但是 IO多路复用程序会监听多个 socket,会将产生事件的 socket 放入队列中排队,事件分派器每次从队列中取出一个 socket,根据 socket 的事件类型交给对应的事件处理器进行处理。
二、一次客户端与redis的完整通信过程#
建立连接
- 首先,redis 服务端进程初始化的时候,会将 server socket 的 AE_READABLE 事件与连接应答处理器关联。
- 客户端 socket01 向 redis 进程的 server socket 请求建立连接,此时 server socket 会产生一个 AE_READABLE 事件,IO 多路复用程序监听到 server socket 产生的事件后,将该 socket 压入队列中。
- 文件事件分派器从队列中获取 socket,交给连接应答处理器。
- 连接应答处理器会创建一个能与客户端通信的 socket01,并将该 socket01 的 AE_READABLE 事件与命令请求处理器关联。
执行一个set请求
- 客户端发送了一个 set key value 请求,此时 redis 中的 socket01 会产生 AE_READABLE 事件,IO 多路复用程序将 socket01 压入队列,
- 此时事件分派器从队列中获取到 socket01 产生的 AE_READABLE 事件,由于前面 socket01 的 AE_READABLE 事件已经与命令请求处理器关联,
- 因此事件分派器将事件交给命令请求处理器来处理。命令请求处理器读取 socket01 的 key value 并在自己内存中完成 key value 的设置。
- 操作完成后,它会将 socket01 的 AE_WRITABLE 事件与命令回复处理器关联。
- 如果此时客户端准备好接收返回结果了,那么 redis 中的 socket01 会产生一个 AE_WRITABLE 事件,同样压入队列中,
- 事件分派器找到相关联的命令回复处理器,由命令回复处理器对 socket01 输入本次操作的一个结果,比如 ok,之后解除 socket01 的 AE_WRITABLE 事件与命令回复处理器的关联。
三、redis为什么效率这么高?#
- 纯内存操作。
- 核心是基于非阻塞的 IO 多路复用机制。
- C 语言实现,语言更接近操作系统,执行速度相对会更快。
- 单线程反而避免了多线程的频繁上下文切换问题,预防了多线程可能产生的竞争问题。
redis 线程模型
文件事件处理器
Redis基于Reactor模式开发了网络事件处理器,这个处理器被称为文件事件处理器。它的组成结构为4部分:多个套接字、IO多路复用程序、文件事件分派器、事件处理器。因为文件事件分派器队列的消费是单线程的,所以Redis才叫单线程模型。
消息处理流程
文件事件处理器使用I/O多路复用(multiplexing)程序来同时监听多个套接字,并根据套接字目前执行的任务来为套接字关联不同的事件处理器。
当被监听的套接字准备好执行连接应答(accept)、读取(read)、写入(write)、关闭(close)等操作时,与操作相对应的文件事件就会产生,这时文件事件处理器就会调用套接字之前关联好的事件处理器来处理这些事件。
尽管多个文件事件可能会并发地出现,但I/O多路复用程序总是会将所有产生事件的套接字都推到一个队列里面,然后通过这个队列,以有序(sequentially)、同步(synchronously)、每次一个套接字的方式向文件事件分派器传送套接字:当上一个套接字产生的事件被处理完毕之后(该套接字为事件所关联的事件处理器执行完毕), I/O多路复用程序才会继续向文件事件分派器传送下一个套接字。
I/O 多路复用程序的实现
Redis的I/O多路复用程序的所有功能是通过包装select、epoll、evport和kqueue这些I/O多路复用函数库来实现的,每个I/O多路复用函数库在Redis源码中都对应一个单独的文件,比如ae_select.c、ae_epoll.c、ae_kqueue.c等。
因为Redis为每个I/O多路复用函数库都实现了相同的API,所以I/O多路复用程序的底层实现是可以互换的,如下图所示。
有关epoll的详细讲解,可以点击查看,彻底搞懂epoll高效运行的原理
Redis在I/O多路复用程序的实现源码中用#include宏定义了相应的规则,程序会在编译时自动选择系统中性能最好的I/O多路复用函数库来作为Redis的I/O多路复用程序的底层实现:
#
/* Include the best multiplexing layer supported by this system.
* The following should be ordered by performances, descending. */
#ifdef HAVE_EVPORT
#include "ae_evport.c"
#else
#ifdef HAVE_EPOLL
#include "ae_epoll.c"
#else
#ifdef HAVE_KQUEUE
#include "ae_kqueue.c"
#else
#include "ae_select.c"
#endif
#endif
# endif
文件事件的类型
I/O 多路复用程序可以监听多个套接字的ae.h/AE_READABLE事件和ae.h/AE_WRITABLE事件,这两类事件和套接字操作之间的对应关系如下:
当套接字变得可读时(客户端对套接字执行write操作,或者执行close操作),或者有新的可应答(acceptable)套接字出现时(客户端对服务器的监听套接字执行connect操作),套接字产生AE_READABLE 事件。
当套接字变得可写时(客户端对套接字执行read操作),套接字产生AE_WRITABLE事件。I/O多路复用程序允许服务器同时监听套接字的AE_READABLE事件和AE_WRITABLE事件,如果一个套接字同时产生了这两种事件,那么文件事件分派器会优先处理AE_READABLE事件,等到AE_READABLE事件处理完之后,才处理AE_WRITABLE 事件。这也就是说,如果一个套接字又可读又可写的话,那么服务器将先读套接字,后写套接字。
文件事件的处理器
Redis为文件事件编写了多个处理器,这些事件处理器分别用于实现不同的网络通讯需求,常用的处理器如下:
为了对连接服务器的各个客户端进行应答, 服务器要为监听套接字关联连接应答处理器。
为了接收客户端传来的命令请求, 服务器要为客户端套接字关联命令请求处理器。
为了向客户端返回命令的执行结果, 服务器要为客户端套接字关联命令回复处理器。
连接应答处理器
networking.c中acceptTcpHandler函数是Redis的连接应答处理器,这个处理器用于对连接服务器监听套接字的客户端进行应答,具体实现为sys/socket.h/accept函数的包装。
当Redis服务器进行初始化的时候,程序会将这个连接应答处理器和服务器监听套接字的AE_READABLE事件关联起来,当有客户端用sys/socket.h/connect函数连接服务器监听套接字的时候, 套接字就会产生AE_READABLE 事件, 引发连接应答处理器执行, 并执行相应的套接字应答操作,如图所示。
命令请求处理器
networking.c中readQueryFromClient函数是Redis的命令请求处理器,这个处理器负责从套接字中读入客户端发送的命令请求内容, 具体实现为unistd.h/read函数的包装。
当一个客户端通过连接应答处理器成功连接到服务器之后, 服务器会将客户端套接字的AE_READABLE事件和命令请求处理器关联起来,当客户端向服务器发送命令请求的时候,套接字就会产生 AE_READABLE事件,引发命令请求处理器执行,并执行相应的套接字读入操作,如图所示。
在客户端连接服务器的整个过程中,服务器都会一直为客户端套接字的AE_READABLE事件关联命令请求处理器。
命令回复处理器
networking.c中sendReplyToClient函数是Redis的命令回复处理器,这个处理器负责将服务器执行命令后得到的命令回复通过套接字返回给客户端,具体实现为unistd.h/write函数的包装。
当服务器有命令回复需要传送给客户端的时候,服务器会将客户端套接字的AE_WRITABLE事件和命令回复处理器关联起来,当客户端准备好接收服务器传回的命令回复时,就会产生AE_WRITABLE事件,引发命令回复处理器执行,并执行相应的套接字写入操作, 如图所示。
当命令回复发送完毕之后, 服务器就会解除命令回复处理器与客户端套接字的 AE_WRITABLE 事件之间的关联。
一次完整的客户端与服务器连接事件示例
假设Redis服务器正在运作,那么这个服务器的监听套接字的AE_READABLE事件应该正处于监听状态之下,而该事件所对应的处理器为连接应答处理器。
如果这时有一个Redis客户端向Redis服务器发起连接,那么监听套接字将产生AE_READABLE事件, 触发连接应答处理器执行:处理器会对客户端的连接请求进行应答, 然后创建客户端套接字,以及客户端状态,并将客户端套接字的 AE_READABLE 事件与命令请求处理器进行关联,使得客户端可以向主服务器发送命令请求。
之后,客户端向Redis服务器发送一个命令请求,那么客户端套接字将产生 AE_READABLE事件,引发命令请求处理器执行,处理器读取客户端的命令内容, 然后传给相关程序去执行。
执行命令将产生相应的命令回复,为了将这些命令回复传送回客户端,服务器会将客户端套接字的AE_WRITABLE事件与命令回复处理器进行关联:当客户端尝试读取命令回复的时候,客户端套接字将产生AE_WRITABLE事件, 触发命令回复处理器执行, 当命令回复处理器将命令回复全部写入到套接字之后, 服务器就会解除客户端套接字的AE_WRITABLE事件与命令回复处理器之间的关联。
Redis疑问快答
详细的解答,请查看笔者之前的几篇Redis博客。
是否使用过Redis集群,集群的原理是什么?
Redis Sentinal着眼于高可用,在master宕机时会自动将slave提升为master,继续提供服务。
Redis Cluster着眼于扩展性,在单个redis内存不足时,使用Cluster进行分片存储。
如何使用过Redis做异步队列?
一般使用list结构作为队列,rpush生产消息,lpop消费消息。当lpop没有消息的时候,要适当sleep一会再重试。
如果不用sleep,list还有个指令叫blpop,在没有消息的时候,它会阻塞住直到消息到来。
如果想要生产一次消费多次,可以使用pub/sub主题订阅者模式,可以实现1:N的消息队列,但在消费者下线后,生产的消息会丢失,想要持久化的话,需要使用消息队列如rabbitmq等。
redis如何实现延时队列?
使用sortedset,拿时间戳作为score,消息内容作为key调用zadd来生产消息,消费者用zrangebyscore指令获取N秒之前的数据轮询进行处理。
如果有大量的key需要设置同一时间过期,需要注意什么?
如果大量的key过期时间设置的过于集中,到过期的那个时间点,redis可能会出现短暂的卡顿现象。一般需要在过期时间上加一个随机值,使得过期时间分散一些。
Redis单点吞吐量
单点TPS达到8万/秒,QPS达到10万/秒,补充下TPS和QPS的概念
QPS: 应用系统每秒钟最大能接受的用户访问量。每秒钟处理完请求的次数,注意这里是处理完,具体是指发出请求到服务器处理完成功返回结果。可以理解在server中有个counter,每处理一个请求加1,1秒后counter=QPS。
TPS:每秒钟最大能处理的请求数。每秒钟处理完的事务次数,一个应用系统1s能完成多少事务处理,一个事务在分布式处理中,可能会对应多个请求,对于衡量单个接口服务的处理能力,用QPS比较合理。
Redis哈希槽
Redis集群没有使用一致性hash,而是引入了哈希槽的概念,当需要在 Redis 集群中放置一个 key-value 时,根据 CRC16(key) mod 16384的值,决定将一个key放到哪个桶中。
Redis集群最大节点个数是多少?
Redis集群预分好16384个桶(哈希槽)
Redis事务是什么?
Redis事务可以一次执行多个命令,有以下特点:
批量操作在发送 EXEC 命令前被放入队列缓存。
收到 EXEC 命令后进入事务执行,事务中任意命令执行失败,其余的命令依然被执行。
在事务执行过程,其他客户端提交的命令请求不会插入到事务执行命令序列中。
事务可以理解为一个打包的批量执行脚本,但批量指令并非原子化的操作,中间某条指令的失败不会导致前面已做指令的回滚,也不会造成后续的指令不做。 ———————————————— 版权声明:本文为CSDN博主「全菜工程师小辉」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。 原文链接:https://blog.csdn.net/y277an/article/details/98342442
面试题:
1、缓存和数据库双写时数据一致性问题?
操作:先更新数据库,再删除缓存
正常的情况是这样的:
- 先操作数据库,成功
再删除缓存,也成功
如果原子性被破坏了:第一步成功(操作数据库),第二步失败(删除缓存),会导致数据库里是新数据,而缓存里是旧数据。
如果第一步(操作数据库)就失败了,我们可以直接返回错误(Exception),不会出现数据不一致。
如果在高并发的场景下,出现数据库与缓存数据不一致的概率特别低,也不是没有:缓存刚好失效//线程A查询数据库,得一个旧值//线程B将新值写入数据库//线程B删除缓存//线程A将查到的旧值写入缓存
要达成上述情况,还是说一句概率特别低,因为这个条件需要发生在读缓存时缓存失效,而且并发着有一个写操作。而实际上数据库的写操作会比读操作慢得多,而且还要锁表,而读操作必需在写操作前进入数据库操作,而又要晚于写操作更新缓存,所有的这些条件都具备的概率基本并不大。对于这种策略,其实是一种设计模式:Cache Aside Pattern
解决删除缓存失败的解决思路
:将需要删除的key发送到消息队列中---自己消费消息,获得需要删除的key---不断重试删除操作,直到成功
操作:先删除缓存,再更新数据库
正常情况是这样的:
1、先删除缓存,成功;
2、再更新数据库,也成功;
3、如果原子性被破坏了
第一步成功(删除缓存),第二步失败(更新数据库),数据库和缓存的数据还是一致的。
如果第一步(删除缓存)就失败了,我们可以直接返回错误(Exception),数据库和缓存的数据还是一致的。
看起来是很美好,但是我们在并发场景下分析一下,就知道还是有问题的了:线程A删除了缓存///线程B查询,发现缓存已不存在
///线程B去数据库查询得到旧值///线程B将旧值写入缓存///线程A将新值写入数据库-------->所以也会导致数据库和缓存不一致的问题。
解决并发下解决数据库与缓存不一致的思路:将删除缓存、修改数据库、读取缓存等的操作积压到队列里边,实现串行化。
或者就是使用对缓存进行双删,
对比两种策略:
先删除缓存、再更新数据库----->在高并发下表现的不如意,在原子性被破环时表现优异 先更新数据库,再删除缓存(Cache Aside Pattern设计模式)) ----->在高并发先表现优异,在原子性被破坏时表现不如意