Redis

Redis可以用来做什么

(1)做缓存。是实现分布式缓存的首选中间件,高性能,高并发

(2)做数据库。实现诸如点赞、关注、排行等对性能要求极高的互联网需求。

(3)分布式锁:通过 Redis 来做分布式锁是一种比较常见的方式。通常情况下,我们都是基于 Redisson 来实现分布式锁。分布式系统下,不同的服务/客户端通常运行在独立的 JVM 进程上。如果多个 JVM 进程共享同一份资源的话,使用本地锁就没办法实现资源的互斥访问了。

(4)消息队列:Redis 自带的 list 数据结构可以作为一个简单的队列使用。Redis 5.0 中增加的 stream 类型的数据结构更加适合用来做消息队列。它比较类似于 Kafka,有主题和消费组的概念,支持消息持久化以及 ACK 机制。和专业的消息队列相比,使用 Redis 来实现消息队列还是有很多欠缺的地方比如消息丢失和堆积问题不好解决。因此,我们通常建议不要使用 Redis 来做消息队列,你完全可以选择市面上比较成熟的一些消息队列比如 RocketMQ、Kafka。

(5)分布式 Session :利用 string 或者 hash 保存 Session 数据,所有的服务器都可以访问。

Redis缓存读写策略

Cache Aside Pattern(旁路缓存模式)

写:先更新DB,然后直接删除cache。缓存的写入速度是比数据库的写入速度快很多。防止不一致

 

Read/Write Through Pattern(读写穿透)

Read/Write Through Pattern 中服务端把 cache 视为主要数据存储,从中读取数据并将数据写入其中。cache 服务负责将此数据读取和写入 db,从而减轻了应用程序的职责。

写:

  • 先查 cache,cache 中不存在,直接更新 db。
  • cache 中存在,则先更新 cache,然后 cache 服务自己更新 db(同步更新 cache 和 db)。

 读:

 

Write Behind Pattern(异步缓存写入)

Write Behind Pattern 和 Read/Write Through Pattern 很相似,两者都是由 cache 服务来负责 cache 和 db 的读写。

但是,两个又有很大的不同:Read/Write Through 是同步更新 cache 和 db,而 Write Behind 则是只更新缓存,不直接更新 db,而是改为异步批量的方式来更新 db。

1.String应用场景

需要存储常规数据的场景

  • 举例:缓存 session、token、图片地址、序列化后的对象(相比较于 Hash 存储更节省内存)。
  • 相关命令:SETGET

需要计数的场景

  • 举例:用户单位时间的请求数(简单限流可以用到)、页面单位时间的访问数。
  • 相关命令:SETGETINCRDECR

分布式锁

利用 SETNX key value 命令可以实现一个最简易的分布式锁(存在一些缺陷,通常不建议这样实现分布式锁)

List应用场景

信息流展示

  • 举例:最新文章、最新动态。
  • 相关命令:LPUSHLRANGE

消息队列

Redis List 数据结构可以用来做消息队列,只是功能过于简单且存在很多缺陷,不建议这样做。

相对来说,Redis 5.0 新增加的一个数据结构 Stream 更适合做消息队列一些,只是功能依然非常简陋。和专业的消息队列相比,还是有很多欠缺的地方比如消息丢失和堆积问题不好解决。

Hsah应用场景

对象数据存储场景

  • 举例:用户信息、商品信息、文章信息、购物车信息。
  • 相关命令:HSET (设置单个字段的值)、HMSET(设置多个字段的值)、HGET(获取单个字段的值)、HMGET(获取多个字段的值)。

Set应用场景

需要存放的数据不能重复的场景

  • 举例:网站 UV 统计(数据量巨大的场景还是 HyperLogLog更适合一些)、文章点赞、动态点赞等场景。
  • 相关命令:SCARD(获取集合数量) 。

需要获取多个数据源交集、并集和差集的场景

  • 举例:共同好友(交集)、共同粉丝(交集)、共同关注(交集)、好友推荐(差集)、音乐推荐(差集)、订阅号推荐(差集+交集) 等场景。
  • 相关命令:SINTER(交集)、SINTERSTORE (交集)、SUNION (并集)、SUNIONSTORE(并集)、SDIFF(差集)、SDIFFSTORE (差集)。

需要随机获取数据源中的元素的场景

  • 举例:抽奖系统、随机点名等场景。
  • 相关命令:SPOP(随机获取集合中的元素并移除,适合不允许重复中奖的场景)、SRANDMEMBER(随机获取集合中的元素,适合允许重复中奖的场景)。

