Created with Sketch.

八股 标签下的文章 共 9 篇

目录 概述 Kafka RocketMQ 常见问题 一、概述 使用场景 解耦:生产者 / 消费者只负责发送 / 消费消息,而不关心对方状态;消费者挂掉后,消息只会积压在队列中,不影响整体业务。 削峰:高并发场景下,把大量请求写入 MQ,消费者按自己的处理能力拉取,避免直接冲垮数据库或服务。 异步:将无需同步完成的步骤放到 MQ 异步处理,提升接口响应速度。 各 MQ 对比 Kafka:性能高,分区有序,采用 sendfile。 RocketMQ:有序,采用 mmap(需要读取消息内容实现死信队列等)。 消息模型 队列模型:一个消息只能被一个消费者消费,未被消费的消息保持在队列中,直到被消费或超时。 发布-订阅模型:消息发送到 Topic,会被所有订阅 Topic 的消费者消费。 二、Kafka 核心概念 Broker:Kafka 集群中的每一个实例。 Partition:每个 Topic 有多个逻辑分区,分布在多个 Broker 上实现并发能力。 Replica:每个分区有 Leader 和 Follower 两种副本,分布在不同 Broker 上。 Leader:生产者和消费者只与这个唯一的 Leader 副本交互。 Follower:Leader 故障时,会从 Follower 中选举出一个 Leader。 Consumer Group:消费者组,共同消费同一个 topic,每个分区同一时间只能由一个消费者读取,故消费者数量与分区数相同时,吞吐量最大。 消息参数 topic、partition、key(相同 key 的消息进入同一 partition)、data 持久化 先写到内存的 PageCache 中,再由操作系统决定什么时候刷盘(默认),可以设置 log.flush.interval.messages 和log.flush.interval.ms 参数,控制日志达多少条、或保留时间超过多少时强制刷盘。 死信队列 Kafka 原生不支持死信队列,因为设计理念专注于消息存储和传递,把消费失败的处理交给上层应用来实现,但是可以通过一些框架实现。 Spring Kafka:在 @KafkaListener 同类下创建 @DltHandler 注解的方法,自动将消息内容、topic、partition、offset 以及异常信息等写入 .DLT 的 topic 中。注意,死信队列本身依赖 Kafka 的可用性,只能作为一道保险,最好增加持久化兜底。 三、RocketMQ 核心概念 Broker:并非 Kafka 中的独立服务实例,而是有主从之分;与 Topic 多对多。 NameServer:注册 Broker 信息,实现 Broker 管理和路由。 Queue:每个 Topic 拆分成多个 Queue 供读写,消息在 Queue 内有序。 持久化 设置 Broker 的 FlushDiskType 参数: 同步刷盘(SYNC_FLUSH):需等待刷盘 ack,安全性高,但性能较低。 异步刷盘(ASYNC_FLUSH):后台异步线程提交,Broker 宕机时会丢失数据。 主从同步 同步复制:只有消息同步双写到主从节点上时才返回写入成功。 异步复制:消息写入主节点之后就直接返回写入成功。 异步复制不影响可靠性,仅作为高可用手段,而可靠性依赖刷盘策略。RocketMQ 不支持自动主从切换,但消费者可以自动切换到从节点进行消费。 单主从架构下,主节点挂掉后整个系统无法生产,可用性差,因此生产环境一般采用多主多从部署。 功能特性 延时消息:将消息放入延时 Topic 队列,通过定时任务检查实现。 顺序消息:保证严格有序,需设置消息组,把消息映射到同一个 Queue。 分布式事务(最终一致性) (1)服务 A 发送 Half Message(消费者不可见)。 (2)服务 A 收到 MQ 响应后,执行本地事务。 (3)执行成功则发送 commit 到 MQ,消息变为可消费。 (4)执行失败则发送 rollback 到 MQ,删除半消息。 (5)如果 MQ 一直未收到服务 A 返回,回调服务 A 回查。 (6)根据回查结果再次 commit 或 rollback。 回溯消费:按照时间维度来回退消费进度。 四、常见问题 重复消费 生产者端:MQ 自身接口保证不会加入重复消息(如缓存已处理的 message_id)。 消费者端:需消费者做幂等,或拉取到消息就响应(会消息丢失,可配合对账)。 消息丢失 生产者端:生产者在 MQ 返回异常时,设置合理的消息重发逻辑。 Kafka:send() 方法返回 ListenableFuture,为异步操作,除了用 get() 使其变成同步操作外,还可以 addCallback() 为其添加回调。 消息队列:持久化 + 集群副本防止单点故障。 Kafka:可配置如下参数。 - acks:默认为 1,L

