砺剑出鞘:我的软件工程师求职之旅

本文最后更新于:2 个月前

写在前面

三月份了,春招在即,我也开始着手准备今年下半年的秋招了。

现阶段的目标是能在短期内找到一份日常实习,再持续投递中大厂的暑期实习岗位。

这注定是一场持久战,从现阶段开始到接下来的九个月里,我不能有丝毫懈怠。

那么,简单记录下我的求职经历吧!

求职经历

2月19日,了解计算机四级网络工程师考点考纲,基本完成项目网站首页,记忆 MySQL 相关八股文,计划开始优化简历。

大三下还打算窝在学校上课,是等着毕业就失业吗?

2月20日,系统分析项目核心功能,优化简历“专业技能”一栏,现阶段第一版简历完成,目标更加清晰;巩固 Spring 相关八股文。

2月21日,着手梳理 Memory API 忆汇廊核心功能;想家了,有感而发,写点生活感悟。

2月22日,着手配置 Canal 监听 MySQL 数据库流水;巩固蓝桥杯考点考纲,了解蓝桥杯算法,做好备考;持续完善项目文档以及 Gitee / GitHub 仓库介绍。

2月23日,记忆 Java、JVM 相关八股文;下午跟小穆聊天,唠家常,唠理想;梳理项目核心功能;尝试 Spring Cloud Gateway 限流。

晚风几许撩人意,夕阳半刻醉人心

2月24日,完善项目架构设计,思考项目优化点和扩展点;梳理项目核心功能,持续优化项目文档以及 Gitee / GitHub 仓库介绍;掘金学习热榜文章:自定义 Starter、MySQL 日志、事务相关知识;计划开始持续输出文章,记录生活;晚点时候,拿到电信流量卡并成功激活。

强者,从来不会抱怨环境。我他妈就是强者

2月25日,简单的早间点滴;第一次来餐厅自习;完善计算机网络八股,开始阅读《HTTP四十讲》;着手购买充电宝、便携式桌椅,为面试做准备。

没有什么光阴真正被虚度,也没有任何事情是徒劳无功的

2月26日,在牛客、BOSS、实习僧等了解行情;持续学习计算机网络;基本完成项目文档编写,完善 SDK Gitee 项目介绍;阅读《孙子兵法》《朝花夕拾》。

知识改变年薪,文化改变命运

2月27日,六级三战失败,血压暂时升高;优化简历“专业技能”一栏,添加设计模式、个人优势等内容;一次性完善“项目经历”一栏内容,简历第二版优化完成;持续巩固计算机网络和 Redsi;过牛客链表算法题;计划明天彻底完善简历,打点校园内的面试环境。

截至二零二四年二月二十七日,下午四时四十五分,我的个人博客共有七十余篇博文,总计九百余张图片、五十余万文字

2月28日,快速记忆 Java 基础、Spring 常考面试题;完善实习僧平台信息;基本完善项目文档,优化简历;成功投递第一波简历。

2月29日,持续巩固学习 MySQL、Redis、并发编程、集合等八股;持续投递本地日常实习;沟通几十个、简历几份,到目前为止还没有回应;学习 RPC、网关等微服务架构知识,系统梳理。

3月1日,远程帮助小伙伴完善项目,持续学习微服务架构;终于有人要我简历了,很期待;桌子,椅子,超大容量充电宝都回来了;写点故事;

3月2日,跑步半小时,连续三天在 BOSS 打招呼,计划找个本地实习,但基本已读不回;做京东笔试题;学习链表算法题;头疼,下午补觉;植物大战僵尸汉化版,休息。

3月3日, BOSS 求职回复寥寥无几,心情越发浮躁;晚上读书,计划明天投递第四波简历。

3月4日,学习 Java 并发编程,学习记录 JVM;谈谈我对微服务架构的理解;下午实习僧一键投递,很快啊,投了五十多份简历;实习僧收到一份七号中午的笔试邀请。

3月5日,继续学习 JVM;前两次要简历的都抱歉了;BOSS 有回复,要求 Java 实习转 GO,无薪资,没有后续;学习 Maven 项目管理工具;计划明天开始投递中大厂,基本放弃本地实习机会。

3月6日,了解租房经验;实习僧约了八号下午四点的面试,人生第一次面试,既紧张又兴奋;准备自我介绍;巩固 MySQL 八股文(索引、事务、锁);巩固 Spring 八股文;晚上吃泡面。

3月7日,计划投递腾讯实习岗位,完善作品集;开设新博文,记录我的求职经历;完成始祖象的笔试;完成携程招聘笔试题,笔试题还挺有意思;同尹老师交流,介绍了个学长,下午吃饭前给学长投了份简历;中午投递 BOSS,约了下周一的线下面试;学习计算机网络。

3月8日,写日记,好久没写日记了;上午尝试投递 BOSS,又有位 BOSS 肯理我了,现在还没有实习岗位,把我简历要过去了。一周之内如果有实习岗位,就会有答复;下午四点腾讯会议面试,第一次面试,时长三十五分钟,问得比较简单,就是没啥经验,话到嘴边表达不清楚的感觉,还得继续努力呵;始祖象算法工程师岗位又发来笔试邀请了,15号的笔试;根据京东大佬指点意见,优化简历。

3月9日,面向面经复习;简历优化,内容更加精炼;写会儿大学故事;在安居乐租房软件上了解租房动态,上B站了解租房注意事项;下午继续 BOSS 投递,又成功投出去一份简历;跟小穆聊天,分享求职进展;晚上面向面经,查缺补漏。

3月10日,跑步,半小时;备考计算机四级网络工程师,找到完备题库,计算机四级稳了;面向面经复习;调试鱼聪明,竟然还要再充会员嘛;中午投递一波简历,又有一份回应;性能优化,Spring Boot 自动装配原理;豚鼠系列《下水道的美人鱼》;回忆大学中的每个假期;API 项目介绍,看相关面经。

3月11日,投递简历,BOSS 询问进展状况;询问学长进展状况;投递各大官网;做腾讯、快手测评;洛阳寰宇网阔科技公司,人事刘先生,态度蛮不错的。事事有回音,不招在校生,还祝我前程似锦。跟导员沟通离校实习,她说不行。。果然导员没啥用。。投递百度实习生岗位;续写《该死的万柏林区》;看《雪山迷踪》;找回状态,高效巩固复习。

3月12日,更新 BOSS 招呼语;跑步半小时;巩固线程池,ThreadLocal,Redis;持续投递 BOSS 平台;面经是永远背不完的!自备战日常实习以来,直到今晚我才能彻底理解这句话;了解学习 Docker 部署。

3月13日,蓝桥杯真题是真看不懂,这跟代码有什么关系,全是数学题。。持续投递 BOSS 平台,太原附近完全没有实习生招生计划吗?系统学习 Elasticsearch 实现原理;拿到蓝桥杯的相关备考资料;牛客网投递了一波暑期实习招聘:五八同城、OPPO、阿里;一周前实习僧海投的岗位回应我了,上海,无兴趣;《地球脉动》。

3月14日,跟太原本地初创公司交流;约了明天和后天的两个面试;再次迭代简历,添加中间件掌握情况;最后一次全面巩固复习面经,为明天的面试做准备;

重点知识总结

操作系统

进程调度:

  • 先来先服务:先来后到

  • 最短作业优先:长作业可能没有执行机会

  • 高响应比优先:等待时间 + 执行时间 / 执行时间,等待时间越长,长作业就越有机会被执行

  • 时间片轮转:为每个进程分配时间片,用完或者提前结束,其他进程就可以抢占 CPU,不好把控时间片大小

  • 最高优先级:为进程设置优先级,静态优先级、动态优先级,优先级低的可能没有执行机会

  • 多级反馈队列调度:多个阻塞队列,按照不同的优先级排列,优先级越低的队列,可执行时间越长。

Java 中的锁机制

Java中的锁机制是用于处理多线程并发情况下数据一致性的重要工具。在Java中,有多个层面的锁机制,包括synchronized关键字和Lock接口等。

  1. synchronized关键字
    • 这是Java语言内置的一种锁机制。
    • 它可以用来实现对代码块或方法的同步控制,确保同一时刻只有一个线程可以执行被锁定的代码块或方法。
    • 当一个线程获取锁时,它会将对象头中的标志位设置为锁定状态,其他线程在尝试获取锁时,如果发现标志位已被设置为锁定状态,就会进入等待状态,直到锁被释放。
  2. Lock接口
    • 提供了比synchronized更灵活的锁机制。
    • 它提供了显式的锁获取和释放操作,允许更细粒度的控制。
    • Lock接口有多种实现,包括ReentrantLock等。

在Java的锁机制中,还可以根据锁的特性进行进一步分类:

  1. 公平锁与非公平锁
    • 公平锁:按照线程申请锁的顺序来获取锁,类似于日常排队。
    • 非公平锁:线程获取锁的顺序并不是按照申请锁的顺序,可能存在插队现象。
  2. 可重入锁(递归锁)
    • 允许同一线程在外层方法获取锁后,进入内层方法时仍能持有该锁并继续运行。
  3. 自旋锁
    • 当线程尝试获取锁失败时,不是立即阻塞等待,而是采用循环的方式尝试获取锁。
    • 这可以减少线程上下文切换的消耗,但当循环次数过多时,会消耗CPU资源。
  4. 读写锁
    • 分为写锁和读锁。写锁是独占锁,一次只能被一个线程持有;读锁是共享锁,可被多个线程持有。
    • 读写锁适用于读操作远多于写操作的场景,可以大大提高读操作的性能。

Java的锁机制为多线程编程提供了丰富的工具,开发者可以根据具体的业务需求选择适合的锁类型,以确保数据的一致性和线程的安全性。

