2019年丰巢科技Java面试题

1.bio与nio的区别
1、bio同步阻塞io:在此种⽅式下,⽤户进程在发起⼀个IO操作以后,必须等待IO操作的完成,只有当真正完成了IO操作以后,⽤户
进程才能运⾏。JAVA传统的IO模型属于此种⽅式!
2、nio同步⾮阻塞式I/O;java NIO采⽤了双向通道进⾏数据传输,在通道上我们可以注册我们感兴趣的事件:连接事件、读写事件;
NIO主要有三⼤核⼼部分:Channel(通道),Buffer(缓冲区), Selector。传统IO基于字节流和字符流进⾏操作,⽽NIO基于Channel和
Buffer(缓冲区)进⾏操作,数据总是从通道读取到缓冲区中,或者从缓冲区写⼊到通道中。Selector(选择区)⽤于监听多个通道的事件
(⽐如:连接打开,数据到达)。因此,单个线程可以监听多个数据通道。

  1. BIO (Blocking I/O):同步阻塞I/O模式,数据的读取写⼊必须阻塞在⼀个线程内等待其完成。这⾥使⽤那个经典的烧开⽔例⼦,
    这⾥假设⼀个烧开⽔的场景,有⼀排⽔壶在烧开⽔,BIO的⼯作模式就是, 叫⼀个线程停留在⼀个⽔壶那,直到这个⽔壶烧开,才去
    处理下⼀个⽔壶。但是实际上线程在等待⽔壶烧开的时间段什么都没有做。
  2. NIO (New I/O):同时⽀持阻塞与⾮阻塞模式,但这⾥我们以其同步⾮阻塞I/O模式来说明,那么什么叫做同步⾮阻塞?如果还拿
    烧开⽔来说,NIO的做法是叫⼀个线程不断的轮询每个⽔壶的状态,看看是否有⽔壶的状态发⽣了改变,从⽽进⾏下⼀步的操作。
  3. AIO ( Asynchronous I/O):异步⾮阻塞I/O模型。异步⾮阻塞与同步⾮阻塞的区别在哪⾥?异步⾮阻塞⽆需⼀个线程去轮询所有
    IO操作的状态改变,在相应的状态改变后,系统会通知对应的线程来处理。对应到烧开⽔中就是,为每个⽔壶上⾯装了⼀个开关,⽔
    烧开之后,⽔壶会⾃动通知我⽔烧开了。
    2.select与poll的区别
    1、io多路复⽤:
    1、概念:IO多路复⽤是指内核⼀旦发现进程指定的⼀个或者多个IO条件准备读取,它就通知该进程。
    2、优势:与多进程和多线程技术相⽐,I/O多路复⽤技术的最⼤优势是系统开销⼩,系统不必创建进程/线程,也不必维护这些进
    程/线程,从⽽⼤⼤减⼩了系统的开销。
    3、系统:⽬前⽀持I/O多路复⽤的系统调⽤有 select,pselect,poll,epoll。
    2、select:select⽬前⼏乎在所有的平台上⽀持,其良好跨平台⽀持也是它的⼀个优点。select的⼀个缺点在于单个进程能够监视的
    ⽂件描述符的数量存在最⼤限制,在Linux上⼀般为1024,可以通过修改宏定义甚⾄重新编译内核的⽅式提升这⼀限制,但是这样也
    会造成效率的降低。
    3、poll:它没有最⼤连接数的限制,原因是它是基于链表来存储的,但是同样有⼀个缺点:
    a. ⼤量的fd的数组被整体复制于⽤户态和内核地址空间之间,⽽不管这样的复制是不是有意义。
    b. poll还有⼀个特点是“⽔平触发”,如果报告了fd后,没有被处理,那么下次poll时会再次报告该fd。
    epoll跟select都能提供多路I/O复⽤的解决⽅案。在现在的Linux内核⾥有都能够⽀持,其中epoll是Linux所特有,⽽select则应该是
    POSIX所规定,⼀般操作系统均有实现。
    3.zookeeper的⼯作原理
    1、定义:zookeeper是⼀种为分布式应⽤所设计的⾼可⽤、⾼性能且⼀致的开源协调服务,它提供了⼀项基本服务:分布式锁服务。
    后来摸索出了其他使⽤⽅法:配置维护、组服务、分布式消息队列、分布式通知/协调等。
    2、特点:
    1、能够⽤在⼤型分布式系统中;
    2、具有⼀致性、可⽤性、容错性,不会因为⼀个节点的错误⽽崩溃;
    3、⽤途:⽤户⼤型分布式系统,作协调服务⻆⾊;
    1、分布式锁应⽤:通过对集群进⾏master选举,来解决分布式系统中的单点故障(⼀主n从,主挂全挂)。
    2、协调服务;
    3、注册中⼼;
    4、原理:
    术语:
    数据结构Znode:zookeeper数据采⽤树形层次结构,和标准⽂件系统⾮常相似,树中每个节点被称为Znode;
    通知机制Watcher:zookeeper可以为所有的读操作(exists()、getChilden()及getData())设置watch,watch事件是⼀次
    性出发器,当watch的对象状态发⽣改变时,将会触发次对象上watch所对应的事件。watch事件将被异步的发送给客户端,并且zookeeper为
    watch机制提供了有序的⼀致性保证。
    基本流程:分布式锁应⽤场景
    1、传统的⼀主n从分布式系统,容易发⽣单点故障,传统解决⽅式是增加⼀个备⽤节点,定期给主节点发送Ping包,主节点回复
    ack,但是如果⽹络原因ack丢失,那么会出现两个主节点,造成数据混乱。
    2、zookeeper的引⼊可以管理两个主节点,其中挂了⼀个,会将另外⼀个作为新的主节点,挂的节点回来时担任备⽤节点;

