骏 伯 Junbo

使命:让移动互联网营销简单高效

价值观:创造价值,分享价值

愿景:做最受信赖的移动互联网营销服务商


蔡枫 的2023年终总结

  • 回顾2023,你印象最深刻的2-3个 高光时刻 是什么?

    1. 愿意去了解,理解新事物的时候,从电影和音乐等感受乐趣,共情与哲思

    2. 准备一年拿到奖学金,重修两门课程高分通过

    3. 通过面试,进入骏伯,一定多向同事们学习

  • 回顾2023,你印象最深刻的2-3个 艰难时刻 是什么?

    1. 放弃考研和出国,感觉是当下比较好的决定

    2. 想到家里情况是多么窘迫的时刻

    3. 发现脱发症状的时候

  • 目标A:准备就业

    大概是八月份,打消了考研以及留学的念头后,由于有Java开发的经验,自然往这个方向准备就业。
    要学习的内容特别多,原以为有基础便能用三个月左右准备好,事实上即使每天都在尽力填充自己,如此半年后还剩下许多没准备好的,更别说要能够在面试时从容对答。但是并不焦虑,只要做好规划,继续学下去,有限的内容总是能够准备完成的,剩下就是准备和面对不喜欢的面试环节。
    2024年1月4日,我通过了骏伯的面试,岗位是Java开发实习生。面试中,我提到希望接触一些高并发、分布式这样的复杂场景中的“高大上”的问题。进入公司两周,我了解到在实际工作中更多的是在不断地解决需求,跟团队成员协作对接,真正重要的是要有解决实际问题的能力。
    每次向导师提问的时候,他的分享毫无保留,各位同事都是我学习的对象。未来的日子,我将尽全力完成每个安排到的任务,提升编码能力,为公司创造价值。


01.10

  • 模块 服务?一个服务占用一个端口, 在 .gradle 配置
    {
    demo-api: 服务化接口定义项目
    demo-service: 服务化实现项目
    }
    api 接口与实现 分离,
    UserApi接口上加 @FeignClient, 请求均分发送到多台服务器上
    spring注入 与 单例模式。。

  • 敏捷开发:一种应对快速变化需求的一种软件开发能力。相对于”非敏捷”,更强调程序员团队与业务专家之间的紧密协作、面对面的沟通(认为比书面的文档更有效)、频繁交付新的软件版本、紧凑而自我组织型的团队、能够很好地适应需求变化的代码编写和团队组织方法,也更注重做为软件开发中人的作用。

01.18

  • 使用不同的窗口打开一个项目下的不同服务、、

  • 使用 IDEA -> Run/Debug Configuration -> Shorten command line 选项用于指定是否缩短命令行,以提高可读性或管理性,其中 None 表示不缩短,Classpath File 表示通过类路径文件缩短。
    Java 源代码被编译成字节码文件(.class 文件)。字节码文件在 Java 虚拟机(JVM)上运行。运行时配置参数通过命令行或配置文件传递给 JVM 和应用程序。运行时参数包括类路径、系统属性、环境变量、应用程序参数等,用于影响应用程序在运行时的行为。

  • DevOps 懂技术的运维? https://learn.lianglianglee.com/%E4%B8%93%E6%A0%8F/%E8%BD%AF%E4%BB%B6%E5%B7%A5%E7%A8%8B%E4%B9%8B%E7%BE%8E/36%20DevOps%E5%B7%A5%E7%A8%8B%E5%B8%88%E5%88%B0%E5%BA%95%E8%A6%81%E5%81%9A%E4%BB%80%E4%B9%88%E4%BA%8B%E6%83%85%EF%BC%9F.md
    DevOps 可以理解为一种开发和运维一起紧密协作的工作方式,从而可以更快更可靠地构建、测试和发布软件。DevOps 的主要原则就是自动化、信息透明可测量、构建协作文化。
    DevOps 工程师,要做的事情就是帮助团队来实践 DevOps 的工作方式。具体可以帮助团队:建立基于持续集成和持续交付工作流程;建立基于日志的监控报警的系统,以及故障响应的流程;构建基于云计算和虚拟化技术的基础设施;形成 DevOps 的文化。DevOps 工程师做的事情,就是帮助团队基于 DevOps 原则来做事,让团队形成紧密协作的工作方式,更快更可靠的构建、测试和发布软件。

01.23

  • Jedis 和 RedisTemplate有什么区别?
    Jedis 是 Redis 的官方 Java 客户端,直接使用 Redis 的命令进行操作。它提供了比较底层的 API,需要手动管理连接、序列化等。RedisTemplate 是 Spring Data Redis 提供的 Redis 客户端,它在 Jedis 的基础上进行了封装,提供了更高层次的抽象和更多的功能。
    如果你在使用 Spring 项目,并且希望更方便地集成 Redis,并提供更高层次的抽象和功能,那么推荐使用 RedisTemplate。如果你对 Redis 的命令较为了解,或者需要更底层的操作,那么 Jedis 可能更适合你。

  • SpringBoot实体类 DO 和 BO

    1. DO(Data Object):通常表示数据库中的数据实体,对应数据库表的结构。它主要用于数据存储和数据库操作,包含与数据库表字段一一对应的属性。DO 类的命名一般以 “DO” 为后缀,例如 UserDOOrderDO。类中通常包含与数据库表字段对应的成员变量、getter 和 setter 方法。它不应包含业务逻辑,主要负责数据的持久化和映射。
    2. BO(Business Object):通常表示业务层的业务实体,主要用于封装业务逻辑。BO 类一般包含与业务逻辑相关的属性和方法,与具体的数据存储形式无关。包含了一些业务逻辑的操作,比如计算、验证等。它不应直接与数据库进行交互,而是通过调用 Service 层或 DAO 层的方法实现数据的获取和存储。
    3. 在 Spring Boot 中,DO和 BO是一些常见的命名规范,用于表示在不同层次中使用的实体类。这些命名规范有助于代码的清晰性和可维护性。在业务逻辑层中,可以通过转换工具或框架进行 DO 和 BO 之间的转换,以完成数据的传递和处理。
      1
      2
      // 利用实例BO对象生成DO对象
      OrderDO orderDO = BeanMapperUtil.map(orderBO, OrderDO.class);
      1
      2
      3
      4
      5
      6
      7
      8
      9
      public static <T> T map(Object sourceObject, Class<T> destObjectclazz) {
      if (sourceObject == null) {
      return null;
      }
      T destObject = BeanUtils.instantiateClass(destObjectclazz);
      // 对象映射: 通过 copyProperties 方法,将源对象的属性值复制到目标对象中
      copyProperties(sourceObject, destObject, null, (String[]) null);
      return destObject;
      }

01.30

App拉新业务,增加白名单功能

02.20

需求:开发媒体上报规则报表的分页查询接口

02.22

  • YY yy = BeanMapperUtil.map(xx, YY.class); 从xx复制属性值而来生成YY类型的实例yy
  • List list = page.getList().stream().map(this::toResponse).collect(Collectors.toList());
    Java中的Stream API 是一种用于处理集合数据的强大工具,在 Java 8 中引入,它允许我们以声明性方式处理数据集合(即你可以描述你想要做什么,而不是描述如何去做),它使得集合操作更加简洁、易读和高效。
    • 创建 Stream:你可以从集合(如 List、Set 等)创建 Stream,或者从数组创建。
      1
      2
      3
      4
      List<String> list = Arrays.asList("a", "b", "c", "d");  
      Stream<String> stream = list.stream();
      // 或者直接从数组创建
      Stream<String> arrayStream = Arrays.stream(new String[]{"a", "b", "c", "d"});
    • 中间操作:是返回新 Stream 的操作,它们可以链接起来形成一个处理链。常见的中间操作有 filter、map、sorted 等。
      1
      2
      3
      4
      5
      6
      List<String> list = Arrays.asList("a", "b", "c", "d");  
      List<String> result = list.stream()
      .filter(s -> s.matches("[a-c]+")) // 过滤出以 a-c 开头的字符串
      .map(String::toUpperCase) // 将字符串转换为大写
      .sorted() // 对字符串进行排序
      .collect(Collectors.toList()); // 收集到新的 List 中
    • 终端操作:结束 Stream 处理并返回结果。常见的终端操作有 forEach、reduce、collect 等。
      1
      2
      3
      4
      5
      6
      7
      8
      9
      List<String> list = Arrays.asList("a", "b", "c", "d");  
      // 使用 forEach 遍历 Stream
      list.stream().forEach(System.out::println);
      // 使用 reduce 对 Stream 中的元素进行归约操作,例如求和
      int sum = list.stream().mapToInt(Integer::parseInt).sum();
      System.out.println(sum); // 输出:6('a'、'b'、'c'、'd' 的 ASCII 码之和)
      // 使用 collect 将 Stream 中的元素收集到新的集合中
      List<String> collectedList = list.stream().collect(Collectors.toList());
      System.out.println(collectedList); // 输出:[a, b, c, d]
    • 并行 Stream:Java Stream API 还支持并行处理,这可以充分利用多核处理器的优势,提高处理速度。要创建一个并行 Stream,你可以使用 parallelStream() 方法。
      1
      2
      3
      4
      List<String> list = Arrays.asList("a", "b", "c", "d");  
      // 使用 parallelStream 创建并行 Stream
      List<String> collectedList = list.parallelStream().collect(Collectors.toList());
      System.out.println(collectedList); // 输出:[a, b, c, d]
  • 在Java中,Stream流和序列化是两个不同的概念,但它们在某些场景下可以相互关联。
    • Stream流是Java 8引入的一个新特性,它允许我们以声明性方式处理数据集合。Stream API提供了一种高效且简洁的方式来处理数据,例如过滤、映射、排序和聚合等操作。Stream流主要关注的是数据的处理和转换,而不是数据的存储或传输。
    • 序列化(Serialization)则是将对象的状态转换为字节流的过程,以便可以将这些字节流持久化保存或通过网络传输到其他位置。序列化允许我们将对象的状态保存起来,然后在需要时恢复对象的状态。Java提供了ObjectOutputStream和ObjectInputStream类来进行对象的序列化和反序列化。
    • 虽然Stream流和序列化是两个不同的概念,但它们在某些情况下可以相互结合使用。例如,当我们需要将一个对象的状态通过网络传输到另一个位置时,我们可以先将对象序列化为字节流,然后使用Stream流来处理和转换这些字节流。在接收端,我们可以使用Stream流来读取字节流,并使用反序列化来恢复对象的状态。

春 招 2024

教育经历

GPA: 3.31(3.34)2020.09 - 2024.07
主修课程:数据结构(90), 计算机网络(84), 数据库(87), 操作系统, C++(94), Java
在校经历:曾获学校与企业奖学金, “三好学生”等荣誉,发表 EI 会议论文一篇。曾加入学院青马工程班学习,担任华工青年志愿者指导中心宣传部副部长,有丰富的志愿活动和学生组织经历。
语言技能:CET-6 4862022.06 CET4:5302021.06
荣誉奖项:五粮液优秀学生奖学金 (2023.10),华南理工大学三等奖学金 (2022.09),广东省第十一届大运会“优秀志愿者” (2022.06)
2022—2023学年度 “三好学生”×,校级“优秀公益组织骨干”,”青马工程”班优秀学员
2021—2022学年度 “三好学生”,“两优两红优秀共青团员”
Publications:
[1] Feng Cai, Jingxu Peng, Peng Zhou, “Current Study on Image Restoration Leveraging CNNs and GANs”, 2024.02.14, 2023 International Conference on Data Science, Advanced Algorithm and Intelligent Computing (DAI 2023)
近年来,图像恢复技术中CNN和GAN应用广泛。DnCNN模型通过残差和批量归一化有效去噪地震图像。SRGAN使用GAN和感知损失函数提高图像质量。Constrained-DnCNN模型进一步优化地震数据解读。CR-SRGAN专注于文物超分辨率和颜色恢复,采用特殊的训练数据集。DCGAN结合CNN和GAN特点,展现出在特征提取和适应数据分布方面的优势,是图像恢复领域的有前景研究方向。 https://www.atlantis-press.com/proceedings/dai-23/125998064

专业技能

Java 后端方向:

  • 熟练掌握 Java 基础,集合等相关知识,了解常见的设计模式。
  • 熟悉 JVM 的垃圾回收机制、类加载机制及 Java 的内存区域。
  • 熟悉 Java 并发编程,掌握JUC中常用的工具类,如 ConcurrentHashMap 等,熟悉多线程,线程池,Java 内存模型。
  • 熟悉 OSI 七层模型和 TCP/ IP 四层体系分层结构,掌握常见网络协议,如 HTTP/ HTTPS、TCP、UDP、DNS等。
  • 熟练使用 MySQL 数据库,熟悉 MySQL 索引、事务、存储引擎、锁机制。
  • 熟悉操作系统的进程通信、死锁、内存管理等知识。了解Linux 常用命令。
  • 熟悉 Redis 数据类型使用场景和内部实现,熟悉持久化和过期淘汰策略,熟悉缓存高并发场景。
  • 熟悉 RabbitMq 的使用,了解消息可靠性的保证、死信队列、延时队列等,了解 Kafka 的基本使用
  • 熟练使用 Spring Boot、Spring、MybatisPlus 等常用框架,熟悉 Spring IOC 、AOP 原理,了解 Nacos 、Zookeeper 等常见组件。
  • 了解 SpringCloud 常用的微服务组件及其使用;如NacosRibbonFeign、HystrixGateway等
  • 数据结构:了解常见数据结构如数组、链表、栈、队列、二叉树等;熟悉常用算法如双指针、递归、排序算法等

实习经历

  • 广州骏伯网络科技有限公司_Java开发实习生_2024年01月~至今_广州

    • 主要工作:
      • 参与用户拉新系统的开发。当用户点击我方投放的广告后,将消息推送到拉新系统的 mq 队列中,我方作为消息的消费者进行消息拦截和转发。通过 Kafka 进行削峰处理,避免 QPS 过高导致的系统负担。
      • 开发白名单功能。在消息转发的过程中,先从本地缓存及 Redis 中查看白名单数据,未命中则调用相关接口拦截非目标用户。在白名单中添加我发的测试手机设备号,从而在投放新的广告后进行联调测试。
      • 参与内部报表平台的开发。根据产品原型图设计数据库表,开发报表数据的分页查询接口。
  • 广东创世科技有限公司__Java开发实习生__2023.03-2023.05__广州
    牙医星球小程序:负责”个人中心”部分接口以及相关数据库字段的设计,实现了在线视频的弹幕功能。
    广东省青少年科技创客大赛系统:参与了该在线编程竞赛平台的学生端功能开发。在已有项目的基础上,编写完善了学生登录、编程题判题等方面的代码。熟悉了团队开发流程以及 git 的日常开发使用。

  • 中国电信股份有限公司东莞分公司__IT开发工程师__2023.07-2023.08__东莞
    参与电信AI知识库的建设,打造电信业务专家水平的对话机器人。负责测试本地知识库,完善知识云文档,以及收集资料供大语言模型进行训练。此外,接触了开源项目,学习了深度学习框架。

项目经历

“校跑跑”校园代拿平台

  • 项目描述:”校跑跑”平台是一款致力于解决大学校园内代拿服务需求的创新型应用,提供方便快捷的外卖、快递、文件等代拿业务,同时确保支付双方的信用和流程的可跟踪。
  • 主要工作:
    • 在任务发布模块,通过产品原型总结出代拿服务任务模型,并使用双值枚举的方式对类型、数量、状态等进行存储。
    • 在任务列表模块,实现了多筛选和多排序条件下的动态分页列表查询。
    • 使用数据库悲观锁,解决同一跑腿任务的超卖订单问题,实现抢单唯一。 
    • 为代拿双方的陌生人服务安全性,对每个状态进行流水记录。流程状态切换中实现了照片上传的可靠性,为了节省服务器带宽,使用腾讯云 COS 直传方案,提供预签名给前端,保障了 COS 的安全性。
    • 在接单前,需要接单人支付押金,实现跑腿人押金的支付、押金退还的事务处理。

高校闲置资源交易系统

  • 系统架构:SpringCloud + SpringBoot + Nacos + Redis + MySQL
  • 项目描述:系统采用微服务架构,旨在为消费者提供一站式商品交易平台。通过细致的业务划分,系统分为用户模块、商品模块、购物车模块与支付模块,确保每个模块服务的高效、可维护、可扩展。
  • 主要工作:
    • 使用 Nacos 实现服务注册,消费者服务通过 Nacos 拉取和订阅服务。
    • 引入 Redis 数据库,优化项目中登录验证码的使用,对需要频繁访问的数据进行缓存。
    • 使用 Gateway 及拦截器实现了登录信息校验,避免各微服务中的代码重复。
    • 通过 Sentinel 完成系统限流熔断功能,防止服务故障后影响整个系统运行。

牛客论坛(-> 高校就业信息分享平台)

  • 项目描述:一个基本功能完整的论坛项目。主要功能有: 基于邮件激活的注册方式,区别登陆状态为游客或已登录用户展示不同界面与功能。支持用户上传头像,实现了发布帖子、评论帖子、发送私信与过滤敏感词等功能,以及点赞,关注与系统通知功能。
    项⽬构建在 Spring Boot + SSM 框架之上,并统⼀的进⾏了状态管理、事务管理、异常处理。
    (项目是一个以就业信息分享为基点的信息分享平台,在平台上学生可进行就业信息或者学习资源的分享,增加学习就业信息的获取渠道,例如,发布帖子,发布招聘信息,学生间可以互相关注,私信来互相分享资源。)
  • 核心功能:
    1、用户模块:使用 Spring Email 辅助发送注册激活邮件,并且使用 Interceptor 拦截器赋予不同类型的用户权限,解决http无状态带来的缺陷问题,保护需登录才能查看的资源。
    2、内容模块:使用 Spring AOP 面向切面编程思想统一记录日志;数据库连接使用 Mybatis 框架来完成,实现SQL语句和代码的分离,解除了SQL语句与代码的耦合。
    3、通知模块:使用 Kafka 消息队列构建异步消息系统,实现点赞关注与通知(系统)间削峰。
  • 项目优化:
    1、引入 Redis 数据库,优化项目中 Session 的使用并利用 Redis 实现点赞和关注功能。
    2、使用 Caffeine + Redis 实现两级缓存,优化了热门帖子的访问。(借用 Jmeter 工具测试吞提升20倍)
    (使用 Spring Security 提高系统的安全性,防CSRF攻击;使用拦截器解决登录状态剧新及频繁刷新问题)
  • 组价描述:
    • 项⽬构建在 Spring Boot + SSM 框架之上,并统⼀的进⾏了状态管理、事务管理、异常处理;
    • 利⽤ Redis 实现了点赞和关注功能,单机可达5000TPS;(对频繁需要访问的数据,如用户基本信息使用Redis作为本地缓存,提高服务器性能。采用事务管理保证数据的正确,采用“先更新数据库,再删除缓存”策略保证数据库与缓存数据的一致性;)
    • 利⽤ Kafka 实现了异步的站内通知,单机可达7000TPS;(在用户被点赞、评论、关注后,放入异步队列,以系统通知的方式推送给用户)
    • 利⽤ ElasticSearch 实现了全⽂搜索功能,可准确匹配搜索结果,并⾼亮显示关键词;
    • 利⽤ Caffeine + Redis 实现了两级缓存,并优化了热⻔帖⼦的访问,单机可达8000QPS。
    • 利⽤ Spring Security 实现了权限控制,实现了多重⻆⾊、URL级别的权限管理;
    • 利⽤ HyperLogLog、Bitmap 分别实现了 UV、 DAU 的统计功能,100万⽤户数据只需*M内存空间;
    • 利⽤ Quartz 实现了任务调度功能,并实现了定时计算帖⼦分数、定时清理垃圾⽂件等功n能;
    • 利⽤ Actuator 对应⽤的 Bean 、缓存、⽇志、路径等多个维度进⾏了监控,并通过⾃定义的端点对数据库连接进⾏了监控。

社团/组织经历

  • 华南理工大学青年志愿者指导中心 宣传部 副部长 2022.04-2023.04
    1.承接中心的摄影任务,组织志愿活动的现场拍摄,负责培养部门成员的基本摄影能力;
    2.协助完成中心公众号推文制作,宣传大运会、广州马拉松、志愿一条街等活动中的志愿服务工作;
    3.负责部门的内建工作,营造良好氛围。

个人陈述/个人优势

  1. 本科就读于华南理工大学网络工程专业,曾获学校与企业奖学金,三好学生等荣誉,以共同第一作者发表 EI 会议论文一篇;(曾加入学院青马工程班学习,担任华工青年志愿者指导中心宣传部副部长,有丰富的志愿活动和学生组织经历;)
  2. 熟悉 Java 开发,能够熟练使用 Spring Boot、Spring、Mybatis 等常用框架,有着坚实的专业知识和实践经验;大学期间,我参与了多个项目或实习,不仅深入了解了软件开发流程,还培养了问题解决的能力和团队协作技巧;
  3. 我对信息科技领域充满热情,对新技术和趋势保持关注,有较强的自我学习能力。希望能在企业数字化转型中,应用技术来推动创新和提高效率。

应聘理由

我对Java后端和软件开发有着坚实的专业知识和实践经验。在大学期间,我参与了多个项目或实习,不仅深入了解了软件开发流程,还培养了问题解决的能力和团队协作技巧。
东莞银行一直以来都在金融科技方面取得了卓越的成就,我渴望加入这个充满活力和创新的团队,共同推动银行业务的数字化转型。我相信自己的技术背景和对金融领域的热情将使我成为东莞银行信息科技团队的有力资产。

  1. 个人优势:
    作为一个经验丰富的Java软件开发者,我在面向对象编程、分布式系统设计以及大规模数据处理方面具备深厚的技术功底。我在之前的项目中,成功地设计和实施了高性能的、可扩展的Java应用程序,这使我对云核心网线Java软件开发的挑战充满信心。我的解决问题的能力以及对新技术的快速学习适应能力,将使我能够在贵公司的项目中迅速融入并取得成绩。
  2. 选择行业:
    我一直对云计算和核心网络领域保持浓厚的兴趣,深知这是当今数字化时代的基础。贵公司在云核心网线方面的领先地位以及对新技术的持续投资,使我对能够参与并推动这个领域的发展充满憧憬。我渴望加入贵公司,通过亲身参与和贡献,不断提升自身技术水平,同时为公司在行业内的创新发展贡献力量。
  3. 商业价值:
    我深刻理解软件开发不仅仅是技术问题,更是在商业环境中为客户提供切实解决方案的过程。我注重将技术与商业需求相结合,通过深入了解客户需求,迅速响应市场变化,为公司创造有价值的产品。我相信我的经验和技能将有助于提高贵公司的竞争力,实现商业和技术的双赢。

个人总结

入学以来,我始终保持着积极向上的态度,希望在各方面都能够取得进步。在这段充实的时光里,我探索了知识的广度和深度,结识了众多志同道合的朋友,培养了坚韧的意志力,以及树立了对未来充满信心的决心。
未来的日子里,我将继续发扬个人优点,脚踏实地,真诚做人,潜心做事,不断地提高自己,为社会作出更大的贡献!

  1. 思想上:我热爱祖国,积极学习党的理论,加入了学院青马工程班学习,努力成为一名合格的入党积极分子。我热心公益服务,在校青志宣传部担任副部长,为志愿精神在校园中的传递而努力。也在各项活动中积极参与志愿者工作,帮助有需要的人。
  2. 学习上:我能够比较自觉和广泛地学习专业知识,勤奋刻苦,在各科目中都取得了不错的成绩,获得校级奖学金两次。我参与到前沿的科研项目中,以共同第一作者发表一篇国际会议论文。
  3. 课余生活中,我也积极参与了各种文体活动。我乐于与人沟通,把握团队合作的机会,在学习与人协作的过程中,我的沟通和协调能力得到了不断加强。我也积极参与到课外实践中,完成了三段企业实习。这不仅锻炼了我的专业技能,更在团队协作的过程中,培养了实际解决问题的能力。同时,我也在不断思考未来的职业发展和目标,努力打下一个好的基础。

2024春招

  • 乐刻运动
    • 一面 3.17
      java锁,,rentanlock
      HashMap如何处理10w条插入?为什么大小是2的指数?一开始就要创建大容量;方便在计算插入下标时与数组大小或运算
      jvm组成??老年代,GC,回收期??
      new一个对象的过程、、
      mq消息确认?事务本地回滚??channel
      redis多路查询为什么这么快??过期了怎么办?手动设置一个迭代中不过期??
      mysql查询优化
      面试官:把基础搞好,在实践中运用
  • 卓望数码
    • 一面 3.18
      多态、、线程创建方法,区别,,工厂模式/抽象工厂模式??
      ACID、、D是永久性!!索引,最左匹配?
      IO,如何读取一个不知道类型的文件A数据到文件B中?buffer,inputstream??
      sentinel流控,,
      远程debug??IntelliJ IDEA 中”Run” 菜单”Edit Configurations”配置远程调试,填写调试服务器的主机名/IP 和端口号,然后启动远程调试会话,设置断点和调试;
      面试官:动手能力不太行,基础知识不深入
  • 小鹏汽车
    • 一面 3.20
      最有收获的项目、、项目几个模块、、
      抢单悲观锁实现、、
      支付怎么做的、、微信支付??
      实习做了什么?拉新具体做了啥?
      kafka消息顺序?
      mysql隔离等级,,
      jvm内存区域几个部分?
      springboot特点?
      spring bean依赖项目还能启动吗?
      OSI七层、、5.会话层、、6. 表示层
  • 新凯来
    • 技术面 3.21(通过)
      实习工作、、没讲清楚
      收获了什么、、mq的使用,kafka技术选型
      项目复杂度、、创业项目,量级小
      lc1023
    • 性格测评重测:不够积极
  • 4399游戏
    • 一面 3.21
      具体怎么熟悉了团队开发流程、、
      实习工作、、消息拦截逻辑,消息参数与枚举值比对,equals??
      非法字检查?微信小程序接口、前缀树?? BST、、
      网络知识学习:实现弹幕功能用了websocket,还可以用什么别的方法?TCP,Redis??
      TCP编程?socket聊天室??
      对这份工作有什么期待、、
  • TPlink联洲
    • 机考算法:1、最长回文子串 java11?? 2、lc1114按序打印
    • 一面 3.21
      TCP/UDP,MySQL索引,synorized与lock,实现hashmap、、尾插法的好处,spring设计模式,动态代理、、jdk,cglib
      实习:如何拦截
      项目:本地缓存与Redis不同步
      有没有offer!?
    • 二面 3.26
      实习、、拦截,可以用spring的设计模式或者技术吗??策略模式,aop非业务,,
      白名单过期时间,量很大用布隆过滤器的缺点、、
      kafak架构,如何实现高吞吐量,前后吞吐量2000qps->200kbs
      项目、、不感兴趣,要说设计还是亮点?
      retranlock,可重入锁,,
      mysql、、隔离等级,索引优化,查看索引的使用 explain?? mvcc??
    • 终面 3.27
      论文、考研、父母工作、数学挂科、实习、、
      项目:腾讯云 COS 部署,校内创业团队人数(产品经理 2、前端1~2、后端核心2迭代有空来、、),使用springcloud框架为什么还用nacos,sentinel??
    • 终面-已进入人才库 4.25
  • 广州农商银行(英才计划)
    • 面试 3.27 放弃
  • 经纬恒润
    • 面试 3.29
      实习、、kafka选型、其他mq了解吗、白名单缓存一致性
      反射、泛型、mysql索引失效,in??设计模式
      事务的使用、事务传播、、spring拦截器,过滤器、、gateway路由,断言
      智力:八个球,其中一个比较重,其余质量相同,如何用天平秤两次找出较重的球?
  • 多益网络(-> CPP游戏引擎研发)
    • 面试 4.3
      最有挑战的项目??难点??
      团队有几个人,工作流程,如何协调(接口文档?)和前端沟通??
      java的优缺点,垃圾回收算法,链表与数组,浅拷贝与深拷贝
      线程安全,如何保证(乐观锁,悲观锁),线程与进程
      静态绑定,动态绑定,编译时多态,运行时多态
      面向对象的三个特点、、为什么需要继承和多态??
      Redis作缓存的缺点与解决方法
      介绍框架:spring、springboot、mybatis
      如何学习的?看视频+chatgpt发文、、有没有使用搜索引擎?google、stackflow??
      玩什么游戏?我的世界,根据三维坐标保存每一个格子的类型,那么要用三维数组表示整个空间,如何优化存储方式??百度实景地图的原理??
    • hr面 4.10
      薪资?20k,18k,如何得出这个薪资的?
      什么时候来实习?实习->试用->正式
      秋招,春招offer情况,考研,每段实习时间都不长
      了解岗位要求,转cpp吗?入门语言
      了解公司业务吗?玩网页游戏少
      工作时长996+周末半天,学习时间
      籍贯,家庭,接受广东外工作地点,对象
      父母怎么看待你找工作?如何评价父亲?
    • 未录用
  • 招商银行·招银网络科技
    • 测评 北森测评
      1. 语言理解:直接看题,提炼几条关键逻辑,再看题目
      2. 图表分析:先看题目,再看图
      3. 图形推理:找规律、、叠加,合并消除,各类图形总数、、
      4. 性格测评:最符合/最不符合
    • 机考 4.1
    • 一面 4.7
      String比较可以用 == 吗?Arraylist.remove() 报什么错?HashMap键可以null?
      jvm内存区域、如何写一个程序让方法区oom
      线程安全问题、synorized和reentrantlock的本质区别,底层实现??cas存在什么问题??
      数据库acid,索引是什么
      tcp三次握手的本质、http的组成,http头有什么部分
      十万个数取最大10个、二分排序
    • 综面 4.16
      技术:项目,商城redis的使用,redis客户端??
      hr:挂科原因,,最大的缺点??实习长短、项目时间重合,实习目的(项目缺人/应届生招聘),如何评审,上线时间紧张怎么办?
      考研!!没考,找工作确实比较晚,技术栈:七月份开始学,秋招技术不足,春招积累面试经验后通过
      反问:招聘流程,工作时长
    • 未通过
  • 中国平安·平安寿险
    • 面试 4.8
      项目介绍很烂。。同一时间代拿任务发布量级很大的情况下怎么动态分页查询??
      实习:拦截具体工作是什么?媒体->拉新,调接口??拉新->APP后台,发http请求,rpc??整个拦截流程,流stream??kafka重复消费,redis标识符,消息量很大??报表具体有什么字段,如何设置的??
  • 深圳农商银行(信息技术岗)
    • 面试 4.8
      实习:kafka如何知道通道、、git怎么用分支的
      项目:如何过滤非法字?乐观、悲观锁,锁升级,MySQL事务,@transaction失效?为什么用nacos?限流和熔断??项目部署,linux文件中查找??
      hr:为什么放弃考研?家庭情况?为什么匹配银行岗位?女朋友?
    • offer 4.23
  • 万得wind
    • 面试 4.9
      kafka怎么实现限流的
      任务发布到发布客户端,重复刷新查mysql??一百个人抢单,请求直接到数据库吗??nacos如何实现服务注册,
      多态,接口与抽象类,有用过吗??设计模式,哪里体现了多态,单例模式,,
  • TPlink普联
    • 面试 4.10
      校跑跑:与实习时间重叠(非全职,有需求就去),redis非重复支付,抢单为什么不用缓存层?商城redis使用
      实习:kafka怎么限流,,考虑不使用kafka优化??延时双删??
      synchronized的底层实现,jvm堆结构、gc、类加载机制,mysql的b+树索引结构+查询优化,mysql优化(数据库结构+具体使用中的sql优化)
    • 二面 4.18
      秋招情况,offer情况,实习,项目
    • 座谈 5.14
    • offer 5.15 17k
  • 58同城
    • 面试 4.11
      动态代理,线程状态、生命周期,线程池,Reentrantlock底层,信号?
      redis数据类型,zset有什么应用
      GC算法,G1和CMS的区别,内存泄露和内存溢出
      springboot特点,自己写一个starter, MyBatis中#和$的区别
      ConcurrentHashMap1.7,ArrayList扩容
      MySQL日志,二阶段提交
      面试官一直试图找到亮点、、
  • Shopee虾皮
    • 面试 4.13
      拉新系统框架,难点,广告重复点击
      kafka架构,消息重复消费,redis删除策略
      lc239
  • 北森云计算(-> C# 北京/成都)
    • 面试 4.17
      白名单作用,key-value怎么设置??
      kafka消费模式??使用场景
      redis基本类型,使用场景
      CPU 飙高系统反应慢怎么排查?
      互斥锁,线程池、删除策略?
      接口与抽象类,值和引用,Array和ArrayList
      Ioc容器及使用,设计模式,工厂模式的使用
      学习路线,职业规划
  • Boss直聘
    • 面试 4.18
      拉新系统解决痛点,拦截逻辑??kafka重复消费,同时重复消费??本地缓存caffeine
      跑腿抢单悲观锁,为什么不用乐观锁?如何优化,加缓存层??
      互斥锁选型、reentranlock底层原理?怎么实现公平锁??
      平时如何学习?博客,技术论坛?
      微服务调用,轮询??
  • 阿里-银泰
    • 面试 4.19
      kafka组成,选型,消息丢失?redis为什么这么快?
      sentinel服务治理,使用场景,自己设计一个组件统计各个微服务的qps
      mysql索引建立与使用,关系型与非关系型,索引中的节点上是什么??
  • 美团·优选
    • 面试 4.22
      kafka消息堆积??白名单二级缓存解决了什么问题?本地、redis、数据库同步??
      报表是在查出a表后分页再联合查b表,还是a,b表联合查询后再分页?
      抢单乐观锁实现,reentrantlock底层,可重入??集群部署下分布式事务??退款事务,先退款先流水??
      hashset的value存了什么?线程池加入线程使用空闲线程?联合索引怎么创建的?浏览器输入url的流程用到什么协议?操作系统与java线程的联系?
  • 比亚迪
    • 面试 4.24 放弃
  • 途牛旅游网
    • 面试 4.24
      jvm类加载过程,双亲加载机制
      hashmap原理,大小为2^n:hash值与数组大小与运算
      redis缓存击穿,缓存穿透,分布式锁
      SpringAOP、spring循环依赖、事务传播,sql优化
  • 宇量晟
    • 背景:华为+国资??半导体
    • 面试 4.26
      岗位:在线系统用c/cpp,离线系统用python,需要转语言
      c指针,智能指针,内存管理,多线程与多进程,二叉树/红黑树
      算法:判断点在三角形内,句子翻转单词不翻转
    • 测评 4.29
      要前后一致,然后要表现的乐观积极,团队合作,不能选带领导意愿的选项?不一定要讨好型,也可以硬气一点??
    • 综面 5.18 放弃
  • 灵动科技
    • AI赛道创业公司?
      • 初创公司:与大厂精细化的分工相比,初创公司开发岗位需要承担的工作可能更加复杂,缺少完善的培养机制和技术积累;需要更多的自我学习和解决问题的能力,锻炼的机会多,发展更灵活,有创新的机会,与公司共同成长。。
      • AI思考:外部(未来趋势,AI一方面在各行业普及,一方面使用门槛降低。。虽然算法研究岗位少,但是也存在很多AI应用岗位),内部(大学期间对AI热情很高,到老师实验室学习(自学经典AI课程),在东莞电信实习也参与了对话机器人项目(了解开源对话大模型),参加夏令营研究项目。。)
      • 岗位认识:这是一家AISaaS公司,专注于打造全球商业市场的通用人工智能应用软件平台,我们正在深圳寻求一位热情目具有技术潜力的应届毕业生后端工程师,作为后端工程师,您将使用大模型和先进技术,开发和实施可扩展和稳健的软件解决方案。一方面,我对AI算法和大模型有基本了解,一方面我掌握Java开发、、
      • 产品了解:GPT-4驱动的翻译软件??出海业务?利用人工智能进行翻译、本地化和全球扩张
      • 反问:作为创业公司,为什么不请经验丰富的工程师,而是应届生?我的情况:六月才能实习,已签三方
    • 初面 4.30
      美团毛利润计算:支出,收入??
      明确:对您公司的岗位有强烈的倾向,收到offer可以立刻入职(非实习),对薪资要求不高(15k)
  • 中国平安·平安金服
    • 初面 4.30
      最左索引底层原理(聚集索引??),b+索引与哈希索引,原子类??,https,对称加密
      感觉你面试经验挺多的??
  • realme真我
    • 面试 5.7
      重载与重写,http头内容,死锁,缓存类型和理解
      redis数据类型,集群部署,数据一致性,场景:查一个字符串类型数据为什么花了5s?
      数据库索引失效情况
      毕设做的什么。。
  • 交通银行
    • 面试 5.9
      项目,spring,依赖注入,单例,手撕:链表中是否有环?
  • 美的·软件工程院
    • 一面 5.10
      logn曲线,快速排序为什么不稳定
      可重复读底层原理,mvcc,Redis分布式锁
    • 二面 5.10
      三个优点,举例勤奋
    • hr面 5.11
      部门(软件工程院办公系统)业务(办公系统)双休(保证??)
      基本工资(12^12+12^3^绩效)五险一金(比例??)餐补(每日20)房补(租房900/月)
    • offer 5.14
    • 入职 7.8
  • 东方财富
    • 一面 5.13
      缓存与数据库的一致性,微服务架构,服务治理

总结

  • 投递:官网(使用内推码),Boss直聘(快速筛选简历),宣讲(甚至直通面试)
  • 前期:中厂刷经验,了解面试流程以及各部分常考题,掌握八股,熟悉个人实习和项目描述
  • 后期:大厂过不去,小厂捡漏,虽然八股基本覆盖还需深挖项目和实习中的每一个步骤,了解场景解决方法和项目优化方案
  • 改进:前期浪费了很多中小厂机会,白给厂没把握住(挂科+终面出错),大厂能力不足,最好一定要参加秋招。。。

签约

  • 流程:签三方(- 毁约)- 入职报道 - 签劳动协议(三方失效)- 试用期 - 转正
  • 文件
    • 毕业生推荐表:一份原件,毁约后要取回
    • 毕业生就业协议:即三方,一次毁约机会
    • 两方协议:约束小
  • 违约(一次机会)
    • 提交违约申请书:本人签字,无需提供原单位解约函,每月的10号、20号、30号之前交给辅导员
    • 违约申请通过:名单见“就业在线”https://jyzx.scut.edu.cn/
    • 原单位解约流程:1、手写并签名书面违约申请。2、违约金。获得原单位解约函。
    • 提交“新单位盖章的违约申请表”和“原单位解约函”给学校,审核通过后生成新就业协议书,与新单位签订三方。
  • 应届毕业生
    • 择业期:应届生身份通常可以保留两年。
    • 失效:一旦就业或缴纳社保,应届毕业生身份即失效。
    • 好处:银行、国企校园招聘优先,落户政策好.
  • 户口
    • 户口未迁来学校的毕业生,如户口需迁往工作单位的按户籍所在地公安机关的管理规定办理。
    • 不同的城市和地区有不同的落户政策,对于高校研究生毕业后的落户条件、流程和所需材料都可能有不同的规定。一些城市可能会对应届毕业生提供更为宽松的落户政策。
    • 户口保留:如果暂时没有找到合适的工作或者希望继续深造,可以选择将户口保留在原学校或原籍。在保留期间,需要注意保管好自己的户口页和相关证明材料,以备将来使用。
  • 毕业去向登记与档案信息填报
    • 档案整理:家庭情况、学习成绩、政治思想表现、身体状况等
    • 档案转递:按“广东大学生就业创业”小程序填写地址转递,一般于2024年7月中下旬开始寄送
    • 通过“广东大学生就业创业”小程序签订就业协议书且回传审核完成的,就业信息同步至“毕业去向登记”和“初始档案信息”模块
  • 社保公积金
    • 五险:即社保,养老、医疗、生育、工伤、失业保险,每月缴,公司缴大头个人缴小头,通常按国家及当地标准缴纳
    • 一金:即住房公积金,按 “个人月薪/当地平均居民收入” 为缴纳基数
    • 到手工资 = 税前年包 - 税款 - 五险一金(个人缴纳部分)
      内容 个人缴纳比例 单位缴纳比例 作用 特点
      养老保险 8% 20% 累计交满15年,退休后终身享受养老金 能中断没有影响
      医疗保险 2% 10% 医疗报销50%以上退休后享受医保待遇特点 中断3个月以上失效
      生育保险 0% 0.8% 生育后资料给单位,报销相关费用 生育津贴个人不能微纳
      工伤保险 0% 0.2~1.4% 工伤时获得医疗费用,保工作期间和下班途中 工伤保险个人不能缴纳
      失业保险 1% 2% 缴费满1年,失业后按照缴费年限领取失业保险金,主动辞职的不可以领取 失业保险个人不能缴纳
      住房公积金 5~12% 5~12% 租房、买房、装修房都可使用,特别是贷款买房利率低 可断缴(影响贷款),可提取/异地转移

面 逝 | 个人

个人

  • 自我介绍
    1、面试官您好,我是蔡枫,本科就读于华南理工大学计算机学院
    2、在校期间,我学习 Java 开发技术与相关组件,也跟着学校的创业团队的开发了一些项目
    3、有几段实习经历,让我对团队中的开发流程有了一定的了解,并且也参与到了实际业务代码的编写
    4、我与您公司岗位比较匹配:中台数据转发、、银行数字化转型、、
    5、我的求职动机:挑战性,成长,机会,,自己清晰的职业规划,表示愿意长期从事这个岗位,,

  • 自我推荐。
    我觉得我对所从事的每一个项目都很努力、负责、勤勉。我在努力成为一个能够不惧解决困难和能够持续学习的人。

  • 校园经历?

    • 大学里最大的收获:在母校的关怀中茁壮成长,敢于尝试,从课内学习、科研、项目经验都积累了,,(因为学校鼓励我们敢于尝试,又会包容我们犯错,激励我们成长)
    • 重来一次哪方面可以做的更好:多融入社会,多尝试新东西,把技能学的更加深入、、
    • 挂科:确实挂了数学,主要原因是刚入学没有适应大学的这种学习模式(上课没学进去,课后也没补上来,或者说不知道如何自学)后面的课程我虽然有的在课上没学到什么,但是课后会主动地通过网络课程、技术网站等方式增进了解,也是增强了信息查找和自我学习的能力、、
    • 参加了什么比赛,实验室项目?
      参加过学院的acm竞赛,,到老师的实验室学习过,参与过一些科研学习的项目,发布过一篇论文。我积极参与实践,有一些项目开发经验,有三段实习经历。。。
    • 你的论文?
      我参加了一个“深度学习-图像处理”项目。五周的时间里,通过导师的授课讲解与指导,我拓展了该领域的专业知识,接着着手阅读文献和动手做实验。之后,我便与小组成员紧密合作,从主题确定,到文献的深入研读,完成论文写作,最终也发表了一篇论文。主要是希望接触学习人工智能这一块的知识(在学校没有机会和资源,,,)
    • 说一下你的社团活动?
      我在华工青年志愿者指导中心宣传部担任了为期一年的(副)部长,这将我的摄影能力和对志愿服务的热情结合起来。我多次承担志愿活动现场的拍摄任务,用相机展现志愿者们的奉献精神,通过青志公众号推文等形式在校园中传播志愿精神。
  • 你对本公司的情况了解吗?为什么选择我们公司?
    1、对公司的了解:、、(行业的前景+公司的规模+公司在行业中的地位+特色产品)
    2、自己跟岗位的匹配:、、(结合岗位要求+自我经历)
    3、符合自己的职业规划:、、

  • 了解 Java 后端开发这个行业岗位吗?是否符合期许?
    我认为 Java 后端开发是一个非常有挑战性的领域,开发能够为人们的生活带来便利的软件,对我来说是有趣的。在互联网企业应用广泛,有活力,发展前景好??
    Java 作为一门强大而灵活的编程语言,具有广泛的应用领域,尤其在企业级应用开发中表现出色。
    我在大学期间学习和做项目的过程中接触到了Java编程,并且在后来的时间加强了对相关技术的学习。我期望能够在一个具有挑战性、创新性和学习机会的团队中工作,我也希望自己能成为一个能够解决困难和不断学习的人。Java开发是一个不错的领域。

  • “想到一个什么样的平台发展?”
    我希望能够在一个具有挑战性的环境中工作,可以让我不断学习和提升自己。
    我注意到贵公司在(行业/领域)取得了显著的成就,我相信我的Java开发技能可以在这里发挥出色的作用,我认为通过在这样一个领先的平台上工作,我将有机会为公司的发展和创新做出贡献,与公司一起成长。

  • “对于第一份工作,你更看重什么?”

    1. 技术挑战和学习机会: 对技术挑战有热情,渴望学习新技术。希望这份工作能够提供丰富的项目经验,快速成长、、
    2. 导师与团队: 当然我希望积累团队协作、项目开发的经历,与不同的人合作,分享知识并共同解决问题。对我的职业生涯进行规划、、
    3. 公司文化和价值观: 强调你对公司文化和价值观的认同。你可以说你更看重一份工作能够与你的价值观相符,让你在一个积极、支持性的工作环境中工作。
  • 认为怎样是好的开发工程师?
    1.技术深度和广度:了解Java语言的方方面面,包括基础语法、多线程、IO等,熟悉常用的框架和工具。以及其他相关技术
    2.问题解决能力:具有良好的问题解决能力。能够独立分析问题、理解需求、提出解决方案,并快速有效地实现。
    3.代码质量:写出高质量的、可维护的代码。注重代码规范、模块化和可测试性。
    4.团队协作:良好的团队协作能力是软件开发中至关重要的一环。有效沟通、分享知识,并能够与团队成员协作解决问题。
    5.持续学习:技术日新月异,一个好的开发者应该保持持续学习的习惯,关注新技术的发展,随时准备适应行业的变化。
    6.创新思维:善于提出新的、创造性的解决方案,对于提升产品和团队的效率具有积极的作用。
    7.用户导向:理解产品和项目的业务需求,站在用户的角度思考问题,以用户体验为中心。


实习(骏伯)

  • 广州骏伯网络?
    使命:让移动互联网营销简单高效; 价值观:创造价值,分享价值; 愿景:做最受信赖的移动互联网营销服务商;
    产品研发中心 - 应用开发部-基础建设组 - Java开发实习生 - 2024.1.8 ~ 4.12

  • 介绍一下实习经历、、

    • 岗位职责:互联网营销行业公司,主要接触了公司核心的拉新系统的迭代开发,另外独立开发了公司内部的报表平台的接口
    • 一个项目:其中拉新系统让我印象深刻、、
      • 项目是什么:拉新指的是给例如 APP 拉取新的用户,实现方法是 我发根据 APP 的要求投放广告到如 抖音 等媒体平台,那么就会产生广告的曝光、用户点击等数据消息,需要转发给 APP 端。我方的拉新系统就在消息转发过程中,进行一个消息拦截的工作。具体来说,消息会先发送到我方拉新系统的mq,一方面进行了kbs的削峰,然后根据需求和策略进行拦截处理,最后才转发给 APP,,
      • 简化版(非技术介绍):我方投放广告,接收“上游”的数据,感觉业务逻辑拦截以及一系列处理后,转发到“下游”;一方面进行了前后行为解耦,一方面削峰处理,为客户提供合适的数据;
      • 为何这样做:这样,就给企业提供了符合要求的消息数据,我方获得收益
      • 负责的工作:就是参与拉新系统的迭代开发,我负责数据拦截这一部分,具体来说实现了白名单功能,,
      • 有什么成果:,,实现联调,,节约成本
      • 最大的收获:
      • 最大的成果:
      • 重来会改变:
  • 拦截到底做了什么??

    • 通过 stream() 调用一系列方法:
      • 参数前置处理(initPreValidate)(一些通用逻辑,比如点击数据ID、时间字段的初始化)。
      • 参数校验(isValid)(校验点击数据是否有效,如果无效则不发给客户,比如媒体可能会将用户曝光数据发过来,此时不需要发给客户)。
      • 参数后置处理(initPostValidate)(一些带有复杂业务逻辑的处理,比如RTA拦截)。
      • 将校验通过的点击数据发给客户(transferToCustomer),http请求。
      • 保存数据到数据库/缓存(save)。
    • 内部处理逻辑(在initPostValidate部分,进行RTA拦截)
      • 拦截指什么:设置状态为 NON_CLICK_NOT_TRANSFER_TO_CUSTOMER,后续代码流程识别出此状态后,不会将该数据发给客户。
      • 拦截逻辑:
        • 校验点击数据是否有效,如果无效则不发给客户,比如媒体可能会将用户曝光数据发过来,此时不需要发给客户
        • RTA拦截:对于某些客户,我方在发送点击数据给对方时,需要调用客户的接口进行确认,判断当前用户是否是客户的目标用户,如果不是,则不发送点击数据给客户。则设置状态RTA_EXCLUDED,后续代码流程识别出此状态后,不会将该数据发给客户。
        • 走RTA拦截,有一些前置条件,包括:
          1、业务方指定了客户点击监测链接ID为RTA链接(客户的点击监测链接是客户提供过来用于接收我方点击数据的接口)(RTA链接是一个标记,标识当前点击数据发给客户前,要先调用客户的RTA接口过滤一道,最终只发送客户需要的用户)
          2、如果当前数据类型为曝光,且曝光总数与点击总数的比例小于requestRate(默认为4:1)
          对于满足前置条件的数据,则发起RTA请求,将结果记录下来(结果指的是客户是否接受当前用户,即是否参竞-isParticipated),用于业务方后续使用
        • 白名单:为实现整个转发过程的联调,不拦截内部测试设备号(可能遭到rta拦截),即使客户不接受当前用户(内部测试手机),不设置状态RTA_EXCLUDED,此时后续流程会正常将该数据发给客户
    • 可以用spring的设计模式或者技术吗???
      • 策略模式,
      • aop非业务,,暴露接口给监控系统
  • kafka 架构?kafka 如何实现高性能,高吞吐量?kafka 消费模式?如何不消费重复数据,顺序消费数据?如何不丢失数据?kafka内部是怎么实现限流的?、、、

  • 你们项目中为什么要用消息队列? MQ 的作用是什么?

    • 【异步】拉新系统接收媒体数据存放于 kafka,再消费即拦截处理后才转发,涉及io、阻断、调用客户接口、数据库、、
    • 【削峰】接口接收2000qps的请求,以 200kbs 消费 mq中的数据,解决系统前后处理能力不一致的问题
  • 不使用kafka,如何优化消息前后处理速度不一致的问题?

    1. 异步处理:将消息的后续处理逻辑设计成异步执行,可以使用 Java 中的线程池或者异步任务(CompletableFuture)来实现。
      • 使用线程池:用ThreadPoolExecutor或者ExecutorService来创建线程池,然后将需要异步执行的任务提交到线程池中。线程池会自动管理线程的创建、执行和销毁,可以有效地减少线程的创建和销毁开销。
        1
        2
        3
        4
        5
        ExecutorService executor = Executors.newFixedThreadPool(10); // 创建固定大小的线程池
        executor.submit(() -> {
        // 异步执行的操作,例如IO操作、数据库操作等
        });
        executor.shutdown(); // 关闭线程池
      • 使用异步框架:有些框架提供了异步执行的支持,例如Spring框架的@Async注解。通过在方法上添加@Async注解,可以将方法变成异步执行的方法。
        1
        2
        3
        4
        5
        6
        7
        @Service
        public class MyService {
        @Async
        public void asyncMethod() {
        // 异步执行的操作
        }
        }
    2. 批量处理:将一批消息聚合在一起批量处理,而不是单个消息逐一处理。这样可以减少处理的次数和开销。
    3. 并发处理:考虑使用多线程或者并发框架来提高处理效率。注意处理任务的并发性要合理控制,避免因并发导致的资源竞争和数据一致性问题。
  • 白名单功能如何实现?

    • 白名单使用逻辑:1、先查看是否在白名单中(认为是目标用户)2、若不在白名单中,再调用户提供的接口进行RTA拦截(目标用户判断)
    • 如何使用:直接查本地缓存caffeine的白名单,因此,必须保证本地缓存与Redis数据的一致性!!(事实上有20min延误)
    • 白名单在Redis中的保存形式:一个 Set
    • 白名单如何维护:需要新增时(上线前)手动Postman调接口,使用 SADD 命令向 Set 中添加一个或多个成员(事实上在数据库中保存了完整的精准的数据,需要时从数据库中同步)
      1
      SADD whitelist member1 member2 member3  # 白名单的 key-value
    • Redis 数据何时过期?可以调用 Redis 的 EXPIRE 命令为单个键(Key)或整个 Set 设置过期时间,也可以通过 SETEX 命令一次性设置键的值和过期时间以确保设置值和过期时间的原子性。
      1
      2
      EXPIRE myset 60         # 设置 myset 整个 Set 结构的过期时间为 60 秒
      EXPIRE myset:value1 60 # 设置 myset 中的 value1 元素的过期时间为 60 秒
    • Redis-MySQL-caffeine数据一致:Redis 提供了键过期时的通知功能,可以通过配置 Redis 的 notify-keyspace-events 参数来开启过期事件通知。在 Redis 配置文件中设置 notify-keyspace-events Ex,表示开启键过期事件通知。
      设置redis数据过期时长为20分钟,到期后,会触发数据库查询,同时会将数据加载到本地缓存中,用于接下来一个20分钟;
  • 为什么需要Redis+caffeine二级缓存?

    1. 关系数据库(Mysql)数据最终存储在磁盘上,每次读取会因为磁盘本身的IO影响读取速度,所以考虑Redis这种内存缓存。
    2. 内存缓存(Redis)确实能够很大程度的提高查询速度,但也许避免同一查询大量的并发查询,也会有明显的网络IO上的消耗。
    3. 那我们针对这种查询非常频繁的数据(热点key),可以考虑存到应用内缓存如caffeine,当应用内缓存有符合条件的数据时,就可以直接使用而不用通过网络到redis中去获取;另外我个人认为,可能对于一些“常规业务无关”的数据,也可以放在本地缓存,避免对Redis查询、、
  • 本地缓存与Redis不同步、、

    1. 延时双删:参考redis与mysql缓存一直方法,先删除 Redis存数,再更新 MySql,延迟几百毫秒除 Redis存数据,这样就算在更新 MySql时,有其他线程读了Mysql,把老数据读到了 Redis中,那么也会被制除掉,从而把数据保持一致
    2. 定时刷新:设置redis数据过期时长为20分钟,到期后,会触发数据库查询,同时会将数据加载到本地缓存中,用于接下来一个20分钟
    3. 异步更新:在应用中实现异步更新机制,当 Redis 中的数据发生变化时,异步地更新 Caffeine 本地缓存中的数据。可以使用消息队列或者异步任务来处理更新操作,确保更新操作不影响应用的正常运行。
    4. 手动更新:更新 Redis 缓存后,由业务代码手动触发本地缓存的更新操作。
  • 如何保持Redis与数据库一致?

    • 延时双删:对于读数据,我会选择旁路缓存策略,如果 cache 不命中,会从 db 加载数据到 cache。对于写数据,我会选择更新 db 后,再删除缓存。
    • 针对删除缓存异常的情况,我还会对 key 设置过期时间兜底,只要过期时间一到,过期的 key 就会被删除了。
    • 除此之外,还有两种方式应对删除缓存失败的情况,都是异步操作:
      • 消息队列:重试缓存的删除
        我们可以引入消息队列,将第二个操作(删除缓存)要操作的数据加入到消息队列,由消费者来操作数据。
        如果应用删除缓存失败,可以从消息队列中重新读取数据,然后再次删除缓存,这个就是重试机制。当然,如果重试超过的一定次数,还是没有成功,我们就需要向业务层发送报错信息了。如果删除缓存成功,就要把数据从消息队列中移除,避免重复操作,否则就继续重试。
      • Canal+MQ:订阅 MySQL binlog再操作缓存
        订阅 MySQL binlog,再操作缓存「先更新数据库,再删缓存」的策略的第一步是更新数据库,那么更新数据库成功,就会产生一条变更日志,记录在 binlog 里。
        于是我们就可以通过订阅 binlog 日志,拿到具体要操作的数据,然后再执行缓存删除,阿里巴巴开源的 Canal 中间件就是基于这个实现的。
        Canal 模拟 MySQL 主从复制的交互协议,把自己伪装成一个 MySQL 的从节点,向 MySQL 主节点发送 dump 请求,MySQL 收到请求后,就会开始推送 Binlog 给 Canal,Canal 解析 Binlog 字节流之后,转换为便于读取的结构化数据,供下游程序订阅使用。
  • 报表平台的开发流程?

    • 根据产品原型图设计数据库表,主键自增,,
    • 使用junboGenerator插件生成表结构对应的MyBatis代码(DO, BO, Mapper文件),用于操作数据库。
    • 实现分页功能,大致步骤包括:
      ①【newenergy-api】定义XXXApi接口及page方法
      ②【newenergybg-service】定义XXXService接口及page方法
      ③【newenergybg-service】定义XXXApiImpl类(Controller),实现XXXApi接口及page方法,通过调用XXXService完成分页查询数据的操作
      ④【newenergybg-service】定义XXXServiceImpl类,实现XXXService接口及page方法,通过MyBatis分页插件及XXXMapper的查询方法,完成分页查询。
      ⑤【newenergy-api】发布XXXApi:修改版本号(xx.snapshot),发布新的jar包(包含新的XXXApi接口)到 Maven 仓库;
      ⑥【newenergy-web-admin】引入最新的XXXApi:更新maven,依赖新版本jar包,以使用新的XXXApi接口
      ⑦【newenergy-web-admin】定义XXXWebApi类及page方法(Controller),通过XXXApi的page方法,完成分页查询。
    • SOA架构,服务关系:
      【newenergy-api】:定义接口信息,同时被【newenergybg-service】和【newenergy-web-admin】依赖
      【newenergybg-service】是【newenergy-api】的实现,实际的服务端;
      【newenergy-web-admin】接收前端请求(防止暴露实际服务端),从 maven 获取 jar 包反射获取接口和实体类信息。
      实际请求发送流程为:前端客户端程序发起请求 ->【newenergy-web-admin】XXXWebApi.page -> 【newenergybg-service】XXXApiImpl.page -> 【newenergybg-service】XXXServiceImpl.page -> 【newenergybg-service】 XXXMapper.selectByExample
    • 参数信息:
      Api方法中:XXXRequest、XXXResponse,在【newenergy-web-admin】XXXWebApi.page方法上加了 @JunboResponse,,,
      分页查询entity类:由于需要返回的数据是由多表查询而来,任一 BO 或 DO 都没法涵盖所有属性字段。所以新增一个 类 ConvCallbackRuleDailyReportPageItem,要在 mapper.xml 里修改 resultMap 中查询数据与返回值的对应关系,在后续数据转换的过程中主要前后类的字段的对应。
    • 集成测试:本地启动newenergybg-service服务和newenergy-web-admin服务,通过postman调用接口进行测试,期望结果是,查出来的结果与数据库中的数据一致。
    • 协同开发:git
  • 分页查询细节、、

    • 先到 “a表” 获取第i到第i+size行的“广告报表数据”(广告id,报表数据类型,时间,,),再根据“广告id”到 “b表” 关联查询该广告的详细信息(客户方,创建时间,,)
    • SQL:select * from a left join b on a.id = b.id limit 9, 10 where a.id < 10
      1. 执行 FROM 和 LEFT JOIN 操作,将表 a 和表 b 进行关联查询。
      2. 然后根据 WHERE 条件进行过滤,筛选出 a 表中 id 小于 10 的数据。
      3. 最后应用 LIMIT 9, 10,表示从第 9 行开始,获取 10 行数据,即获取 id 小于 10 的数据中的第 10 ~ 19 行数据。
    • 优化:在查出表 a 的数据后先进行分页(使用子查询或者临时表来实现)再对表 b 进行关联查询。好处在于:减少不必要的数据传输和处理,提高查询效率和性能。
      1
      2
      3
      SELECT a.*, b.*
      FROM (SELECT * FROM a WHERE id < 10 LIMIT 9, 10) AS a
      LEFT JOIN b ON a.id = b.id;
  • 为什么不考虑转正?
    应届生自然想去大平台,渴望挑战性和创新性的项目,,骏伯是一个比较小的公司,业务是互联网营销行业的,可能就会缺少一些技术积累,,,

  • 遇到了什么困难?
    环境架构都已经配好并且部署在在线环境中,对组件的使用也封装好了,难的难在对实际业务的理解
    我个人开发中遇到的crud不难。比较麻烦的是理解公司的开发模式??使用插件生成BO, DO, Mapper,,

  • 工作中学习到了些什么?和同事沟通了什么?
    工作方面:熟悉团队开发流程(敏捷开发,需求澄清和故事点评估), k8s,掌握git的开发使用,单元测试??
    技术方面:mq的选型与使用、枚举类型直接对应数据库的INTEGER??配置、SOA架构、、(高版本Java的特性(虚拟线程、模式匹配、lambda、ZGC、GraalVM-aot),高并发->NIO、协程、虚拟线程,单元测试)

  • 如何保证代码质量?
    单元测试??+测试

  • 在项目开发中,什么时候让你感到获得了成就感?
    1.项目完成阶段: 成功地完成一个项目的不同阶段或整个项目时,会带来成就感。2.解决难题: 遇到复杂的技术难题,经过努力和钻研后成功解决,这种时刻会让你感到非常有成就感,特别是那些曾经让你感到困扰的问题。3.用户满意。 4.团队协作: 项目中的团队合作是一个重要的方面。当你与团队成员协同工作,成功地推动项目的进展,共同克服了困难,整个团队的成功会带来强烈的成就感。5.学到新知识: 在项目中学到新技术、工具或方法,应用这些知识解决实际问题,这种学习和应用的过程本身就是一种成就感。6.项目的成功实施: 当项目成功实施,交付给用户并产生了积极的影响时,你会感到自己的努力和贡献是有意义的。7.持续改进: 成功地推动了项目的持续改进,通过引入新的工程实践、工具或流程,使项目更加高效和可维护。

实习(创世)

  • “牙医星球”小程序,做了什么?

    1. 接口设计?
      根据用户提供的功能清单和产品原型图,结合已有的数据库表字段,尝试设计了小程序“个人中心”部分的接口以及相关的数据库字段设计。
      例如,根据用户id查询和修改用户信息,分页查询用户在这个小程序上报名的课程列表,查询用户学习在线课程的记录。
      同时,也设计了在线课程学习记录表,按照用户id分区创建不同的线上课学习记录表,避免把所有用户的学习记录放到一张表中。其中“连续学习天数”这个字段,放到了user_info表中作为用户属性字段;同时在user_info表中增加了一个“最近学习日期”,在更新“连续学习天数”时可根据“最近学习日期”判断是否连续,同时将“最近学习日期”更新。
    2. 视频弹幕功能?
      使用 WebSocket 协议,在一个持久连接上进行全双工通信,实现客户端和服务器之间的实时通信。与传统的HTTP通信相比,减少了网络开销和延迟。
      使用 ws 实现实时弹幕的功能:客户端与服务端建立 ws 连接后,使用 WebSocket包中的Session类保存与这个客户端的连接会话,根据ws 连接中携带的用户的connectionId,将这个session保存在静态的线程安全的ConcurrentHashMap中,同时在redis中创建/刷新该用户信息;当服务端收到客户端通过ws连接发送的消息时,从reids中找到所有 最近70s内创建/刷新的 用户连接的connectionId,在Map中找到对应的session,给用户端发送这条新弹幕,同时刷新redis中的用户信息。
      第一次接触http以外的协议,websocket协议是http的升级,服务端接口使用的是@ServerEndpoint(“/“)获取连接信息。对于获取connectionId这个请求,应该在ws连接之前发生,所以使用一个controller实现获取connectionId请求,随后客户端使用js代码调用ws接口,建立ws连接。在开发过程中,发现有关弹幕的service类无法在ws接口中正确注入,所以建议把(通过mybatis)保存弹幕信息到数据库这个行为也分开在controller中实现,点击“发送弹幕”后,先调用controller保存弹幕信息,然后调用ws接口实现实时弹幕。
  • 编程平台项目,做了什么?

    1. 项目整体情况:一个在线少儿编程竞赛平台,学生登录平台,在竞赛时间内获取试题,答题和提交。构建在SSM + SpringBoot上,有统一的返回结果格式 JsonResult,统一的成功/失败返回码。
    2. 学生登录:
      • 生成图像验证码uuid:UUID生成验证码唯一标识 uuid,Hutool 库生成一个圆形验证码。将 uuid 和 验证码的base64编码 返还前端。将验证码标识和验证码内容以键值对的形式(uuid,code)存储到 Redis中,有效期为60秒。
      • 发生短信验证码sendsms:同时以 JsonResult给前端返回该手机号下的所有学生账号(以label、value形式,返回数组 List<Map<String, Object>> stu)因为一个家长的手机号可对应多个孩子的学生账号
      • 登录:使用手机号+学生姓名+密码登录。验证密码是否正确(base64编码+md5加密【不可逆】后,与数据库中存的密码去除头四个尾两个字符后比较),验证码uuid是否存在(返还给前端的标识)且正确(用户输入的验证码),
      • 修改密码:一系列的判断,提交是否为null或空、账号是否存在、新密码是否满足给定的正则格式,接着要与数据库中的原密码(可)和同一登录账号下其他学生密码(不可)判断是否重复。用户输入的密码存入数据库,要经过:base64加密,和项目密钥连接后进行 MD5哈希加密,再通过randomTest.getRandomStr()随机增加到前4个字符和后2个字符。
    3. 编程题判题:
      • @Transactional(rollbackFor = Exception.class) 声明事务,将提交编译题方法 getCompileAnswer包裹在一个数据库事务中,抛异常后回滚。
      • 判断学生id存在,是否在比赛时间,是否已提前交卷(有scoreRecord记录)
      • 使用 restTemplate给布置在服务器上的”判题机”发生编程题判题请求,返回判题结果
      • 如果没有提交记录,新增compileAnswer,判断剩余提交次数;如果此次提交编程题成绩大于原纪录中的,更新记录
      • 返回前端:(success: ; resultCode: ; resultMsg: ; data:通过样例数,平均时间,平均内存)
  • 与产品经理,前端沟通?
    原来使用coding代码协同和项目管理,在蓝湖放了原型设计,通过微信沟通。后面统一改用飞书。
    先开一个项目启动会,确定如静态页面,接口开放,前后端连接调试的进度,时间管理。。??
    开发过程里,我通过看产品原型进行理解,结合项目代码和文档理解,开发接口时与前端对接。。。

  • 你遇到的最大的挑战是什么?怎么解决的?
    一开始的时候,我虽然有ssm,mybatis基础,但是等于一些注解,方法还是一知半解,而且没有看过项目代码,不会看接口文档,没用过git等,,一整个就是非常无从下手。后来就是硬着把需求相关的代码看完,每个小细节都自己查方法或者问同事,从最简单的一个需求开始一个个解决,测试,提交,逐渐上手。当然后面也把缺的技术补上了。
    (技术上遇到的困难,应该就是复杂的业务逻辑吧,如编程题判题)


项目1(跑个腿)

  • “校跑跑”校园代拿平台 https://www.naoffer.com/course/class/302

    • 项目描述:”校跑跑”平台是一款致力于解决大学校园内代拿服务需求的创新型应用,提供方便快捷的外卖、快递、文件等代拿业务,同时确保支付双方的信用和流程的可跟踪
    • 主要工作:
      • 在任务发布模块,通过产品原型总结出代拿服务任务模型,并使用双值枚举的方式对类型、数量、状态等进行存储。
      • 在任务列表模块,实现了多筛选和多排序条件下的动态分页列表查询。
      • 使用数据库悲观锁,解决同一跑腿任务的超卖订单问题,实现抢单唯一。
      • 为代拿双方的陌生人服务安全性,对每个状态进行流水记录。流程状态切换中实现了照片上传的可靠性,为了节省服务器带宽,使用腾讯云 COS 直传方案,提供预签名给前端,保障了 COS 的安全性。
      • 在接单前,需要接单人支付押金,实现押金支付、押金退还的事务处理。
  • 介绍一下跑腿项目!!

    • “校跑跑”是我们学校创业团队做的一个项目,后续会上线运营。它是根据校园中的真实的外卖、文件等物品代拿需求而设计的,另外同学看到送货路线与自己顺路可以顺手接单跑腿。同学们在平台上发布订单,接单,支付,同时能够确保支付双方的信用和流程的可跟踪、、
    • 我参与了项目中几个模块的后端开发。(考虑到校园内用户量和流量量级不大,使用的是单体架构,对于存在的订单超卖等一些场景问题,参考了一些别的项目中的做法);我做的模块里面难点比较多的:
      • 一个是跑腿任务的发布模块,这个模块一是要对信息进行非法过滤,另一个也要基于redis实现非重复支付的方案
      • 然后在详情模块,针对学生代拿需求比较聚集如午饭的情况,会导致多个人同时抢列表的第一个,导致出现并发问题,我这里使用了悲观锁来解决
      • 另外,要在配送过程中,如何去保证双方陌生人的信用安全?比如说在取货的时候,怎么能够知道是跑腿人去拿的货?而不是别人拿着这个验证码去拿到货,或者是说物品送到了,但是说没送到,或者、、
        我们采用的是基于拍照确定地点的方式,比如他到现场,拍照上传,我们才给他公开取货码,他必须在五分钟之内取货,再拍照片,进行验证、、那这个地方就会产生很多的图片,传统的直传方案,会对服务器带宽造成很大的影响,所以项目采用了阿里云或者腾讯云的直传方案,解决了有效性和跨域的签名
      • 第四个押金模块就可以不说了。事务。。
  • 你做过最有挑战的项目?难点是、、

    • S:背景 - T:任务 - A:举措 - R:结果
  • 项目背景

    • 我参与学校创业团队(非全职,有需求就去)的商业项目,目前还未(准备在南航)上线运营,产品需求真实,产品业务做的都是核心的(而一般的实习都是打杂)
    • 需求分析:发单人懒的动弹,需要校内代拿服务;接单人查看送达地点与宿舍很近,回宿舍路上顺便接个单
    • 竞品差别:美团跑腿是根据点到点距离计算费用,我们是根据校内距离不重要,是点到面或者是点到线,也就是这个学生的终点附近的也可以顺手去送,所以可以实现单价比较便宜,一单正常是两块钱
    • 项目架构:单体项目 SpringBoot + Maven
    • 四大模块:跑腿需求发布、跑腿抢单和列表、双方状态和安全、押金和微信支付
    • 产品规划:1、目前是第一版,极小版本上线,核心发布功能、接单功能、押金功能;2、双方信用和评价体系上线;3、集中引用第三方方案(即货物送达方+已经智能货柜)、盗窃监控
    • 团队成员:产品经理*2,后端^3,前端^2
  • 一、跑腿需求发布模块

    • 产品原型(图)
    • 数据库设计
      订单id、发布人id、取货地点、收货地点、货物种类、货物重量、期望送达时间(datetime)、费用、订单状态、创建时间、修改时间
    • 亮点难点
      1. (小亮点): 物品信息非法关键字过滤
        提交时,使用小程序的过滤接口,对发起人的物品信息要素进行非法关键字过滤,并对手机号和“微信”“vx”等字眼进行正则查验,防止后序跑腿双方私下沟通,
      2. (小亮点): 分类等信息使用双重枚举
        对送货的类型、重量等固定有限项要素,要用多值举的方式实现,方便在前端接口和数据库里以字典id类型进行插入,前端传过来的url中携带的是id值(如1:食品、2:文件、、)而不是易错的字符串类型
      3. (大亮点): 后端防止重复提交(一个用户下了多次单)
        (1) 目前是单机项目:在提交接口,使用并发方法(sychronized方法),使用redis随机Key来进行验证(前端生成随机key)
        在下单页面加载时,在后端生成一个唯一的标识符(例如 UUID)作为订单提交的唯一标识符,将这个一次性的标识符存到 Redis。当用户点击提交订单时,在后端根据用户提交的标识符从 Redis 中获取对应的标识符。如果获取到了,则是首次提交,处理后返回提交成功,并且删除redis中的标识符;如果未获取到,则表示是重复提交。
        (2) 后序推广到更多高校,变成分布式环境时,考虑升级到redis分布式锁
  • 防止重复提交的逻辑:
    在下单页面加载时,后端生成一个UUID作为订单提交的唯一标识符,将这个一次性的标识符存到 Redis。当用户点击提交订单时,根据用户提交的标识符从 Redis 中获取对应的标识符。如果获取到了,则是首次提交,处理后返回提交成功,并且从Redis中删除;如果未获取到,则表示是重复提交。

  • 二、跑腿抢单和动态列表模块

    • 产品原型
      查看跑腿订单详细信息后点击“立即接单”,抢单后才可查看发布人信息(电话,取货码(在安全模块处理),备注)
      跑腿动态列表通过排序、筛选,展现跑腿订单(接单人可以将自己宿舍作为收货地址进行筛选)
    • 业务逻辑
      1. 跑腿任务详情模块目前只做展示和下单,接单过程不在本模块。需要提供任务详情展示接口和抢单接口
      2. 列表模块需要考虑多条件动态排序、筛选,实现分页接口
      3. 抢单跑腿详情和订单支付的逻辑
        跑腿详情状态有:1 等待抢单; 2 已被抢; 3 已确认(抢单后需要手动确定或取消,如果1分钟内不取消,自动变成确认);4 超时费单
        (1) 某一用户点击抢单时,需要判断是否是1和2的状态,这两个状态都可以触发排队60s流程
        (2) 如果是1,则用户抢单成功,这个时间详情状态变为2,其它用户只能等待60s(60S退出的时候,再做一次(1)r的逻辑)
        (3) 如果(2)用户取消订单,则状态变成1
        (4) 如果支付成功,则其它用户不能进入流程,已在流程中的依然等待
        (5) 20分钟未支付的逻辑需要定时任务触发,详情变成1
    • 亮点难点
      1. 在抢单模块,因为学生代拿时间集中,会造成同一任务的超卖订单,项目使用悲观锁解决(小规模并发)问题
        并使用60s倒计时的方案,实现并发抢、已抢但放弃的场景
      2. 本模块多处使用定时任务(quartz schedule)更改订单状态,比如1分钟内自动确认和超时废单等
      3. 在列表模块,因为校内多地点的选择场景,实现多筛选、多排序下的动态分页接口
        3.1排序项:最新(默认)、按金额从大到小
        3.2筛选项:多地点、重量、送达时间、类型、数量
  • 乐观锁与悲观锁的区别?怎么用?

    • 悲观锁:认为资源一定存在竞争,加悲观锁,具备阻塞特性,以保证多事务对统一数据的访问的安全性;
      • 在并发量不大且冲突频率较低的情况下是有效的,悲观锁可以提供更加稳定和可靠的数据操作保护,提供较为简单和直接的控制方式;尽管乐观锁在并发量较小且冲突频率不高的情况下具有优势,但在数据修改频率较高、竞争激烈的情况下,悲观锁通常更加高效;
      • 但是如果并发量很高或者冲突频率很高,则可能会导致性能问题和系统的阻塞。
      • 使用方法:
        1. 查询订单信息并加锁:在抢单逻辑中,首先要查询订单信息并加锁,防止其他用户同时抢单。读和写是在同一步,查询并加排他锁,确保其他事务无法修改这些数据,直到当前事务提交或回滚。
          1
          2
          START TRANSACTION;
          SELECT * FROM orders WHERE id = ? AND status IN (1, 2) FOR UPDATE;
        2. 更新订单状态:如果查询到符合条件的订单,就可以更新订单状态并完成抢单操作,示例代码如下:
          1
          UPDATE orders SET status = 2 WHERE id = ?;
        3. 提交事务:完成操作后,需要COMMIT;提交事务来释放锁并保证数据的一致性
        4. ??处理超时情况:针对20分钟未支付的逻辑,可以使用定时任务来触发查询并加锁,然后更新订单状态为1(等待抢单状态)。
    • 乐观锁:认为大部分情况不存在竞争,不加锁(不阻塞),使用版本号version判断是否修改(修改数据同时修改version)
      • 在并发量大的时候,悲观锁用不了,可以考虑用乐观锁;CAS,MVCC是乐观锁的实现
      • 实现方法:
        1. 版本号字段实现乐观锁:在数据库表中增加一个版本号(version)字段,数据类型通常选择整数。每次更新数据时,将版本号加一。在更新数据时,通过比较当前查询到的版本号与更新时的版本号来判断是否更新成功。
          1
          2
          3
          4
          5
          6
          7
          8
          9
          10
          11
          CREATE TABLE orders (
          id INT PRIMARY KEY,
          order_id VARCHAR(50) UNIQUE,
          version INT,
          -- Other columns
          );

          -- 更新订单方法,使用乐观锁
          UPDATE orders
          SET version = version + 1
          WHERE order_id = 'xxx' AND version = current_version;
        2. 时间戳字段实现乐观锁:与之类似
        3. 注意事项:
          • 在使用乐观锁时,需要保证更新操作的原子性,通常可以使用数据库事务来确保。
          • 在比较版本号或者时间戳时,可以通过数据库的原子性操作来实现,例如使用 WHERE 子句中的条件来比较版本号或者时间戳。
          • 当更新操作失败时,可以根据具体业务需求选择重试更新操作或者抛出乐观锁异常供上层处理。
  • 跑腿抢单的请求是直达数据库吗?可以优化吗?

    • 是的。我们项目是第一版,目标也只是在南航一所学校先试点运营,用户量很小,每一个订单的抢单并发 评估过 1s不过超过5人。所以直接使用数据库锁方案。所以我们测试人员只给业务case,完全不做压力测试。
    • 在量级很大的情况下,使用数据库的悲观锁来实现抢单唯一可能会导致性能问题和系统瓶颈。优化方案:
      1. 锁优化:尽量缩短悲观锁的持有时间,只在必要的操作时使用锁,然后尽快释放锁,可以使用事务管理来确保在合适的时候释放锁。尽量减少锁的粒度,比如只针对需要保护的数据进行锁定,而不是整个表或整个数据库。使用乐观锁。
      2. 使用缓存:对于频繁读取的数据,可以考虑使用缓存来减轻数据库压力,提高响应速度。
      3. 异步处理:对于一些非实时性要求较低的操作,可以考虑使用异步处理方式,将任务放入消息队列或异步任务队列中处理,减少对数据库的直接访问。
      4. 分布式架构:如果系统规模非常大并且需要处理大量并发请求,可以考虑使用分布式架构;1、微服务架构:将系统拆分出抢单微服务; 2、负载均衡:将请求分散到多个节点上处理,从而减轻单个节点的压力;3、分布式数据库:分担数据库的读写压力,提高数据库的处理能力和容量;4、分布式锁:确保在分布式环境下,同时只有一个节点(或者说一个进程)能够获取到锁,从而保证对共享资源的操作是互斥的,避免出现数据不一致或者并发冲突的情况。
  • 三、跑腿双方状态与安全保障

    • 模块说明:本模块是指在跑腿人接单后整体送货流程的状态变化,即针对任务详情状态的”进行中”,进一步扩展细化。包括发布人和接单人双方。
    • 发布人和跑腿人的状态:其中234属于一个阶段,567属于一个阶段
      1. 任务已被接单
      2. 物品已被取到
      3. 物品未正常取到(物品损坏、没有该商品、取货人故意没取)
      4. 物品已损坏(取货和过程中,结束)
      5. 物品已通知送达(跑腿人状态)
      6. 物品确认收到
      7. 物品未确认收到
      8. 物品确认有问题(破损等)
      9. 任务正常结束
        另外,本模块进行最基本的流程安全保障,即在送货人在取货和收货环节拍照确认;发布人在收货环节当面确认(后序加入可选收货码)。
    • 确保跑腿人到达取货位置,才能显示取货码,防止每三方恶意盗窃,通过”拍照上传” + “5分钟之内上传取货拍照”的双重验证 (实际上,产品角度还有两个后期更可靠的方案,1是所有地点接入地图,以位置距离确认 2是商家可变二维码)
    • 亮点难点
      1. 在用户跑单时,通过拍照上传(获取取货码) + 5分钟之内上传取货拍照 的双重验证,保证取货码不被第三方获取。
        同时该方案造成大量图片的上传,为缓解服务器带宽压力,使用腾讯云C0S直传来代替传统的后端上传的流程
      2. 用户跑单会在双方产生将近10种状态,以送单人的角度设计状态的分类
  • 腾讯云COS的参数是怎么配置的?
    https://cloud.tencent.com/document/practice/436/9067

  • 图片直传那一块具体做了什么?
    就是后端,使用的官方SDK,针对前端指定的cos文件夹 和指定的文件名,进行临时权限签名的生成, 同时也保证其它人和文件没有权限直传 保证cos安全性。

  • 四、押金和钱包事务模块

    • 模块说明:因为业务上与钱的流水关系很大,需要单独分离是钱包模块,可以是记录流水、以及押金和收入的提现、发起人的退款记录等
    • 亮点难点
      1 押金的退还属于原路退回,主要接入对应支付渠道的退款接口,但是核心是退款流程分为两部分,一是退款成功,二是押金流水变更,两个阶段存在原子性,因为这部分业务,属于较低频型业务,所以使用事务方案,而不是补偿方案。
      2 提现需要使用另外的支付宝公对私打款接口,在收取10%的平台费用后(不做个税扣除),进行批量打款。打款不做强可靠性,用户对提现和退款 不像支付要求那么高
  • ???押金退款的事务?

    • 押金的原路退回可能会破坏退款流水和押金流水的原子性,两个步骤必须一起进行,属于最常见的事务场景 1 退款流水状态变成“已退款” 2 押金退款接口返回成功
      接口肯定得有返回值 正常逻辑不要总想着加异常 就算有可能出现异常 在service方法级别 一般都是内部处理 不抛出??
      能不能理解为退款是调接口实现,押金流水是数据库操作???事务逻辑如下:
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      @Service
      public class RefundService {

      @Autowired
      private CrudService crudService;

      @Transactional(rollbackFor = {RefundException.class, CrudException.class})
      public void refundAndChangeFlow() throws RefundException, CrudException {
      try {
      crudService.changeFlow(); // !!先调用流水变更的业务逻辑
      退款(); // 这里是退款的业务逻辑,需要根据具体情况实现
      } catch (RefundException e) {
      退款撤销();
      throw e;
      } catch (CrudException e) {
      throw e;
      }
      }
      }
  • 升级成分布式、、

    • 实现:把单体架构中的service调用变成了微服务的rpc调用,代码层面区别不大
    • 难点:就是要解决网络的不可靠性。分布式架构无非就是把进程内调用变成进程间调用,跨网络、跨节点的网络调用在时间足够长、流量足够大的情况下,就要考虑网络超时的问题,设置超时时间,避免整个系统被拖挂。
      • 读请求,超时重试
      • 写请求,分布式事务
    • 其他挑战:服务治理(服务发现,服务集群,负载均衡,优雅启停,熔断降级),数据一致性问题(对于开发更重要)
  • 数据一致性

    • 缓存与数据库:异步化
    • 分布式事务

项目2(黑马商城+牛客论坛)

  • 高校闲置资源交易系统

    • 系统架构:SpringCloud + SpringBoot + Nacos + Redis + MySQL
    • 项目描述:系统采用微服务架构,旨在为消费者提供一站式商品交易平台。通过细致的业务划分,系统分为用户模块、商品模块、购物车模块与支付模块,确保每个模块服务的高效、可维护、可扩展。
    • 主要工作:
      • 使用 Nacos 实现服务注册,消费者服务通过 Nacos 拉取和订阅服务。
      • 引入 Redis 数据库,优化项目中登录验证码的使用,对需要频繁访问的数据进行缓存。
      • 使用 Gateway 及拦截器实现了登录信息校验,避免各微服务中的代码重复。
      • 通过 Sentinel 完成系统限流熔断功能,防止服务故障后影响整个系统运行。
  • 单体架构改为微服务架构(分布式架构的一种实现)
    微服务架构,是服务化思想指导下的一套最佳实践架构方案。服务化,就是把单体架构中的功能模块拆分为多个独立项目。
    优点:粒度低、团队自治、服务自治

  • 登录的流程,登录状态的检验??
    提交登录表单,登录请求 controller —> sevice —> Dao,查询、校验用户信息,生成jwt(用jwtTool生成token),返回登录信息和jwt(或登录失败信息),提示登录成功(失败)。登录成功后,用户在发送请求时(如购物车),在请求头中携带 Authorization(即jwt)

  • 用户登录。。redis怎么用的?

    1. ???Redis 结合 Gateway 完成登录校验(直接拦截后调用 jwtTool 验证不就行了)
    2. 使用Redis缓存用户信息:将user缓存到Redis中,获取user时,先从Redis获取。取不到时,则从数据库中查询,再缓存到Redis中。因为很多界面都要用到user信息,并发时,频繁的访问数据库,会导致数据库崩溃。变更数据库时,先更新数据库,再清空缓存;
    3. ???使用Redis缓存验证码:原本添加到session中,减轻服务器压力。将验证码存到Redis中,方便查询检验;
      当⽤户点击刷新验证码时,服务端⾸先给当前需要登陆的游客,设置⼀个随机字符串(kaptchaOwner),⽤于标识当前这个游客,然后将随机字符串存⼊到cookie中,返回给浏览器,然后服务端的redis保存 “key:随机字符串,value:验证码”。接着⽤户输⼊⽤户名,密码,验证码,再次点击登陆时,服务端会从cookie中拿到kaptchaOwner,通过它,可以从Redis中得到正确的验证码,然后与⽤户输⼊的验证码做⽐较,看是否⼀致。
      验证码需要频繁的访问与刷新,对性能要求很高;验证码不需要永久存储,通常在很短的时间内就会失效;分布式部署时,存在session共享问题;
  • Nacos 服务发现

    • 消费者需要连接nacos以拉取和订阅服务,因此服务发现的前两步与服务注册是一样(引入nacos-discovery依赖和配置nacos地址),后面再加上服务调用即可
      1
      2
      3
      4
      5
      6
      7
      8
      9
      private final DiscoveryClient discoveryclient;
      private void handlecartItems(List<CartVO> vos) {
      // 根据服务名称拉取实例列表,负藏均衡挑选一个实例
      List<ServiceInstance> instances = discoveryclient.getInstances("item-service");
      ServiceInstance instance = instances.get(RandomUtil.randomInt(instances.size()));
      // 获取实例的TP和端口
      URI uri= instance.getUri();
      // ...发送请求
      }
  • Nacos 配置管理。
    当微服务部署的实例越来越多,业务配置如 JDBC、日志、openfeign 时常变动,逐个修改微服务配置就会让人抓狂。
    Nacos实现配置共享,配置热更新,动态路由
    https://www.bilibili.com/list/watchlater?oid=961238101&bvid=BV1kH4y1S7wz&spm_id_from=333.1007.top_right_bar_window_view_later.content.click&p=34

  • OpenFeign

    • ??
    • 最佳实践
  • 网关 Gateway

    • application.yml 配置;
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      cloud:
      routes: # 网关路由配置
      - id: user-service # 1. 路由id,自定义,只要唯一即可
      # uri: http://127.0.. # 2. 路由的目标地址 http 就是固定地址
      uri: lb://userservice # lb(loadbalance) 就是负载均衡,后面跟服务名称
      predicates: # 3. 路由断言(可多条),判断请求是否符合路由规则的条件
      - Path=/user/** # 按照路径匹配,只要以/user/开头就符合要求
      filters: # 4. 路由过滤器,,
      - id: order-service # ...
      # ...
  • 网关登录校验。

    • 基于jwt实现登录校验
      • hmall.jks:密钥文件,加密存储
      • application.yml:配置文件,配置密钥文件目录、别名、解析密钥文件的密码、、excludePath(不用登录校验的路径)
      • JwtPropertities:加载 jw t属性配置的类
      • AuthPropertities:加载 excludePath 属性配置的类
      • SecurityConfig:加载jwt配置后,真正读取文件生成密钥的类,类本身声明为一个 @Bean;过程为注入配置,获取密钥工厂,读取密钥对
      • JwtTool:jwt工具类,接收用户id与时间生成jwt密钥、解析token
    • 各模块校验:登录在user模块,验证成功后得到token,后续使用每个微服务(购物车、下单,,)时,如果都要都要进行检验,会造成重复代码以及重复发送jwt秘钥造成泄露风险,,所以,在网关中,转发请求给微服务之前,进行jwt登录校验
  1. 如何登录校验?
    具体来说,要定义一个(自定义)过滤器,放在pre阶段;在过滤器中实现校验业务逻辑,校验通过后才会向下执行(下面的过滤器),最后由 Netty 路由过滤器转发给微服务

    全局过滤器的作用也是处理一切进入网关的请求和微服务响应,与路由过滤器GatewayFilter的作用一样。 区别在于GatewayFilter通过配置定义,可以自由的指定作用的范围,配置自定义参数,使用灵活,但处理逻辑是固定的,难以自定义GatewayFilter; 而GlobalFilter可以自己写代码实现,实现需要的业务逻辑,需要自定义过滤器时大多会选择 GlobalFilter。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    @Component
    @RequiredArgsConstructor
    public class AuthGlobalFilter implements GlobalFilter, Ordered {

    // 添加 @RequiredArgsConstructor 后自动使用构造器注入下面成员变量
    private final AuthProperties authProperties;
    private final JwtTool jwtTool;

    @0verride
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
    // 1.获取请求
    ServerHttpRequest request = exchange.getRequest();
    // 2.判断是否需要做登录拦截(使用 AntPathMatcher 匹配 authProperties 中的路径)
    if(isExclude(request.getPath().tostring())){
    // 放行(如 /user/login)
    return chain.filter(exchange);
    }
    // 3.获取token
    string token = null;
    List<string> headers = request.getHeaders().get("authorization");
    if(headers != null && !headers.isEmpty()){
    token= headers.get(0);
    }
    // 4.校验并解新token
    Long userId = null;
    try {
    userId = jwtTool.parseToken(token);
    }catch(UnauthorizedException e) {
    // 拦裁,设置响应状态码为401(只是抛异常的话响应500,看不出具体错误)
    ServerHttpResponse response = exchange.getResponse();
    response.setstatusCode(Httpstatus.UNAUTHORIZED);
    return response.setcomplete(); // 结束
    }
    // TODO 5.传递用户信息

    // 6.放行
    return chain.filter(exchange);
    }

    public boolean isExclude() {
    // ...
    }

    @0verride
    public int getOrder() {
    //过滤器执行领序,信越小,优先级越高
    return 0;
    }
    }
  2. 如何将用户信息传递给微服务?
    对于单体架构系统,所有功能在部署在一个tomcat上,一个tomcat内部不同线程之间共享ThreadLocal;对于微服务架构,网关本身就是一个微服务,和其他微服务要通过 HTTP请求传递信息,把用户信息放在请求头中不会对业务造成影响,后续从请求头中取出用户信息、、
    修改gateway模块中的登录校验拦截器,在校验成功后保存用户到下游请求的请求头中,使用ServerWebExchange类提供的API:

    1
    2
    3
    4
    5
    6
    7
    // 5.传递用户信息
    String userInfo = userId.tostring();
    ServerWebExchange swe = exchange.mutate().
    request(builder -> builder.header("user-info", userInfo))
    .build();
    // 6.放行修改后的请求头(到下面过滤器链)
    return chain.filter(swe);

    在后续的微服务中,通过拦截器,从请求头中获取登录凭证,进行登录校验,并保存在 ThreadLocal;
    由于每个微服务都可能有获取登录用户的需求,因此我们直接在hm-common模块定义拦截器,这样微服务只需要引入依赖即可生效,无需重复编写。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    // 定义 springmvc 拦截器
    public class UserInfoInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpservletRequest request, HttpservletResponse response, obiect handler) throws Exception {
    // 1.获取登录用户信息
    String userInfo = request.getHeader("user-info");
    // 2.判断是否获取了用户,如果有,存入ThreadLocal
    if(Strutil.isNotBlank(userInfo)) {
    UserContext.setUser(Long.ValueOf(userInfo));
    }
    // 3.放行
    return true;
    }
    @Override
    public void afterCompletion(HttpservletRequest request, HttpservletResponse response, 0bject handler, Exception ex) throws Exception {
    // 清理用户
    UserContext.removeUser();
    }
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // 编写 springmvc 配置类
    @Configuration
    // 防止网关微服务加载此配置
    @ConditionalOnClass(DispatcherServlet.class)
    public class MvcConfig implements WebMvcConfigurer {
    @0verride
    public void addInterceptors(InterceptorRegistry registry) {
    registry.addInterceptor(new UserInfoInterceptor());
    }
    }
    最后,修改每个微服务的配置扫描文件,使其能够扫描到 hm-common模块中定义的 UserInfoInterceptor 拦截器; 注意:网关微服务不能引用 hm-common模块中的 拦截器,因为网关的底层不是 SpringMVC 实现的,无法加载SpringMVC拦截器的功能,会报错;
  3. 如何在微服务间传递信息?
    微服务项目中的很多业务要多个微服务共同合作完成,而这个过程中也需要传递登录用户信息;
    方法同样是把用户信息保存到请求头,发送 Http请求。不同的是微服务之间发送请求通过 Openfeign,而网关发送请求是通过其内部的 http 请求方式(netty过滤器路由分发),,

  • 拦截器(Interceptor),,
    ??
    目的:让未登录用户不能访问某些页面
    原理:在方法前标注自定义注解,拦截所有的请求,只处理带有该注解的方法。
    拦截器:1.自定义拦截器 2.配置拦截器
    首先验证用户(preHandle方法),如果用户存在,则在本次请求中持有用户,放进hostHolder里
    经过controller后,返回到拦截器,拦截器再将用户信息存入model。
    自定义拦截器需要实现HandlerInterceptor,然后重写preHandle(controller前执行),postHandle(controller后执行),以及afterCompletion(模板解析后执行)
    配置拦截器:需要实现WebMvcConfigurer接口,然后重写addInterceptors方法,排除拦截静态页面。

  • ThreadLocal(线程安全)
    从上面的代码可以看出,ThreadLocal set赋值的时候首先会获取当前线程thread,并获取thread线程中的ThreadLocalMap属性。如果map属性不为空,则直接更新value值,如果map为空,则实例化threadLocalMap,并将value值初始化
    ThreadLocalMap是ThreadLocal的内部静态类,而它的构成主要是用Entry来保存数据 ,而且还是继承的弱引用。在Entry内部使用ThreadLocal作为key,使用我们设置的value作为value。
    通过当前线程对象的getMap()方法获取ThreadMap对象 然后将当前ThreadLocal对象作为key值存入map 这能保证线程内的资源共享而不同线程之间独立

  • Sentinel

,,

项目系统(牛客论坛)

  • 论坛系统项目(高校信息平台。。。)
    • 项目描述:一个基本功能完整的论坛项目。主要功能有:基于邮件激活的注册方式,区别登陆状态为游客或已登录用户展示不同界面与功能。支持用户上传头像,实现了发布、评论帖子,发送私信等功能,以及点赞,关注与系统通知功能。
      项⽬构建在 Spring Boot + SSM 框架之上,并统⼀的进⾏了状态管理、事务管理、异常处理。
    • 核心功能与项目优化:
      • 使用 Spring Email 辅助发送注册激活邮件,并且使用 Interceptor 拦截器赋予不同类型的用户权限,解决 http 无状态带来的缺陷问题,保护需登录才能查看的资源。
      • 使用 Spring AOP 面向切面编程思想统一记录日志;
      • 引入 Redis 数据库,优化项目中 Session 的使用并利用 Redis 实现点赞和关注功能。
      • 使用 Kafka 消息队列构建异步消息系统,实现点赞关注与通知间削峰。
      • 使用 Caffeine + Redis 实现两级缓存,优化了热门帖子的访问。
  • 介绍项目
    1. 梳理系统的项目背景以及整个系统架构设计与运转流程
    2. 技术上的亮点:”我在处理数据的过程中,实现了数据的一致性和可靠性,做到了数据零丢失”、“项目引入了规则引擎,其中解决了xxx的业务问题,使得工作效率极大提升”… 等等
    3. 业务上的亮点:”实现了业务隔离,不同类型之间的业务互不干扰,从原来的xxx提升到了、”参考自某平台的xx功XXx”能,从零开始实现了业务,使得平台收益增加了xxx”.. 等等
    4. 梳理项目还可提升的地方:无论是提高性能,还是提高工作效率,总会有的。这时候如果能吹下自己对比过某某公司的同类型系统,借鉴了某某某优点,基于目前自身的业务觉得还有哪里可以继续优化

算法

  • 排列组合(C上n下m,A上n下m)

    • A上n下m是排列,有顺序,表示m是起点(逐个减1,n是个数),从m中按顺序取n个数
      Amn = m x(m-1)x(m-2)x(m-3)x … x(m-n+1);例如:A74= 8x7x6x5
    • C上n下m是组合,无顺序,表示从m中选出n个
      Cmn = Amn / Ann; 例如: C84 = A84 / A44 = (8x7x6x5)/(4x3x2x1)
  • 5万人进行年龄排序
    “使用快速排序算法来进行年龄排序,它的时间复杂度通常为O(n log n)。
    我会首先将人口数据存储在一个数组中,然后根据每个人的年龄进行排序。在排序的过程中,我会使用递归来不断地划分数组,并交换元素的位置,直到整个数组按照年龄有序。如果数据量较大,我也可以考虑一些优化策略,比如随机化快速排序来避免最坏情况的发生,或者利用多线程来加速排序过程。最后,我会对排序后的结果进行验证,确保排序是准确的。”

  • 智力题:25匹马选前3名,有5条赛道
    将25匹马分成 A、B、C、D、E 5组,每组进行组内比赛,假设每组的排名就是A1>A2>A3>A4>A5(以A组为例),这里比赛5次
    第6次,每组的第一名进行比赛,可以找出最快的马,这里假设A1>B1>C1>D1>E1,D1,E1肯定进不了前3,直接排除掉 D、E 两组
    第7次,A2 A3 B1 B2 C1 比赛,可以找出第二、第三名。所以最少比赛需要7次

  • ???智力题:1000瓶酒,其中只有1瓶毒酒,10只小白鼠,如何1轮测试出来毒酒是哪一瓶?
    用小白鼠喝这瓶酒or不喝这评酒分别用1、0表示。可以凑出1024种组合,大于1000种,所以可以给1000瓶酒按照这种方式编号。
    需要十只老鼠,如果按顺序编号,ABCDEFGHIJ分别代表从低位到高位每一个位。 每只老鼠对应一个二进制位,如果该位上的数字为1,则给老鼠喝瓶里的药。
    然后用对应的2进制码的方式给10只小白鼠喝酒,过一段时间之后。通过观察小白鼠是哪些编号死亡,哪些编号存活,可以分别用1和0表示,然后凑出来的2进制转10进制就是答案。

  • 智力题:绳子两头烧
    现有若干不均匀的绳子,烧完这根绳子需要一个小时,问如何准确计时15分钟,30分钟,45分钟,75分钟。。。
    15:对折之后两头烧(要求对折之后绑的够紧,否则看45分钟解法)
    30:两头烧
    45:两根,一根两头烧一根一头烧,两头烧完过了30分钟,立即将第二根另一头点燃,到烧完又过15分钟,加起来45分钟
    75:=30+45


反问

  • 一面最后的时候,面试官一般会问:你有什么要问我的吗?这个时候不要乱问,你可以问以下几个问题:

    1. 你们部门在做些什么?主要业务是什么?如果自己很荣幸的能够进入贵部门会负责些什么?
      因为面试者就是这个部门的,通过他的回答,你也能够了解到这个部门正在做的产品和使用的技术。
    2. 岗位的发展情况、、如果我有幸应聘成功,公司对我会有哪些期望呢,
    3. 您认为我在哪些方面还存在着不足?
      这是一个很巧妙的问题,因为它可以从侧面反映出你这次面试的结果。如果面试官带有指导性的回答出了你的不足,你需要补充的知识点,这样就代表你这次面试差不多了,应该是能好好准备二面了;如果面试官直言不讳的说你很差或者基础太弱这样的话,你也就知道凉凉了。
    4. 请问面试官对自己职业规划的建议?
      面试官大概率是技术大佬或者工作过几年的前辈了,在社会上摸爬滚打了几年,知道的肯定比在校生多。这个问题既表达了对面试官身份的认可,也表现出求职者对当前这份工作得在意程度,并且还能得到技术大佬的分享,怎么看都不是亏本的买卖。
    5. 这个岗位所在的的团队是什么氛围?我这个比较外向(千万要说自己外向,不要说自己内向,原因不必多说了吧)
  • 关于公司,岗位有什么想问的?
    1.关于公司:“公司的核心价值观是什么?” “能否分享一下公司的发展战略和未来规划?” “公司的文化是怎样的?”
    2.关于团队和领导: “我将会与哪些团队成员合作?” “对于这个岗位,我将直接报告给谁?””团队的工作氛围是怎样的?”
    3.关于岗位: “这个岗位的主要职责是什么?绩效评估标准?” “在这个岗位上,有没有机会进行培训和专业发展?”
    4.关于工作环境:“公司对于员工的工作生活平衡看法?公司的灵活工作政策?” “关于工作场所和设施有什么特别的安排?”
    5.关于发展机会: “公司是否鼓励内部晋升和职业发展?” “可以谈谈员工在公司成长的例子吗?”
    6.关于期望:“对于这个职位,你们期望新员工在短期和长期内能够做到什么?” “公司对于创新和改进有怎样的期望?”
    7.关于团队协作:“在团队中如何促进协作和团队精神?” “有没有定期的团队活动或培训?”
    8.关于招聘流程:“对于这个岗位的招聘流程是怎样的?” “预计多久会有关于我的招聘决定?”

  • ??你如何评价我们的面试过程?
    1、积极、建设性的角度:强调你对面试过程的积极印象,比如面试官的专业性、问题的多样性和挑战性等。表达感激之情,感谢他们给予的机会并展现出你对公司或团队的兴趣。
    2、注意事项和改进建议:提及你认为面试过程中可能的改进点,但要以建设性的方式提出,不要过于批评或负面。例如,可以提议增加某些技术领域的深入问题,或者更多关于公司文化和团队合作的问题,以更好地了解公司的工作环境。

  • ??有什么要分享给面试官的?
    1.、展示你的准备和兴趣:提到你在公司产品、服务、项目方面的研究和了解,表现出你对公司的热情和积极性。说明你对该公司的价值观、愿景或在该领域的兴趣,并解释为什么你认为你的技能和经验与公司的需求相匹配。
    2、自我评价和亮点:强调你认为自己最突出的技能、经验或个人特质,并解释为什么这些对公司或团队会有价值。提供具体的案例或经验,展示你的领导能力、团队合作精神、解决问题的能力等方面的优势。
    3、职业发展展望:谈论你对未来职业发展的期望,并说明你希望在该公司或类似领域发展自己的职业生涯。表现出你对个人成长和对公司做出贡献的愿望。


HR 面

  • 对公司文化的理解和价值观的认同

    • 先去了解一下公司的文化和公司的优势之处,这样在被问到为什么选择本公司的时候能够把自己对公司的了解和优势说出来,体现自己的诚意
    • 华为:”我对华为的文化印象深刻,特别是关于坚持自主创新、追求卓越和不断学习的价值观。我接受的教育,使我深刻理解和认同华为一贯的追求卓越的精神。我认为,华为强调的团队协作和开放式沟通也非常符合我的工作风格。我期待能够在这样一个注重技术创新和全球合作的公司中发展自己的职业生涯。”
    • 腾讯:”我对腾讯的文化深感兴奋,尤其是关于用户至上、创新驱动和团队合作的价值观。在我之前的项目中,我注重倾听用户反馈,并通过创新的方式解决问题,这与腾讯强调用户体验和创新的文化非常契合。我也非常欣赏腾讯在团队协作方面的努力,我认为一个团结协作的团队是实现伟大目标的关键。因此,我相信我的工作风格和价值观与腾讯的文化非常匹配,我渴望能够为腾讯的发展贡献力量。”
    • 感觉您这边平台大,技术积累多,项目挑战多,能力提升多
  • 你对加班的看法?
    如果是工作需要我会义不容辞加班。我现在单身,没有任何家庭负担,可以全身心的投入工作。但同时,我也会提高工作效率,减少不必要的加班

  • 你对薪资的要求?
    ①:“我对工资没有硬性要求。我相信贵公司在处理我的问题上会友善合理。我注重的是找对工作机会,所以只要条件公平,我则不会计较太多
    ②:我作为一个科班出身,具备比较完备的工作技能的人,我相信自己能为公司带来价值。。因此,我希望公司能根据我的情况和市场标准的水平,给我合理的薪水。
    ③:如果你必须自己说出具体数目,请不要说一个宽泛的范围,那样你将只能得到最低限度的数字。最好给出一个具体的数字,这样表明你已经对当今的人才市场作了调查,知道像自己这样学历的雇员有什么样的价值。

  • 眼下你生活中最重要的是什么?未来几年的规划是什么?
    对我来说,能在这个领域找到工作是最重要的。
    规划好职业生涯,与同事们很好地合作。我希望能充分展示我在这个行业的能力和智慧。

  • 说说你对行业、技术发展趋势的看法?
    ???
    回答提示:企业对这个问题很感兴趣,只有有备而来的求职者能够过关。求职者可以直接在网上查找对你所申请的行业部门的信息,只有深入了解才能产生独特的见解。企业认为最聪明的求职者是对所面试的公司预先了解很多,包括公司各个部门,发展情况,在面试回答问题的时候可以提到所了解的情况,企业欢迎进入企业的人是“知己”,而不是“盲人”。

  • 职业发展规划?
    T型人才,一方面在技术领域深挖,一方面对整个产品的端到端有一个了解、、

  • 请用三个词描述您的性格特点
    诚实,勤奋,谦虚。在学习知识和与人交往的过程中,我始终保持坦诚和真实,对自己和他人负责。面对生活中的挑战,我积极看待并勤奋努力,相信努力终会有所回报。谦虚让我保持谦逊和虚心,愿意倾听他人意见并不断学习成长。

  • 举例说明你的勤奋。。
    我详细安排毕业设计开发进度,一周内每天熬夜到3点。。
    我想说的是很多时候即使早已做好安排,到头来还是因为各种情况不得不赶进度,重要的是能够在需要时赶好进度的能力。。

  • 你身边的人如何评价你?
    他们说我是一个诚实、学习和工作努力、负责任的人。我一旦答应别人的事情,就一定会负责到底,否则我不会轻易许诺。
    以及我是一个比较随和的人,与不同的人都可以友好相处,在与人相处时,我总是能站在别人的角度考虑问题。

  • 你的优点和缺点。

    • 我是一个诚实,比较勤奋和乐观的人。我的适应能力比较强,喜欢向他人学习,也愿意帮助他人。在以往的实习、项目中,能够很快适应团队开发流程。我相信在经过一段时间的培训及项目实战后,能够在这份工作中取得成功。
    • 我缺乏工作经验。。
      • 在之前的实习过程中只是参与了开发这一环节,没有跨部门合作、与不同岗位的同事深度交流的经验。
      • 有点强迫症,会对任务的细节核对多遍,比如发邮件,填资料的时候必须要重复检查,把信息正着反着过一遍,这样虽然有点浪费时间,但就基本不会出什么差错了;
      • 另外就是有点钻牛角尖,在学习新的知识时有个点花了很长时间都没弄懂,会比较泄气。后来发现可以把不懂的先放着,等学了别的知识,再返回来看这个知识点就触类旁通了;我目前也很习惯使用chatgpt进行学习,通过提问快速发散知识点,建立联系。
  • 高中到大学以来,最大的挫折。
    高中以来一直遭到失败,学业、社团、人际交往,,一度让我十分沮丧,但也是我思考了很多的一段时间,,我渐渐了解:失败是人生的常态。
    我很庆幸一次次经历失败的过程中,我都还是选择了再努力一下,也许就有改变呢,往往最后问题都能够解决了,,
    最重要的,我学会在任何情况下保持冷静,,在未来,在大学面对各种压力时,都可以比较体面地应对了,并且思考如何做得更好、、

  • 学习方法。
    某一个点:根据需求针对性的官方文档,,查技术论坛、、
    某一领域:(看B站视频 ×) 在有了大致了解后,使用chatgpt,不断提问、发散,把知识点都连接起来

  • 最近看了什么书?读了什么开源库?
    ???没看。

  • 考研弃考?秋招0 offer?空白期?
    没考研!!找工作比较晚(因为有java基础和项目经验、、)并且夏令营,毕业实习占用了很多时间,,
    秋招期间,我一直学习java技术栈,(没把握所以)没怎么投简历,,后面我为了丰富自己对开发的了解,也在12月底找了一份实习做到现在了。。目前不考虑考研
    春招期间,,,

  • 表现抗压能力以及处理意外情况的能力

    1. 分享一件你觉得压力比较大的事?你的压力从何来?你是如何克服他的?
      ??1.适应压力,有压力动力更足。2.会合理安排时间,取得工作效率很好的平衡
    2. 你长这么大以来遭受过的最大挫折是什么?你是如何克服它的?
      ??
    3. 二十多年来,你取得的最大成就是什么?
      ??
    4. 你通过多年努力获得的一项技能是什么?你是如何学习从而获得这项技能的,做了哪些工作去改善、精进这项技能?
      ??
      学会面对任何困难保持冷静。从上学时突然被点名发言、突然被委任参加一个比赛,到现在做项目时面对需求从头开始学习新的技术,我发现在面对困难时,有效的方法就是冷静分析局势,以及最重要的是尽早的着手做。这样即使最后无法完成,也有所收获。
  • 你能使组里气氛活跃,并且易于沟通吗?
    我会尽力,和团队中的成员沟通,向他们发问、学习。因为在团队里工作最重要的一件事就是集思广益,而不要只是一个人闷头单干。沟通是很重要的,不仅可以提高工作质量和效率,也可以产生活力。

  • 如何处理与同事在工作中意见的不和?与上级意见冲突?
    我要以更清楚和文明的方式提出我的看法,使对方了解我的观点。
    我会尊重上级,上级一般更有经验,思考更全面,要通过积极沟通找到一个更合适的解决方法。

  • 在完成某项工作时,你认为领导要求的方式不是最好的,自己还有更好的方法,你应该怎么做?
    原则上我会尊重和服从领导的工作安排;同时私底下找机会以请教的口吻,婉转地表达自己的想法,看看领导是否能改变想法;
    如果领导没有采纳我的建议,我也同样会按领导的要求认真地去完成这项工作;
    还有一种情况,假如领导要求的方式违背原则,我会坚决提出反对意见;如领导仍固执己见,我会毫不犹豫地再向上级领导反映。

  • 工作中你难以和同事、上司相处,你该怎么办?
    我会服从领导的指挥,配合同事的工作。
    我会从自身找原因,仔细分析是不是自己工作做得不好让领导不满意,同事看不惯。还要看看是不是为人处世方面做得不好。如果是这样的话 我会努力改正。
    如果我找不到原因,我会找机会跟他们沟通,请他们指出我的不足。有问题就及时改正。
    作为优秀的员工,应该时刻以大局为重,即使在一段时间内,领导和同事对我不理解,我也会做好本职工作,虚心向他们学习,我相信,他们会看见我在努力,总有一天会对我微笑的!

  • 偶像?企业家?
    梅西?除了足球方面的,他有着世界上最宝贵的天赋,谦逊。他沉默地担下所有责任,用实际行动回复质疑和诋毁,以及他个人和职业生涯中的种种挫折,共同让他成长为了这个世界上最坚强的人。
    任正非??一位民族企业家,经历了苦难的人,实干家,尊重教师和科学家。。他的企业即使在最艰难的时刻,也坚持研发投入,有着向上的风气。

  • 如果通过这次面试我们单位录用了你,但工作一段时间却发现你根本不适合这个职位,你怎么办?
    ??
    如果你确实热爱这个职业,那你就要不断学习,虚心向领导和同事学习业务知识和处事经验,了解这个职业的精神内涵和职业要求,力争减少差距;
    你觉得这个职业可有可无,那还是趁早换个职业,去发现适合你的,你热爱的职业,那样你的发展前途也会大点,对单位和个人都有好处。

  • 如果你的工作出现失误,给本公司造成经济损失,你认为该怎么办?
    我本意是为公司努力工作,如果造成经济损失,我认为首要的问题是想方设法去弥补或挽回经济损失。
    如果我无能力负责,希望单位帮助解决;
    分清责任,各负其责,如果是我的责任,我甘愿受罚;如果是一个我负责的团队中别人的失误,作为一个团队,需要互相提携共同完成工作,安慰同事并帮助同事查找原因总结经验。
    总结经验教训,一个人的一生不可能不犯错误,重要的是能从自己的或者是别人的错误中吸取经验教训,并在今后的工作中避免发生同类的错误。检讨自己的工作方法、分析问题的深度和力度是否不够,以致出现了本可以避免的错误。

  • 如果你在这次考试中没有被录用,你怎么打算?
    第一:要敢于面对,面对这次失败不气馁,接受已经失去了这次机会就不会回头这个现实,从心理意志和精神上体现出对这次失败的抵抗力。要有自信,相信自己经历了这次之后经过努力一定能行.能够超越自我;
    第二:善于反思,对于这次面试经验要认真总结,思考剖析,能够从自身的角度找差距。正确对待自己,实事求是地评价自己,辩证的看待自己的长短得失,做一个明白人;
    第三:走出阴影,要克服这一次失败带给自己的心理压力,时刻牢记自己弱点,防患于未然,加强学习,提高自身素质;
    第四:认真工作,回到原单位岗位上后,要实实在在、踏踏实实地工作,三十六行,行行出状元,争取在本岗位上做出一定的成绩;
    第五:再接再厉,成为一名架构师一直是我的梦想,以后如果有机会我仍然后再次参加竞争。

  • 谈谈你对跳槽的看法?
    正常的”跳槽”能促进人才合理流动,应该支持;
    频繁的跳槽对单位和个人双方都不利,应该反对。

  • 你能为我们公司带来什么呢?
    假如你可以的话,试着告诉他们你可以减低他们的费用——“我已经接受过惠普公司一段时间的培训,立刻就可以上岗工作”。
    企业很想知道未来的员工能为企业做什么,求职者应再次重复自己的优势,然后说:“就我的能力,我可以做一个优秀的员工在组织中发挥能力,给组织带来高效率和更多的收益”。企业喜欢求职者就申请的职位表明自己的能力,比如申请营销之类的职位,可以说:“我可以开发大量的新客户,同时,对老客户做更全面周到的服务,开发老客户的新需求和消费。”等等。

  • 你的业余爱好是什么?
    我喜欢足球,我享受和我的队友一起拼尽全力,互相信任,无论输赢一起共进退。在足球比赛中,一个团队的成功不仅仅取决于个别球员的技术水平,更重要的是整个团队的协同合作、沟通和相互理解。(回答提示:找一些富于团体合作精神的)

  • 最喜欢的十本书?

    1. 《三体》- 刘慈欣
    2. 《活着》- 余华
    3. 《围城》- 钱钟书
    4. 《1984》- 乔治·奥威尔
    5. 《动物农场》- 乔治·奥威尔
    6. 《白夜行》- 东野圭吾
    7. 《窗边的小豆豆》- 黑柳彻子
    8. 《苏菲的世界》- 乔斯坦·贾德
    9. 《水浒传》- 施耐庵
    10. 《红楼梦》- 曹雪芹
  • 作为被面试者给我打一下分
    “我认为在这次面试中,我有机会深入了解公司,并且很高兴能够分享我的经验和能力。
    面试官提出的问题涉及到我的专业领域和与公司文化相关的方面,这让我感到这次面试非常有价值。我觉得这是一个很好的机会,展示了我与公司的匹配度,同时也让我更深入地了解了贵公司的期望和文化。
    我期待着有机会为公司做出积极的贡献,如果有进一步的机会,我将非常愿意深入探讨我如何能够在公司中蓬勃发展。”
    (回答提示:试着列出四个优点和一个非常非常非常小的缺点,,可以抱怨一下设施,没有明确责任人的缺点是不会有人介意的)

  • 喜欢这份工作的哪一点?
    ???
    在回答面试官这个问题时可不能太直接就把自己心理的话说出来,尤其是薪资方面的问题,不过一些无伤大雅的回答是不错的考虑,如交通方便,工作性质及内容颇能符合自己的兴趣等等都是不错的答案,不过如果这时自己能仔细思考出这份工作的与众不同之处,相信在面试上会大大加分。

  • 说说你的家庭。
    (企业不喜欢探究个人隐私,而是要了解家庭背景对求职者的塑造和影响。企业希望听到的重点也在于家庭对求职者的积极影响。企业最喜欢听到的是:)
    我的家庭一向很和睦,虽然我的父亲和母亲都是普通人,但是从小,我就看到父母为了家庭起早贪黑,每天勤劳工作,他的行动无形中培养了我认真负责的态度和勤劳的精神。他们也教育我对人诚实、友善,要乐于助人。

  • 就你申请的这个职位,你认为你还欠缺什么?
    回答建议(仅供参考):企业喜欢问求职者弱点,但精明的求职者一般不直接回答。他们希望看到这样的求职者:继续重复自己的优势,然后说:“对于这个职位和我的能力来说,我相信自己是可以胜任的,只是缺乏经验,这个问题我想我可以进入公司以后以最短的时间来解决,我的学习能力很强,我相信可以很快融入公司的企业文化,进入工作状态。”
    企业喜欢能够巧妙地躲过难题的求职者。

  • 你欣赏哪种性格的人?
    诚实、不死板而且容易相处的人、有”实际行动”的人。

  • 你通常如何处理別人的批评?
    沈默是金。不必说什么,否则情况更糟,不过我会接受建设性的批评; 我会等大家冷靜下来再讨论。

  • 你怎样对待自己的失敗?
    我们大家生来都不是十全十美的,我相信我有第二个机会改正我的错误。

  • 你和别人发生过争执吗?你是怎样解决的?
    (千万不要说任何人的过错。应知成功解决矛盾是一个协作团体中成员所必备的能力。考官希望看到你是成熟且乐于奉献的。他们通过这个问题了解你的成熟度和处世能力。)
    在没有外界干涉的情况下,通过妥协的方式解决。

  • 为了做好你工作份外之事,你该怎样获得他人的支持和帮助?
    ???

  • 项目中遇到困难,你怎么办?怎么回答
    ???
    第一种这个要找项目负责人商量、甚至要找产品/运行或其他甲方沟通、
    第二种普遍技术难点的话、参考资料或者和项目负责人商量看看怎么办、
    第三种的话、先自己查资料试着解决、不行就找人其他资讯、、

  • 意向城市,人生发展(以提高自己为主,强化学习,找到一个好的环境)

  • 压薪:如果工资达不到您的预期,还来吗?
    1、感谢hr在面试这几轮的工作和时间
    2、我感觉其实很适合这个岗位了(我就是公司需要的人,不要压我工资完成你的kpi
    3、不管结果如何,希望您能够再帮我争取一下(我也和,,打打招呼,表达诚意,不让您这边为难?
    不直接回答,只表达希望hr能帮我争取,不保证接不接offer??

面 逝 | Java

Java

  • 什么是B/S架构,C/S架构?Java都有那些开发平台?

    1. B/S(Browser/Server),浏览器/服务器程序
    2. C/S(Client/Server),客户端/服务端,桌面应用程序
    3. JAVA SE:主要用在客户端开发
    4. JAVA EE:主要用在web应用程序开发
    5. JAVA ME:主要用在嵌入式应用程序开发
  • JDK, JRE, JVM?
    JDK(Java Development Kit):JDK是Java开发工具包,它是Java开发的完整工具集,包括了Java编译器(javac)、Java虚拟机(JVM)、Java类库等。主要用于Java应用程序的开发,提供了开发、编译、调试和运行Java程序所需的工具。
    JRE(Java Runtime Environment):JRE是Java运行时环境,它是Java应用程序执行的环境,包含了Java虚拟机(JVM)和Java类库。用于在计算机上运行已经编译过的Java应用程序,但不包含用于Java开发的工具,如编译器。
    JVM(Java Virtual Machine):JVM是Java虚拟机,是一个在物理计算机上模拟运行Java字节码(.class)的虚拟机。负责解释和执行Java字节码,通过不同操作系统上的 JVM 解释为该操作系统的机器指令,使得Java程序能够在不同的平台上实现一次编译,到处运行的跨平台特性。

  • Java语言有哪些特点?

    1. 简单易学、有丰富的类库
    2. 面向对象 OOP(Java最重要的特性,让程序耦合度更低,内聚性更高)
      类是对象的抽象,对象是类的具体,类是对象的模板,对象是类的实例
    3. 与平台无关性(JVM是Java跨平台使用的根本)
    4. 可靠安全
    5. 支持多线程
  • 一个java类中包含那些内容?
    属性、方法、内部类、构造方法、代码块。

  • 数据结构?Java的数据结构有那些?Java中有几种数据类型?
    数据结构:计算机保存,组织数据的方式
    java中数据结构有:1.线性表 2.链表 3.栈 4.队列 5.图 6.树
    数据类型有,整形:byte,short,int,long;浮点型:float,double;字符型:char;布尔型:boolean

  • float 和 double.
    两种用于表示浮点数的数据类型。它们之间的主要区别在于精度和存储大小。

    • float是一种单精度浮点数数据类型,通常用于需要小数点的计算,但它不如double类型精确。float类型有7位十进制有效数字,并且它的存储大小是32位(4字节)。
      1
      float myFloat = 3.14f; // 注意 'f'或F 后缀,强制Java编译器将其识别为float类型,否则它会被默认为double
    • double是一种双精度浮点数数据类型,比float类型有更高的精度,通常用于需要更精确计算的情况。double类型有15位十进制有效数字,并且它的存储大小是64位(8字节)。
      1
      double myDouble = 3.141592653589793; // 由于double的精度更高,若无后缀特别指定,Java中的浮点数默认double
  • 如何解决浮点型数据运算出现的误差的问题?float f=3.4;是否正确?
    使用 Bigdecimal类 进行浮点型数据的运算。
    3.4 是双精度数(double),将 double 赋值给 float 属于下转型(down-casting,也称为窄化)会造成精度损失,因此需要强制类型转换 float f =(float)3.4; 或 float f =3.4F

  • short s1 = 1; s1 = s1 + 1;有错吗?short s1 = 1; s1 += 1; 有错吗?
    对于 short s1 = 1; s1 = s1 + 1;由于 1 是 int 类型,因此 s1+1 运算结果也是 int 型,需要强制转换类型才能赋值给 short 型。
    而 short s1 =1; s1 += 1;可正确编译,因为 s1+= 1;相当于 s1 = (short)(s1 + 1);其中有隐含的强制类型转换。

  • int i = 0; count = (i++)+ (i++)+ (i++); 值为?
    count = 0 + 1 + 2 = 3

  • 什么是隐式转换,什么是显式转换?Char类型能不能转成int类型,string类型,double类型?
    显示转换就是类型强转,把一个大类型的数据强制赋值给小类型的数据;隐式转换就是大范围的变量能够接受小范围的数据;隐式转换和显式转换其实就是自动类型转换和强制类型转换。
    Char < int < long < float < double;Char类型可以隐式转成int,double类型,但是不能隐式转换成string;如果char类型转成byte,short类型的时候,需要强转。

  • char 型变量中能不能存贮一个中文汉字?
    可以,因为 Java 中使用的编码是 Unicode(不选择任何特定的编码,直接使用字符在字符集中的编号,这是统一的唯一方法),一个 char 类型占 2 个字节(16 比特),所以放一个中文是没问题的。
    补充:使用 Unicode 意味着字符在 JVM 内部和外部有不同的表现形式,在 JVM内部都是 Unicode,当这个字符被从 JVM 内部转移到外部时(例如存入文件系统中),需要进行编码转换。所以 Java 中有字节流和字符流,以及在字符流和字节流之间进行转换的转换流,如InputStreamReader 和 OutputStreamReader,这两个类是字节流和字符流之间的适配器类,承担了编码转换的任务;对于 C 程序员来说,要完成这样的编码转换恐怕要依赖于 union(联合体/共用体)共享内存的特征来实现了。

  • 数组在java中是一种基本数据类型吗?引用类型?
    在Java中,数组(Array)是一种引用类型,而不是基本数据类型。例如,如果你创建数组:int[] numbers = new int[5];
    这里,numbers是一个引用变量,指向一个包含5个int类型元素的数组对象。这个数组对象本身不是基本数据类型,而是引用类型。数组中的每个元素(在这种情况下是int类型)是基本数据类型。

  • 什么是基本数据类型和引用类型?

    • 基本数据类型:是编程语言中内置的简单数据类型,用于直接存储具体的值。这些值通常是不可变的,即一旦赋值,就不能改变它们的类型。基本数据类型通常包括整形:byte,short,int,long;浮点型:float,double;字符型:char;布尔型:boolean。在Java中,基本数据类型是直接存储值的,它们占用的内存空间是固定的,且不会引用其他对象。
    • 引用类型:与基本数据类型相对,它们不直接存储值,而是存储对内存中对象的引用。这意味着,引用类型的变量实际上是一个指针,指向存储在堆内存中的对象。当你创建一个引用类型的变量(如数组、类、接口等)时,你实际上是在内存中创建了一个对象,并将变量设置为指向这个对象的引用。引用类型包括类(Class)、接口(Interface)、数组(Array)等。
  • Java 中变量是“值传递” 还是 “引用传递”?
    在Java中,所有的变量都是值传递,无论是基本数据类型还是引用类型。

    • 基本数据类型的变量存储的是值本身。当你将一个基本数据类型的值赋给另一个变量时,实际上是复制了这个值。所以,对于基本数据类型,Java确实是通过值来传递的。
      当一个基本类型变量作为方法的参数传递时,传递的是该变量的一个副本。这意味着在方法内部对该副本所做的任何修改都不会影响到原始变量。基本类型变量是存储在栈内存中的,并且它们的值是直接存储的,而不是通过引用。
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      public class Main {  
      public static void main(String[] args) {
      int originalValue = 5;
      modifyValue(originalValue);
      System.out.println(originalValue); // 输出 5,原始值未改变
      }
      public static void modifyValue(int value) {
      value = 10; // 这里修改的是副本的值
      System.out.println(value); // 输出 10,副本的值被修改了
      }
      }
      一旦一个基本类型变量被初始化并存储在栈内存中,你不能直接修改它存储在栈内存中的值。基本类型变量是不可变的(java所有基本类型都不可变?!!),一旦它们被赋值,它们的值就不能被改变。如果想修改其值,必须重新赋一个新的值。这通常是通过声明一个新的变量,或者重新赋值给原来的变量来完成的。
      1
      2
      3
      4
      5
      6
      7
      8
      9
      public class Main {  
      public static void main(String[] args) {
      int number = 5; // number 初始化为 5
      System.out.println("Before modification: " + number); // 输出: Before modification: 5
      // 修改 number 的值
      number = 10; // 重新给 number 赋一个新的值
      System.out.println("After modification: " + number); // 输出: After modification: 10
      }
      }
    • 引用类型的变量实际上存储的是一个指向内存中对象的引用的地址。当你将一个引用类型的变量赋给另一个变量时,你实际上是在复制这个引用地址,而不是对象本身。这意味着两个变量现在指向同一个对象。因此,尽管在引用类型中,传递的是引用(地址),但这并不等同于传统意义上的“引用传递”。
      在Java中,对象本身是通过堆内存来存储的,而引用变量(即对象的引用)是通过栈内存来存储的。当你将一个引用类型的变量赋值给另一个变量时,你实际上是在栈内存中复制了这个引用地址,而不是复制了整个对象。
  • Java创建对象有几种方式?

    1. 使用 new 关键字:
      1
      MyClass obj = new MyClass();
    2. 通过反射机制:
      1
      2
      3
      4
      5
      6
      try {
      Class<?> clazz = Class.forName("MyClass");
      MyClass obj = (MyClass) clazz.newInstance();
      } catch (ClassNotFoundException | InstantiationException | IllegalAccessException e) {
      e.printStackTrace();
      }
    3. 使用 clone() 方法:
      1
      2
      3
      4
      5
      6
      MyClass original = new MyClass();
      try {
      MyClass cloned = (MyClass) original.clone();
      } catch (CloneNotSupportedException e) {
      e.printStackTrace();
      }
    4. 通过反序列化:
      1
      2
      3
      4
      5
      6
      7
      8
      // 假设 MyClass 实现了 Serializable 接口
      ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("object.ser"));
      out.writeObject(new MyClass());
      out.close();

      ObjectInputStream in = new ObjectInputStream(new FileInputStream("object.ser"));
      MyClass obj = (MyClass) in.readObject();
      in.close();
    5. 使用匿名类
    6. 工厂方法
    7. 使用静态工厂方法
  • 面向对象和面向过程的区别。

    1. 面向过程:一种较早的编程思想,顾名思义就是该思想是站着过程的角度思考问题,强调的就是功能行为,功能的执行过程,即先后顺序,而每一个功能我们都使用函数(类似于方法)把这些步骤一步一步实现。使用的时候依次调用函数就可以了。
    2. 面向对象:一种基于面向过程的新编程思想,顾名思义就是该思想是站在对象的角度思考问题,我们把多个功能合理放到不同对象里,强调的是具备某些功能的对象。
      具备某种功能的实体,称为对象。面向对象最小的程序单元是类。面向对象更加符合常规的思维方式,稳定性好,可重用性强,易于开发大型软件产品,有良好的可维护性。
      在软件工程上,面向对象可以使工程更加模块化,实现更低的耦合和更高的内聚。
    3. instanceof 关键字的作用
      instanceof 严格来说是Java中的一个双目运算符,用来测试一个对象是否为一个类的实例,用法为:
      1
      boolean result = obj instanceof Class
      其中 obj 为一个对象,Class 表示一个类或者一个接口,当 obj 为 Class 的对象,或者是其直接或间接子类,或者是其接口的实现类,结果result 都返回 true,否则返回false。注意:编译器会检查 obj 是否能转换成右边的class类型,如果不能转换则直接报错,如果不能确定类型,则通过编译,具体看运行时定。
  • 封装?什么是拆装箱?
    Java面向对象语言,一切操作以对象为基础。对象中封装了属性和操作,使用灵活,数据不被外部修改。封装类在处理集合、泛型、反射等场景中非常有用。
    装箱就是自动将基本数据类型转换为包装器类型(int->Integer);调用方法:Integer的valueOf(int) 方法拆箱就是自动将包装器类型转换为基本数据类型(Integer->int)。

  • int 与 Integer初始值?
    Integer初始值为null,存储在堆内存;int初始值0,存储在栈空间。

  • 抽象
    抽象是将一类对象的共同特征总结出来构造类的过程, 包括数据抽象和行为抽象两方面。抽象只关注对象有哪些属性和行为,并不关注这些行为的细节是什么。

  • 面向对象的特征有哪些方面?

    1. 封装:通常认为封装是把数据和操作数据的方法绑定起来,对数据的访问只能通过已定义的接口。
      • 面向对象的本质就是将现实世界描绘成一系列完全自治、封闭的对象。我们在类中编写的方法就是对实现细节的一种封装;编写一个类就是对数据和数据操作的封装。
      • 优点:
        • 信息隐藏: 允许隐藏对象的内部状态和实现细节,只暴露必要的接口。这样可以保护对象的内部数据不被外部直接访问和修改,减少错误和不一致的状态。
        • 模块化: 将复杂的系统分解为独立的、可管理的模块。每个模块负责特定的功能,使得开发和维护更加简单。
        • 易于维护: 封装使得对象的实现可以独立于其使用者进行更改。只要接口保持不变,内部的修改不会影响到使用该对象的其他部分代码。
    2. 继承:继承是从已有类得到继承信息创建新类的过程.
      • 提供继承信息的类被称为父类(超类、基类);得到继承信息的类被称为子类(派生类)。继承让变化中的软件系统有了一定的延续性,同时继承也是封装程序中可变因素的重要手段。
      • 优点:
        • 代码重用: 继承允许子类继承父类的属性和方法,减少了代码的重复编写,提高了开发效率。
        • 建立层次结构: 继承支持创建一个类的层次结构,这有助于组织和管理具有共同特征的对象。子类可以扩展或覆盖父类的行为,从而实现更具体的功能。
        • 多态的基础: 继承是多态实现的基础。通过继承,可使用父类类型的引用来操作子类对象,为多态提供了可能。
    3. 多态性:是指允许不同子类型的对象对同一消息作出不同的响应。
      • 简单的说就是用同样的对象引用调用同样的方法但是做了不同的事情。多态性分为编译时的多态性和运行时的多态性。
      • 优点:
        • 接口统一: 允许不同的子类对象通过统一的接口进行操作,简化了客户端代码的复杂性,可以处理不同类型的对象而不需要知道对象的具体类型。
        • 扩展性: 提高了代码的扩展性。如果需要引入新的子类,只需确保它遵循现有的接口,而无需修改已有的代码。
        • 动态绑定: 在Java等语言中,多态允许在运行时动态决定对象的具体类型和调用的方法。这种动态绑定提供了更大的灵活性和更强的表达能力。
      • 方法重载(overload)实现的是编译时的多态性(也称为前绑定),编译器在编译时根据参数类型和数量确定哪个方法
        1
        2
        public int add(int a, int b)  return a + b;     
        public double add(double a, double b) return a + b;
      • 方法重写(override)实现的是运行时的多态性(也称为后绑定)。运行时的多态是面向对象最精髓的东西,要实现多态需要做两件事:
        1、方法重写(子类继承父类并重写父类中已有的或抽象的方法);
        2、对象造型(用父类型引用引用子类型对象,这样同样的引用调用同样的方法就会根据子类对象的不同而表现出不同的行为)。
        运行时的多态性主要通过方法重写和对象的多态性实现,运行时根据对象的实际类型来调用相应的方法。这意味着在运行时才能确定调用哪个方法,这取决于对象的实际类型。编译时,Java编译器只知道引用变量的类型,而不知道它所引用的对象的实际类型。因此,方法调用的确定(即分派)被推迟到运行时。
        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        19
        20
        class Animal {  
        public void sound() {
        System.out.println("The animal makes a sound");
        }
        }
        class Dog extends Animal {
        @Override
        public void sound() {
        System.out.println("The dog barks");
        }
        }
        public class Main {
        public static void main(String[] args) {
        // 父类引用指向子类对象
        Animal animal = new Dog();
        // 当调用 animal.sound()时,尽管 animal 是 Animal 类型的引用,
        // 但运行时系统会检查 animal 实际引用的对象类型(即Dog)并调用该对象类型(Dog)中的 sound 方法
        animal.sound();
        }
        }
    4. 总的来说,封装、继承和多态共同为面向对象编程提供了一种强大的方式来构建复杂和可扩展的软件系统。它们使得代码更加清晰、易于维护和扩展,同时也提高了代码的复用性和可读性。极大地提高了软件开发的效率、可维护性和可扩展性。
  • 重载和重写的区别

    • 重写 Override:在子类中把父类本身有的方法重新写一遍。子类继承了父类原有的方法,但有时子类并不想原封不动的继承父类中的某个方法,所以在方法名,参数列表,返回类型(除过子类中方法的返回值是父类中方法返回值的子类时)都相同的情况下, 对方法体进行修改或重写,这就是重写。被重写方法比父类更好访问(即子类函数的访问修饰权限不能少于父类的),不能比父类被重写方法声明更多的异常(里氏代换原则)。
      1、发生在父类与子类之间
      2、方法名,参数列表,返回类型(除过子类中方法的返回类型是父类中返回类型的子类)必须相同
      3、访问修饰符的限制一定要大于被重写方法的访问修饰符(public>protected>default>private),子类重写方法应该要比父类方法具有相同或更广泛的可见性,子类对外暴露的接口比父类更多,这符合面向对象编程中的开闭原则(对扩展开放,对修改关闭),提供了更多的灵活性。
      4、重写方法一定不能抛出新的检查异常或者比被重写方法申明更加宽泛的检查型异常
    • 重载(Overload)在一个类中,同名的方法如果有不同的参数列表(参数类型不同、参数个数不同甚至是参数顺序不同)则视为重载。同时,重载对返回类型没有要求,可以相同也可以不同,但不能通过返回类型是否相同来判断重载。
      1、重载Overload是一个类中多态性的一种表现
      2、重载要求同名方法的参数列表不同(参数类型,参数个数甚至是参数顺序)
      3、重载的时候,返回值类型可以相同也可以不相同。无法以返回型别作为重载函数的区分标准
  • 动态绑定?
    是指在运行时确定方法调用的具体实现。它是 Java 中的一种多态性表现,通过动态绑定,程序可以根据对象的实际类型来决定调用哪个方法实现。
    动态绑定的实现依赖于 Java 中的继承和重写机制。当子类重写父类的方法时,如果父类引用指向子类对象,并且调用被重写的方法时,将根据实际对象类型决定调用哪个版本的方法,这就是动态绑定。使得 Java 具有更强大的多态性,可以根据对象的实际类型来调用不同的方法实现,增强了程序的灵活性和可扩展性。

  • public,private,protected,以及不写(默认)时的区别?
    在Java中,访问修饰符用于指定类、变量、方法和构造函数的访问权限。它们有四种级别:public、private、protected和默认(无修饰符)。这些修饰符决定了哪些其他类可以访问特定的成员。Java 中,外部类的修饰符只能是 public 或默认,类的成员(包括内部类)的 修饰符可以是以上四种。

    • public声明类的成员时,它可以在任何其他类中(同一包中/不同的包中)被访问。
    • private成员只能在其自己的类中被访问。它不能被任何其他类(即使是同一包中的类)访问。
    • protected成员可以在其自己的类、同一包中的其他类以及任何子类(无论子类在哪里)中被访问。
      它提供了一种有限的访问,比private更开放,但仍然限制了对类的直接访问。
      这在创建API或库时特别有用,因为你可能希望允许子类访问某些方法或变量,但不希望它们被其他不相关的类访问。
    • 默认(无修饰符)default:当一个类的成员没有明确的访问修饰符时,它只能在同一包中的其他类中被访问。对于同一个包中的其他类相当于 public,对于不是同一个包中的其他类相当于 private。这意味着它不能被其他包中的类直接访问,但可以被同一包中的任何类访问。这是比private更开放,但比protected和public更受限的访问级别。
  • String 是最基本的数据类型吗?
    不是。Java 中的基本数据类型只有 8 个;除了基本类型 剩下的都是引用类型,Java 5 以后引入的枚举类型也算是一种比较特殊的引用类型。
    String 是不可变类。不可变类指的是无法修改对象的值,当你创建一个 String 对象之后,这个对象就无法被修改。像执行s += “a”; 返回的是一个新的 String 对象,老的 s 指向的对象不会发生变化,只是 s 的引用指向了新的对象而已。“不可变”最主要的好处就是安全,在多线程环境下也是线程安全的;然后,配合常量池可以节省内存空间,且获取效率也更高(如果常量池里面已经有这个字符串对象了,就不需要新建,直接返回即可)。

  • String的初始值是?
    NULL.

  • String 常见题.

    • “字面量创建字符串”:yesA 是一个引用指向了堆里面的字符串常量池里的对象 a。如果字符串常量池已经有了 abb,那么直接返回其引用,如果没有 abb,则会创建 abb 对象,然后返回其引用。
      1
      2
      3
      String yesA = "abb"; 
      String yesB = "abb";
      System.out.printIn(yesA == yesB); // true
    • “new String创建字符串”:先判断字符串常量池里面是否有 abb,如果没有 abb 则创建一个 abb,然后会在堆内存里面创建一个对象 abb,返回堆内存对象 abb 的引用,也就是说返回的不是字符串常量池里面的 abb
      1
      2
      3
      yesA = new String("abb"); 
      yesB = new String("abb");
      System.out.println(yesA == yesB); // false
    • intern():判断下 yesB 引用指向的值在字符串常量里面是否有,如果没有就在字符串常量池里面新建一个 aaabbb 对象,返回其引用,如果有则直接返回引用。
      1
      2
      3
      4
      5
      String yesA = "aaabbb";    // 通过字面量定义了 yesA,在字符串常量池里创建 aaabbb 对象,返回其引用
      String yesB = new String("aaa") + new String("bbb"); // 返回堆内的引用
      String yesC = yesB.intern();
      System.out.println(yesA == yesB); // false
      System.out.println(yesA == yesC); // true
    • JDK 1.6 时,字符串常量池是放置在永久代的;
      ??? JDK 1.7 之后字符串常量池是放在堆内的
      1
      2
      3
      4
      5
      String yesB = new String("aaa") + new String("bbb");  // 此时,堆内会新建一个 aaabbb 对象,字符串常量池里不会创建,因为并没有出现 aaabbb 这个字面量。
      String yesC = yesB.intern(); // 1.7 之后,如果堆内已经存在某个字符串对象的话,再调用 intern 此时不会在字符串常量池内新建对象,而是直接保存这个引用然后返回。
      String yesA = "aaabbb"; // yesA 得到的引用与 yesC 和 yesB 一致,都指向堆内的 aaabbb 对象。
      System.out.println(yesA == yesB); // true
      System.out.println(yesA == yesC); // true
  • String 连接?

    1. 使用 + 运算符:字符串连接最简单的方法,但在大量连接操作时可能效率较低,因为它会生成多个临时的字符串对象。
    2. 使用 StringBuilder:可变的字符序列,适用于需要频繁进行字符串连接的场景。append 方法用于添加字符串内容,最后使用 toString 方法获取最终的字符串。适合单线程环境下使用。
    3. 使用 StringBuffer:与 StringBuilder 类似,也是可变的字符序列,但不同之处在于 StringBuffer 是线程安全的,适用于多线程环境。
  • String,StringBuffer 和 StringBuilder 的区别是什么?
    String是只读字符串,它并不是基本数据类型,而是一个对象。从底层源码来看是一个final类型的字符数组,所引用的字符串不能被改变,一经定义,无法再增删改。每次对String的操作都会生成新的String对象。
    StringBuffer与StringBuilder都继承了AbstractStringBulder类,而AbtractStringBuilder又实现了CharSequence接口,两个类都是用来进行字符串操作的。在做字符串拼接修改删除替换时,效率比string更高。
    StringBuffer是线程安全的,Stringbuilder是非线程安全的。所以Stringbuilder比stringbuffer效率更高,StringBuffer的方法大多都加了synchronized关键字

  • String类的常用方法有那些?
    charAt:返回指定索引处的字符
    indexOf():返回指定字符的索引
    replace():字符串替换
    trim():去除字符串两端空白
    split():分割字符串,返回一个分割后的字符串数组
    getBytes():返回字符串的byte类型数组
    length():返回字符串长度
    toLowerCase():将字符串转成小写字母
    toUpperCase():将字符串转成大写字符
    substring():截取字符串
    format():格式化字符串
    equals():字符串比较

  • 分割字符串常用的方法。

    1. split(String regex) 方法: 使用正则表达式来分割字符串。返回一个字符串数组,包含分割后的子字符串。
      1
      2
      3
      String input = "apple,orange,banana";
      String[] fruits = input.split(",");
      // fruits 数组: {"apple", "orange", "banana"}
    2. split 方法配合正则表达式的转义字符: 如果分隔符是正则表达式的元字符,需要进行转义。
      1
      2
      3
      String input = "apple.orange.banana";
      String[] fruits = input.split("\\.");
      // fruits 数组: {"apple", "orange", "banana"}
    3. substringindexOf 方法: 手动截取子字符串。
      1
      2
      3
      4
      5
      String input = "apple,orange,banana";
      int commaIndex = input.indexOf(",");
      String firstPart = input.substring(0, commaIndex);
      String secondPart = input.substring(commaIndex + 1);
      // firstPart: "apple", secondPart: "orange,banana"
    4. 使用 PatternMatcher 类: 进行更复杂的正则表达式匹配和分割。
      1
      2
      3
      4
      5
      6
      7
      import java.util.regex.Pattern;
      import java.util.regex.Matcher;

      String input = "apple,orange;banana";
      Pattern pattern = Pattern.compile("[,;]");
      String[] fruits = pattern.split(input);
      // fruits 数组: {"apple", "orange", "banana"}
  • JDK9为什么要将 String 的底层实现由 char[] 改为 byte[]?
    jdk中字符用utf-16编码(UTF-16是Unicode的一种实现方式,它使用16位的编码单元来表示一个字符),一个字符char要占用2个字节;但是对于由纯英文字符和ascii字符组成的字符串,只需要一个字节就可以表示所有ascii字符,使用 byte[] 可以节省一半空间。
    只有在需要存储非ascii字符时,才会使用char[]

  • Unicode 和 UTF-8 的区别:

    1. Unicode(统一码):
      Unicode 是一种字符集(Character Set),用于定义字符和字符编码之间的对应关系。
      Unicode 中包含了世界上几乎所有的字符,包括各种语言的文字、符号、表情符号等。
      Unicode 采用 16 位(2 字节)来表示一个字符,因此可以表示的字符范围很广泛,共有 65536 个码位(Code Point)。
    2. UTF-8(Unicode Transformation Format-8):
      UTF-8 是一种变长的字符编码方式,用于将 Unicode 字符编码成字节序列。
      UTF-8 的最大特点是兼容 ASCII 码,即 ASCII 码中的字符(0-127)与 UTF-8 中的编码是相同的,这使得 UTF-8 在 Web 上具有广泛的应用。
      UTF-8 使用 1 到 4 个字节来表示一个 Unicode 字符,根据字符的不同而变化,因此可以灵活地表示各种字符,包括中文、日文、韩文等等。
  • ++i与i++的区别

    • i++:先赋值,后计算;
    • ++i:先计算,后赋值
    • 在JVM层面,这两个操作的实现是通过指令集中的不同指令来完成的。Java虚拟机中的字节码指令包含 iinc 指令用于递增局部变量的值。这两种递增操作在底层都是通过 iinc 指令实现的,但在具体的使用上有一些差异。
      1
      2
      3
      4
      getstatic i // 获取静态变量i的值
      iconst_1 // 准备常量1
      iadd // 自增
      putstatic i // 将修改后的值存入静态变量i
      1
      2
      3
      4
      getstatic i // 获取静态变量i的值
      iconst_1 // 准备常量1
      isub // 自减
      putstatic i // 将修改后的值存入静态变量i
  • count += –count; 值为?
    1、 --count 表示先对 count 执行减一操作,然后将结果赋给 count
    2、 然后将 count 的当前值(执行了减一操作后的值)加上 count 的当前值,再将结果赋给 count

  • a=a+b 与 a+=b 有什么区别吗?

    • += 操作符会进行隐式自动类型转换,此处a+=b隐式的将加操作的结果类型强制转换为持有结果的类型,
      而a=a+b则不会自动进行类型转换.如:
      1
      2
      3
      4
      5
      6
      byte a = 127, b = 127;
      b = a + b; // 报编译错误:cannot convert from int to byte
      b += a;
      short s1= 1;
      s1 = s1 + 1; // 编译器会报错.short类型在进行运算时会自动提升为int类型,也就是说 s1+1 的运算结果是int类型,而s1是short类型,.
      s1 += 1;
  • &和&&的区别
    &是位运算符。&&是布尔逻辑运算符
    在进行逻辑判断时用&处理的前面为false后面的内容仍需处理,用&&处理的前面为false不再处理后面的内容。

  • 200 + null 值为?
    Java 中,如果对一个整数和 null 值进行加法运算,会导致编译错误。在运行时,如果存在 null 值参与运算,会抛出 NullPointerException 异常。

  • (-10) % (-3) = ?

    1. 如果被取模数为正数,结果的符号与被取模数相同。10 = 3 * 3 + 1;
    2. 如果被取模数为负数,结果的符号与除数相同。 (-10) = (-3) * 3 + (-1); 所以 (-10) % (-3) = -1。
  • Java 常用的类,包,接口。
    类:BufferedReader BufferedWriter FileReader FileWirter String Integer
    常用的包:java.lang java.awt java.io java.util java.sql Java.net Java.math
    常用的接口:Remote List Map Document NodeList

  • Object类常用方法有那些?
    Equals
    Hashcode
    toString
    wait
    notify
    clone
    getClass

  • equals与==的区别

    • ==:比较的是变量(栈)内存中存放的对象的(堆)内存地址,用来判断两个对象的地址是否相同,即是否是指相同一个对象。比较的是真正意义上的指针操作。
      1、比较的是操作符两端的操作数是否是同一个对象。
      2、两边的操作数必须是同一类型的(可以是父子类之间)才能编译通过。
      3、比较基本数据类型的 == 操作符直接比较它们的值,值相等则为true。如:int a=10 与 long b=10L 与 double c=10.0都是相同的(为true),因为他们都指向地址为10的堆。而对于 Integer a =10 与 Long b = 10L, 使用 == 比较的是对象的引用而不是值,结果为 false。
    • equals:用来比较的是两个对象的内容是否相等,由于所有的类都是继承自java.lang.Object类的,所以适用于所有对象,如果没有对该方法进行覆盖的话,调用的仍然是Object类中的方法,而Object中的equals方法返回的却是==的判断。
  • equals()hashCode() 方法。为什么重写 equals() 时通常也需要重写 hashCode()

    1. equals() 方法:用于比较两个对象是否在逻辑上相等。默认实现是比较对象的内存地址(相当于 ==),即两个对象是否是同一个对象。在实际应用中,一般需要根据对象的业务含义重写该方法,比较对象的实际内容。
    2. hashCode() 方法:用于获取对象的哈希码,返回一个整数。哈希码是一种用于快速查找的技术,通常在集合(如 HashMap、HashSet)中用到。它可以帮助确定对象在哈希表中的存储位置,提高查找的效率。默认实现是c++编写的native方法,基于对象的内存地址生成哈希码。在实际应用中,一般需要在类中重写该方法,以便相等的对象具有相同的哈希码。
      但是,不是同一个对象,使用hashCode()返回的int值(取值范围2^32)也可能相等,即发生了hash冲突。
    3. 关系:在使用哈希表的集合中,hashCode()equals() 之间存在一定的关系。如果两个对象通过 equals() 方法比较相等,它们的 hashCode() 应该返回相同的值。这是为了保持一致性,使得相等的对象在哈希表中能够正确地识别和处理。确保相等的对象具有相同的哈希码,从而使得在集合中正确地处理相等性。
      如果两个对象通过 equals() 方法比较相等,但它们的 hashCode() 不相等,那么当放入哈希表等集合中时,它们将被视为不同的对象。这可能导致哈希表中存在相等的对象,破坏了集合的一致性。
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      public class MyClass {
      private int id;
      private String name;
      // Constructors, getters, setters...
      @Override
      public boolean equals(Object obj) {
      if (this == obj) return true;
      if (obj == null || getClass() != obj.getClass()) return false;
      MyClass myClass = (MyClass) obj;
      return id == myClass.id && Objects.equals(name, myClass.name);
      }

      @Override
      public int hashCode() {
      return Objects.hash(id, name);
      }
      }
  • 有没有可能两个不相等的对象有相同的hashcode?
    有可能,即产生hash冲突。当hash冲突产生时,一般有以下几种方式来处理:

    1. 拉链法:每个哈希表节点都有一个next指针,多个哈希表节点可以用next指针构成一个单向链表,被分配到同一个索引上的多个节点可以用这个单向链表进行存储.
    2. 开放定址法:一旦发生了冲突,就去寻找下一个空的散列地址,只要散列表足够大,空的散列地址总能找到,并将记录存入
    3. 再哈希:又叫双哈希法,有多个不同的Hash函数.当发生冲突时,使用第二个,第三个….等哈希函数计算地址,直到无冲突.
  • java中有没有指针?java中是值传递引用传递?
    有指针,但是隐藏了,开发人员无法直接操作指针,由jvm来操作指针
    java都是值传递,对于基本数据类型,传递是值的副本,而不是值本身。对于对象类型,传递是对象的引用,当在一个方法操作操作参数的时候,其实操作的是引用所指向的对象。

  • 构造方法 constructor

    • 构造方法是Java类中的一种特殊方法(可省略),用于在创建对象时进行初始化操作。
      构造方法的名称必须与类名相同,并且没有返回类型,包括 void。构造方法通常用于设置对象的初始状态,为对象的属性赋初值或执行其他初始化任务。
    • 方法也可以与class同名,区别在于方法必须要有void或具体返回值类型;
    • 一个类可以拥有多个构造方法,只要它们的参数列表不同。这称为构造方法的重载。
      1
      2
      3
      4
      5
      6
      7
      public class MyClass {
      private int value;
      // 无参构造方法
      public MyClass() {this.value = 0;}
      // 有参构造方法
      public MyClass(int value) {this.value = value;}
      }
    • 构造方法能不能显式调用?
      不能,构造方法当成普通方法调用,只有在创建对象的时候它才会被系统调用
    • 构造方法能不能重写,重载?
      可以重载,但不能重写。
  • Java中有各种不同的类和代码块类型,让我们逐个解释它们:

    1. 普通类:是最基本的类类型,用于创建对象。它可以包含字段、方法、构造方法等。
      1
      2
      3
      public class MyClass {
      // Fields, methods, constructors, etc.
      }
    2. 构造方法:是包含在类中的一组语句块,没有使用任何关键字。它在对象每一次创建时执行,可以用于初始化对象。
      构造代码块是定义在类中的,不带任何修饰符(例如public、private等)。它在每次创建对象时都会执行,执行的时机在构造器调用之前。与实例初始化块不同,构造代码块不能被单独调用。
      1
      2
      3
      4
      5
      6
      7
      8
      9
      public class MyClass {
      MyClass() {
      // 构造器
      }
      {
      // 普通代码块
      // 在对象创建时执行
      }
      }
    3. 内部类:内部类是定义在另一个类内部的类。它有访问外部类成员的权限,并且可以用于实现一些封装和逻辑组织。
      1
      2
      3
      4
      5
      public class OuterClass {
      class InnerClass {
      // 内部类
      }
      }
    4. 外部类: 外部类是普通的顶级类,不嵌套在其他类中。
      1
      2
      3
      public class OuterClass {
      // 外部类
      }
    5. 静态代码块: 使用 static 关键字,包含在类中,用于在类加载时执行初始化操作。它仅执行一次。
      1
      2
      3
      4
      5
      6
      public class MyClass {
      static {
      // 静态代码块
      // 在类加载时执行一次
      }
      }
    6. 静态内部类:是定义在另一个类内部的类,使用 static 修饰。与非静态内部类不同,它不依赖于外部类的实例。
      1
      2
      3
      4
      5
      public class OuterClass {
      static class StaticInnerClass {
      // 静态内部类
      }
      }
  • 静态 static

    1. 静态变量(Static Variables):被声明为 static 的成员变量,属于类而不是类的实例。它被所有类的实例共享,只有一个副本存在于内存中。
      1
      2
      3
      4
      5
      public class Counter {
      // 私有静态变量属于类而不属于类的实例,并且只能在类的内部访问
      private static int count = 0;
      public static void increment() { count++; }
      }
    2. 静态方法(Static Methods):被声明为 static 的方法,它不需要实例化类就可以直接通过类名调用。静态方法不能访问非静态成员,也无法使用 this 关键字。
      静态方法凭什么不能访问成员方法:因为成员方法属于对象实例,静态方法属于类本身,静态方法第一次加载(方法区)的时候还没有对象(堆),也就无法调用成员方法
    3. 静态代码块(Static Blocks):是包含在类中的静态块,它在类加载时执行,并且只执行一次。通常用于初始化静态变量或执行一些静态的初始化操作。
    4. 静态内部类(Static Inner Classes):在类中使用 static 关键字修饰的内部类。静态内部类与外部类实例无关,可以直接通过外部类名访问。
      在使用静态成员时需要注意,它们的生命周期与类的生命周期相同,当类加载时会被初始化。静态成员属于类而不是对象,在合适的场景下能提供便利和效率。然而,过度使用静态成员可能会导致耦合度高和难以测试等问题,因此需要根据实际情况慎重使用。
  • 内部类与静态内部类的区别?

    • 静态内部类相对与外部类是独立存在的,在静态内部类中无法直接访问外部类中变量、方法。如果要访问的话,必须要new一个外部类的对象,使用new出来的对象来访问。但是可以直接访问静态的变量、调用静态的方法;
    • 普通内部类作为外部类一个成员而存在,在普通内部类中可以直接访问外部类属性,调用外部类的方法。
    • 如果外部类要访问内部类的属性或者调用内部类的方法,必须要创建一个内部类的对象,使用该对象访问属性或者调用方法。
    • 如果其他的类要访问普通内部类的属性或者调用普通内部类的方法,必须要在外部类中创建一个普通内部类的对象作为一个属性,外同类可以通过该属性调用普通内部类的方法或者访问普通内部类的属性。
    • 如果其他的类要访问静态内部类的属性或者调用静态内部类的方法,直接创建一个静态内部类对象即可。
  • 静态变量、静态代码块、普通代码块和构造方法的执行顺序?
    执行顺序可以总结为:静态变量(按定义顺序初始化) -> 静态代码块(按定义顺序执行) -> 普通代码块(对象实例化时按照定义顺序执行) -> 构造方法。

    1. 静态变量(静态成员变量):在类加载时按照定义的顺序依次执行初始化,不论该变量在类中定义的位置如何,只会初始化一次。
    2. 静态代码块(Static Blocks):静态代码块在类加载时执行,优先于普通代码块和构造方法。静态代码块只会执行一次。
    3. 构造代码块(普通初始化块):构造代码块在对象实例化时执行,在构造方法之前执行。每次创建对象都会执行一次。
    4. 构造方法(Constructor):构造方法在对象创建时执行,用于初始化对象。构造代码块执行完毕后执行。
  • 子类继承父类,且都包含静态方法、构造方法,那么静态变量、静态代码块、普通代码块和构造方法的执行顺序?
    顺序:父类静态方法 -> 父类静态代码块 -> 子类静态方法 -> 子类静态代码块 -> 父类构造代码块 -> 父类构造方法 -> 子类构造代码块 -> 子类构造方法

  • final在java中的作用,有哪些用法?

    1. 被final修饰的类不可以被继承
    2. 被final修饰的方法不可以被重写
    3. 被final修饰的变量不可以被改变.如果修饰引用,那么表示引用不可变,引用指向的内容可变.
    4. 被final修饰的方法,JVM会尝试将其内联,以提高运行效率
    5. 被final修饰的常量,在编译阶段会存入常量池中.
      除此之外,编译器对final域要遵守的两个重排序规则更好:在构造函数内对一个final域的写入,与随后把这个被构造对象的引用赋值给一个引用变量,这两个操作之间不能重排序初次读一个包含fifinal域的对象的引用,与随后初次读这个fifinal域,这两个操作之间不能重排序
  • Java中的继承是单继承还是多继承?
    类是不支持多继承的,只能有一个父类,
    类可以实现多个接口,这样就可以达到类对多个接口的”多继承”效果。接口(interface)是一种特殊的抽象类,它可以被类实现(implements)而不是被继承(extends)。接口中定义了一组抽象方法和常量,实现该接口的类必须实现接口中定义的所有抽象方法,并且可以拥有自己的字段和方法。

  • super() 与 this() 表示什么?

    • super表示当前类的父类对象,This表示当前类的对象。super()this() 都是特殊的方法调用语句,用于调用构造器,并且都只能在构造器中使用,不能在普通方法中使用。另外,调用构造器时不能形成循环调用,即不能在同一个构造器中同时调用 super()this()
    • super():用于调用父类的构造器。在子类的构造器中使用 super() 可以显式调用父类的构造器,并且必须作为子类构造器的第一条语句出现。如果子类构造器没有显式调用 super(),则会默认调用父类的无参构造器。
      1
      2
      3
      4
      5
      6
      public class Child extends Parent {
      public Child() {
      super(); // 调用父类的无参构造器
      System.out.println("Child constructor");
      }
      }
    • this():用于调用当前类的其他构造器。在一个类的构造器中使用 this() 可以调用同一类中的其他构造器,并且必须作为构造器的第一条语句出现。
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      public class MyClass {
      private int value;

      public MyClass() {
      this(0); // 调用当前类的带参构造器
      System.out.println("Default constructor");
      }

      public MyClass(int value) {
      this.value = value;
      System.out.println("Parameterized constructor");
      }
      }
  • 抽象类(Abstract Class):

    1. 特点:抽象类是一种不能被实例化的类,通常用于定义其他类的结构和行为。它可以包含抽象方法(只有方法签名,没有具体实现),以及普通的方法和字段。一个类只能继承一个抽象类。可以包含构造函数,可以有访问修饰符(public、private、protected)的方法。子类必须实现抽象类中的所有抽象方法,除非子类也是抽象类。
    2. 使用场景:当需要创建一个类,并在其中定义一些方法的行为,但不希望该类被实例化时,可以使用抽象类。抽象类也适合用于在类层次结构中作为其他类的基类,提供通用方法和字段,而具体实现交给其子类。
    3. 抽象方法的方法体不需要使用 {},抽象方法是指没有具体实现的方法。只需在方法签名后面加上分号即可,不需要提供方法体。
      1
      2
      3
      4
      5
      6
      7
      8
      public abstract class AbstractClass {
      // 抽象方法,没有方法体
      public abstract void abstractMethod();
      // 具体方法,有方法体
      public void concreteMethod() {
      System.out.println("This is a concrete method.");
      }
      }
  • 普通类与抽象类有什么区别?
    普通类不能包含抽象方法,抽象类可以包含抽象方法;
    抽象类不能直接实例化,普通类可以直接实例化

  • 抽象的方法是否可同时是静态的,是否可同时是本地方法(native),是否可同时被 synchronized修饰?
    都不能。抽象方法需要子类重写,而静态的方法是无法被重写的,因此二者是矛盾的。本地方法是由本地代码(如 C 代码)实现的方法,而抽象方法是没有实现的,也是矛盾的。synchronized 和方法的实现细节有关,抽象方法不涉及实现细节,因此也是相互矛盾的。

  • abstract 和 final 同时用来修饰同一个类或方法?
    如果一个类使用了abstract修饰符,表示这个类是抽象类,不能被实例化,可以包含抽象方法。
    如果一个类使用了final修饰符,表示这个类是最终类,不能被其他类继承。
    如果一个方法使用了public abstract修饰符,表示这个方法是抽象方法,只有声明没有实现,需要在子类中实现。
    所以,abstract final不能同时修饰类或方法。public abstract final也是不符合Java语法的组合。需要根据具体的需求和设计来选择适当的修饰符。

  • 是否可以从一个静态方法内部发出对非静态方法的调用?
    不可以,静态方法只能访问静态成员,因为非静态方法的调用要先创建对象,在调用静态方法时可能对象并没有被初始化。

  • 接口(Interface):

    1. 特点:接口是一种完全抽象的类别,其中只包含方法的签名,但没有方法的实际实现。类可以实现多个接口,但接口不能包含字段或非抽象方法(在Java 8之后,引入了默认方法和静态方法)。接口就是某个事物对外提供的一些功能的声明,是一种特殊的java类,接口弥补了java单继承的缺点。
    2. 使用场景:当不同类需要共享某些行为,但它们属于不同的类层次结构时,接口是一个很好的选择。接口允许类定义一组规范,以确保实现类必须提供接口中定义的所有方法。可以使用接口来实现多态性,允许不同的类实现相同的接口并具有不同的行为。
    3. 接口有什么特点?
      接口中声明全是public static final修饰的常量
      接口中所有方法都是抽象方法
      接口是没有构造方法的
      接口也不能直接实例化
      接口可以多继承
  • 接口与抽象类!?

    • 接口和抽象类都是为了实现代码的重用和提供一致的编程接口而设计的。然而,接口更多地用于定义规范和合同,以确保实现类提供特定的行为,而抽象类更多地用于提供一些通用的方法和行为实现。
      • 抽象类和接口都用于实现多态性和提供一致的编程接口。它们通常用于大型项目中的类层次结构设计和代码组织。
      • 在设计框架或库时,接口是一个有用的工具,因为它可以定义规范和标准,并允许用户通过实现接口来提供自定义行为。
      • 抽象类用于将一些通用方法和字段提取到一个父类中,以便子类可以继承和共享这些功能。
    • 抽象类和接口的区别?
      • 抽象类:1. 抽象方法,只有行为的概念,没有具体的行为实现。使用abstract关键字修饰,没有方法体。子类必须重写这些抽象方法。2. 包含抽象方法的类,一定是抽象类。3. 抽象类只能被继承,一个类只能继承一个抽象类。
      • 接口:1. 全部的方法都是抽象方法,属性都是常量 2. 不能实例化,可以定义变量。3. 接口变量可以引用具体实现类的实例 4. 接口只能被实现,一个具体类实现接口,必须实现全部的抽象方法 5. 接口之间可以多实现 6. 一个具体类可以实现多个接口,实现多继承现象
    • 抽象类和接口有什么异同?
      抽象类和接口都不能够实例化,但可以定义抽象类和接口类型的引用。一个类如果继承了某个抽象类或者实现了某个接口都需要对其中的抽象方法全部进行实现,否则该类仍然需要被声明为抽象类。接口比抽象类更加抽象,因为抽象类中可以定义构造器,可以有抽象方法和具体方法,而接口中不能定义构造器而且其中的方法全部都是抽象方法。抽象类中的成员可以是 private、默认、protected、public 的,而接口中的成员全都是 public 的。抽象类中可以定义成员变量,而接口中定义的成员变量实际上都是常量。有抽象方法的类必须被声明为抽象类,而抽象类未必要有抽象方法。
  • 接口是否可继承接口?抽象类是否可实现接口?抽象类是否可继承具体类?
    接口可以继承接口 ,而且支持 多重继承 。 抽象类 可以 实现接口 , 抽象类 可继承 具体类 也可以 继承抽象类 。

  • Anonymous Inner Class(匿名内部类)是否可以继承其它类?是否可以实现接口?
    可以继承其他类或实现其他接口,在 Swing 编程和 Android 开发中常用此方式来实现事件监听和回调。

  • 内部类可以引用它的包含类(外部类)的成员吗?有没有什么限制?
    一个内部类对象可以访问创建它的外部类对象的成员,包括私有成员。

  • 匿名类,匿名内部类。
    Java中的两个相关但不同的概念,它们通常用于创建临时的、一次性的类实例。

    • 匿名类:
      1. 概念: 匿名类是指没有明确命名的类,通常用于创建一个实现某个接口或继承某个类的对象。
      2. 语法: 匿名类的语法形式是通过 new 关键字创建一个对象的同时实现接口或继承类,并在花括号内定义类的实现。
        1
        2
        3
        SomeInterface obj = new SomeInterface() {
        // 匿名类的实现
        };
      3. 使用场景: 匿名类通常用于创建简单的、一次性的类实例,不需要为其定义专门的类名。
    • 匿名内部类:
      1. 概念: 匿名内部类是指定义在其他类内部、没有类名的类。通常使用它来实现接口或继承类,并在类的内部进行实现。
      2. 语法: 匿名内部类的语法形式与匿名类相似,但它通常在其他类的方法内部定义,而不是在类的成员变量或其他地方。
        1
        2
        3
        4
        5
        6
        7
        public class SomeClass {
        public void doSomething() {
        SomeInterface obj = new SomeInterface() {
        // 匿名内部类的实现
        };
        }
        }
      3. 使用场景: 匿名内部类通常用于在方法内部创建一个实现某个接口或继承某个类的临时对象,它有助于简化代码结构,避免为一次性的需求专门定义一个新的类。
        总体而言,匿名类和匿名内部类都是用于创建临时的、一次性的类实例,通常在需要实现某个接口或继承某个类的情况下使用。在Java中,Lambda 表达式的引入也提供了一种更简洁的方式来实现函数接口的匿名类。
  • Java的四种引用,强弱软虚

    • 强引用:是平常中使用最多的引用,强引用在程序内存不足(OOM)的时候也不会被回收
      1
      String str = new String("str");
    • 软引用:在程序内存不足时,会被回收,使用方式:
      1
      2
      3
      // 注意:wrf这个引用也是强引用,它是指向SoftReference这个对象的,
      // 这里的软引用指的是指向new String("str")的引用,也就是SoftReference类中T
      SoftReference<String> wrf = new SoftReference<String>(new String("str"));
      可用场景: 创建缓存的时候,创建的对象放进缓存中,当内存不足时,JVM就会回收早先创建的对象。
    • 弱引用:只要JVM垃圾回收器发现了它,就会将之回收,使用方式:
      1
      WeakReference<String> wrf= newWeakReference<String> (str);
      可用场景:Java源码中的j的java.util.WeakHashMap中的key就是使用弱引用,我的理解就是,一旦我不需要某个引用,JVM会自动帮我处理它,这样我就不需要做其它操作。
    • 虚引用:虚引用的回收机制跟弱引用差不多,但是它被回收之前,会被放入ReferenceQueue中。注意哦,其它引用是被JVM回收后才被传入ReferenceQueue中的。由于这个机制,所以虚引用大多被用于引用销毁前的处理工作。还有就是,虚引用创建的时候,必须带有ReferenceQueue,使用例子:
      1
      PhantomReference<String> prf = newPhantomReference<String>(newString("str"),newReferenceQueue<>());
      可用场景:对象销毁前的一些操作,比如说资源释放等。Object.finalize() 虽然也可以做这类动作,但是这个方式即不安全又低效
    • ???上诉所说的几类引用,都是指对象本身的引用,而不是指 Reference 的四个子类的引用
  • 注解 Annotation
    注解在我的理解下,就是代码中的特殊标记,这些标记可以在编译、类加载、运行时被读取,并执行相对应的处理。注解本身只是元数据,它本身不执行任何功能。具体的实现通常是在某个拦截器、切面(Aspect)或其他处理机制中完成的。
    注解在开发中是非常常见的,比如Spring框架的 @Controller / @Param / @Select 等等。一些项目也用到lombok的注解,@Slf4j / @Data 等等。Java原生也有@Overried、@Deprecated、@FunctionalInterface等基本注解。
    Java原生的基本注解大多数用于「标记」和「检查」还,此外有一种叫做元Annotation(元注解),所谓的元Annotation就是用来修饰注解的。
    那你自己写过注解吗?
    @Passtoken,,测试时加上此注解,发送请求时不用验证登录信息,,
    JunboRestResponse:使用该注解,确保被注解的方法或类返回一个特定的“骏伯响应”格式。???如何实现??

  • 4种标准元注解是哪四种?
    元注解的作用是负责注解其他注解。Java5.0 定义了 4 个标准的 meta-annotation 类型,被用来提供对其它 annotation 类型作说明。

    • @Target 修饰的对象范围
      @Target说明了Annotation所修饰的对象范围: Annotation可被用于 packages、types(类、接口、枚举、Annotation 类型)、类型成员(方法、构造方法、成员变量、枚举值)、方法参数和本地变量(如循环变量、catch 参数)。在 Annotation 类型的声明中使用了 target可更加明晰其修饰的目标
    • @Retention 定义 被保留的时间长短
      Retention 定义了该 Annotation 被保留的时间长短:表示需要在什么级别保存注解信息,用于描述注解的生命周期(即:被描述的注解在什么范围内有效),取值(RetentionPoicy)
      由:1. SOURCE:在源文件中有效(即源文件保留)2. CLASS:在 class 文件中有效(即 class 保留)3. RUNTIME:在运行时有效(即运行时保留)4.
    • @Documented 描述-javadoc
      @Documented 用于描述其它类型的 annotation 应该被作为被标注的程序成员的公共 API,因此可以被例如 javadoc 此类的工具文档化。
    • @Inherited 阐述了某个被标注的类型是被继承的
      @Inherited 元注解是一个标记注解,@Inherited 阐述了某个被标注的类型是被继承的。如果一个使用了@Inherited 修饰的 annotation 类型被用于一个 class,则这个 annotation 将被用于该class 的子类。
  • final、finalize()、finally

    • 性质不同:1. final为关键字;2. finalize()为方法;3. finally为区块标志,用于try语句中;
    • 作用:
      1. final为用于标识常量的关键字,final标识的关键字存储在常量池中(在这里final常量的具体用法将在下面进行介绍);
      2. finalize()方法在Object中进行了定义,用于在对象“消失”时,由JVM进行调用用于对对象进行垃圾回收,类似于C++中的析构函数;用户自定义时,用于释放对象占用的资源(比如进行I/0操作);
      3. finally{}用于标识代码块,与try{}进行配合,不论try中的代码执行完或没有执行完(这里指有异常),该代码块之中的程序必定会进行;且finally{}中的 return也会比 try{}中的更早返回!!
  • Java中的异常体系是怎样的?
    Java中的所有异常都来自顶级父类Throwable。Throwable下有两个子类Exception和Error。

    • Error表示非常严重的错误,比如 java.lang.StackOverFlowError 和 Java.lang.OutofMemoryError,通常这些错误出现时,仅仅想靠程序自己是解决不了的,可能是虚拟机、磁盘、操作系统层面出现的问题了,所以通常也不建议在代码中去捕获这些Error,因为捕获的意义不大,因为程序可能已经根本运行不了了。
    • Exception表示异常,表示程序出现Exception时,是可以靠程序自己来解决的比如NullPointerException、legalAccessException等,我们可以捕获这些异常来做特殊处理。
      Exception这种异常又分为两类:运行时异常 和 编译异常。
      1. 运行时异常(不受检异常):RuntimeException类及其子类表示JVM在运行期间可能出现的错误。比如说试图使用空值对象的引用(NullPointerException)、数组下标越界(ArrayIndexOutBoundException)。此类异常属于不可查异常,一般是由程序逻辑错误引起的,在程序中可以选择捕获处理,也可以不处理。
      2. 编译异常(受检异常):Exception中除RuntimeException极其子类之外的异常。如果程序中出现此类异常,比如说IOException、FileNotFoundException、SQLException,必须对该异常进行处理,否则编译不通过。在程序中,通常不会自定义该类异常,而是直接使用系统提供的异常类。
  • 如何自定义一个异常
    继承一个异常类,通常是RumtimeException或者Exception

  • 异常的处理机制有几种?
    异常捕捉:try…catch…finally,异常抛出:throws。

  • try catch finally,try里有return,finally还执行么?
    执行,并且finally的执行早于try里面的return。结论:
    1、不管有木有出现异常,finally块中代码都会执行;
    2、当try和catch中有return时,finally仍然会执行;
    3、finally是在return后面的表达式运算后执行的(此时并没有返回运算后的值,而是先把要返回的值保存起来,管finally中的代码怎么样,返回的值都不会改变,任然是之前保存的值),所以函数返回值是在finally执行前确定的;
    4、finally中最好不要包含return,否则程序会提前退出,返回值不是try或catch中保存的返回值。

  • throw与thorws区别

    • 位置不同
      1. throws 用在函数上,后面跟的是异常类,可以跟多个;而 throw 用在函数内,后面跟的是异常对象。
    • 功能不同:
      1. throws 用来声明异常,让调用者只知道该功能可能出现的问题,可以给出预先的处理方式;throw 抛出具体的问题对象,执行到 throw,功能就已经结束了,跳转到调用者,并将具体的问题对象抛给调用者。也就是说 throw 语句独立存在时,下面不要定义其他语句,因为执行不到。
      2. throws 表示出现异常的一种可能性,并不一定会发生这些异常;throw 则是抛出了异常,执行 throw 则一定抛出了某种异常对象。
      3. 两者都是消极处理异常的方式,只是抛出或者可能抛出异常,但是不会由函数去处理异常,真正的处理异常由函数的上层调用处理。
  • 在Java的异常处理机制中,什么时候应该抛出异常,什么时候捕获异常?

    • 异常相当于一种提示,如果我们抛出异常,就相当于告诉上层方法,我抛了一个异常,我处理不了这个异常,交给你来处理,而对于上层方法来说,它也需要决定自己能不能处理这个异常,是否也需要交给它的上层。
    • 所以我们在写一个方法时,我们需要考虑的就是,本方法能否合理的处理该异常,如果处理不了就继续向上抛出异常,包括本方法中在调用另外一个方法时,发现出现了异常,如果这个异常应该由自己来处理,那就捕获该异常并进行处理。
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      public class ExampleService {

      public void lowerLevelOperation() throws SpecificException {
      // 一些可能抛出 SpecificException 的操作
      throw new SpecificException("Something went wrong at lower level");
      }

      public void higherLevelOperation() throws HigherLevelException {
      try {
      // 调用底层方法
      lowerLevelOperation();
      } catch (SpecificException e) {
      // 捕获底层方法抛出的 SpecificException
      // 记录异常信息或进行其他处理
      System.out.println("Caught specific exception: " + e.getMessage());
      // 封装并递交异常给更高层
      throw new HigherLevelException("Exception at higher level", e);
      }
      }

      public static void main(String[] args) {
      ExampleService service = new ExampleService();
      try {
      // 调用更高层的方法
      service.higherLevelOperation();
      } catch (HigherLevelException e) {
      // 捕获更高层方法抛出的 HigherLevelException
      // 记录异常信息或进行其他处理
      System.out.println("Caught higher level exception: " + e.getMessage());
      }
      }
      }
  • Java 泛型、、

    • 在Java中的泛型简单来说就是:在创建对象或调用方法的时候才明确下具体的类型
      使用泛型的好处就是代码更加简洁(无需强制转换),程序更加健壮(编译期间没有警告,在运行期就无ClassCastException)
    • 使用场景:操作集合的时候,List lists = new ArrayList<>();
      如果是其他场景的话,那就是在写「基础组件」的时候了:再明确一下泛型就是「在创建对象或调用方法的时候才明确下具体的类型」,而组件为了做到足够的通用性,是不知道「用户」传入什么类型参数进来的,所以在这种情况下用泛型就是很好的实践。
    • 泛型是会擦除的,那为什么反射能获取到泛型的信息呢?
      泛型的信息只存在编译阶段,在class字节码就看不到泛型的信息了。那为什么下面这段代码能获取得到泛型的信息呢?
      可以理解为泛型擦除是有范围的,定义在类上的泛型信息是不会被擦除的。
      Java 编译器仍在 class 文件以 Signature 属性的方式保留了泛型信息。Type作为顶级接口,Type下还有几种类型,比如TypeVariable、ParameterizedType、WildCardType、GenericArrayType、以及Class。通过这些接口我们就可以在运行时获取泛型相关的信息。
    • 泛型实例:组件为了做到足够的通用性,是不知道「用户」传入什么类型参数进来的,所以在这种情况下用泛型就是很好的实践。要写组件,还是离不开Java反射机制(能够从运行时获取信息),所以一般组件是泛型 + 反射来实现的。
      1
      2
      3
      4
      5
      6
      7
      8
      9
      // 抽象类,定义泛型<T>
      public abstract class BaseDao<T> {
      public BaseDao(){
      Class clazz = this.getClass();
      ParameterizedType pt = (ParameterizedType) clazz.getGenericSuperclass();
      clazz = (Class) pt.getActualTypeArguments()[0];
      System.out.println(clazz);
      }
      }
      1
      2
      3
      4
      5
      6
      // 实现类
      public class UserDao extends BaseDao<User> {
      public static void main(String[] args) {
      BaseDao<User> userDao = new UserDao();
      }
      }
      1
      2
      // 执行结果输出
      class com.entity.User
  • 泛型中extends和super的区别
    1、<?extends T> 表示包括T在内的任何T的子类
    2、<?super T> 表求包括T在内的任何T的父类

  • Java对象创建过程?new一个对象的步骤?

    1. ???检查类符号引用:首先,JVM 会检查 new 关键字后面的类符号引用,确保在常量池中能找到对应的类。???2. 加载类:如果在常量池中找到了类符号引用,JVM 就会加载这个类,包括加载、连接(验证、准备、解析)和初始化阶段。
    2. (1. )类加载检查:JVM 首先检查是否已经加载并验证了这个类的字节码。如果没有,JVM会通过类加载器(ClassLoader)加载这个类,并进行验证,确保其符合Java语言规范。这个过程可能包括解析类的依赖关系,解析字段和方法,以及进行类型检查。
    3. 分配内存:JVM 会根据类的定义,在堆内存中为对象分配内存空间。这个内存空间包括对象的所有成员变量。这个过程包括选择内存分配方式(如指针碰撞、空闲列表、TLAB)、分配内存并进行内存清零。
    4. 初始化对象:分配完内存空间后,JVM 会将对象的除对象头外的内存空间初始化为默认值(基本数据类型为 0,引用类型为 null)。最后,JVM 会设置对象的对象头,包括哈希码、GC 信息等元信息。
    5. 调用构造方法:执行对象的初始化逻辑,包括对成员变量进行赋值、执行一些初始化操作等。
    6. 返回对象引用:最后,new 操作符会返回一个指向新创建对象的引用,通过这个引用可以在程序中操作对象的属性和调用对象的方法。
    7. 这些步骤是创建一个对象的基本流程,无论是通过 new 关键字创建对象,还是通过反射、序列化等方式创建对象,都要经历这些步骤。
  • Java 反射
    简单来说,反射就是Java可以给我们在运行时获取类的信息
    什么是「运行时」:在编译器写的代码是 .java 文件,经过javac 编译会变成 .class 文件,class 文件会被JVM装载运行(这里就是真正运行着我们所写的代码(虽然是被编译过的),也就所谓的运行时。
    为什么要在「运行时」获取类的信息:其实就是为了让我们所写的代码更具有「通用性」和「灵活性」。一个好用的“工具”是需要兼容各种情况的,不知道用该“工具”的用户传入的是什么对象,但你需要帮他们得到需要的结果。例如 SpringMVC 你在方法上写上对象,传入的参数就会帮你封装到对象上;Mybatis可以让我们只写接口,不写实现类,就可以执行SQL;在类上加上@Component注解,Spring就帮你创建对象…
    这些统统都有反射的身影:约定大于配置,配置大于硬编码。通过”约定”使用姿势,使用反射在运行时获取相应的信息(毕竟作为一个”工具“是真的不知道你是怎么用的),实现代码功能的「通用性」和「灵活性」

  • 除了使用new创建对象之外,还可以使用 Java 反射可以创建对象,谁的效率高?
    通过new创建对象的效率比较高。通过反射时,先找查找类资源,使用类加载器创建,过程比较繁琐,所以效率较低。

  • 用 new 关键字创建对象到底是编译时的还是运行时的方式?有什么区别?
    new 创建对象是一种在编译时进行的方式。在编写代码时,通过 new 关键字可以直接在源代码中创建对象,在源代码被编译成字节码时就确定了对象的创建,然后在运行时,Java 虚拟机(JVM)会加载字节码文件,并根据 new 关键字创建对象。这时会分配内存、调用构造方法等,完成对象的初始化。
    这种方式的主要特点是静态,因为对象的创建和初始化都是在编译时确定的。相比之下,使用反射等机制可以实现在运行时动态创建对象,但也更为灵活,因为它可以处理一些在编译时无法确定的类型和类。

  • java反射的作用
    反射机制是在运行时,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意个对象,都能够调用它的任意一个方法。在java中,只要给定类的名字,就可以通过反射机制来获得类的所有信息。这种动态获取的信息以及动态调用对象的方法的功能称为Java语言的反射机制。

  • 哪里会用到反射机制?

    • jdbc就是典型的反射,hibernate,struts等框架使用反射实现的。
      1
      Class.forName('com.mysql.jdbc.Driver.class');  // 加载MySQL的驱动类
    • 在 Spring 中使用反射机制:目的是为了实现框架的灵活性和可扩展性,使得开发人员能够通过配置和注解等方式,实现各种功能而无需修改源代码:
      1. 依赖注入:Spring 使用反射来实现依赖注入,即通过在配置文件或注解中声明依赖关系,Spring 在运行时动态地注入对象之间的依赖关系。通过反射,Spring 能够实例化和初始化对象,以及在运行时处理依赖注入。
      2. Bean 的自动装配: Spring 的自动装配机制依赖于反射,它能够根据一定的规则自动将 Bean 与其他 Bean 进行关联。通过反射,Spring 可以动态地识别和连接相应的 Bean。
      3. AOP(面向切面编程): 在 Spring 中,AOP 是通过动态代理和反射来实现的。通过反射,Spring 能够在运行时动态地创建代理对象,并在方法执行前后执行额外的逻辑。
      4. Bean 的生命周期管理: Spring 容器可以通过反射来实现对 Bean 的生命周期的管理,包括实例化、初始化、销毁等。
      5. 动态代理:Spring 使用动态代理和反射来实现一些特定的功能,如事务管理。通过动态代理,Spring 能够在运行时创建代理对象,将横切逻辑织入到目标对象中。
      6. 处理注解:Spring 使用反射来处理注解,包括扫描类路径上的注解、解析注解的属性值等。通过反射,Spring 能够在运行时获取和处理注解信息。
  • 反射机制的优缺点

    • 优点:
      1. 能够运行时动态获取类的实例,提高灵活性;
      2. 与动态编译结合
    • 缺点:
      1. 使用反射性能较低,需要解析字节码,将内存中的对象进行解析。解决方案:1、通过setAccessible(true)关闭JDK的安全检查来提升反射速度;2、多次创建一个类的实例时,有缓存会快很多; 3、ReflflectASM工具类,通过字节码生成的方式加快反射速度
      2. 相对不安全,破坏了封装性(因为通过反射可以获得私有方法和属性)
  • 反射的实现方式:

    1. 获取想要操作的类的 Class 对象,他是反射的核心,通过 Class 对象我们可以任意调用类的方法。以下是常用的方法:
      • 使用 Class.forName 静态方法(最安全/性能最好)
        1
        2
        3
        4
        5
        try {
        Class<?> myClass = Class.forName("com.example.MyClass"); // 类的全限定名作为参数
        } catch (ClassNotFoundException e) {
        e.printStackTrace();
        }
      • 使用 ClassLoaderloadClass 方法:也可以加载类并返回 Class 对象。
        1
        2
        3
        4
        5
        6
        ClassLoader classLoader = getClass().getClassLoader();
        try {
        Class<?> myClass = classLoader.loadClass("com.example.MyClass");
        } catch (ClassNotFoundException e) {
        e.printStackTrace();
        }
      • 使用 .class 语法: 在已知类的情况下,可以直接获取该类的 Class 对象。
        1
        Class<?> myClass = MyClass.class;
      • 使用对象的 getClass 方法: 可以返回该对象的 Class 对象。
        1
        2
        MyClass myObject = new MyClass();
        Class<?> myClass = myObject.getClass();
    2. 调用 Class 类中的方法,获取类的结构信息。
      Java 反射 API:用来生成 ??? JVM 中的类、接口或则对象的信息。
      • Class 类:反射的核心类,可以获取类的属性,方法等信息。
      • Field 类:Java.lang.reflec 包中的类,表示类的成员变量,可以用来获取和设置类之中的属性值。
      • Method 类: Java.lang.reflec 包中的类,表示类的方法,它可以用来获取类中的方法信息或者执行方法。
      • Constructor 类: Java.lang.reflec 包中的类,表示类的构造方法。
        1
        2
        3
        4
        5
        6
        7
        8
        9
        //获取 Person 类的 Class 对象
        Class clazz = Class.forName("reflection.Person");
        //获取 Person 类的所有方法信息
        Method[] method = clazz.getDeclaredMethods();
        //获取 Person 类的所有成员属性信息
        Field[] field = clazz.getDeclaredFields();
        //获取 Person 类的所有构造方法信息
        Constructor[] constructor = clazz.getDeclaredConstructors();
        for(Constructor c : constructor) System.out.println(c.toString());
    3. 将获取到的类信息用于实际的操作,例如创建对象、调用方法、获取和设置字段的值等。
      通过反射 API 来实现动态操作。
  • 利用反射动态创建对象实例.

    1
    2
    3
    4
    5
    6
    7
    8
    // 0. 获取 Person 类的 Class 对象
    Class clazz = Class.forName("reflection.Person");
    // 1. 使用 newInstane 方法创建实例对象(这种方法要求该 Class 对象对应的类有默认的空构造器)
    Person p = (Person) clazz.newInstance();
    // 2. 先获取指定的 Constructor 对象,再调用 Constructor 对象的 newInstance()方法来创建 Class 对象实例
    // (通过这种方法可以选定构造方法创建实例)
    Constructor c = clazz.getDeclaredConstructor(String.class,String.class,int.class); // 获取构造方法
    Person p1 = (Person) c.newInstance("李四","男",20); // 创建对象并设置属性
  • 深拷贝和浅拷贝
    深拷贝和浅拷贝就是指对象的拷贝,一个对象中存在两种类型的属性,一种是基本数据类型,一种是实例对象的引用。
    1,浅拷贝是指,只会拷贝基本教据类型的值,以及实例对象的引用地址,并不会复制一份引用地处所指的对象。也就是浅拷贝出来的对象,内部的类属性指向的是同一个对象
    2,深拷贝是指,既会拷贝基本数据类型的值,也会针对实例对象的引用地址所指向的对象进行复制,深拷贝出来的对象,内部的属性指向的不是同一个对象

  • 你了解动态代理吗???

    • 代理模式是设计模式之一。代理模型有静态代理和动态代理。
      • 静态代理需要自己写代理类,实现对应的接口,比较麻烦,在【编译期间】就确定好代理关系。
      • 动态代理这一技术在实际或者框架原理中是非常常见的,在【运行期间】确定好代理关系。
    • 动态代理有什么用:它是一种设计模式,用于在不修改原始对象的情况下,通过代理对象来间接访问原始对象,并在访问前后执行额外的操作。
    • 动态代理中的对象:
      • 目标对象:待增强的对象
      • 代理对象:增强过后的对象,也就是我们使用的对象
    • 两种实现方式:
      • JDK动态代理(反射)其实就是运用了反射的机制,会帮我们实现接口的方法,通过invokeHandler对所需要的方法进行增强;
        • 目标对象 implement 目标接口
        • 代理对象 implement 目标接口
        • 目标对象和代理对象是平级关系
      • CGLIB代理(继承)则用的是利用ASM框架,通过修改其字节码生成子类来处理。
        • 代理对象 extend 目标对象
        • 目标对象和代理对象是父子关系
    • 动态代理的应用:功能增强、控制访问。
      • SpringAOP
      • Spring整合Mybaits管理Mapper接口,不用写实现类,只写接口就可以执行SQL
  • 面向对象设计中五大设计原则 SOLID、、

    1. 单一职责原则(SRP): 每个类或模块应该有且仅有一个引起它变化的原因,即一个类或模块应该只负责一种类型的任务或功能。这样可以提高代码的内聚性和可维护性,减少代码的复杂度。
    2. 开放封闭原则(OCP): 软件实体(类、模块、函数等)应该对扩展开放,对修改关闭。即通过扩展现有的代码来实现新的功能,而不是修改已有的代码。这样可以降低修改已有代码带来的风险,并提高系统的稳定性。
    3. 里氏替换原则(LSP):子类必须能替换父类并出现在父类能够出现的任何地方,而不影响程序的正确性。即子类应该完全实现父类的方法,并且遵循父类的约定和契约。
    4. 接口隔离原则(ISP): 不应该强迫客户端依赖它们不需要的接口。接口应该小而专一,应该根据实际需要定义多个接口,而不是一个臃肿的接口。这样可以降低类之间的耦合度,提高系统的灵活性和可维护性。
    5. 依赖倒置原则(DIP):高层模块不应该依赖于低层模块,它们都应该依赖于抽象。抽象不应该依赖于具体实现细节,具体实现细节应该依赖于抽象。这样可以降低模块之间的耦合度,提高系统的灵活性和可扩展性。
  • 常用的设计模式、、

    • 策略模式:定义了算法族,分别封装起来,让它们之间可以相互替换,此模式让算法的变化独立于使用算法的的客户。
      • 使用场景:如果代码有多个 if…else 等条件分支,并且每个条件分支,可以封装起来替换的,我们就可以使用策略模式来优化。
      • 模式实现:一个接口或者抽象类,里面两个方法(一个方法匹配类型,一个可替换的逻辑实现方法)、不同策略的差异化实现(即,不同策略的实现类)、使用策略模式
      • 模式使用:我们借助 spring 的生命周期,使用 ApplicationcontextAware 接口,把对用的策略,初始化到 map 里面。然后对外提供 resolveFile 方法即可。
    • 责任链模式:实际上是一种处理请求的模式,它让多个处理器(对象节点)都有机会处理该请求,直到其中某个处理成功为止。责任链模式把多个处理器串成链,然后让请求在链上传递。
      • 使用场景:当你想要让一个以上的对象有机会能够处理某个请求的时候,就使用责任链模式。责任链上,每个对象的差异化处理,如本小节的业务场景,就有参数校验对象、安全校验对象、黑名单校验对象、规则拦截对象
      • 模式实现:一个接口或者抽象类、每个对象差异化处理、对象链(数组)初始化(连起来)
      • 这个接口或者抽象类,需要:有一个指向责任下一个对象的属性、一个没置下一个对象的set方法、给子类对象有异化实现的方法(如以下代码的doFiter方法)
    • 模版方法模式:定义一个操作中的算法的骨架流程,而将一些步骤延迟到子类中,使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。它的核心思想就是:定义一个操作的一系列步骤,对于某些暂时确定不下来的步骤,就留给子类去实现,这样不同的子类就可以定义出不同的步骤。
      • 模式实现:一个抽象类,定义骨架流程(抽象方法放一起)确定的共同方法步骤,放到抽象类(去除抽象方法标记)。不确定的步骤,给子类去差异化实现
    • 观察者模式:行为模式,一个对象(被观察者)的状态发生改变,所有的依赖对象(观察者对象)都将得到通知,进行广播通知。
      • 它的主要成员就是观察者和被观察者。
        • 被观察者(0bserverable):目标对象,状态发生变化时,将通知所有的观察者。
        • 观察者(observer):接受被观察者的状态变化通知,执行预先定义的业务。
      • 使用场景:完成某件事情后,异步通知场景。如,登陆成功,发个M消息等等。
      • 模式实现:一个被观察者的类Observerable、多个观察者Observer、观察者的差异化实现、经典观察者模式封装:EventBus实战
    • 工厂模式
      • 工厂模式一般配合策略模式一起使用。用来去优化大量的if..else..或switc…cas…条件语句。
      • 定义一个创建对象的接口,让其子类自己决定实例化哪一个工厂类,工厂模式使其创建过程延迟到子类进行。
    • 单例模式:确保一个类只有一个实例,并提供一个全局访问点
      • 懒汉模式、饿汉模式
  • 工厂模式和抽象工厂的区别

    1. 简单工厂模式:
      • 简单工厂模式是一种创建型设计模式,它通过一个工厂类来封装对象的创建过程,用户只需要通过工厂类来获取所需的对象,而无需直接调用对象的构造方法。
      • 简单工厂模式只包含一个工厂类和多个产品类,工厂类根据用户的请求返回不同的产品对象。
      • 简单工厂模式适用于对象类型较少、不需要频繁变化的情况下,对于新增产品类型或者修改产品构造方法时,需要修改工厂类的代码。
    2. 抽象工厂模式:
      • 抽象工厂模式也是一种创建型设计模式,它通过一个抽象工厂接口和多个具体工厂类来创建一组相关或者相互依赖的对象,而无需指定具体的类。
      • 抽象工厂模式包含抽象工厂接口、具体工厂类、抽象产品接口和具体产品类,每个具体工厂类负责创建一组相关的产品对象。
      • 抽象工厂模式适用于需要创建一组相关或者相互依赖的产品对象,并且对产品的具体类型和实现进行解耦的情况下,当需要新增产品类型时,只需要添加新的具体工厂类和对应的产品类,无需修改现有代码。
    3. 综上所述,简单工厂模式主要用于创建单一类型的对象,工厂类负责根据用户的请求返回相应的产品对象;而抽象工厂模式主要用于创建一组相关或者相互依赖的产品对象,通过抽象工厂接口和具体工厂类来实现对象的创建,并且对产品的具体类型和实现进行解耦,适用于产品类型频繁变化的情况。
  • 手写单例模式

    • 懒汉式单例模式:只有在需要时才会创建实例
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      public class Singleton {

      private static Singleton instance; // 私有静态变量,用于保存唯一实例

      private Singleton() { } // 私有构造函数,防止外部实例化

      public static Singleton getInstance() { // 公共静态方法,用于获取实例
      if (instance == null) { // 使用双重检查锁定(double-checked locking)来确保线程安全
      synchronized (Singleton.class) {
      if (instance == null) {
      instance = new Singleton();
      }
      }
      }
      return instance;
      }
      }
    • 饿汉式单例模式:如果需要在类加载时就创建实例,可以直接在静态变量中初始化
      1
      2
      3
      4
      5
      6
      7
      8
      9
      public class Singleton {
      private static Singleton instance = new Singleton();
      private Singleton() {
      // 可以在这里进行一些初始化操作
      }
      public static Singleton getInstance() {
      return instance;
      }
      }
  • 对序列化和反序列化的理解?

    • 序列化:把内存中的对象转换为字节流,以便实现存储和运输
    • 反序列化:根据从网络或文件获取的对象的字节流,根据字节流中保存的对象描述信息和状态,重新构建一个新的对象
    • 序列化的目的是为了解决网络通信中的对象传输的问题,把当前jvm进程中的对象跨网络传输到另一个jvm进程中并恢复;为保证通信双方对对象的可识别,会把对象先转换为通用的解析格式如:json,xml、、再转换为字节流进行运输
    • 实现方法:序列化对象实现Serializable接口,并再对象中添加serialVersionUID字段。
  • 什么时候用assert
    assertion(断言)在软件开发中是一种常用的调试方式,很多开发语言中都支持这种机制。在实现中,assertion就是在程序中的一条语句,它对一个boolean表达式进行检查,一个正确程序必须保证这个boolean表达式的值为true;如果该值为false,说明程序已经处于不正确的状态下,系统将给出警告或退出。一般来说,assertion用于保证程序最基本、关键的正确性。assertion检查通常在开发和测试时开启。为了提高性能,在软件发布后,assertion检查通常是关闭的

  • Java有没有goto
    java中的保留字,现在没有在java中使用

  • ???拦截器与过滤器?

    • 过滤器(Filter):
      • 过滤器是 Servlet 规范中定义的一种组件,用于对请求进行预处理、后处理以及过滤。过滤器可以在请求进入 Servlet 之前进行预处理,也可以在响应返回客户端之前进行后处理。过滤器主要用于对请求和响应进行修改、验证、记录日志等操作。
      • 过滤器可以通过在 web.xml 配置文件中进行配置,也可以通过注解 @WebFilter 来声明。过滤器需要实现 javax.servlet.Filter 接口,并实现其中的 initdoFilterdestroy 方法。
      • 过滤器可以对所有的请求进行过滤,例如对 URL 模式进行匹配,也可以通过编程方式动态地添加或移除过滤器。
    • 拦截器(Interceptor):
      • 拦截器是 Spring 框架提供的一种机制,用于对请求进行预处理和后处理。拦截器是基于面向切面编程(AOP)的思想,可以对控制器方法进行拦截,对请求进行前置处理、后置处理、异常处理等操作。
      • 拦截器是 Spring MVC 框架的一部分,通过实现 HandlerInterceptor 接口来定义拦截器,并通过配置文件或者 Java 配置类进行声明和注册。
      • 拦截器可以精确地对指定的控制器方法进行拦截,可以在请求处理之前或之后进行操作,并且可以对 Model 和 View 进行修改或者增强。
    • 区别,主要体现在以下几个方面:
      1. 所处框架
        过滤器(Filter)是 Servlet 规范中的一部分,用于对请求和响应进行预处理和后处理。它是在 Web 容器层面的一种功能。
        拦截器(Interceptor)是 Spring MVC 框架中的一部分,用于对控制器方法进行拦截和处理。它是在 Spring MVC 框架的控制器层面的一种功能。
      2. 实现方式
        过滤器需要实现 javax.servlet.Filter 接口,并实现其中的 initdoFilterdestroy 方法。过滤器可以通过 web.xml 配置文件中进行配置,也可以通过注解 @WebFilter 来声明。
        拦截器需要实现 HandlerInterceptor 接口,并实现其中的 preHandlepostHandleafterCompletion 方法。拦截器的声明和注册通常是通过配置文件或者 Java 配置类来完成。
      3. 功能特性
        过滤器对请求和响应进行处理,可以进行内容修改、请求重定向、日志记录等操作。过滤器可以对所有的请求进行过滤。
        拦截器主要用于对控制器方法进行拦截,可以在请求处理之前或之后进行操作,例如权限验证、日志记录、异常处理等。拦截器可以精确地对指定的控制器方法进行拦截。
      4. 使用场景
        过滤器适用于对 Web 应用的全局请求进行处理,例如字符编码过滤、安全过滤、日志记录等。
        拦截器适用于对控制器方法的请求进行处理,例如权限控制、日志记录、异常处理等。
  • 日志、、
    1.选择恰当的日志级别 error warn info debug
    2.日志要打印出参入参数 方便甩锅
    3.选择合适的日志格式 时间戳 线程名字 日志级别等
    4.if-else ,switch 等分支语句都建议打印日志,方便排查
    5.对一些比较低的日志级别进行判断,使用log.isXXXX()方法判断
    6.不建议直接使用log4j ,logback等日志系统,建议使用slf4j框架,方便统一处理
    7.建议使用参数占位符{},而不是+拼接,简洁且提升性能
    8.建议使用异步日志,能有效提升IO性能
    9.不要使用 e.printStackTrace() 打印错误信息,因为太多信息,且是堆栈信息,会使得内存溢出
    10.异常不要只打一半,要完成输出
    11.禁止在线上开启debug 会把磁盘打满
    12.不要记录了异常,又抛出异常
    13.避免重复打印日志,浪费磁盘空间
    14.日志文件分离,不同级别日志存放在不同文件中
    15.核心功能模块,建议打印详细的日志

  • 函数式接口、、

    • 函数式接口具有以下主要特点:
      函数式接口只包含一个抽象方法,但可以包含多个默认方法或静态方法。
      函数式接口可以使用 @FunctionalInterface 注解来显式声明,这样可以让编译器进行检查,确保其满足函数式接口的定义。
      函数式接口可以通过 Lambda 表达式、方法引用等方式进行实例化。
    • 函数式接口的引入使得 Java 可以更加方便地支持函数式编程风格,包括:
      更简洁的代码:通过 Lambda 表达式可以编写更加简洁、可读性更强的代码。
      支持并行操作:函数式接口可以很好地配合 Stream API 使用,支持并行操作和函数式变成。
      提升代码灵活性:函数式接口的使用可以提升代码的灵活性和可维护性,使得代码更易于扩展和修改。
    • 函数式接口在实际应用中有很多场景,例如:
      在并发编程中,可以使用函数式接口配合 CompletableFuture 来进行异步任务处理。
      在集合操作中,使用函数式接口可以简化集合的筛选、映射等操作。
      在事件处理和回调机制中,使用函数式接口可以定义事件处理器。

Java 集合

  • 集合有什么。
    Java 集合 主要包括 Collection 和 Map 两种,Collection 存储着对象的集合,而 Map 存储着键值对(两个对象)的映射表。

  • Collection包结构,与Collections的区别
    Collection是集合类的上级接口,子接口有 Set、List、LinkedList、ArrayList、Vector、Stack、Set;
    Collections是集合类的一个帮助类, 它包含有各种有关集合操作的静态多态方法,用于实现对各种集合的搜索、排序、线程安全化等操作。此类不能实例化,就像一个工具类,服务于Java的Collection框架。

  • 说说 List, Set, Map三者的区别
    List(对付顺序的好帮手): List接口存储一组不唯一(可以有多个元素引用相同的对象),有序的对象
    Set(注重独一无二的性质):不允许重复的集合。不会有多个元素引用相同的对象。
    Map(用Key来搜索的专家): 使用键值对存储。Map会维护与Key有关联的值。两个Key可以引用相同的对象,但Key不能重复,Key可以是任何对象。

  • Java 的 List?
    List在Java里边是一个接口,常见的实现类有ArrayList和LinkedList,ArrayList底层数据结构是数组,LinkedList链表。

  • ArrayList
    实现了动态扩容。当new ArrayList()时,默认会有一个大小为0,空的 Object[] 数组。
    第一次add添加数据的时候,会给数组初始化一个默认值 10 的大小,并将元素添加到数组中。使用ArrayList在每一次add的时候,会先去计算数组空间;如果空间是够的,直接追加上去;如果不够,那就得扩容。
    每一次扩容原来的 1.5倍,新容量的计算方式为(oldCapacity * 3)/2 + 1 即原容量的1.5倍再加1,创建一个新的数组[newData]。然后使用System.arraycopy方法(底层为native方法实现)将旧数组[elementData]中的元素复制到新数组中。这是一个高效的底层数组拷贝操作,避免了逐个元素复制的开销。最后,将新数组[newData]替换为ArrayList的内部数组[elementData]。
    日常开发中用得最多的是ArrayList呢:是由底层的数据结构来决定的,在日常开发中,遍历的需求比增删要多,即便是增删也是往往在List的尾部添加就OK了。像在尾部添加元素,ArrayList的时间复杂度也就O(1)。

  • LinkedList
    基于链表实现的,对于增删操作来说,由于链表节点的指针调整相对比较简单,删除或添加一个节点的开销是 O(1) 的。但在进行遍历和随机访问时,由于链表的非连续存储,性能相对较差。因此,在实际场景中,ArrayList 在随机访问和遍历方面的性能通常比 LinkedList 更好,而在频繁的增删操作时,LinkedList 可能更具优势.
    LinkedList 还实现了 DeQueue,可以对头尾元素操作,所以 LinkedList 也可以当作队列使用。

  • Array与ArrayList有什么不一样?
    Array与ArrayList都是用来存储数据的集合。ArrayList底层是使用数组实现的,但是ArrayList对数组进行了封装和功能扩展,拥有许多原生数组没有的一些功能。我们可以理解成ArrayList是Array的一个升级版。

  • ArrayList 的 遍历;

    • 使用 for 循环:
      1
      2
      3
      for (int j = 0; j < list.size(); j++) {
      System.out.println(list.get(j));
      }
    • 使用增强型 for 循环(for-each 循环):
      1
      2
      3
      for (Integer num : list) {
      System.out.println(num);
      }
    • 使用迭代器(Iterator):
      1
      2
      3
      4
      5
      Iterator<Integer> iterator = list.iterator();
      while (iterator.hasNext()) {
      Integer num = iterator.next();
      System.out.println(num);
      }
  • ArrayList 的 remove、、

    • 根据索引删除元素;删除元素后,后面的元素会向前移动,列表的大小会减少。
      1
      list.remove(0); // 删除索引为 0 的元素
    • 根据对象删除元素:remove(Object o) 方法可以删除列表中第一次出现的指定对象。
      1
      list.remove(Integer.valueOf(5)); // 删除值为 5 的元素
    • 使用迭代器删除元素:使用 Iterator 遍历 ArrayList,并使用迭代器的 remove 方法安全地删除元素。
      1
      2
      3
      4
      5
      6
      7
      Iterator<Integer> iterator = list.iterator();
      while (iterator.hasNext()) {
      Integer num = iterator.next();
      if (num == 5) {
      iterator.remove(); // 删除符合条件的元素
      }
      }
    • **ConcurrentModificationException**异常:在遍历过程中直接调用 remove() 可能会引起并发修改异常。这是因为在遍历过程中修改了集合结构,导致迭代器的检测机制检测到并抛出异常。为避免,可采用以下方法:
      • 在迭代过程中不修改集合的结构,可以通过复制集合或者使用迭代器的 remove 方法进行安全的删除操作。
      • 在多线程环境下,可以使用线程安全的集合类(如 ConcurrentHashMap)或者采用同步机制(如使用 synchronized 关键字或者使用 Lock)来保证集合的线程安全性。
  • ArrayList如何实现线程安全?

    1. 使用Collections.synchronizedList方法:这将返回一个线程安全的 List 包装器。通过这种方式,对 synchronizedList 的所有操作都会在内部被同步,从而确保线程安全。如果写入操作较为频繁,可能需要权衡使用 synchronizedList 或者其他并发集合类,具体根据业务需求来决定。
      1
      List<Type> synchronizedList = Collections.synchronizedList(new ArrayList<Type>());
    2. 使用CopyOnWriteArrayList类:java.util.concurrent 包下的类,它通过在修改操作时复制整个数组来实现线程安全。这意味着在写入操作时,它会创建一个新的数组,从而不影响正在进行的读取操作。如果读取操作频繁而写入操作较少,CopyOnWriteArrayList 可能是更好的选择,因为它对于并发读取操作而言性能较好。
      1
      List<Type> threadSafeList = new CopyOnWriteArrayList<Type>();
  • CopyOnWriteArrayList的底层原理是怎样的

    1. 首先CopyOnWriteArraylst内部也是用过数组来实现的,在向CpyOnWriteAraist添加元素时,会复制一个新的数组,写操作在新数组上进行,读操作在原数组上行
    2. 并且,写操作会加锁,防止出现并发写入丢失数据的问题
    3. 写操作结束之后会把原数组指向新数组
    4. CopyOnWriteArraylist允许在写操作时来读取数据,大大提高了读的性能,因此适合读多写少的应用场,但会占内存,同时可能读到的数据不是实时最新的教据(写线程操作结束后才能读到新数据),所以不适合实时性要求很高的场景
  • Vector 你了解吗?
    Vector是底层结构是数组,一般现在已经很少用了。相对于ArrayList,它是线程安全的,在扩容的时候直接扩容两倍。
    在Java中,Stack 类扩展了Vector,提供了一个后进先出(LIFO)的堆栈数据结构,其中元素的插入和删除都发生在堆栈的顶部。

  • set 集合
    Set 注重独一无二的性质,该体系集合用于存储无序(存入和取出的顺序不一定相同)元素, 值不能重复。对象的相等性本质是对象 hashCode 值(java 是依据对象的内存地址计算出的此序号) 判断的, 如果想要让两个不同的对象视为相等的,就必须覆盖 Object 的 hashCode 方法和 equals 方法。

  • HashSet(Hash 表)
    哈希表边存放的是哈希值。 HashSet 存储元素的顺序并不是按照存入时的顺序(和 List 显然不同) 而是按照哈希值来存的所以取数据也是按照哈希值取得。
    元素的哈希值是通过元素的hashcode 方法来获取的, HashSet 首先判断两个元素的哈希值,如果哈希值一样,接着会比较equals 方法 如果 equls 结果为 true , HashSet 就视为同一个元素。如果 equals 为 false 就不是同一个元素。
    哈希值相同 equals 为 false 的元素是怎么存储呢,就是在同样的哈希值下顺延(可以认为哈希值相同的元素放在一个哈希桶中)。

  • HashSet、HashMap 和 Hashtable 的关系。

    • HashSet 使用 HashMap 作为底层实现,用于存储不重复的元素。
    • HashMap 是键值对的存储结构,而 HashSet 只存储键。
    • Hashtable 也是键值对的存储结构,类似于 HashMap,但是是同步的,因此适合于多线程环境。
    • HashMap 实现了 Map 接口。Hashtable 实现了 Map 接口以及 Dictionary 接口(在 Java 1.0 和 1.1 版本中使用较多,现已被 Map 接口取代)。
    • HashMapHashtable 允许键和值为 null,而 HashSet 只允许一个 null 元素。
      在实际开发中,一般推荐使用 HashMap 而不是 Hashtable,因为 Hashtable 的同步性会带来额外的性能开销。如果需要在多线程环境下使用,也可以考虑使用 Collections.synchronizedMap() 方法来创建一个同步的 HashMap
  • TreeSet

    1. TreeSet()是使用二叉树的原理对新 add()的对象按照指定的顺序排序(升序、降序),每增加一个对象都会进行排序,将对象插入的二叉树指定的位置。
    2. Integer 和 String 对象都可以进行默认的 TreeSet 排序,而自定义类的对象是不可以的, 自己定义的类必须实现 Comparable 接口,并且覆写相应的 compareToTo()函数,才可以正常使用。
    3. 在覆写 compare()函数时,要返回相应的值才能使 TreeSet 按照一定的规则来排序
    4. 比较此对象与指定对象的顺序。如果该对象小于、等于或大于指定对象,则分别返回负整数、零或正整数
  • LinkHashSet( HashSet+LinkedHashMap)
    对于 LinkedHashSet 而言,它继承与 HashSet、又基于 LinkedHashMap 来实现的。LinkedHashSet 底层使用 LinkedHashMap 来保存所有元素,它继承与 HashSet,其所有的方法操作上又与 HashSet 相同,因此 LinkedHashSet 的实现上非常简单,只提供了四个构造方法,并通过传递一个标识参数,调用父类的构造器,底层构造一个 LinkedHashMap 来实现,在相关操作上与父类 HashSet 的操作相同,直接调用父类 HashSet 的方法即可。

  • Map 了解多少?
    Map在Java里边是一个接口,常见的实现类有 HashMap、LinkedHashMap、TreeMap 和 ConcurrentHashMap
    在Java里边,哈希表的结构是数组+链表的方式。HashMap底层数据结构是数组+链表/红黑树;LinkedHashMap是数组+链表/红黑树+双向链表;TreeMap是红黑树;而ConcurrentHashMap是数组+链表/红黑树

  • HashMap

    • hashmap通过put(key,value)存储,通过get(key)获取。当传入key时,hashmap会调用hashcode()方法计算出hash值,根据 hash 值将 value 保存在 bucket 里
    • 实现原理:其实就是有个 Entry 数组,Entry 保存了 key 和 value。当你要塞入一个键值对的时候,会根据一个 hash 算法计算 key 的 hash 值,然后通过数组大小 n-1 & hash 值之后,得到一个数组的下标,然后往那个位置塞入这个 Entry。(hashmap的底层是哈希表,哈希表的实现是数组+链表+红黑树)
    • 为了解决 hash 冲突的问题,采用了链表法
      • 在 JDK 1.7 及之前链表的插入采用的是头插法,即在链表的头部插入新的 Entry。在 JDK 1.8 的时候,改成了尾插法,并且引入了红黑树。当链表的长度大于 8 且数组大小大于等于 64 的时候,就把链表转化成红黑树,当红黑树节点小于 6 的时候,又会退化成链表。
      • 为什么 JDK 1.8 要对 HashMap 做红黑树这个改动?主要是避免 hash 冲突导致链表的长度过长,这样 get 的时候时间复杂度严格来说就不是 O(1) 了,因为可能需要遍历链表来查找命中的 Entry。
      • 为什么定义链表长度为 8 且数组大小大于等于 64 才转红黑树?不要链表直接用红黑树不就得了吗?因为红黑树节点的大小是普通节点大小的两倍,所以为了节省内存空间不会直接只用红黑树,只有当节点到达一定数量才会转成红黑树,这里定义的是 8(泊松分布)
      • 为什么节点少于 6 要从红黑树转成链表?也是为了平衡时间和空间,节点太少链表遍历也很快,节约内存。
    • HashMap 默认大小为16,负载因子的大小为 0.75。
      • HashMap的大小只能是2次幂的,假设你传一个10进去,实际上最终HashMap的大小是16(具体的实现在tableSizeFor可以看到)把元素放进HashMap的时候,需要算出这个元素所在的位置(hash)。在HashMap里用的是位运算来代替取模,更加高效。HashMap的大小只能是2次幂时,才能合理用位运算替代取模。
      • 负载因子的大小决定着哈希表的扩容和哈希冲突。比如默认的HashMap大小为16,负载因子为0.75,这意味着数组最多只能放16*0.75=12个元素,每次put元素进去的时候,都会检查HashMap的大小有没有超过这个阈值,一旦超过12,则哈希表需要扩容。如果把负载因子调高了,哈希冲突的概率会增高,同样会耗时(查找速度变慢了)
    • put:首先对key做hash运算,计算出该key所在的index。如果没碰撞,直接放到数组中,如果碰撞了,需要判断目前数据结构是链表还是红黑树,根据不同的情况来进行插入。假设key是相同的,则替换到原来的值。最后判断哈希表如果满了,扩容。
    • get:还是对key做hash运算,计算出该key所在的index,然后判断是否有hash冲突。假设没有冲突直接返回,假设有冲突则判断当前数据结构是链表还是红黑树,分别从不同的数据结构中取出。
      在HashMap中怎么判断一个元素是否相同?首先会比较hash值,随后会用==运算符和equals()来判断该元素是否相同。如果只有hash值相同,那说明该元素哈希冲突了,如果hash值和equals() || == 都相同,那说明该元素是同一个。
    • Jdk1.7 到 Jdk1.8 HashMap 发生了什么变化(底层)?
      • 1.7中底层是数组+链表,1.8中底层是数组+链表+红黑树,加红黑树的目的是提高HashMap插入和查询整体效率
      • 1.7中链表插入使用的是头插法,1.8使用的是尾插法,因为1.8中插入key和value时需要判断链表元素个数,所以需要遍历链表统计链表元素个数,所以正好就直接使用尾插法
      • 1.7中哈希算法比较复杂,存在各种右移与异或运算,1.8中进行了简化,因为复杂的哈希算法的目的就是提高散列性,来提供HashMap的整体效率,而1.8中新增了红黑树,所以可以适当的简化哈希算法,节省CPU资源
  • hash 函数的优化:

    • 1.8后,在put元素的时候传递的Key,先算出正常的哈希值,然后与高16位做异或运算,产生最终的哈希值。相当于把高位和低位的特征进行组合,结果得到的数组位置的散列度一定会更高,可以增加了随机性,减少了碰撞冲突的可能性。
      1
      2
      3
      4
      static final int hash(Object key) {
      int h;
      return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
      }
  • HashMap 扩容 resize() :默认是扩原来的2倍(因为HashMap的大小只能是2次幂),扩的是数组不是链表。

    • 1.7版本:
      1. 先生成长度为原来2倍的新数组
      2. 遍历老数组中的每个位置上的链表上的每个元素
      3. 取每个元素的key,并基于新数组长度,计算出每个元素在新数组中的下标
      4. 头插法将元素添加到新数组中去
      5. 所有元素转移完了之后,将新数组赋值给HashMap对象的table属性
    • 1.8版本:
      1. 先生成新数组,长度是老数组的2倍
      2. 遍历老数组中的每个位置上的链表或红黑树
      3. 如果是链表,则直接将链表中的每个元素里新计算下标,并添加到新数组中去
        (将链表重新链接,按照低位区和高位区重新分配到新数组;)
      4. 如果是红黑树,则先遍历红黑树,先计算出红黑树中每个元素对应在新数组中的下标位置:统计每个下标位置的元素个数,如果该位置下的元素个数超过了8,则生成一个新的红黑树,并将根节点的添加到新数组的对应位置;如果该位置下的元素个数没有超过8,那么则生成一个链表,并将链表的头节点添加到新数组的对应位置、
        (调用split方法将红黑树重新切分为低位区和高位区2个链表;判断低位区和高位区链表的长度,链表长度小于6,则会进行取消树化的处理,否则会将新生成的链表重新树化;)
      5. 所有元素转移完了之后,将新数组赋值给HashMap对象的table属性
  • Java8为什么将HashMap的插入方法改为了尾插法?

    • 头插法,即新插入的元素会插入到链表的头部,会产生以下问题:
      • 破坏了链表元素的插入顺序:由于头插法是将新插入的元素插入到链表的头部,这样就导致链表的顺序与元素插入的顺序相反,不利于一些需要按照插入顺序遍历的场景。
      • 容易引起链表环形问题:由于头插法需要修改链表头,这会导致在并发环境下,触发resize()时多个线程同时修改新数组的桶节点的链表头,可能会引起链表环形问题,使得链表无法正确遍历或者出现死循环的情况。
        ??
    • 尾插法,即新插入的元素会插入到链表的尾部,这样可以解决很多问题并且有以下优点:。
      • 提高查询效率:尾插法使得链表元素的插入顺序与元素插入的顺序一致,从而方便了元素的查找和遍历操作,提高了HashMap的查询效率。
      • 避免链表环形问题:尾插法是将新插入的元素插入到链表的尾部,不需要修改链表头,因此可以避免在并发环境下多个线程修改链表头导致的链表环形问题。
      • 1.8中插入key和value时需要判断链表元素个数,所以需要遍历链表统计链表元素个数,所以正好就直接使用尾插法
    • Java8及以后的版本插入操作的平均时间要比Java8之前的版本快,差距在1ms左右,这是由于Java8将HashMap的插入方法改为了尾插法,避免了链表环形问题的发生,同时优化了哈希算法和查询效率,从而提高了HashMap的性能。
  • LinkedHashMap?
    LinkedHashMap 底层结构是数组+链表+双向链表,实际上它继承了 HashMap,在 HashMap 的基础上维护了一个双向链表
    LinkedHashMap 把 HashMap 的 Entry数组 加了两个指针:before 和 after。就是要把塞入的 Entry 之间进行关联,串成双向链表;有了这个双向链表,我们的插入可以是有序的,这里的有序不是指大小有序,而是插入有序。LinkedHashMap在遍历的时候实际用的是双向链表来遍历的,所以LinkedHashMap的大小不会影响到遍历的性能
    并且内部还有个 accessOrder 成员,默认是 false, 代表链表是顺序是按插入顺序来排的,如果是 true 则会根据访问顺序来进行调整,就是咱们熟知的 LRU 那种,如果哪个节点访问了,就把它移到最后,代表最近访问的节点。

  • TreeMap?
    TreeMap的底层数据结构是红黑树,TreeMap的key不能为null(如果为null,那还怎么排序呢)。
    TreeMap有序是通过实现 Comparable 接口或者自定义实现一个 comparator 传入构造函数(如果comparator为null,那么就使用自然顺序 ),这样塞入的节点就会根据你定义的规则进行排序。因此它除了作为 Map 外,还可以用作双端队列。

  • Hashtable、HashMap、TreeMap?

    • Hashtable
      • 不允许使用 null 键或 null 值。
      • 使用场景:1、当需要确保数据的线程安全,且在多线程环境中共享 Map 时,可以考虑使用 Hashtable。2、由于其性能相对较低,推荐在遗留代码中或者特定要求线程安全的小规模数据集合中使用。
    • HashMap
      • 允许一个 null 键和多个 null 值。
      • 使用场景: 1、在非多线程环境中,或者在读多写少的场景下(可以通过外部同步来解决线程安全问题),HashMap 是一个优选,因为它提供了更好的性能。 2、当需要快速查找、插入和删除键值对时,特别是在数据量较大的情况下。
      • 当需要多线程操作的时候可以使用线程安全的ConcurrentHashMap。ConcurrentHashMap虽然也是线程安全的,但是它的效率比Hashtable要高好多倍。因为ConcurrentHashMap使用了分段锁,并不对整个数据进行锁定。
    • TreeMap
      • key不能为null
      • 使用场景:1、当需要一个总是保持排序状态的 Map 时,TreeMap 是最合适的选择。它适用于需要频繁地进行有序遍历或范围搜索的场景。 2、 在需要根据键进行排序的应用中,如时间线索引、自然排序的目录结构等。3、 当数据结构的大小频繁变动,且需要保持有序状态时,TreeMap 通常比维护一个 ArrayList 之后再排序要高效。
  • ConcurrentHashMap?

    • ConcurrentHashMap是线程安全的Map实现类,它在juc包下的。线程安全的Map实现类除了ConcurrentHashMap还有一个叫做Hashtable。当然了,也可以使用Collections来包装出一个线程安全的Map,但无论是Hashtable还是Collections包装出来的都比较低效(因为是直接在外层套synchronize);
      HashMap不是线程安全的,多线程环境下有可能会有数据丢失和获取不了最新数据的问题
    • ConcurrentHashMap 本质上是一个 HashMap,因此功能和 HashMap 一样,但是ConcurrentHashMap 在 HashMap 的基础上,提供了并发安全的实现。并发安全的主要实现是通过对指定的 Node 节点加锁,来保证数据更新的安全性
    • 底层数据结构
      JDK1.7底层采用分段的数组+链表实现
      JDK1.8 采用的数据结构跟HashMap1.8的结构一样,数组+链表/红黑二叉树
    • 加锁的方式
      JDK1.7采用Segment分段锁,底层使用的是ReentrantLock
      JDK1.8采用CAS添加新节点,(如果已存在节点)采用synchronized锁定链表或红黑二叉树的首节点,相对Segment分段锁粒度更细,性能更好
  • 红黑树

    • Red-Black Tree是一种自平衡的二叉搜索树,它通过一些附加的信息(颜色标记)保持树的平衡。这种平衡性质确保了在最坏情况下对于各种基本动态集合操作(插入、删除、查找)的性能都有较好的上界,保证了树的高度是对数级别的。
    • 在Java中,TreeMapTreeSet 类使用红黑树来实现有序映射和有序集合。
    • 红黑树具有以下几个特征:
      1. 节点颜色: 每个节点都带有颜色,可以是红色或黑色。
      2. 根节点和叶子节点: 根节点和所有叶子节点(NIL节点)都是黑色的。
      3. 相邻节点颜色: 相邻的节点不能都是红色。也就是说,红色节点不能直接相连,黑色节点可以相连。
      4. 从任一节点到其每个叶子的所有路径都包含相同数目的黑色节点。
        这些规则确保了红黑树的关键性质,即任意一条从根到叶子的路径都不会超过最短路径的两倍长。这保证了红黑树在动态插入和删除操作时能够保持相对平衡,从而避免了出现极端不平衡的情况。
    • 红黑树插入和删除:都可能导致树失去平衡,因此需要通过旋转和重新着色等操作来维护平衡性质。
      • 插入操作:
        1. 将节点插入: 将新节点插入到红黑树的合适位置,通常是按照二叉搜索树的插入规则。
        2. 新节点着色为红色: 插入的新节点着色为红色,以便更容易维护平衡性质。
        3. 重新着色和旋转: 根据父节点、祖父节点、叔叔节点等之间的颜色关系,可能需要进行以下操作:
          • 情况1: 如果父节点是黑色,那么不需要做额外操作,树仍然保持平衡。
          • 情况2: 如果父节点是红色而叔叔节点也是红色,可以通过颜色翻转来保持平衡。
          • 情况3: 如果父节点是红色而叔叔节点是黑色,并且当前节点是父节点的右子节点,可以通过左旋转和右旋转来保持平衡。
          • 情况4: 如果父节点是红色而叔叔节点是黑色,并且当前节点是父节点的左子节点,可以通过右旋转来保持平衡。
      • 删除操作:
        1. 执行普通的二叉搜索树删除操作: 将要删除的节点从树中删除,并根据子节点的情况进行适当的替换。
        2. 重新着色和旋转: 删除操作可能破坏了红黑树的平衡性质,因此可能需要进行以下操作:
          • 情况1: 如果删除的节点或替代节点是红色,只需将替代节点着色为黑色。
          • 情况2: 如果删除的节点是黑色,而替代节点是红色,可以将替代节点着色为黑色。
          • 情况3: 如果删除的节点和替代节点都是黑色,可能需要通过重新着色和旋转来保持平衡。
  • Java 集合使用泛型的好处。

    • 可以确保类型安全,避免强制类型转换,提高代码的可读性和可维护性,能够在编译时错误检测。
    • “泛型” 意味着编写的代码可以被不同类型的对象所重用。
      以集合来举例,使用泛型的好处是我们不必因为添加元素类型的不同而定义不同类型的集合,如整型集合类,浮点型集合类,字符串集合类,而这并不是最重要的,因为这也只需把底层存储设置Object即可,添加的数据全部都可向上转型为Object。?? 更重要的是我们可以通过规则按照自己的想法控制存储的数据类型。?
  • 泛型类

    • 泛型类的声明和非泛型类的声明类似,除了在类名后面添加了类型参数声明部分。和泛型方法一样,泛型类的类型参数声明部分也包含一个或多个类型参数,参数间用逗号隔开。一个泛型参数,也被称为一个类型变量,是用于指定一个泛型类型名称的标识符。因为他们接受一个或多个参数,这些类被称为参数化的类或参数化的类型。
    • 类型通配符 一般是 使用 ? 代替具体的类型参数。 例如 List<?> 在逻辑上是List, List 等所有 List<具体类型实参>的父类。
      1
      2
      3
      4
      5
      6
      7
      8
      9
      public class Box<T> {
      private T t;
      public void add(T t) {
      this.t = t;
      }
      public T get() {
      return t;
      }
      }
  • 类型擦除

    • Java 中的泛型基本上都是在编译器这个层次来实现的,在生成的字节代码中是不包含泛型中的类型信息的。
      使用泛型的时候加上的类型参数,如 List 和 List<?> 等类型在编译时会被擦除为原始类型 List。JVM 看到的只是 List,而由泛型附加的类型信息对 JVM 是不可见的。这个过程就称为类型擦除。
    • 类型擦除的基本过程是找到用来替换类型参数的具体类,一般是 Object。如果指定了类型参数的上界的话,则使用这个上界。编译器会将代码中的类型参数都替换成具体的类。这种替换的行为是泛型在 Java 中实现的一部分,它确保了泛型代码在运行时不会受到泛型类型信息的影响,从而保持了 Java 的向后兼容性。
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      // 有一个泛型类或接口如下
      public class MyClass<T> {
      }
      // 在类型擦除之后,它等价于
      public class MyClass {
      }
      // 对于泛型方法:
      public <T> void myMethod(T item) {
      }
      // 在类型擦除后,它等价于
      public void myMethod(Object item) {
      }

JVM

  • “Java 跨平台”
    因为有 JVM 屏蔽了底层操作系统。Java源代码会被编译为class文件,class文件运行在JVM之上。JVM是面向操作系统的,它负责把Class字节码解释成系统所能识别的指令并执行,同时也负责程序运行时内存的管理。安装JDK的时可以发现JDK是分「不同的操作系统」,JDK里是包含JVM的,所以Java依赖着JVM实现了『跨平台』

  • JVM 组成。
    java虚拟机是jdk的一个部分,有四大组成部分。
    1、类加载器:将class字节码文件中的内容加载到内存中。
    2、运行时数据区域:负责管理JVM 使用到的内存,比如创建对象和销毁对象。
    3、执行引擎:将字节码文件中的指令解释成机器码,同时使用即时编译器优化性能,用gc回收内存。
    4、本地接口:调用本地已经编译好的方法(不在字节码文件中)比如虚拟机中提供的cpp方法。
    而 JVM 的内存结构,往往指的就是JVM定义的「运行时数据区域」。简单来说就分为了5大块:方法区、堆、程序计数器、虚拟机栈、本地方法栈。其中线程共享:堆、方法区,私有:虚拟机栈、程序计数器、本地方法栈。

  • JVM 参数:

    1. 通用参数: -version:显示Java版本信息。 -help-?:显示命令行选项和使用信息。
    2. 堆相关参数: -Xms<size>:设置初始堆大小。 -Xmx<size>:设置最大堆大小。 -Xmn<size>:设置年轻代的大小。
      -XX:MaxPermSize=<size>:设置持久代(Java 8之前)的最大大小。- -XX:MaxMetaspaceSize=<size>:设置元空间(Java 8及更高版本)的最大大小。
    3. 垃圾回收相关参数: - -XX:+UseSerialGC:使用串行垃圾回收器。 - -XX:+UseParallelGC:使用并行垃圾回收器。
      -XX:+UseConcMarkSweepGC:使用CMS垃圾回收器。-XX:+UseG1GC:使用G1回收器。-XX:+UseZGC:使用ZGC
    4. 性能调优参数:
      -XX:ThreadStackSize=<size>:设置线程堆栈大小。
      -XX:CompileThreshold=<threshold>:设置方法调用的编译阈值。
      -XX:MaxGCPauseMillis=<milliseconds>:设置垃圾回收最大停顿时间目标。
    5. 调试参数: -Xdebug:开启远程调试。 -Xrunjdwp:transport=dt_socket,address=<address>,server=y,suspend=n:配置JDWP远程调试。 -verbose:gc:输出垃圾回收详细信息。
    6. 应用程序性能分析参数: - -javaagent:<path-to-agent-jar>:启用Java代理,例如用于性能分析工具。
  • Java 编译到执行的过程?

    • Java源码到执行的过程,从JVM的角度看可以总结为四个步骤:编译->加载->解释->执行
    • 「编译」java源代码经过 语法分析、语义分析、注解处理 最后才生成会class文件。对泛型的擦除和Lombok就在编译阶段。
    • 「加载」又可以细分步骤为:装载->连接->初始化。装载则把class文件装载至JVM,连接则校验class信息、分配内存空间及赋默认值,初始化则为变量赋值为正确的初始值。连接里又可以细化为:验证、准备、解析
      • 「装配」阶段可以总结为:查找并加载类的二进制数据,在JVM「堆」中创建一个java.lang.Class类的对象,并将类相关的信息存储在JVM「方法区」中
        • 【装载时机】为了节省内存的开销,并不会一次性把所有的类都装载至JVM,而是等到「有需要」的时候才进行装载(比如new和反射等等)
        • 【装载发生】class文件是通过「类加载器」装载到jvm中的,为了防止内存中出现多份同样的字节码,使用了双亲委派机制
        • 【装载规则】JDK 中的本地方法类一般由根加载器(Bootstrp loader)装载,JDK 中内部实现的扩展类一般由扩展加载器(ExtClassLoader)实现装载,而程序中的类文件则由系统加载器(AppClassLoader)实现装载。
      • 「连接」这个阶段它做的事情可以总结为:对class的信息进行验证、为「类变量」分配内存空间并对其赋默认值。又可以细化为几个步骤:
        1. 验证:验证类是否符合 Java 规范和 JVM 规范
        2. 准备:为类的静态变量分配内存,初始化为系统的初始值
        3. 解析:将符号引用转为直接引用的过程
      • 「初始化」阶段可以总结为:为类的静态变量赋予正确的初始值。过程大概就是收集class的静态变量、静态代码块、静态方法至()方法,随后从上往下开始执行。如果「实例化对象」则会调用方法对实例变量进行初始化,并执行对应的构造方法内的代码。
    • 「解释」则是把字节码转换成操作系统可识别的执行指令,在JVM中会有字节码解释器和即时编译器。在解释时会对代码进行分析,查看是否为「热点代码」,如果为「热点代码」则触发JIT编译,下次执行时就无需重复进行解释,提高解释速度
      • JVM会对「热点代码」做编译,非热点代码直接进行解释。当JVM发现某个方法或代码块的运行特别频繁的时候,就有可能把这部分代码认定为「热点代码」。
      • 使用「热点探测」来检测是否为热点代码。「热点探测」一般有两种方式,计数器和抽样。HotSpot使用的是「计数器」的方式进行探测,为每个方法准备了两类计数器:方法调用计数器和回边计数器。这两个计数器都有一个确定的阈值,当计数器超过阈值溢出了,就会触发JIT编译。即时编译器把热点方法的指令码保存起来,下次执行的时候就无需重复的进行解释,直接执行缓存的机器语言。
    • 「执行」操作系统把解释器解析出来的指令码,调用系统的硬件执行最终的程序指令。
  • 类加载器、、

    • JDK自带有三个类加载器:Bootstrap ClassLoader、ExtClassLoader、AppClassLoader;
      JDK 中的本地方法类一般由根加载器(Bootstrp)装载,内部实现的扩展类一般由扩展加载器(Ext)装载,而程序中的类文件则由系统加载器(AppClassLoader)实现装载。
      • BootStrapClassLoader是ExtClassLoader的父类加载器,默认负责加载%JAVA HOME%lib下的jar包和class文件
      • ExtClassLoader是AppClassLoader的父类加载器,负贵加载%JAVA HOME%/lib/ext文件夹下的jar包和class类。
      • AppClassLoader是自定义类加载器的父类,负贵加载classpath下的类文件。
  • 双亲委派模型

    • class文件是通过「类加载器」装载至JVM中的,为了防止内存中存在多份同样的字节码,使用了双亲委派机制(它不会自己去尝试加载类,而是把请求委托给父加载器去完成,依次向上)
    • JDK 中的本地方法类一般由根加载器(Bootstrp loader)装载,内部实现的扩展类一般由扩展加载器(ExtClassLoader)实现装载,而程序中的类文件则由系统加载器(AppClassLoader)实现装载。
      AppClassLoader的父加载器为Ext ClassLoader、Ext ClassLoader的父加载器为BootStrap ClassLoader。这里的父子关系并不是通过继承实现的,而是组合。
    • 打破双亲委派机制:自定义ClassLoader,重写loadClass方法(只要不依次往上交给父加载器进行加载,就算是打破双亲委派机制)
    • 打破双亲委派机制案例:Tomcat
      • 为了Web应用程序类之间隔离,为每个应用程序创建WebAppClassLoader类加载器
      • 为了Web应用程序类之间共享,把ShareClassLoader作为WebAppClassLoader的父类加载器,如果WebAppClassLoader加载器找不到,则尝试用ShareClassLoader进行加载
      • 为了Tomcat本身与Web应用程序类隔离,用CatalinaClassLoader类加载器进行隔离和加载Tomcat本身的类
      • 为了Tomcat与Web应用程序类共享,用CommonClassLoader作为CatalinaClassLoader和ShareClassLoader的父类加载器
      • ShareClassLoader、CatalinaClassLoader、CommonClassLoader的目录可以在Tomcat的catalina.properties进行配置
    • ???线程上下文加载器:由于类加载的规则,很可能导致父加载器加载时依赖子加载器的类,导致无法加载成功(BootStrap ClassLoader无法加载第三方库的类),所以存在「线程上下文加载器」来进行加载。
  • JVM的内存结构?

    • .class文件会被类加载器装载至JVM中,并且JVM会负责程序「运行时」的「内存管理」。而JVM的内存结构,往往指的就是JVM定义的「运行时数据区域」。简单来说就分为了5大块:方法区、堆、程序计数器、虚拟机栈、本地方法栈
    • 要值得注意的是:这是JVM「规范」的分区概念,到具体的实现落地,不同的厂商实现可能是有所区别的。
    • 程序计数器:线程切换意味着「中断」和「恢复」,那自然就需要有一块区域来保存「当前线程的执行信息」。所以,程序计数器就是用于记录各个线程执行的字节码的地址(分支、循环、跳转、异常、线程恢复等都依赖于计数器)
    • 虚拟机栈:每个线程在创建的时候都会创建一个「虚拟机栈」,每次方法调用都会创建一个「栈帧」。每个「栈帧」会包含几块内容:局部变量表、操作数栈、动态连接和返回地址。它的作用:它保存方法的局部变量、部分变量的计算并参与了方法的调用和返回。
    • 本地方法栈:本地方法栈跟虚拟机栈的功能类似,虚拟机栈用于管理 Java 函数的调用,而本地方法栈则用于管理本地方法的调用。这里的「本地方法」指的是「非Java方法」,一般本地方法是使用C语言实现的。
    • 方法区:在HotSpot虚拟机,就会常常提到「永久代」这个词,jdk8 前用「永久代」实现了「方法区」,而很多其他厂商虚拟机其实是没有「永久代」的概念的。JDK8中,已经用「元空间」来替代了「永久代」作为「方法区」的实现了。
      • 方法区主要是用来存放已被虚拟机加载的「类相关信息」:包括类信息、常量池。类信息又包括了类的版本、字段、方法、接口和父类等信息;常量池又可以分「静态常量池」和「运行时常量池」
        • 静态常量池主要存储的是「字面量」以及「符号引用」等信息,也包括了我们说的「字符串常量池」。
        • 运行时常量池存储的是类加载时生成的「直接引用」等信息。
      • 从逻辑分区的角度而言「常量池」是属于「方法区」的。但自从在 jdk7 以后,就已经把运行时常量池和静态常量池转移到了堆内存中进行存储(对于物理分区来说运行时常量池和静态常量池就属于堆)
      • 从 jdk8 已经把「方法区」的实现从「永久代」变成「元空间」,有什么区别?
        最主要的区别就是:元空间存储不在虚拟机中,而是使用本地内存,JVM 不会再出现方法区的内存溢出,以往「永久代」经常因为内存不够用导致跑出OOM异常。按JDK8版本,总结起来其实就相当于:「类信息」是存储在「元空间」的(也有人把类信息这块叫做类信息常量池,主要是叫法不同,意思到位就好),而「常量池」从JDK7开始,从物理存储角度上就在「堆」中。
    • :堆是线程共享的区域,几乎类的实例和数组分配的内存都来自于它。堆被划分为「新生代」和「老年代」,「新生代」又被进一步划分为 Eden 和 Survivor 区,最后 Survivor 由 From Survivor 和 To Survivor 组成。将堆内存分开了几块区域,主要跟内存回收有关(垃圾回收机制)
  • jvm线程共享区?
    线程共享:堆区和方法区
    线程独有:栈、本地方法栈和程序计数器

  • JVM内存结构和Java内存模型有啥区别?
    没有啥直接关联,,Java内存模型是跟「并发」相关的,它是为了屏蔽底层细节而提出的规范,希望在上层(Java层面上)在操作内存时在不同的平台上也有相同的效果;JVM内存结构(又称为运行时数据区域),它描述着当我们的class文件加载至虚拟机后,各个分区的「逻辑结构」是如何的,每个分区承担着什么作用。

  • Java对象创建过程?new一个对象的步骤?

    1. 检查类符号引用:首先,JVM 会检查 new 关键字后面的类符号引用,确保在常量池中能找到对应的类。
    2. 加载类:如果在常量池中找到了类符号引用,JVM 就会加载这个类,包括加载、连接(验证、准备、解析)和初始化阶段。(如果已经加载了这个类,那么类信息可以在方法区中找到)
    3. 分配内存:JVM 会根据类的定义,在堆内存中为对象分配内存空间。这个内存空间包括对象的所有成员变量。这个过程包括选择内存分配方式(如指针碰撞、空闲列表、TLAB)、分配内存并进行内存清零。
    4. 初始化对象:分配完内存空间后,JVM 会将对象的除对象头外的内存空间初始化为默认值(基本数据类型为 0,引用类型为 null)。最后,JVM 会设置对象的对象头,包括哈希码、GC 信息等元信息。
    5. 调用构造方法:执行对象的初始化逻辑,包括对成员变量进行赋值、执行一些初始化操作等。
    6. 返回对象引用:最后,new 操作符会返回一个指向新创建对象的引用,通过这个引用可以在程序中操作对象的属性和调用对象的方法。
    7. 这些步骤是创建一个对象的基本流程,无论是通过 new 关键字创建对象,还是通过反射、序列化等方式创建对象,都要经历这些步骤。
  • 简述Java的对象结构(JMM??)

    • Java对象由三个部分组成:对象头、实例数据、对齐填充。
      • 对象头由两部分组成,第一部分存储对象自身的运行时数据:哈希码、GC分代年龄、锁标识状态、线程持有的锁、偏向线程ID(一般占32/64 bit)。第二部分是指针类型,指向对象的类元数据类型(即对象代表哪个类)。如果是数组对象,则对象头中还有一部分用来记录数组长度。
      • 实例数据用来存储对象真正的有效信息(包括父类继承下来的和自己定义的)
      • 对齐填充:JVM要求对象起始地址必须是8字节的整数倍(8字节对齐)
  • 程序员可以根据需要控制JVM在特定时间进行垃圾回收吗?
    在大多数情况下,不能直接控制 JVM 在特定时间进行垃圾回收。垃圾回收是 JVM 的自动内存管理系统的一部分,其目标是在运行时自动回收不再使用的内存,而不需要程序员显式干预。但是,可以通过一些 JVM 参数和 API 间接地影响垃圾回收的行为。

  • java的对象是怎么被回收的?

    • 什么是垃圾:只要对象不再被使用,那即是垃圾。GC就是对堆中的对象回收。
    • 如何判断为垃圾:常用的算法有两个「引用计数法」和「可达性分析法」;
    • JVM使用的是可达性分析算法「GC Roots」:GC Roots是一组必须活跃的引用,跟GC Roots无关联的引用即是垃圾,可被回收
    • 常见的垃圾回收算法:标记清除、标记复制、标记整理。整理算法是前两者的折中:未必要有一块「大的完整空间」才能解决内存碎片的问题,我只要能在「当前区域」内进行移动,把存活的对象移到一边,把垃圾移到一边,那再将垃圾一起删除掉,不就没有内存碎片了嘛
    • 分代:堆分了「新生代」和「老年代」,「新生代」又分为「Eden」和「Survivor」区,「Survivor」区又分为「From Survivor」和「To Survivor」区
      堆内存占比:年轻代占堆内存1/3,老年代占堆内存2/3。Eden区占年轻代8/10,Survivor区占年轻代2/10(其中From 和To 各站1/10)
    • 为什么需要分代:大部分对象都死得早,只有少部分对象会存活很长时间。在堆内存上都会在物理或逻辑上进行分代,为了使「stop the world」持续的时间尽可能短以及提高并发式GC所能应付的内存分配速率。
    • 垃圾回收器:实际上就是「实现了」垃圾回收算法(标记复制、标记整理以及标记清除算法)
      「年轻代」的垃圾收集器有:Seria、Parallel Scavenge、ParNew
      「老年代」的垃圾收集器有:Serial Old、Parallel Old、CMS
    • Minor GC:当Eden区满了则触发,从GC Roots往下遍历,年轻代GC不关心老年代对象
    • 什么是card table【卡表】:空间换时间(类似bitmap),能够避免扫描老年代的所有对应进而顺利进行Minor GC案例:老年代对象持有年轻代对象引用)
    • Full GC:。。。
  • 如何判断对象可以被回收判断?

    1. 引用计数:每个对象有一个引用计数属性,新增一个引用时计数加1,引用释放时计数减1,计数为0时可以回收。此方法简单,无法解决对象相互循环引用的问题。
    2. 可达性分析:从GC Roots开始向下搜索,搜索所走过的路径称为引用链。当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的,不可达对象。
  • JVM有哪些垃圾回收算法?

    1. 标记清除算法:a.标记阶段:把垃圾内存标记出来;b.清除阶段:直接将垃圾内存回收。
      这种算法是比较简单的,但是有个很严重的问题,就是会产生大量的内存碎片。
    2. 复制算法:为了解决标记清除算法的内存碎片问题,就产生了复制算法。复制算法将内存分为大小相等的两半,每次只使用其中一半。垃圾回收时,将当前这一块的存活对象全部拷贝到另一半,然后当前这一半内存就可以直接清除。这种算法有内存碎片,但是他的问题就在于浪费空间。而且,他的效率跟存活对象的个数有关。
    3. 标记压缩算法:为了解决复制算法的缺陷,就提出了标记压缩算法。这种算在标记阶段跟标记清除算法是一样的,但是在完成标记之后,不是直接清理垃圾内存,而是将存活对象往一端移动,然后将边界以外的所有内存直接清除。
  • JVM 分代回收、、

    • 堆的区域划分
      1、堆被分为了两份:「新生代」和「老年代」【1:2】
      2、对于新生代,内部又被分为了「Eden」和「Survivor」区,「survivor」区又分为「From」和「To」区【8:1:1】
    • 对象回收分代回收策略
      1. 新创建的对象,都会先分配到eden区
      2. 当伊甸园内存不足,标记伊甸园与from(现阶段没有)的存活对象
      3. 将存活对象采用复制算法复制到to中,复制完毕后,伊甸园和from 内存都得到释放
      4. 经过一段时间后伊甸园的内存又出现不足,标记eden区域to区存活的对象,将其复制到from区
      5. 当幸存区对象熬过几次回收(最多15次),晋升到老年代(幸存区内存不足或大对象会提前晋升)
    • MinorGC、Mixed Gc、FullGC的区别是什么
      • MinorGC【young GC】(新生代垃圾回收):发生在新生代的垃圾回收,暂停时间短(STW)
      • Mixed GC(混合回收):新生代+老年代部分区域的垃圾回收,G1收集器特有
      • FullGC(老年代垃圾回收):新生代+老年代完整垃圾回收,暂停时间长(STW),应尽力避免
    • Minor GC、Mixed GC 和 Full GC 的触发条件如下:
      • Minor GC:在新对象创建时,当 Eden 区满时触发 Minor GC。通常情况下,Minor GC 会回收年轻代的 Eden 区和 Survivor 区中的垃圾对象。
      • Mixed GC:在 G1 垃圾回收器中,Mixed GC 是指同时执行部分 Young GC 和部分 Old GC 的过程;在一次 Full GC 后,可能会触发 Mixed GC 来对部分老年代和年轻代进行回收。
      • Full GC:在老年代空间不足、永久代空间不足(如果使用永久代)、老年代连续多次触发 Minor GC 无法回收足够的空间时,或者明确调用 System.gc() 方法时,可能会触发 Full GC。
  • CMS垃圾收集器

    • CMS的全称:Concurrent Mark Sweep,翻译过来是「并发标记清除」
      对比其他垃圾收集器(Seria和Parallel和parNew),它最大的不同点就是“并发”:在GC线程工作的时候,用户线程不会完全停止,用户线程在“部分场景下”与GC线程一起并发执行,避免老年代 GC出现长时间的卡顿(Stop The World)
    • CMS可简单分为5个步骤:初始标记、并发标记、并发预清理、重新标记及并发清除,即CMS主要是实现了「标记清除」GC
      1. 「初始标记」会标记GCRoots「直接关联」的对象以及「年轻代」指向「老年代」的对象。这个过程是会发生Stop The World的。但这个阶段的速度算是很快的,因为没有「向下追溯」(只标记一层)
      2. 「并发标记」这个过程是不会停止用户线程的(不会发生 Stop The World)。这一阶段主要是从GC Roots向下「追溯」,标记所有可达的对象;在GC的角度而言,是比较耗费时间的(需要追溯)
      3. 「并发预处理」这个阶段主要想干的事情:希望能减少下一个阶段「重新标记」所消耗的时间,因为下一个阶段「重新标记」是需要Stop The World的
        这个阶段会扫描可能由于「并发标记」时导致老年代发生变化的对象(「并发标记」这个阶段由于用户线程是没有被挂起的,可能有些对象从新生代晋升到了老年代,可能有些大对象直接分配到了老年代,可能老年代或者新生代的对象引用发生了变化…),会再扫描一遍标记为dirty的卡页
        针对老年代的对象,其实还是可以借助类card table的存储(将老年代对象发生变化所对应的卡页标记为dirty);对于新生代的对象,我们还是得遍历新生代来看看在并发标记过程中有没有对象引用了老年代..
      4. 「重新标记」阶段会Stop The World,这个过程的停顿时间其实很大程度上取决于上面「并发预处理」阶段(可以发现,这是一个追赶的过程:一边在标记存活对象,一边用户线程在执行产生垃圾)
      5. 最后就是「并发清除」阶段,不会Stop The World,一边用户线程在执行,一边GC线程在回收不可达的对象。这个过程,还是有可能用户线程在不断产生垃圾,但只能留到下一次GC 进行处理了,产生的这些垃圾被叫做“浮动垃圾”,完了以后会重置 CMS 算法相关的内部数据,为下一次 GC 循环做准备
    • 比起G1,CMS有什么缺点呢?
      1. 空间需要预留:CMS垃圾收集器可以一边回收垃圾,一边处理用户线程,那需要在这个过程中保证有充足的内存空间供用户使用。如果CMS运行过程中预留的空间不够用了,会报错(Concurrent Mode Failure),这时会启动 Serial Old垃圾收集器进行老年代的垃圾回收,会导致停顿的时间很长。
      2. 内存碎片问题:CMS本质上是实现了「标记清除算法」的收集器(从过程就可以看得出),这会意味着会产生内存碎片。由于碎片太多,又可能会导致内存空间不足所触发full GC,CMS一般会在触发full GC这个过程对碎片进行整理。
      3. 要处理内存碎片的问题(整理),整理涉及到「移动」/「标记」,那这个过程肯定会Stop The World的,如果内存足够大(意味着可能装载的对象足够多),那这个过程卡顿也是需要一定的时间的。
  • G1垃圾收集器 https://www.bilibili.com/read/cv26352521/?spm_id_from=333.999.0.0&jump_opus=1

    • 在G1垃圾收集器的世界中,堆的划分不再是物理形式,而是以”逻辑”的形式进行划分;
      使用CSet来存储可回收Region的集合,使用RSet来处理跨代引用的问题(注意:RSet不保留 年轻代相关的引用关系)
      G1垃圾收集器世界的「堆」空间分布:
      从图上就可以发现,堆被划分了多个同等份的区域 Region
      不过像之前的「分代」概念在G1的世界还是一样奏效的,比如说:新对象一般会分配到Eden区、经过默认15次的Minor GC新生代的对象如果还存活,会移交到老年代等等…老年代、新生代、Survivor这些规则是跟CMS一样的,此外,G1中还有一种叫 Humongous(大对象)区域,其实就是用来存储特别大的对象(大于Region内存的一半),一旦发现没有引用指向大对象,就可直接在年轻代的Minor GC中被回收掉
    • 为什么要将堆空间进行细分多个小的区域?
      像以前的垃圾收集器都是对堆进行「物理」划分,如果堆空间(内存)大的时候,每次进行「垃圾回收」都需要对一整块大的区域进行回收,那收集的时间是不好控制的。而划分多个小区域之后,那对这些「小区域」回收就容易控制「收集时间」了
    • GC过程:在G1收集器中可以主要分为有Minor GC(Young GC) 和 Mixed GC,Full GC
      • 【Eden区满则触发】Minor GC 回收过程可简单分为:(STW) 扫描 GC Roots、更新&&处理Rset、复制清除
      • 【整堆空间占一定比例则触发】Mixed GC 依赖「全局并发标记」,得到CSet(可回收Region),就进行「复制清除」
      • 【也有些特殊场景可能会发生】Full GC
    • Minor GC:
      • 触发时机跟前面提到过的垃圾收集器都是一样的
        等到Eden区满了之后,会触发Minor GC。Minor GC同样也是会发生Stop The World的
        要补充说明的是:在G1的世界里,新生代和老年代所占堆的空间是没那么固定的(会动态根据「最大停顿时间」进行调整)
        这块要知道会给我们提供参数进行配置就好了
        所以,动态地改变年轻代Region的个数可以「控制」Minor GC的开销
      • 回收过程,可以简单分为为三个步骤:根扫描、更新&&处理 RSet、复制对象
        第一步应该很好理解,因为这跟之前CMS是类似的,可以理解为初始标记的过程
        第二步涉及到「Rset」的概念
        从上一次我们聊CMS回收过程的时候,同样讲到了Minor GC,它是通过「卡表」(cart table)来避免全表扫描老年代的对象
        因为Minor GC 是回收年轻代的对象,但如果老年代有对象引用着年轻代,那这些被老年代引用的对象也不能回收掉
        同样的,在G1也有这种问题(毕竟是Minor GC)。CMS是卡表,而G1解决「跨代引用」的问题的存储一般叫做RSet
        只要记住,RSet这种存储在每个Region都会有,它记录着「其他Region引用了当前Region的对象关系」
        对于年轻代的Region,它的RSet 只保存了来自老年代的引用(因为年轻代的没必要存储啊,自己都要做Minor GC了)
        而对于老年代的 Region 来说,它的 RSet 也只会保存老年代对它的引用(在G1垃圾收集器,老年代回收之前,都会先对年轻代进行回收,所以没必要保存年轻代的引用)
        那第二步看完RSet的概念,应该也好理解了吧?
        无非就是处理RSet的信息并且扫描,将老年代对象持有年轻代对象的相关引用都加入到GC Roots下,避免被回收掉
        第三步也挺好理解的:把扫描之后存活的对象往「空的Survivor区」或者「老年代」存放,其他的Eden区进行清除
    • Mixed GC:会选定所有的年轻代Region,部分「回收价值高」的老年代Region(回收价值高其实就是垃圾多)进行采集
    • Full GC
  • JVM调优到底是干啥的?

    • 我们一般优化系统的思路是这样的:
      1. 一般来说关系型数据库是先到瓶颈,首先排查是否为数据库的问题(这个过程中就需要评估自己建的索引是否合理、是否需要引入分布式缓存、是否需要分库分表等等)
      2. 然后,我们会考虑是否需要扩容(横向和纵向都会考虑)(这个过程中我们会怀疑是系统的压力过大或者是系统的硬件能力不足导致系统频繁出现问题)
      3. 接着,应用代码层面上排查并优化(扩容是不能无止境的,里头里外都是钱阿。这个过程中我们会审视自己写的代码是否存在资源浪费的问题,又或者是在逻辑上可存在优化的地方,比如说通过并行的方式处理某些请求)
      4. 再接着,JVM层面上排查并优化(审视完代码之后,这个过程我们观察JVM是否存在多次GC问题等等)
      5. 最后,网络和操作系统层面排查(这个过程查看内存/CPU/网络/硬盘读写指标是否正常等等)
    • 绝大多数情况下到第三步就结束了,一般经过「运维团队」给我们设置的JVM和机器上的参数已经满足绝大多数的需求了。
    • 在我的理解下,调优JVM其实就是在「理解」JVM内存结构以及各种垃圾收集器前提下,结合自己的现有的业务来「调整参数」,使自己的应用能够正常稳定运行。一般调优JVM我们认为会有几种指标可以参考:『吞吐量』、『停顿时间』和『垃圾回收频率』。基于这些指标,我们就有可能需要调整:
      1. 内存区域大小以及相关策略(比如整块堆内存占多少、新生代占多少、老年代占多少、Survivor占多少、晋升老年代的条件等等)比如(-Xmx:设置堆的最大值、-Xms:设置堆的初始值、-Xmn:表示年轻代的大小、-XX:SurvivorRatio:伊甸区和幸存区的比例等等)(按经验来说:IO密集型的可以稍微把「年轻代」空间加大些,因为大多数对象都是在年轻代就会灭亡。内存计算密集型的可以稍微把「老年代」空间加大些,对象存活时间会更长些)
      2. 垃圾回收器(选择合适的垃圾回收器,以及各个垃圾回收器的各种调优参数)比如(-XX:+UseG1GC:指定 JVM 使用的垃圾回收器为 G1、-XX:MaxGCPauseMillis:设置目标停顿时间、-XX:InitiatingHeapOccupancyPercent:当整个堆内存使用达到一定比例,全局并发标记阶段 就会被启动等等)
        没错,这些都是因地制宜,具体问题具体分析(前提是得懂JVM的各种基础知识,基础知识都不懂,谈何调优)
    • 一般我们是「遇到问题」之后才进行调优的,而遇到问题后需要利用各种的「工具」进行排查
      1. 通过jps命令查看Java进程「基础」信息(进程号、主类)。这个命令很常用的就是用来看当前服务器有多少Java进程在运行,它们的进程号和加载主类是啥
      2. 通过jstat命令查看Java进程「统计类」相关的信息(类加载、编译相关信息统计,各个内存区域GC概况和统计)。这个命令很常用于看GC的情况
      3. 通过jinfo命令来查看和调整Java进程的「运行参数」。
      4. 通过jmap命令来查看Java进程的「内存信息」。这个命令很常用于把JVM内存信息dump到文件,然后再用MAT( Memory Analyzer tool 内存解析工具)把文件进行分析
      5. 通过jstack命令来查看JVM「线程信息」。这个命令用常用语排查死锁相关的问题
      6. 还有近期比较热门的Arthas(阿里开源的诊断工具),涵盖了上面很多命令的功能且自带图形化界面。
    • JVM 的 JIT 优化技术:比较出名的有两种:方法内联和逃逸分析
      所谓方法内联就是把「目标方法」的代码复制到「调用的方法」中,避免发生真实的方法调用
      因为每次方法调用都会生成栈帧(压栈出栈记录方法调用位置等等)会带来一定的性能损耗,所以「方法内联」的优化可以提高一定的性能。在JVM中也有相关的参数给予我们指定(-XX:MaxFreqInlineSize、-XX:MaxInlineSize)
      而「逃逸分析」则是判断一个对象是否被外部方法引用或外部线程访问的分析技术,如果「没有被引用」,就可以对其进行优化,比如说:
      1. 锁消除(同步忽略):该对象只在方法内部被访问,不会被别的地方引用,那么就一定是线程安全的,可以把锁相关的代码给忽略掉
      2. 栈上分配:该对象只会在方法内部被访问,直接将对象分配在「栈」中(Java默认是将对象分配在「堆」中,是需要通过JVM垃圾回收期进行回收,需要损耗一定的性能,而栈内分配则快很多)
      3. 标量替换/分离对象:当程序真正执行的时候可以不创建这个对象,而直接创建它的成员变量来代替。将对象拆分后,可以分配对象的成员变量在栈或寄存器上,原本的对象就无需分配内存空间了
  • 有没有排查过线上 oom 的问题?

    • 没有,但是,知道。。。
      OOM 是 out of memory 的简称,表示程序需要的内存空间大于 JVM 分配的内存空间。OOM 后果就是导致程序崩溃;可以通俗理解:程序申请内存过大,虚拟机无法满足。
    • 导致 OOM 错误的情况一般是:
      1、给 JVM 虚拟机分配的内存太小,实际业务需求对内存的消耗比较多
      2、Java 应用里面存在内存泄漏的问题,或者应用中有大量占用内存的对象,并且没办法及时释放。我给大家解释一下内存泄漏和内存溢出是两个完全不一样的情况
      内存泄露:申请使用完的内存没有释放,导致虚拟机不机不能再次使用该内存,此时这段内存就泄露了,因为申请者不用了,而又不能被虚拟机分机分配给别人用。
      内存溢出:申请的内存超出了 JVM 能提供的内存大小,此时称之为溢出。
    • 常见的 OOM 异常情况有两种:
      java.lang.OutOfMemoryError: Java heap space ——>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 引用链的信息,就可以比较准确地定位泄漏代码的位置。如果是普通的内存溢出,确实有很多占用内存的对象,那就只需要提升堆内存空间即可。
  • Java 中会存在内存泄漏吗,请简单描述。
    理论上 Java 因为有垃圾回收机制(GC)不会存在内存泄露问题( 这也是 Java 被广泛使用于服务器端编程的一个重要原因 );然而在实际开发中,可能会存在无用但可达的对 象,这些对象 不能被 GC 回收 ,因此也会导致内存泄露的发生 。


JDBC

  • JDBC、Spring DAO、MyBatis?
    Java Database Connectivity、Spring Data Access Object和 MyBatis 是 Java 中用于数据库访问的三种不同的技术或模式。

    1. JDBC 是 Java 提供的标准数据库访问接口,它允许 Java 应用程序与不同的关系型数据库进行通信。JDBC 提供了一组 API,通过这些 API,开发者可以执行 SQL 查询、更新数据库、处理事务等操作。JDBC 是直接与数据库进行交互的底层技术,需要开发者编写较多的代码来处理数据库连接、SQL 执行和结果集处理等细节。
    2. Spring DAO 是 Spring 框架中的一个模块,它提供了一种高层次的、面向对象的数据库访问方式,通过封装底层的 JDBC 操作,简化了数据库访问的代码。Spring DAO 的目标是提供更高级别的抽象,使得开发者可以更专注于业务逻辑而不用过多关心数据库访问的细节。Spring DAO 提供了对声明式事务、异常处理等特性的支持。
    3. MyBatis 是一种基于 Java 的持久层框架,它提供了一种将 SQL 语句与 Java 对象进行映射的方式,通过 XML 或注解配置 SQL 映射关系。MyBatis 避免了手动编写大量 JDBC 代码,同时提供了更灵活的 SQL 控制和结果集映射。MyBatis 是一种半自动化的持久层框架,它允许开发者直接编写 SQL,但提供了一些便利的功能来简化数据库访问。
    4. 关系总结:
      • JDBC 是直接与数据库进行交互的底层技术,需要开发者编写更多的数据库访问相关代码。
      • Spring DAO 是 Spring 框架中的一部分,提供了更高层次的抽象,简化了数据库访问的代码,提供声明式事务等功能。
      • MyBatis 是一种基于 Java 的持久层框架,通过将 SQL 语句与 Java 对象进行映射,简化了数据库访问的代码,提供了更灵活的 SQL 控制和结果集映射。
        在实际应用中,Spring DAO 和 MyBatis 可以与 JDBC 结合使用,以便在不同层次上提供更多的抽象和功能,同时使开发更加方便。例如,可以使用 MyBatis 提供的 SQL 映射和结果集映射功能,与 Spring DAO 集成,同时利用 Spring 提供的声明式事务管理。
  • JDBC操作的步骤:
    加载数据库驱动类
    打开数据库连接
    执行sql语句
    处理返回结果
    关闭资源

  • JDBC事务的步骤:

    1. 连接数据库: 使用 DriverManager 获取数据库连接。这涉及提供数据库的连接字符串、用户名和密码等信息。
      1
      Connection connection = DriverManager.getConnection("jdbc:dbms://localhost:3306/mydatabase", "username", "password");
    2. 关闭自动提交: 默认情况下,每个 JDBC 连接都是自动提交的,即每个 SQL 语句都被立即执行并提交到数据库。在事务中,我们通常关闭自动提交。
    3. 执行事务操作: 在事务中,执行一系列的 SQL 操作,包括插入、更新等。这些操作将被延迟提交,直到显式调用 commit()
      1
      2
      Statement statement = connection.createStatement();
      statement.executeUpdate("INSERT INTO mytable (column1, column2) VALUES ('value1', 'value2')");
    4. 事务回滚: 如果在事务执行过程中发生错误或某些条件不满足,可以调用 rollback() 方法将事务回滚到开始状态,撤销之前的所有更改。
    5. 提交事务: 如果事务的执行没有发生错误,且符合预期,可以调用 commit() 方法将事务中的所有更改提交到数据库。
    6. 关闭连接: 当事务完成后,需要关闭数据库连接。关闭连接将释放资源并结束与数据库的通信。
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      // 整个事务的执行可以通过 try-catch 块进行异常处理,以确保在发生错误时能够执行回滚操作
      try {
      connection.setAutoCommit(false);
      // 执行事务操作
      // ...
      connection.commit();
      } catch (SQLException e) {
      connection.rollback();
      e.printStackTrace();
      } finally {
      connection.close();
      }
  • 在使用jdbc的时候,如何防止出现sql注入的问题?
    使用PreparedStatement类,而不是使用Statement类

  • 怎么在JDBC内调用一个存储过程
    使用CallableStatement

  • 是否了解连接池,使用连接池有什么好处?
    数据库连接是非常消耗资源的,影响到程序的性能指标。连接池是用来分配、管理、释放数据库连接的,可以使应用程序重复使用同一个数据库连接,而不是每次都创建一个新的数据库连接。通过释放空闲时间较长的数据库连接,避免数据库因为创建太多的连接而造成的连接遗漏问题,提高了程序性能。

  • 你所了解的数据源技术有那些?使用数据源有什么好处?
    Dbcp,c3p0等,用的最多还是c3p0,因为c3p0比dbcp更加稳定,安全;通过配置文件的形式来维护数据库信息,而不是通过硬编码。当连接的数据库信息发生改变时,不需要再更改程序代码就实现了数据库信息的更新。


Java并发

  • 并发、并行、串行之间的区别
    1,串行:一个任务执行完,才能执行下一个任务
    2,并行(Parallelism):多核CPU下,多个任务同时执行
    3,并发(Concurency):多核CPU下,多个线程轮流使用一个或多个CPU。两个任务整体看上去是同时执行,在底层,两个任务被拆成了很多份,然后一个一个执行,站在更高的度看来两个任务是同时在执行的

  • 进程与线程的区别。

    • 线程作为最小调度单位,进程作为资源分配的最小单位。
    • 进程是正在运行程序的实例,进程中包含了线程,每个线程执行不同的任务
      不同的进程使用不同的内存空间,在当前进程下的所有线程可以共享内存空间
      线程更轻量,线程上下文切换成本一般上要比进程上下文切换低
  • 怎么理解Java多线程? https://www.bilibili.com/read/cv22973356/?spm_id_from=333.999.0.0&jump_opus=1

  • 创建线程的方式有哪些?
    继承Thread类
    实现runnable接口
    实现Callable接口
    线程池创建线程(项目中使用方式)

  • runnable 和 callable 有什么区别
    Runnable 接口run方法没有返回值
    Callable接口call方法有返回值,需要FutureTask获取结果
    Callable接口的call()方法允许抛出异常;而Runnable接口的run()方法的异常只能在内部消化,不能继续上抛

  • 线程包括哪些状态?
    Thread中的6个枚举State:新建(NEW)、可运行(RUNNABLE)、阻塞(BLOCKED)、等待(WAITING)、时间等待(TIMED_WALTING)、终止(TERMINATED)

  • 线程状态之间是如何变化的?

    • 创建线程对象是新建状态
    • 调用了start()方法转变为可执行状态(还要获取到CPU执行权才能运行)
    • 线程获取到了CPU的执行权,执行结束是终止状态
    • 在可执行状态的过程中,如果没有获取CPU的执行权,可能会切换其他状态
      • 如果没有获取锁(synchronized或lock)进入阻塞状态,获得锁再切换为可执行状态
      • 如果线程调用了wait()方法进入等待状态,其他线程调用notify()唤醒后可切换为可执行状态
      • 如果线程调用了sleep(50)方法,进入计时等待状态,到时间后可切换为可执行状态
  • Java 线程开启、终止、同步的方式

    • 开启线程:
      1. 继承 Thread 类并重写 run 方法,然后创建线程对象并调用 start 方法启动线程。
      2. 实现 Runnable 接口并实例化一个 Runnable 对象,然后通过 Thread 类的构造方法将 Runnable 对象传递给线程,调用 start方法启动。
      3. run()start()有什么区别?
        start():用来启动线程,通过该线程调用run方法执行run方法中所定义的逻辑代码。只能被调用一次。
        run():封装了要被线程执行的代码,可以被调用多次,
    • 线程终止:
      • 使用退出标志,使线程正常退出,也就是当run方法完成后线程终止??用 volatile 标记变量控制线程的执行状态
      • 使用stop方法强行终止(不推荐,方法已作废),可能会导致线程状态不一致或资源未正确释放。
      • 使用interrupt方法中断线程。这种方式会向线程发送一个中断信号,线程可以在适当的时候检查中断标志并做出响应,安全地停止线程。
        • 打断阻塞的线程(sleep,wait,join)的线程,线程会抛出InterruptedException异常
        • 打断正常的线程,可以根据打断状态来标记是否退出线程
    • 线程同步
      • wait():Object 的成员方法,每个对象都有;wait(long)和 wait() 可以被 notify 唤醒,wait() 如果不唤醒就一直等下去
        • wait 方法的调用必须先获取 wait 对象的锁,而 sleep 则无此限制
        • wait 方法执行后会释放对象锁,允许其它线程获得该对象锁(我放弃cpu,但你们还可以用)
      • sleep() :Thread 类的静态方法,用于让当前线程暂停执行一段时间(以毫秒为单位),等待指定时间后重新进入就绪状态,不会停止线程。
        • sleep() 会使当前线程进入阻塞状态,让出 CPU 资源给其他线程使用;如果在 synchronized 代码块中执行,并不会释放对象锁(我放弃 cpu,你们也用不了)
        • 到时间后该线程会重新变为可运行状态,进入就绪队列中等待 CPU 调度器重新分配 CPU 时间片来执行(“就绪队列”是指操作系统内核层面的调度队列,和非公平锁中的等待队列的概念没有太大关系。。)
      • yield() :Thread 类的静态方法,用于让当前线程让出 CPU 时间片,使得其他具有相同优先级的线程有机会执行。不会停止线程,而是让线程重新进入就绪状态,等待调度器重新分配 CPU 时间片。
      • join():设置当前线程,等待另一线程结束后运行
      • notify() :是用于线程间通信的方法,它用于唤醒一个正在等待对象监视器的线程。不会停止线程,而是唤醒wait()线程,使其继续执行。
  • Java内存模型? Java3y https://www.bilibili.com/read/cv24200309/?spm_id_from=333.999.0.0&jump_opus=1

  • 如何理解线程不安全?
    如果多个线程对同一个共享数据进行访问而不采取同步操作的话,那么操作的结果是不一致的。

  • 线程不安全的本质什么?

    • 一个角度:保障 可见性,原子性和有序性
    • 可见性:一个线程对共享变量的修改,另外一个线程能够立刻看到。Java提供了volatile关键字来保证可见性。
      当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。(另外,通过synchronizedLock也能够保证可见性,能保证同一时刻只有一个线程获取锁然后执行同步代码,并且在释放锁之前会将对变量的修改刷新到主存当中。因此可以保证可见性。)
    • 原子性:即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。
      在Java中,对基本数据类型的变量的读取和赋值操作是原子性操作,如果要实现更大范围操作的原子性,可以通过synchronizedLock来实现。由于synchronized和Lock能够保证任一时刻只有一个线程执行该代码块,那么自然就不存在原子性问题了,从而保证了原子性。
    • 有序性:即程序执行的顺序按照代码的先后顺序执行。
      通过volatile关键字来保证一定的“有序性”。另外可以通过synchronized和Lock来保证有序性,很显然,synchronized和Lock保证每个时刻是有一个线程执行同步代码,相当于是让线程顺序执行同步代码,自然就保证了有序性。当然JMM是通过Happens-Before 规则来保证有序性的。
  • Java有什么锁?

    • 乐观锁
      • 最乐观的估计,不怕别的线程来修改共享变量,就算改了也没关系,我吃亏点再重试呗(自旋)
      • CAS
    • 悲观锁
      • 最悲观的估计,得防着其它线程来修改共享变量,我上了锁你们都别想改,我改完了解开锁,你们才有机会。
      • synchronized,reentrantlock
    • 锁的选型?
      1. synchronized:可以用于实现对象级别的同步。它可以修饰方法或代码块;
        优点:简单易用,适合用于对临界资源进行简单的同步。适用于简单的同步需求,如小规模的并发控制,或者不需要可中断锁、公平锁等高级功能的场景。
        缺点:粒度较粗,性能相对较低。
      2. ReentrantLock:可重入锁,提供更加灵活的锁机制,支持可中断锁、超时锁和公平锁等。手动控制锁的获取和释放。
        优点:灵活性高,支持多种锁特性。适用于复杂的并发控制场景,特别是需要利用条件变量、公平锁、可中断锁等高级功能时。
        缺点:相比 synchronized,使用方式稍复杂。
      3. 对于低竞争、并发性能要求高的场景,可以考虑使用自旋锁或者乐观锁;对于读多写少的场景,可以考虑使用 ReadWriteLock;对于复杂的同步需求,可以考虑使用 ReentrantLock。
  • synchronized(同步锁)

    • Synchronized采用互斥的方式让同一时刻至多只有一个线程能持有【对象锁】,其它线程再想获取这个锁时就会阻塞住;只能实现为非公平锁;而且由于Java中的线程和操作系统原生线程是一一对应的,线程被阻塞或者唤醒时时会从用户态切换到内核态,这种转换非常消耗性能。
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      public class SynchronizedExample {
      public synchronized void syncMethod() {
      // 同步方法体
      }

      public void syncBlock() {
      synchronized (this) {
      // 同步代码块
      }
      }
      }
    • 底层实现:synchronized是java提供的原子性内置锁,会在编译之后在同步的代码块前后加上monitorenter和monitorexit字节码指令,依赖操作系统底层互斥锁实现。他的作用主要就是实现原子性操作和解决共享变量的内存可见性问题。
      • 执行monitorenter指令时会尝试获取对象锁,如果对象没有被锁定或者已经获得了锁,锁的计数器+1。此时其他竞争锁的线程则会进入等待队列中。
      • 执行monitorexit指令时则会把计数器-1,当计数器值为0时,则锁释放,处于等待队列中的线程再继续竞争锁。
    • monitor是jvm级别的对象(C++实现),线程获得锁需要使用对象(锁)关联monitor;其内部有三个属性,分别是owner、entrylist、waitset(事实上有这两个队列)
      • owner是关联的获得锁的线程,并且只能关联一个线程;
      • entrylist关联的是处于阻塞状态的线程,在owner为空后线程竞争锁;
      • waitset关联的是处于Waiting状态的线程
    • Java中的synchronized有偏向锁、轻量级锁、重量级锁三种形式,分别对应了锁只被一个线程持有、不同线程交替持有锁、多线程竞争锁三种情况。
      1. 重量级锁:底层使用的Monitor实现,里面涉及到了用户态和内核态的切换、进程的上下文切换,成本较高,性能比较低。一旦锁发生了竞争,就会升级为重量级锁。
      2. 轻量级锁:线程加锁的时间是错开的(也就是没有竞争),可以使用轻量级锁来优化。轻量级修改了对象头的锁标志,相对重量级锁性能提升很多。每次修改都是CAS操作,保证原子性
      3. 偏向锁:一段很长的时间内都只被一个线程使用锁,可以使用了偏向锁,在第一次获得锁时会有一个CAS操作,之后该线程再获取锁只需要判断mark word中是否是自己的线程id即可,而不是开销相对较大的CAS命令
    • 完整执行流程:
      ??先考虑偏向锁,如果当前线程ID与markword存储的不相等,则CAS尝试更换线程ID,CAS成功就获取得到锁了;CAS失败则升级为轻量级锁,实际上也是通过CAS来抢占锁资源(只不过多了拷贝Mark Word到Lock Record的过程),抢占成功到锁就归属给该线程了,但自旋失败一定次数后(竞争激烈??)升级重量级锁;重量级锁通过monitor对象中的队列存储线程,但线程进入队列前,还是会先尝试获取得到锁,如果能获取不到才进入线程等待队列中
  • 具体来说,Synchronized 锁住的是什么?

    • Synchronized 关键字应用在实例方法上时,它锁住的是调用该方法的对象实例,也就是该实例的对象锁。不同实例对象之间的方法调用不会互斥,只有同一个实例对象的方法调用才会互斥,即同一个对象实例的多个线程之间会竞争对象锁。
    • Synchronized 关键字应用在静态方法上时,它锁住的是该类的 Class 对象,也就是类的静态锁。这意味着不同实例对象之间的静态方法调用也会互斥,同一个类的不同实例对象的静态方法调用也会互斥,因为它们共享同一个类的 Class 对象。
    • Synchronized 关键字应用在代码块中时,它锁住的是指定的对象实例或者类的 Class 对象。例如:
      1
      2
      3
      synchronized (obj) {
      // synchronized code block
      }
      这里的 obj 可以是任意一个对象实例,如果多个线程使用相同的 obj 对象进行同步,那么它们之间就会互斥执行代码块内的代码,因为它们共享同一个对象锁。如果将 synchronized 关键字应用在 static 代码块中,那么它将锁住类的 Class 对象,同样会导致同一个类的不同实例对象之间互斥执行。
  • JMM(Java内存模型)

    • 定义了共享内存中多线程程序读写操作的行为规范,通过这些规则来规范对内存的读写操作从而保证指令的正确性
    • JMM把内存分为两块,一块是私有线程的工作区域(工作内存),一块是所有线程的共享区域(主内存)
    • 线程跟线程之间是相互隔离,线程跟线程交互需要通过主内存(同时分为共享资源,所以没加锁,具体实现为CAS???)
  • CAS(Compare And Swap(比较再交换))

    • 它体现的一种乐观锁的思想在无锁状态下保证线程操作数据的原子性。
    • CAS使用到的地方很多:AQS框架、AtomicXXX类
    • 在操作共享变量的时候使用的自旋锁,效率上更高一些(但是反复自旋会导致效率下降)
    • CAS的底层是调用的???Unsafe类中的方法,都是操作系统提供的,其他语言实现
    • CAS 存在的问题和局限性:
      1. ABA 问题: ABA 问题是 CAS 中常见的问题之一。当一个线程读取数据 A,然后另一个线程将 A 改变为 B,再改变为 A,此时第一个线程使用 CAS 进行比较并交换时,会发现值仍然为 A,认为没有被修改过,导致可能出现意外结果。
        为了解决 ABA 问题,可以使用版本号或者标记来辅助 CAS 操作,确保在比较并交换时,不仅比较值是否相同,还需要比较版本号或者标记是否相同,以此来增强 CAS 的正确性。
      2. 循环时间长开销大: CAS 是一个自旋操作,当竞争激烈或者线程长时间无法获取到锁时,会导致自旋时间长,消耗大量的 CPU 资源,降低系统性能。
      3. 只能保证一个共享变量的原子操作: CAS 只能针对一个共享变量进行原子操作,对于多个共享变量的复合操作,需要额外的手段来保证原子性。
  • volatile

    1. 保证线程间的可见性
      用 volatie 修饰共享变量,能够防止编译器等优化发生,让一个线程对共享变量的修改对另一个线程可见
    2. 禁止进行指令重排序
      指令重排:用 volatile 修饰共享变量会在读、写共享变量时加入不同的屏障阻止其他读写操作越过屏障,从而达到阻止重排序的效果
  • 什么是AQS?

    • 多线程中的队列同步器。一种锁机制,它是做为一个基础框架使用的像ReentrantLock、Semaphore都是基于AQS实现的
    • AQS内部维护了一个先进先出的双向队列,队列中存储的排队的线程
    • 在AQS内部还有一个属性state,这个state就相当于是一个资源,默认是0(无锁状态),如果队列中的有一个线程修改成功了state为1,则当前线程就相等于获取了资源
    • 在对state修改的时候使用的cas操作,保证多个线程修改的情况下原子性
    • AQS可以实现为公平锁和非公平锁。
  • Reentrantlock(可重入锁)

    • ReentrantLock表示支持重新进入的锁,调用lock方法获取了锁之后,(同一个线程)再次调用 lock,是不会再阻塞;
      支持公平锁和非公平锁,在提供的构造器的中无参默认是非公平锁,也可以传参设置为公平锁
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      public class ReentrantLockExample {
      private final ReentrantLock lock = new ReentrantLock(); // 默认非公平锁,true 启用公平锁

      public void lockMethod() {
      lock.lock();
      try {
      // 受保护的代码区
      } finally {
      lock.unlock();
      }
      }
      }
    • 底层实现:ReentrantLock主要利用CAS+AQS队列来实现
      1. 线程来抢锁后使用cas的方式修改state状态,修改状态成功为1,则让exclusiveOwnerThread属性指向当前线程,获取锁成功
      2. 假如修改状态失败,则会进入双向队列中等待,head指向双向队列头部,tail指向双向队列尾部当
      3. exclusiveOwnerThread为null的时候,则会唤醒在双向队列中等待的线程
      4. 公平锁则体现在按照先后顺序获取锁,非公平体现在不在排队的线程也可以抢锁
    • 完整过程:
  • synchronized 和 ReentrantLock 的区别? https://www.bilibili.com/opus/788136901432311825?spm_id_from=333.999.0.0

    1. 实现方式:
      synchronized 是 Java 语言级别的关键字,用于实现线程同步。可以修饰代码块或方法,自动获取和释放锁。
      ReentrantLock 是 Java API 提供的锁实现,需要显式地使用 lock()unlock()获取锁和释放锁。
    2. 锁粒度:
      synchronized 的锁粒度比较粗,它可以对整个方法或代码块进行同步,不能灵活控制锁的粒度。
      ReentrantLock 的锁粒度比较细,可以根据需要在代码中灵活地获取和释放锁,可以实现更细粒度的同步。
    3. 功能层面:
      二者均属于悲观锁、都具备基本的互斥、同步、锁重入功能
      Lock 提供了许多 synchronized 不具备的功能,例如公平锁、可打断、可超时、多条件变量;Lock 有适合不同场景的实现,如 ReentrantLock,ReentrantReadWriteLock(读写锁)
    4. 性能:
      在低竞争、线程数量不多的情况下,synchronized 的性能可能更好,因为它是 JVM 内置的机制,无需额外的资源消耗。
      在高竞争、线程数量较多或者需要更细粒度控制的情况下,ReentrantLock 可以提供更好的灵活性和性能。
  • synchronized 和 violated 的区别?

    • synchronized 是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。
    • volatile 本质是在告诉jvm当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取;
      一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,那么就具备了两层语义:
      1、保证了不同线程对这个变量进行操作时的可见性,即一个线程修改某个变量的新值对其他线程是立即可见的;
      2、禁止进行指令重排序。
    • 1.volatile仅能使用在变量级别;synchronized则可以使用在变量、方法、和类级别的
    • 2.volatile仅能实现变量的修改可见性,并不能保证原子性;synchronized则可以保证变量的修改可见性和原子性
    • 3.volatile不会造成线程的阻塞;synchronized可能会造成线程的阻塞。
    • 4.volatile标记的变量不会被编译器优化;synchronized标记的变量可以被编译器优化
  • 公平锁和非公平锁

    • 公平锁:是指多个线程按照请求锁的顺序来获取锁,遵循先来先得的原则。如果锁已被其他线程占用,新来的线程就会进入等待队列。只有当队列中的前面所有线程都获取到锁并释放后,该线程才能获得锁。这种机制虽然保证了锁分配的绝对公平性,但会导致更大的线程切换开销和降低整体吞吐量,频繁的线程调度会消耗大量的系统资源。
      • 优点:
        1)确保无饥饿发生,每个线程最终都可以获取到锁。
        2)适用于事务处理等对执行顺序敏感的场景。
      • 缺点:
        1)效率较低,因为要保证按照请求顺序获得锁,可能导致线程切换和等待的增加。
        2)可能会造成锁的平均获取时间延长。
    • 非公平锁:是指多个线程获取锁的顺序并不是按照请求锁的顺序,允许”插队”。新请求的锁可能会直接获得锁,即使有其他线程正在等待。这种方式不保证等待的线程能够按照请求顺序获得锁,但是在多数情况下能够减少唤醒和阻塞的次数,从而提高系统的吞吐量。
      • 优点:
        1)效率更高,因为减少了队列的调度,可以更快地获得锁,提高了程序的响应速度和吞吐量。
        2)在非竞争或低竞争的环境下,性能优于公平锁。
      • 缺点:
        1)可能会导致线程饥饿,即某些线程可能会很长时间获取不到锁。
        2)在高度竞争的环境下,线程可能会不断地尝试获取锁,从而增加CPU的负担。
    • 如果让你实现的话,你怎么实现公平和非公平锁?
      • 公平锁:可以把竞争的线程放在一个先进先出的队列上。只要持有锁的线程执行完了,唤醒队列的下一个线程去获取锁就好了
      • 非公平锁:可能会经历以下流程、、
        1. 直接尝试获取锁: 如果锁当前没有被其他线程持有,该线程会直接获取到锁并执行同步代码。
        2. 自旋等待:如果锁已经被其他线程持有,线程可能会进行一定次数的自旋等待(反复尝试获取锁),而不立即进入等待队列,这样可以减少线程切换的开销。
        3. 后端阻塞: 如果自旋等待仍然没有获取到锁,可能会进入后端阻塞状态,此时线程会加入等待队列中,并阻塞等待锁的释放。
    • ReentrantLock是如何实现锁公平和非公平性的?
      通过维护一个线程队列,按照线程的请求顺序来获取锁。在创建 ReentrantLock 对象时传入了 true 参数,则这个锁是公平的;传入了 false 参数是非公平锁(默认)。
    • synchronized可以实现公平锁和非公平锁吗?
      默认非公平锁,无法直接指定为公平锁。在 synchronized 中,线程在尝试获取锁时会直接去竞争锁,而不考虑其他等待线程的顺序。
  • ConcurrentHashMap、、

    1. 底层数据结构
      JDK1.7底层采用分段的数组+链表实现
      JDK1.8 采用的数据结构跟HashMap1.8的结构一样,数组+链表/红黑二叉树
    2. 加锁的方式
      JDK1.7采用Segment分段锁,底层使用的是ReentrantLock
      JDK1.8采用CAS添加新节点,(如果已存在节点)采用synchronized锁定链表或红黑二叉树的首节点,相对Segment分段锁粒度更细,性能更好
  • ThreadLocal的底层原理

    1. Threadlocal是java中所提供的线程本地存储机制,可以利用该机制将【资源对象】数据存在某个线程内部,该线程可以在任意时刻、任意方法中获取领存的数据
    2. Threadlocal底层通过ThreadlocaMap来实现的,每个Thread对象(线程)中都存在ThreadlocalMap,保存多个Threadlocal对象
      a)调用 set 方法,就是以 ThreadLocal 自己作为 key,资源对象作为 value,放入当前线程的 ThreadLocalMap 集合中
      b)调用 get 方法,就是以 ThreadLocal自己作为 key,到当前线程中查找关联的资源值
      c)调用 remove 方法,就是以 ThreadLocal 自己作为 key,移除当前线程关联的资源值
    3. ThreadLocal内存泄漏问题
      ThreadLocalMap 中的 key是弱引用,值为强引用;key会被GC释放内存,关联 value的内存并不会释放。建议主动 remove 释放 key,value
    4. Threadlocal经典的应用场就是连接管理(一个线程持有一个连接,该连接对象可以在不同的方法之进行传递,线程之间不共享同一个连接)
  • 线程池

    • 线程池是一种用于管理和复用线程的机制,它提供了一种执行大量异步任务的方式,并且可以在多个任务之间合理地分配和管理系统资源。
      管理一系列线程的资源池,用到了池化技术,像数据库连接池、HTTP 连接池都是池化技术的体现,主要目的是为了提高资源的利用率。
    • 为什么需要线程池?
      JVM在HotSpot的线程模型下,Java线程会一对一映射为内核线程。这意味着,在Java中每次创建以及回收线程都会去内核创建以及回收,这就有可能导致:创建和销毁线程所花费的时间和资源可能比处理的任务花费的时间和资源要更多。线程池的出现是为了提高线程的复用性以及固定线程的数量!!
    • 使用线程池的好处
      • 降低资源消耗,通过重复利用已创建的线程来降低创建和销毁线程的消耗。
      • 提高响应速度,因为当有任务到达时,线程已经被创建完毕,立刻能执行任务。
      • 提高线程的可管理性,可以批量管理线程,并且可以监控和调优,根据实际情况修改参数能将线程池效率发挥最大化。使用线程池可以让多个不相关的任务同时执行。
    • 线程池两种创建方式
      1. 通过 ThreadPoolExecutor 构造函数。(推荐)
      2. 通过 Executor 框架的工具类 Executors 创建。JDK 1.5 之后引入,相比于直接用 Thread 的 Start 方法更好,方便管理,效率更高。包括线程池管理,还提供线程工厂、任务队列和拒绝策略。
  • ThreadPoolExecutor 类

    • 7?大参数:
      corePoolSize(核心线程数量,即使空闲时也不会回收)、maximumPoolSize(最大线程数量)、keepAliveTime(线程空余时间:若当前运行的线程数大于核心线程数,达到空闲时间后就会对线程进行回收)、workQueue(阻塞(任务)队列)、handler(任务拒绝策略:核心线程满,任务队列满,最大线程数满,再有任务就会执行拒绝策略)
      threadFactory:线程工厂
    • 任务提交的流程:
      1. 首先会判断运行线程数是否小于corePoolSize,如果小于,则直接创建新的线程执行任务
      2. 如果大于corePoolSize,判断workQueue阻塞队列是否已满,如果还没满,则将任务放到阻塞队列中
      3. 如果workQueue阻塞队列已满,则判断当前线程数是否大于maximumPoolSize,如果没有则创建新的线程执行任务(超出核心线程数,达到线程空余时间后回收)
      4. 如果大于maximumPoolSize,则执行任务拒绝策略(具体就是你自己实现的handler)
    • 线程池的拒绝策略默认有以下 4 种:
      1、AbortPolicy(中止策略)功能: 直接抛出拒绝执行的异常,中止策略的意思也就是打断当前执行流程。
      2、CallerRunsPolicy(调用者运行策略)功能:只要线程池没有关闭,就由提交任务的当前线程处理。
      3、DiscardPolicy(丢弃策略)功能:直接静悄悄的丢弃这个任务,不触发任何动作。
      4、DiscardOldestPolicy(弃老策略)功能:如果线程池未关闭,就弹出队列头部的元素,然后尝试执行。
    • 如何指定线程数?
      线程池指定线程数这块,首先要考量自己的业务是什么样的,是cpu密集型的还是io密集型的。
      假设运行应用的机器CPU核心数是N,那cpu密集型的可以先给到N+1,io密集型的可以给到2N去试试;
      上面这个只是一个常见的经验做法,具体究竟开多少线程,需要压测才能比较准确地定下来;线程不是说越大越好,多线程是为了充分利用CPU的资源,线程过多的上下文切换也会带来系统的开销
  • ThreadPoolExecutor 的类型
    FixedThreadPool:该方法返回一个固定线程数量的线程池。
    SingleThreadExecutor: 该方法返回一个只有一个线程的线程池。
    CachedThreadPool: 该方法返回一个可根据实际情况调整线程数量的线程池。
    ScheduledThreadPool:该返回一个用来在给定的延迟后运行任务或者定期执行任务的线程池。

  • Java死锁如何避免?

    • 造成死锁的几个原因:
      1. 一个资源每次只能被一个线程使用
      2. 一个线程在阻塞等待某个资源时,不释放已占有资源
      3. 一个线程已经获得的资源,在未使用完之前,不能被强行剥夺
      4. 若干线程形成头尾相接的循环等待资源关系
    • 这是造成死锁必须要达到的4个条件,如果要避免死锁,只需不满足其中某一个即可,而其中前3个是作为锁要符合的条件,所以要避免死锁就要打破第4个条件,不出现循环等待锁的关系。在开发过程中:
      1. 要注意加锁顺序,保证每个线程按同样的顺序进行加锁,比如我们可以使用Hash值的大小来确定加锁的先后
      2. 尽可能缩减加锁的范围,等到操作共享变量的时候才加锁。
      3. 要注意加锁时限,可以针对所设置一个超时时间
      4. 要注意死铁检查,这是一种预防机制,确保在第一时间发现死锁并进行解决
    • 死锁诊断:
      • 当程序出现了死锁现象,我们可以使用jdk自带的工具:jps和jstack
      • jps:输出JVM中运行的进程状态信息
      • jstack:查看java进程内线程的堆栈信息,查看日志,检查死锁;如果有死锁现象,需要查看具体代码分析后修复
      • 可视化工具jconsole、VisualVM也可以检查死锁问题
  • 多线程和多进程、、

    1. 多线程:是指在同一进程内部同时执行多个线程,共享同一份内存空间,可以访问共享变量,线程之间的通信和同步相对比较容易。
      优点:线程切换开销小,共享内存,数据共享方便,适合在单机上进行并发编程。
      缺点:共享数据容易导致竞态条件和数据安全问题,需要额外的同步机制来保护共享资源,编程和调试复杂度较高。
    2. 多进程:是指在操作系统中同时执行多个独立的进程,每个进程拥有独立的内存空间,数据隔离性较好,需要通过进程间通信来进行数据传递和共享。
      优点:数据隔离性好,一个进程崩溃不会影响其他进程,适合在多机分布式环境下进行并发编程。
      缺点:进程切换开销大,需要额外的通信机制来传递数据,资源消耗较大。
    3. 适用场景:
      多线程适合在单机上进行并发编程,例如在计算密集型任务或者I/O密集型任务中可以利用多线程提高系统的性能和响应速度。
      多进程适合在分布式环境下进行并发编程,例如在分布式系统中可以通过多进程来实现任务的分布式计算和数据处理。

Java IO

  • Java 中 IO 流分为几种?

    • 1、按照流的流向分,可以分为输入流和输出流;
    • 2、按照操作单元划分,可以划分为字节流和字符流;
    • 3、按照流的角色划分为节点流和处理流。
    • Java Io 流共涉及 40 多个类,这些类看上去很杂乱,但实际上很有规则,而且彼此之间存在非常紧密的联系,都是从如下 4 个抽象类基类中派生出来的。
      InputStream / Reader: 所有的输入流的基类,前者是字节输入流,后者是字符输入流。
      OutputStream / Writer: 所有输出流的基类,前者是字节输出流,后者是字符输出流。
    • 字节流与字符流的区别
      以字节为单位输入输出数据,字节流按照8位传输
      以字符为单位输入输出数据,字符流按照16位传输
  • 常用io类有那些
    File
    FileInputSteam,FileOutputStream
    BufferInputStream,BufferedOutputSream
    PrintWrite
    FileReader,FileWriter
    BufferReader,BufferedWriter
    ObjectInputStream,ObjectOutputSream
    在所列的 Java I/O 类中,处理流通常是指对已有的流进行封装,以提供额外的功能或性能改进。以下是处理流的类:

  • 处理流有哪些?

    1. FileInputStream,FileOutputStream:这两个类是字节流,但它们常常与其他处理流一起使用,如 BufferedInputStreamBufferedOutputStream,以提供缓冲功能,提高读写性能。
      1
      2
      FileInputStream fis = new FileInputStream("example.txt");
      BufferedInputStream bis = new BufferedInputStream(fis);
    2. BufferedInputStream,BufferedOutputStream:提供了缓冲区功能,可以减少对底层流的直接读写次数,提高效率。
    3. FileReader,FileWriter:这两个类是字符流,但它们也可以和 BufferedReaderBufferedWriter 一起使用。
      1
      2
      FileReader fr = new FileReader("example.txt");
      BufferedReader br = new BufferedReader(fr);
    4. BufferedReader,BufferedWriter:提供了缓冲区功能,可以减少对底层字符流的直接读写次数,提高效率。
    5. ObjectInputStream,ObjectOutputStream:这两个类用于序列化和反序列化对象。它们可以与其他流一起使用,如 BufferedInputStreamBufferedOutputStream
      1
      2
      FileOutputStream fos = new FileOutputStream("object.dat");
      ObjectOutputStream oos = new ObjectOutputStream(fos);
      处理流通常用于提供一些额外的功能,如缓冲、对象序列化等。其他类(File、PrintWriter)不是处理流,而是用于文件或文本输出的类。
  • 操作系统的IO

    • 我们要将内存中的数据写入到磁盘的话,主体可能是一个应用程序,比如一个Java进程(假设网络传来二进制流,一个Java进程可以把它写入到磁盘)。
      应用程序是跑在用户空间的,它不存在实质的IO过程,真正的IO是在操作系统执行的。即应用程序的IO操作分为两种动作:IO调用和IO执行。IO调用是由进程(应用程序的运行态)发起,而IO执行是操作系统内核的工作。此时所说的IO是应用程序对操作系统IO功能的一次触发,即IO调用。
    • 操作系统负责计算机的资源管理和进程的调度。我们电脑上跑着的应用程序,其实是需要经过操作系统,才能做一些特殊操作,如磁盘文件读写、内存的读写等等。因为这些都是比较危险的操作,不可以由应用程序乱来,只能交给底层操作系统来。也就是说,你的应用程序要把数据写入磁盘,只能通过调用操作系统开放出来的API来操作。
    • 一个完整的IO过程包括以下几个步骤:
      应用程序进程向操作系统发起I0调用请求
      操作系统准备数据,把I0外部设备的数据,加载到内核缓冲区
      操作系统拷贝数据,即将内核缓冲区的数据,拷贝到用户进程缓冲区
    • 阻塞分类:
      同步阻塞(blocking-I0)简称BIO
      同步非阻塞(non-blocking-10)简称NIO
      异步非阻塞(asynchronous-non-blocking-10)简称AI0
  • 五大阻塞模型: 阻塞 IO 模型、非阻塞 IO 模型、多路复用 IO 模型、信号驱动 IO 模型、异步 IO 模型

    • 阻塞 IO 模型(如socket、Java BIO)
      最传统的一种 IO 模型,即在读写数据过程中会发生阻塞现象。当用户线程发出 IO 请求之后,内核会去查看数据是否就绪,如果没有就绪就会等待数据就绪,而用户线程就会处于阻塞状态,用户线程交出 CPU。当数据就绪之后,内核会将数据拷贝到用户线程,并返回结果给用户线程,用户线程才解除 block 状态。
      典型的阻塞 IO 模型的例子为: data = socket.read(); 如果数据没有就绪,就会一直阻塞在 read 方法
    • 非阻塞 IO 模型
      • 当用户线程发起一个 read 操作后,并不需要等待,而是马上就得到了一个结果。 如果结果是一个 error 时,它就知道数据还没有准备好,于是它可以再次发送 read 操作。一旦内核中的数据准备好了,并且又再次收到了用户线程的请求,那么它马上就将数据拷贝到了用户线程,然后返回。
        所以事实上,在非阻塞 IO 模型中,用户线程需要不断地询问内核数据是否就绪,也就说非阻塞 IO不会交出 CPU,而会一直占用 CPU。一个非常严重的问题, 在 while 循环中需要不断地去询问内核数据是否就绪,这样会导致 CPU 占用率非常高,因此一般情况下很少使用 while 循环这种方式来读取数据。
      • 典型的非阻塞 IO 模型一般如下:
        1
        2
        3
        4
        5
        6
        while(true){
        data = socket.read();
        if(data!= error){
        //处理数据break;
        }
        }
    • 多路复用 I/O 模型
      • 多路复用 IO 模型是目前使用得比较多的模型。 Java NIO 实际上就是多路复用 IO。
      • IO复用模型核心思路:系统给我们提供一类函数(如我们耳濡目染的select、poll、epoll函数),它们可以同时监控多个 fd(文件标识符)的操作,任何一个返回内核数据就绪,应用进程再发起 recvfrom 系统调用。
      • 在多路复用 IO模型中,会有一个线程不断去轮询多个 socket 的状态,只有当 socket 真正有读写事件时,才真正调用实际的 IO 读写操作。因为在多路复用 IO 模型中,只需要使用一个线程就可以管理多个socket,系统不需要建立新的进程或者线程,也不必维护这些线程和进程,并且只有在真正有socket 读写事件进行时,才会使用IO 资源,所以它大大减少了资源占用。
        IO 为何比非阻塞 IO 模型的效率高?是因为在非阻塞 IO 中,不断地询问 socket 状态时通过用户线程去进行的,而在多路复用IO 中,轮询每个 socket 状态是内核在进行的,这个效率要比用户线程要高的多。
      • 不过要注意的是,多路复用 IO 模型是通过轮询的方式来检测是否有事件到达,并且对到达的事件逐一进行响应。因此对于多路复用 IO 模型, 一旦事件响应体很大,那么就会导致后续的事件迟迟得不到处理,并且会影响新的事件轮询。
        1
        2
        3
        4
        5
        fds = wait_files({fd1, fd2, .. fdn}, ..);  // 内核轮询多个文件标识符,读写多个文件
        for (fd : fds) {
        read(fd, buf);
        do_something(buf);
        }
    • 信号驱动 IO 模型
      当用户线程发起一个 IO 请求操作,会给对应的 socket 注册一个信号函数,然后用户线程会继续执行,当内核数据就绪时会发送一个信号给用户线程,用户线程接收到信号之后,便在信号函数中调用 IO 读写操作来进行实际的 IO 请求操作。
    • 异步 IO 模型
      异步 IO 模型才是最理想的 IO 模型,在异步 IO 模型中,当用户线程发起 read 操作之后,立刻就可以开始去做其它的事。而另一方面,从内核的角度,当它受到一个 asynchronous read 之后,它会立刻返回,说明 read 请求已经成功发起了,因此不会对用户线程产生任何block。然后,内核会等待数据准备完成,然后将数据拷贝到用户线程,当这一切都完成之后,内核会给用户线程发送一个信号,告诉它read 操作完成了。也就说用户线程完全不需要实际的整个 IO 操作是如何进行的, 只需要先发起一个请求,当接收内核返回的成功信号时表示 IO 操作已经完成,可以直接去使用数据了。
      也就说在异步 IO 模型中, IO 操作的两个阶段都不会阻塞用户线程,这两个阶段都是由内核自动完成,然后发送一个信号告知用户线程操作已完成。用户线程中不需要再次调用 IO 函数进行具体的读写。这点是和信号驱动模型有所不同的,在信号驱动模型中,当用户线程接收到信号表示数据已经就绪,然后需要用户线程调用 IO 函数进行实际的读写操作;而在异步 IO 模型中,收到信号表示 IO 操作已经完成,不需要再在用户线程中调用 IO 函数进行实际的读写操作。 注意,异步 IO 是需要操作系统的底层支持,在 Java 7 中,提供了 Asynchronous IO。
  • I/O复用模型的select、poll、epoll函数

    1. select:Unix/Linux 系统中最早的 I/O 复用函数之一,它通过一个文件描述符集合来监视多个文件描述符的状态变化。
      • 使用 fd_set 结构来表示文件描述符集合,通过 FD_ZERO、FD_SET、FD_CLR 等宏来对文件描述符集合进行操作。
      • select 函数会阻塞,直到集合中任意一个文件描述符准备就绪或超时。轮询、、遍历文件描述符
      • 缺点是支持的文件描述符数量有限(一般为 1024),且效率较低,因为每次调用都需要将整个文件描述符集合从用户空间拷贝到内核空间。
    2. poll:对 select 函数的改进,同样是通过一个文件描述符集合来监视多个文件描述符的状态变化。
      • 使用 struct pollfd 结构来表示文件描述符集合,通过 poll 函数进行监视。
      • poll 函数会阻塞,直到集合中任意一个文件描述符准备就绪或超时。
      • 缺点是同样存在文件描述符数量有限的问题,且效率相对于 select 提升不大。
    3. epoll:Linux 特有的 I/O 复用函数,是对 select 和 poll 的改进,效率更高。
      • 使用 epoll_create 创建一个 epoll 实例,并使用 epoll_ctl 添加、修改、删除要监听的文件描述符。
      • epoll_wait 函数用于等待文件描述符集合中任意一个文件描述符准备就绪或超时。事件驱动、、监听事件回调机制:某个fd就绪后,内核回调激活
      • epoll 支持水平触发和边缘触发两种模式,边缘触发模式只在状态发生变化时通知程序。
      • epoll 的优势在于它可以监视大量的文件描述符(最大数量受系统限制),并且不需要每次调用都将文件描述符集合拷贝到内核空间,因此效率更高。
  • Java 的 NIO?

    • https://www.bilibili.com/read/cv22750549/?spm_id_from=333.999.0.0&jump_opus=1
      Java NIO 是JDK 1.4 开始有的,其目的是为了提高速度。传统IO是一次一个字节地处理数据,NIO是以块(缓冲区)的形式处理数据,所以NIO的效率要比IO高很多。最主要的是,NIO可以实现非阻塞,而传统IO只能是阻塞的。IO的实际场景是文件IO和网络IO,NIO在网络IO场景下提升就尤其明显了。
      在Java API中提供了两套NIO,一套是针对标准输入输出NIO,另一套就是网络编程NIO。
    • ???NIO 的缓冲区
      Java IO 面向流意味着每次从流中读一个或多个字节,直至读取所有字节,它们没有被缓存在任何地方。此外,它不能前后移动流中的数据。如果需要前后移动从流中读取的数据, 需要先将它缓存到一个缓冲区。 NIO 的缓冲导向方法不同。数据读取到一个它稍后处理的缓冲区,需要时可在缓冲区中前后移动。这就增加了处理过程中的灵活性。但是,还需要检查是否该缓冲区中包含所有您需要处理的数据。而且,需确保当更多的数据读入缓冲区时,不要覆盖缓冲区里尚未处理的数据。
    • ???NIO 的非阻塞
      IO 的各种流是阻塞的。这意味着,当一个线程调用 read() 或 write()时,该线程被阻塞,直到有一些数据被读取,或数据完全写入。该线程在此期间不能再干任何事情了。 NIO 的非阻塞模式,使一个线程从某通道发送请求读取数据,但是它仅能得到目前可用的数据,如果目前没有数据可用时,就什么都不会获取。而不是保持线程阻塞,所以直至数据变的可以读取之前,该线程可以继续做其他的事情。 非阻塞写也是如此。一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情。 线程通常将非阻塞 IO 的空闲时间用于在其它通道上执行 IO 操作,所以一个单独的线程现在可以管理多个输入和输出通道(channel)。
  • Channel
    首先说一下 Channel,国内大多翻译成“通道”。 Channel 和 IO 中的 Stream(流)是差不多一个等级的。
    只不过 Stream 是单向的,譬如:InputStream, OutputStream, 而 Channel 是双向的,既可以用来进行读操作,又可以用来进行写操作。
    NIO 中的 Channel 的主要实现有:1、FileChannel 2、DatagramChannel 3、SocketChannel 4、ServerSocketChannel,分别可以对应文件 IO、 UDP 和 TCP(Server 和 Client)。

  • Buffer
    Buffer,缓冲区,实际上是一个容器,是一个连续数组。 Channel 提供从文件、网络读取数据的渠道,但是读取或写入的数据都必须经由 Buffer。
    下图描述了从一个客户端向服务端发送数据,然后服务端接收数据的过程。客户端发送数据时,必须先将数据存入 Buffer 中,然后将Buffer 中的内容写入通道。服务端这边接收数据必须通过 Channel 将数据读入到 Buffer 中,然后再从 Buffer 中取出数据来处理。在 NIO 中, Buffer 是一个顶层父类,它是一个抽象类,常用的 Buffer 的子类有:ByteBuffer、 IntBuffer、 CharBuffer、 LongBuffer、DoubleBuffer、 FloatBuffer、ShortBuffer

  • ???Selector
    Selector 类是 NIO 的核心类, Selector 能够检测多个注册的通道上是否有事件发生,如果有事件发生,便获取事件然后针对每个事件进行相应的响应处理。这样一来,只是用一个单线程就可以管理多个通道,也就是管理多个连接。这样使得只有在连接真正有读写事件发生时,才会调用函数来进行读写,就大大地减少了系统开销,并且不必为每个连接都创建一个线程,不用去维护多个线程,并且避免了多线程之间的上下文切换导致的开销。

  • ???介绍一下自己对 Netty 的认识。
    第一:Netty 是一个 基于 NIO 模型的高性能网络通信框架,其实可以认为它是对 NIO 网络模型的封装,提供了简单易用的 API,我们可以利用这些封装好的API 快速开发自己的网络程序。
    第二:Netty 在 NIO 的基础上做了很多优化,比如零拷贝机制、高性能无锁队列、内存池等,因此性能会比 NIO 更高。
    第三:Netty 可以支持多种通信协议,如 Http、WebSocket 等,并且针对数据通信的拆包黏包问题,Netty 内置了拆包策略。

  • 如何读取一个不知道类型的文件A数据到文件B中?
    指定输入文件 input.txt 和输出文件 output.txt 的路径,然后使用 BufferedReader 和 BufferedWriter 来读写文件。
    在 while 循环中,使用 readLine() 逐行读取文件 A 中的数据,然后将每一行数据写入到文件 B 中。通过异常处理来捕获可能出现的 IO 异常。
    以上是针对文本文件的读取和写入,如果要处理的是二进制文件或者特定格式的文件,需要使用对应的输入输出流和处理方式。例如,可以使用 FileInputStream 和 FileOutputStream 来处理二进制文件,使用第三方库如 Apache POI 来处理 Excel 文件等。

  • QPS、TPS、RT以及吞吐量

    1. QPS : 每秒查询数,表示系统在一秒内处理的查询或请求的数量。对于数据库、Web服务器等服务,QPS表示每秒处理的查询请求次数。
    2. TPS: 每秒事务数,表示系统在一秒内处理的事务的数量。在分布式系统和事务性系统中,TPS是一个更为常见的指标,用于表示系统的事务处理能力。
    3. KBS: 衡量系统每秒传输的数据量,通常用于评估网络或存储系统的性能。表示每秒传输的数据量为千字节。
    4. RT : 响应时间,表示系统处理一个请求所花费的时间。通常以毫秒(ms)为单位。较低的响应时间通常表示系统性能较好,对用户体验更为友好。
    5. 吞吐量: 表示在单位时间内处理的数据量或请求总数。在性能测试中,吞吐量是一个综合指标,它同时考虑了系统的处理速度和响应时间。通常以每秒处理的请求数、事务数、或数据量为单位。

大数据问题

  • 大数据问题1:如何读取写入100万条数据?
    数据保存在Excel的20个sheet页中,即每一个sheet中有5万条数据。实现在3秒内导入100万条数据需要高效地处理数据导入和并发操作.
    通过线程池创建了20个线程,每个线程负责导入一个 sheet 中的数据;其中每个sheet又创建5个线程,分别处理1万条数据,将这1万条数据批处理地插入到数据库中。
    https://www.bilibili.com/list/watchlater?oid=1001335801&bvid=BV1gx4y1k7ks&spm_id_from=333.788.top_right_bar_window_view_later.content.click

  • 大数据问题2:如何从十万条数据中挑选最符合要求的十条?
    假设数据是包含对象的列表,我们可以使用 Java 8 的流操作和排序来解决这个问题。

    1
    2
    3
    4
    5
    6
    7
    public static List<DataObject> filterData(List<DataObject> dataList, int count) {
    return dataList.stream()
    .filter(/* Your filtering condition */) // 根据需要添加自定义的条件
    .sorted(Comparator.comparing(/* Your sorting criteria */))
    .limit(count) // 排序和限制用于选择最符合条件的数据
    .collect(Collectors.toList());
    }
  • 批处理,多线程处理,流处理。

    1. 批处理(Batch Processing):
      • 批处理是一种将一组任务按顺序批量处理的方式,通常是将多个作业或任务一次性提交给计算机系统执行,而不需要用户干预。
      • 批处理通常是指在一个独立的进程中依次执行一系列命令或操作,可以是顺序执行、并行执行、循环执行等,但并不一定是多线程处理。每个命令或操作都会等待上一个任务执行完成后才会执行,直到所有任务完成。
      • 在计算机领域中,批处理可以用于自动化执行一系列重复性的任务,比如批量处理文件、数据批量导入导出、批量备份等。
    2. 多线程处理(Multithreading):
      • 多线程处理是指在一个程序中同时执行多个线程,每个线程可以独立执行不同的任务,共享相同的内存空间。
      • 多线程处理可以提高程序的并发性和效率,充分利用计算机的多核处理器和资源,可以同时处理多个任务,提高程序的响应速度和并发能力。
      • 多线程处理通常用于需要同时执行多个任务或需要实现并发性的应用程序,比如网络服务器、图形界面应用程序、多媒体处理等。
    3. 流处理(Stream Processing):
      • 流处理是一种连续不断地处理数据流的方式,通常是从输入端读取数据,经过一系列的处理操作,然后输出到输出端,而不需要存储整个数据集。
      • 流处理可以实现实时处理和即时响应,能够处理大规模的数据流,适用于实时计算、实时分析、实时监控等场景。
      • 流处理通常用于处理实时数据流,比如实时日志处理、实时事件处理、实时数据分析等。常见的流处理框架包括Apache Kafka、Apache Flink等。
  • 如何判断40亿个数中某个数不存在?(布隆过滤器)

    • 布隆过滤器是一种数据结构,可以用于快速判断一个元素是否可能存在于一个集合中。它的基本原理是使用多个哈希函数将元素映射到一个位数组中,当检查一个元素是否存在时,只需检查对应位数组上的位是否都为1。具体步骤如下:
      1. 初始化一个位数组,长度要足够大,可以容纳 40 亿个数的哈希结果。
      2. 使用多个哈希函数对需要判断的数进行哈希,得到多个哈希结果。
      3. 将这些哈希结果对应的位数组位置设为1。
      4. 当需要判断某个数是否存在时,使用同样的多个哈希函数对该数进行哈希,并检查对应的位数组位置是否都为1。
      5. 如果都为1,则说明该数可能存在于集合中;如果有任何一位为0,则说明该数一定不存在于集合中。
        需要注意的是,布隆过滤器存在一定的误判率,即有可能判断某个数不存在于集合中,但实际上它确实存在。因此,在使用布隆过滤器时,需要权衡误判率和内存占用等因素,选择合适的参数配置。
  • 如何在1万个数里面找到最大的100个?(堆排序)

    • 维护一个大小为100的小顶堆(堆顶元素最小)。
    • 遍历1万个数,对于每个数,如果大于堆顶元素,则将堆顶元素替换为当前数,并进行堆调整。
    • 遍历结束后,堆中的100个元素就是最大的100个数。
  • 足球场上5万名观众对他们的年龄排序,怎么最快?(快速排序算法)

    • 选择一个基准元素,将小于基准的元素放在基准的左边,大于基准的元素放在基准的右边。
    • 递归地对基准左右两边的子数组进行排序,直到数组有序。
    • 另外,如果需要最快的排序算法,可以考虑使用基数排序或计数排序。这两种排序算法在特定条件下可以达到线性时间复杂度,但需要满足一定的条件(例如数据范围不大且为整数)。
  • 有一批用户(百万数量级)的杂乱行为数据(千万数量级)每一条都要重新洗数,且洗数过程中同一用户的前后行为有时序性依赖关系(前一个行为的洗数结果是下一个行为进行洗数的必要条件),如果是你会怎么设计洗数流程?要求正确、高效、抗风险、、

    1. 分布式任务调度: 使用分布式任务调度系统(如Kafka、Spring Cloud Data Flow等)来调度洗数任务。这样可以方便地将任务分发到多个节点上并行执行,提高整体处理效率。
    2. 数据分片处理: 将用户的杂乱行为数据进行分片,每个分片包含一部分用户的数据。然后将每个分片分配给不同的洗数任务处理。这样可以减小单个任务的压力,提高并行处理效率。
    3. 洗数任务设计: 每个洗数任务负责处理一个用户的行为数据,并保证前后行为的时序性依赖关系。可以采用状态机等方法来实现对时序性依赖关系的处理。任务之间可以通过消息队列等方式进行通信,保证数据的正确性和一致性。
    4. 结果合并: 每个洗数任务处理完数据后,将结果写入到目标存储(如数据库、HDFS等)。另外可以设计一个结果合并任务,负责将所有洗数任务的结果合并,形成最终的洗数结果。
    5. 监控和异常处理: 设计监控系统,实时监控洗数任务的运行情况和数据处理进度,及时发现和处理异常。对于处理失败的数据,可采用重试、补偿等策略保证数据的完整性和准确性。
    6. 性能优化: 针对百万数量级的用户和千万数量级的行为数据,需要进行性能优化,包括优化数据读取、处理和写入的性能,以及任务调度和并行处理的性能等方面。
    7. 容灾和备份: 设计容灾和备份方案,确保系统在遇到故障或者灾难时能够快速恢复。可以采用数据备份、主备切换、数据同步等方式来保证系统的高可用性和可靠性。

Spring

  • Spring (Spring Framework) 是什么?
    Spring 是个java企业级应用的开源开发框架,旨在降低应用程序开发的复杂度。它是轻量级、松散耦合的。它具有分层体系结构,允许用户选择组件(选择需要的模块:Core、AOP、WEB、、)。它可以集成其他框架,如 Structs、Hibernate、EJB 等,所以又称为框架的框架。
    框架—–》容器——》生态、、

  • Spring的可扩展性指?
    框架本身的设计和架构能够方便地接受和整合新的功能、模块或者扩展点,同时允许开发者通过自定义实现来扩展或修改框架的行为。这一特性使得Spring框架在不断演进的技术环境中能够灵活地适应新的需求和变化。具体而言,Spring框架的可扩展性主要表现在以下几个方面:
    1、模块化设计: Spring框架采用模块化的设计,将不同的功能划分为独立的模块。每个模块都有清晰的职责和接口,使得开发者可以根据需要选择性地集成所需的功能,而不必引入整个框架。这样的设计使得Spring更加轻量、可定制。
    2、扩展点和接口: Spring框架提供了许多扩展点和接口,允许开发者通过自定义实现来增加新的功能或者修改现有功能。例如,BeanPostProcessor接口可以用于在Bean实例化和初始化的过程中对Bean进行定制化操作。
    3、第三方整合: Spring框架支持与许多其他框架和技术的集成,如Hibernate、MyBatis、Quartz等。这种集成性使得开发者可以充分利用其他优秀框架的功能,同时也能够方便地替换或升级这些框架。
    4、自定义注解和注解驱动: Spring支持自定义注解,可以通过注解来声明配置信息、定义切面等。注解驱动的开发方式使得开发者能够以更简洁的方式实现特定的功能,同时也为自定义扩展提供了便利。
    5、事件驱动机制: Spring框架引入了事件驱动的机制,通过Application Event和ApplicationListener接口,开发者可以实现自定义事件和监听器,从而实现在框架中添加新的业务逻辑或处理逻辑。

  • 你们项目中为什么使用Spring框架?Spring框架的好处,特点。
    轻量: Spring 是轻量的,基本的版本大约2MB。
    控制反转: Spring通过控制反转实现了松散稠合,对象们给出它们的依赖,而不是创建或查找依赖的对象们。
    面向切面的编程(AOP): Spring支持面向切面的编程,并且把应用业务逻辑和系统服务分开。
    容器: Spring 包含并管理应用中对象的生命周期和配置。
    MVC框架: Spring的WEB框架是个精心设计的框架,是Web框架的一个很好的替代品。
    事务管理: Spring 提供一个持续的事务管理接口,可以扩展到上至本地事务下至全局事务(JTA)。(编程式事务难)
    异常处理: Spring 提供方便的API把具体技术相关的异常(比如由JDBC,Hibernate orJDO抛出的) 转化为致的unchecked 异常。

  • 使用 Spring 有哪些方式?

    1. 作为一个成熟的 Spring Web 应用程序:Spring 提供了 Spring MVC 框架,用于构建 Web 层,同时支持通过 Spring Boot 等工具轻松配置和启动整个应用。这种方式下,Spring 负责管理应用程序的各个层次,包括表现层、业务逻辑层和数据访问层。
    2. 作为第三方 Web 框架,使用 Spring Frameworks 中间层:可以将 Spring 作为第三方框架集成到现有的 Web 应用程序中,而不必完全采用 Spring Web 应用程序的方式。在这种情况下,可能只使用 Spring 的某些模块,例如 Spring IOC 容器、AOP、事务管理等,以提升现有 Web 应用程序的功能和效率。
    3. 用于远程使用:这可能指的是将 Spring 作为远程服务的一部分,例如通过 Spring 的远程调用支持(如 Spring Remoting 或 Spring Cloud)在分布式系统中进行远程通信。在这种场景下,Spring 的特性可以用于实现远程服务的注册、调用和管理。
    4. 作为企业级 Java Bean,它可以包装现有的 POJO(Plain Old Java Objects):通过 Spring 的依赖注入和其他特性,可以更方便地管理和组装这些对象,使其更易于测试、扩展和维护。这种方式下,Spring 赋予了这些普通对象更多的企业级特性。
  • Spring 配置文件?(”Spring bean配置文件”是其中的一个子集)

    • 通常,当说 “Spring 配置文件” 时,是指包含了整个 Spring 应用程序配置信息的 XML 文件。Spring 配置文件通常是一个 XML 文件,用于配置 Spring 容器的行为,包括定义和配置 Spring 中的各种组件(Bean)。这个文件包含了 Spring Bean 的定义、依赖关系、AOP 配置、事务管理等信息。
    • 在 Spring 的 XML 配置文件中,可以使用 <bean> 元素来定义和配置 Spring Bean 类信息,这些类可以是普通的 Java 类、业务逻辑类、数据访问类等。每个 <bean> 元素包含了对应类的一些信息,例如类名、ID、作用域、属性值等。这样,Spring 容器在启动时会根据配置文件中的信息来实例化和管理这些类,使得它们成为 Spring 管理的 Bean。
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      <!-- Spring 配置文件 applicationContext.xml -->
      <beans xmlns="http://www.springframework.org/schema/beans"
      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      xsi:schemaLocation="http://www.springframework.org/schema/beans
      http://www.springframework.org/schema/beans/spring-beans.xsd">
      <!-- 定义一个名为 "myBean" 的 Bean,它是 com.example.MyBean 类的实例 -->
      <bean id="myBean" class="com.example.MyBean">
      <!-- 设置属性值 -->
      <property name="propertyName" value="propertyValue"/>
      </bean>
      <!-- 其他 Bean 的定义 -->
      </beans>
  • Spring 程序启动步骤。

    • 运行一个 Spring 程序通常需要进行以下几步配置:
      1. 添加 Spring 相关的依赖:在项目的构建工具(如 Maven、Gradle)中,添加 Spring 相关的依赖,包括核心容器、AOP 模块、数据访问模块、Web 模块等,具体依赖根据项目需求而定。
      2. 创建 Spring 配置文件:创建一个 XML 文件用于配置 Spring 容器。这个配置文件通常包含了应用程序的配置信息,包括定义和配置 Spring Bean,设置数据库连接,配置事务管理等。
        1
        2
        3
        4
        5
        <!-- 示例:Spring 配置文件 applicationContext.xml -->
        <beans xmlns="" >
        <!-- 配置 Spring Bean -->
        <bean id="myBean" class="com.example.MyBean"> </bean>
        </beans>
      3. 初始化 Spring 容器:在应用程序的启动阶段,通过加载 Spring 配置文件,初始化 Spring 容器。可以使用 ClassPathXmlApplicationContextFileSystemXmlApplicationContext 等容器实现类。
        1
        2
        // 示例:初始化 Spring 容器
        ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
      4. 获取 Spring Bean:通过 Spring 容器获取已配置的 Bean。通过容器的 getBean() 方法获取 Bean 的实例。
        1
        2
        // 示例:获取 Spring Bean
        MyBean myBean = (MyBean) context.getBean("myBean");
      5. ???运行应用程序:也可以通过注解、AOP 等方式利用 Spring 提供的功能。
        1
        2
        // 示例:运行应用程序
        myBean.doSomething();
    • 使用 Spring Boot 启动类来运行程序的一般步骤:(对Spring的配置进行了简化,,
      1. 添加 Spring Boot 依赖:在项目的构建工具中(如 Maven、Gradle),添加 Spring Boot 相关的依赖,包括 spring-boot-starter 或其他特定模块,根据项目需求选择。
      2. 创建 Spring Boot 启动类:创建一个 Java 类,通常命名为 Application 或其他类似的名字,作为 Spring Boot 应用程序的启动类。@SpringBootApplication 是一个组合注解,包含了 @SpringBootConfiguration@EnableAutoConfiguration@ComponentScan。这个注解表明这是一个 Spring Boot 应用程序的入口类。
        1
        2
        3
        4
        5
        6
        7
        // 示例:Spring Boot 启动类
        @SpringBootApplication
        public class Application {
        public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
        }
        }
      3. 编写业务逻辑: 在应用程序中编写业务逻辑代码,可以创建其他的 Spring Bean,通过依赖注入的方式使用这些 Bean。
      4. 运行应用程序: 通过运行 Spring Boot 启动类的 main 方法启动应用程序。这个方法会启动 Spring Boot 内嵌的 Tomcat 服务器,并自动扫描和配置 Spring Bean。
        1
        2
        3
        4
        // 示例:运行 Spring Boot 应用程序
        public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
        }
  • “三层架构”:表现层 + 业务层 + 数据访问层

    1. 三层架构(Presentation Layer、Business Logic Layer、Data Access Layer)是一种常见的软件架构模式,用于将一个软件系统划分为三个主要的逻辑层,以提高系统的可维护性、可扩展性和可重用性。Spring 框架提供了广泛的支持,使得开发者能够更轻松地实现和管理三层架构。
    2. 表现层(Presentation Layer):
      定义: 主要负责用户界面和用户交互,通常包括 Web 页面、UI 组件等。
      Spring 支持: Spring 提供了 Spring MVC 模块,用于实现 Web 应用的表现层。Spring MVC 提供了强大的控制器(Controller)机制,支持基于注解的映射、视图解析、数据绑定等功能,使得开发者能够轻松地构建和管理 Web 层。
    3. 业务层(Business Logic Layer):
      定义: 包含了应用程序的业务逻辑,处理用户请求、调用数据访问层进行数据操作,并进行业务规则的处理。
      Spring 支持: Spring 提供了 IoC(Inversion of Control)容器和 AOP(Aspect-Oriented Programming)功能,这两者共同构成了 Spring 的核心。开发者可以使用 Spring IoC 容器来管理业务层的对象,而 Spring AOP 可以用于处理横切关注点,如事务管理、日志记录等。
    4. 数据访问层(Data Access Layer):
      定义: 主要用于与数据存储交互,进行数据库访问、数据持久化等操作。
      Spring 支持: Spring 提供了对数据访问的支持,其中最重要的是 Spring 的 JDBC 模块和 Spring 的 ORM 模块。Spring JDBC 简化了 JDBC 操作,而 Spring ORM 支持集成多种 ORM 框架,如 Hibernate、MyBatis 等,使得数据访问层的开发更加便捷。
  • Service层 为什么用的是接口?为什么不直接使用实现类??

    • Service 层负责处理业务逻辑、调用数据访问层(DAO,Data Access Object)并与控制器层进行交互。使用接口而不是直接使用实现类呢,这主要有以下几个原因:
      1. 解耦和可扩展性:使用接口将 Service 层与其实现类解耦。通过面向接口编程,控制器(或其他类)可以只依赖于接口而不是具体的实现类。这样使得代码更加灵活,能够轻松切换不同的实现类或者模拟测试用的虚拟实现。
      2. 单一职责原则:接口定义了 Service 层的契约和行为,实现类负责具体的逻辑实现。这符合单一职责原则,即一个类应该只负责一项职责。
      3. 测试和模拟:??接口的使用使得单元测试更加容易。在测试时,可以使用模拟实现来替代真正的实现类,从而更好地进行单元测试。通过模拟,可以控制和验证不同的行为,而无需依赖于底层实现细节。
      4. 依赖注入:??Spring 容器能够通过依赖注入将接口的实现类注入到需要的地方,而不需要直接关注具体的实现细节。
    • 切换一个接口下的不同的实现类有什么意义?为什么不直接使用不同的实现类?
      1. 灵活性和可维护性:使用接口定义规范可以提高代码的灵活性。通过面向接口编程,可以将调用方与具体实现类解耦,使得代码更易于维护和修改。如果后续需要替换实现类或引入新的实现,只需要修改实现类的绑定,而不需要修改调用方的代码。
      2. 解耦和依赖注入:接口的使用支持依赖注入,使得系统更易于管理和测试。依赖注入能够减少类之间的耦合度,提高了代码的可测试性,有利于单元测试和模拟测试。
      3. 扩展性和适应性:使用接口和不同的实现类使得系统更具扩展性。根据不同的需求和场景,可以轻松地切换实现类,使得系统更具适应性和灵活性。
      4. 遵循设计原则:使用接口遵循了面向对象编程的设计原则,如开闭原则(对扩展开放,对修改关闭)、单一职责原则等。这种设计模式使得代码更清晰、更易于理解和维护。
  • @Component, @Controller, @Repository,@Service 有何区别?
    @Component :这将 java 类标记为 bean。它是任何 Spring 管理组件的通用构造型。spring 的组件扫描机制现在可以将其拾取并将其拉入应用程序环境中。
    @Controller :这将一个类标记为 Spring Web MVC 控制器。标有它的Bean 会自动导入到 IoC 容器中。
    @Service:此注解是组件注解的特化。不对 @Component 提供任何其他行为。在服务层类中使用@Service 而不是@Component,因为它以更好的方式指定了意图。
    @Repository :这个注解是具有类似用途和功能的 @Component 注解的特化。它为 DAO 提供了额外的好处。它将 DAO 导入 IoC 容器,并使未经检查的异常有资格转换为 Spring DataAccessException。

  • @Autowired 为什么不推荐使用了,而是使用构造函数?

    1. @Autowired 仍然是 Spring 框架中用于进行依赖注入的一种方式,它可以用于字段、构造函数和方法上。然而,一些开发者倾向于使用构造函数注入,而不是字段注入或方法注入,这是因为构造函数注入有一些优势:
    2. 显式性和明确性: 构造函数注入更加显式和明确。通过在构造函数参数上使用 @Autowired 注解,你可以清晰地看到依赖关系,而不用深入查看类的其他部分。
    3. 不可变性:通过构造函数注入,你可以将依赖关系声明为不可变的,即一旦对象被创建,它的依赖关系就不能再被修改。这有助于确保对象在使用过程中保持一致性。
    4. 避免循环依赖问题:使用构造函数注入可以有效地避免循环依赖的问题。如果两个类相互依赖,通过构造函数注入,它们将无法创建循环依赖。
    5. 测试方便:构造函数注入使得在单元测试中更容易进行模拟和注入测试数据。
  • @Autowired 与 @Resource的区别。

    • 相同点:都是做bean的注入时使用,都可以写在字段和setter方法上。两者如果都写在字段上,就不需再写setter。(注:最好是放在setter方法上,因为这样更符合面向对象的思想,通过set、get去操作属性,而不是直接去操作属性。)
      • @Autowired 可以更准确地控制应该在何处以及如何进行自动装配。此注解用于在 setter 方法,构造函数,具有任意名称或多个参数的属性或方法上自动装配bean。默认情况下,它是类型驱动的注入。
      • @Required 应用于 bean 属性 setter 方法。此注解仅指示必须在配置时使用bean 定义中的显式属性值或使用自动装配填充受影响的 bean属性。如果尚未填充受影响的 bean 属性,则容器将抛出 eanInitializationException。
    • 不同点
      1. 源头不同:
        • @Autowired 是 Spring 框架自带的注解,属于 Spring 核心;
        • @Resource 是 Java EE提供的注解,不仅被 Spring 支持,也可以在其他 Java EE 容器中使用。
      2. 注入类型不同:
        • @Autowired: 主要通过 byType 的方式进行注入,即按照属性的数据类型从 Spring 容器中匹配并注入。如果存在多个匹配的 Bean,则可以通过 @Qualifier 进行进一步指定。
          1
          2
          3
          @Autowired
          @Qualifier("nameDao")
          private String nameDao;
        • @Resource: 主要通过 byName 的方式进行注入,即按照属性的名称从 Spring 容器中匹配并注入。也可以通过 name 属性指定要注入的 Bean 名称。作用相当于@Autowired,只不过@Autowired按照byType自动注入。
          1
          2
          3
          4
          @Required
          public void setName(String name){
          this.name=name;
          }
          @Resource装配顺序:
          ①如果同时指定了name和type,则从Spring上下文中找到唯一匹配的bean进行装配,找不到则掀出异常。
          ②如果指定了name,则从上下文中查找名称(id)匹配的bean进行装配,找不到则抛出异常。
          ③如果指定了type,则从上下文中找到类似匹配的唯一bean进行装配,找不到或是找到多个,都会抛出异常。
          ④如果既没有指定name,又没有指定type,则自动按照byName方式进行装配;如果没有匹配,则回退为一个原始类型进行匹配,如果匹配则自动装配。
      3. 可选性:
        • @Autowired:默认必须注入的,如果找不到匹配的 Bean,会抛出异常。但可以通过设置 required = false 来允许 null 值。
        • @Resource: 默认是必须注入的,不支持设置类似 required 的属性。但可以通过 @Resource(lookup = "someName") 指定 lookup 属性,如果找不到匹配的 Bean,则不会抛出异常。

Spring 核心模块

  • Spring主要由以下几个模块组成:

    • Core Container
      • Spring Core: 核心类库,提供IOC服务;
      • Spring Bean:构成用户应用程序主干的对象。Bean 由 Spring IoC 容器管理、实例化、配置、装配和管理。Bean 是基于用户提供给容器的配置元数据创建。
      • Spring Context: 提供框架式的Bean访问方式,以及企业级功能 (JNDI、定时任务等);
    • Spring AOP: AOP服务,面向切面编程;
    • Data Access
      • Spring DAO: 对JDBC的抽象,简化了数据访问异常的处理:
      • Spring ORM: 对现有的ORM框架的支持;
    • Web
      • Spring Web: 提供了基本的面向Web的综合特性,例如多方文件上传
      • Spring MVC:提供面向Web应用的Model-View-Controller实现。
      • Servlet
      • Socket
    • Test:该层为使用 JUnit 和 TestNG 进行测试提供支持。
    • 几个杂项模块: Messaging – 该模块为 STOMP 提供支持。它还支持注解编程模型,该模型用于从 WebSocket 客户端路由和处理 STOMP 消息。Aspects –该模块为与 AspectJ 的集成提供支持。
  • Spring Bean。一个 Spring Bean 定义 包含什么?
    Spring beans 是那些形成 Spring 应用的主干的 java 对象。它们被 Spring IOC容器初始化,装配和管理。这些 beans 通过容器中配置的元数据创建,比如以 XML 文件中 的形式定义。Spring 框架定义的 beans 都是单件beans。在 bean tag 中有个属性”singleton”,如果它被赋为 TRUE,bean 就是单件,否则就是一个prototype bean。默认是 TRUE,所以所有在 Spring 框架中的 beans 缺省都是单件。
    一个 Spring Bean 的定义包含容器必知的所有配置元数据,包括如何创建一个bean,它的生命周期详情及它的依赖。

  • Spring 提供了哪些配置方式?一般是怎么定义Bean的?

    • 基于 xml 配置
      bean 所需的依赖项和服务在 XML 格式的配置文件中指定。这些配置文件通常包含许多 bean 定义和特定于应用程序的配置选项。它们通常以 bean 标签开头。例如:
      1
      2
      3
      <bean id="studentbean" class="org.edureka.firstSpring.StudentBean">
      <property name="name" value="Edureka"></property>
      </bean>
    • 基于注解配置
      通过在相关的类、方法或字段声明上使用注解,将 bean 配置为组件类本身,而不是使用 XML 来描述 bean 装配。
      1
      2
      3
      @Component
      public class MyBean {
      }
      默认情况下,Spring 容器中未打开注解装配。因此,您需要在使用它之前在 Spring 配置文件中启用它。例如:
      1
      2
      3
      4
      <beans>
      <context:annotation-config/>
      <!-- bean definitions go here -->
      </beans>
    • ???基于 Java API 配置
      Spring 的 Java 配置是通过使用 @Bean 和 @Configuration 来实现。
      1、 @Bean 注解扮演与 元素相同的角色。
      2、 @Configuration 类允许通过简单地调用同一个类中的其他 @Bean 方法来定义 bean 间依赖关系。 例如:
      1
      2
      3
      4
      5
      6
      @Configuration public class StudentConfig {
      @Bean
      public StudentBean myStudent() {
      return new StudentBean();
      }
      }
  • 依赖注入的方法有几种?@Autowired是如何工作的?

    1. 构造器注入:将被依赖对象通过构造函数的参数注入给依赖对象,并且在初始化对象的时候注入。
      优点:对象初始化完成后便可获得可使用的对象。
      缺点: 当需要注入的对象很多时,构造器参数列表将会很长,不够灵活。若有多种注入方式,每种方式只需注入指定几个依赖,那么就需要提供多个重载的构造函数,麻烦。
      1
      2
      3
      4
      @Autowired
      public MyClass(MyDependency myDependency) {
      this.myDependency = myDependency;
      }
    2. setter方法注入:IOC Service Provider通过调用成员变量提供的setter函数将被依赖对象注入给依赖类
      优点: 灵活,可以选择性地注入需要的对象。用的最多。
      缺点: 依赖对象初始化完成后,由于尚未注入被依赖对象,因此还不能使用。
      1
      2
      3
      4
      @Autowired
      public void setMyDependency(MyDependency myDependency) {
      this.myDependency = myDependency;
      }
    3. 接口注入:依赖类必须要实现指定的接口,然后实现该接口中的一个函数,该函数就是用于依赖注入。该函数的参数就是要注入的对象。
      优点: 接口注入中,接口的名字、函数的名字都不重要,只要保证函数的参数是要注入的对象类型即可。
      缺点:侵入性太强,不建议使用。PS:什么是侵入?如果类A要使用别人提供的一个功能,若为了使用这功能,需要在自己的类中增加额外的代码,这就是侵入性。
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      public interface MyInterface {
      void injectDependency(MyDependency myDependency);
      }

      public class MyClass implements MyInterface {

      private MyDependency myDependency;

      @Override
      @Autowired
      public void injectDependency(MyDependency myDependency) {
      this.myDependency = myDependency;
      }
      }
    4. 使用建议:用构造器参数实现强制依赖,setter 方法实现可选依赖。
    5. 使用 @Autowired 注解可以对字段进行依赖注入。具体注入方式取决于被注入的字段的类型,可以是构造器注入、setter 方法注入、接口注入等。
  • Spring IOC
    Spring IOC 解决的是对象管理和对象依赖的问题。IOC容器可以理解为一个对象工厂,我们都把该对象交给工厂,工厂管理这些对象的创建以及依赖关系;需要用对象的时候,从工厂里边获取就好了
    • 「控制反转」指:把原有自己掌控的事交给别人去处理。它更多的是一种思想或者可以理解为设计模式。比如:本来由我们自己new出来的对象,现在交由IOC容器,把对象的控制权交给它方
    • 「注入依赖」是「控制反转」的实现方式,对象无需自行创建或者管理它的依赖关系,依赖关系将被「自动注入」到需要它们的对象当中去
    • 用 Spring IOC 有什么好处吗?主要在于「将对象集中统一管理」并且「降低耦合度」
      如果项目里的对象都是就new下就完事了,没有多个实现类,那没事,不用Spring也没啥问题。但 Spring核心不仅仅IOC啊,除了把对象创建出来,还有一整套的Bean生命周期管理。用Spring IOC 可以方便 单元测试、对象创建复杂、对象依赖复杂、单例等等的,什么都可以交给Spring IOC
    • 列举 IoC 的一些好处。
      它将最小化应用程序中的代码量。
      ???它将使您的应用程序易于测试,因为它不需要单元测试用例中的任何单例或 JNDI 查找机制。
      它以最小的影响和最少的侵入机制促进松耦合。
      支持即时的实例化和延迟加载服务。
    • spring 中有多少种 IOC 容器?
      • BeanFactory - BeanFactory 就像一个包含 bean 集合的工厂类。它会在客户端要求时实例化 bean。
      • ApplicationContext - ApplicationContext 接口扩展了 BeanFactory 接口。它在 BeanFactory 基础上提供了一些额外的功能。
    • Spring IoC 的实现机制:IoC 的实现原理就是工厂模式加反射机制
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      interface Fruit {
      public abstract void eat();
      }
      class Apple implements Fruit {
      public void eat(){System.out.println("Apple");}
      }
      class Orange implements Fruit {
      public void eat(){System.out.println("Orange");}
      }
      class Factory {
      public static Fruit getInstance(String ClassName) {
      Fruit f=null;
      try {
      f=(Fruit)Class.forName(ClassName).newInstance();
      } catch (Exception e) {
      e.printStackTrace();
      }
      return f;
      }
      }
      class Client {
      public static void main(String[] a) {
      Fruit f=Factory.getInstance("io.github.dunwu.spring.Apple");
      if(f!=null){f.eat();}
      }
      }
  • Ioc的底层实现
    1、先通过createBeanFactory创建出一个Bean工厂(DefaultListableBeanFactory)
    2、开始循环创建对象,因为容器中的bean默认都是单例的,所以优先通过getBean,doGetBean从容器中查找,找不到的话
    3、通过createBean,doCreateBean方法,以反射的方式创建对象,一般情况下使用的是无参的构造方法(getDeclaredConstructornewinstance)
    4、进行对象的属性填充populateBean
    5、进行其他的初始化操作(initializingBean)

  • ApplicationContext 和 BeanFactory 有什么区别?
    Beanfactony是Spring中非常核心的组件,表示Bean工厂,可以生成Bean,维护Bean,而AppitcationContext继承了Beanfactony,所以ApplicationContext拥有Beanfacton所有的特点,也是一个Bean工厂,但是AppicationContext除开继承了Beanfacton之外,还继承了诸如EnvronmentCapable、Messaaesource、AppicationEventPubishe等接口,从而ApplicationCcontext还有获取系统环境变量、国际化、事件发布等功能,这是BeanFactory所不具备的.
    Application contexts 提供一种方法处理文本消息,一个通常的做法是加载文件资源(比如镜像),它们可以向注册为监听器的 bean 发布事件。另外,在容器或容器内的对象上执行的那些不得不由 bean 工厂以程序化方式处理的操作,可以在Application contexts 中以声明的方式处理。Application contexts 实现了MessageSource 接口,该接口的实现以可插拔的方式提供获取本地化消息的方法。

  • ApplicationContext 通常的实现是什么?

    • FileSystemXmlApplicationContext:此容器从一个 XML 文件中加载 beans 的定义,XML Bean 配置文件的全路径名必须提供给它的构造函数。
    • ClassPathXmlApplicationContext:此容器从一个 XML 文件中加载 beans 的定义,你需要正确设置 classpath 因为这个容器将在 classpath里找 bean 配置。
    • WebXmlApplicationContext:此容器加载一个 XML 文件,此文件定义了一个 WEB 应用的所有 bean。
  • Bean Factory 与 FactoryBean 有什么区别?
    相同点:都是用来创建bean对象的
    不同点:使用BeanFactory创建对象的时候,必须要道循严格的生命周期流程,太复杂了,,如果想要简单的自定义某个对象的创建,同时创建完成的对象想交给spring来管理,那么就需要实现FactroyBean接口了(Bean Factory标准化,FactoryBean个性化)
    issingleton:是否是单例对象、getObjectType:获取返回对象的类型、getobject:自定义创建对象的过程(new,反射,动态代理)

  • Spring容器启动流程是怎样的
    1,在创建Spring容器,也就是启动Spring时:
    2,首先会进行扫描,扫描得到所有的BeanDefinition对象,并存在一个Map中
    3,然后筛选出非懒加弱的单例BeanDefinton进行创建Bean,对于多例Bean不需要在启动过程中去进行创建,对于多例Bean会在每次获取Bean时利用BeanDeinition去建
    4,利用BeanDefinition创建Bean就是Bean的创建生命因期,这期间包括了合并BeanDefintion、推断构造法、实例化、属性填充、初始化前、初始化、初始化后等步骤其中AOP就是发生在初始化后这一步骤中
    5,单例Bean创建完了之后,Spring会发布一个容器启动事件
    6,Spring启动结束
    7,在源码中会更复杂,比如源码中会提供一些模板方法,让子类来实现,比如源码中还涉及到一些BeanfatoyPostProcessor和BeanPostprocessor的注册,Spring的描就是通过BenaFactoryPostProcessor来实现的,依赖注入就是通过BeanPostProcessor来实现的8。在sprina启动过程中还会去处理@Import等注解

  • Spring 支持集中 bean scope(范围)?

    • Spring bean 支持 5 种 scope:
      • Singleton - 每个 Spring IoC 容器仅有一个单实例。
      • Prototype - 每次请求都会产生一个新的实例。
      • Request - 每一次 HTTP 请求都会产生一个新的实例,并且该 bean 仅在当前 HTTP 请求内有效。
      • Session - 每一次 HTTP 请求都会产生一个新的 bean,同时该 bean 仅在当前HTTP session 内有效。
      • Global-session - 类似于标准的 HTTP Session 作用域,不过它仅仅在基于portlet 的 web 应用中才有意义。
        ??? Portlet规范定义了全局 Session 的概念,它被所有构成某个 portlet web 应用的各种不同的 portlet 所共享。在 globalsession 作用域中定义的 bean 被限定于全局 portlet Session的生命周期范围内。如果你在 web 中使用global session 作用域来标识 bean,那么 web 会自动当成 session 类型来使用。
        仅当用户使用支持 Web 的 ApplicationContext 时,最后三个才可用。
  • ??单例Bean和单例模式?什么是 spring 的内部 bean?
    单例模式表示JVM中某个类的对象只会存在唯一一个。而单例Bean并不表示JVM中只能存在唯一的某个类的Bean对象。
    只有将 bean 用作另一个 bean 的属性时,才能将 bean 声明为内部 bean。为了定义 bean,Spring 的基于 XML 的配置元数据在 或 中提供了 元素的使用。内部 bean 总是匿名的,它们总是作为原型。

  • SpringBean生命周期原理。

    1. 实例化Bean:反射的方式创建对象
      对于BeanFactory容器,当客户向容器请求一个尚未初始化的bean时,或初始化bean的时候需要注入另一个尚未初始化的依赖时,容器就会调用createBean进行实例化。
      对于ApplicationContext容器,当容器启动结束后,通过获取BeanDefinition对象中的信息,实例化所有的bean。
    2. 设置对象属性(依赖注入):populateBean(),循环依赖的问题(三级缓存)
      实例化后的对象被封装在BeanWrapper对象中,紧接着,Spring根据BeanDefinition中的信息 以及 通过BeanWrapper提供的设置属性的接口完成依赖注入。
    3. 处理Aware接口:接着,Spring会检测该对象是否实现了xxxAware接口,并将相关的xxxAware实例注入给Bean。invokeAwareMethod(完成BeanName,BeanFactory,BeanClassLoader对象的属性设置)
      • 如果这个Bean已经实现了BeanNameAware接口,会调用它实现的setBeanName(String beanId)方法,此处传递的就是Spring配置文件中Bean的id值;
      • 如果这个Bean已经实现了BeanFactoryAware接口,会调用它实现的setBeanFactory()方法,传递的是Spring工厂自身。
      • 如果这个Bean已经实现了ApplicationContextAware接口,会调用setApplicationContext(ApplicationContext)方法,传入Spring上下文;
    4. BeanPostProcessor:如果想对Bean自定义处理,那么可以让Bean实现了BeanPostProcessor接口,那将会调用postProcessBeforeInitialization(Object obj, String s)方法。
      使用比较多的有(ApplicationContextPostProcessor,设置ApplicationContext,Environment,ResourceLoader,EmbeddValueResolver等对象)
    5. InitializingBean 与 init-method:如果Bean在Spring配置文件中配置了 init-method 属性,则会自动调用其配置的初始化方法。
      invokenitmethod(),判断是否实现了initializingBean接口,如果有,调用afterPropertiesset方法,没有就不调用
    6. 调用BeanPostProcessor的后置处理方法:若存在与 bean 关联的任何 BeanPostProcessors,则将调用 postProcessAfterInitialization()方法。
      spring的aop就是在此处实现的,AbstractAutoProxyCreator。注册Destuction相关的回调接口:钩子函数
    7. 获取到完整的对象,可以通过getBean的方式来进行对象的获取
    8. DisposableBean:当Bean不再需要时,会经过清理阶段,如果Bean实现了DisposableBean这个接口,会调用其实现的destroy()方法;
    9. destroy-method:最后,如果这个Bean的Spring配置中配置了destroy-method属性,会自动调用其配置的销毁方法
  • Spring中Bean是线程安全的吗
    Spring本身并没有针对Bean做线程安全的处理,所以:
    1,如果Bean是无状志的,那么Bean则是线程安全的
    2,如果Bean是有状态的,那么Bean则不是线程安全的
    另外,Bean是不是线程安全,跟Bean的作用域没有关系,Bean的作用域只是表示Bean的生命周期范围,对于任何生命周期的Bean都是一个对象,这个对象是不是线挥安全的还得看这个Bean本身。

  • Spring 是如何解决循环依赖问题的?
    如果两个或多个 Bean 互相之间持有对方的引用就会发生循环依赖。循环的依赖将会导致注入死循环。这是 Spring 发生循环依赖的原因。循环依赖有三种形态:
    https://www.bilibili.com/video/BV1Mc411S7B7?p=12&vd_source=ff210768dfaee27c0d74f9c8c50d7274
    、、

  • 三级缓存的作用是什么?Spring 中哪些情况下,不能解决循环依赖问题?
    p156
    、、

  • 缓存的放置时间、删除时间,,
    https://www.bilibili.com/video/BV1Mc411S7B7?p=14&vd_source=ff210768dfaee27c0d74f9c8c50d7274

  • Spring AOP
    AOP(Aspect-Oriented Programming), 即 面向切面编程, 它与OOP( Object-Oriented Programming, 面向对象编程) 相辅相成, 提供了与OOP 不同的抽象软件结构的视角. 在 OOP 中, 我们以类(class)作为我们的基本单元, 而 AOP 中的基本单元是 Aspect(切面)
    Spring AOP 解决的是「重复性」的非业务代码抽取的问题。所谓「面向切面编程」在我理解下其实就是在方法前后增加非业务代码,将那些与业务无关,却为业务模块所同调用的逻辑或责任(例如事务处理、日志管理、权限控制等)封装起来,便于减少系统的重复代码,降低模块间的耦合度,并有利于未来的可扩展性和可维护性。
    AOP 底层的技术是动态代理,???在Spring内实现依赖的是BeanPostProcessor。如果要代理的对象实现了某个接口,那么SpringAOP就会使用JDK动态代理去创建代理对象;而对于没有实现接口的对象,就无法使用IDK动态代理,转而使用CGlib动态代理生成一个被代理对象的子类来作为代理。

  • AOP 可以用于的一些常见场景:

    1. 权限控制: AOP 可以用于在方法执行前或执行后添加权限控制的逻辑,例如检查用户是否有足够的权限执行某个操作。这样可以避免将权限检查逻辑散布在业务代码的各个地方。
    2. 事务控制: AOP 可以用于管理事务,例如在方法执行前开启事务,在方法执行后根据执行结果决定是提交事务还是回滚事务。这样可以确保事务的一致性和可靠性。
    3. 日志记录: AOP 可以用于添加日志记录逻辑,例如在方法执行前记录方法的输入参数,方法执行后记录返回结果。这样可以方便地进行日志管理和分析。
    4. 性能监控: AOP 可以用于添加性能监控逻辑,例如在方法执行前记录开始时间,在方法执行后记录结束时间,计算方法的执行时间。方便进行性能分析和优化。
    5. 异常处理: AOP 可以用于添加异常处理逻辑,例如捕获方法执行过程中的异常并进行统一处理。这样可以减少业务代码中的异常处理逻辑,提高代码的清晰度。
    6. 缓存管理: AOP 可以用于添加缓存管理逻辑,例如在方法执行前检查是否有缓存命中,如果有直接返回缓存结果,否则执行方法并将结果缓存起来。
  • AspectJ?Spring AOP和AspectJ AOP有什么区别?

    • AOP是一种思想,AspectJ是一种实现。SpringAOP中已经集成了AspectJ,AspectJ应该算得上是Java生态系统中最完整的AOP框架了。
    • SpringAOP是属于运行时增强,而Aspect是编译时增强。 SpringAOP基于代理(Proxying),而AspectJ基于字节码操作(BytecodeManipulation)。
      SpringAOP已经集成了AspectJ,Aspect应该算得上是Java生态系统中最完整的AOP框架了。Aspect相比于SpringAOP功能更加强大,但是SpringAOP相对来说更简单。
      如果我们的切面比较少,那么两者性能差异不大。但是,当切面太多的话,最好选择AspectJ,它比SpringAOP快很多。
  • 什么是 Aspect(切面)?
    Aspect 由 pointcount 和 advice 组成, 它既包含了横切逻辑的定义, 也包括了连接点的定义. Spring AOP 就是负责实施切面的框架, 它将切面所定义的横切逻辑编织到切面所指定的连接点中. 简单地认为, 使用 @Aspect 注解的类就是切面.
    AOP 的工作重心在于如何将增强编织目标对象的连接点上, 这里包含两个工作:
    1、如何通过 pointcut 和 advice 定位到特定的 joinpoint 上
    2、如何在advice 中编写切面代码.

  • 在SpringAOP中,关注点和横切关注的区别是什么?什么是连接点呢?切入点是什么?什么是通知呢,有哪些类型呢?

    • 关注点是应用中一个模块的行为,一个关注点可能会被定义成一个我们想实现的一个功能。
    • 横切关注点是一个关注点,此关注点是整个应用都会使用的功能,并影响整个应用,比如日志,安全和数据传输。
    • 连接点代表一个应用程序的某个位置,在这个位置我们可以插入一个AOP切面,它实际上是个程序执行SpringAOP的位置。
    • 切入点(JoinPoint)是一个或一组连接点,通知将在这些位置执行。可以通过表达式或匹配的方 式指明切入点。
    • 通知(Advice)是个在方法执行前或执行后要做的动作(特定 JoinPoint 处的 Aspect 所采取的动作),实际上是程序执行时要通过SpringAOP框架触发的代码段。Spring切面可以应用五种类型的通知:
      • Before:这类 Advice 在 joinpoint 方法之前执行,并使用@Before 注解标记进行配置。
      • After Returning:这类型 Advice 在连接点方法正常执行后执行,并使用@AfterReturning 注解标记进行配置。
      • After Throwing:这些类型的 Advice 仅在 joinpoint 方法通过抛出异常退出并使用 @AfterThrowing 标记配置时执行。
      • After (finally): 在连接点方法之后执行,无论方法退出是正常还是异常返回,并使用 @After 注解标记进行配置。
      • Around:在连接点之前和之后执行,并使用@Around 注解标记进行配置。
  • AOP 有哪些实现方式?

    • 静态代理:使用 AOP 框架提供的命令进行编译,在编译阶段就可生成 AOP 代理类,因此也称编译时增强;
      • 编译时编织(特殊编译器实现)
      • 类加载时编织(特殊的类加载器实现)。
    • 动态代理:在运行时在内存中“临时”生成 AOP 动态代理类,因此也被称为运行时增强。
      • JDK 动态代理
      • CGLIB
    • Spring AOP and AspectJ AOP 有什么区别?
      Spring AOP 基于动态代理方式实现;AspectJ 基于静态代理方式实现。
      ???SpringAOP 仅支持方法级别的 PointCut;提供了完全的 AOP 支持,它还支持属性级别的 PointCut。
    • 什么是编织(Weaving)?
      为了创建一个 advice 对象而链接一个 aspect 和其它应用类型或对象,称为编织(Weaving)。在 Spring AOP 中,编织在运行时执行。
    • 如何理解 Spring 中的代理?
      ???将 Advice 应用于目标对象后创建的对象称为代理。在客户端对象的情况下,目标对象和代理对象是相同的。
  • ??? SpringAOP的底层实现

    • 总: aop概念,应用场景,动态代理
    • 分: bean的创建过程中有一个步骤可以对bean进行扩展实现,aop本身就是IOC 的一个扩展功能,所以在BeanPostProcessor的后置处理方法中来进行实现
      1、代理对象的创建过程(advice,切面,切点)
      2、通过jdk或者cglib的方式来生成代理对象
      3、在执行方法调用的时候,会调用到生成的字节码文件中,直接回找到DynamicAdvisoredlnterceptor类中的intercept方法,从此方法开始执行
      4、根据之前定义好的通知来生成拦截器链
      5、从拦截器链中依次获取每一个通知开始进行执行,在执行过程中,为了方便找到下一个通知是哪个,会有一个CglibMethodlnvocation的对象,找的时候是从-1的位置一次开始查找并且执行的。
  • 在工作中实际用到过AOP优化代码吗???
    有的。当时我用AOP来对我们公司现有的监控客户端进行封装
    一个系统离不开监控,监控基本的指标有QPS、RT、ERROR等等。对外暴露的监控客户端只能在代码里写对应的上报信息(灵活,但会与业务代码掺杂在一起),于是我利用注解+AOP的方式封装了一把,只要方法/类上带有我自定义的注解;方法被调用时,就会上报AQS、RT等信息
    实现了非业务代码与业务代码分离的效果(:

  • Spring DAO
    Spring DAO 使得 JDBC,Hibernate 或 JDO 这样的数据访问技术更容易以一种统一的方式工作。这使得用户容易在持久性技术之间切换。它还允许您在编写代码时,无需考虑捕获每种技术不同的异常。

  • Spring事务管理:

    • Spring框架提供的一种事务管理机制,它简化了事务管理的操作,并提供了对不同事务管理器的统一接口。两种方式:
    • 声明式事务:通过在方法上使用 @Transactional 注解来声明事务。这种方式更为常用,允许开发者将精力集中在业务逻辑上,而不需要关心事务的开始、提交或回滚。
      1
      2
      3
      4
      @Transactional
      public void myTransactionalMethod() {
      // 业务逻辑
      }
    • 编程式事务:通过编写代码手动管理事务的开始、提交或回滚。虽然不太常用,但在一些特殊情况下可能会用到。
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      @Autowired
      private PlatformTransactionManager transactionManager;

      public void myProgrammaticMethod() {
      DefaultTransactionDefinition def = new DefaultTransactionDefinition();
      TransactionStatus status = transactionManager.getTransaction(def);
      try {
      // 业务逻辑
      transactionManager.commit(status);
      } catch (Exception e) {
      transactionManager.rollback(status);
      throw e;
      }
      }
  • Spring事务是如何回滚的?

    • 即:Spring事务管理是怎么实现的?
      考虑数据库事务:建立连接,开启事务->进行sql操作->成功,commit/失败,rollback,,发现sql操作就是业务逻辑,前后工作完全类似AOP的around
    • 总:spring的事务是由aop来实现的,首先要生成具体的代理对象,然后按照aop的整套流程来执行具体的操作逻辑,正常情况下要通过通知来完成核心功能,但是事务不是通过通知来实现的,而是通过一个TansactionInterceptor来实现的,然后调用invoke()实现具体的逻辑
    • ???分:
      1、先做准备工作,解析各个方法上事务相关的属性,根据具体的属性来判断是否开始新事务
      2、当需要开启的时候,获取数据库连接,关闭自动提交功能,开起事务
      3、执行具体的sql逻辑操作
      4、在操作过程中,如果执行失败了,那么会通过completeTransactionAterThrowing看来完成事务的回滚操作,回滚的具体逻辑是通过doRollBack方法来实现的,实现的时候也是要先获取连接对象,通过连接对象来回滚
      5、如果执行过程中,没有任何意外情况的发生,那么通过commitTransactionAfterReturning来完成事务的提交操作,提交的具体逻辑是通过doCommit方法来实现的,实现的时候也是要获取连接,通过连接对象来提交
      6、当事务执行完毕之后需要清除相关的事务信息cleanupTransactionlnfo
      如果想要聊的更加细致的话,需要知道Transactionlnfo,Transactionstatus,
  • Spring中的事务是如何实现的
    1,Spring事务底层是基于数据库事务和AOP机制的
    2,首先对于使用了@Transactional注解的Bean,Spring会创建一个代理对象作为Bean
    3,当调用代理对象的方法时,会先判断该方法上是否加了@Transactional注解
    4,如果加了,那么则利用事务管理器创建一个数据库连接、
    5,并且修改数据库连接的autocommit属性为false,禁止此连接的白动提交,这是实现Spring事务非常重要的一步
    6,然后执行当前方法,方法中会执行sql
    7。执行完当前方法后,如果没有出现异常就直接提交事务
    8,如果出现了异常,并且这个异常是需要回滚的就会回滚事务,否则仍然提交事务
    9,Spring事务的隔离圾别对应的就是数据库的隔离级别
    10,Spring事务的传播机制是Spring事务自己实现的,也是Spring事务中最复杂的
    11,Spring事务的传机制是基于数据库连接来做的,一个数据库连接一个事务,如果传机制配置为需要新开一个事务,那么实际上就是先建立一个数库连接,在此新数据库连接上执行sql

  • Spring事务传播机制
    多个事务方法相互调用时,事务如何在这些法间传摇,方法A是一个事务的方法,方法A执行过程中调用了方法B,那么方法B有无事务以及方法B对事务的要求不同都会对方法A的事务具体执行造成影响,同时方法A的事务对方法B的事务执行也有影响,这种影响具体是什么就由两个方法所定义的事务传播类型所决定。
    1,REQUIRED(Spring默认的事务传播类型): 如果当前没有事务,则自己新建一个事务,如果当前存在事务,则加入这个事务
    2,SUPPORTS:当前存在事务,则加加入当前事务,如果当前没有事务,就以非事务方法执行
    3。MANDATORY:当前存在事务,则加入当前事务,如果当前事务不存在,则抛出异常。
    4,REQUIRES_NEW: 创建一个新事务,如果存在当前事务,则挂起该事务。
    5,NOT_SUPPORTED: 以非事务方式执行如里当前存在事务,则持起当前事务
    6,NEVER:不使用事务,如果当前事务存在,则抛出异常
    7,NESTED: 如果当前事存在,则在嵌套事务中执行,否则REQUIRED的操作一样 (开启一个事务)
    https://www.bilibili.com/video/BV1Mc411S7B7?p=18&vd_source=ff210768dfaee27c0d74f9c8c50d7274

  • Spring事务什么时候会失效?
    spring事务的原理是AOP,进行了切面增强,那么失效的根本原因是这个AOP不起作用了! 常见情况有如下几种
    ??1、发生自调用,类里面使用this调用本类的方法(this通常省略),此时这个this对象不是代理类,而是UserService对象本身!解决方法很简单,让那个this变成UserService的代理类即可!
    2、方法不是public的:@Transactional 只能用于 public 的方法上,否则事务不会失效,如果要用在非 public 方法上,可以开启 Aspectj 代理楼式
    3、数据库不支持事务
    4、没有被spring管理
    5、异常被吃掉,事务不会回滚(或者抛出的异常没有被定义,默认为RuntimeException)

  • Spring中常见的设计模式。

    1. 单例模式:Spring默认情况下,容器中的Bean是单例的,即每个容器中的Bean只有一个实例。有助提高性能和减少资源消耗
    2. 原型模式:指定作用域为prototype
    3. 工厂模式:Spring通过工厂模式实现了IOC(Inversion of Control)容器,使得对象的创建和管理被委托给了Spring容器。 BeanFactory
    4. 代理模式:Spring AOP就是通过代理模式实现的。AOP允许开发者在不修改原始代码的情况下,插入和控制横切关注点,如日志、事务等。
    5. 责任链模式:使用aop时先生成一个拦截器链
    6. 模板方法模式: 在Spring中,JdbcTemplate 和 RedisTemplate 等模板类使用了模板方法模式,将通用的任务实现在模板方法中,而将具体实现留给子类。
      postProcessBeanFactory,onRefresh,initPropertyValue
    7. 观察者模式:Spring的事件机制是基于观察者模式的。通过定义事件和监听器,应用程序可以订阅感兴趣的事件,从而实现松耦合的通信机制。
    8. 策略模式:Spring的资源加载、事务管理等功能用了策略模式,通过定义一组算法族分别封装起来,并使它们可以相互替换。
    9. 适配器模式:SpringAOP中通知(Advice)就是一种适配器模式的应用。通知包装了一个切面逻辑,使得它可以在切点(Join Point)上执行。
    10. 装饰者模式:BeanWrapper
    11. 适配器模式:Adapter

Spring MVC

  • 什么是 MVC?
    MVC是一种设计模式,解决表现层的问题;MVC 模式有助于分离应用程序的不同方面,如输入逻辑,业务逻辑和 UI 逻辑,同时在所有这些元素之间提供松散耦合。
    Model:模型(完成业务逻辑:有javaBean构成,service+dao+entity)
    Controller:控制器(接收请求->调用模型->根据结果派发页面),SpringMVC通过一套注解,可以让普通的JAVA类成为contrlor控制器,无需继承Servlet
    View:视图层(将结果渲染,相应给客户)

  • Spring MVC 框架
    SpringMVC是一个MVC的开源框架,提供 模型-视图-控制器 架构和随时可用的组件,用于开发灵活且松散耦合的 Web 应用程序。
    SpringMVC=struts2+spring,springMVC就相当于是Struts2 加上sring的整合;SpringMVC是spring的一个后续产品,其实就是spring在原有基础上,又提供了web应用的MVC模块,可以简单的把springMVC理解为是spring的一个模块(类似AOP,IOC这样的模块)

  • ???Spring MVC 流程
    https://www.bilibili.com/read/cv26597569/?spm_id_from=333.999.0.0&jump_opus=1
    1、向服务器发送 HTTP 请求,请求被前端控制器 DispatcherServlet 捕获。
    2、 DispatcherServlet 根据 -servlet.xml 中的配置对请求的 URL 进行解析,得到请求资源标识符(URI)。然后根据该 URI,调用HandlerMapping获得该 Handler 配置的所有相关的对象(包括 Handler 对象以及 Handler 对象对应的拦截器),最后以 HandlerExecutionChain 对象的形式返回。
    3、 DispatcherServlet 根据获得的 Handler,选择一个合适的HandlerAdapter。(附注:如果成功获得 HandlerAdapter 后,此时将开始执行拦截器的 preHandler(…)方法)。
    4、提取 Request 中的模型数据,填充 Handler 入参,开始执行 Handler( Controller)。在填充 Handler 的入参过程中,根据你的配置,Spring 将帮你做一些额外的工作: HttpMessageConveter:将请求消息(如 Json、xml 等数据)转换成一个对象,将对象转换为指定的响应信息。 数据转换:对请求消息进行数据转换。如 String 转换成 Integer、Double 等。 数据根式化:对请求消息进行数据格式化。如将字符串转换成格式化数字或格式化日期等。 数据验证:验证数据的有效性(长度、格式等),验证结果存储到BindingResult 或 Error 中。
    5、Handler(Controller)执行完成后,向 DispatcherServlet 返回一个ModelAndView 对象;
    6、根据返回的 ModelAndView,选择一个适合的 ViewResolver(必须是已经注册到 Spring 容器中的 ViewResolver)返回给DispatcherServlet。
    7、 ViewResolver 结合 Model 和 View,来渲染视图。
    8、视图负责将渲染结果返回给客户端。

  • SpringMVC 常见注解
    @RequestMapping:用于处理请求url映射的注解,可用于类或方法上。用于类上,则表示类中 的所有响应请求的方法都是以该地址作为父路径。
    @RequestBody:注解实现接收http请求的json数据,将json转换为java对象。
    @ResponseBody:注解实现将conreoller方法返回对象转化为ison对象响应给客户
    、、、


Spring Boot

  • 什么是 Spring Boot?
    随着新功能的增加,spring 变得越来越复杂。如果必须启动一个新的 Spring 项目,我们必须添加构建路径或添加Maven 依赖关系,配置应用程序服务器,添加 spring 配置,即开始一个新的 spring 项目需要很多努力,因为我们现在必须从头开始做所有事情。
    Spring Boot 最重要的就是起步依赖和自动配置: 使用starter启动器能够自动依赖其他组件,减少了Maven配置的繁琐性;Spring Boot根据当前类路径和jar包自动配置bean,例如,添加spring-boot-starter-web启动器即可拥有web功能,无需额外的配置。因此,Spring Boot 可以帮助我们以最少的工作量,更加健壮地使用现有的 Spring功能。

  • 为什么要用SpringBoot?

    1. 独立运行和简化部署: Spring Boot内嵌了各种servlet容器(如Tomcat、Jetty、Undertow),使得应用能够独立运行,无需打成war包部署到外部容器。通过打包成可执行的jar包,所有依赖包都在一个jar包内,简化了部署和运维。
    2. 简化配置和自动配置: 使用starter启动器能够自动依赖其他组件,减少了Maven配置的繁琐性。Spring Boot根据当前类路径和jar包自动配置bean,例如,添加spring-boot-starter-web启动器即可拥有web功能,无需额外的配置。
    3. 无代码生成和XML配置: Spring Boot的配置过程中无需代码生成,也不需要XML配置文件,通过条件注解实现自动配置,进一步简化了项目配置。
    4. 应用监控和健康检测: Spring Boot提供了一系列端点用于监控服务和应用,实现健康检测等功能。
    5. 减少开发、测试时间和努力: 提供快速启动开发的默认值,减少了开发和测试的时间和工作量。
    6. 使用JavaConfig避免XML配置: 倡导使用JavaConfig替代XML配置,提高了代码的可读性和维护性。
    7. 提供意见发展方法: Spring Boot提供了一套推荐的开发方法,使得团队更容易达成共识,提高了代码的一致性和规范性。
    8. 基于环境的配置: 支持基于环境的配置,通过传递环境参数(例如-Dspring.profiles.active={environment}),可以在不同环境中灵活配置应用程序。
  • 如何创建 Spring Boot Projects ?

    • Spring Initiatlizr 让创建 Spring Boot 项目变的很容易,通过 start.spring.io 创建。
    • 手动设置一个 Maven 项目为 Spring Boot 项目:
      1. 添加 Spring Boot 依赖: 在 Maven 项目的 pom.xml 文件中,添加 Spring Boot 的依赖。通常,你可以添加 spring-boot-starter 或其他特定的 Starter 依赖,具体依赖根据项目需求而定。
        1
        2
        3
        4
        5
        6
        7
        8
        <dependencies>
        <!-- Spring Boot Starter 依赖 -->
        <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter</artifactId>
        </dependency>
        <!-- 其他依赖 -->
        </dependencies>
      2. 添加 Spring Boot 插件:pom.xml 中添加 Spring Boot Maven 插件。插件用于打包和运行 Spring Boot 应用程序。
        1
        2
        3
        4
        5
        6
        7
        8
        <build>
        <plugins>
        <plugin>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-maven-plugin</artifactId>
        </plugin>
        </plugins>
        </build>
      3. 创建 Spring Boot 主类: 在项目中创建一个包含 main 方法的主类,该类用于启动 Spring Boot 应用程序。这个类通常被注解为 @SpringBootApplication
      4. 添加其他 Spring Boot 配置: 根据项目需求,配置其他 Spring Boot 特性,如数据源、JPA、Web 支持等。可以通过配置文件(application.propertiesapplication.yml)或 Java 配置类进行配置。
  • Spring Boot 的核心注解是哪个?它主要由哪几个注解组成的?
    启动类上面的注解是 @SpringBootApplication,它也是 Spring Boot 的核心注解,主要组合包含了以下3个注解:
    1、@SpringBootConfiguration:组合了 @Configuration 注解,实现配置文件的功能。
    2、@EnableAutoConfiguration:打开自动配置的功能,也可以关闭某个自动配置的选项,如关闭数据源自动配置功能:@SpringBootApplication(exclude = { DataSourceAutoConfiguration.class })。
    3、@ComponentScan:Spring组件扫描

  • Spring Boot中常用注解及其底层实现

    • @Bean注解:用来定义Bean,类似于XML中的bean>标签,Spring在启动时,会对加了@Bean注解的方法进行折,将方法的名字做为beanName,并通过执行方法得到bean对象
    • Bean处理
      • @Autowired:依赖注入
      • @Component:泛指组件
      • @Controller、@Service、@Repository
      • @RestController
      • @Configuration
    • Http请求:@GetMapping、@PostMapping、@PutMapping、@DeleteMapping
    • 前后端参数传递:
      • @RequestParam:用在方法的参数前面,获取请求中表单类型的key=value格式的数据
      • @PathVariable:路径变量,参数与大括号里的名字一样要相同
      • @RequestBody:获取请求body中的数据,常用于搭配@PostMapping请求来提交对象数据
      • @ResponseBody:表示该方法的返回结果直接写入HTTP response body中,格式为json
    • 读取配置
      • @value:直接读取各种配置源的属性名
      • @ConfigurationProperties:读取配置信息并与 bean 绑定
      • @PropertySource:指定加载自定义的配置文件
    • 参数校验
      • Bean字段验证注解:@Null、@Min()、,,??
      • ??@Valid:用于标注验证对象的级联属性
      • ??@Validated:Spring提供的注解,于SpringMVC一起使用标注方法参数需要检查
    • 统一异常处理
      • @ControllerAdvice:注解定义全局异常处理类,包含@Component所以可以被Spring扫描到
      • @ExceptionHandler:注解声明异常处理方法,表示遇到这个异常,就执行标注的方法
    • JPA数据持久化
      • @Entity:声明数据库实体类
      • @Table:设置表明
      • @ld:声明一个字段为主键
      • @GeneratedValue:声明主键的生成策略
      • @Column:声明字段,经常用于属性名和表字段的映射
      • @Transient:指定不需要持久化的字段
      • @Lob:声明某个字段为大字段
      • @Enumerated:声明枚举类型的字段
      • @Modifying:加在DAO方法上,提示是修改操作
      • @Transactional
        • 作用于类上:表示所有该类的public 方法都配置相同的事务属性信息
        • 作用于方法上:当类配置了@Transactional,方法也配置了@Transactional,方法的事务会覆盖类的事务配置信息
    • 测试处理
      • @ActiveProfiles:常作思于测试类上,用于声明生效的 Spring 配置文件
      • @Test:声明一个方法为测试方法
      • @Transactional:被声明的测试方法的数据会回滚,避免污染测试数据
      • @WithMockUser:Spring Security 提供的,用来模拟一个真实用户,并且可以赋予权限
    • 配置启动
      • @SpringBootApplication注解: 这个注解标识了一个SpringBoot工程,它实际上是另外三个注解的组合:
        a,@SpringBootConfiguration: 这个注解实际就是一个@Configuration,表示启动类也是一个配置类
        b,@EnableAutoConfiguration:向Spring容器中导入了一Seletor,用来加ClassPath下SpringFactoties中所定义的自动配置类,将这些自动为配置Bean
        c,@ComponentScan:标识扫描路径,因为赋认是没有配置实际扫描路径,所以SpringBoot扫描的路径是启动类所在的当前目录
      • @Conditional
        • @ConditionalOnBean:配置了某个特定的Bean
        • @ConditionalOnMissingBean:没有配置特定的Bean
        • @ConditionalOnClass:Classpath里有指定的类
        • @ConditionalOnMissingClass:Classpath里没有指定的类
        • @ConditionalOnExpression:给定的SpEL表达式计算结果为true
        • @ConditionalOnJava:Java的版本匹配特定值或者一个范围值
        • @ConditionalonIndi:参数中给定的INDI位置必须存在一个,如果没有给参数,则要有JNDIInitialContext
        • @ConditionalOnProperty:指定的配置属性要有一个明确的值
        • @ConditionalOnResource:Classpath里没有指定的资源
        • @ConditionalOnWebApplication:这是一个Web应用程序
        • @ConditionalOnNotWebApplication:这不是一个Web应用程序
  • ??说说SpringBoot自动配置
    1、SpringBoot有着“约定大于配置”的理念,这一理念一定程度上可以用“SpringBoot自动配置”来解释。
    通过 Spring Boot,我们可以快速开发基于 Spring 生态下的应用程序。
    2、Spring Boot 约定由于配置的体现有很多,比如:
    Spring Boot Starter 启动依赖,它能帮我们管理所有 jar 包版本;
    如果当前应用依赖了 spring mvc 相关的 jar,那么 Spring Boot 会自动内置Tomcat 容器来运行 web 应用,我们不需要再去单独做应用部署;
    Spring Boot 的自动装配机制的实现中,通过扫描约定路径下的 spring.factories 文件来识别配置类,实现 Bean 的自动装配;
    默认加载的配置文件 application.properties 等等
    3、在使用SpringBoot的时候,肯定会依赖于autoconfigure这么一个包,autoconfigure这个包里会有一个spring.factories文件,该文件定义了100+个入口的配置类。比如我们经常使用的redis、kafka等等这样常见的中间件都预置了配置类。当我们在启动SpringBoot项目的时候,内部就会加载这个spring.factories文件,进而去加载“有需要”的配置类。那我们在使用相关组件的时候,就会非常的方便(因为配置类已经初始化了一大部分配置信息)。一般我们只要在application配置文件写上对应的配置,就能通过各种template类直接操作对应的组件啦。
    4、不是所有的配置类都会加载的,假设我们没有引入redis-starter的包,那Redis的配置类就不会被加载。具体Spring在实现的时候就是使用@ConditionalXXX进行判断的。比如Redis的配置类就会有@ConditionalOnClass({RedisOperations.class})的配置,说明当前环境下如果有RedisOperations.class这个字节码,才会去加载Redis的配置类。

  • ??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 是约定优于配置这一理念下的产物,所以在很多的地方,都会看到这类的思想。它的出现,让开发人员更加聚焦在了业务代码的编写上,而不需要去关心和业务无关的配置。其实,自动装配的思想,在 SpringFramework3.x 版本里面的@Enable 注解,就有了实现的雏形。@Enable 注解是模块驱动的意思,我们只需要增加某个@Enable 注解,就自动打开某个功能,而不需要针对这个功能去做 Bean 的配置,@Enable 底层也是帮我们去自动完成这个模块相关 Bean 的注入。以上,就是我对 Spring Boot 自动装配机制的理解。

  • Spring Boot 属性源
    1.命令行参数
    2.JVM系统属性
    3.操作系统环境变量
    4.打包在应用程序内的 application.properties 或者 application.yml 文件
    5.通过 @PropertySource 标注的属性源
    6.默认属性

  • 你对SpringBoot starter的理解?
    Starters是什么:Starters可以理解为启动器,它包含了一系列可以集成到应用里面的依赖包,你可以一站式集成Spring及其他技术,而不需要到处找示例代码和依赖包。
    比如 Mybatis 框架会需要引入各种的包才能使用,而starter就是做了一层封装,把相关要用到的jar都给包起来了,并且也写好了对应的版本。这我们使用的时候就不需要引入一堆jar包且管理版本类似的问题了。
    Starters包含了许多项目中需要用到的依赖,它们能快速持续的运行,都是一系列得到支持的管理传递性依赖。

  • 常用的starter。
    spring-boot-starter-web 嵌入tomcat和web开发需要servlet与jsp支持
    spring-boot-starter-data-jpa 数据库支持
    spring-boot-starter-data-redis redis数据库支持
    spring-boot-starter-data-solr solr支持
    mybatis-spring-boot-starter 第三方的mybatis集成starter

  • Spring Boot是如何启动Tomcat的
    1,首先,SpringBoot在启动时会先创建一个Spring容器
    2,在创建 Spring容器过程中,会利用 @ConditionalOnClass 技术来判断当前 classpath中是否存在 Tomcat 依赖,如果存在则会生成一个启动 tomcat 的Bean
    3,Spring 容器创建完之后,就会获取启动 Tomcat 的Bean,并创建 Tomcat 对象,并绑定端口等,然后启动 Tomcat
    ( Spring Boot内嵌tomcat,与springmvc启动tomcat过程相反??

  • ???Spring Boot中的监视器是什么?如何在 Spring Boot 中禁用 Actuator 端点安全性?
    Spring boot actuator是spring启动框架中的重要功能之一。Spring boot监视器可帮助您访问生产环境中正在运行的应用程序的当前状态。有几个指标必须在生产环境中进行检查和监控。即使一些外部应用程序可能正在使用这些服务来向相关人员触发警报消息。监视器模块公开了一组可直接作为HTTP URL访问的REST端点来检查状态
    默认情况下,所有敏感的 HTTP 端点都是安全的,只有具有 ACTUATATOR 角色的用户才能访问它们。安全性是使用标准的HttpServletRequest.isUserInRole 方法实施的。 我们可以使用来禁用安全性。只有在执行机构端点在防火墙后访问时,才建议禁用安全性。

  • 如何使用Spring Boot实现异常处理?
    Spring提供了一种使用ControllerAdvice处理异常的非常有用的方法。 我们通过实现一个ControlerAdvice类,来处理控制器类抛出的所有异常

  • SpringBoot 实现热部署有哪几种方式?
    主要有两种方式:Spring Loaded,Spring-boot-devtools

  • 如何实现 Spring Boot 应用程序的安全性?
    为了实现 Spring Boot 的安全性,我们使用 spring-boot-starter-security 依赖项,并且必须添加安全配置。它只需要很少的代码。配置类将必须扩展WebSecurityConfigurerAdapter 并覆盖其方法。

  • Spring Boot 支持哪些日志框架?推荐和默认的日志框架是哪个?
    Spring Boot 支持 Java Util Logging, Log4j2, Lockback 作为日志框架,如果你使用 Starters 启动器,Spring Boot 将使用 Logback 作为默认日志框架

  • Spring Boot 2.X 有什么新特性?与 1.X 有什么区别?
    配置变更
    JDK 版本升级
    第三方类库升级
    响应式 Spring 编程
    支持HTTP/2
    支持配置属性绑定
    更多改进与加强…

  • SpringBoot 集成 Mybatis 的过程
    添加mybatis的starter maven依赖 org.mybatis.spring.boot mybatis-spring-boot-starter 1.2.0
    在mybatis的接口中 添加@Mapper注解
    在application.yml配置数据源信息

  • 如何对Spring Boot应用进行测试?
    在为Spring应用程序运行集成测试时,我们必须有一个 ApplicationContext。
    为了简化测试,Spring Boot为测试提供了一个特殊的注释 @SpringBootTest。此批注从其 classes 属性指示的配置类创建 ApplicationContext。 如果未设置classes属性,Spring Boot将搜索主配置类。搜索从包含测试的包开始,直到找到使用@SpringBootApplication或@SpringBootConfiguration注释的类。
    ???请注意,如果我们使用 JUnit 4 ,我们必须用 @RunWith(SpringRunner.class) 装饰测试类。

  • Spring Boot 项目结构

    • Spring Boot 项目通常遵循一种约定大于配置的原则,并提供了一种建议的项目结构,使得开发者可以更轻松地组织和管理项目。以下是一个常见的 Spring Boot 项目结构示例:
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      my-spring-boot-project
      |-- src
      | |-- main
      | |-- java
      | | |-- com
      | | |-- example
      | | |-- MySpringBootApplication.java
      | | |-- controller
      | | |-- HomeController.java
      | | |-- service
      | | |-- MyService.java
      | |-- resources
      | |-- application.properties
      | | |-- static
      | | |-- templates
      |-- target
      |-- pom.xml
    • src/main/java: 存放 Java 源代码。
    • com.example.MySpringBootApplication: Spring Boot 应用程序的主类,包含 main 方法,用于启动应用程序。
    • com.example.controller: 存放控制器类,处理 HTTP 请求。
    • com.example.service: 存放服务类,处理业务逻辑。
    • src/main/resources: 存放资源文件。
    • application.properties: Spring Boot 应用程序的配置文件,用于配置各种属性。
    • static: 存放静态资源文件,如 CSS、JavaScript 等。
    • templates: 存放模板文件,如 Thymeleaf 模板。
    • target: Maven 构建目录,包含编译后的类文件和构建产物。
    • pom.xml: Maven 项目的配置文件,定义项目的依赖和构建配置。
  • 最后聊下你是怎么看这块源码的???
    思路:我先从启动类开始,会有个@SpringBootApplication,后面会定位到一个自动配置的注解@EnableAutoConfiguration,那最后就能看到注解内部会去META-INF/spring.factories加载配置类


SpringCloud

  • 什么是 Spring Cloud?
    Spring cloud 流应用程序启动器是基于 Spring Boot 的 Spring 集成应用程序,提供与外部系统的集成。Spring cloud Task,一个生命周期短暂的微服务框架,用于快速构建执行有限数据处理的应用程序。

  • 使用 Spring Cloud 有什么优势?

    1. 高可用性: 分布式架构能够提高系统的可用性。通过将系统划分为多个服务并分布在不同的节点上,即使某个节点或服务出现故障,整个系统仍然能够继续运行。
    2. 扩展性: 分布式系统可以更容易地进行横向扩展。通过增加节点或服务,系统可以更好地处理增加的负载,提高性能和吞吐量。
    3. 容错性: 分布式系统能够通过冗余和备份机制来提高容错性。即使某个节点或服务出现故障,备份节点或服务可以接管工作,保证系统的正常运行。
    4. 灵活性: 分布式系统能够更好地支持异构性,即不同类型的硬件、操作系统和编程语言。这使得系统更加灵活,能够选择最适合特定任务的技术栈。
    5. 资源共享: 分布式系统可以充分利用多个节点的计算和存储资源,实现资源的共享和最优化利用。
    6. 降低单点故障风险: 分布式系统通过将系统划分为多个部分,降低了单点故障的风险。即使某个节点或服务出现问题,其他部分仍然可以继续运行。
    7. 地理分布: 分布式系统支持地理分布,使得服务可以部署在不同的地理位置,提高服务的可用性和响应速度。
    8. 提高性能: 分布式系统可以通过并行计算和分布式存储来提高性能。任务可以同时在多个节点上执行,加速处理过程。
  • 使用 Spring Boot 开发分布式微服务时,我们面临以下问题
    1、与分布式系统相关的复杂性-这种开销包括网络问题,延迟开销,带宽问题,安全问题。
    2、服务发现-服务发现工具管理群集中的流程和服务如何查找和互相交谈。它涉及一个服务目录,在该目录中注册服务,然后能够查找并连接到该目录中的服务。
    3、冗余-分布式系统中的冗余问题。
    4、负载平衡 –负载平衡改善跨多个计算资源的工作负荷,诸如计算机,计算机集群,网络链路,中央处理单元,或磁盘驱动器的分布。
    5、性能-问题 由于各种运营开销导致的性能问题。
    6、部署复杂性-Devops 技能的要求。

  • Spring Cloud 中的五大组件。

    1. Eureka(服务注册与发现): 服务注册与发现框架。它用于管理各个微服务的状态,以实现服务之间的通信和发现。
    2. Ribbon(负载均衡): Ribbon 是一个基于 HTTP 和 TCP 的客户端负载均衡工具,在服务消费者的请求中自动实现负载均衡,从而分摊到多个服务提供者上。
    3. Feign(声明式服务调用): Feign 是一个声明式的伪 HTTP 客户端,它使得编写 HTTP 客户端变得更加简单。通过 Feign,开发者只需使用注解方式就可以轻松地调用服务,而不用手动处理 HTTP 请求和响应。
    4. Hystrix(熔断器): 一种延迟和故障容错库,用于隔离访问远程服务、第三方库或者服务的点,防止故障蔓延到整个系统,从而提高系统的弹性和可用性。
    5. Zuul(API 网关): Zuul 是 Netflix 提供的一个基于 JVM 路由和服务端的负载均衡器,用于构建微服务架构中的 API 网关,对外统一提供服务访问入口,并提供了路由、负载均衡、熔断、安全等功能。
  • 服务注册和发现是什么意思?Spring Cloud 如何实现?
    当我们开始一个项目时,我们通常在属性文件中进行所有的配置。随着越来越多的服务开发和部署,添加和修改这些属性变得更加复杂。有些服务可能会下降,而某些位置可能会发生变化。手动更改属性可能会产生问题。 Eureka 服务注册和发现可以在这种情况下提供帮助。由于所有服务都在 Eureka 服务器上注册并通过调用 Eureka 服务器完成查找,因此无需处理服务地点的任何更改和处理。

  • 负载平衡的意义什么?
    在计算中,负载平衡可以改善跨计算机,计算机集群,网络链接,中央处理单元或磁盘驱动器等多种计算资源的工作负载分布。负载平衡旨在优化资源使用,最大化吞吐量,最小化响应时间并避免任何单一资源的过载。使用多个组件进行负载平衡而不是单个组件可能会通过冗余来提高可靠性和可用性。负载平衡通常涉及专用软件或硬件,例如多层交换机或域名系统服务器进程。

  • 什么是 Hystrix???
    它如何实现容错?Hystrix 是一个延迟和容错库,旨在隔离远程系统,服务和第三方库的访问点,当出现故障是不可避免的故障时,停止级联故障并在复杂的分布式系统中实现弹性。通常对于使用微服务架构开发的系统,涉及到许多微服务。这些微服务彼此协作

  • 什么是微服务
    微服务架构是一种架构模式或者说是一种架构风格,它提倡将单一应用程序划分为一组小的服务,每个服务运行在其独立的自己的进程中,服务之间相互协调、互相配合,为用户提供最终价值。服务之间采用轻量级的通信机制互相沟通(通常是基于HTTP的RESTful API),每个服务都围绕着具体的业务进行构建,并且能够被独立的构建在生产环境、类生产环境等。
    另外,应避免统一的、集中式的服务管理机制,对具体的一个服务而言,应根据业务上下文,选择合适的语言、工具对其进行构建,可以有一个非常轻量级的集中式管理来协调这些服务,可以使用不同的语言来编写服务,也可以使用不同的数据存储。

  • 什么是服务熔断?服务降级?
    服务降级熔断机制是应对雪崩效应的一种微服务链路保护机制。当某个微服务不可用或者响应时间太长时,会进行服务降级,进而熔断该节点微服务的调用,快速返回“错误”的响应信息。当检测到该节点微服务调用响应正常后恢复调用链路。在SpringCloud框架里熔断机制通过Hystrix实现,Hystrix会监控微服务间调用的状况,当失败的调用到一定阈值,缺省是5秒内调用20次,如果失败,就会启动熔断机制。
    服务降级,一般是从整体负荷考虑。就是当某个服务熔断之后,服务器将不再被调用,此时客户端可以自己准备一个本地的fallback回调,返回一个缺省值。这样做,虽然水平下降,但好歹可用,比直接挂掉强。

  • 说说 RPC 的实现原理
    首先需要有处理网络连接通讯的模块,负责连接建立、管理和消息的传输。其次需要有编解码的模块,因为网络通讯都是传输的字节码,需要将我们使用的对象序列化和反序列化。剩下的就是客户端和服务器端的部分,服务器端暴露要开放的服务接口,客户调用服务接口的一个代理实现,这个代理实现负责收集数据、编码并传输给服务器然后等待结果返回。

  • REST 和RPC对比
    1.RPC主要的缺陷是服务提供方和调用方式之间的依赖太强,需要对每一个微服务进行接口的定义,并通过持续继承发布,严格版本控制才不会出现冲突。
    2.REST是轻量级的接口,服务的提供和调用不存在代码之间的耦合,只需要一个约定进行规范。

  • 微服务的优点缺点?说下开发项目中遇到的坑?
    优点:1.每个服务直接足够内聚,代码容易理解2.开发效率高,一个服务只做一件事,适合小团队开发3.松耦合,有功能意义的服务。4.可以用不同语言开发,面向接口编程。5.易于第三方集成6.微服务只是业务逻辑的代码,不会和HTML,CSS或其他界面结合.7.可以灵活搭配,连接公共库/连接独立库
    缺点:1.分布式系统的责任性2.多服务运维难度加大。3.系统部署依赖,服务间通信成本,数据一致性,系统集成测试,性能监控。

  • Spring Cloud 和 Dubbo 有哪些区别?
    Spring cloud是一个微服务框架,提供了微服务领域中的很多功能组件,Dubbo以开始是一个RPC调用框架,核心是解决服务调用间的问题,Springcloud是一个大而全框架,Dubbo则更侧重于服务调用,所以Dubbo所提供的功能没有Springcloud全面,但Dubbo的服务调用性能Springcloud高,不过并不对立的,是可以结合起来一起使用的。

  • Eureka和zookeeper都可以提供服务注册与发现的功能,请说说两个的区别?
    Zookeeper保证了CP(C:一致性,P:分区容错性),Eureka保证了AP(A:高可用)
    1.当向注册中心查询服务列表时,我们可以容忍注册中心返回的是几分钟以前的信息,但不能容忍直接down掉不可用。也就是说,服务注册功能对高可用性要求比较高,但zk会出现这样一种情况,当master节点因为网络故障与其他节点失去联系时,剩余节点会重新选leader。问题在于,选取leader时间过长,30 ~ 120s,且选取期间zk集群都不可用,这样就会导致选取期间注册服务瘫痪。在云部署的环境下,因网络问题使得zk集群失去master节点是较大概率会发生的事,虽然服务能够恢复,但是漫长的选取时间导致的注册长期不可用是不能容忍的。
    2.Eureka保证了可用性,Eureka各个节点是平等的,几个节点挂掉不会影响正常节点的工作,剩余的节点仍然可以提供注册和查询服务。而Eureka的客户端向某个Eureka注册或发现时发生连接失败,则会自动切换到其他节点,只要有一台Eureka还在,就能保证注册服务可用,只是查到的信息可能不是最新的。除此之外,Eureka还有自我保护机制,如果在15分钟内超过85%的节点没有正常的心跳,那么Eureka就认为客户端与注册中心发生了网络故障,此时会出现以下几种情况:
    ①、Eureka不在从注册列表中移除因为长时间没有收到心跳而应该过期的服务。
    ②、Eureka仍然能够接受新服务的注册和查询请求,但是不会被同步到其他节点上(即保证当前节点仍然可用)
    ③、当网络稳定时,当前实例新的注册信息会被同步到其他节点。因此,Eureka可以很好的应对因网络故障导致部分节点失去联系的情况,而不会像Zookeeper那样使整个微服务瘫痪

  • Ribbon和Feign的区别?
    1.都是调用其他服务的,但方式不同。
    2.启动类注解不同,Ribbon是@RibbonClient,feign的是@EnableFeignClients
    3.服务指定的位置不同,Ribbon是在@RibbonClient注解上声明,Feign则是在定义抽象方法的接口中使用@FeignClient声明。
    4.调用方式不同,Ribbon需要自己构建http请求,模拟http请求然后使用RestTeTemplate发送给其他服务,步骤相当繁琐。Feign需要将调用的方法定义成抽象方法即可。

  • Spring Cloud Gateway?
    Spring Cloud Gateway是Spring Cloud官方推出的第二代网关框架,取代Zuul网关。网关作为流量的,在微服务系统中有着非常作用,网关常见的功能有路由转发、权限校验、限流控制等作用。
    使用了一个RouteLocatorBuilder的bean去创建路由,除了创建路由RouteLocatorBuilder可以让你添加各种predicates和filters,predicates断言的意思,顾名思义就是根据具体的请求的规则,由具体的route去处理,filters是各种过滤器,用来对请求做各种判断和修改。

  • 什么是 Ribbon负载均衡?
    1.Spring Cloud Ribbon是基于Netflix Ribbon实现的一套客户端 负载均衡的工具。
    2.Ribbon客户端组件提供一系列完善的配置项如连接超时,重试等。简单的说,就是在配置文件中列出Load Balancer(简称LB)后面所有的机器,Ribbon会自动的帮助你基于某种规则(如简单轮询,随机连接等)去连接这些机器。我们也很容易使用Ribbon实现自定义的负载均衡算法。

面 逝 | 组件

MySQL

  • SQL 语言类别。

    1. DDL(数据定义语言):DDL 用于定义和管理数据库对象(如表、索引、视图等);
      CREATE TABLE:创建表。 ALTER TABLE:修改表结构。DROP TABLE:删除表。CREATE INDEX:创建索引。
    2. DML(数据操作语言):DML 用于对数据库中的数据进行操作;
      SELECT:查询数据。 INSERT:插入数据。 UPDATE:更新数据。 - DELETE:删除数据。
    3. DCL(数据控制语言):管理数据库对象的访问权限, GRANT:授予用户访问权限、REVOKE:回收用户访问权限 等。
    4. TCL(事务控制语言):用于管理数据库的事务,常用的命令包括 COMMIT:提交事务、 ROLLBACK:回滚事务、 SAVEPOINT:设置保存点,用于部分回滚。
  • MySQL 数据类型

    1. 数值类型:
      • 整数类型:INT(整数,4字节) TINYINT(小整数,1字节) SMALLINT(小整数,2字节) MEDIUMINT(中等整数,3字节) BIGINT(大整数,8字节)
      • 浮点数类型:FLOAT(单精度浮点数) DOUBLE(双精度浮点数)
      • 定点数类型:DECIMAL(定点数)
    2. 日期和时间类型:DATE(日期);TIME(时间)DATETIME(日期和时间,包括秒) TIMESTAMP(日期和时间,包括秒,通常用于记录数据的修改时间)
    3. 字符串类型:CHAR(定长字符串); VARCHAR(变长字符串);TEXT(较小的文本) MEDIUMTEXT(中等大小的文本) LONGTEXT(较大的文本)
      • CHAR 和 VAVARCHAR 的区别?
        1、CHAR 和 VAVARCHAR 类型在存储和检索方面有所不同
        2、CHAR 列长度固定为创建表时声明的长度,长度值范围是 1 到 255 当 CHAR值被存储时,它们被用空格填充到特定长度,检索 CHAR 值时需删除尾随空格。
    4. 二进制类型: BINARY(定长二进制字符串) VARBINARY(变长二进制字符串) BLOB(较小的二进制数据) MEDIUMBLOB(中等大小的二进制数据) LONGBLOB(较大的二进制数据)
      • BLOB 和 TEXT 的区别。
        1. 存储内容: BLOB 存储二进制数据,不进行字符集的转换。 TEXT 存储字符数据,会根据字符集进行相应转换
        2. 大小限制: BLOB 可以存储更大的二进制数据。 TEXT 可以存储更大的字符数据。
        3. 排序和比较: BLOB 进行二进制排序和比较。 TEXT 进行字符集排序和比较。
        4. 用途: BLOB 适用存储图像、音频、视频等二进制文件。TEXT 适用存储文本文档、HTML、XML 等字符数据。
    5. 其他类型: ENUM(枚举类型) SET(集合类型)
  • MySQL 里记录货币用什么字段类型好?

    • 在记录货币金额时,一般建议使用 DECIMAL 类型。DECIMAL 类型是一种精确的定点数类型,提供了更可靠的精确度,确保不会发生舍入误差,适合用于存储货币等需要精确计算的数值。
    • 避免使用浮点数类型(如 FLOAT 或 DOUBLE)来表示货币金额,因为浮点数在计算中可能存在精度问题。
      1
      2
      3
      -- 9(precision)代表将被用于存储值的总的小数位数,而 2(scale)代表将被用于存储小数点后的位数。 
      -- salary 列中的值的范围是从-9999999.99 到9999999.99。
      salary DECIMAL(9,2)
    • 为表中得字段选择合适得数据类型
      字段类型优先级: 整形>date,time>enum,char>varchar>blob,text
      优先考虑数字类型,其次是日期或者二进制类型,最后是字符串类型,同级别的数据类型,应该优先选择占用空间小的
  • MySQL 中 INEXISTSLIKE 的作用和用法,如何优化 DISTINCT

    • IN:用于在 WHERE 子句中筛选出符合指定值列表中任一值的数据行。
      1
      2
      -- 查询出 `id` 字段值为 1、2 或 3 的用户数据行
      SELECT * FROM users WHERE id IN (1, 2, 3);
    • EXISTS:用于检查子查询是否返回任何行,如果子查询返回至少一行数据,则 EXISTS 返回 true;否则返回 false。
      1
      2
      -- 上述示例会查询出至少有一笔订单关联的产品数据行
      SELECT * FROM products p WHERE EXISTS (SELECT 1 FROM orders o WHERE o.product_id = p.id);
    • LIKE:用于在 WHERE 子句中进行模糊查询,通常配合通配符使用包括 %(匹配任意长度的字符串)和 _(匹配单个字符)
      1
      2
      -- 上述示例会查询出名称以 "apple" 开头的产品数据行
      SELECT * FROM products WHERE name LIKE 'apple%';
    • DISTINCT 在所有列上转换为 GROUP BY,并与 ORDER BY 子句结合使用。
      1
      2
      3
      SELECT DISTINCT t1.a FROM t1,t2 where t1.a=t2.a;
      -- 转换为等效的 GROUP BY 查询:
      SELECT t1.a FROM t1, t2 WHERE t1.a = t2.a GROUP BY t1.a;
  • 什么是通用 SQL 函数?
    1、CONCATAT(A, B) – 连接两个字符串值以创建单个字符串输出。通常用于将两个或多个字段合并为一个字段。
    2、FORMATAT(X, D)- 格式化数字 X 到 D 有效数字。
    3、CURRDATATE(), CURRTIME()- 返回当前日期或时间。
    4、NOW() – 将当前日期和时间作为一个值返回。
    5、MONTH(),DAYAY(),YEAR(),WEEK(),WEEKDAYAY() – 从日期值中提取给定数据。
    6、HOUR(),MINUTE(),SECOND() – 从时间值中提取给定数据。
    7、DATATEDIFF(A,B) – 确定两个日期之间的差异,通常用于计算年龄
    8、SUBTIMES(A,B) – 确定两次之间的差异。
    9、FROMDAYAYS(INT) – 将整数天数转换为日期值

  • 一条 sql 的执行过程。

    1. 连接层:客户端是否与mysql连接。
    2. server层:mysql8.0前会在缓存中查询是否执行过此sql,命中则直接返回;8.0后取消了这个机制。
    3. 语法解析器:对接收到的 SQL 语句进行解析,以检查其语法和语义是否正确。如果 SQL 语句有语法错误或违反数据库模式的约束,系统会返回相应的错误信息。
    4. 编译、优化SQL 语句:解析后,DBMS 将 SQL 语句编译成一个可执行的查询计划 explain。这个计划是一个数据结构,描述了如何从数据库中获取或操作数据。在编译阶段,DBMS 可能会对查询计划进行优化,以提高执行效率。优化过程包括选择合适的索引、调整连接顺序等。
    5. 执行器:DBMS 根据优化后的查询计划执行 SQL 语句,去存储引擎层读取数据库中的数据。这包括从磁盘读取数据,使用索引加速查询,应用过滤条件等。
      执行器执行前,会检查mysql的innodb的buffer pool的缓存,,未命中要先查db。select直接查询buffer pool,dml(增删改)还要设计日志,,,
    6. 存储引擎:如 innodb、myisam、memory都会向上层提供查询接口。
    7. 返回结果: 执行完成后,DBMS 将结果返回给用户。结果可能是查询的结果集、执行成功的消息,或者在出现错误时的错误信息。
  • Mysql引擎 如何把硬盘上的数据查到?

    1. 解析 SQL 语句: MySQL 首先解析查询语句,检查语法和语义,确保查询是合法的。
    2. 查询优化: MySQL 会对查询进行优化,生成一个查询执行计划。
    3. 执行查询计划: MySQL 数据库引擎按照优化后的执行计划执行查询。这涉及从硬盘读取数据。
    4. 使用索引: 如果查询中使用了索引,并且优化器认为使用索引更有效,MySQL 将使用索引来快速定位和检索数据。索引通常存储在磁盘上,但在需要时会被加载到内存中,以提高查询速度。
      索引的使用发生在磁盘 I/O 操作之前。
    5. 读取数据块: 如果数据没有在内存中,MySQL 数据库引擎将从磁盘读取数据块(通常是页)到内存中。
      MySQL将数据以页(Page)为单位组织在磁盘上。一页通常包含多条记录,每页的大小是固定的。MySQL从磁盘上读取整个页,而不仅仅是所需的单个记录。读磁盘是一次 I/0 操作,MySQL 使用一种称为预读的技术,一次性读取多个相邻的数据块,以提高性能。
      首先,MySQL 发送读取请求到存储设备(硬盘),包括要读取的数据块的位置信息(例如磁盘上的扇区或页)以及读取的数量。磁盘根据请求移动磁头到指定的位置。这个过程称为磁盘寻道,寻道时间是磁盘 I/O 中的主要时间消耗部分。一旦磁头到达目标轨道,磁盘开始旋转,以便将所需的数据块转到磁头下方,磁盘开始传输数据到内存中。
    6. 缓存: MySQL 使用缓存来存储经常访问的数据块,这样在后续查询中可以更快地访问这些数据。这包括查询结果的缓存、索引缓存和数据缓存等。使用一个称为InnoDB Buffer Pool的缓存池来存储数据页。
    7. 返回结果: 当查询完成时,MySQL 将结果返回给用户。
  • 数据库存储引擎

    • 数据库存储引擎是数据库底层软件组织,数据库管理系统(DBMS)使用数据引擎进行创建、查询、更新和删除数据。不同的存储引擎提供不同的存储机制、索引技巧、锁定水平等功能,使用不同的存储引擎,还可以 获得特定的功能。现在许多不同的数据库管理系统都支持多种不同的数据引擎。
      存储引擎主要有: 1. MyIsam, 2. InnoDB, 3. Memory, 4. Archive, 5. Federated
    • InnoDB:底层存储结构为B+树, B树的每个节点对应innodb的一个page, page大小是固定的,一般设为 16k。其中非叶子节点只有键值,叶子节点包含完成数据、、适用场景:
      1)经常更新的表,适合处理多重并发的更新请求。
      2)支持事务。
      3)可以从灾难中恢复(通过 bin-log 日志等)。
      4)外键约束。只有他支持外键。
      5)支持自动增加列属性 auto_increment。
    • MyIASM:是 MySQL默认的引擎,但是它没有提供对数据库事务的支持,也不支持行级锁和外键,因此当 NSERT(插入)或 UPDATATE(更新)数据时即写操作需要锁定整个表,效率便会低一些。
      ISAM 执行读取操作的速度很快,而且不占用大量的内存和存储资源。在设计之初就预想数据组织成有固定长度的记录,按顺序存储的。 —ISAM 是一种静态索引结构。缺点是它不 支持事务处理。
    • Memery 就是将数据放在内存中,数据处理速度很快,但是安全性不⾼。
  • MyISAM 和 InnoDB 有什么区别?
    (1)MyISAM 只支持表级别的锁粒度,InnoDB 支持行级别的锁粒度。
    (2)MyISAM 不提供事务支持。InnoDB 提供事务支持,实现了 SQL 标准定义了四个隔离级别。
    (3)MyISAM 不支持外键,而 InnoDB 支持。
    (4)MyISAM 不支持 MVCC,而 InnoDB 支持。
    (5)虽然 MyISAM 引擎和 InnoDB 引擎都是使用 B+Tree 作为索引结构,但是两者的实现方式不太一样。
    (6)MyISAM 不支持数据库异常崩溃后的安全恢复,而 InnoDB 支持。
    (7)InnoDB 的性能比 MyISAM 更强大。

  • 索引、、
    在 MySQL 中,索引是一种特殊的数据结构,用于加快数据库表中数据的检索速度。索引在数据库表中的一个或多个列上创建,可以将这些列的值快速映射到实际数据的物理位置。
    索引(Index)是帮助 MySQL 高效获取数据的数据结构。 常见的查询算法,顺序查找,二分查找,二叉排序树查找,哈希散列法,分块查找,平衡多路搜索树 B 树(B-tree) ,索引是对数据库表中一个或多个列的值进行排序的结构,建立索引有助于快速获取信息。
    你也可以这样理解:索引就是加快检索表中数据的方法。数据库的索引类似于书籍的索引。在书籍中,索引允许用户不必翻阅完整个书就能迅速地找到所需要的信息。在数据库中,索引也允许数据库程序迅速地找到表中的数据,而不必扫描整个数据库 mysql。
    索引并非是越多越好,创建索引也需要耗费资源,一是增加了数据库的存储空间,二是在插入和删除时要花费较多的时间维护索引
    索引加快数据库的检索速度
    索引降低了插入、删除、修改等维护任务的速度
    唯一索引可以确保每一行数据的唯一性
    通过使用索引,可以在查询的过程中使用优化隐藏器,提高系统的性能
    索引需要占物理和数据空间

  • 索引有哪些种类。

    • 从数据结构维度进⾏分类:
      B+树索引:所有数据存储在叶⼦节点,复杂度为O(logn),适合范围查询。
      哈希索引:适合等值查询,检索效率⾼,⼀次到位
      全⽂索引: MyISAM 和 InnoDB 中都⽀持使⽤全⽂索引,⼀般在⽂本类型char,text,varchar 类型上创建
      R-Tree 索引:⽤来对 GIS 数据类型创建 SPATIAL 索引
    • 从物理存储维度进⾏分类:
      聚集索引:数据存储与索引⼀起存放,叶⼦节点会存储⼀整⾏记录,找到索引也就找到了数据。
      ⾮聚集索引:数据存储与索引分开存放,叶⼦节点不存储数据,存储的是数据⾏地址。
    • 从逻辑维度进⾏分类:
      主键索引:⼀种特殊的唯⼀索引,不允许有空值。
      普通索引:MySQL中基本索引类型,允许空值和重复值
      联合索引:多个字段创建的索引,使⽤时遵循最左前缀原则
      唯⼀索引:索引列中的值必须是唯⼀的,但是允许为空值
      空间索引:MySQL5.7之后⽀持空间索引,在空间索引这⽅⾯遵循OpenGIS⼏何数据模
  • 主键?聚集索引,非聚集索引?

    • 主键:主键是一种用于唯一标识表中每一行数据的列或列组合。主键列的值必须唯一且不为空(NULL)。在InnoDB中,主键索引是表的物理排序顺序,它是表的聚集索引。如果表没有显式定义主键,InnoDB会选择一个合适的唯一非空索引来充当主键索引。
    • 聚集索引:在MySQL中,聚集索引决定了数据在磁盘上的物理存储顺序,即数据的存储顺序与索引顺序一致。在InnoDB存储引擎中,主键索引就是一个聚集索引。如果表没有显式定义主键,则InnoDB会选择一个唯一非空的索引来充当聚集索引。
    • 非聚集索引:非聚集索引在磁盘上维护索引键和对应数据行的引用,索引键的顺序与实际数据行的物理存储顺序无关。在MySQL中,除了InnoDB存储引擎的聚集索引(主键索引),其他索引都是非聚集索引,例如普通索引或唯一索引。
  • InnoDB 建议为大部分表使用默认的自增主键的主要原因

    1. 聚簇索引: InnoDB 表的主键是聚簇索引(Clustered Index),这意味着数据行的物理顺序与聚簇索引的顺序一致。使用自增主键作为聚簇索引可以确保新插入的数据按顺序添加到表的末尾,减少数据页的分裂和碎片,提高数据的顺序性。
    2. 插入性能: 自增主键的顺序性有助于提高插入性能。因为数据行按主键的顺序插入,新的数据行往往直接添加到表末尾,而不会导致页面的分裂和数据的重新排序。
    3. 查询性能: 使用自增主键作为聚簇索引可以提高范围查询和排序查询的性能,因为相关数据在物理上是相邻存储的。
    4. 减少索引大小: 自增主键通常是整数,占用的空间相对较小。相比于使用其他类型的主键,这可以减少非聚簇索引的大小,提高缓存的效率,减少磁盘 I/O。
    5. 减少碎片: 自增主键的插入顺序有助于减少数据页的分裂和碎片,减小了表的维护成本。
  • 什么时候需要创建索引?
    表的主关键字:⾃动建⽴唯⼀索引
    直接条件查询的字段:经常⽤于WHERE查询条件的字段,这样能够提⾼整个表的查询速度
    查询中与其它表关联的字段:例如字段建⽴了外键关系
    查询中排序的字段:排序的字段如果通过索引去访问将⼤⼤提⾼排序速度
    唯⼀性约束列: 如果某列具有唯⼀性约束,那么为了确保数据的唯⼀性,可以在这些列上创建唯⼀索引。
    ⼤表中的关键列: 在⼤表中,如果查询的效率变得很低,可以考虑在关键列上创建索引。

  • 什么时候不需要创建索引?
    ⼩表: 对⼩表创建索引可能会带来额外的开销,因为在⼩数据集中扫描整个表可能⽐使⽤索引更快。
    频繁的插⼊、更新和删除操作: 索引的维护成本会随着数据的插⼊、更新和删除操作⽽增加。如果表经常被修改,过多的索引可能会影响性能。
    数据重复且分布平均的表字段:假如⼀个表有10万⾏记录,性别只有男和⼥两种值,且每个值的分布概率⼤约为50%,那么对这种字段建索引⼀般不会提⾼数据库的查询速度。
    很少被查询的列: 如果某列很少被⽤于查询条件,那么为它创建索引可能没有明显的性能提升。
    查询结果总⾏数较少的表: 如果查询的结果集总⾏数很少,使⽤索引可能不会有太⼤的性能提升。

  • 常见索引原则

    1. 选择唯一性索引,唯一性索引的值是唯一的,可以更快速的通过该索引来确定某条记录。
    2. 为经常需要排序、分组和联合操作的字段建立索引。
    3. 为常用作为查询条件的字段建立索引。
    4. 限制索引的数目:越多的索引,会使更新表变得很浪费时间。尽量使用数据量少的索引
    5. 如果索引的值很长,那么查询的速度会受到影响。尽量使用前缀来索引
    6. 如果索引字段的值很长,最好使用值的前缀来索引。
    7. 删除不再使用或者很少使用的索引
    8. 最左前缀匹配原则,非常重要的原则。
    9. 尽量选择区分度高的列作为索引区分度的公式是表示字段不重复的比例
    10. 索引列不能参与计算,保持列“干净”:带函数的查询不参与索引。
    11. 尽量的扩展索引,不要新建索引
  • 最左前缀匹配原则

    • 在使用复合索引(composite index)时,如果查询条件中使用了索引的第一个列作为条件,那么数据库可以利用这个索引来加速查询;但如果查询条件中只使用了索引的前几个列,而没有使用索引的第一个列作为条件,那么数据库无法利用这个索引来加速查询。
    • 具体来说,假设有一个复合索引 (col1, col2, col3),这个索引按照 col1col2col3 的顺序排列。根据最左前缀匹配原则,以下情况适用:
      1.当查询条件中包含了索引的第一个列 col1 时,数据库可以使用索引来加速查询。例如:
      1
      SELECT * FROM table_name WHERE col1 = 'value';
      2.当查询条件中包含了索引的前几个列 col1col2 时,数据库也可以使用索引来加速查询。例如:
      1
      SELECT * FROM table_name WHERE col1 = 'value1' AND col2 = 'value2';
      3.如果查询条件中包含了索引的所有列 col1col2col3 并且按照索引的顺序出现,那么可以触发索引。
      1
      SELECT * FROM table_name WHERE col1 = 'value1' AND col2 = 'value2' AND col3 = 'value3';
    • 但是,如果查询条件中只使用了索引的后几个列,而没有使用索引的第一个列作为条件,则无法利用这个索引加速查询。如:
      1
      SELECT * FROM table_name WHERE col2 = 'value2' AND col3 = 'value3';
  • 覆盖索引?回表查询?

    • 覆盖索引是指一个查询语句所需的数据可以从索引中直接获取,而无需访问表格中的实际数据行。这种情况下,索引“覆盖”了查询的所有需要的列。覆盖索引的优势在于可以减少磁盘 I/O 和内存的消耗,因为不需要额外的表格访问。
    • 回表查询指的是在使用索引的情况下,通过索引定位到主键,然后再根据主键的值去表中检索数据的过程。这通常发生在覆盖索引(Covering Index)无法满足查询需求时。
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      CREATE TABLE products (
      product_id INT PRIMARY KEY,
      product_name VARCHAR(50),
      price DECIMAL(10, 2),
      quantity INT
      );
      -- 为 product_name 列创建索引 idx_product_name
      CREATE INDEX idx_product_name ON products (product_name);
      -- 假设我们有一条数据
      INSERT INTO products (product_id, product_name, price, quantity) VALUES (1, 'Laptop', 999.99, 50);
      -- 现在,如果我们执行以下查询:
      SELECT product_name, price FROM products WHERE product_name = 'Laptop';
      -- 从索引中获取的列 product_name,但 price 列不在查询的列中,MySQL 将执行回表查询
      -- MySQL 首先使用索引 idx_product_name 找到匹配 Laptop 的行,获取到对应的 product_id(主键),然后根据 product_id 到表格中检索完整的行数据以获取 price 值
      -- 这种情况下,如果我们希望避免回表查询,可以考虑创建一个覆盖索引,将查询语句中需要的所有列都包含在索引中,便无需执行额外的回表查询
      CREATE INDEX idx_product_name_covering ON products (product_name, price);
  • B+ Tree(InnoDB)索引

    • 数据分块存储,每一块称为一页。所有的值都是按顺序存储的,并且每一个叶子到根的距离相同。
    • 非叶节点存储数据的边界,叶子节点存储指向数据行的指针。通过边界缩小数据的范围,从而避免全表扫描,加快了查找的速度。
    • B+ 树索引是一种索引结构,通常用于数据库管理系统中作为数据的索引方式。它可以用作聚集索引或非聚集索引,并不是严格意义上的主键索引。在数据库中,B+树索引在不同存储引擎下,例如在InnoDB中作为主键索引(聚集索引)使用,或作为其他索引(非聚集索引)的实现。
  • B树 与 B+树 的区别

    • B树:节点从小到大排序,一个节点可存多个元素
    • B+树:拥有B树的特点,叶子结点间有指针,叶子结点存储了所有的元素
    • B树和B+树,一般都是应用在文件系统和数据库系统中,用来减少磁盘IO带来的性能损耗。
      以Mysql中的InnoDB为例,当我们通过select语句去查询一条数据时,InnoDB需要从磁盘上去读取数据,这个过程会涉及到磁盘IO以及磁盘的随机IO(如图所示)我们知道磁盘IO的性能是特别低的,特别是随机磁盘IO。因为,磁盘IO的工作原理是,首先系统会把数据逻辑地址传给磁盘,磁盘控制电路按照寻址逻辑把逻辑地址翻译成物理地址,也就是确定要读取的数据在哪个磁道,哪个扇区。为了读取这个扇区的数据,需要把磁头放在这个扇区的上面,为了实现这一个点,磁盘会不断旋转,把目标扇区旋转到磁头下面,使得磁头找到对应的磁道,这里涉及到寻道事件以及旋转时间
      很明显,磁盘IO这个过程的性能开销是非常大的,特别是查询的数据量比较多的情况下。所以在InnoDB中,干脆对存储在磁盘块上的数据建立一个索引,然后把索引数据以及索引列对应的磁盘地址,以B+树的方式来存储。如图所示,当我们需要查询目标数据的时候,根据索引从B+树中查找目标数据即可,由于B+树分路较多,所以只需要较少次数的磁盘IO就能查找到。
  • 为什么 MySQL 的索引要使用 B+ 树而不是其它树形结构?

    • B 树是一种多路平衡树,用这种存储结构来存储大量数据,它的整个高度会相比二叉树来说,会矮很多。
      而对于数据库来说,所有的数据必然都是存储在磁盘上的,而磁盘 IO 的效率实际上是很低的,特别是在随机磁盘 IO 的情况下效率更低。所以树的高度能够决定磁盘 IO 的次数,磁盘 IO 次数越少,对于性能的提升就越大,这也是为什么采用 B 树作为索引存储结构的原因。
    • 对于 B 树,不管叶子节点还是非叶子节点,都会保存数据,这样导致在非叶子节点中能保存的指针数量变少(有些资料也称为扇出),指针少的情况下要保存大量数据,只能增加树的高度,导致 IO 操作变多,查询性能变低。在 Mysql 的 InnoDB 存储引擎里面用了一种增强的 B 树结构,也就是 B+树来作为索引和数据的存储结构。
    • 使用 B+树来实现索引的原因,我认为有几个方面。
      1. B+树非叶子节点不存储数据,所以每一层能够存储的索引数量会增加,意味着 B+树在层高相同的情况下存储的数据量要比 B 树要多,使得磁盘 IO 次数更少。
      2. 在 Mysql 里面,范围查询是一个比较常用的操作,而 B+树的所有存储在叶子节点的数据使用了双向链表来关联,所以在查询的时候只需查两个节点进行遍历就行,而 B 树需要获取所有节点,所以 B+树在范围查询上效率更高。
      3. 在数据检索方面,由于所有的数据都存储在叶子节点,所以 B+树的 IO 次数会更加稳定一些。
      4. 叶子节点存储所有数据,所以 B+树的全局扫描能力更强一些,它只需要扫描叶子节点。但是 B 树需要遍历整个树。
        另外,基于 B+树这样一种结构,如果采用自增的整型数据作为主键,还能更好的避免增加数据的时候,带来叶子节点分裂导致的大量运算的问题。
  • 为什么选择 B+Tree 而不是红黑树?

    • 红黑树等平衡树也可以用来实现索引,但是文件系统及数据库系统,普遍采用 B+ Tree作为索引结构这是因为其访问磁盘数据有更高的性能。我主要从两个点来回答
    • 第一点:对于一个数据库来说 存储的数据量会比较多,导致索引也很大 因此需要将索引存储在磁盘,但是磁盘的 IO 操作又非常耗,所以提高索引效率的关键在于减少磁盘 IO 的次数。相同节点个数 的 B+Tree 的高度更小,树的高度基本决定了磁盘的 IO 次数 ,所以使用 B+Tree 性能要高很多
    • 第二点:B+Tree 有个特点是相邻的数据在物理上也是相邻的,因为 B+Tree 的 node 的大小设为一个页,而一个节点上存有多个相邻的关键字和分支信息,每个节点只需要一次 IO就能完全载入,相当于一次 IO 载入了多个相邻的关键字和分支,而红黑树不具有这个特性,红黑树中大小相邻的数据,在物理结构上可能距离相差很大。由于程序的局部性原理,如果我们在索引中采用了预加载的技术,每次磁盘访问的时候除了将访问到的页加载到磁盘,我们还可以基于局部性原理加载,几页相邻的数据到内存中,而这个加载是不需要消耗多余磁盘 IO 时间的。
      因此 基于局部性原理,以及 B+Tree 存储结构物理上的特性,所以 B+Tree 的索引性能比红黑树要好很多。
  • 在数据库的表上建立了 (time DESC, name DESC) 的复合索引,什么情况下失效/生效?

    1. 索引生效的情况:
      • 查询条件涉及到两个字段: 当查询条件涉及到索引的两个字段时,复合索引可以生效。例如:查询条件包括了 timename,而且与索引的顺序一致,因此该复合索引可以被有效利用。
        1
        SELECT * FROM your_table WHERE time > '2022-01-01' AND name = 'John' ORDER BY time DESC, name DESC;
      • 查询条件只涉及到部分字段: 当查询条件只涉及到索引的一部分字段时,也可能会使用到该复合索引。例如:查询条件只包含了 time 字段,但由于 time 是索引的第一个字段,所以该索引可能仍然生效。
        1
        SELECT * FROM your_table WHERE time > '2022-01-01' ORDER BY time DESC, name DESC;
    2. 索引不生效的情况:
      • 查询条件没有使用到索引的前缀: 如果查询条件中没有使用到索引的前缀字段,那么复合索引可能不会被使用。例如:查询条件只包含了 name 字段,没用到 time 字段,因此 (time DESC, name DESC) 这个复合索引可能不被使用
        1
        SELECT * FROM your_table WHERE name = 'John' ORDER BY time DESC, name DESC;
      • 查询条件的顺序不符合索引的顺序: 复合索引的顺序很重要,如果查询条件的顺序与索引的顺序不一致,索引可能无法被有效利用。例如:虽然包含了索引的两个字段,但查询条件的顺序与索引的顺序不一致,因此索引可能不会被使用。
        1
        SELECT * FROM your_table WHERE name = 'John' AND time > '2022-01-01' ORDER BY time DESC, name DESC;
  • MySQL 索引失效的几种情况

    1. OR 语句前后没有同时使用索引。要想使用or,又想让索引生效,只能将or条件中的每个列都加上索引。
      1
      2
      3
      4
      <!-- 生效的情况 -->
      SELECT * FROM your_table WHERE time > '2022-01-01' OR name = 'John' ORDER BY time DESC, name DESC;
      <!-- 不生效的情况 -->
      SELECT * FROM your_table WHERE time > '2022-01-01' OR other_column = 'some_value' ORDER BY time DESC, name DESC;
    2. 复合索引未用左列字段。对于复合索引,如果不使用前列,后续列也将无法使用。
      1
      SELECT * FROM your_table WHERE name = 'John' ORDER BY time DESC, name DESC;
    3. like以%开头;模糊匹配
      1
      SELECT * FROM your_table WHERE name LIKE '%John' ORDER BY time DESC, name DESC;
    4. 需要类型转换。存在索引列的数据类型隐形转换,则用不上索引。
      1
      SELECT * FROM your_table WHERE CAST(time AS VARCHAR) = '2022-01-01' ORDER BY time DESC, name DESC;
    5. where中索引列有数学运算。
      1
      SELECT * FROM your_table WHERE time * 2 = '2022-01-01' ORDER BY time DESC, name DESC;
    6. where中索引列使用了函数。
      1
      SELECT * FROM your_table WHERE YEAR(time) = 2022 ORDER BY time DESC, name DESC;
    7. 如果mysql觉得全表扫描更快时(数据少)。
  • 联合索引(Composite Index)

    • 也称为复合索引,是指同时包含多个列的索引,它可以更加精确地定位数据,提高查询的效率。
      通常情况下,一个表中可能存在多个需要经常用于查询的列,使用联合索引可以将这些列组合起来,建立一个复合索引。在查询时,如果查询条件同时包含联合索引中的多个列,数据库可以直接使用索引定位到符合条件的行,避免了全表扫描,提高了查询效率。
      需要注意的是,在使用联合索引时,需要考虑索引的顺序。通常情况下,应该将最常用于查询的字段放在索引的前面,这样可以更加有效地利用索引。另外,联合索引也存在一些限制。由于索引是按照索引列的顺序建立的,因此只有在查询条件中包含索引的最左侧的列时,MySQL 才能利用这个索引。如果查询条件中包含的列不是索引的最左侧列,那么 MySQL 就无法使用这个索引。
      此外,由于联合索引包含多个列,因此其维护成本也相对较高。如果经常更新其中一个列的值,可能会导致索引的重建,影响数据库的性能。因此,在建立联合索引时,应该根据具体的应用场景,权衡利弊,避免滥用。
    • 如何使用联合索引?
      1、联合索引的最左前缀匹配指的是where条件一定要有联合索引的第一个字段
      2、是否走联合索引与where条件的顺序无关,只与字段有关
    • 联合索引的最左前缀匹配原则
      最左前缀匹配原则指的是,在使用联合索引时,MySQL 会根据联合索引中的字段顺序,从左到右依次到查询条件中去匹配,如果查询条件中存在与联合索引中最左侧字段相匹配的字段,则就会使用该字段过滤一批数据,直至联合索引中全部字段匹配完成,或者在执行过程中遇到范围查询(如 >、<)才会停止匹配。对于 >=、<=、BETWEEN、like 前缀匹配的范围查询,并不会停止匹配。所以,我们在使用联合索引时,可以将区分度高的字段放在最左边,这也可以过滤更多数据。
    • 联合索引的作用?
      1、减少io操作的开销和磁盘空间的开销;
      2、提升性能。索引列越多,通过索引筛选出的数据越少。
      3、覆盖索引。直接通过遍历索引取得数据,无需回表。
      提高查询效率:联合索引可以加速对多列数据的查询,对于联合索引中包含的列,可以同时使用它们进行筛选,减少了查询的数据量,提高了查询效率。
      减少磁盘IO:联合索引可以将多个列的数据存储在一起,减少了需要读取的磁盘块数,从而降低了IO的开销。
      优化排序操作:如果查询需要按照联合索引中的多个列进行排序,联合索引可以避免对多个独立索引的排序操作,从而提高排序操作的效率
  • MySQL 最左前缀索引的底层原理、、
    涉及到 B-Tree 索引结构以及 MySQL 的查询优化器。
    B-Tree 索引是一种常见的索引结构,用于快速检索数据库中的数据。在 B-Tree 索引中,数据按照键值有序存储,每个节点包含多个键值和对应的指针。MySQL 使用 B-Tree 索引来实现各种索引类型,包括最左前缀索引。
    当你创建一个多列索引时,MySQL 实际上会创建一个按照指定列顺序建立的 B-Tree 索引。这个索引会按照指定的列值顺序存储数据,因此在查询时,如果查询条件只涉及到索引的最左前缀列,MySQL 就可以利用这个索引来加速查询。
    ???

  • 简单描述 MySQL 中,索引,主键,唯一索引,联合索引的区别,对数据库的性能有什么影响?(从读写两方面)
    索引是一种特殊的文件(InnoDB 数据表上的索引是表空间的一个组成部分),它们包含着对数据表里所有记录的引用指针。
    普通索引(由关键字 KEY 或 INDEX 定义的索引)的唯一任务是加快对数据的访问速度。索引可以极大的提高数据的查询速度,但是会降低插入、删除、更新表的速度,因为在执行这些写操作时还要操作索引文件。普通索引允许被索引的数据列包含重复的值。
    如果能确定某个数据列将只包含彼此各不相同的值,在为这个数据列创建索引的时候就应该用关键字 UNIQUE 把它定义为一个唯一索引。唯一索引可以保证数据记录的唯一性。
    主键,是一种特殊的唯一索引,在一张表中只能定义一个主键索引,用于唯一标识一条记录,使用关键字 PRIMARY KEY 来创建。
    索引可以覆盖多个数据列,如像 INDEX(columnA, columnB)索引,这就是联合索引。

  • EXPLAIN 执行计划

    • 通过分析 EXPLAIN 返回的执行计划,可以了解查询的优化情况,如是否使用了索引、是否进行了全表扫描等。对于需要优化的查询,可以根据执行计划进行优化,例如添加适当的索引、调整查询语句等。
      1
      EXPLAIN SELECT * FROM customers WHERE city = 'New York';
    • 执行计划中的信息通常包括以下内容:
      • id:查询步骤的标识符。
      • select_type:查询类型,如 SIMPLE、PRIMARY、SUBQUERY 等。
      • table:查询涉及的表。
      • type:访问类型,如 ALL、INDEX、RANGE 等,表示数据库如何访问表中的数据。
      • possible_keys:可能用到的索引。
      • key:实际使用的索引。
      • rows:估计的返回行数。
      • Extra:其他信息,如使用了临时表、使用了文件排序等。
  • SQL 优化?

    • 优化查询语句、、
      1. 选择需要的列而不是使用 SELECT *
      2. 减少子查询,使用关联查询: 尽量使用关联查询(LEFT JOIN、RIGHT JOIN、INNER JOIN)替代子查询,以提高查询效率。使用 JOIN 操作来代替手动创建的临时表,提高查询效率。
      3. 避免使用 IN 或 NOT IN,使用 EXISTS 或关联查询: 以提高查询性能。
      4. 使用 UNION 或 UNION ALL 代替 OR 查询: 特别是当确定没有重复数据时,使用 UNION ALL 更为效率。
      5. 选择合适的字段属性: 尽可能减少定义字段宽度,设置合适的数据类型。尽量把字段设置 NOT NULL,例如’省份’、’性别’最好适用 ENUM
    • 基于索引查询、、
      1. 为常用的查询条件和连接条件创建索引,以加速查询速度。确保数据库引擎能够充分利用索引。
      2. 避免对索引列进行函数或运算: 尽量避免在索引列上使用函数或运算,以确保索引的有效使用。
      3. WHERE 子句中的 LIKE 使用 % 放在右边: 在使用 LIKE 操作时,尽量将 % 放在右边,以便索引的有效使用。
      4. 尽量使用 SQL 语句用到的索引完成排序: 避免使用文件排序的方式。
      5. 避免在 WHERE 子句中使用 != 或 <> 操作符: 以免引擎放弃使用索引而进行全表扫描。
      6. 避免在 WHERE 子句中对字段进行 NULL 值判断:可以设置默认值或使用其他方式,以免引擎放弃使用索引而进行全表扫描。如: select id from t where num is null 可以在num上设置默认值0,确保表中num列没有null值,然后这样查询:select id from t where num=0
    • 数据库事务、、
      1. 事务处理和锁定表优化: 合理使用事务处理,避免锁定表的情况,以提高数据库并发性能。
  • MySQL 优化?

    • 硬件和操作系统层面的优化
      硬件层面来说,影响 Mysql 性能的因素有,CPU、可用内存大小、磁盘读写速度、网络带宽从操作系层面来说,应用文件句柄数、操作系统网络的配置都会影响到 Mysql 性能。这部分的优化一般由 DBA 或者运维工程师去完成。在硬件基础资源的优化中,我们重点应该关注服务本身承载的体量,然后提出合理的指标要求,避免出现资源浪费!
    • 架构设计层面的优化
      MySQL 是一个磁盘 IO 访问量非常频繁的关系型数据库,在高并发和高性能的场景中承受巨大的并发压力,
      1、搭建 Mysql 主从集群,单个 Mysql 服务容易单点故障,一旦服务器宕机,将会导致依赖 Mysql 数据库的应用全部无法响应。 主从集群或者主主集群可以保证服务的高可用性。
      2、读写分离设计,在读多写少的场景中,通过读写分离的方案,可以避免读写冲突导致的性能影响??
      3、引入分库分表机制,通过分库可以降低单个服务器节点的 IO 压力,通过分表的方式可以降低单表数据量,从而提升 sql 查询的效率。
      4、针对热点数据,可以引入更为高效的分布式数据库,比如 Redis、MongoDB 等,他们可以很好的缓解 Mysql 的访问压力,同时还能提升数据检索性能。
    • MySQL 程序配置优化
      对于 Mysql 数据库本身的优化,一般是通过 Mysql 中的配置文件 my.cnf 来完成的,比如。
      Mysql5.7 版本默认的最大连接数是 151 个,这个值可以在 my.cnf 中修改。binlog 日志,默认是不开启。缓存池 bufferpoll 的默认大小配置等。
      由于这些配置一般都和用户安装的硬件环境以及使用场景有关系,因此这些配置官方只会提供一个默认值,具体情况还得由使用者来修改。
      关于配置项的修改,需要关注两个方面。1. 配置的作用域,分为会话级别和全局;2. 是否支持 热加载
      因此,针对这两个点,我们需要注意的是:1. 全局参数的设定对于已经存在的会话无法生效; 2. 会话参数的设定随着会话的销毁而失效; 3. 全局类的统一配置建议配置在默认配置文件中,否则重启服务会导致配置失效
    • 表结构和索引的优化
      主要可以下面这些方面去优化分库分表、读写分离、为字段选择合适的数据类型、适当的反范式设计,适当冗余设计、为查询操作创建必要的索引但是要避免索引滥用、尽可能使用 Not Null。
    • ??? SQL 优化
      第一、慢 SQL 的定位和排查我们可以通过慢查询日志和慢查询日志分析工具得到有问题的 SQL 列表。
      第二、执行计划分析针对慢 SQL,我们可以使用关键字 explain 来查看当前 sql 的执行计划.可以重点关注type key rows filterd 等字段 ,从而定位该 SQL 执行慢的根本原因。再有的放矢的进行优化
      第三、使用 show profile 工具Show Profile 是 MySQL 提供的可以用来分析当前会话中,SQL 语句资源消耗情况的工具,可用于 SQL 调优的测量。在当前会话中.默认情况下处于 show profile 是关闭状态,打开之后保存最近 15 次的运行结果。针对运行慢的 SQL,通过 profile 工具进行详细分析.可以得到 SQL 执行过程中所有的资源开销情况. 如 IO 开销,CPU 开销,内存开销等.
  • 什么是内联接、左外联接、右外联接、全连接?
    内联接(Inner Join):匹配2张表中相关联的记录。
    左外联接(Left Outer Join):除了匹配2张表中相关联的记录,还会匹配左表中剩余的记录,右表中未匹配到的字段用NULL表示。
    右外联接(Right Outer Join):除了匹配2张表中相关联的记录,还会匹配右表中剩余的记录,左表未匹配到的字段用NULL表示。
    全连接(Full Join 或 Full Outer Join):在使用全连接时,无论左表和右表是否存在匹配,都会返回两个表中所有的行,并在没有匹配的行处填充 NULL 值。

  • drop、delete与truncate的区别
    SQL中的drop、delete、truncate都表示删除,但是三者有一些差别
    delete 和 truncate 只删除表的数据不删除表的结构速度,一般来说: drop> truncate >delete
    delete 语句是 dml,这个操作会放到rollback segement中,事务提交之后才生效;如果有相应的trigger,执行的时候将被触发.
    truncate,drop是 ddl, 操作立即生效,原数据不放到rollbacksegment中,不能回滚. 操作不触发trigger

  • 部分依赖和传递依赖。

    1. 部分依赖:发生在一个表的主键(Primary Key)中包含了多个字段,但是其中的某些字段并不完全依赖于主键的所有部分。简言之,某个非主键字段只依赖于主键的一部分。
      举例:考虑一个订单表,主键是订单号和商品编号(OrderID, ProductID),如果订单表中还有商品名称(ProductName),而商品名称仅依赖于商品编号而不依赖于订单号,那么商品名称对于订单表来说就是一个部分依赖。
    2. 传递依赖:发生在一个非主键字段依赖于其他非主键字段,而这些其他非主键字段又依赖于主键。简言之,某个非主键字段依赖于其他非主键字段,而这些其他字段依赖于主键。
      举例:考虑员工表,主键是员工编号(EmployeeID),表中包含了部门编号(DepartmentID))和部门名称(DepartmentName)。如果部门名称依赖于部门编号,而部门编号又依赖于员工编号,那么部门名称对于员工表来说就是一个传递依赖。
  • 范式:解决部分依赖和传递依赖的一种方法是通过数据库范式化,通常是将表拆分成更小的表。

    • 范式理论是为了解决四种异常。不符合范式的关系(表的属性的组合),会产生很多异常:1、冗余数据。2、修改异常: 修改了一个记录中的信息,但是另一个记录中相同的信息却没有被修改。3、删除异常: 删除一个信息,那么也会丢失其它信息。4、插入异常: 例如想要插入一个学生的信息,如果这个学生还没选课,那么就无法插入。
    • 高级别范式的依赖于低级别的范式,1NF 是最低级别的范式。
      1. 第一范式 (1NF):属性不可分。
      2. 第二范式 (2NF):每个非主属性完全函数依赖于键码。可以通过分解来满足。(一张表分解成多张表)
      3. 第三范式 (3NF):非主属性不传递函数依赖于键码。可以进行分解。
    • 如何通俗地理解三个范式?
      第一范式:1NF 是对属性的原子性约束,要求属性具有原子性,不可再分解;
      第二范式:2NF 是对记录的惟一性约束,要求记录有惟一标识,即实体的惟一性;
      第三范式:3NF 是对字段冗余性的约束,即任何字段不能由其他字段派生出来,它要求字段没有冗余。
  • 什么是基本表?什么是视图?游标是什么?
    基本表是本身独立存在的表,在 SQL 中一个关系就对应一个表。
    视图本身不独立存储在数据库中,是一种虚拟的表,具有和物理表相同的功能。可以对视图进行增,改,查,操作,视图是从一个或几个基本表导出的表,视图通常是有一个表或者多个表的行或列的子集。对视图的修改不影响基本表。它使得我们获取数据更容易,相比多表查询。
    游标:是对查询出来的结果集作为一个单元来有效的处理。游标可以定在该单元中的特定行,从结果集的当前行检索一行或多行。可以对结果集当前行做修改。一般不使用游标,但是需要逐条处理数据的时候,游标显得十分重要。

  • 事务、、

    • 事务(Transaction)是一组数据库操作,它们形成一个逻辑工作单元,要么全部成功执行,要么全部失败回滚。事务的目的是保证数据库的一致性和完整性,确保在多个操作中要么全部成功,要么全部失败,不会留下中间状态。
    • 事务的特性:通常被称为 ACID 特性
      • 原子性(Atomicity) 事务是一个完整的操作。事务的各步操作是不可分的(原子的);要么都执行,要么都不执行。
      • 一致性(Consistency)当事务完成时,数据必须处于一致状态。
      • 隔离性(Isolation) 对数据进行修改的所有并发事务是彼此隔离的, 这表明事务必须是独立的,它不应以任何方式依赖于或影响其他事务。
      • 永久性(Durability) 事务完成后,它对数据库的修改被永久保持,事务日志能够保持事务的永久性
    • 事务的隔离性
      • 并发异常:第一类丢失更新、第二类丢失更新脏读、不可重复读、幻读
      • 隔离级别:Read Uncommitted、Read Conmitted、Repeatable Read、Serializable
  • 并发事务带来哪些问题?
    在典型的应用程序中,多个事务并发运行,经常会操作相同的数据来完成各自的任务(多个用户对同一数据进行操作)。并发虽然是必须的,但可能会导致以下的问题。

    • 脏读(Dirty read): 当一个事务正在访问数据并且对数据进行了修改,而这种修改还没有提交到数据库中,这时另外一个事务也访问了这个数据,然后使用了这个数据。因为这个数据是还没有提交的数据,那么另外一个事务读到的这个数据是“脏数据”,依据“脏数据”所做的操作可能是不正确的。
    • 丢失修改(Lost to modify): 指在一个事务读取一个数据时,另外一个事务也访问了该数据,那么在第一个事务中修改了这个数据后,第二个事务也修改了这个数据。这样第一个事务内的修改结果就被丢失,因此称为丢失修。 例如:事务1读取某表中的数据A=20,事务2也读取A=20,事务1修改A=A-1,事务2也修改A=A-1,最终结果A=19,事务1的修改被丢失。
    • 不可重复读(Unrepeatableread): 指在一个事务内多次读同一数据。在这个事务还没有结束时,另一个事务也访问该数据。那么,在第一个事务中的两次读数据之间,由于第二个事务的修改导致第一个事务两次读取的数据可能不太一样。这就发生了在一个事务内两次读到的数据是不一样的情况,因此称为不可重复读。
    • 幻读(Phantom read): 幻读与不可重复读类似。它发生在一个事务(T1)读取了几行数据,接着另一个并发事务(T2)插入了一些数据时。在随后的查询中,第一个事务(T1)就会发现多了一些原本不存在的记录,就好像发生了幻觉一样,所以称为幻读。不可重复读和幻读区别:不可重复读的重点是修改比如多次读取一条记录发现其中某些列的值被修改,幻读的重点在于新增或者删除比如多次读取一条记录发现记录增多或减少了
  • SQL 标准定义的四个事务隔离级别有哪些?

    • READ-UNCOMMITTED(读取未提交): 最低的隔离级别,允许读取尚未提交的数据变更,可能导致脏读、幻读或不可重复读。
    • READ-COMMITTED(读取已提交): 允许读取并发事务已经提交的数据,可以阻止脏读,但是幻读或不可重复读仍可能发生。
    • REPEATATATABLE-READ(可重复读): 对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改,可以阻止脏读和不可重复读,但幻读仍可能发生(特点情况下)、、InnoDB 存储引擎的默认支持的隔离级别是 【可重复读】
    • SERIALIZABLE(可串行化):最高的隔离级别,完全服从ACID的隔离级别。所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,该级别可以防止脏读、不可重复读以及幻读
  • MySQL的事务隔离级别与锁的关系、、

    • MySQL 中的事务隔离级别通过不同类型的锁(共享锁、排它锁等)以及锁的粒度(行级锁、表级锁等)来实现对事务的隔离,保证了事务在并发环境下的正确执行。选择合适的事务隔离级别需要根据具体业务需求和性能要求进行权衡。
      1. 读未提交(Read Uncommitted):不涉及锁的机制,允许事务读取其他事务未提交的数据,可能会出现脏读。
      2. 读提交(Read Committed):使用共享锁(Shared Locks)和瞬时的排他锁(Exclusive Locks)来实现;
        • 当一个事务读取数据时,会对数据进行共享锁定,这样其他事务可以读取相同的数据,但不能修改。
        • 当一个事务修改数据时,会对数据进行排他锁定,这样其他事务不能读取也不能修改相同的数据,直到该事务提交或回滚。
      3. 可重复读(Repeatable Read):使用共享锁(Shared Locks)和持久的排他锁(Exclusive Locks)来实现;
        • 当一个事务读取数据时,会对数据进行共享锁定,这样其他事务可以读取相同的数据,但不能修改。
        • 当一个事务修改数据时,会对数据进行持久的排他锁定,直到该事务提交或回滚,其他事务都不能读取或修改相同的数据。
      4. 串行化(Serializable)
        • MySQL 会对读取的数据行和写入的数据行都进行(行级)排它锁,确保事务串行执行,避免了幻读的问题。
          1
          2
          3
          4
          5
          START TRANSACTION;
          -- 对某个数据进行读取时,其他事务无法对该数据进行写入或者读取的操作,直到该事务完成读取操作
          SELECT * FROM table_name WHERE condition FOR UPDATE;
          -- 在这里进行读取和处理操作
          COMMIT;
    • 隔离级别越低,锁机制越不严格,事务请求的锁越少,并发性越高,性能开销也越小
    • InnoDB 默认支持的隔离级别是REPEATATATABLE-READ(可重读),在 分布式事务 的情况下一般会用到 SERIALIZABLE(可串行化) 隔离级别。
  • MySQL 支持事务吗?
    在缺省模式下,MySQL 是 autocommit 模式的,所有的数据库更新操作都会即时提交,所以 MySQL 默认不支持事务。
    但是如果你的 MySQL 表类型是使用 InnoDB TaTables 或 BDB tables 的话,你的MySQL 就可以使用事务处理,在非autocommit 模式下,你必须使用 COMMIT 来提交你的更改,或者用 ROLLBACK来回滚你的更改。

  • ???Innodb是如何实现事务的
    Innodb通过BufferPool,LogBuffer,Redo Log,undoLog来实现事务,以一个update语句为例:
    1.Innodb在收到一个update语句后,会先根据条件找到数据所在的页,并将该页缓存在BufferPool中
    2.执行update语句,修改BufferPool中的数据,也就是内存中的数据
    3.针对update语句生成一个Redo Log对象,并存入LogBuffer中
    4.针对update语句生成undolog日志,用于事务回滚
    5.如果事务提交,那么则把Redo Log对象进行持久化,后续还有其他机制将BufferPool中所修改的数据页持久化到磁盘中
    6.如果事务回滚,则利用undolog日志进行回滚

  • innoDB 如何解决幻读?
    1、Mysql 有四种事务隔离级别,其中 InnoDB 在 RR(可重复读)的隔离级别下,解决了幻读的问题(在特定的情况下会出现幻读的问题。具体什么情况下会出现幻读呢???
    2、幻读是指在同一个事务中,前后两次查询相同的范围时,得到的结果不一致
    3、InnoDB 引入了间隙锁和 next-key Lock 机制来解决幻读问题??

  • MySQL 单表为什么不要超过 2千万条 时最优?

    • 查询性能:随着数据量的增加,查询可能变得更慢,特别是在没有合适索引支持的情况下。大表需要更多的计算资源和时间来处理查询,可能导致性能下降。
    • 索引和缓存:维护大表的索引和缓存可能会变得更加困难。索引的大小随着数据量的增加而增加,这可能导致索引扫描变慢,同时也会增加对内存的需求。
      1、一个高度为 3 的 B+ 树可以存放: 1170X1170X16=21902400 条这样的记录,即2千万多些,通过主键查询一条数据,只需要3次磁盘IO访问,当超出2千万条时,索引树高度为4。
      2、mysql都有缓存,树高度为3时,第一层和第二层的数据都在缓存,高度为3查询效率很快,但是超过高度4时,查询效率就急速下降了。
    • 锁和并发:在执行更新或删除操作时,数据库可能需要对表进行锁定,以确保数据的一致性。大表的锁定可能会阻塞其他操作,影响并发性能。
  • 分表、、

    • 分表是一种应对大表数据量的常见方法。通过将大表拆分为多个小表(分区),可以减轻数据库管理系统的负担,提高查询性能和管理效率。分表可以根据业务逻辑或特定的列值进行拆分,例如按时间范围、地理区域等方式进行分区。
    • 在考虑分表之前,建议进行以下操作:
      优化查询和索引:确保数据库表有适当的索引来支持常见的查询,并优化查询语句以提高性能。
      限定数据的范围:务必禁止不带任何限制数据范围条件的查询语句。比如:我们当用户在查询订单历史的时候,我们可以控制在一个月的范围内;
      ???读/写分离:经典的数据库拆分方案,主库负责写,从库负责读;
      垂直和水平分割:考虑将大表进行垂直切分(按列拆分)或水平切分(按行拆分)以减少单个表的数据量。
      使用分区:针对数据库支持的分区功能,可以考虑根据特定的标准将表分成多个逻辑分区。
      数据库优化:定期清理无用数据、重新构建索引、优化数据库配置等,以提高数据库的整体性能。
  • 分库分表之后,id 主键如何处理???
    因为要是分成多个表之后,每个表都是从 1 开始累加,这样是不对的,我们需要一个全局唯一的 id 来支持。生成全局 id 有下面这几种方式:
    UUID:不适合作为主键,因为太长了并且无序不可读,查询效率低。比较适合用于生成唯一的名字的标示比如文件的名字。
    数据库自增 id : 两台数据库分别设置不同步长,生成不重复ID的策略来实现高可用。这种方式生成的 id 有序,但是需要独立部署数据库实例,成本高,还会有性能瓶颈。
    利用 redis 生成 id : 性能比较好,灵活方便,不依赖于数据库。但是,引入了新的组件造成系统更加复杂,可用性降低,编码更加复杂,增加了系统成本。

Mysql锁、、

  • 什么是锁?
    答:数据库是一个多用户使用的共享资源。当多个用户并发地存取数据时,在数据库中就会产生多个事务同时存取同一数据的情况。若对并发操作不加控制就可能会读取和存储不正确的数据,破坏数据库的一致性。加锁是实现数据库并发控制的一个非常重要的技术。当事务在对某个数据对象进行操作前,先向系统发出请求,对其加锁。加锁后事务就对该数据对象有了一定的控制,在该事务释放锁之前,其他的事务不能对此数据对象进行更新操作。

  • 锁的类型(按类型)

    • 悲观锁:悲观锁就是在读取数据的时候,为了不让别人修改自己读取的数据,就会先对自己读取的数据加锁,只有自己把数据读完了,才允许别人修改那部分数据,或者反过来说,就是自己修改某条数据的时候,不允许别人读取该数据,只有的整个事务提交了,才释放自己加上的锁,允许其他用户访问那部分数据。
      • 悲观锁所说的加“锁”,其实分为几种锁,分别是: 排它锁(写锁)和共享锁(读锁) 。
    • 乐观锁:乐观锁认为一个用户读数据的时候,别人不会去写自己所读的数据;悲观锁就刚好相反,觉得自己读数据库的时候,别人可能刚好在写自己刚读的数据,其实就是持一种比较保守的态度;
      • 时间戳:时间戳就是不加锁,通过时间戳来控制并发出现的问题。在数据库表中单独加一列时间戳,比如“TimeStamp”, 每次读出来的时候,把该字段也读出来,当写回去的时候,把该字段加1,提交之前 ,跟数据库的该字段比较一次,如果比数据库的值大的话,就允许保存,否则不允许保存,这种处理方法虽然不使用数据库系统提供的锁机制,但是这种方法可以大大提高数据库处理的并发量
  • 锁的类型(按粒度)

    1. 全局锁:锁定数据库中所有的表(数据备份)
    2. 表级锁:对当前操作的整张表加锁,它实现简单,开销小、加锁快,发生锁冲突的概率高、并发度低,不会出现死锁;被大部分 MySQL 引擎支持。最常使用的 MYISAM 与 INNODB 都支持表级锁定。表级锁定分为表共享读锁(共享锁)与表独占写锁(排他锁)。
      • 表锁:锁住整张表,粒度较大
      • 元数据锁:防止 DML 和 DDL 冲突
      • 意向锁:避免加锁是一行一行查看行锁加锁情况,解决上述低效的问题
        • 意向共享锁 (IS): 表级,准备加共享锁
        • 意向排他锁 (Ix) : 表级,准备加排他锁
    3. 行级锁:开销大、加锁慢,发生锁冲突的概率低、并发度高,会出现死锁
      • 行级锁是一种排他锁,防止其他事务修改此行;在使用以下语句时, Oracle 会自动应用行级锁:
        1. INSERT、 UPDATATE、 DELETE、 SELECT … FOR UPDATATE [OF columns] [WAWAIT n | NOWAWAIT];
        2. SELECT … FOR UPDATATE 语句允许用户一次锁定多条记录进行更新
        3. 使用 COMMIT 或 ROLLBACK 语句释放锁。
      • 共享锁 (s) : 行级,读取一行
      • 排他锁 (x) : 行级,更新一行
      • 间隙锁 (NK) : 行级,使用范围条件时
  • 加锁、、

    • 对范围内不存在的记录加锁。一是为了防止幻读,二是为了满足恢复和复制的需要
    • 增加行级锁之前,InnoDB会自动给表加意向锁
    • 执行DML语句(update、delete和insert)时,InnoDB会自动给数据加排他锁
    • 执行DQL语句时:
      • 通常情况下,单纯的select不会加锁,因为InnoDB默认的隔离级别是可重复读,使用MVCC来避免加锁
      • 共享锁 (s) :SELECT … FROM … WHERE … LOCK IN SHARE MODE
      • 排他锁 (x) :SELECT … FROM … WHERE … FOR UPDATE;
      • 间隙锁 (NK) : 上述sQ采用范围条件时,InnoDB对不存在的记录自动增加间隙锁
  • 数据库并发策略
    并发控制一般采用三种方法,分别是乐观锁和悲观锁以及时间戳。

  • ???锁的优化策略
    1、读写分离
    2、分段加锁
    3、减少锁持有的时间
    4.多个线程尽量以相同的顺序去获取资源不能将锁的粒度过于细化,不然可能会出现线程的加锁和释放次数过多,反而效率不如一次加一把大锁。

  • 存储过程。什么是存储过程?用什么来调用?
    一组为了完成特定功能的 SQL 语句集,存储在数据库中,经过第一次编译后再次调用不需要再次编译,用户通过指定存储过程的名字并给出参数(如果该存储过程带有参数)来执行它。如果某次操作需要执行多次 SQL,使用存储过程比单纯 SQL 语句执行要快。存储过程是数据库中的一个重要对象。

  • 存储过程优化思路

    1. 尽量利用一些 sql 语句来替代一些小循环,例如聚合函数,求平均函数等。
    2. 中间结果存放于临时表,加索引。
    3. 少用游标。sql 是个集合语言,对于集合运算具有较高性能。而 cursors 是过程运算。比如对一个 100 万行的数据进行查询。游标需要读表 100 万次,而不使用游标则只需要少量几次读取。
    4. 事务越短越好。 sqlserver 支持并发操作。如果事务过多过长,或者隔离级别过高,都会造成并发操作的阻塞,死锁。导致查询极慢,cpu 占用率极地。
    5. 使用 try-catch 处理错误异常。6. 查找语句尽量不要放在循环内
  • SQL 注入、、

    • 产生的原因:程序开发过程中不注意规范书写 sql 语句和对特殊字符进行过滤,导致客户端可以通过全局变量 POST 和 GET 提交一些 sql 语句正常执行。
    • 类型:1、恶意拼接查询 2、利用注释执行非法命令 3、传入非法参数 4、添加额外条件
    • 如何避免 SQL 注入?
      1、过滤输入内容,校验字符串:在数据提交到数据库之前,就把用户输入中的不合法字符剔除掉。
      2、参数化查询:参数化查询目前被视作是预防 SQL 注入攻击最有效的方法。指在设计与数据库连接并访问数据时,在需要填入数值或数据的地方,使用参数(Parameter)来给值。
  • MVCC 机制、、???

    • MVCC(Multi-Version Concurrency Control)多版本并发控制,⽤于管理多个事务同时访问和修改数据库的数据,⽽不会导致数据不⼀致或冲突。MVCC的核⼼思想是每个事务在数据库中看到的数据版本是事务开始时的⼀个快照,⽽不是实际的最新版本。这使得多个事务可以并发执⾏,⽽不会互相⼲扰。
      MySQL的隔离性可以通过锁和MVCC来实现,MVCC适合在⼀些锁性能较为差的情况下使⽤,提⾼效率。
    • 如何实现:每⼀个 UndoLog ⽇志中都有⼀个 roll_pointer (回滚指针)⽤于指向上⼀个版本的 Undo Log 。这样对于每⼀条记录就会构成⼀个版本链,⽤于记录所有的修改,每⼀次进⾏新的修改后,新的 Undo Log 会放在版本链的头部。
    • 在我们进⾏查询的时候应该查询哪个版本呢?这时候就可以通过 ReadView 来实现。在事务SELECT查询数据时,就会构造⼀个 ReadView ,它包含了版本链的统计信息:
      m_ids 当前活跃的所有事务id(所有未提交的事务)
      min_trx_id 版本链尾的id
      max_trx_id 下⼀个将要分配的事务id(版本链头事务id+1)
      creator_trx_id 创建这个ReadView的事务的id 查询规则:
      该版本是否为当前事务创建(读取⾃⼰修改的数据),如果是就返回,否则进⼊下⼀个判断
      该版本的事务id是否⼩于min_trx_id(在ReadView创建之前,数据已经提交),可以直接访问
      该版本的事务id是否⼤于max_trx_id(在ReadView创建后,该版本才开启),不能被访问
      该版本事务id在[min_trx_id, max_trx_id]之间,则判断当前版本事务id是否在m_ids中,如果不在,说明事务已经提交可以访问,否则不能访问。
    • 对 MVCC 的理解。
      • 对于 MVCC 的理解,我觉得可以先从数据库的三种并发场景说起:
        第一种:读读就是线程 A 与线程 B 同时在进行读操作,这种情况下不会出现任何并发问题。
        第二种:读写就是线程 A 与线程 B 在同一时刻分别进行读和写操作。这种情况下,可能会对数据库中的数据造成以下问题:事物隔离性问题,出现脏读,幻读,不可重复读的问题
        第三种:写写就是线程 A 与线程 B 同时进行写操作。这种情况下可能会存在数据更新丢失的问题。而 MVCC 就是为了解决事务操作中并发安全性问题的无锁并发控制技术全称为Multi-Version Concurrency Control ,也就是多版本并发控制。它是通过数据库记录中的隐式字段,undo 日志 ,Read View 来实现的。
      • MVCC 主要解决了三个问题
        第一个:通过 MVCC 可以解决读写并发阻塞问题从而提升数据并发处理能力
        第二个:MVCC 采用了乐观锁的方式实现,降低了死锁的概率
        第三个:解决了一致性读的问题。也就是事务启动时根据某个条件读取到的数据,直到事务结束时,再次执行相同条件,还是读到同一份数据,不会发生变化。而我们在使用 MVCC 时一般会根据业务场景来选择组合搭配乐观锁或悲观锁。这两个组合中,MVCC 用来解决读写冲突,乐观锁或者悲观锁解决写写冲突从而最大程度的提高数据库并发性能。以上就是我的对 MVCC 的理解。
  • Mysql日志

    • binlog(归档⽇志) 是 Server 层⽣成的⽇志,主要⽤于数据备份(宕机后的恢复工作)和主从复制, 解决数据库和缓存之间一致性可以用canal??组件去监听binlog
    • redolog 是 Innodb 物理⽇志,记录了某个数据⻚做了什么修改,每当执⾏⼀个事务就会产⽣⼀条或者多条物理⽇志。
      如果发送宕机,对读取到内存中的bufferpool中的数据没有同步到硬盘mysql中,使用redolog来同步,保证事务的一致性。
    • undolog 是 Innodb 逻辑⽇志,用于记录数据修改前的信息(记录对数据库操作的逆操作,事务回滚时逆操作恢复原数据)比如我们删除一条数据的时候,就会在undolog日志文件中新增一条delete语句; 实现了事务中的原⼦性,主要⽤于事务回滚和MVCC。
    • relaylog 中继⽇志,⽤于主从复制场景下, slave 通过io线程拷⻉master的 binlog 后本地⽣成的⽇志
  • 当MySQL的日志空间不足时,可能会导致以下影响:

    1. 写入操作: 日志(例如二进制日志和事务日志)是用来记录数据库的写入操作的,如果日志空间不足,数据库可能无法正常写入事务日志,导致事务无法提交。这可能会导致写入操作被阻塞或失败。
    2. 查询操作: 查询操作通常不直接受到日志空间不足的影响。然而,如果写入操作因为日志空间不足而受阻,可能会导致查询操作因为等待事务完成而变慢。
    3. 备份操作: 备份通常会涉及到数据库的日志文件。如果日志空间不足,可能会影响备份的正常执行。备份通常需要确保数据库的一致性,而事务日志在这个过程中是至关重要的。
  • MySQL 数据库作发布系统的存储,一天五万条以上的增量,预计运维三年,怎么优化?
    1、设计良好的数据库结构,允许部分数据冗余,尽量避免 join 查询,提高效率。
    2、选择合适的表字段数据类型和存储引擎,适当的添加索引。
    3、MySQL 库主从读写分离。
    4、找规律分表,减少单表中的数据量提高查询速度。
    5、添加缓存机制,比如 memcached,apc 等。
    6、不经常改动的页面,生成静态页面。
    7、书写高效率的 SQL。比如 SELECT * FROM TATABEL 改为 SELECT field_1,field_2, field_3 FROM TATABLE.

  • 数据库如何处理大数据量?

    • 分区:一份数据文件拆分多个磁盘文件存储,隔离数据访问。相当于做了负载均衡。
    • 水平分库/表,各个库和表的结构一模一样。
      垂直分库/表,各个库和表的结构不一样。
    • 读写分离:主机负责写,从机负责读。
  • MySQL主从、、

    • 主是主库的意思,从是从库的意思。数据库主库对外提供读写的操作,从库对外提供读的操作。
    • 数据库为什么需要主从架构呢?
      • 高可用,实时灾备,用于故障切换。比如主库挂了,可以切从库。
      • 读写分离,提供查询服务,减少主库压力,提升性能
      • 备份数据,避免影响业务。
    • 主从复制原理,简言之,分三步曲进行:
      • 主数据库有个 bin 1og 二进制文件,纪录了所有增删改 sQL语句。(binlog线程)
      • 从数据库把主数据库的 bin 1og 文件的 sQL 语句复制到自己的中继日志 relay 1og (io线程)
      • 从数据库的 relay 1og 重做日志文件,再执行一次这些sql语句。(Sql执行线程)
    • 怎么保证主从一致?
      我们学习数据库的主从复制原理后,了解到从库拿到并执行主库的binlog日志,就可以保持数据与主库一致了。这是为什么呢?哪些情况会导致不一致呢?
      • 1 长链接
        主库和从库在同步数据的过程中断怎么办呢,数据不就会丢失了嘛。因此主库与从库之间维持了一个长链接,主库内部有一个线程,专门服务于从库的这个长链接的。
      • 2 bintog格式
        binlog 日志有三种格式,分别是statement,row和mixed。
        如果是 statement 格式,binlog记录的是SQL的原文,如果主库和从库选的索引不一致,可能会导致主库不一致。
        如何解决这个问题呢?可以把binlog格式修改为 row。row 格式的 binlog 日志,记录的不是SQL原文,而是两个 event:Table_map 和 Delete_rows 。Table_map event说明要操作的表,Delete_rows event用于定义要删除的行为:记录删除的具体行数。row 格式的binlog记录的就是要删除的主键ID信息,因此不会出现主从不一致的问题。
        但是如果SQL删除10万行数据,使用row格式就会很占空间的,10万条数据都在binlog里面写binlog的时候也很耗I0。但是 statement 格式的binlog可能会导致数据不一致,因此设计MySQL的大叔想了一个折中的方案, mixed 格式的binlog。所谓的mixed格式其实就是 row 和statement 格式混合使用,当MySQL判断可能数据不一致时,就用row 格式,否则使用就用statement 格式。
    • 主从延迟、、
    • 高可用方案
      • 双机主备:两台机器A和B,A为主库,负责读写,B为备库,只备份数据。如果A库发生故障,B库成为主库负责读写。修复故障后,A成为备库,主库B同步数据到备库A
      • 一主一从:两台机器A和B,A为主库,负责读写,B为从库,负责读数据。如果A库发生故障,B库成为主库负责读写。修复故障后,A成为从库,主库B同步数据到从库A
      • 一主多从:多个从库支持读,分担了主库的压力,明显提升了读的并发度。但只有台主机写,因此写的并发度不高
      • MariaDB同步多主机:有代理层实现负载均衡,多个数据库可以同时进行读写操作;各个数据库之间可以通过 Galera Replication 方法进行数据同步,每个库理论上数据是完全一致的。数据库不支持过大。
      • 数据库中间件:mycat分片存储,每个分片配置一主多从的集群。优点:解决高并发高数据量的高可用方案;缺点:维护成本比较大。

MyBatis

  • 什么是 Mybatis?
    1、Mybatis 是一个开源的Java持久层框架,半 ORM(对象关系映射)框架,它内部封装了 JDBC,开发时只需要关注 SQL 语句本身,不需要花费精力去处理加载驱动、创建连接、创建statement 等繁杂的过程。程序员直接编写原生态 sql,可以严格控制 sql 执行性能,灵活度高。(JDBC(Java Database Connectivity)是 Java 语言用于与关系型数据库进行交互的一种标准接口。JDBC 提供了一组 Java API,允许开发者通过 Java 代码来执行 SQL 查询、更新数据库、以及处理数据库事务等操作。)
    2、MyBatis 可以使用 XML 或注解来配置和映射原生信息,将 POJO 映射成数据库中的记录,避免了几乎所有的 JDBC 代码和手动设置参数以及获取结果集。
    3、通过 xml 文件或注解的方式将要执行的各种 statement 配置起来,并通过 java 对象和 statement 中 sql 的动态参数进行映射生成最终执行的 sql 语句,最后由 mybatis 框架执行 sql 并将结果映射为 java 对象并返回。(从执行 sql 到返回 result 的过程)。

  • ORM,DAO,持久层?
    ORM 和 DAO 是持久层的实现方式,ORM 和 DAO 通常一起协同工作,以实现数据的持久化和访问。持久层包括了使用 ORM 框架进行对象和数据库映射的方式,以及使用 DAO 设计模式封装数据访问操作的方式。DAO 提供了数据访问的通用接口,业务逻辑层通过 DAO 来进行数据访问,从而实现了持久层的解耦。

  • 一般 JDBC 执行 SQL 查询或更新操作的流程涉及以下步骤:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    //1. 加载数据库驱动
    Class.forName("com.mysql.cj.jdbc.Driver");
    //2. 建立数据库连接
    Connection connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/xx", "username", "password");
    //3. 创建 Statement 或 PreparedStatement 对象
    Statement statement = connection.createStatement();
    //4. 执行 SQL 语句
    ResultSet resultSet = statement.executeQuery("SELECT * FROM yy");
    //5. 处理结果集(如果有)
    while (resultSet.next()) { // 处理查询结果
    }
    //6. 关闭资源:方法关闭 ResultSet、Statement 和 Connection 对象,释放数据库连接和资源。
    resultSet.close();
    statement.close();
    connection.close();
  • Mybatis执行流程

  1. 加载配置文件:MyBatis 首先会加载配置文件(通常是 XML 格式的),该配置文件包含了数据源信息、SQL 映射文件的位置、全局配置等。
  2. 解析配置文件:MyBatis 会解析加载的配置文件,将配置信息存储在相应的配置对象中,比如数据源对象、全局配置对象、SQL 映射配置对象等。
  3. 创建 SqlSessionFactory:根据解析得到的配置信息,MyBatis 创建一个 SqlSessionFactory 对象,该对象是一个线程安全的工厂类,用于创建 SqlSession 对象。
  4. 创建 SqlSession:通过 SqlSessionFactory 创建 SqlSession 对象,SqlSession 提供了执行 SQL 操作的方法,比如查询、插入、更新等。每个线程都应该有自己的 SqlSession 实例,SqlSession 不是线程安全的,因此通常在方法内部创建和关闭。
  5. 解析 Mapper 文件:在执行 SQL 操作之前,MyBatis 会解析 Mapper 文件,Mapper 文件中定义了 SQL 语句的映射关系,包括 SQL 语句、参数映射、结果映射等。
  6. 执行 SQL 操作:通过 SqlSession 执行 SQL 操作,MyBatis 提供了多种方法来执行 SQL,比如 selectOne、selectList、insert、update、delete 等。
  7. 映射结果:执行 SQL 操作后,MyBatis 将结果映射为 Java 对象,根据 Mapper 文件中的配置将查询结果映射为 Java 对象,并返回给调用者。
  8. 关闭资源:在完成所有操作后,需要关闭 SqlSession,释放数据库连接和其他资源,以避免资源泄露和性能问题。
  • Mybatis的优缺点,适用场合。

    • 优点:
      1.基于 SQL 编程,相当灵活,不会对应用程序或者数据库的现有设计成任何影响,SQL 写在 XML 里,解除 sql 与程序代码的耦合,便于统一管理;提供XML标签,支持编写动态 SQL 语句,并可重用。
      2.与 JDBC 相比,减少了 50%以上的代码量,消除了JDBC 大量几余的代码,不需要手动开关连接;
      3.很好的与各种数据库兼容(因为 Myatis 使用JDBC 来连接数据库,所以只要 JDBC 支持的数据库 MyBatis 都支持)。
      4.能够与Spring 很好的集成;
      5.提供映射标签,支持对象与数据库的 ORM 字段关系映射;提供对象关系映射标签,支持对象关系组件维护。
    • 缺点:
      1.SQL 语句的编写工作量较大,尤其当字段多、关联表多时,对开发人员编写SQL 语句的功底有一定要求.
      2.SQL 语句依赖于数据库,导致数据库移植性差,不能随意更换数据库。
    • MyBatis 框架适用场合
      1、MyBatis 专注于 SQL 本身,是一个足够灵活的 DAO 层解决方案。
      2、对性能的要求很高,或者需求变化较多的项目,如互联网项目,MyBatis 将是不错的选择。
  • #{} 与 ${} 的区别?

    • 两种占位符,都是实现动态sql的方式,可以把参数传递到xml中,执行操作前,mybatis会对这两个占位符进行动态解析
      1. #{}(预编译):#{} 是 MyBatis 的预处理语法,会在 SQL 中使用占位符 ? 的形式,通过预编译的方式来处理参数,可以有效防止 SQL 注入攻击。使用 #{} 时,MyBatis 会将传入的参数值转义并进行预编译,同时会根据参数的类型决定使用不同的 JDBC 类型。这种方式可以确保 SQL 的安全性。
      2. ${}(拼接字符串):${} 是 MyBatis 的字符串替换语法,会直接将传入的参数值以字符串的形式拼接到 SQL 语句中。使用 ${} 时,需要注意潜在的 SQL 注入风险,因为参数值会直接替换到 SQL 语句中,不会进行预编译。
    • #{} 适用于参数值,防止 SQL 注入,用于预编译。${} 适用于非参数值,直接将参数值拼接到 SQL 语句中。
    • 在开发中,推荐使用 #{} 来处理参数,以保障 SQL 的安全性。${} 的使用应谨慎,尽量避免直接将用户输入的数据用于 ${} 形式的参数。
  • 当实体类中的属性名和表中的字段名不一样,怎么办 ?

    1. 使用注解映射:ORM 框架(如 Hibernate)提供了注解(如 @Column)来映射实体类属性和数据库表字段之间的关系。
      可以在实体类的属性上使用注解,指定属性与表字段的映射关系
      1
      2
      3
      4
      5
      6
      7
      8
      9
      @Entity
      @Table(name = "my_table")
      public class MyEntity {
      @Id
      @Column(name = "column_name")
      private Long id;

      // other fields and methods
      }
      优点:灵活,可以通过注解直接在实体类中定义映射关系,不需要修改数据库表结构。
      缺点:如果数据库表结构已经存在,需要在实体类中添加注解进行映射,可能会导致代码中出现大量注解。
    2. 通过在查询的 sql 语句中定义字段名的别名,让字段名的别名和实体类的属性名一致。
      1
      2
      3
      <select id=”selectorder” parametertype=int” resultetype=”me.gacl.domain.order”>
      select order_id id, order_no orderno ,order_price price form orders where order_id=#{id};
      </select>
    3. 通过 resultMap 映射字段名和实体类属性名的一一对应的关系。
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      <select id="getOrder" parameterType="int" resultMap="orderresultmap">
      select * from orders where order_id=#{id}
      </select>
      <resultMap type=”me.gacl.domain.order” id=”orderresultmap”>
      <!–用 id 属性来映射主键字段–>
      <id property=”id” column=”order_id”>
      <!–用 result 属性来映射非主键字段,property 为实体类属性名,column为数据表中的属性–>
      <result property = “orderno” column =”order_no”/>
      <result property=”price” column=”order_price” />
      </reslutMap>
      优点:将映射关系从代码中分离出来,使得配置更加灵活。
      缺点:需要额外的配置文件,增加了维护成本。
  • ???通常一个 Xml 映射文件,都会写一个 Dao 接口与之对应,请问,这个 Dao 接口的工作原理是什么?
    Dao 接口里的方法,参数不同时,方法能重载吗?Dao 接口即 Mapper 接口。接口的全限名,就是映射文件中的 namespace 的值;接口的方法名,就是映射文件中 Mapper 的 Statement的 id 值;接口方法内的参数,就是传递给 sql 的参数。Mapper 接口是没有实现类的,当调用接口方法时,接口全限名+方法名拼接字符串作为 key 值,可唯一定位一个 MapperStatement。在 Mybatis 中,每一个
    ,,、、

  • Mybatis 是否支持延迟加载?如果支持,它的实现原理是什么?

    • 延迟加载的意思是:就是在需要用到数据时才进行加载,不需要用到数据时就不加载数据。Mybatis支持一对一关联对象和一对多关联集合对象的延迟加载
      在Mybatis配置文件中,可以配置是否启用延迟加载lazyLoadingEnabled=truelfalse,默认是关闭的
    • 延迟加载的底层原理
      1.使用 CGLIB 创建目标对象的代理对象
      2.当调用目标方法时,进入拦截器invoke方法,发现目标方法是null值,执行sql查询
      3.获取数据以后,调用set方法设置属性值,再继续查询目标方法,就有值了
  • Mybatis 的一级、二级缓存

    • 一级缓存: 基于 PerpetualCache 的 HashMap 本地缓存,其存储作用域为 Session,当 Session flush 或 close 之后,该 Session 中的所有 Cache 就将清空,默认打开一级缓存。
    • 二级缓存与一级缓存其机制相同,默认也是采用 PerpetualCache,HashMap存储,不同在于其存储作用域为 Mapper(Namespace),并且可自定义存储源,如 Ehcache。默认不打开二级缓存,要开启二级缓存,使用二级缓存属性类需要实现 Serializable 序列化接口(可用来保存对象的状态),可在它的映射文件中配置 ;
    • 对于缓存数据更新机制,当某一个作用域(一级缓存 Session/二级缓存Namespaces)的进行了 C/U/D 操作后,默认该作用域下所有 select 中的缓存将被 clear。
  • Mybatis是怎么分页的?

    • 逻辑分页:先查出数据,如何自己编写逻辑实现分页;
    • 物理分页:使用mysql提供的分页关键词limit
    • Mybatis提供了 3 种分页方式:
      1. mybatis mapper配置文件写分页sql,直接在select语句里面加limit
      2. ??使用mybatis提供的RowBounds对象,实现内存级的分页
      3. ??基于mybatis中的Interceptor拦截器,在select语句执行之前动态拼接分页关键字
  • 千万级数据量,分页查询优化、、
    不要 select * from Table limit 90000, 10
    而先 select id from Table limit 90000, 10,再根据 select * from Table where id = id


Redis

  • 什么是 Redis?

    • Redis 是完全开源免费的,遵守 BSD 协议,是一个高性能的 key-value 数据库。 Redis是一种NoSQL。
      NoSQL(Not Only SQL)是一种用于存储和检索非结构化或半结构化数据的数据库系统,主要包括以下几种类型:
      1、键值存储数据库(Key-Value Stores)包括Redis、Amazon DynamoDB和Riak。
      2、文档型数据库:存储的是类似于JSON或XML格式的文档,可以使用键来检索。示例包括MongoDB和Couchbase。
      3、图形数据库:以图形结构存储数据,用于处理复杂的关系和连接。示例包括Neo4j和Amazon Neptune。
      4、搜索引擎:搜索引擎允许用户通过关键字搜索文本数据。示例包括Elasticsearch和Apache Solr。
    • Redis 与其他 key-value 缓存产品有以下三个特点:
      Redis 支持数据的持久化,可以将内存中的数据保存在磁盘中,重启的时候可以再次加载进行使用。
      Redis 不仅仅支持简单的 key-value 类型的数据,同时还提供 list,set,zset,hash 等数据结构的存储。
      Redis 支持数据的备份,即 master-slave 模式的数据备份。
    • Redis 优势
      性能极高;Redis 能读的速度是 110000 次/s,写的速度是 81000 次/s 。
      丰富的数据类型,Redis 支持二进制案例的 Strings, Lists, Hashes,Sets 及Ordered Sets 数据类型操作。
      原子:Redis 的所有操作都是原子性的,意思就是要么成功执行要么失败完全不执行。通过 MULTI 和 EXEC指令包起来。
      丰富的特性 – Redis 还支持 publish/subscribe, 通知, key 过期等等特性。
  • 为什么要用Redis?
    无论Redis、MySQL、HDFS、HBase都是存储数据的地方,因为设计理念的不同,我们会根据不同的应用场景使用不同的存储。像Redis一般我们会把它用作于缓存(当然,日常有的应用场景比较简单,用个HashMap也能解决很多的问题了
    1、高性能:首先,它是纯内存操作,内存本身就很快。其次,它是单线程的,Redis服务器核心是基于非阻塞的IO多路复用机制,单线程避免了多线程的频繁上下文切换问题
    2、高可靠:主从复制,哨兵机制
    3、高拓展:数据发片,负载均衡

  • Redis 为什么这么快?

    1. Redis 的大部分操作都在内存中完成,并且采用了高效的数据结构(压缩表、跳跃表等方式降低了时间复杂读,同时还提供了不同时间复杂度的数据类型),因此 Redis 瓶颈可能是机器的内存或者网络带宽,而并非 CPU,既然 CPU 不是瓶颈,那么自然就采用单线程的解决方案了;
    2. Redis 采用单线程模型可以避免了多线程之间的竞争,省去了多线程切换的时间和性能上的开销,而且也不会导致死锁问题;
    3. Redis 采用了 I/0 多路复用机制处理大量的客户端 Socket 请求,I0 多路复用机制是指一个线程处理多个I0流,就是我们经常听到的 select/epoll 机制。简单来说,在 Redis 只运行单线程的情况下,该机制允许内核中,同时存在多个监听 Socket 和已连接 Socket.内核会一直监听这些 Socket 上的连接请求或数据请求。一旦有请求到达,就会交给 Redis线程处理,这就实现了一个 Redis 线程处理多个 IO 流的效果。
    4. Redis支持丰富的数据结构,包括字符串、哈希、列表、集合、有序集合等,每种数据结构都针对不同的应用场景进行了优化,能够更高效地处理各种数据操作。
  • Redis 是单进程单线程的?
    Redis 是单进程单线程的,redis 利用队列技术将并发访问变为串行访问,消除了传统数据库串行控制的开销。

  • Redis网络多路I/O复用??

    • 多路复用是一种提高 I/O 效率的技术,它可以同时监听多个文件描述符的 I/O 事件,并且在有事件发生时立即通知应用程序,从而避免了轮询的开销。在 Redis 中,网络 I/O 多路复用主要涉及到客户端与服务端之间的通信,使用类似 epoll 的事件驱动机制来处理网络 I/O 多路复用,通过监听套接字上的事件并采用非阻塞 I/O 模式,实现了高效的网络通信和并发处理能力。
    1. 事件驱动模型
      • Redis 使用事件驱动模型来处理网络 I/O,通过监听套接字上的读写事件,实现异步的网络通信。
      • 在客户端连接到 Redis 服务器时,服务器会将该客户端的套接字加入到事件驱动器中进行监听。
    2. epoll 机制
      • 在 Linux 系统中,Redis 使用 epoll 作为网络 I/O 多路复用的实现机制之一。
      • epoll 是一种高效的 I/O 多路复用技术,可以同时监控多个文件描述符的 I/O 事件,并将有事件发生的文件描述符返回给应用程序处理。
      • Redis 使用 epoll_wait 函数来等待套接字上的事件,并在事件发生时处理对应的网络 I/O 操作。
    3. 非阻塞 I/O
      • Redis 使用了非阻塞 I/O 模式来处理套接字的读写操作,这样可以避免在等待数据到达时线程被阻塞,提高了系统的并发处理能力。
      • 当客户端发送数据给 Redis 服务器时,服务器会尽可能地读取数据,而不是等待数据全部到达再进行处理。
    4. 事件循环
      • Redis 的服务器主循环是一个事件驱动的循环,不断地监听客户端套接字上的事件并处理。
      • 当有新的连接请求、数据到达、连接断开等事件发生时,服务器会相应地调用相应的处理函数来处理这些事件。
  • Redis 常见性能问题和解决方案?
    (1) Master 最好不要做任何持久化工作,如 RDB 内存快照和 AOF 日志文件
    (2) 如果数据比较重要,某个 Slave 开启 AOF 备份数据,策略设置为每秒同步一次
    (3) 为了主从复制的速度和连接的稳定性, Master 和 Slave 最好在同一个局域网内
    (4) 尽量避免在压力很大的主库上增加从库
    (5) 主从复制不要用图状结构,用单向链表结构更为稳定,即: Master <- Slave1 <- Slave2 <-Slave3…

  • Redis的教据结构有?

    数据类型 key string hash list set sorted set bitmap hyperloglog
    最大存储数据量 512M 512M 2^32 - 1 2^32 - 1 2^32 -1 512M 12K
  • Redis数据结构分别有哪些典型的应用场景?

    1. 字符串:可以用来做最简单的数据,可以颂存某个简单的字符串,也可以存某个json格式的字符审,Redis分布式的实现就利用了这种数据结构,还包括可以实现计数器、Session共享、分布式ID
    2. 哈希表:可以用来存储一些key-value对,更适合用来存储对象,统计类数据,购物车
    3. 列表:Redis的列表通过命令的组合,既可以当做栈,也可以当做队列来使用,可以用来缓存类似微信公众号、微博等消息流教据,文章列表,消息队列
    4. 集合:和列表类似,也可以存储多个元素,但是不能重复,集合可以进行交集、并集、差集损作,从而可以实现类似,我和某人共同关注的人、朋友画点赞等功能
    5. 有序集合:集合是无序的,有序集合可以设置顺序,可以用来实现排行榜功能,按时间播放量点击
  • Redis 中 String 的底层实现、、

    • Redis底层是C实现的,Redis中String是一种动态字符串类型,它的底层实现就是 SDS。每个字符串对象(string object)都有对应的 SDS 结构,存储在内存中。
    • SDS 是 Redis 自己实现的字符串抽象数据结构,当 Redis 执行字符串操作如 SET、GET、APPEND 等操作,实际上是对 SDS 结构进行操作,包括修改长度、扩展空间、拷贝数据等操作。相较于 C 语言的原生字符串,SDS 具有以下优势:
      1. O(1) 复杂度的长度计算:SDS 不需要每次计算字符串长度,因为它在结构中记录了字符串的长度,因此获取字符串长度的操作是 O(1) 复杂度的。
      2. 空间预分配:SDS 在空间分配时会预留额外的空间,以减少字符串增长时频繁地重新分配内存的次数,从而提高性能。
      3. 修改操作的高效性:SDS 通过记录字符串的长度,可以直接修改字符串内容,而无需像 C 语言原生字符串那样,重新计算长度和分配内存。
    • SDS 的结构如下所示:
      1
      2
      3
      4
      5
      struct sdshdr {
      int len; // 字符串长度
      int free; // 未使用空间长度
      char buf[]; // 字符串数据
      };
  • Zset底层实现

    • 压缩列表:普通数组+列表长度+尾部偏移量+列表元素个数+列表结束标识,对于非首尾节点仍然使用遍历逐个查询
    • 跳表:根据有序列表元素值,加多级索引以快速查找。时间复杂度为O(logn)(相当于二分查找)
    • 什么时候采用跳表呢?
      1.有序集合保存的元素数量小于128个
      2.有序集合保存的所有元素的长度小于 64字节
    • zset为什么用跳表而不用二叉树或者红黑树呢?
      红黑树不支持范围查找,且构造起来比跳表复杂
  • Redis为什么不用b+树?MySQL为什么不用跳表?
    这个问题在于 Redis是直接操作内存的并不需要磁盘io,跳表明显是更快更简单的方式;而MySQL需要去读取io,所以mysql要使用b+树的方式减少磁盘io

  • Redis 应用场景。

    1. 缓存层。防止所有的请求打到DB,做过多的IO
    2. 分布式锁。正常的 Threadlocal 是单进程JVM内的一个锁,不能对所有的服务起到同步的效果,所有用redis抽象出来做一个集群,这种分布式锁是全局可见的
    3. 作消息队列。redis发布/订阅模式,类似与消息队列。
  • 如何使用 Redis 做异步队列么???
    一般使用 list 结构作为队列,rpush生产消息,lpop消费消息。当 lpop没有消息的时候,要适当 sleep一会再重试。
    可不可以不用 sleep 呢? list 还有个指令叫 blpop,在没有消息的时候,它会阻塞住直到消息到来。
    能不能生产一次消费多次呢?使用 pub/sub 主题订阅者模式,可以实现 1:N 的消息队列。
    pub/sub 有什么缺点?在消费者下线的情况下,生产的消息会丢失,得使用专业的消息队列如 RabbitMQ等。
    redis 如何实现延时队列?使用sortedset,拿时间戳作为score,消息内容作为 key 调用 zadd 来生产消息,消费者用 zrangebyscore 指令获取 N 秒前的数据轮询处理。

  • Redis持久化。

    • Redis是基于内存的,假设不做任何操作,只要Redis服务器重启(或者中途故障挂掉了)那内存的数据就会没掉。所以Redis提供了持久化机制给我们用,分别是RDB和AOF。通过持久化机制把内存中的数据同步到硬盘文件来保证数据持久化。当Redis重启后通过把硬盘文件重新加载到内存,就能达到恢复数据的目的。
    • RDB:Redis默认的持久化方式。根据我们自己配置的时间或者手动去执行BGSAVE或SAVE命令,Redis就会去(fork一个子进程来)生成RDB文件。
      RDB是按照一定的时间周期策略把内存的数据以快照的形式保存到硬盘的二进制文件,Redis可以通过这个文件在启动的时候来还原我们的数据。即Snapshot快照存储,对应产生的数据文件为dump.rdb,通过配置文件中的save参数来定义快照的周期。( 快照可以是其所表示的数据的一个副本,也可以是数据的一个复制品。)
    • AOF:Redis会将每一个收到的写命令都通过Write函数追加到文件最后,类似于MySQL的binlog。当Redis重启是会通过重新执行文件中保存的写命令来在内存中重建整个数据库的内容。当两种方式同时开启时,数据恢复Redis会优先选择AOF恢复。
      https://www.bilibili.com/read/cv28294981/?spm_id_from=333.999.0.0&jump_opus=1
  • 缓存雪崩、缓存击穿、缓存穿透

    1. 缓存穿透
      • 缓存穿透是指用户查询数据,在数据库没有,自然在缓存中也不会有。这样就导致用户查询的时候,在缓存中找不到,每次都要去数据库再查询一遍,然后返回空(相当于进行了两次无用的查询)。这样请求就绕过缓存直接查数据库,这也是经常提的缓存命中率问题。
      • 解决办法:
        • 1、最常见的则是采用布隆过滤器,将所有可能存在的数据哈希到一个足够大的bitmap中,一个一定不存在的数据会被这个bitmap拦截掉,从而避免了对底层存储系统的查询压力。
        • 2、另外也有一个更为简单粗暴的方法,如果一个查询返回的数据为空(不管是数据不存在,还是系统故障),我们仍然把这个空结果进行缓存,但它的过期时间会很短,最长不超过五分钟。通过这个直接设置的默认值存放到缓存,这样第二次到缓冲中获取就有值了,而不会继续访问数据库。
        • 5TB的硬盘上放满了数据,请写一个算法将这些数据进行排重。如果这些数据是一些32bit大小的数据该如何解决?如果是64bit的呢?
          对于空间的利用到达了一种极致,那就是Bitmap和布隆过滤器(Bloom Filter)。
      • Bitmap:典型的就是哈希表;缺点是,Bitmap对于每个元素只能记录1bit信息,如果还想完成额外的功能,恐怕只能靠牺牲更多的空间、时间来完成了
      • 布隆过滤器(推荐):就是引入了 k(k>1) 个相互独立的哈希函数,保证在给定的空间、误判率下,完成元素判重的过程。
        它的优点是空间效率和查询时间都远远超过一般的算法,缺点是有一定的误识别率和删除困难。Bloom-Filter算法的核心思想就是利用多个不同的Hash函数来解决“冲突”。Hash存在一个冲突(碰撞)的问题,用同一个Hash得到的两个URL的值有可能相同。为了减少冲突,我们可以多引入几个Hash,如果通过其中的一个Hash值我们得出某元素不在集合中,那么该元素肯定不在集合中。只有在所有的Hash函数告诉我们该元素在集合中时,才能确定该元素存在于集合中。这便是Bloom-Filter的基本思想。Bloom-Filter一般用于在大数据量的集合中判定某元素是否存在。
    2. 缓存击穿
      • 场景:一份热点数据,它的访问量非常大。在其缓存失效瞬间,大量请求直达存储层,导致服务崩溃
      • 解决方案:
        1、分布式锁:对数据的访问加互斥锁,当一个线程访问该数据时,其他线程只能等待这个线程访问过后,缓存中的数据将被重建,届时其他线程就可以直接从缓存取值.
        2、永不过期:不设置过期时间,所以不会出现上述问题,这是“物理”上的不过期。为每个value设置逻辑过期时间,当发现该值逻辑过期时,使用单独的线程重建缓存.
    3. 缓存雪崩
      • 我们可以简单的理解为:由于原有缓存失效,新缓存未到期间(例如:我们设置缓存时采用了相同的过期时间,在同一时刻出现大面积的缓存过期),所有原本应该访问缓存的请求都去查询数据库了,而对数据库CPU和内存造成巨大压力,严重的会造成数据库宕机。从而形成一系列连锁反应,造成整个系统崩溃。
      • 解决办法:
        1、加锁:大多数系统设计者考虑用加锁(最多的)或者队列的方式保证来保证不会有大量的线程对数据库一次性进行读写,从而避免失效时大量的并发请求落到底层存储系统上。
        2、设置随机值:将缓存失效时间分散开。
  • ???分布式锁、、

    • 场景:修改时,经常需要先将数据读取到内存,在内存中修改后再存回去。在分布式应用中,可能多个进程同时执行上述操作,而读取和修改非原子操作,所以会产生冲突。增加分布式锁,可以解决此类问题.
    • 基本原理:
      1. 同步锁:在多个线程都能访问到的地方,做一个标记,标识该数据的访问权限。
      2. 分布式锁:在多个进程???都能访问到的地方,做一个标记,标识该数据的访问权限
    • 实现方式:1、基于数据库实现分布式锁 2、基于Redis实现分布式锁 3、基于zookeeper实现分布式锁
      具体做法:??? 先拿 setnx 来争抢锁,抢到之后,再用 expire 给锁加一个过期时间防止锁忘记了释放。
    • 分布式锁实现???
      实现分布式锁可以通过数据库 redis zookeeper。
      redis setnx是最简单的一种。可以满足基本使用但有些特性和场景没法满足 比如不可重入 不可重试 锁过期删除的一些问题。redisson是在redis基础上也实现了分布式锁,解决了这几个问题就是redisson里面的可重入锁,比如解决不可重入用的是hash结构解决。解决不可重试基于redis的发布订阅。解决锁过期删除基于自己实现的一个看门狗机制。但是redisson这把可重入锁还有一些问题,比如主从时宕机的问题,redisson里面又可以通过有联锁啊 红锁啊解决。不过setnx基本上可以应用于分布式锁了,有一些业务上需要满足一些锁的需求的时候可以考虑更复杂的实现
      支付宝一面:如何基于Redis实现分布式锁???https://mp.weixin.qq.com/s/y8kAflIenRpa4zIwCP_8iA
  • 缓存预热、缓存更新、缓存降级

    1. 缓存预热
      系统上线后,将相关的缓存数据直接加载到缓存系统。这样就可以避免在用户请求的时候,先查询数据库,然后再将数据缓存的问题!用户直接查询事先被预热的缓存数据!
      解决思路:1、直接写个缓存刷新页面,上线时手工操作下;2、数据量不大,可以在项目启动的时候自动进行加载;3、定时刷新缓存
    2. 缓存更新
      除了缓存服务器自带的缓存失效策略之外(Redis默认的有6中策略可供选择),我们还可以根据具体的业务需求进行自定义的缓存淘汰,常见的策略有两种:
      (1)定时去清理过期的缓存;
      (2)当有用户请求过来时,再判断这个请求所用到的缓存是否过期,过期的话就去底层系统得到新数据并更新缓存。
      两者各有优劣,第一种的缺点是维护大量缓存的key是比较麻烦的,第二种的缺点就是每次用户请求过来都要判断缓存失效,逻辑相对比较复杂!具体用哪种方案,大家可以根据自己的应用场景来权衡
    3. ??缓存降级
      当访问量剧增、服务出现问题(如响应时间慢或不响应)或非核心服务影响到核心流程的性能时,仍然需要保证服务还是可用的,即使是有损服务。系统可以根据一些关键数据进行自动降级,也可以配置开关实现人工降级。
      降级的最终目的是保证核心服务可用,即使是有损的。而且有些服务是无法降级的(如加入购物车、结算)。以参考日志级别设置预案:
      (1)一般:比如有些服务偶尔因为网络抖动或者服务正在上线而超时,可以自动降级;
      (2)警告:有些服务在一段时间内成功率有波动(如在95~100%之间),可以自动降级或人工降级,并发送告警;
      (3)错误:比如可用率低于90%,或者数据库连接池被打爆了,或者访问量突然猛增到系统能承受的最大阀值,此时可以根据情况自动降级或者人工降级;
      (4)严重错误:比如因为特殊原因数据错误了,此时需要紧急人工降级。服务降级的目的,是为了防止Redis服务故障,导致数据库跟着一起发生雪崩问题。因此,对于不重要的缓存数据,可以采取服务降级策略,例如一个比较常见的做法就是,Redis出现问题,不去数据库查询,而是直接返回默认值给用户
  • 热点数据和冷数据?
    数据更新前至少读取两次,缓存才有意义。对于热点数据,比如生日祝福模块中当天的寿星列表,缓存以后可能读取数十万次,同时信息修改频率不高。这个是最基本的策略,若缓存还没有起作用就失效就没有太大价值了。
    那存不存在,修改频率很高,但是又不得不考虑缓存的场景呢?有!比如,这个读取接口对数据库的压力很大,但是又是热点数据,这个时候就需要考虑通过缓存手段,减少数据库的压力,比如我们的某助手产品的,点赞数,收藏数,分享数等是非常典型的热点数据,但是又不断变化,此时就需要将数据同步保存到Redis缓存,减少数据库压力

  • 过期策略。

    • Redis会把设置了过期时间的key放入一个独立的字典里,过期时并不会立刻删除,通过如下两种策略来删除过期的key:
      1、惰性删除:客户端访问某个key时,Redis会检查该key是否过期,若过期则删除。(问题: 有些键值对可能已过期,但是由于没有再被访问,导致未被删除,因而占用内存)。
      2、定期扫描:Redis默认每秒执行10次过期扫描 (配置hz选项) ,扫描策略如下 (1). 从过期字典中随机选择20个key; (2)删除这20个key中已过期的key; (3)如果过期的key的比例超过25%,则重复步骤(1)
    • 为什么不用定时删除策略?
      定时删除,用一个定时器来负责监视key,过期则自动删除。虽然内存及时释放,但是十分消耗CPU资源。在大并发请求下,CPU要将时间应用在处理请求,而不是删除key,因此没有采用这一策略.
    • 定期删除+惰性删除是如何工作的呢?
      定期删除,redis默认每个100ms检查,是否有过期的key,有过期key则删除。需要说明的是,redis不是每个100ms将所有的key检查一次,而是随机抽取进行检查(如果每隔100ms,全部key进行检查,redis岂不是卡死)。因此,如果只采用定期删除策略,会导致很多key到时间没有删除。于是,惰性删除派上用场。也就是说在你获取某个key的时候,redis会检查一下,这个key如果设置了过期时间那么是否过期了?如果过期了此时就会删除。
    • 采用定期删除+惰性删除就没其他问题了么?
      不是的,如果定期删除没删除key。然后你也没即时去请求key,也就是说惰性删除也没生效。这样,redis的内存会越来越高。那么就应该采用内存淘汰机制。在redis.conf中有一行配置
  • 淘汰策略。
    当Redis占用内存超出最大限制(maxmemory)时,可采用如下策略(maxmemory-policy)淘汰一些数据以腾出空间继续提供读写服务:
    noeviction:对可能导致增大内存的命令返回错误 (大多数写命令,DEL除外)
    volatile-ttl:在设置了过期时间的key中,选择剩余寿命(TTI) 最短的key,将其淘汰
    volatile-lru:在设置了过期时间的kev中,选择最少使用的kev (LRu) ,将其淘汰:
    volatile-random:在设置了过期时间的key中,随机选择一些key,将其淘汰;
    allkeys-lru:在所有的ke中,选择最少使用的key (LRu) ,将其淘汰
    allkeys-random:在所有的key中,随机选择一些key,将其淘汰
    (这里其实还有volatile-lfu、allkeys-lfu,所谓==LFU算法==,就是先考虑键值对访问的次数,优先淘汰访问次数少的键值对,对于访问次数相同的键值对,再选择最近久未被访问的键值对进行淘汰(也就是LRU算法))
    LRU算法:维护一个链表,用于顺序存储被访问过的key。在访问数据时,最新访问过的kev将被移动到表头, 即最近访问的key在表头,最少访问的key在表尾。

  • ??Redis事务。
    Redis事务功能是通过 MULTI、EXEC、DISCARD 和 WAWATATCH 四个原语实现的
    Redis会将一个事务中的所有命令序列化,然后按顺序执行。
    1.redis 不支持回滚。在事务失败时不进行回滚,而是继续执行余下的命令,所以 Redis 的内部可以保持简单且快速。
    2.如果在一个事务中的命令出现错误,那么所有的命令都不会执行;
    3.如果在一个事务中出现运行错误,那么正确的命令会被执行。
    1)MULTI命令用于开启一个事务,它总是返回OK。 MULTI执行之后,客户端可以继续向服务器发送任意多条命令,这些命令不会立即被执行,而是被放到一个队列中,当EXEC命令被调用时,所有队列中的命令才会被执行。
    2)EXEC:执行所有事务块内的命令。返回事务块内所有命令的返回值,按命令执行的先后顺序排列。当操作被打断时,返回空值 nil 。
    3)通过调用DISCARD,客户端可以清空事务队列,并放弃执行事务, 并且客户端会从事务状态中退出。
    4)WAWATATCH 命令可以为 Redis 事务提供 check-and-set (CAS)行为。 可以监控一个或多个键,一旦其中有一个键被修改(或删除),之后的事务就不会执行,监控一直持续到EXEC命令

  • 为什么Redis的操作是原子性的,怎么保证原子性的?
    对于Redis而言,命令的原子性指的是:一个操作的不可以再分,操作要么执行,要么不执行。
    Redis的操作之所以是原子性的,是因为Redis是单线程的。Redis本身提供的所有API都是原子操作,Redis中的事务其实是要保证批量操作的原子性。
    多个命令在并发中也是原子性的吗?不一定,将get和set改成单命令操作,???incr 。使用Redis的事务,或者使用Redis+Lua==的方式实现。

  • Redis 的同步机制了解么?
    Redis 可以使用主从同步,从从同步。第一次同步时,主节点做一次 bgsave,并同时将后续修改操作记录到内存 buffer,待完成后将 rdb文件全量同步到复制节点,复制节点接受完成后将 rdb 镜像加载到内存。加载完成后,再通知主节点将期间修改的操作记录同步到复制节点进行重放就完成了同步过程。

  • Redis 多节点部署主要有以下几种方式。

    1. 主从复制(Master-Slave Replication):主从复制是 Redis 的基本高可用性架构。一个 Redis 主节点可以拥有多个从节点,主节点负责写操作和同步数据到从节点,从节点负责复制主节点的数据。当主节点不可用时,可以选择一个从节点提升为主节点,实现故障切换。
    2. 哨兵模式(Redis Sentinel):Redis Sentinel 是用于监控 Redis 实例并支持自动故障转移的组件。它可以监控多个 Redis 主从复制集群,当主节点不可用时,自动将一个从节点晋升为新的主节点,保证服务的可用性。哨兵模式提供了更强大的故障检测和自动切换功能。
    3. 集群模式(Redis Cluster):Redis Cluster 是 Redis 提供的分布式解决方案,用于在多个节点之间分片存储数据。Redis Cluster 将数据分成多个槽(slot),每个槽可以分配给集群中的不同节点。它支持横向扩展、高可用性和自动数据分片。当集群中的某个节点不可用时,可以通过复制和重新分片来保证服务的可用性。
    4. 第三方解决方案:除了 Redis 官方提供的方案外,还有一些第三方解决方案可以用于构建 Redis 的多节点部署,比如一些代理软件或者中间件,它们提供了更多高级功能,比如自动负载均衡、故障转移等。
  • Redis 集群

    • 1、主从复制:Redis 集群中的每个节点通常都有主节点和若干个从节点。主节点负责处理写操作和部分读操作,而从节点负责复制主节点的数据,用于读操作和故障恢复。主从复制可以提高系统的可用性和容错能力。Redis Sentinal 着眼于高可用,在 master 宕机时会自动将 slave 提升为master,继续提供服务。
    • 2、数据分片:Redis 集群通过分片机制将数据分布到多个节点上。每个节点负责一部分数据的存储和处理。这样可以提高系统的并发处理能力和数据存储容量。Redis Cluster 着眼于扩展性,在单个 redis 内存不足时,使用 Cluster 进行分片存储。
    • 3、数据同步:在 Redis 集群中,数据同步是保证数据一致性的关键。主节点将写操作同步到从节点,确保数据在各个节点之间的同步。在读操作时,可以从主节点或者从节点读取数据,但要注意可能存在数据的延迟和不一致性。
    • Redis 集群的主从复制模型是怎样的?
      为了使在部分节点失败或者大部分节点无法通信的情况下集群仍然可用,所以集群使用了主从复制模型,每个节点都会有 N-1 个复制品.
    • 场景:查询一个字符串类型数据为什么花了5秒?
      Redis 集群中的某个节点网络连接不稳定或者网络延迟较高,或者某个节点数据负载较高。
      如果查询的数据正好位于 Redis 集群中的某个分片,而该分片的主节点或者从节点负载较高或者网络延迟较高,会导致查询时间变长。
    • Redis 集群会有写操作丢失吗: Redis 并不能保证数据的强一致性,实际中集群在特定的条件下可能会丢失写操作。
    • 集群之间是如何复制的:异步复制
    • 集群最大节点个数是多少: 6384 个
    • 集群如何选择数据库:Redis 集群目前无法做数据库选择,默认在 0 数据库。
  • Redis 和 Mysql 如何保证数据一致?
    1、先更新 Mysql,再更新 Redis,如果更新 Redis失败,可能仍然不一致
    2、先删除 Redis缓存数据,再更新 MySql,再次查询的时候在将数据添加到缓存中,这种方案能解决方案1的问题,但是在高并发下性能较低,而且仍然会出现数据不一致的问题:比如线程1删除了Redis缓存数据,正在更新 MySql,此时另外一个查询再查询,那么就会把 MySql中老数据又查到 Redis中
    3、延时双删,步聚是:先删除 Redis存数,再更新 MySql,延迟几百毫秒除 Redis存数据,这样就算在更新 MySql时,有其他线程读了Mysql,把老数据读到了 Redis中,那么也会被制除掉,从而把数据保持一致
    4、如果需要在极端情况下仍然保证 Redis 和 Mysql 的数据一致性,就只能采用最终一致性方案。比如基于 RocketMQ 的可靠性消息通信,来实现最终一致性:把(更新redis)失败的请求写入MQ事务消息,然后异步重试,确保成功。还可以直接通过 Canal 组件,监控 Mysql 中 binlog 的日志,把更新后的数据同步到 Redis 里面。??


消息队列

  • 消息队列 mq

    • 消息队列 Message Queue,简称 MQ。是一种应用间的通信方式,主要由三个部分组成。
      • 生产者:Producer,消息的产生者与调用端,主要负责消息所承载的业务信息的实例化,是一个队列的发起方
      • 代理:Broker,主要的处理单元,负责消息的存储、投递、及各种队列附加功能的实现,是消息队列最核心的组成部分
      • 消费者:Consumer,一个消息队列的终端,也是消息的调用端,具体是根据消息承载的信息,处理各种业务逻辑。
    • mq理解
      • 消息路由
        • 路由器的类型和绑定规则
      • 消息不丢失
        • 消息确认机制
          • 配置合适的消息TTL
          • 备份队列
      • 消息消费
        • 顺序消费
          • 消息优先级
          • 单线程消费
        • 重复消费
          • durable参数:true
          • deliveryMode参数:1为非持久,2为持久化
      • 消息持久化
    • mq应用
      • 异步处理
      • 应用解耦
      • 流量削峰
        • 限速
        • 死性队列
        • 消息优先级
        • 公平分发
        • 限流
        • 延迟队列
  • 消息队列的应用场景较多,常用的可以分为三种:

    • 异步处理:主要应用于对实时性要求不严格的场景,比如:用户注册发送验证码、下单通知、发送优惠券等等。服务方只需要把协商好的消息发送到消息队列,剩下的由消费消息的服务去处理,不用等待消费服务返回结果。
    • 应用解耦:可以看作是把相关但耦合度不高的系统联系起来。比如订单系统与 WMS、EHR 系统,有关联但不哪么紧密,每个系统之间只需要把约定的消息发送到 MQ,另外的系统去消费即可。解决了各个系统可以采用不同的架构、语言来实现,从而大大增加了系统的灵活性。
    • 流量削峰:一般应用在大流量入口且短时间内业务需求处理不完的服务中心,为了权衡高可用,把大量的并行任务发送到 MQ 中,依据 MQ 的存储及分发功能,平稳的处理后续的业务,起到一个大流量缓冲的作用。
  • 如何进行消息队列选型?

    • Kafka:日志分析、大数据采集
      • 一种高吞吐量、分布式、基于发布/订阅的消息系统,最初由 LinkedIn 公司开发,使用Scala 语言编写,目前是 Apache 的开源项目。
      • 优点: 各吐量非常大,性能非常好,集群高可用。
      • 缺点:会丢数据,功能比较单一
    • RabbitMQ:小规模场景
      • 是什么:采用 AMQP 高级消息队列协议的一种消息队列技术,最大的特点就是消费并不需要确保提供方存在,实现了服务之间的高度解耦
      • 优点: 消息可靠性高,功能全面。
      • 缺点:吞吐量比较低,消息积累会严重影响性能。erlang语言不好定制.
    • RocketMQ:几乎是全场景
      • 优点:高吞吐、高性能、高可用,功能非常全面。
      • 缺点:开源版功能不如云上商业版。官方文档和周边生态还不够成熟。客户端只支持java。

Kafka

  • kafka https://www.yuque.com/itwanger/gykdzg/ngd5g3glaxii4h6g udd1

  • Kafka 优缺点?

    • 优点
      高性能、高吞吐量、低延迟:Kafka 生产和消费消息的速度都达到每秒10万级高可用:所有消息持久化存储到磁盘,并支持数据备份防止数据丢失
      高并发:支持数千个客户端同时读写
      容错性:允许集群中节点失败(若副本数量为n,则允许 n-1 个节点失败)
      高扩展性:Kafka 集群支持热伸缩,无须停机
    • 缺点
      没有完整的监控工具集
      不支持通配符主题选择
  • kafka 架构?

    • Kafka 将消息以 topic 为单位进行归纳,发布消息的程序称为 Producer,消费消息的程序称为 Consumer。它是以集群的方式运行,可以由一个或多个服务组成,每个服务叫做一个 Broker,Producer 通过网络将消息发送到 kafka 集群,集群向消费者提供消息,broker 在中间起到一个代理保存消息的中转站。
    • kafka 组成:
      1. Producer:消息生产者,发布消息到Kafka集群的终端或服务
      2. Broker:一个 Kafka 节点就是一个 Broker,多个Broker可组成一个Kafka 集群
        如果某个 Topic 下有n个Partition 且集群有n个Broker,那么每个 Broker会存储该 Topic 下的一个 Partition
        如果某个 Topic 下有n个Partition 且集群中有 m+n个Broker,那么只有n个Broker会存储该Topic下的一个Partition
        如果某个 Topic 下有n个Partition 且集群中的Broker数量小于 n,那么一个 Broker 会存储该 Topic 下的一或多个 Partition,这种情况尽量避免,会导致集群数据不均衡
      3. Topic:逻辑概念,消息主题,每条发布到Kafka集群的消息都会归集于此,Kafka是面向Topic 的
      4. Partition:是Topic在物理上的分区,从Partition消费信息。一个Topic可以分为多个Partition,每个Partition是一个有序的不可变的记录序列。单一主题中的分区有序,但无法保证主题中所有分区的消息有序。
      5. Consumer:从Kafka集群中消费消息的终端或服务
      6. Consumer Group:每个Consumer都属于一个Consumer Group,每条消息只能被Consumer Group中的一个Consumer消费,但可以被多个Consumer Group消费。
      7. Zookeeper:Kafka 通过Zookeeper来存储集群中的 meta 消息(0.9版本后,将消息偏移量offset存储本地)
  • 如何判断一个 Broker 是否还有效
    1.Broker必须可以维护和ZooKeeper的连接,Zookeeper通过心跳机制检查每个结点的连接
    2.如果Broker是个Follower,它必须能及时同步Leader的写操作,延时不能太久。

  • kafka分区,partition

    • 分区的概念
      主题是一个逻辑上的概念,还可以细分为多个分区,一个分区只属于单个主题,很多时候也会把分区称为主题分区(Topic-Partition)。同一主题下的不同分区包含的消息是不同的,分区在存储层面可以看做一个可追加的 日志文件 ,消息在被追加到分区日志文件的时候都会分配一个特定的偏移量(offset)。
    • offset:是消息在分区中的唯一标识,表示在分区中消费到消息的位置;
      kafka 通过它来保证消息在分区内的顺序性,不过 offset 并不跨越分区,也就是说,kafka保证的是分区有序而不是主题有序。
    • 在分区中又引入了多副本(replica)的概念,通过增加副本数量可以提高容灾能力。同一分区的不同副本中保存的是相同的消息。副本之间是一主多从的关系,其中主副本负责读写,从副本只负责消息同步。副本处于不同的broker中,当主副本出现异常,便会在从副本中提升一个为主副本。
    • Kafka 中分区的原则???
      1.指明Partition的情况下,直接将指明的值作为Partition值
      2.没有指明Partition值但有 key 的情况下,将 key 的 Hash 值与 topic的Partition值进行取余得到Partition值
      3.既没有Partition值又没有 key 值的情况下,第一次调用时随机生成一个整数(后面每次调用在这个整数上自增),将这个值与Topic可用的Partition总数取余得到Parittion值,也就是常说的round-robin 算法
    • Kafka 为什么要把消息分区
      1.方便在集群中扩展,每个 Partition 可用通过调整以适应它所在的机器,而一个Topic又可以有多个Partition组成,因此整个集群就可以适应任意大小的数据了
      2.可以提高并发,因为可以以Partition为单位进行读写
  • kafka 消费模式

    1. 发布-订阅模式
      • 在发布-订阅模式中,消息被发送到一个主题(Topic),而每个消费者都可以独立地订阅这个主题并接收消息。
      • Push 模式:一种主动推送的方式,即消息生产者(Publisher)将消息直接推送给感兴趣的订阅者(Subscriber)。
        这种模式下,订阅者无需主动拉取消息,而是等待消息被推送过来。因此,消息的发送和接收是异步的。
      • Pull 模式:一种被动获取的方式,即订阅者需要主动向消息服务器请求消息,然后消息服务器才会将消息发送给订阅者。
        在这种模式下,订阅者需要周期性地或者根据需要发起拉取消息的请求,消息的发送和接收是同步的。
    2. 消费者组模式
      • 在消费者组模式中,多个消费者被组织成一个消费者组(Consumer Group),并且每个分区(Partition)只能被同一个消费者组中的一个消费者处理。
      • 当一个新的消费者加入消费者组时,它会负责消费未被分配的分区。
      • 这种模式可以实现消息的负载均衡和水平扩展,适合于处理大量消息的情况。
  • Kafka是如何保证消息不丢失

    • 生产者发送消息到Brocker丢失
      设置异步发送,发送失败使用回调进行记录或重发;失败重试,参数配置,可以设置重试次数
    • 消息在Brocker中存储丢失
      发送确认设置acks=all,让所有的副本都参与保存数据后确认(设置acks=1时,只要leader节点收到消息,生产者就会收到一个来自服务器的成功响应)
    • (发送到Broker之后,走的是操作系统缓存,在异步刷盘(保存到硬盘)这个过程还有可能丢失)
    • 消费者从Brocker接收消息丢失
      关闭自动提交偏移量,开启手动提交偏移量;提交方式,最好是同步+异步提交
  • kafka 消费重复数据?

    • 原因:
      1、服务宕机,5秒钟内没有提交offerset;
      2、5分钟内没消费完partition数据,触发reblance
    • 解决:
      1、关闭自动提交偏移量,开启手动提交偏移量提交方式,最好是同步+异步提交,
      2、提升消费端性能避免触发balance:异步消费、挑战消费时长、减少一次性从broker拉取的消息条数
      3、幂等方案:对消费的消息生成md5存到redis,再存入时比对
  • Kafa 中如何保证顺序消费

    • 问题原因:
      一个topic的数据可能存储在不同的分区Partition 中,Kafka 的消费单元是 Partition,同一个 Partition 使用 offset 作为唯一标识保证顺序性。但这只是保证了在Partition 内部的顺序性而不是 Topic 中的顺序,如果消费者关联了多个分区,不能保证顺序性。
    • 解决方法:(普通情况下)只需要把相同userld/orderld发送到相同的partition,因为一个partition由(一个消费者组的)一个Consumer消费
      • 发送消息时指定分区号
      • 发送消息时按照相同的业务设置相同的key
  • kafka高可用?

    • 可以从两个层面回答,第一个是集群,第二个是复制机制集群:
      • 一个kafka集群由多个broker实例组成,即使某一台宕机,也不耽误其他broker继续对外提供服务复制机制:
      • 一个topic有多个分区,每个分区有多个副本,有一个leader,其余的是follower,副本存储在不同的broker中所有的分区副本的内容是都是相同的,如果leader发生故障时,会自动将其中一个follower提升为leader,保证了系统的容错性、高可用性
    • ISR(in-syncreplica)机制:需要同步复制保存的folower
      分区副本分为了两类,一个是ISR,与leader副本同步保存数据,另外一个普通的副本,是异步同步数据,当leader挂掉之后,会优先从ISR副本列表中选取一个作为leader
  • kafka 如何实现高性能?

    • 消息分区:不受单台服务器的限制,可以不受限的处理更多的数据;我们往一个Topic发送消息或者读取消息时,实际内部是多个Partition在并行处理
    • 顺序读写:磁盘顺序读写,提升读写效率
    • 页缓存:把磁盘中的数据缓存到内存中,把对磁盘的访问变为对内存的访问
    • 零拷贝:减少上下文切换及数据拷贝??在读写数据中也减少CPU拷贝文件的次数
    • 消息压缩:减少磁盘I0和网络I0
    • 分批发送:将消息打包批量发送,减少网络开销
  • kafka消息积压问题

    • 消息积压根本原因是:生产者的生产速度,大于消费者的消费速度。consumer还来不及从PageCache中消费消息,producer就已经生产消息把容量占完了,只能把PageCache中的消息保存到硬盘中,consumer要到硬盘中取出未消费的消息、、
    • 遇到消息积压问题时,我们需要先排查,是不是有bug产生了。如果不是bug,我们可以优化一下消费的逻辑,比如之前是一条一条消息消费处理的话,我们可以确认是不是可以优为批量处理消息。如果还是慢,我们可以考虑水平扩容,增加Topic的队列数,和消费组机器的数量,提升整体消费能力。
    • 如果是bug导致几百万消息持续积压几小时,需要解决bug,临时紧急扩容,大概思路如下:
      1. 先修复consumer消费者的问题,以确保其恢复消费速度,然后将现有consumer 都停掉。
      2. 新建一个 topic:partition 是原来的 10 倍,临时建立好原先10倍的queue 数量。
      3. 然后写一个临时的分发数据的 consumer 程序,这个程序部署上去消费积压的数据,消费之后不做耗时的处理,直接均匀轮询写入临时建立好的10倍数量的queue。
      4. 接着临时征用 10倍的机器来部署 consumer,每一批 consumer 消费一个临时 queue 的数据。这种做法相当于是临时将 queke 资源和 consumer 资源扩大 10倍,以正常的 10倍速度来消费数据。
      5. 等快速消费完积压数据之后,得恢复原先部署的架构,重新用原先的 consumer 机器来消费消息。

网 络

  • 计算机网络:https://leo710aka.github.io/2022/02/28/%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C/

  • 浏览器发出一个请求到收到响应经历了哪些步骤?
    1,浏览器解析用户输入的URL,生成一个HTTP格式的请求
    2,先根据URL域名从本地hosts文件查找是否有映射IP,如果没有就将域名发送给电脑所配置的DNS进行域名解析,得到IP地址
    3,浏览器通过操作系统将请求通过四层网络协议发送出去
    4,途中可能会经过各种路由器、交换机,最终到达服务器
    5,服务器收到请求后,根据请求所指定的端口,将请求传递给绑定了该端口的应用程序,比如8080被tomcat占用了
    6,tomcat接收到请求数据后,按照http协议的格式进行解析,解析得到所要访问的servlet
    7,然后servlet来处理这个请求,如果是pringMVC中的DispatcherServet,那么则会找到对应的Controller中的方法,并执行该方法得到结果
    8,Tomcat得到响应结果后封装成HTTP响应的格式,并再次通过网络发送给浏览器所在的服务器
    9,浏览器所在的服务器拿到结果后再传递给浏览器,浏览器则负责解析并洁染

  • 介绍 TCP/IP 模型。

    1. 链路层(Link Layer):负责物理介质上的数据传输,处理硬件相关的问题。主要包括网络驱动、接口卡、物理介质等。作用:实现原始比特流的传输,通过物理链路进行直接通信。
    2. 网络层(Internet Layer):提供数据包在不同网络之间的传输和路由。主要包括IP、ICMP等。作用:实现不同网络之间的通信,处理数据包的寻址和路由问题。
    3. 传输层(Transport Layer):提供端到端的通信,处理数据流控制、差错校验和恢复等问题。主要包括TCP和UDP。作用:确保数据的可靠传输,处理数据的分段、排序和重组。
    4. 应用层(Application Layer): 提供网络服务和应用软件之间的接口。包括各种应用层协议,如HTTP、FTP、SMTP等。作用:为用户提供网络服务,支持各种应用软件的运行。
  • OSI七层模型从底层到顶层分别是:

    1. 物理层(Physical Layer): 定义物理设备和传输媒体的规范,如电缆、光纤、网卡等。
    2. 数据链路层(Data Link Layer): 提供可靠的点对点通信,处理相邻节点之间的数据链路错误和流控制。
    3. 网络层(Network Layer): 负责在整个网络中找到数据的最佳路径,处理不同网络上的路由和转发。
    4. 传输层(Transport Layer): 提供端到端的通信,负责数据的可靠传输,处理流量控制和差错恢复。
    5. 会话层(Session Layer): 管理用户会话和数据交换,提供对话控制和管理。
    6. 表示层(Presentation Layer): 负责数据格式的转换、加密和压缩,确保一个系统的应用层能理解另一个系统的数据。
    7. 应用层(Application Layer): 提供用户接口和网络服务,是用户直接与网络交互的地方,包括文件传输、电子邮件、远程登录等。
  • HTTP请求常⻅的状态码和字段

    • 常见的HTTP状态码:
      1xx(信息性状态码): 100 Continue:继续。客户端应继续其请求。
      2xx(成功状态码): 200 OK:请求成功。一般用于GET和POST请求。 201 Created:已创建。成功请求并创建了新的资源。 204 No Content:无内容。服务器成功处理请求,但没有返回任何内容。
      3xx(重定向状态码): 301 Moved Permanently:永久重定向。请求的资源已被永久移动到新位置。 302 Found:临时重定向。请求的资源临时移动到新位置。
      4xx(客户端错误状态码): 400 Bad Request:请求无效。服务器无法理解请求的语法。401 Unauthorized:未授权。需要身份验证或权限不足。 403 Forbidden:禁止访问。服务器理解请求,但拒绝执行。 404 Not Found:请求的资源不存在。
      5xx(服务器错误状态码): 500 Internal Server Error:服务器内部错误。通用错误消息,服务器遇到意外的情况。 502 Bad Gateway:网关错误。服务器作为网关或代理,从上游服务器接收到无效的响应。 503 Service Unavailable:服务不可用。服务器目前无法处理请求。
    • 常见的HTTP字段:
      1. 通用字段: Cache-Control:控制缓存行为。 Connection:控制是否保持连接。Date:表示消息创建时间。
      2. 请求字段: Host:指定被请求资源的主机名和端口号。User-Agent:表示客户端信息。
      3. 响应字段: Server:表示服务器信息。 Content-Type:指定响应内容的MIME类型。Content-Length:响应内容长度。
      4. 实体字段:ETag:用于检测资源是否发生改变。 Last-Modified:表示资源的最后修改时间。
  • HTTP中的主要方法及其作用:

    1. GET:从服务器获取资源。GET 方法请求指定的资源,并返回响应主体。它只用于检索数据,而不会修改服务器状态。
    2. POST:向服务器提交数据。POST 方法用于向服务器提交数据,通常用于创建新资源、提交表单数据、上传文件等操作。
    3. PUT:向服务器存储资源。PUT 方法用于向服务器上传或更新指定的资源,通常用于更新现有资源的内容。
    4. DELETE:从服务器删除资源。DELETE 方法用于请求服务器删除指定的资源。
    5. PATCH:对资源进行局部更新。PATCH 方法用于对现有资源进行局部更新,只更新部分内容而不是整个资源。
    6. HEAD:获取资源的元数据。HEAD 方法类似于 GET 方法,但只返回资源的头部信息而不返回响应主体,通常用于获取资源的元数据,如大小、类型等。
    7. OPTIONS:获取服务器支持的方法。OPTIONS 方法用于请求服务器列出对指定资源支持的 HTTP 方法列表,以及其他一些元数据信息。
    8. TRACE:追踪请求的传输路径。TRACE 方法用于测试发送到服务器的请求在其传输路径上的变化,通常用于诊断和调试。
    9. CONNECT:与目标服务器建立代理连接。CONNECT 方法用于建立客户端与目标服务器之间的代理连接,通常用于建立安全的TLS/SSL连接。
  • HTTP1.0 和 HTTP1.1 的区别?

    1. ⻓连接
      HTTP1.1 ⽀持⻓连接,每⼀个TCP连接上可以传送多个HTTP请求和响应,默认开启 Connection:Keep-Alive;
      HTTP1.0 默认为短连接,每次请求都需要建⽴⼀个TCP连接。
    2. 缓存
      HTTP1.0 主要使⽤ If-Modified-Since/Expires 来做为缓存判断的标准
      HTTP1.1 则引⼊了更多的缓存控制策略,如 Entity tag / If-None-Match 等更多可供选择的缓存头来控制缓存策略。
    3. 管道化:基于 HTTP1.1 的⻓连接,使得请求管线化成为可能。管线化使得请求能够“并⾏”传输,但是响应必须按照请求发出的顺序依次返回,性能在⼀定程度上得到改善
    4. 增加Host字段: 使得⼀个服务器能够⽤来创建多个 Web 站点。
    5. 状态码: 新增了24个错误状态响应码
    6. 带宽优化
      HTTP1.0 中,存在⼀些浪费带宽的现象,例如客户端只是需要某个对象的⼀部分,⽽服务器却将整个对象送过来了,并且不⽀持断点续传功能
      HTTP1.1 则在请求头引⼊了 range 头域,它允许只请求资源的某个部分,即返回码是 206(Partial Content)
  • HTTP2.0 与 HTTP1.1 的区别?

    1. ⼆进制分帧:在应⽤层 (HTTP/2.0) 和传输层(TCP or UDP) 之间增加⼀个⼆进制分帧层,从⽽突破 HTTP1.1 的性能限制,改进传输性能,实现低延迟和⾼吞吐量。
    2. 多路复⽤(MultiPlexing):允许同时通过单⼀的 HTTP/2 连接发起多重的请求-响应消息,这个强⼤的功能则是基于“⼆进制分帧”的特性。
    3. ⾸部压缩: HTTP1.1 不⽀持 header 数据的压缩,HTTP/2.0 使⽤ HPACK 算法对 header 的数据进⾏压缩,这样数据体积⼩了,在⽹络上传输就会更快。⾼效的压缩算法可以很⼤的压缩 heeader ,减少发送包的数量从⽽降低延迟。
    4. 服务端推送(server push): 在 HTTP/2 中,服务器可以对客户端的⼀个请求发送多个响应,即服务器可以额外的向客户端推送资源,⽽⽆需客户端明确的请求。
  • ??HTTPS的⼯作原理?(https是怎么建⽴连接的)

    1. ⾸先,客户端向服务器端发送请求报⽂,请求与服务端建⽴连接。
    2. 服务端产⽣⼀对公私钥,然后将⾃⼰的公钥发送给CA机构,CA机构也有⼀对公私钥,然后CA机构使⽤⾃⼰的私钥将服务端发送过来的公钥进⾏加密,产⽣⼀个CA数字证书。
    3. 服务端响应客户端的请求,将CA机构⽣成的数字证书发送给客户端。
    4. 客户端将服务端发送过来的数字证书进⾏解析(因为浏览器产商跟CA机构有合作,所以浏览器中已经保存了⼤部分CA机构的密钥,⽤于对服务端发送过来的数字证书进⾏解密),验证这个数字证书是否合法,如果不合法,会发送⼀个警告。如果合法,取出服务端⽣成的公钥。
    5. 客户端取出公钥并⽣成⼀个随机码key(其实就是对称加密中的密钥)
    6. 客户端将加密后的随机码key发送给服务端,作为接下来的对称加密的密钥
    7. 服务端接收到随机码key后,使⽤⾃⼰的私钥对它进⾏解密,然后获得到随机码key。
    8. 服务端使⽤随机码key对传输的数据进⾏加密,在传输加密后的内容给客户端
    9. 客户端使⽤⾃⼰⽣成的随机码key解密服务端发送过来的数据,之后,客户端和服务端通过对称加密传输数据,随机码Key作为传输的密钥。
  • HTTPS与HTTP的区别
    HTTP 是明⽂传输,⽽ HTTPS 通过 SSL\TLS 进⾏了加密
    HTTP 的端⼝号是 80,HTTPS 是 443
    HTTPS 需要到 CA 申请证书
    HTTP 的连接简单,是⽆状态的;HTTPS 协议是由 SSL+HTTP 协议构建的可进⾏加密传输、身份认证的安全的⽹络协议。

  • 常⻅的请求⽅式?GET 和 POST 请求的区别?

    1. 作⽤不同: GET⽤于从服务端获取资源,POST⼀般⽤来向服务器端提交数据
    2. 参数传递⽅式不同: GET请求的参数⼀般写在URL中,且只接受ASCII字符;POST请求参数⼀般放在请求体中,对于数据类型也没有限制
    3. 安全性不同: 因为参数传递⽅式的不同,所以两者安全性不同,GET请求的参数直接暴露在URL中,所以更不安全,不能⽤来传递敏感信息。
    4. 参数⻓度限制不同
      GET传送的数据量较⼩,不能⼤于2KB。POST传送的数据量较⼤,⼀般被默认为不受限制。
      HTTP 协议没有 Body 和 URL 的⻓度限制,对 URL 限制的⼤多是浏览器和服务器的原因。
    5. 编码⽅式不同
      GET 请求只能进⾏ URL 编码(application/x-www-form-urlencoded)
      POST ⽀持多种编码⽅式(application/x-www-form-urlencoded 或 multipart/form-data。为⼆进制数据使⽤多种编码。)
    6. 缓存机制不同
      GET 请求会被浏览器主动cache,⽽ POST 不会,除⾮⼿动设置。
      GET 请求参数会被完整保留在浏览器历史记录⾥,⽽ POST 中的参数不会被保留。
      GET 产⽣的 URL 地址可以被 保存为书签,⽽ POST 不可以。
      GET 在浏览器回退时是⽆害的,⽽ POST 会再次提交请求。
    7. 时间消耗不同
      GET 产⽣⼀个 TCP 数据包;POST 产⽣两个 TCP 数据包。
      对于 GET ⽅式的请求,浏览器会把 header 和 data ⼀并发送出去,服务器响应 200(返回数据);⽽对于 POST,浏览器先发送 Header,服务器响应 100 continue,浏览器再发送 data,服务器响应 200 ok(返回数据)
    8. 幂等:意思是多次执⾏相同的操作,结果都是「相同」的。
      GET ⽅法就是安全且幂等的,因为它是「只读」操作,⽆论操作多少次,服务器上的数据都是安全的,且每次的结果都是相同的。
      POST 因为是「新增或提交数据」的操作,会修改服务器上的资源,所以是不安全的,且多次提交数据就会创建多个资源,所以不是幂等的。
  • ??什么是强缓存和协商缓存

    • 缓存可以解决什么问题:
      减少不必要的⽹络传输,节约带宽
      更快的加载⻚⾯
      减少服务器负载,避免服务过载的情况出现
    • 强缓存:浏览器判断请求的⽬标资源是否有效命中强缓存,如果命中,则可以直接从内存中读取⽬标资源,⽆需与服务器做任何通讯。
      • Expires强缓存 :设置⼀个强缓存时间,此时间范围内,从内存中读取缓存并返回,因为 Expires 判断强缓存过期的机制是获取本地时间戳,与之前拿到的资源⽂件中的Expires字段的时间做⽐较。来判断是否需要对服务器发起请求。这⾥有⼀个巨⼤的漏洞:“如果我本地时间不准咋办?”所以⽬前已经被废弃了。
      • Cache-Control强缓存 : http1.1 中增加该字段,只要在资源的响应头上写上需要缓 存多久就好了,单位是秒。 Cache-Control:max-age=N , 有max-age、s-maxage、 no-cache、no-store、private、public这六个属性。
        max-age决定客户端资源被缓存多久。 s-maxage决定代理服务器缓存的时⻓。 no-cache表示是强制进⾏协商缓存。 no-store是表示禁⽌任何缓存策略。 public表示资源既可以被浏览器缓存也可以被代理服务器缓存。 private表示资源只能被浏览器缓存,默认为private
    • 基于 last-modified 的协商缓存
      ⾸先需要在服务器端读出⽂件修改时间,将读出来的修改时间赋给响应头的last-modified字段。最后设置Cache-control:no-cache,当客户端读取到last-modified的时候,会在下次的请求标头中携带⼀个字段:If-ModifiedSince,⽽这个请求头中的If-Modified-Since就是服务器第⼀次修改时候给他的时间。之后每次对该资源的请求,都会带上If-Modified-Since这个字段,⽽服务端就需要拿到这个时间并再次读取该资源的修改时间,让他们两个做⼀个⽐对来决定是读取缓存还是返回新的资源。
      缺点:
      因为是更具⽂件修改时间来判断的,所以,在⽂件内容本身不修改的情况下,依然有可能更新⽂件修改时间(⽐如修改⽂件名再改回来),这样,就有可能⽂件内容明明没有修改,但是缓存依然失效了。当⽂件在极短时间内完成修改的时候(⽐如⼏百毫秒)。因为⽂件修改时间记录的最⼩单位是秒,所以,如果⽂件在⼏百毫秒内完成修改的话,⽂件修改时间不会改变,这样,即使⽂件内容修改了,依然不会返回新的⽂件。
    • 基于 ETag 的协商缓存:
      将原先协商缓存的⽐较时间戳的形式修改成了⽐较⽂件指纹(根 据⽂件内容计算出的唯⼀哈希值)。第⼀次请求某资源的时候,服务端读取⽂件并计算出⽂件指纹,将⽂件指纹放在响应头的 Etag字段中跟资源⼀起返回给客户端。第⼆次请求某资源的时候,客户端⾃动从缓存中读取出上⼀次服务端返回的ETag也就是⽂件指纹。并赋给请求头的if-None-Match字段,让上⼀次的⽂件指纹跟随请求⼀起回到服务端。
      服务端拿到请求头中的if-None-Match字段值(也就是上⼀次的⽂件指纹),并再次读取⽬标资源并⽣成⽂件指纹,两个指纹做对⽐。如果两个⽂件指纹完全吻合,说明⽂件没有被改变,则直接返回304状态码和⼀个空的响应体并return。如果两个⽂件指纹不吻合,则说明⽂件被更改,那么将新的⽂件指纹重新存储到响应头的ETag中并返回给客户端。
      缺点:
      ETag需要计算⽂件指纹这样意味着,服务端需要更多的计算开销。。如果⽂件尺⼨⼤,数量多,并且计算频繁,那么ETag的计算就会影响服务器的性能。显然,ETag在这样的场景下就不是很适合。ETag有强验证和弱验证,所谓将强验证,ETag⽣成的哈希码深⼊到每个字节。哪怕⽂件中只有⼀个字节改变了,也会⽣成不同的哈希值,它可以保证⽂件内容绝对的不变。但是,强验证⾮常消耗计算量。ETag还有⼀个弱验证,弱验证是提取⽂件的部分属性来⽣成哈希值。因为不必精确到每个字节,所以他的整体速度会⽐强验证快,但是准确率不⾼。会降低协商缓存的有效性。
    • 有哈希值的⽂件设置强缓存即可。没有哈希值的⽂件(⽐如index.html)设置协商缓存。
  • DNS、、

    • DNS(Domain Name System)域名管理系统,是当⽤户使⽤浏览器访问⽹址之后,使⽤的第⼀个重要协议。DNS 要解决的是域名和 IP 地址的映射问题。
    • 查询过程:
      1. ⾸先⽤户在浏览器输⼊URL地址后,会先查询浏览器缓存是否有该域名对应的IP地址。
      2. 如果浏览器缓存中没有,会去计算机本地的Host⽂件中查询是否有对应的缓存。
      3. 如果Host⽂件中也没有则会向本地的DNS解析器(通常由你的互联⽹服务提供商(ISP)提供)发送⼀个DNS查询请求。
      4. 如果本地DNS解析器没有缓存该域名的解析记录,它会向根DNS服务器发出查询请求。根DNS服务器并不负责解析域名,但它能告诉本地DNS解析器应该向哪个顶级域(.com/.net/.org)的DNS服务器继续查询。
      5. 本地DNS解析器接着向指定的顶级域DNS服务器发出查询请求。顶级域DNS服务器也不负责具体的域名解析,但它能告诉本地DNS解析器应该前往哪个权威DNS服务器查询下⼀步的信息。
      6. 本地DNS解析器最后向权威DNS服务器发送查询请求。 权威DNS服务器是负责存储特定域名和IP地址映射的服务器。当权威DNS服务器收到查询请求时,它会查找”example.com”域名对应的IP地址,并将结果返回给本地DNS解析器。
      7. 本地DNS解析器将收到的IP地址返回给浏览器,并且还会将域名解析结果缓存在本地,以便下次访问时更快地响应
  • HTTP多个TCP连接怎么实现???
    多个tcp连接是靠某些服务器对 Connection: keep-alive 的 Header 进⾏了⽀持。简⽽⾔之,完成这个 HTTP 请求之后,不要断开 HTTP 请求使⽤的 TCP 连接。这样的好处是连接可以被重新使⽤,之后发送 HTTP 请求的时候不需要重新建⽴ TCP 连接,以及如果维持连接,那么 SSL 的开销也可以避免

  • TCP的三次握手和四次挥手

    • 在建立TCP连接时,需要通过三次握手来建立,过程是:
      1,客户端向服务端发送一个SYN
      2,服务端接收到SYN后,给客户端发送一个SYN_ACK
      3,客户端接收到SYN_ACK后,再给服务端发送一个ACK
    • 在断开TCP连接时,需要通过四次挥手来断开,过程是
      1,客户端向服务端发送FIN
      2,服务端接收FIN后,向客户燃发送ACK,表示我接收到了断开连接的请求,客户端你可以不发数据了,不过服务端这边可能还有数据正在处理
      3,服务端处理完所有数据后,向客户端发送FIN,表示服务端现在可以断开连接
      4,客户端收到服务端的FIN,向服务端发送ACK,表示客户端也会断开连接了
  • 三次握⼿的过程,以及为什么是三次,⽽不是四次,两次?

    • 三次握⼿的过程如下:
      1. 客户端向服务器发送 SYN 报⽂、初始化序列号 ISN(seq=x),然后客户端进⼊ SYN_SEND 状态,等待服务器确认。
      2. 服务端发送 ACK 确认服务端的 SYN 报⽂ (ack=x+1) 同时发出⼀个 SYN 报⽂,带上⾃⼰的初始化序列号 (seq=y ),然后服务端进⼊ SYN_RECV 状态。
      3. 客户端接收到服务端的 SYN、ACK 报⽂,ACK确认服务端的 SYNC 报⽂ (ACK=y+1) ,然后客户端和服务器端都进⼊ ESTABLISHED 状态,完成 TCP 三次握⼿
    • 为什么不是四次握⼿? 为什么不能两次握⼿?
      因为三次握⼿才能保证双⽅具有接收和发送的能⼒。 两次握⼿可能导致资源的浪费,由于没有第三次握⼿,服务端就⽆法确认客户端是否收到了⾃⼰的回复,所以每收到⼀个 SYN ,服务器都会主动去建⽴⼀个连接, ⽽四次握⼿可以优化为三次。
  • 四次挥⼿的过程,以及为什么是四次?

    • 四次挥⼿的过程:
      1. 客户端发送⼀个 FIN 报⽂给服务端,表示⾃⼰要断开数据传送,报⽂中会指定⼀个序列号 (seq=x) 。然后,客户端进⼊ FIN-WAIT-1 状态。
      2. 服务端收到 FIN 报⽂后,回复 ACK 报⽂给客户端,且把客户端的序列号值 +1 ,作为ACK + 1 报⽂的序列号 (seq=x+1) 然后,服务端进⼊ CLOSE-WAIT (seq=x+1) 状态,客户端进⼊ FIN-WAIT-2 状态。
      3. 服务端也要断开连接时,发送 FIN 报⽂给客户端,且指定⼀个序列号 (seq=y+1) ,随后服务端进⼊ LAST-ACK 状态。
      4. 客户端收到 FIN 报⽂后,发出 ACK 报⽂进⾏应答,并把服务端的序列号值 +1 作为 ACK 报⽂序列号 (seq=y+2) 。此时客户端进⼊ TIME-WAIT 状态。服务端在收到客户端的 ACK 报⽂后进⼊ CLOSE 状态。如果客户端等待 2MSL 没有收到回复,才关闭连接
    • 为什么是四次挥⼿?
      TCP 是全双⼯通信,可以双向传输数据。任何⼀⽅都可以在数据传送结束后发出连接释放的通知,待对⽅确认后进⼊半关闭状态。 当另⼀⽅也没有数据再发送的时候,则发出连接释放通知,对⽅确认后才会完全关闭了 TCP 连接。 总结:两次握⼿可以释放⼀端到另⼀端的 TCP 连接,完全释放连接⼀共需要四次握⼿
    • 为什么会有 TIME_WAIT 状态?发生在客户端还是服务端?
      1. 确保数据可靠传输:在四次挥手中,最后一次挥手由服务端发送 FIN 报文给客户端,表示服务端已经关闭了连接。此时客户端进入 TIME_WAIT 状态,等待一段时间(通常是2倍的最大报文段生存时间(MSL)),确保服务端接收到最后一个 ACK 报文,以保证数据的可靠传输。
      2. 处理可能的重传数据包:在网络中可能会出现一些延迟到达的数据包或者服务端未收到客户端的确认报文的情况。如果客户端在收到服务端的 FIN 报文后直接关闭连接而不进入 TIME_WAIT 状态,那么如果服务端在关闭连接后的一段时间内重传了 FIN 报文,客户端就无法正确处理这个重传的 FIN 报文,可能会导致连接异常。
        TIME_WAIT 状态一般在客户端设置。这是因为在 TCP 协议中,客户端主动发起连接并主动关闭连接的情况比较常见,因此客户端需要等待一段时间确保连接关闭的可靠性。服务端在关闭连接后会直接进入 CLOSED 状态,不需要等待,因为服务端已经没有发送数据的需求,只需等待客户端的最后一个 ACK 确认即可。通常,操作系统会对 TIME_WAIT 状态的持续时间进行配置,以便合理利用系统资源并确保连接关闭的可靠性。
  • TCP与UDP的概念,特点,区别和对应的使⽤场景?

    • TCP与UDP的概念
      TCP(传输控制协议)是⼀种⾯向连接的、可靠的、基于字节流的传输层通信协议。
      UDP(⽤户数据报协议)为应⽤程序提供了⼀种⽆需建⽴连接就可以发送封装的IP数据包的⽅法。
    • 特点
      TCP:⾯向连接,传输可靠,传输形式为字节流,传输效率慢,所需资源多。
      UDP:⽆连接、传输不可靠、传输形式为数据报⽂段,传输效率快,所需资源少。
    • 区别
      1. 连接:TCP 是⾯向连接的传输层协议,传输数据前先要建⽴连接。UDP 是不需要连接,即刻传输数据。
      2. 服务对象:TCP 是⼀对⼀的两点服务,即⼀条连接只有两个端点。UDP ⽀持⼀对⼀、⼀对多、多对多的交互通信
      3. 是否有状态:TCP 传输是有状态的,它会去记录⾃⼰发送消息的状态⽐如消息是否发送了、是否被接收了等等,⽽ UDP 是⽆状态的。
      4. 可靠性:TCP可靠交付数据,数据可以⽆差错、不丢失、不重复、按需到达。UDP尽最⼤努⼒,不保证可靠交付数据。
        TCP在传递数据之前,会有三次握⼿来建⽴连接;在数据传递时,有确认、窗⼝、重传、拥塞控制机制,保证数据传输的安全性。UDP数据传递不需要给出任何确认,且不保证数据不丢失及到达顺序;没有拥塞控制,即使⽹络⾮常拥堵了,也不会影响 UDP 的发送速率。
      5. ⾸部开销:TCP ⾸部⻓度较⻓,会有⼀定的开销,⾸部在没有使⽤「选项」字段时是 20 个字节,如果使⽤了「选项」字段则会变⻓的 (20 ~ 60字节)。UDP ⾸部只有 8 个字节,并且是固定不变的,开销较⼩。
      6. 传输⽅式:TCP 是⾯向字节流的,UDP ⾯向报⽂。TCP 是流式传输,没有边界,但保证顺序和可靠。UDP 是⼀个包⼀个包的发送,是有边界的,但可能丢包和乱序。
      7. 传输效率:由于TCP 传输的时候多了连接、确认重传等机制,所以TCP 的传输效率要⽐UDP 低。
      8. 分⽚不同:
        • TCP 的数据⼤⼩如果⼤于 MSS ⼤⼩,则会在传输层进⾏分⽚,⽬标主机收到后,也同样在传输层组装 TCP 数据包,如果中途丢失了⼀个分⽚,只需要传输丢失的这个分⽚。
        • UDP 的数据⼤⼩如果⼤于 MTU ⼤⼩,则会在 IP 层进⾏分⽚,⽬标主机收到后,在 IP 层组装完数据,接着再传给传输层,但是如果中途丢了⼀个分⽚,在实现可靠传输的 UDP 时则就需要重传所有的数据包,这样传输效率⾮常差,所以通常 UDP 的报⽂应该⼩于 MTU。
    • 对应的使⽤场景
      TCP常⽤于要求通信数据可靠场景(如⽹⻚浏览、⽂件传输、邮件传输、远程登录、数据库操作等)。
      UDP常⽤于要求通信速度⾼场景(如域名转换、视频直播、实时游戏等)。
  • TCP 的 Keepalive 和 HTTP 的 Keep-Alive 是⼀个东⻄吗?

    • HTTP 的 Keep-Alive,是由应⽤层(⽤户态) 实现的,称为 HTTP ⻓连接;
      每次请求都要经历这样的过程:建⽴ TCP -> 请求资源 -> 响应资源 -> 释放连接,这就是HTTP短连接,但是这样每次建⽴连接都只能请求⼀次资源,所以HTTP 的 Keep-Alive实现了使⽤同⼀个 TCP 连接来发送和接收多个 HTTP 请求/应答,避免了连接建⽴和释放的开销,就就是 HTTP ⻓连接。
    • TCP 的 Keepalive,是由 TCP 层(内核态) 实现的,称为 TCP 保活机制;通俗地说,就是TCP有⼀个定时任务做倒计时,超时后会触发任务,内容是发送⼀个探测报⽂给对端,⽤来判断对端是否存活。
  • TCP连接如何确保可靠性?

    1. 数据块⼤⼩控制: 应⽤数据被分割成TCP认为最合适发送的数据块,再传输给⽹络层,数据块被称为报⽂段或段。
    2. 序列号:TCP给每个数据包指定序列号,接收⽅根据序列号对数据包进⾏排序,并根据序列号对数据包去重。
    3. 校验和:TCP将保持它⾸部和数据的校验和。这是⼀个端到端的检验和,⽬的是检测数据在传输中的任何变化。若收到报⽂的检验和有差错,TCP将丢弃这个报⽂段和不确认收到此报⽂段。
    4. 流量控制: TCP连接的每⼀⽅都有固定⼤⼩的缓冲空间,TCP的接收端只允许发送端发送接收端缓冲区能接纳的数据。当接收⽅来不及处理发送⽅的数据,能提示发送⽅降低发送的速率,防⽌包丢失。TCP利⽤滑动窗⼝实现流量控制。
    5. 拥塞控制: 当⽹络拥塞时,减少数据的发送。
    6. 确认应答: 通过 ARQ 协议实现。基本原理是每发完⼀个分组就停⽌发送,等待对⽅确认。如果没收到确认,会重发数据包,直到确认后再发下⼀个分组。
    7. 超时重传: 当TCP发出⼀个数据段后,它启动⼀个定时器,等待⽬的端确认收到这个报⽂段。如果不能及时收到⼀个确认,将重发这个报⽂段。
  • 既然提到了拥塞控制,那你能说说说拥塞控制是怎么实现的嘛
    拥塞控制算法主要有以下⼏种:

    1. 慢启动: 在连接刚开始时,发送⽅会逐渐增加发送窗⼝⼤⼩,从⽽以指数增⻓的速度增加发送的数据量。
    2. 拥塞避免: ⼀旦慢启动阶段过去,发送⽅进⼊拥塞避免阶段。在这个阶段,发送⽅逐渐增加发送窗⼝的⼤⼩,但增加速率较慢,避免过快增加导致⽹络拥塞。
    3. 超时重传: 如果发送⽅在超时时间内未收到确认,它会认为数据包丢失,并重传这些数据包。这是拥塞控制的最后⼿段,⽤于检测和处理⽹络中的丢包或拥塞情况。当⽹络出现拥塞,也就是会发⽣数据包重传
    4. 快速重传(Fast Retransmit)和快速恢复(Fast Recovery): 当发送⽅发送的数据包丢失或⽹络出现拥塞时,接收⽅会发送重复确认(duplicate ACK)通知 发送⽅有数据包丢失。当发送⽅收到⼀定数量的重复确认时,它会⽴即重传丢失的数据包,⽽不是等待超时。这样可以减少⽹络的拥塞程度。
    5. 拥塞窗⼝调整: 发送⽅根据⽹络的拥塞程度动态调整发送窗⼝的⼤⼩,通过监测⽹络延迟和丢包情况来确定合适的发送速率,以避免⽹络拥塞。
  • TCP 传送数据的过程、、

    1. 建立连接:在 TCP 通信开始前,发送方和接收方需要通过三次握手建立连接。这个过程包括发送方发送 SYN 报文段给接收方,接收方回复 SYN+ACK 报文段给发送方,最后发送方发送 ACK 报文段给接收方,建立起双向的通信连接。
    2. 数据分割:发送方将应用层的数据流分割成适当大小的数据块,每个数据块称为一个报文段(Segment),每个报文段都带有序列号,用于标识数据的顺序。(TCP 基于字节流传输,这种字节流传输的方式是在 TCP 协议的抽象层面上实现的,实际传输时,TCP 使用报文段作为传输单位来保证数据的可靠传输。)
    3. 发送数据:发送方根据 TCP 的滑动窗口机制和拥塞控制算法,将报文段发送给接收方。发送方根据接收方的确认情况和网络情况动态调整发送速率和窗口大小,确保数据的可靠传输和网络的稳定性。
    4. 接收数据:接收方接收到报文段后,会进行确认,即发送 ACK 报文段给发送方,确认接收到的数据。如果有丢失或损坏的数据,接收方会要求发送方重新发送数据。
    5. 数据重组:接收方根据报文段的序列号和数据部分将数据重组成完整的数据流,然后交给应用层进行处理。
    6. 连接关闭:当数据传输完成后,发送方和接收方会通过四次挥手的方式关闭连接。发送方发送 FIN 报文段给接收方,表示不再发送数据;接收方回复 ACK 报文段确认,并发送 FIN 报文段给发送方;发送方再次发送 ACK 报文段给接收方,完成连接关闭。
  • Cookie 和 Session 是什么?有什么区别?

    • 都⽤于管理⽤户的状态和身份, Cookie 通过在客户端记录信息确定⽤户身份, Session 在服务器端记录信息确定⽤户身份。
    • Cookie:是存储在⽤户浏览器中的⼩型⽂本⽂件,⼀般为⼏ KB,⽤于在⽤户和服务器之间传递数据。通常,服务器会将⼀个或多个 Cookie 发送到⽤户浏览器,然后浏览器将这些 Cookie 存储在本地。
      服务器在接收到来⾃客户端浏览器的请求之后,就能够通过分析存放于请求头的Cookie得到客户端特有的信息,从⽽动态⽣成与该客户端相对应的内容。
    • Session:客户端浏览器访问服务器的时候,服务器把客户端信息以某种形式记录在服务器上。这就是 Session,主要⽤于维护⽤户登录状态、存储⽤户的临时数据和上下⽂信息等。存储容量较⼤,通常没有固定限制,取决于服务器的配置和资源。
    • 安全性:由于 Cookie 存储在⽤户浏览器中,因此可以被⽤户读取和篡改。相⽐之下,Session 数据存储在服务器上,更难被⽤户访问和修改。
    • 传输⽅式:Cookie 在每次 HTTP 请求中都会被⾃动发送到服务器,⽽ Session ID 通常通过 Cookie 或 URL 参数传递。
  • WEB 负载均衡方案。
    是通过合理分配网络流量,将请求均匀分发到多个服务器上,以提高系统的性能、可用性和可伸缩性。
    1、硬件负载均衡:通常是专门设计用于处理负载均衡任务的设备。这些设备可以位于网络流量的前端,根据预定义的算法和策略将请求分发给后端的多个服务器。硬件负载均衡的优势在于它可以处理大量的并发连接,同时提供高性能和低延迟。性能优越: 硬件负载均衡器通常具有高性能的硬件处理能力,能够应对大量请求。丰富的算法: 支持多种负载均衡算法,如轮询、加权轮询、最小连接数等。高可用性: 支持冗余配置,以确保负载均衡设备本身的高可用性。
    2、软件负载均衡:通常是通过在服务器上运行特定的负载均衡软件来实现的,这些软件可以在普通服务器上运行,也可以部署在云环境中。软件负载均衡器通常提供一系列配置选项,以便根据特定需求进行定制。
    3、DNS负载均衡:可以将域名映射到多个IP地址,实现流量的分发。DNS负载均衡的优点在于其简单性和低成本,但在实际应用中可能会受到DNS缓存的影响,导致流量分发不均匀。
    4、网络负载均衡:通过网络设备(如交换机、路由器)进行流量分发。这种方式通常涉及更底层的网络配置,可以在不同层次上实现负载均衡。
    适用于需要处理大规模流量的场景。配置和管理较为复杂,需要深入理解网络结构。

  • 192.168.1.0 分成 4 个子网,子网掩码是什么?

    • 子网掩码不能单独存在,它必须结合IP地址一起使用。IP地址我们都知道是计算机在网络内的唯一标识,而子网掩码顾名思义是用于划分子网的
      子网掩码只有一个作用,就是将某个IP地址划分成网络地址和主机地址两部分。子网掩码是一个32位地址,用于屏蔽IP地址的一部分以区别网络标识和主机标识,并说明该IP地址是在局域网上,还是在远程网上。
    • 默认的子网掩码255.255.255.0:将该子网掩码的二进制由24个1和8个0组成,8个0表示该子网掩码划分出的子网容量为256(2的8次方),也就是说192.168.1.0-255都在同一个子网中,这256个地址中可用地址只有254个,因为规定每个子网的第一个IP地址为网段地址,最后一个IP地址为广播地址,都不可用。
    • 某个小型公司有四个部门,每个部门各40台计算机接入公司局域网交换机,如果要在192.168.1.0网段为每个部门划分子网,子网掩码应该怎么设置,每个子网的地址范围分别是什么?
      • 192.168.1.0网段共256个地址,划分4个子网,每个子网需要64个地址;64是2的6次方,子网掩码应该以6个0结尾,剩下的用1补齐,由26个1和6个0组成,转换成十进制是255.255.255.192;
      • 每个子网共64个IP地址,掐头去尾后可用地址只有62个,第1个子网的可用IP地址范围是:192.168.1.1-62,第2个子网可用IP地址范围是192.168.1.65-126,第3个子网的可用IP地址范围是:192.168.1.129-190,第4个子网可用IP地址范围是192.168.1.193-254;
      • 各部门计算机按照上面各子网的IP地址范围进行设置,所有计算机的子网掩码都必须设置为255.255.255.192,设置完毕后各部门内的计算机能正常联网,不同部门间的计算机无法直接联通。

操 作 系 统

  • 进程与线程?

    • 进程是程序的执行实例,拥有独立的内存空间和系统资源,是操作系统进行资源分配和调度的基本单位。是系统进⾏资源分配和调度的基本单位。
    • 线程 Thread 是进程内的独立执行单元,共享相同的内存空间和资源,是进程的子执行单元。是操作系统能够进⾏运算调度的最⼩单位。
      线程⼀个进程⾄少有⼀个线程,⼀个进程可以运⾏多个线程,这些线程共享同⼀块内存。
    • 资源开销:
      进程:由于每个进程都有独⽴的内存空间,创建和销毁进程的开销较⼤。进程间切换需要保存和恢复整个进程的状态,因此上下⽂切换的开销较⾼。
      线程:线程共享相同的内存空间,创建和销毁线程的开销较⼩。线程间切换只需要保存和恢复少量的线程上下⽂,因此上下⽂切换的开销较⼩。
    • ??通信与同步:
      进程:由于进程间相互隔离,进程之间的通信需要使⽤⼀些特殊机制,如管道、消息队列、共享 内存等。
      线程:由于线程共享相同的内存空间,它们之间可以直接访问共享数据,线程间通信更加⽅便。
    • 安全性:
      进程:由于进程间相互隔离,⼀个进程的崩溃不会直接影响其他进程的稳定性。
      线程:由于线程共享相同的内存空间,⼀个线程的错误可能会影响整个进程的稳定性。
  • 为什么进程切换比线程切换开销大?

    1. 地址空间切换: 进程拥有独立的地址空间,进程切换时需要切换地址空间,包括虚拟内存、页表等,而线程之间共享地址空间,线程切换时不需要这种开销。
    2. 资源切换: 进程拥有独立的资源,如文件描述符、堆栈、全局变量等,进程切换时需要保存和恢复这些资源,而线程之间共享资源,线程切换时开销较小。
    3. 上下文切换: 进程切换时需要保存和恢复更多的上下文信息,包括进程控制块(PCB)、寄存器、栈指针等,而线程切换时只需保存和恢复部分上下文信息。
    4. 系统调用开销: 进程切换时可能需要进行系统调用,如切换到内核态、更新进程状态等,而线程切换时可以在用户态完成。
    5. 同步和通信开销: 进程之间的通信和同步需要额外的开销,如信号量、消息队列等,而线程之间可以通过共享内存等更高效的方式进行通信和同步。
  • 进程调度算法你了解多少?
    进程调度算法是操作系统中⽤来管理和调度进程(也称为任务或作业)执⾏的⽅法。这些算法决定了在多任务环境下,如何为各个进程分配 CPU 时间,以实现公平性、⾼吞吐量、低延迟等不同的调度⽬标。

    1. 先来先服务调度算法: 按照进程到达的先后顺序进⾏调度,即最早到达的进程先执⾏,直到完成或阻塞。
    2. 最短作业优先调度算法: 优先选择运⾏时间最短的进程来运⾏
    3. ⾼响应⽐优先调度算法: 综合考虑等待时间和服务时间的⽐率,选择具有最⾼响应⽐的进程来执⾏
    4. 时间⽚轮转调度算法: 将 CPU 时间划分为时间⽚(时间量),每个进程在⼀个时间⽚内运⾏,然后切换到下⼀个进程。
    5. 最⾼优先级调度算法: 为每个进程分配⼀个优先级,优先级较⾼的进程先执⾏。这可能导致低优先级进程⻓时间等待, 可能引发饥饿问题。
    6. 多级反馈队列调度算法: 将进程划分为多个队列,每个队列具有不同的优先级,进程在队列之间移动。具有更⾼优先级的 队列的进程会更早执⾏,⽽⻓时间等待的进程会被提升到更⾼优先级队列。
    7. 最短剩余时间优先: 每次选择剩余执⾏时间最短的进程来执⾏。
    8. 最⼤吞吐量调度: 旨在最⼤化单位时间内完成的进程数量
  • 进程间通信
    进程间通信(IPC)可以通过多种方式实现,包括管道、消息队列、信号量、共享内存等。这些机制允许不同进程之间交换数据和同步操作。

    1. 管道:是⼀种半双⼯的通信⽅式,数据只能单向流动⽽且只能在具有⽗⼦进程关系的进程间使⽤。
    2. 命名管道: 也是半双⼯的通信⽅式,但是它允许⽆亲缘关系进程间的通信。
    3. 信号量:是⼀个计数器,可以⽤来控制多个进程对共享资源的访问,常作为⼀种锁机制,防⽌某进程正在访问共享资源时,其他进程也访问该资源。因此主要作为进程间以及同⼀进程内不同线程之间的同步⼿段。
    4. 消息队列:消息队列是消息的链表,存放在内核中并由消息队列标识符标识。消息队列克服了信号传递信息少、管道只能承载⽆格式字节流以及缓冲区⼤⼩受限等缺点。
    5. 信号:⽤于通知接收进程某个事件已经发⽣,从⽽迫使进程执⾏信号处理程序。
    6. 共享内存:就是映射⼀段能被其他进程所访问的内存,这段共享内存由⼀个进程创建,但多个进程都可以访问。共享内存是最快的进程通信⽅式,它是针对其他进程间通信⽅式运⾏效率低⽽专⻔设计的。它往往与其他通信机制,⽐如信号量配合使⽤,来实现进程间的同步和通信。
    7. Socket 套接字:是⽀持TCP/IP 的⽹络通信的基本操作单元,主要⽤于在客户端和服务器之间通过⽹络进⾏通信
  • 什么是死锁?如何避免死锁?
    死锁是指两个或多个进程在争夺系统资源时,由于互相等待对⽅释放资源⽽⽆法继续执⾏的状态。死锁只有同时满⾜以下四个条件才会发⽣:
    1、互斥条件:⼀个进程占⽤了某个资源时,其他进程⽆法同时占⽤该资源。
    2、请求保持条件:⼀个线程因为请求资源⽽阻塞的时候,不会释放⾃⼰的资源。
    3、不可剥夺条件:资源不能被强制性地从⼀个进程中剥夺,只能由持有者⾃愿释放。
    4、环路等待条件:多个进程之间形成⼀个循环等待资源的链,每个进程都在等待下⼀个进程所占有的资源。
    只需要破坏上⾯⼀个条件就可以破坏死锁。
    破坏请求与保持条件:⼀次性申请所有的资源。
    破坏不可剥夺条件:占⽤部分资源的线程进⼀步申请其他资源时,如果申请不到,可以主动释放它占有的资源。
    破坏循环等待条件:靠按序申请资源来预防。让所有进程按照相同的顺序请求资源,释放资源则反序释放。

  • 多进程与多线程区别:

    • 多进程是在不同的地址空间中执行,相互独立,通信相对复杂,但更稳定。
    • 多线程共享相同的地址空间,通信更方便,但需要考虑同步和竞态条件,可能导致不稳定性。
  • 解释⼀下⽤户态和核⼼态
    ⽤户态 User Mode 和核⼼态 Kernel Mode ,是操作系统中两种不同的执⾏模式,⽤于控制进程或程序对计算机硬件资源的访问权限和操作范围。
    ⽤户态:进程或程序只能访问受限的资源和执⾏受限的指令集,不能直接访问操作系统的核⼼部分,也不能直接访问硬件资源,⽤户态下的 CPU 不允许独占,也就是说 CPU 能够被其他程序获取。
    核⼼态:核⼼态是操作系统的特权级别,允许进程或程序执⾏特权指令和访问操作系统的核⼼部分。在核⼼态下,进程可以直接访问硬件资源,执⾏系统调⽤,管理内存、⽂件系统等操作。处于内核态的 CPU 可以从⼀个程序切换到另外⼀个程序,并且占⽤ CPU 不会发⽣抢占情况,⼀般处于特权级 0 的状态我们称之为内核态

  • 用户态切换到内核态。
    是通过系统调用(System Call)实现的。下面是大致的步骤:
    1、触发系统调用: 当用户程序需要执行一些特权操作(例如文件操作、网络访问、内存分配等),需要请求操作系统的帮助。用户程序通过系统调用指令触发这一请求。在 x86 架构中,通常使用 int 0x80syscall 指令来触发系统调用。
    2、切换到内核态: 当发生系统调用时,处理器会由用户态切换到内核态。这个切换的过程中,处理器的特权级别会提升,从而允许操作系统执行一些受保护的指令。
    3、系统调用处理: 操作系统的内核接收到系统调用请求后,会根据请求的类型执行相应的系统调用服务例程。这些服务例程负责完成用户程序请求的操作,可能涉及到内核中的数据结构和硬件资源。
    4、返回用户态: 完成系统调用服务例程后,操作系统将控制权返回给用户程序,同时降低处理器的特权级别,将其切换回用户态。这个过程中,操作系统可能会将系统调用的结果返回给用户程序。

  • 解释⼀下进程同步和互斥,以及解决方法

    • 进程同步是指多个并发执⾏的进程之间协调和管理它们的执⾏顺序,以确保它们按照⼀定的顺序或时间间隔执⾏。⽐如说,你想要和你的队友⼀起完成⼀个副本,你们需要相互配合,有时候等待对⽅的信号或者消息,有时候按照对⽅的要求执⾏某些动作,这就是进程同步。
    • 互斥指的是在某⼀时刻只允许⼀个进程访问某个共享资源。当⼀个进程正在使⽤共享资源时,其他进程不能同时访问该资源。⽐如说,你想要使⽤⼀个祭坛来祈愿,但是这个祭坛⼀次只能被⼀个⼈使⽤,如果有其他⼈也想要使⽤,他们就必须等待你使⽤完毕后再去使⽤,这就是进程互斥。
    • 解决进程同步和互斥的问题有很多种⽅法,其中⼀种常⻅的⽅法是使⽤信号量和 PV 操作。信号量是⼀种特殊的变量,它表示系统中某种资源的数量或者状态。PV 操作是⼀种对信号量进⾏增加或者减少的操作,它们可以⽤来控制进程之间的同步或者互斥。
    • 举个例⼦,假设有⼀个信号量 s 表示⼀个祭坛是否可⽤,初始值为 1。如果 s 的值为 1,表示祭坛空闲;如果 s 的值为 0,表示祭坛被占⽤;如果 s 的值为 -1,表示有⼀个⼈在等待使⽤祭坛。那么我们可以⽤ PV 操作来实现对祭坛的互斥访问:
      如果你想要使⽤祭坛,你就执⾏ P(s) 操作,将 s 的值减 1。如果结果为 0 或者正数,表示你可以使⽤祭坛;如果结果为负数,表示有⼈在使⽤祭坛,你就必须等待。
      如果你使⽤完了祭坛,你就执⾏ V(s) 操作,将 s 的值加 1。如果结果为正数或者 0 ,表示没有⼈在等待使⽤祭坛;结果为负数,表示有⼈在等待使⽤祭坛,你就需要唤醒他们中的⼀个。
      这样就可以保证每次只有⼀个⼈能够使⽤祭坛,实现了进程互斥。
    • 除此之外,下⾯的⽅法也可以解决进程同步和互斥问题:
      临界区(Critical Section): 将可能引发互斥问题的代码段称为临界区。为了实现互斥,每个进程在进⼊临界区前必须获取⼀个锁,退出临界区后释放该锁。这确保同⼀时间只有⼀个进程可以进⼊临界区。
      互斥锁(Mutex): 互斥锁是⼀种同步机制,⽤于实现互斥。每个共享资源都关联⼀个互斥锁,进程在访问该资源前需要先获取互斥锁,使⽤完后释放锁。只有获得锁的进程才能访问共享资源。
      条件变量(Condition Variable): 条件变量⽤于在进程之间传递信息,以便它们在特定条件下等待或唤醒。通常与互斥锁⼀起使⽤,以确保等待和唤醒的操作在正确的时机执⾏
  • 什么是中段和异常?它们有什么区别?

    • 中断和异常是两种不同的事件,它们都会导致CPU暂停当前的程序执⾏,转⽽去执⾏⼀个特定的处理程序。
    • 中断和异常的区别主要有以下⼏点:
      中断是由外部设备或其他处理器产⽣的,它们通常是异步的,也就是说,它们可以在任何时候发⽣,与当前执⾏的指令⽆关。例如,键盘输⼊、⿏标移动、⽹络数据到达等都会产⽣中断信号,通知CPU去处理这些事件。
      异常是由CPU内部产⽣的,它们通常是同步的,也就是说,它们只会在执⾏某些指令时发⽣,与 当前执⾏的指令有关。例如,除法运算时除数为零、访问⾮法内存地址、执⾏⾮法指令等都会产⽣异常信号,通知CPU去处理这些错误或故障。
      中断可以被屏蔽或禁⽌,这意味着CPU可以通过设置某些标志位或寄存器来忽略或延迟响应某些中断信号。这样可以避免中断过于频繁或⼲扰重要的任务。
      异常不能被屏蔽或禁⽌,这意味着CPU必须⽴即响应异常信号,并进⾏相应的处理。这样可以保证程序的正确性和系统的稳定性。
  • 介绍⼀下⼏种典型的锁

    • 两个基础的锁:
      互斥锁:互斥锁是⼀种最常⻅的锁类型,⽤于实现互斥访问共享资源。在任何时刻,只有⼀个线程可以持有互斥锁,其他线程必须等待直到锁被释放。这确保了同⼀时间只有⼀个线程能够访问被保护的资源。
      ⾃旋锁:⾃旋锁是⼀种基于忙等待的锁,即线程在尝试获取锁时会不断轮询,直到锁被释放。
    • 其他的锁都是基于这两个锁的
      读写锁:允许多个线程同时读共享资源,只允许⼀个线程进⾏写操作。分为读(共享)和写(排他)两种状态。
      悲观锁:认为多线程同时修改共享资源的概率⽐较⾼,所以访问共享资源时候要上锁
      乐观锁:先不管,修改了共享资源再说,如果出现同时修改的情况,再放弃本次操作。
  • 你知道的线程同步的⽅式有哪些?
    线程同步机制是指在多线程编程中,为了保证线程之间的互不⼲扰,⽽采⽤的⼀种机制。常⻅的线程同步机制有以下⼏种:

    1. 互斥锁:互斥锁是最常⻅的线程同步机制。它允许只有⼀个线程同时访问被保护的临界区(共享资源)
    2. 条件变量:条件变量⽤于线程间通信,允许⼀个线程等待某个条件满⾜,⽽其他线程可以发出信号通知等待线程。通常与互斥锁⼀起使⽤。
    3. 读写锁: 读写锁允许多个线程同时读取共享资源,但只允许⼀个线程写⼊资源。
    4. 信号量:⽤于控制多个线程对共享资源进⾏访问的⼯具
  • 什么是内存分段和分⻚?作⽤是什么?

    • 内存分段是将⼀个程序的内存空间分为不同的逻辑段 segments ,每个段代表程序的⼀个功能模块或数据类型,如代码段、数据段、堆栈段等。每个段都有其⾃⼰的⼤⼩和权限。
    • 内存分⻚是把整个虚拟和物理内存空间分成固定⼤⼩的⻚(如4KB)。这样⼀个连续并且尺⼨固定的内存空间,我们叫⻚ Page
    • 作⽤:
      1. 逻辑隔离: 内存分段和分⻚都实现了程序的逻辑隔离,使不同的功能模块或数据类型能够被单独管理和保护,提⾼了程序的可靠性和安全性。
      2. 内存保护: 通过将不同的段或⻚⾯设置为只读、可读写、不可执⾏等权限,操作系统可以确保程序不会越界访问或修改其他段的内容,从⽽提⾼了系统的稳定性。
      3. 虚拟内存: 分段和分⻚都有助于实现虚拟内存的概念,允许应⽤程序认为它们在使⽤的是⼀个⽐实际物理内存更⼤的内存空间。
      4. 内存共享: 通过分⻚,操作系统可以实现内存⻚⾯的共享,从⽽节省内存空间,多个进程可以共享相同的代码或数据⻚⾯。
      5. 内存管理: 分⻚更加灵活,允许操作系统将不同进程的⻚⾯分散存放在物理内存中,从⽽提⾼内存利⽤率。分段则更适⽤于管理不同的逻辑模块。
    • 分段与分⻚的区别
      分⻚对⽤户不可⻅,分段对⽤户可⻅
      分⻚的地址空间是⼀维的,分段的地址空间是⼆维的
      分⻚(单级⻚表)、分段访问⼀个逻辑地址都需要两次访存,分段存储中可以引⼊快表机制
      分段更容易实现信息的共享和保护(纯代码或可重⼊代码可以共享)
    • 分段与分⻚优缺点:
      分⻚管理: 内存空间利⽤率⾼,不会产⽣外部碎⽚,只会有少量的⻚内碎⽚。但是不⽅便按照逻辑模块实现信息的共享和保护 。
      分段管理: 很⽅便按照逻辑模块实现信息的共享和保护。但是如果段⻓过⼤,为其分配很⼤的连续空间会很不⽅便,段式管理会产⽣外部碎⽚
  • 解释⼀下⻚⾯置换算法,例如LRU(最近最少使⽤)、FIFO(先进先出)等

    • 假设你的⼿机内存有限,只能同时运⾏四个原神的⻆⾊。当你想切换到⼀个新的⻆⾊时,你需要从内存中换出⼀个旧的⻆⾊,以便为新的⻆⾊腾出空间。不同的⻚⾯置换算法就相当于不同的切换策略,例如:
      LRU(最近最少使⽤)算法:每次选择最⻓时间没有被使⽤的⻆⾊进⾏切换。这种策略基于你对⻆⾊的喜好,认为最近被使⽤过的⻆⾊很可能还会被使⽤,⽽最久未被使⽤的⻆⾊很可能不会再 被使⽤。 LRU算法可以有效地减少切换次数,但是实现起来⽐较复杂,需要记录每个⻆⾊的使⽤时间或者维护⼀个使⽤顺序的列表。
      FIFO(先进先出)算法:每次选择最早进⼊内存的⻆⾊进⾏切换。这种策略很简单,只需要维护⼀个⻆⾊队列,每次淘汰队⾸的⻆⾊,然后把新的⻆⾊加⼊队尾。但是FIFO算法可能会淘汰⼀些经常被使⽤的⻆⾊,导致切换次数增加。⽽且FIFO算法有可能出现⻉拉迪异常(Belady anomaly),即当分配给内存的空间增加时,切换次数反⽽增加
    • 常⻅⻚⾯置换算法有最佳置换算法(OPT)、先进先出(FIFO)、最近最久未使⽤算法(LRU)、时钟算法(Clock)
      1. 最佳置换算法: 该算法根据未来的⻚⾯访问情况,选择最⻓时间内不会被访问到的⻚⾯进⾏置换。那么就有⼀个问题了,未来要访问什么⻚⾯,操作系统怎么知道的呢?操作系统当然不会知道,所以这种算法只是⼀种理想情况下的置换算法,通常是⽆法实现的。
      2. 先进先出算法:也就是最先进⼊内存的⻚⾯最先被置换出去。这个算法⽐较简单明了,就不过多 解释了。但是先进先出算法会存在⼀个问题,就是Belady问题,即随着分配给进程的空闲⻚⾯数 增加,缺⻚的情次反⽽也会增加。 这和我们常识是相悖的,因为我们通常认为如果⼀个进程经常 发⽣缺⻚,那么就应该应该为他多分配⼀点内存。然⽽使⽤FIFO算法时,反⽽可能导致更多缺⻚ 情况出现。这就是Belady问题,Belady问题只会在使⽤FIFO算法时出现。
      3. 最近最久未使⽤算法:LRU算法基于⻚⾯的使⽤历史,通过选择最⻓时间未被使⽤的⻚⾯进⾏置 换。LRU算法的核⼼思想是,最近被访问的⻚⾯可能在未来被再次访问,⽽最⻓时间未被访问的 ⻚⾯可能是最不常⽤的,因此将其置换出去可以腾出空间给新的⻚⾯。LRU算法通常是使⽤⼀个 数据结构去维护⻚⾯的使⽤历史,维护使⽤历史就是通过访问字段实现的。访问字段的位数和操 作系统分配给该进程的⻚⾯数有关,⽐如分配4个⻚⾯,访问字段就是2位,16个⻚⾯,访问字段 就是4位,依次类推。如此,每⼀个⻚⾯的访问字段都可以不同,通过访问字段的不同,我们就 可以判断⻚⾯的使⽤历史。
      4. 时钟算法:Clock算法基于⼀个环形链表或者循环队列数据结构来管理⻚⾯的访问情况,⽤于选 择被置换的⻚⾯。Clock算法的核⼼思想是通过使⽤⼀个指针(称为时钟指针)在环形链表上遍历, 检查⻚⾯是否被访问过。这个访问过同样需要我们上⾯说到的访问字段来表示,此时访问字段只 有⼀位。每个⻚⾯都与⼀个访问位相关联,标记该⻚⾯是否被访问过。 当需要进⾏⻚⾯置换时,Clock算法从时钟指针的位置开始遍历环形链表。 如果当前⻚⾯的访问 位为0,表示该⻚⾯最久未被访问,可以选择进⾏置换。将访问位设置为1,继续遍历下⼀个⻚ ⾯。 如果当前⻚⾯的访问位为1,表示该⻚⾯最近被访问过,它仍然处于活跃状态。将访问位设 置为0,并继续遍历下⼀个⻚⾯如果遍历过程中找到⼀个访问位为0的⻚⾯,那么选择该⻚⾯进⾏ 置换。
  • CPU 飙高系统反应慢怎么排查?
    CPU 是整个电脑的核心计算资源,对于一个应用进程来说,CPU 的最小执行单元是线程。导致 CPU 飙高的原因有几个方面:
    1、CPU 上下文切换过多,对于 CPU 来说,同一时刻下每个 CPU 核心只能运行一个线程,如果有多个线程要执行,CPU 只能通过上下文切换的方式来执行不同的线程。较多的上下文切换会占据大量 CPU 资源,从而使得 cpu 无法去执行用户进程中的指令,导致响应速度下降。在 Java 中,文件 IO、网络 IO、锁等待、线程阻塞等操作都会造成线程阻塞从而触发上下文切换
    2、CPU 资源过度消耗,也就是在程序中创建了大量的线程,或者有线程一直占用CPU 资源无法被释放,比如死循环!CPU 利用率过高之后,导致应用中的线程无法获得 CPU 的调度,从而影响程序的执行效率
    3、既然是这两个问题导致的 CPU 利用率较高,于是我们可以通过 top 命令,找到 CPU利用率较高的进程,再通过 Shift+H 找到进程中 CPU 消耗过高的线程,有两种情况:
    CPU 利用率过高的线程一直是同一个,说明程序中存在线程长期占用 CPU 没有释放的情况,这种情况直接通过jstack 获得线程的 Dump 日志,定位到线程日志后就可以找到问题的代码。
    CPU 利用率过高的线程 id 不断变化,说明线程创建过多,需要挑选几个线程id通过 jstack 去线程 dump 日志中排查。
    4、最后有可能定位的结果是程序正常,只是在 CPU 飙高的那一刻,用户访问量较大,导致系统资源不够。


分布式

  • 分布式、SOA、微服务之间有什么关系和区别?
    1,分布式架构是指将单体架构中的各个部分拆分,然后部署不同的机器或进程中去,SOA和微服务都是分布式架构的
    2,SOA 是一种面向服务的架构,系统的所有服务都注册在总线上,当调用服务时,从总线上查找服务信息,然后调用
    3,微服务是一种更彻底的面向服务的架构,将系统中各个功能个体抽成一个个小的应用程序,基本保持一个应用对应的一个服务的架构

  • 微服务架构?
    微服务是一种软件开发架构风格,用于构建复杂应用程序。它将大型应用程序拆分成一系列较小、独立的服务,每个服务专注于完成特定的业务功能。这些服务之间通过轻量级的通信机制(通常是基于 HTTP 或 RPC)进行交互,可以独立部署、扩展和管理。

  • 微服务的主要特点包括:

    • 单一责任:每个微服务专注于执行一个明确定义的业务功能。这使得开发人员可以更容易地理解和维护服务。
    • 松耦合:微服务之间是独立的,它们可以使用不同的编程语言、技术堆栈和数据存储。这种松耦合使得开发团队能够独立地开发、测试和部署各个服务。
    • 独立部署:每个微服务都可以独立地部署,这意味着当对一个服务进行更改时,不需要重新部署整个应用程序。这提高了开发和发布的速度,并允许快速迭代和灵活性。
    • 弹性扩展:由于每个微服务是独立的,可以根据需要对它们进行独立的扩展。这使得应用程序能够更好地处理高负载情况,并具有更好的可伸缩性。
    • 有限上下文:每个微服务维护自己的数据存储,这意味着它们可以使用不同类型的数据库或存储技术。这种隔离有助于减少整个系统的复杂性,并提高可靠性。
  • 微服务架构也存在一些挑战和缺点:

    • 分布式系统复杂性:微服务架构中的服务是分布式的,需要处理服务间通信、数据一致性、错误处理等问题。这增加了系统的复杂性,需要更多的设计和管理工作。
    • 服务间通信开销:由于微服务架构中的服务通过网络通信进行交互,会增加一定的延迟和开销。此外,需要实现适当的通信机制和协议来确保可靠性和数据一致性。
    • 运维复杂性:微服务架构中涉及多个独立的服务,每个服务都需要独立进行监控、日志记录和故障排除。这增加了运维的复杂性,需要适当的工具和自动化来管理和监控服务。
    • 数据一致性:由于每个微服务维护自己的数据存储,确保数据一致性变得更加困难。在跨多个服务的业务操作中,需要采取适当的策略和技术来保证数据的一致性和完整性。
  • CAP?

    • CAP 理论指出,在分布式系统中,无法同时满足三个核心属性:一致性(Consistency)、可用性(Availability)和分区容忍性(Partition Tolerance),只能选择其中的两个。
      • 一致性:在分布式系统中的所有节点上,对于某个操作的结果应该是一致的。换句话说,当一个节点执行了更新操作后,其他节点应该能够读取到更新后的值。这意味着所有节点在同一时间看到的数据都是相同的。
      • 可用性:系统在面对用户请求时,应该保证能够进行响应,即系统保持可用状态。即使部分节点发生故障,系统仍然能够继续提供服务。
      • 分区容忍性:系统能够在面对网络分区或者节点之间通信失败的情况下,仍然能够继续运行。即使系统中的某些节点之间无法进行通信,整个系统仍然可以继续工作。
  • Dubbo的架构设计是怎样的?
    Dubbo 中的架构设计是非常优秀的,分为了很多层次,并且每层都是可以扩展的,比如:
    1.Proxy服务代理层,支持JDK动态代理、javassist等代理机制
    2.Registry注册中心层,支持Zookeeper、Redis等作为注册中心
    3.Protocol远程调用层,支持Dubbo、Http等调用协议
    4.Transport网络传输层,支持netty、mina等网络传输框架
    5,Serialize数据序列化层,支持JSON、Hessian等序列化机制

  • 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 更为灵活,服务提供方和调用方的依赖只依靠一纸契约,不存在代码级别的强依赖。

  • 关于“你对 Spring Cloud 的理解”
    Spring Cloud 是一套分布式微服务的技术解决方案,它提供了快速构建分布式系统的常用的一些组件比如说配置管理、服务的注册与发现、服务调用的负载均衡、资源隔离、熔断降级等等;不过 Spring Cloud 只是 Spring 官方提供的一套微服务标准定义,而真正的实现目前有两套体系用的比较多。
    Spring Cloud Alibaba 是基于阿里巴巴开源组件集成的一套微服务解决方案,包括:1. Dubbo————消息通讯 2. Nacos————服务注册与发现 3.Seata————事务隔离 4. Sentinel————熔断降级
    有了 Spring Cloud 这样的技术生态,使得我们在落地微服务架构时。不用去考虑第三方技术集成带来额外成本,只要通过配置组件来完成架构下的技术问题,从而可以让我们更加侧重性能方面

  • 弄懂 RPC。
    常见的远程通信方式,有基于 REST 架构的 HTTP 协议、以及 RPC 框架。

    1. 什么是远程调用
      远程调用是指跨进程的功能调用,跨进程可以理解成一个计算机节点的多个进程,或者多个计算机节点的多个进程
    2. 什么是 RPC
      全称为 Remote Procedure Call,翻译过来就是远程过程调用,它是一种通过网络从远程计算机程机程序上请求服务,而不需要了解底层网络技术的协议,凡是符合该协议的框架,我们都可以称它为 RPC 框架。关于 RPC 协议,通俗来讲就是,A 计算机提供一个服务,B 计算机可以像调用本地服务那样调用 A 计算机的服务。要实现 RPC,需要通过网络传输数据,并对调用的过程进行封装。现在比较流行的 RPC 框架,都会采用 TCP 作为底层传输协议。RPC 强调的是过程调用,调用的过程对用户而言是是透明的,用户不需要关心调用的细节,可以像调用本地服务一样调用远程服务。
    3. RPC 的运用场景和优势
      分布式架构的核心,就是利用多个普通计算机节点,组成一个庞大而复杂的计算网络,提供高性能以及高并发的能力支撑。在分布式架构中,原本的单体应用被拆分成多个独立部署的服务分部在计算机网络上,这些服务必然需要通过网络进行数据交互。而 RPC 框架就是解决在分布式架构中的各个业务服务彼此的网络通信问题。
      一般来说,RPC 服务主要是针对大型的企业,也就说当我们业务复杂度以及用户量都比较高时,需要解耦服务,扩展性强、部署灵活一般市面上开源的 PRC 框架,除了提供基础的远程通信功能以外,还会在性能消耗、传输效率、服务治理等方面做很多的设计,比如阿里开源的 RPC 框架 Dubbo。
  • 负载均衡的实现算法

    • 负载均衡是指将网络流量或工作负载分配到多个服务器或计算资源上,以提高系统的性能、可靠性和可扩展性。在实现负载均衡时,通常会采用以下算法:
      • 轮询(Round Robin):按照轮询的方式依次将请求分发给后端服务器。每个请求按照顺序依次分配给不同的服务器,循环往复。这种算法简单且均衡,适用于服务器性能相似且无状态的情况。
      • 最少连接(Least Connection):根据当前连接数选择连接数最少的服务器来处理新的请求。这种算法可以有效地将负载均衡到连接数较少的服务器上,以保持各服务器的负载相对均衡。
      • IP哈希(IP Hash):根据客户端的 IP 地址进行哈希计算,将同一个 IP 地址的请求发送到同一个后端服务器。这样可以确保同一个客户端的请求都发送到同一台服务器上,适用于需要保持会话一致性的场景。
        加权轮询(Weighted Round Robin):给每个服务器分配一个权重值,根据权重值的比例来分配请求。具有较高权重的服务器会接收到更多的请求,适用于服务器性能不均衡的情况。
      • 加权最少连接(Weighted Least Connection):根据服务器的当前连接数和权重值来选择服务器。连接数越少且权重值越高的服务器会被优先选择。
      • 随机(Random):随机选择一个后端服务器来处理请求。这种算法简单且均衡,但无法保证每个服务器的负载一致。
      • 响应时间加权(Response Time Weighted):根据服务器的平均响应时间或处理时间来分配请求。响应时间较短的服务器会得到更多的请求,以提高系统整体的响应速度。
  • 分布式事务的原理
    分布式事务是指事务的参与者、支持事务的服务器、资源服务器以及事务管理器分别位于分布式系统的不同节点之上。比如大型的电商系统中的下单场景,会涉及到扣库存、优惠促销计算、订单 ID 生成。通常情况下,库存、促销、主键生成策略都位于不同的服务器和数据库表中。下单接口的成功与否,不仅取决于本地节点的数据库操作,而且还依赖第三方系统的结果,这时候分候分布式事务就保证这些操作要么全部成功,要么全部失败。
    因此,本质上来说,分布式事务就是为了保证不同数据库的数据一致性。基于 CAP 定理可以知道,对于上述情况产生的分布式事务问题,我们要么采用强一致性方案、要么采用弱一致性方案。
    ??

  • 实现分布式锁的解决方案中,你认为 Zookeeper和 Redis 哪种更好?
    两种方案都有各自的优缺点:
    对于 redis 的分布式锁而言,它有以下缺点: 它获取锁的方式简单粗暴,如果获取不到锁,会不断尝试获取锁,比较消耗性能。 Redis 是 AP 模型,在集群模式中由于数据的一致性会导致锁出现问题,即便使用Redlock 算法来实现,在某些复杂场景下,也无法保证其实现 100%的可靠性。不过在实际开发中使用 Redis 实现分布式锁还是比较常见,而且大部分场情况下不会遇到”极端复杂的场景“,更重要的是 Redis 性能很高,在高并发场景中比较合适。
    对于 zk 分布式锁而言: zookeeper 天生设计定位就是分布式协调,强一致性。锁的模型健壮、简单易用、适合做分布式锁。 如果获取不到锁,只需要添加一个监听器就可以了,不用一直轮询,性能消耗较小。如果要在两者之间做选择,就我个人而言的话,比较推崇 ZK 实现的锁,因为对于分布式锁而言,它应该符合 CP 模型,但是 Redis 是 AP 模型,所以在这个点上,Zookeeper会更加合适。

  • 什么是Docker?有什么优势?

  • Docker 核心组件。

  • 什么是 Kubernetes?
    近些年,随着 Docker 与微服务的普及,K8s 也乘着这两股东风,迅速蹿红起来。作为最火的容器编排工具之一,它的很多思想设计都契合了微服务和云原生应用的设计法则。也正因如此,越来越多的公司开始使用起 k8s。K8s 全称 Kubernetes,8 是中间 8 个字母的简称。作为一种容器自动部署、扩容以及管理的技术,我们可以简单理解其是一种容器编排技术。前身是 Borg 系统,在谷歌内部已经有了十多年的使用经验。
    ,、、

  • 负载均衡的诞生背景
    在互联网发展早期,由于用户量较少、业务需求也比较简单。对于软件应用,我们只需要一台高配的服务器即可完成业务的支撑,这样的软件架构称为单体架构
    随着用户量的增加,服务器的请流量也随之增加,在这个过程中单体架构会产生两个问题。
    1.软件的性能逐步下降,访问延迟越来越高
    2.容易出现单点故障
    为了解决这个问题,我们引入了集群化部署的架构,也就是把一个软件应用同时部署在多个服务器上。引入了负载均衡的设计,简单来说,负载均衡机制的核心目的是让客户端的请求合理均匀的分发到多台目标服务器,由于请求被多个节点分发,使得服务端的性能得到有效的提升。

  • 如何实现负载均衡呢?
    1、基于 DNS 实现负载均衡:实现方式比较简单,只需要在 DNS服务器上针对某个域名做多个 IP 映射即可。
    2、基于硬件实现负载均衡
    3、基于软件实现负载均衡

  • 负载均衡的常用算法
    所谓负载均衡算法,就是决定当前客户端请求匹配到目标服务器集群中的具体哪个节点。常见的负载均衡算法有:
    1、轮训,也就是多个服务器按照顺序轮训返回,这样每个服务器都能获得相同的请求次数
    2、随机,根据随机算法获得一个目标服务地址,由于该算法具备随机性,因此每个服务器获得的请求数量不一定均等。
    3、一致性 hash,也就是对于具有相同 hash 码的请求,永远发送到同一个节点上。
    4、最小连接数,根据目标服务器的请求数量来决定请求分发的权重,也就是目标服务集群中,请求更少的节点将会获得更多的请求。这是负载均衡中比较好的策略,真正能够实现目标服务器的请求均衡。


Linux

  • 绝对路径用什么符号表示?当前目录、上层目录用什么表示?主目录用什么表示? 切换目录用什么命令?
    绝对路径: 如/etc/init.d
    当前目录和上层目录: ./ ../
    主目录: ~/
    切换目录: cd

  • Linux 下命令有哪几种可使用的通配符?分别代表什么含义?

    • “?” 可替代单个字符。例如,file?.txt 可以匹配 file1.txt、fileA.txt 等。
    • “*” 可替代任意多个字符。例如,*.txt 可以匹配所有以 .txt 结尾的文件。
    • 方括号 “[charset]” 可替代 charset 集中 的任何单个字符,例如 file[123].txt 可以匹配 file1.txt、file2.txt、file3.txt。
    • [a-zA-Z]+:[ ]+[0-9]* 代表:匹配一个或多个字母(大小写不敏感)、冒号、至少一个空格,然后是零个或多个数字。
      例子:Example: 123abc:
  • 正则表达式
    \s+ 表示一个或多个空格;
    \w:表示匹配任意字母、数字或下划线字符(即单词字符),等价于 [a-zA-Z0-9_]
    /a?/:表示字符 a 可以出现 0 次或 1 次,匹配空字符串或者单个字母 a
    i.:匹配一个字符 i 后面紧跟着任意字符。

  • 常用 linux 命令。
    执行退出: exit
    列出文件列表:ls[参数] -a 所有文件; -l 详细信息,包括大小字节数,可读可写可执行的权限等
    用于显示文件后几行内容:tail,例如: tail -n 1000:显示最后1000行
    打包:tar -xvf;打包并压缩:tar -zcvf
    查找字符串:grep
    显示当前所在目录:pwd
    创建空文件:touch
    编辑器:vim vi
    查看用过的命令列表:history

  • 目录命令。
    创建目录:mkdir
    移除目录:rmdir
    创建父目录的示例:mkdir -p
    创建带权限的目录:mkdir -m

  • 目录、文件命令。
    创建文件:典型的如 touch,vi 也可以创建文件,其实只要向一个不存在的文件输出,都会创建文件
    复制文件: cp
    移动文件,改名:mv
    删除文件,空文件夹:rm rmdir
    列出当前系统上已打开的文件(包括目录、文件、网络连接等)的相关信息:lsof 的名字是 “List Open Files” 的缩写。

  • 搜索文件用什么命令? 格式是怎么样的?
    find <指定目录> <指定条件> <指定动作>
    whereis 加参数与文件名
    locate 只加文件名
    find 直接搜索磁盘,较慢。
    find / -name “string*”

  • 查看文件的命令。

    • vi 文件名 #编辑方式查看,可修改
    • cat 文件名 #显示全部文件内容
    • more 文件名 #分页显示文件内容
    • less 文件名 #与 more 相似,更好的是可以往前翻页
      1
      2
      3
      4
      less log2013.log # 查看文件
      ps -ef | less # ps查看进程信息并通过less分页显示
      history | less # 查看命令历史使用记录并通过less分页显示
      less log2013.log log2014.log # 浏览多个文件
    • tail 文件名 #仅查看尾部,还可以指定行数
      1
      2
      3
      4
      5
      6
      7
      tail -n 10 test.log # 查询日志尾部最后10行的日志;
      tail -n +10 test.log # 查询10行之后的所有日志;
      tail -fn 10 test.log # 循环实时查看最后1000行记录(最常用的)
      # 一般还会配合着grep搜索用,例如 :
      tail -fn 1000 test.log | grep '关键字'
      # 如果一次性查询的数据量太大,可以进行翻页查看,例如 :
      tail -n 4700 aa.log |more -1000 可以进行多屏显示 (ctrl + f 或者 空格键可以快捷键)
    • head 文件名 #仅查看头部,还可以指定行数
      1
      2
      head -n 10 test.log 查询日志文件中的头10行日志;
      head -n -10 test.log 查询日志文件除了最后10行的其他所有日志;
    • sed 这个命令可以查找日志文件特定的一段 , 根据时间的一个范围查询,可以按照行号和时间范围查询按照行号
      1
      2
      sed -n '5,10p' filename # 这样你就可以只查看文件的第5行到第10行。
      sed -n '/2014-12-17 16:17:20/,/2014-12-17 16:17:36/p' test.log # 按照时间段
  • 查看日志的方法

    1. 查看系统日志、登录日志、应用程序日志:使用 catless 命令
      1
      cat /var/log/syslog
    2. 使用 dmesg 查看内核日志
    3. 使用 journalctl 查看 Systemd 日志
  • 查看进程线程的方法

    • windows
      任务管理器可以查看进程和线程数,也可以用来杀死进程
      tasklist 查看进程
      taskkill 杀死进程
    • linux
      ps -fe 查看所有进程
      ps -fT -p < PID> 查看某个进程(PID)的所有线程
      kill 杀死进程
      top 按大写 H 切换是否显示线程
      top -H -p < PID> 查看某个进程(PID)的所有线程
    • Java
      jps 命令查看所有 Java 进程
      jstack < PID> 查看某个 Java 进程(PID)的所有线程状态
      jconsole 来查看某个 Java 进程中线程的运行情况(图形界面)
  • Linux 中进程有哪几种状态?在 ps 显示出来的信息中分别用什么符号表示的?
    1、不可中断状态:进程处于睡眠状态,但是此刻进程是不可中断的。不可中断,指进程不响应异步信号。
    2、暂停状态/跟踪状态:向进程发送一个 SIGSTOP 信号,它就会因响应该信号 而进入 TATASK_STOPPED 状态;当进程正在被跟踪时,它处于 TATASK_TRACED 这个特殊的状态。正被跟踪”指的是进程暂停下来,等待跟踪它的进程对它进行操作。
    3、就绪状态:在 run_queue 队列里的状态
    4、运行状态:在 run_queue 队列里的状态
    5、可中断睡眠状态:处于这个状态的进程因为等待某某事件的发生(比如等待socket 连接、等待信号量),而被挂起
    6、zombie 状态(僵尸):父亲没有通过 wait 系列的系统调用会顺便将子进程的尸体(task_struct)也释放掉
    7、退出状态
    D 不可中断 Uninterruptible(usually IO)、R 正在运行,或在队列中的进程、S 处于休眠状态、T 停止或被追踪、Z 僵尸进程、W 进入内存交换(从内核 2.6 开始无效)、X 死掉的进程

  • 使用什么命令查看网络是否连通?使用什么命令查看 ip 地址及接口信息?
    natstat,ifconfig

  • 文件的权限可以用一串字符表示,其中”-rwxrw-rw-“是一个典型的例子。
    每个权限字符串由10个字符组成,分为四个部分:
    1、文件类型: 第一个字符表示文件的类型。包括:-:普通文件。d:目录。l:符号链接(软链接)
    2、用户权限: 接下来的三个字符表示文件所有者的权限。 r:读权限(Read) w:写权限(Write) x:执行权限(eXecute)
    3、组权限: 再接下来的三个字符表示文件所属组的权限。
    4、其他用户权限: 最后的三个字符表示其他用户的权限。
    在给定的例子”-rwxrw-rw-“中:文件类型是普通文件,文件所有者有读、写和执行权限,文件所属组有读和写权限,其他用户有读和写权限。

  • 文件唯一标识符(File Identifier)
    通常是通过文件的inode来实现的,而不是文件目录。
    inode是文件系统中用于唯一标识文件或目录的数据结构。每个文件或目录都有一个唯一的inode号码,它是一个整数值。

Pytorch

PyTorch是一个开源的Python机器学习库,基于Torch库,底层由C++实现,应用于人工智能领域,如计算机视觉和自然语言处理。它最初由Meta Platforms的人工智能研究团队开发,现在属于Linux基金会的一部分。它是在修改后的BSD许可证下发布的自由及开放源代码软件。

许多深度学习软件都是基于 PyTorch 构建的,包括特斯拉自动驾驶、Uber的Pyro、Hugging Face的Transformers、 PyTorch Lightning。

TensorBoard

打开localhost:6006,访问TensorBoard,查看TensorFlow模型的图形、损失函数、精度等信息。

1
2
3
4
5
from tensorboardX import SummaryWriter
writer = SummaryWriter("xxx") # 在文件夹里新建一个abc文件夹
writer.add_scalar("y=2i", 2*i, i) # 加坐标
writer.add_images("test_data", imgs, step) # 加图
writer.close()
1
2
# Pycharm -> Terminal
PS C:\Users\cf\Documents\Visual Studio Code\Python\learn_pytorch> tensorboard --logdir="xxx"

nn.Module

基本神经网络

1
2
3
4
5
class cf(nn.Module):
def __int__(self): super().__init__()
def forward(self, input): # 必须重写forword()
output = input + 1
return output

输入层(Input Layer):接受原始图像或数据作为输入。
卷积层(Convolutional Layer):进行特征提取。通过卷积操作,利用卷积核(或过滤器)从输入图像中提取特定的特征,如边缘、纹理等。卷积核会在整个图像上滑动,产生特征图。
卷积核的输入通道数与输入数据的通道数对应,卷积核的输出通道数与卷积核的数量对应。
卷积操作在神经网络中扮演着关键的角色,它通过特征提取、参数共享、空间局部性和参数数量控制等方面,使得卷积神经网络成为图像处理和计算机视觉任务中非常有效的工具。
池化层(Pooling Layer)【最大ceil, 最小floor】:降采样和减少参数,避免过拟合。池化操作对特征图进行降采样,减少特征图的尺寸,并保留重要的特征信息。常用的池化方式为最大池化(Max Pooling)和平均池化(Average Pooling)。
非线性激活:线性变换(如全连接层)在神经网络中只能实现线性映射,而现实世界中的数据和任务通常都是非线性的。为了解决这个问题,需要在网络中引入非线性激活函数,以增加网络的表达能力和灵活性。 给网络中引入非线性特征,以训练出符合各种曲线的模型。
·ReLU(Rectified Linear Unit):f(x) = max(0, x),它在x大于0时是线性的,小于等于0时为0,能够有效地解决梯度消失问题。
·Sigmoid:f(x) = 1 / (1 + exp(-x)),它将输入映射到[0, 1]区间,用于二分类问题。
·Tanh:f(x) = (exp(x) - exp(-x)) / (exp(x) + exp(-x)),它将输入映射到[-1, 1]区间,也用于解决梯度消失问题。
正则化层(Regularization Layer):正则化层用于防止过拟合。过拟合是指模型在训练数据上表现良好,但在未见过的测试数据上表现较差的现象。正则化层通过添加一些额外的约束或惩罚项来控制模型的复杂度,以避免过度拟合。
·L1正则化:将参数的绝对值加入到损失函数中,使得模型倾向于产生稀疏的权重矩阵。
·L2正则化:将参数的平方加入到损失函数中,限制权重的大小,防止权重过大造成过拟合。
·Dropout正则化:随机在训练过程中丢弃一部分神经元,减少神经元之间的共适应性,提高泛化能力。
全连接层(Fully Connected Layer):将特征矩阵集合向量化。与卷积层不同,全连接层的神经元排成一列,这些神经元与前一层神经元通过权值互连,呈全连接结构。全连接层的层数以及每层神经元数并不固定。通常层数越高,神经元数目越少。
功能:进行分类。在卷积和池化层之后,通过全连接层将得到的特征映射转换为一维向量,并通过一系列的全连接神经元进行分类操作,将输入映射到对应的类别概率。
输出层(Output Layer):经过多层特征提取后,最后一层输出层可视为分类器,预测输入样本的类别。通常使用Softmax函数将全连接层的输出转换为类别概率分布,确定输入图像最可能属于哪个类别。

Sequential

1
2
3
4
5
6
7
8
9
10
11
self.model1 = Sequential(
Conv2d(in_channels=3, out_channels=32, kernel_size=5, padding=2),
MaxPool2d(kernel_size=2, ceil_mode=True),
Conv2d(32, 32, 5, padding=2), # 二维卷积层
MaxPool2d(2),
Conv2d(32, 64, 5, padding=2),
MaxPool2d(2),
Flatten(),
Linear(1024, 64),
Linear(64, 10)
)

一次训练+验证:准备数据 - 加载数据 - 准备模型 - 设置损失函数 - 设置优化器 - 开始训练 - 验证 - 聚合展示

1
2
3
4
5
6
7
8
9
10
11
for epoch in range(20):  # 共20轮训练
running_loss = 0.0
for data in dataloader: # 对数据进行一轮学习
imgs, targets = data
output = fc(imgs)
result_loss = loss(output, targets) # 计算损失
optim.zero_grad() # 优化器中,梯度置0
result_loss.backward() # 损失函数求梯度,反向传播
optim.step() # 优化器,优化卷积核参数
running_loss = running_loss + result_loss
print(running_loss)

Colab

Google Colab是谷歌提供的免费Jupyter笔记本环境,不需要配置环境(本质是Linux虚拟机),可以加!运行bash命令。提供一定免费的GPU,可以跑 Tensorflow、Pytorch 等深度学习框架。Google Colab提供的资源量是受限制的,所有 Colab 运行时都会在一段时间后重置。Colab Pro 订阅者的使用量仍会受到限制,但相比非订阅者可享有的限额要多出大约一倍。Colab Pro+ 订阅者还可获享更高的稳定性。

使用Colab训练比较重要的是处理好路径的关系,找到哪个文件在哪里,文件夹的执行目录在哪里,就可以比较简单的运行起程序了,不过Colab确实存在断线问题,我们需要时刻保存好文件,因此我将权值直接保存在云盘上,这样也不会丢失。
12h的限额比较难受,有时候没到时间就限额了。因此需要及时保存训练的模型,长时间不用gpu的情况下也会被限额(Pro版也一样),因此训练完建议及时改成gpu或停止。

Google Drive

谷歌云盘,谷歌云端硬盘。免费用户可以获取15G的空间,付费用户根据套餐可以选择最大20TB的储存空间。
虚拟机根目录为 /content,谷歌云盘地址为 /content/Drive/MyDrive

深度学习库的下载

使用git clone指令进行下载,然后通过cd指令将根目录转移到了xx文件夹。此时根目录为 /content/xx

1
2
!git clone https://github.com/xx/xx.git
%cd xx/

数据集与预训练权重的上传

数据集压缩,上传Google Drive,在colab中打开的jupyter文件(等于打开了一个Linux虚拟机本地)中挂载Google Drive;将数据集从Drive中复制到当前根目录,解压。直接将数据集布置在谷歌云盘会导致大量的云盘数据传输,且谷歌云盘和虚拟机之间存在通信带宽,速度远不及本地(虚拟机)文件,因此需要将数据集复制解压到本地(虚拟机)里进行处理。 (https://blog.csdn.net/weixin_44791964/article/details/123659637?spm=1001.2014.3001.5501)

1
2
!cp /content/Drive/MyDrive/xx.zip ./
!unzip ./xx.zip -d ./

预训练权重存放在 /content/Drive/MyDrive/Models/xx,然后创建logs和model_data。model_data放置的是预训练文件,logs放置的是网络训练过程中产生的权值。

保存路径设置

如果将权值保存在当前根目录下的logs文件夹(/content/xx/logs),发生断线网络就白训练了,浪费大量的时间。
可以将google云盘软连接到根目录下,那么即使断线,权值也保留在云盘中。
本文之前在云盘中创建了logs文件夹(/content/Drive/MyDrive/Models/xx/logs),将该文件夹链接到当前根目录下的logs文件夹中。

1
!ln -s /content/Drive/MyDrive/Models/xx/logs logs   # 即/content/xx/logs

开始训练

1、标注文件的处理
2、训练文件的处理
1)预训练文件的使用。
2)保存周期的设置,这个设置是因为云盘的存储空间有限,每代都保存会导致存储空间满出。
3、开始训练:!python train.py

常用 colab 命令

1
2
3
4
5
6
7
8
9
10
import shutil
shutil.copytree("复制的文件夹","目标路径+目标路径下的文件夹") # 复制路径下的文件夹
shutil.copytree("/content/drive/My Drive/channel","/content/drive/My Drive/channel10") # ex
shutil.rmtree("移除路径下的文件夹") # 移除路径下的文件夹
shutil.copy("","") # 复制单个文件的命令 同上
# 删除单个文件可以右键删除
!pwd # 显示当前目录, /content 为根目录
import os
os.chdir("/content/federated-learning") # 变更根目录操作
!python "/content/federated-learning/main_nn.py" # 运行python 文件

Git

First Project:)being confused and embrassed… luckily no one is staring at you but willing to help,as long as you try to find and explain a bug,try debugging、、

Git Bash

  • 分布式版本控制工具 git:没有“中央服务器”,每个人的电脑上都是一个完整的版本库(区别于集中式版本库管理系统如 svn)。
  • Git Bash:Git提供的命令行工具,可运行linux命令 cd、ls、touch…
    1
    git config --list                         # 查看配置
  • 配置 Git 的全局用户信息,这些信息在你提交代码到 Git 仓库时会被记录下来,用于标识提交代码的作者信息。
    1
    2
    git config --global user.name "xx"        # 配置 user.name caifeng
    git config --global user.email "xx" # 配置 user.email 1908454905@qq.com

Github

  • leo710aka @qq.com xxxxxxx
  • 使用 SSH 密钥连接 GitHub:如果没有的话,使用???以下命令在本地生成 SSH 密钥
    1
    ssh-keygen -t rsa -b 4096 -C "your_email@example.com"
    生成 SSH 密钥后,将 ~/.ssh/id_rsa.pub(公钥)的内容添加到 GitHub 账号的 SSH 密钥设置中。
  • 使用 HTTPS 认证连接 GitHub:在 GitHub 上克隆仓库时,使用 HTTPS 链接,然后在推送更改时,会提示输入 GitHub 账号的用户名和密码,或者使用个人访问令牌(Personal Access Token)代替账号密码。
  • 访问令牌(Access Token)是一种用于通过API进行身份验证的方式。
    • 创建访问令牌:1. 登录到你的GitHub帐户。 2. 点击右上角的头像,选择 “Settings”。 3. 在左侧导航栏中选择 “Developer settings”。 4. 点击 “Personal access tokens”。 5. 点击 “Generate token”。 6. 提供一个描述,选择需要的权限(scope),然后点击 “Generate token”。 7. 复制生成的访问令牌。请注意,生成后会显示一次,如果忘记了,需要重新生成。
    • 使用访问令牌:可以通过多种方式使用,例如在命令行中、在脚本中或者在应用程序中。
    • Token和SSH密钥是两种不同的身份验证机制,用于在GitHub或其他版本控制系统中进行身份验证。存在一些区别。
      用途不同: 访问令牌用于API身份验证,而SSH密钥用于远程仓库的SSH访问。
      获取方式不同: 访问令牌通过GitHub设置生成,而SSH密钥需要用户生成并添加到GitHub帐户。
      身份验证方式不同: 访问令牌通过HTTP标头进行身份验证,而SSH密钥使用SSH协议进行本地身份验证。
      适用场景不同: 访问令牌适用于与GitHub API进行交互,而SSH密钥适用于直接使用Git与远程仓库进行交互。

Git 指令

  1. clone (克隆):从远程仓库中克隆代码到本地仓库。会下载远程仓库的所有分支的引用,但只会检出[chekout]默认分支到本地。
  2. checkout (检出) :从本地仓库中检出一个仓库分支然后进行修订
  3. add (添加):在提交前先将代码提交到暂存区
  4. commit (提交): 提交到本地仓库。本地仓库中保存修改的各个历史版本
  5. fetch (抓取): 从远程库抓取到本地仓库。这会更新所有远程分支的引用信息,但不会自动合并这些更新到本地分支。
  6. merge(合并): 将指定的分支合并到当前分支。
  7. pull(拉取) : 相当于fetch+merge,获取最新的远程提交,但只合并当前检出的本地分支与其对应的远程分支,然后放到到工作区。
  8. push (推送) :修改完成后,需要和团队成员共享代码时,将代码推送到远程仓库

本地仓库

  • 要使用Git对我们的代码进行版本控制,首先需要创建本地仓库
    (1) 在电脑的任意位置创建一个空目录(文件夹) 作为我们的本地Git仓库
    (2) 进入这个目录中,右键打开Git bash
    (3) 执行命令git init
    (4) 如果创建成功后可在文件夹下看到隐藏的.git目录。
  • 新建仓库中的文件,其状态都是未被跟踪的(未纳入版本管理)
    git add <name> / git add .:跟踪一个/所有文件或目录的状态,将其纳入版本管理(即暂存?),记录其状态或内容变化
    git rm <name>:删除文件
    git rm --cache <name>:保留文件但不再跟踪
  • 添加,提交,推送:在IDEA中,点击Git提交并推送,选择提交、添加信息、修改远程仓库的分支(默认origin:main),推送。
    1
    2
    3
    4
    5
    6
    $ git status                                       # 查看当前状态(红色的字体显示的是修改的文件
    $ git add 单个文件名|通配符(. 添加所有修改) # 添加工作区代码到本地git暂存区
    $ git commit -m "提交的信息" # 提交暂存区代码到本地git仓库(生成一个版本)
    $ git log # 查看提交日志信息(head指向当前操作的分支)
    $ git push <远程仓库名> <本地分支名> # 推送本地代码到远程仓库
    $ git push <远程仓库名> <本地分支名>:<远程分支名> # 将指定的本地分支推送到远程仓库的指定分支

远程仓库

  • 上传本地仓库项目到远程
    本地git bash生成ssh公钥,添加到gitee中,本地验证,添加远程仓库
    1
    2
    3
    4
    5
    6
    $ ssh-keygen -t rsa                                       # 生成ssh密钥
    $ cat ~/.ssh/id_rsa.pub # 查看密钥
    $ ssh -T git@gitee.com # 验证
    $ git remote add origin git@gitee.com:leo710aka/test.git # 添加远程仓库名为origin(别名),加ssh地址
    $ git remote -v # 检查远程仓库配置是否正确
    $ git push origin master # 完成添加、提交后,将本地master分支推送至远程origin仓库
    或者直接在idea中,对一个项目文件添加、提交后,直接在idea中git remote,使用github账号密码进行远程仓库的连接,然后推送。(连接远程仓库,要么使用ssh密钥,要么用github账号密码。)
  • 添加远程仓库项目到本地
    1
    2
    $ git clone <远程仓库地址>                 # 克隆远程仓库到本地(默认分支master)
    $ git clone -b <分支名> <远程仓库地址> # 克隆指定分支(如develop)
    也可以先从远程仓库git clone下来一个项目,完成修改后对项目文件添加、提交,在推送到远程仓库时,再进行 ssh密钥/github账号密码 的验证。

分支

  • 实际开发新功能,是在develop分支上创建feature分支,完成后合并到develop分支(部门开发分支)上,经测试,最后合并到master分支(线上分支,主分支,项目正在上线的分支)
    1
    2
    3
    4
    5
    6
    7
    8
    $ git branch                  # 查看本地所有的分支,当前激活的分支会在列表中以星号(*)标识。
    $ git branch -r # 查看远程仓库的分支
    $ git branch -a # 查看所有分支,包括本地分支和远程分支。
    $ git branch caifeng # 从当前分支上新建分支
    $ git checkout caifeng # 切换分支
    $ git checkout -b caifeng # 创建(若不存在)并切换分支
    $ git checkout -d caifeng # 删除分支(-D 强制删除)
    $ git merge <分支名> # 先切换回如master分支上,将其他分支上的提交合并到master上
  • 分支合并
    1
    2
    3
    $ git merge [remote name] [branch name]   
    $ git log --merge # 显示合并冲突的提交日志
    $ git log --graph --oneline --all # 可视化分支和合并历史,以简化理解合并的情况
  • 更新项目
    1
    2
    $ git fetch [remote name] [branch name]   # 抓取仓库里的更新到本地,不会进行合并
    $ git pull [remote name] [branch name] # 拉取远端仓库的修改拉到本地并自动进行合并,等同于fetch+merge
    在一段时间,A、B用户修改了同一个文件,且修改了同一行位置的代码,此时会发生合并冲突A用户在本地修改代码后优先推送到远程仓库,此时B用户在本地修订代码,提交到本地仓库后,也需要推送到玩程仓库,此时B用户晚于A用户,故需要先拉取远程仓库的提交,经过合并后才能推送到远端分支
    或直接在IDEA里面选择分支pull(Git拉取),克隆/更新后记得修改配置文件为测试/本地(如果有)。
  • 冲突:当两个分支上对文件的修改可能会存在冲突,例如同时修改了同一个文件的同一行,这时就需要手动解决冲突,解决步骤、、
    1.处理文件中冲突的地方
    2.将解决完冲突的文件加入暂存区(add)
    3,提交到仓库(commit)

经典Git流模型

其他操作

  • 贮藏代码:当前代码没写完,要切换分支时,必须先提交当前代码或贮藏
    1
    $ git stash    
  • 版本回退
    1
    2
    $ git reset --hard <提交id>    # 通过提交id切换版本
    $ git reflog # 查看历史操作(已清除提交日志、版本切换日志。。。
  • 分支变基:将一系列的提交从一个分支上 “移动” 到另一个分支的起点,并将它们重新应用(或重新播放)到目标分支上。
    1
    $ git rebase   # 可以让提交历史更线性、更简洁

hexo | Hello World

Welcome to Hexo! This is your very first post. Check documentation for more info. If you get any problems when using Hexo, you can find the answer in troubleshooting or you can ask me on GitHub.

Hexo

Hexo 是一个基于 Node.js 的静态博客框架,提供简单的方式来创建和部署静态博客,使作者专注于写作而不必担心后端服务器的维护。
Hexo 使用 Markdown 作为主要的文章撰写语言,这是一种轻量级的标记语言,使得写作变得简单且易于阅读。
Hexo 生成的是静态页面,因此页面加载速度非常快。这对于提供更好的用户体验和搜索引擎优化(SEO)非常有利。
Hexo 使用版本控制系统(通常是 Git)来管理博客内容,因此用户可以轻松地追踪和管理博客文章的历史。
Hexo 可以将博客部署到各种平台,包括 GitHub Pages、Netlify、Vercel 等。这使得博客的部署变得非常简单。

配置 hexo

https://www.bilibili.com/video/BV1Eg41157tL

1
2
$ git config --global user.name "leo710aka"
$ git config --global user.email "1908454905@qq.com"

_cnfig.yml不能直接复制粘贴过去。。

Create a new post

在C:\Users\cf\Documents\Visual Studio Code\blog下右键Git Bash Here

1
$ hexo new "文件名"

或者直接在..\source\posts下编写md文件. More info: Writing

Run server

1
$ hexo s

在本地服务器看看效果. http://localhost:4000/

Clean、 Generate static files 、Deploy to remote sites

1
2
3
$ hexo clean   # 清理缓存、清理之前生成的静态文件
$ hexo g # 构建网页文件(通过md文件生成html、css、js..)
$ hexo d # 同步github,更新博客上的内容(这一步可能需要多试几次)

没办法同步的话,直接把新构建的 E:\Code\Blog\blog\.deploy_git 中的文件手动上传github仓库;
成功后若博客内容未更新,ctrl+f5清下缓存。

其他配置

_config.yml_config.landscape 中设置,修改配置后先 hexo clean 一下,再发布。

hexo d 报错 Error: Spawn failed ?

  1. 寻找github.com的最新ip,加了host解析后,能够ping通,但hexo d仍失败
  2. 优先解决 SSL/TLS 连接超时问题​​:修改 Hexo 配置文件 _config.yml,将仓库地址从 HTTPS 改为 SSH 协议,根治 HTTPS 不稳定
  3. 将 C:\Users\caife.ssh\id_rsa.pub(公钥)文件内容加到 GitHub → ​​Settings → SSH and GPG Keys → New SSH Key​​,再次执行hexo d成功了。
  4. ​​为何旧密钥直接生效?​​
    你已有的 id_rsa 密钥对是之前通过合法流程生成的(可能曾用于其他服务),只是未添加到 GitHub。上传公钥后,GitHub 即可识别该密钥的签名,因此无需重新生成。生成命令示例​​:
    1
    ssh-keygen -t ed25519 -C "your_email@example.com"  # 推荐更安全的 Ed25519 算法
  5. GitHub 身份验证流程​
    当你执行 hexo d 时:
    ​​客户端发起连接​​:Hexo 通过 Git 向 GitHub 发起 SSH 连接请求。
    ​​服务器生成挑战​​:GitHub 生成一个随机字符串(Challenge),用你账户中存储的​​公钥​​加密后发送给客户端。
    ​​客户端响应签名​​:本地 Git 使用​​私钥​​解密该字符串,生成签名并返回给 GitHub。
    ​​服务器验证签名​​:GitHub 用公钥验证签名有效性,若匹配则认证通过,允许推送文件

Python / Anaconda / Django

Python是一种高级、通用、解释型、面向对象的编程语言。
Python是一种解释型语言,其代码不是直接编译成机器码,而是由解释器逐行解释执行。常见的Python解释器有 CPython、Jython 等。
其中 CPython 是官方标准实现,是由Python的创始人Guido van Rossum领导开发的官方Python解释器。它是使用C语言编写的,是最广泛使用的Python解释器。

Python 环境

  1. 配置 Python 环境:
    • 官方Python(CPython):通过官方网站下载并安装,使用官方的 pip 包管理器进行包的安装。不具备虚拟环境管理工具,但可使用 venv 或 virtualenv 创建虚拟环境。
      使用:添加解释器 -> Virtualenv -> 基础解释器 -> C:\Users\caife\AppData\Local\Programs\Python\Python312\python.exe
    • Anaconda Python:通过 Anaconda 官方网站下载并安装。使用 conda 包管理器进行包的安装和环境的管理。具备强大的虚拟环境管理工具,可以轻松创建、导出、列出和删除环境。
    • 项目中自带的环境:在开发环境中配置项目中的 Python 解释器(xxproject\env\Scripts\python.exe),使项目中使用的 Python 环境与系统中的 Python 环境独立。
  2. Python环境通常包括了解释器、标准库、开发工具和其他一些组件:
    • 虚拟环境(Virtual Environment): 虚拟环境是一个独立的Python环境,允许您在同一台机器上同时运行多个项目,每个项目都有其独立的依赖项和库。venvvirtualenv 是用于创建虚拟环境的工具。
    • 解释器:如 C:\Python312\python.exe,用于运行代码。
    • 包管理器:如 C:\Python312\Scripts\pip3.exe,用于(为解释器/环境)安装、升级和卸载软件包。pip 是Python的官方包管理器,用于从 PyPI 安装第三方库。
    • 库:如 C:\Python312\lib,其中包含了该环境中安装的所有Python包。
  3. 开发环境:
    • IDE(集成开发环境):提供了代码编辑、调试、版本控制等一体化功能的工具。常见的 Python IDE包括 PyCharm、Visual Studio Code、Jupyter Notebook 等。
    • Jupyter Notebook: 交互式计算环境,支持在浏览器中编写和运行 Python代码,并包含文本、图像和公式等。
  4. PyCharm 配置解释器:
    • 使用 Pycharm 开发项目时, 点击添加解释器 —> 添加本地解释器,选择将要运行该项目的虚拟环境下的 Python 解释器。
      选择使用这个虚拟环境对应的编辑器,就是选择了使用这个环境运行项目,使用这个环境中配好的包,版本和依赖库等等。 可以在“外部库”查看当前环境中的类库。
    • 一般来说, 可以直接选择本地Python解释器(Python3.10)环境中已经配好了大部分的类库,项目中需要的软件包可以手动安装(指定地址 C:\Users\caife\AppData\Local\Programs\Python\Python312\Lib)。
  5. PyCharm终端的Python环境不一定与当前加载的项目使用的解释器器环境一致!!
    • 所以要注意终端使用的是哪个编辑器,是否与当前项目使用的编辑器匹配。否则在终端 pip install 把包装到 Pycharm 终端的环境下,而当前项目运行在另一个虚拟环境中,便无法 import 已从终端安装的包。
    • 看pycharm提示符的地址,其实就等于是cmd中cd到该地址执行命令

Pycharm 终端指令

1
2
3
4
pip install xx    # pip安装包到终端的环境中
pip install xx --target C:\Users\caife\AppData\Local\Programs\Python\Python312\Lib # 指定安装位置
python --version # 查看 Python 版本
pip list # 列出已安装的包

数据结构与函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
my_list = [1, 2, 3, "apple"]  # 列表
my_tuple = (1, 2, 3) # 元组
my_dict = {"name": "John", "age": 25} # 字典
my_set = {1, 2, 3} # 集合

# 定义函数
def greet(name):
return "Hello, " + name
message = greet("Alice") # 调用函数

# 面向对象编程(OOP)
# 定义类
class Dog:
def __init__(self, name):
self.name = name
def bark(self):
print("Woof!")
my_dog = Dog("Buddy") # 创建对象
my_dog.bark() # 调用对象方法

异常处理

1
2
3
4
5
6
try:
result = 10 / 0
except ZeroDivisionError:
print("Cannot divide by zero")
finally:
print("This will be executed no matter what")

文件操作

1
2
3
4
5
6
7
8
# 写入文件
with open("example.txt", "w") as file:
file.write("Hello, Python!")

# 读取文件
with open("example.txt", "r") as file:
content = file.read()
print(content)

Pandas

Pandas 是一个强大的数据分析库,主要用于数据处理和分析。它提供了两种主要的数据结构:SeriesDataFrame

  • Series 是一个一维标记数组,可以保存任何数据类型。它由两个主要部分组成:索引(index)和数据(data)。
  • DataFrame 是一个二维表格,类似于 Excel 表格或 SQL 表。它由行索引、列索引和数据组成。
    数据组织:DataFrame 以表格的形式组织数据,包括多个列,每一列可以包含不同的数据类型(整数、浮点数、字符串等)。
    索引:每个行和列都有一个标签索引。行索引表示 DataFrame 中的每个数据行,列索引表示 DataFrame 中的每个数据列。
    :DataFrame 的每一列是一个 Pandas Series 对象,这意味着它们可以包含相同类型的数据。你可以将每一列视为一个数据字段,类似于数据库表中的列。 下面是一个示例 DataFrame 的结构:
    1
    2
    3
    4
    5
    6
    |    | Name     | Age | City       |
    |----|----------|-----|------------|
    | 0 | Alice | 25 | Niu York | -- 行索引(0, 1, 2, 3)标识每一行的位置
    | 1 | Bob | 30 | Los Angeles| -- 列索引(Name, Age, City)表示不同的数据字段
    | 2 | Carol | 28 | Chicago | -- 每一列(Name, Age, City)都包含相应的数据
    | 3 | David | 22 | Houston | -- 每一列都是一个 Pandas Series,包含相同类型的数据
  • 1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    import pandas as pd
    import numpy as np
    s = pd.Series([1, 3, 5, np.nan, 6, 8]) # 创建一个 Series
    df = pd.DataFrame({ # 创建一个 DataFrame
    'Name': ['Alice', 'Bob', 'Charlie'],
    'Age': [25, 30, 35],
    'City': ['New York', 'San Francisco', 'Los Angeles']
    })
    df = pd.DataFrame(columns=, data=) # 指定创建 DataFrame 的列索引和其中的数据

    for index, row in df.iterrows(): # 遍历DataFrame的每一行
    print(f"Index: {index}, Name: {row['Name']}, Age: {row['Age']}, City: {row['City']}")
    # Index: 0, Name: Alice, Age: 25, City: New York...

    # 将 DataFrame 对象保存为 CSV 文件
    # sep 表示数据字段之间的分隔符,header 表示是否将列名写入文件,index 表示是否写入行索引。
    df.to_csv('data.txt', sep = ' ', index = False, header = False)
    # 从 CSV、Excel文件读取数据
    df = pd.read_csv('data.csv')
    df = pd.read_excel('data.xlsx')

Numpy

numpy(Numerical Python的缩写)是一个用于科学计算的强大Python库。以下是一些 numpy 的主要功能和用法:

  1. 多维数组: numpy 提供了ndarray对象,是一个多维数组,用于存储同类型的元素。
    1
    2
    3
    import numpy as np
    # 创建一个numpy数组
    arr = np.array([1, 2, 3, 4, 5])
  2. 数组操作: numpy 提供了许多对数组进行操作的函数,包括数学、逻辑、形状操作等。
    1
    2
    3
    4
    # 数组的数学运算
    result = arr + 2
    # 数组形状操作
    reshaped_arr = arr.reshape(5, 1)
  3. 矩阵操作: numpy 具有广泛的矩阵操作,包括矩阵乘法、转置等。
    1
    2
    3
    4
    matrix_a = np.array([[1, 2], [3, 4]])
    matrix_b = np.array([[5, 6], [7, 8]])
    # 矩阵乘法
    result_matrix = np.dot(matrix_a, matrix_b)
  4. 数学函数: numpy 包括大量的数学函数,用于三角函数、对数、指数等。
    1
    2
    3
    x = np.array([0, 1, 2, 3, 4])
    # 求sin(x)
    sin_values = np.sin(x)
  5. 随机数生成: numpy 提供了生成随机数的函数。
    1
    random_numbers = np.random.rand(5)  # 生成5个在[0, 1)范围内的随机数

Python爬虫

反爬虫策略

  • 设置合理的请求头。
    1
    2
    3
    headers = {   # 伪装浏览器请求头
    'Cookie': 'ispeed_lsm=2; baikeVisitId=b84d4a50-436c-4e0f-9e29-dc2393e9cdca; COOKIE_SESSION=6_1_8_5_10_9_1_0_7_5_0_3_33002_0_2_0_1650022038_1650022034_1650022036%7C8%230_1_1650022030%7C1; BD_UPN=1126314751; BD_HOME=1; BD_CK_SAM=1; H_PS_645EC=89b2Pt9WoxiJHIC80g9QL3FIo7tdoc9Z9Gm9Nd6gkOPipOmTDtckrFlLxEpchFYkItCM; BAIDUID=FD56AC9125756B81A0E4EB7A60F27700:FG=1; BIDUPSID=FD56AC9125756B81E8CE802CC99B8074; PSTM=1648004100; BDUSS=Jpc2d4NGIwdzRCNVFTR0xNeS1IYXBLNTQwfjhzRnl3Z0xRSlZJTDhZeU1ibnhpRVFBQUFBJCQAAAAAAAAAAAEAAAAnQDHOyfq77rXDd2luZHkAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIzhVGKM4VRiT2; H_PS_PSSID=36426_31660_35912_36167_34584_35979_36055_36235_26350; BA_HECTOR=ak20800k8kag8h8le71h8646s0q; BDRCVFR[feWj1Vr5u3D]=I67x6TjHwwYf0; delPer=0; PSINO=6',
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/101.0.4951.64 Safari/537.36'}
  • 使用代理IP。
  • 限制请求频率,避免被封IP。
    1
    2
    sleep_time = random.uniform(0, 2)
    time.sleep(sleep_time)
  • 处理验证码和登录等复杂场景。

网络请求库:

  • requests库:用于发送HTTP请求,获取网页内容。
    1
    2
    3
    4
    5
    import requests
    # url_base = 'http://leo/index.phtml?reportdate={year}&quarter={quarter}&p={page}'
    # url = url_base.format(year=iyear, quarter=iquarter, page=page) # 替换url模板中的占位符
    response = requests.get(url=url, headers=self.headers)
    res = response.content.decode() # 获取服务器响应的内容,将其解码成字符串

HTML解析库:

  • BeautifulSoup:用于解析HTML文档,提取所需信息。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    from bs4 import BeautifulSoup
    soup = BeautifulSoup(res, 'html.parser') # 解析 HTML 内容
    first_paragraph = soup.find('p') # 查找第一个<p>标签
    script = soup.find(id=tag_id) # 找到具有指定 id 属性的标签
    # script.string.replace_with(new_string) # 替换标签内容
    # script.append(new_tag) # 在标签内追加新标签
    # script.extract() # 从文档中删除标签
    text = script.text # 获取标签内容
    # json_str = re.findall(r'\[.+\]', text)[0] # 用正则表达式 re 从 text 中查找一个或多个包含 JSON 数据的字符串的第一个匹配项
    # data = json.loads(json_str) # 把json格式的字符串转换为Python类型
    script1 = soup.find('p', class_='paragraph') # 查找带有指定class的<p>标签
    script2 = soup.find('a', href='https://...') # 查找带有指定属性的<a>标签
    labels = soup.find_all('a', attrs={'href': True}) # 模糊搜索HTML代码中所有含href属性的<a>标签
  • lxml:使用类似 XPath 的功能解析HTML文档。
    1
    2
    3
    4
    from lxml import html
    tree = html.fromstring(html_doc)
    paragraphs = tree.xpath('//p') # 使用 XPath 表达式查找<p>标签
    paragraphs_with_class = tree.xpath('//p[@class="paragraph"]') # 使用 XPath 表达式查找具有指定class属性值的<p>标签

数据存储:

  • 文件存储:将爬取的数据保存为文本文件、CSV文件等。
  • 数据库:使用SQLite、MySQL、MongoDB等数据库存储数据。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    def load(self, path):
    with open(path, encoding='utf-8') as fp: # 加载存储在JSON文件中的数据, 指定文件为UTF-8编码
    data = json.load(fp)
    return data
    def save(self, data, path):
    with open(path, 'w', encoding='utf-8') as fp: # 以json格式保存, 最近一日各国疫情数据
    json.dump(data, fp, ensure_ascii=False)

    def baidu_search(v_result_file): # 保存csv数据
    df = pd.DataFrame(
    {
    '关键字': kw_list, '页码': page_list, '标题': title_list, '百度链接': href_list,
    '真实链接': real_url_list, '更新时间': time_list, '简介': desc_list,
    '网站名称': site_list,
    }
    )
    if os.path.exists(v_result_file):
    header = None # 已存在的csv文件保存时不用加标头
    else:
    # 创建新文件时,设置csv文件标头
    header = ['关键词', '页码', '标题', '百度链接', '真实链接', '更新时间', '简介', '网站名称']
    df.to_csv(v_result_file, mode='a+', index=False, header=header, encoding='utf_8_sig')
    print('结果保存成功:{}'.format(v_result_file))

爬虫框架

  • Scrapy:一个强大的Python爬虫框架,提供了高级功能如异步处理、中间件等。
    1
    scrapy startproject myproject

动态网页爬取

  • 使用Selenium或Headless浏览器模拟浏览器行为。
  • 处理JavaScript渲染的页面。

Anaconda

  1. 利用 Anaconda,创建不同版本的虚拟环境,配置不同的包(如不同版本的 Pytorch),以管理不同的项目。
  2. 用 Pycharm 打开项目时,点击添加解释器 —> 添加本地解释器,选择该项目对应的 conda 虚拟环境下,该虚拟环境的 python 解释器。选择使用这个虚拟环境对应的编辑器,就是选择了使用这个环境运行项目,使用这个环境中配好的包等等。
  3. 所以要注意终端使用的是哪个编辑器,是否与当前项目使用的编辑器匹配。否则 pip install 把包装到一个虚拟环境,而当前项目运行在另一个虚拟环境中,便无法 import 已安装的包。

新建虚拟环境

打开Anaconda Prompt,创建一个名字为py36的python版本为3.6的虚拟环境(默认地址已改成D:\anaconda3\envs),并查看已有环境:

1
2
3
4
5
6
7
(base) C:\Users\cf>conda create -n py36 python=3.6
Proceed ([y]/n)? y
(base) C:\Users\cf>conda env list
# conda environments:
#
base * D:\anaconda3
py36 D:\anaconda3\envs\py36

激活(切换)环境,并查看该环境下已有包:

1
2
(base) C:\Users\cf>conda activate py36
(py36) C:\Users\cf>pip list

装包

方法1:在 Anaconda Prompt 中进入该环境后,执行 conda install … 或 pip install …(需要关闭代理)
方法2:在 Pycharm的terminal(终端),执行安装指令(此时所处的是 Pycharm终端 的环境(不一定是此时加载的项目的环境))

1
2
3
4
5
# 其他pip指令
pip list # 列出所有已安装的pip包
pip install package_name==desired_version # 安装指定版本的Python包
pip install --target=... package_name # 指定pip install包的路径
pip show package_name # 查看安装好的包的信息

Jupyter Notebook

以任意行为块,便于代码阅读和测试修改。 在一个环境中安装 jupyter notebook 后,运行:

1
(py36) C:\Users\cf>jupyter notebook        

在浏览器打开8888端口使用 http://localhost:8888/?token=7e39781c1e364fa7a5ee95ef290ffa92eb0caef0c19aaad8


Django

Django是一个基于Python的开源Web应用框架,它遵循 MTV 框架,与传统的 MVC 有一些区别,但是其基本思想和流程是类似的,旨在简化Web开发过程,提高开发效率,同时保持代码的可读性和可维护性。

MTV 架构

  1. Model(模型):负责处理数据逻辑,定义数据库模型(ORM),进行数据的增删改查操作。
  2. Template(模板):负责呈现用户界面,包含 HTML、CSS、JavaScript 等前端代码,展示数据给用户。
  3. View(视图): 处理用户请求,根据请求调用适当的模型和模板,返回响应给用户。

MTV 流程

  1. 用户发起请求Request到 Django 服务器。
  2. Django 的 URL 配置根据请求的 URL 路径找到对应的视图(View)函数。
  3. 视图函数处理请求,可能需要进行数据库操作,调用模型(Model)获取数据。
  4. 视图函数将获取的数据传递给模板(Template),并渲染生成最终的 HTML 页面。
  5. 服务器将生成的 HTML 页面作为响应Response返回给用户。

目录结构

  1. 项目目录:包含整个 Django 项目的配置和管理文件,settings.py、urls.py、wsgi.py(WSGI 入口)、asgi.py(ASGI 入口)等。
  2. 应用目录:包含具体的应用程序代码和文件,models.py(模型)、views.py(视图)、urls.py(应用URL配置)、templates/ 等。
  3. 静态文件目录: 存放静态资源文件,如 CSS、JavaScript、图片等。默认路径是项目目录下的 static/ 文件夹。
  4. 模板目录: 存放 HTML 模板文件。 默认路径是应用目录下的 templates/ 文件夹。
  5. 数据库文件:默认情况下,Django 使用 SQLite 作为默认数据库,并将数据库文件存放在项目目录下的 db.sqlite3 文件中。
    一个Django项目(project)就是一个基于Django的Web应用,一个Django项目包含一组配置和若干个Django应用
    一个Django应用(blog)就是一个可重用的Python软件包,每个应用可以自己管理模型、视图、模板、路由和静态文件等

几个入门命令

  • 文件路径cmd
    1
    2
    3
    4
    5
    6
    cd E:/.../mainproject                  # 一定要把终端路径切换成项目根目录!!!
    django-admin startproject mainproject # 创建项目,生成工程目录
    python manage.py startapp firstWEB # 创建APP(进入工程目录/IDE console)
    python manage.py makemigrations # 创建库表(进入工程目录/IDE console)
    python manage.py migrate # 执行库表建立(进入工程目录/IDE console)
    python manage.py runserver # 启动项目(进入IDE console)
  • 几个入门设置(settings.py):Django 框架时间设置,Django APP添加,Templates目录设置(在APP日录下,需要独立建立)

HelloWorld

  • 实现一个请求view,访问 主项目project 中的 blog应用 的hello_world接口
    1. 编写应用请求 project/blog/views.py
      1
      2
      def hello_world(request):
      return HttpResponse("2323")
    2. 配置应用路由 project/blog/urls.py
      1
      2
      3
      urlpatterns = [
      path('hello_world', blog.views.hello_world)
      ]
    3. 配置项目路由 project/project/urls.py
      1
      2
      3
      4
      5
      urlpatterns = [
      path('admin/', admin.site.urls),
      # 如果url中含有 'blog',就转发到应用层面的路由处理
      path('blog/', include('blog.urls'))
      ]
    4. 添加blog应用到项目配置中 project/project/settings.py
      1
      2
      3
      4
      INSTALLED_APPS = [
      # ...
      'blog.apps.BlogConfig', # 创建APP时自动创建
      ]
  • 请求转发流程 http://127.0.0.1:8000/blog/
    浏览器 –(项目路由)–> Blog App –> view.py –(应用路由)–> hello_world函数 –> 浏览器

模型层

  • 模型层是什么?
    • 模型层:位于Django视图层和数据库之间, Python对象和数据库表之间转换
    • 模型层作用:屏蔽不同数据库之间的差异, 开发者更加专注于业务逻辑的开发, 提供很多便捷工具有助开发
    • 模型层配置 project/project/settings.py
      1
      2
      3
      4
      5
      6
      DATABASES = {
      'default': {
      'ENGINE': 'django.db.backends.sqlite3', # 自带的数据库
      'NAME': BASE_DIR / 'db.sqlite3',
      }
      }
  • 模型 Model:Model是数据库表的抽象表示。
    • 每个模型都继承自django.db.models.Model类,并定义了一组字段(Field),这些字段对应于数据库表中的列。
      1
      2
      3
      4
      5
      6
      7
      8
      from django.db import models  

      class Author(models.Model): # Author模型有两个字段:name和email
      name = models.CharField(max_length=100)
      email = models.EmailField()
      class Book(models.Model): # Book模型有一个title字段和一个指向Author模型的外键
      title = models.CharField(max_length=200)
      author = models.ForeignKey(Author, on_delete=models.CASCADE)
    • 一旦你定义了模型,Django就会自动为你生成一个数据库表(除非你明确告诉它不要这样做)。然后,你可以使用Django的ORM(对象关系映射)API来查询、创建、更新和删除数据库中的记录。以下是一些基本示例:
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      # 获取所有作者  
      authors = Author.objects.all()
      # 获取标题为"Django教程"的书
      book = Book.objects.get(title="Django教程")
      # 创建一个新作者
      new_author = Author.objects.create(name="John Doe", email="john@example.com")
      # 创建一个新书,并将其与作者关联
      new_book = Book.objects.create(title="Django入门", author=new_author)
      # 获取并更新一本书的标题
      book = Book.objects.get(title="Django教程")
      book.title = "Django进阶"
      book.save()
      # 删除一本书
      book = Book.objects.get(title="Django进阶")
      book.delete()
  • 迁移 Migrations:Django框架中用于管理数据库模式更改的一种强大工具。
    • 当你更改Django模型(Model)时(例如添加、删除或修改字段),Django能够自动计算出需要应用到数据库中的更改,并将这些更改保存为迁移文件。然后,你可以使用Django的迁移命令将这些更改应用到数据库中。
    • 创建迁移文件
      1
      python manage.py makemigrations  # IDE console
    • 运行迁移文件 同步sqlite3数据库
      1
      python manage.py migrate  # IDE console
  • Django shell操作
    • Django shell:继承Django项目环境,用于交互的Python编程,方便快捷
    • 新建文章:IDE console操作
      1
      python manage.py shell  # 进入 Djungo shell
      1
      2
      3
      4
      5
      6
      7
      >>> from blog.models import Article  # 导入模型
      >>> a = Article() # 创建文章
      >>> a.title = 'Test Django Shell'
      >>> a.brief = '...'
      >>> a.save() # 保存文章
      >>> articles = Article.objects.all() # 获取文章
      >>> print(articles[0].title)
  • Django Admin模块
    • Django的后台管理工具,简化Djungo shell的使用;直接读取定义的模型元数据,提供强大的管理使用页面
    • Django的使用
      1
      python manage.py createsuperuser  # 创建管理员用户:caif / cxxxxxxx
      1
      2
      3
      from .models import Article

      admin.site.register(Article) # 将模型注册到admin模块 project/blog/admin.py
      登录页面进行管理:http://127.0.0.1:8000/admin/login/?next=/admin/, 选择模型,进行创建、修改、删除对象操作
  • Model数据返回页面
    1. 编写应用请求 project/blog/views.py
      1
      2
      3
      4
      5
      6
      from blog.models import Article

      def article_content(request): # 就是返回处理过的model数据
      article = Article.object.all()[0] # 从sqlite3中获取对应模型的数据
      ans_str = article.title + ...
      return HttpResponse(ans_str)
    2. 配置应用路由 project/blog/urls.py
    3. 配置项目路由 project/project/urls.py
    4. 添加blog应用到项目配置中 project/project/settings.py

连接本地MySQL

  • 默认情况下,Django连接的是自己带的sqlite数据库。好处是方便,不需要远程连接,打包项目挪到其他电脑上安装一下依赖一会就跑起来了,但是缺点就是,可能会出现各种莫名其面的问题,所以,尽可能在开始的时候,就配置上连接Mysql。
  • Django 连接 MySQL:
    • 安装 Python 访问 MySQL的 客户端模块
      1
      pip install mysqlclient  # Django官方已不建议使用pymysql库,而是改用mysqlclient
    • 修改 Django项目文件夹下的settings.py文件:
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      DATABASES = {
      'default': {
      'ENGINE': 'django.db.backends.mysql', # 使用MySQL引擎
      'NAME': 'HKStock', # 数据库名称
      'USER': 'root', # 数据库用户名
      'PASSWORD': '123456', # 数据库密码
      'HOST': 'localhost', # 数据库主机(如果在本地)
      'PORT': '3306', # 数据库端口(默认为3306)
      }
      }
    • Pycharm 连接 MySQL:点击 Pycharm右上角 database -> “+” -> source -> MySQL,输入数据库名称、用户、密码连接成功
    • 修改项目文件夹下的__init.py文件:要替换默认的数据库引擎,在项目文件夹下的__init__.py 添加以下内容。
      1
      2
      3
      import pymysql

      pymysql.install_as_MySQLdb()
  • 定义模型 Model:将数据库 SQL 映射到面向对象的 Python 中来,使得你可以在 Django 中像操作普通对象一样操作数据库。
  • 迁移 Migrations:定义好了Model,数据库中的表并不会神奇的出现,你还需要把模型转化为对数据库的操作,这就是迁移。
    当你更改Django模型(Model)时(例如添加、删除或修改字段)Django能够自动计算出需要应用到数据库中的更改,并将这些更改保存为迁移文件。然后,你可以使用Django的迁移命令将这些更改应用到数据库中。
  • 新model迁移:
    1. 定义模型(mainapp/models.py)
      1
      2
      3
      class pen(models.Model):
      price = models.IntegerField()
      color = models.CharField(default='black', max_length=20)
    2. 创建迁移文件:指令执行完毕后会生成 mainapp/migrations/0001_initial.py 文件。Django 不会检查你的数据库,而是根据目前的模型的状态,创建一个操作列表,使项目状态与模型定义保持最新。
      1
      python manage.py makemigrations
    3. 运行迁移文件:打开数据库可以看到多了 mainapp_pen 表,并且里面的字段和模型是完全匹配的。插入一些数据。
      1
      python manage.py migrate 
    4. 功能demo
      1
      2
      3
      4
      5
      6
      # 编写url:在项目文件夹 datasite/urls.py
      from mainapp import views

      urlpatterns = [
      path('pen', views.pen) # 配置项目url即可,无需配置应用url
      ]
      1
      2
      3
      4
      5
      6
      7
      # 编写视图(views):在应用文件夹web/views.py,操作model从MySQL中获取数据
      from .models import Pen

      def pen(request):
      pen = Pen.objects.all()
      # 结合html(templates/pen.html)展示数据
      return render(request,"pen.html",{"pen_queryset":pen_queryset})
    5. 启动web:python manage.py runserver
    6. 发送请求:http://127.0.0.1:8000/pen
  • 操作MySQL中已有的表:
    1. 配置Django的数据库设置(项目级settings.py),确保已安装MySQL客户端并且设置好__init__.py文件。
    2. 创建Django模型:尽管数据库表已经存在,但仍需为这些表创建Django模型。
      1
      2
      3
      4
      5
      6
      7
      8
      9
      from django.db import models  

      class Student(models.Model):
      name = models.CharField(max_length=255)
      code = models.CharField(max_length=255)

      class Meta:
      db_table = 'mainapp_student' # 数据库中的实际表名,注意这里应该与你的数据库表名一致
      managed = False # 指示Django该模型对应于现有的数据库表,不由Django迁移管理
    3. 同步Django模型与数据库:只需python manage.py makemigrations,无需python manage.py migrate ??
    4. 在Django中操作数据库表:使用Django的ORM API来查询、创建、更新和删除数据库中的记录了。

视图与模版

  • 使用Bootstrap实现静态博客页面
    • 在 project/blog/template 下创建 index.html
      1
      2
      3
      4
      5
      <!-- 在 header 引入 -->
      <!-- 最新版本的 Bootstrap 核心 CSS 文件 -->
      <link rel="stylesheet" href="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/3.4.1/css/bootstrap.min.css" integrity="sha384-HSMxcRTRxnN+Bdg0JdbxYKrThecOKuH5zCYotlSAcp1+c8xmyTe9GYg1l9a69psu" crossorigin="anonymous">
      <!-- 最新的 Bootstrap 核心 JavaScript 文件 -->
      <script src="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/3.4.1/js/bootstrap.min.js" integrity="sha384-aJ21OjlMXNL5UyIl/XNwTMqvzeRMZH2w8c5cRVpzpU8Y5bApTppSuUkhZXN0VxHd" crossorigin="anonymous"></script>
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      <!-- 静态代码块 -->
      <div class="container page-header">
      <h1 class="text-center mb-4">博客标题</h1>
      </div>
      <div class="container page-body">
      <div class="col-lg-9">
      <!-- 博客详情1, 2, ... -->
      </div>
      <div class="col-lg-3">
      <!-- 博客链接1, 2, ... -->
      </div>
      </div>
  • Django模板系统
    • 一个类似html的文本文件,结合view,实现动态页面。模板语法:
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      <!DOCTYPE html>
      <html>
      <head>
      <title>Blog Post</title>
      </head>
      <body>
      <h1>{{ post.title }}</h1>

      {% if user.is_authenticated %}
      <a href="{% url 'logout' %}">Logout</a>
      {% else %}
      <a href="{% url 'login' %}">Login</a>
      {% endif %}

      <ul>
      {% for comment in post.comments.all %}
      <li>{{ comment.content }} - {{ comment.author.username }}</li>
      {% endfor %}
      </ul>
      </body>
      </html>
    • 使用模板系统渲染博客页面
      在 Django 中,模板文件通常存储在每个应用的 /templates目录下。在应用view中渲染这个模板文件,并传递上下文数据:
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      from django.shortcuts import render
      from datetime import datetime

      # home_view 视图函数渲染了 home.html 模板,并向模板传递了两个变量:name 和 date
      def home_view(request):
      context = {
      'name': 'My Django Website',
      'date': datetime.now(),
      }
      return render(request, 'home.html', context)

: (


ECharts

ECharts,一个使用 JavaScript 实现的开源可视化库,可以流畅的运行在 PC 和移动设备上,兼容当前绝大部分浏览器(IE9/10/11,Chrome,Firefox,Safari等),底层依赖矢量图形库 ZRender,提供直观,交互丰富,可高度个性化定制的数据可视化图表。
https://echarts.apache.org

  • 在项目中引入echarts
    • mainapp/static/assets/js/ 目录中保存 echarts.js
    • mainapp/templates/xx.html 文件中引入 echarts.js
    • 示例:一个简单柱状图
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      <!DOCTYPE html>
      <html>
      <head>
      <meta charset="utf-8" />
      <title>ECharts</title>
      <!-- 引入刚刚下载的 ECharts 文件 -->
      <script src="echarts.js"></script>
      </head>
      <body>
      <!-- 为 ECharts 准备一个定义了宽高的 DOM -->
      <div id="main" style="width: 600px;height:400px;"></div>
      <script type="text/javascript">
      // 基于准备好的dom,初始化echarts实例
      var myChart = echarts.init(document.getElementById('main'));
      // 指定图表的配置项和数据
      var option = {
      xAxis: {
      data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
      },
      yAxis: {},
      series: [
      {
      type: 'bar',
      data: [23, 24, 18, 25, 27, 28, 25]
      }
      ]
      };
      // 使用刚指定的配置项和数据显示图表。
      myChart.setOption(option);
      </script>
      </body>
      </html>
  • 传入数据渲染echarts模版:
    1. 在view函数中准备数据,比如一个Python列表或字典;通过渲染函数(render())将数据传递到模板中;
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      from django.shortcuts import render  

      def chart_view(request):
      # 准备数据
      x_axis_data = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
      y_axis_data = [23, 24, 18, 25, 27, 28, 55]

      # 将数据传递给模板
      context = {
      'x_axis_data_json': json.dumps(x_axis_data), # JSON 字符串形式的数据
      'y_axis_data_json': json.dumps(y_axis_data),
      }
      return render(request, '2.html', context)
    2. 复制示例模版from https://echarts.apache.org/examples/zh/index.html, 使用 Django模板标签将数据嵌入到 js代码中
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      <!DOCTYPE html>  
      <html>
      <head>
      <meta charset="utf-8" />
      <title>ECharts</title>
      <!-- 引入ECharts文件,确保路径正确 -->
      <script src="echarts.js"></script>
      </head>
      <body>
      <div id="main" style="width: 600px;height:400px;"></div>
      <script type="text/javascript">
      // 基于准备好的dom,初始化echarts实例
      var myChart = echarts.init(document.getElementById('main'));

      // 从 Django 模板上下文中取出数据,并转换为 JavaScript 数组
      var x_axis_data = JSON.parse('{{ x_axis_data_json|escapejs }}');
      var y_axis_data = JSON.parse('{{ y_axis_data_json|escapejs }}');

      // 指定图表的配置项和数据
      var option = {
      xAxis: {
      type: 'category',
      data: x_axis_data // 使用从 Django 传递的数据
      },
      yAxis: {
      type: 'value'
      },
      series: [
      {
      data: y_axis_data, // 使用从 Django 传递的数据
      type: 'bar'
      }
      ]
      };

      // 使用刚指定的配置项和数据显示图表。
      myChart.setOption(option);
      </script>
      </body>
      </html>

Redis

Redis是一种支持 key-value 等多种数据结构的存储系统。可用于缓存,事件发布或订阅,高速队列等场景。支持网络,提供字符串,哈希,列表,队列,集合结构直接存取,基于内存,可持久化。 默认端口:6379

启动 Redis

下载 Windows版 msi文件:https://github.com/microsoftarchive/redis/releases
在环境变量中配好 Redis 的安装目录后,直接打开 cmd,启动 Redis 客户端
启动客户端后, 在客户端cmd窗口中测试 Redis 是否正常工作,设置密码(命令行设置的密码在服务重启后失效
注意:在 Windows 环境下,Redis 不支持后台运行模式,因此在启动 Redis 服务器时必须保持 cmd 窗口一直打开。(如果想要在后台运行 Redis,可以考虑使用虚拟机或者 Linux 环境下的 Redis)

  • 1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    C:\Users\蔡枫>redis-cli   # 启动 Redis 客户端(默认Redis服务端未关闭)
    127.0.0.1:6379> ping # 在 Redis 客户端中输入 ping 命令
    PONG # 返回 PONG 表示 正常工作
    127.0.0.1:6379> config set requirepass chskj.2020 # 修改密码
    OK
    127.0.0.1:6379> auth chskj.2020 # 验证密码
    OK
    127.0.0.1:6379> config get requirepass # 查看密码
    1)"requirepass"
    2)"chskj.2020"

启动(重启) Redis 服务器,客户端

进入 Redis 的安装目录(D:\Redis),打开 cmd 窗口(地址栏cmd回车),执行 redis-server.exe redis.windows.conf,即可启动 Redis 服务器
启动Redis服务器后,在 Redis 安装目录打开另一个 cmd 窗口,执行 redis-cli.exe,(或直接双击文件夹中的redis-cli.exe)即可启动(重启)Redis 客户端

Redis中有16个数据库(Database),每个数据库都是一个独立的命名空间,用于存储键值对数据。这些数据库被编号为0到15,默认情况下客户端连接到数据库0。可以通过SELECT命令选择数据库来切换不同的数据库空间,每个数据库都是相互隔离的,数据不会互相干扰。
每个数据库都可以包含键值对数据,命令和配置,但请注意,Redis的每个数据库是相对较小的,因此可以将不同类型的数据存储在不同的数据库中,以便更好地组织和管理数据。

1
SELECT 1   # 切换到数据库1

数据类型

Redis所有的key(键)都是字符串。我们在谈基础数据结构时,讨论的是存储值的数据类型,主要包括常见的5种数据类型,分别是:String、List、Set、Zset、Hash。 三种特殊的数据类型,分别是 HyperLogLogs(基数统计),Bitmaps (位图) 和 geospatial(地理位置)。 Redis5.0 中还增加了一个数据结构Stream,它借鉴了Kafka的设计,是一个新的强大的支持多播的可持久化的消息队列。

结构类型 结构存储的值 结构的读写能力
String字符串 可以是字符串、整数或浮点数 对整个字符串或字符串的一部分进行操作;对整数或浮点数进行自增或自减操作;
List列表 一个链表,链表上的每个节点都包含一个字符串 对链表的两端进行push和pop操作,读取单个或多个元素;根据值查找或删除元素;
Set集合包 含字符串的无序集合 字符串的集合,包含基础的方法有看是否存在添加、获取、删除;还包含计算交集、并集、差集等
Hash散列 包含键值对的无序散列表 包含方法有添加、获取、删除单个元素
Zset有序集合 和散列一样,用于存储键值对字符串成员与浮点数分数之间的有序映射;元素的排列顺序由分数的大小决定; 包含方法有添加、获取、删除单个元素以及根据分值范围或成员来获取元素

String字符串

  • String是redis中最基本的数据类型,一个key对应一个value。
  • String类型是二进制安全的,意思是 redis 的 string 可以包含任何数据。如数字,字符串,jpg图片或者序列化的对象。
  • 实战场景
    • 缓存: 经典使用场景,把常用信息,字符串,图片或者视频等信息放到redis中,redis作为缓存层,mysql做持久化层,降低mysql的读写压力。
    • 计数器:redis是单线程模型,一个命令执行完才会执行下一个,同时数据可以一步落地到其他的数据源。session:常见方案spring session + redis实现session共享,
  • 1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    127.0.0.1:6379> set hello world    # 设置存储在给定键中的值
    OK
    127.0.0.1:6379> get hello # 获取存储在给定键中的值
    "world"
    127.0.0.1:6379> del hello # 删除存储在给定键中的值
    (integer) 1
    127.0.0.1:6379> get hello
    (nil)
    127.0.0.1:6379> set counter 2
    OK
    127.0.0.1:6379> get counter
    "2"
    127.0.0.1:6379> incr counter # 将键存储的值加1
    (integer) 3
    127.0.0.1:6379> get counter
    "3"
    127.0.0.1:6379> incrby counter 100 # 将键存储的值加上整数
    (integer) 103
    127.0.0.1:6379> get counter
    "103"
    127.0.0.1:6379> decr counter # 将键存储的值减1
    (integer) 102
    127.0.0.1:6379> get counter
    "102"

List列表

  • Redis中的List其实就是链表(Redis用双端链表实现List)。
  • 使用List结构,我们可以轻松地实现最新消息排队功能(比如新浪微博的TimeLine)。List的另一个应用就是消息队列,可以利用List的 PUSH 操作,将任务存放在List中,然后工作线程再用 POP 操作将任务取出进行执行。
  • 使用列表的技巧:lpush+lpop=Stack(栈),lpush+rpop=Queue(队列),lpush+ltrim=Capped Collection(有限集合),lpush+brpop=Message Queue(消息队列)
  • 1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    127.0.0.1:6379> lpush mylist 1 2 ll ls mem   # 将给定值推入到列表左端,RPUSH 将给定值推入到列表右端
    (integer) 5
    127.0.0.1:6379> lrange mylist 0 -1 # 获取列表在给定范围上的所有值
    1) "mem"
    2) "ls"
    3) "ll"
    4) "2"
    5) "1"
    127.0.0.1:6379> lindex mylist -1 # 可以使用负数下标,以 -n 表示列表的倒数第n个元素
    "1"
    127.0.0.1:6379> rpop mylist # 从列表的右端弹出一个值,并返回被弹出的值;lpop 从左弹
    "1"
    127.0.0.1:6379> lindex mylist 10 # index不在 mylist 的区间范围内
    (nil)

Set集合

  • Redis 的 Set 是 String 类型的无序集合。集合成员是唯一的,这就意味着集合中不能出现重复的数据。
  • Redis 中集合是通过哈希表实现的,所以添加,删除,查找的复杂度都是 O(1)。
  • 实战场景
    • 标签(tag),给用户添加标签,或用户给消息添加标签,这样有同一标签或者类似标签的可以给推荐关注的事或者关注的人。
    • 点赞,或点踩,收藏等,可以放到set中实现
  • 1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    127.0.0.1:6379> sadd myset hao hao1 xiaohao hao  # 向集合添加一个或多个成员	
    (integer) 3
    127.0.0.1:6379> scard myset # 获取集合的成员数
    (integer) 3
    127.0.0.1:6379> smembers myset # 返回集合中的所有成员
    1) "xiaohao"
    2) "hao1"
    3) "hao"
    127.0.0.1:6379> sismember myset hao # 判断 member 元素是否是集合 key 的成员
    (integer) 1

Hash散列

  • Redis hash 是一个 string 类型的 field(字段) 和 value(值) 的映射表,hash 特别适合用于存储对象。
  • 实战场景 - 缓存: 能直观,相比string更节省空间,的维护缓存信息,如用户信息,视频信息等。
  • 1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    127.0.0.1:6379> hset user name1 hao               # 添加键值对	
    (integer) 1
    127.0.0.1:6379> hset user email1 hao@163.com
    (integer) 1
    127.0.0.1:6379> hgetall user # 获取散列中包含的所有键值对
    1) "name1"
    2) "hao"
    3) "email1"
    4) "hao@163.com"
    127.0.0.1:6379> hget user user # 获取指定散列键的值
    (nil)
    127.0.0.1:6379> hget user name1
    "hao"
    127.0.0.1:6379> hset user name2 xiaohao
    (integer) 1
    127.0.0.1:6379> hdel user name1 # 如果给定键存在于散列中,那么就移除这个键
    (integer) 1
    127.0.0.1:6379> hgetall user
    1) "email1"
    2) "hao@163.com"
    3) "name2"
    4) "xiaohao"

Zset有序集合

  • Redis 有序集合和集合一样也是 string 类型元素的集合,且不允许重复的成员。不同的是每个元素都会关联一个 double 类型的分数。redis 正是通过分数来为集合中的成员进行从小到大的排序。
  • 有序集合的成员是唯一的, 但分数(score)却可以重复。有序集合是通过两种数据结构实现:
    1. 压缩列表(ziplist): ziplist是为了提高存储效率而设计的一种特殊编码的双向链表。它可以存储字符串或者整数,存储整数时是采用整数的二进制而不是字符串形式存储。它能在O(1)的时间复杂度下完成list两端的push和pop操作。但是因为每次操作都需要重新分配ziplist的内存,所以实际复杂度和ziplist的内存使用量相关
    2. 跳跃表(zSkiplist): 跳跃表的性能可以保证在查找,删除,添加等操作的时候在对数期望时间内完成,这个性能是可以和平衡树来相比较的,而且在实现方面比平衡树要优雅,这是采用跳跃表的主要原因。跳跃表的复杂度是O(log(n))。
  • 实战场景 - 排行榜:有序集合经典使用场景。例如小说视频等网站需要对用户上传的小说视频做排行榜,榜单可以按照用户关注数,更新时间,字数等打分,做排行。
  • 1
    2
    3
    4
    5
    6
    7
    8
    9
    127.0.0.1:6379> zadd myscoreset 100 hao 90 xiaohao  # 将一个带有给定分值的成员添加到有序集合里面
    (integer) 2
    127.0.0.1:6379> ZRANGE myscoreset 0 -1 # 根据元素在有序集合中所处位置,从有序集合中获取多个元素
    1) "xiaohao"
    2) "hao"
    127.0.0.1:6379> ZSCORE myscoreset hao
    "100"
    127.0.0.1:6379> zrem myscoreset hao # 如果给定元素成员存在于有序集合中,那么就移除这个元素
    (integer) 1

HyperLogLog

采用一种基数算法,用于完成独立总数的统计。(同一个人多次访问,只记一访问量)(独立访客)
占据空间小,无论统计多少个数据,只占12K的内存空间
不精确的统计算法,标准误差为 0.81%。

Bitmap

不是一种独立的数据结构,实际上就是字符串
支持按位存取数据,可以将其看成是byte数组
适合存储索大量的连续的数据的布尔值。(记录一个人连续一年每天的签到情况 0/1)(日活跃用户)


RedisTemplate

  • Spring Boot提供了RedisTemplate作为与Redis交互的强大工具。通过RedisTemplate,您可以在java项目中执行各种Redis命令来操作数据,包括字符串、列表、集合、散列、有序集合等。以下是一些常见的Redis操作示例:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    // 存储数据:
    redisTemplate.opsForValue().set("myKey", "myValue");
    String value = (String) redisTemplate.opsForValue().get("myKey");
    // 列表操作:
    redisTemplate.opsForList().leftPush("myList", "value1");
    List<String> myList = redisTemplate.opsForList().range("myList", 0, -1);
    // 集合操作:
    redisTemplate.opsForSet().add("mySet", "member1", "member2");
    Set<String> mySet = redisTemplate.opsForSet().members("mySet");
    // 散列操作:
    Map<String, String> myHash = new HashMap<>();
    myHash.put("field1", "value1");
    myHash.put("field2", "value2");
    redisTemplate.opsForHash().putAll("myHash", myHash);
    String fieldValue = (String) redisTemplate.opsForHash().get("myHash", "field1");
    // 有序集合操作:
    redisTemplate.opsForZSet().add("myZSet", "member1", 1.0);
    Set<String> myZSet = redisTemplate.opsForZSet().range("myZSet", 0, -1);
  • 公共命令
    1
    2
    redisTemplate.delete(key);  // 删除一个数据结构
    redisTemplate.hasKey(key); // 是否存在一个数据结构

Redis 事务管理

  • 要使用编程式事务
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    public void testTransaction() {
    Object result = redisTemplate.execute(new SessionCallback() {
    @Override
    public Object execute(RedisOperations redisOperations) throws DataAccessException {
    String redisKey = "text:tx";
    redisOperations.multi(); // 启用事务
    redisOperations.opsForSet().add(redisKey, "zhangsan");
    redisOperations.opsForSet().add(redisKey, "lisi");
    redisOperations.opsForSet().add(redisKey, "wangwu");
    // redis事务内做查询,无效
    // redis事务中的所有代码在事务提交后一起执行
    System.out.println(redisOperations.opsForSet().members(redisKey));
    return redisOperations.exec(); // 提交事务
    }
    });
    }

Redis缓存问题

在高并发的业务场景下,数据库大多数情况都是用户并发访问最薄弱的环节。所以,就需要使用redis做一个缓冲操作,让请求先访问到redis,而不是直接访问Mysql等数据库。这样可以大大缓解数据库的压力。当缓存库出现时,必须要考虑如下问题:

  1. 缓存穿透
    • 问题来源
      缓存穿透是指缓存和数据库中都没有的数据,而用户不断发起请求。由于缓存是不命中时被动写的,并且出于容错考虑,如果从存储层查不到数据则不写入缓存,这将导致这个不存在的数据每次请求都要到存储层去查询,失去了缓存的意义,
      在流量大时,可能DB就挂掉了,要是有人利用不存在的kev频繁攻击我们的应用,这就是漏洞.
      如发起为id为”-1”的数据或id为特别大不存在的数据。这时的用户很可能是攻击者,攻击会导致数据库压力过大。
    • 1、接口层增加校验,如用户鉴权校验,id做基础校验,id<=0的直接拦截
      2、从缓存取不到的数据,在数据库中也没有取到,这时也可以将key-value对写为key-null,缓存有效时间可以设置短点,如30秒 (设置太长会导致正常情况也没法使用)。这样可以防止攻击用户反复用同一个id暴力攻击
      3、布隆过滤器。bloomfilter就类似于一个hash set,用于快速判某个元素是否存在于集合中,其典型的应用场景就是快速判断个key是否存在于某容器,不存在就直接返回。布隆过滤器的关键就在于hash算法和容器大小,
  2. 缓存击穿
    • 问题来源: 缓存击穿是指缓存中没有但数据库中有的数据(一般是缓存时间到期),这时由于并发用户特别多,同时读缓存没读到数据,又同时去数据库去取数据,引起数据库压力瞬间增大,造成过大压力.
    • 1、设置热点数据永远不过期
      2、接口限流与熔断,降级。重要的接口一定要做好限流策略,防止用户恶意刷接口,同时要降级准备,当接口中的某些 /3务不可用时候,进行熔断,失败快速返回机制。
      3、加互斥锁
  3. 缓存雪崩
    • 问题来源: 缓存雪崩是指缓存中数据大批量到过期时间,而查询数据量巨大,引起数据库压力过大甚至down机。和缓存击穿不同的是缓存击穿指并发查同一条数据,缓存雪崩是不同数据都过期了,很多数据都查不到从而查数据库。
    • 1、缓存数据的过期时间设置随机,防止同一时间大量数据过期现象发生
      2、如果缓存数据库是分布式部署,将热点数据均匀分布在不同的缓存数据库中
      3、设置热点数据永远不过期.
  4. 缓存污染(或满了)
    • 缓存污染问题说的是缓存中一些只会被访问一次或者几次的的数据,被访问完后,再也不会被访问到,但这部分数据依然留存在缓存中,消耗缓存空间.
    • 缓存污染会随着数据的持续增加而逐渐显露,随着服务的不新运行,缓存中会存在大量的永远不会再次被访问的数据。缓存空间是有限的,如果缓存空间满了,再往缓存里写数据时就会有额外开销,影响Redis性能。这部分额外开销主要是指写的时候判断淘汰策略,根据淘汰策略去选择要淘汰的数据,然后进行删除操作。

缓存淘汰策略

Redis共支持八种淘汰策略,分别是noeviction, volatile-random、 volatile-ttl, volatile-ru、 volatile-lfuallkeys-lru、allkeys-Iandom 和 allkevs-lfu 策略。主要看分三类看:

  • 不淘汰
    noeviction (v4.0后默认的):一旦缓存被写满了,再有写请求来时,Redis 不再提供服务,而是直接返回错误。这种策略不会淘汰数据,所以无法解决缓存污染问题。一般生产环境不建议使用。
  • 对设置了过期时间的数据中进行淘汰
    随机 volatile-random:在设置了过期时间的键值对中,进行随机删除。因为是随机删除,无法把不再访问的数据筛选出来,所以可能依然会存在缓存污染现象,无法解决缓存污染问题。
    tvolatile-ttl:Redis在筛选需删除的数据时,越早过期的数据越优先被选择。随机删除就无法解决缓存污染问题。
    volatile-lru:LRU 算法的全称是 Least Recently Used,按照最近最少使用的原则来筛选数据
    volatile-lfu:LFU 缓存策略是在 LRU 策略基础上,为每个数据增加了一个计数器,来统计这个数据的访问次数。当使用 LFU 策略筛选淘汰数据时,首先会根据数据的访问次数进行筛选,把访问次数最低的数据淘汰出缓存。如果两个数据的访问次数相同,LFU 策略再比较这两个数据的访问时效性,把距离上一次访问时间更久的数据淘汰出缓存。
  • 全部数据进行淘汰
    随机 allkeys-random:从所有键值对中随机选择并删除数据。
    allkeys-lru:使用 LRU 算法在所有数据中进行筛选。
    allkeys-lfu:使用 LFU 算法在所有数据中进行筛选。

Redis 持久化

Redis 提供了两种主要的持久化方式,用于在系统重启时保持数据的持久性:RDB 持久化和 AOF 持久化。
用户可以根据实际需求选择 RDB 持久化、AOF 持久化或两者结合使用。通常情况下,AOF 持久化是更安全的选择,因为它可以提供更好的持久性保障,但也需要更多的磁盘空间。

  1. RDB 持久化:
    • RDB 持久化是通过在指定的时间间隔内将内存中的数据集快照写入磁盘的方式来实现的。
    • 快照是一个二进制文件,它记录了某个时间点上 Redis 数据集的所有键值对。
    • RDB 持久化是一个“点对点”操作,它在指定的时间点创建了一个数据快照。
    • RDB 持久化适用于备份、灾难恢复等场景。
      RDB 持久化的配置选项可以在 Redis 配置文件中进行设置,例如:
      1
      2
      3
      save 900 1      # 表示在 900 秒(15分钟)内,如果至少有 1 个 key 发生了变化,则执行快照操作
      save 300 10 # 表示在 300 秒(5分钟)内,如果至少有 10 个 key 发生了变化,则执行快照操作
      save 60 10000 # 表示在 60 秒内,如果至少有 10000 个 key 发生了变化,则执行快照操作
  2. AOF 持久化:
    • AOF(Append Only File)持久化记录了服务器执行的所有写操作指令,以追加的方式将这些指令记录到一个文件中。
    • AOF 持久化是一个“追加”操作,每个写操作都被追加到文件末尾。
    • AOF 持久化适用于对数据的实时持久化需求。
      AOF 持久化的配置选项可以在 Redis 配置文件中进行设置,例如:
      1
      2
      appendonly yes            # 启用 AOF 持久化
      appendfsync everysec # 每秒钟执行一次 fsync 操作
    • appendfsync 选项可以设置 alwayseverysecnoalways 表示每个写命令都会立即被写入磁盘,everysec 表示每秒执行一次 fsync 操作,no 表示由操作系统自行决定何时进行写入磁盘操作。
    • Redis 还提供了 AOF 重写机制,可以通过 auto-aof-rewrite-percentageauto-aof-rewrite-min-size 选项配置。这允许 Redis 在不停机的情况下优化 AOF 文件的大小。