Java后端高频知识点学习笔记7---Redis

参考地址:牛 _ 客 _ 网
https://www.nowcoder.com/discuss/819310

1、IO多路复用

I/O 多路复用模型是利用select、poll、epoll可以同时监察多个流的 I/O 事件的能力,在空闲的时候,会把当前线程阻塞掉,当有一个或多个流有I/O事件时,就从阻塞态中唤醒,于是程序就会轮询一遍所有的流(epoll是只轮询那些真正发出了事件的流),依次顺序的处理就绪的流,这种做法就避免了大量的无用操作;这里“多路”指的是多个网络连接,“复用”指的是复用同一个线程

采用多路 I/O 复用技术可以让单个线程高效的处理多个连接请求(尽量减少网络IO的时间消耗),且Redis在内存中操作数据的速度非常快(内存内的操作不会成为这里的性能瓶颈),主要以上两点造就了Redis具有很高的吞吐量

2、Redis中IO多路复用

redis的服务器在处理客户端的请求时,是通过socket处理的,客户端是通过socket和服务端相连的

当客户端和服务器在连接、关闭、写入、返回4个时刻是需要进行处理的

用一个线程(IO多路复用程序)监听多个socket的状态,需要处理再进行处理

IO多路复用程序只负责监听,如果同时有多个socket需要处理,所以需要一个队列,让事件有序的进行处理

当IO多路复用程序监听到socket处于这样的状态时,就将这个socket对象放到队列中,让当前需要处理的socket对象变得有序

文件事件分派器通过读取队列中的socket逐个进行处理,因为socket的状态是不同的,所以不同的socket需要进行不同的处理,根据每个socket的情况分派给不同的事件处理器进行处理

事件处理器有:命令请求处理器、命令回复处理器、连接应答处理器等

底层实现:select、epoll、evport、kequeue。Redis中IO对路复用程序会根据当前的操作系统选择一个性能最高的实现方式

httclient java 多路复用 java多路复用原理_数据

3、Redis是单线程的,为什么还能这么快

1、对服务端程序来说,线程切换和锁通常是性能杀手,而单线程避免了线程切换和竞争所产生的消耗
2、Redis的大部分操作在内存上完成,这是它实现高性能的一个重要原因
3、Redis采用了IO多路复用机制,使其在网络IO操作中能并发处理大量的客户端请求,实现高吞吐率

httclient java 多路复用 java多路复用原理_Redis_02

4、Redis缓存淘汰策略

① LRU(最近最少使用):是按照最近最少使用的原则筛选数据,即最近最少被使用的数据会被筛选出来

标准LRU:把所有的数据组成一个链表,表头和表尾分别表示最常使用端和最少使用端;刚被访问的数据和新增的数据会被移动到最常使用端,当链表的空间被占满时,它会删除最少使用端的数据

近似LRU:Redis会记录每个数据的最近一次访问的时间戳;近似LRU会随机采样N个key,然后淘汰掉最旧的key,若淘汰后的内存仍然超出限制,则继续采样淘汰

缺点:若一个key很少被访问,只是偶尔被访问了一次,则它旧被认为是热点数据,短时间内不会被淘汰

② LFU(最近使用频率):它根据key的最近访问频率进行淘汰;每个数据都有一个计数器,记录这个数据的访问次数。当使用LFU策略淘汰数据时,首先会根据数据的访问次数进行筛选,把访问次数最低的数据淘汰出内存;如果两个数据的访问次数相同,LFU再比较这两个数据的访问时间,把访问时间更早的数据淘汰出内存

默认的缓存淘汰策略:
noeviction:默认策略,不淘汰任何数据,如果内存达到了设定的值,再添加数据时会报错

5、Redis的过期策略

1、惰性删除:当访问一个key的时候,Redis会先检查它的过期时间,如果发现过期就立即删除这个key

优点:删除操作只发生在从redis中取出key的时候发生,而且只删除当前key,所以对CPU的时间占用是比较少的

缺点:若大量的key在超出超时时间后,很多一段时间内都没有被获取过,那么可能发生内存泄漏

2、定期删除:Redis会将设置了过期时间的key放入一个独立的字典中,以后会定时遍历这个字典来删除过期的key,Redis默认每秒进行10次过期扫描

  • 过期扫描不会遍历过期字典中所有的key,而是采用一种简单的贪心策略。步骤如下:
    1、先从过期字典中随机选出20个key
    2、删除这20个key中已经过期的key
    3、如果过期的key的比例超过25%,就重复步骤1
    同时,为了保证过期扫描不会出现循环过度,导致线程卡死的现象,算法还增加了扫描时间的上限,默认不会超过25ms