4.cap理论
1、概念:⼀个分布式系统最多只能同时满⾜⼀致性(Consistency)、可⽤性(Availability)和分区容错性(Partition tolerance)这
三项中的两项。
2、⼀致性:更新操作成功并返回客户端完成后,所有节点在同⼀时间的数据完全⼀致,所以,⼀致性,说的就是数据⼀致性。
3、可⽤性:服务⼀直可⽤,⽽且是正常响应时间。
4、分区容错性:分布式系统在遇到某节点或⽹络分区故障的时候,仍然能够对外提供满⾜⼀致性和可⽤性的服务。
5.⼆段式满⾜cap理论的哪两个理论
两阶段提交协议在正常情况下能保证系统的强⼀致性,但是在出现异常情况下,当前处理的操作处于错误状态,需要管理员⼈⼯⼲预
解决,因此可⽤性不够好,这也符合CAP协议的⼀致性和可⽤性不能兼得的原理。
6.线程池的参数配置,为什么java官⽅提供⼯⼚⽅法给线程池
1、线程池简介:
2、核⼼参数:
3、⼯⼚⽅法作⽤:ThreadPoolExecutor类就是Executor的实现类,但ThreadPoolExecutor在使⽤上并不是那么⽅便,在实例化时需
要传⼊很多歌参数,还要考虑线程的并发数等与线程池运⾏效率有关的参数,所以官⽅建议使⽤Executors⼯程类来创建线程池对
象。
7.分布式框架dubbo的好处,不⽤dubbo可不可以。为什么要使⽤分布式
1、dubbo好处:
1、远程通讯: 提供对多种基于⻓连接的NIO框架抽象封装, 包括多种线程模型,序列化,以及“请求-响应”模式的信息交换⽅
式。
2、软负载均衡及容错机制: 提供基于接⼝⽅法的透明远程过程调⽤,包括多协议⽀持,以及软负载均衡,失败容错,地址路由,
动态配置等集群⽀持。
可在内⽹替代F5等硬件负载均衡器,降低成本,减少单点。
3、服务⾃动注册与发现: 基于注册中⼼⽬录服务,使服务消费⽅能动态的查找服务提供⽅,使地址透明,使服务提供⽅可以平滑
增加或减少机器 。
4、提供完善的管理控制台dubbo-admin与简单的控制中⼼dubbo-monitor
5、Dubbo提供了伸缩性很好的插件模型,很⽅便进⾏扩展(ExtensionLoader)
2、不⽤dubbo可不可以:可以,使⽤springcloud。
3、分布式作⽤:
a. 系统之间的耦合度⼤⼤降低,可以独⽴开发、独⽴部署、独⽴测试,系统与系统之间的边界⾮常明确,排错也变得相当容易,
开发效率⼤⼤提升。
b. 系统之间的耦合度降低,从⽽系统更易于扩展。我们可以针对性地扩展某些服务。假设这个商城要搞⼀次⼤促,下单量可能会
⼤⼤提升,因此我们可以针对性地提升订单系统、产品系统的节点数量,⽽对于后台管理系统、数据分析系统⽽⾔,节点数量维
持原有⽔平即可。
c. 服务的复⽤性更⾼。⽐如,当我们将⽤户系统作为单独的服务后,该公司所有的产品都可以使⽤该系统作为⽤户系统,⽆需重
复开发。
8.七个垃圾回收器之间如何搭配使⽤

  1. Serial New收集器是针对新⽣代的收集器,采⽤的是复制算法;
  2. Parallel New(并⾏)收集器,新⽣代采⽤复制算法,⽼年代采⽤标记整理;
  3. Parallel Scavenge(并⾏)收集器,针对新⽣代,采⽤复制收集算法;
  4. Serial Old(串⾏)收集器,新⽣代采⽤复制,⽼年代采⽤标记清理;
  5. Parallel Old(并⾏)收集器,针对⽼年代,标记整理;
  6. CMS收集器,基于标记清理;
  7. G1收集器(JDK):整体上是基于标记清理,局部采⽤复制;
    综上:新⽣代基本采⽤复制算法,⽼年代采⽤标记整理算法。cms采⽤标记清理;
    9.接⼝限流⽅案
  8. 限制 总并发数(⽐如 数据库连接池、线程池)
  9. 限制 瞬时并发数(如 nginx 的 limit_conn 模块,⽤来限制 瞬时并发连接数)
  10. 限制 时间窗⼝内的平均速率(如 Guava 的 RateLimiter、nginx 的 limit_req 模块,限制每秒的平均速率)
  11. 限制 远程接⼝ 调⽤速率
  12. 限制 MQ 的消费速率
  13. 可以根据 ⽹络连接数、⽹络流量、CPU 或 内存负载 等来限流
    10.ConcurrentHashMap使⽤原理
    1、⼯作机制(分⽚思想):它引⼊了⼀个“分段锁”的概念,具体可以理解为把⼀个⼤的Map拆分成N个⼩的segment,根据key.hashCode()
    来决定把key放到哪个HashTable中。可以提供相同的线程安全,但是效率提升N倍,默认提升16倍。
    2、应⽤:当读>写时使⽤,适合做缓存,在程序启动时初始化,之后可以被多个线程访问;
    3、hash冲突:
    1、简介:HashMap中调⽤hashCode()⽅法来计算hashCode。由于在Java中两个不同的对象可能有⼀样的hashCode,所以不同的键可
    能有⼀样hashCode,从⽽导致冲突的产⽣。
    2、hash冲突解决:使⽤平衡树来代替链表,当同⼀hash中的元素数量超过特定的值便会由链表切换到平衡树
    4、⽆锁读:ConcurrentHashMap之所以有较好的并发性是因为ConcurrentHashMap是⽆锁读和加锁写,并且利⽤了分段锁(不是在所有的
    entry上加锁,⽽是在⼀部分entry上加锁);
    读之前会先判断count(jdk1.6),其中的count是被volatile修饰的(当变量被volatile修饰后,每次更改该变量的时候会将更改结果写到
    系统主内存中,利⽤多处理器的缓存⼀致性,其他处理器会发现⾃⼰的缓存⾏对应的内存地址被修改,就会将⾃⼰处理器的缓存⾏设置为失效,并强制
    从系统主内存获取最新的数据。),故可以实现⽆锁读。
    11.解决map的并发问题⽅案
    HashMap不是线程安全的;Hashtable线程安全,但效率低,因为是Hashtable是使⽤synchronized的,所有线程竞争同⼀把锁;⽽
    ConcurrentHashMap不仅线程安全⽽且效率⾼,因为它包含⼀个segment数组,将数据分段存储,给每⼀段数据配⼀把锁,也就是所谓的锁分段技
    术。
    12.什么是协程,以及实现要点
    1、⽣产者/消费者模式不是⾼性能的实现:
    1.涉及到同步锁。
    2.涉及到线程阻塞状态和可运⾏状态之间的切换。
    3.涉及到线程上下⽂的切换。
    2、协成定义:协程,英⽂Coroutines,是⼀种⽐线程更加轻量级的存在。正如⼀个进程可以拥有多个线程⼀样,⼀个线程也可以拥
    有多个协程。最重要的是,协程不是被操作系统内核所管理,⽽完全是由程序所控制(也就是在⽤户态执⾏)。
    这样带来的好处就是性能得到了很⼤的提升,不会像线程切换那样消耗资源。
    3、协成优点:协程的暂停完全由程序控制,线程的阻塞状态是由操作系统内核来进⾏切换。因此,协程的开销远远⼩于线程的开
    销。
    4、实现:
    1、Lua语⾔
    Lua从5.0版本开始使⽤协程,通过扩展库coroutine来实现。
    2、Python语⾔
    正如刚才所写的代码示例,python可以通过 yield/send 的⽅式实现协程。在python 3.5以后, async/await 成为了更好的替
    代⽅案。
    3、Go语⾔
    Go语⾔对协程的实现⾮常强⼤⽽简洁,可以轻松创建成百上千个协程并发执⾏。
    4、Java语⾔
    Java语⾔并没有对协程的原⽣⽀持,但是某些开源框架模拟出了协程的功能
    13.lru cache 使⽤hash map 的实现(算法)
    1、概念:其实解释起来很简单,LRU就是Least Recently Used的缩写,翻译过来就是“最近最少使⽤”。也就是说LRU算法会将最近
    最少⽤的缓存移除,让给最新使⽤的缓存。⽽往往最常读取的,也就是读取次数最多的,所以利⽤好LRU算法,我们能够提供对热点
    数据的缓存效率,能够提⾼缓存服务的内存使⽤率。
    2、实现:
    1、思路:
    i. 限制缓存⼤⼩
    ii. 查询出最近最晚⽤的缓存
    iii. 给最近最少⽤的缓存做⼀个标识
    2、代码:
    1 import java.util.LinkedHashMap;
    2 import java.util.Map;
    3 /**
    4 * 简单⽤LinkedHashMap来实现的LRU算法的缓存
    5 */
    6 public class LRUCache extends LinkedHashMap {
    7 private int cacheSize;
    8 public LRUCache(int cacheSize) {
    9 super(16, (float) 0.75, true);
    10 this.cacheSize = cacheSize;
    11 }
    12 protected boolean removeEldestEntry(Map.Entry eldest) {
    13 return size() > cacheSize;
    14 }
    15 }
    14.图的深度遍历和⼴度遍历(算法)
    1、深度优先遍历:
    深度优先遍历结果是: A B E F C D G H I
    深度优先遍历尽可能优先往深层次进⾏搜索
    2、⼴度优先遍历:
    ⼴度优先遍历结果是: A B C D E F G H I
    ⼴度优先遍历按层次优先搜索最近的结点,⼀层⼀层往外搜索。
    15.基本排序(算法)
  14. 快速排序:
    a. 原理:快速排序采⽤的是⼀种分治的思想,它先找⼀个基准数,然后将⽐这个基准数⼩的数字都放到它的左边,然后再递归调⽤,分
    别对左右两边快速排序,直到每⼀边只有⼀个数字.整个排序就完成了.
    b. 复杂度:O(n)
    c. 特点:快速排序是我们平常最常使⽤的⼀种排序算法,因为它速度快,效率⾼,是最优秀的⼀种排序算法.
  15. 冒泡排序:
    a. 原理:冒泡排序其实就是逐⼀⽐较交换,进⾏⾥外两次循环,外层循环为遍历所有数字,逐个确定每个位置,⾥层循环为确定了位置
    后,遍历所有后⾯没有确定位置的数字,与该位置的数字进⾏⽐较,只要⽐该位置的数字⼩,就和该位置的数字进⾏交换.
    b. 复杂度:O(n^2),最佳时间复杂度为O(n)
    c. 特点:冒泡排序在我们实际开发中,使⽤的还是⽐较少的.它更加适合数据规模⽐较少的时候,因为它的效率是⽐较低的,但是优点
    是逻辑简单,容易让我们记得.
  16. 直接插⼊排序:
    a. 原理:直接插⼊排序是将从第⼆个数字开始,逐个拿出来,插⼊到之前排好序的数列⾥.
    b. 复杂度:O(n^2),最佳时间复杂度为O(n)
    c. 特点:
  17. 直接选择排序:
    a. 原理:直接选择排序是从第⼀个位置开始遍历位置,找到剩余未排序的数据⾥最⼩的,找到最⼩的后,再做交换
    b. 复杂度:O(n^2)
    c. 特点:和冒泡排序⼀样,逻辑简单,但是效率不⾼,适合少量的数据排序
    16.设计模式的使⽤
    答案⻅“题库/3、设计模式”;
    17.java 8 流式使⽤
    1 List evens = nums.stream().filter(num -> num % 2 == 0).collect(Collectors.toList());
    2 //1、stream()操作将集合转换成⼀个流,
    3 //2、filter()执⾏我们⾃定义的筛选处理,这⾥是通过lambda表达式筛选出所有偶数,
    4 //3、最后我们通过collect()对结果进⾏封装处理,并通过Collectors.toList()指定其封装成为⼀个List集合返回。
    18.说说b+树?
  18. B-tree:
    B-tree 利⽤了磁盘块的特性进⾏构建的树。每个磁盘块⼀个节点,每个节点包含了很关键字。把树的节点关键字增多后树的层级
    ⽐原来的⼆叉树少了,减少数据查找的次数和复杂度。
    B-tree巧妙利⽤了磁盘预读原理,将⼀个节点的⼤⼩设为等于⼀个⻚(每⻚为4K),这样每个节点只需要⼀次I/O就可以完全载
    ⼊。
    B-tree 的数据可以存在任何节点中。
  19. B+tree:
    B+tree 是 B-tree 的变种,B+tree 数据只存储在叶⼦节点中。这样在B树的基础上每个节点存储的关键字数更多,树的层级更少
    所以查询数据更快,所有指关键字指针都存在叶⼦节点,所以每次查找的次数都相同所以查询速度更稳定;
    19.内存屏障与volatile:
  20. 由于现代操作系统都是多处理器操作系统,每个处理器都会有⾃⼰的缓存,可能存再不同处理器缓存不⼀致的问题,⽽且由于操作
    系统可能存在重排序,导致读取到错误的数据,因此,操作系统提供了⼀些内存屏障以解决这种问题。
    LoadLoad屏障
    对于Load1; LoadLoad; Load2 ,操作系统保证在Load2及后续的读操作读取之前,Load1已经读取。
    StoreStore屏障
    对于Store1; StoreStore; Store2 ,操作系统保证在Store2及后续的写操作写⼊之前,Store1已经写⼊。
    LoadStore屏障
    对于Load1; LoadStore; Store2,操作系统保证在Store2及后续写⼊操作执⾏前,Load1已经读取。
    StoreLoad屏障
    对于Store1; StoreLoad; Load2 ,操作系统保证在Load2及后续读取操作执⾏前,Store1已经写⼊,开销较⼤,但是同时具备其
    他三种屏障的效果。
  21. 当我们声明某个变量为volatile时,这个变量便具有了线程可⻅性。volatile通过在读写操作前后添加内存屏障,完成了数据的及时
    可⻅性。

