面试题、四

 

1、死信、延迟、重试队列

死信队列

DLQ(Deal Letter Queue),死信队列。当一个消息在队列中变成死信之后,他能被重新发送到 DLQ 中,与 DLQ 绑定到队列就是死信队列。

#什么情况下需要死信队列

· 消息被拒绝

· 消息过期

· 队列达到最大长度

生产者生产一条消息,存储到普通队列中;设置队列的过期时间为 10 秒,在 10 秒内没有消费者消费消息,那么判定消息过期;此时如果设置了死信队列,过期消息被丢给死信队列交换机,然后被存储在死信队列中。

#延迟队列

顾名思义就是延迟执行消息,比如我们可以增加一个队列并设置其超时时间为 10 秒并且不设置任何消费者,等到消息超时,我们可以将消息放入死信队列,让消费者监听这个死信队列就达到了延迟队列的效果。

#重试队列

重试的消息在延迟的某个时间点(业务可设置)后,再次投递给消费者。而如果一直这样重复消费都持续失败到一定次数,就会投递到死信队列,最后需要进行人工干预。

2、Zookeeper 假死脑裂

该问题就是服务集群因为网络震荡导致的多主多从问题,解决方案就是设置服务切换的超时时间,但也同时会导致无法达到高可用的要求。

3、MySQL 优化

  • 表关联查询时务必遵循 小表驱动大表 原则;

  • 使用查询语句 where 条件时,不允许出现 函数,否则索引会失效;

  • 使用单表查询时,相同字段尽量不要用 OR,因为可能导致索引失效,可以使用 UNION 替代;

  • LIKE 语句不允许使用 % 开头,否则索引会失效;

  • 组合索引一定要遵循 从左到右 原则,否则索引会失效;

  • 索引不宜过多,根据实际情况决定,尽量不要超过 10 个;

  • 每张表都必须有 主键,达到加快查询效率的目的;

  • 分表,可根据业务字段尾数中的个位或十位或百位(以此类推)做表名达到分表的目的;

  • 分库,可根据业务字段尾数中的个位或十位或百位(以此类推)做库名达到分库的目的;

  • 表分区,类似于硬盘分区,可以将某个时间段的数据放在分区里,加快查询速度,可以配合 分表 + 表分区 结合使用;

神器 EXPLAIN 语句

EXPLAIN 显示了 MySQL 如何使用索引来处理 SELECT 语句以及连接表。可以帮助选择更好的索引和写出更优化的查询语句。

使用方法,在 SELECT 语句前加上 EXPLAIN 即可,如:

EXPLAIN SELECT * FROM tb_item WHERE cid IN (SELECT id FROM tb_item_cat)

 

    • id: SELECT 识别符。这是 SELECT 的查询序列号

    • select_type: SELECT类型,可以为以下任何一种

    • table: 输出的行所引用的表

    • partitions: 表分区

    • type: 联接类型。下面给出各种联接类型,按照 从最佳类型到最坏类型 进行排序

    • system: 表仅有一行(=系统表)。这是 const 联接类型的一个特例。

possible_keys:

    • 指出 MySQL 能使用哪个索引在该表中找到行

    • key: 显示 MySQL 实际决定使用的键(索引)。如果没有选择索引, 键是 NULL。

    • key_len: 显示 MySQL 决定使用的键长度。如果键是 NULL, 则长度为 NULL。

    • ref: 显示使用哪个列或常数与 key 一起从表中选择行。

    • rows: 显示 MySQL 认为它执行查询时必须检查的行数。多行之间的数据相乘可以估算要处理的行数。

    • filtered: 显示了通过条件过滤出的行数的百分比估计值。

    • Extra: 该列包含 MySQL 解决查询的详细信息

4、JDK8新特性

概述**

以下列出两点重要特性:

  • Lambda 表达式(匿名函数)

  • Stream 多线程并行数据处理(重要)

  • 接口的默认方法只需要使用 default 关键字即可,这个特征又叫做 扩展方法

  • Lambda 表达式

  • Functional 接口 函数式接口 是指仅仅只包含一个抽象方法的接口,每一个该类型的 Lambda 表达式都会被匹配到这个抽象方法。你只需要给你的接口添加 @FunctionalInterface 注解

  • 使用 :: 双冒号关键字来传递方法(静态方法和非静态方法)

  • Predicate 接口和 Lambda 表达式

  • Function 接口

  • Supplier 接口,返回一个任意范型的值,和 Function 接口不同的是该接口 没有任何参数

  • Consumer 接口,接收一个任意范型的值,和 Function 接口不同的是该接口 没有任何值

  • Optional 类

