面试

文章目录

  • 面试
  • 自我介绍
  • 基础
    • int类型的取值范围
    • String底层为什么是final修饰的
    • final关键字
    • 面向对象
    • 向上转型和向下转型
    • Java自动类型转换
    • Java数据类型自动提升(注意以下讨论的是二元操作符)
    • 抽象类和接口的区别
    • 静态代码块,构造代码块和构造函数的执行顺序
    • 反射
      • Java中利用反射获取对象的方式有:
    • 异常
  • 集合
    • ArrayList动态数组扩容机制
    • HashMap的结构
    • hashTable, hashMap, concerrentHashMap
    • HashMap的遍历
    • HashMap的动态删除
    • 理解和使用Java8中的时间API
  • JVM
    • 可达性分析
    • 双亲委派模型
    • Java对象的创建过程
    • Java类的生命周期
    • 垃圾回收算法与垃圾回收器
    • 调优命令
    • 逃逸分析和栈上分配
      • 栈上分配
      • 逃逸分析
    • 什么是Stop The World? 什么是OopMap? 什么是安全点?
    • 什么是指针碰撞?什么是空闲列表?什么是TLAB?
  • 多线程
    • Java中实现多线程有几种方法
    • 线程池
    • 线程死锁
    • wait()和notify()
    • wait()和sleep()区别
    • sychronized关键字
    • volitle关键字
    • ThreadLocal关键字
    • Lock
    • countDownLatch
  • 框架
    • JDBC
    • Mybatis
    • Servlet
    • SpringMVC
    • Spring
      • Bean的生命周期
      • IOC
      • 循环依赖
      • AOP
  • 数据库
    • 数据库引擎 Innodb 和 myisam 区别
    • InnoDB的B+树结构(mysq数据库中的索引结构)
    • 深入了解MySQL数据库的索引及底层结构
      • 导致SQL执行慢的原因?
      • 慢sql问题解决入口:
      • 什么是索引?
      • 索引类型:
      • 索引的种类:
      • MySQL聚集索引和非聚集索引的区别?
      • 哈希索引:
      • B+TREE索引:
      • 联合索引以及联合索引的失效问题
    • 数据库连接池只有100个连接,有3000个连接请求
    • 数据库锁
      • MySQL锁概述
      • 数据库的三种锁特性
      • 什么是锁?
      • 什么是死锁
      • 数据库死锁出现的案列
      • 如何处理死锁?
      • 如何避免死锁?
      • innodb默认是如何对待死锁的?
      • 如何开启死锁检测?
      • 什么是全局锁?它的应用场景有哪些?
      • 使用全局锁会导致的问题?
      • 如何处理逻辑备份时,整个数据库不能插入的情况?
      • innodb如何实现行锁?
      • 什么是共享锁?
      • 什么是排他锁?
      • 悲观锁和乐观锁的区别?
      • 乐观锁有什么优点和缺点?
      • innodb存储引擎有几种算法?
      • 优化锁方面的意见?
    • mysql数据库读写分离原理
    • mysql主从复制机制
    • redis中的数据类型和数据结构
    • redis主从复制机制
  • 并发
    • redis和数据库双写一致性问题
      • 1.先更新数据库,再更新缓存(有脏数据,不采用)
      • 2.先删缓存,再更新数据库(有脏数据,不采用,可用延时双删策略解决)
      • 3.延时双删策略(可以采用)
      • 4.先更新数据库,再删缓存(可以采用)
    • 高并发情况下如何保证数据安全
    • 高并发情况下如何保证系统安全
    • 高并发情况下如何解决redis雪崩和穿透
  • 事务
    • ACID
    • Spring 和 数据库 事务的隔离级别
    • 传播行为
    • Spring事务什么时候会失效?
    • 分布式事务控制
      • TPC
      • TCC
    • 幂等性
  • 工具
    • Nginx
    • rabbitMQ
      • rabbitmq生产
      • rabbitmq消费
    • kafka
      • kafka生产
      • kafka消费
    • kafka如何确保数据不丢失
    • kafka怎么保证消息的消费顺序?
      • kafka保证消息顺序有2种方法。
    • RabbitMQ和Kafka的区别
    • kafka的优势
    • Zookeeper
      • 什么是ZooKeeper
      • zookeeper 都有哪些功能?
      • zookeeper 有三种部署模式:
      • zookeeper 怎么保证主从节点的状态同步?
  • 设计模式
  • 网络
    • HTTP和HTTPS的区别
    • HTTP和TCP的区别和联系
      • TCP连接
      • HTTP连接
      • 相互联系和区别

自我介绍


At first, Thank you so much for giving me this opportunity for this interview.

My name is XXX, and you can call me Alex Leon which is my English name.

I graduated from Shanghai Maritime University with bachelor’s degree at 2016, I have worked for two companies, have been engaged in Java development for about five years

I passed CET4 during my college years,and I got Java Software Development Special Skill Certificate at 2019 , which is issued by the (MIIT)Ministry of industry and information technology, Now I am studying and preparing for the exam of software designer.

I have good foundation and coding practice of Java, and also I know the skills of Groovy, Mysql and Oracle, Im familiar with popular framework such as Spring, SpringBoot, SpringMVC, SpringCloud, Mybatis, and Grails, and I often use the tools like Kafka, RabbitMq, Redis and Nginx

The latest project I participated is SPDB(Shanghai Pudong Development Bank) ecosystem marketing project, with framework springBoot, springCloud and grails. it is distributed and microserviced.

Im good at learning new technologies, I love coding, I love programming, and I always keep a good self-drive for learning.

CitiBank is a large and international company, on the other hand, I have similar project experience of bank, so I really hope to join Citibank

Thank you so much


各位面试官好, 我叫XXX,16年毕业于上海海事大学毕业, 毕业之后一直在上海发展,一共呆过两家公司,从事java开发工作5年左右

我的大概情况是: 参加工作前两年从事企业传统项目,主要是ERP\CRM这些生产管理系统,用到的技术点主要是传统的单体框架,SSM框架,数据库是mysql。
后来这两年参与浦发银行生态圈项目,涉及分布式和微服务的架构

我近期参与的项目是浦发银行生态圈营销系统
主要功能是为浦发银行所有生态产品,比如手机银行app、浦惠到家app、浦慧app、甜橘app等,为这些产品提供制券和活动页面配置的管理端系统,以及这些h5活动页面的运行时服务支持
采用微服务分布式架构,开发语言采用的是groovy, 框架使用的是grails框架,包管理工具使用的是gradle, 同时集成了springCloud的相关组件

角色情况,前期作为初级开发工程师,主要以开发功能模块为主,近一年作为开发组长, 带领8个人的小团队,也参与到需求分析评估、项目流程管理,包括ci cd发布流程、以及代码审核的工作。

平时我喜欢看一些源码,会去github或者gitee逛一些开源项目, 也租了服务器购买和备案域名并搭建了一些个人项目,比如主页,在线简历,浏览器搜索页等

19年通过了工信部专项技能认证java开发工程师的考试, 21年去年我参加了国家软考软件设计师考试, 下午题目考了62发挥不错,上午题目就差5分就能通过
所以今年第一个目标就是换个工作,第二个目标就是能通过软考


基础


int类型的取值范围

-2^31 ~ 2^31 – 1

String底层为什么是final修饰的

  • 1.为了实现字符串池

只有字符串是不可变的,字符串池才有可能实现。不同的字符串变量可以指向池中的同一个字符串,节省heap空间。但如果字符串是可变的,如果变量改变了它的值,那么其它指向这个值的变量的值也会一起改变。

  • 2.为了线程安全

只有字符串是不可变的,多线程才安全,同一个字符串实例可以被多个线程共享。这样便不用因为线程安全问题而使用同步,字符串本身就是线程安全的。

  • 3.为了实现String可以创建HashCode不可变性

只有字符串是不可变的,则在它创建的时候HashCode就可以被允许缓存,并且不会在每次调用 String 的 hashcode 方法时重新计算。这就使得字符串很适合作为Map中的键,字符串的处理速度要快过其它的键对象。这就是HashMap中的键往往都使用字符串。

  • 4.为了系统安全

而且String类中的很多方法的实现不是Java代码,而是调用操作系统的本地方法来完成的,如果String类不被final修饰,被继承重写方法的话,系统会很不安全。

虽然final修饰代表了不可变,但仅仅是引用地址不可变,并不代表了数组本身不会变

final关键字

final关键字可以修饰类,方法和变量:

  • 被final修饰的类不能被继承,即它不能拥有自己的子类;
  • 被final修饰的方法不能被重写;
  • final修饰的变量,无论是类变量、实例变量、参数变量(形参)还是局部变量,都需要进行初始化操作。

