JavaWeb知识整理-增强篇

1、Java基础

1.1、序列化和反序列化的底层实现原理

定义?

序列化:把对象转换成有序字节流,以便在网络上传输或者保存在本地文件中。传递或者保存对象时,保证对象的完整性或可传递性。

反序列化:客户端从文件中或网络上获得序列化后的对象字节流后,根据字节流中所保存的对象状态及描述信息,通过反序列化重建对象。

怎么实现?

实现Serializable或Externalizable接口。

原理:

序列化图示:

反序列化图示:

相关注意事项

1、序列化时,只对对象的状态进行保存,而不管对象的方法;

2、当一个父类实现序列化,子类自动实现序列化,不需要显式实现Serializable接口;

3、当一个对象的实例变量引用其他对象,序列化该对象时也把引用对象进行序列化;

4、并非所有的对象都可以序列化,至于为什么不可以,有很多原因了,比如:

安全方面的原因,比如一个对象拥有private,public等field,对于一个要传输的对象,比如写到文件,或者进行RMI传输等等,在序列化进行传输的过程中,这个对象的private等域是不受保护的;

资源分配方面的原因,比如socket,thread类,如果可以序列化,进行传输或者保存,也无法对他们进行重新的资源分配,而且,也是没有必要这样实现;

5、声明为static和transient类型的成员数据不能被序列化。因为static代表类的状态,transient代表对象的临时数据。

6、序列化运行时使用一个称为 serialVersionUID 的版本号与每个可序列化类相关联,该序列号在反序列化过程中用于验证序列化对象的发送者和接收者是否为该对象加载了与序列化兼容的类。为它赋予明确的值。显式地定义serialVersionUID有两种用途:

在某些场合,希望类的不同版本对序列化兼容,因此需要确保类的不同版本具有相同的serialVersionUID;

在某些场合,不希望类的不同版本对序列化兼容,因此需要确保类的不同版本具有不同的serialVersionUID。

7、Java有很多基础类已经实现了serializable接口,比如String,Vector等。但是也有一些没有实现serializable接口的;

8、如果一个对象的成员变量是一个对象,那么这个对象的数据成员也会被保存!这是能用序列化解决深拷贝的重要原因;
--------------------- 

摘自:https://blog.csdn.net/xlgen157387/article/details/79840134

序列化、反序列化:https://kb.cnblogs.com/page/515982/

1.2、Object类中常用的方法,为什么wait notify会放到Object里面

1、toString()、equals()、hashCode()、wait()、notify()-随机唤醒一个、notifyAll()、clone()

 1 registerNatives()   //私有方法
 2 getClass()    //返回此 Object 的运行类。
 3 hashCode()    //用于获取对象的哈希值。
 4 equals(Object obj)     //用于确认两个对象是否“相同”。
 5 clone()    //创建并返回此对象的一个副本。 
 6 toString()   //返回该对象的字符串表示。   
 7 notify()    //唤醒在此对象监视器上等待的单个线程。   
 8 notifyAll()     //唤醒在此对象监视器上等待的所有线程。   
 9 wait(long timeout)    //在其他线程调用此对象的 notify() 方法或 notifyAll() 方法,或        者超过指定的时间量前,导致当前线程等待。   
10 wait(long timeout, int nanos)    //在其他线程调用此对象的 notify() 方法或 notifyAll() 方法,或者其他某个线程中断当前线程,或者已超过某个实际时间量前,导致当前线程等待。
11 wait()    //用于让当前线程失去操作权限,当前线程进入等待序列
12 finalize()    //当垃圾回收器确定不存在对该对象的更多引用时,由对象的垃圾回收器调用此方法。

2、Obj.wait()与Obj.notify()必须要与synchronized(Obj)一起使用,也就是wait与notify是针对已经获取了Obj锁的对象来进行操作。 
(1)Obj.wait()、Obj.notify必须在synchronized(Obj){…}语句块内。 
(2)wait就是说线程在获取对象锁后,主动释放对象锁,同时本线程休眠。直到有其它线程调用对象的notify()唤醒该线程,才能继续获取对象锁,并继续执行。相应的notify()就是对对象锁的唤醒操作。

3、因为synchronized中的这把锁可以是任意对象,所以任意对象都可以调用wait()和notify();所以wait和notify属于Object

参考:

wait、notify与synchronized一起用:

https://blog.csdn.net/chy555chy/article/details/52279544

https://blog.csdn.net/lengyue309/article/details/79639245

https://blog.csdn.net/qq_39907763/article/details/79301813

1.3、Java提供的排序算法怎么实现的

Array.sort:

Collections.sort():

是否使用归并?

否则 TimeSort(Timsort是一种混合、稳定高效的排序算法,源自合并排序和插入排序,旨在很好地处理多种真实数据)。

TimeSort:https://blog.csdn.net/sinat_35678407/article/details/82974174

参考:

Java排序算法:https://blog.csdn.net/xlgen157387/article/details/79863301

1.4、占用多少磁盘大小

Java中基本数据类型占用空间:

byte 1
boolean 1
char 2
short2
int 4
float 4
long 8
double 8

String占用字节数:

str.getBytes().length

比特位,比特,兆等换算:

8bit(位)=1Byte(字节)
1024Byte(字节)=1KB
1024KB=1MB
1024MB=1GB
1024GB=1TB

参考:

把字节数B转化为KB、MB、GB的方法:https://blog.csdn.net/yongh701/article/details/45769547

2、多线程

2.1、CAS无锁的概念

CAS 操作包含三个操作数 —— 内存位置(V)、预期原值(A)和新值(B)。如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值。否则,处理器不做任何操作。无论哪种情况,它都会在 CAS 指令之前返回该位置的值

参考:https://www.cnblogs.com/Mainz/p/3546347.html

2.2、JUC 锁架构

2.3、AQS同步队列

AQS核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制AQS是用CLH队列锁实现的,即将暂时获取不到锁的线程加入到队列中。

CLH(Craig,Landin,and Hagersten)队列是一个虚拟的双向队列(虚拟的双向队列即不存在队列实例,仅存在结点之间的关联关系)。AQS是将每条请求共享资源的线程封装成一个CLH锁队列的一个结点(Node)来实现锁的分配。

看个AQS(AbstractQueuedSynchronizer)原理图:

AQS,它维护了一个volatile int state(代表共享资源)和一个FIFO线程等待队列(多线程争用资源被阻塞时会进入此队列)。这里volatile是核心关键词,具体volatile的语义,在此不述。state的访问方式有三种:

  • getState()
  • setState()
  • compareAndSetState()

AQS定义两种资源共享方式:Exclusive(独占,只有一个线程能执行,如ReentrantLock)和Share(共享,多个线程可同时执行,如Semaphore/CountDownLatch)。

不同的自定义同步器争用共享资源的方式也不同。自定义同步器在实现时只需要实现共享资源state的获取与释放方式即可,至于具体线程等待队列的维护(如获取资源失败入队/唤醒出队等),AQS已经在顶层实现好了。自定义同步器实现时主要实现以下几种方法:

  • isHeldExclusively():该线程是否正在独占资源。只有用到condition才需要去实现它。
  • tryAcquire(int):独占方式。尝试获取资源,成功则返回true,失败则返回false。
  • tryRelease(int):独占方式。尝试释放资源,成功则返回true,失败则返回false。
  • tryAcquireShared(int):共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
  • tryReleaseShared(int):共享方式。尝试释放资源,成功则返回true,失败则返回false。
  • 以ReentrantLock为例,state初始化为0,表示未锁定状态。A线程lock()时,会调用tryAcquire()独占该锁并将state+1。此后,其他线程再tryAcquire()时就会失败,直到A线程unlock()到state=0(即释放锁)为止,其它线程才有机会获取该锁。当然,释放锁之前,A线程自己是可以重复获取此锁的(state会累加),这就是可重入的概念。但要注意,获取多少次就要释放多么次,这样才能保证state是能回到零态的。

再以CountDownLatch以例,任务分为N个子线程去执行,state也初始化为N(注意N要与线程个数一致)。这N个子线程是并行执行的,每个子线程执行完后countDown()一次,state会CAS(Compare and Swap)减1。等到所有子线程都执行完后(即state=0),会unpark()主调用线程,然后主调用线程就会从await()函数返回,继续后余动作。

一般来说,自定义同步器要么是独占方法,要么是共享方式,他们也只需实现tryAcquire-tryRelease、tryAcquireShared-tryReleaseShared中的一种即可。但AQS也支持自定义同步器同时实现独占和共享两种方式,如ReentrantReadWriteLock。
--------------------- 

摘自:AQS浅析:https://blog.csdn.net/m_xiaoer/article/details/73459444

acquire流程:

  1. 调用自定义同步器的tryAcquire()尝试直接去获取资源,如果成功则直接返回;
  2. 没成功,则addWaiter()将该线程加入等待队列的尾部,并标记为独占模式;
  3. acquireQueued()使线程在等待队列中休息,有机会时(轮到自己,会被unpark())会去尝试获取资源。获取到资源后才返回。如果在整个等待过程中被中断过,则返回true,否则返回false。
  4. 如果线程在等待过程中被中断过,它是不响应的。只是获取资源后才再进行自我中断selfInterrupt(),将中断补上。

release流程:

release()是独占模式下线程释放共享资源的顶层入口。它会释放指定量的资源,如果彻底释放了(即state=0),它会唤醒等待队列里的其他线程来获取资源。(共享的话,会唤醒其他满足条件的线程)。

注意:其中用到的LockSupport是用来创建锁和其他同步类的基本线程阻塞原语。 
LockSupport中的park() 和 unpark() 的作用分别是阻塞线程和解除阻塞线程,而且park()和unpark()不会遇到“Thread.suspend 和 Thread.resume所可能引发的死锁”问题。
因为park() 和 unpark()有许可的存在;调用 park() 的线程和另一个试图将其 unpark() 的线程之间的竞争将保持活性。

参考:

JUC源码解析(强烈推荐):https://www.cnblogs.com/skywang12345/category/455711.html

Java并发之AQS详解:http://www.cnblogs.com/waterystone/p/4920797.html

AQS:https://blog.csdn.net/zhangdong2012/article/details/79983404

