java面试大全

目录

1、JDK对象与数据结构

1.1 HashMap:

1.2 Hashtable、StringBuffer等是如何做到安全的

1.3 finalize方法

1.4 红黑树

1.5 B-树(B树,不能读B减树)

1.​6 B+树

1.7 transient 关键字

1.8 threadlocal关键字

2、序列化

3、MySql数据库

3.1 索引数据结构:B+树

3.2 数据库索引

3.3 Mysql 中 MyISAM 和 InnoDB 的区别有哪些?

3.4 MySQL 执行计划

4、缓存框架redis

4.1分布式锁的几种实现

4.2 分布式锁的基本功能

4.3 缓存被“击穿”问题:

4.4 Redis内存淘汰策略:

4.5 Redis数据持久化方案

4.6 Redis高可用设计:主服务器可能存在单点故障,加入Sentinel(哨兵)

4.7 Redis为什么使用单进程单线程方式也这么快:

4.8 redis的setnx锁到了超时时间失效,并发死锁问题

4.9 Redis如何做数据分片

4.10 epoll为啥比select/poll好

4.11 sentinel实现redis切换的原理那就是

4.12 主从复制原理

5、JVM相关

5.1 System.gc()

5.2 强引用、软引用

​5.3 堆外内存

5.4 类加载

5.5 类加载JVM参数调优

5.6 如何排查线上请求慢的问题

5.7 稳定的 Java 堆 VS 动荡的 Java 堆

6、GC算法与调优

6.1 Minor GC:

6.2 Major GC:

7、设计模式

8、算法题

9、分布式系统设计

9.1 限流

9.2 重试

9.3 降级

9.4 熔断

10、分布式系统理论与技术

11、JUC

11.1 CountDownLatch、CyclicBarrier、Semaphore

11.2 AQS(AbstractQueuedSynchronizer):Java并发之AQS详解

11.3 线程池

12、系统安全设计

12.1 XSS:跨站脚本攻击

12.2 CSRF(Cross-site request forgery):跨站请求伪造

12.3 https

12.4  加解密

13、netty

13.1 channel结构图:

13.2 传播机制

13.3 pipeline

13.4 TCP粘包/拆包

13.5 Netty设置高低水位

14、锁类型:锁类型

15、Zookeeper

16、 单点登录

16.2 Session

16.3 单点登录(SSO):token

17、TCP与UDP

17.1 TCP与UDP的区别

17.2 TCP(Transmission Control Protocol) 传输控制协议

18、MQ(kafka)

18.1 为什么要使用 MQ 消息中间件:

18.2 如何保证高吞吐量和消息的可靠传输:

18.3 kafka如何保证消息的顺序性

18.4 kafka为什么那么快

18.4.1 内存缓存池

18.4.2 Reactor多路复用模型

18.4.3 写入数据的超高性能:页缓存技术 + 磁盘顺序写

18.4.4 零拷贝


1、JDK对象与数据结构

1.1 HashMap:

  • 数组+链表:链表在增删方面的高效和数组在寻址上的优势。
  • HashMap的主干是一个Entry数组。Entry是HashMap的基本组成单元,每一个Entry包含一个key-value键值对。数组是HashMap的主体,链表则是主要为了解决哈希冲突而存在的,如果定位到的数组位置不含链表(当前entrynext指向null),那么对于查找,添加等操作很快,仅需一次寻址即可;如果定位到的数组包含链表,对于添加操作,其时间复杂度为O(n),首先遍历链表,存在即覆盖,否则新增;对于查找操作来讲,仍需遍历链表,然后通过key对象的equals方法逐一比对查找。所以,性能考虑,HashMap中的链表出现越少,性能才会越好。
  • initialCapacity默认为16,loadFactory默认为0.75,大于16*0.75=12的时候,扩容为之前的2倍,所以扩容相对来说是个耗资源的操作。
  • 在JDK1.8及以后的版本中引入了红黑树结构,HashMap的实现就变成了数组+链表或数组+红黑树。添加元素时,若桶中链表个数超过8,链表会转换成红黑树;删除元素、扩容时,若桶中结构为红黑树并且树中元素个数较少时会进行修剪或直接还原成链表结构,以提高后续操作性能;遍历、查找时,由于使用红黑树结构,红黑树遍历的时间复杂度为 O(logn),所以性能得到提升。
  • TreeMap:基于红黑树实现,特点有些是源于红黑树的一些特点,比如TreeMap是有序的,TreeMap不允许key-value为空等

     
    HashMap的put操作
    final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
        HashMap.Node<K,V>[] tab;
        ......
        //数组对应位置没有存放过元素,直接占坑
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        //数组对应位置已经存放过元素,需要继续判断:
        else {
            HashMap.Node<K, V> e;
            K k;
            //1、如果当前put的key的hash值与当前占坑的元素一样且key也相等,覆盖即可
            if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) e = p;
                //2、否则需要重新找坑
                //2.1 如果采用了红黑树,则把当前put的元素插入到红黑树中
            else if (p instanceof HashMap.TreeNode)
                e = ((HashMap.TreeNode<K, V>) p).putTreeVal(this, tab, hash, key, value);
                //2.2 如果没用红黑树,即老的链表结构,往后插入一个Node即可,
            else {
                for (int binCount = 0; ; ++binCount) {
                    //3.1 找到末尾位置插入node
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        //3.2 如果单链表中节点数,达到触发链表转红黑树的阈值,则转红黑树
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }......} }......}
    }

1.2 Hashtable、StringBuffer等是如何做到安全的

Hashtable、StringBuffer的实现方法里面都添加了synchronized关键字来确保线程同步

1.3 finalize方法

finalize方法在垃圾回收器将对象从内存中清除出去之前做必要的清理工作,它是在 Object 类中定义的,因此所有的类都继承了它。子类覆盖 finalize() 方法以整理系统资源或者执行其他清理工作。finalize() 方法是在垃圾收集器删除对象之前对这个对象调用的。例如在一些必须要做清楚的操作中,比如说InputStream,我们都知道执行完了得把流关掉为了避免用户的误操作,可以在InputStream的finalize方法中关闭流,这样为资源的释放增加了一层保护网,虽然不保证一定能够执行。看下FileInputStream的源码,这个对象重写了finalize方法:

   /**
     * Ensures that the <code>close</code> method of this file input stream is
     * called when there are no more references to it.
     *
     * @exception  IOException  if an I/O error occurs.
     * @see        java.io.FileInputStream#close()
     */
    protected void finalize() throws IOException {
        if ((fd != null) &&  (fd != FileDescriptor.in)) {
            /* if fd is shared, the references in FileDescriptor
             * will ensure that finalizer is only called when
             * safe to do so. All references using the fd have
             * become unreachable. We can call close()
             */
            close();
        }
    }

finalize方法执行细节,请移步:https://www.cnblogs.com/benwu/articles/5812903.html

1.4 红黑树

红黑树 R-B Tree,全称是Red-Black Tree,又称为“红黑树”,它一种特殊的二叉查找树。红黑树的每个节点上都有存储位表示节点的颜色,可以是红(Red)或黑(Black)。

红黑树的特性:
1)每个节点或者是黑色,或者是红色。
2)根节点是黑色。
3)每个叶子节点(NIL)是黑色。 [注意:这里叶子节点,是指为空(NILNULL)的叶子节点!]
4)如果一个节点是红色的,则它的子节点必须是黑色的。
5)从一个节点到该节点的子孙节点的所有路径上包含相同数目的黑节点。

注意:

  • 特性(3)中的叶子节点,是只为空(NIL或null)的节点。
  • 特性(5),确保没有一条路径会比其他路径长出俩倍。因而,红黑树是相对是接近平衡的二叉树。

上述的性质约束了红黑树的关键:从根到叶子的最长可能路径不多于最短可能路径的两倍长。得到这个结论的理由是:

  1. 红黑树中最短的可能路径是全部为黑色节点的路径
  2. 红黑树中最长的可能路径是红黑相间的路径

优点:红黑树是不符合AVL树的平衡条件的,即每个节点的左子树和右子树的高度最多差1的二叉查找树。但是提出了为节点增加颜色,红黑是用非严格的平衡来换取增删节点时候旋转次数的降低,任何不平衡都会在三次旋转之内解决,而AVL是严格平衡树,因此在增加或者删除节点的时候,根据不同情况,旋转的次数比红黑树要多。所以红黑树的插入效率更高!!!

1.5 B-树(B树,不能读B减树)

B-树(Balance Tree),一个m阶的B树具有如下几个特征:

  • 1.根结点至少有两个子女。
  • 2.每个中间节点都包含k-1个元素和k个孩子,其中 m/2 <= k <= m
  • 3.每一个叶子节点都包含k-1个元素,其中 m/2 <= k <= m
  • 4.所有的叶子结点都位于同一层。
  • 5.每个节点中的元素从小到大排列,节点当中k-1个元素正好是k个孩子包含的元素的值域分划。

1.B+

B+树,一个m阶的B+树具有如下几个特征:

  • k个子树的中间节点包含有k个元素(B树中是k-1个元素),每个元素不保存数据,只用来索引,所有数据都保存在叶子节点。
  • 所有的叶子结点中包含了全部元素的信息,及指向含这些元素记录的指针,且叶子结点本身依关键字的大小自小而大顺序链接。
  • 所有的中间节点元素都同时存在于子节点,在子节点元素中是最大(或最小)元素。

由于B+树中间节点无数据,所以同样大小的磁盘页可以容纳更多的节点元素;

B-树,由于每个节点都有数据,所以每次查询是不稳定的;

B+树,由于数据都在叶子节点,所以每次查询都是稳定的,且由于B+树在叶子节点,数据间通过链表链接,方便查询一个范围内的数据,效率比B-树高很多

总结:

B+树比B-树优点:

  • 单一节点存储更多的元素,使得查询的IO次数更少。
  • 所有查询都要查找到叶子节点,查询性能稳定。
  • 所有叶子节点形成有序链表,便于范围查询。

1.7 transient 关键字

  • 1)transient修饰的变量不能被序列化;
  • 2)transient只作用于实现 Serializable 接口;
  • 3)transient只能用来修饰普通成员变量字段;
  • 4)不管有没有 transient 修饰,静态变量都不能被序列化;

1.8 threadlocal关键字

