全网最全Redis面试总结(图文讲解,建议收藏)

1、简单介绍一下 Redis 。

  • Redis是使用C语言开发的数据库,但Redis的数据是存在内存中的(内存数据库),读写速度非常快,Redis被广泛应用于缓存方向,此外,还经常用来做分布式锁和消息队列。
  • Redis提供了多种数据类型来支持不同的业务场景,Redis支持事务、持久化、Lua脚本、多种集群方案。

2、说一下 Redis 和 Memcached 的区别和共同点。

  • 共同点:

1)都是基于内存的数据库,一般都用来当作缓存使用,两者的性能都较高
2)两者都有过期时间

  • 区别:

1)Redis支持更丰富的数据类型(支持更复杂的应用场景),提供map、list、hash、set、zset等数据结构的存储。Memcached 只支持最简单的key-value数据类型的存储。
2)Redis支持数据的持久化(AOF和RDB方式),可以将内存中的数据写入磁盘,重启的时候,可以加载进行使用,故有在灾难恢复机制。而Memcached需要将数据全部保存在内存中。
3)Redis在服务器内存使用完之后,可以将不用的数据放到磁盘中。而Memcached 在服务器内存使用完后,会直接报异常。
4)Redis支持原生的集群模式(cluster),Memcached 没有原生的集群模式,需要依赖客户端往集群中分片写入数据。
5)Redis使用单线程的多路IO复用模型(Redis 6.0使用了多线程IO)。Memcached 是多线程,非阻塞IO复用的网络模型。
6)Redis支持发布订阅模型、事务、Lua脚本等功能。而Memcached 不支持,且Redis支持更多的编程语言。
7)Redis过期数据的删除策略同时使用到了惰性删除与定期删除,而Memcached 是支持惰性删除。

基于上述Redis的优点,当前项目中基本都使用Redis来做项目的分布式缓存。

3、解释下Redis的单线程模型。

  • Redis是基于Reactor 模式来设计开发的一套高效的事件处理模型(Reactor模式高性能IO的基石,Netty的线程模型也是基于Reactor 模式),该事件处理模型对应的是Redis中的文件事件处理器(file event handler),因为文件事件处理器是以单线程的方式运行的,故说 Redis是单线程模型。
  • Redis怎样监听大量的客户端连接?

1)Redis通过IO多路复用程序 监听客户端的大量连接,将需要的事件和类型(读、写)注册到内核中并监听每个事件是否发生。如此,Redis不需要创建多余的线程来监听客户端的大量连接,降低了资源的消耗。
2)Redis服务器是一个事件驱动程序,服务器需要处理 文件事件和时间事件。
时间事件不需要多花时间了解,我们接触最多的还是 文件事件(客户端进行读取写入等操作,涉及一系列网络通信)。
3)文件事件处理器(file event handler)主要是包含 4 个部分:

  • 多个Socket(客户端连接)
  • IO多路复用程序(支持多个客户端连接的关键)
  • 文件事件分派器(将socket关联到相应的时间处理器)
  • 事件处理器(连接应答处理器、命令请求处理器、命令回复处理器)
  • 详情见《Redis 设计与实现》中关于文件事件的介绍:

Redis 基于 Reactor 模式开发了自己的网络事件处理器:这个处理器被称为文件事件处理器(file event handler)。文件事件处理器使用 I/O 多路复用(multiplexing)程序来同时监听多个套接字,并根据 套接字目前执行的任务来为套接字关联不同的事件处理器。

当被监听的套接字准备好执行连接应答(accept)、读取(read)、写入(write)、关 闭(close)等操作时,与操作相对应的文件事件就会产生,这时文件事件处理器就会调用套接字之前关联好的事件处理器来处理这些事件。

虽然文件事件处理器以单线程方式运行,但通过使用 I/O 多路复用程序来监听多个套接字,文件事件处理器既实现了高性能的网络通信模型,又可以很好地与 Redis 服务器中其他同样以单线程方式运行的模块进行对接,这保持了 Redis 内部单线程设计的简单性。

文件事件处理器

  • Redis单线程为什么快?

1)纯内存操作
2)核心是基于非阻塞的IO多路复机制来处理客户端的Socket
3)单线程避免了多线程频繁的上下文切换带来的性能问题

4、 Redis 没有使用多线程?为什么不使用多线程?

在这里插入图片描述

  • 实际上,在Redis4.0之后的版本中,就已经加入了对多线程的处理,但主要是用于删除一些大键值对的操作命令,使用这些命令就会使用主处理程序之外的其他线程来"异步处理"。
  • 在Redis6.0之前,主要还是单线程处理,不用多线程处理的原因如下

1)单线程编程较简单且更容易维护。
2)Redis的性能瓶颈不再是CPU,而是内存和网络。
3)多线程会有死锁,线程上下文切换等问题,甚至会影响性能。

5、 Redis6.0 之后为何引入了多线程?

  • Redis 6.0引入多线程主要是为了提高 网络IO的读写性能(Redis的瓶颈主要受限于内存和网络),Redis的多线程只是使用在读写网络数据等耗时操作上,执行命令仍然是单线程顺序执行,因此,不需要担心线程安全问题。
  • Redis 6.0的多线程默认是禁用的,只使用主线程, 如需开启需要修改 redis 配置文件 redis.conf
io-threads-do-reads yes

在这里插入图片描述

开启多线程后,还需要设置线程数,否则是不生效的。同样需要修改 redis 配置文件 redis.conf :

io-threads 4 #官网建议4核的机器建议设置为2或3个线程,8核的建议设置为6个线程

在这里插入图片描述

  • 关于线程数的设置,官方有一个建议:4核的机器建议设置为2或3个线程,8核的建议设置为6个线程,线程数一定要小于机器核数。还需要注意的是,线程数并不是越大越好,官方认为超过了8个基本就没什么意义了。

6、Redis的发布与订阅

  • Redis 发布订阅 (pub/sub) 是一种消息通信模式:发送者 (pub) 发送消息,订阅者 (sub) 接收消息。Redis 客户端可以订阅任意数量的频道。
  • 客户端可以订阅频道:
    在这里插入图片描述
    打开一个客户端,订阅频道:
127.0.0.1:6379> subscribe channel1 channel2 channel3 # 订阅频道channel1、channel2、channel3
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "channel1"
3) (integer) 1
1) "subscribe"
2) "channel2"
3) (integer) 2
1) "subscribe"
2) "channel3"
3) (integer) 3

当给这个频道发布消息后,消息就会发送给订阅的客户端:
在这里插入图片描述
再另外打开一个频道,发布消息:

127.0.0.1:6379> publish channel1 "This is a message of channel1"
(integer) 1    # 返回的1是订阅者数量

其后,订阅channel1、channel2、channel3的一端就会接收到消息发布端的发送的消息:

1) "message"
2) "channel1"
3) "This is a message of channel1"

在这里插入图片描述

127.0.0.1:6379> publish channel2 "hello,redis 222"
(integer) 1
127.0.0.1:6379> publish channel3 "welcome to study redis 3333"
(integer) 1

在这里插入图片描述

7、说下Redis 常见的数据结构及使用场景。

7.1、String:
  • String 类型数据是简单的 key-value 类型。虽然 Redis 是用 C 语言写的,但是 Redis 并没有使用 C 的字符串表示,而是自己构建了一种 简单动态字符串(simple dynamic string,SDS)。相比于 C 的原生字符串,Redis 的 SDS 不光可以保存文本数据,还可以保存二进制数据,并且获取字符串长度复杂度为 O(1)(C 字符串为 O(N)),除此之外,Redis 的 SDS API 是安全的,不会造成缓冲区溢出

  • 常用命令: set,get,strlen,exists,dect,incr,setex 等等。

  • 应用场景 :一般常用在需要计数的场景,比如用户的访问次数、热点文章的点赞转发数量等等。

  • 普通字符串的基本操作:

127.0.0.1:6379> set key1 var1 #设置 key-value 类型的值
OK
127.0.0.1:6379> get key1 # 根据 key 获得对应的 value
"var1"
127.0.0.1:6379> exists key  # 判断某个 key 是否存在
(integer) 1
127.0.0.1:6379> strlen key # 返回 key 所储存的字符串值的长度。
(integer) 4
127.0.0.1:6379> del key # 删除某个 key 对应的值
(integer) 1
127.0.0.1:6379> get key
(nil)
  • 批量设置和获取:
127.0.0.1:6379> mset user1 zhangsan user2 lisi user3 maliu # 批量设置 key-value 类型的值
OK
127.0.0.1:6379> mget user1 user2 user3 # 批量获取多个 key 对应的 value
1) "zhangsan"
2) "lisi"
3) "maliu"
  • 计数器(字符串的内容为整数的时候可以使用):
127.0.0.1:6379> set number 10
OK
127.0.0.1:6379> incr number # 将key中储存的数字值增一,相当于number++
(integer) 11
127.0.0.1:6379> get number
"11"
127.0.0.1:6379> incrby num 20  # 相当于num = num + 20;
(integer) 31
127.0.0.1:6379> decr number # 将 key 中储存的数字值减一
(integer) 30
127.0.0.1:6379> get number
"30"
  • 设置key的过期时间
127.0.0.1:6379> expire dtime 60 # 数据在 60s 后过期
(integer) 1
127.0.0.1:6379> setex dtime 60 value # 数据在 60s 后过期 (setex:[set] + [ex]pire)
OK
127.0.0.1:6379> ttl key # 查看数据还有多久过期,单位为秒
(integer) 56
7.2、list
  • list 即是 链表。链表是一种非常常见的数据结构,特点是易于数据元素的插入和删除并且且可以灵活调整链表长度,但是链表的随机访问困难。许多高级编程语言都内置了链表的实现比如 Java 中的 LinkedList,但是 C 语言并没有实现链表,所以 Redis 实现了自己的链表数据结构。Redis 的 list 的实现为一个 双向链表,即可以支持反向查找和遍历,更方便操作,不过带来了部分额外的内存开销。
  • 常用命令: rpush,lpop,lpush,rpop,lrange、llen 等。
  • 应用场景: 发布与订阅或者说消息队列、慢查询。
  • 通过 rpush/lpop 实现队列:
127.0.0.1:6379> rpush myList value1 # 向 list 的头部(右边)添加元素
(integer) 1
127.0.0.1:6379> rpush myList value2 value3 # 向list的头部(最右边)添加多个元素
(integer) 3
127.0.0.1:6379> lpop myList # 将 list的尾部(最左边)元素取出
"value1"
127.0.0.1:6379> lpop myList # 将 list的尾部(最左边)元素取出
"value2"
127.0.0.1:6379> lpop myList # 将 list的尾部(最左边)元素取出
"value3"
  • 通过 rpush/rpop 实现栈:
127.0.0.1:6379> rpush myList2 value1 value2 value3
(integer) 3
127.0.0.1:6379> rpop myList2 # 将 list的头部(最右边)元素取出
"value3"
127.0.0.1:6379> rpop myList2 
"value2"
127.0.0.1:6379> rpop myList2 
"value1"
  • 通过 lrange 查看对应下标范围的列表元素:
127.0.0.1:6379> rpush myList3 value1 value2 value3
(integer) 3
127.0.0.1:6379> lrange myList3 0 1 # 查看对应下标的list列表, 0 为 start,1为 end
1) "value1"
2) "value2"
127.0.0.1:6379> lrange myList3 0 -1 # 查看列表中的所有元素,-1表示倒数第一
1) "value1"
2) "value2"
3) "value3"
127.0.0.1:6379> llen myList3  # 查看List链表的长度
(integer) 3

rpush/rpop和lpush/lpop的操作示意图:
在这里插入图片描述

7.3、hash
  • hash 类似于 JDK1.8 前的 HashMap,内部实现也差不多(数组 + 链表)。不过,Redis 的 hash 做了更多优化。另外,hash 是一个 String 类型的 field 和 value 的映射表,特别适合用于存储对象,后续操作的时候,你可以直接仅仅修改这个对象中的某个字段的值。 比如我们可以 hash 数据结构来存储用户信息,商品信息等等。
  • 常用命令: hset,hmset,hexists,hget,hgetall,hkeys,hvals 等。
  • 应用场景: 适用于系统中对象数据的存储。
  • 使用示例:
127.0.0.1:6379> hmset emp ename lihua gender man age 26 email 123@qq.com
OK
127.0.0.1:6379> hkeys emp # 获取emp对象的所有key
1) "ename"
2) "gender"
3) "age"
4) "email"
127.0.0.1:6379> hvals emp # 获取emp对象的所有value
1) "lihua"
2) "man"
3) "26"
4) "123@qq.com"
127.0.0.1:6379> hgetall emp # 获取在哈希表中指定 key 的所有键和值
1) "ename"
2) "lihua"
3) "gender"
4) "man"
5) "age"
6) "26"
7) "email"
8) "123@qq.com"
127.0.0.1:6379> hexists emp ename # 查看 key 对应的 value中指定的字段是否存在。
(integer) 1
127.0.0.1:6379> hget emp ename # 获取存储在哈希表中指定字段的值。
"lihua"
127.0.0.1:6379> hget emp email
"123@qq.com"
127.0.0.1:6379> hset emp ename zhangsan # 如果ename为空,则将ename设置为后面的值,如果ename不为空,则将ename修改为后面的值
(integer) 0
127.0.0.1:6379> hget emp ename
"zhangsan"
7.4、set
  • 介绍 : set 类似于 Java 中的 HashSet 。Redis 中的 set 类型是一种无序集合,集合中的元素没有先后顺序。当你需要存储一个列表数据,又不希望出现重复数据时,set 是一个很好的选择,并且 set 提供了判断某个成员是否在一个 set 集合内的重要接口,这个也是 list 所不能提供的。可以基于 set 轻易实现交集、并集、差集的操作。比如:你可以将一个用户所有的关注人存在一个集合中,将其所有粉丝存在一个集合。Redis 可以非常方便的实现如共同关注、共同粉丝、共同喜好等功能。这个过程也就是求交集的过程。
  • 常用命令: sadd,spop,smembers,sismember,scard,sinterstore,sunion 等。
  • 应用场景: 需要存放的数据不能重复以及需要获取多个数据源交集和并集等场景。
  • 使用示例:
127.0.0.1:6379> sadd myset1 v1 v2 v3 # 向set集合中添加元素
(integer) 3
127.0.0.1:6379> sadd myset1 v1 v2 # 不能向set集合中添加重复元素
(integer) 0
127.0.0.1:6379> smembers myset1 # 查看myset1中的所有元素
1) "v3"
2) "v2"
3) "v1"
127.0.0.1:6379> sadd myset1 v4
(integer) 1
127.0.0.1:6379> smembers myset1
1) "v3"
2) "v2"
3) "v4"
4) "v1"
127.0.0.1:6379> scard myset1  # 查看myset1集合的长度
(integer) 4
127.0.0.1:6379> sismember myset1 v1 # 检查某个元素是否存在set 中,只能接收单个元素
(integer) 1
127.0.0.1:6379> sismember myset1 val1
(integer) 0
127.0.0.1:6379> sadd myset2 var1 var2 v1 v2 var3
(integer) 5
127.0.0.1:6379> sinterstore myset3 myset1 myset2 # 获取 mySet1 和 mySet2 的交集并存放在 mySet3 中
(integer) 2
127.0.0.1:6379> smembers myset3
1) "v2"
2) "v1"
127.0.0.1:6379> sunion myset4 myset1 myset2 # 获取 mySet1 和 mySet2 的并集并存放在 mySet4 中
1) "v3"
2) "v4"
3) "v1"
4) "var2"
5) "v2"
6) "var1"
7) "var3"
7.5、zset
  • 有序集合zset与普通集合set非常相似,是一个没有重复元素的字符串集合。不同之处是有序集合的每个成员都关联了一个评分(score),这个评分(score)被用来按照从最低分到最高分的方式排序集合中的成员。集合的成员是唯一的,但是评分可以重复 。
    因为元素是有序的, 所以也可以很快的根据评分(score)或者次序(position)来获取一个范围的元素。访问有序集合的中间元素也是非常快的,因此你能够使用有序集合作为一个没有重复成员的智能列表。
  • 常用命令: zadd,zcard,zscore,zrange,zrevrange,zrem 等。
    -应用场景: 需要对数据根据某个权重进行排序的场景。比如在直播系统中,实时排行信息包含直播间在线用户列表,各种礼物排行榜,弹幕消息(可以理解为按消息维度的消息排行榜)等信息。
  • 使用示例:
127.0.0.1:6379> zadd score 110 Math 120 English 105 Chinese 130 History # 向zset集合(score)中添加元素
(integer) 4
127.0.0.1:6379> zcard score # 查看score集合中的元素个数
(integer) 4
127.0.0.1:6379> zscore score Math # 查看键为Math的权重(分数/权重)
110.0
127.0.0.1:6379> zrange score 0 -1 withscores # 将课程成绩从低到高正序输出,且带权重,如果不需要带权重,即不用后面添加withscores 
1) "Chinese"
2) 105.0
3) "Math"
4) 110.0
5) "English"
6) 120.0
7) "History "
8) 130.0
127.0.0.1:6379> zrank score Math # 返回该值在集合中的排名,排名从0开始
(integer) 1
127.0.0.1:6379> zrank score Chinese
(integer) 0
127.0.0.1:6379> zcount score 115 150  # 统计该集合,分数区间内(115到150)的元素个数  
127.0.0.1:6379> zrem score History # 删除score集合下,指定值的元素
(integer) 1
  • 案例:如何利用zset实现一个文章访问量的排行榜?