2.3、ConcurrentHashMap实现原理

结构分析:

ConcurrentHashMap 类中包含两个静态内部类 HashEntry 和 Segment。HashEntry 用来封装映射表的键 / 值对;Segment 用来充当锁的角色,每个 Segment 对象守护整个散列映射表的若干个桶。每个桶是由若干个 HashEntry 对象链接起来的链表。一个 ConcurrentHashMap 实例中包含由若干个 Segment 对象组成的数组。

进一步分析:

Segment ( 默认初始16 个 Segment 对象)类继承于 ReentrantLock 类,从而使得 Segment 对象能充当锁的角色。每个 Segment 对象用来守护其(成员对象 table 中)包含的若干个桶。

table 是一个由 HashEntry 对象组成的数组。table 数组的每一个数组成员就是散列映射表的一个桶。

count 变量是一个计数器,它表示每个 Segment 对象管理的 table 数组(若干个 HashEntry 组成的链表)包含的 HashEntry 对象的个数。每一个 Segment 对象都有一个 count 对象来表示本 Segment 中包含的 HashEntry 对象的总数。注意,之所以在每个 Segment 对象中包含一个计数器,而不是在ConcurrentHashMap 中使用全局的计数器,是为了避免出现“热点域”而影响 ConcurrentHashMap 的并发性。

用分离锁实现多个线程间的并发写操作

在 ConcurrentHashMap 中,线程对映射表做读操作时,一般情况下不需要加锁就可以完成,对容器做结构性修改的操作才需要加锁。注意:这里的加锁操作是针对(键的 hash 值对应的)某个具体的 Segment,锁定的是该 Segment 而不是整个 ConcurrentHashMap。因为插入键 / 值对操作只是在这个 Segment 包含的某个桶中完成,不需要锁定整个ConcurrentHashMap。此时,其他写线程对另外 15 个Segment 的加锁并不会因为当前线程对这个 Segment 的加锁而阻塞。同时,所有读线程几乎不会因本线程的加锁而阻塞(除非读线程刚好读到这个 Segment 中某个 HashEntry 的 value 域的值为 null,此时需要加锁后重新读取该值)。

相比较于 HashTable 和由同步包装器包装的 HashMap每次只能有一个线程执行读或写操作,ConcurrentHashMap 在并发访问性能上有了质的提高。在理想状态下,ConcurrentHashMap 可以支持 16 个线程执行并发写操作(如果并发级别设置为 16),及任意数量线程的读操作。

用 HashEntry 对象的不变性来降低读操作对加锁的需求

在 ConcurrentHashMap 中,不允许用 null 作为键和值,当读线程读到某个 HashEntry 的 value 域的值为 null 时,便知道产生了冲突——发生了重排序现象,需要加锁后重新读入这个 value 值。

用 Volatile 变量协调读写线程间的内存可见性

读线程在读取散列表时,基本不需要加锁就能成功获得需要的值。这两个特性相配合,不仅减少了请求同一个锁的频率(读操作一般不需要加锁就能够成功获得值),也减少了持有同一个锁的时间(只有读到 value 域的值为 null 时 , 读线程才需要加锁后重读)。

ConcurrentHashMap 实现高并发的总结

基于通常情形而优化

在实际的应用中,散列表一般的应用场景是:除了少数插入操作和删除操作外,绝大多数都是读取操作,而且读操作在大多数时候都是成功的。正是基于这个前提,ConcurrentHashMap 针对读操作做了大量的优化。通过 HashEntry 对象的不变性和用 volatile 型变量协调线程间的内存可见性,使得 大多数时候,读操作不需要加锁就可以正确获得值。这个特性使得 ConcurrentHashMap 的并发性能在分离锁的基础上又有了近一步的提高。

总结

ConcurrentHashMap 是一个并发散列映射表的实现,它允许完全并发的读取,并且支持给定数量的并发更新。相比于 HashTable 和用同步包装器包装的 HashMap(Collections.synchronizedMap(new HashMap())),ConcurrentHashMap 拥有更高的并发性。在 HashTable 和由同步包装器包装的 HashMap 中,使用一个全局的锁来同步不同线程间的并发访问。同一时间点,只能有一个线程持有锁,也就是说在同一时间点,只能有一个线程能访问容器。这虽然保证多线程间的安全并发访问,但同时也导致对容器的访问变成串行化的了。

在使用锁来协调多线程间并发访问的模式下,减小对锁的竞争可以有效提高并发性。有两种方式可以减小对锁的竞争:

  1. 减小请求 同一个锁的 频率。
  2. 减少持有锁的 时间。

ConcurrentHashMap 的高并发性主要来自于三个方面:

  1. 用分离锁实现多个线程间的更深层次的共享访问。
  2. 用 HashEntry 对象的不变性来降低执行读操作的线程在遍历链表期间对加锁的需求。
  3. 通过对同一个 Volatile 变量的写 / 读访问,协调不同线程间读 / 写操作的内存可见性。

使用分离锁,减小了请求 同一个锁的频率。

通过 HashEntery 对象的不变性及对同一个 Volatile 变量的读 / 写来协调内存可见性,使得 读操作大多数时候不需要加锁就能成功获取到需要的值。由于散列映射表在实际应用中大多数操作都是成功的 读操作,所以 2 和 3 既可以减少请求同一个锁的频率,也可以有效减少持有锁的时间。

通过减小请求同一个锁的频率和尽量减少持有锁的时间 ,使得 ConcurrentHashMap 的并发性相对于 HashTable 和用同步包装器包装的 HashMap有了质的提高。

摘自:https://www.ibm.com/developerworks/cn/java/java-lo-concurrenthashmap/

2.3、ThreadLocal原理分析,为什么会出现OOM

参考:https://blog.csdn.net/zangdaiyang1991/article/details/87927216

2.4、什么是ABA问题,JDK是怎么解决ABA的

ABA:

CAS算法实现一个重要前提需要取出内存中某时刻的数据,而在下时刻比较并替换,那么在这个时间差类会导致数据的变化。

比如说一个线程one从内存位置V中取出A,这时候另一个线程two也从内存中取出A,并且two进行了一些操作变成了B,然后two又将V位置的数据变成A,这时候线程one进行CAS操作发现内存中仍然是A,然后one操作成功。尽管线程one的CAS操作成功,但是不代表这个过程就是没有问题的。如果链表的头在变化了两次后恢复了原值,但是不代表链表就没有变化。因此前面提到的原子操作AtomicStampedReference/AtomicMarkableReference就很有用了。这允许一对变化的元素进行原子操作。

参考:

CAS及ABA问题解决:https://www.cnblogs.com/549294286/p/3766717.html

1、数据库锁

悲观并发控制:(锁粒度划分方式为行锁(for update)、表锁(lock table)、数据库锁等)

(1) 读锁-共享锁

(2) 写锁-互斥锁

两阶段锁协议的两个变种:

  1. Strict 2PL:事务持有的互斥锁必须在提交后再释放;
  2. Rigorous 2PL:事务持有的所有锁必须在提交后释放;

死锁:两个或者多个事务持有对方需要的锁,并等待对方持有的锁。

解决死锁:

1、预防

事务的有向无环图(死锁检测与恢复的手段,环中的一个事务回滚,遵从最小代价原则)

多个锁获取遵循固定统一的规则

2、产生后解决

抢占加事务回滚(抢占:时间戳,谁先拿到谁持有,事务回滚:另一事务回滚)

乐观并发控制:

基于时间戳的协议:保证读写分别按照时间戳串行

基于验证的协议:通过版本号等验证则写入,否则回滚或等待重试

多版本并发控制:

在实际环境中数据库的事务大都是只读的,读请求是写请求的很多倍,如果写请求和读请求之前没有并发控制机制,那么最坏的情况也是读请求读到了已经写入的数据,这对很多应用完全是可以接受的。

在这种大前提下,数据库系统引入了另一种并发控制机制 - 多版本并发控制(Multiversion Concurrency Control),每一个写操作都会创建一个新版本的数据,读操作会从有限多个版本的数据中挑选一个最合适的结果直接返回;在这时,读写操作之间的冲突就不再需要被关注,而管理和快速挑选数据的版本就成了 MVCC 需要解决的主要问题。

MySQL与MVCC:

MySQL 中实现的多版本两阶段锁协议(Multiversion 2PL)将 MVCC 和 2PL 的优点结合了起来,每一个版本的数据行都具有一个唯一的时间戳,当有读事务请求时,数据库程序会直接从多个版本的数据项中具有最大时间戳的返回。

该段摘自:数据库并发控制-锁和MVCC:https://draveness.me/database-concurrency-control

                  数据库锁详解(强烈推荐):https://blog.csdn.net/soonfly/article/details/70238902

2、Java锁

1、synchronized

偏向锁、轻量级锁、自旋锁(乐观锁)

重量级锁(悲观锁)

2、ReentrantLock

读写锁

公平锁

3、共同特性

可重入锁:同步A方法,内部调用同步B方法,可以直接进入,无需重新获取锁

独享锁:线程独享

4、对比

参考:

数据库并发控制-锁和MVCC:https://draveness.me/database-concurrency-control

Java并发编程:Lockhttps://www.cnblogs.com/dolphin0520/p/3923167.html

Java中的锁分类:https://www.cnblogs.com/qifengshi/p/6831055.html

Java锁浅谈:https://blog.csdn.net/u010648018/article/details/79750608

2.5.2、偏向锁等

1.偏向锁是为了避免某个线程反复获得/释放同一把锁时的性能消耗,如果仍然是同个线程去获得这个锁,尝试偏向锁时会直接进入同步块,不需要再次获得锁。

2.而轻量级锁和自旋锁都是为了避免直接调用操作系统层面的互斥操作,因为挂起线程是一个很耗资源的操作。
为了尽量避免使用重量级锁(操作系统层面的互斥),首先会尝试轻量级锁,轻量级锁会尝试使用CAS操作来获得锁,如果轻量级锁获得失败,说明存在竞争。但是也许很快就能获得锁,就会尝试自旋锁,将线程做几个空循环,每次循环时都不断尝试获得锁。如果自旋锁也失败,那么只能升级成重量级锁。