推荐阅读:[19 你知道哪几种锁?分别有什么特点? (lianglianglee.com)](https://learn.lianglianglee.com/专栏/Java 并发编程 78 讲-完/19 你知道哪几种锁?分别有什么特点?.md)

Synchronized 实现原理

JVM基于进入和退出Monitor对象来实现方法同步和代码块同步,但两者的实现细节不一样。

代码块同步是使用monitorenter 和monitorexit指令实现的,而方法同步是使用另外一种方式实现的。

monitorenter指令是在编译后插入到同步代码块的开始位置,而monitorexit是插入到方法结束处和异常处,

JVM要保证每个monitorenter必须有对应的monitorexit与之配对。

任何对象都有 一个monitor与之关联,当且一个monitor被持有后,它将处于锁定状态。线程执行到

monitorenter 指令时,将会尝试获取对象所对应的monitor的所有权,即尝试获得对象的锁。

Java 并发容器

HashMap

HashMap的put流程和扩容原理是Java集合框架中非常重要的部分,下面我将详细地描述这两个过程。

HashMap的put流程

  1. 计算key的哈希值:首先,HashMap会根据key的hashCode()方法计算出一个哈希值,这个哈希值会用来确定key在HashMap中的存储位置。
  2. 计算数组索引:HashMap内部有一个Entry数组,用来存储键值对。计算出的哈希值会进一步通过一个算法(通常是哈希值与数组长度取模)转换成一个数组索引。
  3. 检查是否存在键值对:在对应的数组索引位置,HashMap会检查是否已经存在一个键值对。如果存在,并且key相同,那么就用新的value替换旧的value。
  4. 处理哈希冲突:如果计算出的数组索引位置已经有键值对,并且key不同,那么就发生了哈希冲突。HashMap通过链表或红黑树(在JDK 1.8及以后版本中,当链表长度超过一定阈值时,会转换为红黑树)来解决哈希冲突。
  5. 插入新的键值对:如果数组索引位置是空的,或者key相同但value需要更新,那么就在该位置插入新的键值对。

HashMap的扩容原理

当HashMap中的元素数量超过数组容量的一定比例(负载因子,默认是0.75)时,就会触发扩容操作。

  1. 创建新的数组:HashMap会创建一个新的数组,其容量通常是原数组容量的两倍。
  2. 重新计算索引:扩容后,原来的键值对需要根据新的数组长度重新计算索引。由于新的数组长度是原来的两倍,所以原来的哈希值取模的结果要么不变,要么变为原来的索引加上原数组长度。这个特性被称为“幂等扩容”,它保证了在扩容过程中,只需要对原数组中的元素进行一次重新索引操作,而不需要遍历整个数组。
  3. 移动键值对:HashMap会遍历原数组中的每个位置,对于每个非空的键值对,重新计算其在新数组中的索引,并将其移动到新的位置。这个过程可能会触发哈希冲突的解决操作(即链表的插入或红黑树的调整)。

需要注意的是,扩容操作是一个相对耗时的过程,因为它涉及到内存的重新分配和键值对的重新索引。因此,在实际使用中,应该尽量避免频繁的扩容操作,可以通过预估合适的初始容量和负载因子来优化HashMap的性能。

ArrayList

简单介绍一下 ArrayList

实现了 RandomAccess 接口,支持随机访问;实现了 Clonable 接口,支持复制;实现了 Serializable,支持序列化传输。

底层使用数组实现,该数组为可变长数组,也称之为动态数组。默认初始容量为10,超出容量限制会自动扩容1.5倍。

扩容就是新建一个1.5倍容量大的数组,把原数组内容拷贝到新数组中,将新数组作为扩容后的数组。数组扩容代价很高,使用时应该尽量避免数组扩容,要预知要保存元素的多少,构建 ArrayList 时就指定其初始容量。

remove() 方法有按照索引删除,也有按照元素值删除。会使从所删除元素的下标开始,到数组末尾的元素全部向前移动一个位置,置空最后一位,方便 GC 回收。删除中间元素,需要挪动大量的数组元素,操作代价很大;如果是末尾元素,代价是最小的。

不是线程安全的,只能用在单线程环境下。在多线程环境中,可以考虑使用 Collections.synchronizedList(List l)函数返回一个线程安全的ArrayList 类。

采用了Fail-Fast机制,面对并发的修改时,迭代器很快就会完全失败,报异常 concurrentModificationException(并发修改一次)

add(E e) 实现原理

ensureCapacityInternal(size +1),修改 modCount 标识自增一,calculateCapacity() 确保数组已使用长度加一后不会溢出,即足够存放下一个元素,不满足就使用 grow() 扩容为原容量的1.5倍。

ElementData[size++] = e,添加新元素到数组中。

返回新元素添加成功的布尔值。

add(int index, E element) 实现原理:大同小异。

首先确保要插入元素的位置小于等于当前数组长度,并且不能小于0,否则抛出异常。

判断是否需要扩容后,确保了数组有足够的容量。使用 System.arraycopy() 将要插入的位置之后的所有元素向后移动一位,再将新的数组元素放到指定位置 ElementData[index] = e。

扩容原理:ArrayList的扩容原理主要涉及到底层数组的容量调整。当ArrayList中的元素数量达到当前数组的容量时,它会自动进行扩容以容纳更多的元素。

具体来说,ArrayList的扩容过程如下:

  1. 计算新容量:默认情况下,新的容量会是当前容量的1.5倍。这通常是通过将当前容量右移一位(相当于除以2)然后加上当前容量来实现的。例如,如果当前容量是10,那么新的容量会计算为10 + (10 >> 1) = 15。
  2. 创建新数组:根据计算出的新容量,ArrayList会创建一个新的数组。
  3. 复制元素:将原数组中的元素复制到新数组中。这个过程涉及到遍历原数组,并将每个元素依次放入新数组的对应位置。
  4. 更新引用:将ArrayList的内部引用从原数组更新为新数组。

需要注意的是,ArrayList在初始化时会有一个默认的容量(通常为10),当第一个元素被添加时,如果还没有进行初始化,那么就会创建一个默认容量的数组。之后的扩容操作都是基于这个原理进行的。

频繁的扩容操作可能会对性能产生影响,因为每次扩容都需要创建新数组并复制元素。因此,在实际使用中,如果可能的话,最好预先估计所需容量并设置合适的初始容量,以减少扩容的次数。

另外,与ArrayList不同,LinkedList是基于双向链表实现的,因此它不需要扩容机制。在LinkedList中,添加或删除元素只需要调整链表的节点连接,而不需要像ArrayList那样复制整个数组。这使得LinkedList在元素增加和删除操作上的效率通常比ArrayList要高。

remove() 的实现过程:从列表中删除指定元素,有多种重载形式。按值删除,遍历整个列表寻找给定元素,找到就删除,同时将后面元素向前移动一个位置。按索引删除,直接定位到给定索引的元素,执行删除操作。

remove() 的实现原理:以按照索引删除为例。ElementData 数组根据索引下标找到元素值;根据 size - index - 1 判断删除元素是否为最后一个元素。如果不是最后一个元素,执行 System.arraycopy() 数组拷贝,所有元素向前移动一个位置。最后,把数组最后一位置空,为 null,为 GC 做准备。

Fail-Fast 机制:这是一个错误检测机制,用于在并发修改列表时抛出 ConcurrentModificationException 异常。

当使用迭代器遍历 ArrayList 时,如果列表在迭代过程中被结构性地修改了(例如,通过 addremoveclear 方法),迭代器就会快速失败并抛出 ConcurrentModificationException。结构性修改是指那些改变列表大小的操作,或者那些可能干扰迭代器行为的操作。

这种机制的实现依赖于 ArrayList 内部的一个 modCount 字段。每当列表被结构性修改时,modCount 就会增加。迭代器在每次迭代时都会检查 modCount 是否与迭代器创建时的 expectedModCount 相等。如果不相等,就抛出 ConcurrentModificationException

如果你尝试在迭代过程中使用 list.remove(item) 来删除元素,就会触发 Fail-Fast 机制并抛出异常。因此,总是应该使用迭代器的 remove 方法来删除元素,当你需要在迭代过程中修改列表时。

虽然 Fail-Fast 机制可以帮助发现并发问题,但它并不是线程安全的解决方案。在多线程环境中,你仍然需要使用适当的同步机制(如 synchronized 块或 Collections.synchronizedList)来确保线程安全。Fail-Fast 机制主要是为了帮助开发者在开发过程中更早地发现并发问题,而不是作为一个完整的并发控制机制。

循环中删除 ArrayList 的元素

  1. 使用ArrayListremove方法直接删除元素:普通 for 循环通常是通过索引来遍历数组或容器中的元素的,而在循环中删除元素,列表的大小会改变,后续元素会向前移动,可能会抛出数组越界异常问题。
  2. 使用迭代器的remove方法:Fail-Fast 机制
  3. 使用增强型for循环删除元素:增强型for循环(也称为”foreach”循环)在内部使用迭代器,因此当在循环体内部使用ArrayListremove方法时,会抛出ConcurrentModificationException
  4. 从列表末尾向前删除元素:这种从后向前迭代的方法特别适用于需要基于索引删除元素的情况,因为它不会受到删除元素后列表大小变化的影响。

普通for循环本身不会抛出ConcurrentModificationException

类加载器

​ 类加载器是一个负责加载类的对象,用于实现类加载过程中的加载这一步。每个 Java 类都有一个引用指向加载它的 ClassLoader。类加载器的主要作用就是加载 Java 类的字节码( .class 文件)到 JVM 中(在内存中生成一个代表该类的 Class 对象)。 字节码可以是 Java 源程序(.java文件)经过 javac 编译得来,也可以是通过工具动态生成或者通过网络下载得来。

类加载机制

​ 首先要明确的一点是,JVM 启动的时候,并不会一次性加载所有的类,而是根据需要去动态加载。也就是说,大部分类在具体用到的时候才会去加载,这样对内存更加友好。

​ 对于已经加载的类会被放在 ClassLoader 中。在类加载的时候,系统会首先判断当前类是否被加载过。已经被加载的类会直接返回,否则才会尝试加载。也就是说,对于一个类加载器来说,相同二进制名称的类只会被加载一次。

类加载器分类

​ JVM 中内置了三个重要的 ClassLoaderBootstrapClassLoader(启动类加载器):最顶层的加载类,由 C++实现,通常表示为 null,并且没有父级,主要用来加载 JDK 内部的核心类库;ExtensionClassLoader(扩展类加载器);AppClassLoader(应用程序类加载器):面向我们用户的加载器,负责加载当前应用 classpath 下的所有 jar 包和类。除了这三种类加载器之外,用户还可以加入自定义的类加载器来进行拓展,以满足自己的特殊需求。

双亲委派模型

​ 每当一个类加载器接收到加载请求时,它会先将请求转发给父类加载器。在父类加载器没有找到所请求的类的情况下,该类加载器才会尝试去加载。

  • 在类加载的时候,系统会首先判断当前类是否被加载过。已经被加载的类会直接返回,否则才会尝试加载(每个父类加载器都会走一遍这个流程)。
  • 类加载器在进行类加载的时候,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成(调用父加载器 loadClass()方法来加载类)。这样的话,所有的请求最终都会传送到顶层的启动类加载器 BootstrapClassLoader 中。
  • 只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载(调用自己的 findClass() 方法来加载类)。
  • 如果子类加载器也无法加载这个类,那么它会抛出一个 ClassNotFoundException 异常

​ 双亲委派模型保证了 Java 程序的稳定运行,可以避免类的重复加载。原因是这样的:JVM 区分不同类的方式不仅仅根据类名,相同的类文件被不同的类加载器加载产生的是两个不同的类。JVM 不仅要看类的全名是否相同,还要看加载此类的类加载器是否一样。只有两者都相同的情况,才认为两个类是相同的。

打破双亲委派模型

​ 自定义加载器的话,需要继承 ClassLoader 。如果我们不想打破双亲委派模型,就重写 ClassLoader 类中的 findClass() 方法即可,无法被父类加载器加载的类最终会通过这个方法被加载。但是,如果想打破双亲委派模型则需要重写 loadClass() 方法。

​ 我们比较熟悉的 Tomcat 服务器为了能够优先加载 Web 应用目录下的类,然后再加载其他目录下的类,就自定义了类加载器 WebAppClassLoader 来打破双亲委托机制。这也是 Tomcat 下 Web 应用之间的类实现隔离的具体原理。

类加载过程

类的加载过程:首先要了解执行 Java 程序之后,由编译器将 Java 代码编译转换为字节码,再由 JVM 逐条解释执行字节码,类的加载就发生在 JVM 解释执行阶段。

当虚拟机要使用一个类时,它需要读取并解析 Class 文件获取相关信息,再将信息存入到方法区。方法区会存储已被虚拟机加载的 类信息、字段信息、方法信息、常量、静态变量、即时编译器编译后的代码缓存等数据

在加载阶段,通过类的全限定名获取指定类的字节流,将字节流代表的静态存储结构转换为方法区的动态运行时结构,同时在内存中生成 Class 对象,作为将来在方法区访问数据的入口。这里需要了解下 JVM 内存结构类加载器双亲委派模型等。

在连接阶段,首先进行 Class 文件格式检查、字节码语义检查、程序语义检查等工作,确保字节流中的信息符合规范。还要进行符号引用验证,确保该类所使用的其他类、方法、字段是否存在。完成类相关信息验证之后,开始为类变量分配内存并赋初值。最后,针对类、接口、类方法和接口方法,JVM 会将常量池内的符号引用替换为直接引用,也就是得到类或字段在内存中的指针或者偏移量,确保了在程序执行方法时,系统能够明确该方法所在位置。

在初始化阶段,作为类加载的最后一个阶段,JVM 开始真正执行指定类的 Java 程序代码,即字节码文件,对类进行初始化,创建类的对象实例。这个阶段我们了解到如果要初始化一个类,首先保证其父类完成初始化。整个初始化过程就是实例化对象并投入使用,过程也简单:在堆中分配内存空间 + 初始化对象 + 将该对象指向堆中分配的内存空间地址。

至此,一个类的加载就已经完成了,并且可以创建实例对象投入使用了。这里需要了解下 Java 对象的创建过程。

使用阶段不必多言,使用完毕之后进入卸载阶段。由于所有的对象实例都存放在堆中,当一个类的所有实例对象都已被 GC,在堆中已经不存在该类的实例对象了、该类没有在任何地方被引用,且该类的类加载器的示例也已被 GC 后,这个类就可以被卸载,即该类的 Class 对象被 GC。这里需要了解一下垃圾回收器垃圾回收算法等。

JVM 内存模型

运行时数据区域

程序计数器:字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制,如:顺序执行、选择、循环、异常处理。在多线程的情况下,程序计数器用于记录当前线程执行的位置,从而当线程被切换回来的时候能够知道该线程上次运行到哪儿了。

虚拟机栈:方法调用的数据需要通过栈进行传递,每一次方法调用都会有一个对应的栈帧被压入栈中,每一个方法调用结束后,都会有一个栈帧被弹出。栈由一个个栈帧组成,而每个栈帧中都拥有:局部变量表、操作数栈、动态链接、方法返回地址。和数据结构上的栈类似,两者都是先进后出的数据结构,只支持出栈和入栈两种操作。

  • 局部变量表 主要存放了编译期可知的各种数据类型和对象引用。
  • 操作数栈 用于存放方法执行过程中产生的中间计算结果和计算过程中产生的临时变量。
  • 动态链接 用于管理调用其他方法的符号引用,主要服务一个方法需要调用其他方法的场景。

栈帧随着方法调用而创建,随着方法结束而销毁。无论方法正常完成还是异常完成都算作方法结束。

堆:Java 虚拟机所管理的内存中最大的一块,Java 堆是所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例以及数组都在这里分配内存。

方法区:当虚拟机要使用一个类时,它需要读取并解析 Class 文件获取相关信息,再将信息存入到方法区。方法区会存储已被虚拟机加载的 类信息、字段信息、方法信息、常量、静态变量、即时编译器编译后的代码缓存等数据

运行时常量池:Class 文件中除了有类的版本、字段、方法、接口等描述信息外,还有用于存放编译期生成的各种字面量(Literal)和符号引用(Symbolic Reference)的 常量池表(Constant Pool Table)

字面量是源代码中的固定值的表示法,即通过字面我们就能知道其值的含义。字面量包括整数、浮点数和字符串字面量。常见的符号引用包括类符号引用、字段符号引用、方法符号引用、接口方法符号。常量池表会在类加载后存放到方法区的运行时常量池中。

以前在永久代即运行时数据区域,现在存放在元空间即本地内存。

字符串常量池:字符串常量池 是 JVM 为了提升性能和减少内存消耗针对字符串(String 类)专门开辟的一块区域,主要目的是为了避免字符串的重复创建。以前在永久代即运行时数据区域,现在存放在堆中。

主要是因为永久代(方法区实现)的 GC 回收效率太低,只有在整堆收集 (Full GC)的时候才会被执行 GC。Java 程序中通常会有大量的被创建的字符串等待回收,将字符串常量池放到堆中,能够更高效及时地回收字符串内存。

直接内存:直接内存的分配不会受到 Java 堆的限制,但是,既然是内存就会受到本机总内存大小以及处理器寻址空间的限制。这些内存直接受操作系统管理(而不是虚拟机),这样做的结果就是能够在一定程度上减少垃圾回收对应用程序造成的影响。

垃圾回收

当前虚拟机的垃圾收集都采用分代收集算法,这种算法没有什么新的思想,只是根据对象存活周期的不同将内存分为几块。一般将 Java 堆分为新生代和老年代,这样我们就可以根据各个年代的特点选择合适的垃圾收集算法。

如果 TLAB 中没有足够的内存空间,就会在共享 Eden 区(shared Eden space)之中分配。如果共享 Eden 区也没有足够的空间,就会触发一次 年轻代 GC 来释放内存空间。如果 GC 之后 Eden 区依然没有足够的空闲内存区域,则对象就会被分配到老年代空间(Old Generation)。

各种垃圾收集器的实现细节虽然并不相同,但总体而言,垃圾收集器都专注于两件事情:

  • 查找所有存活对象
  • 抛弃其他的部分,即死对象,不再使用的对象。

第一步,记录(census)所有的存活对象,在垃圾收集中有一个叫做 标记(Marking) 的过程专门干这件事。

标记 阶段完成后,GC 进行下一步操作,删除不可达对象。

删除不可达对象(Removing Unused Objects)

各种 GC 算法在删除不可达对象时略有不同,但总体可分为三类:清除(sweeping)、整理(compacting)和复制(copying)。[下一小节] 将详细讲解这些算法。

Java 对象的创建过程

类加载检查:遇到一条 new 指令,指定实例化哪个类,检查该类是否已经加载完成。如果没有,先执行类加载操作。

分配内存:类加载完成,即类加载检查通过后,在堆中为实例对象划分空间,分配内存。根据不同的垃圾回收机制,如果存在内存碎片,即堆内存不规整,虚拟机会维护一个列表分配一块连续的存储空间。如果不存在内存碎片,虚拟机会使用分界指针找到空闲的内存区域来分配内存空间。

初始化零值:虚拟机将所分配的所有内存空间全部初始化零值,实现了对象实例的成员属性有默认值,不赋初始值就能直接使用。

设置对象头:要确保如何根据实例对象找到该对象对应的类,需要设置对象头。对象头中包括了类信息、GC 分代收集年龄、对象哈希码等内容。

执行构造方法:按照程序员意愿初始化对象,至此,Java 对象创建完成。

MySQL

MySQL 默认的 InnoDB 引擎中,索引数据结构是 B+树。索引按数据结构分类可以分为 B树、B+树、Hash 表等。

索引的运作原理?自平衡的多叉树,按照索引键实现快速检索数据。有主键就用主键作索引键,再者用非空且唯一字段,或者生成一个隐式自增id作为索引键。

聚簇索引和非聚簇索引,回表查询,覆盖索引。

MySQL 索引为什么用 B+树?B+树的数据结构特点,都是自平衡树多叉树

  • 它只有叶子节点存放数据,非叶子节点只存放索引键(能存放更多的索引键高度更加低,结构更加“矮胖”,磁盘IO次数会更少;单点查询,波动更小)
  • 叶子节点构成有序链表(范围查询,更快)
  • (插入和删除效率更高,有冗余节点,非叶子节点都是冗余节点,不会发生复杂的变化)

如何创建索引?

1
create index index_id on table(id)

主键索引,唯一索引,普通索引,联合索引什么是最左匹配原则?索引失效的原因有哪些?索引下推是什么?

什么时候需要创建索引?

  • 索引的优点:最大的好处就是提高查询速度。
  • 缺点:占用的物理空间大;数据量增大、数据的增删改,使得创建索引和维护索引更耗时

所以索引也要在合适的场景下使用。比如字段有唯一性限制的,编码;经常用于作查询条件的,多个字段还可以建立联合索引,提高查询速度;频繁用于排序的字段。表数据量很少、字段频繁更新、区分度不大(男女),就不需要创建索引了。

如何优化索引呢?

当然是使用执行计划分析语句了,explain

  • 使用覆盖索引,在指定字段上创建索引,尽量减少回表操作;
  • 防止索引失效,模糊匹配、联合索引不遵循最左匹配原则、在查询条件中对索引列做了计算、函数、类型转换等操作,都会发生索引失效,走全表扫描。

执行一条 SQL 语句,期间发生了什么?

一条简单的 select 查询语句,它的执行流程是这样的:

MySQL 架构分为两层:

  • Server 层:负责建立连接、分析执行 SQL 语句
  • 存储引擎层:负责数据的存储和提取(InnoDB 成为 MySQL 的默认存储引擎,支持并默认使用 B+树索引)

执行过程:

  • 连接器:经过 TCP 三次握手,启动 MySQL 服务,验证用户名和密码,获取用户操作权限。默认空闲连接时常为八小时,长短连接。
1
mysql -uroot -p
  • 查询缓存:键值对保存形式,键为 SQL 查询语句,值为 SQL 语句查询结果。很鸡肋,表频繁更新,查询缓存会被清理,MySQL 8.0 删除了查询缓存。
  • 解析 SQL:解析器,词法分析,检查字符串中的字段,找出关键字 select 这些;语法分许,更具语法规则,判断是否满足 MySQL 语法,不满足(关键词拼写错误)就出错,满足就构建语法树。
  • 执行 SQL:
  • 预处理:表不存在、字段不存在
  • 优化器:制定执行计划,将 SQL 语句的执行方案确定下来,比如作索引优化,使用覆盖索引避免回表查询等
  • 执行器:真正开始执行 SQL 语句,与存储引擎交互。使用聚簇索引也好,使用全表扫描也罢,整体思路就是:执行器的查询过程就是一个while 循环,会调用一个函数指针,可以理解为调用存储引擎去查询记录。根据制定的 SQL 语句执行计划,存储引擎通过 B+树结构或者全表扫描定位数据记录,将结果返回给执行器,并报告查询完毕。执行器收到存储引擎报告的查询完毕的信息,退出循环,停止查询。

这里可以了解到一个索引优化策略:索引下推。

索引下推是怎样的呢?当联合索引遇到范围查询时,会停止匹配。不用索引下推的话,执行过程是这样的:存储引擎根据指定的 SQL 查询计划,根据联合索引的第一条索引,定位到该记录获取主键值,回表查询整行记录值,并将结果返回给 Server 层。Server 层拿到记录,会再次根据第二条索引判断该记录是否满足,满足就返回记录,不满足就跳过该记录。

使用了索引下推之后,存储引擎在根据联合索引第一条索引定位到记录获取主键值之后,不会先回表查询,而是直接根据第二条索引判断记录是否满足,不满足就直接跳过,满足后再回表查询整行记录值,最后返回结果到 Server 层。

索引下推,就是在联合索引索引失效之后,直接在存储引擎层根据所有索引,过滤出满足的条件的记录主键值之后,才进行回表查询,节省了很多回表操作。

事务

事务有哪些特性?经典的转账问题。原子性(undo log)、持久性(redo log)、隔离性(MVCC,锁机制),才能保证事务的一致性

事务一定要保证隔离性,并发事务可能会带来的问题:脏读(一个事务读到了另一个事务未提交的数据)、不可重复读(两次读取到的数据不一样)、幻读(两次读取到的记录数量不一样)。

这就要提到 SQL 标准提出的四种隔离级别:读未提交、读提交、可重复读和串行化。读提交解决了脏读,一个事务提交了之后,它所做的变更才能被其他事务看到;可重复读解决了不可重复读,事务在执行过程中看到的数据,一直跟这个事务启动时看到的是一样的。MySQL 是通过 MVCC 实现这两种隔离级别的。针对幻读,MVCC 也能解决一部分幻读,比如普通的 select 查询语句是快照读,事务在执行过程中看到的数据,一直跟这个事务启动时看到的是一样的,避免了幻读。而针对当前读,事务读取到的数据总是最新的,使用记录锁 + 间隙锁解决了幻读问题。

那么 MVCC 是如何工作的?明确两点:Read View(快照版本)和聚簇索引记录中的两个与事务有关的隐藏列。

每个事务启动之后,会生成一个 Read View,有四个重要的字段:创建该快照的事务 id、当前数据库中活跃的事务id列表(已启动还未提交)、i最小的事务 id(启动最早的)、应该给到的下一个事务 id。

对于使用 InnoDB 存储引擎的数据库表,聚簇索引记录中的两个与事务有关的隐藏列:当某个事务对该记录进行改动之后,记录该事务id;隐藏的指针,指向旧的版本记录,形成版本链,通过版本链可以找到修改前的记录。

一个事务去访问记录的时候,除了自己的更新记录可见,会这样做:

  • 生成一个 Read View,先判断已经提交的事务:
  • 查看该记录的trx_id,即最近一次对该记录修改的事务id,比较 Read View 中的 min_trx_id,小,那就是比当前最小的活跃事务都小,说明该提交记录在创建该 Read View 之前就提交了,可见的。
  • 查看该记录的trx_id,即最近一次对该记录修改的事务id,比较 Read View 中的 max_trx_id,大于等于,那就是比应该给到的下一个事务都大,说明该提交记录在创建该 Read View 之后才提交,不可见的。
  • 如果 trx_id 在这两者之间,即该事务在创建 Read View 之后启动,需要判断该事务是否提交。根据快照的活跃事务列表,,仍然活跃,说明未提交,不可见;反之可见。

这就是通过版本链来控制并发事务访问同一个记录的行为,这就是 MVCC。可重复读跟读提交隔离级别的区别就是:前者在启动事务时生成一个 Read View,在整个事务期间都在用这个 Read View,而后者是在每次读取数据时,都会重新生成一个新的 Read View。

Undo log 作用:实现事务原子性、实现 MVCC,Redo log 实现 事务的持久化;Bin log 实现数据恢复和主从同步。

网络传输安全

防止窃听,机密性

  • 对称加密算法:通信双方使用唯一的密钥来加密通信数据。

问题:通信之前,如何把用来加密数据的密钥安全地传输给对方?无法保证。

  • 非对称加密算法:通信双方各持有一个密钥对,公钥是公开的,私钥自己持有。使用对方的公钥加密数据,只有对方才能用私钥解密

问题:非对称算法的运算速度很慢、性能很差,如果传输过程中频繁使用非对称加密算法加密数据,网络的传输效率是很低的

  • 混合加密:将对称加密算法和非对称加密算法结合,通信开始前使用双方使用非对称加密方式传输密钥,保证了密钥的安全传输,此后通信双方可以使用该密钥来加密通信数据,保证了通信数据的保密性。

防止篡改,完整性

  • 摘要算法:一种特殊的单向加密的压缩算法,它能够把任意长度的数据“压缩”成固定长度、而且独一无二的“摘要”字符串,就好像是给这段数据生成了一个数字“指纹”。摘要和原数据是完全等价的,加密后的数据无法解密,不能从摘要逆推出原文。发送方把加密后的数据,使用摘要算法生成摘要,把加密数据和该摘要一同发往接收方。接收方使用同样的摘要算法对加密数据进行计算,比照生成的摘要和接受的摘要是否一致,保证了通信数据的完整性。

身份认证,真实性

  • 数字签名:发送方要保证通信数据是真实可信的,不是别人伪造的。使用自己的私钥对摘要加密,生成数字签名。数字签名和加密数据被一同发往接收方。接收方使用发送方的公钥解密,验证签名,拿到摘要,再比对原数据验证完整性。这样就可以像签署文件一样,证明消息确实是发送方发的。

  • 数字证书:接收方能够使用公钥验签,但是公钥是公开的。我们还缺少防止黑客伪造公钥的手段,也就是说,怎么来判断这个公钥就是发送方的公钥呢?CA(证书认证机构)具有极高的可信度,由它来为各个公钥签名,这样的公钥就是可信的。CA 对公钥的签名认证也是有格式的,不是简单地把公钥绑定在持有者身份上就完事了,还要包含序列号、用途、颁发者、有效时间等等,把这些打成一个包再签名,完整地证明公钥关联的各种信息,形成数字证书。

  • 通信双方的数据是加密传输的,保证了数据是保密的,没有被窃听;使用摘要保证了数据的完整性,没有被篡改;使用数字签名,保证了发送方的身份是可靠的,没有被伪造;使用数字证书,保证了接收方的身份是可信的。

Redis 基础

熟悉 Redis 基础知识:什么是 Redis,Redis 为什么这么快,Redis 线程模型,Redis 内存管理,Redis 底层数据结构

Redis 缓存

熟悉常见的生产问题:缓存雪崩,缓存击穿,缓存穿透,保证 Redis 缓存和数据库的一致性(即 Redis 读写策略)

Redis 持久化

AOF 日志:

写后日志,先执行命令,后写日志,然后把日志写入内存缓冲区,在合适的时机刷入磁盘(避免写入错误的命令,减小检查开销;不会阻塞当前的写入操作)

写后日志也会带来问题:不阻塞当前写操作,但可能会阻塞后面的写操作;先执行命令后,服务挂了,日志没有及时写入。这就要研究 AOF 写入磁盘的时机了。

同步写回:每条命令执行完毕,立刻写回磁盘;每秒写回:每隔一秒把缓冲区内容写回磁盘;不写回,由操作系统判断合适的时机写回磁盘。这三种策略性能越来越好,对主线程的影响越来越小;但数据完整性越来越差,可能导致更多的数据丢失,无法及时写回磁盘。

随着写命令越来越多,AOF 文件会越来越大:文件过大,无法保存;文件过大,追加命令的效率变低;文件过大,数据恢复效率变低。

Redis AOF 重写日志。AOP 日志记录的内容就是具体的键值和命令,扫描整个日志文件,只保存最新的数据:把旧文件中的多条命令,改写为一条命令,减小日志文件体量。

AOF 重写日志也会阻塞主线程,重写的过程很有趣(一次拷贝,两个日志):

总的来说,AOF 日志文件能很好地保证数据完整性,尽最大限度减少数据丢失,但数据恢复有点慢。

RDB 快照:

直接记录某一时刻的数据,写入内存中,再写回磁盘。

我们要对哪些数据做快照?快照期间,数据还能变化吗?

Redis 提供了两个命令来生成 RDB 文件,分别是 save 和 bgsave。save 会阻塞主线程,bgsave 会创建一个子线程,专门用来执行全量快照。

写时复制机制:现在保证了执行快照期间,可以不阻塞主线程。那么如何保证执行快照的同时,主线程也能正常执行写操作,变化数据呢。

写时复制机制保证快照期间数据可修改,这个过程也很有趣(拟修改,数据副本):

如何控制快照频率?频繁地执行全量快照写入磁盘,会给磁盘带来很大压力。可以执行增量快照,只写入变化的数据。这就需要记住哪些数据被修改(键值对),也会带来空间性能开销。

总的来说,RDB 快照实现了数据的快速恢复,但是不能很好的保证数据的完整性,因为频率不好把控。

混合持久化:设置合适的快照间隔,在两次快照的间隔期间,使用 AOF 日志持久化。AOF 日志能很好地解决两次快照期间的数据丢失问题,当第二次快照执行完毕,前一次 AOF 日志就可以直接清空,使用新的 RDB 快照进行快速回复就行了。这个方法既能享受到 RDB 文件快速恢复的好处,又能享受到 AOF 只记录操作命令、尽可能保证数据不丢失的简单优势。

Redission 分布式锁

解决问题:分布式锁,即分布式系统中的锁,主要用于在分布式系统中控制共享资源的访问。

主要基于 Redis 的setnx 命令实现,还使用 lua 脚本保证了释放锁的原子性。

实现原理:

多个客户端同时竞争锁。客户端在尝试获取锁时,向 Redis 发送 setnx 命令,设置一个随机的 UUID 作为 value。如果设置成功,则说明该客户端抢到锁了,可以执行临界区的代码。如果设置失败,客户端可以选择等待一段时间再次获取,或者直接立即返回。临界区代码执行完毕,该客户端需要释放锁,使用 lua 脚本实现正确释放锁,保证释放锁的原子性。为了防止执行过程中崩溃导致锁无法及时释放,还实现了锁的自动续期机制。使用定时任务,当客户端持有锁时,延长锁的过期时间。用户释放锁,就取消定时任务,直接释放锁。

使用步骤:根据 key 创建锁对象,并在临界区代码前后分别调用lock()和unlock()方法来获取和释放锁。

Reddssion 分布式限流

解决问题:在分布式系统中,对系统的访问进行限流,以避免过多的请求导致系统崩溃。

主要基于 Redis 的list 数据结构实现,还使用 lua 脚本保证获取和释放令牌的原子性。

实现原理:

当客户端请求到达,会向 Redis 发送一个 lua 脚本尝试获取令牌,即往 list 数据结构中添加一个元素,list 容量反映了令牌发放的数量。客户端成功获取到令牌,就可以进行接下来的操作。如果 list 已满,即达到最大容量,客户端就获取不到令牌。此时客户端可以选择等待、重试。为了防止令牌堆积,实现了令牌的过期机制,当令牌存在的时间超过一定阈值,就会自动移除令牌。

使用步骤:根据 key 创建限流器对象,设置每秒生成多少令牌,刷新间隔为多少,调用tryAcquire()方法尝试获取令牌。

常见的限流算法:主要包括计数器限流、滑动窗口限流、漏桶限流和令牌桶限流。这些算法都用于控制接口或服务的访问频率,以避免系统过载或崩溃。

  1. 计数器限流
    • 原理:在固定时间段内记录并限制接口调用的次数。例如,设定每分钟只能调用100次接口。
    • 实现:每次接口被调用时,计数器加1。如果当前时间与第一次调用时间的间隔不超过设定时间段(如1分钟),且计数器超过限定的次数(如100次),则拒绝新的调用。
    • 缺陷:在时间段临界值附近,如果请求密集,可能导致单位时间内调用次数超过限流次数。
  2. 滑动窗口限流
    • 原理:以时间窗口为滑动单位,记录并限制在窗口时间内的接口调用次数。
    • 实现:窗口随时间滑动,每次检查当前时间窗口内的调用次数是否超过限定值。
    • 优点:解决了计数器限流在临界值附近的问题。
  3. 漏桶限流
    • 原理:将请求比作水,漏桶比作系统处理能力。无论流入多少水,漏桶流出的水是恒定的。
    • 实现:请求按照固定速率流出,当请求流入速率超过漏桶的流出速率时,多余的请求会被拒绝。
    • 特点:限制了请求的流出速率,平滑了突发请求。
  4. 令牌桶限流
    • 原理:系统以固定速率往令牌桶中添加令牌,每次请求需要消耗一个令牌。
    • 实现:如果请求到来时桶中有令牌,则消耗一个令牌并处理请求;否则,拒绝请求。
    • 特点:允许一定程度的突发流量,只要桶中有令牌就可以处理请求。

这些限流算法各有特点,选择哪种算法取决于具体的业务场景和需求。例如,计数器限流实现简单但可能存在临界值问题;滑动窗口限流解决了临界值问题但实现相对复杂;漏桶限流平滑了突发请求但可能限制了系统的处理能力;令牌桶限流则允许突发请求并提供了较好的灵活性。

Redission 分布式 session

当然,基于Redis的分布式Session是一种解决分布式系统中Session共享问题的方案。下面我尽量用简单明了、通俗易懂的方式为您介绍。

首先,我们要明白什么是Session。Session简单来说,就是服务器为每一个客户端(如浏览器)创建的会话,用来保存用户的状态信息。在传统的单机系统中,Session信息通常保存在服务器的内存中。

然而,当我们的系统变成分布式系统,也就是说有多个服务器节点时,问题就出现了。因为每个节点都是独立的,它们之间的Session信息并不共享。这就会导致一个用户从一个节点登录后,访问另一个节点时却需要重新登录,因为那个节点没有他的Session信息。

为了解决这个问题,我们可以使用Redis来实现分布式Session。Redis是一个高性能的内存数据库,可以作为共享存储来保存Session信息。

基于Redis的分布式Session的原理是这样的:当用户在一个节点上登录时,服务器会在Redis中为这个用户创建一个Session,并保存用户的状态信息。然后,服务器会把这个Session的ID返回给客户端(通常是放在Cookie里)。接下来,无论用户访问哪个节点,这个节点都会从Redis中根据Session ID获取用户的Session信息,从而知道用户的状态。

这种方式的好处是,所有的Session信息都保存在Redis这个共享的存储中,所以无论用户访问哪个节点,都能获取到正确的Session信息。这就实现了Session的共享,解决了分布式系统中的Session问题。

总的来说,基于Redis的分布式Session是一种高效、可靠的解决方案,能够让我们在分布式系统中方便地管理用户的会话状态。

定时任务的原理

简单来说,@Scheduled 注解在Spring框架中就像一个定时钟,你可以告诉它每隔多少时间或者按照什么规律来执行某个方法。当Spring应用启动时,它会检查哪些方法上使用了这个注解,并按照设定的规则自动去调用这些方法。这样,你就可以很方便地实现定时任务,比如每天定时发送邮件、每小时检查一次数据库等,而不需要手动去编写复杂的线程和调度逻辑。

在Java中,@Scheduled 注解通常与Spring框架一起使用,用于实现定时任务。这个注解提供了一种简洁的方式,使得开发者可以很容易地配置定时任务,而无需手动创建和管理线程。

以下是 @Scheduled 注解的一些关键点及其工作原理:

  1. 注解定义:
    @Scheduled 是Spring框架中的一个注解,它用于标记一个方法作为定时任务。这个注解可以定义任务的执行频率、开始延迟等。
  2. 配置:
    为了使用 @Scheduled 注解,你需要在Spring配置中启用任务调度功能。这通常通过在配置类上添加 @EnableScheduling 注解来完成。
  3. 任务注册:
    当Spring容器启动时,它会扫描所有带有 @Scheduled 注解的方法,并将这些方法注册为定时任务。
  4. 任务调度器:
    Spring内部使用一个任务调度器(例如,基于Java的 ScheduledThreadPoolExecutor)来管理这些定时任务。调度器会根据 @Scheduled 注解中定义的规则(如固定速率、固定延迟或Cron表达式)来安排任务的执行。
  5. 线程管理:
    调度器使用一个线程池来执行这些任务,这意味着多个定时任务可以并发执行。线程池的大小和配置可以根据需要进行调整。
  6. 异常处理:
    如果定时任务在执行过程中抛出异常,Spring会捕获这个异常并记录它。你可以配置异常处理器来进一步处理这些异常,例如,发送通知或记录到日志文件中。
  7. 动态性:
    虽然 @Scheduled 注解提供了一种静态的方式来定义定时任务,但Spring还提供了更高级的功能,如动态地创建和修改定时任务。这通常通过编程方式使用 TaskScheduler 接口来实现。

下面是一个简单的示例,展示了如何使用 @Scheduled 注解来创建一个定时任务:

1
2
3
4
5
6
7
8
9
10
11
12
13
import org.springframework.scheduling.annotation.EnableScheduling;  
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

@Component
@EnableScheduling
public class ScheduledTasks {

@Scheduled(fixedRate = 5000) // 每5秒执行一次
public void reportCurrentTime() {
System.out.println("当前时间: " + new Date());
}
}

在这个例子中,reportCurrentTime 方法会被Spring每隔5秒调用一次,因为我们在 @Scheduled 注解中设置了 fixedRate = 5000(以毫秒为单位)。

总之,@Scheduled 注解提供了一种声明式的方式来创建和管理定时任务,使得开发者能够专注于业务逻辑,而无需过多关注任务的调度和执行细节。

TCP 三次握手 / 四次挥手扩展问题

TCP 连接三次握手的过程是怎样的?

TCP 是面向连接的协议,使用 TCP 前必须建立连接。

  • 一开始,客户端和服务端都处于 CLOSE 状态。
  • 服务端主动监听某端口,进入 LISTEN 状态。
  • 客户端随机初始化序号(c1),即 TCP 报文首部的序号字段,同时把同步标志位置为1,表示这是一个 TCP 连接请求报文。接着客户端把该报文发往服务端,表示向服务端发起连接,之后客户端进入同步已发送状态。
  • 服务端收到客户端的 TCP 连接请求报文。
  • 服务端首先初始化序号(s1),即 TCP 报文首部的序号字段;然后初始化确认应答号为 c1 + 1,表示该序号之前的内容全部接收完成,期望收到该序号之后的报文。同时把同步标志位和确认标志位置为1,表示这是一个 TCP 连接响应报文。最后把该报文发往客户端,表示已收到并回应客户端的连接请求,之后服务端进入同步已接收状态。
  • 客户端收到服务端的 TCP 连接响应报文。
  • 客户端还要向服务端发送最后一个应答报文,这个报文是普通的 TCP 报文。客户端设置确认应答号为 s1 + 1,同时设置确认标志位为1,表示这是一个普通的 TCP 应答报文。客户端把该报文发往服务端,这次报文可以携带客户端的数据到服务端,之后客户端进入已连接状态。
  • 服务端收到客户端的应答报文后,也进入已连接状态。

一旦完成三次握手,双方都处于已连接状态,连接建立完成,客户端和服务端就可以相互发送数据了。

TCP 四次挥手的过程是怎样的?

双方都可以主动断开连接,以客户端主动断开连接为例。

  • 客户端打算关闭连接,把终止标志位置为1,表示这是一个连接终止请求报文。客户端把该报文发往服务端,进入终止等待1状态。
  • 服务端收到该报文,向客户端发送普通的 TCP 应答报文,之后服务端进入关闭等待状态。
  • 客户端收到服务端的应答报文,之后进入终止等待2状态。
  • 等待服务端处理完数据,服务端再把终止标志位置为1,向客户端发送连接终止请求报文,之后服务端进入最后等待状态。
  • 客户端收到该报文,再次往服务端发送一个普通的 TCP 应答报文,之后客户端进入时间等待状态。
  • 服务端收到该应答报文,之后进入关闭状态,至此服务端已经完成了连接的关闭。
  • 客户端在经过 2MSL(最大报文存活时间)后,自动进入关闭状态,至此客户端也完成了连接的关闭。

JWT 原理

性能优化方案

代码层面的优化:

  1. 算法改进

    • 选择更高效的算法来替代低效的算法。例如,使用快速排序替代冒泡排序,使用哈希表替代线性搜索等。
    • 尽量减少时间复杂度较高的操作,如嵌套循环和递归调用。
  2. 数据结构选择

    • 根据数据的特点和使用场景,选择合适的数据结构。例如,如果需要频繁地插入和删除元素,可以使用链表或哈希表;如果需要保持元素的顺序,可以使用数组或平衡二叉树。
    • 避免不必要的数据结构转换和复制操作,以减少内存消耗和CPU时间。
  3. 代码精简与复用

    • 移除冗余代码和重复逻辑,提高代码的可读性和可维护性。
  • 使用函数和模块来封装重复使用的代码,减少代码的重复编写和调试工作。

系统层面的优化:

  1. 缓存策略

    • 利用缓存技术来存储和复用频繁访问的数据,减少对数据库或远程服务的访问次数。
    • 选择合适的缓存策略,如LRU(最近最少使用)算法,来管理缓存空间,确保缓存中的数据是最有价值的。
  2. 并发控制

    • 利用多线程或多进程技术,实现任务的并行处理,提高系统的吞吐量和响应速度。
    • 使用合适的并发控制机制,如锁、信号量或条件变量,来避免并发访问导致的数据冲突和死锁问题。
  3. 网络优化

    • 优化网络传输协议和参数设置,减少网络延迟和丢包率。

    • 使用压缩算法对传输的数据进行压缩,减少网络带宽的占用。

数据库层面的优化:

  1. 索引设计

    • 根据查询需求和表结构,设计合适的索引。选择具有高选择性的列作为索引键,以提高查询效率。
    • 定期分析和优化索引的使用情况,避免索引失效或冗余索引导致的性能问题。
  2. 查询优化

    • 优化SQL查询语句,减少不必要的JOIN操作、子查询和聚合函数的使用。
    • 使用数据库提供的查询执行计划工具,分析查询的执行过程,找出性能瓶颈并进行优化。
  3. 分区与分片

    • 对于大型数据库表,可以使用分区技术将数据分成多个部分,提高查询和管理效率。
    • 在分布式数据库系统中,可以使用分片技术将数据分散到多个节点上,实现负载均衡和水平扩展。
  4. 数据库配置与参数调优

    • 根据硬件环境和业务特点,调整数据库的配置参数,如缓冲区大小、连接池大小等,以获得最佳的性能表现。
  • 监控数据库的性能指标,如响应时间、吞吐量、CPU利用率等,及时发现和解决性能问题。

需要注意的是,性能优化是一个持续的过程,需要不断地对系统进行监控和分析,并根据实际情况进行调整和优化。同时,在优化过程中要权衡利弊,避免过度优化导致系统复杂性和维护成本的增加。

设计模式

什么是设计模式

设计模式是软件设计中常见的问题解决方案的归纳总结,是在特定情境下的经验性的解决方案。

它们被广泛接受和应用于软件开发中,旨在提高代码的可重用性、可维护性和灵活性。

七大原则

设计模式的七大原则是作为设计模式的基础准则,可以用来指导设计模式的选择和应用。这些原则是:

  1. 单一职责原则(SRP):一个类应该只有一个引起它变化的原因。
  2. 开放-封闭原则(OCP):软件实体(类、模块、函数等)应该对扩展是开放的,对修改是封闭的。
  3. 里氏替换原则(LSP):子类型必须能够替换其基类型,而不会影响程序的正确性。
  4. 接口隔离原则(ISP):建立最小的依赖,不要依赖不需要的接口。
  5. 依赖倒置原则(DIP):依赖于抽象,而不是具体实现。
  6. 迪米特法则(LoD):一个对象应该对其他对象保持最少的了解。
  7. 组合/聚合复用原则(CARP):优先使用组合和聚合,而不是继承。

分类

设计模式根据功能和用途可以分为三大分类:

  1. 创建型设计模式:这些模式关注对象的创建机制,主要包括工厂模式抽象工厂模式单例模式原型模式建造者模式等。
  2. 结构型设计模式:这些模式关注如何组合和使用对象,主要包括适配器模式装饰器模式代理模式组合模式享元模式桥接模式等。
  3. 行为型设计模式:这些模式关注对象之间的通信和协作方式,主要包括策略模式模板方法模式观察者模式迭代器模式访问者模式命令模式备忘录模式状态模式解释器模式等。

这些分类和原则将设计模式划分到更具体的范畴,并提供了指导设计和实施设计模式的准则。理解和应用这些原则和分类有助于开发人员更好地使用设计模式来解决问题。

Maven

Maven 学习:

基础:Maven 的下载安装、IDEA 创建 Maven 项目,了解 Maven 项目结构

依赖管理:依赖坐标(GAV)、依赖范围管理、Maven 的工作原理、Maven 的生命周期

依赖冲突:依赖的传递性、自动解决依赖冲突、排除依赖

分模块开发:通过 mvn install 命令,将不同的 Maven 项目安装到本地仓库,其他工程就能通过 GAV 坐标引入该工程了。实现业务模块拆分,简化项目管理,提高代码复用性,方便团队协作

聚合工程:一个项目允许创建多个子模块,多个子模块组成一个整体,可以统一进行项目的构建。公共的依赖、配置、插件等,都可以配置在父工程里。父工程可以定义可选依赖<dependencyManagement>,该标签里的依赖项,子工程可选择使用。子工程可以使用<optional>true</optional>开启隐藏依赖,该依赖不会传递给其他工程。Maven聚合工程可以对所有子工程进行统一构建,而不需要像传统的分模块项目一样,需要挨个打包、测试、发布。

Maven 属性:在 <properties> 标签下,自定义属性(依赖版本,项目环境属性,Java 环境变量等)。

多环境配置:

多模块开发:

①简化项目管理,拆成多个模块/子系统后,每个模块可以独立编译、打包、发布等;

②提高代码复用性,不同模块间可以相互引用,可以建立公共模块,减少代码冗余度;

③方便团队协作,多人各司其职,负责不同的模块,Git管理时也能减少交叉冲突;

④构建管理度更高,更方便做持续集成,可以根据需要灵活配置整个项目的构建流程

巩固复习了 Maven,终于明白了 API 接口开放平台的运作原理。

Maven 是什么,Maven 是一个项目构建和管理工具,是 Apache 下的一个纯 Java 开发的开源项目,我们现在基本都在使用 Maven 来构建和管理 Java 项目,当然也有其他类似的的项目构建和管理工具,比如 Gradle。(2023/12/01晚)

那么我们为什么要使用 Maven 呢?使用它有什么好处?我们首先要认识到,在没有使用 Maven 工具之前,项目构建和管理存在很多问题:

  • 依赖关系管理困难:在手动构建项目时,需要手动下载和添加项目所需的依赖库,这不仅耗时而且容易出错。此外,如果项目中有多个模块,需要确保每个模块都有正确的依赖版本,这需要花费大量时间和精力。
  • 构建过程繁琐:在没有自动化构建工具的情况下,开发人员需要手动编译、测试和打包项目。这不仅耗时,而且容易出错。此外,如果项目中有多个模块,需要分别构建每个模块,这会进一步增加构建的复杂性。
  • 项目结构五花八门:在没有统一的项目构建和管理规范的情况下,每个项目可能会有自己独特的项目结构,这使得项目之间的协作和交流变得困难。
  • 版本控制和发布困难:在没有Maven之前,版本控制和发布需要手动完成,这不仅耗时而且容易出错。此外,如果项目中有多个模块,需要分别发布每个模块,这会进一步增加发布的复杂性。
  • 团队协作效率低下:在没有Maven之前,团队成员之间需要手动共享项目文件和依赖库,这不仅效率低下,而且容易出错。此外,如果项目中有多个模块,需要分别管理每个模块的代码和依赖库,这会进一步降低团队协作的效率

总之,没有Maven之前,项目构建和管理可能会面临许多痛点,包括依赖关系管理困难构建过程繁琐项目结构五花八门版本控制和发布困难以及团队协作效率低下等问题。而 Maven 等自动化构建工具的出现有效地解决了这些问题,提高了项目构建和管理的效率和准确性。

那么Maven 项目的结构是怎样的呢?通常包括以下几个部分:

  • src:包含了项目所有的源代码和资源文件以及测试代码。其中src/main/java这个目录下储存java源代码,src/main/resources储存主要的资源文件,比如spring的xml配置文件和log4j的properties文件,src/test/java存放测试代码。
  • target:编译后内容放置的文件夹。
  • pom.xml:这是Maven的基础配置文件,也是 Maven 项目核心配置文件,相当关键,用于配置项目的基本信息、依赖范围管理、解决依赖冲突、实现分模块开发、多环境配置

其他相关的还有:Maven 的私服搭建、配置 Maven 镜像源、Maven 的生命周期

Mybatis 工作流程

加载配置文件:读取MyBatis的配置文件(mybatis-config.xml),包含全局配置信息:运行环境、数据库连接信息。加载映射文件,包含将要执行的 SQL 语句。

构建会话工厂:即创建SqlSessionFactory:使用配置文件创建SqlSessionFactory对象,该对象负责创建创建会话对象 SqlSession。

创建会话对象,根据会话工厂 SqlSessionFactory获取SqlSession对象,SqlSession包含了执行 SQL 的所有方法,代表一次会话。

执行映射文件:使用会话对象 SqlSession ,根据 Mybatis 提供的 API(增删改查语句),执行映射文件中定义的SQL语句。

处理操作结果:解析传入的参数,构建最终要执行的 SQL 语句,获取数据库连接,执行 SQL,将SQL执行的结果进行转换,映射为Java对象或集合。

返回处理结果:释放与数据库的连接资源,关闭会话对象 SqlSession,返回最终的处理结果。

结合工作实践来讲,MyBatis 所具备的亮点可总结为如下三个方面。

第一,MyBatis 本身就是一款设计非常精良、架构设计非常清晰的持久层框架,并且 MyBatis 中还使用到了很多经典的设计模式,例如,工厂方法模式、适配器模式、装饰器模式、代理模式等。 在阅读 MyBatis 代码的时候,你也许会惊奇地发现:原来大师设计出来的代码真的是一种艺术。所以,从这个层面来讲,深入研究 MyBatis 原理,甚至阅读它的源码,不仅可以帮助你快速解决工作中遇到的 MyBatis 相关问题,还可以提高你的设计思维。

第二,MyBatis 提供了很多扩展点,例如,MyBatis 的插件机制、对第三方日志框架和第三方数据源的兼容等。 正由于这种可扩展的能力,让 MyBatis 的生命力非常旺盛,这也是很多 Java 开发人员将 MyBatis 作为自己首选 Java 持久化框架的原因之一,反过来促进了 MyBatis 用户的不断壮大。

第三,开发人员使用 MyBatis 上手会非常快,具有很强的易用性和可靠性。这也是 MyBatis 流行的一个很重要的原因。当你具备了 MySQL 和 JDBC 的基础知识之后,学习 MyBatis 的难度远远小于 Hibernate 等持久化框架。

什么是 Spring MVC?

MVC 是一种常用的软件设计思想,它将业务逻辑、数据模型和界面显示分离,使得代码更加清晰、可维护。

SpringMVC 是 Spring 框架中的一个重要模块,它基于MVC(Model-View-Controller)设计模式,是一个用于构建Web应用程序的轻量级Web框架。

在SpringMVC中,Controller(控制器)负责处理用户请求并返回响应。

Model(模型)是数据的表示,它包含了应用程序的状态和业务逻辑。

View(视图)是用户界面的表示,它负责显示数据给用户。

Spring MVC 工作原理

客户端(浏览器)发送请求, DispatcherServlet拦截请求。

DispatcherServlet 根据请求信息调用 HandlerMappingHandlerMapping 根据 URL 去匹配查找能处理的 Handler(也就是我们平常说的 Controller 控制器) ,并会将请求涉及到的拦截器和 Handler 一起封装。

DispatcherServlet 调用 HandlerAdapter适配器执行 Handler

Handler 完成对用户请求的处理后,会返回一个 ModelAndView 对象给DispatcherServletModelAndView 顾名思义,包含了数据模型以及相应的视图的信息。Model 是返回的数据对象,View 是个逻辑上的 View

ViewResolver 会根据逻辑 View 查找实际的 View

DispaterServlet 把返回的 Model 传给 View(视图渲染)。

View 返回给请求者(浏览器)。

当用户发送请求到Web服务器时,SpringMVC的DispatcherServlet(前端控制器)会拦截这些请求,HandlerMapping(处理映射器)根据请求的 URL 映射 / 匹配查找能处理的 Handler(也就是我们平常说的 Controller 控制器),并调用 HandlerAdapter(处理适配器)执行相应的 Controller。Controller 会调用业务逻辑层(通常是 Service 层)来处理请求,获取相应的数据,然后将数据传递给Model。Model 将数据传递给 View 进行渲染;最后,View 将渲染结果返回给用户。

总的来说,SpringMVC 通过 MVC 设计模式将 Web 应用程序的不同部分进行分离,使得代码更加清晰、可维护,提高了开发效率。同时,SpringMVC 还提供了丰富的功能和特性,如数据绑定、异常处理、拦截器等,帮助开发人员更好地构建Web应用程序。

RequestMapping

在SpringMVC中,@RequestMapping是一个用于映射Web请求到特定处理器函数(通常是Controller中的方法)的注解。它可以定义URL路径、HTTP请求方法(GET、POST等)、请求头、请求参数等,使得Controller能够处理特定的请求。

请求控制器

请求控制器在SpringMVC中通常指的是Controller类及其中的方法。它们负责处理用户的请求,调用业务逻辑,并返回视图或数据。Controller是MVC模式中的C部分,负责接收请求和发送响应。

拦截器

拦截器(Interceptor)在SpringMVC中用于在请求处理过程中拦截用户的请求和响应,可以在请求到达Controller之前或响应返回给用户之前执行一些预处理或后处理操作。例如,可以用来进行权限验证、日志记录、性能监控等。

请求参数封装

在SpringMVC中,请求参数可以自动封装到Controller方法的参数中。SpringMVC利用参数绑定机制,可以将请求中的参数(如GET请求的查询参数、POST请求的请求体等)自动绑定到JavaBean、Map或其他数据类型中,简化了参数的获取和处理。

请求过滤器

请求过滤器(Filter)是Servlet规范中的一部分,与SpringMVC不完全相关,但经常在Java Web应用程序中使用。过滤器可以在请求到达Servlet容器中的任何资源之前或之后执行代码。它们常用于处理编码问题、记录日志、压缩响应、身份验证等。

全局异常处理

在SpringMVC中,可以通过实现HandlerExceptionResolver接口或使用@ControllerAdvice@ExceptionHandler注解来全局处理异常。这样,当Controller中的方法抛出异常时,可以统一捕获和处理这些异常,避免在Controller中分散处理异常代码,提高了代码的可维护性。

RestFul风格

RestFul风格是一种Web服务的设计和开发方式,它强调资源的表示、状态转移和HTTP方法的正确使用。在RestFul风格的Web服务中,每个URL代表一个资源,不同的HTTP方法(GET、POST、PUT、DELETE等)用于操作这些资源。这种设计方式使得Web服务更加简洁、直观和易于理解。

JSON框架

JSON(JavaScript Object Notation)是一种轻量级的数据交换格式。在Java Web应用程序中,常用的JSON框架有Jackson、Gson等。这些框架可以帮助Java应用程序将Java对象转换为JSON格式的字符串,或者将JSON格式的字符串转换为Java对象,从而方便地与前端进行数据交换。

Bean 的生命周期

简单总结 Spring Bean 生命周期流程:

  1. 实例化:启动 Spring 应用,IOC 容器为所有已声明的 Bean 创建一个实例
  2. 属性赋值:实例化后,Spring 通过反射机制给 Bean 的属性赋值
  3. 调用 Bean 的初始化方法:调用 Bean 配置的 @PostConstructafterPropertiesSet方法或者init-method指定的方法
  4. Bean 运行期:Bean 已经准备好被程序使用了,它已经被初始化并赋值完成
  5. Bean 销毁:当容器关闭时,调用Bean的销毁方法

Spring 的启动流程

Spring 的启动流程大致有以下几个关键步骤:

加载配置文件:Spring 在启动时,首先加载其 XML 格式的配置文件,配置文件中定义了 Spirng 容器中管理的 Bean 属性以及依赖关系。

解析配置文件:加载完配置文件,由 XML 解析器将将配置文件中的 Bean 定义转化为 Spring 容器可以理解和使用的内部数据结构。

创建并初始化 Bean:根据解析得到的 Bean 定义,Spring 开始创建相应的 Bean 实例,进行初始化,包括设置属性值、处理依赖关系。

注册 Bean:Bean 初始化完成会被注册到 Spring 容器中,其他 Bean 就可以通过容器获取这些 Bean 引用进行使用,这就是依赖注入。

至此,当所有的 Bean 都创建并初始化完成后,Spring 启动完成,可以对外提供服务了。

Spring Boot 的启动流程跟 Spring 的启动流程在核心逻辑上基本上是一致的,但存在一些差异:

自动化配置:传统的 Spring 应用程序,需要在 appicationContext.xml 或者 mybatis-config,xml 中手动配置数据源,包括数据库 URL、用户名、密码等,很繁琐。而 Spring Boot 提供了自动配置功能:导入 jar 包依赖,在 resource 下的 yaml / properties 配置文件里简单地写清楚配置。Spring Boot 就会自动创建已经配置好的数据源 Bean,不再需要手动配置数据源。

简化依赖管理:Spring Boot 提供了一系列 starter 依赖,只需要在项目中引入 starter 依赖,Spring Boot 便会自动引入所需的库。不再需要自己处理复杂的依赖关系,比如解决依赖冲突、版本兼容等问题,大大简化了依赖管理。

内嵌服务器:不同于 Spring 需要开发者手动配置和部署 Web 服务器。Spring Boot 内嵌了常用的 Web 服务器(如 Tomcat、Jetty 等),使得开发者无需单独配置和部署 Web 服务器。只需导入 jar 包依赖(GAV 坐标),在 resource 的 yaml / properties 配置文件中指定监听端口,在启动 Spring Boot 应用程序时,内置服务器就会自动启动并监听相应的端口。

快速启动:由于 Spring Boot 的自动化配置、简化依赖管理、内嵌服务器等,只需要直接运行 @SpringBootApplication 标注的启动类,就可以一键启动 Spring Boot 应用程序。

简化部署:通过一行命令:java -jar 等即可快速将 Spring Boot应用程序打包成一个可执行的 JAR 或 WAR 文件。再将打包好的文件复制到目标服务器上,通过一行命令就可以一键启动,部署变得十分简单和方便。

Spring 自动配置原理

Starter 依赖,集成第三方,启动类,扫描并加载所在包以及子包的 Bean,开启自动配置功能,发现并加载自动配置类,根据条件注解等,实现自动装配。

@SpringApplication是Spring Boot的核心注解,它包含了@SpringBootConfiguration@EnableAutoConfiguration@ComponentScan等注解。

  • @SpringBootConfiguration:声明当前类是配置类,允许使用@Bean注解定义bean。
  • @EnableAutoConfiguration:根据项目的依赖关系,自动配置Spring Boot项目。
  • @ComponentScan:让Spring扫描到Configuration类并把它加入到程序上下文。

SpringBoot的自动装配原理主要基于Spring框架的依赖注入和条件化Bean机制,并结合SpringBoot的特定功能和设计原则实现的。

首先,SpringBoot通过引入一系列starter依赖,使得开发者能够便捷地集成各种第三方库和框架。每个starter都包含了一系列预定义的配置和Bean定义,用于简化特定功能的集成过程。

在SpringBoot应用启动时,@SpringBootApplication注解起到了关键作用。这个注解实际上是一个复合注解,其中@EnableAutoConfiguration是实现自动装配的核心。它告诉SpringBoot开启自动配置功能。

SpringBoot的自动配置功能通过加载META-INF/spring.factories文件来发现并加载各种自动配置类。这些自动配置类使用条件注解(如@ConditionalOnClass@ConditionalOnProperty等)来确定是否应该创建和配置特定的Bean。条件注解允许根据类路径、属性设置、Bean是否存在等条件来启用或禁用自动配置。

当SpringBoot启动时,它会扫描这些自动配置类,并根据条件注解的判断来决定是否创建和注册相应的Bean到Spring容器中。如果某个自动配置类满足其条件注解指定的条件,那么它定义的Bean就会被创建并注入到Spring容器中,从而完成自动装配过程。

除了自动配置类,SpringBoot还利用了一些核心组件来实现自动装配,如AutoConfigurationImportSelectorSpringFactoriesLoaderAutoConfigurationImportSelector负责从spring.factories中加载自动配置类,并根据条件进行筛选。SpringFactoriesLoader则用于加载META-INF/spring.factories文件中定义的配置。

通过这种方式,SpringBoot能够自动配置应用的很多方面,如数据库连接、消息队列、Web服务等,从而大大简化了开发者的配置工作,提高了开发效率。

需要注意的是,虽然自动装配能够极大地简化配置工作,但在某些情况下,开发者仍然需要手动配置一些Bean或覆盖自动配置的默认设置,以满足特定的业务需求。因此,理解SpringBoot的自动装配原理并熟悉其配置方式,对于开发者来说是非常重要的。

Spring 的扩展点

API 项目介绍

API 项目介绍:分为四个模块:api-core,实现用户管理、接口管理、接口调用等;api-gateway,做统一的访问控制、流量染色、用户鉴权,完成统一登录校验、API 签名校验、接口调用统计和请求响应前后的日志处理;api-client,自主设计对外提供接口服务;api-common,抽象公共接口 / 方法、公共实体类。

使用 Maven聚合工程管理子模块,都需要在本地 install。接口调用的逻辑:请求 api-core 的接口调用服务:首先校验参数,判断接口是否存在。获取当前登录用户,拿到 ak 和 sk,以此作为参数构建 SDK 客户端。SDK 客户端通过封装请求参数(包含用户信息、调用接口信息),使用 Hutool 工具包,发送请求到网关。

网关实现全局过滤,获取请求头的一切信息:请求参数、请求路径、请求来源地址等。做 API 签名校验,鉴定用户身份;根据请求路路径和方法判断接口状态,是否存在,是否发布或下线;设置 ip 黑白名单,只允许当前服务器的请求可以通过;流量染色,给请求添加统一的请求头。完成一系列校验之后,将该合法请求转发给真正的接口服务,处理接口调用,返回响应。最后在 haddleReponse 响应处理器中,完成接口调用统计等业务逻辑,更新相关字段,比如用户剩余调用次数、接口调用总次数等。返回响应,结束整个调用流程。

不希望引入复杂的业务逻辑,为减小网关模块体量,遵循单一原则,抽象公共业务逻辑和公共实体类到 api-common 模块。引入 Dubbo 轻量的 RPC 框架,用 EnabbleDubbo、@DubboReference、@DubboServie 等,使用 Nacos 做注册中心,实现服务注册。网关服务作为消费者实现服务拉取。实现服务间方法调用。

微服务架构

从单体应用迁移到微服务架构:

单体应用线上发布和部署效率低下、团队协作开发成本高、系统可用性差。

相较于单体应用,微服务实现了更细粒度的服务拆分、更高效的服务部署和独立维护、提供了更清晰的服务治理方案。

要实现从单体应用迁移到微服务架构,就要做好服务化拆分,设计服务间调用。明确微服务架构的基本组件:服务描述、注册中心、服务框架、服务监控、服务追踪、服务治理。

CAP 理论

  • CAP定理指出,一个分布式系统不可能同时满足一致性(Consistency)、可用性(Availability)和分区容错性(Partition tolerance)这三个基本需求。
  • 在设计分布式系统时,需要根据实际需求权衡这三个属性。
  • 大多数分布式系统会选择牺牲一致性(CP)或可用性(AP)来满足分区容错性。

CAP理论是分布式系统设计中的一个基本原则,它涉及的是在一个分布式系统中,Consistency(一致性)、Availability(可用性)和Partition Tolerance(分区容错性)这三个目标之间的关系。CAP理论断言,在一个分布式系统中,最多只能同时满足其中的两个目标,而无法同时满足三个。

具体来说,一致性要求分布式系统中的所有数据备份在同一时刻具有相同的值。可用性则意味着在集群中一部分节点故障后,集群整体仍然能够响应客户端的读写请求,即系统提供的服务必须一直处于可用的状态,对于用户的每一个操作请求总是能够在有限的时间内返回结果。分区容错性则是指分布式系统虽然由多个节点组成,但对外应该表现为一个整体,即使内部某个节点或网络出现故障,系统对外也不应出现异常。

根据CAP理论,存在两种常见的模式供系统设计者选择:CP模式和AP模式。在CP模式下,系统会优先保证数据的一致性和分区容错性,这意味着在网络分区发生时,系统可能会拒绝部分请求以确保数据的一致性。这种模式适用于对数据一致性要求较高的场景,如金融系统。而在AP模式下,系统则会优先保证可用性和分区容错性,即使在网络分区发生时,系统仍然可以接受请求并提供部分功能,这种模式适用于对可用性要求较高的场景。

因此,在设计分布式系统时,需要根据实际应用场景和需求来权衡这三个目标,从而选择适合的CAP策略。

服务发布和引用

注册中心(服务注册和发现)

在微服务架构下,主要有三种角色:服务提供者(RPC Server)、服务消费者(RPC Client)和服务注册中心(Registry),三者的交互关系请看下面这张图,我来简单解释一下。

RPC Server提供服务,在启动时,根据服务发布文件server.xml中的配置的信息,向Registry注册自身服务,并向Registry定期发送心跳汇报存活状态。

RPC Client调用服务,在启动时,根据服务引用文件client.xml中配置的信息,向Registry订阅服务,把Registry返回的服务节点列表缓存在本地内存中,并与RPC Sever建立连接。

当RPC Server节点发生变更时,Registry会同步变更,RPC Client感知后会刷新本地内存中缓存的服务节点列表。

RPC Client从本地缓存的服务节点列表中,基于负载均衡算法选择一台RPC Sever发起调用。

RPC 调用

想要完成 RPC 调用,你需要解决四个问题:

  • 客户端和服务端如何建立网络连接?
  • 服务端如何处理请求?(选择合适的通信框架比如 Netty,解决客户端与服务端如何建立连接、管理连接以及服务端如何处理请求的问题)
  • 数据传输采用什么协议?(选择合适的通信协议比如 HTTP,解决客户端和服务端采用哪种数据传输协议的问题)
  • 数据该如何序列化和反序列化?(多种序列化格式比如 Java 原生序列化、JSON、XML、Thrift,解决客户端和服务端采用哪种数据编解码的问题)

Socket通信

Socket通信是基于TCP/IP协议的封装,建立一次Socket连接至少需要一对套接字,其中一个运行于客户端,称为ClientSocket ;另一个运行于服务器端,称为ServerSocket 。就像下图所描述的,Socket通信的过程分为四个步骤:服务器监听、客户端请求、连接确认、数据传输。

  • 服务器监听:ServerSocket通过调用bind()函数绑定某个具体端口,然后调用listen()函数实时监控网络状态,等待客户端的连接请求。
  • 客户端请求:ClientSocket调用connect()函数向ServerSocket绑定的地址和端口发起连接请求。
  • 服务端连接确认:当ServerSocket监听到或者接收到ClientSocket的连接请求时,调用accept()函数响应ClientSocket的请求,同客户端建立连接。
  • 数据传输:当ClientSocket和ServerSocket建立连接后,ClientSocket调用send()函数,ServerSocket调用receive()函数,ServerSocket处理完请求后,调用send()函数,ClientSocket调用receive()函数,就可以得到得到返回结果。

服务监控

服务监控主要包括四个流程:数据采集、数据传输、数据处理和数据展示

监控对象有用户端监控、接口监控、资源监控、基础监控,监控指标有请求量、响应时间和错误率

服务追踪

在微服务架构下,由于进行了服务拆分,一次请求往往需要涉及多个服务,每个服务可能是由不同的团队开发,使用了不同的编程语言,还有可能部署在不同的机器上,分布在不同的数据中心。

如果有一个系统,可以跟踪记录一次用户请求都发起了哪些调用,经过哪些服务处理,并且记录每一次调用所涉及的服务的详细信息,这时候如果发生调用失败,你就可以通过这个日志快速定位是在哪个环节出了问题,这个系统就是今天我要讲解的服务追踪系统。

服务治理

在一次服务调用过程中,服务提供者、服务消费者、注册中心、网络都有可能出问题。我们要尽可能保证服务调用成功,这就是服务治理。

服务治理可以从很多方面考虑:

  • 从服务健康状态考虑。要做到服务节点管理,比如使用心跳检测机制,这种机制要求服务提供者定时的主动向注册中心汇报心跳
  • 从服务节点访问优先级考虑。一般情况下,服务提供者节点不是唯一的,多是以集群的方式存在。要选择合适的负载均衡策略,充分利用机器的性能。
  • 从调用的健康状态考虑。服务调用并不总是一定成功的,对于服务调用失败的情况,需要有手段自动恢复,来保证调用成功。

Dubbo 的工作原理

Dubbo框架的工作原理主要基于服务注册与发现、远程调用、负载均衡以及服务监控等核心机制。以下是Dubbo框架工作原理的详细解释:

  1. 服务注册与发现
    • 服务提供者(Provider)在启动时,会将自己提供的服务接口信息注册到注册中心(Registry)。注册中心是一个服务目录框架,用于服务的注册和服务事件的发布和订阅。
    • 服务消费者(Consumer)在启动时,会向注册中心订阅自己所需的服务。注册中心会返回服务提供者地址列表给消费者。如果服务提供者地址列表有变更,注册中心会基于长连接推送变更数据给消费者。
  2. 远程调用
    • Dubbo采用了代理机制来实现远程调用。服务消费者通过代理对象来调用远程服务,而代理对象会负责将调用请求序列化为网络传输格式,并通过网络通信框架发送给服务提供者。
    • 服务提供者接收到请求后,会进行反序列化,并执行相应的服务逻辑。执行结果同样会被序列化为网络传输格式,并返回给服务消费者。
  3. 负载均衡
    • Dubbo提供了多种负载均衡策略,如随机、轮询、一致性哈希等。服务消费者在调用远程服务时,会根据负载均衡策略从服务提供者地址列表中选择一个合适的服务提供者进行调用。
    • 如果调用失败,服务消费者可以根据配置选择重试或切换到其他服务提供者。
  4. 服务监控
    • Dubbo提供了服务监控中心(Monitor),用于统计服务的调用次数、调用时间等日志信息。服务提供者和消费者在内存中累计调用次数和调用时间,并定时发送统计数据到监控中心。
    • 监控中心可以对这些数据进行分析和可视化展示,帮助开发人员了解服务的运行状况,并进行性能调优和故障排查。

总的来说,Dubbo框架通过注册中心实现了服务的动态注册与发现,通过代理机制实现了远程调用的透明化,通过负载均衡策略保证了服务的可用性和性能,通过监控中心提供了服务的运行状况分析和优化手段。这些机制共同构成了Dubbo框架的核心工作原理,使得分布式系统中的服务调用更加高效、可靠和易于管理。

Dubbo是一个高性能、轻量级的开源Java RPC框架,主要用于服务之间的远程调用。其实现原理和流程如下:

实现原理

  1. 代理机制:Dubbo通过Java的动态代理或CGLIB代理生成服务消费者端的代理对象。当消费者调用服务时,实际上是调用这个代理对象。
  2. 注册中心:服务提供者将服务注册到注册中心(如Zookeeper、Nacos等),服务消费者从注册中心订阅所需的服务。注册中心负责服务的发现和通知。
  3. 序列化与反序列化:Dubbo使用特定的序列化协议(如Hessian2、Kryo等)将请求和响应数据进行序列化和反序列化,以便在网络中传输。
  4. 通信协议:Dubbo定义了自己的通信协议,包括请求头、请求体等结构,用于服务提供者和消费者之间的通信。

流程

  1. 服务提供者启动时,将服务注册到注册中心。
  2. 服务消费者启动时,向注册中心订阅所需的服务。
  3. 注册中心通知消费者可用的服务提供者列表。
  4. 消费者通过负载均衡策略选择一个提供者,并通过代理对象发起远程调用。
  5. 代理对象将调用请求序列化后发送给选定的提供者。
  6. 提供者接收到请求后,进行反序列化,并执行相应的服务逻辑。
  7. 提供者将执行结果序列化后返回给消费者。
  8. 消费者接收到结果后,进行反序列化,并返回给调用方。

为什么可以调用到对应服务

Dubbo通过注册中心实现了服务的动态发现和注册,使得服务消费者能够知道哪些服务提供者是可用的。同时,Dubbo的代理机制和通信协议使得消费者能够通过网络调用远程服务提供者。这种机制保证了服务的透明性和可扩展性,使得消费者无需关心服务的具体位置和实现细节,只需关注服务接口即可。

Dubbo是一个高性能、轻量级的开源Java RPC框架。它的核心作用是实现远程服务调用,即在不同的进程或机器之间进行通信。Dubbo采用了服务提供者和服务消费者的角色划分,通过注册中心进行服务的注册与发现。服务提供者将服务接口实现暴露出来,并注册到注册中心;服务消费者从注册中心获取服务提供者的地址列表,然后通过代理对象调用远程服务。Dubbo支持多种通信协议和序列化方式,可以根据不同的场景选择合适的配置。

在使用Dubbo时,我会先定义服务接口,并在服务提供者中实现该接口。然后,在Dubbo的配置文件中指定服务接口的实现类、注册中心地址等信息。服务提供者启动后,Dubbo会自动将服务注册到注册中心。在服务消费者端,我只需要引入服务提供者的接口依赖,并在Dubbo的配置文件中指定所需服务的名称和注册中心地址。Dubbo会为我们生成代理对象,通过该对象就可以像调用本地方法一样调用远程服务。

Nacos 工作原理

Nacos的核心运作原理主要围绕服务注册与发现、配置管理、服务管理以及集群管理展开。

首先,服务注册与发现。当服务提供者启动时,它会将自身的服务信息,如IP地址、端口号、服务名称等,注册到Nacos平台。而服务消费者则通过订阅或轮询的方式,从Nacos平台获取可用的服务列表,以便在需要时调用这些服务。这样,服务提供者和服务消费者就可以实现动态的关联和通信。

其次,配置管理。Nacos提供了一个集中式的配置中心,开发人员可以将各种配置信息,如数据库连接信息、系统参数等,存储在这个中心化的配置中心中。服务消费者可以实时地从配置中心获取最新的配置信息,而无需重启应用程序。这大大提高了系统的灵活性和可维护性。

再者,服务管理。Nacos提供了丰富的服务管理功能,包括服务的启停、监控、日志查看等。开发人员可以通过Nacos的界面或API对服务进行实时的管理和控制,确保服务的稳定运行。

最后,集群管理。Nacos支持服务的集群管理,通过负载均衡、容错和故障转移等机制,确保服务的可用性和可靠性。即使部分服务节点出现故障,Nacos也能自动调整服务调用策略,确保服务的连续性和稳定性。

总的来说,Nacos通过集中式的服务注册与发现、配置管理、服务管理以及集群管理等功能,为分布式系统提供了一套高效、稳定且易于管理的解决方案。这使得开发人员能够更专注于业务逻辑的实现,而无需过多关注服务的部署、管理和维护等琐碎事务。

首先,Nacos是一个更易于构建云原生应用的动态服务发现、配置管理和服务管理平台。它的注册中心功能主要实现了服务的注册与发现。当服务提供者启动时,它会将自己的服务信息注册到Nacos注册中心;服务消费者启动时,会从Nacos注册中心获取所需服务提供者的地址列表。这样,服务消费者就可以通过负载均衡策略选择一个服务提供者进行通信。同时,Nacos还支持服务的健康检查、服务上下线通知等功能,以确保服务的可用性和稳定性。

在实际使用中,我会根据项目的需求配置Nacos服务端的地址和端口,然后在服务提供者和消费者中引入Nacos的客户端依赖。在服务提供者中,我会配置服务名称、版本等信息,并启动服务注册。在服务消费者中,我会配置所需服务的名称和版本,然后启动服务发现。通过Nacos的API,我可以方便地获取服务提供者的地址列表,并进行负载均衡调用。

通过结合使用Nacos注册中心和Dubbo RPC框架,我能够构建出高效、稳定且易于扩展的分布式系统。在实际项目中,我会根据业务需求进行配置和优化,确保服务的性能和可靠性。同时,我也会关注相关的技术动态和最佳实践,不断学习和提升自己的能力。

希望以上回答能够满足您的要求,如果有进一步的问题或需要更多的细节,请随时提问。

Elasticsearch

什么是 Elasticsearch?

Elasticsearch是一个基于Lucene构建的开源、分布式、RESTful搜索引擎。它设计用于云计算中,能够达到实时搜索,稳定,可靠,快速,安装使用方便。

Elasticsearch的运作原理主要基于以下几个关键概念:

  1. 节点与集群:每个节点是一个运行在单个机器上的独立的Elasticsearch实例。集群中的节点通过互相通信和协调工作来实现数据的分布式存储和搜索。这种分布式的架构使得Elasticsearch能够处理大规模的数据集,并提供高可用性和容错能力。
  2. 索引与文档:在Elasticsearch中,索引是逻辑上相关的文档集合,类似于关系数据库中的数据库。文档是可以被索引和搜索的基本信息单位,它由一组字段组成,每个字段包含一个数据值。文档使用JSON格式表示,可以包含各种字段的复杂结构。
  3. 分片与副本:为了支持横向扩展和处理大规模数据集,Elasticsearch将索引分割成多个分片。每个分片是一个独立的索引,可以分布在不同的节点上。分片可以并行处理搜索请求,并在集群中进行负载均衡。此外,为了数据的冗余备份和故障恢复,Elasticsearch还提供了副本机制,副本分布在不同的节点上,确保数据的可靠性和高可用性。
  4. 倒排索引:Elasticsearch使用倒排索引来加速搜索过程。倒排索引与传统的正向索引不同,它不是按照文档的ID来建立索引,而是以词为单位,记录每个词在哪些文档中出现以及出现的位置等信息。这样,当用户进行搜索时,Elasticsearch可以快速定位到包含相关词的文档,并返回结果。

在运作过程中,用户将数据提交到Elasticsearch数据库中,数据首先经过分词控制器进行分词处理,然后将分词结果和权重信息存入倒排索引中。当用户发起搜索请求时,Elasticsearch根据倒排索引快速定位到相关文档,并根据权重进行排名和打分,最终将结果呈现给用户。由于Elasticsearch采用了近实时搜索技术,从建立索引到索引可以被搜索之间的延迟通常很小,通常是1秒左右,这使得Elasticsearch能够满足实时搜索的需求。

总的来说,Elasticsearch通过分布式架构、倒排索引以及实时搜索等技术,实现了高效、可靠的大规模数据搜索和处理能力。

Elasticsearch 的运作原理

首先,我们要理解Elasticsearch的基本架构和核心概念。Elasticsearch是一个基于Lucene构建的分布式搜索和分析引擎,它的核心是由多个节点组成的集群,每个节点上运行着Elasticsearch的实例。

数据写入过程

  1. 创建索引:在Elasticsearch中,索引是一个包含多个文档的集合。当我们需要存储数据时,首先会定义一个索引,并为它配置相应的设置和映射。
  2. 写入文档:向Elasticsearch写入数据是通过发送HTTP请求来实现的,通常是将JSON格式的文档发送到指定的索引中。每个文档都有一个唯一的ID,用于在索引中标识它。
  3. 分词与倒排索引:当文档被写入Elasticsearch时,它首先会通过分词器进行分词处理。分词器将文档中的文本分解成单独的词或词组(称为词条)。然后,Elasticsearch为每个词条创建倒排索引。倒排索引是一个数据结构,它记录了每个词条在哪些文档中出现,以及出现的位置和频率等信息。这样,当执行搜索查询时,Elasticsearch可以快速定位到包含相关词条的文档。

搜索查询过程

  1. 发送查询请求:用户通过发送HTTP请求来执行搜索查询。查询请求可以包含各种条件,如关键词、范围、过滤器等。
  2. 解析查询:Elasticsearch接收到查询请求后,会解析查询语句,将其转换为内部可执行的查询对象。
  3. 搜索倒排索引:Elasticsearch使用解析后的查询对象在倒排索引中进行搜索。它会查找与查询条件匹配的词条,并获取包含这些词条的文档列表。
  4. 排序与评分:根据查询条件,Elasticsearch会对文档进行排序和评分。排序可以根据字段的值进行升序或降序排列,而评分则基于文档与查询的匹配程度进行计算。
  5. 返回结果:最后,Elasticsearch将搜索结果的文档列表返回给用户。这个结果列表通常包含文档的ID、字段值以及评分等信息。

在整个过程中,Elasticsearch的分布式架构发挥了重要作用。通过将索引分成多个分片,并在集群中的不同节点上进行存储和查询,Elasticsearch能够实现水平扩展,处理大规模的数据集。同时,通过复制分片到不同的节点,Elasticsearch还提供了高可用性和容错能力,确保数据的可靠性和稳定性。

此外,Elasticsearch还支持实时搜索和近实时搜索。实时搜索意味着在文档被写入后立即就可以进行搜索,而近实时搜索则允许在文档写入后有极短的延迟时间(通常是几百毫秒到几秒)后进行搜索。这种实时或近实时的能力使得Elasticsearch非常适合于需要快速响应的搜索和分析场景。

总结来说,Elasticsearch通过分词、倒排索引、分布式存储和查询等技术,实现了高效、可靠的大规模数据搜索和分析功能。

RabbitMQ 消息队列

RabbitMQ实现消息队列的功能,主要依赖于其内部的几个核心组件以及它们之间的交互。以下是RabbitMQ实现消息传递的基本过程:

  1. 生产者(Producer)与交换机(Exchange)
    • 生产者负责创建并发送消息。这些消息不是直接发送到队列,而是首先发送到交换机。
    • 交换机负责接收来自生产者的消息,并根据其类型、路由键(Routing Key)或其他属性来决定这些消息应该发送到哪些队列。RabbitMQ支持多种类型的交换机,如直接交换机、主题交换机等,每种类型都有其特定的路由逻辑。
  2. 队列(Queue)
    • 队列是存储消息的容器。交换机根据路由规则将消息发送到相应的队列。
    • 队列是持久的,即使RabbitMQ服务器重启,队列中的消息也不会丢失(当然,这取决于队列和消息的持久化设置)。
  3. 消费者(Consumer)
    • 消费者是从队列中接收并处理消息的程序。
    • 消费者通过订阅队列来接收消息。一旦有消息到达队列,且消费者处于活跃状态,消息就会被传递给消费者进行处理。
  4. 绑定(Binding)
    • 绑定是交换机和队列之间的连接关系。它定义了交换机如何将消息路由到队列。
    • 通过定义绑定,可以实现灵活的路由策略,使得消息能够按照预期的方式流动。
  5. 确认机制(Acknowledgment)
    • 为了确保消息的可靠传递,RabbitMQ提供了消息确认机制。当消费者成功处理一条消息后,它会发送一个确认信号给RabbitMQ,RabbitMQ收到确认后才会从队列中删除该消息。
    • 如果消费者在处理消息时失败或崩溃,而没有发送确认信号,RabbitMQ会认为该消息没有被成功处理,并重新将其放回队列以供其他消费者处理。
  6. 持久化(Persistence)
    • 为了确保在RabbitMQ服务器故障时不会丢失数据,可以将交换机、队列和消息标记为持久的。这样,即使服务器重启,这些数据也会被保留下来。

综上所述,RabbitMQ通过交换机、队列、消费者、绑定、确认机制和持久化等功能,实现了消息的可靠传递和系统的解耦。这使得RabbitMQ成为一个强大且灵活的消息队列系统,广泛应用于各种分布式系统和微服务架构中。

简单知识复盘

怎么让两个线程轮流执行?使用wait()和notify()。一个线程执行完之后调用notify()唤醒正在等待中的线程,自身调用wait()进入等待状态。

static静态变量有什么优势/缺点?节省内存空间,方便访问;数据不一致,难以理解和维护。

UDP与TCP比较。无连接,不可靠,实时性较高,能容忍一定的数据丢失,不保证数据的顺序性完整性,没有序列号、确认机制、超市重传、流量控制、拥塞控制。

为什么选择 Redis?Redis 是单独的中间件,不同客户端把session存放在redis上,实现在分布式结构中的资源可见性,解决用户登录失效问题;Redis 单线程,支持 lua 脚本,保证了并发操作安全的同时,能很好地实现分布式锁;作为缓存数据库,存储热门数据,减轻数据库压力。

项目的开发流程:参考已有的产品学习了解,总结比较好的功能点。结合自己扩展的功能特色,做整体设计,经过需求分析后得到产品原型。选用合适的技术,解决具体的业务问题。

为什么使用 Dubbo RPC?首先为了减小网关模块的体量,避免引入复杂的业务逻辑,保证设计模式遵循的单一原则,决定使用 RPC 实现服务间调用。OpenFeign 的方式也考虑过,本质上是构建 HTTP 请求,发送请求去调用对外提供的服务。这种方式需要添加很多请求头,使用 JSON 序列化的效率也不高,更加适合外部服务。选用 Dubbo,基于 TCP 协议,避免无用的请求头,序列化为二进制流传输,效率高。

如何使用 RPC 框架?详细的业务流程。实现服务间交互,先说说具体的技术选型👆和实现原理(Nacos 注册中心、服务消费者、服务生产者等),获取用户相关信息,用户是否存在,有没有权限调用,API 签名校验是否通过;获取接口相关信息,接口是否存在,免费的还是付费的,是否发布或者已下线;做好接口调用统计,在 HaddleResponse 处理器中,发起 RPC调用,操作数据库,用户可调用次数减少、接口被调用次数增加(如何实现?👇)。

网关保证接口可用和稳定性,隐藏真实的接口地址,请求转发。实现全局过滤,获取请求头的一切信息:请求参数、请求路径、请求来源地址等。做 API 签名校验,鉴定用户身份;根据请求路路径和方法判断接口状态,是否存在,是否发布或下线;设置 ip 黑白名单,只允许当前服务器的请求可以通过;流量染色,给请求添加统一的请求头。完成一系列校验之后,将该合法请求转发给真正的接口服务,处理接口调用,返回响应。最后在 haddleReponse 响应处理器中,完成接口调用统计等业务逻辑,更新相关字段,比如用户剩余调用次数、接口调用总次数等。返回响应,结束整个调用流程。

接口调用次数排行怎么实现?每次接口调用完成,在 haddleResponse 处理器中发起 RPC 请求,更新数据库。并发请求下,会出现统计不准确的问题。没有在业务层面加锁,数据库并发写,压力太大。可以使用 Redis,Sorted Set 数据结构,score 权值实现排序。不使用加锁影响效率,同时单线程 Redis 很好解决并发问题。持久化。

怎么实现 SDK 的?实现过程 + 实现原理。添加依赖,编写自动配置类,添加 @Configuration 注解;在 resouce / MEATA-INF 下的 spring.factories 文件下,指定自动配置类的全路径;绑定配置文件,@ CofigurationProperties 加载配置文件,映射为 Java 类;执行 mvn install 命令,安装到本地仓库,其他模块导入依赖后,编写配置文件中的 ak,sk,即可拿到客户端 SDK 对象,发起接口调用。

实现原理就是 Spring Boot 自动装配机制。

一个对象的创建流程?(最好能接着说类加载机制;最好能说出创建对象有几种方式:反射,序列化,unsafe类实现Cloneable接口重写clone方法)

arrayList 的 remove 是怎么实现的?从列表中删除指定元素,有多种重载形式。按值删除,遍历整个列表寻找给定元素,找到就删除,同时将后面元素向前移动一个位置。按索引删除,直接定位到给定索引的元素,执行删除操作。

Java 8 的新特性有用过哪些?stream 流,Date/Time API,BigDecimal,Optional 容器等。

有了解过 HashMap 的 put() 和扩容机制吗?有了解过 ArayList 的 add() 和扩容机制吗?

对于开源框架的深入理解,比如Spring 的启动流程、Spring IOC 创建流程、Bean 的生命周期、Spring MVC 处理流程、Spring Boot 的启动流程和自动配置原理、Spring Boot 的扩展点等。了解了这些,就更能认识到 Spring Boot 的简洁和快速。

设计模式的七大原则

  • 单一职责原则:一个类应该只有一个引起变化的原因。
  • 开放封闭原则:软件实体(类、模块、函数等)应该是可扩展的,但是不可修改。
  • 里氏替换原则:子类必须能够替换其基类。
  • 接口隔离原则:使用多个专门的接口,而不使用单一的总接口,客户端不应该依赖它不需要的接口。
  • 依赖倒置原则:要依赖于抽象,不要依赖于具体。
  • 迪米特法则(最少知道原则):一个实体应当尽量少地与其他实体之间发生相互作用,使得系统功能模块相对独立。
  • 合成复用原则:尽量使用合成/聚合的方式,而不是使用继承。

for 循环使用迭代器删除元素

解决慢SQL问题

解决慢SQL问题通常涉及以下步骤:

  • 使用SQL执行计划分析查询。
  • 优化索引,确保查询能够高效利用索引。
  • 避免在查询中使用SELECT *,只选择需要的字段。
  • 减少JOIN操作或优化JOIN条件。
  • 避免在WHERE子句中使用非SARGable函数。
  • 考虑对数据库进行分区或分片。
  • 监控数据库性能,定期审查和调整SQL语句。

分布式锁的实现方式

分布式锁的实现方式有多种,包括但不限于:

  • 基于数据库实现,如使用数据库的排他锁。
  • 基于Redis实现,利用Redis的setnx命令或RedLock算法。
  • 基于Zookeeper实现,利用Zookeeper的临时顺序节点和watch机制。

线程池的核心参数通常包括

  • corePoolSize:核心线程数,即使线程处于空闲状态,也不会被销毁,除非设置了allowCoreThreadTimeOut。
  • maximumPoolSize:线程池允许的最大线程数。
  • keepAliveTime:线程空闲时间,当线程数大于核心线程数时,此为终止前多余的空闲线程等待新任务的最长时间。
  • workQueue:用于存放待执行的任务的阻塞队列。

拒绝策略有四种:

  • AbortPolicy:直接抛出RejectedExecutionException异常。
  • CallerRunsPolicy:用调用者所在的线程来执行任务。
  • DiscardOldestPolicy:丢弃阻塞队列中等待最久的任务,然后重新尝试执行任务。
  • DiscardPolicy:直接丢弃任务,不处理。

JVM如何判断对象存活

JVM通过垃圾回收器来判断对象是否存活。主要使用两种算法:引用计数法和可达性分析。现代JVM主要使用可达性分析算法。

可达性分析的基本思路是:从一系列称为“GC Roots”的对象开始向下搜索,搜索走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。

在Java中,可作为GC Roots的对象包括:

  • 虚拟机栈中引用的对象,如方法中的局部变量。
  • 方法区中类静态属性引用的对象。
  • JNI(Java Native Interface)中引用的对象。
  • 本地方法栈中JNI引用的对象。

当进行垃圾回收时,JVM会遍历这些GC Roots,然后递归地搜索它们所引用的对象,标记为存活。未被标记的对象则被认为是不可达的,即垃圾对象,可以被回收。

ConcurrentHashMap的实现原理及扩容机制

实现原理:

  • ConcurrentHashMap(简称CHM)是Java并发包java.util.concurrent下提供的一个线程安全的HashMap实现。
  • CHM内部将数据分为多个段(Segment),每个段其实就是一个小的HashMap,每个段都有自己的锁。这样,多线程并发访问时,不同段的数据可以并行处理,从而提高并发性能。
  • 每个段内部使用链表+红黑树的组合来存储键值对,当链表长度超过一定阈值(TREEIFY_THRESHOLD,默认为8)时,会转化为红黑树来优化查询性能;当树的大小小于UNTREEIFY_THRESHOLD(默认为6)时,会退化为链表。

扩容机制:

  • 当CHM中的元素数量超过当前容量的某个阈值时(通常是容量的0.75倍),会触发扩容。
  • 扩容时,会创建一个新的数组,其容量是原数组的两倍。
  • 然后遍历原数组中的每个段,重新计算每个键值对的索引位置,并放入新数组中。
  • 为了保证线程安全,这个过程会采用分段锁的方式,确保同一时间只有一个线程在扩容某个段。

GC算法:

  • 标记-清除(Mark-Sweep): 分为两个阶段,标记阶段从根对象开始递归访问所有可达对象并标记它们,清除阶段则回收未被标记的对象。优点是简单,但缺点是会产生内存碎片。
  • 复制(Copying): 将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。优点是简单且没有内存碎片,但缺点是内存利用率低。
  • 标记-整理(Mark-Compact): 标记阶段和标记-清除算法相同,但在清除阶段会将所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。优点是解决了内存碎片问题,但缺点是效率相对较低。
  • 分代收集(Generational Collection): 根据对象存活周期的不同将内存划分为几块。一般是把Java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用“标记-清除”或者“标记-整理”算法来进行回收。

优缺点:

  • 标记-清除: 优点是实现简单,缺点是会产生内存碎片,需要定期进行内存碎片整理。
  • 复制: 优点是避免了内存碎片问题,缺点是内存利用率低,只适合对象存活率较低的场景。
  • 标记-整理: 优点是解决了内存碎片问题,且内存利用率高,但缺点是算法效率相对较低。
  • 分代收集: 优点是结合了多种算法的优点,根据对象的存活周期选择合适的算法,提高了垃圾收集的效率。缺点是实现相对复杂。

对象的生命周期:

  • 创建阶段: 使用new关键字或者反射API等方式创建对象实例。
  • 使用阶段: 对象被程序引用并使用,执行相应的操作。
  • 不可达阶段: 当对象没有任何引用指向它时,该对象就变得不可达了。此时,它将被垃圾收集器标记为可回收。
  • 回收阶段: 在垃圾收集器运行期间,不可达的对象会被清理掉,释放其占用的内存空间。

项目中的数据权限怎么实现

数据权限的实现通常涉及到以下几个方面:

  • 基于角色的权限控制: 为不同的角色分配不同的数据访问权限。例如,管理员可以访问所有数据,而普通用户只能访问自己的数据。
  • 基于数据字段的权限控制: 对于某些敏感字段,可以设定只有特定角色或用户才能查看或修改。
  • 数据行级权限控制: 根据某些条件(如用户ID、部门ID等)来控制用户能够访问的数据行。
  • 权限校验: 在用户访问数据时,系统需要进行权限校验,确保用户只能访问其被授权的数据。这通常涉及到在业务逻辑中加入权限判断的代码,或者通过数据库层面的视图、存储过程等来实现。
  • 日志记录: 记录用户对数据的访问和操作行为,以便在发生问题时进行审计和追踪。

Java中的锁机制是用于处理多线程并发情况下数据一致性的重要工具。在Java中,有多个层面的锁机制,包括synchronized关键字和Lock接口等。

  1. synchronized关键字
    • 这是Java语言内置的一种锁机制。
    • 它可以用来实现对代码块或方法的同步控制,确保同一时刻只有一个线程可以执行被锁定的代码块或方法。
    • 当一个线程获取锁时,它会将对象头中的标志位设置为锁定状态,其他线程在尝试获取锁时,如果发现标志位已被设置为锁定状态,就会进入等待状态,直到锁被释放。
  2. Lock接口
    • 提供了比synchronized更灵活的锁机制。
    • 它提供了显式的锁获取和释放操作,允许更细粒度的控制。
    • Lock接口有多种实现,包括ReentrantLock等。

在Java的锁机制中,还可以根据锁的特性进行进一步分类:

  1. 公平锁与非公平锁
    • 公平锁:按照线程申请锁的顺序来获取锁,类似于日常排队。
    • 非公平锁:线程获取锁的顺序并不是按照申请锁的顺序,可能存在插队现象。
  2. 可重入锁(递归锁)
    • 允许同一线程在外层方法获取锁后,进入内层方法时仍能持有该锁并继续运行。
  3. 自旋锁
    • 当线程尝试获取锁失败时,不是立即阻塞等待,而是采用循环的方式尝试获取锁。
    • 这可以减少线程上下文切换的消耗,但当循环次数过多时,会消耗CPU资源。
  4. 读写锁
    • 分为写锁和读锁。写锁是独占锁,一次只能被一个线程持有;读锁是共享锁,可被多个线程持有。
    • 读写锁适用于读操作远多于写操作的场景,可以大大提高读操作的性能。

Java的锁机制为多线程编程提供了丰富的工具,开发者可以根据具体的业务需求选择适合的锁类型,以确保数据的一致性和线程的安全性。

好的,针对您提出的问题,我将逐一进行回答:

1、介绍一下 Redis

Redis 是一个开源的使用 ANSI C 语言编写的、支持网络、可基于内存亦可持久化的日志型、Key-Value 数据库,并提供多种语言的 API。它通常被称为数据结构服务器,因为值(value)可以是 字符串(string)、哈希(Hash)、列表(list)、集合(sets)、有序集合(sorted sets) 等类型。Redis 支持数据的持久化,可以将内存中的数据保存在磁盘中,重启后可以再次加载进行使用。

2、介绍一下 Redis 的数据结构及其底层实现原理

Redis 支持多种数据结构,每种数据结构都有其特定的底层实现原理:

  • 字符串(String):字符串是最简单的数据类型,其底层实现就是一个简单的动态字符串。当对这个字符串进行修改的时候,如果超过了当前分配的空间,会进行2倍的空间扩展。
  • 哈希(Hash):哈希类型实际上是 field 和 value 的映射表,类似于 Java 中的 HashMap。Redis 的哈希类型底层实现为压缩列表或哈希表两种数据结构。当哈希类型元素较少时,使用压缩列表;当元素较多时,则使用哈希表。
  • 列表(List):列表类型用来存储多个有序的字符串元素,列表中的元素可以重复。列表类型有两个特点:可以添加重复的元素和保留元素插入的顺序。其底层实现为双向链表或压缩列表。
  • 集合(Set):集合类型用来存储多个无序的字符串元素,且集合中的元素不能重复。其底层实现为整数集合或哈希表。
  • 有序集合(Sorted Set):有序集合与集合一样不允许有重复的元素,但每个元素都会关联一个 double 类型的分数,Redis 正是通过分数来为集合中的元素从小到大进行从小到大的排序。其底层实现为压缩列表或跳跃表和哈希表的组合。

3、介绍一下你对 Redis 线程模型的了解

Redis 是单线程模型,这里的单线程主要指的是 Redis 的网络 I/O 和键值对读写是由一个线程来完成的。虽然 Redis 的其他功能,如持久化、异步删除、集群数据同步等,是由额外的线程来处理的,但这些并不会影响 Redis 主线程处理网络 I/O 和键值对读写的工作。Redis 采用单线程模型主要是为了避免多线程带来的锁竞争和上下文切换的开销,从而确保 Redis 的高性能。

4、介绍一下 Redis 集群,以及 Redis 是如何实现高可用的

Redis 集群是一个提供在多个 Redis 节点间进行数据共享的程序集。Redis 集群不支持那些需要同时操作多个键的 Redis 命令,因为这需要在不同的节点间移动数据,从而无法达到像单个 Redis 实例那样的性能,在设计的时候这就是不被支持的。

Redis 实现高可用主要通过以下几种方式:

  • 主从复制:Redis 支持主从复制功能,即一个主节点可以有多个从节点。当主节点出现故障时,从节点可以接管主节点的任务,继续提供服务,从而实现高可用。
  • 哨兵(Sentinel):哨兵是 Redis 的高可用性解决方案:由一个或多个 Sentinel 节点组成的 Sentinel 系统可以监视任意数量的主服务器,以及这些主服务器属下的所有从服务器,并在被监视的主服务器进入下线状态时,自动将下线主服务器属下的某个从服务器升级为新的主服务器。
  • 集群(Cluster):Redis 集群实现了数据的分片存储,每个节点只存储部分数据。当某个节点出现故障时,其他节点仍然可以正常工作,并且可以通过重新分配数据来恢复数据的完整性。此外,Redis 集群还支持在线扩容,可以方便地添加新的节点来提高系统的处理能力和存储容量。

5、说一下 Redis 中的 hot key 应该如何处理

hot key 是指那些被频繁访问的 Redis 键。处理 hot key 可以从以下几个方面入手:

  • 缓存穿透:当查询一个不存在的数据时,由于缓存中也没有,因此每次请求都会直接打到数据库上,造成缓存穿透。对于这类情况,我们可以将空对象或默认值进行缓存,或者对查询条件进行校验,避免无效的查询。
  • 缓存雪崩:当缓存中大量的 key 在同一时间失效或者 Redis 服务宕机时,所有的请求都会直接打到数据库上,造成缓存雪崩。为了避免这种情况,我们可以设置不同的 key 的过期时间,或者使用 Redis 的持久化功能来确保数据的可靠性。
  • 热点 key 的访问:对于热点 key,我们可以考虑使用分布式锁来限制并发访问,或者使用本地缓存来减少对 Redis 的访问次数。此外,还可以考虑对热点 key 进行拆分或者增加其副本数量来分散访问压力。

6、你用过 Redis 吗?说一下你在哪些场景下用的

是的,我在实际项目中经常使用 Redis。以下是一些我使用 Redis 的场景:

  • 缓存:这是 Redis 最常见的使用场景。我将一些热点数据或者计算结果存储在 Redis 中,作为缓存使用,以减少对数据库的访问压力,提高系统的响应速度。

  • 计数器:Redis 的原子操作特性使得它非常适合用于实现计数器功能。例如,我可以使用 Redis 来记录网站的访问量、用户的点赞数等。

  • 排行榜:Redis 的有序集合(Sorted Set)数据结构使得实现排行榜功能变得非常简单。我可以根据分数将元素进行排序,从而轻松获取排行榜信息。

  • 消息队列:Redis 的列表(List)数据类型可以被用作简单的消息队列。生产者可以将消息推送到队列中,消费者可以从队列中拉取消息进行处理。

  • 分布式锁:Redis 的 setnx 命令可以实现分布式锁的功能。在多个进程或线程需要同时访问共享资源时,我可以使用 Redis 的分布式锁来确保资源的安全访问。

  • 会话管理:在 Web 应用中,我可以将用户的会话信息存储在 Redis 中,实现跨服务器的会话共享。

  • 社交功能:Redis 也常被用于实现一些社交功能,如用户的关注列表、粉丝列表等。由于这些列表通常需要频繁地进行添加、删除和查询操作,Redis 的高性能特性使得它成为实现这些功能的理想选择。

    以上只是我使用 Redis 的一些常见场景,实际上 Redis 的应用场景非常广泛,几乎涵盖了所有需要高性能数据存储和访问的场景。

如何保证 Redis 缓存一致性?

选择合适的缓存读写策略:

旁路缓存策略,可分为读策略和写策略。读穿 / 写穿策略,写回策略。

读策略的步骤是:

  • 从缓存中读取数据;
  • 如果缓存命中,则直接返回数据;
  • 如果缓存不命中,则从数据库中查询数据;
  • 查询到数据后,将数据写入到缓存中,并且返回给用户。

写策略的步骤是:

  • 更新数据库中的记录;
  • 删除缓存记录。

先删除缓存,后更新数据库呢?更新数据库效率相对来讲比较低,在两者之间如果有读请求(读缓存,缓存未命中,读数据库,回写缓存),缓存被回写了,此时更新后的数据库就与缓存不一致了。

img

使用线程池的好处

使用线程池比手动创建线程主要有三点好处。

  1. 第一点,线程池可以解决线程生命周期的系统开销问题,同时还可以加快响应速度。因为线程池中的线程是可以复用的,我们只用少量的线程去执行大量的任务,这就大大减小了线程生命周期的开销。而且线程通常不是等接到任务后再临时创建,而是已经创建好时刻准备执行任务,这样就消除了线程创建所带来的延迟,提升了响应速度,增强了用户体验。
  2. 第二点,线程池可以统筹内存和 CPU 的使用,避免资源使用不当。线程池会根据配置和任务数量灵活地控制线程数量,不够的时候就创建,太多的时候就回收,避免线程过多导致内存溢出,或线程太少导致 CPU 资源浪费,达到了一个完美的平衡。
  3. 第三点,线程池可以统一管理资源。比如线程池可以统一管理任务队列和线程,可以统一开始或结束任务,比单个线程逐一处理任务要更方便、更易于管理,同时也有利于数据统计,比如我们可以很方便地统计出已经执行过的任务的数量。

拒绝策略

  • 第一种拒绝策略是 AbortPolicy,这种拒绝策略在拒绝任务时,会直接抛出一个类型为 RejectedExecutionException 的 RuntimeException,让你感知到任务被拒绝了,于是你便可以根据业务逻辑选择重试或者放弃提交等策略。
  • 第二种拒绝策略是 DiscardPolicy,这种拒绝策略正如它的名字所描述的一样,当新任务被提交后直接被丢弃掉,也不会给你任何的通知,相对而言存在一定的风险,因为我们提交的时候根本不知道这个任务会被丢弃,可能造成数据丢失。
  • 第三种拒绝策略是 DiscardOldestPolicy,如果线程池没被关闭且没有能力执行,则会丢弃任务队列中的头结点,通常是存活时间最长的任务,这种策略与第二种不同之处在于它丢弃的不是最新提交的,而是队列中存活时间最长的,这样就可以腾出空间给新提交的任务,但同理它也存在一定的数据丢失风险。
  • 第四种拒绝策略是 CallerRunsPolicy,相对而言它就比较完善了,当有新任务提交后,如果线程池没被关闭且没有能力执行,则把这个任务交于提交任务的线程执行,也就是谁提交任务,谁就负责执行任务。这样做主要有两点好处。
    • 第一点新提交的任务不会被丢弃,这样也就不会造成业务损失。
    • 第二点好处是,由于谁提交任务谁就要负责执行任务,这样提交任务的线程就得负责执行任务,而执行任务又是比较耗时的,在这段期间,提交任务的线程被占用,也就不会再提交新的任务,减缓了任务提交的速度,相当于是一个负反馈。在此期间,线程池中的线程也可以充分利用这段时间来执行掉一部分任务,腾出一定的空间,相当于是给了线程池一定的缓冲期。

线程池的内部结构主要由四部分组成

  • 第一部分是线程池管理器,它主要负责管理线程池的创建、销毁、添加任务等管理操作,它是整个线程池的管家。
  • 第二部分是工作线程,也就是图中的线程 t0~t9,这些线程勤勤恳恳地从任务队列中获取任务并执行。
  • 第三部分是任务队列,作为一种缓冲机制,线程池会把当下没有处理的任务放入任务队列中,由于多线程同时从任务队列中获取任务是并发场景,此时就需要任务队列满足线程安全的要求,所以线程池中任务队列采用 BlockingQueue 来保障线程安全。
  • 第四部分是任务,任务要求实现统一的接口,以便工作线程可以处理和执行。

你可以看到,这几种自动创建的线程池都存在风险,相比较而言,我们自己手动创建会更好,因为我们可以更加明确线程池的运行规则,不仅可以选择适合自己的线程数量,更可以在必要的时候拒绝新任务的提交,避免资源耗尽的风险。

选择合适的线程数量

CPU 密集型任务

首先,我们来看 CPU 密集型任务,比如加密、解密、压缩、计算等一系列需要大量耗费 CPU 资源的任务。对于这样的任务最佳的线程数为 CPU 核心数的 1~2 倍,如果设置过多的线程数,实际上并不会起到很好的效果。此时假设我们设置的线程数量是 CPU 核心数的 2 倍以上,因为计算任务非常重,会占用大量的 CPU 资源,所以这时 CPU 的每个核心工作基本都是满负荷的,而我们又设置了过多的线程,每个线程都想去利用 CPU 资源来执行自己的任务,这就会造成不必要的上下文切换,此时线程数的增多并没有让性能提升,反而由于线程数量过多会导致性能下降。

针对这种情况,我们最好还要同时考虑在同一台机器上还有哪些其他会占用过多 CPU 资源的程序在运行,然后对资源使用做整体的平衡。

耗时 IO 型任务

第二种任务是耗时 IO 型,比如数据库、文件的读写,网络通信等任务,这种任务的特点是并不会特别消耗 CPU 资源,但是 IO 操作很耗时,总体会占用比较多的时间。对于这种任务最大线程数一般会大于 CPU 核心数很多倍,因为 IO 读写速度相比于 CPU 的速度而言是比较慢的,如果我们设置过少的线程数,就可能导致 CPU 资源的浪费。而如果我们设置更多的线程数,那么当一部分线程正在等待 IO 的时候,它们此时并不需要 CPU 来计算,那么另外的线程便可以利用 CPU 去执行其他的任务,互不影响,这样的话在任务队列中等待的任务就会减少,可以更好地利用资源。

  • 线程的平均工作时间所占比例越高,就需要越少的线程;
  • 线程的平均等待时间所占比例越高,就需要越多的线程;
  • 针对不同的程序,进行对应的实际测试就可以得到最合适的选择。

线程池怎么实现线程复用的?

Syncronized 锁升级过程:

第一种分类是偏向锁/轻量级锁/重量级锁,这三种锁特指 synchronized 锁的状态,通过在对象头中的 mark word 来表明锁的状态。

  • 偏向锁

如果自始至终,对于这把锁都不存在竞争,那么其实就没必要上锁,只需要打个标记就行了,这就是偏向锁的思想。一个对象被初始化后,还没有任何线程来获取它的锁时,那么它就是可偏向的,当有第一个线程来访问它并尝试获取锁的时候,它就将这个线程记录下来,以后如果尝试获取锁的线程正是偏向锁的拥有者,就可以直接获得锁,开销很小,性能最好。

  • 轻量级锁

JVM 开发者发现在很多情况下,synchronized 中的代码是被多个线程交替执行的,而不是同时执行的,也就是说并不存在实际的竞争,或者是只有短时间的锁竞争,用 CAS 就可以解决,这种情况下,用完全互斥的重量级锁是没必要的。轻量级锁是指当锁原来是偏向锁的时候,被另一个线程访问,说明存在竞争,那么偏向锁就会升级为轻量级锁,线程会通过自旋的形式尝试获取锁,而不会陷入阻塞。

  • 重量级锁

重量级锁是互斥锁,它是利用操作系统的同步机制实现的,所以开销相对比较大。当多个线程直接有实际竞争,且锁竞争时间长的时候,轻量级锁不能满足需求,锁就会膨胀为重量级锁。重量级锁会让其他申请却拿不到锁的线程进入阻塞状态。

img

你可以发现锁升级的路径:无锁→偏向锁→轻量级锁→重量级锁。

综上所述,偏向锁性能最好,可以避免执行 CAS 操作。而轻量级锁利用自旋和 CAS 避免了重量级锁带来的线程阻塞和唤醒,性能中等。重量级锁则会把获取不到锁的线程阻塞,性能最差。

今日待总结:线程池的复用原理,ES 实现原理,定时任务原理,RPC 原理,HashMap 原理,计算机网络,操作系统,算法,新项目

面经总结

  1. 自我介绍及项目中的技术问题和优化

自我介绍
我是一名具有多年经验的Java开发工程师,专注于后端开发,对Java生态系统中的技术和框架有深入的了解。

项目中的技术问题和优化
在最近的项目中,我们遇到了系统在高并发场景下性能下降的问题。经过分析,发现是由于数据库访问过于频繁导致的。为了解决这个问题,我引入了缓存机制,使用Redis缓存热点数据,减少了数据库访问次数,从而提升了系统性能。

此外,我还对系统中的某些算法进行了优化,比如使用了更高效的数据结构和算法,减少了计算复杂度,提高了处理速度。

  1. ArrayList和LinkedList的介绍及优雅创建ArrayList

ArrayList
ArrayList是基于动态数组实现的List接口,支持随机访问元素,但在插入和删除元素时可能需要移动其他元素,因此效率较低。

LinkedList
LinkedList是基于链表实现的List接口,插入和删除元素时效率较高,但访问元素时需要从头或尾开始遍历,因此随机访问效率较低。

优雅创建ArrayList
在创建ArrayList时,如果预先知道要存储的元素数量,可以通过构造函数指定初始容量,以避免多次扩容带来的性能开销。例如:ArrayList<String> list = new ArrayList<>(10);

  1. HashMap底层为何进化成红黑树及红黑树关键点

进化成红黑树的原因
当HashMap中的某个桶(bucket)的链表长度过长时,查找效率会降低。为了解决这个问题,HashMap在JDK 1.8中引入了红黑树来优化性能。当红黑树的节点数少于一定数量时,会退化为链表,以保持简单性。

红黑树关键点
红黑树是一种自平衡的二叉搜索树,它满足以下五个性质:

  • 每个节点要么是红色,要么是黑色。
  • 根节点是黑色。
  • 所有叶子节点(NIL或空节点)是黑色。
  • 如果一个节点是红色的,则它的两个子节点都是黑色的。
  • 对于每个节点,从该节点到其所有后代叶子节点的简单路径上,均包含相同数目的黑色节点。
  1. HashMap解决哈希冲突的方式

HashMap解决哈希冲突的方式主要有两种:

  • 拉链法:将哈希值相同的元素存储在同一个桶的链表中。
  • 红黑树法(在JDK 1.8及以后):当某个桶的链表长度过长时,会转换为红黑树来存储元素,以提高查找效率。
  1. Java中哪里用到了开放定址法

在Java中,ThreadLocal并没有直接使用开放定址法来解决哈希冲突。ThreadLocal内部使用了一个简单的哈希表来存储线程局部变量,但具体的冲突解决策略并不是开放定址法。通常,ThreadLocal的哈希表大小是固定的,并且每个线程都有一个独立的ThreadLocalMap实例,因此哈希冲突的情况相对较少。

  1. ThreadLocal底层实现原理及并发解决方式

ThreadLocal底层实现原理
ThreadLocal为每个线程提供其自己的变量副本。每个线程在第一次访问某个ThreadLocal变量时,ThreadLocal通过调用其setInitialValue()方法为该线程创建变量副本,并在ThreadLocalMap中以线程为键保存。后续对该变量的访问或修改都是基于线程自己的副本进行的,因此不会影响其他线程。

并发解决方式
ThreadLocal通过为每个线程提供独立的变量副本来解决并发问题。由于每个线程都有自己的变量副本,因此不存在多线程间的数据竞争和同步问题。

  1. ThreadLocal使用产生的问题、原因及解决方案

产生的问题

  • 内存泄漏:由于ThreadLocalMap的生命周期与线程的生命周期相同,如果线程长时间运行而不结束,那么ThreadLocalMap中存储的键值对(包括ThreadLocal的弱引用和变量的强引用)也无法被垃圾回收,从而导致内存泄漏。

原因

  • ThreadLocalMap使用ThreadLocal的弱引用作为键,而值是强引用。当ThreadLocal不再被引用时,由于它是弱引用,可以被垃圾回收。但是,如果ThreadLocalMap不被清理,那么它仍然持有值的强引用,导致内存泄漏。

解决方案

  • 在使用完ThreadLocal后,显式调用其remove()方法,从当前线程的ThreadLocalMap中移除对应的条目。
  • 设计合理的线程池管理策略,避免线程长时间运行。
  1. 手动实现Redis分布式锁

实现Redis分布式锁通常涉及以下步骤:

  1. 使用Redis的SETNX命令尝试设置一个锁键,并设置过期时间。
  2. 如果设置成功,则获取到锁,执行临界区代码。
  3. 执行完临界区代码后,删除锁键。
  4. 如果设置失败(即锁已被其他客户端持有),则等待或重试。

在实现过程中,需要注意以下几点:

  • 锁的粒度:锁的粒度应该尽可能小,以减少锁竞争。
  • 锁的过期时间:设置合理的过期时间,避免锁因客户端崩溃或网络问题而长时间无法释放。
  • 锁的续期:如果临界区代码执行时间较长,可能需要考虑锁的续期,以避免过期后被其他客户端误抢。
  • 避免死锁:确保在异常情况下能够释放锁,避免死锁。
  1. 实现Redis分布式锁时加锁和释放锁需注意的事项

加锁时需注意

  • 设置锁的键值时,应使用唯一标识(如UUID)作为锁的值,以便于识别锁的持有者。
  • 设置锁的过期时间,确保锁不会因客户端异常而长时间持有。
  • 考虑使用Redis的事务或Lua脚本来保证加锁操作的原子性。

释放锁时需注意

  • 在删除锁键之前,应验证当前客户端是否确实持有该锁(即锁的值是否与客户端设置的唯一标识匹配)。
  • 仅当客户端确实持有锁时,才应删除锁键,以避免误删其他客户端的锁。
  • 考虑使用Redis的事务或Lua脚本来保证释放锁操作的原子性。
  1. Redis淘汰策略中的LRU和LFU,问题及LFU后续版本的改进

LRU(Least Recently Used)
LRU算法淘汰最久未使用的数据。但问题在于,它无法很好地处理“冷数据”突然变为“热数据”的情况。即使这些数据最近被访问过,但由于它们之前长时间未被访问,仍可能被错误地淘汰。

LFU(Least Frequently Used)
LFU算法根据数据的访问频率来淘汰数据。但它也有问题:一旦某个数据变为热数据,即使之后访问频率降低,它也可能因为之前的高访问频率而长时间留在缓存中。

LFU后续版本的改进
为了解决LFU的上述问题,后续版本引入了降频机制。降频机制的基本思想是:对于长时间未被访问但访问频率仍然很高的数据,逐渐降低其访问频率计数,使其更容易被淘汰。这样既能保证热数据在缓存中的留存,又能避免冷数据被错误地保留。具体实现时,可以通过为每个数据的访问频率计数设置一个衰减因子或时间窗口来实现降频。

  1. @SpringApplication包含的注解及启动流程

@SpringApplication是Spring Boot的核心注解,它包含了@SpringBootConfiguration@EnableAutoConfiguration@ComponentScan等注解。

  • @SpringBootConfiguration:声明当前类是配置类,允许使用@Bean注解定义bean。
  • @EnableAutoConfiguration:根据项目的依赖关系,自动配置Spring Boot项目。
  • @ComponentScan:让Spring扫描到Configuration类并把它加入到程序上下文。

启动流程大致如下:

  1. 创建SpringApplication对象,进行初始化设置。
  2. 运行SpringApplication的run方法,执行Spring应用的启动流程。
  3. 创建Spring容器,加载并注册配置类。
  4. 自动配置,根据项目的依赖关系,自动加载和配置相关的bean。
  5. 扫描并加载其他组件。
  6. 运行已注册的CommandLineRunner。
  7. Arrays.asList()的注意事项

Arrays.asList()方法返回的是一个固定大小的列表,它不支持增加或删除元素。如果尝试修改列表的大小,会抛出UnsupportedOperationException。此外,返回的列表在结构上与原始数组是紧密关联的,修改原数组的内容也会影响到列表。

  1. 用for循环时如何删除元素

在for循环中直接删除元素可能会导致意外的行为或错误,因为删除元素会改变集合的大小,从而影响到循环的迭代。建议使用迭代器(Iterator)来删除元素,或者在删除元素时重新构建一个新的集合。

  1. synchronized锁升级过程及锁状态

synchronized锁在Java中主要有四种状态:无锁状态、偏向锁、轻量级锁和重量级锁。锁升级的过程大致是:当线程访问同步块并获取锁时,首先尝试偏向锁;如果失败,则升级为轻量级锁;若轻量级锁竞争失败,则升级为重量级锁。

  1. 观察者模式及设计模式的七大原则

观察者模式是一种行为设计模式,它定义了一种一对多的依赖关系,让多个观察者对象同时监听某一个主题对象。当主题对象状态发生变化时,它的所有依赖者(观察者)都会收到通知并自动更新。

设计模式的七大原则包括:

  • 单一职责原则:一个类应该只有一个引起变化的原因。
  • 开放封闭原则:软件实体(类、模块、函数等)应该是可扩展的,但是不可修改。
  • 里氏替换原则:子类必须能够替换其基类。
  • 接口隔离原则:使用多个专门的接口,而不使用单一的总接口,客户端不应该依赖它不需要的接口。
  • 依赖倒置原则:要依赖于抽象,不要依赖于具体。
  • 迪米特法则(最少知道原则):一个实体应当尽量少地与其他实体之间发生相互作用,使得系统功能模块相对独立。
  • 合成复用原则:尽量使用合成/聚合的方式,而不是使用继承。
  1. 解决慢SQL问题

解决慢SQL问题通常涉及以下步骤:

  • 使用SQL执行计划分析查询。
  • 优化索引,确保查询能够高效利用索引。
  • 避免在查询中使用SELECT *,只选择需要的字段。
  • 减少JOIN操作或优化JOIN条件。
  • 避免在WHERE子句中使用非SARGable函数。
  • 考虑对数据库进行分区或分片。
  • 监控数据库性能,定期审查和调整SQL语句。
  1. 分布式限流及令牌桶算法

分布式限流是为了防止系统因流量过载而崩溃的一种措施。令牌桶算法是其中一种常见的限流算法。

令牌桶算法中,有一个固定容量的令牌桶,以一定的速率往桶里添加令牌。当请求到达时,尝试从桶中取出一个令牌,如果取到令牌则允许请求通过,否则拒绝请求或进行限流处理。这种方式能够平滑地处理突发流量。

  1. 分布式锁的实现方式

分布式锁的实现方式有多种,包括但不限于:

  • 基于数据库实现,如使用数据库的排他锁。
  • 基于Redis实现,利用Redis的setnx命令或RedLock算法。
  • 基于Zookeeper实现,利用Zookeeper的临时顺序节点和watch机制。
  1. 线程池的核心参数及拒绝策略

线程池的核心参数通常包括:

  • corePoolSize:核心线程数,即使线程处于空闲状态,也不会被销毁,除非设置了allowCoreThreadTimeOut。
  • maximumPoolSize:线程池允许的最大线程数。
  • keepAliveTime:线程空闲时间,当线程数大于核心线程数时,此为终止前多余的空闲线程等待新任务的最长时间。
  • workQueue:用于存放待执行的任务的阻塞队列。

拒绝策略有四种:

  • AbortPolicy:直接抛出RejectedExecutionException异常。
    • CallerRunsPolicy:用调用者所在的线程来执行任务。
    • DiscardOldestPolicy:丢弃阻塞队列中等待最久的任务,然后重新尝试执行任务。
    • DiscardPolicy:直接丢弃任务,不处理。

JVM如何判断对象存活

JVM通过垃圾回收器来判断对象是否存活。主要使用两种算法:引用计数法和可达性分析。现代JVM主要使用可达性分析算法。

可达性分析的基本思路是:从一系列称为“GC Roots”的对象开始向下搜索,搜索走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。

在Java中,可作为GC Roots的对象包括:

  • 虚拟机栈中引用的对象,如方法中的局部变量。
  • 方法区中类静态属性引用的对象。
  • JNI(Java Native Interface)中引用的对象。
  • 本地方法栈中JNI引用的对象。

当进行垃圾回收时,JVM会遍历这些GC Roots,然后递归地搜索它们所引用的对象,标记为存活。未被标记的对象则被认为是不可达的,即垃圾对象,可以被回收。

这些面试题涵盖了从基础概念到高级技术的多个方面,需要一定的专业知识才能准确回答。希望以上的回答能够帮助到你,并为你的面试提供有益的参考。

当 Java8 发布时,引入了一系列重大的新特性和改进,其中一些最突出的包括:

1)Lambda 表达式:Lambda 表达式是一种更简洁、更易读的语法,使得函数式编程在Java中变得更加方便。它们可以帮助简化集合操作,使代码更具表现力和可读性。
2)Stream API:Stream AP!提供了一种新的抽象,用于处理集合数据。它允许开发人员以声明性的方式对数据进行过滤、映射、排序等操作,从而简化了并行处理和集合操作。
3)新的日期和时间 API:Java8引入了全新的日期和时间 API,位于 java.time 包中。这个 API解决了1日 API中一些设计缺陷和线程安全问题,提供了更清晰、更易用的方式来处理日期和时间。
4)默认方法:接口中引入了默认方法,允许在接口中提供默认的方法实现。这使得接口可以更灵活地进行演化,而不会破坏现有的实现。
5)方法引用:方法引用是一种简化 Lambda 表达式的语法,使代码更加简洁易读。它提供了一种直接引用现有方法的方式,而不是为了传递给 Lambda 表达式而编写额外的代码。
6)重复注解:Java8允许在同一类型或方法上使用多个相同的注解,这使得代码更加整洁和可读。
7)optional 类:Optional 类是一种用于处理可能为 nu 的值的容器类。它提供了一种优雅的方式来避免空指针异常,并鼓励开发人员更加 defensice 的编程风格。