Sorted Set应用场景

需要随机获取数据源中的元素根据某个权重进行排序的场景

  • 举例:各种排行榜比如直播间送礼物的排行榜、朋友圈的微信步数排行榜、王者荣耀中的段位排行榜、话题热度排行榜等等。
  • 相关命令:ZRANGE (从小到大排序)、 ZREVRANGE (从大到小排序)、ZREVRANK (指定元素排名)。

Redis和传统的关系型数据库有什么不同

Redis是一种基于键值对的NoSQL数据库,而键值对的值是由多种数据结构和算法组成的。Redis的数据都存储于内存中,因此它速度惊人,读写性能可达10万/秒,远超关系型数据库。

关系型数据库是基于二维数据表来存储数据的,它的数据格式更为严谨,并支持关系查询。关系型数据库的数据存储于磁盘上,可以存放海量的数据,但性能远不如Redis。

1.对数据库高并发读写的需求
web2.0网站要根据用户个性化信息来实时生成动态页面和提供动态信息,所以基本上无法使用动态页面静态化技术,因此数据库并发负载非常高,往往要达到每秒上万次读写请求。关系数据库应付上万次SQL查询还勉强顶得住,但是应付上万次SQL写数据请求,硬盘IO就已经无法承受了。

2.对海量数据的高效率存储和访问的需求
类似Facebook,twitter,Friendfeed这样的SNS网站,每天用户产生海量的用户动态,以Friendfeed为例,一个月就达到了2.5亿条用户动态,对于关系数据库来说,在一张2.5亿条记录的表里面进行SQL查询,效率是极其低下乃至不可忍受的。再例如大型web网站的用户登录系统,例如腾讯,盛大,动辄数以亿计的帐号,关系数据库也很难应付。

3.对数据库的高可扩展性和高可用性的需求
在基于web的架构当中,数据库是最难进行横向扩展的,当一个应用系统的用户量和访问量与日俱增的时候,你的数据库却没有办法像web server和app server那样简单的通过添加更多的硬件和服务节点来扩展性能和负载能力。对于很多需要提供24小时不间断服务的网站来说,对数据库系统进行升级和扩展是非常痛苦的事情,往往需要停机维护和数据迁移。

 

传统关系模型:元组(行)是受限的结构,只能包含一系列的值,不能嵌套另外的元组和列表。所有操作都以元组为目标,而且其返回值必须是元组。适用于多种查询条件获取数据的需求。
聚合模型:是NoSQL操作数据时所用的单元,其结构比元组复杂,这种结构可以存放列表或嵌套其他记录。聚合数据模型的特点就是把经常访问的数据放在一起(聚合在一块),这样带来的好处很明显,对于某个查询请求,能够在与数据库一次交互中将所有数据都取出来。

 

Redis持久化机制

使用缓存的时候,我们经常需要对内存中的数据进行持久化也就是将内存中的数据写入到硬盘中。大部分原因是为了之后重用数据(比如重启机器、机器故障之后恢复数据),或者是为了做数据同步(比如 Redis 集群的主从节点通过 RDB 文件同步数据)。

RDB持久化:

Redis 可以通过创建快照来获得存储在内存里面的数据在 某个时间点 上的副本。Redis 创建快照之后,可以对快照进行备份,可以将快照复制到其他服务器从而创建具有相同数据的服务器副本(Redis 主从结构,主要用来提高 Redis 性能),还可以将快照留在原地以便重启服务器的时候使用。

快照持久化是 Redis 默认采用的持久化方式。

AOF持久化:

与快照持久化相比,AOF 持久化的实时性更好。

开启 AOF 持久化后每执行一条会更改 Redis 中的数据的命令,Redis 就会将该命令写入到 AOF 缓冲区 server.aof_buf 中,然后再写入到 AOF 文件中(此时还在系统内核缓存区未同步到磁盘),最后再根据持久化方式( fsync策略)的配置来决定何时将系统内核缓存区的数据同步到硬盘中的。

只有同步到磁盘中才算持久化保存了,否则依然存在数据丢失的风险,比如说:系统内核缓存区的数据还未同步,磁盘机器就宕机了,那这部分数据就算丢失了。

RDB与AOF的差异:

1.RDB 文件存储的内容是经过压缩的二进制数据, 保存着某个时间点的数据集,文件很小,适合做数据的备份,灾难恢复。

2.使用 RDB 文件恢复数据,直接解析还原数据即可,不需要一条一条地执行命令,速度非常快。而 AOF 则需要依次执行每个写命令,速度非常慢。也就是说,与 AOF 相比,恢复大数据集的时候,RDB 速度更快。

