程序员|Redis从入门到精通,至少要看看这篇,几乎囊括了Java的所有知识点

Redis 特点如下:

  • 数据类型丰富
  • 支持数据磁盘持久化存储
  • 支持主从
  • 支持分片
为什么 Redis 能这么快
Redis 的效率很高,官方给出的数据是 100000+QPS,这是因为:
  • Redis 完全基于内存,绝大部分请求是纯粹的内存操作,执行效率高。
  • Redis 使用单进程单线程模型的(K,V)数据库,将数据存储在内存中,存取均不会受到硬盘 IO 的限制,因此其执行速度极快。
另外单线程也能处理高并发请求,还可以避免频繁上下文切换和锁的竞争,如果想要多核运行也可以启动多个实例。
  • 数据结构简单,对数据操作也简单,Redis 不使用表,不会强制用户对各个关系进行关联,不会有复杂的关系限制,其存储结构就是键值对,类似于 HashMap,HashMap 最大的优点就是存取的时间复杂度为 O(1)。
  • Redis 使用多路 I/O 复用模型,为非阻塞 IO。
注:Redis 采用的 I/O 多路复用函数:
epoll/kqueue/evport/select。
选用策略:
  • 因地制宜,优先选择时间复杂度为 O(1) 的 I/O 多路复用函数作为底层实现。
  • 由于 Select 要遍历每一个 IO,所以其时间复杂度为 O(n),通常被作为保底方案。
  • 基于 React 设计模式监听 I/O 事件。
Redis 的数据类型
String
最基本的数据类型,其值最大可存储 512M,二进制安全(Redis 的 String 可以包含任何二进制数据,包含 jpg 对象等)。
程序员|Redis从入门到精通,至少要看看这篇,几乎囊括了Java的所有知识点
文章图片

注:如果重复写入 key 相同的键值对,后写入的会将之前写入的覆盖。
Hash
String 元素组成的字典,适用于存储对象。
程序员|Redis从入门到精通,至少要看看这篇,几乎囊括了Java的所有知识点
文章图片

List
列表,按照 String 元素插入顺序排序。其顺序为后进先出。由于其具有栈的特性,所以可以实现如“最新消息排行榜”这类的功能。
程序员|Redis从入门到精通,至少要看看这篇,几乎囊括了Java的所有知识点
文章图片

Set
String 元素组成的无序集合,通过哈希表实现(增删改查时间复杂度为 O(1)),不允许重复。
程序员|Redis从入门到精通,至少要看看这篇,几乎囊括了Java的所有知识点
文章图片

另外,当我们使用 Smembers 遍历 Set 中的元素时,其顺序也是不确定的,是通过 Hash 运算过后的结果。
Redis 还对集合提供了求交集、并集、差集等操作,可以实现如同共同关注,共同好友等功能。
Sorted Set
通过分数来为集合中的成员进行从小到大的排序。
程序员|Redis从入门到精通,至少要看看这篇,几乎囊括了Java的所有知识点
文章图片

更高级的 Redis 类型
用于计数的 HyperLogLog、用于支持存储地理位置信息的 Geo。
从海量 Key 里查询出某一个固定前缀的 Key
假设 Redis 中有十亿条 Key,如何从这么多 Key 中找到固定前缀的 Key?
方法 1:使用 Keys [pattern]:查找所有符合给定模式 Pattern 的 Key
使用 Keys [pattern] 指令可以找到所有符合 Pattern 条件的 Key,但是 Keys 会一次性返回所有符合条件的 Key,所以会造成 Redis 的卡顿。
假设 Redis 此时正在生产环境下,使用该命令就会造成隐患,另外如果一次性返回所有 Key,对内存的消耗在某些条件下也是巨大的。
例:
keys test* //返回所有以test为前缀的key
方法 2:使用 SCAN cursor [MATCH pattern] [COUNT count]
注:
  • cursor:游标
  • MATCH pattern:查询 Key 的条件
  • Count:返回的条数