好的,我会按照您提供的问题进行逐一解答:

  1. MySQL索引与事务

MySQL索引

  • 索引是数据库表中一列或多列的值进行排序的一种数据结构,其作用是加快数据的检索速度。
  • 常见的索引类型有:B-Tree索引、哈希索引、全文索引等。
  • 在创建索引时,需要权衡查询速度与索引维护(如插入、更新、删除操作)的开销。
  • 不恰当的索引可能导致性能下降,因此需要根据实际查询需求来创建和优化索引。

MySQL事务

  • 事务是一系列数据库操作的逻辑单元,这些操作要么全部执行,要么全部不执行。
  • 事务具有ACID四个特性:原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)、持久性(Durability)。
  • MySQL支持事务的存储引擎主要是InnoDB。
  • 在使用事务时,需要注意并发控制,如使用锁来避免脏读、不可重复读和幻读等问题。
  1. Redis底层数据结构,Redis过期策略

Redis底层数据结构

  • Redis主要使用了简单动态字符串(SDS)、双端链表、字典、跳跃表、整数集合等数据结构来实现其高效的数据存储和访问。
  • 这些数据结构在Redis的不同功能中得到了广泛应用,如字符串、列表、集合、有序集合等数据类型。

Redis过期策略

  • Redis为每个键设置了过期时间,当键过期时,Redis会根据过期策略来删除这些键。
  • 过期策略主要有两种:惰性删除和定期删除。
    • 惰性删除:在访问一个键时,检查该键是否过期,如果过期则删除它。
    • 定期删除:Redis每隔一段时间会随机检查一部分键的过期时间,并删除其中的过期键。
  • 此外,Redis还提供了FLUSHDBFLUSHALL命令来主动删除所有键。
  1. MA事务消息如何保证消息不丢失

