蹊源的Java笔记—Redis服务器

前言

前段时间我对​​Mysql​​​数据库的知识点进行了梳理,本篇博客我对​​Redis​​​服务器的相关的知识点进行整理,​​Redis​​​可以是我们在​​Web​​​应用中提升性能的利器,可以说​​Redis​​是一个中高级开发者必备的技能点。

Mysql的知识点可参考我的博客​​蹊源的Java笔记—Mysql数据库​​

消息队列可参考我的博客​​蹊源的Java笔记—Redis服务器​​

正文

Redis

Redis常见应用场景:

  • 实现缓存系统和内存数据库:会话缓存和全页缓存
  • 使用​​redis​​来搭建消息队列
  • 排行榜/计数器,​​redis​​在内存中对数字进行递增或者递减的操作实现比较好。
  • 发布/订阅,通过​​redis​​来实现朋友圈功能

Redis的优势:

  • 绝大数的请求操作都是纯粹的内存操作。
  • 采用了单线模式,避免了不必要的上下文切换和竞争条件,这里的单线程指的是网络请求模块只使用了一个线程(所以不必考虑并发安全性),即一个线程处理所有网络请求,其他模块仍使用了多个线程
  • 采用了非阻塞I/O多路复用机制,​​Redis​​管道技术使得请求不会发生阻塞。
  • 采用了动态字符串(​​SDS​​),对于字符串会预留一定的空间,避免了字符串在做拼接和截取引起内存重新分配导致性能的损耗。

缓存失效策略

三种主要算法:

  • FIFO:​​First In First Out​​,先进先出。判断被存储的时间,离目前最远的数据优先被淘汰。
  • LRU:​​Least Recently Used​​,最近最少使用。判断最近被使用的时间,目前最远的数据优先被淘汰。(时间远近)
  • LFU:​​Least Frequently Used​​,最不经常使用。在一段时间内,数据被使用次数最少的,优先被淘汰。(次数多少)

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

  • volatile-lru:从已设置过期时间的数据集中挑选最近最少使用的数据淘汰
  • volatile-ttl:从已设置过期时间的数据集中挑选将要过期的数据淘汰
  • volatile-random:从已设置过期时间的数据集中任意选择数据淘汰
  • allkeys-lru:从数据集中挑选最近最少使用的数据淘汰
  • allkeys-random:从数据集中任意选择数据淘汰
  • no-enviction(驱逐):禁止驱逐数据 默认

三种过期策略:

  • 定时删除:在设置​​key​​​的过期时间的同时,为该​​key​​​创建一个定时器,让定时器在​​key​​​的过期时间来临时,对​​key​​进行删除
  • 惰性删除: ​​key​​​过期的时候不删除,每次从数据库获取​​key​​​的时候去检查是否过期,若过期,则删除,返回​​null​​。
  • 定期删除: 每隔一段时间执行一次删除过期​​key​​操作

​redis​​​一般使用定时删除、定期删除, ​​mercached​​只使用惰性删除。

Redis持久化方式

方式一:快照
​​​RDB​​​(默认) 持久化可以在指定的时间间隔内生成数据集的时间点快照。
以下设置会让 ​​​Redis​​​ 在满足“ 60 秒内有至少有 1000 个键被改动”这一条件时, 自动保存一次数据集。
​​​RDB​​的优劣势:

  • 弊端:在保存时间的间断中如果不满足保存条件,突然发生断电或者系统崩溃会导致数据丢失。
  • 优势:数据恢复十分快。

方式二:同步到数据文件
​​​AOF​​​ 持久化录服务器执行的所有写操作命令,并在服务器启动时,通过重新执行这些命令来还原数据集。(经所有的指令存储到文本文件中)
​​​AOF​​的优劣势:

  • 弊端:每一条指令都记录到文本文件中会极大地拖垮​​redis​​的性能,数据恢复速度慢
  • 优势:执行的周期比​​rdp​​短,能防止间隔异常导致数据丢失

方式三:使用虚拟内存的方式