3.可见偏向锁,轻量级锁,自旋锁都是乐观锁。
--------------------- 
摘自:https://blog.csdn.net/u010648018/article/details/79750608 

参考:

java 中的锁 -- 偏向锁、轻量级锁、自旋锁、重量级锁:https://blog.csdn.net/zqz_zqz/article/details/70233767/

                                                                                        https://blog.csdn.net/noble510520/article/details/78834224

                                                                                         

2.6、一个线程连着调用两次start会出现什么状况

第二次调用必然会抛出IllegalThreadStateException,这是一种运行时异常,多次调用start被认为是编程错误。

参考:https://blog.csdn.net/zl1zl2zl3/article/details/80776112

2.7、线程池中任务拒接策略有哪几种?

当超过workQueue的任务缓存区上限的时候,就可以通过该策略处理请求。可以实现自己的拒绝策略,例如记录日志等等,实现RejectedExecutionHandler接口即可。可以拒绝策略有4种:
a. AbortPolicy:直接抛出异常RejectedExecutionException,默认策略
b. CallerRunsPolicy:调用者所在线程来运行该任务,此策略提供简单的反馈控制机制,能够减缓新任务的提交速度。
c. DiscardPolicy:直接丢弃任务
d. DiscardOldestPolicy:如果执行程序尚未关闭,则位于工作队列头部的任务将被删除,然后重新尝试执行任务(如果再次失败,则重复此过程)。
--------------------- 
摘自:https://blog.csdn.net/u013256816/article/details/85697507 

3、设计模式

3.1、动态代理

三种代理模型:https://blog.csdn.net/maoyuanming0806/article/details/80186248

3.2、工厂模式

三种工厂模式:https://blog.csdn.net/llussize/article/details/80276627

3.3、责任链模式

责任链模式:https://blog.csdn.net/qian520ao/article/details/73558275

4、MySQL

4.1、MySQL覆盖索引是什么?

覆盖索引(Covering Indexes):
如果索引包含满足查询的所有数据,就称为覆盖索引。覆盖索引是一种非常强大的工具,能大大提高查询性能。只需要读取索引而不用读取数据有以下一些优点:
(1)索引项通常比记录要小,所以MySQL访问更少的数据;
(2)索引都按值的大小顺序存储,相对于随机访问记录,需要更少的I/O;
(3)大多数据引擎能更好的缓存索引。比如MyISAM只缓存索引。
(4)覆盖索引对于InnoDB表尤其有用,因为InnoDB使用聚集索引组织数据,如果二级索引中包含查询所需的数据,就不再需要在聚集索引中查找了。

InnoDB存储引擎支持覆盖索引,即从辅助索引中就可以得到查询的记录,而不需要查询聚集索引中的记录。

使用覆盖索引有啥好处?

  • 可以减少大量的IO操作

覆盖索引不能是任何索引,只有B-TREE索引存储相应的值。而且不同的存储引擎实现覆盖索引的方式都不同,并不是所有存储引擎都支持覆盖索引(Memory和Falcon就不支持)。
对于索引覆盖查询(index-covered query),使用EXPLAIN时,可以在Extra一列中看到“Using index”。例如,在sakila的inventory表中,有一个组合索引(store_id,film_id),对于只需要访问这两列的查询,MySQL就可以使用索引,如下:

mysql> EXPLAIN SELECT store_id, film_id FROM sakila.inventory\G

*************************** 1. row ***************************

           id: 1

 select_type: SIMPLE

        table: inventory

         type: index

possible_keys: NULL

          key: idx_store_id_film_id

      key_len: 3

          ref: NULL

         rows: 5007

        Extra: Using index

1 row in set (0.17 sec)

在大多数引擎中,只有当查询语句所访问的列是索引的一部分时,索引才会覆盖。但是,InnoDB不限于此,InnoDB的二级索引在叶子节点中存储了primary key的值。因此,sakila.actor表使用InnoDB,而且对于是last_name上有索引,所以,索引能覆盖那些访问actor_id的查询,如:

mysql> EXPLAIN SELECT actor_id, last_name

    -> FROM sakila.actor WHERE last_name = 'HOPPER'\G

*************************** 1. row ***************************

           id: 1

 select_type: SIMPLE

        table: actor

         type: ref

possible_keys: idx_actor_last_name

          key: idx_actor_last_name

      key_len: 137

          ref: const

         rows: 2

        Extra: Using where; Using index

摘自:https://blog.csdn.net/tongdanping/article/details/79878302

MySQL聚簇索引、覆盖索引:https://blog.csdn.net/u012006689/article/details/73195837

MySQL索引实现原理:https://blog.csdn.net/waeceo/article/details/78702584

为什么索引失效了:https://blog.csdn.net/xlgen157387/article/details/79572598

MySQL两个存储引擎区别:https://blog.csdn.net/xlgen157387/article/details/68978320

4.2、explain命令:

EXPLAIN 命令的输出内容大致如下:

mysql> explain select * from user_info where id = 2\G
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: user_info
   partitions: NULL
         type: const
possible_keys: PRIMARY
          key: PRIMARY
      key_len: 8
          ref: const
         rows: 1
     filtered: 100.00
        Extra: NULL
1 row in set, 1 warning (0.00 sec)

各列的含义如下:

  • id: SELECT 查询的标识符. 每个 SELECT 都会自动分配一个唯一的标识符.

  • select_type: SELECT 查询的类型.

  • table: 查询的是哪个表

  • partitions: 匹配的分区

  • type: join 类型

  • possible_keys: 此次查询中可能选用的索引

  • key: 此次查询中确切使用到的索引.

  • ref: 哪个字段或常数与 key 一起被使用

  • rows: 显示此查询一共扫描了多少行. 这个是一个估计值.

  • filtered: 表示此查询条件所过滤的数据的百分比

  • extra: 额外的信息(using filesort 需要排序,需优化;using temporary 使用了临时表,需优化 ;using index 使用了索引)

extra释义:

  • Using where:列数据是从仅仅使用了索引中的信息而没有读取实际的行动的表返回的,这发生在对表的全部的请求列都是同一个索引的部分的时候,表示mysql服务器将在存储引擎检索行后再进行过滤
  • Using temporary:表示MySQL需要使用临时表来存储结果集,常见于排序和分组查询
  • Using filesort:MySQL中无法利用索引完成的排序操作称为“文件排序”
  • Using join buffer:改值强调了在获取连接条件时没有使用索引,并且需要连接缓冲区来存储中间结果。如果出现了这个值,那应该注意,根据查询的具体情况可能需要添加索引来改进能。
  • Impossible where:这个值强调了where语句会导致没有符合条件的行。
  • Select tables optimized away:这个值意味着仅通过使用索引,优化器可能仅从聚合函数结果中返回一行

总结:
• EXPLAIN不会告诉你关于触发器、存储过程的信息或用户自定义函数对查询的影响情况
• EXPLAIN不考虑各种Cache
• EXPLAIN不能显示MySQL在执行查询时所作的优化工作
• 部分统计信息是估算的,并非精确值
• EXPALIN只能解释SELECT操作,其他操作要重写为SELECT后查看执行计划。

参考:

Explain详解:https://www.cnblogs.com/xuanzhi201111/p/4175635.html

                       https://segmentfault.com/a/1190000008131735

4.3、MySQL遇到的死锁问题,怎么排查解决

一、 什么是死锁

死锁是指两个或两个以上的进程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去.此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等的进程称为死锁进程.

二、 死锁产生的四个必要条件

•互斥条件:指进程对所分配到的资源进行排它性使用,即在一段时间内某资源只由一个进程占用。如果此时还有其它进程请求资源,则请求者只能等待,直至占有资源的进程用毕释放

•请求和保持条件:指进程已经保持至少一个资源,但又提出了新的资源请求,而该资源已被其它进程占有,此时请求进程阻塞,但又对自己已获得的其它资源保持不放

•不剥夺条件:指进程已获得的资源,在未使用完之前,不能被剥夺,只能在使用完时由自己释放

•环路等待条件:指在发生死锁时,必然存在一个进程——资源的环形链,即进程集合{P0,P1,P2,···,Pn}中的P0正在等待一个P1占用的资源;P1正在等待P2占用的资源,……,Pn正在等待已被P0占用的资源

这四个条件是死锁的必要条件,只要系统发生死锁,这些条件必然成立,而只要上述条件之一不满足,就不会发生死锁。
 

三、如何预防死锁

阻止死锁的途径就是避免满足死锁条件的情况发生,为此我们在开发的过程中需要遵循如下原则:

1.尽量避免并发的执行涉及到修改数据的语句。

2.要求每一个事务一次就将所有要使用到的数据全部加锁,否则就不允许执行。

3.预先规定一个加锁顺序,所有的事务都必须按照这个顺序对数据执行封锁。如不同的过程在事务内部对对象的更新执行顺序应尽量保证一致。

4.每个事务的执行时间不可太长,对程序段的事务可考虑将其分割为几个事务。在事务中不要求输入,应该在事务之前得到输入,然后快速执行事务。

5.使用尽可能低的隔离级别。

6.数据存储空间离散法。该方法是指采用各种手段,将逻辑上在一个表中的数据分散的若干离散的空间上去,以便改善对表的访问性能。主要通过将大表按行或者列分解为若干小表,或者按照不同的用户群两种方法实现。

7.编写应用程序,让进程持有锁的时间尽可能短,这样其它进程就不必花太长的时间等待锁被释放。

四、解除死锁

解除死锁的两种方法:

(1)终止(或撤销)进程。终止(或撤销)系统中的一个或多个死锁进程,直至打破循环环路,使系统从死锁状态中解除出来。

(2)抢占资源。从一个或多个进程中抢占足够数量的资源,分配给死锁进程,以打破死锁状态。


--------------------- 

摘自:https://blog.csdn.net/qq_34107571/article/details/78001309 

五、出现死锁如何定位

解除正在死锁的状态有两种方法:

第一种:

1.查询是否锁表

show OPEN TABLES where In_use > 0;

2.查询进程(如果您有SUPER权限,您可以看到所有线程。否则,您只能看到您自己的线程)

show processlist

3.杀死进程id(就是上面命令的id列)

kill id

第二种:

1.查看下在锁的事务 

SELECT * FROM INFORMATION_SCHEMA.INNODB_TRX;

