【面试题】Java 高级工程师面试刷题100题(四)

文章目录

双写一致性问题如何解决?

先做一个说明,从理论上来说,给缓存设置过期时间,是保证最终一致性的解决方案。这种方案下,我们可以对存入缓存的数据设置过期时间,所有的写操作以数据库为准,对缓存操作只是尽最大努力更新即可。也就是说如果数据库写成功,缓存更新失败,那么只要到达过期时间,则后面的读请求自然会从数据库中读取新值然后回填缓存。因此,接下来讨论的思路不依赖于给缓存设置过期时间这个方案。
在这里,我们讨论三种更新策略:

  1. 先更新缓存,再更新数据库。(不可取)
  2. 先更新数据库,再更新缓存。(不可取)
  3. 先删除缓存,再更新数据库。(不可取)
  4. 先更新数据库,再删除缓存。(可取,有问题待解决)

大前提:

先读缓存,如果缓存没有,才从数据库读取。

(1)先更新数据库,再更新缓存

这套方案,大家是普遍反对的。为什么呢?有如下两点原因。
原因一(线程安全角度)
同时有请求 A 和请求 B 进行更新操作,那么会出现
(1)线程 A 更新了数据库
(2)线程 B 更新了数据库
(3)线程 B 更新了缓存
(4)线程 A 更新了缓存
这就出现请求 A 更新缓存应该比请求 B 更新缓存早才对,但是因为网络等原因,B 却比 A 更早更新了缓存。这就导致了脏数据,因此不考虑。
原因二(业务场景角度)
有如下两点:
(1)如果你是一个写数据库场景比较多,而读数据场景比较少的业务需求,采用这种方案就会导致,数据压根还没读到,缓存就被频繁的更新,浪费性能。
(2)如果你写入数据库的值,并不是直接写入缓存的,而是要经过一系列复杂的计算再写入缓存。那么,每次写入数据库后,都再次计算写入缓存的值,无疑是浪费性能的。显然,删除缓存更为适合。

接下来讨论的就是争议最大的,先删缓存,再更新数据库。还是先更新数据库,再删缓存的问题。

(2)先删缓存,再更新数据库

该方案会导致不一致的原因是。同时有一个请求 A 进行更新操作,另一个请求 B 进行查询操作。那么会出现如下情形:
(1)请求 A 进行写操作,删除缓存
(2)请求 B 查询发现缓存不存在
(3)请求 B 去数据库查询得到旧值
(4)请求 B 将旧值写入缓存
(5)请求 A 将新值写入数据库
上述情况就会导致不一致的情形出现。而且,如果不采用给缓存设置过期时间策略,该数据永远都是脏数据。
那么,如何解决呢?采用延时双删策略

(1)先淘汰缓存
(2)再写数据库(这两步和原来一样)
(3)休眠 1 秒,再次淘汰缓存
这么做,可以将 1 秒内所造成的缓存脏数据,再次删除。
那么,这个 1 秒怎么确定的,具体该休眠多久呢?
针对上面的情形,读者应该自行评估自己的项目的读数据业务逻辑的耗时。然后写数据的休眠时间则在读数据业务逻辑的耗时基础上,加几百 ms 即可。这么做的目的,就是确保读请求结束,写请求可以删除读请求造成的缓存脏数据。
如果你用了 mysql 的读写分离架构怎么办?
ok,在这种情况下,造成数据不一致的原因如下,还是两个请求,一个请求 A 进行更新操作,另一个请求 B 进行查询操作。
(1)请求 A 进行写操作,删除缓存
(2)请求 A 将数据写入数据库了,
(3)请求 B 查询缓存发现,缓存没有值
(4)请求 B 去从库查询,这时,还没有完成主从同步,因此查询到的是旧值
(5)请求 B 将旧值写入缓存
(6)数据库完成主从同步,从库变为新值
上述情形,就是数据不一致的原因。还是使用双删延时策略。只是,睡眠时间修改为在主从同步的延时时间基础上,加几百 ms。
采用这种同步淘汰策略,吞吐量降低怎么办?
ok,那就将第二次删除作为异步的。自己起一个线程,异步删除。这样,写的请求就不用沉睡一段时间后了,再返回。这么做,加大吞吐量。
第二次删除,如果删除失败怎么办?
这是个非常好的问题,因为第二次删除失败,就会出现如下情形。还是有两个请求,一个请求 A 进行更新操作,另一个请求 B 进行查询操作,为了方便,假设是单库:
(1)请求 A 进行写操作,删除缓存
(2)请求 B 查询发现缓存不存在
(3)请求 B 去数据库查询得到旧值
(4)请求 B 将旧值写入缓存
(5)请求 A 将新值写入数据库
(6)请求 A 试图去删除,请求 B 写入对的缓存值,结果失败了。
ok,这也就是说。如果第二次删除缓存失败,会再次出现缓存和数据库不一致的问题。
如何解决呢?

(3)先更新数据库,再删缓存

首先,先说一下。老外提出了一个缓存更新套路,名为《Cache-Aside pattern》。其中就指出

  • 失效:应用程序先从 cache 取数据,没有得到,则从数据库中取数据,成功后,放到缓存中。
  • 命中:应用程序从 cache 中取数据,取到后返回。
  • 更新:先把数据存到数据库中,成功后,再让缓存失效。