宕机如何处理

  • 创建一个定期任务, 每小时将一个 ​​RDB​​​ 文件备份到一个文件夹, 并且每天将一个 ​​RDB​​ 文件备份到另一个文件夹。
  • 确保快照的备份都带有相应的日期和时间信息, 每次执行定期任务脚本时, 使用 ​​find​​ 命令来删除过期的快照: 比如说, 你可以保留最近 48 小时内的每小时快照, 还可以保留最近一两个月的每日快照。
  • 至少每天一次, 将 ​​RDB​​​ 备份到你的数据中心之外, 或者至少是备份到你运行 ​​Redis​​ 服务器的物理机器之外。

Redis 事务

Redis事务是一组命令的集合。
​​​Redis​​ 事务可以一次执行多个命令, 并且带有以下的保证:

  • 批量操作在发送 ​​EXEC​​ 命令前被放入队列缓存。
  • 收到 ​​EXEC​​ 命令后进入事务执行,事务中任意命令执行失败,其余的命令依然被执行(不具有原子性)。
  • 在事务执行过程,其他客户端提交的命令请求不会插入到事务执行命令序列中。

一个事务从开始到执行会经历以下三个阶段:

  1. 开始事务:事务开始的时候先向​​Redis​​​服务器发送 ​​MULTI​​ 命令
  2. 命令入队:然后依次发送需要在本次事务中处理的命令
  3. 执行事务:最后再发送 ​​EXEC​​ 命令表示事务命令结束

命令并不会立刻马上执行,只有在执行​​EXEC​​命令才会逐一执行命令。

事务相关的命令:

  • MULTI:开启事务
  • EXEC:提交事务
  • DISCARD:放弃事务
  • WATCH:监控
  • QUEUED:将命令加入执行的队列

Redis事务与Mysql事务的区别

蹊源的Java笔记—Redis服务器_大厂面试

Redis分布式锁
Redis分布式锁的三种行为:

  • 加锁:使用​​setnx​​来抢夺锁,将锁的标识符设置为1,表示锁已被占用。
  • 解锁:使用​​setnx​​来释放锁,将锁的标识符设置为0,表示锁已被释放。
  • 锁过期:用 ​​expire​​​ 给锁加一个过期时间防止锁忘记了释放,​​expire​​时间过期将返回0。

​Setnx​​​和​​expire​​​都是原子操作,实际应用中使用​​lua​​脚本来确保操作的原子性。

Redis 发布订阅

​Redis​​​ 发布订阅(​​pub​​​/​​sub​​)是一种消息通信模式:

  • 发送者(​​pub​​)发送消息
  • 订阅者(​​sub​​)接收消息

​Redis​​​ 客户端可以订阅任意数量的频道。
下图展示了频道 ​​​channel1​​​ , 以及订阅这个频道的三个客户端 —— ​​client2​​​ 、 ​​client5​​​ 和 ​​client1​​ 之间的关系:

蹊源的Java笔记—Redis服务器_缓存_02

当有新消息通过 ​​PUBLISH​​​ 命令发送给频道 ​​channel1​​ 时, 这个消息就会被发送给订阅它的三个客户端:

蹊源的Java笔记—Redis服务器_蹊源_03

Redis数据库支持的数据类型

  • String:字符串、整数或者浮点数
  • Hash:包含键值对的无序散列表
  • List:链表,每个节点都是一个字符串
  • Set:无序收集器
  • Zset:有序集合

String
​​​Redis​​​的​​String​​​采用的是动态​​String​​:

  • 不会出现字符串变更造成的内存溢出问题
  • 获取字符串长度时间复杂度为1
  • 空间预分配, 惰性空间释放​​free​​字段,会默认留够一定的空间防止多次重分配内存

应用场景:

  • ​String​​ 缓存结构体用户信息,计数

Hash
数组+链表的基础上,进行了一些​​​rehash​​优化;

  1. ​Reids​​​的​​Hash​​采用链地址法来处理冲突,然后它没有使用红黑树优化。
  2. 哈希表节点采用单链表结构。
  3. ​rehash​​​优化 (采用分而治之的思想,将庞大的迁移工作量划分到每一次​​CURD​​中,避免了服务繁忙)

​rehash​​​指的是当​​hash​​​表中的负载因子达到负载极限的时候,​​hash​​表会自动成倍的增加容量(桶的数量),并将原有的对象重新的分配并加入新的桶内。

应用场景:

  • 保存结构体信息可部分获取不用序列化所有字段

List

应用场景:

  • 比如​​twitter​​​的关注列表,粉丝列表等都可以用​​Redis​​​的​​list​​结构来实现
  • ​List​​的实现为一个双向链表,即可以支持反向查找和遍历