2.杀死进程id(就是上面命令的trx_mysql_thread_id列)

kill 线程ID

其它关于查看死锁的命令:

1:查看当前的事务
SELECT * FROM INFORMATION_SCHEMA.INNODB_TRX;

2:查看当前锁定的事务

SELECT * FROM INFORMATION_SCHEMA.INNODB_LOCKS;

3:查看当前等锁的事务
SELECT * FROM INFORMATION_SCHEMA.INNODB_LOCK_WAITS;

摘自:https://blog.csdn.net/yucaifu1989/article/details/79400446

更多参考:

死锁产生原因和解决办法:https://blog.csdn.net/tr1912/article/details/81668423

4.4、MySQL锁、索引、MVCC(重点)

参考:

MySQL锁与索引详解:https://blog.csdn.net/zangdaiyang1991/article/details/88323767

4.4、什么是redo日志、什么是undo日志

redo:是先记录操作日志,虽然记录操作日志也是应用了缓存(innodb_log_buffer)但是它还是比数据更新之前更新redo日志到磁盘日志文件中,数据的更新会在后面的线程刷新操作过程中被更新,redo操作事务提交后只有日志被持久化数据暂时未被持久化。

undo:undo操作也使用了缓存,只是它在事务提交的时候会同时将数据和日志更新到磁盘,这步操作就是和redo的主要区别,并且该操作对磁盘IO的消耗非常大,所以undo操作保证了事务的原子性,事务一旦提交数据也被持久化了。

InnoDB存储引擎的恢复机制:

     必须要将Undo Log持久化,而且必须要在写Redo Log之前将对应的Undo Log写入磁盘。
     Undo和Redo Log的这种关联,使得持久化变得复杂起来。为了降低复杂度,InnoDB将Undo Log看作
     数据,因此记录Undo Log的操作也会记录到redo log中。这样undo log就可以象数据一样缓存起来,
     而不用在redo log之前写入磁盘了。

参考:

Redo与undo日志:https://blog.csdn.net/chast_cn/article/details/50910861

主从复制原理:

1、master在执行sql之后,记录二进制log文件(bin-log)(Binlog dump thread线程(也可称为IO线程))。

2、slave连接master,并从master获取binlog(Slave I/O thread线程),存于本地relay-log中,然后从上次记住的位置起执行SQL语句(Slave SQL thread线程),一旦遇到错误则停止同步。

MySQL主从复制原理:https://blog.csdn.net/xlgen157387/article/details/52451613

4.5、分布式数据库

4.5.1、常见的几种分布式ID的设计方案?

1、数据库自增长序列或字段(缺点:可能有单点故障或性能问题)

2、UUID(缺点:无法保证递增,传输量大,存储空间大,查询效率低)

3、UUID->INT64,添加时间戳,保证有序

4、Redis生成,可以用Redis的原子操作 INCR和INCRBY来实现

5、Twitter的snowflake算法

snowflake是Twitter开源的分布式ID生成算法,结果是一个long型的ID。其核心思想是:使用41bit作为毫秒数,10bit作为机器的ID(5个bit是数据中心,5个bit的机器ID),12bit作为毫秒内的流水号(意味着每个节点在每毫秒可以产生 4096 个 ID),最后还有一个符号位,永远是0。

6、ZooKeeper生成

zookeeper主要通过其znode数据版本来生成序列号,可以生成32位和64位的数据版本号,客户端可以使用这个版本号来作为唯一的序列号。
很少会使用zookeeper来生成唯一ID。主要是由于需要依赖zookeeper,并且是多步调用API,如果在竞争较大的情况下,需要考虑使用分布式锁。因此,性能在高并发的分布式环境下,也不甚理想。

参考:

分布式ID的集中方案:https://www.cnblogs.com/haoxinyue/p/5208136.html

4.5.2、分库和分表带来的分布式困境和应对之策(如何解决分布式下的分库分表、全局表)

参考:https://blog.csdn.net/zdyueguanyun/article/details/60141645

           https://blog.csdn.net/u010963948/article/details/83383826

4.5.3、如何拆分服务,水平分割、垂直分割

整体方案:

优先考虑分区  -->  当分区不能满足需求时,开始考虑分表,合理的分表对效率的提升会优于分区 --> 最后才是分库

1、分区:

就是把一张表的数据分成N个区块,在逻辑上看最终只是一张表,但底层是由N个物理区块组成的

2、分表:

就是把一张表按一定的规则分解成N个具有独立存储空间的实体表。系统读写时需要根据定义好的规则得到对应的字表明,然后操作它。

常用规则:

  1. Range(范围)
  2. Hash(哈希)
  3. 按照时间拆分
  4. Hash之后按照分表个数取模
  5. 在认证库中保存数据库配置,就是建立一个DB,这个DB单独保存user_id到DB的映射关系

3、分库:

垂直分库----->水平分库------------->读写分离

垂直拆分

将系统中不存在关联关系或者需要join的表可以放在不同的数据库不同的服务器中。

按照业务垂直划分。比如:可以按照业务分为资金、会员、订单三个数据库。

需要解决的问题:跨数据库的事务、jion查询等问题。

方案:全局表、字段冗余、系统层组装、事务问题(同分布式事务解决方案)

水平拆分

例如,大部分的站点。数据都是和用户有关,那么可以根据用户,将数据按照用户水平拆分。

按照规则划分,一般水平分库是在垂直分库之后的。比如每天处理的订单数量是海量的,可以按照一定的规则水平划分。

需要解决的问题:数据路由、组装。

解决方案:数据迁移、容量规划、扩容问题、跨分片排序、跨分片函数处理、跨分片join。

读写分离

对于时效性不高的数据,可以通过读写分离缓解数据库压力。

需要解决的问题:在业务上区分哪些业务上是允许一定时间延迟的,以及数据同步问题。

解决方案:负载均衡、DBProxy、主备同步

参考:

MySQL分区分表、读写分离:https://blog.csdn.net/liangz/article/details/79352870

                                                https://blog.csdn.net/will5451/article/details/72617724

千万数据分库分表:https://blog.csdn.net/mingover/article/details/71108852

分布式分库分表:https://blog.csdn.net/zhufuyi/article/details/72637902

数据库表的三种分割方式:https://blog.csdn.net/baidu_21578557/article/details/52384876

4.6、MySQL数据库表的存储结构

myisam 更适合读取大于写入的业务,同时不支持事务。 
innodb 支持事物,效率上比myisam稍慢。

文件存储:

myisam物理文件结构为:

.frm文件:与表相关的元数据信息都存放在frm文件,包括表结构的定义信息等(与引擎无关的文件)。

.myd文件:myisam存储引擎专用,用于存储myisam表的数据

.myi文件:myisam存储引擎专用,用于存储myisam表的索引相关信息

innodb的物理文件结构为:

.frm与表相关的元数据信息都存放在frm文件,包括表结构的定义信息等(与引擎无关的文件)。 
.ibd文件和.ibdata文件: 
这两种文件都是存放innodb数据的文件,之所以用两种文件来存放innodb的数据,是因为innodb的数据存储方式能够通过配置来决定是使用共享表空间存放存储数据,还是用独享表空间存放存储数据。

独享表空间存储方式使用.ibd文件,并且每个表一个ibd文件

共享表空间存储方式使用.ibdata文件,所有表共同使用一个ibdata文件

觉得使用哪种方式的参数在mysql的配置文件中 innodb_file_per_table=ON/OFF
--------------------- 
摘自:https://blog.csdn.net/java_zone/article/details/51645100 

MySQL共享表空间与独立表空间:

1. 简介

    Innodb存储引擎可将所有数据存放于ibdata*的共享表空间,也可将每张表存放于独立的.ibd文件的独立表空间(部分数据)。
    共享表空间以及独立表空间都是针对数据的存储方式而言的。
    共享表空间:  某一个数据库的所有的表数据,索引文件全部放在一个文件中,默认这个共享表空间的文件路径在data目录下。 默认的文件名为:ibdata1  初始化为10M。

    可以在配置文件my.cnf中使用参数innodb_data_file_path设置一个或者多个文件组成表空间。

    共享表空间中会包含Undo信息,在事务未提交时数据即已经写入了表空间文件,当事务rollback时Undo信息不会被删除,但是此空间会被标记,后续会以覆盖的方式被重新使用。

    独立表空间:  每一个表都将会生成以独立的文件方式来进行存储,每一个表都有一个.frm表描述(结构)文件,还有一个.ibd文件。 其中这个文件包括了单独一个表的数据、索引、插入缓冲的内容,其余数据仍存放在共享表空间中,默认情况下独立表空间的存储位置也是在表的位置之中。

    可以在配置文件my.cnf中通过配置参数innodb_file_per_table = ON来开启独立表空间。

2. 共享表空间VS独立表空间
    2.1 共享表空间:

    优点:
    可以将表空间分成多个文件存放到各个磁盘上(表空间文件大小不受表大小的限制,如一个表可以分布在不同的文件上)。数据和文件放在一起方便管理。
    缺点:
    所有的数据和索引存放到一个文件中,虽然可以把一个大文件分成多个小文件,但是多个表及索引在表空间中混合存储,这样对于一个表做了大量删除操作后表空间中将会有大量的空隙,特别是对于统计分析,日值系统这类应用最不适合用共享表空间。

    2.2 独立表空间:


    优点:
    1. 每个表都有自已独立的表空间。
    2. 每个表的数据和索引都会存在自已的表空间中。
    3. 可以实现单表在不同的数据库中移动。
    4. 空间可以回收(除drop table操作处,表空不能自已回收)
        a. Drop table操作自动回收表空间,如果对于统计分析或是日值表,删除大量数据后可以通过:alter table TableName engine=innodb; 回缩不用的空间。
        b. 对于使innodb-plugin的Innodb使用turncate table也会使空间收缩。
        c. 对于使用独立表空间的表,不管怎么删除,表空间的碎片不会太严重的影响性能,而且还有机会处理。
    缺点:
    单表增加过大,如超过100个G。
    相比较之下,使用独占表空间的效率以及性能会更高一点。
--------------------- 
摘自:https://blog.csdn.net/u010472499/article/details/78234452 