面向对象

重视对象思维,关注每个对象需要做什么,而不是关注过程和步骤

  1. 封装:

    明确标识出允许外部使用的所有成员函数和数据项

    内部细节对外部调用者透明,外部调用无需修改或者关心内部实现的细节

  2. 继承

    继承基类的方法,并作出自己的改变和扩展

    子类共性的方法或者属性(抽取出来)直接使用继承的父类的,不需要自己再定义,只需扩展自己个性化的

  3. 多态

    基于对象所属类的不同,外部对同一个方法的调用,实际执行的逻辑不同,使得程序更易扩展

    多态有个条件就是继承,多态和继承是一脉相承的

    多态的条件:继承,方法重写,父类引用指向子类对象

使用引用变量调用的方法实际上是子类重写的方法,而不是父类的

弊端:多态调用方法不能是子类特有/独有的方法,因为能调用的方法必须是重写父类的方法,所以父类中没有的方法不能调用。

向上转型和向下转型

所以我们常说的向上转型,其实就是多态,即父类引用指向子类对象,此时调用方法实际上使用的是子类的实现, 而子类独有的方法是无法调用的

但如果将该变量强制类型转换成子类(向下转型)后,就可以使用子类特有的方法

Java自动类型转换

  1. 两种类型是彼此兼容的
  2. 转换的目的类型占得空间范围一定要大于转化的源类型

正向过程:由低字节向高字节自动转换

byte->short->int->long->float->double

逆向过程:使用强制转换,可能丢失精度。

int a=(int)3.14;

Java数据类型自动提升(注意以下讨论的是二元操作符)

Java定义了若干使用于表达式的类型提升规则:

  1. 所有的byte型. short型和char型运算后将被提升到int型(例外: final修饰的short, char变量相加后不会被自动提升。)
  2. 如果一个操作数是long形 计算结果就是long型;
  3. 如果一个操作数是float型,计算结果就是float型;
  4. 如果一个操作数是double型,计算结果就是double型;

另一种归纳方式(《Java核心技术卷I》P43):

  1. 如果两个操作数其中有一个是double类型,另一个操作就会转换为double类型。
  2. 否则,如果其中一个操作数是float类型,另一个将会转换为float类型。
  3. 否则,如果其中一个操作数是long类型,另一个会转换为long类型。
  4. 否则,两个操作数都转换为int类型。

抽象类和接口的区别

区别抽象类接口
默认的方法实现它可以有默认的方法实现接口完全是抽象的,所有方法都必须是抽象的,java1.8之后允许接口有默认实现
实现方式子类使用extends关键字来继承一个抽象类,如果子类不是抽象类的话,那么子类必须实现父类所有的抽象方法的具体实现实现类使用implements关键字来实现接口,它需要提供接口中所有声明的方法的具体实现
构造器抽象类可以有构造器接口不能有构造器
与正常Java类的区别除了你不能实例化抽象类之外,它几乎和正常的类没有任何区别接口是完全不同的类型
访问修饰符抽象方法可以是public、protected和default这些修饰符接口里的方法默认修饰符是public,也只能是public
普通变量抽象类可以对变量没有限制,和正常类一样接口中的变量必须是public static final的
多继承性由于java单继承局限,当继承了抽象类,就不能继承其他的类了一个类可以实现多个接口,并且对你继承另一个类时,没有限制
添加新的方法你可以往抽象类中添加新的正常方法,并且你不需要改变你现在的代码如果你往接口中添加新的方法,那么你必须在实现了该接口的类中实现接口的新方法

静态代码块,构造代码块和构造函数的执行顺序

静态代码块:最早执行,类被载入内存时执行,只执行一次。没有名字、参数和返回值,有关键字static。

构造代码块:执行时间比静态代码块晚,比构造函数早,和构造函数一样,只在对象初始化的时候运行。没有名字、参数和返回值。

构造函数:执行时间比构造代码块时间晚,也是在对象初始化的时候运行。没有返回值,构造函数名称和类名一致。

注意:静态代码块在类加载的时候就执行,所以的它优先级高于main()方法。

下面我们看一下有继承时的情况:

public class Parent {    public Parent() {        System.out.println("Parent的构造方法");    }    static {        System.out.println("Parent的静态代码块");    }    {        System.out.println("Parent的构造代码块");    }}public class Son extends Parent {    public Son() {        System.out.println("Son的构造方法");    }    static {        System.out.println("Son的静态代码块");    }    {        System.out.println("Son的构造代码块");    }    public static void main(String[] args) {        System.out.println("main方法");        new Son();    }}
Parent的静态代码块        Son的静态代码块        main方法        Parent的构造代码块        Parent的构造方法        Son的构造代码块        Son的构造方法

可以看出:父类始终先调用(继承先调用父类),并且这三者之间的相对顺序始终保持不变。

到此貌似没什么问题,但是请看如下变形:

public class B {    public static B t1 = new B();    public static B t2 = new B();    {        System.out.println("构造代码块");    }    public B() {        System.out.println("构造函数");    }    static {        System.out.println("静态代码块");    }    public static B t3 = new B();    public static void main(String[] args) {        new B();    }}
构造代码块        构造函数        构造代码块        构造函数        静态代码块        构造代码块        构造函数        构造代码块        构造函数

因为b1、b2、b3用static修饰,与静态块处于同一优先级,同一优先级就按先后顺序来执行。

反射

在运行时动态获取调用或修改类信息,属性,方法。

Java中利用反射获取对象的方式有:

  • a)类名.class,不会加执行态代码块
Class<Object> c1 =Object.class
  • b)Class.forName(“包名.类名”) ,会执行静态代码块
Class<?> c2 = Class.forName("java.lang.Object");
  • c)类的实例对象.getClass(),会执行静态代码块
Class<?> c3 = new Object().getClass();
  • d)Class.forName(“包名.类名”, boolean,loader)
Class<?> c4 = Class.forName("com.java.oop.ClassA", false, ClassLoader.getSystemClassLoader());
  • e)类加载器.load(“包名+类名”) 不会执行静态代码块
ClassLoader loader = ClassLoader.getSystemClassLoader();        Class<?> c5 = loader.loadClass("com.java.oop.ClassA");//不会执行静态代码块。

异常


集合

ArrayList动态数组扩容机制

在JDK1.8中,如果通过无参构造的话,初始数组容量为0,当真正对数组进行添加时(即添加第一个元素时),才真正分配容量,默认分配容量为10;

当容量不足时(容量为size,添加第size+1个元素时),先判断按照1.5倍(位运算)的比例扩容能否满足最低容量要求,若能,则以1.5倍扩容,否则以最低容量要求进行扩容。

执行add(E e)方法时,先判断ArrayList当前容量是否满足size+1的容量;在判断是否满足size+1的容量时,先判断ArrayList是否为空,若为空,则先初始化ArrayList初始容量为10,再判断初始容量是否满足最低容量要求;若不为空,则直接判断当前容量是否满足最低容量要求;若满足最低容量要求,则直接添加;若不满足,则先扩容,再添加。

ArrayList的最大容量为Integer.MAX_VALUE

ArrayList扩容的例子:ArrayList相当于在没指定initialCapacity时就是会使用延迟分配对象数组空间,当第一次插入元素时才分配10(默认)个对象空间。

假如有20个数据需要添加,那么会分别在第一次的时候,将ArrayList的容量变为10 (如下图一);之后扩容会按照1.5倍增长。也就是当添加第11个数据的时候,Arraylist继续扩容变为10*1.5=15(如下图二);当添加第16个数据时,继续扩容变为15 * 1.5 =22个。

HashMap的结构

HashMap是数组➕单向链表的数据结构

数组中保存的不是key value, 严格意义上讲保存的是一个Node实现了Map.Entry接口

我可以围绕它源码的三个主要方法来讲一下
put() get()和resize()

put()方法时, 先对key进行hashCode(), 得到的值再去与数组容量进行与操作,得到一个哈希值

这步操作是为了使得key的哈希值都在数组下标范围内,定位到数组下标的bucket

当这个bucket为空时,直接将这个node放进去,所以多线程下线程不安全,多条线程同时判断到bucket为空,同时放入node导致有些数据没了,解决办法有Collections.Sychronized,或使用concurrentHashMap

当这个bucket中已经有值,说明存在hash冲突,此时遍历链表对比key.equals()

如果链表中已有则覆盖oldValue,如果没有则在链表的尾部(尾插法)进行add,1.8之前是头插法,重新赋值第一个节点然后指向前一个节点,多线程情况下可能导致next节点永不为空从而造成死链