另外,知名社交网站 facebook 也在论文《Scaling Memcache at Facebook》中提出,他们用的也是先更新数据库,再删缓存的策略。
这种情况不存在并发问题么?
不是的。假设这会有两个请求,一个请求 A 做查询操作,一个请求 B 做更新操作,那么会有如下情形产生
(1)缓存刚好失效
(2)请求 A 查询数据库,得一个旧值
(3)请求 B 将新值写入数据库
(4)请求 B 删除缓存
(5)请求 A 将查到的旧值写入缓存
ok,如果发生上述情况,确实是会发生脏数据。
然而,发生这种情况的概率又有多少呢?
发生上述情况有一个先天性条件,就是步骤(3)的写数据库操作比步骤(2)的读数据库操作耗时更短,才有可能使得步骤(4)先于步骤(5)。可是,大家想想,数据库的读操作的速度远快于写操作的(不然做读写分离干嘛,做读写分离的意义就是因为读操作比较快,耗资源少),因此步骤(3)耗时比步骤(2)更短,这一情形很难出现。
假设,有人非要抬杠,有强迫症,一定要解决怎么办?
如何解决上述并发问题?
首先,给缓存设有效时间是一种方案。其次,采用策略(2)里给出的异步延时删除策略,保证读请求完成以后,再进行删除操作。
还有其他造成不一致的原因么?
有的,这也是缓存更新策略(2)和缓存更新策略(3)都存在的一个问题,如果删缓存失败了怎么办,那不是会有不一致的情况出现么。比如一个写数据请求,然后写入数据库了,删缓存失败了,这会就出现不一致的情况了。这也是缓存更新策略(2)里留下的最后一个疑问。
如何解决?
提供一个保障的重试机制即可,这里给出两套方案。
方案一
如下图所示
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-l0eISlpI-1645802808797)(images/o_update1.png)]
流程如下所示
(1)更新数据库数据;
(2)缓存因为种种问题删除失败
(3)将需要删除的 key 发送至消息队列
(4)自己消费消息,获得需要删除的 key
(5)继续重试删除操作,直到成功
然而,该方案有一个缺点,对业务线代码造成大量的侵入。于是有了方案二,在方案二中,启动一个订阅程序去订阅数据库的 binlog,获得需要操作的数据。在应用程序中,另起一段程序,获得这个订阅程序传来的信息,进行删除缓存操作。
方案二
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RoW2ynBc-1645802808799)(images/o_update2.png)]
流程如下图所示:
(1)更新数据库数据
(2)数据库会将操作信息写入 binlog 日志当中
(3)订阅程序提取出所需要的数据以及 key
(4)另起一段非业务代码,获得该信息
(5)尝试删除缓存操作,发现删除失败
(6)将这些信息发送至消息队列
(7)重新从消息队列中获得该数据,重试操作。

**备注说明:**上述的订阅 binlog 程序在 mysql 中有现成的中间件叫 canal,可以完成订阅 binlog 日志的功能。至于 oracle 中,博主目前不知道有没有现成中间件可以使用。另外,重试机制,博主是采用的是消息队列的方式。如果对一致性要求不是很高,直接在程序中另起一个线程,每隔一段时间去重试即可,这些大家可以灵活自由发挥,只是提供一个思路。

分布式微服务项目你是如何设计的?

我一般设计成两层:业务层和能力层(中台),业务层接受用户请求,然后通过调用能力层来完成业务逻辑。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-j9jwZTJn-1645802808800)(images/image-20210522172654370.png)]

认证 (Authentication) 和授权 (Authorization)的区别是什么?

Authentication(认证) 是验证您的身份的凭据(例如用户名/用户 ID 和密码),通过这个凭据,系统得以知道你就是你,也就是说系统存在你这个用户。所以,Authentication 被称为身份/用户验证。
Authorization(授权) 发生在 Authentication(认证) 之后。授权,它主要掌管我们访问系统的权限。比如有些特定资源只能具有特定权限的人才能访问比如 admin,有些对系统资源操作比如删除、添加、更新只能特定人才具有。
这两个一般在我们的系统中被结合在一起使用,目的就是为了保护我们系统的安全性。

Cookie 和 Session 有什么区别?如何使用 Session 进行身份验证?

Session 的主要作用就是通过服务端记录用户的状态。 典型的场景是购物车,当你要添加商品到购物车的时候,系统不知道是哪个用户操作的,因为 HTTP 协议是无状态的。服务端给特定的用户创建特定的 Session 之后就可以标识这个用户并且跟踪这个用户了。

Cookie 数据保存在客户端(浏览器端),Session 数据保存在服务器端。相对来说 Session 安全性更高。如果使用 Cookie 的一些敏感信息不要写入 Cookie 中,最好能将 Cookie 信息加密然后使用到的时候再去服务器端解密。

那么,如何使用 Session 进行身份验证?

很多时候我们都是通过 SessionID 来实现特定的用户,SessionID 一般会选择存放在 Redis 中。举个例子:用户成功登陆系统,然后返回给客户端具有 SessionID 的 Cookie,当用户向后端发起请求的时候会把 SessionID 带上,这样后端就知道你的身份状态了。关于这种认证方式更详细的过程如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-eNy6UABN-1645802808800)(images/image-20210520130119426.png)]

用户向服务器发送用户名和密码用于登陆系统。
服务器验证通过后,服务器为用户创建一个 Session,并将 Session 信息存储 起来。
服务器向用户返回一个 SessionID,写入用户的 Cookie。
当用户保持登录状态时,Cookie 将与每个后续请求一起被发送出去。
服务器可以将存储在 Cookie 上的 Session ID 与存储在内存中或者数据库中的 Session 信息进行比较,以验证用户的身份,返回给用户客户端响应信息的时候会附带用户当前的状态。
使用 Session 的时候需要注意下面几个点:

依赖 Session 的关键业务一定要确保客户端开启了 Cookie。
注意 Session 的过期时间

为什么 Cookie 无法防止 CSRF 攻击,而 token 可以?

**CSRF(Cross Site Request Forgery)**一般被翻译为 跨站请求伪造 。那么什么是 跨站请求伪造 呢?说简单用你的身份去发送一些对你不友好的请求。举个简单的例子:

小壮登录了某网上银行,他来到了网上银行的帖子区,看到一个帖子下面有一个链接写着“科学理财,年盈利率过万”,小壮好奇的点开了这个链接,结果发现自己的账户少了 10000 元。这是这么回事呢?原来黑客在链接中藏了一个请求,这个请求直接利用小壮的身份给银行发送了一个转账请求,也就是通过你的 Cookie 向银行发出请求。

<a src=http://www.mybank.com/Transfer?bankId=11&money=10000>科学理财,年盈利率过万</>
进行 Session 认证的时候,我们一般使用 Cookie 来存储 SessionId,当我们登陆后后端生成一个 SessionId 放在 Cookie 中返回给客户端,服务端通过 Redis 或者其他存储工具记录保存着这个 Sessionid,客户端登录以后每次请求都会带上这个 SessionId,服务端通过这个 SessionId 来标示你这个人。如果别人通过 cookie 拿到了 SessionId 后就可以代替你的身份访问系统了。

Session 认证中 Cookie 中的 SessionId 是由浏览器发送到服务端的,借助这个特性,攻击者就可以通过让用户误点攻击链接,达到攻击效果。

但是,我们使用 token 的话就不会存在这个问题,在我们登录成功获得 token 之后,一般会选择存放在 local storage 中。然后我们在前端通过某些方式会给每个发到后端的请求加上这个 token,这样就不会出现 CSRF 漏洞的问题。因为,即使有个你点击了非法链接发送了请求到服务端,这个非法请求是不会携带 token 的,所以这个请求将是非法的。

什么是 Token?什么是 JWT?如何基于 Token 进行身份验证?

我们知道 Session 信息需要保存一份在服务器端。这种方式会带来一些麻烦,比如需要我们保证保存 Session 信息服务器的可用性、不适合移动端(依赖 Cookie)等等。

有没有一种不需要自己存放 Session 信息就能实现身份验证的方式呢?使用 Token 即可!JWT (JSON Web Token) 就是这种方式的实现,通过这种方式服务器端就不需要保存 Session 数据了,只用在客户端保存服务端返回给客户的 Token 就可以了,扩展性得到提升。

JWT 本质上就一段签名的 JSON 格式的数据。由于它是带有签名的,因此接收者便可以验证它的真实性。

下面是 RFC 7519 对 JWT 做的较为正式的定义。

JSON Web Token (JWT) is a compact, URL-safe means of representing claims to be transferred between two parties. The claims in a JWT are encoded as a JSON object that is used as the payload of a JSON Web Signature (JWS) structure or as the plaintext of a JSON Web Encryption (JWE) structure, enabling the claims to be digitally signed or integrity protected with a Message Authentication Code (MAC) and/or encrypted. ——JSON Web Token (JWT)

JWT 由 3 部分构成:

Header :描述 JWT 的元数据。定义了生成签名的算法以及 Token 的类型。
Payload(负载):用来存放实际需要传递的数据
Signature(签名):服务器通过 Payload、Header 和一个密钥(secret)使用 Header 里面指定的签名算法(默认是 HMAC SHA256)生成。
在基于 Token 进行身份验证的的应用程序中,服务器通过 Payload、Header 和一个密钥(secret)创建令牌(Token)并将 Token 发送给客户端,客户端将 Token 保存在 Cookie 或者 localStorage 里面,以后客户端发出的所有请求都会携带这个令牌。你可以把它放在 Cookie 里面自动发送,但是这样不能跨域,所以更好的做法是放在 HTTP Header 的 Authorization 字段中:Authorization: Bearer Token。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YmjdRQm6-1645802808801)(images/image-20210520130410868.png)]

用户向服务器发送用户名和密码用于登陆系统。
身份验证服务响应并返回了签名的 JWT,上面包含了用户是谁的内容。
用户以后每次向后端发请求都在 Header 中带上 JWT。
服务端检查 JWT 并从中获取用户相关信息。

分布式架构下,Session 共享有什么方案?

  1. 不要有 session:但是确实在某些场景下,是可以没有 session 的,其实在很多接口类系统当中,都提倡【API 无状态服务】;也就是每一次的接口访问,都不依赖于 session、不依赖于前一次的接口访问;
  2. 存入 cookie 中:将 session 存储到 cookie 中,但是缺点也很明显,例如每次请求都得带着 session,数据存储在客户端本地,是有风险的;
  3. session 同步:对个服务器之间同步 session,这样可以保证每个服务器上都有全部的 session 信息,不过当服务器数量比较多的时候,同步是会有延迟甚至同步失败;
  4. 使用 Nginx(或其他复杂均衡软硬件)中的 ip 绑定策略,同一个 ip 只能在指定的同一个机器访问,但是这样做风险也比较大,而且也是去了负载均衡的意义;
  5. 我们现在的系统会把 session 放到 Redis 中存储,虽然架构上变得复杂,并且需要多访问一次 Redis,但是这种方案带来的好处也是很大的:实现 session 共享,可以水平扩展(增加 Redis 服务器),服务器重启 session 不丢失(不过也要注意 session 在 Redis 中的刷新/失效机制),不仅可以跨服务器 session 共享,甚至可以跨平台(例如网页端和 APP 端)。