InnoDB对比MyISAM的优势:

1、事务支持

2、行级锁支持

3、外键支持

更多:

MyISAM与InnoDB的区别:https://blog.csdn.net/perfectsorrow/article/details/80150672

6、Redis

分布式学习,一致性Hash,ZooKeeper,Redis:https://blog.csdn.net/u013679744/article/category/7424631

6.1、缓存雪崩、缓存穿透、缓存预热、缓存更新、缓存降级

参考:https://blog.csdn.net/xlgen157387/article/details/79530877

6.2、Redis常见的回收策略

Redis内存回收机制主要体现在以下两个方面:

1.删除过期键对象

Redis所有的键都可以设置过期属性,内部保存在过期字典中。由于进程内保存了大量的键,维护每个键精准的过期删除机制会导致消耗大量的CPU,对于单线程的Redis来说成本过高,因此Redis采用惰性删除定时任务删除机制实现过期键的内存回收。

- 惰性删除:惰性删除用于当客户端读取带有超时属性的键时,如果已经超过键设置的过期时间,会执行删除操作并返回空,这种策略是出于节省CPU成本考虑,不需要单独维护TTL链表来处理过期键的删除。但是单独用这种方式存在内存泄露的问题,当过期键一直没有访问将无法得到及时删除,从而导致内存不能及时释放。正因为如此,Redis还提供另一种定时任务删除机制作为惰性删除的补充。
- 定时任务删除:Redis内部维护一个定时任务,默认每秒运行10次(通过配置hz控制)。定时任务中删除过期键逻辑采用了自适应算法,根据键的过期比例,使用快慢两种速率模式回收键。
比如:

  1. 定时任务在每个数据库空间随机检查20个键,当发现过期时删除对应的键。
  2. 如果超过检查数25%的键过期,循环执行回收逻辑直到不足25%或运行超时为止,慢模式下超时时间为25ms。
  3. 如果之前回收键逻辑超时,则在Redis触发内部事件之前再次以快模式运行回收过期键任务,快模式下超时时间为1ms且2s内只能运行1次。
  4. 快慢两种模式内部删除逻辑相同,只是执行的超时时间不同。

2. 内存溢出控制策略

当Redis所用内存达到maxmemory上限时会触发相应的溢出控制策略。具体策略受maxmemory-policy参数控制,Redis支持6种策略,如下所示:

  1. noeviction:默认策略,当内存不足以容纳新写入数据时,新写入操作会报错。应该没人用吧。
  2. allkeys-lru:当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的 Key。推荐使用,目前项目在用这种。
  3. allkeys-random:当内存不足以容纳新写入数据时,在键空间中,随机移除某个 Key。应该也没人用吧,你不删最少使用 Key,去随机删。
  4. volatile-lru:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,移除最近最少使用的 Key。这种情况一般是把 Redis 既当缓存,又做持久化存储的时候才用。不推荐。
  5. volatile-random:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,随机移除某个 Key。依然不推荐。
  6. volatile-ttl:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,有更早过期时间的 Key 优先移除。不推荐。如果没有对应的键,则回退到noeviction策略。

摘自:https://www.jianshu.com/p/6a5eb0ddf57b

Redis其他常见问题概述:

1. 使用Redis有哪些好处?

(1) 速度快,因为数据存在内存中,类似于HashMap,HashMap的优势就是查找和操作的时间复杂度都是O(1)

(2) 支持丰富数据类型,支持string,list,set,sorted set,hash

(3) 支持事务,操作都是原子性,所谓的原子性就是对数据的更改要么全部执行,要么全部不执行

(4) 丰富的特性:可用于缓存,消息,按key设置过期时间,过期后将会自动删除

2. redis相比memcached有哪些优势?

(1) memcached所有的值均是简单的字符串,redis作为其替代者,支持更为丰富的数据类型

(2) redis的速度比memcached快很多

(3) redis可以持久化其数据

3. redis常见性能问题和解决方案:

(1) Master最好不要做任何持久化工作,如RDB内存快照和AOF日志文件

(2) 如果数据比较重要,某个Slave开启AOF备份数据,策略设置为每秒同步一次

(3) 为了主从复制的速度和连接的稳定性,Master和Slave最好在同一个局域网内

(4) 尽量避免在压力很大的主库上增加从库

(5) 主从复制不要用图状结构,用单向链表结构更为稳定,即:Master <- Slave1 <- Slave2 <- Slave3...

这样的结构方便解决单点故障问题,实现Slave对Master的替换。如果Master挂了,可以立刻启用Slave1做Master,其他不变。

----------------

摘自:https://blog.csdn.net/qq_29108585/article/details/63251491

6.3、Hash一致性算法

简单来说,一个物理节点与多个虚拟节点映射,在hash的时候,使用虚拟节点数目而不是物理节点数目。当物理节点变化的时候,虚拟节点的数目无需变化,只涉及到虚拟节点的重新分配。而且,调整每个物理节点对应的虚拟节点数目,也就相当于每个物理节点有不同的权重。

无虚拟节点:

加入虚拟节点,防止雪崩:

图片摘自:https://blog.csdn.net/u013679744/article/details/79166256

6.4、Redis与数据库中数据一致性问题探讨

数据库与缓存读写模式策略

写完数据库后是否需要马上更新缓存还是直接删除缓存?

(1)、如果写数据库的值与更新到缓存值是一样的,不需要经过任何的计算,可以马上更新缓存,但是如果对于那种写数据频繁而读数据少的场景并不合适这种解决方案,因为也许还没有查询就被删除或修改了,这样会浪费时间和资源

(2)、如果写数据库的值与更新缓存的值不一致,写入缓存中的数据需要经过几个表的关联计算后得到的结果插入缓存中,那就没有必要马上更新缓存,只有删除缓存即可,等到查询的时候在去把计算后得到的结果插入到缓存中即可

(3)、分布式锁保证有序

所以一般的策略是当更新数据时,先删除缓存数据,然后更新数据库,而不是更新缓存,等要查询的时候才把最新的数据更新到缓存

数据库与缓存双写情况下导致数据不一致问题

场景一

当更新数据时,如更新某商品的库存,当前商品的库存是100,现在要更新为99,先更新数据库更改成99,然后删除缓存,发现删除缓存失败了,这意味着数据库存的是99,而缓存是100,这导致数据库和缓存不一致。

场景一解决方案

这种情况应该是先删除缓存,然后在更新数据库,如果删除缓存失败,那就不要更新数据库,如果说删除缓存成功,而更新数据库失败,那查询的时候只是从数据库里查了旧的数据而已,这样就能保持数据库与缓存的一致性。

场景二

在高并发的情况下,如果当删除完缓存的时候,这时去更新数据库,但还没有更新完,另外一个请求来查询数据,发现缓存里没有,就去数据库里查,还是以上面商品库存为例,如果数据库中产品的库存是100,那么查询到的库存是100,然后插入缓存,插入完缓存后,原来那个更新数据库的线程把数据库更新为了99,导致数据库与缓存不一致的情况

场景二解决方案

遇到这种情况,可以用队列的去解决这个问,创建几个队列,如20个,根据商品的ID去做hash值,然后对队列个数取摸,当有数据更新请求时,先把它丢到队列里去,当更新完后在从队列里去除,如果在更新的过程中,遇到以上场景,先去缓存里看下有没有数据,如果没有,可以先去队列里看是否有相同商品ID在做更新,如果有也把查询的请求发送到队列里去,然后同步等待缓存更新完成。
这里有一个优化点,如果发现队列里有一个查询请求了,那么就不要放新的查询操作进去了,用一个while(true)循环去查询缓存,循环个200MS左右,如果缓存里还没有则直接取数据库的旧数据,一般情况下是可以取到的。

在高并发下解决场景二要注意的问题
(1)读请求时长阻塞
 由于读请求进行了非常轻度的异步化,所以一定要注意读超时的问题,每个读请求必须在超时间内返回,该解决方案最大的风险在于可能数据更新很频繁,导致队列中挤压了大量的更新操作在里面,然后读请求会发生大量的超时,最后导致大量的请求直接走数据库,像遇到这种情况,一般要做好足够的压力测试,如果压力过大,需要根据实际情况添加机器。
(2)请求并发量过高
 这里还是要做好压力测试,多模拟真实场景,并发量在最高的时候QPS多少,扛不住就要多加机器,还有就是做好读写比例是多少
(3)多服务实例部署的请求路由
可能这个服务部署了多个实例,那么必须保证说,执行数据更新操作,以及执行缓存更新操作的请求,都通过nginx服务器路由到相同的服务实例上
(4)热点商品的路由问题,导致请求的倾斜
某些商品的读请求特别高,全部打到了相同的机器的相同丢列里了,可能造成某台服务器压力过大,因为只有在商品数据更新的时候才会清空缓存,然后才会导致读写并发,所以更新频率不是太高的话,这个问题的影响并不是很大,但是确实有可能某些服务器的负载会高一些。

数据库与缓存数据一致性解决方案流程图


--------------------- 
摘自:https://blog.csdn.net/simba_1986/article/details/77823309 

参考:

redis缓存与数据库一致性:https://blog.csdn.net/qq_27384769/article/details/79499373

简短版本:

1、不一致产生的原因?

我们在是使用redis过程中,通常会这样做,先读取缓存,如果缓存不存在,则读取数据库。

不管是先写库,再删除缓存;还是先删除缓存,再写库,都有可能出现数据不一致的情况。

因为写和读是并发的,没法保证顺序,如果删除了缓存,还没有来得及写库,另一个线程就来读取,发现缓存为空,则去数据库中读取数据写入缓存,此时缓存中为脏数据。如果先写了库,在删除缓存前,写库的线程宕机了,没有删除掉缓存,则也会出现数据不一致情况。

如果是redis集群,或者主从模式,写主读从,由于redis复制存在一定的时间延迟,也有可能导致数据不一致。

2、优化思路

(1)读操作优先读取redis,不存在的话就去访问MySql,并把读到的数据写回Redis中;

(2)写操作的话,直接写MySql,成功后再写入Redis,替换掉原来的旧数据(可以在MySql端定义CRUD触发器,在触发CRUD操作后写数据到Redis,也可以在Redis端解析binlog,再做相应的操作)

