JAVA工程师面试题汇总

一:mysql

1、mysql Nested-Loop算法,Block-Nested-Loop算法,join优化

答:Nested-Loop:选取(mysql自动优化选择)一个表作为驱动表,循环驱动表结果集,查询下一个表的数据,然后合并结果集。如果是多表join,则将前一次合并的结果作为循环数据,查询下一个表。
Block-Nested-Loop(默认开启):在NL算法的基础上,将外层循环的结果缓存起来,内层循环一次比较多条数据,减少总循环次数。比如,外层查询有100条结果,缓存10条,每次内层循环比较10条数据,则只需要循环10次。

优化:

  1. 使用小结果集作为驱动表,减少总循环次数(自动优化)
  2. 优先优化内层查询,减少每一次循环的时间
  3. 关联查询条件建立索引
  4. 设置适当的join_buffer_size,当join_buffer_size大于外层结果集时,再增大join_buffer_size不会变得更快

2、mysql索引原理

答:mysql使用B+Tree(平衡多路查找树)作为索引的数据结构,而innoDB和MyIsAm对索引的实现方式略有不同。使用B+Tree而不使用平衡二叉树、红黑树等结构的原因和计算机物理结构有关。索引本身需要存储,而索引一般比较大,因此索引往往存在磁盘中。而磁盘IO效率非常低,所以判断索引结构好坏的一个重要指标就是磁盘IO次数。计算机为提高磁盘IO效率,在读取数据时会进行预读,预读的大小为页的整数倍。数据库系统将B+Tree结点的大小刚好设置为一页的大小,因此一次IO就能完全载入一个结点。因为B+Tree的一个结点中会存储多个关键字,所以B+Tree的高度相比其他几种树会低很多,IO次数也会少很多。

MyIsAm索引:索引文件与数据文件是分离的。索引的叶子结点存储数据行的地址。因为相邻的叶子节点分配的物理地址并不一定相邻,所以这种索引是非聚簇索引。

InnoDB的主键索引:数据文件就是索引文件。索引的叶子结点存储完整的数据记录。逻辑相邻的数据行在物理上的存储也是相邻的,属于聚簇索引。若没有定义主键,则mysql选取第一个唯一非空的索引作为聚簇索引。若也没有唯一非空的索引,则会创建一个隐藏的聚簇索引。建表时最好使用无意义的自增列作为主键,每次插入数据只需要按顺序往后排即可。如果主键不是自增的,插入新结点时可能导致结点分裂(一个结点保存的数据记录超过一定大小就会分裂),进而导致后序其他的结点分裂,在数据量大的时候,效率非常低下。

InnoDB的普通索引:叶子结点存储主键值,普通索引的顺序与主键索引不一定一样,所以是非聚簇索引。

3、索引的选择性

答:索引的选择性=不重复的值得个数/总记录数,取值范围为(0,1]。索引的选择性越小,使用索引的效果越不明显(越接近全表扫描)。极端情况下,假设只有一种不重复的值,使用索引(需要扫描整个B+Tree或hash值查出的记录为所有记录)和全表扫描完全一样。

4、hash索引和B+Tree索引的区别

答:

  1. hash索引只能用于=、in和!=的查询,不能用于范围查询,不能用于排序。因为hash索引通过查找hash值一样的数据来进行索引,而hash之前的值大小与hash值大小不一定存在对应关系。

  2. 对于组合索引,不能利用前面的一个或多个索引查询

  3. hash存在冲突的情况,因此查到一个hash值之后,还需要在多个数据记录之间选择

  4. 除上述情况外,hash索引效率远高于B+Tree

5、快照读和当前读

答:快照读指读的是数据的快照(历史版本),当前读指读取实时数据。

  1.  快照读(snapshot read)

简单的select操作(不包括 select ... lock in share mode, select ... for update)

  1. 当前读(current read)

select ... lock in share mode

select ... for update

insert

update

delete

6、mvcc(多版本并发控制)机制