springcloud 核心组件有哪些?

服务注册与发现——Netflix Eureka、Nacos、Zookeeper

客户端负载均衡——Netflix Ribbon、SpringCloud LoadBalancer

服务熔断器——Netflix Hystrix、Alibaba Sentinel、Resilience4J

服务网关——Netflix Zuul、SpringCloud Gateway

服务接口调用——Netflix Feign、 Resttemplate、Openfeign

链路追踪——Netflix Sleuth、Skywalking、Pinpoint

聚合 Hystrix 监控数据——Netflix Turbine

监控中心---- SpringBoot Admin

配置中心——Spring Cloud Config 、Apollo、nacos

微服务架构原理是什么?

主要是面向 SOA 理念,更细小粒度服务的拆分,将功能分解到各个服务当中,从而降低系统的耦合性,并提供更加灵活的服务支持。

注册中心的原理是什么?

服务启动后向 Eureka 注册,Eureka Server 会将注册信息向其他 Eureka Server 进行同步,当服务消费者要调用服务提供者,则向服务注册中心获取服务提供者地址,然后会将服务提供者地址缓存在本地,下次再调用时,则直接从本地缓存中取,完成一次调用

配置中心的原理是什么?

在服务运行之前,将所需的配置信息从配置仓库拉取到本地服务,达到统一化配置管理的目的

配置中心是如何实现自动刷新的?

  1. 配置中心 Server 端承担起配置刷新的职责

  2. 提交配置触发 post 请求给 server 端的 bus/refresh 接口

  3. server 端接收到请求并发送给 Spring Cloud Bus 总线

  4. Spring Cloud bus 接到消息并通知给其它连接到总线的客户端

  5. 其它客户端接收到通知,请求 Server 端获取最新配置

  6. 全部客户端均获取到最新的配置

配置中心是如何保证数据安全的?

1.保证容器文件访问的安全性,即保证所有的网络资源请求都需要登录

2.将配置中心里所有配置文件中的密码进行加密,保证其密文性

3.开发环境禁止拉取生产环境的配置文件

用 zookeeper 和 eureka 做注册中心有什么区别?

Zookeeper 保证的是 CP(一致性,容错性), 而 Eureka 则是 AP(可用性,容错性)。

Spring Cloud 和 Dubbo 有哪些区别?

  1. dubbo 是二进制传输,对象直接转成二进制,使用 RPC 通信。

SpringCloud 是 http 传输,同时使用 http 协议一般会使用 JSON 报文,json 再转二进制,消耗会更大。

  1. Dubbo 只是实现了服务治理,而 Spring Cloud 下面有几十个子项目分别覆盖了微服务架构下的方方面面,服务治理只是其中的一个方面,一定程度来说,Dubbo 只是 Spring Cloud Netflix 中的一个子集。

Ribbon 负载均衡原理是什么?

  1. Ribbon 通过 ILoadBalancer 接口对外提供统一的选择服务器(Server)的功能,此接口会根据不同的负载均衡策略(IRule)选择合适的 Server 返回给使用者。

  2. IRule 是负载均衡策略的抽象,ILoadBalancer 通过调用 IRule 的 choose()方法返回 Server

  3. IPing 用来检测 Server 是否可用,ILoadBalancer 的实现类维护一个 Timer 每隔 10s 检测一次 Server 的可用状态

  4. IClientConfig 主要定义了用于初始化各种客户端和负载均衡器的配置信息,器实现类为 DefaultClientConfigImpl

微服务熔断降级机制是什么?

微服务框架是许多服务互相调用的,要是不做任何保护的话,某一个服务挂了,就会引起连锁反应,导致别的服务也挂。Hystrix 是隔离、熔断以及降级的一个框架。如果调用某服务报错(或者挂了),就对该服务熔断,在 5 分钟内请求此服务直接就返回一个默认值,不需要每次都卡几秒,这个过程,就是所谓的熔断。但是熔断了之后就会少调用一个服务,此时需要做下标记,标记本来需要做什么业务,但是因为服务挂了,暂时没有做,等该服务恢复了,就可以手工处理这些业务。这个过程,就是所谓的降级。

什么是 Hystrix?实现原理是什么?

Hystrix 是一个延迟和容错库,旨在隔离对远程系统、服务和第三方库的访问点,停止级联故障,并在 不可避免发生故障的复杂分布式系统中实现快速恢复。主要靠 Spring 的 AOP 实现

实现原理

正常情况下,断路器关闭,服务消费者正常请求微服务

一段事件内,失败率达到一定阈值,断路器将断开,此时不再请求服务提供者,而是只是快速失败的方法(断路方法)

断路器打开一段时间,自动进入“半开”状态,此时,断路器可允许一个请求方法服务提供者,如果请求调用成功,则关闭断路器,否则继续保持断路器打开状态。

断路器 hystrix 是保证了局部发生的错误,不会扩展到整个系统,从而保证系统的即使出现局部问题也不会造成系统雪崩

注册中心挂了,或者服务挂了,应该如何处理?

注册中心挂了,可以读取本地持久化里的配置

服务挂了 应该配有服务监控中心 感知到服务下线后可以通过配置的邮件通知相关人员排查问题。

说说你对 RPC、RMI 如何理解?

RPC 远程过程调用协议,通过网络从远程计算机上请求调用某种服务。

RMI:远程方法调用 能够让在客户端 Java 虚拟机上的对象像调用本地对象一样调用服务端 java 虚拟机中的对象上的方法。

redis 持久化机制:RDB 和 AOF