(3)设定合理的超时时间,即经过超时时间,自动将redis中相应的数据删除。这样最差的情况是在超时时间内,内存存在不一致。当然这种策略要考虑redis和数据库主从同步的耗时,所以在第二次删除前最好休眠一定的时间,比如500毫秒,这样无疑又增加了写请求的耗时。
--------------------- 

摘自:https://blog.csdn.net/g1607058603/article/details/81544028

6.5、Redis中zSet跳跃表问题

参考:

Redis跳跃表:https://blog.csdn.net/universe_ant/article/details/51134020

                       https://blog.csdn.net/lz710117239/article/details/78408919

6.6、Redis集群如何构建
 

简述:

redis集群原理:

         一,设置redis服务器先经过CRC16哈希到一个指定的Node上范围是0-16384  (平均分配,不能重复也不能缺失,否则会导致对象重复存储或无法存储,比如:三台啊服务器:节点1分配0-5600,节点二分配应该书5601-12000,节点3,12001-16384). 

         二,当数据要保存到redis时,通过CRC16哈希到一个指定RC16哈希值,保存在对应的节点上。

         三,获取,当要获取一个数据时,先通过key获取到RC16哈希值,再通过RC16哈希值找到对应的节点,然后就能在对应的节点马上找到kye的值了。

参考:

Redis集群方案实现:https://blog.csdn.net/yfkiss/article/details/38944179

Redis集群搭建及原理:https://blog.csdn.net/truelove12358/article/details/79612954

三张图了解Redis集群:https://blog.csdn.net/yejingtao703/article/details/78484151

Redis集群搭建:https://blog.csdn.net/u010963948/article/details/78963572

Redis集群搭建:https://www.cnblogs.com/leeSmall/p/8414687.html

新手搭建:https://blog.csdn.net/qq_42815754/article/details/82912130

 

6.7、Redis是什么?为什么?怎么用
 

参考:

Redis概述:https://blog.csdn.net/middleware2018/article/details/80355418

Redis知识点:https://blog.csdn.net/qq_34337272/article/details/80012284

Redis基础及底层实现:https://blog.csdn.net/u013679744/article/details/79195563

6.8、Redis的数据存储方式、操作方法、读写操作在底层怎么实现的

1、数据持久化方式

Redis支持两种数据持久化方式:RDB方式和AOF方式。前者会根据配置的规则定时将内存中的数据持久化到硬盘上,后者则是在每次执行写命令之后将命令记录下来。两种持久化方式可以单独使用,但是通常会将两者结合使用。

(1)、RDB方式

     RDB方式的持久化是通过快照的方式完成的。当符合某种规则时,会将内存中的数据全量生成一份副本存储到硬盘上,这个过程称作”快照”,Redis会在以下几种情况下对数据进行快照:

  • 根据配置规则进行自动快照;
  • 用户执行SAVE, BGSAVE命令;
  • 执行FLUSHALL命令;
  • 执行复制(replication)时。

快照生成原理:

快照执行的过程如下:

(1)Redis使用fork函数复制一份当前进程(父进程)的副本(子进程);
(2)父进程继续处理来自客户端的请求,子进程开始将内存中的数据写入硬盘中的临时文件;
(3)当子进程写完所有的数据后,用该临时文件替换旧的RDB文件,至此,一次快照操作完成。

需要注意的是:

在执行fork的时候操作系统(类Unix操作系统)会使用写时复制(copy-on-write)策略,即fork函数发生的一刻,父进程和子进程共享同一块内存数据,当父进程需要修改其中的某片数据(如执行写命令)时,操作系统会将该片数据复制一份以保证子进程不受影响,所以RDB文件存储的是执行fork操作那一刻的内存数据。所以RDB方式理论上是会存在丢数据的情况的(fork之后修改的的那些没有写进RDB文件)。

(2)、AOF方式

     在使用Redis存储非临时数据时,一般都需要打开AOF持久化来降低进程终止导致的数据丢失,AOF可以将Redis执行的每一条写命令追加到硬盘文件中,这一过程显然会降低Redis的性能,但是大部分情况下这个影响是可以接受的,另外,使用较快的硬盘能提高AOF的性能。

开启AOF

默认情况下,Redis没有开启AOF(append only file)持久化功能,可以通过在配置文件中作如下配置启用:

  • appendonly yes

开启之后,Redis每执行一条写命令就会将该命令写入硬盘中的AOF文件。AOF文件保存路径和RDB文件路径是一致的,都是通过dir参数配置,默认文件名是:appendonly.aof,可以通过配置appendonlyfilename参数修改,例如:

AOF持久化的实现

AOF以纯文本的形式记录了Redis执行的写命令。

AOF文件重写(删除AOF中无用的命令)

      AOF文件是可识别的纯文本,它的内容就是一个个的Redis标准命令,
      AOF日志也不是完全按客户端的请求来生成日志的,比如命令 INCRBYFLOAT 在记AOF日志时就被记成一条SET记录,因为浮点数操作可能在不同的系统上会不同,所以为了避免同一份日志在不同的系统上生成不同的数据集,所以这里只将操作后的结果通过SET来记录。

      每一条写命令都生成一条日志,AOF文件会很大。

     AOF重写是重新生成一份AOF文件,新的AOF文件中一条记录的操作只会有一次,而不像一份老文件那样,可能记录了对同一个值的多次操作。其生成过程和RDB类似,也是fork一个进程,直接遍历数据,写入新的AOF临时文件。在写入新文件的过程中,所有的写操作日志还是会写到原来老的AOF文件中,同时还会记录在内存缓冲区中。当重完操作完成后,会将所有缓冲区中的日志一次性写入到临时文件中。然后调用原子性的rename命令用新的 AOF文件取代老的AOF文件。

 命令:BGREWRITEAOF

同步硬盘数据

     虽然每次执行更改数据库的内容时,AOF都会记录执行的命令,但是由于操作系统本身的硬盘缓存的缘故,AOF文件的内容并没有真正地写入硬盘,在默认情况下,操作系统会每隔30s将硬盘缓存中的数据同步到硬盘,但是为了防止系统异常退出而导致丢数据的情况发生,我们还可以在Redis的配置文件中配置这个同步的频率:

1 # appendfsync always
2 appendfsync everysec
3 # appendfsync no

第一行表示每次AOF写入一个命令都会执行同步操作,这是最安全也是最慢的方式;
第二行表示每秒钟进行一次同步操作,一般来说使用这种方式已经足够;
第三行表示不主动进行同步操作,这是最不安全的方式。

选项:

  1、appendfsync no

  当设置appendfsync为no的时候,Redis不会主动调用fsync去将AOF日志内容同步到磁盘,所以这一切就完全依赖于操作系统的调试了。对大多数Linux操作系统,是每30秒进行一次fsync,将缓冲区中的数据写到磁盘上。

  2、appendfsync everysec

      当设置appendfsync为everysec的时候,Redis会默认每隔一秒进行一次fsync调用,将缓冲区中的数据写到磁盘。但是当这一次的fsync调用时长超过1秒时。Redis会采取延迟fsync的策略,再等一秒钟。也就是在两秒后再进行fsync,这一次的fsync就不管会执行多长时间都会进行。这时候由于在fsync时文件描述符会被阻塞,所以当前的写操作就会阻塞。所以,结论就是:在绝大多数情况下,Redis会每隔一秒进行一次fsync。在最坏的情况下,两秒钟会进行一次fsync操作。这一操作在大多数数据库系统中被称为group commit,就是组合多次写操作的数据,一次性将日志写到磁盘。

  3、appednfsync always

      当设置appendfsync为always时,每一次写操作都会调用一次fsync,这时数据是最安全的,当然,由于每次都会执行fsync,所以其性能也会受到影响。

   建议采用 appendfsync everysec(缺省方式)

  快照模式可以和AOF模式同时开启,互补影响。

(3)、二者的区别

     RDB持久化是指在指定的时间间隔内将内存中的数据集快照写入磁盘,实际操作过程是fork一个子进程,先将数据集写入临时文件,写入成功后,再替换之前的文件,用二进制压缩存储。

 AOF持久化以日志的形式记录服务器所处理的每一个写、删除操作,查询操作不会记录,以文本的方式记录,可以打开文件看到详细的操作记录。

(4)、二者优缺点

RDB存在哪些优势呢?

    1). 一旦采用该方式,那么你的整个Redis数据库将只包含一个文件,这对于文件备份而言是非常完美的。比如,你可能打算每个小时归档一次最近24小时的数据,同时还要每天归档一次最近30天的数据。通过这样的备份策略,一旦系统出现灾难性故障,我们可以非常容易的进行恢复。
    2). 对于灾难恢复而言,RDB是非常不错的选择。因为我们可以非常轻松的将一个单独的文件压缩后再转移到其它存储介质上。
    3). 性能最大化。对于Redis的服务进程而言,在开始持久化时,它唯一需要做的只是fork出子进程,之后再由子进程完成这些持久化的工作,这样就可以极大的避免服务进程执行IO操作了。
    4). 相比于AOF机制,如果数据集很大,RDB的启动效率会更高。
    
RDB又存在哪些劣势呢?

    1). 如果你想保证数据的高可用性,即最大限度的避免数据丢失,那么RDB将不是一个很好的选择。因为系统一旦在定时持久化之前出现宕机现象,此前没有来得及写入磁盘的数据都将丢失。
    2). 由于RDB是通过fork子进程来协助完成数据持久化工作的,因此,如果当数据集较大时,可能会导致整个服务器停止服务几百毫秒,甚至是1秒钟。

