java面试看老李(持续更新)

目录

一、java基础

java语言

面向对象与面向过程的区别?

java语言有什么特性?

JRE、JDK、JVM的区别?

为什么java代码可以一次编写,到处运行?

​java的数据类型有哪些?

重写和重载的区别?

java创建对象有几种方式?

什么是反射?有什么优缺点?

==和equals()的区别?

final、finally、 finalize 的区别?

java常见的异常类型有哪些?有什么区别?

String类为什么设计成不可变?

String、StringBuffer和StringBuilder的区别?

同步和异步的区别?阻塞和非阻塞的区别?

BIO、NIO、AIO 的区别?

​什么是序列化和反序列化?如何实现序列化?

什么是serialVersionUID?如何生成serialVersionUID?

Java中有了基本类型为什么还需要包装类?

什么是拆箱与装箱?

集合

ArrayList和LinkedList的区别?

​HashMap的数据结构?

HashMap put元素和get元素的原理?

HashMap为什么容量必须是2的幂?

HashMap扩容的原理?

ConcurrentHashMap工作原理?

HashMap和HashTable的区别?

Collection 和 Collections 有什么区别?

fail-fast 和 fail-safe的区别?

多线程

进程和线程的区别?

线程有几种状态?

sleep() 和 wait() 有什么区别?

创建线程有哪几种方式?

​如何停止一个正在运行的线程?

线程池主要参数有哪些?

线程池的工作原理?

线程池的拒绝策略/抛弃策略有哪些?

java实现锁的方式有哪些?synchronized与ReentrantLock的区别?

什么是死锁?如何防止死锁?

什么是threadlocal?工作原理?

volatile的作用和原理?

​​​有三个线程T1,T2,T3如何保证顺序执行?

JVM

JVM对锁进行了哪些优化?

JVM 是由哪几部分组成的?

谈谈类的加载过程?

类加载器有哪些?

​什么是双亲委派模型?有没有办法打破?

java对象的内存结构?

什么是引用?java中引用有几种类型?

深拷贝和浅拷贝的区别?

gc有哪些类型?有什么区别?

常用的垃圾回收算法有哪些?

jvm有哪些垃圾收集器?

哪些对象可以作为GC时的根节点?

常用的 jvm 调优方法?

OOM的常见场景及其原因、解决方法?

Spring

Spring、SpringMVC、SpringBoot的关系?

Spring MVC 的工作流程 ?

Spring IOC的原理?// TODO

Spring AOP 底层原理?

Spring Bean 都有哪些作用域 ?

Spring 框架中用到了哪些设计模式?

Spring的事务传播机制有哪些?// TODO

过滤器和拦截器的区别?// TODO

Maven能解决什么问题?为什么要用?

高CPU100%,怎么进行问题的定位?

二、计算机基础

网络通信

TCP三次握手的过程?

TCP四次挥手的过程?

TCP是3次握手,但挥手为什么需要4次,而不是3次挥手?

TCP和UDP的区别?

TCP如何实现可靠?

如何基于UDP实现可靠的网络传输?

TCP 长连接和短连接了解么?

操作系统

虚拟内存是什么,虚拟内存的原理是什么?

进程间通信的方式有哪些?

三、MySQL

什么是关系型数据库,什么是非关系型数据库?

mysql的架构?

mysql的查询过程?

数据的三大范式?

MyISAM和innoDB的区别?

什么是索引?索引为什么会让查询变快?

索引为什么使用B+树数据结构?

什么是覆盖索引?

什么是全文索引?

聚集索引和非聚集索引的区别?

普通索引、唯一索引和主键索引的区别?

索引有什么缺点?

索引失效的场景

undo log、redo log与binlog的区别?

什么是事务?事务有哪些特性?

数据库并发事务会出现哪些问题?

数据库的事务隔离策略?

什么是MVCC?

MySQL热点数据更新会带来哪些问题?如何解决?

数据库数据迁移方案?/ 如何安全地更换数据库?

设计数据库表时,需要注意什么?

mysql的主从复制原理?

读写分离的原理

如何进行数据分片?为什么需要数据分片?

数据分片有哪些常用的分片规则?

分库分表有哪些常用的中间件?

MySQL高可用集群架构

MySQL数据备份方案

什么是Canal,他的工作原理是什么?

什么是 MyBatis? 有什么优缺点?适用于什么场景?

char和varchar的区别?

binlog有几种格式?

四、redis

redis为什么快?

redis为什么是单线程?

redis的应用场景有哪些?

redis和memcached的区别?

redis支持的数据结构有哪些?

redis 持久化有几种方式?

如何保证 Redis 中的数据都是热点数据?redis的淘汰策略有哪些?

如何解决Redis和数据库的一致性问题?

redis中key过期了一定会立即删除吗?

Redis 支持的 Java 客户端都有哪些?推荐用哪个?

使用过 Redis 做异步队列么,你是怎么用的?

缓存常见问题及其解法?

redis的集群部署方式有哪些?

AOF文件太大怎么办?

zset跳表了解吗?

Redis 主从同步是怎么实现的?

Redis 数据分片 / 哈希槽?

redis主从数据不一致

Redis中,大量key同时过期,会出现什么问题?

五、消息队列

消息队列的作用?如何选型?

消息队列常见问题及其解法?

消息队列使用拉模式好还是推模式好?为什么?

RocketMQ怎么实现消息分发的?

如何保障消息一定能发送到RabbitMQ?

RabbitMQ的事务机制?

六、分布式

什么是CAP理论和BASE理论?

分布式锁的实现方案?// TODO

什么是ZK?主要应用场景有哪些?

zk的选举机制

dubbo的架构和工作原理?

Eureka和Zookeeper注册中心的区别

spring cloud 和 dubbo 的区别

什么是幂等?如何实现?

什么是ZAB 协议?

什么是paxos帕克索斯算法?

分布式事务的解决方案?

分布式与集群的区别是什么?

七、架构设计

DDD

高可用

限流算法

安全

加密算法有哪些?

加密和签名,有什么区别?

HTTP2的作用?

什么是DDoS攻击?如何防止被攻击?

八、场景设计

商城

库存/积分的扣减如何设计?

商城系统,如何选择分布式事务解决方案?

社交

实现朋友圈点赞功能?

九、算法

单例

十、中间件

ES

ES相比于MYSQL的优势?

如何保证ES和数据库的数据一致性?

netty

什么是netty?

netty的优点?

netty的使用场景?

netty的原理?


一、java基础

java语言

面向对象与面向过程的区别?

面向过程:简称POP,编程思想:

分析解决问题需要哪些步骤 → 每一个步骤使用函数实现 → 依次调用函数来解决问题

面向对象:简称OOP,思路:

分析问题由哪些对象组成 → 使用类实现每一个对象 → 调用对象的方法来解决问题

区别:

面向过程:

  • 优点:性能好,面向过程直接调用函数,而面向对象需要先对类进行实例化,再调用对象的方法,

  • 缺点:复用性差,扩展性差

面向对象:

  • 优点:面向对象有封装、继承、多态的特性,易复用、易扩展

  • 缺点:性能相对差一些,一般来说java程序执行速度比C慢10倍​


java语言有什么特性?

1. 封装:隐藏内部实现细节,向外提供接口来访问数据。安全性高。

2. 继承:从已有类继承信息,创建新类的过程。复用性强。

3. 多态:允许不同子类型对象,对同一操作作出不同的响应。多态性分为编译时多态和运行时多态。方法重载(overload)是编译时多态,方法重写(override)是运行时多态。

4. 抽象:将一类对象的共同特征总结出来程,包括两方面:数据抽象和行为抽象。


JRE、JDK、JVM的区别?

JDK包含JRE,JRE包含JVM。

  • JDK:Java Development Kit,是一个工具包,用于开发和运行Java程序,包含了java开发工具和JRE。
  • JRE:Java Runtime Environment,提供了一个java程序的运行环境。
  • JVM:Java 虚拟机,负责执行 Java 程序。

为什么java代码可以一次编写,到处运行?

java文件会被先编译为class文件,运行在JVM中。

不同的平台(Linux、Windows、Mac),有不同的JVM。


​java的数据类型有哪些?

java的数据类型有两种:

  1. 基本数据类型

  2. 引用数据类型

基本数据类型又包括四类8种:

  1. 整数型:byte 1字节 [-128~127],short 2字节,int 4字节,long 8字节

  2. 浮点型:float 4字节,double(默认) 8字节

  3. 字符型:char,2字节,unicode编码值

  4. 布尔型:boolean,1字节,值只有true和false


重写和重载的区别?

  • 重载:overloading,发生在类内部,方法名相同,参数不同。

  • 重写:overriding,是子类对父类的方法进行重写, 返回值和形参不能改变。

  • 重写和重载是java多态性的不同表现形式,重载是编译时多态,重写是运行时多态。


java创建对象有几种方式?

1. new

Person person = new Person(18);

2. Class.newInstance

Class person = Person.class; Person person = null; 
try { 
    person = (Person) person.newInstance(); 
} catch (Exception e) { 
    e.printStackTrace(); 
}

3. 反序列化(比较耗内存)

Person person = new Person("fsx", 18); 
byte[] bytes = SerializationUtils.serialize(person); 
Object deserPerson = SerializationUtils.deserialize(bytes);

4. clone()(对象必须实现Cloneable接口,并重写clone方法)

Person person1 = new Person(18); 
Person person2 = person1.clone();

什么是反射?有什么优缺点?

反射是java的一种机制,可以在运行态获取类的属性和方法,用来创建对象、调用方法、对属性进行赋值。

Class clz = Class.forName("xxx.User"); 
Object object = clz.newInstance();
  • 优点:能够在运行时动态获取类的实例,提高了程序的灵活性。

  • 缺点:反射机制中包括了一些动态类型,JVM无法对反射代码进行优化,因此性能较差,对性能要求高的程序尽量少用反射。


==和equals()的区别?

==:引用的内存地址是否相同

equals():值是否相同

String a = new String("123"); 
String b = new String("123"); 
a == b //false 
a.equals(b) //true

final、finally、 finalize 的区别?

  • final:java的1个关键字,如果类被声明为final,不能被继承;如果变量声明为final,给定初始值后,不可修改;如果方法声明为final,不能被重写,可重载。

  • finally:java的一种异常处理机制,finally代码总会执行。

  • finalize:java中的一个方法名,在Object类中定义,因此所的类都继承了它,finalize()方法在垃圾收集器删除对象之前对会被调用。


java常见的异常类型有哪些?有什么区别?

java异常的顶层类是Throwable,他的子类是:Error和Exception

1. Error:运行时环境错误,如:内存溢出、系统崩溃等,程序无法恢复。

