基础知识
1: spring
1、Spring是什么?
Spring是一个轻量级的IoC和AOP容器框架。是为Java应用程序提供基础性服务的一套框架,目的是用于简化企业应用程序的开发,它使得开发者只需要关心业务需求。主要包括以下七个模块:
Spring Context:提供框架式的Bean访问方式,以及企业级功能(JNDI、定时任务等);
Spring Core:核心类库,所有功能都依赖于该类库,提供IOC和DI服务;
Spring AOP:AOP服务;
Spring Web:提供了基本的面向Web的综合特性,提供对常见框架如Struts2的支持,Spring能够管理这些框架,将Spring的资源注入给框架,也能在这些框架的前后插入拦截器;
Spring MVC:提供面向Web应用的Model-View-Controller,即MVC实现。
Spring DAO:对JDBC的抽象封装,简化了数据访问异常的处理,并能统一管理JDBC事务;
Spring ORM:对现有的ORM框架的支持;

2、Spring 的优点?
(1)spring属于低侵入式设计,代码的污染极低;
(2)spring的DI机制将对象之间的依赖关系交由框架处理,减低组件的耦合性;
(3)Spring提供了AOP技术,支持将一些通用任务,如安全、事务、日志、权限等进行集中式管理,从而提供更好的复用。
(4)spring对于主流的应用框架提供了集成支持。
3、Spring的IoC理解:

4、Spring的AOP理解

5、Spring AOP里面的几个名词的概念:


6、Spring通知(Advice)有哪些类型?

7、Spring容器的启动流程:

8、BeanFactory和ApplicationContext有什么区别?

9、Spring Bean的生命周期?


10、 Spring中bean的作用域:

11、Spring框架中的Bean是线程安全的么?如果线程不安全,那么如何处理?

12、Spring基于xml注入bean的几种方式:
- set()方法注入;
- 构造器注入:①通过index设置参数的位置;②通过type设置参数类型;
- 静态工厂注入;
- 实例工厂;
详细内容请参考这篇文章:Spring中bean的注入方式
13、Spring如何解决循环依赖问题:





14、Spring的自动装配:

15、Spring事务的实现方式和实现原理:


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

17、Spring框架中有哪些不同类型的事件?

2: JVM
1: JVM的划分区域


2、说一下新生代、老年代、永久代


3: 简述内存分配与回收策略

4: 强、软、弱、虚引用的区分?

5.垃圾回收机制






6.Minor GC和Full GC触发条件

7: Minor GC 和 Full GC 有什么不一样吗

8:简述GC中Stop the world(STW),并说出安全点

9: G1和CMS的比较

10: CMS垃圾回收器存在的问题及解决方案

11: JVM里的有几种classloader;


12: 双亲委派模型

13: 谈谈双亲委派模型的"破坏"

14: JDBC和双亲委派模型关系分析

15.JVM锁优化(偏向、轻量级锁)和锁膨胀过程(高频考点)




16.相关JVM的调优参数有哪些?在工作中怎么调优的?


17: JVM性能监控有哪些?

18: 一个4核8G的服务器,初步设置JVM参数,如何分配?(高频考点)

19: ClassLoader如何工作的?

20: 对象的创建过程是什么样的?

21: 内存泄漏和泄漏的原因?

22: OOM说一下?怎么排查?哪些会导致OOM?(高频考点)

23: JVM虚拟机老年代什么情况下会发生gc,给你一个场景,一台4核8G的服务器,每隔两个小时就要出现一次老年代gc,现在有日志,怎么分析是哪里出了问题?

3: 多线程
1: 线程池的原理,为什么要创建线程池?创建线程池的方式;
答:
①降低资源的消耗,通过重复利用已经创建的线程降低线程创建和销毁造成的消耗。
②提高相应速度,当任务到达的时候,任务可以不需要等到线程创建就能立刻执行。
③提高线程的可管理性,线程是稀缺资源,使用线程池可以统一的分配、调优和监控。
继承Thread类创建线程类
实现Runnable接口
通过Callable和Future创建线程

ThreadPoolExecutor执行execute方法分下面 4 种情况:
- 1、如果当前运行的线程少于corePoolSize,则创建新线程来执行任务(注意,执行这一步骤需要获取全局锁)。
- 2、如果运行的线程等于或多于corePoolSize,则将任务加入BlockingQueue。
- 3、如果无法将任务加入BlockingQueue(队列已满),则创建新的线程来处理任务(注意,执行这一步骤需要获取全局锁)。
- 4、如果创建新线程将使当前运行的线程超出maximumPoolSize,任务将被拒绝,并调用RejectedExecutionHandler.rejectedExecution()方法。
创建线程有三种方式:
- 继承 Thread 重写 run 方法
- 实现 Runnable 接口
- 实现 Callable 接口 (有返回值)
线程有哪些状态?
- NEW(初始),新建状态,线程被创建出来,但尚未启动时的线程状态;
- RUNNABLE(就绪状态),表示可以运行的线程状态,它可能正在运行,或者是在排队等待操作系统给它分配 CPU 资源;
- BLOCKED(阻塞),阻塞等待锁的线程状态,表示处于阻塞状态的线程正在等待监视器锁,比如等待执行 synchronized 代码块或者使用 synchronized 标记的方法;
- WAITING(等待),等待状态,一个处于等待状态的线程正在等待另一个线程执行某个特定的动作,比如,一个线程调用了 Object.wait()方法,那它就在等待另一个线程调用 Object.notify() 或 Object.notifyAll()方法;
- TIMED_WAITING(超时等待),计时等待状态,和等待状态(WAITING)类似,它只是多了超时时间,比如调用了有超时时间设置的方法 Object.wait(long timeout)和 Thread.join(long timeout)等这些方法时,它才会进入此状态;
- TERMINATED,终止状态,表示线程已经执行完成。
![]()
线程池的状态有那些?
- running:这是最正常的状态,接受新的任务,处理等待队列中的任务。
- shutdown:不接受新的任务提交,但是会继续处理等待队列中的任务。
- stop:不接受新的任务提交,不再处理等待队列中的任务,中断正在执行任务的线程。
- tidying:所有的任务都销毁了,workcount 为 0,线程池的状态在转换 tidying 状态时,会执行钩子方法 terminated()。
线程池中 sumbit() 和 execute() 方法有什么区别?
- execute(): 只能执行 Runable 类型的任务。
- submit() 可以执行 Runable 和Callable类型的任务。
Callable 类型的任务可以获取执行的返回值,而 Runnable 执行无返回值。
2: 线程的生命周期,什么时候会出现僵死进程;


3; 什么时候会出现僵死进程
一个进程创建了子进程,父子进程异步执行。当子进程死亡时,操作系统会释放子进程占用的资源,但仍保留进程号、状态等信息,直到父进程调用wait或waitpid方法,这些信息才会被清除。若子进程先于父进程死亡,但父进程不调用wait或waitpid,则子进程的信息会一直保留,一直占用着进程号,这就是僵死进程。若父进程先于子进程死亡,子进程的父进程由init代替,当子进程死亡时init会负责释放子进程的信息,不会出现僵死进程。
4: 说说线程安全问题,什么实现线程安全,如何实现线程安全;
我们知道操作系统中线程程的调度是抢占式执行的, 宏观上上的感知是随机的, 这就导致了多线程在进行线程调度时线程的执行顺序是不确定的, 因此多线程情况下的代码的执行顺序可能就会有无数种, 我们需要保证这无数种线程调度顺序的情况下, 代码的执行结果都是正确的, 只要有一种情况下, 代码的结果没有达到预期, 就认为线程是不安全的, 对于多线程并发时会使程序出现BUG的代码称作线程不安全的代码, 这就是线程安全问题.
在Java中,通过如下方法实现线程安全:
- 使用线程同步
- 同步代码块
- 同步方法
- 锁机制
- 使用Volatile关键字
- 使用Atomic变量
- 使用final关键字
5: 创建线程池有哪几个核心参数? 如何合理配置线程池的大小?


6: volatile、ThreadLocal的使用场景和原理;


7; ThreadLocal什么时候会出现OOM的情况?为什么?


synchronized、volatile区别、synchronized锁粒度、模拟死锁场景、原子性与可见性;


https://blog.csdn.net/a158123/article/details/78616562
8: 进程与线程的区别

9: 守护线程是什么?

10: 在java中创建线程有哪些方式

11: runnable 和 callable 两个接口创建线程有什么不同呢?

12: 线程中的 wait 和 sleep方法有什么不同呢

13: synchronized 锁升级

14: wait和sleep和notify和notityAll区别(同步调度方法)

15: 线程同步和线程互斥的区别

16: 什么是CAS操作,缺点是什么

17: synchronized与Lock两者区别


18: 常见线程池有哪几种

19: HashMap线程安全问题

20: String不可变原因

21: 同步、异步、阻塞、非阻塞
同步,一个任务的完成之前不能做其他操作,必须等待(等于在打电话)
异步,一个任务的完成之前,可以进行其他操作(等于在聊QQ)
阻塞,是相对于CPU来说的, 挂起当前线程,不能做其他操作只能等待
非阻塞,,无须挂起当前线程,可以去执行其他操作
22: BIO、NIO、AIO
BIO:同步并阻塞,服务器实现一个连接一个线程,即客户端有连接请求时服务器端就需要启动一
个线程进行处理,没处理完之前此线程不能做其他操作。
NIO:同步非阻塞,服务器实现一个连接一个线程,即客户端发送的连接请求都会注册到多路复用
器上,多路复用器轮询到连接有I/O请求时才启动一个线程进行处理。NIO方式适用于连接数目多
且连接比较短(轻操作)的架构,比如聊天服务器。
AIO:异步非阻塞,服务器实现模式为一个有效请求一个线程,客户端的I/O请求都是由操作系统
先完成了再通知服务器应用去启动线程进行处理,AIO方式使用于连接数目多且连接比较长(重操
作)的架构,比如相册服务器。
23:浅拷贝与深拷贝

