最近阿里各部门已经陆续开始春招,自己也写完了项目最后的一部分,简历也刚刚完成了1.0版本,但是仍旧按捺不住躁动的内心。对于简历投递与面试,我是既期望又紧张,害怕它来又怕他不来。与其临渊羡鱼,不如退而结网,有躁动的功夫,不如温习一下知识点,正好最近一直在写项目, 知识点也快忘的差不多了。接下来打算结合自己的简历,从面试官的角度,对自己进行提问。
Java
集合类是否了解?
Set List Queue
List: Vector, ArrayList, LinkedList, Stack
Set: TreeSet, HashSet
Queue
Vector和ArrayList区别
首先要明确一点,从List接口下来的主要有三个实现类,Vector, ArrayList, LinkedList.
Vector和ArrayList底层都是动态数据,不同的是Vector是线程安全(synchronized修饰)
所以如果说有哪些线程安全的List, 那就有SynchronizedList, Vector, CopyOnWriteList.
然后!Stack又是继承自Vector, 所以Stack也是线程安全的(这都不知道,罪孽啊)。
Set 和 List 区别
同步容器有哪些
Vector Stack HashTable
HashMap是否了解
初始容量,负载因子,扩容?
容量为什么是2的因子
插入:头插尾插? 1.7是头插,
如何计算hash, 计算hash的时候为什么要取模
hashCode和equals的重写, equals和==的区别
HashMap有什么问题?
做结构性修改的时候不安全,总结来说,头插涉及到好几个代码片段,他们不是原子操作,那么多线程时就会出事,比如形成环形链表。
针对HashMap的这些弊端有解决措施吗?
ConcurrentHashMap
讲一讲ConcurrentHashMap
1.7中使用分段锁, 1.8中使用CAS+synchronized, 1.8中引入了红黑树,1.7和1.8中size()方法也不一样,1.7中多次计算,然后决定是否加锁,1.8中使用baseCount+CounterCell
为什么从1.7的分段锁变成了1.8的CAS+synchronized
谈谈你对synchronized关键字的理解
是一种Java内置的锁,修饰方法或者代码块,修饰代码块时,底层是moniterenter和monitorexit指令实现的,也就是加锁的意思。
内存语义:可见性,原子性,有序性
synchronized和lock
synchronized是内置的,lock是基于AQS的
synchronized等待中的线程不能响应中断,lock等待队列中的线程是可以相应中断的。
synchronized是非公平锁,ReentrantLock默认非公平锁,可以设置为公平锁。
synchronized发生异常时会自动释放,lock需要手动释放
synchronized是获取锁还是进入等待队列我们是不知道的,但是通过Lock的tryLock可以得知有没有获取锁成功。
lock可以有多个condition
Monitor机制是否了解
volatile关键字是否了解
对Java中锁的了解
CAS是什么
聊聊你熟悉的设计模式
Java内存模型, JVM内存模型,类加载,垃圾回收,OOM
fail-fast 和 fail-safe
HashMap 和HashTable区别
- 继承不同:一个实现map接口,一个来自Dictionary
- 线程安全不同
- 允不允许null值:HashMap允许键或者值为null, 且只允许有一个键为null的值(因为如果有多个键为null时,我怎么知道我get的null是哪个null),但是可以有一个或者多个键的值为null, 且当hashmap中没有某个键时,get方法会返回null, 所以说,我们不能用get方法来判断hashmap中是否有某个键(因为get方法返回null的时候,可能是值为null, 也可能是key不存在),而应该用containsKey()
线程池拒绝策略
> 直接丢弃
>
> 丢弃队列中最老的
>
> 抛出异常
>
> CallerRunsPolicy: 将任务分给调用线程去执行
线程安全的list
Vector 很古老
CopyOnWriteArrayList juc包中的,锁住了整个对象
synchronizedList Collections工具类中的
Spring为什么要用动态代理而不是静态代理?
静态代理的代理关系是在编译期就生成的,比较死板,而且是需要从class文件转成运行时的类的,而动态代理比较灵活,直接在运行期生成字节码并加载到JVM中去,Spring中代理应该很多,用静态代理,不灵活,而且会生成很多不必要的class文件。
JUC
阻塞队列
ArrayBlockingQueue: 有界的阻塞队列
DelayQueue: 无界的阻塞队列,每个元素都有一个延时,只有过期了才能出队
LinkedBlockingQueue:
PriorityBlockingQueue:
SynchronousQueue: 只能装一个元素的队列
常见的OOM的情况
java.lang.OutOfMemoryError:Java heap space
: 堆不够用了。或者内存泄漏java.lang.OutOfMemoryError:GC overhead limit exceeded
:当应用程序花费超过98%的时间用来做GC并且回收了不到2%的堆内存时,就会抛出该异常。java.lang.OutOfMemoryError: PermGen space
java.lang.OutOfMemoryError: Metaspace
java.lang.OutOfMemoryError:Unable to create new native thread
:线程建的太多了。java.lang.OutOfMemoryError:Out of swap space?
java.lang.OutOfMemoryError:Requested array size exceeds VM limit
-
Out of memory:Kill process or sacrifice child
ArrayList扩容, HashMap扩容
ArrayList的扩容是基于动态数组的,也就是说,当它的size快达到length时,会进行一个1.5倍的动态扩容,底层是通过
Arrays.copyof()
来实现的,这是一个浅拷贝。Spring启动过程
web.xml中的ContextLoaderListener在容器启动时会触发初始化,调用contextInitialized, 初始化一个WebApplicationContext上下文。
然后初始化DispatcherServlet, 它会以上面得到的上下文作为自己的parent上下文,然后再初始化一个自己的上下文。
JUC工具类
CountDownLatch
: 用于主线程等待子线程,主线程中设置await()
,子线程中countDown()
每次减一,只有当减到零的时候主线程中await()
后面的内容才能继续执行。如何实现:还是通过内聚AQS实现的,我们设置的
count
就是state
, 每次的countDown()
就是AQS的一个release
操作,主线程的await()
就是acquire()
操作, 如果count
不等于零,则假如同步队列自旋,自旋能出来的条件是count==0
,总体来说套用还是AQS的框架。CyclicBarrier
: 作用和上面的差不多,用法相对简单,而且可以循环利用。上面的CountDownLatch
主要用于一个线程等待其他线程都执行到某个地方时主线程才能继续,而CyclicBarrier
主要用于多个线程之间互相等待,因为它只有一个await
方法放在子线程中用于相互等待。Samphare
: 类似于操作系统中的信号量机制,一共五颗糖,那么也就最多只有五个线程能同时运行,只有当其中某个线程把糖还回来了其他线程没拿到糖的线程才能继续。
受检异常 非受检异常
受检异常:程序运行中容易出现的,情理可容的异常,出现了就要try catch或者throw
不受检异常:编译器不要求强制处理的异常,包括RunTimeException和Error
Executor创建线程池
newCachedThreadPool
1
2
3return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());核心池0,最大池
Integer.MAX_VALUE
,同步队列大小为1,所以相当于是每次都创建临时线程去工作的。newSingleThreadExecutor
1
2
3
4return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));核心池1,最大池1,但是阻塞队列是无界的,
LinkedBlockingQueue
默认大小是Integer.MAX_VALUE
,所以也容易造成OOM.newFixedThreadPool
1
2
3return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());道理同上。
AQS
Lock下的锁都是通过聚合一个AQS的子类来实现的,这里对AQS做一个简单的介绍。
AQS的核心有两个,一个是state, 一个是同步队列,通过对state值修改是否成功表示一个线程是否获取锁成功,未成功获取锁的线程则加入同步队列自旋。
AQS里还分了共享式获取锁和独占式获取锁,分别对应了不同的模板方法。
创建线程
- 继承Thread
- 继承Runnable
- Future + Callable:
Future = service.submit(Callable)
- FutureTask包装Callable:
service.submit(futuretask)
,通过futuretask.get获得结果
缓存一致性
如何保证保证缓存一致性?
- 加总线锁:CPU和其他部件的访问是通过总线进行的,总线加锁,那么就只能等一个CPU操作完了其他才能继续。
- 缓存一致性协议:当CPU写变量时发现该变量是共享变量时,就会通知其他CPU使他们的缓存无效,从而被迫只能从主存读取。
happens-before原则
什么是happens-before?它是一套规则,这套规则阐述了多线程操作之间的内存可见性。
在JMM中,如果一个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须存在happens-before关系。
Java内存模型具备一些先天的“有序性”,即不需要通过任何手段就能够得到保证的有序性,这个通常也称为 happens-before 原则。如果两个操作的执行次序无法从happens-before原则推导出来,那么它们就不能保证它们的有序性,虚拟机可以随意地对它们进行重排序。
线程中断
通过interrupt()方法可以修改标志位,然后监控标志位的变化伺机退出线程。这里需要注意,监控标注位的变化有两个方法:
interrupted()
:静态方法,调用它时,标志位会被清除。这意味着如果你连着调用两次这个方法,那么第二次结果肯定是false.isInterrupted()
:非静态方法,调用它时,标志位不会被清除
还需要注意的一点是如果被中断的线程是被wait()等方法阻塞的话,标志位会被清除且抛出一个中断异常,我们需要捕获异常进行退出。
线程状态
Spring
核心方法
Spring是容器,最基本的容器是BeanFactory, 然后ApplicationContext又继承自它,但其实不能认为ApplicationContext是BeanFactory的实现类,因为事实是ApplicationContext内部持有了一个实例化的BeanFactory(DefaultListableBeanFactory).
refresh方法是整个容器启动的核心。方法主要有以下几个功能:
- obtainFreshBeanFactory(): 创建容器,加载注册Bean.
- prepareBeanFactory(): 设置类加载器,添加BeanPostProcessor
- finishBeanFactoryInitialization(beanFactory):初始化所有的Singleton Beans
- createBeanInstance: 实例化bean
- populateBean: 属性设置,处理依赖
- initializeBean: 处理各种回调
- 检查aware相关接口(aware接口是为了让bean可以获取到框架自身的一些对象)
- BeanPostProcessor前置处理
- 如果实现InitializeBean, 调用afterPropertiesSet()
- BeanPostProcessor后置处理‘
BeanDefinition是Bean装载进容器中的一种表示,里面具体有是否是单例,它的依赖,是否懒加载,类名称,等等。
JVM
JVM内存结构
本地方法栈
虚拟机栈
程序计数器
堆
方法区
方法区是一种规范,永久代是方法区的一种实现,jdk8以后,去掉了永久代,用元空间代替,元空间存在于直接内存中,同时把常量池放在了堆中,
常见的OOM
堆溢出:疯狂地给一个list里面加对象
虚拟机栈和本地方法栈:栈的请求深度大于本来深度时会栈溢出,比如递归没写base case的时候。疯狂创建线程的时候会造成OOM: unable to create native thread.
引用
强引用:常见引用
软引用:内存溢出之前先回收软引用,如果回收了内存还不够再报异常。
弱引用:只能生存到下一次垃圾回收前
虚引用:唯一目的就是被回收时能收到一个系统通知。
stop the world
枚举根节点的时候需要stop the world
如何实现自定义的类加载器
继承ClassLoader, 重写findClass方法。
垃圾收集器
serial :采用复制算法的新生代收集器,单线程,需要stop the word
ParNew: Serial的多线程版本,采用复制算法。
Parallel Scavenge: 并行的多线程新生代收集器,使用复制算法,关注吞吐量
Serial Old: Serial的老年代版本,使用标记-清除算法
Parallel Old: Parallel Scavenge老年代本,使用标记整理。
CMS收集器:Concurrent Mark-Sweep, 并发标记清除,分为:
- 初始标记:需要stop the world
- 并发标记:
- 重新标记
- 并发清除
它有什么问题呢?
cpu敏感,因为占用线程
无法处理浮动垃圾
内存碎片
G1收集器:Garbage-First, 特点
- 并行与并发
- 分代收集
- 空间整合:标记整理
- 可预测的停顿
- 避免全堆扫描 Remembered Set: G1收集器把堆分成若干个region, 当程序对引用类型进行操作时,会检查其引用的对象是否在不同的regon中,如果在,就把引用信息记录在remenbered set中。
CMS收集器
设计原因:为了减少停顿以及和应用程序并行而设计。
G1收集器
分成大小相同的若干region
young GC: eden区满时会触发,这就涉及到要GC Root, 因为region的划分可能会使得老年代也引用了年轻代的对象,为了解决这个跨代问题,G1中引入了Rememer Set, 用来记录老年代的哪些对象引用了它。
global concurrent marking:
- 初始标记:stop the world, 标记了从GC Root开始直接可达的对象,期间执行一次young GC,
- 根区域扫描:主要扫描survivor,
- 并发标记:
- 最终标记: 使用snapshot-at-the-beginning(SATB)完成最终存活标记
- 清除垃圾
mixed GC:G1中的MIXGC选定所有新生代里的Region,外加根据global concurrent marking统计得出收集收益高的若干老年代Region,在用户指定的开销目标范围内尽可能选择收益高的老年代Region进行回收。所以MIXGC回收的内存区域是新生代+老年代。
可以发现,global concurrent marking和CMS的过程是很类似的,不同的是G1中的global concurrent marking 是为mixed GC服务的。
和CMS相比的优势:有内存整理,可预测的停顿时间。
JVM命令
jmap: 生成堆转储快照。
jhat: 堆转储快照分析工具。
自己定义的类能被最顶级的类加载器加载吗?
不能。Bootstrap类加载器只加载指定路径上的类,而且是按照名字识别的。
新生代和老年代所采用的GC算法
复制算法,标记清除算法。
数据结构
平衡树?红黑树?
平衡树相比于红黑树,它的要求更为严格,它要求所有节点的左右子树高度差不超过1,这种严格要求每次插入或者删除后如果不满足条件就要进行旋转,而这个旋转是很耗时的。
相比之下,红黑树通过节点着色的要求,红黑树确保没有一条路径会比其它路径长出两倍。所以可以认为它是一种弱平衡树,而且相对于AVL树,它的旋转次数少,更适合于那种插入删除频繁的场景。
TreeMap底层就是红黑树。
MYSQL
查询语句流程
事务
事务的四个特性:ACID
事务隔离:
read uncommitted:一个事务未提交的内容可以被另一个事务读到,会导致脏读问题(指读到的东西不对)。
read committed: 一个事务只有提交了其他事务才能读到,大多数数据库默认的隔离级别。会导致不可重复读问题,也就是一个事务前后两次读到的内容可能会不一样。
repeatable read: 一个事务前后两次读到的内容是一样的。这是MySQL默认的隔离级别。
serializable: 事务之间串行执行。
在实现上,数据库里面会创建一个视图,访问的时候以视图的逻辑结果为准。在“可重复读”隔离级别下,这个视图是在事务启动时创建的,整个事务存在期间都用这个视图。在“读提交”隔离级别下,这个视图是在每个 SQL 语句开始执行的时候创建的。这里需要注意的是,“读未提交”隔离级别下直接返回记录上的最新值,没有视图概念;而“串行化”隔离级别下直接用加锁的方式来避免并行访问。
mysql redo log 和 bin log undo log
redo log是innodb引擎所有。binlog是server层的。
redo log是物理日志,记录物理操作。 binlog是逻辑日志,记录逻辑操作。
redo log循环写。 binlog追加写。
undo log主要是为了保证原子性,用在MVCC中的
MySQL行锁,表锁
行锁有三种算法:
- 记录锁: 锁的是索引
- gap lock
- next-key lock
MySQL innodb如何解决幻读
MVCC + next-key lock
MySQL主从复制
Slave上面的IO进程连上Mater,请求读取binlog的内容,
Slave收到信息后,追加到relay-log中
Slave的SQL进程检查到relay-log的变化后,执行relay Log中的内容。
数据库范式
第一范式:没有重复列
第二范式:非主属性完全依赖于主属性(消除部分子函数依赖)
第三范式:属性不依赖于其他非主属性(消除传递依赖)、
分库分表的理解
分表可以解决单表压力,但是整体数据库的压力还在,所以就可以考虑分库了:把一些表放到新的数据库里
意向锁
首先要明确一点,意向锁是表级锁,它的出现主要是为了提升加锁判断时的性能。
举个例子,事务A给表中某行加了共享锁,这时候事务B想给表加个表锁,那么它就得知道能不能加,如果没有意向锁,那么这个判断就要逐行进行了。
而有了意向锁后,事情会变成这样:事务A加共享锁行锁的时候,数据库会先给表加个意向共享锁,意思就是告诉别的事务,这个表里有被加了共享行锁,那么这时候当别的事务想给这个表加排他锁表锁的时候,一检查发现有个共享意向锁,就知道这个排他锁表锁不能加了。
Redis
redis一次通信过程
memcached
区别?
数据类型
事务
multi开启事务
exec提交事务
discard丢弃事务
watch: 提供类似CAS的作用,当被监视的值被其他客户端修改时,整个事务会失败。如果监控的这个值过期了,exec正常工作。
1
2
3
4
5
6WATCH mykey
val = GET mykey
val = val + 1
MULTI
SET mykey $val
EXEC持久化
redis支持的数据类型更为丰富:有string, list, set, hash, sorted set, 而memcached支持的数据类型比较简单,对于复杂类型需要自己在客户端进行支持。
redis支持主从复制模式,而memcached的服务器之间是没有关系的,需要客户端去做一致性hash处理,这样做的一个后果是集群扩展时可能导致大量缓存失效。
redis是单线程的,memached是多线程的。
redis中可以通过multi,exec,discard等命令开启事务,2.6以后还支持lua脚本,而memcached除了一些像increment/decrement这样的原子操作,是不支持事务的。
redis支持RDB和AOF两种持久化方式,而memcached本身是不支持持久化的,但是有一些基于memcached协议的项目支持持久化。
总结,从数据类型的角度考虑,从事务的角度考虑,从持久化的角度考虑。参考:https://www.jianshu.com/p/e94fa7340923
redis分布式锁
redis做分布式锁主要是实现作为锁的一种互斥的功能,也就是说给A加了就不能给B加,就像一个开关一样,要么朝开那边,要么朝关那边。实现这一功能的命令是
setnx
, 意思是set if not exist
,setnx key value
,当key不存在的时候设置value成功,key存在的时候不做动作。利用这个setnx就可以很生动的给多个服务加锁,还需要再加上一个expire
命令,防止redis突然掉电导致锁不释放。但这么做也有个问题,万一redis在执行了setnx
之后但是在执行expire
之前掉电怎么办?其实是可以把这两条命令合成一条的。
redis KEYS命令和SCAN命令的区别
scan支持增量式迭代, 它们每次执行都只会返回少量元素, 所以这些命令可以用于生产环境, 而不会出现像 KEYS 命令、 SMEMBERS 命令带来的问题 —— 当 KEYS 命令被用于处理一个大的数据库时, 又或者 SMEMBERS 命令被用于处理一个大的集合键时, 它们可能会阻塞服务器达数秒之久。
不过, 增量式迭代命令也不是没有缺点的: 举个例子, 使用 SMEMBERS 命令可以返回集合键当前包含的所有元素, 但是对于 SCAN 这类增量式迭代命令来说, 因为在对键进行增量式迭代的过程中, 键可能会被修改, 所以增量式迭代命令只能对被返回的元素提供有限的保证
redis过期策略,内存淘汰策略
过期策略:
- 定时过期:内存友好,但是会占用大量CPU处理过期资源
- 惰性过期:节省CPU,内存不友好
- 定期过期:这种方案
内存淘汰
- noeviction
- allkeys-lru:从设置了过期时间的里面选
- allkeys-random
- volatile-lru
- volatile-random
- volatile-ttl
redis持久化原理?
RDB全量持久化
save命令:以阻塞方式全量持久化
bgsave
: fork一个子进程做持久化优点:文件小,恢复快,子进程做备份,不影响性能
缺点:隔一段时间同步一次,会有数据丢失
AOF做增量持久化
优点:安全性高,最多丢失一秒
缺点:文件体积大,恢复慢
redis string的实现
使用一种叫做简单动态字符串(Simple Dynamic String,SDS)的数据结构,这个数据结构里记录了字符串已使用的长度,为使用的长度,通过动态的扩展与缩减长度,来提升性能。
redis事务
redis事务命令可以将多个命令打包,需要注意的是其中有一个命令执行失败的话其他命令会继续执行,没有回滚。
缓存雪崩
缓存雪崩:指的是缓存大面积失效,所有请求一下子都打到数据库的情况。
解决:可以给失效时间加个随机数。
缓存穿透:指的是缓存没有,数据库也没有,直接打穿
解决:对请求进行校验;布隆过滤(用于验证一个数是否在一个集合中)。
缓存击穿:所有的请求都集中在一个热点key上,而这个key恰好刚刚失效。
解决:设置热点key永不过期
字典设计原理
字典底层其实就是个哈希表,不过需要注意的是它有两个哈希表,通常情况下我们只用一个,只有在rehash的时候才会启用另一个。那何时进行rehash? 当键值对太多或者太少的时候就会对哈希表进行扩张或者收缩。那具体何时,具体如何?请看下一节。
渐进式rehash
何时rehash
服务器目前没有在执行 BGSAVE 命令或者 BGREWRITEAOF 命令, 并且哈希表的负载因子大于等于
1
;服务器目前正在执行 BGSAVE 命令或者 BGREWRITEAOF 命令, 并且哈希表的负载因子大于等于
5
;
为何要渐进
数据量太大的话会阻塞服务器很久
如何渐进
字典的数据结构里有两个哈希表,通常情况下只用一个,只有rehash的时候才会用到另一个,通过
rehashidx
的标记来一点一点的rehash, 在这过程中遇见增加则增加到新的上面,删除则依次寻找删除,更新也是以此寻找。负载因子:used/size
redis主从同步
一主多从,主写从读。一开始从会给主发一个
psync
命令,然后主节点做一次
bgsave
, 然后后续操作存在buffer中,bgsave完成后将RDB文件发给从节点,从节点将其加载进内存。加载完成后再通知主节点把期间buffer内容发过来进行同步,此后主节点的每次写操作都会同步给从节点。
经过这么一波操作,主从就同步了,但是细心的同学可能会发现,万一后面又有命令操作主服务器呢?这时候就涉及到命令传播了,就是说后续每个修改命令,我们都同时也让从服务器执行以下,就ok了。
双写一致性
先删缓存再更新数据库
也会有问题,可以采用延时双删策略,就是说过一会儿回来把脏缓存再删一次
或者严格点:串行化。
SDS设计原理
为什么用动态字符串?反过来讲,用固定字符数组有什么问题?
- 获取数组长度的时间复杂度
- 字符串append的时候会不会溢出?
SDS比较鸡贼的地方就在于它是有预留空间的,每次修改字符串后会多分配和当前字符串长度一样的空闲空间。
假如我要做一个拼接的操作,首先检查空闲长度够不够,如果不够,我先扩容到所需要的长度,然后拼接,你以为这就完了吗,并没有,拼接完了之后,还会给分配一个等量的空闲空间,以备不时之需。
RDB原理
pipelined好处
使得多次IO时间折合成一次。
redis哨兵?
通过一个独立的进程运行哨兵,对主从节点进行监视,当主节点down掉之后会主动将从节点升为主节点,然后通过发布订阅模式同时其他从节点修改配置。
手写LRU
继承LinkedHashMap, 重写它的
removeEldestEntry()
方法、、
一致性哈希
> 哈希的整个值空间是一个圆,把服务器按照哈希算法映射到圆上,把数据也映射到圆上,每个数据对应的服务器就是从它所在位置向前走能走到的那个服务器。
>
> 它的好处是新增和down掉服务器对整体数据的影响较小,
>
> 坏处是服务器数量极少的时候可能会导致数据分布不均匀。(可以通过添加一些虚拟节点来解决)
操作系统
信号量
信号量是一种同步机制,通过pv操作来实现互斥资源的访问,也就是wait 和 signal,但是要注意p操作和v操作是两个操作系统原语,也就是说它们是具有原子性的。
死锁
形成死锁的条件:互斥访问,请求保持,不可剥夺,循环等待
进程间通信
- 管道(pipe),流管道(s_pipe)和有名管道(FIFO)
- 信号(signal)
- 消息队列
- 共享内存
- 信号量
- 套接字(socket)
进程和线程
说白了,他俩都是CPU工作时间段的描述,只不过粒度不同。
CPU执行进程,包括加载上下文,执行任务,保存上下文。
线程是对进程更小的划分,CPU执行进程任务的时候,又可以分成执行进程里的a线程,b线程,这些线程是共享上下文的。
select poll epoll
select 一个最大的问题是能够监视的文件描述符数量存在最大限制,而且需要遍历文件描述符来获取已就绪的socket
poll解决了文件描述符数量限制问题
select的几大缺点:
(1)每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大
(2)同时每次调用select都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很大
(3)select支持的文件描述符数量太小了,默认是1024
MQ
使用场景
销峰,异步,解耦
销峰只得是,巨大流量涌进来的时候,先让它走消息队列,而不是直接打在服务器上。
异步可不可以用线程池做?
线程池是基于内存的,掉电就没了,而且耦合度较高
消息队列会导致什么问题吗?
数据的一致性问题:采用分布式事务解决。
可用性:万一MQ突然挂掉咋整
重复消费:幂等。进行校验
顺序消费:
消息堆积:
分布式事务
主要通过半消息机制+回查来解决。
RocketMQ的架构?
NameServer: 管理元数据,包括对topic和路由信息的管理。
每个 Broker 在启动的时候会到 NameServer 注册,Producer 在发送消息前会根据 Topic 到 NameServer 获取到 Broker 的路由信息,Consumer 也会定时获取 Topic 的路由信息。
producer: 生产者
broker:消息中转,负责存储消息。
技术选型
首先宏观上讲,定制的肯定要比开源的好,主要的优势可以体现在:
可靠性:RocketMQ支持异步/同步刷盘;异步/同步Replication;
Kafka使用异步刷盘方式,异步R/同步eplication。
RocketMQ所支持的同步方式提升了数据的可靠性,这对于金融IT很重要。
单机支持的队列:RocketMQ单机支持的队列更多,意味着可以有更多的topic
消费失败重试机制:RocketMQ支持消费失败重试。
消息顺序性:一台broker宕机后,RocketMQ消息不会乱序。
定时消息:kafka不支持
分布式事务消息:kafka不支持。
消息查询:kafka不支持。
消息回溯:kafka按照offset回溯,RocketMQ可以按照时间回溯。
所以阿里为什么要自研?
- 金融业务对消息的可靠性,队列个数等很有要求。
- 当业务增长到一定规模,采用开源方案的技术成本会变高。
消费模式
集群模式:一个消费者对应一个队列
广播模式:一个消费者对应tpoic下的所有队列
集群模式进度保存在队列上,广播模式保存在消费者上。
RocketMQ是推还是拉
都有,但是推的本质还是拉
MQ如何保证高可用,如何保证消息不被重复消费(如何保证幂等),如何保证可靠性传输(如何处理消息丢失),如何保证消息的顺序性
如果项目中用到了MQ, 这些问题肯定跑不了,下面一一来看下。
高可用:在kafka中,一个topic分为多个partition, 而这些partition又是散落在不同的broker上的,并且每个partition在其他broker上又有备份,以此来达到高可用。
幂等:在MQ后面通过一些id之类的东西做校验。
如何保证可靠传输:言外之意,如何保证消息不丢,那么就要考虑消息会在哪丢了。
- 消费端弄丢了数据:首先要关闭自动提交offset, 进行手动提交,然后自己在消费端这边保证幂等。
- MQ弄丢数据:也有可能,万一leader挂了,其他的follower还没同步完,这样会导致这个follower被选举成leader后,还缺了点数据。对于这种情况,需要进行一些设置,比如必须要所有副本写完,才算写成功,还有就是至少让一个follower和leader保持紧密联系。
- 生产者:我们要求必须是所有的副本也写成功了才算是发送成功。
网络
get post区别
先说结论:没有本质区别,大部分所谓的区别其实都是浏览器或者服务器的一些限制。
get: 参数在url中; post:参数在请求体中。 (这个是Http协议用法的约定)
get提交的数据长度有限制,post则可以无限大。(这个是服务器或者浏览器的区别)
post会稍微安全一点,因为它的数据在请求体中。
get是幂等的,post不是,所以在网页回退或者刷新时,post的数据会被重新提交。
常见错误码
3**:页面重定向
4**:客户端错误
5**:服务器错误
301:资源永久移动。302:资源临时移动
cookie, session, token
为什么会有cookie, session? 因为http是个无状态的协议,通过cookie和session的操作可以让服务端知道请求是谁发来的。
那么cookie session有什么问题呢?在分布式场景下,每个服务器都要保存一份session, 很麻烦,但是如果把这些session集中放在一个服务器的话,又担心这个服务器崩了整个session就没了。
而token呢,整个用户信息都在里面,而且是经过加密的,每次客户端访问的时候只要带着这个token, 服务端解密一下就知道来者何人了。相比之下,用户信息既没有存在客户端也没有存在服务端。
Http1.0 和 Http2.0区别
http1.0:
- 每次请求都要建立一个TCP连接
- Head of Line Blocking: 请求队列的第一个请求因为服务器忙,导致后面的请求被阻塞。
http1.1:
- 支持持久连接:通过设置header里connection是close还是keep-alive, 一个TCP连接可以发送多个http请求
- 支持管道:本来情况是发一个请求,响应了才能发下一个。使用管道后就把客户端的队列搬迁到了服务端,客户端可以不用等这个响应就发下一个,但是服务端要按序响应。
- 可以使用多个TCP连接并行发请求。
http2.0
多路复用,一个HTTP连接可以处理多个请求
服务端推送
TCP如何保证可靠连接
- 三次握手,四次挥手
- 连续ARQ(回退N,超时重传)保证数据传输的正确性,滑动窗口进行流量控制
- 拥塞控制:慢开始,拥塞避免,快重传,快恢复
- 数据合理分片和排序
UDP优点
UDP是一种尽力而为的传输协议,它只是报文的搬运工,不做拼接,不做拆分,应用层下来的报文加个ip头就直接甩到下一层,他没有什么拥塞控制,想法就发,所以说是一种尽力而为,所以它适用于那些实时性比较高的场景。
TCP为何被称为流协议
说到流协议,我觉和和粘包拆包是有关系的,发送端出于性能考虑,会把几个小包合成一个大包去发送,在接收端又有自己的缓存,如果取数据不够快的话,缓存里就会连着存放好几个数据包。
UDP为何没有粘包
因为UDP发送端没有采用优化算法,而且接收端的缓冲区是链式结构。
OSI七层模型
####TCP拥塞控制
满开始,拥塞避免,
快重传:快重传要求接收方在收到一个失序的报文段后就立即发出重复确认,发送方只要一连收到三个重复确认就应当立即重传对方尚未收到的报文段
快恢复:当发送方连续收到三个重复确认时,就执行“乘法减小”算法,把ssthresh门限减半(为了预防网络发生拥塞)。但是接下去并不执行慢开始算法
考虑到如果网络出现拥塞的话就不会收到好几个重复的确认,所以发送方现在认为网络可能没有出现拥塞。所以此时不执行慢开始算法,而是将cwnd设置为ssthresh减半后的值,然后执行拥塞避免算法,使cwnd缓慢增大
三次握手
http报文结构
请求报文: 请求行,请求头,空行,请求体。
响应报文:状态行,响应头,空行,响应正文。
转发 重定向
转发:服务端进行,只能访问到当前web容器下的url
重定向:进行两次请求,返回的302响应里面会包含一个新地址,这个地址不受容器范围限制。
分布式
分布式的优点
理论可以无限水平扩容
CAP理论
Consistency 一致性:每次读取,要么获得最新的数据,要么返回一个错误。
Available 可用性:每次都能获得一个非错误的响应,但不能保证获取到的是最新的。
Partition tolerance 分区容错性:节点或者网络故障时,仍能对外提供服务。
对一个分布式系统来说,P是基本要求,所以一般我们都是通过权衡CA, 同时想办法尽可能地提升P
BASE理论
Basically Available 基本可用:损失部分可用性,保证核心可用。
Soft State 软状态:允许系统存在中间状态。 允许不同节点间副本同步的延时。
Eventually Consistency 最终一致性:所有副本经过一定时间,最终达到一致的状态。
分布式事务,两阶段提交,三阶段提交,Paxos
分布式事务:相比于普通的集中式事务来说,最大的问题就是它只能知道自身事务提交的情况,而对于其他机器上的事务,只能通过网络信息来了解。
因此,在分布式事务中,我们就提出了一个第三方,或者说叫做协调者,它负责协调所有机器上的事务。
两阶段提交:事务提交分为两个阶段,第一个是准备阶段,协调者发起询问,参与者执行询问,参与者返回响应。第二个是提交阶段,协调者根据参与者发回的请求决定提交还是回滚。
两阶段提交有什么问题呢?:
- 同步阻塞:一个参与者阻塞住了,其他人都得等。
- 单点故障:万一协调者挂了咋整。
- 数据不一致:协调者发送commit后只有一部分参与者收到了命令,而且此时协调者挂掉了 ,那就只有那部分提交了事务而其他的没有提交,就会造成数据不一致的情况。
- 两阶段提交无法解决的问题:假如在提交阶段,协调者发出commit之后宕机,收到这个commit命令的参与者也宕机,那么这时候即使有新的协调者,这条事务的状态也是不确定的。
在此之上,又提出了一个三阶段提交,它相当于是两阶段提交的升级版,它分为三个阶段:
三阶段提交:canCommit, preCommit, doCommit.
然后它还引入了一个超时机制。
相比于两阶段提交,三阶段提交主要是解决这个单点故障问题,当参与者超时还没收到commit命令时,它会自动提交,当然,这个也会导致不一致问题。
Linux
Linux查看内存命令
top, ps, free
COW
Copy On Write(写时复制),说白了是一种优化策略,在操作系统层面和java层面都有实现。
操作系统层面:众所周知,fork()会产生一个子进程,它的内容是父进程的拷贝,需要注意,这里是一个虚拟地址空间的拷贝,就是说有各自的虚拟地址,但是共享物理地址(骚就骚在这了),看似拷贝,实则共享,等到真正父子进程哪个被修改的时候,我们才执行真正的物理内存的拷贝,需要注意的是,这里拷贝的是对应的页,而不是全部拷贝,不然内存哪里吃得消。
设计模式
依赖倒置原则
我觉得依赖倒置的核心就是面向接口编程。具体来说就是:
- 上层不应该依赖于底层,两者都应该依赖于抽象
- 抽象不应该依赖于细节,细节应该依赖抽象。
设计原则
单里一接迪开
单一职责原则:
里氏替换原则:能用基类的地方都能用子类
依赖倒置原则:面向接口编程。
接口隔离原则:接口要尽量做到精简,不要一个接口里啥都有
迪米特法则:类间尽可能解耦
开放封闭原则:对扩展开发,对修改封闭。
工具
nginx架构
一个master进程,多个worker进程,
master进程负责读取和验证config文件,管理worker进程
每个worker进程都各自维护一个线程。
nginx如何热部署
每次修改config文件后,会重新生成新的worker,新的请求会交给新的worker处理,老worker工作完后就被kill
nginx如何高并发
epoll模型
nginx挂了怎么办
keepalive + nginx, keepalive
nginx负载均衡
轮询,哈希,最少连接
JWT
JWT分为三部分,header, payload, signature,
然后整体会用base64URL转成字符串
JWT的特点
- 默认不加密,不能存储秘密数据
- 一旦签发,始重有效。
如何认识前后端分离
- 方便开发 2. 请求的压力不会都堆到应用服务器上。
Spring事务传播行为
required: 没有则创建,有则沿用。
supports: 没有则不用,有则沿用。
mandatory: 必须运行在事务内,没有就抛异常。
require_new: 无论是否存在,都会创建新事务。
not_supported: 不需要事务,没有也不创建,有则挂起。
never: 不支持事务,否则报异常。
nested: 嵌套事务。