admin管理员组

文章数量:1530257

文章目录

  • JVM
    • Error和Exception的区别
    • 类加载器(快递员,将编译好的class加载进JVM)
    • 双亲委派
    • 运行时方法区
      • 1. 本地方法栈(线程私有)
      • 2. 程序计数器(线程私有)
      • 3. 方法区(Class Non-Heap存在少量垃圾回收)
      • 4. 虚拟机栈(线程私有)
      • 5. 堆(Heap)
        • 垃圾回收机制
        • 堆参数调优
  • HashMap
    • 数据结构
    • HashMap的数据插入原理(put方法)呃~~[做沉思状]。我觉得还是应该画个图比较清楚,如下
    • `刁难面试题(了解):`
    • 线程安全的HaspMap
  • Redis
    • 什么是redis
    • 使用场景
    • Redis的数据类型以及使用场景
    • Redis的持久化机制
    • `redis三大并发问题 `
    • Redis key过期策略
    • Redis的分布式锁,及应用场景,redisson
    • Redis集群
  • Rabbit MQ
    • 什么是rabbitMQ
    • 为什么要使用rabbitMQ(有什么特点)
    • 说下rabbitMQ的几种工作模式
    • 消息队列丢失的三种情况怎么防止
      • 1. 生产者丢失数据
      • 2. rabbitMQ弄丢了数据
      • 3. 消费端丢失了数据
    • rabbitMQ的适用场景
    • 详细的说明下削峰、解耦
    • rabbitMQ集群
  • mysql
    • InnoDB与MyISAM的区别
    • 索引失效的情况
    • expain执行计划
    • 什么是mycat
    • 主从复制
    • 读写分离
    • 分库
    • 分表
    • 分库分表的优缺点
    • 什么是B-Tree
    • 事务
    • MVCC (Multi-Version Concurrent Control)
      • 什么是MVCC
      • 当前读
      • 快照读
      • 实现原理
  • 多线程
    • Java内存模型(JMM:Java Memory Model)
    • 进程和线程
    • Volatile
    • 内存屏障
    • Atomic原子类
    • CAS(乐观锁)
    • Syncronized
    • Lock锁
    • 线程8锁实现
    • Synchronized和lock的区别
    • Start和run的区别
    • Wait和sleep的区别
    • Notify和NotifyAll的区别
    • 线程的生命周期
    • 线程的状态
    • 实现多线程的三种方式
    • Thread类
    • 线程池
      • 创建线程池的四种方法
      • 创建线程池的七大参数
    • 锁升级
    • 项目中使用多线程
  • spring全家桶
    • Spring
      • 什么是Spring
      • Spring Framework有哪些功能
      • Spring Frameowrok的模块
      • Spring IOC 容器
      • 依赖注入
      • beanFactory和ApplicationContext的区别
      • 什么是Spring Bean
      • Spring Bean的作用域
      • 什么是AOP
      • AOP代理模式
      • AOP应用场景-事务管理
      • Spring事务的传播特性
      • Bean的生命周期
      • 三级缓存
        • 循环依赖:
        • 一级缓存:
        • 循环依赖及三级缓存
        • 二级缓存
    • SpringMVC
      • 什么是SpringMVC
      • SpringMVC的工作流程
    • SpringBoot
      • 什么是springboot
      • 什么是约定大于配置
      • 启动原理
        • 三大注解
      • 读取配置文件的注解
    • SpringCloud
      • 微服务
      • 什么是SpringCloud
      • SpringCloud技术栈
      • SpringCloud<->springBoot版本对应
      • 五大神兽
        • Eureka注册中心
        • Nacos注册中心
        • Consul作为注册中心
        • 分布式事务
  • Zookeeper
    • 什么是Zookeeper
    • 应用场景
    • 集群选举机制
    • 数据结构
    • 集群
    • 分布式锁
  • Dubbo
    • 什么是Dubbo
    • Dubbo、springCloud的区别
  • Mybatis
    • 什么是Mybatis
    • Mybatis的优点
    • Mybatis的缺点
    • Mybatis与Hibernate的区别
    • #{}和${}的区别
    • Mybatis的动态标签
    • Mybatis的一级、二级缓存
    • 为什么分布式下实体类需要实现序列化接口
  • Elasticsearch
    • 什么是Elasticsearch
    • 与关系型数据库的数据结果对比
    • 倒排索引
    • ES索引文档的过程
    • Elasticsearch和 solr 的区别
  • 网络通信
    • 三次握手
    • 四次挥手

JVM

Error和Exception的区别

  • Error类一般是与虚拟机相关的问题,如系统崩溃,虚拟机错误,内存空间不足,方法调用栈溢等。对于这类错误的导致的应用程序中断,仅靠程序本身无法恢复和和预防,遇到这样的错误,建议让程序终止。

  • Exception类表示程序可以处理的异常,可以捕获且可能恢复。遇到这类异常,应该尽可能处理异常,使程序恢复运行,而不应该随意终止异常。

类加载器(快递员,将编译好的class加载进JVM)

  1. 启动类加载器(bootstrap,C++编写)
  2. 扩展类加载器(Extension,Java编写)
  3. 应用程序加载器(AppClassLoader)
  4. 用户自定义加载器(Java.lang.ClassLoader的子类,用户可以定制类的加载方式)

双亲委派

当一个类收到了类加载请求,他首先不会尝试自己去加载这个类,而是把这个请求委派给父类去完成,每一个层次类加载器都是如此,因此所有的加载请求都应该传送到启动类加载器中,只有父类加载器反馈给自己无法完成这个请求的时候(在它的加载路径没有找到所需加载的Class),子类加载器才会尝试自己去加载。

运行时方法区

运行时方法区(本地方法栈、程序计数器、虚拟机栈、方法区、堆)

1. 本地方法栈(线程私有)

  1. 没啥好说的,这辈子可能用不到,本来就是为了融合不同的编程语言为java所用,最初是为了融合C/C++程序,java诞生的时候是 C/C++横行的时候,要想立足,必须有调用C/C++程序,于是就在内存中开辟了一款区域处理标记native的代码(屈服了),除非是跟硬件有关的应用的,比如什么用java操作打印机啊,管理生产设备啊~~~~

2. 程序计数器(线程私有)

  • 每个线程都有一个程序计数器,是线程私有的,就是一个指针,指向方法区中的方法字节码(用来存储指向下一条指令的地址,也就是即将要执行的指令指针),由执行引擎读取下一条指令,是一个非常小的内存空间,几乎可以忽略不计。
  • 是当前线程所执行的字节码的行号指示器,字节码解释器通过改变这个计数器的值来选取下一条需要执行的字节码指令。
  • 如果执行的是一个native方法,那这个计数器就是null。
    用以完成分支、循环、跳转、异常处理、线程回复等基础功能。不会发生内存溢出错误。

3. 方法区(Class Non-Heap存在少量垃圾回收)

  • 供每个线程共享的运行时内存区域。说到底就是存储类的模型,储存了每一个类的结构信息,例如运行时的常量池、字段和方法数据、构造函数和普通方法的字节码内容,也是就是个规范,每个虚拟机里头的实现是不一样的,最典型的就是永久代(1.7)和元空间(1.8)。但是哦,变量的实例存在堆中,和方法区无关。

永久代是一个常驻内存区域,用于存放JDK自身所挟带的Class,Interface的元数据,也就是说它存储的是运行环境必须的类信息,被装载进此区域的数据是不会被垃圾回收器回收掉的,关闭JVM才会释放此区域所占用的内存。

栈管运行,堆管存储

4. 虚拟机栈(线程私有)

  • 栈也叫栈内存,主管java程序的运行,是在线程创建时创建,它的生命是跟随线程的生命期,线程结束栈内存也就释放,对于栈来说,不存在垃圾回收问题,只要线程一结束栈就销毁了,生命周期和线程一致,是线程私有的。8种基本数据类型和对象的引用变量、实例方法都是在函数的栈内存中分配。
  • 栈帧:
    说白了就是在栈中的方法,比如执行一个main方法,里面按顺序执行a、b两个方法,a方法又调用了c方法,那么在栈中就是main先压栈,接着是a,然后是c,最后是b。执行完成后出栈就是从后往前,b-c-a-main

5. 堆(Heap)

  • Java 中的堆是 JVM 所管理的最大的一块内存空间,主要用于存放各种类的实例对象。堆中会存放访问类元数据的地址,reference储存的就直接是对象的地址
  • 新生代、老年代
    • 在 Java 内存逻辑中,堆被划分成两个不同的区域:新生代、老年代。新生代又被划分为三个区域:Eden、From Survivor、To Survivor。
    • 这样划分的目的是为了使 JVM 能够更好的管理堆内存中的对象,包括内存的分配以及回收。
    • 新生代与老年代的比例的值为 1:2, 即:新生代= 1/3 的堆空间大小。老年代= 2/3 的堆空间大小, 其中,新生代被细分为 Eden 和 两个 Survivor.
    • 默认的,Edem : from : to = 8 :1 : 1,即: Eden = 80% 的新生代空间大小,from = to = 10% 的新生代空间大小。JVM 每次只会使用 Eden 和其中的一块 Survivor 区域来为对象服务,所以无论什么时候,总是有一块Survivor区域是空闲着的。因此,新生代实际可用的内存空间为 90%。
垃圾回收机制

垃圾回收(Garbage Collection)是Java虚拟机(JVM)垃圾回收器提供的一种用于在空闲时间不定时回收无任何对象引用的对象占据的内存空间的一种机制

  • Java 中的堆也是 GC 收集垃圾的主要区域。GC 分为两种:Minor GC、FullGC ( 或称为 Major GC )。
  • Minor GC 是发生在新生代中的垃圾收集动作,所采用的是复制算法。
  • 新生代几乎是所有 Java 对象出生的地方,即 Java 对象申请的内存以及存放都是在这个地方。Java 中的大部分对象通常不需长久存活,具有朝生夕灭的性质。当一个对象被判定为 “死亡” 的时候,GC 就有责任来回收掉这部分对象的内存空间。新生代是 GC 收集垃圾的频繁区域。当对象在 Eden ( 包括一个 Survivor 区域,这里假设是 from 区域 ) 出生后,在经过一次 Minor GC 后,如果对象还存活,并且能够被另外一块 Survivor 区域所容纳( 上面已经假设为 from 区域,这里应为 to 区域,即 to 区域有足够的内存空间来存储 Eden 和 from 区域中存活的对象 ),则使用复制算法将这些仍然还存活的对象复制到另外一块 Survivor 区域 ( 即 to 区域 ) 中,然后清理所使用过的 Eden 以及 Survivor 区域 ( 即from 区域 ),并且将这些对象的年龄设置为1,以后对象在 Survivor 区每熬过一次 Minor GC,就将对象的年龄 + 1,当对象的年龄达到某个值时 ( 默认是 15 岁),这些对象就会成为老年代。
  • 但这也不是一定的,对于一些较大的对象 ( 即需要分配一块较大的连续内存空间 ) 则是直接进入到老年代
  • Full GC 是发生在老年代的垃圾收集动作,所采用的是标记-清除算法。
  • 现实的生活中,老年代的人通常会比新生代的人"早死"。堆内存中的老年代(Old)不同于这个,老年代里面的对象几乎个个都是在 Survivor 区域中熬过来的,它们是不会那么容易就 “死掉” 了的。因此,Full GC 发生的次数不会有 Minor GC 那么频繁,并且做一次 Full GC 要比进行一次 Minor GC 的时间更长。
  • 另外,标记-清除算法收集垃圾的时候会产生许多的内存碎片 ( 即不连续的内存空间 ),此后需要为较大的对象分配内存空间时,若无法找到足够的连续的内存空间,就会提前触发一次 GC 的收集动作。
  • 可以举一个战争的例子,新生代的对象实例是新兵蛋子,MonorGC就是敌人,第一打仗活下来了升一级进入to-survivor,打了15次仗还活着,成为将军,进入养老区,如果养老区也快满了,就安排将军和FullGC干仗,但是有些将军是浑水摸鱼进来的,会被强大的FullGC处理掉,如果养老区也满了,就会报堆溢出异常。
堆参数调优

java1.8中,永久代已经被移除了,被元空间代替,但是本质还是类似的。源元空间与永久代最大的区别就在于:永久代使用的JVM的堆内存,但是java8以后的元空间并不在虚拟机中而是使用本机物理内存。因此,默认情况下,元空间的大小仅受到本地内存限制。类的元数据放入native memory,字符串池和类的静态变量放入java堆中,这样可以加载多少类的元数据就不再有MaxPermSize控制,而是由系统的实际可用空间来控制。
调优的目的:减少Full GC ,STW(stop the work)
设定堆内存大小

