Redis由浅入深深入剖析(深入了解redis)
mhr18 2024-11-14 16:21 25 浏览 0 评论
作者:ObjectSpace
juejin.im/post/5d809a89e51d456206115ab3
前言
常用的SQL数据库的数据都是存在磁盘中的,虽然在数据库底层也做了对应的缓存来减少数据库的IO压力,但由于数据库的缓存一般是针对查询的内容,而且粒度也比较小,一般只有表中的数据没有发生变动的时候,数据库的缓存才会产生作用,但这并不能减少业务逻辑对数据库的增删改操作的IO压力,因此缓存技术应运而生,该技术实现了对热点数据的高速缓存,可以大大缓解后端数据库的压力。
主流应用架构
客户端在对数据库发起请求时,先到缓存层查看是否有所需的数据,如果缓存层存有客户端所需的数据,则直接从缓存层返回,否则进行穿透查询,对数据库进行查询,如果在数据库中查询到该数据,则将该数据回写到缓存层,以便下次客户端再次查询能够直接从缓存层获取数据。
缓存中间件 -- Memcache和Redis的区别
Memcache:代码层类似Hash
- 支持简单数据类型
- 不支持数据持久化存储
- 不支持主从
- 不支持分片
Redis
- 数据类型丰富
- 支持数据磁盘持久化存储
- 支持主从
- 支持分片
为什么Redis能这么快
Redis的效率很高,官方给出的数据是100000+QPS(query per second),这是因为:
- Redis完全基于内存,绝大部分请求是纯粹的内存操作,执行效率高。
- Redis使用单进程单线程模型的(K,V)数据库,将数据存储在内存中,存取均不会受到硬盘IO的限制,因此其执行速度极快,另外单线程也能处理高并发请求,还可以避免频繁上下文切换和锁的竞争,如果想要多核运行也可以启动多个实例。
- 数据结构简单,对数据操作也简单,Redis不使用表,不会强制用户对各个关系进行关联,不会有复杂的关系限制,其存储结构就是键值对,类似于HashMap,HashMap最大的优点就是存取的时间复杂度为O(1)。
- Redis使用多路I/O复用模型,为非阻塞IO(非阻塞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对象等)。
注:如果重复写入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是一个基于游标的迭代器,需要基于上一次的游标延续之前的迭代过程。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。
由于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会一直存在,等同于锁一直被持有无法释放。出现此问题的根本原因为:原子性得不到满足。
解决:从Redis2.6.12版本开始,我们就可以使用Set操作,将Setnx和expire融合在一起执行,具体做法如下。
SET KEY value [EX seconds] [PX milliseconds] [NX|XX]
- 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持久化的方式
- 根据redis.conf配置里的SAVE m n 定时触发(实际上使用的是BGSAVE)
- 主从复制时,主节点自动触发。
- 执行Debug Reload
- 执行Shutdown且没有开启AOF持久化。
BGSAVE的原理
启动:
1.检查是否存在子进程正在执行AOF或者RDB的持久化任务。如果有则返回false。
2.调用Redis源码中的rdbSaveBackground方法,方法中执行fork()产生子进程执行rdb操作。
3.关于fork()中的Copy-On-Write
fork()在linux中创建子进程采用Copy-On-Write(写时拷贝技术),即如果有多个调用者同时要求相同资源(如内存或磁盘上的数据存储),他们会共同获取相同的指针指向相同的资源,直到某个调用者试图修改资源的内容时,系统才会真正复制一份专用副本给调用者,而其它调用者所见到的最初的资源仍然保持不变。
RDB持久化方式的缺点
- 内存数据全量同步,数据量大的状况下,会由于I/O而严重影响性能。
- 可能会因为Redis宕机而丢失从当前至最近一次快照期间的数据。
AOF(Append-Only-File)持久化:保存写状态
AOF持久化是通过保存Redis的写状态来记录数据库的。相对RDB来说,RDB持久化是通过备份数据库的状态来记录数据库,而AOF持久化是备份数据库接收到的指令。
- AOF记录除了查询以外的所有变更数据库状态的指令。
- 以增量的形式追加保存到AOF文件中。
开启AOF持久化
1.打开redis.conf配置文件,将appendonly属性改为yes。
2.修改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(写时拷贝)。重写过程如下:
- 调用fork(),创建一个子进程。
- 子进程把新的AOF写到一个临时文件里,不依赖原来的AOF文件。
- 主进程持续将新的变动同时写到内存和原来的AOF里。
- 主进程获取子进程重写AOF的完成信号,往新AOF同步增量变动。
- 使用新的AOF文件替换掉旧的AOF文件。
AOF和RDB的优缺点
- RDB优点:全量数据快照,文件小,恢复快。
- RDB缺点:无法保存最近一次快照之后的数据。
- AOF优点:可读性高,适合保存增量数据,数据不易丢失。
- AOF缺点:文件体积大,恢复时间长。
RDB-AOF混合持久化方式
redis4.0之后推出了此种持久化方式,RDB作为全量备份,AOF作为增量备份,并且将此种方式作为默认方式使用。
在上述两种方式中,RDB方式是将全量数据写入RDB文件,这样写入的特点是文件小,恢复快,但无法保存最近一次快照之后的数据,AOF则将redis指令存入文件中,这样又会造成文件体积大,恢复时间长等弱点。
在RDB-AOF方式下,持久化策略首先将缓存中数据以RDB方式全量写入文件,再将写入后新增的数据以AOF的方式追加在RDB数据的后面,在下一次做RDB持久化的时候将AOF的数据重新以RDB的形式写入文件。
这种方式既可以提高读写和恢复效率,也可以减少文件大小,同时可以保证数据的完整性。在此种策略的持久化过程中,子进程会通过管道从父进程读取增量数据,在以RDB格式保存全量数据时,也会通过管道读取数据,同时不会造成管道阻塞。可以说,在此种方式下的持久化文件,前半段是RDB格式的全量数据,后半段是AOF格式的增量数据。此种方式是目前较为推荐的一种持久化方式。
Redis数据的恢复
RDB和AOF文件共存情况下的恢复流程
从图可知,Redis启动时会先检查AOF是否存在,如果AOF存在则直接加载AOF,如果不存在AOF,则直接加载RDB文件。
Pineline
Pipeline和Linux的管道类似,它可以让Redis批量执行指令。
Redis基于请求/响应模型,单个请求处理需要一一应答。如果需要同时执行大量命令,则每条命令都需要等待上一条命令执行完毕后才能继续执行,这中间不仅仅多了RTT,还多次使用了系统IO。Pipeline由于可以批量执行指令,所以可以节省多次IO和请求响应往返的时间。但是如果指令之间存在依赖关系,则建议分批发送指令。
Redis的同步机制
主从同步原理
Redis一般是使用一个Master节点来进行写操作,而若干个Slave节点进行读操作,Master和Slave分别代表了一个个不同的RedisServer实例,另外定期的数据备份操作也是单独选择一个Slave去完成,这样可以最大程度发挥Redis的性能,为的是保证数据的弱一致性和最终一致性。另外,Master和Slave的数据不是一定要即时同步的,但是在一段时间后Master和Slave的数据是趋于同步的,这就是最终一致性。
全同步过程
- Slave发送sync命令到Master。
- Master启动一个后台进程,将Redis中的数据快照保存到文件中。
- Master将保存数据快照期间接收到的写命令缓存起来。
- Master完成写文件操作后,将该文件发送给Slave。
- 使用新的AOF文件替换掉旧的AOF文件。
- Master将这期间收集的增量写命令发送给Slave端。
增量同步过程
- Master接收到用户的操作指令,判断是否需要传播到Slave。
- 将操作记录追加到AOF文件。
- 将操作传播到其它Slave:1.对齐主从库;2.往响应缓存写入指令。
- 将缓存中的数据发送给Slave。
Redis Sentinel(哨兵)
主从模式弊端:当Master宕机后,Redis集群将不能对外提供写入操作。Redis Sentinel可解决这一问题。
解决主从同步Master宕机后的主从切换问题:
- 监控:检查主从服务器是否运行正常。
- 提醒:通过API向管理员或者其它应用程序发送故障通知。
- 自动故障迁移:主从切换(在Master宕机后,将其中一个Slave转为Master,其他的Slave从该节点同步数据)。
Redis集群
原理:如何从海量数据里快速找到所需?
分片
按照某种规则去划分数据,分散存储在多个节点上。通过将数据分到多个Redis服务器上,来减轻单个Redis服务器的压力。
一致性Hash算法
既然要将数据进行分片,那么通常的做法就是获取节点的Hash值,然后根据节点数求模,但这样的方法有明显的弊端,当Redis节点数需要动态增加或减少的时候,会造成大量的Key无法被命中。所以Redis中引入了一致性Hash算法。
该算法对2^32 取模,将Hash值空间组成虚拟的圆环,整个圆环按顺时针方向组织,每个节点依次为0、1、2…2^32-1,之后将每个服务器进行Hash运算,确定服务器在这个Hash环上的地址,确定了服务器地址后,对数据使用同样的Hash算法,将数据定位到特定的Redis服务器上。如果定位到的地方没有Redis服务器实例,则继续顺时针寻找,找到的第一台服务器即该数据最终的服务器位置。
Hash环的数据倾斜问题
Hash环在服务器节点很少的时候,容易遇到服务器节点不均匀的问题,这会造成数据倾斜,数据倾斜指的是被缓存的对象大部分集中在Redis集群的其中一台或几台服务器上。
如上图,一致性Hash算法运算后的数据大部分被存放在A节点上,而B节点只存放了少量的数据,久而久之A节点将被撑爆。
针对这一问题,可以引入虚拟节点解决。简单地说,就是为每一个服务器节点计算多个Hash,每个计算结果位置都放置一个此服务器节点,称为虚拟节点,可以在服务器IP或者主机名后放置一个编号实现。
例如上图:将NodeA和NodeB两个节点分为Node A#1-A#3 NodeB#1-B#3。
结语
这篇准(tou)备(lan)了相当久的时间,因为有些东西总感觉自己拿不准不敢往上写,差点自闭,就算现在发出来了也感觉有很多地方是需要改动的。如果有同学觉得哪里写的不对劲的,评论区留言…嗯,我不要你觉得,我要我觉得。
相关推荐
- 说说Redis的单线程架构(redis的单线程模型)
-
一句话总结Redis采用单线程处理命令请求,避免了多线程的上下文切换和锁竞争,保证原子性操作。其基于内存的高效执行和I/O多路复用模型支撑了高并发性能。网络I/O和持久化操作(如RDB/AOF)由后台...
- 答记者问之 - Redis 的高效架构与应用模式解析
-
问:极客程序员你好,请帮我讲一讲redis答:redis主要涉及以下核心,我来一一揭幕Redis的高效架构与应用模式解析Redis是一个开源的内存数据存储系统,因其高性能、丰富的数据结构和易用性...
- Redis的5种核心数据结构,及其最经典的“应用场景”
-
Redis凭什么稳坐缓存界头把交椅?全靠这五个“身怀绝技”的数据结构!在分布式系统的江湖里,Redis就像一位身怀绝技的武林高手,而它的五大核心数据结构正是克敌制胜的五套绝学。今天咱们就来拆解这些独门...
- 精准定位文件包含漏洞:代码审计中的实战思维
-
前言最近看到由有分析梦想cms的,然后也去搭建了一个环境看了一看,发现了一个文件包含漏洞的点,很有意思,下面是详细的复现和分析,以后代码审计又多了一中挖掘文件包含漏洞的新思路环境搭建下载https...
- ARDM:一款国产跨平台的Redis管理工具
-
ARDM(AnotherRedisDesktopManager)是一款免费开源的Redis桌面管理客户端,支持Windows、Mac、Linux跨平台。功能特性ARDM提供的主要功能如...
- SpringBoot的Web应用开发——Web缓存利器Redis的应用!
-
Web缓存利器Redis的应用Redis是目前使用非常广泛的开源的内存数据库,是一个高性能的keyvalue数据库,它支持多种数据结构,常用做缓存、消息代理和配置中心。本节将简单介绍Redis的使...
- Windows服务器部署CRMEB开源电商系统,详细教程来了!
-
安装PHP已经安装过PHP的可以跳过首先安装VC运行库下载地址https://docs.microsoft.com/zh-cn/cpp/windows/latest-supported-vc-redi...
- Windows系统下Redis各个安装包介绍与选择指南
-
简介Redis作为高性能的键值数据库,广泛应用于缓存、消息队列等场景。在Windows系统中部署Redis时,用户可以选择多种安装包以满足不同的需求。本文将详细介绍以下Redis8.0.3版本的安装...
- 从面试题入手,深度剖析Redis Cluster原理
-
揭开RedisCluster的神秘面纱**在当今数字化浪潮中,数据量呈爆炸式增长,应用程序对数据存储和处理的要求也日益严苛。Redis作为一款高性能的内存数据库,凭借其出色的读写速度和丰富的数...
- 给大家推荐些好的c语言代码的网站
-
C语言,那就来推荐几个吧,部分含有C++:1、TheLinuxKernelArchives(kernel.org)Linux内核源码,仅限于C,但内核庞大,不太适合新手;2、redis(redi...
- Redis String 类型的底层实现与性能优化
-
RedisString是Redis中最基础也是应用最广泛的数据类型,它能存储文本、数字、二进制数据等多种形式的信息。深入理解其底层实现对构建高性能分布式系统至关重要。Redis字符串的底层结...
- 阿里面试问:Redis 为什么把简单的字符串设计成 SDS?
-
分享了一道面阿里的redis题,我看了以后觉得挺有意思。题目大致是这样的面试官:了解redis的String数据结构底层实现嘛?铁子:当然知道,是基于SDS实现的面试官:redis是用C语言开发的,那...
- 编程语言那么多,为何C语言能成为最成功的语言?
-
编程语言那么多,为何C语言能成为最成功的语言?2025年嵌入式岗位暴增47%,新人却还在问"C语言过时了吗"。真相是连机器人关节驱动都得靠它写,不会指针连芯片手册都看不懂。见过用Pyt...
- go-zero 使用 redis 作为 cache 的 2 种姿势
-
在go-zero框架内,如在rpc的应用service中,其内部已经预置了redis的应用,所以我们只需要在配置中加入相关字段即可,另外,在svcContext声明redisc...
- Redis事务深度解析:ACID特性、执行机制与生产实践指南
-
一、Redis事务的本质与核心机制Redis事务通过MULTI、EXEC、WATCH等命令实现,其本质是将多个命令序列化后一次性执行,而非传统数据库的严格事务模型。核心特点如下:命令队列化:MULT...
你 发表评论:
欢迎- 一周热门
- 最近发表
- 标签列表
-
- oracle位图索引 (74)
- oracle批量插入数据 (65)
- oracle事务隔离级别 (59)
- oracle主从同步 (56)
- oracle 乐观锁 (53)
- redis 命令 (83)
- php redis (97)
- redis 存储 (67)
- redis 锁 (74)
- 启动 redis (73)
- redis 时间 (60)
- redis 删除 (69)
- redis内存 (64)
- redis并发 (53)
- redis 主从 (71)
- redis同步 (53)
- redis 哨兵 (52)
- redis结构 (53)
- redis 登录 (54)
- redis 面试 (58)
- 阿里 redis (59)
- redis 搭建 (53)
- redis的缓存 (55)
- lua redis (58)
- redis 连接池 (61)