Redis是单线程的,为什么还能这么快?Redis6.0为什么引入了多线程

采用单线程还这么快的原因?

(1)对服务端程序来说,线程切换和锁通常是性能杀手,而单线程避免了线程切换和竞争所产生的消耗;

(2)Redis的大部分操作是在内存中完成的,这是它实现高性能的一个重要原因;

(3)Redis采用了IO多路复用机制,使其在网络IO操作中能并发处理大量的客户端请求,实现高吞吐率。I/O 多路复用技术的使用让 Redis 不需要额外创建多余的线程来监听客户端的大量连接,降低了资源的消耗

文件事件处理器(file event handler)主要是包含 4 个部分:

  • 多个 socket(客户端连接)
  • IO 多路复用程序(支持多个客户端连接的关键)
  • 文件事件分派器(将 socket 关联到相应的事件处理器)
  • 事件处理器(连接应答处理器、命令请求处理器、命令回复处理器)

引入多线程的原因

因为读写网络的read/write系统调用在Redis执行期间占用了大部分CPU时间,如果把网络读写做成多线程的方式对性能会有很大提升。Redis的多线程部分只是用来处理网络数据的读写和协议解析,执行命令仍然是单线程的。

总结

Redis选择使用单线程模型处理客户端的请求主要还是因为CPU不是Redis服务器的瓶颈,所以使用多线程模型带来的性能提并不能抵消它带来的开发和维护成本,系统性能瓶颈主要在网络I/O操作上;而Redis引入多线程也是出于性能上的考虑,对于一些大键值对的删除操作,通过多线程非阻塞地释放内存空间也能减少对Redis主线程阻塞的时间。

关于Redis的单线程架构实现,如下图:

 

 如何实现Redis 的高可用

(一)哨兵

Redis Sentinel(哨兵)是一个分布式架构,它包含若干个哨兵节点和数据节点。每个哨兵节点会对数据节点和其余的哨兵节点进行监控,当发现节点不可达时,会对节点做下线标识。如果被标识的是主节点,它就会与其他的哨兵节点进行协商,当多个哨兵节点都认为主节点不可达时,他们便会选举出一个哨兵来完成自动故障转移工作,同时还会将这个变化实时地通知给应用方。整个过程是自动的,不需要人工介入,有效地解决了Redis的高可用问题。

一组哨兵可以监控一个主节点,也可以同时监控多个主节点,两种情况的拓扑结构如图:

哨兵节点包含如下的特征:
• 哨兵节点会定期监控数据节点,其他哨兵节点是否可达;
• 哨兵节点会将故障转移的结果通知给应用方;
• 哨兵节点可以将从节点晋升为主节点,并维护后续正确的主从关系;
• 哨兵模式下,客户端连接的是哨兵节点集合,从中获取主节点信息;
• 节点的故障判断是由多个哨兵节点共同完成的,可有效地防止误判;
• 哨兵节点集合是由多个哨兵节点组成的,即使个别哨兵节点不可用,整个集合依然是健壮的;
• 哨兵节点也是独立的Redis节点,是特殊的Redis节点,它们不存储数据,只支持部分命令。

(二)集群

Redis集群采用虚拟槽分区来实现数据分片,它把所有的键根据哈希函数映射到0-16383整数槽内,计算公式为slot=CRC16(key)&16383,每一个节点负责维护一部分槽以及槽所映射的键值数据。虚拟槽分区具有如下特点:
• 解耦数据和节点之间的关系,简化了节点扩容和收缩的难度;
• 节点自身维护槽的映射关系,不需要客户端或者代理服务维护槽分区元数据;
• 支持节点、槽、键之间的映射查询,用于数据路由,在线伸缩等场景。
Redis集群中数据的分片逻辑如下图:

 

 Redis的主从同步是如何实现的?

从2.8版本开始,Redis使用psync命令完成主从数据同步,同步过程分为全量复制和部分复制。全量复制一般用于初次复制的场景,部分复制则用于处理因网络中断等原因造成数据丢失的场景。psync命令需要以下参数的支持:

(1)复制偏移量:主节点处理写命后,会把命令长度做累加记录,从节点在接收到写命令后,也会做累加记录;从节点每秒钟上报一次自身的复制偏移量给主节点,而主节点则会保存从节点的复制偏移量;

(2)积压缓存区:保存在主节点上的一个固定长度的队列,默认大小为1M,当主节点有连接的从节点时被创建;主节点处理写命令时,不但会把命令发送给从节点,还会写入积压缓冲区;缓冲区是先进先出的队列,可以保存最近已复制的数据,用于部分复制和命令丢失的数据补救。

