零碎的知识点-4

零碎的知识点-4


select、poll、epoll之间的区别总结

select、poll、epoll都是IO多路复用的机制。I/O多路复用就通过一种机制,可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作。但select,poll,epoll本质上都是同步I/O,因为他们都需要在读写事件就绪后自己负责进行读写,也就是说这个读写过程是阻塞的,而异步I/O则无需自己负责进行读写,异步I/O的实现会负责把数据从内核拷贝到用户空间。

select的几大缺点:

  1. 每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大
  2. 同时每次调用select都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很大
  3. select支持的文件描述符数量太小了,默认是1024

poll实现

poll的实现和select非常相似,只是描述fd集合的方式不同,poll使用pollfd结构而不是select的fd_set结构,相比select模型,poll使用链表保存文件描述符,因此没有了监视文件数量的限制,但其他三个缺点依然存在。

epoll (事件驱动)

epoll既然是对select和poll的改进,就应该能避免上述的三个缺点。那epoll都是怎么解决的呢?在此之前,我们先看一下epoll和select和poll的调用接口上的不同,select和poll都只提供了一个函数——select或者poll函数。而epoll提供了三个函数,epoll_create,epoll_ctl和epoll_wait,epoll_create是创建一个epoll句柄;epoll_ctl是注册要监听的事件类型;epoll_wait则是等待事件的产生。

总结:

  1. select,poll实现需要自己不断轮询所有fd集合,直到设备就绪,期间可能要睡眠和唤醒多次交替。而epoll其实也需要调用 epoll_wait不断轮询就绪链表,期间也可能多次睡眠和唤醒交替,但是它是设备就绪时,调用回调函数,把就绪fd放入就绪链表中,并唤醒在 epoll_wait中进入睡眠的进程。虽然都要睡眠和交替,但是select和poll在“醒着”的时候要遍历整个fd集合,而epoll在“醒着”的 时候只要判断一下就绪链表是否为空就行了,这节省了大量的CPU时间。这就是回调机制带来的性能提升。

  2. select,poll每次调用都要把fd集合从用户态往内核态拷贝一次,并且要把current往设备等待队列中挂一次,而epoll只要 一次拷贝,而且把current往等待队列上挂也只挂一次(在epoll_wait的开始,注意这里的等待队列并不是设备等待队列,只是一个epoll内 部定义的等待队列)。这也能节省不少的开销。


Top K 问题

堆排解法

堆排序利用的大(小)堆顶所有节点元素都比父节点小(大)的性质来实现的。
既然一个大顶堆的顶是最大的元素,那我们要找最小的K个元素,是不是可以先建立一个包含K个元素的堆,然后遍历集合,如果集合的元素比堆顶元素小(说明它目前应该在K个最小之列),那就用该元素来替换堆顶元素,同时维护该堆的性质,那在遍历结束的时候,堆中包含的K个元素是不是就是我们要找的最小的K个元素?

实现:

在堆排的基础上,稍作了修改,buildHeap和heapify函数都是一样的实现。
速记口诀:最小的K个用最大堆,最大的K个用最小堆。

public class TopK {

    public static void main(String[] args) {
        // TODO Auto-generated method stub
        int[] a = { 1, 17, 3, 4, 5, 6, 7, 16, 9, 10, 11, 12, 13, 14, 15, 8 };
        int[] b = topK(a, 4);
        for (int i = 0; i < b.length; i++) {
            System.out.print(b[i] + ", ");
        }
    }

    public static void heapify(int[] array, int index, int length) {
        int left = index * 2 + 1;
        int right = index * 2 + 2;
        int largest = index;
        if (left < length && array[left] > array[index]) {
            largest = left;
        }
        if (right < length && array[right] > array[largest]) {
            largest = right;
        }
        if (index != largest) {
            swap(array, largest, index);
            heapify(array, largest, length);
        }
    }

    public static void swap(int[] array, int a, int b) {
        int temp = array[a];
        array[a] = array[b];
        array[b] = temp;
    }

    public static void buildHeap(int[] array) {
        int length = array.length;
        for (int i = length / 2 - 1; i >= 0; i--) {
            heapify(array, i, length);
        }
    }

    public static void setTop(int[] array, int top) {
        array[0] = top;
        heapify(array, 0, array.length);
    }

    public static int[] topK(int[] array, int k) {
        int[] top = new int[k];
        for (int i = 0; i < k; i++) {
            top[i] = array[i];
        }
        //先建堆,然后依次比较剩余元素与堆顶元素的大小,比堆顶小的, 说明它应该在堆中出现,则用它来替换掉堆顶元素,然后沉降。
        buildHeap(top);
        for (int j = k; j < array.length; j++) {
            int temp = top[0];
            if (array[j] < temp) {
                setTop(top, array[j]);
            }
        }
        return top;
    }
}

快排解法

用快排的思想来解Top K问题,必然要运用到”分治”结果的使用上。我们知道,分治函数会返回一个position,在position左边的数都比第position个数小,在position右边的数都比第position大。我们不妨不断调用分治函数,直到它输出的position = K-1,此时position前面的K个数(0到K-1)就是要找的前K个数。

实现:

“分治”还是原来的那个分治,关键是getTopK的逻辑

public class TopK {

    public static void main(String[] args) {
        // TODO Auto-generated method stub
        int[] array = { 9, 3, 1, 10, 5, 7, 6, 2, 8, 0 };
        getTopK(array, 4);
        for (int i = 0; i < array.length; i++) {
            System.out.print(array[i] + ", ");
        }
    }

    // 分治
    public static int partition(int[] array, int low, int high) {
        if (array != null && low < high) {
            int flag = array[low];
            while (low < high) {
                while (low < high && array[high] >= flag) {
                    high--;
                }
                array[low] = array[high];
                while (low < high && array[low] <= flag) {
                    low++;
                }
                array[high] = array[low];
            }
            array[low] = flag;
            return low;
        }
        return 0;
    }

    public static void getTopK(int[] array, int k) {
        if (array != null && array.length > 0) {
            int low = 0;
            int high = array.length - 1;
            int index = partition(array, low, high);
            //不断调整分治的位置,直到position = k-1
            while (index != k - 1) {
                //大了,往前调整
                if (index > k - 1) {
                    high = index - 1;
                    index = partition(array, low, high);
                }
                //小了,往后调整
                if (index < k - 1) {
                    low = index + 1;
                    index = partition(array, low, high);
                }
            }
        }
    }
}

CAP 理论 、BASE思想, 最终一致性和五分钟原则

CAP理论

  • C : Consisitency 一致性
  • A: Availablity 可用性(指得事快速获取数据)
  • P: Tolerance of network Partition 分区容忍性(分布式)

定理:任何分布式系统只可同时满足二点,没法三者兼顾。三者只能选其二。

  • CA: 传统关系数据库
  • AP:key-value 数据库

ACID

关系数据库的ACID模型拥有 高一致性 + 可用性 很难进行分区:

  • Atomicity原子性:一个事务中所有操作都必须全部完成,要么全部不完成。
  • Consistency一致性: 在事务开始或结束时,数据库应该在一致状态。
  • Isolation隔离层: 事务将假定只有它自己在操作数据库,彼此不知晓。
  • Durability: 一旦事务完成,就不能返回。

跨数据库两段提交事务:2PC (two-phase commit), 2PC is the anti-scalability pattern (Pat Helland) 是反可伸缩模式的,JavaEE中的JTA事务可以支持2PC。因为2PC是反模式,尽量不要使用2PC,使用BASE来回避。

BASE思想

BASE模型反ACID模型,完全不同ACID模型,牺牲高一致性,获得可用性或可靠性:

  • Basically Available基本可用。支持分区失败(e.g. sharding碎片划分数据库)
  • Soft state软状态 状态可以有一段时间不同步,异步。
  • E
  • ventually consistent最终一致,最终数据是一致的就可以了,而不是时时高一致。

最终一致性

过程松,结果紧,最终结果必须保持一致性 。(DNS系统)

  • 强一致性 任何时刻,所有节点中的数据是一样的。用一时间点,你在节点A中获取到key1的值与节点B中获取到key1的值应该都是一样的。
  • 弱一致性 包含多种不同的实现,目前分布式系统中广泛实现的是最终一致性。(其他还有因果一致性)

