Redis 特点如下:
- 数据类型丰富
- 支持数据磁盘持久化存储
- 支持主从
- 支持分片
Redis 的效率很高,官方给出的数据是 100000+QPS,这是因为:
- Redis 完全基于内存,绝大部分请求是纯粹的内存操作,执行效率高。
- Redis 使用单进程单线程模型的(K,V)数据库,将数据存储在内存中,存取均不会受到硬盘 IO 的限制,因此其执行速度极快。
- 数据结构简单,对数据操作也简单,Redis 不使用表,不会强制用户对各个关系进行关联,不会有复杂的关系限制,其存储结构就是键值对,类似于 HashMap,HashMap 最大的优点就是存取的时间复杂度为 O(1)。
- Redis 使用多路 I/O 复用模型,为非阻塞 IO。
epoll/kqueue/evport/select。
选用策略:
- 因地制宜,优先选择时间复杂度为 O(1) 的 I/O 多路复用函数作为底层实现。
- 由于 Select 要遍历每一个 IO,所以其时间复杂度为 O(n),通常被作为保底方案。
- 基于 React 设计模式监听 I/O 事件。
String
最基本的数据类型,其值最大可存储 512M,二进制安全(Redis 的 String 可以包含任何二进制数据,包含 jpg 对象等)。
文章图片
注:如果重复写入 key 相同的键值对,后写入的会将之前写入的覆盖。
Hash
String 元素组成的字典,适用于存储对象。
文章图片
List
列表,按照 String 元素插入顺序排序。其顺序为后进先出。由于其具有栈的特性,所以可以实现如“最新消息排行榜”这类的功能。
文章图片
Set
String 元素组成的无序集合,通过哈希表实现(增删改查时间复杂度为 O(1)),不允许重复。
文章图片
另外,当我们使用 Smembers 遍历 Set 中的元素时,其顺序也是不确定的,是通过 Hash 运算过后的结果。
Redis 还对集合提供了求交集、并集、差集等操作,可以实现如同共同关注,共同好友等功能。
Sorted Set
通过分数来为集合中的成员进行从小到大的排序。
文章图片
更高级的 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 以 0 作为游标,开始一次新的迭代,直到命令返回游标 0 完成一次遍历。
此命令并不保证每次执行都返回某个给定数量的元素,甚至会返回 0 个元素,但只要游标不是 0,程序都不会认为 SCAN 命令结束,但是返回的元素数量大概率符合 Count 参数。另外,SCAN 支持模糊查询。
例:
SCAN 0 MATCH test* COUNT 10 //每次返回10条以test为前缀的key
如何通过 Redis 实现分布式锁
分布式锁
分布式锁是控制分布式系统之间共同访问共享资源的一种锁的实现。如果一个系统,或者不同系统的不同主机之间共享某个资源时,往往需要互斥,来排除干扰,满足数据一致性。
分布式锁需要解决的问题如下:
- 互斥性:任意时刻只有一个客户端获取到锁,不能有两个客户端同时获取到锁。
- 安全性:锁只能被持有该锁的客户端删除,不能由其他客户端删除。
- 死锁:获取锁的客户端因为某些原因而宕机继而无法释放锁,其他客户端再也无法获取锁而导致死锁,此时需要有特殊机制来避免死锁。
- 容错:当各个节点,如某个 Redis 节点宕机的时候,客户端仍然能够获取锁或释放锁。
使用 SETNX 实现,SETNX key value:如果 Key 不存在,则创建并赋值。
该命令时间复杂度为 O(1),如果设置成功,则返回 1,否则返回 0。
文章图片
由于 SETNX 指令操作简单,且是原子性的,所以初期的时候经常被人们作为分布式锁,我们在应用的时候,可以在某个共享资源区之前先使用 SETNX 指令,查看是否设置成功。
如果设置成功则说明前方没有客户端正在访问该资源,如果设置失败则说明有客户端正在访问该资源,那么当前客户端就需要等待。
但是如果真的这么做,就会存在一个问题,因为 SETNX 是长久存在的,所以假设一个客户端正在访问资源,并且上锁,那么当这个客户端结束访问时,该锁依旧存在,后来者也无法成功获取锁,这个该如何解决呢?
由于 SETNX 并不支持传入 EXPIRE 参数,所以我们可以直接使用 EXPIRE 指令来对特定的 Key 来设置过期时间。
用法:
EXPIRE key seconds
文章图片
程序:
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 操作成功完成时才会返回 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 消费消息。
文章图片
此时我们可以看到,该队列是使用 Rpush 生产队列,使用 LPOP 消费队列。
在这个生产者-消费者队列里,当 LPOP 没有消息时,证明该队列中没有元素,并且生产者还没有来得及生产新的数据。
缺点:LPOP 不会等待队列中有值之后再消费,而是直接进行消费。
弥补:可以通过在应用层引入 Sleep 机制去调用 LPOP 重试。
②使用 BLPOP key [key…] timeout
BLPOP key [key …] timeout:阻塞直到队列有消息或者超时。
文章图片
文章图片
文章图片
缺点:按照此种方法,我们生产后的数据只能提供给各个单一消费者消费。能否实现生产一次就能让多个消费者消费呢?
③Pub/Sub:主题订阅者模式
发送者(Pub)发送消息,订阅者(Sub)接收消息。订阅者可以订阅任意数量的频道。
文章图片
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 持久化。
文章图片
启动:
- 检查是否存在子进程正在执行 AOF 或者 RDB 的持久化任务。如果有则返回 false。
- 调用 Redis 源码中的 rdbSaveBackground 方法,方法中执行 fork() 产生子进程执行 RDB 操作。
- 关于 fork() 中的 Copy-On-Write。
他们会共同获取相同的指针指向相同的资源,直到某个调用者试图修改资源的内容时,系统才会真正复制一份专用副本给调用者,而其他调用者所见到的最初的资源仍然保持不变。
④RDB 持久化方式的缺点
RDB 持久化方式的缺点如下:
- 内存数据全量同步,数据量大的状况下,会由于 I/O 而严重影响性能。
- 可能会因为 Redis 宕机而丢失从当前至最近一次快照期间的数据。
AOF 持久化是通过保存 Redis 的写状态来记录数据库的。
相对 RDB 来说,RDB 持久化是通过备份数据库的状态来记录数据库,而 AOF 持久化是备份数据库接收到的指令:
- AOF 记录除了查询以外的所有变更数据库状态的指令。
- 以增量的形式追加保存到 AOF 文件中。
《一线大厂Java面试题解析+后端开发学习笔记+最新架构讲解视频+实战项目源码讲义》存写状态**
【docs.qq.com/doc/DSmxTbFJ1cmN1R2dB】 完整内容开源分享
AOF 持久化是通过保存 Redis 的写状态来记录数据库的。
相对 RDB 来说,RDB 持久化是通过备份数据库的状态来记录数据库,而 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的所有知识点】重写过程如下:
推荐阅读
- java|Java小白从入门到精通,Java零基础入门看这一篇就够了
- 面试|前端面试宝典
- 深入理解Spring生态|Spring第三讲(SpringMVC 从入门到精通)
- 使用Salesforce AppExchange在企业中快速获胜
- Magento简介(在顶级电子商务生态系统中导航)
- 吸引,管理和留住软件开发人员的提示
- Trello vs. Jira(从开发人员的角度进行比较)
- 缩小差距(DevOps通信的重要性)
- Tech Talk · 云技术有话聊 | 基于低代码的数据开发如何提升效率()