Redis 持久化

Redis 提供了不同级别的持久化方式:

  • RDB 持久化方式能够在指定的时间间隔能对你的数据进行快照存储.

  • AOF 持久化方式记录每次对服务器写的操作,当服务器重启的时候会重新执行这些命令来恢复原始的数据,AOF 命令以 redis 协议追加保存每次写的操作到文件末尾.Redis 还能对 AOF 文件进行后台重写,使得 AOF 文件的体积不至于过大.

  • 如果你只希望你的数据在服务器运行的时候存在,你也可以不使用任何持久化方式.

  • 你也可以同时开启两种持久化方式, 在这种情况下, 当 redis 重启的时候会优先载入 AOF 文件来恢复原始的数据,因为在通常情况下 AOF 文件保存的数据集要比 RDB 文件保存的数据集要完整.

  • 最重要的事情是了解 RDB 和 AOF 持久化方式的不同,让我们以 RDB 持久化方式开始:

    RDB 的优点

  • RDB 是一个非常紧凑的文件,它保存了某个时间点得数据集,非常适用于数据集的备份,比如你可以在每个小时报保存一下过去 24 小时内的数据,同时每天保存过去 30 天的数据,这样即使出了问题你也可以根据需求恢复到不同版本的数据集.

  • RDB 是一个紧凑的单一文件,很方便传送到另一个远端数据中心或者亚马逊的 S3(可能加密),非常适用于灾难恢复.

  • RDB 在保存 RDB 文件时父进程唯一需要做的就是 fork 出一个子进程,接下来的工作全部由子进程来做,父进程不需要再做其他 IO 操作,所以 RDB 持久化方式可以最大化 redis 的性能.

  • 与 AOF 相比,在恢复大的数据集的时候,RDB 方式会更快一些.

    RDB 的缺点

  • 如果你希望在 redis 意外停止工作(例如电源中断)的情况下丢失的数据最少的话,那么 RDB 不适合你.虽然你可以配置不同的 save 时间点(例如每隔 5 分钟并且对数据集有 100 个写的操作),是 Redis 要完整的保存整个数据集是一个比较繁重的工作,你通常会每隔 5 分钟或者更久做一次完整的保存,万一在 Redis 意外宕机,你可能会丢失几分钟的数据.

  • RDB 需要经常 fork 子进程来保存数据集到硬盘上,当数据集比较大的时候,fork 的过程是非常耗时的,可能会导致 Redis 在一些毫秒级内不能响应客户端的请求.如果数据集巨大并且 CPU 性能不是很好的情况下,这种情况会持续 1 秒,AOF 也需要 fork,但是你可以调节重写日志文件的频率来提高数据集的耐久度.

    AOF 优点

  • 使用 AOF 会让你的 Redis 更加耐久: 你可以使用不同的 fsync 策略:无 fsync,每秒 fsync,每次写的时候 fsync.使用默认的每秒 fsync 策略,Redis 的性能依然很好(fsync 是由后台线程进行处理的,主线程会尽力处理客户端请求),一旦出现故障,你最多丢失 1 秒的数据.

  • AOF 文件是一个只进行追加的日志文件,所以不需要写入 seek,即使由于某些原因(磁盘空间已满,写的过程中宕机等等)未执行完整的写入命令,你也也可使用 redis-check-aof 工具修复这些问题.

  • Redis 可以在 AOF 文件体积变得过大时,自动地在后台对 AOF 进行重写: 重写后的新 AOF 文件包含了恢复当前数据集所需的最小命令集合。 整个重写操作是绝对安全的,因为 Redis 在创建新 AOF 文件的过程中,会继续将命令追加到现有的 AOF 文件里面,即使重写过程中发生停机,现有的 AOF 文件也不会丢失。 而一旦新 AOF 文件创建完毕,Redis 就会从旧 AOF 文件切换到新 AOF 文件,并开始对新 AOF 文件进行追加操作。

  • AOF 文件有序地保存了对数据库执行的所有写入操作, 这些写入操作以 Redis 协议的格式保存, 因此 AOF 文件的内容非常容易被人读懂, 对文件进行分析(parse)也很轻松。 导出(export) AOF 文件也非常简单: 举个例子, 如果你不小心执行了 FLUSHALL 命令, 但只要 AOF 文件未被重写, 那么只要停止服务器, 移除 AOF 文件末尾的 FLUSHALL 命令, 并重启 Redis , 就可以将数据集恢复到 FLUSHALL 执行之前的状态。

    AOF 缺点

  • 对于相同的数据集来说,AOF 文件的体积通常要大于 RDB 文件的体积。

  • 根据所使用的 fsync 策略,AOF 的速度可能会慢于 RDB 。 在一般情况下, 每秒 fsync 的性能依然非常高, 而关闭 fsync 可以让 AOF 的速度和 RDB 一样快, 即使在高负荷之下也是如此。 不过在处理巨大的写入载入时,RDB 可以提供更有保证的最大延迟时间(latency)。

    4.X 版本的整合策略

    在 AOF 重写策略上做了优化

    在重写 AOF 文件时,4.x 版本以前是把内存数据集的操作指令落地,而新版本是把内存的数据集以 rdb 的形式落地

    这样重写后的 AOF 依然追加的是日志,但是,在恢复的时候是先 rdb 再增量的日志,性能更优秀

扩展知识

异步线程知识点

计算机组成原理

fork

copy on write

系统 IO

pagecache

fsync


redis 的过期键有哪些删除策略

过期精度

在 Redis 2.4 及以前版本,过期期时间可能不是十分准确,有 0-1 秒的误差。