当链表长度大于8时,会转换成红黑树,利用红黑树的左旋右旋来提高效率,当小于6时又会转换成链表

get()方法时,先对key哈希,找到数组的bucket,然后遍历链表查询key.equals()是否存在

resize()方法,数组长度默认起始是16,默认负载因子为0.75f,所以当数组大小超过16*0.75=12时,会对数组进行双倍扩容

hashTable, hashMap, concerrentHashMap

hashtable中不能有null key或者value, hashmap中允许

Hashtable中使用了sycronize同步,效率较低,虽然多线程中相对安全,但也不常使用

因为可以使用Collections.sycronized去实现

或者直接使用concurrentHashMap, 它在hashMap的基础上外层多维护了一个segment

是分段进行加锁的,所以多线程时安全又提高了效率

concurrentHashMap中通过自旋锁和CAS确保不同线程获取到的是同一个segment对象

HashMap的遍历

  • 声明一个map
HashMap<String, String> map = new HashMap<>();map.put("a","123");map.put("b","456");map.put("c","789");
  • 方法1:普通的foreach, 遍历的是key或者value
for (String val : map.values()){System.out.println("method1_foreach value:"+val);}for(String key : map.keySet()){System.out.println("method1_foreach key:"+ key + "; value:" + map.get(key));}
  • 方法2:迭代器装载entry, 或者迭代器装载keySet, 可以在遍历中同时使用map(动态删除首选)
Iterator<Map.Entry<String, String>> iterator = map.entrySet().iterator();while (iterator.hasNext()){Map.Entry<String,String> entry = iterator.next();System.out.println("method2_iterator: key:" + entry.getKey() + "; value:" + entry.getValue());}
  • 方法3:entrySet foreach, 非常推荐
for (Map.Entry<String,String> entry : map.entrySet()){System.out.println("method3_entrySetForeach: key:" + entry.getKey() + "; value:"+entry.getValue());}
  • 方法4:lamda表达式
map.forEach((key, value) ->{System.out.println(key + ": " + value);});
  • 方法5: stream API 单线程
map.entrySet().stream().forEach((entry) ->{System.out.println(entry.getKey() + ": " + entry.getValue());});

HashMap的动态删除

只能使用迭代器的方式(迭代器装载entrySet或者装载keySet),否则报异常ConcurrentModificationException

  • 迭代器装载keySet实现动态删除
Iterator<Integer> iter = map.keySet().iterator();while(iter.hasNext()) {int key = iter.next();System.out.println(key + ": " + map.get(key));if(key == 2) {iter.remove();}}
  • 迭代器装载entrySet实现动态删除
Iterator<Map.Entry<Integer, String>> mapiter = map.entrySet().iterator();while(mapiter.hasNext()) {@SuppressWarnings("unchecked")Map.Entry<Integer, String> entry = mapiter.next();System.out.println(entry.getKey() + ": " + entry.getValue());if(entry.getKey() == 2) {mapiter.remove();}}

理解和使用Java8中的时间API

hoohack


JVM

主要实现了Java的跨系统,不同系统由JVM编译处理成不同的机器码,所以不同的系统对应的JVM版本也不同

主要分为 类装载子系统、字节码执行引擎、运行时数据区

类装载子系统用于加载字节码

字节码执行引擎主要有三个作用

  • 执行字节码

  • 修改程序计数器

  • 创建和管理垃圾回收线程

    最重要的是运行时数据区,主要分为线程公有区和线程私有区

    线程公有区包含堆和方法区(元数据区)

  • 堆是存放对象的

  • 方法区用来存放常量、静态变量、类元信息

    线程私有区包含线程栈、本地方法栈、程序计数器

  • 当程序执行到native关键字修饰的本地方法的时候,会由本地方法栈分配空间

  • 程序计数器用于记录当前字节码执行到的位置,因为线程是交替获取cpu资源进行执行的,需要知道该从哪里执行

  • 线程栈中包含多个栈帧,每个线程分配一个线程栈,而线程中的方法又会分配不同的栈帧

    • 局部变量表:存放方法的局部变量
    • 操作数栈:为加减乘除等运算提供内存空间,变量先复制到操作数栈,运算完再将结果赋给相应变量. 所以i=i++还是等于原来的值, 是因为jvm先执行了压栈,将i压入操作数栈顶,然后执行自增操作,这个时候栈顶的i为1,本地变量i为2,但是又将栈顶的i弹出操作数栈并赋值给本地变量i了,所以本地变量i最终不变
    • 动态链接:我们调用一个方法的时候,那些方法名称包括括号实际上是”符号”,比如math.compute(), “compute()”实际上是一种符号,动态链接就是根据这些东西去找到它的内存地址,从而找到对应的代码
    • 方法出口:一个方法执行完后,需要找到调用他的上一个方法中的位置好继续执行

当伊甸园区满了,会触发minor gc, minor gc会回收整个年轻代

幸存的对象会从伊甸园区 移动到 其中一个幸存区s0, 当再次触发minor gc, 幸存对象又会被挪到另一个空的幸存区s1, 然后s0会被清空

所以当一个对象如果一直幸存,它会在幸存区 s0 和 s1 之间反复横跳

每经历一次gc,对象的分代年龄会加1, 当加到15, 这个对象会被移动到老年代

如果幸存对象在幸存区放不下,gc后也会被直接放到老年代

当老年代放满之后,jvm会再开启一个垃圾回收线程,专门进行full gc, full gc会将年轻代和老年代都回收

当full gc之后还是没法腾出足够空间,就会内存溢出OOM, OutOfMemeryException

可达性分析

GCRoot根结点:线程栈的本地变量、静态变量、本地方法栈的变量。

将GCRoot作为起点,从这些节点开始向下搜索引用的对象,找到的对象都标记为非垃圾对象,其余为标记的对象都是垃圾对象。

双亲委派模型

其实就是一种类加载器的层次关系

Java对象的创建过程

Java类的生命周期

当我们编写一个java的源文件后,经过编译会生成一个后缀名为class的文件,这种文件叫做字节码文件,只有这种字节码文件才能够在java虚拟机中运行,java类的生命周期就是指一个class文件从加载到卸载的全过程。

一个java类的完整的生命周期会经历加载、连接、初始化、使用、和卸载五个阶段,当然也有在加载或者连接之后没有被初始化就直接被使用的情况

加载

就是找到需要加载的类并把类的信息加载到jvm的方法区中,然后在堆区中实例化一个java.lang.Class对象,作为方法区中这个类的信息的入口。

类的加载方式比较灵活,我们最常用的加载方式有两种,一种是根据类的全路径名找到相应的class文件,然后从class文件中读取文件内容;另一种是从jar文件中读取

连接

连接阶段比较复杂,一般会跟加载阶段和初始化阶段交叉进行,这个阶段的主要任务就是做一些加载后的验证工作以及一些初始化前的准备工作,可以细分为三个步骤:验证、准备和解析。

  • 验证:当一个类被加载之后,必须要验证一下这个类是否合法,比如这个类是不是符合字节码的格式、变量与方法是不是有重复、数据类型是不是有效、继承与实现是否合乎标准等等。总之,这个阶段的目的就是保证加载的类是能够被jvm所运行。

  • 准备:准备阶段的工作就是为类的静态变量分配内存并设为jvm默认的初值,对于非静态的变量,则不会为它们分配内存。有一点需要注意,这时候,静态变量的初值为jvm默认的初值,而不是我们在程序中设定的初值。jvm默认的初值是这样的:

    • 基本类型(int、long、short、char、byte、boolean、float、double)的默认值为0。
    • 引用类型的默认值为null。
    • 常量的默认值为我们程序中设定的值,比如我们在程序中定义final static int a = 100,则准备阶段中a的初值就是100。
  • 解析:这一阶段的任务就是把常量池中的符号引用转换为直接引用。

那么什么是符号引用,什么又是直接引用呢?

我们来举个例子:我们要找一个人,我们现有的信息是这个人的身份证号是1234567890。只有这个信息我们显然找不到这个人,但是通过公安局的身份系统,我们输入1234567890这个号之后,就会得到它的全部信息:比如安徽省黄山市余暇村18号张三,通过这个信息我们就能找到这个人了。这里,123456790就好比是一个符号引用,而安徽省黄山市余暇村18号张三就是直接引用。在内存中也是一样,比如我们要在内存中找一个类里面的一个叫做show的方法,显然是找不到。但是在解析阶段,jvm就会把show这个名字转换为指向方法区的的一块内存地址,比如c17164,通过c17164就可以找到show这个方法具体分配在内存的哪一个区域了。这里show就是符号引用,而c17164就是直接引用。在解析阶段,jvm会将所有的类或接口名、字段名、方法名转换为具体的内存地址。