2. Exception:可捕获且可恢复的异常,Exception又分为两类:

  • CheckedException(可检查异常/编译时异常):可检查异常需要在源代码里显式地使用try catch捕获,否则编译不过。如IOException、SQLException等。

  • RuntimeException(不可检查异常/运行时异常):运行时异常是可能被程序员忽略的异常。如:ArrayIndexOutOfBoundsException(数组下标越界)、NullPointerException(空指针异常等。


String类为什么设计成不可变?

1. 字符串常量池:字符串常量池(String pool) 是Java堆内存中的一个特殊存储区域,当创建一个String对象时,如果此字符串值已存在于常量池,则不会创建一个新的对象,而是引用已经存在的对象,达到复用的效果。String类如果可变,常量池就不支持了。

2. 效率:String对象经常会被比较,如果不可变,String对象的哈希码就可以被缓存,不必每次都计算哈希码,提升性能。

3. 安全:String经常用做重要参数使用,例如URL、文件路径等,如果可变,会有安全隐患。


String、StringBuffer和StringBuilder的区别?

String是一个字符串常量,不可改变;StringBuffer和StringBuilder是字符串变量,可以改变,但StringBuffer是线程安全的,StringBuilder是非线程安全的。

如何选择?如果修改少,使用String;如果在多线程下经常修改,使用SreingBuffer;如果是单线程下经常修改,使用StringBuilder。


同步和异步的区别?阻塞和非阻塞的区别?

先了解下1个IO请求的处理过程:

​同步和异步的区别:同步和异步,是处理方处理IO请求的2种方式,同步是指IO处理线程会一直等待相关的IO数据就绪后再执行逻辑处理,异步是指IO处理线程不会一直等待相关的IO数据就绪,比如可以轮询查看相关IO数据是否已经准备OK。

阻塞和非阻塞的区别:阻塞和非阻塞,是调用方的2种IO请求方式,阻塞是指调用方一直等待处理方的结果,非阻塞是调用方不会一直等待处理方的结果,可以先去执行其他任务,过一段时间再来查看结果是否返回。


BIO、NIO、AIO 的区别?

BIO:Blocking IO,同步阻塞IO。调用方发起IO请求后,会一直阻塞等待结果返回,同时处理方会一直等到IO数据就绪后,再进入处理。优点:一请求一应答的方式,逻辑简单,易实现。缺点:大量等待,性能很差。

NIO:Non-Blocking IO,同步非阻塞IO。调用方发起IO请求后,会一直阻塞等待结果返回。IO处理线程不会原地等待IO数据,可以先做其他事情,定时轮询检查IO数据是否就绪。

AIO:异步非阻塞IO。调用方发起IO请求后,不会等待结果,先处理其他事情,处理方执行完操作后利用系统函数告知调用方结果。IO处理线程不会原地等待IO数据,可以先做其他事情,定时轮询检查IO数据是否就绪。


​什么是序列化和反序列化?如何实现序列化?

什么是序列化? 序列化是指将对象写入IO流,反序列化是指从IO流中恢复对象。

序列化的作用? 1. 远程传输对象;2. 持久化保存对象​

如何实现?

public class User implements Serializable { 
    private static final long serialVersionUID = -7890663945232864573L; 
    private String userName; 
    public String getUserName() { } 
    public void setUserName(String userName) { } 
    @Override public String toString() { } 
}

方式1:jackson序列化

大多数公司都将json作为服务器端返回的数据格式,Json序列化一般会使用jackson包。

方式2:FastJson/Gson

fastjson是由阿里巴巴开源的Json解析器和生成器,不过安全漏洞多,不建议使用,推荐使用Google的Gson。

注意:JavaBean实体类必须实现Serializable接口,否则无法序列化。​


什么是serialVersionUID?如何生成serialVersionUID?

什么是serialVersionUID?serialVersionUID是序列化的版本号, 序列化时,其值与数据一起存储;反序列化时,将检查序列化数据是否与当前类版本匹配。

如何生成serialVersionUID?

1. 使用默认值:private static final long serialVersionUID = 1L;

2. 自动生成: private static final long serialVersionUID = 4603642343377807741L;

3. 看诉求:如果希望类的不同版本序列化时兼容,需确保类的不同版本具有相同的serialVersionUID;如果不希望类的不同版本序列化时兼容,需确保类的不同版本具有不同的serialVersionUID。


Java中有了基本类型为什么还需要包装类?

Java 是一种面向对象语言,很多地方都需要使用对象而不是基本数据类型。比如,在集合类中,我们是无法将 int、double 等类型放进去的。因为集合的容器要求元素是 Object 类型。

为了让基本类型也具有对象的特征,就出现了包装类型,它相当于将基本类型“包装起来”,使得它具有了对象的性质,并且为其添加了属性和方法,丰富了基本类型的操作。


什么是拆箱与装箱?

包装类是对基本类型的包装,所以,把基本数据类型转换成包装类的过程就是装箱;反之,把包装类转换成基本数据类型的过程就是拆箱。

为了减少开发人员的工作,Java 提供了自动拆箱和自动装箱的功能。

Integer i=1; //自动装箱 
int x=i; //自动拆箱

集合

ArrayList和LinkedList的区别?

1. 都是对List接口的实现,但一个底层是Array(动态数组),一个是Link(链表)

2. 随机访问时(get和set操作),ArrayList比LinkedList的效率更高,因为LinkedList是线性的数据存储方式,所以需要移动指针从前往后依次查找。

3. 对数据进行增加和删除的操作时(add和remove操作),LinkedList比ArrayList的效率更高,因为ArrayList是数组,所以在其中进行增删操作时,会对操作点之后所有数据的下标索引造成影响,需要进行数据的移动。​​


​HashMap的数据结构?

  • JDK7,HashMap的内部数据结构是数组+链表:

  • JDK8开始,当链表长度 > 8时会转化为红黑树,当红黑树元素个数 ≤ 6时会转化为链表。


HashMap put元素和get元素的原理?

put元素的原理:

  1. 计算K的hash值:hash = (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16)

  2. 计算K的数组位置:index = hash & (length - 1)

  3. 如果有相同K,则覆盖V

  4. 如果没有相同K,JDK7采用头插法(刚添加的元素被访问的概率大),但会引入循环引用问题,导致CPU高,JDK8开始采用尾插法,避免了这个问题。

  5. 如果元素个数超过阈值,进行扩容或数据结构变更(链表 → 红黑树)的操作。

get元素的原理:

  1. 计算K的hash值

  2. 计算K的数组index值

  3. 遍历寻找元素​


HashMap为什么容量必须是2的幂?

计算K的数组位置公式:index = h & (length-1),由于2的幂次方-1都是1,这样运算时就可以充分利用到数据的高低位特点,减少hash冲突的概率,提升存取效率。


HashMap扩容的原理?

什么时候扩容:当元素数量超过阈值时扩容,阈值 = 数组容量 * 加载因子,数组容量默认16,加载因子默认0.75,所以默认阈值12。

扩容原理:1. 创建新数组,容量翻倍;2. 旧数组元素迁移到新数组


ConcurrentHashMap工作原理?

HashMap线程不安全,多线程环境可以使用Collections.synchronizedMap、HashTable实现线程安全,但性能不佳。ConurrentHashMap比较适合高并发场景使用。

1. ConcurrentHashMap JDK7 数据结构

  • ConcurrentHashMap由一个Segment数组构成(默认长度16),Segment继承自ReentrantLock,所以加锁时Segment数组元素之间相互不影响,所以可实现分段加锁,性能高。

  • Segment本身是一个HashEntry链表数组,所以每个Segment相当于是一个HashMap。

2. ConcurrentHashMap JDK8的数据结构:Node数组+链表/红黑树

  • 为提升存取效率,摒弃Segment,使用Node数组+链表/红黑树的数据结构。其中,Node和HashEntry的作用相同,但把值和next采用了volatile修饰,保证了可见性;引入了红黑树,元素多时,存取效率高。

  • 并发控制使用Synchronized和CAS来操作,整体看起来像是线程安全的JDK8 HashMap。


HashMap和HashTable的区别?

1. HashTable的方法是Synchronize的,所以线程安全,HashMap非线程安全。

2. HashMap允许将null作为key或value,Hashtable不允许。

3. HashTable初始长度是11,HashMap是16,负载因子都是0.75

4. HashTable扩容是两倍+1 ,HashMap的扩容是两倍。​


Collection 和 Collections 有什么区别?

  • java.util.Collection 是一个集合接口(集合类的一个顶级接口)。它提供了对集合对象进行基本操作的通用接口方法。

  • Collections 则是集合类的一个工具类/帮助类,其中提供了一系列静态方法,用于对集合中元素进行排序、搜索以及线程安全等各种操作。


fail-fast 和 fail-safe的区别?

1. fail-fast

  • fail-fast 是 Java 中的一种 快速失败 机制

  • java.util 包下所有的集合都是快速失败的,快速失败会抛出 ConcurrentModificationException 异常,fail-fast 可以把它理解为一种快速检测机制它只能用来检测错误,不会对错误进行恢复

2. fail-safe

  • fail-safe 是 Java 中的一种安全失败机制,在遍历时不是直接在原集合上进行访问,而是先复制原有集合内容,在拷贝的集合上进行遍历。

  • 在遍历过程中对原集合修改不会触发ConcurrentModificationException。

  • java.util.conc urrent 包下的容器都是安全失败的,可以在多线程条件下使用,并发修改

多线程

进程和线程的区别?

  • 进程是执行中的一段程序,而一个进程中执行中的每个任务即为一个线程。

  • 一个线程只可以属于一个进程,一个进程能包含多个线程。

  • 进程是资源分配的最小单位,线程是资源调度的最小单位。

  • 进程有自己的独立地址空间,每启动一个进程,系统就会为它分配地址空间。进程中的线程共享进程中的数据。


线程有几种状态?

  • 新建状态(New):新创建一个线程对象

  • 就绪状态(Runnable):调用线程对象的start()方法,变得可运行,等待cpu的使用权。

  • 运行状态(Running):就绪状态的线程获取到了cpu的时间片,执行程序代码。

  • 阻塞状态(Blocked):线程因为某种原因(调用sleep()、wait()等)放弃cpu的使用权,暂停或停止运行,直到线程进入就绪状态,才有机会获得cpu的使用权从而转入运行状态。

  • 死亡状态(Dead):线程执行完或因异常而退出run()方法


sleep() 和 wait() 有什么区别?

相同点:可以使当前的线程进入阻塞状态

不同点:

  1. 所属类不同:sleep()在Thread类中,wait()在Object类中

  2. 调用要求不同:sleep()可以在任何场景下调用,wait()必须使用在同步代码块或同步方法中调用

  3. 是否释放锁:如果两个方法都使用在同步代码块或同步方法中,sleep()不会让线程释放锁,wait()会让线程释放锁。


创建线程有哪几种方式?

1. 继承Thread

public class MyThread extends Thread{   
    public void run(){ 
        //重写run方法    
        ...   
    } 
} 
new MyThread().start();

2. 实现Runnable接口

public class MyRunnable implements Runnable {   
    public void run(){ 
        //重写run方法    
        ...   
    } 
} 
MyRunnable myRunnable = new MyRunnable(); 
Thread thread = new Thread(myRunnable); 
thread().start();

3. 使用Callable和Future

和Runnable接口不一样,Callable接口提供了一个call()方法作为线程执行体,call()方法可以有返回值。

4. 线程池


​如何停止一个正在运行的线程?

1. 【推荐】使用退出标志,使线程正常退出run()方法

public void run() { 
    while(!flag) { xxx } 
}

2. 使用interrupt()方法中断线程

MyThread thread = new MyThread(); 
thread.start(); 
thread.interrupt();

3. 使用stop()方法强行终止【不推荐,已废弃】

MyThread thread = new MyThread(); 
thread.start(); thread.stop();

线程池主要参数有哪些?

public ThreadPoolExecutor( 
    int corePoolSize, //核心线程数 
    int maximumPoolSize, //最大线程数量 
    long keepAliveTime, //空闲线程存活时间 
    TimeUnit unit, //时间单位 
    BlockingQueue workQueue) //任务队列 
{}

线程池的工作原理?

1. 先线程池提交任务

2. 如果核心线程池没满,则创建核心线程执行任务

3. 如果核心线程池已满,但等待队列没满,则加入等待队列

4. 如果核心线程池已满,等待队列已满,但没有达到最大线程数,则创建非核心线程执行任务

5. 如果核心线程池已满,等待队列已满,达到最大线程数,则执行抛弃策略


线程池的拒绝策略/抛弃策略有哪些?

  • AbortPolicy:抛异常【默认策略】

  • DiscardPolicy:直接抛弃

  • DiscardOldestPolicy:丢弃队列里最老的任务

  • CallerRunsPolicy:谁提交谁执行​​


java实现锁的方式有哪些?synchronized与ReentrantLock的区别?

java实现加锁,主要的方式是synchronized与ReentrantLock。

1. 实现原理不同

synchronized原理:

  • synchronized是java的1个关键字,所以其加锁是依赖JVM实现的。

  • 如果synchronized修饰的是一般方法,对应的锁则是对象;修饰静态方法,锁的是当前类的Class实例;修饰代码块,锁的是传入synchronized的对象。

public synchronized void fun() {} 
public static synchronized void fun() {} 
synchronized(obj) {}
  • synchronized 关键字经过编译,会在同步块的前后生成 monitorenter 和 monitorexit 这两个字节码指令,他们的作用就是获取和释放对象的锁。

  • 那对象的锁在哪里?1个对象由三个部分组成:对象头、实例数据、对齐填充,1个对象的锁状态就存储在对象头的markword中,有无锁、偏向锁、轻量级锁、重量级锁,4种锁状态。

ReentrantLock的原理:

  • ReentrantLock是Lock的子类,底层使用CAS+AQS队列来实现加锁,使用lock()方法加锁,unlock()解锁。
private volatile ReentrantLock lock = new ReentrantLock(); 
lock.lock(); 
try { 
    //xxx 
} finally { 
    lock.unlock(); 
}
  • lock()方法:当线程调用该方法,如果锁当前没有被任何线程占用,则当前线程获取到锁,然后设置锁的拥有者为当前线程,并设置AQS的状态值为1;如果当前线程之前已获得该锁,则只把AQS的状态值加1;如果锁已被其他线程持有,则线程会被放入AQS队列后阻塞挂起。

2. 是否公平锁

公平锁:先来先得,按照申请锁的顺序去获得锁

  • synchronized为非公平锁

  • ReentrantLock可以选择公平非公平,通过构造方法传入boolean值进行选择,默认false非公平,true为公平。

3. 是否可主动释放锁

  • synchronized 不需要手动释放锁,优点是不会忘记释放锁,缺点是无法干预锁,只能等JVM释放。

  • ReentrantLock需要手动释放锁,优点是灵活,不需要锁了就可以放弃,不需要一直阻塞等待,缺点是可能忘记释放锁,导致死锁。

4. 锁是否可中断

  • synchronized不可中断

  • ReentrantLock可调用interrupt方法进行中断,更加灵活。

如何选择?

  • 如果对公平、中断、可释放等有诉求,可以选ReentrantLock,否则都可以使用synchronized,JVM一直在对synchronized进行优化,性能不差。


什么是死锁?如何防止死锁?

死锁:多个进程或线程一直在互相等待对方资源

产生死锁的四大必要条件:

  1. 资源互斥:资源同一时刻只能被一个进程或线程使用

  2. 请求和保持:线程获得资源后,又对其他资源发出请求,但是该资源被其他进程占有,此时请求阻塞,但又对自己已有的资源保持不放

  3. 资源不可剥夺:资源不能被其它进程或线程强制剥夺,需要等资源占有者主动释放。

  4. 环路等待:系统中有两个或两个以上的进程或线程组成一条等待环路

防止死锁产生的方法:破坏4大条件​


什么是threadlocal?工作原理?

  • 多线程访问共享变量的场景中,一般的解决办法是对共享变量加锁。但是加锁会带来性能的下降。

  • ThreadLocal用了一种空间换时间的设计思想,在每个线程里面,都有一个容器来存储共享变量的副本,然后每个线程只对自己的变量副本来做更新操作,这样既解决了线程安全问题,又避免了多线程竞争加锁的开销。

  • 具体实现:在Thread类里面有一个成员变量ThreadLocalMap,专门存储当前线程的共享变量副本,后续这个线程对于共享变量的操作,都是从这个ThreadLocalMap里面进行变更,不会影响全局共享变量的值。​


volatile的作用和原理?

并发编程三要素 :原子性、可见性、有序性。

volatile具备可见性、有序性,不具备原子性,并不能保证线程安全,但常与 CAS 结合,形成一种高性能的无锁。

1. 可见性

  • 当一个共享变量被 volatile 修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。即可见性。

  • Java内存模型规定了所有的变量都存储在主内存中,每条线程还有自己的工作内存,线程的工作内存中保存了该线程中是用到的变量的主内存副本拷贝:

  • 当一个线程修改volatile变量,会立即被更新到主内存中;当其他线程读取volatile变量时,它会直接从主内存中读取。

2. 有序性

  • 编译器和处理器为了优化程序性能而对指令序列进行重排序,重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。被volatile修饰的变量,会禁止指令重排序。从而保证有序性。


​​​有三个线程T1,T2,T3如何保证顺序执行?

join() 方法是用来等待一个线程执行完成的方法,当调用某个线程的 join() 方法时,当前线程会被阻塞,直到该线程执行完成后才会继续执行。

public class ThreadJoinDemo { 
    public static void main(String[] args) throws InterruptedException { 
        Thread t1 = new Thread(() -> System.out.println("t1")); 
        Thread t2 = new Thread(() -> System.out.println("t2")); 
        Thread t3 = new Thread(() -> System.out.println("t3")); 
        t1.start(); 
        t1.join(); // 等待 t1 执行完成 
        t2.start(); 
        t2.join(); // 等待 t2 执行完成 
        t3.start(); 
        t3.join(); // 等待 t3 执行完成 
    } 
}

JVM

JVM对锁进行了哪些优化?

1. 锁升级

锁有无锁、偏向锁、轻量级锁、重量级锁,4状态,标识在对象头中的 mark word。

  • 偏向锁:如果锁不存在竞争,就没必要上锁,打个标记就行。当第一个线程获取锁,会记录下这个线程ID,如果该线程再次尝试获取锁,就可以直接获取锁,开销小。

  • 轻量级锁:大部分情况下,synchronized 中的代码块是被多个线程交替执行的,并不存在太多的竞争发生或者只有短时间的锁竞争,重量级锁是没必要的,采用 CAS 更加合理。轻量级锁是指偏向锁被另一个线程访问时,说明发生了竞争,那么偏向锁就会升级为轻量级锁,线程会通过自旋的方式尝试获取锁。

  • 重量级锁:当多个线程获取轻量级锁,大量自旋反而带来性能消耗,锁会膨胀为重量级锁。重量级锁会让其他申请却拿不到锁的线程进入阻塞状态。

2. 锁消除:经过逃逸分析,发现某些对象不可能被其他线程访问,会把相关锁去除。

3. 锁粗化:把几个 synchronized 块合并为一个同步块,把中间无意义的解锁和加锁消除,避免无效的加解锁,提升性能。

4. 自适应的自旋锁:根据自旋的成功率、当前锁拥有者的状态等因素,决定自旋等待的时间,更加智能。


JVM 是由哪几部分组成的?

JVM主要包含4个部分,运行时数据区、类加载器、执行引擎、本地库接口。其中运行时数据区最关键,由5部分组成:

  • 堆:线程共享,大部分对象在这里分配内存。

  • 方法区:线程共享,存储已被虚拟机加载的类信息,比如常量、静态变量

  • 虚拟机栈:存储java方法的局部变量、方法出口等信息。

  • 本地方法栈:与虚拟机栈作用一样

  • 程序计数器:线程当前执行的字节码行号


谈谈类的加载过程?

类加载主要有3个步骤:加载 → 链接 → 初始化

1. 加载:使用类加载器把class字节码文件装载入内存中。(类加载器包括启动类加载器、扩展类加载器、应用类加载器、的自定义类加载器)

2. 链接:链接又分为3步

  • 验证:保证加载进来的字节码符合虚拟机规范,不会造成安全问题。

  • 准备:为类变量(不是实例变量)分配内存,赋初始值。

  • 解析:把类名、方法名、字段名等(符号引用),替换为具体的内存地址(直接引用)。

3. 初始化:对static修饰的变量或语句,进行初始化。如果初始化一个类的时候,其父类尚未初始化,则优先初始化其父类。


类加载器有哪些?

JVM在运行的时候,会产生3个类加载器,这三个类加载器组成了一个层级关系每个类加载器分别去加载不同作用范围的jar包:

1. Bootstrap ClassLoader:加载Java核心类库,也就是 %{JDK_HOME}\lib下的rt.jar、resources.jar

2. Extension ClassLoader:加载%{JDK_HOME}\lib\ext目录下的jar包和class文件

3. Application ClassLoader:加载当前应用里面的classpath下的所有jar包和类文件

4. 自定义加载器:除了系统提供的类加载器外,还可以通过ClassLoader类实现自定义加载器,去满足一些特殊场景的需求。


​什么是双亲委派模型?有没有办法打破?

当需要加载一个class文件的时候,首先会把这个class的查询和加载委派给父加载器去执行,如果父加载器都无法加载,再尝试自己来加载这个class。

双亲委派并不是一个强制性的约束模型,可以通过一些方式去打破:

  1. 继承ClassLoader抽象类,重写loadClass方法,在这个方法可以自定义要加载的类使用的类加载器。

  2. 使用线程上下文加载器,可以通过java.lang.Thread类的setContextClassLoader()方法来设置当前类使用的类加载器类型。


java对象的内存结构?

java对象由三部分组成:对象头、对象体、对齐字节

1. ​对象头:

  • Mark Word:表示对象的线程锁状态

  • Klass Word:指向方法区中对应的Class信息

  • 数组长度:可选,只有对象是数组时才会有这个部分

2. 对象体:保存对象的属性和值

3. 对齐字节:减少堆内存碎片


什么是引用?java中引用有几种类型?

什么是引用?在Java中,访问对象时,不会直接访问对象在内存中的数据,而是通过引用去访问。因此,引用也是一种数据类型,类似于C/C++ 语言中的指针。

引用的类型:

  • 强引用:当我们使用new创建对象时,被创建的对象就是强引用,如Object object = new Object()。只要强引用存在,垃圾回收器将永远不会回收被引用的对象。内存不足时,JVM宁愿抛出OutOfMemoryError,也不会去回收。如果想中断强引用与对象之间的联系,可以显示的将强引用赋值为null,JVM就可以适时回收对象。

  • 软引用:用来描述一些非必需但仍有用的对象。只有内存不足时,系统才会回收软引用对象,可以用SoftReference类来表示软引用。

  • 弱引用:只要 JVM 开始进行垃圾回收,弱引用关联的对象都会被回收。可以用 WeakReference 来表示弱引用。

  • 虚引用:对象仅持有虚引用,和没有任何引用一样,随时可能会被回收,可以用PhantomReference 类来表示,可以用来跟踪对象被回收的状态。


深拷贝和浅拷贝的区别?

  • 浅拷贝:只拷贝对象的第一层属性,如果这些属性是对象,则不会对这些对象进行拷贝,而是直接复制对象的引用。如果原对象属性值发生变化,浅拷贝后的对象属性值也会改变。

  • 深拷贝:拷贝对象的所有属性,如果属性是对象,也会对这些对象进行深拷贝,直到最底层的基本数据类型为止。所以,原对象的属性值发生了变化,深拷贝后的对象不会受到影响。


gc有哪些类型?有什么区别?

1. Full GC/Major GC:针对整个堆进行GC,包括年轻代、老年代、元空间(永久代)

2. MinorGC:年轻代的内存分布为:Eden : From survivor : To survivor = 8:1:1,大多数新对象都在Eden区,当Eden区被占满时,会触发MinorGC,把存活下来的对象转移到To survivor,将From survivor清空。


常用的垃圾回收算法有哪些?

1. 引用计数法

假设有一个对象A,任何一个对象对A的引用,那么对象A的引用计数器+1,当引用失效时,对象A的引用计数器就-1,如果对象A的计算器的值为0,就说明对象A没有引用了,可以被回收。

缺点:无法解决循环引用问题

虽然a和b都为null,但是由于a和b存在循环引用,这样a和b永远都不回被回收。

2. 标记清除法

  • 标记 :从根节点开始标记引用的对象。

  • 清除 :未被标记引用的对象就是垃圾对象,可以被清理。

缺点:效率较低,标记和清除两个动作都需要遍历所有的对象;内存碎片化严重

3. 标记压缩算法

标记压缩算法是在标记清除算法的基础之上,将存活的对象压缩到内存的一端,然后清理边界以外的垃圾,从而解决了碎片化的问题。

缺点:多了一步移动内存的步骤,对效率一定影响。

4. 复制算法

将内存空间一分为二,每次只用其中的一块,在垃圾回收时,将正在使用的对象复制到另一个内存空间中,然后将该内存空间清空。

当垃圾对象较多时,需要复制的对象就会少,效率比较高,反之则不适合,会浪费大量内存空间。

5. 分代回收算法

年轻代使用复制算法:

  1. ​GC开始前,对象只会存在于Eden区和名为“From”的Survivor区,Survivor区“To”是空的。

  2. 紧接着进行GC,Eden区中所有存活的对象都会被复制到“To”,而在“From”区中,仍存活的对象会根据它们的年龄值来决定去向。年龄达到一定值的对象会被移动到年老代中,没有达到阀值的对象会被复制到“To”区域。

  3. 经过这次GC后,Eden区和From区已被清空。这时候,“From”和“To”会交换他们的角色,也就是新的“To”就是上次GC前的“From”,新的“From”就是上次GC前的“To”。

  4. GC会一直重复这样的过程,直到“To”区被填满,“To”区被填满之后,会将所有对象移动到年老代中。

老年代使用标记整理算法:

  • 老年代的垃圾回收算法的速度至少比新生代的垃圾回收算法的速度慢10倍。如果系统频繁出现老年代的Full GC垃圾回收,会导致系统性能被严重影响,出现频繁卡顿的情况。

  • JVM优化的核心,就是尽可能让对象都在新生代里分配和回收,尽量别让太多对象频繁进入老年代,避免频繁对老年代进行垃圾回收,造成系统卡顿。


jvm有哪些垃圾收集器?

1. 串行垃圾收集器:使用单线程进行垃圾回收,垃圾回收时所有线程都要暂停,等待垃圾回收的完成,这种现象称之为STW (Stop-The-World)

2. 并行垃圾收集器:将串行垃圾收集器的单线程改为了多线程,缩短垃圾回收的时间,但仍会暂停应用程序。

3. ParNew垃圾收集器/Parallel垃圾收集器:用在年轻代上的并行垃圾收集器

4. CMS垃圾收集器

CMS是一款并发的、使用标记-清除算法的垃圾回收器,使用在老年代。

GC过程:

  • 初始标记:从根对象开始,扫描和根对象「直接关联」的对象,并作标记,这个过程虽然STW,但很快就完成了。

  • 并发标记:紧随初始标记阶段,在初始标记的基础上继续向下追溯标记,这个过程与用户线程并行运行。

  • 并发预清理:与用户线程同时运行

  • 重新标记:并发阶段会出现新的垃圾,但不多,所以可以STW进行追加标记

  • 并发清除:与用户线程同时运行

CMS缺点:

  • 因为CMS算法有多个并行阶段,那么就需要堆空间预留更多的内存来为新对象分配内存,默认当老年代使用68%时,CMS就开始了,降低了堆空间的利用率。

  • 只是标记清除,没有标记压缩,所以有内存碎片。

5. G1垃圾收集器

  • G1在jdk7正式引入,jdk9中变成默认垃圾收集器,替代CMS。

  • G1最大的特点是取消了年轻代、老年代的物理划分,取而代之的是将堆划分为若干个区域(Region),这些区域中包含了逻辑上的年轻代、老年代。

  • 年轻代的垃圾收集,依然采用STW的方式,将存活对象拷贝到老年代或Survivor空间,不会有cms的内存碎片问题。

  • 在G1,有一个特殊的区域,叫Humongous区域。如果一个对象占用的空间超过了region容量50%以上,G1收集器就认为这是一个巨型对象。短期存在的巨型对象,就会对垃圾收集器的性能造成比较大的影响。为了解决这个问题,G1划分了一个Humongous区,它用来专门存放巨型对象。如果一个H区装不下一个巨型对象,那么G1会寻找连续的H分区来存储。


哪些对象可以作为GC时的根节点?

  1. 在虚拟机栈中引用的对象,如调用方法时使用的参数、局部变量、临时变量等

  2. 类的静态变量或常量

  3. synchronized同步锁的持有对象


常用的 jvm 调优方法?

b

一般我们是「遇到问题」之后才进行调优,用各种的「工具」进行排查。

  • 使用 ps -ef|grep java 查看java进程id

  • jstat -gc 进程id //查看年轻代、老年代等区域的内存使用情况、垃圾回收次数/消耗时间

  • jmap生成堆转储快照dump文件,再使用MAT( Memory Analyzer tool 内存解析工具)分析

2. 调整参数,以减少GC的频率、减少Full GC次数

  • 调整堆大小:-Xmx:设置堆的最大值、-Xms:设置堆的初始值

  • 调整年轻代、老年代内存占比:-Xmn:年轻代的大小

3. 无法继续优化,就扩容


OOM的常见场景及其原因、解决方法?

1. 堆溢出:java.lang.OutOfMemoryError: Java heap space

原因:

  • 存在大对象

  • 存在内存泄漏,导致多次GC后,还是无法找到足够大的内存容纳对象

2. 方法区溢出

  • 永久代溢出:java.lang.OutOfMemoryError:PermGen space

  • 元空间溢出:java.lang.OutOfMemoryError: Metaspace,JDK8后,元空间替换了永久代,元空间使用的是本地内存,溢出的概率变小

原因:

  • 运行期间生成了大量类,应用程序长时间运行没有重启,导致方法区被撑爆

  • 元空间内存设置过小

3. 线程溢出:Java.lang.OutOfMemeoryError:unable to create new native thread

原因:创建了大量的线程导致的

4. StackOverflowError

原因:递归调用导致堆栈空间用尽

解决方法:

  • 查代码:大对象,内存泄漏

  • 利用工具分析内存泄漏:通过jmap命令把堆内存dump下来,使用MAT分析

  • 加堆内存、加机器

Spring

Spring、SpringMVC、SpringBoot的关系?

Spring:Spring是一个轻量级的开源框架,核心作用:简化java开发,比如:

  • IOC:Inverse of Control,控制反转,原本程序手动创建对象的控制权,交由Spring框架来创建。当需要创建对象时,只需要配置好配置文件或注解即可。

  • AOP:Aspect-Oriented Programming,面向切面编程,能够将那些与业务逻辑无关的公共逻辑(日志管理、权限控制等)封装起来,减少重复代码,降低模块间的耦合度。

​SpringMVC:SpringMVC是属于SpringWeb里面的一个功能模块(SpringWebMVC),基于MVC模式,专门用于开发Web项目。

SpringBoot:SpringBoot包含了Spring的核心:IOC和AOP,并在Spring的基础上进行了功能扩展,用于简化Spring应用的开发,使开发、测试和部署更加方便,比如:

  1. 搭建项目快:几秒钟就可以搭建完成1个完整的项目

  2. 部署项目快:SpringBoot内嵌各种servlet容器,如:Tomcat、Jetty等,只要将项目打成一个可执行的jar包,就能在本地快速独立运行。

  3. 简化配置:根据你引入的jar包,对项目进行自动配置,免去了Spring繁琐的配置过程

  4. 测试方便:内置多种测试框架,如:JUnit、Spring Boot Test等,方便测试

三者的关系是:spring mvc ∈ spring ∈ spring boot


Spring MVC 的工作流程 ?

1. 用户向服务端发送一次请求,请求先到前端控制器 DispatcherServlet

2. DispatcherServlet 接收到请求后会调用 HandlerMapping 处理器映射器,由此得知该请求该由哪个 Controller 来处理

3. DispatcherServlet 调用 HandlerAdapter 处理器适配器,告诉处理器适配器应该要去执行哪个 Controller

4. HandlerAdapter 处理器适配器去执行 Controller 并得到 ModelAndView(数据和视图),并层层返回给 DispatcherServlet

5. DispatcherServlet 将 ModelAndView 交给 ViewReslover 视图解析器解析,然后返回真正的视图。

6. DispatcherServlet 将模型数据填充到视图中

7. DispatcherServlet 将结果响应给用户


Spring IOC的原理?// TODO


Spring AOP 底层原理?

aop 底层是采用动态代理机制实现的:接口+实现类

  • 如果要代理的对象,实现了某个接口,那么 Spring AOP 会使用 JDK Proxy,去创建代理对象。

  • 没有实现接口的对象,就无法使用 JDK Proxy 去进行代理了,这时候 Spring AOP 会使用Cglib 生成一个被代理对象的子类来作为代理。就是由代理创建出一个和 impl 实现类平级的一个对象,但是这个对象不是一个真正的对象,只是一个代理对象,但它可以实现和 impl 相同的功能,这个就是 aop 的横向机制原理,这样就不需要修改源代码。


Spring Bean 都有哪些作用域 ?

  • 单例 singleton : bean 在每个 Spring IOC 容器中只有一个实例。

  • 原型 prototype:一个 bean 的定义可以有多个实例。

  • request:每次 http 请求都会创建一个 bean。

  • session:在一个 HTTP Session 中,一个 bean 定义对应一个实例。

  • globalsession


Spring 框架中用到了哪些设计模式?

1. 工厂设计模式 : Spring 使用工厂模式通过 BeanFactory、ApplicationContext 创建bean 对象。

2. 代理设计模式 : Spring AOP 功能的实现。

3. 单例设计模式 : Spring 中的 Bean 默认都是单例的。

4. 观察者模式: Spring 事件驱动模型就是观察者模式很经典的一个应用。


Spring的事务传播机制有哪些?// TODO


过滤器和拦截器的区别?// TODO

Maven能解决什么问题?为什么要用?


高CPU100%,怎么进行问题的定位?

1. top 找出进程 CPU 比较高 PID

2. top -Hp PID 打印 该 PID 进程下哪条线程的 CPU 占用比较高 tid

3. printf “%x\n” tid 将该 id 进行 16 进制转换 tidhex

4. jstack PID |grep tidhex 打印线程的堆栈信息

二、计算机基础

网络通信

TCP三次握手的过程?

  • 第一次握手:客户端向服务端发送连接请求报文,报文中包含客户端的数据通讯初始序号,随即进入 SYN-SENT 状态。

  • 第二次握手:服务端如果同意,会发送一个ACK应答,同时包含服务端的数据通讯初始序号,进入 SYN-RECEIVED 状态。

  • 第三次握手:客户端回复ACK,进入 ESTABLISHED 状态,服务端收到ACK,进入 ESTABLISHED 状态,建连成功。

为什么不是两次握手?如果客户端的第一个建连请求在网络中滞留,导致客户端已不再发送数据,而服务端收到后建连成功,会一直等待客户端发送数据,浪费资源。 SYN洪范攻击:SYN Flood。客户端发送大量建连请求,但不回复服务端ACK,导致大量半连接存在,耗尽服务端资源。可以根据来源、频次、设备指纹(数美)等策略识别非法建连请求


TCP四次挥手的过程?

  • 第一次挥手:Clien发送FIN,表示想关闭数据通道,随即进入FIN WAIT 1状态。

  • 第二次挥手:Server收到FIN,发送ACK给Client,表示收到,进入CLOSE WAIT状态。

  • 第三次挥手:Server发送FIN,表示同意关闭随即LAST ACK状态。

  • 第四次挥手:Client收到FIN,回复ACK,表示收到,Server随即进入CLOSED状态;Client会先进入TIME WAIT状态,经过一段时间后,也进入CLOSED状态。


TCP是3次握手,但挥手为什么需要4次,而不是3次挥手?

  • 3次握手和4次挥手的区别:当服务端收到客户端的建立连接请求时,会立即回复ACK(确认收到)+SYN(可以建连);当服务端收到客户端的关闭连接请求时,会先回复ACK(确认收到),再回复FIN(可以关闭)。

  • 为什么分2次回复ACK、FIN?当服务端收到FIN时,可能还在进行数据处理工作,无法关闭连接,只能先回复ACK,表示收到关闭请求,等数据处理完,再回复 FIN确认关闭。


TCP和UDP的区别?

TCP和UDP都是传输层协议,区别:

  • 面向连接:TCP面向连接,通信前需要三次握手;UDP发送数据前不会建立连接,只会将数据发送出去。

  • ACK机制:没有返回ACK确认接收,会重发。

  • 速度:TCP的可靠性策略会使得速度慢于UDP,对于音频、视频等场景,它们对实时性要求高,同时有一定的容错性,更适合使用UDP。

  • 拥塞控制:当网络出现拥塞,TCP能够减小发送量,缓解拥塞

  • 有序:TCP确保数据以正确的顺序到达目标


TCP如何实现可靠?

  • 面向连接:TCP面向连接,通信前需要三次握手;UDP发送数据前不会建立连接,只会将数据发送出去。

  • ACK机制:没有返回ACK确认接收,会重发。

  • 有序:TCP确保数据以正确的顺序到达目标

  • 拥塞控制:当网络出现拥塞,TCP能够减小发送量,缓解拥塞


如何基于UDP实现可靠的网络传输?

UDP是传输层协议,不好改,只能在应用层想办法弥补:

  • 建连:数据传输前,确保双方处于就绪状态

  • 引入ACK确认机制+超时重发

  • 有序:数据包中添加字段标识顺序,服务端重排

  • 拥塞控制:根据响应时间调整发送数据量


TCP 长连接和短连接了解么?

TCP 在进行读写之前,server 与 client 之间必须提前建立一个连接。建立连接的过程,需要我们常说的三次握手,释放/关闭连接的话需要四次挥手。这个过程是比较消耗网络资源并且有时间延迟的。

短连接说的就是 server 端 与 client 端建立连接之后,读写完成之后就关闭掉连接,如果下一次再要互相发送消息,就要重新连接。短连接的优点很明显,就是管理和实现都比较简单,缺点也很明显,每一次的读写都要建立连接必然会带来大量网络资源的消耗,并且连接的建立也需要耗费时间。

长连接说的就是 client 向 server 双方建立连接之后,即使 client 与 server 完成一次读写,它们之间的连接并不会主动关闭,后续的读写操作会继续使用这个连接。长连接的可以省去较多的 TCP 建立和关闭的操作,降低对网络资源的依赖,节约时间。对于频繁请求资源的客户来说,非常适用长连接。

操作系统

虚拟内存是什么,虚拟内存的原理是什么?

虚拟内存是计算机系统内存管理的一种技术。

虚拟内存有以下两个优点:1. 虚拟内存地址空间是连续的,没有碎片;2. 虚拟内存的最大空间就是 cup 的最大寻址空间,不受内存大小的限制,能提供比内存更大的地址空间。

当每个进程创建的时候,内核会为每个进程分配虚拟内存,这个时候数据和代码还在磁盘上,当运行到对应的程序时,进程去寻找页表,如果发现页表中地址没有存放在物理内存上,而是在磁盘上,于是发生缺页异常,于是将磁盘上的数据拷贝到物理内存中并更新页表,下次再访问该虚拟地址时就能命中了。


进程间通信的方式有哪些?

1. 管道:管道是一种半双工的通信方式,数据只能单向流动,而且只能在具有亲缘关系的进程间使用。进程的亲缘关系通常是指父子进程关系。

2. 有名管道:有名管道也是半双工的通信方式,但是它允许无亲缘关系进程间的通信。

3. 信号量:信号量是一个计数器,可以用来控制多个进程对共享资源的访问。它常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段。

4. 消息队列:消息队列是由消息的链表,存放在内核中并由消息队列标识符标识。消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。

5. 共享内存:共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。共享内存是最快的 IPC 方式,它是针对其他进程间通信方式运行效率低而专门设计的。它往往与其他通信机制,如信号两,配合使用,来实现进程间的同步和通信。

三、MySQL

什么是关系型数据库,什么是非关系型数据库?

关系型数据库,是指采用了关系模型来组织数据的数据库(关系模型可以简单理解为二维表格模型),其以行和列的形式存储数据,以便于用户管理。

关系型数据库中有表的概念,表中包含了行和列,多张(或1张)表可以组成数据库。

我们常见的MySQL、Oracle等都是关系型数据库。但是因为关系型数据库强调强一致性、以及基于硬盘存储,所以存在着一定的性能问题,相比之下,非关系型数据库在这方面就会相对有些优势。

NoSQL表示非关系型数据库,主要指那些非关系型的、分布式的,且一般不保证ACID的数据存储系统,主要代表如Redis、MongoDB等。

非关系型数据库的存储方式是基于键值来存储的,对于值的类型也有不同的支持,所以没有固定的要求和限制。


mysql的架构?

1. 客户端层:实现对数据库的访问、操作

2. server层:

  • 连接器:负责跟客户端建立连接

  • 查询缓存:不建议使用,mysql已删除

  • 分析器:语法分析,判断 sql 是否正确

  • 优化器:比如多个索引时,该如何选择

  • 执行器:调用引擎接口进行数据操作

3. 储存引擎层:负责数据的存储和提取,支持 InnoDB、MyISAM 等引擎,默认存储引擎是 InnoDB


mysql的查询过程?


数据的三大范式?

  • 第一范式:每个列都不可以再拆分。

  • 第二范式:在第一范式的基础上,非主键列完全依赖于主键,而不能是依赖于主键的一部分。

  • 第三范式:在第二范式的基础上,非主键列只依赖于主键,不依赖于其他非主键。

在设计数据库结构的时候,要尽量遵守三范式,如果不遵守,必须有足够的理由。比如性能。事实上我们经常会为了性能而妥协数据库的设计。


MyISAM和innoDB的区别?

MyISAM和innoDB是MySQL常用的存储引擎

区别:

  • InnoDB支持事务,并发量大的系统的表现更好,MyISAM不支持,但也因此更快。

  • 都使用了B+树的数据结构,但innodb使用聚集索引,索引和记录存储在一起;myisam为非聚集索引,索引和记录分开存储。

  • InnoDB支持表锁、行级锁(默认),MyISAM只支持表级锁。但注意:InnoDB的行锁实现在索引上,不是锁物理行记录,即如果访问没有命中索引,无法使用行锁,退化为表锁。

如何选择?如果执行大量的读操作,MyISAM性能更好;如果读写都有,选择innodb(默认引擎),支持事务、行锁。


什么是索引?索引为什么会让查询变快?

  • 数据库中的数据需要存放在硬盘中,不可避免地需要进行磁盘IO操作。

  • 磁盘IO操作需要寻道(找到数据所在的同心圆)、寻址(找到数据所在的同心圆的位置),是耗时操作,所以需要想办法减少磁盘IO操作。

  • 索引就相当于目录。为了方便查找书中的内容,通过对内容建立索引形成目录。索引是一个文件,它是要占据物理空间的。


索引为什么使用B+树数据结构?

  • B+树的非叶节点不存储data,可以存储更多的索引值,使得树更矮,减少磁盘IO操作。

  • B+树的叶结点构成了一个有序链表,对于范围型的查找和搜索,可以减少磁盘IO操作。


什么是覆盖索引?

  • 如果1个索引包含了所需要查询的所有字段,在索引树上可以直接获得结果,不需要回表查询,这种索引就是覆盖索引,即索引值可以覆盖查询结果。

  • 我们可以将经常要查询的列,设置为索引或联合索引,避免回表查询,提升查询效率。


什么是全文索引?

  • 只有在 MyISAM 引擎上才能使用

  • 只能在 CHAR,VARCHAR,TEXT 类型字段上使用全文索引

  • 全文索引就是在一堆文字中,通过其中的某个关键字,就能找到该字段所属的记录行,比如有"你是个靓仔,靓女 ...",通过「靓仔」就可以找到该条记录。


聚集索引和非聚集索引的区别?

  • 聚集索引是指数据库表行中数据的物理顺序与键值的逻辑(索引)顺序相同。

  • 一个表只能有一个聚簇索引,因为一个表的物理顺序只有一种情况,所以,对应的聚簇索引只能有一个。

  • 聚簇索引的叶子节点就是数据节点,既存储索引值,又在叶子节点存储行数据。

  • 非聚集索引(MyISAM 引擎的底层实现)的逻辑顺序与磁盘上行的物理存储顺序不同。非聚簇 索引的叶子节点仍然是索引节点,只不过有指向对应数据块的指针。索引命中后,需要回表查询。


普通索引、唯一索引和主键索引的区别?

  • 普通索引:允许索引列插入重复值和空值,纯粹为了查询数据更快一点。

  • 唯一索引:索引列的值必须是唯一的,但是允许为空值。

  • 主键索引:一种特殊的唯一索引,不允许有空值。


索引有什么缺点?

  • 时间方面:创建索引和维护索引要耗费时间,具体地,当对表中的数据进行增加、删除和修改的时候,索引也要动态的维护,会降低增/改/删的执行效率。

  • 空间方面:索引需要占物理空间


索引失效的场景

1. mysql认为全表扫描更快

2. 使用联合索引时没遵循最左匹配原则

如果创建了 (a, b, c) 联合索引,最左匹配:

where a=1;

where a=1 and b=2;

where a=1 and b=2 and c=3;

3. 左like:"ABC%"不会失效,"%ABC"会失效

4. 索引使用了函数:对索引使用函数,比如查询条件中对 name 字段使用了 LENGTH 函数

5. OR中有非索引:在 WHERE 中, OR 的含义是两个只要满足一个即可,只要有一个条件列不是索引,就会进行全表扫描。


undo log、redo log与binlog的区别?

  • binlog:记录数据库写入操作,以二进制的形式保存在磁盘中。使用场景:主从复制和故障恢复 。

  • redo log:事务操作执行时,会同时生成redo log,因为事务的数据先写入内存,再写入磁盘,当写入磁盘过程中出现数据库异常,可利用redo log 确保数据被持久化。

  • undo log:数据库事务开始前,会将修改先存放到 undo log,当事务回滚或数据库崩溃时,可利用其撤销操作。


什么是事务?事务有哪些特性?

事务是操作数据库的1个执行单元,具有ACID特性:

  • A (Atomic)原子性:事务包含的所有操作,要么全部执行,要么全部失败回滚

  • C (Consistency)一致性:事务开始前,数据处于一致状态,事务中,数据发生修改,当事务完成,数据必须再次回到一致状态。

  • I (Islation)隔离性:多个事物并发执行时,一个事物的执行不影响其它事物的执行

  • D (Durability)持久性:事务一旦提交修改,应该永久保存数据库中


数据库并发事务会出现哪些问题?

  • 脏读:一个事务读取到了另外一个事务未提交的数据

  • 不可重复读:A事务在读数据,事务B在更新数据,导致事务A读取到的字段内容不一致。

  • 幻读:A事务在读数据,事务B在增删数据,导致事务A读取数量不一致。


数据库的事务隔离策略?

  • Read uncommitted(读未提交):一个事务可以读取另一个事务未提交的数据,会引发脏读。

  • Read committed (读已提交):一个事务要等另一个事务提交后才能读取数据,会引发不可重复读

  • Repeatable read (可重复读)【默认】:事务开始读取数据时,不再允许修改操作,会引发幻读。

  • Serializable (序列化):事务串行执行,可避免脏读、不可重复读、幻读,但效率低,不推荐。


什么是MVCC?

数据库并发场景有三种∶

1. 读读∶ 不需要并发控制

2. 读写:可能造成脏读、幻读、不可重复读

3. 写写:可能存在更新丢失问题。

MVCC:多版本并发控制,用于处理读写并发场景,使用无锁并发控制的策略处理读写冲突,即读不阻塞写,写不阻塞读,提升事务并发处理能力。

原理:每个修改保存一个版本,版本与事务时间戳关联,读操作只读该事务开始前的数据库快照。

MVCC只在读已提交、可重复读,这2种隔离级别下工作。可以解决脏读、幻读、不可重复读等事务并发问题,但不能解决更新丢失问题。


MySQL热点数据更新会带来哪些问题?如何解决?


数据库数据迁移方案?/ 如何安全地更换数据库?

如何将数据从原来的单实例数据库迁移到新的数据库集群,也是一大技术挑战。不但要确保数据的正确性,还要保证每执行一个步骤后,一旦出现问题,能快速地回滚到上一个步骤。

  1. 不停机迁移方案

  • 把旧库的数据复制到新库中,上线一个同步程序,使用 Binlog等方案实时同步旧库数据到新库。

  • 上线双写订单新旧库服务,只读写旧库。

  • 开启双写,同时停止同步程序,开启对比补偿程序,确保新库数据和旧库一致。

  • 逐步将读请求切到新库上。

  • 读写都切换到新库上,对比补偿程序确保旧库数据和新库一致。

  • 下线旧库,下线订单双写功能,下线同步程序和对比补偿程序。

2. 停机迁移方案

  • 上线新订单系统,执行迁移程序将两个月之前的订单同步到新库,并对数据进行稽核。

  • 将商城V1应用停机,确保旧库数据不再变化。

  • 执行迁移程序,将第一步未迁移的订单同步到新库并进行稽核。

  • 上线商城V2应用,开始测试验证,如果失败则回退到商城V1应用(新订单系统有双写旧库的开关)。

选择:考虑到不停机方案的改造成本较高,而夜间停机方案的业务损失并不大,最终选用的是停机迁移方案。


设计数据库表时,需要注意什么?

  • 职能单一原则:如果一张表负责了两个或两个以上的职责,那么该表应进行拆分。

  • 直接关联原则:如果一个字段与当前表是间接关联的,那么就该创建一张新的表来保存该字段。

  • 字段最小原子化原则:一个字段如果包含了多个信息或含义,则该字段就应该拆成多个字段。

  • 设置索引:结合实际业务场景,设计索引


mysql的主从复制原理?

为什么需要主从复制?

  • 主从复制是读写分离的前提,提升性能

  • 主从复制是故障切换的前提,提升可用性

主从复制的原理:

  1. 主记录binlog日志(记录所有修改操作:insert、delete、update、create等)

  2. 从启动IO线程,读取主的binlog日志,写入relay log(中继日志)

  3. 从启动SQL线程,回放relay log


读写分离的原理

实现原理:

  • 数据库服务器搭建主从集群,通过主从复制将数据同步到从机

  • 业务服务器将写操作发给数据库主机,将读操作发给数据库从机

  • 主从是热备,主备是冷备

读写分离的问题:

  • 主从复制延迟,造成数据不一致。

主从复制延迟的解法:

  • 关键业务读写操作全部指向主机,可容错业务采用读写分

  • 二次读取:读从失败后再读一次主


如何进行数据分片?为什么需要数据分片?

MySQL单表容量超过 1千万 时,查询性能明显下降。这时候就要引入数据分片策略。 如何数据分片? 数据分片的分类:垂直拆分/水平拆分、分库/分表 1. 垂直分库:大系统拆分为多个小系统

2. 垂直分表

  • 把一个表的多个字段拆成多个表,冷热拆分:热字段一个表,冷字段一个表,提升查询性能。

  • 垂直分片的缺点:分库后无法Join,只能通过接口聚合方式解决,提升开发复杂度。如果表字段之间关联性强,优先水平拆分。

3. 水平分库

4. 水平分表

5. 垂直拆分&水平拆分相结合:先梳理好模块,做垂直拆分;再根据表数据,做水平拆分。


数据分片有哪些常用的分片规则?

1. Hash取模:id%分表数量

优点:数据分片均匀 缺点:扩容时,映射关系发生变化,需要迁移旧数据。 2. 数值Range:按照id值切分

优点:扩展方便 缺点:导致查询不均匀,如:按时间字段分片,最近数据频繁读写,历史数据很少读写。 3. 一致性Hash 将 2^32 想象成一个圆 → 确定服务器在哈希环的位置:hash(服务器的IP) % 2^32 → 数据映射到哈希环上:hash(图片名称) % 2^32 → 沿顺时针方向遇到的第一个服务器就是图片存放的服务器

优点:节点的增减都只需重定位环空间中的一小部分数据 缺点:数据倾斜

解法:虚拟节点 虚拟节点均匀分布,一个实际物理节点可以对应多个虚拟节点


分库分表有哪些常用的中间件?

分库分表中间件Sharding-Sphere [ʃɑːdɪŋ sfɪə]

Sharding-JDBC 最早是当当网使用的一款分库分表框架,2017年开始对外开源,现已更名为 ShardingSphere,它是一款分布式数据库中间件, 提供数据分片、读写分离、加密等能力。


MySQL高可用集群架构


MySQL数据备份方案

备份的目的:做灾难恢复:对损坏的数据进行恢复和还原 冷备(cold backup):需要关mysql服务,读写请求均不允许状态下进行 温备(warm backup):服务在线,但仅支持读请求,不允许写请求 热备(hot backup):备份的同时,业务不受影响。 热备+冷备+异地冷备:


什么是Canal,他的工作原理是什么?

Canal是阿里巴巴开源的数据同步工具,他是一个用于数据库的数据变更捕获,它可以捕获数据库中的变更操作(如插入、更新、删除),并将这些变更以实时流的方式发布给其他系统进行消费。主要应用场景之一是数据库的增量数据同步,通常在数据仓库、缓存、搜索引擎等系统中使用。

我们经常会在数据迁移、数据同步的场景中需要用到canal,比如分库分表时买家表同步出一张卖家表来,比如我们要把mysql中的数据同步到es中等等,这些场景,canal都能大显神威。

Canal会模拟 MySQL slave 的交互协议,把自己伪装成为一个 MySQL slave ,向 MySQL master 发送dump 协议,MySQL master 收到 dump 请求后,会开始向这个伪装的slavee ( canal )推送 binlog ,canal 把 binlog 解析成流,然后对接到各个后续的消费者中,如ES、数据库等。


什么是 MyBatis? 有什么优缺点?适用于什么场景?

什么是 Mybatis?

  • Mybatis内部封装了 JDBC,开发时只需要关注 SQL 语句本身,不需要花费精力去处理加载驱动、创建连接、创建 statement 等繁杂的过程。

  • MyBatis 专注于 SQL 本身,是一个足够灵活的 DAO 层解决方案。

Mybaits 的优点:

  • 消除了 JDBC 大量冗余的代码,不需要手动开关连接

  • SQL 写在 XML 里,解除 sql 与程序代码的耦合,便于统一管理

  • 提供 XML 标签,支持编写动态 SQL 语句

MyBatis 的缺点:

  • SQL 语句的编写工作量较大,尤其当字段多、关联表多时,对开发人员编写 SQL 语句的功底有一定要求。

MyBatis 适用的场景:

对性能的要求很高,或需求变化较多的项目,如互联网项目,MyBatis 将是不错的选择。


char和varchar的区别?


binlog有几种格式?

四、redis

redis为什么快?

  • redis是一种高性能的 K-V 数据库,官方称单机可支持10w/qps,那redis为什么能这么快?

  • 直接基于内存进行数据的读写操作,减少磁盘读写操作

  • 采用单线程,避免了不必要的线程上下文切换、加锁/解锁

  • 使用多路I/O复用模型,提升/O事件的处理效率

  • 自己构建VM系统,节省了调用系统函数的时间


redis为什么是单线程?

  • redis基于内存进行数据操作,瓶颈是内存、带宽,不是cpu,单线程的执行效率够了

  • 单线程会使得命令串行化,保证了命令的原子性,避免了加锁/解锁的操作

  • redis处理请求时,只有1个线程,但redis内部还是存在多线程,比如持久化时会使用以子进程或子线程。


redis的应用场景有哪些?


redis和memcached的区别?


redis支持的数据结构有哪些?

1. string

  • 介绍:string 数据结构是简单的 key-value 类型。

  • 使用场景: 一般常用在需要计数的场景,比如用户的访问次数、热点文章的点赞转发数量等

2. list

  • 介绍:list 即是 链表

  • 使用场景:发布与订阅或者说消息队列、慢查询。

3. hash

  • 介绍:hash 类似于 JDK1.8 前的 HashMap,内部实现也差不多(数组 + 链表)。

  • 使用场景:系统中对象数据的存储。

4. set

  • 介绍:set 类似于 Java 中的 HashSet 。Redis 中的 set 类型是一种无序集合,集合中的元素没有先后顺序。当你需要存储一个列表数据,又不希望出现重复数据时,set 是一个很好的选择,并且 set 提供了判断某个成员是否在一个 set 集合内的重要接口,这个也是 list 所不能提供的。可以基于 set 轻易实现交集、并集、差集的操作

  • 使用场景: 需要存放的数据不能重复以及需要获取多个数据源交集和并集等场景。

5. sorted set

  • 介绍:和 set 相比,sorted set 增加了一个权重参数 score,使得集合中的元素能够按 score 进行有序排列,还可以通过 score 的范围来获取元素的列表。有点像是 Java 中 HashMap 和TreeSet 的结合体。

  • 使用场景:需要对数据根据某个权重进行排序的场景。比如在直播系统中,实时排行信息包含直播间在线用户列表,各种礼物排行榜,弹幕消息(可以理解为按消息维度的消息排行榜)等信息。

6. bitmap

  • 介绍:bitmap 存储的是连续的二进制数字(0 和 1),通过 bitmap, 只需要一个 bit 位来表示某个元素对应的值或者状态,key 就是对应元素本身 。我们知道 8 个 bit 可以组成一个byte,所以 bitmap 本身会极大的节省储存空间。

  • 使用场景:适合需要保存状态信息(比如是否签到、是否登录...)并需要进一步对这些信息进行分析的场景。比如用户签到情况、活跃用户情况、用户行为统计(比如是否点赞过某个视频)


redis 持久化有几种方式?

方式1:RDB

  • RDB:Redis DataBase,是redis的一种持久化技术执行流程:

  • 执行bgsave命令,父进程 fork创建子进程,fork操作会使父进程阻塞,不过一般时间很短

  • 子进程根据父进程指向的内存地址生成快照文件

  • RDB可以在指定的时间间隔对数据进行快照存储,比如15分钟备份一次。

  • 缺点:快照期间,如果父进程修改数据,子进程无法感知,存在数据不一致的可能

方式2:AOF

  • AOF:Append Only File,将redis执行的每一条写命令追加到磁盘文件appendonly.aof中,当 redis启动时,从AOF文件恢复数据。

  • 一直保存命令,使AOF文件太大怎么办?为解决 AOF 文件膨胀问题,redis 提供了文件重写 (rewrite) 功能,当执行AOF文件重写操作时,会创建一个当前AOF文件的优化版本,包含了恢复当前数据集所需的最小命令集合,然后替换旧文件。

  • AOF的同步策略:

  • AOF FSYNC NO:不保存

  • AOF FSYNC EVERYSEC:每秒保存一次(默认)

  • AOF_FSYNC ALWAYS:每执行一个命令保存一次(不推荐)

  • 缺点:

  • 频繁记录会对性能有一定的影响

  • AOF文件大于 RDB,修复速度也比 RDB 慢,所以redis的默认持久化配置是 RDB

方式3:混合持久化模式

  • 如何开启?redis.conf配置文件中aof- use-rdb-preamble参数设置为yes,可以开启redis的混合持久化模式

  • 混合持久化过程:

  • 如果没有AOF文件,则加载 RDB文件

  • 如果AOF文件开头为RDB格式,则加载 RDB 内容,再加载剩余 AOF 内容

  • 如果AOF文件开头不是RDB格式,则以AOF格式加载整个文件

  • 混合持久化的优点:RDB文件较小,读取快,但实时性差;AOF实时记录写操作,数据丢失少,但文件大,读取慢。混合持久化结合了RDB和AOF的优点,文件开头为 RDB格式数据,使得Redis 可以更快启动,同时追加AOF格式数据,减低数据丢失风险。

  • 混合持久化的缺点:AOF文件中添加了RDB格式内容可读性差


如何保证 Redis 中的数据都是热点数据?redis的淘汰策略有哪些?

Redis 内存数据集大小上升到一定大小的时候,就会施行数据淘汰策略。Redis 提供 6 种数据淘汰策略:

  • volatile-lru:从已设置过期时间的数据集(server.db[i].expires)中挑选最近最少使用的数据淘汰

  • volatile-ttl:从已设置过期时间的数据集(server.db[i].expires)中挑选将要过期的数据淘汰

  • volatile-random:从已设置过期时间的数据集(server.db[i].expires)中任意选择数据淘汰

  • allkeys-lru:从数据集(server.db[i].dict)中挑选最近最少使用的数据淘汰

  • allkeys-random:从数据集(server.db[i].dict)中任意选择数据淘汰

  • no-enviction(驱逐):禁止驱逐数据


如何解决Redis和数据库的一致性问题?

1. 先删 Redis,再写 MySQL【不推荐】

弊端:写请求A,先删缓存,后写DB;读请求B,先读缓存/DB,后写缓存。出现数据不一致。

这种情况出现的概率比较大,因为请求 A 更新 MySQL 可能耗时会比较长,而请求 B 的前两步都是查询,会非常快。

2. 缓存双删:先删 Redis,再写 MySQL,再删 Redis

弊端:实现复杂

3. 先写 MySQL,再删除 Redis

  • 先更新数据库,再删除缓存也是会出现数据不一致性的问题,但是在实际中,这个问题出现的概率并不高。 因为缓存的写入通常要远远快于数据库的写入,所以在实际中很难出现请求 B 已经更新了数据库并且删除了缓存,请求 A 才更新完缓存的情况。

  • 而一旦请求 A 早于请求 B 删除缓存之前更新了缓存,那么接下来的请求就会因为缓存不命中而从数据库中重新读取数据,所以不会出现这种不一致的情况。

  • 所以,「先更新数据库 + 再删除缓存」的方案,是可以保证数据一致性的

4. 更新数据库 + 更新缓存【推荐】

  • 先更新数据库,再删除缓存」的方案虽然保证了数据库与缓存的数据一致性,但是每次更新数据的时候,缓存的数据都会被删除,这样会对缓存的命中率带来影响。

  • 所以,如果我们的业务对缓存命中率有很高的要求,我们可以采用「更新数据库 + 更新缓存」的方案,因为更新缓存并不会出现缓存未命中的情况。


redis中key过期了一定会立即删除吗?

不会。redis的过期key删除策略:

  • 被动删除:客户端访问一个键,发现已过期,则删除。

  • 主动删除:每秒从带有过期时间的键集合中随机选择20个键 → 删除已经过期的键 → 如果过期键占比超过25%,则重复,直到过期Key占比下降到 25%。


Redis 支持的 Java 客户端都有哪些?推荐用哪个?

Redisson、Jedis、lettuce 等等,官方推荐使用 Redisson。

Jedis 是 Redis 的 Java 实现的客户端,其 API 提供了比较全面的 Redis 命令的支持;

Redisson 实现了分布式和可扩展的 Java 数据结构,和 Jedis 相比,功能较为简单,不支持字符串操作,不支持排序、事务、管道、 分区等 Redis 特性。

Redisson 的宗旨是促进使用者对 Redis 的关注分离,从而让使用者能够将精 力更集中地放在处理业务逻辑上。


使用过 Redis 做异步队列么,你是怎么用的?

一般使用 list 结构作为队列,rpush 生产消息,lpop 消费消息。当 lpop 没有消息的时候,要适当 sleep 一会再重试。如果对方追问可不可以不 用 sleep 呢?list 还有个指令叫blpop,在没有消息的时候,它会阻塞住直 到消息到来。如果对方追问能不能生产一次消费多次呢?使用 pub/sub 主题订 阅者模式,可以实现 1:N 的消息队列。

pub/sub 有什么缺点?

在消费者下线的情况下,生产的消息会丢失,得使用专业的消息队列如 RabbitMQ 等。

redis 如何实现延时队列?

使用 sortedset,拿时间 戳作为 score,消息内容作为key 调用 zadd 来生产消息,消费者用 zrangebyscore 指令获取 N 秒之前的数据轮询进行处理。


缓存常见问题及其解法?

使用缓存可以提升请求的处理性能,但凡事都有正反两面,也会带来很多问题。

问题1:缓存穿透/缓存击穿:如果访问数据库不存在的数据,会先缓存查一次,再数据库查一次,如果这类请求过多,会造成性能降低,甚至数据库崩溃。

解法:

  • 对不存在的key,在缓存中置value为null,快速返回,但TTL不能太长,防止key真有数据录入了数据库。

  • 布隆过滤器bloom filter:通过 bloom filter 判断 key 是否存在,如果不存在直接返回即可,无需查缓存。

  • 请求做参数校验,过滤无效请求。

问题2:缓存雪崩:某一时刻,多个key同时失效,压力全部到数据库,导致数据库出现异常。

解法:

  • key的过期时间增加随机值,不会同时失效

问题3:数据不一致:更新数据后,数据库和缓存中同1个key的value不一致。

  • 数据库更新成功后立即删除缓存

  • 缩短TTL,及时读取DB最新数据。

问题4:HotKey/热key:同一时间大量请求访问特定key,打爆带宽,影响整体redis集群。

解法:

  • 凭借业务经验、实时监控,及时识别HotKey,下发到客户端缓存,减少请求量。

  • 采用redis集群部署,提升可用性。

问题5:BigKey:BigKey指一个K的value很大,会导致占用过多内存空间、处理时间长导致阻塞后续请求,影响整体redis集群性能。

解法:

  • 拆分为小key,再逻辑重组。


redis的集群部署方式有哪些?

方式1:主从模式

  • 主库:所有的写操作都在主库发生,然后主库同步数据到从库,同时也可以进行读操作;

  • 从库:只负责读操作

添加图片注释,不超过 140 字(可选)

  • 复制偏移量 offset:主服务器和从服务器会维护一个复制偏移量,主每次向从传递 N 个字节后,会将自己的复制偏移量加上 N;从收到主的 N 个字节数据,也会将自己的复制偏移量加上 N。通过主从的偏移量对比可以知道数据是否一致。

  • 增量同步:从把当前的偏移量告诉主,主计算偏移量差距,然后把两者之间相差的命令操作同步给从。

  • 全量同步:从首次加入集群,发生的是全量同步,主库通过 bgsave 命令生成 RDB 文件,然后将 RDB 文件传送到从库。

缺点:主节点故障,集群无法进行工作,需要人工干预,可用性低。

方式2:哨兵模式

什么是哨兵机制:sentinel(哨兵机制)是 Redis 集群实现高可用的重要策略,哨兵节点是特殊的 Redis 服务,不提供读写功能,主要用于监控 Redis 中的实例节点,如果主出现故障,在从服务器中选取新主。

如何监控:

  • 哨兵通过 PING 命令检测它和从库、主库之间的连接情况,如果发现响应超时就会认为服务已下线。但会存在误判,如果误判的节点是从,影响不大,拿掉一个从节点,对整体影响不大;如果误判主节点,影响就很大了。

  • 如何减少误判?引入哨兵集群,一个哨兵节点可能会误判,引入多个哨兵节点一起做决策,就能减少误判。当大部分(N/2 + 1)哨兵认为主库下线,主库才会真正被下线。

主观下线和客观下线:

  • 哨兵节点发送ping命令,当超过一定时间后,如果节点未回复,则哨兵认为主观下线。

  • 如果该节点为主,哨兵会进行故障切换,询问其他哨兵节点是否认为该主节点是主观下线,当达到指定数量,就会认为是客观下线,此时会进行主从切换操作。

如何选主:

  1. 过滤无效机器:已下线,最近5秒没有心跳等

  2. 根据slave-priority设置节点选主的优先级,如果优先级相同,根据复制偏移量再排序

缺点:

  1. 只有一个主机处理写请求,写操作受单机瓶颈影响

  2. 集群里所有节点保存的都是全量数据,浪费内存空间,没有真正实现分布式存储。

方式3:Redis Cluster

  • redis cluster主要是针对海量数据+高并发的场景,如果数据量不大,使用sentinel就够了。

  • Redis Cluster采用虚拟哈希槽分区而非一致性hash算法,一个切片集群共有16384个哈希槽,每个键会被映射到一个哈希槽。如果集群中有N个实例,那么,每个实例上的槽个数为16384/N个,每一个分区内的master节点负责维护一部分槽。

  • 官方推荐,至少要 3 台以上的master节点,最好使用 3 主 3 从六个节点的模式。


AOF文件太大怎么办?


zset跳表了解吗?


Redis 主从同步是怎么实现的?

Redis 主从同步策略:主从刚刚连接的时候,进行全量同步;全同步结束后,进行增量同步,如不成功,则进行全量同步。

1. 全量同步

master 服务器会开启一个后台进程,用于将 redis 中的数据生成一个 rdb 文件,将 rdb 文件传递给 slave 服务器,而 slave 服务器会将 rdb 文件保存在磁盘并通过读取该文件将数据加载到内存,在此之后 master 服务器会将在此期间缓存的命令通过 redis 传输协议发送给 slave 服务器,然后 slave 服务器将这些命令依次作用于自己本地的数据集上最终达到数据的一致性。

2. 增量同步

master给每个slave维护了一份同步日志和同步标识,当主从连接断掉之后,slave主动尝试和 master 服务器进行连接,如果从服务器携带的偏移量标识还在 master 服务器上的同步备份日志中,那么就从 slave 发送的偏移量开始继续上次的同步操作,如果 slave发送的偏移量已经不再 master 的同步备份日志中,则必须进行一次全量更新。


Redis 数据分片 / 哈希槽?

Redis的数据分片(sharding)是一种将一个Redis数据集分割成多个部分,分别存储在不同的Redis节点上的技术。它可以用于将一个单独的Redis数据库扩展到多个物理机器上,从而提高Redis集群的性能和可扩展性。

Redis 集群没有使用一致性 hash,而是引入了哈希槽的概念,Redis 集群 有 16384 个哈希槽,每个 key 通过 CRC16 校验后对 16384 取模来决定放置 哪个槽,集群的每个节点负责一部分 hash 槽。


redis主从数据不一致


Redis中,大量key同时过期,会出现什么问题?

Redis的键有两种过期方式:一种是被动过期,另一种是主动过期。

主动过期会在定时的去删除key,那么带来一个问题,那就是:Redis中如果有一批key同时过期,会导致其它key的读写效率降低。

原因是因为Redis的主动过期定时任务也是在Redis的单线程模型中的主线程中执行的,也就是说如果出现了一批key同时过期,就需要删除大量的Key。那么因为命令执行是单线程的,所以这时候后面来的业务操作请求,就需要等这个删除命令执行完才可以处理业务请求。

可以将键的过期时间随机分布,而不是在同一时间点过期。这样可以分散过期键删除的压力,避免大规模的键同时过期。同时也能避免缓存雪崩的问题。

五、消息队列

消息队列的作用?如何选型?

消息队列是分布式系统的重要中间件,消息队列的作用:

  1. 解耦系统/团队:比如你要做1个登录送优惠券的活动,那么就会涉及登录逻辑、优惠券逻辑、活动逻辑,1种方式是都在1个服务内实现,但逻辑比较复杂,而且不可复用;另1种方式是,涉及登录服务、优惠券服务、活动服务,各服务通过消息队列通信,比如:用户登录后,登录服务生产登录信息到消息队列,活动服务及时消费这个消息,然后告知优惠券系统发券。

  2. 削峰填谷提性能:对于高并发场景,可以先将请求先存储在消息队列,然后按照服务器可承受的处理速度去消费请求,避免服务器被高流量击垮。

  3. 实现广播功能

消息队列主要有RocketMQ、RabbitMQ、Kafka,如何选择?

  • Kafka:由Scala和Java编写,适合大数据领域的实时计算、日志采集等场景

  • RocketMQ:使用Java语言开发,适合可靠性要求很高的场景,比如订单、交易等金融场景。RocketMQ 是阿里出品,经历多次淘宝双十一的考验,稳定性值得信赖。

  • RabbitMQ:社区活跃,文档完善,适合数据量不大的中小公司。缺点是用 Erlang 语言编写,开发和维护成本大。


消息队列常见问题及其解法?

问题1:消息丢失

原因:

  1. 消息的生产者没有成功发送消息到MQ Broker

  2. 消息发送到MQ Broker后,Broker宕机

  3. 消费者消费消息时出现异常

解决方案:

  1. MQ Broker收到消息后回复ACK,没有收到ACK可以再次发送消息

  2. MQ收到消息后,进行消息持久化,出现故障可恢复

  3. 消费者在处理完消息后手动返回ack,MQ收到消费者ack后再删除持久化的消息。

问题2:消息重复

原因:

  1. 生成端:由于网络延迟,Broker没有收到消息,没有返回ACK,生产者会重新发送,最终Broker收到两条相同的消息。

  2. 消息端:消费者处理消息后,由于消费端异常或网络原因,ACK没有返回Broker,消息没有被删除,会再次被消费。

解法:消息的处理逻辑具备幂等性,比如根据消息内的数据,查询是否已存在消费记录。

问题3:消息积压

原因:发送端或消费端性能不足,导致消息发送积压、发送消费积压。

解法:

  1. 提升发送性能:并发发送、批量发送

  2. 提升消费性能:Comsumer扩容、并发处理


消息队列使用拉模式好还是推模式好?为什么?

推的模式就消费者端和消息中间件建立TCP长链接或者注册一个回调,当服务端数据发生变化,立即通过这个已经建立好的长连接(或者注册好的回调)将数据推送到客户端。但如果消息的生产速度大于消费速度,可能会导致消息大量堆积在消费者端,会对消费者造成很大的压力,甚至可能把消费者压垮。

拉的模式就是消费者轮询,通过不断轮询的方式检查数据是否发生变化,变化的话就把数据拉回来。但轮询的时间太短,对服务端是压力;太长,数据不够实时。

一般来说,推的模式适合实时性要求比较高的场景。而拉的模式适合实时性要求没那么高的场景。

在很多中间件的实现上,可能并没有在直接用长连接或者轮询,而是把二者结合了一下,使用长轮询的方式进行拉消息的。即消费者向消息中间件发起一个长轮询请求,消息中间件如果有消息就直接返回,如果没有也不会立即断开连接,而是等待一会,等待过程中如果有新消息到达,再把消息返回。如果超时还没有消息,那就结束了,等下次长轮询。

比如Kafka和RocketMQ都是支持基于长轮询进行拉取消息的。


RocketMQ怎么实现消息分发的?


如何保障消息一定能发送到RabbitMQ?

我们知道,RabbitMQ的消息最终时存储在Queue上的,而在Queue之前还要经过Exchange,那么这个过程中就有两个地方可能导致消息丢失。第一个是Producer到Exchange的过程,第二个是Exchange到Queue的过程。

可以利用confirm机制,注册回调来监听是否成功。

Publisher Confirm是一种机制,用于确保消息已经被Exchange成功接收和处理。一旦消息成功到达Exchange并被处理,RabbitMQ会向消息生产者发送确认信号(ACK)。如果由于某种原因(例如,Exchange不存在或路由键不匹配)消息无法被处理,RabbitMQ会向消息生产者发送否认信号(NACK)。

注册回调监听,用于在消息发送到Exchange或者Queue失败时进行异常处理。

通常我们可以在失败时进行报警或者重试来保障一定能发送成功。


RabbitMQ的事务机制?

六、分布式

什么是CAP理论和BASE理论?

什么是CAP?

CAP是指分布式系统只能同时满足CAP中的两个特性:

C:Consistency,一致性;A:Availability,可用性;P:Partition tolerance,区容错性

  • CA:放弃P分区容错性,等同于放弃了分布式系统,不可取。

  • AP:放弃C数据一致性,适合数据准确性要求不高,强调用户体验的项目,如新闻资讯。典型:Eureka,优先保证服务可用,部分时间数据可能不一致。

  • CP:放弃A可用性,适合数据准确性要求很高的业务场景,如交易系统。典型:ZooKeeper,为保证数据一致性,部分时间服务可能不可用。

如何证明CAP理论?P和C不可能同时存在当节点发生故障,肯定会出现数据不一致。

什么是BASE理论?

BASE理论是基于CAP理论演化而来:

  • BA(Basically Available):基本可用,允许损失部分可用性

  • S(Soft State):软状态,允许数据存在中间状态,用于不同节点间进行数据同步

  • E(Eventually Consistent):最终一致,数据副本经过一段时间的异步数据同步之后,数据最终能达到一致,不要求达到实时数据同步的强一致性。


分布式锁的实现方案?// TODO


什么是ZK?主要应用场景有哪些?

什么是ZK?

ZK:zoo keeper,动物园管理员,是分布式系统常用的一种中间件,很多知名框架都使用到了ZK,如:Dubbo、Kafka 等。

ZK的数据结构:

ZK本质是一个树形结构的文件系统。树中的节点被称为 znode,其中根节点为 /,每个节点上都会保存自己的数据和节点信息。但ZK的设计初衷是实现分布式系统下的服务协调,而不是文件存储,因此 znode 存储数据大小被限制在 1MB 以内。

ZK的使用场景:

  1. 注册中心:dubbo

  2. 分布所锁


zk的选举机制

ZooKeeper其实会在两种情况下进行Leader选举,第一种是集群的启动阶段,第二个是Leader失效了的情况。

1. 初始化阶段: 在一个ZooKeeper集群中,每个Follower节点都可以成为Leader。初始状态下,所有Follower节点都是处于"LOOKING"状态,即寻找Leader。每个节点都会监视集群中的其他节点,以侦听Leader选举消息。

2. 提名和投票:当一个节点启动时,它会向其他节点发送投票请求,称为提名。节点收到提名后可以选择投票支持这个提名节点,也可以不投票。每个节点只能在一个选举周期内投出一票。在投票过程中,节点首先会认为自己是最强的,所以他会在投票时先投自己一票,然后把自己的投票信息广播出去,这里面包含了zxid和sid。,zxid就是自己的事务ID,sid就是标识出自己是谁的唯一标识。这样集群中的节点们就会不断受到别人发过来的投票结果,然后这个节点就会拿别人的zxid和自己的zxid进行比较,如果别人的zxid更大, 说明他的数据更新,那么就会重新投票,把zxid和sid都换成别人的信息再发出去。

3. 选举过程:选举过程分为多个轮次,每个轮次被称为一个"选举周期"。在每个选举周期中,节点根据投票数来选择新的Leader候选者。如果一个候选者获得了大多数节点(超过半数)的投票,那么它就会成为新的Leader。否则,没有候选者能够获得足够的投票,那么这个选举周期失败,所有节点会继续下一个选举周期。

4. Leader确认: 一旦一个候选者获得了大多数节点的投票,它就会成为新的Leader。这个Leader会向其他节点发送Leader就绪消息,告知它们自己已经成为Leader,并且开始处理客户端的请求。

5. 集群同步: 一旦新的Leader选举完成,其他节点会与新Leader同步数据,确保所有节点在一个一致的状态下运行。这个同步过程也包括了未完成的客户端请求,以保证数据的一致性。


dubbo的架构和工作原理?

Dubbo是一个用于分布式系统服务调用框架,架构:

​节点角色说明:

  • Provider:服务提供方

  • Container: 服务运行的容器

  • Consumer:服务消费方

  • Registry:服务注册与发现中心

  • Monitor:统计服务调用次数和时间的监控中心

调用时序:

  1. Container启动服务

  2. Provider向Registry注册服务

  3. Consumer向注册中心订阅自己需要的服务

  4. Registry返回Provider地址列表给Consumer,如果地址有变更,Registry将基于长连接推送给Consumer。

  5. Consumer从地址列表中,基于负载均衡策略,选一台调用,如果调用失败,再选另一台。

  6. Consumer和Provider,在内存中累计调用次数和调用时间,定时每分钟发送统计数据到监控中心。


Eureka和Zookeeper注册中心的区别

  • SpringCloud和Dubbo都支持多种注册中心,SpringCloud用Eureka较多,Dubbo以Zookeeper为主。

  • 从CAP来看:Eureka满足AP,为保证整个服务可用性,Eureka集群各节点没有主从关系,每个节点都能对外提供能力,但可能出现数据不一致;而Zookeeper满足CP原则,使用同步策略保证各节点数据一致性,牺牲了一定的服务可用性。

  • 从拉取方式来看:Eureka采用服务主动拉取策略,消费者按照固定频率(默认30秒)去Eureka拉取服务并缓存在本地;ZK中的消费者首次启动到ZK订阅自己需要的服务信息,并缓存在本地,然后监听服务列表变化,以后服务变更ZK会推送给消费者。


spring cloud 和 dubbo 的区别

  • 两者都是现在主流的微服务框架

  • 定位不同:SpringCloud依托于Spring平台,定位为微服务架构的一站式解决方案;Dubbo 是 SOA 时代的产物,一开始只是做RPC远程调用,关注点主要在于服务的调用和治理。

  • SpringCloud和Dubbo都支持多种注册中心,不过SpringCloud用Eureka较多,Dubbo以Zookeeper为主。


什么是幂等?如何实现?


什么是ZAB 协议?

ZAB 协议是为分布式协调服务 Zookeeper 专门设计的一种支持崩溃恢复的原子 广播协议。

ZAB 协议包括两种基本的模式:崩溃恢复和消息广播。

当整个 zookeeper 集群刚刚启动或者 Leader 服务器宕机、重启或者网络故障 导致不存在过半的服务器与 Leader 服务器保持正常通信时,所有进程(服务 器)进入崩溃恢复模式,首先选举产生新的 Leader 服务器,然后集群中 Follower 服务器开始与新的 Leader 服务器进行数据同步,当集群中超过半数 机器与该 Leader 服务器完成数据同步之后,退出恢复模式进入消息广播模 式,Leader 服务器开始接收客户端的事务请求生成事物提案来进行事务请求处理。


什么是paxos帕克索斯算法?


分布式事务的解决方案?


分布式与集群的区别是什么?

分布式: 一个业务分拆多个子业务,部署在不同的服务器上

集群: 同一个业务,部署在多个服务器上。比如之前做电商网站搭的 redis 集群以及 solr集群都是属于将 redis 服务器提供的缓存服务以及 solr 服务器提供的搜索服务部署在多个服务器上以提高系统性能、并发量解决海量存储问题。

七、架构设计

架构设计,要遵循的原则?

  • 演进原则:系统架构是随着业务的发展而逐步演进的,一上来不要过度设计。

  • 走在业务前面:要持续预判业务的发展方向,提前制定架构演进方案,不能问题驱动架构演进。

设计1个系统,需要关注哪些方面?

设计1个系统,需要关注6个维度:性能、可用性、安全性、扩展性、数据一致性、成本。

是否每个维度都需要设计,每个维度需要设计到什么地步,要根据实际情况看。

举例:运营希望晚上8点开1个秒杀活动,抢优惠券,用来拉付费。

那么,这个业务场景需要重点关注:1. 可用性,秒杀的流量把系统打挂;2. 安全性,会有刷子来刷;3. 数据一致性,不能多发、发错。性能的优先级就没有那么高了,可以使用产品策略来缓解流量压力,比如:排队中。扩展性、成本,优先级也不高。

如何进行技术选型?

  • 开发人员熟悉度/学习成本

  • 社区活跃度/支撑力度

DDD

高可用

系统可用性的核心指标?

SLA服务可用性:99.95%

RTO故障恢复时间

如何设计一个高可用系统?

5大设计思想:

  • 过载保护:限流、降级、熔断

  • 缩短关键路径

  • 互备:数据副本、多机房容灾、双协议

  • 隔离:数据隔离、部署隔离

  • 重试:超时重试、失败重试

4大保障手段:

  • 感知:监控告警、值班

  • 止损:逃生/兜底方案、回滚能力、预案

  • 复盘

  • 预防:故障演练、高危盘点与改进、压测

限流算法

高性能

如何设计一个高性能系统?

分层维度

  • 用户端:本地缓存、静态资源上CDN、防抖、排队、批请求

  • 网关层:负载均衡、长连接

  • 服务层:

    • 缓存:内存缓存、redis

    • 异步:消息队列削峰填谷

    • 批处理

  • 数据层:数据分片(分库分表)、读写分离、冷热分离

整体维度

  • 缩短关键路径

  • 预加载:CDN和key

  • 无状态设计,灵活扩展

数据一致性

如何实现系统的数据一致性?

强一致性:

  • 采用同步思想,所有节点操作成功,才会更新数据。但性能会下降明显。

弱一致性:

  • 采用异步思想,比如读写分离,主从同步有一定时延。

最终一致性:

  • 通过一段业务可接受的时延,优先保证系统性能、可用性,最终实现数据一致性。

  • 解决方案:消息队列、本地消息表

安全

如何设计一个高安全系统?

人机验证

风控


加密算法有哪些?

(1) 摘要算法/hash算法

  • 根据不同的hash函数对信息进行摘要,生成一段固定长度的hash值。

  • 不可逆:不能通过这个hash值获得原始信息

  • 常见的摘要算法:

  • MD5:产生一个128位的哈希值,md5(“cc”):c9c1ebed56b2efee7844b4158905d845

  • SHA:产生一个256位哈希值,sha1(“cc”): bdb480de655aa6ec75ca058c849c4faf3c0f75b1

  • 撞库:黑客收集互联网已泄露的用户+密码信息,生成摘要字典表,可以反向得到消息。

  • 撞库的解法:可多重摘要、加盐摘要,增加破解成本

(2) 对称加密算法

  • 使用密钥对信息加密,然后使用相同的密钥解密

  • AES是一种经典的对称加密/解密算法

(3) 非对称加密算法

  • 乙生成一对密钥(公钥和私钥),公钥给甲,私钥自己保留。

  • 甲传输数据给乙前,用乙的公钥加密

  • 乙拿到数据后,用本地私钥解密,私钥只有乙有。

  • 密钥是在双方根据SSL/TLS协议建立网络通信通道时,通过hello报文传输。

(4) 混合加密算法

RSA加密算法虽然安全,但是计算量非常大,性能差;AES密钥在直接在网络电传输,存在被拦截的风险。可使用 RSA+AES 混合加密,兼顾效率和安全:

  1. 双方建立通道,传输RSA公钥

  2. 发送方使用接收方的RSA公钥,对本地的AES密钥加密

  3. 将加密后的AES秘钥,传输给接收方,接收方通过本地RSA私钥解密,得到AES密钥,原始AES密钥不通过网络传输,安全性高

  4. 发送方使用AES密钥对数据加密,效率高,然后发送数据

  5. 接收方使用AES密钥解密数据


加密和签名,有什么区别?

加密是为了防止信息被泄露,签名是为了防止信息被篡改。

通信过程:

  1. 【建连】建立通信通道,互换公钥

  2. 【加密】发送方使用接收方的RSA公钥加密信息得到data

  3. 【加签】发送方对要发送的信息,先进行消息摘要得到p,然后使用自己的RSA私钥加密p得到sign

  4. 【发送】发送data、p、sign

  5. 【解密】接收方使用自己的私钥解密信息data

  6. 【验签】接收方使用发送方的公钥解密sign,得到 p’,如果 p’== p,说明信息没有被篡改


HTTP2的作用?


什么是DDoS攻击?如何防止被攻击?

高扩展

如何设计一个高扩展系统?

成本

研发团队的成本主要有哪些?如何做好研发团队的成本控制与优化?

八、场景设计

商城

如何设计电商平台?


库存/积分的扣减如何设计?

  • 性能:使用redis lua脚本进行扣减,可以应对高流量。

  • 数据一致性:redis执行是单线程,可以保证原子性+有序性。

  • 异步持久化:发送消息到消息队列,异步同步数据到数据库,避免redis数据丢失。

  • 对账机制:准实时对账,防止超卖、少卖


商城系统,如何选择分布式事务解决方案?

分布式事务解决的是分布式系统下的数据一致性问题,不同业务场景对数据一致性的要求不同,方案也不同。 场景1:订单创建和取消时,需要对库存和优惠券系统进行操作。 这个操作如果不能保证强一致性,可能导致库存超卖或优惠券重复使用。 对于强一致性场景,我们采用Seata的AT模式来处理。

场景2:支付成功后通知发货系统发货,确认收货后通知积分系统发放积分。 只要保证能够通知成功即可,不需要同时成功同时失败。 对于最终一致性场景,我们采用的是本地消息表方案:在本地事务中将要执行的异步操作记录在消息表中,如果执行失败,可以通过定时任务来补偿。


订单表如何设计?


订单表按什么字段分表?

社交

实现朋友圈点赞功能?

功能分析:首先我们需要分析下朋友圈点赞需要有哪些功能,首先记录某个朋友圈的点赞数量,并且支持点赞数数量的查看,支持点赞和取消点赞操作。并且支持查看哪些人点过赞,并且点赞的顺序是可以看得到的。

在数据结构上,我们可以采用ZSet来实现,KEY就是这个具体的朋友圈的ID,ZSET的value表示点赞用户的ID,score表示点赞时间的时间戳。这样可以方便地按照时间顺序查询点赞信息,并支持对点赞进行去重,

九、算法

单例

实现思路:

  • 构造函数设置成private,否则可以随时通过构造函数创建对象。

  • 提供一个方法,可以获取单例对象,并且要保证只能初始化一个单例对象。

1. 懒汉:需要时才创建对象

public class Singleton{ 
    // new 一个单例对象 
    private static final Singleton singleton = new Singleton(); 
    // 私有构造 
    private Singleton(){} 
    // 静态函数:用于获取实例对象 
    public static Singleton getInstance() { 
        return singleton; 
    } 
}

2. 双重检查锁

public class Singleton { 
    private volatile static Singleton singleton; 
    private Singleton (){} 
    public static Singleton getSingleton() { 
        if (singleton == null) { 
            synchronized (Singleton.class) { 
                if (singleton == null) { 
                    singleton = new Singleton(); 
                } 
            } 
        } 
        return singleton; 
    } 
}

3. 枚举

public enum Singleton { 
    INSTANCE; 
    public void whateverMethod() { } 
}

这种实现方式还没有被广泛采用,但这是实现单例模式的最佳方法。因为:1. 写法简单;2. 枚举实现的单例天然是线程安全的;3. 自动支持序列化机制,防止反序列化重新创建新的对象。

数组

合并两个有序数组

给你两个按 非递减顺序 排列的整数数组 nums1 和 nums2,另有两个整数 m 和 n ,分别表示 nums1 和 nums2 中的元素数目。

请你 合并 nums2 到 nums1 中,使合并后的数组同样按 非递减顺序 排列。

注意:最终,合并后数组不应由函数返回,而是存储在数组 nums1 中。为了应对这种情况,nums1 的初始长度为 m + n,其中前 m 个元素表示应合并的元素,后 n 个元素为 0 ,应忽略。nums2 的长度为 n 。

示例:

输入:nums1 = [1,2,3,0,0,0], m = 3, nums2 = [2,5,6], n = 3

输出:[1,2,2,3,5,6]

// 方法1:合并,排序 
public void merge(int[] nums1, int m, int[] nums2, int n) { 
    for(int i = m; i < m + n; i++){ 
        nums1[i] = nums2[i - m]; 
    } 
    Arrays.sort(nums1); 
} 

// 方法2:从后往前遍历,大的放最后,可以避免覆盖的问题。 
public void merge(int[] nums1, int m, int[] nums2, int n) { 
    while(m > 0 && n > 0){ 
        if(nums1[m - 1] > nums2[n - 1]){ 
            nums1[m + n - 1] = nums1[m - 1]; 
            m--; 
        } else { 
            nums1[m + n - 1] = nums2[n - 1]; 
            n--; 
        } 
    } 
    if(n > 0){ 
        while(n > 0){ 
            nums1[n - 1] = nums2[n - 1]; 
            n--; 
        } 
    } 
}

买卖股票的最佳时机

给定一个数组 prices ,它的第 i 个元素 prices[i] 表示一支给定股票第 i 天的价格。

你只能选择 某一天 买入这只股票,并选择在 未来的某一个不同的日子 卖出该股票。设计一个算法来计算你所能获取的最大利润。

示例:

输入:[7,1,5,3,6,4]

输出:5

解释:在第 2 天(股票价格 = 1)的时候买入,在第 5 天(股票价格 = 6)的时候卖出,最大利润 = 6-1 = 5 。

// 简化问题:第i天卖出股票的最大收益,维护当前最小值、当前最大收益,2个变量。 
public int maxProfit(int[] prices) { 
    int max = 0; 
    int min = prices[0]; 
    for(int i = 1; i < prices.length; i++){ 
        if(prices[i] < min){ 
            min = prices[i]; 
            continue; 
        } else { 
            max = Math.max(max, prices[i] - min); 
        } 
    } 
    return max; 
}

买卖股票的最佳时机 II

给你一个整数数组 prices ,其中 prices[i] 表示某支股票第 i 天的价格。

在每一天,你可以决定是否购买和/或出售股票。你在任何时候 最多 只能持有 一股 股票。你也可以先购买,然后在 同一天 出售。

返回 你能获得的 最大 利润 。

示例:

输入:prices = [7,1,5,3,6,4]

输出:7

解释:在第 2 天(股票价格 = 1)买入,第 3 天(股票价格 = 5)卖出,利润 = 5 - 1 = 4 ;在第 4 天(股票价格 = 3)买入,在第 5 天(股票价格 = 6)卖出,利润 = 6 - 3 = 3 。总利润为 4 + 3 = 7 。

// 只要今天比昨天高,就可以买,遍历数组,累积差值。 
public int maxProfit(int[] prices) { 
    int profit = 0; 
    for(int i = 1; i < prices.length; i++){ 
        if(prices[i] > prices[i - 1]) 
            profit += prices[i] - prices[i - 1]; 
    } 
    return profit; 
}

赎金信

给你两个字符串:ransomNote 和 magazine ,判断 ransomNote 能不能由 magazine 里面的字符构成。

如果可以,返回 true ;否则返回 false 。

magazine 中的每个字符只能在 ransomNote 中使用一次。

示例1:

输入:ransomNote = "aa", magazine = "ab"

输出:false

示例2:

输入:ransomNote = "aa", magazine = "aab"

输出:true

// 方法1:使用HashMap统计字符数量 
public boolean canConstruct(String ransomNote, String magazine) { 
    Map<Character, Integer> map = new HashMap(); 
    for(int i = 0; i < magazine.length(); i++){ 
        if(map.containsKey(magazine.charAt(i))){ 
            map.put(magazine.charAt(i), map.get(magazine.charAt(i)) + 1); 
        } else { 
            map.put(magazine.charAt(i), 1); 
        } 
    }
 
    for(int j = 0; j < ransomNote.length(); j++){     
        if(map.containsKey(ransomNote.charAt(j)) 
            && map.get(ransomNote.charAt(j)) > 0){ 
            map.put(ransomNote.charAt(j), map.get(ransomNote.charAt(j)) - 1); 
        } else { 
            return false; 
        } 
    } 
    return true; 
} 

// 方法2:利用数组int[26]统计字符数量,内存小 
public boolean canConstruct(String ransomNote, String magazine) { 
    int[] dic = new int[26]; 
    for(int i = 0; i < magazine.length(); i++){ 
        dic[magazine.charAt(i) - 'a']++; 
    } 
    for(int j = 0; j < ransomNote.length(); j++){ 
        if(dic[ransomNote.charAt(j) - 'a'] < 1) 
            return false; 
        dic[ransomNote.charAt(j) - 'a']--; 
    } 
    return true; 
}

轮转数组

给定一个整数数组 nums,将数组中的元素向右轮转 k 个位置,其中 k 是非负数。

示例:

输入: nums = [1,2,3,4,5,6,7], k = 3

输出: [5,6,7,1,2,3,4]

解释:

向右轮转 1 步: [7,1,2,3,4,5,6]

向右轮转 2 步: [6,7,1,2,3,4,5]

向右轮转 3 步: [5,6,7,1,2,3,4]

// 先整体翻转,再局部翻转。 
public void rotate(int[] nums, int k) { 
    k = k % nums.length; 
    reverse(nums, 0, nums.length - 1); 
    reverse(nums, 0, k - 1); 
    reverse(nums, k, nums.length - 1); 
} 

public void reverse(int[] nums, int left, int right){ 
    while(left < right){ 
        int tmp = nums[left]; 
        nums[left] = nums[right]; 
        nums[right] = tmp; 
        left++; 
        right--; 
    } 
}

跳跃游戏

给你一个非负整数数组 nums ,你最初位于数组的 第一个下标 。数组中的每个元素代表你在该位置可以跳跃的最大长度。

判断你是否能够到达最后一个下标,如果可以,返回 true ;否则,返回 false 。

示例 1:

输入:nums = [2,3,1,1,4]

输出:true

示例 2:

输入:nums = [3,2,1,0,4]

输出:false

解释:无论怎样,总会到达下标为 3 的位置。但该下标的最大跳跃长度是 0 , 所以永远不可能到达最后一个下标。

// 将复杂问题转化为一系列简单问题:nums[i]的最远跳跃距离 
public boolean canJump(int[] nums) { 
    int max = nums[0]; 
    for(int i = 0; i < nums.length - 1; i++){ 
        max = Math.max(i + nums[i], max); // 最远距离
        if(i + 1 > max) 
            return false; // 下一步不可达 
    } 
    return max >= nums.length - 1; 
}

跳跃游戏 II

给定一个长度为 n 的 0 索引整数数组 nums。初始位置为 nums[0]。

每个元素 nums[i] 表示从索引 i 向前跳转的最大长度。

返回到达 nums[n - 1] 的最小跳跃次数。生成的测试用例可以到达 nums[n - 1]。

示例:

输入: nums = [2,3,1,1,4]

输出: 2

解释: 从下标为 0 跳到下标为 1 的位置,跳 1 步,然后跳 3 步到达数组的最后一个位置。

// 简化问题:第X跳的最远距离 
public int jump(int[] nums) { 
    if(nums.length == 1) 
        return 0; 
    int count = 1; 
    int left = 0; 
    int right = nums[0]; 
    while(right < nums.length - 1){ 
        count++; 
        left++; 
        int end = right; 
        for(int i = left; i <= end; i++){ 
            // 下一跳的最远距离 
            right = Math.max(right, i + nums[i]); 
            if(right >= nums.length - 1) 
                break; 
        } 
    } 
    return count; 
}

H 指数

给你一个整数数组 citations ,其中 citations[i] 表示研究者的第 i 篇论文被引用的次数。计算并返回该研究者的 h 指数。

根据维基百科上 h 指数的定义:h 代表“高引用次数” ,一名科研人员的 h 指数 是指他(她)至少发表了 h 篇论文,并且每篇论文 至少 被引用 h 次。如果 h 有多种可能的值,h 指数 是其中最大的那个。

示例:

输入:citations = [3,0,6,1,5]

输出:3

解释:给定数组表示研究者总共有 5 篇论文,每篇论文相应的被引用了 3, 0, 6, 1, 5 次。由于研究者有 3 篇论文每篇 至少 被引用了 3 次,其余两篇论文每篇被引用 不多于 3 次,所以她的 h 指数是 3。

// 1. 学会利用排序简化问题 
// 2. 从右到左逐一尝试citations[citations.length - h] 和 h 的大小关系 
public int hIndex(int[] citations) { 
    Arrays.sort(citations); 
    int h = citations.length; 
    while(h > 0){ 
        if(citations[citations.length - h] >= h) 
            return h; 
        h--; 
    } 
    return 0; 
}

除自身以外数组的乘积

给你一个整数数组 nums,返回 数组 answer ,其中 answer[i] 等于 nums 中除 nums[i] 之外其余各元素的乘积 。

题目数据 保证 数组 nums之中任意元素的全部前缀元素和后缀的乘积都在 32 位 整数范围内。

请 不要使用除法,且在 O(n) 时间复杂度内完成此题。

示例:

输入: nums = [1,2,3,4]

输出: [24,12,8,6]

// 总乘积 = 左侧乘积 * 右侧乘积 
public int[] productExceptSelf(int[] nums) { 
    int length = nums.length; 
    int[] L = new int[length]; 
    L[0] = 1; 
    for(int i = 1; i < length; i++){ 
        L[i] = L[i - 1] * nums[i - 1]; 
    } 
    int[] R = new int[length]; 
    R[length - 1] = 1; 
    for(int j = length - 2; j >=0 ; j--){ 
        R[j] = R[j + 1] * nums[j + 1]; 
    } 
    int[] answer = new int[length]; 
    for(int k = 0; k < length; k++){ 
        answer[k] = L[k] * R[k]; 
    } 
    return answer; 
}

和为 K 的子数组

一个整数数组 nums 和一个整数 k ,请统计并返回该数组中和为 k 的连续子数组的个数 。

示例:

输入:nums = [1,1,1], k = 2

输出:2

// 固定起点,向后累加,=k就计数。 
public int subarraySum(int[] nums, int k) { 
    int count = 0; 
    for(int i = 0; i < nums.length; i++){ 
        int sum = 0; 
        for(int j = i; j < nums.length; j++){ 
            sum += nums[j]; 
            if(sum != k) { 
                continue; 
            } else { 
                count++; 
            } 
        } 
    } 
    return count; 
}

最短无序连续子数组

给你一个整数数组 nums ,你需要找出一个 连续子数组 ,如果对这个子数组进行升序排序,那么整个数组都会变为升序排序。

请你找出符合题意的 最短 子数组,并输出它的长度。

示例:

输入:nums = [2,6,4,8,10,9,15]

输出:5

解释:你只需要对 [6, 4, 8, 10, 9] 进行升序排序,那么整个表都会变为升序排序。

// 方法1:和排序好的数组做对比,找到左右差异位置。 
public int findUnsortedSubarray(int[] nums) { 
    int[] numsSort = new int[nums.length]; 
    for(int i = 0; i < nums.length; i++){ 
        numsSort[i] = nums[i]; 
    } 
    Arrays.sort(numsSort); 
    int left = 0; 
    int right = nums.length - 1; 
    while(left <= nums.length - 1){ 
        if(nums[left] != numsSort[left]) 
            break; 
        left++; 
    } 
    while(right >= 0){ 
        if(nums[right] != numsSort[right]) 
            break; 
        right--; 
    } 
    if(left < right){ 
        return right - left + 1; 
    } 
    return 0; 
} 


// 方法2:从左往右找最后一个nums[i]<当前max的位置,从右往左找最后一个nums[i]>当前min的位置。 
public int findUnsortedSubarray(int[] nums) { 
    int max = nums[0]; 
    int right = 0; 
    for(int i = 1; i < nums.length; i++){ 
        if(nums[i] < max){ 
            right = i; 
        } 
        max = Math.max(max, nums[i]); 
    } 
    int min = nums[nums.length - 1]; 
    int left = nums.length - 1; 
    for(int j = nums.length - 2; j >= 0; j--){ 
        if(nums[j] > min){ 
            left = j; 
        } 
        min = Math.min(min, nums[j]); 
    } 
    return left >= right ? 0 : right - left + 1; 
}

旋转图像

给定一个 n × n 的二维矩阵 matrix 表示一个图像。请你将图像顺时针旋转 90 度。

你必须在 原地 旋转图像,这意味着你需要直接修改输入的二维矩阵。请不要 使用另一个矩阵来旋转图像。

示例:

输入:matrix = [[1,2,3],[4,5,6],[7,8,9]]

输出:[[7,4,1],[8,5,2],[9,6,3]]

// 1. 枚举几个case,找到规律:[i, j] → [j, length - 1 - i ] 
// 2. 二步走,先[i, j] → [j, i] 对角线,再[j, i] → [j, length - 1 - i] 垂直翻转

合并区间

数组 intervals 表示若干个区间的集合,请合并所有重叠的区间,并返回 一个不重叠的区间数组。

示例:

输入:intervals = [[1,3],[2,6],[8,10],[15,18]]

输出:[[1,6],[8,10],[15,18]]

解释:区间 [1,3] 和 [2,6] 重叠, 将它们合并为 [1,6].

// 按左元素排序,逐一判断。 
// 关键是不会写数组排序:Arrays.sort(intervals, (o1, o2) -> (o1[0] - o2[0])); 
// 也不会将List快速转化为数组:list.toArray(new int[list.size()][2]); 
public int[][] merge(int[][] intervals) { 
    Arrays.sort(intervals, (o1, o2) -> (o1[0] - o2[0])); 
    int length = intervals.length; 
    List<int[]> list = new ArrayList(); 
    int left = intervals[0][0]; 
    int right = intervals[0][1]; 
    for(int i = 1; i < length; i++){ 
        if(intervals[i][0] <= right) { 
            right = Math.max(right, intervals[i][1]); 
        } else { 
            list.add(new int[]{left, right}); 
            left = intervals[i][0]; 
            right = intervals[i][1]; 
        } 
    } 
    list.add(new int[]{left, right}); 
    return list.toArray(new int[list.size()][2]); 
}

数组中重复的数据

给你一个长度为 n 的整数数组 nums ,其中 nums 的所有整数都在范围 [1, n] 内,且每个整数出现 一次 或 两次 。请你找出所有出现 两次 的整数,并以数组形式返回。

时间复杂度为 O(n),使用常量额外空间的算法。

示例:

输入:nums = [4,3,2,7,8,2,3,1]

输出:[2,3]

// 由于都是1-n的数,构建访问规则:访问nums[i]后,将nums[nums[i] - 1]置为负数 
// 遍历过程中,如果发现访问规则下出现负数,说明已访问过,加入结果集。 
public List<Integer> findDuplicates(int[] nums) { 
    List<Integer> list = new ArrayList(); 
    for(int i = 0; i < nums.length; i++){ 
        int cur = Math.abs(nums[i]); 
        if(nums[cur - 1] > 0) { 
            nums[cur - 1] *= -1; 
        } else { 
            list.add(cur); 
        } 
    } 
    return list; 
}

搜索

搜索插入位置

给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置。

请必须使用时间复杂度为 O(log n) 的算法。

示例 1:

输入: nums = [1,3,5,6], target = 5

输出: 2

示例 2:

输入: nums = [1,3,5,6], target = 2

输出: 1

// 二分查找 
public int searchInsert(int[] nums, int target) { 
    int left = 0; 
    int right = nums.length - 1; 
    while(left <= right){ 
        int mid = (left + right) / 2; 
        if(nums[mid] == target){ 
            return mid; 
        } else if (nums[mid] < target){ 
            left = mid + 1; 
        } else { 
            right = mid - 1; 
        } 
    } 
    return left; 
}

多数元素

给定一个大小为 n 的数组 nums ,返回其中的多数元素。多数元素是指在数组中出现次数 大于 ⌊ n/2 ⌋ 的元素。

// 方法1:排序,取中位。 
public int majorityElement(int[] nums) { 
    Arrays.sort(nums); 
    return nums[(nums.length - 1) / 2]; 
} 

// 方法2:投票法,设立候选人,记录出现次数,遍历数组时,相同count++,不同count--,count小于0时更换候选人。 
public int majorityElement(int[] nums) { 
    int cur = nums[0]; 
    int count = 0; 
    for(int i = 1; i < nums.length; i++){ 
        if(nums[i] == cur){ 
            count++; 
        } else { 
            count--; 
            if(count < 0){ 
                cur = nums[i + 1]; 
            } 
        } 
    } 
    return cur; 
}

字符串

罗马数字转整数

罗马数字包含以下七种字符: I, V, X, L,C,D 和 M。

字符 数值

I 1

V 5

X 10

L 50

C 100

D 500

M 1000

例如, 罗马数字 2 写做 II ,即为两个并列的 1 。12 写做 XII ,即为 X + II 。 27 写做 XXVII, 即为 XX + V + II 。

特殊规则:

I 可以放在 V (5) 和 X (10) 的左边,来表示 4 和 9。

X 可以放在 L (50) 和 C (100) 的左边,来表示 40 和 90。

C 可以放在 D (500) 和 M (1000) 的左边,来表示 400 和 900。

给定一个罗马数字,将其转换成整数。

示例1:

输入: s = "LVIII"

输出: 58

解释: L = 50, V= 5, III = 3.

示例2:

输入: s = "MCMXCIV"

输出: 1994

解释: M = 1000, CM = 900, XC = 90, IV = 4.

// 方法1:逐个字符累加,遇到特殊情况向后再探测1位。 
public int romanToInt(String s) { 
    int result = 0; 
    for(int i = 0; i < s.length(); i++){ 
        char c = s.charAt(i); 
        if(c == 'M'){ 
            result += 1000; 
        } else if (c == 'D'){ 
            result += 500; 
        } else if (c == 'C'){ 
            if(i < s.length() - 1 && s.charAt(i + 1) == 'D'){ 
                result += 400; 
                i++; 
            } else if (i < s.length() - 1 && s.charAt(i + 1) == 'M'){ 
                result += 900; 
                i++; 
            } else { 
                result += 100; 
            } 
        } else if (c == 'L'){ 
            result += 50; 
        } else if (c == 'X'){ 
            if(i < s.length() - 1 && s.charAt(i + 1) == 'L'){ 
                result += 40; 
                i++; 
            } else if (i < s.length() - 1 && s.charAt(i + 1) == 'C'){ 
                result += 90; 
                i++; 
            } else { 
                result += 10; 
            } 
        } else if (c == 'V'){ 
            result += 5; 
        } else if (c == 'I'){ 
            if(i < s.length() - 1 && s.charAt(i + 1) == 'V'){ 
                result += 4; 
                i++; 
            } else if (i < s.length() - 1 && s.charAt(i + 1) == 'X'){ 
                result += 9; 
                i++; 
            } else { 
                result += 1; 
            } 
        } 
    } 
    return result; 
} 


// 方法2:替换特殊字符,建立字典,遍历累加 
public int romanToInt(String s) { 
    s = s.replace("IV", "a"); // 不能原地修改,需要取replace后的引用 
    s = s.replace("IX", "b"); 
    s = s.replace("XL", "c"); 
    s = s.replace("XC", "d"); 
    s = s.replace("CD", "e"); 
    s = s.replace("CM", "f"); 

    Map<Character, Integer> map = new HashMap(); 
    map.put('I', 1); 
    map.put('V', 5); 
    map.put('X', 10); 
    map.put('L', 50); 
    map.put('C', 100); 
    map.put('D', 500); 
    map.put('M', 1000); 
    map.put('a', 4); 
    map.put('b', 9); 
    map.put('c', 40); 
    map.put('d', 90); 
    map.put('e', 400); 
    map.put('f', 900); 

    int result = 0; 
    for(int i = 0; i < s.length(); i++){ 
        result += map.get(s.charAt(i)); 
    } 

    return result; 
}

最长公共前缀

编写一个函数来查找字符串数组中的最长公共前缀。

如果不存在公共前缀,返回空字符串 ""。

示例:

输入:strs = ["flower","flow","flight"]

输出:"fl"

// 纵向扫描,遇到长度越界或字符不相等,立即返回结果。 
public String longestCommonPrefix(String[] strs) { 
    String result = ""; 
    for(int i = 0; i < strs[0].length(); i++){ 
        char c = strs[0].charAt(i); 
        for(int j = 1; j < strs.length; j++){ 
            if (strs[j].length() > i && strs[j].charAt(i) == c){ 
                continue; 
            } else { 
                return result; 
            } 
        } 
        result += strs[0].charAt(i); 
    } 
    return result; 
}

找出字符串中第一个匹配项的下标

给你两个字符串 haystack 和 needle ,请你在 haystack 字符串中找出 needle 字符串的第一个匹配项的下标(下标从 0 开始)。如果 needle 不是 haystack 的一部分,则返回 -1 。

示例:

输入:haystack = "sadbutsad", needle = "sad"

输出:0

// 利用substring对比值 
public int strStr(String haystack, String needle) { 
    int index = -1; 
    int start = 0; 
    while(start < haystack.length()){ 
        if(start + needle.length() > haystack.length()) 
            return index; 
        String tmp = haystack.substring(start, start + needle.length());
        if(tmp.equals(needle)) 
            return start; 
        start++; 
    } 
    return index; 
}

验证回文串

如果在将所有大写字符转换为小写字符、并移除所有非字母数字字符之后,短语正着读和反着读都一样。则可以认为该短语是一个 回文串 。

字母和数字都属于字母数字字符。

给你一个字符串 s,如果它是 回文串 ,返回 true ;否则,返回 false 。

示例:

输入: s = "A man, a plan, a canal: Panama"

输出:true

解释:"amanaplanacanalpanama" 是回文串。

// 利用StringBuilder.append字符,利用Character.toLowerCase(c)转化大写,利用左右指针判断回文 
public boolean isPalindrome(String s) { 
    StringBuilder sb = new StringBuilder(); 
    for(int i = 0; i < s.length(); i++){ 
        char c = s.charAt(i); 
        if((c >= 'a' && c <= 'z') || (c >= '0' && c <= '9')){ 
            sb.append(c); 
        } else if(c >= 'A' && c <= 'Z') { 
            sb.append(Character.toLowerCase(c)); // Character.toLowerCase(c)不会! 
        } 
    } 
    int left = 0; 
    int right = sb.length() - 1; 
    while(left <= right){ 
        if(sb.charAt(left) == sb.charAt(right)) { 
            left++; 
            right--; 
        } else { 
            return false; 
        } 
    } 
    return true; 
}

回文子串

给你一个字符串 s ,请你统计并返回这个字符串中 回文子串 的数目。

回文字符串 是正着读和倒过来读一样的字符串。

子字符串 是字符串中的由连续字符组成的一个序列。

具有不同开始位置或结束位置的子串,即使是由相同的字符组成,也会被视作不同的子串。

示例 1:

输入:s = "abc"

输出:3

解释:三个回文子串: "a", "b", "c"

示例 2:

输入:s = "aaa"

输出:6

解释:6个回文子串: "a", "a", "a", "aa", "aa", "aaa"

// 方法1:遍历所有以i为起点的字符串,判断是否为回文。效率低。 
public int countSubstrings(String s) { 
    char[] arr = s.toCharArray(); 
    int count = 0; 
    for(int i = 0; i < arr.length; i++){ 
        for(int j = i; j < arr.length; j++){ 
            if(check(arr, i, j)) 
                count++; 
        } 
    } 
    return count; 
} 

public boolean check(char[] arr, int left, int right){ 
    while(left <= right){ 
        if(arr[left] != arr[right]) 
            return false; 
        left++; 
        right--; 
    } 
    return true; 
} 


// 方法2:判断以i为中心的字符串是否为回文,有2种形式:xax,xaax。效率高,可随时中止枚举。 
public int countSubstrings(String s) { 
    char[] arr = s.toCharArray(); 
    int count = 0; 
    for(int i = 0; i < arr.length; i++){ 
        for(int j = 0; i + j < arr.length && i - j >= 0; j++){ 
            if(arr[i + j] != arr[i - j]) 
                break; 
            count++; 
        } 
        for(int j = 0; i + 1 + j < arr.length && i - j >= 0; j++){ 
            if(arr[i + 1 + j] != arr[i - j]) 
                break; 
            count++; 
        } 
    } 
    return count; 
}

最长回文子串

给你一个字符串 s,找到 s 中最长的回文子串。

如果字符串的反序与原始字符串相同,则该字符串称为回文字符串。

示例:

输入:s = "babad"

输出:"bab"

解释:"aba" 同样是符合题意的答案。

// 中心扩散法(xax,xaax),虽然代码长,但是效率高,因为不会枚举所有。 
public String longestPalindrome(String s) { 
    char[] arr = s.toCharArray(); 
    int length = s.length(); 
    int left = 0, right = 0; 
    int max = 0;
 
    for(int i = 0; i < length; i++){ 
        for(int j = 1; i - j >= 0 && i + j <= length - 1; j++){ 
            if(arr[i - j] == arr[i + j]){ 
                if(2 * j > max){ 
                    max = 2 * j; 
                    left = i - j; 
                    right = i + j; 
                } 
            } else { 
                break; 
            } 
        } 
        for(int k = 0; i - k >= 0 && i + 1 + k <= length - 1; k++){ 
            if(arr[i - k] == arr[i + 1 + k]){ 
                if((2 * k + 1) > max){ 
                    max = 2 * k + 1; 
                    left = i - k; 
                    right = i + 1 + k; 
                } 
            } else { 
                break; 
            } 
        } 
    } 

    return s.substring(left, right + 1); 
}

双指针

判断子序列

给定字符串 s 和 t ,判断 s 是否为 t 的子序列。

字符串的一个子序列是原始字符串删除一些(也可以不删除)字符而不改变剩余字符相对位置形成的新字符串。(例如,"ace"是"abcde"的一个子序列,而"aec"不是)。

示例:

输入:s = "abc", t = "ahbgdc"

输出:true

// 双指针 + while 
public boolean isSubsequence(String s, String t) { 
    int i = 0, j = 0; 
    while(i < s.length() && j < t.length()){ 
        if(s.charAt(i) == t.charAt(j)) { 
            i++; 
        } 
        j++; 
    } 
    if(i == s.length()) 
        return true; 
    return false; 
}

删除有序数组中的重复项

给你一个 非严格递增排列 的数组 nums ,请你 原地 删除重复出现的元素,使每个元素 只出现一次 ,返回删除后数组的新长度。元素的 相对顺序 应该保持 一致 。然后返回 nums 中唯一元素的个数。

示例:

输入:nums = [0,0,1,1,1,2,2,3,3,4]

输出:5, nums = [0,1,2,3,4]

解释:函数应该返回新的长度 5 , 并且原数组 nums 的前五个元素被修改为 0, 1, 2, 3, 4 。不需要考虑数组中超出新长度后面的元素。

// 左右指针,右指针从1→n,找到不等于左指针的数时,他的最终位置就是left++ 
public int removeDuplicates(int[] nums) { 
    int left = 0; 
    int right = 1; 
    while(right < nums.length){ 
        if(nums[right] == nums[left]){ 
            right++; 
        } else { 
            left++; 
            nums[left] = nums[right]; 
            right++; 
        } 
    } 
    left++; 
    return left; 
}

删除有序数组中的重复项 II

给你一个有序数组 nums ,请你 原地 删除重复出现的元素,使得出现次数超过两次的元素只出现两次 ,返回删除后数组的新长度。

不要使用额外的数组空间,你必须在 原地 修改输入数组 并在使用 O(1) 额外空间的条件下完成。

示例:

输入:nums = [0,0,1,1,1,1,2,3,3]

输出:7, nums = [0,0,1,1,2,3,3]

解释:函数应返回新长度 length = 7, 并且原数组的前五个元素被修改为 0, 0, 1, 1, 2, 3, 3。不需要考虑数组中超出新长度后面的元素。

// 左右指针+计数,不要小看,细节很多,很容易错! 
public int removeDuplicates(int[] nums) { 
    if(nums.length == 1) 
        return 1; 
    int left = 0; 
    int right = 1; 
    int count = 1; 
    while(right < nums.length){ 
        if(nums[right] == nums[left]){ 
            if(count < 2) { 
                nums[++left] = nums[right++]; 
            } else { 
                right++; 
            } 
            count++; 
        } else { 
            nums[++left] = nums[right++]; 
            count = 1; 
        } 
    } 
    return left + 1; 
}

移除元素

给你一个数组 nums 和一个值 val,你需要 原地 移除所有数值等于 val 的元素,并返回移除后数组的新长度。

不要使用额外的数组空间,你必须仅使用 O(1) 额外空间并 原地 修改输入数组。

元素的顺序可以改变。你不需要考虑数组中超出新长度后面的元素。

示例:

输入:nums = [0,1,2,2,3,0,4,2], val = 2

输出:5, nums = [0,1,4,0,3]

// 首位双指针,遇到等于val的交换放后面。 
public int removeElement(int[] nums, int val) { 
    int left = 0; 
    int right = nums.length - 1; 
    int count = 0; 

    while(left <= right){ 
        if(nums[left] == val){ 
            int tmp = nums[left]; 
            nums[left] = nums[right]; 
            nums[right] = tmp; 
            right--; 
            count++; 
            continue; 
        } 
        left++; 
    } 
    
    return nums.length - count; 
}

快乐数

编写一个算法来判断一个数 n 是不是快乐数。

「快乐数」 定义为:

对于一个正整数,每一次将该数替换为它每个位置上的数字的平方和。

然后重复这个过程直到这个数变为 1,也可能是 无限循环 但始终变不到 1。

如果这个过程 结果为 1,那么这个数就是快乐数。

如果 n 是 快乐数 就返回 true ;不是,则返回 false 。

示例:

输入:n = 19

输出:true

解释:

12 + 92 = 82

82 + 22 = 68

62 + 82 = 100

12 + 02 + 02 = 1

示例 2:

输入:n = 2

输出:false

// 1. 利用取余计算平方和 
// 2. 假设n有32位,平方和的最大值=32*99,是有穷的,并不大,说明无限循环是一个环。 
// 3. 环的问题,可以使用快慢指针解决。 
public boolean isHappy(int n) { 
    int slow = get(n); 
    int fast = get(slow); 
    while(slow != fast){ 
        slow = get(slow); 
        fast = get(get(fast)); 
    } 
    return slow == 1; 
} 

public int get(int n){ 
    int result = 0; 
    while(n > 0){ 
        int tmp = n % 10; 
        result += tmp * tmp; 
        n = n / 10; 
    } 
    return result; 
}

两数之和 II - 输入有序数组

给你一个下标从 1 开始的整数数组 numbers ,该数组已按 非递减顺序排列 ,请你从数组中找出满足相加之和等于目标数 target 的两个数。

你所设计的解决方案必须只使用常量级的额外空间。

示例:

输入:numbers = [2,7,11,15], target = 9

输出:[1,2]

解释:2 与 7 之和等于目标数 9 。因此 index1 = 1, index2 = 2 。返回 [1, 2] 。

// 左右指针从两边往中间移动,利用了递增特性。 
public int[] twoSum(int[] numbers, int target) { 
    int left = 0; 
    int right = numbers.length - 1; 
    while(left < right){ 
        if(numbers[left] + numbers[right] == target) { 
            break; 
        } else if(numbers[left] + numbers[right] > target){ 
            right--; 
        } else { 
            left++; 
        } 
    } 
    return new int[]{left + 1, right + 1}; 
}

盛最多水的容器

给定一个长度为 n 的整数数组 height 。有 n 条垂线,第 i 条线的两个端点是 (i, 0) 和 (i, height[i]) 。

找出其中的两条线,使得它们与 x 轴共同构成的容器可以容纳最多的水。

返回容器可以储存的最大水量。

示例:

输入:[1,8,6,2,5,4,8,3,7]

输出:49

解释:图中垂直线代表输入数组 [1,8,6,2,5,4,8,3,7]。在此情况下,容器能够容纳水(表示为蓝色部分)的最大值为 49。

// 左右指针从两侧向内移动,由于移动长板,面积肯定小,只能移动短板。 
public int maxArea(int[] height) { 
    int left = 0; 
    int right = height.length - 1; 
    int max = Integer.MIN_VALUE; 
    while(left < right){ 
        max = Math.max(max, 
            (right - left) * Math.min(height[left], height[right]));
        if(height[left] < height[right]) { 
            left++; 
        } else { 
            right--; 
        } 
    } 
    return max; 
}

哈希

两数之和

给定一个整数数组 nums 和一个整数目标值 target,请你在该数组中找出 和为目标值 target 的那 两个 整数,并返回它们的数组下标。

你可以假设每种输入只会对应一个答案。但是,数组中同一个元素在答案里不能重复出现。

你可以按任意顺序返回答案。

示例:

输入:nums = [2,7,11,15], target = 9

输出:[0,1]

解释:因为 nums[0] + nums[1] == 9 ,返回 [0, 1] 。

// 一边遍历,一遍使用哈希表存储中间结果 
public int[] twoSum(int[] nums, int target) { 
    Map<Integer, Integer> map = new HashMap(); 
    int[] result = new int[2]; 
    for(int i = 0; i < nums.length; i++){ 
        if(map.containsKey(target - nums[i])) { 
            result[0] = map.get(target - nums[i]); 
            result[1] = i; 
            return result; 
        } else { 
            map.put(nums[i], i); 
        } 
    } 
    return result; 
}

同构字符串

给定两个字符串 s 和 t ,判断它们是否是同构的。

如果 s 中的字符可以按某种映射关系替换得到 t ,那么这两个字符串是同构的。

每个出现的字符都应当映射到另一个字符,同时不改变字符的顺序。不同字符不能映射到同一个字符上,相同字符只能映射到同一个字符上,字符可以映射到自己本身。

示例 1:

输入:s = "egg", t = "add"

输出:true

示例 2:

输入:s = "foo", t = "bar"

输出:false

示例 3:

输入:s = "paper", t = "title"

输出:true

// HashMap动态统计映射关系,注意containsValue()的使用 
public boolean isIsomorphic(String s, String t) { 
    Map<Character, Character> map = new HashMap(); 
    for(int i = 0; i < s.length(); i++){ 
        if(!map.containsKey(s.charAt(i))){ 
            if(map.containsValue(t.charAt(i))) 
                return false; 
            map.put(s.charAt(i), t.charAt(i)); 
        } else { 
            if(t.charAt(i) != map.get(s.charAt(i))) { 
                return false; 
            } 
        } 
    } 
    return true; 
}

常用方法:

  • push(); // 入栈

  • pop(); // 出栈

  • peek(); // 访问栈顶,不删除元素

  • empty(); // 判断是否为空

  • size(); // 元素个数

有效的括号

给定一个只包括 '(',')','{','}','[',']' 的字符串 s ,判断字符串是否有效。

有效字符串需满足:

左括号必须用相同类型的右括号闭合。

左括号必须以正确的顺序闭合。

每个右括号都有一个对应的相同类型的左括号。

示例 1:

输入:s = "()"

输出:true

示例 2:

输入:s = "(]"

输出:false

// 使用栈存储左括号,弹出右括号。 
public boolean isValid(String s) { 
    if(s.length() == 0 || s.length() % 2 != 0) 
        return false; 
    Stack<Character> stack = new Stack(); 
    for(int i = 0; i < s.length(); i++){ 
        char c = s.charAt(i); 
        if(c == '(' || c == '{' || c == '['){ 
            stack.push(c); 
        } else { 
            if(stack.isEmpty()) 
                return false; 
            char tmp = stack.pop(); 
            if(c == ')' && tmp != '(') 
                return false; 
            if(c == '}' && tmp != '{') 
                return false; 
            if(c == ']' && tmp != '[') 
                return false; 
        } 
    } 
    return stack.size() == 0; 
}

链表

反转链表

给定单链表的头节点 head ,请反转链表,并返回反转后的链表的头节点。

示例:

输入:head = [1,2,3,4,5]

输出:[5,4,3,2,1]

// cur.next = pre; pre = cur; cur = next; 
public ListNode reverseList(ListNode head) { 
    ListNode pre = null; 
    ListNode cur = head; 
    while(cur != null){ 
        ListNode next = cur.next; 
        cur.next = pre; 
        pre = cur; 
        cur = next; 
    } 
    return pre; 
}

链表是否有环

// 快慢指针 
public boolean hasCycle(ListNode head) { 
    ListNode slow = head; 
    ListNode fast = head; 
    while(true){ 
        if(slow == null || fast == null || fast.next == null) 
            return false; 
        slow = slow.next; 
        fast = fast.next.next; 
        if(slow == fast) 
            return true; 
    } 
}

环形链表 II

给定一个链表的头节点 head ,返回链表开始入环的第一个节点。 如果链表无环,则返回 null。

思路:快慢指针相遇后,重新从链表头开始移动,同时慢指针继续移动,相遇处即是环入口。

合并两个有序链表

将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。

示例:

输入:l1 = [1,2,4], l2 = [1,3,4]

输出:[1,1,2,3,4,4]

// 迭代对比元素值,难点是如何返回head。 
public ListNode mergeTwoLists(ListNode list1, ListNode list2) { 
    ListNode head = new ListNode(); 
    ListNode cur = head; // 使用head的替身进行迭代 
    while(list1 != null && list2 != null){ 
        if(list1.val < list2.val){ 
            cur.next = list1; 
            list1 = list1.next; 
        } else { 
            cur.next = list2; 
            list2 = list2.next; 
        } 
        cur = cur.next; 
    } 
    if(list1 != null) 
        cur.next = list1; 
    if(list2 != null) 
        cur.next = list2; 
    return head.next; // 返回第一个处理节点 
}

两数相加

给你两个 非空 的链表,表示两个非负的整数。它们每位数字都是按照 逆序 的方式存储的,并且每个节点只能存储 一位 数字。

请你将两个数相加,并以相同形式返回一个表示和的链表。

示例:

输入:l1 = [2,4,3], l2 = [5,6,4]

输出:[7,0,8]

解释:342 + 465 = 807.

// 1. 创建头节点,赋值给cur用于遍历,同时用于最终返回头结点。 
// 2. 记录进位carry,同时判断尾部是否需要新创建节点1 
public ListNode addTwoNumbers(ListNode l1, ListNode l2) { 
    int carry = 0; 
    ListNode head = new ListNode(); 
    ListNode cur = head; 
    while(l1 != null || l2 != null){ 
        int a = l1 == null ? 0 : l1.val; 
        int b = l2 == null ? 0 : l2.val; 
        int sum = a + b + carry; 
        carry = sum > 9 ? 1 : 0; 
        ListNode tmp = new ListNode(); 
        tmp.val = sum % 10; 
        cur.next = tmp; 
        cur = cur.next; 
        l1 = l1 == null ? null : l1.next; 
        l2 = l2 == null ? null : l2.next; 
    } 
    if(carry == 1){ 
        ListNode last = new ListNode(); 
        last.val = 1; 
        cur.next = last; 
    } 
    return head.next; 
}

二叉树

二叉树的最大深度

// 1. 先找出递归规律:当前节点高度 = max(左子高度, 右子高度) + 1 
// 2. 再找出递归的条件:空节点高度为0 
public int maxDepth(TreeNode root) { 
    if(root == null) return 0; 
    int left = maxDepth(root.left); 
    int right = maxDepth(root.right); 
    return Math.max(left, right) + 1; 
}

相同的树

给你两棵二叉树的根节点 p 和 q ,编写一个函数来检验这两棵树是否相同。

// 递归规律:左右子树都相同,当前节点才算true 
public boolean isSameTree(TreeNode p, TreeNode q) { 
    if(p == null && q == null) return true; 
    if(p == null || q == null) return false; 
    if(p.val != q.val) return false; 
    return isSameTree(p.left, q.left) && isSameTree(p.right, q.right); 
}

翻转二叉树

给你一棵二叉树的根节点 root ,翻转这棵二叉树,并返回其根节点。

// 方法1:递归,调整当前节点 → 调整左右子树 → 判断退出递归 
public TreeNode invertTree(TreeNode root) { 
    if(root == null) return null; 
    TreeNode tmp = root.left; 
    root.left = root.right; 
    root.right = tmp; 
    invertTree(root.left); 
    invertTree(root.right); 
    return root; 
} 

// 方法2:迭代,使用LinkedList作为队列,层序遍历调整。 
public TreeNode invertTree(TreeNode root) { 
    if(root == null) return root; 
    LinkedList<TreeNode> queue = new LinkedList(); 
    queue.add(root); 
    while(!queue.isEmpty()){ 
        // 1. 取出元素 
        TreeNode cur = queue.pop(); // 队列和栈,弹出元素都是pop 
        // 2. 调整左右 
        TreeNode left = cur.left; 
        TreeNode right = cur.right; 
        cur.left = right; 
        cur.right = left; 
        // 3. 存入队列 
        if(left != null){ 
            queue.add(left); // 队列满时,add抛异常,offer返回false 
        } 
        if(right != null){ 
            queue.add(right); 
        } 
    } 
    return root; 
}

对称二叉树

给你一个二叉树的根节点 root , 检查它是否轴对称。

// 递归:先列举不依赖左右子树的场景,如:节点为空、值不相等,都没问题时再递归判断左右子树。 
public boolean isSymmetric(TreeNode root) { 
    return check(root, root); 
} 

public boolean check(TreeNode node1, TreeNode node2){ 
    if(node1 == null && node2 == null) return true; 
    if(node1 == null || node2 == null) return false; 
    if(node1.val != node2.val) return false; 
    return check(node1.left, node2.right) && check(node1.right, node2.left); 
}

路径总和

给你二叉树的根节点 root 和一个表示目标和的整数 targetSum 。判断该树中是否存在 根节点到叶子节点 的路径,这条路径上所有节点值相加等于目标和 targetSum 。如果存在,返回 true ;否则,返回 false 。

// 递归: 
// 1. 还没有到叶子节点时,向下传递targetSum-cur.val,到叶子节点时,判断是否相等 
// 2. 左右子树递归结果取或运算,很巧妙。 
public boolean hasPathSum(TreeNode root, int targetSum) { 
    if(root == null) return false; 
    if(root.left == null && root.right == null){ 
        return root.val == targetSum; 
    } 
    return hasPathSum(root.left, targetSum - root.val) 
        || hasPathSum(root.right, targetSum - root.val); 
}

二叉树的层平均值

给定一个非空二叉树的根节点 root , 以数组的形式返回每一层节点的平均值。与实际答案相差 10-5 以内的答案可以被接受。

示例:

输入:root = [3,9,20,null,null,15,7]

输出:[3.00000,14.50000,11.00000]

解释:第 0 层的平均值为 3,第 1 层的平均值为 14.5,第 2 层的平均值为 11 。因此返回 [3, 14.5, 11] 。

// 使用queue的size进行层序遍历 
public List<Double> averageOfLevels(TreeNode root) { 
    List<Double> result = new ArrayList(); 
    LinkedList<TreeNode> queue = new LinkedList(); 
    queue.add(root); 
    while(!queue.isEmpty()){ 
        double sum = 0; 
        int size = queue.size(); 
        for(int i = 0; i < size; i++){ 
            TreeNode node = queue.pop(); 
            sum += node.val; 
            if(node.left != null) 
                queue.add(node.left); 
            if(node.right != null) 
                queue.add(node.right); 
        } 
        result.add(sum/size); 
    } 
    return result; 
}

二叉树的中序遍历

给定一个二叉树的根节点 root ,返回 它的 中序 遍历 。

示例:

输入:root = [1,null,2,3]

输出:[1,3,2]

// 中序遍历:递归到最左子树,对元素做处理,再处理右子树。 
public List<Integer> inorderTraversal(TreeNode root) { 
    List<Integer> result = new ArrayList(); 
    search(root, result); 
    return result; 
} 

public void search(TreeNode node, List<Integer> result){ 
    if(node == null) return; 
    search(node.left, result); 
    result.add(node.val); 
    search(node.right, result); 
}

二叉搜索树的最小绝对差

给你一个二叉搜索树的根节点 root ,返回 树中任意两不同节点值之间的最小差值 。

差值是一个正数,其数值等于两值之差的绝对值。

示例:

输入:root = [4,2,6,1,3]

输出:1

// 中序遍历,得到升序数组,两两对比得到最小差值。 
public int getMinimumDifference(TreeNode root) { 
    List<Integer> list = new ArrayList(); 
    search(root, list); 
    int result = Integer.MAX_VALUE; 
    for(int i = 1; i < list.size(); i++){ 
        result = Math.min(result, list.get(i) - list.get(i - 1)); 
    } 
    return result; 
} 

public void search(TreeNode node, List<Integer> list){ 
    if(node == null) return; 
    search(node.left, list); 
    list.add(node.val); 
    search(node.right, list); 
}

将有序数组转换为二叉搜索树

给你一个整数数组 nums ,其中元素已经按 升序 排列,请你将其转换为一棵 高度平衡 二叉搜索树。

高度平衡 二叉树是一棵满足「每个节点的左右两个子树的高度差的绝对值不超过 1 」的二叉树。

示例:

输入:nums = [-10,-3,0,5,9]

输出:[0,-3,9,-10,null,5]

解释:[0,-10,5,null,-3,null,9] 也将被视为正确答案。

// 递归的2个关键: 
// 1. 递归和动态规划,本质都是将复杂问题拆解为子问题。 
// 2. 子问题的解如何得到复杂问题的解?之间的联系,就是递归的规律。 
// 3. 递归要有退出条件 
public TreeNode sortedArrayToBST(int[] nums) { 
    return transfor(nums, 0, nums.length - 1); 
} 

public TreeNode transfor(int[] nums, int left, int right){ 
    if(left > right) return null; 
    TreeNode node = new TreeNode(nums[(left + right) / 2]); 
    node.left = transfor(nums, left, (left + right) / 2 - 1); 
    node.right = transfor(nums, (left + right) / 2 + 1, right); 
    return node; 
}

验证二叉搜索树

验证二叉搜索树

给你一个二叉树的根节点 root ,判断其是否是一个有效的二叉搜索树。

二叉搜索树定义如下:

节点的左子树只包含 小于 当前节点的数。

节点的右子树只包含 大于 当前节点的数。

所有左子树和右子树自身必须也是二叉搜索树。

示例:

输入:root = [2,1,3]

输出:true

// 方法1:按中序遍历取出所有值,判断是否升序。 

// 方法2:中序遍历中,当前节点的值不能小于前一节点的值。 
Integer pre = null; 
public boolean isValidBST(TreeNode root) { 
    if(root == null) return true; 
    boolean left = isValidBST(root.left); 
    // 中序遍历形式下,当前节点的val应该是升序的,不能比前一个节点小 
    if(pre != null && root.val <= pre) 
        return false; 
    // 更新前一个节点的值 
    pre = root.val; 
    boolean right = isValidBST(root.right); 
    return left && right; 
}

DFS/回溯/递归

括号生成

数字 n 代表生成括号的对数,请你设计一个函数,用于能够生成所有可能的并且 有效的 括号组合。

示例:

输入:n = 3

输出:["((()))","(()())","(())()","()(())","()()()"]

// 深度遍历,每个节点的左右孩子是 ( 和 ) ,根据左右括号数量做递归退出和结果收集。 
public List<String> generateParenthesis(int n) { 
    List<String> result = new ArrayList(); 
    dfs("", 0, 0, result, n); 
    return result; 
} 

public void dfs(String s, int left, int right, List<String> result, int n){ 
    if(left > n || right > n || right > left) return; 
    if(left ==n && right == n){ 
        result.add(s); 
        return; 
    } 
    dfs(s + "(", left + 1, right, result, n); 
    dfs(s + ")", left, right + 1, result, n); 
}

全排列

给定一个不含重复数字的数组 nums ,返回其 所有可能的全排列 。你可以 按任意顺序 返回答案。

示例:

输入:nums = [1,2,3]

输出:[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]

// 以每个元素为开头,递归向下深度遍历。当遍历到的长度等于length时记录到结果集。 
// 维护used数组,没遍历就加到list,然后标记为已遍历。 
// 深度遍历到底后,回溯时记得从list删除,以及恢复为未遍历状态。 
public List<List<Integer>> permute(int[] nums) { 
    boolean[] uesed = new boolean[nums.length]; 
    List<List<Integer>> result = new ArrayList(); 
    dfs(nums, uesed, new ArrayList(), result); 
    return result; 
} 

public void dfs(int[] nums, boolean[] uesed, List<Integer> list,
    List<List<Integer>> result){ 
    if(list.size() == nums.length){ 
        result.add(new ArrayList(list)); 
        return; 
    } 
    for(int i = 0; i < nums.length; i++){ 
        if(uesed[i]) continue; 
        list.add(nums[i]); 
        uesed[i] = true; 
        dfs(nums, uesed, list, result); 
        list.remove(list.size() - 1); 
        uesed[i] = false; 
    } 
}

数学运算

二进制求和

给你两个二进制字符串 a 和 b ,以二进制字符串的形式返回它们的和。

示例:

输入:a = "11", b = "1"

输出:"100"

// 1. 定长遍历使用for,动态遍历使用while。 
// 2. 字符和'0'做减法,可以得到对应的值。 
// 3. StirngBuilder自带reverse()方法。 
public String addBinary(String a, String b) { 
    int size = Math.max(a.length(), b.length()); 
    int cal = 0; 
    StringBuilder sb = new StringBuilder(); 
    for(int i = 0; i < size; i++){ 
        int aa = a.length() > i ? a.charAt(a.length() - 1 - i) - '0' : 0; 
        int bb = b.length() > i ? b.charAt(b.length() - 1 - i) - '0' : 0; 
        if(aa + bb + cal > 1){ 
            sb.append(aa + bb + cal - 2); 
            cal = 1; 
        } else { 
            sb.append(aa + bb + cal); 
            cal = 0; 
        } 
    } 

    if(cal != 0) sb.append("1"); 
    return sb.reverse().toString(); 
}

颠倒二进制位

颠倒给定的 32 位无符号整数的二进制位。

示例:

输入:n = 00000010100101000001111010011100

输出:964176192 (00111001011110000010100101000000)

// 1. 如何获取二进制的位数?n & 1,n >> 1 
// 2. 如何计算2的幂次方? n << x 
public int reverseBits(int n) { 
    int result = 0; 
    int length = 31; 
    int tmp = 0; 
    while(length >= 0 && n != 0){ // n=0时没有必要继续计算 
        tmp = n & 1; // 取最低位值 
        n = n >> 1; // 右移一位 
        result += tmp << length; 
        length--; 
    } 
    return result; 
}

位1的个数

编写一个函数,输入是一个无符号整数(以二进制串的形式),返回其二进制表达式中数字位数为 '1' 的个数(也被称为汉明重量)。

// 方法1:逐个计算低位是否为1,注意:无符号右移>>>才不会超时。 
public int hammingWeight(int n) { 
    int count = 0; 
    while(n != 0){ 
        if((n & 1) == 1) count++; 
        n = n >>> 1; 
    } 
    return count; 
} 

// 方法2:n&n−1,会使n的最低位1被翻转为0,因此运算次数就等于1的个数。 
public int hammingWeight(int n) { 
    int count = 0; 
    while(n != 0){ 
        n = n & (n - 1); 
        count++; 
    } 
    return count; 
}

只出现一次的数字

给你一个 非空 整数数组 nums ,除了某个元素只出现一次以外,其余每个元素均出现两次。找出那个只出现了一次的元素。

示例:

输入:nums = [4,1,2,1,2]

输出:4

// 异或:不同为1,相同为0,0 ^ a = a,a ^ a = 0 
public int singleNumber(int[] nums) { 
    if(nums.length == 1) return nums[0]; 
    int result = 0; 
    for(int i = 0; i < nums.length; i++){ 
        result ^= nums[i]; 
    } 
    return result; 
}

加一

给定一个由 整数 组成的 非空 数组所表示的非负整数,在该数的基础上加一。

最高位数字存放在数组的首位, 数组中每个元素只存储单个数字。

你可以假设除了整数 0 之外,这个整数不会以零开头。

示例:

输入:digits = [1,2,3]

输出:[1,2,4]

// 如果是9,则继续往前遍历,否则直接+1,同时考虑全是9的case。 
public int[] plusOne(int[] digits) { 
    int index = digits.length - 1; 
    while(index >= 0){ 
        if(digits[index] == 9){ 
            digits[index--] = 0; 
        } else { 
            digits[index] += 1; 
            break; 
        } 
    } 
    if(index < 0){ 
        int[] result = new int[digits.length + 1]; 
        result[0] = 1; 
        return result; 
    } 
    return digits; 
}

x 的平方根

给你一个非负整数 x ,计算并返回 x 的 算术平方根 。

由于返回类型是整数,结果只保留 整数部分 ,小数部分将被 舍去 。

示例 1:

输入:x = 4

输出:2

示例 2:

输入:x = 8

输出:2

解释:8 的算术平方根是 2.82842..., 由于返回类型是整数,小数部分将被舍去。

// 二分查找,细节:由于mid*mid可能超int范围,改为 mid=x/mid 除法做判断 
public int mySqrt(int x) { 
    if(x == 0) return 0; 
    if(x == 1) return 1; 
    int left = 0; 
    int right = x; 
    int mid = 0; 
    while(left <= right){ 
        mid = (left + right) / 2; 
        if(mid == x / mid){ 
            return mid; 
        } else if(mid < x / mid){ 
            left = mid + 1; 
        } else { 
            right = mid - 1; 
        } 
    } 
    return left - 1; 
}

动态规划

爬楼梯

假设你正在爬楼梯。需要 n 阶你才能到达楼顶。

每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?

// dp[i] = dp[i - 1] + dp[i - 2] 
public int climbStairs(int n) { 
    if(n == 1) return 1; 
    if(n == 2) return 2; 
    int[] dp = new int[n]; 
    dp[0] = 1; 
    dp[1] = 2; 
    for(int i = 2; i < n; i++){ 
        dp[i] = dp[i - 1] + dp[i - 2]; 
    } 
    return dp[n - 1]; 
}

不同路径

一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为 “Start” )。

