爆肝四万字,写给Java零基础小白的自学路线和进阶指南,收藏能省掉几万学费。

Java核心架构进阶知识点

面试成功其实都是必然发生的事情,因为在此之前我做足了充分的准备工作,不单单是纯粹的刷题,更多的还会去刷一些Java核心架构进阶知识点,比如:JVM、高并发、多线程、缓存、Spring相关、分布式、微服务、RPC、网络、设计模式、MQ、Redis、MySQL、设计模式、负载均衡、算法、数据结构、kafka、ZK、集群等。而这些也全被整理浓缩到了一份pdf——《Java核心架构进阶知识点整理》,全部都是精华中的精华,本着共赢的心态,好东西自然也是要分享的

image

image

image

内容颇多,篇幅却有限,这就不在过多的介绍了,大家可根据以上截图自行脑补

本文已被CODING开源项目:【一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码】收录

需要这份系统化的资料的朋友,可以点击这里获取

1.标记-清除

两次遍历:

  • 标记垃圾

  • 清除垃圾

优点:

  • 不需要格外空间,适合回收对象较少的区域

缺点:

  • 效率低,遍历两次,时间复杂度O(n^2)

  • 会有线程停顿,stop the world (STW)

  • 空间碎片,因为垃圾可能不是连续的,大量的空间碎片会导致提前GC,这也是最主要的问题。

2.标记-复制

将空间分为相等大小的两部分,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用的内存空间一次清理掉,这样一来就不容易出现内存碎片的问题。牺牲空间解决碎片问题。

优点:

  • 高效无碎片

缺点:

  • 占用大量空间

3.标记-整理

同样是为了解决空间碎片提出,区别是通过牺牲时间的方式。

和标记-清除类似,不一样的是在完成标记之后,它不是直接清理可回收对象,而是将存活对象都向一端移动,然后清理掉端边界以外的内存。

优点:

  • 解决空间碎片的问题

  • 不浪费空间

缺点:

  • 相对比较耗时

完整讲解

JVM完整讲解

配套资料

1.深入理解Java虚拟机

附带读书笔记。

2.视频

5.多线程


理解多线程,才能更好的理解框架源码,进行高并发的架构设计,重中之重。

并行和并发

并行:多个任务在同一个 CPU 核上,按细分的时间片轮流(交替)执行,从逻辑上来看那些任务是同时执行。

并发:多个处理器或多核处理器同时处理多个任务。

举例:

并发 = 两个队列和一台咖啡机。

并行 = 两个队列和两台咖啡机。

线程和进程

一个程序下至少有一个进程,一个进程下至少有一个线程,一个进程下也可以有多个线程来增加程序的执行速度。

守护线程

守护线程是运行在后台的一种特殊进程。它独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。在 Java 中垃圾回收线程就是特殊的守护线程。

创建线程4种方式

  • 继承 Thread 重新 run 方法;

  • 实现 Runnable 接口;

  • 实现 Callable 接口。

  • 线程池

synchronized 底层实现

synchronized 是由一对 monitorenter/monitorexit 指令实现的,monitor 对象是同步的基本实现单元。

在 Java 6 之前,monitor 的实现完全是依靠操作系统内部的互斥锁,因为需要进行用户态到内核态的切换,所以同步操作是一个无差别的重量级操作,性能也很低。

但在 Java 6 的时候,Java 虚拟机 对此进行了大刀阔斧地改进,提供了三种不同的 monitor 实现,也就是常说的三种不同的锁:偏向锁(Biased Locking)、轻量级锁和重量级锁,大大改进了其性能。

synchronized 和 volatile 的区别

volatile 是变量修饰符;synchronized 是修饰类、方法、代码段。

volatile 仅能实现变量的修改可见性,不能保证原子性;而 synchronized 则可以保证变量的修改可见性和原子性。

volatile 不会造成线程的阻塞;synchronized 可能会造成线程的阻塞。

synchronized 和 ReentrantLock 区别

synchronized 早期的实现比较低效,对比 ReentrantLock,大多数场景性能都相差较大,但是在 Java 6 中对 synchronized 进行了非常多的改进。

主要区别如下:

ReentrantLock 使用起来比较灵活,但是必须有释放锁的配合动作;

ReentrantLock 必须手动获取与释放锁,而 synchronized 不需要手动释放和开启锁;

ReentrantLock 只适用于代码块锁,而 synchronized 可用于修饰方法、代码块等。

volatile 标记的变量不会被编译器优化;synchronized 标记的变量可以被编译器优化。

synchronized 和 Lock 区别

synchronized 可以给类、方法、代码块加锁;而 lock 只能给代码块加锁。

synchronized 不需要手动获取锁和释放锁,使用简单,发生异常会自动释放锁,不会造成死锁。

lock 需要自己加锁和释放锁,如果使用不当没有 unLock()去释放锁就会造成死锁。

通过 Lock 可以知道有没有成功获取锁,而 synchronized 却无法办到。

配套资料

1 .java并发编程的艺术