优点:定期删除,会在一段时间后主动删除过期的key,不会造成内存泄漏问题

缺点:因为是采用贪心的策略,因此存在部分过期的key仍然没有被删除。如果过期的key较多,也会占用很多CPU时间

6、Redis持久化机制

Redis支持 RDB持久化AOF持久化RDB-AOF混合持久化 三种持久化方式

1、RDB持久化(默认方式)

  • RDB持久化是Redis默认采用的持久化方式,它以快照的形式将数据持久化到硬盘中。RDB会创建一个经过压缩的二进制文件,文件以“.rdb”结尾,内部存储了各个数据库的键值对数据等信息

RDB持久化的触发方式有两种:
1、手动触发:通过SAVE或BGSAVE命令触发RDB持久化操作,创建“.rdb”文件
2、自动触发:通过配置选项,让服务器在满足指定条件时自动执行BGSAVE命令

  • SAVE命令执行期间,Redis服务器将阻塞,直到“.rdb”文件创建完毕为止。而BGSAVE命令是异步版本的SAVE命令,它会使用Redis服务器进程的子进程,创建“.rdb”文件;BGSAVE命令在创建子进程时会存在短暂的阻塞,之后服务器便可以继续处理其他客户端的请求;总之,BGSAVE命令是针对SAVE阻塞问题做的优化,Redis内部所有涉及RDB的操作都采用BGSAVE的方式,而SAVE命令已经废弃!

优点:RDB恢复数据的速度非常快。RDB生成压缩的二进制文件,体积小
缺点:RDB持久化没法做到实时的持久化(在执行BGSAVE命令时会创建子进程,会存在短暂的阻塞)

2、AOF持久化

  • AOF持久化是以独立日志的方式,记录了每次写入命令;可通过执行AOF文件中的命令来恢复数据
    AOF持久化解决了数据持久化的实时性,是Redis目前持久化的主流方式
    AOF存储的是协议文本,体积要比二进制格式的“.rdb”文件大很多
    AOF在进行重写时也需要创建子进行,在数据库体积较大时将占用大量资源,会导致服务器的短暂阻塞

优点:与RDB持久化可能丢失大量的数据相比,AOF可以将数据丢失的时间窗口限制在1s之内
缺点:AOF需要执行AOF文件中的命令来恢复数据库,恢复速度比RDB慢很多

3、RDB-AOF混合持久化;Redis从4.0开始引入RDB-AOF持久化模式,这种模式是基于AOF持久化构建而来的

RDB-AOF混合持久化将会根据数据库当前的状态生成RDB数据,并将其写入AOF文件;对于持久化开始到持久化结束这段时间执行的Redis命令,将以协议文本的方式追加到AOF文件的末尾,即RDB数据之后

优点:通过使用RDB-AOF混合持久化,用户可以同时获得RDB持久化和AOF持久化的优点,服务器既可以通过AOF文件包含的RDB数据来实现快速的数据恢复操作,又可以通过AOF文件包含的AOF数据来将丢失数据的时间窗口限制在1s之类

缺点:兼容性差,一旦开启了混合持久化,在4.0之前版本都不识别该 AOF 文件,同时由于前部分是 RDB 格式,阅读性较差

7、Redis事务

Redis 通过 MULTI、EXEC、WATCH 等命令来实现事务(transaction)功能;事务提供了⼀种将多个命令请求打包,然后⼀次性、按顺序地执⾏多个命令的机制,并且在事务执⾏期间,服务器不会中断事务⽽改去执⾏其他客户端的命令请求,它会将事务中的所有命令都执⾏完毕,然后才去处理其他客户端的命令请求

在传统的关系式数据库中,常常⽤ ACID 性质来检验事务功能的可靠性和安全性;在 Redis 中,事务总是具有原⼦性(Atomicity)、⼀致性(Consistency)和隔离性(Isolation),并且当 Redis 运⾏在某种特定的持久化模式下时,事务也具有持久性(Durability)

  • 【注意】
    事务具有原子性指的是,数据库将事务中的多个操作当做一个整体来执行,服务器要么执行事务中的所有操作,要么就一个操作也不执行
    对于Redis的事务功能来说,事务队列中的命令要么就全部都执行,要么就一个都不执行,因此Redis的事务是具有原子性的