127.0.0.1:6379> zadd topn 1600 a1 2000 a2 800 a3 1200 a4 600 a5
(integer) 4
127.0.0.1:6379> zrevrange topn 0 -1 withscores # 输出所有文章访问量的排行榜
# 逆序输出某个范围区间的元素,0 为排序的起始下标,后面的值表示排序的最后下标,-1表示下标倒数第一
1) "a2"
2) 2000.0
3) "a1"
4) 1600.0
5) "a4"
6) 1200.0
7) "a3"
8) 800.0
9) "a5"
10) 600.0
127.0.0.1:6379> zrevrange topn 0 2 withscores # 输出访问量前三名的文章(a2、a1、a4)
1) "a2"
2) 2000.0
3) "a1"
4) 1600.0
5) "a4"
6) 1200.0

8、说下Redis的新数据类型。

8.1、Bitmaps
  • 合理地使用操作位能够有效地提高内存使用率和开发效率。
    Redis提供了Bitmaps这个“数据类型”可以实现对位的操作:
    1)Bitmaps本身不是一种数据类型, 实际上就是字符串(key-value) , 但是它可以对字符串的位进行操作
    2)Bitmaps单独提供了一套命令, 所以在Redis中使用Bitmaps和使用字符串的方法不太相同。 可以把Bitmaps想象成一个以位为单位的数组, 数组的每个单元只能存储0和1, 数组的下标在Bitmaps中叫做偏移量。
    在这里插入图片描述
  • 常用命令:
 setbit<key><offset><value>  # 设置Bitmaps中某个偏移量的值(0或1),偏移量从0开始
 getbit<key><offset>         # 获取Bitmaps中某个偏移量的值
 bitcount<key>[start end]    # 统计字符串从start字节到end字节比特值为1的数量
 bitop  and(or/not/xor) <destkey> [key…] # bitop是一个复合操作, 它可以做多个Bitmaps的and(交集) 、 or(并集) 、 not(非) 、 xor(异或) 操作并将结果保存在destkey中。
使用示例:

可以将每个独立用户是否访问过网站存放在Bitmaps中, 将访问的用户记做1, 没有访问的用户记做0, 用偏移量作为用户的id。设置键的第offset个位的值(从0算起) , 假设现在有20个用户,userid=1, 6, 11, 15, 19的用户对网站进行了访问, 那么当前Bitmaps初始化结果如图:
在这里插入图片描述

用users:20210816 代表2021-08-16这天的独立访问用户的Bitmaps:

1)setbit
127.0.0.1:6379> setbit users:20210816 1 1
(integer) 0
127.0.0.1:6379> setbit users:20210816 6 1
(integer) 0
127.0.0.1:6379> setbit users:20210816 11 1
(integer) 0
127.0.0.1:6379> setbit users:20210816 15 1
(integer) 0
127.0.0.1:6379> setbit users:20210816 19 1
(integer) 0
  • 很多应用的用户id以一个指定数字(例如10000) 开头, 直接将用户id和Bitmaps的偏移量对应势必会造成一定的浪费, 通常的做法是每次做setbit操作时将用户id减去这个指定数字。
2)getbit

获取id=6和id=12的用户是否在2021-08-16这天访问过, 返回0说明没有访问过,返回1则访问过:

127.0.0.1:6379> getbit users:20210816 6
(integer) 1
127.0.0.1:6379> getbit users:20210816 12
(integer) 0
3)bitcount
- 这里需要注意的是:
bitcount <key> [start end] 统计字符串从start字节到end字节比特值为1的数量(当start和end不指定时,即该命令求的是key中所有1的数量,当指定start和end时,start和end代表起始和结束字节数,字节计数从0开始)

计算2021-08-16这天独立访问的总用户数量

> 127.0.0.1:6379> bitcount users:20210816
(integer) 5

用户id在第1个字节到第2个字节(9到24位)之间的独立访问用户数, 对应的用户id是11, 15, 19,在第0个字节到第1个字节(0到16位)之间的独立访问用户数,对应的用户id是1,6,11,15
users:20210816 -->【01000001 01000000 00000000 00100001】,对应的字节为【0,1,2,3】

127.0.0.1:6379> bitcount users:20210816 1 2
(integer) 3
127.0.0.1:6379> bitcount users:20210816 0 1
(integer) 4
4) bitop
# 2021-08-16 日访问网站的userid=1,6,11,15,19。
setbit users:20210816 1 1
setbit users:20210816 6 1
setbit users:20210816 11 1
setbit users:20210816 15 1
setbit users:20210816 19 1
# 2021-08-17 日访问网站的userid=1,6,11,15,19。
setbit users:20210817 6 1
setbit users:20210817 8 1
setbit users:20210817 10 1
setbit users:20210817 11 1
setbit users:20210817 20 1
setbit users:20210817 30 1
  • 计算出08-16和08-17这两天都访问过网站的用户数量,可以使用and求交集
- 127.0.0.1:6379> bitop and anskey users:20210816 users:20210817
(integer) 4
127.0.0.1:6379> bitcount anskey  # 结果为2,userid为6和11的用户在这两天都访问过
(integer) 2 
  • 计算出任意一天都访问过网站的用户数量(例如月活跃就是类似这种),可以使用or求并集
127.0.0.1:6379> bitop or anskey users:20210816 users:20210817
(integer) 4
127.0.0.1:6379> bitcount anskey 
# 结果为9,userid为1,6,8, 10,11,15, 19,20,30的用户在这两天中的任意一天访问过
(integer) 9
8.2、HyperLogLog
  • 在工作当中,我们经常会遇到与统计相关的功能需求,比如统计网站PV(PageView页面访问量),可以使用Redis的incr、incrby轻松实现。但像UV(UniqueVisitor,独立访客)、独立IP数、搜索记录数等需要去重和计数的问题如何解决?
  • 这种求集合中不重复元素个数的问题称为基数问题。解决基数问题有很多种方案:
    (1)数据存储在MySQL表中,使用distinct count计算不重复个数
    (2)使用Redis提供的hash、set、bitmaps等数据结构来处理
    以上的方案结果精确,但随着数据不断增加,导致占用空间越来越大,对于非常大的数据集是不切实际的。
  • Redis的HyperLogLog 是用来做基数统计的算法,HyperLogLog 的优点是,在输入元素的数量或者体积非常非常大时,计算基数所需的空间总是固定的、并且是很小的。每个 HyperLogLog 键只需要花费 12 KB 内存,就可以计算接近 2^64 个不同元素的基数。这和计算基数时,元素越多耗费内存就越多的集合形成鲜明对比。
    但是,因为 HyperLogLog 只会根据输入元素来计算基数,而不会储存输入元素本身,所以 HyperLogLog 不能像集合那样,返回输入的各个元素。
  • 什么是基数?比如数据集 {1, 3, 5, 7, 5, 7, 8}, 那么这个数据集的基数集为 {1, 3, 5 ,7, 8}, 基数(不重复元素)为5。 基数估计就是在误差可接受的范围内,快速计算基数。
  • 常用命令:
pfadd <key>< element> [element ...]   添加指定元素到 HyperLogLog 中
pfcount<key> [key ...] 计算HLL的近似基数,可以计算多个HLL
pfmerge<destkey><sourcekey> [sourcekey ...]  将一个或多个HLL合并后的结果存储在另一个HLL中,比如每月活跃用户可以使用每天的活跃用户来合并计算可得
  • 使用实例
127.0.0.1:6379> pfadd language1 java php python C++ 
(integer) 1
127.0.0.1:6379> pfcount language1 # language1的元素基数为4个
(integer) 4
127.0.0.1:6379> pfadd language1 java go python # HyperLogLog经常被用来统计集合中的元素基数,重复元素不会重复计数
(integer) 1
127.0.0.1:6379> pfcount language1 # language1的元素只多了go,基数变为5
(integer) 5
127.0.0.1:6379> pfadd language2 perl ruby go java
(integer) 1
127.0.0.1:6379> pfcount language1 language2 # 统计language1和language2两者中元素的基数(基数:集合中不重复元素个数)
(integer) 7
127.0.0.1:6379> pfmerge resKey language1 language2 #language1和language2中的元素合并到resKey,(即resKey 是language1和language2两者的并集)
OK
127.0.0.1:6379> pfcount resKey 
(integer) 7
8.3、Geospatial
  • Redis 3.2 中增加了对GEO类型的支持。GEO,Geographic,地理信息的缩写。该类型,就是元素的2维坐标,在地图上就是经纬度。redis基于该类型,提供了经纬度设置,查询,范围查询,距离查询,经纬度Hash等常见操作。
  • 常用命令:
geoadd<key>< longitude><latitude><member> [longitude latitude member...]   # 添加地理位置(经度,纬度,名称)
geopos  <key><member> [member...]  # 获得指定地区的坐标值
geodist<key><member1><member2>  [m|km|ft|mi ] 
# 获取两个位置之间的直线距离,单位:
# m 表示单位为米[默认值]。
# km 表示单位为千米。
# mi 表示单位为英里。
# ft 表示单位为英尺。
如果用户没有显式地指定单位参数, 那么 GEODIST 默认使用米作为单位
georadius<key>< longitude><latitude>radius  m|km|ft|mi   #以给定的经纬度为中心,找出某一半径内的元素
  • 使用实例:
127.0.0.1:6379> geoadd china:city 121 31 shanghai 114 22 shenzhen 116 39 beijing # 向集合china:city中添加城市信息(经度 纬度 城市名)
(integer) 3
127.0.0.1:6379> geopos china:city beijing shenzhen # 获取beijing和shenzhen的经度和纬度
1) 1) "116.00000113248825073"
   2) "38.99999918434559731"
2) 1) "114.00000125169754028"
   2) "21.99999950739083232"
127.0.0.1:6379> geodist china:city shanghai shenzhen km
"1218.8985"
127.0.0.1:6379> geodist china:city beijing shanghai km # 获取beijing和shanghai两个位置之间的直线距离
"999.2077"
127.0.0.1:6379> georadius china:city 110 30 1000 km #  以给定的经纬度(110,30)为中心,找出某一半径(1000km)内的城市
1) "shenzhen" 

9、缓存数据的处理流程是怎么样的?

1)如果用户请求的数据在缓存中,就直接返回对应的数据。
2)请求数据不再缓存中,就去数据库中查找。
3)如果数据库能找到需要的数据,就将数据更新到缓存中,并返回对应的结果。
4)如果数据库不能找到数据,就返回空数据。

在这里插入图片描述

10、为什么要用 Redis/为什么要用缓存?

  • 简单来说,使用缓存主要是为了提升用户体验以及应对更多的用户。
    下面我们主要从“高性能”和“高并发”这两点来看待这个问题。
    在这里插入图片描述

高性能

对照上面的图。我们设想这样的场景:

假如用户第一次访问数据库中的某些数据的话,这个过程是比较慢,毕竟是从硬盘中读取的。但是,如果说,用户访问的数据属于高频数据并且不会经常改变的话,那么我们就可以很放心地将该用户访问的数据存在缓存中。
这样有什么好处呢?那就是保证用户下一次再访问这些数据的时候就可以直接从缓存中获取了。操作缓存就是直接操作内存,所以速度相当快。

不过,要保持数据库和缓存中的数据的一致性。 如果数据库中的对应数据改变的之后,同步改变缓存中相应的数据即可!

高并发:

一般像 MySQL 这类的数据库的 QPS 大概都在 1w 左右(4 核 8g) ,但是使用 Redis 缓存之后很容易达到 10w+,甚至最高能达到 30w+(就单机 redis 的情况,redis 集群的话会更高)。

QPS(Query Per Second):服务器每秒可以执行的查询次数;

所以,直接操作缓存能够承受的数据库请求数量是远远大于直接访问数据库的,所以我们可以考虑把数据库中的部分数据转移到缓存中去,这样用户的一部分请求会直接到缓存这里而不用经过数据库。进而,我们也就提高的系统整体的并发。

11、Redis 给缓存数据设置过期时间有啥用?

  • 一般情况下,我们设置保存的缓存数据的时候都会设置一个过期时间。为什么呢?因为内存是有限的,如果缓存中的所有数据都是一直保存的话,分分钟直接Out of memory。

  • Redis 自带了给缓存数据设置过期时间的功能,比如:

127.0.0.1:6379> expire key  60 # 数据在 60s 后过期
(integer) 1
127.0.0.1:6379> setex key 60 value # 数据在 60s 后过期 (setex:[set] + [ex]pire)
OK
127.0.0.1:6379> ttl key # 查看数据还有多久过期
(integer) 56
  • 注意:Redis中除了字符串类型有自己独有设置过期时间的命令 setex 外,其他方法都需要依靠 expire 命令来设置过期时间 。另外, persist 命令可以移除一个键的过期时间:

过期时间除了有助于缓解内存的消耗,还有什么其他用么?

  • 很多时候,我们的业务场景就是需要某个数据只在某一时间段内存在,比如我们的短信验证码可能只在1分钟内有效,用户登录的 token 可能只在 1 天内有效。而如果使用传统的数据库来处理的话,一般都是自己判断过期,这样更麻烦并且性能要差很多。

12、Redis是如何判断数据是否过期的呢?

Redis 通过一个叫做过期字典(可以看作是hash表)来保存数据过期的时间。过期字典的key(键)指向Redis数据库中的某个key(键),过期字典的value(值)是一个long long类型的整数,这个整数保存了key所指向的数据库键的过期时间(毫秒精度的UNIX时间戳)。

在这里插入图片描述
过期字典是存储在redisDb这个结构里的:

 typedef struct redisDb {
    ...
    
    dict *dict;     //数据库键空间,保存着数据库中所有键值对
    dict *expires   // 过期字典,保存着键的过期时间
    ...
} redisDb;

13、过期的数据的删除策略了解么?

  • 如果假设你设置了一批 key 只能存活 1 分钟,那么 1 分钟后,Redis 是怎么对这批 key 进行删除的呢?
  • 常用的过期数据的删除策略就两个(重要!自己造缓存轮子的时候需要格外考虑的东西):

1) 惰性删除 :只会在取出key的时候才对数据进行过期检查。这样对CPU最友好,但是可能会造成太多过期 key 没有被删除。
2)定期删除 : 每隔一段时间抽取一批 key 执行删除过期key操作。并且,Redis 底层会通过限制删除操作执行的时长和频率来减少删除操作对CPU时间的影响。

  • 定期删除对内存更加友好,惰性删除对CPU更加友好。两者各有千秋,所以Redis 采用的是 定期删除+惰性/懒汉式删除
    但是,仅仅通过给 key 设置过期时间还是有问题的。因为还是可能存在定期删除和惰性删除漏掉了很多过期 key 的情况。这样就导致大量过期 key 堆积在内存里,然后就Out of memory了。

怎么解决这个问题呢?答案就是: Redis 内存淘汰机制。

14、Redis 内存淘汰机制了解么?

相关问题:MySQL 里有 2000w 数据,Redis 中只存 20w 的数据,如何保证 Redis 中的数据都是热点数据?

Redis 提供 6 种数据淘汰策略:

1)volatile-lru(least recently used):从已设置过期时间的数据集(server.db[i].expires)中挑选最近最少使用的数据淘汰
2)volatile-ttl:从已设置过期时间的数据集(server.db[i].expires)中挑选将要过期的数据淘汰
3) volatile-random:从已设置过期时间的数据集(server.db[i].expires)中任意选择数据淘汰
4.)allkeys-lru(least recently used):当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的 key(这个是最常用的)
5) allkeys-random:从数据集(server.db[i].dict)中任意选择数据淘汰
6) no-eviction:禁止驱逐数据,也就是说当内存不足以容纳新写入数据时,新写入操作会报错。
4.0 版本后增加以下两种:
7) volatile-lfu(least frequently used):从已设置过期时间的数据集(server.db[i].expires)中挑选最不经常使用的数据淘汰
8) allkeys-lfu(least frequently used):当内存不足以容纳新写入数据时,在键空间中,移除最不经常使用的 key

15、Redis 持久化机制(怎么保证 Redis 挂掉之后再重启数据可以进行恢复)

很多时候我们需要持久化数据也就是将内存中的数据写入到硬盘里面,大部分原因是为了之后重用数据(比如重启机器、机器故障之后恢复数据),或者是为了防止系统故障而将数据备份到一个远程位置。

Redis 不同于 Memcached 的很重要一点就是,Redis 支持持久化,而且支持两种不同的持久化操作。Redis 的一种持久化方式叫快照(snapshotting,RDB),另一种方式是只追加文件(append-only file, AOF)。这两种方法各有千秋,下面我会详细这两种持久化方法是什么,怎么用,如何选择适合自己的持久化方法。

1)快照(snapshotting)持久化(RDB)

Redis 可以通过创建快照来获得存储在内存里面的数据在某个时间点上的副本。Redis 创建快照之后,可以对快照进行备份,可以将快照复制到其他服务器从而创建具有相同数据的服务器副本(Redis 主从结构,主要用来提高 Redis 性能),还可以将快照留在原地以便重启服务器的时候使用。

快照持久化是 Redis 默认采用的持久化方式,在 Redis.conf 配置文件中默认有此下配置:

save 900 1           #在900秒(15分钟)之后,如果至少有1个key发生变化,Redis就会自动触发BGSAVE命令创建快照。
save 300 10          #在300秒(5分钟)之后,如果至少有10个key发生变化,Redis就会自动触发BGSAVE命令创建快照。
save 60 10000        #在60秒(1分钟)之后,如果至少有10000个key发生变化,Redis就会自动触发BGSAVE命令创建快照。
2)AOF(append-only file)持久化

与快照持久化相比,AOF 持久化 的实时性更好,因此已成为主流的持久化方案。默认情况下 Redis 没有开启 AOF(append only file)方式的持久化,可以通过 appendonly 参数开启:

appendonly yes

开启 AOF 持久化后每执行一条会更改 Redis 中的数据的命令,Redis 就会将该命令写入硬盘中的 AOF 文件。AOF 文件的保存位置和 RDB 文件的位置相同,都是通过 dir 参数设置的,默认的文件名是 appendonly.aof。

在 Redis 的配置文件中存在三种不同的 AOF 持久化方式,它们分别是:

appendfsync always    #每次有数据修改发生时都会写入AOF文件,这样会严重降低Redis的速度
appendfsync everysec  #每秒钟同步一次,显示地将多个写命令同步到硬盘
appendfsync no        #让操作系统决定何时进行同步