《Java并发编程的艺术》内容涵盖Java并发编程机制的底层实现原理、Java内存模型、Java并发编程基础、Java中的锁、并发容器和框架、原子类、并发工具类、线程池、Executor框架等主题,每个主题都做了深入的讲解,同时通过实例介绍了如何应用这些技术。

6.设计模式


好多人觉得设计模式模式,那是因为你学的还不够深入,还没有看过源码,所以我特意将设计模式往前放了。

建造者模式

定义


官方定义

将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。

通俗解读

提供一种创建对象的方式,创建的东西细节复杂,还必须暴露给使用者。屏蔽过程而不屏蔽细节

类似建房子,只需要把材料和设计图纸给工人,就能建成想要的房子,不关注工人建房子的过程,但对于细节,我们又可以自己设计。

结构图


代码演示


本文源码:建造者模式 提取码: vpqt

目录结构

建议跟着一条学设计模式的小伙伴都建一个maven工程,并安装lombok依赖和插件。

并建立如下包目录,便于归纳整理。

pom如下

org.projectlombok

lombok

1.16.10

开发场景

现在有一个手机的建造者,我要让它为我生产不用品牌和配置的手机。该怎么实现?

代码演示

1.创建手机类

@Data

public class Phone {

//处理器

protected String cpu;

//内存

protected String mem;

//磁盘

protected String disk;

//屏幕大小

protected String size;

}

2.创建建造者接口

//定义建造者的模板方法

public interface Builder {

Phone phone = new Phone();

void buildCpu(String cpu);

void buildMem(String mem);

void buildDisk(String disk);

void buildSize(String size);

default Phone getPhone(){

return phone;

}

}

3.创建Vivo手机的建造者

public class VivoPhoneBuilder implements Builder{

//建造者细节的实现

@Override

public void buildCpu(String cpu) {

phone.cpu=cpu;

}

@Override

public void buildMem(String mem) {

phone.mem=mem;

}

@Override

public void buildDisk(String disk) {

phone.disk=disk;

}

@Override

public void buildSize(String size) {

phone.size=size;

}

}

4.创建测试类

public class MainTest {

public static void main(String[] args) {

VivoPhoneBuilder builder = new VivoPhoneBuilder();

builder.buildCpu(“888”);

builder.buildDisk(“512”);

builder.buildMem(“16”);

builder.buildSize(“plus”);

Phone phone = builder.getPhone();

System.out.println(phone);

}

}

5.输出结果

如果我这时需要生产OPPO手机,只需新建一个OppoPhoneBuilder实现Builder接口即可。

链式调用


相信大家在开发中都遇见过这样的代码,像链子一样可以一直调用下去。

那么如何实现链式建造者呢?

有以下两种方式:

1.修改返回值为Builder

public interface Builder {

Phone phone = new Phone();

// void 改为 Builder 同步修改实现类

Builder buildCpu(String cpu);

Builder buildMem(String mem);

Builder buildDisk(String disk);

Builder buildSize(String size);

default Phone getPhone(){

return phone;

}

}

测试1

public class MainTest {

public static void main(String[] args) {

// ……

VivoPhoneBuilder builder2 = new VivoPhoneBuilder();

Phone phone1 = builder2

.buildCpu(“888”)

.buildDisk(“512”)

.buildMem(“16”)

.buildSize(“plus”)

.getPhone();

System.out.println(“phone1:”+phone1);

}

}

结果1

2.使用lombok

@Data

@Builder //使用链式建造者

@NoArgsConstructor

@AllArgsConstructor

public class Phone {

// ……

}

测试2

public class MainTest {

public static void main(String[] args) {

// ……

Phone build = Phone.builder()

.cpu(“888”)

.mem(“16”)

.disk(“512”)

.size(“plus”).build();

System.out.println(“builder:”+build);

}

}

结果2

应用场景


  • StringBuilder:append(); 给谁append呢?

public AbstractStringBuilder append(String str) {

if (str == null)

return appendNull();

int len = str.length();

ensureCapacityInternal(count + len);

str.getChars(0, len, value, count);

count += len;

return this;

}

  • Swagger-ApiBuilder;

  • 快速实现:Lombok-@Builder

总结


建造者模式提供了对于同一构建过程的不同表示,像流水线一样生产对象。对于新增的对象,只需要创建对应的建造者即可,不需要修改源代码。

lombok为我们提供了建造者模式的快速实现(@Builder),要应用到实际编码中。

更多设计模式

更多设计模式

7.SSM框架


这对于初学者来说,是一个坎,前几年学完这些,已经可以开始找工作了,所以恭喜你能坚持带这里,胜利就在前方。

ORM 框架?

ORM(Object Relation Mapping)对象关系映射,是把数据库中的关系数据映射成为程序中的对象。

使用 ORM 的优点:提高了开发效率降低了开发成本、开发更简单更对象化、可移植更强。

MyBatis 中 #{}和 的区别

#是预编译处理,{}的区别是什么?#{}是预编译处理,的区别是什么?#是预编译处理,{}是字符替换。 在使用 #{}时,MyBatis 会将 SQL 中的 #{}替换成“?”,配合 PreparedStatement 的 set 方法赋值,这样可以有效的防止 SQL 注入,保证程序的运行安全。

