JDK是java标准开发包,提供了编译、运行所需要的各种工具和资源,包括java编译器、java运行时环境,以及常用的java类库等
JRE是java运行环境,用于运行编译后的字节码class文件。包括了JVM和JVM工作时所需要的类库。
JVM是java虚拟机,是负责运行字节码文件的
三者之间是一个包含关系,JDK中包括了JRE,而JRE中又包括了JVM。
我们开发时编写的.java文件,需要先使用JDK中的编译器javac来进行编译生成字节码文件。JVM就可以直接运行编译后的字节码文件了。
另外,JVM在运行字节码文件时,需要把字节码解释为机器指令,而不同的操作系统的机器指令是不同的,所以不同的操作系统上的JVM也不一样,最终就导致了我们在安装JDK的时候需要选择操作系统
判断一个对象是否相等,首先是调用该对象的hashCode()方法获取到hash值进行比较,如果不相等就表示这两个对象不相等,如果hash值相等再调用equals()方法判断是否相等。
如下图所示,一个User类中有一个name属性,如果要根据name属性的值来比较User对象是否相等的话就需要如下图所示重写hashCode()和equals()方法
通过equals()方法比较比较重,逻辑比较多,而hashCode()得到的实际上就是一个数字,相对轻量级,所以比较两个对象是否相同时通常会根据hashCode相比较一下。
< ? extends T >
表示包括T在内的任何T的子类< ? super T>
表示包括T在内的任何T的父类ArrayList和LinkedList都实现了List接口,区别是底层的实现方式不同:
JDK1.7版本:
ConcurentHashMap默认有16个Segment,每一个Segment可以看成一个HashMap,如果要进行扩容的话Segment是不会进行扩容的,只会是Segment内部的HashMap进行扩容,会创建一个容量*2的新数组,然后将扩容前旧HashMap中的元素一个一个的拷贝到新HashMap中去,然后在把Segment中的一个指针指向新HashMap
JDK1.8版本:
它只有一个数组了,扩容的时候就直接把当前这个数组进行扩容2两倍。那么现在就需要把旧数组中的key-value对象拷贝到新数组中来
那么是怎么拷贝的嘞?JDK8有一个多线程扩容的特性,有多个线程,每个线程负责拷贝一块区域的数据,这些多个线程同时对旧数组中的元素进行拷贝到新数组中
总结:
1.7版本
1.8版本
jdk1.7是先判断是否扩容再插入,JDK1.8是先插入再判断是否要扩容
JDK1.7版本
JDK1.8
先创建一个当前容量*2的新数组
遍历老数组中每个位置上链表或红黑树的元素
如果是链表就可JDK1.7的处理方式一样,直接计算出各个元素在新数组中的位置,并添加到新数组中去
如果是红黑树,则先遍历红黑树,计算每个元素在新数组中的位置
所有的元素转移完后,将新数组赋值给HashMap的table属性。
因为ArrayList不是线程安全的,所以我们需要一个线程安全的CopyOnWriteArrayList。它适用于读多写少的场景
我们通过JDK中的编译器javac将java源文件编译成字节码文件后,可以做到一次编译到处运行,windows编译好的.class文件,linux上可以直接运行,通过这种方式做到跨平台。
虽然字节码文件是通用的,但是需要把字节码文件解释成各个操作系统的机器码是需要不同的解释器的,所以我们需要针对不同的操作系统安装不同的JDK或JRE
采用个字节码的好处是,一方面实现了跨平台,另一方面也提高了代码执行的性能,编译器在编译源代码时可以做一些编译器的优化,比如锁消除、标量替换、方法内联等。
java中的所有异常都来自顶级父类Throwable,Throwable下有两个子类Exception和Error
Error表示非常严重的错误,比如OutOfMemoryError内存溢出,仅仅靠程序自己是解决不了的
Exception表示异常,当程序出现异常时可以靠程序自己来解决,Exception又分为运行期异常和编译期异常
在本方法中,是否能够合理的处理这个异常,如果能处理就进行捕获处理,如果不能处理就向上抛出
JDK自带三个类加载器:Bootstrap ClassLoader、ExtClassLoader、AppClassLoader
Bootstrap ClassLoader
是ExtClassLoader
的父类加载器,默认负责加载%JAVA_HOME%lib
下的jar包和class文件ExtClassLoader
是 AppClassloader
的父类加载器,复制加载%JAVA_HOME%/lib/ext
文件夹下的jar包和class文件AppClassloader
是自定义类加载器的父类,复制加载classpath下的类文件。JVM中存在三个类加载器:BootStrapClassLoader、ExtClassLoader、AppClassLoader
AppClassLoader的父类是ExtClassLoader,ExtClassLoader的父类是BootStrapClassLoader
JVM在加载一个类的时候,会通过AppClassLoader的loadClass()方法来加载这个类,在这个方法中会先调用ExtClassLoader类中的loadClass()方法来加载,同样,在ExtClassLoader类中的loadClass()方法中会先调用BootstrapClassLoader类的loadClass()方法来加载。如果BootstrapClassLoader类加载到了就直接成功,如果没有加载到就通过ExtClassLoader来加载,如果还是没有加载成功再通过AppClassLoader来加载。
所以,双亲委派指的是,JVM在加载类时,会委派给Ext和Bootstrap进行加载,如果没有加载到才由自己来进行加载
堆和方法区是线程共享的,栈、本地方法栈、程序计数器是每个线程独享的。
栈:
堆
方法区:
比如下方的以为数组内存图,栈中存储方法相关的信息包括局部变量等,堆中存放创建好了的对象,然后局部变量在指向堆中的对象地址。
对于还在正常运行的系统:
对于已经发生OOM的系统:
一般生产系统中都会设置当系统发生了OOM时,生成当时的dump文件(设置如下2个参数)
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp/heapdump.hprof
-XX:+HeapDumpOnOutOfMemoryError 设置当首次遭遇内存溢出时导出此时堆中相关信息
-XX:HeapDumpPath=/tmp/heapdump.hprof 指定导出堆信息时的路径或文件名
我们可以利用jsisualvm等工具来分析dump文件
根据dump文件找到异常的实例对象和异常的线程(占用cpu高),定位到具体的代码
然后再进行详细的分析和调试。
STW:stop the world,停止整个世界。是在垃圾回收算法执行过程中,将JVM的内存内存冻结的一种状态。在STW状态下,java除了GC线程之外的线程都停止执行。所以JVM调优的重点就是减少STW。
JVM的启动参数非常多,常用的JVM配置参数也就10来个,如下所示
# 设置堆内存
-Xmx4g -Xms4g
# 指定GC算法
-XX:+UseG1GC -XX:MaxGCPauseMillis=50
# 指定GC并行线程数
-XX:ParallelGCThreads=4
# 打印GC日志
-XX:+PrintGCDetails -XX:+PrintGCDateStamps
# 指定GC日志文件
-Xloggc:gc.log
# 指定Mete区的最大值
-XX:MaxMetaspaceSize=2g
# 设置单个线程栈的大小
-Xss1m
# 指定对内存溢出时自动进行Dump
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp/heapdump.hprof
线程安全指的是某一段代码,在多线程同时执行的时候不会产生混乱,能够得到正常的结果。就比如i++ ,i的初始组为0,如果两个线程都执行这行代码,那么i的结果应该是2。如果出现了两个线程的结果都是1,则表示这段代码不是线程安全的。
所以线程安全这段就是多个线程同时执行一段代码能否得到正确的结果。
在java中线程分为用户线程和守护线程。用户线程就是普通的线程,而守护线程是JVM的后台线程,垃圾回收就是守护线程,守护线程和普通该线程的一个主要区别就是,普通线程执行完后就停止了,而守护线程是会一直运行的,当所有普通线程都停止运行之后守护线程才会自动关闭。
我们可以通过thread.setDaemon(true)
来把一个线程设置为守护线程。
public class Test {
private ThreadLocal<String> threadLocal = new ThreadLocal<String>();
public void setName(){
threadLocal.set("胡尚");
}
public void getName(){
String name = threadLocal.get();
threadLocal.remove();
}
}
源码如下:
// 首先获取当前线程对象,然后获取当前线程对象中的属性ThreadLocalMap,再往这里面存值,key是我们创建的ThreadLoca对象,value是我们要存的值
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
// 首先获取当前线程对象,然后获取当前线程对象中的属性ThreadLocalMap,再通过key获取到Entry对象再获取到value返回
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
// 我们可以创建多个ThreadLocal对象,往当前线程中的ThreadLocalMap中存储多个值,特别的当前线程还是线程池中的线程时,这个线程就不会轻易被回收,那么线程中的ThreadLocalMap就会越来越大,就有可能造成内存泄漏,所以当我们确定某个值使用完成后就及时的调用remove()方法删除
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
首先我们需要知道造成死锁的原因:
这是造成死锁的四个必要条件,如果要打破死锁只要不满足其中任意一个条件即可。而其中前三个条件是锁的必要条件,所以要避免死锁就需要打破第四个条件,不出现循环等待锁的关系。
线程池内部是通过队列+线程实现的。当我们利用线程池执行任务时:
先要保证线程池中的线程数量达到核心线程数corePoolSize。
如果此时线程池中的线程数小于corePoolSize,即使线程池中的线程处于空间状态也要先创建新的线程达到corePoolSize后再来处理被添加的任务
线程池数量等于corePoolSize,并且都在执行任务没有空闲,但是缓存队列workQueue未满,那么任务被放入到缓存队列中
线程池数量>=corePoolSize,并且workQueue也满了,但是线程池的数量
如果线程次数量>=corePoolSize,并且workQueue也满了,线程池的数量等于maxmumPoolSize,那么就通过handler指定的拒绝策略来处理该任务
当线程池中的数量>=corePoolSize,如果某些线程空闲时间超过了keepAliveTime,该线程将被终止。
总结:先创建核心线程数的线程–>再用缓存队列–>再创建线程达到最大线程数–>如果还不行就触发拒绝策略 --> 如果线程数大于了核心线程数,线程的空闲时间超过了keepAliveTime线程就被终止。
拿生活的例子来说,本来公司10个程序员能够正常进行业务的开发,随着公司的发展业务需求越来越多,需求就先写在需求列表中,程序员加班加点还是勉勉强强能完成,当需求列表都写满了,就不得不再招人了。
创建线程和销毁线程需要cpu资源的,就比如公司只是某一段时间需求很多,这个时候招人与辞退需要成本的。
首先不管是公平锁还是非公平锁,都是基于AQS排队机制的。区别是 线程在使用lock()加锁时,如果是公平锁,线程首先会检查AQS队列中是否有线程再进行排队,如果有就跟着排队;而非公平锁是不管AQS队列中是否有线程再排队,都去尝试直接竞争锁
不管是公平锁还是非公平锁,一旦没有竞争到锁都会进入到AQS队列中排队,当锁释放时,都是唤醒排在最前面的线程,所以非公平锁只是体现在了线程加锁阶段,而没有体现在线程被唤醒阶段
另外ReentrantLock是可重入锁,不管是公平锁或非公平锁都是可重入的。
synchronize和ReentrantLock都是可重入锁,可重入锁是线程得到了当前对象的锁后,可以在锁中再次进入带有锁的方法。
CountDownLatch表示计数器,可以给CountDownLatch设置一个数字,线程调用await()方法将会阻塞,其他线程调用countDown()方法将会对数字减一,当数字被减成0后所有await的线程都将被唤醒。
对应的底层原理就是,调用await()方法的线程会利用AQS排队,一旦数字被减为0,则会将AQS中排队的线程依次唤醒
Semaphore表示信号量,可以设置许可的个数,表示同时允许多少个线程使用该信号量,通过调用acquire()来获取许可,如果没有许可则阻塞,并通过AQS队列排队,可以通过release()方法释放许可,当某个线程释放许可后,会从AQS中正在排队的第一个线程依次唤醒,直到没有空闲许可。
AQS同步队列,在AQS中主要是维护了一个state变量和一个双向链表的队列,这个队列是给线程去排队的,里面存储的就是一个一个的线程对象,state是一个标记,用来控制线程去排队或者获取锁放行。
在可重入锁的场景下,state表示加锁的次数。0表示没有加锁,没加一次锁,state就加1,释放锁就减1。
TCP协议是传输层中的协议,负责数据的可靠传输。
在建立TCP连接时需要通三次握手来建立,过程是:
在断开连接时,TCP需要通过四次挥手来断开,过程是:
spring的两大特性:IOC和APO;IOC就是控制反转
我们可以从以下几个点来分析:
我们在使用Spring的时候,我们会定义类,并使用@Autowrite注解,这个时候获取到的对象是已经创建好了的并且属性中也是有值的,这就是控制,将对象的创建和对象属性的赋值交给了spring来控制,如果不使用spring,那么对象的控制权就需要我们自己来进行,而使用spring就将对象的控制权转移给了Spring,这就是反转
如果不使用反转,那么类的创建与属性的赋值就需要我们自己来进行,就比如现在有三个类:
我们需要编写的代码如下
A a = new A();
B b = new B();
C c = new C();
a.c = c;
b.c = c;
如果类多一点,属性多一点,我们就需要写非常多的这些代码。所以我们可以将控制权交由spring来控制。
总结:IOC是控制反转,如果用Spring,那么对象的创建以及对象属性的赋值将由spring来进行,对象的控制权将交由spring。
单例模式表示JVM中某个类的对象只会存在一个
而单例Bean并不表示JVM中只能存在唯一的某个类的Bean对象,单例Bean是表示在spring容器中,通过一个bean的名字获取到的是同一个对象。就比如User类型的对象可以在Spring容器中存在多个,但是他们的名字可能叫user1、user2、user3, 我们通过bean的名字获取到的是同一个对象。
事务的传播机制是:在使用spring事务的过程中,我们调用一个方法会开启一个事务,当这个方法再去调用其他方法的时候要不要开启一个新的事务,还是说共用一个事务,还是不以事务的方式运行这些都是可以去选择的。
如下图所示有七种事务传播行为
Spring事务的原理是AOP,要查看事务是否会失效主要就是看是否是Spring事务所产生的代理对象在调用这个方法。
首先Spring并没有对Bean做线程安全的处理,Bean其实就是一个对象,这个对象是不是线程安全的还是要看这个Bean本身
有状态指的就是如果有方法对Bean内部属性的值有修改操作,那么这个bean 就是有状态的。
首先ApplicationContext和BeanFactory都是spring的Bean工厂,可以生成Bean,维护Bean。
从源码中可以知道,ApplicationContext接口继承了BeanFactory接口。区别是ApplicationContext除了继承BeanFactory接口之外还继承了很多其他的接口,也就还有很多其他的功能,比如:获取系统环境变量、国际化、事件发布等功能
// 其中的ListableBeanFactory, HierarchicalBeanFactory都继承了BeanFactory接口
// 所以ApplicationContext也继承了BeanFactory接口
public interface ApplicationContext extends EnvironmentCapable, ListableBeanFactory, HierarchicalBeanFactory, MessageSource, ApplicationEventPublisher, ResourcePatternResolver {
String getId();
String getApplicationName();
String getDisplayName();
long getStartupDate();
ApplicationContext getParent();
AutowireCapableBeanFactory getAutowireCapableBeanFactory() throws IllegalStateException;
}
首先我们知道Spring的事务是基于AOP来实现的,那么具体大致的实现步骤如下所示:
对于使用了@Transaction注解的Bean,spring就会创建一个代理对象
当代理对象去调用方法时会判断方法上面是否加了@Transaction注解
如果加了,那么就会利用事务管理器创建一个数据库连接对象
将自动提交autocommit改为false
执行当前方法,方法中 执行sql
执行完成后如果没有出现异常就提交事务
如果出现才异常,并且这个异常是需要回滚的就回滚事务,否则仍然提交
spring的事务隔离级别对应的是数据库的隔离级别
spring事务的传播机制是Spring事务自己实现的,也是Spring事务最复杂的的,它是基于数据库连接来做的,一个数据库连接对应一个事务,如果事务传播机制需要重新开一个事务实际上就是重新创建一个数据库连接,在此新数据库连接上执行sql。
Spring要使用的话肯定是需要先启动Spring容器的,启动Spring容器的主要目的是为了创建Bean对象做一些准备。
在启动Spring时,首先会进行扫描,扫描得到所有的BeanDefinition对象,并存入Map中
扫描筛选出非懒加载的单例BeanDefinition进行创建Bean。多例Bean不需要在启动的时候进行创建,它会在每次获取Bean时利用BeanDefinition去创建。懒加载的也是一样的。
利用BeanDefinition创建Bean,也就是Bean的创建生命周期,包括合并BeanDefinition、推断构造函数、实例化、依赖注入进行属性赋值、初始化前@PostConstruct注解、初始化、初始化后的步骤 AOP
Bean创建完成后,会发布一个容器启动事件
Spring容器启动结束
在源码中会更加复杂,比如源码中会提供一些模板方法让子类实现、源码中还涉及到BeanFactoryPostProcessor和BeanPostProcessor的注册,Spring的扫描就是在BeanFactoryPostProcessor来实现的,依赖注入就是通过BeanPostProcessor来实现的、还有@Import等注解的处理
以jar包发布springboot项目时,默认会先使用jar包同级目录下的application.properties来作为项目配置文件。但使用–spring.config.location指定了配置文件,则读取指定的配置文件。
如果在不同的目录中存在多个配置文件,它的读取顺序是:
优点
缺点:
#{}是预编译处理、是占位符;可以防止SQL注入,提高系统安全性
${}是字符串替换、是拼接符
mybatis在处理#{}时,会将SQL中的#{}替换为? 然后调用PreparedStatement来赋值
mybatis在处理 时,会将 S Q L 中的 {}时,会将SQL中的 时,会将SQL中的{}替换成变量的值,调用Stetement来赋值
Consistency一致性:更新操作发生后,所有的节点在同一时间的数据要保证完全一致
Availability可用性:即服务一直是可用的状态
Partition Tolerance分区容错性:即分布式系统在遇到某个节点或网络分区故障的时候,仍然能够对外提供满足一致性和可用性的服务。比如分布式系统中某几个服务器宕机了,剩下的服务器还能够正常运转满足系统的需求,对于用户而言并没有什么体验上的影响。
CP和AP:分区容错性是必须保证的,当发生网络分区的时候,如果要继续提供服务,那么强一致性和可用性只能二选一。在分布式集群中,某一个节点与其他节点网络断开了,这就是网络分区,这个时候有一些更新操作,这时数据是不一致的,那么就需要选择了,一是继续对外提供服务,但是暂时数据不一致;二是先保证数据一致性,但是在处理一致性的过程中暂时不能对外提供服务,所以只能二选一
BASE理论
BASE理论是对CAP中一致性和可用性权衡的结果,是基于CAP理论逐步演化而来的。BASE理论的核心思想是:即使无法做到强一致性,但每个应用都可以根据自身业务特点采用适当的方式来使系统达到最终一致性
基本可用:
软状态:数据同步允许一定的延迟
最终一致性:系统中所有的数据副本在经过一段时间的同步后,最终能够达到一个一致的状态,不要求实时。
RPC,远程过程调用,对于java这种面向对象语言来说我们可以翻译为远程方法调用。
PRC调用和HTTP调用是有区别的,RPC表示的是一种调用远程方法的方式,我们只要保证A进程中的方法能够调用B进程中的方法就行了,底层可以使用HTTP协议或者直接基于TCP协议来实现RPC。
在java中,我们可以通过直接使用某个服务器接口的代理对象来执行方法,而底层则通过构造HTTP请求来调用远端的方法,所以有一种说法是RPC协议是HTTP协议之上的一种协议。
在单体架构中,加锁是为了解决多个线程去访问同一个资源的并发安全。ReentrantLock、Synchronized都是一个进程中去控制多个线程的锁机制。
而在分布式架构中,有多个进程分别运行在不同的服务器上面,这种情况下我们也需要去控制一些共享的资源就需要使用分布式锁
现在主流的分布式锁的实现方案有两种:
ZAB协议是zookeeper用来实现一致性的原子广播协议,该协议描述了zookeeper是如何实现一致性的,分为三个阶段:
但值得注意的是,zookeeper只是尽量的在达到强一致性,实际上仍然只是最终一致性
因为网路等其他问题,可能客户会在短时间内点击多次按钮,那么后台会就接收到多次请求,这就是幂等性问题,当前前端可以使用防抖解决。
幂等性:在高并发场景下,相同的请求参数,重复点击按钮发送请求,能不能保证数据是正确的
实现方法如下:
也就是分布式id问题,分库分表之后的主键如何生成
服务雪崩:在微服务调用链路中,由于某个服务不可用导致上游服务不停的挂掉,解决方法就是服务降级和服务熔断
服务限流:对访问服务的请求进行数量上的限制
服务熔断:服务A调用服务B不可用时,为了保证本身服务不受影响,从而不再调用服务B,直接返回一个结果,直到服务B恢复
服务降级:当发现系统压力过载时,可以通过关闭某个服务或者限流某个服务来减轻系统压力
服务熔断是下游服务故障触发的,服务降低是为了降低系统的负载
DDD是一个思想,一个方法论 。它不是一个技术框架。会利用它在微服务拆分中作为一个指导的思想
中台就是将业务线上可以复用的一些功能抽取出来,剥离个性,抽取共性,形成一些可复用的组件。
大体上,中台可以分为三类:业务中台、数据中台和技术中台
中台和DDD结合,DDD会通过界限上下文将系统拆分成一个一个的领域,而这种界限上下文,天生就成了中台之间的逻辑屏障
DDD在技术与资源调度方面都能够给中台建设提供不错的指导
DDD分为战略设计和战术设计。上层的战略设计能够很好的指导中台划分,下层的战术设计能够很好的指导微服务搭建。
索引其实就是对某些字段的值进行排序的数据结构。
就是我们在为表创建索引时,需要遵守的一些原则,主要目的就是使查询更快、占用空间更小
事务的基本特性
原子性:一个事务中的操作要么全部成功、要么全部失败。靠Undo Log来实现的
一致性:指的是数据库从一个一致性的状态转换为另一个一致性的状态。靠其他三个特性来保证的
持久性:事务进行提交后的操作是会永久保存到数据库的。靠Redo Log来实现的
隔离性:一个事务的的修改操作在提交前对其他事务是不可见的。靠锁来实现的
隔离级别:
MVCC,多版本并发控制。
指的是在读已提交和可重复读这两种隔离级别下,事务在执行select操作时并不是去读取数据库中真正存储的数据而是会去访问记录的版本链的过程。多版本的意思就是一条数据的多个版本会形成一个链表,如果是多个事务同时来读取的话,会根据事务的id和版本链上面的事务id进行一个比对,从而返回当前这个事务应该看到的一个数据。
这两种隔离级别不同点就是生成ReadView的时机不同,READ COMMITTD在每次进行普通的select操作前都会生成一个ReadView,而REPEATABLE READ只在第一次进行select操作前生成一个ReadView,之后的查询操作都重复使用这个ReadView。
MyISAM
InnoDB
索引覆盖指的是,一个SQL在执行时,可以使用索引来快速查找,并且这个SQL所需要查询的所有字段该索引对应的字段中都包含了,所需要查询的字段在当前索引的叶子节点中都包含,不需要进行回表查询了。
当一个SQL想要利用索引时,那么就一定要在查询条件中有创建索引时最左边的字段,至于放在什么位置不重要,查询优化器在底层会进行优化。
之所以要满足最左前缀原则是因为B+树在底层对字段进行排序时,首先就是按照创建索引时指定的第一个字段的大小进行排序的,第一个字段相同的情况下再去使用第二个字段,以此类推。最终创建好一个B+树。
Mysql中的B+树其实是B树的一个升级,主要增加了两个特征:
Mysql之所以使用B+树,原因是使用B+树,树的高度不会太高,B+树的一个节点对应的InnoDB的一页,默认16KB。在非叶子结点中,它如果不存储数据仅仅存储索引那么一个节点中就能够存储更多的元素,进而降低树的高度。
按锁的粒度分为:
还可以分为:
还可以分为:
RDB(Redis DataBase),在指定的时间间隔内将内存中的数据集快照写入磁盘,实际操作过是fork一个子进程,先将数据集写入临时文件,写入成功后在替换之前的dump.rdb文件,用二进制压缩存储
优点:
缺点:
AOF(Append Only File),以日志的形式记录服务器所处理的每一个更新操作,查询不会被记录,以文本的方式记录
优点:
缺点
总结AOF文件比RDB文件更新频率高,有限使用AOF还原数据,AOF比RDB更安全,RDB比AOF性能好,如果两个都配置了优先加载AOF。
redis中同时使用了惰性过期和定期过期两种过期策略。
还有一种定时过期,但是redis没有采用这种策略。它是为每个设置过期时间的key都创建一个定时器,到期就立刻清理,缺点是会占用大量cpu资源
redis的事务是把多条redis命令放在一个队列中一起执行,如果出现了命令语法有问题那么所有的命令都不会执行,如果是set key value时 key冲突这种问题,那么其他命名正常执行。
事务开始
MULTI
命令的执行,标识着一个事务的开始。MULTI
命令会将客户端状态的flags
属性中打开REDIS_MULTI
标识来完成的
命令入队
当一个客户端切换到事务状态之后,服务器会根据这个客户端发送的命令来执行不同的操作。
如果客户端发送的命令是MULTI EXEC WATCH DISCARD
中的一个,立即执行这个命令
其他命令,首先检查此命令的语法格式是否正确,如果不正确服务器会在客户端状态(redisClient)的flags属性关闭REDIS_MULTI
标识,并返回错误信息给客户端,如果正确,将命令放入一个事务队列里面,然后向客户端返回QUEUED
回复
事务执行
客户端发送EXEC命令,服务器执行EXEC命令逻辑。
REDIS_MULTI
标识,或者包含REDIS_DIRTY_CAS
或者REDIS_DIRTY_EXEC
表示,那么就直接取消事务的执行redis不支持事务回滚机制,但是它会检查每一个事务中的命令是否有错误。
通过slaveof命令让一个服务器去复制另一个服务器中的数据。主库可以读写操作,当写操作导致数据改变时会自动将数据同步给从数据库。从数据库只读,接收主数据库同步过来的数据。
我们需要先明白两个概念
全量复制:
增量复制:
过程原理:
主从复制的流程如下:
redis提供了三种集群策略
如果Redis中数据量不大可以选择哨兵模式,如果要存的数据量大并且需要持续的库容就选择cluster模式
缓存的目的是存放热点数据,请求可以直接从缓冲中获取而不用访问mysql
缓冲雪崩:某一时刻,大量的热点数据同时过期,那么就有可能导致大量请求直接访问mysql了;
解决方法:
在过期时间上加一点随机值不要同一时刻过期
缓存预热,主要针对系统刚启动时,缓存中是没有数据的,所以大量请求就直接访问mysql
互斥锁,可以在方法上加锁,不让大量的请求直接落在mysql中,只处理一些请求,就比如sentinel限流策略
缓存击穿:和缓存雪崩类似,缓存雪崩是大量特点数据过期,缓存击穿是某一个热点数据过期,也导致了大量请求直接访问mysql数据库,解决方法:
缓存穿透:缓存和数据库中都没有数据,导致所有的请求都落到数据库上,造成数据库短时间内承受大量请求而宕机
解决方法:
如果数据库中没有查询到,可以存一个key-null进缓存,缓存时间可以设置短一点30s。
使用布隆过滤器,它的作用是如果它认为一个key不存在那么这个key就肯定不存在,所以可以在缓存之前加一层布隆过滤器来拦截不存在的key。
延时双删:先删除redis中的数据,更新mysql,延迟几百毫秒之后再删除redis中的数据。
Redis的数据是保存在内存中的,那么宕机就会造成数据丢失,所以就还需要持久化的机制将数据保存在文件中,redis有两种持久化机制:RDB和AOF
RDB:Redis DataBase 将某一时刻的内存快照,以二进制的方式写入磁盘
手动触发:
save命令:使redis处于阻塞状态,知道rdb持久化完成后才会响应其他客户端发送来的命令。
bgsave命令:fork出一个子进程执行持久化,主进程只在fork过程中有短暂的阻塞,子进程创建完成后主进程就可以响应客户端请求了。
这里有一个问题,两个进程都在进行操作,主进程在执行写操作,那么如何保证子进程生成的rdb快照文件的数据是正确的嘞?
redis的主进程和子进程都是在操作一块共享内存空间,数据都是存储在这里,Redis是利用COW(copy on write)机制来解决生成的快照文件就是这个时刻的数据。当子进程在使用共享内存空间生成rdb快照文件时,假如这个时候主进程触发了写操作,主进程会把共享内存中要写的这条数据拷贝出来,copy出一个副本,在副本中进行修改操作,此时子进程读取的还是原来共享内存空间的数据,最后再把副本中的数据写回去。
自动触发:
优点:
缺点:
AOF :append only file 以日志的形式记录服务器所处理的每一个写、删除操作以文件的方式记录,可以打开文件看到详细的操作记录
同步策略:
优点:
缺点:
AOF文件比RDB文件更新效率高,优先使用AOF还原数据
AOF比RDB更安全也更大
RDB新能比AOF好
如果两个都配置了有限加载AOF
布隆过滤器就是一个int数组,就比如长度为10的数组int[10],每个int类型的整数占4*8=32bit,则int[10]共有320bit,每个比特位是二进制非0即1,初始化时都是0 。
添加数据时,首先将key进行哈希运算,得到一个哈希值,这个哈希值对应一个bit位,将对应的bit位改为1。哈希函数可以定义多个,假如定义3个哈希函数,key经过三个哈希运算后会得到三个哈希值,将这三个哈希对应的bit位上的值都改为1。多个哈希函数的目的是减少哈希冲突
查询数据时,首先对key经过哈希运算,对应的bit位上,如果有一个0,则表示数据不在bit中,如果都为1则表示数据可能在bit中。
优点:
缺点
在数据量很大时,一台redis能存储是数据量是有限的,那么就要使用cluster模式的集群,一个redis存储一部分的key。在查询的时候就需要寻址算法了。
hash算法:根据key进行hash运算,结果和分片数取模,确定分片
优点实现简单,适用于固定分片的场景
缺点是扩展分片或减少分片时,所有的数据都需要重新计算分片,重新存储
一致性hash:将整个hash值的区间组织成一个闭合的圆环,计算每台服务器的hash值、映射到圆环中。使用相同的的hash算法计算数据的hash值,映射到圆环中,顺时针寻址,找到的第一个服务器就是数据存储的服务器。
新增或减少节点时只会影响到它逆时针最近的一个服务器之间的值
存在hash倾斜的问题,即服务器分布不均匀,可以通过虚拟节点解决
hash slot:hash槽,将数据与服务器隔离开,数据与slot映射,slot与服务器映射,数据进行hash决定存放的slot,新增即删除节点时,将slot进行迁移即可。
RocketMQ由NameServer集群、producer集群、consumer集群、broker集群组成。
消息生产和消费的大致原理如下:
消息可靠传输代表了两层意思,既不能多也不能少。
消息不能多,就是消息不能重复,生成者不能重复生成消息,消费者也不能重复消费消息
确保生产者消息不多发,这个不常出现,也比较难控制,因为如果出现了多次,很大的原因是生产者自己的原因,如果要避免出现问题,就需要在消费端做控制,最保险的机制就是消费者实现幂等性,保证就算重复消费也不会有问题。
消息不能少,就是消息不能丢失,要保证生产者可靠生产消息,消费者可靠消费消息
生产者发送消息时,要确认Broker确实收到了并持久化了这条消息,比如RabbitMQ的confirm机制,Kafka的ack机制都可以保证生产者能正确的将消息发送给Broker
Broker要等待消费者真正确认消费到了消息才会删除掉消息,通常就是消费端的ack机制,消费者接收到一条消息后,如果确认没问题了就向Broker发送一个ack,Broker接收到ack后才会删除消息
零拷贝:kafka和RocketMQ都是通过零拷贝技术来优化文件读写
传统方式中,程序要执行一个复制文件操作需要进行四次拷贝,程序是运行在用户空间的,用户空间不能直接访问硬件,所以需要先拷贝到内核空间,然后拷贝到用户空间,程序操作完成后再拷贝回去。所需要经历的过程如下图所示:
零拷贝有两种方式,mmap和transfile。
mmap就是省略掉内核空间到用户空间的拷贝,用户空间就不拿内核空间的完整内容了,只是拿文件的一个映射,映射主要包括文件的内存地址、长度等信息,用户空间的操作就不操作文件了,只是对映射做一些修改,实际上整个文件是在内核空间中直接完成读写,这样就少了两次文件拷贝
还有一种方式就是在底层的时候使用DMA技术, 它允许不同的设置共同访问一个内存空间,这样就不需要cpu进行大量的中断负载,减少cpu的消耗
所以零拷贝并不是真正的不进行文件拷贝了,其实还是有两次拷贝,只是内核空间不会往用户空间进行拷贝了。
java中对零拷贝进行了API封装,mmap通过MapedByteBuffer对象操作,trannsfile通过FileChannel来进行操作
mmap适合比较小的文件,通常文件大小不要超过1.5G~2G
transfile没有文件大小限制
RocketMQ采用Mmap方式来对它的文件进行读写。
kafka当中,它的index日志文件也是通过mmap方式来读写的,其他的文件当中并没有使用零拷贝的方式,它使用transfile方式将硬盘数据加载到网卡。