为了兼顾数据和写入性能,用户可以考虑 appendfsync everysec 选项 ,让 Redis 每秒同步一次 AOF 文件,Redis 性能几乎没受到任何影响。而且这样即使出现系统崩溃,用户最多只会丢失一秒之内产生的数据。当硬盘忙于执行写入操作的时候,Redis 还会优雅的放慢自己的速度以便适应硬盘的最大写入速度。

拓展:Redis 4.0 对于持久化机制的优化

Redis 4.0 开始支持 RDB 和 AOF 的混合持久化(默认关闭,可以通过配置项 aof-use-rdb-preamble 开启)。

如果把混合持久化打开,AOF 重写的时候就直接把 RDB 的内容写到 AOF 文件开头。这样做的好处是可以结合 RDB 和 AOF 的优点, 快速加载同时避免丢失过多的数据。当然缺点也是有的, AOF 里面的 RDB 部分是压缩格式不再是 AOF 格式,可读性较差。

补充内容:AOF 重写

AOF 重写可以产生一个新的 AOF 文件,这个新的 AOF 文件和原有的 AOF 文件所保存的数据库状态一样,但体积更小。

AOF 重写是一个有歧义的名字,该功能是通过读取数据库中的键值对来实现的,程序无须对现有 AOF 文件进行任何读入、分析或者写入操作。

在执行 BGREWRITEAOF 命令时,Redis 服务器会维护一个 AOF 重写缓冲区,该缓冲区会在子进程创建新 AOF 文件期间,记录服务器执行的所有写命令。当子进程完成创建新 AOF 文件的工作之后,服务器会将重写缓冲区中的所有内容追加到新 AOF 文件的末尾,使得新旧两个 AOF 文件所保存的数据库状态一致。最后,服务器用新的 AOF 文件替换旧的 AOF 文件,以此来完成 AOF 文件重写操作

16、Redis 事务

16.1、介绍及基本使用
  • Redis事务是一个单独的隔离操作事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。Redis事务的主要作用就是串联多个命令防止别的命令插队
  • Redis 可以通过 MULTI,EXEC,DISCARD 和 WATCH 等命令来实现事务(transaction)功能。
127.0.0.1:6379> multi # 开启事务
OK
127.0.0.1:6379(TX)> mset k1 100 k2 200 # 将命令放入命令队列中,直到输入命令 exec才会依次执行命令队列中的命令
QUEUED
127.0.0.1:6379(TX)> mset k3 300 k4 400
QUEUED
127.0.0.1:6379(TX)> get k1
QUEUED
127.0.0.1:6379(TX)> mget k2 k3 
QUEUED
127.0.0.1:6379(TX)> exec # 执行队列中的命令
1) OK
2) OK
3) "100"
4) 1) "200"
   2) "300"
  • 从输入ulti命令开始,输入的命令都会依次进入命令队列中,但不会执行,直到输入Exec后,Redis会将之前的命令队列中的命令依次执行。
    组队的过程中可以通过discard来放弃组队。
    在这里插入图片描述
16.2、事务的错误处理

组队中某个命令出现了错误,执行时整个队列所有命令都会被取消。
在这里插入图片描述
如果执行阶段某个命令报出了错误,则只有报错的命令不会被执行,而其他的命令都会执行,不会回滚。
在这里插入图片描述

16.3、Redis中的锁
  • Redis中的锁使用的是乐观锁,用check-and-set机制实现事务。
  • 乐观锁(Optimistic Lock), 顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量。
    乐观锁图解
  • 另外补充下悲观锁。悲观锁(Pessimistic Lock), 顾名思义,就是很悲观,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会block直到它拿到锁。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。
    乐观锁图解
16.4、watch和unwatch

在执行multi之前,先执行watch key1 [key2],可以监视一个(或多个) key ,如果在事务执行之前这个(或这些) key 被其他命令所改动,那么事务将被打断
取消 WATCH 命令对所有 key 的监视
如果在执行 WATCH 命令之后,EXEC 命令或DISCARD 命令先被执行了的话,那么就不需要再执行UNWATCH 了。

127.0.0.1:6379> mset eid 101 age 26 ename lisi gender man
OK
127.0.0.1:6379> watch eid age
OK
127.0.0.1:6379> mset eid 301 age 40
OK
127.0.0.1:6379> multi 
OK
127.0.0.1:6379(TX)> incrby age 5 
QUEUED
127.0.0.1:6379(TX)> set gender woman # 如果事务在后面被执行,age == 45,woman=="woman"
QUEUED
127.0.0.1:6379(TX)> exec  # 事务被打断
(nil)
127.0.0.1:6379> mget age gender
1) "40"
2) "man"
16.5、Redis事务三特性
  • 1)单独的隔离操作
    事务中的所有命令都会序列化、按顺序地执行事务在执行的过程中,不会被其他客户端发送来的命令请求所打断
  • 2)没有隔离级别的概念
    队列中的命令没有提交之前都不会实际被执行(即exec之前的所有命令都会放在等待队列中等待执行),因为事务提交前任何指令都不会被实际执行
  • 3)不保证原子性
    事务中如果有一条命令执行失败,其后的命令仍然会被执行,没有回滚
16.6、Redis事务和关系型数据库的事务
  • Redis 的事务和我们平时理解的关系型数据库的事务不同。我们知道事务具有四大特性:原子性、隔离性、持久性、一致性(ACID)。

1)原子性(Atomicity): 事务是最小的执行单位,不允许分割。事务的原子性确保动作要么全部完成,要么完全不起作用;
2) 隔离性(Isolation): 并发访问数据库时,一个用户的事务不被其他事务所干扰,各并发事务之间数据库是独立的;
3) 持久性(Durability): 一个事务被提交之后。它对数据库中数据的改变是持久的,即使数据库发生故障也不应该对其有任何影响。
4)一致性(Consistency): 执行事务前后,数据保持一致,多个事务对同一个数据读取的结果是相同的;

  • Redis 是不支持 roll back 的,因而不满足原子性的(而且不满足持久性)。 Redis官网也解释了自己为啥不支持回滚。简单来说就是Redis开发者们觉得没必要支持回滚,这样更简单便捷并且性能更好。Redis开发者觉得即使命令执行错误也应该在开发过程中就被发现而不是生产过程中。

    可以理解为 :Redis事务提供了一种将多个命令请求打包的功能。然后,再按顺序执行打包的所有命令,确保事务执行过程中不会被中途打断。