新特性

小栗子

package com.funtl.jdk8.feature.lambda;

import java.util.Arrays;import java.util.List;import java.util.stream.Collectors;

/**

* Lambda 基本用法

* <p>Title: BaseLambda</p>

* <p>Description: </p>

* @author Lusifer

* @version 1.0.0

* @date 2019/1/6 10:42

*/public class BaseLambda {

public static void main(String[] args) {

testForeach();

testStreamDuplicates();

}

/**

* Lambda 遍历

*/

public static void testForeach() {

// 定义一个数组

String[] array = {

"尼尔机械纪元",

"关于我转生成为史莱姆这件事",

"实力至上主义教师",

"地狱少女"

};

// 转换成集合

List<String> acgs = Arrays.asList(array);

// 传统的遍历方式

System.out.println("传统的遍历方式:");

for (String acg : acgs) {

System.out.println(acg);

}

System.out.println();

// 使用 Lambda 表达式以及函数操作(functional operation)

System.out.println("Lambda 表达式以及函数操作:");

acgs.forEach((acg) -> System.out.println(acg));

System.out.println();

// 在 Java 8 中使用双冒号操作符(double colon operator)

System.out.println("使用双冒号操作符:");

acgs.forEach(System.out::println);

System.out.println();

}

/**

* Stream 去重复

* String 和 Integer 可以使用该方法去重

*/

public static void testStreamDuplicates() {

System.out.println("Stream 去重复:");

// 定义一个数组

String[] array = {

"尼尔机械纪元",

"尼尔机械纪元",

"关于我转生成为史莱姆这件事",

"关于我转生成为史莱姆这件事",

"实力至上主义教师",

"实力至上主义教师",

"地狱少女",

"地狱少女"

};

// 转换成集合

List<String> acgs = Arrays.asList(array);

// Stream 去重复

acgs = acgs.stream().distinct().collect(Collectors.toList());

// 打印

acgs.forEach(System.out::println);

}}

 

5、token和session

什么是token?

token是服务端生成的一串字符串,目的是作为客户端进行请求的一个令牌。当第一次登录后,服务器生成一个token(一串字符串),并将此token返回给客户端,此后页面接收到请求后,只需要找到token即可获取信息,无需再输入登录名和密码。

token一般用于验证表明身份的数据或是别的口令数据;token可以用url传参,也可以是post提交,也可以夹在http的header中。

什么是session?

session从字面上讲就是会话。

为了让服务器知道当前请求是从哪里发过来的,所以服务器给每个客户端分配了不同的“身份标识”(类似id),之后客户端每次发送请求都携带上这个标识(id),这样做,服务端就可以知道是谁发送的请求。

session和token的区别:

服务器在使用session把用户信息临时保存在服务器上,用户离开网站后session就会被销毁。

但session有一个缺陷:如果web服务器做了负载均衡,那么下一个操作请求到了另一台服务器的时候session会丢失。

而token最大特点就是支持跨平台操作,不论是在App还是在PC端,token都可以保留。

6、java锁机制的问题

