中级java后台开发复习题(最新更新时间20210206)


算法

  1. 动态规划
  2. 正则
  3. 排序
  4. 二叉树

面经

  • https://github.com/xbox1994/Java-Interview
  • https://github.com/guanzhenxing/java_interview_manual
  • https://github.com/OUYANGSIHAI/JavaInterview

集合框架

  • list:arraylisy,voctor (安全) linkedList
  • map:HashMap LinkedHashMap Hashtable(安全)
  • set:hashset TreeSet(有序) LinkedHashSet(有序)

linux命令

  • 模拟http请求
 

post:curl -H "Content-Type:application/json" -d"{'title':'asas'}" "localhost:9417/notice/message/lumi/pushMessage"

get: curl “http://www.baidu.com”

  • 获取系统某进程号,也可以看出启动的参数
 

ps -ef|grep java

  • 查询gc日志
 

运行前:-XX:+PrintGCDetails

运行时:jps查询出进程号 pid +进程名字,然后使用jstat -gc pid

  • 查看jvm的类加载情况
 

jps查询出进程号 pid +进程名字,然后使用jstat -class pid

  • 传输文件命令
 

scp xxx文件 用户名@ip地址:目标地址

  • 查看日志命令
 

tail -200f xxx.log

less xxx.log

  • 查看mysql死锁状态
 

mysql -uroot -p 输入密码

show engine innodb status \G

  • 服务器性能查看
 

top

  • 查看磁盘占用情况
 

du -h 文件/文件夹 文件情况

df -h 磁盘使用情况

  • 查看网络情况
 

netstat 查看自己

telnet ip 端口号 查看别人


java基础

  • 描述下Object相关方法
  • getClass:返回当前对象运行时的class对象
  • hashcode:返回对象的哈希值
  • clone:需要实现cloneable接口,区分浅拷贝(对基本类型进行值拷贝,引用类型进行引用拷贝),深拷贝(对基本类型进行值拷贝,对引用类型拷贝对象的引用,属性和方法),区别在于深拷贝创建了一个新的对象
  • equals:内存地址的比较,string重写为值得比较
  • toString:返回类名+@+哈希值
  • notify:唤醒当前对象监视器上的任一个线程
  • notifyAll:唤醒当前对象监视器上的全部线程
  • wait:停止当前线程,释放锁(sleep不释放),参数有三个(等待多久,额外等待多久,一直等待)
  • finalize:对象被垃圾回收器执行时调用的方法
  • 基本类型
  • 整型:byte(8),short(16),int(32),long(64)
  • 浮点型:float(32),double(64)
  • 布尔型:boolean(8)
  • 字符型:char(16)
  • 描述下序列化
  • 序列化需要实现serializable接口
  • 反序列化生成对象由jvm实现,并不是对象的构造方法
  • 实例化的对象中的引用类型成员变量,需是可序列化的,不然报错
  • 不想被实例化需要用transient修饰
  • 单例类序列化,需要实现readresolve方法
  • 描述下string,stringbuffer,stringbuilder
  • java中的字符串分可变和不可变
  • string不可变,其本质是使用final修饰的char[]数组,定义时候已经固定了大小,对于字符串的sub,replace,concat操作,是生成一个新的数组,并把指针指向新的引用对象
  • stringbuilder线程不安全,初始大小是16位,可扩容,执行sub,replace,concat操作,是执行数组拷贝操作,tostring执行的是new String
  • stringbuffer线程安全,在stringbiffer的append上进行加锁,也会进行缓存,tostring时优先使用共享缓存,更新操作清空缓存
  • 重写与重载的区别
  • 重写发生下父子类中,方法名,参数相同,返回值<=父类,异常<=父类,修饰符>=父类。父类被private,final修饰则不可重写
  • 重载发生下同类,方法名相同,参数数量,顺序,类型不同,修饰符号,返回值可相同或不同
  • final修饰符的作用
  • 不可继承不可重写
  • 基本类型,不可修改
  • 引用类型,不可指向另一个引用

hashMap

  • 数据结构
  • jdk1.7与jdk1.8有比较大的区别,1.7使用的是数组加链表的数据结构,entry是主要的内部类,几个比较重要的参数分别的数组数量的长度16,负载因子0.75,以及长度*负载因子得到的最大容量,当数据插入时,判断是否大于最大容量,是进行resize扩容,设置一个两倍大的数组,使用transfer()获得小数组的数据,期间需要进行过rehash计算所有key在新数组曹植,否使用数据的key进行hash计算,在数组中查看是否已经存在,若不存在则直接插入,若存在则在链表上遍历,存在相同key直接覆盖,不存在则使用头插法插入链表,1.8使用的是数据加链表加红黑树的数据结构,entry节点也变成了node节点,当链表的结构大于8时候,自动转换为红黑树,优化了查询的效率,插入和1.7差不多,不过在链表转换成红黑树的时候,需要根据红黑树的条件进行构建,使用红黑树可以使得在链表查询的时间复杂度变成log2n。
  • 自动扩容
  • 参看redis hash扩容
  • 线程安全
  1. 在jdk1.7中,在多线程环境下,插入时会造成环形链或数据丢失。
  2. 在jdk1.8中,在多线程环境下,会发生数据覆盖的情况。
  3. 使用ConcurrentHashMap,分段锁,锁住桶默认16个桶
  4. 使用Collections.synchronizedMap(),使得一个map变成线程安全,和Hashtable类似整个加锁
  5. 使用Hashtable,锁住整个
  • 红黑树
  • 特点:根节点黑色,从任意一节点到叶子节点黑色相同,新插入为红色 效率:logn,以2为底

mysql

  • mysql的优化思路
  • 参数调整
  • 索引
  • 读写分析
  • 分库分表
  • 慢sql怎么排查

https://www.jianshu.com/p/47f4bc08776b

  1. 写:

SELECT →FROM → JOIN → ON → WHERE → GROUP BY → HAVING → ORDER BY→ LIMIT

写sql需要经过连接器(客户端可服务端都有缓冲池),分析器,查询优化器,执行器(有存储引擎),在执行器时先判断butter缓冲池(lru最少使用算法,后优化为冷热区)是否有需要的表的数据,有则加锁更新,无则申请在写入。

写sql需要经过连接器(客户端可服务端都有缓冲池),分析器,查询优化器,执行器(有存储引擎),在执行器时先判断butter缓冲池(lru最少使用算法,后优化为冷热区)是否有需要的表的数据,有则加锁更新,无则申请在写入。

  • 高并发下redo log file(循环追加)被快速写满了,需要将 checkpoint(针对缓存而言)向前推进,推进的这部分日志对应到脏页就需要刷入磁盘,此时阻塞更新。
  • 数据库内存不足的时候,淘汰到脏页,需要把脏页写到磁盘。
  • 遇到所要修改的数据行或表加了锁时,需要等待锁释放后才能进行后续操作,SQL 执行也会变慢。
  1. 读: 一条完整的读sql先使用连接器连接好的通道,进入缓存查询,(8.0之后被删除),查询不到进入分析器进行语义分析,在进入优化器进行索引原则,连表顺序选择,在进入执行器执行,查询缓冲池的预读数据,有则直接返回,无则io并返回结果。若在执行器上未命中索引从而导致全表扫描,可以通过开启慢sql日志, explain 方式对 SQL 语句进行分析。 另一种原因是在读操作时,要读入的数据页不在内存中,此时内存紧张,需要通过淘汰脏页才能申请新的数据页从而导致执行变慢。
  • explain命令
  • possible_keys:表示查询时,可能使用的索引
  • key:表示实际使用的索引
  • Extra:执行情况的描述和说明
  • 索引不命中