答:mvcc只有在Read Commited和Repeatable Read隔离级别下有效。在Read Uncommited下,每次读都是当前读(读最新行,而不是符合系统版本号的行),在Sirializable级别下,所有读都加锁,因此这两种级别不适用mvcc。在InnoDB的 mvcc是通过在每行记录后面保存两个隐藏的列来实现的。这两个列一个保存了行的创建时系统版本号,一个保存了行的删除时系统版本号。每开始一个新事务,系统版本号都会递增。事务开始时的系统版本号就是事务版本号。下面是crud的mvcc实现(注:在事务a、b并发执行的情况下,假设事务a读取行,事务b插入(更新、删除)行。若事务b先执行,则会锁行,事务a会等b执行完之后读取最新结果,这种情况与mvcc机制无关。若事务a先执行,b是插入操作时,b的版本号大于a,a不能查到新增的行;b是更新操作时,新增的行版本号大于a,读到的仍是原始行;b是删除操作时,删除版本号大于a的版本号,仍能查到b删除的这行。以上情况无论b是否已经提交都成立,这样就解决了快照读下的脏读、不可重复读以及幻读):

  1. select:只有符合以下两个条件的记录才会作为返回结果

    • 行的创建版本号小于或等于事务版本号。这可以确保事务读取的行,要么是事务开始前已经存在的,要么是事务自身插入或者修改过得。。

    • 行的删除版本要么未定义,要么大于当前事务版本号。这可以确保事务读到的行,在事务开始之前未被删除。

  2. insert:新插入的每一行保存当前系统版本号作为行版本号。

  3. delete:删除的每一行保存当前系统版本号作为行删除版本号。

  4. update:插入一条新纪录,保存当前系统版本号作为行版本号,同时保存当前系统版本号作为原来的行的删除版本号。

7、如何解决当前读引起的幻读

答:mvcc无法解决因当前读(增、删、改均先有一次当前读)引起的幻读。示例:

  1. 事务a:select * from table_name

  2. 事务b:insert into table_name (a) values (1),commit b

  3. 事务a:select * from table_name

  4. 事务a:update table_name set a=1 where a=1

  5. 事务a:select * from table_name,commit a

1、3之间虽然被事务b插入了一条数据,但这两次查询结果是一样的,这符合mvcc的规则,此时没有出现幻读。执行4的时候,由于update是当前读,会读到事务b提交的最新数据,这次更新操作会把新行的版本号和原始行的删除版本号设置为事务a的版本号。因此,执行查询5的时候,不能查到b新增的行(删除版本号相等),却查出了更新之后的新行(行版本号相等)。同一个事务里面的三次查询,第三次查到的结果和第一第二次查询结果不同,也可以算是幻读。

解决方法:为查询加共享锁LOCK IN SHARE MODE或排它锁FOR UPDATE,如select * from table_name lock in share mode,若字段a没有唯一索引,还需要使用RR隔离级别(在InnoDB中,RR与RC的唯一区别就是RR使用了间隙锁)。

8、InnoDB的锁

答:根据锁的获取权限,可以分为共享锁s,排它锁x,以及意向共享锁is,意向排它锁ix。根据锁的范围,可以分为Record Lock(行锁):锁定一行记录的索引;Gap Lock(间隙锁):锁定范围内不存在的索引(例如表中有数据1,5,6,对不存在的数据2加锁就是间隙锁);Next-Key Lock:上述两种加起来。

  1. 共享锁和排它锁锁定行,意向锁锁定表。加锁顺序为先加意向锁,获得意向锁之后再加其他锁。这四种锁的兼容关系如下:

由图可知,一个事务获取了意向锁,其他事务也可以获取该表的意向锁。一个事务获取了共享锁之后,其他事务只能获取意向共享锁和共享锁。一个事务获取排它锁之后,其他事务不能获取任何类型的锁。锁是加在索引上的,不同类型的索引加的锁类型不同,示例语句delete from table_name where a = 8;

  1. a是主键索引,在这条记录的索引上加行锁

  2. a是非主键唯一索引,这条记录的唯一索引和主键索引均加行锁

  3. a无索引,在RC隔离级别下,表的所有行加行锁。在RR隔离级别下,所有行加行锁和间隙锁。

  4. a是非唯一索引,在RC隔离级别下,所有满足条件的记录均加行锁。在RR隔离级别下,除行锁外,还会加间隙锁(满足条件的值两侧的间隙),这两个锁合起来即Next-Key Lock。如数据库有记录5,8,9,则锁的范围是(5,9),因为如果再插入一条8,则新结点一定插入在(5,9)这个间隙的结点上。正是间隙锁解决了当前读的幻读问题。唯一索引不需要间隙锁是因为唯一索引下的同一个值只会插入一次。