目录 概述 日志 事务 类型 索引 优化 主从架构 概述 基础架构 MySQL 主要分为 Server 层和存储引擎层: Server 层:所有跨存储引擎的功能都在这一层实现,如存储过程、触发器、视图,函数等,还有一个通用的 binlog 日志模块。 查询缓存在 MySQL 8.0 后被移除,因为缓存失效在实际业务场景中可能会非常频繁,如果对一个表更新,这个表上的所有的查询缓存都会被清空。 存储引擎:主要负责数据的存储和读取,采用可替换的插件式架构,支持 InnoDB、MyISAM 等多个存储引擎,其中 InnoDB 自带 redo log 日志模块,采用聚簇索引,支持事务、行锁、外键等(以下所有内容都是基于 InnoDB 的)。 查询语句 先检查该语句是否有权限,如果没有权限直接返回错误信息;如果有,以这条 SQL 语句为 key 在内存中查询是否有缓存(MySQL 8.0 以前),无缓存则执行下一步。 通过分析器进行词法分析,提取 SQL 语句的关键元素,如这个语句是 select,查询的表名、列,以及查询条件等。然后判断是否有语法错误。 接下来优化器会根据自己的优化算法选择其所认为执行效率最高的方案(有时不一定最好),例如上面的 SQL 可以有两种执行方案: a. 先查询表中姓名为“张三”的学生,再判断年龄是否是 18 岁。 b. 先找出学生中年龄为 18 岁的,再查询姓名为“张三”的学生。 确认了执行计划后,进行权限校验,如果没有权限就会返回错误信息,否则调用数据库引擎接口,返回引擎的执行结果。 Server 层每从存储引擎读到一条记录就会发送给客户端,之所以客户端是直接显示所有记录的,是因为客户端是等查询语句完成后才会显示。 更新语句(两阶段提交) 在 InnoDB 引擎下,这个语句的执行流程如下: 先通过 where 条件查询到张三这条数据,如果该行数据不在 InnoDB Buffer Pool(内存)中,则从磁盘加载对应的数据页。 InnoDB 修改内存中的该行数据,同时生成 undo log(更新前的值,用于回滚)。 WAL(Write-Ahead Logging)机制:InnoDB 写入 redo log(保证宕机后可恢复数据),进入 prepare 状态,并通知执行器准备提交事务。 执行器记录 binlog(逻辑日志,用于主从复制、数据恢复),记录该语句。 执行器调用 InnoDB 引擎提交 redo log,将其改为 commit 状态,更新完成。 两阶段提交 上述过程中,redo log 的写入拆成了两个步骤 prepare 和 commit。 写入 binlog 时发生异常时:MySQL 根据 redo log 日志恢复数据时,发现 redo log 还处于 prepare 阶段,且没有对应 binlog 日志,就会回滚该事务。 redo log 在 commit 阶段发生异常时:虽然 redo log 处于 prepare 状态,但是能通过事务 id 找到对应的 binlog 日志,所以 MySQL 认为是完整的,就会提交事务。 日志 binlog MySQL 中的逻辑日志,用于记录语句的原始逻辑。数据备份、主从复制需要依靠 binlog 来同步数据。它有三种格式: statement:SQL 语句原文,但 update_time=now() 会获取当前系统时间。 row:update_time=now() 变成了具体的时间。 mixed:前两者的混合,因为 row 更占用空间,恢复与同步时会更消耗 IO 资源。 写入机制 一个事务的 binlog 不能被拆开,无论这个事务多大,也要确保一次性写入,所以系统会给每个线程分配一个块内存作为 binlog cache。 单个线程 binlog cache 的大小可以由参数 binlog_cache_size 控制,如果存储内容超过了这个参数,就要暂存到磁盘。 write 和 fsync 的时机,可以由参数 sync_binlog 控制。 在出现 IO 瓶颈的场景里,将 sync_binlog 设置成较大的值可以提升性能。 0:每次提交事务都只 write,由系统自行判断什么时候执行 fsync 。 1:每次提交事务都会执行 fsync ,防止宕机时 cache 中 binlog 丢失。 N(N>1):每次提交事务都 write,但累积 N 个事务后才 fsync。 undo log undo log 属于逻辑日志,记录的是 SQL 语句,比如事务执行一条 DELETE 语句,那 undo log 就会记录一条相对应的 INSERT 语句。同时,undo log 的信息也会被记录到 redo log 中,因为 undo log 也要实现持久性保护。当执行事务过程中出现错误或者需要执行回滚操作的话,MySQL 可以利用 undo log 将数据恢复到事