什么是Spring

spring 提供 ioc 技术,容器会帮你管理依赖的对象,从而不需要自己创建和管理依赖对象了,更轻松的实现了程序的解耦。

spring 提供了事务支持,使得事务操作变的更加方便。

spring 提供了面向切片编程,这样可以更方便的处理某一类的问题。

更方便的框架集成,spring 可以很方便的集成其他框架,比如 MyBatis、hibernate 等。

什么是 aop

aop 是面向切面编程,通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术。

简单来说就是统一处理某一“切面”(类)的问题的编程思想,比如统一处理日志、异常等。

什么是 ioc

ioc:Inversionof Control(中文:控制反转)是 spring 的核心,对于 spring 框架来说,就是由 spring 来负责控制对象的生命周期和对象间的关系。

简单来说,控制指的是当前对象对内部成员的控制权;控制反转指的是,这种控制权不由当前对象管理了,由其他(类,第三方容器)来管理。

spring mvc 运行流程

spring mvc 先将请求发送给 DispatcherServlet。

DispatcherServlet 查询一个或多个 HandlerMapping,找到处理请求的 Controller。

DispatcherServlet 再把请求提交到对应的 Controller。

Controller 进行业务逻辑处理后,会返回一个ModelAndView。

Dispathcher 查询一个或多个 ViewResolver 视图解析器,找到 ModelAndView 对象指定的视图对象。

视图对象负责渲染返回给客户端。

什么是 spring boot?

spring boot 是为 spring 服务的,是用来简化新 spring 应用的初始搭建以及开发过程的。

为什么要用 spring boot?

配置简单

独立运行

自动装配

无代码生成和 xml 配置

提供应用监控

易上手

提升开发效率

8.Redis


随着QPS的逐渐升高,传统的mysql数据库已经无法满足。所以有了基于内存的redis缓存数据库来存储热点数据。

什么是Redis

Redis 是一个使用 C 语言开发的高速缓存数据库。

Redis 使用场景:

  • 记录帖子点赞数、点击数、评论数;

  • 缓存近期热帖;

  • 缓存文章详情信息;

  • 记录用户会话信息。

Redis 的功能

  • 数据缓存功能

  • 分布式锁的功能

  • 支持数据持久化

  • 支持事务

  • 支持消息队列

Redis 和 memcache

存储方式不同:memcache 把数据全部存在内存之中,断电后会挂掉,数据不能超过内存大小;Redis 有部份存在硬盘上,这样能保证数据的持久性。

数据支持类型:memcache 对数据类型支持相对简单;Redis 有复杂的数据类型。

使用底层模型不同:它们之间底层实现方式,以及与客户端之间通信的应用协议不一样,Redis 自己构建了 vm 机制,因为一般的系统调用系统函数的话,会浪费一定的时间去移动和请求。

value 值大小不同:Redis 最大可以达到 1gb;memcache 只有 1mb。

Redis 为什么是单线程的

因为 cpu 不是 Redis 的瓶颈,Redis 的瓶颈最有可能是机器内存或者网络带宽。既然单线程容易实现,而且 cpu 又不会成为瓶颈,那就顺理成章地采用单线程的方案了。

关于 Redis 的性能,官方网站也有,普通笔记本轻松处理每秒几十万的请求。

而且单线程并不代表就慢 nginx 和 nodejs 也都是高性能单线程的代表。

缓存穿透

缓存穿透:指查询一个一定不存在的数据,由于缓存是不命中时需要从数据库查询,查不到数据则不写入缓存,这将导致这个不存在的数据每次请求都要到数据库去查询,造成缓存穿透。

解决方案:最简单粗暴的方法如果一个查询返回的数据为空(不管是数据不存在,还是系统故障),我们就把这个空结果进行缓存,但它的过期时间会很短,最长不超过五分钟。

Redis 数据类型

Redis 支持的数据类型:string(字符串)、list(列表)、hash(字典)、set(集合)、zset(有序集合)。

配套资料

Redis 深度历险:核心原理与应用实践

《Redis 深度历险:核心原理与应用实践》分为基础和应用篇、原理篇、集群篇、拓展篇、源码篇共 5 大块内容。基础和应用篇讲解对读者来说最有价值的内容,可以直接应用到实际工作中;原理篇、集群篇让开发者透过简单的技术表面看到精致的底层世界;拓展篇帮助读者拓展技术视野和夯实基础,便于进阶学习;源码篇让高阶的读者能够读懂源码,掌握核心技术实力。

9.Zookeeper


Zookeeper作为统一配置文件管理和集群管理框架,是后续学习其他框架的基础,在微服务中,还可以用来做注册中心。

什么是zookeeper

zookeeper 是一个分布式的,开放源码的分布式应用程序协调服务,是 google chubby 的开源实现,是 hadoop 和 hbase 的重要组件。它是一个为分布式应用提供一致性服务的软件,提供的功能包括:配置维护、域名服务、分布式同步、组服务等。

zookeeper 的功能