参数作用备注
-Xmx堆内存最大限新生代不宜太小,否则会有大量对象涌入老年代
-XX:NewSize新生代大小
-XX:NewRatio新生代和老生代占比
-XX:SurvivorRati伊甸园空间和幸存者空间的占比
-XX:+UseParNewGC设定垃圾回收器 - 新生代用
-XX:+UseConcMarkSweepGC设定垃圾回收器 - 老年代用
-XX:MaxTenuringThresholdfrom和to的次数
## 简单linux启动脚本

对象的四种引用
强引用被引用的对象不管怎么样都不会被垃圾回收器回收
软引用可以理解为可有可无的对象,内存足够就留着,内存不够就清除
弱引用只要垃圾收集器一运转,那么弱引用的对象就会被自动回收
虚引用用来跟踪对象被回收的状态,收到一个回收的通知

HashMap

数据结构

  • JDK1.7的时候是数组加链表, 无冲突放入数组,有冲突放入链表。
  • JDK1.8的时候是数组加链表,无冲突放入数组,有冲突的话链表长度小于8放入链表,大于8放入红黑树。

HashMap的数据插入原理(put方法)呃~~[做沉思状]。我觉得还是应该画个图比较清楚,如下

1.判断数

  1. 判断数组是否为空,为空进行初始化;
  2. 不为空,计算 k 的 hash 值,通过(n - 1) & hash计算应当存放在数组中的下标 index;
  3. 查看 table[index] 是否存在数据,没有数据就构造一个Node节点存放在 table[index] 中;
  4. 存在数据,说明发生了hash冲突(存在二个节点key的hash值一样), 继续判断key是否相等,相等,用新的value替换原数据;
  5. 如果不相等,判断当前节点类型是不是树型节点,如果是树型节点,创造树型节点插入红黑树中;(如果当前节点是树型节点证明当前已经是红黑树了)
  6. 如果不是树型节点,创建普通Node加入链表中;判断链表长度是否大于 8并且数组长度大于64, 大于的话链表转换为红黑树;
  7. 插入完成之后判断当前节点数是否大于阈值,如果大于开始扩容为原数组的二倍。
  • 一般如果new HashMap() 不传值,默认大小是16,负载因子是0.75, 如果自己传入初始大小k,初始化大小为大于k的 2的整数次方,例如如果传10,大小为16

刁难面试题(了解):

  • 【你提到hash函数,你知道HashMap的哈希函数怎么设计的吗?】
    hash函数是先拿到 key 的hashcode,是一个32位的int值,然后让hashcode的高16位和低16位进行异或操作。
  • 【那你知道为什么这么设计吗?】
    这个也叫扰动函数,这么设计有二点原因:
    1、一定要尽可能降低hash碰撞,越分散越好;
    2、算法一定要尽可能高效,因为这是高频操作, 因此采用位运算;
  • 【为什么采用hashcode的高16位和低16位异或能降低hash碰撞?hash函数能不能直接用key的hashcode?】
    如果是使用这种方式,那么在数组长度小的时候,hash只有几位进行了运算,高位数根本没有使用到,所以采用这种异或运算保证hash值可以被充分的利用。
    因为key.hashCode()函数调用的是key键值类型自带的哈希函数,返回int型散列值。int值范围为**-2147483648~2147483647**,前后加起来大概40亿的映射空间。只要哈希函数映射得比较均匀松散,一般应用是很难出现碰撞的。但问题是一个40亿长度的数组,内存是放不下的。你想,如果HashMap数组的初始大小才16,用之前需要对数组的长度取模运算,得到的余数才能用来访问数组下标。
    源码中模运算就是把散列值和数组长度-1做一个"与"操作,位运算比取余%运算要快。
    顺便说一下,这也正好解释了为什么HashMap的数组长度要取2的整数幂。因为这样(数组长度-1)正好相当于一个“低位掩码”。“与”操作的结果就是散列值的高位全部归零,只保留低位值,用来做数组下标访问。以初始长度16为例,16-1=15。2进制表示是00000000 00000000 00001111。和某散列值做“与”操作如下,结果就是截取了最低的四位值。
    但这时候问题就来了,这样就算我的散列值分布再松散,要是只取最后几位的话,碰撞也会很严重。更要命的是如果散列本身做得不好,分布上成等差数列的漏洞,如果正好让最后几个低位呈现规律性重复,就无比蛋疼。
  • 【你刚刚说到1.8对hash函数做了优化,1.8还有别的优化吗?】
  1. 数组+链表改成了数组+链表或红黑树;
  2. 链表的插入方式从头插法改成了尾插法,简单说就是插入时,如果数组位置上已经有元素,1.7将新元素放到数组中,原始节点作为新节点的后继节点,1.8遍历链4表,将元素放置到链表的最后;
  3. 扩容的时候1.7需要对原数组中的元素进行重新hash定位在新数组的位置,1.8采用更简单的判断逻辑,位置不变或索引+旧容量大小;
  4. 在插入时,1.7先判断是否需要扩容,再插入,1.8先进行插入,插入完成再判断是否需要扩容;
  • 【那HashMap是线程安全的吗?】
    不是,在多线程环境下,1.7 会产生死循环、数据丢失、数据覆盖的问题,1.8 中会有数据覆盖的问题,以1.8为例,当A线程判断index位置为空后正好挂起,B线程开始往index位置的写入节点数据,这时A线程恢复现场,执行赋值操作,就把B线程的数据给覆盖了;还有++size这个地方也会造成多线程同时扩容等问题。
  • 【那你平常怎么解决这个线程不安全的问题?】
    Java中有HashTable、Collections.synchronizedMap、以及ConcurrentHashMap可以实现线程安全的Map。
    HashTable是直接在操作方法上加synchronized关键字,锁住整个数组,粒度比较大,Collections.synchronizedMap是使用Collections集合工具的内部类,通过传入Map封装出一个SynchronizedMap对象,内部定义了一个对象锁,方法内通过对象锁实现;ConcurrentHashMap使用分段锁,降低了锁粒度,让并发度大大提高。

线程安全的HaspMap

  1. HashTable
    采用synchronized方法加锁,使用阻塞同步,效率低。
    在每一个写操作是加上synchronized。
  2. collections.synchronizedMap(map)
    采用synchronized方法加锁,使用阻塞同步,效率低。
  3. CopyOnWriteMap
    读写分离思想的map,java不提供,需要自己编写。
  4. ConcurrentHashMap
    采用锁分段技术,减小锁的粒度,效率高。
    ConcurrentHashMap中是一次锁住一个桶。
    ConcurrentHashMap默认将hash表分为16个桶,诸如get、put、remove等常用操作才会锁住当前所需要用到的桶。16个线程指的是写线程,而读操作大部分时候都不需要用到锁。只有在size等操作需要锁住整个hash。
    在数组初始化时和节点为空时用到cas无锁机制保证线程安全,在链表和红黑树的情况下使用synchronized加锁保证线程安全。

Redis

什么是redis

Redis是一个完全开源免费的高性能的非关系型key-values数据库

使用场景

  1. 会话缓存
    最常用的一种redis的情景是会话缓存。用redis缓存会话比其他存储的优势在于:redis提供持久化。当维护一个不严格一致性的缓存时,如果用户的购物车信息全部丢失,大部分都会不高兴,随着redis这些年的改进,很容易找到怎么恰当的使用redis来缓存会话的文档。

  2. 全页缓存
    除了基本的会话 token 之外,redis还提供了和简便的 FPC 平台,回到一致性问题,即使重启redis实例,因为有磁盘的持久化,用户也不会看到页面加载速度的下降。

  3. 队列
    Redis内存存储引擎领域的一大优点提供 list 和 set 这使得redis能作为一个很好的消息队列平台来使用。Redis作为队列使用操作,就类似于本地程序语言对list的push/pop操作。(左进右出+阻塞队列)

  4. 排行榜/计数器
    Redis在内存中对数字进行递增或递减的操作实现的非常好。Set和zset也使得我们在执行这些操作的时候变的非常简单,redis只是正好提供了这两种数据结构。

  5. 发布/订阅
    发布和订阅的使用场景确实非常多。我已看见人们在社交网络连接中使用,还可以作为基于发布/订阅的脚本触发器,甚至用redis的发布和订阅来建立聊天系统。

Redis的数据类型以及使用场景

  1. String
    这个其实没啥说的,最常用的set/get操作,vlaue可以是数字,一般做一些复杂的计数功能的操作。
  2. hash
    这里value存放的是结构的对象,比较方便的就是操作其中的某个字段。在做单点登录的时候,就可以用这种数据结构存放用户的信息,以cookie作为key,设置30分钟为缓存过期时间,能很好做出类似session的效果。
  3. list
    使用list的数据结构,可以做简单的消息队列的功能,另外还有一个就是,可以利用range命令,做redis的分页功能,性能极佳,用户体验好。List可以很好的做出队列(lpush + rpop)、栈(lpush + lpop)等数据结构。
  4. set
    因为set堆放的是一堆不重复的集合,所以可以做全局去重的功能。那么为什么不用jvm自带的set呢,因为有可能我们的系统是分布式的,总不可能一个一个的去做去重吧。另外就是利用交集、并集、差集等操作,可以计算出共同喜好、全部喜好、自己独有的喜欢等功能。
  5. zset
    zset比set多出了一个权重参数score,集合中的元素都可以按score进行排序。可以做排行榜应用。

Redis的持久化机制

Redis是一个支持持久化的内存数据库,通过持久化机制把内存中的数据同步到硬盘文件来保证数据持久化。当Redis重启后通过把硬盘文件重新加载到内存,就能达到恢复数据的目的。

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

  2. AOF
    Redis会将每一个收到的写命令都通过Write函数追加到文件最后,Redis重启是会通过重新执行文件中保存的写命令来在内存中重建整个数据库的内容。当两种方式同时开时,数据恢复Redis会优先选择AOF恢复。

redis三大并发问题

  1. 缓存雪崩
    由于缓存在同一时刻出现大面积的key过期,所有原本应该访问缓存的数据都去查询数据库了,对数据库造成巨大的压力,严重还会造成数据库宕机。
    解决方法:
    • 设置随机的key过期时间,将key的过期时间分散开。
    • 给每一个缓存数据增加相应的缓存标记,记录缓存的是否失效,如果缓存标记失效,则更新数据缓存
    • 加锁
  2. 缓存穿透
    用户查询一个缓存和数据库都没有的数据,每次请求在缓存中没有,就去数据库查询,比一个黑客故意制造我们缓存中不存在的key发送大量的请求,就会导致请求直接落到数据库上。
    解决方法:
    • 最直接方法方法,不是说缓存中没有吗,那就放缓存中有,如果这个key缓存中没有,数据库也没有,那就存入缓存一个空值,设置一个较短的过期时间。
    • 布隆过滤器(broome filter),这是一个神奇的数据结构,实质上是一个bit数组,占用的空间少而且和高效更高,但是也是有缺点的,它返回的值是有概率性的,并不是那么准确。
  3. 缓存击穿透
    一个热点key在高并发的时候过期了,导致所有的请求被打到数据库,导致数据库宕机
    解决方法:
    • 既然是热点key,电商中称为“爆款”,那就设置key为永不过期呗。
    • 使用互斥锁
    • 缓存预热

Redis key过期策略

Redis采用的是定期删除+惰性删除策略

  • 为什么不用定时删除策略
    定时删除,用一个定时器来负责监视key,过期则自动删除,虽然内存及时释放,但是十分消耗CPU资源,在大并发下,CPU要将时间应用在处理请求上,而不是删除key,因此没有采用这一策略。
  • 定时删除+惰性删除时如何工作的
    定期删除,redis默认每个100ms检查,是否有过期key则删除,需要说明的是,redis不是每个100ms将所有的key检查一次,而是随机抽取进行检查(如果每隔100ms,全部key检查,redis岂不是卡死),因此,如果只采用定期删除策略,会导致key到时间没有删除。于是,惰性删除派上用场了。也就是说你获取某个key的时候,redis会检查一下,这个key是否设置了过期时间,那么是否过期了,如果过期了此时就会删除。
  • 采用了定期删除+惰性就没其他问题了吗
    不是的,如果定期删除没有删除key。然后你也没即时去请求key,也就是说惰性删除也没生效。这样,redis的内存会越来越高。那么就应该采用内存淘汰机制。

Redis的分布式锁,及应用场景,redisson