通过explian,我们可以看到很多可以优化的地方,比如看下是否遵循左前缀法,看下是否查询条件有以下情况

  • 没查询条件或者条件上没索引
  • or中有一个没索引
  • like以%开头
  • 字符串与数字直接比较
  • 条件中使用函数
  • 条件中有not in,not exist
  • 索引使用了isnull
  • 链表时,字符编码不同
  • 使用了比较长的in条件
  • 优化小技巧
  • in后面跟小表,exists后面跟着大表
  • 多多使用覆盖索引,减少回表的次数
  • 注意使用索引下推
  • 左关联,type都是all的话,我们可以找右表建立索引
  • 聚簇索引和非聚簇索引
  • 聚集索引是一种数据结构,数据库就是使用一个非空的索引构建b加树,叶子结点是表的数据。非聚集索引是一种二级索引,表中可以有多个,叶子结点是主键指针+数据,是的话需要进行二次索引,也可以设置成覆盖索引,叶子结点即为所要的数据,减少了回表。
  • Icp索引下推
  • 当存在索引的列做为判断条件时,MySQL服务器将这一部分判断条件传递给存储引擎,然后存储引擎通过判断索引是否符合MySQL服务器传递的条件,只有当索引符合条件时才会将数据检索出来返回给MySQL服务器。
  • 数据库死锁查询和分析
 