集群管理:监控节点存活状态、运行请求等。

主节点选举:主节点挂掉了之后可以从备用的节点开始新一轮选主,主节点选举说的就是这个选举的过程,使用 zookeeper 可以协助完成这个过程。

分布式锁:zookeeper 提供两种锁:独占锁、共享锁。独占锁即一次只能有一个线程使用资源,共享锁是读锁共享,读写互斥,即可以有多线线程同时读同一个资源,如果要使用写锁也只能有一个线程使用。zookeeper可以对分布式锁进行控制。

命名服务:在分布式系统中,通过使用命名服务,客户端应用能够根据指定名字来获取资源或服务的地址,提供者等信息。

zookeeper 的部署模式

zookeeper 有三种部署模式:

单机部署:一台集群上运行;

集群部署:多台集群运行;

伪集群部署:一台集群启动多个 zookeeper 实例运行。

10.Kafka


kafka和zookeeper的关系

kafka 不能脱离 zookeeper 单独使用,因为 kafka 使用 zookeeper 管理和协调 kafka 的节点服务器。

kafka的数据保留的策略?

kafka 有两种数据保存策略:按照过期时间保留和按照存储的消息大小保留。

kafka 同时设置了 7 天和 10G 清除数据,到第五天的时候消息达到了 10G,这个时候 kafka 将如何处理?

这个时候 kafka 会执行数据清除工作,时间和大小不论那个满足条件,都会清空数据。

kafka性能瓶颈

cpu 性能瓶颈

磁盘读写瓶颈

网络瓶颈

kafka集群

集群的数量不是越多越好,最好不要超过 7 个,因为节点越多,消息复制需要的时间就越长,整个群组的吞吐量就越低。

集群数量最好是单数,因为超过一半故障集群就不能用了,设置为单数容错率更高。

配套资料

Apache Kafka实战

《Apache Kafka实战》是一本涵盖Apache Kafka各方面的具有实践指导意义的工具书和参考书。作者结合典型的使用场景,对Kafka整个技术体系进行了较为全面的讲解,以便读者能够举一反三,直接应用于实践。同时,本书还对Kafka的设计原理及其流式处理组件进行了较深入的探讨,并给出了翔实的案例。


11.ES


elasticsearch简写es,es是一个高扩展、开源的全文检索和分析引擎,它可以准实时地快速存储、搜索、分析海量的数据。

12.Dubbo


Dubbo是一个分布式服务框架,致力于提供高性能和透明化的RPC远程服务调用方案,以及SOA服务治理方案。简单的说,dubbo就是个服务框架

13.SpringCloud


Spring Cloud是一个微服务框架。Spring Cloud提供了全套的分布式系统解决方案,不仅对微服务基础框架Netflix的多个开源组件进行了封装,同时还实现了和云端平台以及Spring Boot开发框架的集成。

什么是 spring cloud

spring cloud 是一系列框架的有序集合。它利用 spring boot 的开发便利性巧妙地简化了分布式系统基础设施的开发,如服务发现注册、配置中心、消息总线、负载均衡、断路器、数据监控等,都可以用 spring boot 的开发风格做到一键启动和部署。

spring cloud 的核心组件

Eureka:服务注册于发现。

Feign:基于动态代理机制,根据注解和选择的机器,拼接请求 url 地址,发起请求。

Ribbon:实现负载均衡,从一个服务的多台机器中选择一台。

Hystrix:提供线程池,不同的服务走不同的线程池,实现了不同服务调用的隔离,避免了服务雪崩的问题。

Zuul:网关管理,由 Zuul 网关转发请求给对应的服务。

断路器的作用

在分布式架构中,断路器模式的作用也是类似的,当某个服务单元发生故障(类似用电器发生短路)之后,通过断路器的故障监控(类似熔断保险丝),向调用方返回一个错误响应,而不是长时间的等待。这样就不会使得线程因调用故障服务被长时间占用不释放,避免了故障在分布式系统中的蔓延。

配套资料

14.Nginx


Nginx是一个高性能的HTTP和反向代理服务器。具有占内存少和并发能力强的特点。

15.Netty


netty 是一个基于nio的客户、服务器端编程框架,netty提供异步的,事件驱动的网络应用程序框架和工具,可以快速开发高可用的客户端和服务器。

16.架构设计


好的架构从来不是设计出来的,而是演变出来的。所以我们在日常开发中就要不断思考性能的优化。

下面拿如何设计一个百万人抽奖系统举例说明架构的演进。

V0——单体架构

如果现在让你实现几十人的抽奖系统,简单死了吧,直接重拳出击!

两猫一豚走江湖,中奖入库,调通知服务,查库通知,完美!

相信大家学java时可能都做过这种案例,思考🤔一下存在什么问题?

  • 单体服务,一着不慎满盘皆输

  • 抽了再抽,一个人就是一支军队

  • 恶意脚本,没有程序员中不了的奖

接下来就聊聊怎么解决这些问题?

V1——负载均衡

当一台服务器的单位时间内的访问量越大时,服务器压力就越大,大到超过自身承受能力时,服务器就会崩溃。