需要解决的问题:
超卖:
多个线程同一时间访问一个key,对这个key进行操作,导致的数据不一致的原子性问题。多数用户访问用一个商品,但是redis没有加锁,在这个商品库存为0的使用还可以被购买。

  • 最简单常用的一种的就使用setnx随便存入一个key来争抢锁,再给一个过期时间(防止宕机),执行完这个代码块的时候加一个finally方法删除这个key(防止中间异常)。
  • 比如设置过期时间为10秒,但如果a线程的执行时间是15秒,线程a执行还未结束,这个key就过期了,这时候当好线程b进来了,又创建了一个key,这时候线程a又执行完了,删除了这个key,刚好这时候线程 c进来了,因为线程a删掉了线程b创建的key,线程c又可以继续执行了,就会导致这个锁形同虚设。

解决方法: 加一个数据的value,删除的时候判断是否是这个value,但是不可以完美的解决。

  • Redisson

Redisson是架设在Redis基础上的一个Java驻内存数据网格(In-Memory Data Grid)。充分的利用了Redis键值数据库提供的一系列优势,基于Java实用工具包中常用接口,为使用者提供了一系列具有分布式特性的常用工具类。使得原本作为协调单机多线程并发程序的工具包获得了协调分布式多机多线程并发系统的能力,大大降低了设计和研发大规模分布式系统的难度。同时结合各富特色的分布式服务,更进一步简化了分布式环境中程序相互之间的协作。

给每个线程加锁,如果a线程的没有执行完毕,线程b就会进入阻塞,while自转,一直尝试加锁,只有线程a执行完毕释放完锁后才会进行加锁。线程被加锁成功后,再后台进行另一个分线程,每过一段时间判断当前线程的锁是否还在,如果还在,就延长过期时间,延长时间为当前过期时间的1/3。

Redis集群

所谓集群,就是通过添加服务器的数量,提供相同的服务,从而让服务达到一个稳定、高效的状态。

  1. redis Sentinal着眼于高可用,在master宕机时会自动将slave提升为master,继续提供服务。
  • 哨兵模式:
    哨兵模式是一种特殊的模式,首先Redis提供了哨兵的命令,哨兵是一个独立的进程,作为进程,它会独立运行。其原理是哨兵通过发送命令,等待Redis服务器响应,从而监控运行的多个Redis实例。
  • 这里的哨兵有两个作用
    通过发送命令,让Redis服务器返回监控其运行状态,包括主服务器和从服务器。
    当哨兵监测到master宕机,会自动将slave切换成master,然后通过发布订阅模式通知其他的从服务器,修改配置文件,让它们切换主机。
  • 然而一个哨兵进程对Redis服务器进行监控,可能会出现问题,为此,我们可以使用多个哨兵进行监控。各个哨兵之间还会进行监控,这样就形成了多哨兵模式。
  • 用文字描述一下故障切换(failover)的过程。假设主服务器宕机,哨兵1先检测到这个结果,系统并不会马上进行failover过程,仅仅是哨兵1主观的认为主服务器不可用,这个现象成为主观下线。当后面的哨兵也检测到主服务器不可用,并且数量达到一定值时,那么哨兵之间就会进行一次投票,投票的结果由一个哨兵发起,进行failover操作。切换成功后,就会通过发布订阅模式,让各个哨兵把自己监控的从服务器实现切换主机,这个过程称为客观下线。这样对于客户端而言,一切都是透明的。
  1. redis Cluster着眼于扩展性,在单个redis内存不足时,使用Cluster进行分片存储。

Rabbit MQ

什么是rabbitMQ

采用amqp高级消息队列协议的一种消息队列技术,最大的特点就是在消费并不需要确保提供方存在,实现了服务间的高度解耦。

为什么要使用rabbitMQ(有什么特点)

具有异步提速,服务解耦,削峰填谷、持久化等一系列高级功能。

说下rabbitMQ的几种工作模式

  1. simple简单模式
    消息生产者将消息放入队列,消息的消费者监听消息队列,如果队列中有消息,就消费消息,消息被拿走,自动从对中删除(隐患:消息可能没有被消费者正确处理,已经从队列中消失了,造成消息的丢失)。
    引用场景 聊天
  2. work工作模式(资源的竞争)
    消息生产者将消息放入队列中,消费可以有多个,同时监听同一个队列。消息被消费,C1和C2同时争抢当前的消息队列内容,谁先拿到消费信息(隐患,高并发情况下,默认会产生某一个消息被多个消费者共同使用,可以设置一个syncronize(和同步锁的性能不一样),
    引用场景 红包,大项目中的资源调度,任务系统不知道哪一个任务执行系统在空闲没直接将任务扔到消息队列中,空闲的系统自动争抢。
  3. publish/subscribe发布订阅(共享资源)
    X表示交换机,一种rabbitMQ内部组件。
    消息生产者将消息放入交换机,交换机发布订阅把消息发送到所有消息队列中,对应的消息队列的消费者拿到消息进行消费。
    引用场景 邮箱、群聊天、广播、广告
  4. routing路由模式
    需要将一个队列绑定到交换机上,要求该消息与一个特定的路由键完全匹配。这是一个完整的匹配。
  5. topic主题模式(路由模式中的一种)
    将路由键和某模式进行匹配,此时队列需要绑定在一个模式上,“#”匹配一个词或多个词,“*”只匹配一个词。

消息队列丢失的三种情况怎么防止

1. 生产者丢失数据

生产者将数据发送到mq的时候,消息可能以为网络等问题在传输过程中丢失。
解决方法

  • 开启rabbitMQ事务
    使用用 RabbitMQ 提供的事务功能,就是生产者发送数据之前开启 RabbitMQ 事务channel.txSelect,然后发送消 息,如果消息没有成功被 RabbitMQ 接收到,那么生产者会收到异常报错,此时就可以回滚事务channel.txRollback,然后重试发送消息;如果收到了消息,那么可以提交事务channel.txCommit
  • 开启confirm模式
    在生产者端设置开启confirm模式之后,你每次写的消息都会分配一个唯一的id,然后如果写入了rabbitMQ中,rabbitMQ会回传一个ack消息,告诉你说这个消息ok了。如果rabbitMQ没能处理这个消息,会回调你的一个nack接口,告诉你这个消息接受失败,你可以重试。而且可以结合这个机制自己在内存中维护每个消息id的状态,如果超过一定时间还没接受到这个消息的回调,那么你可以重发。
  • 事务机制和confirm机制最大的不同在于:
    • 事务机制是同步的,提交一个事务之后会阻塞在那儿;基本上吞吐量会下来,消耗性能。
    • Confirm机制是异步的,你发送消息后就可以发送下一个消息,然后消息被rabbitMQ接收之后会异步调用你的接口,通知你这个消息接收到了。所有一般在生产者这块避免数据丢失,都是采用confirm机制的。

2. rabbitMQ弄丢了数据

  • 如果是rabbitMQ自己丢失了数据,这个你必须开始rabbitMQ的持久化,就是消息写入之后会持久化到磁盘,哪怕是rabbitMQ自己挂了,恢复了会自动读取之前存储的数据,一般数据不会丢。除非极其罕见的是,rabbitMQ还没持久化,自己就挂了,可能导致少量数据丢失,但是这个概率较小。
    设置持久化的两个步骤
    • 创建queue的时候将其设置为持久化,保证rabbitMQ持久化queue的元数据,但是它是不会持久化queue的数据的。
    • delivery Mode设置为2,。即将消息设置为持久化,此时rabbitMQ就会将消息持久化磁盘上去。
      必须要同时设置这两个持久化才行,rabbitMQ哪怕是挂了,再次重启,也会从磁盘上重启恢复queue,恢复这个queue里的数据。
  • 持久化机制和生产者的confirm机制配合
    • 开启rabitMQ的持久化机制,也有一种可能,就是这个消息写到了rabbitMQ中,但是还没来得及持久化到磁盘上,结果不巧,此时rabbitMQ挂了,就会导致内存中的一点点数据丢失。所以,持久化可以很生产者那边的confirm机制配合起来,只有消息被持久化磁盘之后,才会通知生产者ack了,所以哪怕是在持久化到磁盘之前,rabbitMQ挂了,数据丢了,生产者收不到ack,需要自己重发。

3. 消费端丢失了数据

  • RabbitMQ如果丢失了数据,主要是因为你消费的时候,刚消费到,还没处理,结果消费进程挂了。导致rabbitMQ认为已经消费了数据。
    解决方案
    • 关闭rabbitMQ的自动ack,可以通过一个api来调用就行。确保每次数据处理完后手动ack。这样的话,如果你还没处理完,不就没有ack了?那么rabbitMQ就认为你还没处理完,这个时候rabitMQ会把这个消费分配给别的consumer去处理,消息是不会丢的。

rabbitMQ的适用场景

商品处理订单、聊天、广告

详细的说明下削峰、解耦

  • 削峰:
    当有大量请求同时访问我们的系统,系统有可能会扛不住,如果这个时候加一个mq,那么所有的请求会被平均分担进系统。
  • 解耦:
    比如,本来商品系统直接调用订单系统,如果这时候订单系统进行修改,那么可能订单系统也需要跟着修改,耦合性极高,这时候要是在中间添加一个mq,那么就可以做到两个服务之间耦合降低。

rabbitMQ集群

  • 镜像集群模式
    • 你创建的 queue,无论元数据还是 queue 里的消息都会存在于多个实例上,然后每次你写消息到 queue 的时候,都会自动把消息到多个实例的 queue 里进行消息同步。
    • 好处在于,你任何一个机器宕机了,没事儿,别的机器都可以用。坏处在于,第一,这个性能开销也太大了吧,消息同步所有机器,导致网络带宽压力和消耗很重!第二,这么玩儿,就没有扩展性可言了,如果某个 queue 负载很重,你加机器,新增的机器也包含了这个 queue的所有数据,并没有办法线性扩展你的 queue

mysql

InnoDB与MyISAM的区别

  1. InnoDB支持事务,MyISAM不支持,对于InnoDB每一条SQL语句都默认封装成事务,自动提交。
  2. InnoDB支持外键,而MyISAM不支持。对一个包含外键的InnoDB表转换成MyISAM会失败。
  3. InnoDB是聚集索引,数据是和索引绑在一起的,必须要有主键,通过主键索引效率很高。但是辅助索引需要两次查询,先查询到主键,然后通过主键查询到全部数据,因此,主键不应该过大。MyISAM是非聚集索引,数据文件是分离的,索引保存的是数据文件的指针。
  4. InnoDB不保存表的具体行数,执行count(*)时需要全表扫描。而MyISAM用一个变量保存了整个表的行数,执行上述语句时只需要读出该变量就可。
  5. InnoDB不支持全文索引,MyISAM支持全文索引,查询效率上MyISAM要高。

索引失效的情况

  1. like搜索,全模糊和左模糊索引文件具有B-tree的最左前缀匹配原则,如果左边的值未确定,那么无法使用索引
  2. or语句前后没有同时使用索引。当or左右查询字段只有一个索引,该索引失效,只有当or左右查询字段均为索引时,才会生效。
  3. 联合索引,没有使用第一列进行索引,索引失效,最左原则。
  4. 数据类型出现隐式转换。如varchar不加单引号的话可能会自动转换为int类型,使索引无效。
  5. 在索引列上使用is null或者is not null操作。索引是不索引空值的,所以这样的操作不能使用索引,可以用其他的方法处理。
  6. 左连接和右连接查询关联的字段编码格式不一样。
    7)查询的列上有运算或者有函数的

expain执行计划

Explain是什么
在select语句前加上expain,mysql将会解释它是如何处理select的,提供有关表查询的性能数据。

column含义
id查询序号
selcect_type查询类型
table表名
partitions匹配的分区
type索引类型
prossible_keys可能会选择的索引
key实际选择的索引
key_len索引的长度
ref与索引作比较的列
rows要检索的行数
fitered查询条件过滤的行数的百分比
Extra额外信息
  • 其中我们在做优化的时候,最常用到的就是 type和rows字段
    Type可以显示查询语句的索引类型,有:
    System → const → eq_ref → ref → range → index → ALL。
  • 根据阿里java开发手册规定:
    SQL性能优化的目标:至少达到renge级别,要求是ref级别,最好是consts级别。
  • Rows可以估算需要扫描的行数,但不是精确值,这个值可以很直观的下你是sql效率的好坏,原则上rows越少越好。