AOF的优势有哪些呢? 

  1). 该机制可以带来更高的数据安全性,即数据持久性。Redis中提供了3中同步策略,即每秒同步、每修改同步和不同步。事实上,每秒同步也是异步完成的,其效率也是非常高的,所差的是一旦系统出现宕机现象,那么这一秒钟之内修改的数据将会丢失。而每修改同步,我们可以将其视为同步持久化,即每次发生的数据变化都会被立即记录到磁盘中。可以预见,这种方式在效率上是最低的。至于无同步,无需多言,我想大家都能正确的理解它。
    2). 由于该机制对日志文件的写入操作采用的是append模式,因此在写入过程中即使出现宕机现象,也不会破坏日志文件中已经存在的内容。然而如果我们本次操作只是写入了一半数据就出现了系统崩溃问题,不用担心,在Redis下一次启动之前,我们可以通过redis-check-aof工具来帮助我们解决数据一致性的问题。
    3). 如果日志过大,Redis可以自动启用rewrite机制。即Redis以append模式不断的将修改数据写入到老的磁盘文件中,同时Redis还会创建一个新的文件用于记录此期间有哪些修改命令被执行。因此在进行rewrite切换时可以更好的保证数据安全性。
    4). AOF包含一个格式清晰、易于理解的日志文件用于记录所有的修改操作。事实上,我们也可以通过该文件完成数据的重建。
    
AOF的劣势有哪些呢?
    1). 对于相同数量的数据集而言,AOF文件通常要大于RDB文件。RDB 在恢复大数据集时的速度比 AOF 的恢复速度要快。
    2). 根据同步策略的不同,AOF在运行效率上往往会慢于RDB。总之,每秒同步策略的效率是比较高的,同步禁用策略的效率和RDB一样高效。

   二者选择的标准,就是看系统是愿意牺牲一些性能,换取更高的缓存一致性(aof),还是愿意写操作频繁的时候,不启用备份来换取更高的性能,待手动运行save的时候,再做备份(rdb)。rdb这个就更有些 eventually consistent的意思了。

------------------------

摘自:Redis持久化数据方式对比:https://www.cnblogs.com/xiaoxi/p/7065328.html

2、Redis底层数据结构、读写怎么实现的

参考:

Redis基础及底层实现:https://blog.csdn.net/u013679744/article/details/79195563

Redis持久化数据方式对比:https://www.cnblogs.com/xiaoxi/p/7065328.html

Redis各种数据类型应用场景:https://www.cnblogs.com/xiaoxi/p/7007695.html

6.9、Redis与Memcached对比

参考:

Redis与Memcached比较:https://blog.csdn.net/TiaoZhanJi_Xian/article/details/80301976

Redis与Memcached的比较 ,然后选择了Redis:https://www.sojson.com/blog/108.html

为什么Redis那么快:https://blog.csdn.net/zangdaiyang1991/article/details/86468333

7、Nginx

7.1、解释什么是C10K问题

参考:

C10K问题:https://www.jianshu.com/p/ba7fa25d3590

7.2、正向代理和反向代理

正向代理:

正向代理通过下面的图理解其实就是用户想从服务器拿资源数据,但是只能通过proxy服务器才能拿到,所以用户A只能去访问proxy服务器然后通过proxy服务器去服务器B拿数据,这种情况用户是明确知道你要访问的是谁,在我们生活中最典型的案例就是“翻墙“了,也是通过访问代理服务器最后访问外网的。

反向代理:

反向代理其实就是客户端去访问服务器时,他并不知道会访问哪一台,感觉就是客户端访问了Proxy一样,而实则就是当proxy关口拿到用户请求的时候会转发到代理服务器中的随机(算法)某一台。而在用户看来,他只是访问了Proxy服务器而已,典型的例子就是负载均衡了。

更多参考:

正向反向代理区别:https://blog.csdn.net/zt15732625878/article/details/78941268

7.3、Nginx中常用的几种负载均衡策略

负载均衡策略:

轮询 默认方式
weight 权重方式
ip_hash 依据ip分配方式
least_conn 最少连接方式
fair(第三方) 响应时间方式
url_hash(第三方) 依据URL分配方式

参考:

Nginx中几种负载均衡方式:https://www.cnblogs.com/handongyu/p/6410405.html

                                             http://www.cnblogs.com/1214804270hacker/p/9325150.html

7.4、Nginx服务器中的Master和Worker进程分别是什么,架构及工作原理

参考:

Nginx架构简析:https://www.cnblogs.com/dormant/p/5218266.html

Nginx高性能实现原理:https://www.cnblogs.com/chenjfblog/p/8715580.html

Nginx架构简介:https://blog.csdn.net/zangdaiyang1991/article/details/84424260

7.5、Nginx在项目部署的时候哪些参数是必须要的配置,哪些可以调优

#运行用户
user nobody;
#启动进程,通常设置成和cpu的数量相等
worker_processes  1;
 
#全局错误日志及PID文件
#error_log  logs/error.log;
#error_log  logs/error.log  notice;
#error_log  logs/error.log  info;
 
#pid        logs/nginx.pid;
 
#工作模式及连接数上限
events {
    #epoll是多路复用IO(I/O Multiplexing)中的一种方式,
    #仅用于linux2.6以上内核,可以大大提高nginx的性能
    use   epoll; 
 
    #单个后台worker process进程的最大并发链接数    
    worker_connections  1024;
 
    # 并发总数是 worker_processes 和 worker_connections 的乘积
    # 即 max_clients = worker_processes * worker_connections
    # 在设置了反向代理的情况下,max_clients = worker_processes * worker_connections / 4  为什么
    # 为什么上面反向代理要除以4,应该说是一个经验值
    # 根据以上条件,正常情况下的Nginx Server可以应付的最大连接数为:4 * 8000 = 32000
    # worker_connections 值的设置跟物理内存大小有关
    # 因为并发受IO约束,max_clients的值须小于系统可以打开的最大文件数
    # 而系统可以打开的最大文件数和内存大小成正比,一般1GB内存的机器上可以打开的文件数大约是10万左右
    # 我们来看看360M内存的VPS可以打开的文件句柄数是多少:
    # $ cat /proc/sys/fs/file-max
    # 输出 34336
    # 32000 < 34336,即并发连接总数小于系统可以打开的文件句柄总数,这样就在操作系统可以承受的范围之内
    # 所以,worker_connections 的值需根据 worker_processes 进程数目和系统可以打开的最大文件总数进行适当地进行设置
    # 使得并发总数小于操作系统可以打开的最大文件数目
    # 其实质也就是根据主机的物理CPU和内存进行配置
    # 当然,理论上的并发总数可能会和实际有所偏差,因为主机还有其他的工作进程需要消耗系统资源。
    # ulimit -SHn 65535
 
}
 
 
http {
    #设定mime类型,类型由mime.type文件定义
    include    mime.types;
    default_type  application/octet-stream;
    #设定日志格式
    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"';
 
    access_log  logs/access.log  main;
 
    #sendfile 指令指定 nginx 是否调用 sendfile 函数(zero copy 方式)来输出文件,
    #对于普通应用,必须设为 on,
    #如果用来进行下载等应用磁盘IO重负载应用,可设置为 off,
    #以平衡磁盘与网络I/O处理速度,降低系统的uptime.
    sendfile     on;
    #tcp_nopush     on;
 
    #连接超时时间
    #keepalive_timeout  0;
    keepalive_timeout  65;
    tcp_nodelay     on;
 
    #开启gzip压缩
    gzip  on;
    gzip_disable "MSIE [1-6].";
 
    #设定请求缓冲
    client_header_buffer_size    128k;
    large_client_header_buffers  4 128k;
 
 
    #设定虚拟主机配置
    server {
        #侦听80端口
        listen    80;
        #定义使用 www.nginx.cn访问
        server_name  www.nginx.cn;
 
        #定义服务器的默认网站根目录位置
        root html;
 
        #设定本虚拟主机的访问日志
        access_log  logs/nginx.access.log  main;
 
        #默认请求
        location / {
            
            #定义首页索引文件的名称
            index index.php index.html index.htm;   
 
        }
 
        # 定义错误提示页面
        error_page   500 502 503 504 /50x.html;
        location = /50x.html {
        }
 
        #静态文件,nginx自己处理
        location ~ ^/(images|javascript|js|css|flash|media|static)/ {
            
            #过期30天,静态文件不怎么更新,过期可以设大一点,
            #如果频繁更新,则可以设置得小一点。
            expires 30d;
        }
 
        #PHP 脚本请求全部转发到 FastCGI处理. 使用FastCGI默认配置.
        location ~ .php$ {
            fastcgi_pass 127.0.0.1:9000;
            fastcgi_index index.php;
            fastcgi_param  SCRIPT_FILENAME  $document_root$fastcgi_script_name;
            include fastcgi_params;
        }
 
        #禁止访问 .htxxx 文件
            location ~ /.ht {
            deny all;
        }
 
    }
}

参考:

Nginx基本配置及优化:https://www.cnblogs.com/zhang-shijie/p/5428640.html

                                      https://www.cnblogs.com/knowledgesea/p/5175711.html

7.6、负载均衡常见的几种算法?

1、轮询

2、加权轮询

3、随机

4、加权随机

5、Hash法:根据客户端的IP,或者请求的“Key”,计算出一个hash值,然后对节点数目取模

6、最少连接

参考:

负载均衡的总结及思考:http://www.cnblogs.com/xybaby/p/7867735.html

Web负载均衡方案:https://blog.csdn.net/u012562943/article/details/78247781

几种负载均衡:https://blog.csdn.net/github_37515779/article/details/79953788

Nginx架构简介:https://blog.csdn.net/zangdaiyang1991/article/details/84424260

8、集群分布式

8.1、RPC基本原理、什么是RPC、如何实现RPC、RPC实现原理、什么是gRPC

参考:

什么是RPC:https://blog.csdn.net/xlgen157387/article/details/53543009

8.2、什么是Dubbo,Dubbo的基本原理、执行流程

参考:https://blog.csdn.net/zangdaiyang1991/article/details/86468333

9、搜索引擎

9.1、倒排索引

参考:https://blog.csdn.net/u011239443/article/details/60604017

10、数据结构

10.1、常见排序算法

Java排序:https://blog.csdn.net/EternalInk/article/details/78613269

11、计算机网络基础

11.1、三次握手与四次挥手,为什么挥手需要四次

为什么建立连接协议是三次握手,而关闭连接却是四次握手呢?

这是因为服务端的LISTEN状态下的SOCKET当收到SYN报文的连接请求后,它可以把ACK和SYN(ACK起应答作用,而SYN起同步作用)放在一个报文里来发送。但关闭连接时,当收到对方的FIN报文通知时,它仅仅表示对方没有数据发送给你了;但未必你所有的数据都全部发送给对方了,所以你可能未必会马上会关闭SOCKET,也即你可能还需要发送一些数据给对方之后,再发送FIN报文给对方来表示你同意现在可以关闭连接了,所以它这里的ACK报文和FIN报文多数情况下都是分开发送的。

