字节期间学习笔记

字节期间学习笔记(脱敏版)

一、Java基础

(一)、集合基础

1.1 HashMap

https://zhuanlan.zhihu.com/p/76735726

a、底层数据结构:数组 [<k,v>,<k,v>]

img

哈希表底层数据结构实际上就是数组。它利用数组支持按照下标随机访问的时候,时间复杂度是o(1)的特性。我们通过哈希函数把元素的键值映射为下标,然后将数据存储在数组中对应下标的位置。当我们按照键值查询元素时,我们使用相同的哈希函数,将键值转化为数组下标,从对应的数组下标的位置取出数据。

b、hash算法和寻址算法

hash算法的优化:低十六位的与运算

对每个hash值,将他的高低十六位进行异或操作,让低十六位同时保持了高低十六位的特征。同时也可以避免一些hash值后续出现冲突。

核心是低十六位的与运算

寻址算法的优化:与运算替代取模,提升性能

寻址算法就是对长度为n的数组取模,得到在数组中的位置。根据数学规律,对n取模,就是和n-1进行与运算与运算的效率远远高于求模运算,所以采用与运算。而数组的长度通常没有很大,所以高位与出来都是0,如果不进行hash算法优化,那么高位的信息就会丢失。

hash对n取模的效果-> hash&(n- 1),效果是一样的,后者的性能更高

综上就是JDK8的hash算法的优化。

c、HashMap解决hash碰撞

链表 + 红黑树 ,o(n)和o(logn)

相同的数组位置有多个元素,该数组位置变成链表,在链表里面放入多个元素。

假设链表很长,可能会遍历链表,性能O(n)

优化:当链表长度达到一定的长度之后,我们将链表转换成红黑树,红黑树的查询时间复杂度是O(logn)

d、HashMap的扩容

hashMap底层默认是一个数组,当这个数组满了以后,就会自动扩容,变成一个更大的数组,可以在里面放更多的元素。

hashMap的默认大小是16位的,当16存满以后就会进行2倍扩容,变成长度为32的数组。这个时候就要对原先数组中存储的元素进行rehash,即将他们的哈希值和(32-1)进行与运算,原本在长度为16的处于相同位置的几个元素,可能就要变换位置,不在同样的位置了。

为什么进行两倍扩容?

两倍扩容就是二进制位的上一位变成1,比如

0000 0000 0000 1111

变成

0000 0000 0001 1111

在进行rehash操作时,判断二进制结果是否多了一个bit的1,如果没多,那么就是原来的index,如果多了,那么就是index + oldcap,通过这个方式,避免rehash的时候,进行取模运算,位运算的性能更高。

e、其他集合的底层结构和优劣

List: List是用来存储数据的集合,也就是容器。它的特点是支持存储重复的元素,是有序的。(这里的有序指的是取出的顺序和存储的顺序是相同的。)

Set:set也是用来存储数据的集合,也是一种容器。它的特点是不支持重复元素,实际上java中的set在底层都是通过map来实现的,就是取map中key的那一列,所以只需要理解下面的map就能理解set。

Map

ConcurrentHashMap

我们可以看到向两个不同的位置添加元素,也被锁管理了,这明显是没有必要的,会造成效率低下。我们需要解决这个问题。JDK并发包里推出了ConcurrentHashMap,默认实现了线程的安全性。

在JDK 1.7 版本,它的实现方式是分段加锁,将HashMap在底层的数组分段成几个小数组,然后给每个数组分别加锁。

JDK1.8以及之后,做了一些优化和改进,锁粒度的细化。

这里仍然是一个大的数组,数组中每个元素进行put都是有一个不同的锁,刚开始进行put的时候,如果两个线程都是在数组[5]这个位置进行put,这个时候,对数组[5]这个位置进行put的时候,采取的是CAS策略。

同一时间,只有一个线程能成功执行CAS,其他线程都会失败。

这就实现了分段加锁的第一步,如果很多个线程对数组中不同位置的元素进行操作,大家是互相不会影响的。

如果多个线程对同一个位置进行操作,CAS失败的线程,就会在这个位置基于链表+红黑树来进行处理,synchronized([5]),进行加锁。

综上所述,JDK1.8之后,只有对相同位置的元素操作,才会加锁实行串行化操作,对不同位置进行操作是并发执行的。

ConcurrentHashMap 和 Hashtable 的区别

ConcurrentHashMap 和 Hashtable 的区别主要体现在实现线程安全的方式上不同。

img

img

img

JDK1.8 的 ConcurrentHashMap 不在是 Segment 数组 + HashEntry 数组 + 链表,而是 Node 数组 + 链表 / 红黑树。不过,Node 只能用于链表的情况,红黑树的情况需要使用 TreeNode。当冲突链表达到一定长度时,链表会转换成红黑树。

Collections

Collections 工具类常用方法:

  1. 排序
  2. 查找、替换操作
  3. 同步控制(不推荐,需要线程安全的集合类型时请考虑使用 JUC 包下的并发集合)

Collections 提供了多个synchronizedXxx()方法·,该方法可以将指定集合包装成线程同步的集合,从而解决多线程并发访问集合时的线程安全问题。

最好不要用下面这些方法,效率非常低,需要线程安全的集合类型时请考虑使用 JUC 包下的并发集合。

img

(二)、并发编程基础

多线程并发编程一些问题

说说 sleep() 方法和 wait() 方法区别和共同点?

run()和start()?

new 一个 Thread,线程进入了新建状态。调用 start()方法,会启动一个线程并使线程进入了就绪状态,当分配到时间片后就可以开始运行了。 start() 会执行线程的相应准备工作,然后自动执行 run() 方法的内容,这是真正的多线程工作。

但是,直接执行 run() 方法,会把 run() 方法当成一个 main 线程下的普通方法去执行,并不会在某个线程中执行它,所以这并不是多线程工作。

总结: 调用 start() 方法方可启动线程并使线程进入就绪状态,直接执行 run() 方法的话不会以多线程的方式执行。

并发编程的三个重要特性:

  1. 原子性 : 一个的操作或者多次操作,要么所有的操作全部都得到执行并且不会收到任何因素的干扰而中断,要么所有的操作都执行,要么都不执行。synchronized 可以保证代码片段的原子性。
  2. 可见性 :当一个变量对共享变量进行了修改,那么另外的线程都是立即可以看到修改后的最新值。volatile 关键字可以保证共享变量的可见性。
  3. 有序性 :代码在执行的过程中的先后顺序,Java 在编译器以及运行期间的优化,代码的执行顺序未必就是编写代码时候的顺序。volatile 关键字可以禁止指令进行重排序优化。

synchronized 关键字和 volatile 关键字的区别

synchronized 关键字和 volatile 关键字是两个互补的存在,而不是对立的存在!

volatile为什么不能保证原子性?

简单的说,修改volatile变量分为四步:

1)读取volatile变量到local

2)修改变量值

3)local值写回

4)插入内存屏障,即lock指令,让其他线程可见

这样就很容易看出来,前三步都是不安全的,取值和写回之间,不能保证没有其他线程修改。原子性需要锁来保证。

这也就是为什么,volatile只用来保证变量可见性,但不保证原子性。

2.1 Synchronized

原理:

Synchronized是由JVM实现的一种实现互斥同步的一种方式,如果你查看被Synchronized修饰过的程序块编译后的字节码,会发现,被Synchronized修饰过的程序块,在编译前后被编译器生成了monitorenter和monitorexit两个字节码指令。

在虚拟机执行到monitorenter指令时,首先要尝试获取对象的锁:如果这个对象没有锁定,或者当前线程已经拥有了这个对象的锁,把锁的计数器+1;当执行monitorexit指令时将锁计数器-1;当计数器为0时,锁就被释放了。如果获取对象失败了,那当前线程就要阻塞等待,直到对象锁被另外一个线程释放为止。Java中Synchronize通过在对象头设置标记,达到了获取锁和释放锁的目的。

对象的锁:

“锁”的本质其实是monitorenter和monitorexit字节码指令的一个Reference类型的参数,即要锁定和解锁的对象。

1、如果Synchronized明确指定了锁对象,比如Synchronized(变量名)、Synchronized(this)等,说明加解锁对象为该对象。

2、如果没有明确指定:若Synchronized修饰的方法为非静态方法,表示此方法对应的对象为锁对象;若Synchronized修饰的方法为静态方法,则表示此方法对应的类对象为锁对象。注意,当一个对象被锁住时,对象里面所有用Synchronized修饰的方法都将产生堵塞,而对象里非Synchronized修饰的方法可正常被调用,不受锁影响。

悲观锁乐观锁:

Synchronized显然是一个悲观锁,因为它的并发策略是悲观的:

不管是否会产生竞争,任何的数据操作都必须要加锁、用户态核心态转换、维护锁计数器和检查是否有被阻塞的线程需要被唤醒等操作。

乐观锁的核心算法是CAS(CompareandSwap,比较并交换),它涉及到三个操作数:内存值、预期值、新值。当且仅当预期值和内存值相等时才将内存值修改为新值。这样处理的逻辑是,首先检查某块内存的值是否跟之前我读取时的一样,如不一样则表示期间此内存值已经被别的线程更改过,舍弃本次操作,否则说明期间没有其他线程对此内存值操作,可以把新值设置给此块内存。

乐观锁缺点:

乐观锁避免了悲观锁独占对象的现象,同时也提高了并发性能,但它也有缺点:

1、乐观锁只能保证一个共享变量的原子操作。如果多一个或几个变量,乐观锁将变得力不从心,但互斥锁能轻易解决,不管对象数量多少及对象颗粒度大小。

2、长时间自旋可能导致开销大。假如CAS长时间不成功而一直自旋,会给CPU带来很大的开销。

3、ABA问题。CAS的核心思想是通过比对内存值与预期值是否一样而判断内存值是否被改过,但这个判断逻辑不严谨,假如内存值原来是A,后来被一条线程改为B,最后又被改成了A,则CAS认为此内存值并没有发生改变,但实际上是有被其他线程改过的,这种情况对依赖过程值的情景的运算结果影响很大。解决的思路是引入版本号,每次变量更新都把版本号加一。

CAS机制的理解与其底层:

synchronized关键字会让没有得到锁资源的线程进入BLOCKED状态,而后在争夺到锁资源后恢复为RUNNABLE状态,这个过程中涉及到操作系统用户模式和内核模式的转换,代价比较高。这个过程是一个串行的过程,效率很低。(很多线程都要去排队等待)

尽管JAVA 1.6为synchronized做了优化,增加了从偏向锁到轻量级锁再到重量级锁的过过度,但是在最终转变为重量级锁之后,性能仍然比较低。所以面对这种情况,我们就可以使用java中的“原子操作类”。该原子操作类的底层原理就是CAS。

Atomic包中提供了很多Atomicxxx的类:AtomicInteger、AtomicBoolean等等

CAS机制中使用了3个基本操作数:内存地址V,旧的预期值A,要修改的新值B。

更新一个变量的时候,只有当变量的预期值A和内存地址V当中的实际值相同时,才会将内存地址V对应的值修改为B。(具体实现详细的见上面的博客中介绍)

从思想上来说,synchronized属于悲观锁,悲观的认为程序中的并发情况严重,所以严防死守,CAS属于乐观锁,乐观地认为程序中的并发情况不那么严重,所以让线程不断去重试更新。

说了这么多,CAS是否是完美的呢,答案也是否定的。下面是说一说CAS的缺点:

1) CPU开销过大

在并发量比较高的情况下,如果许多线程反复尝试更新某一个变量,却又一直更新不成功,循环往复,会给CPU带来很到的压力。

2) 不能保证代码块的原子性

CAS机制所保证的只是一个变量的原子性操作,而不能保证整个代码块的原子性。比如需要保证3个变量共同进行原子性的更新,就不得不使用synchronized了。

3) ABA问题

这是CAS机制最大的问题所在。

2.2 ReentrantLock

其实,锁的实现原理基本是为了达到一个目的:让所有的线程都能看到某种标记。

Synchronized通过在对象头中设置标记实现了这一目的,是一种JVM原生的锁实现方式,而ReentrantLock以及所有的基于Lock接口的实现类,都是通过用一个volitile修饰的int型变量,并保证每个线程都能拥有对该int的可见性和原子修改,其本质是基于所谓的AQS框架。

AQS框架:

AQS(AbstractQueuedSynchronizer类)是一个用来构建锁和同步器的框架,各种Lock包中的锁(常用的有ReentrantLock、ReadWriteLock),以及其他如Semaphore、CountDownLatch,甚至是早期的FutureTask等,都是基于AQS来构建。

AQS在内部定义了一个volatile int state变量,表示同步状态:当线程调用lock方法时,如果state=0,说明没有任何线程占有共享资源的锁,可以获得锁并将state=1;如果state=1,则说明有线程目前正在使用共享变量,其他线程必须加入同步队列进行等待。

他们的异同

ReentrantLock是Lock的实现类,是一个互斥的同步锁。

从功能角度,ReentrantLock比Synchronized的同步操作更精细(因为可以像普通对象一样使用)。

从锁释放角度,Synchronized在JVM层面上实现的,不但可以通过一些监控工具监控Synchronized的锁定,而且在代码执行出现异常时,JVM会自动释放锁定;

从性能角度,Synchronized早期实现比较低效,对比ReentrantLock,大多数场景性能都相差较大。在竞争不激烈时,Synchronized的性能要优于ReetrantLock;在高竞争情况下,Synchronized的性能会下降几十倍,但是ReetrantLock的性能能维持常态。

img

非公平锁,就是当线程1结束运行释放锁以后,它去唤醒等待队列中的线程2,但是还没等线程2CAS成功,这时候冒出来一个线程3插队,优先实现加锁,线程2CAS失败,继续等待,这就是非公平锁。

公平锁,就是线程3,在想插队时,会进行判断,等待队列中是否还有线程,如果有它就不能插队,会进入等待队列中排队。这就是公平锁。

2.3 ThreadLocal

通常情况下,我们创建的变量是可以被任何一个线程访问并修改的。如果想实现每一个线程都有自己的专属本地变量该如何解决呢? JDK 中提供的ThreadLocal类正是为了解决这样的问题。 ThreadLocal类主要解决的就是让每个线程绑定自己的值,可以将ThreadLocal类形象的比喻成存放数据的盒子,盒子中可以存储每个线程的私有数据。

如果你创建了一个ThreadLocal变量,那么访问这个变量的每个线程都会有这个变量的本地副本,这也是ThreadLocal变量名的由来。他们可以使用 get() 和 set() 方法来获取默认值或将其值更改为当前线程所存的副本的值,从而避免了线程安全问题。

每个****Thread****中都具备一个****ThreadLocalMap****,而****ThreadLocalMap****可以存储以****ThreadLocal****为 key ,Object 对象为 value 的键值对。

比如我们在同一个线程中声明了两个 ThreadLocal 对象的话,会使用 Thread内部都是使用仅有那个ThreadLocalMap 存放数据的,ThreadLocalMap的 key 就是 ThreadLocal对象,value 就是 ThreadLocal 对象调用set方法设置的值。

ThreadLocal 内存泄露问题

ThreadLocalMap 中使用的 key 为 ThreadLocal 的弱引用,而 value 是强引用。所以,如果 ThreadLocal 没有被外部强引用的情况下,在垃圾回收的时候,key 会被清理掉,而 value 不会被清理掉。这样一来,ThreadLocalMap 中就会出现 key 为 null 的 Entry。假如我们不做任何措施的话,value 永远无法被 GC 回收,这个时候就可能会产生内存泄露。ThreadLocalMap 实现中已经考虑了这种情况,在调用 set()、get()、remove() 方法的时候,会清理掉 key 为 null 的记录。使用完 ThreadLocal方法后 最好手动调用remove()方法

2.4 线程池

系统是不可能频繁的创建线程有销毁线程的,这样会非常影响性能,所以我们需要线程池。

降低资源消耗,复用已创建的线程,降低开销、控制最大并发数。

隔离线程环境,可以配置独立线程池,将较慢的线程与较快的隔离开,避免相互影响。

实现任务线程队列缓冲策略和拒绝机制。

实现某些与时间相关的功能,如定时执行、周期执行等。

img

如果执行完你的任务之后,这个线程是不会死掉的,他会尝试从一个无界的LinkedBlockingQueue里获取新的任务,如果没有新的任务,此时就会阻塞住,等待新的任务到来。

你持续提交任务,上述流程反复执行,只要线程池的线程数量小于corePoolSize,都会直接创建新线程来执行这个任务,执行完了就尝试从无界队列里获取任务,直到线程池里有corePoolSize个线程。

如果我们把queue创建成有界队列,假设corePoolSize所有线程都在繁忙的工作,这个时候仍然有大量的任务进入队列,队列满了,此时怎么办?

这个时候,如果你的maximumPoolSize是比corePoolSize大的,此时线程池就会继续创建额外的线程放入线程池中,来处理这些任务。这些额外创建的线程如果处理完了一个任务也会尝试从队列中获取任务来执行。线程池总共可以创建的线程的数量就是maximumPoolSize。

实现原理:

在Java中,所谓的线程池中的“线程”,其实是被抽象为了一个静态内部类Worker,它基于AQS实现,存放在线程池的HashSetworkers成员变量中;

而需要执行的任务则存放在成员变量workQueue(BlockingQueueworkQueue)中。

整个线程池实现的基本思想就是:

从workQueue中不断取出需要执行的任务,放在Workers中进行处理。

线程池参数

通过配置不同的参数,创建出行为不同的线程池,这几个参数包括:

线程池默认初始化后不启动Worker,等待有请求时才启动。每当我们调用execute()方法添加一个任务时,线程池会做如下判断:

如果正在运行的线程数量小于corePoolSize,那么马上创建线程运行这个任务;

如果正在运行的线程数量大于或等于corePoolSize,那么将这个任务放入队列;如果这时候队列满了,而且正在运行的线程数量小于maximumPoolSize,那么还是要创建非核心线程立刻运行这个任务;

如果队列满了,而且正在运行的线程数量大于或等于maximumPoolSize,那么线程池会抛出异常RejectExecutionException。

当一个线程完成任务时,它会从队列中取下一个任务来执行。当一个线程无事可做,超过一定的时间(keepAliveTime)时,线程池会判断。如果当前运行的线程数大于corePoolSize,那么这个线程就被停掉。所以线程池的所有任务完成后,它最终会收缩到corePoolSize的大小。

线程池队列满了怎么办:

通过上一个问题分析我们发现使用无界队列可能会因为线程处理任务速度比较慢,但是有很多任务堆积导致堆内存溢出。