什么是mycat

  • Mycat是一个开源的分布式数据库系统,是一个实现了mysql协议的服务器。在后端,可以用mysql原生协议与多个mysql服务器通信,也可以用JDBC协议与大多数主流数据库服务器通信。其核心功能是分库分表、读写分离,也就是将一个大表分割为N个小表储存在后端mysql服务器里或者其他数据库里。
  • 而Mycat并没有属于自己的独有数据库引擎,所有严格意义上说并不能算是一个完整的数据库系统,只能说是一个在应用和数据库之间起桥梁作用的中间件。引入Mycat中间件能很好地对程序和数据库进行解耦。Mycat中间件的原理是对数据进行分片处理,从原有的一个库,被切分为多个分片数据库,所有的分片数据库集群构成完成的数据库存储。

主从复制

  • 数据可从一个mysql数据库服务器主节点复制一个或多个从节点。Mysql默认采用异步复制方式,这样从节点不用一直访问主服务器来更新自己的数据,数据的更新可以在远程连接上进行,从节点可以复制主数据库或者特定的数据库,或者特定的表。

读写分离

  • 在主从复制的基础上,利用主从数据库来实现读写分离,从而分担主数据库的压力。在多个服务器上部署mysql,将其中一台认为主数据库,而其他为从数据库,实现主从同步。其中主数据库负责主动写的操作,而从数据库则只负责主动读的操作(slave从数据库仍然会被动的进行写操作,为了保持数据一致性),这样就可以很大程度上的避免数据丢失的问题,同时也可减少数据库的连接,减轻主数据库的负载。

分库

  1. 垂直拆分:
    这个特别好理解,就拿电商项目举例子,按项目模块划分库,分成用户库、商品库、订单库、优惠活动库等等。
  2. 水平拆分:
    根据业务,通过某种公式或者规则平均的拆分库。

分表

  1. 垂直拆分:
    就比如有一个用户表,用户所有的信息都在里面,会导致一张表过于的复杂,可读性也很低,这时候我们就可以将用户重要的信息和一般信息分开两张表或几张表存储。用户的密码、电话、号码、昵称、创建时间、登录次数为一张,个人信息的为另一张。主键可以是用一个。
  2. 水平拆分:
    根据业务,通过某种公式或者规则平均的拆分表。

分库分表的优缺点

  1. 垂直分库分表
    • 优点
      • 拆分后业务清晰。
      • 数据库维护简单、按业务不同放到不同机器。
    • 缺点
      • 导致单表的数据量大,写读压力大。
      • 受某种业务来决定、或者被限制。也就是说一个业务往往会影响到数据库的瓶颈。比如双11的时候,用户系统也就登录的时候访问量大一点,商品系统肯定已经缓存预热过了,就是订单数据库读写的量会特别大。
        3)部分的业务无法进行关联,
  2. 水平分库分表
    • 优点
      • 单库(表)的数据保持一定的量,有助于性能提高。
      • 提高了系统的稳定性和负载能力。
      • 拆分的表的结构相同、程序改造较少。
    • 缺点
      • 数据的扩容很有难度,如果数据量大,一个库(表)容纳不下,扩容对程序的规则维护量大。
      • 分片事务的一致性的问题部分业务无法关联,join、where语句需要程序去完成。如:订单到底是按照userid还是orderid分割,用户角度是userid,那么管理员就希望orderid了。

什么是B-Tree

这里的B是Balance,平衡的意思,b树是一个多路自平衡的查找树,它类似普通的平衡二叉树,不同的一点是B树允许每个节点有更多的子节点。

特点
所有的键值都分布在整棵树中。
任何一个关键字出现且出现在一个结点中。
搜索有可能在非叶子节点结束,最好的情况O(1)就能找到数据。
在关键字全集内做一次查找,性能逼近二分查找
  • 二叉树,平衡二叉树,红黑树:

    • 二叉树(每个节点最多有2个子节点,小于放左边,大于放右边,不平衡,在极端有序的情况下会形成单项链表,查找效率低)。
    • AVL(平衡二叉树)(每个节点最多有2个子节点,小于放左边,大于放右边,通过旋转保持树平衡)。
    • 红黑树(根节点默认是黑,通过旋转保证数的平衡)
  • 红黑树与平衡二叉树的比较:

    • 平衡二叉树是严格的平衡树,因此在增加或者删除节点的时候,根据不同情况,旋转的次数比红黑树要多;
    • 红黑树是非严格的平衡树,增删节点时候旋转的次数降低。
      所以应用中,搜索的次数远远大于插入和删除,那么选择平衡二叉树,如果搜索,插入删除次数差不多,应选择红黑树
  • 加密方式

    • MD5(不可逆,一般密码加密)
    • Base64(可以加密,可以对所有的流加密,一般图片传输,字符串加密用)
    • DES(对称加密,加密和解密必须使用同一个密钥)
    • AES(对称加密,加密和解密必须使用同一个密钥)
    • RSA(非对称加密,有一对密钥,公钥加密私钥解密,支付宝对接用的是RSA算法)

事务

事务是作为单个逻辑工作单元执行的一系列操作,这些操作作为一个整体一起向系统提交,要么都执行,要么都不执行。

事务的四大特性
原子性事务是一个完整的操作。事务的各步操作是不可分割的。要么全部执行成功,要么全部失败回滚
隔离性对数据进行修改的所有并发事务是彼此隔离的,这表明事务必须是独立的,它不应以任何方式依赖或影响其他事务
一致性对数据进行修改的所有并发事务是彼此隔离的,这表明事务必须是独立的,它不应以任何方式依赖或影响其他事
持久性事务被成功提交后,对数据库的操作是永久的
并发事务带来的问题
脏读当一个事务正在访问数据并且对数据进行了修改,而这种修改还没有提交到数据库中,这时另外一个事务也访问到这个数据,然后使用了这个‘脏数据’
丢失数据指在一个事务读取一个数据时,另一个事务也访问了该数据,那么在第一个事务中修改了这个数据后,第二个事务也修改了这个数据,这样第一个事务修改的结果就会被丢失了
不可重复读指在一个事务内多次读同一数据。在这个事务还没结束时,另一个事务也访问该数据。那么,在第一个事务中的两次读数据之间,由于第二个事务的修改导致第一个事务两次读取的数据可能不太一样
幻读幻读和不可重复读类似。它发生在一个事务读取了几行数据,接着另一个事务插入或者删除了一些数据。在随后的查询中,第一个事务就会发现多了一些原本不存在或者是少了某些数据,就好像发生了幻觉一样的
四种隔离几级别
读未提交最低的隔离级别,允许读取尚未提交的数据变更,可能会导致脏读、幻读、不可重复读
读已提交允许读取并发事务已经提交的数据,可以阻止脏读,但是幻读和不可重复读仍可能发生
可重复读(默认)对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改,可以阻止脏读和不可重复读,但幻读仍有可能发生
可串行化最高的隔离级别,所有的事务依次逐个执行,这样事务之间不可能产生干扰,也就是说,可以防止脏读、不可重复读和幻读

MVCC (Multi-Version Concurrent Control)

什么是MVCC

使用版本来控制并发情况下的数据问题,在B事务修改数据且事务未提交时,当A事务需要读物数据的时候,此时会读取到B事务修改操作前的数据的副本数据,但是如果A事务需要修改数据就需要等待事务B提交事务。
MVCC的实现主要是为了提高数据库并发性能,用更好的方式去处理读写冲突,做到即使有读写冲突时,也能做到不加锁,非阻塞并发读写。

当前读

  • 像select lock in share mode(共享锁),select for update()、update、insert、delete(排它锁),这些操作都是一种当前读。它读取的是记录的最新版本,读取时还要保证其他并发事务不能修改当前记录,会对读取的记录进行加锁。

快照读

  • 像不加锁的select操作就是快照读,即不加锁的非阻塞读;快照读的前提是隔离级别不是串行化,串行化下的快照读会退化成当前读;之所以出现快照读,是基于提高并发性能得到考虑,快照读的实现是基于多版本并发控制的,也就是MVCC,可以认为MVCC是行锁的一个变种,但它在很多情况下,避免了开销;既然是基于多版本,即快照读可能读到的并不是数据的最新版本,而有可能是之前的历史版本。

实现原理

  • MVCC的实现原理主要是依赖于记录中的三个隐式字段、undo日志、Read View来实现的。
    每行记录除了我们自定义的字段外,还有数据隐式定义的:

    • DB_TRX_ID
      事务ID:记录创建这条记录/最后一次修改该记录的事务ID。
    • DB_ROLL_POINTER
      回滚指针:指向这一条记录的上一个版本。
    • DB_ROW_ID
      隐含的自增ID,如果数据表没有主键,InnoDB会自动以DB_ROW_ID产生一个聚集索引。
      其实还有一个flag,表示当前记录时是否被删除
  • Undo日志主要分为两种

    • insert undo log
      代表事务在insert新记录时产生的undo log,不仅在事务回滚时需要,在快照读时也需要,并且在事务提交后可以被立即丢弃。
    • update undo log
      事务在进行update和delete时产生的undo log;不仅在事务回滚时需要,在快照读时也需要;所以不能随便删除,只有在快速读或者事务回滚不涉及该事务时,对应的日志才会被线程同一清除。
  • Read View

    • 是事务进行快照读操作的时候产生的读视图,当该事务执行的快照读的那一刻,会生成数据库系统当前的一个快照。
    • 在不同的隔离级别下生成Read View的时机也是不一样的;在读已提交中每次查询都会生成一个实时的Read View,做到保证每次提交后的数据是处于当前的可见状态。而在可重复读中,在当前事务第一次查询时生成当前的Read View,并且当前的Read View会一直沿用到当前事务提交,以此来保证可重复读。

  • 乐观锁
    乐观锁认为一个用户读数据的时候,别人不会去写自己所读的数据。
    大多是基于数据版本( Version )记录机制实现。何谓数据版本?即为数据增加一个版本标识,在基于数据库表的版本解决方案中,一般是通过为数据库表增加一个 “version” 字段来实现。读取出数据时,将此版本号一同读出,之后更新时,对此版本号加一。此时,将提交数据的版本数据与数据库表对应记录的当前版本信息进行比对,如 果提交的数据版本号大于数据库表当前版本号,则予以更新,否则认为是过期数据。

  • 悲观锁
    悲观锁就是在读取数据的时候,为了不让别人修改自己读取的数据,就会先对自己读取的数据加锁,只有自己把数据读完了,才允许别人修改那部分数据,只有等自己整个事务提交了,才释放自己加上的锁。

    • 排它锁:
      排它锁又称为写锁,若事务T对数据对象A加上X锁,则只允许T读取和修改A,其它任何事务都不能再对A加任何类型的锁,直到T释放A上的锁。它防止任何其它事务获取资源上的锁,直到在事务的末尾将资源上的原始锁释放为止。

      • 表级锁
        开销小,加锁快,不会出现死锁,锁定粒度大,发生锁冲突的概率最高,并发度最低。
      • 行级锁
        开销大,加锁慢,会出现死锁,锁定粒度最小,发生锁冲突的概率最低,并发量也最高。
    • 共享锁
      又称为读锁,可以查看但无法修改和删除的一种数据锁。

  • 时间戳
    时间戳就是在数据库表中单独加一列时间戳,比如“TimeStamp”, 每次读出来的时候,把该字段也读出来,当写回去的时候,把该字段加1,提交之前 ,跟数据库的该字段比较一次,如果比数据库的值大的话,就允许保存,否则不允许保存,这种处理方法虽然不使用数据库系统提供的锁机制,但是这种方法可以大大提高数据库处理的并发量.

  • 页面锁
    开销和加锁时间介于表锁和行锁之间,会出现死锁,锁定粒度介于表锁和行锁之间,并发度一般。

多线程

Java内存模型(JMM:Java Memory Model)

  • 本身是一种抽象的概念并不真实存在,它描述的是一组规则和规范,通过这组规范定义了程序中各个变量(包括实例字段、静态字段和构成数组对象的元素)的访问方式。
  • 由于JVM运行程序的实体是线程,而每个线程创建时JVM都会为其创建一个工作内存(栈空间),工作内存是每个线程的私有数据区域,而java内存模型中规定所有变量都存储在主内存中,主内存是共享内存区域,所有线程都可以访问,但线程对变量的操作必须在工作内存中进行,首先要将变量从主内存拷贝到线程自己的工作空间然后对变量进行操作,操作完成后再将变量写回主内存,不能直接操作主内存的变量,各个线程的工作内存中存储着主内存中的变量副本,因此不同的线程间无法访问对方的工作内存,线程间的通信必须通过主内存来完成。

进程和线程

  • 进程是资源分配的最小单位,线程是CPU调度的最小单位,进程是线程集合,线程是进程的一条执行路径。
    进程=火车,线程=车厢

