目录💻
前言
java知识点汇总🍅
redis篇
Redis是一个开源的的内存数据结构存储系统,可用做数据库、缓存和消息消息中间件,支持多种数据类型。适用于各种应用场景,尤其适用于需要高性能读写操作的场景、例如缓存、会话管理、排行榜等等。
而Redis之所以这么快主要是基于一下特点:
- 基于内存实现(相对于磁盘来说,读写速度不是一个量级)
- 高效的数据结构(hash结构)
- 合理的数据编码(redis有多种数据类型,并且每个数据类型针对不同场景也有不同的底层数据结构编码)
- 合适的线程模型(多路复用模型)
1、数据类型
1.1、String:字符串
String是Redis中最基础的数据类型,也是在实际应用中使用最多的数据类型。底层的数据结构实现主要有INT和SDS(简单动态字符串)两种数据结构实现。
-
INT:以long类型存储,用来存储一些可以直接转换成整数型的数据,并且可以直接进行加减操作运算
-
SDS:SDS的字符串是动态字符串,是可以修改的字符串,内部结构实现上类似于 Java 的ArrayList,采用预分配冗余空间的方式来减少内存空间的的频繁分配,
- **动态调整大小:SDS可以根据字符串的长度动态调整内存大小,**当字符串长度小于 1M 时,扩容都是加倍现有的空间,如果超过 1M,扩容时一次只会多扩 1M 的空间。需要注意的是字符串最大长度为 512M。
- **SDS 获取字符串长度的时间复杂度是 O(1):**因为内部有len字段维护了字符串长度信息,因此不管长度时多少都可以直接获取长度。并且可以有效的防止缓冲区溢出。
- **SDS不仅可以保存文本数据,还可以保存二进制数据:**SDS的所有API都会以处理二进制的方式处理SDS存储在buf[]中的数据。所以SDS不仅可以保存文本数据,还可以保存图片、视频等二进制数据。
SDN结构如下:
struct sdshdr { int len; // 已使用的长度 int free; // 剩余的可用空间 char buf[]; // 实际保存字符串的地方 };
字符串对象内部编码有3种:int
、raw
和embstr
。
字符串对象和编码以及底层数据结构关系图如下:
如果字符串对象保存的是一个整数值,并且这个整数值可以用long
类型来表示,则编码类型采用int类型;如果字符串对象保存的字符串则会使用SDS来保存这个字符串,
如果这个字符串的长度小于等于32字节则会采用embstr
编码(embstr
编码是专门用于保存短字符串的一种优化方式);如果字符串长度大于32字节那么编码方式会变为raw
。
应用场景:
- 缓存:直接缓存值
- 计数器:因为Redis是单线程,所以执行的过程是原子的,因此可以用作计数器来记录点击量、点赞数等等。
- 分布式锁:SET有一个NX参数,可以实现只有在[key不存在时才可以插入]
- 共享session信息:在处理会话时,一般需要通过session保存用户登陆信息。单系统的话可以直接保存在服务器。但分布式系统则不行,会出现重复登陆,则需要借助Redis进行统一管理和存储。
1.2、List:数组
主要用于存储有序的字符串列表,可以从列表的两端添加或删除元素
底层数据结构在3.2版本以前则采用的是LinkedList和ZipList,而在3.2版本以后则使用quicklist代替了双端链表和压缩链表**。**
-
LinkedList(双端列表):在数据较多时采用双端链表,双端列表允许我们在O(1) 时间内从头部和尾部进行插入和删除操作
-
**ZipList(压缩链列表):**是一段连续的内存空间,将多个元素顺序存储在一个紧凑的结构中。它类似于一个数组,但元素之间没有明确的分隔符,而是通过特殊的编码方式来区分。
-
**quicklist(快速列表):**底层是由zipList和LinkedList组合的混合体,外部是一个LinkedList内部是存储的是一个zipList;相当于是由多个zip使用双向指针串接起来
应用场景:
-
消息队列:利用list都PUSH 操作,将任务存储在list中,然后工作线程在通过POP操作取出。要注意的一点,list的队列并不会主动的去通知消费者有新消息写入,如果消费者想要及时处理消息,则需要不停的去调用POP命令,有新消息就读取到,没有就读取到空值。
-
所以Redis提供了一个BRPOP命令,可以实现阻塞式读取。
-
也可以用作消息排队、栈等先进先出的需求等
1.3、Set:集合
Set类型是一种无序、不重复的集合,集合中的元素没有先后顺序但都是唯一,有点类似于java的HashSet。主要可用作存储一个列表数据,又不希望出现重复数据时,Set是一个很好的选择。并且Set还可以判断某个元素是否在一个Set集合内。Set还可以支持多个集合之间的并集、差集、交集操作
底层数据结构主要分两种,如果元素都是整数并且小于一定范围则使用IntSet来存储;如果不满足前面的条件则会用HashTable来进行存储。
-
IntSet(整数集合):使用整数集合来存储成员,适用于所有数据都是整数的小集合
-
dict(字典表):字典表主要是通过hash函数将元素映射到桶中的数据结构。当不满足使用IntSet的时候就会采用dict来存储,会把value设置为nul。
应用场景:
- 点赞:比如key是文章id,value是用户id,这样就可以有效的避免出现用户重复点赞的情况
- 获取共同好友:如key是用户id,value是好友id,通过
SINTER key1 key2
判断两个集合的交集,则可以获取到共同好友数 - **抽奖活动:**每次从集合中抽出指定的中奖名额,如果允许重复中奖则可以使用
SRANDMEMBER lucky 中奖数
,如果不允许重复中奖则可以用SPOP lucky 中奖数
1.4、ZSet:有序集合
redis的有序集合,每个元素相比于set都多了一个排序属性score,都关联一个分数,这些元素会按照分数进行排序。其他的和set差不多都是不能有重复的元素。
Zset底层主要使用的ZipList和SkipList + dict实现,如果元素个数小于128个,并且每个元素的值小于64字节,则使用ziplist作为底层数据结构;否则则采用SkipList + dict作为底层数据结构**。在Redis7.0之后ZSet的底层的ziplist也逐渐被quicklist**代替
-
skiplist + dict 的组合实现
-
**skiplist:**用来存储ZSet的成员和分数,并维护成员按照分数升序排序。跳跃表的多层结构保证了它能在 O(log n) 的平均时间复杂度内高效的查找、插入和删除操作。并且SkipList是一个概率数据结构,它是根据概率来计算后向指针列表层数,这从而也会影响到具体的一个查找性能,
skiplist的数据结构定义://用来计算指针列表层数的 #define ZSKIPLIST_MAXLEVEL 32 #define ZSKIPLIST_P 0.25 // zskiplistNode定义了skiplist中的每一个节点结构 typedef struct zskiplistNode { robj *obj; //节点数据 double score; //分数 struct zskiplistNode *backward; //指向链表前一个节点的指针(前向指针),节点只有1个前向指针 struct zskiplistLevel { struct zskiplistNode *forward; //后向指针指向 unsigned int span; //表示跨越了多少个节点,这个计数不包括指针的起点节点,但包括指针的终点节点。 } level[]; //存放指向各层链表后一个节点的指针(后向指针)的数组对象 } zskiplistNode; // zskiplist定义了真正的skiplist结构 typedef struct zskiplist { struct zskiplistNode *header, *tail; //头指针和尾指针 unsigned long length; //链表长度 int level;节点层数的最大值 } zskiplist;
-
-
dict (字典):用于快速查找成员,和set类型类似。字典的键是成员,值是 skiplist 中对应节点的指针。查找某个成员时,先通过字典找到对应 skiplist 节点,然后可以直接访问成员和分数。
应用场景:
- 排行榜:如成绩排名等需要根据分数值排序的场景
skiplist具体案例实现介绍
把下面分数下输入到zset中
- Alice 87.5
- Bob 89.0
- Charles 65.5
- David 78.0
- Emily 93.5
- Fred 87.5
注意:图中后向指针指向上面括号中的数字,表示对呀的span的值。表示当前指针跨越了多少个节点,不包括指针的开始节点,但包括结束节点。
假设我们要查找score=89.0的元素(即Bob的成绩)的排名,红色的就是指向的最终方向。
首先使用89.0跟78.0,然后在和87.5比,比他们大,则继续向后比,如果比他们小则在当前节点的
level[]
取下层的元素;然后再通过87.5节点上指向的就是89.0分的元素,则代表找到这个元素。通过把这些指针上面的指针累加起来,就得到Bob的排名(2+2+1)-1=4(减1是因为rank值以0起始)。需要注意这里算的是从小到大的排名,而如果要算从大到小的排名,只需要用skiplist长度减去查找路径上的span累加值,即6-(2+2+1)=1。
1.5、Hash:哈希
hash类似于java的hashMap,用于存储键值对的集合,特别适用于存储对象信息,可以用来存储对象的多个字段信息。Hash 中的每个键都是唯一的字符串,对应的值可以是字符串、数字或其他数据类型。
底层实现主要由ziplist和dict构成**,如果元素个数少于512个,并且每个字段多键和值都比较短时,就会采用zipList,否则则采用dict进行存储。在Redis7.0之后Hash的底层的ziplist也逐渐被quicklist**代替
应用场景:
- **缓存对象信息:**Hash 类型的 (key,field, value) 的结构与对象的(对象id, 属性, 值)的结构相似,也可以用来存储对象。
- **购物车:**用户id,商品id,商品数量
1.6、Bitmap:位图
Bitmap是一种高效的存储大量布尔值(0或1)的数据结构。它可以被理解为一个位为单位的数组,每个位为单位的数组,每个位可以表示一个布尔值,0代表false,1代表true
Bitmap 本身是用 String 类型作为底层数据结构实现的一种统计二值状态的数据类型。
应用场景:
- 用户签到:可以使用BitMap记录每个用户每日签到情况。
- 统计活跃用户:统计用户在特定时间内的活跃情况
- 记录用户行为:可以使用 BitMap 记录用户是否执行了某些操作
1.7、Geospatial:地理位置
顾名思义,Geospatial主要是用来存储地理信息(经纬度)的,它主要用来构建地理位置信息的应用。
使用 Geohash 算法将经纬度编码成字符串,并存储在键中,Geohash是一种将二维空间映射到一维字符串的算法,它可以将地理位置,编码成一个字符串,并且字符串的前缀可以表示一个区域。
例如:37.785389, -122.405691 (旧金山) ====⇒ 9q97q5
1.8、Stream:流
Stream类型是一种新的数据结构,主要是用于构建消息队列和流式数据处理应用的。它可以被理解为一个有序的日志,Stream 类型提供了对消息的持久化、有序存储和消费者组管理等功能,适用于实时数据处理和消息队列场景。并且Stream中的消息会被持久化到磁盘中,而且Stream还支持消息确认机制,相当于一个轻量级别的MQ。
Stream有效的解决了redis消息队列的一些缺陷,在Stream没出现之前,Redis实现消息队列的两种方式都有一定的缺陷,如:
- 发布订阅模式:不能持久化也就无法保证可靠的保存消息,并且对于离线重连的客户端不能读取历史消息的缺陷
- List实现的消息队列不能进行重复消费,一个消息被消费完就会被删除,而且生产者需要自行实现全局唯一ID
应用场景:
- 消息队列:可以用作构建一个消息队列,提供给不同服务传递数据
- 实时数据处理:可以使用Stream接收实时数据流,并进行实时处理和分析。
- 日志收集:可以使用Stream收集应用程序的日志,并且进行存储
2、持久化方案
2.1、RDB快照
RDB持久化会将Redis的数据以快照的方式保存到一个二进制文件中(默认文件为dump.rdb)。Redis定期将内存中的数据写入磁盘,形成一个快照文件。
触发条件主要分为手动触发和自动触发两种。
-
手动触发:使用**
save
,bgsave
**命令- save:save命令会阻塞redis服务器进程,直到RDB文件创建完毕为止,在服务器进行阻塞期间,服务器不能处理任何命令请求。
- bgsave:bgsave命令会通过fork函数创建一个子进程在后台生成快照文件,不会阻塞redis服务,服务进程可以继续存储命令请求,真正阻塞的只有fork那很短暂的时刻。
- fork函数主要是利用linux操作系统的写时复制(Copy On Write,即 COW)机制,让父子线程共享内存,从而减少内存占用,并且避免了没有必要的数据复制
bgsave命令期间,客户端发送的save和bgsave命令会被拒绝,这样主要是为了防止父子进程之间产生竞争
-
自动触发:
-
通过配置
redis.conf
文件触发规则,自行执行# 当用户设置了多个save的选项配置,只要其中任一条满足,Redis都会触发一次BGSAVE操作 save 900 1 save 300 10 save 60 10000 # 以上配置的含义:900秒之内至少一次写操作、300秒之内至少发生10次写操作、 # 60秒之内发生至少10000次写操作,只要满足任一条件,均会触发bgsave
-
执行shutdown命令关闭服务器时,如果没有开启AOF持久化功能,那么就会自动执行一次bgsave
-
主从同步:
-
优缺点:
- 优点:容易简单,恢复速度快,适合大规模的数据恢复场景,如备份全量恢复
- 缺点:没办法做到实时持久化,可能会在间隔期间宕机会出现数据丢失的情况,占用磁盘空间可能会比较大
2.2、AOF日志
AOF持久化是将Redis执行的所有写操作都记录到一个日志(appendonly.aof)文件中,redis启动时会读取AOF文件,将记录的操作重复一遍,从而恢复数据。
AOF默认是关闭的,通过redis.conf配置文件进行开启
## 此选项为aof功能的开关,默认为“no”,可以通过“yes”来开启aof功能
## 只有在“yes”下,aof重写/文件同步等特性才会生效
appendonly yes
## 指定aof文件名称
appendfilename appendonly.aof
## 指定aof缓冲区同步策略,有三个合法值:always everysec no,默认为everysec
appendfsync everysec
AOF在写入时,并不是每次都直接往磁盘写,而是会将写命令追加到AOF缓冲区的末位,之后AOF缓冲区再同步到磁盘。这样主要是为了提高效率,毕竟如果每次写到话性能太低了。
AOF缓冲区同步到磁盘的过程由flushAppendonlyFile
函数完成,该参数有以下三个选项:
- **always:**每次发生写时都同步到AOF文件,是最安全的的选项,效率最低
- **everysec:**每秒同步一次到AOF文件,在性能和安全之间做到一个平衡
- **no:**不主动写入AOF文件,何时同步由操作系统决定
AOF重写
AOF重写主要是对AOF文件的一个优化,虽然叫重写,但并不是去对向有的数据进行重写,AOF重写主要是通过去redis服务器中读取当前的数据来进行写的。
如:假设我给分别执行了下面几条命令
rpush list "A"
rpush list "B"
rpush list "C"
如果redis为了保存当前list的数据,就需要AOF文件保存三条命令,而我现在要对AOF进行重写,不是去重新写入这三条命令,而是会去直接读取redis服务器中list的数据,然后用一条命令:rpush list "A" "B" "C" "D" "E" "F"
进行存储,这样就会由六条命令变为一条数据
优缺点:
优点:数据的一致性和完整性更高, 并且可以配置不同的同步策略:例如 always
、everysec
、no
,控制 AOF 文件的同步频率,平衡性能和数据安全。
缺点:AOF记录的内容越多,文件越大,数据恢复越慢
2.3、混合持久化
Redis4.0之后新推出的一个持久化方案,RDB-AOF 混合持久化。
开启混合持久化后,会先以RDB格式把当前Redis服务器的数据写入到(appendonly.aof)
文件中,然后在把后续的增量数据追加到(appendonly.aof)
文件文件后面。因为RDB是二进制写入到,这样文件的体积会变的更小,并且后续的增量数据是以AOF的格式写入的。而且恢复数据的时候也是先恢复前面的RDB的在去恢复后面追加的AOF数据。
这样既避免了存粹的AOF模式写入文件过大数据恢复慢的缺点也避免了RDB模式在写入时会有暂停的缺点。
文件格式:
3、缓存问题
3.1、缓存击穿
产生原因:
由于大量请求数据库的数据在redis缓存中都没查询到数据,则都会去服务器查询,一般造成这个的原因主要可能是业务/开发/运维设计有问题造成大量请求无法在redis中命中,还可能是黑客dos攻击等原因造成。
解决办法:
- 通过网关过滤一次ip请求,以及请求过多等异常非法请求,并且对参数进行校准排除非法参数
- 如果这个请求redis查询不到,在数据库查询还是查询为nul,则把nul也存储到redis
- 使用布隆过滤器,快速判断请求的值是否存储在redis中,如果有则请求通过放行到redis中去查询,如果没有,则直接返回结果
- 具体实现:它由初始值为0的位图数组和N个哈希函数组成,一个对一个key进行N个hash算法获取N个值,在比特数组中将这N个值散列后设定为1,然后查的时候如果特定的这几个位置都为1,那么布隆过滤器判断该值存在(会出现误判)
3.2、缓存穿透
产生原因:
当redis存储的热点数据过期时,大量的请求到来,从而造成大批量的请求打到数据库。
解决办法:
- 对热点数据不设置过期时间,或者通过异步线程定时更新那些快要过期的热点数据(类似redisson的”看门狗机制”)的过期时间
- 使用互斥锁方案,缓存失效时,不是立即去数据库查询,而是通过原子命令让只有一个成功的请求可以去查询数据库,其他的失败请求重新去查询缓存,这样就只会有一个请求到达数据库
3.3、缓存雪崩
产生原因:
redis存储的key大量过期,请求的数据都访问到数据库,引起数据库宕机
解决办法:
- 设置过期时间时,在后面加上随机数,避免统一设置的key统一过期,
- 搭建高可用的redis集群
4、常用场景
4.1、分布式锁
产生原因:在开发过程中,我们为例提高效率,我们往往会引用多线程并行的方式来提高访问修改并发,我们在对面并发问题时,多个线程一起修改时,如果是单应用部署,直接通过synchronized关键字、 ReetrantLock 类等进程锁来控制一个JVM进程那多个线程对本地共享资源对访问。
但如果是在分布式、微服务等架构下,不同的服务器/客户端通常运行在独立的JVM进程上,使用本地锁的方式就不能有效的解决这个问题,因此就引出了一个分布式锁的概念,具体架构如下。
通过redis实现分布式锁主要可分为三种方式:
-
直接通过SETNX命令实现:
SETNX:SETNX是redis的一个原子操作命令,仅当该键不存在时才能设置成功,如果成功则会返回1,不成功则会返回0
- **实现流程:**模拟一个扣减库存逻辑
- A线程和B线程同时去获取锁扣减库存,假如A线程获取到锁,B线程在去获取则会获取失败。而在设置的时候为了避免出现A线程因为某些问题一直不释放锁的情况,我们会在设置一个过期时间,使得可以A线程在达到特定的时间后自动释放锁。
- 在这之中B线程会一直循环去获取锁,直到A线程执行完业务代码,通过delete命令释放锁,才会成功获取到锁。
- 删除锁的时候也出现一种情况,如果A线程在执行完业务的时候锁已经到期自动释放锁了,这时候再去删除的话,可能就会误删B线程的锁。所以在删除锁的时候可以做一个优化,设置给每一个请求设置一个唯一ID,在删除之前去验证该锁是不是自己的锁,这样就可以避免出现误删的情况。
- **可能会出现的问题:**在释放锁的时候可能会出现,验证完id到删除锁不是原子操作,就可能会验证完后,A线程获取到的锁自动过期,B线程去获取到了锁,此时A线程在去删除则会把B线程的锁删除。
- **实现流程:**模拟一个扣减库存逻辑
-
通过SETNX命令和编写Lua脚本实现
-
实现流程:
-
前面的获取锁是一样的通过SETNX命令获取,
-
释放锁针对释放锁时不能原子操作,可以优化为使用Lua脚本编写一个验证锁是否是自己的,在去删除的脚本实现原子操作。
private static final String UNLOCK_SCRIPT = "if redis.call('get', KEYS[1]) == ARGV[1] then " + "redis.call('del', KEYS[1]); " + "return true; " + "else return false; " + "end";
-
-
**可能会出现的问题:**该方式虽然没啥大的问题,但该方式因为设置锁时,由于时间只能设置固定的,因此在一些特定的业务场景可能还是会出现一定的问题。需要很好的去把握设置锁的过期时间,过长可能会造成失效不及时,过短可能会导致频繁地去获取锁。
-
-
通过Redisson框架实现
Redisson是一个基于Redis的分布式java对象和服务框架,它提供了一系列的分布式对象和服务,使得在java应用程序中使用Redis变得更加方便和高效。并且内部提供了很多锁供我们选择
Redisson常用分布式锁对象:
redissonClient.getLock(key) :
可重入锁(最常用的)redissonClient.getFairLock(key):
公平锁redissonClient.getSpinLock(key):
自旋锁redissonClient.getReadWriteLock(key)
:读写锁redissonClient.getMultiLock(lock):
多重锁
获取锁的方式:
lock.lock()
:阻塞的方式获取,该方式去获取锁,会一直阻塞请求,直到获取到锁。该方式获取到锁如果。默认30秒为最大时间10秒续期一次,lock.tryLock(expireTime,TimeUnit.SECONDS)
:非阻塞的方式获取锁,最多可以设置三个参数,分别指定重试时间,锁过期时间和时间单位;一般建议设置一个重试时间,如果指定时间没法则返回false,因为毕竟是非阻塞执行,如果不设置则会一直去请求获取锁。
**释放锁:**则直接通过
lock.unlock()
进行释放即可具体执行逻辑则类似
4.2、接口幂等性
在编程过程中,接口接口幂等指的是多次调用一个接口,对系统的产生与影响只有一次,相同参数多次重复调用方法或者接口不会改变业务状态,可以保证重复调用的结果和单次调用的结果一致。
出现的原因:
- 前端重复提交:用户注册,用户创建商品等操作,前端都会提交一些数据给后台服务,后台需要根据用户提交的数据在数据库中创建记录,如果用户不小心多点了几次,后端收到好几次提交,这时数据库就会出现多条记录
- 接口超时重试:对于给第三方调用的接口,有可能会因为网络原因而调用失败,这时,一般在设计的时候会对接口调用加上失败重试的机制。如果第一次调用已经执行了一半时,发生网络异常,这时再次调用时就会出现脏数据的存在
- 消息重复消费:在使用消息中间件来处理消息队列,且手动ack确认消息被正常消费时。如果消费者突然断开连接。那么已经执行了一半的消息会重新放回队列,则会被其他消费者重新消费
解决方案:
-
通过token机制实现:
- 客户端先发送一次请求去获取token,服务端会生成一个全局唯一的ID作为token保存在redis中,同时把这个ID返回给客户端
- 客户端在确认提交时,则携带这个token请求
- 服务端会校验这个token,如果校验成功,则执行业务,并删除redis中的token
- 如果校验失败,说明已经有请求成功执行了。表示重复消费
-
基于redis的原子命令SETNX实现:
- 前端提交后端端时候携带一个唯一字段(可以是订单id或者客户请求客户端IP加路径等)
- 将该字段以SETNX的方式存如redis中,并根据业务设置相应的超时时间(可以通过lua脚本实现,因为需要设置时一起设置超时时间)
- 如果设置成功,证明这是第一次请求,则执行后续业务逻辑
- 如果设置失败,则代表前面已经有请求执行成功了,表示重复消费
-
或者通过mysql的唯一主键实现。
5、高可用
Redis单机版可能在生产环境中常常无法保证高可用性需求,一旦单点故障,整个服务都会瘫痪。所以就需要使用一些高可用的方案解决这个问题。
具体方案:
-
Redis主从模式:redis提供的主从模式,是通过复制的方式,将主节点上的数据同步一份到从节点上。主节点只会有一个,从节点可以有多个,并且在主从复制的基础上实现了读写分离;主节点负责写,从节点负责读,在写多读上的场景上非常适用。
优缺点:
- 优点:相对简单,两台机子就行。
- 缺点,如果主节点挂了,需要手动设置一个从节点转为主节点,同时需要修改应用方的 主节点地址,整个过程需要人工,相对比较麻烦
-
Redis Sentinel( 哨兵)模式:Sentinel 是一个独立进程,用于管理多个 Redis 实例。它监控主服务器的状态,如果主服务器出现故障,Sentinel 会自动将其中一个从服务器提升为主服务器,提供高可用性。部署至少三台节点实现
在监控的过程中如果主节点挂了,则sentinel会通过投票选举出一个从节点作为主节点
优缺点:
- 优点:可以通过哨兵监听节点状态,在自动选举出主节点,提高可用性
- 缺点: 单点故障问题依然存在(Sentinel 集群本身需要高可用性保障), 脑裂问题(brain split,多个 Sentinel 同时认为主服务器挂了,各自选出一个新的主服务器),数据丢失风险(主从复制延迟)
-
Redis Cluster集群模式:Redis Cluster是一种分布式数据库方案,集群通过数据分片进行数据管理,每个节点负责存储一部分数据,客户端通过客户端路由算法连接到正确的节点。 每个节点都是主从结构。
Redis Cluster采用的是无中心结构,每个节点都可以保存数据和整个集群状态,每个节点都和其他所有节点连接。要让集群正常运作至少需要三个主节点,即 Redis Cluster 至少为 6 个实例才能保证组成完整高可用的集群。
优缺点:
- 优点:数据分区,突破了 Redis 单机内存大小的限制,存储容量大大增加,另一方面 每个主节点都可以对外提供读服务和写服务,大大提高了集群的响应能力。并且集群支持主从复制和主节点的故障转移(与哨兵类似)。
- 缺点:耗费资源相对较多,配置和运维成本较高;数据迁移可能导致短暂服务中断。
6、过期策略和淘汰机制
6.1、过期策略
redis在存储数据时,如果设置了过期时间,缓存数据到了过期时间就会失效,那么redis是如何进行数据清除掉掉这些失效的数据的呢,这就要用到redis的过期策略了。
过期策略分类:
- 定时删除策略:在设置键的过期的同时,同时设置一个定时器,如果键过期则同时删除这个数据
- 优点:该策略可以立即删除掉这些过期的数据,对内存比较友好,可以有效的减少内存的占有
- 缺点:该策略因为每一个键都需要设置定时器去删除数据,因此对CPU的占用也会非常大,从而也会影响到缓存的响应时间和吞吐量
- 惰性删除策略:键过期时,暂时不删除,需要等到下一次有请求对该键进行访问的时候,查看该键是否过期了,如果过期了则会把该数据进行清除,否则则继续保留
- 优点:该策略可以最大的节省CPU资源,因为减少了定时器线程的建立,
- 缺点:对内存非常不友好,因为数据不是及时删除,需要等待到下一次访问,在一些极端情况下,可能会缓存大量的为删除的过期数据,占有大量内存
- 定期删除策略:每隔一段时间都会扫描一定数量的key,看这部分数据哪些对key的时间过期,过期则把这部分数据进行清除
- 优点:可以定期对数据进行清除释放内存,对内存相对比较友好,并且不用每个每个键设置定时器耗费过大的cup性能
- 缺点:删除的时候会耗费性能,但相对前两个中和很多
7、网络模型
使用过redis的都知道redis处理数据非常的快,官方做过测试,在一台普通的linux上跑单个redis实例,处理简单的命令,QPS都可以达到8w+,至于为何可以做到这么快有以下几种原因:
- Redis是基于纯内存的IO操作,相对于其他的磁盘IO来说,速度性能上基本可以快上多个量级
- 还有就是因为Redis采用的是单线程模型,因为redis的操作相对比较简单,而且基于内存,每条命令的处理速度会非常快,而使用单线程可以有效的减少了上下文切换,以及同步机制下锁带来的开销。
- 再有就是redis使用的网络模型进行了深度优化,采用的是基于Reactor开发了自己的IO多路复用的事件驱动模型
Reactor(反应堆模式):redis是基于该设计模式开发了自己的网络模型,形成了一个完备的基于IO多路复用的事件驱动服务器
redis在6.0版本之前使用的是单线程机制实现的,在6.0之后引入了单线程机制,但不是完全抛弃单线程,**只是在解析客户端命令和回写响应数据时采用了多线程,**核心的命令执行,IO多路复用模块还是主线程执行
-
单线程事件循环
-
redis服务器启动,开启事件循环,注册
acceptTcpHandler
连接应答处理器到用户配置的监听端口对应的文件描述符,等待新连接的到来 -
客户端和服务器建立网络连接,
-
acceptTcpHandler
被调用,读取处理器绑定到新连接,并初始化一个 client 绑定这个客户端连接 -
客户端发送redis命令,redis主线程通过
readQueryFromClient
读取客户端数据并解析成命令。 -
redis主线程执行命令,访问数据结构,并生成结果
-
redis主线程通过
sendReplyToClient
将执行结果返回给客户端
-
-
多线程异步复用
- redis服务器启动,开启事件循环,注册
acceptTcpHandler
连接应答处理器到用户配置的监听端口对应的文件描述符,等待新连接的到来 - 客户端和服务器建立网络连接,
acceptTcpHandler
被调用,读取处理器绑定到新连接,并初始化一个 client 绑定这个客户端连接- 客户端发送redis命令,redis主线程不会自己去处理解析这个数据会把这个数据client放入一个 LIFO 队列 clients_pending_read。
- 主线程利用 Round-Robin 轮询负载均衡策略,均匀的把LIFO分配给各个IO线程进行处理解析数据。
- 主线程轮休完毕后,会去读取IO线程解析的命令执行,访问数据结构,并将执行的结果交给IO线程,IO线程把处理结果返回给客户端
- redis服务器启动,开启事件循环,注册