I/O的五分钟法则

如果一个数据的访问周期在5分钟以内则存放在内存中,否则应该存放在硬盘中。


Spring 生命周期

对于普通的Java对象,当new的时候创建对象,当它没有任何引用的时候被垃圾回收机制回收。而由Spring IoC容器托管的对象,它们的生命周期完全由容器控制。Spring中每个Bean的生命周期如下:
Spring生命周期

  1. 实例化Bean
    对于BeanFactory容器,当客户向容器请求一个尚未初始化的bean时,或初始化bean的时候需要注入另一个尚未初始化的依赖时,容器就会调用createBean进行实例化。 对于ApplicationContext容器,当容器启动结束后,便实例化所有的bean。 容器通过获取BeanDefinition对象中的信息进行实例化。并且这一步仅仅是简单的实例化,并未进行依赖注入。 实例化对象被包装在BeanWrapper对象中,BeanWrapper提供了设置对象属性的接口,从而避免了使用反射机制设置属性。
  2. 设置对象属性(依赖注入)
    实例化后的对象被封装在BeanWrapper对象中,并且此时对象仍然是一个原生的状态,并没有进行依赖注入。 紧接着,Spring根据BeanDefinition中的信息进行依赖注入。 并且通过BeanWrapper提供的设置属性的接口完成依赖注入。
  3. 注入Aware接口
    紧接着,Spring会检测该对象是否实现了xxxAware接口,并将相关的xxxAware实例注入给bean,包括包括了BeanNameAware、BeanFactoryAware,ApplicationContextAware接口。
  4. BeanPostProcessor
    当经过上述几个步骤后,bean对象已经被正确构造,但如果你想要对象被使用前再进行一些自定义的处理,就可以通过BeanPostProcessor接口实现。 该接口提供了两个函数:postProcessBeforeInitialzation( Object bean, String beanName ) 当前正在初始化的bean对象会被传递进来,我们就可以对这个bean作任何处理。 这个函数会先于InitialzationBean执行,因此称为前置处理。 所有Aware接口的注入就是在这一步完成的。postProcessAfterInitialzation( Object bean, String beanName ) 当前正在初始化的bean对象会被传递进来,我们就可以对这个bean作任何处理。 这个函数会在InitialzationBean完成后执行,因此称为后置处理。
  5. InitializingBean与init-method
    当BeanPostProcessor的前置处理完成后就会进入本阶段。 InitializingBean接口只有一个函数:afterPropertiesSet()这一阶段也可以在bean正式构造完成前增加我们自定义的逻辑,但它与前置处理不同,由于该函数并不会把当前bean对象传进来,因此在这一步没办法处理对象本身,只能增加一些额外的逻辑。 若要使用它,我们需要让bean实现该接口,并把要增加的逻辑写在该函数中。然后Spring会在前置处理完成后检测当前bean是否实现了该接口,并执行afterPropertiesSet函数。当然,Spring为了降低对客户代码的侵入性,给bean的配置提供了init-method属性,该属性指定了在这一阶段需要执行的函数名。Spring便会在初始化阶段执行我们设置的函数。init-method本质上仍然使用了InitializingBean接口。
  6. DisposableBean
    和destroy-method和init-method一样,通过给destroy-method指定函数,就可以在bean销毁前执行指定的逻辑。

Spring容器中Bean的作用域

当通过Spring容器创建一个Bean实例时,不仅可以完成Bean实例的实例化,还可以为Bean指定特定的作用域。Spring支持如下5种作用域:

  • singleton:单例模式,在整个Spring IoC容器中,使用singleton定义的Bean将只有一个实例
  • prototype:原型模式,每次通过容器的getBean方法获取prototype定义的Bean时,都将产生一个新的Bean实例
  • request:对于每次HTTP请求,使用request定义的Bean都将产生一个新实例,即每次HTTP请求将会产生不同的Bean实例。只有在Web应用中使用Spring时,该作用域才有效
  • session:对于每次HTTP Session,使用session定义的Bean豆浆产生一个新实例。同样只有在Web应用中使用Spring时,该作用域才有效
  • globalsession:每个全局的HTTP Session,使用session定义的Bean都将产生一个新实例。典型情况下,仅在使用portlet context的时候有效。同样只有在Web应用中使用Spring时,该作用域才有效

其中比较常用的是singleton和prototype两种作用域。
对于singleton作用域的Bean,每次请求该Bean都将获得相同的实例。容器负责跟踪Bean实例的状态,负责维护Bean实例的生命周期行为;
如果一个Bean被设置成prototype作用域,程序每次请求该id的Bean,Spring都会新建一个Bean实例,然后返回给程序。在这种情况下,Spring容器仅仅使用new 关键字创建Bean实例,一旦创建成功,容器不在跟踪实例,也不会维护Bean实例的状态。

如果不指定Bean的作用域,Spring默认使用singleton作用域。


SpringMVC执行流程

组件介绍:

  • DispatcherServlet
    前端控制器,作用就是接收请求,响应结果,相当于转发器
  • HandleMapping
    处理器映射器,作用就是根据请求的URL查找Handler
  • HandlerAdapter
    处理器适配器,作用就是按照特定的规则去执行Handler,也就是开发Handler时需要满足HandlerAdapter的规则,这样HandlerAdapter才能执行Handler。
  • View resolver
    视图解析器,作用根据逻辑视图解析成真正的视图(view)
  • view
    视图,是一个接口,其实现类能支持不同的view类型,jsp、freemarker、Excel等

执行过程文字描述

  1. 用户发起请求到前端控制器DispatcherServlet

  2. DispatcherServlet请求处理器映射器HandlerMapping查找Handler
    可以是根据xml查找,也可以是根据注解查找

  3. HandlerMappingDispatcherServlet返回一个执行链HandlerExecutionChain,包含HandlerInterceptionHandler

  4. HandlerMapping调用处理器适配器HandlerAdapter去执行Handler

  5. 处理器适配器去执行Handler

  6. Handler执行完给处理器适配器返回ModelAndView
    ModelAndView是SpringMVC框架的一个底层对象,包括Model和View

  7. 处理器适配器给DispatcherServlet返回ModelAndView

  8. DispatcherServlet请求视图解析器View resolver进行视图解析
    根据逻辑视图解析成真正的物理视图(jsp等)

  9. 视图解析器向DispatcherServlet返回view

  10. DispatcherServlet进行视图渲染

  11. DispatcherServlet向用户响应结果

SpringMVC执行流程


Java内存模型与共享变量可见性

Java内存模型(JMM)

Java内存模型的主要目标:定义在虚拟机中将变量储存到内存和从内存中取出变量这样的底层细节。
注意:上边的变量指的是共享变量(实例字段、静态字段、数组对象元素),不包括线程私有变量(局部变量、方法参数),因为私有变量不会存在竞争关系。

JMM

说明:

  • 所有共享变量存于主内存
  • 每一条线程都有自己的工作内存(就是上图所说的本地内存)
  • 工作内存中保存了被该线程使用到的变量的主内存副本

注意:

  • 线程对变量的操作都要在工作内存中进行,不能直接操作主内存
  • 不同的线程之间无法直接访问对方的工作内存中的变量
  • 不同线程之间的变量的传递必须通过主内存

8条内存屏障指令

  • lock(锁定): 作用于主内存,把一个变量标识为一条线程独占的状态
  • unlock(解锁): 作用于主内存,把一个处于锁定的变量解锁。
  • use(使用): 作用于工作内存的变量,把工作内存中一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用到变量的值的字节码指令时将会执行这个操作。
  • assign(赋值): 作用于工作内存的变量,它把一个从执行引擎接收到的值赋给工作内存的变量,每当虚拟机遇到一个变量复制的字节码指令时执行只个操作。

下表的四条是与volatile实现内存的可见性直接相关的四条(store、write、read、load)

  • store: 把工作内存中的变量的值传到主内存变量中。
  • write: 把store操作从工作内存中得到的变量放到主内存的变量中
  • read: 把一个变量的值从主内存中传输到线程的工作内存
  • load: 把read操作从主内存中获取到的变量放到工作内存的变量中去

