Redis面试必备知识点
1. Redis五种基本的数据结构
- 字符串(strings)
这是最简单Redis类型。如果你只用这种类型,Redis就像一个可以持久化的memcached服务器.key,value就是字符串.
string.jpg
set mykey maskwang
get mykey
//设置多个值
mset a 10 b 20 c 30
//设置5秒的过期
expire key 5
- 列表(Redis Lists)
一个列表结构可以有序的存储多个字符串,多个字符串是可以重复的。
未命名文件 (1).jpg
> rpush mylist A
(integer) 1
> rpush mylist B
(integer) 2
> lpush mylist first
(integer) 3
//顺序取列表中每个值
> lrange mylist 0 -1
1) "first"
2) "A"
3) "B"
- 集合(set)
Redis的集合和列表都可以存储多个字符串,不同之处在于,列表可以存储多个相同的字符串,而集合则通过散列表保证自己存储的每个字符串都是各不相同的。
未命名文件 (3).jpg
> sadd myset 1 2 3
(integer) 3
> smembers myset
1. 3
2. 1
3. 2
- 散列(hash)
Redis的散列可以存储多个键值对之间的映射。
未命名文件.png
> hmset user:1000 username maskwang birthyear 1994 verified 1
OK
> hget user:1000 username
"maskwang"
> hget user:1000 birthyear
"1993"
> hgetall user:1000
1) "username"
2) "maskwang"
3) "birthyear"
4) "1994"
5) "verified"
6) "1"
- 有序集合(zset)
有序集合和散列一样,都用于存储键值对:有序集合的键成为成员(member),每个成员都是各不相同的;而有序集合的值称为分值(score),分值必须是浮点数。有序集合是Redis里面唯一一个既可以根据成员访问元素,又可以根据分值的排列顺序来访问元素的结构。
未命名文件 (1).png
> zadd student 1994 "maskwang"
(integer) 1
> zadd student 1987 "tom"
(integer 1)
> zadd student 1999 "bob"
(integer) 1
> zadd student 1949 "mary"
(integer) 1
//遍历zset,元素则按分值大小从小到大显示。
> zrange hackers 0 -1
1) "mary"
2) "tom"
3) "maskwang"
4) "bob"
2. Redis持久化
Redis是内存数据库,它把数据存储在内存中,这样在加快读取速度的同时也对数据安全性产生了新的问题,即当redis所在服务器发生宕机后,redis数据库里的所有数据将会全部丢失。Redis提供了两种持久化方式,分别是RDB和AOF。
- 持久化之全量写入:RDB
RDB持久化是把当前进程数据生成快照保存到硬盘的过程.类似于savepoint.
如果要启用RDB,需要在在redis.conf配置如下。
//900秒内有1次写入,则会触发BGSAVE命令,执行RDB持久化
save 900 1
save 300 10
save 60 10000
dbfilename "dump.rdb" #持久化文件名称
dir "./" #持久化数据文件存放的路径
RDB的优点
(1)RDB是一个紧凑的单一文件,它保存了某个时间点得数据集,非常适用于数据集的备份。
(2)RDB在保存RDB文件时父进程唯一需要做的就是fork出一个子进程,接下来的工作全部由子进程来做,父进程不需要再做其他IO操作,所以RDB持久化方式可以最大化redis的性能.
(3)与AOF相比,在恢复大的数据集的时候,RDB方式会更快一些.
RDB的缺点
(1)如果你希望在redis意外停止工作(例如电源中断)的情况下丢失的数据最少的话,那么RDB不适合你.会丢失两次备份之间这段时间内数据的新增,删除,以及变化。
(2)RDB 需要经常fork子进程来保存数据集到硬盘上,当数据集比较大的时候,fork的过程是非常耗时的,可能会导致Redis在一些毫秒级内不能响应客户端的请求.如果数据集巨大并且CPU性能不是很好的情况下,这种情况会持续1秒。(这个过程的原理是一个CopyOnWrite机制,将原来的数据复制到一个新的空间进行修改,读写分离的思想)
fork()用于创建一个进程,所创建的进程复制父进程的代码段/数据段/BSS段/堆/栈等所有用户空间信息;在内核中操作系统重新为其申请了一个PCB,并使用父进程的PCB进行初始化;
- 持久化之增量写入:AOF
与RDB的保存整个redis数据库状态不同,AOF是通过保存对redis服务端的写命令(如set、sadd、rpush)来记录数据库状态的,即保存你对redis数据库的写操作.
如果要启用AOF,需要在在redis.conf配置如下。
dir "./" #AOF文件存放目录
3 appendonly yes #开启AOF持久化,默认关闭
4 appendfilename "appendonly.aof" #AOF文件名称(默认)
5 appendfsync no #AOF持久化策略
6 auto-aof-rewrite-percentage 100 #触发AOF文件重写的条件(默认)
7 auto-aof-rewrite-min-size 64mb #触发AOF文件重写的条件(默认)
AOF 优点
(1)默认的fsync策略是1秒,一旦出现故障,你最多丢失1秒的数据.(这个策略可以配置)
(2)Redis 可以在 AOF 文件体积变得过大时,自动地在后台对 AOF 进行重写: 重写后的新 AOF 文件包含了恢复当前数据集所需的最小命令集合。
(3)AOF 文件有序地保存了Redis执行的所有写入操作, 这些写入操作以 Redis 协议的格式保存, 因此 AOF 文件的内容非常容易被人读懂, 对文件进行分析也很轻松。
AOF 缺点
(1)对于相同的数据集来说,AOF 文件的体积通常要大于 RDB 文件的体积。
(2)根据所使用的 fsync 策略,AOF 的速度可能会慢于 RDB 。 在一般情况下, 每秒 fsync 的性能依然非常高, 而关闭 fsync 可以让 AOF 的速度和 RDB 一样快, 即使在高负荷之下也是如此。 不过在处理巨大的写入载入时,RDB 可以提供更有保证的最大延迟时间。
3. Redis事务
事务提供了一种“将多个命令打包, 然后一次性、按顺序地执行”的机制, 并且事务在执行的期间不会主动中断 —— 服务器在执行完事务中的所有命令之后, 才会继续处理其他客户端的其他命令,通常如下。
> MULTI
OK
> SET book-name "Mastering C++ in 21 days"
QUEUED
> GET book-name
QUEUED
> SADD tag "C++" "Programming" "Mastering Series"
QUEUED
> SMEMBERS tag
QUEUED
> EXEC
1) OK
2) "Mastering C++ in 21 days"
3) (integer) 3
4) 1) "Mastering Series"
2) "C++"
3) "Programming"
一个事务从开始到执行会经历以下三个阶段:
- 开始事务
这个命令唯一做的就是, 将客户端的 REDIS_MULTI 选项打开, 让客户端从非事务状态切换到事务状态。
image.png
- 命令入队
命令执行过程如下:
image.png
当客户端处于非事务状态下时, 所有发送给服务器端的命令都会立即被服务器执行。
> SET msg "maskwang"
OK
> GET msg
"maskwang"
但是, 当客户端进入事务状态之后, 服务器在收到来自客户端的命令时, 不会立即执行命令, 而是将这些命令全部放进一个事务队列里, 然后返回 QUEUED , 表示命令已入队:
> MULTI
OK
> SET msg "maskwang"
QUEUED
> GET msg
QUEUED
事务队列是一个数组, 每个数组项是都包含三个属性:
要执行的命令(cmd),命令的参数(argv),参数的个数(argc)。
- 执行事务
(1)如果客户端正处于事务状态, 那么当 EXEC 命令执行时, 服务器根据客户端所保存的事务队列, 以先进先出(FIFO)的方式执行事务队列中的命令。
(2)执行事务中的命令所得的结果会以 FIFO 的顺序保存到一个回复队列中。
(3)当事务队列里的所有命令被执行完之后,EXEC 命令会将回复队列作为自己的执行结果返回给客户端, 客户端从事务状态返回到非事务状态, 至此, 事务执行完毕。
需要注意的地方:
除了 EXEC 之外, 服务器在客户端处于事务状态时, 不加入到事务队列而直接执行的另外三个命令是 DISCARD 、 MULTI 和 WATCH 。
- 带 WATCH 的事务
WATCH 命令用于在事务开始之前监视任意数量的键: 当调用 EXEC 命令执行事务时, 如果任意一个被监视的键已经被其他客户端修改了, 那么整个事务不再执行, 直接返回失败。
redis> WATCH name
OK
> MULTI
OK
> SET name mask
QUEUED
> EXEC
(nil)
带 WATCH 的事务是以乐观锁的形式执行的,也就是说先执行,再判断所监控的键有没有变化,是一种CAS的思想。
- DISCARD
DISCARD命令用于取消一个事务, 它清空客户端的整个事务队列, 然后将客户端从事务状态调整回非事务状态, 最后返回字符串 OK 给客户端, 说明事务已被取消。
Redis发布订阅
Redis 通过 PUBLISH、 SUBSCRIBE等命令实现了订阅与发布模式, 这个功能提供两种信息机制, 分别是订阅/发布到频道和订阅/发布到模式。这个功能和消息队列类似于rabbitmq,rocketmq等消息队列的用处一样。由于Redis在这方面的性能表现不及专门的消息队列好,但是在小规模下,依然有应用的场景。
- 频道的订阅和发布
Redis的SUBSCRIBE命令可以让客户端订阅任意数量的频道, 每当有新信息发送到被订阅的频道时, 信息就会被发送给所有订阅指定频道的客户端。
当有新消息通过PUBLISH命令发送给频道
channel1
时, 这个消息就会被发送给订阅它的三个客户端:image.png
原理:通过一个pubsub_channels的字典来实现的,字典的键为频道,与每个频道对应的值为一个链表,对应着订阅了某频道的客户端。
image.png
- 模式的订阅与信息发送
当使用 PUBLISH命令发送信息到某个频道时, 不仅所有订阅该频道的客户端会收到信息, 如果有某个/某些模式和这个频道匹配的话, 那么所有订阅这个/这些频道的客户端也同样会收到信息。
下图展示了一个带有频道和模式的例子, 其中 tweet.shop.*
模式匹配了 tweet.shop.kindle
频道和 tweet.shop.ipad
频道, 并且有不同的客户端分别订阅它们三个:
当有信息发送到 tweet.shop.kindle 频道时, 信息除了发送给 clientX 和 clientY 之外, 还会发送给订阅 tweet.shop.* 模式的 client123 和 client256 。模式的订阅与发布的实现原理和频道的相似,只不过是现在字典key是一个模式,利用正则表达式匹配客户端而已。
Redis实现分布式锁
分布式锁一般有三种实现方式:1. 数据库乐观锁;2. 基于Redis的分布式锁;3. 基于ZooKeeper的分布式锁,实现redis锁需要确保锁的实现同时满足以下四个条件:
- 互斥性:在任意时刻,只有一个客户端能持有锁。
- 无死锁:即使有一个客户端在持有锁的期间崩溃而没有主动解锁,也能保证后续其他客户端能加锁。
- 容错性:只要大部分的Redis节点正常运行,客户端就可以加锁和解锁。
- 解锁和加锁必须是同一个对象:加锁和解锁必须是同一个客户端,客户端自己不能把别人加的锁给解了。
网上的实现:https://yq.aliyun.com/articles/307547?utm_content=m_37928
官方推荐的实现Redisson实现:https://redisson.org/
Redis实现Session共享
在tomcat等web容器中,Session是保存在本机内存中。如果我们对tomcat做集群,不可避免要涉及到Session同步的问题,必须保证同一个集群中的tomcat的Session是共享的,因此如何让Session在不同的节点之间共享就成为关键之一。通常来说有Session sticky,redis保存Session等方式。
这里采用的是Spring Session+Redis简单实现的,把Session存储在Redis里面。
- pom文件
<!-- spring 引入 session 信息存储到redis里的依赖包 -->
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>
- application.properties里面配置如下
# session的存储方式的类型配置
spring.session.store-type=redis
# session 存活时间
server.session.timeout=300
配置完之后,就可以跟平常一样获取Session。
HttpSession httpSession = request.getSession();
SpringSession实现会话共享的关键代码是通过SessionRepositoryFilter
这个过滤器拦截每个每个请求,通过 Filter 将使用我们上文的SessionRepositoryRequestWrapper
封装HttpServletRequest
请求,之后每次获取Session,都是通过 SessionRepositoryRequestWrapper
来获取。如果Redis存在,则Redis里面获取,否则生成新的,放入到Redis里面。
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
request.setAttribute(SESSION_REPOSITORY_ATTR, this.sessionRepository);
SessionRepositoryFilter<S>.SessionRepositoryRequestWrapper wrappedRequest = new SessionRepositoryFilter.SessionRepositoryRequestWrapper(request, response, this.servletContext);
SessionRepositoryFilter<S>.SessionRepositoryResponseWrapper wrappedResponse = new SessionRepositoryFilter.SessionRepositoryResponseWrapper(wrappedRequest, response);
HttpServletRequest strategyRequest = this.httpSessionStrategy.wrapRequest(wrappedRequest, wrappedResponse);
HttpServletResponse strategyResponse = this.httpSessionStrategy.wrapResponse(wrappedRequest, wrappedResponse);
try {
filterChain.doFilter(strategyRequest, strategyResponse);
} finally {
wrappedRequest.commitSession();
}
}
上述问题是最常见的,我也是抛砖引玉。另外觉得写的有帮助的话,麻烦点下二维码关注下。你的关注是我不断创作的动力。
参考文章:
Redis 分布式锁的正确实现方式( Java 版 )
redis设计与实现
Redis持久化
Spring Boot系列十二 通过redis实现Tomcat集群的Session同步及从源码分析其原理