(3)主节点运行ID:每个Redis节点启动后,都会动态分配一个40位的十六进制字符串作为运行ID;如果使用IP和端口的方式标识主节点,那么主节点重启变更了数据集(RDB/AOF),从节点再基于复制偏移量复制数据将是不安全的,因此当主节点的运行ID变化后,从节点将做全量复制。

Redis为什么存的快,内存断电数据怎么恢复?

Redis存的快是因为它的数据都存放在内存里,并且为了保证数据的安全性,Redis还提供了三种数据的持久化机制,即RDB机制、AOF持久化,RDB-AOF混合持久化。若服务器断电,那么我们可以利用持久化文件,对数据进行恢复。理论上来讲,持久化可以将丢失数据的窗口控制在1S之内。

Redis的缓存淘汰策略

LRU(Least Recently Used)是按照最近最少使用原则来筛选数据,即最不常用的数据会被筛选出来!
标准LRU:把所有的数据组成一个链表,表头和表尾分别表示MRU和LRU端,即最常使用端和最少使用端。刚被访问的数据会被移动到MRU端,而新增的数据也是刚被访问的数据,也会被移动到MRU端。当链表的空间被占满时,它会删除LRU端的数据。
 
近似LRU:Redis会记录每个数据的最近一次访问的时间戳(LRU)。Redis执行写入操作时,若发现内存超出maxmemory,就会执行一次近似LRU淘汰算法。近似LRU会随机采样N个key,然后淘汰掉最旧的key,若淘汰后内存依然超出限制,则继续采样淘汰。可以通过maxmemory_samples配置项,设置近似LRU每次采样的数据个数,该配置项的默认值为5。
 
LRU算法的不足之处在于,若一个key很少被访问,只是刚刚偶尔被访问了一次,则它就被认为是热点数据,短时间内不会被淘汰。
LFU算法正式用于解决上述问题,LFU(Least Frequently Used)是Redis4新增的淘汰策略,它根据key的最近访问频率进行淘汰。LFU在LRU的基础上,为每个数据增加了一个计数器,来统计这个数据的访问次数。当使用LFU策略淘汰数据时,首先会根据数据的访问次数进行筛选,把访问次数最低的数据淘汰出内存。如果两个数据的访问次数相同,LFU再比较这两个数据的访问时间,把访问时间更早的数据淘汰出内存。

请介绍以下Redis的过期策略

Redis支持如下两种过期策略:
①惰性删除:客户端访问一个key的时候,Redis会先检查它的过期时间,如果发现过期就立刻删除这个key。
②定期删除:Redis会将设置了过期时间的key放到一个独立的字典中,并对该字典进行每秒10次的过期扫描,
过期扫描不会遍历字典中所有的key,而是采用了一种简单的贪心策略。该策略的删除逻辑如下:
• 从过期字典中随机选择20个key;
• 删除这20个key中已过期的key;
• 如果已过期key的比例超过25%,则重复步骤1。

缓存穿透、缓存击穿、缓存雪崩有什么区别,该如何解决?

缓存穿透(客户端请求redis查询不到,内存请求过多):

客户端根本查询不同数据,使得数据请求直达存储层,导致其负载过大,甚至宕机。出现这种情况的原因,可能是业务层误将缓存和库中的数据删除了,也可能是有人恶意攻击库中不存在的数据。

解决方案:

(1)缓存空对象。存储层未命中后,仍然将空值存储缓存层,客户端再次访问数据时,缓存层会直接返回空值。

(2)布隆过滤器。将数据存入布隆过滤器,访问缓存之前以过滤器拦截,若请求的数据不存在则直接返回空值。

(3)最基本的就是首先做好参数校验,一些不合法的参数请求直接抛出异常信息返回给客户端。

缓存击穿(热数据缓存未存在Redis中,一份失效):

一份热点数据,它的访问量非常大。大量请求直达存储层,导致服务崩溃。

解决方案:

(1)永不过期。热点数据不设置过期时间,所以不会出现上述问题。或者为每个数据设置逻辑过期时间,当发现该数据逻辑过期时,使用单独的线程重建缓存。

(2)加互斥锁:对数据的访问加互斥锁,当一个数据访问该数据时,其他线程只能等待。这个线程访问后,缓存中的数据将被重建,届时其他线程就可以直接从缓存中获取值。

缓存雪崩(redis突然大面积失效,大量失效):