9、不同sql语句的加锁类型

  1. SELECT ... FROM在RC和RR隔离级别下不加锁。而在SERIALIZABLE隔离级别下是锁定读,会在扫描的索引记录范围内添加Next-key行锁,如果扫描的是唯一索引,那么只会添加Record lock
  2. SELECT ... FROM ... LOCK IN SHARE MODE在扫描到的索引记录上添加S模式的Next-key行锁,同样的如果扫描的是唯一索引,那么只会添加S模式的Record lock。
  3. SELECT ... FROM ... FOR UPDATE在扫描到的索引记录上添加X模式的Next-key行锁,同样的如果扫描的是唯一索引,那么只会添加X模式的Record lock。
  4. UPDATE ... WHERE ...语句会在扫描到的所有记录上添加X模式的next-key lock(即便被更新的行不存在),同样的如果扫描的是唯一索引,那么只会添加X模式的Record lock。
  5. DELETE FROM ... WHERE ...语句会在扫描到的所有记录上添加X模式的next-key lock(即便被删的行不存在),同样的如果扫描的是唯一索引,那么只会添加X模式的Record lock。
  6. INSERT语句会在插入的行上添加X模式的Record lock和IX模式的gap lock.
  7. INSERT ... ON DUPLICATE KEY UPDATE,这种插入语句和普通的INSERT语句区别在于,他会在发生重复性键值错误时向索引记录上添加X行锁
  8. REPLACE语句可以看做是INSERT ... ON DUPLICATE KEY UPDATE的简写
  9. INSERT INTO T SELECT ... FROM S WHERE ...语句会在T表的每个被插入的行上添加X模式的record lock(无gap锁)
  10. CREATE TABLE ... SELECT ...语句的加锁机制与INSERT INTO T SELECT ... FROM S WHERE ...完全一致

10、如何避免死锁

答:死锁产生的原因是不同的事务获取锁的顺序相反(不一定非要多条sql语句才会形成死锁,如果两个事务分别通过两个非主键索引更新数据,这两个索引对应的主键顺序刚好相反时就形成了死锁。例如事务A执行update t set name="123" where age>10,事务B执行delete from t where id >=1。这时假如A的结果集对应的id是(2,4,1,3,5),事务B的结果集是(1,2,3,4,5),则可能死锁)。常见的避免死锁的方法有:

  1. 尽量以相同的顺序访问表,若某两个事务因使用不同的非主键索引引起死锁,可以尝试拆分sql语句,通过非主键索引查出主键,再用主键更新记录。

  2. 尝试升级锁定的颗粒度,通过表锁减少死锁的概率

二:java

1、内存模型

答:

在jdk1.8中,元数据区取代了永久代,类信息存放在本地内存中,常量和静态变量放在堆中。

堆内存细分为新生代、老年代、永久代

新生代分为eden、survivor1、survivor2三部分,垃圾回收采用复制算法。大部分对象在eden区中生成。当eden区满时,还存活的对象将被复制到s1并清空eden区。当s1也满了时,eden和s1中还存活的对象被复制到s2并清空eden和s1。经过若干次ygc之后还存活的对象将进入老年代。

老年代的对象存活率较高,且老年代占用内存大,采用复制算法效率很低并且会浪费50%的空间。因此老年代垃圾回收采用标记整理算法,所有被标记的存活对象都向一端移动,然后直接清理掉边界之外的内存。

2、CopyOnWriteArrayList和Vector或通过Collections.synchronizedList()获取到的SynchronizedList的区别

答:SynchronizedList和Vector属于同步容器,所有方法均加锁,并发性能很差。而CopyOnWriteArrayList只对写操作加锁,修改操作并不直接修改原始数组的内容,而是创建一个新数组并把原数组的引用指向新数组。由于该数组引用是volatile的并且更新和添加操作直接替换引用,因此不需要对读加锁。CopyOnWriteArrayList体现了读写分离的思想,对于并发读取的性能远高于SynchronizedList。CopyOnWriteArrayList没加读锁,因此获取到的数据不能保证实时一致,比如正在执行add操作,但还没有执行到数组赋值的那一行,则读到的还是add之前的数据。另外,CopyOnWriteArrayList占用内存较多,只有在遍历、获取操作远多于写操作是才考虑使用。SynchronizedList无法在遍历时修改(可以使用iterator的方法修改,但需要额外的同步处理),而CopyOnWriteArrayList可以(但不能通过iterator方法修改,其iterator没有提供相应的方法),但遍历时不会获取到修改的部分(遍历的是那一刻的数组副本)。