关于MA事务消息,我假设您是指某种分布式事务中的消息保证机制。在分布式系统中,确保消息不丢失通常涉及以下策略:

  • 消息持久化:确保消息在发送前和发送后都被持久化存储,以防止在故障时丢失。
  • 确认机制:接收方在成功处理消息后发送确认给发送方,发送方在收到确认前会重试发送消息。
  • 事务日志:记录所有发送和接收的消息,以便在出现问题时可以恢复或重试。
  • 重试策略:对于发送失败或未确认的消息,实施合理的重试策略。
  • 监控与告警:对消息发送、接收和确认过程进行监控,并在出现异常时及时告警。
  1. 分布式事务,CAP

分布式事务

  • 在分布式系统中,涉及多个服务或数据库的事务称为分布式事务。
  • 分布式事务需要确保跨多个组件的数据一致性和完整性。
  • 实现分布式事务的方法包括两阶段提交(2PC)、三阶段提交(3PC)、分布式事务框架(如Seata)等。

CAP定理

  • CAP定理指出,一个分布式系统不可能同时满足一致性(Consistency)、可用性(Availability)和分区容错性(Partition tolerance)这三个基本需求。
  • 在设计分布式系统时,需要根据实际需求权衡这三个属性。
  • 大多数分布式系统会选择牺牲一致性(CP)或可用性(AP)来满足分区容错性。
  1. Spring循环依赖