连接阶段完成之后会根据使用的情况(直接引用还是被动引用)来选择是否对类进行初始化。

初始化

如果一个类被直接引用,就会触发类的初始化。在java中,直接引用的情况有:

  • 通过new关键字实例化对象、读取或设置类的静态变量、调用类的静态方法。
  • 通过反射方式执行以上三种行为。
  • 初始化子类的时候,会触发父类的初始化。
  • 作为程序入口直接运行时(也就是直接调用main方法)。

垃圾回收算法与垃圾回收器

垃圾回收算法:

  1. 标记清除算法:将所有需要回收的对象进行标记,标记结束后对标记的对象进行回收,但是效率低,会造成大量的碎片。
  2. 复制算法: 复制算法将空间分为两部分,每次使用其中的一部分。当一块内存用完了,就将这块的所有对象复制到另一块,将已使用的块清除。不会产生碎片,但是会浪费一定的内存空间。 在堆中的年轻代使用该算法,因为年轻代的对象多为生存周期比较短的对象。年轻代将内存分为一个Eden,两个survivor。每次使用Eden与一个survivor。当回收时,将survivor与Eden中存活的对象复制到另一个survivor,最后清理掉Eden与survivor。当survivor与Eden中存活的对象大小超过另一个survivor,则需要老年代来担保。
  3. 标记整理算法:复制算法在对象存活率较高时,复制会使得效率降低。根据老年代的特点,使用标记整理算法。标记之后将所有存活的对象移向一端,将其他的清理。解决了碎片问题。
  4. 分代收集算法:在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。在老年代中,因为对象存活率高、没有额外空间对它进行分配担保,就必须使用“标记—清理”或者“标记—整理”算法来进行回收。

垃圾回收器:(Serial、ParNew、Parallel Scavaenge 、Serial Old、Parallel Old、CMS、G1)

Serial收集器是单线程的收集器,在进行垃圾回收时,需要停止其他的所有工作线程。

ParNew收集器时Serial的多线程版本。在单线程的环境下,parnew绝不比serial收集器具有更改的效果,因为存在着线程的开销,但是随着cpu的增加,便可以体现出优势。默认情况下线程个数与cpu数量相同。

Parallel Scavenge收集器:年轻代收集器,多线程并行收集,使用复制算法,与parnew相似。CMS,Parnew,Serial的设计目标是为了缩短用户线程的停顿时间。但是parallel scavenge的设计目标时实现一个可控的吞吐量(cpu运行用户代码时间/cpu消耗的总时间)。可以设置两个参数最大垃圾收集停顿时间、吞吐量大小,但是最大垃圾收集停顿

时间越小,系统设置的新生代越小,GC频率增加。

Serial Old 收集器:是serial在老年代的版本。

CMS:是一种获取最短停顿时间为目标的收集器。基于标记清除(老年代唯一一个基于标记清除的算法,除G1外)的算法实现。整个过程有四个步骤:初始标记、并发标记、重新标记、并发清除,其中初始标记与重新标记仍要停顿所有用户线程。初始阶段,主要负责标记gcroot能直接关联的对象,速度很快;并发标记是从GCRoot开始继续向下标记;重新标记是统计那些在并发标记过程中发生变化的标记;这个阶段的时间要比初始标记长,但是低于并发标记。并发清除是清除老年代中的垃圾。

CMS存在缺点:1.采用标记清除的算法(老年代唯一一个采用标记清除的算法),会产生碎片。 2.不能处理浮动垃圾(浮动垃圾:在并发清除时,用户线程还在运行,还会有新的垃圾产生,这部分只能等到下次GC时清理)。 3.对cpu特别敏感。由于CMS在GC时,最耗时的并发标记与并发 清除是与用户线程同时执行的,因此可以降低停顿时间,但是并发标记时会占用一部分的cpu资源,导致应用程序变慢。

G1收集器:唯一一个可以同时用于年轻代与老年代的垃圾收集器。G1收集器采用标记整理的算法,避免碎片。使用该收集器时,其堆的内存布局就发生变化,将堆分为不同的大小相等的region(每个region有一个remembered Set,为了避免做可达性分析是扫描这个堆,当引用在不同的region之间时,则将相关引用信息记录到remembered Set中),避免在整个堆中进行全区域的垃圾收集,能建立可预测的停顿时间模型。整个过程包括如下四个步骤:初始标记、并发标记、最终标记、筛选回收。初始标记与并发标记与CMS相似;最终标记:将并发标记阶段那些发生变化的对象的变化记录写入线程remembered set log,同时与remembered set合并;筛选回收阶段:通过对每个region的价值和成本进行筛选,已得到一个最好的回收方案,并回收。


调优命令

Sun JDK监控和故障处理命令有jps jstat jmap jhat jstack jinfo

  • jps,JVM Process Status Tool,显示指定系统内所有的HotSpot虚拟机进程。
  • jstat,JVM statistics Monitoring是用于监视虚拟机运行时状态信息的命令,它可以显示出虚拟机进程中的类装载、内存、垃圾收集、JIT编译等运行数据。
  • jmap,JVM Memory Map命令用于生成heap dump文件
  • jhat,JVM Heap Analysis Tool命令是与jmap搭配使用,用来分析jmap生成的dump,jhat内置了一个微型的HTTP/HTML服务器,生成dump的分析结果后,可以在浏览器中查看
  • jstack,用于生成java虚拟机当前时刻的线程快照。
  • jinfo,JVM Configuration info 这个命令作用是实时查看和调整虚拟机运行参数。

常用调优工具jconsole,jvisualvm