4: MYSQL
其他
1、三大范式
- 第一范式(1NF):要求数据库表的每一列都是不可分割的原子数据项,即列中不可再分,强调列的原子性。
- 第二范式(2NF):在满足1NF的基础上,要求每个表必须有一个数据项作为主键,其他数据项与主键一一对应,即要求数据库表中的每个非主键列必须完全依赖于主键,不能只依赖于主键的一部分(主要针对复合主键而言)。
- 第三范式(3NF):在满足2NF的基础上,要求任何非主属性不依赖于其他非主属性,即非主键列之间没有传递依赖关系。
事务
1、事务4大特性
事务4大特性:原子性、一致性、隔离性、持久性
原⼦性: 事务是最⼩的执⾏单位,不允许分割。事务的原⼦性确保动作要么全部完成,要么全不执行
一致性: 执⾏事务前后,数据保持⼀致,多个事务对同⼀个数据读取的结果是相同的;
隔离性: 并发访问数据库时,⼀个⽤户的事务不被其他事务所⼲扰,各并发事务之间数据库是独⽴的;
持久性: ⼀个事务被提交之后。它对数据库中数据的改变是持久的,即使数据库发⽣故障也不应该对其有任何影响。
实现保证:
MySQL的存储引擎InnoDB使用重做日志保证一致性与持久性,回滚日志保证原子性,使用各种锁来保证隔离性。
2、事务隔离级别
读未提交:最低的隔离级别,允许读取尚未提交的数据变更,可能会导致脏读、幻读或不可重复读。
读已提交:允许读取并发事务已经提交的数据,可以阻⽌脏读,但是幻读或不可重复读仍有可能发⽣。
可重复读:同⼀字段的多次读取结果都是⼀致的,除⾮数据是被本身事务⾃⼰所修改,可以阻⽌脏读和不可重复读,会有幻读。
串行化:最⾼的隔离级别,完全服从ACID的隔离级别。所有的事务依次逐个执⾏,这样事务之间就完全不可能产⽣⼲扰。
|
隔离级别 |
并发问题 |
|
读未提交 |
可能会导致脏读、幻读或不可重复读 |
|
读已提交 |
可能会导致幻读或不可重复读 |
|
可重复读 |
可能会导致幻读 |
|
可串行化 |
不会产⽣⼲扰 |
3、默认隔离级别-RR
默认隔离级别:可重复读;
同⼀字段的多次读取结果都是⼀致的,除⾮数据是被本身事务⾃⼰所修改;
可重复读是有可能出现幻读的,如果要保证绝对的安全只能把隔离级别设置成SERIALIZABLE;这样所有事务都只能顺序执行,自然不会因为并发有什么影响了,但是性能会下降许多。
第二种方式,使用MVCC解决快照读幻读问题(如简单select),读取的不是最新的数据。维护一个字段作为version,这样可以控制到每次只能有一个人更新一个版本。
select id from table_xx where id = ? and version = Vupdate id from table_xx where id = ? and version = V+1
第三种方式,如果需要读最新的数据,可以通过GapLock+Next-KeyLock可以解决当前读幻读问题,
select id from table_xx where id > 100 for update;select id from table_xx where id > 100 lock in share mode;
4、RR和RC使用场景
事务隔离级别RC(read commit)和RR(repeatable read)两种事务隔离级别基于多版本并发控制MVCC(multi-version concurrency control)来实现。
|
RC |
RR |
|
|
实现 |
多条查询语句会创建多个不同的ReadView |
仅需要一个版本的ReadView |
|
粒度 |
语句级读一致性 |
事务级读一致性 |
|
准确性 |
每次语句执行时间点的数据 |
第一条语句执行时间点的数据 |
5、行锁,表锁,意向锁
InnoDB⽀持⾏级锁(row-level locking)和表级锁,默认为⾏级锁
InnoDB按照不同的分类的锁:
共享/排它锁(Shared and Exclusive Locks):行级别锁,
意向锁(Intention Locks),表级别锁
间隙锁(Gap Locks),锁定一个区间
记录锁(Record Locks),锁定一个行记录
表级锁:(串行化)
Mysql中锁定 粒度最大的一种锁,对当前操作的整张表加锁,实现简单 ,资源消耗也比较少,加锁快,不会出现死锁 。其锁定粒度最大,触发锁冲突的概率最高,并发度最低,MyISAM和 InnoDB引擎都支持表级锁。
行级锁:(RR、RC)
Mysql中锁定 粒度最小 的一种锁,只针对当前操作的行进行加锁。 行级锁能大大减少数据库操作的冲突。其加锁粒度最小,并发度高,但加锁的开销也最大,加锁慢,会出现死锁。 InnoDB支持的行级锁,包括如下几种:
记录锁(Record Lock): 对索引项加锁,锁定符合条件的行。其他事务不能修改和删除加锁项;
间隙锁(Gap Lock): 对索引项之间的“间隙”加锁,锁定记录的范围,不包含索引项本身,其他事务不能在锁范围内插入数据。
Next-key Lock: 锁定索引项本身和索引范围。即Record Lock和Gap Lock的结合。可解决幻读问题。
InnoDB 支持多粒度锁(multiple granularity locking),它允许行级锁与表级锁共存,而意向锁就是其中的一种表锁。
共享锁( shared lock, S )锁允许持有锁读取行的事务。加锁时将自己和子节点全加S锁,父节点直到表头全加IS锁;当事务对数据加上读锁后,其他事务只能对该数据加读锁,不能加写锁。
排他锁( exclusive lock, X )锁允许持有锁修改行的事务。 加锁时将自己和子节点全加X锁,父节点直到表头全加IX锁
主要是防止其它事务和当前加锁事务锁定同一对象。同一对象主要有两层含义:
当排他锁加在表上,则其它事务无法对该表进行 insert、update、delete、alter、drop 等更新操作;
当排他锁加在表的行上,则其它事务无法对该行进行 insert、update、delete、alter、drop 等更新操作。
意向锁:
由于表锁和行锁虽然锁定范围不同,但是会相互冲突。当你要加表锁时,势必要先遍历该表的所有记录,判断是否有排他锁,这种遍历检查的方式显然是一种低效的方式,为了快速的判断表中是否存在行锁,MySql 引入了意向锁,来检测表锁和行锁的冲突。
意向锁,Intention Lock,它是一种表锁,用来标识事务打算在表中的行上获取什么类型的锁。 不同的事务可以在同一张表上获取不同种类的意向锁,但是第一个获取表上意向排他锁(IX)的事务会阻止其它事务获取该表上的任何 S锁 或 X锁。反之,第一个获得表上意向共享锁(IS)的事务可防止其它事务获取该表上的任何 X锁。
意向锁通常有两种类型:
意向共享锁(IS),表示事务打算对表中的行设置共享锁。
意向排他锁(IX),表示事务打算对表中的行设置排他锁。
|
互斥性 |
共享锁(S) |
排它锁(X) |
意向共享锁IS |
意向排他锁IX |
|
共享锁(S) |
✅ |
❌ |
✅ |
❌ |
|
排它锁(X) |
❌ |
❌ |
❌ |
❌ |
|
意向共享锁IS |
✅ |
❌ |
✅ |
✅ |
|
意向排他锁IX |
❌ |
❌ |
✅ |
✅ |
6、MVCC多版本并发控制
MVCC是一种多版本并发控制机制,通过事务的可见性看到自己预期的数据,能降低其系统开销.(RC和RR级别工作)
InnoDB的MVCC,是通过在每行记录后面保存系统版本号(可以理解为事务的ID),每开始一个新的事务,系统版本号就会自动递增,事务开始时刻的系统版本号会作为事务的ID。这样可以确保事务读取的行,要么是在事务开始前已经存在的,要么是事务自身插入或者修改过的,防止幻读的产生。
1.MVCC手段只适用于Msyql隔离级别中的读已提交(Read committed)和可重复读(Repeatable Read).
2.Read uncimmitted由于存在脏读,即能读到未提交事务的数据行,所以不适用MVCC.
3.简单的select快照度不会加锁,删改及select for update等需要当前读的场景会加锁
原因是MVCC的创建版本和删除版本只要在事务提交后才会产生。客观上,mysql使用的是乐观锁的一整实现方式,就是每行都有版本号,保存时根据版本号决定是否成功。Innodb的MVCC使用到的快照存储在Undo日志中,该日志通过回滚指针把一个数据行所有快照连接起来。
版本链
在InnoDB引擎表中,它的聚簇索引记录中有两个必要的隐藏列:
trx_id
这个id用来存储的每次对某条聚簇索引记录进行修改的时候的事务id。
roll_pointer
每次对哪条聚簇索引记录有修改的时候,都会把老版本写入undo日志中。这个roll_pointer就是存了一个指针,它指向这条聚簇索引记录的上一个版本的位置,通过它来获得上一个版本的记录信息。(注意插入操作的undo日志没有这个属性,因为它没有老版本)
每次修改都会在版本链中记录。SELECT可以去版本链中拿记录,这就实现了读-写,写-读的并发执行,提升了系统的性能。
7、快照读和当前读
- RC隔离级别下,是每个快照读都会生成并获取最新的Read View;
- 而在RR隔离级别下,则是同一个事务中的第一个快照读才会创建Read View, 之后的快照读获取的都是同一个Read View,之后的查询就不会重复生成了,所以一个事务的查询结果每次都是一样的。
8.快照读解决幻读问题
- 快照读:通过MVCC来进行控制的,不用加锁。按照MVCC中规定的“语法”进行增删改查等操作,以避免幻读。
- 当前读:通过next-key锁(行锁+gap锁)来解决问题的。
索引
1、Innodb和Myisam引擎
Myisam:支持表锁,适合读密集的场景,不支持外键,不支持事务,索引与数据在不同的文件
Innodb:支持行、表锁,默认为行锁,适合并发场景,支持外键,支持事务,索引与数据同一文件
2、哈希索引
哈希索引用索引列的值计算该值的hashCode,然后在hashCode相应的位置存执该值所在行数据的物理位置,因为使用散列算法,因此访问速度非常快,但是一个值只能对应一个hashCode,而且是散列的分布方式,因此哈希索引不支持范围查找和排序的功能
3、B+树索引
优点:
B+树的磁盘读写代价低,更少的查询次数,查询效率更加稳定,有利于对数据库的扫描
B+树是B树的升级版,B+树只有叶节点存放数据,其余节点用来索引。索引节点可以全部加入内存,增加查询效率,叶子节点可以做双向链表,从而提高范围查找的效率,增加的索引的范围
在大规模数据存储的时候,红黑树往往出现由于树的深度过大而造成磁盘IO读写过于频繁,进而导致效率低下的情况。所以,只要我们通过某种较好的树结构减少树的结构尽量减少树的高度,B树与B+树可以有多个子女,从几十到上千,可以降低树的高度。
磁盘预读原理:将一个节点的大小设为等于一个页,这样每个节点只需要一次I/O就可以完全载入。为了达到这个目的,在实际实现B-Tree还需要使用如下技巧:每次新建节点时,直接申请一个页的空间,这样就保证一个节点物理上也存储在一个页里,加之计算机存储分配都是按页对齐的,就实现了一个node只需一次I/O。
4、创建索引
CREATE [UNIQUE | FULLTEXT] INDEX 索引名 ON 表名(字段名) [USING 索引方法];说明:UNIQUE:可选。表示索引为唯一性索引。FULLTEXT:可选。表示索引为全文索引。INDEX和KEY:用于指定字段为索引,两者选择其中之一就可以了,作用是一样的。索引名:可选。给创建的索引取一个新名称。字段名1:指定索引对应的字段的名称,该字段必须是前面定义好的字段。注:索引方法默认使用B+TREE。
5、聚簇索引和非聚簇索引
聚簇索引:将数据存储与索引放到了一块,索引结构的叶子节点保存了行数据(主键索引)
非聚簇索引:将数据与索引分开存储,索引结构的叶子节点指向了数据对应的位置(辅助索引)
聚簇索引的叶子节点就是数据节点,而非聚簇索引的叶子节点仍然是索引节点,只不过有指向对应数据块的指针。
6、最左前缀问题
最左前缀原则主要使用在联合索引中,联合索引的B+Tree是按照第一个关键字进行索引排列的。
联合索引的底层是一颗B+树,只不过联合索引的B+树节点中存储的是键值。由于构建一棵B+树只能根据一个值来确定索引关系,所以数据库依赖联合索引最左的字段来构建。
采用>、<等进行匹配都会导致后面的列无法走索引,因为通过以上方式匹配到的数据是不可知的。
SQL查询
1、SQL语句的执行过程
查询语句:
select * from student A where A.age='18' and A.name='张三';

结合上面的说明,我们分析下这个语句的执行流程:
①通过客户端/服务器通信协议与 MySQL 建立连接。并查询是否有权限
②Mysql8.0之前开看是否开启缓存,开启了 Query Cache 且命中完全相同的 SQL 语句,则将查询结果直接返回给客户端;
③由解析器进行语法语义解析,并生成解析树。如查询是select、表名tb_student、条件是id='1'
④查询优化器生成执行计划。根据索引看看是否可以优化
⑤查询执行引擎执行 SQL 语句,根据存储引擎类型,得到查询结果。若开启了 Query Cache,则缓存,否则直接返回。
2、回表查询和覆盖索引
普通索引(唯一索引+联合索引+全文索引)需要扫描两遍索引树
(1)先通过普通索引定位到主键值id=5;
(2)在通过聚集索引定位到行记录;
这就是所谓的回表查询,先定位主键值,再定位行记录,它的性能较扫一遍索引树更低。
覆盖索引:主键索引==聚簇索引==覆盖索引
如果where条件的列和返回的数据在一个索引中,那么不需要回查表,那么就叫覆盖索引。
实现覆盖索引:常见的方法是,将被查询的字段,建立到联合索引里去。
3、Explain及优化
参考:https://www.jianshu.com/p/8fab76bbf448
mysql> explain select * from staff;+----+-------------+-------+------+---------------+------+---------+------+------+-------+| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |+----+-------------+-------+------+---------------+------+---------+------+------+-------+| 1 | SIMPLE | staff | ALL | NULL | 索引 | NULL | NULL | 2 | NULL |+----+-------------+-------+------+---------------+------+---------+------+------+-------+1 row in set
索引优化:
①最左前缀索引:like只用于'string%',语句中的=和in会动态调整顺序
②唯一索引:唯一键区分度在0.1以上
③无法使用索引:!= 、is null 、 or、>< 、(5.7以后根据数量自动判定)in 、not in
④联合索引:避免select * ,查询列使用覆盖索引
SELECT uid From user Where gid = 2 order by ctime asc limit 10ALTER TABLE user add index idx_gid_ctime_uid(gid,ctime,uid) #创建联合覆盖索引,避免回表查询
语句优化:
①char固定长度查询效率高,varchar第一个字节记录数据长度
②应该针对Explain中Rows增加索引
③group/order by字段均会涉及索引
④Limit中分页查询会随着start值增大而变缓慢,通过子查询+表连接解决
select * from mytbl order by id limit 100000,10 改进后的SQL语句如下:select * from mytbl where id >= ( select id from mytbl order by id limit 100000,1 ) limit 10select * from mytbl inner ori join (select id from mytbl order by id limit 100000,10) as tmp on tmp.id=ori.id;
⑤count会进行全表扫描,如果估算可以使用explain
⑥delete删除表时会增加大量undo和redo日志, 确定删除可使用trancate
表结构优化:
①单库不超过200张表
②单表不超过500w数据
③单表不超过40列
④单表索引不超过5个
数据库范式 :
①第一范式(1NF)列不可分割
②第二范式(2NF)属性完全依赖于主键 [ 消除部分子函数依赖 ]
③第三范式(3NF)属性不依赖于其它非主属性 [ 消除传递依赖 ]
配置优化:
配置连接数、禁用Swap、增加内存、升级SSD硬盘
4、JOIN查询

left join(左联接) 返回包括左表中的所有记录和右表中关联字段相等的记录
right join(右联接) 返回包括右表中的所有记录和左表中关联字段相等的记录
inner join(等值连接) 只返回两个表中关联字段相等的行
集群
1、主从复制过程
MySQl主从复制:
- 原理:将主服务器的binlog日志复制到从服务器上执行一遍,达到主从数据的一致状态。
- 过程:从库开启一个I/O线程,向主库请求Binlog日志。主节点开启一个binlog dump线程,检查自己的二进制日志,并发送给从节点;从库将接收到的数据保存到中继日志(Relay log)中,另外开启一个SQL线程,把Relay中的操作在自身机器上执行一遍
- 优点:
- 作为备用数据库,并且不影响业务
- 可做读写分离,一个写库,一个或多个读库,在不同的服务器上,充分发挥服务器和数据库的性能,但要保证数据的一致性
binlog记录格式:statement、row、mixed
基于语句statement的复制、基于行row的复制、基于语句和行(mix)的复制。其中基于row的复制方式更能保证主从库数据的一致性,但日志量较大,在设置时考虑磁盘的空间问题
2、数据一致性问题
"主从复制有延时",这个延时期间读取从库,可能读到不一致的数据。
缓存记录写key法:
在cache里记录哪些记录发生过的写请求,来路由读主库还是读从库
异步复制:
在异步复制中,主库执行完操作后,写入binlog日志后,就返回客户端,这一动作就结束了,并不会验证从库有没有收到,完不完整,所以这样可能会造成数据的不一致。
半同步复制:
当主库每提交一个事务后,不会立即返回,而是等待其中一个从库接收到Binlog并成功写入Relay-log中才返回客户端,通过一份在主库的Binlog,另一份在其中一个从库的Relay-log,可以保证了数据的安全性和一致性。
全同步复制:
指当主库执行完一个事务,所有的从库都执行了该事务才返回给客户端。因为需要等待所有从库执行完该事务才能返回,所以全同步复制的性能必然会收到严重的影响。
3、集群架构
https://blog.csdn.net/meser88/article/details/133523855
MM双主方案


MMM (Multi-Master Replication Manager)方案(单主)

MySQL 高可用方案之 MMM(Multi-Master Replication Manager)是一种常用的解决方案,用于实现 MySQL 数据库的高可用性和负载均衡。虽然是双主架构,但是业务上同一时间只允许一 个节点进行写入操作。
MHA (Master High Availability)架构(单主)

它的设计目标是确保在主数据库发生故障时,能够快速自动地将备库(Slave)提升为新的主库,以保证系统的连续性和可用性。
Keepalived + VIP + MySQL 主从/双主
当写节点 Master db1 出现故障时,由 MMM Monitor 或 Keepalived 触发切换脚本,将 VIP 漂移到可用的 Master db2 上。当出现网络抖动或网络分区时,MMM Monitor 会误判,严重时来回切换写 VIP 导致集群双写,当数据复制延迟时,应用程序会出现数据错乱或数据冲突的故障。有效避免单点失效的架构就是采用共享存储,单点故障切换可以通过分布式哨兵系统监控。
- MHA+Arksentinel: 这是一种更加强大的集群架构方案,结合了MHA和Arksentinel两种工具的优势。Arksentinel是一种高可用性监控工具,可以实时监控数据库的状态,并及时发现异常情况。将MHA与Arksentinel结合使用,可以实现更加可靠的数据库集群管理,提高系统的稳定性和可用性。

架构选型:MMM 集群 -> MHA集群 -> MHA+Arksentinel。

4、故障转移和恢复
转移方式及恢复方法
1. 虚拟IP或DNS服务 (Keepalived +VIP/DNS 和 MMM 架构)
问题:在虚拟 IP 运维过程中,刷新ARP过程中有时会出现一个 VIP 绑定在多台服务器同时提供连接的问题。这也是为什么要避免使用 Keepalived+VIP 和 MMM 架构的原因之一,因为它处理不了这类问题而导致集群多点写入。
2. 提升备库为主库(MHA、QMHA)
尝试将原 Master 设置 read_only 为 on,避免集群多点写入。借助 binlog server 保留 Master 的 Binlog;当出现数据延迟时,再提升 Slave 为新 Master 之前需要进行数据补齐,否则会丢失数据。
面试题
分库分表
垂直分库分表:


水平分库分表:


水平拆分:一个表放到多个库,分担高并发,加快查询速度
- id保证业务在关联多张表时可以在同一库上操作
- range方便扩容和数据统计
- hash可以使得数据更加平均
分库分表带来的问题
任何事情都有两面性,分库分表也不例外,如果采用分库分表,会引入新的的问题
1、分布式事务问题
使用分布式事务中间件解决,具体是通过最终一致性还是强一致性分布式事务,看业务需求,这里就不多说。
2、跨节点关联查询 Join 问题
切分之前,我们可以通过Join来完成。而切分之后,数据可能分布在不同的节点上,此时Join带来的问题就比较麻烦了,考虑到性能,尽量避免使用Join查询。
解决这个问题的一些方法:
全局表
全局表,也可看做是 "数据字典表",就是系统中所有模块都可能依赖的一些表,为了避免跨库Join查询,可以将 这类表在每个数据库中都保存一份。这些数据通常
很少会进行修改,所以也不担心一致性的问题。
字段冗余
利用空间换时间,为了性能而避免join查询。例:订单表保存userId时候,也将userName冗余保存一份,这样查询订单详情时就不需要再去查询"买家user表"了。
数据组装
在系统层面,分两次查询。第一次查询的结果集中找出关联数据id,然后根据id发起第二次请求得到关联数据。最后将获得到的数据进行字段拼装。
3、跨节点分页、排序、函数问题
跨节点多库进行查询时,会出现Limit分页、Order by排序等问题。分页需要按照指定字段进行排序,当排序字段就是分片字段时,通过分片规则就比较容易定位到指定的分片;
当排序字段非分片字段时,就变得比较复杂了。需要先在不同的分片节点中将数据进行排序并返回,然后将不同分片返回的结果集进行汇总和再次排序,最终返回给用户。
4、全局主键避重问题
如果都用主键自增肯定不合理,如果用UUID那么无法做到根据主键排序,所以我们可以考虑通过雪花ID来作为数据库的主键,
有关雪花ID可以参考我之前写的博客:静态内部类单例模式实现雪花算法
5、数据迁移问题
采用双写的方式,修改代码,所有涉及到分库分表的表的增、删、改的代码,都要对新库进行增删改。同时,再有一个数据抽取服务,不断地从老库抽数据,往新库写,
边写边按时间比较数据是不是最新的。
系统性能的评估及扩容
和家亲目前有1亿用户:场景 10万写并发,100万读并发,60亿数据量
设计时考虑极限情况,32库*32表~64个表,一共1000 ~ 2000张表
- 支持3万的写并发,配合MQ实现每秒10万的写入速度
- 读写分离6万读并发,配合分布式缓存每秒100读并发
- 2000张表每张300万,可以最多写入60亿的数据
- 32张用户表,支撑亿级用户,后续最多也就扩容一次
动态扩容的步骤
- 推荐是 32 库 * 32 表,对于我们公司来说,可能几年都够了。
- 配置路由的规则,uid % 32 = 库,uid / 32 % 32 = 表
- 扩容的时候,申请增加更多的数据库服务器,呈倍数扩容
- 由 DBA 负责将原先数据库服务器的库,迁移到新的数据库服务器上去
- 修改一下配置,重新发布系统,上线,原先的路由规则变都不用变
- 直接可以基于 n 倍的数据库服务器的资源,继续进行线上系统的提供服务。
如何生成自增的id主键
- 使用redis可以
- 并发不高可以单独起一个服务,生成自增id
- 设置数据库step自增步长可以支撑水平伸缩
- UUID适合文件名、编号,但是不适合做主键
- snowflake雪花算法,综合了41时间(ms)、10机器、12序列号(ms内自增)
其中机器预留的10bit可以根据自己的业务场景配置
线上故障及优化
更新失败 | 主从同步延时
以前线上确实处理过因为主从同步延时问题而导致的线上的 bug,属于小型的生产事故。
是这个么场景。有个同学是这样写代码逻辑的。先插入一条数据,再把它查出来,然后更新这条数据。在生产环境高峰期,写并发达到了 2000/s,这个时候,主从复制延时大概是在小几十毫秒。线上会发现,每天总有那么一些数据,我们期望更新一些重要的数据状态,但在高峰期时候却没更新。用户跟客服反馈,而客服就会反馈给我们。
我们通过 MySQL 命令:
show slave status
查看 Seconds_Behind_Master ,可以看到从库复制主库的数据落后了几 ms。
一般来说,如果主从延迟较为严重,有以下解决方案:
- 分库,拆分为多个主库,每个主库的写并发就减少了几倍,主从延迟可以忽略不计。
- 重写代码,写代码的同学,要慎重,插入数据时立马查询可能查不到。
- 如果确实是存在必须先插入,立马要求就查询到,然后立马就要反过来执行一些操作,对这个查询设置直连主库或者延迟查询。主从复制延迟一般不会超过50ms
应用崩溃 | 分库分表优化
我们有一个线上通行记录的表,由于数据量过大,进行了分库分表,当时分库分表初期经常产生一些问题。典型的就是通行记录查询中使用了深分页,通过一些工具如MAT、Jstack追踪到是由于sharding-jdbc内部引用造成的。
通行记录数据被存放在两个库中。如果没有提供切分键,查询语句就会被分发到所有的数据库中,比如查询语句是 limit 10、offset 1000,最终结果只需要返回 10 条记录,但是数据库中间件要完成这种计算,则需要 (1000+10)*2=2020 条记录来完成这个计算过程。如果 offset 的值过大,使用的内存就会暴涨。虽然 sharding-jdbc 使用归并算法进行了一些优化,但在实际场景中,深分页仍然引起了内存和性能问题。
这种在中间节点进行归并聚合的操作,在分布式框架中非常常见。比如在 ElasticSearch 中,就存在相似的数据获取逻辑,不加限制的深分页,同样会造成 ES 的内存问题。
业界解决方案:
方法一:全局视野法
(1)将order by time offset X limit Y,改写成order by time offset 0 limit X+Y
(2)服务层对得到的N*(X+Y)条数据进行内存排序,内存排序后再取偏移量X后的Y条记录
这种方法随着翻页的进行,性能越来越低。
方法二:业务折衷法-禁止跳页查询
(1)用正常的方法取得第一页数据,并得到第一页记录的time_max
(2)每次翻页,将order by time offset X limit Y,改写成order by time where time>$time_max limit Y
以保证每次只返回一页数据,性能为常量。
方法三:业务折衷法-允许模糊数据
(1)将order by time offset X limit Y,改写成order by time offset X/N limit Y/N
方法四:二次查询法
(1)将order by time offset X limit Y,改写成order by time offset X/N limit Y
(2)找到最小值time_min
(3)between二次查询,order by time between 𝑡𝑖𝑚𝑒𝑚𝑖𝑛𝑎𝑛𝑑time_i_max
(4)设置虚拟time_min,找到time_min在各个分库的offset,从而得到time_min在全局的offset
(5)得到了time_min在全局的offset,自然得到了全局的offset X limit Y
查询异常 | SQL 调优
分库分表前,有一段用用户名来查询某个用户的 SQL 语句:
select * from user where name = "xxx" and community="other";
为了达到动态拼接的效果,这句 SQL 语句被一位同事进行了如下修改。他的本意是,当 name 或者 community 传入为空的时候,动态去掉这些查询条件。这种写法,在 MyBaits 的配置文件中,也非常常见。大多数情况下,这种写法是没有问题的,因为结果集合是可以控制的。但随着系统的运行,用户表的记录越来越多,当传入的 name 和 community 全部为空时,悲剧的事情发生了:
select * from user where 1=1
数据库中的所有记录,都会被查询出来,载入到 JVM 的内存中。由于数据库记录实在太多,直接把内存给撑爆了。由于这种原因引起的内存溢出,发生的频率非常高,比如导入Excel文件时。
通常的解决方式是强行加入分页功能,或者对一些必填的参数进行校验

Controller 层
现在很多项目都采用前后端分离架构,所以 Controller 层的方法,一般使用 @ResponseBody 注解,把查询的结果,解析成 JSON 数据返回。这在数据集非常大的情况下,会占用很多内存资源。假如结果集在解析成 JSON 之前,占用的内存是 10MB,那么在解析过程中,有可能会使用 20M 或者更多的内存
因此,保持结果集的精简,是非常有必要的,这也是 DTO(Data Transfer Object)存在的必要。互联网环境不怕小结果集的高并发请求,却非常恐惧大结果集的耗时请求,这是其中一方面的原因。
Service 层
Service 层用于处理具体的业务,更加贴合业务的功能需求。一个 Service,可能会被多个 Controller 层所使用,也可能会使用多个 dao 结构的查询结果进行计算、拼装。
int getUserSize() { List<User> users = dao.getAllUser(); return null == users ? 0 : users.size();}
代码review中发现了定时炸弹,这种在数据量达到一定程度后,才会暴露问题。
ORM 层
比如使用Mybatis时,有一个批量导入服务,在 MyBatis 执行批量插入的时候,竟然产生了内存溢出,按道理这种插入操作是不会引起额外内存占用的,最后通过源码追踪到了问题。
这是因为 MyBatis 循环处理 batch 的时候,操作对象是数组,而我们在接口定义的时候,使用的是 List;当传入一个非常大的 List 时,它需要调用 List 的 toArray 方法将列表转换成数组(浅拷贝);在最后的拼装阶段,又使用了 StringBuilder 来拼接最终的 SQL,所以实际使用的内存要比 List 多很多。
事实证明,不论是插入操作还是查询动作,只要涉及的数据集非常大,就容易出现问题。由于项目中众多框架的引入,想要分析这些具体的内存占用,就变得非常困难。所以保持小批量操作和结果集的干净,是一个非常好的习惯。
5: Redis
Redis数据结构面试题
什么是布隆过滤器?
布隆过滤器是一个叫“布隆”的人提出的,它本身是一个很长的二进制向量,既然是二进制的向量,那么显而易见的,存放的不是0,就是1。布隆过滤器是一种由位数组和多个哈希函数组成概率数据结构,返回两种结果可能存在和一定不存在。布隆过滤器里的一个元素由多个状态值共同确定。位数组存储状态值,哈希函数计算状态值的位置。
优点:由于存放的不是完整的数据,所以占用的内存很少,而且新增,查询速度够快;
缺点: 随着数据的增加,误判率随之增加;无法做到删除数据;只能判断数据是否一定不存在,而无法判断数据是否一定存在。
Redis中String常用命令及应用场景。
常用命令: set,get,decr,incr,mget 等。
含义:String数据结构是简单的Key-Value类型,value不仅可以是String,也可以是数字。
数据结构:内部结构实现上类似于 Java 的 ArrayList,采用预分配冗余空间的方式来减少内存的频繁分配,如图所示:

len 是当前字符串实际长度,capacity 是为字符串分配的可用空间,当字符串长度小于 1M 时,扩容都是加倍现有的空间,如果超过 1M,扩容时一次只会多扩 1M 的空间。字符串最大长度为 512M。
应用场景: 常规计数,微博数,粉丝数等。
Redis中Hash常用命令及应用场景。
常用命令: hget,hset,hgetall 等。
含义:Redis中的哈希结构就如同Java中的map一样,Hash是一个string类型的field和value的映射表,hash特别适合用于存储对象。
数据结构:Redis Hash通过分桶的方式解决 hash 冲突。它是无序字典。内部实现结构是同样的数组 + 链表二维结构。第一维 hash 的数组位置碰撞时,就会将碰撞的元素使用链表串接起来。第一维是数组,第二维是链表。数组中存储的是第二维链表的第一个元素的指针。

应用场景:存储用户信息,商品信息等等。例如修真院的首页的职业信息,只是简单的信息集合,我们可以直接将它储存到Redis中,在读取的过程中就不用序列化对象,直接操作。
Redis中List常用命令及应用场景。
常用命令: lpush,rpush,lpop,rpop,lrange等
含义:list就是链表,Redis list的实现为一个双向链表,即可以支持反向查找和遍历,更方便操作,不过带来了部分额外的内存开销。
数据结构:Redis 的列表相当于 Java 语言中的 LinkedList,注意它是链表而不是数组。这意味着 list 的插入和删除操作非常快,时间复杂度为 O(1),但是索引定位很慢,时间复杂度为 O(n)。
list的特点是:
1)有序
2)可以重复
3)右边进左边出或者左边进右边出,则列表可以充当队列
4)左边进左边出或者右边进右边出,则列表可以充当栈
应用场景:微博的关注列表,粉丝列表,最新消息排行等功能
Redis中Set常用命令及应用场景。
常用命令:sadd,spop,smembers,sunion 等
含义:set对外提供的功能与list类似,是一个列表的功能,特殊之处在于set是可以自动排重的。 并且set提供了判断某个成员是否在一个set集合内的重要接口,这个也是list所不能提供的。
数据结构:set和字典非常类似,其内部实现就是上述的hashTable的特殊实现,与字典不同的地方有两点:
1)只关注key值,所有的value都是NULL。
2)在新增数据时会进行去重。

场景应用:
1.共同好友、二度好友
2.利用唯一性,可以统计访问网站的所有独立 IP
3.好友推荐的时候,根据 tag 求交集,大于某个 threshold 就可以推荐
Redis中Sorted Set常用命令及应用场景。
常用命令: zadd,zrange,zrem,zcard等
含义:和set相比,sorted set增加了一个权重参数score,使得集合中的元素能够按score进行有序排列。
数据结构:zset是Redis非常有特色的数据结构,它是基于Set并提供排序的有序集合。其中最为重要的特点就是支持通过score的权重来指定权重。一些排行榜、延迟任务比如指定1小时后执行, 就是使用这个数据结构实现的。

应用场景:在直播系统中,实时排行信息包含直播间在线用户列表,各种礼物排行榜,弹幕消息(可以理解为按消息维度的消息排行榜)等信息,适合使用Redis中的SortedSet结构进行存储。
Redis的Hash冲突怎么办?
Redis 作为一个K-V的内存数据库,它使用用一张全局的哈希来保存所有的键值对。这张哈希表,有多个哈希桶组成,哈希桶中的entry元素保存了key和value指针,其中*key指向了实际的键,*value指向了实际的值。

所谓的哈希冲突通是指过不同的key,计算出一样的哈希值,导致落在同一个哈希桶中。
Redis为了解决哈希冲突,采用了链式哈希。链式哈希是指同一个哈希桶中,多个元素用一个链表来保存,它们之间依次用指针连接。

