Java后端学习路线β阶段--Redis

64 阅读23分钟

1、Redis是什么?

小汪:Redis是什么,它和MySQL有什么区别?

大榜:Redis是一种非关系型数据库,与MySQL的区别是:MySQL是关系型数据库,存储了表结构和表的字段等信息,可以看成是一个个二维表格,而且MySQL的数据是存储在硬盘上;而Redis是一种非关系型数据库,存储的是key-value键值对,而且Redis的数据是存储在内存中。

小汪:这么一对比,我就懂了。那Redis支持哪些数据结构呢?

大榜:Redis的全称是REmote Dictionary Server,根据英文名字,顾名思义的话,Redis是一个远程字典服务器。Redis中的键总是一个字符串对象类型,值可以是下面5种数据类型:字符串对象、列表对象、哈希对象、集合对象、有序集合对象。

小汪:我用学过Java来类比一下的话,Redis中的字符串相当于Java中的String对象;列表相当于Java中的LinkedList;哈希相当于Java中的HashMap;集合相当于Java中的Set;有序集合相当于Java中的TreeSet。这样类比之后,我一下子就记住了Redis的5种数据类型。

大榜:哈哈哈,是啊,这就叫知识类比迁移能力。更进一步地话,这5种数据类型中,每种数据类型在不同场景下会存在编码转换。举个栗子,列表支持ziplist和linkedlist两种编码方式,只有当列表对象同时满足下面2个条件时,列表对象才会使用ziplist编码:

1)列表对象保存的所有字符串元素的长度都小于64字节;
2)列表对象保存的元素数量小于512字节。

如果不能同时满足上述的2个条件,列表对象将采用linkedlist编码。这样做的好处是可以优化每种数据类型在不同使用场景下的使用效率。

小汪:你的意思是不是下面这样:对于使用ziplist编码的列表对象来说,当使用ziplist编码时,发现不能同时满足上述的2个条件时,对象就会做编码转换操作。编码转换操作具体流程:就是将原本保存在压缩列表中的所有列表元素都会被转移,并保存到双端链表linkedlist里面,列表对象的编码也会从ziplist变为linkedlist。是这样意思把?

大榜:你描述得很准确。

2、Redis能干嘛?

小汪:我知道Redis是个什么玩意了之后,那Redis能干嘛?有什么用呢?

大榜:Redis有很多功能,可以作为缓存、消息队列,还可以做持久化,也可以实现分布式锁。业界使用Redis最多的是作为缓存、作为分布式锁的实现。

2.1、Redis的缓存功能

小汪:缓存,我知道是个啥,它是为了提高速度,比如在CPU和内存之间加一个缓存,就可以大大提高CPU获取指令的速度。

大榜:是滴了,《码农翻身》书中就举了一个形象的比喻来说明缓存--坐飞机的怎么和坐驴车的打交道。书中,将坐飞机的类比为CPU,将坐驴车的类比为内存,CPU和内存之间打交道就是通过缓存。关于缓存的思想与背景,可以参考这篇文章:缓存的应用

小汪:这篇文章介绍的很全面,讲解了缓存的思想与背景,还介绍了Web服务中的缓存。我记得Web后端服务中使用缓存,一般是在MySQL数据库前面放一层Redis缓存,用户请求数据库时,先从缓存中获取,如果缓存中有,直接返回给用户;如果缓存中没有,再从数据库中找,然后将查找的数据写入到缓存中。

大榜:是滴了。有一点要提醒你,如果你开发的Web服务,访问量少得可怜,就没必要使用Redis了。因为多用了一个东西Redis,毕竟还是会增加复杂性,复杂性越高越不好控制,我们设计一个软件架构,就是要让它在够用的前提下尽可能简单,这包括实现简单、控制简单、维护简单。

2.2、Redis的分布式锁

小汪:分布式锁是什么,我只听过synchronized锁?

大榜:synchronized锁是针对单个JVM的,如果我们需要对某个共享变量进行多线程同步访问时,就可以使用synchronized锁进行同步访问,这是单机应用。想象一下,随着业务飞速发展,你的应用就需要做集群部署,也就是将一个应用部署到多台机器上然后做负载均衡,如下图:

image.png

上图可以看到,变量A存在三台服务器各自的内存中(变量A是一个有状态的变量),如果有3个请求分别操作3个不同内存区域的数据,由于每台服务器中都存在一个变量A,这3个变量A之间不存在共享,显然处理结果是不对的。解决思路是将变量A移到共享内存中,并且使用锁来控制共享内存中变量A的同步访问。