参考:

三次握手与四次挥手:https://www.cnblogs.com/zmlctt/p/3690998.html

11.2、什么是TCP粘包/拆包,解决办法是什么?

参考:

TCP粘包拆包解决:https://blog.csdn.net/wxy941011/article/details/80428470

                                https://www.cnblogs.com/duan2/p/8858138.html

11.3、从浏览器输入URL到页面加载发生了什么

总体来说分为以下几个过程:

  1. DNS解析

  2. TCP连接

  3. 发送HTTP请求

  4. 服务器处理请求并返回HTTP报文

  5. 浏览器解析渲染页面

  6. 连接结束

参考:

URL输入发生了什么:http://www.cnblogs.com/daijinxue/p/6640153.html

12、操作系统

12.1、BIO、NIO、AIO的概念

参考:

BIO、NIO、AIO:https://blog.csdn.net/u013851082/article/details/53942947

                              https://blog.csdn.net/ty497122758/article/details/78979302

12.2、什么是长连接和短连接

第一个区别是决定的方式,一个TCP连接是否为长连接,是通过设置HTTP的Connection Header(Connection: keep-alive)来决定的,而且是需要两边都设置才有效。而一种轮询方式是否为长轮询,是根据服务端的处理方式来决定的,与客户端没有关系。

第二个区别就是实现的方式,连接的长短是通过协议来规定和实现的。而轮询的长短,是服务器通过编程的方式手动挂起请求来实现的。



摘自:https://www.jianshu.com/p/3fc3646fad80

参考:

长连接、短连接:https://www.cnblogs.com/gotodsp/p/6366163.html

                             https://www.cnblogs.com/cl2Blogs/p/9524427.html

                             https://blog.csdn.net/Ideality_hunter/article/details/77712242

                             

12.3、零拷贝

参考:

零拷贝:https://www.jianshu.com/p/fad3339e3448

12.4、父子进程、孤儿进程、僵死进程

参考:

父子进程:https://www.cnblogs.com/jian-99/p/7719085.html

                  https://blog.csdn.net/renchunlin66/article/details/51931461

浅析孤儿、僵死进程:https://www.cnblogs.com/wannable/p/6021617.html

                                    https://blog.csdn.net/dream_1996/article/details/71001006

13、场景题目

1、算法-KMP算法(一种改进的字符串匹配算法)

参考:

https://blog.csdn.net/x__1998/article/details/79951598

https://www.cnblogs.com/yjiyjige/p/3263858.html

2、算法-topK问题

Top K问题:
1. 有一个1G大小的一个文件,里面每一行是一个词,词的大小不超过16字节,内存限制大小是1M。返回频数最高的100个词。

①分治:顺序读文件,对每个词x取Hash(x)%2000,按照该值存到2000个小文件中。每个文件是500k左右。如果有文件超过了1M则继续分割。O(N)

②Trie树/Hash_map:字符串用Trie树最好。对每个小文件,统计其中出现的词频。O(N)*(平均字符长度),长度一般是常数,也就是O(N). 

③小顶堆:用容量为100的小顶堆,以频率为value值插入,取每个文件现频率最大的100个词,把这100个词及相应的频率存入文件。最差O(N)*lg(100),也就是O(N).注:2,3步骤合起来需要一轮磁盘存取过程。存入文件的个数可以缩减一下,因为主要开销在磁盘读取上,减少文件读取次数,可以在每个文件存取最大容量的字符数量,比如这道题1*(M/16字节字符串长度+频率(int)8字节)的数存到一个文件中。比如20000个词存在一个文件中,可以缩减到10个文件。这样最后一步只需要读取10次就可以了。

④归并:将得到的10个文件里面的数进行归并,取前100个词。注:我觉得其实不需要多路归并,因为只需要找top100的数,归并排序首先是nlgn的复杂度,第二是频繁的磁盘存取,这里最好是还是在内存建立容量为100的小顶堆,依次读文件,遍历每个文件中的元素更新小顶堆,这样只需10次存取,并且时间复杂度是nlog100,也就是O(n)的。

注释:为什么说用Trie树好,我之前一直没想明白,因为网上说Trie树是空间换时间,而这道题是空间敏感呀的。总结了一下,其实是两点我没想明白:

1.字符串会通过一个hash算法(BKDRHash,APHash,DJBHash,JSHash,RSHash,SDBMHash,可以自己看一下,基本就是按位来进行hash的)映射为一个正整数然后对应到hash表中的一个位置,表中记录的value值是次数,这样统计次数只需要将字符串hash一下找到对应位置把次数+1就行了。如果这样的话hash中是不是不用存储字符串本身?如果不存储字符串本身,那应该是比较省空间的。而且效率的话因为Tire树找到一个字符串也是要按位置比较一遍,所以效率差不多呀。但是,其实字符串的hash是要存储字符串本身的,不管是开放地址法还是散列表法,都无法做到不冲突。除非桶个数是字符串的所有情况26^16,那是肯定空间不够的,因此hash表中必须存着字符串的值,也就是key值。字符串本身,那么hash在空间上肯定是定比不过Trie树的,因为Trie树对公共前缀只存储一次。

2.为什么说Trie树是空间换时间呢,我觉得网上这么说不甚合理,这句话其实是相对于二叉查找树来说的,之所以效率高,是因为二叉查找树每次查找都要比较大小,并且因为度为2,查找深度很大,比较次数也多,因此效率差。而Trie树是按位进行hash的,比如26个字母组成的字符串,每次找对应位的字符-‘a’就是位置了。而且度是26,查找深度就是字符串位数,查找起来效率自然就很快。但是为啥说是空间换时间,是因为字符串的Trie树若想存储所有的可能字符串,比如16位,一个点要对应下一位26种情况,也就是26个分支,也得26^16个位置,所以空间是很大的。但是Trie树的话可以采用依次插入的,不需要每个点记录26个点,而是只存在有值的分支,Trie树节点只要存频率次数,插入的流程就是挨个位子找分支,没有就新建,有就次数+1就行了。因此空间上很省,因为重复前缀就统计一次,而效率很高,O(length)。

参考:

TopK问题:https://blog.csdn.net/juzihongle1/article/details/70212243

海量数据中找出TopK:https://www.cnblogs.com/qlky/p/7512199.html

字典(Trie)树:https://www.cnblogs.com/xujian2014/p/5614724.html

3、业务-秒杀场景如何设计

参考:

秒杀系统设计:https://blog.csdn.net/suifeng3051/article/details/52607544

秒杀场景设计:https://www.jianshu.com/p/c4a743bbe3a4

14、安全相关

14.1、如何防范常见的Web攻击,如何防止SQL注入

1、输入输出校验

2、避免拼接用户输入到sql

参考:

常见Web攻击及防范:

https://blog.csdn.net/a401461843/article/details/77622299

https://www.cnblogs.com/miketwais/articles/webhack.html

https://blog.csdn.net/iwebsecurity/article/details/1693877

14.2、什么是XSS攻击,XSS攻击的一般表现形式有哪些,如何防止XSS攻击

脚本攻击

1、输入输出校验

2、输入输出编码

以上题目为Java知识点问答总结前三篇遗漏部分,查漏补缺。

题目参考:https://blog.csdn.net/xlgen157387/article/details/88051362

15、API网关

参考:

API网关介绍:https://www.cnblogs.com/savorboard/p/api-gateway.html

16、云计算相关

1、云计算的业务模式,系统架构,解决方案

1.1、云计算概述

参考:

云计算百度百科:https://baike.baidu.com/item/%E4%BA%91%E8%AE%A1%E7%AE%97

虚拟化百度百科:https://baike.baidu.com/item/%E8%99%9A%E6%8B%9F%E5%8C%96

云计算架构:https://blog.csdn.net/skyboy11yk/article/details/78672294

云计算基础:https://blog.csdn.net/Remoa_Dengqinyi/article/details/72084648

                      https://blog.csdn.net/sophies671207/article/details/78208116

1.2、scale out && scale up架构

参考:https://blog.csdn.net/truong/article/details/73056934

2、弹性计算、多region、容灾、备份方案

2.1、弹性计算

2.2、存储

2.3、数据库

2.4、网络

2.5、中间件

2.6、安全

3、熟悉Linux平台,熟练Python或Shell

17、双重检查单例的缺陷

指令重排序导致返回不完善数据

参考:https://blog.csdn.net/a_842297171/article/details/79316591

18、找出单链表的后N个元素

使用两个指针,一个先走N步,当这个指针到达最后,另一个取出后面几个元素即可。

参考:https://lueye.iteye.com/blog/2176940

19、synchronized锁代码块和锁函数是怎么实现的?有什么不同

区别:

synchronized作用静态方法或使用(xx.class)锁定代码块:类锁,在同个类内,所属线程独占类锁,其他线程阻塞。

synchronized作用于非静态方法或使用(this)作用于代码块:类的实例锁,在同个实例对象内,所属线程独占对象锁,其他线程阻塞;不同的实例对象内无影响。

参考:https://blog.csdn.net/linlvting1314/article/details/79325828

实现原理:

monitorenter  monitorexit指令,enter后不允许其他线程进入,进入一个监控器中等待。

监视器的实现:

当有多个线程同时请求某个对象监视器时,对象监视器会设置几种状态来区分请求的线程:

Contention List:所有请求锁的线程被首先放置在该竞争队列中,
Entry List:Contention List 中有机会获得锁的线程被放置到Entry List
Wait Set:调用wait()方法被阻塞的线程被放置到Wait Set中
OnDeck:任何一个时候只能有一个线程竞争锁 该线程称作OnDeck
Owner:获得锁的线程成为Owner
!Owner:释放锁的线程
转换关系如下图:

参考:

彻底了解synchronized:https://www.jianshu.com/p/d53bf830fa09(强烈推荐)

synchronized实现之对象监视器monitor的实现:https://blog.csdn.net/Thousa_Ho/article/details/77992743

猜你喜欢

转载自blog.csdn.net/zangdaiyang1991/article/details/89893066