在某一时刻,缓存层无法继续提供服务,导致所有的请求直达存储层,造成数据库宕机。可能是缓存中有大量数据同时过期,也可能是Redis节点发生故障,导致大量请求无法得到处理。
解决方案:
(1)避免数据同时过期:设置过期时间时,附加一个随机数,避免大量的key同时过期。
(2)启用降级和熔断措施:在发生雪崩时,若应用访问的不是核心数据,则直接返回预定义信息/空值/错误信息。或者在发生雪崩时,对于访问缓存接口的请求,客户端并不会把请求发给Redis,而是直接返回。
(3)构建高可用的Redis服务:采用哨兵或集群模式,部署多个Redis实例,个别节点宕机,依然可以保持服务的整体可用。
 

如何利用redis实现分布式session

在web开发中,我们会把用户的登录信息存储在session里。而session是依赖于cookie的,即服务器创建session时会给它分配一个唯一的ID,并且在响应时创建一个cookie用于存储这个SESSIONID。当客户端收到这个cookie之后,就会自动保存这个SESSIONID,并且在下次访问时自动携带这个SESSIONID,届时服务器就可以通过这个SESSIONID得到与之对应的session,从而识别用户的身。如下图:

 

 

现在的互联网应用,基本都是采用分布式部署方式,即将应用程序部署在多台服务器上,并通过nginx做统一的请求分发。而服务器与服务器之间是隔离的,它们的session是不共享的,这就存在session同步的问题了,如下图:

 

 

如果客户端第一次访问服务器,请求被分发到了服务器A上,则服务器A会为该客户端创建session。如果客户端再次访问服务器,请求被分发到服务器B上,则由于服务器B中没有这个session,所以用户的身份无法得到验证,从而产生了不一致的问题。
 
解决这个问题的办法有很多,比如可以协调多个服务器,让他们的session保持同步。也可以在分发请求时做绑定处理,即将某一个IP固定分配给同一个服务器。但这些方式都比较麻烦,而且性能上也有一定的消耗。更合理的方式就是采用类似于Redis这样的高性能缓存服务器,来实现分布式session。
 
从上面的叙述可知,我们使用session保存用户的身份信息,本质上是要做两件事情。第一是保存用户的身份信息,第二是验证用户的身份信息。如果利用其它手段实现这两个目标,那么就可以不用session,或者说我们使用的是广义上的session了。
 
具体实现的思路如下图,我们在服务端增加两段程序:第一是创建令牌的程序,就是在用户初次访问服务器时,给它创建一个唯一的身份标识,并且使用cookie封装这个标识再发送给客户端。那么当客户端下次再访问服务器时,就会自动携带这个身份标识了,这和SESSIONID的道理是一样的,只是改由我们自己来实现了。另外,在返回令牌之前,我们需要将它存储起来,以便于后续的验证。而这个令牌是不能保存在服务器本地的,因为其他服务器无法访问它。因此,我们可以将其存储在服务器之外的一个地方,那么Redis便是一个理想的场所。
 
第二是验证令牌的程序,就是在用户再次访问服务器时,我们获取到了它之前的身份标识,那么我们就要验证一下这个标识是否存在了。验证的过程很简单,我们从Redis中尝试获取一下就可以知道结果。 

 

 说一说你对布隆过滤器的理解?

布隆过滤器可以用很低的代价,估算出数据是否真实存在。例如:给用户推荐新闻时,要去掉重复的新闻,就可以利用布隆过滤器,判断该新闻是否已经推荐过。
布隆过滤器的核心包括两部分:
• 一个大型的位数组;
• 若干个不一样的哈希函数,每个哈希函数都能将哈希值算的比较均匀。
布隆过滤器的工作原理:
添加key时,每个哈希函数都利用这个key计算出一个哈希值,再根据哈希值计算一个位置,并将位数组中这个位置的值设置为1。
询问key时,每个哈希函数都利用这个key计算出一个哈希值,再根据哈希值计算一个位置。然后对比这些哈希函数
在位数组中对应位置的数值:
• 如果这几个位置中,有一个位置的值是0,就说明这个布隆过滤器中,不存在这个key。
• 如果这几个位置中,所有位置的值都是1,就说明这个布隆过滤器中,极有可能存在这个key。之所以不是百分之百确定,是因为也可能是其他的key运算导致该位置为1。

 

posted @   白非立  阅读(63)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· AI 智能体引爆开源社区「GitHub 热点速览」
· 从HTTP原因短语缺失研究HTTP/2和HTTP/3的设计差异
· 三行代码完成国际化适配,妙~啊~
点击右上角即可分享
微信分享提示