为了避免服务器崩溃,让用户有更好的体验,我们通过负载均衡的方式来分担服务器压力。

负载均衡就是建立很多很多服务器,组成一个服务器集群,当用户访问网站时,先访问一个中间服务器,好比管家,由他在服务器集群中选择一个压力较小的服务器,然后将该访问请求引入该服务器。

如此以来,用户的每次访问,都会保证服务器集群中的每个服务器压力趋于平衡,分担了服务器压力,避免了服务器崩溃的情况。

负载均衡是用「反向代理」的原理实现的。具体负载均衡算法及其实现方式我们下文再续。

负载均衡虽然解决了单体架构一着不慎满盘皆输的问题,但服务器成本依然不能保护系统周全,我们必须想好一旦服务器宕机,如何保证用户的体验。

即如何缓解开奖一瞬间时的大量请求。

V2——服务限流

限流主要的作用是保护服务节点或者集群后面的数据节点,防止瞬时流量过大使服务和数据崩溃(如前端缓存大量实效),造成不可用。

还可用于平滑请求。

在上一小节我们做好了负载均衡来保证集群的可用性,但公司需要需要考虑服务器的成本,不可能无限制的增加服务器数量,一般会经过计算保证日常的使用没问题。

限流的意义就在于我们无法预测未知流量,比如刚提到的抽奖可能遇到的:

  • 重复抽奖

  • 恶意脚本

其他一些场景:

  • 热点事件(微博)

  • 大量爬虫

这些情况都是无法预知的,不知道什么时候会有10倍甚至20倍的流量打进来,如果真碰上这种情况,扩容是根本来不及的(弹性扩容都是虚谈,一秒钟你给我扩一下试试)

明确了限流的意义,我们再来看看如何实现限流

防止用户重复抽奖

重复抽奖和恶意脚本可以归在一起,同时几十万的用户可能发出几百万的请求。

如果同一个用户在1分钟之内多次发送请求来进行抽奖,就认为是恶意重复抽奖或者是脚本在刷奖,这种流量是不应该再继续往下请求的,在负载均衡层给直接屏蔽掉。

可以通过nginx配置ip的访问频率,或者在在网关层结合sentinel配置限流策略。

用户的抽奖状态可以通过redis来存储,后面会说。

拦截无效流量

无论是抽奖还是秒杀,奖品和商品都是有限的,所以后面涌入的大量请求其实都是无用的。

举个例子,假设50万人抽奖,就准备了100台手机,那么50万请求瞬间涌入,其实前500个请求就把手机抢完了,后续的几十万请求就没必要让他再执行业务逻辑,直接暴力拦截返回抽奖结束就可以了。

同时前端在按钮置灰上也可以做一些文章。

那么思考一下如何才能知道奖品抽完了呢,也就是库存和订单之前的数据同步问题。

服务降级和服务熔断

有了以上措施就万无一失了吗,不可能的。所以再服务端还有降级和熔断机制。

在此简单做个补充,详细内容请持续关注作者。

有好多人容易混淆这两个概念,通过一个小例子让大家明白:

假设现在一条粉丝数突破100万,冲上微博热搜,粉丝甲和粉丝乙都打开微博观看,但甲看到了一条新闻发布会的内容,乙却看到”系统繁忙“,过了一会,乙也能看到内容了。

(请允许一条幻想一下😎)

在上述过程中,首先是热点时间造成大量请求,发生了服务熔断,为了保证整个系统可用,牺牲了部分用户乙,乙看到的”系统繁忙“就是服务降级(fallback),过了一会有恢复访问,这也是熔断器的一个特性(hystrix)

V3 同步状态

接着回到上一节的问题,如何同步抽奖状态?

这不得不提到redis,被广泛用于高并发系统的缓存数据库。

我们可以基于Redis来实现这种共享抽奖状态,它非常轻量级,很适合两个层次的系统的共享访问。

当然其实用ZooKeeper也是可以的,在负载均衡层可以基于zk客户端监听某个znode节点状态。一旦抽奖结束,抽奖服务更新zk状态,负载均衡层会感知到。

V4线程优化

对于线上环境,工作线程数量是一个至关重要的参数,需要根据自己的情况调节。

众所周知,对于进入Tomcat的每个请求,其实都会交给一个独立的工作线程来进行处理,那么Tomcat有多少线程,就决定了并发请求处理的能力。

但是这个线程数量是需要经过压测来进行判断的,因为每个线程都会处理一个请求,这个请求又需要访问数据库之类的外部系统,所以不是每个系统的参数都可以一样的,需要自己对系统进行压测。

但是给一个经验值的话,Tomcat的线程数量不宜过多。因为线程过多,普通服务器的CPU是扛不住的,反而会导致机器CPU负载过高,最终崩溃。

同时,Tomcat的线程数量也不宜太少,因为如果就100个线程,那么会导致无法充分利用Tomcat的线程资源和机器的CPU资源。

所以一般来说,Tomcat线程数量在200~500之间都是可以的,但是具体多少需要自己压测一下,不断的调节参数,看具体的CPU负载以及线程执行请求的一个效率。

