面试 - 进阶

原创
2020/02/24 16:55
阅读数 332

Tomcat调优方案?
工作方式选择
为了提升性能,首先就要对代码进行动静分离,让Tomcat只负责jsp文件的解析工作。如采用Apache和Tomcat的整合方式,他们之间的连接方案有三种选择,JK、http_proxy和ajp_proxy。相对于JK的连接方式,后两种在配置上比较简单的,灵活性方面也一点都不逊色。但就稳定性而言不像JK这样久经考验,所以建议采用JK的连接方式。
Connector连接器的配置
之前文件介绍过的Tomcat连接器的三种方式:bio、nio和apr,三种方式性能差别很大,apr的性能最优,bio的性能最差。而Tomcat7使用的Connector默认就启用的Apr协议,但需要系统安装Apr库,否则就会使用bio方式。
配置文件优化
默认配置下,Tomcat会为每个连接器创建一个绑定的线程池(最大线程数200),服务启动时,默认创建了5个空闲线程随时等待用户请求。
JVM优化
Tomcat启动命令行中的优化参数,就是JVM的优化。Tomcat首先跑在JVM之上的,因为它的启动其实也只是一个java命令行,首先我们需要对这个JAVA的启动命令行进行调优。不管是YGC还是FullGC,GC过程中都会对导致程序运行中中断,正确的选择不同的GC策略,调整JVM、GC的参数,可以极大的减少由于GC工作,而导致的程序运行中断方面的问题,进而适当的提高Java程序的工作效率。但是调整GC是以个极为复杂的过程,由于各个程序具备不同的特点,如:web和GUI程序就有很大区别(Web可以适当的停顿,但GUI停顿是客户无法接受的),而且由于跑在各个机器上的配置不同(主要cup个数,内存不同),所以使用的GC种类也会不同。
 
有三个线程T1 T2 T3,如何保证他们按顺序执?