注意:

  • 一个变量在同一时刻只允许一条线程对其进行lock操作
  • lock操作会将该变量在所有线程工作内存中的变量副本清空,否则就起不到锁的作用了
  • lock操作可被同一条线程多次进行,lock几次,就要unlock几次(可重入锁)
  • unlock之前必须先执行store-write
  • store-write必须成对出现(工作内存–>主内存)
  • read-load必须成对出现(主内存–>工作内存)

内存访问重排序与Java内存模型

根据Java内存模型中的规定,可以总结出以下几条happens-before规则。Happens-before的前后两个操作不会被重排序且后者对前者的内存可见。

  • 程序次序法则:线程中的每个动作A都happens-before于该线程中的每一个动作B,其中,在程序中,所有的动作B都能出现在A之后。
  • 监视器锁法则:对一个监视器锁的解锁 happens-before于每一个后续对同一监视器锁的加锁。
  • volatile变量法则:对volatile域的写入操作happens-before于每一个后续对同一个域的读写操作。
  • 线程启动法则:在一个线程里,对Thread.start的调用会happens-before于每个启动线程的动作。
  • 线程终结法则:线程中的任何动作都happens-before于其他线程检测到这个线程已经终结、或者从Thread.join调用中成功返回,或Thread.isAlive返回false。
  • 中断法则:一个线程调用另一个线程的interrupt happens-before于被中断的线程发现中断。
  • 终结法则:一个对象的构造函数的结束happens-before于这个对象finalizer的开始。
  • 传递性:如果A happens-before于B,且B happens-before于C,则A happens-before于C

实现内存可见性:

要实现共享变量的可见性,必须保证两点

  • 线程修改后的共享变量值能够及时从工作内存中刷新到主内存中
  • 其他线程能够及时把共享变量的最新值从主内存更新到自己的工作内存中
    *

    synchronized实现可见性

    synchronized能够实现:

    • 原子性(同步)
    • 可见性

JMM关于synchronized的两条规定:

  • 线程解锁前,必须把共享变量的最新值刷新到主内存中
  • 线程加锁时,将清空工作内存中共享变量的值,从而使用共享变量时需要从主存中重新读取最新的值

线程解锁前对共享变量的修改在下次加锁时对其他线程可见

线程执行互斥代码的过程

  1. 获得互斥锁
  2. 清空工作内存
  3. 从主内存拷贝变量的最新副本到工作内存
  4. 执行代码
  5. 将更改后的共享变量的值刷新到主内存中
  6. 释放互斥锁

volatile实现可见性

volatile关键字:

  • 能够保证volatile变量的可见性
  • 不能保证volatile变量复合操作的原子

volatile如何实现内存的可见性 :

深入来说:通过加入内存屏障禁止重排序优化来实现的

  • 在每个volatile写操作前插入StoreStore屏障,在写操作后插入StoreLoad屏障
    • 在每个volatile读操作前插入LoadLoad屏障,在读操作后插入LoadStore屏障

通俗地讲:volatile变量在每次被线程访问时,都强迫从主内存中重读该变量的值,而当该变量发生变化时,又会强迫将最新的值刷新到主内存。这样任何时刻,不同的线程总能看到该变量的最新值。

线程写volatile变量的过程:

  1. 改变线程工作内存中volatile变量副本的值
  2. 将改变后的副本的值从工作内存刷新到主内存

线程读volatile变量的过程:

  1. 从主内存中读取volatile变量的最新值到线程的工作内存中
  2. 从工作内存中读取volatile变量的副本

synchronized vs volatile

  • volatile不需要加锁,比synchronized更轻量级,不会阻塞线程
  • synchronized既能保证可见性,又能保证原子性,而volatile只能保证可见性,无法保证原子性

可作为GC Root的对象

在Java虚拟机中判断一个对象是否可以被回收,有一种做法叫可达性分析算法,也就是从GC Root到各个对象,如果GC Root到某个对象还有可达的引用链,那么这个对象就还不能被回收,否则就等着被收割吧。

所谓“GC roots”,或者说tracing GC的“根集合”,就是一组必须活跃的引用
例如说,这些引用可能包括:

  • 所有Java线程当前活跃的栈帧里指向GC堆里的对象的引用;换句话说,当前所有正在被调用的方法的引用类型的参数/局部变量/临时值
  • VM的一些静态数据结构里指向GC堆里的对象的引用,例如说HotSpot VM里的Universe里有很多这样的引用。
  • JNI handles,包括global handles和local handles
  • (看情况)所有当前被加载的Java类
  • (看情况)Java类的引用类型静态变量
  • (看情况)Java类的运行时常量池里的引用类型常量(String或Class类型)
  • (看情况)String常量池(StringTable)里的引用

Tracing GC的根本思路就是:给定一个集合的引用作为根出发,通过引用关系遍历对象图,能被遍历到的(可到达的)对象就被判定为存活,其余对象(也就是没有被遍历到的)就自然被判定为死亡。注意再注意:tracing GC的本质是通过找出所有活对象来把其余空间认定为“无用”,而不是找出所有死掉的对象并回收它们占用的空间。


幂等性

概述

幂等性原本是数学上的概念,即使公式:f(x)=f(f(x)) 能够成立的数学性质。用在编程领域,则意为对同一个系统,使用同样的条件,一次请求和重复的多次请求对系统资源的影响是一致的。

幂等的常用思路

1. MVCC:

多版本并发控制,乐观锁的一种实现,在数据更新时需要去比较持有数据的版本号,版本号不一致的操作无法成功。例如博客点赞次数自动+1的接口:

public boolean addCount(Long id, Long version);
UPDATE blogTable SET count=count+1,version=version+1 WHERE id=321 AND version=123

每一个version只有一次执行成功的机会,一旦失败必须重新获取。

2. 去重表

利用数据库表单的特性来实现幂等,常用的一个思路是在表上构建唯一性索引,保证某一类数据一旦执行完毕,后续同样的请求再也无法成功写入。

例子还是上述的博客点赞问题,要想防止一个人重复点赞,可以设计一张表,将博客id与用户id绑定建立唯一索引,每当用户点赞时就往表中写入一条数据,这样重复点赞的数据就无法写入。

3. TOKEN机制:

这种机制就比较重要了,适用范围较广,有多种不同的实现方式。其核心思想是为每一次操作生成一个唯一性的凭证,也就是token。一个token在操作的每一个阶段只有一次执行权,一旦执行成功则保存执行结果。对重复的请求,返回同一个结果。

以电商平台为例子,电商平台上的订单id就是最适合的token。当用户下单时,会经历多个环节,比如生成订单,减库存,减优惠券等等。

每一个环节执行时都先检测一下该订单id是否已经执行过这一步骤,对未执行的请求,执行操作并缓存结果,而对已经执行过的id,则直接返回之前的执行结果,不做任何操作。这样可以在最大程度上避免操作的重复执行问题,缓存起来的执行结果也能用于事务的控制等。


MySQL锁机制

MySQL有三种锁的级别:页级、表级、行级。

  • MyISAM 表级锁(table-level locking)
  • MEMORY 表级锁(table-level locking)
  • BDB 页面锁(page-levellocking),但也支持表级锁;
  • InnoDB 既支持行级锁(row-level locking),也支持表级锁,但默认情况下是采用行级锁。

以下讲的都是在innodb引擎的前提下。

  1. 共享锁(Share Locks,即S锁),使用方式select … LOCK IN SHARE MODE
    SELECT … FOR UPDATE同时只能有一个在语句执行,另一个会阻塞;SELECT … LOCK IN SHARE MODE可以多个同时执行(这也是和for update最大的区别)
  1. 排它锁(Exclusive Locks,即X锁),使用方式:select … FOR UPDATE
    select … for update锁住的行记录,其它事务不可修改,如果有修改会wait直到锁释放(事务commit)或超时

MySQL 乐观锁与悲观锁

悲观锁

悲观锁(Pessimistic Lock),顾名思义,就是很悲观,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会block直到它拿到锁。

乐观锁

乐观锁(Optimistic Lock),顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在提交更新的时候会判断一下在此期间别人有没有去更新这个数据。乐观锁适用于读多写少的应用场景,这样可以提高吞吐量。

乐观锁:假设不会发生并发冲突,只在提交操作时检查是否违反数据完整性。