因为哈希冲突链上的元素只能通过指针逐一查找再操作,所以当往哈希表插入数据很多,冲突也会越多,冲突链表就会越长,那查询效率就会降低了。为了保持高效,Redis 会对哈希表做rehash操作,也就是增加哈希桶,减少冲突。为了rehash更高效,Redis还默认使用了两个全局哈希表,一个用于当前使用,称为主哈希表,一个用于扩容,称为备用哈希表。
跳表(ZSET底层结构):
跳跃表主要由以下几部分构成:
表头(head):负责维护跳跃表的节点指针。
跳跃表节点:保存着元素值,以及多个层。
层:保存着指向其他元素的指针。高层的指针越过的元素数量大于等于低层的指针,为了提高查找的效率,程序总是从高层先开始访问,然后随着元素值范围的缩小,慢慢降低层次。
表尾:全部由 NULL 组成,表示跳跃表的末尾。
上面所说的 zset 需要支持随机的插入和删除,所以它不宜使用数组来实现,关于排序问题,我们也很容易就想到红黑树/ 平衡树这样的树形结构,那为什么 Redis 不使用这样一些结构呢?主要两方面进行考虑:
实现方面:在复杂度与红黑树相同的情况下,跳跃表实现起来更简单,看起来也更加直观,比如平衡树的插入和删除操作可能引发子树的调整,逻辑复杂,而 skip List 的插入和删除只需要修改相邻节点的指针,操作简单又快速。
性能方面:在高并发的情况下,树形结构需要执行一些类似于 rebalance 这样的可能涉及整棵树的操作,相对来说跳跃表的变化只涉及局部。
一致性hash:
一致性hash是一个2^32位的圆环,节点按照ip 哈希后,映射到环上。数据按照 key哈希后,也映射到环上。然后每个数据沿顺时针方向,在环上找最近的节点。

1.2 雪崩
当一个节点挂掉 或被删除时,此节点的数据全部交给下一个节点。如上图中 node1 挂掉后,node1的数据会全部转移给 node2。 如此以来,node2 的负载会骤增,很容易崩掉。如果 node2崩掉了,那么 原 node1 和 node2的数据都会转给 node4。node4的压力更大,更容易挂掉,以此类推。
1.3 数据倾斜
hash算法没法保证节点均匀的分布在环上,因此当节点个数很少时,可能出现下图的情况。这样大部分数据会放在node1上,只有少部分数据会放在node2上。

1.4 虚拟节点
每个节点对应 K 个虚拟节点,来解决上面的 “雪崩”和“数据倾斜”问题。比如下图,每个节点对应两个虚拟节点,将虚拟节点 hash映射到环上。由于节点变多了,节点的分布会变得均匀一些,解决了数据倾斜的问题。

说说Redis哈希槽的概念?
Redis集群没有使用一致性hash,而是引入了哈希槽的概念,Redis集群有16384个哈希槽,每个key通过CRC16校验后对16384取模来决定放置哪个槽,集群的每个节点负责一部分hash槽。
redis 集群划分了 2^14(16384)个槽,每个节点对应固定范围的槽区。比如下图中,master1 负责0-2000的槽区,master2负责2000-8000的槽区....
ps:这里的master 和 slave 是集群的主备模式。数据仅在master节点内进行读写,当master挂了,slave中会选举出一个新的master节点。

初始时,槽区会被均匀的分给各master节点。
删除master节点时,先把其下面的slave节点摘掉,然后把槽区转给其他master节点,才能删除此节点。新增master节点也需要手动分配槽区,不然无法存储数据。
Redis网络模型
1、 Redis中的单线程模型
Redis基于Reactor模式开发了自己的网络事件处理器,称之为文件事件处理器(File Event Hanlder)。文件事件处理器由Socket、IO多路复用程序、文件事件分派器(dispather),事件处理器(handler)四部分组成,文件事件处理器的模型如下所示:


2、网络IO模型基本概念


3、五种 IO 模型
阻塞IO模型\非阻塞IO模型\IO复用模型\信号驱动IO模型\异步IO模型






4: select\pool\epool区别

Redis可用性
1、redis持久化
持久化就是把内存中的数据持久化到本地磁盘,防止服务器宕机了内存数据丢失
Redis 提供两种持久化机制 RDB(默认) 和 AOF 机制,Redis4.0以后采用混合持久化,用 AOF 来保证数据不丢失,作为数据恢复的第一选择; 用 RDB 来做不同程度的冷备
RDB:是Redis DataBase缩写快照
RDB是Redis默认的持久化方式。按照一定的时间将内存的数据以快照的形式保存到硬盘中,对应产生的数据文件为dump.rdb。通过配置文件中的save参数来定义快照的周期。
优点:
1)只有一个文件 dump.rdb,方便持久化;
2)容灾性好,一个文件可以保存到安全的磁盘。
3)性能最大化,fork 子进程来进行持久化写操作,让主进程继续处理命令,只存在毫秒级不响应请求。
4)相对于数据集大时,比 AOF 的启动效率更高。
缺点:
数据安全性低,RDB 是间隔一段时间进行持久化,如果持久化之间 redis 发生故障,会发生数据丢失。
AOF:持久化
AOF持久化(即Append Only File持久化),则是将Redis执行的每次写命令记录到单独的日志文件中,当重启Redis会重新将持久化的日志中文件恢复数据。
优点:
1)数据安全,aof 持久化可以配置 appendfsync 属性,有 always,每进行一次 命令操作就记录到 aof 文件中一次。
2)通过 append 模式写文件,即使中途服务器宕机,可以通过 redis-check-aof 工具解决数据一致性问题。
缺点:
1)AOF 文件比 RDB 文件大,且恢复速度慢。
2)数据集大的时候,比 rdb 启动效率低。
2、redis事务
事务是一个单独的隔离操作:事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。事务是一个原子操作:事务中的命令要么全部被执行,要么全部都不执行。
Redis事务的概念
Redis 事务的本质是通过MULTI、EXEC、WATCH等一组命令的集合。事务支持一次执行多个命令,一个事务中所有命令都会被序列化。在事务执行过程,会按照顺序串行化执行队列中的命令,其他客户端提交的命令请求不会插入到事务执行命令序列中。总结说:redis事务就是一次性、顺序性、排他性的执行一个队列中的一系列命令。
Redis的事务总是具有ACID中的一致性和隔离性,其他特性是不支持的。当服务器运行在AOF持久化模式下,并且appendfsync选项的值为always时,事务也具有耐久性。
Redis事务功能是通过MULTI、EXEC、DISCARD和WATCH 四个原语实现的
事务命令:
MULTI:用于开启一个事务,它总是返回OK。MULTI执行之后,客户端可以继续向服务器发送任意多条命令,这些命令不会立即被执行,而是被放到一个队列中,当EXEC命令被调用时,所有队列中的命令才会被执行。
EXEC:执行所有事务块内的命令。返回事务块内所有命令的返回值,按命令执行的先后顺序排列。当操作被打断时,返回空值 nil 。
WATCH :是一个乐观锁,可以为 Redis 事务提供 check-and-set (CAS)行为。可以监控一个或多个键,一旦其中有一个键被修改(或删除),之后的事务就不会执行,监控一直持续到EXEC命令。(秒杀场景)
DISCARD:调用该命令,客户端可以清空事务队列,并放弃执行事务,且客户端会从事务状态中退出。
UNWATCH:命令可以取消watch对所有key的监控。
3、redis失效策略
内存淘汰策略
1)全局的键空间选择性移除
noeviction:当内存不足以容纳新写入数据时,新写入操作会报错。(字典库常用)
allkeys-lru:在键空间中,移除最近最少使用的key。(缓存常用)
allkeys-random:在键空间中,随机移除某个key。
2)设置过期时间的键空间选择性移除
volatile-lru:在设置了过期时间的键空间中,移除最近最少使用的key。
volatile-random:在设置了过期时间的键空间中,随机移除某个key。
volatile-ttl:在设置了过期时间的键空间中,有更早过期时间的key优先移除。
缓存失效策略
定时清除:针对每个设置过期时间的key都创建指定定时器
惰性清除:访问时判断,对内存不友好
定时扫描清除:定时100ms随机20个检查过期的字典,若存在25%以上则继续循环删除。
4、redis读写模式
CacheAside旁路缓存
写请求更新数据库后删除缓存数据。读请求不命中查询数据库,查询完成写入缓存


业务端处理所有数据访问细节,同时利用 Lazy 计算的思想,更新 DB 后,直接删除 cache 并通过 DB 更新,确保数据以 DB 结果为准,则可以大幅降低 cache 和 DB 中数据不一致的概率
如果没有专门的存储服务,同时是对数据一致性要求比较高的业务,或者是缓存数据更新比较复杂的业务,适合使用 Cache Aside 模式。如微博发展初期,不少业务采用这种模式
// 延迟双删,用以保证最终一致性,防止小概率旧数据读请求在第一次删除后更新数据库public void write(String key,Object data){ redis.delKey(key); db.updateData(data); Thread.sleep(1000); redis.delKey(key);}
高并发下保证绝对的一致,先删缓存再更新数据,需要用到内存队列做异步串行化。非高并发场景,先更新数据再删除缓存,延迟双删策略基本满足了
- 先更新db后删除redis:删除redis失败则出现问题
- 先删redis后更新db:删除redis瞬间,旧数据被回填redis
(延迟双删除策略):
- Redis的"延时双删"策略主要用于解决缓存和数据库间数据一致性问题。这里的“双删”是指在更新数据库数据后,先删除缓存,然后再更新数据库数据,并再次删除缓存。这样可以防止缓存中的数据变成旧数据。
Read/Write Though(读写穿透)
先查询缓存中数据是否存在,如果存在则直接返回,如果不存在,则由缓存组件负责从数据库中同步加载数据.

先查询要写入的数据在缓存中是否已经存在,如果已经存在,则更新缓存中的数据,并且由缓存组件同步更新到数据库中。

用户读操作较多.相较于Cache aside而言更适合缓存一致的场景。使用简单屏蔽了底层数据库的操作,只是操作缓存.
场景:
微博 Feed 的 Outbox Vector(即用户最新微博列表)就采用这种模式。一些粉丝较少且不活跃的用户发表微博后,Vector 服务会首先查询 Vector Cache,如果 cache 中没有该用户的 Outbox 记录,则不写该用户的 cache 数据,直接更新 DB 后就返回,只有 cache 中存在才会通过 CAS 指令进行更新。
Write Behind Caching(异步缓存写入)

比如对一些计数业务,一条 Feed 被点赞 1万 次,如果更新 1万 次 DB 代价很大,而合并成一次请求直接加 1万,则是一个非常轻量的操作。但这种模型有个显著的缺点,即数据的一致性变差,甚至在一些极端场景下可能会丢失数据。
5、多级缓存
浏览器本地内存缓存:专题活动,一旦上线,在活动期间是不会随意变更的。
浏览器本地磁盘缓存:Logo缓存,大图片懒加载
服务端本地内存缓存:由于没有持久化,重启时必定会被穿透
服务端网络内存缓存:Redis等,针对穿透的情况下可以继续分层,必须保证数据库不被压垮
为什么不是使用服务器本地磁盘做缓存?
当系统处理大量磁盘 IO 操作的时候,由于 CPU 和内存的速度远高于磁盘,可能导致 CPU 耗费太多时间等待磁盘返回处理的结果。对于这部分 CPU 在 IO 上的开销,我们称为 iowait
Redis七大经典问题
1、缓存雪崩
指缓存同一时间大面积的失效,所以,后面的请求都会落到数据库上,造成数据库短时间内承受大量请求而崩掉。
解决方案:
- Redis 高可用,主从+哨兵,Redis cluster,避免全盘崩溃
- 本地 ehcache 缓存 + hystrix 限流&降级,避免 MySQL 被打死
- 缓存数据的过期时间设置随机,防止同一时间大量数据过期现象发生。
- 逻辑上永不过期给每一个缓存数据增加相应的缓存标记,缓存标记失效则更新数据缓存
- 多级缓存,失效时通过二级更新一级,由第三方插件更新二级缓存。
2、缓存穿透
https://blog.csdn.net/lin777lin/article/details/105666839
缓存穿透是指缓存和数据库中都没有的数据,导致所有的请求都落到数据库上,造成数据库短时间内承受大量请求而崩掉。
解决方案:
1)接口层增加校验,如用户鉴权校验,id做基础校验,id<=0的直接拦截;
2)从缓存取不到的数据,在数据库中也没有取到,这时也可以将key-value对写为key-null,缓存有效时间可以设置短点,如30秒。这样可以防止攻击用户反复用同一个id暴力攻击;
3)采用布隆过滤器,将所有可能存在的数据哈希到一个足够大的 bitmap 中,一个一定不存在的数据会被这个 bitmap 拦截掉,从而避免了对底层存储系统的查询压力。(宁可错杀一千不可放过一人)
3、缓存击穿
这时由于并发用户特别多,同时读缓存没读到数据,又同时去数据库去取数据,引起数据库压力瞬间增大,造成过大压力。和缓存雪崩不同的是,缓存击穿指并发查同一条数据,缓存雪崩是不同数据都过期了,很多数据都查不到从而查数据库
解决方案:
1)设置热点数据永远不过期,异步线程处理。
2)加写回操作加互斥锁,查询失败默认值快速返回。
3)缓存预热
系统上线后,将相关可预期(例如排行榜)热点数据直接加载到缓存。
写一个缓存刷新页面,手动操作热点数据(例如广告推广)上下线。
4、数据不一致
在缓存机器的带宽被打满,或者机房网络出现波动时,缓存更新失败,新数据没有写入缓存,就会导致缓存和 DB 的数据不一致。缓存 rehash 时,某个缓存机器反复异常,多次上下线,更新请求多次 rehash。这样,一份数据存在多个节点,且每次 rehash 只更新某个节点,导致一些缓存节点产生脏数据。
- Cache 更新失败后,可以进行重试,则将重试失败的 key 写入mq,待缓存访问恢复后,将这些 key 从缓存删除。这些 key 在再次被查询时,重新从 DB 加载,从而保证数据的一致性
- 缓存时间适当调短,让缓存数据及早过期后,然后从 DB 重新加载,确保数据的最终一致性。
- 不采用 rehash 漂移策略,而采用缓存分层策略,尽量避免脏数据产生。
5、数据并发竞争
数据并发竞争在大流量系统也比较常见,比如车票系统,如果某个火车车次缓存信息过期,但仍然有大量用户在查询该车次信息。又比如微博系统中,如果某条微博正好被缓存淘汰,但这条微博仍然有大量的转发、评论、赞。上述情况都会造成并发竞争读取的问题。
- 加写回操作加互斥锁,查询失败默认值快速返回。
- 对缓存数据保持多个备份,减少并发竞争的概率
6、热点key问题
明星结婚、离婚、出轨这种特殊突发事件,比如奥运、春节这些重大活动或节日,还比如秒杀、双12、618 等线上促销活动,都很容易出现 Hot key 的情况。
如何提前发现HotKey?
- 对于重要节假日、线上促销活动这些提前已知的事情,可以提前评估出可能的热 key 来。
- 而对于突发事件,无法提前评估,可以通过 Spark,对应流任务进行实时分析,及时发现新发布的热点 key。而对于之前已发出的事情,逐步发酵成为热 key 的,则可以通过 Hadoop 对批处理任务离线计算,找出最近历史数据中的高频热 key。
解决方案:
- 这 n 个 key 分散存在多个缓存节点,然后 client 端请求时,随机访问其中某个后缀的 hotkey,这样就可以把热 key 的请求打散,避免一个缓存节点过载
- 缓存集群可以单节点进行主从复制和垂直扩容
- 利用应用内的前置缓存,但是需注意需要设置上限
- 延迟不敏感,定时刷新,实时感知用主动刷新
- 和缓存穿透一样,限制逃逸流量,单请求进行数据回源并刷新前置
- 无论如何设计,最后都要写一个兜底逻辑,千万级流量说来就来
7、BigKey问题
比如互联网系统中需要保存用户最新 1万 个粉丝的业务,比如一个用户个人信息缓存,包括基本资料、关系图谱计数、发 feed 统计等。微博的 feed 内容缓存也很容易出现,一般用户微博在 140 字以内,但很多用户也会发表 1千 字甚至更长的微博内容,这些长微博也就成了大 key
- 首先Redis底层数据结构里,根据Value的不同,会进行数据结构的重新选择
- 可以扩展新的数据结构,进行序列化构建,然后通过 restore 一次性写入
- 将大 key 分拆为多个 key,设置较长的过期时间
Redis分区容错
1、redis数据分区
Hash:(不稳定)
客户端分片:哈希+取余
节点伸缩:数据节点关系变化,导致数据迁移
迁移数量和添加节点数量有关:建议翻倍扩容
一个简单直观的想法是直接用Hash来计算,以Key做哈希后对节点数取模。可以看出,在key足够分散的情况下,均匀性可以获得,但一旦有节点加入或退出,所有的原有节点都会受到影响,稳定性无从谈起。
一致性Hash:(不均衡)
客户端分片:哈希+顺时针(优化取余)
节点伸缩:只影响邻近节点,但是还是有数据迁移
翻倍伸缩:保证最小迁移数据和负载均衡
一致性Hash可以很好的解决稳定问题,可以将所有的存储节点排列在收尾相接的Hash环上,每个key在计算Hash后会顺时针找到先遇到的一组存储节点存放。而当有节点加入或退出时,仅影响该节点在Hash环上顺时针相邻的后续节点,将数据从该节点接收或者给予。但这又带来均匀性的问题,即使可以将存储节点等距排列,也会在存储节点个数变化时带来数据的不均匀。
Codis的Hash槽
Codis 将所有的 key 默认划分为 1024 个槽位(slot),它首先对客户端传过来的 key 进行 crc32 运算计算 哈希值,再将 hash 后的整数值对 1024 这个整数进行取模得到一个余数,这个余数就是对应 key 的槽位。
RedisCluster
Redis-cluster把所有的物理节点映射到[0-16383]个slot上,对key采用crc16算法得到hash值后对16384取模,基本上采用平均分配和连续分配的方式。
2、主从模式=简单
主从模式最大的优点是部署简单,最少两个节点便可以构成主从模式,并且可以通过读写分离避免读和写同时不可用。不过,一旦 Master 节点出现故障,主从节点就无法自动切换,直接导致 SLA 下降。所以,主从模式一般适合业务发展初期,并发量低,运维成本低的情况