ThreadLocal的实现是这样的:每个thread中都存在一个map, map的类型是ThreadLocal.ThreadLocalMap. Map中的key为一个threadlocal实例. 这个Map的确使用了弱引用,不过弱引用只是针对key. 每个key都弱引用指向threadlocal. 当把threadlocal实例置为null以后,没有任何强引用指向threadlocal实例,所以threadlocal将会被gc回收. 但是,我们的value却不能回收,因为存在一条从current thread连接过来的强引用. 只有当前thread结束以后, current thread就不会存在栈中,强引用断开, Current Thread, Map, value将全部被GC回收。所以得出一个结论就是只要这个线程对象被gc回收,就不会出现内存泄露,但在threadLocal设为null和线程结束这段时间不会被回收的,就发生了我们认为的内存泄露。其实这是一个对概念理解的不一致,也没什么好争论的。最要命的是线程对象不被回收的情况,这就发生了真正意义上的内存泄露。比如使用线程池的时候,线程结束是不会销毁的,会再次使用的。就可能出现内存泄露。PS.Java为了最小化减少内存泄露的可能性和影响,在ThreadLocal的get,set的时候都会清除线程Map里所有key为null的value。所以最怕的情况就是,threadLocal对象设null了,开始发生“内存泄露”,然后使用线程池,这个线程结束,线程放回线程池中不销毁,这个线程一直不被使用,或者分配使用了又不再调用get,set方法,那么这个期间就会发生真正的内存泄露。

结论:同步与ThreadLocal是解决多线程中数据访问问题的两种思路,前者是数据共享的思路,后者是数据隔离的思路

           同步是一种以时间换空间的思想,ThreadLocal是一种空间换时间的思想

2、序列化

序列化,你们用了哪些?在选择序列化工具时,你注重是什么,在乎什么?

性能:包括两个方面,时间复杂度和空间复杂度: 第一、空间开销(Verbosity), 序列化需要在原有的数据上加上描述字段,以为反序列化解析之用。如果序列化过程引入的额外开销过高,可能会导致过大的网络,磁盘等各方面的压力。对于海量分布式存储系统,数据量往往以TB为单位,巨大的的额外空间开销意味着高昂的成本。 第二、时间开销(Complexity),复杂的序列化协议会导致较长的解析时间,这可能会使得序列化和反序列化阶段成为整个系统的瓶颈。

当下比较流行的序列化协议,包括XML、JSON、Protobuf:

协议 效率 编码复杂度 可读性 适用场景
xml 本地存储配置文件
json 进程间消息通讯或网络间消息通讯
protobuf 低 (二进制格式) 进程间消息通讯或网络间消息通讯

3、MySql数据库

3.1 索引数据结构:B+树

联合索引(a, b),第一个和第二个触发条件,where a=会触发嘛,where b=会嘛,为什么?

CREATE TABLE E (e1 INT, e2 VARCHAR(9), e3 INT, PRIMARY KEY(e1, e3)); 这样就建立了一个联合索引:e1,e3

触发联合索引是有条件的:偏左原理

1、使用联合索引的全部索引键,可触发索引的使用。

例如:SELECT E.* FROM E WHERE E.e1=1 AND E.e3=2

2、使用联合索引的前缀部分索引键,如“key_part_1 常量”,可触发索引的使用。

例如:SELECT E.* FROM E WHERE E.e1=1

3、使用部分索引键,但不是联合索引的前缀部分,如“key_part_2 常量”,不可触发索引的使用。

例如:SELECT E.* FROM E WHERE E.e3=1

二叉树不能作为数据库的索引数据结构,是因为其高度太高,磁盘IO次数过多导致性能降低;查找一个数据,可能需要树高度次数的IO(一次无法读取所有的索引数据,因为太大,每次只能读取一个磁盘页),而B树(B-)是多路平衡查找树,不是二叉树哦,他的每个节点最多会有K个子节点(比2更多),k被称作B树的阶,他要把原本瘦高的树结构变得矮胖。磁盘IO永远都比内存中的计算慢很多,所以尽量减少磁盘IO的操作就会加快查询速度。

3.2 数据库索引

1、聚集索引、非聚集索引

聚集索引:数据行的物理顺序与列值(一般是主键的那一列,primary key)的逻辑顺序相同,一个表中只能拥有一个聚集索引。聚集索引的好处了,索引的叶子节点就是对应的数据节点

非聚集索引:该索引中索引的逻辑顺序与磁盘上行的物理存储顺序不同,一个表中可以拥有多个非聚集索引。其实按照定义,除了聚集索引以外的索引都是非聚集索引,只是人们想细分一下非聚集索引,分成普通索引,唯一索引,全文索引。如何解决非聚集索引的二次查询问题:复合索引(覆盖索引),注意使用复合索引需要满足最左侧索引的原则。

3.3 Mysql 中 MyISAM 和 InnoDB 的区别有哪些?

区别:

1. InnoDB支持事务,MyISAM不支持,对于InnoDB每一条SQL语言都默认封装成事务,自动提交,这样会影响速度,所以最好把多条SQL语言放在begin和commit之间,组成一个事务;

2. InnoDB支持外键,而MyISAM不支持。对一个包含外键的InnoDB表转为MYISAM会失败;

3. InnoDB是聚集索引,数据文件是和索引绑在一起的,必须要有主键,通过主键索引效率很高。但是辅助索引需要两次查询,先查询到主键,然后再通过主键查询到数据。因此,主键不应该过大,因为主键太大,其他索引也都会很大。而MyISAM是非聚集索引,数据文件是分离的,索引保存的是数据文件的指针。主键索引和辅助索引是独立的。

4. InnoDB不保存表的具体行数,执行select count(*) from table时需要全表扫描。而MyISAM用一个变量保存了整个表的行数,执行上述语句时只需要读出该变量即可,速度很快;

5. Innodb不支持全文索引,而MyISAM支持全文索引,查询效率上MyISAM要高;

如何选择:

1. 是否要支持事务,如果要请选择innodb,如果不需要可以考虑MyISAM;

2. 如果表中绝大多数都只是读查询,可以考虑MyISAM,如果既有读写也挺频繁,请使用InnoDB。

3. 系统奔溃后,MyISAM恢复起来更困难,能否接受;

4. MySQL5.5版本开始Innodb已经成为Mysql的默认引擎(之前是MyISAM),说明其优势是有目共睹的,如果你不知道用什么,那就用InnoDB,至少不会差。

注意:mysql5.7的innodb已支持全文索引

  • text类型最大多少长度

  • mysql事务的隔离级别:

      

    脏读 事务A读取了事务B更新的数据,然后B回滚操作,那么A读取到的数据是脏数据
    不可重复读 事务 A 多次读取同一数据,事务 B 在事务A多次读取的过程中,对数据作了更新并提交,导致事务A多次读取同一数据时,结果 不一致
    幻读

    系统管理员A将数据库中所有学生的成绩从具体分数改为ABCDE等级,但是系统管理员B就在这个时候插入了一条具体分数的记录,当系统管理员A改结束后发现还有一条记录没有改过来,就好像发生了幻觉一样,这就叫幻读。

小结:不可重复读的和幻读很容易混淆,不可重复读侧重于修改,幻读侧重于新增或删除。解决不可重复读的问题只需锁住满足条件的行,解决幻读需要锁表

事务隔离级别 脏读 不可重复读 幻读
读未提交(read-uncommitted)
不可重复读(read-committed)
可重复读(repeatable-read)
串行化(serializable)

mysql默认的事务隔离级别为可重复读(repeatable-read)

3.4 MySQL 执行计划

explain select * from t_base_user where name="zhengchao"

执行计划输出结果每一列解释:执行计划-含义

  • id:表示查询中select操作表的顺序,按顺序从大到依次执行

  • select_type:该表示选择的类型,可选值有: SIMPLE(简单的),

  • type:该属性表示访问类型,有很多种访问类型。最常见的其中包括以下几种: ALL(全表扫描), index(索引扫描),range(范围扫描),ref (非唯一索引扫描),eq_ref(唯一索引扫描,),(const)常数引用, 访问速度依次由慢到快。其中 : range(范围)常见与 between and …, 大于 and 小于这种情况。提示 : 慢SQL是否走索引,走了什么索引,也就可以通过该属性查看了。

  • table:表示该语句查询的表

  • possible_keys:顾名思义,该属性给出了,该查询语句,可能走的索引,(如某些字段上索引的名字)这里提供的只是参考,而不是实际走的索引,也就导致会有possible_Keys不为null,key为空的现象。

  • key:显示MySQL实际使用的索引,其中就包括主键索引(PRIMARY),或者自建索引的名字。

  • key_len:表示索引所使用的字节数,

  • ref:连接匹配条件,如果走主键索引的话,该值为: const, 全表扫描的话,为null值

  • rows:扫描行数,也就是说,需要扫描多少行,采能获取目标行数,一般情况下会大于返回行数。通常情况下,rows越小,效率越高, 也就有大部分SQL优化,都是在减少这个值的大小。注意:  理想情况下扫描的行数与实际返回行数理论上是一致的,但这种情况及其少,如关联查询,扫描的行数就会比返回行数大大增加)

  • Extra:这个属性非常重要,该属性中包括执行SQL时的真实情况信息,如上面所属,使用到的是”using where”,表示使用where筛选得到的值,常用的有:“Using temporary”: 使用临时表 “using filesort”: 使用文件排序

4、缓存框架redis

4.1分布式锁的几种实现

(1)zookeeper分布式锁,基于自增节点
(2)redis分布式锁,基于setnx命令;

4.2 分布式锁的基本功能

(1)同一时刻只能存在一个锁
(2)需要解决意外死锁问题,也就是锁能超时自动释放;
(3)支持主动释放锁
(3)分布式锁解决什么问题:多进程并发执行任务时,需要保证任务的有序性或者唯一性

4.3 缓存被“击穿”问题

(1)概念:缓存在某个时间点过期的时候,恰好在这个时间点对这个Key有大量的并发请求过来,这些请求发现缓存过期一 般都会从后端DB加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把后端DB压垮。
(2)如何解决:业界比较常用的做法,是使用mutex(互斥)。简单地来说,就是在缓存失效的时候(判断拿出来的值为空),不是立即去load db,而是先使用缓存工具的某些带成功操作返回值的操作(比如Redis的SETNX或者Memcache的ADD)去set一个mutex key,当操作返回成功时,再进行load db的操作并回设缓存;否则,就重试整个get缓存的方法。类似下面的代码:

public String get(key) {
     String value = redis.get(key);      
     if (value == null) { //代表缓存值过期
         //设置3min的超时,防止del操作失败的时候,下次缓存过期一直不能load db
         if (redis.setnx(key_mutex, 1, 3 * 60) == 1) {  //代表设置成功
              value = db.get(key);
              redis.set(key, value, expire_secs);
              redis.del(key_mutex);
         } else {//代表同时候的其他线程已经load db并回设到缓存了,这时候重试获取缓存值即可
              sleep(50);
              get(key);  //重试
         }
     } else {              
        return value;      
     }
 }