3、ConcurrentHashMap和HashTable或通过Collections.synchronizedMap()获取到的SynchronizedMap的区别

答:HashTable和SynchronizedMap属于同步容器,所有方法均加锁。ConcurrentHashMap不对整个方法加锁。get操作直接获取内存最新值,put操作若table的当前位置为空,则使用CAS插入新结点,若不为空,则对当前位置加锁后插入。ConcurrentHashMap相对同步容器大大增加了并发度。由于get、size等方法是无锁的,因此不能实时的获取(如get一个key的同时put,get获取不到但put操作完成后这个key是存在的),size计算的也不是准确值。同步容器只能在使用iterator遍历时,使用iterator的remove方法做删除操作,ConcurrentHashMap可以在遍历的同时调用put或remove。

4、HashMap的容量为什么是2的k次幂

答:HashMap的结构是一个链表(或红黑树)数组,不同的Node根据hash值散列在table数组中,hash值相同的Node组成一个链表(或红黑树)。table.length>64并且链表长度>8时,会将链表结构转化成红黑树,以此来提升hash值相同时的查找效率。当table.length>threshold(容量*负载因子),或者table.length<=64并且链表长度>8时,会发生扩容,每次扩容之后的容量都是2的k次幂(左移一位)。Node的hash值对table.length取模就可以得到Node在table中的下标,但取模运算%是非常耗时的。我们把table.length记为n,当n=2^k(2的k次幂)时满足下列等式:hash%2^k=hash&(2^k-1)=hash&(n-1),因此将table.length设置为2的k次幂即可把低效的取模运算转化成高效的位运算。

5、HashMap resize原理

答:

  1. 该链表只有一个链表结点,直接rehash

  2. 该结点是树结点,略。。

  3. 该链表有多个结点:由于是根据取模操作计算位置,而不同的hash值取模可能相同,因此不同的hash值也可能存在同一个链表上。而扩容之后hash值对newCap取模与原来不一定相同,因此不能简单的移动整个链表。HashMap根据hash&(n-1)计算key在table中的位置,其中n=2^k,扩容后n=2^(k+1),因此n-1与扩容前相比在高位多了一个1(若2^k-1=1111,那么2^(k+1)-1=11111).若hash在对应的位置为0,则&操作之后与扩容前相同;若hash在对应的位置为1,则&操作之后相当于增加了10000(等于oldCap的值2^k)。即原链表上的结点在扩容之后的位置只有两种可能,要么还在原位置,要么在原位置+oldCap的位置。遍历原链表,根据hash对应oldCap的最高位是0还是1分成两个链表。下图a为扩容前两个hash值对n-1做&操作的过程,图b为扩容后的过程。

6、JAVA的CAS(compare and swap)

答:一个线程间共享的变量,首先在主存中会保留一份,然后每个线程的工作内存也会保留一份副本。多个线程并发执行时,工作内存的值可能和主内存值不一致,因此导致计算结果错误。以下几种方式可以确保线程安全

  1. synchronized是悲观锁,在锁定范围内无论未来是否会发生冲突都同时只能有一个线程可以执行。获取锁的时候读取主内存值到工作内存,释放锁的时候把工作内存写入主内存。

  2. 使用volatile关键字,每次更新操作都写入内存,每次读操作都读取内存值,并且不使用重排序。可以保证更新操作对其他线程可见,但不能保证复合操作的原子性,如i++这种读-改-写操作。如果修饰的是对象或者数组,只能保证引用的可见性,不能保证对象的属性或数组元素可见性。

  3. 使用CAS。CAS操作:只有当内存值V和预期值A相同时,才将内存值V修改成新值B,否则不修改。下图是Unsafe类中的一个方法,这个方法使用CAS实现了i+=n的操作。这种使用死循环进行cas操作代替互斥锁(如synchronize,需要进行挂起操作)的方式也称为cas自旋锁。这个方法相当于乐观锁,并不在一开始就加锁,而是发生冲突时等待直到冲突解决。CAS通常与volatile或getIntVolatile()一起使用,先获取内存值,然后在CAS中判断刚刚获取到的值是否过期,没过期则修改。使用CAS算法可以保证单个变量的复合操作也是原子的,但不能保证多个变量的操作原子性,多个变量的原子性只能使用synchronize。JAVA中可以通过Unsafe类执行CAS操作(Unsafe封装了很多直接操作内存的方法,类似C语言),但并不建议直接使用Unsafe类。jdk并发相关的类中大量使用了Unsafe类。