当写⼊⼀个volatile变量时,JMM会把该线程对应的本地内存中的共享变量刷新到主内存。
当读⼀个volatile变量时,JMM会把该线程对应的本地内存置为⽆效,从主内存中读取所有的共享变量。
4.

  • volatile读之前,会添加LoadLoad内存屏障。
  • volatile读之后,会添加LoadStore内存屏障。
  • volatile写之前,会添加StoreStore内存屏障。
  • volatile写之后,会添加StoreLoad型内存屏障。
    20.java 域的概念
    field,域是⼀种属性,可以是⼀个类变量,⼀个对象变量,⼀个对象⽅法变量或者是⼀个函数的参数。
    21.分布式设计领域的概念
    1、分布式系统设计的两⼤思路:中⼼化和去中⼼化
    中⼼化:中⼼化的设计思想在⾃然界和⼈类⽣活中是如此的普遍和⾃然,它的设计思想也很简单,分布式集群中的节点按照⻆
    ⾊分⼯,可以分为两种⻆⾊–“领导”和“⼲活的”,中⼼化的⼀个思路就是“领导”通常分发任务并监督“⼲活的”,谁空闲了就给它
    安排任务,谁病倒了就⼀脚踢出去,然后把它的任务分给其他⼈;中⼼化的另⼀个思路是领导只负责⽣成任务⽽不再指派任务,
    由每个“⼲活的”⾃发去领任务。
    去中⼼化:全球IP互联⽹就是⼀个典型的去中⼼化的分布式控制架构,联⽹的任意设备宕机都只会影响很⼩范围的功能。去中
    ⼼化设计通常没有“领导”和“⼲活的”,⻆⾊⼀样,地位平等,因此不存在单点故障。实际上,完全意义的去中⼼化分布式系统并
    不多⻅,很多看起来是去中⼼化但⼯作机制采⽤了中⼼化设计思想的分布式系统正在不断涌现,在这种架构下,集群中的领导是
    动态选择出来的,⽽不是⼈为预先指定的,⽽且在集群发⽣故障的情况下,集群的成员会⾃发举⾏会议选举新的领导。典型案例
    如:zookeeper、以及Go语⾔实现的Etcd。
    2、分布式系统的⼀致性原理
    在说明⼀致性原理之前,可以先了解⼀下cap理论和base理论,具体⻅《事务与柔性事务》中的说明。
    对于多副本的⼀致性处理,通常有⼏种⽅法:同步更新–即写操作需要等待两个节点都更新成功才返回,这样的话如果⼀旦发
    ⽣⽹络分区故障,写操作便不可⽤,牺牲了A。异步更新–即写操作直接返回,不需要等待节点更新成功,节点异步地去更新数
    据,这种⽅式,牺牲了C来保证A。折衷–只要保证集群中超过半数的节点正常并达到⼀致性即可满⾜要求,此时读操作只要⽐较
    副本集数据的修改时间或者版本号即可选出最新的,所以系统是强⼀致性的。如果允许“数据⼀致性存在延迟时间”,则是最终⼀
    致性。
    如Cassandra中的折衷型⽅案QUORUM,只要超过半数的节点更新成功便返回,读取时返回多数副本的⼀致的值。然后,对于
    不⼀致的副本,可以通过read repair的⽅式解决。read repair:读取某条数据时,查询所有副本中的这条数据,⽐较数据与⼤多
    数副本的最新数据是否⼀致,若否,则进⾏⼀致性修复。此种情况是强⼀致性的。
    ⼜如Redis的master-slave模式,更新成功⼀个节点即返回,其他节点异步地去备份数据。这种⽅式只保证了最终⼀致性。最
    终⼀致性:相⽐于数据时刻保持⼀致的强⼀致性,最终⼀致性允许某段时间内数据不⼀致。但是随着时间的增⻓,数据最终会到
    达⼀致的状态。此种情况只能保证最终⼀致性。著名的DNS也是最终⼀致性的成功例⼦。
    强⼀致性算法:1989年就诞⽣了著名的Paxos经典算法(zookeeper就采⽤了Paxos算法的近亲兄弟Zab算法),但由于Paxos
    算法难以理解、实现和排错,所以不断有⼈尝试优化算法,2013年终于有了重⼤突破:Raft算法的出现,其中Go语⾔实现的Raft
    算法就是Etcd,功能类似于zookeeper。
    Base的思想:基本可⽤、柔性状态、最终⼀致性,主要针对数据库领域的数据拆分,通过数据分⽚(如Mycat、Amodeba等)
    来提升系统的可⽤性。由于分⽚拆分后会涉及分布式事务,所以接下来看⼀下如何⽤最终⼀致性的思路来实现分布式事务,也就
    是柔性事务。
    3、柔性事务:具体⻅《事务与柔性事务》。
    4、分布式系统的关键Zookeeper
    ⽬标是解决分布式系统的⼏个问题:集群集中化配置,集群节点动态发现机制,简单可靠的节点Leader选举机制,分布式锁。
    ZNode有⼀个ACL访问权限控制列表,提供对节点增删改查的API,提供监听ZNode变化的实时通知接⼝–Watch接⼝。
    ZNode类型:持久节点(可以实现配置中⼼)、临时节点(和创建这个节点的客户端会话绑定,可实现集群节点动态发现,可
    以实现服务注册中⼼)、时序节点(创建节点时会加上数字后缀,通过选择编号最⼩的ZNode可以实现Leader选举机制)、临时
    性时序节点(同时具备临时节点和时序节点的特性,主要⽤于分布式锁的实现)。
    22.如何实现双11的购物限流(redis实现⽅案)
    1、限流策略:
    Nginx接⼊层限流
    按照⼀定的规则如帐号、IP、系统调⽤逻辑等在Nginx层⾯做限流
    业务应⽤系统限流
    通过业务代码控制流量这个流量可以被称为信号量,可以理解成是⼀种锁,它可以限制⼀项资源最多能同时被多少进程访问。
    2、lua脚本:
    1 local key = KEYS[1] --限流KEY(⼀秒⼀个)
    2 local limit = tonumber(ARGV[1]) --限流⼤⼩
    3 local current = tonumber(redis.call(‘get’, key) or “0”)
    4 if current + 1 > limit then --如果超出限流⼤⼩
    5 return 0
    6 else --请求数+1,并设置2秒过期
    7 redis.call(“INCRBY”, key,“1”)
    8 redis.call(“expire”, key,“2”)
    9 end
    10 return 1
    减少⽹络开销: 不使⽤ Lua 的代码需要向 Redis 发送多次请求, ⽽脚本只需⼀次即可, 减少⽹络传输;
    原⼦操作: Redis 将整个脚本作为⼀个原⼦执⾏, ⽆需担⼼并发, 也就⽆需事务;
    复⽤: 脚本会永久保存 Redis 中, 其他客户端可继续使⽤.
    2、ip限流lua脚本:
    1 local key = “rate.limit:” … KEYS[1]
    2 local limit = tonumber(ARGV[1])
    3 local expire_time = ARGV[2]
    4
    5 local is_exists = redis.call(“EXISTS”, key)
    6 if is_exists == 1 then
    7 if redis.call(“INCR”, key) > limit then
    8 return 0
    9 else
    10 return 1
    11 end
    12 else
    13 redis.call(“SET”, key, 1)
    14 redis.call(“EXPIRE”, key, expire_time)
    15 return 1
    16 end
    3、java执⾏代码:
    1 import org.apache.commons.io.FileUtils;
    2
    3 import redis.clients.jedis.Jedis;
    4
    5 import java.io.File;
    6 import java.io.IOException;
    7 import java.net.URISyntaxException;
    8 import java.util.ArrayList;
    9 import java.util.List;
    10 import java.util.concurrent.CountDownLatch;
    11
    12 public class RedisLimitRateWithLUA {
    13
    14 public static void main(String[] args) {
    15 final CountDownLatch latch = new CountDownLatch(1);
    16
    17 for (int i = 0; i < 7; i++) {
    18 new Thread(new Runnable() {
    19 public void run() {
    20 try {
    21 latch.await();
    22 System.out.println(“请求是否被执⾏:”+accquire());
    23 } catch (Exception e) {
    24 e.printStackTrace();
    25 }
    26 }
    27 }).start();
    28
    29 }
    30
    31 latch.countDown();
    32 }
    33
    34 public static boolean accquire() throws IOException, URISyntaxException {
    35 Jedis jedis = new Jedis(“127.0.0.1”);
    36 File luaFile = new File(RedisLimitRateWithLUA.class.getResource("/").toURI().getPath() + “limit.lua”
    37 String luaScript = FileUtils.readFileToString(luaFile);
    38
    39 String key = “ip:” + System.currentTimeMillis()/1000; // 当前秒
    40 String limit = “5”; // 最⼤限制
    41 List keys = new ArrayList();
    42 keys.add(key);
    43 List args = new ArrayList();
    44 args.add(limit);
    45 Long result = (Long)(jedis.eval(luaScript, keys, args)); // 执⾏lua脚本,传⼊参数
    46 return result == 1;
    47 }
    48 }
    49
    23.mysql调优
    1、选择最合适的字段属性:类型、⻓度、是否允许NULL等;尽量把字段设为not null,⼀⾯查询时对⽐是否为null;
    2.要尽量避免全表扫描,⾸先应考虑在 where 及 order by 涉及的列上建⽴索引。
    3.应尽量避免在 where ⼦句中对字段进⾏ null 值判断、使⽤!= 或 <> 操作符,否则将导致引擎放弃使⽤索引⽽进⾏全表扫描
    4.应尽量避免在 where ⼦句中使⽤ or 来连接条件,如果⼀个字段有索引,⼀个字段没有索引,将导致引擎放弃使⽤索引⽽进⾏全表扫描
    5.in 和 not in 也要慎⽤,否则会导致全表扫描
    6.模糊查询也将导致全表扫描,若要提⾼效率,可以考虑字段建⽴前置索引或⽤全⽂检索;
    7.如果在 where ⼦句中使⽤参数,也会导致全表扫描。因为SQL只有在运⾏时才会解析局部变量,但优化程序不能将访问计划的选择推迟到运
    ⾏时;它必须在编译时进⾏选择。然 ⽽,如果在编译时建⽴访问计划,变量的值还是未知的,因⽽⽆法作为索引选择的输⼊项。
    9.应尽量避免在where⼦句中对字段进⾏函数操作,这将导致引擎放弃使⽤索引⽽进⾏全表扫描。
    10.不要在 where ⼦句中的“=”左边进⾏函数、算术运算或其他表达式运算,否则系统将可能⽆法正确使⽤索引。
    11.在使⽤索引字段作为条件时,如果该索引是复合索引,那么必须使⽤到该索引中的第⼀个字段作为条件时才能保证系统使⽤该索引,否则该
    索引将不会被使⽤,并且应尽可能的让字段顺序与索引顺序相⼀致。
    12.不要写⼀些没有意义的查询,如需要⽣成⼀个空表结构:
    13.Update 语句,如果只更改1、2个字段,不要Update全部字段,否则频繁调⽤会引起明显的性能消耗,同时带来⼤量⽇志。
    14.对于多张⼤数据量(这⾥⼏百条就算⼤了)的表JOIN,要先分⻚再JOIN,否则逻辑读会很⾼,性能很差。
    15.select count() from table;这样不带任何条件的count会引起全表扫描,并且没有任何业务意义,是⼀定要杜绝的。
    16.索引并不是越多越好,索引固然可以提⾼相应的 select 的效率,但同时也降低了 insert 及 update 的效率,因为 insert 或
    update 时有可能会重建索引,所以怎样建索引需要慎重考虑,视具体情况⽽定。⼀个表的索引数最好不要超过6个,若太多则应考虑⼀些不常使⽤到
    的列上建的索引是否有 必要。
    17.应尽可能的避免更新 clustered 索引数据列,因为 clustered 索引数据列的顺序就是表记录的物理存储顺序,⼀旦该列值改变将导致
    整个表记录的顺序的调整,会耗费相当⼤的资源。若应⽤系统需要频繁更新 clustered 索引数据列,那么需要考虑是否应将该索引建为
    clustered 索引。
    18.尽量使⽤数字型字段,若只含数值信息的字段尽量不要设计为字符型,这会降低查询和连接的性能,并会增加存储开销。这是因为引擎在处
    理查询和连 接时会逐个⽐较字符串中每⼀个字符,⽽对于数字型⽽⾔只需要⽐较⼀次就够了。
    19.尽可能的使⽤ varchar/nvarchar 代替 char/nchar ,因为⾸先变⻓字段存储空间⼩,可以节省存储空间,其次对于查询来说,在⼀
    个相对较⼩的字段内搜索效率显然要⾼些。
    20.任何地⽅都不要使⽤ select * from t ,⽤具体的字段列表代替“
    ”,不要返回⽤不到的任何字段。
    21.尽量使⽤表变量来代替临时表。如果表变量包含⼤量数据,请注意索引⾮常有限(只有主键索引)。
  1. 避免频繁创建和删除临时表,以减少系统表资源的消耗。临时表并不是不可使⽤,适当地使⽤它们可以使某些例程更有效,例如,当需要重
    复引⽤⼤型表或常⽤表中的某个数据集时。但是,对于⼀次性事件, 最好使⽤导出表。
    23.在新建临时表时,如果⼀次性插⼊数据量很⼤,那么可以使⽤ select into 代替 create table,避免造成⼤量 log ,以提⾼速度;
    如果数据量不⼤,为了缓和系统表的资源,应先create table,然后insert。
    24.如果使⽤到了临时表,在存储过程的最后务必将所有的临时表显式删除,先 truncate table ,然后 drop table ,这样可以避免系统
    表的较⻓时间锁定。
    25.尽量避免使⽤游标,因为游标的效率较差,如果游标操作的数据超过1万⾏,那么就应该考虑改写。
    26.使⽤基于游标的⽅法或临时表⽅法之前,应先寻找基于集的解决⽅案来解决问题,基于集的⽅法通常更有效。
    27.与临时表⼀样,游标并不是不可使⽤。对⼩型数据集使⽤ FAST_FORWARD 游标通常要优于其他逐⾏处理⽅法,尤其是在必须引⽤⼏个表才
    能获得所需的数据时。在结果集中包括“合计”的例程通常要⽐使⽤游标执⾏的速度快。如果开发时 间允许,基于游标的⽅法和基于集的⽅法都可以尝
    试⼀下,看哪⼀种⽅法的效果更好。
    28.在所有的存储过程和触发器的开始处设置 SET NOCOUNT ON ,在结束时设置 SET NOCOUNT OFF 。⽆需在执⾏存储过程和触发器的每个
    语句后向客户端发送 DONE_IN_PROC 消息。
    29.尽量避免⼤事务操作,提⾼系统并发能⼒。
    30.尽量避免向客户端返回⼤数据量,若数据量过⼤,应该考虑相应需求是否合理。
    24.cdn(异地多活)
    1、异地多活:异地多活指分布在异地的多个站点同时对外提供服务的业务场景。异地多活是⾼可⽤架构设计的⼀种,与传统的灾备设
    计的最主要区别在于“多活”,即所有站点都是同时在对外提供服务的。
    2、两地容灾切换⽅案:
    容灾是异地多活中最核⼼的⼀环, 以两个城市异地多活部署架构图为例:
    在两个城市(城市1位于华南1地域、城市2位于华东1地域)均部署⼀套完整的业务系统。
    下单业务按照“user_id”% 100 进⾏分⽚,在正常情况下:
    [00~49]分⽚所有的读写都在城市1的数据库实例主库。
    [50~99]分⽚所有的读写都在城市2的数据库实例主库。
    “城市1的数据库实例主库”和 “城市2的数据库实例主库”建⽴DTS双向复制。
    当出现异常时,需要进⾏容灾切换。可能出现的场景有以下4种:
    将第2种、第3种异常情况,全部采⽤第2种⽅案进⾏处理,那么不管是所有的APP Server异常、所有的数据库异常、整个城市异常,就
    直接按照城市级容灾⽅案处理,直接将APP Server、数据库切换到到另⼀个城市。
    3、多城异地多活:
    多城市异地多活模式指的是3个或者3个以上城市间部署异地多活。该模式下存在中⼼节点和单元节点:
    中⼼节点:指单元节点的增量数据都需要实时的同步到中⼼节点,同时中⼼节点将所有分⽚的增量数据同步到其他单元节点。
    单元节点:即对应分⽚读写的节点,该节点需要将该分⽚的增量同步到中⼼节点,并且接收来⾃于中⼼节点的其他分⽚的增量数据。
    下图是3城市异地多活架构图,其中华东1就是中⼼节点,华南1和华北1是单元节点。
    25.进程之间的通信⽅式
    1、匿名管道通信:
    a. ⽗进程创建管道,得到两个⽂件描述符指向管道的两端
    b. ⽗进程fork出⼦进程,⼦进程也有两个⽂件描述符指向同⼀管道。
    c. ⽗进程关闭fd[0],⼦进程关闭fd[1],即⽗进程关闭管道读端,⼦进程关闭管道写端(因为管道只⽀持单向通信)。⽗进程可以往
    管道⾥写,⼦进程可以从管道⾥读,管道是⽤环形队列实现的,数据从写端流⼊从读端流出,这样就实现了进程间通信。
    2、⾼级管道通信:将另⼀个程序当做⼀个新的进程在当前程序进程中启动,则它算是当前程序的⼦进程,这种⽅式我们成为⾼级管
    道⽅式。
    3、有名管道通信:有名管道也是半双⼯的通信⽅式,但是它允许⽆亲缘关系进程间的通信。
    4、消息队列通信:消息队列是由消息的链表,存放在内核中并由消息队列标识符标识。消息队列克服了信号传递信息少、管道只能
    承载⽆格式字节流以及缓冲区⼤⼩受限等缺点。
    5、信号量通信:
    6、信号通信:
    7、共享内存通信:
    8、套接字通信:
    26.tcp/ip协议、http协议
    答案⻅“⾯试题库/11、⽹络”
    27.写⼀个redis分布式锁:
  2. 实现代码:
    1 private static final String LOCK_SUCCESS = “OK”;
    2 private static final String SET_IF_NOT_EXIST = “NX”;
    3 private static final String SET_WITH_EXPIRE_TIME = “PX”;
    4 private static final Long RELEASE_SUCCESS = 1L;
    5
    6 // Redis客户端
    7 private Jedis jedis;
    8
    9 /**
    10 * 尝试获取分布式锁
    11 * @param lockKey 锁
    12 * @param expireTime 超期时间
    13 * @return 是否获取成功
    14 /
    15 public boolean lock(String lockKey, int expireTime) {
    16 //获取客户唯⼀识别码,例如:mac+线程信息
    17 String custId = getCustId();
    18 String result = jedis.set(lockKey, custId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);
    19
    20 if (LOCK_SUCCESS.equals(result)) {
    21 return true;
    22 }
    23
    24 return false;
    25 }
    26
    27 /
    *
    28 * 释放分布式锁
    29 * @param lockKey 锁
    30 * @param requestId 请求标识
    31 * @return 是否释放成功
    32 /
    33 public boolean unlock(String lockKey,) {
    34 //获取客户唯⼀识别码,例如:mac+线程信息
    35 String custId = getCustId();
    36 String script = “if redis.call(‘get’, KEYS[1]) == ARGV[1] then return redis.call(‘del’, KEYS[1]) else return 0 end”
    37 Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList
    38
    39 if (RELEASE_SUCCESS.equals(result)) {
    40 return true;
    41 }
    42 return false;
    43 }
    44
    45 /
    *
    46 * 获取锁信息
    47 * @param lockKey 锁
    48 * @return 是否重⼊锁
    49 */
    50 public boolean checkReentrantLock(String lockKey){
    51 //获取客户唯⼀识别码,例如:mac+线程信息
    52 String custId = getCustId();
    53
    54 //获取当前锁的客户唯⼀表示码
    55 String currentCustId = redis.get(lockKey);
    56 if (custId.equals(currentCustId)) {
    57 return true;
    58 }
    59 return false;
    60 }
    61
  3. 调⽤示例:
    1 public void test() {
    2 String lockKey = “lockKey”;
    3 //判断是否重⼊锁
    4 if (!checkReentrantLock(lockKey)) {
    5 //⾮重⼊锁
    6 while (!lock(lockKey)) {
    7 //获取锁失败, 则阻塞⾄获取锁
    8 try{
    9 Thread.sleep(100)
    10 } catch(Exception e) {
    11 }
    12 }
    13 }
    14 //TODO 业务处理
    15
    16 //释放锁
    17 unlock(lockKey);
    18 }
    19
    28.spring 7种事务的传播⾏为:
    PROPAGATION_REQUIRED如果当前没有事务,就新建⼀个事务,如果已经存在⼀个事务中,加⼊到这个事务中。这是最常⻅的选
    择。
    PROPAGATION_SUPPORTS⽀持当前事务,如果当前没有事务,就以⾮事务⽅式执⾏。
    PROPAGATION_MANDATORY使⽤当前的事务,如果当前没有事务,就抛出异常。
    PROPAGATION_REQUIRES_NEW新建事务,如果当前存在事务,把当前事务挂起。
    PROPAGATION_NOT_SUPPORTED以⾮事务⽅式执⾏操作,如果当前存在事务,就把当前事务挂起。
    PROPAGATION_NEVER以⾮事务⽅式执⾏,如果当前存在事务,则抛出异常。
    PROPAGATION_NESTED如果当前存在事务,则在嵌套事务内执⾏。如果当前没有事务,则执⾏与PROPAGATION_REQUIRED类似的
    操作。
    29.分布式下down机的处理⽅案(⼼跳检测)
    1、dubbo:服务器宕机,zk临时被删除;
    2、springcloud:每30s发送⼼跳检测重新进⾏租约,如果客户端不能多次更新租约,它将在90s内从服务器注册中⼼移除。
    3、apm监控:
    30、分析下分布式强⼀致性、弱⼀致性、最终⼀致性?
  4. 强⼀致性:
    当更新操作完成之后,任何多个后续进程或者线程的访问都会返回最新的更新过的值。这种是对⽤户最友好的,就是⽤户上⼀次
    写什么,下⼀次就保证能读到什么。根据 CAP 理论,这种实现需要牺牲可⽤性。
  5. 弱⼀致性:
    系统并不保证续进程或者线程的访问都会返回最新的更新过的值。系统在数据写⼊成功之后,不承诺⽴即可以读到最新写⼊的
    值,也不会具体的承诺多久之后可以读到。
  6. 最终⼀致性:
    弱⼀致性的特定形式。系统保证在没有后续更新的前提下,系统最终返回上⼀次更新操作的值。在没有故障发⽣的前提下,不⼀
    致窗⼝的时间主要受通信延迟,系统负载和复制副本的个数影响。DNS 是⼀个典型的最终⼀致性系统。
    31、dubbo与zookeeper 两者作为注册中⼼的区别,假如注册中⼼挂了,消费者还能调⽤服务吗,⽤什么调⽤的
  7. 注册中⼼对等集群,任意⼀台宕掉后,会⾃动切换到另⼀台
  8. 注册中⼼全部宕掉,服务提供者和消费者仍可以通过本地缓存通讯
  9. 服务提供者⽆状态,任⼀台 宕机后,不影响使⽤
  10. 服务提供者全部宕机,服务消费者会⽆法使⽤,并⽆限次重连等待服务者恢复
    32、dubbo的原理图(画出注册中⼼,消费者,⽣产者的关系图,并说出每个⻆⾊的作⽤)
    Consumer服务消费者,Provider服务提供者。Container服务容器。消费当然是invoke提供者了,invoke这条实线按照图上的说明当
    然同步的意思了。但是在实际调⽤过程中,Provider的位置对于Consumer来说是透明的,上⼀次调⽤服务的位置(IP地址)和下⼀次调
    ⽤服务的位置,是不确定的。这个地⽅就需要使⽤注册中⼼来实现软负载。
    33、项⽬中有没有⽤到多线程?
    1、发送邮件、短信,使⽤多线程异步发送;
    2、定时任务使⽤线程池;
    34、HashMap的底层原理(包括底层数据结构,怎么扩容的)
    1、数据结构中有数组和链表来实现对数据的存储,但这两者基本上是两个极端。那么我们能不能综合两者的特性,做出⼀种寻址容易,插⼊删除
    也容易的数据结构?答案是肯定的,这就是我们要提起的哈希表。哈希表((Hash table)既满⾜了数据的查找⽅便,同时不占⽤太多的内容空间,
    使⽤也⼗分⽅便。
    2、HashMap底层是采⽤数组来维护的.Entry静态内部类的数组
    1 /**
    2 * The table, resized as necessary. Length MUST Always be a power of two.
    3 */
    4 transient Entry[] table;
    5
    6 static class Entry implements Map.Entry {
    7 final K key;
    8 V value;
    9 Entry next;
    10 final int hash;
    11 ……
    12 }
    3、HashMap添加元素:将准备增加到map中的对象与该位置上的对象进⾏⽐较(equals⽅法),如果相同,那么就将该位置上的那个对象(Entry
    类型)的value值替换掉,否则沿着该Entry的链继续重复上述过程,如果到链的最后任然没有找到与此对象相同的对象,那么这个时候就会被增加到数
    组中,将数组中该位置上的那个Entry对象链到该对象的后⾯(先hashcode计算位置,如果有值便替换,⽆值则重复hashcode计算,直到最后在添加
    到hashmap最后⾯;)
    35、ConcurrentHashMap的原理
    1、⼯作机制(分⽚思想):它引⼊了⼀个“分段锁”的概念,具体可以理解为把⼀个⼤的Map拆分成N个⼩的segment,根据key.hashCode()
    来决定把key放到哪个HashTable中。可以提供相同的线程安全,但是效率提升N倍,默认提升16倍。
    2、应⽤:当读>写时使⽤,适合做缓存,在程序启动时初始化,之后可以被多个线程访问;
    3、hash冲突:
    1、简介:HashMap中调⽤hashCode()⽅法来计算hashCode。由于在Java中两个不同的对象可能有⼀样的hashCode,所以不同的键可
    能有⼀样hashCode,从⽽导致冲突的产⽣。
    2、hash冲突解决:使⽤平衡树来代替链表,当同⼀hash中的元素数量超过特定的值便会由链表切换到平衡树
    4、⽆锁读:ConcurrentHashMap之所以有较好的并发性是因为ConcurrentHashMap是⽆锁读和加锁写,并且利⽤了分段锁(不是在所有的
    entry上加锁,⽽是在⼀部分entry上加锁);

读之前会先判断count(jdk1.6),其中的count是被volatile修饰的(当变量被volatile修饰后,每次更改该变量的时候会将更改结果
写到系统主内存中,利⽤多处理器的缓存⼀致性,其他处理器会发现⾃⼰的缓存⾏对应的内存地址被修改,就会将⾃⼰处理器的缓存⾏设置为失效,并
强制从系统主内存获取最新的数据。),故可以实现⽆锁读。
36、 分布式锁的实现
基本原理:⽤⼀个状态值表示锁,对锁的占⽤和释放通过状态值来标识。
1、三种分布式锁:
1、Zookeeper:基于zookeeper瞬时有序节点实现的分布式锁,其主要逻辑如下(该图来⾃于IBM⽹站)。⼤致思想即为:每个客户端对
某个功能加锁时,在zookeeper上的与该功能对应的指定节点的⽬录下,⽣成⼀个唯⼀的瞬时有序节点。判断是否获取锁的⽅式很简单,只需要判断
有序节点中序号最⼩的⼀个。当释放锁的时候,只需将这个瞬时节点删除即可。同时,其可以避免服务宕机导致的锁⽆法释放,⽽产⽣的死锁问题。

2、优点
锁安全性⾼,zk可持久化,且能实时监听获取锁的客户端状态。⼀旦客户端宕机,则瞬时节点随之消失,zk因⽽能第⼀时间释放
锁。这也省去了⽤分布式缓存实现锁的过程中需要加⼊超时时间判断的这⼀逻辑。
3、缺点
性能开销⽐较⾼。因为其需要动态产⽣、销毁瞬时节点来实现锁功能。所以不太适合直接提供给⾼并发的场景使⽤。
4、实现
可以直接采⽤zookeeper第三⽅库curator即可⽅便地实现分布式锁。
5、适⽤场景
对可靠性要求⾮常⾼,且并发程度不⾼的场景下使⽤。如核⼼数据的定时全量/增量同步等。
2、memcached:memcached带有add函数,利⽤add函数的特性即可实现分布式锁。add和set的区别在于:如果多线程并发set,则每
个set都会成功,但最后存储的值以最后的set的线程为准。⽽add的话则相反,add会添加第⼀个到达的值,并返回true,后续的添加则都会返回
false。利⽤该点即可很轻松地实现分布式锁。
2、优点
并发⾼效
3、缺点
memcached采⽤列⼊LRU置换策略,所以如果内存不够,可能导致缓存中的锁信息丢失。
memcached⽆法持久化,⼀旦重启,将导致信息丢失。
4、使⽤场景
⾼并发场景。需要 1)加上超时时间避免死锁; 2)提供⾜够⽀撑锁服务的内存空间; 3)稳定的集群化管理。
3、redis:redis分布式锁即可以结合zk分布式锁锁⾼度安全和memcached并发场景下效率很好的优点,其实现⽅式和memcached类
似,采⽤setnx即可实现。需要注意的是,这⾥的redis也需要设置超时时间。以避免死锁。可以利⽤jedis客户端实现。
1 ICacheKey cacheKey = new ConcurrentCacheKey(key, type);
2 return RedisDao.setnx(cacheKey, “1”);
2、数据库死锁机制和解决⽅案:
1、死锁:死锁是指两个或者两个以上的事务在执⾏过程中,因争夺锁资源⽽造成的⼀种互相等待的现象。
2、处理机制:解决死锁最有⽤最简单的⽅法是不要有等待,将任何等待都转化为回滚,并且事务重新开始。但是有可能影响并发性能。
1、超时回滚,innodb_lock_wait_time设置超时时间;
2、wait-for-graph⽅法:跟超时回滚⽐起来,这是⼀种更加主动的死锁检测⽅式。InnoDB引擎也采⽤这种⽅式。
37、分布式session ,如何保持⼀致
1、Session粘滞
1、将⽤户的每次请求都通过某种⽅法强制分发到某⼀个Web服务器上,只要这个Web服务器上存储了对应Session数据,就可以
实现会话跟踪。
2、优点:使⽤简单,没有额外开销。
3、缺点:⼀旦某个Web服务器重启或宕机,相对应的Session数据将会丢失,⽽且需要依赖负载均衡机制。
4、适⽤场景:对稳定性要求不是很⾼的业务情景。
2、Session集中管理
1、在单独的服务器或服务器集群上使⽤缓存技术,如Redis存储Session数据,集中管理所有的Session,所有的Web服务器都从
这个存储介质中存取对应的Session,实现Session共享。
2、优点:可靠性⾼,减少Web服务器的资源开销。
3、缺点:实现上有些复杂,配置较多。
4、适⽤场景:Web服务器较多、要求⾼可⽤性的情况。
5、可⽤⽅案:开源⽅案Spring Session,也可以⾃⼰实现,主要是重写HttpServletRequestWrapper中的getSession⽅法,博主
也动⼿写了⼀个,github搜索joincat⽤户,然后⾃取。
3、基于Cookie管理
1、这种⽅式每次发起请求的时候都需要将Session数据放到Cookie中传递给服务端。
2、优点:不需要依赖额外外部存储,不需要额外配置。
3、缺点:不安全,易被盗取或篡改;Cookie数量和⻓度有限制,需要消耗更多⽹络带宽。
4、适⽤场景:数据不重要、不敏感且数据量⼩的情况。
总结
这四种⽅式,相对来说,Session集中管理更加可靠,使⽤也是最多的。
38、消息中间件都⽤到哪些,他们的区别

  1. 中⼩型公司⾸选RabbitMQ:管理界⾯简单,⾼并发。

  2. ⼤型公司可以选择RocketMQ:更⾼并发,可对rocketmq进⾏定制化开发。

  3. ⽇志采集功能,⾸选kafka,专为⼤数据准备。
    丰巢–0313

  4. 我们知道hashmap线程不安全,那⽤什么类可以代替它保证线程安全呢?他们⼜是如何实现线程安全的呢?
    使⽤ConcurrentHashMap保证线程安全,ConcurrentHashMap是HashMap的线程安全实现,允许多个修改操作同时进⾏(使⽤
    特性 ActiveMQ RabbitMQ RocketMQ kafka
    开发语⾔ java erlang java scala
    单机吞吐量 万级 万级 10万级 10万级
    时效性 ms级 us级 ms级 ms级以内
    可⽤性 ⾼(主从架构) ⾼(主从架构) ⾮常⾼(分布式架构) ⾮常⾼(分布式架构)
    功能特性
    成熟的产品,在很多公司
    得到应⽤;有较多的⽂
    档;各种协议⽀持较好
    基于erlang开发,所以并
    发能⼒很强,性能极其
    好,延时很低;管理界⾯较
    丰富
    MQ功能⽐较完备,扩展
    性佳
    只⽀持主要的MQ功能,
    像⼀些消息查询,消息回
    溯等功能没有提供,毕竟
    是为⼤数据准备的,在⼤
    数据领域应⽤⼴。
    了锁分离技术),它使⽤了多个锁来控制对hash表的不同段进⾏的修改,每个段其实就是⼀个⼩的hashtable,它们有⾃⼰的锁。

  5. 说说⼏种GC机制?

  6. 引⽤计数法(没有被java采⽤):
    a. 原理:对于⼀个对象A,只要有任何⼀个对象引⽤了A,则A的引⽤计数器就加1,当引⽤失效时,引⽤计数器就减1,只要
    对象A的引⽤计数器的值为0,则对象A就会被回收。
    b. 问题:
    i. 引⽤和去引⽤伴随加法和减法,影响性能;
    ii. 很难处理循环引⽤。

  7. 标记清除法:
    a. 原理:现代垃圾回收算法的思想基础。标记-清除算法将垃圾回收分为两个阶段:标记阶段和清除阶段。⼀种可⾏的实现
    是,在标记节点,⾸先通过根节点,标记所有从根节点开始的可达对象。因此,未被标记的对象就是未被引⽤的垃圾对象。
    然后在清除阶段,清除所有未被标记的对象。
    b. 问题:
    i. 标记和清除两个过程效率不⾼,产⽣内存碎⽚导致需要分配较⼤对象时⽆法找到⾜够的连续内存⽽需要触发⼀次GC操
    作。

  8. 标记压缩法:
    a. 原理:适合⽤于存活对象较多的场合,如⽼年代。它在标记-清除算法的基础上做了⼀些优化。标记阶段⼀样,但之后,
    将所有存活对象压缩到内存的⼀端。之后,清除边界外所有的空间。
    b. 优点:
    i. 解决了标记- 清除算法导致的内存碎⽚问题和在存活率较⾼时复制算法效率低的问题。

  9. 复制算法:
    a. 原理:将原有的内存空间分为两块,每次只使⽤其中⼀块,在垃圾回收时,将正在使⽤的内存中的存活对象复制到未使⽤
    的内存块中,之后清除正在使⽤的内存块中的所有对象,交换两个内存的⻆⾊,完成垃圾回收。
    b. 问题:
    i. 不适⽤于存活对象⽐较多的场合,如⽼年代。

  10. 分代回收法:
    a. 原理:根据对象存活周期的不同将内存划分为⼏块,⼀般是新⽣代和⽼年代,新⽣代基本采⽤复制算法,⽼年代采⽤标记
    整理算法。

  11. 说说⼀致性hash?
    1、⼀致性hash算法:我们的memcached客户端(这⾥我看的spymemcache的源码),使⽤了⼀致性hash算法ketama进⾏数据存储节点
    的选择。与常规的hash算法思路不同,只是对我们要存储数据的key进⾏hash计算,分配到不同节点存储。⼀致性hash算法是对我们要存储数据的服
    务器进⾏hash计算,进⽽确认每个key的存储位置。这⾥提到的⼀致性hash算法ketama的做法是:选择具体的机器节点不在只依赖需要缓存数据的
    key的hash本身了,⽽是机器节点本身也进⾏了hash运算。
    1、⼀致性hash算法是分布式系统中常⽤算法,设计⽬的是为了解决因特⽹中的热点(hot spot)问题。解决了P2P环境最为关键问题—
    如何在动态⽹络拓扑中分布存储和路由;
    2、⼀致性hash算法引⼊虚拟节点机制,解决服务节点少时数据倾斜问题(即对每⼀个服务节点计算多个哈希,每个计算结果位置都放
    置⼀个此服务节点,称为虚拟节点。);
    2、具体做法:如果有⼀个写⼊缓存的请求,其中Key值为K,计算器hash值Hash(K), Hash(K) 对应于图 – 1环中的某⼀个点,如果该
    点对应没有映射到具体的某⼀个机器节点,那么顺时针查找,直到第⼀次找到有映射机器的节点,该节点就是确定的⽬标节点,如果超过了2^32仍然
    找不到节点,则命中第⼀个机器节点。⽐如 Hash(K) 的值介于A~B之间,那么命中的机器节点应该是B节点(如上图 )。
    3、数据保存流程:
    1、⾸先求出memcached服务器(节点)的哈希值,并将其配置到0~232的圆(continuum)上。
    2、然后采⽤同样的⽅法求出存储数据的键的哈希值,并映射到相同的圆上。
    3、然后从数据映射到的位置开始顺时针查找,将数据保存到找到的第⼀个服务器上。如果超过232仍然找不到服务器,就会保存到第⼀
    台memcached服务器上。

  12. mybatis基础知识;
    需了解基础语法;

  13. mysql基础知识;
    需了解sql优化、搜索引擎。

  14. mysql单表达到多少数据量需要分库分表?
    理论上确实5000万以上才会性能急剧下降,有些db架构师也是这么说过。但是实际情况却不⼀定,按照⼀部分的真实项⽬,
    500万以上就会开始慢了。当然,这个跟具体的业务逻辑、响应要求、表设计也是很有关系的。表字段全是int类型的,可以到1000万以上
    再优化。到了300万,是时候作好优化的技术储备了。但是不⼀定分表,可以通过缓存、搜索引擎等技术也都可以提⾼性能。

  15. hibernate基础知识。
    需要了解hibernate⼀级缓存和⼆级缓存。

  16. 说说kafka的原理,为什么能保证这么⾼的吞吐量?
    1.消息分类按不同类别,分成不同的Topic,Topic⼜拆分成多个partition,每个partition均衡分散到不同的服务器(提⾼并发访问的能
    ⼒)
    2.消费者按顺序从partition中读取,不⽀持随机读取数据,但可通过改变保存到zookeeper中的offset位置实现从任意位置开始读取
    3.服务器消息定时清除(不管有没有消费)
    4.每个partition还可以设置备份到其他服务器上的个数以保证数据的可⽤性。通过Leader,Follower⽅式
    5.zookeeper保存kafka服务器和客户端的所有状态信息.(确保实际的客户端和服务器轻量级)
    6.在kafka中,⼀个partition中的消息只会被group中的⼀个consumer消费;每个group中consumer消息消费互相独⽴;我们可以认为
    ⼀个group是⼀个"订阅"者,⼀个Topic中的每个partions,只会被⼀个"订阅者"中的⼀个consumer消费,不过⼀个consumer可以消费多个
    partitions中的消息
    7.如果所有的consumer都具有相同的group,这种情况和queue模式很像;消息将会在consumers之间负载均衡.
    8.如果所有的consumer都具有不同的group,那这就是"发布-订阅";消息将会⼴播给所有的消费者.
    9.持久性,当收到的消息时先buffer起来,等到了⼀定的阀值再写⼊磁盘⽂件,减少磁盘IO.在⼀定程度上依赖OS的⽂件系统(对⽂件系统本
    身优化⼏乎不可能)
    10.除了磁盘IO,还应考虑⽹络IO,批量对消息发送和接收,并对消息进⾏压缩。
    11.在JMS实现中,Topic模型基于push⽅式,即broker将消息推送给consumer端.不过在kafka中,采⽤了pull⽅式,即consumer在和
    broker建⽴连接之后,主动去pull(或者说fetch)消息;这种模式有些优点,⾸先consumer端可以根据⾃⼰的消费能⼒适时的去fetch消息并处
    理,且可以控制消息消费的进度(offset);此外,消费者可以良好的控制消息消费的数量,batch fetch.
    12.kafka⽆需记录消息是否接收成功,是否要重新发送等,所以kafka的producer是⾮常轻量级的,consumer端也只需要将fetch后的
    offset位置注册到zookeeper,所以也是⾮常轻量级的.
    –kafka使⽤场景
    对于⼀些常规的消息系统,kafka是个不错的选择;partitons/replication和容错,可以使kafka具有良好的扩展性和性能优势.
    不过到⽬前为⽌,我们应该很清楚认识到,kafka并没有提供JMS中的"事务性"“消息传输担保(消息确认机制)”“消息分组"等企业级特性;
    kafka只能使⽤作为"常规"的消息系统,在⼀定程度上,尚未确保消息的发送与接收绝对可靠(⽐如,消息重发,消息发送丢失等)
    kafka的特性决定它⾮常适合作为"⽇志收集中⼼”;application可以将操作⽇志"批量""异步"的发送到kafka集群中,
    ⽽不是保存在本地或者DB中;kafka可以批量提交消息/压缩消息等,这对producer端⽽⾔,⼏乎感觉不到性能的开⽀.
    consumer端采⽤批量fetch⽅式,此时consumer端也可以使hadoop等其他系统化的存储和分析系统

  17. 对webservice有什么了解?
    WebService,顾名思义就是基于Web的服务。它使⽤Web(HTTP)⽅式,接收和响应外部系统的某种请求。从⽽实现远程调⽤.
    我们可以调⽤互联⽹上查询天⽓信息Web服务,然后将它嵌⼊到我们的程序(C/S或B/S程序)当中来,当⽤户从我们的⽹点看到天
    ⽓信息时,他会认为我们为他提供了很多的信息服务,但其实我们什么也没有做,只是简单调⽤了⼀下服务器上的⼀段代码⽽已。
    学习WebService可以将你的服务(⼀段代码)发布到互联⽹上让别⼈去调⽤,也可以调⽤别⼈机器上发布的WebService,就像使⽤⾃
    ⼰的代码⼀样.。

  18. 说说你们公司git分⽀管理⽅案?

  19. mysql如何进⾏分表分库?
    ⼀般就是垂直切分和⽔平切分,这是⼀种结果集描述的切分⽅式,是物理空间上的切分。 我们从⾯临的问题,开始解决,阐述:
    ⾸先是⽤户请求量太⼤,我们就堆机器搞定(这不是本⽂重点)。
    然后是单个库太⼤,这时我们要看是因为表多⽽导致数据多,还是因为单张表⾥⾯的数据多。 如果是因为表多⽽数据多,使⽤垂直切
    分,根据业务切分成不同的库。
    如果是因为单张表的数据量太⼤,这时要⽤⽔平切分,即把表的数据按某种规则切分成多张表,甚⾄多个库上的多张表。 分库分
    表的顺序应该是先垂直分,后⽔平分。 因为垂直分更简单,更符合我们处理现实世界问题的⽅式。

  20. 你们如何和前端进⾏接⼝联调?

  21. 开发前先定义好接⼝⽂档;

  22. 开发时严格按照接⼝⽂档开发;

  23. 如接⼝有调整需及时告知前端;

  24. 开发完成之后联调测试,保证业务逻辑正常。

  25. 说说你平时遇到的重⼤难题或者挑战,以及你解决问题的思路和流程。
    平时关注⼀下公司线上问题的解决⽅案。

你可能感兴趣的:(Java,Java面试,Java面试题,面试题,面试)