Set
内部实现是一个​​​value​​​为​​null​​​的​​HashMap​​​,实际就是通过计算​​hash​​​的方式来快速排重的,这也是​​set​​能提供判断一个成员是否在集合内的原因。

应用场景:

  • 去重的场景,交集(​​sinter​​​)、并集(​​sunion​​​)、差集(​​sdiff​​)
  • 实现如共同关注、共同喜好、二度好友等功能

Zset
内部使用​​​HashMap​​​和跳跃表(​​SkipList​​)来保证数据的存储和有序:

  • ​HashMap​​​里放的是成员到​​score​​​的映射,​​score​​是排序的依据。
  • 跳跃表里存放的是所有的成员,它的每个节点中维持多个指向其他节点的指针,从而达到快速访问节点的目的

应用场景:

  • 实现延时队列

跳跃表

  • 跳跃表是一种基于有序链表的扩展,简称跳表。
  • 跳跃表示通过在链表中建索引(可以是多级索引)的方式,牺牲存储空间换取性能。
  • 索引是占内存的。原始链表中存储的有可能是很大的对象,而索引结点只需要存储关键值值和几个指针,并不需要存储对象,因此当节点本身比较大或者元素数量比较多的时候,其优势必然会被放大,而缺点则可以忽略。

Redis集群化

主从备份

在​​Redis​​​中,用户可以通过执行​​SLAVEOF​​​命令或者设置​​slaveof​​​选项,让一个服务器去复制(​​replicate​​​)另一个服务器,我们称呼被复制的服务器为主服务器(​​master​​​),而对主服务器进行复制的服务器则被称为从服务器(​​slave​​)。

蹊源的Java笔记—Redis服务器_redis集群_04

格式:从服务器 ​​saveof​​ 主服务器

Redis同步机制
​​​Redis​​ 可以使用主从同步,从从同步:

  • RDB镜像同步:主节点做一次 ​​bgsave​​​,并同时将后续修改操作记录到内存 ​​buffer​​​,待完成后将 ​​RDB​​​ 文件全量同步到复制节点,复制节点接受完成后将 ​​RDB​​ 镜像加载到内存。
  • AOP文件同步:再通知主节点将期间修改的操作记录同步到复制节点进行重放就完成了同步过程。

bgsave
​​​bgsave​​​ 命令用于在后台异步保存当前数据库的数据到磁盘。
​​​save​​​ 和 ​​bgsave​​​ 两个命令都会调用 ​​rdbSave​​ 函数,但它们调用的方式各有不同:

  • ​save​​​直接调用 ​​rdbSave​​​ ,阻塞 ​​Redis​​ 主进程,直到保存完成为止。在主进程阻塞期间,服务器不能处理客户端的任何请求。
  • ​bgsave​​​则 ​​fork​​​ 出一个子进程,子进程负责调用 ​​rdbSave​​​ ,并在保存完成之后向主进程发送信号,通知保存已完成。 ​​Redis​​​服务器在​​bgsave​​执行期间仍然可以继续处理客户端的请求。

Redis集群化的底层原理

  • 分片:自动将数据进行分片,每个​​master​​上放一部分数据。
  • 一致性hash算法:提供16384槽点,借助一致性​​hash​​算法来决定数据分片放在那个槽点中。

Redis集群化的三个阶段:

  • 主从复制: 实现了读写分离。
  • 哨兵模式:主从可以自动切换,系统更加健壮,可用性更高。
  • Redis-cluster:实现​​redis​​的分布式存储,实现数据的去中心化。

Redis集群化的方案:

  • 官方​​Redis-cluster​​方案
  • twemproxy代理方案,​​twemproxy​​​是一个单点,很容易对其造成很大的压力,所以通常会结合​​keepalived​​​来实​​twemproy​​的高可用
  • ​codis​​ 基于客户端来进行分片

Redis集群的特性

  • 高可用:在​​master​​​宕机时会自动将​​slave​​​提升为​​master​​,继续提供服务。
  • 扩展性:当单个​​redis​​​内存不足时,使用​​cluster​​进行分片存储。

Redis集群并不能保证数据的强一致性

  • 在特定条件下, ​​Redis​​ 集群可能会丢失已经被执行过的写命令。
  • ​Redis​​采用的是异步复制:主节点处理完写命令后立即返回客户度,并不等待从节点复制完成。