4.4 Redis内存淘汰策略

redis 内存数据集大小上升到一定大小的时候,就会进行数据淘汰策略。
通过配置redis.conf中的maxmemory(config set maxmemory 100000:设置最大内存)这个值来开启内存淘汰功能(maxmemory为0的时候表示我们对Redis的内存使用没有限制)。
通过配置redis.conf中的maxmemory-policy设置淘汰策略设置:策略类型:
 1、最近最少使用(设置、不设置了过期时间的key数据集)
 2、将要过期的数据(设置、不设置设置了过期时间的key数据集)
 3、任意选择数据(设置、不设置了过期时间的key数据集)
 4、不可写入任何数据集(也不删除)

4.5 Redis数据持久化方案

RDB:是Redis默认的持久化方式。按照一定的时间周期策略把内存的数据以快照的形式保存到硬盘的二进制文件。即Snapshot快照存储,对应产生的数据文件为dump.rdb,通过配置文件中的save()参数来定义快照的周期。

AOF:Redis会将每一个收到的写命令都通过Write函数追加到文件最后。当Redis重启是会通过重新执行文件中保存的写命令来在内存中重建整个数据库的内容。

4.6 Redis高可用设计:主服务器可能存在单点故障,加入Sentinel(哨兵)

4.7 Redis为什么使用单进程单线程方式也这么快:

Redis快的主要原因是:
   1、完全基于内存;
   2、数据结构简单,对数据操作也简单;
   3、使用多路 I/O 复用模型;(nio的Selector也是基于select/poll模型实现,是基于IO复用技术的非阻塞IO)

4.8 redis的setnx锁到了超时时间失效,并发死锁问题

redis的setnx锁到了超时时间失效,并发的问题

4.9 Redis如何做数据分片

Redis集群使用数据分片(sharding)而非一致性哈希(consistency hashing)来实现:一个Redis集群包含16384个哈希槽(hash slot),数据库中的每个键都属于这16384个哈希槽的其中一个,集群使用公式CRC16(key)%16384来计算键key属于哪个槽,其中CRC16(key)语句用于计算键key的CRC16校验和。
图片123456
节点A负责处理0号至5500号哈希槽。
节点B负责处理5501号至11000号哈希槽。
节点C负责处理11001号至16384号哈希槽。
而对于mysql来说,一般情况下,如果某个表的数据有明显的时间特征,比如订单、交易记录等,则他们通常比较合适用时间范围分片,因为具有时效性的数据,我们往往关注其近期的数据,查询条件中往往带有时间字段进行过滤,比较好的方案是,当前活跃的数据,采用跨度比较短的时间段进行分片,而历史性的数据,则采用比较长的跨度存储。

4.10 epoll为啥比select/poll好

Redis IO多路复用技术以及epoll实现原理

4.11 sentinel实现redis切换的原理那就是

sentinel心跳检测到主节点出现异常后,通过修改redis.conf配置文件的形式实现redis主从节点之间的故障切换

4.12 主从复制原理

Redis主从复制原理总结

全量同步
Redis全量复制一般发生在Slave初始化阶段,这时Slave需要将Master上的所有数据都复制一份。具体步骤如下: 
-  从服务器连接主服务器,发送SYNC命令; 
-  主服务器接收到SYNC命名后,开始执行BGSAVE命令生成RDB文件并使用缓冲区记录此后执行的所有写命令; 
-  主服务器BGSAVE执行完后,向所有从服务器发送快照文件,并在发送期间继续记录被执行的写命令; 
-  从服务器收到快照文件后丢弃所有旧数据,载入收到的快照; 
-  主服务器快照发送完毕后开始向从服务器发送缓冲区中的写命令; 
-  从服务器完成对快照的载入,开始接收命令请求,并执行来自主服务器缓冲区的写命令;

增量同步
Redis增量复制是指Slave初始化后开始正常工作时主服务器发生的写操作同步到从服务器的过程。 
增量复制的过程主要是主服务器每执行一个写命令就会向从服务器发送相同的写命令,从服务器接收并执行收到的写命令。
Redis主从同步策略
主从刚刚连接的时候,进行全量同步;全同步结束后,进行增量同步。当然,如果有需要,slave 在任何时候都可以发起全量同步。redis 策略是,无论如何,首先会尝试进行增量同步,如不成功,要求从机进行全量同步。

当主服务器不进行持久化时复制的安全性

在进行主从复制设置时,强烈建议在主服务器上开启持久化,当不能这么做时,比如考虑到延迟的问题,应该将实例配置为避免自动重启。为什么不持久化的主服务器自动重启非常危险呢?为了更好的理解这个问题,看下面这个失败的例子,其中主服务器和从服务器中数据库都被删除了。设置节点A为主服务器,关闭持久化,节点B和C从节点A复制数据。这时出现了一个崩溃,但Redis具有自动重启系统,重启了进程,因为关闭了持久化,节点重启后只有一个空的数据集。节点B和C从节点A进行复制,现在节点A是空的,所以节点B和C上的复制数据也会被删除。当在高可用系统中使用Redis Sentinel,关闭了主服务器的持久化,并且允许自动重启,这种情况是很危险的。比如主服务器可能在很短的时间就完成了重启,以至于Sentinel都无法检测到这次失败,那么上面说的这种失败的情况就发生了。如果数据比较重要,并且在使用主从复制时关闭了主服务器持久化功能的场景中,都应该禁止实例自动重启。

5、JVM相关

5.1 System.gc()

这个方法执行后是立即回收内存吗?答:程序员可以手动执行System.gc(),通知GC运行,但是Java语言规范并不保证GC一定会执行;因为这个命令只是建议JVM安排GC运行, 还有可能完全被拒绝。 GC本身是会周期性的自动运行的,由JVM决定运行的时机,而且现在的版本有多种更智能的模式可以选择,还会根据运行的机器自动去做选择,就算真的有性能上的需求,也应该去对GC的运行机制进行微调,而不是通过使用这个命令来实现性能的优化

5.2 强引用、软引用


5.3 堆外内存

1. 堆内存完全由JVM负责分配和释放;
2. 使用堆外内存,就是为了能直接分配和释放内存,提高效率。
3. JDK5.0之后,代码中能直接操作本地内存的方式有2种:使用未公开的Unsafe和NIO包下ByteBuffer。
4. NIO直接内存的回收,需要依赖于System.gc()。如果我们的应用中使用了java nio中的direct memory,那么使用-XX:+DisableExplicitGC一定要小心(这个参数作用是禁止代码中显示调用GC),存在潜在的内存泄露风险
5. 我们知道java代码无法强制JVM何时进行垃圾回收,也就是说垃圾回收这个动作的触发,完全由JVM自己控制,它会挑选合适的时机回收堆内存中的无用java对象。代码中显示调用System.gc(),只是建议JVM进行垃圾回收,但是到底会不会执行垃圾回收是不确定的,可能会进行垃圾回收,也可能不会。什么时候才是合适的时机呢?一般来说是,系统比较空闲的时候(比如JVM中活动的线程很少的时候),还有就是内存不足,不得不进行垃圾回收。我们例子中的根本矛盾在于:堆内存由JVM自己管理,堆外内存必须要由我们自己释放;堆内存的消耗速度远远小于堆外内存的消耗,但要命的是必须先释放堆内存中的对象,才能释放堆外内存,但是我们又不能强制JVM释放堆内存。
6. Direct Memory的回收机制:Direct Memory是受GC控制的,例如ByteBuffer bb = ByteBuffer.allocateDirect(1024),这段代码的执行会在堆外占用1k的内存,Java堆内只会占用一个对象的指针引用的大小,堆外的这1k的空间只有当bb对象被回收时,才会被回收,这里会发现一个明显的不对称现象,就是堆外可能占用了很多,而堆内没占用多少,导致还没触发GC,那就很容易出现Direct Memory造成物理内存耗光。Direct ByteBuffer分配出去的内存其实也是由GC负责回收的,而不像Unsafe是完全自行管理的,Hotspot在GC时会扫描Direct ByteBuffer对象是否有引用,如没有则同时也会回收其占用的堆外内存。
7. 使用堆外内存与对象池都能减少GC的暂停时间,这是它们唯一的共同点。生命周期短的可变对象,创建开销大,或者生命周期虽长但存在冗余的可变对象都比较适合使用对象池。生命周期适中,或者复杂的对象则比较适合由GC来进行处理。然而,中长生命周期的可变对象就比较棘手了,堆外内存则正是它们的菜。堆外内存的好处是:

(1)可以扩展至更大的内存空间。比如超过1TB甚至比主存还大的空间;

(2)理论上能减少GC暂停时间;

(3)可以在进程间共享,减少JVM间的对象复制,使得JVM的分割部署更容易实现;

(4)它的持久化存储可以支持快速重启,同时还能够在测试环境中重现生产数据

5.4 类加载

加载过程中会先检查类是否被已加载,检查顺序是自底向上,从Custom ClassLoader到APP ClassLoaderExtention ClassLoaderBootStrap ClassLoader逐层检查,只要某个classloader已加载就视为已加载此类,保证此类只所有ClassLoader加载一次。而加载的顺序是自顶向下,也就是说当发现这个类没有的时候会先去让自己的父类去加载,父类没有再让儿子去加载。面试题:自己写一个String类会被加载嘛?答:不能,因为java.lang.String已经被Bootstrap ClassLoader加载了,所以App ClassLoader就不会再去加载我们写的String类了,导致我们写的String类是没有被加载的。

5.5 类加载JVM参数调优

JVM调优总结 -Xms -Xmx -Xmn -Xss

5.6 如何排查线上请求慢的问题

(1) top命令看cpu负载情况 ,找到占用cpu过高的进程pid
(2) jmap -heap pid该命令用于:展示pid的整体堆信息:eden、S1、S2、年老代的大小,以及已经使用、剩余的大小;(3) jstack用来查看某Java进程内的线程堆栈信息。命令的输出可以定位进程的所有线程的当前运行状态,是否死锁等。
(4) jstat命令打印出gc信息:各个区的大小、使用情况、gc次数、gc耗费时间

5.7 稳定的 Java 堆 VS 动荡的 Java 堆

一般来说,稳定的堆大小对垃圾回收是有利的。获得一个稳定的堆大小的方法是使-Xms 和-Xmx 的大小一致,即最大堆和最小堆 (初始堆) 一样。如果这样设置,系统在运行时堆大小理论上是恒定的,稳定的堆空间可以减少 GC 的次数。因此,很多服务端应用都会将最大堆和最小堆设置为相同的数值。但是,一个不稳定的堆并非毫无用处。稳定的堆大小虽然可以减少 GC 次数,但同时也增加了每次 GC 的时间。让堆大小在一个区间中震荡,在系统不需要使用大内存时,压缩堆空间,使 GC 应对一个较小的堆,可以加快单次 GC 的速度。基于这样的考虑,JVM 还提供了两个参数用于压缩和扩展堆空间。