SCAN 是一个基于游标的迭代器,需要基于上一次的游标延续之前的迭代过程。
SCAN 以 0 作为游标,开始一次新的迭代,直到命令返回游标 0 完成一次遍历。
此命令并不保证每次执行都返回某个给定数量的元素,甚至会返回 0 个元素,但只要游标不是 0,程序都不会认为 SCAN 命令结束,但是返回的元素数量大概率符合 Count 参数。另外,SCAN 支持模糊查询。
例:
SCAN 0 MATCH test* COUNT 10 //每次返回10条以test为前缀的key
如何通过 Redis 实现分布式锁
分布式锁
分布式锁是控制分布式系统之间共同访问共享资源的一种锁的实现。如果一个系统,或者不同系统的不同主机之间共享某个资源时,往往需要互斥,来排除干扰,满足数据一致性。
分布式锁需要解决的问题如下:
  • 互斥性:任意时刻只有一个客户端获取到锁,不能有两个客户端同时获取到锁。
  • 安全性:锁只能被持有该锁的客户端删除,不能由其他客户端删除。
  • 死锁:获取锁的客户端因为某些原因而宕机继而无法释放锁,其他客户端再也无法获取锁而导致死锁,此时需要有特殊机制来避免死锁。
  • 容错:当各个节点,如某个 Redis 节点宕机的时候,客户端仍然能够获取锁或释放锁。
如何使用 Redis 实现分布式锁
使用 SETNX 实现,SETNX key value:如果 Key 不存在,则创建并赋值。
该命令时间复杂度为 O(1),如果设置成功,则返回 1,否则返回 0。
程序员|Redis从入门到精通,至少要看看这篇,几乎囊括了Java的所有知识点
文章图片

由于 SETNX 指令操作简单,且是原子性的,所以初期的时候经常被人们作为分布式锁,我们在应用的时候,可以在某个共享资源区之前先使用 SETNX 指令,查看是否设置成功。
如果设置成功则说明前方没有客户端正在访问该资源,如果设置失败则说明有客户端正在访问该资源,那么当前客户端就需要等待。
但是如果真的这么做,就会存在一个问题,因为 SETNX 是长久存在的,所以假设一个客户端正在访问资源,并且上锁,那么当这个客户端结束访问时,该锁依旧存在,后来者也无法成功获取锁,这个该如何解决呢?
由于 SETNX 并不支持传入 EXPIRE 参数,所以我们可以直接使用 EXPIRE 指令来对特定的 Key 来设置过期时间。
用法:
EXPIRE key seconds
程序员|Redis从入门到精通,至少要看看这篇,几乎囊括了Java的所有知识点
文章图片

程序:
RedisService redisService = SpringUtils.getBean(RedisService.class);
long status = redisService.setnx(key,“1”);
if(status == 1){
redisService.expire(key,expire);
doOcuppiedWork();
}
这段程序存在的问题:假设程序运行到第二行出现异常,那么程序来不及设置过期时间就结束了,则 Key 会一直存在,等同于锁一直被持有无法释放。
出现此问题的根本原因为:原子性得不到满足。
解决:从 Redis 2.6.12 版本开始,我们就可以使用 Set 操作,将 SETNX 和 EXPIRE 融合在一起执行,具体做法如下:
  • EX second:设置键的过期时间为 Second 秒。
  • PX millisecond:设置键的过期时间为 MilliSecond 毫秒。
  • NX:只在键不存在时,才对键进行设置操作。
  • XX:只在键已经存在时,才对键进行设置操作。