Volatile

  • 保证线程的可见性
    • 使用volatile关键词修饰的变量,在多线程情况下,会开启总线嗅探机制,保证变量在各个线程单独的内存空间之间的可见性。
    • 就好比线程A和线程B同时对Flag进行操作,线程B对Flag赋值后会写会主内存,线程A会在总线进行监视,检测到Flag有了变化,会将自己工作内存中的Flag副本作废,再一次读取的时候会再去主内存读取。
  • 不保证原子性。
    • 解决方法:
    1. 加上Synchronize修饰
    2. 使用JUC底下的Atomic原子类。
  • 禁止指令重排。
    • 在CPU对代码进行编译时,有可能会对没有依赖关系的代码进行重新排序,就好像考试时先做会做的,有时候我们不需要指令重排。加上volatile后会使用内存屏障保证代码有序执行。

内存屏障

类型说明
LoadLoad确保Load数据的装载先于Load及所有后续装载指令的装载
StoreStore确保Store数据对其他处理器可见先于Store及所有后续存储指令的储存
LoadStore确保Load数据装载先于Store及所有后续存储指令刷新到内存
StoreLoad确保Store数据对其他处理器变得可见先于Load及所有后续装载指令的装载。

Atomic原子类

  • Atomic是指一个操作是不可中断的。即使是在多个线程一起执行的时候,一个操作一旦开始,就不会被其他线程干扰

CAS(乐观锁)

  • CAS包含3个参数,CAS(V,E,N),E表示要更新的值,E表示预期值,N表示新值。
  • 仅当V值等于E值时,才会将V的值设为N,如果V值和E值不同,则说明已经有其他线程做了更新,则当前线程什么都不做,最后,CAS返回当前V的真实值。CAS操作是抱着乐观的态度进行的,他总是认为自己可以完成操作。当多个线程同时使用CAS操作一个变量时,只有一个会胜出,并成功更新,其余均为失败,失败的线程不会被挂起,仅是被告知失败,并且再次尝试,当然也允许失败的线程放弃操作。基于这样的原理,CAS操作即使没有锁,也可以发现其他线程对当前线程的干扰,并进行恰当的处理。

Syncronized

  • Synchronized是java1.5后引入的新的同步锁。非公平锁
    被Synchronized修饰的代码块,在编译时会在前后形成monitorenter和monitorexit这两个字节码指令。在执行monitorenter指令时,首先要尝试获取对象锁,如果对象没被锁定,或者当前线程已经拥有了对象锁,把锁的计算器加1,相应的,在执行monitorexit指令时会将计算器减1,当计算器为0时,锁就被释放了。如果获取对象锁失败,那当前线程就要阻塞,直到对象锁另一个线程释放。
作用域
代码块作用于调用的对象
方法 作用域调用的对象
静态方法Class
整个对象

Lock锁

  • 很Synchronized类似,也是一个同步锁,但是比Synchronized更灵活,更加的轻,粒度更小。就拿ReentrantLock这个类来说,它提供了一些高级功能:
  1. 等待可中断,持有锁的线程长期不释放锁的时候,正在等待的线程可以选择放弃等待。
  2. 公平锁,多个线程等待同一个锁,必须按照申请的时间顺序来获得锁。
    ReentrantLock的底层默认是非公平锁,在创建的时候传递参数ture可以设置为公平锁。
  3. 一个ReentrantLock对象可以同时绑定多个对象。

线程8锁实现

普通同步代码和静态同步代码的对比。
观看 【狂神说Java】:《JUC并发编程最新版通俗易懂》

Synchronized和lock的区别

  • 这两种方式最大区别就是对于Synchronized来说,它是java语言的关键字,是原生语法层面的互斥,需要jvm实现。
  • 而ReentrantLock它是JDK 1.5之后提供的API层面的互斥锁,需要lock()和unlock()方法配合try/finally语句块来完成。

Start和run的区别

  • start:用start方法来启动线程,真实实现了多线程运行,这是无需等待run方法体代码执行完毕而直接执行下面的代码。通过调用Thread类的start方法来启动一个线程,这是此线程处于就绪状态,并没有运行,一旦得到cpu时间片,就开始执行run方法,这里方法run称为线程体,它包含了要执行的这个线程的内容,run方法运行结束,次线程随机终止。
  • run:run方法只是类的一个普通方法而已,如果直接调用run方法,程序中依然只有主线程一个线程,其线程执行路径还是只有一条,还是要顺序执行,还是等待run方法体执行完毕后才可继续执行下面的代码,这样就没有达到多线程的目的。
  • 总结:调用start方法可以启动线程,而run方法只是Thread的一个普通方法调用,还是在主线程里执行。start方法启动线程将自动调用run方法,这是jvm的内存机制规定的。
    Java无法开启线程,start方法在底层调用的是native方法。

Wait和sleep的区别

  1. 对于sleep方法,我们首先要知道该方法是属于Thread类中的。而wait方法是属于Object类的。
  2. sleep方法导致了程序暂定指定的时间,让出cpu给其他线程,但是他的监视状态依然保持着,当指定的时间到了又会自动恢复运行状态。
  3. 在调用sleep方法的过程中,线程不会释放对象锁。
  4. 而当调用wait方法的时候,线程会放弃对象锁,进入等待此对象的等待锁定池,只有针对此对象调用notify方法后本线程才进入对象锁定池准备获取对象锁进入运行状态。

Notify和NotifyAll的区别

  1. notify可能会导致死锁,而notifyAll则不会。
  2. 任何时候只有一个线程可以获得锁,也就是说只有一个线程可以运行synchronized中的方法,使用notifyAll,可以唤醒所有处于wait状态的线程
    ,使其重新进 入锁的争夺队列中,而notify只能唤醒一个。
  3. wait应配合while循环使用,不应使用if,务必在wait调用前后都检查条件,如果不满足,必须调用notify唤醒另外的线程来处理,自己继续wait直到条件满足再往下执行。
  4. nititf是对notifyAll的一个优化,单它有精确的应用场景,并且要求正确使用。不然可能导致死锁。正确的场景应该是waitSet中等待的是相同的条件,唤醒任一个都能正确处理接下来的事项,如果唤醒的线程无法正确处理,务必确保继续notify下一个线程,并且自身需要重新回答waitSet中。

线程的生命周期

初始→就绪→运行→阻塞→终止

线程的状态

新建→运行→阻塞→等待→超时等待→终止

实现多线程的三种方式

  1. 继承Thread类。
  2. 实现Runnable接口,没有返回值,效率低于callable。
  3. 实现Callable接口,有缓存,有返回值,可能会阻塞。
    使用Callable接口

Thread类

  • Thead类本质上是实现了Runnable接口的一个实例,代表一个线程的实例。启动线程的唯一方法就是通过Thread类的start实例方法。Start方法是一个native方法,它将启动一个新线程,并执行run方法。

线程池

线程池是指在初始化一个多线程应用程序过程中创建一个线程集合,然后在需要执行新的任务时重用这些线程而不是新建一个线程。线程池中的每一个线程都有被分配一个任务,一旦任务完成了,线程回到池子中等待下一次分配任务。共享充电宝

创建线程池的四种方法

Java通过Executor提供的四种线程池:

  1. newCachedThreadPool创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,如无可回收,则创建新线程。兜兜有两个小组,如果有4个任务,就会再扩招两个小组,干完了再辞退他们。

  2. newFixedThradPool创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。兜兜有5个小组,如果有六个任务,那就会有一个小组多干一个任务。

  3. newScheduledThreadPool创建一个定长线程池,支持定时及周期性任务执行。(了解)

  4. newSingleThreadExecutor创建一个单线程的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序执行。全部的活全部兜兜小组一个小组没日没夜的干。

阿里JAVA开发手册中声明

线程池不允许使用Executors创建,而是通过底层的ThreadPoolExecutor的方式创建。

Executors返回的线程池对象的弊端

FixedThreadPool和SingleThreadPool、CachethreadPool:允许的请求队列长度为Integer.MAX_VALUE,可能会堆积大量的请求,从而导致OOM。

创建线程池的七大参数

corePoolSize核心线程数,一直存活,即使线程数小于核心线程数且有空闲,线程也会创建新的线程象
maximumPoolSiz最大线程数,当线程数大于核心线程数并且任务队列已经满了的时候,线程池会创建新的线程,当线程数大于最大线程数并且任务队列已经满的时候,会抛出异常
KeepAliveTime线程空闲时间,当线程的空闲时间达到keepaliveTime时,线程会退出,直到线程数等于核心线程数,可以设置参数allowCoreThreadTimeout=true,则会直到线程数为0
TimeUnit unit超时时间单位
Blockingqueue workQueue阻塞队列,任务队列的容
ThreadFactory threadFactory线程池工厂,默认的就行
RejectedExecutionHandler handler拒绝策略
线程池的四大拒绝策略
AbortPolicy()线程池满了,如果还有线程想加入,不处理这个请求,抛出异常。(默认)
CallerRunsPolicy()哪来的回哪去
DiscardPolicy()队列满了,丢掉任务,不会抛出异
DiscardOldestPolicy()列满了,尝试去和和最早的竞争,不会抛出异常

  • 死锁:
    所谓死锁,是指多个进程在运行过程中因争夺资源而造成的一种僵局,当进程处于这种僵持状态时,若无外力作用,它们都将无法再向前推进。
    产生死锁的必要条件:

    1. 互斥条件:进程要求对所分配的资源进行排它性控制,即在一段时间内某资源仅为一进程所占用。
    2. 请求和保持条件:当进程因请求资源而阻塞时,对已获得的资源保持不放。
    3. 不剥夺条件:进程已获得的资源在未使用完之前,不能剥夺,只能在使用完时由自己释放。
    4. 环路等待条件:在发生死锁时,必然存在一个进程–资源的环形链。

    预防死锁
    1. 通过协议来预防或避免死锁,确保系统不会进入死锁状态。
    2. 可以允许系统进入死锁状态,然后检测它,并加以恢复。
    3. 可以忽视这个问题,认为死锁不可能在系统内发生。

  • 自旋锁:
    自旋锁是采用让当前线程不停地在循环体内执行实现的,当循环的条件被其他线程改变时才能进入临界区

public class SpinLock {
	private AtomicReference<Thread> sign = new AtomicReference<>();
	public void lock() {
		Thread current = Thread.currentThread();
		while (!sign.compareAndSet(null, current)) {
		}
	};
	public void unlock() {
		Thread current = Thread.currentThread();
		sign.compareAndSet(current, null);
	};
}
  • 互斥锁
    一次最多只能有一个线程持有的锁,在jdk1.5之前,我们通常使用synchronized机制控制多个线程对共享资源的操作,之后又引入了lock接口以及实现类ReentrantLock。

  • 可重入锁
    也叫递归锁,指的是同一线程外层函数获得锁之后,内层递归函数仍然有获取该锁的代码。(Synchronized、ReentrantLock)

  • 信号量
    有时被称为信号灯,是在多线程环境下使用的一种设施,它负责协调各个线程,以保证它们能够正确、合理的使用公共资源。

锁升级

  • 锁有四个状态
    • 无锁、偏向锁、轻量级锁、重量级锁
    • 不可降级
      当一个线程访问同步代码块时,升级成偏向锁;有锁竞争时,升级为轻量级锁。自旋10次失败(锁膨胀)升级为重量级锁

项目中使用多线程

  1. 如一张表与其他的表有关联的,当修改了这张表的数据,需要同步到其他表中,如品牌表、商品表
  2. 秒杀

spring全家桶

Spring

什么是Spring

Spring是一个全栈的轻量级开源应用框架,宗旨在降低应用程序开发的复杂度。它具有分层体系结构,允许用户选择组件,同时还为J2EE应用程序开发提供一个有凝聚力的框架

Spring Framework有哪些功能

  • 轻量级:spring在代码量和透明度都很轻便。
  • IOC控制反转
  • AOP面向切面编程可以将应用业务逻辑和系统服务分离,以实现高聚合。
  • 容器:spring负责创建和管理对象(bean)的生命周期和配置。
  • 事务管理:提供了用于事务管理的通用抽象类。Spring的事务支持也可用于容器较少的环境。
  • JSBC异常:spring的JDBC抽象层提供了一个异常层次结构,简化了错误处理策略。