public class TestJoin {
    public static void main(String[] args) throws InterruptedException {
        final Thread t1 = new Thread(new Runnable() {
            public void run() {
                System.out.println(Thread.currentThread().getName() + " run 1");
            }
        }, "T1");
        final Thread t2 = new Thread(new Runnable() {
            public void run() {
                System.out.println(Thread.currentThread().getName() + " run 2");
                try {
                    t1.join(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "T2");
        final Thread t3 = new Thread(new Runnable() {
            public void run() {
                System.out.println(Thread.currentThread().getName() + " run 3");
                try {
                    t2.join(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "T3");
        //method1
        //t1.start();
        //t2.start();
        //t3.start();
        //method 2 使用 单个任务的线程池来实现。保证线程的依次执行
        ExecutorService executor = Executors.newSingleThreadExecutor();
        executor.submit(t1);
        executor.submit(t2);
        executor.submit(t3);
        executor.shutdown();
    }
}

缓存优化
缓存雪崩:
可能是因为数据未加载到缓存中,或者缓存同一时间大面积的失效,从而导致所有请求都去查数据库,导致数据库CPU和内存负载过高,甚至宕机。解决思路:加锁计数(即限制并发的数量,可以用semphore)或者起一定数量的队列来避免缓存失效时大量请求并发到数据库。但这种方式会降低吞吐量。分析用户行为,然后失效时间均匀分布。或者在失效时间的基础上再加1~5分钟的随机数。如果是某台缓存服务器宕机,则考虑做主备。
缓存穿透:
指用户查询数据,在数据库没有,自然在缓存中也不会有。这样就导致用户查询的时候,在缓存中找不到,每次都要去数据库中查询。解决思路:如果查询数据库也为空,直接设置一个默认值存放到缓存,这样第二次到缓冲中获取就有值了,而不会继续访问数据库。设置一个过期时间或者当有值的时候将缓存中的值替换掉即可。可以给key设置一些格式规则,然后查询之前先过滤掉不符合规则的Key。
缓存并发:
如果网站并发访问高,一个缓存如果失效,可能出现多个进程同时查询DB,同时设置缓存的情况,如果并发确实很大,这也可能造成DB压力过大,还有缓存频繁更新的问题。解决思路:对缓存查询加锁,如果KEY不存在,就加锁,然后查DB入缓存,然后解锁;其他进程如果发现有锁就等待,然后等解锁后返回数据或者进入DB查询。
缓存预热:
目的就是在系统上线前,将数据加载到缓存中。解决思路:数据量不大的话,在系统启动的时候直接加载。自己写个简单的缓存预热程序。

缓存算法
FIFO算法:First in First out,先进先出。原则:一个数据最先进入缓存中,则应该最早淘汰掉。也就是说,当缓存满的时候,应当把最先进入缓存的数据给淘汰掉。
LFU算法:Least Frequently Used,最不经常使用算法。
LRU算法:Least Recently Used,近期最少使用算法。
LRU和LFU的区别。LFU算法是根据在一段时间里数据项被使用的次数选择出最少使用的数据项,即根据使用次数的差异来决定。而LRU是根据使用时间的差异来决定的。

高并发、大流量处理及解决方法?
扩容、动静分离、缓存、服务降级和限流。
限流的常用算法和实践思路
令牌桶算法:
主要限制流量的流入速率,允许出现一定程度的突发流量。
每秒会有r个令牌按照固定速率放入桶中。桶的容量是固定不变的,如果桶满了再放入令牌,则溢出。若桶中的可用令牌不足,则改请求会被进行限流处理(被抛弃或缓存)。
漏桶算法:
主要限制流量的流出速率,并且流出速率是固定不变的 
可以以任意速率向桶中流入水滴。桶的容量是固定不变的,如果桶满了则溢出。按照固定的速率从桶中流出水滴。
计数器算法:
生产环境中的商品抢购可以使用,具体不同的sku限流规则配置在配置中心内,支持动态更改。可抢购次数的扣减操作,既可以用redis,也可以用JVM。如果是集群并且选择用JVM,则要根据总并发数量除以集群数量,得出单台机器的并发数。(比如总并发数5000,集群机器10台,则每台机器的并发为5000/10=500)。

高并发读需求
对于一件抢购商品的流量来说,因为key是同一个,所以流量必然会都引入到同一个redis缓存节点中,这时就容易出现单点故障。因此有下面两种解决方式:在每个master节点都挂slave从节点,当主节点挂了可以自动顶上。多级Cache方案,多用LocalCache来过滤掉一部分流量。 
本地缓存一般只缓存一些热点商品数据,缓存内容一般是商品详情和商品库存。 
本地缓存跟分布式缓存的同步一般有两种方式:一种是定时主动拉取更新策略。这种会存在一定时间的不一致,要视业务情况而定,例如库存,暂时的不一致导致超卖,单到真正下单的时候还会再进行库存的判断,所以影响较小,可以接受。这种方式要注意关掉缓存的定时失效,防止当用户流量突然过大,都到分布式缓存中拉取数据;第二种方式是每次商品更新,都发布一个消息,订阅此消息的节点监听到后再更新本地缓存的内容。

热点自动发现方案
可以将交易系统产生的相关数据,以及在上游系统中埋点上报的相关数据异步写入日志系统中,然后通过实时热点自动发现平台对收集到的日志数据做调用次数统计和热点分析。数据符合热点条件后,就立即通知交易系统做好热点保护。
Redis使用watch命令实现高并发抢购需求
一般高并发这里,不用悲观锁,会迅速增加系统资源;而使用队列,容易造成请求堆积,内存效果过快。所以一般使用乐观锁,可以用redis的watch命令实现。watch命令会监视给定的key,当exec时,如果监视的key从调用watch后发生过变化,则事务会失败。注意watch的可以是对整个连接有效的,事务也一样。如果连接断开,监视和事务都会被自动清除。当然exec,discard,unwatch命令都会清除连接中的所有监视。
协程(纤程)Fiber
协程概念:一种用户态的轻量级线程,其实就是单线程,指定执行整个函数中到一部分然后就先出去执行别的,等条件满足时,协程下次更新帧到了再继续往下执行。优点是无需线程上下文切换的开销,充分开发了单CPU的能力,资源占用低,适合高并发I/O。缺点也很明显,就是没办法利用多CPU的优势。
框架:Quasar,调度器使用ForkJoinPool来调度这些fiber。Fiber调度器FiberScheduler是一个高效的、work-stealing、多线程的调度器。
场景:服务A平时需要调用其他服务,但其他服务在并发高的时候延迟很严重。 一开始可以用httpClient连接池+线程池来处理,但如果调用服务的时候延迟太高或者超时,则会导致服务A的吞吐量会特别差。原因主要是一般一个链接由一个线程来处理,是阻塞的,所以在线程池数有限的情况下,吞吐量肯定上不去。并且当所有线程都I/O阻塞的时候,会很浪费CPU资源,并且CPU会一直做无用的上下文切换。这时候可以考虑协程来替换。
ForkJoinPool线程池
Fork/Join框架是Java7提供了的一个用于并行执行任务的框架,是一个把大任务分割成若干个小任务,最终汇总每个小任务结果后得到大任务结果的框架。
工作窃取(work-stealing)算法是Fork/Join框架最重要的特性。一般一个线程会对应一个任务队列,当处理较快的线程处理完自己的任务之后,就会窃取另外一个处理比较慢的线程对应的任务,这时候会存在两个线程同时处理一个队列的情况,所以任务队列一般使用双端队列,被窃取任务线程永远从双端队列的头部拿任务执行,而窃取任务的线程永远从双端队列的尾部拿任务执行。优点是充分利用线程进行并行计算,并减少了线程间的竞争。

说说秒杀如何实现的?
核心思路是流量控制和性能优化。
流量控制
请求流控:可以通过前端系统进行拦截,限制最终流入系统的请求数量。
客户端流控:较为合适的做法是屏蔽用户高频请求,比如在网页中设置5s一次访问限制,可以防止用户过度刷接口。
Web端流控:常见做法是可以通过在内存或缓存服务中加入请求访问信息,来实现访问量限制。
数据库存在瓶颈,为了应对大量的并发请求,使用redis缓存来拦截大量请求,使用redis原子操作incr和decr来确保库存。
对此方案优化
当库存为0时,需要在本地缓存中直接关闭标示位,即可以不去访问Redis。
当扣减库存之后,可以使用异步操作来创建订单等一系列操作。

CAP定理
一个分布式系统最多只能同时满足一致性(Consistency)、可用性(Availability)和分区容错性(Partition tolerance)这三项中的两项。
一致性(Consistency)
即更新操作成功并返回客户端完成后,所有节点在同一时间的数据完全一致。
可用性(Availability)
即服务一直可用,而且是正常响应时间。
分区容错性(Partition tolerance)
即分布式系统在遇到某节点或网络分区故障的时候,仍然能够对外提供满足一致性和可用性的服务。

BASE理论
BASE理论是对CAP理论的延伸,核心思想是即使无法做到强一致性(Strong Consistency,CAP的一致性就是强一致性),但应用可以采用适合的方式达到最终一致性(Eventual Consitency)。
基本可用(Basically Available)
基本可用是指分布式系统在出现故障的时候,允许损失部分可用性,即保证核心可用。
电商大促时,为了应对访问量激增,部分用户可能会被引导到降级页面,服务层也可能只提供降级服务。这就是损失部分可用性的体现。
软状态(Soft State)
软状态是指允许系统存在中间状态,而该中间状态不会影响系统整体可用性。分布式存储中一般一份数据至少会有三个副本,允许不同节点间副本同步的延时就是软状态的体现。mysql replication 的异步复制也是一种体现。
最终一致性(Eventual Consistency)
最终一致性是指系统中的所有数据副本经过一定时间后,最终能够达到一致的状态。弱一致性和强一致性相反,最终一致性是弱一致性的一种特殊情况。

ACID和BASE的区别与联系
ACID是传统数据库常用的设计理念,追求强一致性模型。BASE 支持的是大型分布式系统,提出通过牺牲强一致性获得高可用性。
ACID和BASE代表了两种截然相反的设计哲学,在分布式系统设计的场景中,系统组件对一致性要求是不同的,因此 ACID 和 BASE 又会结合使用。

HashMap JDK 1.7 vs JDK 1.8 区别
JDK 1.7
put()

get()

JDK 1.8
put()

get()

区别:
JDK 1.7的HashMap中存在很多重复的代码。例如putForNullKey()和put()方法中重复的链表遍历,大量重复的hash值计算逻辑等等。而在JDK 1.8中则对这部分的代码进行了重构。例如将putForNullKey()和put()方法重复的代码整合成putVal()方法,hash()方法处理key为null时的情况。
JDK 1.8中的put()方法会在链表超过树化阈值的时候,将链表转化为红黑树。而JDK 1.7中则只有链表。
JDK 1.7的链表节点插入为头插法(不需要判断链表是否存在),而JDK 1.8的链表节点插入则为尾插法。
JDK 1.8增加了对putIfAbsent()方法(存在才进行更新)的支持,详情可以看putVal()中关于onlyIfAbsent参数的处理逻辑。

HashMap为什么使用红黑树不是用BTree
选择红黑树是为了解决二叉查找树的缺陷,二叉查找树在特殊情况下会变成一条线性结构,遍历查找会非常慢。
而红黑树在插入新数据后可能需要通过左旋,右旋、变色这些操作来保持平衡,引入红黑树就是为了查找数据快,解决链表查询深度的问题,我们知道红黑树属于平衡二叉树,但是为了保持“平衡”是需要付出代价的,但是该代价所损耗的资源要比遍历线性链表要少,所以当长度大于8的时候,会使用红黑树,如果链表长度很短的话,根本不需要引入红黑树,引入反而会慢。

AQS
AQS就是一个并发包的基础组件,用来实现各种锁,各种同步组件的。 它包含了state变量、加锁线程、等待队列等并发中的核心组件。
AQS对象内部有一个核心的变量叫做state,是int类型的,代表了加锁的状态。 初始状态下,这个state的值是0。 这个AQS内部还有一个关键变量,用来记录当前加锁的是哪个线程,初始化状态下,这个变量是null。
线程跑过来调用ReentrantLock的lock()方法尝试进行加锁,这个加锁的过程,直接就是用CAS操作将state值从0变为1。如果之前没人加过锁,那么state的值肯定是0,此时线程就可以加锁成功。

Synchronized
Synchronized是通过对象内部的一个叫做监视器锁(monitor)来实现的,监视器锁本质又是依赖于底层的操作系统的Mutex Lock(互斥锁)来实现的。
在代码进入同步块的时候,如果此对象没有被锁定(锁标志位为“01”状态),虚拟机首先在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储对象目前Mark Word的拷贝。然后虚拟机使用CAS操作尝试将对象的Mark Word更新为指向锁记录(Lock Record)的指针。如果更新成功,那么这个线程就拥有了该对象的锁,并且对象的Mark Word标志位转变为“00”,即表示此对象处于轻量级锁定状态;如果更新失败,虚拟机首先会检查对象的Mark Word是否指向当前线程的栈帧,如果说明当前线程已经拥有了这个对象的锁,那就可以直接进入同步块中执行,否则说明这个锁对象已经被其他线程占有了。如果有两条以上的线程竞争同一个锁,那轻量级锁不再有效,要膨胀为重量级锁,锁标志变为“10”,Mark Word中存储的就是指向重量级锁的指针,而后面等待的线程也要进入阻塞状态。
在所有的锁都启用的情况下线程进入临界区时会先去获取偏向锁,如果已经存在偏向锁了,则会尝试获取轻量级锁,如果以上两种都失败,则启用自旋锁,如果自旋也没有获取到锁,则使用重量级锁,没有获取到锁的线程阻塞挂起,直到持有锁的线程执行完同步块唤醒他们。

展开阅读全文
打赏
0
0 收藏
分享
加载中
更多评论
打赏
0 评论
0 收藏
0
分享
返回顶部
顶部