机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 “Finish” )。

问总共有多少条不同的路径?

示例:

输入:m = 3, n = 7

输出:28

// 标准动态规划:初始化边界 → 子问题解逐步推导出最终解 
public int uniquePaths(int m, int n) { 
    int[][] dp = new int[m][n]; 
    for(int i = 0; i < m; i++){ 
        dp[i][0] = 1; 
    } 
    for(int j = 0; j < n; j++){ 
        dp[0][j] = 1; 
    } 
    for(int k = 1; k < m; k++){ 
        for(int l = 1; l < n; l++){ 
            dp[k][l] = dp[k - 1][l] + dp[k][l - 1]; 
        } 
    } 
    return dp[m - 1][n - 1]; 
}

排序

冒泡排序

// 两两对比,如果前>后,则交换,这样1趟就可以把最大的数放到最后。持续n趟。 
public static void bubbleSort(int[] arr){ 
    for(int i = 0; i < arr.length - 1; i++){ 
        for(int j = 0; j < arr.length - i - 1; j++){ 
            if(arr[j] > arr[j + 1]) 
                swap(arr, j, j + 1); 
        } 
    } 
}

快速排序

// 1. 选取基准值,通过一次遍历,将所有大于基准值的放右边,小于的放左边,基准值放中间。 
// 2. 递归调用以上方法,确定基准值位置后,对[left, index - 1]和[index + 1, right]分别遍历。 
public static void main(String[] args) { 
    int[] arr = {1, 8, 2, 7, 6, 1, 5, 4}; 
    quickSort(arr, 0, arr.length - 1); 
    for(int i : arr){ 
        System.out.println(i); 
    } 
} 