Spring Frameowrok的模块

  • Spring核心容器:该层基本上就是springFramework的核心。

    • Spring Core
    • Spring Bean
    • Spring Expression Language
    • Spring Context
  • 数据访问/集成:该层提供与数据库交互的支持

    • JDBC
    • ORM
    • OXM
    • JMS
    • Transaction
  • Web:该层提供了创建web应用程序的支持。

    • Web
    • Web Servlet
    • Web Socket
    • Web Portlet
  • AOP:支持面向切面编程。

  • Instrumentation:该层为检测和类加载器实现提供支持。

  • Test:为使用Junit和TestNG进行测试提供支持。

  • Messaging:为stomp提供支持,它还支持注解编程模型,该模型用于WebSocket客户端路由和处理STOMP消息。

  • Aspects:为与AspectJ的继承提供支持。

Spring IOC 容器

  • Spring框架的核心是spring容器。容器创建对象,将它们装配在一起,配置它们并管理它们的完整生命周期。Spring容器使用依赖注入管理组成应用程序的组件。容器通过读取提供的配置元数据来接收对象进行实例化,配置和组装的指令。该元数据可以通过xml,java注解或者java代码提供。
    自己找女朋友和婚介公司的案例

依赖注入

  • 创建注入中,不必创建对象,但必须描述如何创建他们。也不是直接在代码中将组件和服务连接在一起,而是描述配置文件中哪些组件需要哪些服务。由ioc容器将它们装配在一起。
  • 三种依赖注入方式:
    • 构造函数注入
    • setter注入
    • 接口注入

beanFactory和ApplicationContext的区别

BeanFactoryApplicationContext
使用懒加载使用即时加载
使用语法显式提供资源对自己创建和管理资源对象
不支持国际化支持国际化
不支持基于依赖的注解支持基于依赖的注解

什么是Spring Bean

  • 它们是构成用户应用程序主干的对象。Bean是SpringIOC容器管理。它们由springIOC容器实例化,配置,装配和管理。Bean是基于用户提供给容器的配置元数据创建

Spring Bean的作用域

singletonbean在每个spring ioc容器中只有一个实例
prototype一个bean的定义可以有多个实例
request每次http请求都会创建一个bean
session在每一个Http Session中,一个bean定义对应一个实例
global-session在一个全局的Http Session中,一个bean定义对应一个实例。 缺省的spring bean的作用域的是singleton request、session和goble-session都只在基于web的spring ApplicationContext情形下有效

什么是AOP

  • 任何一个系统都是由不用组件组成的,每个组件负责一块特定的功能,当然会存在很多组件跟业务无关的,例如日志、事务、权限等等核心服务组件,这些核心服务组件经常需要融入到具体的业务逻辑上去,如果我们为每一个具体业务都加上这样的代码,很明显代码冗余太多,因此我们需要将这些公共的代码逻辑抽象出来变成一个切面,然后注入到目标对象(具体业务)中去,AOP正是基于这样的一个思路实现的,通过动态代理的方式,将需要注入的对象进行代理,在进行调用的时候,将公共的逻辑直接添加进去,而不需要修改原有业务的逻辑代码,只需要在原来的业务逻辑基础上做一些增强功能即可

AOP代理模式

  1. 静态代理:

    • 张三去租房,但他找不到房东,只能去找中介,中介再帮他找房东,而这个中介就起到了代理的作用。
    • 张三是调用者,中介就是代理类,房东就是目标类真正需要调用的类,这时代理类就可以在中间做点手脚,起到增强方法的作用。
    • 但有个问题就是如果目标类有很多方法,代理类也应该有这么多方法,这时代理类和目标类应该要有一种约定,所以代理类和目标类都应该实现同一个接口。
    • 缺点耦合度太高:因为代理对象需要与目标对象实现一样的接口,所以会有很多代理类,类太多.同时,一旦接口增加方法,目标对象与代理对象都要维护。
  2. 解决办法动态代理:

    • 其实动态代理和静态代理的思想是不变的,动态代理和静态代理的区别就是,动态代理不用我们去手编写代理类,在运行时,动态的在内存中生产代理类。
      Proxy cglib

AOP应用场景-事务管理

  • 对数据库进行写操作或者连表操作的时候,为了保证数据的一致性和正确性吗,我们需要添加事务管理机制进行管理。当对数据库的数据进行操作失败时,事务管理可以很好保证所有的数据回滚到原来的数据,如果成功,则保证所有需要更新的数据持久化。

  • 编程式事务:

    • 直接在业务代码加上处理事务的代码。
  • 声明式事务:

    • 其本质是对方法前后进行拦截,然后在目标方法开始之前创建或者加入一个事务,在执行完目标方法之后根据执行情况提交或者回滚事务。声明式事务最大的优点就是不需要通过编程的方式管理事务,这样就不需要在业务逻辑代码中掺杂事务管理的代码,只需要在配置文件中做相关的事务规则声明或通过@Transactional注解,便可以将事务规则应用到业务逻辑中。
    • 声明式事务明显要优于编程式事务,这正是spring倡导的非侵入式的开发方式。声明式事务管理使使业务代码不受污染,一个普通的pojo对象,只要加上注解就可以获得完全的事务支持。和编程式事务相比,声明式事务唯一不足的地方就是后者的最细粒度只能作用到方法级别,无法做到像编程式事务可以做到代码块级别。

Spring事务的传播特性

  1. required(默认) 必需的
    如果存在一个事务,则支持当前事务。如果没有事务则开启一个新的事务。被设置成这个级别时,会为每一个被调用的方法创建一个逻辑事务域。如果前面的方法已经创建了事务,那么后面的方法支持当前的事务,如果当前没有事务会重新建立事务。
  2. mandatory 强制的
    支持当前事务,如果当前没有事务,就抛出异常。
  3. never
    以非事务方式执行,如果当前存在事务,则抛出异常。
  4. not_support 支持
    以非事务方式执行,如果当前存在事务,就把当前事务挂起。
    5、requires_new
    新建当前事务,如果当前没有事务,就以非事务方式执行。
  5. support
    支持当前事务,如果没有当前事务,就以非事务方式执行。
  6. nested /'nestɪd/ 嵌套
    支持当前事务,新增savepoint点,与当前事务同步提交或回滚。嵌套事务一个非常重要的概念就是内层事务依赖于外层事务。外层事务失败时,会回滚内层事务所做的动作。而内层事务操作失败并不会引起外层事务的回滚。

Bean的生命周期

  • 构造器或者工厂
    方法创建Bean实例。
  • 为Bean设置属性和对其他Bean的引用
  • IOC依赖注入属性,setBeanName设置ID,以及ApplicationContextAware的实现完成对其他Bean的引用
  • Bean前置处理器(postProcessBeforeInitialization)
  • 调用初始化(执行自定义的初始化方法)
  • Bean后置处理器(postProcessAfterInitialization)
  • Bean后置处理器用来检查Bean的属性的正确性以及更改属性
  • 以上Bean就创建成功可以使用了。当Bean过期时,调用销毁方法。
  • 如果实现DisposableBean接口的话,直接调用destroy方法。如果设置了destroy-method属性,会调用自己的销毁方法。

三级缓存

循环依赖:
  • Aservice里注入了Bservice,Bservice里注入了Aservice,在A实例化后开始填充属性,开始实例化B,B开始填充A,因为A还没有实例化完成,所以找不到A,然后去创建A,这就陷入了一个死循环。
一级缓存:
  • 在单例bean的初始化过程大致如下:
    1. 标记bean为创建中
    2. new出bean对象
    3. 如果支持循环依赖则生成三级缓存,可以提前暴露bean
    4. 填充bean属性,解决属性依赖
    5. 初始化bean,处理Aware接口并执行各类bean后处理器,执行初始化方法,如果需要生成aop代理对象
    6. 如果之前解决了aop循环依赖,则缓存中放置了提前生成的代理对象,然后使用原始bean继续执行初始化,所以需要再返回最终bean前,把原始bean置换为代理对象返回。
    7. 此时bean已经可以被使用,进行bean注册(标记)并注册销毁方法。
    8. 将bean放入容器中(一级缓存),移除创建中标记及二三级缓存(后面再具体分析)
循环依赖及三级缓存
  • 根据以上步骤可以看出bean初始化是一个相当复杂的过程,假如初始化A bean时,发现A bean依赖B bean,即A初始化执行到了第3步填充属性,需要注入B bean,此时B还没有初始化,则需要暂停A,先去初始化B,那么此时new出来的A对象放哪里,直接放在容器Map里显然不合适,半残品怎么能用,所以需要提供一个可以标记创建中bean(A)的Map,可以提前暴露正在创建的bean供其他bean依赖,而如果初始化A所依赖的bean B时,发现B也需要注入一个A的依赖(即发生循环依赖),则B可以从创建中的beanMap中直接获取A对象(创建中)注入A,然后完成B的初始化,返回给正在注入属性的A,最终A也完成初始化,皆大欢喜。
  • 如果配置不允许循环依赖,则上述缓存就用不到了,A 依赖B,就是创建B,B依赖C就去创建C,创建完了逐级返回就行,所以,一级缓存之后的其他缓存(二三级缓存)就是为了解决循环依赖!而配置支持循环依赖后,就一定要解决循环依赖吗?肯定不是!循环依赖在实际应用中也有,但不会太多,简单的应用场景是: controller注入service,service注入mapper,只有复杂的业务,可能service互相引用,有可能出现循环依赖,所以为了出现循环依赖才去解决,不出现就不解决,虽然支持循环依赖,但是只有在出现循环依赖时才真正暴露早期对象,否则只暴露个获取bean的方法,并没有真正暴露bean,因为这个方法不会被执行到,这块的实现就是三级缓存(singletonFactories),只缓存了一个单例bean工厂。
二级缓存
  • 三级缓存已经解决所有问题了,二级缓存用来做什么呢?为什么三级缓存不直接叫做二级缓存?这个应该是在缓存使用时决定的:
  • 三级缓存中提到出现循环依赖才去解决,也就是说出现循环依赖时,才会执行工厂的getObject生成(获取)早期依赖,这个时候就需要给它挪个窝了,因为真正暴露的不是工厂,而是对象,所以需要使用一个新的缓存保存暴露的早期对象(earlySingletonObjects),同时移除提前暴露的工厂,也不需要在多重循环依赖时每次去执行getObject。

补充

  • 有人觉得三级缓存没必要,存在aop代理时,直接生成代理对象并暴露出去,生成二级缓存就够了。
  • 为什么Spring不这么做呢?我认为这是Spring发展过程中产生的历史问题,早期的版本应该是不支持循环依赖的!后来遇到了循环依赖的问题,Spring为了尽可能小的影响原来的核心代码,就对当时AOP代理过程做了扩展,而不是推翻重写。
  • Spring正常的代理应该是发生在bean初始化后,由AbstractAutoProxyCreator.postProcessAfterInitialization处理。而循环依赖要求bean在填充属性前就提前生成代理,所以Spring在代码中开了个口子,循环依赖发生时,提前代理,没有循环依赖,代理方式不变,依然是初始化以后代理,所以不是不能直接提前生成代理,而是所有bean都提前生成代理,那AbstractAutoProxyCreator.postProcessAfterInitialization直接废了,相当于把原本的逻辑推翻重写了,这么做只是为了解决循环依赖得不尝试,没有完全必要的情况下对核心代码大改甚至推翻重写是一种大忌。
  • 而三级缓存的实现提供了提前生成代理的口子,而不是直接生成代理,只有发生循环依赖执行getObject才会执行代理,达到上述循环依赖发生时,提前代理,没有循环依赖,代理方式不变,依然是初始化以后代理的目的。

SpringMVC

什么是SpringMVC

在web模型中,MVC是一种很流行的框架,通过把Model,View,Controller分离,把较为复杂的web应用分成逻辑清晰的几部分,是为了简化开发,减少出错。还是为了组内开发人员之间的配合。

SpringMVC的工作流程

  1. DispatcherServlet表示前置控制器,是整个SpringMVC的控制中心。用户发出请求,DispatcherServlet接收请求并拦截请求。
  2. HandlerMappeing为处理器映射。DispatcherServlet调用HandlerMappeing,HandlerMapping根据请求url查找Hanler。
  3. 返回处理器执行链,根据url查找控制器,并且将解析后的信息传递给DispatcherServlet。
  4. HandlerAdapter表示处理器适配器,其按照特定的规则去执行Hanler。
  5. 执行Handler找到具体的处理器。
  6. Controller将具体的执行信息返回给HanlerAdapter,如ModelAndView。
  7. HandlerAdapter将视图逻辑名或模型传递给DispatcherServlet。
  8. DispatcherServlet调用视图解析器来解析HandlerAdapter传递的逻辑视图名。
  9. 视图解析器将解析的逻辑视图名传给DispatcherServlet。
  10. DispatcherServlet根据视图解析器解析的视图结果,调用具体的视图,进行视图渲染。
  11. 将相应数据返回给客户端。