Spring中的循环依赖主要发生在两个或多个bean相互依赖对方,形成一个闭环。Spring容器在初始化这些bean时,会遇到无法解析依赖的问题。

Spring主要使用三级缓存来解决单例模式下的循环依赖问题:

  • 一级缓存:singletonObjects,用于存放已经完全初始化好的bean实例。
  • 二级缓存:earlySingletonObjects,用于存放提前暴露出来的、尚未完成属性填充的bean实例(但已经完成了实例化)。
  • 三级缓存:singletonFactories,用于存放bean的ObjectFactory对象,通过这个ObjectFactory可以获取到bean的早期引用(即尚未完成属性填充的对象)。

当Spring检测到循环依赖时,它会利用这些缓存来提前暴露尚未完全初始化的bean,从而解决循环依赖问题。但需要注意的是,循环依赖并不是一个好的设计实践,应该尽量避免。如果确实存在循环依赖,应该重新审视设计,看是否有更好的解决方案。

针对您提出的这些问题,我将逐一进行回答:

  1. 项目中的难点及解决方案
    • 难点:可能包括性能瓶颈、复杂的业务逻辑、系统稳定性等。
    • 解决方案:性能瓶颈可以通过优化算法、使用缓存、分布式部署等方式解决;复杂的业务逻辑可以通过拆分模块、引入中间件、使用设计模式等方式简化;系统稳定性可以通过监控告警、熔断降级、冗余备份等手段提高。
  2. 数据迁移和流量切换
    • 数据迁移:可以使用数据同步工具或编写脚本进行数据迁移,确保数据的一致性和完整性。迁移前应进行充分的测试,并在低峰时段进行迁移操作。
    • 流量切换:可以通过灰度发布、蓝绿部署或A/B测试等方式逐步切换流量,确保新系统或新功能上线后的稳定性和可靠性。
  3. 在项目中负责的内容
    这可能包括但不限于需求分析、系统设计、编码实现、测试部署、系统维护等。具体负责的内容取决于项目的规模、团队分工以及个人的能力和职责。
  4. 配置中心的设计和大文件传递
    • 配置中心设计:可以基于分布式系统实现,使用数据库或分布式缓存存储配置信息,提供API供其他服务获取配置。同时,应支持配置版本控制、动态刷新等功能。
    • 大文件传递:可以使用文件传输协议(如FTP、SFTP)或分布式文件系统(如HDFS)进行大文件传递。对于特别大的文件,可以考虑分块传输或压缩传输。
  5. 系统可靠性和多主节点数据一致性
    • 系统可靠性:可以通过冗余备份、负载均衡、容错处理等方式提高系统可靠性。
    • 多主节点数据一致性:可以使用分布式一致性协议(如Raft、Paxos)来确保多主节点之间的数据一致性。这些协议通过选举主节点、日志复制、安全性校验等手段确保数据的最终一致性。
  6. MQ如何保证数据不丢失
    • 确保MQ的持久化配置正确,消息被写入磁盘。
    • 使用确认机制(如ACK),确保消息被消费者成功处理后再删除。
    • 监控MQ的运行状态,及时处理异常情况。
  7. ES查询快的原因
    Elasticsearch查询快主要得益于其倒排索引、分布式架构、优化查询算法等特性。倒排索引可以快速定位到包含特定关键词的文档;分布式架构可以并行处理查询请求;优化查询算法可以减少不必要的计算和数据传输。
  8. Redis的数据结构和ZSet
    Redis支持多种数据结构,包括字符串(string)、哈希(hash)、列表(list)、集合(set)和有序集合(sorted set,即ZSet)。ZSet是有序集合,每个元素都会关联一个double类型的分数,Redis正是通过分数来为集合中的元素进行从小到大的排序。
  9. Redis的高可用实现方式
    Redis的高可用可以通过主从复制、哨兵模式(Sentinel)或集群模式(Cluster)实现。主从复制可以实现数据的备份和读写分离;哨兵模式可以自动监测主节点的故障并进行故障转移;集群模式可以实现数据的分片存储和水平扩展。
  10. Redis的淘汰策略
    当Redis内存使用达到预设的最大值时,Redis会触发淘汰策略,以释放内存空间。常见的淘汰策略包括:根据键值对的过期时间进行淘汰、随机淘汰、根据键值对的访问频率进行淘汰等。
  11. 事务请求第三方接口和保存数据库的一致性
    可以使用分布式事务或两阶段提交(2PC)等机制来确保事务的一致性。对于长事务问题,可以考虑拆分事务、使用补偿机制或引入消息队列等方式进行处理。
  12. Spring的IOC理解和循环依赖的解决
    Spring的IOC(控制反转)是一种设计思想,通过Spring容器来管理对象的生命周期和依赖关系,降低代码之间的耦合度。循环依赖问题在Spring中通常通过构造器注入和setter注入的不同处理方式来解决。构造器注入由于是在对象创建时就确定依赖关系,因此无法解决循环依赖问题;而setter注入则是在对象创建后通过setter方法设置依赖关系,可以通过代理对象等方式解决循环依赖问题。

