大数据程序员程序员技术交流

Redis面试必备知识点

2018-07-28  本文已影响15人  maskwang520

1. Redis五种基本的数据结构

    这是最简单Redis类型。如果你只用这种类型,Redis就像一个可以持久化的memcached服务器.key,value就是字符串.


string.jpg
set mykey maskwang
get mykey
//设置多个值
mset a 10 b 20 c 30
//设置5秒的过期
expire key 5

    一个列表结构可以有序的存储多个字符串,多个字符串是可以重复的。


未命名文件 (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"

    Redis的集合和列表都可以存储多个字符串,不同之处在于,列表可以存储多个相同的字符串,而集合则通过散列表保证自己存储的每个字符串都是各不相同的。


未命名文件 (3).jpg
> sadd myset 1 2 3
(integer) 3
> smembers myset
1. 3
2. 1
3. 2

    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"

    有序集合和散列一样,都用于存储键值对:有序集合的键成为成员(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持久化是把当前进程数据生成快照保存到硬盘的过程.类似于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进行初始化;

    与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 命令用于在事务开始之前监视任意数量的键: 当调用 EXEC 命令执行事务时, 如果任意一个被监视的键已经被其他客户端修改了, 那么整个事务不再执行, 直接返回失败。

redis> WATCH name
OK
> MULTI
OK
> SET name mask
QUEUED
> EXEC
(nil)

    带 WATCH 的事务是以乐观锁的形式执行的,也就是说先执行,再判断所监控的键有没有变化,是一种CAS的思想。

     DISCARD命令用于取消一个事务, 它清空客户端的整个事务队列, 然后将客户端从事务状态调整回非事务状态, 最后返回字符串 OK 给客户端, 说明事务已被取消。

Redis发布订阅

    Redis 通过 PUBLISH、 SUBSCRIBE等命令实现了订阅与发布模式, 这个功能提供两种信息机制, 分别是订阅/发布到频道和订阅/发布到模式。这个功能和消息队列类似于rabbitmq,rocketmq等消息队列的用处一样。由于Redis在这方面的性能表现不及专门的消息队列好,但是在小规模下,依然有应用的场景。

    Redis的SUBSCRIBE命令可以让客户端订阅任意数量的频道, 每当有新信息发送到被订阅的频道时, 信息就会被发送给所有订阅指定频道的客户端。

image.png
    当有新消息通过PUBLISH命令发送给频道 channel1 时, 这个消息就会被发送给订阅它的三个客户端:
image.png
    原理:通过一个pubsub_channels的字典来实现的,字典的键为频道,与每个频道对应的值为一个链表,对应着订阅了某频道的客户端。
image.png

    当使用 PUBLISH命令发送信息到某个频道时, 不仅所有订阅该频道的客户端会收到信息, 如果有某个/某些模式和这个频道匹配的话, 那么所有订阅这个/这些频道的客户端也同样会收到信息。

    下图展示了一个带有频道和模式的例子, 其中 tweet.shop.* 模式匹配了 tweet.shop.kindle 频道和 tweet.shop.ipad 频道, 并且有不同的客户端分别订阅它们三个:

image.png
    当有信息发送到 tweet.shop.kindle 频道时, 信息除了发送给 clientX 和 clientY 之外, 还会发送给订阅 tweet.shop.* 模式的 client123 和 client256 。模式的订阅与发布的实现原理和频道的相似,只不过是现在字典key是一个模式,利用正则表达式匹配客户端而已。

Redis实现分布式锁

    分布式锁一般有三种实现方式:1. 数据库乐观锁;2. 基于Redis的分布式锁;3. 基于ZooKeeper的分布式锁,实现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里面。

<!-- spring 引入 session 信息存储到redis里的依赖包  -->
        <dependency>
            <groupId>org.springframework.session</groupId>
            <artifactId>spring-session-data-redis</artifactId>
        </dependency>
# 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同步及从源码分析其原理

qrcode_for_gh_2d4794fe3d16_258.jpg
上一篇下一篇

猜你喜欢

热点阅读