二面面经
一、分布式session的问题?知不知道怎么通过微服务怎么解决这个问题?
1. tomcat的session共享
优点:不需要额外开发,只需搭建tomcat集群即可
缺点:tomcat 是全局session复制,集群内每个tomcat的session完全同步(也就是任何时候都完全一样的) 在大规模应用的时候,用户过多,集群内tomcat数量过多,session的全局复制会导致集群性能下降, 因此,tomcat的数量不能太多,而且依赖tomcat容器移植性不好(所以不采用)
2. 使用Redis做缓存session的统一缓存
这种方案呢,其实就是把每次用户的请求的时候生成的sessionID给放到Redis的服务器上。然后在基于Redis的特性进行设置一个失效时间的机制,这样就能保证用户在我们设置的Redis中的session失效时间内,都不需要进行再次登录。
二、什么场景使用kafka?为什么使用kafka?kafka原理?
1. kafka使用场景
-
日志收集:可以用Kafka收集各种服务的log,通过kafka以统一接口服务的方式开放给各种consumer。
-
消息系统:解耦生产者和消费者、缓存消息等。
-
用户活动跟踪:kafka经常被用来记录web用户或者app用户的各种活动,如浏览网页、搜索、点击等活动,这些活动信息被各个服务器发布到kafka的topic中,然后消费者通过订阅这些topic来做实时的监控分析,亦可保存到数据库。
2. 为什么使用kafka?
消息队列中间件是分布式系统中的重要组件,主要解决耦合、异步、削峰问题。
- 耦合:
传统模式下系统间的耦合性太强。怎么说呢,举个例子:系统 A 通过接口调用发送数据到 B、C、D 三个系统,如果将来 E 系统接入或者 B 系统不需要接入了,那么系统 A 还需要修改代码,非常麻烦。
如果系统 A 产生了一条比较关键的数据,那么它就要时时刻刻考虑 B、C、D、E 四个系统如果挂了该咋办?这条数据它们是否都收到了?显然,系统 A 跟其它系统严重耦合。
而如果我们将数据(消息)写入消息队列,需要消息的系统直接自己从消息队列中消费。这样下来,系统 A 就不需要去考虑要给谁发送数据,不需要去维护这个代码,也不需要考虑其他系统是否调用成功、失败超时等情况,反正我只负责生产,别的我不管。
- 异步
先来看传统同步的情况,举个例子:系统 A 接收一个用户请求,需要进行写库操作,还需要同样的在 B、C、D 三个系统中进行写库操作。如果 A 自己本地写库只要 1ms,而 B、C、D 三个系统写库分别要 100ms、200ms、300ms。最终请求总延时是 1 + 100 + 200 + 300 = 601ms,用户体验大打折扣。
如果使用消息队列,那么系统 A 就只需要发送 3 条消息到消息队列中就行了,假如耗时 5ms,A 系统从接受一个请求到返回响应给用户,总时长是 1 + 5 = 6ms,对于用户而言,体验好感度直接拉满。
- 削峰
如果没有使用缓存或者消息队列,那么系统就是直接基于数据库 MySQL 的,如果有那么一个高峰期,产生了大量的请求涌入 MySQL,毫无疑问,系统将会直接崩溃。
那如果我们使用消息队列,假设 MySQL 每秒钟最多处理 1k 条数据,而高峰期瞬间涌入了 5k 条数据,不过,这 5k 条数据涌入了消息队列。这样,我们的系统就可以从消息队列中根据数据库的能力慢慢的来拉取请求,不要超过自己每秒能处理的最大请求数量就行。
也就是说消息队列每秒钟 5k 个请求进来,1k 个请求出去,假设高峰期 1 个小时,那么这段时间就可能有几十万甚至几百万的请求积压在消息队列中。不过这个短暂的高峰期积压是完全可以的,因为高峰期过了之后,每秒钟就没有那么多的请求进入消息队列了,但是数据库依然会按照每秒 1k 个请求的速度处理。所以只要高峰期一过,系统就会快速的将积压的消息给处理掉。
3. kafka原理
对于一些常规的消息系统,kafka是个不错的选择;partitons/replication和容错,可以使kafka具有良好的扩展性和性能优势。
-
Producer:
消息生产者,就是向 Kafka broker 发消息的客户端。 -
Consumer:
消息消费者,向 Kafka broker 取消息的客户端。 -
Consumer Group(CG):
消费者组,由多个 Consumer 组成。消费者组内每个消费者负责消费不同分区的数据,一个分区只能由一个组内消费者消费;消费者组间互不影响。所有的消费者都属于某个消费者组,即消费者组是逻辑上的一个订阅者。 -
Broker:
一台 Kafka 服务器就是一个 broker。一个集群由多个 broker 组成。一个 broker 可以容纳多个 topic。 -
Topic:
可以理解为一个队列,生产者和消费者面向的都是一个 topic。 -
Partiton:
为了实现拓展性,一个非常大的 topic 可以分布到多个 broker(即服务器)上,一个 topic 可以分为多个 Partition,每个 partition 都是一个有序的队列。 -
Replication:
副本,为保证集群中某个节点发生故障时,该节点上的 partition 数据不丢失,且 Kafka 仍然可以继续工作,Kafka 提供了副本机制,一个 topic 的每个分区都有若干个副本,一个 leader 和若干个 follower。 -
Leader:
每个分区多个副本的 ” 主 “,生产者发送数据的对象,以及消费者消费数据时的对象都是 leader。 -
Follower:
每个分区多个副本的 “从”,实时从 leader 中同步数据,保持和 leader 数据的同步。leader 发生故障时,某个 follower 会成为新的 leader。 -
Offset:
消费者消费的位置信息,监控数据消费到什么位置,当消费者挂掉再重新恢复的时候,可以从消费位置继续消费。 -
ZooKeeper:
Kafka 集群能够正常⼯作,需要依赖于 ZooKeeper,ZooKeeper 帮助 Kafka存储和管理集群信息。
三、quartz原理?
Quartz 组件
Quartz实现,首先要理解Schedule(任务调度器),Job(作业任务)和Trigger(触发器)三者的关系
1. Job 任务
- 我们要定时执行某件事情,然后我们把它写成一个接口,这就是 Job;
- 只有一个方法void execute(JobExecutionContext context)。
2. JobDetail 任务详情
- 具体任务类有了,还需要任务详情类( JobDetail )去实现 Job;
- Quartz在执行Job时,需接收一个Job实现类,以便运行时通过反射机制实例化Job;
- 描述Job的实现类及其它相关的静态信息-----JobDetail;
- 真正执行的任务并不是Job接口的实例,而是用反射的方式实例化的一个JobDetail实例
3. Trigger 触发器
- 是一个类,描述触发Job执行的时间触发规则;
- 主要有SimpleTrigger和CronTrigger这两个子类:
- SimpleTrigger:仅需触发一次或者以固定时间间隔周期执行适用;
- CronTrigger:可以通过Cron表达式定义出各种复杂时间规则的调度方案。
4. Scheduler 调度器
- 代表一个Quartz的独立运行容器;
- Trigger和JobDetail可以注册到Scheduler中;
- 两者拥有各自的组及名称,组及名称必须唯一(两者可以相同,因为类型不同);
- Scheduler定义了多个接口,允许外部通过组及名称访问和控制容器中Trigger和JobDetail。
5. 关系描述:
- Scheduler可以将Trigger绑定到某一JobDetail中,这样当Trigger触发时,对应的Job就被执行。
- 一个Job可以对应多个Trigger,但一个Trigger只能对应一个Job。
- 通过SchedulerFactory创建Scheduler实例,
- Scheduler拥有一个SchedulerContext,保存着Scheduler上下文信息,Job和Trigger都可以访问
- SchedulerContext内部通过Map维护这些上下文数据
- SchedulerContext为保存和获取数据提供了多个put()和getXxx()的方法。
四、基于redis分布式锁
setnx写入操作命令:setnx(key,val)
- 当key不存在,插入一个key为val的字符串,返回1
- 当key存在,什么都不做,返回0
1. 获取锁
调用setnx,key为lockid
- 返回0,说明锁被人使用
- 返回1,成功获取锁
2. 释放锁
判断锁是否存在,存在的话,redis的delete操作释放锁
五、redis主从复制
1. 概念
- 主从复制,是指将一台Redis服务器的数据,复制到其他的Redis服务器。前者称为主节点(Master/Leader),后者称为从节点(Slave/Follower).。
- 数据的复制是单向的!只能由主节点复制到从节点(主节点以写为主、从节点以读为主)。
- 默认情况下,每台Redis服务器都是主节点,一个主节点可以有0个或者多个从节点,但每个从节点只能有一个主节点。
2. 作用(优点)
- 数据冗余:主从复制实现了数据的热备份,是持久化之外的一种数据冗余的方式。
- 故障恢复:当主节点故障时,从节点可以暂时替代主节点提供服务,是一种服务冗余的方式
- 负载均衡:在主从复制的基础上,配合读写分离,由主节点进行写操作,从节点进行读操作,分担服务器的负载;尤其是在多读少写的场景下,通过多个从节点分担负载,提高并发量。
- 高可用(集群)基石:主从复制还是哨兵和集群能够实施的基础。
六、spring aop和ioc
1. ioc
- 之前,程序是主动创建的,控制权在程序员手中。
- 使用了set注入后,程序不在具有主动性,而是变成了被动的接收对象。
这种思想,从本质上解决了问题,我们程序员不用再去管理对象的创建了。系统的耦合性大大降低了,可以更加专注的在业务的实现上,这是IOC的原型(Inversion of Control)。
ioc----》反射原理?
反射与工厂模式实现IOC
我们首先看一下不用反射机制时的工厂模式:
interface fruit{
public abstract void eat();
}
class Apple implements fruit{
public void eat(){
System.out.println("Apple");
}
}
class Orange implements fruit{
public void eat(){
System.out.println("Orange");
}
}
//构造工厂类
//也就是说以后如果我们在添加其他的实例的时候只需要修改工厂类就行了
class Factory{
public static fruit getInstance(String fruitName){
fruit f=null;
if("Apple".equals(fruitName)){
f=new Apple();
}
if("Orange".equals(fruitName)){
f=new Orange();
}
return f;
}
}
class hello{
public static void main(String[] a){
fruit f=Factory.getInstance("Orange");
f.eat();
}
}
上面写法的缺点是当我们再添加一个子类的时候,就需要修改工厂类了。如果我们添加太多的子类的时候,改动就会很多。下面用反射机制实现工厂模式:
interface fruit{
public abstract void eat();
}
class Apple implements fruit{
public void eat(){
System.out.println("Apple");
}
}
class Orange implements fruit{
public void eat(){
System.out.println("Orange");
}
}
class Factory{
public static fruit getInstance(String ClassName){
fruit f=null;
try{
f=(fruit)Class.forName(ClassName).newInstance();
}catch (Exception e) {
e.printStackTrace();
}
return f;
}
}
class hello{
public static void main(String[] a){
fruit f=Factory.getInstance("Reflect.Apple");
if(f!=null){
f.eat();
}
}
}
现在就算我们添加任意多个子类的时候,工厂类都不需要修改。使用反射机制实现的工厂模式可以通过反射取得接口的实例,但是需要传入完整的包和类名。而且用户也无法知道一个接口有多少个可以使用的子类,所以我们通过属性文件的形式配置所需要的子类。
总之:
Spring 中的 IOC 的底层实现原理就是反射机制,Spring 的容器会帮我们创建实例,该容器中使用的方法就是反射,通过解析 xml 文件,获取到 id 属性和 class 属性里面的内容,利用反射原理创建配置文件里类的实例对象,存入到 Spring 的 bean 容器中。
把IOC容器的工作模式看做是工厂模式的升华,可以把IOC容器看作是一个工厂,这个工厂里要生产的对象都在配置文件中给出定义,然后利用编程语言提供的反射机制,根据配置文件中给出的类名生成相应的对象。
2. aop
对业务逻辑各部分分离,解耦
加上与业务逻辑无关的Aspect(切面):日志、事务、缓存
不影响原业务的情况下,动态增强代码
七、数学建模
八、MySQL引擎, 最左前缀法则
where a = ‘’ and b > c and c = ‘’ order by f怎么建立索引, 最左前缀底层存储
我觉得是建立联合索引(a,c,b,f)
九、场景题1:如何设计一个上课点到系统
表:管理员表、学生表、考勤表
功能:老师可以作为管理员的存在
- 登陆系统
- 管理学生(crud)
- 查看考勤表
- 随机抽取N个(自定义输入n)学生进行点名(考勤录入)
十、场景题2:怎么在10亿文件中寻找最大的十个
1. 最小堆
首先读入前10个数来创建大小为10的最小堆,然后遍历后续的数字,并于堆顶(最小)数字进行比较。如果比最小的数小,则继续读取后续数字;如果比堆顶数字大,则替换堆顶元素并重新调整堆为最小堆。整个过程直至10亿个数全部遍历完为止。然后按照中序遍历的方式输出当前堆中的所有10个数字。该算法的时间复杂度为O(nmlogm),空间复杂度是10(常数)。
复杂度分析
建堆时间复杂度是O(m),堆调整的时间复杂度是O(logm),最终时间复杂度等于,1次建堆时间+n次堆调整时间=O(m+nlogm)=O(nlogm)
这里的n为10亿,m为10
2. 分治法
将10亿个数据分成100份,每份1000万个数据,找到每份数据中最大的10个,最后在剩下的 100 * 10个数据里面找出最大的10个。
十一、操作系统中的堆栈区别
1. 堆栈空间分配区别:
1、栈(操作系统):由操作系统自动分配释放 ,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈;
2、堆(操作系统): 一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收,分配方式倒是类似于链表。
2. 堆栈缓存方式区别:
1、栈使用的是一级缓存, 他们通常都是被调用时处于存储空间中,调用完毕立即释放;
2、堆是存放在二级缓存中,生命周期由虚拟机的垃圾回收算法来决定(并不是一旦成为孤儿对象就能被回收)。所以调用这些对象的速度要相对来得低一些。
堆:内存中,存储的是引用数据类型,引用数据类型无法确定大小,堆实际上是一个在内存中使用到内存中零散空间的链表结构的存储空间,堆的大小由引用类型的大小直接决定,引用类型的大小的变化直接影响到堆的变化
栈:是内存中存储值类型的,大小为2M,超出则会报错,内存溢出
3. 堆栈数据结构区别:
堆(数据结构):堆可以被看成是一棵树,如:堆排序;
栈(数据结构):一种先进后出的数据结构。