小汪:我懂你的意思了。在传统的单体应用且单机部署的情况下,我们可以使用synchronizeds锁进行同步访问。但随着业务的发展,原来的单机部署的系统演化为分布式集群后,由于分布式系统部署在不同的机器上,这使得原单机部署情况下的并发控制锁策略(synchronized锁是其中的一种策略)失效。为了解决跨机器的互斥机制来控制共享资源的访问,分布式锁就产生了。

大榜:你说得很对,所以说synchronized锁也称之为单机版锁,与分布式锁的概念区分开来。分布式锁有3种实现方式:

1)基于数据库实现分布式锁:使用唯一索引、主键索引来实现分布式锁。

2)基于Redis实现分布式锁:一种方法是使用setnx指令和expire指令来实现分布式锁;另一种方法是使用redission实现,redission的底层是使用redis和lua脚本来实现多条指令的原子性操作。

3)基于Zookeeper实现分布式锁:使用Zookeeper的客户端Curator来实现分布式锁。

小汪:我记得上操作系统课的时候,老师挂在嘴边的一句话就是:"使用锁的话,我们要特别小心,一定要避免死锁情况。"

我猜,基于Redis实现分布式锁中,expire指令是为键设置一个超时时间,超过这个时间锁会自动释放,用来避免死锁,是这样吗?

大榜:你说得很对。使用锁的时候,需要设置锁的过期时间,防止死锁。

3、Redis的单机特性

小汪:接下来我们应该要讨论Redis的单机特性了把,我记得有事务、消息队列、持久化功能。

大榜:是的了。我觉得学习Redis要建立客户端-服务器的基本思想,因为对于程序员来说,一般都是使用Redis客户端来连接、访问Redis服务器。Redis服务器使用单线程单进程的方式来处理命令请求,并与多个客户端进行网络通信。客户端与服务器的关系如下图:

image.png

3.1、Redis的事务功能

小汪:Redis的事务功能,和MySQL的事务有啥区别?

大榜:关系型数据库如MySQL,用ACID特性来检验事务功能的可靠性和安全性。ACID分别成为原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)、持久性(Durability)。

MySQL中的事务满足ACID特性。

对于Redis中的事务,具有原子性、一致性、隔离性,当服务器运行在AOF持久化模式下,并且appendfsync选项的值为always时,事务具有持久性。Redis的事务和传统关系型数据库事务的最大区别在于,Redis不支持事务回滚机制,即使事务队列中的某个命令在执行期间出现了错误,整个事务也会继续执行下去,直到将事务队列中的所有命令都执行完毕为止。

3.2、Redis的发布与订阅

小汪:Redis的发布与订阅都有哪些功能?我感觉类似于RabbitMQ的消息队列。

大榜:是的。Redis的发布相当于RabbitMQ的生产者,订阅相当于RabbitMQ的消费者,RabbitMQ的介绍及入门,可以参考拙作:SpringBoot与RabbitMQ整合,发送和接收消息实战(代码可运行)

Redis中的订阅有2种,一种是频道的订阅,另一种是模式的订阅。当然,Redis也支持频道的退订、模式的退订功能。

小汪:频道订阅、模式订阅有啥区别吗?

大榜:频道订阅:当一个客户端执行订阅命令,订阅一个或多个频道时,这个客户端与被被订阅频道之间就建立了一种订阅关系,如订阅频道 "news.it"

模式订阅,如订阅模式 "news.*" ,类似于RabbitMQ的topic主题交换机,支持通配符的匹配方式。

3.3、Redis的持久化

小汪:Redis的持久化,其作用是把数据保存到硬盘中。

大榜:是滴了。Redis是内存式数据库,数据是保存在内存中的,我们都知道内存读、取的速度远大于硬盘,但内存的缺点是断电后数据丢失,而使用硬盘的好处是即使断电,数据会在硬盘上继续保存着,不会断电丢失。

小汪:我在想,我们编写HTTP接口时,使用MyBatis等持久层框架将数据存储到MySQL数据库中,这是MySQL的持久化功能。那Redis的持久化和MySQL的持久化有什么区别吗?

大榜:对于MySQL来说,它底层最终是将数据存储到磁盘上,所以说数据存储到MySQL中后,数据就已经持久化成功了,即使断电了,存储的数据也不会丢失。但对于Redis来说,底层是将数据放在内存中,如果我们只是将数据保存到Redis中,而没有使用持久化命令,那重启Redis后,原来存储到Redis中的数据就没有了。

