java 分享
java 分享
0.编码之外
- 把事情想明白,说清楚,跟别人商量好,写好文档
- 工作中做好计划和进度跟踪,及时沟通和汇报,不把问题遗留到变成事故
1.java基础题
String和StringBuffer,StringBuilder的区别
String:字符串常量
StringBuffer:字符串变量(线程安全)
StringBuilder:字符串变量(线程不安全)
2.集合
3.锁相关
聊聊AQS
1.AQS锁的三大核心
- 自旋: 获取锁失败的线程会通过自旋的方式重复获取锁,自旋也可以理解为死循环
- lockSupport:lockSupport解决的问题是线程自旋过程中浪费cpu资源的问题,他的原理是阻塞当前自旋线程,直到被唤醒后再次自旋获取锁。
- CAS
2.AQS的核心思想
锁,实际上是排队,依次对资源进行占用。
AQS围绕着对资源的占用和释放进行操作,
如果某个线程占用了资源,那么他就可以工作(剔除出等待队列),其他线程就加入等待队列;
如果线程释放了资源,那么就应该唤醒其他线程(分两种:头部线程、所有线程)
4.多线程与并发
说说线程池的底层工作原理?
说说java内存模型?
各种线程池在什么场景下使用?
- FixedThreadPool
负载比较重要,而且负载比较稳定的场景
比如后台系统,每分钟执行几百个复杂大SQL,用FixedThreadPool比较合适
如果瞬间涌入大量请求,固定线程处理不过来,直接无限加入内存中排队,导致内存溢出OOM死了
- CachedThreadPool
使用场景
1.耗时较短的任务。2.任务处理速度 > 任务提交速度
每天大部分负载很低,CachedThreadPool用少量线程就可以满足负载,用FixedThreadPool就浪费了,不会给系统引入太大压力;但是每天如果有少量高峰期,比如早上或者晚上,高峰期可能需要一下子几百个线程出来,那么CachedThreadPool可以满足这个场景,高峰期过去之后,线程处于空闲状态超过60s,自动回收空闲线程,避免给系统带来过大负载
如果线程数量是在太多了,会导致CPU负载过高,堆内存溢出
如何设置线程池参数?
- corePoolSize
算一下每个任务需要耗费多少时间,比如一个任务大概100ms,那么每个线程每秒可以处理10个任务。
再算算每秒需要处理多少个任务,比如200个任务,就需要20个线程。但是一般会比预估的多设置一些,比如30个
- maxmumPoolSize
对cpu负载的影响,如果流量瞬间高峰期,线程池一下子创建上万个线程,CPU瞬间打满
需要设置一个cpu能够负载的最大线程数
一个经验值:4核8G虚拟机,线程池启动100个线程就差不多了,如果同时有100个线程,而且做频繁操作,CPU很快就会达到 80 90
- workQueue
对内存的影响,在FixedPool的参数中,对于workQueue默认是无界队列,无限排队,容易把内存吃完,机器搞死,换成 ArrayBlockingQueue 有界队列,设置一个最大长度。一旦超出了这个最大长度,就让handler去处理,对handler接口实现自己的逻辑,比如把数据放到数据库或者redis中,做个离线存储什么的。
线程池关闭原理
- shutdown()
调用之后不允许提交新的任务了,所有调用之前的任务都会提交,等所有任务执行完成,才会真正关闭线程池
这是线程池优雅关闭方式
- shutdownNow()
返回还没执行的task列表,不让等待的task执行,尝试停止正在执行的task,非优雅关闭,是强制关闭
说说可见性,原子性,有序性?
能从java底层角度聊聊volatile关键字的原理吗?
从内存模型开始讲起,原子性,可见性,有序性的理解,再讲volatile关键字的原理。
volatile关键字是用来解决可见性和有序性的,但是不能保证原子性
进程之间是如何通信的?线程之间又是如何切换的?
进程之间通信方式有:管道、命名管道、消息队列、共享内存
线程之间的切换:时间片算法,cpu给每个线程一个时间片来执行,时间片结束后,保存这个线程的状态,切换到下一个线程去执行。所谓的多线程并发执行,就是多线程来回切换,每个线程就一个时间片里执行
CAS的2个缺点
CAS是一个乐观锁
CAS会操作3个数字,当前内存中的值,旧的预期值,新的修改值,只有当旧的预期值跟内存中的值一样的时候,才会将内存中的值修改为新的修改值。举个例子吧,比如int a = 3,这是内存中的当前值,然后你CAS(3, 5),第一个是旧的预期值,如果3和a是一样的,那么就将a修改为5
public final int incrementAndGet() {
for (;;) {
int current = get(); // 先拿到i当前的值,0
int next = current + 1; // 对i加1 -> 1
if (compareAndSet(current, next))
return next;
}
}
- ABA问题。
如果某个值一开始是A,后来变成了B,然后又变成了A,你本来期望的是值如果是第一个A才会设置新值,结果第二个A一比较也ok,也设置了新值,跟期望是不符合的。
假设一开始变量i = 1,你先获取这个i的值是1,然后累加了1,变成了2
但是在此期间,别的线程将i -> 1 -> 2 -> 3 -> 1
这个期间,这个值是被人改过的,只不过最后将这个值改成了跟你最早看到的值一样的值
结果你后来去compareAndSet的时候,会发现这个i还是1,就将它设置成了2,就设置成功了
- 无限循环问题
看源码可以知道,Atomic类设置值的时候,会进入无限循环,只要不成功,就不停循环,这个在高并发中修改时,会比较频繁出现
5.jvm与性能
编译期与运行期
https://www.cnblogs.com/myitnews/p/11457649.html
- 编译期
把源码交给编译器编译成计算机可以执行文件的过程。
在java中,把java代码编译为class文件的过程,就是编译期,还没有把代码放进 内存中执行起来
- 运行期
把编译后的文件交给计算机执行,直到程序运行结束。
所谓的 运行期,就是把磁盘中的代码放到内存中执行起来,在java中,把磁盘中的代码放到内存中就是类加载的过程,类加载是运行期的开始部分
加载->验证->准备->解析->初始化->使用->卸载
- 我们平时写的java代码,是如何运行起来的?
我们写好java代码后,会通过java编译器,编译.class文件
类加载器把编译好的.class字节码加载到JVM中,JVM会基于字节码执行引擎,来执行加载到内存中我们写好的类。
这过程可以分为:加载验证准备解析初始化使用卸载
区分常量池、运行时常量池、字符串常量池、class常量池:
https://www.processon.com/view/5f45d1d9e0b34d638e07816d
class文件常量池存储的是当class文件被java虚拟机加载进来后存放在方法区的一些字面量和符号引用,字面量包括字符串,基本类型的常量。
运行时常量池是当class文件被加载完成后,java虚拟机会将class文件常量池里的内容转移到运行时常量池里,在class文件常量池的符号引用有一部分是会被转变为直接引用的,比如说类的静态方法或私有方法,实例构造方法,父类方法,这是因为这些方法不能被重写其他版本,所以能在加载的时候就可以将符号引用转变为直接引用,而其他的一些方法是在这个方法被第一次调用的时候才会将符号引用转变为直接引用的。
双亲委派机制
简单说,就是先由父加载器加载,再由子加载器加载
如果线上服务器CPU使用达到了100%了,如何排查、定位和解决问题?(高负载问题)
核心思路:找到这台服务器上,哪个进程的哪个线程的哪段代码,导致cpu 100了。(考察是否熟练运用线上命令)
步骤:
1.定位耗费CPU的进程
top -c 显示进程列表,输入p,按照cpu使用率排序。
2.定位耗费cpu的线程
top -Hp 43987,输入那个进程的id,输入p,按照cpu使用率排序,看到这个进程里的哪个线程耗费cpu最高
3.定位那段代码导致的cpu过高
pringf “%x\n” 16872, 把线程pid转换为16进制,比如 41e8,
jstack 43987 | grep ‘0x41e8’ -C5 –color
这个就是用jstack打印进程的堆栈信息,而且通过grep那个线程的16进制的pid,找到那个线程相关东西。
可以从打印出来的代码中,看到是哪个类的哪个方法导致cpu 100% 的问题
JVM问题
- 常见的有JVM问题: 内存泄露,内存溢出,线程卡死,频繁gc,jvm崩溃
- 面试题连环炮
jvm栈在哪些情况下会溢出?java堆在哪些情况下会溢出?做过哪些jvm优化?用了哪些方法?达到了什么样的效果?jvm问题(内存泄露、线程卡死、jvm崩溃、内存溢出、频繁gc),该如何定位和排查(jmap和jstack)?
垃圾收集算法有哪些?垃圾收集器有哪些?说说他们的特点
标记清除、标记整理算法,会比复制算法慢10倍以上
新生代中,每次收集都会有99%的对象被回收,可以选择复制算法,只需要付出少量对象的复制成本就可以完成每次垃圾收集。
对JVM优化,就是尽可能让对象都在新生代里分配和回收,尽量别让太多对象频繁进入老年代,避免频繁对老年代进行垃圾回收(Full GC)。同时给系统充足内存大小,避免新生代频繁的进行垃圾回收
收集算法是内存回收的方法论,垃圾收集器是内存回收的具体实现
https://www.processon.com/view/link/5fdafcf41e085304fb7bdbf8
可达性分析算法
可达性分析算法的思想:从一个被称为GC Roots的对象开始向下搜索,如果一个对象到GC Roots没有任何引用链相连时,则说明此对象不可用。
哪些可以成为GC Roots?
方法的局部变量和类的静态变量是GC Roots.
1.虚拟机栈 变量
2.静态变量引用的对象
3.常量池中的对象
4.本地方法栈(Native方法)里面的引用对象
什么时候进行FullGC?
对老年代触发垃圾回收的时机有两个:
1.在Minor GC之前,一通检查发现很可能Minor GC之后要进入老年代的对象太多了,老年代放不下,此时需
要提前触发Full GC然后再带着进行Minor GC
2.在Minor GC之后,发现剩余对象太多放入老年代都放不下了
parnew+cms的gc,如何保证只做ygc,jvm参数如何配置?
1.加大分代年龄,比如默认15加到30; (可能降低)
1.给jvm 堆加一些内存,从原来的4G加到8G
2.修改新生代老年代比例,比如新生代老年代比例改成2:1 (默认为1:2)
3.修改e区和s区比例,比如改成6:2:2 (默认为 8:1:1)
我们自己内部服务已经做到fullgc次数为0,只做ygc
这里的关键点就是必须让Survivor区放下
基本解决fullGC频繁的基本思想
避免年轻代对象进入老年代:
- 新生代gc过后存活对象过多无法放入Survivor区域
- 动态年龄判定规则
ParNew + CMS的组合让我们有哪些痛点?
-XX:+UseParNewGC -XX:+UseConcMarkSweepGC
Stop the World
平时GC是由于内存泄露而导致FullGC的,平时订单量也就每秒不到10个,负载低,JVM本身没啥好调优的,就是一些定时任务,或者上传下载需要注意点。
超大内存使用 G1收集器,普通 4核8G,8核16G 基本上是用 ParNew + CMS组合
G1很适合大内存机器,因为比如你给JVM分配32G内存,要是用ParNew+CMS,每次gc都是内存快满了,此时一下子要回收对象太多了,就会导致gc停顿时间很长,所以针对那种大内存机器,用G1是很合适的
6.中间件:kafka、rocketMQ 、redis、elasticsearch
6.1 kafka
- 略
6.2 RocketMQ
你了解RocketMQ对分布式事务支持的底层原理吗?
如果没有RocketMQ,那你们如何实现最终一致性?
可以自己写一个基于数据库的可靠消息服务组件,实现以下功能:
1.接收 业务方produce 发送的half message,返回响应 message success
2.produce执行本地事务,接着发送commit/rollback给可靠消息服务
3.可靠消息服务启动一个后台线程定时扫描本地数据表中所有half message,超过一定时间没commit/rollback就回调produce,确认本地事务是否成功,获取commit/rollback
4.如果消息被rollback就废弃掉,如果消息被commit就发送这个消息给下游服务,或者发送到消息中间件,然后由下游服务消费,必须回调可靠消息服务进行ACK
5.如果一段时间没有收到ack,则重发消息给下游服务
6.3 redis
什么是缓存雪崩,有什么解决方案?
缓存在同一时间大面积失效,后面的请求直接落到数据库上了,造成数据库在短时间内承受大量请求。
比如系统的缓存模块出了问题,宕机不可用。造成系统所有访问要走数据库
解决方案:
- 采用redis集群,避免单机出问题,导致整个服务都没办法使用
- 限流,避免同时处理大量请求
- 随机设置失效时间,不要让缓存同时失效
- 对于一些变化不多的数据,设置缓存永不失效
什么是缓存穿透,有什么解决方案?
缓存穿透就是指大量请求的key不存在缓存中,导致请求直接到了数据库上,根本没有经过缓存这一层。
这里有2类场景:
1.黑客恶意请求,请求了大量无效的key,不论是缓存还是数据库中都查不到
2.突发事件,或者突然商品火爆,总之是无法预料到的事情。这个是需要弄一个探测生成热点key的
解决方案:
- 布隆过滤器
所有可能存在的数据哈希到一个足够大的bitmap中,一个一定不存在的数据会被 这个bitmap拦截掉,从而避免了对底层存储系统的查询压力
- 京东热点探测器,探测key,直接存放到系统内存中,而不是redis
什么是缓存击穿,有什么解决方案?
针对某一个key,已经过期了,但是在某些时间点被超高并发地访问,这些请求发现缓存过期一般都会从后端DB加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把后端DB压垮。
解决方案:
- 热点探测
- 永不过期
- 设置一个互斥锁,在锁中放到redis中
7.docker与k8s
8.MySQL与SQL调优
常见面试题
数据库锁有哪些?锁是如何实现的MySQL行锁有哪些?一定会锁定指定的行吗?为什么?悲观锁和乐观锁是什么?使用场景是什么?MySQL死锁原理以及如何定位和解决?
MySQL分为 表锁、行锁、页锁
innodb的行锁有共享锁(S)和排他锁(X)
共享锁(S): 多个事务可以加共享锁读同一行数据,但是别的事务不能写这行数据
排他锁(X): 一个事务可以读写这行数据,其他事务不能读和写
todo: https://zhuanlan.zhihu.com/p/29150809/
经典问题
如果你的业务,日订单量40W,那么一个月就差不多1200W了,你对于这种千万级别的大表, 查询性能怎么样?如何分表?按什么字段分?是否建立索引映射表?跨库跨表的分页查询又该怎么做?选用什么数据库中间件(说说原理)?然后说说你如何做数据迁移的?完事了之后,未来如果又需要扩容,你怎么做?
9.分布式经典问题
10.设计模式与DDD
11.系统设计
说说你们一个服务从开发到上线,服务注册、网关路由、服务调用的流程?
画一下你们系统的整体架构图,说说各个服务在生产环境怎么部署的?
大部分的系统,高峰期每秒几百个请求,平时也就几十个请求。
每个服务两台机器部署,机器也就4核8G,每台机器每秒抗几百请求没啥问题
中小型的系统,拆分为10-20个微服务,每个服务基本上都是2台的,保证基本的可用性
注册中心:部署2台机器,4核8G,高可用冗余,任何一台机器挂掉了都不会影响系统运行
做了一些调优,服务上线,注册表多级缓存1秒同步,注册表拉取频率降低为1秒
服务心跳,1秒上报1次
服务发现,1秒钟检查1次心跳,如果发现2秒内服务没上报心跳,就认为是故障了
网关系统,4核8G的机器,一台机器每秒扛几百请求,多部署几台,大概3-4台,保证网关系统每台机器压力比较小,进一步保证网关的可靠性
数据库,mysql 16核32G,物理机最佳。每秒钟扛个几千请求问题不大,平时扛个几十到几百请求,到了三四千请求,也只是导致MySQL机器的负载很高,COU使用率、磁盘IO负载,网络负载都比较高
你们系统每天有多大访问量?每个服务高峰QPS有多少?压测过服务最大QPS吗?
每个服务每天多少请求量,高峰期每秒钟多少请求量,可以在代码中加一些metrics代码
在我负责的核心服务,核心接口中,开发一个简单的metric统计机制
弄个AtomicLong做计数,计算出各种请求量,每秒请求量,成功次数,失败次数,对关注的接口统计出来,每分钟每个接口被访问的次数打印到日志文件中去
计算下每个接口从请求到执行完毕,需要耗费多长时间,算一下每个接口平均的请求延时TP99,TP95,TP90,TP50
如果99%的请求耗费时间在100ms内,但是1%的请求可能耗费在100ms之上
TP99=100ms
平均响应延时:可以把每次调用的耗时跟历史总耗时加起来,除以当前请求次数即可
如果访问量扩大10倍,你们考虑过系统扩容方案吗?
网关和服务:横向扩容
数据库和配置中心:提高配置
1.网关层:直接扩容10倍机器即可,nginx做好负载均衡把流量均匀分布到各个网关机器
2.各个服务扩容:多加机器,部署启动,启动后自动注册到注册中心去,其他服务会自动感知到
3.eureka:服务实例变多了10倍,此时几十个服务实例变成几百个服务实例,对eureka机器会造成几百个请求。
由于eureka的架构,即使加了服务也会同步所有的请求,所以需要横向提高配置来扩容,比如弄个8核16G的配置,单机扛上千请求很轻松
4.数据库:本来每秒钟几百个请求,10倍后,每秒高峰期的请求是三四千请求,如果横向扩容比较麻烦,可以考虑给单个数据库部署的机器提高配置,32核64G高配物理机,每秒扛几千个请求没有问题
如果数据暴增10倍,会产生什么问题?怎么解决?
1.数据库承载的数据量
2.数据库数据处理能力
你们生产上是怎么配置超时和重试机制的
每个服务第一次被请求是,都会去初始化一个ribbon主键,初始化这些组件需要耗费一定的时间,会很容易导致timeout。让每个服务启动时候直接初始化Ribbon相关主键,避免第一次请求时候初始化
ribbon:
egare-load:
enable: true
服务注册中心面试题
常见的服务注册中心有哪些? 他们之间的区别在哪?你们当时怎么选的?这么选的理由是什么呢?你们生产环境中的服务注册中心是怎么部署的?你怎么考虑高可用?
Eureka、zeekeper
Dubbo作为服务框架的,一般使用zk
springcloud作为服务框架爱的,一般服务注册中心选择eureka
(1)原理
eureka: peer-to-peer,部署一个集群,但是集群里每个机器的地位是对等的,各个服务可以向任何一个eureka实例服务注册和服务发现,集群里任何一个eureka实例接收到写请求后,会自动同步给其他所有的eureka实例
zookeeper:服务注册和发现原理,leader+follower两种角色,只有leader可以写,也就是服务注册,它可以把数据同步给follower,读的时候leader/follower都可以读
(2)一致性保障: CP/AP
CAP:C 一致性,A 可用性 P 分区容错性
CP: 牺牲可用性来保证一致性和分区容错性,zk
AP: 短暂牺牲一致性来保证可用性和分区容错性,能保证最终一致性,eureka
zk:有一个leader节点接受数据,然后同步到follower,一旦leader挂了,将进行重新选举leader,这个过程中为了保证一致性,就牺牲可用性了,会有一段时间不可用,在这段时间里,leader选举好了,数据也同步好了
eureka:peer模式,可能数据还没有同步过去,自己就挂了,此时还是可以从别的机器上拉取注册表,但是看到的不是最新的数据了,但是保证了可用性,以及能够保证最终一致性
(3)服务注册发现的时效性
zk:时效性秒级感知
eureka:默认配置比较差,服务发现感知要几十秒,甚至分钟级别
(4)容量
ZK:不适合大规模的服务实例,因为服务上下线时候,需要瞬间推送数据到所有其他服务实例,所以一旦服务规模太大,到了几千个服务实例时候,会导致网络带宽被大量占用
eureka:也很难支撑大规模的服务实例,因为每个eureka实例都要接受所有的请求,实例多了压力太大,扛不住,也很难到几钱服务实例
服务注册和发现时效性有多高,有没有做相关优化?
eureka默认时效性比较慢。
慢的点在于:
1.ReadOnly缓存与ReadWriter缓存同步间隔,默认30s,可以调整为3s
2.服务发现,拉取ReadOnly缓存间隔,默认30s,可以调整为3s,尽快拉取服务注册表
3.心跳发送间隔,服务挂掉后应该尽快感知到。心跳间隔默认30s,设置为3s
4.心跳检测,默认60s,修改时为6s
如果需要部署上万服务实例,现有的服务注册中心能否抗住?如何优化?
模仿kafka架构
网关面试题
说说生产环境下,你们是怎么实现网关对服务的动态路由?
如果网关需要抗每秒10w并发,你应该如何优化?
zuul网关部署的机器配置:
8核16G,对网关路由转发的请求,每秒抗个几千请求不成问题,10w并发需要几十台zuul网关机器。如果是32核心64G高配物理机,每秒可以抗一两万个请求,5台就差不多了
优化:
可以基于mysql,apollo,redis等做一些简单的二次开发,比如动态路由,授权认证,性能监控
分布式锁面试题
你是如何设计分布式锁的?
设计一个分布式锁,目的就是为了锁定全局资源,让请求串行化处理。
需要考虑的方面为:
- 加锁
- 解锁
- 死锁
- 锁超时与续命
加锁与解锁
我们使用的是jedis,set key时,NX和EX两个参数。
NX参数为了保证这个key值不存在时候能够成功写入,而EX参数可以让key有一个超时时间。
解锁就是将这个key值删掉
死锁
由于程序执行异常退出,但是又没有释放锁,或者服务挂了,没有超时机制,导致这个锁一直无法被释放掉,就造成了死锁的情况
锁超时与续命
首先说一个分布式锁失效的问题:
我们把锁的超时时间设置为10s,正常执行情况只需要一两秒,可能由于网络抖动,或者某种异常,导致这个任务超过10s还没有完成。 第一个线程的锁被释放了,第二个线程进来了。而第一个线程执行完后,会释放第二个线程加的锁,导致第三个线程能够进来,第二个线程也释放了第三个线程加的锁,以此类推,导致锁失效问题。
处理方案:
1)拿到锁后,开启一个守护线程或者延时队列,每隔一段时间检查当前线程是否还持有锁,如果持有,给当前线程续命,最多续多久~
2)加锁时,设置一个uuid作为锁的value值,在释放锁之前,校验一下是否与当前的value一致,一致才能释放锁
分布式事务面试题
画一下你们电商系统的核心交易链路图,说说分布式架构下存在什么问题?你是怎么设计分布式事务技术方案的?你能说说一个TCC分布式事务框架的核心架构原理吗?现有TCC事务方案的性能瓶颈在哪?能支撑高并发交易场景吗?如何优化
有分布式事务问题
分布式事务方案:XA、TCC、可靠消息最终一致性、最大努力通知方案、Sega
核心链路中的各个服务都需要跟TC这个角色进行频繁的网络通信,频繁的网络通信就会带来性能开销,本来一次请求不引入分布式事务只需要100ns,此时引入分布式事务后,可能需要200ms
网络请求可能还挺耗时的,上报一些分支事务的状态给TC,seate-server,选择基于哪种存储来放这些分布式事务日志或者状态,file,磁盘文件,mysql数据库来存放
在高并发场景向,如果数据量增加,seate-server也需要支持扩容,也需要部署多台机器,用一个数据库来存放分布式事务日志和状态的话,假设并发每秒上万,分库分表,对TC背后的数据库也会有同样的压力,这个时候对TC背后的DB也需要分库分表,扛更高并发压力
说说高并发场景下,数据库连接池如何进行优化?
以druid为例:
1.maxWait
表示从池里获取连接的等待时间。
万一暂时没有可用连接,就需要等待别的连接用完后释放,再去使用。通常设置1000以上,也就是1s以上,比如设置个1200,因为有时候要等待建立新的TCP链接,最多在1s内,就需要等一会了
如果这个参数默认设置为0,无限的等待获取连接,在高并发场景下,可能瞬间连接池耗尽,大量请求卡死在这里等待获取连接,进而导致tomcat中没有可用线程,服务假死
而且还拖累了调用我的其他内部服务,卡死在调用我的请求上,导致整体系统大量服务雪崩
设置个靠谱点的参数,大量线程获取不到连接,1s左右就快速失败了,不至于拖死整个服务,也不至于拖死其他调用我的服务,导致雪崩
2.connectionProperties
里面可以放connectionTimeout和socketTimeout,分别代表建立TCP连接的超时时间,以及发送请求后等待响应的超时时间。
比如将connectionTimeout设置为1200,socketTimeout设置为3000
在高并发场景下,万一遇到网络问题,导致跟书库的Socket连接异常无法通信,此时socket可能一直卡死等待某个请求的响应。其他请求无法获取连接,只能重启系统重新建立连接
所以设置一下超时时间,可以让网络异常之后,连接自动超时断开重连
3.maxActive
最大连接池数量
一般建议设置个20就够了,如果确实有高并发场景,可以适当增加到3-5倍,不要太多,这个在几十到100就很大了,仅仅是一个服务连接数据库的数量,数据库整体能承受的连接数量是有限的
不是连接越多越好,数据库连接多了,会导致CPU负载高,反而导致性能降低
更多的应该优化每个请求的性能,别让一个请求占用连接太长时间
如果压测时发现系统TPS不达标,应该如何优化系统?
对系统进行压测,每秒压个几百到上千请求,甚至上万请求,发现死活压不上,压来压去也就是每秒处理几百个请求,TPS不达标,需要优化。
实际上关注系统的请求,每个请求处理时间比较长,导致单位时间内,在有限的线程数量下,能处理的TPS就比较少了,先优化性能,再提TPS
假设一共200个线程,每个请求耗费500ms,每个线程每秒只能处理2个请求,200个线程,每秒只能处理400个请求。期望单机每秒处理500-600个请求
1.提升单机TPS->降低每个请求耗时量
如何做: 打日志,做监控,检查服务每个环节的性能开销,做缓存、ES、MQ,优化
把每个请求里的请求,每次数据库,缓存,ES之类的操作耗时都记录到日志中,每个请求执链路中的每个耗时环节都记录清清楚楚。比如你的一个请求过来,一共500ms,此时发现就是某个SQL语句耗时多了300ms,其他操作都在正常范围之内
分析这个待优化的SQL语句,搞了个全表扫描,写SQL时没有考虑到使用索引,此时建立新的索引,或者改写SQL语句,使用到我建立好的索引,SQL语句优化到了100ms
每个请求基本上只要300ms就可以了,每个线程每喵可以处理3个请求,200个线程每秒可以处理600个请求,达到了我的期望
2.增加机器数量,线性扩容。
比如服务层面,每个服务单机最多抗800个请求,扩容到部署10台机器,可以扛8000个请求。此处还得考虑以来的数据库,MQ,Redis能不能扛下这么多并发
对于核心接口的防重幂等性,你们是怎么设计的?怎么防止重复下单问题?
1.数据库唯一索引
2.基于redis实现一套幂等性防重框架
插入数据可以直接用数据库唯一索引解决防重幂等性;
而扣减库存,累加积分等更新操作,很难通过数据库唯一索引来保证,需要根据具体业务来做防重。
举例:在系统中,调用方A调用系统B的接口进行用户的扣费操作时,由于网络不稳定,A重试了N次该请求,那么不管B是否接收到多少次请求,都应该保证只会扣除该用户一次费用。
在一个业务流程的处理中,我们需要一个不重复的业务流水号,以保证幂等性。
假设让你来负责微信朋友圈社交系统,应该如何设计?
- 朋友圈的发布和存取
- 朋友圈对好友显示的权限控制
- 好友点赞与评论
微信使用的是「推」,因为每个人的好友一般不超过三位数,有好友数量限制。
每当有action的时候就向所有的好友推送,每个用户的接受的feed表都是单独的,是现成的,如果有好友动作的话单独添加此用户的feed表 。因此,每次刷新朋友圈不会一次次都去遍历所有朋友圈内容,而是去feed表抓取数据。所以微信目前也不支持编辑操作,只有添加/删除。
加入让你来设计微博,应该如何设计?
新浪微博推拉并存,以拉为主。
若粉丝数很大的用户,采用拉模式更为合理,当粉丝上线的时候,对微博进行数据拉取,这样可以有效避免数据过度冗余产生的浪费,尤其是大量僵尸粉存在的情况。
若关注的用户很多,则需要在后端进行多次拉取再取并集,但是查询效率会降低,因此一般的微博都有关注上限。
大多时候应该是推拉模式并存。
另外,新浪微博的「拉」做了很大程度的优化,每次拉取数据时参考上次拉取的时间,也就是说每次拉去的时候只是拉去新的,这样就减少了很多资源。每次拉出来的数据是暂存在缓存结构中。
12.微服务源码/源码天地
Spring Boot
说说SpringBoot的自动配置的原理?
1.自动配置的场景与作用
springboot的自动配置,使得在应用程序中配置其他功能时候变得简单,避免了以前繁琐的写配置注入功能组件步骤。
比如引入redis、kafka、rocketmq,mysql等等
一般被引入的项目分为两个模块:自动配置(autoconfigure)、Starter
其中starter模块没有代码,只在pom文件中引用了autoconfigure模块
而在autoconfigure模块中,除了核心代码之外,还有spring.factories文件
springboot就是通过这个spring.factories文件去做自动配置的
2.springboot自动配置是如何加载进来的
先说几个核心点,再对这几个点展开说,最后再串起来说一下springboot启动流程
spring ioc
@SpringBootApplication 注解
spring.factories
1)spring ioc
springboot在启动时,会调用到spring的 refresh方法.
走到invokeBeanFactoryPostProcessors时,这一步会解析spring.factories文件的内容,并注册到到bean定义中。
当然,这里实际上springboot是通过spring留有的扩展方法进行对接的,扩展的方法是 BeanDefinitionRegistryPostProcessor接口的postProcessBeanDefinitionRegistry方法
2)@SpringBootApplication注解
分析这个SpringBootApplication注解,可以看到他实际上导入了一个 AutoConfigurationImportSelector 类,这个类实现了ImportSelector接口,实际上用法跟AOP导入 AspectJAutoProxyRegistrar类相似,只不过这里就是实际上解析spring.factories文件内容的地方。
3)spring.factories
实际上这个文件中配置的内容并不是要全部加载到ioc容器中,而是由@ConditionalOnClass这样的条件控制,如果条件成立,比如添加了某某starter,就加载配置类
当然,springboot启动时,是直接加载spring.factories中EnableAutoConfiguration指定的值的。
这些值作为自动配置类的bean定义,经过ioc注入到容器中,完成自动配置的工作。
说说springboot的启动过程?
Spring Cloud底层架构原理
eureka
多级缓存策略:优化并发冲突,将频率降到最低
feign
feign对外发布一个接口,实际上就相当于springmvc发布一个http接口一样
原理是:
feign对接口打了一个注解,针对这个注解标注的接口生成动态代理,然后针对feign的动态代理去调用他的方法时候,会在底层生成http协议格式的请求
feign底层:使用http通信的框架组件,HttpClient,先使用Ribbon去从本地eureka注册表的缓存中获取出调用方机器列表,然后进行负载均衡选择一台机器,针对那台机器发送http请求
Zuul
建立好请求路径和服务的映射关系,你的请求到了网关后,通过请求路径匹配到服务,然后把请求转发到目标服务的某台机器,Ribbon从eureka本地缓存列表中获取一台机器,负载均衡后,把请求直接用http通信框架发送到
指定机器上
13.Spring专题
spring ioc 和 aop,动态代理技术,bean线程安全问题,事务机制,事务实现原理,事务传播机制
Spring Bean生命周期?
1.实例化Bean
2.设置对象属性(依赖注入)
构造函数注入,属性值注入
3.处理Aware接口
如果这个bean已经实现了ApplicationContextAware接口,Spring容器就会调用bean的serApplicationContext(ApplicationContext)方法,传入Spring上下文,把Spring容器传给这个bean
4.BeanPostProcessor
如果想在bean实例构造好后,对bean进行自定义处理,可以让bean实现BeanPostProcessor接口
5.InitializingBean与init-method
6.如果这个bean实现了BeanPostProcessor接口,将会调用postProcessorAfterInitialization方法
7.DisposableBean
当bean不再被需要时,会经过清理阶段,如果bean实现了DisposableBean接口,会调用其实现的destory()方法
8.destory-method
最后这个bean的spring中配置了destory-method属性,会自动调用其配置的销毁方法
IOC初始化过程
1.Resource定位
2.BeanDefinition的载入和解析
3.BeanDefinition的注册
- BeanDefinition的装载和解析:装载就是BeanDefinition的载入。 BeanDefinitionReader读取,解析Resource资源,就是将用户定义的Bean表示为IOC容器的内部数据结构:BeanDefinition
- 在IOC容器内部维护着一个BeanDefinition Map数据结构
- 在配置文件中,每一个
都对应一个BeanDefinition对象
- BeanDefinition的注册:向IOC容器注册第二步已经解析好的BeanDefinition,这个过程是通过BeanDefinitionRegistry接口来实现的,实际上就是把BeanDefinition对象put进map中,把beanName也put到一个map中罢了。
- 这个过程并没有完成依赖注入(bean创建),bean的创建是发生在应用第一次调用 #getBean(…)方法,向容器索要Bean时
- 我们可以通过设置预处理,对某个Bean设置 lazyinit=false 属性,让这个bean的依赖注入在容器初始化时完成
说说你对ioc控制反转的理解?
1.系统解释ioc是啥~(八股文)
2.举例子说ioc可以干啥~
tomcat在启动时候,会直接启动Spring容器
Spring ioc根据XML配置,或者注解,去实例化定义好的bean对象,对bean对象之间的引用关系,进行依赖注入,某个bean引用了另一个bean
底层的核心技术就是反射,通过反射直接根据类去构建对应的对象出来
Spring IOC让系统的类和类之间彻底解耦
正是Spring的IOC核心功能(控制反转 依赖注入)来创建和管理我们的组件(Controller service dao…)等等
很多第三方开源框架也看中了这一点,把自己的组件交给Spring去管理,给程序员提供方便。
比如Redis的RedisTemplate,RabbitMQ的AmqpRemlpate这些,程序可以直接通过Spring去拿,而不需要我们自己创建了
比如Spring和Mybatis整合,,利用的是Spring的扩展接口BeanFactoryPostProcessor接口
Spring和Nacos整合,用的是Spring的ApplicationListener接口
Spring和eureka整合,利用的是SmartLifecycle接口
Spring和ribbon整合,利用的就是SmartInitializingSinglenton接口
Spring和Sentinel整合,利用的就是BeanPostProcessor接口
另外,在我的开发经历中,也用这一功能简化了代码,提高了可读性和扩展性。
比如在bean带参构造函数中注入list,这个list作为策略模式去遍历,避免了if else
观察者模式 ,spring事件机制
说说对Spring的AOP机制理解?
Spring核心框架中,最关键的两个机制,就是ioc和aop,根据xml机制或者注解,去实例化我们所有的bean,管理bean之间的依赖注入,让类与类之间解耦,维护代码的时候可以更加轻松便利
Spring导入bean有多少种方式?
1.xml
2.@bean注解
3.@Import注解
4.@CompentScanner+@Compent @Service @Controller @Dao
被Spring扫描成bean定义。
bean定义可以修改吗?
可以,通过下面三个接口去修改
BeanFactoryPostProcessor
BeanDefinitionRegistryPostProcessor
ImportBeanDefinitionRegistrar
Spring中的Bean是线程安全的吗?
Spring容器中bean有5种作用域…
spring bean的signleton是线程不安全的
Spring事务实现原理,事务传播机制
14.网络
TCP/IP协议四层模型
应用层:http协议
传输层:TCP协议、socket编程规范
网络层:IP协议
数据链路层:以太网协议
聊聊HTTPS原理
浏览器请求www.baidu.com的全过程是怎样的?
1.系统网络角度
先假设,我们的电脑设置以下值:
ip地址:192.168.31.37
子网掩码:255.255.255.0网关地址:192.168.31.1DNS地址:8.8.8.8
在浏览器中请求www.baidu.com地址,这个时候找DNS服务器,DNS服务器解析域名之后,返回一个ip地址,比如172.194.26.108。
接着会判断两个ip地址是不是一个子网的,用子网掩码255.255.255.0,对两个ip地址做与运算,拿到192.168.31.0和172.194.26.0,明显不是一个子网的。
同一子网会直接找到目标服务器,不同子网会交给上级路由器,多个路由器转交到目标机器中
由客户端所在的机器将http数据包转换为TCP数据包,IP数据包,以太网数据包,根据数据大小进行拆包,再由网卡发出,经由路由器和交换机,目标服务器再合包,解包,接收请求
2.事件顺序角度
1)域名解析 (域名变为Ip地址)
1.1 浏览器搜索自己的DNS缓存、操作系统DNS缓存、hosts文件
1.2 找TCP/IP参数中设置的首选dns服务器(即本地dns服务器)
1.3 本地dns服务器向根域名服务器发起请求,根域名服务器返回com域的顶级服务器地址,再向com域顶级服务器发起请求,返回baidu.com的域名服务器地址,向baidu.com域名服务器发起请求,得到www.baidu.com的ip地址
1.4 dns服务器返回给操作系统,并缓存,操作系统将ip返回给浏览器并缓存
2)浏览器发起tcp三次握手,与服务器建立tcp连接
3)浏览器发起http请求,请求百度首页
4)服务器响应http请求,客户端得到html代码
5)浏览器解析http响应,渲染页面
3.涉及到的协议
1)应用层 (HTTP、DNS)
DNS解析域名为目的IP,通过IP找到服务器路径,客户端向服务器发送HTTP会话,然后通过TCP协议封装数据宝,在tcp协议基础上传输
2)传输层(TCP)
HTTP会话被分成多个报文段,添加源、目的端口
3)网络层(IP协议,ARP)
主要做的是通过查找路由表确定如何到达服务器,期间经过多个路由器,这些数据包转发由路由器完成,无非就是通过查找路由表决定通过哪个路径到达服务器
IP数据包传输和路由选择
ARP将ip地址映射为MAC地址,为数据包选择路由
4) 链路层(ARP)
浏览器请求一个地址,先按照应用层的http协议,封装一个应用层数据包,数据包放着请求报文。(此时数据包只有数据部分,还没有头)(应用层:根据http协议,生成一个)
TCP数据结构
- 序号Seq:占32位,用来标识从计算机A到计算机B的数据包的序号,计算机发送数据时对此进行标记。
- 确认号Ack:占32位,客户端和服务端都可以发送,Ack=Seq+1
- 标志位:每个标志位占用1bit,共有6个
- URG
- ACK
- PSH
- PST
- SYN:建立一个新连接
- FIN:断开一个连接
TCP连接为什么是3次握手?
不是为了三次握手而三次握手。而是为了尽可能的建立可靠的通信协议,保证可靠的核心就是双方都需要确认自己发送和接收信息的功能是正常的,由于网络环境的不稳定性,这一秒发送可能下一秒就网络堵塞了,不存在完全可靠的通信协议。
三次握手完成两个很重要的功能:
- 双方做好发送数据的准备工作,并且都知道彼此已经做好了准备
- 允许双方就初始序列号进行协商,这个序列号在握手的过程中被发送和确认
如果是两次握手:不能保证可靠,导致浪费资源
假设服务器给客户端在第二次握手时发送数据,数据从服务器发出,服务器认为连接已经建立,但在发送数据的过程中数据丢失,客户端认为连接没有建立,会进行重传。假设每次发送的数据一直在丢失,客户端一直SYN,服务器就会产生多个无效连接,占用资源,这个时候服务器可能会挂掉。这个现象就是我们听过的“SYN的洪水攻击”。
第三次握手是为了防止:如果客户端迟迟没有收到服务器返回确认报文,这时会放弃连接,重新启动一条连接请求,但问题是:服务器不知道客户端没有收到,所以他会收到两个连接,浪费连接开销。如果每次都是这样,就会浪费多个连接开销。
为什么不是四次握手?
因为三次握手即可达成目的。
TCP断开连接的4次挥手?
前两次挥手用于断开一个方向的连接,后两次分手用于断开另一个方向的连接。
什么是长连接?http长连接是什么?
http1.0 tcp是短连接,一个网页发起请求,tcp三次握手发送请求获取响应,四次挥手断开连接;每次请求都是先连接再断开
http1.1 tcp是长连接,tcp三次握手,建立连接,无论多少次请求都是走一个tcp连接,然后tcp连接通过四次挥手断开后被释放掉 (默认是2小时后)
说说什么是NIO?
15.其他
JDK动态代理和cglib动态代理有什么区别?
AOP实际上就是动态创建一个代理类出来,创建这个代理类的实例对象,在动态类里面引用你真正自己写的类,所有方法的调用,都是先走代理类的对象,它负责做一些代码上的增强,再去调用我们写的类
我们对类和方法做了切面,定义好了要增强的方法,Spring就需要对这些类生成动态代理,在动态代理中执行定义的增强代码。
如果你的类实现了某个接口,spring aop会使用jdk动态代理,生成一个跟你实现同样接口的代理类,构造一个实例对象出来,jdk动态代理,是在类有接口时候使用的
没有实现接口时,spring aop会改用cglib来生成动态代理,它是生成类的子类,动态生成字节码,覆盖你的方法,在方法里面加入增强代码的地方
JDK动态代理:目标类需要实现接口,会生成一个代理类,实现同样接口,构造一个实例对象,这个对象