编程满天星题解分享
根据chatgpt采集
Java 后端开发工程师、React 前端开发工程师、全栈开发工程师、iOS 开发工程师、Android 开发工程师等。
1 | 我想让你担任 Java 后端开发工程师候选人。我成为面试官,将向你询问 Java 后端开发工程师面试问题。我希望你只作为候选者回答。 |
char 和 varchar 的区别,哪个索引效率更高?
1 | char 和 varchar 都是用来存储字符串的数据类型,在索引效率上通常 char 会比 varchar 更高。 |
JDK 和 JRE 的区别
1 | JDK 和 JRE 是 Java 平台的两个主要组件,它们的区别如下: |
datetime 和 timestamp 的区别
1 | 在数据库系统中,datetime 和 timestamp 都用来存储日期和时间信息,但它们在行为和用途上有一些关键的区别: |
为什么说 MyBatis 是半自动 ORM 映射工具?它与全自动的区别在哪里
1 | MyBatis 被称为半自动ORM(Object-Relational Mapping)映射工具的原因在于,它在对象与数据库表之间的映射处理上,需要开发者明确指定映射规则。在MyBatis中,你通常需要写SQL语句,并将它们与对象的属性手动映射。这为开发者提供了更高的控制权,但也意味着不能像全自动ORM工具那样自动完成映射。 |
MyBatis 是如何实现对象与数据库表之间的映射的?
1 | MyBatis 实现对象与数据库表之间的映射主要通过以下机制: |
MySQL 索引底层结构(B/B+ 树原理与区别)
1 | 索引有哪几种底层实现? |
有哪些隔离级别,分别解决什么问题(丢失更新、脏读、不可重复读、幻读)
1 | 数据库的事务隔离级别通常分为以下四种,每种隔离级别解决了不同的并发问题: |
在MySQL的InnoDB存储引擎中,为什么Repeatable Read能够防止幻读问题?
1 | 在MySQL的InnoDB存储引擎中,Repeatable Read隔离级别能够防止幻读问题主要是因为它使用了一种称为Next-Key Locking的锁机制。Next-Key Locking是一种组合了行锁定和间隙锁定的锁机制,它不仅锁定了满足查询条件的索引记录,还锁定了这些记录之间的间隙。 |
四种隔离级别是怎么实现的(详细说说 MVCC 的原理)
1 | 数据库中提供了四种不同的事务隔离级别,它们是为了平衡在多用户环境下并发事务带来的问题(如脏读、不可重复读和幻读)与系统性能之间的关系。隔离级别由高到低分别是: |
TCP 三次握手的过程
1 | 第一次握手(客户端 -> 服务端):客户端会随机初始化序号(client_isn),将此序号置于 TCP 首部的「序列号」字段中,同时把 SYN 标志位置为 1 ,表示 SYN 报文。接着把第一个 SYN 报文发送给服务端,表示向服务端发起连接,该报文不包含应用层数据,之后客户端处于 SYN-SENT 状态。 |
为什么TCP三次握手 不是2次 或者4次
1 | 确保双方准备就绪:第一次握手是客户端告诉服务器它想建立连接;第二次握手是服务器告诉客户端它也准备好了,可以接受客户端的数据;第三次握手是客户端再次确认之后,数据传输才会开始。 |
TCP的2MSL等待时间有什么作用,它是如何保持可靠性和网络数据唯一性的?
1 | 确保最后一个ACK被对方收到:等待足够长的时间可以使得最后发送的ACK确认包能够到达对方。如果对方没有收到这个ACK,它会重新发送FIN包。而在这个2MSL窗口内,主动关闭的一方还可以接收并对这些重新传输的FIN包做出响应。 |
TCP的四次挥手的过程
1 | TCP的四次挥手过程是指在TCP/IP网络通信协议中关闭一个已经建立的连接时所采用的过程。四次挥手包括以下步骤: |
2MSL等待时间如何确保对方收到最终的ACK?
1 | 在TCP连接的四次挥手过程中,2MSL等待时间被用来确保对方收到最终的ACK的方法是: |
1 | 客户端进入TIME_WAIT状态的目的主要是为了确保最后一个ACK报文能够被服务器收到,这样服务器就可以正常地关闭连接。如果该ACK在网络中丢失,服务器将会重传FIN报文,客户端在TIME_WAIT状态会期待这种情况,并且可以重新发送ACK报文以响应服务器的FIN重传。 |
创建线程池时的基本参数?如何合理配置这些参数
1 | 创建线程池时的基本参数主要包括: |
Java提供了java.util.concurrent.Executors
类,它包含了多种方法来创建不同类型的线程池。以下是几种常用的线程池构造方法及其特点:
Fixed Thread Pool: 使用
Executors.newFixedThreadPool(int nThreads)
方法创建。这个线程池有固定的线程数量,如果所有线程都在工作,新任务会在队列中等待。1
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(5);
Single Thread Executor: 使用
Executors.newSingleThreadExecutor()
方法创建。这个线程池只有一个线程,它确保所有的任务都在同一个线程中按序执行。1
ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
Cached Thread Pool: 使用
Executors.newCachedThreadPool()
方法创建。这个线程池会根据需要创建新线程,并且如果线程空闲60秒以上则将其销毁。这是一个灵活回收空闲线程的线程池。1
ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
Scheduled Thread Pool: 使用
Executors.newScheduledThreadPool(int corePoolSize)
方法创建。这个线程池可以延迟或定期执行任务。1
ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(5);
Single Thread Scheduled Pool: 使用
Executors.newSingleThreadScheduledExecutor()
方法创建。这个线程池支持单个后台线程执行定期或延迟任务,并保证任务按顺序执行。1
ScheduledExecutorService singleThreadScheduledExecutor = Executors.newSingleThreadScheduledExecutor();
Executors
创建的线程池便于快速使用,但它们有默认的行为和限制,例如:
Fixed
和Single Thread Executor
具有无界的任务队列。Cached Thread Pool
和Scheduled Thread Pool
允许创建的线程数量几乎没有上限。
这些特性可能导致线程或内存资源耗尽,尤其是在处理大量任务时。因此,在生产环境中,通常推荐根据具体需求创建ThreadPoolExecutor
的实例,它是Executors
类中线程池的底层实现,并且提供更多的配置选项,使得你可以自定义线程池的参数,如:最大线程数、存活时间、工作队列和拒绝策略等,以更好地控制资源消耗和任务执行的行为。
说下这几种线程池发生OOM的场景’
线程池可能发生OOM(Out Of Memory,内存溢出)的情况主要与两个因素有关:线程数量的管理和任务队列的大小。以下是这些线程池发生OOM的一些可能场景:
- Fixed Thread Pool: 由于有固定数量的线程,如果任务提交速度远大于消耗速度,那么未执行的任务会在无界队列里积压,这可能会导致内存溢出。
- Single Thread Executor: 这实际上是固定线程池大小为1的特例。它使用无界队列来保存待执行任务,如果任务提交速度持续大于其处理速度,同样会积压任务,造成内存溢出。
- Cached Thread Pool: 因为线程数量理论上是没有限制的(实际上受限于系统资源和整数最大值),如果持续提交任务而没有限制,可能会创建大量线程,每个线程都有自己的栈内存等资源,因此也可能导致内存耗尽。
- Scheduled Thread Pool: 即使这个线程池有核心线程数的限制,但其所使用的延迟队列也是无界的,这意味着大量的延迟/周期性任务堆积也可能导致内存耗尽。
在使用线程池避免OOM时应该注意:
- 合理设置最大线程数,避免创建过多线程;
- 使用有界队列来限制任务积压量;
- 为线程池设置合理的拒绝策略,如在队列满时采取某种策略来处理新进的任务;
- 监控线程池和系统的健康状况,及时处理可能的内存溢出风险;
- 在系统设计时考虑反压机制,当系统负载过高时减缓任务的提交速率。
说明下几种队列,并列举使用场景
- ArrayBlockingQueue: 一个由数组结构组成的有界阻塞队列。此队列按FIFO(先进先出)原则对元素进行排序。适用于固定大小的线程池,你可以明确知道队列所需的最大容量。
- LinkedBlockingQueue: 一个由链表结构组成的可选有界阻塞队列。此队列同样按FIFO排序元素,吞吐量通常要高于ArrayBlockingQueue。用于任务队列时,如果不设置容量,则为无界队列,可能导致内存溢出。有界时适用于任务较多的情况,需要在内存和CPU使用之间进行权衡。
- PriorityBlockingQueue: 一个支持优先级排序的无界阻塞队列。元素按照自然顺序或者构造时提供的
Comparator
进行排序。适用于需要按照给定优先级处理任务的情况。 - DelayQueue: 一个使用优先级队列实现的无界阻塞队列,其中的元素只能在其指定的延迟时间之后取出。适用于实现任务延迟执行的场景。
- SynchronousQueue: 一个不存储元素的阻塞队列。每个插入操作必须等到另一个线程调用移除操作,反之亦然。适用于传递性的任务执行场景,如线程池的交付机制。
- ConcurrentLinkedQueue: 一个基于链接节点的无界非阻塞队列,此队列按FIFO(先进先出)排序元素。适合多线程并发操作,数据一致性要求不是非常严格的场景。
- ConcurrentLinkedDeque: 一个基于链接节点的无界非阻塞双端队列,允许线程安全地在队列的两端插入和移除元素。适合需要快速响应的场景,能够从不同的端进行操作。
这些队列按照不同的需求进行选择和使用,例如任务调度、资源共享、生产者消费者问题等。在实际应用中,还需要根据具体需求和系统资源合理地选择或者自定义队列类型
说一下 MySQL 的事务(ACID 特性)
1 | MySQL的事务是一组操作,要么全部执行,要么全部不执行,是数据库管理系统执行过程中的一个逻辑单位。事务具有以下四个基本特性,通常称为ACID特性: |
数据库三大范式(Normalization)是指数据库设计的三个层次的规范要求,目的是减少数据冗余、提高数据的逻辑一致性。这些范式分别是:
- 第一范式(1NF): 数据表的每一列都是不可再分的基本数据项,同一个数据表中不存在重复的记录。这意味着表是具有行和列的矩阵结构,并且每个表中的字段值都是单一的,不可分割的。
- 第二范式(2NF): 在满足第一范式的基础上,消除非主属性对于码的部分函数依赖。一个表要达到第二范式,它必须首先是满足第一范式的标准,而且它的所有非主属性都完全依赖于主关键字。也就是说,表中不可能出现仅与主键的一部分相关联的字段。
- 第三范式(3NF): 在满足第二范式的基础上,消除非主属性对于码的传递函数依赖。这意味着除主键外的所有字段都应该只依赖于主键,不依赖于其他非主键字段(非主属性不依赖于其他非主属性)。
基本上,数据库设计的目标是为了避免冗余数据和更新异常,而这些范式提供了避免这些问题的指导原则。当然,有时为了查询性能、柔性的需求等,设计人员可能会有意识地对某些要求进行放松,这种设计上的权衡称为反规范化
1 | 让我们通过一个假设的学校数据库表设计的例子来解释这三大范式吧。 |
知道哪些线程安全的 Map(synchronizedMap,Hashtable,ConcurrentHashMap)
1 | 在Java中,有几种线程安全的Map实现,它们通过不同的方式来保证线程安全: |
在Java中,有几种线程安全的Map实现,它们通过不同的方式来保证线程安全:
Hashtable:
Hashtable
是一个古老的线程安全的Map实现,所有的方法都是同步的。不过,由于它的线程安全是通过锁住整个Map来实现的,这会导致在多线程环境中的性能下降。1
Map<K, V> hashtable = new Hashtable<>();
synchronizedMap:
Collections.synchronizedMap
是一个包装器,它取一个不是线程安全的Map并添加同步。和Hashtable
类似,synchronizedMap
为访问Map的每个方法提供了一个同步块,这意味着每次只有一个线程能够修改Map,性能上不适合高并发场景。1
Map<K, V> synchronizedMap = Collections.synchronizedMap(new HashMap<>());
ConcurrentHashMap:
ConcurrentHashMap
是Java 5中加入的一个线程安全的Map实现。它提供了更好的并发性能,因为它采用了分段锁(在Java 8及以后版本中使用CAS操作和synchronized来保证安全性)。多个线程可以同时读写Map的不同部分,因此在高并发环境下通常比Hashtable
和synchronizedMap
表现得更好。1
Map<K, V> concurrentHashMap = new ConcurrentHashMap<>();
不同的线程安全Map适用于不同的应用场景,选择哪种取决于具体的性能需求和线程安全级别。在现代Java应用中,ConcurrentHashMap
由于其高效的并行性,是首选的线程安全Map实现。
ConcurrentHashMap是如何通过分段锁来提高并发性能的?
ConcurrentHashMap通过分段锁提高并发性能的过程(特指Java 7及之前版本)可以这样理解:ConcurrentHashMap内部将存储的数据分成一些小的段,这称为segments,每个segment拥有自己的锁。 当需要进行操作(如添加、删除、更新元素)时,并非锁定整个Map,而只是锁定包含被操作数据的那个segment。因此,如果有多个线程访问ConcurrentHashMap,只要它们访问的是不同的segment,线程间就不会产生锁的竞争,它们可以同时进行访问。这大大增加了在高并发情况下的Map的性能,理论上能够同时支持并发的segment数量的线程。 这种设计相较于单一锁机制(例如Hashtable或synchronizedMap)来说,锁的粒度更细,因此在多线程环境中,它能够提供更高的并发性,并减少线程间的竞争和阻塞。这也就是为什么ConcurrentHashMap通常作为首选的线程安全Map用于高并发环境。
在Java 8中,ConcurrentHashMap的实现方式相较于Java 7及之前版本发生了显著的变化,移除了原有的Segment分段锁,而采用了一种新的并发策略,主要变化包括:
- 数据结构的改变:Java 8的ConcurrentHashMap放弃了Segment的结构,而是直接使用节点数组(Node array),每个节点包含了键值对。在链表的基础上,进一步使用了红黑树来优化性能。
- CAS操作:大量使用了基于CAS(Compare-And-Swap)的无锁操作来实现线程安全的更新,例如在扩容或插入新元素时使用CAS来减少需要同步的地方。
- synchronized:在需要同步的情况下,比如链表转换为红黑树或者更新链表中的节点时,使用了细粒度的锁,即直接对节点对象进行加锁。
- 红黑树的引入:当链表中的元素数目超过一定阈值(默认为8)时,会将链表转换为红黑树,提高在大量哈希冲突情况下的查询效率。
这些改变使得Java 8及之后版本的ConcurrentHashMap在并发性和性能上相对于旧版本有了很大提升,它减小了锁的粒度,并有效降低了锁竞争,特别适合读多写少的并发场景。同时,这种设计还改善了内存占用和扩展性,从而使得Java 8的ConcurrentHashMap在实际应用中更加高效。
详细说明下put 及hash寻址的过程
在 Java 8 的 ConcurrentHashMap
中,put
操作及哈希寻址的过程可以分为以下几个步骤:
- 计算哈希: 当你尝试向
ConcurrentHashMap
中put
一个键值对时,首先会为键计算哈希值。Java 8 中的ConcurrentHashMap
对键的hashCode()
返回的哈希值进行了进一步的扰动处理,以降低哈希碰撞的几率。 - 定位桶位置: 使用计算出的哈希值确定该键值对在内部数组(
Node
数组)中的索引位置。这通过将哈希值和(表长度 - 1)进行按位与操作完成,这假设表的长度总是 2 的幂。 - 检查是否需要初始化表: 如果数组为空,则需要先进行初始化。
- 无锁快速路径: 如果定位到的桶位置为空(没有发生哈希碰撞),使用 CAS 操作直接尝试在该桶位置设立一个新的
Node
,添加键值对,如果 CAS 操作成功则完成了put
操作。 - 锁定桶的首个节点: 如果在桶位置已经有节点存在(发生了哈希碰撞),则锁定该桶的首个节点,这是为了确保任何更新操作的线程安全。
- 链表操作: 在桶中,你将遍历链表,检查键是否已经存在:如果键已存在,更新该键对应的值。如果键不存在,将新的节点添加到链表的末端。如果链表的长度超过一定阈值(
TREEIFY_THRESHOLD
),可能会将链表转换为红黑树以提高后续的查找效率。 - 树形结构操作: 如果该桶已经转变成了一个红黑树,那么进行相应的树节点插入或更新操作。
- 检查是否需要扩容: 在添加节点后,如果发现当前的桶位数量太多,或者整个数组需要扩容来保证性能,可能会触发一个扩容操作。在 Java 8 中,扩容是通过多个线程共同协作完成,每个线程负责迁移数组中的一部分元素到新数组。
以上就是 ConcurrentHashMap
中 put
方法及其哈希寻址过程的大致步骤,在不同的情况下可能会有一些变化或优化,但核心原理基本相同。这些操作确保了并发环境下的安全性和有效性,同时还能提供相对较高的性能。
在ConcurrentHashMap中,扩容操作是一个精心设计的过程,用于在元素太多、造成过多哈希冲突时增加桶的数量。扩容操作通常包括以下步骤:
- 触发扩容条件:当某个线程在插入元素时发现整个HashMap的容量达到了阈值(load factor * 当前容量),或者某个桶中元素太多,超过了特定的阈值,就会触发扩容。
- 初始化新数组:扩容首先会创建一个容量是原数组两倍的新数组。
- 迁移数据:之后,要将旧数组中的所有元素迁移到新数组。从Java 8开始,ConcurrentHashMap采用了分段迁移,即不是一次性迁移所有元素,而是将数组分成多段,每个线程负责迁移一部分数据。
- 并发迁移:在迁移数据时,多个线程可以并发执行,每个线程可以独立地将旧数组的一个或多个段迁移到新数组。通过使用前向指针(forwarding pointers),算法确保了即使在迁移过程中,仍然可以进行查询和插入操作。
- 桶迁移:迁移一个桶时,将会处理整个链表或红黑树中的所有节点,计算它们在新数组中的位置,并在新位置重新链接。
- 迁移完成:当所有线程完成自己负责的段后,迁移完成。ConcurrentHashMap会切换引用,让所有操作都使用新的数组。
这个扩容机制配合其他并发控制手段,如CAS和synchronized,确保了在并发环境下扩容操作的安全性和有效性。
volatile 的作用、原理
1 | 在 Java 中,`volatile` 关键字用于声明一个变量为 "易变",其作用是确保变量在多线程环境中的可见性和有序性。 |
JMM模型和消息总线机制说明
Java 内存模型(JMM)是一个抽象的概念,它描述了Java虚拟机(JVM)在计算机内存中的工作方式,以及处理器和内存交互时如何实现多线程之间的通信。JMM主要处理内存的可见性、原子性、顺序性等问题,确保在多线程环境中程序的正确执行。
TCP/IP 五层模型
从下到上依次是:
1)物理层:主要是指具体的物理媒介和物理设备
- 任务:物理层的主要功能是利用传输介质为数据链路层提供物理联接,负责数据流的物理传输工作(主要定义了系统的电气、机械、过程和功能标准。如:电压、物理数据速率、最大传输距离、物理联接器和其他的类似特性)。基本单位是比特流,即 0 和 1,也就是最基本的电信号或光信号
- 传输单位:比特
- 所实现的硬件:集线器,中继器
2)数据链路层:负责在物理层面上传输数据
- 任务:物理层只是简单的把计算机连接起来并在上面传输比特流,仅仅靠物理层是无法保证数据传输的正确性的,对于发送端来说,数据链路层会把网络层传下来的 IP 数据报封装成帧(添加一些控制信息),这样,接收端接收到这个帧的时候,就可以根据其中的控制信息来判断是否出现了差错,另外,还可以根据这些控制信息知道这个帧从哪个比特开始从哪个比特结束
- 传输单位:帧
- 所实现的硬件:交换机、网桥
3)网络层:负责在不同网络之间传输数据,实现了不同网络之间的互联
- 任务:对于发送端来说,网络层会将传输层传下来的 TCP 报文段或 UDP 用户数据报封装成 IP 数据报进行传输;通过路由选择协议选中合适的路由,使得源主机运输层所传下来的分组能够通过网络中的路由器找到目的主机
- 传输单位:分组(也叫 IP 数据报、数据报)。为了提供通信性能和可靠性,体积较大的 TCP 报文段或 UDP 用户数据报可能会被分成多个更小的部分,在每个部分的前面加上 TCP 或 UDP 首部,就构成了一个个较小的分组
- 所实现的硬件:路由器
- 代表协议:IP 协议、ARP 地址解析协议、ICMP 网际报文控制协议
4)传输层:提供端到端的可靠数据传输和错误恢复功能
任务:负责为两个主机中进程之间的通信提供服务。对于发送端来说,传输层会将应用层传下来的报文封装成 TCP 报文段或者 UDP 用户数据报进行传输。由于一个主机可同时运行多个进程,因此运输层有复用和分用的功能
- 复用,就是多个应用层进程可同时使用下面传输层的服务
- 分用,就是传输层把收到的信息分别交付给上面应用层中相应的进程
通过物理层、数据链路层以及网络层的互相作用,我们已经把数据成功从计算机 A 传送到计算机 B 了,可是,计算机 B 里面有各种各样的应用程序,计算机 B 该如何知道这些数据是给哪个应用程序的呢?
所以,我们在从计算机 A 传数据给计算表 B 的时候,还得指定一个端口(Port),以供特定的应用程序来接受处理。即 IP 地址 + 端口号就可以唯一确定某个主机上的某个应用进程
也就是说,网络层的功能是建立主机到主机的通信,而传输层的功能就是建立端口到端口的通信(也可以说是进程到进程之间的通信)
传输单位:报文段(TCP)或用户数据报(UDP)
代表协议:TCP、UDP
5)应用层:直接为应用程序提供服务的层
任务:直接为用户的应用进程提供服务
传输单位:报文。报文包含了将要发送的完整的数据信息,其长短不需一致
代表协议:DHC 动态主机配置协议、DNS 域名解析协议、HTTP、HTTPS、FTP、SMTP
OSI 七层模型
OSI 七层网络协议模型就是把应用层继续细分成了:会话层 + 表示层 + 应用层
- 会话层(Session layer):负责建立、管理和终止会话。代表协议有:RPC(Remote Procedure Call)、NFS(Network File System)等。
- 表示层(Presentation layer):负责数据的加密、压缩、格式转换等。代表协议有:JPEG(Joint Photographic Experts Group)、MPEG(Moving Picture Experts Group)等。
- 应用层(Application layer):提供各种服务,如电子邮件、文件传输、远程登录等。代表协议有:HTTP(HyperText Transfer Protocol)、FTP(File Transfer Protocol)、SMTP(Simple Mail Transfer Protocol)等
Redis ZSet 的原理和使用场景(延迟队列)
Redis中的ZSet(有序集合)是一种存储唯一元素的集合,但每个元素都会关联一个double类型的分数。Redis根据这些分数来为集合中的元素进行从小到大的排序。ZSet保证了元素的唯一性,因为它是通过HashMap实现的,同时元素的排序则是通过跳表(Skip List)来实现的。
ZSet的原理
- HashMap:保证了元素的唯一性,并能够快速地进行元素的访问和查找,时间复杂度为O(1)。
- 跳表(Skip List):一个跳表是由多层链表组成的数据结构,它允许快速的节点访问,并能够保持元素有序。插入、删除、搜索操作的平均时间复杂度和最坏时间复杂度都是O(logN)。
ZSet实现延迟队列
延迟队列是一种先进先出的队列,不过队列中的元素会在指定时间后才能被消费。ZSet可以方便地实现延迟队列的功能:
- 元素:队列中的元素可以存储实际要执行的任务信息。
- 分数:元素关联的分数可以设置为任务需要被执行的具体时间戳。
当需要从队列中获取任务执行时,可以使用ZSet的范围查询命令,比如ZRANGEBYSCORE
Redis ZSet (有序集合) 保证元素唯一性的关键在于其内部数据结构的使用。ZSet 的实现包含两个主要的数据结构:一个是哈希表(hash table),另一个是跳跃表(skip list)。
- 哈希表:ZSet 中的哈希表用来存储元素及其对应的分数。哈希表的键(key)是集合中的元素,而值(value)是元素的分数。由于哈希表的特性,键必须是唯一的,这就确保了ZSet中的每个元素都是唯一的。如果尝试插入一个已经存在的元素,那么Redis将会更新这个元素的分数而不是添加一个新元素。
- 跳跃表:用于维护元素的排序顺序。跳跃表是一种可以进行高效搜索的数据结构,它通过多层链表来实现快速访问。在ZSet中,跳跃表的节点是元素和它们的分数,节点按分数进行排序。由于每个元素只能有一个分数,而且元素本身是唯一的,跳跃表也就保持了元素的唯一性。
通过这两种数据结构的结合使用,Redis ZSet能够保证集合中元素的唯一性,同时提供高效的元素插入、删除、查找和有序访问操作。
Redis String 原理和使用场景(分布式锁)
Redis 的 String 类型是其最基本的数据类型,它能够存储任何形式的字符串,包括二进制数据。每一个 Redis String 可以存储的数据大小最多可以是 512MB。
1 | ### Redis String 原理: |
为什么使用 SDS
1 | 1. 常数时间复杂度获取字符串长度**:C 字符串需要遍历整个字符串来确定长度,时间复杂度为 O(N)。而 SDS 有一个 `len` 属性来记录字符串长度,获取长度的复杂度为 O(1)。 |
SDS的扩容过程
1 | 1. **计算新长度**:首先计算出添加新字符串后,SDS 需要的总长度。 |
SDS惰性空间
1 | 1. **保留多余空间**:在缩短 SDS 后,例如删除或截断字符串的一部分,多余的内存不会被立即释放。相反,这部分内存会被标记为未使用,并更新 SDS 的 `free` 属性。 |
redis 渐进式rehash的 过程
1 | 1. 每次操作迁移一部分**:在进行rehash时,Redis并不是一次性将旧哈希表中的所有键值对迁移到新哈希表,而是在每次执行写入、删除或查找操作时,顺带迁移一小部分键值对。 |
说下哨兵选主的详细实现流程
- 监控:
每个 Sentinel 节点定期发送PING命令来监控所有的Master和Slave节点的状态。 - 主观下线:
如果一个 Sentinel 节点在指定的时间内没能从 Master 接收到有效回复(通常是PONG响应),它会将该 Master 标记为主观下线(SDOWN)。 - 客观下线:
当一个 Sentinel 节点将 Master 标记为SDOWN后,它会询问其他 Sentinel 节点它们是否也认为该 Master 下线。如果超过配置的quorum数量的 Sentinel 节点同意,那么 Master 将被标记为客观下线(ODOWN)。 - 领导者选举:
一旦Master被标记为ODOWN,Sentinel节点之间会进行领导者选举来决定哪个Sentinel节点负责执行故障转移过程。这通过Raft算法中的选举过程进行。 - 选择Slave节点:
领导者Sentinel会从所有可用的Slave节点中选择一个来晋升为新的Master。选择基于的条件包括:Slave与Master的数据同步延迟、Slave的运行时间、是否处于SDOWN状态以及Slave的配置优先级等。 - 晋升Slave为Master:
选定的 Slave 将接收到 SLAVEOF NO ONE 命令使其成为新的 Master 节点。 - 配置其他Slave节点:
其他 Slave 节点将被重新配置,使它们复制新的 Master 节点。这是通过向它们发送 SLAVEOF 新Master的IP和端口号实现的。 - 更新Sentinel配置:
所有 Sentinel 节点将更新它们的配置,将新晋升的节点作为当前的 Master。 - 通知客户端:
Sentinel 可以配置为在故障转移完成后通知客户端应用程序。这可以通过发布/订阅机制或者配置特定的通知脚本来完成。
哨兵选主的命令
SENTINEL get-master-addr-by-name <master-name>
这个命令用于获取当前被监控的 Master 节点的地址。SENTINEL is-master-down-by-addr <ip> <port> <current-epoch> <runid>
Sentinel 节点使用这个命令来确认一个 Master 是否被其他 Sentinel 节点视为下线。这个命令的响应会包含是否所有 Sentinel 节点都同意该 Master 下线的信息。SENTINEL failover <master-name>
这个命令用于手动触发对指定 Master 节点的故障转移。通常情况下,故障转移是自动发生的,但是在特定情况下,管理员可能会选择手动触发这个过程。SENTINEL slaves <master-name>
使用这个命令可以列出所有属于指定 Master 节点的 Slave 节点。这有助于 Sentinel 在选举新的 Master 时做出决策。INFO REPLICATION
这不是 Sentinel 命令,但 Sentinel 会在内部对 Redis 节点使用此命令来检查复制状态,包括各个 Slave 节点与 Master 的偏移量,以决定哪个 Slave 最适合被晋升为新的 Master。SLAVEOF <new-master-ip> <new-master-port>
当 Sentinel 决定晋升一个 Slave 为新的 Master 后,它会向该 Slave 发送SLAVEOF NO ONE
命令,让它成为新的 Master。对于其他 Slave,它们会收到SLAVEOF <new-master-ip> <new-master-port>
命令,以开始复制新的 Master。
Sentinel 如何确定一个 Master 节点是否被其他 Sentinel 节点视为下线?
- 主观下线(Subjective Down):
当一个 Sentinel 节点无法从一个 Master 节点接收到期望的 PONG 响应,超出了配置的超时时间,它会认为这个 Master 节点是主观下线(SDOWN)。这个状态是“主观”的,因为它只基于当前 Sentinel 的观察。 - 询问其他 Sentinel 节点:
当 Sentinel 节点将一个 Master 标记为 SDOWN 后,它会开始询问其他 Sentinel 节点它们是否也认为该 Master 节点是不可达的。它使用SENTINEL is-master-down-by-addr
命令来执行这个询问操作。 - 客观下线(Objective Down):
如果足够多的 Sentinel 节点(达到配置的 quorum 数量)回复确认它们也观察到了相同的 Master 不可达状态,那么该 Master 节点会被标记为客观下线(ODOWN)。这个状态是“客观”的,因为它是基于多个 Sentinel 节点的共识。 - 选举领导 Sentinel:
一旦一个 Master 被标记为 ODOWN,Sentinel 节点会通过一个选举过程来选出一个领导 Sentinel,该领导节点负责协调故障转移过程。 - 开始故障转移:
领导 Sentinel 开始故障转移过程,包括选举新的 Master 节点,并指示其他 Sentinel 节点和 Redis 节点更新他们的配置以识别新的 Master。
Redis Sentinel 和 Redis Cluster 都使用一种形式的 Gossip 协议来交换信息,但它们在细节上有所不同,特别是在故障检测和处理方面。
Redis Sentinel 的 Gossip 协议
Redis Sentinel 使用 Gossip-like 的通信机制来交换关于主观下线状态的信息。Sentinel 节点之间不断地交换关于它们监控的 Redis 服务器(Master 和 Slaves)的状态信息。
- 故障检测:每个 Sentinel 节点独立地检测 Master 或 Slave 的可达性。当一个 Sentinel 节点认为一个 Master 节点主观下线时,它会询问其他 Sentinel 节点他们是否也认为同样的 Master 下线了。
- 共识构建:当超过配置的 quorum 数量的 Sentinel 节点同意某个 Master 主观下线时,该 Master 节点会被标记为客观下线,并且将开始故障转移流程。
Sentinel 的 Gossip-like 机制主要用来达成是否进行故障转移的共识,而不是用来广播状态信息。
Redis Cluster 的 Gossip 协议
Redis Cluster 使用真正的 Gossip 协议来维护集群元数据的一致性和集群成员之间的通信。
- 状态交换:Redis Cluster 中的每个节点定期向其他节点发送 Gossip 信息,这包括它们自己的状态和它们知道的其他节点的状态。
- 故障检测:如果一个节点在指定的时间段内没有响应,则发送节点会标记该节点为 PFAIL(可能失败)。当足够多的节点都报告一个节点 PFAIL 时,该节点会被标记为 FAIL。
- 集群拓扑维护:Gossip 消息还包含了集群配置的更改,如新节点的加入、现有节点的移除或者 slots 的重新分配。这些信息帮助集群成员维护当前的集群状态。
Redis Cluster 的 Gossip 协议涉及更多的节点状态和集群拓扑信息的交换,用于维护整个集群的健康和元数据的一致性。
区别总结
Redis Sentinel 的通信机制更偏向于监控和故障转移决策,而 Redis Cluster 的 Gossip 协议更全面,涵盖了节点状态交换、故障检测、配置更新等多个方面。Sentinel 聚焦于高可用性,主要用于故障检测和触发自动故障转移,而 Redis Cluster 的 Gossip 协议是为了维护分布式数据库的整体健康和一致性。
redis Cluster 故障转移过程
- 故障检测:
每个 Redis 节点都会定期向其他节点发送 PING 消息,并期待 PONG 响应。如果一个 Master 节点在指定的超时时间内没有响应,则会被认为是疑似失败(PFAIL)。 - 故障声明:
当足够多的节点(至少需要集群节点的半数加一)都认为某个 Master 节点已经失败(即收到了关于该 Master 的 PFAIL 消息),这个 Master 就会被标记为失败(FAIL)。 - Slave 观察:
与被标记为 FAIL 的 Master 节点相关联的 Slave 节点会持续观察这个状态,并在 Master 被标记为 FAIL 一段时间后(这个时间可以配置),开始选举过程。这个延迟时间是为了确保所有的 Slave 都有机会接收到关于 Master 故障的消息。 - Slave 选举:
当选举开始时,每个 Slave 都会评估自己是否有资格成为新的 Master。这包括检查自己的数据是否足够新,以及是否有足够的连接到其他节点。最有资格的 Slave 会被选举为新的 Master。 - 晋升为 Master:
选出的 Slave 会执行 CLUSTER FAILOVER 命令,将自己晋升为 Master。 - 配置更新:
一旦 Slave 节点晋升为 Master,集群的状态会更新。其他节点会感知到新 Master 的存在,并且开始接受来自它的命令。原 Master 的其他 Slave 节点(如果有的话)会重新配置自己,开始复制新的 Master。 - 客户端重定向:
客户端连接到集群时,如果尝试向失败的 Master 节点发送命令,它们将会收到重定向错误,指示它们连接到新的 Master 节点。 - 持久化新配置:
新的配置将被持久化到节点的磁盘上,以便在重启后仍能记住新的 Master。
Sentinel适用于那些需要故障转移但不需要数据分片的场景,Redis Cluster适用于那些需要自动数据分片和高可用性的场景。
搭建 Redis 集群的详细步骤
准备工作
- 获取 Redis:确保你有 Redis 的最新稳定版本,因为集群功能是在 Redis 3.0 及以上版本提供的。
- 服务器准备:至少需要三个 Redis 节点来搭建一个基本的集群,每个节点运行在不同的服务器或者虚拟机上。为了高可用性,你可能需要六个节点,三个作为主节点,另外三个作为相应的从节点。
配置 Redis 实例
对于每个 Redis 实例,你需要进行以下配置:
- redis.conf:对每个 Redis 实例创建一个配置文件
redis.conf
,并设置以下参数:port <port>
:设置每个 Redis 实例监听的端口号。cluster-enabled yes
:启用集群模式。cluster-config-file nodes-<port>.conf
:指定集群节点的配置文件,它会被 Redis 自动维护。cluster-node-timeout <milliseconds>
:设置节点超时时间,这会影响到故障检测的灵敏度。appendonly yes
:启用 AOF 持久化模式。appendfilename "appendonly-<port>.aof"
:为每个实例指定不同的 AOF 文件。
- 启动实例:为每个 Redis 实例指定配置文件并启动:
sh
1 | redis-server /path/to/redis.conf |
创建 Redis 集群
使用 Redis 的 redis-cli
工具创建集群:
- 创建集群:使用
redis-cli
的--cluster create
命令,指定所有 Redis 实例的 IP 地址和端口号,以及--cluster-replicas
参数来指定每个主节点的从节点数量。例如,如果你有六个节点,命令可能如下:
sh
1 | redis-cli --cluster create 192.168.1.1:7000 192.168.1.2:7000 192.168.1.3:7000 \ |
这个命令会创建一个集群,其中包含三个主节点和三个从节点。
- 检查集群:使用
redis-cli --cluster check <IP>:<port>
命令来检查集群状态。
测试集群
- 数据操作:尝试在集群上执行一些基本的 Redis 命令,比如
SET
和GET
,来验证数据能够被正确分配到不同的节点。 - 集群信息:可以在任何节点上运行
CLUSTER INFO
和CLUSTER NODES
命令来获取集群的状态和节点信息。
监控和维护
- 监控:使用
redis-cli
或其他管理工具监控集群状态和性能。 - 维护:对于集群的扩展和维护,你可能需要使用
redis-cli --cluster
命令来添加或移除节点。
这些步骤提供了搭建基本 Redis 集群的框架。在实际部署时,你可能需要考虑额外的因素,如网络安全、持久化策略、备份和灾难恢复计划等。确保在生产环境中进行充分的测试,以验证集群的性能和容错能力。
搭建redis sentinel的步骤
. 安装 Redis
确保你已经安装了 Redis。Redis Sentinel 是 Redis 的一部分,自 Redis 2.8 版本起就已经包含了 Sentinel 功能。
2. 配置 Redis 主节点和从节点
在你配置 Sentinel 之前,确保你的 Redis 主节点和从节点已经按照正常的方式配置好并运行。
主
1 | bind 0.0.0.0 |
从
1 | bind 0.0.0.0 |
1 | redis-cli |
- 在复制过程中,从节点是只读的。所有写命令必须在主节点上执行。
- 确保所有节点的配置文件和持久化策略(RDB 或 AOF)是一致的,以避免数据不一致。
- 为了高可用性和容错,可以配置多个从节点。
- 从节点在启动时会完整同步主节点的数据,这可能会占用大量带宽和时间,具体取决于数据量的大小。
- 如果需要,可以配置 Redis Sentinel 来监控主从节点,并在主节点失效时自动进行故障转移。
3. 配置 Redis Sentinel
对于每个 Sentinel 实例,你需要创建一个配置文件。通常,这个配置文件被命名为 sentinel.conf
。以下是 sentinel.conf
文件中的一些基础配置项:
sh
1 | sentinel monitor mymaster 127.0.0.1 6379 2 |
mymaster
是你监控的 Redis 主节点的名称。127.0.0.1 6379
是主节点的 IP 地址和端口号。2
是要求的最小 Sentinel 投票数,用于执行故障转移。down-after-milliseconds
配置 Sentinel 多久没有收到主节点的响应时,就认为该节点是下线的。parallel-syncs
配置在故障转移过程中,最多有多少个从节点同时进行同步。failover-timeout
配置故障转移超时的时间。
这些配置项是 Sentinel 的基本设置。你可能需要根据实际情况调整配置参数。
4. 启动 Redis Sentinel
一旦配置好了 sentinel.conf
文件,就可以启动 Sentinel 实例了。启动 Sentinel 的命令如下:
sh
1 | redis-sentinel /path/to/sentinel.conf |
你需要至少三个 Sentinel 实例来形成一个容错系统,这样即使一个 Sentinel 实例出现问题,集群仍然可以正常运行。
5. 验证 Sentinel 状态
启动 Sentinel 后,可以使用 Redis 命令行工具 redis-cli
来检查 Sentinel 状态和配置。
sh
1 | redis-cli -p <sentinel-port> INFO |
6. 测试 Sentinel 故障转移
一旦 Sentinel 启动,你可以通过停止 Redis 主节点的服务,来测试 Sentinel 是否会自动将从节点提升为新的主节点。
7. 调整和优化配置(可选)
根据实际运营中的观察和需求,你可能需要调整 Sentinel 的配置参数,以优化性能和响应时间。
8. 设置持久化和日志记录(可选)
在生产环境中,可能还需要配置 Sentinel 的持久化和日志记录,以便于故障排查和系统的稳定运行。
注意事项
- 确保 Sentinel 的配置文件对于所有 Sentinel 实例都是可访问的,且每个 Sentinel 实例都有其唯一的配置文件。
- Sentinel 默认使用 26379 端口,确保这个端口在防火墙上是开放的。
- Sentinel 集群中的节点数量应为奇数,以避免脑裂(split-brain)问题。
- 在生产环境中,Sentinel 应该分布在不同的物理服务器上,以提供高可用性。
搭建完成后,Redis Sentinel 会监控主从节点的状态,并在主节点不可用时自动进行故障转移,将一个从节点提升为新的主节点。这样可以确保 Redis 的高可用性。
- 写命令:在故障转移过程中,集群暂时没有主节点,因此新的写命令会被拒绝。应用程序在这种情况下可能会收到错误,应当实现重试逻辑来处理这种暂时的写入失败。
- 读命令:如果应用程序只从主节点读取,那么在没有主节点的情况下,读命令也会失败。如果应用程序配置为可以从从节点读取,那么读命令可以继续被处理,前提是从节点上的数据满足应用程序的需求(即从节点的数据足够新)。
Redis Sentinel
- 自动重新连接:大多数 Redis 客户端库支持自动重新连接机制。当客户端检测到主节点不可用时,它会尝试重新连接。
- 订阅 Sentinel 通知:客户端可以订阅 Sentinel 的
+switch-master
事件,当 Sentinel 完成故障转移后,客户端会收到新主节点的地址。客户端可以根据这个地址更新自己的配置并重新连接。 - 查询 Sentinel:在故障转移期间,如果客户端不能写入数据,它可以定期(例如每隔几秒)向 Sentinel 查询当前的主节点地址。一旦 Sentinel 返回新主节点的地址,客户端就可以使用这个地址来恢复写操作。
Redis Cluster
- 自动重定向:Redis Cluster 客户端可以处理特殊的错误代码
-MOVED
和-ASK
,这些错误代码表明客户端需要向集群中的其他节点重新发起请求。客户端库通常会自动处理这些重定向。 - 更新集群状态:如果主节点发生变化,客户端会收到一个
-MOVED
错误。客户端库会使用这个信息来请求集群当前的状态,并更新本地的节点映射。 - 周期性刷新:一些客户端库可能会定期刷新它们对集群状态的认知,即使没有收到
-MOVED
或-ASK
重定向错误。
通用步骤
- 捕获异常:在客户端代码中,捕获连接异常或写入失败的情况,并尝试重新连接或重试写操作。
- 重试逻辑:实现重试逻辑以处理暂时的网络分区或节点宕机。在重试之前,可能需要等待一个合理的时间间隔,以给故障转移足够的时间完成。
- 配置更新:在某些情况下,可能需要手动更新客户端配置文件中的主节点地址。
正确配置的客户端在 Redis Sentinel 或 Redis Cluster 完成选主后,通常能够无需人工干预,自动恢复写操作。然而,不同的客户端库可能有不同的行为和配置选项,因此开发者应该参阅所使用的客户端库的文档,以确保最佳实践被遵循。
集群状态出现问题时,如何进行故障定位
集群状态检查:
- 使用
redis-cli
连接到集群中的任何节点,并执行CLUSTER INFO
命令查看集群的总体状态。 - 执行
CLUSTER NODES
或CLUSTER SLOTS
命令来查看每个节点的状态和数据槽(slot)分配情况。
- 使用
配置一致性检查:
- 检查所有节点的配置文件,确保
cluster-enabled
被设置为yes
,并且所有节点使用相同的cluster-config-file
和cluster-node-timeout
参数。
性能指标分析:
- 使用
INFO
命令查看性能相关的指标,如命令处理速率、网络输入/输出、已连接客户端数等。
- 检查所有节点的配置文件,确保
你知道哪些垃圾回收器
在Java中,垃圾回收器(Garbage Collector, GC)是负责回收无用对象内存的一部分。垃圾回收器的选择对于Java应用程序的性能有显著影响。不同的垃圾回收器适用于不同类型的应用和工作负荷。以下是截至目前Java虚拟机(JVM)提供的一些主要垃圾回收器:
- Serial GC:这是最简单的GC,适用于单线程环境。它对堆内存进行单线程的垃圾回收,适合于小型应用和单处理器机器。
- Parallel GC(也称为Throughput Collector):Parallel GC使用多线程进行垃圾回收,主要关注提高吞吐量。它适用于需要大量内存和多CPU的多线程应用程序。
- Concurrent Mark Sweep (CMS) GC:CMS GC是一种以最小化应用程序暂停时间为目标的垃圾回收器,适用于互联网或者服务端应用。它在垃圾回收时尝试最小化停顿,但是可能会产生较多的内存碎片。
- **G1 GC (Garbage-First Collector)**:G1 GC是一种面向服务端应用的垃圾回收器,旨在兼顾高吞吐量和低延迟。G1将堆划分为多个区域,并优先回收垃圾最多的区域,逐渐保持整个堆的垃圾回收。
- **ZGC (Z Garbage Collector)**:ZGC是一种可伸缩的低延迟垃圾回收器,旨在针对大堆内存(几十GB或更多)的系统提供低延迟的GC停顿(目标在10ms以内)。
- Shenandoah GC:与ZGC类似,Shenandoah GC也是一种低停顿时间的垃圾回收器,采用了先进的算法来减少GC引起的停顿时间,特别适合对响应时间敏感的应用。
- Epsilon GC(No-Op GC):这是一个实验性的垃圾回收器,它基本上不进行任何回收。它可以用来测试最佳性能场景或内存压力测试。
每种垃圾回收器都有其特点和使用场景,选择合适的垃圾回收器可以帮助提高Java应用的性能和响应能力。随着Java版本的更新,可能会引入新的GC算法或对现有GC进行改进。
Concurrent Mark Sweep (CMS) GC介绍下回收过程及回收原理
Concurrent Mark Sweep (CMS) GC是一种以最小化应用程序暂停时间为目标的垃圾回收器,主要用于需要快速响应时间的应用,比如Web服务器和交互式应用程序。CMS的设计目的是尽可能减少应用程序的停顿时间。
CMS回收过程
CMS GC的回收过程主要分为以下几个阶段:
- 初始标记(Initial Mark):
- 这是一个STW(Stop-The-World)阶段,意味着在这个阶段所有的应用线程都会暂停。
- 在此阶段,GC会标记所有从根对象直接可达的对象。这包括全局静态对象和活跃线程的局部变量等。
- 并发标记(Concurrent Mark):
- 应用线程和GC线程同时运行,没有停顿。
- 在此阶段,GC遍历初始标记阶段找到的对象,并标记所有从这些对象可达的对象。
- 预清理(Pre-Cleanup):
- 这个阶段是为了准备最终清理阶段,可能会有短暂的停顿,但通常比初始标记阶段的要短。
- 该阶段主要是为了处理并发标记阶段期间因应用程序继续运行而产生的变动。
- 最终标记(Remark):
- 也是一个STW阶段,但CMS GC尝试尽量缩短这个阶段的时间。
- 该阶段完成了剩余的标记工作,包括处理并发标记阶段遗留下来的变动。
- 并发清除(Concurrent Sweep):
- 应用线程和GC线程同时运行,没有停顿。
- GC遍历堆内存,清理掉未被标记的对象,回收它们占用的内存。
CMS回收原理
CMS的主要原理是“标记-清除”(Mark-Sweep)算法。在“标记”阶段,它通过遍历对象图来标记所有活着的对象。在“清除”阶段,它遍历堆内存,清除掉未被标记的对象,释放它们占用的空间。
CMS的特点是大部分工作和应用线程同时进行,从而减少停顿时间。但是,CMS也存在一些缺点,比如在清除阶段不会压缩或整理内存,这可能会导致内存碎片化问题。此外,在高负载情况下,CMS GC可能会由于无法及时清除垃圾而导致“并发模式失败”,这时候它会退化为Serial Old GC来处理剩余的垃圾收集工作,这会引起较长时间的停顿。
那你知道三色标记和 cms关系吗
是的,三色标记是一种垃圾回收算法中用于标记对象的一种方法,它在CMS(Concurrent Mark Sweep)垃圾回收器中得到了应用。三色标记算法涉及将对象标记为三种颜色:
- 白色:表示对象尚未被访问或标记。在算法开始时,所有对象都被视为白色。
- 灰色:表示对象已经被标记为存活,但是该对象引用的其他对象尚未被完全检查。换句话说,如果一个对象是灰色的,意味着垃圾回收器已经发现了这个对象,但还没有检查它引用的其他对象。
- 黑色:表示该对象及其所有引用的对象都已经被访问和标记。黑色对象是安全的,因为它们已经被确认为存活对象,并且不会在接下来的标记过程中被清除。
在CMS的并发标记阶段,垃圾回收器遍历对象图,按照三色标记的规则来标记对象。初始标记阶段会将直接可达的对象标记为灰色(从白色变为灰色),然后在并发标记阶段中,它继续遍历这些灰色对象,将它们引用的对象标记为灰色,同时将已经检查过的对象标记为黑色。整个过程中,应用线程依然在运行,这可能会改变对象之间的引用关系。
为了处理在并发标记过程中因应用程序继续运行而产生的变动(即对象引用的变化),CMS使用了一种叫做“写入屏障”(Write Barrier)的机制来记录这些改动。这样,在最终标记阶段,垃圾回收器可以快速地重新检查这些变化,确保所有存活的对象都被正确标记。
三色标记算法在CMS中的应用是为了能在应用程序运行的同时进行垃圾回收,减小停顿时间。不过,如果在并发标记过程中无法跟上对象引用的变化速度,可能会引起“并发模式失败”,导致GC不得不停止应用线程来完成垃圾回收,从而产生较长的停顿时间
CMS中的并发标记阶段如何处理对象引用的变化?
在CMS垃圾回收器的并发标记阶段,垃圾回收器需要记录所有存活的对象。由于该阶段是与应用程序的执行并发进行的,因此对象的引用关系可能会在标记过程中发生变化。为了正确处理这些变化,CMS采用了以下机制:
写入屏障(Write Barrier)
写入屏障是一种机制,用于监控和记录对象引用的变化。当应用程序更改一个对象的引用时,写入屏障会触发并记录这次写操作。这些记录会在垃圾回收过程中被检查,以确保所有的存活对象都被正确标记。
快照式写入屏障(Snapshot At The Beginning, SATB)
CMS使用的是一种名为快照式写入屏障的特殊机制。在并发标记的初始阶段(也就是并发标记开始之前的初始标记阶段),它会记录下当前所有对象的状态。然后,在并发标记过程中,如果一个应用程序线程修改了对象的引用,写入屏障将记录下修改前的引用。这样,即使对象的引用被修改了,垃圾回收器也能基于最初的快照来找到所有的存活对象。
最终标记(Remark)
尽管CMS的大部分标记工作是并发进行的,但是在并发标记阶段结束时,它需要一个短暂的STW(Stop The World)暂停来完成标记。这个阶段被称为最终标记(Remark)阶段。在这个阶段,垃圾回收器处理所有在并发阶段收集的信息,这主要包括写入屏障记录的对象引用变化。这确保了所有因应用程序的运行而变化的对象引用都得到处理
HTTP/HTTPS 什么区别?HTTP 特性有哪些
HTTP(超文本传输协议)和HTTPS(HTTP Secure)之间的主要区别在于安全性。以下是它们之间的关键差异:
HTTP和HTTPS的区别:
- 加密:
- HTTP:不使用加密,数据以明文形式传输,容易被第三方截获和篡改。
- HTTPS:通过SSL/TLS协议提供了数据传输的加密,保护了数据的安全性,防止了数据在传输过程中被截获和篡改。
- 端口:
- HTTP:默认使用端口80。
- HTTPS:默认使用端口443。
- 性能:
- HTTP:由于没有加密处理的开销,性能上略优于HTTPS。
- HTTPS:由于加密和解密过程需要额外的计算,可能会稍微降低性能。
- URL前缀:
- HTTP:网址以
http://
开头。 - HTTPS:网址以
https://
开头,表示安全链接。
- HTTP:网址以
- 证书:
- HTTP:不需要证书。
- HTTPS:需要SSL/TLS证书,这个证书由证书颁发机构(CA)签发,用于验证网站的身份。
HTTP的特性:
- 简单快速:客户端向服务器请求服务时,只需传送请求方法和路径。
- 灵活:HTTP允许传输任意类型的数据对象。正在传输的类型由
Content-Type
头部指定。 - 无状态:HTTP协议是无状态的,即服务器不会记住任何客户端信息。为了解决这个问题,引入了Cookie技术。
- 无连接:默认情况下,HTTP使用“无连接”的方式,即每次连接只处理一个请求。服务器处理完客户端的请求并收到客户端的应答后,即断开连接。但现代HTTP协议支持持久连接来优化这个过程。
- 支持B/S模型:HTTP协议定义了请求和响应的结构,使其成为浏览器/服务器模型中的理想选择。
HTTP由于其无状态和无连接的特性,虽然在某些情况下可以提高传输速度,但也带来了如需保持连接状态时的不便。HTTPS则在HTTP的基础上加入SSL/TLS,提供了数据的加密传输和完整性校验等安全特性,适用于需要保护数据安全的场景,如网上银行和在线购物。
TTP相对于HTTPS的主要性能优势在于其传输过程中不涉及数据的加密和解密。这种差异导致在某些情况下,HTTP的传输速度和响应时间可能会稍微快于HTTPS。具体来说,HTTP的性能优势体现在以下几个方面:
- 无加密开销:HTTP通信不需要加密数据,这意味着不需要进行加密和解密的计算,从而节省了处理时间和CPU资源。
- 简化的握手过程:在建立一个HTTPS连接之前,客户端和服务器之间需要进行一个TLS握手过程,以协商加密参数,并验证服务器的证书。这个握手过程涉及多个往返通信和密钥的计算,增加了延迟。HTTP不需要这个握手过程。
- 无需处理证书:HTTPS需要使用证书来验证服务器的身份,这个过程包括了证书的传输、验证和可能的证书撤销检查。HTTP不涉及证书处理,进一步减少了连接建立时的时间。
- 较低的数据传输量:由于HTTPS需要额外的头信息和加密后的数据通常会比原始数据略大,因此HTTPS可能需要传输更多的数据。HTTP没有这些额外的数据负担。
- 更少的服务器资源消耗:因为HTTPS需要服务器进行加密处理,所以服务器端的CPU和内存资源消耗相对较大。对于HTTP来说,服务器端的资源消耗较少,能够处理更多的并发请求。
然而,随着近年来计算机处理能力的提升和现代浏览器以及服务器对HTTPS的优化,这些性能差异已经大大减小。现在,对于大多数现代网站和Web应用程序而言,推荐使用HTTPS来确保数据的安全性,尤其是隐私数据和敏感操作。实际上,许多现代Web技术,如HTTP/2,都要求使用HTTPS来实现性能改进。此外,搜索引擎也倾向于更高地排名使用HTTPS的网站,使得HTTPS成为了一种Web安全的最佳实践。
HTTPS加密过程涉及到对称加密和非对称加密两种机制,并且使用了TLS(传输层安全协议)或其前身SSL(安全套接层协议)。整个过程主要包括以下几个步骤:
- 客户端发起连接:
客户端(通常是Web浏览器)向服务器发送一个HTTPS请求,这通常是通过在URL中使用https://
作为前缀来实现的。 - 服务器响应:
服务器响应客户端的请求,并提供其TLS证书。这个证书包含了服务器的公钥以及由证书颁发机构(CA)对服务器身份的验证。 - 证书验证:
客户端验证服务器证书的合法性。它会检查证书是否由受信任的CA签发,是否在有效期内,以及证书中的域名是否与正在访问的服务器域名匹配。 - 密钥交换:
客户端使用服务器的公钥来加密一个随机生成的对称密钥(会话密钥),并将加密后的会话密钥发送给服务器。服务器使用其私钥来解密这个信息,从而获得会话密钥。这个过程利用了非对称加密。 - 对称加密通信:
一旦双方都有了会话密钥,它们就使用这个密钥来对通信内容进行对称加密。从这一点开始,客户端和服务器之间发送的所有数据都将使用这个会话密钥进行加密和解密。这意味着即使数据在传输过程中被截获,没有密钥的第三方也无法解密数据内容。 - 安全通信:
客户端和服务器现在可以开始进行安全的通信。发送的每个消息都会被加密,接收端收到消息后使用会话密钥进行解密,这样就保证了数据的机密性和完整性。
这个过程不仅保证了数据在客户端和服务器之间传输的安全性,还通过证书和密钥交换过程确保了通信双方的身份验证。这种机制使得HTTPS非常适合保护用户在互联网上的隐私和数据安全。
##
MySQL 聚集索引 (主键索引) 和非聚集索引 (辅助索引/普通索引) 的区别
MySQL中的聚集索引和非聚集索引是两种不同类型的索引,它们在数据存储和查询优化方面有各自的特点和用途。
聚集索引(Clustered Index):
- 数据存储:在聚集索引中,表中行的物理顺序和键值的逻辑(索引)顺序相同。实际的数据行直接存储在索引的叶子节点上。
- 主键索引:在MySQL的InnoDB存储引擎中,聚集索引通常是主键索引,表中的数据按照主键的顺序存储。
- 唯一性:一个表只能有一个聚集索引,因为数据只能以一种顺序物理存储。
- 访问速度:由于数据行和索引是紧密绑定的,聚集索引通常可以提供最快的查询速度,尤其是对主键的查询和范围查询。
非聚集索引(Non-Clustered Index):
- 数据存储:非聚集索引有一个单独的索引结构,索引项包含索引键值和指向数据行的指针,而不是数据本身。
- 辅助索引:这些索引是对表中非主键列的索引,可以有多个非聚集索引。
- 额外的查找步骤:查询非聚集索引通常需要两个步骤:首先在索引中查找键值,然后通过索引中的指针找到实际的数据行。
- 覆盖索引:如果一个索引包含了查询所需的所有列,那么它可以直接返回结果,而无需回表查询数据行,这被称为“覆盖索引”。
区别总结:
- 物理存储:聚集索引决定了表数据的物理顺序,而非聚集索引则是一个单独的结构,不影响数据的物理存储。
- 索引数量:一个表只能有一个聚集索引,但可以有多个非聚集索引。
- 查找效率:聚集索引通常在查找速度上更优,特别是进行主键查找和范围查找时。
- 空间使用:非聚集索引可能会占用更多空间,因为它们需要存储额外的指针信息。
- 插入速度:聚集索引可能会使插入操作变慢,特别是在插入值导致页分裂时。
- 更新操作:如果更新操作涉及聚集索引键的变动,可能会导致数据行移动,这比更新非聚集索引更耗时。
选择合适的索引类型和设计索引策略对于数据库性能优化至关重要。通常,聚集索引用于频繁查询的主键列,而非聚集索引用于优化那些涉及非主键列的查询。
常见排序算法及其时间复杂度、各种排序算法对比
各种排序算法的比较:
- 时间复杂度:对于小数据集,冒泡排序、选择排序和插入排序可能足够好,但对于大数据集,归并排序、快速排序和堆排序更优,因为它们提供了 O(n log n) 的平均时间复杂度。
- 空间复杂度:对于空间敏感的应用,原地排序算法如冒泡排序、选择排序、插入排序和堆排序更有优势,因为它们的空间复杂度是 O(1)。
- 稳定性:稳定的排序算法可以保持相同元素的相对顺序不变。归并排序、插入排序、计数排序和基数排序在这方面表现良好。
- 适用场景:计数排序和基数排序适用于数字范围较小或者可以转换为整数的场景。归并排序适用于需要稳定排序的场景。快速排序在大多数情况下效率很高,但在极端情况下可能会退化到 O(n^2)。
冒泡算法
- 最好时间复杂度: O(n)
- 平均时间复杂度: O(n^2)
- 最坏时间复杂度: O(n^2)
- 空间复杂度: O(1)
- 稳定性: 稳定
1 | public void bubbleSort(int[] arr) { |
选择排序
- 最好时间复杂度: O(n^2)
- 平均时间复杂度: O(n^2)
- 最坏时间复杂度: O(n^2)
- 空间复杂度: O(1)
- 稳定性: 不稳定
1 | public class SelectionSort { |
插入排序(Insertion Sort)
- 最好时间复杂度: O(n)
- 平均时间复杂度: O(n^2)
- 最坏时间复杂度: O(n^2)
- 空间复杂度: O(1)
- 稳定性: 稳定
1 | public class InsertionSortExample { |
归并排序(Merge Sort)
- 最好时间复杂度: O(n log n)
- 平均时间复杂度: O(n log n)
- 最坏时间复杂度: O(n log n)
- 空间复杂度: O(n)
- 稳定性: 稳定
1 | public class MergeSort { |
归并排序是一种分而治之的算法,通过递归地将数组分成两半进行排序,然后合并两个有序的子列表来构建最终的排序列表。它包括两个主要的步骤:
- 分解步骤:将当前序列分成两个子序列,递归地对这两个子序列进行归并排序。
- 合并步骤:将两个排序好的子序列合并成一个最终的排序序列。
快速排序(Quick Sort)
- 最好时间复杂度: O(n log n)
- 平均时间复杂度: O(n log n)
- 最坏时间复杂度: O(n^2)
- 空间复杂度: O(log n)
- 稳定性: 不稳定
1 | public class QuickSort { |
速排序是一个高效的排序算法,通过一个称为“分区”的操作来将数组分为两个(可能不等大小的)子数组,然后递归地对这两个子数组进行快速排序。分区操作选择一个“基准”元素,并重新排列数组中的元素,使得所有比基准小的元素都在基准之前,而所有比基准大的元素都在基准之后。这个分区操作的结果就是基准元素所在的最终位置
堆排序(Heap Sort)
- 最好时间复杂度: O(n log n)
- 平均时间复杂度: O(n log n)
- 最坏时间复杂度: O(n log n)
- 空间复杂度: O(1)
- 稳定性: 不稳定
1 | import java.util.ArrayList; |
计数排序(Counting Sort)
- 最好时间复杂度: O(n+k)
- 平均时间复杂度: O(n+k)
- 最坏时间复杂度: O(n+k)
- 空间复杂度: O(k)
- 稳定性: 稳定
1 | public class CountingSort { |
计数排序中的计数数组(Count Array)起着至关重要的作用。它用于统计每个不同的元素在待排序数组中出现的次数。下面是计数数组工作原理的详细解释:
- 确定数值范围:首先,我们需要遍历待排序的数组来找出数组中的最大值和最小值。这两个值能够帮助我们确定计数数组的大小,即最大值和最小值之间的范围。
- 创建计数数组:根据最大值和最小值的差值(加上1,以包含最大值本身),我们创建一个计数数组。这个数组的每个索引对应原始数组中可能出现的一个值。
- 计数:接着,我们再次遍历待排序数组,对于数组中的每个元素
x
,我们将计数数组中对应于x - min
的值增加1。这样做是因为计数数组的索引是从0开始的,而我们的数值可能不是从0开始。通过减去最小值min
,我们确保了计数数组的索引正确对应于原始数组的元素。 - 排序:最后,我们可以通过遍历计数数组来重建排序后的数组。对于计数数组中的每个索引,如果其对应的计数大于0,我们就将对应的元素(索引值加上最小值
min
)添加到排序后的数组中,并且次数为计数数组中记录的次数。
计数数组的作用就是作为一个中介,记录原始数组中各个元素的分布情况。因为计数排序不是基于比较的排序算法,它通过这种统计元素出现次数的方式来实现对原始数组的排序。这也是计数排序可以在线性时间内完成排序的关键所在,尤其适用于元素值分布在一个较小范围内的数组。
基数排序(Radix Sort)
- 最好时间复杂度: O(nk)
- 平均时间复杂度: O(nk)
- 最坏时间复杂度: O(nk)
- 空间复杂度: O(n+k)
- 稳定性: 稳定
1 | 看不懂 |
在这个实现中:
radixSort
方法确定了数组中的最大数,并决定了循环的次数(即最大数的位数)。- 对于数组中的每个元素,
countingSortByDigit
方法根据当前位(由exp
确定)进行计数排序。 - 计数排序不是直接对原数组进行操作,而是使用了一个临时数组
output
来存放排序的结果。 - 通过迭代每个位,
radixSort
最终能够得到一个完全排序的数组。
请注意,上述代码假设我们正在排序的数字都是非负的。如果数组中包含负数,那么基数排序的实现需要额外的逻辑来处理正负数的情况。
希尔排序(Shell Sort)
- 最好时间复杂度: O(n log n)
- 平均时间复杂度: O(n(log n)^2)
- 最坏时间复杂度: O(n(log n)^2)
- 空间复杂度: O(1)
- 稳定性: 不稳定
1 |
|
在这段代码中:
- 我们首先设置一个初始增量
gap
,通常为数组长度的一半。 - 然后在外层循环中,逐渐减小
gap
的值,每次都将gap
减半,直到gap
为1。 - 对于每个给定的
gap
值,我们从下标为gap
的元素开始,通过内层循环遍历数组。 - 我们将当前元素保存在
current
中,并寻找它在已排序序列中的正确位置。 - 如果遇到一个大于
current
的元素,我们就将它向后移动gap
位。当找到适合current
的位置时,我们将其插入。 - 最终,当
gap
减小到1时,整个数组将被完整地排序。
希尔排序比传统的插入排序有更好的性能,因为它允许交换距离较远的元素,这样可以帮助减少总的比较和移动次数。
为什么会出现OOM呢?一般都是由这些问题引起
- 分配过少:JVM 初始化内存小,业务使用了大量内存;或者不同 JVM 区域分配内存不合理
- 内存泄漏:某一个对象被频繁申请,不用了之后却没有被释放,发生内存泄漏,导致内存耗尽(比如ThreadLocal泄露)****
场景一:堆内存OOM(也叫堆内存溢出)
这是最常见的OOM场景了,发生在JVM试图分配对象空间时,却发现剩余的堆内存不足以存储新对象。
例如我们执行下面的代码,就可以模拟出堆内存OOM的场景
1 |
|
那么当出现线上应用OOM场景时,该如何解决呢?
分析方法通常有两种:
- 类型一:在线分析,属于轻量级的分析:
- 类型二:离线分析,属于重量级的分析:
类型一:在线OOM分析:
在线分析Java OOM(内存溢出)问题,通常涉及到监控运行中的Java应用,捕获内存溢出时的信息,分析堆转储(Heap Dump)文件,以及利用一些工具和命令来辅助定位问题。下面是一套详细的分析流程和命令,帮助你在线分析和解决Java OOM问题:
1、启用JVM参数以捕获Heap Dump
在Java应用启动命令中加入以下JVM参数,以确保在发生OOM时能自动生成堆转储文件:
1 | -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/path/to/heapdump.hprof |
这些参数的作用是:
-XX:+HeapDumpOnOutOfMemoryError
:指示JVM在遇到OOM错误时生成堆转储文件。-XX:HeapDumpPath
:指定堆转储文件的存储路径,可以自定义路径和文件名。
2、实时监控内存使用情况
使用jvisualvm
或jconsole
等工具可以实时监控Java应用的内存使用情况。这些工具可以帮助你了解内存消耗的趋势,从而预测和避免OOM的发生。
- JVisualVM:集成了多个JDK命令行工具,提供了可视化界面,可以监控内存使用、查看线程、分析堆等。
- JConsole:Java监控和管理控制台,用于对JVM中的内存、线程和类等进行监控。
3、分析Heap Dump文件
当应用抛出OOM并且根据上述设置生成了堆转储文件后,使用Heap Dump分析工具来分析这个文件。常用的工具有:
- **Eclipse Memory Analyzer (MAT)**:一个强大的Java堆分析工具,可以帮助识别内存泄露和查看内存消耗情况。
- VisualVM:除了监控功能外,也支持加载和分析Heap Dump文件。
在MAT中打开Heap Dump文件,主要关注以下几点:
- 查找内存中对象的分布,特别是占用内存最多的对象。
- 分析这些对象的引用链,确定是哪部分代码引起的内存泄漏或过度消耗。
- 检查ClassLoader,以确认是否有过多的类被加载导致的元空间(Metaspace)OOM。
4、使用命令行工具
JDK提供了一些命令行工具,如jmap
,可以用来生成Heap Dump文件:
1 | jmap -dump:live,format=b,file=heapdump.hprof <pid> |
其中<pid>
是Java进程的ID。-dump:live
选项表示只转储活动对象,可以减小Heap Dump文件的大小。
5、分析日志和异常信息
最后,不要忽视应用的日志和抛出的异常信息。OOM之前的日志可能会提供一些导致内存溢出的操作或业务逻辑的线索。
类型二:离线OOM分析,这个属于重量级分析
离线分析Java OOM(OutOfMemoryError)通常是在问题发生后,通过分析JVM生成的堆转储(Heap Dump)文件来进行。这个过程涉及到获取堆转储文件、使用分析工具进行深入分析和解读分析结果
1、获取Heap Dump文件
首先,确保你已经有了一个Heap Dump文件。这个文件可能是在JVM遇到OOM时自动生成的(如果启用了-XX:+HeapDumpOnOutOfMemoryError
JVM参数),或者你可以在应用运行期间手动生成:
使用
jmap
命令生成Heap Dump文件:1
jmap -dump:live,format=b,file=/path/to/heapdump.hprof <pid>
其中
<pid>
是Java进程的ID,/path/to/heapdump.hprof
是你希望保存Heap Dump文件的位置。
2、使用Heap Dump分析工具
有了Heap Dump文件后,你需要使用专门的工具来进行分析。以下是一些常用的分析工具:
- **Eclipse Memory Analyzer (MAT)**:非常强大的内存分析工具,能帮助识别内存泄漏和查看内存消耗情况。
- VisualVM:提供了一个可视化界面,可以用来分析Heap Dump文件。
- JVisualVM:随JDK一起提供的工具,也支持加载Heap Dump文件进行分析。
3、分析Heap Dump文件
使用MAT(Eclipse Memory Analyzer)作为示例,分析流程如下:
- 打开Heap Dump文件:启动MAT并打开Heap Dump文件(.hprof)。
- 运行Leak Suspects Report:MAT可以自动生成一个内存泄漏报告(Leak Suspects Report),这个报告会指出可能的内存泄漏路径。
- 分析Dominators Tree:这个视图显示了占用最多内存的对象及其引用。通过它,你可以找到最大的内存消耗者。
- 查看Histogram:对象Histogram列出了所有对象的实例数和总大小,帮助你识别哪种类型的对象占用了最多的内存。
- 检查GC Roots:为了确定对象为什么没有被垃圾回收,可以查看对象到GC Roots的引用链。
- 分析引用链:通过分析对象的引用链,你可以确定是什么持有了这些对象的引用,导致它们无法被回收。
下面给大家提供一份Java应用上线前参考的的JVM配置(内存8G),以后系统上线前可以先配置下JVM,不要啥都不配置就上线了
1 | -Xms6g -Xmx6g (按不同容器,4G及以下建议为50%,6G以上,建议设置为70%) |
场景二:元空间(MetaSpace)OOM
什么是元空间?
Java元空间(Metaspace)是Java虚拟机(JVM)中用于存放类的元数据的区域,从Java 8开始引入,替代了之前的永久代(PermGen)
图中红色箭头所指就是元空间
元空间是方法区在HotSpot JVM
中的实现,方法区主要用于存储类的信息、常量池、方法数据、方法代码等。方法区逻辑上属于堆的一部分,但是为了与堆进行区分,通常又叫“非堆”。
元空间的本质和永久代类似,都是对JVM规范中方法区的实现。
不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。理论上取决于32位/64位系统可虚拟的内存大小,可见也不是无限制的,需要配置参数。
元空间(Metaspace) 垃圾回收,会对僵死的类及类加载器的垃圾回收会进行回收,元空间(Metaspace) 垃圾回收的时机是,在元数据使用达到“MaxMetaspaceSize”参数的设定值时进行。
元空间OOM的现象
JVM 在启动后或者某个时间点开始,MetaSpace 的已使用大小在持续增长,同时每次 GC 也无法释放,调大 MetaSpace 空间也无法彻底解决。
元空间OOM的核心原因:生成了大量动态类
比如:
- 使用大量动态生成类的框架(如某些ORM框架、动态代理技术、热部署工具等)
- 程序代码中大量使用反射,反射在大量使用时,因为使用缓存的原因,会导致ClassLoader和它引用的Class等对象不能被回收
例如下面的生成大量动态代理类的代码示例,则会导致元空间的OOM
1 | // 使用CGLIB动态生成大量类导致元空间溢出 |
元空间(Metaspace) OOM 解决办法:
- 减少程序中反射的大量使用
- 做好熔断限流措施,对应用做好过载保护,比如阿里的sentinel限流熔断中间件
场景三:堆外内存OOM
Java对外内存(Direct Memory)OOM指的是Java直接使用的非堆内存(off-heap memory)耗尽导致的OutOfMemoryError。这部分内存主要用于Java NIO库,允许Java程序以更接近操作系统的方式管理内存,常用于高性能缓存、大型数据处理等场景
例如下面的代码,如何堆外内存太小,就会导致堆外内存的OOM:
1 | // 分配大量直接内存导致OOM |
堆外内存的原因
- 分配过量的直接内存:程序中大量使用DirectByteBuffer等直接内存分配方式,而没有相应的释放机制,导致内存迅速耗尽,常见于NIO、Netty等相关组件。
- 内存泄露:如果分配的直接内存没有被及时释放(例如,ByteBuffer未被回收),就可能发生内存泄露。
- JVM对外内存限制设置不当:通过
-XX:MaxDirectMemorySize
参数控制对外内存大小,如果设置过小,可能无法满足应用需求。
堆外内存OOM的解决方案
- 合理设置对外内存大小:根据应用的实际需求调整
-XX:MaxDirectMemorySize
参数,给予足够的直接内存空间。 - 优化内存使用:减少不必要的直接内存分配,重用DirectByteBuffer等资源。
- 内存泄露排查:使用工具(如VisualVM、JProfiler等)定位和解决内存泄露问题。
- 代码优化:确保使用完直接内存后显式调用
sun.misc.Cleaner.clean()
或通过其他机制释放内存。
进程间通信 (同步) 方式
进程间通信(IPC, Inter-Process Communication)是在不同进程之间传递数据或信号的机制。同步IPC指的是在发送和接收数据时,涉及的进程需要协调它们的工作节奏,即一个进程在等待操作完成时会被阻塞。以下是一些常用的同步IPC方式:
- **管道 (Pipes)**:
管道是最古老的IPC机制之一,它允许一个进程和另一个有父子关系的进程进行通信。数据以字节流的形式单向流动。命名管道(也称为FIFO)则允许不相关的进程通信。 - **消息队列 (Message Queues)**:
消息队列允许不同进程发送和接收消息。进程可以在任何时候发送消息到队列,接收者可以同步或异步地接收消息。 - **信号量 (Semaphores)**:
信号量是一个计数器,用于控制多个进程对共享资源的访问。它主要用于实现进程间的同步。 - **共享内存 (Shared Memory)**:
共享内存是一段能够被多个进程访问的内存区域。进程可以直接读写这段内存,但是需要使用同步机制如信号量来防止竞态条件。 - **套接字 (Sockets)**:
套接字是更为通用的IPC机制,支持网络通信。本地套接字(UNIX套接字)可用于在同一台机器上运行的进程间的同步通信。 - **文件锁 (File Locking)**:
进程可以对文件的特定部分加锁,以防止其他进程同时访问该部分。这是一种同步机制,确保只有一个进程能够同时写入文件。 - **条件变量 (Condition Variables)**:
条件变量通常与互斥锁(mutexes)一起使用,允许进程以阻塞的方式等待特定条件的发生。 - **事件计数器 (Event Counters)**:
事件计数器是用来跟踪和同步特定事件发生次数的同步机制。 - **信号 (Signals)**:
信号是一种限制性的IPC,用于通知接收进程某个事件已发生。尽管信号不是同步机制,但它们可以用来在进程之间同步事件。 - **远程过程调用 (RPC)**:
RPC允许一个进程调用另一个进程的函数,就像调用本地函数一样。RPC通常涉及网络通信,但也可以用于本地进程通信。
使用这些同步IPC机制时,设计良好的同步策略是至关重要的,以避免死锁、竞态条件和资源争用等问题。
操作系统中内存分段和分页使用场景
操作系统中的内存分段和分页是两种内存管理技术,它们都旨在使物理内存的使用更加高效和灵活。这两种技术虽然有不同的特点和使用场景,但在现代操作系统中往往是同时使用的。
内存分段(Segmentation)
分段是一种内存管理方案,它将程序的不同部分划分为不同的段,如代码段、数据段、堆栈段等。
使用场景:
- 逻辑组织:分段根据程序的逻辑结构来组织内存,使得每个段都有一个特定的功能或含义,这有助于程序的模块化和保护。
- 保护和隔离:每个段可以有不同的访问权限,提供内存保护功能,防止程序错误地访问其他段的内存。
- 动态加载和链接:在支持动态加载和链接的系统中,分段可以让不同的程序段独立加载和替换,便于动态更新程序。
内存分页(Paging)
分页是一种内存管理方案,它将物理内存划分为固定大小的块,称为页(page),同时将虚拟内存空间划分为同样大小的页。
使用场景:
- 物理内存管理:分页简化了物理内存的管理,因为所有的页都是同样大小,所以易于跟踪和分配。
- 虚拟内存:分页是虚拟内存实现的基础,它允许非连续的物理内存被映射到连续的虚拟地址空间。
- 内存隔离:每个进程有自己的页表,保证了不同进程间虚拟内存空间的隔离。
- 换入换出:分页支持将数据从磁盘换入到RAM中,以及将不经常访问的内存页交换到磁盘(换出)以释放RAM。
- 内存去碎片化:分页通过将内存划分为固定大小的页,有助于减少内存碎片化
申请内存的步骤
- 请求内存:
程序通过调用操作系统提供的API(例如C语言中的malloc
或C++中的new
)来请求内存。- 生成段:
操作系统将程序的不同部分划分成段,如代码段、数据段、堆段和栈段。- 创建段表:
对于每个段,操作系统在内存中创建一个段表,记录每个段的基地址、长度和访问权限。- 分配页框:
当程序请求的内存无法在当前的空闲内存中得到满足时,操作系统将分配页框(物理内存中的页)。- 更新页表:
操作系统更新页表,将虚拟地址映射到新分配的物理页框。页表保存了虚拟页到物理页框的映射。- 返回内存地址:
请求内存的操作完成后,操作系统返回一个指向申请到的内存的指针。
使用内存的步骤
- 生成逻辑地址:
当程序需要访问内存时,它生成一个逻辑地址(也称为虚拟地址),逻辑地址由段选择子和段内偏移组成。- 段翻译:
系统使用段选择子来在段表中查找对应的段描述符,获取段的基地址、界限和其他属性。- 计算线性地址:
通过将段的基地址与段内偏移相加,操作系统计算出线性地址(虚拟内存中的地址)。- 分页翻译:
线性地址被分解为页号和页内偏移。操作系统使用页号通过当前进程的页表查找对应的物理页框号。- 计算物理地址:
将物理页框号与页内偏移相结合,计算出物理地址。- 访问物理内存:
一旦计算出物理地址,处理器便可访问对应的物理内存位置。
在整个申请和使用内存的过程中,操作系统通过段表和页表来管理内存,并在访问内存时进行地址转换。如果在分页翻译过程中页表项指示的是不在内存中的页(即页面失效),操作系统会从磁盘中加载所需的页到物理内存,这一过程称为页面换入。这种段页式内存管理为操作系统提供了同时实现内存保护、内存共享和虚拟内存的能力。
操作系统什么时候会发生中断
操作系统会在多种情况下发生中断。中断是指处理器暂停当前的任务去响应某个事件的机制,它是操作系统的一个关键特性,用于处理异步事件。中断可以分为硬件中断和软件中断。
硬件中断
硬件中断通常由系统外部的硬件设备触发。常见的硬件中断包括:
- I/O中断:
当输入/输出操作完成时,比如从硬盘读取数据或向打印机发送数据完成时,设备会向处理器发出中断信号。 - 时钟中断:
系统时钟周期性地发出中断,通常用于操作系统的调度程序,实现时间片轮转等功能。 - 外部信号中断:
如用户按下Ctrl+C键终止程序,或者其他外部事件(如电源键)发出信号。 - 硬件故障:
硬件发生错误,如电源故障或内存错误,也会引发中断。
软件中断
软件中断通常由运行在处理器上的程序主动触发。常见的软件中断包括:
- 系统调用:
程序执行系统调用指令(如Linux中的int 0x80
或syscall
指令)请求操作系统服务时会触发中断。 - 异常:
当程序运行出现错误时,如除零错误、无效的内存访问(段错误)或者执行非法指令,处理器会触发异常中断。 - 软件中断指令:
程序中显式包含的中断指令(如int
指令)会直接触发中断。
专门的中断情况
还有一些专门的中断情况,如:
- 中断请求(IRQ):
多个设备可能共享一个中断请求线,它们会通过这个线向处理器发送中断请求。 - 中断向量:
每种中断类型都会被分配一个中断向量,这是一个索引,用于在中断向量表中查找中断处理程序的地址。 - 快速系统调用:
某些系统调用(如获取系统时间)可能会通过专门的快速路径来执行,以减少常规系统调用的开销。
无论是硬件中断还是软件中断,当中断发生时,处理器会立即暂停当前正在执行的任务(除非正在处理更高优先级的中断),并且跳转到一个预先定义的中断处理程序去响应中断。中断处理程序执行完毕后,处理器会返回到被中断的任务继续执行。通过这种方式,操作系统能够及时响应内部和外部的事件,并控制系统资源的分配和任务的执行
JAVA对象组成
在 Java 中,一个对象主要由三部分组成:
- 对象头(Object Header):包含了对象自身的运行时数据,如哈希码(HashCode)、GC 分代年龄、锁状态标志、线程持有的锁、偏向线程 ID、偏向时间戳等。这部分数据的长度在 32 位和 64 位的 JVM(Java Virtual Machine)中分别为 32 bit 和 64 bit。
- 实例数据(Instance Data):也就是在类中所定义的各种类型的字段内容。
- 对齐填充(Padding):不是必需的,仅仅起到占位符的作用,没有实际意义。主要是为了对象大小满足系统要求。
这三部分组成了 Java 对象在内存中的布局。
对象头中又分为两部分:Mark Word 和 类型指针。
- Mark Word:存储对象自身的运行时数据,如哈希码、GC 分代年龄、锁状态标志、线程持有的锁、偏向线程 ID、偏向时间戳等。
- 类型指针:是指向对象的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。
死锁
在MySQL中,死锁是指两个或多个事务在互相等待对方释放锁定资源的情况下,导致它们都无法继续执行的一种状态
- 查看InnoDB状态输出:
- 使用
SHOW ENGINE INNODB STATUS;
查询可以提供关于最近发生的死锁的详细信息。这个命令的输出包含了很多信息,你需要查找LATEST DETECTED DEADLOCK
部分来获取死锁的具体信息。- 错误日志:
- MySQL将死锁信息写入到错误日志中。你可以检查MySQL的错误日志文件来找到死锁相关的信息。位置和名称可能因安装而异,通常是
/var/log/mysql/error.log
或者/var/log/mysqld.log
。- 性能模式表:
- 如果你的MySQL服务器开启了性能模式(Performance Schema),那么你可以查询
performance_schema.data_locks
和performance_schema.data_lock_waits
表来检测死锁。- 使用InnoDB监控工具:
- 一些第三方监控工具和服务(如Percona Toolkit、MonYog等)可以帮助你检测和分析MySQL服务器的死锁情况。
- 启用死锁日志:
- 你可以设置
innodb_print_all_deadlocks
选项为ON
,这样每个发生死锁的事件都会被记录到MySQL错误日志中。死锁产生的四大必要条件通常被称为死锁的四个Coffman条件,由E. G. Coffman, Jr.等人在1971年首先提出。这些条件必须同时满足,才会导致系统进入死锁状态:
- 互斥条件(Mutual Exclusion):
至少有一个资源必须处于非共享模式,也就是说,在一段时间内只有一个进程可以使用资源。如果其他进程请求该资源,请求者只能等待直到资源被释放。- 持有并等待条件(Hold and Wait):
一个进程至少持有一个资源,并且正在等待获取其他进程持有的额外资源,而这些资源可能正在被其他进程持有并等待释放。- 不可抢占条件(No Preemption):
已经分配给一个进程的资源不能被非自愿地抢占。这意味着资源只能被占有它的进程释放,只有在该进程完成任务后或者主动放弃资源时,资源才会被释放。- 循环等待条件(Circular Wait):
必须存在一个进程等待链,链中的每一个进程至少持有一个其他进程所需要的资源,形成一个闭合的循环等待链。比如,进程P1等待由进程P2持有的资源,P2等待P3持有的资源,而P3可能又在等待P1持有的资源。只有当这四个条件同时发生时,才会产生死锁。预防或避免死锁的一些策略就是基于破坏这四个条件中的一个或多个来实现的。例如,可以通过资源的一次性分配来避免持有并等待条件,或者允许资源被抢占来避免不可抢占条件。
HTTP/HTTPS 什么区别?HTTP 特性有哪些
1 | HTTP/HTTPS 什么区别?HTTP 特性有哪些 |
MySQL 聚集索引 (主键索引) 和非聚集索引 (辅助索引/普通索引) 的区别
MySQL中的聚集索引(主键索引)和非聚集索引(辅助索引/普通索引)是两种不同类型的索引,它们在数据存储和检索上有本质的区别:
聚集索引(主键索引):
- 数据存储:聚集索引决定了表中数据的物理存储顺序。在聚集索引中,表中的行是按照索引键值的顺序存储的。因此,一个表只能有一个聚集索引。
- 主键约束:在许多情况下,聚集索引是根据表的主键创建的。如果没有定义主键,MySQL可能会使用唯一索引作为聚集索引。如果这些都不存在,MySQL会自动生成一个隐藏的聚集索引。
- 性能优势:由于数据行与索引是紧密结合的,聚集索引通常在查找数据时提供更快的检索速度,尤其是对于范围查询。
- 插入速度:插入速度可能会受到影响,因为要保持数据的物理顺序,新行或更新可能需要移动其他行。
非聚集索引(辅助索引/普通索引)
- 数据存储:非聚集索引和数据行是分开存储的。索引结构包含索引键值和指向数据行的指针,而不是数据本身。
- 多个索引:一个表可以有多个非聚集索引,每个索引都是基于不同的列或列组合。
- 性能考虑:非聚集索引在检索特定数据时可能比聚集索引慢,因为它需要两次查找:首先在索引中查找,然后通过索引中的指针查找实际的数据行。
- 插入速度:插入速度可能更快,因为它不需要对数据行进行排序,但是更新索引本身可能需要时间,特别是如果索引列经常变化。
总的来说,聚集索引是根据数据的存储顺序建立的,而非聚集索引则是一个单独的结构,它引用了数据的存储位置。聚集索引在执行范围查询时特别有效,而非聚集索引适用于快速查找特定值。在设计数据库和选择索引时,通常需要根据查询模式和性能要求来决定使用哪种类型的索引。
什么是微服务架构?
1 | 微服务架构是一种设计方法,它将应用程序作为一套小的服务来构建,每个服务都运行在其自己的进程中,并且通常是围绕业务能力构建的。这些服务可以通过轻量级的通信机制(通常是HTTP RESTful API)相互沟通。每个服务都是独立的,并且可以单独部署在自动化部署机制的帮助下。它们可以使用不同的编程语言来编写,可以使用不同的数据存储技术。 |
怎么保证微服务架构的高可用
1 | 实施故障隔离:通过实施故障隔离(如使用bulkheads模式),将组件分隔开,以限制故障的影响范围。 |
现在需要设计一个开放平台,即提供接口给合作伙伴用,你觉得需要考虑一些什么问题?
1 | 设计一个开放平台API时,应该考虑以下问题: |
什么是注册中心?
注册中心(Service Registry)在微服务架构中扮演着关键角色,库,维护着分布式系统中所有可用服务的信息。注册中心充当所有服务实例的单一真实来源,提供诸如服务位置、状态和元数据等信息。
在微服务架构中,服务实例在启动时会在服务注册中心注册自己,这样其他服务或客户端就可以通过注册中心发现这些服务实例并与之通信。注册中心需要高度可用并且保持最新状态,以便能够提供准确的服务发现和路由功能。
它的主要职责和特点包括:
服务注册:当微服务实例启动时,它会向注册中心注册自己> 的信息,包括网络地址、端口号、版本号、健康状态等。
服务发现:其他服务或应用程序可以查询注册中心以发现和定位其他微服务实例。这允许服务之间的动态发现和交互,而无需硬编码服务的位置。
健康检查:注册中心通常会定期执行健康检查,以确保注册的服务实例仍然可用。如果服务实例不健康或不再响应,注册中心会将其标记为不可用,并从可用服务列表中移除。
负载均衡:注册中心可以支持负载均衡,通过在多个服务实例之间分配请求,来优化资源利用率和响应时间。
动态配置:注册中心可能还包含服务配置的信息,这样服务就可以在运行时动态地获取配置更新,而不需要重新启动。
高可用性:注册中心自身必须高度可用,以避免成为系统的单点故障。这通常通过部署多个注册中心实例和使用复制来实现。
服务注销:当微服务实例需要下线时,它会从注册中心注销自己,以确保不会有新的请求被发送到该实例。
安全性:注册中心应该实施适当的安全措施,以确保只有授权的服务可以注册和发现服务实例。
元数据管理:注册中心还可以存储有关服务的元数据,例如服务的描述、标签或其他自定义数据。
注册中心的实现可以使用各种技术和平台,如Eureka、Consul、Zookeeper等。在微服务架构中,注册中心是实现服务治理、确保微服务之间能够有效通信的基础设施。
服务注册与发现机制的基本模型是怎样的?
服务注册与发现机制的基本模型在微服务架构中通常遵循以下步骤:
- 服务注册:每个微服务实例启动时,都会通过自注册(self-registration)将自己的信息,比如网络地址、端口、运行状态等,注册到服务注册中心。服务注册中心暴露了一个REST API,服务实例可以通过发送POST请求来注册自己,并通过DELETE请求来注销。
- 服务发现:当一个服务需要调用另一个服务时,它会查询服务注册中心以获取所需服务的实例信息。服务注册中心包含了所有可用服务实例的地址和其他重要信息。
- 客户端侧发现:在客户端侧发现模式中,客户端知道服务注册中心的地址,并直接查询注册中心以获取服务实例的位置信息。
- 服务端侧发现:在服务端侧发现模式中,客户端通过一个中间层(例如API网关)发送请求,该中间层负责查询服务注册中心并将请求路由到相应的服务实例。
- 负载均衡:服务发现机制通常包括负载均衡的功能,客户端或中间层可以使用一些算法(如轮询、随机、最少连接等)在多个服务实例之间分配请求。
- 健康检查和注销:服务注册中心定期对服务实例进行健康检查。如果服务实例不再健康或者主动下线,它会从注册中心注销自己,注册中心随后更新其记录。
这个模型确保了微服务之间可以动态地发现彼此,并且能够根据实时信息进行通信,这对于构建灵活、可伸缩的分布式系统至关重要。
服务上线与服务下线的步骤是什么?
服务上线(启动)步骤:
- 初始化:服务实例启动时,首先初始化其配置和所需的本地资源。
- 健康检查:服务启动后进行自我健康检查,确保所有组件都能正常工作。
- 注册:服务实例将自己的信息(如IP地址、端口、健康状态等)注册到服务注册中心。
- 同步元数据:服务实例可能会同步一些元数据到注册中心,如服务版本、API路径等。
- 服务就绪:服务完成注册并通过健康检查后,宣告自己已经就绪,可以开始接收外部请求。
服务下线(关闭)步骤:
- 停止接收新请求:服务实例停止接收新的外部请求。
- 完成处理当前请求:服务实例等待当前正在处理的请求完成,或者在规定时间内强制关闭。
- 注销:服务实例从服务注册中心注销,表明自己不再可用。
- 清理资源:服务实例释放所有占用的资源,如数据库连接、缓存、临时文件等。
- 关闭服务:服务实例完成所有清理工作后,关闭进程。
在整个过程中,服务的平滑启动和优雅关闭是非常重要的,这可以防止在服务启动和关闭过程中出现流量丢失或请求失败的情况。此外,服务注册中心需要能够及时更新服务实例的状态,以保证服务发现机制的准确性。
蚂蚁金服SOFARegistry之服务上线
- Client 调用 publisher.register 向 SessionServer 注册服务。
- SessionServer 收到服务数据 (PublisherRegister) 后,将其写入内存 (SessionServer 会存储 Client 的数据到内存,用于后续可以跟 DataServer 做定期检查),再根据 dataInfoId 的一致性 Hash 寻找对应的 DataServer,将 PublisherRegister 发给 DataServer。
- DataServer 接收到 PublisherRegister 数据,首先也是将数据写入内存 ,DataServer 会以 dataInfoId 的维度汇总所有 PublisherRegister。同时,DataServer 将该 dataInfoId 的变更事件通知给所有 SessionServer,变更事件的内容是 dataInfoId 和版本号信息 version。
- 同时,异步地,DataServer 以 dataInfoId 维度增量地同步数据给其他副本。因为 DataServer 在一致性 Hash 分片的基础上,对每个分片保存了多个副本(默认是3个副本)。
- SessionServer 接收到变更事件通知后,对比 SessionServer 内存中存储的 dataInfoId 的 version,若发现比 DataServer 发过来的小,则主动向 DataServer 获取 dataInfoId 的完整数据,即包含了所有该 dataInfoId 具体的 PublisherRegister 列表。
- 最后,SessionServer 将数据推送给相应的 Client,Client 就接收到这一次服务注册之后的最新的服务列表数据。
参考资料:https://www.cnblogs.com/rossiXYZ/p/14226749.html
因为篇幅所限,本文讨论的是前两点,后续会有文章介绍另外几点**。
0xFF 参考
[蚂蚁金服服务注册中心如何实现 DataServer 平滑扩缩容](https://www.sofastack.tech/blog/sofa-registry-dataserver-smooth-expansion-contraction/)
[蚂蚁金服服务注册中心 SOFARegistry 解析 | 服务发现优化之路](https://www.sofastack.tech/blog/sofa-registry-service-discovery-optimization/)
[服务注册中心 Session 存储策略 | SOFARegistry 解析](https://www.sofastack.tech/blog/sofa-registry-session-storage/)
[海量数据下的注册中心 - SOFARegistry 架构介绍](https://www.sofastack.tech/blog/sofa-registry-introduction/)
[服务注册中心数据分片和同步方案详解 | SOFARegistry 解析](https://www.sofastack.tech/blog/sofa-registry-data-fragmentation-synchronization-scheme/)
[蚂蚁金服开源通信框架SOFABolt解析之连接管理剖析](https://www.sofastack.tech/blog/sofa-blot-connection-management-deep-dive/)
[蚂蚁金服开源通信框架SOFABolt解析之超时控制机制及心跳机制](https://www.sofastack.tech/blog/sofa-bolt-timeout-and-heart-beat-deep-dive/)
[蚂蚁金服开源通信框架 SOFABolt 协议框架解析](https://www.sofastack.tech/blog/sofa-bolt-framework-deep-dive/)
[蚂蚁金服服务注册中心数据一致性方案分析 | SOFARegistry 解析](https://blog.csdn.net/SOFAStack/article/details/104645427/)
[蚂蚁通信框架实践](https://blog.csdn.net/weixin_47364682/article/details/sohu.com/a/227222689_609518)
[sofa-bolt 远程调用](https://www.jianshu.com/p/c740810af40c)
[sofa-bolt学习](https://blog.csdn.net/weixin_47364682/article/details/jianshu.com/p/f1fae13c7848)
[SOFABolt 设计总结 - 优雅简洁的设计之道](https://www.jianshu.com/p/65c823de1249)
[SofaBolt源码分析-服务启动到消息处理](https://blog.csdn.net/qq_34088913/article/details/108469517)
[SOFABolt 源码分析](https://www.cnblogs.com/java-zhao/p/9824283.html)
[SOFABolt 源码分析9 - UserProcessor 自定义处理器的设计](https://www.jianshu.com/p/f2b8a2099323)
[SOFARegistry 介绍](https://www.sofastack.tech/projects/sofa-registry/overview/)
[SOFABolt 源码分析13 - Connection 事件处理机制的设计](https://blog.csdn.net/weixin_47364682/article/details/jianshu.com/p/d17b60418c54)
数据库同步迁移双写
你们单库拆分的时候是如何做数据迁移的 / 你们修改大表结构的时候是怎么做数据迁移的?怎么在保持应用不停机的情况下做数据迁移?什么是双写?为什么要引入双写?如果双写的过程中,有一边写失败了,怎么办?你可以用本地事务来保证双写要么都成功,要么都失败吗?
1 | 在进行单库拆分或修改大表结构时,数据迁移是一个必须要考虑的重要步骤,尤其是在保持应用不停机的情况下。以下是进行数据迁移时常见的一些步骤和概念解释: |
分布式事务呢?为什么有一个阶段是双写,但是以目标表为准?干嘛不直接切换到单写目标表?你们有什么容错方案?
分布式事务是一种确保在多个数据库或服务之间进行的操作要么全部成功,要么全部失败的机制。在执行数据迁移或单库拆分时,使用分布式事务可以帮助保持多个数据库之间数据的一致性。然而,分布式事务的实现通常比较复杂,并且可能会对性能产生较大影响。
在数据迁移的过程中,双写阶段的存在通常有以下几个原因:
- 渐进式迁移:由于直接切换到新数据库可能会引入风险,所以通过双写可以在一段时间内确保旧数据库和新数据库都有最新的数据。这样可以在迁移过程中逐步转移流量,并验证新数据库的性能和稳定性。
- 数据一致性:在迁移过程中,保持旧数据库和新数据库数据的同步,确保在迁移完成之前,任何数据更改都会反映在两个数据库中,这样即使迁移过程中出现问题,也可以从旧数据库恢复数据。
- 业务无感知:使用双写可以让业务方在不感知底层数据迁移的情况下继续正常运作,直到新数据库完全准备好并验证无误后,再进行最终切换。
关于为什么不直接单写目标表,这主要是为了避免在迁移过程中发生故障导致数据不一致或丢失。在完成所有的验证和数据校验之前,双写可以作为一种安全措施。
至于容错方案,除了分布式事务之外,还有一些其他的策略可以用来保障数据迁移的稳定性:
- 蓝绿部署:通过部署两个完全相同的生产环境,一个运行旧版本(蓝色),一个运行新版本(绿色)。一旦新环境验证稳定,就可以将流量切换到新环境。
- 金丝雀发布:逐渐将用户流量引导到新的数据库,开始时只有少部分流量,然后根据监控和校验结果逐步增加。
- 版本标记:在双写期间,可以通过版本标记来跟踪数据项的状态,以此来管理数据同步和回滚。
- 回滚方案:确保有一个快速可靠的回滚方案,以便在遇到问题时能够迅速切换回旧数据库。
每个容错方案都有其优缺点,适用于不同的场景。在选择方案时,应该充分考虑数据的重要性、业务的容忍度以及迁移的复杂性。
比如说如果在迁移过程中出错了,你们的应用会怎么办?你们是怎么校验数据的?增量数据校验你们是怎么做的?数据迁移你能够做到数据绝对不出错吗?如果数据出错了你们怎么修复?怎么避免并发问题?
在数据迁移过程中,出现错误是常见的风险。以下是一些常见的错误处理和校验方法,以及增量数据校验的策略:
- 错误处理:
- 监控和告警:设置实时监控和告警,在迁移过程中监控系统的性能指标和错误日志,一旦发现异常,立即触发告警。
- 暂停迁移:在检测到错误后,可能需要暂停迁移过程,以避免错误扩散。
- 日志记录:记录详细的操作日志,以便在出现问题时追踪和诊断。
- 回滚:准备好回滚机制,以便在发现数据不一致或其他严重问题时,能迅速恢复到迁移前的状态。
- 数据校验:
- 全量校验:迁移完成后,可以通过对比源数据库和目标数据库的数据快照来校验数据的完整性和一致性。
- 抽样校验:在大规模数据迁移中,全量校验可能不实际,此时可以通过抽样校验来确保数据的准确性。
- 增量数据校验:
- 时间戳/版本号:为数据记录添加时间戳或版本号,以便跟踪数据变更,并进行增量校验。
- 校验和:计算数据的校验和,对比迁移前后的校验和来检测数据一致性。
- 双写验证:在双写阶段,对于每一笔写入,可以在新旧数据库之间进行校验,确保数据的一致性。
- 数据绝对不出错的可能性:
- 在实际情况中,很难保证数据迁移绝对不出错。但是,通过上述的监控、校验和错误处理机制,可以将风险降到最低。
- 数据出错修复:
- 数据对比和修复:通过数据对比工具检测不一致的数据,并进行修复。
- 备份恢复:使用迁移前的备份数据来恢复错误数据。
- 手动修复:在无法自动修复的情况下,可能需要手动介入进行数据修复。
- 避免并发问题:
- 锁机制:在迁移期间,对于正在迁移的数据行使用适当的锁机制,避免并发访问导致的数据不一致。
- 队列和缓冲:使用消息队列和缓冲区来处理并发写入请求,确保按顺序处理。
- 最终一致性模型:在某些场景下,可以接受短暂的数据不一致,只要系统最终能达到一致状态。
总之,尽管无法保证数据绝对不出错,但是通过综合的监控、校验、错误处理和并发控制机制,可以最大限度地减少迁移风险并确保数据的一致性和完整性。
让你迁移一个 2000 万行的表,你的方案是什么
迁移一个包含2000万行数据的表是一个需要谨慎规划的任务,以确保数据的完整性、迁移的效率以及最小化对生产环境的影响。以下是一个迁移大表的基本步骤和方案:
- 准备工作:
- 评估数据:了解表的数据模型、大小、索引以及任何特定的数据类型或结构。
- 选择迁移时间:选择一个业务低谷期进行迁移,以最小化对用户的影响。
- 备份数据:在迁移前对原始数据进行完整备份,以便在迁移失败时可以恢复。
- 设计迁移方案:
- 迁移策略:确定是一次性迁移还是分批迁移。对于2000万行的数据,分批迁移通常更可靠,可以减轻对生产系统的压力。
- 迁移工具选择:选择合适的迁移工具,如ETL工具、数据库自带的数据导出导入工具或编写自定义的迁移脚本。
- 数据迁移:
- 创建目标表结构:在目标数据库创建与原表结构一致的表,包括索引和约束。
- 分批迁移数据:将数据分批迁移,每批迁移量根据系统容量和业务需求确定。可以通过主键范围、时间戳等方式进行分批。
- 双写设置:在迁移过程中,可能需要设置双写机制,确保新写入的数据同步到新旧表。
- 同步增量数据:
- 记录日志:在迁移过程中记录变化的数据,以便于后续同步。
- 增量同步:在分批迁移后,同步迁移期间产生的增量数据。
- 数据校验:
- 校验数据完整性:对比源表和目标表的记录数、校验和等,确保数据的一致性和完整性。
- 业务逻辑校验:可能需要进行更深入的数据校验,以确保迁移后的数据满足业务逻辑要求。
- 切换流量:
- 测试验证:在目标数据库上进行充分的测试,确保所有业务功能正常。
- 流量切换:在确认数据迁移完成并且数据一致性无误后,切换应用流量到新的数据库。
- 监控:
- 迁移监控:在迁移过程中监控性能指标和日志,确保迁移顺利进行。
- 生产监控:在切换流量后,继续监控系统性能和业务指标,确保系统稳定运行。
- 回滚计划:
- 准备回滚方案:在迁移过程中遇到不可解决的问题时,需要有计划地回滚到迁移前的状态。
这个方案包含了迁移前的准备、迁移过程中的操作以及迁移后的验证等多个阶段,每个阶段都需要仔细规划和执行。记住,通信和文档也是迁移成功的关键,确保所有参与者都了解迁移计划和状态。
⽹络的七层模型了解过吗?他和5层模型的区别在哪?为什么⽹络的模型这么设计?
网络的七层模型通常指的是OSI(Open Systems Interconnection)模型,而五层模型则是互联网模型。这两种模型都是为了理解和设计网络通信而创建的,但是它们在结构和理念上有所不同。
OSI模型的七层分别是:
- 物理层(Physical Layer)
- 数据链路层(Data Link Layer)
- 网络层(Network Layer)
- 传输层(Transport Layer)
- 会话层(Session Layer)
- 表示层(Presentation Layer)
- 应用层(Application Layer)
互联网模型的五层分别是:
- 物理层(Physical Layer)
- 数据链路层(Data Link Layer)
- 网络层(Network Layer)
- 传输层(Transport Layer)
- 应用层(Application Layer)
两者的区别主要在于:
- OSI模型更为详细,它将会话层、表示层和应用层分开考虑,而互联网模型将这三层合并为一个应用层。
- OSI模型是一个理论上的模型,它为网络通信提供了一个标准的参考框架。而互联网模型则是基于实践的,它更加贴近现实世界中的互联网结构。
网络模型之所以这么设计,是因为复杂的网络通信需要被分解成不同的层次来理解和管理。每一层都有特定的功能和协议,这样可以简化网络设计,使不同层次之间的通信和接口标准化,便于不同系统和设备之间的互操作性。通过这种分层,可以使网络通信更加灵活,易于开发和维护。
⾮对称加密了解过吗?它主要⽤在哪些地⽅?
非对称加密,也称为公开密钥加密,是密码学中的一种加密方法。它使用一对密钥:公钥和私钥。公钥可以公开分享,用于加密信息,而私钥保持私密,用于解密信息。非对称加密的主要特点是加密和解密使用的是两个不同的密钥。
常见的非对称加密算法包括:
- RSA算法:最早且最广泛使用的公钥加密算法,常用于数字签名、数据加密等。
- ECC(椭圆曲线加密):基于椭圆曲线数学的加密算法,相比RSA,它可以使用较短的密钥提供相同级别的安全性,因此运算速度更快,更适合移动设备。
- Diffie-Hellman:主要用于安全地在线交换密钥,而不直接用于加密或解密信息。
- DSA(数字签名算法):主要用于数字签名。
非对称加密主要用在以下几个方面:
- 数据加密:用于保护数据的隐私,确保只有持有相应私钥的接收方才能解密和阅读数据。
- 数字签名:确保消息的完整性和发送者的身份,接收方可以用发送者的公钥验证消息是由发送者签名且未被篡改。
- 安全通信:如SSL/TLS协议中,非对称加密用于安全地交换对称加密的密钥,之后的通信会使用对称加密,因为对称加密在处理大量数据时更高效。
- 身份认证:通过非对称加密可以验证一个实体的身份,如SSH登录、HTTPS网站等。
非对称加密的设计,使得它在需要安全密钥交换、数据加密和数字签名的任何场景中都非常有用,尤其是在互联网通信和数据保护方面。
mysql事务中的不可复读和幻读的区别
MySQL中,事务是一种机制,用来保证一系列的操作要么全部成功,要么全部失败,它提供了一种称为ACID的属性,即原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)、持久性(Durability)。在隔离性这一属性中,不可重复读和幻读是两种可能发生的现象,它们与隔离级别设置有关。
不可重复读(Non-repeatable reads):
这种现象指的是在一个事务中,同一查询被执行了多次,但是由于其他事务的更新操作,返回了不同的结果集。这通常发生在一个事务中多次读取同一数据时,另一个并发事务却修改了这些数据,导致第一个事务两次读取的数据不一致。幻读(Phantom reads):
幻读指的是在一个事务中执行相同的查询,由于其他事务插入了符合查询条件的新行,导致每次查询返回的行数不同。简单来说,事务A读取了一个范围的数据,事务B在这个范围内插入了新的数据,当事务A再次读取这个范围的数据时,就会发现有之前未见的“幻”行。区别:
- 不可重复读的重点是“修改”:即同一数据项的内容被外部事务修改导致数据不一致。
- 幻读的重点是“新增或删除”:即新的数据行的插入或删除导致一个事务多次执行同一查询时返回的结果集不同。
mvcc怎么实现的
MVCC, Multi-Version Concurrency Control 是多版本并发控制,实现数据库隔离级别的一种技术点,分为两个点,读视图(Read View)及 版本控制。
读视图是针对当前事务有个快照,定义了事务可以看到哪些数据版本。这保证了在整个事务中,读取的数据是一致的,即使其他事务在此期间提交了新的更改。
版本并发控制 是指数据行中有个隐藏的两个字段(创建版本号、删除版本号)、每次事务开启有个事务版本号。根据比较版本号来区分隔离性(事务修改表中的一行数据时,InnoDB会在Undo日志中记录该行的旧版本。如果一个事务需要看到早期版本的数据,InnoDB会使用Undo日志来构建这个旧版本。)
指针和引⽤的区别
- 指针和引用是编程中用于间接访问变量的两种机制,它们在使用上有着本质的区别。主要体现在C++这类支持指针和引用概念的语言中
- 指针是一个变量,存储的是内存地址,可以被重新指向另一个对象或者被赋值为
nullptr
/NULL
。指针支持算术运算。- 引用是一个已存在变量的别名,必须在定义时初始化,且之后不能再改变引用的关联(指向)。引用更安全,使用起来更接近于普通变量,但功能上没有指针灵活。
引用的引入是为了提供一种比指针更安全、使用更简便的方式来间接访问变量。在某些情况下,如函数参数传递时,使用引用可以避免指针可能引入的错误和复杂性
RAII机制了解过吗
RAII(Resource Acquisition Is Initialization)机制是一种在C++中常用的管理资源(如动态内存、文件描述符、锁等)的方法。RAII的基本思想是利用局部对象的生命周期来管理资源的获取和释放,确保在任何情况下资源都能被正确地管理,核心在与资源的释放和获取,智能指针(如std::unique_ptr
、std::shared_ptr
)就是典型应用
1 |
|
__END__