丧心病狂的面试知识

设计模式

  1. 单例模式(线程安全的对象发布写法)
  2. 工厂模式
  3. 代理模式(实现代理模式的方式有哪些 jdk的invocationHandler,基于接口 ,cglib,(MethodInterceptor)基于方法)
  4. 享元模式:主要用于减少创建对象的数量,以减少内存占用和提高性能。尝试重用现有的同类对象。
  5. 迭代器模式(Iterator):对外不暴露内部的数据结构,提供统一的接口,实现next,hashNext的访问数据的基本功能
设计模式 设计模式应用场景
单例模式 连接池,线程池,工厂类
工厂模式
代理模式 功能增强,比如日志处理,权限判断等等
享元模式 java 中的string,如果有就返回,否则创建并保存到字符串缓存池;数据库的数据池
迭代器模式 隐藏内部数据结构,对外提供统一的数据访问方式

面向对象的特点

  1. 封装,继承,多态

软件设计原则:

  1. 开闭原则:对扩展开放,对修改关闭,需要增加新功能,增加一个实现接口的具体类就好了
    比如伪代码: 判断武器类型,给出伤害值
  2. 里氏代换原则: 开闭原则的补充,(保障基类的可复用性)
  3. 迪米特原则:类与类的少了解,即类只需要知道依赖类必须知道的方法,减少类之间的联系,只需要知道关心的方法,不要过多的依赖
  4. 依赖倒置:调用者不依赖被调用者(具体实现),而是依赖抽象,被调用者的具体实现发生更改,后续,被调用者可以被无感替换
  5. 接口隔离: 接口定义要最小化,就是接口只专注一个领域,不要大而全,要小而细。Cloneable 接口只负责克隆

数据结构

  1. 数组与链表(连续内存与非连续内存,随机访问效率,删除效率不一样)

  2. 排序算法实现以及时间,空间复杂度,改良分析

  3. 堆和栈

  4. 二叉树,搜索树,完全二叉时,满二叉树,哈夫曼树(最优树)

  5. 二叉树的递归遍历,借助栈的非递归遍历

  6. 字典树(原理以及应用场景,可能的优化角度)

  7. B 树,B+树,特点,以及应用场景(数据库索引)

  8. 邻接矩阵与邻接表

  9. 查找

  10. hash法,hash冲突的解决方式(可能会考虑到redis集群时的hash算法)
    冲突解决方法:开放地址法(线性探测(删除比较麻烦,只能标记为删除)
    随机探测(步长随机数序列))
    拉链法(指针数组+链表)

  11. 深度优先于广度优先
    广度优先:类比成树中 层次遍历
    深度优先:类比树中序遍历

  12. 图的基本概念
    无向图:
    有向图:
    表示方式:
    邻接表:
    邻接矩阵:

  13. 最短路径(算法)
    **单源最短路径:dj(贪心算法),**使用一维数组D[i] 记录起点到i的最短距离,
    如果起点v0 到i需要经过 k,则D[i]=min{d[i],D[k]+e(k,i)}
    多源最短路径:floyd(dp)
    d[i] [j] 表示i与j之间的最短距离
    递推公式: d[i][j]=min(d[i][j],d[i][k]+d[k][j]);

  14. 最小生成树
    可使用贪心算法:

算法名 描述 使用场景,复杂度
prims 设点集为S,已选取点为S1,未选取点集为S-S1, 1,初始化,选取第一个点2, 遍历 已选取点和未选取点之间最小边,并将该点加入S1,直到S-S1为空,完成 时间复杂度:o(n 2 ^2 2),与边数目无关,适用于稠密图
cruskal 1.初始化 将边按照从小到大排列,所有点看成是孤立的;2,依次选取边, 如果该边的两个端点在同一个连通图中,则跳过;否选取该边;3,重复2直到只剩一个连通图,这就得到最小生成树 复杂度:o(elge) ,e是边的条数,这里主要是排序来体现的,适合于稀疏图
查找方式 数据结构 时间复杂度
顺序查找 顺序存储,链式存储 顺序存储(o(1)),链式存储o(n)
随机查找 顺序存储(o(1)),链式存储o(n)
二分查找 有序顺序存储结构 o(lgn)
hash查找 o(1)
索引查找 o(lgn)+o(k) 先使用二分查找找到块,然后在块中顺序查找

spring

  1. servlet
    Servlet对象只会创建、初始化一次,如果有多个请求同时访问,会从线程池中获取一个线程还是用这个Servlet对象处理用户请求,算是单例多线程,这就会带来一个问题,线程安全问题,访问特别是修改Servlet类中的全局变量时会导致数据错误,所以尽量不使用在Servlet类中声明全局变量。实在不行就需要给线程加锁。
  2. springMvc 的流程

请求-》前端控制器(dispatchServlet)->调用处理映射器(HandlerMapping),生成处理器对象以及处理拦截器->DispatchServlet(前端控制器)->调用处理适配器进一步调用处理器-》执行处理器( Controller)->返回ModelAndView给dispatchServlet,传递modelAndView -》ViewResolver(视图解析器)-》返回具体View ->DispatchServlet对View 进行渲染-》响应用户
丧心病狂的面试知识_第1张图片

  1. springmvc 的控制器是单例,线程问题怎么解决?(用同步会影响性能,不要写字段)
  2. 简述IOC,DI
    IOC:是控制反转

创建对象的权限:由开发者new 交由spring 去创建(需要将类的全限定名配置到xml文件中,spring底层根据反射去创建对象)
不使用ioc时:
A a=new A();
使用ioc后 A a=SpringContext.getBean("");

DI:是依赖注入(注入方式:构造+settter,一般使用后者)
3. spring中的bean有些什么类型?
4. spring 线程安全吗?
大多数bean 是单例的,而且spring并没有考虑线程安全,但是大多数bean是无状态的
每个bean自身的设计。不要在bean中声明任何有状态的实例变量或类变量,如果必须如此,那么就使用ThreadLocal把变量变为线程私有的,如果bean的实例变量或类变量需要在多个线程之间共享,那么就只能使用synchronized、lock、CAS等这些实现线程同步的方法了。
下面将通过解析ThreadLocal的源码来了解它的实现与作用,ThreadLocal是一个很好用的工具类,它在某些情况下解决了线程安全问题(在变量不需要被多个线程共享时)。
5. FactoryBean 与BeanFactory 的区别
BeanFactory,以Factory结尾,表示它是一个工厂类(接口),用于管理Bean的一个工厂。在Spring中,BeanFactory是IOC容器的核心接口,它的职责包括:实例化、定位、配置应用程序中的对象及建立这些对象间的依赖。
 FactoryBean 以Bean结尾,表示它是一个Bean,不同于普通Bean的是:它是实现了FactoryBean接口的Bean,根据该Bean的ID从BeanFactory中获取的实际上是FactoryBean的getObject()返回的对象,而不是FactoryBean本身,如果要获取FactoryBean对象,请在id前面加一个&符号来获取。

  1. 声明式事务的类型(xml+注解)
    优点:避免侵入业务代码,配置方便,便于管理
  2. 事务的特性
特性 描述
原子性 将事务中所做的操作捆绑成一个原子单元,即对于事务所进行的数据修改等操作,要么全部执行,要么全部不执行。
隔离性 由并发事务所做的修改必须与任何其他事务所做的修改相隔离。事务查看数据时数据所处的状态,要么是被另一并发事务修改之前的状态,要么是被另一并发事务修改之后的状态,即事务不会查看由另一个并发事务正在修改的数据。这种隔离方式也叫可串行性
持久性 事务完成之后,它对系统的影响是永久的,即使出现系统故障也是如此
一致性 事务在完成时,必须使所有的数据都保持一致状态,而且在相关数据中,所有规则都必须应用于事务的修改,以保持所有数据的完整性。事务结束时,所有的内部数据结构都应该是正确的

