有8种基本数据类型,分别是:
按流划分:有输入流和输出流。
按单位划分:有字节流和字符流。
字节流:inputstream,outputstream。
子父类:reader、writer。
反射是Java的一种机制,可以让我们在运行时获取类的信息
通过反射我们可以获取到类的所有信息,比如它的属性、构造器、方法、注解等
序列化就是一种用来处理对象流的机制,将对象内容流化,将流化后的对象传输于网络之间。
序列化是将对象转换为容易传输的格式的过程。
例如,可以序列化一个对象,然后通过HTTP通过Internet在客户端和服务器之间传输该对象。在另一端,反序列化将从流中构造成对象。
不能,因为String类有final修饰符,是最终类,所以是不能被继承的。
stirng a = ‘123’ 会将其分配到常量池中,常量池中没有重复的元素,如果常量池中有相同的元素,就将其地址赋值给变量,如果没有就创建一个再赋值个变量。
string a = new string(‘123’),会将对象分配到堆中,即内存中一样,还是会创建一个新的对象。
string是字符串常量,是不可变的字符序列。string创建的字符在常量池中,对于string类的任何改变都会返回一个新的string对象。
stringbuffer、stringbuilder是字符串变量,是可变的字符序列。在堆中创建对象,对这两种中的内容修改都是返回当前对象。
stringbuilder是非线程安全的,stringbuffer是线程安全的,因为stringbuffer对于方法加了同步锁,而stringbuilder没有。
操作少量数据:适用String。
单线程操作字符串缓冲区下大量数据:适用stringbuilder。
多线程操作字符串缓冲区下大量数据:适用stringbuffer。
数据结构不同:ArrayList是动态数组、LinkedList是链表。
效率不同:
自由性不同:
ArrayList需要手动指定固定大小容量,会预留一定空间,使用方便,只需要创建然后添加数据、通过下标进行使用。而LinkedList能够动态的随着数据量变化而变化,只需要存储节点信息和指针。
红黑树主要是为了解决链表过长导致的查询速度慢的问题,当链表长度大于等于8时,就会转变为红黑树,当链表长度小于等于6时,由红黑树转变回链表,因为链表过短时引入红黑树反而会降低查询速度。
链表主要是为了解决数组中key发生hash碰撞,将发生碰撞的key存到链表中。
当两个不同的输入值,通过计算后得到相同散列值的现象,称为哈希碰撞。
hashMap中有个参数叫负荷因子,其实就是一个小数值0.75,也可以理解成75%。
hashMap默认大小是16,当填满了75%的空间大小时就该扩容了。
16乘以0.75,得到12,那就说明当集合中有12个元素的时候就进行扩容,扩容成原来的2倍。
判断键值对数组是否为空,或者length是否为0,如果是则调用 resize 方法进行扩容。
根据键值对key计算hash值得到插入的数组索引。
判断索引是否为null,为null就直接新建节点进行添加,不为null则判断key是否一样,一样就直接覆盖。
判断索引是否为treenode,即判断是否为红黑树,如果是红黑树,直接在树中插入键值对。
如果不是treenode,则遍历链表,判断链表长度是否大于8,如果大于8就转为红黑树,在树中执行插入,如果不大于8就在链表中执行插入,在遍历的过程中判断key是否存在,存在就直接覆盖value值。
插入成功后,判断实际存储的键值对数量size是否超过了最大容量,如果超过了,就执行resize扩容。
spring是一个轻量级的IOC和AOP容器框架,是为JavaWeb程序提供基础服务的一套框架,目的是用于简化企业应用程序的开发,它使得开发者只需关心业务需求。
控制反转,将对象的控制权转移给spring框架,由spring来负责控制对象的生命周期和对象之间的依赖关系。
直观来说就是:以前创建对象的时机和主动权都是由自己把控的,如果在一个对象中使用另外一个对象,就必须通过new指令去创建依赖对象,使用完后还需要销毁,对象就会和其他接口或类耦合起来,而IOC则是由专门的容器来帮忙创建对象,将所有的类都在spring容器中登记,当需要某个对象时,把你想要的对象主动给你。也就是说,对于某个具体的对象而言,以前是由自己控制它所引用的对象生命周期,而在IOC容器中,所有的对象都被spring控制,控制对象生命周期的不再是引用它的对象,而是spring容器,创建对象、注入依赖对象、都交由spring容器。而引用对象只是被动性的接受依赖对象,这就叫控制反转。
面向切面编程,使用AOP可以抽取一些公共模块出来,减少公共模块和业务代码的耦合。利用AOP可以对业务逻辑的各个部分进行隔离。
例如常用的日志功能,我们在调用每一个接口的时候说会去做记录日志的动作,那么使用AOP技术就可将记录日志的这个操作抽取出来做一个公共的模块,从而减少跟业务代码的耦合。
spring aop 是通过代理来实现面向切面编程的,它使用的代理有两种,JDK代理和CGlib代理。当我们的bean对象实现某一接口后,spring默认采用JDK动态代理,当我们bean对象没有实现接口时,默认采用CGlib代理。
实例化 --> 属性赋值 --> 初始化 --> 销毁
循环依赖其实就是循环引用,也就是两个或者两个以上的bean相互持有对方、最终形成闭环。
spring 解决循环依赖问题是通过三级缓存来实现的 ,3级缓存里边1级是存放已经实例化好并且进行依赖注入的bean对象,2级缓存里面是已经实例化好但是还没有进行依赖注入的bean对象,3级缓存主要用来存放bean工厂,主要用来生成原始bean对象,并且放到2级缓存里,3级缓存核心思想就是把bean的实例化和依赖注入分离,通过2级缓存存放的不完整的bean实例作为突破口解决循环依赖问题。
在spring中,使用AutoWtire来配置自动装载模式,对象无需自己查找或者创建与其关联的其他对象,由容器负责把需要相互协作的对象引用赋予各个对象。
使用 @Autowired、@Resource 注解来自动装配指定的bean。
在启动 spring ioc 时,当扫描到 @Autowired、@Resource 就会在ioc容器自动查找需要的bean,并装配给该对象属性。
在使用 @Autowired 时,首先会在容器中查询对应类型的bean,如果查询刚好为1个,就将该bean装配给 @Autowired 指定的数据;如果查询结果不止一个,那么就 @Autowired 会根据名称来查找。如果查找结果为空,就会抛出异常。可以使用 required = false 来解决。
Autowired 默认是按照类型装配注入的,默认情况下它要求依赖对象必须存在(required = true)。
Resource 默认是按照名称来装配注入的,只有当找不到名称匹配的bean才会按照类型来装配注入。
Spring MVC 是一个基于 Java 的实现MVC设计模型的轻量级Web框架,属于Spring 框架的一个模块。
MVC 就是 Model(模型)、View(视图)、controller(控制器)的缩写。
model 模型是指模型表示业务的规则(业务逻辑层)。
view 视图是指用户看到的、和用户交互的界面(视图层)。
controller 控制器是接收用户输入然后调用模型和视图完成用户的需求(控制器层)
用来简化 spring 应用的初始化搭建以及开发过程,使用特定方式来进行配置,比如 properties 或 yml 文件,创建独立的spring引用程序。
嵌入 tomcat 无需安装部署。
简化 maven 配置。
spring boot 来简化 spring 应用开发,约定大于配置,去繁从简。
spring 最重要的特征就是依赖注入。所有的 springModules 不是依赖注入就是 ioc 反转。
spring mvc 提供了一种分离式的方法来开发web应用,通过运用想 DispatcherServelet、MoudlAndView 等一些简单的概念,开发web 应用将会变得非常简单。
spring boot 就是 spring 和 spring 的结合体,因为 spring 和 spring 的问题在于需要大量的配置参数。spring boot 通过一个自动配置和启动项目来解决这个问题。
spring boot stater 可以放在自己的程序中,你可以一站式获取你所需要的 spring 和相关技术,而不需要依赖示例代码或者搜索和复制粘贴的方式的负载。
例如如果我们想使用 jpa 访问数据库,那么我们只需要项目包含 spring boot stater data jpa 依赖,就可以完美进行。
在 spring 程序 main 方法中,添加 @springbootApplication 或者 enableAutoConfigtion,就会自动去maven中读取每个 stater 中的 spring.factories 文件,该文件里面配置了所有需要被创建 spring 容器中的bean。
可以不需要,内置了 tomcat 容器。
微服务架构就是将单体应用程序分成多个应用程序,这多个应用程序就成为了微服务,每个微服务运行在自己的进程中,并使用轻量级的机制通信,这些服务围绕着业务来划分,并通过自动化部署机制来独立部署,这些服务可以使用不同的编程语言,不同的数据库,保证最低限度集中式管理。
spring cloud 是一系列框架的集合,它利用 spring boot 的开发便利性巧妙简化了分布式系统基础设施开发,如服务发现注册、配置中心、路由、数据监控等等。
等等
当我们开发一个项目时,我们通常在属性文件中进行所有的配置,随着越来越多的服务开发和部署,添加和修改的这些属性变得更加复杂,有些服务可能会下降,某些位置可能会发生变化,手动更改这些属性可能会产生问题。这时候我们就需要用到一些服务发现组件,所有服务在服务发现组件上注册,因此我们无需处理服务任何更改和处理。
eureka 和 nacos 都支持服务发现、拉取。
网关就相当于一个网络服务架构的入口,所有网络请求都必须经过网关转发到具体服务。
假设说现在由100件事情要做,如果都给一个人做,那么这个人就很累,但是如果均匀的分配给更多人做,那么每个人就会轻松很多。
ribbon 是负载均衡算法,当服务出现超时、重试等,简单来说就是在配置文件中列出来的所有服务,ribbon 会自动的帮助你基于某种规则(如简单的轮询)去连接这些服务,我们也可以很容易使用ribbon实现自定义负载均衡算法。
ribbon 会从注册中心读取目标服务器信息,对于同一请求计数,使用取余算法获得目标服务集群索引,返回获取到的目标服务信息。
当一个服务调用另一个服务时由于网络原因或自身出现的问题,调用者就会等待被调用者的响应,当更多的服务请求到这些资源导致更多的请求等待,放生雪崩效应。
断路器由三种状态:
在分布式系统中,我们一定会依赖各种服务,那么这些服务一定会出现失败的情况,就会导致雪崩,Hystrix就是这样一个工具,防雪崩利器,它具有服务降级,服务熔断,服务隔离、监控等一些防止雪崩的技术。
当某个服务发生宕机时,调用这个服务的其他服务也会发生宕机,大型项目中的微服务之间是互通的,这样就会将服务的不可用扩大到各个其他服务中,从而使整个项目的服务宕机崩溃。
一般使用 Hystrix 框架,实现服务隔离来避免出现服务雪崩效应,从而达到保护服务的效果,当然在微服务中,高并发的数据库访问量导致访问线程阻塞,从而导致单个访问宕机,服务的不可用会蔓延到其他服务,引起整体服务雪崩,使用服务降级能有效为不同的服务分配资源,一旦服务不可用则返回友好提示,不占用其他服务资源,从而避免单个服务崩溃引发整体服务的不可用。
服务降级:当客户端请求服务端的时候,防止客户端一直等待,不会处理业务逻辑代码,直接返回友好提示。
服务熔断是在服务降级的基础上,更直接的一种保护方式,当在一个统计时间范围内的请求失败数量到达设定值,或者当期的请求错误频率到达设定的错误阈值时,会开启熔断,之后的请求直接走 fallback 方法,在设定时间后尝试恢复。
服务隔离就是为隔离的服务开启一个独立的线程池,这样在高并发的情况下不会影响其他服务,服务隔离有线程池和信号量两种方式。一般使用线程池。
feign 是一个声明式 web 客户端,使用它我们编写 web 服务客户端更加容易,只需将我们需要调用的服务方法定义成抽象方法保存在本地就可以了,不需要自己构建http请求了,直接调用接口就可以。
Feign
RestTemplate
Ribbon
Ribbon 需要我们自己构建 http 请求,模拟 http 请求然后通过 restTemplate 发送给其服务,步骤相对繁琐。
而 feign 则是在 ribbon 的基础上进行了一次改进,采用接口的形式将我们需要调用的服务方法定义成抽象方法保存在本地就可以了,不需要自己构建http请求。直接调用接口就可以了。
查询缓存功能是在连接器之后发生的,优点是效率高,如果已经有缓存则直接返回结果。
查询缓存的缺点是失效太繁琐,导致命中率较低,任何更新表操作都会清空查询缓存,因此导致查询缓存非常容易失效。
InnoDB、MylSAM、Memory 等,mysql5 版本后,InnoDB成为了默认存储引擎。
InnoDB 和 MylSAM 最大的区别就是 InnoDB 支持事务,而 mylsam 不支持。
innodb 支持崩溃后安全恢复、mylsam 不支持。
innodb 支持行级锁、mylsam 不支持,只支持表锁。
innodb 支持外键、mylsam 不支持。
mylsam 比 innodb 性能高。
普通索引查询到主键索引后,回到主键索引树搜索的过程,我们称为回表查询。
因为 B+ 树相对于 B 树而言 B+ 树的层级更少,搜索效率更高,因为 B 树无论是叶子节点还是非叶子节点都会存储数据,这样会导致一页中的存储键值减少,指针也跟着减少,要同样保存大量的数据,只能增加树的高度,从而导致性能降低。
因为 hash 索引不支持范围查询和排序,而 B+ 可以。
针对数据量较大、且查询比较频繁的表建立索引。
针对于常作为查询条件、排序、分组操作的字段建立索引。
尽量选择区分度高的字段作为索引。
尽量建立唯一索引。
尽量使用联合索引,减少单列索引,查询时,联合索引很多时候可以覆盖索引,节省存储空间,避免回表查询,提高效率。
要控制索引的数量,索引不是越多越好,索引越多,维护索引的结构代价就越大,会影响增删改操作的效率。
按照逻辑划分:
按照数据结构划分:
最左前缀原则是Mysql中一种重要的原则,指的是索引以最左边为起点任何连续的索引都能匹配上,当遇到范围查询(>,<,like)就会停止匹配。比如复复合索引(a,b,c)。
where a=1 只使用了索引 a;
where a=1 and b=2 只使用了索引 a,b;
where a=1 and b=2 and c=3 使用 a,b,c;
where b=1 or where c=1 不使用索引;
where a=1 and c=3 只使用了索引 a;
where a=3 and b like ‘xx%’ and c=3 只使用了索引 a,b。
唯一索引和普通索引的性能对比分为两种情况:
满足业务需求的情况下,尽量降低主键长度。
插入数据时,尽量选择顺序插入,选择使用自增主键。
尽量不要使用uuid做主键或者是其他自然主键、比如身份证。
业务操作时,避免对主键的修改。
会,不设置主键InnoDB默认会生成一个rowid作为主键。
left join(左连接、左外连接):返回左表中的所有记录和右表中连接字段相等的记录。
right join(右连接、右外连接):返回右表中所有记录和左表中连接字段相等的记录。
inner join(内连接、等值连接):只返回两个表中连接字段相等的记录。
full join(全外连接):返回两个表中所有的记录和左右表中连接字段相等的记录。
事务是一系列的数据库操作,要么全部成功,要么全部失败。
有四种隔离级别:
两种常见的方式:
全局锁:就是给整个数据库加锁,加锁后就处于只读的状态,后续的更新操作都会被阻塞。
表记锁:就是给整张表加锁,锁的粒度比较大。
行级锁:就是给整行数据加锁,锁的粒度小。
只有通过索引条件检索数据,InnoDB 才会使用行锁,否则 InnoDB 将使用表锁, 使用 for update 来实现行锁。
QPS:每秒查询次数,数据库每秒能够处理的查询次数。
TPS:每秒处理事务次数。
通过 show status 查询当前数据库的状态信息。
错误日志:记录运行过程中的错误信息,比如无法加载数据库文件,权限不正确都会记录在这里,默认情况下是开启的。
查询日志:记录运行过程中的所有命令(不仅仅是select)。默认情况下是关闭的。
慢查询日志:记录查询超过指定时间的语句,默认情况下是关闭的。
回滚日志(undo log):记录日志被修改前的值,从而保证如果修改出现异常,可以使用 undo log 来实现回滚。
二进制日志(big log):是一个二进制文件,记录所有数据库表结构变更以及表修改的操作。
使用 Mysql 自带功能,开启慢查询日志,在Mysql的安装目录下找到 my.cnf 文件配置 slow-quey-log-on 开启慢查询,慢查询默认时长为10s,默认存储文件名 host_name_slow.log
使用 mysql 中的 explain 分析执行语句:
explain select * from where id = 5
主要关注的就是 type 字段:
等等,其他的不了解
两种:
mysql 多实例就是在同一台机器上启动多个 mysql 程序,然后它们去监听不同的端口,运行多个服务进程,它们相互独立,互不影响的对外提供服务。
数据库分片是将一个大型的数据库分割成多个部分,分别存储在多个独立的服务器节点上,从而实现数据库存储的水平扩展,这种方式能够解决单一数据库的容量限制、处理能力限制、备份恢复限制等问题。
有两种,客户端代理方式、中间件代理方式:
常见的优化方案:
读写分离:主库负责写,从库负责读。
垂直区分:根据数据的属性,单独拆分表,甚至拆分成库。
水平区分:保持表结构不变,根据策略存储数据分片,这样每一片数据被分散到不同的表或者库中,水平拆分只是为了解决单张表数据量过大的问题。
redis 是一种 noSQL 也就是 key-value 类型的内存数据库,整个数据库全部加载在内存中进行操作。
优点:
缺点:
String:字符串,一个 key 对应一个 value 字符串。
list:列表,一个key 对应一个 value 列表。
set:集合,一个key 对应一个没有重复元素的 set 集合。
zset:有序集合,一个key 对应一个没有重复元素的有序 set 集合。
hash:哈希,一个 key 对应多个键值对。
虽然 redis 很块,但是它也有一些局限性,不能完全替代主数据库。
虽然 redis 非常快,但是它还有一些限制,不能完全替代主数据库,所以,使用 redis 作为缓存是一种很好方式,可用提高应用程序的性能。
缓存热点数据,缓解数据库压力。
利用 redis 原子性的自增操作,可用实现计数的功能,比如统计用户点赞、用户访问计数等。
分布式锁,在分布式的场景下,无法实现单机环境下的锁来对多个节点上的进程同步。可用使用 redis 自带的 setnx 命令来实现分布式锁,除此之外,还可以使用官方提供的 redlock 分布式锁实现。
简单的消息队列,可用使用 redis 自身的发布/订阅模式来实现简单的消息队列。
缓存穿透就是查询一个不存在的数据,然后数据库查不到也不会直接写入缓存,就会导致每次请求都查数据库。
缓存空数据,按照某一个查询条件查询到的数据为空时,将这个结果存到redis中,这样下次查询进到redis就能查到该数据了。
但是会出现数据不一致的情况,假如数据库中这个条件的数据更新了,redis中的数据可能没有进行更新,那么就需要加一步更新到redis的操作。
缓存雪崩就是在同一时间段内,redis中的大量的key同时失效了,或者服务器宕机了,导致大量请求打到数据库,给数据库带来巨大压力。
给不同的key设置不同的过期时间,预防同时失效。
利用 redis 集群提高服务的高可用性,预防服务器宕机。
给缓存业务添加降级限流策略。
缓存击穿也叫热点key,就是一个高并发服务并且缓存重建业务较复杂的key,突然失效了,无数的请求访问会在瞬间给数据库带来冲击。
双写一致性就是当修改了数据库的数据后,也要更新到缓存,缓存中的数据要和数据库中的保持一致。
确保双写一致性有两种场景,一种是强一致性,一种是延时一致性,具体看业务需求来定。
当 redis 中的内存不够用时,此时再向 redis 中添加新的数据,那么 redis 就会按照某一种规则将内存中的数据删除掉,这种数据的删除规则被成为内存淘汰策略。
redis 支持 8 种 不同的策略来选择要删除的 key。
redis 的线程模型是:单线程、多路复用、异步非阻塞。采用这个模型的原因是希望通过单线程来避免切换,锁竞争等带来的性能问题。
redis 单线程指的是:接收客户端请求、解析请求、进行数据读写操作、发送数据给客户端的这个过程是由一个线程来完成的。
但是 Redis 程序并不是单线程的,Redis 在启动的时候,是会启动有后台线程的。
关闭文件、AOF刷盘、释放内存,这些任务需要创建独立的线程来处理,因为这些任务的操作都是很耗时的,如果把这些任务都放在主线程来处理,那么 redis 主线程就很容易发生阻塞,这样就无法处理后面的请求了。
后台线程就相当于一个消费者,生产者把耗时的任务丢到队列中去,消费者不停的轮询这个队列,拿出任务去执行对应的方法即可。
redis 的大部分操作都是在内存中完成的,并且采用了高效的数据结构,因此 redis 瓶颈可能是机器内存或者网络带宽,并非 cpu、既然 cpu 不是瓶颈,那么自然就采用单线程的解决方案了。
redis 采用单线程模型可以避免了多线程之间的竞争,省去了多线程来回切换带来的时间和性能上的开销,而且也不会出现死锁的问题。
redis 采用了 IO 多路复用机制处理大量客户端 socket 请求,IO 多路复用机制是指一个线程处理多个 IO 流,简单来说,在 redis 只运行单线程的情况下,该机制允许内核中,同时存在多个监听 socket 和已连接 socket。内核会一直监听这些 socket 上的连接请求或者数据请求。一旦有请求到达,就会交给 redis 线程处理,这就实现了一个 redis 线程处理多个 IO 流的效果。
可以使用 redisson 实现分布式锁,底层是 setnx 和 lua 脚本,setnx 是 set if not exists 如果不存在则set 的意思,lua 脚本的作用是能够调用 redis 命令,保证多条命令执行的原子性。
单节点的 redis 并发能力是有上限的,要进一步提高 redis 的并发能力,就需要搭建主从集群,实现读写分离,一般是一主多从,主节点负责写,从节点负责读。
首先可以搭建主从集群,再加上 redis 中的哨兵模式,哨兵模式可以实现主从集群的自动故障恢复,里面包含了对主从服务的监控、自动故障恢复、通知;如果 master 故障,sentinel 会将一个 slave 提升为 master。当故障实例恢复后也以新的 master 为主,同时 sentinel 也充当 redis 客户端的服务发现来源,当集群发生故障转移时,会将最新的信息推送给 redis 客户端,所以一般项目都会采用哨兵模式来保证redis的高并发高可用。
有时候由于网络等原因可能会出现脑裂的情况,就是说,由于 redis 主节点和从节点和 sentinel 处于不同的网络分区,使得 sentinel 没有能够心跳感知到主节点,所以通过选举的方式提升了一个从节点为主节点,这样就出现了两个主节点,就像大脑分裂了一样,这样就会导致客户端在旧的主节点写入数据,新的节点就无法同步数据,当网络恢复后,sentinel 会将旧的主节点降为从节点,这时再从主节点中同步数据,就会导致旧的主节点中的大量数据丢失。
解决的话,我记得 redis 中可以设置:我们可以设置最少的从节点的个数,比如设置至少有一个从节点才能同步数据,还有就是可以设置主从数据同步的延迟时间,达不到要求就拒绝,就可以避免大量的数据丢失。
rabbitMQ 是一个开源的 AMQP 高级消息队列协议的实现软件,服务端用 Erlang 语言编写,用于在分布式系统中存储和转发消息、有易用性、扩展性、高可用性。
主要特征就是面向消息、队列、路由(包括对点的发布订阅)、可靠性、安全。
消息中间件主要用于组件之间的解耦,消息的生产者无需知道消息的消费者的存在,反过来消费者也无需知道生产者的存在。
由于 TCP 连接的创建和销毁开销很大,且并发数受系统资源限制,会造成性能瓶颈。
rabbtiMQ 使用通信的方式来传输数据,信道是建立在真实的TCP连接内的虚拟连接,且每条TCP连接上的信道数量没有限制。
如果队列中至少有一个消费者订阅,消息以循环的方式发送给消费者,每条消息只会发给一个订阅的消费者。通过路由可以实现多消费的功能。
消息发布到交换器时,消息将拥有一个路由键,在消息创建时设定。通过队列路由键,可以把队列绑定到交换器上。
消息到达交换器后,rabbitMQ 会将消息的路由键与队列的路由键进行匹配。
消息持久化,当然前提是队列必须持久化,rabbitMQ 确保持久性消息能从服务器重启中恢复的方式是,将他们写入磁盘上的一个持久化日志文件,当发布一条持久化消息到交换器上时,rabbitMQ 会在消息提交到日志文件后才发送响应。一旦消息消费者从持久化队列中消费了一条持久化消息,rabbitMQ 会在持久化日志中把这条消息标记为等待垃圾收集。
如果持久化消息在被消费之前,rabbitMQ 重启了,那么 rabbitMQ 会自动创建交换器和队列,并重新发布持久化日志文件中的消息到合适的队列。
可以使用发送方确认模式:将信道设置成发送方确认模式,则所有在信道上发布的消息都会被指派一个唯一的 ID。一旦消息被投放到目的队列后,或者消息被写入磁盘后,信道会发送一个确认给生产者。
如果 rabbitMQ 发生内部错误从而导致消息丢失,会发送一条未确认消息。
发送者确认模式是异步的,生产者在等待确认的同时,可以继续发送消息。当确认消息到达生产者,生产者的回调方法会被触发来处理确认消息。
可以使用接收方确认机制:消费者接受每一条消息后都必须确认,消息接收和消息确认是两个不同的操作。只有消费者确认了消息,rabbitMQ 才能安全的把消息从队列中删除。
在消息产生时,MQ 内部针对每条生产者发送的消息生成一个 inner-msg-id 作为去重的依据,避免重复消费的消息进入队列,在消费时,要求消息体中必须有一个 bizid(对于同一业务全局唯一,如订单id,支付id)作为去重依据,避免同一条消息被重复消费。
镜像集群模式:创建的队列,无论是元数据还是队列里面的消息都会存于多个 实例上,然后每次你写消息到队列的时候,都会自动把消息发送到多个实例的队列里面进行消息同步。
好处在于,任何一个集群宕机了,别的机器都可以用,坏处就是:新能开销大,消息同步到所有集群,导致网络带宽压力和消耗很重。
rabbitMQ 消息中间件主要用于组件之间的解耦,消息的生产者无需知道消息的消费者的存在,反过来消费者也无需知道生产者的存在。
JDK 是整个 Java 的核心,包括了 Java 运行环境 JRE,Java 工具和Java 基础类库。
JRE 是运行 Java 程序必须的环境,它包含了 JVM 标准实现及 Java 核心类库。
JVM 是整个 Java 实现跨平台的最核心的部分,能够运行 Java 语言编写的程序。
不同的虚拟机的运行时数据区可能略微不同,但是都会遵从Java虚拟机规范,Java 虚拟机规范规定的区域有个部分:
队列和栈都是用来存储数据的
队列允许先进先出去检索元素
栈是先进后出去检索元素
首先 Java 源文件,也就是 xxx.java 会先经过编译器进行编译,生成 class 文件,在这个过程当中会涉及到一系列的词法、语法、语义分析、字节码生成器等操作。
字节码文件生成后,也就是 xxx.class 会通过类加载子系统加载进入内存当中,也就是运行时数据区、在这个过程当中会涉及到字节码校验、翻译字节码、解释和执行或者编译执行、也就是执行引擎那部分。
执行引擎是非常重要的一部分,因为操作系统本身是不识别字节码指令的,只能够识别机器指令,那么字节码指令翻译成机器指令的过程就是依靠执行引擎来完成的。
JVM 支持两种类型的类加载器,分别是引导类加载器(Bootstrap class loader)和扩展类加载器(extension class loader)。
如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类去加载器去执行。
如果父类加载器还存在其父类加载器,则会进一步向上委托,依次递归,请求最终到达顶层的引导类加载器。
如果父类加载器可以完成类加载任务,就成功返回,无法完成类加载任务的情况下,子加载器才会尝试自己去加载,这就是双亲委派机制。
假设说我们自定义了一个 String 类并且执行该类的 main 方法,但是在加载自定义 String 类的时候会率先使用引导类加载器加载,而引导类加载器在加载的过程中会先加载 JDK 自带的文件,java.lang.String,就会报错说没有找到main,因为 java.lang.String 没有main 方法,这样就保证了对 Java核心的源代码的保护,这就是沙箱机制。
因为 CPU 需要不断的切换各个线程,这时候切换回来以后,就得知道接着从哪开始继续执行。
为了能够保证准确的记录各个线程正在执行的当前字节码指令地址,最好的办法自然就是给每个线程都分配一个PC寄存器,这样以来各个线程便可以进行独立计算,从而不会出现相互干扰的情况。
Java 虚拟机规范允许 Java 栈的大小是动态或者固定不变的。
如果采用固定大小的 Java 虚拟机栈,那每一个线程的 Java 虚拟机栈容量可以在线程创建的时候独立选定,如果线程请求分配的栈容量超过 Java 虚拟机允许的最大容量,Java 虚拟机将会抛出一个 stackOverFlowError 异常。
如果 Java 虚拟机栈可以动态扩展,并且在尝试扩展的时候无法申请到足够的内存,或者在创建新的线程时没有足够的内存去创建对应的虚拟机栈,那么 Java 虚拟机将会抛出一个 outOfMemoryError 异常。
每个线程都有自己的栈,栈中的数据都是以栈帧的格式存储的。
在这个线程上执行的每个方法都有各自对应的一个栈帧。
一共有四种垃圾回收算法:
只了解 CMS
CMS 是以牺牲吞吐量代价来换取最短回收停顿时间的垃圾回收器。对于要求服务器响应速度上的应用,这种垃圾回收器非常适合。
分代回收器有两个分区;老年代和新生代,新生代默认空间是总空间的1/3,老年代的默认占比是 2/3;
新生代使用的是复制算法,新生代里面有三个分区:eden、to survivor、from survivor,它们默认占比是:8:1:1。
执行流程就是:
先把 eden 和 from survivor 存活的对象放入 to survivor 区;
然后清空 eden 和 from survivor 分区;
最后 from survivor 和 to survivor 分区交换;
每次 from survivor 到 to survivor 移动时都存活的对象,年龄就 +1,当年龄到达 15(默认配置就是15)时,升级为老年代。
老年代占用空间到达某个值之后就会触发全局垃圾回收,一般使用标记整理执行算法,这就是整个分代垃圾回收器的执行过程。
常用的就是 JDK 的 bin 目录下,其中最常用的是 jconsole 和 jvisualvm 这两款视图监控工具。
JVM 中堆和栈属于不同的内存区域,使用目的也不同。栈常用于保存方法帧和局 部变量,而对象总是在堆上分配。
栈通常都比堆小,也不会在多个线程之间共享, 而堆被整个 JVM 的所有线程共享。
设计模式就是一套被反复使用,前辈的代码设计经验的总结,使用设计模式是为了可重用代码、让代码更容易被他人理解、保证代码的可靠性。
看懂源代码,如果你不懂设计模式的话,然后去看一些jdk、spring 底层的源码,你会很迷茫,看不懂。
可以编写出自己理想中的好代码,我个人反正是这样,对于自己开发的项目我会很认真。
创建型模式:工厂方法、抽象工厂、单例、建造者。
结构型模式:适配器、装饰器、代理、外观、桥接、组合、享元。
行为型模式:策略、模板方法、观察者、命令。。。。
单例模式是创建型模式,保证一个类只有一个实例,并且提供一个全局访问点。
Spring IOC 容器中的类默认是单实例的。
多线程的线程池的设计一般也是单例模式,因为单线程要方便对池中的线程进行控制。
使用时不能用反射模式创建单例,否则会实例化一个新的对象。
使用懒汉式单例模式时需要注意线程安全问题
饿汉式单例模式和懒汉式单例模式构造方法都是私有的,因此是不能被继承的。
主要使用的是懒汉式和饿汉式
饿汉式:类初始化时、会立刻加载该类对象,线程安全、调用效率高
懒汉式:类初始化时、不会立即创建该类对象,真正需要使用的时候才会创建该类的对象,具备懒加载的功能。
静态内部类方式:结合了饿汉式和懒汉式的优点,真正需要对象的时候才会加载,加载类是线程安全的。
枚举方式:使用枚举实现单例模式、优点:实现简单、调用效率高、因为枚举本身就是单例,由JVM从根本上提供保障、避免通过反射和反序列化的漏洞,缺点是没有延迟加载。
策略模式是行为型模式,抽取共同行为,根据抽象策略实现不同策略,交给不同的子类实现。策略模式主要解决多重条件判断的问题。
算法需要灵活去切换
多个类都有公共的抽象行为,通过的行为实现不同的场景。
比如说支付场景(微信、支付宝、银联)
还有登录场景(微信登录、QQ登录、手机号码登录)
发短信这些。
观察者模式是行为型模式,当一个对象状态发生变化时,已经登记的其他对象能够观察到这一变化从而做出自己相应的改变,通过这种方式来达到减少依赖关系,解耦合的作用。
对一个对象的状态更新,需要其他对象同步更新,而且其他对象的数量是动态可变的。
对象仅仅需要将自己的更新通知给其他对象而不需要知道其他对象的细节。
模板方法模式是行为型模式,就是定义一个操作的算法骨架(父类),而将一些步骤延迟到子类。模板方法模式使得子类可以不改变一个算法的结构来重定义该算法。
实现一些操作时,整体的步骤很固定,但是呢,就是其中一小部分需要动态改变,这时候就可以使用模板方法,把易变的部分抽象出来,供子类去实现。
进程就是运行在操作系统上的一个任务,进行是计算机调度的一个单位,操作系统在启动程序的时候,会为其创建一个进程,JVM 就是一个进程,进程与进程之间是相互隔离的,每个进程都有独立的存储空间。
线程就是进程的最小任务只需单元,具体来说就是,一个程序顺序执行的流程就是一个线程,我们常见的比如说执行一个 main 方法,就说一个线程。
notify() 和 notifyAll() 都是 Object 对象用于通知处在等待该对象的线程的方法。
void notify(): 唤醒一个正在等待该对象的线程。
void notifyAll(): 唤醒所有正在等待该对象的线程。
notify 可能会导致死锁,而 notifyAll 则不会
任何时候只有一个线程可以获得锁,也就是说只有一个线程可以运行 synchronized 中的代码
使用 notifyall, 可以唤醒 所有处于 wait 状态的线程,使其重新进入锁的争夺队列中,而 notify 只能唤醒一个
加在静态方法,是对类进行加锁,假设类中有方法A和方法B都是被 synchronized 修饰的静态方法,此时有两个线程分别调用 方法A 和方法B,那么就会有一条线程会阻塞等待知道另一条线程执行完成才能执行。
修饰实例方法,是对实例进行加锁,锁的是实例对象的对象头,如果调用同一个对象的两个不同的被 synchronized 修饰的实例方法时,看到的效果和前面的一样,如果调用不同对象的两个不同的 synchronized 修饰的实例方法时,则不会阻塞。
不是公平锁
继承 Thread 类
实现 Runnable 接口
实现 Callable 接口,结合 FutureTask 使用
通过线程池创建线程
首先需要确认业务是CPU密集型还是 IO 密集型
如果是 CPU 密集型,那么就应该尽可能少的线程数量,一般为CPU 的核心数 + 1
如果是 IO 密集型,就可以分配多一点 CPU 核心数,好像是核心数 * 2吧
可以让主线程 await,业务线程进行业务处理,处理完成时调用 countDown 方法。
也可以让业务线程 await,主线程处理完数据之后执行 countDown 方法,此时业务线程被唤醒,然后去主线程拿数据,或者执行自己的业务逻辑。
目前只用过两种
corePoolSize:线程池核心线程大小
maxmumPoolSize:线程池最大线程数量
keepAliveTime:空闲线程存活时间
TimeUnit:空闲线程存活时间单位
当工作队列中的任务已经到达最大限制,并且线程池中的线程数量也到达最大限制,这时如果有新的任务提交进来,就会执行拒绝策略。
只了解 3 种 JDK 拒绝策略
存在一个名为 workers 的 hashset 里面,(未使用的线程也是)
提供方法参数传递
提供共享变量
如果在同一个线程里面,还可以使用 ThreadLocal 进行传递。
是一个线程变量,在ThreadLocal中填充的变量只属于当前线程,该变量对其他线程而言是隔离的,也就是说该变量是当前线程独有的变量。
存储用户的 session 或者 token
1、发挥多核CPU 的优势
现在的笔记本、台式机乃至商用的应用服务器至少也都是双核的,4 核、8 核甚至 16 核的也都不少见,如果是单线程的程序,那么在双核 CPU 上就浪费了 50%, 在 4 核 CPU 上就浪费了 75%。单核 CPU 上所谓的"多线程"那是假的多线程,同一时间处理器只会处理一段逻辑,只不过线程之间切换得比较快,看着像多个线程"同时"运行罢了。多核 CPU 上的多线程才是真正的多线程,它能让你的多段逻辑同时工作,多线程,可以真正发挥出多核CPU 的优势来,达到充分利用CPU 的目的。
2、防止阻塞
从程序运行效率的角度来看,单核 CPU 不但不会发挥出多线程的优势,反而会因为在单核CPU 上运行多线程导致线程上下文的切换,而降低程序整体的效率。但是单核 CPU 我们还是要应用多线程,就是为了防止阻塞。试想,如果单核 CPU 使用单线程,那么只要这个线程阻塞了,比方说远程读取某个数据吧,对端迟迟未返回又没有设置超时时间,那么你的整个程序在数据返回回来之前就停止运行了。多线程可以防止这个问题,多条线程同时运行,哪怕一条线程的代码执行读取数据阻塞,也不会影响其它任务的执行
用 join 方法。
只有调用了 start()方法,才会表现出多线程的特性,不同线程的 run()方法里面的代码交替执行。如果只是调用 run()方法,那么代码还是同步执行的,必须等待一个线程的 run()方法里面的代码全部执行完毕之后,另外一个线程才可以执行其 run()方法里面的代码。
stop 终止,不推荐。
网络编程的本质就是多台计算机之间的数据交换,不就是把一个设备中数据发送给其他设备,然后接受另外一个设备反馈的数据,现在的网络编程基本上都是基于请求和响应的方式。
例如打电话,我给对方打过去就类似于一个客户端,对方接电话的人必须保持电话畅通就类似于一个服务器,然后打通了之后,就是客户端和服务端可以进行数据传递了,而且双方的身份是等价的,程序既有客户端功能也有服务端功能,最常见的软件就是QQ和微信这类了。
在计算机中要交换数据,就必须遵守一些约定好的规则,比如说数据交换的格式,是否需要发送一个应答的信息,这些规则就被称为网络协议。
TCP/IP 分有四层协议(数据链路层、网络层、传输层、应用层)
TCP 就是一个传输的网络协议,发送数据前要先建立连接,发送方和接收方之间必须建立连接,也就是说,通过 TCP 连接传输的数据不会丢失、没有重复。
UDP 他是属于 TCP 协议的一种,是无连接协议,发送数据前不需要建立连接,是没有可靠性的,因为不需要建立连接,所以可以在网络上任何路径上传输。
对某些实时性要求比较高的情况下,使用UDP,比如游戏,媒体通信,实时直播这种,即使出现传输错误也可以容忍,其他大部分情况下,HTTP 都是用的 TCP,因为传输要求内容的可靠性,不出现丢失的情况。
http 协议客户端和服务端之间,数据实现可靠性的传输,文字、图片、视频等超文本数据的规范,简称就是 超文本传输协议。
http 属于应用层。
http 协议是运行在 TCP 之上,明文传输,客户端和服务端都无法验证对方的身份,https 是带有那个 SSL 证书,是添加了加密和认证机制的 http。
端口不同,http 和 https 使用不同的连接方式,用的端口也不一样,http 是 80 , https 是 443。
资源消耗不同,https 通信会由于加密处理消耗更多的 cpu 资源。
开销不同, https 需要购买 SSL 证书。
首先 https 请求,服务端生成证书,然后客户端验证证书的有效期、合法性、域名和请求域名是否一致等。
客户端验证通过后、就会根据证书的公钥,生产随机数,随机数使用公钥进行加密(RSA)。
消息体产生后,对他的摘要进行加密(MD5/SHA1),此时就得到了RSA 签名;
然后发送给服务端,服务端用私钥(RSA)解密。
三次握手其实主要的一个目的就是为了建立可靠的通信通道,就是双方确认自己与对方是否能正常的进行发送和接收数据。
第一次握手:客户端什么都不用确认,服务端确认了对方发送是否正常。
第二次握手:客户端确认自己发送和接收正常、对方发送和接收正常。然后服务端确认自己接收正常、对方发送正常。
第三次握手:客户端确认自己发送和接收正常、对方发送和接收正常。然后服务端确认自己发送和接收正常、对方发送和接收正常。
所以三次握手后就能确认双方功能的正常了,缺一不可。
四次挥手就是,比如说 A 向 B 发送出FIN报文段时,只是表示 A 已经没有数据要发送了,而此时A 还能够接收到 B 发来的数据,B 要向 A 发出 ACK 报文段也只是告诉 A,它知道自己 A 没有数据要发了,但B还能够向A 发送数据。
所以想要愉快的结束这次对话就需要四次挥手。
目的是为了防止已失效的连接请求报文又传到了服务端,因此而产生错误。
假设不采用 三次握手,那么只要B发出确认,新的连接就建立了。由于现在A并没有发出建立连接的请求,因此不会理睬B的确认,也不会向B发送数据,但B却以为新的连接已经建立了,并一直等待A发来数据,这样,B的很多资源就浪费了。采用三次握手就可以避免这种情况的发生。
Scoket 我理解的就是 TCP 协议的一个外观模式吧,它把复杂的 TCP 协议隐藏在 Socket 后面,对于用户来说,一组简单的接口就是全部,让 Socket 去组织数据,以符合指定的协议。
Socket 连接是所谓的长连接,理论上是客户端和服务端一旦建立连接后将不会主动断掉。
Socket 适用场景是,游戏、直播等
http 是短连接,就是客户端向服务端发送一次请求,服务端响应后就会断开连接。
http 适用场景是,网站这些
先是由域名解析为IP地址,然后寻址IP地址的过程的先经过浏览器、系统缓存、hosts 文件、路由器缓存,再到根域名服务器。
然后开始建立 TCP 连接。
由浏览器发送一个 HTTP 请求。
经过路由器的转发,该请求到达了 HTTP 服务器
服务器处理 HTTP 请求,返回一个HTML文件,或者 xml、json
浏览器解析返回的数据,并显示在页面上。
就是 http 是一个没有状态的协议,也就是没有记忆力,就意味着每一次请求都是独立的,缺少状态意味着如果后续处理需要前面的信息,则必须重新传,这样可能导致每次连接传输的数据量增大。
cookie 实际上是一段小的文本信息,客户端请求服务器,如果服务器需要记录该用户状态,就使用 response 向客户端浏览器发送一个 cookie,然后客户端浏览器就会把 cookie 保存起来。当浏览器再次请求该网站时,浏览器就会把请求地址和 cookie 一起提交给服务器,服务器检测 cookie,以此来辨认用户状态,还可以根据需求修改 cookie 的内容
session 就是保存在服务端的会话状态,客户端请求服务器,如果服务器需要记录该用户的状态,就获取 session 来保存状态,如果服务器已经为这个用户创建过 session,服务器就按照 sessionId 把这个 session 检索出来使用,如果没有创建过就为这个用户创建一个 session 并且生成一个 sessionId,并将这个 sessionId 返回给客户端保存。
application 是与一个 web 应用程序相对应,为应用程序提供一个全局的状态,所有客户都可以使用该状态。
GET:通过URL传参的方式访问资源
POST:通过Body 传参的方式访问资源
PUT:一般用于传输文件
DELETE:删除对于 URI 位置的文件
1、1xx(临时响应)
2、2xx(成功)
3、3xx(重定向):表示要完成请求需要进一步操作
4、4xx(错误):表示请求可能出错,妨碍了服务器的处理
5、5xx(服务器错误):表示服务器在尝试处理请求时发生内部错误
沾包就是指发送方发送若干个数据包到接收方时沾成了一个包,后一个包是数据头紧接着前一个包的数据尾。只有 TCP 有沾包的现象,UDP 不会。
当发送连续的数据时,会将较小的内容拼接成大的内容(有一个算法),一次性发送到服务器端,因此造成沾包。就是当发送内容较大时,由于服务器的 buffer_size 方法中 buffer_size 较小,不能一次性完全接受全部内容,因此下一次请求到达时,接收内容依然是上一次还没有完全接收的内容,这就是沾包现象。
Java 注解就是代码中的一些特殊标记,就是在运行时进行解析和使用的,它的本质就是继承了 Annotation 的特殊接口,具体实现类就是 JDK 动态代理的代理类。
通过反射获取到注解时,返回的也是 Java 运行时生成的动态代理对象。通过代理对象调用自定义注解的方法。
① 创建一个自定义注解:与创建接口类似,但自定义注解需要使用 @interface
② 添加元注解信息,比如 @Target、@Retention、@Document、@Inherited 等
③ 创建注解方法,但注解方法不能带有参数
④ 注解方法返回值为基本类型、String、Enums、Annotation 或其数组
⑤ 注解可以有默认值