但是!!!

  • Redis的事务和传统的关系型数据库事务的最大区别在于:Redis不支持事务回滚机制,即使事务队列中的某个命令在执行期间出现了错误,整个事务也会继续执行下去,直到将事务队列中的所有命令都执行完毕

解释

  • 作者在事务功能的文档解释说,不支持事务回滚是因为这种复杂的功能和Redis追求简单高效的设计主旨不相符,并且他认为,Redis事务的执行时错误通常都是编程错误产生的,这种错误通常只会出现开发环境中,而很少会在实际生产环境中出现

8、如何保证缓存与数据库数据的一致性

1、先更新缓存,再更新数据库
2、先更新数据库,再更新缓存
3、先删除缓存,再更新数据库
4、先更新数据库,再删除缓存
5、延迟双删

更新缓存还是删除缓存?

更新缓存:
优点:每次数据变化都及时更新缓存,所以查询时不容易出现未命中的情况
缺点:更新缓存消耗比较大。如果数据需要经过复杂的计算再写入缓存,那么频繁的更新缓存,就会影响服务器的性能,如果是写入数据频繁的业务场景,那么可能频繁的更新缓存时,却没有业务读取该数据

删除缓存:
优点:操作简单,无论是更新操作是否复杂,都是将缓存中的数据直接删除
缺点:删除缓存后,下一次查询缓存会出现未命中,这时需要重新读取一次数据库

所以,一般情况下,删除缓存是更优的方案

先删除缓存再更新数据库或者先更新数据库再删除缓存,第二步失败的情况

① 先删除缓存,再更新数据库

可能出现的问题:删除缓存成功,更新数据库失败,最终缓存和数据库的数据是一致的,但是是旧数据

1、进程A删除缓存成功
2、进程A更新数据库失败
3、进程B从缓存中读取数据
4、由于缓存被删,进程B无法从缓存中得到数据,进而从数据库读取数据。
5、进程B从数据库成功获取数据,然后将数据更新到了缓存。

最终:缓存和数据库的数据是一致的,但是是旧数据。而我们的期望是二者数据一致,并且是新的数据

httclient java 多路复用 java多路复用原理_数据_03

② 先更新数据库再删除缓存

可能出现的问题:更新数据库成功,但是删除缓存失败,最终缓存和数据库中的数据是不一致的。

1、进程A更新数据库成功
2、进程A删除缓存失败
3、进程B读取缓存成功,由于缓存删除失败,所以进程B读取到的数据是旧数据

httclient java 多路复用 java多路复用原理_httclient java 多路复用_04

可以看出,无论是先更新数据库还是先删除缓存,都会出现问题;在第二步出现问题的时候,都建议采用重试机制解决;为了避免重试机制影响主要业务的执行,一般建议重试机制采用异步的方式执行

例如:在先更新数据库,再删除缓存的方式。来说明重试机制的主要步骤
1、更新数据库成功
2、删除缓存失败
3、将此数据加入消息队列
4、业务代码消费这条消息的内容,发起重试机制,从缓存中删除这条记录。

httclient java 多路复用 java多路复用原理_缓存_05

先删除缓存再更新数据库或者先更新数据库再删除缓存,不失败的情况

① 先删除缓存再更新数据库

在没有失败时可能出现的问题:在删除缓存和更新数据库之间,其它进程访问了缓存,将数据库中旧数据存入了缓存。最终,缓存中存的是旧数据,数据库中是新数据,二者数据不一致。

1、进程A删除缓存成功
2、进程B读取缓存失败
3、进程B读取数据库成功,得到旧的数据
4、进程B将旧的数据成功的更新到了缓存
5、进程A将新的数据成功更新到了数据库

httclient java 多路复用 java多路复用原理_缓存_06

② 先更新数据库再删除缓存

在没有失败时可能出现的问题:在更新数据库和删除缓存之间,可能出现其他进程访问了旧缓存中的数据。最终缓存和数据库中的数据是一致的且都是新数据。

1、进程A更新数据库成功
2、进程B读取缓存成功
3、进程A更新数缓存成功

httclient java 多路复用 java多路复用原理_缓存_07

最终结论:先更新数据库,再删除缓存这种方案影响更小,如果第二步出现失败,可采用重试机制解决问题

这种情况还可能出现的问题:
1、请求A访问缓存,缓存刚好失效
2、请求A查询数据库,得到一个旧值
3、请求B更新数据库,将新值写入数据库
4、请求B删除缓存
5、请求A将旧值写入缓存

这样,缓存中存的是旧值,数据库中存的是新值,缓存和数据库中的数据不一致