在CPU负载尚可,以及请求执行性能正常的情况下,尽可能提高一些线程数量。

但是如果到一个临界值,发现机器负载过高,而且线程处理请求的速度开始下降,说明这台机扛不住这么多线程并发执行处理请求了,此时就不能继续上调线程数量了。

V5业务逻辑

抽奖逻辑怎么做?

好了,现在该研究一下怎么做抽奖了

在负载均衡那个层面,已经把比如50万流量中的48万都拦截掉了,但是可能还是会有2万流量进入抽奖服务。

因为抽奖活动都是临时服务,可以阿里云租一堆机器,也不是很贵,tomcat优化完了,服务器的问题也解决了,还剩啥呢?

Mysql,是的,你的Mysql能抗住2万的并发请求吗?

答案是很难,怎么办呢?

把Mysql给替换成redis,单机抗2万并发那是很轻松的一件事情。

而且redis的一种数据结构set很适合做抽奖,可以随机选择一个元素并剔除。

V6流量削峰

由上至下,还剩中奖通知部分没有优化。

思考这个问题:假设抽奖服务在2万请求中有1万请求抽中了奖品,那么势必会造成抽奖服务对礼品服务调用1万次。

那也要和抽奖服务同样处理吗?

其实并不用,因为发送通知不要求及时性,完全可以让一万个请求慢慢发送,这时就要用到消息中间件,进行限流削峰。

也就是说,抽奖服务把中奖信息发送到MQ,然后通知服务慢慢的从MQ中消费中奖消息,最终完成完礼品的发放,这也是我们会延迟一些收到中奖信息或者物流信息的原因。

假设两个通知服务实例每秒可以完成100个通知的发送,那么1万条消息也就是延迟100秒发放完毕罢了。

同样对MySQL的压力也会降低,那么数据库层面也是可以抗住的。

看一下最终结构图:

17.Linux


作为Java程序员,不会用Linux会让人笑掉大牙的。我们不必像运维兄弟一样精通,基本的命令还是要熟练掌握。

  • 理解一切皆文件

  • 文件操作命令

  • 权限管理命令

  • 网络命令

  • 系统磁盘命令

  • ……

18.Git


网上经常传出不会git被开除的新闻,所以还不学起来?

命令大全

200条Git命令大全

19.数据结构和算法


问一下各位什么是程序?——数据结构+算法。所以,学吧,刷题吧

排序算法

《八大排序》源码]( ),提取码:5ehp

准备

古语云:“兵马未动,粮草先行”。想跟着一条一块把「排序算法」弄明白的,建议先准备好以下代码模板。

📢 观看本教程需知道基本循环语法两数交换双指针等前置知识。

📚 建议先看完代码逐步分析后再尝试自己写。

  • 新建一个Java工程,本文全篇也基于Java语言实现代码。

  • 建立如下目录结构

  • MainTest测试类中编写测试模板。

/**

  • 测试类

  • Author:一条

  • Date:2021/09/23

*/

public class MainTest {

public static void main(String[] args) {

//待排序序列

int[] array={6,10,4,5,2,8};

//调用不同排序算法

// BubbleSort.sort(array);

// 创建有100000个随机数据的数组

int[] costArray=new int[100000];

for (int i = 0; i < 100000; i++) {

// 生成一个[0,100000) 的一个数

costArray[i] = (int) (Math.random() * 100000);

}

Date start = new Date();

//过长,先注释掉逐步打印

//BubbleSort.sort(costArray);

Date end = new Date();

System.out.println(“耗时:”+(end.getTime()-start.getTime())/1000+“s”);

}

}

该段代码内容主要有两个功能:

  • 调用不同的排序算法进行测试

  • 测试不同排序算法将10w个数排好序需要的时间。更加具象的理解时间复杂度的不同

1.冒泡排序

基本思想

通过对乱序序列从前向后遍历,依次比较相邻元素的值,若发现逆序则交换,使值较大的元素逐渐从前移向后部。

像水底下的气泡一样逐渐向上冒一样。

动图讲解

代码实现

不理解的小伙伴可以用debug模式逐步分析。

/**

  • 冒泡排序

  • Author:一条

  • Date:2021/09/23

*/

public class BubbleSort{

public static int[] sort(int[] array){

for (int i = 0; i < array.length; i++) {

for (int j = 0; j < array.length-1; j++) {

//依次比较,将最大的元素交换到最后

if (array[j]>array[j+1]){

// 用临时变量temp交换两个值

int temp=array[j];

array[j]=array[j+1];

array[j+1]=temp;

}

}

//输出每一步的排序结果

System.out.println(Arrays.toString(array));

}

return array;

}

}

输出结果

逐步分析

  1. 初始数组:[6,10,4,5,2,8]

  2. 6拿出来和后一个10比较,6<10,不用交换。- > j++;

  3. 10拿出来和后一个4比较,10>4,交换。- > [6,4,10,5,2,8]

  4. 依次执行j++与后一个比较交换

  5. 第一层i循环完,打印第一行- > [6, 4, 5, 2, 8, 10],此时最后一位10在正确位置上。 - > i++

  6. 4开始,继续比较交换,倒数第二位8回到正确位置。

  7. 如上循环下去 - > ……

  8. 最终结果 - > [2, 4, 5, 6, 8, 10]