目录 Java 内存区域 JVM 垃圾回收 类加载器与类加载器 HotSpot 虚拟机对象实现 一、Java 内存区域 程序计数器 当前线程所执行的字节码的行号指示器。字节码解释器工作时通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等功能都需要依赖这个计数器来完成。程序计数器是唯一一个不会出现 OutOfMemoryError 的内存区域,它的生命周期随着线程的创建而创建,随着线程的结束而死亡。 虚拟机栈 除Native方法外,所有的方法都是通过栈来实现的。方法调用的数据需要通过栈进行传递,每次方法调用,都会有一个对应的栈帧被压入栈中,每次方法调用结束,都会有一个栈帧被弹出。栈的生命周期也和线程相同。 局部变量表:存放了编译期可知的各种数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference类型)。 操作数栈:作为方法调用的中转站,用于存放方法执行过程中的临时操作数和中间计算结果。由于JVM的无寄存器设计,操作数栈成为核心的计算区域。例如: 动态链接:指向运行时常量池中的方法引用(Method References)。用于在方法中调用其他方法时,将Class文件常量池中指向方法的符号引用(因为编译期无法知道实际内存地址)转化为其在内存地址中的直接引用。 程序运行中栈可能会出现两种错误: StackOverFlowError:栈的内存大小不允许动态扩展时,线程请求栈的深度超过当前虚拟机栈最大深度(如函数调用陷入无限循环)。 OutOfMemoryError:栈的内存大小允许动态扩展时,虚拟机在动态扩展栈时无法申请到足够的内存空间。 本地方法栈 与虚拟机栈类似,区别在于其为Native方法服务,HotSpot中与虚拟机栈合二为一。 堆 在JVM启动时创建,是由所有线程共享的一块最大的内存区域。堆的唯一目的就是存放对象实例,几乎所有的对象实例以及数组都在这里分配内存。 JDK1.7后默认开启逃逸分析,如果某些方法中的对象引用没有被返回或未被外面使用(即未逃逸),那么对象可以直接在栈上分配内存。 堆也称为GC堆,是垃圾收集器管理的主要区域。收集器基本都采用分代垃圾收集算法,因此堆可以细分为:新生代、老年代、永久代,JDK8后永久代被元空间取代。新生代还能划分为Eden、Survivor。详见内存分配与回收原则。 程序运行中堆最容易出现 OutOfMemoryError 错误,例如: java.lang.OutOfMemoryError: GC Overhead Limit Exceeded:当JVM花费过多的时间进行GC,却只能回收很少的堆空间时。 java.lang.OutOfMemoryError: Java heap space:堆内存不足以存放新创建的对象。(受制于配置的最大堆内存 -Xmx 和物理内存大小。) 方法区 方法区是JVM运行时线程共享的一块逻辑区域(抽象概念),在不同的虚拟机上,方法区的实现(如永久代、元空间)是不同的。当JVM要使用一个类时,它会读取并解析Class文件获取相关信息(类信息、字段信息、方法信息、常量、静态变量、即时编译器编译后的代码缓存等),再将信息存入到方法区。 为什么要将永久代 (PermGen) 替换为元空间 (MetaSpace) ? 永久代受制于JVM设置的固定大小 -XX:MaxPermSize ,启动后无法调整,加载大量类时容易出现 java.lang.OutOfMemoryError: PermGen space 错误;而元空间使用本地内存,加载多少个类的元数据只受制于本机实际可用内存,溢出的概率更小。(元空间溢出时会出现 java.lang.OutOfMemoryError: MetaSpace 错误) -XX:MaxMetaspaceSize:设置最大元空间大小,默认值为unlimited。 -XX:MetaspaceSize:设置元空间的初始大小,如果未指定,则根据运行时的应用程序需求动态调整。 永久代与堆共享垃圾回收器,增加了GC的复杂度,且回收效率低。 JDK8以前,HotSpot通过永久代存储类的元数据,而JRockit中没有类似的概念,因此在两者合并后,就没有必要额外设置一个永久代了。 运行时常量池 Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有用于存放编译期生成的各种字面量和符号引用的常量池表(Constant Pool Table)。常量池表会在类加载后存放到方法区或元空间的运行时常量池中,内存受制于方法区或元空间。 字面量:程序中显式写出的值,如数字、字符、字符串等。 符号引用:对类、字段、方法、接口方法等的引用。解析阶段就是JVM把常量池中的符号引用替换成直接引用的过程。 字符串常量池 字符串常量池是JVM为了提