乐观锁一般来说有以下2种方式:

  1. 使用数据版本(Version)记录机制实现,这是乐观锁最常用的一种实现方式。何谓数据版本?即为数据增加一个版本标识,一般是通过为数据库表增加一个数字类型的 “version” 字段来实现。当读取数据时,将version字段的值一同读出,数据每更新一次,对此version值加一。当我们提交更新的时候,判断数据库表对应记录的当前版本信息与第一次取出来的version值进行比对,如果数据库表当前版本号与第一次取出来的version值相等,则予以更新,否则认为是过期数据。

  2. 使用时间戳(timestamp)。乐观锁定的第二种实现方式和第一种差不多,同样是在需要乐观锁控制的table中增加一个字段,名称无所谓,字段类型使用时间戳(timestamp), 和上面的version类似,也是在更新提交的时候检查当前数据库中数据的时间戳和自己更新前取到的时间戳进行对比,如果一致则OK,否则就是版本冲突。

MySQL隐式和显示锁定

MySQL InnoDB采用的是两阶段锁定协议(two-phase locking protocol)。在事务执行过程中,随时都可以执行锁定,锁只有在执行 COMMIT或者ROLLBACK的时候才会释放,并且所有的锁是在同一时刻被释放。前面描述的锁定都是隐式锁定,InnoDB会根据事务隔离级别在需要的时候自动加锁。

另外,InnoDB也支持通过特定的语句进行显示锁定,这些语句不属于SQL规范:

  • SELECT ... LOCK IN SHARE MODE
    是IS锁(意向共享锁),即在符合条件的rows上都加了共享锁,这样的话,其他session可以读取这些记录,也可以继续添加IS锁,但是无法修改这些记录直到你这个加锁的session执行完成(否则直接锁等待超时)。
  • SELECT ... FOR UPDATE
    是IX锁(意向排它锁),一种行级锁,一旦用户对某个行施加了行级加锁,则该用户可以查询也可以更新被加锁的数据行,其它用户只能查询但不能更新被加锁的数据行。真正对表进行更新时,是以独占方式锁表,一直到提交或复原该事务为止。行锁永远是独占方式锁。

总结:经过测试, 这两种模式加锁的无数据的情况下,锁不会起作用,必须加锁的表必须有数据。 lock in share mode 也叫间隙锁,带where 条件加锁,where字段是整型并且是主键,会变成行锁。

乐观锁与悲观锁的区别