有界队列不存在堆内存溢出的问题,但同样会有它的问题。如果我们将maximumPoolSize设置的非常大,那么当任务很多时,就会创建很多的额外线程,一台机器上,有几千个,甚至是几万个线程,每个线程都有自己的栈内存,占用一定的内存资源,创建太多的线程会导致栈内存耗尽,可能会产生栈内存溢出。

那么如果我们将maximumPoolSize设置的很小,又会导致额外线程也满了但是任务还是多出队列的限制,此时就会拒绝策略,导致某些任务不能够顺利执行。

这里提供一种自定义策略的思路作为参考,我们可以把这个任务信息持久化写入到磁盘中去,后台专门启动一个线程,后续等待线程池的工作负载降低了,可以慢慢的从磁盘中读取之前持久化的任务,重新提交到线程池中去执行。

综上所述,我们在选用线程池时要综合考虑。

如果线上机器突然宕机,线程池的阻塞队列中的请求怎么办:任务落库

必然会导致线程池中积压的任务都会丢失。

我们可以在提交任务之前,在数据库中插入这个任务的信息,更新任务的状态:未提交、已提交、已完成。提交成功后,更新它的状态是已提交状态。

系统重启后,用一个后台线程去扫描数据库里的未提交和已提交状态的任务,可以把任务的信息读取出来,重新提交到线程池里去,继续进行执行。

线程池的选择策略

可以从以下角度分析:①任务性质:CPU 密集型、IO 密集型和混合型。②任务优先级。③任务执行时间。④任务依赖性:是否依赖其他资源,如数据库连接。

性质不同的任务可用不同规模的线程池处理,CPU 密集型任务应配置尽可能小的线程,如配置 Ncpu+1 个线程的线程池。由于 IO 密集型任务线程并不是一直在执行任务,应配置尽可能多的线程,如 2*Ncpu。混合型的任务,如果可以拆分,将其拆分为一个 CPU 密集型任务和一个 IO 密集型任务,只要两个任务执行的时间相差不大那么分解后的吞吐量将高于串行执行的吞吐量,如果相差太大则没必要分解。

优先级不同的任务可以使用优先级队列 PriorityBlockingQueue 处理。

执行时间不同的任务可以交给不同规模的线程池处理,或者使用优先级队列让执行时间短的任务先执行

依赖数据库连接池的任务,由于线程提交 SQL 后需要等待数据库返回的结果,等待的时间越长 CPU 空闲的时间就越长,因此线程数应该尽可能地设置大一些,提高 CPU 的利用率。

建议使用有界队列,能增加系统的稳定性和预警能力,可以根据需要设置的稍微大一些。

2.5 内存模型

read、load、use、assign、store、write

img

Java的内存模型定义了程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存中取出这样的底层细节。此处的变量包括实例字段、静态字段和构成数组对象的元素,但是不包括局部变量和方法参数,因为这些是线程私有的,不会被共享,所以不存在竞争问题。

可见性、原子性、有序性:

Java中各个线程是怎么彼此看到对方的变量的呢?

Java中定义了主内存与工作内存的概念:所有的变量都存储在主内存,每条线程还有自己的工作内存,保存了被该线程使用到的变量的主内存副本拷贝。线程对变量的所有操作(读取、赋值)都必须在工作内存中进行,不能直接读写主内存的变量。不同的线程之间也无法直接访问对方工作内存的变量,线程间变量值的传递需要通过主内存。

volatile: 使其具备可见性

关键字volatile是Java虚拟机提供的最轻量级的同步机制。当一个变量被定义成volatile之后,具备两种特性:

volatile保证可见性的原理,如果多个线程操作一个被volatile修饰的变量,当其中一个线程成功对组内存中的数据完成修改以后,它会将其他线程工作内存中的该变量的数据设为失效状态,迫使其它线程重新从主内存中读取变量数据,从而实现有可见性。在很多的开源中间件系统的源码里,大量的使用了volatile。常常使用的一个场景是对于一个变量,有的线程要更新它有的线程要读取它来进行判断操作,这个时候就需要使用volatile关键字,来保证读取到最新的数据。

volatile 对比 Synchronized:

Synchronized既能保证可见性,又能保证原子性,而volatile只能保证可见性,无法保证原子性。ThreadLocal和Synchonized都用于解决多线程并发访问,防止任务在共享资源上产生冲突。但是ThreadLocal与Synchronized有本质的区别。

Synchronized用于实现同步机制,是利用锁的机制使变量或代码块在某一时该只能被一个线程访问,是一种“以时间换空间”的方式。

而ThreadLocal为每一个线程都提供了变量的副本,使得每个线程在某一时间访问到的并不是同一个对象,根除了对变量的共享,是一种“以空间换时间”的方式。

2.6 乐观锁与悲观锁

乐观锁对应于生活中乐观的人总是想着事情往好的方向发展,悲观锁对应于生 活中悲观的人总是想着事情往坏的方向发展。这两种人各有优缺点,不能不以 场景而定说一种人好于另外一种人。

悲观锁:

总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿 数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁(共享资 源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线 程)。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。Java 中 synchronized和ReentrantLock 等独占锁就是悲观锁思想的实现。

乐观锁:

总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上 锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号机制和 CAS 算法实现。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库提供的类似于 write_condition 机制,其实都是提供的乐观锁。在 Java 中 java.util.concurrent.atomic 包下面的原子变量类就是使用了乐观锁的一种实现方式 CAS 实现的。

乐观锁适用于写比较少的情况下(多读场景),以一般多写的场景下用悲观锁就比较合适。

乐观锁的实现:

乐观锁的缺点:

2.7 各种锁

  1. 悲观锁

悲观锁并不是某一个锁,是一个锁类型,无论是否并发竞争资源,都会锁住资源,并等待资源释放下一个线程才能获取到锁。 这明显很悲观,所以就叫悲观锁。这明显可以归纳为一种策略,只要符合这种策略的锁的具体实现,都是悲观锁的范畴。

  1. 乐观锁

与悲观锁相对的,乐观锁也是一个锁类型。当线程开始竞争资源时,不是立马给资源上锁,而是进行一些前后值比对,以此来操作资源。例如常见的CAS操作,就是典型的乐观锁。示例如下

  1. 自旋锁

自旋锁是一种基础的同步原语,用于保障对共享数据的互斥访问。与互斥锁的相比,在获取锁失败的时候不会使得线程阻塞而是一直自旋尝试获取锁。当线程等待自旋锁的时候,CPU不能做其他事情,而是一直处于轮询忙等的状态。

  1. 公平锁

多个线程竞争同一把锁,如果依照先来先得的原则,那么就是一把公平锁。

  1. 非公平锁

多个线程竞争锁资源,抢占锁的所有权。

  1. 共享锁

多个线程可以共享这个锁的拥有权。一般用于数据的读操作,防止数据被写修改。

  1. 死锁

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

死锁的产生条件

互斥条件。即某个资源在一段时间内只能由一个进程占有,不能同时被两个或两个以上的进程占有。这种独占资源如CD-ROM驱动器,打印机等等,必须在占有该资源的进程主动释放它之后,其它进程才能占有该资源。这是由资源本身的属性所决定的。如独木桥就是一种独占资源,两方的人不能同时过桥。

不剥夺条件。进程所获得的资源在未使用完毕之前,资源申请者不能强行地从资源占有者手中夺取资源,而只能由该资源的占有者进程自行释放。如过独木桥的人不能强迫对方后退,也不能非法地将对方推下桥,必须是桥上的人自己过桥后空出桥面(即主动释放占有资源),对方的人才能过桥。

请求和保持条件。进程至少已经占有一个资源,但又申请新的资源;由于该资源已被另外进程占有,此时该进程阻塞;但是,它在等待新资源之时,仍继续占用已占有的资源。还以过独木桥为例,甲乙两人在桥上相遇。甲走过一段桥面(即占有了一些资源),还需要走其余的桥面(申请新的资源),但那部分桥面被乙占有(乙走过一段桥面)。甲过不去,前进不能,又不后退;乙也处于同样的状况。

循环等待条件。存在一个进程等待序列{P1,P2,...,Pn},其中P1等待P2所占有的某一资源,P2等待P3所占有的某一源,......,而Pn等待P1所占有的的某一资源,形成一个进程循环等待环。就像前面的过独木桥问题,甲等待乙占有的桥面,而乙又等待甲占有的桥面,从而彼此循环等待。

(三)、JVM

img

  1. Java文件经过编译后变成 .class 字节码文件
  2. 字节码文件通过类加载器被搬运到 JVM 虚拟机中
  3. 虚拟机主要的5大块:方法区,堆都为线程共享区域,有线程安全问题,栈和本地方法栈和计数器都是独享区域,不存在线程安全问题,而 JVM 的调优主要就是围绕堆,栈两大块进行

Main方法的执行步骤

  1. 编译好 App.java 后得到 App.class 后,执行 App.class,系统会启动一个 JVM 进程,从 classpath 路径中找到一个名为 App.class 的二进制文件,将 App 的类信息加载到运行时数据区的方法区内,这个过程叫做 App 类的加载
  2. JVM 找到 App 的主程序入口,执行main方法
  3. 这个main中的第一条语句为 Student student = new Student("tellUrDream") ,就是让 JVM 创建一个Student对象,但是这个时候方法区中是没有 Student 类的信息的,所以 JVM 马上加载 Student 类,把 Student 类的信息放到方法区中
  4. 加载完 Student 类后,JVM 在堆中为一个新的 Student 实例分配内存,然后调用构造函数初始化 Student 实例,这个 Student 实例持有指向方法区中的 Student 类的类型信息的引用
  5. 执行student.sayName();时,JVM 根据 student 的引用找到 student 对象,然后根据 student 对象持有的引用定位到方法区中 student 类的类型信息的方法表,获得 sayName() 的字节码地址。
  6. 执行sayName()

3.1 内存结构

img

类的加载机制:

Java 语言是一种具有动态性的解释型语言,类(Class)只有被加载到 JVM 后才能运行。当运行指定程序时,JVM 会将编译生成 的 .class 文件按照需求和一定的规则加载到内存中,并组织成为一个完整的 Java 应用程序。这个加载过程是由类加载器完成,具体来说,就是由 ClassLoader 和它的子类来实现的。类加载器本 身也是一个类,其实质是把类文件从硬盘读取到内存中。

类的加载方式分为隐式加载和显示加载。隐式加载指的是程序在使 用 new 等方式创建对象时,会隐式地调用类的加载器把对应的类加载到 JVM 中。显示加载指的是通过直接调用 class.forName() 方法来把所需的类加载到 JVM 中。

任何一个工程项目都是由许多类组成的,当程序启动时,只把需要的类加载到 JVM 中,其他类只有被使用到的时候才会被加载,采用这种方法一方面可以加快加载速度,另一方面可以节约程序运行时对内存的开销。此外,在 Java 语言中,每个类或接口都对应一个 .class 文件,这些文件可以被看成是一个个可以被动态加载的单元,因此当只有部分类被修改时,只需要重新编译变化的类即可,而不需要重新编译所有文件,因此加快了编译速度。

在 Java 语言中,类的加载是动态的,它并不会一次性将所有类全部加载后再运行,而是保证程序运行的基础类(例如基类)完全加载到 JVM 中,至于其他类,则在需要的时候才加载。

类加载的步骤:

链接又可分为 3 个小步:

堆的结构:

JVM 的堆是运行时数据区,所有类的实例和数组都是在堆上分配内存。它在 JVM 启动的时候被创建。对象所占的堆内存是由自动内存管理系统也就是垃圾收集器回收。堆内存是由存活和死亡的对象组成的。存活的对象是应用可以访问的,不会被垃圾回收。死亡的对象是应用不可访问尚且还没有被垃圾收集器回收掉的对象。

一直到垃圾收集器把这些 对象回收掉之前,他们会一直占据堆内存空间。

3.2 垃圾回收

img

图中程序计数器、虚拟机栈、本地方法栈,3个区域随着线程的生存而生存的。内存分配和回收都是确定的。随着线程的结束内存自然就被回收了,因此不需要考虑垃圾回收的问题。而Java堆和方法区则不一样,各线程共享,内存的分配和回收都是动态的。因此垃圾收集器所关注的都是堆和方法这部分内存。

垃圾回收

在 Java 中,程序员是不需要显示的去释放一个对象的内存的,而是由虚拟机自行执行。在 JVM 中,有一个垃圾回收线程,它是低优先级的,在正常情况下是不会执行的,只有在虚拟机空闲或者当前堆内存不足时,才会触发执行,扫面那些没有被任何引用的对象,并将它们添加到要回收的集合中,进行回收。

判断对象存活

引用计数法:所谓引用计数法就是给每一个对象设置一个引用计数器,每当有一个地方引用这个对象时,就将计数器加一,引用失效时,计数器就减一。当一个对象的引用计数器为零时,说明此对象没有被引用, 也就是“死对象”,将会被垃圾回收。存在循环引用问题。

可达性算法:(引用链法)该算法的思想是:从一个被称为 GC Roots 的对象开始向下搜索, 如果一个对象到 GC Roots 没有任何引用链相连时,则说明此对象不可用。

img

判断常量是废弃常量

运行时常量池主要回收的是废弃的常量。假如在字符串常量池中存在字符串 "abc",如果当前没有任何 String 对象引用该字符串常量的话,就说明常量 "abc" 就是废弃常量,如果这时发生内存回收的话而且有必要的话,"abc" 就会被系统清理出常量池了。

判断类是无用的类

方法区主要回收的是无用的类,那么如何判断一个类是无用的类的呢?

判定一个常量是否是“废弃常量”比较简单,而要判定一个类是否是“无用的类”的条件则相对苛刻许多。类需要同时满足下面 3 个条件才能算是“无用的类”

虚拟机可以对满足上述 3 个条件的无用类进行回收,这里说的仅仅是“可以”,而并不是和对象一样不使用了就会必然被回收。

img

img

img

对象优先在 eden 区分配

目前主流的垃圾收集器都会采用分代回收算法,因此需要将堆内存分为新生代和老年代,这样我们就可以根据各个年代的特点选择合适的垃圾收集算法。

大多数情况下,对象在新生代中 eden 区分配。当 eden 区没有足够空间进行分配时,虚拟机将发起一次 Minor GC.

大对象直接进入老年代

大对象就是需要大量连续内存空间的对象(比如:字符串、数组)。

为了避免为大对象分配内存时由于分配担保机制带来的复制而降低效率。

长期存活的对象将进入老年代

既然虚拟机采用了分代收集的思想来管理内存,那么内存回收时就必须能识别哪些对象应放在新生代,哪些对象应放在老年代中。为了做到这一点,虚拟机给每个对象一个对象年龄(Age)计数器。

如果对象在 Eden 出生并经过第一次 Minor GC 后仍然能够存活,并且能被 Survivor 容纳的话,将被移动到 Survivor 空间中,并将对象年龄设为 1.对象在 Survivor 中每熬过一次 MinorGC,年龄就增加 1 岁,当它的年龄增加到一定程度(默认为 15 岁),就会被晋升到老年代中。对象晋升到老年代的年龄阈值,可以通过参数 -XX:MaxTenuringThreshold 来设置。

动态对象年龄判定

大部分情况,对象都会首先在 Eden 区域分配,在一次新生代垃圾回收后,如果对象还存活,则会进入 s0 或者 s1,并且对象的年龄还会加 1(Eden 区->Survivor 区后对象的初始年龄变为 1),当它的年龄增加到一定程度(默认为 15 岁),就会被晋升到老年代中。对象晋升到老年代的年龄阈值,可以通过参数

-XX:MaxTenuringThreshold来设置。

主要进行 gc 的区域

针对 HotSpot VM 的实现,它里面的 GC 其实准确分类只有两大种:

部分收集 (Partial GC):

整堆收集 (Full GC):收集整个 Java 堆和方法区。

垃圾收集算法

img

标记-清楚算法

该算法分为“标记”和“清除”阶段:首先标记出所有不需要回收的对象,在标记完成后统一回收掉所有没有被标记的对象。它是最基础的收集算法,后续的算法都是对其不足进行改进得到。这种垃圾收集算法会带来两个明显的问题:

  1. 效率问题
  2. 空间问题(标记清除后会产生大量不连续的碎片)

img

标记-复制算法

为了解决效率问题,“标记-复制”收集算法出现了。它可以将内存分为大小相同的两块,每次使用其中的一块。当这一块的内存使用完后,就将还存活的对象复制到另一块去,然后再把使用的空间一次清理掉。这样就使每次的内存回收都是对内存区间的一半进行回收。

img

标记-整理算法

根据老年代的特点提出的一种标记算法,标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象回收,而是让所有存活的对象向一端移动,然后直接清理掉端边界以外的内存。

img

分代收集算法

当前虚拟机的垃圾收集都采用分代收集算法,这种算法没有什么新的思想,只是根据对象存活周期的不同将内存分为几块。一般将 java 堆分为新生代和老年代,这样我们就可以根据各个年代的特点选择合适的垃圾收集算法。

比如在新生代中,每次收集都会有大量对象死去,所以可以选择”标记-复制“算法,只需要付出少量对象的复制成本就可以完成每次垃圾收集。而老年代的对象存活几率是比较高的,而且没有额外的空间对它进行分配担保,所以我们必须选择“标记-清除”或“标记-整理”算法进行垃圾收集。

3.3 对象的创建

img

Step1:类加载检查

虚拟机遇到一条 new 指令时,首先将去检查这个指令的参数是否能在常量池中定位到这个类的符号引用,并且检查这个符号引用代表的类是否已被加载过、解析和初始化过。如果没有,那必须先执行相应的类加载过程。

Step2:分配内存

类加载检查通过后,接下来虚拟机将为新生对象分配内存。对象所需的内存大小在类加载完成后便可确定,为对象分配空间的任务等同于把一块确定大小的内存从 Java 堆中划分出来。分配方式“指针碰撞”“空闲列表”两种,选择哪种分配方式由 Java 堆是否规整决定,而 Java 堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定

内存分配的两种方式:(补充内容,需要掌握)

选择以上两种方式中的哪一种,取决于 Java 堆内存是否规整。而 Java 堆内存是否规整,取决于 GC 收集器的算法是"标记-清除",还是"标记-整理"(也称作"标记-压缩"),值得注意的是,复制算法内存也是规整的

img

内存分配并发问题(补充内容,需要掌握)

在创建对象的时候有一个很重要的问题,就是线程安全,因为在实际开发过程中,创建对象是很频繁的事情,作为虚拟机来说,必须要保证线程是安全的,通常来讲,虚拟机采用两种方式来保证线程安全:

Step3:初始化零值

内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头),这一步操作保证了对象的实例字段在 Java 代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。

Step4:设置对象头

初始化零值完成之后,虚拟机要对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的 GC 分代年龄等信息。这些信息存放在对象头中。另外,根据虚拟机当前运行状态的不同,如是否启用偏向锁等,对象头会有不同的设置方式。