-XX:MinHeapFreeRatio 参数用来设置堆空间最小空闲比例,默认值是 40。当堆空间的空闲内存小于这个数值时,JVM 便会扩展堆空间。

-XX:MaxHeapFreeRatio 参数用来设置堆空间最大空闲比例,默认值是 70。当堆空间的空闲内存大于这个数值时,便会压缩堆空间,得到一个较小的堆。

当-Xmx 和-Xms 相等时,-XX:MinHeapFreeRatio 和-XX:MaxHeapFreeRatio 两个参数无效。

6、GC算法与调优

6.1 Minor GC:

  • eden区满时,触发MinorGC,即申请一个对象时,发现eden区不够用,则触发一次MinorGC(优化:扩容新生代)
  • 在cms算法中,young gc的实现过程?先找出根对象,如Java栈中引用的对象、静态变量引用的对象和系统词典中引用的对象等待,把这些对象标记成活跃对象,并复制到to区,接着遍历这些活跃对象中引用的对象并标记,找出老年代对象在eden区有引用关系的对象并标记,最后把这些标记的对象复制到to,在复制过程还要判断活跃对象的gc年龄是否已经达到阈值,如果已经达到阈值,就直接晋升到老年代,YGC结束之后把from和to的引用互换。

6.2 Major GC:

  • CMS在重标记(Remark)阶段,Remark阶段是Stop-The-World,即在执行垃圾回收时,Java应用程序中除了垃圾回收器线程之外其他所有线程都被挂起,意味着在此期间,用户正常工作的线程全部被暂停下来,这是低延时服务不能接受的。新生代对象持有老年代中对象的引用,这种情况称为“跨代引用”。因它的存在,Remark阶段必须扫描整个堆来判断对象是否存活,包括图中灰色的不可达对象。新生代中对象的特点是“朝生夕灭”,这样如果Remark前执行一次Minor GC,大部分对象就会被回收。CMS就采用了这样的方式
  • 触发条件 (1)调用System.gc时,系统建议执行Full GC,但是不必然执行;(2)老年代空间不足;(3)方法区空间(Perm空间)不足;(4)通过Minor GC后进入老年代的平均大小大于老年代的可用内存;(5)由Eden区、From区向To区复制时,对象大小>To内存,把该对象转存到老年代,但老年代可用内存<该对象大小; (6)  CMS GC时出现promotion failed和concurrent mode failure(concurrent mode failure发生的原因一般是CMS正在进行,但是由于老年代空间不足,需要尽快回收老年代里面的不再被使用的对象,这时停止所有的线程,同时终止CMS,直接进行Serial Old GC);

7、设计模式

  1. 静态代理、动态代理:Java 动态代理
  2. 动态代理与CGLIB:动态代理proxy与CGLib的区别
  3. 单例模式:单例模式集锦

8、算法题

9、分布式系统设计

9.1 限流

限流策略:

  • 计数器算法:设置一个计数器统计单位时间内某个请求的访问量,在进入下一个单位时间内把计数器清零,对于单位时间内超过计数器的访问,可以放入等待队列、直接拒接访问等策略
  • 漏斗算法:一个固定容量的漏桶,按照常量固定速率流出水滴;可以以任意速率流入水滴到漏桶;如果流入水滴超出了桶的容量,则流入的水滴溢出了,而漏桶容量是不变的。
  • 令牌桶算法:令牌将按照固定的速率被放入令牌桶中。比如每秒放10个。每次请求调用需要先获取令牌,只有拿到令牌,才有机会继续执行,否则选择选择等待可用的令牌、或者直接拒绝。当令牌桶满时,新添加的令牌被丢弃或拒绝

漏桶算法与令牌桶算法在表面看起来类似,很容易将两者混淆。但事实上,这两者具有截然不同的特性,且为不同的目的而使用。漏桶算法与令牌桶算法的区别在于,漏桶算法能够强行限制数据的传输速率,令牌桶算法能够在限制数据的平均传输速率的同时还允许某种程度的突发传输。需要注意的是,在某些情况下,漏桶算法不能够有效地使用网络资源,因为漏桶的漏出速率是固定的,所以即使网络中没有发生拥塞,漏桶算法也不能使某一个单独的数据流达到端口速率。因此,漏桶算法对于存在突发特性的流量来说缺乏效率。而令牌桶算法则能够满足这些具有突发特性的流量。通常,漏桶算法与令牌桶算法结合起来为网络流量提供更高效的控制。

我们公司:

       单机限流:google的guava令牌桶算法实现;
       集群限流:Redis的计数实现精确;

9.2 重试

9.3 降级

9.4 熔断

10、分布式系统理论与技术

1、一致性hash算法深入一致性哈希(Consistent Hashing)算法原理

 一致性Hashing在分布式系统中经常会被用到, 用于尽可能地降低节点变动带来的数据迁移开销

  • hash算法缺陷:先来简单理解下Hash是解决什么问题。假设一个分布式任务调度系统,执行任务的节点有n台机器,现有m个job在这n台机器上运行,这m个Job需要逐一映射到n个节点中一个,这时候可以选择一种简单的Hash算法来让m个Job可以均匀分布到n个节点中,比如 hash(Job)%n ,看上去很完美,但考虑如下两种情形:1、n个节点中有一个宕掉了,这时候节点数量变更为n-1,此时的映射公式变成 hash(Job)%(n-1);2、由于Job数量增加,需要新增机器,此时的映射公式变成 hash(Job)%(n+1), 1、2两种情形可以看到,基本上所有的Job会被重新分配到跟节点变动前不同的节点上,意味着需要迁移几乎所有正在运行的Job,想想这样会给系统带来多大的复杂性和性能损耗。
  • 一致性hash算法:见上面链接

2、CAP 定理、 BASE 理论、2PC、3PC

CAP理论

  • 一致性(C):在分布式系统中的所有数据备份,在同一时刻是否同样的值。(等同于所有节点访问同一份最新的数据副本)
  • 可用性(A):在集群中一部分节点故障后,集群整体是否还能响应客户端的读写请求。(对数据更新具备高可用性)
  • 分区容忍性(P):以实际效果而言,分区相当于对通信的时限要求。系统如果不能在时限内达成数据一致性,就意味着发生了分区的情况,必须就当前操作在C和A之间做出选择。

zookeeper基于CP、Eureka(SpringCloud基于它实现服务注册发现)则是AP。

BASE 理论:

BASE是Basically Available(基本可用)、Soft state(软状态)和Eventually consistent(最终一致性)三个短语的简写,BASE是对CAP中一致性和可用性权衡的结果,其来源于对大规模互联网系统分布式实践的结论,是基于CAP定理逐步演化而来的,其核心思想是即使无法做到强一致性(Strong consistency),但每个应用都可以根据自身的业务特点,采用适当的方式来使系统达到最终一致性(Eventual consistency)。接下来我们着重对BASE中的三要素进行详细讲解。

2PC(分布式事务):

     1. 事务管理器要求每个涉及到事务的数据库预提交(precommit)此操作,并反映是否可以提交。

     2. 事务协调器要求每个数据库提交数据,或者回滚数据。

(第一阶段:准备阶段(投票阶段)和第二阶段:提交阶段(执行阶段))

优点: 尽量保证了数据的强一致,实现成本较低,在各大主流数据库都有自己实现,对于 MySQL 是从 5.5 开始支持。

缺点:单点问题:事务管理器在整个流程中扮演的角色很关键,如果其宕机,比如在第一阶段已经完成,在第二阶段正准备提交的时候事务管理器宕机,资源管理器就会一直阻塞,导致数据库无法使用。同步阻塞:在准备就绪之后,资源管理器中的资源一直处于阻塞,直到提交完成,释放资源。数据不一致:两阶段提交协议虽然为分布式数据强一致性所设计,但仍然存在数据不一致性的可能。比如在第二阶段中,假设协调者发出了事务 Commit 的通知,但是因为网络问题该通知仅被一部分参与者所收到并执行了 Commit 操作,其余的参与者则因为没有收到通知一直处于阻塞状态,这时候就产生了数据的不一致性。总的来说,2PC 协议比较简单,成本较低,但是其单点问题,以及不能支持高并发(由于同步阻塞)依然是其最大的弱点。

3PC(分布式事务):

针对两阶段提交存在的问题,三阶段提交协议通过引入一个“预询盘”阶段,以及超时策略来减少整个集群的阻塞时间,提升系统性能。三阶段提交的三个阶段分别为:can_commit,pre_commit,do_commit。

第一阶段:can_commit

该阶段协调者会去询问各个参与者是否能够正常执行事务,参与者根据自身情况回复一个预估值,相对于真正的执行事务,这个过程是轻量的,具体步骤如下:

1. 协调者向各个参与者发送事务询问通知,询问是否可以执行事务操作,并等待回复。

2. 各个参与者依据自身状况回复一个预估值,如果预估自己能够正常执行事务就返回确定信息,并进入预备状态,否则返回否定信息。

第二阶段:pre_commit

本阶段协调者会根据第一阶段的询盘结果采取相应操作,询盘结果主要有三种:

1. 所有的参与者都返回确定信息。

2. 一个或多个参与者返回否定信息。

3. 协调者等待超时

针对第一种情况,协调者会向所有参与者发送事务执行请求,具体步骤如下:

1. 协调者向所有的事务参与者发送事务执行通知。

2. 参与者收到通知后,执行事务,但不提交。

3. 参与者将事务执行情况返回给客户端。

在上面的步骤中,如果参与者等待超时,则会中断事务。 针对第二、三种情况,协调者认为事务无法正常执行,于是向各个参与者发出abort通知,请求退出预备状态,具体步骤如下:

1. 协调者向所有事务参与者发送abort通知

2. 参与者收到通知后,中断事务

第三阶段:do_commit

如果第二阶段事务未中断,那么本阶段协调者将会依据事务执行返回的结果来决定提交或回滚事务,分为三种情况:

1. 所有的参与者都能正常执行事务。

2. 一个或多个参与者执行事务失败。

3. 协调者等待超时。

针对第一种情况,协调者向各个参与者发起事务提交请求,具体步骤如下:

1. 协调者向所有参与者发送事务commit通知。

2. 所有参与者在收到通知之后执行commit操作,并释放占有的资源。

3. 参与者向协调者反馈事务提交结果。

