- Redis 基础
- Redis 持久化
- Redis事务
- 主从复制(Master/Slave)
- 常见面试问题
- 1. Redis为什么这么快?
- 2. 什么是CAP? Redis满足CAP吗?
- 3. 如何解决Redis和数据库的不一致性问题?
- 4. Redis满足ACID吗?
- 5. 哪些情况会触发RDB持久化
- 6. 当数据过期后, redis是如何删除数据的?
- 7. 当redis数据过多, 撑满内存时, redis会发生什么?
- 8. Memcache和Redis的区别
- 9. 如何在海量数据中检索出以某一个字符串开头的key?
- 10. 如何通过redis实现分布式锁?
- 11. 如何避免大量key同时过期导致redis同时清除这些key而出现短暂卡顿的问题?
- 12. Redis的常见应用
- 13. 什么是缓存穿透?
- 14. 什么是缓存击穿?
- 15. 什么是缓存雪崩?
- 16. Redis的SDS和C中字符串相比有什么优势?
- 17. Redis的字典是如何实现的?简述渐进式rehash过程
- 18. 谈谈对Redis的内存回收机制的理解
- 19. 谈谈基于Redis的分布式锁和Redlock算法
Redis 基础
Redis的三个特点
- redis支持数据持久化
- redis不仅支持k-v类型数据, 同时还提供其他数据结构的存储
- redis支持数据备份, 即主从模式的数据备份
Redis的CAP理论
- C:consistency(一致性) : 对某个指定的客户端来说,读操作保证能够返回最新的写操作结果。
- A:availability (可用性) : 非故障的节点在合理的时间内返回合理的响应(不是错误和超时的响应)。
- P:Partition(分区)-tolerence to partition(分区容错性) : 当出现网络分区后,系统能够继续“履行职责”。
CAP理论是说 : 在一个分布式系统(指互相连接并共享数据的节点的集合)中,当涉及读写操作时,只能保证一致性(Consistence)、可用性(Availability)、分区容错性(Partition Tolerance)三者中的两个,另外一个必须被牺牲。
虽然 CAP 理论定义是三个要素中只能取两个,但放到分布式环境下来思考,我们会发现必须选择 P(分区容忍)要素,因为网络本身无法做到 100% 可靠,有可能出故障,所以分区是一个必然的现象。如果我们选择了 CA 而放弃了 P,那么当发生分区现象时,为了保证 C,系统需要禁止写入,当有写入请求时,系统返回 error(例如,当前系统不允许写入),这又和 A 冲突了,因为 A 要求返回 no error 和 no timeout。因此,分布式系统理论上不可能选择 CA 架构,只能选择 CP 或者 AP 架构。
全局命令
- select index
选择index号库, redis默认分为16个库, 编号从0开始.
- DBSIZE
查询当前库中总共有多少个key
- keys 查询表达式
keys * : 查看当前库中所有的key
keys k? : 查询当前库中所有以k开头的key
- exists key
是否存在key
- expire key time
设置指定key的存活时间
- ttl key
查看指定key的剩余存活日期, -1为无过期时间, -2 为不存在此key
- type key
查看指定key的数据类型
- move key index
将指定key-value数据移动到index库中
- Flushall
Redis Flushall 命令用于清空整个 Redis 服务器的数据(删除所有数据库的所有 key )。
- shutdown
关闭redis, 此时redis 会自动将数据备份到磁盘.
- save
字符串(string)
- 字符串结构使用非常广泛,一个常见的用途就是缓存用户信息。我们将用户信息结构体使用 JSON 序列化成字符串,然后将序列化后的字符串塞进 Redis 来缓存。同样,取用户信息会经过一次反序列化的过程。
- Redis 的字符串是动态字符串,是可以修改的字符串,内部结构实现上类似于 Java 的ArrayList,采用预分配冗余空间的方式来减少内存的频繁分配. 当字符串长度小于 1M 时,扩容都是加倍现有的空间,如果超过 1M,扩容时一次只会多扩 1M 的空间。需要注意的是字符串最大长度为 512M。
常用命令
单一操作
set 增 改
set name hanmeimei
get 查
get name
exists 是否存在
exists name
del 删
del name
append key 向指定key的value中追加内容
strlen key 查看指定key的value长度
批量操作
mset 批量增
mset name lihua age 16 gender 男
mget 批量查
mget name age gender
其他
expire 过期
expire gender 5 # 5s后过期
setex 等价于 set + expire
setex name 5 zhangsan # 设置姓名为张三, 5秒后失效
setnx 判空后创建
setnx name mazi # 如果name不存在则创建, 如果已存在则不做任何操作
incre 如果值为一个整数, 则可以进行自增操作
incr age # 对age的值自增1
incrby 指定增加多少, 可为负值
incrby age 10 # 增加10
incrby age -1 # 减1
列表(list)
- Redis 的列表相当于 Java 语言里面的 LinkedList,注意它是链表而不是数组。这意味着
list 的插入和删除操作非常快,时间复杂度为 O(1),但是索引定位很慢,时间复杂度为
O(n) - 当列表弹出了最后一个元素之后,该数据结构自动被删除,内存被回收。
相关命令
rpush 从右边插入元素
rpush lang python java golang php
rpop 从右边弹出元素
rpop lang
lpush 从左边插入元素 lpop 从左边弹出元素
lrange key index1 index2 查看指定key的指定范围内的值 lrange list1 0 -1 从左边依次取出list1中的所有元素 lpush lrange 类似 栈 rpush lrange 类似 队列
llen 查看列表元素个数
llen lang
lindex 相当于 Java 链表的 get(int index)方法,它需要对链表进行遍历
lindex lang 2 # 取lang列表中的第三个元素(索引从0开始)
ltrim 其后跟的两个参数 start_index 和 end_index 定义了一个区间(闭区间),在这个区间内的值,要保留,区间之外统统砍掉。
ltrim lang 1 2 # 保留lang列表的第二个和第三个元素, 其他删除
ltrim lang 1 -1 # 保留第一个元素一直到末尾元素
ltrim lang 0 -1 # 区间长度为负数, 相当于清空列表
使用list模拟队列(右边进左边出)
rpush lang python java golang php # 从右入队
lpop # 左边出
使用list模拟栈(右边进右边出)
rpush lang python java golang php # 从右入队
rpop # 右边出
快速链表(quicklist)
如果再深入一点,你会发现 Redis 底层存储的还不是一个简单的 linkedlist,而是称之为快速链表 quicklist 的一个结构 。
首先在列表元素较少的情况下会使用一块连续的内存存储,这个结构是 ziplist,也即是压缩列表。它将所有的元素紧挨着一起存储,分配的是一块连续的内存。当数据量比较多的时候才会改成 quicklist。因为普通的链表需要的附加指针空间太大,会比较浪费空间,而且会加重内存的碎片化。比如这个列表里存的只是 int 类型的数据,结构上还需要两个额外的指针 prev 和 next 。所以 Redis 将链表和 ziplist 结合起来组成了 quicklist。也就是将多个ziplist 使用双向指针串起来使用。这样既满足了快速的插入删除性能,又不会出现太大的空间冗余 。
字典(hash)
Redis 的字典相当于 Java 语言里面的 HashMap,它是无序字典。内部实现结构上同Java 的 HashMap 也是一致的,同样的数组 + 链表二维结构。第一维 hash 的数组位置碰撞时,就会将碰撞的元素使用链表串接起来。
不同的是, Redis 的字典的值只能是字符串,另外它们 rehash 的方式不一样,因为Java 的 HashMap 在字典很大时, rehash 是个耗时的操作,需要一次性全部 rehash。 Redis为了高性能,不能堵塞服务,所以采用了渐进式 rehash 策略。
常用命令
hset
hset books java "think in java" # 命令行的字符串如果包含空格,要用引号括起来
hset books golang "concurrency in go"
hset books python "python cookbook"
hgetall
hgetall books # entries(), key 和 value 间隔出现
1) "java"
2) "think in java"
3) "golang"
4) "concurrency in go"
5) "python"
6) "python cookbook"
hlen
hlen books # 3
hget
hget books java
"think in java"
hmget key field [ field … ] 批量get指定key的某些字段
hgetall key 获取指定key的所有字段的键和值
hmset # 批量set
hmset books java "effective java" python "learning python" golang "modern golang
programming"
hdel key field 删除指定key的指定字段
hexists key field 查询指定key中是否存在指定字段
hkeys key 查询指定hash中的所有key
hvals key 查询指定hash中的所有value
hincrby key field increment 对指定hash的指定字段进行自增自减操作
hsetnx key field value 指定字段不存在时添加
集合(set)
Redis 的集合相当于 Java 语言里面的 HashSet,它内部的键值对是无序的唯一的。它的内部实现相当于一个特殊的字典,字典中所有的 value 都是一个值 NULL。
当集合中最后一个元素移除之后,数据结构自动删除,内存被回收。 set 结构可以用来存储活动中奖的用户 ID,因为有去重功能,可以保证同一个用户不会中奖两次。
常用命令
sadd
sadd books python
sadd books python # 重复,不会添加
sadd books java golang # 批量添加
smembers # 获取set值
smembers books # 注意顺序,和插入的并不一致,因为 set 是无序的
1) "java"
2) "python"
3) "golang"
sismember # 查询指定set中是否是否存在指定value
sismember books java # 查询某个 value 是否存在,相当于 contains(o)
scard # 获取set长度
scard books # 获取长度相当于 count()
spop # 弹出一个值
spop books # 弹出一个, 随机出
srandmember key count 随机抽取count个值出来
smove set1 set2 value 将set1中的value移到set2中
sdiff set1 set2 求差集, 即set1 - set2
sinter set1 set2 求交集, 即set1 ∩ set2
sunion set1 set2 求并集 即set1 ∪ set2
有序集合(zset)
zset 可能是 Redis 提供的最为特色的数据结构,它也是在面试中面试官最爱问的数据结构。它类似于 Java 的SortedSet 和 HashMap 的结合体,一方面它是一个 set,保证了内部value 的唯一性,另一方面它可以给每个 value 赋予一个double类型的 score,代表这个 value 的排序权重。它的内部实现用的是一种叫着「跳跃列表」的数据结构。
zset 中最后一个 value 被移除后,数据结构自动删除,内存被回收。 zset 可以用来存粉丝列表, value 值是粉丝的用户 ID, score 是关注时间。我们可以对粉丝列表按关注时间进行排序。
zset 还可以用来存储学生的成绩, value 值是学生的 ID, score 是他的考试成绩。我们可以对成绩按分数进行排序就可以得到他的名次。
常用命令
zadd
zadd books 9.0 "think in java"
zadd books 8.9 "java concurrency" 8.6 "java cookbook" # 批量添加
zrange books 0 -1 # 按 score 排序列出,参数区间为排名范围 -1代表最后一个
zrange books 0 -1 # 按 score 排序列出, score从低到高
1) "java cookbook"
2) "java concurrency"
3) "think in java"
zrevrange
zrevrange books 0 -1 # 按 score 逆序列出, score从高到低
1) "think in java"
2) "java concurrency"
3) "java cookbook"
zcard
zcard books # 相当于 count()
(integer) 3
zscore
zscore books "java concurrency" # 获取指定 value 的 score
"8.9000000000000004" # 内部 score 使用 double 类型进行存储,所以存在小数点精度问题
zrank
zrank books "java concurrency" # 排名, 从0开始
(integer) 1
zrangebyscore books 0 8.91 # 根据分值区间遍历 zset
zrangebyscore books 0 8.91
1) "java cookbook"
2) "java concurrency"
zrangebyscore
zrangebyscore books -inf 8.91 withscores # 根据分值区间 (-∞, 8.91] 遍历 zset,同时返
回分值。 inf 代表 infinite,无穷大的意思。
1) "java cookbook"
2) "8.5999999999999996"
3) "java concurrency"
4) "8.9000000000000004"
limit index count 类似于数据库的limit分页操作
withscores 带分数输出
zrem # 删除
zrem books "java concurrency" # 删除 value
(integer) 1
容器型数据结构的通用规则
list/set/hash/zset 这四种数据结构是容器型数据结构,它们共享下面两条通用规则:
- create if not exists
如果容器不存在,那就创建一个,再进行操作。比如 rpush 操作刚开始是没有列表的,Redis 就会自动创建一个,然后再 rpush 进去新元素。
- drop if no elements
如果容器里元素没有了,那么立即删除元素,释放内存。这意味着 lpop 操作到最后一个元素,列表就消失了。
关于过期时间
Redis 所有的数据结构都可以设置过期时间,时间到了, Redis 会自动删除相应的对象。需要注意的是过期是以对象为单位,比如一个 hash 结构的过期是整个 hash 对象的过期,而不是其中的某个子 key。
还有一个需要特别注意的地方是如果一个字符串已经设置了过期时间,然后你调用了set 方法修改了它,它的过期时间会消失。
Redis 持久化
1. RDB (Redis DataBase)
在指定时间间隔内将内存中的数据集快照写入磁盘, 也就是行话讲的Snapshot快照, 它恢复时是将快照文件直接读到内存中。快照文件一般为dump.rdb
快照策略默认为:
# 900s内至少达到一条修改数据的命令, 建议只保留这一条, 下面两条可去除
save 900 1
# 300s内至少达至10条写命令
save 300 10
# 60s内至少达到10000条写命令
save 60 10000
执行SHUTDOWN命令会触发RDB机制, 但如果是意外宕机情况, 并不会触发RDB持久化机制, 如意外断电
当达到触发条件时,会forks一个子进程进行数据同步。不过最好不要通过这方式来触发RDB持久化,因为设置触发的时间太短,则容易频繁写入rdb文件,影响服务器性能,时间设置太长则会造成数据丢失。
当进行RDB持久化时,redis会fork一个子进程来完成备份操作, fork是系统调用,fork用于创建子进程,当前进程调用fork()
,会创建一个跟当前进程完全相同的子进程(除了pid)。如果按传统的做法,会直接将父进程的数据拷贝到子进程中,拷贝完之后,父进程和子进程之间的数据段和堆栈是相互独立的。
但是redis调用fork时会使用COW(copy on write)写时复制机制, 原理也很简单:
- fork创建出的子进程,与父进程共享内存空间。也就是说,如果父进程不对内存空间进行写入操作的话,内存空间中的数据并不会复制给子进程,这样创建子进程的速度就很快了!(不用复制,直接引用父进程的物理空间)。只有当其中的某一个进程试图对该区域进行写操作时,内核就会在物理存储器中为子进程开辟一个新的物理页面,把需要写的页从父进程复制一份给子进程,然后对新的物理页面进行写操作。这时就是实现了对不同进程的操作而不会产生影响其他的进程,同时也节省了很多的物理存储器。
一句话总结cow : fork出的子进程共享父进程的物理空间,当父子进程有内存写入操作时,read-only内存页发生中断,将触发的异常的内存页复制一份(其余的页还是共享父进程的)。
详解Copy On Write 机制
核心思路:fork一个子进程,只有在父进程发生写操作修改内存数据时,才会真正去分配内存空间,并复制内存数据,而且也只是复制被修改的内存页中的数据,并不是全部内存数据;
- Redis中执行BGSAVE命令生成RDB文件时,本质就是调用Linux中的fork()命令,Linux下的fork()系统调用实现了copy-on-write写时复制;
- fork()是类Unix操作系统上创建线程的主要方法,fork用于创建子进程(等同于当前进程的副本);
- 传统的普通进程复制,会直接将父进程的数据拷贝到子进程中,拷贝完成后,父进程和子进程之间的数据段和堆栈是相互独立的;
- copy-on-write技术,在fork出子进程后,与父进程共享内存空间,两者只是虚拟空间不同,但是其对应的物理空间是同一个;
Linux中CopyOnWrite实现原理
fork()之后,kernel把父进程中所有的内存页的权限都设为read-only,然后子进程的地址空间指向父进程。当父子进程都只读内存时,相安无事。当其中某个进程写内存时,CPU硬件检测到内存页是read-only的,于是触发页异常中断(page-fault),陷入kernel的一个中断例程。中断例程中,kernel就会把触发的异常的页复制一份,于是父子进程各自持有独立的一份。
RDB的优点
- 与AOF方式相比,通过rdb文件恢复数据比较快。
- rdb文件非常紧凑,适合于数据备份。
- 通过RDB进行数据备份,由于使用子进程生成,所以对Redis服务器性能影响较小。
RDB的缺点
- 如果服务器宕机的话,采用RDB的方式会造成某个时段内数据的丢失,比如我们设置10分钟同步一次或5分钟达到1000次写入就同步一次,那么如果还没达到触发条件服务器就死机了,那么这个时间段的数据会丢失。
- 使用save命令会造成服务器阻塞,直接数据同步完成才能接收后续请求。
- 使用bgsave命令在forks子进程时,如果数据量太大,forks的过程也会发生阻塞,另外,forks子进程会耗费内存。
2. AOF (Append Only File)
以日志的形式来记录每一个写操作,将redis执行过的所有写指令记录下来,只许追加文件但不可以改写文件, redis启动之初会读取文件重新执行一遍这些操作,以恢复数据。追加文件一般为appendonly.aof
追加策略:
- Always :每次数据发生变化被立刻记录到磁盘aof文件中,性能较差, 但数据完整性较好
- Everysec : redis默认配置 , 异步操作,每秒记录,如果一秒内宕机,有数据丢失。
- No : 不保存,
- AOF的Rewrite机制, 随着备份内容越来越多, AOF文件持续增长而变得越来越大, 会fork出一条新进程来将文件重写. 也就是将整个内存中的数据用命令方式重写一个aof文件, 替换旧的aof文件, 这点与快照有点类似.
- Rewrite默认触发机制 : Redis会记录上次重写的AOF的大小, 当AOF文件大小是上次AOF的rewrite文件大小的一倍且文件大于64M时触发, 默认64M太小, 实际工作中可能会改成2-3G以上.
Redis默认不开启AOF持久化方式,我们可以在配置文件中开启并进行更加详细的配置,如下面的redis.conf文件:
# 开启aof机制
appendonly yes
# aof文件名
appendfilename "appendonly.aof"
# 写入策略,always表示每个写操作都保存到aof文件中,也可以是everysec或no
appendfsync always
# 默认不重写aof文件
no-appendfsync-on-rewrite no
# 保存目录
dir ~/redis/
AOF的优点
3. 混合持久化
重启Redis时,我们很少使用RDB来恢复内存状态,因为会丢失大量数据。我们通常使用AOF日志回放,但是AOF日志回放速度很慢,这种方式启动会花费很长时间。因此Redis 4.0带来了一个新的持久化选项 — 混合持久化
混合持久化的原理很简单,就是将RDB文件和AOF日志文件存在一起。这里的AOF日志不再是全量日志,而是RDB持久化期间的增量日志。于是Redis重新启动的时候,会先加载RDB文件的内容,然后再重放增量AOF日志文件,重启效率就大大提升了。
Redis事务
为了确保连续多个操作的原子性,一个成熟的数据库通常都会有事务支持, Redis 也不例外。 Redis 的事务使用非常简单,不同于关系数据库,我们无须理解那么多复杂的事务模型,就可以直接使用。不过也正是因为这种简单性,它的事务模型很不严格,这要求我们不能像使用关系数据库的事务一样来使用 Redis。
Redis事务相关命令
- discard 取消事务
- exec 执行所有事务块内的命令
- multi 标记一个事务块的开始
- watch 监视一个或多个key, 如果在事务执行之前, 这些key被其他命令所改动, 那么事务将被打断
- unwatch 取消watch命令对所有key的监视
主从复制(Master/Slave)
即数据更新后根据配置和策略,自动同步到备机的机制,Master以写为主,Slave以读为主。主要功能就是读写分离和容灾恢复。
相关命令
- info replication : 查看当前本机主从信息
- slaveof ip地址 端口 : 将本机设为slave, 绑定到指定ip和端口的“主机”上(也可以是从机, 一主二仆和薪火相传)
- slaveof no one : 反客为主,master挂了之后执行该命令,可将本机升级为master
主从复制原理
两个概念:
- 同步操作用于将从服务器的数据库状态更新至主服务器当前所处的状态。
- 命令传播操作则用于在主服务器的数据库状态被修改,导致主从服务器的数据库状态不一致时,让主从服务器的数据库重新回到一致状态。
- 旧版redis复制原理
- Slave启动成功连接到master后会发送一个sync命令
- Master收到SYNC命令的主服务器执行BGSAVE命令,在后台生成一个RDB文件,并使用一个缓冲区记录从现在开始执行的所有写命令。
- 当主服务器的BGSAVE命令执行完毕,主服务器将生成的RDB文件发送给从服务器,从服务器接收并加载这个RDB文件,将自己的数据库状态更新至主服务器执行BGSAVE命令是的数据库状态。
- 主服务器将记录在缓冲区里面的所有写命令发送给从服务器,从服务器执行这些写命令,将自己的数据库状态更新至主服务器当前所处的状态。
- 在同步操作完毕后,主从服务器两者的数据库状态达到一致,但是如果客户端发送写命令给主服务器时,将导致主从服务器状态不一致。
- 为了让主从服务器数据再次一致,主服务器需要对从服务器执行“命令传播”操作:主服务器会将自己执行的那条写命令,发送给从服务器执行,当从服务器执行了该写命令后,主从服务器将再次回到一致状态。
该复制原理的缺陷:
- 当主从服务器因网络故障断开连接后,从服务器会通过自动重连尝试重新连接上主服务器,当再次连上主服务器时,主服务器中的数据可能已经与从服务器不一致了,此时从服务器会再次发送sync命令要求数据同步,此时主服务器又会返回了一个新的RDB文件,如果RDB文件很大,必将导致严重的内存问题。
- 新版redis复制原理
为了解决旧版的问题,Redis2.8以后引入了PSYNC代替SYNC命令执行复制时的同步操作。PSYNC命令具有完全同步和部分重同步功能。完全同步和旧版的同步是一样的。部分重同步主要是用于断线后的重复制情况:当从服务器断开重新连接后,如果条件允许,主服务器将断开期间的写命令发给从服务器,从服务器执行断开期间的写命令完成同步,将数据库更新到和主服务器一致。
部分重同步的实现
- 主服务器的复制偏移量和从服务器的复制偏移量。
- 主服务器的复制积压缓冲区(队列,默认1MB)。
- 服务器的允许ID。
主服务器和从服务器都会维护一个复制偏移量,主要是用于对比复制的执行结果。例如主从服务器的复制偏移量均为1000,当主服务器完成了3个写命令后,主服务器偏移量为1003,这时候将3个命令给从服务器执行,从服务器执行完毕,复制偏移量也为1003。
如果主从服务器的数据是一致的,那么他们的偏移量也是一致的。
复制积压缓冲区,在主服务器将写命令发给从服务器后,还会写入到复制积压缓冲区,如果执行到了偏移量为1003的时候,从服务器A断开,主服务器继续执行了7个写入命令,这时候主服务器的偏移量为1100,A服务器连接上,请求复制主服务器,这时候主服务器会分情况处理。
1)主服务器不是从服务器之前复制的主服务器(根据服务器ID判断),则执行完全同步。
2)主服务器发现从服务器之前复制的是自己。根据从服务器的偏移量去复制积压缓冲区中看1004-1100命令是否依然存在,如果存在将1004-1100偏移量的命令返回给A从服务器执行。如果不存在,只能执行完全同步恢复数据一致了。
哨兵模式
哨兵模式就是主从复制反客为主的自动版,能够在后台监控主机是否出现故障,如果主服务器发生故障,则会根据投票数量自动从slave中选出得票最高的从服务器转换为主服务器。当下线的主服务器再次上线时,会出现谋权篡位的情况,即当前恢复的主服务器会转变成从服务器,自动跟随上一步选出来的主服务器。
实现方式:
编写 sentinel.conf配置文件,内容如下:
# 配置监听的主服务器,这里sentinel monitor代表监控,mymaster代表服务器的名称,可以自定义,192.168.11.128代表监控的主服务器,6379代表端口,
# 2代表只有两个或两个以上的哨兵认为主服务器不可用的时候,才会进行failover操作。
sentinel monitor mymaster 192.168.11.128 6379 2
然后执行命令:redis-sentinel sentinel.conf 即可开启哨兵模式。
常见面试问题
1. Redis为什么这么快?
- 内存型数据库,完全基于内存, 绝大部分请求是纯粹的内存操作, 执行效率高, 读写快
- key-value型nosql数据库,数据结构简单, 对数据的操作也简单, 比关系型数据库快
- 采用的是单线程(处理网络请求是单线程的),省去了线程上下文切换和锁竞争。即不存在多进程或者多线程导致的切换而消耗 CPU,不用去考虑各种锁的问题,不存在加锁释放锁操作,没有因为可能出现死锁而导致的性能消耗;
- 采用了多路IO复用模型,非阻塞IO(NIO)
2. 什么是CAP? Redis满足CAP吗?
CAP理论是说 : 在一个分布式系统(指互相连接并共享数据的节点的集合)中,当涉及读写操作时,只能保证一致性(Consistence)、可用性(Availability)、分区容错性(Partition Tolerance)三者中的两个,另外一个必须被牺牲。
虽然 CAP 理论定义是三个要素中只能取两个,但放到分布式环境下来思考,我们会发现必须选择 P(分区容忍)要素,因为网络本身无法做到 100% 可靠,有可能出故障,所以分区是一个必然的现象。如果我们选择了 CA 而放弃了 P,那么当发生分区现象时,为了保证 C,系统需要禁止写入,当有写入请求时,系统返回 error(例如,当前系统不允许写入),这又和 A 冲突了,因为 A 要求返回 no error 和 no timeout。因此,分布式系统理论上不可能选择 CA 架构,只能选择 CP 或者 AP 架构。
3. 如何解决Redis和数据库的不一致性问题?
4. Redis满足ACID吗?
5. 哪些情况会触发RDB持久化
答:
- 按照配置文件中的策略, 间隔一段时间触发
- 执行shutdown命名,并且没有开启aof时, 会触发
- 执行save或者bgsave命令时触发
- 执行flushall命令时触发
6. 当数据过期后, redis是如何删除数据的?
7. 当redis数据过多, 撑满内存时, redis会发生什么?
8. Memcache和Redis的区别
答:
- Memcache不支持持久化存储
- Memcache不支持主从同步
9. 如何在海量数据中检索出以某一个字符串开头的key?
答:一般如果数据量不大, 则可以直接使用keys patten指令匹配出所有结果,当数据量过大时,耗时较长,效率较低。此时可以使用scan命令小批量分批次获取数据, scan命令的格式如下:
需要注意的是:scan cursor match pattern count num
如:
scan 0 mathc k1* count 10 // 从头开始取以k1开头的key, 每次取十个
- 改命令返回结果分两部分
- 游标部分
- 数据部分
- cursor是游标, 0表示从头开始, 返回的游标如果是0表示已搜索完一遍, 返回的游标需要作为下次取数据的游标
- 数据部分返回的数据量不一定是count参数指定的数据量
10. 如何通过redis实现分布式锁?
分布式锁是控制分布式系统之间共同访问共享资源的一种锁的实现, 如果不同的系统或同一个系统的不同主机之间共享了某个资源时,往往需要互斥来防止彼此干扰,进而保证一次性.
分布式所需要解决的问题如下 :
- 第一个需要解决的问题是互刺性,任意时刻只能有一个客户端获取锁,不能同时有两个客户端获取到锁 ;
- 第二个需要解决的问题是安全性, 锁只能被持有该锁的客户端删除,不能由其他客户端删除 ;
- 第三个需要解决的问题是死锁, 获取锁的客户端因为某些原因导致宕机而未能释放锁, 其他客户端再也无法获取到该锁而导致的死锁。此时需要有机制来避免这个问题的发生 ;
- 第四个需要解决的是容错, 当部分redis节点宕机的时候,客户端仍然能够获取锁和释放锁。
使用redis实现的方式如下:
setnx key value
expire key seconds
可以使用setnx命令实现的原因是这个指令是原子操作
此时存在的问题是如果setnx 执行成功后, 客户端故障导致没有执行expire 指令, 就会导致锁无法释放, 因此redis2之后可通过以下方式实现这种原子操作 :
set key value seconds NX|XX
# NX : 只在键不存在时,才操作
# XX : 只在键存在时,才操作
11. 如何避免大量key同时过期导致redis同时清除这些key而出现短暂卡顿的问题?
答: 在设置key的过期时间的时候, 可以给每个key加一个小随机值
12. Redis的常见应用
- 字符串应用
- ID生成器
- 计数器
- 散列表HashMap应用
- 用户登录状态
- 用户登录过期期限
- 热帖缓存
- 列表List应用
- 队列
- 栈
- 集合Set应用
- 唯一计数器
- 点赞
- 共同关注
- 推荐关注
- 抽奖
- 有序集合ZSet应用
- 排行榜
- HyperLogLog
- 求基数(去重)
- GEO
- 查找附近的人
- 求两个位置之间的直线距离
- Bitmap
- 白名单
13. 什么是缓存穿透?
原因:出现大量无缓存的数据,导致请求涌向数据库,数据库崩溃,一般是恶意用户故意而为之。
解决方案 :
- 使用布隆过滤器,将数据库中所有数据放入布隆过滤器,一个一定不存在的数据会被 这个bitmap拦截掉
- 从缓存取不到的数据,在数据库中也没有取到,这时也可以将key-value对写为key-null,缓存有效时间可以设置短点,如30秒(设置太长会导致正常情况也没法使用)。这样可以防止攻击用户反复用同一个id暴力攻击
14. 什么是缓存击穿?
原因:某热点key过期,大量包含该热点key的查询涌向数据库,数据库崩溃
15. 什么是缓存雪崩?
原因:大量key同一时间过期,导致大量请求涌向数据库,数据库崩溃
解决方案:
- 缓存数据的过期时间设置随机,防止同一时间大量数据过期现象发生。
16. Redis的SDS和C中字符串相比有什么优势?
- O(1)获取长度: C字符串需要遍历而sds中有len可以直接获得;
- 防止缓冲区溢出bufferoverflow: 当sds需要对字符串进行修改时,首先借助于len和alloc检查空间是否满足修改所需的要求,如果空间不够的话,SDS会自动扩展空间,避免了像C字符串操作中的覆盖情况;
- 有效降低内存分配次数:C字符串在涉及增加或者清除操作时会改变底层数组的大小造成重新分配、sds使用了空间预分配和惰性空间释放机制,说白了就是每次在扩展时是成倍的多分配的,在缩容是也是先留着并不正式归还给OS,这两个机制也是比较好理解的;
- 二进制安全:C语言字符串只能保存ascii码,对于图片、音频等信息无法保存,sds是二进制安全的,写入什么读取就是什么,不做任何过滤和限制;
17. Redis的字典是如何实现的?简述渐进式rehash过程
字典可以基于ziplist(元素数量少时)和hashtable来实现,我们只讨论基于hashtable实现的原理。
hashtable与java中的HashMap基本一致,都是数组+链表的二维结构,不同的是,redis的一个字典中存有两个hashtable,用于渐进式rehash的过程。
Redis的rehash动作并不是一次性完成的,而是分多次、渐进式地完成的,原因在于当哈希表里保存的键值对数量很大时, 一次性将这些键值对全部rehash到ht[1]可能会导致服务器在一段时间内停止服务,这个是无法接受的。
针对这种情况Redis采用了渐进式rehash,过程如下:
- 分配空间,每次数组长度扩展为之前长度的两倍
- 保留新旧两个hashtable,采用一点一点rehash的策略
- 在rehash进行期间,每次对字典执行增删改查操作时,顺带旧哈希表上的一个键值对rehash到新哈希表上,完成后将rehashidx加1,指向下一个需要rehash的键值对。
- 随着字典操作的不断执行,最终ht[0]的所有键值对都会被rehash至ht[1],再将rehashidx属性的值设为-1来表示 rehash操作已完成。
渐进式 rehash的思想在于将rehash键值对所需的计算工作分散到对字典的每个添加、删除、查找和更新操作上,从而避免了集中式rehash而带来的阻塞问题。
18. 谈谈对Redis的内存回收机制的理解
1. 过期键删除:
typedef struct redisDb {
dict *dict; /* The keyspace for this DB */
dict *expires; /* Timeout of keys with a timeout set */
dict *blocking_keys; /* Keys with clients waiting for data (BLPOP)*/
dict *ready_keys; /* Blocked keys that received a PUSH */
dict *watched_keys; /* WATCHED keys for MULTI/EXEC CAS */
int id; /* Database ID */
long long avg_ttl; /* Average TTL, just for stats */
unsigned long expires_cursor; /* Cursor of the active expire cycle. */
list *defrag_later; /* List of key names to attempt to defrag one by one, gradually. */
} redisDb;
Redis中存有一个expires过期字典,设置了过期时间的kv会存在这里,过期键删除有两种策略,分别是定期扫描(主动删除)和惰性删除(被动删除)
- 定期扫描:Redis默认进行10次/s的扫描,但不会遍历expires字典中所有的key,而是采用一种简单策略:
- 从expires字典中随机选取20个key
- 删除20个key中已经过期的key
- 如果过期的key的比例超过1/4,则重复步骤1
- 惰性删除:很简单,当获取键时先查看其是否过期,过期就删除,否则就保留;
2. 内存淘汰
常用的淘汰机制:LRU和LFU
在Redis的配置中有几种淘汰策略可以选择,详细如下:
- noeviction: 当内存不足以容纳新写入数据时,新写入操作会报错;
- allkeys-lru:当内存不足以容纳新写入数据时,在键空间中移除最近最少使用的 key;
- allkeys-random:当内存不足以容纳新写入数据时,在键空间中随机移除某个 key;
- volatile-lru:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,移除最近最少使用的 key;
- volatile-random:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,随机移除某个 key;
- volatile-ttl:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,有更早过期时间的 key 优先移除;
LFU是Redis新增策略:
- volatile-lfu:针对设置了过期时间的键
- allkeys-lfu:所有键一视同仁
19. 谈谈基于Redis的分布式锁和Redlock算法
最初分布式锁借助于setnx和expire命令,但是这两个命令不是原子操作,如果执行setnx之后获取锁但是此时客户端挂掉,这样无法执行expire设置过期时间就导致锁一直无法被释放,因此在2.8版本中Antirez为setnx增加了参数扩展,使得setnx和expire具备原子操作性。
但是这种方式在集群模式下还是有点问题:
在Sentinel集群中,一个客户端在主节点中申请了一把锁,但是这把锁还没来得及同步到从节点,主节点宕机了,主节点重新上线后会变成从节点,此时新的主节点中是没有这把锁的,所以当一个新的客户端也过来加锁时,同样会加锁成功,这样就导致了系统中同一把锁被两个客户端同时持有的情况。这个时候就可以使用到Redlock算法来解决了。
Redlock算法的核心思想就是使用“大多数机制”,也就是加锁时会向过半的节点依次发送set指令,只要过半节点set成功,就认为加锁成功。释放锁时也需要向所有节点发送del指令。所以要使用Redlock算法,就要求提供多个Redis实例,且这些实例之间相互独立,没有主从关系。