Step5:执行 init 方法

在上面工作都完成之后,从虚拟机的视角来看,一个新的对象已经产生了,但从 Java 程序的视角来看,对象创建才刚开始,方法还没有执行,所有的字段都还为零。所以一般来说,执行 new 指令之后会接着执行方法,把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算完全产生出来。

3.3 类加载过程

类的生命周期:

img

类的加载过程:

系统加载 Class 类型的文件主要三步:加载->连接->初始化

连接过程又可分为三步:验证->准备->解析

img

要工作如下:

装载:查找和导入类或接口的二进制数据;

链接:执行下面的校验、准备和解析步骤,其中解析步骤是可以选择的;

校验:检查导入类或接口的二进制数据的正确性;

准备:给类的静态变量分配并初始化存储空间;

解析:将符号引用转成直接引用;

初始化:激活类的静态变量的初始化Java代码和静态Java代码块。

加载:

类加载过程的第一步,主要完成下面3件事情:

  1. 通过全类名获取定义此类的二进制字节流
  2. 将字节流所代表的静态存储结构转换为方法区的运行时数据结构
  3. 在内存中生成一个代表该类的 Class 对象,作为方法区这些数据的访问入口

一个非数组类的加载阶段(加载阶段获取类的二进制字节流的动作)是可控性最强的阶段,这一步我们可以去完成还可以自定义类加载器去控制字节流的获取方式(重写一个类加载器的****loadClass()****方法)。数组类型不通过类加载器创建,它由 Java 虚拟机直接创建。

验证:

img

准备:

准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些内存都将在方法区中分配。对于该阶段有以下几点需要注意:

  1. 这时候进行内存分配的仅包括类变量(static),而不包括实例变量,实例变量会在对象实例化时随着对象一块分配在 Java 堆中。
  2. 这里所设置的初始值"通常情况"下是数据类型默认的零值(如0、0L、null、false等),比如我们定义了public static int value=111,那么 value 变量在准备阶段的初始值就是 0 而不是111(初始化阶段才会赋值)。特殊情况:比如给 value 变量加上了 fianl 关键字public static final int value=111,那么准备阶段 value 的值就被赋值为 111。

解析:

解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程,也就是得到类或者字段、方法在内存中的指针或者偏移量。

初始化:

初始化是类加载的最后一步,也是真正执行类中定义的 Java 程序代码(字节码),初始化阶段是执行初始化方法 ()方法的过程。

对于初始化阶段,虚拟机严格规范了有且只有5种情况下,必须对类进行初始化(只有主动去使用类才会初始化类):

  1. > 当遇到 new 、 getstatic、putstatic或invokestatic 这4条直接码指令时,比如 new 一个类,读取一个静态字段(未被 final 修饰)、或调用一个类的静态方法时。
  1. 使用java.lang.reflect包的方法对类进行反射调用时如Class.forname("..."),newInstance()等等。 ,如果类没初始化,需要触发其初始化。

  2. 初始化一个类,如果其父类还未初始化,则先触发该父类的初始化。

  3. 当虚拟机启动时,用户需要定义一个要执行的主类 (包含 main 方法的那个类),虚拟机会先初始化这个类。

  4. MethodHandle和VarHandle可以看作是轻量级的反射调用机制,而要想使用这2个调用, 就必须先使用findStaticVarHandle来初始化要调用的类。

  5. 「补充,来自****issue745****」当一个接口中定义了JDK8新加入的默认方法(被default关键字修饰的接口方法)时,如果有这个接口的实现类发生了初始化,那该接口要在其之前被初始化。

卸载:

卸载类即该类的Class对象被GC。

卸载类需要满足3个要求:

  1. 该类的所有的实例对象都已被GC,也就是说堆不存在该类的实例对象。
  2. 该类没有在其他任何地方被引用
  3. 该类的类加载器的实例已被GC

3.4 JVM参数

显式指定堆内存****–Xms****和****-Xmx

堆:Java 虚拟机所管理的内存中最大的一块,Java 堆是所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例以及数组都在这里分配内存。

与性能有关的最常见实践之一是根据应用程序要求初始化堆内存。

显式新生代内存(Young Ceneration)

在堆总可用内存配置完成之后,第二大影响因素是为Young Generation在堆内存所占的比例。默认情况下,YG 的最小大小为 1310MB,最大大小为无限制

通过-XX:NewSize和-XX:MaxNewSize指定

通过-Xmn[unit] 指定

如果我们要为 新生代分配256m的内存(NewSize与MaxNewSize设为一致),我们的参数应该这样来写:

垃圾收集相关:

JVM具有四种类型的GC实现:

可以使用以下参数声明这些实现:

(四)、IO

Java Io 流共涉及 40 多个类,这些类看上去很杂乱,但实际上很有规则,而且彼此之间存在非常紧密的联系, Java I0 流的 40 多个类都是从如下 4 个抽象类基类中派生出来的。

按操作方式分类结构图:

img

按操作对象分类结构图:

img

NIO:同步非阻塞式IO,服务器实现模式为一个请求一个线程,即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有I/O请求时才启动一个线程进行处理。

BIO: (Blocking I/O)同步阻塞I/O模式,数据的读取写入必须阻塞在一个线程内等待其完成。

AIO:异步非阻塞式IO,服务器实现模式为一个有效请求一个线程,客户端的I/O请求都是由OS先完成了再通知服务器应用去启动线程进行处理。

https://www.cnblogs.com/zedosu/p/6666984.html

(五)、Spring

5.1 IOC机制

img

根据xml配置或者注解,去实例化我们所有的bean,管理bean之间的依赖注入,让类与类之间的耦合性降低,维护代码的时候更加方便轻松。

程序的耦合和解耦 IOC叫控制翻转其目的就是为了降低程序的耦合性

IoC(Inversion of Control)是控制反转的意思,这是一种面向对象编程的设计思想。在不采用这种思想的情况下,我们需要自己维护对象与对象之间的依赖关系,很容易造成对象之间的耦合度过高,在一个大型的项目中这十分的不利于代码的维护。IoC则可以解决这种问题,它可以帮我们维护对象与对象之间的依赖关系,降低对象之间的耦合度。

为什么我们在进行Jdbc注册驱动的时候要使用class.forname()目的就是将编译时期的错误转换成运行时的异常。解耦的解决方案就是将new操作用反射来实现,但是反射就要用到全限定类名来生成对象,我们通过读取配置文件的方式来获取全限定类名,这样就可以避免在全限定类名产生变化时要修改源码。

5.2 AOP机制

AOP(Aspect Oriented Programming)是面向切面编程,它是一种编程思想,是面向对象编程(OOP)的一种补充。面向对象编程将程序抽象成各个层次的对象,而面向切面编程是将程序抽象成各个切面。如下图,可以很形象地看出,所谓切面,相当于应用对象间的横切点,我们可以将其单独抽象为单独的模块。

img

5.3 Spring MVC

MVC是一种设计模式,在这种模式下软件被分为三层,即Model(模型)、View(视图)、Controller(控制器)。Model代表的是数据,View代表的是用户界面,Controller代表的是数据的处理逻辑,它是Model和View这两层的桥梁。将软件分层的好处是,可以将对象之间的耦合度降低,便于代码的维护。

DAO层

DAO是Data Access Obejct的缩写,即数据访问对象,在项目中它通常作为独立的一层,专门用于访问数据库。这一层的具体实现技术有很多,常用的有Spring JDBC、Hibernate、JPA、MyBatis等,在Spring框架下无论采用哪一种技术访问数据库,它的编程模式都是统一的。

Spring MVC的执行流程

  1. 整个过程开始于客户端发出的一个HTTP请求,Web应用服务器接收到这个请求。如果匹配DispatcherServlet的请求映射路径,则Web容器将该请求转交给DispatcherServlet处理。
  2. DispatcherServlet接收到这个请求后,将根据请求的信息(包括URL、HTTP方法、请求报文头、请求参数、Cookie等)及HandlerMapping的配置找到处理请求的处理器(Handler)。可将HandlerMapping看做路由控制器,将Handler看做目标主机。值得注意的是,在Spring MVC中并没有定义一个Handler接口,实际上任何一个Object都可以成为请求处理器。
  3. 当DispatcherServlet根据HandlerMapping得到对应当前请求的Handler后,通过HandlerAdapter对Handler进行封装,再以统一的适配器接口调用Handler。HandlerAdapter是Spring MVC框架级接口,顾名思义,HandlerAdapter是一个适配器,它用统一的接口对各种Handler方法进行调用。
  4. 处理器完成业务逻 辑的处理后,将返回一个ModelAndView给DispatcherServlet,ModelAndView包含了视图逻辑名和模型数据信息。
  5. ModelAndView中包含的是“逻辑视图名”而非真正的视图对象,DispatcherServlet借由ViewResolver完成逻辑视图名到真实视图对象的解析工作。
  6. 当得到真实的视图对象View后,DispatcherServlet就使用这个View对象对ModelAndView中的模型数据进行视图渲染。
  7. 最终客户端得到的响应消息可能是一个普通的HTML页面,也可能是一个XML或JSON串,甚至是一张图片或一个PDF文档等不同的媒体形式。
Spring MVC注解

@RequestMapping****:

作用:该注解的作用就是用来处理请求地址映射的,也就是说将其中的处理器方法映射到url路径上。

属性:

@RequestParam****:

作用:是将请求参数绑定到你的控制器的方法参数上,是Spring MVC中的接收普通参数的注解。

属性:

@RequestBody****:

作用:如果作用在方法上,就表示该方法的返回结果是直接按写入的Http responsebody中(一般在异步获取数据时使用的注解)。

属性:required,是否必须有请求体。它的默认值是true,在使用该注解时,值得注意的当为true时get的请求方式是报错的,如果你取值为false的话,get的请求是null。

@PathVaribale****:

作用:该注解是用于绑定url中的占位符,但是注意,spring3.0以后,url才开始支持占位符的,它是Spring MVC支持的rest风格url的一个重要的标志。

@Transactional

我们知道:Exception分为运行时异常RuntimeException和非运行时异常。事务管理对于企业应用来说是至关重要的,即使出现异常情况,它也可以保证数据的一致性。

当@Transactional注解作用于类上时,该类的所有 public 方法将都具有该类型的事务属性,同时,我们也可以在方法级别使用该标注来覆盖类级别的定义。如果类或者方法加了这个注解,那么这个类里面的方法抛出异常,就会回滚,数据库里面的数据也会回滚。

在@Transactional注解中如果不配置rollbackFor属性,那么事务只会在遇到RuntimeException的时候才会回滚,加上rollbackFor=Exception.class,可以让事务在遇到非运行时异常时也回滚。

处理常见的HTTP请求类型:

5 种常见的请求类型:

@GetMapping("users") 等价于@RequestMapping(value="/users",method=RequestMethod.GET)

@PutMapping("/users/{userId}") 等价于@RequestMapping(value="/users/{userId}",method=RequestMethod.PUT)

@DeleteMapping("/users/{userId}")等价于@RequestMapping(value="/users/{userId}",method=RequestMethod.DELETE)

@value(常用)

使用 @Value("${property}") 读取比较简单的配置信息:

测试相关注解

@ActiveProfiles一般作用于测试类上, 用于声明生效的 Spring 配置文件。

@Test声明一个方法为测试方法

@Transactional被声明的测试方法的数据会回滚,避免污染测试数据。

@WithMockUser Spring Security 提供的,用来模拟一个真实用户,并且可以赋予权限。

Spring MVC的拦截器

拦截器会对处理器进行拦截,这样通过拦截器就可以增强处理器的功能。Spring MVC中,所有的拦截器都需要实现HandlerInterceptor接口,该接口包含如下三个方法:preHandle()、postHandle()、afterCompletion()。

img

通过上图可以看出,Spring MVC拦截器的执行流程如下:

5.4 Bean的生命周期

img

  1. Spring启动,查找并加载需要被Spring管理的bean,进行Bean的实例化
  2. Bean实例化后对将Bean的引入和值注入到Bean的属性中
  3. 如果Bean实现了BeanNameAware接口的话,Spring将Bean的Id传递给setBeanName()方法
  4. 如果Bean实现了BeanFactoryAware接口的话,Spring将调用setBeanFactory()方法,将BeanFactory容器实例传入
  5. 如果Bean实现了ApplicationContextAware接口的话,Spring将调用Bean的setApplicationContext()方法,将bean所在应用上下文引用传入进来。
  6. 如果Bean实现了BeanPostProcessor接口,Spring就将调用他们的postProcessBeforeInitialization()方法。
  7. 如果Bean 实现了InitializingBean接口,Spring将调用他们的afterPropertiesSet()方法。类似的,如果bean使用init-method声明了初始化方法,该方法也会被调用
  8. 如果Bean 实现了BeanPostProcessor接口,Spring就将调用他们的postProcessAfterInitialization()方法。
  9. 此时,Bean已经准备就绪,可以被应用程序使用了。他们将一直驻留在应用上下文中,直到应用上下文被销毁。
  10. 如果bean实现了DisposableBean接口,Spring将调用它的destory()接口方法,同样,如果bean使用了destory-method 声明销毁方法,该方法也会被调用。

5.5 Spring自动装配

我觉得我们可以从以下几个方面回答:

  1. 什么是 SpringBoot 自动装配?
  2. SpringBoot 是如何实现自动装配的?如何实现按需加载?
  3. 如何实现一个 Starter?

没有 Spring Boot 的情况下,如果我们需要引入第三方依赖,需要手动配置,非常麻烦。但是,Spring Boot 中,我们直接引入一个 starter 即可。比如你想要在项目中使用 redis 的话,直接在项目中引入对应的 starter 即可。

引入 starter 之后,我们通过少量注解和一些简单的配置就能使用第三方组件提供的功能了。

在我看来,自动装配可以简单理解为:通过注解或者一些简单的配置就能在 Spring Boot 的帮助下实现某块功能。

大概可以把 @SpringBootApplication看作是 @Configuration、@EnableAutoConfiguration、@ComponentScan 注解的集合。根据 SpringBoot 官网,这三个注解的作用分别是:

(六)、MyBatis

https://snailclimb.gitee.io/javaguide/#/docs/system-design/framework/mybatis/mybatis-interview

MyBatis中的$和#有什么区别?

使用#设置参数时,MyBatis会创建预编译的SQL语句,然后在执行SQL时MyBatis会为预编译SQL中的占位符(?)赋值。预编译的SQL语句执行效率高,并且可以防止注入攻击。

使用$设置参数时,MyBatis只是创建普通的SQL语句,然后在执行SQL语句时MyBatis将参数直接拼入到SQL里。这种方式在效率、安全性上均不如前者,但是可以解决一些特殊情况下的问题。例如,在一些动态表格(根据不同的条件产生不同的动态列)中,我们要传递SQL的列名,根据某些列进行排序,或者传递列名给SQL都是比较常见的场景,这就无法使用预编译的方式了。

MyBatis缓存机制:

一级缓存:

一级缓存也叫本地缓存,它默认会启用,并且不能关闭。一级缓存存在于SqlSession的生命周期中,即它是SqlSession级别的缓存。在同一个 SqlSession 中查询时,MyBatis 会把执行的方法和参数通过算法生成缓存的键值,将键值和查询结果存入一个Map对象中。如果同一个SqlSession 中执行的方法和参数完全一致,那么通过算法会生成相同的键值,当Map 缓存对象中己经存在该键值时,则会返回缓存中的对象。

二级缓存:

二级缓存存在于SqlSessionFactory 的生命周期中,即它是SqlSessionFactory级别的缓存。若想使用二级缓存,需要在如下两处进行配置。

在MyBatis 的全局配置settings 中有一个参数cacheEnabled,这个参数是二级缓存的全局开关,默认值是true ,初始状态为启用状态。

(六)、其他知识

类的加载过程:

在Java中,类装载器把一个类装入Java虚拟机中,要经过三个步骤来完成:装载、链接和初始化,其中链接又可以分成校验、准备和解析三步,除了解析外,其它步骤是严格按照顺序完成的,各个步骤的主要工作如下:

装载:查找和导入类或接口的二进制数据;

链接:执行下面的校验、准备和解析步骤,其中解析步骤是可以选择的;

校验:检查导入类或接口的二进制数据的正确性;

准备:给类的静态变量分配并初始化存储空间;

解析:将符号引用转成直接引用;

初始化:激活类的静态变量的初始化Java代码和静态Java代码块。

Class.forName(className)方法,其实调用的方法是Class.forName(className,true,classloader);注意看第2个boolean参数,它表示的意思,在loadClass后必须初始化。比较下我们前面准备jvm加载类的知识,我们可以清晰的看到在执行过此方法后,目标对象的 static块代码已经被执行,static参数也已经被初始化。

再看ClassLoader.loadClass(className)方法,其实他调用的方法是ClassLoader.loadClass(className,false);还是注意看第2个 boolean参数,该参数表示目标对象被装载后不进行链接,这就意味着不会去执行该类静态块中间的内容。因此2者的区别就显而易见了。

最后还有必要在此提一下new方法和newInstance方法的区别

newInstance: 弱类型。低效率。只能调用无参构造。

new: 强类型。相对高效。能调用任何public构造。

异常:

img

java.lang.Throwable是所有异常的根

java.lang.Error是错误信息

java.lang.Exception是异常信息

我们比较熟悉的Checked异常有

Java.lang.ClassNotFoundException

Java.lang.NoSuchMetodException

java.io.IOException

我们比较熟悉的RumtimeException子类:

Java.lang.ArithmeticException

Java.lang.ArrayStoreExcetpion

Java.lang.ClassCastException

Java.lang.IndexOutOfBoundsException

Java.lang.NullPointerException

hashcode:

hashcode()方法提供了对象的hashCode值,是一个native方法,返回的默认值与System.identityHashCode(obj)一致。

通常这个值是对象头部的一部分二进制位组成的数字,具有一定的标识对象的意义存在,但绝不定于地址。

作用是:用一个数字来标识对象。比如在HashMap、HashSet等类似的集合类中,如果用某个对象本身作为Key,即要基于这个对象实现Hash的写入和查找,那么对象本身如何实现这个呢?就是基于hashcode这样一个数字来完成的,只有数字才能完成计算和对比操作。

equals相等两个对象,则hashcode一定要相等。但是hashcode相等的两个对象不一定equals相等。

hashcode是为了算法快速定位数据而存在的,而equals是为了对比真实值而存在的。

二、中间件

(一)、消息队列

1.1 通信方式

在p2p的场景里,相互通信的双方是通过一个类似于队列的方式来进行交流。和前面pub-sub的区别在于一个topic有一个发送者和多个接收者,而在p2p里一个queue只有一个发送者和一个接收者。

消息丢失怎么办:

防止重复消费:

消息通信的基本方式:

同步方式:两个通信应用服务之间必须要进行同步,两个服务之间必须都是正常运行的。发送程序和接收程序都必须一直处于运行状态,并且随时做好相互通信的准备。发送程序首先向接收程序发起一个请求,称之为发送消息,发送程序紧接着就会堵塞当前自身的进程,不与其他应用进行任何的通信以及交互,等待接收程序的响应,待发送消息得到接收程序的返回消息之后会继续向下运行,进行下一步的业务处理。

异步方式:两个通信应用之间可以不用同时在线等待,任何一方只需各自处理自己的业务,比如发送方发送消息以后不用登录接收方的响应,可以接着处理其他的任务。也就是说发送方和接收方都是相互独立存在的,发送方只管方,接收方只能接收,无须去等待对方的响应。

1.2 性能调优

1.3 死信队列

如果你想在消息处理失败后,不被服务器删除,还能被其他消费者处理或重试,可以关闭AUTO_ACKNOWLEDGE,将ack交由程序自己处理。那如果使用了AUTO_ACKNOWLEDGE,消息是什么时候被确认的,还有没有阻止消息确认的方法?

消费消息有2种方法,一种是调用consumer.receive()方法,该方法将阻塞直到获得并返回一条消息。这种情况下,消息返回给方法调用者之后就自动被确认了。另一种方法是采用listener回调函数,在有消息到达时,会调用listener接口的onMessage方法。在这种情况下,在onMessage方法执行完毕后,消息才会被确认,此时只要在方法中抛出异常,该消息就不会被确认。那么问题来了,如果一条消息不能被处理,会被退回服务器重新分配,如果只有一个消费者,该消息又会重新被获取,重新抛异常。就算有多个消费者,往往在一个服务器上不能处理的消息,在另外的服务器上依然不能被处理。难道就这么退回-获取--报错死循环了吗?

在重试6次后,ActiveMQ认为这条消息是“有毒”的,将会把消息丢到死信队列里。如果你的消息不见了,去ActiveMQ.DLQ里找找,说不定就躺在那里。

1.4 消息队列

为什么要使用消息队列?

其实就是问问你消息队列都有哪些使用场景,然后你项目里具体是什么场景,说说你在这个场景里用消息队列是什么。

解耦:发布订阅模型

img

img

异步:消息发出去就不管了

img

img

削峰填谷:

img

img

消息队列的缺点?

系统可用性降低:系统引入的外部依赖越多,越容易挂掉,本来你就是A系统调用BCD三个系统的接口就好了,人ABCD四个系统好好的,没啥问题,你偏加个MQ进来,万一MQ挂了咋整?MQ挂了,整套系统崩溃了,你不就完了么。

系统复杂性提高:硬生生加个MQ进来,你怎么保证消息没有重复消费?怎么处理消息丢失的情况?怎么保证消息传递的顺序性?头大头大,问题一大堆,痛苦不已

一致性问题:A系统处理完了直接返回成功了,人都以为你这个请求就成功了;但是问题是,要是BCD三个系统那里,BD两个系统写库成功了,结果C系统写库失败了,咋整?你这数据就不一致了。

img

多种MQ的对比:

一般的业务系统要引入MQ,最早大家都用ActiveMQ,但是现在确实大家用的不多了,没经过大规模吞吐量场景的验证,社区也不是很活跃,所以大家还是算了吧,我个人不推荐用这个了;

后来大家开始用RabbitMQ,但是确实erlang语言阻止了大量的java工程师去深入研究和掌控他,对公司而言,几乎处于不可控的状态,但是确实人是开源的,比较稳定的支持,活跃度也高;

不过现在确实越来越多的公司,会去用RocketMQ,确实很不错,但是我提醒一下自己想好社区万一突然黄掉的风险,对自己公司技术实力有绝对自信的,我推荐用RocketMQ,否则回去老老实实用RabbitMQ吧,人是活跃开源社区,绝对不会黄

所以中小型公司,技术实力较为一般,技术挑战不是特别高,用RabbitMQ是不错的选择;大型公司,基础架构研发实力较强,用RocketMQ是很好的选择

如果是大数据领域的实时计算、日志采集等场景,用Kafka是业内标准的,绝对没问题,社区活跃度很高,绝对不会黄,何况几乎是全世界这个领域的事实性规范

如何保证消息队列的高可用 ?

(二)、Redis

Redis存在的问题:

有强一致性要求的数据,不能放缓存。采取正确更新策略,先更新数据库,再删缓存。其次,因为可能存在删除缓存失败的问题,提供一个补偿措施即可,例如利用消息队列。

1)如果对这个key操作,不要求顺序这种情况下,准备一个分布式锁,大家去抢锁,抢到锁就做set操作即可,比较简单。

(2)如果对这个key操作,要求顺序假设有一个key1,系统A需要将key1设置为valueA,系统B需要将key1设置为valueB,系统C需要将key1设置为valueC. 期望按照key1的value值按照 valueA-->valueB-->valueC的顺序变化。这种时候我们在数据写入数据库的时候,需要保存一个时间戳。

假设这会系统B先抢到锁,将key1设置为{valueB 3:05}。接下来系统A抢到锁,发现自己的valueA的时间戳早于缓存中的时间戳,那就不做set操作了。以此类推。

Redis相比Memcached的优势:

Redis:单线程异步NIO

img

为啥Redis单线程效率还有这么高?

1)纯内存操作

2)核心是基于非阻塞的IO多路复用机制

3)单线程反而避免了多线程的频繁上下文切换问题(百度)

img

Redis6.0 之后为何引入了多线程?

Redis6.0 引入多线程主要是为了提高网络 IO 读写性能,因为这个算是 Redis 中的一个性能瓶颈(Redis 的瓶颈主要受限于内存和网络)。Redis6.0 引入了多线程,但是 Redis 的多线程只是在网络数据的读写这类耗时操作上使用了, 执行命令仍然是单线程顺序执行。因此,你也不需要担心线程安全问题。

2.1 数据类型

(1)string

就是普通的set和get,做简单的kv缓存

(2)hash

这个是类似map的一种结构,这个一般就是可以将结构化的数据,比如一个对象(前提是这个对象没嵌套其他的对象)给缓存在redis里,然后每次读写缓存的时候,可以就操作hash里的某个字段。

hash类的数据结构,主要是用来存放一些对象,把一些简单的对象给缓存起来,后续操作的时候,你可以直接仅仅修改这个对象中的某个字段的值

(3)list

介绍list即是链表。链表是一种非常常见的数据结构,特点是易于数据元素的插入和删除并且且可以灵活调整链表长度,但是链表的随机访问困难。许多高级编程语言都内置了链表的实现比如 Java 中的LinkedList,但是 C 语言并没有实现链表,所以 Redis 实现了自己的链表数据结构。Redis 的 list 的实现为一个双向链表,即可以支持反向查找和遍历,更方便操作,不过带来了部分额外的内存开销。

常用命令:rpush,lpop,lpush,rpop,lrange、llen等。

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

有序列表,这个是可以玩儿出很多花样的

微博,某个大v的粉丝,就可以以list的格式放在redis里去缓存

比如可以通过list存储一些列表型的数据结构,类似粉丝列表了、文章的评论列表了之类的东西

比如可以通过lrange命令,就是从某个元素开始读取多少个元素,可以基于list实现分页查询,这个很棒的一个功能,基于redis实现简单的高性能分页,可以做类似微博那种下拉不断分页的东西,性能高,就一页一页走

比如可以搞个简单的消息队列,从list头怼进去,从list尾巴那里弄出来

(4)set

无序集合,自动去重

直接基于set将系统里需要去重的数据扔进去,自动就给去重了,如果你需要对一些数据进行快速的全局去重,你当然也可以基于jvm内存里的HashSet进行去重,但是如果你的某个系统部署在多台机器上呢

得基于redis进行全局的set去重

可以基于set玩儿交集、并集、差集的操作,比如交集吧,可以把两个人的粉丝列表整一个交集,看看俩人的共同好友是谁?对吧

把两个大v的粉丝都放在两个set中,对两个set做交集

(5)sorted set

排序的set,去重但是可以排序,写进去的时候给一个分数,自动根据分数排序,这个可以玩儿很多的花样,最大的特点是有个分数可以自定义排序规则

比如说你要是想根据时间对数据排序,那么可以写入进去的时候用某个时间作为分数,人家自动给你按照时间排序了

排行榜:将每个用户以及其对应的什么分数写入进去,zadd board score username,接着zrevrange board 0 99,就可以获取排名前100的用户;zrank board username,可以看到用户在排行榜里的排名

2.2 过期策略

定期删除:

指的是redis默认是每隔100ms就随机抽取一些设置了过期时间的key,检查其是否过期,如果过期就删除。定期删除可能会导致很多过期key到了时间并没有被删除掉 - > 惰性删除

惰性删除:

并不是key到时间就被删除掉,而是你查询这个key的时候,redis再懒惰的检查一下。

如果定期删除漏掉了很多过期key,然后你也没及时去查,也就没走惰性删除,此时会怎么样?如果大量过期key堆积在内存里,导致redis内存块耗尽了,咋整? - > 内存淘汰机制

Redis的内存淘汰机制:

noeviction:当内存不足以容纳新写入数据时,新写入操作会报错

allkeys-lru:当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的key(这个是最常用的)

allkeys-random:当内存不足以容纳新写入数据时,在键空间中,随机移除某个key,这个一般没人用吧,为啥要随机,肯定是把最近最少使用的key给干掉啊

volatile-lru:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,移除最近最少使用的key(这个一般不太合适)

volatile-random:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,随机移除某个key

volatile-ttl:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,有更早过期时间的key优先移除

手写LRU算法:

Redis的适合场景:

使用缓存会出现的问题:

1)缓存与数据库双写不一致

2)缓存雪崩

3)缓存穿透

4)缓存并发竞争

2.3 持久化

快照(RDB)持久化:通过 bgsave 命令触发,然后父进程执行 fork 操作创建子进程,子进程创建 RDB 文件,根据父进程内存生成临时快照文件,完成后对 原有文件进行原子替换(定时一次性将所有数据进行快照生成一份副本存储在硬 盘中)

优点:是一个紧凑压缩的二进制文件,Redis 加载 RDB 恢复数据远远快于 AOF 的方式。

缺点:由于每次生成 RDB 开销较大,非实时持久化

AOF 持久化:开启后,Redis 每执行一个修改数据的命令,都会把这个命令添加到 AOF 文件中。

优点:实时持久化。

缺点:所以 AOF 文件体积逐渐变大,需要定期执行重写操作来降低文件体积, 加载慢

2.4 哨兵模式

就是通过哨兵节点进行自主监控主从节点以及其他哨兵节点,发现主节点故障时自主进行故障转移。

三个定时监控任务:

每隔 10s,每个 S 节点(哨兵节点)会向主节点和从节点发送 info 命令获 取最新的拓扑结构。

每隔 2s,每个 S 节点会向某频道上发送该 S 节点对于主节点的判断以及当 前 Sl 节点的信息, 同时每个 Sentinel 节点也会订阅该频道,来了解其他 S 节点以及它们对主节点 的判断(做客观下线依据)。

每隔 1s,每个 S 节点会向主节点、从节点、其余 S 节点发送一条 ping 命令 做一次心跳检测(心跳检测机制),来确认这些节点当前是否可达。

主客观下线:

主观下线:根据第三个定时任务对没有有效回复的节点做主观下线处理。

客观下线:若主观下线的是主节点,会咨询其他 S 节点对该主节点的判断, 超过半数,对该主节点做客观下线。

故障转移:

选举出某一哨兵节点作为领导者,来进行故障转移。选举方式:raft 算法。每个 S 节点有一票同意权,哪个 S 节点做出主观下线的时候,就会询问其 他 S 节点是否同意其为领导者。获得半数选票的则成为领导者。基本谁先做出客 观下线,谁成为领导者。

2.5 缓存穿透

用户的请求最终都要跑到数据库中查询一遍

最基本的就是首先做好参数校验,一些不合法的参数请求直接抛出异常信息返回给客户端。

1)缓存无效key

如果缓存和数据库都查不到某个 key 的数据就写一个到 Redis 中去并设置过期时间,具体命令如下:SET key value EX 10086。这种方式可以解决请求的 key 变化不频繁的情况,如果黑客恶意攻击,每次构建不同的请求 key,会导致 Redis 中缓存大量无效的 key 。很明显,这种方案并不能从根本上解决此问题。如果非要用这种方式来解决穿透问题的话,尽量将无效的 key 的过期时间设置短一点比如 1 分钟。

2)布隆过滤器

布隆过滤器是一个非常神奇的数据结构,通过它我们可以非常方便地判断一个给定数据是否存在于海量数据中。我们需要的就是判断 key 是否合法。

img

当一个元素加入布隆过滤器中的时候,会进行哪些操作:

当我们需要判断一个元素是否存在于布隆过滤器的时候,会进行哪些操作:

2.6 缓存雪崩

缓存在同一时间大面积的失效,请求都直接落到了数据库上,造成数据库短时间内承受大量请求。

系统的缓存模块出了问题比如宕机导致不可用。造成系统的所有访问,都要走数据库。

解决办法:

针对 Redis 服务不可用的情况:

  1. 采用 Redis 集群,避免单机出现问题整个缓存服务都没办法使用。
  2. 限流,避免同时处理大量的请求。

针对热点缓存失效的情况:

  1. 设置不同的失效时间比如随机设置缓存的失效时间。
  2. 缓存永不失效。

2.7 缓存读写策略

旁路缓存模式

是我们平时使用比较多的一个缓存读写模式,比较适合读请求比较多的场景。服务端需要同时维系 DB 和 cache,并且是以 DB 的结果为准。

:

读写穿透

务端把 cache 视为主要数据存储,从中读取数据并将数据写入其中。cache 服务负责将此数据读取和写入 DB,从而减轻了应用程序的职责。

这种缓存读写策略小伙伴们应该也发现了在平时在开发过程中非常少见。抛去性能方面的影响,大概率是因为我们经常使用的分布式缓存 Redis 并没有提供 cache 将数据写入DB的功能。

写(Write Through):

读(Read Through):

异步缓存写入

和读写穿透相似,两者都是由 cache 服务来负责 cache 和 DB 的读写。

Read/Write Through 是同步更新 cache 和 DB,而 Write Behind Caching 则是只更新缓存,不直接更新 DB,而是改为异步批量的方式来更新 DB。

利用Redis实现分布式Session

没有分布式Session的情况:

img

我们使用session保存用户的身份信息,本质上是要做两件事情。第一是保存用户的身份信息,第二是验证用户的身份信息。如果利用其它手段实现这两个目标,那么就可以不用session,或者说我们使用的是广义上的session了。

img

三、网络

(一)、层级结构

img

应用层:通过应用进程间的交互来完成特定网络应用。(DNS、HTTP)

运输层:负责向两台主机进程之间的通信提供通用的数据传输服务。(TCP、UDP)

网络层:计算机网络中进行通信的两个计算机之间可能会经过很多个数据链路,也可能还要经过很多通信子网。网络层的任务就是选择合适的网间路由和交换结点, 确保数据及时传送。

数据链路层:两台主机之间的数据传输,总是在一段一段的链路上传送的,数据链路层将网络层交下来的 IP 数据报组装成帧,在两个相邻节点间的链路上传送帧。

物理层:实现相邻计算机节点之间比特流的透明传送,尽可能屏蔽掉具体传输介质和物理设备的差异。

img

(二)、三握四挥

三次握手

为了准确无误地把数据送达目标处,TCP协议采用了三次握手策略。

三次握手的目的是建立可靠的通信信道,说到通讯,简单来说就是数据的发送与接收,而三次握手最主要的目的就是双方确认自己与对方的发送与接收是正常的。

img

接收端传回发送端所发送的ACK是为了告诉客户端,我接收到的信息确实就是你所发送的信号了,这表明从客户端到服务端的通信是正常的。而回传SYN则是为了建立并确认从服务端到客户端的通信。

四次挥手

img

断开一个 TCP 连接则需要“四次挥手”:

任何一方都可以在数据传送结束后发出连接释放的通知,待对方确认后进入半关闭状态。当另一方也没有数据再发送的时候,则发出连接释放通知,对方确认后就完全关闭了TCP连接。

TCP 滑动窗口

窗口是缓存的一部分,用来暂时存放字节流。发送方和接收方各有一个窗口,接收方通过 TCP 报文段中的窗口字段告诉发送方自己的窗口大小,发送方根据这个值和其它信息设置自己的窗口大小。

发送窗口内的字节都允许被发送,接收窗口内的字节都允许被接收。如果发送窗口左部的字节已经发送并且收到了确认,那么就将发送窗口向右滑动一定距离,直到左部第一个字节不是已发送并且已确认的状态;接收窗口的滑动类似,接收窗口左部字节已经发送确认并交付主机,就向右滑动接收窗口。

接收窗口只会对窗口内最后一个按序到达的字节进行确认,例如接收窗口已经收到的字节为 {31, 34, 35},其中 {31} 按序到达,而 {34, 35} 就不是,因此只对字节 31 进行确认。发送方得到一个字节的确认之后,就知道这个字节之前的所有字节都已经被接收。

img

TCP 流量控制

流量控制是为了控制发送方发送速率,保证接收方来得及接收。

接收方发送的确认报文中的窗口字段可以用来控制发送方窗口大小,从而影响发送方的发送速率。将窗口字段设置为 0,则发送方不能发送数据。

TCP 拥塞控制

如果网络出现拥塞,分组将会丢失,此时发送方会继续重传,从而导致网络拥塞程度更高。因此当出现拥塞时,应当控制发送方的速率。这一点和流量控制很像,但是出发点不同。流量控制是为了让接收方能来得及接收,而拥塞控制是为了降低整个网络的拥塞程度。

img

TCP 主要通过四个算法来进行拥塞控制:慢开始、拥塞避免、快重传、快恢复。

发送方需要维护一个叫做拥塞窗口(cwnd)的状态变量,注意拥塞窗口与发送方窗口的区别:拥塞窗口只是一个状态变量,实际决定发送方能发送多少数据的是发送方窗口。

为了便于讨论,做如下假设:

img

\1. 慢开始与拥塞避免

发送的最初执行慢开始,令 cwnd = 1,发送方只能发送 1 个报文段;当收到确认后,将 cwnd 加倍,因此之后发送方能够发送的报文段数量为:2、4、8 ...

注意到慢开始每个轮次都将 cwnd 加倍,这样会让 cwnd 增长速度非常快,从而使得发送方发送的速度增长速度过快,网络拥塞的可能性也就更高。设置一个慢开始门限 ssthresh,当 cwnd >= ssthresh 时,进入拥塞避免,每个轮次只将 cwnd 加 1。