public static void quickSort(int[] arr, int left, int right){ 
    if(left >= right) return; 
    int index = partition(arr, left, right); 
    quickSort(arr, left, index - 1); 
    quickSort(arr, index + 1, right); 
} 

public static int partition(int[] arr, int left, int right){ 
    int cur = arr[left]; 
    while(left < right){ 
        while(left < right && arr[right] >= cur) 
            right--; // 【关键】找到右边第一个小于基准值的数后,立即放到left。 
        arr[left] = arr[right]; 
        while(left < right && arr[left] <= cur) 
            left++; 
        arr[right] = arr[left]; 
    } 
    arr[left] = cur; 
    return left; 
}

最小K个数

设计一个算法,找出数组中最小的k个数。以任意顺序返回这k个数均可。

示例:

输入: arr = [1,3,5,7,2,4,6,8], k = 4

输出: [1,2,3,4]

// 快排思想:先partition,再递归左右区间继续partiotion。 
// 一次partition后,根据index和k的大小关系,只需选择左右的1个区间继续partition. 
// partition中,找到元素后要立即交换,这个细节不会! 
public int[] smallestK(int[] arr, int k) { 
    sort(arr, 0, arr.length - 1, k); 
    int[] result = new int[k]; 
    for(int i = 0; i < k; i++){ 
        result[i] = arr[i]; 
    } 
    return result; 
} 