但是,如果要发生这种情况,步骤3的写数据库操作,要比步骤2的读数据库操作耗时更短,才能使步骤4先于步骤5。但是数据库读操作的速度远远大于写操作的速度。因此步骤3比步骤2耗时更短,这一情形很难出现

如果非要解决这种问题:采用先删除缓存,再更新数据库的延迟删除策略,保证读请求完成以后,再进行删除操作

httclient java 多路复用 java多路复用原理_数据_08

推荐方案:延迟双删

如果是先删除缓存,再更新数据库的情况,在第二步没有出现失败时,也可能会导致数据的不一致

在删除缓存和更新数据库之间,其它进程访问了缓存,将数据库中旧数据存入了缓存。最终,缓存中存的是旧数据,数据库中是新数据,二者数据不一致

解决方法:延迟双删
1、删除缓存
2、更新数据库
3、sleep N毫秒
4、再次删除缓存

阻塞一段时间之后,再次删除缓存,就可以把这个过程中缓存中不一致的数据删除掉

httclient java 多路复用 java多路复用原理_数据_09

但是,采用延迟删除缓存,在清空缓存之前还是会有很多请求查询到旧缓存中的数据

解决方法:
1、采用加锁的方法解决。一次性不让太多的线程来请求
2、可以尽量缩短第一次删除缓存和更新数据库的时间差,这样可以使其他事务第一时间获取到更新数据库之后的数据

httclient java 多路复用 java多路复用原理_Redis_10

9、如何利用Redis实现一个分布式锁

单节点的分布式锁

使用Redis实现分布式锁,在redis里存一份代表锁的数据,通常用字符串即可

1、在加锁时就要给锁设置一个标识,进程要记住这个标识。当进程解锁的时候,要进行判断,是自己持有的锁才能释放,否则不能释放。可以为key赋一个随机值,来充当进程的标识
2、解锁时要先判断、再释放,这两步需要保证原子性,否则第二步失败的话,就会出现死锁。而获取和删除命令不是原子的,这就需要采用Lua脚本,通过Lua脚本将两个命令编排在一起,而整个Lua脚本的执行是原子的

。。。。。。

10、Redis常见数据类型及底层实现

Redis常见数据类型

  • 1、String;2、list;3、hash;4、set;5、zset

1、String

String对象的编码可以是int、raw、embstr

  • 1、如果一个字符串对象保存的是整数值,并且这个整数值可以使用long类型来表示,那么字符串对象会将整数值保存在字符串对象结构的ptr属性中,并将字符串对象的编码设置为int
  • 2、如果一个字符串对象保存的是字符串值,并且这个字符串的长度>39字节,那么字符串对象将使用一个简单动态字符串(SDS)来保存这个字符串值,并将对象的编码设置为raw
  • 3、如果一个字符串对象保存的字符串值,并且这个字符串的长度<=39字节,那么字符串对象将使用embstr编码的方式来保存这个字符串值(也是简单动态字符串)
  • embstr编码是专门用于保存短字符串的一种优化编码方式,这种编码和raw编码一样,都使用redisObject结构和sdshdr结构来表示字符串对象,但raw编码会调用两次内存分配函数分别创建redisObject结构和sdshdr结构,而embstr编码则通过调用一次内存分配函数来分配一块连续的空间,空间中依此包含redisObject和sdshdr两个结构

2、 list
在3.2版本之前,list对象的编码可以是ziplist和linkedlist

  • ziplist编码的列表对象使用压缩列表作为底层实现,每个压缩列表节点保存了一个列表元素
  • linkedlist编码的列表对象使用双端链表作为底层实现,每个双端链表节点都保存了一个字符串对象,而每个字符串对象都保存了一个列表元素
  • 当list对象可以同时满足以下两个条件时,list对象使用ziplist编码,不满足时使用linkedlist编码
    1、list对象保存的所有字符串元素的长度都小于64字节
    2、list对象保存的元素数量小于512个

从3.2版本开始,list对象的编码升级为quicklist

  • quicklist编码的列表采用快速列表作为底层实现。quicklist是 链表 和 压缩列表 的结合

3、hash
hash对象的编码可以是 ziplist 或者 hashtable

  • ziplist编码的hash对象使用ziplist作为底层实现
  • hashtable编码的hash对象使用字典作为底层实现
  • 当hash对象可以同时满足以下两个条件时,hash对象使用ziplist编码,否则使用hashtable编码
    1、hash对象保存的所有键值对的key和value的字符串长度都小于64字节
    2、hash对象保存的键值对数量小于512个

4、set