这些只是一些基本的解答,每个问题实际上都可以深入探讨和扩展。如果您对某个问题有更深入或具体的疑问,欢迎继续提问。

Java异常体系

Java异常体系是Java用来处理程序中可能发生的错误或异常情况的一种机制。异常体系主要分为两大类:ErrorException

  • Error:表示系统级的错误,通常是Java虚拟机无法处理的严重问题,如OutOfMemoryError

  • ```
    Exception

    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
    50
    51
    52
    53

    :表示程序需要捕获或声明的异常,是程序可以处理的。它又分为两类:

    - `RuntimeException`:运行时异常,通常是由程序逻辑错误导致的,如`NullPointerException`、`ArrayIndexOutOfBoundsException`等。这类异常可以不使用`try-catch`语句捕获,但通常建议捕获以便更好地处理或记录。
    - `Checked Exception`:非运行时异常,这类异常在编译时期就必须处理,否则编译不通过。如`IOException`、`SQLException`等。

    synchronized关键字的用法及原理

    `synchronized`是Java中的一个关键字,用于保证多线程环境下的代码块或方法在同一时刻只能被一个线程访问,实现线程同步。

    用法:

    1. 修饰实例方法:对整个对象加锁。
    2. 修饰静态方法:对类对象加锁,即所有实例共享一把锁。
    3. 修饰代码块:指定加锁对象,对给定对象加锁。

    原理:

    `synchronized`关键字在JVM层面通过对象监视器(monitor)实现同步。每个对象都有一个监视器锁,当线程进入`synchronized`方法或代码块时,会尝试获取对象的监视器锁,成功则进入同步代码块,执行完毕后释放锁;失败则阻塞等待,直到获取到锁为止。

    **String对象不可变的原因**

    String对象不可变的原因主要有以下几点:

    1. **安全性**:字符串常量池中的字符串是不可变的,这样可以确保字符串在共享时不会被意外修改,提高了程序的安全性。
    2. **效率**:字符串拼接时,如果String是可变的,每次拼接都需要创建新的对象,效率低下。而不可变的String可以利用字符串常量池进行缓存,提高了效率。
    3. **哈希码**:由于String是不可变的,其哈希码在创建时就已经确定,并且在对象生命周期内保持不变,这对于哈希表等数据结构的实现非常有利。

    **StringBuffer、StringBuilder的区别**

    `StringBuffer`和`StringBuilder`都是可变字符串,它们的主要区别在于线程安全性:

    - `StringBuffer`是线程安全的,其方法都是同步的,因此适用于多线程环境,但性能相对较低。
    - `StringBuilder`是线程不安全的,其方法没有同步,因此适用于单线程环境,性能较高。在单线程环境下,推荐使用`StringBuilder`以提高性能。

    **熟悉的设计模式**

    设计模式是解决在软件设计中经常遇到的一类问题的最佳实践。常见的设计模式包括:单例模式、工厂模式、观察者模式、原型模式、建造者模式、适配器模式、桥接模式、组合模式、装饰器模式、外观模式、享元模式、代理模式、模板方法模式、迭代器模式、状态模式、策略模式、职责链模式、访问者模式等。

    **如何实现单例模式**

    单例模式确保一个类仅有一个实例,并提供一个全局访问点。以下是实现单例模式的几种常见方法:

    1. **饿汉式**:类加载时就完成初始化,线程安全。

    ```java
    public class Singleton {
    private static Singleton instance = new Singleton();
    private Singleton() {}
    public static Singleton getInstance() {
    return instance;
    }
    }
  1. 懒汉式(线程不安全):延迟加载,首次使用时才进行初始化。