从 Redis 2.6 起,过期时间误差缩小到 0-1 毫秒。

过期和持久

Keys 的过期时间使用 Unix 时间戳存储(从 Redis 2.6 开始以毫秒为单位)。这意味着即使 Redis 实例不可用,时间也是一直在流逝的。

要想过期的工作处理好,计算机必须采用稳定的时间。 如果你将 RDB 文件在两台时钟不同步的电脑间同步,有趣的事会发生(所有的 keys 装载时就会过期)。

即使正在运行的实例也会检查计算机的时钟,例如如果你设置了一个 key 的有效期是 1000 秒,然后设置你的计算机时间为未来 2000 秒,这时 key 会立即失效,而不是等 1000 秒之后。

Redis 如何淘汰过期的 keys

Redis keys 过期有两种方式:被动和主动方式。

当一些客户端尝试访问它时,key 会被发现并主动的过期。

当然,这样是不够的,因为有些过期的 keys,永远不会访问他们。 无论如何,这些 keys 应该过期,所以定时随机测试设置 keys 的过期时间。所有这些过期的 keys 将会从密钥空间删除。

具体就是 Redis 每秒 10 次做的事情:

  1. 测试随机的 20 个 keys 进行相关过期检测。
  2. 删除所有已经过期的 keys。
  3. 如果有多于 25%的 keys 过期,重复步奏 1.

这是一个平凡的概率算法,基本上的假设是,我们的样本是这个密钥控件,并且我们不断重复过期检测,直到过期的 keys 的百分百低于 25%,这意味着,在任何给定的时刻,最多会清除 1/4 的过期 keys。

在复制 AOF 文件时如何处理过期

为了获得正确的行为而不牺牲一致性,当一个 key 过期,DEL将会随着 AOF 文字一起合成到所有附加的 slaves。在 master 实例中,这种方法是集中的,并且不存在一致性错误的机会。

然而,当 slaves 连接到 master 时,不会独立过期 keys(会等到 master 执行 DEL 命令),他们任然会在数据集里面存在,所以当 slave 当选为 master 时淘汰 keys 会独立执行,然后成为 master。

扩展

绝对时间点过期

相对时间点过期

时钟轮算法


redis 线程模型有哪些,单线程为什么快

IO 模型维度的特征

IO 模型使用了多路复用器,在 linux 系统中使用的是 EPOLL

类似 netty 的 BOSS,WORKER 使用一个 EventLoopGroup(threads=1)

单线程的 Reactor 模型,每次循环取 socket 中的命令然后逐一操作,可以保证 socket 中的指令是按顺序的,不保证不同的 socket 也就是客户端的命令的顺序性

命令操作在单线程中顺序操作,没有多线程的困扰不需要锁的复杂度,在操作数据上相对来说是原子性质的

架构设计模型

自身的内存存储数据,读写操作不设计磁盘 IO

redis 除了提供了 Value 具备类型还为每种类型实现了一些操作命令

实现了计算向数据移动,而非数据想计算移动,这样在 IO 的成本上有一定的优势

且在数据结构类型上,丰富了一些统计类属性,读写操作中,写操作会 O(1)负载度更新 length 类属性,使得读操作也是 O(1)的


缓存雪崩、缓存穿透、缓存击穿在实际中如何处理

缓存穿透

缓存穿透是指查询一个一定不存在的数据,由于缓存是不命中时被动写的,并且出于容错考虑,如果从存储层查不到数据则不写入缓存,这将导致这个不存在的数据每次请求都要到存储层去查询,失去了缓存的意义。在流量大时,可能 DB 就挂掉了,要是有人利用不存在的 key 频繁攻击我们的应用,这就是漏洞。

解决方案

有很多种方法可以有效地解决缓存穿透问题,最常见的则是采用布隆过滤器,将所有可能存在的数据哈希到一个足够大的 bitmap 中,一个一定不存在的数据会被 这个 bitmap 拦截掉,从而避免了对底层存储系统的查询压力。另外也有一个更为简单粗暴的方法(我们采用的就是这种),如果一个查询返回的数据为空(不管是数 据不存在,还是系统故障),我们仍然把这个空结果进行缓存,但它的过期时间会很短,最长不超过五分钟。

缓存击穿

对于一些设置了过期时间的 key,如果这些 key 可能会在某些时间点被超高并发地访问,是一种非常“热点”的数据。这个时候,需要考虑一个问题:缓存被“击穿”的问题,这个和缓存雪崩的区别在于这里针对某一 key 缓存,前者则是很多 key。

缓存在某个时间点过期的时候,恰好在这个时间点对这个 Key 有大量的并发请求过来,这些请求发现缓存过期一般都会从后端 DB 加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把后端 DB 压垮。

解决方案

缓存失效时的雪崩效应对底层系统的冲击非常可怕。大多数系统设计者考虑用加锁或者队列的方式保证缓存的单线 程(进程)写,从而避免失效时大量的并发请求落到底层存储系统上。这里分享一个简单方案就时讲缓存失效时间分散开,比如我们可以在原有的失效时间基础上增加一个随机值,比如 1-5 分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件。

缓存雪崩

缓存雪崩是指在我们设置缓存时采用了相同的过期时间,导致缓存在某一时刻同时失效,请求全部转发到 DB,DB 瞬时压力过重雪崩。

解决方案