7、线程池原理

答:线程池ExecutorService可以通过Executors的工厂方法创建,返回一个ThreadPoolExecutor对象。ThreadPoolExecutor中保存了两个数据结构,即BlockingQueue和HashSet。BlockingQueue中保存的是通过submit()或execute()方法生产的任务,HashSet中保存的是消费者Worker。每个Worker都会新建一个线程,并无限循环调用BlockingQueue的take()方法获取任务执行。当Worker执行任务被中断时跳出无限循环,并将该worker从HashSet中清除。调用shutdown()方法会把线程池状态设置为SHUTDOWN,并中断空闲的Worker(worker执行任务时加锁,shutdown()时会尝试获取锁,获取不到锁则不中断)。shutdownNow()方法把线程池状态设置为STOP,并中断所有Worker。只有RUNNING状态才能提交任务,因此调用shutdown()和shutdownNow()之后不能继续添加任务。而中断worker是通过调用worker中保存的Thread的interrupt()方法做到的,如果任务中没有因wait、join、sleep、可中断的I/O操作(NIO)等而阻塞,并且没有人为地判断线程中断标志,则线程仍会继续执行,因此执行shutdownNow()之后程序并不一定会立即停止运行。由于线程池创建的线程是用户线程并在线程内部无限循环,不会主动结束,因此如果不调用shutdown()或shutdownNow()程序将永远不会停止。

  1. 固定大小的线程池(如newFixedThreadPool())中的BlockingQueue是LinkedBlockingQueue,提交任务时,若Worker没有达到最大数量,则新建一个Worker并保存到HashSet中,同时在该Worker对象中执行本次任务,否则将任务放入BlockQueue,BlockQueue的大小没有限制。

  2. 计划任务线程池newScheduledThreadPool()与固定大小线程池类似,只是BlockingQueue的类型为DelayedWorkQueue。

  3. cache类型的线程池(如newCachedThreadPool())中的BlockingQueue是SynchronousQueue,提交新任务时首先判断能否将任务放入阻塞队列中(即有空闲的Worker在等待任务。对于SynchronousQueue,只有当存在消费者调用take()方法等待生产者时,offer()或add()方法才返回true;反之亦然,只有当生产者调用put()方法等待消费者时,poll()方法才不返回null。生产者和消费者一一匹配执行,队列中可以保存的任务数量为0),放入队列失败则新建Worker并保存在HashSet中,Worker数量没有限制。

8、volatile在单例模式中的应用

public class Singleton {
    private volatile static Singleton uniqueInstance;
    private Singleton(){}
    public static Singleton getInstance(){
        if(uniqueInstance == null){
            synchronized(Singleton.class){
                if(uniqueInstance == null){
                    uniqueInstance = new Singleton();
                    //如果不加volatile,可能因为重排序导致先分配内存,再掉用构造函数,其他线程会获取到一个还没构造完成的对象
                }
            }
        }
        return uniqueInstance;// 后面B线程执行时将引发:对象尚未初始化错误。
    }
}
 

9、在线程池中使用ThreadLocal

答:ThreadLocal为每一个线程保存一个变量副本,在线程池中由于线程会重复利用,并不是一个线程对应一个任务,而是一个线程对应多个任务,因此在任务中使用完ThreadLocal之后必须将值清除掉以避免多个任务获取到的值是相同的。

10、Object wait()/notify()和Condition await()/signal()区别

  1. object.wait()/notify()在synchronize中使用,Condition在Lock中使用
  2. object只能表示一个通信条件,Condition可以表示多个通信条件

前者的使用方式:

Object object = new Object();

synchronized (object) {

    try {

        object.wait();

    } catch (InterruptedException e) {

        e.printStackTrace();

    }

}

后者的使用方式

ReentrantLock lock = new ReentrantLock(true);

Condition c1= lock .newCondition();

Condition c2 = lock .newCondition();

{

    lock.lock();

    c1.await();

    do something

    c2.signal();

    lock.unlock();

}

10、ReadWriteLock的使用

读写锁的机制:

   "读-读" 不互斥

   "读-写" 互斥

   "写-写" 互斥

锁升级:从读锁变成写锁。ReentrantReadWriteLock不支持,会产生死锁

