问jvm相关(没啥变数,只要不打断把如下内容背下来基本就没有问题可问了)
- 先回答jvm概念:jvm是java虚拟机,有了jvm虚拟机之后成勋可以跨平台运行程序,平台差异性由jvm来解决,另外jvm识别的是class二进制文件,意味着只要支持编译成class文件的各种语言都能跑在jvm上,比如groovy、kotlin、scala等。
- 回答运行时区域:jvm在启动后运行时区域主要分为堆、方法区、虚拟机栈、本地方法栈、程序计数器,其中堆和方法区都是线程共享的,虚拟机栈、本地方法栈、程序计数器都是线程私有的,
- 程序计数器主要存放的是当前正在执行指令的地址,也是唯一不会发生内存溢出的区域。
- 本地方法栈主要存放加了native修饰的那些方法栈帧
- 虚拟机栈中主要存放栈帧,每个栈帧主要分为局部变量表、操作数栈、动态连接、返回地址等。一般一个线程对应的栈的默认大小为1mb,因此如果递归太多层容易发生栈内存溢出
- 方法区现在在jdk1.8中主要指元空间,而且内存理论上就是操作系统的剩余内存大小,当然也可以通过参数限制,主要存放类对象及元数据、运行时生成的字节码对象比如cglib代理生成的类、以及一些jsp等
- 堆是jvm中内存占用最大的一块,也是gc发生最多的区域,根据不同的gc类型可以划分为不同的部分,目前主流的还是cms和parallel以及g1。现在用的最多的还是jdk1.8所以堆内存一般划分为新生代和老年代,新生代中又划分了eden和2个survivor区。
- 回答gc: gc一般都是采用分代收集,比如新生代一般采用复制算法,即eden区满了之后触发minorgc,通过可达性分析将存活的对象移动到survivor1区,然后下次发生minorgc时将存活对象移动到survivor2区,交替执行。默认存活15次后对象晋升到老年代。如果老年代也满了就会触发full gc,老年代一般采用标记整理或者标记清除算法,fullgc对性能相比minorgc有较大影响,因此一般jvm调优的目标就是降低full gc的频率。
- 一般来说jvm调优只是最后的手段,一般考虑从编程角度或者架构设计方面来优化性能,比如最常见的oom问题。
- 过渡到内存溢出和内存泄漏
- oom常见的原因主要有栈溢出,比如写了死递归或嵌套层数过多导致,这种通过日志比较容易排查
- 还有方法区溢出,一般是程序动态生成大量对象或者cglib字节码增强或操作系统本身内存不足
- 还有一些类似于cannot open file或者socket相关错误,这种一般是系统资源耗尽或者设置的值过小
- 其他还有比如io等资源用完没有即时释放导致的内存溢出
- 除了上述比较容易排查的情况外,一般会设置启动参数,发生oom时导出堆栈dump文件,可以通过mat进行分析,一般会给一些预测性的结果,此外也可以根据对象的深堆浅堆进行分析对象的引用情况。
- 过渡到jdk命令。
- 除此外一般还有一些常用命令来分析程序运行情况
- 比如jps\jstat查看gc类加载等\jinfo查看系统程序参数及可动态修改\jstack查看线程快照信息,一般在死锁时使用
缓存穿透(通常会结合实际场景问,变数较大)
- 先说概念:缓存穿透是指访问了缓存及数据库中不存在的数据。
- 解决方案:
- 一般来说根据业务场景可以做不同的处理,比如常见的增加参数校验,对非法id进行过滤,像极少更新的数据或者对实时性要求不强的场景可以简单的将不存在的key直接缓存起来,下次查询直接返回空。
- 此外可以考虑采用布隆过滤器,布隆过滤器可以保证有的数据一定能命中,不存在的数据根据布隆过滤器空间大小存在一定的命中率,将所有合法的id放入其中,然后请求优先查询布隆过滤器,这样可以拦截绝大部分缓存穿透的情况,而redis的bitmap正好可以作为布隆过滤器。
缓存击穿
- 除了缓存穿透可能还会遇到缓存击穿的情况,这种是指某一个热点数据缓存过期导致短时间请求都打到数据库上导致的性能问题。
- 解决方案:
- 这种现象一般在缓存的有效期上下手,可以考虑根据热点时间错开过期时间,或者设置不过期或者提前预热数据等。请求量不大的情况下可以在代码层面对key加锁,确保只有一个线程进行查库操作,其他线程暂时等待。
缓存雪崩
- 缓存雪崩可以理解成缓存击穿的并发场景,处理方式类似。
双亲委派(主要问有啥好处,可能引申到tomcat打破的流程,或者其他还有哪些场景打破了,需要去了解)
- 概念:双亲委派是当类加载器加载类时,自己先看是否已有缓存,没有的话会交给父类加载器去加载,然后如果父类加载器找不到就会依次交给子类加载器加载。
- 目的:主要目的就是为了jvm的运行安全考虑,举个例子如果没有这套机制,然后string类被恶意类加载加载,在tostring方法中执行恶意代码,后果很严重。
- 除了安全问题外也能提高性能,因为类加载后会缓存起来。
说说Spring中用到的设计模式(比较开放的问题,不一定要全答,回答了一个设计模式可能会问该模式相关问题)
- 最先看到的就是模版设计模式,在abstractapplicationcontext里面的refresh方法能很好的体现出来,留了一些钩子方法等子类实现
- 另外还有很多工厂模式,比如beanfactory等
- 还有比较常见的装饰器模式的使用,很多地方都用到了wrapper包装对象,比如beanwrapper,有了wrapper之后可以对bean的属性进行读写操作,也可以做一些类型转换的工作
- 还有aop中使用的动态代理模式,将符合切点条件的bean都生成代理对象,springboot2.0版本中默认使用cglib代理。
- 像刚说到的aop中还用到了适配器模式,因为切面分了好几种比如前置后置环绕异常等,其中像前置这种没有实现methodinterceptor接口,而aop的执行的必须要实现这个接口,所以会通过适配器模式来完成前置通知功能
- 还有观察者模式,主要用在事件监听机制,比如上下文初始化之前,之后等时机,比如dubbo中service就是在finishrefresh中的事件通知中进行服务暴露的
Spring的aop是怎么实现的(主要考的是实现细节,需要看源码或流程图巩固下)
- 首先aop触发的时机是在bean实例化并且初始化后的beanpostprocess中完成的,其中会执行proxybeanpostprocessor的postprocessafterinitialization,在里面会对bean进行代理增强
- 主要流程是先会从beandefinition中找到所有加了aspect注解的类,然后会将类中的各种增强切面封装为advice,然后和切点pointcut封装为advisor对象
- 接下来就是匹配bean和切点,通过cglib字节码增强生成代理类完成切面功能
生产中用过线程池(主要考的是7个参数以及分别设置成多少合理)
- 生产中用了自定义线程池,几个比较重要的初始化参数比如核心线程数、最大线程数、最大线程过期时间以及时间单位、还有阻塞队列、如果有个性化需求可以自定义threadfactory,比如自定义线程名称等,最后还有个拒绝策略,默认用的是超过最大线程之后丢弃新任务并抛出异常,此外还有丢弃最旧任务策略、丢弃新任务不抛异常策略、还有让业务线程来跑任务策略,当然也能自定义拒绝策略。
- 我们的业务场景是消费mq数据,因为后续处理耗时比消费mq可能要慢,所以这里采用的是业务线程来跑任务,这样做的好处就是一旦后续处理不过来,可以自动降低mq消费速度,因为这个时候消费mq的线程已经用来跑任务了。
使用线程池有哪些好处
- 可以避免频繁的创建和销毁线程的流程,降低cpu开销
- 可以控制线程的并发数,避免无休止的创建线程导致内存溢出等问题
- 可以起到统一管理线程的作用,提前做好规划
mybatis中的mapper接口为什么没有实现类也能使用(主要考的是factorybean的使用,可以延伸到sqlsession的缓存失效问题)
- 首先mapper接口是有实现类的,只不过是mybatis框架帮我们生的代理类,mybatis和spring整合后是借助factorybean来帮我们生成代理类的,其中会通过一个sqlsessiontemplate的模版session,从中生成代理对象,在执行mapper方法时还会从threadlocal中判断是否有sqlsession缓存,如果没有的话就会新生成一个sqlsession。
- 这也是为什么mybatis和spring整合后如果方法没加事务会导致mybatis一级缓存失效的原因,因为一级缓存的生命周期是sqlsession,如果是新生成的sqlsession,自然一级缓存也就失效了。
spring和springboot有什么区别(主要考的是springboot的特性-自动装配,最好说出springboot的启动流程)
- Spring是一个基础框架,主要提供ioc和aop功能,帮助我们管理实例的生命周期以及初始化工作,同时也提供了大量的扩展点
- springboot是基于spring的一个框架,主要作用是提供了自动装配功能,免去了spring的一些配置,采用约定大于配置的理念来实现的。
- 那么自动装配是怎么实现的呢
- 首先是把启动配置类当作参数传入springapplication.run方法,也就是加了springbootapplication注解的类,因为这个注解包含了几个很重要的注解,比如componentscan,enableAutoConfiguration等等,接下来在初始化applicationcontext上下文时会内置加载几个很重要的postprocessor,比如autowiredAnnotationBeanpostprocessor、commonAnnotationBeanpostprocesso、configurationClassPostprocessor。
- 其中最重要的就是configurationClassPostprocessor,他其实是一个factoryPostprocessor接口的实现类,也就是说他的生命周期方法在beandefinition生成前后执行,因此他承担了几个很重要的注解引入的类的加载,比如componentscan注解,enableAutoConfiguration注解,其中enableAutoConfiguration注解就是实现自动装配的关键点,这个注解import了一个autoConfigurationimportSelector类,这个类会通过spi的机制,也就是通过springFactoriesLoader去加载mete-inf目录下的spring.factories文件,然后去加载key为enableAutoConfiguration的所有value对象并解析为beandefinition,后续流程就是和以前读取xml配置文件后的流程一致了
- 主要步骤就是bean的实例化,再进行属性依赖注入,再进行beanpostprocessor的before方法,再进行自定义初始化方法,也就是afterpropertiesset方法和init方法,最后进行beanpostprocessor的after方法,aop的主要入口也在这里。
spring循环依赖
- 首先Spring为我们解决了绝大部分场景下的循环依赖,比如a类有属性b,b类有属性a这种,但是如果ab都是通过构造方法注入的话spring是解决不了的。
- 因为spring解决循环依赖主要的做法是采用三级缓存,也就是当a类在实例化后会将a的objectFactory加入到3级缓存,接下来ioc注入b,这时发现b还没有实例化,又会进行b的实例化并加入3级缓存,接下来b又开始注入a,这个时候就能从3级缓存中拿到a的半成品,这个时候a只是刚完成实例化,这样b就能完成整个bean的创建及初始化,完成后b后再回到a的整个bean的初始化,这样就解决了循环依赖,这也是为什么不支持构造方法之间的相互依赖。
- 按理说二级缓存就能解决循环依赖问题,为啥需要三级缓存
- 因为还要考虑到支持aop,在三级缓存中放的是a的objectFactory,如果只用2级缓存,那么意味着这个时候必须先把a生成代理类后放入2级缓存,再注入到b,这就与spring设计的bean初始化的流程违背了,因为spring设计的代理生成是在bean完成实例化并且完成初始化方法之后进行的,而循环依赖只是一种特殊情况,所以不得已在注入时先对a生成了代理类。
zk惊群效应怎么解决
- 一般我们实现分布式锁时可能遇见这种情况,也就是当资源释放时,将会唤醒所有等待资源的线程,但最终也只有一个线程能获取资源,非常容易造成网络冲击以及资源消耗。
- 所以一般都会在实现分布式锁时都只会监听前一个节点的状态,这样也就相当于每次资源释放时只会唤醒一个下一个节点,避免了全部唤醒。
项目中用了哪些设计模式
- 常见的如模版模式,一般会先写好主要的业务流程,然后再填充
- 代理模式,也就是借助spring的aop来实现的一些切面,另外有通过factorybean的方式代理生成rpc调用
- 策略模式,比较常见的就是通过Spring的特性,在注入属性时注入Map类型,这样可以把同接口的不同逻辑的实现类全部注入,然后再根据类似于type参数来确定调用哪个实现类。
kafka的消息流转过程
- 首先kafka的消息可以批量写入,然后消息是以topic作为区分队列的,然后每个topic可以分为多个分区,每个分区相当于一个文件,默认情况是将消息轮询的发到不同的分区,然后消费者可以以分组的形式去重复消费同一个topic,同一个group下的消费者可以一个消费者对应多个分区,但是一个分区在同一个group下只能由一个消费者消费。
- 另外kafka中采用偏移量的概念来确定消息是否消费,默认情况是采用自动提交,5秒钟消费者提交一次偏移量到一个指定的topic,证明这一段偏移量的数据已消费完成,也可以采用同步提交或者异步提交的方式
redis底层数据结构优化
- 首先string这种类型在底层采用了sds数据类型来保存,他相比于c中的string来说多记录了字符串的长度而且预先分配了额外空间,所以在修改或者读取时都有时间上的优势,提高了读写速度。
- 另外像hash这种数据结构redis在扩容时做了优化,不是一次性完成扩容,这样在key很多的场景下对性能影响较大,而是采用渐进式扩容,也就是新建一个hash,将新数据放入新hash,查询的时候可能需要查询2次。
- 还有就是在有序集合的底层采用了跳表来加快查找速度,跳表就是有多层的链表结构,每次从顶层开始找,依次向后向下开始找,然后插入的时候会随机将插入值升级到上一级,所以从概率的角度来看跳表的每一层链表都是下一层的一半,虽然需要冗余一些数据,但是性能也就跟平衡二叉树差不多了,而且实现和理解却要简单不少。
- 其中有序集合zset中元素个数小于阈值且元素长度小于阈值的时候用的是ziplist,否则用的是hash+跳表实现
- 还有对整数类型的集合进行了不同长度类型的区分,节省了内存
- 底层还用了压缩列表的数据结构,他保存了每个元素的长度,可以理解成让数组中每个元素按实际长度存储,主要用在list和hash上,可以节省内存
redis的zset如果要先按分数排序,同分数按时间靠前的排序怎么做
- 因为zset类型会关联一个score字段,可以考虑拆分分数,把高位作为分数,低位作为时间戳,而且是比如用2099年的时间戳-当前时间戳,这样就是时间越靠前差值越大就排在前面,
- 其中有个问题,因为分数超过16位整数精度会丢失,所以时间戳不能太长,用秒级10位应该够用了,并且要和分数综合考虑下高位和地位的分割。
单例模式有哪几种,哪些是线程安全的
- 单例模式主要有懒汉式和饿汉式,饿汉式就是在类加载后就完成初始化了,懒汉式则要等到使用时才进行初始化
- 饿汉式是线程安全的,懒汉式如果采用内部静态类来实现也是线程安全的,由jdk的语言特性来确保加载完成前其他线程处于阻塞,或者双重锁检查并且加上volatile来禁止初始化时指令重排序也可以做到线程安全,最后枚举也能实现线程安全的单例模式。
讲一讲线程安全(volatile,threadlocal,cas,lock,aqs等)
- 一般从代码角度来讲,应尽量避免在单例中使用成员属性,容易导致数据异常,非要使用的话可以考虑加volatile关键字来保证其可见性,但是并不能保证其并发修改安全
- 此外如果是线程独有的数据可以考虑使用threadlocal来维护线程私有的属性,因为每个线程中都包含了一个map对象,key就是threadlocald对象,value就是业务数据。所以threadlocal能保证线程安全,但是他也容易造成内存泄漏,所以我们在使用完成后应该及时手动remove其中的数据。
- 如果说确实存在需要争抢的资源,如果是简单的数值类型也可以考虑使用atomicInteger等原子类数据,他主要通过cas操作来保证一定成功,主要思想就是每次修改时都会传入修改后的值和修改前的值,先比较修改前的值和内存中的值是否一致,一致则修改,否则取出新的值重新计算,整个过程没有加锁但是也耗费cpu性能,所以使用场景有限
- 一般生产中用的最多的还是使用锁来保证线程安全,主要有synchronized和lock
synchronized与lock理解
- synchronized是jdk的语言特性,封装了加锁释放锁的细节,使用起来简单,而且因为是语言特性所以如果后期优化还能享受这块的福利,但是也存在不灵活性,比如一旦没抢到锁就只能等待。
- lock实现的锁就比较灵活了,有trylock这种尝试获取锁,没拿到锁还可以做一些其他业务逻辑,另外还支持过期时间以及可中断锁,此外还支持公平锁,读写锁等,并且是基于aqs实现的,性能也不错
- 可能会追问aqs了
- aqs的数据结构主要是一个双向链表,然后空表头,后面挂的节点就是一个个线程,为了防止惊群效应,这里设计的是每个节点仅监听上一个节点的状态位,并且持有资源的线程释放锁后只会唤醒下一个节点。如果是非公平锁那么就是在加入队列之前会先去尝试拿锁,这样效率更高,因为cas操作的开销远远低于唤醒线程
juc工具包用了哪些
- 常见的比如原子类atomicInteger等
- 还有读写锁、可重入锁等
- 还有一些工具类比如countdownlatch、信号量、cyclebarrier
- countdownlatch可以用来实现阻塞通知,而且是一次性的
- 信号量主要用来做流控的,但是要注意他不会对令牌数做限制,也就是可以放入比初始化更多的令牌
- cyclebarrier可以用来做合并计算的场景,可以重复使用
- 此外还有线程池(上面有了线程池就不再复述了)
mysql的引擎,索引分类
- mysql目前用的版本5.7存储引擎默认为innodb,还有myisam、memory等等,innodb不仅支持事务还支持行锁,所以一般用的都是这个。
- innodb的数据结构使用的是b+树,特点是每个节点可以保存很多索引,默认是4kb大小,所以树的高度比较低,另外数据仅放在叶子结点,所以每次查找速度相比于b树结构来说比较稳定。
- 其中索引还分为聚簇索引和非聚簇索引,聚簇索引指的就是以主键作为索引,或者唯一索引,如果你不建唯一索引还会自动生成隐式唯一主键,然后每一个主键对应的行数据都放在叶子结点,因此通过走主键索引查询效率比较高
- 此外还有非聚簇索引,一般指普通索引,这种叶子结点存放的是主键索引对应的值,需要先通过普通索引找到主键的值,再通过聚簇索引查到对应的行数据,因此效率比聚簇索引低,当然比全表扫描块
索引失效
- 所以一般开发中都要注意避免索引失效,常见的有like关键字需要以明确信息开头,不能开头用通配符,另外where条件中用or可能导致失效,可以考虑用union或者in来代替,再比如索引字段的类型不能改变,比如字符串类型字段不能用数字类型查,另外值也不能进行加减乘除等类似操作,还有范围条件查询最好放最后,否则后面的索引可能会失效
mysql事务
- mysql事务一般说的指当用innodb引擎下的事务,一般分为四种隔离级别
- 读未提交:这种是隔离级别最低的,意思是a事务能读到b未提交的事务,可能造成脏读、幻读、不可重复读问题,一般生产不使用
- 还有读已提交:意思是a事务可以读到b事务已提交的数据,但依然存在幻读和不可重复读
- 另外还有可重复读:意思是a事务第一次查询后,后续都是快照读,因此可以解决不可重复读,但是仍然存在幻读的可能,所以一般当我们需要在一个事物内先查询后修改数据时一般都会采select for update这种当前读模式锁住指定的行数据,另外mysql默认就是这个隔离级别。
- 最后一种就是串行化,不会发生上述情况,但是性能也很低,生产几乎不会使用。
数据分库分表
- 通常需要根据业务情况来进行合适的水平拆分或者垂直拆分
- 常用的比如可以根据日期来分表、根据租户来分表
- 另外比如将订单表垂直拆分为主表和附表
mycat使用(待补充)
mysql的char和varchar的区别
- char都是定长的字符串,就是在建表时就确定了这个字段的字节长度,假设10个字节长度,只存入了6个,则会在左边补位,所以空间占用都是连续不会发生变化,因此性能较好,
- 一般使用场景是长度固定或者长度变化范围很小的字段,或者简单字符类似枚举值,还有像很短的长度这些场景比较合适,
- varchar是变长的字符串,除了会存储字段本身外还会存储字段的长度,假设建表时设置10个字节长度,但是只存入了5个字节,那么实际使用的是6个字节,因为还需要额外一个字节来表示长度。这种比较省空间,
- 一般使用场景为该字段最大长度远大于平均长度,或者更新比较少也就是很少产生空间碎片,再比如存放了类似utf-8这种变长复杂的字符集
统计文档每个字符出现的次数并排序
- hash+list
- hash+优先队列,自定义排序规则
- hash+桶排序
dubbo中用的协议,协议特点
- dubbo支持很多通讯协议,比如dubbo协议、rmi协议、hessian协议、webservice、http、restful等等,最新版本也支持了grpc协议
- 目前常用的还是dubbo协议,因为他是基于tcp之上的协议,比较精简,而且使用的是nio模型,因此特别适用于多个消费者少量服务者,并且报文较小的场景,一般来说绝大部分场景都适用于这种。
dubbo中用的序列化,序列化特点
- dubbo本身支持hessian2、json、xml、jdk序列化
- 其中默认走dubbo协议,默认hessian2序列化
- hessian2序列化主要有跨平台、二进制、体积小的有点
- 此外也可以使用像protobuf、thrift等
redis红锁
- 这个是redis官方提出的一个概念,主要指的是几个独立的redis节点,当要进行上锁时,必须超过半数的节点均获取锁成功才算成功,他是依次向节点获取锁并记录获取时间,并计入过期时间内
- 红锁主要用来解决redis主从切换时,主从之间数据异步同步导致重复上锁的场景问题。
zuul和gateway区别
- zuul是spring cloud早期集成的一个网关组件,他是基于servlet容器实现的,采用bio模型,提供了动态路由、安全认证、性能检测等功能,主要是通过filter来实现的,另外也支持集成hystix实现服务降级等功能
- gateway是spring团队基于netty框架开发的,采用的是nio模型,因此性能比较高,由于是依赖spring-webflux开发的,所以也只适用于springcloud环境中,此外本身支持限流、断言、过滤、以及websocket协议
分布式id有哪些方式实现
- 常见的有uuid、redis自增、数据库自增id、雪花算法、号段模式
雪花算法有什么缺点
- 雪花算法的原理就是把一个长整型数字根据位划分为几块,每块表示不同含义,通常包括时间戳、机器码、序列号等,这样就能确保在分布式场景下生成不同并且是递增的id
- 但是雪花算法强依赖系统时钟,一旦发生时钟回拨可能导致id重复,可以考虑等待时间追回或者记录下最后一个id,然后递增序列号等,具体根据实际场景来处理,
网关限流方式有哪些
- 网关限流通常采用两种方式
- 令牌桶,就是在固定时间内生成一定的令牌,每笔请求消耗一个令牌,超过了则拒绝请求
- 漏桶,指的是固定桶容量,每笔请求占一个位置,超过容量则拒绝,并且放行的速度是恒定的
redis锁续期
- 锁续期指的是当使用redis锁时,通常都会给锁设置有效期,防止异常情况锁无法释放,但是也存在有效期到了之后仍然没有完成业务逻辑,还不能释放锁。所以此时需要进行锁续期。
- 通常我们使用redisson封装好的lock来实现redis分布式锁,他提供了锁续期功能,主要是采用看门狗机制,即当获取到锁之后会隔10s检测锁是否持有,如果仍然持有则延长锁时间。
synchronized锁升级过程
- synchronized在jdk1.8中已经摆脱了性能低下的标签,因为他采用了锁升级的机制,也就是不同场景使用不同的锁机制,
- 比如当一个锁总是一个线程获取,这时为偏向锁,会在mark word进行标记,此时相当于无锁化
- 当有第二个线程来获取锁时会检测到标记为偏向锁,此时会等到安全时期暂停第一个线程并将锁升级到轻量级锁
- 接着第二个线程会判断当前锁是否释放,如果第一个已经释放了则第二个通过cas获取锁,如果锁已经存在了则膨胀为重量级锁
- 将第二个线程加入等待队列并park,等待上个节点做unpark
当有事情需要其他组协助时该怎么沟通才能完成(套话)
- 拉群,加上领导,然后群里确定时间,无法确定时间的则确定什么时候能确定时间
- 如遇到装死的则召开会议,并输出会议纪要
- 与对方领导沟通
jump原因(套话)
- 技术栈
- 待遇
学习途径(套话)
- 书籍:相对来说比较权威而且全面,但是缓慢且容易过时
- 个大博客,类似csdn、博客园、掘金、github、简书等,适合了解新鲜知识及入门,但深度相对较浅且容易良莠不齐
- 官网:最权威,适合深入学习
- 视频:相对来说容易上手和理解,但速度较慢且视频质量参差不齐
还有些套路的,只列标题