目录 概述 HTTP/HTTPS (应用层) DNS(应用层) TCP/UDP(传输层) NAT(网络层) ARP(网络层) ICMP(网络层) 其它 一、概述 OSI 和 TCP/IP 网络分层模型 各层传输的数据单位 | 单位 | 层级 | 描述 | |-----------------------------|----------------|------------------------------------------------| | 数据(Data) | 应用层 | 例如 HTTP 请求、FTP 数据等 | | 报文段(Segment) | 传输层 | TCP 使用报文段 | | 数据报(Datagram) | 传输层 | UDP 使用数据报 | | 数据包(Packet) | 网络层 | 包含 IP 地址,用于跨网络的节点通信 | | 帧(Frame) | 链路层 | 包含 MAC 地址,用于同一局域网内的节点通信 | | 比特(Bits) | 物理层 | 最基本的传输单位,通过物理介质传输 0/1 | 二、HTTP/HTTPS (应用层) HTTP vs HTTPS HTTP 运行在 TCP 之上,传输的内容都是明文,客户端和服务端间无法验证身份。 HTTPS 运行在 SSL/TLS 之上,SSL/TLS 运行在 TCP 之上,建立连接时,通过证书颁发机构(CA,Certificate Authority)颁发的证书进行非对称加密,此后传输的内容采用对称加密(非对称加密效率低且加密的数据长度受限),但对称加密的密钥用服务器方的证书进行了非对称加密,相比 HTTP 安全性更高,但会耗费更多服务器资源。 HTTPS 请求流程 设有客户端 C,服务器 S,第三方信赖机构 CA,攻击者 A: 如果攻击者 A 向 C 发送一个诈包,假装是 S 公钥,C 后续就会用 A 的公钥对数据进行加密,在公开信道传输,那么 A 将捕获这些加密包,用 A 的私钥解密,就截获了 C 本要给 S 发送的内容。 为解决这一问题,SSL/TLS 采用了数字签名:CA 采用散列技术对证书生成摘要,通过 CA 私钥对其加密,附在证书下方。 TLS Handshake:C 向 S 发送 ClientHello(包含支持的加密算法、协议版本等)。 S 返回证书:S 选择加密算法,并向 C 发送 CA 颁发的证书(包含 S 的公钥和域名、CA 信息、证书有效期、数字签名)。 C 验证证书:C 根据本地的 CA 信任列表验证 CA 是否可信,确认证书是否已过期,以及检查证书上的域名是否与访问的域名匹配;之后 C 获取 CA 公钥,将签名解密成摘要,并对证书进行相同散列处理得到摘要,两个摘要如果相同,则信任 S 的公钥。 C 发送预主密钥:C 生成一个随机的预主密钥,用 S 的公钥加密并发送给 S。 双方计算会话秘钥:S 用自己的私钥解密后,C 和 S 共同计算最终的对称会话密钥。 预主密钥(Pre-Master Secret)作用:前向安全性(即使 S 私钥泄露,过去的会话密钥仍然安全);master secret 长度非常大,会增加通信延迟。 HTTP/1.0 vs HTTP/1.1 连接方式:HTTP/1.0 为短连接,HTTP/1.1 支持长连接。 状态响应码:HTTP/1.1 中新加入了大量的状态码。 缓存机制:HTTP/1.0 中主要使用 Header 里的 If-Modified-Since 、Expires 作为缓存判断的标准,HTTP/1.1 引入了更多的缓存控制策略,如 Entity tag 、If-Unmodified-Since 、If-Match 等。 带宽:HTTP/1.1 在请求头引入了 range 头域,允许只请求资源的某个部分而非整个对象,返回码为 206(Partial Content),实现断点续传,避免了带宽浪费。 Host 头:HTTP/1.1 引入了 Host 头字段,允许在同一IP地址上托管多个域名,从而支持虚拟主机的功能。 HTTP/1.1 vs HTTP/2.0 多路复用:HTTP/1.1 为了弥补串行化(一个请求完成后才能处理下一个请求)的限制,通常会打开多个 TCP 连接;而 HTTP/2.0 在同一连接上可以并行传输多个请求和响应,减少了 TCP 连接数量和网络延迟。 二进制帧:HTTP/1.1 使用文本格式的报文进行数据传输;HTTP/2.0 使用更加紧凑的二进制帧,减少了数据传输量和带宽消耗。 头部压缩:HTTP/1.1 仅支持对 Body 压缩,HTTP/2.0 支持对 Header 压缩。 服务器推送:HTTP/2.0 支持服务器推送,在客户端请求一个资源时,将其他相关资源一并推送给客户端,从而减少了客户端的请求次数。 RPC(Remote Procedure Call)基于 TC

目录 单例模式 装饰器模式 适配器模式 工厂模式 观察者模式 代理模式 命令模式 单例模式 装饰器模式 适配器模式 工厂模式 策略模式 相较于工厂模式,更关注状态的切换。 观察者模式 代理模式 使用代理对象来代替对真实对象的访问,这样就可以在不修改原目标对象的前提下,扩展目标对象的功能,提供额外的功能操作。 例子:下面将通过多种方式去增强 send 方法。 1. 静态代理 2. JDK 动态代理 在 Java 动态代理机制中,InvocationHandler 接口用于自定义处理逻辑,Proxy 类的 newProxyInstance() 方法用于生成代理对象。 实现 3. CGLIB 动态代理 依赖 实现 4. 三种方式的对比 静态代理中,接口一旦增加新方法,目标对象和代理对象都要进行修改;而动态代理不需要实现接口,可以直接代理实现类,并且不需要对每个目标类都创建一个代理类。 静态代理在编译时就将接口、实现类、代理类这些都变成了一个个实际的 class 文件;而动态代理是在运行时动态生成类字节码,并加载到JVM中的。 JDK动态代理只能代理实现了接口的类或者直接代理接口,而CGLIB可以代理未实现任何接口的类。 JDK动态代理效率相较于CGLIB更加优秀。 CGLIB是通过生成一个被代理类的子类来拦截被代理类的方法调用,因此不能代理声明为 final 类型的类和方法。 命令模式

