- 为啥用redis?
因为传统的关系型数据库如MySQL已经不能适用所有的场景,比如秒杀的库存扣件,APP首页的访问流量高峰等,都很容易把数据库打崩,所以引入缓存中间件,目前市场上比较常用的缓存中间件有Redis和Memcached。
redis 和 memcached的区别?
redis有哪些数据结构?
String、Hash、List、Set、SortedSet常见类型
数据结构 HyperLogLog、Geo、Pub/Sub
Redis Module,如BloomFilter(布隆过滤器),RedisSearch,Redis-ML如果有大量的key 需要设置同一时间过期,一般需要注意什么?
如果大量的key过期时间设置的过于几种,到过期的那个时间点,Redis可能会出现短暂的卡顿现象。严重给你的话会出现缓存雪崩,我们一般需要在时间上加一个随机值,使得过期时间分散一些。Redis分布式锁,是什么?
先拿setnx来争抢锁,抢到之后,再用expire给锁加一个过期时间防止锁忘记了释放。
参考redis 分布式锁如果在setnx之后执行expire之前进程意外crash或者要重启维护,会如何?
将获取分布式锁的操作命令和设置过期时间的操作命令合并为一个原子操作,set(***, nx=, expire=)假如Redis里面有一亿个key,其中有10w个key 是以某个固定的已知的前缀开头的,如何将它们全部找出来?
使用keys指令可以扫除指定模式的key列表。如果这个redis正在给线上的业务提供服务,那使用keys指令会有什么问题?
这个时候要回答Redis关键的一个特性:Redis的单线程。keys指令会导致线程阻塞一段时间,线上服务会停顿,知道指令执行完毕,服务才能恢复。这个时候可以使用scan指令,scan指令可以无阻塞的提取住指定模式的key列表,但是会有一定的重复概率,在客户端做一次去重就可以,但是正题所话费的时间会比直接用keys指令长。
不过,增量式迭代命令也不是没有缺点的:使用SMEMBERS命令可以返回集合键当前包含的所有元素,但是对于SCAN这类增量式迭代命令来说,因为在对键进行增量式迭代的过程中,键可能会被修改,所以增量式迭代命令只能对被返回的元素提供有限的保证。
Redis做异步队列,如何用?
一般使用list结构作为队列,rpush产生消息,lpop消费消息。当lpop没有消息的时候,要适当sleep一会再重试。可不可以不用sleep?
list还有一个指令叫blpop,在没有消息的时候,它会阻塞住知道消息到来。
能不能生产一次消费多次?
使用pub/sub主题订阅者模式,可以实现1:N的消息队列。
pub/sub有什么缺点?
在消费者下线的情况下,生产的消息会丢失,得使用专业的消息队列。redis如何实现延时队列?
使用sortedset,拿时间戳作为score,消息内容作为key调用zadd来生产消息,消费者用zrangebyscroe质量获取N秒之前的数据轮询进行处理。Redis是怎么持久化的?服务主从数据怎么交互的?
RDB做镜像全量持久化,AOF做增量持久化。因为RDB会耗费较长时间,不够实时,在停机的时候会导致大量丢失数据,所以需要AOF来配合使用。在redis实例重启时,会使用RDB持久化文件重新构建内存,再使用AOF重放近期的操作指令来实现完整恢复重启之前的状态。
把RDB理解为一整个表全量的数据,AOF理解为每次操作的日志,服务器重启的时候先把表的数据全部搞进去,但是他可能不完整,你再回放一下日志,数据就完整了。不过Redis本社的机制是AOF持久化开启且存在AOF文件时,有限加载AOF文件;AOF关闭或者AOF文件不存在时,加载RDB文件;加载AOF/RDB文件后,Redis启动成功;AOF/RDB文件存在错误时,Redis启动失败并打印错误信息。如果突然机器掉电会怎样?
取决于AOF日志sync属性的配置,如果不要求性能,在每条写指令时都sync一下磁盘,就不会丢失数据。但是在高性能的要求下每次都sync是不现实的,一般都使用定时sync。RDB的原理是什么?
fork和cow。fork是指redis通过创建子进程来进行RDB操作,cow指的是copy on write,子进程创建后,父子进程共享数据段,父进程继续提供读写服务,写脏的页面数据会逐渐和子进程分离开来。
注:回答这个问题的时候,最好加上AOF和RDB的优缺点。Pipeline有什么好处,为什么要用pipeline?
可以将多次IO往返的时间缩减为一次,前提是pipeline执行的指令之间没有因果相关性。使用redis-benchmark进行压测的时候可以发现影响redis的QPS峰值的一个重要因素是pipeline批次指令的数目。Redis的同步机制了解么?
Rdis可以使用主从同步,从从同步。第一次同步时,主节点做一次bgsave,并同时将后续修改操作记录到内存buffer,待完成后将RDB文件全量同步到复制节点,复制节点接受完成后将RDB镜像加载到内存。加载完成后,再通知主节点将期间修改的操作记录同步到复制节点进行重放就完成了同步过程。后续的赠来那个数据通过AOF日志同步即可,有点类似数据库的binlog。是否使用过Redis集群,集群的高可用怎么保证,集群的原理是什么?
Redis Sentinal 着眼于高可用,在master宕机时会自动将slave提升为master,继续提供服务。
Redis Cluster 着眼于扩展性,在单个redis内存不足时,使用Cluster进行分片存储。
Redis持久化
RDB Redis DataBase 缩写快照
RDB是Redis默认的持久化方式。按照一定的时间将内存的数据以快照的形式保存到磁盘中,对应产生的数据文件为dump.rdb。通过配置文件的save参数来定义快照的周期。
优点:
1) 只有一个文件 dump.rdb,方便持久化
2) 容灾性好,一个文件可以保存到安全的磁盘
3) 性能最大化,fork子进程来完成写操作,让主进程继续处理命令,所以是IO最大化。使用单独子进程来进行持久化,主进程不会进行任何IO操作,保证了redis 的高性能。
4) 相对于数据集大时,比AOF的启动效率更高。
缺点:
1) 数据安全性低。RDB是间隔一段时间进行持久化,如果持久化之间redis 发生故障,会发生数据丢失。所以这种方式更适合数据要求不严谨的时候。
2) AOF(Append-only file)持久化方式:是指所有的命令行记录以 redis 命令请求协议的格式完全持久化存储,保存为aof文件。
出发机制-主要三种方式
save(同步):客户端想redis发送save命令来创建一个快照文件。自行save命令的时候,如果存在老的快照文件,新的将会替换老得。
bgsave(异步):客户端想redis发送bgsave命令,redis调用fork创建一个子进程,然后子进程负责将快照写入磁盘,而父进程则继续处理命令请求。
命令 | save | bgsave |
---|---|---|
IO类型 | 同步 | 异步 |
是否阻塞 | 是 | 是 |
复杂度 | O(n) | O(n) |
优点 | 不会消耗额外内存 | 不阻塞客户端命令 |
缺点 | 阻塞客户端命令 | 需要fork,消耗内存 |
自动生成:通过配置,满足任何一个条件就会创建快照文件。
AOF Append Only File 持久化
是将redis执行的每次写命令记录到单独的日志文件中,当重启redis会重新将持久化的日志中文件恢复数据。
当两种方式同时开启时,数据恢复Redis会有限选择AOF恢复。
优点:
1) 数据安全,aof持久化可以配置appendfsync属性,有always,没进行一次命令操作就记录到aof文件中一次。
2) 通过append模式写文件,即使中途服务宕机,可以通过redis-check-aof工具解决数据一致性问题。
3) AOF机制的rewrite模式。AOF文件没被rewrite之前,(文件过大时会对命令进行合并重写),可以删除其中的某些命令(比如误操作)
缺点:
1) AOF文件比RDB文件大,且恢复速度慢
2)数据集大的时候,比rdb启动效率低
AOF三种策略
always:每条redis写命令都同步写入磁盘
everysec:每秒执行一次同步,将多个命令写入磁盘。
no:有操作系统决定何时同步
三种策略对比:生产环境中需要根据实际的需求进行选择
命令 | always | everysec | no |
---|---|---|---|
优点 | 不丢失数据 | 每秒一次fsync | 不用管 |
缺点 | IO开销较大,一般的SATRA盘只有几百TPS | 丢1s数据 | 不可控 |
两者相较的优缺点:
1) AOF文件比RDB更新频率搞,有限使用AOF还原数据。
2) AOF比RDB更安全也更大
3) RDB性能比AOF好
4) 如果两个都配了优先加载AOF
如何选择何时的持久化方式
一般情况,想达到足以媲美PostgreSQL的数据安全性,同时使用两种持久化功能。在这种情况下,当redis重启的时候会优先载入AOF文件来恢复原始的数据,因为在通常情况下AOF文件保存的数据集要比RDB文件保存的数据集要完整。
非常关心数据,仍可以承受数分钟以内的数据丢失,可以只使用RDB持久化。
不推荐只使用AOF持久化,因为定时生成的RDB快照(snapshot)非常便于进行数据库备份,并且RDB恢复数据集的速度也要比AOF恢复的速度要快,使用RDB还可以避免AOF程序的bug。
reids 持久化数据和缓存怎么做扩容?
如果redis被当作缓存使用,使用一致性哈希实现动态扩容缩容。
如果redis被当作一个持久化存储使用,必须使用固定的keys-to-nodes映射关系,节点的数量一旦确定不能变化。否则的话(即redis即诶但需要动态变化的情况),必须使用可以在运行时进行数据再平衡的一套系统,而当前只有redis集群可以做到这样。
缓存穿透
缓存穿透是指查询一个一定不存在的数据,由于缓存是不明中时被动写的,并且处于容错考虑,如果从存储层查不到数据则不写如缓存,这将导致这个不存在的数据每次请求都要到存储层去查询,失去了缓存的一亿。在流量大时,可能DB就挂掉了,要是有人利用不存在的key频繁攻击我们的应用,这就是漏洞。
解决方案
有很多中方法可以有效的解决缓存穿透问题,最常见的是采用布隆过滤器,将所有可能存在的数据哈希到一个足够大的bitmap中,一个一定不存在的数据会被这个bitmap拦截掉,从而避免了对底层存储系统的查询压力。另外也有一个更为简单粗暴的方法,如果一个查询返回的数据唯恐(不管是数据不存在,还是系统故障),仍然把这个空结果进行缓存,但它的过期时间很短。
缓存雪崩
缓存雪崩是指在设置缓存时采用了相同的过期时间,导致缓存在某一时刻同时失效,请求全部转发到DB,DB瞬时压力锅中雪崩。
解决方案
缓存失效时的雪崩效应对底层系统的冲击非常可怕。大多数系统设计者考虑用加锁或者队列的方式保证缓存的单线程(进程)写,从而避免失效时大量的并发请求落到底层存储系统上。简单方案:将缓存失效时间分散开,比如可以在原有的失效时间基础上增加一个随机值,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件。
缓存击穿
对于一些设置了过期时间的key,如果这些key可能会在某些时间点被超高并发访问,是一种非常“热点”的数据。这个时候,需要考虑一个问题:缓存被“击穿”的问题,这个和缓存雪崩的区别在于这里针对某一key缓存,前者则是很多key。
缓存在某个时间点过期的时候,恰好在这个时间点对这个Key有大量的并发请求过来,这些请求发现缓存过期一般都会从后端DB家在数据并回射到缓存,这个时候大并发的请求可能会瞬间把后端DB压垮。
解决方案
- 使用互斥锁(mutex key)
业界比较常用的做法,是使用mutex。在缓存失效的时候(判断拿出来的值为空),不是立即去load db,而是先使用缓存工具的某些带成功操作返回值的操作(比如Redis的SETNX或者Memcache的ADD)去set一个mutex key,当操作返回成功时,再进行load db的操作并回射缓存;否则,就重试整个get缓存的方法。 - “提前”使用互斥锁(mutex key)
在value内部设置一个超时值A,这个超时值A比实际的超时值小。当从cache读取到超时值A发现它已经过期的时候,马上延长超时值A并重新设置到cache。然后再从数据加载数据并设置到cache中。 - “永远不过期”
(1)从redis上看,确实没有设置过期时间,这就保证了不会出现热点key过期问题,也就是“物理”不过期
(2)从功能上看,如果不过期,那不就成静态的了?所以把过期时间存在key对应的value里,如果发现要过期了,通过一个后台的异步线程进行缓存的构建,也就是“逻辑”过期
从实战看,这种方法对于性能非常友好,唯一不足的就是构建缓存的时候,其余线程(非构建缓存的线程)可能访问的是老数据,但是对于一般的互联网功能来说这个还是可以忍受的。 - 资源保护
采用netflix的hystrix,可以做资源的隔离保护主线程池,如果把这个应用到缓存的构建也未尝不可。
四种解决方案:没有最佳只有最合适
解决方案 | 优点 | 缺点 |
---|---|---|
简单分布式互斥锁(mutex key) | 1. 思路简单 2. 保证一致性 | 1. 代码复杂度增大 2. 存在死锁的风险 3. 存在线程池阻塞的风险 |
“提前”使用互斥锁 | 1. 保证一致性 | 同上 |
不过期 | 1. 异步构建缓存,不会阻塞线程池 | 1. 不保证一致性 2. 代码复杂度增大(每个value都要维护一个timekey) 3. 占用一定的内存空间(每个value都要维护一个timekey) |
资源隔离组建hystrix | 1. hystrix技术成熟,有效保证后端 2. hystrix监控强大 | 1. 部分访问存在降级策略 |
总结
对于缓存系统常见的缓存满了和数据丢失问题,需要根据具体业务分析,通常采用LRU策略处理溢出,Redis的RDB和AOF持久化策略来保证一定情况下的数据安全。
Redis Modules
Redis Modules 是redis 4.0 引入的一种扩展机制,用户可以通过实现redis module 提供的 C api 接口为 redis 服务添加定制化共鞥。 redisLab 也希望借此来规范 redis 社区的 ecosystem 实现。
redis module 本身的版本独立于redis,并且以编译成动态加载 .so 文件的方式release,不同版本的 reids 可以 load 同一版本的 module.so 文件。
redis 提供了两种概念加载方式。可以通过在 conf 文件中加入 loadmodule /path/to/mymodule.so,也可以在redis-cli中使用命令 MODULE LOAD /path/to/module.so 动态加载,MODULE UNLOAD 卸载。
RedisSearch
为redis提供全文索引相关的搜索功能
特性
- 基于文档的全文索引
- 高性能增量索引
- 支持文档评分,文档字段(field)权重机制
- 支持布尔复杂查询
- 支持自动补全
- 基于 snowball 的词干分析,多语言支持。使用friso 支持中文分词
- utf-8 字符集支持
- redis 数据持久化支持
- 自定义评分机制
Redis-避免缓存穿透的利器 BloomFilter
Bloom Filter 概念
布隆过滤器,实际上是一个很长的二进制向量和一系列随机映射函数。可以用于检索一个元素是否在一个集合中。它的优点是空间效率和查询时间都远远超过一般的算法,缺点是有一定的误识别率和删除困难。
Bloom Filter 原理
原理是,当一个元素被加入集合时,通过K个散列函数将这个元素映射称一个位数组中的K个点,把他们置为1。检索时,我们只要看看这个点是不是都是1就(大约)直到集合中有没有它了:如果这些点有任何一个0,则被检元素一定不再;如果都是1,则被检元素很可能在。这就是布隆过滤器的基本思想。
Bloom Filter跟哈希函数Bit-Map不同指出在于:Bloom Filter适用了K个哈希函数,每个字符串跟K个bit对应。从而降低冲突的概率。
缓存穿透(每次查询都会直接打到DB)
添加过滤器,防止缓存击穿。
Bloom Filter 的缺点
之所以能坐到在时间和空间上的效率比较高,是因为牺牲了判断的准确性、删除的便利性
- 存在误判,可能要查到的元素并没有在容器中,但是hash之后得到的K的为止上值都是1。如果bloom filter中存储的是黑名单,那么可以通过建立一个白名单来存储可能会误判的元素。
- 删除困难。一个放入容器的元素映射到bit数组的K个位置上是1,删除的时候不能简单的直接置为0,可能会影响其他元素的判断。可以采用Counting Bloom Filter。
Bloom Filter 实现
布隆过滤器有许多实现与优化
在使用bloom filter时,绕不过的两点是预估数据量N以及期望的误判率FPP;在实现时,绕不过的两点就是Hash函数的选取以及bit数据的大小。
对于一个确定的场景,我们预估要存的数据量为N,期望的误判率为FPP,然后需要计算我们需要的Bit数据的大小M,以及Hash函数的个数K,并选择Hash函数
- Bit数据大小选择
根据预估数据量N以及误判率FPP,Bit数组大小的M的计算方式:
- 哈希函数选择
由预估数据量N以及Bit数据长度M,可以得到一个Hash函数的个数K:
哈希函数的选择对性能的影响很大,一个好的哈希函数能近似等概率的将字符串映射到各个Bit。选择K个不同的哈希函数比较麻烦,一种简单的方法是选择一个哈希函数,然后送入K个不同的参数。
哈希函数个数K、位数组大小M、加入的字符串数量N的关系可以参考Bloom Filters - the math、Bloom_filter-wikipedia。
错误率越大,所需空间和时间越小,错误率越小,所需时间和空间越大。
常见的几个应用场景:
- cerberus 在收集监控数据的时候,有的系统的监控项量会很大,需要检查一个监控项的名字是否已经被记录到DB过了,如果没有的化就需要写入DB。
- 爬虫过滤已抓到的url就不再抓
- 垃圾邮件过滤。如果用哈希表,每存储一亿个email地址,就需要1.6GB的内存(用哈希表实现的具体方法是将一个email地址对应称一个八字节的信息指纹,然后将这些信息指纹存入哈希表,由于哈希表的存储效率一般只有50%,因此一个email地址需要占用16个字节。一亿个地址大约要1.6GB,即十六亿字节的内存)。因此存贮几十亿个邮件地址可能需要上百GB的内存。而Bloom Filter只需要哈希表 1/8 到 1/4 的大小就能解决同样的问题。
总结
布隆过滤器主要是在回答缓存穿透的时候引出来的,主要作用是过滤和拦截,且需求场景允许错误率。
Counting Bloom Filter
原理
BF为什么不支持删除
按照BF的数据结构,若操作删除,就会造成误判。
什么是Counting Bloom Filter
它将标准的Bloom Filter位数组的每一位扩展为一个小的计数器(Counter),在插入元素时给对应的K(K为哈希函数个数)个Counter的值分别加1,删除元素时给对应的K个Counter的值分别减1.Counting Bloom Filter 通过多占用几倍的存储空间的代价,给 Bloom Filter 增加了删除操作。
Counter 大小的选择
CBF 和 BF 的一个主要不同就是 CBF 用一个Counter 取代了 BF 的一位,那么 Counter 到底取多大比较合适?要考虑空间利用率的问题,从使用的角度来看,当然越大越好,因为Counter越大就能表示越多的信息。但是越大的Counter 就意味着更多的资源占用,而且在很多时候会造成极大的空间浪费。
因此,在选择 Counter 的时候,可以看 Counter 取值的范围多小可以满足需求。
与BF的区别
和 BF 的区别:
- 位数组中位数表示由boolean变成了计算器
- 多了remove的方法进行集合元素的删除
总结
解决了BF的不能删除元素的问题,自身仍有不少的缺陷待完善,比如Counter的引入就会带来很大的资源浪费,CBF 的 FP 还有很大可以降低的空间爱你,因此在实际的使用场景中会有很多CBF的升级版。
比如 SBF(Spectral Bloom Filter)在 CBF 的基础上提出了元素出现频率查询的概念,将CBF的应用扩展到了 multi-set 的领域; dlCBF(d-Left Counting Bloom Filter)利用 d-left hashing 的方法存储 fingerprint,解决哈希表的负载平衡问题; ACBF(Accurate Counting Bloom Filter)通过 offset indexing 的方式将 Counter 数组划分成多个层级,来降低误判率。