1
2
3
4
5
6
7
8
9
10
public class Singleton {  
private static Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
  1. 懒汉式(线程安全,同步方法):使用synchronized关键字确保线程安全。
1
2
3
4
5
6
7
8
9
10
public class Singleton {  
private static Singleton instance;
private Singleton() {}
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
  1. 双重检查锁定(DCL):减少同步开销,但在JDK 1.5以前的版本中可能存在单例初始化问题。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Singleton {  
private volatile static Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}

这些问题涉及到了计算机网络、数据结构与算法、数据库和分布式系统等多个领域。下面我将逐一进行回答:

  1. TCP与UDP的区别

    • TCP(传输控制协议)是面向连接的、可靠的、字节流的协议。它提供流量控制、错误检测和重传机制,确保数据按序到达。
    • UDP(用户数据报协议)是无连接的、不可靠的、数据报协议。它不保证数据包的顺序、可靠性或完整性,但传输速度快,常用于实时通信或流媒体。
  2. 死锁的产生及解决方法

    • 死锁发生在两个或更多进程因争夺系统资源而相互等待的现象,若无外力作用,它们都将无法向前推进。
    • 解决方法包括:预防死锁(通过资源有序分配等方法避免死锁的发生)、避免死锁(通过银行家算法等确保系统始终处于安全状态)、检测与解除死锁(通过检测机制发现死锁,并采取资源剥夺或撤销进程等方法解除死锁)。
  3. 什么时候用数组,什么时候用链表