(1、ABA问题

CAS 会导致“ABA问题”。
CAS 算法实现一个重要前提需要取出内存中某时刻的数据,而在下时刻比较并替换,那么在这个时间差类会导致数据的变化。
比如说一个线程 one 从内存位置 V 中取出 A,这时候另一个线程 two 也从内存中取出 A,并且 two 进行了一些操作变成了 B,然后 two 又将 V 位置的数据变成 A,这时候线程 one 进行 CAS 操作发现内存中仍然是 A,然后 one 操作成功。尽管线程 one 的 CAS 操作成功,但是不代表这个过程就是没有问题的。
部分乐观锁的实现是通过版本号(version)的方式来解决 ABA 问题,乐观锁每次在执行数据的修改操作时,都会带上一个版本号,一旦版本号和数据的版本号一致就可以执行修改操作并对版本号执行 +1 操作,否则就执行失败。因为每次操作的版本号都会随之增加,所以不会出现 ABA 问题,因为版本号只会增加不会减少。

(2、CAS乐观锁

CAS 是项乐观锁技术,当多个线程尝试使用 CAS 同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试。
CAS 操作包含三个操作数 —— 内存位置(V)、预期原值(A)和新值(B)。如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值。否则,处理器不做任何操作。无论哪种情况,它都会在 CAS 指令之前返回该位置的值。(在 CAS 的一些特殊情况下将仅返回 CAS 是否成功,而不提取当前值。)CAS 有效地说明了“我认为位置 V 应该包含值 A;如果包含该值,则将 B 放到这个位置;否则,不要更改该位置,只告诉我这个位置现在的值即可。”这其实和乐观锁的冲突检查 + 数据更新的原理是一样的。

(3、synchronize实现原理

同步代码块是使用 monitorenter 和 monitorexit 指令实现的,同步方法(在这看不出来需要看 JVM 底层实现)依靠的是方法修饰符上的 ACC_SYNCHRONIZED 实现。

(4、synchronize与lock的区别

synchronized 和 ``lock` `的用法区别

osynchronized(隐式锁):在需要同步的对象中加入此控制,synchronized 可以加在方法上,也可以加在特定代码块中,括号中表示需要锁的对象。
olock(显示锁):需要显示指定起始位置和终止位置。一般使用 ReentrantLock 类做为锁,多个线程中必须要使用一个 ReentrantLock 类做为对象才能保证锁的生效。且在加锁和解锁处需要通过 ``lock``() 和 unlock() 显示指出。所以一般会在 ``finally` `块中写 unlock() 以防死锁。

synchronized 和 ``lock` `性能区别 synchronized 是托管给 JVM 执行的,而 ``lock` `是 Java 写的控制锁的代码。在 JDK 1.5 中,synchronize 是性能低效的。因为这是一个重量级操作,需要调用操作接口,导致有可能加锁消耗的系统时间比加锁以外的操作还多。相比之下使用 Java 提供的 Lock 对象,性能更高一些。但是到了 JDK 1.6,发生了变化。synchronize 在语义上很清晰,可以进行很多优化,有适应自旋,锁消除,锁粗化,轻量级锁,偏向锁等等。导致在 JDK 1.6 上 synchronize 的性能并不比 Lock 差。


synchronized 和 ``lock` `机制区别

osynchronized 原始采用的是 CPU 悲观锁机制,即线程获得的是独占锁。独占锁意味着其 他线程只能依靠阻塞来等待线程释放锁。
oLock 用的是乐观锁方式。所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。乐观锁实现的机制就是 CAS 操作(Compare and Swap)。

(5、volatile实现原理

在 JVM 底层 ``volatile` `是采用“内存屏障”来实现的
缓存一致性协议(MESI协议)它确保每个缓存中使用的共享变量的副本是一致的。其核心思想如下:当某个 CPU 在写数据时,如果发现操作的变量是共享变量,则会通知其他 CPU 告知该变量的缓存行是无效的,因此其他 CPU 在读取该变量时,发现其无效会重新从主存中加载数据

(6、乐观锁的业务场景及实现方式

乐观锁(Optimistic Lock):
每次获取数据的时候,都不会担心数据被修改,所以每次获取数据的时候都不会进行加锁,但是在更新数据的时候需要判断该数据是否被别人修改过。如果数据被其他线程修改,则不进行数据更新,如果数据没有被其他线程修改,则进行数据更新。由于数据没有进行加锁,期间该数据可以被其他线程进行读写操作。
比较适合读取操作比较频繁的场景,如果出现大量的写入操作,数据发生冲突的可能性就会增大,为了保证数据的一致性,应用层需要不断的重新获取数据,这样会增加大量的查询操作,降低了系统的吞吐量。

(7、说说线程安全的问题

线程安全是多线程领域的问题,线程安全可以简单理解为一个方法或者一个实例可以在多线程环境中使用而不会出现问题。
在 Java 多线程编程当中,提供了多种实现 Java 线程安全的方式:
最简单的方式,使用 Synchronization 关键字
使用 java.util.concurrent.atomic 包中的原子类,例如 AtomicInteger
使用 java.util.concurrent.locks 包中的锁
使用线程安全的集合 ConcurrentHashMap
使用 ``volatile` `关键字,保证变量可见性(直接从内存读,而不是从线程 cache 读)

猜你喜欢

转载自www.cnblogs.com/zhaozhitong/p/12450113.html