针对第二、三种情况,协调者认为事务无法正常执行,于是向各个参与者发送事务回滚请求,具体步骤如下:

1. 协调者向所有参与者发送事务rollback通知。

2. 所有参与者在收到通知之后执行rollback操作,并释放占有的资源。

3. 参与者向协调者反馈事务提交结果。

在本阶段如果因为协调者或网络问题,导致参与者迟迟不能收到来自协调者的commit或rollback请求,那么参与者将不会如两阶段提交中那样陷入阻塞,而是等待超时后继续commit。相对于两阶段提交虽然降低了同步阻塞,但仍然无法避免数据的不一致性。

3、分布式事务:TCC(Try-Confirm-Cancel)理论

TCC 将事务提交分为 Try - Confirm - Cancel 3个操作。

  • Try:预留业务资源/数据效验
  • Confirm:确认执行业务操作
  • Cancel:取消执行业务操作

TCC事务处理流程和 2PC 二阶段提交类似,不过 2PC通常都是在跨库的DB层面,而TCC本质就是一个应用层面的2PC。

原本的一个接口,要改造为3个逻辑,Try-Confirm-Cancel。

  • 先是服务调用链路依次执行Try逻辑

  • 如果都正常的话,TCC分布式事务框架推进执行Confirm逻辑,完成整个事务

  • 如果某个服务的Try逻辑有问题,TCC分布式事务框架感知到之后就会推进执行各个服务的Cancel逻辑,撤销之前执行的各种操作

先来Try一下,不要把业务逻辑完成,先试试看,看各个服务能不能基本正常运转,能不能先冻结我需要的资源。如果Try都ok,也就是说,底层的数据库、redis、elasticsearch、MQ都是可以写入数据的,并且你保留好了需要使用的一些资源(比如冻结了一部分库存)。接着,再执行各个服务的Confirm逻辑,基本上Confirm就可以很大概率保证一个分布式事务的完成了。那如果Try阶段某个服务就失败了,比如说底层的数据库挂了,或者redis挂了,等等。此时就自动执行各个服务的Cancel逻辑,把之前的Try逻辑都回滚,所有服务都不要执行任何设计的业务逻辑。保证大家要么一起成功,要么一起失败。这就是所谓的TCC分布式事务。

如果有一些意外的情况发生了,比如说订单服务突然挂了,然后再次重启,TCC分布式事务框架是如何保证之前没执行完的分布式事务继续执行的呢?所以,TCC事务框架都是要记录一些分布式事务的活动日志的,可以在磁盘上的日志文件里记录,也可以在数据库里记录。保存下来分布式事务运行的各个阶段和状态。问题还没完,万一某个服务的Cancel或者Confirm逻辑执行一直失败怎么办呢?那也很简单,TCC事务框架会通过活动日志记录各个服务的状态。举个例子,比如发现某个服务的Cancel或者Confirm一直没成功,会不停的重试调用他的Cancel或者Confirm逻辑,务必要他成功!

TCC优点:让应用自己定义数据库操作的粒度,使得降低锁冲突、提高吞吐量成为可能。

TCC不足之处:

  • 对应用的侵入性强。业务逻辑的每个分支都需要实现try、confirm、cancel三个操作,应用侵入性较强,改造成本高。
  • 实现难度较大。需要按照网络状态、系统故障等不同的失败原因实现不同的回滚策略。为了满足一致性的要求,confirm和cancel接口必须实现幂等。

11、JUC

11.1 CountDownLatch、CyclicBarrier、Semaphore

CountDownLatchCountDownLatch latch = new CountDownLatch(4);比如有一个任务A,它要等待其他4个任务执行完毕之后才能执行;A调用await(),等其他四个任务都执行完countDown()方法后,才能继续执行A被阻塞的逻辑;

CyclicBarrierCyclicBarrier barrier  = new CyclicBarrier(4);有4干个线程都要进行写数据操作,并且只有所有4个线程都完成写数据操作之后,这些线程才能继续做后面的事情,此时就可以利用CyclicBarrier了;只有await方法,4个线程都执行完await方法后,4个线程才能继续自己后面的任务;

Semaphore:信号量:Semaphore semaphore = new Semaphore(5),默认非公平模式;可以控同时访问某个资源的线程个数,通过 acquire() 获取一个许可,如果没有就等待,而 release() 释放一个许可。

总结:

  1)CountDownLatch和CyclicBarrier都能够实现线程之间的等待,只不过它们侧重点不同:

    CountDownLatch一般用于某个线程A等待若干个其他线程执行完任务之后,它才执行;

    而CyclicBarrier一般用于一组线程互相等待至某个状态,然后这一组线程再同时执行;

    另外,CountDownLatch是不能够重用的,而CyclicBarrier是可以重用的。

  2)Semaphore其实和锁有点类似,它一般用于控制对某组资源的访问权限。

11.AQS(AbstractQueuedSynchronizer):Java并发之AQS详解

11.3 线程池

如何设置合理的线程数

任务一般分为:CPU密集型、IO密集型、混合型,对于不同类型的任务需要分配不同大小的线程池

CPU密集型:

  • 尽量使用较小的线程池,一般Cpu核心数+1;因为CPU密集型任务CPU的使用率很高,若开过多的线程,只能增加线程上下文的切换次数,带来额外的开销

IO密集型:

  • 方法一:可以使用较大的线程池,一般CPU核心数 * 2;IO密集型CPU使用率不高,可以让CPU等待IO的时候处理别的任务,充分利用cpu时间;
  • 方法二:线程等待时间所占比例越高,需要越多线程。线程CPU时间所占比例越高,需要越少线程。举个例子:比如平均每个线程CPU运行时间为0.5s,而线程等待时间(非CPU运行时间,比如IO)为1.5s,CPU核心数为8,那么根据上面这个公式估算得到:((0.5+1.5)/0.5)*8=32。这个公式进一步转化为:最佳线程数目 = (线程等待时间与线程CPU时间之比 + 1)* CPU数目

如何设置合理的队列大小

  • 基于空间 :比如队列可以占用10M内存,每个请求大小10K ,那么workQueue队列长度为1000合适
  • 基于时间 :对于单个线程,如果请求超时时间为1s,单个请求平均处理时间10ms,那么队列长度为100合适

12、系统安全设计

12.1 XSS:跨站脚本攻击

XSS危害

  1. 通过document.cookie盗取cookie
  2. 使用js或css破坏页面正常的结构与样式
  3. 流量劫持(通过访问某段具有window.location.href定位到其他页面)
  4. Dos攻击:利用合理的客户端请求来占用过多的服务器资源,从而使合法用户无法得到服务器响应。
  5. 利用iframe、frame、XMLHttpRequest或上述Flash等方式,以(被攻击)用户的身份执行一些管理动作,或执行一些一般的如发微博、加好友、发私信等操作。
  6. 利用可被攻击的域受到其他域信任的特点,以受信任来源的身份请求一些平时不允许的操作,如进行不当的投票活动。

XSS防御

从以上的反射型和DOM XSS攻击可以看出,我们不能原样的将用户输入的数据直接存到服务器,需要对数据进行一些处理。以上的代码出现的一些问题如下

  1. 没有过滤危险的DOM节点。如具有执行脚本能力的script, 具有显示广告和色情图片的img, 具有改变样式的link, style, 具有内嵌页面的iframe, frame等元素节点。
  2. 没有过滤危险的属性节点。如事件, style, src, href等
  3. 没有对cookie设置httpOnly。

如果将以上三点都在渲染过程中过滤,那么出现的XSS攻击的概率也就小很多。

12.2 CSRF(Cross-site request forgery):跨站请求伪造

要完成一次CSRF攻击,受害者必须依次完成两个步骤:

  1.登录受信任网站A,并在本地生成Cookie。

  2.在不登出A的情况下,访问危险网站B。

理解CSRF攻击:攻击者盗用了你的身份,以你的名义发送恶意请求。CSRF能够做的事情包括:以你名义发送邮件,发消息,盗取你的账号,甚至于购买商品,虚拟货币转账......造成的问题包括:个人隐私泄露以及财产安全。
预防措施:

服务端: 1、验证HTTP Referer字段;  2、 校验token

客户端:在请求地址中添加token并验证

HttpServletRequest request = (HttpServletRequest) servletRequest;
String origin = request.getHeader("Origin");
//我们假设只有这个origins列表里面的才是合法的
List<String> origins = Arrays.asList("www.baidu.com", "www.nba.com");
for (String originTemp : origins) {
    if (originTemp.equals(origin)) {
        //安全的
    }
}
//TODO  非安全的 

// 从 HTTP 头中取得 Referer 值
 String referer=request.getHeader("Referer");
 // 判断 Referer 是否以 bank.example 开头
 if((referer!=null) &&(referer.trim().startsWith(“bank.example”))){
    chain.doFilter(request, response);
 }else{
request.getRequestDispatcher(“error.jsp”).forward(request,response);
 } 

12.3 https

HTTP(超文本传输协议)被用于在Web浏览器和网站服务器之间,以明文方式传递信息,不提供任何方式的数据加密,因此使用HTTP协议传输隐私信息(如:银行卡号、密码等支付信息)非常不安全。为了解决这一安全缺陷,网景公司设计了SSL(Secure Sockets Layer)协议,在HTTP的基础上加入了SSL(Secure Sockets Layer)协议,SSL依靠SSL证书来验证服务器的身份,并为浏览器和服务器之间的通信加密。从而诞生了HTTPS(安全套接字层超文本传输协议)。简单来说,HTTPS协议="SSL+HTTP协议"构建的可进行加密传输、身份认证的网络协议,是HTTP的安全版。
SSL/TLS协议运行机制的概述
HTTPS 如何保证数据传输的安全性

SSL证书作用

  • 实现加密传输:网站安装SSL证书后,使用HTTPS加密协议访问网站,可激活客户端浏览器到网站服务器之间的SSL加密通道,实现高强度双向加密传输。
  • 认证服务器真实身份:在网站使用HTTPS协议后,通过SSL证书,浏览器内置安全机制,实时查验证书状态,通过浏览器向用户展示网站认证信息,让用户轻松识别网站真实身份。
  • 保障信息不被窃取:使用了SSL证书后,数据实现了加密传输,保护数据不被泄露或者恶意篡改。