SET KEY value [EX seconds] [PX milliseconds] [NX|XX]
注:SET 操作成功完成时才会返回 OK,否则返回 nil。
有了 SET 我们就可以在程序中使用类似下面的代码实现分布式锁了:
RedisService redisService = SpringUtils.getBean(RedisService.class);
String result = redisService.set(lockKey,requestId,SET_IF_NOT_EXIST,SET_WITH_EXPIRE_TIME,expireTime);
if(“OK.equals(result)”){
doOcuppiredWork();
}
如何实现异步队列
①使用 Redis 中的 List 作为队列
使用上文所说的 Redis 的数据结构中的 List 作为队列 Rpush 生产消息,LPOP 消费消息。
程序员|Redis从入门到精通,至少要看看这篇,几乎囊括了Java的所有知识点
文章图片

此时我们可以看到,该队列是使用 Rpush 生产队列,使用 LPOP 消费队列。
在这个生产者-消费者队列里,当 LPOP 没有消息时,证明该队列中没有元素,并且生产者还没有来得及生产新的数据。
缺点:LPOP 不会等待队列中有值之后再消费,而是直接进行消费。
弥补:可以通过在应用层引入 Sleep 机制去调用 LPOP 重试。
②使用 BLPOP key [key…] timeout
BLPOP key [key …] timeout:阻塞直到队列有消息或者超时。
程序员|Redis从入门到精通,至少要看看这篇,几乎囊括了Java的所有知识点
文章图片

程序员|Redis从入门到精通,至少要看看这篇,几乎囊括了Java的所有知识点
文章图片

程序员|Redis从入门到精通,至少要看看这篇,几乎囊括了Java的所有知识点
文章图片

缺点:按照此种方法,我们生产后的数据只能提供给各个单一消费者消费。能否实现生产一次就能让多个消费者消费呢?
③Pub/Sub:主题订阅者模式
发送者(Pub)发送消息,订阅者(Sub)接收消息。订阅者可以订阅任意数量的频道。
程序员|Redis从入门到精通,至少要看看这篇,几乎囊括了Java的所有知识点
文章图片

Pub/Sub模式的缺点:消息的发布是无状态的,无法保证可达。对于发布者来说,消息是“即发即失”的。
此时如果某个消费者在生产者发布消息时下线,重新上线之后,是无法接收该消息的,要解决该问题需要使用专业的消息队列,如 Kafka…此处不再赘述。
Redis 持久化
什么是持久化
持久化,即将数据持久存储,而不因断电或其他各种复杂外部环境影响数据的完整性。
由于 Redis 将数据存储在内存而不是磁盘中,所以内存一旦断电,Redis 中存储的数据也随即消失,这往往是用户不期望的,所以 Redis 有持久化机制来保证数据的安全性。
Redis 如何做持久化
Redis 目前有两种持久化方式,即 RDB 和 AOF,RDB 是通过保存某个时间点的全量数据快照实现数据的持久化,当恢复数据时,直接通过 RDB 文件中的快照,将数据恢复。
RDB(快照)持久化
RDB持久化会在某个特定的间隔保存那个时间点的全量数据的快照。
RDB 配置文件,redis.conf:
save 900 1 #在900s内如果有1条数据被写入,则产生一次快照。 save 300 10 #在300s内如果有10条数据被写入,则产生一次快照 save 60 10000 #在60s内如果有10000条数据被写入,则产生一次快照
stop-writes-on-bgsave-error yes #stop-writes-on-bgsave-error : 如果为yes则表示,当备份进程出错的时候, 主进程就停止进行接受新的写入操作,这样是为了保护持久化的数据一致性的问题。
①RDB 的创建与载入
SAVE:阻塞 Redis 的服务器进程,直到 RDB 文件被创建完毕。SAVE 命令很少被使用,因为其会阻塞主线程来保证快照的写入,由于 Redis 是使用一个主线程来接收所有客户端请求,这样会阻塞所有客户端请求。
BGSAVE:该指令会 Fork 出一个子进程来创建 RDB 文件,不阻塞服务器进程,子进程接收请求并创建 RDB 快照,父进程继续接收客户端的请求。
子进程在完成文件的创建时会向父进程发送信号,父进程在接收客户端请求的过程中,在一定的时间间隔通过轮询来接收子进程的信号。
我们也可以通过使用 lastsave 指令来查看 BGSAVE 是否执行成功,lastsave 可以返回最后一次执行成功 BGSAVE 的时间。
②自动化触发 RDB 持久化的方式
自动化触发RDB持久化的方式如下:
  • 根据 redis.conf 配置里的 SAVE m n 定时触发(实际上使用的是 BGSAVE)。
  • 主从复制时,主节点自动触发。
  • 执行 Debug Reload。
  • 执行 Shutdown 且没有开启 AOF 持久化。