目录 概述 语法和重要概念 数据类型 集合 异常 一、概述 Java语言的特点 面向对象:封装、继承、多态。 封装(Encapsulation):将数据和操作数据的方法包装在一起,隐藏内部细节,只能通过对外提供的接口,对封装在内部的属性和方法进行访问和操作。 继承(Inheritance):子类复用和扩展父类的属性和方法,实现层次结构。 多态(Polymorphism):调用相同方法做出不同行为。两种实现方式:通过继承(子类方法重写)、通过接口。 平台无关性:JVM实现“Write Once, Run Anywhere.”。 可靠性:异常处理、自动内存管理机制。 安全性:如访问权限修饰符、限制程序直接访问操作系统资源。 编译与解释共存 .java 文件编译 为 *字节码*(JVM能理解的代码,即 .class 文件),字节码解释为 *机器码* 。由于字节码只面向JVM,因此程序无须重新编译便可在不同操作系统上运行。 JIT(Just in Time Compilation)编译器:运行时编译。当JIT编译器完成第一次编译后,会将字节码对应的机器码保存下来,下次直接使用。 AOT(Ahead of Time Compilation):在程序被执行前就将其编译成机器码,可以提高程序的启动速度,避免预热时间长,但无法支持反射、动态代理、动态加载、JNI(Java Native Interface)等。 二、语法和重要概念 重载和重写 重载(Overload):同一类中方法名相同,参数列表不同。发生在编译期。 重写(Override):子类重写父类方法,子类方法的返回值类型和异常比父类方法更小或相等;发生在运行期。 浅拷贝和深拷贝 浅拷贝:在堆上创建一个新对象,如果原对象内部属性有引用类型,浅拷贝会直接复制内部对象的引用地址,也就是说和原对象共用同一个内部对象。 深拷贝:会完全复制整个对象,包括其所包含的内部对象。 序列化和反序列化 位于 TCP/IP 协议中的应用层:将应用层的用户数据进行处理转换为二进制流。 序列化:将数据结构或对象转换成二进制字节流、JSON 、XML 等。 反序列化:将序列化过程中生成的数据转换为原始数据结构或对象的过程。 应用场景:网络传输、存储到文件、存储到缓存数据库、存储到内存。 JDK自带的序列化方式:实现 java.io.Serializable 接口。 serialVersionUID 用于标识类的序列化版本,没有被序列化。 static 修饰的变量因为不属于任何对象,所以不会被序列化。 transient 修饰的变量不会被序列化,反序列化后会被置成类型默认值。 缺点:不支持跨语言调用、性能差、安全问题。 其它序列化方式:Kryo、Protobuf、ProtoStuff、Hessian。 强引用/软引用/弱引用/虚引用 | 类型 | 特点 | 典型用途 | |-----------------|--------------------------------------------------------------------------------------------------|---------------------------------| | 强引用 | 普通的对象引用,垃圾回收器不会回收。 | 常规对象使用。 | | 软引用 | 内存不足时回收对象; 可以和引用队列联合使用。 | 缓存数据(如图片缓存)。 | | 弱引用 | 不论内存是否充足,GC时都会回收; 可以和引用队列联合使用。 | 缓存、引用池中的对象引用。 | | 虚引用 | 不能访问对象,用于跟踪对象回收; 必须和引用队列联合使用。 | 监控对象回收、清理资源。 | 反射 / 注解 代码块执行顺序 顺序:静态代码块(只执行一次)->普通代码块->构造函数->普通代码块->构造函数 值传递 Java只有值传递,没有引用传递,方法接收的是实参值的拷贝(可以是实参的地址),会创建副本。 三、数据类型 包装类型 包装类型与基本类型的区别 用途:除了定义一些常量和局部变量之外,方法参数、对象属性中常用包装类型。包装类型可用于泛型,而基本类型不行。 存储方式:基本类型的局部变量存放在栈的局部变量表中,成员变量(未被 static 修饰)存放在堆中。包装类型属于对象类型,因此存在堆中。 占用空间:基本类型占用的空间往往更小。 默认值:基本类型有默认值,而包装类型为 null。 比较方式:因为包装类型是对象,== 比较的是内存地址,equals() 比较的是值。 阿里Java开发手册: 【强制】定义 DO/DTO/VO 等 POJO 类时,不要设定默认值。 【强制】所有的 POJO 类属性必须使用包装数据类型。 【强制】RPC 方法的返回值和参数必须使用包装数据类型。 【推荐】所有的局部变量使用基本数据类型。 包装类型的缓存机

