零碎的知识点-4
select、poll、epoll之间的区别总结
select、poll、epoll都是IO多路复用的机制。I/O多路复用就通过一种机制,可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作。但select,poll,epoll本质上都是同步I/O,因为他们都需要在读写事件就绪后自己负责进行读写,也就是说这个读写过程是阻塞的,而异步I/O则无需自己负责进行读写,异步I/O的实现会负责把数据从内核拷贝到用户空间。
select的几大缺点:
- 每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大
- 同时每次调用select都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很大
- 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时间。这就是回调机制带来的性能提升。
- 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的生命周期如下:
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等
执行过程文字描述
用户发起请求到前端控制器
DispatcherServlet
DispatcherServlet
请求处理器映射器HandlerMapping
查找Handler
可以是根据xml查找,也可以是根据注解查找HandlerMapping
向DispatcherServlet
返回一个执行链HandlerExecutionChain
,包含HandlerInterception
和Handler
HandlerMapping
调用处理器适配器HandlerAdapter
去执行Handler处理器适配器去执行
Handler
Handler执行完给处理器适配器返回
ModelAndView
ModelAndView
是SpringMVC框架的一个底层对象,包括Model和View处理器适配器给
DispatcherServlet
返回ModelAndView
DispatcherServlet
请求视图解析器View resolver
进行视图解析
根据逻辑视图解析成真正的物理视图(jsp等)视图解析器向
DispatcherServlet
返回viewDispatcherServlet
进行视图渲染DispatcherServlet
向用户响应结果
Java内存模型与共享变量可见性
Java内存模型(JMM)
Java内存模型的主要目标:定义在虚拟机中将变量储存到内存和从内存中取出变量这样的底层细节。
注意:上边的变量指的是共享变量(实例字段、静态字段、数组对象元素),不包括线程私有变量(局部变量、方法参数),因为私有变量不会存在竞争关系。
说明:
- 所有共享变量存于主内存
- 每一条线程都有自己的工作内存(就是上图所说的本地内存)
- 工作内存中保存了被该线程使用到的变量的主内存副本
注意:
- 线程对变量的操作都要在工作内存中进行,不能直接操作主内存
- 不同的线程之间无法直接访问对方的工作内存中的变量
- 不同线程之间的变量的传递必须通过主内存
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的两条规定:
- 线程解锁前,必须把共享变量的最新值刷新到主内存中
- 线程加锁时,将清空工作内存中共享变量的值,从而使用共享变量时需要从主存中重新读取最新的值
线程解锁前对共享变量的修改在下次加锁时对其他线程可见
线程执行互斥代码的过程
- 获得互斥锁
- 清空工作内存
- 从主内存拷贝变量的最新副本到工作内存
- 执行代码
- 将更改后的共享变量的值刷新到主内存中
- 释放互斥锁
volatile实现可见性
volatile关键字:
- 能够保证volatile变量的可见性
- 不能保证volatile变量复合操作的原子
volatile如何实现内存的可见性 :
深入来说:通过加入内存屏障和禁止重排序优化来实现的
- 在每个volatile写操作前插入StoreStore屏障,在写操作后插入StoreLoad屏障
- 在每个volatile读操作前插入LoadLoad屏障,在读操作后插入LoadStore屏障
通俗地讲:volatile变量在每次被线程访问时,都强迫从主内存中重读该变量的值,而当该变量发生变化时,又会强迫将最新的值刷新到主内存。这样任何时刻,不同的线程总能看到该变量的最新值。
线程写volatile变量的过程:
- 改变线程工作内存中volatile变量副本的值
- 将改变后的副本的值从工作内存刷新到主内存
线程读volatile变量的过程:
- 从主内存中读取volatile变量的最新值到线程的工作内存中
- 从工作内存中读取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引擎的前提下。
共享锁(Share Locks,即S锁),使用方式select … LOCK IN SHARE MODE
SELECT … FOR UPDATE同时只能有一个在语句执行,另一个会阻塞;SELECT … LOCK IN SHARE MODE可以多个同时执行(这也是和for update最大的区别)排它锁(Exclusive Locks,即X锁),使用方式:select … FOR UPDATE
select … for update锁住的行记录,其它事务不可修改,如果有修改会wait直到锁释放(事务commit)或超时
MySQL 乐观锁与悲观锁
悲观锁
悲观锁(Pessimistic Lock),顾名思义,就是很悲观,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会block直到它拿到锁。
乐观锁
乐观锁(Optimistic Lock),顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在提交更新的时候会判断一下在此期间别人有没有去更新这个数据。乐观锁适用于读多写少的应用场景,这样可以提高吞吐量。
乐观锁:假设不会发生并发冲突,只在提交操作时检查是否违反数据完整性。
乐观锁一般来说有以下2种方式:
使用数据版本(Version)记录机制实现,这是乐观锁最常用的一种实现方式。何谓数据版本?即为数据增加一个版本标识,一般是通过为数据库表增加一个数字类型的 “version” 字段来实现。当读取数据时,将version字段的值一同读出,数据每更新一次,对此version值加一。当我们提交更新的时候,判断数据库表对应记录的当前版本信息与第一次取出来的version值进行比对,如果数据库表当前版本号与第一次取出来的version值相等,则予以更新,否则认为是过期数据。
使用时间戳(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写数据的机制等等也都是通过缓存提升系统的吞吐量或者实现系统的保护措施。甚至消息中间件,你也可以认为是一种分布式的数据缓存。
降级
服务降级是当服务器压力剧增的情况下,根据当前业务情况及流量对一些服务和页面有策略的降级,以此释放服务器资源以保证核心任务的正常运行。降级往往会指定不同的级别,面临不同的异常等级执行不同的处理。根据服务方式:可以拒接服务,可以延迟服务,也有时候可以随机服务。根据服务范围:可以砍掉某个功能,也可以砍掉某些模块。总之服务降级需要根据不同的业务需求采用不同的降级策略。主要的目的就是服务虽然有损但是总比没有好。
限流
限流可以认为服务降级的一种,限流就是限制系统的输入和输出流量已达到保护系统的目的。一般来说系统的吞吐量是可以被测算的,为了保证系统的稳定运行,一旦达到的需要限制的阈值,就需要限制流量并采取一些措施以完成限制流量的目的。比如:延迟处理,拒绝处理,或者部分拒绝处理等等。
限流算法
计数器
计数器是最简单粗暴的算法。比如某个服务最多只能每秒钟处理100个请求。我们可以设置一个1秒钟的滑动窗口,窗口中有10个格子,每个格子100毫秒,每100毫秒移动一次,每次移动都需要记录当前服务请求的次数。内存中需要保存10次的次数。可以用数据结构LinkedList来实现。格子每次移动的时候判断一次,当前访问次数和LinkedList中最后一个相差是否超过100,如果超过就需要限流了。漏桶
漏桶算法即leaky bucket是一种非常常用的限流算法,可以用来实现流量整形(Traffic Shaping)和流量控制(Traffic Policing)。
漏桶算法的主要概念如下:
- 一个固定容量的漏桶,按照常量固定速率流出水滴;
- 如果桶是空的,则不需流出水滴;
3.可以以任意速率流入水滴到漏桶; - 如果流入水滴超出了桶的容量,则流入的水滴溢出了(被丢弃),而漏桶容量是不变的
漏桶算法比较好实现,在单机系统中可以使用队列来实现,在分布式环境中消息中间件或者Redis都是可选的方案。
- 令牌桶
令牌桶算法是一个存放固定容量令牌(token)的桶,按照固定速率往桶里添加令牌。令牌桶算法基本可以用下面的几个概念来描述:
- 令牌将按照固定的速率被放入令牌桶中。比如每秒放10个。
- 桶中最多存放b个令牌,当桶满时,新添加的令牌被丢弃或拒绝。
- 当一个n个字节大小的数据包到达,将从桶中删除n个令牌,接着数据包被发送到网络上。
- 如果桶中的令牌不足n个,则不会删除令牌,且该数据包将被限流(要么丢弃,要么缓冲区等待)。
漏桶和令牌桶的比较
令牌桶可以在运行时控制和调整数据处理的速率,处理某时的突发流量。放令牌的频率增加可以提升整体数据处理的速度,而通过每次获取令牌的个数增加或者放慢令牌的发放速度和降低整体数据处理速度。而漏桶不行,因为它的流出速率是固定的,程序处理速度也是固定的。
整体而言,令牌桶算法更优,但是实现更为复杂一些。