SpringBoot

什么是springboot

Springboot是一个框架,一种全新的编程规范,它的产生简化了传统spring众多框架中所需的大量且繁琐的配置文件,其核心理念就是“约定大于配置”,所以说,springboot是一个服务于框架的框架,服务范围是简化配置文件。

什么是约定大于配置

约定大于配置,也称作按约定编程,是一种软件设计范式,主要思想在减少开发人员做决定的数量,获得简单的好处,而不失灵活性。

  1. 开发人员仅需规定不符合约定的部分。
  2. 在没有规定配置的地方,采用默认配置,已力求最简配置为核心思想。

启动原理

三大注解

@SpringBootConfiguration

这里的@Configuration就是用来读取spring.factories文件的

@EnableAutoConfiguration

自动导入机制,使用Spring底层注解@import,给容器导入一个组件,导入的组件由AutoConfigurationPackages.Registrar类处理导入。@AutoConfigurationPackage注释的作用就是将主配置类所在的包下面所有的组件都扫描到Spring容器中

@ComponentScan

@ComponentScan注解的参数basePackages和value互为别名,用来定义要扫描的范围,即包名,扫描的范围就是定义的包中的类以及子包中的类,在未指定参数是,默认是扫描当前类所在的包及其子包的类。参数excludeFilters用于排除一些满足过滤器定义条件的类,它是一个数组类型,说明可以指定多个不扫描的过滤器规则。 TypeExcludeFilter.class:加载spring bean池中所有针对TypeExcludeFilter的扩展,并循环遍历这些扩展类调用其match方法 AutoConfigurationExcludeFilter.class:过滤掉会自动配置的配置类

读取配置文件的注解

  • @ConfigurationProperties
  • @Value
  • @Environment
  • @PropertySource

SpringCloud

微服务

微服务架构是一种架构模式,它提倡将单一应用程序划分成一组小的服务,服务之间互相协调、互相配合、为用户提供最终价值。每个服务运行在其独立的进程中,服务与服务之间采用轻量级的通信机制互相协调。每个服务都围绕着具体业务进行构建,并且能够被独立的部署到生产环境等。

什么是SpringCloud

springCloud是分布式微服务架构的一站式解决方案,是多种微服务架构落地技术的集合体,俗称微服务全家桶。

SpringCloud技术栈

服务注册与发现Eureka、nacos
服务负载与调用Netflix oss Ribbon
Netflix Feign
服务熔断降级Hystrix
服务网关Zuul、gateway
服务分布式配置SpringCloud Config、nacos
服务开发Spring boot

SpringCloud<->springBoot版本对应

CloudBoot
Hoxton2.2.x
GreenWich2.1.x
Finchley2.0.x
Edgware1.5.x
Dalston1.5.x

五大神兽

Eureka注册中心
  • 自我保护机制
    默认情况下,如果EurekaServer在一定时间内没有接收到某个微服务实例的心跳,EurekaServer将会注销该实例(默认90秒)。但是当网络分区故障发生时,微服务与EurekaServer之间无法正常通信,以上行为可能变成非常危险了——因为微服务本身其实是健康的,此时本不应该注销这个微服务。Eureka通过“自我保护机制”来解决这个问题——当EurekaServer节点在短时间内丢失过多客户端时,那么这个节点就会进入自我保护模式。

  • ACP
    A 可用性 Availability
    C 一致性 Consistency
    P 分区容错性 Tolerance of network Partition

  • Eureka:保证ap
    Eureka是所有服务平等,只要有一个服务存活就能保证整个网络使用,但不保证是最新的。

  • Zookeeper:保证cp
    Zk服务宕机会有一段时间选举master导致整个系统无法使用,所以是cp

  • Ribbon负载均衡

    • 轮询算法
      使用CAS算法保证一个线程不会拿到同一次请求。
      请求次数 % 请求机器集群总数 = 机器下标
    • 随机算法
      在存活的服务节点中随机选择一个下标的服务。
    • 权重策略
      复合判断服务所在区域的性能和服务的可用性,轮询选择服务器。
    • 一致hash(dubbo)
      用户服务的hash值选择服务器,每一个相同的请求每次请求都是相同的机器。
  • Hystrix熔断

    • Hystrix是一个用于处理的分布式系统的延迟和容错的开源库,在分布式系统里,许多依赖不可避免的会调用失败,Hystrix能保证在一个依赖出问题的情况下,不会导致整体服务失败,避免联级故障,以提高分布式系统的弹性。
    • 雪崩效应
      多个微服务之间互相调用,其中某个微服务节点不可用,导致其他依赖节点均不可用。
    • 服务降级fallback
      如同if else的else,switch的default,在我的服务无法使用、异常、或者长时间未响应的情况下,可以给系统一个兜底的方法。
    • 服务熔断break
      类似于电路中的保险丝,当系统达到最大访问量后,直接拒绝访问,然后可以调用服务降级的方式返回友好提示,之后在慢慢地恢复连接。
      服务的降级→熔断→恢复调用链路
    • 服务限流flowlimit
      这个限流就和mq大同小异了,高并发场景下,所有请求全部一起打过来,进行排队,一秒N个,有序的进行。
  • Zuul网关

    • Zull是Netflix开源的微服务网关,他可以和Eureka、Ribbon、Hystrix等组件配合使用。
    • 它包含了对请求的路由和过滤两个最主要的功能:其中路由功能负责将外部请求转发到具体的微服务实例上,是实现外部访问同一入口的基础而过滤功能则负责队请求的处理过程进行干预,是实现请求效验、服务器聚合等功能的基础。
  • feign

    • Feign是一个声明式的web service客户端。它的出现使开发web service客户端变得很简单。使用feign只需要创建一个接口加上对应的注解。
    • Feign是一种声明式。模板化的http客户端。在spring cloud中使用http请求访问远程服务,就像调用本地方法的,开发者完全感知不到这是在调用远程方法,更感知不到访问http请求。
  • Config配置中心

    • 为了方便服务配置文件统一管理,实时更新,所以需要分布式配置中心组件。在Spring Cloud中,有分布式配置中心组件spring cloud config ,它支持配置服务放在配置服务的内存中(即本地),也支持放在远程Git仓库中。在spring cloud config 组件中,分两个角色,一是config server,二是config client。
  • Gateway 网关

    • Spring Cloud Gateway是Spring Cloud官方推出的第二代网关框架,取代Zuul网关。网关作为流量的,在微服务系统中有着非常作用,网关常见的功能有路由转发、权限校验、限流控制等作用。底层使用的是Netty。
      核心逻辑就是 路由转发+执行过滤器链
    • Zuul和gateway的区别
      • 相同点:
        1. 底层都 是servlet
        2. 两者均是web网关,处理的是http请求。
      • 不同点:
ZullGateway
内部实现 可以扩展至其他微服务,其内部没有实现限流、负载均衡Gateway对比zuul多依赖了spring-webflux,在spring的支持下,功能更强大,内部实现了限流、负载均衡等,扩展性更强,但同时也限制了仅适合spring Cloud套件
是否支持异步Zull仅支持同步 Gateway支持异步。理论上gateway则更适合于提高系统吞吐量(但不一定有更好的性能),最终性能还需要严密的压测来决定
框架设计的角度Gateway具有更好的扩展性
性能 Zull是基于阻塞io的API网关,虽然后面发布的2版本是基于netty,也是非阻塞的,支持长连接,但是Spring Cloud暂时还没有整合计划 Gateway整合了spring-webflux,spring-webflux有一个全新的非阻塞的函数式Reactive Web框架,可以用来构建异步、非阻塞、事件驱动的服务,在伸缩性方面表现的非常好。使用的是非阻塞API
总结 总的来说,在微服务架构,如果使用了Spring Cloud生态的基础组件,则Spring Cloud Gateway相比而言更加具有优势,单从流式编程+支持异步就足以让开发者选择了。 对于小型微服务架构或是复杂架构(不仅包括微服务应用还有其他非Spring Cloud服务节点),zull也是一个不错的选择
Nacos注册中心
  • ZK作为SpringCloud注册中心
    和Eureka类似,修改pom,修改配置文件,实际也很少会用Zookeeper作为SpringCloud的注册中心。
Consul作为注册中心
  • 什么是Consul

Consul是一套开源的分布式服务发现和配置管理系统,使用Go语言开发的。
它提供了微服务系统中的服务治理,配置中心,控制总线等功能。这些功能中的每一个都可以根据需要单独使用,也可以一起使用以构建全方位的服务网络,总之Consul提供了一种完整的服务网格解决方案。
它具有很多优点。包括:基于raft协议,比较简洁;支持健康检查,同时支持HTTP和DNS协议支持跨数据中心的WAN集群,提供图形界面,跨平台,支持Linux、Max、Windows。

  • 怎么使用Consul作为注册中心

和Eureka类似,修改pom,修改配置文件,如果不是nocas的诞生,那么Consul就可能会成为Eureka的替代品。

分布式事务
  • 2PC (Two-phase commit protocol)
    • 中文叫二阶段提交。是一种强一致性,2PC引入一个事务协调的角色来协调管理各参与者的提交和回滚,二阶段分别指的是准备和提交两个阶段。
    • 准备阶段时协调者会给各参与者发送准备命令,可以把准备命令理解成除了提交事务之外什么事都做完了;
    • 同步等待所有资源响应后进入提交阶段,提交阶段不一定是提交,也有可能是回滚。加入在准备阶段所有参与者都返回准备成功,那么协调者则向所有参与者发送提交命令,然后等待所有事务提交成功之后,返回事务执行成功。
    • 对于分布式事务的问题是不可能100%解决,如果在协调者中或者提交回滚事务时发生错误,可以使用补偿机制、分析日志或者人工干预的方式来解决。
  • 3PC
    • 三阶段提交又称3PC,其在两阶段提交的基础上增加了CanCommit阶段,并引入了超时机制。一旦事务参与者迟迟没有收到协调者的Commit请求,就会自动进行本地commit,这样相对有效地解决了协调者单点故障的问题。
  • TCC
    • 说起分布式事务的概念,不少人都会搞混淆,似乎好像分布式事务就是TCC。实际上TCC与2PC、3PC一样,只是分布式事务的一种实现方案而已。
    • TCC(Try-Confirm-Cancel)又称补偿事务。其核心思想是:“针对每个操作都要注册一个与其对应的确认和补偿(撤销操作)”。它分为三个操作:
    • Try阶段:主要是对业务系统做检测及资源预留。
    • Confirm阶段:确认执行业务操作。
    • Cancel阶段:取消执行业务操作。
    • TCC事务的处理流程与2PC两阶段提交类似,不过2PC通常都是在跨库的DB层面,而TCC本质上就是一个应用层面的2PC,需要通过业务逻辑来实现。这种分布式事务的实现方式的优势在于,可以让应用自己定义数据库操作的粒度,使得降低锁冲突、提高吞吐量成为可能。
    • 而不足之处则在于对应用的侵入性非常强,业务逻辑的每个分支都需要实现try、confirm、cancel三个操作。此外,其实现难度也比较大,需要按照网络状态、系统故障等不同的失败原因实现不同的回滚策略。为了满足一致性的要求,confirm和cancel接口还必须实现幂等。
  • 最终一致性
    • 现在有商品购买的服务,调用创建订单的服务,这时候在中间加一个队列,只有在商品购买成功后,才能发送消息给创建订单服务。

Zookeeper

什么是Zookeeper

ZK从设计模式角度来理解:是一个基于观察者模式设计的分布式服务管理框架,他负责存储和管理大家都关心的数据,然后接受观察者的注册,一旦这些数据的状态发生变化,ZK就将负责通知已经在ZK上注册的那些观察者做出相应的反应。

大保健讲解

  • 就比如去洗脚,这个ZK集群就是老板,各个服务器就是技师,客户端(程序)就是客人。
  • 每一个技师都要去老板那里注册,信息都要保存在老板那里,这时候客人就可以看到技师的情况,挑选自己喜欢的技师,但比如今天喜欢的技师生病了,染了艾滋,那就要通知客人。
  • ZK = 文件系统 + 通知机制
  • 文件系统:每一个技师的信息都存在老板那里,谁今天可以上钟,谁不可以,老板都知道,就算有新的技师来了,老板也会知道。
  • 通知机制:客人也可以去关心某个技师的情况,通过老板来知道心仪的技师的情况,今天呢能不能干活啊。老板说好,有什么情况我都会通知你。