set对象的编码可以是intset或者hashtable

  • intset编码的集合对象使用整数集合作为底层实现,集合对象包含的所有元素都被保存在整数集合里面
  • hashtable编码的集合对象使用字典作为底层实现,字典的每个键都是一个字符串对象,每个字符串对象包含了一个集合元素,而字典的值则全部被设置为NULL
  • 当set对象可以同时满足以下两个条件时,对象使用intset编码,否则使用hashtable编码
    1、set对象保存的所有元素都是整数值
    2、set对象保存的元素数量不超过512个

5、zset

zset的编码可以是ziplist或者skiplist

  • ziplist编码的zset对象使用ziplist作为底层实现,每个集合元素使用两个紧挨在一起的ziplist节点来保存,第一个节点保存元素的成员,而第二个元素则保存元素的分值;
    ziplist内的集合元素按分值从小到大进行排序,分值较小的元素被放置在靠近表头的位置,而分值较大的元素则被放置在靠近表尾的位置
  • skiplist编码的zset对象使用zset结构作为底层实现,一个zset结构同时包含一个字典和一个跳跃表
  • 当zset对象可以同时满足以下两个条件时,对象使用ziplist编码,否则使用skiplist编码
    1、有序集合保存的元素数量小于128个
    2、有序集合保存的所有元素成员的长度都小于64字节

11、Redis的一致性Hash算法

一致性Hash算法主要应用于Redis分布式缓存的存储中

使用一致性Hash算法可以有效地解决在分布式存储结构下 动态增加 和 删除 节点后尽量有多的请求命中原来的服务器节点

  • 总结:一致性hash主要用于分布式系统中,用于解决数据选择节点存储、选择节点访问、增删节点后数据的迁移和重分布问题;一致性Hash算法针对的是几个独立的Redis节点

Redis集群并没有使用一致性hash,而是使用了hash槽来解决数据分配的问题

  • 场景描述:假设有3个redis节点用于存储数据,这3台Redis节点的编号是0,1,2。如果希望将数据均匀的存储在这3个节点上,可以采用对数据的key进行Hash计算,将Hash计算后的结果对redis节点的数量进行取模运行,通过取模后的结果,决定将数据存在哪一个Redis节点上;index = hash(key)% N
  • 当下次访问这个数据时,只需要再次对数据的key进行Hash计算,即可得出数据在哪个redis上,然后从对应的redis节点上获取数据
  • 存在的问题:当增加redis节点或者当部分redis节点挂掉后,再进行hash计算然后对节点的数量取模时,就无法找到对应的数据,造成大量的缓存同时失效

一致性Hash算法

一致性Hash算法是对 232 取模;Hash(服务器的IP地址) % 232 (余数:0~232-1)

将232个数想象成由(0~232-1)共232个点组成的圆环,把这个圆环称为Hash环;将每台服务器的ip地址先进行hash计算,然后再对232取模 。通过上述方法,可将各服务器映射到hash环上;
当存储数据时,对数据的key进行hash计算,然后再对232取模,将数据映射到hash环上;然后在Hash环上,从数据的位置开始,沿顺时针方向遇到的第一个服务器,就将数据存储在这里

假如有4个数据,如图4,数据1和2将会被存储在服务器A,数据3将会被存储在服务器B,数据4将会被存储在服务器C

httclient java 多路复用 java多路复用原理_缓存_11

当服务器B挂掉后,按照执行hash算法的规则,数据3将会被存储在服务器C中,而其它的数据却没有改变,这就是一致性hash算法的优点

httclient java 多路复用 java多路复用原理_缓存_12

如果使用之前的hash算法对服务器的数量取模,那么当服务器的数量发生改变时,就会找不到数据,所有的缓存将会同时失效;而使用一致性Hash算法,服务器数量如果发生改变,并不是所有缓存都会失效,而是只有部分缓存会失效

Hash环的偏斜问题

  • 假设在理想的情况下,3台服务器将会均匀的映射在Hash环上,但是现实可能是3台服务器在Hash环上的映射的位置挨的比较近;那么,将会有很多数据被存储在某一台服务器上。这样,其它服务器并没有得到平均的充分的利用;如果此时,那个节点出现故障,造成的损失也是最大的

解决方法:使用虚拟节点

  • 虚拟节点是实际节点在Hash环上的一个复制品,一个实例节点可以对应多个虚拟节点;引入虚拟节点后,节点在Hash环上的分布就变得均衡了;虚拟节点越多,Hash环上的节点就越多,数据被均匀分布的概率就越大