如果出现了超时,则令 ssthresh = cwnd / 2,然后重新执行慢开始。

2.快重传与快恢复

在接收方,要求每次接收到报文段都应该对最后一个已收到的有序报文段进行确认。例如已经接收到 M1和 M2,此时收到 M4,应当发送对 M2的确认。

在发送方,如果收到三个重复确认,那么可以知道下一个报文段丢失,此时执行快重传,立即重传下一个报文段。例如收到三个 M2,则 M3丢失,立即重传 M3。

在这种情况下,只是丢失个别报文段,而不是网络拥塞。因此执行快恢复,令 ssthresh = cwnd / 2 ,cwnd = ssthresh,注意到此时直接进入拥塞避免。

慢开始和快恢复的快慢指的是 cwnd 的设定值,而不是 cwnd 的增长速率。慢开始 cwnd 设定为 1,而快恢复 cwnd 设定为 ssthresh。

img

(三)、浏览器相关

3.1 浏览器浏览网页

img

  1. DNS解析
  2. TCP连接
  3. 发送HTTP请求
  4. 服务器处理请求并返回HTTP报文
  5. 浏览器解析渲染页面
  6. 连接结束

img

100继续请求者应当继续提出请求。服务器已收到请求的一部分,正在等待其余部分。

101切换协议请求者已要求服务器切换协议,服务器已确认并准备切换。

200成功服务器已成功处理了请求。

201已创建请求成功并且服务器创建了新的资源。

202已接受服务器已接受请求,但尚未处理。

203非授权信息服务器已成功处理了请求,但返回的信息可能来自另一来源。

204无内容服务器成功处理了请求,但没有返回任何内容。

205重置内容服务器成功处理了请求,内容被重置。

206部分内容服务器成功处理了部分请求。

300多种选择针对请求,服务器可执行多种操作。

301永久移动请求的网页已永久移动到新位置,即永久重定向。

302临时移动请求的网页暂时跳转到其他页面,即暂时重定向。

303查看其他位置如果原来的请求是 POST,重定向目标文档应该通过 GET 提取。

304未修改此次请求返回的网页未修改,继续使用上次的资源。

305使用代理请求者应该使用代理访问该网页。

307临时重定向请求的资源临时从其他位置响应。

400错误请求服务器无法解析该请求。

401未授权请求没有进行身份验证或验证未通过。

403禁止访问服务器拒绝此请求。

404未找到服务器找不到请求的网页。

405方法禁用服务器禁用了请求中指定的方法。

406不接受无法使用请求的内容响应请求的网页。

407需要代理授权请求者需要使用代理授权。

408请求超时服务器请求超时。

409冲突服务器在完成请求时发生冲突。

410已删除请求的资源已永久删除。

411需要有效长度服务器不接受不含有效内容长度标头字段的请求。

412未满足前提条件服务器未满足请求者在请求中设置的其中一个前提条件。

413请求实体过大请求实体过大,超出服务器的处理能力。

414请求 URI 过长请求网址过长,服务器无法处理。

415不支持类型请求的格式不受请求页面的支持。

416请求范围不符页面无法提供请求的范围。

417未满足期望值服务器未满足期望请求标头字段的要求。

500服务器内部错误服务器遇到错误,无法完成请求。

501未实现服务器不具备完成请求的功能。

502错误网关服务器作为网关或代理,从上游服务器收到无效响应。

503服务不可用服务器目前无法使用。

504网关超时服务器作为网关或代理,但是没有及时从上游服务器收到请求。

505HTTP 版本不支持服务器不支持请求中所用的 HTTP 协议版本。

HTTP长连接

而从HTTP/1.1起,默认使用长连接,用以保持连接特性。使用长连接的HTTP协议,会在响应头加入这行代码:Connection:keep-alive

3.2 Cookie&Session

Cookie 一般用来保存用户信息

①我们在 Cookie 中保存已经登录过得用户信息,下次访问网站的时候页面可以自动帮你登录的一些基本信息给填了;

②一般的网站都会有保持登录也就是说下次你再访问网站的时候就不需要重新登录了,这是因为用户登录的时候我们可以存放了一个 Token 在 Cookie 中,下次登录的时候只需要根据 Token 值来查找用户即可(为了安全考虑,重新登录一般要将 Token 重写);

③登录一次网站后访问网站其他页面不需要重新登录。

Session 的主要作用就是通过服务端记录用户的状态

典型的场景是购物车,当你要添加商品到购物车的时候,系统不知道是哪个用户操作的,因为 HTTP 协议是无状态的。服务端给特定的用户创建特定的 Session 之后就可以标识这个用户并且跟踪这个用户了。

Cookie 数据保存在客户端(浏览器端),Session 数据保存在服务器端。

3.2 HTTP&HTTPS

端口:HTTP的URL由“http://”起始且默认使用端口80,而HTTPS的URL由“https://”起始且默认使用端口443。

安全性和资源消耗:HTTP协议运行在TCP之上,所有传输的内容都是明文,客户端和服务器端都无法验证对方的身份。HTTPS是运行在SSL/TLS之上的HTTP协议,SSL/TLS 运行在TCP之上。所有传输的内容都经过加密,加密采用对称加密,但对称加密的密钥用服务器方的证书进行了非对称加密。所以说,HTTP 安全性没有 HTTPS高,但是 HTTPS 比HTTP耗费更多服务器资源。

HTTP中没有加密机制,可以通过SSL(Secure Socket Layer 安全套接层)或TLS(Transport Layer Security 安全层传输协议)的组合使用,加密HTTP的通信内容。

SSL工作在OSI七层模型中的表示层,TCP/IP 四层模型的应用层。

3.3 HTTP2.0

对1.x协议语意的完全兼容

2.0协议是在1.x基础上的升级而不是重写,1.x协议的方法,状态及api在2.0协议里是一样的。

性能的大幅提升

2.0协议重点是对终端用户的感知延迟、网络及服务器资源的使用等性能的优化。

二进制分帧(Binary Format)- http2.0的基石

在二进制分帧层上,http2.0会将所有传输信息分割为更小的消息和帧,并对它们采用二进制格式的编码将其封装,新增的二进制分帧层同时也能够保证http的各种动词,方法,首部都不受影响,兼容上一代http标准。其中,http1.X中的首部信息header封装到Headers帧中,而request body将被封装到Data帧中。

img

多路复用 (Multiplexing) / 连接共享

在http1.1中,浏览器客户端在同一时间,针对同一域名下的请求有一定数量的限制,超过限制数目的请求会被阻塞。这也是为何一些站点会有多个静态资源 CDN 域名的原因之一。

而http2.0中的多路复用优化了这一性能。多路复用允许同时通过单一的http/2 连接发起多重的请求-响应消息。

http 2.0 连接都是持久化的,而且客户端与服务器之间也只需要一个连接(每个域名一个连接)即可。http2连接可以承载数十或数百个流的复用,多路复用意味着来自很多流的数据包能够混合在一起通过同样连接传输。

头部压缩(Header Compression)

http1.x的头带有大量信息,而且每次都要重复发送。http/2使用encoder来减少需要传输的header大小,通讯双方各自缓存一份头部字段表,既避免了重复header的传输,又减小了需要传输的大小。

对于相同的数据,不再通过每次请求和响应发送,通信期间几乎不会改变通用键-值对(用户代理、可接受的媒体类型,等等)只需发送一次。

用header字段表里的索引代替实际的header。

img

请求优先级(Request Priorities)

把http消息分为很多独立帧之后,就可以通过优化这些帧的交错和传输顺序进一步优化性能。每个流都可以带有一个31比特的优先值:0 表示最高优先级;2的31次方-1 表示最低优先级。

服务器可以根据流的优先级,控制资源分配(CPU、内存、带宽),而在响应数据准备好之后,优先将最高优先级的帧发送给客户端。高优先级的流都应该优先发送,但又不会绝对的。绝对地准守,可能又会引入首队阻塞的问题:高优先级的请求慢导致阻塞其他资源交付。

服务端推送(Server Push)

服务器可以对一个客户端请求发送多个响应,服务器向客户端推送资源无需客户端明确的请求。并且,服务端推送能把客户端所需要的资源伴随着index.html一起发送到客户端,省去了客户端重复请求的步骤。

http2.0性能瓶颈

启用http2.0后会给性能带来很大的提升,但同时也会带来新的性能瓶颈。因为现在所有的压力集中在底层一个TCP连接之上,TCP很可能就是下一个性能瓶颈,比如TCP分组的队首阻塞问题,单个TCP packet丢失导致整个连接阻塞,无法逃避,此时所有消息都会受到影响。未来,服务器端针对http 2.0下的TCP配置优化至关重要。

3.3 cookie&session

  1. 存储位置不同:cookie存放于客户端;session存放于服务端。
  2. 存储容量不同:单个cookie保存的数据<=4KB,一个站点最多保存20个cookie;而session并没有上限。
  3. 存储方式不同:cookie只能保存ASCII字符串,并需要通过编码当时存储为Unicode字符或者二进制数据;session中能够存储任何类型的数据,例如字符串、整数、集合等。
  4. 隐私策略不同:cookie对客户端是可见的,别有用心的人可以分析存放在本地的cookie并进行cookie欺骗,所以它是不安全的;session存储在服务器上,对客户端是透明的,不存在敏感信息泄露的风险。
  5. 生命周期不同:可以通过设置cookie的属性,达到cookie长期有效的效果;session依赖于名为JSESSIONID的cookie,而该cookie的默认过期时间为-1,只需关闭窗口该session就会失效,因此session不能长期有效。
  6. 服务器压力不同:cookie保存在客户端,不占用服务器资源;session保管在服务器上,每个用户都会产生一个session,如果并发量大的话,则会消耗大量的服务器内存。
  7. 浏览器支持不同:cookie是需要浏览器支持的,如果客户端禁用了cookie,则会话跟踪就会失效;运用session就需要使用URL重写的方式,所有用到session的URL都要进行重写,否则session会话跟踪也会失效。
  8. 跨域支持不同:cookie支持跨域访问,session不支持跨域访问。

3.4 TCP&UDP

1、TCP面向连接(如打电话要先拨号建立连接);UDP是无连接的,即发送数据之前不需要建立连接

2、TCP提供可靠的服务。也就是说,通过TCP连接传送的数据,无差错,不丢失,不重复,且按序到达;UDP尽最大努力交付,即不保证可靠交付

3、TCP面向字节流,实际上是TCP把数据看成一连串无结构的字节流;UDP是面向报文的

UDP没有拥塞控制,因此网络出现拥塞不会使源主机的发送速率降低(对实时应用很有用,如IP电话,实时视频会议等)

4、每一条TCP连接只能是点到点的;UDP支持一对一,一对多,多对一和多对多的交互通信

5、TCP首部开销20字节;UDP的首部开销小,只有8个字节

6、TCP的逻辑通信信道是全双工的可靠信道,UDP则是不可靠信道

1、基于连接与无连接;

2、对系统资源的要求(TCP较多,UDP少);

3、UDP程序结构较简单;

4、流模式与数据报模式 ;

5、TCP保证数据正确性,UDP可能丢包;

6、TCP保证数据顺序,UDP不保证。

(四)、跨域和DNS

出于浏览器的同源策略限制。所谓同源(即指在同一个域)就是两个页面具有相同的协议(protocol),主机(host)和端口号(port)

非同源限制

【1】无法读取非同源网页的 Cookie、LocalStorage 和 IndexedDB

【2】无法接触非同源网页的 DOM

【3】无法向非同源地址发送 AJAX 请求

DNS用了哪些协议

  1. DNS在进行区域传输的时候使用TCP协议,其它时候则使用UDP协议;

DNS的规范规定了2种类型的DNS服务器,一个叫主DNS服务器,一个叫辅助DNS服务器。在一个区中主DNS服务器从自己本机的数据文件中读取该区的DNS数据信息,而辅助DNS服务器则从区的主DNS服务器中读取该区的DNS数据信息。当一个辅助DNS服务器启动时,它需要与主DNS服务器通信,并加载数据信息,这就叫做区传送(zone transfer)。

  1. 为什么既使用TCP又使用UDP?

UDP报文的最大长度为512字节,而TCP则允许报文长度超过512字节。当DNS查询超过512字节时,协议的TC标志出现删除标志,这时则使用TCP发送。通常传统的UDP报文一般不会大于512字节。

  1. 区域传送时使用TCP,主要有以下两点考虑:
    1. 辅域名服务器会定时(一般时3小时)向主域名服务器进行查询以便了解数据是否有变动。如有变动,则会执行一次区域传送,进行数据同步。区域传送将使用TCP而不是UDP,因为数据同步传送的数据量比一个请求和应答的数据量要多得多。
    2. TCP是一种可靠的连接,保证了数据的准确性。
  2. 域名解析时使用UDP协议:

客户端向DNS服务器查询域名,一般返回的内容都不超过512字节,用UDP传输即可。不用经过TCP三次握手,这样DNS服务器负载更低,响应更快。虽然从理论上说,客户端也可以指定向DNS服务器查询的时候使用TCP,但事实上,很多DNS服务器进行配置的时候,仅支持UDP查询包。

(五)、Socket

img

img

img

四、数据库

(一)、MySQL

MySQL 主要分为 Server 层和引擎层,Server 层主要包括连接器、查询缓存、分析器、优化器、执行器,同时还有一个日志模块(binlog),这个日志模块所有执行引擎都可以共用,redolog 只有 InnoDB 有。

1.1 存储引擎

myisam:不支持事务,不支持外键约束,索引文件和数据文件分开,这样在内存中可以缓存更多的索引,对查询的性能会更好,适用于少量的从插入大量查询的场景。

innodb:是现在最常用的存储引擎,是mysql5.5之后的默认存储引擎。主要特点就是支持事务,走聚簇索引,强制要求有主键,支持外键约束,高并发、大数据量、高可用等相关成熟的数据库架构,分库分表、读写分离、主备切换,全部都可以基于innodb存储引擎来实现。到这里就可以展开来说一说是怎么支撑大数据的,怎么进行读写分离的。

不同点:

三个范式:

第一范式(1NF):数据库表中的字段都是单一属性的,不可再分。这个单一属性由基本类型构成,包括整型、实数、字符型、逻辑型、日期型等。

第二范式(2NF):数据库表中不存在非关键字段对任一候选关键字段的部分函数依赖(部分函数依赖指的是存在组合关键字中的某些字段决定非关键字段的情况),也即所有非关键字段都完全依赖于任意一组候选关键字。

第三范式(3NF):在第二范式的基础上,数据表中如果不存在非关键字段对任一候选关键字段的传递函数依赖则符合第三范式。所谓传递函数依赖,指的是如 果存在"A → B → C"的决定关系,则C传递函数依赖于A。因此,满足第三范式的数据库表应该不存在如下依赖关系: 关键字段 → 非关键字段 x → 非关键字段y

几种连接:

img

1.2 索引

为什么要使用索引

  1. 通过创建唯一性索引,可以保证数据库表中每一行数据的唯一性。
  2. 可以大大加快 数据的检索速度(大大减少的检索的数据量), 这也是创建索引的最主要的原因。
  3. 帮助服务器避免排序和临时表。
  4. 将随机IO变为顺序IO
  5. 可以加速表和表之间的连接,特别是在实现数据的参考完整性方面特别有意义。

为什么不能每个字段都用索引

  1. 当对表中的数据进行增加、删除和修改的时候,索引也要动态的维护,这样就降低了数据的维护速度。
  2. 索引需要占物理空间,除了数据表占数据空间之外,每一个索引还要占一定的物理空间,如果要建立聚簇索引,那么需要的空间就会更大。
  3. 创建索引和维护索引要耗费时间,这种时间随着数据量的增加而增加。

索引的注意事项

  1. 在经常需要搜索的列上,可以加快搜索的速度;
  2. 在经常使用在WHERE子句中的列上面创建索引,加快条件的判断速度。
  3. 在经常需要排序的列上创 建索引,因为索引已经排序,这样查询可以利用索引的排序,加快排序查询时间;
  4. 对于中到大型表索引都是非常有效的,但是特大型表的话维护开销会很大,不适合建索引
  5. 在经常用在连接的列上,这 些列主要是一些外键,可以加快连接的速度;
  6. 避免 where 子句中对字段施加函数,这会造成无法命中索引。
  7. 在使用InnoDB时使用与业务无关的自增主键作为主键,即使用逻辑主键,而不要使用业务主键。
  8. 将某一列设置为default null,where 是可以走索引,另外索引列是否设置 null 是不影响性能的。 但是,还是不建议列上允许为空。最好限制not null,因为null需要更多的存储空间并且null值无法参与某些运算。
  9. 删除长期未使用的索引,不用的索引的存在会造成不必要的性能损耗 MySQL 5.7 可以通过查询 sys 库的 chema_unused_indexes 视图来查询哪些索引从未被使用
  10. 在使用 limit offset 查询缓慢时,可以借助索引来提高性能

如何添加索引

1.添加PRIMARY KEY(主键索引)

2.添加UNIQUE(唯一索引)

3.添加INDEX(普通索引)

https://zhuanlan.zhihu.com/p/62018452

B-树:对id简历索引:15、56、77、20、49

img

B+树:非叶子节点不存储data 方便范围的查找

img

select * from table where id = 15 -> 0x07物理地址 -> 15,张三,22

myisam最大的特点是数据文件和索引文件是分开的,先是索引文件里搜索,然后到数据文件里定位一个行。

innodb存储引擎,要求必须有主键,会根据主键建立一个默认索引,叫做聚簇索引,innodb的数据文件本身同时也是个索引文件,索引存储结构大致如下:

15,data:完整的一行数据,(15,张三,22)

22,data:完整的一行数据,(22,李四,30)

就是因为这个原因,innodb表是要求必须有主键的,但是myisam表不要求必须有主键。另外一个是,innodb存储引擎下,如果对某个非主键的字段创建个索引,那么最后那个叶子节点的值就是主键的值,因为可以用主键的值到聚簇索引里根据主键值再次查找到数据,即所谓的回表,例如:

select * from table where name = ‘张三’

先到name的索引里去找,找到张三对应的叶子节点,叶子节点的data就是那一行的主键,id=15,然后再根据id=15,到数据文件里面的聚簇索引(根据主键组织的索引)根据id=15去定位出来id=15这一行的完整的数据

1.3 事务ACID

https://zhuanlan.zhihu.com/p/65281198

img

事务实现原理:

事务日志是通过redo和innodb的存储引擎日志缓冲(Innodb log buffer)来实现的,当开始一个事务的时候,会记录该事务的lsn(log sequence number)号; 当事务执行时,会往InnoDB存储引擎的日志的日志缓存里面插入事务日志;当事务提交时,必须将存储引擎的日志缓冲写入磁盘(通过innodb_flush_log_at_trx_commit来控制),也就是写数据前,需要先写日志。这种方式称为“预写日志方式”