目录 概述 用户态和内核态 用户态(User Mode) : 用户态的进程拥有较低的权限,可以读取用户程序的数据。 内核态(Kernel Mode):内核态的进程拥有更高的权限,几乎可以访问计算机的任何资源(包括内存、设备、驱动程序等),能够执行更底层、更敏感的操作。当应用程序需要执行某些需要特殊权限的操作(如读写磁盘、网络通信等),就需要向操作系统发起系统调用请求,进入内核态。进入内核态的开销较高(上下文切换和权限检查),因此应尽量减少进入内核态的次数,以提高系统性能。 如果只有内核态会怎么样? 恶意程序可能会覆盖操作系统的关键代码或数据,或终止其他进程,导致系统崩溃。 所有进程都可以无限占用内存、CPU、磁盘等资源,导致资源枯竭。 用户态切换到内核态的 3 种方式 系统调用:用户态进程主动要求切换到内核态。 中断:当外围设备完成用户请求的操作后(如硬盘读写操作完成),会向 CPU 发送中断信号,CPU 立即保存当前执行上下文并转入内核态执行中断处理程序。如果中断发生时 CPU 运行的是用户态代码,则会发生用户态到内核态的切换。 异常:当 CPU 在执行用户态进程时发生异常(如缺页异常、除零错误),会触发 CPU 进入内核态,执行相应的异常处理程序,以恢复或终止进程。 系统调用 程序基本都是运行在用户态,当我们要调用操作系统提供的内核态级别的子功能(设备管理、文件管理、进程控制、内存管理等)时,就需要系统调用,由操作系统代为完成。 系统调用的过程 用户态程序通过 glibc(与系统调用一一对应的函数库)调用 syscall ,触发 Trap 进入内核态。 CPU 保存用户态上下文,并根据系统调用号查找相应的系统调用处理函数。 内核执行完系统调用后,使用 sysret 等恢复用户态上下文,CPU 切换回用户态。 中断 为了避免中断处理程序执行时间过长,Linux 将中断处理程序分为上半部和下半部: 上半部:硬中断,由硬件触发,用来快速处理中断。 下半部:软中断,由内核触发,用来异步处理上半部未完成的工作,通常都是耗时比较长的事情,特点是延迟执行。 进程和线程 进程(Process) 指正在运行的一个程序实例。举例:你打开的微信就是一个进程。 线程(Thread) 也称轻量级进程。多个线程可以在同一个进程中同时执行,并且共享进程的资源(如内存空间、文件句柄、网络连接等)。举例:你打开的微信里就有一个线程专门用来拉取别人发你的最新的消息。 进程和线程的区别 线程是进程划分成的更小的运行单位,一个进程在其执行的过程中可以产生多个线程。 进程拥有一个完整的资源平台,而线程只独享必不可少的资源,如寄存器和栈。 各进程是独立的,而各线程则不一定,因为同一进程中的线程极有可能会相互影响。 线程执行开销小,但不利于资源的管理和保护;而进程正相反。 有了进程为什么还需要线程? 线程切换和调度的成本远低于进程(共享虚拟内存无需切换页表)。 多个线程可以并发处理不同的任务,更有效地利用了多核计算机资源。而进程只能在一个时间干一件事,如果在执行过程中阻塞,就会挂起直到结果返回。 同一进程内的线程共享内存和文件,相互通信无须调用内核。 进程状态 创建状态:进程正在被创建,尚未到就绪状态。 就绪状态:进程已处于准备运行状态,即进程获得了除了处理器之外的一切所需资源,一旦得到处理器资源(处理器分配的时间片)即可运行。 运行状态:进程正在 CPU 上运行(单核 CPU 任意时刻只有一个进程在运行状态)。 阻塞状态:又称为等待状态,进程正在等待某一事件而暂停运行如等待某资源为可用或等待 IO 操作完成。即使处理器空闲,该进程也不能运行。 结束状态:进程正在从系统中消失。可能是进程正常结束或其他原因中断退出运行。 进程控制块 PCB PCB(Process Control Block)数据结构是进程存在的唯一标识,每个进程都对应着一个独立的 PCB。当进程执行时,PCB 中的信息会不断变化,相同状态进程的 PCB 通过链表链在一起(形成就绪队列、阻塞队列等),操作系统会根据这些信息来管理和调度进程: 进程描述信息,进程名称、进程标识符、用户标识符等 进程调度信息,进程状态、阻塞原因、进程优先级等。 资源需求情况,CPU 时间、内存空间、I/O 设备等。 打开的文件信息,文件描述符、文件类型、打开模式等。 处理机状态信息,通用寄存器、指令计数器、程序状态字 PSW、用户栈指针。 进程间通信方式 管道/匿名管道(Pipes) :用于具有亲缘关系的父子进程间或者兄弟进程间的通信,只存在于内存中。 有名管道(Named Pipes) : 可以实现本机上任意两个进程间通信。有名管道遵循 FIFO,以磁盘文件的方式存在。 消息队列(Message Queuing) :无需等待消息被读出再返回;并且每个消息体都是固定大小的存储块,发送方和接收方能够约定好其数据类型;此外