9. 如何实现分布式事务

没有应用场景,个人分析,分布式事务的产生是由于某个业务操作需要在
本地事务的实现当然很简单,加上Transactional 注解
如果是分布式事务:
方案一:2PC 提交
前提:
存在一个节点作为协调者,其他节点作为参与者,节点间可以相互网络通信
所有节点采用预写日志,并且日志保存在可靠的存储设备上,即使节点损坏也不会导致日志丢失
所有节点都不会永久性损坏(损坏后可以恢复)

阶段 说明
投票 本地执行事务成功(各节点事务信息写入redo,undo日志)的节点发消息给协调者,内容是"同意",如果某个节点执行失败,返回消息"终止"
提交执行阶段 如果协调者收到的回复全部是"同意",协调者发起消息"正式提交",节点正式完成操作,参与者节点返回"完成"信息,协调者如果收到全部都回复为"完成"时,完成事务; 如果出现一个"终止",协调者向所有参与节点发起"回滚"请求,参与者节点利用之前的undo日志,执行回滚操作,然后发送给协调者"回滚完成",收到所有参与者的回滚完成消息后,协调者取消事务

  1. 如何实现以下场景; 一个业务处理,包含两个操作,如何实现操作b 失败时,a操作事务不回滚,只是回滚b操作
//1. 获取连接
conn=getConn();
SavePoint savePoint=null;
// 2. 关闭自动提交事务  conn.setAutoCommit(false)
try{

A(发工资);
savePoint=conn.setSavePoint();

B(工资发放通知短信)(出现异常,如果A也回滚,那么就没工资了)
}catch(Exception e){
if(savePoint!=null){
// 说明 A 发工资是没有异常,那么就是B 发短信出现了异常
A 需要提交
conn.rollBack(savePoint);
conn.commit();
}
// A 操作出错,全部回滚
else{
conn.roolBack();
}
}
  1. spring aop 的实现方式
    基于注解,基于配置
-  核心是定义切点:需要被增强的方法
 -  定义通知:具体要增加的功能
-    定义切面:切点与通知组合形成切面
-  定义代理工厂对象(ProxyFactoryBean):将目标对象,切面组合,目标对象接口组合到一起用于制造代理对象

    
    <bean id="myPointCut" class="org.springframework.aop.support.JdkRegexpMethodPointcut">
        <property name="pattern" value=".*study"/>
    bean>
    
    <bean id="myAdvice" class="top.forethought.framework.aop.xml.StudyAdvice">
    bean>
    
    <bean id="myAdvisor" class="org.springframework.aop.support.DefaultPointcutAdvisor">
        <property name="advice"  ref="myAdvice"/>
        <property name="pointcut" ref="myPointCut"/>
    bean>
    
    <bean id="studentProxy" class="org.springframework.aop.framework.ProxyFactoryBean">
        <property name="target" ref="studentObj"/>
        <property name="interceptorNames" value="myAdvisor" />
        <property name="proxyInterfaces" value="top.forethought.framework.aop.BaseInterface" />
    bean>
   
    <bean id="studentObj" class="top.forethought.framework.aop.xml.Student">bean>
问题 解答
什么是AOP 面向切面编程,是oop的补充
使用场景,用途 不改变原有代码,横向抽取(相对于继承而言)添加功能,比如事务管理,缓存,日志(可能是手写代码记录日志,有些日志是不需要记录的)
spring aop java 编写,运行期通过代理方式向目标类织入增强代理(cglib ,不依赖接口类,使用字节码增强),查看字节码发现是extends
jdk 的invocationhandler 依赖接口,反射生成动态代理类,查看生成的class文件发现是implements

起初,jdk 动态代理运行效率低于cglib,创建效率高,但随着jdk版本的提高,1.8版本jdk动态代理的运行效率已经高于cglib
jdk 动态代理基于接口 (可以观察生成的字节码)
cglib 动态代理基于继承

spring 也可以配置使用jdk动态代理

 proxy-target-class=true表示使用CGLib代理,false 则使用jdk 动态代理
  1. aop 的几个术语示意图
  • [目标类]需要被aop作用的类,也就是需要被增强的类(初始类)
  • [ 连接点] :可能被增强的的方法
  • [ 切入点] :被增强的方法
  • [通知] : before(),after() ,具体实现切入点需要增强的功能
  • [织入] weaving:将原始类和切入点组合在一起生成代理类的过程
  • [代理类] (proxy):是原始类和切入点组合的类(拥有与原始类一样的方法,只是方法中使用了通知)
  • [切面] aspect:通知+切点构成一个"面"

丧心病狂的面试知识_第2张图片
13. 什么是微服务
单体应用,功能很多,使用的技术也较为统一,难以升级新技术,时间推移,代码维护难度比较大。扩展比较难,耦合可能比较高。
微服务:将某些相对独立的功能以服务的形式抽离出来,单独开发,不同的服务可以使用不同的技术开发,通过http通信的方式给整个应用提供支持,从外部看,是一个完整的整体。

hibernate

  1. 锁机制有什么用?简述Hibernate的悲观锁和乐观锁机制
    锁机制:有些业务逻辑在操作数据时,需要保持排他性,不希望数据被其他操作修改

悲观锁:悲观的认为极有可能存在修改数据的并发事务(包括本系统的其他事务或来自外部系统的事务),于是将处理的数据设置为锁定状态。悲观锁必须依赖数据库本身的锁机制才能真正保证数据访问的排他性

乐观锁:乐观的认为并发访问时很少的,使用版本号来标识,每次操作将版本号+1, 读取数据时获取一次数据库版本号,写入数据时,如果新的版本号大于数据库版本号,允许操作;否则认为是过期数据无法更新。Hibernate中通过Session的get()和load()方法从数据库中加载对象时可以通过参数指定使用悲观锁;而乐观锁可以通过给实体类加整型的版本字段再通过XML或@Version注解进行配置
2. hibernate 与mybatis 对比
3. hibernate 对象的三态(暂时态(新创建的),持久态(在数据库中有相应的记录,与session关联),游离态(与session 缓存失去关联),)

jvm

1. 类加载过程

**1,加载**:是类加载的一个步骤,主要是
      a,通过全限定名获取定义此类的二进制字节流
      b,将字节流所代表的的静态数据结构转为方法区的运行时数据结构
      c,在内存中生成一个代表此类额java.lang.Class 对象,作为方法区这个类的各个数据访问的入口
       
**2, 验证**: 确保Class文件的字节流中包含的信息是否符合当前虚拟机的要求,不对当前虚拟机造成危害
**3, 准备**:正式为**类变量**(static 修饰的,不包含成员变量)分配内存并设置类变量的初始值阶段(这里的初始值
通常指零值,但是如果是static final ,则会生成Constant Value 属性,值为java代码指定的值)
**4, 解析**:机将常量池中的符号引用替换为直接引用
**5, 初始化**:执行类中定义的代码(执行程序员代码的主观代码初始化)

2. 对象内存分布