应用场景

  1. 统一命名服务
    在分布式环境下,经常需要对应用/服务进行统一的命名,便识别。例如:ip不容易记住,而域名和容易记住。
  2. 统一配置文件
    配置管理交个ZK管理,将配置信息写入ZNode,各个客户端服务器监听这个Znode,
    一旦Znode中的数据被修改,ZK将通知各个客户端服务器。
  3. 统一集群管理
    ZK可以实现实时监控节点状态变化,可将节点信息写入ZK上的一个ZNode,然后监听这个ZNode可获取它实时状态变化。
  4. 服务器动态上下线
    客户端能实时洞察到服务上下线的变化。(大保健案例)
  5. 软负载均衡
    在ZK中记录每台服务器的访问数,让访问数最少的服务器去处理最新的客户端请求。

集群选举机制

  • SID:服务器id。用来唯一标识一台ZK集群中的及其,每台机器不能重复,和myid一致。
  • ZXID:事务id。用来标识一次服务器状态的变更。在某一时刻,集群中的每台机器的ZXID都不一定完全一致,这和ZK服务器对于客户端“更新请求”有关。
  • Eposh:每个Leader任期的代号。没有Leader时同一轮投票过程中的逻辑时钟是相同的。每投完一次票这个数据就会增加。
  1. 第一次启动

    • 服务器1启动,发起一起选举。服务器1投自己一票。此时服务器1票数为一票,不够半数以上(3票),选举无法完成,服务器1状态保持为LOOKING。
    • 服务器2启动,在发起一次选举。服务器1和2分别投自己一票并交换选票信息:此时服务器1发现服务器2的myid比自己目前投票推举的大,更改选票为推举服务器2。此时服务器1票数为0,服务器2票数为2,没有半数以上结果,选举无法完成,服务器1、2保持LOOKING。
    • 服务器3启动,发起一起选举。此时服务器1和服务器2更选票为服务器3。此时投票结果,服务器1为0,服务器2为0,服务器3为3。服务器3的票数已经超过半数,服务器3当选leader。服务器1、2更改状态为follower。
    • 服务器4启动,发起一次选举。此时服务器1、2、3,已经不是LOOKING状态,不会更改选票信息,交换选票结果,服务器3为3票,服务器4为1票。少数服从多数,更改选票信息为服务器 - 更改状态为follwer。
    • 服务器5启动,同4一样当小弟。
  2. 服务器运行期间无法和leader保持连接

    • 按照(Eposh,ZXID,SID)的顺序,依次对比剩下可用的服务器。

数据结构

  • ZK数据模型的结构整体上可以看作是一棵树,每个节点称作一个ZNode.每一个ZNode默认能够存储1MB的数据,每个ZNode都可以通过其路径唯一标识。
  • ZK是为读多写少的场景所设计的,Znode并不是用来储存大规模业务数据,而是用于存储少量的状态和配置信息。

集群

  1. ZK集群有一个领导者(Leader),多个跟随者(Follower)
  2. 集群中只要半数以上结点存活,ZK集群就可以正常服务。所以ZK适合安装奇数台服务器。
  3. 全局数据一致,每一个Server保存一份相同的数据副本,Client无论连接到哪个Server,数据都是一致的。
  4. 更新请求顺序执行,来自同一个Client的更新请求按其发送顺序依次执行。谁先来先处理谁。
  5. 数据更新原子性,一次数据更新要么成功,要么失败。
  6. 实时性,在一定时间范围内,Client能读到最新数据。

分布式锁

为解决集群下各个服务器在多线程执行不同步的问题
ZK的节点有一个唯一的特性,创建一个节点,如果有这个节点,会报错。
步骤:

  1. 接收到请求后,在/locks节点下创建一个临时顺序节点。
  2. 判断自己是不是当前节点下最小的节点:是:获取到锁;不是,对前一个节点进行监听。
  3. 获取到锁,处理完业务后,delete节点释放锁,然后下面的节点将收到通知,重复第二步判断。

Dubbo

什么是Dubbo

Dubbo是一个分布式服务框架,提供了高性能和透明化的RPC远程服务调用方案,以及SOA服务治理方案,说白了其实dubbo就是一个远程调用的分布式框架。

Dubbo、springCloud的区别

DubboSpringCloud
服务注册中心ZoopeekerSpring Cloud Netflix Eureka
服务调用中心RPCREST API
服务网关Spring Cloud Netflix Zuul
断路器不完善Spring Coud Netflix Hystrix
分布式配置Spring Cloud Config
服务跟踪Spring Cloud Sleuth
消息总线Spring Cloud Bus
数据流Spring Cloud Stream
批量任务Spring Cloud Task

使用Dubbo构建的微服务就像组装电脑,各环节我们的选择自由度很高,但是最终有可能因为一条内存质量不行就点不亮了,总是让人不怎么放心,需要大量的时间去维护各个服务间的兼容性和可用性。而Spring Cloud就像是品牌机,在Spring Source的整合下,做了大量的兼容性测试,保证了机器拥有更高的稳定性,但是如果在使用非原装的组件,就需要对其基础有一定的了解。

Mybatis

什么是Mybatis

  • Mybatis是一个半ORM(对象关系映射)框架,它内部封装了JDBC,开发时是需要关注sql语句本身,不需要花费精力去处理加载驱动、创建连接、创建statement等繁杂的过程。程序员直接编写原生态sql,可以严格控制sql执行性能,灵活度高。
  • Mybatis可以使用xml或者注解来配置和映射原生信息,将pojo映射成数据库中的记录,避免了几乎所有的JDBC代码和手动设置参数以及获取结果集。
  • 通过xml文件或注解的方式将要执行的各种statement配置起来,并通过java对象和statement中sql的动态参数进行映射生成最终执行的sql语句,最后mybatis框架执行sql并将结果映射为java对象并返回。

Mybatis的优点

  1. 基于sql语句编程,相当灵活,不会对应用程序或者数据库的现有设计造成任何影响,sql写在xml里,解除sql与程序代码的耦合,便于统一管理;提供xml标签,支持编写动态sql,并可重用。
  2. 与JDBC相比,减少了50%以上的代码量,消除了JDBC大量冗余的代码,不需要手动开关连接。
  3. 很好的与各种数据库兼容,只有是JDBC支持的数据库都可以。
  4. 能够与spring框架很好的集成。
  5. 提供映射标签,支持对象与数据库的ORM字段关系映射;提供对象关系映射标签,支持对象关系组件维护。

Mybatis的缺点

  1. sql语句的编写工作量较大,尤其是字段多、关联表多时,对开发人员编写sql语句的功底有一定要求。
  2. sql语句依赖于数据库,导致数据库移植性差,不能随意更换数据库。

Mybatis与Hibernate的区别

  1. mybatis不完全是一个ORM框架,因为Mybatis需要程序员自己编写sql语句。
  2. mybatis直接编写原生态sql,可以严格控制sql执行性能,灵活度高,非常适合对关系型数据模型要求不高的软件开发,因为这类软件需要变化频繁,一旦需求变化要求迅速出成果。但是灵活的前提是mybatis无法做到数据库无关性,如果需要实现支持多种数据库的软件,则需要自定义多套sql映射文件,工作量大。
  3. Hibernate对象/关系映射能力强,数据库无关性好,对于关系模型要求高的软件,如果用hibernate可以节省很多代码,提高效率。

#{}和${}的区别

  • #{}是预编译处理,${}是字符串替换
  • Mybatis在处理#{}时,会将sql中的#{}替换为?号,调用preparedStatement的Set方法来赋值。
  • Mybaits在处理 时,就是 {}时,就是 时,就是{}里的值直接替换成变量的值。
  • 使用#{}可以有效的防止sql注入,提高系统安全性。

Mybatis的动态标签

Mybatis动态sql可以在Xml 映射文件内,以标签的形式编写动态 sql,执行原理是根据表达式的值完成逻辑判断并动态拼接sql的功能。

Mybatis提供了9种动态sql标签

  • trim
  • where
  • set
  • foreach
  • if
  • choose
  • when
  • otherwise
  • bind

Mybatis的一级、二级缓存

  1. 一级缓存:
    基于perpatualCache的HashMap本地缓存,其储存作用域为Session,当Session flush或close之后,该Session中的所有Cache就将清空,默认打开一级缓存。
  2. 二级缓存:
    二级缓存与一级缓存机制相同,默认也是采用perpatualCache,HashMap存储,不同在于其存储作用域为Mapper(NameSpace),并且可以自定义储存源。默认不打开二级缓存,要开启二级缓存,使用二级缓存属性类需要实现Serializable序列化接口。
  3. 对于缓存数据更新机制,当某一个作用域进行了C、U、D操作后,默认该作用域下所有Select的缓存将被Clear。

为什么分布式下实体类需要实现序列化接口

实现 serializable接口的作用是就是可以把对象存到字节流,然后可以恢复。如果对象没实现序列化怎么才能进行网络传输呢,要网络传输就得转为字节流,所以在分布式应用中,你就得实现序列化,如果不需要分布式应用,那就没那个必要实现序列化。

Elasticsearch

什么是Elasticsearch

Es是一个基于Lucene的搜索引擎。它提供了具有http web界面和无架构json文档的分布式、多组户能力的全文搜索引擎。ES是java开发的,根据Apache许可源作为开源发布。

与关系型数据库的数据结果对比

关系型数据库 数据库 database表 table记录 row列 column
Elasticsearch 索引 index类型 type文档 document字段 field

倒排索引

  • ES是根据倒排索引的方式查找数据的;要了解什么是倒排索引,那得先了解下什么是正排索引:像mysql使用的就是正排索引,通过索引来查询关键字、文章这些数据;
  • 倒排索引就是在每次写入数据的时候,es会为数据进行分词,然后给分出的关键词创建索引表,里面存放的是存在该关键词的文档,下次查询的时候就也会对查询的数据进行分词,然后根据关键词查找到索引值,在通过索引值来查询文档;倒排索引是建立在正排索引之上的。

ES索引文档的过程

  1. 客户向集群节点写入数据,发送请求(如果没有指定路由/协调节点,请求的节点扮演路由节点的角色)
  2. 节点1接收请求后,使‘用文档_id’确定文档属于分片0,。请求被转到另外的节点,假定为节点3。因此分片0的主分片分配到节点3上。
  3. 节点3在主节点执行写操作,如果成功,则将请求转发到节点1和节点2的副本节点上,等待结果返回。所有的副本分片都报告成功,节点3将向协调节点报告成功,节点1向请求客户端报告写入成功。

文档获取分片过程(路由算法)

Hash(_routing) % num_of_primary_shards

Elasticsearch和 solr 的区别

  • 背景:它们都是基于Lucene搜索服务器基础之上开发,一款优秀的,高性能的企业级搜索服务器。【是因为他们都是基于分词技术构建的倒排索引的方式进行查询】
  • 诞生时间:
    • Solr :2004年诞生。
    • Es:2010年诞生。
  • Elasticsearch 与 Solr 的比较总结
    • 二者安装都很简单;
    • Solr 利用 Zookeeper 进行分布式管理,而 Elasticsearch 自身带有分布式协调管理功能;
    • Solr 支持更多格式(xml,json,csv等)的数据,而 Elasticsearch 仅支持json文件格式;
    • Solr 官方提供的功能更多,而 Elasticsearch 本身更注重于核心功能,高级功能多有第三方插件提供;
    • Solr 在传统的搜索应用中表现好于 Elasticsearch(插入删除慢),但在处理实时搜索应用时效率明显低于 Elasticsearch。
    • Solr 是传统搜索应用的有力解决方案,但 Elasticsearch 更适用于新兴的实时搜索应用。
    • Solr比较成熟,有一个更大,更成熟的用户、开发和贡献者社区,而Elasticsearch相对开发维护者较少,恒信太快,学习使用成本较高

网络通信

三次握手

在网络传输的过程中,为了保证数据传输无误,通常采用三次握手的策略。

  • 客户端发送带有SYN标志的数据包,代表第一次握手。
  • 服务端发送带有SYN/ACK标志的数据包,代表第二次握手
  • 客户端发送带有ACK标志的数据包,代表第三次握手。

四次挥手

任何一方都可以在数据传送结束后发出连接释放的通知,等待对象确认后进入半关闭状态。当另一方也没有数据再发送的时候,则发出连接释放的通知,对方确认后就完全关闭了TCP连接。举个例子:你和张三打电话,然后你没啥说的了,你跟张三说我们挂了吧,然后张三说好,但是张三还没有说完,于是又说了一会儿,然后张三说,好了没啥说的了,挂了吧,然后你回张三,好的。

本文标签: 宝典Java