虽然HTTPS可以保证你与网站的通信是加密的,是安全的,但是不能阻止恶意网站,所以浏览网页时千万不要放松警惕,特别是涉及资金或者个人信息等操作时。目前要保护上网安全的话,唯一的方法还是使用一些第三方安全公司的安全软件,比如浏览器。这些公司一般都有丰富的钓鱼网站数据库,如果你访问了钓鱼网站的话,都是可以识别出来并发出警告的。HTTPS有两种,上文提到的就是绿色的HTTPS,绿色的HTTPS表示网站有证书,并且与网站服务器进行通信时数据是加密的,另外还有红色HTTPS,红色HTTPS表示通信时依然是加密的,并且网站有证书,但是证书可能过期或者没有验证过,所以如果在浏览网页时看到有网站的网址前面有红色HTTPS的话,最好不要在这种网站中输入任何账号密码。

12.4  加解密

  • Base64位加密(可加密解密): 最简单的加密方式,没有密钥,这种方式只要让别人拿到你的密文,就可以直接解密,只能用来迷惑,一般情况下不单独使用,因为真的并没有什么卵用~可以和其他加密方式混合起来,作为一层外部包装。
  • MD5加密(加密不可逆): MD5的全称是Message-Digest Algorithm 5(信息-摘要算法)。128位长度。MD5是一种不可逆算法。具有很高的安全性。它对应任何字符串都可以加密成一段唯一的固定长度的代码。(小贴士:为啥MD5加密算法不可逆呢?因为MD5加密是有种有损的加密方式,比如一段数据为'123',我在加密的时候,遇到1和3都直接当做是a,加密后变成了'a2a',所以解密的时候就出现了4种组合'323''121''123''321',数据一多,自然找不到原始的数据了,当然这种方式加密的密文也不需要解密,需要的时候直接发送原始密文就好了~只是看不到密文原本的内容)

  • sha1加密(加密不可逆): SHA1的全称是Secure Hash Algorithm(安全哈希算法) 。SHA1基于MD5,加密后的数据长度更长。它对长度小于264的输入,产生长度为160bit的散列值。比MD5多32位。因此,比MD5更加安全,但SHA1的运算速度就比MD5要慢了。使用方法和MD5其实是一样的。

  • AES加密(需要密钥才能解密):AES(Advanced Encryption Standard):高级加密标准,是下一代的加密算法标准,速度快,安全级别高;AES加密为对称密钥加密,加密和解密都是用同一个解密规则,AES加密过程是在一个4×4的字节矩阵上运作,这个矩阵又称为"状态(state)",因为密钥和加密块要在矩阵上多次的迭代,置换,组合,所以对加密快和密钥的字节数都有一定的要求,AES密钥长度的最少支持为128、192、256,加密块分组长度128位。这种加密模式有一个最大弱点:甲方必须把加密规则告诉乙方,否则无法解密。保存和传递密钥,就成了最头疼的问题。 

  • RSA加密(公钥加密,私钥解密): 它是目前最重要的加密算法!计算机通信安全的基石,保证了加密数据不会被破解。你可以想象一下,信用卡交易被破解的后果。甲乙双方通讯,乙方生成公钥和私钥,甲方获取公钥,并对信息加密(公钥是公开的,任何人都可以获取),甲方用公钥对信息进行加密,此时加密后的信息只有私钥才可以破解,所以只要私钥不泄漏,就能保证信息的安全性。

    # 先生成一对密钥,然后保存.pem格式文件,当然也可以直接使用 

    # load公钥和密钥

    # 用公钥加密、再用私钥解密

    # sign 用私钥签名认证、再用公钥验证签名

    鲍勃给苏珊回信,写完后用Hash函数,生成信件的摘要(digest)

    然后,鲍勃使用私钥,对这个摘要加密,生成"数字签名"(signature)。

    鲍勃将这个签名,附在信件下面,一起发给苏珊。

    苏珊收信后,取下数字签名,用鲍勃的公钥解密,得到信件的摘要。由此证明,这封信确实是鲍勃发出的。

    苏珊再对信件本身使用Hash函数,将得到的结果,与上一步得到的摘要进行对比。如果两者一致,就证明这封信未被修改过。

    复杂的情况出现了。道格想欺骗苏珊,他偷偷使用了苏珊的电脑,用自己的公钥换走了鲍勃的公钥。因此,他就可以冒充鲍勃,写信给苏珊。

    苏珊发现,自己无法确定公钥是否真的属于鲍勃。她想到了一个办法,要求鲍勃去找"证书中心"(certificate authority,简称CA),为公钥做认证。证书中心用自己的私钥,对鲍勃的公钥和一些相关信息一起加密,生成"数字证书"(Digital Certificate)。

    鲍勃拿到数字证书以后,就可以放心了。以后再给苏珊写信,只要在签名的同时,再附上数字证书就行了。

    苏珊收信后,用CA的公钥解开数字证书,就可以拿到鲍勃真实的公钥了,然后就能证明"数字签名"是否真的是鲍勃签的。

  • 总结:

    1)信息 + HASH = 摘要    摘要 + 私钥签名(给收方做对比用的,验证收发内容是否一致)

    2)公钥 + 相关信息 + CA私钥数字证书(验证发送者是否正确,是可信任的公钥)

13、netty

13.1 channel结构图:

通过上图我们可以看到, 一个 Channel 包含了一个 ChannelPipeline, 而 ChannelPipeline 中又维护了一个由 ChannelHandlerContext 组成的双向链表. 这个链表的头是 HeadContext, 链表的尾是 TailContext, 并且每个 ChannelHandlerContext 中又关联着一个 ChannelHandler。

13.2 传播机制

Inbound 事件传播方法有:(head -> customContext -> tail)
ChannelHandlerContext.fireChannelRegistered()
ChannelHandlerContext.fireChannelActive()
ChannelHandlerContext.fireChannelRead(Object)
ChannelHandlerContext.fireChannelReadComplete()
ChannelHandlerContext.fireExceptionCaught(Throwable)
ChannelHandlerContext.fireUserEventTriggered(Object)
ChannelHandlerContext.fireChannelWritabilityChanged()
ChannelHandlerContext.fireChannelInactive()
ChannelHandlerContext.fireChannelUnregistered()

Oubound 事件传输方法有:(tail -> customContext -> head)
ChannelHandlerContext.bind(SocketAddress, ChannelPromise)
ChannelHandlerContext.connect(SocketAddress, SocketAddress, ChannelPromise)
ChannelHandlerContext.write(Object, ChannelPromise)
ChannelHandlerContext.flush()
ChannelHandlerContext.read()
ChannelHandlerContext.disconnect(ChannelPromise)
ChannelHandlerContext.close(ChannelPromise)

数据从head节点流入,先拆包,然后解码成业务对象,最后经过业务Handler处理,调用write,将结果对象写出去。而写的过程先通过tail节点,然后通过encoder节点将对象编码成ByteBuf,最后将该ByteBuf对象传递到head节点,调用底层的Unsafe写到jdk底层管道。

13.3 pipeline

final class DefaultChannelPipeline implements ChannelPipeline {

    final AbstractChannel channel;

    final AbstractChannelHandlerContext head;
    final AbstractChannelHandlerContext tail;

    DefaultChannelPipeline(AbstractChannel channel) {
        if (channel == null) {
            throw new NullPointerException("channel");
        }
        this.channel = channel;

        tail = new TailContext(this);
        head = new HeadContext(this);

        head.next = tail;
        tail.prev = head;
    }

static final class TailContext extends AbstractChannelHandlerContext implements ChannelInboundHandler {
实现Inbound接口,且方法全部空实现


static final class HeadContext extends AbstractChannelHandlerContext implements ChannelOutboundHandler {
实现Outbound接口,且方法全部空实现

TailContext是outbound事件的起点,inbound的终点,他的所有inbound方法都是空实现,而outbound方法则是简单的把控制权交给前面的handler。有了tail,我们能够保证inbound处理流最终能够优雅的结束掉(空实现,事件不会再往后传播),而不用在进行各种空判断;而对于outbound事件,它直接唤起下一个handler处理,充当了一个隐形的老好人。可以这么说,虽然它确实没什么太大的卵用,但是没有它,你会发现很多地方的代码将变得很难看。

HeadContext是inbound的起点,outbound的终点。作为inbound的起点,他也只是默默的把控制权交给后面的handler。而作为outbound的终点,他承担的责任则非常重要,他负责对outbound事件进行最终的底层调用(这里的底层是netty底层,而非jdk底层或操作系统底层),因此如果你暂时不关心编解码,而想了解write方法之类的最终实现,可以直接在HeadContext的对应方法上加断点,然后就可以直接了解这一部分知识了。

13.4 TCP粘包/拆包

在TCP传输中,一个完整的消息包可能会被TCP拆分成多个包进行发送,也有可能把多个小的包封装成一个大的数据包发送,因而数据接收方无法区分消息包的头尾,在接收方来看,可能一次接收的数据内容只是一次完整请求的部分数据,或包含多次请求的数据等情况。基于此,常见有三种解决方式:

  • 消息包定长,固定每个消息包长度,不够的话用空格补,缺点是长度不能灵活控制,字节不够的情况下补空格会造成浪费,字节过多还要处理分包传输的逻辑
  • 使用定长消息头和变长消息体,其中消息体的长度必须在消息头指出,在接收方每次接收数据时,先确保获取到定长的消息头,再从消息头中解析出消息体的长度,以此来界定获取以此完整请求的消息包数据。
  • 在消息包尾部指定分隔符,缺点是需要遍历数据,判断是否为分隔符,影响效率。

公司内部RPC框架采用第二种方式,如何进行粘包/拆包处理细节:FrameDecoder
实现原理:不断读取接收到的字节流,并累加到cumulation变量,通过调用callDecode来尝试对当前累加的字节Buffer cumulation进行解码,直到解析出一个完整请求的feame对象,最后会调用Channels#fireMessageReceived触发Handler的pipeline调用来完成一次完整请求。

个人图解,读者忽略:

粘包拆包

13.5 Netty设置高低水位

Server端:
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.childOption(ChannelOption.WRITE_BUFFER_HIGH_WATER_MARK, 32 * 1024);
bootstrap.childOption(ChannelOption.WRITE_BUFFER_LOW_WATER_MARK, 8 * 1024);

Client端:
Bootstrap bootstrap = new Bootstrap();
bootstrap.option(ChannelOption.WRITE_BUFFER_HIGH_WATER_MARK, 32 * 1024);
bootstrap.option(ChannelOption.WRITE_BUFFER_LOW_WATER_MARK, 8 * 1024)

设置高低水位的意义:
英文:For instance, imagine you have a queue of tasks on server side that is filled by clients and processed by backend. In case clients send tasks too quick the length of the queue grows. One needs to introduce so named high watermark and low watermark. If queue length is greater than high watermark stop reading from sockets and queue length will decrease. When queue length becomes less than low watermark start reading tasks from sockets again.Note, to make it possible for clients to adapt to speed you process tasks (actually to adapt window size) one shouldn't make a big gap between high and low watermarks. From the other side small gap means you'll be too often add/remove sockets from the event loop.
中文:例如,假设您在服务器端有一个队列,由客户端填充,并由后端处理。 如果客户端发送任务的速度过快,则队列的长度会增加。 需要引入所谓的高水位和低水位。 如果队列长度大于高水位标记,请停止从套接字读取数据,队列长度将减少。 当队列长度小于低水位标记时,再次开始从套接字读取任务。请注意,为了使客户能够适应您的处理任务(实际上是适应窗口大小)的速度,不应在高水位线和低水位线之间留出较大的差距。 另一方面,小间隙意味着您经常会从事件循环中添加/删除套接字。自己根据业务特点找平衡。

14、锁类型:锁类型