// 递归partition 
public void sort(int[] arr, int left, int right, int k){ 
    if(left >= right) return; 
    int index = partition(arr, left, right); 
    if(index == k - 1){ 
        return; 
    } else if(index > k - 1){ 
        sort(arr, left, index - 1, k); 
    } else { 
        sort(arr, index + 1, right, k); 
    } 
} 

// 单次partition 
public int partition(int[] arr, int left, int right){ 
    int cur = arr[left]; 
    while(left < right){ 
        while(left < right && arr[right] >= cur) right--; 
        arr[left] = arr[right]; //【关键】找到右边第一个元素,立即交换。 
        while(left < right && arr[left] <= cur) left++; 
        arr[right] = arr[left]; 
    } 
    arr[left] = cur; 
    return left; 
}

其他

LRU 缓存

设计并实现一个满足 LRU (最近最少使用) 缓存 约束的数据结构。

实现 LRUCache 类:

  • LRUCache(int capacity) 以 正整数 作为容量 capacity 初始化 LRU 缓存

  • int get(int key) 如果关键字 key 存在于缓存中,则返回关键字的值,否则返回 -1 。

  • void put(int key, int value) 如果关键字 key 已经存在,则变更其数据值 value ;如果不存在,则向缓存中插入该组 key-value 。如果插入操作导致关键字数量超过 capacity ,则应该 逐出 最久未使用的关键字。

  • 函数 get 和 put 必须以 O(1) 的平均时间复杂度运行。