1.使用互斥锁(mutex key)
业界比较常用的做法,是使用 mutex。简单地来说,就是在缓存失效的时候(判断拿出来的值为空),不是立即去 load db,而是先使用缓存工具的某些带成功操作返回值的操作(比如 Redis 的 SETNX 或者 Memcache 的 ADD)去 set 一个 mutex key,当操作返回成功时,再进行 load db 的操作并回设缓存;否则,就重试整个 get 缓存的方法。
SETNX,是「SET if Not eXists」的缩写,也就是只有不存在的时候才设置,可以利用它来实现锁的效果。在 redis2.6.1 之前版本未实现 setnx 的过期时间

2."提前"使用互斥锁(mutex key):
在 value 内部设置 1 个超时值(timeout1), timeout1 比实际的 memcache timeout(timeout2)小。当从 cache 读取到 timeout1 发现它已经过期时候,马上延长 timeout1 并重新设置到 cache。然后再从数据库加载数据并设置到 cache 中。

3.“永远不过期”:
这里的“永远不过期”包含两层意思:

(1) 从 redis 上看,确实没有设置过期时间,这就保证了,不会出现热点 key 过期问题,也就是“物理”不过期。

(2) 从功能上看,如果不过期,那不就成静态的了吗?所以我们把过期时间存在 key 对应的 value 里,如果发现要过期了,通过一个后台的异步线程进行缓存的构建,也就是“逻辑”过期

从实战看,这种方法对于性能非常友好,唯一不足的就是构建缓存时候,其余线程(非构建缓存的线程)可能访问的是老数据,但是对于一般的互联网功能来说这个还是可以忍受。

总结

穿透:缓存不存在,数据库不存在,高并发,少量 key

击穿:缓存不存在,数据库存在,高并发,少量 key

雪崩:缓存不存在,数据库存在,高并发,大量 key

语义有些许差异,但是,都可以使用限流的互斥锁,保障数据库的稳定


redis 事务是怎么实现的

MULTI 、 EXEC 、 DISCARD 和 WATCH 是 Redis 事务相关的命令。事务可以一次执行多个命令, 并且带有以下两个重要的保证:

事务是一个单独的隔离操作:事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。

事务是一个原子操作:事务中的命令要么全部被执行,要么全部都不执行。

EXEC 命令负责触发并执行事务中的所有命令:

如果客户端在使用 MULTI 开启了一个事务之后,却因为断线而没有成功执行 EXEC ,那么事务中的所有命令都不会被执行。
另一方面,如果客户端成功在开启事务之后执行 EXEC ,那么事务中的所有命令都会被执行。

当使用 AOF 方式做持久化的时候, Redis 会使用单个 write(2) 命令将事务写入到磁盘中。

然而,如果 Redis 服务器因为某些原因被管理员杀死,或者遇上某种硬件故障,那么可能只有部分事务命令会被成功写入到磁盘中。

如果 Redis 在重新启动时发现 AOF 文件出了这样的问题,那么它会退出,并汇报一个错误。

使用 redis-check-aof 程序可以修复这一问题:它会移除 AOF 文件中不完整事务的信息,确保服务器可以顺利启动。

从 2.2 版本开始,Redis 还可以通过乐观锁(optimistic lock)实现 CAS (check-and-set)操作,具体信息请参考文档的后半部分。

事务中的错误

使用事务时可能会遇上以下两种错误:

事务在执行 EXEC 之前,入队的命令可能会出错。比如说,命令可能会产生语法错误(参数数量错误,参数名错误,等等),或者其他更严重的错误,比如内存不足(如果服务器使用 maxmemory 设置了最大内存限制的话)。
命令可能在 EXEC 调用之后失败。举个例子,事务中的命令可能处理了错误类型的键,比如将列表命令用在了字符串键上面,诸如此类。

对于发生在 EXEC 执行之前的错误,客户端以前的做法是检查命令入队所得的返回值:如果命令入队时返回 QUEUED ,那么入队成功;否则,就是入队失败。如果有命令在入队时失败,那么大部分客户端都会停止并取消这个事务。

不过,从 Redis 2.6.5 开始,服务器会对命令入队失败的情况进行记录,并在客户端调用 EXEC 命令时,拒绝执行并自动放弃这个事务。

在 Redis 2.6.5 以前, Redis 只执行事务中那些入队成功的命令,而忽略那些入队失败的命令。 而新的处理方式则使得在流水线(pipeline)中包含事务变得简单,因为发送事务和读取事务的回复都只需要和服务器进行一次通讯。

至于那些在 EXEC 命令执行之后所产生的错误, 并没有对它们进行特别处理: 即使事务中有某个/某些命令在执行时产生了错误, 事务中的其他命令仍然会继续执行。

为什么 Redis 不支持回滚(roll back)

如果你有使用关系式数据库的经验, 那么 “Redis 在事务失败时不进行回滚,而是继续执行余下的命令”这种做法可能会让你觉得有点奇怪。

以下是这种做法的优点:

  • Redis 命令只会因为错误的语法而失败(并且这些问题不能在入队时发现),或是命令用在了错误类型的键上面:这也就是说,从实用性的角度来说,失败的命令是由编程错误造成的,而这些错误应该在开发的过程中被发现,而不应该出现在生产环境中。
  • 因为不需要对回滚进行支持,所以 Redis 的内部可以保持简单且快速。

有种观点认为 Redis 处理事务的做法会产生 bug , 然而需要注意的是, 在通常情况下, 回滚并不能解决编程错误带来的问题。 举个例子, 如果你本来想通过 INCR 命令将键的值加上 1 , 却不小心加上了 2 , 又或者对错误类型的键执行了 INCR , 回滚是没有办法处理这些情况的。


redis 集群方案有哪些

常见集群分类

主从复制集群

分片集群