性能调优参数

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SdISCxkA-1648185335899)(https://blog.csdn.net/rodesad/article/details/51544977)]

逃逸分析和栈上分配

栈上分配

什么对象可以被栈上分配呢?没有被逃逸的小对象可以栈上分配

class Stack{}public class TestStackMemory01 {public static void main(String[] args) {     //小对象(占用内存资源比较少),未被逃逸.     //栈上分配(这样的对象在方法结束之后,生命周期就结束)Stack s1 = new Stack();}}

小对象有可能在内存分配时会存储在栈上。
堆中分配对象的生命周期会长一些,而且对象要想被销毁还得启用GC,而GC操作有可能导致系统暂停回收垃圾(毫秒级)。
如果将小对象分配到栈上(线程私有),方法执行结束会出栈,对象会销毁。声明周期短而且不用启用GC

逃逸分析

package com.java.memory;class Container{int[] array = new int[1024];//1024*4个字节@Overrideprotected void finalize() throws Throwable {System.out.println("finalilze()...");//此方法属于Object.lang下面,gc回收之前会调用这个方法,我们通过此方法来观察对象是否被回收}}public class TestHeapMemory01 {static Container c2;public static void main(String[] args) {Container c1 = new Container();c2=new Container();c1=null;System.gc();//启动gc回收机制}}

对于c1,如果没有16行代码c1=null,Container对象有引用指向,这时候GC不会回收
当加上c1=null的代码,此时就没有引用指向此对象,调用GC时会被回收,并在回收前调用finalize方法,这是堆区的GC回收机制

对于c2,方法外部有引用指向这个对象,属于逃逸对象,方法执行结束也不会被回收

逃逸分析的基本行为就是分析对象动态作用域:当一个对象在方法中被定义后,它可能被外部方法所引用,例如作为调用参数传递到其他地方中,称为方法逃逸。

例如以下代码:

public static StringBuffer craeteStringBuffer(String s1, String s2) {StringBuffer sb = new StringBuffer();sb.append(s1);sb.append(s2);return sb;}public static String createStringBuffer(String s1, String s2) {StringBuffer sb = new StringBuffer();sb.append(s1);sb.append(s2);return sb.toString();}

第一段代码中的sb就逃逸了,而第二段代码中的sb就没有逃逸。

使用逃逸分析,编译器可以对代码做如下优化:

  • 同步省略。如果一个对象被发现只能从一个线程被访问到,那么对于这个对象的操作可以不考虑同步。
  • 将堆分配转化为栈分配。如果一个对象在子程序中被分配,要使指向该对象的指针永远不会逃逸,对象可能是栈分配的候选,而不是堆分配。
  • 分离对象或标量替换。有的对象可能不需要作为一个连续的内存结构存在也可以被访问到,那么对象的部分(或全部)可以不存储在内存,而是存储在CPU寄存器中。

在Java代码运行时,通过JVM参数可指定是否开启逃逸分析,

  • -XX:+DoEscapeAnalysis : 表示开启逃逸分析
  • -XX:-DoEscapeAnalysis : 表示关闭逃逸分析 从jdk 1.7开始已经默认开始逃逸分析,如需关闭,需要指定-XX:-DoEscapeAnalysis

什么是Stop The World” />

Servlet

servlet是一个java程序,面向请求和响应,生成动态的web

Servlet的框架是由两个Java包组成的:javax.servlet与javax.servlet.http

  • Web Client 客户端向Servlet容器(Tomcat)发出Http请求;
  • 接收到之后,找到项目名称和servlet名称,找到class文件的完整路径,基于反射创建HttpServlet对象
  • 同时创建一个HttpRequest对象,将Web Client请求的信息封装到这个对象中; 创建一个HttpResponse对象;
  • Servlet容器调用HttpServlet对象的service方法,把HttpRequest对象与HttpResponse对象作为参数传给 HttpServlet对象;
  • HttpServlet调用HttpRequest对象的有关方法(doGet/doPost),获取Http请求信息;HttpServlet调用HttpResponse对象的有关方法,生成响应数据;
  • Servlet容器把HttpServlet的响应结果传给Web Client;

SpringMVC

springMVC是以请求为驱动,围绕servlet设计

其核心是dispatcherServlet,它是一个Servlet

  • (1) 客户端(浏览器)发送请求url,直接请求到DispatcherServlet。
  • (2) DispatcherServlet根据请求信息调用HandlerMapping,解析请求对应的Handler, 并返回Controller的名字
  • (3) 解析到对应的Handler名字后,开始由HandlerAdapter适配器处理。
  • (4) HandlerAdapter会根据名字来调用真正的处理器ControllerHandler开处理请求,并处理相应的业务逻辑。
  • (5) 处理器处理完业务后,会返回一个ModelAndView对象,Model是返回的数据对象,View是个逻辑上的View(View的名字)。
  • (6) ViewResolver会根据逻辑View查找实际的View。
  • (7) DispaterServlet把返回的Model传给View,进行渲染。
  • (8) 通过View返回给请求者(浏览器)

Spring

Spring是个轻量级的框架集合, 模块包括IOC,AOP,DAO,ORM,WEB,MVC

其中最主要的是IOC和AOP

IOC实现了bean的周期管理,缓存等

Bean的生命周期

  1. spring 启动是会加载spring管理的bean,并对bean进行实列化(默认实列化 单例模式scope=singleton)
  2. 对实列化bean进行属性设置
  3. 检查aware相关方法以及设置相关依赖
    • 实现beanNameAware接口,将bean的id设置到setBeanName()方法
    • 实现beanFactoryAware接口,spring将调用setBeanFactory方法,将beanFactory实列传进来
    • 实现beanClassLoadAware接口,setBeanClassLoader方法被调用,将类加载器传入到bean
  4. 调用beanPostProcess前置处理
    • bean实现BeanPostProcessor接口,它的postProcessBeforeInitialization方法将被调用
  5. 检查是否是InitializingBean以决定是否调用afterProperties方法
    • bean实现了InitializingBean接口,spring将调用它的afterPropertiesSet接口
  6. 检查是否配置自定义init-method方法
    • 配置了init-method方法,会被调用
  7. 调用beanPostProcess后置处理
    • bean实现了BeanPostProcessor接口,它的postProcessAfterInitialization接口方法被调用
  8. 注册必要的destruction回调接口
  9. bean的准备已经完成,可以在应用上下文进行调用
  10. 是否实现disposableBean接口
    • bean实现了disposableBean接口,spring就会调用destroy()接口
  11. 是否配置自定义的destroy方法
    • bean上配置了destroy-method属性就会调用destroy接口

IOC

ApplicationContext为对象提供一个存储和应用的环境

有两种实现, (Xml)ClassPathXmlApplicationContext和(注解)AnnotationConfigApplicationContext

每个类被包扫描之后,会将它的配置信息存放在beanDefinition容器中

创建好的bean对象被放入beanPool池中

getBean的时候先去beanPool池中取,没有beanFactory再基于beanDefinition中的配置信息创建

Spring依赖注入的四种方式:1.构造器注入,2.setter方法注入,3.静态工厂注入,4.实例工厂注入

循环依赖

即A的属性中依赖B,B的属性中也依赖A

创建bean对象的时候分为3步, 1实例化对象,2注入属性,3初始化

解决依赖注入的条件

  • 1.必须是单例
  • 2.至少有一方是setter方式注入

A先实例化,然后创建一个工厂,将工厂放到三级缓存,然后进行属性注入的时候需要依赖B

B再实例化, 属性注入的时候需要依赖A, 这时候去三级缓存中取工厂,通过工厂获取A的实例对象或者代理对象,然后放入二级缓存并清除三级缓存中的工厂, 然后初始化B并走完后续流程

然后A对象进行初始化,并将bean放入一级缓存并清除二级缓存,结束

AOP

Aop是基于动态代理来实现的, 在动态代理中通过invoke织入功能

动态代理有两种 1.jdk动态代理。2.cglib动态代理

区别是 jdk动态代理与目标类的关系是同级且组合关系, cglib与目标类的关系是子与父的继承关系

  • @Aspect:声明是一个切面类
  • @Component: 将该类注入到spring中管理
  • @PointCut:配置一个切点
  • @PostConstruct:服务器加载servlet的时候执行一次,可以简单理解为spring容器加载时执行(被注解方法必须是void方法、非、static、不能抛出异常等)


数据库

数据库引擎 Innodb 和 myisam 区别

Myisam使用的是表锁,不支持高并发。

InnoDB使用的是行锁,支持高并发。

Myisam不支持外键。

InnoDB支持外键。

Myizam支持全文索引。

InnoDB不支持全文索引。不过可以通过中间件实现,比如Solr,ElasticSearch.

Myisam不支持事务,innoDB支持事务

Myisam使用的是非聚集索引,也就是树节点存储的是数据的指针(地址),当查找数据的时候,首先找到树节点相对应的指针,再根据指针去数据实际存储的位置查找真正的数据。(索引文件和数据文件是分离的)。

InnoDB使用的是聚集索引,也就是树节点实际存储的就是真实的数据,当查找数据的时候,查找到相对应的叶子结点对应的数据就结束了。(叶节点包含了完整的数据记录)

InnoDB的B+树结构(mysq数据库中的索引结构)

  • 什么是读写分离(读写分离原理)

读写分离,基本的原理是让主数据库处理事务性增、改、删操作(INSERT、UPDATE、DELETE),而从数据库处理SELECT查询操作。

数据库复制被用来把事务性操作导致的变更同步到集群中的从数据库。

读写分离就是在主服务器上修改,数据会同步到从服务器,从服务器只能提供读取数据,不能写入,实现备份的同时也实现了数据库性能的优化,以及提升了服务器安全。

  • 为什么读写分离

因为数据库的“写”(写10000条数据到oracle可能要3分钟)操作是比较耗时的。

但是数据库的“读”(从oracle读10000条数据可能只要5秒钟)。

所以读写分离,解决的是,数据库的写入,影响了查询的效率。

  • 什么时候用读写分离

数据库不一定要读写分离,如果程序使用数据库较多时,而更新少,查询多的情况下会考虑使用,利用数据库 主从同步

可以减少数据库压力,提高性能。当然,数据库也有其它优化方案。memcache 或是 表折分,或是搜索引擎。都是解决方法。

mysql主从复制机制

  • mysq支持的复制类型
  1. 基于语句的复制。在服务器上执行sql语句,在从服务器上执行同样的语句,mysql默认采用基于语句的复制,执行效率高。
  2. 基于行的复制。把改变的内容复制过去,而不是把命令在从服务器上执行一遍。
  3. 混合类型的复制。默认采用基于语句的复制,一旦发现基于语句无法精确复制时,就会采用基于行的复制。
  • 复制的工作过程
  1. 在每个事务更新数据完成之前,master在二进制日志记录这些改变。写入二进制日志完成后,master通知存储引擎提交事务。

  2. Slave将master的binary log复制到其中继日志

    • 首先slave开始一个工作线程(I/O)
    • I/O线程在master上打开一个普通的连接,然后开始binlog dump process
    • binlog dump process从master的二进制日志中读取事件
    • 如果已经跟上master,它会睡眠并等待master产生新的事件,I/O线程将这些事件写入中继日志。
  3. Sql slave thread(sql从线程)处理该过程的最后一步,

    • sql线程从中继日志读取事件,并重放其中的事件而更新slave数据,使其与master中的数据一致,
    • 只要该线程与I/O线程保持一致,中继日志通常会位于os缓存中,所以中继日志的开销很小。
  • 前较为常见的Mysql读写分离分为以下两种:
  1. 基于程序代码内部实现

在代码中根据select 、insert进行路由分类,这类方法也是目前生产环境下应用最广泛的。优点是性能较好,因为程序在代码中实现,不需要增加额外的硬件开支,缺点是需要开发人员来实现,运维人员无从下手。

  1. 基于中间代理层实现

代理一般介于应用服务器和数据库服务器之间,代理数据库服务器接收到应用服务器的请求后根据判断后转发到,后端数据库,有以下代表性的程序。

(1)mysql_proxy。mysql_proxy是Mysql的一个开源项目,通过其自带的lua脚本进行sql判断。

MySQL官方提供的数据库代理层产品MySQLProxy搭建读写分离。

MySQLProxy实际上是在客户端请求与MySQLServer之间建立了一个连接池。所有客户端请求都是发向MySQLProxy,然后经由MySQLProxy进行相应的分析,判断出是读操作还是写操作,分发至对应的MySQLServer上。对于多节点Slave集群,也可以起做到负载均衡的效果

(2)Atlas。是由 Qihoo 360, Web平台部基础架构团队开发维护的一个基于MySQL协议的数据中间层项目。它是在mysql-proxy 0.8.2版本的基础上,对其进行了优化,增加了一些新的功能特性。360内部使用Atlas运行的mysql业务,每天承载的读写请求数达几十亿条。支持事物以及存储过程。

(3)Amoeba。由阿里巴巴集团在职员工陈思儒使用序java语言进行开发,阿里巴巴集团将其用户生产环境下,但是他并不支持事物以及存储过程。

redis中的数据类型和数据结构

redis主从复制机制

常用3招

  • 一主二仆

一个Master,两个Slave,Slave只能读不能写;

当Slave与Master断开后需要重新slave of连接才可建立之前的主从关系;Master挂掉后,Master关系依然存在,Master重启即可恢复。

  • 薪火相传

上一个Slave可以是下一个Slave的Master,Slave同样可以接收其他slaves的连接和同步请求,那么该slave作为了链条中下一个slave的Master,如此可以有效减轻Master的写压力。如果slave中途变更转向,会清除之前的数据,重新建立最新的。

  • 反客为主

当Master挂掉后,Slave可键入命令 slaveof no one使当前redis停止与其他Master redis数据同步,转成Master redis。

复制原理

  1. Slave启动成功连接到master后会发送一个sync命令;
  2. Master接到命令启动后的存盘进程,同时收集所有接收到的用于修改数据集命令,在后台进程执行完毕之后,master将传送整个数据文件到slave,以完成一次完全同步;
  3. 全量复制:而slave服务在数据库文件数据后,将其存盘并加载到内存中;
  4. 增量复制:Master继续将新的所有收集到的修改命令依次传给slave,完成同步;
  5. 但是只要是重新连接master,一次完全同步(全量复制)将被自动执行。

哨兵模式(sentinel)

反客为主的自动版,能够后台监控Master库是否故障,如果故障了根据投票数自动将slave库转换为主库。一组sentinel能同时监控多个Master。

使用步骤:

  1. 在Master对应redis.conf同目录下新建sentinel.conf文件,名字绝对不能错;
  2. 配置哨兵,在sentinel.conf文件中填入内容:
    sentinel monitor 被监控数据库名字(自己起名字) ip port 1

并发

redis和数据库双写一致性问题

从理论上来说,给缓存设置过期时间,是保证最终一致性的解决方案。这种方案下,我们可以对存入缓存的数据设置过期时间,所有的写操作以数据库为准,对缓存操作只是尽最大努力即可。也就是说如果数据库写成功,缓存更新失败,那么只要到达过期时间,则后面的读请求自然会从数据库中读取新值然后回填缓存。因此,接下来讨论的思路不依赖于给缓存设置过期时间这个方案。

三种策略可供参考

  • 先更新数据库,再更新缓存
  • 先删除缓存,再更新数据库
  • 先更新数据库,再删除缓存

1.先更新数据库,再更新缓存(有脏数据,不采用)

举例:同时有请求A和请求B进行更新操作,那么会出现

  • 线程A更新了数据库
  • 线程B更新了数据库
  • 线程B更新了缓存
  • 线程A更新了缓存

这就出现请求A更新缓存应该比请求B更新缓存早才对,但是因为网络等原因,B却比A更早更新了缓存。这就导致了脏数据,因此不考虑。

2.先删缓存,再更新数据库(有脏数据,不采用,可用延时双删策略解决)

该方案会导致不一致的原因是

同时有一个请求A进行更新操作,另一个请求B进行查询操作

那么会出现如下情形:

  • 请求A进行写操作,删除缓存
  • 请求B查询发现缓存不存在
  • 请求B去数据库查询得到旧值
  • 请求B将旧值写入缓存
  • 请求A将新值写入数据库

上述情况就会导致不一致的情形出现。而且,如果不采用给缓存设置过期时间策略,该数据永远都是脏数据。

那么,如何解决上面这种情况呢?采用延时双删策略

3.延时双删策略(可以采用)

  • 先淘汰删除缓存
  • 再写数据库(这两步和原来一样)
  • 休眠1秒,再次淘汰删除缓存

这么做,可以将1秒内所造成的缓存脏数据,再次删除。

那么,这个1秒怎么确定的,具体该休眠多久呢?

针对上面的情形,应该自行评估自己的项目的读数据业务逻辑的耗时。

然后写数据的休眠时间则在读数据业务逻辑的耗时基础上,加几百ms即可。这么做的目的,就是确保读请求结束,写请求可以删除读请求造成的缓存脏数据。

4.先更新数据库,再删缓存(可以采用)

  • 失效:应用程序先从cache取数据,没有得到,则从数据库中取数据,成功后,放到缓存中。
  • 命中:应用程序从cache中取数据,取到后返回。
  • 更新:先把数据存到数据库中,成功后,再让缓存失效。

杠精:这种策略也有极端情况

并发情况,假设这会有两个请求,一个请求A做查询操作,一个请求B做更新操作,那么会有如下情形产生

  • 缓存刚好失效
  • 请求A查询数据库,得一个旧值
  • 请求B将新值写入数据库
  • 请求B删除缓存
  • 请求A将查到的旧值写入缓存

但是这种情况概率很低,因为先天条件是数据库 写 操作 比 读 操作 慢,这一点很难发生

高并发情况下如何保证数据安全

数据库行锁

kafka异步执行数据库操作,redis控制库存数量,利用redis的incr和decr的原子性

分布式锁(效率差)

高并发情况下如何保证系统安全

高并发情况下如何解决redis雪崩和穿透

解决雪崩: 限流、降级、熔断、缓存备份

解决穿透:业务层面查询到null,也在redis中存null,以保证请求不再向后面服务发送


事务

ACID

1、原子性:

一个事务(transaction)中的所有操作,要么全部完成,要么全部不完成,不会结束在中间某个环节。
事务在执行过程中发生错误,会被恢复(Rollback)到事务开始前的状态,就像这个事务从来没有执行过一样。

2、一致性:

在事务开始之前和事务结束以后,数据库的完整性没有被破坏。这表示写入的资料必须完全符合所有的预设规则,这包含资料的精确度、串联性以及后续数据库可以自发性地完成预定的工作。

3、隔离性:

数据库允许多个并发事务同时对其数据进行读写和修改的能力,隔离性可以防止多个事务并发执 行时由 于 交 叉 执 行而导致数据 的不一致 。 事务隔离分为不同级别,包括读未 提 交 ( Readuncommitted)、读提交(read committed)、可重复读(repeatable read)和串行化(Serializable)。

4、持久性:

事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失。

Spring 和 数据库 事务的隔离级别

脏读是指一个事务在处理数据的过程中,读取到另一个为提交事务的数据。

不可重复读是指对于数据库中的某个数据,一个事务范围内的多次查询却返回了不同的结果,这是由于在查询过程中,数据被另外一个事务修改并提交了

幻读是事务非独立执行时发生的一种现象。

例如事务T1对一个表中所有的行的某个数据项做了从“1”修改为“2”的操作,这时事务T2又对这个表中插入了一行数据项,而这个数据项的数值还是为“1”并且提交给数据库。而操作事务T1的用户如果再查看刚刚修改的数据,会发现还有一行没有修改,其实这行是从事务T2中添加的,就好像产生幻觉一样,这就是发生了幻读。

幻读和不可重复读都是读取了另一条已经提交的事务(这点就脏读不同),所不同的是不可重复读查询的都是同一个数据项,而幻读针对的是一批数据整体(比如数据的个数)

  1. 未提交读: 会导致 读脏
  2. 提交读: 解决脏读
  3. 可重复读: 解决脏读,不可重复读
  4. 序列化(串行化): 解决脏读,不可重复读,和幻读;

    这是最高的隔离级别,它通过强制事务排序,使之不可能相互冲突,从而解决幻读问题。简言之,它是在每个读的数据行上加上共享锁。在这个级别,可能导致大量的超时现象和锁竞争。

  5. Spring还有一个默认, 按照数据库事务级别来

传播行为

有七大传播行为,也是在TransactionDefinition接口中定义。

  • PROPAGATION_REQUIRED:支持当前事务,如当前没有事务,则新建一个。
  • PROPAGATION_SUPPORTS:支持当前事务,如当前没有事务,则已非事务性执行(源码中提示有个注意点,看不太明白,留待后面考究)。
  • PROPAGATION_MANDATORY:支持当前事务,如当前没有事务,则抛出异常(强制一定要在一个已经存在的事务中执行,业务方法不可独自发起自己的事务)。
  • PROPAGATION_REQUIRES_NEW:始终新建一个事务,如当前原来有事务,则把原事务挂起。
  • PROPAGATION_NOT_SUPPORTED:不支持当前事务,始终已非事务性方式执行,如当前事务存在,挂起该事务。
  • PROPAGATION_NEVER:不支持当前事务;如果当前事务存在,则引发异常。
  • PROPAGATION_NESTED:如果当前事务存在,则在嵌套事务中执行,如果当前没有事务,则执行与 PROPAGATION_REQUIRED 类似的操作(注意:当应用到JDBC时,只适用JDBC 3.0以上驱动)。

Spring实现编程式事务,依赖于2大类,PlatformTransactionManager,与模版类TransactionTemplate(推荐使用)
声明式事务实现方式, Spring的tx:advice定义事务通知与AOP相关配置实现,另为一种通过@Transactional

Spring事务什么时候会失效?

  1. 如果数据库不支持事务,则失效

因为事务是作用于数据库。例如使用MySQL且引擎是MyISAM,则事务会不起作用,因为MyISAM引擎本身不支持事务;如果改成InnoDB,则可以。

  1. Service类没有被Spring管理

因为Spring的事务是基于AOP,所以如果Service类没有被Spring管理,变成一个Spring Bean,即使添加了@Transactional注解,事务也是无效的。

  1. 内部调用

不带事务的方法调用该类中带事务的方法,不会回滚。因为Spring的回滚是用过代理模式生成的,如果是一个不带事务的方法调用该类的带事务的方法,直接通过this.xxx()调用,而不生成代理事务,所以事务不起作用。常见解决方法“拆类”

  1. 发生的异常不是RuntimeException, 比如IOException

spring的事务默认是对RuntimeException进行回滚,而不继承RuntimeException的不回滚

因为在java的设计中,它认为不继承RuntimeException的异常是CheckException或普通异常,如IOException,这些异常在java语法中是要求强制处理的。

对于这些普通异常,Spring默认它们都已经处理,所以默认不回滚。可以添加rollbackfor=Exception.class来表示所有的Exception都回滚

  1. 异常被捕捉处理没有抛出
  2. 事务只能应用于 public 方法

@Transactional注解只能应用于public方法,如果你在protected、private或者默认可见性的方法上使用 @Transactional 注解,这将被忽略,也不会抛出任何异常。

分布式事务控制

TPC

一般的实现形式:所有的事务一阶段执行sql不提交 ,都成功之后TC通知所有事务进行二阶段主动提交,如果有一个失败TC通知所有事务进行二阶段回滚

一阶段:协调器问“你们几个子事务参与者对应的活能不能干成?” 子事务参与者们一一回复“能干/干不成”

二阶段:协调器问根据子事务参与者们的反馈如果都能干则告诉所有人都去干吧,如果有人说干不了,特通知大家别干了

TCC

try阶段所有参与者进行尝试提交业务(eg:创建订单的订单状态是CREATING,减库存虽然进行了100-2=98,但是会记录本次有2个冻结中的库存,等类似try操作);

Confirm阶段 如果try阶段的执行都成功了则TM通知所有参与者执行真正的提交(eg:创建订单的订单状态改为CREATED,减库存 被冻结的2个库存直接删掉,等类似Confirm操作【因为用网络超时等原因可能会有重复的调用所有要求支持幂等性】);

cancel阶段 如果try中有一个执行失败则TM通知所有参与者进行补偿操作(eg:创建订单的订单状态改为CANCELED,减库存中被冻结的2重新加回到数据库中,等类似cancel操作

幂等性

幂等性:就是用户对于同一操作发起的一次请求或者多次请求的结果是一致的,不会因为多次点击而产生了副作用。

举个最简单的例子,那就是支付,用户购买商品后支付,支付扣款成功,但是返回结果的时候网络异常,此时钱已经扣了,用户再次点击按钮,此时会进行第二次扣款,返回结果成功,用户查询余额发现多扣钱了,流水记录也变成了两条。在以前的单应用系统中,我们只需要把数据操作放入事务中即可,发生错误立即回滚,但是再响应客户端的时候也有可能出现网络中断或者异常等等。

  • 1.token机制解决幂等性

使用uuid生成一个防重令牌token,并把token放到redis里,然后把这个token,封装到出参,给到前端的订单确定页面

  • 2.防重表解决幂等性
  • 3.业务层分布式锁

工具

Nginx

反向代理

正向代理

server{listen 80;server_name manage.jt.com;location / {proxy_pass http://jtWindows;}}#定义windows集群upstream jtWindows {server localhost:8081;server localhost:8082;server localhost:8083;}

rabbitMQ

rabbitmq生产

rabbitmq消费

kafka

kafka生产

kafka消费

kafka对消息保存时根据Topic进行归类,发送消息者就是Producer,消息接受者就是Consumer,每个kafka实例称为broker。然后三者都通过Zookeeper进行协调

kafka中的broker 是消息的代理,Producers往Brokers里面的指定Topic中写消息,Consumers从Brokers里面pull拉取指定Topic的消息,然后进行业务处理,broker在中间起到一个代理保存消息的中转站。

每个Topic被分成多个partition(区)。每条消息在partition中的位置称为offset(偏移量),类型为long型数字。消息即使被消费了,也不会被立即删除,而是根据broker里的设置,保存一定时间后再清除,比如log文件设置存储两天,则两天后,不管消息是否被消费,都清除。

每个consumer属于一个consumer group。在kafka中,一个partition的消息只会被group中的一个consumer消费

kafka如何确保数据不丢失

  • 生产时

kafka的ack机制:在kafka发送数据的时候,每次发送消息都会有一个确认反馈机制,确保消息正常的能够被收到。

  • 消费时

通过offset commit 来保证数据的不丢失,kafka自己记录了每次消费的offset数值,下次继续消费的时候,接着上次的offset进行消费即可

kafka怎么保证消息的消费顺序?

kafka只保证单partition有序,如果Kafka要保证多个partition有序,不仅broker保存的数据要保持顺序,消费时也要按序消费。

假设partition1堵了,为了有序,那partition2以及后续的分区也不能被消费,这种情况下,Kafka 就退化成了单一队列,毫无并发性可言,极大降低系统性能。因此Kafka使用多partition的概念,并且只保证单partition有序。这样不同partiiton之间不会干扰对方。

kafka保证消息顺序有2种方法。

  • 第1种:全局消费顺序

实现方式:1个Topic(主题)只创建1个Partition(分区),这样生产者的所有数据都发送到了一个Partition(分区),保证了消息的消费顺序。

比如3个直播间同时发消息,全局顺序就是保证直播间1先发的消息那么一定先到

  • 第2种:局部消费顺序

实现方式:生产者在发送消息的时候指定要发送到哪个Partition(分区)(1个)。

比如3个直播间同时发消息,局部顺序就是直播间1先发,直播间2后发,但是可能直播间2的消息先到,这个顺序是不保证的。但是直播间1先发了“消息1”,再发了“消息2”,这个顺序是能保证的,也就是在直播间内是有序的,但是直播间之间的消息顺序不能保证。

消费者以组的名义订阅topic,topic下有多个partition,消费者组中有多个消费者实例。
同一时刻,一条消息只能被组中的一个消费者实例消费。
如果按照从属关系来说的话就是,主题下的每个分区只从属于组中的一个消费者,不可能出现组中的两个消费者负责同一个分区。消息就是存储在partition中。

RabbitMQ和Kafka的区别

  • rabbitmq:
  1. producer,broker遵循AMQP(exchange,bind,queue),consumer;
  2. broker为中心,exchange分topic,direct,fanout和header,路由模式适合多种场景;
  3. consumer消费位置由broker通过确认机制保存;
  • kafka:
  1. producer,broker,consumer,未遵循AMQP;
  2. consumer为中心,获取消息模式由consumer自己决定;
  3. offset保存在消费者这边,broker无状态;
  4. 消息是名义上的永久存储,每个parttition按segment保存自己的消息为文件(可配置清理周期);
  5. consumer可以通过重置offset消费历史消息;
  6. 需要绑定zk;
  • AMQP是什么

AMQP(Advanced Message Queuing Protocol,高级消息队列协议)是一个进程间传递异步消息的网络协议。

发布者(Publisher)发布消息(Message),经由交换机(Exchange)。

交换机根据路由规则将收到的消息分发给与该交换机绑定的队列(Queue)。

最后 AMQP 代理会将消息投递给订阅了此队列的消费者,或者消费者按照需求自行获取。

kafka的优势

  1. 高吞吐量、低延迟:kafka每秒可以处理几十万条的消息,它的延迟最低只有几毫秒
  2. 持久性、可靠性:消息被持久化到本地磁盘,并且支持数据备份防止数据丢失
  3. 容错性:允许集群中节点故障(若副本数量为N,则允许N-1个节点故障)
  4. 高并发:支持数千个客户端同时读写(多个生产者和多个消费者同时读写)
  5. 可扩展性:kafka集群支持热扩展

Zookeeper

什么是ZooKeeper

zookeeper 是一个分布式的,开放源码的分布式应用程序协调服务,是 google chubby 的开源实现,是 hadoop 和 hbase 的重要组件。它是一个为分布式应用提供一致性服务的软件,提供的功能包括:配置维护、域名服务、分布式同步、组服务等。

zookeeper 都有哪些功能?

  • 集群管理:监控节点存活状态、运行请求等。
  • 主节点选举:主节点挂掉了之后可以从备用的节点开始新一轮选主,主节点选举说的就是这个选举的过程,使用 zookeeper 可以协助完成这个过程。
  • 分布式锁:zookeeper 提供两种锁:独占锁、共享锁。独占锁即一次只能有一个线程使用资源,共享锁是读锁共享,读写互斥,即可以有多线线程同时读同一个资源,如果要使用写锁也只能有一个线程使用。zookeeper可以对分布式锁进行控制。
  • 命名服务:在分布式系统中,通过使用命名服务,客户端应用能够根据指定名字来获取资源或服务的地址,提供者等信息。

zookeeper 有三种部署模式:

  • 单机部署:一台集群上运行;
  • 集群部署:多台集群运行;
  • 伪集群部署:一台集群启动多个 zookeeper 实例运行。

zookeeper 怎么保证主从节点的状态同步?

zookeeper的核心是原子广播,这个机制保证了各个server 之间的同步。

实现这个机制的协议叫做 zab 协议。

zab 协议有两种模式,分别是恢复模式(选主)和广播模式(同步)。

当服务启动或者在领导者崩溃后,zab 就进入了恢复模式,当领导者被选举出来,且大多数 server 完成了和 leader 的状态同步以后,恢复模式就结束了。

状态同步保证了 leader 和 server 具有相同的系统状态。


设计模式

http://c.biancheng.net/design_pattern/


网络

HTTP和HTTPS的区别

HTTP 明文传输,数据都是未加密的,安全性较差,HTTPS(SSL+HTTP) 数据传输过程是加密的,安全性较好。

HTTPS比HTTP更加安全,对搜索引擎更友好,利于SEO,谷歌、百度优先索引HTTPS网页;

使用 HTTPS 协议需要到 CA(数字证书认证机构) 申请SSL证书。

HTTP 页面响应速度比 HTTPS 快,主要是因为 HTTP 使用 TCP 三次握手建立连接,客户端和服务器需要交换 3 个包,而 HTTPS除了 TCP 的三个包,还要加上SSL 握手需要的 9 个包,所以一共是 12 个包。

HTTP 和 HTTPS 用的端口也不一样,前者是 80,后者是 443。

在 OSI 网络模型中,HTTP 工作于应用层,而 HTTPS 工作在传输层。

HTTP和TCP的区别和联系

TCP连接

手机能够使用联网功能是因为手机底层实现了TCP/IP协议,可以使手机终端通过无线网络建立TCP连接。TCP协议可以对上层网络提供接口,使上层网络数据的传输建立在“无差别”的网络之上。

建立起一个TCP连接需要经过“三次握手”:

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

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

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

握手过程中传送的包里不包含数据,三次握手完毕后,客户端与服务器才正式开始传送数据。理想状态下,TCP连接一旦建立,在通信双方中的任何一方主动关闭连 接之前,TCP 连接都将被一直保持下去。断开连接时服务器和客户端均可以主动发起断开TCP连接的请求,断开过程需要经过“四次握手”(过程就不细写 了,就是服务器和客户端交互,最终确定断开)

HTTP连接

HTTP协议即超文本传送协议(Hypertext Transfer Protocol ),是Web联网的基础,也是手机联网常用的协议之一,HTTP协议是建立在TCP协议之上的一种应用。

HTTP连接最显著的特点是客户端发送的每次请求都需要服务器回送响应,在请求结束后,会主动释放连接。从建立连接到关闭连接的过程称为“一次连接”。

1)在HTTP 1.0中,客户端的每次请求都要求建立一次单独的连接,在处理完本次请求后,就自动释放连接。

2)在HTTP 1.1中则可以在一次连接中处理多个请求,并且多个请求可以重叠进行,不需要等待一个请求结束后再发送下一个请求。

由于HTTP在每次请求结束后都会主动释放连接,因此HTTP连接是一种“短连接”,要保持客户端程序的在线状态,需要不断地向服务器发起连接请求。通常的 做法是即时不需要获得任何数据,客户端也保持每隔一段固定的时间向服务器发送一次“保持连接”的请求,服务器在收到该请求后对客户端进行回复,表明知道客 户端“在线”。若服务器长时间无法收到客户端的请求,则认为客户端“下线”,若客户端长时间无法收到服务器的回复,则认为网络已经断开。

相互联系和区别

TCP是传输层,而http是应用层

http是要基于TCP连接基础上的: 简单的说,TCP就是单纯建立连接,不涉及任何我们需要请求的实际数据,简单的传输。http是用来收发数据,即实际应用上来的。

TCP协议是传输层协议,主要解决数据如何在网络中传输,而HTTP是应用层协议,主要解决如何包装数据。

TCP/IP和HTTP协议的关系,从本质上来说,二者没有可比性,我们在传输数据时,可以只使用(传输层)TCP/IP协议,但是那样的话,如果没有应用层,便无法识别数据内容,如果想要使传输的数据有意义,则必须使用到应用层协议,应用层协议有很多,比如HTTP、FTP、TELNET等,也可以自己定义应用层协议。WEB使用HTTP协议作应用层协议,以封装HTTP 文本信息,然后使用TCP/IP做传输层协议将它发到网络上。

Http协议是建立在TCP协议基础之上的,当浏览器需要从服务器获取网页数据的时候,会发出一次Http请求。Http会通过TCP建立起一个到服务器的连接通道,当本次请求需要的数据完毕后,Http会立即将TCP连接断开,这个过程是很短的,所以Http连接是一种短连接,是一种无状态的连接。所谓的无状态,是指浏览器每次向服务器发起请求的时候,不是通过一个连接,而是每次都建立一个新的连接。如果是一个连接的话,服务器进程中就能保持住这个连接并且在内存中记住一些信息状态。而每次请求结束后,连接就关闭,相关的内容就释放了,所以记不住任何状态,称为无状态连接。而我们直接通过Socket编程使用TCP协议的时候,因为我们自己可以通过代码区控制什么时候打开连接什么时候关闭连接,只要我们不通过代码把连接关闭,这个连接就会在客户端和服务端的进程中一直存在,相关状态数据会一直保存着。

形象的描述:HTTP是轿车,提供了封装或者显示数据的具体形式;Socket是发动机,提供了网络通信的能力。对于从C#编程的角度来讲,为了方便,你可以直接选择已经制造好的轿车Http来与服务器交互。但是有时候往往因为环境因素或者其他的一些定制的请求,必须要使用TCP协议,这时就需要使用Socket编程,然后自己去处理获取的数据。就像是你用已有的发动机,自己造了一辆卡车,去从服务器交互。

HTTP(超文本传输协议)是利用TCP在两台电脑(通常是Web服务器和客户端)之间传输信息的协议。客户端使用Web浏览器发起HTTP请求给Web服务器,Web服务器发送被请求的信息给客户端。