  • 偏向锁/轻量级锁/重量级锁:这三种锁是指锁的状态,并且是针对Synchronized。在Java 5通过引入锁升级的机制来实现高效Synchronized。这三种锁的状态是通过对象监视器在对象头中的字段来表明的。
  • 1、偏向锁:是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁。降低获取锁的代价。
  • 2、轻量级锁:是指当锁是偏向锁的时候,被另一个线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,提高性能。
  • 3、重量级锁:是指当锁为轻量级锁的时候,另一个线程虽然是自旋,但自旋不会一直持续下去,当自旋一定次数的时候,还没有获取到锁,就会进入阻塞,该锁膨胀为重量级锁。重量级锁会让其他申请的线程进入阻塞,性能降低。
  • 自旋锁:在Java中,自旋锁是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU。

15、Zookeeper

Zookeeper有四种类型的znode: 

  • PERSISTENT-持久化目录节点 :客户端与zookeeper断开连接后,该节点依旧存在 
  • PERSISTENT_SEQUENTIAL-持久化顺序编号目录节点 :客户端与zookeeper断开连接后,该节点依旧存在,只是Zookeeper给该节点名称进行顺序编号 
  • EPHEMERAL-临时目录节点 :客户端与zookeeper断开连接后,该节点被删除 
  • EPHEMERAL_SEQUENTIAL-临时顺序编号目录节点 :客户端与zookeeper断开连接后,该节点被删除,只是Zookeeper给该节点名称进行顺序编号 

Zookeeper 的核心是原子广播,这个机制保证了各个Server之间的同步。实现这个机制的协议叫做Zab协议。Zab协议有两种模式,它们分别是恢复模式(选主)和广播模式(同步)。当服务启动或者在领导者崩溃后,Zab就进入了恢复模式,当领导者被选举出来,且大多数Server完成了和 leader的状态同步以后,恢复模式就结束了。状态同步保证了leader和Server具有相同的系统状态。 

直接使用Zookeeper原生API的人并不多,因为:

  • 1)连接的创建是异步的,需要开发人员自行编码实现等待 
  • 2)连接没有超时自动的重连机制 
  • 3)Zookeeper本身没提供序列化机制,需要开发人员自行指定,从而实现数据的序列化和反序列化 
  • 4)Watcher注册一次只会生效一次,需要不断的重复注册 
  • 5)Watcher的使用方式不符合java本身的术语,如果采用监听器方式,更容易理解 
  • 6)不支持递归创建树形节点

消息广播:

在消息广播的过程中,leader服务器会为每一个follower服务器都各自分配一个单独的队列,然后将需要广播的事务proposal依次放入这些队列中去,并且根据FIFO策略进行消息发送。每一个follower服务器在接受到这个事务proposal后,都会首先将其以事务日志的形式写入到本地磁盘中去,并且在成功写入后反馈给leader服务器一个ack响应。当leader服务器收到超过半数follower的ack响应后,就会广播一个commit消息给所有的follower服务器以通知其进行事务提交,同时leader自身也会完成对事务的提交,而每一个follower服务器在收到commit消息后,也会完成对事务的提交

崩溃恢复:

目前有5台服务器,每台服务器均没有数据,它们的编号分别是1,2,3,4,5,按编号依次启动,它们的选择举过程如下:

  • 服务器1启动,给自己投票,然后发投票信息,由于其它机器还没有启动所以它收不到反馈信息,服务器1的状态一直属于Looking。
  • 服务器2启动,给自己投票,同时与之前启动的服务器1交换结果,由于服务器2的编号大所以服务器2胜出,但此时投票数没有大于半数,所以两个服务器的状态依然是LOOKING。
  • 服务器3启动,给自己投票,同时与之前启动的服务器1,2交换信息,由于服务器3的编号最大所以服务器3胜出,此时投票数正好大于半数,所以服务器3成为领导者,服务器1,2成为小弟。
  • 服务器4启动,给自己投票,同时与之前启动的服务器1,2,3交换信息,尽管服务器4的编号大,但之前服务器3已经胜出,所以服务器4只能成为小弟。
  • 服务器5启动,后面的逻辑同服务器4成为小弟

16、 单点登录

Cookie通过在客户端记录信息确定用户身份,Session通过在服务器端记录信息确定用户身份。

  • cookie数据存放在客户的浏览器上,session数据放在服务器上。
  • cookie不是很安全,别人可以分析存放在本地的cookie并进行cookie欺骗,考虑到安全应当使用session。
  • session会在一定时间内保存在服务器上。当访问增多会占用你服务器的性能,考虑到减轻服务器性能方面应当使用cookie。
  • 单个cookie保存的数据不能超过4K,很多浏览器都限制一个站点最多保存20个cookie。
  • 可以考虑将登陆信息等重要信息存放为session,其他信息如果需要保留,可以放在cookie中。

16.2 Session

Session是另一种记录客户状态的机制,不同的是Cookie保存在客户端浏览器中,而Session保存在服务器上。客户端浏览器访问服务器的时候,服务器把客户端信息以某种形式记录,在服务器上,这就是Session。客户端浏览器再次访问时只需要从该Session中查找该客户的状态就可以了。每个用户访问服务器都会建立一个session,那服务器是怎么标识用户的唯一身份呢?事实上,用户与服务器建立连接的同时,服务器会自动为其分配一个SessionId。

两个问题:

  • 什么东西可以让你每次请求都把SessionId自动带到服务器呢?显然就是cookie了,如果你想为用户建立一次会话,可以在用户授权成功时给他一个唯一的cookie。当一个用户提交了表单时,浏览器会将用户的SessionId自动附加在HTTP头信息中,(这是浏览器的自动功能,用户不会察觉到),当服务器处理完这个表单后,将结果返回SessionId所对应的用户。试想,如果没有 SessionId,当有两个用户同时进行注册时,服务器怎样才能知道到底是哪个用户提交了哪个表单呢。
  • 储存需要的信息。服务器通过SessionId作为key,读写到对应的value,这就达到了保持会话信息的目的。

16.3 单点登录(SSO):token

相比于单系统登录,sso需要一个独立的认证中心,只有认证中心能接受用户的用户名密码等安全信息,其他系统不提供登录入口,只接受认证中心的间接授权。间接授权通过令牌实现,sso认证中心验证用户的用户名密码没问题,创建授权令牌,在接下来的跳转过程中,授权令牌作为参数发送给各个子系统,子系统拿到令牌,即得到了授权,可以借此创建局部会话,局部会话登录方式与单系统的登录方式相同。这个过程,也就是单点登录的原理,用下图说明;比如说, 小F已经登录了系统, 我给他发一个令牌(token), 里边包含了小F的 user id, 下一次小F 再次通过Http 请求访问我的时候, 把这个token 通过Http header 带过来不就可以了。不过这和session id没有本质区别啊, 任何人都可以可以伪造,  所以我得想点儿办法, 让别人伪造不了。那就对数据做一个签名吧, 比如说我用HMAC-SHA256 算法,加上一个只有我才知道的密钥,  对数据做一个签名, 把这个签名和数据一起作为token ,   由于密钥别人不知道, 就无法伪造token了。

图片123455

这个token 我不保存,  当小F把这个token 给我发过来的时候,我再用同样的HMAC-SHA256 算法和同样的密钥,对数据再计算一次签名, 和token 中的签名做个比较, 如果相同, 我就知道小F已经登录过了,并且可以直接取到小F的user id ,  如果不相同, 数据部分肯定被人篡改过, 我就告诉发送者: 对不起,没有认证。

阿斯顿发发
Token 中的数据是明文保存的(虽然我会用Base64做下编码, 但那不是加密), 还是可以被别人看到的, 所以我不能在其中保存像密码这样的敏感信息。

17、TCPUDP

17.1 TCPUDP的区别

  • TCP面向连接(如打电话要先拨号建立连接);UDP是无连接的,即发送数据之前不需要建立连接
  • TCP提供可靠的服务。也就是说,通过TCP连接传送的数据,无差错,不丢失,不重复,且按序到达;UDP尽最大努力交付,即不保证可靠交付
  • TCP面向字节流,实际上是TCP把数据看成一连串无结构的字节流;UDP是面向报文的
  • UDP没有拥塞控制,因此网络出现拥塞不会使源主机的发送速率降低(对实时应用很有用,如IP电话,实时视频会议等)
  • 每一条TCP连接只能是点到点的;UDP支持一对一,一对多,多对一和多对多的交互通信
  • TCP首部开销20字节;UDP的首部开销小,只有8个字节
  • TCP的逻辑通信信道是全双工的可靠信道,UDP则是不可靠信道

17.2 TCP(Transmission Control Protocol) 传输控制协议

TCP是主机对主机层的传输控制协议,提供可靠的连接服务,采用三次握手确认建立一个连接:

Sequence number(顺序号码) Acknowledge number(确认号码)

  • 第一次握手:主机 A 发送位码为 syn=1,随机产生 seq number=1234567 的数据包到服务器,主机 B 由 SYN=1 知道,A 要求建立联机;
  • 第二次握手:主机 B 收到请求后要确认联机信息,向 A 发送 ack number=(主机 A 的seq+1),syn=1,ack=1,随机产生seq=7654321 的包
  • 第三次握手:主机 A 收到后检查 ack number 是否正确,即第一次发送的 seq number+1,以及位码 ack 是否为 1,若正确,主机 A 会再发送 ack number=(主机 B 的 seq+1),ack=1,主机 B 收到后确认 seq 值与 ack=1 则连接建立成功。 