show engine innodb status \G

  • 事务之间交替访问数据,期间加了锁,如相互等待间隙锁
  • 索引不当造成全表扫描
  • 数据库事务:
  • 数据库的事务是四个点就是原子性,持久性,隔离性,一致性。其中事务的原子性通过undo log来实现,(undo log记录的是完整的数据行数据,在回滚时会负责恢复数据)事务的持久性是通过redo log来实现(redo log记录了物理日志,分为redo log butter和redo log file ,在事务commit之前会记录到file文件中,后面择机记录到数据库文件),事务的隔离性是通过读写锁和MVCC实现(锁有排他锁,共享锁等,表锁,行锁,间隙锁(唯一索引:命中只加行锁,部分命中或不命中加(],普通索引:命中加前后,不命中加(],若不存在索引覆盖,还要关注数据对应的唯一索引的间隙锁),而事务的一致性就是通过回滚,以及恢复,和在并发环境下的隔离做到一致性。
  • 在执行器中----先写日志(记录 undo/redo log buffer(至于何时持久化,是可以通过innodb_flush_log_at_trx_commit配置的,默认同时=1,还是提交后定时=0,还是提交后先存在系统缓存,从缓存写入磁盘=2)),再更新内存,标记可以提交。 写 binlog并持久化。 进行提交事务。
  • 数据库的性能很大影响在于sync_binlog,多少周期(0代表不受限制,参数用来保证redo log 和 binlog的一致)写binlog,和innodb_flush_log_at_trx_commit,何时写入redo log buffer的影响
  • 数据库事务与锁的关系
  • 数据库事务有两个比较简单的,读未提交和串行,他们分别就是加锁和不加锁,稍微复杂的是读已提交和可重复读,他们使用了行锁和mvcc(多版本并发控制)进行实现,实现原理是记录了数据修改的每个版本,select按情况在版本中取得数据,使得读写,写读可以并发进行。
  • mvcc实现原理:undolog是mvcc重要的组成,聚集索引上有三个隐藏的字段...事务进行更新,删除,插入时,mvcc会将undolog通过版本号指针由新到旧联成链。若事务a插更新id=1的数据,但是尚未提交。事务b进行查询,此时获取正在进行ReadView事务的列表,称之为readView,通过readView和查询数据版本链上事务的大小关系比较,来决定读取到哪个版本的数据。可重复度和读提交,本质在于生成readView的时机,可重复度是每次select都进行更新。所谓的快照读就是从第一个select开始取得快照的。
  1. 假设当前列表里的事务id为[80,100]。如果你要访问的记录版本的事务id为50,比当前列表最小的id80小,那说明这个事务在之前就提交了,所以对当前活动的事务来说是可访问的。如果你要访问的记录版本的事务id为90,发现此事务在列表id最大值和最小值之间,那就再判断一下是否在列表内,如果在那就说明此事务还未提交,所以版本不能被访问。如果不在那说明事务已经提交,所以版本可以被访问。如果你要访问的记录版本的事务id为110,那比事务列表最大id100都大,那说明这个版本是在ReadView生成之后才发生的,所以不能被访问。
  2. DB_TRX_ID:操作数据的事务id号

DB_ROLL_PTR:指向本数据上一条unnolog的指针

DB_ROW_IDDB_ROW_ID :未指定聚集索引时候,创建的自增id

  • 分库分表
  • 单表数据达到多少的时候会影响数据库的查询性能?为什么?

在阿里的开发手册中,单表的数据量操作500w或大于2g就建议进行分库分表,大量的数据会使得b+树的深度很深,新增和查询都很慢,为了提高效率索引会被转载到缓冲池的索引页中,但是内存装不下了及时查询就不得不走io,所用其实主要的限制还是在于服务器的内存。

  • 什么是分库分表?设计高并发系统的时候,数据库层面该如何设计?

分库分表就是把表按照水平或垂直的方式,进行拆分。按照java开发手册在预料未来3年内都不会达到很大的数据量和大的数据并发请求,可以不进行分库分表避免过度设计,可以通过数据库的参数调整来,比如redo log 写入磁盘的逻辑,binlog的写入逻辑,索引设计,设置上读写分离。若达到了分库分表的需求,要很熟悉业务的领域,根据领域进行拆分,如垂直分库,就是多个表进行分类,存在不同的服务器中分库,或者拆分出非热点数据,拆分出比较老旧的数据,期间还有一些小细节,如为了避免热点分表比较好的就是使用key的 hash来,比较均衡。分库分表之后,单机变成多机,承受并发高,磁盘使用率下降,内存使用率也会下降,但是也会带来分布式事务,跨库join等问题。

  • 有没有做MySQL读写分离?如何实现mysql的读写分离?

MySQL读写分离是基于主从架构实现的,有一个主库负责写数据,挂着多个从库负责读。同步的流程是这样的,主库biglon写完了会创建binlog dump线程,对接从库的IO线程生成repay log,从库重做线程来根据日志执行命令。会造成主从不一致的情况,1.主库并发高,从库重做比较慢 2.从库进行查询会有锁等待如在一个线程中进行更新并马上读数据,可能会读不到,解决办法就是并发重做。可以通过 show slave status 命令查看 Seconds_Behind_Master 的值来看是否出现同步延迟。主从模式也可能会出现宕机造成复制失败,从库少数据,这样子我们可以通过设置为半同步复制,写操作的完成需要等待数据发送到从库,甚至全同步复制。

  • 用过哪些分库分表中间件?不同的分库分表中间件都有什么优点和缺点?

不太数据,记得有阿里巴巴的mycat,TDDL,主要是区分应用层依赖类(和应用强耦合)和中间层代理类,主要的思想就是屏蔽用户开发,在应用和jdbc层面之间对分库分表的数据库做透明化处理,使得我们crud,分布式id啊等不需要改变

  • 如果是MySQL引起的CPU消耗过大,你会如何优化?

io等待,使用索引减少io,mysql缓冲池的脏页写io,redo log写io,binlog写io,上主从复制的话也会写io。

  • 优化器的工作逻辑
  • 优化器的主要作用是决定连表的顺序,选择合适的索引,有时候会有选择错的情况,优化器怎么选择索引呢?主要的参数就是估算索引基数计算百分比*总数得到查询的条数,走索引,回表的消耗。选错的原因就是基数的估算是抽样检测的,要数值准确可以使用analyze分析下表,也可以force index强制索引或者修改表的语句

spring

  • Spring mvcl流程
  1. 发送请求——>DispatcherServlet拦截器拿到交给HandlerMapping
  2. 依次调用配置的拦截器,最后找到配置好的业务代码Handler并执行业务方法
  3. 包装成ModelAndView返回给ViewResolver解析器渲染页面
  • ioc
  • 我所理解的ioc是解决对象管理和对象依赖的问题,本来需要使用new方法的对象交给了ioc容器来进行管理,ioc容器理解为对象的工厂来管理对象的创建和依赖。ioc有两个概念,控制反转和依赖注入,控制反转就是上面说的对象交给ioc容器进行管理,依赖注入是控制反转的实现方式,对象会被自动注入到需要他们的对象中去,进而把对象和对象的依赖降低耦合。另外提供了一整套完整的bean的生命周期管理。
  • 怎么在容器中创建bean
  • 注解:@controller @import() @bean
  • xml
  • javaconfig
  • 生命周期
  • Bean自身的方法:这个包括了Bean本身调用的方法和通过配置文件中<bean>的init-method和destroy-method指定的方法
  • Bean级生命周期接口方法:这个包括了BeanNameAware、BeanFactoryAware、InitializingBean和DiposableBean这些接口的方法

implements BeanFactoryAware, BeanNameAware, InitializingBean, DisposableBean

  • 容器级生命周期接口方法:这个包括了InstantiationAwareBeanPostProcessor 和BeanPostProcessor(spring动态代理的原理) 这两个接口实现,一般称它们的实现类为“后处理器”。

implements InstantiationAwareBeanPostProcessor, BeanPostProcessor

  • 工厂后处理器接口方法:这个包括了AspectJWeavingEnabler, ConfigurationClassPostProcessor, CustomAutowireConfigurer等等非常有用的工厂后处理器接口的方法。工厂后处理器也是容器级的。在应用上下文装配配置文件之后立即调用。
  1. Spring启动,查找并加载需要被Spring管理的bean,进行Bean的实例化
  2. Bean实例化后对将Bean的引入和值注入到Bean的属性中
  3. 如果Bean实现了BeanNameAware接口的话,Spring将Bean的Id传递给setBeanName()方法
  4. 如果Bean实现了BeanFactoryAware接口的话,Spring将调用setBeanFactory()方法,将BeanFactory容器实例传入
  5. 如果Bean实现了ApplicationContextAware接口的话,Spring将调用Bean的setApplicationContext()方法,将bean所在应用上下文引用传入进来。
  6. 如果Bean实现了BeanPostProcessor接口,Spring就将调用他们的postProcessBeforeInitialization()方法。
  7. 如果Bean 实现了InitializingBean接口,Spring将调用他们的afterPropertiesSet()方法。类似的,如果bean使用init-method声明了初始化方法,该方法也会被调用
  8. 如果Bean 实现了BeanPostProcessor接口,Spring就将调用他们的postProcessAfterInitialization()方法。
  9. 此时,Bean已经准备就绪,可以被应用程序使用了。他们将一直驻留在应用上下文中,直到应用上下文被销毁。
  10. 如果bean实现了DisposableBean接口,Spring将调用它的destory()接口方法,同样,如果bean使用了destory-method 声明销毁方法,该方法也会被调用。
  • aop:
  • 就是切面编程,解决的是非业务代码的抽取问题,原理是动态代理。我理解的aop的用法就是方法的前后进行非业务的处理,减少代码的耦合在我们的业务中,使用了自定义注解+aop来对接口进行监控,打印出请求参数和qps等数据。
  • jdk动态代理使用了proxy,需要实现invocationHandler接口,属于接口代理
  • cglib动态代理这是使用底层的字节码技术,为类生成一个子类,子类拦截父类的方法,实现代理,属于继承代理。默认是jdk动态代理。
  • spring的事务

https://www.jianshu.com/p/2449cd914e3c

Spring提供了编程式事务和声明式事务两种实现方式,

  • spring的事务围绕的是PlatformTransactionManager接口提供的获取事务状态方法getTransaction,提交方法commit。回滚方法rollback,这个接口并不直接管理事务,而是委托给相关持久化框架的事务机制去实现的。事务原理是aop,由TransactionInterceptor的invoke进入,
  • 先准备事务:从@Transactional获取注解信息(可以使用在方法,类,接口上,优先顺序也是方法,类,接口)
  • 再开启事务:获得数据库连接,设置非自动提交,判断事务的传播级别进行事务挂起,事务恢复
  • 最后:执行目标方法或者执行AOP拦截器链中的拦截器,根据异常来判断是否是回滚还是提交
  • 循环依赖解决办法
 

//lazy

@Component

public class ServiceA {

@Autowired

@Lazy

private ServiceB serviceB;

}

//对setter注入。当依赖最终被使用时才进行注入,对当前代码少做修改

@Component

public class ServiceA {

private ServiceB serviceB;

@Autowired

public void setServiceB(ServiceB serviceB) {

this.serviceB = serviceB;

}

}

  • 体会
  • 学的时候觉得spring的对象配置比较繁琐,但是升级为springboot后就比较方便了,加个bean定义注解和自动注入注解就可以使用了,配合spring的其他注解对于开发者来说相当还用,如@Transactional,不过也会出现一直问题比如说bean创建失败,循环依赖等

jvm

  • 存放位置
  • 堆:存放对象的实例
  • 方法区:类的信息,静态变量,常量,编译后的代码
  • 程序计数器
  • 本地方法栈:存放栈帧,栈帧存放局部变量表(若是局部变量引用了成员对象,则指向方法区),操作数栈(存放方法过程中数据的操作,如求和,交换,对应了出栈入栈),动态链接(指向运行时常量池的方法引用,接口的实例对象在堆里的地址)和返回地址(返回或者异常退出的地址)。
  • 虚拟栈:同本地方法栈,存储native函数
  • 怎么查看gc日志
  • 启动设置jvm参数 比较常用就是 -xx:+printGcDetails可以查看使用的收集器,新生区和老年区的gc前后情况,总容量,消耗时间等也可以使用jvm常用的内存分析命令

jsp 虚拟机内进程

jstat 某进程内虚拟机运行信息 如(-gc java堆状况,容量,已用,gc时间)

jmap 查看堆转存文件

jstack 线程快照

  • gc算法
  • 标记清除:效率不高,会产生碎片。使用在老年代
  • 复制:浪费空间。使用在新生代。
  • 标记整理:移动存活的对象。使用在老年代

(引用计数法,可达性分析法)

  • gc回收器:垃圾回收算法是方法论,回收器是具体的实现,有以下几个
  • serial:单线程,执行时还会停止工作线程(新生代-复制、老年代-标记整理)
  • ParNew :Serial多线程版 (新生代-复制、老年代-标记整理)
  • Parallel Scavenge :也是多线程,关注吞吐量
  • serial old 单线程,serial老年代版本,使用标记整理算法,主要被用在 client 模式下.
  • Parallel old 多线程, Parallel Scavenge老年代版本 标记-整理算法.
  • CMS:一种以获取最短回收停顿时间为目标的收集器.

步骤:

初始标记(单线程,停工作)

并发标记(多线程,并行,花费时间长)

重新标记(多线程,停工作)

并发清除(多线程,并行)

缺点:

基于标记清除算法导致产生大量空间碎片.

并发清除时可能会有浮动的垃圾产生

对cpu资源比较敏感

  • G1 : 可控制的停顿.

步骤:

初始标记(单线程,停工作)

并发标记(多线程,并行,花费时间长)

重新标记(多线程,停工作)

筛选回收(多线程,并行)

显著优点:

基于标记整理算法,不会产生碎片,有压缩的过程

另一个就是可以精准控制停顿,既能让使用者明确指定一个长度为M毫秒的时段片内,消耗在垃圾收集上的时间不得超过 N 毫秒. 不牺牲吞吐量的前提下完成低停顿的内存回收.

优先收集高价值的区域

  • zgc:jdk11
  • 调优思路
  • 减少gc次数:调整堆分代的大小
  • 加快gc的速度:选择合适的收集器,加大内存
  • 类加载的机制
  1. 加载:加载class文件,堆中创建对象,方法区存放数据
  2. 验证:验证字节码的安全性
  3. 准备:类中变量初始化
  4. 解析:符号引用解析成直接引用
  5. 初始化:调用构造函数,初始化类的信息(new,反射,子类初始化,main方法)
  6. 使用:业务使用
  7. 卸载:jvm中删除
  • 双亲委派机制
  • 启动类加载器:环境变量下jre/lib 的jar包
  • 拓展类加载器:环境变量下jre/lib/ext 的jar包
  • 应用类加载器:classPath的内容
  • 自定义加载器:继承ClassLoader
  • 当一个类加载器收到了类加载的请求的时候,他不会直接去加载指定的类,而是把这个请求委托给自己的父加载器去加载。只有父加载器无法加载这个类的时候,才会由当前这个加载器来负责类的加载。如用户自定义的一个类。
  1. 避免重复加载
  2. 保证核心的类不会被篡改,因为核心的类存放在jdk中
  • 实现的方式是类的组合,不是继承。loadClass 方法 会先检测类是否被加载过,否使用父类,父类返回失败使用子类。使用自定义类加载器,覆盖loadClass方法,或者重写findClass() 根据名称或位置加载.class字节码,就可以破坏双亲委派,用于加载一些框架的自定义类,加载重名类,过滤不需要的类

redis

  • redis的基本数据类型
  • string hash list set sortset Stream(5.0之后)
  • Redis 如何实现 key 的过期删除?
  • 定时删除,定时从设置了过期时间的数据中随机抽取,判断是否过期,过期则删除
  • 惰性删除,被使用时判断是否过期11,过期则删除
  • 定时器删除,为设置了过期时间的数据创建一个定时器,到点删除
  • Redis 如何实现 key 的内存淘汰
  • 默认不处理等报错
  • 从所有集中挑选淘汰:(随机;淘汰最使用时间距离现在最久的;淘汰使用频率最少的)
  • 从设置了过期时间中进行淘汰(随机;淘汰最使用时间距离现在最久的;淘汰使用频率最少的;淘汰剩余过期时间最短的)
  • Redis的持久化有哪几种方式?不同的持久化机制都有什么优缺点?持久化机制具体底层是如何实现的?
  • redis持久化的意义在于故障恢复,属于服务高可用的范畴,当redis遇到灾难性宕机,内存的数据将会丢失,此时我们可以从磁盘上持久化的数据进行恢复,避免缓存雪崩。
  • 持久化方式有rdb,aof。rdb机制对数据进行周期性备份,每一份都是某个时刻的redis全数据,适合做冷备份,恢复方便快捷,但是由于是周期备份,有丢失最近周期的风险;aof是基于日志的备份,保存每条执行命令的日志,备份性能高,磁盘消耗小,数据可读可追溯,但是依赖恢复命令万一出了问题可能没办法恢复完整,单个文件也可能会较大。
  • 企业级应用一般是rdb,aof相结合
  • 如何解决 Redis 缓存雪崩,缓存穿透,缓存击穿?
  • 缓存雪崩:缓存过期导致大量请求落在数据库,引发数据库压力大而崩溃
  1. 错开缓存的过期时间,在设置缓存时可增加一个额外的时间
  • 缓存穿透:大量请求没命中缓存,直接落在数据库
  1. 布隆过滤器,将所有可能存在的数据存放在一个足够大的哈希map中;

将没命中的数据进行缓存,值为null,设置较短的过期时间

  • 缓存击穿:单个热点key过期,短时间大量请求进入数据库
  1. 永不过期
  2. 互斥锁,访问缓存发现为空,使用带返回值的操作,返回成功马上加锁从数据库获取值并设置缓存,返回失败则再尝试获取,失败后睡眠等待重试
  3. 后台刷新,定时任务刷新即将过期的数据
  4. 检查更新,设置一个小于缓存过期时间的值a,并绑定上key,在范围缓存时候进行判断,若已经超过时间a但是没超过时间缓存时间,则进行刷新
  5. 分级缓存,设计一个缓存时间长的l1,缓存时间短的l2,优先查询l1,若过期,加锁刷新l1,l2,刷新过程中其他线程从l2取得
  • 如何使用 Redis 实现消息队列?风险点?
  • Redis自带订阅发布,可以使用命令publish,subscrible
  • 使用list数据结构,一边lpush,一边循环lrop
  • 使用sortset,类似list实现,在值上可以赋予一个权重,当成id使用,实现有序队列
  • 使用Stream,Stream指的是一条消息的集合,有增删等操作,多个消费组挂在stream上,使用独立的游标标记位置,消费组中有多个消费者,相互竞争消息,消费成功会使得消费者中的状态变量改变,消费组的游标前移
  • 依赖于redis的稳定,容易丢失数据。如Pub/Sub 模式,若订阅者不在线就会丢失数据,多个订阅者接受时间不一致,单个订阅者消息积压可能会断开,(5.0引入stream)利用sortset数据结构的消息不支持重复消息
  • 说说redis集群方式
  • 主从复制

集群机器有一个主服务器,若干个从服务器。从服务器连接主服务器,发送sync同步命令,主服务器接受同步命令,执行bgsave命令生成rdb文件并记录此后的每一条命令,bgsave命令完成,向所有从服务器发送快照文件,此时正常接受执行,记录写命令。从服务器接收到快照文件,覆盖原数据。主服务器发送完快照文件,发送缓存区的写命令,从服务器接受写命令并执行。此时从服务器初始化完成,主服务器接收到命令,就会向从服务器发送相同的命令

优点:可以进行读写分离,从服务器进行读数据

缺点:若同步时候出现延迟,错误,可能出现主从不一致,集群容量较大在线拓展较为复杂

  • 哨兵模式

当主服务器宕机下线,可以将一台从服务器提升为主服务器,哨兵提供了自动化的机制去完成监控,提醒,故障恢复。每个哨兵进程1s/1次发送ping到主服务器,从服务器,其他哨兵。若一个实例超过down-after-milliseconds的值尚未回复,则被哨兵标记为主观下线,当这个实例为主服务器,所有哨兵都1s/1次确认主服务器是否主观下线,一定数量确认后主服务器被标记为客观下线,不超过则移除。确认主机客观下线后,通过投票将一台从服务器升级为主服务器,并通过发布订阅模式通知其他的从服务器,修改配置文件,让它们切换主机。

  • 集群模式

实现分布式存储,每个节点都有一个插槽,一个管理集群的插件。当我们访问缓存时,redis通过crc16算法计算出一个数值,数值除以16383取余数,由余数对应到插槽中的节点去取得相应的值。插槽上的节点可接入主从哨兵

  • Redis的持久化有哪几种方式?不同的持久化机制都有什么优缺点?持久化机制具体底层是如何实现的?
  • redis的hash扩容和ConcurrentHashMap的扩容区别
  • redis的hash中有一个计算所得的负载因子,越大显示冲突越多,当到达一定数量时,会进行扩容。此时为准备好的h1数组分配空间,大小为第一个大于原数组.used的2倍的2的n次方的数,将原数组进行rehash到旧的数组上,冲突依然使用头插入的链表法,直到救旧的数组被清空,新数组设置为旧表,旧数组分配一个空的hash表。为了避免一次的性能消耗过大,redis使用单线程渐进式rehash,每次插入时,使用新的hash方法,定位到新的数组,并且从旧的数组中的同一个地址上的数据进行rehash到新的数组上,循环下去直到旧数组被清空,读操作先读旧数组,在读新数据。
  • ConcurrentHashMap采用多线程协同式rehash,多个线程同时进行迁移操作,此时读的话,线程知道数据在新的桶还是旧的,直接取得,写操作会帮助一起进行迁移,在进行写操作。
  • 扩容所花费的时间对比:
  1. 一个单线程渐进扩容,一个多线程协同扩容。在平均的情况下,是ConcurrentHashMap快
  2. 读操作,两者的性能相差不多。
  3. 删除、写操作,Redis的字典返回更快些,因为它不像ConcurrentHashMap那样去帮着扩容(当要写的桶位已经搬到了newTable时),等扩容完才能进行操作。
  • 保证缓存一致性(先更新缓存还是数据)
  • 更库->更缓;删缓->更库;都容易在中间发生其他线程的操作,更库->删缓+定时是比较推荐的做法,更加好的是删缓->更库->延迟一会再删缓

多线程

  • 线程与进程

进程是资源分配和调度的基本单位,每一个进程都有自己的内存空间和系统资源,实现了多处理机环境下的任务调度,切换时候需要很大时间空间开销。线程是为了解决任务切换耗时的问题,是cpu调度的基本单位。进程资源内存独立,线程内存共享,若只用一个线程,那么无法发挥多核cpu的性能,但是多个线程在执行任务就会引发出线程安全的问题。

  • 线程创建方法

采用继承Thread类方式,重写run

(1)优点:编写简单,如果需要访问当前线程,无需使用Thread.currentThread()方法,直接使用this,即可获得当前线程。

(2)缺点:因为线程类已经继承了Thread类,所以不能再继承其他的父类。

采用实现Runnable接口方式,实现run

(1)优点:线程类只是实现了Runable接口,还可以继承其他的类。在这种方式下,可以多个线程共享同一个目标对象,所以非常适合多个相同线程来处理同一份资源的情况,从而可以将CPU代码和数据分开,形成清晰的模型,较好地体现了面向对象的思想。

(2)缺点:编程稍微复杂,如果需要访问当前线程,必须使用Thread.currentThread()方法。

  • 线程的生命周期
  • 新建(new方法),
  • 就绪(rubable状态,等待cpu的时间片),
  • 运行,得到时间片开始运行,可能会被阻塞
  • 阻塞,1.线程中调用join,2,使用了sleep,3调用yield礼让,3调用wait等待
  • 销毁,任务完成
  1. sleep 不释放锁、释放cpu
  2. yiled 不释放锁、释放cpu
  3. join 释放锁、抢占cpu
  4. wait 释放锁、释放cpu
  • 守护线程是一种特殊的线程,它的特性有“陪伴”的含义,当进程中不存在非守护线程了,则守护线程自动销毁。典型的守护线程就是垃圾回收线程,当进程中没有非守护线程了,则垃圾回收线程则没有存在的必要了,自动销毁。
  • start会创建线程,run不会
  • 线程池的创建方式

Java通过Executors(jdk1.5并发包)提供四种线程池,分别为:

  • newCachedThreadPool创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
  • newFixedThreadPool 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。
  • newScheduledThreadPool 创建一个定长线程池,支持定时及周期性任务执行。
  • newSingleThreadExecutor 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。
  • ThreadLocal
  • 创建 :ThreadLocal<ConcurrentHashMap<String, String>> THREADLOCAL_MAP = new ThreadLocal<>();
  • 获得THREADLOCAL_MAP.get()
  • 设置参数THREADLOCAL_MAP.set()

  • Java锁机制
  • java锁机制分为synchronized和Lock。synchronized实在jvm层面上实现的,在正常执行完毕和出现异常时候会释放锁Lock是纯java代码实现,需要加finally保证unLock()的执行。
  • ReentrantLock
  • Lock有个实现类ReentrantLock,基于Aqs实现了不同的获锁方式

lock(),尝试获得锁,失败了放入Aqs的队列

trylock(),尝试获得锁,失败了直接返回

tryLock((long timeout,TimeUnit unit)尝试获得锁,失败了根据时间配置进行等待

lockInterruptibly:尝试获得锁,失败了进行休眠,休眠可以被别的线程中断

  • ReentrantLock相对稳定,在竞争比较激烈的情况下synchronized升级为重量锁,ReentrantLock锁较优
  • ReentrantLock如何实现aqs:​
  1. 线程1调用reetrantLock.lock,判断state数值,为0时cas尝试获得锁,不为0查看锁是否是当前线程,是的话可重入,state+1,否的话转为node节点放入同步队列,自行中断。
  2. 后面进来的线程2也是进行相同的操作,结果被cas放入同步队列的尾部。
  3. 线程1执行完毕后,tryRelease释放资源,将state进行减少,若是公平的:为0时调用unparkSuccessor获得继任者,则从同步队列同步取得首节点,获得资源,若是不公平的,其他线程直接进行cas抢占。
  • ReentrantLock的同步队列和等待队列:
  1. ReentrantLock同步队列是基于aqs实现的,等待队列是使用了Condition接口,两个配合实现线程之间的等待通知机制。
  2. 线程1获得锁资源,实例化condition,调用condition.await(),将自己中断,放入fifo的等待队列中,此时锁释放,线程2从同步队列中出列,获得锁资源,若他调用了condition.signal(),线程1会从等待队列中出来,放入同步队列队尾排队。这个机制可以实现完全的顺序操作。
 

public class ConditionTest {

static ReentrantLock lock = new ReentrantLock();

static Condition condition = lock.newCondition();

public static void main(String[] args) throws InterruptedException {

lock.lock();

new Thread(new SignalThread()).start();

System.out.println("主线程等待通知");

try {

condition.await();

} finally {

lock.unlock();

}

System.out.println("主线程恢复运行");

}

static class SignalThread implements Runnable {

@Override

public void run() {

lock.lock();

try {

condition.signal();

System.out.println("子线程通知");

} finally {

lock.unlock();

}

}

}

}

  • ReentrantReadWriteLock
  • 可重入读写锁, 维护了一对相关的锁,一个用于只读操作,另一个用于写入操作。只要没有 writer,读取锁可以由多个 reader 线程同时保持,而写入锁是独占的。
  • 内部实现也是lock的实现类readLock和wirteLock,也是基于aqs实现
  • aqs
  • aqs是AbstractQueuedSynchronizer抽象队列同步器,维护了一个volatie修饰得共享资源state和fifo队列,当一个线程首次访问同步资源,他会获得锁,其他资源cas发现无法获得锁,转而被构建成node节点加入队列中,加入的方式也是是cas, 等到持有资源线程释放锁,队列头部出列获得资源。 aqs还定义了独占和共享的资源占用方式,用state的数值来实现。具体实现有ReentrantLock和ReentrantReadWriteLock的readlock,wirtelock内部类。
  • synchronized原理 与 volatile原理
  • synchronized(是通过对象内部的一个叫做监视器锁(monitor)来实现的,监视器锁本质又是依赖于底层的操作系统的 Mutex Lock(互斥锁)来实现的。而操作系统实现线程之间的切换需要从用户态转换到核心态,这个成本非常高,状态之间的转换需要相对比较长的时间)同步关键字可用于修饰代码块,静态方法,实例方法,实现了原子性,可见性和有序性,原理是基于monitor对象的进入和退出来实现的。若修饰的是方法,则在进入方法时查看方法常量池中的方法结构中的acc_synchronized属性是否被设置了来区分是否是同步方法,(所有的对象头部都有锁状态标记) 是则尝试获monitor,执行方法,完成后再释放。若修饰的是同步代码块,则会使用字节码技术在同步代码快前设置monitorEnter,代码之后设置monitorexit,(此时反编译就可以看到),进入方法后,将montor对象计数器加一,出来后计数器减1,在进入之前若已经是1了,则阻塞等待。
  • 不同实例同时调用同一同步方法不会有问题,因为spring是单例的,需要竞争同一个锁
  • volatile修饰变量,保证线程间的可见性,因为他可以保证对缓存的修改立即刷入主存,并使得其他线程的缓存失效, 但是不保证原子性,因为读取,操作,更新不保证事务,能禁止指令重排序(编译器和处理器会在happen-before原则下进行重排序),所以volatile能在一定程度上保证有序性。
  • synchronized锁升级

1.6之后加入的升级过程

https://www.cnblogs.com/lwh1019/p/13093581.html

  1. 无锁:线程在循环中尝试访问并修改同一个资源,同一时间只有一个成功
  2. 偏向锁:线程cas修改对象头部的锁标志位,记录当前的线程id,执行同步代码。相同的线程再次入的时候,会直接通过不需要进行加锁操作
  3. 轻量锁:当第二个线程加入锁竞争,在全局安全点(无字节码运行)会暂停持有锁的线程,偏向锁会升级为轻量锁,没有抢到锁的线程会进行自旋
  4. 重量锁:自旋超过十次,轻量锁升级为重量锁,竞争线程被阻塞


Kafka

  • 为什么选择kafka作为消息队列
  • 选择消息队列的原因是每次消息通知的数量比较多,目前是4000+,随着业务发展可以预见未来的 数量和消息的类型还会上升,消息队列可以做到应用的解耦,异步处理流量削锋,消息持久化。对比其他mq比如rabbitmq,有则更高的吞吐量和优秀的安全性,消息是持久化的,但是也有相应的缺点,比如高度依赖zk,实现分区的有序性而全局的有序需要代码来实现,不支持事务(RabbitMQ支持,配置消费消息后才确认,从队列移除消息,检测到断开就发给其他消费者),可能会有重复消息的消费。综合起来还是选择了kafka。
  • 组成
  • Kafka将消息以topic进行归纳,以集群的方式运行,产生消息的称为producer,消费消息consumer,组成集群的称为broker。kafka消息采用的是拉模式,对比推模式,更为灵活消费者按照自己的消费能力去取得消息。
  • 生产者生产的消息以topic进行归纳,默认情况下会根据消息的key,被kafka分散存在不同的parttion中,保证顺序,每一消息都有一个ofset偏移量,相当于索引。消息是持久化的,消费者可以控制ofset来读取消息。至于消费者也有一个消费组的概念,在订阅同一个topic的情况下,同个消费者只能收到一次,不同的则都能收到,很灵活。为了保证高可用,partion会进行备份。
  • kafka是分布式的,不同的broker以为zk来联通。
  • 再均衡是怎么样的
  • 按照kafka的设计,一个topic的一个分区会被消费组中的一个消费者消费,看做一一对应的关系,但是4个改变会改变其对应,称之为再均衡: 同个消费组消费者的增加和删除,分区数量的变化(减少不过不支持),topic主题的新增(指的是使用了正则的topic),再均衡的过程由broker上的协调者来处理,协调者和消费者通过心跳检测来交互,当协调者检测到变化就会发送通知消费者,消费者立即提交偏移量(提交不成功可能会有重复消费的问题),发送joinGroup请求,协调者收到的第一个请求,那么请求者就是leader,接下来收集其他请求,发送joinGroup响应给消费者,learer知道自己是leader后会指定分配方案,发送sysGroup给协调者,协调者将方案分发给其他消费者
  • 高可用高性能

高可用

  • 分区备份:每个分区都有备份数据,均匀分布在不同的broker,一个为lead负责读写,剩下负责备份,备份的程度用ack参数配置。副本通过与zk保持心跳来确认为存活,长时间的心跳失败或者备份者太落后于leader,就会被当成是宕机。kafka维护了一个irs列表,保存了很接近leader数据的副本,leader挂了,就会在其中进行选举。
  • 消息持久化,持久化在分区的文件夹中,通过offset读取

高性能

  • 实现了零拷贝(Producer生产的数据持久化到broker,采用mmap文件映射,实现顺序的快速写入;

Customer从broker读取数据,采用sendfile,将磁盘文件读到OS内核缓冲区后,直接转到socket buffer进行网络发送)内存-用户空间-系统空间

  • 日志的磁盘顺序读写
  • 分区
  • 消息压缩
  • 分区规则和消费规则
  • 默认根据消息的key的hash值并发配,若为空会先在缓存中取得,在空则随机并设置缓存,缓存定时清除。我们也可以实现kafka提供的接口再实现自定义。
  • 同个消费组中监听同个topic的消费者一般不多于分区,不然会有人收不到
  • kafka comsumer消费消息后不commit分析
  • 若有三条消息,m1,m2已经被消费者拿走但是没提交,m3进来了被消费并提交,此时消费组记录的偏移量会是m3,除非在m3进来之前触发了再均衡或者重启,那么消费m1,m2会重新消费。由此引发出重复消费的问题,消息丢失问题更多的在于服务器的宕机,网络不稳定的时候出现。
  • 重复消费:消费者做处理;日志存储时间比偏移量存储时间长,唯一消费者重启后拿不到offset而获得默认值,导致重复消费,可以配置log.retention.hours(7天)和offsets.retention.minutes(一天),
  • 消息丢失:设置合理的ack;设置合理的分区备份使其分散在不同broker上
  • 重新选举出现脑裂:由zk来处理脑裂问题,在新选举的lead赋予一个更加的值,当时两个leader发送命令,只听从值大的
  • 零拷贝:
  • 使用虚拟内存的特性,第一个虚拟地址可以映射同一个物理地址。减少了用户空间和内核空间数据的拷贝。类似java中对象的引用,在使用了引用的地方对其进行修改都会修改到同一个对象。

反射机制

  • 反射就是在java运行时动态获取类的对象信息,从而操作对象的属性和方法。本质就是jvm得到class对象,对其进行反编译从而获取对象信息,因为是动态操作所以jvm没法进行优化,还一些get方法上尤其消耗性能,比如获取对象方法时,也是遍历对象的方法名字的

网络

  • 浏览器请求全程
  1. 浏览器向 DNS 服务器请求解析该 URL 中的域名所对应的 IP 地址;
  2. 解析出 IP 地址后,根据该 IP 地址和默认端口 80,和服务器建立TCP连接;
  3. 浏览器发出读取文件(URL 中域名后面部分对应的文件)的HTTP 请求,该请求报文作为 TCP 三次握手的第三个报文的数据发送给服务器;
  4. 服务器对浏览器请求作出响应,并把对应的 html 文本发送给浏览器;
  5. 释放 TCP连接;
  6. 浏览器进行html解析,请求html中静态资源
  7. 浏览器进行渲染
  • dns解析过程
  1. dns的解析过程是一个类似递归调用的过程,中间进行了缓存来减少查询
  2. 输入域名,计算机会在浏览器的缓存中查询目标ip地址
  3. 查询不到,就向系统本地缓存进行查询。操作系统也有一个域名解析的过程,此时host文件会fa发挥作
  4. 查询不到,向本地域名服务器查询,一般会有缓存会返回结果
  5. 本地域名服务器查询根域名服务器,会返回一个gtld域服务器地址
  6. 本地域名服务器查询域服务器地址,返回解析出来的name service服务器地址
  7. 本地域名服务器查询name service服务器地址,得到最终服务器的ip地址,进行缓存和使用。
  • 三次握手流程
  1. 客户端发起tcp请求,首先将同步序列编码syn(seq=j),发送到服务器端,客户端进入syn_sent状态;
  2. 服务器端接收到syn,确认后,发送syn(seq=k)+ack(ack=j+1),发送到客户端,自己进入syn_recv状态,此次自己会为客户端的syn(seq=j)维护一个未连接的队列
  3. 客户端接收到服务器的syn+ack后,发送ack(seq=k+1),发送完毕,客户端和服务器端进入连接成功状态。
  • 必须3次:前两次保证服务器可以接受客户端消息并成功响应,后面两次保证客户端可以接受到服务端消息并成功响应,少于3次不能保证通信的正常,多余浪费资源 挥手流程 客户端发送fin,服务端接收后先发送ack,并通知自己的程序,后发送fin,客户端接收到fin,发送ack,成功后连接中止
  • 必须4次:因为tcp是全双工的,断开连接需要单独断开两条通道,若第二次和三次合并,在一般情况下没啥问题但是若这个时候,但是若服务端还有没处理完的数据需要发给客户端, 那客户端直接关闭了就不行了,等待处理完再发fin。
  • 数据传输流程(http+tcp+ip)
  1. 应用层增加http协议信息
  2. 传输层增加tcp头部
  3. 网络层增加ip头部信息
  4. 网络层接口层增加以太网头部尾部信息
  5. 网络层接口接收到信息,找到mac地址判断是不是直接的包,若不是则丢弃
  6. 网络层判断是不是自己的ip地址(路由器会有一张表去判断并分发)
  7. 传输层进行数据校验,根据端口号找到处理的应用程序
  8. 应用层解析数据并处理
  • 防止攻击

明文传输会有中间人来伪造数据,或者截获数据,也会利用服务器发出握手的第二次时维护的等待队列进行洪水攻击。

  • 洪水攻击:缩短等待重试时间
  • 使用代理去承受大量请求
  • 有钱的多多部署几台服务器,增加带宽
  • 升级为https,在http上加了ssl/tsl(tsl高级),主要的改造就是通过CA认证体系发布的证书来识别彼此,后续使用加密的数据
  • http原理

  • HTTP协议是超文本传输协议,采用了请求/响应模型。客户端向服务器发送一个请求报文,请求报文包含请求行(请求的方法、URL、协议版本)、请求头部,空行,请求数据。服务器以一个状态行作为响应,响应的内容包括协议的版本、成功或者错误代码、服务器信息、响应头部和响应数据。
  • http基于tcp实现,因为tcp是端到端的可靠的网络协议,特点:灵活允许用户自定义请求信息,语言无关,无状态(也是缺点,出现了cookie),明文
  • https://developer.51cto.com/art/201906/597961.htm

NIO

  • jdk1.4引入的同步非阻塞IO,与其相对应就是阻塞的i/0,我们称之为bio。基于传统的bio阻塞模式,客户端和服务器端的通信使用socket套接字进行通信,服务器端提供ip和端口号。客户端使用这些信息发起连接请求通过tcp三次握手之后,由socket维持通信,是一应一答的模式,一个线程通过流进行数据传输,必须等到成功或者发生异常才返回,此时阻塞不做其他事情,读也一样,服务商一直等待数据。比较耗时耗性能。nio引入了channel,buffer 和多路复用器selector,服务器与客户端之间建立全双工的通道,读写之前使用缓冲区进行缓冲,线程立即返回,使用多路复用器来监视有无新的连接或者新的数据进来,也就是就绪状态的管道,我们可以用单线程启动的多路复用器同时监控多台服务器的请求,轮训出就绪状态使用的是expoll,事件驱动。
  • UNIX的io模型有阻塞,非阻塞,io复用,异步io,信号量驱动io。多路复用器selector使用的是基于epoll的io复用技术。io复用技术有select,poll和epoll。select和epoll的区别就是epoll不受到一个进程打开的文件描述符fd数量限制,仅限制于系统的最多句柄数,select默认只有1024。select会线性扫描全部的fd,效率不好,epoll是基于事件驱动,只有活跃的fd才会执行回调函数。
  • 这个最大的句柄数限制在工作中踩过坑,xxljob2.0的版本在测试环境频繁出现句柄不足的异常。查看了源码也是使用nio通讯的,但是在建立好连接之后,服务端就不再去维护socket的状态直到正常关闭,但是异常情况下客户端或者服务端掉线了,没关闭socket fd,socket fd越来越多导致后来的连接再次新建,报错。升级到高版本,在管道里加多了检测存活处理器,超过10分钟的不会活跃socket会被释放。

泛型

jdk1.5加入放入。就是在创建对象或者调用方法时候才明确参数的类型,实际上就是把参数的类型也当成是参数。一般使用在如new 数组指定数组的类型,或者是在写通用组件的时候,使用过他来写一个日志记录接口,制定了只有继承了某个基类的参数,才能调用。这样子代码健壮性比较强又避免了强制的类型转换,强制的类型转换还可能会抛 异常ClassCastException


微服务

  • CAP原则
  • 又称CAP定理,指的是在一个分布式系统中,Consistency(一致性)、 Availability(可用性)、Partition tolerance(分区容错性),三者不可兼得。
  • 一致性(C):对某个指定的客户端来说,读操作能返回最新的写操作结果
  • 可用性(A):非故障节点在合理的时间返回合理的响应
  • 分区容错性(P):分区容错性是指当网络出现分区(两个节点之间无法连通)之后,系统能否继续履行职责
  • 怎么拆分

使用DDD领取驱动设计,进行整体业务的建模,划分出不同的区域,我们对ddd的落地也走了不少弯路,最终落地成为一个设计的思想以及项目的代码结构。具体就是各个模块的职责的划分,网关,业务,基础设施。详细的子模块划分可以为以后的升级提供便利,如容器化。

  • 分布式事务的思路
  • 引入协调者,使用两段式的提交方式
  • 使用本地事务表,记录每个操作是否成功,成功就修改为成功,不成功由定时任务去重试或者回滚
  • 队列,如rabbitmq,执行完毕再去通知消息删除,失败重新分配消费者
  • 分布式锁
  • 基于数据库实现排他锁
  • 使用redis来获得一个锁
  • 基于Zookeeper实现分布式锁(队列形式?)
  • 消息队列的分布式事务是怎么实现的
  • 负载均衡算法
  • 负载均衡的算法有很多,最常见实现比较简单的就是轮训,随机,源地址哈希算法,但是由于多机环境,机器的性能并非都是一样的,机器数量也会更改,导致效果比较差,从而引出了加权轮训,加权随机,最少活跃(计数器),最少连接,一致性哈希等算法。一致性哈希算法的实现比较特别,是一个环形,先计算出hash再定位到实际的服务器节点,解决的是减少机器数量变更导致的请求地址变更,大部分请求还可以打到原来的服务器上,容错性拓展性很好。这个算法的精髓在于引入了虚拟节点解决节点分配不均的问题,其次好的hash算法也应该使得hash足够分散
  • 新表需要增加一个属性:
  • 版本号+json字段,确点是就没办法建立索引,考虑升级到mongoDB
  • 将表设计成纵向,id,key,vlaues,由来service来做屏蔽,但是列会太多
  • fegin原理
  • springboot启动的时候,会根据注解去扫描feign接口,并使用jdk的proxy类生成代理类并进行实例化,放入ioc容器中等待使用。当成接口被调用时,代理类实例去完成实际的http请求工作。调用的流程主要是:首先实现参数构造http request对象,再使用encoder将来bean转换成http报文的正文,经过拦截器进行装饰,日志记录后发送请求,与Ribbon负载均衡发起服务之间的调用,失败了会有重试,使用tcp/ip协议传输并获得应答之后返回,返回后也会经过拦截器
  • spring拦截器

拦截器示例

 

@Aspect

@Component

@Slf4j

public class ApiRequestLogMonitor {

@Pointcut("execution(* com.lumi.retail..*Controller.*(..))")

public void webLog() {

}

@Around("webLog()")

public Object doAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {

// 开始打印请求日志

ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();

if (attributes == null) {

return proceedingJoinPoint.proceed();

}

HttpServletRequest request = attributes.getRequest();

// 请求开始处理时间戳

long startTime = System.currentTimeMillis();

Object result = proceedingJoinPoint.proceed();

// 请求处理结束后时间戳

long endTime = System.currentTimeMillis();

// 打印请求相关参数

log.info("\r\n========================================== Start ==========================================" +

"\r\nURL : {}" +

"\r\nHTTP Method : {}" +

"\r\nIP : {}" +

"\r\nTime-Consuming : {} ms" +

"\r\n=========================================== End ===========================================" +

"\r\n",

request.getRequestURL().toString(), request.getMethod(), request.getRemoteAddr(), endTime - startTime);

return result;

}

}

  • springboot自动装配原理
  • springboot自动装配的思想是"约定优与配置",在springboot启动时候,启动类上的注解sprongbootApplication来启动自动装配,他有几个参数用于过滤不需要装配的bean,这个注解的上还有一个EnableAutoConfiguration注解才是自动装配的核心,这个注解通过import注解引入了AutoConfigurationImportSelector类,实现selectImports方法,他会扫描所有引入jar包中的类路径下的spring,factoris文件,得到一个map(key为接口全类名,values为对应的配置类),难免会有重复,需要进行去重,通过配置进行去重不需要的配置(注解的参数可以排除,配置类上有一个conditional注解条件)。这样子使用的时候通过传入的class名称,就可以从配置工厂通过反射实例化
  • mybatis映射原理

容器化

  • docker基础
  • k8s基础

设计模式

  • 我觉得设计模式是对业务的深入理解,提炼出来的一套耦合度低和拓展强的代码解决方案。在我业务中使用的比较多的是策略+工厂。我在负责的售后模块中就用到了,不同的售后单类型需要走得流程大致一致,每个节点都需要进行审核和审核后进入下一个节点,但是处理的细节缺是不一样的。我...最终暴露的接口就是传入售后单的类型和当前节点,调用接口就可以实现不同的处理,以后要拓展售后单类型,只需要继承公共接口并实现逻辑。此外还有责任链(在客户状态统计用到了,客户状态是层层递进的,高状态会覆盖低状态,若把每个客户的状态 放入责任链,就会形成一个倒三角的结构,满足业务想要实现客户状态转化情况的需求),阅读源码的时候还学习了一些,比如mybatis写的最好。
  1. 日志模块的适配器模式( 为上层提供统一的接口,为不同的日志组件如log4j2,commons log提供适配),
  2. 初始化时的建造者模式(初始化工作需要进行nybatis-config.xml文件解析,映射文件解析,注释解析等工作,由统一一个builer进入,按需传参,屏蔽内部构建细节。

高并发项目架构

  • 新零售系统架构设计

  • 秒杀系统架构设计
  • 全部问题都是高并发引起的问题,就秒杀系统而言
  1. 超卖:redis的lua脚本实现原子操作
  2. 前端资源显示:cdn加速
  3. 恶意请求+前端暴露接口不安全:动态url或者URL参数加盐
  4. 秒杀模块性能:nginx负载均衡,秒杀模块多实例,职责单一,引入熔断
  5. 数据库性能:redis预热,上redis主从复制哨兵,做到稳定的读写分离
  6. 秒杀后序业务:消息队列削峰

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值