示例:

输入

["LRUCache", "put", "put", "get", "put", "get", "put", "get", "get", "get"]

[[2], [1, 1], [2, 2], [1], [3, 3], [2], [4, 4], [1], [3], [4]]

输出

[null, null, null, 1, null, -1, null, -1, 3, 4]

解释

LRUCache lRUCache = new LRUCache(2);

lRUCache.put(1, 1); // 缓存是 {1=1}

lRUCache.put(2, 2); // 缓存是 {1=1, 2=2}

lRUCache.get(1); // 返回 1

lRUCache.put(3, 3); // 该操作会使得关键字 2 作废,缓存是 {1=1, 3=3}

lRUCache.get(2); // 返回 -1 (未找到)

lRUCache.put(4, 4); // 该操作会使得关键字 1 作废,缓存是 {4=4, 3=3}

lRUCache.get(1); // 返回 -1 (未找到)

lRUCache.get(3); // 返回 3

lRUCache.get(4); // 返回 4

// 1. LinkedHashMap内部是HashMap+双向链表,可实现O(1)get和put。 
// 2. get时,如果存在元素,先remobve,再重新put,这样key会放到链表尾部。 
// 3. put时,如果满了,要删除头节点 map.keySet().iterator.nexe() 
class LRUCache { 
    private int cap; // 容量 
    Map<Integer,Integer> cache = new LinkedHashMap<>(); 
    public LRUCache(int capacity) { 
        cap = capacity; 
    } 
    public int get(int key) { 
        // 先remove再put到尾部 
        if(cache.containsKey(key)){ 
            Integer data = cache.get(key); 
            cache.remove(key); 
            cache.put(key,data); 
            return data; 
        } 
        return -1; 
    } 