ReadWriteLock rtLock = new ReentrantReadWriteLock();
rtLock.readLock().lock();
rtLock.writeLock().lock();

锁降级:从写锁变成读锁。

ReadWriteLock rtLock = new ReentrantReadWriteLock();
rtLock.writeLock().lock();
rtLock.readLock().lock();降级为读锁
rtLock.writeLock().unlock();降级之后仍然需要手动释放写锁
do somethine
rtLock.readLock().unlock();

以下是使用读写锁的缓存示例:

class CachedData {
  Object data;
  volatile boolean cacheValid;
  final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();

  public void processCachedData() {
    rwl.readLock().lock();
    if (!cacheValid) {
      // 获取写锁前必须释放读锁,否则产生死锁
      rwl.readLock().unlock();
      rwl.writeLock().lock();
      try {
        if (!cacheValid) {
          data = ...
          cacheValid = true;
        }
        // 在释放写锁之前通过获取读锁降级写锁,如果这里不获取读锁,其他线程就可能获取写锁修改data
        rwl.readLock().lock();
      } finally {
        rwl.writeLock().unlock(); // 释放写锁而此时已经持有读锁
      }
    }

    try {
        //如果没有前面的锁降级,这里的data可能已经被其他线程修改了
      use(data);
    } finally {
      rwl.readLock().unlock();
    }
  }
}

三:网络

1、tcp三次握手四次挥手

  • SYN表示建立连接,

  • FIN表示关闭连接,

  • ACK表示响应

三次握手建立连接:

第一次握手:客户端发送syn包(seq=x)到服务器,并进入SYN_SEND状态,等待服务器确认

第二次握手:服务器收到syn包,必须确认客户的SYN(ack=x+1),同时自己也发送一个SYN包(seq=y),即SYN+ACK包,此时服务器进入SYN_RECV状态

第三次握手:客户端收到服务器的SYN+ACK包,向服务器发送确认包ACK(ack=y+1),此包发送完毕,客户端和服务器进入ESTABLISHED状态,完成三次握手。

四次挥手断开连接:

第一次挥手:主动关闭方发送一个FIN,用来关闭主动方到被动关闭方的数据传送,也就是主动关闭方告诉被动关闭方:我已经不会再给你发数据了(当 然,在fin包之前发送出去的数据,如果没有收到对应的ack确认报文,主动关闭方依然会重发这些数据),但此时主动关闭方还可以接受数据。

第二次挥手:被动关闭方收到FIN包后,发送一个ACK给对方,确认序号为收到序号+1(与SYN相同,一个FIN占用一个序号)。

第三次挥手:被动关闭方发送一个FIN,用来关闭被动关闭方到主动关闭方的数据传送,也就是告诉主动关闭方,我的数据也发送完了,不会再给你发数据了。

第四次挥手:主动关闭方收到FIN后,发送一个ACK给被动关闭方,确认序号为收到序号+1,至此,完成四次挥手。

2、tcp如何保证可靠传输

TCP通过序列号、检验和、确认应答信号、重发控制、连接管理、窗口控制、流量控制、拥塞控制实现可靠性。

  • 应用数据被分割成 TCP 认为最适合发送的数据块。
  • TCP 给发送的每一个包进行编号,接收方对数据包进行排序,把有序数据传送给应用层。
  • 校验和: TCP 将保持它首部和数据的检验和。这是一个端到端的检验和,目的是检测数据在传输过程中的任何变化。如果收到段的检验和有差错,TCP 将丢弃这个报文段和不确认收到此报文段。
  • TCP 的接收端会丢弃重复的数据。
  • 流量控制: TCP 连接的每一方都有固定大小的缓冲空间,TCP的接收端只允许发送端发送接收端缓冲区能接纳的数据。当接收方来不及处理发送方的数据,能提示发送方降低发送的速率,防止包丢失。TCP 使用的流量控制协议是可变大小的滑动窗口协议。 (TCP 利用滑动窗口实现流量控制)
  • 拥塞控制: 当网络拥塞时,减少数据的发送。
  • 停止等待协议: 也是为了实现可靠传输的,它的基本原理就是每发完一个分组就- 停止发送,等待对方确认。在收到确认后再发下一个分组。 超时重传: 当 TCP 发出一个段后,它启动一个定时器,等待目的端确认收到这个报文段。如果不能及时收到一个确认,将重发这个报文段。