小汪:我往Redis中set key1 val1,重启Redis后,键key1没有了,好可怕啊。如果我的数据很重要,那就要使用持久化命令将数据持久化保存起来。榜哥,如果我想要对Redis中的数据进行持久化,该怎么做呢?

大榜:为了解决Redis的持久化问题,Redis提供了两种持久化模式,一种是RDB模式;另一种是AOF模式。这两种模式,都可以将Redis在内存中的数据库状态保存到磁盘里面,避免数据意外丢失。

小汪:那我们先讨论下RDB持久化模式。

3.3.1、RDB持久化模式

大榜:RDB持久化既可以手动执行,也可以根据服务器的配置选项定期执行,RDB持久化功能可以将某个时间点上的数据库状态保存到一个RDB文件中,如下图所示:

image.png

小汪:保存为RDB文件,类似于生成快照文件,后续根据快照文件还原得到数据库的状态,是这样把?

大榜:是的。RDB文件是一个经过压缩的二进制文件,通过该文件可以还原生成RDB文件时的数据库状态,如下图所示:

image.png

你看,RDB文件是保存在硬盘里面的,所以即使Redis服务器进程退出,甚至运行Redis服务器的计算机停机,只要RDB文件仍然存在,Redis服务器就可以用它来还原数据库的状态。

小汪:那RDB持久化如何使用呢?

大榜:有2个命令可以用于生成RDB文件,一个是SAVE命令,另一个是BGSAVE命令。SAVE命令会阻塞Redis服务器进程,直到RDB文件创建完毕为止,在服务器进程阻塞期间,服务器不能处理任何命令请求。而BGSAVE命令不会阻塞服务器进程,它会派生出一个子进程,然后由子进程负责创建RDB文件,服务器进程(父进程)继续处理命令请求。

小汪:你上面提高RDB持久化既然可手动执行,也可以根据服务器的配置选项定期自动地执行。你刚刚说的SAVE、BGSAVE命令,我们通过手动输入这些命令来进行持久化。那如何根据服务器的配置选项自动进行持久化呢?

大榜:Redis给我们提供了配置选项,放在了redis.conf配置文件中,配置内容是下面这样的:

save 900 1            服务器在900秒之内,对数据库进行了至少1次修改;
save 300 10           服务器在300秒之内,对数据库进行了至少10次修改;
save 60 10000         服务器在60秒之内,对数据库进行了至少10000次修改;

我们开启上面的配置选项后,只要满足以上3个条件中的任意一个,BGSAVE命令就会被执行。

3.3.2、AOF持久化模式

小汪:RDB持久化挺好用的,感觉AOF持久化没有必要了?

大榜:AOF的全称是Append Only File。RDB持久化是通过保存数据库中的键值对来记录数据库的状态,而AOF持久化是通过保存Redis服务器所执行的写命令来记录数据库状态的,如下图所示:

image.png

小汪:保存Redis服务器所执行的写命令,是啥意思?榜哥又说人话了啊。

大榜:哈哈哈,那举个栗子,比如你执行了SET、SADD、RPUSH这3条命令,代码是这样的:

image.png

RDB持久化保存数据库状态的方法是将msg、fruits、numbers三个键值对保存到RDB文件中,不会存储SET、SADD、RPUSH命令;而AOF持久化则是将服务器执行的SET、SADD、RPUSH这3条命令保存到AOF文件中。

小汪:我懂了,AOF持久化是将写命令追加到AOF的。

大榜:AOF持久化功能的实现分为命令追加、文件写入、文件同步。redis.conf配置文件中,appendfsync选项的值,分为如下3类,分别是always、everysec、no。appendfsync选项的默认值为everysec,即每秒将写命令同步到AOF缓存区。

image.png

大榜:AOF持久化是通过保存被执行的写命令来记录服务器状态的,所以随着服务器运行时间的流逝,AOF文件中的内容会越来越多,文件的体积会越来越大,如果不加以控制的话,体积过大的AOF文件很可能对Redis服务器、甚至整个宿主计算机造成影响。所以,为了解决AOF文件体积膨胀的问题,Redis提供了AOF文件重写功能。通过该功能,Redis服务器可以创建一个新的AOF文件来替代掉现有的AOF文件。

小汪:我懂了,AOF重写的作用就是为了减少AOF文件的体积。那AOF重写的流程是什么样的?

大榜:流程如下所示:

image.png

4、Redis的多机特性