    • 数组适用于存储固定大小的数据集,且数据访问频繁。数组在内存中是连续的,因此通过索引访问数据速度快。
    • 链表适用于存储动态变化的数据集,且插入和删除操作频繁。链表在内存中不是连续的,通过指针或引用连接各个节点,因此插入和删除操作相对灵活。
  4. 红黑树的定义

    • 红黑树是一种自平衡的二叉查找树,它满足以下五个性质:每个节点要么是红色,要么是黑色;根节点是黑色;所有叶子都是黑色(叶子是NIL或空节点);如果一个节点是红色的,则它的两个子节点都是黑色的;对于每个节点,从该节点到其所有后代叶子节点的简单路径上,均包含相同数目的黑色节点。
  5. HashMap

    • HashMap是Java中的一个哈希表实现,它允许存储键值对。HashMap基于哈希算法存储数据,提供常数时间复杂度的插入和查找操作。HashMap内部使用数组和链表(或红黑树)来存储数据,当链表长度超过一定阈值时,会转换为红黑树以优化性能。
  6. POST和GET的区别

    • GET用于请求数据,它将参数附加在URL中,大小有限制,且不适合传输敏感信息。GET请求可以被缓存、书签保存和无限次重放。
    • POST用于提交数据,它将参数放在请求体中,大小没有限制,适合传输大量或敏感数据。POST请求不会被缓存,也不会出现在浏览器的历史记录中。
  7. MySQL索引

    • 索引是MySQL中用于提高查询性能的一种数据结构。它类似于书籍的目录,可以快速定位到数据库表中的特定数据。常见的索引类型有B-Tree索引、哈希索引、全文索引等。
  8. 什么情况索引会失效

    • 索引失效通常发生在以下情况:查询条件中使用函数或表达式;查询中未使用到索引列;查询中使用不等于(<>)或NOT IN等操作符;数据类型不匹配导致隐式转换;使用LIKE查询时以通配符开头;OR连接的条件列不是同一个索引等。
  9. 事务

    • 事务是数据库操作的一个逻辑单位,它包含了一系列对数据库的读/写操作。这些操作要么全部成功,要么全部失败,从而保证数据的完整性和一致性。事务具有ACID四个特性:原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)和持久性(Durability)。
  10. 事务实现方法

    • 事务的实现通常依赖于数据库的支持。在SQL中,事务可以通过BEGIN TRANSACTION开始,通过COMMIT提交,通过ROLLBACK回滚。此外,许多编程语言和框架也提供了对事务的支持,如Java的JDBC、Spring框架等。
  11. JWT(JSON Web Token)

    • JWT是一种开放标准(RFC 7519)定义的方法,用于在双方之间安全地传输信息。这些信息可以被验证和信任,因为它们是数字签名的。JWT可以使用HMAC算法或者使用RSA或ECDSA的公钥/私钥对进行签名。JWT由三部分组成:头部(Header)、负载(Payload)和签名(Signature)。JWT通常用于身份验证和信息交换,因为它们可以包含所需的所有信息,并且可以在服务器之间安全地传输。

这些只是对这些概念的简要概述。每个主题都有更深入的细节和特定的应用场景,需要根据具体需求进行学习和应用。

好的,下面是对您提出的问题的详细回答:

  1. 索引
    索引是数据库表中一个或多个字段的排序列表,用于加速数据的检索速度。常见的索引类型包括主键索引、唯一索引、普通索引等。索引能够显著提高查询性能,但也会占用额外的存储空间,并可能增加插入、删除和更新操作的开销。
  2. 索引使用的注意事项
    • 避免过度索引,因为每个索引都需要维护,会占用存储空间并降低写操作的性能。
    • 选择最常用作查询条件的字段建立索引。
    • 考虑索引的选择性,即索引中不同值的比例,高选择性的索引通常更有效。
    • 对于联合索引,要注意字段的顺序,因为索引是按照字段顺序进行排序的。
    • 定期维护索引,例如重建或重新组织索引,以保持其性能。
  3. 索引使用的数据结构
    大多数数据库系统使用B+树或其变种作为索引的数据结构。B+树是一种平衡的多路搜索树,能够保持数据的有序性,并且具有良好的查询性能。
  4. 为什么是B+树
    • B+树能够保持数据的有序性,这对于范围查询非常有利。
    • B+树的非叶子节点不存储数据,只存储关键字和子节点的指针,这使得B+树能够比B树存储更多的关键字,降低树的高度,减少查询时的磁盘I/O次数。
    • B+树的叶子节点之间通过指针相连,方便进行范围查询。
  5. 联合索引在非叶子节点是原子性的吗
    联合索引在非叶子节点不是原子性的。联合索引的每个字段在树的不同层级上可能作为索引的一部分,但在非叶子节点上不会存储完整的记录或数据行。非叶子节点只存储索引关键字和指向下一级节点的指针。
  6. 走索引范围查询的原理和全流程
    范围查询利用索引的有序性,通过遍历索引树来找到满足条件的记录。首先,根据查询条件的起始值,在索引树中定位到起始节点;然后,沿着索引树的路径向下遍历,直到找到所有满足条件的叶子节点;最后,返回这些叶子节点对应的记录。
  7. 创建不可改变的集合
    在Java中,可以使用Collections.unmodifiableCollection方法将一个已存在的集合转换为不可修改的集合。这个不可修改的集合在试图修改时会抛出UnsupportedOperationException
  8. 创建不可被修改的对象
    创建不可被修改的对象通常涉及将对象的所有字段都设置为final,并确保没有提供修改这些字段的方法。此外,对于集合类型的字段,可以使用不可修改的集合来确保它们的内容也不会被修改。
  9. HashMap的底层
    HashMap的底层实现主要基于数组和链表(在Java 8及以后版本中,当链表长度达到一定阈值时,会转换为红黑树)。HashMap通过哈希函数将键(key)映射到数组的某个索引位置,如果该位置已经存在元素,则通过链表或红黑树解决冲突。
  10. 为什么用红黑树
    HashMap在链表长度过长时转换为红黑树是为了优化性能。红黑树是一种自平衡的二叉搜索树,能够在动态插入和删除操作中保持相对平衡,从而确保查询、插入和删除操作的时间复杂度接近O(log n)。这有助于避免在链表过长时导致的性能下降。
  11. Spring Boot自动装配与反射的关系
    Spring Boot的自动装配功能在很大程度上依赖于Java反射机制。通过反射,Spring容器能够在运行时动态地创建对象、调用方法、访问字段等。自动装配则是Spring框架根据配置文件或注解自动创建和装配bean的过程。在这个过程中,Spring利用反射来实例化bean、注入依赖等。
  12. AOP动态代理的原理
    AOP(面向切面编程)通过动态代理技术实现。在Java中,动态代理主要有两种实现方式:基于接口的JDK动态代理和基于类的CGLIB动态代理。JDK动态代理利用反射机制在运行时动态生成代理类,代理类实现与目标对象相同的接口,并拦截对目标对象方法的调用。CGLIB则通过继承目标类来创建代理子类,并覆盖目标方法以实现代理逻辑。
  13. G1回收器的原理
    G1(Garbage-First)回收器是Java虚拟机(JVM)中的一种垃圾回收器,旨在提供可预测的停顿时间,同时保持较高的吞吐量。G1将堆内存划分为多个大小相等的区域(Region),并优先回收垃圾最多的区域。它采用了一种标记-复制-清理的算法,通过跟踪对象的引用关系来标记存活对象,并将存活对象复制到新的区域中,然后清理旧区域中的垃圾。G1还通过优先回收垃圾最多的区域来优化性能,减少全堆扫描的开销。

  1. 红黑树的特征以及HashMap选择红黑树的原因

红黑树是一种自平衡的二叉搜索树,它满足以下五个特性:

  • 每个节点要么是红色,要么是黑色。
  • 根节点是黑色。
  • 所有叶子节点(NIL或空节点)是黑色。
  • 如果一个节点是红色,则它的两个子节点都是黑色。
  • 对于每个节点,从该节点到其所有后代叶子节点的简单路径上,均包含相同数目的黑色节点。

HashMap选择红黑树作为链表过长时的替代结构,主要是因为红黑树在查找、插入和删除操作上的时间复杂度都是O(log n),相较于链表(O(n))有更好的性能。当HashMap中的桶(bucket)链表长度超过一定阈值时,将其转换为红黑树可以显著提高性能。然而,当树的大小小于某个阈值时,为了避免树的维护开销,它会退化为链表。

  1. 线程安全的集合

Java中线程安全的集合主要包括:

  • Vector
  • Hashtable
  • Collections工具类中的同步包装方法(如Collections.synchronizedList)包装后的集合
  • ConcurrentHashMap(适用于高并发场景)
  • CopyOnWriteArrayList(读多写少的场景)

这些集合通过内部同步机制或特殊的并发控制策略来保证线程安全。

  1. Spring AOP与AspectJ的区别

Spring AOP和AspectJ都提供了面向切面编程(AOP)的功能,但它们之间存在一些区别:

  • Spring AOP是基于代理的,它只能拦截通过Spring容器管理的bean之间的方法调用。而AspectJ则是一个完整的AOP框架,它提供了编译时和加载时的织入,能够拦截任何方法调用,不仅仅是Spring容器管理的bean。
  • Spring AOP支持的通知类型较少,而AspectJ支持更丰富的通知类型,如环绕通知等。
  • AspectJ的功能更为强大,但使用起来也相对复杂一些。Spring AOP则更加轻量级和易于集成到Spring应用中。
  1. Spring中AOP的JDK动态代理与CGLIB代理的区别及替代方案

JDK动态代理是基于接口的代理,它要求被代理的对象必须实现一个或多个接口。而CGLIB代理是基于类的代理,通过继承目标类来创建代理对象,因此目标类不能是final类。

如果不用JDK动态代理和CGLIB代理,还可以使用AspectJ来实现AOP。AspectJ提供了更为强大的AOP功能,包括编译时和加载时的织入。

  1. JVM如何与操作系统交互

JVM通过JNI(Java Native Interface)与本地方法库进行交互,从而可以调用操作系统提供的本地方法。此外,JVM还通过文件系统、网络、进程间通信(如管道、信号、共享内存等)与操作系统进行交互。例如,JVM加载类文件时需要访问文件系统;JVM中的线程实际上是操作系统中的线程或进程,因此线程的管理和调度也涉及到与操作系统的交互。

  1. HTTP协议简介

HTTP(Hypertext Transfer Protocol)是一种应用层协议,用于在Web浏览器和服务器之间传输超文本。它基于请求-响应模型,客户端(如浏览器)发送请求到服务器,服务器处理请求并返回响应。HTTP协议是无状态的,即服务器不会记住之前与客户端的交互信息。HTTP/1.1版本中引入了持久连接(keep-alive)和管道化(pipelining)等特性来提高性能。此外,还有HTTP/2版本,它进一步通过多路复用、头部压缩等技术优化了性能。

  1. 进程间通信(IPC)

进程间通信(IPC)是指在不同进程之间传递信息或数据的方式。常见的IPC机制包括:

  • 管道(Pipe):用于父子进程之间的通信。
  • 消息队列(Message Queue):允许进程之间通过发送和接收消息来进行通信。
  • 共享内存(Shared Memory):允许多个进程访问同一块内存区域,从而实现数据的共享和通信。
  • 信号量(Semaphore):用于同步和互斥,确保多个进程对共享资源的正确访问。
  • 套接字(Socket):用于不同主机之间的进程通信,是网络编程的基础。
  1. 用Redis实现分布式事务

Redis本身不支持传统的ACID事务,但可以通过一些策略来实现分布式事务的效果。一种常见的方法是使用Redis的事务(MULTI/EXEC)和Lua脚本,结合watch机制来检测键的变化。另外,还可以利用Redis的发布订阅功能来协调多个客户端的操作,实现分布式事务的提交或回滚。在实际应用中,可能还需要结合业务逻辑和补偿机制来确保事务的完整性和一致性。

  1. Web请求过程

Web请求过程通常涉及以下步骤:

  • 用户输入URL:用户在浏览器的地址栏中输入一个网址(URL)。
  • DNS解析:浏览器向DNS服务器发出请求,将输入的域名解析为对应的IP地址。
  • 建立TCP连接:浏览器通过TCP协议与服务器建立连接。如果是HTTPS协议,则还需进行SSL/TLS握手,以建立加密的通信通道。
  • 发送HTTP请求:浏览器根据用户在地址栏中输入的URL,或者通过表单、AJAX等方式,构建HTTP请求,并通过TCP连接发送给服务器。
  • 服务器处理请求:服务器接收到HTTP请求后,根据请求的URL和HTTP方法(如GET、POST等),找到对应的处理程序或资源,并进行相应的处理。这可能涉及读取数据库、执行业务逻辑、生成动态页面等。
  • 发送HTTP响应:服务器处理完请求后,生成HTTP响应,并通过TCP连接发送回浏览器。响应中包含状态码(如200表示成功,404表示未找到资源等)、响应头和响应体(如HTML页面、JSON数据等)。
  • 浏览器解析渲染:浏览器接收到HTTP响应后,解析响应头和内容,并根据需要加载相关的资源(如CSS、JavaScript、图片等)。然后,浏览器引擎将HTML内容渲染成用户可以看到的页面。
  • 断开TCP连接:在数据传输完成后,浏览器和服务器通常会断开TCP连接。如果是HTTP/1.1中的持久连接(keep-alive),连接可能会保持一段时间以便复用,但在一段时间后或双方同意的情况下,连接仍会被关闭。

整个Web请求过程中,还可能涉及缓存机制、重定向、负载均衡、代理服务器等技术或组件,这些都会影响请求的处理和响应的速度。同时,安全性也是Web请求过程中需要考虑的重要因素,包括HTTPS加密、防止跨站脚本攻击(XSS)、跨站请求伪造(CSRF)等安全措施的应用。

好的,我会一一回答你的问题。

  1. 项目介绍

项目介绍通常包括项目的背景、目标、主要功能、技术栈、面临的挑战以及解决方案等。具体的内容会根据实际项目的不同而有所差异。例如,一个电商项目可能会介绍其如何实现商品展示、购物车管理、订单处理等功能,以及使用了哪些技术栈(如Spring Boot、MySQL、Redis等)来构建这个项目。

  1. MQ(消息队列)如何保证数据不丢失,消息消费失败了怎么办,消费者数量怎么设计
  • 数据不丢失

    • 使用持久化存储:确保消息队列将数据持久化到磁盘或其他可靠存储中。
    • 确认机制:生产者发送消息后,等待消息队列的确认;消费者消费消息后,也发送确认消息给队列。
    • 备份和恢复:定期备份消息队列数据,以便在出现问题时可以恢复。
  • 消息消费失败

    • 重试机制:消费者消费失败时,可以设定重试次数和重试间隔。
    • 死信队列:将多次重试仍失败的消息发送到死信队列,由专门的程序或人工处理。
    • 延迟队列:对于需要延迟处理的消息,可以使用延迟队列。
  • 消费者数量设计

    • 根据业务需求和系统性能来设计。
    • 考虑消息的处理速度、系统的负载能力、消息的实时性等因素。
    • 可以动态调整消费者数量以应对流量变化。
  1. ES(Elasticsearch)的特性、分词、倒排、排序
  • 特性
    • 分布式搜索引擎。
    • 实时分析。
    • 近实时的搜索和分析。
    • 全文搜索。
    • 结构化搜索。
  • 分词:将文本切分为单个的词或词组,以便进行索引和搜索。
  • 倒排索引:Elasticsearch的核心数据结构,用于快速查找包含特定词的文档。
  • 排序:Elasticsearch支持多种排序方式,如按字段值排序、按距离排序等。
  1. MySQL相关
  • 索引覆盖:查询只需要通过索引就可以获取到数据,无需回表到数据行。
  • 联合索引:多个字段组合在一起的索引,使用时需遵循最左前缀原则。
  • 回表:当查询的字段不是索引的全部时,需要回到数据行中获取剩余字段的值。
  • 查看索引使用情况:可以通过EXPLAIN命令查看查询是否使用了索引,以及使用了哪个索引。
  • 索引失效:如使用函数、隐式类型转换等都可能导致索引失效。
  • **四大特性(ACID)**:原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)、持久性(Durability)。
  1. 线程池
  • 拒绝策略
    • AbortPolicy:直接抛出异常。
    • CallerRunsPolicy:调用者运行任务,即让提交任务的线程自己执行该任务。
    • DiscardOldestPolicy:丢弃队列中等待最久的任务,然后重新尝试执行任务。
    • DiscardPolicy:直接丢弃任务,不做任何处理。
  • CallerRunsPolicy详解:当线程池队列已满,且线程池中的线程数量也达到最大值时,如果继续提交任务,就会使用CallerRunsPolicy策略。这时,提交任务的线程会自己执行该任务,而不是将任务丢弃或抛出异常。这样可以确保任务不会被丢失,但也可能导致提交任务的线程被阻塞,从而影响系统的性能。
  • 线程创建方式
    • 继承Thread类并重写run方法。
    • 实现Runnable接口并重写run方法。
    • 实现Callable接口并重写call方法(可以返回结果并抛出异常)。
    • 使用线程池(如ExecutorService)。
  1. ThreadLocal
  • ThreadLocal:提供线程内的局部变量。每个线程都有自己独立的变量副本,不会和其他线程的变量互相干扰。
  • 用途:常用于保存线程上下文信息,如数据库连接、用户信息等。
  • 注意事项:使用完毕后要及时清理ThreadLocal中的变量,避免内存泄漏。

希望这些回答能帮到你!如果你有其他问题或需要更详细的解释,请随时告诉我。


砺剑出鞘:我的软件工程师求职之旅
http://example.com/2024/03/07/砺剑出鞘:我的软件工程师求职之旅/
作者
Memory
发布于
2024年3月7日
更新于
2024年3月15日
许可协议