3、http1.0和2.0区别

  • 新的二进制格式(Binary Format),HTTP1.x的解析是基于文本。基于文本协议的格式解析存在天然缺陷,文本的表现形式有多样性,要做到健壮性考虑的场景必然很多,二进制则不同,只认0和1的组合。基于这种考虑HTTP2.0的协议解析决定采用二进制格式,实现方便且健壮。

  • 多路复用(MultiPlexing),即连接共享,即每一个request都是是用作连接共享机制的。一个request对应一个id,这样一个连接上可以有多个request,每个连接的request可以随机的混杂在一起,接收方可以根据request的 id将request再归属到各自不同的服务端请求里面。

  • header压缩,如上文中所言,对前面提到过HTTP1.x的header带有大量信息,而且每次都要重复发送,HTTP2.0使用encoder来减少需要传输的header大小,通讯双方各自cache一份header fields表,既避免了重复header的传输,又减小了需要传输的大小。

  • 服务端推送(server push),服务端接收到客户端主请求,能够“预测”主请求的依赖资源,在响应主请求的同时,主动并发推送依赖资源至客户端。客户端解析主请求响应后,可以”无延时”从本地缓存获取依赖资源, 减少访问延时, 提高访问体验,也加大了链路的并发能力。

四:其他

1、redis持久化方案

答:RDB和AOF。

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

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

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)。

2、缓存穿透、缓存击穿、缓存雪崩解决方案

缓存穿透是指用户请求缓存和数据库中都没有的数据。解决方案:

  1. 接口层增加校验,如用户鉴权校验,id做基础校验,id<=0的直接拦截;
  2. 从缓存取不到的数据,在数据库中也没有取到,这时也可以将key-value对写为key-null,缓存有效时间可以设置短点,如30秒。这样可以防止攻击用户反复用同一个id暴力攻击

缓存击穿是指某一条缓存到期后,高并发下在成数据库压力增大。解决方案:

  1. 设置热点数据永远不过期。
  2. 读数据库的部分加锁

缓存雪崩是缓存中大量不同的数据同时过期引起数据库压力增大。解决方案:

  1. 缓存数据的过期时间设置随机,防止同一时间大量数据过期现象发生。
  2. 如果缓存数据库是分布式部署,将热点数据均匀分布在不同搞得缓存数据库中。
  3. 设置热点数据永远不过期。

3、nio线程模型、netty线程模型

4、高并发系统的保护方案

答:高并发系统有三种保护方案:缓存、降级和限流。

  1. 缓存:在读多写少的系统中,缓存可以极大的缓解数据库压力。在写操作比较多的系统中,也可以先缓存起来之后批量写入。消息中间件也可以认为是缓存的一种方式。
  2. 降级:在服务器压力骤增时,可以对一些非核心服务进行降级以保证核心服务能够正常运行。降级往往会指定不同的级别,面临不同的异常等级执行不同的处理。根据服务方式:可以拒接服务,可以延迟服务,也有时候可以随机服务。根据服务范围:可以砍掉某个功能,也可以砍掉某些模块。总之服务降级需要根据不同的业务需求采用不同的降级策略。主要的目的就是服务虽然有损但是总比没有好。
  3. 限流:使用nginx对ip进行限流,防止恶意攻击。

http{
    ...
    #定义一个名为one的limit_req_zone用来存储session,大小是10M内存,
    #以$binary_remote_addr 为key,限制平均每秒的请求为20个,
    #1M能存储16000个状态,rete的值必须为整数,
    #如果限制两秒钟一个请求,可以设置成30r/m
    limit_req_zone $binary_remote_addr zone=one:10m rate=20r/s;
    ...
    server{
        ...
        location {
            ...
            #限制每ip每秒不超过20个请求,漏桶数burst为5
            #brust的意思就是,如果第1秒、2,3,4秒请求为19个,
            #第5秒的请求为25个是被允许的。
            #但是如果你第1秒就25个请求,第2秒超过20的请求返回503错误。
            #nodelay,如果不设置该选项,严格使用平均速率限制请求数,
            #第1秒25个请求时,5个请求放到第2秒执行,
            #设置nodelay,25个请求将在第1秒执行。
            limit_req zone=one burst=5 nodelay;
            ...
        }
        ...
    }
    ...
}

5、zookeeper数据一致性

6、倒排索引

7、分布式事务解决方案

  • 2
    点赞
  • 33
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值