    public void put(int key, int value) { 
        if(cache.containsKey(key)){ 
            // 有则删除 
            cache.remove(key); 
        } 
        if(cap == cache.size()){ 
            // 扩容时删除头节点 
            cache.remove(cache.keySet().iterator().next()); 
        } 
        cache.put(key,value); // 放到尾部 
    } 
}

十、中间件

ES

ES相比于MYSQL的优势?

  • 基于分词后的全文检索:例如select * from test where name like ‘%张三%’,对于mysql来说,因为索引失效,会进行全表检索;对es而言分词后,每个字都可以利用FST高速找到倒排索引的位置,并迅速获取文档id列表,大大的提升了性能,减少了磁盘IO。

  • 分布式的实时分析搜索引擎,海量数据下近实时秒级响应。

  • 在架构上,es天然就是分布式的,可以很容易的横向扩容,mysql不行。

如何将 MySQL 数据同步到 ES ? 为了便于管理后台的查询,我们将订单数据冗余存储在Elasticsearch中,那么,如何在MySQL的订单数据变更后,同步到ES中呢? 这里要考虑的是数据同步的时效性和一致性、对业务代码侵入小、不影响服务本身的性能等。 MQ方案: ES更新服务作为消费者,接收订单变更MQ消息后对ES进行更新