并发事务带来的问题:

不可重复读和幻读区别:

不可重复读的重点是修改比如多次读取一条记录发现其中某些列的值被修改,幻读的重点在于新增或者删除比如多次读取一条记录发现记录增多或减少了。

隔离级别:

这个也叫做不可重复读,就是所谓的一个事务内对一个数据两次读,可能会读到不一样的值。

接下来我们聊下MySQL是如何实现Read Repeatable的吧,因为一般我们都不修改这个隔离级别,但是你得清楚是怎么回事儿,MySQL是通过MVCC机制来实现的,就是多版本并发控制,multi-version concurrency control。

当我们使用innodb存储引擎,会在每行数据的最后加两个隐藏列,一个保存行的创建时间,一个保存行的删除时间,但是这儿存放的不是时间,而是事务id,事务id是mysql自己维护的自增的,全局唯一。

事务id,在mysql内部是全局唯一递增的,事务id=1,事务id=2,事务id=3

img

事务id=121的事务,查询id=1的这一行的时候,一定会找到创建事务id <= 当前事务id的那一行

select * from table where id=1,就可以查到上面那一行

事务id=122的事务,将id=1的这一行给删除了,此时就会将id=1的行的删除事务id设置成122

事务id=121的事务,再次查询id=1的那一行,能查到吗?

能查到,要求创建事务id <= 当前事务id,当前事务id < 删除事务id

事务id=121的事务,查询id=2的那一行,查到name=李四

事务id=122的事务,将id=2的那一行的name修改成name=小李四

事务id=121的事务,查询id=2的那一行,答案是:李四,创建事务id <= 当前事务id,当前事务id < 删除事务id

1.4 锁

img

innodb行锁和表锁:

行锁:访问数据库的时候,锁定整个行数据,防止并发错误。开销小,加锁快,不会出现死锁;锁定力度大,发生锁冲突概率 高,并发度最低

表锁:访问数据库的时候,锁定整个表数据,防止并发错误。开销大,加锁慢,会出现死锁;锁定粒度小,发生锁冲突的概率 低,并发度高

对于 InnoDB 表,在绝大部分情况下都应该使用行级锁,因为事务和行锁往往是 我们之所以选择 InnoDB 表的理由。但在个别特殊事务中,也可以考虑使用表级 锁。

什么时候使用表锁:

第一种情况是:事务需要更新大部分或全部数据,表又比较大,如果使用默认的行锁,不仅这个事务执行效率低,而且可能造成其他事务长时间锁等待和锁冲突,这种情况下可以考虑使用表锁来提高该事务的执行速度。

第二种情况是:事务涉及多个表,比较复杂,很可能引起死锁,造成大量事务回滚。这种情况也可以考虑一次性锁定事务涉及的表,从而避免死锁、减少数据库因事务回滚带来的开销。

悲观锁:顾名思义,就是很悲观,每次去拿数据的时候都认为别人会修改, 所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会 block 直到它 拿到锁。 传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁, 写锁等,都是在做操作之前先上锁。

乐观锁: 顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修 改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这 个数据,可以使用版本号等机制。

像乐观锁适用于写比较少的情况 下,即冲突真的很少发生的时候,这样可以省去了锁的开销,加大了系统的整个 吞吐量。但如果经常产生冲突,上层应用会不断的进行 retry,这样反倒是降低 了性能,所以这种情况下用悲观锁就比较合适。

悲观锁适合写操作多的场景,先加锁可以保证写操作时数据正确。

乐观锁适合读操作多的场景,不加锁的特点能够使其读操作的性能大幅提升。

间隙锁:

间隙锁(简称为 Gap)是对索引记录之间的间隙的锁定,或者是对第一条索引记录之前的间隙和对最后一条记录之后的间隙的锁。间隙锁是防止幻读的主要手段之一,幻读是同一个事务在不同的时间执行相同的查询语句,得出的结果集不同。那么间隙锁是如何防止幻读的呢?实际上就是通过锁定指定的间隙,使得这些间隙无法插入新的记录,从而防止了数据的增长。

1.5 大表优化

当MySQL单表记录数过大时,数据库的CRUD性能会明显下降,一些常见的优化措施如下:

限定数据的范围

务必禁止不带任何限制数据范围条件的查询语句。比如:我们当用户在查询订单历史的时候,我们可以控制在一个月的范围内;

读/写分离

经典的数据库拆分方案,主库负责写,从库负责读;

垂直分区

根据数据库里面数据表的相关性进行拆分。例如,用户表中既有用户的登录信息又有用户的基本信息,可以将用户表拆分成两个单独的表,甚至放到单独的库做分库。

简单来说垂直拆分是指数据表列的拆分,把一张列比较多的表拆分为多张表。

img

水平分区

保持数据表结构不变,通过某种策略存储数据分片。这样每一片数据分散到不同的表或者库中,达到了分布式的目的。 水平拆分可以支撑非常大的数据量。

img

数据库连接本质就是一个 socket 的连接。数据库服务端还要维护一些缓存和用户权限信息之类的 所以占用了一些内存。我们可以把数据库连接池是看做是维护的数据库连接的缓存,以便将来需要对数据库的请求时可以重用这些连接。为每个用户打开和维护数据库连接,尤其是对动态数据库驱动的网站应用程序的请求,既昂贵又浪费资源。在连接池中,创建连接后,将其放置在池中,并再次使用它,因此不必建立新的连接。如果使用了所有连接,则会建立一个新连接并将其添加到池中。 连接池还减少了用户必须等待建立与数据库的连接的时间。

1.6 LOG

binlog(Binary Log)

二进制日志文件就是常说的binlog。二进制日志记录了MySQL所有修改数据库的操作,然后以二进制的形式记录在日志文件中,其中还包括每条语句所执行的时间和所消耗的资源,以及相关的事务信息。

redo log

重做日志用来实现事务的持久性,即事务ACID中的D。它由两部分组成:一是内存中的重做日志缓冲(redo log buffer),其是易失的;二是重做日志文件(redo log file),它是持久的。

即当事务提交(COMMIT)时,必须先将该事务的所有日志写入到重做日志文件进行持久化,待事务的COMMIT操作完成才算完成。

undo log

重做日志记录了事务的行为,可以很好地通过其对页进行“重做”操作。但是事务有时还需要进行回滚操作,这时就需要undo。因此在对数据库进行修改时,InnoDB存储引擎不但会产生redo,还会产生一定量的undo。这样如果用户执行的事务或语句由于某种原因失败了,又或者用户用一条ROLLBACK语句请求回滚,就可以利用这些undo信息将数据回滚到修改之前的样子。

五、操作系统

(一)、系统调用

用户态(user mode) : 用户态运行的进程可以直接读取用户程序的数据。

系统态(kernel mode):可以简单的理解系统态运行的进程或程序几乎可以访问计算机的任何资源,不受限制。

也就是说在我们运行的用户程序中,凡是与系统态级别的资源有关的操作(如文件管理、进程控制、内存管理等),都必须通过系统调用方式向操作系统提出服务请求,并由操作系统代为完成。

(二)、进程线程

进程:指在系统中正在运行的一个应用程序;程序一旦运行就是进程;进程——资源分配的最小单位。

线程:系统分配处理器时间资源的基本单元,或者说进程之内独立执行的一个单元执行流。线程——程序执行的最小单位。

总结:

1.进程要分配一大部分的内存,而线程只需要分配一部分栈就可以了.

2.一个程序至少有一个进程,一个进程至少有一个线程.

3.进程是资源分配的最小单位,线程是程序执行的最小单位。

4.一个线程可以创建和撤销另一个线程,同一个进程中的多个线程之间可以并发执行.

线程是进程划分成的更小的运行单位,一个进程在其执行的过程中可以产生多个线程。线程和进程最大的不同在于基本上各进程是独立的,而各线程则不一定,因为同一进程中的线程极有可能会相互影响。线程执行开销小,但不利于资源的管理和保护;而进程正相反。

一个进程中可以有多个线程,多个线程共享进程的方法区 (JDK1.8 之后的元空间)资源但是每个线程有自己的程序计数器、虚拟机栈本地方法栈

img

进程的五种状态

创建状态:进程在创建时需要申请一个空白PCB,向其中填写控制和管理进程的信息,完成资源分配。如果创建工作无法完成,比如资源无法满足,就无法被调度运行,把此时进程所处状态称为创建状态

就绪状态:进程已经准备好,已分配到所需资源,只要分配到CPU就能够立即运行

执行状态:进程处于就绪状态被调度后,进程进入执行状态

阻塞状态:正在执行的进程由于某些事件(I/O请求,申请缓存区失败)而暂时无法运行,进程受到阻塞。在满足请求时进入就绪状态等待系统调用

终止状态:进程结束,或出现错误,或被系统终止,进入终止状态。无法再执行

进程间的通信方式

那线程间的同步的方式有哪些呢?

线程同步是两个或多个共享关键资源的线程的并发执行。应该同步线程以避免关键的资源使用冲突。操作系统一般有下面三种线程同步的方式:

进程的调度算法

为了确定首先执行哪个进程以及最后执行哪个进程以实现最大 CPU 利用率,计算机科学家已经定义了一些算法,它们是:

死锁的产生和消除

当线程A持有独占锁a,并尝试去获取独占锁b的同时,线程B持有独占锁b,并尝试获取独占锁a的情况下,就会发生AB两个线程由于互相持有对方需要的锁,而发生的阻塞现象,我们称为死锁。

造成死锁必须达成的4个条件(原因)

  1. 互斥条件:一个资源每次只能被一个线程使用。
  2. 请求与保持条件:一个线程因请求资源而阻塞时,对已获得的资源保持不放。
  3. 不剥夺条件:线程已获得的资源,在未使用完之前,不能强行剥夺。
  4. 循环等待条件:若干线程之间形成一种头尾相接的循环等待资源关系。

互斥条件 ---> 独占锁的特点之一。

请求与保持条件 ---> 独占锁的特点之一,尝试获取锁时并不会释放已经持有的锁

不剥夺条件 ---> 独占锁的特点之一。

循环等待条件 ---> 唯一需要记忆的造成死锁的条件。

(三)、Linux

注意:如果直接用 ps((Process Status))命令,会显示所有进程的状态,通常结合 grep 命令查看某进程的状态。

先用 ps 查找进程,然后用 kill 杀掉

查看带有关键字的日志文件:

cat 路径/文件名 | grep 关键词

cat test.log | grep"http"

grep -i 关键词 路径/文件名

grep -i"http" ./test.log 

压缩解压缩:

端口与进程:

linux下通过进程名查看其占用端口

先查看进程PID

ps -ef | grep 进程名

通过pid查看占用端口

netstat -nap | grep pid

linux通过端口查看进程

netstat -nap | grep 端口号

查找关键字:

grep -rn 'hello,world!' ./

./ : 表示路径为当前目录

-r 是递归查找

-n 是显示行号

六、测试

(一)、 测试基础

测试要点

img

如何写测试用例

1、测试人员尽早介入,彻底理解清楚需求,这个是写好测试用例的基础

2、如果以前有类似的需求,可以参考类似需求的测试用例,然后还需要看类似需求的bug情况

3、清楚输入、输出的各种可能性,以及各种输入的之间的关联关系,理解清楚需求的执行逻辑,通过等价类、边界值、判定表等方法找出大部分用例

4、找到需求相关的一些特性,补充测试用例

5、根据自己的经验分析遗漏的测试场景

6、多总结类似功能点的测试点,才能够写出质量越来越高的测试用例

7、书写格式一定要清晰

测试的工作

搭建测试环境

撰写测试用例

执行测试用例

写测试计划,测试报告

测试,并提交BUG表单

跟踪bug修改情况

执行自动化测试,编写脚本,执行,分析,报告

进行性能测试,压力测试等其他测试,执行,分析,调优,报告

αβ测试

α测试:在受控的环境中进行,由用户在开发者的场所进行,并且在开发者对用户的指导下进行测试,开发者负责记录发现的错误和使用中遇到的问题

β测试:在开发者不能控制的环境中的真实应用,由软件的最终用户们在一个或多个客户场所下进行,由用户记录在测试中遇到的一系列问题,并定期报给开发者。

单元集成系统测试

单元测试是在软件开发过程中要进行的最低级别的测试活动,在单元测试活动中,软件的独立单元将在与程序的其他部分相隔离的情况下进行测试,测试重点是系统的模块,包括子程序的正确性验证等。

集成测试,也叫组装测试或联合测试。在单元测试的基础上,将所有模块按照设计要求,组装成为子系统或系统,进行集成测试。实践表明,一些模块虽然能够单独地工作,但并不能保证连接起来也能正常的工作。程序在某些局部反映不出来的问题,在全局上很可能暴露出来,影响功能的实现。测试重点是模块间的衔接以及参数的传递等。

系统测试(比较重要)是将经过测试的子系统装配成一个完整系统来测试。它是检验系统是否确实能提供系统方案说明书中指定功能的有效方法。测试重点是整个系统的运行以及与其他软件的兼容性。

回归测试:回归测试是指在发生修改之后重新测试先前的测试用例以保证修改的正确性。理论上,软件产生新版本,都需要进行回归测试,验证以前发现和修复的错误是否在新软件版本上再次出现。根据修复好了的缺陷再重新进行测试。回归测试的目的在于验证以前出现过但已经修复好的缺陷不再重新出现。一般指对某已知修正的缺陷再次围绕它原来出现时的步骤重新测试。

验收测试是指系统开发生命周期方法论的一个阶段,这时相关的用户或独立测试人员根据测试计划和结果对系统进行测试和接收。它让系统用户决定是否接受系统。它是一项确定产品是否能够满足合同或用户所规定需求的测试。验收测试包括Alpha测试和Beta测试。

APP测试的性能指标

1、内存:内存消耗测试节点的设计目标是为了让应用不占用过多的系统资源,且及时释放内存,保障整个系统的稳定性。当然关于内存测试,在这里我们需要引入几个概念:空闲状态、中等规格、满规格。

空闲状态指打开应用后,点击home键让应用后台运行,此时应用处于的状态叫做空闲;中等规格和满规格指的是对应用的操作时间的间隔长短不一,中等规格时间较长,满规格时间较短。

内存测试中存在很多测试子项,清单如下:

●空闲状态下的应用内存消耗;

●中等规格状态下的应用内存消耗;

●满规格状态下的应用内存消耗;

●应用内存峰值;

●应用内存泄露;

●应用是否常驻内存;

●压力测试后的内存使用。

2、CPU:

使用Android提供的view plaincopy在CODE上查看代码片派生到我的代码片

adbshell dumpsys CPUinfo |grep packagename >/address/CPU.txt来获取;

使用top命令view plaincopy在CODE上查看代码片派生到我的代码片

adbshell top |grep packagename>/address/CPU.txt来获取。

3、流量:

网络流量测试是针对大部分应用而言的,可能还有部分应用会关注网速、弱网之类的测试。

流量测试包括以下测试项:

应用首次启动流量提示;

应用后台连续运行2小时的流量值;

应用高负荷运行的流量峰值。

4、电量:

●测试手机安装目标APK前后待机功耗无明显差异;

●常见使用场景中能够正常进入待机,待机电流在正常范围内;

●长时间连续使用应用无异常耗电现象。

5、启动速度:

第一类:首次启动--应用首次启动所花费的时间;

第二类:非首次启动--应用非首次启动所花费的时间;

第三类:应用界面切换--应用界面内切换所花费的时间。

6、滑动速度、界面切换速度

7、与服务器交互的网络速度

测试IP地址

https://www.nowcoder.com/ta/review-test/review?query=&asc=true&order=&page=57

测试登录界面

测试的设计都是通用的,首先功能测试看功能有没有实现,然后再对性能、压力、容量、健壮性、安全性、可靠性、恢复性、备份、协议、兼容性、可用性、配置、GUI这些非功能测试去思考。

一、功能测试

1.输入正确的用户名和密码,点击提交按钮,验证是否能正确登录。

2.输入错误的用户名或者密码,验证登录会失败,并且提示相应的错误信息。

3.登录成功后能否能否跳转到正确的页面

4.用户名和密码,如果太短或者太长,应该怎么处理

5.用户名和密码,中有特殊字符(比如空格),和其他非英文的情况

6.记住用户名的功能

7.登陆失败后,不能记录密码的功能

8.用户名和密码前后有空格的处理

9.密码是否非明文显示显示,使用星号圆点等符号代替。

10.牵扯到验证码的,还要考虑文字是否扭曲过度导致辨认难度大,考虑颜色(色盲使 用者),刷新或换一个按钮是否好用

11.登录页面中的注册、忘记密码,登出用另一帐号登陆等链接是否正确

12.输入密码的时候,大写键盘开启的时候要有提示信息。

13.什么都不输入,点击提交按钮,检查提示信息。

二、界面测试

1.布局是否合理,testbox和按钮是否整齐。

2.testbox和按钮的长度,高度是否复合要求。

\3. 界面的设计风格是否与UI的设计风格统一。

\4. 界面中的文字简洁易懂,没有错别字。

三、性能测试

1.打开登录页面,需要的时间是否在需求要求的时间内。

2.输入正确的用户名和密码后,检查登录成功跳转到新页面的时间是否在需求要求的时间内。

3.模拟大量用户同时登陆,检查一定压力下能否正常登陆跳转。

四、安全性测试

1.登录成功后生成的Cookie,是否是httponly (否则容易被脚本盗取)。

2.用户名和密码是否通过加密的方式,发送给Web服务器。

3.用户名和密码的验证,应该是用服务器端验证, 而不能单单是在客户端用javascript 验证。

4.用户名和密码的输入框,应该屏蔽SQL注入攻击。

5.用户名和密码的的输入框,应该禁止输入脚本 (防止XSS攻击)。

6.防止暴力破解,检测是否有错误登陆的次数限制。

\7. 是否支持多用户在同一机器上登录。

\8. 同一用户能否在多台机器上登录。

五、可用性测试

\1. 是否可以全用键盘操作,是否有快捷键。

\2. 输入用户名,密码后按回车,是否可以登陆。

\3. 输入框能否可以以Tab键切换。

六、兼容性测试

1.不同浏览器下能否显示正常且功能正常(IE,6,7,8,9, Firefox, Chrome, Safari,等)。

2.同种浏览器不同版本下能否显示正常且功能正常。

2.不同的平台是否能正常工作,比如Windows, Mac。

3.移动设备上是否正常工作,比如Iphone, Andriod。

4.不同的分辨率下显示是否正常。

七、本地化测试

\1. 不同语言环境下,页面的显示是否正确。

吃鸡游戏压力测试