主从复制原理:
①通过从服务器发送到PSYNC命令给主服务器
②如果是首次连接,触发一次全量复制。此时主节点会启动一个后台线程,生成 RDB 快照文件
③主节点会将这个 RDB 发送给从节点,slave 会先写入本地磁盘,再从本地磁盘加载到内存中
④master会将此过程中的写命令写入缓存,从节点实时同步这些数据
⑤如果网络断开了连接,自动重连后主节点通过命令传播增量复制给从节点部分缺少的数据
缺点
所有的slave节点数据的复制和同步都由master节点来处理,会照成master节点压力太大,使用主从从结构来解决,redis4.0中引入psync2 解决了slave重启后仍然可以增量同步。
3、哨兵模式=读多
由一个或多个sentinel实例组成sentinel集群可以监视一个或多个主服务器和多个从服务器。哨兵模式适合读请求远多于写请求的业务场景,比如在秒杀系统中用来缓存活动信息。 如果写请求较多,当集群 Slave 节点数量多了后,Master 节点同步数据的压力会非常大。

当主服务器进入下线状态时,sentinel可以将该主服务器下的某一从服务器升级为主服务器继续提供服务,从而保证redis的高可用性。
检测主观下线状态
Sentinel每秒一次向所有与它建立了命令连接的实例(主服务器、从服务器和其他Sentinel)发送PING命 令
实例在down-after-milliseconds毫秒内返回无效回复Sentinel就会认为该实例主观下线(SDown)
检查客观下线状态
当一个Sentinel将一个主服务器判断为主观下线后 ,Sentinel会向监控这个主服务器的所有其他Sentinel发送查询主机状态的命令
如果达到Sentinel配置中的quorum数量的Sentinel实例都判断主服务器为主观下线,则该主服务器就会被判定为客观下线(ODown)。
选举Leader Sentinel
当一个主服务器被判定为客观下线后,监视这个主服务器的所有Sentinel会通过选举算法(raft),选出一个Leader Sentinel去执行**failover(故障转移)**操作。
Raft算法
Raft协议是用来解决分布式系统一致性问题的协议。 Raft协议描述的节点共有三种状态:Leader, Follower, Candidate。 Raft协议将时间切分为一个个的Term(任期),可以认为是一种“逻辑时间”。 选举流程:
①Raft采用心跳机制触发Leader选举系统启动后,全部节点初始化为Follower,term为0
②节点如果收到了RequestVote或者AppendEntries,就会保持自己的Follower身份
③节点如果一段时间内没收到AppendEntries消息,在该节点的超时时间内还没发现Leader,Follower就会转换成Candidate,自己开始竞选Leader。 一旦转化为Candidate,该节点立即开始下面几件事情:
--增加自己的term,启动一个新的定时器
--给自己投一票,向所有其他节点发送RequestVote,并等待其他节点的回复。
④如果在计时器超时前,节点收到多数节点的同意投票,就转换成Leader。同时通过 AppendEntries,向其他节点发送通知。
⑤每个节点在一个term内只能投一票,采取先到先得的策略,Candidate投自己, Follower会投给第一个收到RequestVote的节点。
⑥Raft协议的定时器采取随机超时时间(选举的关键),先转为Candidate的节点会先发起投票,从而获得多数票。
主服务器的选择
当选举出Leader Sentinel后,Leader Sentinel会根据以下规则去从服务器中选择出新的主服务器。
- 过滤掉主观、客观下线的节点
- 选择配置slave-priority最高的节点,如果有则返回没有就继续选择
- 选择出复制偏移量最大的系节点,因为复制偏移量越大则数据复制的越完整
- 选择run_id最小的节点,因为run_id越小说明重启次数越少
故障转移
当Leader Sentinel完成新的主服务器选择后,Leader Sentinel会对下线的主服务器执行故障转移操作,主要有三个步骤:
1、它会将失效 Master 的其中一个 Slave 升级为新的 Master , 并让失效 Master 的其他 Slave 改为复制新的 Master ;
2、当客户端试图连接失效的 Master 时,集群会向客户端返回新 Master 的地址,使得集群当前状态只有一个Master。
3、Master 和 Slave 服务器切换后, Master 的 redis.conf 、 Slave 的 redis.conf 和 sentinel.conf 的配置文件的内容都会发生相应的改变,即 Master 主服务器的 redis.conf配置文件中会多一行 replicaof 的配置, sentinel.conf 的监控目标会随之调换。
4、集群模式=写多
为了避免单一节点负载过高导致不稳定,集群模式采用一致性哈希算法或者哈希槽的方法将 Key 分布到各个节点上。其中,每个 Master 节点后跟若干个 Slave 节点,用于出现故障时做主备切换,客户端可以连接任意 Master 节点,集群内部会按照不同 key 将请求转发到不同的 Master 节点
集群模式是如何实现高可用的呢?集群内部节点之间会互相定时探测对方是否存活,如果多数节点判断某个节点挂了,则会将其踢出集群,然后从 Slave 节点中选举出一个节点替补挂掉的 Master 节点。整个原理基本和哨兵模式一致
虽然集群模式避免了 Master 单节点的问题,但集群内同步数据时会占用一定的带宽。所以,只有在写操作比较多的情况下人们才使用集群模式,其他大多数情况,使用哨兵模式都能满足需求
5、分布式锁
利用Watch实现Redis乐观锁
乐观锁基于CAS(Compare And Swap)比较并替换思想,不会产生锁等待而消耗资源,但是需要反复的重试,但也是因为重试的机制,能比较快的响应。因此我们可以利用redis来实现乐观锁(秒杀)。具体思路如下:
1、利用redis的watch功能,监控这个redisKey的状态值
2、获取redisKey的值,创建redis事务,给这个key的值+1
3、执行这个事务,如果key的值被修改过则回滚,key不加1
利用setnx防止库存超卖

分布式锁是控制分布式系统之间同步访问共享资源的一种方式。 利用Redis的单线程特性对共享资源进行串行化处理
// 获取锁推荐使用set的方式String result = jedis.set(lockKey, requestId, "NX", "EX", expireTime);String result = jedis.setnx(lockKey, requestId); //如线程死掉,其他线程无法获取到锁
// 释放锁,非原子操作,可能会释放其他线程刚加上的锁if (requestId.equals(jedis.get(lockKey))) { jedis.del(lockKey);}// 推荐使用redis+lua脚本String lua = "if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end";Object result = jedis.eval(lua, Collections.singletonList(lockKey),
分布式锁存在的问题:
- 客户端长时间阻塞导致锁失效问题
计算时间内异步启动另外一个线程去检查的问题,这个key是否超时,当锁超时时间快到期且逻辑未执行完,延长锁超时时间。
- **Redis服务器时钟漂移问题导致同时加锁
redis的过期时间是依赖系统时钟的,如果时钟漂移过大时 理论上是可能出现的 **会影响到过期时间的计算。 - 单点实例故障,锁未及时同步导致丢失RedLock算法
- 获取当前时间戳T0,配置时钟漂移误差T1
- 短时间内逐个获取全部N/2+1个锁,结束时间点T2
- 实际锁能使用的处理时长变为:TTL - (T2 - T0)- T1该方案通过多节点来防止Redis的单点故障,效果一般,也无法防止:
- 主从切换导致的两个客户端同时持有锁大部分情况下持续时间极短,而且使用Redlock在切换的瞬间获取到节点的锁,也存在问题。已经是极低概率的时间,无法避免。Redis分布式锁适合幂等性事务,如果一定要保证安全,应该使用Zookeeper或者DB,但是,性能会急剧下降。
与zookeeper分布式锁对比
- redis 分布式锁,其实需要自己不断去尝试获取锁,比较消耗性能。
- zk 分布式锁,注册个监听器即可,不需要不断主动尝试获取锁,ZK获取锁会按照加锁的顺序,所以是公平锁,性能和mysql差不多,和redis差别大
Redission生产环境的分布式锁
Redisson是基于NIO的Netty框架上的一个Java驻内存数据网格(In-Memory Data Grid)分布式锁开源组件。

但当业务必须要数据的强一致性,即不允许重复获得锁,比如金融场景(重复下单,重复转账),请不要使用redis分布式锁。可以使用CP模型实现,比如:zookeeper和etcd。
|
Redis |
zookeeper |
etcd |
|
|
一致性算法 |
无 |
paxos(ZAB) |
raft |
|
CAP |
AP |
CP |
CP |
|
高可用 |
主从集群 |
n+1 |
n+1 |
|
实现 |
setNX |
createNode |
restfulAPI |
6、redis心跳检测
在命令传播阶段,从服务器默认会以每秒一次的频率向主服务器发送ACK命令:
1、检测主从的连接状态 检测主从服务器的网络连接状态
lag的值应该在0或1之间跳动,如果超过1则说明主从之间的连接有 故障。
2、辅助实现min-slaves,Redis可以通过配置防止主服务器在不安全的情况下执行写命令
min-slaves-to-write 3 (min-replicas-to-write 3 )min-slaves-max-lag 10 (min-replicas-max-lag 10)
上面的配置表示:从服务器的数量少于3个,或者三个从服务器的延迟(lag)值都大于或等于10 秒时,主服务器将拒绝执行写命令。
3、检测命令丢失,增加重传机制
如果因为网络故障,主服务器传播给从服务器的写命令在半路丢失,那么当从服务器向主服务器发 送REPLCONF ACK命令时,主服务器将发觉从服务器当前的复制偏移量少于自己的复制偏移量, 然后主服务器就会根据从服务器提交的复制偏移量,在复制积压缓冲区里面找到从服务器缺少的数据,并将这些数据重新发送给从服务器。
Redis实战
1、Redis优化