乐观锁的思路一般是表中增加版本字段,更新时where语句中增加版本的判断,算是一种CAS(Compare And Swep)操作,商品库存场景中number起到了版本控制(相当于version)的作用( AND number=#{number})。

悲观锁之所以是悲观,在于他认为本次操作会发生并发冲突,所以一开始就对商品加上锁(SELECT … FOR UPDATE),然后就可以安心的做判断和更新,因为这时候不会有别人更新这条商品库存。


消费者的推拉模式

消息中间件的主要功能是消息的路由(Routing)和缓存(Buffering)。在AMQP中提供类似功能的两种域模型:Exchange 和 Message queue。

JMS中定义了两种消息模型:点对点(point to point, queue)和发布/订阅(publish/subscribe,topic)。主要区别就是是否能重复消费。

点对点:Queue,不可重复消费

消息生产者生产消息发送到queue中,然后消息消费者从queue中取出并且消费消息。消息被消费以后,queue中不再有存储,所以消息消费者不可能消费到已经被消费的消息。
Queue支持存在多个消费者,但是对一个消息而言,只会有一个消费者可以消费。
注:Kafka不遵守JMS协议,所以Kafka实际应用中,很可能会需要ack(确认),然后多个消费者能够会同时消费。。需要具体看。

发布/订阅:Topic,可以重复消费

消息生产者(发布)将消息发布到topic中,同时有多个消息消费者(订阅)消费该消息。
和点对点方式不同,发布到topic的消息会被所有订阅者消费。

支持订阅组的发布订阅模式:

发布订阅模式下,当发布者消息量很大时,显然单个订阅者的处理能力是不足的。实际上现实场景中是多个订阅者节点组成一个订阅组负载均衡消费topic消息即分组订阅,这样订阅者很容易实现消费能力线性扩展。

消费者获取消息:Push和Pull

  • Push方式:由消息中间件主动地将消息推送给消费者;
  • Pull方式:由消费者主动向消息中间件拉取消息。

流行模型比较:

  • RabbitMQ
    既支持内存队列也支持持久化队列,消费端为Push模型,消费状态和订阅关系由服务端负责维护,消息消费完后立即删除,不保留历史消息。
  • Kafka
    只支持消息持久化,消费端为Pull模型,消费状态和订阅关系由客户端端负责维护,消息消费完后不会立即删除,会保留历史消息。
  • ActiveMQ
    一条消息从producer端发出之后,一旦被broker正确保存,那么它将会被consumer消费,然后ACK,broker端才会删除;不过当消息过期或者存储设备溢出时,也会终结它。

高并发系统的限流,缓存,降级

缓存

在大型高并发系统中,如果没有缓存数据库将分分钟被爆,系统也会瞬间瘫痪。使用缓存不单单能够提升系统访问速度、提高并发访问量,也是保护数据库、保护系统的有效方式。
大型网站一般主要是“读”,缓存的使用很容易被想到。在大型“写”系统中,缓存也常常扮演者非常重要的角色。比如累积一些数据批量写入,内存里面的缓存队列(生产消费),以及HBase写数据的机制等等也都是通过缓存提升系统的吞吐量或者实现系统的保护措施。甚至消息中间件,你也可以认为是一种分布式的数据缓存。

降级

服务降级是当服务器压力剧增的情况下,根据当前业务情况及流量对一些服务和页面有策略的降级,以此释放服务器资源以保证核心任务的正常运行。降级往往会指定不同的级别,面临不同的异常等级执行不同的处理。根据服务方式:可以拒接服务,可以延迟服务,也有时候可以随机服务。根据服务范围:可以砍掉某个功能,也可以砍掉某些模块。总之服务降级需要根据不同的业务需求采用不同的降级策略。主要的目的就是服务虽然有损但是总比没有好。

限流

限流可以认为服务降级的一种,限流就是限制系统的输入和输出流量已达到保护系统的目的。一般来说系统的吞吐量是可以被测算的,为了保证系统的稳定运行,一旦达到的需要限制的阈值,就需要限制流量并采取一些措施以完成限制流量的目的。比如:延迟处理,拒绝处理,或者部分拒绝处理等等。

限流算法

  1. 计数器
    计数器是最简单粗暴的算法。比如某个服务最多只能每秒钟处理100个请求。我们可以设置一个1秒钟的滑动窗口,窗口中有10个格子,每个格子100毫秒,每100毫秒移动一次,每次移动都需要记录当前服务请求的次数。内存中需要保存10次的次数。可以用数据结构LinkedList来实现。格子每次移动的时候判断一次,当前访问次数和LinkedList中最后一个相差是否超过100,如果超过就需要限流了。

  2. 漏桶
    漏桶算法即leaky bucket是一种非常常用的限流算法,可以用来实现流量整形(Traffic Shaping)和流量控制(Traffic Policing)。

漏桶算法的主要概念如下:

  1. 一个固定容量的漏桶,按照常量固定速率流出水滴;
  2. 如果桶是空的,则不需流出水滴;
    3.可以以任意速率流入水滴到漏桶;
  3. 如果流入水滴超出了桶的容量,则流入的水滴溢出了(被丢弃),而漏桶容量是不变的

漏桶算法比较好实现,在单机系统中可以使用队列来实现,在分布式环境中消息中间件或者Redis都是可选的方案。

  1. 令牌桶

令牌桶算法是一个存放固定容量令牌(token)的桶,按照固定速率往桶里添加令牌。令牌桶算法基本可以用下面的几个概念来描述:

  1. 令牌将按照固定的速率被放入令牌桶中。比如每秒放10个。
  2. 桶中最多存放b个令牌,当桶满时,新添加的令牌被丢弃或拒绝。
  3. 当一个n个字节大小的数据包到达,将从桶中删除n个令牌,接着数据包被发送到网络上。
  4. 如果桶中的令牌不足n个,则不会删除令牌,且该数据包将被限流(要么丢弃,要么缓冲区等待)。

漏桶和令牌桶的比较

令牌桶可以在运行时控制和调整数据处理的速率,处理某时的突发流量。放令牌的频率增加可以提升整体数据处理的速度,而通过每次获取令牌的个数增加或者放慢令牌的发放速度和降低整体数据处理速度。而漏桶不行,因为它的流出速率是固定的,程序处理速度也是固定的。

整体而言,令牌桶算法更优,但是实现更为复杂一些。


零碎的知识点-3

零碎的知识点-3


TCP拥塞控制-慢启动、拥塞避免、快重传、快启动

一般原理:发生拥塞控制的原因:资源(带宽、交换节点的缓存、处理机)的需求>可用资源。

作用:拥塞控制就是为了防止过多的数据注入到网络中,这样可以使网络中的路由器或者链路不至于过载。拥塞控制要做的都有一个前提:就是网络能够承受现有的网络负荷。

对比流量控制:拥塞控制是一个全局的过程,涉及到所有的主机、路由器、以及降低网络相关的所有因素。流量控制往往指点对点通信量的控制。是端对端的问题。

拥塞窗口:发送方为一个动态变化的窗口叫做拥塞窗口,拥塞窗口的大小取决于网络的拥塞程度。发送方让自己的发送窗口等于拥塞窗口,但是发送窗口不是一直等于拥塞窗口的,在网络情况好的时候,拥塞窗口不断的增加,发送方的窗口自然也随着增加,但是接受方的接受能力有限,在发送方的窗口达到某个大小时就不在发生变化了。

发送方如果知道网络拥塞了呢?发送方发送一些报文段时,如果发送方没有在时间间隔内收到接收方的确认报文段,则就可以人为网络出现了拥塞。

慢启动算法的思路:主机开发发送数据报时,如果立即将大量的数据注入到网络中,可能会出现网络的拥塞。慢启动算法就是在主机刚开始发送数据报的时候先探测一下网络的状况,如果网络状况良好,发送方每发送一次文段都能正确的接受确认报文段。那么就从小到大的增加拥塞窗口的大小,即增加发送窗口的大小。

慢开始和拥塞避免算法的实现举例

拥塞避免:为了防止cwnd增加过快而导致网络拥塞,所以需要设置一个慢开始门限ssthresh状态变量(拥塞控制的标识),它的用法:

               1. 当cwnd < ssthresh,使用慢启动算法,

               2. 当cwnd > ssthresh,使用拥塞控制算法,停用慢启动算法。

               3. 当cwnd = ssthresh,这两个算法都可以。

拥塞避免的思路:是让cwnd缓慢的增加而不是加倍的增长,每经历过一次往返时间就使cwnd增加1,而不是加倍,这样使cwnd缓慢的增长,比慢启动要慢的多。

无论是慢启动算法还是拥塞避免算法,只要判断网络出现拥塞,就要把慢启动开始门限(ssthresh)设置为设置为发送窗口的一半(>=2),cwnd(拥塞窗口)设置为1,然后在使用慢启动算法,这样做的目的能迅速的减少主机向网络中传输数据,使发生拥塞的路由器能够把队列中堆积的分组处理完毕。拥塞窗口是按照线性的规律增长,比慢启动算法拥塞窗口增长块的多。

快重传:快重传算法要求首先接收方收到一个失序的报文段后就立刻发出重复确认,而不要等待自己发送数据时才进行捎带确认。接收方成功的接受了发送方发送来的M1、M2并且分别给发送了ACK,现在接收方没有收到M3,而接收到了M4,显然接收方不能确认M4,因为M4是失序的报文段。如果根据可靠性传输原理接收方什么都不做,但是按照快速重传算法,在收到M4、M5等报文段的时候,不断重复的向发送方发送M2的ACK,如果接收方一连收到三个重复的ACK,那么发送方不必等待重传计时器到期,由于发送方尽早重传未被确认的报文段。

快重传示意图

快恢复

  1. 当发送发连续接收到三个确认时,就执行乘法减小算法,把慢启动开始门限(ssthresh)减半,但是接下来并不执行慢开始算法。
  2. 此时不执行慢启动算法,而是把cwnd设置为ssthresh的一半, 然后执行拥塞避免算法,使拥塞窗口缓慢增大。

快恢复


TCP报头

TCP/IP协议栈主要分为四层:应用层、传输层、网络层、数据链路层,每层都有相应的协议,如下图

TCP/IP协议体系

TCP数据报头格式:
TCP数据报头格式

  • 端口号:32位,16位源端口+16位目的端口, 最大端口为65536
  • 位序号:32位,也称顺序号,简写位SEQ。
  • 确认序号:32位,简写为ACK,在握手阶段,确认序号将发送方的序号加1作为回答。
  • 数据偏移:规定了整个报头的长度。
  • 6位标志字段:
    • ACK 置1时表示确认号(为合法,为0的时候表示数据段不包含确认信息,确认号被忽略。
    • RST 置1时重建连接。如果接收到RST位时候,通常发生了某些错误。
    • SYN 置1时用来发起一个连接。
    • FIN 置1时表示发端完成发送任务。用来释放连接,表明发送方已经没有数据发送了。
    • URG 紧急指针,告诉接收TCP模块紧要指针域指着紧要数据。注:一般不使用。
    • PSH 置1时请求的数据段在接收方得到后就可直接送到应用程序,而不必等到缓冲区满时才传送。注:一般不使用。
  • 检验和:16位,检验和覆盖了整个的TCP报文段: TCP首部和TCP数据。这是一个强制性的字段,一定是由发端计算和存储,并由收端进行验证。
  • 紧急指针:16位,一般不用。
  • 可选与变长选项:通常为空,可根据首部长度推算。用于发送方与接收方协商最大报文段长度(MSS,1460是以太网默认的大小),或在高速网络环境下作窗口调节因子时使用。首部字段还定义了一个时间戳选项。

UDP报头

UDP协议也是传输层协议,它是无连接,不保证可靠的传输层协议。
UDP报头

  • 源端口:16位,源端口是一个大于1023的16位数字,由基于UDP应用程序的用户进程随机选择。
  • 目的端口:16位,
  • 数据包长度:16位,指明了包括首部在内的UDP报文段长度。UDP长字段的值是UDP报文头的长度(8字节)与UDP所携带数据长度的总和。
  • 校验和:16位,是指整个UDP报文头和UDP所带的数据的校验和(也包括伪报文头)。伪报文头不包括在真正的UDP报文头中,但是它可以保证UDP数据被正确的主机收到了。因在校验和中加入了伪头标,故ICMP除能防止单纯数据差错之外,对IP分组也具有保护作用。

DNS解析过程

DNS(Domain Name System,域名系统),DNS协议运行在UDP协议之上,使用端口号53。

主机解析域名的顺序

  1. 浏览器缓存:浏览器会按照一定的频率缓存DNS记录。
  2. 操作系统缓存:如果浏览器缓存中找不到需要的DNS记录,那就去操作系统中找(如/etc/hosts文件)。
  3. 路由缓存:路由器也有DNS缓存。
  4. ISP的DNS服务器:ISP是互联网服务提供商(Internet Service Provider)的简称,ISP有专门的DNS服务器应对DNS查询请求。
  5. 根服务器:ISP的DNS服务器还找不到的话,它就会向根服务器发出请求,进行递归查询(DNS服务器先问根域名服务器.com域名服务器的IP地址,然后再问.com域名服务器,依次类推)。(root -> com -> baidu.com)

get和post请求的区别

GET的语义是请求获取指定的资源。GET方法是安全、幂等、可缓存的(除非有 Cache-ControlHeader的约束),GET方法的报文主体没有任何语义。

POST的语义是根据请求负荷(报文主体)对指定的资源做出处理,具体的处理方式视资源类型而不同。POST不安全,不幂等,(大部分实现)不可缓存。

  • GET后退按钮/刷新无害,POST数据会被重新提交(浏览器应该告知用户数据会被重新提交)。
  • GET书签可收藏,POST为书签不可收藏。
  • GET能被缓存,POST不能缓存 。
  • GET编码类型application/x-www-form-url,POST编码类型encodedapplication/x-www-form-urlencoded 或 multipart/form-data。为二进制数据使用多重编码。
  • GET历史参数保留在浏览器历史中。POST参数不会保存在浏览器历史中。
  • GET对数据长度有限制,当发送数据时,GET 方法向 URL 添加数据;URL 的长度是受限制的(URL 的最大长度是 2048 个字符)。POST无限制。
  • GET只允许 ASCII 字符。POST没有限制。也允许二进制数据。与 POST 相比,GET 的安全性较差,因为所发送的数据是 URL 的一部分。在发送密码或其他敏感信息时绝不要使用 GET !

http和https区别,https在请求时额外的过程,https是如何保证数据安全的

http是应用层协议,它会将要传输的数据以明文的方式给传输层,这样显然不安全。https则是在应用层与传输层之间又加了一层,该层遵守SSL/TLS协议。

加密的方式有两种,

  • 对称加密:对称加密速度快,但是加密和解密的钥匙是相同的。
  • 非对称加密,算法更加复杂,速度慢,加密和解密钥匙不相同。

在https中,加密过程大致如下:
首先服务器将公钥给浏览器,浏览器拿到公钥之后,生成一个“会话密钥”,这个会话密钥属于对称加密,然后用公钥加密这个“会话密钥”发送给服务器,最后,在数据传输的过程中,就用这个会话密钥来加密数据。打个比方:我有二把钥匙,我把其中一把钥匙放在信封里,然后把信封交给你。但是这个信封只有你能打开。

上述的过程是在3次握手中完成,采用明文发送,握手完成以后,客户端和服务端就约定好了“会话密钥”,以后的数据传输,就采用这个会话密钥加密。

在上述的过程中要注意
公钥怎么给客户端?
如果公钥A在发送给客户端的过程中,被截取,被黑客替换成了公钥B,以后的事情可想而知,你就会用公钥B加密“会话密钥”,然后。。。你的隐私就被窃取喽。对于这个问题,要引入证书和数据签名的概念。


Java正则表达式

java.util.regex 包主要包括以下三个类:

  • Pattern 类:
    pattern 对象是一个正则表达式的编译表示。Pattern 类没有公共构造方法。要创建一个 Pattern 对象,你必须首先调用其公共静态编译方法,它返回一个 Pattern 对象。该方法接受一个正则表达式作为它的第一个参数。

  • Matcher 类:
    Matcher 对象是对输入字符串进行解释和匹配操作的引擎。与Pattern 类一样,Matcher 也没有公共构造方法。你需要调用 Pattern 对象的 matcher 方法来获得一个 Matcher 对象。

PatternSyntaxException:
PatternSyntaxException 是一个非强制异常类,它表示一个正则表达式模式中的语法错误。

捕获组

捕获组是把多个字符当一个单独单元进行处理的方法,它通过对括号内的字符分组来创建。

      // 按指定模式在字符串查找
      String line = "This order was placed for QT3000! OK?";
      String pattern = "(\\D*)(\\d+)(.*)";

      // 创建 Pattern 对象
      Pattern r = Pattern.compile(pattern);

      // 现在创建 matcher 对象
      Matcher m = r.matcher(line);
      if (m.find( )) {
         System.out.println("Found value: " + m.group(0) );
         System.out.println("Found value: " + m.group(1) );
         System.out.println("Found value: " + m.group(2) );
         System.out.println("Found value: " + m.group(3) ); 
      } else {
         System.out.println("NO MATCH");
      }

切记,要先调用find( )方法才能调用group()方法


初步理解AQS

AQS全称AbstractQueuedSynchronizer,它是concurrent包中最重要的基础设施类之一,负责作为模板类向业务层提供对临界区的管理。AQS定义了一套多线程访问共享资源的同步器框架,许多同步类实现都依赖于它,如常用的ReentrantLock/Semaphore/CountDownLatch/FutureTask。

它维护了一个volatile int state(代表共享资源)和一个FIFO线程等待队列(多线程争用资源被阻塞时会进入此队列)。这里volatile是核心关键词,具体volatile的语义,

AQS定义两种资源共享方式:Exclusive(独占,只有一个线程能执行,如ReentrantLock)和Share(共享,多个线程可同时执行,如Semaphore/CountDownLatch)。

以ReentrantLock为例,state初始化为0,表示未锁定状态。A线程lock()时,会调用tryAcquire()独占该锁并将state+1。此后,其他线程再tryAcquire()时就会失败,直到A线程unlock()到state=0(即释放锁)为止,其它线程才有机会获取该锁。当然,释放锁之前,A线程自己是可以重复获取此锁的(state会累加),这就是可重入的概念。但要注意,获取多少次就要释放多么次,这样才能保证state是能回到零态的。

再以CountDownLatch以例,任务分为N个子线程去执行,state也初始化为N(注意N要与线程个数一致)。这N个子线程是并行执行的,每个子线程执行完后countDown()一次,state会CAS减1。等到所有子线程都执行完后(即state=0),会unpark()主调用线程,然后主调用线程就会从await()函数返回,继续后余动作。

一般来说,自定义同步器要么是独占方法,要么是共享方式,他们也只需实现tryAcquire-tryRelease、tryAcquireShared-tryReleaseShared中的一种即可。但AQS也支持自定义同步器同时实现独占和共享两种方式,如ReentrantReadWriteLock。

类名 互斥锁 资源 如何访问 备注
Mutex 互斥锁 1 仅排他 出现在AQS类的注释中
Semaphore 信号量 可指定(构造参数permits) 仅共享
ReentrantReadWriteLock 读写(可重入)锁 无上限(实际为16位unsigned short) 读写共享访问,写锁排他访问 可重入

复用AQS

Step1/3. 定义state。
state是一个int,它没有具体语义,但作为AQS的核心成员变量,可根据业务类的需要赋予state具体的语义。

| 方法 | 简介 |
| getState() | 读状态 | setState (int newState) | 写状态 |
| compareAndSetState (int expect, int update) | CAS写状态 |

Step2/3. 新建内部类Sync,继承自AQS并按需实现钩子方法

钩子方法 简介
tryAcquire (int arg) 排他获取(资源数)
tryRelease(int arg) 排他释放(资源数)
tryAcquireShared(int arg) 共享获取(资源数)
tryReleaseShared(int arg) 共享释放(资源数)
isHeldExclusively() 是否排他状态

Step3/3. 业务类把业务方法的实现委托给Sync。Sync可以直接使用继承自AQS的模板方法。

AQS内部细节

概述

AQS实现了一个FIFO队列,用于管理等候在临界区外的线程。队列的每个节点保留了线程信息,因此线程可以安然休眠,待临界区的资源可用时再次唤醒。注意,队列的存在并不保证公平,因为AQS并不确保被唤醒的队首线程能进入临界区,唤醒过程中可能会被其它尚未入队的竞争线程抢占先机。

线程控制

线程的休眠和唤醒是通过LockSupport.park()LockSupport.unpark()实现的。LockSupport是整个concurrent包最底层的API之一。在AQS的注释中写到,如果你对其排队机制不满,可以重新定义队列的数据结构并通过LockSupport调度。

队列

数据结构:双向链表构成的FIFO队列(链表是其物理结构,队列是其逻辑用途。注释中称其为CLH Lock队列的变种,而CLH Lock常用于实现自旋锁,其确保无饥饿性和公平性)

CLH Lock 队列


LockSupport的park和unpark的基本使用,以及对线程中断的响应性

LockSupport是JDK中比较底层的类,用来创建锁和其他同步工具类的基本线程阻塞原语。java锁和同步器框架的核心AQS:AbstractQueuedSynchronizer,就是通过调用LockSupport.park()和LockSupport.unpark()实现线程的阻塞和唤醒的。LockSupport很类似于二元信号量(只有1个许可证可供使用),如果这个许可还没有被占用,当前线程获取许可并继续执行;如果许可已经被占用,当前线程阻塞,等待获取许可。

许可默认是被占用的。

LockSupport是不可重入。

线程如果因为调用park而阻塞的话,能够响应中断请求(中断状态被设置成true),但是不会抛出InterruptedException。

零碎的知识点-2

零碎的知识点-2

死锁及其解决方案(避免、预防、检测)

死锁产生的原因

因竞争资源发生死锁 现象:系统中供多个进程共享的资源的数目不足以满足全部进程的需要时,就会引起对诸资源的竞争而发生死锁现象。

死锁的四个必要条件:

  • 互斥条件:进程对所分配到的资源不允许其他进程进行访问,若其他进程访问该资源,只能等待,直至占有该资源的进程使用完成后释放该资源

  • 请求和保持条件:进程获得一定的资源之后,又对其他资源发出请求,但是该资源可能被其他进程占有,此事请求阻塞,但又对自己获得的资源保持不放

  • 不可剥夺条件:是指进程已获得的资源,在未完成使用之前,不可被剥夺,只能在使用完后自己释放

  • 环路等待条件:是指进程发生死锁后,必然存在一个进程–资源之间的环形链

处理死锁的基本方法

预防死锁(破坏四个必要条件)

  • 资源一次性分配:(破坏请求和保持条件)
  • 可剥夺资源:即当某进程新的资源未满足时,释放已占有的资源(破坏不可剥夺条件)

  • 资源有序分配法:系统给每类资源赋予一个编号,每一个进程按编号递增的顺序请求资源,释放则相反(破坏环路等待条件)

避免死锁(银行家算法)

上面的预防算法会严重地损害系统性能。而避免死锁算法是让系统在进行资源分配之前预先计算资源分配的安全性。

检测死锁

首先为每个进程和每个资源指定一个唯一的号码;

然后建立资源分配表和进程等待表。

解除死锁

当发现有进程死锁后,便应立即把它从死锁状态中解脱出来,常采用的方法有:

剥夺资源

从其它进程剥夺足够数量的资源给死锁进程,以解除死锁状态;

撤消进程

可以直接撤消死锁进程或撤消代价最小的进程,直至有足够的资源可用,死锁状态.消除为止;所谓代价是指优先级、运行代价、进程的重要性和价值等。

Linux 命令&和&&的区别

在Linux控制台命令的执行过程中,

&表示该命令后台执行。

例如执行:ping 192.168.1.* &

表示后台不停的执行ping命令,即便你强制中断掉该线程,它仍然会不停的输出结果。除非你将该线程kill掉。

&&表示并行执行前后命令。

Linux—shell中$(( ))、$( )、$${ }的区别

在bash中,$( )与(反引号)都是用来作命令替换的。
命令替换与变量替换差不多,都是用来重组命令行的,先完成引号里的命令行,然后将其结果替换出来,再重组成新的命令行。

redis的持久化方式RDB和AOF的区别

redis提供两种方式进行持久化,一种是RDB持久化(原理是将Reids在内存中的数据库记录定时dump到磁盘上的RDB持久化),另外一种是AOF持久化(原理是将Reids的操作日志以追加的方式写入文件)。

二者却别:

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

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

MYSQL innodb中MVCC的一些理解

MVCC简介

MVCC (Multiversion Concurrency Control),即多版本并发控制技术,它使得大部分支持行锁的事务引擎,不再单纯的使用行锁来进行数据库的并发控制,取而代之的是把数据库的行锁与行的多个版本结合起来,只需要很小的开销,就可以实现非锁定读,从而大大提高数据库系统的并发性能

  • 读锁:也叫共享锁、S锁,若事务T对数据对象A加上S锁,则事务T可以读A但不能修改A,其他事务只能再对A加S锁,而不能加X锁,直到T释放A上的S 锁。这保证了其他事务可以读A,但在T释放A上的S锁之前不能对A做任何修改。

  • 写锁:又称排他锁、X锁。若事务T对数据对象A加上X锁,事务T可以读A也可以修改A,其他事务不能再对A加任何锁,直到T释放A上的锁。这保证了其他事务在T释放A上的锁之前不能再读取和修改A。

  • 表锁:操作对象是数据表。Mysql大多数锁策略都支持(常见mysql innodb),是系统开销最低但并发性最低的一个锁策略。事务t对整个表加读锁,则其他事务可读不可写,若加写锁,则其他事务增删改都不行。

  • 行级锁:操作对象是数据表中的一行。是MVCC技术用的比较多的,但在MYISAM用不了,行级锁用mysql的储存引擎实现而不是mysql服务器。但行级锁对系统开销较大,处理高并发较好。

MVCC有下面几个特点:

  • 每行数据都存在一个版本,每次数据更新时都更新该版本
  • 修改时Copy出当前版本随意修改,各个事务之间无干扰
  • 保存时比较版本号,如果成功(commit),则覆盖原记录;失败则放弃copy(rollback)

一句话讲,MVCC就是用 同一份数据临时保留多版本的方式 的方式,实现并发控制。

TCP三次握手和四次挥手

三次握手

第一次握手:建立连接时,客户端发送syn包(syn=j)到服务器,并进入SYN_SENT状态,等待服务器确认;SYN:同步序列编号(Synchronize Sequence Numbers)。

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

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

完成三次握手,客户端与服务器开始传送数据,在上述过程中,还有一些重要的概念:

未连接队列

在三次握手协议中,服务器维护一个未连接队列,该队列为每个客户端的SYN包(syn=j)开设一个条目,该条目表明服务器已收到SYN包,并向客户发出确认,正在等待客户的确认包。这些条目所标识的连接在服务器处于SYN_RECV状态,当服务器收到客户的确认包时,删除该条目,服务器进入ESTABLISHED状态。

TCP握手

TCP挥手

TCP挥手

为什么TCP协议终止链接要四次?

  1. 当主机A确认发送完数据且知道B已经接受完了,想要关闭发送数据口(当然确认信号还是可以发),就会发FIN给主机B。
  2. 主机B收到A发送的FIN,表示收到了,就会发送ACK回复。
  3. 但这是B可能还在发送数据,没有想要关闭数据口的意思,所以FIN与ACK不是同时发送的,而是等到B数据发送完了,才会发送FIN给主机A。
  4. A收到B发来的FIN,知道B的数据也发送完了,回复ACK, A等待2MSL以后,没有收到B传来的任何消息,知道B已经收到自己的ACK了,A就关闭链接,B也关闭链接了。

TCP挥手时,为什么等待2MSL,从TIME_WAIT到CLOSE?

在Client发送出最后的ACK回复,但该ACK可能丢失。Server如果没有收到ACK,将不断重复发送FIN片段。所以Client不能立即关闭,它必须确认Server接收到了该ACK。Client会在发送出ACK之后进入到TIME_WAIT状态。Client会设置一个计时器,等待2MSL的时间。如果在该时间内再次收到FIN,那么Client会重发ACK并再次等待2MSL。所谓的2MSL是两倍的MSL(Maximum Segment Lifetime)。MSL指一个片段在网络中最大的存活时间,2MSL就是一个发送和一个回复所需的最大时间。如果直到2MSL,Client都没有再次收到FIN,那么Client推断ACK已经被成功接收,则结束TCP连接。

C语言文件读写

C语言文件读写

Redis分布式锁

可靠性

首先,为了确保分布式锁可用,我们至少要确保锁的实现同时满足以下四个条件:

  • 互斥性 在任意时刻,只有一个客户端能持有锁。
  • 不会发生死锁 即使有一个客户端在持有锁的期间崩溃而没有主动解锁,也能保证后续其他客户端能加锁。
  • 具有容错性 只要大部分的Redis节点正常运行,客户端就可以加锁和解锁。
  • 解铃还须系铃人 加锁和解锁必须是同一个客户端,客户端自己不能把别人加的锁给解了。

代码实现

加锁代码

public class RedisTool {

    private static final String LOCK_SUCCESS = "OK";
    private static final String SET_IF_NOT_EXIST = "NX";
    private static final String SET_WITH_EXPIRE_TIME = "PX";

    /**
     * 尝试获取分布式锁
     * @param jedis Redis客户端
     * @param lockKey 锁
     * @param requestId 请求标识
     * @param expireTime 超期时间
     * @return 是否获取成功
     */
    public static boolean tryGetDistributedLock(Jedis jedis, String lockKey, String requestId, int expireTime) {

        String result = jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);

        if (LOCK_SUCCESS.equals(result)) {
            return true;
        }
        return false;

    }

}

解锁代码

···java
public class RedisTool {

private static final Long RELEASE_SUCCESS = 1L;

/**
 * 释放分布式锁
 * @param jedis Redis客户端
 * @param lockKey 锁
 * @param requestId 请求标识
 * @return 是否释放成功
 */
public static boolean releaseDistributedLock(Jedis jedis, String lockKey, String requestId) {

    String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
    Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId));

    if (RELEASE_SUCCESS.equals(result)) {
        return true;
    }
    return false;

}

}
`

总结

先拿setnx来争抢锁,抢到之后,再用expire给锁加一个过期时间防止锁忘记了释放。

如果在setnx之后执行expire之前进程意外crash或者要重启维护了,那会怎么样?

set指令有非常复杂的参数,这个应该是可以同时把setnx和expire合成一条指令来用的。

Java类加载机制

双亲委派加载机制

双亲委派模型的工作流程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把请求委托给父加载器去完成,依次向上,因此,所有的类加载请求最终都应该被传递到顶层的启动类加载器中,只有当父加载器在它的搜索范围中没有找到所需的类时,即无法完成该加载,子加载器才会尝试自己去加载该类。

  1. 遇见一个类需要加载的类,它会优先让父加载器去加载。层层传递。
  2. 每个类加载器都有自己的加载区域,它也只能在自己的加载区域里面寻找。
  3. 自定义类加载器也必须实现这样一个双亲委派模型。
  4. 双亲委派机制是隔离的关键, 如String.class:
    1. 一个JVM里面只能有一个String.class。
    2. 用户没法自定义个String.class出来。
    3. 每个Classloader都有自己的加载区域,需要注意部分配置文件的存放地点。

类加载的隔离机制

类加载的隔离机制
通过不同的 完整类名 和 classloader, 可以区分两个类。好处为内存隔离(最常见的就是静态变量)。

  • 类名不一致一定不是同一个类
  • 类名一致类加载器不一致也不是同一个类(eaquels false)
  • 类名一致类加载器一致但是类加载器实例不一致也不是同一个类。

Tomcat提供了一个Child优先的类加载机制:首先由子类去加载, 加载不到再由父类加载。就很好的规避了Tomcat lib里面的包与webapps中用户的包互相冲突。WEB-INF/lib 目录下的类的加载优先级是优于Tomcat lib的。(配置文件在server.xml里面的 default false)上。

针对Tomcat, 做一个加载路径的介绍:

  • Tomcat起始于catalina.sh里面的命令 java org.apache.catalina.startup.Bootstrap start
  • 因为显式的指定了java命令,因此
    • BootstrapClassLoader负责加载${JAVA_HOME}/jre/lib部分jar包
    • ExtClassLoader加载${JAVA_HOME}/jre/lib/ext下面的jar包
    • AppClassLoader加载bootstrap.jartomcat-juli.jar(只显示的指定了这两个jar包)
    • 之后Tomcat通过初始化了三个URLClassLoader, 并指定加载路径 (见catalina.properties#common.loader配置)
    • 除了common外, server和shardLoader的加载路径一般都没有显示的指定, 因此这三个Loader实际上都是URLClassLoader。
    • 同时,它顺便指定了当前线程的contextClassLoader。
    • Tomcat对于WEB应用的启动都是依赖于web.xml的, 里面配置的Filter、Listener、Servlet根据Tomcat的定义都是由WebappClassLoaderBase来加载的。
    • 毕竟FilterListenerServlet等入口都是被WebappClassLoaderBase加载的,而一般开发者不会主动指定ClassLoader。那么除非指定了ClassLoader,所有的webapp都是它加载的(刚好它的加载空间包含了这些类)
    • 在需要Spring的时候已经由App自身加载得到, 就不会再去寻找Tomcat lib里面的Spring。
    • 自此,Tomcat的类加载区分完毕。 通过 “子优先” 这个机制,可以保证多个 Tomcat App 之间做到良好的隔离。

类加载的顺序

当需要用一个类的时候, 必须先加载它。

  1. 装载:查找和导入Class文件;
  2. 链接:把类的二进制数据合并到JRE中;
  3. 校验:检查载入Class文件数据的正确性;
  4. 准备:给类的静态变量分配存储空间;
  5. 解析:将符号引用转成直接引用;
  6. 初始化:对类的静态变量,静态代码块执行初始化操作

类加载的触发点

  • 遇到new、getstatic、putstatic或invokestatic这4条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。生成这4条指令的最常见的Java代码场景是:使用new关键字实例化对象的时候,读取或设置一个类的静态字段(被final修饰、已在编译期把结果放入常量池的静态字段除外)的时候,以及调用一个类的静态方法的时候。
    • 使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。
    • 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。

类的实例化

类只有在加载进入JVM之后才能被使用, 但是一般情况下还需要把类做实例化操作后来用。 一般区分为显式的实例化和隐式的示例化。

类的实例化的目的是为了得到一个类的对象。

  • new 方法为隐式的实例化。方便可用性高。
  • newInstance()为显式的实例化。必须要一个无参构造器。
  • 两种实例化方式都需要ClassLoader的参与。显式的实例化往往指定,隐式的实例化则默认是调用的这个类的类加载器。
  • 实例化的目的都是为了得到类的对象。而类在实例化之前已经初始化完毕。
  • 实例化的时候会为域变量赋值,并执行代码块的方法。

为什么静态元素和静态代码块在一个虚拟机里面只会执行一次 ?

  1. 默认习惯都是不会指定ClassLoader的,所属类也就只有一次初始化过程。
  2. 赋值静态域,或者执行静态代码块,是在类加载的流程中执行的。而这样的操作只会有一次。
  3. 赋值域,或者执行代码块,是在类实例化的流程中执行的,这样的操作根据程序需求可能有多次。

JVM的符号引用和直接引用

在JVM中类加载过程中,在解析阶段,Java虚拟机会把类的二级制数据中的符号引用替换为直接引用。

  1. 符号引用(Symbolic References): 符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能够无歧义的定位到目标即可。Java中,一个java类将会编译成一个class文件。在编译时,java类并不知道所引用的类的实际地址,因此只能使用符号引用来代替。

  2. 直接引用:
    直接引用可以是
    (1)直接指向目标的指针(比如,指向“类型”【Class对象】、类变量、类方法的直接引用可能是指向方法区的指针)
    (2)相对偏移量(比如,指向实例变量、实例方法的直接引用都是偏移量)
    (3)一个能间接定位到目标的句柄

直接引用是和虚拟机的布局相关的,同一个符号引用在不同的虚拟机实例上翻译出来的直接引用一般不会相同。如果有了直接引用,那引用的目标必定已经被加载入内存中了。

MySQL主从复制、半同步复制和主主复制概述

MySQL数据库支持同步复制、单向、异步复制,在复制的过程中一个服务器充当主服务,而一个或多个服务器充当从服务器。主服务器将更新写入二进制日志文件,并维护文件的一个索引以跟踪日志循环。这些日志可以记录发送到从服务器的更新。当一个从服务器连接主服务器时,它通知主服务器从服务器在日志中读取的最后一次成功更新的位置。从服务器接收从那时起发生的任何更新,然后封锁并等待主服务器通知新的更新。

主从复制的原理:

  1. 数据库有个bin-log二进制文件,记录了所有sql语句。
  2. 我们的目标就是把主数据库的bin-log文件的sql语句复制过来。
  3. 让其在从数据的relay-log重做日志文件中再执行一次这些sql语句即可。
  4. 下面的主从配置就是围绕这个原理配置

主从复制

原理

  1. 具体需要三个线程来操作:

    1. binlog输出线程:每当有从库连接到主库的时候,主库都会创建一个线程然后发送binlog内容到从库。

      在从库里,当复制开始的时候,从库就会创建两个线程进行处理:

    2. 从库I/O线程:当START SLAVE语句在从库开始执行之后,从库创建一个I/O线程,该线程连接到主库并请求主库发送binlog里面的更新记录到从库上。从库I/O线程读取主库的binlog输出线程发送的更新并拷贝这些更新到本地文件,其中包括relay log文件。

    3. 从库的SQL线程:从库创建一个SQL线程,这个线程读取从库I/O线程写到relay log的更新事件并执行。

可以知道,对于每一个主从复制的连接,都有三个线程。拥有多个从库的主库为每一个连接到主库的从库创建一个binlog输出线程,每一个从库都有它自己的I/O线程和SQL线程。