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分就能通过
所以今年第一个目标就是换个工作,第二个目标就是能通过软考
-2^31 ~ 2^31 - 1
只有字符串是不可变的,字符串池
才有可能实现。不同的字符串变量可以指向池中的同一个字符串
,节省heap空间。但如果字符串是可变的,如果变量改变了它的值,那么其它指向这个值的变量的值也会一起改变。
只有字符串是不可变的,多线程才安全
,同一个字符串实例可以被多个线程共享。这样便不用因为线程安全问题而使用同步,字符串本身就是线程安全的。
只有字符串是不可变的,则在它创建的时候HashCode就可以被允许缓存
,并且不会在每次调用 String 的 hashcode 方法时重新计算。这就使得字符串很适合作为Map中的键,字符串的处理速度要快过其它的键对象。这就是HashMap中的键往往都使用字符串。
而且String类中的很多方法的实现不是Java代码,而是调用操作系统的本地方法来
完成的,如果String类不被final修饰,被继承重写方法的话,系统会很不安全。
虽然final修饰代表了不可变,但仅仅是引用地址
不可变,并不代表了数组本身不会变
final关键字可以修饰类,方法和变量:
重视对象思维,关注每个对象需要做什么,而不是关注过程和步骤
封装:
明确标识出允许外部使用的所有成员函数和数据项
内部细节对外部调用者透明,外部调用无需修改或者关心内部实现的细节
继承
继承基类的方法,并作出自己的改变和扩展
子类共性的方法或者属性(抽取出来)直接使用继承的父类的,不需要自己再定义,只需扩展自己个性化的
多态
基于对象所属类的不同,外部对同一个方法的调用,实际执行的逻辑不同,使得程序更易扩展
多态有个条件就是继承,多态和继承是一脉相承的
多态的条件:继承,方法重写,父类引用指向子类对象
使用引用变量调用的方法实际上是子类重写的方法,而不是父类的
弊端:多态调用方法不能是子类特有/独有的方法,因为能调用的方法必须是重写父类的方法,所以父类中没有的方法不能调用。
所以我们常说的向上转型,其实就是多态,即父类引用指向子类对象,此时调用方法实际上使用的是子类的实现, 而子类独有的方法是无法调用的
但如果将该变量强制类型转换成子类(向下转型)后,就可以使用子类特有的方法
正向过程:由低字节向高字节自动转换
byte->short->int->long->float->double
逆向过程:使用强制转换,可能丢失精度。
int a=(int)3.14;
Java定义了若干使用于表达式的类型提升规则:
另一种归纳方式(《Java核心技术卷I》P43):
区别 | 抽象类 | 接口 |
---|---|---|
默认的方法实现 | 它可以有默认的方法实现 | 接口完全是抽象的,所有方法都必须是抽象的,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修饰,与静态块处于同一优先级,同一优先级就按先后顺序来执行。
在运行时动态获取调用或修改类信息,属性,方法。
Class<Object> c1 =Object.class
Class<?> c2 = Class.forName("java.lang.Object");
Class<?> c3 = new Object().getClass();
Class<?> c4 = Class.forName("com.java.oop.ClassA", false, ClassLoader.getSystemClassLoader());
ClassLoader loader = ClassLoader.getSystemClassLoader();
Class<?> c5 = loader.loadClass("com.java.oop.ClassA");//不会执行静态代码块。
在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是数组➕单向链表的数据结构
数组中保存的不是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中不能有null key或者value, hashmap中允许
Hashtable中使用了sycronize同步,效率较低,虽然多线程中相对安全,但也不常使用
因为可以使用Collections.sycronized去实现
或者直接使用concurrentHashMap, 它在hashMap的基础上外层多维护了一个segment
是分段进行加锁的,所以多线程时安全又提高了效率
concurrentHashMap中通过自旋锁和CAS确保不同线程获取到的是同一个segment对象
HashMap<String, String> map = new HashMap<>();
map.put("a","123");
map.put("b","456");
map.put("c","789");
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));
}
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());
}
for (Map.Entry<String,String> entry : map.entrySet()){
System.out.println("method3_entrySetForeach: key:" + entry.getKey() + "; value:"+entry.getValue());
}
map.forEach((key, value) ->{
System.out.println(key + ": " + value);
});
map.entrySet().stream().forEach((entry) ->{
System.out.println(entry.getKey() + ": " + entry.getValue());
});
只能使用迭代器的方式(迭代器装载entrySet或者装载keySet),否则报异常ConcurrentModificationException
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();
}
}
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();
}
}
hoohack
主要实现了Java的跨系统,不同系统由JVM编译处理成不同的机器码,所以不同的系统对应的JVM版本也不同
主要分为 类装载子系统、字节码执行引擎、运行时数据区
类装载子系统用于加载字节码
字节码执行引擎主要有三个作用
执行字节码
修改程序计数器
创建和管理垃圾回收线程
最重要的是运行时数据区,主要分为线程公有区和线程私有区
线程公有区包含堆和方法区(元数据区)
堆是存放对象的
方法区用来存放常量、静态变量、类元信息
线程私有区包含线程栈、本地方法栈、程序计数器
当程序执行到native关键字修饰的本地方法的时候,会由本地方法栈分配空间
程序计数器用于记录当前字节码执行到的位置,因为线程是交替获取cpu资源进行执行的,需要知道该从哪里执行
线程栈中包含多个栈帧,每个线程分配一个线程栈,而线程中的方法又会分配不同的栈帧
当伊甸园区满了,会触发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的源文件后,经过编译会生成一个后缀名为class的文件,这种文件叫做字节码文件,只有这种字节码文件才能够在java虚拟机中运行,java类的生命周期就是指一个class文件从加载到卸载的全过程。
一个java类的完整的生命周期会经历加载、连接、初始化、使用、和卸载五个阶段,当然也有在加载或者连接之后没有被初始化就直接被使用的情况
加载
就是找到需要加载的类并把类的信息加载到jvm的方法区中,然后在堆区中实例化一个java.lang.Class对象,作为方法区中这个类的信息的入口。
类的加载方式比较灵活,我们最常用的加载方式有两种,一种是根据类的全路径名找到相应的class文件,然后从class文件中读取文件内容;另一种是从jar文件中读取
连接
连接阶段比较复杂,一般会跟加载阶段和初始化阶段交叉进行,这个阶段的主要任务就是做一些加载后的验证工作以及一些初始化前的准备工作,可以细分为三个步骤:验证、准备和解析。
验证:当一个类被加载之后,必须要验证一下这个类是否合法,比如这个类是不是符合字节码的格式、变量与方法是不是有重复、数据类型是不是有效、继承与实现是否合乎标准等等。总之,这个阶段的目的就是保证加载的类是能够被jvm所运行。
准备:准备阶段的工作就是为类的静态变量分配内存并设为jvm默认的初值,对于非静态的变量,则不会为它们分配内存。有一点需要注意,这时候,静态变量的初值为jvm默认的初值,而不是我们在程序中设定的初值。jvm默认的初值是这样的:
解析:这一阶段的任务就是把常量池中的符号引用转换为直接引用。
那么什么是符号引用,什么又是直接引用呢?
我们来举个例子:我们要找一个人,我们现有的信息是这个人的身份证号是1234567890。只有这个信息我们显然找不到这个人,但是通过公安局的身份系统,我们输入1234567890这个号之后,就会得到它的全部信息:比如安徽省黄山市余暇村18号张三,通过这个信息我们就能找到这个人了。这里,123456790就好比是一个符号引用,而安徽省黄山市余暇村18号张三就是直接引用。在内存中也是一样,比如我们要在内存中找一个类里面的一个叫做show的方法,显然是找不到。但是在解析阶段,jvm就会把show这个名字转换为指向方法区的的一块内存地址,比如c17164,通过c17164就可以找到show这个方法具体分配在内存的哪一个区域了。这里show就是符号引用,而c17164就是直接引用。在解析阶段,jvm会将所有的类或接口名、字段名、方法名转换为具体的内存地址。
连接阶段完成之后会根据使用的情况(直接引用还是被动引用)来选择是否对类进行初始化。
初始化
如果一个类被直接引用,就会触发类的初始化。在java中,直接引用的情况有:
垃圾回收算法:
垃圾回收器:(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
常用调优工具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个字节
@Override
protected 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就没有逃逸。
使用逃逸分析,编译器可以对代码做如下优化:
在Java代码运行时,通过JVM参数可指定是否开启逃逸分析,
进行垃圾回收的过程中,会涉及对象的移动。为了保证对象引用更新的正确性,必须暂停所有的用户线程,像这样的停顿,虚拟机设计者形象描述为Stop The World。
在HotSpot中,有个数据结构(映射表)称为OopMap。一旦类加载动作完成的时候,HotSpot就会把对象内什么偏移量上是什么类型的数据计算出来,记录到OopMap。在即时编译过程中,也会在特定的位置生成 OopMap,记录下栈上和寄存器里哪些位置是引用。
这些特定的位置主要在:
这些位置就叫作安全点(safepoint)。 用户程序执行时并非在代码指令流的任意位置都能够在停顿下来开始垃圾收集,而是必须是执行到安全点才能够暂停。
一般情况下,JVM的对象都放在堆内存中(发生逃逸分析除外)。当类加载检查通过后,Java虚拟机开始为新生对象分配内存。如果Java堆中内存是绝对规整的,所有被使用过的的内存都被放到一边,空闲的内存放到另外一边,中间放着一个指针作为分界点的指示器,所分配内存仅仅是把那个指针向空闲空间方向挪动一段与对象大小相等的实例,这种分配方式就是指针碰撞
如果Java堆内存中的内存并不是规整的,已被使用的内存和空闲的内存相互交错在一起,不可以进行指针碰撞啦,虚拟机必须维护一个列表,记录哪些内存是可用的,在分配的时候从列表找到一块大的空间分配给对象实例,并更新列表上的记录,这种分配方式就是空闲列表
对象创建在虚拟机中是非常频繁的行为,可能存在线性安全问题。如果一个线程正在给A对象分配内存,指针还没有来的及修改,同时另一个为B对象分配内存的线程,仍引用这之前的指针指向,这就出「问题」了。
可以把内存分配的动作按照线程划分在不同的空间之中进行,每个线程在Java堆中预先分配一小块内存,这就是TLAB(Thread Local Allocation Buffer,本地线程分配缓存)。虚拟机通过-XX:UseTLAB设定它的。
通过ThreadPoolExecutor类,可以构造出各种需求的线程池。底层是通过workQueue实现
实际应用中直接用静态类Executor
多个线程同时被阻塞,他们中的一个或者全部都在等待某个资源被释放。
由于线程被无限期地阻塞,因此程序不可能正常终止,最终导致死锁产生。
线程 A 持有资源 2,线程 B 持有资源 1,他们同时都想申请对方的资源,所以这两个线程就会因互相等待而进入死锁状态。
学过操作系统的朋友都知道,产生死锁必须具备以下四个条件:
同理,只要任意破坏产生死锁的四个条件中的其中一个就可以了:
破坏互斥条件
该条件没有办法破坏,因为用锁的意义本来就是想让他们互斥的(临界资源需要互斥访问)
破坏请求与保持条件
一次性申请所有的资源;
破坏不剥夺条件
占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源;
破坏循环等待条件
靠按序申请资源来预防。按某一顺序申请资源,释放资源则反序释放。
Object.wait() -挂起一个线程
Object.notify() -唤醒一个线程
唤醒是根据线程优先级来选择的
class Source {
public int count = 0;
public boolean flag = false; // 是否有数据
}
class Producer implements Runnable {
private Source source;
public Producer(Source source) {
this.source = source;
}
@Override
public void run() {
while (true) {
synchronized(source) {
if(!source.flag) {
source.count++;
source.flag = true;
System.out.println("生产商品:"+source.count);
source.notify(); // 唤醒另一线程
} else {
try {
source.wait(); // 等前线程等待
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
}
class Comsumer implements Runnable {
private Source source;
public Comsumer(Source source) {
this.source = source;
}
@Override
public void run() {
while (true) {
synchronized (source) {
if(source.flag) {
source.flag = false;
System.out.println("消费商品:"+source.count);
source.notify(); // 唤醒另一线程
} else {
try {
source.wait(); // 等前线程等待
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
}
public class MQTest {
public static void main(String[] args) {
Source source = new Source();
Producer producer = new Producer(source);
Comsumer comsumer = new Comsumer(source);
Thread t1 = new Thread(producer);
Thread t2 = new Thread(comsumer);
t1.start();
t2.start();
}
}
synchronized经过编译之后,对应的是class文件中的monitorenter和monitorexit这两个字节码指令。
这两个字节码对应的内存模型的操作是lock(上锁)和unlock(解锁)。因为这两个操作之间运行的都是原子的(这个操作保证了变量为一个线程独占的,也就是说只有获得锁的线程才能够操作被锁定的内存区域),所以synchronized也具有原子性。
这两个字节码都需要一个对象来作为锁。因此,
执行monitorenter字节码时, 如果这个对象没有被上锁,或者当前线程已经持有了该锁,那么锁的计数器会+1,
而在执行monitorexit字节码时,锁的计数器会-1,当计数器为0时,锁被释放。
如果获取对象的锁失败,那么该线程会被阻塞等待,直到之前把这个对象上锁的线程释放这个锁为止。
每个对象都有一个monitor(监视器)与之关联,所谓的上锁,就是获得对象的monitor的独占权(因为只用获得monitor才能访问这个对象)
执行monitorenter字节码的时候,线程就会尝试获得monitor的所有权,也就是尝试获得对象的锁
只有获得了monitor,才能进入同步块,或者执行同步方法
独占对象的本质是独占对象的monitor
此外,synchronized也具有可见性,因为它调用的unlock解锁这个操作规定,放开对某个变量的锁的之前,需要把这个变量从缓存更新到主内存,因此它也具有可见性
为什么synchronized无法禁止指令重排,却能保证有序性??
因为在一个线程内部,他不管怎么指令重排,他都是as if serial的
也就是说单线程即使重排序之后的运行结果和串行运行的结果是一样的,是类似串行的语义。
而当线程运行到同步块时,会加锁,其他线程无法获得锁,也就是说此时同步块内的方法是单线程的
根据as if serial,可以认为他是有序的
而指令重排序导致线程不安全是多线程运行的时候,不是单线程运行的时候
因此多线程运行时禁止指令重排序也可以实现有序性,这就是volatile。
保证了变量在多线程中的可见性、有序性、但不保证原子性
Volitle修饰的变量,在一个线程中被改变,会立刻通知总线,并通知其他线程
实现原理:如果使用这个修饰符,对该变量进行写操作之后,会立即执行store和write操作(对应的汇编代码中会加上一个lock前缀),立即将该变量从工作内存(或者说缓存)写入主内存,保证了对别的线程立即可见(因为这会导致别的线程的工作内存中该变量的缓存会失效),并且同时其他的cpu的工作内存中的值无效,直接从主内存读取并刷新工作内存。
第二个特征是禁止指令重排序优化,也就是保证volatile修饰的变量不会被指令重排序优化,从而保证代码的执行顺序和程序顺序相同,保证了有序性。
实现是当变量被声明为volatile时,通过在生成的字节码中插入“内存屏障”,来禁止特定类型的指令重排序(定义了很多情况下禁止指令重排序)。
举个例子:每个volatile变量在写操作之前会有一个“写写屏障”,这表示这个写操作之前的写操作和它禁止重排序,后面会有一个“写读屏障”,这表示这个写操作和后面的读操作不能重排序。
但volatile关键字不保证对变量操作的原子性(synchronized可以保证原子性)
比如i++操作,这不是一个原子操作,它包括四个字节码指令,首先把i放到操作数栈栈顶,然后把int类型1放到栈顶,两个出栈相加,再入栈;而如果相加之前别线程修改了i的值,栈顶的i就是过期的,会发生错误。因此线程不安全。
也因此,使用volatile而不会引起线程不安全的前提是:1、对该变量的运算不依赖于该变量的值,或者只有一个线程能修改该变量的值。2、变量不需要与其他状态变量共同参与不变约束。
使用ThreadLocal声明的变量
ThreadLocal num = new ThreadLocal
会在线程中维护一个map, key是这个ThreadLocal对象,value是所真正保存的值
可以保证当前线程获取的这个变量值一定是它自己的,不会获取到其他线程的
但是有个弊端,如果使用线程池的话, 线程处理完业务后并不会被销毁,而是放到线程池中会被再取出用于其他业务处理。虽然数据上不会取到之前的,但是之前的那个ThreadLocal对象一直在被引用没有被销毁掉,可能导致oom, 解决办法处理完业务后,最后要将ThreadLocal修饰的变量手动清除引用,比如赋值为null
csdn
concurrent包里面的CountDownLatch其实可以把它看作一个计数器,只不过这个计数器的操作是原子操作,同时只能有一个线程去操作这个计数器,也就是同时只能有一个线程去减这个计数器里面的值。
CountDownLatch的一个非常典型的应用场景是:有一个任务想要往下执行,但必须要等到其他的任务执行完毕后才可以继续往下执行。假如我们这个想要继续往下执行的任务调用一个CountDownLatch对象的await()方法,其他的任务执行完自己的任务后调用同一个CountDownLatch对象上的countDown()方法,这个调用await()方法的任务将一直阻塞等待,直到这个CountDownLatch对象的计数值减到0为止。
举一个我们项目中的例子:之前我们有个活动,月享实惠,根据每月消费总额之类的等等条件,去判断各个集卡模块是否达标
每个页面需要异步发十几个请求到我们后端,然后我们后端处理后还要再发请求到下游接口 去真正查询客户的达标状态
如果采用countDownLatch,一个线程等待收集其他线程的结果,然后再向下处理,这样就能减轻我们系统的并发压力
public class Worker implements Runnable{
private CountDownLatch downLatch;
private String name;
public Worker(CountDownLatch downLatch, String name){
this.downLatch = downLatch;
this.name = name;
}
public void run() {
this.doWork();
try
{
TimeUnit.SECONDS.sleep(new Random().nextInt(10));
}catch(InterruptedException ie){
}
System.out.println(this.name + "活干完了!");
this.downLatch.countDown();
}
private void doWork()
{
System.out.println(this.name + "正在干活!");
}
}
public class Boss implements Runnable {
private CountDownLatch downLatch;
public Boss(CountDownLatch downLatch){
this.downLatch = downLatch;
}
public void run() {
System.out.println("老板正在等所有的工人干完活......");
try {
this.downLatch.await();
} catch (InterruptedException e) {
}
System.out.println("工人活都干完了,老板开始检查了!");
}
}
public class CountDownLatchDemo {
public static void main(String[] args) {
ExecutorService executor = Executors.newCachedThreadPool();
CountDownLatch latch = new CountDownLatch(3);
Worker w1 = new Worker(latch,"张三");
Worker w2 = new Worker(latch,"李四");
Worker w3 = new Worker(latch,"王二");
Boss boss = new Boss(latch);
executor.execute(w3);
executor.execute(w2);
executor.execute(w1);
executor.execute(boss);
executor.shutdown();
}
}
Jdbc的连接过程:
Mybatis主要是封装了jdbc,对外提供了api接口,对内提供了接口的实现, 不同的厂商都基于这个接口规范,比如mysql,oracle,db2等驱动。
整体流程是这样的,xmlConfigBuilder将mybatis-config.xml中的配置信息封装成一个environment, xmlMapperBuilder将mapper.xml中的sql封装成mappedStatement这两个共同组成了configuration, sqlSessionFactionBuilder基于configuration创建sqlSessionFactory然后创建sqlSession
sqlSession中引用了executor,引用了statementHandler, paramterHandler, resultSetHandler, 执行语句注入参数处理结果集
servlet是一个java程序,面向请求和响应,生成动态的web
Servlet的框架是由两个Java包组成的:javax.servlet与javax.servlet.http
springMVC是以请求为驱动,围绕servlet设计
其核心是dispatcherServlet,它是一个Servlet
Spring是个轻量级的框架集合, 模块包括IOC,AOP,DAO,ORM,WEB,MVC
其中最主要的是IOC和AOP
IOC实现了bean的周期管理,缓存等
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初始化
解决依赖注入的条件
A先实例化,然后创建一个工厂,将工厂放到三级缓存,然后进行属性注入的时候需要依赖B
B再实例化, 属性注入的时候需要依赖A, 这时候去三级缓存中取工厂,通过工厂获取A的实例对象或者代理对象,然后放入二级缓存并清除三级缓存中的工厂, 然后初始化B并走完后续流程
然后A对象进行初始化,并将bean放入一级缓存并清除二级缓存,结束
Aop是基于动态代理来实现的, 在动态代理中通过invoke织入功能
动态代理有两种 1.jdk动态代理。2.cglib动态代理
区别是 jdk动态代理与目标类的关系是同级且组合关系, cglib与目标类的关系是子与父的继承关系
Myisam使用的是表锁,不支持高并发。
InnoDB使用的是行锁,支持高并发。
Myisam不支持外键。
InnoDB支持外键。
Myizam支持全文索引。
InnoDB不支持全文索引。不过可以通过中间件实现,比如Solr,ElasticSearch.
Myisam不支持事务,innoDB支持事务
Myisam使用的是非聚集索引,也就是树节点存储的是数据的指针(地址),当查找数据的时候,首先找到树节点相对应的指针,再根据指针去数据实际存储的位置查找真正的数据。(索引文件和数据文件是分离的)。
InnoDB使用的是聚集索引,也就是树节点实际存储的就是真实的数据,当查找数据的时候,查找到相对应的叶子结点对应的数据就结束了。(叶节点包含了完整的数据记录)
csdn
二叉树:
二叉查找树:
B树:
B+树在B树基础上做了增强:
B树和B+树一般是应用于文件系统或者数据库系统中,用于减少磁盘IO带来的磁盘损耗机制
以Mysql中innoDB为例,当我们通过select语句查询一条数据的时候,innoDB需要从磁盘上读取数据
而这个过程涉及到磁盘IO和磁盘随机IO,性能比较低
系统会把数据的逻辑地址传给磁盘,磁盘控制电路按照寻址的逻辑把逻辑地址翻译成物理地址,也就是确定数据在哪个磁道,哪个扇区
为了读取这个扇区的数据需要把磁头放到这个扇区上面,磁盘会不断的旋转,把目标的扇区旋转到磁头下面,使得磁头能够找到对应的磁道
涉及到寻道的时间和旋转时间的损耗,所以在查询数量比较多的情况下,磁盘IO的性能损耗是非常大的
所以在InnoDB里干脆对存储在磁盘上的数据建立一个索引,然后把索引数据以及索引列对应的磁盘地址以B+树的方式进行存储
由于B+树的分支比较多,只需要较少次数的磁盘IO就能查到目标数据
AVL树高度比B树或者B+树高度更高,而高度就意味着磁盘IO的数量,所以文件系统或数据库才会选择B树或者B+树
索引是帮助MySQL高效获取数据的数据结构(快速查找排好序的数据结构)
MySQL索引的二种结构:b+Tree索引和hash索引
索引其实也是一张表,该表保存了主键和索引的字段,并指向实体表的记录
索引的缺点:索引提高了查询速度,但是降低了更新表的速度,更新表的时候,MySQL会保存数据还需要保存一个索引文件,建立索引会占用磁盘空间。
FullText、hash、BTREE、RTREE
普通索引,唯一索引,组合索引,全文索引
根本区别:聚集索引和非聚集索引区别在 表记录排列的顺序和索引的排列顺序是否一致
优缺点:
聚集索引:
非聚集索引:
聚集索引和非聚集索引的存储区别?
哈希索引就是采用一定的哈希算法,只需一次哈希算法即可立刻定位到相应的位置,速度比较快(实质:利用哈希值进行快速的定位)
哈希索引的缺点:
MySQL的常用的innodb引擎中使用的是b+tree的索引
比如要查询key为18到49的所有数据记录,当找到18的时候,只需要顺着节点和指针顺序遍历就可以一次性访问到所有的数据节点,极大的提高区间查询的效率(无需返回到上层父节点重复遍历查找减少IO操作)
当多条件联合查询时,优化器会评估哪一个条件的索引效率高,会选择最佳的索引去使用。
因此:多个单列索引在条件查询时候优化器会选择最优索引策略,可能只用一个索引,也可能将多个索引都用上。
多个索引的底层都是建立在多个B+TREE,比较占用空间,也就是会浪费搜索效率,所以多条件联合查询最好建联合索引,
联合索引:遵循左匹配原则,最左优先,以最左边的为起点任何连续的索引都会匹配上,如果出现不连续,就会出现不匹配
联合索引的失效情况?
相对其他数据库而言,MySQL的锁机制比较简单,其最显著的特点是不同的存储引擎支持不同的锁机制。
比如,MyISAM和MEMORY存储引擎采用的是表级锁(table-level locking);
BDB存储引擎采用的是页面锁(page-level locking),但也支持表级锁;
InnoDB存储引擎既支持行级锁(row-level locking),也支持表级锁,但默认情况下是采用行级锁。
锁 | 开销 | 加锁速度 | 死锁 | 粒度 | 并发性能 |
---|---|---|---|---|---|
表级锁 | 开销小 | 加锁快 | 不会出现死锁 | 锁定粒度大 | 发生锁冲突的概率最高,并发度最低。 |
行级锁 | 开销大 | 加锁慢 | 会出现死锁 | 锁定粒度最小,发生锁冲突的概率最低,并发度也最高。 | |
页面锁 | 开销界于表锁和行锁之间 | 加锁时间界于表锁和行锁之间 | 会出现死锁 | 锁定粒度界于表锁和行锁之间 | 并发度一般。 |
锁(lock)在多人处理同一个数据的时候,保证每次只有一个人可以操作。
MySQL提供了页锁(全局锁)、行锁、表锁。其中innodb采用的是行锁和表锁,myisam只支持表锁。
是指二个或者二个以上的进程在执行时候,因为争夺资源造成相互等待的现象,进程一直处于等待中,无法得到释放,这种状态就叫做死锁,
批量入库,存在则更新,不存在则插入,insert into tab(xx,xx) on duplicate key update xx=‘xx’
为了在单个innodb表上执行多个并发写入操作时避免死锁,可以在事务开始时,通过为预期要修改行,使用select …for update语句来获取必要的锁,即使这些行的更改语句是在之后才执行的
在事务中,如果要更新记录,应该直接申请足够级别的锁,即排他锁,而不应先申请共享锁,更新时在申请排他锁。因为这时候当用户在申请排他锁时,其他事务可能又已经获得了相同记录的共享锁,
如果事务需要修改或锁定多个表,则应在每个事务中以相同的顺序使用加锁语句。在应用中,如果不同的程序会并发获取多个表,应尽量约定以相同的顺序来访问表,这样可以大大降低产生死锁的机会。
通过 select …lock in share mode获取行的读锁后,如果当前事务在需要对该记录进行更新操作,则很有可能造成死锁;
改变事务隔离级别
innodb默认是使用设置死锁时间来让死锁超时的策略,默认innodblockwait_timeout设置的时长是50s
设置innodbdeadlockdetect设置为on可以主动检测死锁,在innodb中这个值默认就是on开启的状态
全局锁就是对整个数据库实例加锁,它的典型使用场景就是做全库逻辑备份,这个命令可以使用整个库处于只读状态,使用该命令之后,数据更新语句,数据定义语句,更新类事务的提交语句等操作都会被阻塞。
如果在主库备份,在备份期间不能更新,业务停止,所以更新业务会处于等待状态
如果在从库备份,在备份期间不能执行主库同步的binlog,导致主从延迟
如果使用全局锁进行逻辑备份都会让整个库成为只读状态,解决办法需要使用MySQLdump时,使用参数-single-transaction就会在导入数据之前启动一个事务来保证数据的一致性,并且这个过程是支持数据更细操作的。
行锁是MySQL中粒度最小的一种锁,innodb的行锁有共享锁和排他锁两种
共享锁允许事务读一行记录,不允许任何线程对行记录进行修改操作
排他锁允许当前事务删除或者更新一行记录,其他线程不能操作该记录。
共享锁是读取操作创建的锁。其他用户可以并发读取数据,但是任何事务都不能对数据进行修改(获取数据上的排他锁),直到已释放所有共享锁。当如果事务对读锁进行修改操作,很可能会操作死锁。
排他锁又称写锁
事务对某一行加上排他锁,只能这个事务对其进行读写操作,在事务结束以前,其他事务不能对其进行加任何锁,其他进程可以读取,但是不能进行写操作,需要等待释放以后。排他锁是悲观锁的一种实现。
事务1对数据对象A加上排他锁,事务1可以读写A,其他事务就不能在对A进行任何加锁操作,直到事务1释放A上的锁,这保证其他事务在事务1释放A上的锁之前不能在读取和修改A,排他锁会阻塞所有的排他锁和共享锁。
悲观锁:每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据都会block直到它拿到锁。因此,悲观锁需要耗时比较的多,跟乐观锁比较,悲观锁是有数据库自己实现的,用的时候我们直接调用数据的相关语句就可以
悲观锁涉及到的另两个锁,他们是共享锁和排他锁,共享锁和排他锁时悲观锁的不同的实现,属于悲观锁的范畴。
乐观锁是用数据版本记录机制实现,这是乐观锁最常用的方式
所谓的数据版本,为数据增加一个版本号的字段,一般是通过为数据表增加一个数据类型的version字段实现,当读取数据时,将把二十年字段的值一同读取出来,数据每次更新都需要对version值加一,在我们提交更新的时候,判断数据表对应记录的当前版本信息与第一次取出来的version值进行对比,如果数据库的表当前版本号鱼取出来的version值相等,则给与更新否则认为过期数据不给与更新。
乐观锁虽然叫锁其实在使用的时候是没有加锁,所以执行性能高。
缺点:会产生ABA的问题,ABA问题指的是有一个变量V初次读取的时候是A值,并且在准备赋值的时候检查到他的值还是为A值,会误认为没有被修改做为正常的执行修改操作,实际上这段时间他的值可能被修改为其他值,之后又被修改为A值,
1.record lock—单个行记录上的锁
2.gap lock — 间隙锁,锁定一个氛围,不包括记录本身
3.next-key lock–锁定一个范围,包含记录本身
读写分离,基本的原理是让主数据库处理事务性增、改、删操作(INSERT、UPDATE、DELETE),而从数据库处理SELECT查询操作。
数据库复制被用来把事务性操作导致的变更同步到集群中的从数据库。
读写分离就是在主服务器上修改,数据会同步到从服务器,从服务器只能提供读取数据,不能写入,实现备份的同时也实现了数据库性能的优化,以及提升了服务器安全。
因为数据库的“写”(写10000条数据到oracle可能要3分钟)操作是比较耗时的。
但是数据库的“读”(从oracle读10000条数据可能只要5秒钟)。
所以读写分离,解决的是,数据库的写入,影响了查询的效率。
数据库不一定要读写分离,如果程序使用数据库较多时,而更新少,查询多的情况下会考虑使用,利用数据库 主从同步
可以减少数据库压力,提高性能。当然,数据库也有其它优化方案。memcache 或是 表折分,或是搜索引擎。都是解决方法。
在每个事务更新数据完成之前,master在二进制日志记录这些改变。写入二进制日志完成后,master通知存储引擎提交事务。
Slave将master的binary log复制到其中继日志
Sql slave thread(sql从线程)处理该过程的最后一步,
在代码中根据select 、insert进行路由分类,这类方法也是目前生产环境下应用最广泛的。优点是性能较好,因为程序在代码中实现,不需要增加额外的硬件开支,缺点是需要开发人员来实现,运维人员无从下手。
代理一般介于应用服务器和数据库服务器之间,代理数据库服务器接收到应用服务器的请求后根据判断后转发到,后端数据库,有以下代表性的程序。
(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语言进行开发,阿里巴巴集团将其用户生产环境下,但是他并不支持事物以及存储过程。
常用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。
复制原理
哨兵模式(sentinel)
反客为主的自动版,能够后台监控Master库是否故障,如果故障了根据投票数自动将slave库转换为主库。一组sentinel能同时监控多个Master。
使用步骤:
从理论上来说,给缓存设置过期时间,是保证最终一致性的解决方案。这种方案下,我们可以对存入缓存的数据设置过期时间,所有的写操作以数据库为准,对缓存操作只是尽最大努力即可。也就是说如果数据库写成功,缓存更新失败,那么只要到达过期时间,则后面的读请求自然会从数据库中读取新值然后回填缓存。因此,接下来讨论的思路不依赖于给缓存设置过期时间这个方案。
三种策略可供参考
举例:同时有请求A和请求B进行更新操作,那么会出现
这就出现请求A更新缓存应该比请求B更新缓存早才对,但是因为网络等原因,B却比A更早更新了缓存。这就导致了脏数据,因此不考虑。
该方案会导致不一致的原因是
同时有一个请求A进行更新操作,另一个请求B进行查询操作
那么会出现如下情形:
上述情况就会导致不一致的情形出现。而且,如果不采用给缓存设置过期时间策略,该数据永远都是脏数据。
那么,如何解决上面这种情况呢?采用延时双删策略
这么做,可以将1秒内所造成的缓存脏数据,再次删除。
那么,这个1秒怎么确定的,具体该休眠多久呢?
针对上面的情形,应该自行评估自己的项目的读数据业务逻辑的耗时。
然后写数据的休眠时间则在读数据业务逻辑的耗时基础上,加几百ms即可。这么做的目的,就是确保读请求结束,写请求可以删除读请求造成的缓存脏数据。
杠精:这种策略也有极端情况
并发情况,假设这会有两个请求,一个请求A做查询操作,一个请求B做更新操作,那么会有如下情形产生
但是这种情况概率很低,因为先天条件是数据库 写 操作 比 读 操作 慢
,这一点很难发生
数据库行锁
kafka异步执行数据库操作,redis控制库存数量,利用redis的incr和decr的原子性
分布式锁(效率差)
解决雪崩: 限流、降级、熔断、缓存备份
解决穿透:业务层面查询到null,也在redis中存null,以保证请求不再向后面服务发送
1、原子性:
一个事务(transaction)中的所有操作,要么全部完成,要么全部不完成,不会结束在中间某个环节。
事务在执行过程中发生错误,会被恢复(Rollback)到事务开始前的状态,就像这个事务从来没有执行过一样。
2、一致性:
在事务开始之前和事务结束以后,数据库的完整性没有被破坏。这表示写入的资料必须完全符合所有的预设规则,这包含资料的精确度、串联性以及后续数据库可以自发性地完成预定的工作。
3、隔离性:
数据库允许多个并发事务同时对其数据进行读写和修改的能力,隔离性可以防止多个事务并发执 行时由 于 交 叉 执 行而导致数据 的不一致 。 事务隔离分为不同级别,包括读未 提 交 ( Readuncommitted)、读提交(read committed)、可重复读(repeatable read)和串行化(Serializable)。
4、持久性:
事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失。
脏读是指一个事务在处理数据的过程中,读取到另一个为提交事务的数据。
不可重复读是指对于数据库中的某个数据,一个事务范围内的多次查询却返回了不同的结果,这是由于在查询过程中,数据被另外一个事务修改并提交了
幻读是事务非独立执行时发生的一种现象。
例如事务T1对一个表中所有的行的某个数据项做了从“1”修改为“2”的操作,这时事务T2又对这个表中插入了一行数据项,而这个数据项的数值还是为“1”并且提交给数据库。而操作事务T1的用户如果再查看刚刚修改的数据,会发现还有一行没有修改,其实这行是从事务T2中添加的,就好像产生幻觉一样,这就是发生了幻读。
幻读和不可重复读都是读取了另一条已经提交的事务(这点就脏读不同),所不同的是不可重复读查询的都是同一个数据项,而幻读针对的是一批数据整体(比如数据的个数)
有七大传播行为,也是在TransactionDefinition接口中定义。
Spring实现编程式事务,依赖于2大类,PlatformTransactionManager,与模版类TransactionTemplate(推荐使用)
声明式事务实现方式, Spring的tx:advice定义事务通知与AOP相关配置实现,另为一种通过@Transactional
因为事务是作用于数据库。例如使用MySQL且引擎是MyISAM,则事务会不起作用,因为MyISAM引擎本身不支持事务;如果改成InnoDB,则可以。
因为Spring的事务是基于AOP,所以如果Service类没有被Spring管理,变成一个Spring Bean,即使添加了@Transactional注解,事务也是无效的。
不带事务的方法调用该类中带事务的方法,不会回滚。因为Spring的回滚是用过代理模式生成的,如果是一个不带事务的方法调用该类的带事务的方法,直接通过this.xxx()调用,而不生成代理事务,所以事务不起作用。常见解决方法“拆类”
spring的事务默认是对RuntimeException进行回滚,而不继承RuntimeException的不回滚
因为在java的设计中,它认为不继承RuntimeException的异常是CheckException或普通异常,如IOException,这些异常在java语法中是要求强制处理的。
对于这些普通异常,Spring默认它们都已经处理,所以默认不回滚。可以添加rollbackfor=Exception.class来表示所有的Exception都回滚
@Transactional注解只能应用于public方法,如果你在protected、private或者默认可见性的方法上使用 @Transactional 注解,这将被忽略,也不会抛出任何异常。
一般的实现形式:所有的事务一阶段执行sql不提交 ,都成功之后TC通知所有事务进行二阶段主动提交,如果有一个失败TC通知所有事务进行二阶段回滚
一阶段:协调器问“你们几个子事务参与者对应的活能不能干成?" 子事务参与者们一一回复“能干/干不成”
二阶段:协调器问根据子事务参与者们的反馈如果都能干则告诉所有人都去干吧,如果有人说干不了,特通知大家别干了
try阶段所有参与者进行尝试提交业务(eg:创建订单的订单状态是CREATING,减库存虽然进行了100-2=98,但是会记录本次有2个冻结中的库存,等类似try操作);
Confirm阶段 如果try阶段的执行都成功了则TM通知所有参与者执行真正的提交(eg:创建订单的订单状态改为CREATED,减库存 被冻结的2个库存直接删掉,等类似Confirm操作【因为用网络超时等原因可能会有重复的调用所有要求支持幂等性】);
cancel阶段 如果try中有一个执行失败则TM通知所有参与者进行补偿操作(eg:创建订单的订单状态改为CANCELED,减库存中被冻结的2重新加回到数据库中,等类似cancel操作
幂等性:就是用户对于同一操作发起的一次请求或者多次请求的结果是一致的,不会因为多次点击而产生了副作用。
举个最简单的例子,那就是支付,用户购买商品后支付,支付扣款成功,但是返回结果的时候网络异常,此时钱已经扣了,用户再次点击按钮,此时会进行第二次扣款,返回结果成功,用户查询余额发现多扣钱了,流水记录也变成了两条。在以前的单应用系统中,我们只需要把数据操作放入事务中即可,发生错误立即回滚,但是再响应客户端的时候也有可能出现网络中断或者异常等等。
使用uuid生成一个防重令牌token,并把token放到redis里,然后把这个token,封装到出参,给到前端的订单确定页面
反向代理
正向代理
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;
}
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的ack机制:在kafka发送数据的时候,每次发送消息都会有一个确认反馈机制,确保消息正常的能够被收到。
通过offset commit 来保证数据的不丢失,kafka自己记录了每次消费的offset数值,下次继续消费的时候,接着上次的offset进行消费即可
kafka只保证单partition有序,如果Kafka要保证多个partition有序,不仅broker保存的数据要保持顺序,消费时也要按序消费。
假设partition1堵了,为了有序,那partition2以及后续的分区也不能被消费,这种情况下,Kafka 就退化成了单一队列,毫无并发性可言,极大降低系统性能。因此Kafka使用多partition的概念,并且只保证单partition有序。这样不同partiiton之间不会干扰对方。
实现方式:1个Topic(主题)只创建1个Partition(分区),这样生产者的所有数据都发送到了一个Partition(分区),保证了消息的消费顺序。
比如3个直播间同时发消息,全局顺序就是保证直播间1先发的消息那么一定先到
实现方式:生产者在发送消息的时候指定要发送到哪个Partition(分区)(1个)。
比如3个直播间同时发消息,局部顺序就是直播间1先发,直播间2后发,但是可能直播间2的消息先到,这个顺序是不保证的。但是直播间1先发了“消息1”,再发了“消息2”,这个顺序是能保证的,也就是在直播间内是有序的,但是直播间之间的消息顺序不能保证。
消费者以组的名义订阅topic,topic下有多个partition,消费者组中有多个消费者实例。
同一时刻,一条消息只能被组中的一个消费者实例消费。
如果按照从属关系来说的话就是,主题下的每个分区只从属于组中的一个消费者,不可能出现组中的两个消费者负责同一个分区。消息就是存储在partition中。
AMQP(Advanced Message Queuing Protocol,高级消息队列协议)是一个进程间传递异步消息的网络协议。
发布者(Publisher)发布消息(Message),经由交换机(Exchange)。
交换机根据路由规则将收到的消息分发给与该交换机绑定的队列(Queue)。
最后 AMQP 代理会将消息投递给订阅了此队列的消费者,或者消费者按照需求自行获取。
zookeeper 是一个分布式的,开放源码的分布式应用程序协调服务,是 google chubby 的开源实现,是 hadoop 和 hbase 的重要组件。它是一个为分布式应用提供一致性服务的软件,提供的功能包括:配置维护、域名服务、分布式同步、组服务等。
zookeeper的核心是原子广播,这个机制保证了各个server 之间的同步。
实现这个机制的协议叫做 zab 协议。
zab 协议有两种模式,分别是恢复模式(选主)和广播模式(同步)。
当服务启动或者在领导者崩溃后,zab 就进入了恢复模式,当领导者被选举出来,且大多数 server 完成了和 leader 的状态同步以后,恢复模式就结束了。
状态同步保证了 leader 和 server 具有相同的系统状态。
http://c.biancheng.net/design_pattern/
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 工作在传输层。
手机能够使用联网功能是因为手机底层实现了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协议即超文本传送协议(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服务器发送被请求的信息给客户端。