首先明确需要测试压力的内容:

吞吐量、平均响应时间、QPS、CPU、内存

游戏服务器硬件

网络压力

数据库

多线程的最优线程数

朋友圈点赞

1.是否可以正常点赞和取消;

2.点赞的人是否在可见分组里;

3.点赞状态是否能即时更新显示;

4.点赞状态,共同好友是否可见;

5.不同手机,系统显示界面如何;

6.性能检测,网速快慢对其影响;

7.点赞显示的是否正确,一行几个;

8.点赞是否按时间进行排序,头像对应的是否正确;

9.是否能在消息列表中显示点赞人的昵称、备注;

10.可扩展性测试,点赞后是否能发表评论;

11.是否在未登录时可查看被点赞的信息。

页面进行测试

1、UI测试:页面布局、页面样式检查、控件长度是否够长;显示时,是否会被截断;支持的快捷键,Tab键切换焦点顺序正确性等。

2、功能测试:页面上各类控件的测试范围,测试点。结合控件的实际作用来补充检查点: 比如, 密码框是否*显示, 输入是否做trim处理等。

3、安全测试:输入特殊字符,sql注入,脚本注入测试。后台验证测试,对于较重要的表单 ,绕过js检验后台是否验证;数据传输是否加密处理,比如, 直接请求转发,地址栏直接显示发送字符串?

4、兼容性测试

5、性能测试

淘宝搜索框测试

一、功能测试

\1. 输入关键字,查看: 返回结果是否准确,返回的文本长度需限制

1.1输入可查到结果的正常关键字、词、语句,检索到的内容、链接正确性;

1.2输入不可查到结果的关键字、词、语句;

1.3输入一些特殊的内容,如空、特殊符、标点符、极限值等,可引入等价类划分的方法等;

\2. 结果显示:标题,卖家,销售量,单行/多行,是否有图片

\3. 结果排序:价格 销量 评价 综合

4.返回结果庞大时,限制第一页的现实量,需支持翻页

\5. 多选项搜索:关键字 品牌 产地 价格区间 是否天猫 是否全国购

\6. 是否支持模糊搜索,支持通配符的查询

7, 网速慢的情况下的搜索

\8. 搜索结果为空的情况

\9. 未登录情况和登录情况下的搜索(登录情况下 存储用户搜索的关键字/搜索习惯)

二、性能测试

1压力测试:在不同发用户数压力下的表现(评价指标如响应时间等)

2负载测试:看极限能承载多大的用户量同时正常使用

3稳定性测试:常规压力下能保持多久持续稳定运行

4内存测试:有无内存泄漏现象

5大数据量测试:如模拟从庞大的海量数据中搜索结果、或搜索出海量的结果后列示出来,看表现如何等等。

三、易用性:交互界面的设计是否便于、易于使用

1依据不同的查询结果会有相关的人性化提示,查不到时告知?查到时统计条数并告知?有疑似输入条件错误时提示可能正确的输入项等等处理;

2查询出的结果罗列有序,如按点击率或其他排序规则,确保每次查询出的结果位置按规则列示方便定位,显示字体、字号、色彩便于识别等等;

3标题查询、全文检索、模糊查询、容错查询、多关键字组织查询(空格间格开)等实用的检索方式是否正常?

4输入搜索条件的控件风格设计、位置摆放是否醒目便于使用者注意到,有否快照等快捷查看方式等人性化设计?

四、兼容性

1、WINDOWS/LINUX/UNIX等各类操作系统下及各版本条件下的应用

2、IE/FIREFOX/GOOGLE/360/QQ等各类浏览器下及各版本条件下、各种显示分辨率条件下的应用

3、SQL/ORACLE/DB2/MYSQL等各类数据库存储情况下的兼容性测试

4、简体中文、繁体中文、英文等各类语种软件平台下的兼容性测试

5、IPHONE/IPAD、安卓等各类移动应用平台下的兼容性测试

6、与各相关的监控程序的兼容性测试,如输入法、杀毒、监控、防火墙等工具同时使用

五、安全性

1、被删除、加密、授权的数据,不允许被SQL注入等攻击方式查出来的,是否有安全控制设计;

2、录入一些数据库查询的保留字符,如单引号、%等等,造成查询SQL拼接出的语句产生漏洞,如可以查出所有数据等等,这方面要有一些黑客攻击的思想并引入一些工具和技术,如爬网等。

3、通过白盒测试技术,检查一下在程序设计上是否存在安全方面的隐患;

4、对涉及国家安全、法律禁止的内容是否进行了相关的过滤和控制;

性能测试指标

从外部看,主要有

1、吞吐量:每秒钟系统能够处理的请求数,任务数

2、响应时间:服务处理一个请求或一个任务的耗时

3、错误率:一批请求中结果出错的请求所占比例

从服务器的角度看,性能测试关注CPU,内存,服务器负载,网络,磁盘IO

对登录功能做性能测试

单用户登陆的响应界面是否符合预期

单用户登陆时后台请求数量是否过多

高并发场景下用户登录的响应界面是否符合预期

高并发场景下服务端的监控指标是否符合预期

高集合点并发场景下是否存在资源死锁和不合理的资源等待

长时间大量用户连续登录和登出,服务器端是否存在内存泄漏

怎么测出可同时处理的最大请求数量

可以采用性能测试工具(WeTest服务器性能),该工具是腾讯wetest团队出品,使用起来很简单方便,但测试功能相当强大,能提供10w+以上的并发量,定位性能拐点,测出服务器模型最大并发

(二)、 GIT

  1. 检测当前文件状态:git status
  2. 提出更改(把它们添加到暂存区):git add filename (针对特定文件)、git add *(所有文件)、git add *.txt(支持通配符,所有 .txt 文件)
  3. 忽略文件:.gitignore文件
  4. 提交更新:git commit -m "代码提交信息"(每次准备提交前,先用git status看下,是不是都已暂存起来了, 然后再运行提交命令git commit)
  5. 跳过使用暂存区域更新的方式:git commit -a -m "代码提交信息"。git commit加上-a选项,Git 就会自动把所有已经跟踪过的文件暂存起来一并提交,从而跳过git add步骤。
  6. 移除文件:git rm filename(从暂存区域移除,然后提交。)
  7. 对文件重命名:git mv README.md README(这个命令相当于mv README.md README、git rm README.md、git add README这三条命令的集合)
  8. 查看本地与服务端diff:git diff

七、区块链

区块链项目背景

医疗行业是一个孤岛式、严守各自数据和技术的行业。医疗行业又是一个需要共同来维护人类生命健康安全的行业,许多疾病需要通过企业机构间共享数据和技术来提高治愈率。医疗行业目前各自为战的孤岛效应十分严重,已经被各个国家政府、联合国呼吁企业间协作、服务于全人类的健康,但仍然收效甚微。其根源在于传统的商业模式非常不利于医疗的商业协作和数据共通。

根据医疗行业现状,不难发现存在着数据存储中心化和各医疗机构间数据共享困难的问题,甚至许多乡镇医院、诊所等只进行纸质病历记录或只治疗而不进行病历记录。这是因为缺乏一个统一而广泛适用的医疗数据记录、保存和同步平台。而数据中心化存储与共享困难也造成了一系列不必要的问题。如患者在就医问诊时重复测量各项指标,被问及重复问题;在紧急情况下调取病人病历查询重要资料有可能耗时过长,而导致病人错过最佳救助时期;中心化数据库一旦数据泄露或者被黑客攻击,就会对无数人带来影响;药物来源难溯导致的药物造假和不合格问题;个人对于自身医疗数据缺乏追溯权、知情权及访问控制权等。

什么是区块链?

从结构上说:

区块链这个名词源自于交易的处理方式,通过区块批量处理交易,每个区块包含前一个区块的hash,以此形成区块链。区块链非常聪明地把两个基于哈希值的数据结构结合起来:

第一个数据结构是区块链,每一个区块都有一个去快头部,里面有一个哈希指针指向上一个区块。第二个数据结构是一个树状数据结构,也就是以树状结构把区块内所有交易的哈希值进行排列存储。”

从技术上说:

区块链使用了密码学、p2p网络、博弈论等不是完全使用技术手段,而是将技术手段与一种明智的激励机制相结合,做到了去中心化,在这个网络中没有中心化节点,通过节点之间的共识算法达到数据的一致性,通过社区治理链发展所以区块链的三大基石社区、共识、激励。

什么是共识机制?

所谓的共识就是在人人平等的社会里需要大家共同形成一个共识,产生一个操作者、临时决策者,代表大家来进行中心化的操作,大家按照这个共识来维持去中心化的网络世界。

区块链可以看作一本记录所有交易的分布式公开帐簿,区块链网络中的每个参与者都把它看作一本所有权的权威记录。

公开账本历史数据不可篡改,只允许往后添加,每个节点都具有相同的权限,那么就带来一个问题:

公开账本每个新区块由谁来负责写入?

因为所有节点都一样,如果所有节点同时一起写入账本数据,那么肯定数据会不一致。

因此需要一种机制来保证区块链中的每一区块只能由一个节点来负责写入,如何选出写入账本数据的节点,这就是共识机制。让平等的参与者按照某种秩序达成一致意见。

共识算法有哪些类型?

Pow工作量证明,就是大家熟悉的挖矿,通过与或运算,计算出一个满足规则的随机数,即获得本次记账权,发出本轮需要记录的数据,全网其它节点验证后一起存储;

Pos权益证明,Pow的一种升级共识机制;根据每个节点所占代币的比例和时间;等比例的降低挖矿难度,从而加快找随机数的速度。

DPos股份授权证明机制,类似于董事会投票,持币者投出一定数量的节点,代理他们进行验证和记账。

PBFT实用拜占庭容错

区块链架构的核心组件是什么?

节点-区块链架构中的用户/计算机。

交易-这是区块链系统的最小组成部分。

-用于维护一组分配给网络中所有节点的事务。

-块顺序。

矿工-在添加到区块链结构之前执行块验证过程的特定节点。

共识协议-进行区块链操作的规则集。

什么是Dapp?

去中心化应用程序(DApp)是在点对点网络的计算机上而不是单台计算机上运行的应用程序。去中心化应用程序自点对点网络出现以来就已经存在,它是一种旨在以不受任何实体控制的方式存在于Internet上的软件工具。

去中心化应用程序是一个基于区块链的网站,其中智能合约允许它连接到区块链中。说起来有点绕,为了更好地理解去中心化应用程序和智能合约,让我们先来了解传统网站的工作方式。

传统的Web:前端→API→数据库。

Dapp:前端→智能合约→区块链。

什么是智能合约?

Solidity 中智能合约类似 Java中的class

智能合约的实例就是在区块链上部署的一个智能合约。

Solidity支持多继承(multiple inheritance),但不支持方法重载(Overloading)。

智能合约主要由存储变量,函数和事件组成。

什么是IPFS?

IPFS是一个点对点的分布式文件系统,原理用基于内容的地址替代基于域名的地址,也就是用户寻找的不是某个地址而是储存在某个地方的内容,不需要验证发送者的身份,而只需要验证内容的哈希,通过这样可以让网页的速度更快、更安全、更健壮、更持久。

IPFS 技术就是把文件打碎,分散地存储在不同的硬盘里,下载的时候,再从这些散落在全球各地的硬盘里读取。

IPFS是一个协议,也是一个P2P网络,类似于BT网络,但是拥有更加强大的功能,使得IPFS拥有可以取代HTTP的潜力。

IPFS和传统文件系统的一个重要区别就是——内容寻址。顾名思义,就是文件的内容定了,其地址(访问路径)也就确定了。这和我们平时存放文件不一样。通常,我可以给一张图片随意更换文件名,把它拷贝到不同的路径。这样,一模一样的文件,其访问方式却随时变化,不可能根据文件的内容确定其访问路径。

相比较而言,内容寻址的IPFS就具有一个天然的优势——防篡改。数据只要修改了一个bit,其地址就彻底变化。

IPFS存储原理?

上传一个文件:

当我们往 IPFS 节点添加一个文件时,如果文件大小超过 256K(这个值可以设置),IPFS 会自动将文件分片,每个分片 256K,然后将切片分散存储到网络的各个节点中。每个分片都会生成唯一的哈希,然后把所有的分片的哈希值拼接之后在计算得到该文件哈希。 每个 IPFS 节点都会保存一张分布式的哈希表(DHT),包含数据块与目标节点的映射关系。无论哪个节点新增了数据,都会同步更新 DHT。

每个 IPFS 节点都会保存一张分布式的哈希表(DHT),包含数据块与目标节点的映射关系。无论哪个节点新增了数据,都会同步更新 DHT。

下载一个文件:

当我们需要访问这个文件的时候,IPFS 通过使用一个分布式哈希表,可以快速(在一个拥有 10,000,000 个节点的网络中只需要 20 跳) 地找到拥有数据的节点,获取文件的所有分片哈希,然后重新组合成完整的文件,并使用哈希验证这是否是正确的数据。

常见的七种加密算法及实现

数字签名

数字签名,简单来说就是通过提供 可鉴别数字信息 验证 自身身份 的一种方式。一套 数字签名 通常定义两种 互补 的运算,一个用于 签名,另一个用于 验证。分别由 发送者 持有能够 代表自己身份私钥 (私钥不可泄露),由 接受者 持有与私钥对应的 公钥 ,能够在 接受 到来自发送者信息时用于 验证 其身份。

img

对称加密和非对称加密

加密算法分对称加密非对称加密,其中对称加密算法的加密与解密密钥相同,非对称加密算法的加密密钥与解密密钥不同,此外,还有一类不需要密钥散列算法

常见的对称加密算法主要有DES、3DES、AES等,常见的非对称算法主要有RSA、DSA等,散列算法主要有SHA-1、MD5等。

对称加密

对称加密算法是应用较早的加密算法,又称为共享密钥加密算法。在对称加密算法中,使用的密钥只有一个,发送接收双方都使用这个密钥对数据进行加密解密。这就要求加密和解密方事先都必须知道加密的密钥。

img

非对称加密

非对称加密算法,又称为 公开密钥加密算法。它需要两个密钥,一个称为 公开密钥 (public key),即 公钥,另一个称为 私有密钥 (private key),即 私钥

因为 加密解密 使用的是两个不同的密钥,所以这种算法称为 非对称加密算法

img

八、秒杀项目(不全)

秒杀流程

img

https://blog.csdn.net/qq_46225886/article/details/107409512

九、分布式

高性能的实践方案

  1. 集群部署,通过负载均衡减轻单机压力。
  2. 多级缓存,包括静态数据使用CDN、本地缓存、分布式缓存等,以及对缓存场景中的热点key、缓存穿透、缓存并发、数据一致性等问题的处理。
  3. 分库分表和索引优化,以及借助搜索引擎解决复杂查询问题。
  4. 考虑NoSQL数据库的使用,比如HBase、TiDB等,但是团队必须熟悉这些组件,且有较强的运维能力。
  5. 异步化,将次要流程通过多线程、MQ、甚至延时任务进行异步处理。
  6. 限流,需要先考虑业务是否允许限流(比如秒杀场景是允许的),包括前端限流、Nginx接入层的限流、服务端的限流。
  7. 对流量进行削峰填谷,通过MQ承接流量。
  8. 并发处理,通过多线程将串行逻辑并行化。
  9. 预计算,比如抢红包场景,可以提前计算好红包金额缓存起来,发红包时直接使用即可。
  10. 缓存预热,通过异步任务提前预热数据到本地缓存或者分布式缓存中。
  11. 减少IO次数,比如数据库和缓存的批量读写、RPC的批量接口支持、或者通过冗余数据的方式干掉RPC调用。
  12. 减少IO时的数据包大小,包括采用轻量级的通信协议、合适的数据结构、去掉接口中的多余字段、减少缓存key的大小、压缩缓存value等。
  13. 程序逻辑优化,比如将大概率阻断执行流程的判断逻辑前置、For循环的计算逻辑优化,或者采用更高效的算法。
  14. 各种池化技术的使用和池大小的设置,包括HTTP请求池、线程池(考虑CPU密集型还是IO密集型设置核心参数)、数据库和Redis连接池等。
  15. JVM优化,包括新生代和老年代的大小、GC算法的选择等,尽可能减少GC频率和耗时。
  16. 锁选择,读多写少的场景用乐观锁,或者考虑通过分段锁的方式减少锁冲突。

上述方案无外乎从计算和 IO 两个维度考虑所有可能的优化点,需要有配套的监控系统实时了解当前的性能表现,并支撑你进行性能瓶颈分析,然后再遵循二八原则,抓主要矛盾进行优化。

高可用的实践方案

  1. 对等节点的故障转移,Nginx和服务治理框架均支持一个节点失败后访问另一个节点。
  2. 非对等节点的故障转移,通过心跳检测并实施主备切换(比如redis的哨兵模式或者集群模式、MySQL的主从切换等)。
  3. 接口层面的超时设置、重试策略和幂等设计。
  4. 降级处理:保证核心服务,牺牲非核心服务,必要时进行熔断;或者核心链路出问题时,有备选链路。
  5. 限流处理:对超过系统处理能力的请求直接拒绝或者返回错误码。
  6. MQ场景的消息可靠性保证,包括producer端的重试机制、broker侧的持久化、consumer端的ack机制等。
  7. 灰度发布,能支持按机器维度进行小流量部署,观察系统日志和业务指标,等运行平稳后再推全量。
  8. 监控报警:全方位的监控体系,包括最基础的CPU、内存、磁盘、网络的监控,以及Web服务器、JVM、数据库、各类中间件的监控和业务指标的监控。
  9. 灾备演练:类似当前的“混沌工程”,对系统进行一些破坏性手段,观察局部故障是否会引起可用性问题。

高可用的方案主要从冗余、取舍、系统运维3个方向考虑,同时需要有配套的值班机制和故障处理流程,当出现线上问题时,可及时跟进处理。

高扩展的实践方案

  1. 合理的分层架构:比如上面谈到的互联网最常见的分层架构,另外还能进一步按照数据访问层、业务逻辑层对微服务做更细粒度的分层(但是需要评估性能,会存在网络多一跳的情况)。
  2. 存储层的拆分:按照业务维度做垂直拆分、按照数据特征维度进一步做水平拆分(分库分表)。
  3. 业务层的拆分:最常见的是按照业务维度拆(比如电商场景的商品服务、订单服务等),也可以按照核心接口和非核心接口拆,还可以按照请求源拆(比如To C和To B,APP和H5)。

线上CPU100%,问题排查:

img

Top -Hp 43987

img

线上进程Kill不掉:

img

服务器存储空间快满了:

img

接口基本功能

边界值

外部依赖RPC

可用性设计

性能测试

容灾设计

事务验证

十、常见问题

基本数据类型

内置数据类型