先挖个坑,之后再填。。 目录 B Tree & B+ Tree 红黑树(Red Black Tree) 哈希表 (Hash Table) 跳表(SkipList) 整数集合(IntSet) 压缩列表(ZipList) 快速列表(QuickList) 紧凑列表(ListPack) 布隆过滤器(BloomFilter) B Tree & B+ Tree ...... 红黑树(Red Black Tree) ...... 哈希表(Hash Table) ...... 跳表(SkipList) 思想:在单链表上增加多级索引(空间换时间)。地铁快慢线?(bushi) 时间复杂度:O(log N);空间复杂度:O(n) ;插入/删除时间复杂度:O(log N) 。 下图中,如果要找到值为 17 的结点,单链表需要遍历10个结点,而跳表仅需遍历7个。 如下图所示,假如我们要查找的数据是 x ,在第 k 级索引中,我们遍历到 y 结点之后,发现 y < x < z ,所以我们通过 y 的down指针下降到第 k-1 级索引。在第 k-1 级索引中,y 和 z 之间只有3个结点,故每一级索引最多只需要遍历3个结点。 索引动态更新:当我们不断地往跳表中插入数据时,我们如果不更新索引,就有可能出现某两个索引节点间数据非常多的情况,极端情况下,跳表还会退化成单链表,如: 因此,当我们往跳表中插入数据的时候,我们可以通过一个随机函数生成值 K ,来决定这个结点添加到第一级到第 K 级的索引中: Redis中Zset的实现:由 zskiplist(保存跳跃表节点的相关信息)和 zskiplistNode(表示跳跃节点)定义。 zskiplist 包含以下属性: header:指向跳跃表的表头节点。 tail:指向跳跃表的表尾节点。 level:层数最大的节点层数(不含表头节点)。 length:目前包含节点的数量(不含表头节点)。 zskiplistNode 包含以下属性: level:L1 代表第一层,L2 代表第二层,以此类推。每个层都带有两个属性:前进指针和跨度。前进指针用于访问位于表尾方向的其它节点,而跨度则记录了前进指针所指向节点和当前节点的距离。 BW:后退(backward)指针,指向当前节点的前一个节点,在程序从表尾向表头遍历时使用。 score:各节点保存的分值,节点默认按分值升序排列。 obj:节点所保存的成员对象(实际开发中,索引节点无需存储完整对象)。 Redis为什么用跳表而不用平衡树? 内存占用:平衡树每个节点包含 2 个指针(指向左右子树),而跳表每个节点平均包含 1/(1-p) 个指针,Redis中取 p=1/4 ,即 1.33 个指针。 范围查找:平衡树中找到指定范围的小值之后,还需要以中序遍历的顺序继续寻找其它不超过大值的节点;跳表只需要在找到小值之后,进行若干步的遍历即可。 实现简单:平衡树的插入和删除可能会引发子树的调整;跳表仅需修改相邻节点指针。 内存友好:B+ 树的设计目标是优化磁盘 I/O,通过减少树的高度来降低磁盘寻道次数,而 Redis 是内存数据库。 参考资料:1,2,3 整数集合(IntSet) 整数集合本质上也是一块连续内存空间。当新加入的元素类型( int32_t )比整数集合现有所有元素的类型( int16_t )都要长时,整数集合需要先自动升级,按新元素的类型扩展 contents 数组的空间大小,然后才能将新元素加入到整数集合里,升级的过程中也要维持整数集合的有序性。 压缩列表(ZipList) 压缩列表是由连续内存块组成的顺序型数据结构,类似于数组。不仅可以利用CPU缓存,而且会针对不同长度的数据,进行相应编码,能够有效节省内存开销。 不能保存过多的元素,否则查询效率就会降低(从头遍历到尾)。 元素数量增加或长度发生变化时,内存空间需要重新分配,可能引发连锁更新的问题。因此仅用于节点数量不多的场景,只要数量足够小,即使发生连锁更新也能接受。 快速列表(QuickList) 快速列表其实就是双向链表 + 压缩列表组合:QuickList 是一个链表,链表中的每个元素又是一个压缩列表。通过控制每个链表节点中的压缩列表大小或元素个数,来减少连锁更新带来的影响,从而提供了更好的访问性能。 紧凑列表(ListPack) 紧凑列表不再像压缩列表一样记录前一个节点长度的字段,只记录当前节点的长度。当向 ListPack 加入新元素时,不会影响其他节点的长度字段的变化,从而避免了连锁更新。 布隆过滤器(BloomFilter)