小汪:接下来我们讨论Redis的多机特性,也就是多台Redis服务器在一起时,会有哪些特性,主要包含主从服务器的复制、哨兵部署、集群部署这3大功能。

4.1、Redis的主从复制

大榜:主从复制是指当用户指定一个服务器(从服务器)去复制另一个服务器(主服务器)时,主从服务器之间执行了什么操作,进行了什么数据交互。

小汪:主从复制的作用,应该是保证主从服务器之间的数据同步、数据的一致性。Redis的主从复制是如何实现的呢?

大榜:我们可以使用PSYNC命令来执行主从复制操作。PSYNC具有完整重同步和部分重同步两种模式。其中完整重同步用于处理初次复制的情况,让主服务器创建并发送RDB文件,以及向从服务器发送保存在缓存区里面的写命令来进行同步。

而部分重同步则用于处理断线后重新复制的情况:当从服务器在断线后重新连接主服务器时,如果条件允许,主服务器可以只将主从服务器连接断开期间执行的写命令发送给从服务器,从服务器只要接收并执行这些写命令,就可以将从服务器的数据库更新至主服务器当前所处的状态。

主从服务器在执行部分重同步的通信过程如下所示:

image.png

使用PSYNC命令来进行断线后重新复制,时序流程如下:

image.png

你看,在时间T10087,主、从服务器的连接断开,T10088、T10089、T10090这3个时间,执行了3条SET命令;在T10091时间,主从服务器重新连接上了;接着,T10092时间,从服务器发送PSYNC命令给主服务器,主服务器将连接断开期间的执行的写命令,也就是3条SET命令发送给从服务器;在T10096时间,从服务器接收到这3条SET命令并执行,最后主从服务器再次完成同步。

小汪:部分重同步相比于完整重同步,只需要复制 连接断开期间执行的写命令发送给从服务器就可以了,降低了系统的资源消耗,很香啊。那PSYNC中的部分重同步,是如何实现的呢?

大榜:部分重同步是通过如下3部分构成:

1)主服务器的复制偏移量和从服务器的复制偏移量:主服务器和从服务器会分别维护一个复制偏移量;

2)主服务器的复制积压缓存区:主服务器的复制积压缓冲区中,保存着一部分最近传播的写命令和对应每个字节的复制偏移量;

3)主服务器的运行ID:服务器启动时会自动生成运行ID,当进行初次主从复制时,主服务器会将自己的运行ID传送给从服务器,便于从服务器后续使用。

4.2、哨兵Sentinel

小汪:接下来,我们该讨论哨兵了,哨兵是什么啊?

大榜:哨兵,其实只是一个运行在特殊模式下的Redis服务器,它使用了和普通模式不同的命令表,所以哨兵模式能够使用的命令和普通Redis服务器能够使用的命令不同。

小汪:那哨兵能干嘛?

大榜:如果你不使用哨兵,只是使用单台的Redis服务器,当宕机时,我们需要手动去运维和重启这台Redis服务器。如果是在凌晨3点钟宕机,想想都麻烦得一匹。如果用了哨兵之后,哨兵是Redis高可用部署的一种解决方案,由一个或多个哨兵实例组成的哨兵系统可以监视主、从服务器,并在被监视的主服务器进入下线状态时,进行故障转移,并将从服务器升级为新的主服务器。服务器与哨兵系统的关系如下图:

image.png

小汪:感觉Redis底层给我们做了很多事,底层是如何实现哨兵模式的呢?

大榜:哨兵是一个运行在特殊模式下的Redis服务器,哨兵以每秒一次的频率向实例(包括主服务器、从服务器、其他哨兵)发送PING命令,并根据实例对PING命令的回复来判断实例是否在线,当一个实例在指定的时长中连续向哨兵发送无效回复时,哨兵会将这个实例判断为主观下线状态。

小汪:有主观下线状态,应该有客观下线状态把?

大榜:哈哈哈,是滴了。当哨兵将一个主服务器判断为主观下线时,它会向同样监视这个主服务器的其他哨兵进行询问,看它们是否同意这个主服务器已经进入主观下线状态。当哨兵收集到足够多的主观下线投票之后,它会将主服务器判断为客观下线,并发起一次针对主服务器的故障转移操作。

小汪:故障转移,是个新词汇,它干了什么事儿?

大榜:故障转移首先要投票选举出领头哨兵,然后领头哨兵将对已下线的主服务器执行故障转移操作,该操作包含以下3个步骤:

1)在已下线主服务器属下的所有从服务器里面,挑选出一个从服务器,并将其转换为主服务器;

2)让已下线主服务器属下的所有从服务器改为去复制新的主服务器;

3)将已下线主服务器设置为新的主服务器的从服务器,当这个旧的主服务器重新上线时,它就会成为新的主服务器的从服务器。

4.2、Redis集群

小汪:故障转移做了很多事啊,最终的目的是保证Redis的高可用。还有其他的高可用方案吗?

大榜:当然有了,Redis集群是Redis提供的分布式数据库的高可用方案。一个Redis集群是由多个节点组成,集群通过分片来进行数据共享,并提供复制和故障转移功能。

小汪:分片是干什么的?有什么作用?

大榜:分片顾名思义就是将整个数据库分成很多片。Redis集群的整个数据库被分为16384个槽,数据库中的每个键都属于这16384个槽中的其中一个,集群中的每个节点可以处理0或最多16384个槽。举个栗子,你部署了3个节点,分别为节点7000(处理槽0至5000)、节点7001(处理槽5001至10000)、节点7002(处理槽10000至16383),当给节点分配了对应的槽后,Redis集群就上线了,可以提供服务给客户端了。

小汪:有个问题啊,如果我们向拥有3个节点的集群中,再添加一个节点7003呢,那原来的16384个槽,应该有一部分是需要移动到节点7003的把?

大榜:是的,这叫做重新分片。也就是对槽进行重新分片,需要注意的是:重新分片只会移动部分槽到新的节点7004中,不会对所有的槽进行移动,因为槽的移动也就是数据移动,会占用很大的系统资源开销。所以说,重新分片会将16384个槽均匀分配到这4个节点中:节点7000、节点7001、节点7002、节点7003。

小汪:如果增加或者一个节点,需要移动所有的槽数据,那CPU、内存的开销会相当大,影响Redis服务器的正常对外服务。所以说,将Redis整个数据库划分为16384个槽后,如果节点挂掉或新增节点,只需要移动一部分槽数据,这个设计思想好牛逼啊。Redis集群提供复制和故障转移功能,复制是干什么用的?

大榜:复制也就是4.1节我们一起讨论的主从复制,通过PSYNC命令来保证主从服务器的一致性。Redis集群中的节点分为主节点和从节点,其中主节点用于处理槽,而从节点则用于复制某个主节点,并在被复制的主节点下线时,代替下线主节点继续处理命令请求。

汪:Redis集群的故障转移功能,和哨兵的故障转移有什么区别吗?

大榜:Redis集群的故障转移多了一个槽指派。执行步骤如下:

1)复制下线主节点的所有从节点里面,会投票选中一个从节点,成为新的主节点;

2)新的主节点会撤销所有对已下线主节点的槽指派,并将这些槽全部派给自己;

3)新的主节点向集群中广播一个消息,让集群中的其他节点立即知道这个节点已经由从节点变成了主节点,并且这个主节点已经接管了原本由已下线节点负责处理的槽;

4)新的主节点开始接收和自己负责处理的槽有关的命令请求,故障转移完成。

5、Redis怎么玩?

小汪:Redis集群的高可用,Redis底层做了很多脏活累活啊。Redis如何使用呢?

大榜:使用Redis要有服务器-客户端的概念,我们首先要安装Redis服务器,然后通过Redis客户端与服务器进行命令请求。如何使用的话,可以参考菜鸟教程-Redis

6、Redis的踩坑点

小汪:使用Redis有哪些踩坑点呢?

大榜:第一个踩坑点就是:如果项目中没有使用缓存、分布式锁的需求,就没必要使用Redis,因为软件架构设计就是要保证满足需求的前提下尽可能简单;如果确实有需求,才去使用Redis。

第二个踩坑点:是在高并发场景中才会存在,如在秒杀业务场景中,使用缓存需要考虑缓存穿透、缓存击穿、缓存雪崩,以及对应的解决方案

7、总结

通过小汪和大榜的对话,我们一起讨论了Redis作为缓存、分布式锁的实现,接着讨论了单机版Redis的特性,紧接着探讨了集群版Redis的原理实现。黄健宏编写的这本书《Redis设计与实现》真心不错,把Redis的实现原理讲解得深入浅出,适合那些想要了解Redis原理的程序员们。

8、参考内容

1、Vue + Spring Boot 项目实战(二十一):缓存的应用

2、分布式锁概述

3、《Redis设计与实现(第二版)》-黄健宏

4、菜鸟教程-Redis