读写方式
简单来说就是不用keys等,用range、contains之类。比如,用户粉丝数,大 V 的粉丝更是高达几千万甚至过亿,因此,获取粉丝列表只能部分获取。另外在判断某用户是否关注了另外一个用户时,也只需要关注列表上进行检查判断,然后返回 True/False 或 0/1 的方式更为高效。
KV size
如果单个业务的 KV size 过大,需要分拆成多个 KV 来缓存。拆分时应考虑访问频率
key 的数量
如果数据量巨大,则在缓存中尽可能只保留频繁访问的热数据,对于冷数据直接访问 DB。
读写峰值
如果小于 10万 级别,简单分拆到独立 Cache 池即可
如果达到 100万 级的QPS,则需要对 Cache 进行分层处理,可以同时使用 Local-Cache 配合远程 cache,甚至远程缓存内部继续分层叠加分池进行处理。(多级缓存)
命中率
缓存的命中率对整个服务体系的性能影响甚大。对于核心高并发访问的业务,需要预留足够的容量,确保核心业务缓存维持较高的命中率。比如微博中的 Feed Vector Cache(热点资讯),常年的命中率高达 99.5% 以上。为了持续保持缓存的命中率,缓存体系需要持续监控,及时进行故障处理或故障转移。同时在部分缓存节点异常、命中率下降时,故障转移方案,需要考虑是采用一致性 Hash 分布的访问漂移策略,还是采用数据多层备份策略。
过期策略
可以设置较短的过期时间,让冷 key 自动过期;也可以让 key 带上时间戳,同时设置较长的过期时间,比如很多业务系统内部有这样一些 key:key_20190801。
缓存穿透时间
平均缓存穿透加载时间在某些业务场景下也很重要,对于一些缓存穿透后,加载时间特别长或者需要复杂计算的数据,而且访问量还比较大的业务数据,要配置更多容量,维持更高的命中率,从而减少穿透到 DB 的概率,来确保整个系统的访问性能。
缓存可运维性
对于缓存的可运维性考虑,则需要考虑缓存体系的集群管理,如何进行一键扩缩容,如何进行缓存组件的升级和变更,如何快速发现并定位问题,如何持续监控报警,最好有一个完善的运维平台,将各种运维工具进行集成。
缓存安全性
对于缓存的安全性考虑,一方面可以限制来源 IP,只允许内网访问,同时加密鉴权访问。
2、Redis热升级
在 Redis 需要升级版本或修复 bug 时,如果直接重启变更,由于需要数据恢复,这个过程需要近 10 分钟的时间,时间过长,会严重影响系统的可用性。面对这种问题,可以对 Redis 扩展热升级功能,从而在毫秒级完成升级操作,完全不影响业务访问。
热升级方案如下,首先构建一个 Redis 壳程序,将 redisServer 的所有属性(包括redisDb、client等)保存为全局变量。然后将 Redis 的处理逻辑代码全部封装到动态连接库 so 文件中。Redis 第一次启动,从磁盘加载恢复数据,在后续升级时,通过指令,壳程序重新加载 Redis 新的 redis-4.so 到 redis-5.so 文件,即可完成功能升级,毫秒级完成 Redis 的版本升级。而且整个过程中,所有 Client 连接仍然保留,在升级成功后,原有 Client 可以继续进行读写操作,整个过程对业务完全透明。
6: 网络基础
TCP三次握手
三次握手过程:
客户端——发送带有SYN标志的数据包——服务端 一次握手 Client进入syn_sent状态
服务端——发送带有SYN/ACK标志的数据包——客户端 二次握手 服务端进入syn_rcvd
客户端——发送带有ACK标志的数据包——服务端 三次握手 连接就进入Established状态
为什么三次:
主要是为了建立可靠的通信信道,保证客户端与服务端同时具备发送、接收数据的能力
为什么两次不行?
1、防止已失效的请求报文又传送到了服务端,建立了多余的链接,浪费资源
2、 两次握手只能保证单向连接是畅通的。(为了实现可靠数据传输, TCP 协议的通信双方, 都必须维 护一个序列号, 以标识发送出去的数据包中, 哪些是已经被对方收到的。 三次握手的过程即是通信双方 相互告知序列号起始值, 并确认对方已经收到了序列号起始值的必经步骤;如果只是两次握手, 至多只有连接发起方的起始序列号能被确认, 另一方选择的序列号则得不到确认)
**TCP四次挥手过程 **
四次挥手过程:
客户端——发送带有FIN标志的数据包——服务端,关闭与服务端的连接 ,客户端进入FIN-WAIT-1状态
服务端收到这个 FIN,它发回⼀ 个 ACK,确认序号为收到的序号加1,服务端就进入了CLOSE-WAIT状态
服务端——发送⼀个FIN数据包——客户端,关闭与客户端的连接,客户端就进入FIN-WAIT-2状态
客户端收到这个 FIN,发回 ACK 报⽂确认,并将确认序号设置为收到序号加1,TIME-WAIT状态
为什么四次:
因为需要确保客户端与服务端的数据能够完成传输。
CLOSE-WAIT:
这种状态的含义其实是表示在等待关闭
TIME-WAIT:
为了解决网络的丢包和网络不稳定所带来的其他问题,确保连接方能在时间范围内,关闭自己的连接
如何查看TIME-WAIT状态的链接数量?
netstat -an |grep TIME_WAIT|wc -l 查看连接数等待time_wait状态连接数
为什么会TIME-WAIT过多?解决方法是怎样的?
可能原因: 高并发短连接的TCP服务器上,当服务器处理完请求后立刻按照主动正常关闭连接
解决:负载均衡服务器;Web服务器首先关闭来自负载均衡服务器的连接
1、OSI与TCP/IP 模型
OSI七层:物理层、数据链路层、网络层、传输层、会话层、表示层、应用层
TCP/IP五层:物理层、数据链路层、网络层、传输层、应用层
2、常见网络服务分层
应用层:HTTP、SMTP、DNS、FTP
传输层:TCP 、UDP
网络层:ICMP 、IP、路由器、防火墙
数据链路层:网卡、网桥、交换机
物理层:中继器、集线器
3、TCP与UDP区别及场景
|
类型 |
特点 |
性能 |
应用过场景 |
首部字节 |
|
|
TCP |
面向连接、可靠、字节流 |
传输效率慢、所需资源多 |
文件、邮件传输 |
20-60 |
|
|
UDP |
无连接、不可靠、数据报文段 |
传输效率快、所需资源少 |
语音、视频、直播 |
8个字节 |
基于TCP的协议:HTTP、FTP、SMTP
基于UDP的协议:RIP、DNS、SNMP
4、TCP滑动窗口,拥塞控制
TCP通过:应用数据分割、对数据包进行编号、校验和、流量控制、拥塞控制、超时重传等措施保证数据的可靠传输;
拥塞控制目的:为了防止过多的数据注入到网络中,避免网络中的路由器、链路过载
拥塞控制过程:TCP维护一个拥塞窗口,该窗口随着网络拥塞程度动态变化,通过慢开始、拥塞避免等算法减少网络拥塞的发生。
5、TCP粘包原因和解决方法
TCP粘包是指:发送方发送的若干包数据到接收方接收时粘成一包
发送方原因:
TCP默认使用Nagle算法(主要作用:减少网络中报文段的数量):
收集多个小分组,在一个确认到来时一起发送、导致发送方可能会出现粘包问题
接收方原因:
TCP将接收到的数据包保存在接收缓存里,如果TCP接收数据包到缓存的速度大于应用程序从缓存中读取数据包的速度,多个包就会被缓存,应用程序就有可能读取到多个首尾相接粘到一起的包。
解决粘包问题:
最本质原因在与接收对等方无法分辨消息与消息之间的边界在哪,通过使用某种方案给出边界,例如:
- 发送定长包。每个消息的大小都是一样的,接收方只要累计接收数据,直到数据等于一个定长的数值就将它作为一个消息。
- 包尾加上\r\n标记。FTP协议正是这么做的。但问题在于如果数据正文中也含有\r\n,则会误判为消息的边界。
- 包头加上包体长度。包头是定长的4个字节,说明了包体的长度。接收对等方先接收包体长度,依据包体长度来接收包体。
6、TCP、UDP报文格式
TCP报文格式:

源端口号和目的端口号:
用于寻找发端和收端应用进程。这两个值加上ip首部源端ip地址和目的端ip地址唯一确定一个tcp连接。
序号字段:
序号用来标识从T C P发端向T C P收端发送的数据字节流,它表示在这个报文段中的的第一个数据字节。如果将字节流看作在两个应用程序间的单向流动,则 T C P用序号对每个字节进行计数。序号是32 bit的无符号数,序号到达 2^32-1后又从0开始。
当建立一个新的连接时,SYN标志变1。序号字段包含由这个主机选择的该连接的初始序号ISN(Initial Sequence Number)。该主机要发送数据的第一个字节序号为这个ISN加1,因为SYN标志消耗了一个序号
确认序号:
既然每个传输的字节都被计数,确认序号包含发送确认的一端所期望收到的下一个序号。因此,确认序号应当是上次已成功收到数据字节序号加 1。只有ACK标志为 1时确认序号字段才有效。发送ACK无需任何代价,因为 32 bit的确认序号字段和A C K标志一样,总是T C P首部的一部分。因此,我们看到一旦一个连接建立起来,这个字段总是被设置, ACK标志也总是被设置为1。TCP为应用层提供全双工服务。这意味数据能在两个方向上独立地进行传输。因此,连接的每一端必须保持每个方向上的传输数据序号。
首都长度:
首部长度给出首部中 32 bit字的数目。需要这个值是因为任选字段的长度是可变的。这个字段占4 bit,因此T C P最多有6 0字节的首部。然而,没有任选字段,正常的长度是 2 0字节。
标志字段:在T C P首部中有 6个标志比特。它们中的多个可同时被设置为1.
URG紧急指针(u rgent pointer)有效
ACK确认序号有效。
PSH接收方应该尽快将这个报文段交给应用层。
RST重建连接。
SYN同步序号用来发起一个连接。这个标志和下一个标志将在第 1 8章介绍。
FIN发端完成发送任务。
窗口大小:
T C P的流量控制由连接的每一端通过声明的窗口大小来提供。窗口大小为字节数,起始于确认序号字段指明的值,这个值是接收端期望接收的字节。窗口大小是一个 16 bit字段,因而窗口大小最大为 65535字节。
检验和:
检验和覆盖了整个的 T C P报文段:T C P首部和T C P数据。这是一个强制性的字段,一定是由发端计算和存储,并由收端进行验证。
紧急指针:
只有当URG标志置1时紧急指针才有效。紧急指针是一个正的偏移量,和序号字段中的值相加表示紧急数据最后一个字节的序号。 T C P的紧急方式是发送端向另一端发送紧急数据的一种方式。
选项:
最常见的可选字段是最长报文大小,又称为 MSS (Maximum Segment Size)。每个连接方通常都在通信的第一个报文段(为建立连接而设置 S Y N标志的那个段)中指明这个选项。它指明本端所能接收的最大长度的报文段。
UDP报文格式:

端口号:
用来表示发送和接受进程。由于 I P层已经把I P数据报分配给T C P或U D P(根据I P首部中协议字段值),因此T C P端口号由T C P来查看,而 U D P端口号由UDP来查看。T C P端口号与UDP端口号是相互独立的。
长度:
UDP长度字段指的是UDP首部和UDP数据的字节长度。该字段的最小值为 8字节(发送一份0字节的UDP数据报是 O K)。
检验和:
UDP检验和是一个端到端的检验和。它由发送端计算,然后由接收端验证。其目的是为了发现UDP首部和数据在发送端到接收端之间发生的任何改动。
IP报文格式:普通的IP首部长为20个字节,除非含有可选项字段。
4位版本:
目前协议版本号是4,因此IP有时也称作IPV4.
4位首部长度:
首部长度指的是首部占32bit字的数目,包括任何选项。由于它是一个4比特字段,因此首部长度最长为60个字节。
服务类型(TOS):
服务类型字段包括一个3bit的优先权字段(现在已经被忽略),4bit的TOS子字段和1bit未用位必须置0。4bit的TOS分别代表:最小时延,最大吞吐量,最高可靠性和最小费用。4bit中只能置其中1比特。如果所有4bit均为0,那么就意味着是一般服务。
总长度:
总长度字段是指整个IP数据报的长度,以字节为单位。利用首部长度和总长度字段,就可以知道IP数据报中数据内容的起始位置和长度。由于该字段长16bit,所以IP数据报最长可达65535字节。当数据报被分片时,该字段的值也随着变化。
标识字段:
标识字段唯一地标识主机发送的每一份数据报。通常每发送一份报文它的值就会加1。
生存时间:
TTL(time-to-live)生存时间字段设置了数据报可以经过的最多路由器数。它指定了数据报的生存时间。TTL的初始值由源主机设置(通常为 3 2或6 4),一旦经过一个处理它的路由器,它的值就减去 1。当该字段的值为 0时,数据报就被丢弃,并发送 ICMP 报文通知源主机。
首部检验和:
首部检验和字段是根据 I P首部计算的检验和码。它不对首部后面的数据进行计算。 ICMP、IGMP、UDP和TCP在它们各自的首部中均含有同时覆盖首部和数据检验和码。
以太网报文格式:
目的地址和源地址:
是指网卡的硬件地址(也叫MAC 地址),长度是48 位,是在网卡出厂时固化的。
数据:
以太网帧中的数据长度规定最小46 字节,最大1500 字节,ARP 和RARP 数据包的长度不够46 字节,要在后面补填充位。最大值1500 称为以太网的最大传输单元(MTU),不同的网络类型有不同的MTU,如果一个数据包从以太网路由到拨号链路上,数据包度大于拨号链路的MTU了,则需要对数据包进行分片fragmentation)。ifconfig 命令的输出中也有“MTU:1500”。注意,MTU 个概念指数据帧中有效载荷的最大长度,不包括帧首部的长度。
HTTP协议
1、HTTP协议1.0_1.1_2.0
HTTP1.0:服务器处理完成后立即断开TCP连接(无连接),服务器不跟踪每个客户端也不记录过去的请求(无状态)
HTTP1.1:KeepAlived长连接避免了连接建立和释放的开销;通过Content-Length来判断当前请求数据是否已经全部接受(有状态)
HTTP2.0:引入二进制数据帧和流的概念,其中帧对数据进行顺序标识;因为有了序列,服务器可以并行的传输数据。
http1.0和http1.1的主要区别如下:
1、缓存处理:1.1添加更多的缓存控制策略(如:Entity tag,If-Match)
2、网络连接的优化:1.1支持断点续传
3、错误状态码的增多:1.1新增了24个错误状态响应码,丰富的错误码更加明确各个状态
4、Host头处理:支持Host头域,不在以IP为请求方标志
5、长连接:减少了建立和关闭连接的消耗和延迟。
http1.1和http2.0的主要区别:
1、新的传输格式:2.0使用二进制格式,1.0依然使用基于文本格式
2、多路复用:连接共享,不同的request可以使用同一个连接传输(最后根据每个request上的id号组合成正常的请求)
3、header压缩:由于1.X中header带有大量的信息,并且得重复传输,2.0使用encoder来减少需要传输的hearder大小
4、服务端推送:同google的SPDUY(1.0的一种升级)一样
2、HTTP与HTTPS之间的区别
HTTP与HTTPS之间的区别:
|
HTTP |
HTTPS |
|
默认端口80 |
HTTPS默认使用端口443 |
|
明文传输、数据未加密、安全性差 |
传输过程ssl加密、安全性较好 |
|
响应速度快、消耗资源少 |
响应速度较慢、消耗资源多、需要用到CA证书 |
HTTPS链接建立的过程:
1.首先客户端先给服务器发送一个请求
2.服务器发送一个SSL证书给客户端,内容包括:证书的发布机构、有效期、所有者、签名以及公钥
3.客户端对发来的公钥进行真伪校验,校验为真则使用公钥对对称加密算法以及对称密钥进行加密
4.服务器端使用私钥进行解密并使用对称密钥加密确认信息发送给客户端
5.随后客户端和服务端就使用对称密钥进行信息传输
对称加密算法:
双方持有相同的密钥,且加密速度快,典型对称加密算法:DES、AES
非对称加密算法:
密钥成对出现(私钥、公钥),私钥只有自己知道,不在网络中传输;而公钥可以公开。相比对称加密速度较慢,典型的非对称加密算法有:RSA、DSA
3、Get和Post请求区别
HTTP请求:
|
方法 |
描述 |
|
GET |
向特定资源发送请求,查询数据,并返回实体 |
|
POST |
向指定资源提交数据进行处理请求,可能会导致新的资源建立、已有资源修改 |
|
PUT |
向服务器上传新的内容 |
|
HEAD |
类似GET请求,返回的响应中没有具体的内容,用于获取报头 |
|
DELETE |
请求服务器删除指定标识的资源 |
|
OPTIONS |
可以用来向服务器发送请求来测试服务器的功能性 |
|
TRACE |
回显服务器收到的请求,用于测试或诊断 |
|
CONNECT |
HTTP/1.1协议中预留给能够将连接改为管道方式的代理服务器 |
get和Post区别:
|
GET |
POST |
|
|
可见性 |
数据在URL中对所有人可见 |
数据不会显示在URL中 |
|
安全性 |
与post相比,get的安全性较差,因为所 |
安全,因为参数不会被保存在浏览器 |
|
数据长度 |
受限制,最长2kb |
无限制 |
|
编码类型 |
application/x-www-form-urlencoded |
multipart/form-data |
|
缓存 |
能被缓存 |
不能被缓存 |
4、HTTP常见响应状态码
100:Continue --- 继续。客户端应继续其请求。
200:OK --- 请求成功。一般用于GET与POST请求。
301:Moved Permanently --- 永久重定向。
302:Found --- 暂时重定向。
400:Bad Request --- 客户端请求的语法错误,服务器无法理解。
403:Forbideen --- 服务器理解请求客户端的请求,但是拒绝执行此请求。
404:Not Found --- 服务器无法根据客户端的请求找到资源(网页)。
500:Internal Server Error --- 服务器内部错误,无法完成请求。
502:Bad Gateway --- 作为网关或者代理服务器尝试执行请求时,从远程服务器接收到了无效的响应。
5、重定向和转发区别
重定向:redirect:
地址栏发生变化
重定向可以访问其他站点(服务器)的资源
重定向是两次请求。不能使用request对象来共享数据
转发:forward:
转发地址栏路径不变
转发只能访问当前服务器下的资源
转发是一次请求,可以使用request对象共享数据
6、Cookie和Session区别。
Cookie 和 Session都是用来跟踪浏览器用户身份的会话方式,但两者有所区别:
Cookie 数据保存在客户端(浏览器端),Session 数据保存在服务器端。
cookie不是很安全,别人可以分析存放在本地的COOKIE并进行欺骗,考虑到安全应当使用session。
Cookie ⼀般⽤来保存⽤户信息,Session 的主要作⽤就是通过服务端记录⽤户的状态
浏览器输入URL过程
过程:DNS解析、TCP连接、发送HTTP请求、服务器处理请求并返回HTTP报文、浏览器渲染、结束
|
过程 |
使用的协议 |
|
1、浏览器查找域名DNS的IP地址 |
DNS:获取域名对应的ip |
|
2、根据ip建立TCP连接 |
TCP:与服务器建立连接 |
|
3、浏览器向服务器发送HTTP请求 |
HTTP:发送请求 |
|
4、服务器响应HTTP响应 |
HTTP |
|
5、浏览器进行渲染 |
7: 操作系统基础
进程和线程的区别
进程:是资源分配的最小单位,一个进程可以有多个线程,多个线程共享进程的堆和方法区资源,不共享栈、程序计数器
线程:是任务调度和执行的最小单位,线程并行执行存在资源竞争和上下文切换的问题
协程:是一种比线程更加轻量级的存在,正如一个进程可以拥有多个线程一样,一个线程可以拥有多个协程。
1、进程间通信方式IPC
管道pipe:
亲缘关系使用匿名管道,非亲缘关系使用命名管道,管道遵循FIFO,半双工,数据只能单向通信;
信号:
信号是一种比较复杂的通信方式,用户调用kill命令将信号发送给其他进程。
消息队列:
消息队列克服了信号传递信息少,管道只能承载无格式字节流以及缓冲区大小受限等特点。
共享内存(share memory):
- 使得多个进程可以可以直接读写同一块内存空间,是最快的可用IPC形式。是针对其他通信机制运行效率较低而设计的。
- 由于多个进程共享一段内存,因此需要依靠某种同步机制(如信号量)来达到进程间的同步及互斥。
信号量(Semaphores) :
信号量是⼀个计数器,⽤于多进程对共享数据的访问,这种通信⽅式主要⽤于解决与同步相关的问题并避免竞争条件。
套接字(Sockets) :
简单的说就是通信的两⽅的⼀种约定,⽤套接字中的相关函数来完成通信过程。
2、用户态和核心态
用户态:只能受限的访问内存,运行所有的应用程序
核心态:运行操作系统程序,cpu可以访问内存的所有数据,包括外围设备
为什么要有用户态和内核态:
由于需要限制不同的程序之间的访问能力, 防止他们获取别的程序的内存数据, 或者获取外围设备的数据, 并发送到网络
用户态切换到内核态的3种方式:
a. 系统调用
主动调用,系统调用的机制其核心还是使用了操作系统为用户特别开放的一个中断来实现,例如Linux的int 80h中断。
b. 异常
当CPU在执行运行在用户态下的程序时,发生了某些事先不可知的异常,比如缺页异常,这时会触发切换内核态处理异常。
c. 外围设备的中断
当外围设备完成用户请求的操作后,会向CPU发出相应的中断信号,这时CPU会由用户态到内核态的切换。
3、操作系统的进程空间
栈区(stack)— 由编译器自动分配释放 ,存放函数的参数值,局部变量的值等。
堆区(heap)— 一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。
静态区(static)—存放全局变量和静态变量的存储
代码区(text)—存放函数体的二进制代码。
线程共享堆区、静态区
操作系统内存管理
存管理方式:页式管理、段式管理、段页式管理
分段管理:
将程序的地址空间划分为若干段(segment),如代码段,数据段,堆栈段;这样每个进程有一个二维地址空间,相互独立,互不干扰。段式管理的优点是:没有内碎片(因为段大小可变,改变段大小来消除内碎片)。但段换入换出时,会产生外碎片(比如4k的段换5k的段,会产生1k的外碎片)
分页管理:
在页式存储管理中,将程序的逻辑地址划分为固定大小的页(page),而物理内存划分为同样大小的页框,程序加载时,可以将任意一页放入内存中任意一个页框,这些页框不必连续,从而实现了离散分离。页式存储管理的优点是:没有外碎片(因为页的大小固定),但会产生内碎片(一个页可能填充不满)
段页式管理:
段⻚式管理机制结合了段式管理和⻚式管理的优点。简单来说段⻚式管理机制就是把主存先分成若⼲段,每个段⼜分成若⼲⻚,也就是说 段⻚式管理机制 中段与段之间以及段的内部的都是离散的
1、页面置换算法FIFO、LRU
置换算法:先进先出FIFO、最近最久未使用LRU、最佳置换算法OPT
先进先出FIFO:
缺点:没有考虑到实际的页面使用频率,性能差、与通常页面使用的规则不符合,实际应用较少
最近最久未使用LRU:
原理:选择最近且最久未使用的页面进行淘汰
优点:考虑到了程序访问的时间局部性,有较好的性能,实际应用也比较多
缺点:没有合适的算法,只有适合的算法,lFU、random都可以
/**
* @program: Java
* @description: LRU最近最久未使用置换算法,通过LinkedHashMap实现
* @author: Mr.Li
* @create: 2020-07-17 10:29
**/
public class LRUCache {
private LinkedHashMap<Integer,Integer> cache;
private int capacity; //容量大小
/**
*初始化构造函数
* @param capacity
*/
public LRUCache(int capacity) {
cache = new LinkedHashMap<>(capacity);
this.capacity = capacity;
}
public int get(int key) {
//缓存中不存在此key,直接返回
if(!cache.containsKey(key)) {
return -1;
}
int res = cache.get(key);
cache.remove(key); //先从链表中删除
cache.put(key,res); //再把该节点放到链表末尾处
return res;
}
public void put(int key,int value) {
if(cache.containsKey(key)) {
cache.remove(key); //已经存在,在当前链表移除
}
if(capacity == cache.size()) {
//cache已满,删除链表头位置
Set<Integer> keySet = cache.keySet();
Iterator<Integer> iterator = keySet.iterator();
cache.remove(iterator.next());
}
cache.put(key,value); //插入到链表末尾
}
}
/**
* @program: Java
* @description: LRU最近最久未使用置换算法,通过LinkedHashMap内部removeEldestEntry方法实现
* @author: Mr.Li
* @create: 2020-07-17 10:59
**/
class LRUCache {
private Map<Integer, Integer> map;
private int capacity;
/**
*初始化构造函数
* @param capacity
*/
public LRUCache(int capacity) {
this.capacity = capacity;
map = new LinkedHashMap<Integer, Integer>(capacity, 0.75f, true) {
@Override
protected boolean removeEldestEntry(Map.Entry eldest) {
return size() > capacity; // 容量大于capacity 时就删除
}
};
}
public int get(int key) {
//返回key对应的value值,若不存在,返回-1
return map.getOrDefault(key, -1);
}
public void put(int key, int value) {
map.put(key, value);
}
}
最佳置换算法OPT:
原理:每次选择当前物理块中的页面在未来长时间不被访问的或未来不再使用的页面进行淘汰
优点:具有较好的性能,可以保证获得最低的缺页率
缺点:过于理想化,但是实际上无法实现(没办法预知未来的页面)
2、死锁条件、解决方式。
死锁是指两个或两个以上进程在执行过程中,因争夺资源而造成的下相互等待的现象;
死锁的条件:
互斥条件:进程对所分配到的资源不允许其他进程访问,若其他进程访问该资源,只能等待至占有该资源的进程释放该资源;
请求与保持条件:进程获得一定的资源后,又对其他资源发出请求,阻塞过程中不会释放自己已经占有的资源
非剥夺条件:进程已获得的资源,在未完成使用之前,不可被剥夺,只能在使用后自己释放
循环等待条件:系统中若干进程组成环路,环路中每个进程都在等待相邻进程占用的资源
解决方法:破坏死锁的任意一条件
乐观锁,破坏资源互斥条件,CAS
资源一次性分配,从而剥夺请求和保持条件、tryLock
可剥夺资源:即当进程新的资源未得到满足时,释放已占有的资源,从而破坏不可剥夺的条件,数据库deadlock超时
资源有序分配法:系统给每类资源赋予一个序号,每个进程按编号递增的请求资源,从而破坏环路等待的条件,转账场景
8: Java基础
面向对象三大特性
特性:封装、继承、多态
封装:对抽象的事物抽象化成一个对象,并对其对象的属性私有化,同时提供一些能被外界访问属性的方法;
继承:子类扩展新的数据域或功能,并复用父类的属性与功能,单继承,多实现;
多态:通过继承(多个⼦类对同⼀⽅法的重写)、也可以通过接⼝(实现接⼝并覆盖接⼝)
1、Java与C++区别
不同点:c++支持多继承,并且有指针的概念,由程序员自己管理内存;Java是单继承,可以用接口实现多继承,Java 不提供指针来直接访问内存,程序内存更加安全,并且Java有JVM⾃动内存管理机制,不需要程序员⼿动释放⽆⽤内存
2、多态实现原理
多态的底层实现是动态绑定,即在运行时才把方法调用与方法实现关联起来。
静态绑定与动态绑定:
一种是在编译期确定,被称为静态分派,比如方法的重载;
一种是在运行时确定,被称为动态分派,比如方法的覆盖(重写)和接口的实现。
多态的实现
虚拟机栈中会存放当前方法调用的栈帧(局部变量表、操作栈、动态连接 、返回地址)。多态的实现过程,就是方法调用动态分派的过程,如果子类覆盖了父类的方法,则在多态调用中,动态绑定过程会首先确定实际类型是子类,从而先搜索到子类中的方法。这个过程便是方法覆盖的本质。
3、static和final关键字
static:可以修饰属性、方法
static修饰属性:
类级别属性,所有对象共享一份,随着类的加载而加载(只加载一次),先于对象的创建;可以使用类名直接调用。
static修饰方法:
随着类的加载而加载;可以使用类名直接调用;静态方法中,只能调用静态的成员,不可用this;
final:关键字主要⽤在三个地⽅:变量、⽅法、类。
final修饰变量:
如果是基本数据类型的变量,则其数值⼀旦在初始化之后便不能更改;
如果是引⽤类型的变量,则在对其初始化之后便不能再让其指向另⼀个对象。
final修饰方法:
把⽅法锁定,以防任何继承类修改它的含义(重写);类中所有的 private ⽅法都隐式地指定为 final。
final修饰类:
final 修饰类时,表明这个类不能被继承。final 类中的所有成员⽅法都会被隐式地指定为 final ⽅法。
一个类不能被继承,除了final关键字之外,还有可以私有化构造器。(内部类无效)
4、抽象类和接口
抽象类:包含抽象方法的类,即使用abstract修饰的类;抽象类只能被继承,所以不能使用final修饰,抽象类不能被实例化,
接口:接口是一个抽象类型,是抽象方法的集合,接口支持多继承,接口中定义的方法,默认是public abstract修饰的抽象方法
相同点:
① 抽象类和接口都不能被实例化
② 抽象类和接口都可以定义抽象方法,子类/实现类必须覆写这些抽象方法
不同点:
① 抽象类有构造方法,接口没有构造方法
③抽象类可以包含普通方法,接口中只能是public abstract修饰抽象方法(Java8之后可以)
③ 抽象类只能单继承,接口可以多继承
④ 抽象类可以定义各种类型的成员变量,接口中只能是public static final修饰的静态常量
抽象类的使用场景:
既想约束子类具有共同的行为(但不再乎其如何实现),又想拥有缺省的方法,又能拥有实例变量
接口的应用场景:
约束多个实现类具有统一的行为,但是不在乎每个实现类如何具体实现;实现类中各个功能之间可能没有任何联系
5、泛型以及泛型擦除
参考:https://blog.csdn.net/baoyinwang/article/details/107341997
泛型:
泛型的本质是参数化类型。这种参数类型可以用在类、接口和方法的创建中,分别称为泛型类、泛型接口和泛型方法。
泛型擦除:
Java的泛型是伪泛型,使用泛型的时候加上类型参数,在编译器编译生成的字节码的时候会去掉,这个过程成为类型擦除。
如List等类型,在编译之后都会变成 List。JVM 看到的只是 List,而由泛型附加的类型信息对 JVM 来说是不可见的。
可以通过反射添加其它类型元素
6、反射原理以及使用场景
Java反射:
是指在运行状态中,对于任意一个类都能够知道这个类所有的属性和方法;并且都能够调用它的任意一个方法;
反射原理:
反射首先是能够获取到Java中的反射类的字节码,然后将字节码中的方法,变量,构造函数等映射成 相应的 Method、Filed、Constructor 等类
如何得到Class的实例:
1.类名.class(就是一份字节码)
2.Class.forName(String className);根据一个类的全限定名来构建Class对象
3.每一个对象多有getClass()方法:obj.getClass();返回对象的真实类型
使用场景:
- 开发通用框架 - 反射最重要的用途就是开发各种通用框架。很多框架(比如 Spring)都是配置化的(比如通过 XML 文件配置 JavaBean、Filter 等),为了保证框架的通用性,需要根据配置文件运行时动态加载不同的对象或类,调用不同的方法。
- 动态代理 - 在切面编程(AOP)中,需要拦截特定的方法,通常,会选择动态代理方式。这时,就需要反射技术来实现了。JDK:spring默认动态代理,需要实现接口CGLIB:通过asm框架序列化字节流,可配置,性能差
- 自定义注解 - 注解本身仅仅是起到标记作用,它需要利用反射机制,根据注解标记去调用注解解释器,执行行为。
7、Java异常体系
Throwable 是 Java 语言中所有错误或异常的超类。下一层分为 Error 和 Exception
Error :
是指 java 运行时系统的内部错误和资源耗尽错误。应用程序不会抛出该类对象。如果出现了这样的错误,除了告知用户,剩下的就是尽力使程序安全的终止。
Exception 包含:RuntimeException 、CheckedException
编程错误可以分成三类:语法错误、逻辑错误和运行错误。
语法错误(也称编译错误)是在编译过程中出现的错误,由编译器检查发现语法错误
逻辑错误指程序的执行结果与预期不符,可以通过调试定位并发现错误的原因
运行错误是引起程序非正常终端的错误,需要通过异常处理的方式处理运行错误
RuntimeException: 运行时异常,程序应该从逻辑角度尽可能避免这类异常的发生。
如 NullPointerException 、 ClassCastException ;
CheckedException:受检异常,程序使用trycatch进行捕捉处理
如IOException、SQLException、NotFoundException;
8: transient关键字
其实这个关键字的作用很好理解,就是简单的一句话:将不需要序列化的属性前添加关键字transient,序列化对象的时候,这个属性就不会被序列化。
9:"abc"\"ab"+"c"\"a"+"b"+"c" 比较