概述 数据结构 持久化 集群 事务 缓存 分布式锁 场景 一、概述 Redis 的优缺点 Redis 的优点?为什么快? 基于内存操作:绝大部分操作和数据都在内存中,相比传统磁盘文件操作减少了IO。 高效的数据结构:优化的 String、Hash、List、Set、Zset 等数据结构。 采用单线程:省去上下文切换和CPU的开销,同时不存在资源竞争,避免死锁。 单线程:命令执行使用单线程进行处理。因为 Redis 的瓶颈不是 CPU,最有可能是机器内存或网络带宽,并且单线程易于实现。 多线程:Redis 6.0 以后,多线程用于处理网络数据的读写和协议解析,充分利用 CPU 资源,减少网络 I/O 阻塞带来的性能损耗。 删除大 key 时使用 unlink 异步删除而不是 del,否则会造成单线程阻塞。 I/O多路复用:一个服务端进程(复用)同时处理多个套接字描述符(多路),根据 Socket 上的事件来选择对应的事件处理器进行处理。 Redis 的缺点?为什么不做主数据库只做缓存? 内存限制:数据库容量受到物理内存的限制,不能用作海量数据的高性能读写。 数据持久化:尽管采用了数据持久化机制,如果服务器崩溃或断电,内存中数据仍可能丢失。 数据安全:不具备像主数据库一样复杂的认证和审计机制。 结构化查询:作为键值(Key-Value)数据库,对结构化查询支持较差。 事务处理:对复杂的事务无能为力,比如跨多个键的事务处理。 在线扩容:在集群容量达到上限时在线扩容会变得很复杂。 为什么用 Redis 而不用 Map 做缓存? Map 实现的是本地缓存,其生命周期随着 JVM 的销毁而结束,且在多实例的情况下,每个实例都需要各自保存一份缓存,缓存不具有一致性;而 Redis 的分布式缓存具有一致性,各实例共用一份缓存数据。 Redis 可单独部署,在多个项目间共享。 Redis 的缓存可以持久化,Map 是内存对象,程序一重启数据就没了。 Redis 可以用几十G内存来做缓存,Map 不行。 Redis 可以处理每秒百万级的并发。 Redis 缓存有过期机制和丰富的 API。 Redis 应用场景 缓存热点数据:缓解数据库的压力。用户在访问业务数据时,先到 Redis 中拿;如果不存在,再到 MySQL 中拿,接着把访问过的数据写入 Redis。 社交网络:Redis 的哈希、集合等数据结构能很方便的的实现排行榜、共同好友等功能;利用 Redis 原子性的自增操作,可以实现计数器的功能,比如统计用户点赞数等;也可作限速器,如秒杀场景中防止用户快速点击带来不必要的压力。 消息队列:Redis 提供了发布/订阅模式及阻塞队列功能,能够实现简单的消息队列,实现异步操作。 分布式锁:分布式场景下,无法使用单机环境下的锁对多个节点上的进程同步。可以使用 Redis 自带的 SETNX(SET if Not eXists)命令或 RedLock 分布式锁实现。 数据过期策略 Redis 采用了惰性删除和定期删除相结合的过期策略: 惰性删除:不主动删除过期键,访问 key 时再检测是否过期,如果过期则删除。这种方式对 CPU 友好,但如果 key 过期后一直没有使用,则在内存中永远不会释放。 定期删除:每隔一段时间取出一些 key 进行检查并删除过期 key。分两种模式,SLOW 模式是定时任务,FAST 模式执行频率不固定,但两次间隔不低于 2ms。 数据淘汰策略 Redis 的内存不够用时,有 8 种策略来选择要删除的 key: noeviction:默认,不淘汰任何 key,内存满时拒绝写入。 volatile-ttl:优先淘汰更早过期的 key。 volatile-random:随机淘汰设置了过期时间的 key。 volatile-lru:淘汰所有设置了过期时间中最近最久未使用的 key。 volatile-lfu:淘汰设置了过期时间中最少频率使用的 key。 allkeys-random:随机淘汰任意 key。 allkeys-lru:淘汰最近最久未使用的 key。 allkeys-lfu:淘汰最少频率使用的 key。 Redis 如何做内存优化? 尽可能的将数据模型抽象到一个哈希表里面。比如一个用户对象,不要为这个用户的名称,邮箱等设置单独的 Key,而是将这个用户的所有信息存储到一张哈希表里。 二、数据结构 String 底层由 *SDS*(简单动态字符串)实现:具有 len(字符串长度 O(1) 查询)、alloc(分配给字符数组的空间长度)、flags(类型)、buf[](字符数组)属性;拼接前会自动扩容。用于: 缓存对象:JSON 等。 共享Session信息:解决了分布式系统下多服务器 Session 不一致的问题。 分布式锁:利用 SETNX 命令。 计数器:支持原子性数值操作,用于访问次数、点赞、库存等。 Hash