Redis Cluster
Redis Cluster是​​Redis​​官方多机部署方案,在官方推荐中使用6实例,其中3个为主节点,3个为从节点。

在​​Redis cluster​​框架中:

  • ​Redis cluster​​​的节点会通过​​meet​​操作来实现共享信息,每个节点都知道是哪个节点负责哪个范围内的数据槽。
  • 默认的情况在​​Redis cluster​​​中​​redis-master​​​用于接收读写,而​​redis-slave​​​则用于备份,当有请求是在向​​slave​​​发起时,会直接重定向到对应​​key​​​所在的​​master​​来处理。
  • 如果存在​​redis-cluster​​​对数据的实时性要求不高时,可以通过​​readonly​​​,将​​slave​​​设置成可读的,然后通过​​slave​​​直接获取相关的​​key​​,达到读写分离。

缓存和数据库一致性问题

CAP原理
​​​CAP​​原理指的是,一个提供数据服务的存储系统无法同时满足:

  • C数据一致性:所有应用程序都能访问到相同的数据。
  • A数据可用性:任何时候,任何应用程序都可以读写访问。
  • P分区耐受性:系统可以跨网络分区线性伸缩。(通俗来说就是数据的规模可扩展)

在大型网站中通常都是牺牲C,选择AP。为了可能减小数据不一致带来的影响,都会采取各种手段保证数据最终一致。

  • 数据强一致:各个副本的数据在物理存储中总是一致的。
  • 数据用户一致:数据在物理存储的各个副本可能是不一致的,但是通过纠错和校验机制,会确定一个一致的且正确的数据返回给用户。
  • 数据最终一致:物理存储的数据可能不一致,终端用户访问也可能不一致,但是一段时间内数据会达成一致。

​Redis​​由于器异步复制的特性,所以它本身是数据最终一致性的。

解决缓存一致性的解决方案:

  • 延时双删策略
  • 通过消息队列来更新缓存
  • 通过​​binlog​​​来同步​​mysql​​​数据库到​​redis​​中

延时双删策略
一个写操作会进行以下流程:

  1. 先淘汰缓存
  2. 再写数据库
  3. 休眠1秒,再次淘汰缓存

接着我们要明确为什么要采用先淘汰缓存,再写数据库的策略。

先写数据库再更新缓存的弊端:
1.线程安全方向(为什么要先操作缓存,再操作数据库)
同时有请求A和请求B进行更新操作,那么会出现:

  • 线程A更新了数据库;
  • 线程B更新了数据库;
  • 线程B更新了缓存;
  • 线程A更新了缓存;(A出现网络波动)

这就出现请求A更新缓存应该比请求B更新缓存早才对,但是因为网络等原因,B却比A更早更新了缓存。这就导致了脏数据。并且这种情况只能等缓存失效,才能够得到解决,这样的话很大程度会对业务产生比较大的影响。

2.业务方向(为什么选择淘汰缓存,而不是更新缓存)

  • 缓存的意义就是为了提升读操作的性能,如果你写操作比较频繁,频繁更新缓存且没有读操作,会造成性能浪费,所以应该由读操作来触发生成缓存,故而在写操作的时候应采用淘汰缓存的策略。
  • 有的时候我们在存入缓存可能也会做一些其他转化操作,但是如果又立马被修改,也会造成性能的浪费。

采用先淘汰缓存,再写数据库事实上不是完美的方案,但是是相对而言最合理的方法,它有下面的特殊情况:

  1. 写请求A进行写操作,删除缓存;
  2. 读请求B查询发现缓存不存在;
  3. 读请求B去数据库查询得到旧值;
  4. 读请求B将旧值写入缓存;
  5. 写请求A将新值写入数据库;(这里不采取行动,会造成数据库与缓存数据不一致)

上述情况就会导致不一致的情形出现。

延时双删策略是为了解决采用先淘汰缓存,再写数据库可能造成数据不一致的问题,这个时候写请求A应采用休眠1秒,再次淘汰缓存的策略:

  • 采用上述的做法,第一次写操作,会出现将近1秒(小于 1秒-读请求操作时间)的数据不一致的问题,1秒后再次执行缓存淘汰,下次读操作后就会保证数据库与缓存数据的一致性了。
  • 这里提到的1秒,是用来确保读请求结束(一般是几百ms),写请求可以删除读请求造成的缓存脏数据。

