- Redis概述
- 数据类型
- 缓存 击穿、雪崩、穿透
- Redis 分布式锁
- Redis 内存淘汰机制
- Redis 事务
- Sentinel 哨兵机制
- Pub/Sub
- Redis 持久化
- 常见问题
- 如果有大量的key需要设置同一时间过期,一般需要注意什么?
- 那你使用过Redis分布式锁么,它是什么回事?
- 如果在setnx之后执行expire之前进程意外crash或者要重启维护了,那会怎么样?
- 假如Redis里面有1亿个key,其中有10w个key是以某个固定的已知的前缀开头的,如何将它们全部找出来?
- 如果这个redis正在给线上的业务提供服务,那使用keys指令会有什么问题?
- 使用过Redis做异步队列么,你是怎么用的?
- 可不可以不用sleep呢?
- 如果对方接着追问能不能生产一次消费多次呢?
- pub/sub有什么缺点?
- Redis如何实现延时队列?
- 如果突然机器掉电会怎样?
- Pipeline有什么好处,为什么要用pipeline?
- Redis的同步机制了解么?
- 是否使用过Redis集群,集群的高可用怎么保证,集群的原理是什么?
- 单机会有瓶颈,那你们是怎么解决这个瓶颈的?
- 如何解决redis的并发竞争key问题
Redis概述
Redis采用的是基于内存的采用的是单进程单线程模型的 KV 数据库,由C语言编写,官方提供的数据是可以达到100000+的QPS(每秒内查询次数)。
完全基于内存,绝大部分请求是纯粹的内存操作,非常快速。它的,数据存在内存中,类似于HashMap,HashMap的优势就是查找和操作的时间复杂度都是O(1);
数据结构简单,对数据操作也简单,Redis中的数据结构是专门进行设计的;
采用单线程,避免了不必要的上下文切换和竞争条件,也不存在多进程或者多线程导致的切换而消耗 CPU,不用去考虑各种锁的问题,不存在加锁释放锁操作,没有因为可能出现死锁而导致的性能消耗;
使用多路I/O复用模型,非阻塞IO;
使用底层模型不同,它们之间底层实现方式以及与客户端之间通信的应用协议不一样,Redis直接自己构建了VM 机制 ,因为一般的系统调用系统函数的话,会浪费一定的时间去移动和请求;
数据类型
字符串 String
String 类型是 Redis 最简单的数据类型,使用方式就是当作像 Python 字典一样 GET 和 SET:
SET some_key
GET some_key
EXISTS some_key
不同的是,Redis 提供 Expire 功能,通过设置过期时间,定时批量删除已过期的 key:
EXPIRE some_key time_in_second
TTL some_key
EXPIRE 指定一个键(不管值是 String 还是其它类型)多长时间之后过期,EXPIREAT 指定到什么时候过期。
列表 List
Redis 的 List 类型就是一个键对应一个双向链表结构的 List,所以索引第 N 个元素的效率是 O(N)
List 的操作主要就是增(LINSERT、LPUSH)删(LPOP、LREM)取(LPOP、LRANGE)以及取多个连续元素 (LRANGE)等等。
由于 Redis 的 List 是一个双向链表结构,因此上面提到的 LPOP、LPUSH 命令存在对应的 RPOP、RPUSH 命令,效率都是 O(1)
。
集合 Set
Set 是集合,和 List 相比 Set 无序不允许重复,Set 的存储是通过散列而非链表实现,取效率上永远是 O(1)
相关的操作有 SPOP,SREM(虽然 SREM 的效率是 O(N),但 N 是同时删除的元素个数, 因此 SREM 一个元素的效率是 O(1))。
除普通的增删操作外,Set 还支持交集(SINTER、SINTERSTORE)、并集(SUNION、SUNIONSTORE)和差集 (SDIFF、SDIFFSTORE )
这样的数学操作,其中差集的时间复杂度是 O(N),交集、并集的复杂度是 O(N*M),SxxxSTORE 命令的作用是将集合运算的结果存储起来,并不影响时间复杂度。
有序集 Sorted Set
Redis 还支持有序集,通常简写为 Zset。Zset 中每个键对应一个集合,集合中每个元素都对应一个权, 理解起来类似于每个 Zset 都是一个键名对应一个有序字典。
ZADD key value1 weight1 value2 weight2
ZCARD value1
ZCARD value2
ZCARD value3
Zset 同样支持 ZUNIONSTORE、ZINTERSTORE,但并不支持 ZUNION、ZINTER。 Zset 也不支持差集操作。
相对于 Set,Zset 的“键-值-权”的结构可能更类似散列表。
散列表 Hash
对于众多类似字段通常是建议存储在 Hash 结构中的,而不是平铺在整个数据库中, 这样避免了键名重复部分对内存的占用,也就增加了 Redis 的存储容量。 这样做的缺点是值只能是简单类型,Redis 不支持 hash 结构的嵌套。
HSET key name1 value1
HSET key name2 value2
HGET key name1
HGETALL key
HVALS key
地理位置信息 GEO
地理位置信息(GEO)存储是 Redis 3.2 以后新支持的结构,每个 GEO 类型的键都包含了一系列点, 每个点信息由经纬和名字(member)度组成,存储结构类似于有序集 Zset,因此继承其一部分命令。
限制:Redis GEO 仅支持经度 -180~180 度,纬度 85.05112878~85.05112878 度之间的点。
GEOADD china longitude1 latitude1 beijing longitude2 latitude2 shanghai
GEODIST china beijing shanghai
GEO 相关目前支持增(GEOADD)、查(GEOPOS)、范围查询(GEORADIUS、GEORADIUSBYMEMBER)、 距离查询(GEODIS)以及批量查询(GEOHASH)。
缓存 击穿、雪崩、穿透
Redis雪崩
一般缓存都是定时任务去刷新,或者是查不到之后去更新的,定时任务刷新就有一个问题。
举个简单的例子:如果所有首页的Key失效时间都是12小时,中午12点刷新的,我零点有个秒杀活动大量用户涌入
假设当时每秒 6000 个请求,本来缓存在可以扛住每秒 5000 个请求,但是缓存当时所有的Key都失效了。
此时 1 秒 6000 个请求全部落数据库,数据库必然扛不住,它会报一下警,真实情况可能DBA都没反应过来就直接挂了。
此时,如果没用什么特别的方案来处理这个故障,DBA 很着急,重启数据库,但是数据库立马又被新的流量给打死了。这就是我理解的缓存雪崩。
同一时间大面积失效,那一瞬间Redis跟没有一样
但是这个数量级别的请求直接打到数据库几乎是灾难性的
这种情况咋整?你都是怎么去应对的?
处理缓存雪崩简单,在批量往Redis存数据的时候,把每个Key的失效时间都加个随机值就好了,这样可以保证数据不会在同一时间大面积失效,我相信,Redis这点流量还是顶得住的。
setRedis(Key,value,time + Math.random() * 10000);
如果Redis是集群部署,将热点数据均匀分布在不同的Redis库中也能避免全部失效的问题,不过本渣我在生产环境中操作集群的时候,单个服务都是对应的单个Redis分片,是为了方便数据的管理,但是也同样有了可能会失效这样的弊端,失效时间随机是个好策略。
或者设置热点数据永远不过期,有更新操作就更新缓存就好了(比如运维更新了首页商品,那你刷下缓存就完事了,不要设置过期时间),电商首页的数据也可以用这个操作,保险。
Redis缓存穿透
缓存穿透是指缓存和数据库中都没有的数据,而用户不断发起请求
我们数据库的 id 都是1开始自增上去的,如发起为id值为 -1 的数据或 id 为特别大不存在的数据。
这时的用户很可能是攻击者,攻击会导致数据库压力过大,严重会击垮数据库。
如果不对参数做校验,数据库id都是大于0的,一直用小于0的参数去请求
每次都能绕开Redis直接打到数据库,数据库也查不到,每次都这样,并发高就容易崩。
如何解决缓存穿透
可以在接口层增加校验,比如用户鉴权校验,参数做校验,不合法的参数直接代码Return
比如:id 做基础校验,id <=0的直接拦截等。
这里我想提的一点就是,我们在开发程序的时候都要有一颗“不信任”的心,就是不要相信任何调用方,比如你提供了API接口出去,你有这几个参数,那我觉得作为被调用方,任何可能的参数情况都应该被考虑到,做校验,因为你不相信调用你的人,你不知道他会传什么参数给你。
从缓存取不到的数据,在数据库中也没有取到,这时也可以将对应Key的Value对写为null、位置错误、稍后重试这样的值
具体问产品,或者看具体的场景,缓存有效时间可以设置短点,如30秒(设置太长会导致正常情况也没法使用)。
这样可以防止攻击用户反复用同一个id暴力攻击,但是我们要知道正常用户是不会在单秒内发起这么多次请求的
网关层Nginx有配置项,可以让运维大大对单个IP每秒访问次数超出阈值的IP都拉黑。
Redis还有一个高级用法布隆过滤器 Bloom Filter, 也能很好的防止缓存穿透的发生
原理也很简单就是利用高效的数据结构和算法快速判断出你这个Key是否在数据库中存在
不存在就return,存在你就去查了DB刷新KV再return。
Redis缓存击穿
跟缓存雪崩有点像,但是又有一点不一样
缓存雪崩是因为大面积的缓存失效,打崩了DB
而缓存击穿不同的是缓存击穿是指一个Key非常热点,在不停的扛着大并发,大并发集中对这一个点进行访问
当这个Key在失效的瞬间,持续的大并发就穿破缓存,直接请求数据库,就像在一个完好无损的桶上凿开了一个洞。
解决方案
设置热点数据永远不过期。
或者加上互斥锁
Redis 分布式锁
为了保证数据的最终一致性 会用到分布式锁。
实现思想: 1) 获取锁的时候,使用setnx(SET if Not eXists)加锁,并使用expire命令为锁添加一个超时时间,超过该时间则自动释放锁,锁的value值为一个随机生成的UUID,通过此在释放锁的时候进行判断。
2) 获取锁的时候还设置一个获取的超时时间,若超过这个时间则放弃获取锁。
3) 释放锁的时候,通过UUID判断是不是该锁,若是该锁,则执行delete进行锁释放
如果在setnx之后执行expire之前进程意外crash或者要重启维护了,那会怎么样?
这个锁就永远得不到释放了。可以使用带参数的set指令同时设置setnx和expire。
set key value [EX second] [PX millisceonds] [NX|XX]
使用redis的SETNX实现分布式锁,多个进程执行以下Redis命令:
SETNX lock.id <current Unix time + lock timeout + 1>
SETNX是将 key 的值设为 value,当且仅当 key 不存在。若给定的 key 已经存在,则 SETNX 不做任何动作。
-
返回1,说明该进程获得锁,SETNX将键 lock.id 的值设置为锁的超时时间,当前时间 +加上锁的有效时间。
-
返回0,说明其他进程已经获得了锁,进程不能进入临界区。进程可以在一个循环中不断地尝试 SETNX 操作,以获得锁。
Redis 内存淘汰机制
Redis的过期策略,是有定期删除+惰性删除两种。
定期删除就是默认100s就随机抽一些设置了过期时间的key,去检查是否过期,过期了就删了。
如果全表扫描,压力过大,可能影响正常的业务
惰性删除,不主动删,只等查询后检查过期与否,过期就删了并不返回,没过期就正常处理。
内存淘汰
官网上给到的内存淘汰机制是以下几个:
-
noeviction:返回错误当内存限制达到并且客户端尝试执行会让更多内存被使用的命令(大部分的写入指令,但DEL和几个例外)
-
allkeys-lru: 尝试回收最少使用的键(LRU),使得新添加的数据有空间存放。
-
volatile-lru: 尝试回收最少使用的键(LRU),但仅限于在过期集合的键,使得新添加的数据有空间存放。
-
allkeys-random: 回收随机的键使得新添加的数据有空间存放。
-
volatile-random: 回收随机的键使得新添加的数据有空间存放,但仅限于在过期集合的键。
-
volatile-ttl: 回收在过期集合的键,并且优先回收存活时间(TTL)较短的键,使得新添加的数据有空间存放。
如果没有键满足回收的前提条件的话,策略volatile-lru, volatile-random以及volatile-ttl就和noeviction 差不多了。
Redis 事务
1)Redis 的事务是通过 MULTI,EXEC,DISCARD 和 WATCH 这四个命令来完成的。
2)Redis 的单个命令都是原子性的,所以这里确保事务性的对象是命令集合。
3)Redis 将命令集合序列化并确保处于同一事务的命令集合连续且不被打断的执行
4)Redis不支持回滚操作
当客户端处于非事务状态下时, 所有发送给服务器端的命令都会立即被服务器执行
但是, 当客户端进入事务状态之后, 服务器在收到来自客户端的命令时, 不会立即执行命令, 而是将这些命令全部放进一个事务队列里, 然后返回 QUEUED , 表示命令已入队:
1)MULTI:用于标记事务块的开始。Redis 会将后续的命令逐个放入队列中,然后使用 EXEC 命令原子化地执行这个命令序列。
127.0.0.1:6379> multi # 开启事务
OK
127.0.0.1:6379> set k1 alan # 设置 k1
QUEUED # k1 加入队列
127.0.0.1:6379> set k2 tom # 设置 k2
QUEUED # k2 加入队列
2)EXEC:在一个事务中执行所有先前放入队列的命令,然后恢复正常的连接状态。
127.0.0.1:6379> exec
1) OK
2) OK
使用 EXEC 命令原子化地执行这个命令序列,刚刚我们设置了 k1 和 k2 两条命令,执行EXEC 命令后,给我们反馈了两个 OK,说明上述两条命令全部执行成功。
3)DISCARD:清除所有先前在一个事务中放入队列的命令,然后恢复正常的连接状态。
127.0.0.1:6379> multi
OK
127.0.0.1:6379> set k3 lucy
QUEUED
127.0.0.1:6379> set k4 jack
QUEUED
127.0.0.1:6379> discard
OK
4) WATCH:当某个事务需要按条件执行时,就要使用这个命令将给定的键设置为受监控的状态。注意:使用该命令可以实现 Redis 的乐观锁。
当被监控的数据发生改变后,开启的事务执行是无法成功的,只有被监控的数据不发生变化,事务才能正常执行。
5) Redis不支持回滚 为什么 Redis 不支持事务回滚?
(1)大多数事务失败是因为语法错误或者类型错误,这两种错误,在开发阶段都是可以预见的。
(2)Redis 为了性能方面就忽略了事务回滚。
Sentinel 哨兵机制
其实 Redis 主从复制有一个很大的缺点就是没有办法对 master 进行动态选举
(当 master 挂掉后,会通过一定的机制,从 slave 中选举出一个新的 master),需要使用 Sentinel 机制完成动态选举
1)Sentinel(哨兵) 进程是用于监控 Redis 集群中 Master 主服务器工作的状态
2)在 Master 主服务器发生故障的时候,可以实现 Master 和 Slave 服务器的切换,保证系统的高可用(High Availability)
哨兵的作用
1)监控(Monitoring):哨兵(sentinel) 会不断地检查你的 Master 和 Slave 是否运作正常。
2)提醒(Notification):当被监控的某个Redis节点出现问题时, 哨兵(sentinel) 可以通过 API 向管理员或者其他应用程序发送通知。(使用较少)
3)自动故障迁移(Automatic failover):当一个 Master 不能正常工作时,哨兵(sentinel) 会开始一次自动故障迁移操作。具体操作如下:
(1)它会将失效 Master 的其中一个 Slave 升级为新的 Master, 并让失效 Master 的其他Slave 改为复制新的 Master。
(2)当客户端试图连接失效的 Master 时,集群也会向客户端返回新 Master 的地址,使得集群可以使用现在的 Master 替换失效 Master。
(3)Master 和 Slave 服务器切换后,Master 的 redis.conf、Slave 的 redis.conf 和sentinel.conf 的配置文件的内容都会发生相应的改变,即 Master 主服务器的 redis.conf 配置文件中会多一行 slaveof 的配置,sentinel.conf 的监控目标会随之调换。
哨兵的工作方式
哨兵进程的工作方式
-
每个 Sentinel(哨兵)进程以每秒钟一次的频率向整个集群中的 Master 主服务器,Slave 从服务器以及其他 Sentinel(哨兵)进程发送一个 PING命令。(此处我们还没有讲到集群,下一章节就会讲到,这一点并不影响我们模拟哨兵机制)
-
如果一个实例(instance)距离最后一次有效回复 PING 命令的时间超过 down-after-milliseconds 选项所指定的值, 则这个实例会被 Sentinel(哨兵)进程标记为主观下线(SDOWN)。
-
如果一个 Master 主服务器被标记为主观下线(SDOWN),则正在监视这个 Master 主服务器的所有 Sentinel(哨兵)进程要以每秒一次的频率确认 Master 主服务器的确进入了主观下线状态。
-
当有足够数量的 Sentinel(哨兵)进程(大于等于配置文件指定的值)在指定的时间范围内确认 Master 主服务器进入了主观下线状态(SDOWN), 则 Master 主服务器会被标记为客观下线(ODOWN)。
-
在一般情况下, 每个 Sentinel(哨兵)进程会以每 10 秒一次的频率向集群中的所有Master 主服务器、Slave 从服务器发送 INFO 命令。
-
当 Master 主服务器被 Sentinel(哨兵)进程标记为客观下线(ODOWN)时,Sentinel(哨兵)进程向下线的 Master 主服务器的所有 Slave 从服务器发送 INFO 命令的频率会从 10 秒一次改为每秒一次。
-
若没有足够数量的 Sentinel(哨兵)进程同意 Master 主服务器下线, Master 主服务器的客观下线状态就会被移除。若 Master 主服务器重新向 Sentinel(哨兵)进程发送 PING 命令返回有效回复,Master 主服务器的主观下线状态就会被移除。
哨兵必须用三个实例去保证自己的健壮性的,哨兵+主从并不能保证数据不丢失,但是可以保证集群的高可用。
M1所在的机器挂了,哨兵还有两个,两个人一看他不是挂了嘛,那我们就选举一个出来执行故障转移不就好了。
哨兵组件的主要功能:
-
集群监控:负责监控 Redis master 和 slave 进程是否正常工作。
-
消息通知:如果某个 Redis 实例有故障,那么哨兵负责发送消息作为报警通知给管理员。
-
故障转移:如果 master node 挂掉了,会自动转移到 slave node 上。
-
配置中心:如果故障转移发生了,通知 client 客户端新的 master 地址。
1.主从复制解决了读请求的分担,从节点下线,会使得读请求能力有所下降
2.Master只有一个,写请求是一个单点问题
3.Sentinel会在Master下线后自动执行Failover操作,提升一台slave为Master,并让其他Slaves重新成为新的Master的Slaves
4.主从复制+哨兵Sentinel只解决了 读性能 和 高可用 问题,但是没有解决写性能问题(一个主节点存在瓶颈问题)
Pub/Sub
Redis 的 SUBSCRIBE
命令可以让客户端订阅任意数量的频道, 每当有新信息发送到被订阅的频道时, 信息就会被发送给所有订阅指定频道的客户端。
作为例子, 下图展示了频道 channel1 , 以及订阅这个频道的三个客户端 —— client2 、 client5 和 client1 之间的关系:
当有新消息通过 PUBLISH
命令发送给频道 channel1 时, 这个消息就会被发送给订阅它的三个客户端:
实现原理
订阅频道
每个 Redis 服务器进程都维持着一个表示服务器状态的 redis.h/redisServer 结构, 结构的 pubsub_channels 属性是一个字典, 这个字典就用于保存订阅频道的信息:
struct redisServer {
// ...
dict *pubsub_channels;
// ...
};
其中,字典的键为正在被订阅的频道, 而字典的值则是一个链表, 链表中保存了所有订阅这个频道的客户端。
比如说,在下图展示的这个 pubsub_channels 示例中, client2 、 client5 和 client1 就订阅了 channel1 , 而其他频道也分别被别的客户端所订阅:
当客户端调用 SUBSCRIBE 命令时, 程序就将客户端和要订阅的频道在 pubsub_channels 字典中关联起来。
举个例子,如果客户端 client10086 执行命令 SUBSCRIBE channel1 channel2 channel3 ,那么前面展示的 pubsub_channels 将变成下面这个样子:
通过 pubsub_channels 字典, 程序只要检查某个频道是否为字典的键, 就可以知道该频道是否正在被客户端订阅; 只要取出某个键的值, 就可以得到所有订阅该频道的客户端的信息。
发布推送
了解了 pubsub_channels 字典的结构之后, 解释 PUBLISH 命令的实现就非常简单了:
当调用 PUBLISH channel message 命令, 程序首先根据 channel 定位到字典的键, 然后将信息发送给字典值链表中的所有客户端。
比如说,对于以下这个 pubsub_channels 实例, 如果某个客户端执行命令 PUBLISH channel1 “hello moto”
那么 client2 、 client5 和 client1 三个客户端都将接收到 “hello moto” 信息:
Redis 持久化
持久化是Redis高可用中比较重要的一个环节,因为Redis数据在内存的特性,持久化必须得有,持久化是有两种方式的。
-
RDB:RDB 持久化机制,是对 Redis 中的数据执行周期性的持久化。
-
AOF:AOF 机制对每条写入命令作为日志,以 append-only 的模式写入一个日志文件中,因为这个模式是只追加的方式,所以没有任何磁盘寻址的开销,所以很快,有点像Mysql中的binlog。
两种方式都可以把Redis内存中的数据持久化到磁盘上,然后再将这些数据备份到别的地方去,RDB更适合做冷备,AOF更适合做热备
比如我杭州的某电商公司有这两个数据,我备份一份到我杭州的节点,再备份一个到上海的,就算发生无法避免的自然灾害,也不会两个地方都一起挂
tip:两种机制全部开启的时候,Redis在重启的时候会默认使用AOF去重新构建数据,因为AOF的数据是比RDB更完整的。
RDB做镜像全量持久化,AOF做增量持久化。
因为RDB会耗费较长时间,不够实时,在停机的时候会导致大量丢失数据,所以需要AOF来配合使用。
在redis实例重启时,会使用RDB持久化文件重新构建内存,再使用AOF重放近期的操作指令来实现完整恢复重启之前的状态。
把RDB理解为一整个表全量的数据,AOF理解为每次操作的日志就好
服务器重启的时候先把表的数据全部搞进去,但是他可能不完整,再回放一下日志,数据就完整了。
不过Redis本身的机制是 AOF持久化开启且存在AOF文件时,优先加载AOF文件;
AOF关闭或者AOF文件不存在时,加载RDB文件;
加载AOF/RDB文件后,Redis启动成功; AOF/RDB文件存在错误时,Redis启动失败并打印错误信息
RDB
镜像全量持久化,RDB 持久化机制,是对 Redis 中的数据执行周期性的持久化。
RDB的原理是什么?
fork和cow。
fork是指redis通过创建子进程来进行RDB操作
cow指的是copy on write,子进程创建后,父子进程共享数据段,父进程继续提供读写服务,写脏的页面数据会逐渐和子进程分离开来。
优点
会生成多个数据文件,每个数据文件分别都代表了某一时刻Redis里面的数据,这种方式,有没有觉得很适合做冷备,完整的数据运维设置定时任务,定时同步到远端的服务器,比如阿里的云服务,这样一旦线上挂了,你想恢复多少分钟之前的数据,就去远端拷贝一份之前的数据就好了。
RDB对Redis的性能影响非常小,是因为在同步数据的时候他只是fork了一个子进程去做持久化的,而且他在数据恢复的时候速度比AOF来的快。
缺点
RDB都是快照文件,都是默认五分钟甚至更久的时间才会生成一次,这意味着你这次同步到下次同步这中间五分钟的数据都很可能全部丢失掉。AOF则最多丢一秒的数据,数据完整性上高下立判。
还有就是RDB在生成数据快照的时候,如果文件很大,客户端可能会暂停几毫秒甚至几秒,你公司在做秒杀的时候他刚好在这个时候fork了一个子进程去生成一个大快照,哦豁,出大问题。
AOF
增量持久化,AOF 机制对每条写入命令作为日志,以 append-only 的模式写入一个日志文件中,因为这个模式是只追加的方式,所以没有任何磁盘寻址的开销,所以很快,有点像Mysql中的binlog。
优点
上面提到了,RDB五分钟一次生成快照,但是AOF是一秒一次去通过一个后台的线程fsync操作,那最多丢这一秒的数据。
AOF在对日志文件进行操作的时候是以append-only的方式去写的,他只是追加的方式写数据,自然就少了很多磁盘寻址的开销了,写入性能惊人,文件也不容易破损。
AOF的日志是通过一个叫非常可读的方式记录的,这样的特性就适合做灾难性数据误删除的紧急恢复了,比如公司的实习生通过flushall清空了所有的数据,只要这个时候后台重写还没发生,你马上拷贝一份AOF日志文件,把最后一条flushall命令删了就完事了。
缺点
一样的数据,AOF文件比RDB还要大。
AOF开启后,Redis支持写的QPS会比RDB支持写的要低,每秒都要去异步刷新一次日志(fsync),当然即使这样性能还是很高,我记得ElasticSearch 也是这样的,异步刷新缓存区的数据去持久化
常见问题
如果有大量的key需要设置同一时间过期,一般需要注意什么?
如果大量的key过期时间设置的过于集中,到过期的那个时间点,redis可能会出现短暂的卡顿现象。
严重的话会出现缓存雪崩,我们一般需要在时间上加一个随机值,使得过期时间分散一些。
电商首页经常会使用定时任务刷新缓存,可能大量的数据失效时间都十分集中
如果失效时间一样,又刚好在失效的时间点大量用户涌入,就有可能造成缓存雪崩
那你使用过Redis分布式锁么,它是什么回事?
先拿setnx来争抢锁,抢到之后,再用expire给锁加一个过期时间防止锁忘记了释放。
如果在setnx之后执行expire之前进程意外crash或者要重启维护了,那会怎么样?
set指令有非常复杂的参数,这个应该是可以同时把setnx和expire合成一条指令来用的!
假如Redis里面有1亿个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有什么缺点?
在消费者下线的情况下,生产的消息会丢失,得使用专业的消息队列如RocketMQ等。
Redis如何实现延时队列?
使用sortedset,拿时间戳作为score,消息内容作为key调用zadd来生产消息
消费者用zrangebyscore指令获取N秒之前的数据轮询进行处理。
如果突然机器掉电会怎样?
取决于AOF日志sync属性的配置,如果不要求性能,在每条写指令时都sync一下磁盘,就不会丢失数据。
但是在高性能的要求下每次都sync是不现实的,一般都使用定时sync,比如1s1次,这个时候最多就会丢失1s的数据。
Pipeline有什么好处,为什么要用pipeline?
可以将多次IO往返的时间缩减为一次,前提是pipeline执行的指令之间没有因果相关性。使用redis-benchmark进行压测的时候可以发现影响redis的QPS峰值的一个重要因素是pipeline批次指令的数目。
Redis的同步机制了解么?
Redis可以使用主从同步,从从同步。
第一次同步时,主节点做一次bgsave,并同时将后续修改操作记录到内存buffer
待完成后将RDB文件全量同步到复制节点,复制节点接受完成后将RDB镜像加载到内存。加载完成后,再通知主节点将期间修改的操作记录同步到复制节点进行重放就完成了同步过程。后续的增量数据通过AOF日志同步即可,有点类似数据库的binlog。
是否使用过Redis集群,集群的高可用怎么保证,集群的原理是什么?
Redis Sentinal着眼于高可用,在master宕机时会自动将slave提升为master,继续提供服务。
Redis Cluster着眼于扩展性,在单个redis内存不足时,使用Cluster进行分片存储。
单机会有瓶颈,那你们是怎么解决这个瓶颈的?
我们用到了集群的部署方式也就是Redis cluster,并且是主从同步读写分离,类似Mysql的主从同步
Redis cluster 支撑 N 个 Redis master node,每个master node都可以挂载多个 slave node。
这样 Redis 就可以横向扩容了。如果你要支撑更大数据量的缓存,那就横向扩容更多的 master 节点,每个 master 节点就能存放更多的数据了。
如何解决redis的并发竞争key问题
这个问题大致就是,同时有多个子系统去set一个key。
(1)如果对这个key操作,不要求顺序
这种情况下,准备一个分布式锁,大家去抢锁,抢到锁就做set操作即可,比较简单。
(2)如果对这个key操作,要求顺序
假设有一个key1,系统A需要将key1设置为valueA,系统B需要将key1设置为valueB,系统C需要将key1设置为valueC.
期望按照key1的value值按照 valueA–>valueB–>valueC的顺序变化。这种时候我们在数据写入数据库的时候,需要保存一个时间戳。假设时间戳如下
系统A key 1 {valueA 3:00}
系统B key 1 {valueB 3:05}
系统C key 1 {valueC 3:10}
那么,假设这会系统B先抢到锁,将key1设置为{valueB 3:05}。接下来系统A抢到锁,发现自己的valueA的时间戳早于缓存中的时间戳,那就不做set操作了。