java面试题梳理整合
2025-02-27 09:16:16 0 举报
针对Java面试题目进行梳理整合,通常涵盖了核心编程知识点、设计理念以及最佳实践。在整理时,重点强调Java基础(如数据类型、数组、字符串处理)、面向对象编程(包括继承、封装、多态、抽象类、接口等)、异常处理机制、集合框架(List、Set、Map等的应用与区别)、多线程与并发编程(线程池、synchronized、volatile关键字)、Java虚拟机(JVM)内存模型、垃圾回收机制、以及常用的设计模式。 此外,还会涉及到Java 8新特性(如Lambda表达式、Stream API)、核心类库(如I/O、NIO、网络编程)、数据库操作(JDBC以及ORM框架如Hibernate、MyBatis)、Web技术(Servlet、Spring MVC)、以及最新的Spring框架(如Spring Boot、Spring Cloud)等。 旨在帮助求职者全面掌握Java技术栈,通过这些梳理和整合的面试题,求职者能够充分展示其技术深度与广度,以及解决实际问题的能力。最终的目的是让面试者能够在面试中脱颖而出,获得心仪的工作机会。在文件格式上,这类内容适合整理成清晰的PDF文档,便于阅读和打印;修饰语应体现出准备充分、准确到位,例如:“全面掌握”、“技术深度广度”、“解决实际问题的能力”。
作者其他创作
大纲/内容
回答模板
回答你对 xxx 的理解的模板
定义和概述
核心要素/特点
作用和价值
应用场景
与其他相关概念的关系
个人的实践经验(如果有)
总结和展望
你对 xxx 的理解模板
1. 概念简介
- 简要介绍技术概念的名称和它在技术领域中的基本定位。
- 简要介绍技术概念的名称和它在技术领域中的基本定位。
2. 核心原理与机制
- 详细阐述该技术的核心工作原理,包括关键算法、数据结构、流程等。
- 结合具体的技术细节和示例进行说明。
- 详细阐述该技术的核心工作原理,包括关键算法、数据结构、流程等。
- 结合具体的技术细节和示例进行说明。
3. 关键特性与优势
- 列举该技术的主要特性和突出优势。
- 解释这些特性和优势如何为实际应用带来价值。
- 列举该技术的主要特性和突出优势。
- 解释这些特性和优势如何为实际应用带来价值。
4. 应用场景与案例
- 描述该技术常见的应用场景,并提供实际的应用案例。
- 分析在这些案例中技术的具体运用和效果。
- 描述该技术常见的应用场景,并提供实际的应用案例。
- 分析在这些案例中技术的具体运用和效果。
5. 技术挑战与应对
- 指出在使用该技术时可能遇到的技术挑战和限制。
- 分享应对这些挑战的常见方法和策略。
- 指出在使用该技术时可能遇到的技术挑战和限制。
- 分享应对这些挑战的常见方法和策略。
6. 与其他技术的关联
- 阐述该技术与相关或类似技术的关系,包括相似之处和差异。
- 说明在不同场景下如何选择合适的技术。
- 阐述该技术与相关或类似技术的关系,包括相似之处和差异。
- 说明在不同场景下如何选择合适的技术。
7. 个人经验与见解
- 结合自己的项目经验,谈谈对该技术的实际应用感受和心得。
- 对该技术的未来发展趋势提出个人的看法。
- 结合自己的项目经验,谈谈对该技术的实际应用感受和心得。
- 对该技术的未来发展趋势提出个人的看法。
8. 学习资源与建议
- 推荐一些学习该技术的优质资源,如书籍、在线课程、开源项目等。
- 给出对初学者或深入学习者的学习建议。
- 推荐一些学习该技术的优质资源,如书籍、在线课程、开源项目等。
- 给出对初学者或深入学习者的学习建议。
9. 过往的使用经验
你对 xxx 的理解模板
是什么
能解决什么问题
有什么优势和特点
核心原理是什么
为什么能解决这类问题
在技术面试中展示技术深度和广度可的方式
1. 知识体系的全面呈现
- 准备一份清晰、有条理的技术知识大纲,涵盖您熟悉的多个技术领域,如编程语言、数据库、操作系统、网络等。在回答问题时,能够从不同角度关联和整合这些知识。
- 准备一份清晰、有条理的技术知识大纲,涵盖您熟悉的多个技术领域,如编程语言、数据库、操作系统、网络等。在回答问题时,能够从不同角度关联和整合这些知识。
2. 深入理解核心技术
- 对于您擅长的关键技术,如某种编程语言的底层机制、特定数据库的优化策略等,能够详细阐述其工作原理、性能特点和适用场景。不仅要知其然,还要知其所以然。
- 分享您在实际项目中如何深入运用这些技术解决复杂问题,展示您的技术深度。
- 对于您擅长的关键技术,如某种编程语言的底层机制、特定数据库的优化策略等,能够详细阐述其工作原理、性能特点和适用场景。不仅要知其然,还要知其所以然。
- 分享您在实际项目中如何深入运用这些技术解决复杂问题,展示您的技术深度。
3. 项目经验的深度剖析
- 详细介绍您参与的重要项目,特别是那些具有技术挑战性的部分。解释您在项目中所承担的角色、遇到的技术难题以及您采取的解决方案。
- 强调您在项目中所做的技术创新或对现有技术的优化改进。
- 详细介绍您参与的重要项目,特别是那些具有技术挑战性的部分。解释您在项目中所承担的角色、遇到的技术难题以及您采取的解决方案。
- 强调您在项目中所做的技术创新或对现有技术的优化改进。
4. 对新技术的关注和学习能力
- 提及您对当前热门技术趋势的了解,如人工智能、区块链、云计算等,并能阐述这些技术的基本概念和潜在应用。
- 展示您如何快速学习和适应新技术,例如通过自学、实验项目或参与开源社区。
- 提及您对当前热门技术趋势的了解,如人工智能、区块链、云计算等,并能阐述这些技术的基本概念和潜在应用。
- 展示您如何快速学习和适应新技术,例如通过自学、实验项目或参与开源社区。
5. 技术交流和分享
- 如果您有参与技术社区、发表技术博客、参与开源项目或在技术会议上分享经验的经历,一定要在面试中提及。这能体现您的技术热情和对行业的贡献,同时也反映出您的技术广度。
- 如果您有参与技术社区、发表技术博客、参与开源项目或在技术会议上分享经验的经历,一定要在面试中提及。这能体现您的技术热情和对行业的贡献,同时也反映出您的技术广度。
6. 解决复杂问题的能力
- 准备一些复杂的技术问题案例,并能够清晰地描述您的分析思路、解决步骤和最终的效果。这展示了您在面对未知和困难时的技术攻坚能力。
- 准备一些复杂的技术问题案例,并能够清晰地描述您的分析思路、解决步骤和最终的效果。这展示了您在面对未知和困难时的技术攻坚能力。
7. 技术思维和方法论
- 阐述您在技术工作中遵循的方法论,如敏捷开发、设计模式的应用、代码质量保障等。这显示您不仅掌握具体技术,还具备良好的技术思维和工程实践能力。
- 阐述您在技术工作中遵循的方法论,如敏捷开发、设计模式的应用、代码质量保障等。这显示您不仅掌握具体技术,还具备良好的技术思维和工程实践能力。
8. 提问与互动
- 在面试过程中,适时提出有深度和针对性的问题,与面试官进行技术交流。这不仅能展示您的思考能力,还能让面试官了解您对技术的好奇心和探索精神。
- 在面试过程中,适时提出有深度和针对性的问题,与面试官进行技术交流。这不仅能展示您的思考能力,还能让面试官了解您对技术的好奇心和探索精神。
技术框架
代码实例
单例模式
懒汉
简单版
示例
提升获取效率降低锁粒度-双重检查
示例
处理可能出现的指令重排
示例
饿汉
示例
场景
问题排查
CPU 飙高系统反应慢怎么排查?
1. 查看系统进程
- 使用系统自带的任务管理器(Windows 系统)或 top 命令(Linux 系统),查看当前正在运行的进程,找出 CPU 占用率高的进程。
- 使用系统自带的任务管理器(Windows 系统)或 top 命令(Linux 系统),查看当前正在运行的进程,找出 CPU 占用率高的进程。
2. 分析进程详情
- 确定占用 CPU 高的进程后,进一步查看该进程的详细信息,如进程名称、PID(进程标识符)、所属用户等。
- 确定占用 CPU 高的进程后,进一步查看该进程的详细信息,如进程名称、PID(进程标识符)、所属用户等。
3. 检查进程的线程
- 对于复杂的进程,可以使用相关工具(如 Windows 上的 Process Explorer 或 Linux 上的 ps 命令结合线程选项)查看其内部的线程情况,找出具体占用 CPU 高的线程。
- 对于复杂的进程,可以使用相关工具(如 Windows 上的 Process Explorer 或 Linux 上的 ps 命令结合线程选项)查看其内部的线程情况,找出具体占用 CPU 高的线程。
4. 关联代码或服务
- 根据进程的名称和相关信息,确定其对应的应用程序或服务,思考近期是否对相关代码进行了修改或部署。
- 根据进程的名称和相关信息,确定其对应的应用程序或服务,思考近期是否对相关代码进行了修改或部署。
5. 查看系统日志
- 检查系统日志、应用程序日志,看是否有相关的错误、警告或异常信息,这可能提供有关 CPU 飙高的线索。
- 检查系统日志、应用程序日志,看是否有相关的错误、警告或异常信息,这可能提供有关 CPU 飙高的线索。
6. 资源竞争排查
- 检查是否存在资源竞争的情况,如磁盘 I/O 瓶颈、内存不足等,这可能间接导致 CPU 使用率升高。
- 检查是否存在资源竞争的情况,如磁盘 I/O 瓶颈、内存不足等,这可能间接导致 CPU 使用率升高。
7. 代码审查
- 如果确定是某个应用程序导致的,对相关代码进行审查,特别是近期修改的部分,查找可能存在的死循环、大量计算、频繁的对象创建和销毁等问题。
- 如果确定是某个应用程序导致的,对相关代码进行审查,特别是近期修改的部分,查找可能存在的死循环、大量计算、频繁的对象创建和销毁等问题。
8. 监控系统性能指标
- 持续监控系统的性能指标,如 CPU 使用率、内存使用、网络流量等,观察在采取某些措施后系统的变化情况。
- 持续监控系统的性能指标,如 CPU 使用率、内存使用、网络流量等,观察在采取某些措施后系统的变化情况。
9. 考虑外部因素
- 检查是否有外部的攻击、病毒或恶意软件导致系统异常。
- 检查是否有外部的攻击、病毒或恶意软件导致系统异常。
10. 回滚操作
- 如果是近期的更新或配置更改导致的问题,可以尝试回滚到之前稳定的状态,观察 CPU 使用率是否恢复正常。
- 如果是近期的更新或配置更改导致的问题,可以尝试回滚到之前稳定的状态,观察 CPU 使用率是否恢复正常。
Dubbo 的服务请求失败怎么处理?
豆包回答
Dubbo 处理服务请求失败的机制涉及多个方面。
首先,服务请求失败通常指在调用远程服务的过程中出现了异常情况,导致服务无法正常响应。
Dubbo 处理服务请求失败的方式主要有以下几种:
1、重试机制:Dubbo 可以配置重试次数和重试间隔,当一次请求失败后,会在一定条件下自动进行重试。
2、容错策略:例如 Failover 策略,会自动切换到其他可用的服务提供者进行请求;Failfast 策略则是立即报错,不再进行重试。
3、监控和报警:Dubbo 能够对失败的请求进行监控和统计,并在达到一定阈值时发出报警,以便及时发现和处理问题。
在我参与的一个分布式系统项目中,使用 Dubbo 时遇到过服务提供者暂时不可用的情况。通过配置的 Failover 策略,自动切换到其他正常的服务提供者,保证了服务的连续性。同时,利用监控机制及时发现了服务提供者的故障,并进行了相应的处理。
首先,服务请求失败通常指在调用远程服务的过程中出现了异常情况,导致服务无法正常响应。
Dubbo 处理服务请求失败的方式主要有以下几种:
1、重试机制:Dubbo 可以配置重试次数和重试间隔,当一次请求失败后,会在一定条件下自动进行重试。
2、容错策略:例如 Failover 策略,会自动切换到其他可用的服务提供者进行请求;Failfast 策略则是立即报错,不再进行重试。
3、监控和报警:Dubbo 能够对失败的请求进行监控和统计,并在达到一定阈值时发出报警,以便及时发现和处理问题。
在我参与的一个分布式系统项目中,使用 Dubbo 时遇到过服务提供者暂时不可用的情况。通过配置的 Failover 策略,自动切换到其他正常的服务提供者,保证了服务的连续性。同时,利用监控机制及时发现了服务提供者的故障,并进行了相应的处理。
书中回答
Dubbo 是一个 RPC 框架,它为我们的应用提供了远程通信能力的封装,同时,Dubbo在 RPC 通信的基础上,逐步在向一个生态在演进,它涵盖了服务注册、动态路由、容错、服务降级、负载均衡等能力,基本上在微服务架构下面临的问题,Dubbo 都可以解决。
而对于 Dubbo 服务请求失败的场景,默认提供了重试的容错机制,也就是说,如果基于 Dubbo 进行服务间通信出现异常,服务消费者会对服务提供者集群中其他的节点发起重试,确保这次请求成功,默认的额外重 试次数是 2 次。
除此之外,Dubbo 还提供了更多的容错策略,我们可以根据不同的业务场景来进行选择。
1. 快速失败策略,服务消费者只发起一次请求,如果请求失败,就直接把错误抛出去。这种比较适合在非幂等性场景中使用
2. 失败安全策略,如果出现服务通信异常,直接把这个异常吞掉不做任何处理
3. 失败自动恢复策略,后台记录失败请求,然后通过定时任务来对这个失败的请求进行重发。
4. 并行调用多个服务策略,就是把这个消息广播给服务提供者集群,只要有任何一个节点返回,就表示请求执行成功。
5. 广播调用策略,逐个调用服务提供者集群,只要集群中任何一个节点出现异常,就表示本次请求失败
应用问题
Redis 和 Mysql 如何保证数据一致性
书中回答
一般情况下,Redis 用来实现应用和数据库之间读操作的缓存层,主要目的是减少数据库 IO,还可以提升数据的 IO 性能。这是它的整体架构。当应用程序需要去读取某个数据的时候,首先会先尝试去 Redis 里面加载,如果命中就直接返回。如果没有命中,就从数据库查询,查询到数据后再把这个数据缓存到Redis 里面。
在这样一个架构中,会出现一个问题,就是一份数据,同时保存在数据库和Redis 里面,当数据发生变化的时候,需要同时更新 Redis 和 Mysql,由于更新是有先后顺序的,并且它不像 Mysql
1. 先更新数据库,再更新缓存
2. 先删除缓存,再更新数据库
如果先更新数据库,再更新缓存如果缓存更新失败,就会导致数据库和 Redis 中的数据不一致。
如果是先删除缓存,再更新数据库,理想情况是应用下次访问 Redis 的时候,发现Redis 里面的数据是空的,就从数据库加载保存到 Redis 里面,那么数据是一致的。但是在极端情况下,由于删除 Redis和更新数据库这两个操作并不是原子的,所以这个过程如果有其他线程来访问,还是会存在数据不一致问题。
所以,如果需要在极端情况下仍然保证Redis 和 Mysql 的数据一致性,就只能采用最终一致性方案。
豆包回答
Redis 和 MySQL 保证数据一致性是一个具有挑战性的问题。
数据一致性指的是在多个数据存储系统中,数据的状态始终保持一致和准确。在 Redis 和 MySQL 结合使用的场景中,可能会由于数据更新的不同步导致不一致的情况出现。
保证数据一致性的方案有以下几种:
1. 先更新数据库,再删除 Redis 缓存:这种方式可以避免更新缓存导致的不一致,但要注意处理删除缓存失败的情况。
2. 使用分布式事务:例如通过两阶段提交等方式来保证数据库和 Redis 的操作要么都成功,要么都失败。但分布式事务会带来性能开销和复杂性。
3. 基于消息队列:数据库更新后发送消息到队列,消费端处理消息来更新 Redis,要确保消息的可靠传递和处理。
在我参与的一个电商项目中,采用了先更新数据库再删除 Redis 缓存的方式。但为了应对删除缓存失败的情况,增加了重试机制和日志记录。如果多次重试仍失败,会发出告警以便人工处理。核心代码示例如下:
```java
@Transactional
public void updateDataInDatabaseAndDeleteCache(String key) {
// 更新数据库的逻辑
//...
// 尝试删除 Redis 缓存
boolean deleted = redisTemplate.delete(key);
if (!deleted) {
// 记录日志,进行重试
//...
}
}
```
数据一致性指的是在多个数据存储系统中,数据的状态始终保持一致和准确。在 Redis 和 MySQL 结合使用的场景中,可能会由于数据更新的不同步导致不一致的情况出现。
保证数据一致性的方案有以下几种:
1. 先更新数据库,再删除 Redis 缓存:这种方式可以避免更新缓存导致的不一致,但要注意处理删除缓存失败的情况。
2. 使用分布式事务:例如通过两阶段提交等方式来保证数据库和 Redis 的操作要么都成功,要么都失败。但分布式事务会带来性能开销和复杂性。
3. 基于消息队列:数据库更新后发送消息到队列,消费端处理消息来更新 Redis,要确保消息的可靠传递和处理。
在我参与的一个电商项目中,采用了先更新数据库再删除 Redis 缓存的方式。但为了应对删除缓存失败的情况,增加了重试机制和日志记录。如果多次重试仍失败,会发出告警以便人工处理。核心代码示例如下:
```java
@Transactional
public void updateDataInDatabaseAndDeleteCache(String key) {
// 更新数据库的逻辑
//...
// 尝试删除 Redis 缓存
boolean deleted = redisTemplate.delete(key);
if (!deleted) {
// 记录日志,进行重试
//...
}
}
```
死锁的发生原因和怎么避免
书中
死锁,简单来说就是两个或者两个以上的线程在执行的过程中,争夺同一个共享资源造成的相互等待的现象。如果没有外部干预,线程会一直阻塞无法往下执行,这些一直处于相互等待资源的线程就称为死锁线程。
导致死锁的条件有四个,也就是这四个条件同时满足就会产生死锁。
互斥条件,共享资源 X 和 Y 只能被一个线程占用;
请求和保持条件,线程 T1 已经取得共享资源 X,在等待共享资源 Y 的时候,不释放共享资源 X;
不可抢占条件,其他线程不能强行抢占线程 T1 占有的资源;
循环等待条件,线程 T1 等待线程 T2 占有的资源,线程 T2 等待线程 T1 占有的资源,就是循环等待。导致死锁之后,只能通过人工干预来解决,比如重启服务,或者杀掉某个线程。
按照死锁发生的四个条件,只需要破坏其中的任何一个,就可以解决,但是,互斥条件是没办法破坏的,因为这是互斥锁的基本约束,其他三方条件都有办法来破坏:
对于“请求和保持”这个条件,我们可以一次性申请所有的资源,这样就不存在等待了。
对于“不可抢占”这个条件,占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源,这样不可抢占这个条件就破坏掉了。
对于“循环等待”这个条件,可以靠按序申请资源来预防。所谓按序申请,是指资源是有线性顺序的,申请的时候可以先申请资源序号小的,再申请资源序号大的,这样线性化后自然就不存在循环了。
豆包
死锁是指两个或多个线程或进程在执行过程中,因互相等待对方持有的资源而导致的一种僵持状态。
死锁发生的原因通常有以下几种:
1. 竞争不可抢占资源:系统中某些资源是不可抢占的,当一个进程占有不可抢占资源,又去申请其他进程持有的不可抢占资源时,就可能导致死锁。
2. 竞争可消耗资源:进程在等待新资源时,如果资源的分配不当,可能导致死锁。
3. 进程推进顺序不当:进程在运行过程中,如果推进顺序不当,可能导致死锁。
避免死锁的方法主要有以下几种:
1. 预防死锁:通过破坏死锁产生的四个必要条件之一来预防死锁。比如避免请求和保持条件,可以一次性请求所有需要的资源;避免不剥夺条件,可以允许剥夺进程已占有的资源;避免环路等待条件,可以对资源进行编号,进程按照编号顺序请求资源。
2. 避免死锁:在资源分配过程中,使用银行家算法等方式,判断此次资源分配是否会导致系统进入不安全状态,从而决定是否进行分配。
3. 检测死锁:通过资源分配图等方法来检测系统是否存在死锁。
4. 解除死锁:一旦检测到死锁,通过剥夺资源、撤销进程等方式来解除死锁。
在我参与的一个分布式任务调度系统项目中,为了避免死锁,采用了资源预分配和资源有序申请的策略。在任务执行前,一次性申请所需的全部资源,并且按照资源的优先级和编号顺序进行申请。
以下是一个简单的模拟死锁的示例代码:
```java
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class DeadlockExample {
private Lock lock1 = new ReentrantLock();
private Lock lock2 = new ReentrantLock();
public void method1() {
lock1.lock();
try {
Thread.sleep(100);
lock2.lock();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock2.unlock();
lock1.unlock();
}
}
public void method2() {
lock2.lock();
try {
Thread.sleep(100);
lock1.lock();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock1.unlock();
lock2.unlock();
}
}
public static void main(String[] args) {
DeadlockExample example = new DeadlockExample();
new Thread(() -> example.method1()).start();
new Thread(() -> example.method2()).start();
}
}
```
死锁发生的原因通常有以下几种:
1. 竞争不可抢占资源:系统中某些资源是不可抢占的,当一个进程占有不可抢占资源,又去申请其他进程持有的不可抢占资源时,就可能导致死锁。
2. 竞争可消耗资源:进程在等待新资源时,如果资源的分配不当,可能导致死锁。
3. 进程推进顺序不当:进程在运行过程中,如果推进顺序不当,可能导致死锁。
避免死锁的方法主要有以下几种:
1. 预防死锁:通过破坏死锁产生的四个必要条件之一来预防死锁。比如避免请求和保持条件,可以一次性请求所有需要的资源;避免不剥夺条件,可以允许剥夺进程已占有的资源;避免环路等待条件,可以对资源进行编号,进程按照编号顺序请求资源。
2. 避免死锁:在资源分配过程中,使用银行家算法等方式,判断此次资源分配是否会导致系统进入不安全状态,从而决定是否进行分配。
3. 检测死锁:通过资源分配图等方法来检测系统是否存在死锁。
4. 解除死锁:一旦检测到死锁,通过剥夺资源、撤销进程等方式来解除死锁。
在我参与的一个分布式任务调度系统项目中,为了避免死锁,采用了资源预分配和资源有序申请的策略。在任务执行前,一次性申请所需的全部资源,并且按照资源的优先级和编号顺序进行申请。
以下是一个简单的模拟死锁的示例代码:
```java
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class DeadlockExample {
private Lock lock1 = new ReentrantLock();
private Lock lock2 = new ReentrantLock();
public void method1() {
lock1.lock();
try {
Thread.sleep(100);
lock2.lock();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock2.unlock();
lock1.unlock();
}
}
public void method2() {
lock2.lock();
try {
Thread.sleep(100);
lock1.lock();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock1.unlock();
lock2.unlock();
}
}
public static void main(String[] args) {
DeadlockExample example = new DeadlockExample();
new Thread(() -> example.method1()).start();
new Thread(() -> example.method2()).start();
}
}
```
扫码登录到底是怎么实现的?
书中
扫码登录的本质是,通过已经登录过的 App应用,扫描未登录的 Web 端程序中的二维码, 通过某种机制触发登录凭证的写入从而 实现 Web 端自动登录的过程。
首先,在网页端打开登录页面,展示一个二维码,这个二维码有一个唯一编号是服务端生成的。然后浏览器定时查询这个二维码的状态个临时 token
接着,APP 扫描这个二维码,把 APP 的token 信息、二维码 ID 发送给 Server 端,Server 收到请求后修改二维码的扫码状态,并生成一个临时 token
此时,网页端展示的二维码状态会提示已扫码,待确认。 而 APP 端扫码之后,会提示确认授权的操作。
于是,用户确认登录后,携带临时 token 给到 server,server 端修改二维码状态并为网页端生成授权 token
最后,网页端轮询到状态变化并获取到token,从而完成扫码授权。
为什么一线互联网公司严禁使用存储过程?
书中
之所以互联网公司不让用,主要有几个方面的原因:
存储过程不好调试,一旦涉及非常复杂的逻辑,定位问题的时候比较麻烦
存储过程的一致性很差, 如果从 Oracle迁移到 Mysql,涉及部分数据库独有特性的时候,整个存储过程就需要重写,不仅成本高,而且还 有可能对上游业务造成影响
存储过程的管理比较困难,一旦存储过程的量比较大的时候,就会陷入一个混乱的状态
存储过程的优化和维护很麻烦, 随着业务的发展,数据库的表结构也许会发生变化,这些变化需要同步给到存储过程,并且有可能原来的SQL 语句的执行计划不是最优的,也需要重新维护。
在互联网公司,业务的迭代速度是日新月异的,把业务逻辑沉淀在应用程序中,反而更加灵活。
订单超时自动取消功能如何设计?
书中
轮询
时间轮
MQ 演示队列
豆包
订单超时自动取消功能的设计涉及到多个方面的概念和情境。
首先,需要有一个定时机制来检测订单是否超时。可以使用定时任务,如 Java 中的`ScheduledExecutorService`,或者利用数据库的定时任务功能。
其次,要有明确的超时时间设定规则,根据业务需求确定不同类型订单的超时时间。
另外,还需要考虑在订单超时取消时的相关处理逻辑,比如释放占用的资源、更新订单状态、通知相关方等。
针对这个问题,以下是一些可能的解决方案:
1. 使用数据库的定时任务,通过存储过程或者作业来定期检查订单是否超时,并进行相应处理。
2. 在应用程序中创建定时线程池,定时扫描订单表,判断是否超时。
3. 利用消息队列,当创建订单时发送一个延迟消息,超时后由消费者进行处理。
首先,需要有一个定时机制来检测订单是否超时。可以使用定时任务,如 Java 中的`ScheduledExecutorService`,或者利用数据库的定时任务功能。
其次,要有明确的超时时间设定规则,根据业务需求确定不同类型订单的超时时间。
另外,还需要考虑在订单超时取消时的相关处理逻辑,比如释放占用的资源、更新订单状态、通知相关方等。
针对这个问题,以下是一些可能的解决方案:
1. 使用数据库的定时任务,通过存储过程或者作业来定期检查订单是否超时,并进行相应处理。
2. 在应用程序中创建定时线程池,定时扫描订单表,判断是否超时。
3. 利用消息队列,当创建订单时发送一个延迟消息,超时后由消费者进行处理。
有没有排查过线上 oom 的问题,如何排查的
书中
导致 OOM 错误的情况一般是:
给 JVM 虚拟机分配的内存太小,实际业务需求对内存的消耗比较多
Java 应用里面存在内存泄漏的问题,或者应用中有大量占用内存的对象,并且没办法及时释放。
内存泄漏和内存溢出是两个完全不一样的情况
内存泄漏:申请使用完的内存没有释放,导致虚拟机不能再次使用该内存,此时这段内存就泄露了,因为申请者不用了,而又不能被虚拟机分配给别人用。
内存溢出:申请的内存超出了 JVM 能提供的内存大小,此时称之为溢出。
常见的 OOM
java.lang.OutOfMemoryError: Java heapspace ------>java 堆内存溢出,此种情况最常见,一般由于内存泄漏或者堆的大小设置不当引起。对于内存泄漏,需要通过内存监控软件查找程序中的泄露代码,而堆大小可以通过虚拟机参数-Xms,-Xmx 来修改。
java.lang.OutOfMemoryError: PermGen space 或 java.lang.OutOfMemoryError:MetaSpace ------>java 方法区,溢出了,一般出现在大量 Class,或者采用 cglib等反射机制的情况,因为这些情况会产生大量的 Class 信息存储于方法区。这种情况可以通过更改方法区的大小来解决,使用类似-XX:PermSize=64m-XX:MaxPermSize=256m 的形式修改。另外,过多的常量尤其是字符串也会导致方法区溢出。
遇到这类问题,通常的排查方式是,先获取内存的 Dump 文件。
Dump 文件有两种方式来生成,
第一种是配置 JVM 启动参数,当触发了 OOM 异常的时候自动生成
第二种是使用 jmap 工具来生成。
然后使用 MAT 工具来分析 Dump 文件。
如果是内存泄漏,可进一步通过工具查看泄漏对象到 GC Roots 的引用链。掌握了泄漏对象的类信息和 GC Roots 引用链的信息,就可以比较准确地定位泄漏代码的位置。
如果是普通的内存溢出,确实有很多占用内存的对象,那就只需要提升堆内存空间即可。
怎么理解接口幂等,项目中如何保证的接口幂等
书中
通常的解决方案有几种。
使用数据库唯一索引的方式实现,我们可以专门创建一个消息表,里面有一个消息内容的字段并且设置为唯一索引,每次收到消息以后生成md5 值插入到这个消息表里面。一旦出现重复消息,就会抛异常,我们可以捕获这个异常来避免重复对数据做变更。
使用 Redis 里面的 setNx 命令,我们可以把当前请求中带有唯一标识的信息存储到Redis 里面,根据 setNx 命令返回的结果来判断是否是重复执行,如果是则丢弃该请求。
使用状态机的方式来实现幂等,在很多的业务场景中,都会存在业务状态的流转,并且这些状态流转只会前进,所以我们在对数据进行修改的时候,只需要在条件里面带上状态,就能避免数据被重复修改的问题。
消息推送中的已读消息和未读消息设计难题
书中
从 B 站崩溃的故障排查和恢复过程中学到什么?
书中
会员批量过期的方案怎么实现?
书中
第一种,系统不主动轮询,而是等用户登录到系统以后,触发一次检查。如果发现会员的过期时间小于设定的阈值,就触发一次弹窗和邮件提醒。这种方式规避了轮询问题,不会对数据库和后端应用程序造成任何压力。缺点是,如果用户一直不登录,就一直无法实现会员过期,并且也无法提前去根据运营策略发送续期的提醒消息。
第二种,我们可以使用搜索引擎,比如Solr,或者 Elasticsearch。把会员表里面的会员 id 和会员到期时间存储一份到搜索引擎中。搜索引擎的优势在于大数据量的快速检索,并且具有高可扩展性和高可靠性,非常适合大规模数据的处理。
第三种,可以使用 Redis 来实现。用户开通会员以后,在 Redis 里面存储这个会员 id,以及设置这个 id 的过期时间。然后可以使用 redis 的过期提醒功能,把配置项 notify-keyspace-events 改为 notify-keyspace-events "Ex"当 Redis 里面的 key 过期以后,会触发一个key 过期事件,我们可以在应用程序中监听这个事件来处理。
第四种,可以直接使用 MQ 提供的延迟队列,当用户开通会员以后,直接计算这个会员的过期时间,然后发送一个延迟消息到MQ 上,一旦消息达到过期时间,消费者就可以消费这个消息来触发会员过期的提醒。
入职一家新公司,如何快速上手开发跳槽到一家新公司后,如何以最快的速度融入团队协作上手项目开发?
书中
第一点,了解业务背景
做开发千万不能眼里只有技术细节,技术只能告诉你怎么做,而产品、业务逻辑则是告诉你做什么进入新公司以后,首先要搞清楚新公司的产品功能以及业务逻辑公司产品的用户是谁、提供的核心功能是什么、系统在上下游的地位。带着这些问题,去熟悉产品的核心功能,对项目有一个全局性把控。而了解业务逻辑,本质上是了解这个业务背后的运转模式,基于运转模式进行深入。只有在理解业务逻辑的基础上做出的技术方案,才有价值和针对性。在了解产品和业务逻辑后,我们还可以站在行业的高度对竞品有一定的了解通过阅读一些行业报告来加深对公司业务未来发展趋势的认知
第二点,了解技术栈
刚入职,不用太着急下沉到技术细节里去。可以先从宏观上了解技术。比如当前的技术栈、使用的语言、明确使用的工具和框架,然后对照业务模型,对系统功能进行拆解。系统对外提供的接口有哪些?依赖的内部或者外部系统的接口有哪些?采用的通信方式、通信协议分别是什么?其次,项目内部分哪几个模块,每个模块的职责,最后是熟悉代码,目录结构,配置文件的位置,业务功能在源码里面的体现;有了宏观的了解之后,可以针对接口的重要程度,从头到尾绘制一下接口的时序图,顺序图,加深对核心业务的理解。
第三点,从小需求开始,尝试编码
很多小伙伴加入新公司后,觉得自己对项目还不了解,不着急做需求,一直处于准备状态。但是,站在岸上,永远学不会游泳。只有带着具体问题去实践,逐步踩坑,才能快速上手;
例如在本地完成工程的搭建后,针对代码进行 debug,了解每步实现的处理逻辑和数据变化,从而完全掌握这一个小模块或功能点的实现。接下来, 就可以接一些小的需求进行开发了多做几个小需求后,对应用的实现细节就会越来越熟悉刚开始做的小需求不用求快,而是以规范为主。做完之后有了成就感,对新入职的你也是一种正向激励。
第四点,也是最重要的一点,具备 owner 的心态
在新公司接手一个系统后,就需要以 owner的心态来对待。有些小伙伴习惯性地事无巨细都请教老员工,缺少了一份独立思考,成长的速度就会慢一些。
遇到疑问首先有自己的判断,不论判断的是否正确,经过一次思考后,对系统的理解将会上一个台阶。在熟悉系统的过程中,可以多问一下,如果是我来设计这个项目,或者由我来实现这个功能,应该怎么做。而且现有的项目可能由于历史原因,并不是以最优方式实现,对比自己期望的做法,可以快速了解到系统这样做的深层原因。通过一次次对自己的追问中,更快地理解系统的深层背景,同时增加自身的设计能力。而自己原有的技能,也能更好地反馈在新的项目当中。
概念理解
您对项目管理的理解
项目管理,简单来说,是指在特定的时间、预算和资源限制下,通过有效的规划、组织、领导和控制,实现项目目标的过程。
其核心要素包括明确的项目目标、详细的项目计划、合理的资源分配、有效的沟通协调以及严格的进度监控和风险管理。项目目标是整个项目的方向指引,计划则为实现目标制定具体步骤,资源分配要确保人力、物力和财力的合理运用,沟通协调能促进团队合作,进度监控和风险管理有助于及时发现和解决问题。
项目管理在企业中具有重要价值,它可以提高项目成功率,确保按时交付,降低成本,提升质量,增强客户满意度。比如在软件开发项目中,良好的项目管理能避免需求变更导致的混乱,保证按时推出稳定的产品。
常见的应用场景包括建筑工程、软件开发、市场营销活动等。与项目管理相关的概念如质量管理、风险管理等,相互补充和影响。质量管理关注项目成果的质量标准和保证措施,风险管理侧重于识别和应对可能影响项目的不确定性因素。
常见的应用场景包括建筑工程、软件开发、市场营销活动等。与项目管理相关的概念如质量管理、风险管理等,相互补充和影响。质量管理关注项目成果的质量标准和保证措施,风险管理侧重于识别和应对可能影响项目的不确定性因素。
我在之前的工作中参与过[具体项目],在其中负责[具体工作],深刻体会到项目管理中沟通和计划的重要性。通过有效的沟通,解决了团队成员之间的信息不对称问题,而合理的计划让项目进度始终可控。
总的来说,项目管理是一项综合性的工作,随着技术和市场的发展,未来可能会更加注重敏捷方法的应用和数字化工具的支持,以更好地应对快速变化的环境和需求
你对 redis 的理解
Redis 是一个开源的、高性能的键值对存储数据库。
其核心要素和特点包括:
- 数据结构丰富:支持字符串、哈希、列表、集合、有序集合等多种数据结构。
- 内存存储:数据存储在内存中,使得读写操作速度极快。
- 持久化机制:提供了 RDB 和 AOF 两种持久化方式,保证数据的安全性。
- 高并发处理能力:能处理大量的并发请求。
- 数据结构丰富:支持字符串、哈希、列表、集合、有序集合等多种数据结构。
- 内存存储:数据存储在内存中,使得读写操作速度极快。
- 持久化机制:提供了 RDB 和 AOF 两种持久化方式,保证数据的安全性。
- 高并发处理能力:能处理大量的并发请求。
Redis 的作用和价值在于:
- 缓存加速:将经常访问的数据存储在 Redis 中,减少对后端数据库的访问压力,提高系统性能。
- 任务队列:如使用列表实现消息队列。
- 分布式锁:保证在分布式环境下资源访问的唯一性和安全性。
- 缓存加速:将经常访问的数据存储在 Redis 中,减少对后端数据库的访问压力,提高系统性能。
- 任务队列:如使用列表实现消息队列。
- 分布式锁:保证在分布式环境下资源访问的唯一性和安全性。
它在很多应用场景中都有出色表现,比如:
- 电商网站的商品浏览量统计。
- 社交平台的在线用户状态管理。
- 电商网站的商品浏览量统计。
- 社交平台的在线用户状态管理。
与其他数据库相比,Redis 是内存型数据库,适合快速读写的场景,而传统关系型数据库更适合复杂的关系和事务处理。
在我之前的项目中,我们使用 Redis 作为缓存,大幅提高了系统的响应速度。例如,在用户登录验证时,先从 Redis 中获取用户信息,如果不存在再从数据库查询并写入 Redis,这样有效减少了数据库的查询次数
总的来说,Redis 是一款功能强大、应用广泛的数据库,随着技术的发展,它在云计算、大数据等领域的应用也会越来越广泛。
谈谈你对 Seata 的理解
Seata 是一款开源的分布式事务解决方案,它致力于提供高性能和简单易用的分布式事务服务。
在微服务架构下,由于数据库和应用服务被拆分,原本在一个事务单元中的多个数据操作,可能会变成跨进程或跨数据库的多个事务单元的操作,传统的数据库事务难以处理这类问题,于是分布式事务的概念应运而生。分布式事务本质上是要解决跨网络节点的多个事务的数据一致性问题。
常见的解决方法包括强一致性和最终一致性(也叫弱一致性)。强一致性要求所有事务参与者要么全部成功,要么全部失败,全局事务协调者需掌握每个参与者的执行状态来决定数据的提交或回滚;而最终一致性则允许多个网络节点的数据暂时不一致,但在最终的某个时间点会达到一致。基于 CAP 定理,强一致性方案可能对应用的性能和可用性产生影响,所以在对数据一致性要求不那么高的场景中,通常会采用最终一致性算法。
在分布式事务的实现方面,强一致性可以通过基于 XA 协议的二阶段提交来达成,而最终一致性可以基于 TCC 事务模型、可靠性消息模型等方案来实现。Seata 中封装了四种分布式事务模式:
- **AT 模式**:这是一种基于本地事务和二阶段协议来实现最终数据一致性的方案,也是 Seata 默认的模式。它采用两阶段提交,一阶段中业务数据和回滚日志记录在同一个本地事务中提交,释放本地锁和连接资源;二阶段提交异步化,能快速完成,若需回滚则通过一阶段的回滚日志进行反向补偿。该模式的优点是对代码无侵入,且并发度高,本地锁在一阶段就会释放,也不需要数据库对 XA 协议的支持。但它只能用于支持 ACID 的关系型数据库,且 SQL 解析还不能支持全部语法。
- **TCC 模式**:工作分为 prepare、commit、cancel 三个阶段。TM 向 TC 申请全局事务 XID 并传播给各个子调用,子调用的所在 TM 向 TC 注册分支事务并执行本地 prepare,然后向 TC 报告执行结果,TC 根据各分支事务的执行结果确定二阶段是执行 commit 还是 rollback。TCC 模式的优点是不依赖本地事务,但缺点是回滚逻辑需手动编码,业务侵入性较大。
- **Saga 模式**:Saga 是将一个长事务分解成一系列的子事务集合,每个子事务都有对应的补偿动作,用于撤销该子事务产生的影响。在 Saga 模式中,业务流程中的每个参与者都提交本地事务,若出现某一个参与者失败,则补偿前面已经成功的参与者,一阶段的正向服务和二阶段的补偿服务均由业务开发实现。这种模式适用于全局事务中涉及到无法管理其资源管理器的外部系统,或者难以改造成 TCC 的情况。
- **XA 模式**:可以认为是一种强一致性的事务解决方法,它利用事务资源(如数据库、消息服务等)对 XA 协议的支持,以 XA 协议的机制来管理分支事务。
从这四种模式可以看出,在不同的业务场景中,可以使用 Seata 的不同事务模式来解决相应的分布式事务问题,因此 Seata 可被视为一个一站式的分布式解决方案。Seata 的出现简化了分布式事务的处理,让开发者能够更专注于核心业务的开发,而无需过多关注分布式事务的复杂细节。其官网地址为:https://seata.io/zh-cn/docs/overview/what-is-seata.html 。
在实际应用中,Seata 的三个核心组件相互协作来实现分布式事务的管理。其中,TC(Transaction Coordinator,事务协调者)负责维护全局和分支事务的状态,驱动全局事务的提交或回滚;TM(Transaction Manager,事务管理器)用于定义全局事务的范围,开始、提交或回滚全局事务;RM(Resource Manager,资源管理器)则管理分支事务上的资源,向 TC 注册分支事务,汇报分支事务的状态,并驱动分支事务的提交或回滚。TC 以 Server 形式独立部署,而 TM 和 RM 集成在应用中启动。
总之,Seata 为解决分布式系统中的事务问题提供了有效且灵活的手段,帮助确保分布式环境下数据的一致性和可靠性。但具体使用哪种模式,需要根据业务的需求、特点以及对一致性的要求等因素来综合考虑。
谈谈你对 Seata 的理解
服务架构下,由于数据库和应用服务的拆分,导致原本一个事务单元中的多个DML 操作,变成了跨进程或者跨数据库的多个事务单元的多个 DML 操作,而传统的数据库事务无法解决这类的问题,所以就引出了分布式事务的概念。
分布式事务本质上要解决的就是跨网络节点的多个事务的数据一致性问题,业内常见的解决方法有两种
a. 强一致性,就是所有的事务参与者要么全部成功,要么全部失败,全局事务协调者需要知道每个事务参与者的执行状态,再根据状态来决定数据的提交或者回滚!
b. 最终一致性,也叫弱一致性,也就是多个网络节点的数据允许出现不一致的情况,但是在最终的某个时间点会达成数据一致。
基于 CAP 定理我们可以知道,强一致性方案对于应用的性能和可用性会有影响,所以对于数据一致性要求不高的场景,就会采用最终一致性算法。
在分布式事务的实现上,对于强一致性,我们可以通过基于 XA 协议下的二阶段提交来实现,对于弱一致性,可以基于 TCC 事务模型、可靠性消息模型等方案来实现。
市面上有很多针对这些理论模型实现的分布式事务框架,我们可以在应用中集成这些框架来实现分布式事务。而 Seata 就是其中一种,它是阿里开源的分布式事务解决方案,提供了高性能且简单易用的分布式事务服务。
Seata 中封装了四种分布式事务模式,分别是:
AT 模式,是一种基于本地事务+二阶段协议来实现的最终数据一致性方案,也是 Seata默认的解
TCC 模式,TCC 事务是 Try、Confirm、Cancel 三个词语的缩写,简单理解就是把一个完整的业务逻辑拆分成三个阶段,然后通过事务管理器在业务逻辑层面根据每个分支事务的执行情况分别调用该业务的 Confirm 或者 Cacel方法。
Saga 模式,Saga 模式是 SEATA 提供的长事务解决方案,在 Saga 模式中,业务流程中每个参与者都提交本地事务,当出现某一个参与者失败则补偿前面已经成功的参与者。
XA 模式,XA 可以认为是一种强一致性的事务解决方法,它利用事务资源(数据库、消息服务等)
b 树和 b+树的理解
B 树和 B+树都是用于数据库和文件系统中高效组织和检索数据的数据结构。
B 树是一种平衡的多路查找树,每个节点可以存储多个关键字和对应的孩子指针。它具有以下特点:
1. 节点中的关键字数量满足一定的范围,以保持树的平衡。
2. 可以在非叶子节点中存储数据,也可以只存储索引。
3. 适合在磁盘等外存设备上进行数据存储和检索,因为它可以减少磁盘 I/O 次数。
B+树是 B 树的一种变形,它与 B 树有一些不同之处:
1. 非叶子节点只用于索引,不存储实际的数据,所有的数据都存储在叶子节点中。
2. 叶子节点之间通过链表相连,方便进行范围查询。
在数据库系统中,B+树常用于索引结构,因为其具有更好的范围查询性能和更高的磁盘空间利用率。
在我参与的一个数据库优化项目中,曾经对使用 B 树和 B+树的索引进行过性能评估和调整。通过分析数据访问模式和查询需求,选择了更适合的索引结构,显著提高了数据库的查询效率。
以下是一个简单的 B 树示例代码(核心部分):
```java
class BTreeNode {
int[] keys;
BTreeNode[] children;
int numKeys;
boolean isLeaf;
// 构造函数、插入、删除等方法
}
```
B 树是一种平衡的多路查找树,每个节点可以存储多个关键字和对应的孩子指针。它具有以下特点:
1. 节点中的关键字数量满足一定的范围,以保持树的平衡。
2. 可以在非叶子节点中存储数据,也可以只存储索引。
3. 适合在磁盘等外存设备上进行数据存储和检索,因为它可以减少磁盘 I/O 次数。
B+树是 B 树的一种变形,它与 B 树有一些不同之处:
1. 非叶子节点只用于索引,不存储实际的数据,所有的数据都存储在叶子节点中。
2. 叶子节点之间通过链表相连,方便进行范围查询。
在数据库系统中,B+树常用于索引结构,因为其具有更好的范围查询性能和更高的磁盘空间利用率。
在我参与的一个数据库优化项目中,曾经对使用 B 树和 B+树的索引进行过性能评估和调整。通过分析数据访问模式和查询需求,选择了更适合的索引结构,显著提高了数据库的查询效率。
以下是一个简单的 B 树示例代码(核心部分):
```java
class BTreeNode {
int[] keys;
BTreeNode[] children;
int numKeys;
boolean isLeaf;
// 构造函数、插入、删除等方法
}
```
能谈一下 CAS 机制吗?
豆包回答
CAS 机制,即 Compare and Swap(比较并交换),是一种实现多线程同步的原子操作机制。
它的核心思想是:在对一个变量进行更新操作时,先比较变量的当前值是否与预期值相等,如果相等则将变量更新为新值,否则不进行更新。
CAS 机制的优点包括:
1. 非阻塞性:线程不会被挂起等待,而是立即返回结果。
2. 高效性:避免了锁带来的上下文切换和线程阻塞开销。
然而,CAS 机制也存在一些局限性:
1. ABA 问题:可能存在值从 A 变为 B 再变回 A 的情况,仅通过 CAS 无法察觉中间的变化。
2. 只能保证单个变量的原子性操作。
在我之前参与的一个高并发系统优化项目中,就使用了 CAS 机制来优化共享数据的并发访问。例如在一个计数器的更新操作中,通过 CAS 保证了计数的准确和高效。
以下是一个简单的 CAS 实现示例代码(核心部分):
```java
import java.util.concurrent.atomic.AtomicInteger;
public class CASExample {
public static void main(String[] args) {
AtomicInteger atomicInteger = new AtomicInteger(0);
int expectedValue = 0;
int newValue = 1;
boolean updated = atomicInteger.compareAndSet(expectedValue, newValue);
System.out.println("更新是否成功: " + updated + ", 当前值: " + atomicInteger.get());
}
}
```
它的核心思想是:在对一个变量进行更新操作时,先比较变量的当前值是否与预期值相等,如果相等则将变量更新为新值,否则不进行更新。
CAS 机制的优点包括:
1. 非阻塞性:线程不会被挂起等待,而是立即返回结果。
2. 高效性:避免了锁带来的上下文切换和线程阻塞开销。
然而,CAS 机制也存在一些局限性:
1. ABA 问题:可能存在值从 A 变为 B 再变回 A 的情况,仅通过 CAS 无法察觉中间的变化。
2. 只能保证单个变量的原子性操作。
在我之前参与的一个高并发系统优化项目中,就使用了 CAS 机制来优化共享数据的并发访问。例如在一个计数器的更新操作中,通过 CAS 保证了计数的准确和高效。
以下是一个简单的 CAS 实现示例代码(核心部分):
```java
import java.util.concurrent.atomic.AtomicInteger;
public class CASExample {
public static void main(String[] args) {
AtomicInteger atomicInteger = new AtomicInteger(0);
int expectedValue = 0;
int newValue = 1;
boolean updated = atomicInteger.compareAndSet(expectedValue, newValue);
System.out.println("更新是否成功: " + updated + ", 当前值: " + atomicInteger.get());
}
}
```
书中回答
CAS 是 Java 中 Unsafe 类里面的方法,它的全称是 CompareAndSwap,比较并交换的意思。它的主要功能是能够保证在多线程环境下,对于共享变量的修改的原子性。
我来举个例子,比如说有这样一个场景(如图),有一个成员变量 state,默认值是 0,定义了一个方法 doSomething(),这个方法的逻辑是,判断 state 是否为 0 ,如果为0,就修改成 1。这个逻辑看起来没有任何问题,但是在多线程环境下,会存在原子性的问题,因为这里是一个典型的,Read - Write 的操作。
一般情况下,我们会在 doSomething()这个方法上加同步锁来解决原子性问题。
但是,加同步锁,会带来性能上的损耗,所以,对于这类场景,我们就可以使用 CAS机制来进行优化这个是优化之后的代码(如图)在 doSomething()方法中,我们调用了 unsafe 类中的 compareAndSwapInt()方法来达到同样的目的,这个方法有四个参数,分别是:当前对象实例、成员变量 state 在内存地址中的偏移量、预期值 0、期望更改之后的值 1。
CAS 机制会比较 state 内存地址偏移量对应的值和传入的预期值 0 是否相等,如果相等,就直接修改内存地址中 state 的值为 1.否则,返回 false,表示修改失败,而这个过程是原子的,不会存在线程安全问题。
CompareAndSwap 是一个 native 方法,实际上它最终还是会面临同样的问题,就是先从内存地址中读取 state 的值,然后去比较,最后再修改。这个过程不管是在什么层面上实现,都会存在原子性问题。
所以呢,CompareAndSwap 的底层实现中,在多核 CPU 环境下,会增加一个 Lock指令对缓存或者总线加锁,从而保证比较并替换这两个指令的原子性。
CAS 主要用在并发场景中,比较典型的使用场景有两个。
1. 第一个是 J.U.C 里面 Atomic 的原子实现,比如 AtomicInteger,AtomicLong。
2. 第二个是实现多线程对共享资源竞争的互斥性质,比如在 AQS、ConcurrentHashMap,ConcurrentLinkedQueue 等都有用到。
分布式事务的原理
书中
分布式事务是指事务的参与者、支持事务的服务器、资源服务器以及事务管理器分别位于分布式系统的不同节点之上。
比如大型的电商系统中的下单场景,会涉及扣库存、优惠促销计算、订单 ID 生成。通常情况下,库存、促销、主键生成策略都位于不同的服务器和数据库表中。下单接口的成功与否,不仅取决于本地节点的数据库操作,而且还依赖第三方系统的结果,这时候分布式事务就保证这些操作要么全部成功,要么全部失败。因此,本质上来说,分布式事务就是为了保证不同数据库的数据一致性。基于 CAP 定理可以知道,对于上述情况产生的分布式事务问题,我们要么采用强一致性方案、要么采用弱一致性方案。
所谓强一致性,就是指通过第三方的事务管理器来协调多个节点的事务,保证每个节点的事务达到同时成功和同时失败,为了实现这样一个需求,我们会引入 Xopen/DTP 模型提供的XA 协议,基于 2pc 或者 3pc 的方式来实现。但是,在如果全局事务管理器中的多个节点中,如果任意一个节点在进行事务提交确认时,由于网络通信延迟导致阻塞,就会影响到所有节点事务的提交,而这个阻塞过程也会影响阻塞用户的请求线程,这对于用户体验以及整体性能的影响较大。而弱一致性方案,就是针对强一致性方案所衍生出来性能和数据一致性平衡的一个方案,简单来说就是损失掉强一致性,数据在某一个时刻会存在不一致的状态,但是最终这些数据会达成一致。这样的好处是提升了系统的性能。在弱一致性方案中,常见的解决方案:
1. 使用分布式消息队列,来实现最终一致性
2. 基于 TCC 事务,通过演进版本的二阶段提交实现最终一致性
3. 使用 Seata 事务框架,它提供了多种事务模式,如 AT、XA、Saga、TCC 等
豆包
分布式事务指的是在分布式系统中,涉及多个节点或服务的事务操作。
其原理主要基于以下几个关键概念和技术:
1. 两阶段提交(2PC):分为准备阶段和提交阶段。在准备阶段,协调者询问参与者是否可以提交事务;在提交阶段,根据参与者的反馈决定是提交还是回滚事务。
2. 三阶段提交(3PC):在 2PC 的基础上增加了预提交阶段,减少了阻塞的可能性。
3. 基于消息的事务:通过可靠的消息队列来保证事务的最终一致性。
4. TCC(Try-Confirm-Cancel)模式:通过自定义的尝试、确认和取消操作来实现事务。
在我之前参与的一个大型分布式电商系统中,我们综合使用了 2PC 和基于消息的事务来处理复杂的分布式事务场景。比如在订单支付和库存扣减的过程中,通过 2PC 保证了支付和库存操作的原子性,同时利用消息队列来处理可能的异常情况,确保最终一致性。
以下是一个简单的基于 2PC 的分布式事务核心代码示例(仅作示意,不包含完整的异常处理等):
```java
import java.util.concurrent.ConcurrentHashMap;
class Coordinator {
private ConcurrentHashMap<String, Participant> participants = new ConcurrentHashMap<>();
public void addParticipant(String id, Participant participant) {
participants.put(id, participant);
}
public boolean preparePhase() {
for (Participant participant : participants.values()) {
if (!participant.prepare()) {
return false;
}
}
return true;
}
public void commitPhase() {
for (Participant participant : participants.values()) {
participant.commit();
}
}
public void rollbackPhase() {
for (Participant participant : participants.values()) {
participant.rollback();
}
}
}
interface Participant {
boolean prepare();
void commit();
void rollback();
}
class DatabaseParticipant implements Participant {
private boolean canCommit;
@Override
public boolean prepare() {
// 模拟准备操作
canCommit = true;
return canCommit;
}
@Override
public void commit() {
// 模拟提交操作
System.out.println("数据库提交成功");
}
@Override
public void rollback() {
// 模拟回滚操作
System.out.println("数据库回滚成功");
}
}
```
其原理主要基于以下几个关键概念和技术:
1. 两阶段提交(2PC):分为准备阶段和提交阶段。在准备阶段,协调者询问参与者是否可以提交事务;在提交阶段,根据参与者的反馈决定是提交还是回滚事务。
2. 三阶段提交(3PC):在 2PC 的基础上增加了预提交阶段,减少了阻塞的可能性。
3. 基于消息的事务:通过可靠的消息队列来保证事务的最终一致性。
4. TCC(Try-Confirm-Cancel)模式:通过自定义的尝试、确认和取消操作来实现事务。
在我之前参与的一个大型分布式电商系统中,我们综合使用了 2PC 和基于消息的事务来处理复杂的分布式事务场景。比如在订单支付和库存扣减的过程中,通过 2PC 保证了支付和库存操作的原子性,同时利用消息队列来处理可能的异常情况,确保最终一致性。
以下是一个简单的基于 2PC 的分布式事务核心代码示例(仅作示意,不包含完整的异常处理等):
```java
import java.util.concurrent.ConcurrentHashMap;
class Coordinator {
private ConcurrentHashMap<String, Participant> participants = new ConcurrentHashMap<>();
public void addParticipant(String id, Participant participant) {
participants.put(id, participant);
}
public boolean preparePhase() {
for (Participant participant : participants.values()) {
if (!participant.prepare()) {
return false;
}
}
return true;
}
public void commitPhase() {
for (Participant participant : participants.values()) {
participant.commit();
}
}
public void rollbackPhase() {
for (Participant participant : participants.values()) {
participant.rollback();
}
}
}
interface Participant {
boolean prepare();
void commit();
void rollback();
}
class DatabaseParticipant implements Participant {
private boolean canCommit;
@Override
public boolean prepare() {
// 模拟准备操作
canCommit = true;
return canCommit;
}
@Override
public void commit() {
// 模拟提交操作
System.out.println("数据库提交成功");
}
@Override
public void rollback() {
// 模拟回滚操作
System.out.println("数据库回滚成功");
}
}
```
两阶段提交(2PC)是一种保证分布式事务原子性和一致性的协议。
在 2PC 中,主要涉及两个阶段:
准备阶段(Prepare Phase):协调者向所有参与者发送准备请求,询问它们是否能够完成事务操作。参与者在收到准备请求后,执行本地事务操作,但不提交,将执行结果记录下来,并返回是否可以提交的响应给协调者。如果所有参与者都返回可以提交,那么进入提交阶段;否则,协调者会通知所有参与者回滚事务。
提交阶段(Commit Phase):如果在准备阶段所有参与者都表示可以提交,协调者会向所有参与者发送提交请求,参与者收到后正式提交事务,并释放相关资源。如果在准备阶段有参与者返回不能提交,协调者会向所有参与者发送回滚请求,参与者执行回滚操作。
2PC 的优点在于原理相对简单,能够保证强一致性。但也存在一些明显的缺点:
同步阻塞问题:在执行过程中,参与者在等待协调者的指令时处于阻塞状态,占用资源。
单点故障:协调者一旦出现故障,整个事务流程就会受到影响。
数据不一致风险:在提交阶段,如果部分参与者收到提交请求而部分未收到,可能导致数据不一致。
在 2PC 中,主要涉及两个阶段:
准备阶段(Prepare Phase):协调者向所有参与者发送准备请求,询问它们是否能够完成事务操作。参与者在收到准备请求后,执行本地事务操作,但不提交,将执行结果记录下来,并返回是否可以提交的响应给协调者。如果所有参与者都返回可以提交,那么进入提交阶段;否则,协调者会通知所有参与者回滚事务。
提交阶段(Commit Phase):如果在准备阶段所有参与者都表示可以提交,协调者会向所有参与者发送提交请求,参与者收到后正式提交事务,并释放相关资源。如果在准备阶段有参与者返回不能提交,协调者会向所有参与者发送回滚请求,参与者执行回滚操作。
2PC 的优点在于原理相对简单,能够保证强一致性。但也存在一些明显的缺点:
同步阻塞问题:在执行过程中,参与者在等待协调者的指令时处于阻塞状态,占用资源。
单点故障:协调者一旦出现故障,整个事务流程就会受到影响。
数据不一致风险:在提交阶段,如果部分参与者收到提交请求而部分未收到,可能导致数据不一致。
关于“你对 Spring Cloud 的理解”
书中
Spring Cloud 是一套分布式微服务的技术解决方案,它提供了快速构建分布式系统的常用的一些组件比如说配置管理、服务的注册与发现、服务调用的负载均衡、资源隔离、熔断降级等等不过 Spring Cloud 只是Spring 官方提供的一套微服务标准定义,而真正地实现目前有两套体系用得比较多。一个是 Spring Cloud Netflix一个是 Spring Cloud Alibaba
Spring Cloud Netflix 是基于 Netflix 这个公司的开源组件集成的一套微服务解决方案,其中的组件有
1. Ribbon——负载均衡2. Hystrix——服务熔断3.Zuul——网关4. Eureka——服务注册与发现5. Feign——服务调用
Spring Cloud Alibaba 是基于阿里巴巴开源组件集成的一套微服务解决方案,其中包括
1. Dubbo——消息通讯2. Nacos——服务注册与发现3.Seata——事务隔离4. Sentinel——熔断降级
谈谈你对 ES 的理解
书中
ES是一个基于 Apache Lucene 开源的一个分布式搜索引擎框架。一般用它来做● 日志记录和分析● 公共数据采集● 全文检索● 数据可视化分析等等
Elasticsearch ,简称 ES 。它是建立在全文搜索引擎库 Apache Lucene 基础之上的一个开源的搜索引擎,也可以作为 NoSQL 数据库,存储任意格式的文档和数据。也可以做大数据的分析,是一个跨界开源产品。它最主要的应用场景是 ELK 的日志分析系统。另外它还有以下特点:
1. 第一,采用 Master-slave 架构,实现数据的分片和备份
2. 第二,使用 Java 编写,并对 Lucene 进行封装,隐藏了 Lucene 的复杂性
3. 第三,能胜任上百个服务节点的扩展,并支持 PB 级别的结构化或者非结构化数据
4. 第四,ES 提供的 Restful API,不仅简化了 ES 的操作,还支持任何语言的客户端提供 API 接口,另外 Restful API 的风格还实现了 CURD 操作、创建索引,删除索引等功能。
在 ES 的使用上我也有一些经验心得,比如:
1. es 里面复杂的关联查询尽量别用,一旦用了性能都不太好。最好是先在 Java 系统里就完成关联,将关联好的数据直接写入 ES中
2. 避免一些太复杂的操作,比如join/nested/parent-child 搜索,不然容易出现性能问题。
3. 避免深分页查询,ES 集群的分页查询支持 from 和 size 参数,查询的时候,每个分片必须构造一个长度为 from+size 的优先队列,然后回传到网关节点,网关节点再对这些优先队列进行排序找到正确的 size 文档。当 from 足够 大的时候,就算不发生 OOM,也会影响到CPU 和带宽等,从而影响到整个集群的性能。
谈谈你对 MVCC 的理解
书中
对于 MVCC 的理解,我觉得可以先从数据库的三种并发场景说起:
第一种:读读,就是线程 A 与线程 B 同时在进行读操作,这种情况下不会出现任何并发问题。
第二种:读写,就是线程 A 与线程 B 在同一时刻分别进行读和写操作。这种情况下,可能会对数据库中的数据造成以下问题:事物隔离性问题,出现脏读,幻读,不可重复读的问题;
第三种:写写,就是线程 A 与线程 B 同时进行写操作这种情况下可能会存在数据更新丢失的问题。
而 MVCC 就是为了解决事务操作中并发安全性问题的无锁并发控制技术全称为 Multi-VersionConcurrency Control ,也就是多版本并发控制。它是通过数据库记录中的隐式字段,undo 日志 ,Read View 来实现的。
MVCC 主要解决了三个问题
第一个是:通过 MVCC 可以解决读写并发阻塞问题从而提升数据并发处理能力
第二个是:MVCC 采用了乐观锁的方式实现,降低了死锁的概率
第三个是:解决了一致性读的问题也就是事务启动时根据某个条件读取到的数据,直到事务结束时,再次执行相同条件,还是读到同一份数据,不会发生变化。
而我们在使用 MVCC 时一般会根据业务场景来选择组合搭配乐观锁或悲观锁。这两个组合中,MVCC 用来解决读写冲突,乐观锁或者悲观锁解决读写冲突从而最大程度地提高数据库并发性能。
豆包
MVCC(多版本并发控制)是一种用于数据库管理系统中实现并发控制的技术。
MVCC 的核心概念是在数据库中为每行数据保存多个版本,每个事务在操作数据时,看到的是符合其事务开始时一致性状态的版本。这样可以实现多个事务并发读取和修改数据,而不会相互阻塞。
MVCC 的工作原理通常包括以下几个方面:
1. 版本号:为每行数据添加版本号,用于标识不同的修改版本。
2. 事务隔离级别:不同的隔离级别决定了事务能看到的数据版本范围。
3. 读操作:读操作可以读取特定版本的数据,而不会被写操作阻塞。
MVCC 的优点有:
1. 提高并发性能:允许多个事务并发执行,减少了锁竞争和阻塞。
2. 实现较高的事务隔离级别:在不引入过多锁开销的情况下,提供较好的隔离性。
MVCC 的核心概念是在数据库中为每行数据保存多个版本,每个事务在操作数据时,看到的是符合其事务开始时一致性状态的版本。这样可以实现多个事务并发读取和修改数据,而不会相互阻塞。
MVCC 的工作原理通常包括以下几个方面:
1. 版本号:为每行数据添加版本号,用于标识不同的修改版本。
2. 事务隔离级别:不同的隔离级别决定了事务能看到的数据版本范围。
3. 读操作:读操作可以读取特定版本的数据,而不会被写操作阻塞。
MVCC 的优点有:
1. 提高并发性能:允许多个事务并发执行,减少了锁竞争和阻塞。
2. 实现较高的事务隔离级别:在不引入过多锁开销的情况下,提供较好的隔离性。
说一下你对 Redis 的理解
书中
1. Redis 是一个高性能的基于 Key-Value 结构存储的 Nosql 开源数据库。
2. 目前市面上绝大部分公司都采用 Redis 来实现分布式缓存,从而提高数据的检索效率。
3. Redis 之所以这么流行,主要有几个特点:
a. 它是基于内存存储,在进行数据 IO 操作时,能够 10WQPS
b. 提供了非常丰富的数据存储结构,如String、List、Hash、Set、ZSet 等。
c. Redis 底层采用单线程实现数据的 IO,所以在数据算法层面并不需要考虑并发安全性,所以底层算法上的时间复杂度基本上都是常量。
4. Redis 虽然是内存存储,但是它也可以支持持久化,避免因为服务器故障导致数据丢失的问题
基于这些特点,Redis 一般用来实现分布式缓存,从而降低应用程序对关系型数据库检索带来的性能影响。除此之外,Redis 还可以实现分布式锁、分布式队列、排行榜、查找附近的人等功能,为复杂应用提供非常方便和成熟的解决方案
说一下你对 zookeeper 的理解
书中
对于 Zookeeper 的理解,我觉得可以从分布式系统中的三种典型应用场景说起:
第一种:集群管理在多个节点组成的集群中,为了保证集群的HA 特性,每个节点都会冗余一份数据副本。这种情况下需要保证客户端访问集群中的任意一个节点都是最新的数据。
第二种:分布式锁如何保证跨进程的共享资源的并发安全性,对于分布式系统来说也是一个比较大的挑战,而为了达到这样一个目的,必须使用跨进程的锁也就是分布式锁来实现。
第三种:Master 选举在多个节点组成的集群中,为了降低集群数据同步的复杂度,一般会存在 Master 和Slave 两种角色的节点,Master 负责事务和非事务请求处理,Slave 负责非事务请求处理。但是在分布式系统中如何确定某个节点是 Master 还是 Slave,也成了一个难度不小的挑战。
它是一个分布式开源协调组件,简单来说,就是类似于一个裁判员的角色,专门负责协调和解决分布式系统中的各类问题。
1. 集群管理(凸显)Zookeeper 提供了 CP 的模型,来保证集群中的每个节点的数据一致性,当然 Zk 本身的集群并不是CP 模型,而是顺序一致性模型,如果要保证 CP 特性,需要调用`sync`同步方法。
2. 分布式锁(凸显)Zookeeper 提供了多种不同的节点类型,如持久化节点、临时节点、有序节点、容器节点等,其中对于分布式锁这个场景来说,Zookeeper 可以利用有序节点(插入图片)的特性来实现。除此之外,还可以利用同一级节点的唯一性特性来实现分布式锁。
3. Master 选举(凸显)Zookeeper 可以利用持久化节点来存储和管理其他集群节点的信息,从而进行 Master 选举机制。或者还可以利用集群中的有序节点特性,来实现 Master 选举。
目前主流的 Kafka、Hbase、Hadoop 都是通过 Zookeeper 来实现集群节点的主动选举。
总的来说,Zookeeper 就是经典的分布式数据一致性解决方案,致力于为分布式应用提供高性能、高可用,并且具有严格顺序访问控制能力的分布式协调服务。它底层通过基于 Paxos 算法演化而来的ZAB 协议实现。
说说什么是 jvm
书中
JVM 全称是 Java 虚拟机,JVM运⾏并管理 Java 源码⽂件所⽣成的 Class⽂件,在不同的操作系统上安装不同的 JVM,从⽽实现了跨平台的保证。⼀般情况下,对于开发者⽽⾔,即使不熟悉JVM 的运⾏机制并不影响业务代码的开发,因为在安装完JDK 或者 JRE 之后,其中就已经内置了 JVM,所以只需要将 Class⽂件交给 JVM 运⾏即可。但当程序运⾏的过程中出现了问题,⽽这个问题发生在 JVM 层⾯的,那我们就需要熟悉 JVM 的运⾏机制,才能迅速排查并解决JVM 的性能问题。
我们先看下目前主流的 JVM HotSpot 的架构图,通过这张架构图,我们可以看出 JVM的大致流程是把一个 class 文件通过类加载器加载进系统,然后放到不同的区域,通过编译器编译。
第一个部分 Class Files
在 Java 中,Class⽂件是由源码⽂件⽣成的,⾄于源码⽂件的内容,是每个 Java 开发者在 JavaSE 阶段的必备知识,这⾥就不再赘述了,我们可以关注⼀下 Class⽂件的格式,⽐如其中的常量池、成员变量、⽅法
第二个部分 Class Loader Subsystem 即类加载机制
Class⽂件加载到内存中,需要借助 Java 中的类加载机制。类加载机制分为装载、链接和初始化,其主要就是对类进⾏查找、验证以及分配相关的内存空间和赋值
第三个部分 Runtime Data Areas 也就是通常所说的运⾏时数据区
其解决的问题就是 Class⽂件进入内存之后,该如何进⾏存储不同的数据以及数据该如何进⾏扭转。比如:Method Area 通常会储存由 Class⽂件常量池所对应的运⾏时常量池、字段和⽅法的元数据信息、类的模板信息等;Heap 是存储各种 Java 中的对象实例;Java Threads 通过线程以栈的⽅式运⾏加载各个⽅法;Native Internal Thread 可以理解为是加载运⾏native 类型的⽅法;PCRegister 则是保存每个线程执⾏⽅法的实时地址。
第四个部分 Garbage Collector 也就是通常所说的垃圾回收
就是对运⾏时数据区中的数据进⾏管理和回收。回收机制可以基于不同的垃圾收集器,⽐如 Serial、Parallel、CMS、G1、ZGC等,可以针对不同的业务场景选择不同的收集器,只需要通过 JVM 参数设置即可。如果我们打开 hotspot 的源码,可以发现这些收集器其实就是对于不同垃圾收集算法的实现,核⼼的算法有 3 个:标记-清除、标记-整理、复制
第五个部分是 JIT Compiler 和 Interpreter
通俗理解就是翻译器,Class 的字节码指令通过 JIT Compiler 和 Interpreter 翻译成对应操作系统的CPU 指令,只不过可以选择解释执⾏或者编译执⾏,在 HotSpot JVM 默认采用的是这两种⽅式的混合。
第六就是 JNI 的技术
如果我们想要找 Java 中的某个 native⽅法是如何通过 C 或者 C++实现的,那么可以通过 NativeMethod Interface 来进⾏查找,也就是所谓的 JNI 技术。通过官⽹上给出的 HotSpot 架构图,我们就能够知道 JVM 到底是如何运行的了,当然在实际操作的
什么是负载均衡
书中
关于负载均衡,我会从四个方面去说。
1. 负载均衡产生的背景
在互联网发展早期,由于用户量较少、业务需求也比较简单。对于软件应用,我们只需要一台高配的服务器即可完成业务的支撑,这样的软件架构称为单体架构,随着用户量的增加,服务器的请流量也随之增加,在这个过程中单体架构会产生两个问题。
1. 软件的性能逐步下降,访问延迟越来越高
2. 容易出现单点故障
为了解决这2个问题,我们引入了集群化部署的架构,也就是把一个软件应用同时部署在多个服务器上
架构的变化带来了两个问题:
1. 客户端请求如何均匀地分发到多台目标服务器上?
2. 如何检测目标服务器的健康状态,使得客户端请求不向已经宕机的服务器发送请求。
为了解决这两个问题,引入了负载均衡的设计,简单来说,负载均衡机制的核心目的是让客户端的请求合理均匀地分发到多台目标服务器,由于请求被多个节点分发,使得服务端的性能得到有效地提升。
2. 负载均衡的实现技术
常见的实现方案有三种
基于 DNS 实现负载均衡
基于 DNS 实现负载均衡的方式,它的实现方式比较简单,只需要在 DNS服务器上针对某个域名做多个 IP 映射即可。
它的工作原理是:当用户通过域名访问某个网站时,会先通过 DNS 服务器进行域名解析得到一个 IP地址,DNS 服务器可以随机 分配一个 IP 地址进行访问,这样就可以实现目标服务集群的请求分发。
除此之外,DNS 还可以根据不同的地域分配就近机房的 IP,比如长沙的小伙伴,可能会得到在湖南范围内最近的一个机房的 IP,在这个模式下可以实现“就近原则”实现请求处理,缩短了通信距离从而提升网站访问效率。
DNS 实现负载均衡的优点是: 配置简单,实现成本低,无需额外的开发和维护。
不过缺点也很明显:由于 DNS 多级缓存的特性,当我们修改DNS 配置之后,会因为缓存导致 IP 变更不及时,从而影响负载均衡的效果。
基于硬件实现负载均衡
硬件负载设备,我们可以简单把它理解成一个网络设备,类似于网络交换机
它的性能很好,每秒能够处理百万级别的请求
支持多种负载均衡算法,我们可以非常灵活地配置不同的负载策略
它还具备防火墙等安全功能。
硬件负载是商业产品,有专门的售后来支持,所以企业不需要花精力去做维护。
F5 是比较常见的硬件负载设备,由于硬件负载设备价格比较贵,一般应用在大型银行、政府、电信等领域。
基于软件实现负载均衡
所谓软件负载,就是通过一些开源软件或者商业软件来完成负载均衡的功能。常见的软件负载技术有:Nginx、LVS、HAProxy 等。
目前互联网企业绝大部分采用的都是软件负载,主要原因是:
1. 免费,企业不需要投入较高的成本。
2. 开源,不同企业对于负载均衡的要求有差异,所以可以基于开源软件上做二次开发。
3. 灵活性较高
这三种方式,没有好坏之分,只有是否合适,因此大家可以根据实际情况选择。
3. 负载均衡的作用范围
负载均衡是作用在网络通信上,来实现请求的分发。而在网络架构中,基于 OSI 模型,又分为 7层网络模型
也就是意味着我们可以在网络的某些分层上做请求分发处理,因此根据这样一个特性,对于负载均衡的作用范围又可以分为2,3,4,7层负载
二层负载:基于 Mac 地址来实现请求分发,一般采用虚拟 Mac 的方式实现,服务器收到请求后,通过动态分配后端服务的实际 Mac 地址进行响应从而实现负载均衡
三层负载:基于 IP 层负载,一般通过虚拟IP 的方式实现,外部请求访问虚拟 IP,服务器收到请求后根据后端实际 IP 地址进行转发。
四层负载:通过请求报文中的目标地址和端口进行负载,Nginx、F5、LVS 等都可以实现四层负载。
七层负载:七层负载是基于应用层负载,也就是服务器端可以根据 http 协议中请求的报文信息来决定把请求分发到哪个目标服务器上,比如 Cookie、消息体、RequestHeader 等。
4. 负载均衡的常用算法
所谓负载均衡算法,就是决定当前客户端请求匹配到目标服务器集群中的具体哪个节点。常见的负载均衡算法有:
1. 轮训,也就是多台服务器按照顺序轮训返回,这样每个服务器都能获得相同的请求次数
2. 随机,根据随机算法获得一个目标服务地址(就像古时候皇帝翻牌子),由于该算法具备随机性,因此每个服务器获得的请求数量不一定均等。
3. 一致性 hash,也就是对于具有相同 hash码的请求,永远发送到同一个节点上。
4. 最小连接数,根据目标服务器的请求数量 来决定请求分发的权重,也就是目标服务集群中,请求更少的节点将会获得更多的请求。这是负载均衡中比较好的策略,真正能够实现目标服务器的请求均衡。
什么是消息队列
书中
消息队列 Message Queue,简称 MQ。是一种应用间的通信方式,主要由三个部分组成。
生产者:Producer,消息的产生者与调用端,主要负责消息所承载的业务信息的实例化,是一个队列的发起方
代理:Broker,主要的处理单元,负责消息的存储、投递,及各种队列附加功能的实现,是消息队列最核心的组成部分
消费者:Consumer,一个消息队列的终端,也是消息的调用端,具体是根据消息承载的信息,处理各种业务逻辑。
消息队列的应用场景较多,常用的可以分为三种:
异步处理
主要应用于对实时性要求不严格的场景,比如:用户注册发送验证码、下单通知、发送优惠券等等。服务方只需要把协商好的消息发送到消息队列,剩下的由消费消息的服务去处理,
应用解耦
应用解耦可以看作是把相关但耦合度不高的系统联系起来。比如订单系统与 WMS、EHR 系统,有关联但不那么紧密,每个系统之间只需要把约定的消息发送到MQ,另外的系统去消费即可。解决了各个系统可以采用不同的架构、语言来实现,从而大大增加了系统的灵活性。
流量削峰
流量削峰一般应用在大流量入口且短时间内业务需求处理不完的服务中心,为了权衡高可用,把大量的并行任务发送到 MQ 中,依据 MQ 的存储及分发功能,平稳地处理后续的业务,起到一个大流量缓冲的作用。
目前市面上常见的消息队列中间件主要有ActiveMQ、RabbitMQ、Kafka、RocketMQ 这几种,在架构技术选型的时候一般根据业务的需求选择合适的中间件:
比如中小型公司,低吞吐量的一般用ActiveMQ、RabbitMQ 较为合适,大数据高吞吐量的大型公司一般选用 Kafka 和RocketMQ。
布隆过滤器到底是什么东西?它有什么用
书中
BitMap 的基本原理就是用一个 bit 位来存储当前数据是否存在的状态值,也就是把一个数据通过hash 运算取模后落在 bit 位组成的数组中,通过 1 对该位置进行标记。这种方式适用于大规模数据,但数据状态又不是很多的情况,通常是用来判断某个数据存不存在的。
布隆过滤器就是在位图的基础上做的一个优化设计,它的原理是,当一个元素被加入集合时,通过 K 个散列函数将这个元素映射成一个位数组中的 K 个点,把它们置为 1。检索的时候,使用同样的方式去映射,只要看到每个映射的位置的值是不是 1,就可以大概知道该元素是否存在集合中了。如果这些点有任何一个 0,则被检查的元素一定不在;如果都是 1,则被检查的元素很可能存在。
说一下你对双亲委派的理解
书中
首先,我简单说一下类的加载机制,就是我们自己写的 java 源文件到最终运行,必须经过编译和类加载两个阶段。
编译的过程就是把.java 文件编译成.class 文件。
类加载的过程,就是把 class 文件装载到JVM 内存中,装载完成以后就会得到一个Class 对象
而类的加载过程,需要涉及类加载器。
JVM 在运行的时候,会产生 3 个类加载器,这三个类加载器组成了一个层级关系每个类加载器分别去加载不同作用范围的 jar包,
比如Bootstrap ClassLoader,主要是负责 Java 核心类库的加载,也就是 %{JDK_HOME}\lib 下的rt.jar、resources.jar 等
Extension ClassLoader,主要负责%{JDK_HOME}\lib\ext 目录下的 jar 包和class 文件
Application ClassLoader,主要负责当前应用里面的 classpath 下的所有 jar 包和类文件
除了系统自己提供的类加载器以外,还可以通过 ClassLoader 类实现自定义加载器,去满足一些特殊需求;
比如当需要加载一个 class 文件的时候,首先会把这个 class 的查询和加载委派给父加载器去执行,如果父加载器都无法加载,再尝试自己来加载这个 class。
这样设计的好处,我认为有几个。
安全性,因为这种层级关系实际上代表的是一种优先级,也就是所有的类的加载,优先给 BootstrapClassLoader。那对于核心类库中的类,就没办法去破坏,比如自己写一个java.lang.String,最终还是会交给启动类加载器。再加上每个类加载器的作用范围,那么自己写的 java.lang.String 就没办法去覆盖类库中类。
我认为这种层级关系的设计,可以避免重复加载导致程序混乱的问题,因为如果父加载器已经加载过了,那幺子类就没必要去加载了。
豆包
双亲委派模型是 Java 类加载机制中的一种重要原则。
它指的是当一个类加载器收到类加载的请求时,首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成。只有当父类加载器无法完成加载请求时,子加载器才会尝试自己去加载。
双亲委派模型的出现主要是为了保证 Java 程序的稳定性和安全性,避免类的重复加载以及恶意代码的篡改。
常见的场景比如在加载核心类库时,始终由启动类加载器来完成,保证了核心类库的唯一性和权威性。
在解决实际问题时,可能会遇到需要打破双亲委派模型的情况,比如热部署等场景。
它指的是当一个类加载器收到类加载的请求时,首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成。只有当父类加载器无法完成加载请求时,子加载器才会尝试自己去加载。
双亲委派模型的出现主要是为了保证 Java 程序的稳定性和安全性,避免类的重复加载以及恶意代码的篡改。
常见的场景比如在加载核心类库时,始终由启动类加载器来完成,保证了核心类库的唯一性和权威性。
在解决实际问题时,可能会遇到需要打破双亲委派模型的情况,比如热部署等场景。
什么是守护线程,它有什么特点
书中
简单来说,守护线程就是一种后台服务线程,他和我们在 Java 里面创建的用户线程是一模一样的。守护线程和用户线程的区别有几个点,这几个点也是守护线程本身的特性:1. 在线程创建方面,对于守护线程,我们需要主动调用 setDaemon()并且设置成true。2. 我们知道,一个 Java 进程中,只要有任何一个用户线程还在运行,那么这个 java 进程就不会结束,否则,这个程序才会终止。注意,Java 进程的终止与否,只和用户线程有关。如果当前还有守护线程正在运行,也不会阻止 Java程序的终止。因此,守护线程的生命周期依赖于用户线程。
守护线程不能用在线程池或者一些 IO任务的场景里面,因为一旦 JVM 退出之后,守护线程也会直接退出。就可能可能导致任务没有执行完或者资源没有正确释放的问题。
基础概念
中间件
kafka 的零拷贝原理?
1. 是什么?
- Kafka 的零拷贝是一种数据传输优化技术,旨在减少数据在不同存储区域之间的复制操作。
- Kafka 的零拷贝是一种数据传输优化技术,旨在减少数据在不同存储区域之间的复制操作。
2. 能解决什么问题?
- 解决了传统数据传输方式中多次数据拷贝带来的性能瓶颈问题,包括大量的 CPU 开销、内存带宽消耗以及数据传输延迟。
- 解决了传统数据传输方式中多次数据拷贝带来的性能瓶颈问题,包括大量的 CPU 开销、内存带宽消耗以及数据传输延迟。
3. 有什么优势和特点?
- 优势在于显著提高了数据传输的效率,减少了系统资源的消耗,特别是在处理大规模数据和高并发场景下,能够提升 Kafka 的整体性能和吞吐量。特点是避免了不必要的数据拷贝,直接在内核空间完成数据的传输。
- 优势在于显著提高了数据传输的效率,减少了系统资源的消耗,特别是在处理大规模数据和高并发场景下,能够提升 Kafka 的整体性能和吞吐量。特点是避免了不必要的数据拷贝,直接在内核空间完成数据的传输。
4. 核心原理是什么?
- 核心原理是利用操作系统提供的机制,如 Linux 中的 `sendfile` 系统调用,以及 Java 中的 `FileChannel.transferTo()` 方法,将数据从文件系统缓存(内核缓冲区)直接传输到网络套接字缓冲区,而无需经过用户空间缓冲区。
- 核心原理是利用操作系统提供的机制,如 Linux 中的 `sendfile` 系统调用,以及 Java 中的 `FileChannel.transferTo()` 方法,将数据从文件系统缓存(内核缓冲区)直接传输到网络套接字缓冲区,而无需经过用户空间缓冲区。
5. 为什么能解决这类问题?
- 因为传统的数据传输方式需要多次在用户空间和内核空间之间进行数据拷贝,这增加了 CPU 的负担和数据传输的时间。而零拷贝技术直接在内核空间完成数据传输,避免了这些额外的拷贝操作,从而能够高效地处理大量数据,满足高并发的需求。
- 因为传统的数据传输方式需要多次在用户空间和内核空间之间进行数据拷贝,这增加了 CPU 的负担和数据传输的时间。而零拷贝技术直接在内核空间完成数据传输,避免了这些额外的拷贝操作,从而能够高效地处理大量数据,满足高并发的需求。
整合后
Dubbo 连问
什么是 Dubbo?它有哪些核心功能?
书中
Dubbo 是以高性能 RPC 框架,它提供了分布式架构下的服务之间通信方案,使得开发者可以不需要关心网络通信的细节。通过该框架可以使得远程服务调用方式和本地服务调用方式一样简单。Dubbo 是一款高性能、轻量级的开源 RPC框架。由 10 层模式构成,整个分层依赖由上至下。
也可以理解为三层
第一层的 Business 业务逻辑层由我们自己来提供接口和实现还有一些配置信息。
第二层的 RPC 调用的核心层负责封装和实现整个 RPC 的调用过程、负载均衡、集群容错、代理等核心功能。
Remoting 则是对网络传输协议和数据转换的封装。
Dubbo 提供了六大核心能力
面向接口代理的高性能 RPC 调用。
智能容错和负载均衡。
服务自动注册和发现。
高度可扩展能力。
运行期流量调度
可视化的服务治理与运维。
既然说到 Dubbo 的功能,请详细说说Dubbo 负载均衡的几种策略
书中
Dubbo 有五种负载策略:
第一种是加权随机:假设我们有一组服务器servers = [A, B, C],他们对应的权重为weights = [5, 3,2],权重总和为 10。现在把这些权重值平铺在一维坐标值上,0, 5) 区间属于服务器 A,5, 8) 区间属于服务器 B,8,10) 区间属于服务器 C。接下来通过随机数生成器生成一个范围在 0, 10) 之间的随机数,然后计算这个随机数会落到哪个区间上就可以了。
第二种是最小活跃数:每个服务提供者对应一个活跃数 active,初始情况下,所有服务提供者活跃数均为 0。每收到一个请求,活跃数加 1,完成请求后则将活跃数减 1。在服务运行一段时间后,性能好的服务提供者处理请求的速度更快,因此活跃数下降得也越快,此时这样的服务提供者能够优先获取到新的服务请求。
第三种是一致性 hash:通过 hash 算法,把provider 的 invoke 和随机节点生成 hash,并将这个 hash投射到 [0, 2^32 - 1] 的圆环上,查询的时候根据 key 进行 md5 然后进行 hash,得到第一个节点的值大于等于当前hash 的 invoker。
第四种是加权轮询:比如服务器 A、B、C权重比为 5:2:1,那么在 8 次请求中,服务器 A 将收到其中的 5 次请求,服务器 B 会收到其中的 2 次请求,服务器 C 则收到其中的 1 次请求。
第五种是最短响应时间权重随机:计算目标服务的请求的响应时间,根据响应时间最短的服务,配置更高的权重进行随机访问。
Dubbo 的工作原理是什么样的?
书中
示意图
1.服务启动的时候,provider 和 consumer根据配置信息,连接到注册中心 register,分别向注册中心注册和订阅服务
2.register根据服务订阅关系,返回 provider 信息到consumer,同时 consumer 会把 provider信息缓存到本地。如果信息有变更,consumer 会收到来自 register 的推送
3.consumer 生成代理对象,同时根据负载均衡策略,选择一台 provider,同时定时向 monitor 记录接口的调用次数和时间信息
4.拿到代理对象之后,consumer 通过代理对象发起接口调用
5.provider 收到请求后对数据进行反序列化,然后通过代理调用具体的接口实现
最后再说说 Dubbo 与 Spring Cloud 的区别吧!
书中
Dubbo 是 SOA 时代的产物,它的关注点主要在于服务的调用,流量分发、流量监控和熔断。而 Spring Cloud 诞生于微服务架构时代,考虑的是微服务治理的方方面面,另外由于依托了 Spirng、Spirng Boot 的优势之上,两个框架在开始目标就不一致
Dubbo 定位服务治理、Spirng Cloud 是一个生态。
两者最大的区别是 Dubbo 底层是使用 Netty这样的 NIO 框架,是基于 TCP 协议传输的,配合以Hession 序列化完成 RPC 通信。
而SpringCloud 是基于 Http 协议+Rest 接口调用远程过程的通信,相对来说,Http 请求会有更大的报文,占的带宽也会更多。但是REST 相比 RPC 更为灵活,服务提供方和调用方的依赖只依靠一纸契约,不存在代码级别的强依赖。
Zookeeper 和 Redis 哪种作为分布式锁更好?理由是什么
书中
为什么使用分布式锁?
使用分布式锁的目的,是为了保证同一时间只有一个 JVM 进程可以对共享资源进行操作。根据锁的用途可以细分为以下两类:
允许多个客户端操作共享资源,我们称为共享锁
这种锁的一般是对共享资源具有幂等性操作的场景,主要是为了避免重复操作共享资源频繁加锁带来的性能开销。
只允许一个客户端操作共享资源,我们称为排他锁
这种锁一般是用在对共享资源操作具有非幂等性操作的场景,也就是需要保证在同一时刻只有一个进程或者线程能够访问这个共享资源。
目前实现分布式锁最常用的中间件是Redis 和 Zookeeper.
Redis 可以通过两种方式来实现
i. 利用 Redis 提供的`SET key value NXPX milliseconds `指令,这个指令是设置 一个 key-value,如果 key 已经存在,则返回0,否则返回 1,我们基于这个返回值来判断锁的占用情况从而实现分布式锁。
ii. 基于 Redission 客户端来实现分布式锁,Redisson 提供了分布式锁的封装方法,我们只需要调用 api 中的`lock()`和`unlock()`方法。它帮我们封装锁实现的细节和复杂度
1. redisson 所有指令都通过 lua 脚本执行并支持 lua 脚本原子性执行
2. redisson 中有一个 watchdog 的概念,翻译过来就是看门狗,它会在你获取锁之后,每隔 10 秒帮你把 key 的超时时间设为30s,就算一直持有锁也不会出现 key 过期了。“看门狗”的逻辑保证了没有死锁发生。
Zookeeper 实现分布式锁的方法比较多,我们可以使用有序节点来实现
i. 每个线程或进程在 Zookeeper 上的`lock`目录下创建一个临时有序的节点表示去抢占锁,所有创建的节点会按照先后顺序生成一个带有序编号的节点。
ii. 线程创建节点后,获取/lock 节点下的所有子节点,判断当前线程创建的节点是否是所有的节点的序号最小的。
iii.如果当前线程创建的节点是所有节点序号最小的节点,则认为获取锁成功。
iv.如果当前线程创建的节点不是所有节点序号最小的节点,则对节点序号的前一个节点添加一个事件监听,当前一个被监听的节点释放锁之后,触发回调通知,从而再次去尝试抢占锁。
对于 redis 的分布式锁而言,它有以下缺点:
它获取锁的方式简单粗暴,如果获取不到锁,会不断尝试获取锁,比较消耗性能。
Redis 是 AP 模型,在集群模式中由于数据的一致性会导致锁出现问题,即便使用Redlock 算法来实现,在某些复杂场景下,也无法保证其实现 100%的可靠性。
过在实际开发中使用 Redis 实现分布式锁还是比较常见,而且大部分情况下不会遇到极端复杂的场景,更重要的是 Redis 性能很高,在高并发场景中比较合适。
豆包
docker 连问
什么是 Docker
书中
我个人理解,Docker 是一个超轻量级的虚拟机,也是实现容器化技术的一种应用工具。Docker 是使用时下很火的 Golang 语言进行开发的,基于 Linux 内核的 cgroup,namespace,以及AUFS 类的 UnionFS 等技术,对进程进行封装隔离,属于操作系统层面的虚拟化技术。 由于隔离的进程独立于宿主和其他的隔离进程,因此也被称为容器。
面试官:那你再说说为什么要用 Dcoker,它具有哪些优势?
基于我的理解,Docker 有六大优势。
资源占用小
由于容器不需要进行硬件虚拟,也不需要运行完整操作系统等额外的资源开销,使得Docker 对系统资源的利用率更高,无论是应用执行速度还是文件存储速度,都要比传统虚拟机技术更高效,内存消 耗更少 。
启动速度快
传统的虚拟机技术启动应用服务往往需要较长时间,而 Docker 容器应用,由于直接运行于宿主内核,无需启动完整的操作系统,因此可以做到秒级,甚至毫秒级的启动时间,大大地节约了开发,测试,部署的时间。
迁移更轻松
由于 Docker 确保了执行环境的一致性,使得应用的迁移更加容易,Docker 可以在很多平台上运行,无论是物理机,虚拟机,公有云,私有云,它们的运行结果是一致的,因此用户可以很轻易地将一个平台上运行的应用,迁移到另一个平台上,而不用担心运行环境的变化导致应用无法正常运行这类的问题。
维护和拓展更轻松
docker 使用的分层存储和镜像技术,让应用重复部分的复用更容易,也让应用的维护更新更简单,基于基础镜像进一步扩展镜像也变得十分简单。另外,docker 团队和各个开源项目团队一起维护了一大批高质量的官网镜像,既可以直接在生产环境使用,又可以作为基础进一步定制,大大降低了应用服务的镜像制作成本。
运行环境一致
开发过程中一个常见的问题是环境一致性问题,由于开发环境,测试环境,生产环境不一致,导致有些 bug 并未在开发过程中被发现,而 Docker 的镜像提供了除内核外完整的运行时环境,确保了应用运行环境一致性。
持续交付和部署
使用 Docker 可以通过定制应用镜像来实现持续集成,持续交付,部署。开发人员可以通过 Dockerfile来进行镜像构建,并结合持续集成系统进行集成测试,而运维人员则可以在生产环境中快速部署该镜像,甚至结合持续部署系统进行自动部署
再说说 Docker 核心组件吧!
Docker 包含三个基本组件:
镜像(Image)
一个特殊的文件系统,简单地理解,Docker 镜像就是一个 Linux的文件系统(Root FileSystem),这个文件 系统里面包含可以运行在 Linux 内核的程序以及相应的数据。
一个镜像可以包含一个完整的操作系统环境,里面仅安装了 Apache 或用户需要的其他应用程序。镜像可以用来创建 Docker 容器。Docker 提供了一个很简单的机制来创建镜像或者更新现有的镜像,用户甚至可以直接从其他人那里下载一个已经做好的镜像来直接使用。
容器(Contaier)
镜像运行的实体,Docker 利用容器来运行应用。容器是从镜像创建的运行实例。它可以被启动、开始、停止、删除。每个容器都是相互隔离的、保证安全的平台。可以把容器看作是一个简易版的 Linux 环境(包括 root 用户权限、进程空间、用户空间和网络空间等)和运行在其中的应用程序。
仓库(Repository)
集中存放镜像文件的地方
仓库是集中存放镜像文件的场所。很多人会把仓库和仓库注册服务器(Registry)混为一谈。实际上,仓库注册服务器上往往存放着多个仓库,每个仓库中又包含了多个镜像,每个镜像有不同的标签(tag)。仓库分为公开仓库(Public)和私有仓库(Private)两种形式。最大的公开仓库是Docker Hub,存放了数量庞大的镜像供用户下载。
K8s连问
说说什么是 kubernetes
书中
K8s 全称 Kubernetes,8 是中间 8 个字母的简称。作为一种容器自动部署、扩容以及管理的技术,我们可以简单理解其是一种容器编排技术。前身是 Borg系统,在谷歌内部已经有了十多年的使用经验。
那什么是容器编排技术?
我们先以 Docker 容器为例,如果大家不熟悉什么是 Docker 的话,可以翻阅一下Docker 相关的内容。
在一台 centos 物理机上安装了Docker 镜像,然后在镜像上跑几个container 容器。
那最明显的一个问题是,当物理机 centos资源不够使用的时候,就没办法再创建新的container 了,另外一个问题就是单点故 障,也就是当安装了 Docker Engine 的这台centos 机器挂了之后,所有的 container 都会挂掉,这样很多中间件和服务都变成不可用了,所以最直接的解决方案,就是对centos 机器做集群化的部署;
但问题是怎样通过一个统一的入口,管理安装了 Docker Engine 的 3 台 centos 机器,不然每台 centos就变成孤立的了,这里就需要用到容器编排的技术,我们就直接以 K8s中的组件和大家来分析一下,它究竟是如何做到统一管理的。
可以看到这是由 3 台 centos 机器组成的Docker 集群或者 K8s 集群,其中 1 台是master,另外 2 台是 worker。每台机器上都安装了 Docker Engine,当需要对容器进行操作时,会通过 kubectl 来接 收用户的操作命令。API Server 会做客户端的命令进行认证授权,接下来会通过 Scheduler 组件进行调度,就可以知道要创建的资源到底会落到哪个机器上。然后会通过不同类型的 Controller Manager进行管理要创建的资源,比如要将资源创建在其中一个 worker 节点上,就会把请求转发给 worker 节点的 kube-proxy 组件。
看到这里,要跟大家强调一点,在 K8s 中最小的操作单元是 Pod,它可以理解为是 container 的逻辑单位,不会直接操作docker 中的 container,而是会以 Pod 的形式来操作,因为 Pod 里面包含了各个类型的 container。
那在 worker 中是如何操作 Pod 的呢?
就是通过 kubelet 组件完成的。当然可以发现,整个 K8s 集群中还可以持久化一些数据到 ETCD 组件里,可以通过 Dashboard 查看整个集群资源的状态,可以通过 DNS 组件进行相应的域名解析等, 这些组件和功能都是可选的、可扩展的。到这里我们不妨再想一个问题,K8s 集群中的物理机可以是不同的硬件和操作系统吗?显然是没问题的,这样一来我们就可以通过Docker 和 K8s 来屏蔽基础设施的差异性,在上层进行容器创建以及管理就变得方便多了,所以 K8s 也是云原生中很重要的基础保障。
最下面的基础设施可以是各种公有云、私有云、混合云,在此基础上构建K8s 的集群支撑,然后就可以非常方便调度管理不同的容器了。
Redis 为什么这么快?
书中
决定 Redis 请求效率的因素主要是三个方面,分别是网络、cpu、内存。
在网络层面,Redis 采用多路复用的设计,提升了并发处理的连接数,不过这个阶段,{如图}Server端的所有 IO 操作,都是由同一个主线程处理的这个时候 IO 的瓶颈就会影响到 Redis 端的整体处理性能。
所以从 Redis6.0 开始{如图},在多路复用及层面增加了多线程的处理,来优化 IO 处理的能力。不过,具体的数据操作仍然是由主线程来处理的,所以我们可以认为 Redis 对于数据IO 的处理依然是单线程。
从 CPU 层面来说,Redis 只需要采用单线程即可,原因有两个。
如果采用多线程,对于 Redis 中的数据操作,都需要通过同步的方式来保证线程安全性,这反而会影响到 redis 的性能在 Linux 系统上 Redis 通过 pipelining 可以处理 100w 个请求每秒,而应用程序的计算复杂度主要是 O(N) 或 O(log(N)) ,不会消耗太多CPU从内存层面来说,Redis 本身就是一个内存数据库,内存的 IO 速度本身就很快,所以内存的瓶颈只是受限于内存大小。最后,Redis 本身的数据结构也做了很多的优化,比如压缩表、跳跃表等方式降低了时间复杂度,同时还提供了不同时间复杂度的数据类型。
Redis 存在线程安全问题吗?为什么?
书中
Redis Server 本身是一个线程安全的 K-V数据库,也就是说在 Redis Server 上执行的指令,不需要任何同步机制,不会存在线程安全问题。
虽然 Redis 6.0 里面,增加了多线程的模型,但是增加的多线程只是用来处理网络 IO 事件,对于指令的执行过程,仍然是由主线程来处理,所以不会存在多个线程同时执行操作指令的情况。
Redis Server 本身可能出现的性能瓶颈点无非就是网络 IO、CPU、内存。但是 CPU 不是 Redis,的瓶颈点,所以没必要使用多线程来执行指令。
如果采用多线程,意味着对于 redis 的所有指令操作,都必须考虑到线程安全问题,也就是说需要加锁来解决,这种方式带来的性能影响反而更大。
第二个,从 Redis 客户端层面。虽然 Redis Server 中的指令执行是原子的,但是如果有多个 Redis 客户端同时执行多个指令的时候,就无法保证原子性。假设两个 redis client 同时获取 RedisServer 上的 key1, 同时进行修改和写入, 因为多线程环境下的原子性无法被保障,以及多进程情况下的共享资源访问的竞争问题,使得数据的安全性无法得到保障。
当然,对于客户端层面的线程安全性问题,解决方法有很多,比如尽可能地使用 Redis里面的原子指令,或者对多个客户端的资源访问加锁,或者通过 Lua 脚本来实现多个指令的操作等等。
说一下你对 SPI 机制的理解以及 Dubbo 中有哪些 SPI 机制的实现
书中
SPI 全称为 Service Provider Interface,是Java 中一种服务发现机制。在 SPI 机制中,服务提供者为某个接口实现具体的类,而在运行时,通过 SPI 机制,查找到对应的实现类,并将其加载进行使用。比较典型的一个例子就是 java.jdbc.Driver这个数据库驱动接口,在 Java 中只提供了接口的定义,具体的实现类由各个数据库厂商提供的驱动包来完成,程序在运行的时候会根据当前导入的驱动包来完成对应数据库 的连接
在 Dubbo 中,根据 Java SPI 的思想,衍生了不同的 SPI 实现。指定名称的扩展点实现,也就是根据指定名称获取并加载对应的扩展点实现类
@Adaptive 注解定义自适应扩展点,它会根据上下文参数动态适配一个具体的实现类
@Activate 注解定义的激活扩展点,它会根据上下文的相关参数配置,来决定哪些扩展点会被动态激活,主要体现在 Filter 中。
RDB 和 AOF 的实现原理以及优缺点
书中
关于 RDB 和 AOF 的实现原理以及优缺点,我觉得可以分成三个部分去回答。
第一部分,先说明这两种持久化机制的特性。RDB 和 AOF 都是 Redis 里面提供的持久化机制,RDB 是通过快照方式实现持久化、AOF 是通过命令追加的方式实现持久化。
第二部分,说明这两种机制的工作原理。RDB 持久化机制会根据快照触发条件,把内存里面的数据快照写入到磁盘,以二进制的压缩文件进行存储。RDB 持久化机制会根据快照触发条件,把内存里面的数据快照写入到磁盘,以二进制的压缩文件进行存储。RDB 快照的触发方式有很多,比如执行 bgsave 命令触发异步快照,执行 save命令触发同步快照,同步快照会阻塞客户端的执行指 令。根据 redis.conf 文件里面的配置,自动触发bgsave主从复制的时候触发;
AOF 持久化机制是近乎实时的方式来完成持久化的,就是客户端执行一个数据变更的操作,Redis Server 就会把这个命令追加到 aof 缓冲区的末尾,然后再把缓冲区的数据写入到磁盘的 AOF 文件里面,至于最终什么时候真正持久化到磁盘,是根据刷盘的策略来决定的。
第三部分,AOF 和 RDB 的优缺点分析我认为 RDB 和 AOF 的优缺点有两个。RDB 是每隔一段时间触发持久化,因此数据安全性低,AOF 可以做到实时持久化,数据安全性较高RDB 文件默认采用压缩的方式持久化,AOF存储的是执行指令,所以 RDB 在数据恢复的时候性能比 AOF 要好
Redis 的内存淘汰算法和原理是什么?
书中
Redis 里面的内存淘汰策略,是指内存的使用率达到 maxmemory 上限的时候的一种内存释放的行为。Redis 里面提供了很多中内存淘汰算法,归纳起来主要就四种
1. Random 算法,随机移除某个 key
2. TTL 算法 ,在设置了过期时间的键中,把更早过期时间的 key 有限移除
3. LRU 算法,移除最近很少使用的 key
LRU 是比较常见的一种内存淘汰算法,在 Redis 里面会维护一个大小为 16 的候选池,这个候选池里面的数据会根据时间进行排序,然后每一次随机取出 5 个 key 放入到这个候选池里面,当候选池满了以后,访问的时间间隔最大的 key 就会从候选池里面取出来淘汰掉。通过这样的一个设计,就可以把真实的最少访问的 key 从内存中淘汰掉。但是这样的一种 LRU 算法还是存在一个问题,假如一个 key 的访问频率很低,但是最近一次偶尔被访问到,那么 LRU 就会认为这是一个热点 Key,不会被淘汰。
4. LFU 算法,最不经常使用
相比于 LRU,LFU 增加了访问频率这个维度来统计数据的热点情况。LFU 的主要设计是,使用了两个双向链表形成一个二维双向链表,一个用来保存访问频率,另一个用来保存访问频率相同的所有元素。当添加元素的时候,访问次数默认为 1,于是找到相同访问频次的节点,然后添加到相同频率节点对应的双向链表头部。当元素被访问的时候,就会增加对应 key 的访问频次,并且把当前访问的节点移动到下一个频次节点。有可能出现某个数据前期访问次数很多,然后后续就一直不用了,如果单纯按照访问频率,这个 key就很难被淘汰,所以在 LFU 中通过使用频率和上次访问时间来标记数据的热度,如果有读写,就增加;
所以,通过 LFU 算法改进之后,就可以真正达到非热点数据的淘汰了。当然,LFU 也有缺点,相比 LRU算法,LFU 增加了访问频次的维护,以及实现的复杂度要比 LRU 更高。
网上
redis内存淘汰策略,具体如下:
1.no-envicition:该策略对于写请求不再提供服务,会直接返回错误,当然排除del等特殊操作,redis默认是no-envicition策略。
2.allkeys-random:从redis中随机选取key进行淘汰
3.allkeys-lru:使用LRU(Least Recently Used,最近最少使用)算法,从redis中选取使用最少的key进行淘汰
4.volatile-random:从redis中设置过过期时间的key,进行随机淘汰
5.volatile-ttl:从redis中选取即将过期的key,进行淘汰
6.volatile-lru:使用LRU(Least Recently Used,最近最少使用)算法,从redis中设置过过期时间的key中,选取最少使用的进行淘汰
7.volatile-lfu:使用LFU(Least Frequently Used,最不经常使用),从设置了过期时间的键中选择某段时间之内使用频次最小的键值对清除掉8.allkeys-lfu:使用LFU(Least Frequently Used,最不经常使用),从所有的键中选择某段时间之内使用频次最少的键值对清除
1.no-envicition:该策略对于写请求不再提供服务,会直接返回错误,当然排除del等特殊操作,redis默认是no-envicition策略。
2.allkeys-random:从redis中随机选取key进行淘汰
3.allkeys-lru:使用LRU(Least Recently Used,最近最少使用)算法,从redis中选取使用最少的key进行淘汰
4.volatile-random:从redis中设置过过期时间的key,进行随机淘汰
5.volatile-ttl:从redis中选取即将过期的key,进行淘汰
6.volatile-lru:使用LRU(Least Recently Used,最近最少使用)算法,从redis中设置过过期时间的key中,选取最少使用的进行淘汰
7.volatile-lfu:使用LFU(Least Frequently Used,最不经常使用),从设置了过期时间的键中选择某段时间之内使用频次最小的键值对清除掉8.allkeys-lfu:使用LFU(Least Frequently Used,最不经常使用),从所有的键中选择某段时间之内使用频次最少的键值对清除
Kafka 如何保证消息不丢失?
书中
kafka 是一个用来实现异步消息通信的中间件,它的整个架构由 Producer、Consumer、Broker
首先是 Producer 端,需要确保消息能够到达 Broker 并实现消息存储,在这个层面,有可能出现网络问题,导致消息发送失败,所以,针对 Producer 端,可以通过 2 种方式来避免消息丢失
Producer 默认是异步发送消息,这种情况下要确保消息发送成功,有两个方法
a. 把异步发送改成同步发送,这样producer 就能实时知道消息发送的结果。
b. 添加异步回调函数来监听消息发送的结果,如果发送失败,可以在回调中重试。
Producer 本身提供了一个重试参数retries,如果因为网络问题或者 Broker 故障导致发送失败,Producer 会自动重试。
然后是 Broker 端,Broker 需要确保Producer 发送过来的消息不会丢失,也就是只需要把消息持久化到磁盘就可以了。
但是,Kafka 为了提升性能,采用了异步批量刷盘的实现机制,也就是说按照一定的消息量和时间间隔来刷盘,而最终刷新到磁盘的这个动作,是由操作系统来调度的,所以如果在刷盘之前系统崩溃,就会导致数据丢失。
Kafka 并没有提供同步刷盘的实现,所以针对这个问题,需要通过 Partition 的副本机制和 acks机制来一起解决。“我简单说一下 Partition 副本机制,它是针对每个数据分区的高可用策略,每个partition 副本集包含唯一的一个 Leader 和多个 Follower,Leader 专门处理事务类的请求,Follower 负责同步 Leader的数据”。在这样的一种机制的基础上,kafka 提供了一个 acks 的参数,Producer 可以设置acks 参数再结合Broker 的副本机制来群共同保障数据的可靠性。
acks 有几个值的选择。
9. acks=0, 表示 producer 不需要等Broker 的响应,就认为消息发送成功,这种情况会存在消息丢失。
10. acks=1, 表示 Broker 中的 LeaderPartition 收到消息以后,不等待其他Follower Partition 同步完,就给 Producer 返回确认,这种情况下 Leader Partition 挂了,会存在数据丢失。
11. acks=-1,表示 Broker 中的 LeaderParititon 收到消息后,并且等待 ISR 列表中的 follower 同步完成,再给 Producer 返回确认,这个配置可以保证数据的可靠性。
最后,就是 Consumer 必须能消费到这个消息,实际上,我认为,只要 producer 和broker 的消息可靠得到了保障,那么消费端是不太可能出现消息无法消费的问题,除非是 Consumer 没有消费完这个消息就直接提交了,但是即便是这个情况,也可以通过调整 offset 的值来重新消费。
豆包
Kafka 保证消息不丢失主要通过以下几个方面:
首先,消息的持久化机制。Kafka 会将消息存储在磁盘上,确保数据的可靠性。在数据写入磁盘时,采用了异步批量写入和顺序写入的方式,提高写入效率和数据的安全性。
其次,副本机制。为每个分区设置多个副本,当主副本出现故障时,能够从副本中恢复数据,保证消息不丢失。
再者,生产者的确认机制。生产者可以设置 acks 参数来控制消息发送的确认策略,例如 acks=all 表示只有当所有的副本都接收到消息后,才认为消息发送成功。
还有,消费者的正确处理。消费者在消费消息时,需要正确提交偏移量,以确保不会重复消费或丢失消息
首先,消息的持久化机制。Kafka 会将消息存储在磁盘上,确保数据的可靠性。在数据写入磁盘时,采用了异步批量写入和顺序写入的方式,提高写入效率和数据的安全性。
其次,副本机制。为每个分区设置多个副本,当主副本出现故障时,能够从副本中恢复数据,保证消息不丢失。
再者,生产者的确认机制。生产者可以设置 acks 参数来控制消息发送的确认策略,例如 acks=all 表示只有当所有的副本都接收到消息后,才认为消息发送成功。
还有,消费者的正确处理。消费者在消费消息时,需要正确提交偏移量,以确保不会重复消费或丢失消息
acks 参数用于控制生产者发送消息后等待服务器的确认方式。acks 可以有以下几种取值及含义:
acks=0:表示生产者在发送消息后,不需要等待服务器的任何确认。这种设置下,消息可能会丢失,但能获得最高的发送性能。
acks=1:默认值。表示生产者只要收到服务器的 Leader 分区的确认就认为消息发送成功。在这种情况下,如果 Leader 分区在消息成功复制到其他副本之前崩溃,可能会导致消息丢失。
acks=all:表示生产者需要等待所有的副本(包括 Leader 和 follower 分区)都确认接收到消息后,才认为消息发送成功。这能最大程度地保证消息不丢失,但会对性能有一定影响。
综合考虑我们项目对消息可靠性的高要求,以及对性能的可接受程度,选择将 acks 设置为 all 以确保消息不丢失。
acks=0:表示生产者在发送消息后,不需要等待服务器的任何确认。这种设置下,消息可能会丢失,但能获得最高的发送性能。
acks=1:默认值。表示生产者只要收到服务器的 Leader 分区的确认就认为消息发送成功。在这种情况下,如果 Leader 分区在消息成功复制到其他副本之前崩溃,可能会导致消息丢失。
acks=all:表示生产者需要等待所有的副本(包括 Leader 和 follower 分区)都确认接收到消息后,才认为消息发送成功。这能最大程度地保证消息不丢失,但会对性能有一定影响。
综合考虑我们项目对消息可靠性的高要求,以及对性能的可接受程度,选择将 acks 设置为 all 以确保消息不丢失。
网络
请说一下网络四元组
豆包回答
网络四元组通常指的是源 IP 地址、源端口、目的 IP 地址和目的端口这四个元素。
在网络通信中,这四个元素共同确定了一个特定的网络连接或通信会话。源 IP 地址标识了发送数据的设备在网络中的位置,源端口用于区分同一设备上的不同应用程序或进程。目的 IP 地址指定了接收数据的设备的网络位置,目的端口则用于确定接收设备上接收数据的应用程序或服务。
在处理网络数据包转发、网络安全策略制定、网络流量监控和分析等场景中,网络四元组都起着关键的作用。
在我参与的一个网络监控项目中,就通过对网络四元组的分析来识别异常的网络连接和流量模式。
以下是一个简单的网络四元组示例代码(核心部分):
```java
import java.net.InetAddress;
import java.net.Socket;
public class NetworkQuadrupleExample {
public static void main(String[] args) throws Exception {
InetAddress sourceIP = InetAddress.getLocalHost();
int sourcePort = 8080;
InetAddress destinationIP = InetAddress.getByName("www.example.com");
int destinationPort = 80;
Socket socket = new Socket(destinationIP, destinationPort);
}
}
```
在网络通信中,这四个元素共同确定了一个特定的网络连接或通信会话。源 IP 地址标识了发送数据的设备在网络中的位置,源端口用于区分同一设备上的不同应用程序或进程。目的 IP 地址指定了接收数据的设备的网络位置,目的端口则用于确定接收设备上接收数据的应用程序或服务。
在处理网络数据包转发、网络安全策略制定、网络流量监控和分析等场景中,网络四元组都起着关键的作用。
在我参与的一个网络监控项目中,就通过对网络四元组的分析来识别异常的网络连接和流量模式。
以下是一个简单的网络四元组示例代码(核心部分):
```java
import java.net.InetAddress;
import java.net.Socket;
public class NetworkQuadrupleExample {
public static void main(String[] args) throws Exception {
InetAddress sourceIP = InetAddress.getLocalHost();
int sourcePort = 8080;
InetAddress destinationIP = InetAddress.getByName("www.example.com");
int destinationPort = 80;
Socket socket = new Socket(destinationIP, destinationPort);
}
}
```
书中回答
四元组,简单理解就是在 TCP 协议中,去确定一个客户端连接的组成要素,它包括源IP 地址、目标 IP地址、源端口号、目标端口号。
情况下,我们对于网络通信的认识可能是这样。服务端通过 ServerSocket 建立一个对指定端口号的监听,比如 8080。 客户端通过目标 ip 和端口就可以和服务端建立一个连接,然后进行数据传输。
但是我们知道的是,一个 Server 端可以接收多个客户端的连接,比如像这种情况(如图)。那,当多个客户端连接到服务端的时候,服务端需要去识别每一个连接。
并且(如图),TCP 是全双工协议,也就是说数据允许在连接的两个方向上同时传输,因此这里的客户端,如果是反向通信,它又变成了服务端。
所以基于这两个原因,就引入了四元组的设计,也就是说,当一个客户端和服务端建立一个 TCP 连接的时候,通过源 IP 地址、目标 IP 地址、源端口号、目标端口号来确定一个唯一的 TCP 连接。因为服务器的 IP 和端口是不变的,只要客户端的 IP 和端口彼此不同就 OK 了。
什么是 RPC?
书中
什么是远程调用?
远程调用是指跨进程的功能调用,跨进程可以理解成一个计算机节点的多个进程,或者多个计算机节点的多个进程。
什么是 RPC?
全称为 Remote Procedure Call,翻译过来就是远程过程调用,它是一种通过网络从远程计算机程序上请求服务,而不需要了解底层网络技术的协议,凡是符合该协议的框架,我们都可以称它为 RPC 框架。关于 RPC 协议,通俗来讲就是,A 计算机提供一个服务,B 计算机可以像调用本地服务那样调用 A 计算机的服务。要实现 RPC,需要通过网络传输数据,并对调用的过程进行封装。
作为底层传输协议。RPC 强调的是过程调用,调用的过程对用户而言是透明的,用户不需要关心调用的细节,可以像调用本地服务一样调用远程服务。
一个完整的 RPC 架构里面包含了四个核心的组件,分别是 Client ,Server,Client Stub 以及 Server Stub:
1)客户端(Client),服务的调用方。
2)服务端(Server),真正的服务提供者。
3)客户端存根(Client Stub),存放服务端的地址消息,再将客户端的请求参数打包成网络消息,然后通过网络远程发送给服务方。
4)服务端存根(Server Stub),接收客户端发送过来的消息,将消息解包,并调用本地的方法。
目前比较流行的开源 RPC 框架有 Google的 gRPC,Facebook 的 Thrift,阿里巴巴的 Dubbo。
RPC 的运用场景和优势
在开发电商系统时,随着业务越来越复杂,走分布式架构这个方向是绝大部分互联网企业必然的选择。
分布式架构落地的里程碑,应该是阿里的去IOE 运动的成功,它让互联网企业看到了,如何利用更少的软硬件成本来支撑海量的用户。
分布式架构的核心,就是利用多个普通计算机节点,组成一个庞大而复杂的计算网络,提供高性能以及高并发的能力支撑。在分布式架构中,原本的单体应用被拆分成多个独立部署的服务分布在计算机网络上,这些服务必然需要通过网络进行数据交互。
RPC 和 HTTP 协议有什么区别?
RPC 全称(Remote Procedure Call),它是一种针对跨进程或者跨网络节点的应用之间的远程过程调用协议。它的核心目标是,让开发人员在进行远程方法调用的时候,就像调用本地方法一样,不需要额外为了完成这个交互做过的编码。
为了达到这样一个目标,就有了这样一个RPC 协议模型。Client stub 作为客户端存根,代理客户端请求并把请求参数进行组装,再把组装的参数通过 Pack 也就是序列化。最后通过远程通信协议把序列化的数据传输到目标服务器上。服务端收到数据以后,通过 Unpack 也就是反序列化,再通过 Server stub 服务端存根来调用目标方法获取执行结果。
所以,通过这样一个模型,就实现了 RPC的目标,让开发者在没有任何感知的情况下实现了远程方法的调用。
Http 协议是为 Web 浏览器与 Web 服务器之间的通信而设计的远程通信协议,它定 义了通信协议的报文规范(如图),我们可以使用 http 协议来实现跨网络节点的数据传输。
基于这样的特点,在 RPC 协议底层的数据传输,即可以直接使用 TCP 协议,也可以使用 http 协议。
微服务
什么是服务网格?
书中回答
服务网格,也就是 Service Mesh,它是专门用来处理服务通信的基础设施层。它的主要功能是处理服务之间的通信,并且负责实现请求的可靠性传递。
Service Mesh,我们通常把他称为第三代微服务架构,既然是第三代,那么意味着他是在原来的微服务架构下做的升级。
为了更好地说明 Service Mesh,那我就不得不说一下微服务架构部分的东西。首先,当我们把一个电商系统以微服务化架构进行拆分后,会得到这样的一个架构(如图),其中包括Webserver、payment、inventory 等等。
这些微服务应用,会被部署到Docker 容器,或者 Kubernetes 集群。由于每个服务的业务逻辑是独立的,比如payment 会实现支付的业务逻辑、order 实现订单的处理、Webserver 实现客户端
所以,服务之间必须相互通信,才能实现功能的完整性。比如用户把一个商品加入购物车,而在这个过程中,每个服务之间必须知道对方的通信地址,并且当有新的节点加入进来的时候,还需要对这些通信地址进行动态维护。所以,在第一代微服务架构中,每个微服务除了要实现业务逻辑以外,还需要解决上下游寻址、通讯,以及容错等问题。
于是,在第二代微服务架构下,引入了服务注册中心来实现服务之间的寻址,并且服务之间的容错机制、负载均衡也逐步形成了独立的服务框架,比如主流的 SpringCloud,或者 Spring CloudAlibaba。
在第二代微服务架构中,负责业务开发的小伙伴不仅仅需要关注业务逻辑,还需要花大量精力去处理微服务中的一些基础性配置工作,虽然 Spring Cloud 已经尽可能去完成了这些事情,但对于开发人员来说,学习Spring Cloud,以及针对 Spring Cloud 的配置和维护,仍然存在较大的挑战。另外 呢,也增加了整个微服务的复杂性。
实际上,在我看来,“微服务中所有的这些服务注册、容错、重试、安全等工作,都是为了保证服务之间通信的可靠性”。
于是,就有了第三代微服务架构,ServiceMesh。
原本模块化到微服务框架里的微服务基础能力,被进一步地从一个 SDK 中演进成了一个独立的代理进程-SideCarSideCar 的主要职责就是负责各个微服务之间的通信,承载了原本第二代微服务架构中 的服务发现、调用
之所以我们称 Service Mesh 为服务网格,是因为在大规模微服务架构中,每个服务的通信都是由SideCar 来代理的,各个服务之间的通信拓扑图,看起来就像一个网格形状Istio 是目前主流的 Service Mesh 开源框架。
豆包
服务网格是一种用于处理服务间通信的基础设施层。
它主要出现在微服务架构中,用于管理和控制服务之间复杂的网络流量、监控通信情况、实现安全策略等。
服务网格的实现方式通常是通过在每个服务实例旁部署一个代理,这些代理共同构成了服务网格的数据平面,负责实际的流量处理和策略执行。同时,还有一个控制平面,用于配置和管理这些代理的行为。
常见的服务网格解决方案有 Istio、Linkerd 等。
在我过往参与的一个大型微服务项目中,引入了 Istio 作为服务网格,有效地解决了服务间通信的可靠性、安全性和可观测性问题。例如,通过 Istio 的流量控制功能,实现了对不同版本服务的精确流量分配,以进行金丝雀发布和 A/B 测试。
它主要出现在微服务架构中,用于管理和控制服务之间复杂的网络流量、监控通信情况、实现安全策略等。
服务网格的实现方式通常是通过在每个服务实例旁部署一个代理,这些代理共同构成了服务网格的数据平面,负责实际的流量处理和策略执行。同时,还有一个控制平面,用于配置和管理这些代理的行为。
常见的服务网格解决方案有 Istio、Linkerd 等。
在我过往参与的一个大型微服务项目中,引入了 Istio 作为服务网格,有效地解决了服务间通信的可靠性、安全性和可观测性问题。例如,通过 Istio 的流量控制功能,实现了对不同版本服务的精确流量分配,以进行金丝雀发布和 A/B 测试。
多线程
线程池如何知道一个线程的任务已经执行完成
书中1
在 Java 中,线程池判断一个线程的任务是否执行完成主要通过以下几种方式:
首先,线程池中的线程在执行任务时,任务本身会有一个明确的执行逻辑和结束标志。当任务的所有执行步骤都完成,并且没有后续的操作需要进行,就可以认为任务执行完成。
另外,线程池通常会利用 Future 接口来获取任务的执行结果和状态。通过 submit 方法向线程池提交任务时,可以获取一个 Future 对象。通过调用 Future 的 isDone 方法可以判断任务是否完成,如果返回 true 则表示任务完成,还可以使用 get 方法获取任务的返回值,但 get 方法会阻塞直到任务完成。
还有一种方式是通过线程池提供的回调机制。可以自定义任务在完成时执行特定的回调函数,在线程任务执行结束时触发这个回调,从而得知任务已完成。
此外,线程池内部也会维护一些状态信息和计数机制。比如记录已完成任务的数量,通过与提交的任务总数进行对比,来判断所有任务是否都已执行完毕。
综上所述,线程池通过任务自身的结束标志、Future 对象、回调机制以及内部的状态管理等多种方式来确定一个线程的任务是否已经执行完成。
综上所述,线程池通过任务自身的结束标志、Future 对象、回调机制以及内部的状态管理等多种方式来确定一个线程的任务是否已经执行完成。
书中2
在线程池内部,当我们把一个任务丢给线程池去执行,线程池会调度工作线程来执行这个任务的run 方法,run 方法正常结束,也就意味着任务完成了。所以线程池中的工作线程是通过同步调用任务的 run()方法并且等待 run 方法返回后,再去统计任务的完成数量。
如果想在线程池外部去获得线程池内部任务的执行状态,有几种方法可以实现。
a. 线程池提供了一个 isTerminated()方法,可以判断线程池的运行状态,我们可以循环判断isTerminated()方法的返回结果来了解线程池的运行状态,一旦线程池的运行状态是Terminated,意味着线程池中的所有任务都已经执行完了。想要通过这个方法获取状态的前提是,程序中主动调用了线程池的shutdown()方法。在实际业务中,一般不会主动去关闭线程池,因此这个方法在实用性和灵活性方面都不是很好。
b. 在线程池中,有一个 submit()方法,它提供了一个 Future 的返回值,我们通过Future.get()方法来获得任务的执行结果,当线程池中的任务没执行完之前,future.get()方法会一直阻塞,直到任务执行结束。因此,只要future.get()方法正常返回,也就意味着传入到线程池中的任务已经执行完成了!
c. 可以引入一个 CountDownLatch 计数器,它可以通过初始化指定一个计数器进行倒计时,其中有两种方法分别是 await()阻塞线程,以及 countDown()进行倒计时,一旦倒计时归零,所以被阻塞在 await()方法的线程都会被释放。基于这样的原理,我们可以定义一个CountDownLatch 对象并且计数器为 1,接着在线程池代码块后面调用 await()方法阻塞主线程,然后,当传入到线程池中的任务执行完成后,调用 countDown()方法表示任务执行结束。最后,计数器归零 0,唤醒阻塞在 await()方法的线程。
3. 基于这个问题,我简单总结一下,不管是线程池内部还是外部,要想知道线程是否执行结束,我们必须获取线程执行结束后的状态,而线程本身没有返回值,所以只能通过阻塞-唤醒的方式来实现,future.get 和CountDownLatch 都是这样一个原理。
自定义线程池时需要注意哪些性能问题?
Java 自定义线程池的几个关键参数包括:
1. `corePoolSize`(核心线程数):线程池中始终保持运行的线程数量。一般根据系统的资源情况和任务的并发量来设定。如果任务是 CPU 密集型,可设置为 CPU 核心数;如果是 I/O 密集型,可以适当增大。
2. `maximumPoolSize`(最大线程数):线程池允许创建的最大线程数量。通常根据系统资源和预期的最大并发量来确定。
3. `keepAliveTime`(线程存活时间):当线程数量大于核心线程数时,多余的空闲线程在超过这个时间后会被销毁。其设置取决于任务的频率和资源利用情况。
4. `unit`(时间单位):配合 `keepAliveTime` 使用,用于指定时间的单位。
5. `workQueue`(任务队列):用于存储等待执行的任务。常见的有 `ArrayBlockingQueue`、`LinkedBlockingQueue` 等。队列大小的设定要考虑任务的积压情况和内存使用。
在我们的项目中,对于一个主要处理 I/O 密集型任务的系统,设定 `corePoolSize` 为 10,`maximumPoolSize` 为 20,`keepAliveTime` 为 30 秒,`unit` 为 `TimeUnit.SECONDS`,任务队列使用了 `LinkedBlockingQueue` 并根据预估的最大任务积压量设置了合适的大小。
以下是一个简单的自定义线程池参数设定的核心代码示例(仅作示意,不包含完整的功能和错误处理):
```java
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class CustomThreadPoolSettings {
public static void main(String[] args) {
int corePoolSize = 10;
int maximumPoolSize = 20;
long keepAliveTime = 30;
TimeUnit unit = TimeUnit.SECONDS;
BlockingQueue<Runnable> workQueue = new ArrayBlockingQueue<>(50);
ThreadPoolExecutor executor = new ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);
// 提交任务
executor.execute(() -> {
// 任务逻辑
});
}
}
```
1. `corePoolSize`(核心线程数):线程池中始终保持运行的线程数量。一般根据系统的资源情况和任务的并发量来设定。如果任务是 CPU 密集型,可设置为 CPU 核心数;如果是 I/O 密集型,可以适当增大。
2. `maximumPoolSize`(最大线程数):线程池允许创建的最大线程数量。通常根据系统资源和预期的最大并发量来确定。
3. `keepAliveTime`(线程存活时间):当线程数量大于核心线程数时,多余的空闲线程在超过这个时间后会被销毁。其设置取决于任务的频率和资源利用情况。
4. `unit`(时间单位):配合 `keepAliveTime` 使用,用于指定时间的单位。
5. `workQueue`(任务队列):用于存储等待执行的任务。常见的有 `ArrayBlockingQueue`、`LinkedBlockingQueue` 等。队列大小的设定要考虑任务的积压情况和内存使用。
在我们的项目中,对于一个主要处理 I/O 密集型任务的系统,设定 `corePoolSize` 为 10,`maximumPoolSize` 为 20,`keepAliveTime` 为 30 秒,`unit` 为 `TimeUnit.SECONDS`,任务队列使用了 `LinkedBlockingQueue` 并根据预估的最大任务积压量设置了合适的大小。
以下是一个简单的自定义线程池参数设定的核心代码示例(仅作示意,不包含完整的功能和错误处理):
```java
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class CustomThreadPoolSettings {
public static void main(String[] args) {
int corePoolSize = 10;
int maximumPoolSize = 20;
long keepAliveTime = 30;
TimeUnit unit = TimeUnit.SECONDS;
BlockingQueue<Runnable> workQueue = new ArrayBlockingQueue<>(50);
ThreadPoolExecutor executor = new ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);
// 提交任务
executor.execute(() -> {
// 任务逻辑
});
}
}
```
你是怎么理解线程安全问题的
书中
所谓线程安全问题,简单来说,就是在多个线程访问某个方法或者对象的时候,不管通过任何的方式调用以及线程如何去交替执行。在程序中不做任何同步干预操作的情况下,这个方法或者对象的执行/修改都能按照预期的结果来反馈。
实际上,线程安全问题的具体表现在三个方面,原子性、有序性、可见性。
原子性呢,是指当一个线程执行一系列程序指令操作的时候,它应该是不可中断的,因为一旦出现中断,站在多线程的视角来看,这一系列的程序指令会出现前后执行结果不一致的问题。就是一段程序只能由一个线程完整地执行完成,而不能存在多个线程干扰。
CPU 的上下文切换,是导致原子性问题的核心,而 JVM 里面提供了Synchronized 关键字来解决原子性问题。
可见性,就是说在多线程环境下,由于读和写是发生在不同的线程里面,有可能出现某个线程对共享变量的修改,对其他线程不是实时可见的。导致可见性问题的原因有很多,比如 CPU的高速缓存、CPU 的指令重排序、编译器的指令重排序。
有序性,指的是程序编写的指令顺序和最终CPU 运行的指令顺序可能出现不一致的现象,这种现象也可以称为指令重排序,所以有序性也会导致可见性问题。可见性和有序性可以通过 JVM 里面提供了一个 Volatile 关键字来解决。
java基础
HashMap 是怎么解决哈希冲突的?
豆包回答
以下是在面试中可能对“HashMap 是怎么解决哈希冲突的”这一问题的回答:
“在 Java 的 HashMap 中,主要通过以下几种方式来解决哈希冲突:
首先,HashMap 采用链地址法来处理冲突。当不同的键通过哈希函数计算得到相同的哈希值时,它们会被存储在同一个哈希桶中形成一个链表。
其次,当链表长度超过一定阈值(默认是 8)时,HashMap 会将链表转换为红黑树结构。红黑树是一种自平衡的二叉搜索树,能够提高查找、插入和删除操作的效率。
此外,HashMap 在扩容时也能一定程度上缓解哈希冲突。当 HashMap 中的元素数量超过负载因子(默认是 0.75)与容量的乘积时,HashMap 会进行扩容,重新计算元素的哈希值并重新分布,使得元素在更大的空间中存储,从而减少冲突的概率。
综上所述,HashMap 通过链地址法、红黑树转换以及适时的扩容操作来有效地解决哈希冲突问题,以保证其在存储和检索数据时能够保持较好的性能。”
“在 Java 的 HashMap 中,主要通过以下几种方式来解决哈希冲突:
首先,HashMap 采用链地址法来处理冲突。当不同的键通过哈希函数计算得到相同的哈希值时,它们会被存储在同一个哈希桶中形成一个链表。
其次,当链表长度超过一定阈值(默认是 8)时,HashMap 会将链表转换为红黑树结构。红黑树是一种自平衡的二叉搜索树,能够提高查找、插入和删除操作的效率。
此外,HashMap 在扩容时也能一定程度上缓解哈希冲突。当 HashMap 中的元素数量超过负载因子(默认是 0.75)与容量的乘积时,HashMap 会进行扩容,重新计算元素的哈希值并重新分布,使得元素在更大的空间中存储,从而减少冲突的概率。
综上所述,HashMap 通过链地址法、红黑树转换以及适时的扩容操作来有效地解决哈希冲突问题,以保证其在存储和检索数据时能够保持较好的性能。”
书中
要了解 Hash 冲突,那首先我们要先了解Hash 算法和 Hash 表。
a. Hash 算法,就是把任意长度的输入,通过散列算法,变成固定长度的输出,这个输出结果是散列值。
b. Hash 表又叫做“散列表”,它是通过 key直接访问在内存存储位置的数据结构,在具体实现上,我们通过 hash 函数把 key 映射到表中的某个位置,来获取这个位置的数据,从而加快查
c. 所谓 hash 冲突,是由于哈希算法被计算的数据是无限的,而计算后的结果范围有限,所以总会存在不同的数据经过计算后得到的值相同,这就是哈希冲突。
d. 通常解决 hash 冲突的方法有 4 种。
i. 开放定址法,也称为线性探测法,就是从发生冲突的那个位置开始,按照一定的次序从hash 表中找到一个空闲的位置,然后把发生冲突的元素存入到这个空闲位置中。ThreadLocal 就用到了线性探测法来解决hash 冲突的。像这样一种情况(如图),在 hash 表索引 1的位置存了一个 key=name,当再次添加key=hobby时,hash 计算得到的索引也是1,这个就是 hash 冲突。而开放定址法,就是按顺序向前找到一个空
ii. 链式寻址法,这是一种非常常见的方法,简单理解就是把存在 hash 冲突的 key,以单向链表的方式来存储,比如 HashMap 就是采用链式寻址法来实现的。
iii.再 hash 法,就是当通过某个 hash 函数计算的 key 存在冲突时,再用另外一个hash 函数对这个 key 做 hash,一直运算直到不再产生冲突。这种方式会增加计算时间,性能影响较大。
iv.建立公共溢出区, 就是把 hash 表分为基本表和溢出表两个部分,凡是存在冲突的元素,一律放入溢出表中。
e. HashMap 在 JDK1.8 版本中,通过链式寻址法+红黑树的方式来解决 hash 冲突问题,其中红黑树是为了优化 Hash 表链表过长导致时间复杂度增加的问题。当链表长度大于 8 并且hash表的容量大于 64 的时候,再向链表中添加元素就会触发转化。
什么叫做阻塞队列的有界和无界
豆包回答
“首先,阻塞队列的有界和无界是描述队列容量限制的概念。
有界的阻塞队列,指的是队列具有固定的容量大小,当队列已满时,再进行入队操作的线程会被阻塞等待,直到队列有空间可用。无界的阻塞队列则没有固定的容量限制,可以一直添加元素。
有界阻塞队列能解决内存资源有限的问题,防止因为无限添加元素导致内存溢出。无界阻塞队列能满足对存储容量不确定性较大的场景需求。
有界阻塞队列的优势和特点在于能够有效地控制资源使用,保证系统的稳定性和可靠性。无界阻塞队列的优势在于不需要时刻关注队列的容量,使用相对简单,但可能存在潜在的内存风险。
有界阻塞队列的核心原理是通过维护一个固定大小的存储空间,并使用同步机制和等待/通知机制来控制对队列的操作。无界阻塞队列的核心原理则是基于动态的数据结构扩展,通常内部实现会根据需要动态调整存储。
之所以能解决上述问题,是因为有界阻塞队列通过限制容量,避免了资源的过度消耗。无界阻塞队列通过提供灵活的存储方式,适应了某些难以预估数据量的情况,但需要谨慎使用以防止内存问题。”
有界的阻塞队列,指的是队列具有固定的容量大小,当队列已满时,再进行入队操作的线程会被阻塞等待,直到队列有空间可用。无界的阻塞队列则没有固定的容量限制,可以一直添加元素。
有界阻塞队列能解决内存资源有限的问题,防止因为无限添加元素导致内存溢出。无界阻塞队列能满足对存储容量不确定性较大的场景需求。
有界阻塞队列的优势和特点在于能够有效地控制资源使用,保证系统的稳定性和可靠性。无界阻塞队列的优势在于不需要时刻关注队列的容量,使用相对简单,但可能存在潜在的内存风险。
有界阻塞队列的核心原理是通过维护一个固定大小的存储空间,并使用同步机制和等待/通知机制来控制对队列的操作。无界阻塞队列的核心原理则是基于动态的数据结构扩展,通常内部实现会根据需要动态调整存储。
之所以能解决上述问题,是因为有界阻塞队列通过限制容量,避免了资源的过度消耗。无界阻塞队列通过提供灵活的存储方式,适应了某些难以预估数据量的情况,但需要谨慎使用以防止内存问题。”
书中
有界队列就是说队列中的元素个数是有限制的,而无界对接表示队列中的元素个数没有限制!
阻塞队列,是一种特殊的队列,它在普通队列的基础上提供了两个附加功能
a. 当队列为空的时候,获取队列中元素的消费者线程会被阻塞,同时唤醒生产者线程。
2. 其中,阻塞队列中能够容纳的元素个数,通常情况下是有界的,比如我们实例化一个ArrayBlockingList,可以在构造方法中传入一个整形的数字,表示这个基于数组的阻塞队列中能够容纳的元素个数。这种就是有界队列。
3. 而无界队列,就是没有设置固定大小的队列,不过它并不是像我们理解的那种元素没有任何限制,而是它的元素存储量很大,像LinkedBlockingQueue,它的默认队列长度是 Integer.Max_Value,所以我们感知不到它的长度限制。
4. 无界队列存在比较大的潜在风险,如果在并发量较大的情况下,线程池中可以几乎无限制地添加任务,容易导致内存溢出的问题!
ConcurrentHashMap 底层具体实现知道吗?实现原理是什么?
ConcurrentHashMap 是 Java 中一个支持高并发访问的哈希表。
ConcurrentHashMap 的底层实现基于分段锁(Segment)机制。它将数据分成多个段(Segment),每个段类似于一个小型的 HashMap,并且拥有独立的锁。这样,在多线程并发访问时,不同线程可以同时访问不同的段,从而提高了并发性能。
其实现原理主要包括以下几点:
1、分段锁:通过将数据分段,降低了锁的粒度,从而减少了锁竞争。
2、哈希表结构:每个段内部采用类似于 HashMap 的数组 + 链表/红黑树的结构来存储键值对。
3、并发安全的修改操作:在进行修改操作(如添加、删除、更新)时,通过对相应段加锁来保证线程安全。
在我过往的一个大型电商系统项目中,使用 ConcurrentHashMap 存储高并发访问下的用户购物车信息。通过其优秀的并发性能,很好地满足了系统的需求。
以下是一个简单的 ConcurrentHashMap 示例代码(核心部分):
```java
import java.util.concurrent.ConcurrentHashMap;
public class ConcurrentHashMapExample {
public static void main(String[] args) {
ConcurrentHashMap<String, String> map = new ConcurrentHashMap<>();
map.put("key1", "value1");
map.put("key2", "value2");
}
}
```
ConcurrentHashMap 的底层实现基于分段锁(Segment)机制。它将数据分成多个段(Segment),每个段类似于一个小型的 HashMap,并且拥有独立的锁。这样,在多线程并发访问时,不同线程可以同时访问不同的段,从而提高了并发性能。
其实现原理主要包括以下几点:
1、分段锁:通过将数据分段,降低了锁的粒度,从而减少了锁竞争。
2、哈希表结构:每个段内部采用类似于 HashMap 的数组 + 链表/红黑树的结构来存储键值对。
3、并发安全的修改操作:在进行修改操作(如添加、删除、更新)时,通过对相应段加锁来保证线程安全。
在我过往的一个大型电商系统项目中,使用 ConcurrentHashMap 存储高并发访问下的用户购物车信息。通过其优秀的并发性能,很好地满足了系统的需求。
以下是一个简单的 ConcurrentHashMap 示例代码(核心部分):
```java
import java.util.concurrent.ConcurrentHashMap;
public class ConcurrentHashMapExample {
public static void main(String[] args) {
ConcurrentHashMap<String, String> map = new ConcurrentHashMap<>();
map.put("key1", "value1");
map.put("key2", "value2");
}
}
```
lock 和 synchronized 区别
首先,lock 和 synchronized 都是用于实现线程同步的机制,但它们存在一些显著的区别
synchronized 是 Java 内置的关键字,使用起来相对简单直观,它会自动释放锁。而 Lock 是一个接口,需要通过相关的实现类(如 ReentrantLock)来使用,并且需要手动进行加锁和解锁操作。
在功能上,Lock 提供了更丰富的特性。例如,它支持尝试非阻塞地获取锁(tryLock),可以设置获取锁的超时时间,这在某些复杂的场景下能更好地控制线程的等待行为。而 synchronized 不支持这些特性。
在性能方面,在竞争不激烈的情况下,synchronized 的性能表现不错。但在竞争激烈的高并发环境中,Lock 的性能可能更优,因为它的实现机制更灵活,能够减少线程阻塞和唤醒的开销。
从可中断性来说,Lock 支持获取锁的线程在等待过程中被中断,而使用 synchronized 时,线程无法被中断等待。
另外,Lock 可以实现更灵活的公平锁和非公平锁策略,而 synchronized 只能是非公平锁。
fail-safe 机制与 fail-fast 机制分别有什么作用
fail-safe 和 fail-fast ,是多线程并发操作集合时的两种失败处理机制。
Fail-fast : 表示快速失败,在集合遍历过程中,一旦发现容器中的数据被修改了,会立刻抛出ConcurrentModificationException 异常
例如定义一个 Map 集合,使用 Iterator 迭代器进行数据遍历,在遍历过程中,对集合数据做变更时,就会发生 fail-fast。
Fail-safe,表示失败安全,也就是在这种机制下,出现集合元素的修改,不会抛出ConcurrentModificationException。
CopyOnWriteArrayList,在对这个集合遍历过程中,对集合元素做修改后,不会抛出异常,但同时也不会打印出增加的元素。
谈谈你对 AQS 的理解
AQS 是多线程同步器,它是 J.U.C 包中多个组件的底层实现
如 Lock、CountDownLatch、Semaphore等都用到了AQS.
从本质上来说,AQS 提供了两种锁机制
排他锁
排他锁,是存在多线程竞争同一共享资源时,同一时刻只允许一个线程访问该共享资源,也就是多个线程中只能有一个线程获得锁资源,比如 Lock 中的 ReentrantLock 重入锁实现就是用到了 AQS 中的排它锁功能。
共享锁
共享锁也称为读锁,就是在同一时刻允许多个线程同时获得锁资源,比如CountDownLatch 和Semaphore 都是用到了 AQS 中的共享锁功能。
netty 连问
介绍下你对 netty 的认识
书中
好的,我用三点来简单地介绍下 Netty
第一:Netty 是一个 基于 NIO 模型的高性能网络通信框架,其实可以认为它是对 NIO 网络模式。型的封装,提供了简单易用的 API,我们可以利用这些封装好的 API 快速开发自己的网络程序。
第二:Netty 在 NIO 的基础上做了很多优化,比如零拷贝机制、高性能无锁队列、内存池等,因此性能会比 NIO 更高。
第三:Netty 可以支持多种通信协议,如Http、WebSocket 等,并且针对数据通信的拆包黏包问题,Netty 内置了拆包策略。
那你再说说为什么要用 Netty ?
Nety 相比于直接使用 JDK 自带的 NIO 相关的 API 来说更加易用。同时,它还具有以下特点:
1. 统一的 API,支持多种传输类型,如阻塞、非阻塞,以及 epoll、poll 等模型。
2. 我们可以使用非常少的代码来实现,多线程 Reactor 模型以及主从多线程 Reactor模型
3. 自带编解码器解决 TCP 粘包/拆包问题。
4. 自带各种协议栈。
5. 比直接使用 Java 库中的 NIO API 有更高的吞吐量、更低的延迟、更低的资源消耗和更少的内存复制。
6. 安全性不错,有完整的 SSL/TLS 以及StartTLS 支持。
7. 社区活跃成熟稳定,经历了大型项目的使用和考验,而且很多开源项目都使用到了Netty, 比如我们经常接触的 Dubbo、RocketMQ 等等。
那你再通俗地说一下 Netty 可以做什么事情?
我们之所以要用 Netty,核心点还是在于解决服务器如何承载更多的用户同时访问的问题。传统的 BIO 模型,由于阻塞的特性,使得在高并发场景中,很难获得更高的吞吐量。而后来基于 NIO的多路复用模型虽然在阻塞方面进行了优化,但是它的 API 使用比较复杂,对于初学者来说使用不是很友好。而Netty 是基于 NIO 的封装,提供了成熟且简单易用的 API,降低了使用成本和学习成本。本质上来说,Netty 和 NIO 所扮演的角色是相同的,都是为了提升服务端的吞吐量,让用户获得更好的产品体验。另外,Netty 这个中间件经过很多年的验证,在目前主流的中间件如 Zookeeper、Dubbo、RocketMQ中都有应用。
Netty 核心组件了解吗?分别有什么作用?
Netty 由三层结构构成:网络通信层、事件调度器与服务编排层在网络通信层有三个核心组件:Bootstrap、ServerBootStrap、Channel
Bootstrap 负责客户端启动并用来连接远程netty server
ServerBootStrap 负责服务端监听,用来监听指定端口,
Channel 是负责网络通信的载体
事件调度器有两个核心组件:EventLoopGroup 与 EventLoop
EventLoopGroup 本质上是一个线程池,主要负责接收 I/O 请求,并分配线程执行处理请求。
EventLoop。相当于线程池中的线程
在服务编排层有三个核心组件ChannelPipeline、ChannelHandler、ChannelHandlerContext
ChannelPipeline 负责将多个Channelhandler 连接在一起
ChannelHandler 针对 IO 数据的处理器,数据接收后,通过指定的 Handler 进行处理。
ChannelHandlerContext 用来保存ChannelHandler 的上下文信息
再说说 Netty 有几种线程模型吧?
Netty 提供了三种 Reactor 模型的支持
单线程单 Reactor 模型
多线程单 Reactor 模型
多线程多 Reactor 模型
你说一下对于这三种线程 Reactor模型的理解?
Reactor 模型有三个重要的组件:
1. Reactor :将 I/O 事件发派给对应的Handler
2. Acceptor :处理客户端连接请求
3. Handlers :执行非阻塞读/写
单 Reactor 单线程模型
Reactor 线程,负责多路分离套接字,有新连接到来触发 connect 事件之后,交由Acceptor 进行处理,有 IO 读写事件之后交给 hanlder 处理。
Acceptor 主要任务就是构建 handler ,在获取到和 client 相关的 SocketChannel 之后 ,绑定到相应的 hanlder 上,对应的SocketChannel 有读写事件之后,基于racotor 分发,hanlder 就可以处理了(所有的 IO 事件都绑定到 selector 上,有Reactor 分发)
多线程单 Reactor 模型
单线程 Reactor 这种实现方式存在着缺点,从实例代码中可以看出,handler 的执行是串行的,如果其中一个 handler 处理线程阻塞将导致其他的业务处理阻塞。由于handler 和 reactor 在同一个线程中的执行,这也将导致无法接收新的请求。
为了解决这种问题,有人提出使用多线程的方式来处理业务,也就是在业务处理的地方加入线程池异步处理,将 reactor 和handler 在不同的线程来执行,这就是多线程单 Reactor 模型
多线程多 Reactor 模型
在多线程单 Reactor 模型中,所有的 I/O 操作是由一个 Reactor 来完成,而 Reactor 运行在单个线程中,它需要处理包括Accept()/read()/write/connect 操作,对于小容量的场景,影响不大。但是对于高负载、大开发或大数据量的应用场景时,容易成为瓶颈,主要原因如下:一个 NIO 线程同时处理成百上千的链路,性能上无法支撑,即便 NIO 线程的 CPU 负荷达到 100%,也无法满足海量消息的读取和发送;当 NIO 线程负载过重之后,处理速度将变慢,这会导致大量客户端连接超时,超时之后往往会进行重发,这更加重了 NIO 线程的负载,最终会导致大量消息积压和处理超时,成为系统的性能瓶颈;所以,我们还可以更进一步优化,引入多Reactor 多线程模式,Main Reactor 负责接收客户端的连接请求,然后把接收到的请求传递给SubReactor(其中subReactor 可以有多个),具体的业务 IO 处理由 SubReactor 完成。Acceptor,请求接收者,在实践时其职责类似服务器,并不真正负责连接请求的建立,而只将其请求委托 Main Reactor 线程池来实现,起到一个转发的作用。Main Reactor,主 Reactor 线程组,主要负责连接事件,并将 IO 读写请求转发到SubReactor线程池。Sub Reactor,Main Reactor 通常监听客户端连接后会将通道的读写转发到 SubReactor 线程池中一个线程(负载均衡),负责数据的读写。在 NIO 中 通常注册通道的读(OP_READ)、写事件(OP_WRITE)。
为什么阿里巴巴的 Java 开发手册不建议使用 Java 自带的线程池
书中
Executors 里面默认提供的几个线程池是有一些弊端的,如果是不懂多线程,或者是新手直接盲目使用,就可能会造成比较严重的生产事故。
FixedThreadPool 和SingleThreadPool 中,阻塞队列长度是Integer.Max_Value,一旦请求量增加,就会堆积大量请求阻塞在队列中,可能会造成内存溢出的问题。
CachedThreadPool 和ScheduledThreadPool 中最大线程数量是Integer.Max_value,一旦请求量增加,导致创建大量的线程,使得处理性能下降。甚至可能会出现宕机的问题。
为了避免这类问题出现,我们可以直接实例化 ThreadPoolExecutor,然后自己设置参数的值,从而确保线程池的可控性。实际上,很多源码或者中间件里面,都是使用这类的方式。
讲一下 wait 和 notify 这个为什么要在synchronized 代码块中?
书中
wait 和 notify 用来实现多线程之间的协调,wait 表示让线程进入阻塞状态,notify表示让阻塞的线程唤醒。
wait 和 notify 用来实现多线程之间的协调,wait 表示让线程进入阻塞状态,notify表示让阻塞的线程唤醒。
通过 notify()方法来唤醒这个被阻塞的线程,从而实现多线程之间的通信。
在多线程里面,要实现多个线程之间的通信,除了管道流以外,只能通过共享变量的方法来实现,也就是线程 t1 修改共享变量 s,线程 t2 获取修改后的共享变量 s,从而完成数据通信。但是多线程本身具有并行执行的特性,也就是在同一时刻,多个线程可以同时执行。在这种情况下,线程 t2 在访问共享变量 s 之前,必须知道线程 t1 已经修改过了共享变量s,否则就需要等待。同时,线程 t1 修改过了共享变量 S 之后,还需要通知在等待中的线程 t2。所以要在这种特性下要去实现线程之间的通信,就必须有一个竞争条件控制线程在什么条件下等待,什条件下恢复;
而 Synchronized 同步关键字就可以实现这样一个互斥条件,也就是在通过共享变而 Synchronized 同步关键字就可以实现这样一个互斥条件,也就是在通过共享变量来实现多个线程通信的场景里面,参与通信的线程必须竞争到这个共享变量的锁资源,才有资格对共享变量做修改,修改完成后就释放锁,那么其他的线程就可以再次来竞争同一个共享变量的锁来获取修改后的数据,从而完成线程之前的通信。
所以这也是为什么 wait/notify 需要放在Synchronized 同步代码块中的原因,有了Synchronized同步锁,就可以实现对多个通信线程之间的互斥,实现条件等待和条件唤醒。
另外,为了避免 wait/notify 的错误使用,jdk 强制要求把 wait/notify 写在同步代码块里面,否则会抛出 IllegalMonitorStateException
最后,基于 wait/notify 的特性,非常适合实现生产者消费者的模型,比如说用wait/notify 来实现连接池就绪前的等待与就绪后的唤醒。
说一说 Mybatis 里面的缓存机制
书中
Mybatis 里面设计的二级缓存是用来提升数据的检索效率,避免每次数据的访问都需要去查询数据库。一级缓存,是 SqlSession 级别的缓存,也叫本地缓存,因为每个用户在执行查询的时候都需要使用SqlSession 来执行,为了避免每次都去查数据库,Mybatis 把查询出来的数据保存到 SqlSession 的本地缓存中,后续的 SQL如果命中缓存,就可以直接从本地缓存读取了。
如果想要实现跨 SqlSession 级别的缓存?那么一级缓存就无法实现了,因此在Mybatis 里面引入了二级缓存,就是当多个用户在查询数据的时候,只要有任何一个SqlSession 拿到了数据就会放入到二级缓存里面,其他的SqlSession 就可以从二级缓存加载数据。
一级缓存的具体实现原理是:在 SqlSession 里面持有一个 Executor,每个 Executor 中有一个 LocalCache 对象。当用户发起查询的时候,Mybatis 会根据执行语句在 Local Cache 里面查询,如果没命中,再去查询数据库并写入到LocalCache,否则直接返回。
使用 CachingExecutor 装饰了 Executor,所以在进入一级缓存的查询流程之前,会先通过CachingExecutor 进行二级缓存的查询。
开启二级缓存以后,会被多个 SqlSession共享,所以它是一个全局缓存。因此它的查询流程是先查二级缓存,再查一级缓存,最后再查数据库。
另外,MyBatis 的二级缓存相对于一级缓存来说,实现了 SqlSession 之间缓存数据的共享,同时缓存粒度也能够到 namespace级别,并且还可以通过 Cache 接口实现类不同的组合,对 Cache 的可控性也更强。
豆包
MyBatis 中的缓存机制指的是用于提高数据访问性能,减少数据库查询次数的一种机制。
在一些频繁访问相同数据的场景中,缓存可以避免重复的数据库操作,从而提升系统的整体性能。
MyBatis 主要有两种类型的缓存:
1. 一级缓存:默认开启,它是基于 SqlSession 级别的缓存。同一个 SqlSession 中执行相同的查询语句,第一次查询会从数据库获取数据,并将数据放入缓存,后续相同的查询会直接从缓存中获取数据,不再访问数据库。但如果在同一个 SqlSession 中执行了数据的修改、添加或删除操作,会清空该 SqlSession 的缓存。
2. 二级缓存:是基于 Mapper 级别的缓存,多个 SqlSession 可以共享。需要在对应的 Mapper XML 文件中进行配置开启。二级缓存可以跨 SqlSession 共享数据,但需要注意缓存数据的更新和一致性问题。
在我之前参与的一个企业管理系统项目中,我们合理地利用了 MyBatis 的缓存机制。对于一些不常变化的数据,如系统的配置信息,启用了二级缓存,并设置了合适的过期时间和刷新策略,以保证数据的及时性和准确性。同时,对于经常变化的数据,如用户的操作记录,我们谨慎使用缓存,避免数据不一致的情况。
以下是一个简单的二级缓存配置示例(在 Mapper XML 文件中):
```xml
<mapper namespace="com.example.dao.UserMapper">
<cache />
</mapper>
```
在一些频繁访问相同数据的场景中,缓存可以避免重复的数据库操作,从而提升系统的整体性能。
MyBatis 主要有两种类型的缓存:
1. 一级缓存:默认开启,它是基于 SqlSession 级别的缓存。同一个 SqlSession 中执行相同的查询语句,第一次查询会从数据库获取数据,并将数据放入缓存,后续相同的查询会直接从缓存中获取数据,不再访问数据库。但如果在同一个 SqlSession 中执行了数据的修改、添加或删除操作,会清空该 SqlSession 的缓存。
2. 二级缓存:是基于 Mapper 级别的缓存,多个 SqlSession 可以共享。需要在对应的 Mapper XML 文件中进行配置开启。二级缓存可以跨 SqlSession 共享数据,但需要注意缓存数据的更新和一致性问题。
在我之前参与的一个企业管理系统项目中,我们合理地利用了 MyBatis 的缓存机制。对于一些不常变化的数据,如系统的配置信息,启用了二级缓存,并设置了合适的过期时间和刷新策略,以保证数据的及时性和准确性。同时,对于经常变化的数据,如用户的操作记录,我们谨慎使用缓存,避免数据不一致的情况。
以下是一个简单的二级缓存配置示例(在 Mapper XML 文件中):
```xml
<mapper namespace="com.example.dao.UserMapper">
<cache />
</mapper>
```
再深入说下sqlsession
SqlSession 是 MyBatis 框架中的一个核心接口,它负责与数据库进行交互,并管理事务和执行 SQL 语句。
通常在实际应用中,SqlSession 会在需要与数据库进行操作时被创建和使用。比如执行查询、插入、更新、删除等操作。
SqlSession 具有以下一些重要特点和功能:
1. 执行 SQL 操作:可以通过它来执行各种类型的 SQL 语句,包括映射文件中定义的和动态生成的 SQL 。
2. 事务管理:支持手动开启、提交和回滚事务,以确保数据操作的完整性和一致性。
3. 结果映射:将数据库返回的结果映射为 Java 对象,方便在应用程序中进行处理。
4. 资源管理:负责管理数据库连接等资源,确保资源的正确获取和释放,避免资源泄漏。
在我参与的一个在线教育平台项目中,对于一些关键的用户数据操作,我们通过 SqlSession 来严格控制事务的边界,确保数据的准确性。例如,在用户注册和课程购买的流程中,通过 SqlSession 开启事务,执行一系列相关的数据操作,然后根据操作结果决定是提交事务还是回滚事务。
以下是一个使用 SqlSession 执行查询操作的简单示例代码:
```java
SqlSession sqlSession = sqlSessionFactory.openSession();
try {
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
User user = userMapper.getUserById(1);
// 对获取到的用户数据进行处理
} finally {
sqlSession.close();
}
```
通常在实际应用中,SqlSession 会在需要与数据库进行操作时被创建和使用。比如执行查询、插入、更新、删除等操作。
SqlSession 具有以下一些重要特点和功能:
1. 执行 SQL 操作:可以通过它来执行各种类型的 SQL 语句,包括映射文件中定义的和动态生成的 SQL 。
2. 事务管理:支持手动开启、提交和回滚事务,以确保数据操作的完整性和一致性。
3. 结果映射:将数据库返回的结果映射为 Java 对象,方便在应用程序中进行处理。
4. 资源管理:负责管理数据库连接等资源,确保资源的正确获取和释放,避免资源泄漏。
在我参与的一个在线教育平台项目中,对于一些关键的用户数据操作,我们通过 SqlSession 来严格控制事务的边界,确保数据的准确性。例如,在用户注册和课程购买的流程中,通过 SqlSession 开启事务,执行一系列相关的数据操作,然后根据操作结果决定是提交事务还是回滚事务。
以下是一个使用 SqlSession 执行查询操作的简单示例代码:
```java
SqlSession sqlSession = sqlSessionFactory.openSession();
try {
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
User user = userMapper.getUserById(1);
// 对获取到的用户数据进行处理
} finally {
sqlSession.close();
}
```
AbstractQueuedSynchronized 为什么采用双向链表
书中
第一个方面,双向链表的优势:双向链表提供了双向指针,可以在任何一个节点方便向前或向后进行遍历,这种对于有反向遍历需求的场景来说非常有用。双向链表可以在任意节点位置实现数据的插入和删除,并且这些操作的时间复杂度都是O(1),不受链表长度的影响。这对于需要频繁对链表进行增删操作的场景非常有用。
第二个方面,说一下 AQS 采用双向链表的原因存储在双向链表中的线程,有可能这个线程出现异常不再需要竞争锁,所以需要把这些异常节点从链表中删除,而删除操作需要找到这个节点的前驱节点,如果不采用双向链表,就必须从头节点开始遍历,时间复杂度就变成了O(n)。状态的时候才会让当前线程阻塞,所以这里也会涉及前驱节点的查找,采用双向链表能够更好地提线程在加入链表中后,会通过自旋的方式去尝试竞争锁来提升性能,在自旋竞争锁的时候为了保证锁竞争的公平性,需要先判断当前线程所在节点的前驱节点是否是头节点。这个判断也需要获取当前节点的前驱节点,同样采用双向链表能提高查找效率。
请说一下对象的创建过程
书中
在实例化一个对象的时候,JVM 首先会去检查目标对象是否已经被加载并初始化了。如果没有,JVM 需要立刻去加载目标类,然后调用目标类的构造器完成初始化。 目标类的加载是通过类加载器来实现的,主要就是把一个类加载到内存里面。然后初始化的过程,主要是对目标类里面的静态变量、成员变量、静态代码块进行初始化。
当目标类被初始化以后,就可以从常量池里面找到对应的类元信息,并且目标对象的大小在类加载之后就已经确定了,所以这个时候就需要为新创建的对象,根据目标对象的大小在堆内存里面分配内存空间。内存分配的方式一般有两种,一种指针碰撞,另一种是空闲列表,JVM 会根据 Java堆内存是否规整来决定内存分配方式。接下来,JVM 会把目标对象里面的普通成员变量初始化为零值,比如 int 类型初始化为0,对象类型初始化为 null,(类变量在类加载的准备阶段就已经初始化过了)。
这一步操作主要是保证对象里面的实例字段,不用初始化就可以直接使用,也就是程序能够获得这些字段对应数据类型的零值。然后,JVM 还需要对目标对象的对象头做一些设置,比如对象所属的类元信息、对象的GC 分代年龄、hashcode、锁标记等等。完成这些步骤以后,对于 JVM 来说,新对象的创建工作已经完成。但是基于 Java 语言来说,对象创建才算是开始。接下来要做的,就是执行目标对象内部生成的 init 方法,初始化成员变量的值、执行构造块、最后执行目标对象的构造方法,完成对象的创建。其中,init 方法是 Java 文件编译之后在字节码文件中生成的,它是一个实例构造器,这个构造器会把语句块、变量初始化、调用父类构造器等操作组织在一起。所以调用 init方法能够完成一系列的初始化动作。
JDK 动态代理为什么只能代理有接口的类?
书中
在 Java 里面,动态代理是通过Proxy.newProxyInstance()方法来实现的,它需要传入被动态代理的接口类。之所以要传入接口,不能传入类,还是取决于 JDK 动态代理的底层实现。JDK 动态代理会在程序运行期间动态生成一个代理类$Proxy0,这个动态生成的代理类会继承java.lang.reflect.Proxy 类,同时还会实现被代理类的接口 IHelloService。
在 Java 中,是不支持多重继承的。而每个动态代理类都会继承 Proxy 类(这也是 JDK动态代理的实现规范),所以就导致 JDK 里面的动态代理只能代理接口,而不能代理实现类。
我分析过动态代理的源码,发现 Proxy 这个 类只是保存了动态代理的处理器InvocationHandler,如果不抽出来,直接设置到$Proxy0 动态代理类里面,也是可以的。如果这么做,就可以针对实现类来做动态代理了。作者为什么这么设计,我认为有几个方面的原因。
1. 动态代理本身的使用场景或者需求,只是对原始实现的一个拦截,然后去做一些功能的增强或者扩展。而实际的开发模式也都是基于面向接口来开发,所以基于接口来实现动态代理,从需求和场景都是吻合的。当然确实可能存在有些类没有实现接口的,那这个时候,JDK动态代理确实无法满足。
2. 在 Java 里面,类的继承关系的设计,更多的是考虑到共性能力的抽象,从而提高代码的重用性和扩展性,而动态代理也是在做这样一个事情,它封装了动态代理类生成的抽象逻辑、判断一个类是否是动态代理类、InvocationHandler 的持有等等,那么把这些抽象的公共逻辑放在 Proxy 这个分类里 面,很显然是一个比较正常的设计思路。
你能说一下什么是受检异常和非受检异常吗?
书中
我觉得可以从三个方面回答这个问题
一、首先是异常的本质受检异常和非受检异常,都是继承自Throwable 这个类中,分别是 Error 和Exception,Error 是程序报错,系统收到无法处理的错误消息,它和程序本身无关。Excetpion 是指程序运行时抛出需要处理的异常信息如果不主动捕获,则会被 jvm 处理。
二、然后是对受检异常和非受检异常的定义前面说过受检异常和非受检异常均派生自Exception 这个类。
受检异常的定义是程序在编译阶段必须主动捕获的异常,遇到该异常有两种处理方法通过 try/catch 捕获该异常或者通过 throw把异常抛出去
非受检异常的定义是程序不需要主动捕获该异常,一般发生在程序运行期间,比如NullPointException
三、最后我还可以说下他们优点和缺点
受检异常优点有两个:
第一,它可以响应一个明确的错误机制,这些错误在写代码的时候可以随时捕获并且能很好地提高 代码的健壮性。
第二,在一些连接操作中,它能很好地提醒我们关注异常信息,并做好预防工作。
不过受检异常的缺点是:抛出受检异常的时候需要上声明,而这个做法会直接破坏方法签名导致版本不兼容。这个恶心特性导致我会经常使用 RuntimeException 包装。
非受检异常的好处是可以去掉一些不需要的异常处理代码,而不好之处是开发人员可能忽略某些应该处理的异常,导致带来一些隐藏很深的 Bug,比如忘记关闭?连接忘记释放等。
Spring
Spring Boot 的约定优于配置,你的理解是什么?
1. 是什么?
- “约定优于配置”是 Spring Boot 框架的一个重要设计理念。
- “约定优于配置”是 Spring Boot 框架的一个重要设计理念。
2. 能解决什么问题?
- 解决了传统 Spring 框架配置繁琐、复杂的问题。减少了开发人员在项目初始阶段对大量配置文件的编写和维护工作,提高了开发效率。
- 解决了传统 Spring 框架配置繁琐、复杂的问题。减少了开发人员在项目初始阶段对大量配置文件的编写和维护工作,提高了开发效率。
3. 有什么优势和特点?
- 优势在于加快了项目的启动速度,使开发者能够更快地进入业务逻辑的开发。特点是提供了一套默认的配置和规范,让开发者无需过多关注底层的配置细节。
- 优势在于加快了项目的启动速度,使开发者能够更快地进入业务逻辑的开发。特点是提供了一套默认的配置和规范,让开发者无需过多关注底层的配置细节。
4. 核心原理是什么?
- 其核心原理是基于对常见应用场景和最佳实践的总结,预设了一系列默认的配置规则和路径。例如,默认的项目结构、默认的端口、默认的数据库连接配置等。
- 其核心原理是基于对常见应用场景和最佳实践的总结,预设了一系列默认的配置规则和路径。例如,默认的项目结构、默认的端口、默认的数据库连接配置等。
5. 为什么能解决这类问题?
- 因为在大多数情况下,这些默认的约定能够满足常见的开发需求。当默认配置不满足特定需求时,开发者仍然可以通过少量的显式配置来进行定制,实现了灵活性和效率的平衡。这种方式避免了开发者在每个项目中都从头开始进行大量的基础配置,从而集中精力处理业务相关的独特需求。
- 因为在大多数情况下,这些默认的约定能够满足常见的开发需求。当默认配置不满足特定需求时,开发者仍然可以通过少量的显式配置来进行定制,实现了灵活性和效率的平衡。这种方式避免了开发者在每个项目中都从头开始进行大量的基础配置,从而集中精力处理业务相关的独特需求。
Spring Boot 中自动装配机制的原理
书中回答
自动装配,简单来说就是自动把第三方组件的 Bean 装载到 Spring IOC 器里面,不需要开发人员再去写 Bean 的装配配置。在 Spring Boot 应用里面,只需要在启动类加上@SpringBootApplication 注解就可以实现自动装配。@SpringBootApplication 是一个复合注解,真正实现自动装配的注解是@EnableAutoConfiguration。
自动装配的实现主要依靠三个核心关键技术。
1. 引入 Starter 启动依赖组件的时候,这个组件里面必须包含@Configuration 配置类,在这个配置类里面通过@Bean 注解声明需要装配到IOC 容器的 Bean 对象。
2. 这个配置类是放在第三方的 jar 包里面,然后通过 SpringBoot 中的约定优于配置思想,把这个配置类的全路径放在 classpath:META-INF/spring.factories 文件中。这样SpringBoot 就可以知道第三方 jar 包里面的配置类的位置,这个步骤主要是用到了Spring 里面的 SpringFactoriesLoader来完成的。
3. SpringBoot 拿到第三方 jar 包里面声明的配置类以后,再通过 Spring 提供的ImportSelector 接口,实现对这些配置类的动态加载。在我看来,SpringBoot 是约定优于配置这一理念下的产物,所以在很多的地方,都会看到这类的思想。
豆包
Spring Boot 的自动装配机制是指在应用启动时,根据项目的依赖和配置,自动将相关的组件和配置加载到应用上下文中。
这一机制的出现主要是为了简化 Spring 应用的配置工作,提高开发效率。在一个复杂的项目中,可能需要配置众多的 Bean、依赖以及各种配置项,如果手动进行配置会非常繁琐且容易出错。
自动装配的原理主要基于以下几点:
1. 条件注解:通过定义一系列的条件注解,如 @ConditionalOnClass、@ConditionalOnProperty 等,来判断是否满足装配的条件。
2. 配置文件:读取项目中的配置文件,如 application.properties 或 application.yml,获取相关的配置信息,从而决定是否进行装配。
3. 扫描特定的包:默认会扫描启动类所在的包及其子包,查找带有特定注解(如 @Component、@Service 等)的类,并将其注册为 Bean。
这一机制的出现主要是为了简化 Spring 应用的配置工作,提高开发效率。在一个复杂的项目中,可能需要配置众多的 Bean、依赖以及各种配置项,如果手动进行配置会非常繁琐且容易出错。
自动装配的原理主要基于以下几点:
1. 条件注解:通过定义一系列的条件注解,如 @ConditionalOnClass、@ConditionalOnProperty 等,来判断是否满足装配的条件。
2. 配置文件:读取项目中的配置文件,如 application.properties 或 application.yml,获取相关的配置信息,从而决定是否进行装配。
3. 扫描特定的包:默认会扫描启动类所在的包及其子包,查找带有特定注解(如 @Component、@Service 等)的类,并将其注册为 Bean。
请简述你对 Spring MVC 的理解?
书中
SpringMVC 是一种基于 Java 语言开发,实现了 Web MVC 设计模式,请求驱动类型的轻量级 Web框架。
采用了 MVC 架构模式的思想,通过把Model,View,Controller 分离,将 Web层进行职责解耦,从而把复杂的 Web 应用分成逻辑清晰的几个组件,在 Spring MVC中有 9 大重要的组件。
下面详细说明一下这些组件的作用和初始化方法:
1、MultipartResolver 文件处理器对应的初始化方法是initMultipartResolver(context),用于处理上传请求。
2、LocaleResolver 当前环境处理器其对应的初始化方法是initLocaleResolver(context)SpringMVC 主要有两个地方用到了Locale:一是 ViewResolver 视图解析的时候;二是用到国际化资源或者主题的时候。
3、ThemeResolver 主题处理器其对应的初始化方法是initThemeResolver(context),用于解析主题。 也就是解析样式、图片及它们所形成的显示效果的集合。
4、HandlerMapping 处理器映射器其对应的初始化方法是initHandlerMappings(context) ,在SpringMVC 中会有很多请求,每个请求都需要一个 Handler 处理。HandlerMapping 的作用便是找到请求响应的处理器 Handler 和 Interceptor。
5、HandlerAdapter 处理器适配器其对应的初始化方法是initHandlerAdapters(context)从名字上看,它就是一个适配器。HandlerAdapters 要做的事情就是如何让固定的Servlet 处理方法调用灵活的 Handler 来进行处理
6、HandlerExceptionResolver 异常处理器对应的初始化方法是initHandlerExceptionResolvers(context)它的主要作用是处理其他组件产生的异常情况。
7、RequestToViewNameTranslator 视图名称翻译器其对应的初始化方法是initRequestToViewNameTranslator(context)它的作用是从请求中获取 ViewName。有的 Handler 处理完后并没有设置 View 也没有设置 ViewName,这时就需要从request 中获取,而RequestToViewNameTranslator 就是为request 提供获取 ViewName 的实现。
8、ViewResolvers 页面渲染处理器其对应的初始化方法是initViewResolvers(context)ViewResolvers 的主要作用是将 String 类型的视图名和 Locale 解析为 View 类型的视图
9、FlashMapManager 参数传递管理器其对应的初始化方法是initFlashMapManager(context)在实际应用中,为了避免重复提交,我们可以在处理完 post 请求后重定向到另外一个get 请求,这个 get 请求可以用来返回页面渲染需要的信息。FlashMap 就是用于这种请求重定向场景中的参数传递。
Spring 循环依赖连问
Spring 是如何解决循环依赖问题的?
书中
将两个或多个Bean 互相之间持有对方的引用就会发生循环依赖。循环的依赖将会导致陷入死循环。这是 Spring 发生循环依赖的原因。
循环依赖有三种形态:
第一种互相依赖:A 依赖 B,B 又依赖 A,它们之间形成了循环依赖。
第二种三者间依赖:A 依赖 B,B 依赖 C,C 又依赖 A,形成了循环依赖
Spring 中设计了三级缓存来解决循环依赖问题,当我们去调用 getBean()方法的时候,Spring 会先从一级缓存中去找到目标Bean,如果发现一级缓存中没有便会去二级缓存中去找,而如果一、二级缓存中都没有找到,意味着该目标 Bean 还没有实例化。于是,Spring 容器会实例化目标 Bean然后,将目标 Bean 放入二级缓存中,同时,加上标记是否存在循环依赖。如果不存在循环依赖便会将目标 Bean 存入到二级缓存,否则,便会标记该 Bean 存在循环依赖,然后将等待下一次轮询赋值,也就是解析@Autowired 注解。等@Autowired 注解赋值完成后(PS:完成赋值的 Bean 称为成熟Bean) ,会将目标 Bean 存入到一级缓存。
Spring 一级缓存中存放所有的成熟 Bean,二级缓存中存放所有的早期 Bean,先取一级缓存,再去二级缓存。
豆包
循环依赖指的是两个或多个对象之间相互依赖,形成一个封闭的依赖环。
在 Spring 框架中,解决循环依赖的主要方式有以下几种:
三级缓存机制:Spring 创建对象时,会先将正在创建的对象放入三级缓存中。当需要解决循环依赖时,从缓存中获取半成品对象来完成依赖注入。
提前暴露对象:在创建对象的过程中,会提前将创建中的对象暴露出来,以便在其他对象依赖时能够获取到。
在 Spring 框架中,解决循环依赖的主要方式有以下几种:
三级缓存机制:Spring 创建对象时,会先将正在创建的对象放入三级缓存中。当需要解决循环依赖时,从缓存中获取半成品对象来完成依赖注入。
提前暴露对象:在创建对象的过程中,会提前将创建中的对象暴露出来,以便在其他对象依赖时能够获取到。
那么,前面有提到三级缓存,三级缓存的作用是什么?
三级缓存是用来存储代理 Bean,当调用getBean()方法时,发现目标 Bean 需要通过代理工厂来创建,此时会将创建好的实例保存到三级缓存,最终也会将赋值好的Bean 同步到一级缓存中。
Spring 中哪些情况下,不能解决循环依赖问题?
1.多例 Bean 通过 setter 注入的情况,不能解决循环依赖问题
2.构造器注入的 Bean 的情况,不能解决循环依赖问题
3.单例的代理 Bean 通过 Setter 注入的情况,不能解决循环依赖问题
4.设置了@DependsOn 的 Bean 的情况,不能解决循环依赖问题
能回答一下 Spring Bean 生命周期的执行流程吗
书中
Spring 生命周期全过程大致分为五个阶段:创建前准备阶段、创建实例阶段、依赖注入阶段、容器缓存阶段和销毁实例阶段。
1. 创建前准备阶段【干什么、作用】
这个阶段主要的作用是,Bean 在开始加载之前,需要从上下文和相关配置中解析并查找 Bean 有关的扩展实现
比如像`init-method`-容器在初始化 bean时调用的方法、destory-method`容器在销毁 bean 时调用的方法。
以及,BeanFactoryPostProcessor 这类的bean 加载过程中的前置和后置处理。
这些类或者配置其实是 Spring 提供给开发者,用来实现 Bean 加载过程中的扩展机制,在很多和 Spring集成的中间件中比较常见,比如 Dubbo。
2. 创建实例阶段
这个阶段主要是通过反射来创建 Bean 的实 例对象,并且扫描和解析 Bean 声明的一些属性。
3. 依赖注入阶段
如果被实例化的 Bean 存在依赖其他 Bean对象的情况,则需要对这些依赖 bean 进行对象注入。比如常见的`@Autowired`setter 注入等依赖注入的配置形式。同时,在这个阶段会触发一些扩展的调用,比如常见的扩展类:BeanPostProcessors(用来实现 bean初始化前后的扩展回调)、 InitializingBean(这个类有一个afterPropertiesSet(),这个在工作中也比较常见)、BeanFactoryAware 等等。 InitializingBean(这个类有一个afterPropertiesSet(),这个在工作中也比较常见)、BeanFactoryAware 等等。
4. 容器缓存阶段
容器缓存阶段主要是把 bean 保存到容器以及 Spring 的缓存中,到了这个阶段,Bean就可以被开发者使用了。这个阶段涉及的操作,常见的有,init-method`这个属性配置的方法, 会在这个阶段调用。
以及像 BeanPostProcessors 方法中的后置 处理器方法如:postProcessAfterInitialization,也会在这个阶段触发。
5. 销毁实例阶段
当 Spring 应用上下文关闭时,该上下文中的所有 bean 都会被销毁。如果存在 Bean 实现了 DisposableBean 接口,或者配置了`destory-method`属性,会在这个阶段被调用。
豆包
Spring Bean 的生命周期指的是从 Bean 被创建到销毁的整个过程。
在一个典型的 Spring 应用中,Bean 生命周期的执行流程涉及多个阶段,这对于确保 Bean 的正确初始化、配置和资源清理非常重要。
具体的流程如下:
1. 实例化:通过构造方法或工厂方法创建 Bean 的实例。
2. 属性赋值:为 Bean 的属性设置值,可以通过 XML 配置、注解或者 Java 代码进行。
3. 初始化前处理:可以通过实现 `BeanPostProcessor` 的 `postProcessBeforeInitialization` 方法进行一些前置处理。
4. 初始化:调用 Bean 定义中指定的初始化方法,例如通过 `@PostConstruct` 注解标注的方法或者实现 `InitializingBean` 接口的 `afterPropertiesSet` 方法。
5. 初始化后处理:通过实现 `BeanPostProcessor` 的 `postProcessAfterInitialization` 方法进行一些后置处理。
6. 应用使用:Bean 可以被应用中的其他组件使用。
7. 销毁:当应用上下文关闭或者 Bean 不再被需要时,会调用 Bean 定义中指定的销毁方法,例如通过 `@PreDestroy` 注解标注的方法或者实现 `DisposableBean` 接口的 `destroy` 方法。
在我参与的一个微服务项目中,利用 Spring Bean 的生命周期来实现一些资源的初始化和释放。例如,在初始化阶段加载一些配置文件,在销毁阶段关闭一些连接资源。
以下是一个简单的示例代码,展示了使用 `@PostConstruct` 和 `@PreDestroy` 注解:
```java
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
public class SampleBean {
@PostConstruct
public void init() {
System.out.println("Bean 初始化");
}
@PreDestroy
public void destroy() {
System.out.println("Bean 销毁");
}
}
```
在一个典型的 Spring 应用中,Bean 生命周期的执行流程涉及多个阶段,这对于确保 Bean 的正确初始化、配置和资源清理非常重要。
具体的流程如下:
1. 实例化:通过构造方法或工厂方法创建 Bean 的实例。
2. 属性赋值:为 Bean 的属性设置值,可以通过 XML 配置、注解或者 Java 代码进行。
3. 初始化前处理:可以通过实现 `BeanPostProcessor` 的 `postProcessBeforeInitialization` 方法进行一些前置处理。
4. 初始化:调用 Bean 定义中指定的初始化方法,例如通过 `@PostConstruct` 注解标注的方法或者实现 `InitializingBean` 接口的 `afterPropertiesSet` 方法。
5. 初始化后处理:通过实现 `BeanPostProcessor` 的 `postProcessAfterInitialization` 方法进行一些后置处理。
6. 应用使用:Bean 可以被应用中的其他组件使用。
7. 销毁:当应用上下文关闭或者 Bean 不再被需要时,会调用 Bean 定义中指定的销毁方法,例如通过 `@PreDestroy` 注解标注的方法或者实现 `DisposableBean` 接口的 `destroy` 方法。
在我参与的一个微服务项目中,利用 Spring Bean 的生命周期来实现一些资源的初始化和释放。例如,在初始化阶段加载一些配置文件,在销毁阶段关闭一些连接资源。
以下是一个简单的示例代码,展示了使用 `@PostConstruct` 和 `@PreDestroy` 注解:
```java
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
public class SampleBean {
@PostConstruct
public void init() {
System.out.println("Bean 初始化");
}
@PreDestroy
public void destroy() {
System.out.println("Bean 销毁");
}
}
```
Spring 中事务的传播行为有哪些?
书中
多个声明了事务的方法相互调用的时候存在事务嵌套问题,那么这个事务的行为应该如何进行传递?比如说,methodA()调用methodB(),两个方法都显示的开启了事务。那么 methodB()是开启一个新事务,还是继续在 methodA()这个事务中执行?就取决于事物的传播行为。
Spring 为了解决这个问题,定义了 7种事务传播行为。
REQUIRED:默认的 Spring 事务传播级别,如果当前存在事务,则加入这个事务,如果不存在事务,就新建一个事务。
REQUIRE_NEW:不管是否存在事务,都会新开一个事务,新老事务相互独立。外部事务抛出异常回滚不会影响内部事务的正常提交。
NESTED:如果当前存在事务,则嵌套在当前事务中执行。如果当前没有事务,则新建一个事务,类似于 REQUIRE_NEW。
SUPPORTS:表示支持当前事务,如果当前不存在事务,以非事务的方式执行。
NOT_SUPPORTED:表示以非事务的方式来运行,如果当前存在事务,则把当前事务挂起。
MANDATORY:强制事务执行,若当前不存在事务,则抛出异常.
NEVER:以非事务的方式执行,如果当前存在事务,则抛出异常。
说说 Spring MVC 的执行流程?
书中
Spring MVC 的工作流程可以分为几个步骤
1. 用户发起请求,请求先被 Servlet 拦截转发给 Spring MVC 框架
2. Spring MVC 里面的 DispatcherSerlvet核心控制器,会接收到请求并转发给HandlerMapping
3. HandlerMapping 负责解析请求,根据请求信息和配置信息找到匹配的 Controller 类,不过这里如果有配置拦截器,就会按照顺序执行拦截器里面的 preHandle 方法
4. 找到匹配的 Controller 以后,把请求参数传递给 Controller 里面的方法
5. Controller 中的方法执行完以后,会返回一个 ModeAndView,这里面会包括视图名称和需要传递给视图的模型数据
6. 视图解析器根据名称找到视图,然后把数据模型填充到视图里面再渲染成 Html 内容返回给客户端
数据库
谈一谈你对 MySQL 性能优化的理解。
书中
MySQL 的性能优化我认为可以分为 4 大部分
硬件和操作系统层面的优化
从硬件层面来说,影响 Mysql 性能的因素有,CPU、可用内存大小、磁盘读写速度、网络带宽
从操作系统层面来说,应用文件句柄数、操作系统网络的配置都会影响到 Mysql 性能。这部分的优化一般由 DBA 或者运维工程师去完成。
在硬件基础资源的优化中,我们重点应该关注服务本身承载的体量,然后提出合理的指标要求,避免出现资源浪费!
架构设计层面的优化
MySQL 是一个磁盘 IO 访问量非常频繁的关系型数据库在高并发和高性能的场景中.MySQL 数据库必然会承受巨大的并发压力,而此时,我们的优化方式可以分为几个部分。
1. 搭建 Mysql 主从集群,单个 Mysql 服务容易单点故障,一旦服务器宕机,将会导致依赖 Mysql数据库的应用全部无法响应。 主从集群或者主要集群可以保证服务的高可用性。
2. 读写分离设计,在读多写少的场景中,通过读写分离的方案,可以避免读写冲突导致的性能影响
3. 引入分库分表机制,通过分库可以降低单个服务器节点的 IO 压力,通过分表的方式可以降低单表数据量,从而提升 sql 查询的效率。
4. 针对热点数据,可以引入更为高效的分布式数据库,比如 Redis、MongoDB 等,他们可以很好地缓解 Mysql 的访问压力,同时还能提升数据检索性能。
MySQL 程序配置优化
MySQL 是一个经过互联网大厂验证过的生产级别的成熟数据库,对于 Mysql 数据库本身的优化,一般是通过 Mysql 中的配置文件my.cnf 来完成的,比如:Mysql5.7 版本默认的最大连接数是 151 个,这个值可以在 my.cnf 中修改。
binlog 日志,默认是不开启
缓存池 bufferpoll 的默认大小配置等
关于配置项的修改,需要关注两个方面。配置的作用域,分为会话级别和全局是否支持热加载
因此,针对这两个点,我们需要注意的是:全局参数的设定对于已经存在的会话无法生效;会话参数的设定随着会话的销毁而失效;
全局类的统一配置建议配置在默认配置文件中,否则重启服务会导致配置失效
SQL 优化
SQL 优化又能分为三部曲
第一,慢 SQL 的定位和排查
我们可以通过慢查询日志和慢查询日志分析工具得到有问题的 SQL 列表。
第二,执行计划分析
针对慢 SQL,我们可以使用关键字 explain来查看当前 sql 的执行计划.可以重点关注type key rowsfilterd 等字段 ,从而定位该SQL 执行慢的根本原因。再有的放矢地进行优化
第三,使用 show profile 工具
Show Profile 是 MySQL 提供的可以用来分析当前会话中,SQL 语句资源消耗情况的工具,可用于 SQL调优的测量。在当前会话 中.默认情况下处于 show profile 是关闭状态,打开之后保存最近 15 次的运行结果针对运行慢的 SQL,通过 profile 工具进行详细分析.可以得到 SQL 执行过程中所有的资源开销情况.如 IO 开销,CPU 开销,内存开销等.
豆包
MySQL 性能优化指的是通过一系列的方法和策略来提高 MySQL 数据库的运行效率和响应速度,以满足不断增长的业务需求和处理更多的数据量。
在复杂的业务场景中,随着数据量的增加、并发访问的增多,如果不进行性能优化,可能会出现查询响应慢、数据写入延迟、系统资源消耗过高甚至崩溃等问题。
MySQL 性能优化的方法有很多:
1. 优化表结构:合理选择字段的数据类型,减少数据冗余,适当使用索引。
2. 优化查询语句:避免使用不必要的 `SELECT *`,尽量精确指定所需字段;合理使用索引,确保索引能够被有效利用;避免复杂的子查询和连接操作。
3. 配置参数优化:根据服务器的硬件资源和业务特点,调整 MySQL 的配置参数,如缓存大小、连接数等。
4. 分表和分区:对于数据量巨大的表,可以采用分表或分区的方式来提高查询和处理的效率。
5. 存储引擎选择:根据业务需求选择合适的存储引擎,如 InnoDB 适合事务处理,MyISAM 适合读多写少的场景。
在我之前参与的一个电商项目中,遇到过数据库查询缓慢的问题。经过分析,发现是部分查询语句没有合理使用索引。通过使用 `EXPLAIN` 命令分析查询计划,添加了合适的索引后,查询速度得到了显著提升。
以下是一个简单的添加索引的示例代码:
```sql
ALTER TABLE your_table ADD INDEX index_name (column_name);
```
在复杂的业务场景中,随着数据量的增加、并发访问的增多,如果不进行性能优化,可能会出现查询响应慢、数据写入延迟、系统资源消耗过高甚至崩溃等问题。
MySQL 性能优化的方法有很多:
1. 优化表结构:合理选择字段的数据类型,减少数据冗余,适当使用索引。
2. 优化查询语句:避免使用不必要的 `SELECT *`,尽量精确指定所需字段;合理使用索引,确保索引能够被有效利用;避免复杂的子查询和连接操作。
3. 配置参数优化:根据服务器的硬件资源和业务特点,调整 MySQL 的配置参数,如缓存大小、连接数等。
4. 分表和分区:对于数据量巨大的表,可以采用分表或分区的方式来提高查询和处理的效率。
5. 存储引擎选择:根据业务需求选择合适的存储引擎,如 InnoDB 适合事务处理,MyISAM 适合读多写少的场景。
在我之前参与的一个电商项目中,遇到过数据库查询缓慢的问题。经过分析,发现是部分查询语句没有合理使用索引。通过使用 `EXPLAIN` 命令分析查询计划,添加了合适的索引后,查询速度得到了显著提升。
以下是一个简单的添加索引的示例代码:
```sql
ALTER TABLE your_table ADD INDEX index_name (column_name);
```
关于索引的底层实现,为什么选择 B+Tree而不是红黑树?
书中
我主要从两个点来回答
第一点 对于一个数据库来说存储的数据量会比较多,导致索引也很大因此需要将索引存储在磁盘,但是磁盘的 IO 操作又非常少,所以提高索引效率的关键在于减少磁盘 IO 的次数。
举个例子 对于 31 个节点的树来说 ,一个 5阶 B+Tree 的高度是 3 一个红黑树的最小高度是 5,树的高度基本决定了磁盘的 IO 次数 ,所以使用 B+Tree 性能要高很多
第二点 B+Tree 有个特点是相邻的数据在物理上也是相邻的,因为 B+Tree 的 node 的大小设为一个页,而一个节点上存有多个相邻的关键字和分支信息,每个节点只需要一次 IO就能完全载入,相当于一次 IO载入了多个相邻的关键字和分支,而红黑树不具有这个特性,红黑树中大小相邻的数据,在物理结构上可能距离相差很大。由于程序的局部性原理,如果我们在索引中采用了预加载的技术,每次磁盘访问的时候除了将访问到的页加载到磁盘,我们还可以基于局部性原理加载,几页相邻的数据到内存中,而这个加载是不需要消耗多余磁盘 IO 时间的。
因此 基于局部性原理,以及 B+Tree 存储结好的关于普通人和高手的回答谁的回答较好
请你说一下 Mysql 中的性能调优方法
书中
Mysql 性能调优方法可以从四个方面来说,分别是
表结构与索引
表结构和索引的优化,主要可以从下面这些方面去优化分库分表、读写分离、为字段选择合适的数据类型、适当的反范式设计,适当冗余设计、 为查询操作创建必要的索引但是要避免索引滥用、尽可能使用 Not Null。
SQL 语句优化
SQL 语句优化可以从几个方面来做通过慢查询分析需要优化的 SQL 进行合理优化、利用 explain、profile 等工具分析SQL 执行计划、避免使用 SELECT *查询。尽可能使用索引扫描来排序。
Mysql 参数优化
参数调优方面,主要可以设置Buffer_pool 的大小,建议占总内存的 70%左右。设置刷盘策略,平衡好数据安全性和性能的关系等
硬件及系统配置
硬件方面、主要是 CPU 核数、磁盘的读写性能(减少寻道时间、旋转时间、传输时间),可以选择 SSD、网卡、内存等方面。
为什么索引要用 B+树来实现呢,而不是 B树?
书中
B 树是一种多路平衡树,用这种存储结构来存储大量数据,它的整个高度会相比二叉树来说,会矮很多。
而对于数据库来说,所有的数据必然都是存储在磁盘上的,而磁盘 IO 的效率实际上是很低的,特别是在随机磁盘 IO 的情况下效率更低。
所以树的高度能够决定磁盘 IO 的次数,磁盘 IO 次数越少,对于性能的提升就越大,这也是为什么采用 B 树作为索引存储结构的原因。
但是在 Mysql 的 InnoDB 存储引擎里面,它用了一种增强的 B 树结构,也就是B+树来作为索引和数据的存储结构。
相比较于 B 树结构,B+树做了几个方面的优化。
B+树的所有数据都存储在叶子节点,非叶子节点只存储索引。叶子节点中的数据使用双向链表的方式进行关联。B+树非叶子节点不存储数据,所以每一层能够存储的索引数量会增加,意味着B+树在层高相同的情况下存储的数据量要比 B 树要多,使得磁盘 IO 次数更少。
在 Mysql 里面,范围查询是一个比较常用的操作,而 B+树的所有存储在叶子节点的数据使用了双向链表来关联,所以在查询的时候只需将两个节点进行遍历就行,而 B 树需要获取所有节点,所以B+树在范围查询上效率更高。
在数据检索方面,由于所有的数据都存储在叶子节点,所以 B+树的 IO 次数会更加稳定一些。因为叶子节点存储所有数据,所以 B+树的全局扫描能力更强一些,因为它只需要扫描叶子节点。但是 B 树需要遍历整个树。
另外,基于 B+树这样一种结构,如果采用自增的整型数据作为主键,还能更好地避免增加数据的时候,带来叶子节点分裂导致的大量运算的问题。总的来说,我认为技术方案的选型,更多的是去解决当前场景下的特定问题,并不一定是说 B+树就是最好的选择,就像 MongoDB里面采用 B 树结构,本质上来说,其实是关系型数据库和非关系型数据库的差异。
请你简单说一下 Mysql 的事务隔离级别
书中
在 SQL 标准中定义了四种隔离级别,分别是:
读未提交,在这种隔离级别下,可能会产生脏读、不可重复读、幻读。
读已提交(RC),在这种隔离级别下,可能会产生不可重复读和幻读。
可重复读(RR),在这种隔离级别下,可能会产生幻读
串行化,在这种隔离级别下,多个并行事务串行化执行,不会产生安全性问题。
limit 1000000,10 加载很慢该怎么优化
书中
如果 id 是连续的,可以直接使用这样的方式。select id,name from user where id>1000000 limit 10.这种方式其实就是先对数据做过滤,然后再limit,可以有效提升查询效率
通过 order by+索引来解决select id,name from user order by id limit 1000000,10需要注意 id 是索引列,通过索引排序后再limit,同样减少了计算次数
从业务层面来考虑,限制页数,一般情况下用户去翻 100W 页来找数据,如果让你们老板去翻 100w页,估计第二天就把你开除了。我们通常会通过搜索来优化查找过程。
说一下你日常工作中是怎么优化 SQL 的
书中
索引
加索引,增加索引是一种简单高效的手段,但是需要选择合适的列,同时避免导致索引失效的操作,比如 like、函数等。
减少返回
避免返回不必要的数据列,减少返回的数据列可以提高查询的效率。
使用分析器检查sql
根据查询分析器适当优化 SQL 的结构,比如是否走全表扫描、避免子查询等
分库分表
分库分表,在单表数据量较大或者并发连接数过高的情况下,通过这种方式可以有效提升查询效率
读写分离
读写分离,针对读多写少的场景,这样可以保证写操作的数据库承受更小的压力,也可以缓解独占
我的回答
优化sql要考虑的事情主要是为什么会慢,怎么样要快
首先是缩小查找范围,也就是使用索引,缩小查找范围,分库分表也是一个原理,将圈定的资源放在比较小的范围去查找
其次你获取的所有数据要从磁盘中读取,走网络,多一点数据就多一些资源消耗,所以要控制返回结果的大小,不需要的资源就不要查了
再次读写分离就是读取的地方和写入的地方分别提供服务,把事情分给两个地方去做,相当于分工,去提升效率
以上两个基本就完成了初步优化,要进一步优化就要看sql语句的分析器了,看看使用的什么索引,查询了多少数据;根据情况再去进行优化
Mysql 主从集群同步延迟问题怎么解决
书中
复制过程分为几个步骤:
1. 主库的更新事件(update、insert、delete)被写到 binlog
2. 从库发起连接,连接到主库。
3. 此时主库创建一个 binlog dump thread,把 binlog 的内容发送到从库。
4. 从库启动之后,创建一个 I/O 线程,读取主库传过来的 binlog 内容并写入到 relaylog
5. 从库还会创建一个 SQL 线程,从 relaylog 里面读取内容,从Exec_Master_Log_Pos 位置开始执行读取到的更新事件,将更新内容写入到slave 的 db主从数据同步涉及网络数据传输,由于网络通信的延迟以及从库数据处理的效率问题, 就会导致主从数据同步延迟的情况。
一般可以通过以下几个方法来解决
设计一主多从来分担从库压力,减少主从同步延迟问题
如果对数据一致性要求高,在从库存在延迟的情况下,可以强制走主库查询数据
可以在从库上执行 show slave status 命令,获取 seconds_behind_master 字段的延迟时间,然后通过 sleep 阻塞等待固定时间后再次查询通过并行复制解决从库复制延迟的问题
Mysql 的 binlog 有几种格式?分别有什么区别
书中
Binlog 有三种格式:statement,row 和 mixed。
statement,记录的是 SQL 的原文。好处是,不需要记录每一行的变化,减少了binlog 日志量,节约了 IO,提高性能。由于 sql 的执行是有上下文的,因此在保存的时候需要保存相关的信息,同时还有一些使用了函数之类的语句无法被记录复制。
row,不记录 sql 语句上下文相关信息,仅保存哪条记录被修改。记录单元为每一行的改动,基本是可以全部记下来但是由于很多操作,会导致大量行的改动(比如 alter table),因此这种模式的文件保存的信息太多,日志量太大。
mixed,一种折衷的方案,普通操作使用statement 记录,当无法使用 statement 的时候使用row。
索引有哪些缺点以及具体有哪些索引类型
书中
索引的优缺点
优点:合理地增加索引,可以提高数据查询的效率,减少查询时间有一些特殊的索引,可以保证数据的完整性,比如唯一索引
缺点:创建索引和维护索引需要消耗时间索引需要额外占用物理空间对创建了索引的表进行数据的增加、修改、删除时,会同步动态维护索引,这个部分会造成性能的影响
第二个,索引的类型
主键索引:数据列不允许重复,不允许为NULL,一个表只能有一个主键。
唯一索引:数据列不允许重复,允许为NULL 值,一个表允许多个列创建唯一索引。
普通索引:基本的索引类型,没有唯一性的限制,允许为 NULL 值。
全文索引:是目前搜索引擎使用的一种关键技术,对文本的内容进行分词、搜索。
覆盖索引:查询列要被所建的索引覆盖,不必读取数据行
组合索引:多列值组成一个索引,用于组合搜索,效率大于索引合并
MySQL 数据库 cpu 飙升的话,要怎么处理呢?
书中
第一步,排查问题使用 top 命令,找到 cpu 占用过高的进程是否是 mysqld.如果是,可以在 mysql 中通过 show processlist 查看当前的会话情况,确定是否有消耗资源的 SQL;正在运行找到消耗过高的 SQL,通过执行计划进行具体的分析
第二步,处理方式, 如果确定是 SQL 问题,可以通过 SQL 的优化手段进行调整重新执行 SQL 分析确认是否有达到优化的目的
第三步,其他情况如果不是 SQL 的问题导致,那就需要分析CPU 飙高的这个时间段,Mysql 的整体并发连接数。如果有大量的请求连接进来,那我们就需要分析这个时间段业务的情况,再作出相应的调整。
binlog 和 redolog 有什么区别?
书中
首先,binlog 和 redolog 都是 Mysql 里面用来记录数据库数据变更操作的日志。
其中 binlog 主要用来做数据备份、数据恢复和数据同步,大家初步接触这个概念 ,应该是在 Mysql 的主从数据同步的场景中,master 节点的数据变更,会写入到binlog 中,然后再把 binlog中的数据通过网络传输给 slave 节点,实现数据同步。
而 redolog,主要是在 Mysql 数据库事务的ACID 特性里面,用来保证数据的持久化特性。但是其实它还有很多的作用。
比如数据库崩溃时,可以通过 Redo Log 来恢复未完成的数据,保证数据的完整性。通过合理地配置 Redo Log 的大小和数量,还可以优化 Mysql 的性能。
binlog 和 redolog 的区别有很多,我可以简单总结三个点
1. 使用场景不同,
binlog 主要用来做数据备份、数据恢复,以及主从集群的数据同步;
Redo Log主要用来实现 Mysql 数据库的事务恢复,保证事务的 ACID 特性。当数据库出现崩溃的时候,Redo Log 可以把未提交的事务回滚,把已提交的事务进行持久化,从而保证数据的一致性和持久性。
2. 记录的信息不同,
binlog 是记录数据库的逻辑变化,它提供了三种日志格式分别是statement,row 以及 mixed;
redo log 记录的是物理变化,也就是数据页的变化结果。
3. 记录的时机不同
binlog 是在执行 SQL语句的时候,在主线程中生成逻辑变化写入到磁盘中,所以它是语句级别的记录方式;
RedoLog 是在 InnoDB 存储引擎层面的操作,它是在 Mysql 后台线程中生成并写入到磁盘中的,所以它是事务级别的记录方式,一个事务操作完成以后才会被写入到 redo log 中。
innoDB 如何解决幻读
首先,InnoDB 解决幻读主要通过其行锁和间隙锁机制。
它能解决在事务并发执行过程中,一个事务在两次查询结果集的行数不一致的问题,即幻读现象。
InnoDB 解决幻读的优势和特点在于:保证了事务的隔离性和数据的一致性,让数据库操作在多事务并发的情况下更加可靠。
其核心原理是,在可重复读隔离级别下,除了对查询涉及到的数据行加行锁,还会对可能插入新记录的间隙加间隙锁,阻止其他事务在这个间隙中插入新行,从而避免幻读的发生。
之所以能解决幻读这类问题,是因为通过行锁和间隙锁的组合,有效地限制了其他事务对数据的插入操作,确保了在一个事务执行期间,其读取到的数据不会因为其他事务的插入而发生变化,保证了事务读取结果的稳定性和可重复性。
未分类
volatile 关键字有什么用?它的实现原理是什么?
书中
volatile 关键字有两个作用。
1. 可以保证在多线程环境下共享变量的可见性。
我理解的可见性,是指当某一个线程对共享变量的修改,其他线程可以立刻看到修改之后的值。其实这个可见性问题,我认为本质上是由几个方面造成的。
CPU 层面的高速缓存,在 CPU里面设计了三级缓存去解决 CPU 运算效率和内存 IO 效率问题,但是带来的就是缓存的一致性问题, 而在多线程并行执行的情况下,缓存一致性就会导致可见性问题。
所以,对于增加了 volatile 关键字修饰的共享变量,JVM 虚拟机会自动增加一个#Lock汇编指令,这个指令会根据 CPU 型号自动添加总线锁或/缓存锁我简单说一下这两种锁,总线锁是锁定了 CPU 的前端总线,从 而导致在同一时刻只能有一个线程去和内存通信,这样就避免了多线程并发造成的可见性。缓存锁是对总线锁的优化,因为总线锁导致了 CPU 的使用效率大幅度下降,所以缓存锁只针对 CPU 三级缓存中的目标数据加锁,缓存锁是使用 MESI 缓存一致性来实现的。
2. 通过增加内存屏障防止多个指令之间的重排序。
指令重排序,所谓重排序,就是指令的编写顺序和执行顺序不一致,在多线程环境下导致可见性问题。指令重排序本质上是一种性能优化的手段,它来自几个方面。
CPU 层面,针对 MESI 协议的更进一步优化去提升 CPU 的利用率,引入了 StoreBuffer机制,而这一种优化机制会导致 CPU 的乱序执行。当然为了避免这样的问题,CPU 提供了内存屏障指令,上层应用可以在合适的地方插入内存屏障来避免 CPU 指令重排序问题。编译器的优化,编译器在编译的过程中,在 不改变单线程语义和程序正确性的前提下,对指令进行合理的重排序优化来提升性能。
所以,如果对共享变量增加了 volatile 关键字,那么在编译器层面,就不会去触发编译器优化,同时在JVM 里面,会插入内存屏障指令来避免重排序问题。
当然,除了 volatile 以外,从 JDK5 开始,JMM 就使用了一种 Happens-Before 模型去描述多线程之间的内存可见性问题。如果两个操作之间具备 Happens-Before 关系,那么意味着这两个操作具备可见性关系,不需要再额外去考虑增加 volatile 关键字来提供可见性保障。
说说缓存雪崩和缓存穿透的理解,以及如何避免?
书中
缓存雪崩,就是存储在缓存里面的大量数据,在同一个时刻全部过期,原本缓存组件抗住的大部分流量全部请求到了数据库。导致数据库压力增加造成数据库服务器崩溃的现象。
导致缓存雪崩的主要原因,我认为有两个:
1. 缓存中间件宕机,当然可以对缓存中间件做高可用集群来避免。
2. 缓存中大部分 key 都设置了相同的过期时间,导致同一时刻这些 key 都过期了。对于这样的情况,可以在失效时间上增加一个 1到 5 分钟的随机值。
缓存穿透问题,表示短时间内有大量的不存在的 key 请求到应用里面,而这些不存在的key 在缓存里面又找不到,从而全部穿透到了数据库,造成数据库压力。
我认为这个场景的核心问题是针对缓存的一种攻击行为,因为在正常的业务里面,即便是出现了这样的情况,由于缓存的不断预热,影响不会很大。
而攻击行为就需要具备时间的持续性,而只有 key 确实在数据库里面也不存在的情况下,才能达到这个目的,所以,我认为有两个方法可以解决:
3. 把无效的 key 也保存到 Redis 里面,并且设置一个特殊的值,比如“null”,这样的话下次再来访问,就不会去查数据库了。
4. (如图)但是如果攻击者不断用随机的不存在的 key 来访问,也还是会存在问题,所以可以用布隆过滤器来实现,在系统启动的时候把目标数据全部缓存到布隆过滤器里面,当攻击者用不存在的 key 来请求的时候,先到布隆过滤器里面查询,如果不存在,那意味着这个key 在数据库里面也不存在。
请说一下你对分布式锁的理解,以及分布式锁的实现
书中
分布式锁,是一种跨进程的跨机器节点的互斥锁,它可以用来保证多机器节点对于共享资源访问的排他性。
我觉得分布式锁和线程锁本质上是一样的,线程锁的生命周期是单进程多线程,分布式锁的生命周期是多进程多机器节点。
在本质上,他们都需要满足锁的几个重要特性:
排他性,也就是说,同一时刻只能有一个节点去访问共享资源。
可重入性,允许一个已经获得锁的进程,在没有释放锁之前重新获得锁
锁的获取、释放的方法
锁的失效机理,避免死锁的问题
所以,我认为,只要能够满足这些特性的技术组件都能够实现分布式锁。
豆包
分布式锁是在分布式系统环境下,用于控制多个进程或线程对共享资源的访问,以保证在同一时刻只有一个进程或线程能够获得锁并进行操作,从而避免并发访问导致的数据不一致或错误。
分布式锁的应用场景通常出现在多个服务实例需要协调对共享资源的操作时。
分布式锁的实现方式有多种,常见的有以下几种:
基于数据库实现,通过创建表和相关字段来模拟锁的获取和释放。
基于 Redis 实现,利用 Redis 的命令如 SETNX 来获取锁,并通过设置过期时间来防止死锁。
基于 Zookeeper 实现,利用其节点的特性和临时顺序节点来实现锁的机制。
分布式锁的应用场景通常出现在多个服务实例需要协调对共享资源的操作时。
分布式锁的实现方式有多种,常见的有以下几种:
基于数据库实现,通过创建表和相关字段来模拟锁的获取和释放。
基于 Redis 实现,利用 Redis 的命令如 SETNX 来获取锁,并通过设置过期时间来防止死锁。
基于 Zookeeper 实现,利用其节点的特性和临时顺序节点来实现锁的机制。
ThreadLocal 是什么?它的实现原理呢?
书中
这个问题我从三个方面来回答。
1. ThreadLocal 是一种线程隔离机制,它提供了多线程环境下对于共享变量访问的安全性。
2. 在多线程访问共享变量的场景中(出现下面第一个图),一般的解决办法是对共享变量加锁(出现下面第二个图),从而保证在同一时刻只有一个线程能够对共享变量进行更新,并且基于Happens-Before 规则里面的监视器锁规则,又保证了数据修改后对其他线程的可见性。
3.但是加锁会带来性能的下降,所以ThreadLocal用了一种空间换时间的设计思想,也就是说在每个线程中,都有一个容器来存储共享变量副本,然后每个线程只对自己的变量副本来做更新操作,这样既解决了线程安全问题,又避免了多线程竞争加锁的开销
ThreadLocal 的具体实现原理是,在Thread 类里面有一个成员变量ThreadLocalMap,它专门来存储当前线程的共享变量副本,后续这个线程对于共享变量的操作,都是从这个ThreadLocalMap里面进行变更,不会影响全局共享变量的值。
豆包
ThreadLocal 是 Java 中用于实现线程局部变量的工具类。
ThreadLocal 使得每个线程都可以拥有自己独立的变量副本,互不干扰。这在多线程环境中,当某些数据不需要在多个线程之间共享,而是每个线程都需要有自己独立的一份时非常有用。
其实现原理主要是通过每个线程维护一个 ThreadLocalMap 来存储线程本地变量。ThreadLocalMap 中的键是 ThreadLocal 对象本身,值则是具体的线程本地变量值。
在实际应用场景中,比如在一个 Web 应用中,存储每个线程的用户认证信息,避免在不同线程间混淆。
在我参与的一个高并发的服务项目中,就使用了 ThreadLocal 来存储每个线程的请求上下文信息,保证了线程之间数据的隔离性。
以下是一个简单的 ThreadLocal 使用示例代码:
```java
public class ThreadLocalExample {
private static ThreadLocal<String> threadLocal = new ThreadLocal<>();
public static void main(String[] args) {
new Thread(() -> {
threadLocal.set("Thread 1");
System.out.println("Thread 1: " + threadLocal.get());
}).start();
new Thread(() -> {
threadLocal.set("Thread 2");
System.out.println("Thread 2: " + threadLocal.get());
}).start();
}
}
```
ThreadLocal 使得每个线程都可以拥有自己独立的变量副本,互不干扰。这在多线程环境中,当某些数据不需要在多个线程之间共享,而是每个线程都需要有自己独立的一份时非常有用。
其实现原理主要是通过每个线程维护一个 ThreadLocalMap 来存储线程本地变量。ThreadLocalMap 中的键是 ThreadLocal 对象本身,值则是具体的线程本地变量值。
在实际应用场景中,比如在一个 Web 应用中,存储每个线程的用户认证信息,避免在不同线程间混淆。
在我参与的一个高并发的服务项目中,就使用了 ThreadLocal 来存储每个线程的请求上下文信息,保证了线程之间数据的隔离性。
以下是一个简单的 ThreadLocal 使用示例代码:
```java
public class ThreadLocalExample {
private static ThreadLocal<String> threadLocal = new ThreadLocal<>();
public static void main(String[] args) {
new Thread(() -> {
threadLocal.set("Thread 1");
System.out.println("Thread 1: " + threadLocal.get());
}).start();
new Thread(() -> {
threadLocal.set("Thread 2");
System.out.println("Thread 2: " + threadLocal.get());
}).start();
}
}
```
基于数组的阻塞队列 ArrayBlockingQueue原理
书中
1. (如图)阻塞队列(BlockingQueue)是在队列的基础上增加了两个附加操作,在队列为空的时候,获取元素的线程会等待队列变为非空。当队列满时,存储元素的线程会等待队列可用。
2. 由于阻塞队列的特性,可以非常容易实现生产者消费者模型,也就是生产者只需要关心数据的生产,消费者只需要关注数据的消费,所以如果队列满了,生产者就等待,同样,队列空了,消费者也需要等待。
3. 要实现这样的一个阻塞队列,需要用到两个关键的技术,队列元素的存储,以及线程阻塞和唤醒。
4. 而 ArrayBlockingQueue 是基于数组结构的阻塞队列,也就是队列元素是存储在一个数组结构里面,并且由于数组有长度限制,为了达到循环生产和循环消费的目的,ArrayBlockingQueue 用到了循环数组。
5. 而线程的阻塞和唤醒,用到了 J.U.C 包里面的 ReentrantLock 和 Condition。Condition 相当于wait/notify 在 JUC 包里面的实现。
豆包
ArrayBlockingQueue 是一个基于数组实现的有界阻塞队列。
它的主要概念是用于存储元素的数组有固定的大小,当队列已满时,进行入队操作的线程会被阻塞;当队列为空时,进行出队操作的线程会被阻塞。
ArrayBlockingQueue 的实现原理基于以下几个关键方面:
1.内部使用一个固定大小的数组来存储元素。
2通过两个指针,分别指向队头和队尾,来管理元素的入队和出队操作。
3.使用锁和条件变量来实现线程的阻塞和唤醒,以保证线程安全和阻塞操作的正确性。
它的主要概念是用于存储元素的数组有固定的大小,当队列已满时,进行入队操作的线程会被阻塞;当队列为空时,进行出队操作的线程会被阻塞。
ArrayBlockingQueue 的实现原理基于以下几个关键方面:
1.内部使用一个固定大小的数组来存储元素。
2通过两个指针,分别指向队头和队尾,来管理元素的入队和出队操作。
3.使用锁和条件变量来实现线程的阻塞和唤醒,以保证线程安全和阻塞操作的正确性。
什么是聚集索引和非聚集索引
书中
1. 简单来说,聚集索引就是基于主键创建的索引,除了主键索引以外的其他索引,称为非聚集索引,也叫作二级索引。
2. 由于在 InnoDB 引擎里面,一张表的数据对应的物理文件本身就是按照 B+树来组织的一种索引结构,而聚集索引就是按照每张表的主键来构建一颗 B+树,然后叶子节点里面存储了这个表的每一行数据记录。
3. 所以基于 InnoDB 这样的特性,聚集索引并不仅仅是一种索引类型,还代表着一种数据的存储方式。
4. 同时也意味着每个表里面必须有一个主键,如果没有主键,InnoDB 会默认选择或者添加一个隐藏列作为主键索引来存储这个表的数据行。一般情况是建议使用自增 id 作为主键,这样的话 id 本身具有连续性使得对应的数据也会按照顺序存储在磁盘上,写入性能和检索性能都很高。否则,如果使用 uuid 这种随机id,那么在频繁插入数据的时候,就会导致随机磁盘 IO,从而导致性能降低。
5. 需要注意的是,InnoDB 里面只能存在一个聚集索引,原因很简单,如果存在多个聚集索引,那么意味着这个表里面的数据存在多个副本,造成磁盘空间的浪费,以及数据维护的困难。
6. (如图)由于在 InnoDB 里面,主键索引表示的是一种数据存储结构,所以如果是基于非聚集索引来查询一条完整的记录,最终还是需要访问主键索引来检索。
豆包
聚集索引和非聚集索引是数据库中用于优化数据查询和存储的重要概念。
聚集索引决定了表中数据的物理存储顺序。也就是说,表中的数据是按照聚集索引列的值进行排序存储的。一张表只能有一个聚集索引。通常,主键会被设置为聚集索引。
非聚集索引则不影响表中数据的实际存储顺序。它类似于书籍的目录,只包含索引列的值和指向数据行的指针。一个表可以有多个非聚集索引。
在实际应用中,比如在一个电商系统的订单表中,如果经常按照订单号查询订单详情,可将订单号设置为聚集索引;而如果经常按照用户 ID 查询订单,可将用户 ID 设为非聚集索引。
在我参与的一个数据库优化项目中,曾经对一张商品表进行了索引优化。最初,查询商品信息时速度较慢,经过分析发现是索引设置不合理。我们根据业务需求,将商品 ID 设为聚集索引,商品分类 ID 设为非聚集索引,大大提高了查询效率。
以下是一个简单的示例,展示了创建聚集索引和非聚集索引的 SQL 语句:
```sql
-- 创建聚集索引
CREATE CLUSTERED INDEX idx_order_id ON orders(order_id);
-- 创建非聚集索引
CREATE NONCLUSTERED INDEX idx_user_id ON orders(user_id);
```
聚集索引决定了表中数据的物理存储顺序。也就是说,表中的数据是按照聚集索引列的值进行排序存储的。一张表只能有一个聚集索引。通常,主键会被设置为聚集索引。
非聚集索引则不影响表中数据的实际存储顺序。它类似于书籍的目录,只包含索引列的值和指向数据行的指针。一个表可以有多个非聚集索引。
在实际应用中,比如在一个电商系统的订单表中,如果经常按照订单号查询订单详情,可将订单号设置为聚集索引;而如果经常按照用户 ID 查询订单,可将用户 ID 设为非聚集索引。
在我参与的一个数据库优化项目中,曾经对一张商品表进行了索引优化。最初,查询商品信息时速度较慢,经过分析发现是索引设置不合理。我们根据业务需求,将商品 ID 设为聚集索引,商品分类 ID 设为非聚集索引,大大提高了查询效率。
以下是一个简单的示例,展示了创建聚集索引和非聚集索引的 SQL 语句:
```sql
-- 创建聚集索引
CREATE CLUSTERED INDEX idx_order_id ON orders(order_id);
-- 创建非聚集索引
CREATE NONCLUSTERED INDEX idx_user_id ON orders(user_id);
```
怎么理解线程安全?
书中
简单来说,在多个线程访问某个方法或者对象的时候,不管通过任何的方式调用以及线程如何去交替执行。在程序中不做任何同步干预操作的情况下,这个方法或者对象的执行/修改都能按照预期的结果来反馈,那么这个类就是线程安全的。
实际上,线程安全问题的具体表现体现在三个方面,原子性、有序性、可见性。原子性呢,是指当一个线程执行一系列程序指令操作的时候,它应该是不可中断的,因为一旦出现中断,站在多线程的视角来看,这一系列的程序指令会出现前后执行结果不一致的问题。这个和数据库里面的原子性是一样的,简单来说就是一段程序只能由一个线程完整地执行完成,而不能存在多个线程干扰。
CPU 的上下文切换,是导致原子性问题的核心,而 JVM 里面提供了Synchronized 关键字来解决原子性问题。
可见性,就是说在多线程环境下,由于读和写是发生在不同的线程里面,有可能出现某个线程对共享变量的修改,对其他线程不是实时可见的。导致可见性问题的原因有很多,比如 CPU的高速缓存、CPU 的指令重排序、编译器的指令重排序。
有序性,指的是程序编写的指令顺序和最终CPU 运行的指令顺序可能出现不一致的现象,这种现象也可以称为指令重排序,所以有序性也会导致可见性问题。
可见性和有序性可以通过 JVM 里面提供了一个 Volatile 关键字来解决。在我看来,导致有序性、原子性、可见性问题的本质,是计算机工程师为了最大化提升CPU 利用率导致的。比如为了提升 CPU 利用率,设计了三级缓存、设计了StoreBuffer、设计了缓存行这种预读机制、在操作系统里面,设计了线程模型、在编译器里面,设计了编译器的深度优化机制。
请简述一下伪共享的概念以及如何避免
书中
首先,计算机工程师为了提高 CPU 的利用率,平衡 CPU 和内存之间的速度差异,在CPU 里面设计了三级缓存。CPU 在向内存发起 IO 操作的时候,一次性会读取 64 个字节的数据作为一个缓存行,缓存到 CPU 的高速缓存里面。在 Java 中一个 long 类型是 8 个字节,意味着一个缓存行可以存储 8 个 long 类型的变量。这个设计是基于空间局部性原理来实现的,也就是说,如果一个存储器的位置被引用,那么将来它附近的位置也会被引用。所以缓存行的设计对于 CPU 来说,可以有效地减少和内存的交互次数,从而避免了CPU 的 IO 等待,以提升 CPU 的利用率。正是因为这种缓存行的设计,导致如果多个线程修改同一个缓存行里面的多个独立变量的时候,基于缓存一致性协议,就会无意中影响了彼此的性能,这就是伪共享的问题。
像这样一种情况,CPU0 上运行的线程想要更新变量 X、CPU1 上的线程想要更新变量 Y,而X/Y/Z 都在同一个缓存行里面。每个线程都需要去竞争缓存行的所有权对变量做更新,基于缓存一致性协议。
一旦运行在某个 CPU 上的线程获得了所有权并执行了修改,就会导致其他 CPU 中的缓存行失效。
因为伪共享问题导致缓存锁的竞争,所以在并发场景中的程序执行效率一定会受到较大的影响。
这个问题的解决办法有两个:
1. 使用对齐填充,因为一个缓存行大小是 64个字节,如果读取的目标数据小于 64 个字节,可以增加一些无意义的成员变量来填充。
2. 在 Java8 里面,提供了@Contented 注解,它也是通过缓存行填充来解决伪共享问题的,被@Contented 注解声明的类或者字段,会被加载到独立的缓存行上。
豆包
伪共享指的是在多线程环境中,虽然不同线程操作的是不同变量,但由于这些变量在内存中处于同一个缓存行,当一个线程修改其中一个变量时,会导致整个缓存行失效,其他线程在访问同缓存行中的其他变量时,就需要重新从内存中读取,从而影响性能。
避免伪共享的常见方法有以下几种:
字节填充:通过在变量之间填充无用的字节,使得相关变量处于不同的缓存行。
使用缓存行对齐的类库,例如 jdk8 中的 @Contended 注解。
避免伪共享的常见方法有以下几种:
字节填充:通过在变量之间填充无用的字节,使得相关变量处于不同的缓存行。
使用缓存行对齐的类库,例如 jdk8 中的 @Contended 注解。
为什么要使用 Spring 框架?
书中
Spring 是一个轻量级应用框架,它提供了IoC 和 AOP 这两个核心的功能。它的核心目的是简化企业级应用程序的开发,使得开发者只需要关心业务需求,不需要关心 Bean 的管理,以及通过切面增强功能减少代码的侵入性。从 Spring 本身的特性来看,我认为有几个关键点是我们选择 Spring 框架的原因。轻量:Spring 是轻量的,基本的版本大约2MB。IOC/DI:Spring 通过 IOC 容器实现了Bean 的生命周期的管理,以及通过 DI 实现依赖注入,从而实现了对象依赖的松耦合管理。
面向切面的编程(AOP):Spring 支持面向切面的编程,从而把应用业务逻辑和系统服务分开。MVC 框架:Spring MVC 提供了功能更加强大且更加灵活的 Web 框架支持事务管理:Spring 通过 AOP 实现了事务的统一管理,对应用开发中的事务处理提供了非常灵活的支持
Spring生态几乎提供了非常完善地支持,更重要的是社区的活跃度和技术的成熟度都非常高,以上就是我对这个问题的理解。
Dubbo 是如何动态感知服务下线的?
书中
首先,Dubbo 默认采用 Zookeeper 实现服务的注册与服务发现,简单来说啊,就是多个 Dubbo 服务之间的通信地址,是使用Zookeeper 来维护的。
在 Zookeeper 上,会采用树形结构的方式来维护 Dubbo 服务提供端的协议地址,Dubbo 服务消费端会从 ZookeeperServer 上去查找目标服务的地址列表,从而完成服务的注册和消费的功能。
Zookeeper 会通过心跳检测机制,来判断Dubbo 服务提供端的运行状态,来决定是否应该把这个服务下线
当 Dubbo 服务提供方出现故障导致Zookeeper 剔除了这个服务的地址,那么Dubbo 服务消费端需要感知到地址的变化,从而避免后续的请求发送到故障节点,导致请求失败。也就是说 Dubbo 要提供服务下线的动态感知能力。
这个能力是通过 Zookeeper 里面提供的 Watch 机制来实现的,简单来说呢,Dubbo 服务消费端会使用 Zookeeper里面的 Watch 来针对 Zookeeper Server 端的/providers 节点注册监听,一旦这个节点下的子节点发生变化,Zookeeper Server 就会发送一个事件通知 Dubbo Client 端.
Dubbo Client 端收到事件以后,就会把本地缓存的这个服务地址删除,这样后续就不会把请求发送到失败的节点上,完成服务下线感知。
豆包
Dubbo 能够动态感知服务下线,这对于维持系统的稳定性和可靠性非常重要。
Dubbo 实现服务下线感知的原理主要基于注册中心。当服务提供者下线时,会主动向注册中心注销自己的服务信息。注册中心会实时监测服务提供者的状态变化,并将这些变化通知给服务消费者。
实现动态感知服务下线的方式有多种。一种常见的方式是通过心跳机制,服务提供者定期向注册中心发送心跳信息,若注册中心在一定时间内未收到某个服务提供者的心跳,则认为该服务已下线,并通知相关的服务消费者。另外,注册中心还可以通过长连接的方式与服务提供者和消费者保持通信,及时传递服务状态的变化。
Dubbo 实现服务下线感知的原理主要基于注册中心。当服务提供者下线时,会主动向注册中心注销自己的服务信息。注册中心会实时监测服务提供者的状态变化,并将这些变化通知给服务消费者。
实现动态感知服务下线的方式有多种。一种常见的方式是通过心跳机制,服务提供者定期向注册中心发送心跳信息,若注册中心在一定时间内未收到某个服务提供者的心跳,则认为该服务已下线,并通知相关的服务消费者。另外,注册中心还可以通过长连接的方式与服务提供者和消费者保持通信,及时传递服务状态的变化。
Spring 中 Bean 的作用域有哪些?
书中
首先呢,Spring 框架里面的 IOC 容器,可以非常方便地去帮助我们管理应用里面的Bean 对象实例。
理论上来说,常规的生命周期只有两种:
singleton, 也就是单例,意味着在整个Spring 容器中只会存在一个 Bean 实例。
prototype,翻译成原型,意味着每次从 IOC容器去获取指定 Bean 的时候,都会返回一 个新的实例对象。
但是在基于 Spring 框架下的 Web 应用里面,增加了一个会话维度来控制 Bean 的生命周期,主要有三个选择request, 针对每一次 http 请求,都会创建一个新的 Bean
session,以 sesssion 会话为纬度,同一个session 共享同一个 Bean 实例,不同的session 产生不同的 Bean 实例
globalSession,针对全局 session 纬度,共享同一个 Bean 实例
豆包
在 Spring 中,Bean 的作用域决定了 Bean 实例的创建和管理方式。
常见的作用域有以下几种:
singleton(单例):在整个 Spring 容器中,只会创建一个 Bean 实例,默认的作用域就是单例。
prototype(原型):每次获取 Bean 时都会创建一个新的实例。
request:在 Web 应用中,针对每个 HTTP 请求创建一个新的 Bean 实例。
session:在 Web 应用中,针对每个用户会话创建一个新的 Bean 实例。
global-session:在基于 Portlet 的 Web 应用中使用,作用类似于 session 作用域。
在解决不同的业务需求时,可以根据具体情况选择合适的作用域。比如,如果资源消耗较大且不常使用,可能选择原型作用域;对于需要在整个应用中共享且不变的对象,通常使用单例作用域。
常见的作用域有以下几种:
singleton(单例):在整个 Spring 容器中,只会创建一个 Bean 实例,默认的作用域就是单例。
prototype(原型):每次获取 Bean 时都会创建一个新的实例。
request:在 Web 应用中,针对每个 HTTP 请求创建一个新的 Bean 实例。
session:在 Web 应用中,针对每个用户会话创建一个新的 Bean 实例。
global-session:在基于 Portlet 的 Web 应用中使用,作用类似于 session 作用域。
在解决不同的业务需求时,可以根据具体情况选择合适的作用域。比如,如果资源消耗较大且不常使用,可能选择原型作用域;对于需要在整个应用中共享且不变的对象,通常使用单例作用域。
Zookeeper 中的 Watch 机制的原理?
书中
Zookeeper 是一个分布式协调组件,为分布式架构下的多个应用组件提供了顺序访问控制能力。
它的数据存储采用了类似于文件系统的树形结构,以节点的方式来管理存储在Zookeeper 上
Zookeeper 提供了一个 Watch 机制,可以让客户端感知到 Zookeeper Server 上存储的数据变化,这样一种机制可以让 Zookeeper 实现很多的场景,比如配置中心、注册中心等。
Watch 机制采用了 Push 的方式来实现,也就是说客户端和 Zookeeper Server 会建立一个长连接,一旦监听的指定节点发生了变化,就会通过这个长连接把变化的事件推送给客户端。
Watch 的具体流程分为几个部分:首先,是客户端通过指定命令比如 exists、get,对特定路径增加 watch然后服务端收到请求以后,用 HashMap 保存这个客户端会话以及对应关注的节点路径,同时客户端也会使用 HashMap存储指定节点和事件回调函数的对应关系。当服务端指定被 watch 的节点发生变化后,就会找到这个节点对应的会话,把变化的事件和节点信息发给这个客户端。
豆包
Zookeeper 中的 Watch 机制是一种用于实现分布式系统中节点变化通知的重要机制。
Watch 机制的概念在于允许客户端在 Zookeeper 节点上设置监视点(Watch),当被监视的节点发生数据变更、子节点变更或节点删除等操作时,Zookeeper 服务端会主动向客户端发送通知。
其实现原理主要包括以下几个方面:
1. 客户端在向服务端请求读取数据时,可以同时设置 Watch。
2. 服务端会记录客户端设置的 Watch 信息。
3. 当节点发生变化时,服务端会遍历相关的 Watch 列表,找到对应的客户端并发送通知。
4. 客户端收到通知后,会进行相应的处理,然后可能会重新设置 Watch 以继续监视节点的变化。
在解决分布式系统中的数据一致性和实时响应问题时,Watch 机制发挥了重要作用。比如在实现分布式配置管理时,可以通过 Watch 机制及时感知配置的变化。
在我参与的一个分布式任务调度系统项目中,就利用了 Zookeeper 的 Watch 机制。当任务分配节点发生变化时,通过 Watch 机制及时通知相关的工作节点,实现了任务的动态分配和调整。
以下是一个简单的示意代码,展示了如何在 Java 中使用 Zookeeper 的 Watch 机制(并非完整可运行的代码):
```java
import org.apache.zookeeper.WatchedEvent;
import org.apache.zookeeper.Watcher;
import org.apache.zookeeper.ZooKeeper;
public class ZookeeperWatchExample implements Watcher {
private ZooKeeper zooKeeper;
public ZookeeperWatchExample(String connectionString) throws Exception {
zooKeeper = new ZooKeeper(connectionString, 5000, this);
}
@Override
public void process(WatchedEvent event) {
// 处理收到的通知事件
System.out.println("Received event: " + event.getType());
}
public static void main(String[] args) throws Exception {
ZookeeperWatchExample example = new ZookeeperWatchExample("localhost:2181");
// 进行其他操作
}
}
```
Watch 机制的概念在于允许客户端在 Zookeeper 节点上设置监视点(Watch),当被监视的节点发生数据变更、子节点变更或节点删除等操作时,Zookeeper 服务端会主动向客户端发送通知。
其实现原理主要包括以下几个方面:
1. 客户端在向服务端请求读取数据时,可以同时设置 Watch。
2. 服务端会记录客户端设置的 Watch 信息。
3. 当节点发生变化时,服务端会遍历相关的 Watch 列表,找到对应的客户端并发送通知。
4. 客户端收到通知后,会进行相应的处理,然后可能会重新设置 Watch 以继续监视节点的变化。
在解决分布式系统中的数据一致性和实时响应问题时,Watch 机制发挥了重要作用。比如在实现分布式配置管理时,可以通过 Watch 机制及时感知配置的变化。
在我参与的一个分布式任务调度系统项目中,就利用了 Zookeeper 的 Watch 机制。当任务分配节点发生变化时,通过 Watch 机制及时通知相关的工作节点,实现了任务的动态分配和调整。
以下是一个简单的示意代码,展示了如何在 Java 中使用 Zookeeper 的 Watch 机制(并非完整可运行的代码):
```java
import org.apache.zookeeper.WatchedEvent;
import org.apache.zookeeper.Watcher;
import org.apache.zookeeper.ZooKeeper;
public class ZookeeperWatchExample implements Watcher {
private ZooKeeper zooKeeper;
public ZookeeperWatchExample(String connectionString) throws Exception {
zooKeeper = new ZooKeeper(connectionString, 5000, this);
}
@Override
public void process(WatchedEvent event) {
// 处理收到的通知事件
System.out.println("Received event: " + event.getType());
}
public static void main(String[] args) throws Exception {
ZookeeperWatchExample example = new ZookeeperWatchExample("localhost:2181");
// 进行其他操作
}
}
```
Redis 存在线程安全问题吗?为什么?
书中
第一个,从 Redis 服务端层面。
Redis Server 本身是一个线程安全的 K-V数据库,也就是说在 Redis Server 上执行的指令,不需要任何同步机制,不会存在线程安全问题。
虽然 Redis 6.0 里面,增加了多线程的模型,但是增加的多线程只是用来处理网络 IO 事件,对于指令的执行过程,仍然是由主线程来处理,所以不会存在多个线程同时执行操作指令的情况。
为什么 Redis 没有采用多线程来执行指令,我认为有几个方面的原因。Redis Server 本身可能出现的性能瓶颈点无非就是网络 IO、CPU、内存。但是 CPU 不是 Redis的瓶颈点,所以没必要使用多线程来执行指令。
第二个,从 Redis 客户端层面。
虽然 Redis Server 中的指令执行是原子的,但是如果有多个 Redis 客户端同时执行多个指令的时候,就无法保证原子性。假设两个 redis client 同时获取 RedisServer 上的 key1, 同时进行修改和写入, 因为多线程环境下的原子性无法被保障,以及多进程情况下的共享资源访问的竞争问题,使得数据的安全性无法得到保障。
当然,对于客户端层面的线程安全性问题,解决方法有很多,比如尽可能地使用 Redis里面的原子指令,或者对多个客户端的资源访问加锁,或者通过 Lua 脚本来实现多个指令的操作等等。
Spring 中 BeanFactory 和 FactoryBean 的区别
书中
首先,Spring 里面的核心功能是 IOC 容器,所谓 IOC 容器呢,本质上就是一个Bean 的容器或者是一个 Bean 的工厂。它能够根据 xml 里面声明的 Bean 配置进行bean 的加载和初始化,然后 BeanFactory 来生产我们需要的各种各样的 Bean。
所以我对 BeanFactory 的理解了有两个。BeanFactory 是所有 Spring Bean 容器的顶级接口,它为 Spring 的容器定义了一套规范,并提供像 getBean 这样的方法从容器中获取指定的Bean 实例。BeanFactory 在产生 Bean 的同时,还提供了解决 Bean 之间的依赖注入的能力,也就是所谓的DI。
FactoryBean 是一个工厂 Bean,它是一个接口,主要的功能是动态生成某一个类型的Bean 的实例,也就是说,我们可以自定义一个 Bean 并且加载到 IOC 容器里面。它里面有一个重要的方法叫 getObject(),这个方法里面就是用来实现动态构建 Bean的过程。Spring Cloud 里面的 OpenFeign 组件,客户端的代理类,就是使用了 FactoryBean 来实现的。
豆包
在 Spring 框架中,`BeanFactory` 和 `FactoryBean` 是两个不同的概念:
**BeanFactory**
`BeanFactory` 是 Spring 容器的核心接口,它负责管理 Bean 的定义和实例化。它提供了基本的功能来获取 Bean 的实例、检查 Bean 是否存在、获取 Bean 的类型等。
`BeanFactory` 采用延迟加载策略,即只有在实际请求获取 Bean 实例时,才会创建 Bean 实例。
**FactoryBean**
`FactoryBean` 是一个特殊的 Bean,它本身不是要创建的对象,而是用于创建其他对象的工厂。
实现 `FactoryBean` 接口的类可以自定义对象的创建逻辑。通过实现 `getObject()` 方法来创建和返回实际的对象,还可以通过 `getObjectType()` 方法指定返回对象的类型。
例如,如果您需要创建一个复杂的对象,其创建过程涉及多个步骤或依赖于特定的条件,就可以实现 `FactoryBean` 来定制创建过程。
下面是一个简单的 `FactoryBean` 示例:
```java
public class CustomFactoryBean implements FactoryBean<MyObject> {
@Override
public MyObject getObject() throws Exception {
// 复杂的对象创建逻辑
return new MyObject();
}
@Override
public Class<?> getObjectType() {
return MyObject.class;
}
@Override
public boolean isSingleton() {
return true;
}
}
```
在配置文件中,可以这样配置使用:
```xml
<bean id="customBean" class="com.example.CustomFactoryBean" />
```
当从容器中获取 `customBean` 时,实际上得到的是通过 `CustomFactoryBean` 创建的 `MyObject` 对象。
总的来说,`BeanFactory` 是容器的基础接口,负责管理 Bean ;而 `FactoryBean` 是用于自定义创建 Bean 实例的一种机制。
**BeanFactory**
`BeanFactory` 是 Spring 容器的核心接口,它负责管理 Bean 的定义和实例化。它提供了基本的功能来获取 Bean 的实例、检查 Bean 是否存在、获取 Bean 的类型等。
`BeanFactory` 采用延迟加载策略,即只有在实际请求获取 Bean 实例时,才会创建 Bean 实例。
**FactoryBean**
`FactoryBean` 是一个特殊的 Bean,它本身不是要创建的对象,而是用于创建其他对象的工厂。
实现 `FactoryBean` 接口的类可以自定义对象的创建逻辑。通过实现 `getObject()` 方法来创建和返回实际的对象,还可以通过 `getObjectType()` 方法指定返回对象的类型。
例如,如果您需要创建一个复杂的对象,其创建过程涉及多个步骤或依赖于特定的条件,就可以实现 `FactoryBean` 来定制创建过程。
下面是一个简单的 `FactoryBean` 示例:
```java
public class CustomFactoryBean implements FactoryBean<MyObject> {
@Override
public MyObject getObject() throws Exception {
// 复杂的对象创建逻辑
return new MyObject();
}
@Override
public Class<?> getObjectType() {
return MyObject.class;
}
@Override
public boolean isSingleton() {
return true;
}
}
```
在配置文件中,可以这样配置使用:
```xml
<bean id="customBean" class="com.example.CustomFactoryBean" />
```
当从容器中获取 `customBean` 时,实际上得到的是通过 `CustomFactoryBean` 创建的 `MyObject` 对象。
总的来说,`BeanFactory` 是容器的基础接口,负责管理 Bean ;而 `FactoryBean` 是用于自定义创建 Bean 实例的一种机制。
什么是可重入,什么是可重入锁? 它用来解决什么问题?
书中
可重入是多线程并发编程里面一个比较重要的概念,简单来说,就是在运行的某个函数或者代码,因为抢占资源或者中断等原因导致函数或者代码的运行中断,等待中断程序执行结束后,重新进入到这个函数或者代码中运行,并且运行结果不会受到影响,那么这个函数或者代码就是可重入的。(如图) 而可重入锁,简单来说就是一个线程如果抢占到了互斥锁资源,在锁释放之前再去竞争同一把锁的时候,不需要等待,只需要记录重入次数。
在多线程并发编程里面,绝大部分锁都是可重入的,比如 Synchronized、ReentrantLock 等,但是也有不支持重入的锁,比如 JDK8 里面提供的读写锁StampedLock。
锁的可重入性,主要解决的问题是避免线程死锁的问题。因为一个已经获得同步锁 X 的线程,在释放锁 X 之前再去竞争锁 X 的时候,相当于会出现自己要等待自己释放锁,这很显然是无法成立的。
豆包
可重入指的是一个函数在执行过程中可以再次被自身调用,并且不会出现错误或异常情况。
可重入锁是一种特殊的锁,它允许同一个线程多次获取该锁而不会造成死锁。
可重入锁主要用来解决以下问题:
1. 避免同一个线程因为多次获取锁而导致死锁。
2. 方便在嵌套函数或递归函数中正确地进行同步控制。
在实际开发中,比如在一个复杂的业务逻辑中,可能存在多层嵌套的方法调用,如果使用不可重入锁,就可能导致线程被阻塞,而可重入锁能很好地应对这种情况。
在我之前参与的一个资源管理系统项目中,就使用了可重入锁来处理复杂的资源分配逻辑。当一个线程在执行一个方法获取了可重入锁后,又在内部调用了另一个需要相同锁的方法,由于是可重入锁,所以不会出现死锁问题。
以下是一个简单的 Java 可重入锁示例代码(并非完整可运行的代码):
```java
import java.util.concurrent.locks.ReentrantLock;
public class ReentrantLockExample {
private ReentrantLock lock = new ReentrantLock();
public void method1() {
lock.lock();
try {
// 业务逻辑
method2();
} finally {
lock.unlock();
}
}
public void method2() {
lock.lock();
try {
// 业务逻辑
} finally {
lock.unlock();
}
}
}
```
可重入锁是一种特殊的锁,它允许同一个线程多次获取该锁而不会造成死锁。
可重入锁主要用来解决以下问题:
1. 避免同一个线程因为多次获取锁而导致死锁。
2. 方便在嵌套函数或递归函数中正确地进行同步控制。
在实际开发中,比如在一个复杂的业务逻辑中,可能存在多层嵌套的方法调用,如果使用不可重入锁,就可能导致线程被阻塞,而可重入锁能很好地应对这种情况。
在我之前参与的一个资源管理系统项目中,就使用了可重入锁来处理复杂的资源分配逻辑。当一个线程在执行一个方法获取了可重入锁后,又在内部调用了另一个需要相同锁的方法,由于是可重入锁,所以不会出现死锁问题。
以下是一个简单的 Java 可重入锁示例代码(并非完整可运行的代码):
```java
import java.util.concurrent.locks.ReentrantLock;
public class ReentrantLockExample {
private ReentrantLock lock = new ReentrantLock();
public void method1() {
lock.lock();
try {
// 业务逻辑
method2();
} finally {
lock.unlock();
}
}
public void method2() {
lock.lock();
try {
// 业务逻辑
} finally {
lock.unlock();
}
}
}
```
请说一下 ReentrantLock 的实现原理?
书中
ReentrantLock 是一种可重入的排他锁,主要用来解决多线程对共享资源竞争的问题。
它支持可重入,也就是获得锁的线程在释放锁之前再次去竞争同一把锁的时候,不需要加锁就可以直接访问。
4. 它支持公平和非公平特性5. 它提供了阻塞竞争锁和非阻塞竞争锁的两种方法,分别是 lock()和 tryLock()。(如图)然后,ReentrantLock 的底层实现有几个非常关键的技术。6. 锁的竞争,ReentrantLock 是通过互斥变量,使用 CAS 机制来实现的。
7. 没有竞争到锁的线程,使用了AbstractQueuedSynchronizer 这样一个队列同步器来存储,底层是通过双向链表来实现的。当锁被释放之后,会从 AQS 队列里面的头部唤醒下一个等待锁的线程。8. 公平和非公平的特性,主要是体现在竞争锁的时候,是否需要判断 AQS 队列存在等待中的线程。9. 最后,关于锁的重入特性,在 AQS 里面有一个成员变量来保存当前获得锁的线程,
当同一个线程下次再来竞争锁的时候,就不会去走锁竞争的逻辑,而是直接增加重入次数。
豆包
ReentrantLock 是基于 AQS(AbstractQueuedSynchronizer)实现的。
AQS 是一个用于构建锁和同步器的框架。ReentrantLock 利用 AQS 的状态变量来表示锁的持有情况。
ReentrantLock 的实现原理主要包括以下几个方面:
1. 状态管理:通过一个整型变量 state 来表示锁的获取次数。
2. 线程等待队列:未获取到锁的线程会被放入等待队列中。
3. 锁的获取与释放:获取锁时,如果 state 为 0 则成功获取并增加 state 值;如果不为 0 且当前线程是持有锁的线程,则增加 state 值,否则将线程加入等待队列。释放锁时,减少 state 值,若为 0 则唤醒等待队列中的线程。
在实际应用中,比如在一个多线程并发处理任务的系统中,ReentrantLock 可以确保线程安全地访问共享资源。
在我之前参与的一个分布式任务调度系统中,就使用了 ReentrantLock 来保证任务分配的线程安全。
以下是一个简单的 ReentrantLock 示例代码的核心部分(并非完整可运行的代码):
```java
import java.util.concurrent.locks.ReentrantLock;
public class ReentrantLockExample {
private ReentrantLock lock = new ReentrantLock();
public void doSomething() {
lock.lock();
try {
// 共享资源的操作
} finally {
lock.unlock();
}
}
}
```
AQS 是一个用于构建锁和同步器的框架。ReentrantLock 利用 AQS 的状态变量来表示锁的持有情况。
ReentrantLock 的实现原理主要包括以下几个方面:
1. 状态管理:通过一个整型变量 state 来表示锁的获取次数。
2. 线程等待队列:未获取到锁的线程会被放入等待队列中。
3. 锁的获取与释放:获取锁时,如果 state 为 0 则成功获取并增加 state 值;如果不为 0 且当前线程是持有锁的线程,则增加 state 值,否则将线程加入等待队列。释放锁时,减少 state 值,若为 0 则唤醒等待队列中的线程。
在实际应用中,比如在一个多线程并发处理任务的系统中,ReentrantLock 可以确保线程安全地访问共享资源。
在我之前参与的一个分布式任务调度系统中,就使用了 ReentrantLock 来保证任务分配的线程安全。
以下是一个简单的 ReentrantLock 示例代码的核心部分(并非完整可运行的代码):
```java
import java.util.concurrent.locks.ReentrantLock;
public class ReentrantLockExample {
private ReentrantLock lock = new ReentrantLock();
public void doSomething() {
lock.lock();
try {
// 共享资源的操作
} finally {
lock.unlock();
}
}
}
```
Mybatis 中#{}和${}的区别是什么?
书中
首先,Mybatis 提供的#号占位符和$号占位符,都是实现动态 SQL 的一种方式,通过这两种方式把参数传递到 XML 之后,在执行操作之前,Mybatis 会对这两种占位符进行动态解析。(如图)#号占位符,等同于 jdbc 里面的?号占位符。它相当于向 PreparedStatement 中的预处理语句中设置参数,而 PreparedStatement 中的 sql 语句是预编译的,SQL 语句中使用了占位符,规定了 sql 语句的结构。并且在设置参数的时候,如果有特殊字符,会自动进行转义。所以#号占位符可以防止 SQL 注入。
而使用$的方式传参,相当于直接把参数拼接到了原始的 SQL 里面,Mybatis不会对它进行特殊处理。 所以$和#最大的区别在于,前者是动态参数,后者是占位符, 动态参数无法防止SQL 注入的问题,所以在实际应用中,应该尽可能地使用#号占位符。另外,$符号的动态传参,可以适合应用在一些动态 SQL 场景中,比如动态传递表名、动态设置排序字段等。
数据库连接池有什么用?它有哪些关键参数?
书中
0 条评论
下一页