:创建的实例对象在此,线程共享(由于是GC的主要发生场所,又叫GC堆)
:(本地方法栈+虚拟机栈(hotspot 虚拟机已经合并) :
对象的内存布局:

含义
对象头 运行时数据(hash,GC分代年龄,锁状态标志,线程持有的锁,偏向线程id,偏向时间戳)+类型指针
实例数据 程序代码中定义的各种类型字段内容,包括从父类继承下来的
对齐填充 非必须,占位填充

3. 垃圾回收算法

算法名 描述 特点(适用场景)
标记-清除 通过GCRoot 对象,引用链标记,被标记的表示存活,未标记对象被清除 会产生大不连续内存碎片,如果后序出现大对象,会提前触发垃圾回收
复制 内存对半分,只能使用其中一半,将存活对象拷贝到另一半内存,然后清除之前的内存 无内存碎片,空间利用率低,如果存活率高,效率较低
标记-整理 标记,然后将存活对象往某一端移动,然后抹去边界之外的空间 无内存碎片,无空间浪费
分代收集 根据对象的存活率来使用不同的回收算法 少量存活,选用复制算法;大量存活,则选择标记-清除或者标记-整理
  1. jvm 参数
  2. io
    a. InputStream,OutputStream

4 NIO

NIO(NEW IO),可以通过本地方法在堆外开辟内存(直接内存),减少在jvm定义的内存中复制,提高效率(基于缓冲与管道)

5. 内存分配与回收策略

相关参数
优先在Eden分配 Eden区没足够空间分配时,虚拟机将发起MinorGC(发生在新生代的GC
大对象直接进入老年代 大对象是指需要连续分配内存的对象,比如长字符串以及数组 参数-XX:pREtenureSzieSizeThreshold,大于此值的对象直接在老年代分配
超期存活的对象进入老年代 对象年龄(Age)计数器,在Eden区出生,经过一次MinorGC 之后,仍然存活并且能被survivor区收纳,移到survivor区,年龄设置为1,每熬过一次MinorGC,年龄+1,默认是到达15时,进入到老年代 称为老年代的阈值可以使用-XX:MaxTenuringThreshold
动态对象年龄判定 如果survivor区相同年龄对象的内存总和大于survivor区内存的一半,则将年龄>=该年龄的对象直接进入老年代
空间分配担保 在发生MinorGC之前,检查老年代最大可用连续空间是否大于新生代所有对象总空间,如果成立,MinorGc 就确保是安全的。如果不成立,检查HandlerPromotionFailure 是否设置为允许担保失败,如果是允许担保失败,继续检查老年代最大连续可用空间是否大于历次晋升到老年代对象的平均大小,如果大于,则尝试经进行这次有风险的MinorGC。 如果不允许冒险,使用FULLGC

双亲委派模型优缺点

类加载器:用来实现类的加载动作
常见类加载器:

加载器名 描述
启动类加载器(Bootstrap ClassLoader) c++实现,加载java_home\lib 下的类
扩展类加载器(Extension ClassLoader) java 实现,加载java_home\lib\ext 目录下
应用类加载器(ApplicationClassLoader) 负责加载ClassPath 上指定的类库 ,(如果没有自定义类加载器,那么程序中默认是使用这个类加载器)

双亲委派模型::指java虚拟机只存在两种不同的类加载器,意识c++ 实现的BootstrapClassLoader,另一种则是其他用java实现的类加载器
委派:是指类加载器加载对象时,总是尝试让自己的父类加载器去完成,如父类加载器不能完成,再自己完成类加载
优点: java 类随着他的类加载器一起具备了一种带有优先级的层次关系. 例如java.lang.Object,存在不于rt.jar,这样在各种类加载器环境中都是同一个类

redis

1. 为什么速度快

单线程(没有上下文切换,操作大多基于内存),数据结构简单(hash的使用),

2. 底层的数据结构

string,hash(适合对象 ,hset 对象名 属性名 值),zSet()有序

redis的动态字符串:
不以’\0’ 结束
字符串定义属性有:

属性 含义
int free 剩余可用空间长度
int len 当前字符串内容长度
char [] buf 字符串数组
相比c中传统的字符串有啥优点呢?
1. 不使用'\0'结尾,读取内容是o(1)  :起始地址到len
2. 杜绝缓冲区溢出,c字符串只能通过'\0'来计算长度,
比如:连续的存放两个字符串  a='redis' b='memcache'
 如果将a 修改为'redisclauster',那么会导致内存覆盖到字符串b,导致b 的内容发生改变
 通过free 属性,可以判断出剩余的空闲内存能否容纳修改后的字符串值
如果不能容纳:free=修改后字符串长度(预分配内存)
len=修改后字符串长度
以后的修改可以减少内存的分配次数(应为预先分配了)
 3. 减少内存分配次数
 4. 可以保存二进制数据(因为不再以'\0'作为字符串结尾判断,而是通过len)

3. 与memecached 对比

比较项 redis memcache
持久化 aof(append ),rdb日志
数据类型 string ,list,set,hash,zset
分布式 支持(redis clauster)) 不支持(可以通过在客户端使用一致性hash来实现分布式存储,在存储和查询时,需要现在客户端计算一次数据所在节点)
内存管理机制 长久未使用的valu交换到磁盘 一直在内存,使用特定长度的块存储数据,完全解决内存碎片问题,但是内存利用率不高
键过期时间 每个键可设置过期时间,到期自动删除
内存淘汰策略 可设置内存最大使用量,超过时,执行数据淘汰策略(6中)

4. redis持久化机制

aof:appendonly file

机制:redis 每收到一个写命令,都通过write 函数追加到文件中(默认是appendonly.aof)

appendonly yes              //启用aof持久化方式
# appendfsync always      //每次收到写命令就立即强制写入磁盘,最慢的,但是保证完全的持久化,不推荐使用
appendfsync everysec     //每秒钟强制写入磁盘一次,在性能和持久化方面做了很好的折中,推荐
# appendfsync no    //完全依赖os,性能最好,持久化没保证

如何压缩aof 文件?提供bgrewriteaof 命令,收到此命令,redis 将内存中的数据已命令的方式保存到临时文件中,最后替换原来的文件

  • redis 调用fork,现在有父子连个进程
  • 子进程根据内存中的数据库快照,往临时文件中写入重建数据库状态的命令
  • 父进程继续处理client请求,除了把写命令写入到原来的aof文件中,同时将写命令缓存.这是为了保证如果子进程重写失败而不出现问题
  • 当子进程把快照内容以命令的方式写入到临时文件中后,子进程发信号通知父进程,然后父进程把缓存的命令也写入到临时文件
  • 最后父进程使用临时文件替换老的aof文件,并且重名名,后面收到的写命令也开始往新的aof文件追加
    注:重写aof文件,并没有读取旧的aof文件,而是将整个内存中的数据库内容以命令的方式重写了一个新的aof文件,类似快照

rdb:定时将内存中数据快照写入到二进制文件.rdb中

 save 900 1     #900秒内如果超过1个key被修改,则发起快照保存
   save 300 10    #300秒内容如超过10个key被修改,则发起快照保存
   save 60 10000

5. redis 淘汰策略

策略 描述
volatile-lru 从已设置过期时间的数据集中挑选最近最少使用的数据淘汰
volatile-ttl 从已设置过期时间的数据集中挑选将要过期的数据淘汰
volatile-random 从已设置过期时间的数据集中任意选择数据淘汰
allkeys-lru 从所有数据集中选择使用lru
allkeys-random
noeviction 禁止驱逐数据
使用 Redis 缓存数据时,为了提高缓存命中率,需要保证缓存数据都是热点数据。可以将内存最大使用量设置为热点数据占用的内存量,然后启用 allkeys-lru 淘汰策略,将最近最少使用的数据淘汰。

Redis 4.0 引入了 volatile-lfu 和 allkeys-lfu 淘汰策略,LFU 策略通过统计访问频率,将访问频率最少的键值对淘汰

6使用场景

a. 消息队列(由于Redis的列表是使用双向链表实现的,保存了头尾节点,所以在列表头尾两边插取元素都是非常快的)
b. 缓存
5. 缓存穿透:查询数据库不存在的数据:缓存null值(过期时间设置较小)
6. 缓存雪崩:缓存集中失效,不同类型产品设置不同过期时间,并且加上随机因子
7. 缓存击穿(单个点):某个缓存数据成为热点,失效时,大量的并发查询数据库(可以考虑将爆款设置为永不过期)
8. 缓存一致性

1.更新服务器时,立即更新缓存,或者删除对应缓存
2.读取缓存是,判断是否为最新缓存,若不是,需要冲到服务器读取

6. 分布式缓存: Master-slave

slave 发送请求让master传递最新的内存快照给自己
在传输过程中,master 正在接受的数据,放到与slave建立的连接的缓存里(list),快照传输完成后,发送缓存中的数据给slave
借助redis,memched等缓存工具,将服务器本地缓存同步到分布式缓存服务器,需要时间,实现难度较大

7. 分布式锁

命令 含义
setNX set not exist:如果key不存在,就设置上,返回1,否则返回0
getset 返回就值并且设置新值(相当于是原子操作,是单独使用get ,再set所不能达到的效果)
分布式锁的实现:
现在有并发线程 c1,c2,锁被C3持有,并且C3 已经挂掉
c1,c2 分别使用 setNX("LOCK");//显然是失败(返回0)的,而且锁也没被释放
c1 del锁,加锁成功
c2 del 锁,加锁成功(但是这是错误的)

解决:
加锁时,将加锁时间戳,以及过期时间(即持有锁的最长时间)
并发线程 c1,c2,锁被C3持有,并且C3 已经挂掉
c4 setNX("LOCK")// 返回0
如果获取失败:
c4  get  命令,检查是否过期,
        如果没过期,继续等待/ /检查上次加锁时间以及过期时间算出当前锁是否应该被释放  (lockTime+expirTime

7. 事物的实现(队列,其实出错后,不会回滚没出错的指令)

命令multi  开启事务

set key1 val1
set key2 val2
....

命令:exec 提交事务
你会发现一旦一条指令出错,其余是**不会回滚的。**
redis 是将多个命令放入一个队列,输入exec 命令,依次执行队列中的命令

丧心病狂的面试知识_第3张图片

rabbitmq

1. 协议

2. 交换机模式

direct:
单发多接收:

3. 使用场景

化同步为异步(应用内部,流量削峰,比如下单秒杀,用户比较活跃,同步会导致计算机性能下降),松耦合(多个应用之间)

场景 描述
单发单接收 简单的发送与接收,没有特别的处理
单发多接收 一个发送端,多个接收端,如分布式的任务派发。为了保证消息发送的可靠性,不丢失消息,使消息持久化了。同时为了防止接收端在处理消息时down掉,只有在消息处理完成后才发送ack消息
Publish/Subscribe 发布、订阅模式,发送端发送广播消息,多个接收端接收
Routing (按路线发送接收 发送端按routing key发送消息,不同的接收端按不同的routing key接收消息

4. 持久化

默认是没有开启持久化的,如果在产生消息时,指定消息时持久消息,那么消息会发送到持久化交换机,然后交给持久化队列,(会将信息记录在持久化日志中),如果消息被路由到非持久队列,那么持久化日志中就会删掉持久化记录

多线程

1. 线程与进程对比

进程是指运行中的程序,有独立的内存空间
线程是进程中最小的执行单元,共享进程的内存空间,以及上下文,当然也有自己

2. 线程的状态(生命周期)

新建(new 创建)
就绪(调用了start)
运行中(running)
阻塞(由于锁导致),
无限等待(需要其他线程唤醒),
有限等待(时间到,就继续),
终止(线程的run 执行完就结束)

3. 线程的实现方式(继承thread,实现runnable 接口,或者实现Callable 接口)

4. 锁的概念

5. synchronized 与lock 接口对比

synhronized lock 描述 方法
隐示获取锁,释放锁(先获取,再释放,简化同步,但是缺少扩展) 显式获取锁,释放锁
可中断的获取锁 在获取锁的过程,可以响应线程中断,当获取到锁的的线程被中断时,锁可以被释放) lockInterruptibly()
尝试非阻塞式的获取锁 当前线程尝试获取锁,如果这一时刻锁没有被其他线程占有,则成功获取锁 tryLock()
超时获取锁 在指定的截止时间之前返回(1.获得锁,返回true 2.被中断,抛出异常 3. 超时时间结束,返回false) boolean tryLock(long time,TimeUnit unit)

6. 锁分类

名称 概念 特点
偏向锁 如果对象头的偏向锁标志位指向当前线程,那么当前线程不需要通过cas 获取锁,直接进入同步块
自旋锁 线程获取锁失败,通过循环尝试获取锁,占用cpu,避免线程切换带来开销 需要注意自旋的次数以及间隔时间,不要无限自旋下去(jvm默认自旋次数是10,可以使用-XX:PreBlockSpin更改;除此之外,1.6引入自适应自旋锁,自旋时间取决于当前线程上次获得锁用时长度,如果上次获取锁是在短时间内获取,那么这次自旋也很可能成功获取锁,那么jvm可以给出较长的时间,给他;如果上一次获取锁耗时很长,那么这次也认为他很难通过自旋获取到锁,那么就直接省略自旋操作)
可重入锁 当前线程如果已经获取到锁,可以重复加锁 比如递归调用某种需要加锁的方法时,不会被自己阻塞
排它锁(独占锁) 只能由一个线程获取到锁,如果锁已经被其他线程获取,当前线程被阻塞 比如synchronized,ReentrantLock
共享锁 可多个线程同时获取到锁 比如semphore,信号量,可以控制并发的线程数(连接池可以使用)
读写锁 使用锁分离的思想,读锁时共享可重入锁,写锁是独占可重入锁 写锁加锁过程:如果当前线程是已经获取到锁的线程,可以直接获取锁(可重入),如果写锁状态为0,表示没有线程获取到写锁,当前线程获取锁,写锁状态+1。 写锁释放:可以将写锁状态置为0,读锁加锁过程:如果当前线程是持有写锁的线程,或者是写锁状态为0,则可以获取读锁,读锁状态+1;否则需要被阻塞;锁降级:持有写锁的线程,可以直接获取读锁,然后释放持有的写锁
公平锁 线程最终获取到锁的顺序和请求锁的顺序一致(也就是FIFO)
非公平锁 同一个线程可能连续多次获取到锁 可减少上下文切换,提高吞吐量(也是锁的默认实现方式)

7. AQS=AbstractQueueSynchronized

并发同步器,使用来实现Lock接口定义方法功能的基础
同步队列:双向链表, 加锁时,如果同步队列为空,使用cas将其设置到队列头
如果同步队列不空,使用cas将其设置到队列尾部
释放锁:(头节点是获取同步状态成功的线程节点),释放锁时,会唤起后继节点
由于某些节点发生中断以及为null的情况,从队列尾部向前找,找到所谓的后继节点,然后由获取同步状态成功的线程去将头结点设置为自己所在节点(这个过程不需要cas)
每个非头部节点自旋检测自己的前驱结点是否为头节点,如果是尾头节点,那么该节点可以尝试获取同步状态;除此之外,所有节点之间基本没有数据通信

状态队列:

9. synchonized 加锁过程

几个概念:

概念 解释
mark word 用于存储对象自身的运行时数据,比如哈希码,GC分代年龄,锁状态标志,线程持有的锁,偏向线程id,偏向时间
对象头
监视器 用来实现同步,与每一个java对象关联,每个java对象都有一个监视器与之对应,是synchronized实现内置锁的基础
自旋锁 (忙人) 让不满足条件的线程,等待一段时间,看能不能获取锁,通过占用处理器,避免线程切换带来开销(注意自旋时间和次数应该有一定限制,到达限制,任然没有获取锁,则需要挂起等待)
偏向锁(熟人) 大多数情况下,同一个对象的锁总是由同一个线程多次获得。当一个线程访问同步块,并且获取到锁,会在对象头和栈帧中的锁记录里存储锁偏向(偏爱)的线程id,偏向锁是可重入锁(可以想象带上synchronized修饰的递归方法,就会同一个线程多次进入同步代码块,但不会每次每次进入去获取锁,第一次就获取到了)。如果锁的对象头中的markword存储着指向当前线程的偏向锁,则不需要重新通过cas 加锁,解锁,持有偏向锁的线程(不处于活动状态)时,才会释放锁。偏向锁无法使用自旋锁优化,因为有其他线程来竞争锁,就破坏了偏向锁的假设前提
轻量级锁 相对重量级锁而言,轻量级锁是为了减少在无实际竞争情况下,使用重量级锁带来的性能消耗。jvm 在当前线程的栈帧中创建用于存储锁记录的空间,并且将对象头中的mark word 复制到锁记录中,然后尝试CAS 将对象头的mark word 替换为指向锁记录的指针,如果成功,则表示当前线程获取锁成功,失败,则表示当前存在其他线程在竞争锁。当前线程采用自旋的方式获取,自旋失败,则升级为重量级锁
重量级锁 通过对象内部的监视器实现,底层依赖操作系统的Mutex Lock 实现,操作系统实现线程的切换需要从用户态到内核态的切换,成本很高。线程竞争不使用自旋,不消耗cpu。线程进入阻塞,等待被唤醒。

7. object 的notify (notifyall)方法与wait方法的作用

(必须使用在synchronized修饰的{} 内部)
wait 将持有该对象的线程进入waiting状态,同时会释放锁,只有其他线程获取到该对象的锁,执行对象的notify方法,然后释放锁,waiting状态才会转变为runnable

9. 什么是线程安全?

 线程安全就是说多线程访问同一代码,不会产生不确定的结果
 (主存与内存,线程是读取主存的变量的拷贝,操作完之后将结果写入主内存,多个线程操作可能导致,读取的虽然是同个数据,各自对自己的副本进行操作,写入主内存时,可能出现只保存其中一个线程的结果)

10. 线程安全的集合类,不安全的类的对比,线程安全的实现方式

方式1:同步集合类,Vector,HashTable,
方式2:不可变容器类,unmodifiableXXX
方式3:读写分离,CopyOnWriteArrayList(对写加锁,读不加锁)
方式4:锁细化:1.7 的concurrentHashMap 分片锁, 1.8的头节点加锁
concurrentHashMap也有乐观锁的用法,在doPut 方法中,
方式5:skipList,无锁并发,利用概率算法计算出元素层(level),可实现o(lgn) 查找
插入节点时,需要判断后继节点之前插入marker,隔离防止并发操作
插入之后,新节点的next 指向marker 的next

方式5:跳跃表:ConcurrentSkipListMap,
a . hashmap (不安全)与hashTable(synchonized ,全表锁定) ,currentHashMap(分片锁,(1.7)(1.8 使用锁分离,只对头结点加锁)
b. ArrayList 与Vector
11. hashmap线程不安全,有线程安全的map吗?(并发包的currentHashMap(1.7分片锁),或者是Collections 下的unmodifierableMap,所有的写操作都是直接抛出异常)
10.hashmap与hashtable
后者是同步(速度就会降低)
迭代器:前者是fail-fast 类型,遍历过程不允许删除,后者(enumerator迭代器)允许
12. 保证线程安全的方式有哪些

  • [线程封闭] 将对象封装到一个线程里,只有这一个线程看到这个对象,实现方式
    a. 堆栈封闭,局部变量,无并发问题
    b. ThreadLoacl,根本就么有并发问题,看查看set 方法
    丧心病狂的面试知识_第4张图片
  • [使用不可变对象],比如Collections.unmodifiablexxx,final 关键字修饰变量
  • 加同步锁,代码层面synchronized关键字,数据库层面是乐观锁和悲观锁
  1. ThreadLocal 的作用
    可以当做是线程的本地变量处理,是与调用的线程所绑定的
  2. synchronized 关键字底层实现原理
  3. 什么是cas?
  4. cas 面临的问题,以及如何解决
可能面临的问题 解决方式
ABA cas 在操作值的时候,会检查值是否发生变化,如果没有发生变化,则更新,但是如果数据由A ->B->A ,检查发现A 没有改变,(事实上已经改变),解决思路使用 版本号,1A->2B->3A
循环时间长,开销大 CAS 如果循环时间过长,会对cpu造成较大的开销,解决思路:1. 如果jvm支持处理器提供的pause指令,可以延迟流水线执行指令,也可以避免在退出循环的时候因内存顺序冲突引起cpu流水线被清空
  1. 并发编程的最佳实践

1,使用本地变量
2.使用不可边量
3.最小化锁的作用与范围
4. 使用线程池的Excutor,而不是直接new Thread 执行
5. 宁可使用同步,也不要使用现成的wait 与notify
6.使用BlockingQueueingQueue 实现生产-消费模式
7. 使用并发集合而不是使用加了锁的同步集合
8. 使用Semaphore 创建有界的访问
9. 宁可使用同步代码块,也不实用同步的方法
10. 避免使用静态变量(在并发情况下,使用final,使用只读集合)

  1. 线程调度方式:
线程调度方式 含义
协同式调度 当前线程执行完,通知cpu切换到另一个线程(可能长时间阻塞,如果当前线程时长时间任务)
抢占式调度 每个线程由系统来分配时间,线程切换不由线程自身决定,java 就是使用这种方式

注意:虽然java中使用抢占式调度,线程还是可以"建议"系统给自己分配多一些时间,设置线程优先级(java 有10个)
但是操作系统有可能自己来改变线程的优先级,比如windows,优先级推进器,发现某些工作效率很高的线程,那么会额外的给他更多的时间

计算机网络

  1. 三次握手与四次挥手

  2. 三次握手(为了确认双方发送与接受数据是没问题的)能保证一定建立连接吗?
        a与b建立连接
        第一次握手(syn=1,seq=client_isn) a说我想和你建立联系,你能不能收到我的消息啊(确认b能收到自己的消息)
        第二次握手:(syn=1,seq=server_isn,ack=client_isn+1)b 回复可以啊,但是你能不能收到我发送的消息啊
        第三次握手:(syn=0,seq=client_isn+1,ack=server_isn+1)a说,老弟,我能收到你的消息,赶紧发数据给我吧,老板催我干活呢
    三次握手:保证了双方都信任对方能接受到自己的消息,那么就可以发正式的数据了
    注:
    ack 字段是为了告诉对方自己已经收到了你的ack-1 那条信息
    seq 字段是为了标识这条记录是自己这边的编号,对方返回的ack 是基于我这个seq来的

四次挥手:假设A发起断开连接请求
A:“喂,我不说了。”A->FIN_WAIT1
B:“我知道了。等下,上一句还没说完。Balabala……”B->CLOSE_WAIT | A->FIN_WAIT2
B:”好了,说完了,我也不说了。”B->LAST_ACK
A:”我知道了。”A->TIME_WAIT | B->CLOSED

A等待2MSL,保证B收到了消息,否则重说一次”我知道了”,A->CLOSED

  1. tcp与udp 的区别
     
    1.基于连接与无连接;
    2.对系统资源的要求(TCP较多,UDP少);
    3.UDP程序结构较简单;
    4.流模式与数据报模式 ;
    5.TCP保证数据正确性,UDP可能丢包,TCP保证数据顺序,UDP不保证。
    UDP 用于实时性要求比较高,比如直播,tcp适用于通信可靠性比较高的情景
  2. 常见状态码含义
  3. ip地址分类
  4. URI 与URL

操作系统

  1. 什么是死锁
    所有申请的资源都被其他等待进程占有,那么该等待进程有可能在无法改变其状态,这种情况称为死锁(deadlock)
  2. 造成死锁条件
条件(全部满足发生死锁) 描述 解决方式
互斥 至少有一个资源处于非共享模式(即一次只有一个进程使用)如果另一个进程申请该资源,那么申请进程必须等到该资源被释放为止
占有并等待 一个进程必须占有至少一个资源,并且等待另一个资源,而该资源为其他进程所占有
非抢占 资源不能被抢占(即资源只能在进程完成任务后自动释放
循环等待 一组等待进程,p0,pn ,p0 等待p1,p1等待p2,…pn 等待p1
  1. 解决或者是避免死锁的算法

  2. 银行家算法

  3. 哲学家吃饭问题

  4. rpc(socket+反射)
    rpc 是一种协议
    实现此协议的有tomcat,dubbo 等等

  5. 什么是CPU流水线(将指令分成多步,不同指令各步操作重叠,从而实现几条指令并行处理,加速程序运行)

 以洗车为例:A,B,C,D四辆车,都需要喷水->洗洁剂->擦洗->烘干四个步骤
   如果不将洗车这个任务成多个小步骤,那么B 车必须等到A 烘干后才能开始洗车(有些步骤的场所就是空闲的)
   如果使用流水线:
   如果A 车到了洗洁剂,那么B 车可以前往喷水(这样很多步骤不会闲着,等其他步骤完成,提高了效率)
  1. 进程间通信的方式
    父,子进程间共享状态信息,共享文件表,但是不共享用户地址空间。 进程有自己的独立的地址空间,不会担心进程的虚拟内存相互覆盖
    但是独立的地址空间,使得进程间通信变得更加困难
    (需要使用显式的IPC 通信)
进程间通信的方式 描述
套接字 不同主机上的进程间通信,交换任意的字节流
  1. 内存交换技术
PCB:进程控制块,常驻内存,如果内存紧张,会将进程数据对换到磁盘,控制块挂起,当内存不紧张时,停止交换
控制块被调度时,将磁盘中进程读取到内存
  1. 内存覆盖技术
  2. 内存分配算法
名称 描述 分区排列顺序 特点 缺点
首次适应 从头到尾到合适的分区 空闲分区以地址递增次序排列 综合看性能最好,算法开销小,回收分区后一般不需要对分区队列重新排序
最佳适应 优先使用更小的分区,已保留更大的分区 空闲分区以容量递增次序排列 会有更多的大分区被保留,满足大进程使用 会产生很多太小、难以利用的碎片:算法开销大,回收分区可能需要对空闲分区队列重新排序
最坏适应 优先使用更大的分区,以防产生太小的不可用的内存碎片 空闲分区以容量递减排列 可以减少难以利用的小碎片 大分区容易被用完,不利于大进程:算法开销大,理由同上
临近适应 由首次适应演变而来,每次从上次查找结束位置开始查找 空闲分区以地址递增 不用每次从低地址的小分区开始检索,算法开销小,原因同首次适应算法 会使高分区的大分区也被用完

观察可以知道;首次适应,最佳适应,最坏适应,以及邻近适应中,首次适应是最佛系的,没有特别要求说是优先使用大,小空闲块,而是遇到合适的,就使用;不适合就继续往下找
首次适应于邻近适应的区别是:首次适应每次都是从头开始查找,邻近适应是从上次查找结束的位置开始
最佳适应与最坏适应区别是:前者是优先使用较小的空闲块,后者是优先使用大空闲块

mysql

  1. mysql 支持的存储引擎

innodb(事务性) vs Myisam(不支持事务)

innodb Myisam
B+
支持事务 不支持事务
支持表级锁和行级锁 只支持表级锁

redo log和undo log都属于InnoDB的事务日志

innodb 事务持久性的实现原理:使用redo log

数据存储在磁盘中,但是io速度很慢,于是有了缓冲(buffer pool),缓冲中保存磁盘中部分数据页的映射,
查询数据时,先到缓冲查询,,如果没有,读取磁盘,加入到缓冲,这提高了查询效率.
写数据也是先写入缓冲,定时将缓冲中数据刷到磁盘中(刷脏)
但是问题来了,如果突然断电,或者宕机,缓存中的数据就消失了,会导致丢失数据,无法保证事务持久性.
redo log 出现了,
1. 在日志将一个事务中需要执行的sql记录下来,
2. 接着往缓存写数据
3. 事物提交时,调用fsync 接口将redo log中的sql 执行刷入到磁盘
4. 如果宕机,只会丢失缓存中的数据,但是缓存中的数据是在记录到redolog 之后才添加的,也就是redo log还保留着数据的记录,读取redo log,执行操作,这就保证了事物的持久性

至于  redolog 也是磁盘io,为什么速度会比缓存中刷脏速度快呢?
5. 缓存刷脏数随机io,redolog是追加,属于顺序io
6. 刷脏是以数据页(Page)为单位的,MySQL默认页大小是16KB,
一个Page上一个小修改都要整页写入;而redo log中只包含真正需要写入的部分,无效IO大大减少

原子性,隔离性实现原理:undo log(原子性,单元要么都成功,要么都不成功一个出现失败,就需要回滚)

InnoDB实现回滚,靠的是undo log:当事务对数据库进行修改时,InnoDB会生成对应的undo log;
如果事务执行失败或调用了rollback,导致事务需要回滚,便可以利用undo log中的信息将数据
回滚到修改之前的样子。

undo log属于逻辑日志,它记录的是sql执行相关的信息。当发生回滚时,InnoDB会根据undo log的内容
做与之前相反的工作:对于每个insert,回滚时会执行delete;对于每个delete,回滚时会执行insert;
对于每个update,回滚时会执行一个相反的update,把数据改回去。
  1. 引擎的对比以及使用场景

1. 
读多写少:使用myisam
写多读少:(需要保证事物).使用innodb


  1. 索引的实现原理
    底层使用B+树实现
  2. 为什么不使用hash,AVL等数据结构呢?
    B,B+ 是自平衡的多叉树,相同数量的节点,B,B+ 树高度会低于二叉树
    如果使用avl,有序序列是中序遍历,也就是说得到有序序列,需要往根回退
    hash 对于查找单个数据很快,但是对于范围查找就显得无能为力了
B树 B+树
非叶子节点有数据 叶子节点无数据,只有key (节省空间,可存放更多的索引值),每个节点大小一致(一个界节点可以对应上磁盘上的一个数据页,避免了跨页查找) ,减少io,提高性能
叶子节点有数据 叶子节点有数据,并且有指针相连,成有序单链表(方便顺序查找)
  1. 并发版本控制 MVCC
    通过在数据表中添加一列隐藏列记录该条记录版本号之类的东西,
    假设读取数据时:版本号是 A
    更新:事务开始时版本号设置为A+1,但是提交失误时,发现数据库中版本号不再是A+1了,就是说明数据库记录被另一个线程修改了,而当前的修改就是在旧记录上修改而成的,显然是不满住要求的,所以重新取出数据库的记录,执行操作
    并发版本控制实现了无锁操作数据,提高了并发性能
每行存储版本号,而不是存储事件实际发生的时间。
  每次事物的开始这个版本号都会增加。自记录时间开始,每个事物都会保存记录的系统版本号。

秒杀系统顺序:

化创建订单,减库存并行为串行
一般是:数据裤查库存,大于0,创建订单
但是如果此时是并行访问,可能只有1件商品,但是却创建了两件订单

改进:使用redis的 原子减操作,如果返回结果<0,表示没有库存,反回秒杀结束
但是又有另一个场景redis减库存成功,但是数据库创建订单失败
改进:化并行为串行,原子减操作>=0,
redis 设置 userid-goodsid 这样的记录,表示已经提交过秒杀订单,不能重复秒杀.
将秒杀请求放入消息队列
消费者取出消息队列中的订单请求数据,创建订单,减数据库秒杀库存

  1. 预加载库存到redis
  2. 下单请求
  3. 查询redis是否存在userId-goodsId(避免重复单)
  4. 非重复单,查询(redis,原子减)库存(减库存)(库存小于0,则返回秒杀结束)
  5. 减库存成功,请求入队,返回排队中
  6. 消费者,取队列消息,查询数据库中库存,如果为0,该请求不能创建订单
  7. 创建订单:先查数据库库存,(减数据库库存),再建立订单(可以在同一个事务中提交))

电话一面补漏

  1. 为什么要重写equal方法?
    答案:因为Object的equal方法默认是两个对象的引用的比较,意思就是指向同一内存,地址则相等,否则不相等;如果你现在需要利用对象里面的值来判断是否相等,则重载equal方法。
Note that it is generally necessary to override the {@code hashCode}
     * method whenever this method is overridden, so as to maintain the
     * general contract for the {@code hashCode} method, which states
     * that equal objects must have equal hash codes.
  1. 为什么重写hashCode方法?
    因为规定;equals 成立的对象必须hashcode相同
    hashMap 中:key 的位置计算是通过hash ,key 的是否存在是通过equals //如果equals 成立,而hash 不等,那么在hashMap 这些结构中是找不到放入的key 的
    答案:一般的地方不需要重载hashCode,只有当类需要放在HashTable、HashMap、HashSet等等hash结构的集合时才会 重载hashCode,那么为什么要重载hashCode呢?就HashMap来说,好比HashMap就是一个大内存块,里面有很多小内存块,小内存块 里面是一系列的对象,可以利用hashCode来查找小内存块hashCode%size(小内存块数量),所以当equal相等时,hashCode必 须相等,而且如果是object对象,必须重载hashCode和equal方法。

hasmap 的普通方法:

key1,key2 如果equals 成立,但是没有重写hashcode,那么就能同时存在key1和key2 在hashmap中
在通过key1执行get方法时,通过equals 成立的key去获取数据,会出现获取到不同的数据

 public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }

hashMap 的get 方法:
要求hash相同并且 (key == 或者equeals)

 final Node<K,V> getNode(int hash, Object key) {
 
 if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        return e;
                        }
  1. http 协议
    超文本传输协议,基于tcp实现

  2. 微信绑定登录时是如何实现的
    如何获取openId 的??
    在登录拦截器中,判断当前用户是否登录;
    如果未登录,
    调用微信api提供的一个接口(获取openid),主要参数:appid+回调接口地址(自己开发的)
    微信api收到请求后,会回调你的接口
    在自定义的回调接口中,需要调用weixin api的另一个接口,这时才能够返回openid

大致流程是这样的:
登录拦截器->
调用微信api接口1(带上appid,自定义回调接口1)
->微信调用自定义回调接口1(微信返回一个code)
->自定义回调接口1 处理逻辑:接收微信返回的code,然后再带上参数appid,请求微信api的2号接口
->微信api接口2返回openid

(有点像是三次握手,客户端发起请求,表明身份,服务端返回code,表示同意你请求,然后客户端带着code 去请求服务端获取数据)
5. 阅读计数怎么实现的
阅读记录表: userid-文章id生成一条记录(按照文章发布月份建立表,避免判断某篇文章是否被某人阅读还需要跨表查询的问题)
文章表 添加字段 :阅读次数,点赞数

点赞与阅读计数 具体场景处理;

前提:
文章表(文章id,title,content,点赞数,阅读数)
阅读记录表:(id,文章id,用户id,阅读时间,是否点赞)

阅读次数:同一用户阅读同一篇文章,重复阅读只算一次阅读
8 .如何获得阅读总数(某一篇文章)

先查缓存,存在,则返回
如果不存在,查数据库,并且存入缓存,返回
  1. 阅读次数怎么处理?是用select count(*) 吗?
文章表添加字段记录总阅读数,总点赞数类似

10.阅读次数怎么更新,实时吗?

阅读次数 实时在缓存中更新,定时将缓存更新到数据库
  1. 用户当天反复阅读,怎么办?(如何避免重复查询用户是否阅读过这篇文章)
       查询文章详情时
       1. 取缓存,是否存在用户阅读记录,
                1.1 若存在,返回
        2若不存在,则查数据库
                     2.1若依然不存在,存入阅读记录,缓存也存入一条记录,缓存中 总阅读次数+1
                     2.2如果数据库存在,也是存入缓存,返回(这里避免了短时间内反复阅读时,反复查询数据库,去判断是否需要插入阅读记录)
  1. 点赞次数怎么处理?(同阅读次数)
  2. 如何处理用户的频繁点赞,取消点赞
  1. 查询缓存中是否存在点赞记录,
           1.1如果存在,更新缓存中点赞状态;
  2 如果不存在,取数据库点赞记录,
               2.1 数据库存在,修改点赞状态(只是修改缓存中的点赞状态,数据库中此时还不修改)
               2.2  如果不存在,数据库插入点赞记录; 
   2. 更新(或插入)缓存中点赞状态;  更新缓存中点赞总数
   3. 定时器同步缓存中的点赞总数到数据库;同步各用户的点赞状态到数据库
  1. 如果随着时间推移,阅读记录表变得很大怎么处理?
    可以考虑分表:
 1. 将按照文章发布年月来建立阅读记录表,一个月内发布的文章的阅读记录全部在同一张表,通过文章发布年月定位到阅读记录表名
 2.  或者按照文章发布的日期,1-31 建立31张表,每月同一天发布的文章的阅读记录都放到同一张表

电话2,3面知识点

  1. http 协议

  2. 如何分布式架构下的秒杀系统,如何设计,比如如何限流(令牌筒,扩容(负载均衡))

  3. 如何保证集群(主-从库的高可用,如果主库崩掉了咋办)
    可投票选举新的主节点
    崩掉的节点是有log的,redolog,重启会自动执行

  4. 消息队列分布式下面如何使用

  5. 线程池的配置方法(配置的理论依据,比如配置多少合适啊,)
    需要考虑是cpu 密集型还是io密集型,如果是cpu密集,则表示cpu空闲时间少,线程数可以设置为N(cpu)+1

  6. 线程池的好处
    a. 可以即时响应,任务到达时,不必创建线程后再执行
    b. 提高线程的可管理性,线程是不能无限制创建的,线程池可以统一分配,调优,和监控
    c. 降低资源消耗避免创建,销毁带来的消耗

  7. 线程池的实现原理

这里的是书上原话,感觉有问题!!

当一个任务到来时
a. 线程池查看核心线程是否都在工作
 1. 如果不是,创建一个新的工作线程来执行任务
 2. 否则,进入下一个步骤
b. 线程池判断工作队列是否已满
 1. 如果没满,将新提交的任务储存在工作队列
 2. 否则,进入下个流程
 
 c. 线程池判断线程池中线程是否都处于工作状态
 1. 如果没有,创建一个新的工作线程来执行任务
 2. 否则交给饱和策略处理这个任务 
 

线程池参数:

参数名 含义
corePoolSize 线程池基本大小,即使当前线程足够完成任务,但是线程数量小于该值,会继续创建线程
  1. 系统中用到了redis,有没有过性能对比
  2. 有没有看过日志(redis,mysql)
  3. 看的书籍(并发艺术,jvm 需要再看看,一定要随时随地可以说出类加载,垃圾回收,线程池,锁机制,synchronized与lock接口, ReentrantLock,CountDownLatch, 特点以及使用场景)
  4. 并发包,底层实现以及特点
  5. 存储引擎没问

自我总结:不善于引导面试官去往自己擅长的(了解的方面引导)

比如:

  • 问:问道为什么使用redis
  • 答:因为redis是得益于底层的独特的数据结构,单线程,以及数据基本是存在于内存中,效率奇高,适用场景之一是用作缓存,减少查询对数据库的压力,提高查询速度
(暗示了了四个可提问点,
1,数据结构实现(string,zMap,Hash,),可能还有事务等等
2,单线程有啥好处,
3,内存那就相对于磁盘了,数据存储方式
4,缓存可以延伸出适用场景)
  • 1 问:既然你说到了使用场景,那除了缓存之外还有其他什么场景吗?
  • 答:redis除了用作缓存外,数据也可以持久化到磁盘,主要是aop与rdb两种方式,也就是说可以当做数据库使用,他本本身也是key-val型数据库,(类似的还有memache);
    除此之外,redis自带了订阅-发布功能,可以实现普通的消息队列的类似的功能
暗示了其他提问点:
1. aop 与rdb有啥区别,具体说说(前者是基于日志(也就是文件),后者是基于二进制文件),持久化的效率不一样,
而且 配置方式也不一样,
2. memched也是key-val 型,这两者有啥区别?  (可支持的数据类型,缓存淘汰机制,持久化,是否支持分布式,
等方面区别)
3. 订阅发布,与中间件,比如rabbitmq有啥区别?(可能需要从稳定性,分布式,使用场景等等方面去比较)
  • 2问:我看你项目使用了rabbitmq,为什么使用rabbitmq
  • 答:秒杀场景,是典型的高并发场景,众多用户同时抢购同一件为数不多的商品,可能会出现重复订单,以及超卖现象,而mq的一种应用场景则是化同步为异步(化并行为串行),然后巴拉巴拉
暗示:
4. 消息队列的其它使用场景
5. 消息队列使用到的设计模式(订阅-发布模式)
  • 3问: 这样用户会不会由于迟迟不能得知是否秒杀成功,体验不好
  • 答:用户请求一放入消息队列,就返回给前端排队中,然后前端轮询查询用户订单是否创建成功(或者是咋在订单创建成功后,给客户端推送一条消息,或者发送一条短信,商品抢购成功,请前往支付)
  • 4问:可以不使用消息队列实现秒杀这一要求吗
  • 可以,并发场景所有的问题都是归根于对于某些资源,或者数据的能否线程安全的操作,也就是读和写
    读需要保证各线程看到的是一致的,写需要保证不能统一时刻写,写结果而且需要给其他线程可见
    这里的秒杀线程并发的问题无疑是在以下几个地方:
1.查询库存: (查数据库,多个线程查询数据库,对数据库造成压力,可以使用redis预减库存
,查库存可在redis 中进行,因为redis是单进程单线程的,并发的请求自然而然的变成了串行的方式)
2. 创建订单:如果是并发的创建订单,删减也可能会出现问题,当然数据库也有行级锁,表级锁,
还有MVCC,这些东西可以被暗示提出来
 这可以使用redis 实现分布式锁,主要实现思路:
   1.setNx
   2.设置锁超时时间(比如锁最长持有10秒,这需要根据业务处理的时间来定,不能太长也不能太短),比如客户端挂了
   锁没有释放,那么其他客户端一段时间可以获取到锁
   3. 自旋获取锁,配置尝试次数
   4. 限时获取锁,这指定时间内获取不到锁,就返回
   5. 针对获取到锁的客户端没释放锁就挂掉的情况,其他客户端获取锁之前可以先setbnX,
如果失败,自旋获取当前锁超时时间,如果超时时间已经过了,删除锁,然后setNx,获取到锁

  • 5问:这两者推荐使用哪一种呢?
  • 个人建议使用消息队列,可以立刻给前端以响应,加锁获取锁都需要时间,因为秒杀的商品不一定是一件,假设是1000种秒杀商品,使用锁,那么会有1000个锁,极端点也就是1000个并发线程,各自操作各自的商品,这样对数据库还是有冲击的(因为数据库连接池对连接数是有限制的,也就是除了这个分布式锁之外,请求也可能阻塞在获取数据连接上),但是使用消息队列完全就是异步了,不是简单的串行,对数据库基本就没有什么压力
  • 6问:消息队列还有其他适用场景吗
    1. 应用解耦(高内聚,低耦合\迪米特原则)(比如订单系统与库存系统之间的,不能因为库存系统减库存失败,导致订单创建失败,库存是可以机动的调整的,而不仅仅是字面上的数字)
    1. 流量削峰(秒杀)
    1. 异步处理:非必要业务逻辑异步处理,比如注册短信发送,传统响应时间是=写入数据库+发送短信
      异步处理后:只要写入数据库成功,就可以响应用户,短信可以在之后发送也是可以的
  • 7问:平时喜欢看一些技术书籍,那么你看了哪些书籍
  • 看了jvm,和java并发艺术:
    jvm 让我了解到虚拟机内部的一些列巧妙的设计(分代回收),还有一个类从.class 文件到对象的全部过程;
    并发艺术打开了我对多线程学习的大门,还有一些jdk中一些经典数据结构的具体应用(hashmap,红黑树,跳跃表,双向队列并发容器),还有锁机制以及实现,lock接口,线程池,队列等等,才开始发现数据结构是如此的重要
  • N 问,谈谈锁机制,线程安全,红黑树,synchronized与Lock 接口(或者是reentrentLock)区别,锁降级(读写锁),锁分离,锁优化(自旋,锁粒度扩张,锁粒度细化),类加载机制,双亲委派模型,同步容器与并发容器对比(数据结构,加锁方式)

N面被虐

  1. 两个有序数组求交集
    需要给出时间复杂度分析和方法对比
  2. 设计排行榜,千万用户,得分实时变化, (id,得分,排名)
    需要的功能:
    根据id获取得分
    根据id获取排名
    获取topN
    思路:使用redis实现,key=id,val=score
    zSet
    根据得分范围分段,比如得分范围是1-100万
    则可以0-10,10-20, 使用10个redis,各自负责10万数据的存储
    当分数发生变化时:检查新得分是否还在当前redis范围,如果不在就删除,然后插入到合适的redis中
    根据id获取得分,则需要在这10个reddis 桶中去查找
    可以在某个redis记录id对应的redis编号,方便快速去准确的redis中查找数据
    根据id获取排名:这点需要注意,每个同存储的记录需要单独记录,比如10号redis当前存储1000个
    9号当前存储2000个,那么第8个redis的某个用户的排名=1000+2000+当前桶中排名
    获取topN: 检查n落在哪一个桶内,如果落在9号redis,则需要取出10号全部数据+9号的前 n-10号筒数据量数

spring 为什么要使用工厂模式?

直观的感受是代替了new 操作
更多的是由于某些对象创建需要很多参数
工厂通过反射创建对象,其余参数配置放到配置文件

volatile 为什么使用?

volitaile 变量在修改后,立刻将值刷回主内存,会将其他线程持有的该变量在本地内存置为失效,使得其他线程使用前必须从主内存读取
happens before :读在其他线程写之后(可以将其看成是一把锁,来理解)

事务的隔离级别

hasemap 扩容原理,为什么选择2的n次幂去扩容?

因为在确定key的链表头结点位置时,是通过hash&size
如果不是2的n次幂
会出现:
比如 15
1111,全部是用上了
如果是10
0110 会有几位是浪费掉的

海量数据,千万或者是亿万级别,两两成对的数据,

加入一个不一样的数,找出这个数
使用位运算异或:相同的数据异或结束坑定是0,0^任何数=数本身

你可能感兴趣的:(走心系列)