redis 有那些:

主从复制集群,手动切换

带有哨兵的 HA 的主从复制集群

客户端实现路由索引的分片集群

使用中间件代理层的分片集群

redis 自身实现的 cluster 分片集群


redis 主从复制的原理是什么

主从复制机制

当一个 master 实例和一个 slave 实例连接正常时, master 会发送一连串的命令流来保持对 slave 的更新,以便于将自身数据集的改变复制给 slave , :包括客户端的写入、key 的过期或被逐出等等。

当 master 和 slave 之间的连接断开之后,因为网络问题、或者是主从意识到连接超时, slave 重新连接上 master 并会尝试进行部分重同步:这意味着它会尝试只获取在断开连接期间内丢失的命令流。

当无法进行部分重同步时, slave 会请求进行全量重同步。这会涉及到一个更复杂的过程,例如 master 需要创建所有数据的快照,将之发送给 slave ,之后在数据集更改时持续发送命令流到 slave 。

主从复制的关注点

Redis 使用异步复制,slave 和 master 之间异步地确认处理的数据量

一个 master 可以拥有多个 slave

slave 可以接受其他 slave 的连接。除了多个 slave 可以连接到同一个 master 之外, slave 之间也可以像层叠状的结构(cascading-like structure)连接到其他 slave 。自 Redis 4.0 起,所有的 sub-slave 将会从 master 收到完全一样的复制流。

Redis 复制在 master 侧是非阻塞的。这意味着 master 在一个或多个 slave 进行初次同步或者是部分重同步时,可以继续处理查询请求。

复制在 slave 侧大部分也是非阻塞的。当 slave 进行初次同步时,它可以使用旧数据集处理查询请求,假设你在 redis.conf 中配置了让 Redis 这样做的话。否则,你可以配置如果复制流断开, Redis slave 会返回一个 error 给客户端。但是,在初次同步之后,旧数据集必须被删除,同时加载新的数据集。 slave 在这个短暂的时间窗口内(如果数据集很大,会持续较长时间),会阻塞到来的连接请求。自 Redis 4.0 开始,可以配置 Redis 使删除旧数据集的操作在另一个不同的线程中进行,但是,加载新数据集的操作依然需要在主线程中进行并且会阻塞 slave 。

复制既可以被用在可伸缩性,以便只读查询可以有多个 slave 进行(例如 O(N) 复杂度的慢操作可以被下放到 slave ),或者仅用于数据安全。

可以使用复制来避免 master 将全部数据集写入磁盘造成的开销:一种典型的技术是配置你的 master Redis.conf 以避免对磁盘进行持久化,然后连接一个 slave ,其配置为不定期保存或是启用 AOF。但是,这个设置必须小心处理,因为重新启动的 master 程序将从一个空数据集开始:如果一个 slave 试图与它同步,那么这个 slave 也会被清空。
任何时候数据安全性都是很重要的,所以如果 master 使用复制功能的同时未配置持久化,那么自动重启进程这项应该被禁用。

Redis 复制功能是如何工作的

每一个 Redis master 都有一个 replication ID :这是一个较大的伪随机字符串,标记了一个给定的数据集。每个 master 也持有一个偏移量,master 将自己产生的复制流发送给 slave 时,发送多少个字节的数据,自身的偏移量就会增加多少,目的是当有新的操作修改自己的数据集时,它可以以此更新 slave 的状态。复制偏移量即使在没有一个 slave 连接到 master 时,也会自增,所以基本上每一对给定的

Replication ID, offset

都会标识一个 master 数据集的确切版本。

当 slave 连接到 master 时,它们使用 PSYNC 命令来发送它们记录的旧的 master replication ID 和它们至今为止处理的偏移量。通过这种方式, master 能够仅发送 slave 所需的增量部分。但是如果 master 的缓冲区中没有足够的命令积压缓冲记录,或者如果 slave 引用了不再知道的历史记录(replication ID),则会转而进行一个全量重同步:在这种情况下, slave 会得到一个完整的数据集副本,从头开始。

下面是一个全量同步的工作细节:

master 开启一个后台保存进程,以便于生产一个 RDB 文件。同时它开始缓冲所有从客户端接收到的新的写入命令。当后台保存完成时, master 将数据集文件传输给 slave, slave 将之保存在磁盘上,然后加载文件到内存。再然后 master 会发送所有缓冲的命令发给 slave。这个过程以指令流的形式完成并且和 Redis 协议本身的格式相同。

你可以用 telnet 自己进行尝试。在服务器正在做一些工作的同时连接到 Redis 端口并发出 SYNC 命令。你将会看到一个批量传输,并且之后每一个 master 接收到的命令都将在 telnet 回话中被重新发出。事实上 SYNC 是一个旧协议,在新的 Redis 实例中已经不再被使用,但是其仍然向后兼容:但它不允许部分重同步,所以现在 PSYNC 被用来替代 SYNC。

之前说过,当主从之间的连接因为一些原因崩溃之后, slave 能够自动重连。如果 master 收到了多个 slave 要求同步的请求,它会执行一个单独的后台保存,以便于为多个 slave 服务。

无需磁盘参与的复制

正常情况下,一个全量重同步要求在磁盘上创建一个 RDB 文件,然后将它从磁盘加载进内存,然后 slave 以此进行数据同步。

如果磁盘性能很低的话,这对 master 是一个压力很大的操作。Redis 2.8.18 是第一个支持无磁盘复制的版本。在此设置中,子进程直接发送 RDB 文件给 slave,无需使用磁盘作为中间储存介质。

微信公众号

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Tellsea

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值