Binlog方案: ES更新服务借助canal等开源项目,把自己伪装成MySQL的从节点,接收Binlog并解析得到实时的数据变更信息,然后根据这个变更信息去更新ES。

其中BinLog方案比较通用,但实现起来也较为复杂,我们最终选用的是MQ方案。 因为ES数据只在管理后台使用,对数据可靠性和同步实时性的要求不是特别高。 考虑到宕机和消息丢失等极端情况,在后台增加了按某些条件手动同步ES数据的功能来进行补偿。


如何保证ES和数据库的数据一致性?

  1. 双写:对数据库和ES进行双写,并且先操作本地数据库,后操作ES,两个操作放到一个事务中

  2. MQ异步消费:先操作数据库,然后异步通知ES去更新,可以借助本地消息表保证最终一致性。

  3. 监听binlog同步:业内比较流行的方案是基于binlog监听,ES一般对毫秒级的延迟是可以接受的,可以基于canal做数据同步。

netty

什么是netty?

netty是1个java网络通信框架,性能很好,基于事件驱动、异步的思想,我们熟知的Dubbo、Rocketmq、Hadoop等都使用它作为底层的通信组件。


netty的优点?

  • API使用简单,学习成本低。

  • 性能高,对比其他主流的NIO框架,Netty的性能最优。

  • Dubbo、Elasticsearch都采用了Netty,可用性得到验证。


netty的使用场景?

分布式系统中,各个节点之间需要远程服务调用,高性能的RPC框架必不可少。Netty作为高性能的网络通信框架,往往作为基础的通信组件被这些RPC框架使用。比如:阿里分布式服务框架Dubbo的RPC框架使用Netty作为基础通信组件,用于实现各进程节点之间的内部通信。


netty的原理?

1. 基于NIO实现IO多路复用,利用一个线程可以并发处理多个io请求

NIO原理:

  • 服务器为每个客户端分配单独的Channel和Buffer,数据通过通道 Channel 传输的,往Channel中读写数据需要先经过缓冲区Buffer。

  • 将每个客户端对应的Channel的IO事件注册到多路复用器 Selector上,Selector通过轮询,就可以找到有IO活动的channel并进行处理,实现一个线程可以非阻塞地处理多个客户端的IO请求。

  • 这种IO处理模式也称为Reactor模式

netty参考了主从Reactors多线程模型:

  • MainReactor负责客户端的连接请求,并将请求转交给SubReactor

  • SubReactor负责相应通道的IO读写请求

  • 非IO请求(具体逻辑处理)的任务则会直接写入队列,等待worker threads进行处理

​netty的架构:

  • netty架构主要包含了两个事件循环组:BossGroup 和 WorkerGroup。

  • BoosGroup 用于专门创建连接,其中有多个事件循环线程,每个事件循环都监听对应通道的建立连接请求并进行处理。

  • WorkGroup 中也有多个事件循环线程,负责对应通道的IO事件。一个线程可以负责多个通道的IO,实现了IO多路复用。

2. 使用零拷贝技术减少数据在内存中的拷贝次数

​磁盘中的数据发送到网络的过程:

  1. 磁盘数据先拷贝到内核缓冲区

  2. 再拷贝到应用程序内存

  3. 再拷贝到Socket缓冲区

  4. 最后再发向网络

数据在内存中拷贝了两次,一次是内核缓冲区到用户程序内存,另一次是应用程序内存到Socket缓冲区。零拷贝技术,可以将内核缓冲区、应用程序内存、Socket缓冲区建立了地址映射,无需拷贝,大幅提升IO性能。

管理

什么是技术管理?

  • 定方向:技术规划

    • 明确团队定位:业务研发、中台研发

    • 自上而下拆解

  • 带团队:管人

    • 选|育|用|留 + 能力|意愿

  • 拿结果:理事

    • 项目管理:明确目标-验收指标-确定资源-实现路径-过程管控-总结复盘

    • 流程制度建设

    • 工程师文化建设:敬畏、热爱

研发团队如何保障交付质量?

高频强化质量意识:

明确流程/制度:技术方案评审、CR、线上变更审批及验收、值班表

流程自查

复盘

工具:静态代码扫描findbugs、单元测试springtest和junit

认知

如何有效沟通?

1. 少玩套路,真诚一些。

2.要意识到, 信息传递必然存在折损,保持理解一致。

3. 增加沟通频次,减少误解。

4. 使用金字塔结构进行表达。

如何建立信任?

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值