阿斯顿发发加水淀粉

 TCP 建立连接要进行三次握手,而断开连接要进行四次。这是由于 TCP 的半关闭造成的。因为 TCP 连 接是全双工的(即数据可在两个方向上同时传递)所以进行关闭时每个方向上都要单独进行关闭。这个单 方向的关闭就叫半关闭。当一方完成它的数据发送任务,就发送一个 FIN 来向另一方通告将要终止这个 方向的连接。 

  • 1) 关闭客户端到服务器的连接:首先客户端 A 发送一个 FIN,用来关闭客户到服务器的数据传送, 然后等待服务器的确认。其中终止标志位 FIN=1,序列号 seq=u 
  • 2) 服务器收到这个 FIN,它发回一个 ACK,确认号 ack 为收到的序号加 1。
  • 3) 关闭服务器到客户端的连接:也是发送一个 FIN 给客户端。
  • 4) 客户段收到 FIN 后,并发回一个 ACK 报文确认,并将确认序号 seq 设置为收到序号加 1。 

撒发生

 首先进行关闭的一方将执行主动关闭,而另一方执行被动关闭。主机 A 发送 FIN 后,进入终止等待状态, 服务器 B 收到主机 A 连接释放报文段后,就立即 给主机 A 发送确认,然后服务器 B 就进入 close-wait 状态,此时 TCP 服务器进程就通知高 层应用进程,因而从 A 到 B 的连接就释放了。此时是“半关闭”状态。即 A 不可以发送给 B,但是 B 可以发送给 A。此时,若 B 没有数据报要发送给 A 了,其应用进程就通知 TCP 释 放连接,然后发送给 A 连接释放报文段,并等待确认。A 发送确认后,进入 time-wait,注 意,此时 TCP 连接还没有释放掉,然后经过时间等待计时器设置的 2MSL 后,A 才进入到 close 状态。 

18、MQ(kafka)

18.1 为什么要使用 MQ 消息中间件:

  • 解耦,要做到系统解耦;
  • 面对大流量并发时容易被冲垮;设置流量缓冲池,可以让后端系统按照自身吞吐能力进行消费,不被冲垮
  • 存在性能问题(RPC接口基本上是同步调用)即整体的服务性能遵循“木桶理论”,即链路中最慢的那个接口;

18.2 如何保证高吞吐量和消息的可靠传输:

说明:

一个集群,为什么需要多个Broker(kafka实例),多实例保证一个broker挂了,整个集群还能继续使用,高可用

一个topic,为什么要有多个part:为了方便多个生产者忘多个part并发写入消息,提供吞吐量;

一个part,为什么要有多个副本:为了防止某个broker宕机,导致消息的丢失,提高可用性;

几个名词:

ISR(In-Sync Replicas):副本 同步队列,

OSR(Outof-Sync Replicas):副本 非同步队列

AR(Assigned Replicas)= ISR+OSR

Kafka消息保证生产的信息不丢失和重复消费问题:

  • 使用同步模式的时候,有3种状态保证消息被安全生产,在配置为1(只保证写入leader成功)的话,如果刚好leader partition挂了,数据就会丢失。
  • 还有一种情况可能会丢失消息,就是使用异步模式的时候,当缓冲区满了,如果配置为0(还没有收到确认的情况下,缓冲池一满,就清空缓冲池里的消息),数据就会被立即丢弃掉。

在数据生产时避免数据丢失的方法:
只要能避免上述两种情况,那么就可以保证消息不会被丢失。

  • 在同步模式的时候,确认机制设置为-1,也就是让消息写入leader和所有的副本。
  • 在异步模式下,如果消息发出去了,但还没有收到确认的时候,缓冲池满了,在配置文件中设置成不限制阻塞超时的时间,也就说让生产端一直阻塞,这样也能保证数据不会丢失。在数据消费时,避免数据丢失的方法:如果使用了storm,要开启storm的ackfail机制;如果没有使用storm,确认数据被完成处理之后,再更新offset值。低级API中需要手动控制offset值。

数据重复消费的情况,如何处理:

  • 去重:将消息的唯一标识保存到外部介质中,每次消费处理时判断是否处理过(幂等)
  • 不管:大数据场景中,报表系统或者日志信息丢失几条都无所谓,不会影响最终的统计分析结
  • Consumer端丢失消息的情形比较简单:如果在消息处理完成前就提交了offset,那么就有可能造成数据的丢失。由于Kafka consumer默认是自动提交位移的,所以在后台提交位移前一定要保证消息被正常处理了,因此不建议采用很重的处理逻辑,如果处理耗时很长,则建议把逻辑放到另一个线程中去做。为了避免数据丢失,现给出两点建议:enable.auto.commit=false  关闭自动提交位移在消息被完整处理之后再手动提交位移。

18.3 kafka如何保证消息的顺序性

举个例子,你在 mysql 里增删改一条数据,对应出来了增删改 3 条 binlog 日志,接着这三条 binlog 发送到 MQ 里面,再消费出来依次执行,起码得保证人家是按照顺序来的吧?不然本来是:增加、修改、删除;你楞是换了顺序给执行成删除、修改、增加,不全错了么。本来这个数据同步过来,应该最后这个数据被删除了;结果你搞错了这个顺序,最后这个数据保留下来了,数据同步就出错了。那kafka有什么措施能保证消息的顺序性呢?

原理:Kafka保证同一个partition中的消息是有序的,即如果生产者按照一定的顺序发送消息,broker就会按照这个顺序把他们写入partition,消费者也会按照相同的顺序读取他们。

先说说kafka如何发送(生产)消息:(消息格式:每个消息是一个ProducerRecord对象,必须指定消息所属的Topic和消息值Value,此外还可以指定消息所属的Partition以及消息的Key)

  1. 序列化ProducerRecord
  2. 如果ProducerRecord中指定了Partition,则Partitioner不做任何事情;否则,Partitioner根据消息的key得到一个Partition。这是生产者就知道向哪个Topic下的哪个Partition发送这条消息。
  3. 消息被添加到相应的batch中,独立的线程将这些batch发送到Broker上
  4. broker收到消息会返回一个响应。如果消息成功写入Kafka,则返回RecordMetaData对象,该对象包含了Topic信息、Patition信息、消息在Partition中的Offset信息;若失败,返回一个错误

比如说我们建了一个 topic,有三个 partition。生产者在写的时候,其实可以指定一个 key,比如说我们指定了某个订单 id 作为 key,那么这个订单相关的数据,一定会被分发到同一个 partition 中去,而且这个 partition 中的数据一定是有顺序的。
消费者从 partition 中取出来数据的时候,也一定是有顺序的。

以上即可保证消息顺序的从kafka中获取出来,但是无法保证:三条顺序从kafka中获取的消息,在你自己的业务代码中,有没有顺序执行,考虑到多线程的处理,第一个获取的消息不一定第一个被更新到数据库中;但不用多线程,效率肯定会降低很多;处理方式:写 N 个内存 queue,具有相同 key 的数据都到同一个内存 queue;然后对于 N 个线程,每个线程分别消费一个内存 queue 即可,这样就能保证顺序性。

18.4 kafka为什么那么快

18.4.1 内存缓存池

客户端发消息到服务端,不是一条一条发送的,而是有一个内存缓存池(18.2中有讲到这个概念)的设计思路在里面,当消息累计到一定数量后再发给服务端;内存缓存池避免每次新建立内存导致的GC过于频繁。

18.4.2 Reactor多路复用模型

Kafka采用的架构策略是Reactor多路复用模型,简单来说,就是搞一个acceptor线程,基于底层操作系统的支持,实现连接请求监听。如果有某个设备发送了建立连接的请求过来,那么那个线程就把这个建立好的连接交给processor线程。每个processor线程会被分配N多个连接,一个线程就可以负责维持N多个连接,他同样会基于底层操作系统的支持监听N多连接的请求。如果某个连接发送了请求过来,那么这个processor线程就会把请求放到一个请求队列里去。接着后台有一个线程池,这个线程池里有工作线程,会从请求队列里获取请求,处理请求,接着将请求对应的响应放到每个processor线程对应的一个响应队列里去。最后,processor线程会把自己的响应队列里的响应发送回给客户端。

18.4.3 写入数据的超高性能:页缓存技术 + 磁盘顺序写

1)kafka是以磁盘顺序写的方式来写的。也就是说,仅仅将数据追加到文件的末尾,不是在文件的随机位置来修改数据。普通的机械磁盘如果你要是随机写的话,确实性能极差,也就是随便找到文件的某个位置来写数据。但是如果你是追加文件末尾按照顺序的方式来写数据的话,那么这种磁盘顺序写的性能基本上可以跟写内存的性能本身也是差不多的。

2)操作系统本身有一层缓存,叫做page cache,是在内存里的缓存,我们也可以称之为os cache,意思就是操作系统自己管理的缓存。你在写入磁盘文件的时候,可以直接写入这个os cache里,也就是仅仅写入内存中,接下来由操作系统自己决定什么时候把os cache里的数据真的刷入磁盘文件中。仅仅这一个步骤,就可以将磁盘文件写性能提升很多了,因为其实这里相当于是在写内存,不是在写磁盘。

18.4.4 零拷贝

从Kafka里我们经常要消费数据,那么消费的时候实际上就是要从kafka的磁盘文件里读取某条数据然后发送给下游的消费者,如图一所示。假设要是kafka什么优化都不做,就是很简单的从磁盘读数据发送给下游的消费者,那么大概过程图二所示:先看看要读的数据在不在os cache里,如果不在的话就从磁盘文件里读取数据后放入os cache。接着从操作系统的os cache里拷贝数据到应用程序进程的缓存里,再从应用程序进程的缓存里拷贝数据到操作系统层面的Socket缓存里,最后从Socket缓存里提取数据后发送到网卡,最后发送出去给下游消费。Kafka为了解决这个问题,在读数据的时候是引入零拷贝技术。也就是说,直接让操作系统的cache中的数据发送到网卡后传输给下游的消费者,中间跳过了两次拷贝数据的步骤,Socket缓存中仅仅会拷贝一个描述符过去,不会拷贝数据到Socket缓存,如图三。通过零拷贝技术,就不需要把os cache里的数据拷贝到应用缓存,再从应用缓存拷贝到Socket缓存了,两次拷贝都省略了,所以叫做零拷贝。对Socket缓存仅仅就是拷贝数据的描述符过去,然后数据就直接从os cache中发送到网卡上去了,这个过程大大的提升了数据消费时读取文件数据的性能。而且大家会注意到,在从磁盘读数据的时候,会先看看os cache内存中是否有,如果有的话,其实读数据都是直接读内存的。如果kafka集群经过良好的调优,大家会发现大量的数据都是直接写入os cache中,然后读数据的时候也是从os cache中读。相当于是Kafka完全基于内存提供数据的写和读了,所以这个整体性能会极其的高。

发布了142 篇原创文章 · 获赞 345 · 访问量 45万+

猜你喜欢

转载自blog.csdn.net/zhengchao1991/article/details/87558948