这时再回去看动图理解。

耗时测试

记得先注释掉排序类逐步打印代码。

时间复杂度O(n^2)

算法优化

优化点一

外层第一次遍历完,最后一位已经是正确的,j就不需要再比较,所以结束条件应改为j-i-1;

优化点二

因为排序的过程中,各元素不断接近自己的位置,如果一趟比较下来没有进行过交换,就说明序列有序,因此要在排序过程中设置一个标志flag判断元素是否进行过交换。从而减少不必要的比较。

优化代码

public static int[] sortPlus(int[] array){

System.out.println(“优化冒泡排序开始----------”);

for (int i = 0; i < array.length; i++) {

boolean flag=false;

for (int j = 0; j < array.length-i-1; j++) {

if (array[j]>array[j+1]){

flag=true;

int temp=array[j];

array[j]=array[j+1];

array[j+1]=temp;

}

}

if (flag==false){

break;

}

// System.out.println(Arrays.toString(array));

}

return array;

}

优化测试

通过基础测试看到当序列已经排好序,即不发生交换后终止循环。

耗时测试由27s优化到17s

2.选择排序

基本思想

选择排序和冒泡排序很像,是从乱序序列的数据中,按指定的规则选出某一元素,再依规定交换位置后达到排序的目的。

动图讲解

代码实现

public class SelectSort {

public static int[] sort(int[] array) {

System.out.println(“选择排序开始----------”);

for (int i = 0; i < array.length; i++) {

//每个值只需与他后面的值进行比较,所以从开始

for (int j = i; j < array.length; j++) {

//注意此处是哪两个值比较

if (array[i]>array[j]){

int temp=array[i];

array[i]=array[j];

array[j]=temp;

}

}

System.out.println(Arrays.toString(array));

}

return array;

}

}

输出结果

逐步分析

  • 初始数组:[6,10,4,5,2,8]

  • 拿出610比较,不交换 - > j++

  • 62比较,交换 - > j++

  • 注意此时是拿2继续比较,都不交换,确定第一位(最小的数)为2 - > i++

  • 循环下去,依次找到第一小,第二小,……的数

  • 最终结果 - > [2, 4, 5, 6, 8, 10]

这时再回去看动图理解。

耗时测试

时间复杂度:O(n^2)

算法优化

上诉代码中使用交换的方式找到较小值,还可以通过移动的方式,即全部比较完只交换一次。

这种对空间的占有率会有些增益,但对时间的增益几乎没有,可忽略,亦不再演示。

3.插入排序

基本思想

把n个乱序的元素看成为一个有序表和一个无序表,开始时有序表中只包含一个元素,无序表中包含有n-1个元素,排序过程中通过不断往有序表插入元素,获取一个局部正确解,逐渐扩大有序序列的长度,直到完成排序。

动图讲解

2021-09-25 19.20.05

代码实现

/**

  • 插入排序

  • Author:一条

  • Date:2021/09/23

*/

public class InsertSort {

public static void sort(int[] array) {

for (int i = 1; i < array.length; i++) {

//插入有序序列,且将有序序列扩大

for (int j = i; j > 0; j–) {

if (array[j]>array[j-1]){

int temp=array[j];

array[j]=array[j-1];

array[j-1]=temp;

}

}

// System.out.println(Arrays.toString(array));

}

}

}

输出结果

耗时测试

算法优化

见下方希尔排序,就是希尔对插入排序的优化。

4.希尔排序

希尔排序是插入排序的一个优化,思考往[2,3,4,5,6]中插入1,需要将所有元素的位置都移动一遍,也就是说在某些极端情况下效率不高,也称该算法不稳定

希尔排序是插入排序经过改进之后的一个更高效的版本,也称为缩小增量排序

基本思想

希尔排序是把记录按下标的一定增量分组,对每组使用插入排序;

随着增量逐渐减少,每组包含的关键词越来越多,当增量减至1时,整个序列恰被分成一组,算法便终止。

和插入排序一样,从局部到全部,希尔排序是局部再局部。

动图讲解

图片源于网络

代码实现

/**

  • 希尔排序

  • Author:一条

  • Date:2021/09/23

*/

public class ShellSort {

public static void sort(int[] array) {

System.out.println(“希尔排序开始--------”);

//gap初始增量=length/2 逐渐缩小:gap/2

for (int gap = array.length/2; gap > 0 ; gap/=2) {

//插入排序 交换法

for (int i = gap; i < array.length ; i++) {

int j = i;

while(j-gap>=0 && array[j]<array[j-gap]){

//插入排序采用交换法

int temp = array[j];

array[j]=array[j-gap];

array[j-gap]=temp;

j-=gap;

}

}

System.out.println(Arrays.toString(array));

}

}

}

输出结果

耗时测试

算法优化

5.快速排序