byte、short、int、long、float、 double、char、boolean;

引用类型

Java 中的基本数据类型只有 8 个:byte、short、int、long、float、 double、char、boolean;除了基本类型(primitive type)和枚举类型(en umeration type),剩下的都是引用类型(reference type)。

Length&length()

数组没有 length()方法,有 length 的属性。String 有 length()方法。Jav aScript 中,获得字符串的长度是通过 length 属性得到的,这一点容易和 Java 混淆。

接口和抽象类

抽象类是用来捕捉子类的通用特性的。它不能被实例化,只能被用作子类的超类。抽象类是被用来创建继承层级里子类的模板。

接口是抽象方法的集合。如果一个类实现了某个接口,那么它就继承了这个接口的抽象方法。

什么时候使用抽象类和接口:

如果拥有一些方法并且想让它们中的一些有默认实现,使用抽象类。

如果想实现多重继承,必须使用接口。由于Java不支持多继承,子类不能够继承多个类,但可以实现多个接口。

如果基本功能在不断改变,那么就需要使用抽象类。如果不断改变基本功能并且使用接口,那么就需要改变所有实现了该接口的类。

OOM类型与调优

除了程序计数器不会抛出OOM外,其他各个内存区域都可能会抛出OOM。

最常见的OOM情况有以下三种:

OOM分析--heapdump

要dump堆的内存镜像,可以采用如下两种方式:

dump堆内存信息后,需要对dump出的文件进行分析,从而找到OOM的原因。常用的工具有:

双亲委派

双亲委派模型的工作流程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把请求委托给父加载器去完成,依次向上,因此,所有的类加载请求最终都应该被传递到顶层的启动类加载器中,只有当父加载器在它的搜索范围中没有找到所需的类时,即无法完成该加载,子加载器才会尝试自己去加载该类。

双亲委派机制:

AQS

Java并发包(JUC)中提供了很多并发工具,这其中,很多我们耳熟能详的并发工具,譬如ReentrangLock、Semaphore,它们的实现都用到了一个共同的基类--AbstractQueuedSynchronizer,简称AQS。AQS是一个用来构建锁和同步器的框架,使用AQS能简单且高效地构造出应用广泛的大量的同步器,比如我们提到的ReentrantLock,Semaphore,其他的诸如ReentrantReadWriteLock,SynchronousQueue,FutureTask等等皆是基于AQS的。

AQS使用一个int成员变量来表示同步状态,通过内置的FIFO队列来完成获取资源线程的排队工作。状态信息通过procted类型的getState,setState,compareAndSetState进行操作

AQS支持两种同步方式:1.独占式2.共享式。这样方便使用者实现不同类型的同步组件,独占式如ReentrantLock,共享式如Semaphore,CountDownLatch,组合式的如ReentrantReadWriteLock。总之,AQS为使用提供了底层支撑,如何组装实现,使用者可以自由发挥。同步器的设计是基于模板方法模式的,一般的使用方式是这样:

1.使用者继承AbstractQueuedSynchronizer并重写指定的方法。(这些重写方法很简单,无非是对于共享资源state的获取和释放)

2.将AQS组合在自定义同步组件的实现中,并调用其模板方法,而这些模板方法会调用使用者重写的方法。

volatile

一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,那么就具备了两层语义:1)保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。

2)禁止进行指令重排序

《深入理解Java虚拟机》中对volatile的描述:“观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令”

lock前缀指令实际上相当于一个内存屏障(也成内存栅栏),内存屏障会提供3个功能:

1)它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;

2)它会强制将对缓存的修改操作立即写入主存;

3)如果是写操作,它会导致其他CPU中对应的缓存行无效。

因为volatile关键字无法保证操作的原子性。通常来说,使用volatile必须具备以下2个条件:

1)对变量的写操作不依赖于当前值

2)该变量没有包含在具有其他变量的不变式中

多态

多态就是指程序中定义的引用变量所指向的具体类型和通过该引用变量发出的方法调用在编程时并不确定,而是在程序运行期间才确定,即一个引用变量倒底会指向哪个类的实例对象,该引用变量发出的方法调用到底是哪个类中实现的方法,必须在由程序运行期间才能决定。因为在程序运行时才确定具体的类,这样,不用修改源程序代码,就可以让引用变量绑定到各种不同的类实现上,从而导致该引用调用的具体方法随之改变,即不修改程序代码就可以改变程序运行时所绑定的具体代码,让程序可以选择多个运行状态,这就是多态性。

Java实现多态有三个必要条件:继承、重写、向上转型。

\1. 继承:在多态中必须存在有继承关系的子类和父类。

\2. 重写:子类对父类中某些方法进行重新定义,在调用这些方法时就会调用子类的方法。

\3. 向上转型:在多态中需要将子类的引用赋给父类对象,只有这样该引用才能够具备技能调用父类的方法和子类的方法。

Java中有两种形式可以实现多态,继承和接口:

\1. 基于继承的实现机制主要表现在父类和继承该父类的一个或多个子类对某些方法的重写,多个子类对同一方法的重写可以表现出不同的行为。

\2. 基于接口的多态中,指向接口的引用必须是指定这实现了该接口的一个类的实例程序,在运行时,根据对象引用的实际类型来执行对应的方法。

访问修饰符

作用域  当前类  同一package  子孙类  其它package

public   √ √    √    √

protected √    √   √ ×

friendly   √ √   × ×

private   √    ×  × ×

线程安全

synchronized关键字,它通过互斥的方式保证同步。同时synchronize对于同一条线程来说是可重入的;其次它也是阻塞的。我们知道java线程是映射到操作系统上的,而且是混用的内核态线程和用户态线程(N:M),而将线程从阻塞/唤醒,需要将线程从用户态转换到内核态,这样会消耗太多的资源,所以synchronize是一个重量级锁。

ReentrantLock类: volatile 变量 + CAS设置值 + AQS

img

ReentrantLock锁的实现步骤总结为三点

1、未竞争到锁的线程将会被CAS为一个链表结构并且被挂起。

2、竞争到锁的线程执行完后释放锁并且将唤醒链表中的下一个节点。

3、被唤醒的节点将从被挂起的地方继续执行逻辑。

区别:

1.可重入性

字面的意思就是可以再次进入的锁,synchronized其实也是可重锁,同一线程每进入一次,锁的计数器都会加一,在释放锁是计数器都会减一,只有当计数器为0 时才能释放锁

2.锁的实现

ReentrantLock是JDK实现的 Synchronized 是JVM实现

前者可以直接看到源码,后者实现难以看到

3.性能的区别

在Synchronized优化以前,synchronized的性能是比ReenTrantLock差很多的,但是自从Synchronized引入了偏向锁,轻量级锁(自旋锁)后,两者的性能就差不多了,在两种方法都可用的情况下,官方甚至建议使用synchronized,其实synchronized的优化我感觉就借鉴了ReenTrantLock中的CAS技术。都是试图在用户态就把加锁问题解决,避免进入内核态的线程阻塞。

4.功能的区别

便利性:很明显Synchronized的使用比较方便简洁,并且由编译器去保证锁的加锁和释放,而ReenTrantLock需要手工声明来加锁和释放锁,为了避免忘记手工释放锁造成死锁,所以最好在finally中声明释放锁。

锁的细粒度和灵活度:很明显ReenTrantLock优于Synchronized

当你需要时候一下三种功能是需要使用ReentrantLock

ReentranLock 可以指定公平锁还是非公平锁

(公共锁就是先等待的线程先获得锁)

实现自旋锁,通过循环调用CAS操作来实现加锁,性能比较好,避免进入内核态的线程阻塞。

提供了Condition类,可以分组唤醒需要唤醒的线程

提供能够中断等待锁的线程的机制,lock.lockInterruptibly()

具体使用场景要根据实际的业务进行分析

使用Synchronized时不需要释放锁,jvm会帮助我们做释放锁的操作

常用容器

(1)vector

vector是一种动态数组,在内存中具有连续的存储空间,支持快速随机访问。由于具有连续的存储空间,所以在插入和删除操作方面,效率比较慢。vector有多个构造函数,默认的构造函数是构造一个初始长度为0的内存空间,且分配的内存空间是以2的倍数动态增长的,即内存空间增长是按照20,21,22,23.....增长的,在push_back的过程中,若发现分配的内存空间不足,则重新分配一段连续的内存空间,其大小是现在连续空间的2倍,再将原先空间中的元素复制到新的空间中,性能消耗比较大,尤其是当元素是非内部数据时(非内部数据往往构造及拷贝构造函数相当复杂)。

API:

vector v;//采用模板实现类实现,默认构造函数

assign(begin(),end());//将【begin(),end()】区间中的元素拷贝给本身

size();//返回元素容器中元素个数

at(int idx);//返回索引idx所指的数据,如果idx越界,抛出out_of_range异常

(2)deque

deque和vector类似,支持快速随机访问。二者最大的区别在于,vector只能在末端插入数据,而deque支持双端插入数据。deque的内存空间分布是小片的连续,小片间用链表相连,实际上内部有一个map的指针。deque空间的重新分配要比vector快,重新分配空间后,原有的元素是不需要拷贝的。

API:

dequedeqT;//默认构造形式

assign(begin,end);//将【begin,end】区间的数据拷贝赋值给自身

deque.size();//返回容器中元素的个数

push_back(elem);//在容器尾部添加一个数据

(3)list

list是一个双向链表,因此它的内存空间是可以不连续的,通过指针来进行数据的访问,这使list的随机存储变得非常低效,因此list没有提供[]操作符的重载。但list可以很好地支持任意地方的插入和删除,只需移动相应的指针即可。

API:

listlstT;//list采用模板类实现对象的默认构造函数

push_back(elem);//在容器尾部加入一个元素

size();//返回元素容器中元素个数

empty();//判断容器是否为空

(4)map

map是一种关联容器,该容器用唯一的关键字来映射相应的值,即具有key-value功能。map内部自建一棵红黑树(一种自平衡二叉树),这棵树具有数据自动排序的功能,所以在map内部所有的数据都是有序的,以二叉树的形式进行组织。

API:

map<t1,T2>mapT;//map默认构造函数

size();//返回元素中元素的数目

empty();//判断容器是否为空

clear();//删除所有元素

(5)set

set也是一种关联性容器,它同map一样,底层使用红黑树实现,插入删除操作时仅仅移动指针即可,不涉及内存的移动和拷贝,所以效率比较高。set中的元素都是唯一的,而且默认情况下会对元素进行升序排列。所以在set中,不能直接改变元素值,因为那样会打乱原本正确的顺序,要改变元素值必须先删除旧元素,再插入新元素。不提供直接存取元素的任何操作函数,只能通过迭代器进行间接存取。

API:

setsT;//set默认构造函数

size();//返回容器中元素的数目

empty();//判断容器是否为空

insert(elem);//在容器中插入元素

clear();//清除所有元素

(6)queue

queue是一个队列,实现先进先出功能,queue不是标准的STL容器,却以标准的STL容器为基础。queue是在deque的基础上封装的。之所以选择deque而不选择vector是因为deque在删除元素的时候释放空间,同时在重新申请空间的时候无需拷贝所有元素。

API:

queuequeT;//queue采用模板类实现,queue对象的默认构造形式

push(elem);//往队尾添加元素

(7)stack

stack是实现先进后出的功能,和queue一样,也是内部封装了deque,这也是为啥称为容器适配器的原因吧(纯属猜测)。自己不直接维护被控序列的模板类,而是它存储的容器对象来为它实现所有的功能。stack的源代码原理和实现方式均跟queue相同。

API:

stackstkT;//采用模板类实现,stack对象的默认构造形式

push(elem);//向栈顶添加元素

十一、HR面

自我介绍

名字+擅长技能+过去成绩+未来价值,明确表达自己想来。。工作的期望

面试官你们好,我叫XX,就读于XX。在大学我每学年的综合成绩都在年级前10%,连续三年获得国家励志奖学金、获得过一次我校最高企业奖学金和2020年度中国大学生自强之星”奖学金。在竞赛方面,我也收获颇丰。获得过全国高校区块链大赛国家一等奖、互联网+大学生创新创业大赛国赛银奖铜奖,省赛金奖银奖、中国大学生生命科学竞赛国家一等奖、挑战杯课外学术作品竞赛省赛二等奖等等30余项省级校级竞赛奖励。在科研方面我主持的大学生创新创业训练计划项目,产出了两项新型实用专利和一份软件著作权,获得了省级优秀结项并评选为优秀成果一等奖。在实习方面,从去年11月份至今在字节跳动的广告计费部门做测试开发实习生,主要的工作内容是服务端接口测试,负责了计费系统风控模块的业务测试,掌握了基本的测试方法和使用基本的测试工具。

一直以来阿里都是我的理想公司,我也希望能够去更大的平台学习更全面的技术体系,能够为阿里贡献自己的力量。

阿里企业价值观

img

讲一下实习遇到的难点

mock问题

一开始负责风控模块新业务 没有过多的参考资料

百度查找相关资料

你希望通过这份工作获得什么?

对我来说,最重要的是自己所做的工作是否适合我。我的意思是说,这份工作应该能让我发挥专长——这会给我带来一种满足感。我还希望所做的工作能够对我目前的技能水平形成挑战,从而能促使我提升自己。

你个人短期和长期目标分别是什么?

不论长期还是短期,我的策略是根据当前目标评价自己并修改自己的计划。

同所有现实目标一样,我的目标经常改变。不论在长期还是短期,我的个人策略是根据当前目标评价自己所处的位置,然后相应地修改自己的计划。比如,我每五年就制定一项个人计划,这个计划中包含一个总体目标和一系列短期目标。每6个月我就回顾一下自己的进展,然后做出必要的修改。

给你一个任务,你会怎么做?

尝试谈谈,你会怎么做。如果做到你会怎么样,做不到你会如何去调整。

第一、分析项目的优劣点,明确项目的目标,然后开始分析,看自己的角度和领导的想法、产品的方向是否相契合。这个主要处理问题站的角度不一样,或者利弊分析不一样。所以首先要会站在领导的角度去考虑这个问题。

你对加班的看法?

如果是工作需要我会义不容辞加班,可以全身心的投入工作。

但同时,我也会提高工作效率,减少不必要的加班。

工作出现失误,给本公司造成经济损失,你认为该怎么办?

①我本意是为公司努力工作,如果造成经济损失,我认为首要的问题是想方设法去弥补或挽回经济损失。如果我无能力负责,希望单位帮助解决。

②分清责任,各负其责,如果是我的责任,我甘愿受罚;如果是一个我负责的团队中别人的失误,也不能幸灾乐祸,作为一个团队,需要互相提携共同完成工作,安慰同事并且帮助同事查找原因总结经验。

③总结经验教训,一个人的一生不可能不犯错误,重要的是能从自己的或者是别人的错误中吸取经验教训,并在今后的工作中避免发生同类的错误。检讨自己的工作方法、分析问题的深度和力度是否不够,以致出现了本可以避免的错误。

如果你在这次面试中没有被录用,你怎么打算?

①要敢于面对,面对这次失败不气馁,接受已经失去了这次机会就不会回头这个现实,从心理意志和精神上体现出对这次失败的抵抗力。要有自信,相信自己经历了这次之后经过努力一定能行,能够超越自我。

②善于反思,对于这次面试经验要认真总结,思考剖析,能够从自身的角度找差距。正确对待自己,实事求是地评价自己,辩证的看待自己的长短得失,做一个明白人。

③走出阴影,要克服这一次失败带给自己的心理压力,时刻牢记自己弱点,防患于未然,加强学习,提高自身素质。

④认真工作,回到原单位岗位上后,要实实在在、踏踏实实地工作,三十六行、行行出状元,争取在本岗位上做出一定的成绩。

你最大的缺点是什么?

我的英语书面翻译是过关的,但口语不够好。所以我现在已经报名参加了一个英语口语的培训班,希望能尽快弥补这方面的差距。

我有缺点(Reality),但是针对缺点,我已经做了改善(Action),并获得了成果(Result)。

讲一下实习中的收获

从需要人代,到独立解决问题独当一面,负责风控业务的测试

你觉得自己有什么优势

沟通能力强

抗压能力强

完美主义者

个人成长影响最大的事情或者人

保研失利

对于测试开发岗位的理解

业务测试 + 测试工具的构建

测试开发首先离不开测试,而软件测试是指,在规定的条件下对程序进行操作,以发现程序错误,衡量软件质量,并对其是否能满足设计要求进行评估的过程。

而且,现在不仅仅是通过手工测试来发现定位Bug,也会通过编写脚本、测试工具来完成自动化测试,因此,对于测试开发人员来说,他除了保证产品质量之外,还要编写脚本以及开发测试工具。这就是我对测试开发的一点理解。

承担一些编码开发的工作,用来制作一些自动化测试的脚本,或者自动化测试的工具,又或者另外的一些在软件测试工作中用到的提高工作效率的小工具什么的。

首先,在近几年,国内对软件测试越来越重视,测试的前景是非常好的。

其次,测试在一个项目开发的过程中是非常重要的一环。开发人员很难在开发的时候又要全面兼顾产品的质量,测试人员就是项目内部的最后把关者,最大程度的保证项目上线不会出现问题。责任非常大,责任越大成就感就越大。我很喜欢这样的工作。

测试人员应具备的素质

良好的测试基础理论;

首先要有一定的沟通协调能力,因为测试人员经常会与开发人员接触处理一些问题,需要心平气和地沟通。还需要有一定的耐心,不能放过每一个错误;

要有责任感,要尽自己最大的能力,保证产品的质量。

要有好奇心,保持一种怀疑的态度测试人员的任务是找出缺陷,不是证明没有缺陷,所以需要保持怀疑。

要细心;乐观;

你对速卖通有什么了解

俄罗斯 美国 西班牙 巴西 法国最大的交易国家

速卖通驱动品牌出海,跨境电商成为企业品牌升级新战场

阿里的愿景全球买全球卖

全球最大的跨境电商平台

在跨境电商,平台本身具有供货商,进入的门槛比其他平台低,全球市场广阔,订单多,交易活跃等特点,加上阿里巴巴集团的大力支持和资金的投入,近几年业务的发展特别迅猛。

中国是个制造大国,对外输出

全球速卖通:国货出口 面向全球

LAZADA:国货出口 针对东南亚

天猫国际:洋货进口

天猫出海:国货出口 针对海外华人

淘宝、天猫、闲鱼、躺平的技术与业务;

职业规划

现在还不行,需要在工作中拓展技术的深度和广度,加深对业务的敏感性,适应公司的需求

比较热衷于技术,一方面希望能够在日常工作中提高自己的技术水平,另一方面也通过技术来解决实际问题产生价值。

前几年先积累经验,不断地更新自己、改正自己,然后一步一步朝着高级测试开发工程师走去。