另外还存在一种极端情况是:如果第二次淘汰缓存失败,会导致数据和缓存一直不一致的问题,所以:

  • 缓存要设置失效时间
  • 设置重试机制或者采用消息队列的方式保证缓存被淘汰。

通过消息队列来更新缓存
采用消息队列中间件的方式能够保证数据库数据与缓存数据的最终一致性。

  • 实现了异步更新缓存,降低了系统的耦合性
  • 但是破坏了数据变化的时序性
  • 成本相对比较高

通过binlog来同步Mysql数据库到Redis中
​​​Mysql​​​数据库任何时间对数据库的修改都会记录在​​binlog​​​中;当数据发生增删改,创建数据库对象都会记录到​​binlog​​​中,数据库的复制也是基于​​binlog​​进行同步数据:

  • 在​​mysql​​压力不大情况下,延迟较低;
  • 和业务完全解耦;
  • 解决了时序性问题。
  • 成本相对而言比较大

蹊源的Java笔记—Redis服务器_蹊源_05

缓存机制

一个高性能网站一般都会用到缓存架构,缓存意味着高性能,通过空间换时间。

存储和缓存的区别

  • 存储要求数据可以进行持久化,数据不能轻易被丢失
  • 存储要保证数据结构的完整性,所以要求数据支持更多的数据类型

四层缓存

  • 基于浏览器等设备的客户端缓存
  • 基于​​CDN​​​加速的网络层缓存,通过​​CDN​​能够实现对页面的缓存
  • 基于​​Ngnix​​等负载均衡组件的路由层缓存
  • 基于​​Redis​​等的业务层缓存

业务层的缓存可以细分为三级缓存:

  • 一级缓存(会话级缓存):在维持一个会话时,查询获取的数据会存放在一级缓存中,下次使用从缓存中获取。
  • 二级缓存(应用级缓存):当会话关闭时,一级缓存的数据会保存在二级缓存中。
  • 三级缓存(数据库级缓存):可以实现跨​​jvm​​,通过远程调用的方式实现数据同步。

缓存中的常见问题
缓存雪崩问题
缓存雪崩,是指在某一个时间段,缓存集中过期失效。
解决方法:

  • 根据业务特点,对不同的“记录”设置不同的失效周期。
  • 在并发量不是很高的情况下,使用加锁排队的方式。
  • 给每一个缓存数据添加相应缓存标记,记录缓存是否失效,如果缓存标记失效,则更新数据缓存。

缓存预热问题
新启动的缓存系统如果没有任何数据,在重建缓存数据的过程中,系统的性能和数据库负载造成压力。
解决方法:

  • 缓存系统启动时就把热点数据加载好,如一些元数据—城市地名列表、类目信息等。

缓存穿透问题
缓存穿透,是指查询一个数据库一定不存在的数据。 假如有恶意攻击,就可以利用这个漏洞,对数据库造成压力,甚至压垮数据库。即便是采用​​​UUID​​​,也是很容易找到一个不存在的​​KEY​​​,进行攻击。
解决方法:

  1. 再​​web​​服务器启动时,提前将有可能被频繁并发访问的数据写入缓存。
  2. 如果从数据库查询的对象为空,也放入缓存,只是设定的缓存过期时间较短,比如设置为60秒(避免大量空值的​​key​​占用缓存的空间)。
  3. 规范​​key​​​的命名,使用布隆过滤器的方式对一些定义好的​​key​​规范进行检测,过滤一些恶意访问的请求。

缓存击穿问题
缓存击穿,是指一个​​​key​​​非常热点,在不停的扛着大并发,大并发集中对这一个点进行访问,当这个​​key​​​在失效的瞬间,持续的大并发就穿破缓存,直接请求数据库。
解决方法:

  • 对热点数据设置比较长的生命周期或者永不过期。

缓存的并发竞争问题
并发竞争问题指的是: 同时有多个子系统去​​​set​​​一个​​key​​,解决方案主要有两种:

  • 分布式锁:采用​​redis​​​的​​setnx​​命令
  • 利用消息队列: 通过消息中间件进行处理,把并行读写进行串行化

蹊源的Java笔记—Redis服务器_大厂面试_06