快速排序(Quicksort)是对冒泡排序的一种改进,相比冒泡排序,每次的交换都是跳跃式的。

基本思想

将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。

体现出分治的思想。

动图讲解

代码实现

思路如下:

  • 首先在这个序列中找一个数作为基准数,为了方便可以取第一个数。
  • 遍历数组,将小于基准数的放置于基准数左边,大于基准数的放置于基准数右边。此处可用双指针实现。
  • 此时基准值把数组分为了两半,基准值算是已归位(找到排序后的位置)
  • 利用递归算法,对分治后的子数组进行排序。

public class QuickSort {

public static void sort(int[] array) {

System.out.println(“快速排序开始---------”);

mainSort(array, 0, array.length - 1);

}

private static void mainSort(int[] array, int left, int right) {

if(left > right) {

return;

}

//双指针

int i=left;

int j=right;

//base就是基准数

int base = array[left];

//左边小于基准,右边大于基准

while (i<j) {

//先看右边,依次往左递减

while (base<=array[j]&&i<j) {

j–;

}

//再看左边,依次往右递增

while (base>=array[i]&&i<j) {

i++;

}

//交换

int temp = array[j];

array[j] = array[i];

array[i] = temp;

}

//最后将基准为与i和j相等位置的数字交换

array[left] = array[i];

array[i] = base;

System.out.println(Arrays.toString(array));

//递归调用左半数组

mainSort(array, left, j-1);

//递归调用右半数组

mainSort(array, j+1, right);

}

}

输出结果

逐步分析

  • 6作为基准数,利用左右指针使左边的数<6,右边的数>6

  • 对左右两边递归,即左边用5作为基准数继续比较。

  • 直到left > right结束递归。

耗时测试

算法优化

优化一

三数取中(median-of-three):我们目前是拿第一个数作为基准数,对于部分有序序列,会浪费循环,可以用三数取中法优化,感性的小伙伴可自行了解。

优化二

快速排序对于长序列非常快,但对于短序列不如插入排序。可以综合使用。

完整文章

leetcode刷题

最后

由于篇幅有限,这里就不一一罗列了,20道常见面试题(含答案)+21条MySQL性能调优经验小编已整理成Word文档或PDF文档

MySQL全家桶笔记

还有更多面试复习笔记分享如下

Java架构专题面试复习

本文已被CODING开源项目:【一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码】收录

需要这份系统化的资料的朋友,可以点击这里获取

array[j-gap]=temp;

j-=gap;

}

}

System.out.println(Arrays.toString(array));

}

}

}

输出结果

耗时测试

算法优化

5.快速排序

快速排序(Quicksort)是对冒泡排序的一种改进,相比冒泡排序,每次的交换都是跳跃式的。

基本思想

将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。

体现出分治的思想。

动图讲解

代码实现

思路如下:

  • 首先在这个序列中找一个数作为基准数,为了方便可以取第一个数。
  • 遍历数组,将小于基准数的放置于基准数左边,大于基准数的放置于基准数右边。此处可用双指针实现。
  • 此时基准值把数组分为了两半,基准值算是已归位(找到排序后的位置)
  • 利用递归算法,对分治后的子数组进行排序。

public class QuickSort {

public static void sort(int[] array) {

System.out.println(“快速排序开始---------”);

mainSort(array, 0, array.length - 1);

}

private static void mainSort(int[] array, int left, int right) {

if(left > right) {

return;

}

//双指针

int i=left;

int j=right;

//base就是基准数

int base = array[left];

//左边小于基准,右边大于基准

while (i<j) {

//先看右边,依次往左递减

while (base<=array[j]&&i<j) {

j–;

}

//再看左边,依次往右递增

while (base>=array[i]&&i<j) {

i++;

}

//交换

int temp = array[j];

array[j] = array[i];

array[i] = temp;

}

//最后将基准为与i和j相等位置的数字交换

array[left] = array[i];

array[i] = base;

System.out.println(Arrays.toString(array));

//递归调用左半数组

mainSort(array, left, j-1);

//递归调用右半数组

mainSort(array, j+1, right);

}

}

输出结果

逐步分析

  • 6作为基准数,利用左右指针使左边的数<6,右边的数>6

  • 对左右两边递归,即左边用5作为基准数继续比较。

  • 直到left > right结束递归。

耗时测试

算法优化

优化一

三数取中(median-of-three):我们目前是拿第一个数作为基准数,对于部分有序序列,会浪费循环,可以用三数取中法优化,感性的小伙伴可自行了解。

优化二

快速排序对于长序列非常快,但对于短序列不如插入排序。可以综合使用。

完整文章

leetcode刷题

最后

由于篇幅有限,这里就不一一罗列了,20道常见面试题(含答案)+21条MySQL性能调优经验小编已整理成Word文档或PDF文档

[外链图片转存中…(img-ghqm0ksP-1715693186928)]

还有更多面试复习笔记分享如下

[外链图片转存中…(img-7U3AZhcV-1715693186928)]

本文已被CODING开源项目:【一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码】收录

需要这份系统化的资料的朋友,可以点击这里获取

  • 14
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值