③BGSAVE 的原理
程序员|Redis从入门到精通,至少要看看这篇,几乎囊括了Java的所有知识点
文章图片

启动:
  • 检查是否存在子进程正在执行 AOF 或者 RDB 的持久化任务。如果有则返回 false。
  • 调用 Redis 源码中的 rdbSaveBackground 方法,方法中执行 fork() 产生子进程执行 RDB 操作。
  • 关于 fork() 中的 Copy-On-Write。
fork() 在 Linux 中创建子进程采用 Copy-On-Write(写时拷贝技术),即如果有多个调用者同时要求相同资源(如内存或磁盘上的数据存储)。
他们会共同获取相同的指针指向相同的资源,直到某个调用者试图修改资源的内容时,系统才会真正复制一份专用副本给调用者,而其他调用者所见到的最初的资源仍然保持不变。
④RDB 持久化方式的缺点
RDB 持久化方式的缺点如下:
  • 内存数据全量同步,数据量大的状况下,会由于 I/O 而严重影响性能。
  • 可能会因为 Redis 宕机而丢失从当前至最近一次快照期间的数据。
AOF 持久化:保存写状态
AOF 持久化是通过保存 Redis 的写状态来记录数据库的。
相对 RDB 来说,RDB 持久化是通过备份数据库的状态来记录数据库,而 AOF 持久化是备份数据库接收到的指令:
  • AOF 记录除了查询以外的所有变更数据库状态的指令。
  • 以增量的形式追加保存到 AOF 文件中。
**AOF 持久化:保
《一线大厂Java面试题解析+后端开发学习笔记+最新架构讲解视频+实战项目源码讲义》
【docs.qq.com/doc/DSmxTbFJ1cmN1R2dB】 完整内容开源分享
存写状态**
AOF 持久化是通过保存 Redis 的写状态来记录数据库的。
相对 RDB 来说,RDB 持久化是通过备份数据库的状态来记录数据库,而 AOF 持久化是备份数据库接收到的指令:
  • AOF 记录除了查询以外的所有变更数据库状态的指令。
  • 以增量的形式追加保存到 AOF 文件中。
开启 AOF 持久化
①打开 redis.conf 配置文件,将 appendonly 属性改为 yes。
②修改 appendfsync 属性,该属性可以接收三种参数,分别是 always,everysec,no。
always 表示总是即时将缓冲区内容写入 AOF 文件当中,everysec 表示每隔一秒将缓冲区内容写入 AOF 文件,no 表示将写入文件操作交由操作系统决定。
一般来说,操作系统考虑效率问题,会等待缓冲区被填满再将缓冲区数据写入 AOF 文件中。
appendonly yes
#appendsync always
appendfsync everysec
appendfsync no 日志重写解决 AOF 文件不断增大
随着写操作的不断增加,AOF 文件会越来越大。假设递增一个计数器 100 次,如果使用 RDB 持久化方式,我们只要保存最终结果 100 即可。
而 AOF 持久化方式需要记录下这 100 次递增操作的指令,而事实上要恢复这条记录,只需要执行一条命令就行,所以那一百条命令实际可以精简为一条。
Redis 支持这样的功能,在不中断前台服务的情况下,可以重写 AOF 文件,同样使用到了 COW(写时拷贝)。
【程序员|Redis从入门到精通,至少要看看这篇,几乎囊括了Java的所有知识点】重写过程如下:

    推荐阅读