10: static变量和正常变量初始化过程


数据结构

1、ArrayList和LinkedList
ArrayList:
底层基于数组实现,支持对元素进行快速随机访问,适合随机查找和遍历,不适合插入和删除。(提一句实际上)
默认初始大小为10,当数组容量不够时,会触发扩容机制(扩大到当前的1.5倍),需要将原来数组的数据复制到新的数组中;当从 ArrayList 的中间位置插入或者删除元素时,需要对数组进行复制、移动、代价比较高。
LinkedList:
底层基于双向链表实现,适合数据的动态插入和删除;
内部提供了 List 接口中没有定义的方法,用于操作表头和表尾元素,可以当作堆栈、队列和双向队列使用。(比如jdk官方推荐使用基于linkedList的Deque进行堆栈操作)
ArrayList与LinkedList区别:
都是线程不安全的,ArrayList 适用于查找的场景,LinkedList 适用于增加、删除多的场景
实现线程安全:
可以使用原生的Vector,或者是Collections.synchronizedList(List list)函数返回一个线程安全的ArrayList集合。
建议使用concurrent并发包下的CopyOnWriteArrayList的。
①Vector: 底层通过synchronize修饰保证线程安全,效率较差
②CopyOnWriteArrayList:写时加锁,使用了一种叫写时复制的方法;读操作是可以不用加锁的
2、List遍历快速和安全失败
①普通for循环遍历List删除指定元素
for(int i=0; i < list.size(); i++){
if(list.get(i) == 5)
list.remove(i);
}
② 迭代遍历,用list.remove(i)方法删除元素
Iterator<Integer> it = list.iterator();
while(it.hasNext()){
Integer value = it.next();
if(value == 5){
list.remove(value);
}
}
③foreach遍历List删除元素
for(Integer i:list){
if(i==3) list.remove(i);
}
fail—fast:快速失败
当异常产生时,直接抛出异常,程序终止;
fail-fast主要是体现在当我们在遍历集合元素的时候,经常会使用迭代器,但在迭代器遍历元素的过程中,如果集合的结构(modCount)被改变的话,就会抛出异常ConcurrentModificationException,防止继续遍历。这就是所谓的快速失败机制。
fail—safe:安全失败
采用安全失败机制的集合容器,在遍历时不是直接在集合内容上访问的,而是先复制原有集合内容,在拷贝的集合上进行遍历。由于在遍历过程中对原集合所作的修改并不能被迭代器检测到,所以不会触发ConcurrentModificationException。
缺点:基于拷贝内容的优点是避免了ConcurrentModificationException,但同样地,迭代器并不能访问到修改后的内容,即:迭代器遍历的是开始遍历那一刻拿到的集合拷贝,在遍历期间原集合发生的修改迭代器是不知道的。
场景:java.util.concurrent包下的容器都是安全失败,可以在多线程下并发使用,并发修改。
3、详细介绍HashMap
角度:数据结构+扩容情况+put查找的详细过程+哈希函数+容量为什么始终都是2^N,JDK1.7与1.8的区别。
参考:https://www.jianshu.com/p/9fe4cb316c05
数据结构:
HashMap在底层数据结构上采用了数组+链表+红黑树,通过散列映射来存储键值对数据
扩容情况:
默认的负载因子是0.75,如果数组中已经存储的元素个数大于数组长度的75%,将会引发扩容操作。
【1】创建一个长度为原来数组长度两倍的新数组。
【2】1.7采用Entry的重新hash运算,1.8采用高于与运算。
put操作步骤:

1、判断数组是否为空,为空进行初始化;
2、不为空,则计算 key 的 hash 值,通过(n - 1) & hash计算应当存放在数组中的下标 index;
3、查看 table[index] 是否存在数据,没有数据就构造一个Node节点存放在 table[index] 中;
4、存在数据,说明发生了hash冲突(存在二个节点key的hash值一样), 继续判断key是否相等,相等,用新的value替换原数据;
5、若不相等,判断当前节点类型是不是树型节点,如果是树型节点,创造树型节点插入红黑树中;
6、若不是红黑树,创建普通Node加入链表中;判断链表长度是否大于 8,大于则将链表转换为红黑树;
7、插入完成之后判断当前节点数是否大于阈值,若大于,则扩容为原数组的二倍
哈希函数:
通过hash函数(优质因子31循环累加)先拿到 key 的hashcode,是一个32位的值,然后让hashcode的高16位和低16位进行异或操作。该函数也称为扰动函数,做到尽可能降低hash碰撞,通过尾插法进行插入。
容量为什么始终都是2^N:
先做对数组的⻓度取模运算,得到的余数才能⽤来要存放的位置也就是对应的数组下标。这个数组下标的计算⽅法是“ (n - 1) & hash ”。(n代表数组⻓度)。方便数组的扩容和增删改时的取模。
JDK1.7与1.8的区别:
JDK1.7 HashMap:
底层是 数组和链表 结合在⼀起使⽤也就是链表散列。如果相同的话,直接覆盖,不相同就通过拉链法解决冲突。扩容翻转时顺序不一致使用头插法会产生死循环,导致cpu100%
JDK1.8 HashMap:
底层数据结构上采用了数组+链表+红黑树;当链表⻓度⼤于阈值(默认为 8-泊松分布),数组的⻓度大于 64时,链表将转化为红⿊树,以减少搜索时间。(解决了tomcat臭名昭著的url参数dos攻击问题)
**4、ConcurrentHashMap **
可以通过ConcurrentHashMap 和 Hashtable来实现线程安全;Hashtable 是原始API类,通过synchronize同步修饰,效率低下;ConcurrentHashMap 通过分段锁实现,效率较比Hashtable要好;
ConcurrentHashMap的底层实现:
JDK1.7的 ConcurrentHashMap 底层采⽤ 分段的数组+链表 实现;采用 分段锁(Sagment) 对整个桶数组进⾏了分割分段(Segment默认16个),每⼀把锁只锁容器其中⼀部分数据,多线程访问容器⾥不同数据段的数据,就不会存在锁竞争,提⾼并发访问率。
JDK1.8的 ConcurrentHashMap 采⽤的数据结构跟HashMap1.8的结构⼀样,数组+链表/红⿊树;摒弃了Segment的概念,⽽是直接⽤ Node 数组+链表+红⿊树的数据结构来实现,通过并发控制 synchronized 和CAS来操作保证线程的安全。
5、序列化和反序列化
序列化的意思就是将对象的状态转化成字节流,以后可以通过这些值再生成相同状态的对象。对象序列化是对象持久化的一种实现方法,它是将对象的属性和方法转化为一种序列化的形式用于存储和传输。反序列化就是根据这些保存的信息重建对象的过程。
序列化:将java对象转化为字节序列的过程。
反序列化:将字节序列转化为java对象的过程。
优点:
a、实现了数据的持久化,通过序列化可以把数据永久地保存到硬盘上(通常存放在文件里)Redis的RDB
b、利用序列化实现远程通信,即在网络上传送对象的字节序列。 Google的protoBuf
反序列化失败的场景:
序列化ID:serialVersionUID不一致的时候,导致反序列化失败
6、String、StringBuilder和StringBuffer的区别,应用场景
String 使用数组存储内容,数组使用 final 修饰,因此 String 定义的字符串的值也是不可变的
StringBuffer 对方法加了同步锁,线程安全,效率略低于 StringBuilder

7: B树和B+树的区别?B树和B+树的优点分别是?
B树:

B+树:


8: 排序算法的种类和复杂度

9: HashMap和Hashtable的原理、区别、应用场景


10: ArrayList和Vector的区别

11: Collection下有哪些子类

12: Comparable和Comparator的区别

9: JUC
1: CAS

2: AQS




3: Atomic 原子更新
Java 从 JDK1.5 开始提供了 java.util.concurrent.atomic 包,方便程序员在多线程环 境下,无锁的进行原子操作。在 Atomic 包里一共有 12 个类,四种原子更新方式,分别是原子更新基本类型,原子更新数组,原子更新引用和原子更新字段。在 JDK 1.8 之后又新增几个原子类。如下如:

列举几个AtomicLong 的常用方法
- long getAndIncrement() :以原子方式将当前值加1,注意,返回的是旧值。(i++)
- long incrementAndGet() :以原子方式将当前值加1,注意,返回的是新值。(++i)
- long getAndDecrement() :以原子方式将当前值减 1,注意,返回的是旧值 。(i--)
- long decrementAndGet() :以原子方式将当前值减 1,注意,返回的是新值 。(--i)
- long addAndGet(int delta) :以原子方式将输入的数值与实例中的值(AtomicLong里的value)相加,并返回结果
说说 AtomicInteger 和 synchronized 的异同点?
相同点
- 都是线程安全
不同点
- 1、背后原理
synchronized 背后的 monitor 锁。在执行同步代码之前,需要首先获取到 monitor 锁,执行完毕后,再释放锁。原子类,线程安全的原理是利用了 CAS 操作。 - 2、使用范围
原子类使用范围是比较局限的,一个原子类仅仅是一个对象,不够灵活。而 synchronized 的使用范围要广泛得多。比如说 synchronized 既可以修饰一个方法,又可以修饰一段代码,相当于可以根据我们的需要,非常灵活地去控制它的应用范围 - 3、粒度
原子变量的粒度是比较小的,它可以把竞争范围缩小到变量级别。通常情况下,synchronized锁的粒度都要大于原子变量的粒度。 - 4、性能
synchronized是一种典型的悲观锁,而原子类恰恰相反,它利用的是乐观锁。
LongAdder


4: 并发工具CountDownLatch、Semaphore

区别:

5: locks 锁
公平锁与非公平锁
ReentrantLock支持公平锁和非公平锁两种方式。公平锁指锁的分配和竞争机制是公平的,即遵循先到先得原则。非公平锁指JVM遵循随机、就近原则分配锁的机制。ReentrantLock通过在构造函数ReentrantLock(boolean fair)中传递不同的参数来定义不同类型的锁,默认的实现是非公平锁。这是因为,非公平锁虽然放弃了锁的公平性,但是执行效率明显高于公平锁。如果系统没有特殊的要求,一般情况下建议使用非公平锁。
synchronized 和 lock 有什么区别?
- synchronized 可以给类,方法,代码块加锁,而lock 只能给代码块加锁。
- synchronized 不需要手动获取锁和释放锁,使用简单,发生异常会自动释放锁,不会造成死锁,而 lock 需要手动自己加锁和释放锁,如果使用不当没有 unLock 去释放锁,就会造成死锁。
- 通过 lock 可以知道有没有成功获取锁,而 synchronized 无法办到。
synchronized 和 Lock 如何选择?
- synchronized 和Lock 都是用来保护资源线程安全的。
- 都保证了可见性和互斥性。
- synchronized 和 ReentrantLock都拥有可重入的特点。
不同点:
- 用法(lock 需要配合finally )
- ReentrantLock可响应中断、可轮回,为处理锁提供了更多的灵活性
- ReentrantLock通过Condition可以绑定多个条件
- 加解锁顺序()
- synchronized 锁不够灵活
- 是否可以设置公平/非公平
- 二者的底层实现不一样:synchronized是同步阻塞,采用的是悲观并发策略;Lock是同步非阻塞,采用的是乐观并发策略。
使用
- 如果能不用最好既不使用 Lock 也不使用 synchronized。
- 如果 synchronized关键字适合你的程序,这样可以减少编写代码的数量,减少出错的概率
- 如果特别需要 Lock 的特殊功能,比如尝试获取锁、可中断、超时功能等,才使用 Lock。
Lock接口的主要方法
- void lock():获取锁,调用该方法当前线程将会获取锁,当锁获得后,从该方法返回
- void lockInterruptibly() throws InterruptedException:可中断地获取锁,和lock方法地不同之处在于该方法会响应中断,即在锁的获取中可以中断当前线程
- boolean tryLock(): 尝试非阻塞地获取锁,调用该方法后立刻返回,如果能够获取则返回 true 否则 返回false
- boolean tryLock(long time, TimeUnit unit):超时地获取锁,当前线程在以下 3 种情况下会返回:
- 当前线程在超时时间内获得了锁
- 当前线程在超时时间被中断
- 超时时间结束后,返回 false
- void unlock(): 释放锁
- Condition newCondition():获取锁等待通知组件,该组件和当前的锁绑定,当前线程只有获得了锁,才能调用该组件的 wait() 方法,而调用后,当前线程将释放锁。
tryLock、lock和lockInterruptibly的区别
tryLock、lock和lockInterruptibly的区别如下。
- tryLock若有可用锁,则获取该锁并返回true,否则返回false,不会有延迟或等待;tryLock(long timeout, TimeUnit unit)可以增加时间限制,如果超过了指定的时间还没获得锁,则返回 false。
- lock若有可用锁,则获取该锁并返回true,否则会一直等待直到获取可用锁。
- 在锁中断时lockInterruptibly会抛出异常,lock不会。
ReentrantReadWriteLock 读写锁的获取规则
要么是一个或多个线程同时有读锁,要么是一个线程有写锁,但是两者不会同时出现。也可以总结为:读读共享、其他都互斥(写写互斥、读写互斥、写读互斥)
ReentrantLock 适用于一般场合,ReadWriteLock 适用于读多写少的情况,合理使用可以进一步提高并发效率。
ConcurrentHashMap JDK 1.7/JDK 1.8




什么是阻塞队列?

介绍一下ReentrantReadwriteLock
ReentrantReadWriteLock 是Java并发包 `java.util.concurrent.locks` 中的一个类,它实现了读写锁(Read-Write Lock)的概念。与普通的互斥锁不同,读写锁允许多个线程同时对共享资源进行读操作,但在同一时刻只允许一个线程进行写操作。
特点:
1. 读写分离:ReentrantReadWriteLock 包含两个锁,一个是读锁(也称为共享锁),另一个是写锁(也称为独占锁)。多个读线程可以同时持有读锁并执行读取操作,但当有线程持有了写锁时,其他所有读写线程都必须等待,直到写锁被释放。
2.可重入性:无论是读锁还是写锁都是可重入的,这意味着如果一个已经持有读或写锁的线程尝试再次获取相同的锁,那么这个请求将成功,并且锁计数会增加,确保了递归调用和嵌套同步的支持。
3. 公平性选择:ReentrantReadWriteLock 提供了公平和非公平两种模式的选择。公平锁遵循FIFO队列策略,即等待时间最长的线程优先获得锁;而非公平锁则不保证这种顺序,可能会让新到来的线程插队获取锁,这通常具有更高的性能,但可能导致线程饥饿现象。
4. 状态管理:ReentrantReadWriteLock 使用内部状态来跟踪当前有多少读线程和写线程持有锁,以及每个线程的重入次数。
使用场景:
读写锁非常适合那些读多写少的应用场景,例如缓存系统、数据库访问等。在这些场景下,读操作远比写操作频繁,通过允许多个读线程并发执行,能够大大提高系统的并发性能和吞吐量。


浙公网安备 33010602011771号