17、缓存穿透(缓存没有key)

1)问题说明
  • key对应的数据在数据源并不存在,每次针对此key的请求从缓存获取不到,请求都会压到数据源,从而可能压垮数据源。比如用一个不存在的用户id获取用户信息,不论缓存还是数据库都没有,若黑客利用此漏洞(非正常的url访问)进行攻击可能压垮数据库。
  • 具体情况说明:
    1)应用服务器压力突然变大
    2)当有大量访问时,每次操作会先查询缓存,缓存如有对应数据,直接返回,如没有数据,会再查询数据库,相应数据会存入缓存中,当有大量请求时,Redis的命中率降低(查询到结果的概率持续降低),可能会造成数据库的崩溃
    在这里插入图片描述
  • 一个一定不存在缓存及查询不到的数据,由于缓存是不命中时被动写的,并且出于容错考虑,如果从存储层查不到数据则不写入缓存,这将导致这个不存在的数据每次请求都要到存储层去查询,失去了缓存的意义。
2)解决方案

(1)对空值缓存:如果一个查询返回的数据为空(不管是数据是否不存在),我们仍然把这个空结果(null)进行缓存,设置空结果的过期时间会很短,最长不超过五分钟
(2)设置可访问的名单(白名单):
使用bitmaps类型定义一个可以访问的名单,名单id作为bitmaps的偏移量,每次访问和bitmap里面的id进行比较,如果访问id不在bitmaps里面,进行拦截,不允许访问。
(3)采用布隆过滤器:(布隆过滤器(Bloom Filter)是1970年由布隆提出的。它实际上是一个很长的二进制向量(位图)和一系列随机映射函数(哈希函数)。
布隆过滤器可以用于检索一个元素是否在一个集合中。它的优点是空间效率和查询时间都远远超过一般的算法,缺点是有一定的误识别率和删除困难。)
将所有可能存在的数据哈希到一个足够大的bitmaps中,一个一定不存在的数据会被 这个bitmaps拦截掉,从而避免了对底层存储系统的查询压力。
(4)进行实时监控:当发现Redis的命中率开始急速降低,需要排查访问对象和访问的数据,和运维人员配合,可以设置黑名单限制服务

18、缓存击穿(缓存中的key过期)

1)问题说明
  • key在redis中过期,此时若有大量并发请求过来,这些请求发现缓存过期,一般都会从后端DB加载数据并回设到缓存,这个时候,数据库的访问压力瞬时间增大,高并发的请求可能会瞬间把后端DB压垮,而Redis还处于正常运行状态。
    在这里插入图片描述
2)解决方案

key可能会在某些时间点被超高并发地访问,是一种非常“热点”的数据。这个时候,需要考虑一个问题:缓存被“击穿”的问题。
解决问题:
(1)预先设置热门数据:在redis高峰访问之前,把一些热门数据提前存入到redis里面,加大这些热门数据key的时长
(2)实时调整:现场监控哪些数据热门,实时调整key的过期时长
(3)使用锁:
(1)就是在缓存失效的时候(判断拿出来的值为空),不是立即去load db。
(2)先使用缓存工具的某些带成功操作返回值的操作(比如Redis的SETNX)去set一个mutex key
(3)当操作返回成功时,再进行load db的操作,并回设缓存,最后删除mutex key;
(4)当操作返回失败,证明有线程在load db,当前线程睡眠一段时间再重试整个get缓存的方法。
在这里插入图片描述

19、缓存雪崩(缓存中大量key过期)

1)问题描述
  • 很多key对应的数据存在,但在redis中过期,此时若有大量并发请求过来,这些请求发现缓存过期一般都会从后端DB加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把后端DB压垮。(即在极少的时间段中,查询大量key(多个热门)的集中过期情况)
    缓存雪崩与缓存击穿的区别在于,缓存雪崩针对很多key缓存缓存击穿则是某个key的大量访问。
    在这里插入图片描述
    缓存瞬间失效:
    在这里插入图片描述
2)解决方案

(1)构建多级缓存架构:nginx缓存 + redis缓存 +其他缓存(ehcache等)
(2)使用锁或队列
用加锁或者队列的方式保证来保证不会有大量的线程对数据库一次性进行读写,从而避免失效时大量的并发请求落到底层存储系统上。不适用高并发情况
(3)设置过期标志更新缓存
记录缓存数据是否过期(设置提前量),如果过期会触发通知另外的线程在后台去更新实际key的缓存。
(4)将缓存失效时间分散开
比如我们可以在原有的失效时间基础上增加一个随机值,比如1-5分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件。

20、如何保证缓存和数据库数据的一致性?

实现方式可以有很多种,下面单独对 Cache Aside Pattern(旁路缓存模式) 来聊聊。

Cache Aside Pattern 中遇到写请求是这样的:更新 DB,然后直接删除 cache 。如果更新数据库成功,而删除缓存这一步失败的情况的话,简单说两个解决方案:

1)缓存失效时间变短(不推荐,治标不治本) :我们让缓存数据的过期时间变短,这样的话缓存就会从数据库中加载数据。另外,这种解决办法对于先操作缓存后操作数据库的场景不适用。
2) 增加cache更新重试机制(常用): 如果 cache 服务当前不可用,导致缓存删除失败的话,我们就隔一段时间进行重试,重试次数可以自己定。如果多次重试还是失败的话,我们可以把当前更新失败的 key 存入队列中,等缓存服务可用之后,再将 缓存中对应的 key 删除即可

参考:
1、Redis 命令参考
2、《Redis 设计与实现》
3、关于 Redis 事务不满足原子性的问题
4、尚硅谷Redis教程

  • 1
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值