Java精简攻略
2023-06-16 09:06:33 6 举报
AI智能生成
奥里给
作者其他创作
大纲/内容
技术栈
CPU
LINUX
TCP、IP
Java
反射
枚举
io
字节流
字符流
nio
selector
channal
buffer
List
ArrayList
jdk1.7初始化的时候就分配10个容量,jdk8初始化不分配,add元素时才会分配10个元素
CopyOrWriteArrayList、Collections.synchronizedList(),
扩容为1.5倍
LinkedList
链表,添加元素使用尾插法,不用考虑效率问题,保存了尾结点
ArrayList插入或删除元素一定比LinkedList慢么
在数据量大的情况下,因为ArrayList底层数组, LinkedList底层双向链表。ArrayList增删时,越靠前头部,增删效率越低,因为ArrayList增删的时候是需要拷贝数组的。而LinkedList当增删、查找效率都不是很高,特别是对象处于链表中部位置
所以当插入删除元素在中间/或者随机查找的时候,数据量大的情况下,ArrayList可能会比LinkedList快。
所以当插入删除元素在中间/或者随机查找的时候,数据量大的情况下,ArrayList可能会比LinkedList快。
论遍历ArrayList要⽐LinkedList快得多,ArrayList遍历最⼤的优势在于内存的连续性,CPU的内部缓存 结构会缓存连续的内存⽚段,可以⼤幅降低读取内存的性能开销
LinkedList.get(index)具体是怎么实现的
判断是链表的前半段还是后半段,再去遍历
hashmap
数组 链表 红黑树
每个节点非红即黑,根节点是黑色、红节点的子节点必须是黑色、叶子结点必须是黑的空节点、每天链路的黑色节点数目一致
JDK1.7之前使用头插法、JDK1.8使用尾插法
默认容量16
太小了就有可能频繁发生扩容,影响效率。太大了又浪费空间,不划算。
扩容因子 0.75
扩容2倍,位置会重新计算,可能是原下表或者是原下标加旧数组长度
hashcode相等,两个对象不一定相等,node会存在链表中
node大于8,变成红黑树,小于6,变回链表,这样会减少变换的次数
hash的实现
通过hashCode()的高16位异或低16位实现的,果当哈希值的高位变化很大,低位变化很小,这样就很容易造成哈希冲突了,所以这里把高低位都利用起来
用异或运算符
保证了对象的hashCode的32位值只要有一位发生改变,整个hash()返回值就会改变。尽可能的减少碰撞。
使用红黑树的原因,是因为二叉树在极端情况变成链表,红黑树可以通过左旋 右旋和变色来保持二叉树的平衡,但是旋转和变色是需要消耗资源的
LinkedHashMap保存了记录的插入顺序,在用Iterator遍历时,先取到的记录肯定是先插入的;遍历比HashMap慢;
TreeMap实现SortMap接口,能够把它保存的记录根据键排序(默认按键值升序排序,也可以指定排序的比较器)
TreeMap实现SortMap接口,能够把它保存的记录根据键排序(默认按键值升序排序,也可以指定排序的比较器)
HashMap和HashTable有什么区别?
①、HashMap是线程不安全的,HashTable是线程安全的;
②、由于线程安全,所以HashTable的效率比不上HashMap;
③、HashMap最多只允许一条记录的键为null,允许多条记录的值为null,而HashTable不允许;
④、HashMap默认初始化数组的大小为16,HashTable为11,前者扩容时,扩大两倍,后者扩大两倍+1;
⑤、HashMap需要重新计算hash值,而HashTable直接使用对象的hashCode;
①、HashMap是线程不安全的,HashTable是线程安全的;
②、由于线程安全,所以HashTable的效率比不上HashMap;
③、HashMap最多只允许一条记录的键为null,允许多条记录的值为null,而HashTable不允许;
④、HashMap默认初始化数组的大小为16,HashTable为11,前者扩容时,扩大两倍,后者扩大两倍+1;
⑤、HashMap需要重新计算hash值,而HashTable直接使用对象的hashCode;
concurentHashmap
JVM
内存模型
程序计数器
本地方法栈
虚拟机栈
堆
方法区
本地方法栈
虚拟机栈
堆
方法区
try-catch-finally
try关键字监控代码块,有异常会goto到指定的异常表中的异常,最后会执行finally代码,如果finally中有return关键字,会弹至操作栈顶。
i++
会在栈中的局部变量表中和操作数栈来回操作
1 + "" = "1"
对象回收算法
引用计数法
占内存,浪费时间,循环引用
可达性分析法
四种引用
强、软、弱、虚
垃圾回收算法
标记清除
标记整理
复制
gc大概流程
首先对象会分配在eden区,年轻代空间不足时,会触发minor gc,将eden区和from区幸存的对象复制到to区,存活的年龄+1,再交换from和to,当然,minor gc会触发STW,存活的年龄到达阈值,对象晋升至老年代,如果老年代空间不足,首先触发minor gc,如果仍然不足,触发full gc。
CMS
1)初始标记(CMSinitialmark)
2)并发标记(CMSconcurrentmark)
3)重新标记(CMSremark)
4)并发清除(CMSconcurrentsweep)
2)并发标记(CMSconcurrentmark)
3)重新标记(CMSremark)
4)并发清除(CMSconcurrentsweep)
G1
ZGC
排查工具
jps 工具 查看当前系统中有哪些 java 进程
jmap 工具 查看堆内存占用情况 jmap - heap 进程id
jconsole 工具 图形界面的,多功能的监测工具,可以连续监测
jvisualvm
MAT
jmap 工具 查看堆内存占用情况 jmap - heap 进程id
jconsole 工具 图形界面的,多功能的监测工具,可以连续监测
jvisualvm
MAT
arthas
调优
发现FullGC前后的内存差距很大,排查发现有sql查大表,返回的数据很大,所以优化了一下,改用缓存去存的
发现Minor GC频繁,扩大年轻代到百分之45,默认为堆内存的三分之一,官网推荐年轻代占堆内存的百分之二十五到百分之五十
请求高峰期发生 Full GC,单次暂停时间特别长 (CMS),-XX:+CMSScavengeBeforeRemark:在重新标记之前在新生代执行一次垃圾回收,减少扫描对象,减轻扫描压力和时间消耗以及gc压力。
Excel导出,发生oom,直接内存unsafe,后用中间件做的
多态实现
方法表,接口或父类的方法引用指向子类或实现类的方法表中的方法
反射优化,16次缓存
类加载流程
加载
将字节码加载至方法区
验证
验证魔术,比如版本等
准备
给变量初始化,如果是static final,则直接赋值
解析
将字符引用变成直接引用
初始化
类初始化
类加载器
启动类加载器
扩展类加载器
应用加载器
双亲委派机制
查找类的规则,首先看父类加载器是否加载,如果没加载,再自己加载
破坏双亲委派机制
双亲委派机制是jdk1.2出现的,但是类加载的设计在之前就有,所以做出了妥协,增加了一个protect findClass
线程上下文类加载器,比如jdbc就是用Java SPI机制完成类的加载,ServiceLoader
热部署,自己实现了类加载,在触发热部署时,会连同类加载器一同更换掉,而且类加载机制用的不是官网推荐的树状结构,是网状结构
自定义类加载器
继承 ClassLoader 父类
要遵从双亲委派机制,重写 findClass 方法
不是重写 loadClass 方法,否则不会走双亲委派机制
要遵从双亲委派机制,重写 findClass 方法
不是重写 loadClass 方法,否则不会走双亲委派机制
new Object()
占16个字节,对象头64位就是8字节,类指针压缩是4字节,不压缩是8字节,默认是压缩的,对齐(被8整除)
new dup init
markword
锁、线程、id
synchronized
在代码块前后加了监控,步入和步出,在jvm内部维护了一个锁的模型,有owner,wait,entryList
锁升级
无锁
01
偏向锁在mark world维护了一个偏向锁标识,0无锁,1有锁,存了线程id,在jdk15已经被移除(用锁就说明有并发场景),偏向锁默认程序启动一秒之后启动
轻量级锁,实现机制是用锁记录,自旋10次,锁膨胀,晋升
00
重量级锁
10
volatile
保证内存可见性和禁止指令重排序
cpu三级缓存
每次线程读取数据会从高速缓存中读取
hotspot是用lock关键字实现在volitile读写操作前后加读写屏障
缓存行的数据写回到内存
清除缓存了该地址的数据
cas
比较并交换,c调用cpu指令cmpxchg
JUC
线程创建
继承Thread
实现Runable
实现Callable和Future
线程池
单线程,固定线程数的,缓存的,定时的
自定义线程池
核心线程 最大线程 线程存活时间 时间单位 队列 线程工厂 拒绝策略
拒绝策略
AbortPolicy - 抛出异常,中止任务(默认)
CallerRunsPolicy - 使用调用线程执行任务
DiscardPolicy - 直接丢弃
DiscardOldestPolicy - 丢弃队列最老任务,添加新任务
线程池执行流程
线程状态
new runable blocked wait timedwait terminated
sleep 和 yield
sleep 会让当前线程从 Running 进入 Timed Waiting 状态
调用 yield 会让当前线程从 Running 进入 Runnable 就绪状态
线程优先级priority
几乎没用
join
interrupt
// 打断当前线程,但是打断sleep,wait,join时,会清除打断状态。
thread.interrupt();
// 可清除打断标记
Thread.interrupted();
thread.interrupt();
// 可清除打断标记
Thread.interrupted();
守护线程
随着用户线程的结束而结束,所以springboot内嵌的tomcat中,将守护线程设置为false
wait notify notifyAll
obj.wait() 让进入 object 监视器的线程到 waitSet 等待
obj.notify() 在 object 上正在 waitSet 等待的线程中挑一个唤醒
obj.notifyAll() 让 object 上正在 waitSet 等待的线程全部唤醒
obj.notify() 在 object 上正在 waitSet 等待的线程中挑一个唤醒
obj.notifyAll() 让 object 上正在 waitSet 等待的线程全部唤醒
Park & Unpark
LockSupport.park()可以被interrupt()打断,并且不会被清除打断标记,也可以通过LockSupport.unpark(thread1)方法结束LockSupport.park()并继续执行。
LongAdder
分段锁 + cell + cpu伪共享
AQS
同步工具,它提供了一种用于构建锁和同步器的框架。
ReentrantLock
ReentrantLock是一个可重入的独占锁
公平锁和非公平锁
条件变量
ReentrantReadWriteLock
ReentrantReadWriteLock 是一个基于 AQS 实现的可重入读写锁
锁降级
StampedLock
StampedLock还支持优化的读操作和乐观锁模式
CountdownLatch
计数器
CyclicBarrier和Semaphore
CyclicBarrier的计数器用于等待所有线程都到达某一点后再继续执行,
而Semaphore的计数器用于控制同时访问某一资源的线程数量。
而Semaphore的计数器用于控制同时访问某一资源的线程数量。
Phaser
Phaser与CyclicBarrier和CountDownLatch等同步工具类似,但提供了更为灵活和高效的同步机制。
ConcurrentHashMap
jdk1.7
把哈希桶分为多个段,每个段包含多个hashEntry,再把将每个段上锁
JDK1.8
Node数组+链表+红黑树
锁住这个链表头节点(红黑树的根节点)
put
根据key算出hash,定位到node,如果为空,则通过cas的方式添加,不满足则加synchronized方式去添加
get
根据key算出hash,定位node,如果是首节点,直接返回,不是就查找红黑树,如果不是红黑树就遍历链表
ConcurrentHashMap 不支持 key 或者 value 为 null 的原因
因为并发情况下,不知道是value存的是null,还是没查到,对于hashmap为什么可以,因为hashmap可以用containsKey方法区判断是后存在key
JDK1.8 中为什么使用内置锁 synchronized替换 可重入锁 ReentrantLock?
在 JDK1.6 中,对 synchronized 锁的实现引入了大量的优化,并且 synchronized 有多种锁状态,会从无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁一步步转换。
减少内存开销 。假设使用可重入锁来获得同步支持,那么每个节点都需要通过继承 AQS 来获得同步支持。但并不是每个节点都需要获得同步支持的,只有链表的头节点(红黑树的根节点)需要同步,这无疑带来了巨大内存浪费。
减少内存开销 。假设使用可重入锁来获得同步支持,那么每个节点都需要通过继承 AQS 来获得同步支持。但并不是每个节点都需要获得同步支持的,只有链表的头节点(红黑树的根节点)需要同步,这无疑带来了巨大内存浪费。
ConcurrentHashMap 迭代器是弱一致性
Spring
IOC
Bean生命周期
构建BeanDrfinetionMap
- Spring启动
- 扫描(BenaFactoryPostProcessor)
- 生成BeanDefinition
- put进BeanDefinitionMap
实例化
实例化前
- BeanDefinitionRegistryPostProcessor
- BeanFactoryPostProcessors
实例化
单例BeanDefinition
- 遍历BeanDefinitionMap
- 筛选出单例的BeanDefinition
推断创建Bean的构造方法
通过@Autowired等
通过反射创建对象
初始化
初始化前
Aware
BeanNameAware
BeanClassLoaderAware
BeanFactoryAware
BeanPostProcessor#postProcessBeforeInitialization
初始化
@PostConstruct
InitializingBean
@Bean(initMethod = "myInitMethod")
初始化后
BeanPostProcessor#postProcessAfterInitialization
@PreDestroy
Destroyable
@Bean(destroyMethod = "myDestroyMethod")
容器生命周期
SmartLifecycle
SmartLifecycle -- isAutoStartup方法
SmartLifecycle -- getPhase方法
Lifecycle -- isRunning方法
SmartLifecycle -- isAutoStartup方法
SmartLifecycle -- isAutoStartup方法
Lifecycle -- start方法
SmartLifecycle -- stop方法
Lifecycle -- stop方法
AOP
代理切换
Spring5默认Aop代理为JDK动态代理;SpringBoot2.0默认代理为CgLib代理,可在配置文件配置spring.aop.proxy-target-class=false,将Cglib代理切换为Jdk动态代理
创建代理对象
1. 众所周知,AOP代理对象是在Bean的初始化之后创建
// 属性填充
populateBean(beanName, mbd, instanceWrapper);
// 代理对象创建,如果不需要代理,则返回原始bean
exposedObject = initializeBean(beanName, exposedObject, mbd);
populateBean(beanName, mbd, instanceWrapper);
// 代理对象创建,如果不需要代理,则返回原始bean
exposedObject = initializeBean(beanName, exposedObject, mbd);
2. 初始化完毕后,执行后置处理器的postProcessAfterInitialization方法,其中AbstractAutoProxyCreator为核心处理器
核心方法:获取这个bean的advice集合,判断是否需要代理,推断代理策略,创建代理
核心方法:获取这个bean的advice集合,判断是否需要代理,推断代理策略,创建代理
@Override
public Object postProcessAfterInitialization(@Nullable Object bean, String beanName) {
if (bean != null) {
Object cacheKey = getCacheKey(bean.getClass(), beanName);
if (this.earlyProxyReferences.remove(cacheKey) != bean) {
// 核心方法
return wrapIfNecessary(bean, beanName, cacheKey);
}
}
return bean;
}
public Object postProcessAfterInitialization(@Nullable Object bean, String beanName) {
if (bean != null) {
Object cacheKey = getCacheKey(bean.getClass(), beanName);
if (this.earlyProxyReferences.remove(cacheKey) != bean) {
// 核心方法
return wrapIfNecessary(bean, beanName, cacheKey);
}
}
return bean;
}
3. 获取这个bean的advice集合
// Create proxy if we have advice.
Object[] specificInterceptors = getAdvicesAndAdvisorsForBean(bean.getClass(), beanName, null);
Object[] specificInterceptors = getAdvicesAndAdvisorsForBean(bean.getClass(), beanName, null);
3. 选择代理策略
public AopProxy createAopProxy(AdvisedSupport config) throws AopConfigException {
if (config.isOptimize() || config.isProxyTargetClass() || hasNoUserSuppliedProxyInterfaces(config)) {
Class<?> targetClass = config.getTargetClass();
...
if (targetClass.isInterface() || Proxy.isProxyClass(targetClass) || ClassUtils.isLambdaClass(targetClass)) {
return new JdkDynamicAopProxy(config);
}
return new ObjenesisCglibAopProxy(config);
}
if (config.isOptimize() || config.isProxyTargetClass() || hasNoUserSuppliedProxyInterfaces(config)) {
Class<?> targetClass = config.getTargetClass();
...
if (targetClass.isInterface() || Proxy.isProxyClass(targetClass) || ClassUtils.isLambdaClass(targetClass)) {
return new JdkDynamicAopProxy(config);
}
return new ObjenesisCglibAopProxy(config);
}
4. 创建代理对象
初始化cglib的字节码增强器Enhancer后,就开始创建代理对象
proxyInstance = objenesis.newInstance(proxyClass, enhancer.getUseCache());
代理对象invoke流程
文字描述
执行around拦截器,前置
执行around拦截器,准备调用目标方法
准备调用的这个目标方法中,会调用before拦截器
紧接着会调用after拦截器,在after拦截器中,首先会真正的执行目标方法,然后在执行自己的after方法
然后,将执行完的after方法、before方法依次弹栈,回到around方法,执行around方法的后置代码
执行around拦截器,准备调用目标方法
准备调用的这个目标方法中,会调用before拦截器
紧接着会调用after拦截器,在after拦截器中,首先会真正的执行目标方法,然后在执行自己的after方法
然后,将执行完的after方法、before方法依次弹栈,回到around方法,执行around方法的后置代码
图解
代理对象invoke流程图解
通知顺序
@Around -- before
@Before
目标方法
@AfterThrowing
@AfterReturning
@After
@Around -- after
api
JoinPoint
- getArgs(): 返回连接点的参数。
- getKind(): 返回连接点的类型。
- getSignature(): 返回连接点的签名。
- getStaticPart(): 返回连接点静态表示的信息。
- getSourceLocation(): 返回连接点在源代码中的位置。
- getThis(): 返回当前正在执行的对象。
ProceedingJoinPoint
- getArgs(): 返回连接点参数。
- getKind(): 返回连接点类型。
- getSignature(): 返回连接点签名。
- getStaticPart(): 返回连接点静态表示的信息。
- getSourceLocation(): 返回连接点在源代码中的位置。
- getThis(): 返回当前正在执行的对象。
- proceed(): 使用连接点上的参数调用目标方法,并返回结果。
- proceed(Object[] args): 使用指定的参数数组调用目标方法,并返回结果。
- proceedWithInvocation(): 调用目标方法,使用连接点上的原始参数数组。
- toLongString(): 返回连接点详细描述。
- toShortString(): 返回连接点简短描述。
Signature
- getDeclaringTypeName():获取定义该方法的类的名称。
- getMethod():获取被通知的方法对象。
- getName():获取被通知的方法的名称。
- getParameterTypes():获取被通知方法的参数类型数组。
- getReturnType():获取被通知方法的返回类型。
- toLongString():获取完整的方法签名,包括返回类型、方法名、参数类型等信息。
- toString():获取简要的方法签名,只包括方法名和参数类型。
事务
执行流程
创建代理对象
- 首先获取切面列表(其中包含事务属性相关)
- 创建事务代理对象
执行事务
获取事务属性
Spring容器初始化的时候,已经获取过切面列表,这里直接从缓存中获取即可
根据当前事务的传播行为来创建事务
- 获取数据库连接对象
- 关闭事务自动提交
- 激活事务
- 设置超时时间
执行目标方法
回滚事务
如果有异常抛出,根据异常判断事务是否需要回滚
事务流程图解
隔离级别
在SPRNG中,支持以下五种事务隔离级别:
DEFAULT:使用底层数据存储的默认隔离级别。
READ_UNCOMMITTED:允许脏读,也就是说,某个事务可以读取另一个事务尚未提交的数据。
READ_COMMITTED:禁止脏读,但是允许不可重复读和幻读。也就是说,一个事务只能读取已经提交的数据,但是可能会看到其他事务已提交但还未提交的更改。
REPEATABLE_READ:禁止脏读和不可重复读,但是允许幻读。也就是说,一个事务在读取某行数据后,不允许其他事务对该行进行修改,直到本事务结束。
SERIALIZABLE:禁止脏读、不可重复读和幻读。也就是说,一个事务在读取某行数据后,其他事务不能对该行进行任何形式的修改,直到本事务结束。
需要注意的是,隔离级别越高,事务的安全性越高,但是并发性越低,因为需要加锁来保证一致性。因此,在选择隔离级别时需要根据具体场景进行权衡。
DEFAULT:使用底层数据存储的默认隔离级别。
READ_UNCOMMITTED:允许脏读,也就是说,某个事务可以读取另一个事务尚未提交的数据。
READ_COMMITTED:禁止脏读,但是允许不可重复读和幻读。也就是说,一个事务只能读取已经提交的数据,但是可能会看到其他事务已提交但还未提交的更改。
REPEATABLE_READ:禁止脏读和不可重复读,但是允许幻读。也就是说,一个事务在读取某行数据后,不允许其他事务对该行进行修改,直到本事务结束。
SERIALIZABLE:禁止脏读、不可重复读和幻读。也就是说,一个事务在读取某行数据后,其他事务不能对该行进行任何形式的修改,直到本事务结束。
需要注意的是,隔离级别越高,事务的安全性越高,但是并发性越低,因为需要加锁来保证一致性。因此,在选择隔离级别时需要根据具体场景进行权衡。
传播方式
事务传播行为定义了方法在执行过程中如何参与到已经存在的事务中,或者如何开始一个新的事务。Spring支持以下七种事务传播行为:
REQUIRED(默认):如果当前存在事务,则加入该事务;如果不存在,则创建一个新的事务。
REQUIRES_NEW:创建一个新的事务,并在它自己的事务中运行。如果当前存在事务,则将当前事务挂起。
SUPPORTS:如果当前存在事务,则加入该事务;如果不存在,则以非事务方式运行。
MANDATORY:如果当前存在事务,则加入该事务;如果不存在,则抛出异常。
NOT_SUPPORTED:以非事务方式运行,如果当前存在事务,则将其挂起。
NEVER:以非事务方式运行,如果当前存在事务,则抛出异常。
NESTED:如果当前存在事务,则在嵌套事务中执行。如果不存在,则执行REQUIRED类似的操作。
需要注意的是,不同的传播行为对应不同的事务行为,选择合适的事务传播行为可以保证业务的正确性和事务的一致性。同时,需要注意不同的传播行为可能会带来不同的性能开销,因此需要根据具体场景进行选择。
REQUIRED(默认):如果当前存在事务,则加入该事务;如果不存在,则创建一个新的事务。
REQUIRES_NEW:创建一个新的事务,并在它自己的事务中运行。如果当前存在事务,则将当前事务挂起。
SUPPORTS:如果当前存在事务,则加入该事务;如果不存在,则以非事务方式运行。
MANDATORY:如果当前存在事务,则加入该事务;如果不存在,则抛出异常。
NOT_SUPPORTED:以非事务方式运行,如果当前存在事务,则将其挂起。
NEVER:以非事务方式运行,如果当前存在事务,则抛出异常。
NESTED:如果当前存在事务,则在嵌套事务中执行。如果不存在,则执行REQUIRED类似的操作。
需要注意的是,不同的传播行为对应不同的事务行为,选择合适的事务传播行为可以保证业务的正确性和事务的一致性。同时,需要注意不同的传播行为可能会带来不同的性能开销,因此需要根据具体场景进行选择。
MVC
执行流程
1. 在启动Tomcat过程中,会创建DispatcherServlet对象,并执行它的初始化逻辑
2. DispatcherServlet初始化过程中会创建Spring容器(根据用户的Spring配置)
3. 然后初始化过程中还是初始化HandlerMapping、HandlerAdapter等等
4. SpringMVC中默认提供了好几个HandlerMapping,其中有一个为RequestMappingHandlerMapping
5. RequestMappingHandlerMapping的作用是去寻找Spring容器中有哪些加了@RequestMapping的方法
6. 找到这些方法后,就会解析该注解上的信息,包含了指定的path,然后就把path作为key,Method作为value存到一个map中
7. 当DispatcherServlet接收到请求后,RequestMappingHandlerMapping就会负责根据请求路径从map中找到对应的Method
8. 然后准备执行Method,只不过,在执行Method之前,会解析该方法的各个参数
9. 比如参数前面加了@RequestParam注解,那SpringMVC就会解析该注解,并从请求中取出对应request param中的数据传给该参数
10. 解析完各个参数并从请求中拿到了对应的值之后,就会执行方法了
11. 执行完方法得到了方法返回值后,SpringMVC会进一步解析
12. 比如方法上如果加了@ResponseBody,那么就直接把返回值返回给浏览器
13. 如果方法上没有加@ResponseBody,那么就要根据返回值找到对应的页面,并进行服务端渲染,再把渲染结果返回给浏览器
2. DispatcherServlet初始化过程中会创建Spring容器(根据用户的Spring配置)
3. 然后初始化过程中还是初始化HandlerMapping、HandlerAdapter等等
4. SpringMVC中默认提供了好几个HandlerMapping,其中有一个为RequestMappingHandlerMapping
5. RequestMappingHandlerMapping的作用是去寻找Spring容器中有哪些加了@RequestMapping的方法
6. 找到这些方法后,就会解析该注解上的信息,包含了指定的path,然后就把path作为key,Method作为value存到一个map中
7. 当DispatcherServlet接收到请求后,RequestMappingHandlerMapping就会负责根据请求路径从map中找到对应的Method
8. 然后准备执行Method,只不过,在执行Method之前,会解析该方法的各个参数
9. 比如参数前面加了@RequestParam注解,那SpringMVC就会解析该注解,并从请求中取出对应request param中的数据传给该参数
10. 解析完各个参数并从请求中拿到了对应的值之后,就会执行方法了
11. 执行完方法得到了方法返回值后,SpringMVC会进一步解析
12. 比如方法上如果加了@ResponseBody,那么就直接把返回值返回给浏览器
13. 如果方法上没有加@ResponseBody,那么就要根据返回值找到对应的页面,并进行服务端渲染,再把渲染结果返回给浏览器
组件
DispatcherServlet
DispatcherServlet 是整个 Spring MVC 框架的核心,它是一个 Servlet,所有的请求都会经过它。DispatcherServlet 的主要作用是将请求分派给相应的 HandlerMapping,然后将处理结果返回给客户端。DispatcherServlet 在初始化时会加载一些配置文件,比如 applicationContext.xml、spring-mvc.xml 等等。这些配置文件中定义了 Spring MVC 的各个组件,包括 HandlerMapping、HandlerAdapter、ViewResolver 等等。
DispatcherServlet 是整个 Spring MVC 框架的核心,它是一个 Servlet,所有的请求都会经过它。DispatcherServlet 的主要作用是将请求分派给相应的 HandlerMapping,然后将处理结果返回给客户端。DispatcherServlet 在初始化时会加载一些配置文件,比如 applicationContext.xml、spring-mvc.xml 等等。这些配置文件中定义了 Spring MVC 的各个组件,包括 HandlerMapping、HandlerAdapter、ViewResolver 等等。
在初始化bean之后
// Instantiate all remaining (non-lazy-init) singletons.
finishBeanFactoryInitialization(beanFactory);
// Last step: publish corresponding event.
finishRefresh();
// Instantiate all remaining (non-lazy-init) singletons.
finishBeanFactoryInitialization(beanFactory);
// Last step: publish corresponding event.
finishRefresh();
tomcat启动时,servlet生命周期开始,调用init()方法,最终调用DispatcherServlet#onRefresh方法,
完成初始化。
完成初始化。
HandlerMapping
HandlerMapping 用来映射请求到相应的处理器,它根据请求的 URL 和其他条件来确定最终的处理器。Spring MVC 中提供了多种 HandlerMapping 实现,比如 BeanNameUrlHandlerMapping、RequestMappingHandlerMapping、SimpleUrlHandlerMapping 等等。其中,RequestMappingHandlerMapping 是最常用的 HandlerMapping 实现,它会扫描应用程序中所有带有 @Controller 注解的类,并将其中所有带有 @RequestMapping 注解的方法注册为处理器。
HandlerMapping 用来映射请求到相应的处理器,它根据请求的 URL 和其他条件来确定最终的处理器。Spring MVC 中提供了多种 HandlerMapping 实现,比如 BeanNameUrlHandlerMapping、RequestMappingHandlerMapping、SimpleUrlHandlerMapping 等等。其中,RequestMappingHandlerMapping 是最常用的 HandlerMapping 实现,它会扫描应用程序中所有带有 @Controller 注解的类,并将其中所有带有 @RequestMapping 注解的方法注册为处理器。
SpringBoot自动装配的时候,会注入WebMvcAutoConfiguration类
@ConditionalOnMissingBean(WebMvcConfigurationSupport.class)
public class WebMvcAutoConfiguration {
public class WebMvcAutoConfiguration {
WebMvcConfigurationSupport内部注入了@Bean【RequestMappingHandlerMapping】
RequestMappingInfoHandlerMapping父类的父类实现了InitializingBean接口,RequestMappingInfoHandlerMapping实现了afterPropertiesSet方法
重写了InitializingBean接口,就可以在bean初始化的时候,最终会保存进MappingRegistry#registry,在registry这个HashMap中:
1. 请求方法和路径({GET [/user/test]})作为key
2. 方法名(public java.lang.String com.ossa.web3.controller.TestController.transaction())作为value。
1. 请求方法和路径({GET [/user/test]})作为key
2. 方法名(public java.lang.String com.ossa.web3.controller.TestController.transaction())作为value。
for (String beanName : getCandidateBeanNames()) {
if (!beanName.startsWith(SCOPED_TARGET_NAME_PREFIX)) {
// put进registry这个HashMap中
processCandidateBean(beanName);
}
}
if (!beanName.startsWith(SCOPED_TARGET_NAME_PREFIX)) {
// put进registry这个HashMap中
processCandidateBean(beanName);
}
}
HandlerAdapter
HandlerAdapter 用来确定请求处理器的类型,并调用相应的方法来处理请求。Spring MVC 中提供了多种 HandlerAdapter 实现,比如 HttpRequestHandlerAdapter、SimpleControllerHandlerAdapter、AnnotationMethodHandlerAdapter 等等。其中,AnnotationMethodHandlerAdapter 是最常用的 HandlerAdapter 实现,它会根据方法的参数类型和返回值类型动态地确定请求处理器的类型,并调用相应的方法来处理请求。
HandlerAdapter 用来确定请求处理器的类型,并调用相应的方法来处理请求。Spring MVC 中提供了多种 HandlerAdapter 实现,比如 HttpRequestHandlerAdapter、SimpleControllerHandlerAdapter、AnnotationMethodHandlerAdapter 等等。其中,AnnotationMethodHandlerAdapter 是最常用的 HandlerAdapter 实现,它会根据方法的参数类型和返回值类型动态地确定请求处理器的类型,并调用相应的方法来处理请求。
RequestMappingHandlerAdapter实现了InitializingBean
初始化时处理@ControllerAdvice注解
初始化时处理@ControllerAdvice注解
ViewResolver
ViewResolver 用来将逻辑视图名解析为实际的视图对象,它根据逻辑视图名和其他条件来确定最终的视图对象。Spring MVC 中提供了多种 ViewResolver 实现,比如 InternalResourceViewResolver、FreeMarkerViewResolver、VelocityViewResolver 等等。其中,InternalResourceViewResolver 是最常用的 ViewResolver 实现,它会将逻辑视图名解析为 JSP 文件名,并返回一个 InternalResourceView 对象。
ViewResolver 用来将逻辑视图名解析为实际的视图对象,它根据逻辑视图名和其他条件来确定最终的视图对象。Spring MVC 中提供了多种 ViewResolver 实现,比如 InternalResourceViewResolver、FreeMarkerViewResolver、VelocityViewResolver 等等。其中,InternalResourceViewResolver 是最常用的 ViewResolver 实现,它会将逻辑视图名解析为 JSP 文件名,并返回一个 InternalResourceView 对象。
ModelAndView
ModelAndView 是 Spring MVC 中最常用的视图模型对象,它包含了视图名称和模型数据。在处理请求时,请求处理器将模型数据填充到 ModelAndView 对象中,并返回一个 ModelAndView 对象作为处理结果。DispatcherServlet 会将 ModelAndView 对象传递给 ViewResolver,ViewResolver 会使用视图名称和模型数据来渲染响应结果。
ModelAndView 是 Spring MVC 中最常用的视图模型对象,它包含了视图名称和模型数据。在处理请求时,请求处理器将模型数据填充到 ModelAndView 对象中,并返回一个 ModelAndView 对象作为处理结果。DispatcherServlet 会将 ModelAndView 对象传递给 ViewResolver,ViewResolver 会使用视图名称和模型数据来渲染响应结果。
图解
简图
流程图
生命周期
servlet > filter > interceptor
拦截器
代码实现
实现拦截器,需要实现HandlerInterceptor接口,该接口内部含有三个默认的方法,前置和后置分别在目标方法的前后执行,afterCompletion方法,在最后执行并且prehandle方法返回true才会执行。
@Component
public class MyInterceptor1 implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
System.out.println("前置 == 1 ==");
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
System.out.println("后置 == 1 ==");
}
public class MyInterceptor1 implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
System.out.println("前置 == 1 ==");
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
System.out.println("后置 == 1 ==");
}
@Component
public class MyInterceptor2 implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
System.out.println("前置 == 2 ==");
return HandlerInterceptor.super.preHandle(request, response, handler);
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
System.out.println("后置 == 2 ==");
HandlerInterceptor.super.postHandle(request, response, handler, modelAndView);
}
}
public class MyInterceptor2 implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
System.out.println("前置 == 2 ==");
return HandlerInterceptor.super.preHandle(request, response, handler);
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
System.out.println("后置 == 2 ==");
HandlerInterceptor.super.postHandle(request, response, handler, modelAndView);
}
}
@Configuration
public class MyWebConfig implements WebMvcConfigurer {
@Autowired
private MyInterceptor1 myInterceptor1;
@Autowired
private MyInterceptor2 myInterceptor2;
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 数越小,权重越高,越先执行,默认order为0,按添加的顺序
registry.addInterceptor(myInterceptor1).order(0);
registry.addInterceptor(myInterceptor2).order(1);
}
}
public class MyWebConfig implements WebMvcConfigurer {
@Autowired
private MyInterceptor1 myInterceptor1;
@Autowired
private MyInterceptor2 myInterceptor2;
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 数越小,权重越高,越先执行,默认order为0,按添加的顺序
registry.addInterceptor(myInterceptor1).order(0);
registry.addInterceptor(myInterceptor2).order(1);
}
}
原理
通过SpringBoot的SPI机制,自动装配WebMvcAutoConfiguration类,这个类有一个配置类EnableWebMvcConfiguration,
此配置类又会自动注入@Bean【WelcomePageHandlerMapping】,该类会解析所有拦截器,包括自定义的,然后缓存起来。
此配置类又会自动注入@Bean【WelcomePageHandlerMapping】,该类会解析所有拦截器,包括自定义的,然后缓存起来。
@Bean
public WelcomePageHandlerMapping welcomePageHandlerMapping(ApplicationContext applicationContext,
FormattingConversionService mvcConversionService, ResourceUrlProvider mvcResourceUrlProvider) {
WelcomePageHandlerMapping welcomePageHandlerMapping = new WelcomePageHandlerMapping(
new TemplateAvailabilityProviders(applicationContext), applicationContext, getWelcomePage(),
this.mvcProperties.getStaticPathPattern());
welcomePageHandlerMapping.setInterceptors(getInterceptors(mvcConversionService, mvcResourceUrlProvider));
welcomePageHandlerMapping.setCorsConfigurations(getCorsConfigurations());
return welcomePageHandlerMapping;
}
public WelcomePageHandlerMapping welcomePageHandlerMapping(ApplicationContext applicationContext,
FormattingConversionService mvcConversionService, ResourceUrlProvider mvcResourceUrlProvider) {
WelcomePageHandlerMapping welcomePageHandlerMapping = new WelcomePageHandlerMapping(
new TemplateAvailabilityProviders(applicationContext), applicationContext, getWelcomePage(),
this.mvcProperties.getStaticPathPattern());
welcomePageHandlerMapping.setInterceptors(getInterceptors(mvcConversionService, mvcResourceUrlProvider));
welcomePageHandlerMapping.setCorsConfigurations(getCorsConfigurations());
return welcomePageHandlerMapping;
}
protected final Object[] getInterceptors(
FormattingConversionService mvcConversionService,
ResourceUrlProvider mvcResourceUrlProvider) {
if (this.interceptors == null) {
InterceptorRegistry registry = new InterceptorRegistry();
addInterceptors(registry);
registry.addInterceptor(new ConversionServiceExposingInterceptor(mvcConversionService));
registry.addInterceptor(new ResourceUrlProviderExposingInterceptor(mvcResourceUrlProvider));
this.interceptors = registry.getInterceptors();
}
return this.interceptors.toArray();
}
FormattingConversionService mvcConversionService,
ResourceUrlProvider mvcResourceUrlProvider) {
if (this.interceptors == null) {
InterceptorRegistry registry = new InterceptorRegistry();
addInterceptors(registry);
registry.addInterceptor(new ConversionServiceExposingInterceptor(mvcConversionService));
registry.addInterceptor(new ResourceUrlProviderExposingInterceptor(mvcResourceUrlProvider));
this.interceptors = registry.getInterceptors();
}
return this.interceptors.toArray();
}
在发起一个请求时,会在真正执行目标方法的前后去执行拦截器对应的方法
过滤器
第一种
import jakarta.servlet.*;
import java.io.IOException;
import jakarta.servlet.FilterConfig;
public class MyFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
System.out.println("filter === 1 === 初始化");
Filter.super.init(filterConfig);
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException, IOException {
System.out.println("filter 1 开始执行");
filterChain.doFilter(servletRequest, servletResponse);
}
@Override
public void destroy() {
System.out.println("filter === 1 === 销毁");
Filter.super.destroy();
}
}
import java.io.IOException;
import jakarta.servlet.FilterConfig;
public class MyFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
System.out.println("filter === 1 === 初始化");
Filter.super.init(filterConfig);
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException, IOException {
System.out.println("filter 1 开始执行");
filterChain.doFilter(servletRequest, servletResponse);
}
@Override
public void destroy() {
System.out.println("filter === 1 === 销毁");
Filter.super.destroy();
}
}
@Configuration
public class FilterConfig {
@Bean
public FilterRegistrationBean<Filter> baseFilter(){
FilterRegistrationBean<Filter> filterRegistrationBean = new FilterRegistrationBean<>();
filterRegistrationBean.setFilter(new MyFilter());
filterRegistrationBean.setUrlPatterns(Collections.singletonList("/*"));
filterRegistrationBean.setOrder(1);
return filterRegistrationBean;
}
@Bean
public FilterRegistrationBean<Filter> baseFilter2(){
FilterRegistrationBean<Filter> filterRegistrationBean = new FilterRegistrationBean<>();
filterRegistrationBean.setFilter(new MyFilter2());
filterRegistrationBean.setUrlPatterns(Collections.singletonList("/*"));
filterRegistrationBean.setOrder(2);
return filterRegistrationBean;
}
}
public class FilterConfig {
@Bean
public FilterRegistrationBean<Filter> baseFilter(){
FilterRegistrationBean<Filter> filterRegistrationBean = new FilterRegistrationBean<>();
filterRegistrationBean.setFilter(new MyFilter());
filterRegistrationBean.setUrlPatterns(Collections.singletonList("/*"));
filterRegistrationBean.setOrder(1);
return filterRegistrationBean;
}
@Bean
public FilterRegistrationBean<Filter> baseFilter2(){
FilterRegistrationBean<Filter> filterRegistrationBean = new FilterRegistrationBean<>();
filterRegistrationBean.setFilter(new MyFilter2());
filterRegistrationBean.setUrlPatterns(Collections.singletonList("/*"));
filterRegistrationBean.setOrder(2);
return filterRegistrationBean;
}
}
第二种
import jakarta.servlet.*;
import java.io.IOException;
import jakarta.servlet.FilterConfig;
import jakarta.servlet.annotation.WebFilter;
@WebFilter(urlPatterns = {"/*"})
public class MyFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
System.out.println("filter === 1 === 初始化");
Filter.super.init(filterConfig);
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException, IOException {
System.out.println("filter 1 开始执行");
filterChain.doFilter(servletRequest, servletResponse);
}
@Override
public void destroy() {
System.out.println("filter === 1 === 销毁");
Filter.super.destroy();
}
}
import java.io.IOException;
import jakarta.servlet.FilterConfig;
import jakarta.servlet.annotation.WebFilter;
@WebFilter(urlPatterns = {"/*"})
public class MyFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
System.out.println("filter === 1 === 初始化");
Filter.super.init(filterConfig);
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException, IOException {
System.out.println("filter 1 开始执行");
filterChain.doFilter(servletRequest, servletResponse);
}
@Override
public void destroy() {
System.out.println("filter === 1 === 销毁");
Filter.super.destroy();
}
}
@SpringBootApplication
@ServletComponentScan
public class Main {
public static void main(String[] args) {
SpringApplication.run(Main.class, args);
}
}
@ServletComponentScan
public class Main {
public static void main(String[] args) {
SpringApplication.run(Main.class, args);
}
}
Servlet生命周期
init
service
distory
SpringBoot
内嵌Tomcat
自动装配
启动流程
SPI机制
Java SPI
SpringBoot SPI
注解
@AutoConfiguration
@EnableConfigurationProperties
工具类
BeanFactoryUtils
ScopedProxyUtils
SecurityUtil
面试
Spring中的Bean生命周期的步骤
1. 推断构造方法
2. 实例化
3. 填充属性,也就是依赖注入
4. 处理Aware回调
5. 初始化前,处理@PostConstruct注解
6. 初始化,处理InitializingBean接口
7. 初始化后,进行AOP
2. 实例化
3. 填充属性,也就是依赖注入
4. 处理Aware回调
5. 初始化前,处理@PostConstruct注解
6. 初始化,处理InitializingBean接口
7. 初始化后,进行AOP
Bean生命周期简图
Spring容器启动流程
1. 在创建Spring容器,也就是启动Spring时:
2. 首先会进行扫描,扫描得到所有的BeanDefinition对象,并存在一个Map中
3. 然后筛选出非懒加载的单例BeanDefinition进行创建Bean,对于多例Bean不需要在启动过程中去进行创建,对于多例Bean会在每次获取Bean时利用BeanDefinition去创建
4. 利用BeanDefinition创建Bean就是Bean的创建生命周期,这期间包括了合并BeanDefinition、推断构造方法、实例化、属性填充、初始化前、初始化、初始化后等步骤,其中AOP就是发生在初始化后这一步骤中
5. 单例Bean创建完了之后,Spring会发布一个容器启动事件
6. Spring启动结束
7. 在源码中会更复杂,比如源码中会提供一些模板方法,让子类来实现,比如源码中还涉及到一些BeanFactoryPostProcessor和BeanPostProcessor的注册,Spring的扫描就是通过BenaFactoryPostProcessor来实现的,依赖注入就是通过BeanPostProcessor来实现的
8. 在Spring启动过程中还会去处理@Import等注解
2. 首先会进行扫描,扫描得到所有的BeanDefinition对象,并存在一个Map中
3. 然后筛选出非懒加载的单例BeanDefinition进行创建Bean,对于多例Bean不需要在启动过程中去进行创建,对于多例Bean会在每次获取Bean时利用BeanDefinition去创建
4. 利用BeanDefinition创建Bean就是Bean的创建生命周期,这期间包括了合并BeanDefinition、推断构造方法、实例化、属性填充、初始化前、初始化、初始化后等步骤,其中AOP就是发生在初始化后这一步骤中
5. 单例Bean创建完了之后,Spring会发布一个容器启动事件
6. Spring启动结束
7. 在源码中会更复杂,比如源码中会提供一些模板方法,让子类来实现,比如源码中还涉及到一些BeanFactoryPostProcessor和BeanPostProcessor的注册,Spring的扫描就是通过BenaFactoryPostProcessor来实现的,依赖注入就是通过BeanPostProcessor来实现的
8. 在Spring启动过程中还会去处理@Import等注解
Spring启动流程
单例Bean是单例模式吗?
Spring中的单例Bean也是一种单例模式,只不过范围比较小,范围是beanName,一个beanName对应同一个Bean对象,不同beanName可以对应不同的Bean对象(就算是同一个类也是可以的)。
Spring中Bean是线程安全的吗?
Spring本身并没有针对Bean做线程安全的处理,所以,如果Bean是无状态的(无成员变量),那么Bean则是线程安全的,如果Bean是有状态的(有成员变量),那么Bean则不是线程安全的。
另外,Bean是不是线程安全,跟Bean的作用域没有关系,Bean的作用域只是表示Bean的生命周期范围,对于任何生命周期的Bean都是一个对象,这个对象是不是线程安全的,还是得看这个Bean对象本身。
另外,Bean是不是线程安全,跟Bean的作用域没有关系,Bean的作用域只是表示Bean的生命周期范围,对于任何生命周期的Bean都是一个对象,这个对象是不是线程安全的,还是得看这个Bean对象本身。
ApplicationContext和BeanFactory有什么区别?
BeanFactory是Spring中非常核心的组件,表示Bean工厂,可以生成Bean,维护Bean,而ApplicationContext继承了BeanFactory,所以ApplicationContext拥有BeanFactory所有的特点,也是一个Bean工厂,但是ApplicationContext除开继承了BeanFactory之外,还继承了诸如EnvironmentCapable、MessageSource、ApplicationEventPublisher等接口,从而ApplicationContext还有获取系统环境变量、国际化、事件发布等功能,这是BeanFactory所不具备的
@Index
事务失效场景
https://www.cnblogs.com/wangyunhong/articles/16833043.html
Mybatis
Mybatis-plus
#和$
#
会把参数解析成字符串,可以防止sql注入
$
直接显示传入的值,不能防止sql注入,一般结合order by用,正序倒序
代理对象
JDK动态代理
Redis
新特性
redis7
十大数据类型
这里都是指的value,key都是String
持久化
RDB
AOF
io多路复用
NIO
selecter
epool
单线程
减少上下文切换
0拷贝
减少用户态和内核态切换
页缓存
kafka
0拷贝
批处理
集群
三主三从
Mysql
语法
DQL: 数据查询语言,用来查询数据库中表的记录
DDL: 数据定义语言,用来定义数据库对象(数据库、表、字段)
DML: 数据操作语言,用来对数据库表中的数据进行增删改
DQL: 数据查询语言,用来查询数据库中表的记录
DCL: 数据控制语言,用来创建数据库用户、控制数据库的控制权限
原理
存储引擎
InnoDB
InnoDB 是一种兼顾高可靠性和高性能的通用存储引擎,在 MySQL 5.5 之后,InnoDB 是默认的 MySQL 引擎。
特点:
DML 操作遵循 ACID 模型,支持事务
行级锁,提高并发访问性能
支持外键约束,保证数据的完整性和正确性
特点:
DML 操作遵循 ACID 模型,支持事务
行级锁,提高并发访问性能
支持外键约束,保证数据的完整性和正确性
MyISAM
MyISAM 是 MySQL 早期的默认存储引擎。
特点:
不支持事务,不支持外键
支持表锁,不支持行锁
访问速度快
特点:
不支持事务,不支持外键
支持表锁,不支持行锁
访问速度快
Memory
Memory 引擎的表数据是存储在内存中的,受硬件问题、断电问题的影响,只能将这些表作为临时表或缓存使用。
存储引擎的选择
在选择存储引擎时,应该根据应用系统的特点选择合适的存储引擎。对于复杂的应用系统,还可以根据实际情况选择多种存储引擎进行组合。
InnoDB: 如果应用对事物的完整性有比较高的要求,在并发条件下要求数据的一致性,数据操作除了插入和查询之外,还包含很多的更新、删除操作,则 InnoDB 是比较合适的选择
MyISAM: 如果应用是以读操作和插入操作为主,只有很少的更新和删除操作,并且对事务的完整性、并发性要求不高,那这个存储引擎是非常合适的。
Memory: 将所有数据保存在内存中,访问速度快,通常用于临时表及缓存。Memory 的缺陷是对表的大小有限制,太大的表无法缓存在内存中,而且无法保障数据的安全性
电商中的足迹和评论适合使用 MyISAM 引擎,缓存适合使用 Memory 引擎。
InnoDB: 如果应用对事物的完整性有比较高的要求,在并发条件下要求数据的一致性,数据操作除了插入和查询之外,还包含很多的更新、删除操作,则 InnoDB 是比较合适的选择
MyISAM: 如果应用是以读操作和插入操作为主,只有很少的更新和删除操作,并且对事务的完整性、并发性要求不高,那这个存储引擎是非常合适的。
Memory: 将所有数据保存在内存中,访问速度快,通常用于临时表及缓存。Memory 的缺陷是对表的大小有限制,太大的表无法缓存在内存中,而且无法保障数据的安全性
电商中的足迹和评论适合使用 MyISAM 引擎,缓存适合使用 Memory 引擎。
性能分析
查看执行频次
查看当前数据库的 INSERT, UPDATE, DELETE, SELECT 访问频次:
SHOW GLOBAL STATUS LIKE 'Com_______'; 或者 SHOW SESSION STATUS LIKE 'Com_______';
SHOW GLOBAL STATUS LIKE 'Com_______'; 或者 SHOW SESSION STATUS LIKE 'Com_______';
慢查询日志
慢查询日志记录了所有执行时间超过指定参数(long_query_time,单位:秒,默认10秒)的所有SQL语句的日志。
MySQL的慢查询日志默认没有开启,需要在MySQL的配置文件(/etc/my.cnf)中配置如下信息:
# 开启慢查询日志开关
slow_query_log=1
# 设置慢查询日志的时间为2秒,SQL语句执行时间超过2秒,就会视为慢查询,记录慢查询日志
long_query_time=2
更改后记得重启MySQL服务,日志文件位置:/var/lib/mysql/localhost-slow.log
查看慢查询日志开关状态:
show variables like 'slow_query_log';
MySQL的慢查询日志默认没有开启,需要在MySQL的配置文件(/etc/my.cnf)中配置如下信息:
# 开启慢查询日志开关
slow_query_log=1
# 设置慢查询日志的时间为2秒,SQL语句执行时间超过2秒,就会视为慢查询,记录慢查询日志
long_query_time=2
更改后记得重启MySQL服务,日志文件位置:/var/lib/mysql/localhost-slow.log
查看慢查询日志开关状态:
show variables like 'slow_query_log';
profile
show profile 能在做SQL优化时帮我们了解时间都耗费在哪里。通过 have_profiling 参数,能看到当前 MySQL 是否支持 profile 操作:
SELECT @@have_profiling;
profiling 默认关闭,可以通过set语句在session/global级别开启 profiling:
SET profiling = 1;
查看所有语句的耗时:
show profiles;
查看指定query_id的SQL语句各个阶段的耗时:
show profile for query query_id;
查看指定query_id的SQL语句CPU的使用情况
show profile cpu for query query_id;
SELECT @@have_profiling;
profiling 默认关闭,可以通过set语句在session/global级别开启 profiling:
SET profiling = 1;
查看所有语句的耗时:
show profiles;
查看指定query_id的SQL语句各个阶段的耗时:
show profile for query query_id;
查看指定query_id的SQL语句CPU的使用情况
show profile cpu for query query_id;
explain
EXPLAIN 或者 DESC 命令获取 MySQL 如何执行 SELECT 语句的信息,包括在 SELECT 语句执行过程中表如何连接和连接的顺序。
语法:
# 直接在select语句之前加上关键字 explain / desc
EXPLAIN SELECT 字段列表 FROM 表名 HWERE 条件;
语法:
# 直接在select语句之前加上关键字 explain / desc
EXPLAIN SELECT 字段列表 FROM 表名 HWERE 条件;
EXPLAIN 各字段含义:
id:select 查询的序列号,表示查询中执行 select 子句或者操作表的顺序(id相同,执行顺序从上到下;id不同,值越大越先执行)
select_type:表示 SELECT 的类型,常见取值有 SIMPLE(简单表,即不适用表连接或者子查询)、PRIMARY(主查询,即外层的查询)、UNION(UNION中的第二个或者后面的查询语句)、SUBQUERY(SELECT/WHERE之后包含了子查询)等
type:表示连接类型,性能由好到差的连接类型为 NULL、system、const、eq_ref、ref、range、index、all
possible_key:可能应用在这张表上的索引,一个或多个
Key:实际使用的索引,如果为 NULL,则没有使用索引
Key_len:表示索引中使用的字节数,该值为索引字段最大可能长度,并非实际使用长度,在不损失精确性的前提下,长度越短越好
rows:MySQL认为必须要执行的行数,在InnoDB引擎的表中,是一个估计值,可能并不总是准确的
filtered:表示返回结果的行数占需读取行数的百分比,filtered的值越大越好
id:select 查询的序列号,表示查询中执行 select 子句或者操作表的顺序(id相同,执行顺序从上到下;id不同,值越大越先执行)
select_type:表示 SELECT 的类型,常见取值有 SIMPLE(简单表,即不适用表连接或者子查询)、PRIMARY(主查询,即外层的查询)、UNION(UNION中的第二个或者后面的查询语句)、SUBQUERY(SELECT/WHERE之后包含了子查询)等
type:表示连接类型,性能由好到差的连接类型为 NULL、system、const、eq_ref、ref、range、index、all
possible_key:可能应用在这张表上的索引,一个或多个
Key:实际使用的索引,如果为 NULL,则没有使用索引
Key_len:表示索引中使用的字节数,该值为索引字段最大可能长度,并非实际使用长度,在不损失精确性的前提下,长度越短越好
rows:MySQL认为必须要执行的行数,在InnoDB引擎的表中,是一个估计值,可能并不总是准确的
filtered:表示返回结果的行数占需读取行数的百分比,filtered的值越大越好
索引
索引是帮助 MySQL 高效获取数据的数据结构(有序)。在数据之外,数据库系统还维护着满足特定查找算法的数据结构,这些数据结构以某种方式引用(指向)数据,这样就可以在这些数据结构上实现高级查询算法,这种数据结构就是索引。
优缺点:
优点:
提高数据检索效率,降低数据库的IO成本
通过索引列对数据进行排序,降低数据排序的成本,降低CPU的消耗
缺点:
索引列也是要占用空间的
索引大大提高了查询效率,但降低了更新的速度,比如 INSERT、UPDATE、DELETE
优缺点:
优点:
提高数据检索效率,降低数据库的IO成本
通过索引列对数据进行排序,降低数据排序的成本,降低CPU的消耗
缺点:
索引列也是要占用空间的
索引大大提高了查询效率,但降低了更新的速度,比如 INSERT、UPDATE、DELETE
为什么 InnoDB 存储引擎选择使用 B+Tree 索引结构?
相对于二叉树,层级更少,搜索效率高
对于 B-Tree,无论是叶子节点还是非叶子节点,都会保存数据,这样导致一页中存储的键值减少,指针也跟着减少,要同样保存大量数据,只能增加树的高度,导致性能降低
相对于 Hash 索引,B+Tree 支持范围匹配及排序操作
对于 B-Tree,无论是叶子节点还是非叶子节点,都会保存数据,这样导致一页中存储的键值减少,指针也跟着减少,要同样保存大量数据,只能增加树的高度,导致性能降低
相对于 Hash 索引,B+Tree 支持范围匹配及排序操作
1. 以下 SQL 语句,哪个执行效率高?为什么?
select * from user where id = 10;
select * from user where name = 'Arm';
-- 备注:id为主键,name字段创建的有索引
select * from user where id = 10;
select * from user where name = 'Arm';
-- 备注:id为主键,name字段创建的有索引
答:第一条语句,因为第二条需要回表查询,相当于两个步骤。
InnoDB 主键索引的 B+Tree 高度为多少?
假设一行数据大小为1k,一页中可以存储16行这样的数据。InnoDB 的指针占用6个字节的空间,主键假设为bigint,占用字节数为8.
可得公式:n * 8 + (n + 1) * 6 = 16 * 1024,其中 8 表示 bigint 占用的字节数,n 表示当前节点存储的key的数量,(n + 1) 表示指针数量(比key多一个)。算出n约为1170。
如果树的高度为2,那么他能存储的数据量大概为:1171 * 16 = 18736;
如果树的高度为3,那么他能存储的数据量大概为:1171 * 1171 * 16 = 21939856。
可得公式:n * 8 + (n + 1) * 6 = 16 * 1024,其中 8 表示 bigint 占用的字节数,n 表示当前节点存储的key的数量,(n + 1) 表示指针数量(比key多一个)。算出n约为1170。
如果树的高度为2,那么他能存储的数据量大概为:1171 * 16 = 18736;
如果树的高度为3,那么他能存储的数据量大概为:1171 * 1171 * 16 = 21939856。
语法
创建索引:
CREATE [ UNIQUE | FULLTEXT ] INDEX index_name ON table_name (index_col_name, ...);
如果不加 CREATE 后面不加索引类型参数,则创建的是常规索引
查看索引:
SHOW INDEX FROM table_name;
删除索引:
DROP INDEX index_name ON table_name;
案例:
-- name字段为姓名字段,该字段的值可能会重复,为该字段创建索引
create index idx_user_name on tb_user(name);
-- phone手机号字段的值非空,且唯一,为该字段创建唯一索引
create unique index idx_user_phone on tb_user (phone);
-- 为profession, age, status创建联合索引
create index idx_user_pro_age_stat on tb_user(profession, age, status);
-- 为email建立合适的索引来提升查询效率
create index idx_user_email on tb_user(email);
-- 删除索引
drop index idx_user_email on tb_user;
CREATE [ UNIQUE | FULLTEXT ] INDEX index_name ON table_name (index_col_name, ...);
如果不加 CREATE 后面不加索引类型参数,则创建的是常规索引
查看索引:
SHOW INDEX FROM table_name;
删除索引:
DROP INDEX index_name ON table_name;
案例:
-- name字段为姓名字段,该字段的值可能会重复,为该字段创建索引
create index idx_user_name on tb_user(name);
-- phone手机号字段的值非空,且唯一,为该字段创建唯一索引
create unique index idx_user_phone on tb_user (phone);
-- 为profession, age, status创建联合索引
create index idx_user_pro_age_stat on tb_user(profession, age, status);
-- 为email建立合适的索引来提升查询效率
create index idx_user_email on tb_user(email);
-- 删除索引
drop index idx_user_email on tb_user;
使用规则
最左前缀法则
如果索引关联了多列(联合索引),要遵守最左前缀法则,最左前缀法则指的是查询从索引的最左列开始,并且不跳过索引中的列。
如果跳跃某一列,索引将部分失效(后面的字段索引失效)。
联合索引中,出现范围查询(<, >),范围查询右侧的列索引失效。可以用>=或者<=来规避索引失效问题。
如果跳跃某一列,索引将部分失效(后面的字段索引失效)。
联合索引中,出现范围查询(<, >),范围查询右侧的列索引失效。可以用>=或者<=来规避索引失效问题。
索引失效情况
在索引列上进行运算操作,索引将失效。如:explain select * from tb_user where substring(phone, 10, 2) = '15';
字符串类型字段使用时,不加引号,索引将失效。如:explain select * from tb_user where phone = 17799990015;,此处phone的值没有加引号
模糊查询中,如果仅仅是尾部模糊匹配,索引不会是失效;如果是头部模糊匹配,索引失效。如:explain select * from tb_user where profession like '%工程';,前后都有 % 也会失效。
用 or 分割开的条件,如果 or 其中一个条件的列没有索引,那么涉及的索引都不会被用到。
如果 MySQL 评估使用索引比全表更慢,则不使用索引。
like %
字符串类型字段使用时,不加引号,索引将失效。如:explain select * from tb_user where phone = 17799990015;,此处phone的值没有加引号
模糊查询中,如果仅仅是尾部模糊匹配,索引不会是失效;如果是头部模糊匹配,索引失效。如:explain select * from tb_user where profession like '%工程';,前后都有 % 也会失效。
用 or 分割开的条件,如果 or 其中一个条件的列没有索引,那么涉及的索引都不会被用到。
如果 MySQL 评估使用索引比全表更慢,则不使用索引。
like %
SQL 提示
是优化数据库的一个重要手段,简单来说,就是在SQL语句中加入一些人为的提示来达到优化操作的目的。
例如,使用索引:
explain select * from tb_user use index(idx_user_pro) where profession="软件工程";
不使用哪个索引:
explain select * from tb_user ignore index(idx_user_pro) where profession="软件工程";
必须使用哪个索引:
explain select * from tb_user force index(idx_user_pro) where profession="软件工程";
use 是建议,实际使用哪个索引 MySQL 还会自己权衡运行速度去更改,force就是无论如何都强制使用该索引。
例如,使用索引:
explain select * from tb_user use index(idx_user_pro) where profession="软件工程";
不使用哪个索引:
explain select * from tb_user ignore index(idx_user_pro) where profession="软件工程";
必须使用哪个索引:
explain select * from tb_user force index(idx_user_pro) where profession="软件工程";
use 是建议,实际使用哪个索引 MySQL 还会自己权衡运行速度去更改,force就是无论如何都强制使用该索引。
覆盖索引&回表查询
尽量使用覆盖索引(查询使用了索引,并且需要返回的列,在该索引中已经全部能找到),减少 select *。
explain 中 extra 字段含义:
using index condition:查找使用了索引,但是需要回表查询数据
using where; using index;:查找使用了索引,但是需要的数据都在索引列中能找到,所以不需要回表查询
如果在聚集索引中直接能找到对应的行,则直接返回行数据,只需要一次查询,哪怕是select *;如果在辅助索引中找聚集索引,如select id, name from xxx where name='xxx';,也只需要通过辅助索引(name)查找到对应的id,返回name和name索引对应的id即可,只需要一次查询;如果是通过辅助索引查找其他字段,则需要回表查询,如select id, name, gender from xxx where name='xxx';
所以尽量不要用select *,容易出现回表查询,降低效率,除非有联合索引包含了所有字段
面试题:一张表,有四个字段(id, username, password, status),由于数据量大,需要对以下SQL语句进行优化,该如何进行才是最优方案:
select id, username, password from tb_user where username='itcast';
解:给username和password字段建立联合索引,则不需要回表查询,直接覆盖索引
explain 中 extra 字段含义:
using index condition:查找使用了索引,但是需要回表查询数据
using where; using index;:查找使用了索引,但是需要的数据都在索引列中能找到,所以不需要回表查询
如果在聚集索引中直接能找到对应的行,则直接返回行数据,只需要一次查询,哪怕是select *;如果在辅助索引中找聚集索引,如select id, name from xxx where name='xxx';,也只需要通过辅助索引(name)查找到对应的id,返回name和name索引对应的id即可,只需要一次查询;如果是通过辅助索引查找其他字段,则需要回表查询,如select id, name, gender from xxx where name='xxx';
所以尽量不要用select *,容易出现回表查询,降低效率,除非有联合索引包含了所有字段
面试题:一张表,有四个字段(id, username, password, status),由于数据量大,需要对以下SQL语句进行优化,该如何进行才是最优方案:
select id, username, password from tb_user where username='itcast';
解:给username和password字段建立联合索引,则不需要回表查询,直接覆盖索引
前缀索引
当字段类型为字符串(varchar, text等)时,有时候需要索引很长的字符串,这会让索引变得很大,查询时,浪费大量的磁盘IO,影响查询效率,此时可以只降字符串的一部分前缀,建立索引,这样可以大大节约索引空间,从而提高索引效率。
语法:create index idx_xxxx on table_name(columnn(n));
前缀长度:可以根据索引的选择性来决定,而选择性是指不重复的索引值(基数)和数据表的记录总数的比值,索引选择性越高则查询效率越高,唯一索引的选择性是1,这是最好的索引选择性,性能也是最好的。
求选择性公式:
select count(distinct email) / count(*) from tb_user;
select count(distinct substring(email, 1, 5)) / count(*) from tb_user;
show index 里面的sub_part可以看到接取的长度
语法:create index idx_xxxx on table_name(columnn(n));
前缀长度:可以根据索引的选择性来决定,而选择性是指不重复的索引值(基数)和数据表的记录总数的比值,索引选择性越高则查询效率越高,唯一索引的选择性是1,这是最好的索引选择性,性能也是最好的。
求选择性公式:
select count(distinct email) / count(*) from tb_user;
select count(distinct substring(email, 1, 5)) / count(*) from tb_user;
show index 里面的sub_part可以看到接取的长度
单列索引&联合索引
单列索引:即一个索引只包含单个列
联合索引:即一个索引包含了多个列
在业务场景中,如果存在多个查询条件,考虑针对于查询字段建立索引时,建议建立联合索引,而非单列索引。
单列索引情况:
explain select id, phone, name from tb_user where phone = '17799990010' and name = '韩信';
这句只会用到phone索引字段
注意事项
多条件联合查询时,MySQL优化器会评估哪个字段的索引效率更高,会选择该索引完成本次查询
联合索引:即一个索引包含了多个列
在业务场景中,如果存在多个查询条件,考虑针对于查询字段建立索引时,建议建立联合索引,而非单列索引。
单列索引情况:
explain select id, phone, name from tb_user where phone = '17799990010' and name = '韩信';
这句只会用到phone索引字段
注意事项
多条件联合查询时,MySQL优化器会评估哪个字段的索引效率更高,会选择该索引完成本次查询
设计原则
针对于数据量较大,且查询比较频繁的表建立索引
针对于常作为查询条件(where)、排序(order by)、分组(group by)操作的字段建立索引
尽量选择区分度高的列作为索引,尽量建立唯一索引,区分度越高,使用索引的效率越高
如果是字符串类型的字段,字段长度较长,可以针对于字段的特点,建立前缀索引
尽量使用联合索引,减少单列索引,查询时,联合索引很多时候可以覆盖索引,节省存储空间,避免回表,提高查询效率
要控制索引的数量,索引并不是多多益善,索引越多,维护索引结构的代价就越大,会影响增删改的效率
如果索引列不能存储NULL值,请在创建表时使用NOT NULL约束它。当优化器知道每列是否包含NULL值时,它可以更好地确定哪个索引最有效地用于查询
针对于常作为查询条件(where)、排序(order by)、分组(group by)操作的字段建立索引
尽量选择区分度高的列作为索引,尽量建立唯一索引,区分度越高,使用索引的效率越高
如果是字符串类型的字段,字段长度较长,可以针对于字段的特点,建立前缀索引
尽量使用联合索引,减少单列索引,查询时,联合索引很多时候可以覆盖索引,节省存储空间,避免回表,提高查询效率
要控制索引的数量,索引并不是多多益善,索引越多,维护索引结构的代价就越大,会影响增删改的效率
如果索引列不能存储NULL值,请在创建表时使用NOT NULL约束它。当优化器知道每列是否包含NULL值时,它可以更好地确定哪个索引最有效地用于查询
SQL 优化
插入数据
普通插入:
采用批量插入(一次插入的数据不建议超过1000条)
手动提交事务
主键顺序插入
采用批量插入(一次插入的数据不建议超过1000条)
手动提交事务
主键顺序插入
大批量插入:
如果一次性需要插入大批量数据,使用insert语句插入性能较低,此时可以使用MySQL数据库提供的load指令插入。
# 客户端连接服务端时,加上参数 --local-infile(这一行在bash/cmd界面输入)
mysql --local-infile -u root -p
# 设置全局参数local_infile为1,开启从本地加载文件导入数据的开关
set global local_infile = 1;
select @@local_infile;
# 执行load指令将准备好的数据,加载到表结构中
load data local infile '/root/sql1.log' into table 'tb_user' fields terminated by ',' lines terminated by '\n';
如果一次性需要插入大批量数据,使用insert语句插入性能较低,此时可以使用MySQL数据库提供的load指令插入。
# 客户端连接服务端时,加上参数 --local-infile(这一行在bash/cmd界面输入)
mysql --local-infile -u root -p
# 设置全局参数local_infile为1,开启从本地加载文件导入数据的开关
set global local_infile = 1;
select @@local_infile;
# 执行load指令将准备好的数据,加载到表结构中
load data local infile '/root/sql1.log' into table 'tb_user' fields terminated by ',' lines terminated by '\n';
主键优化
数据组织方式:在InnoDB存储引擎中,表数据都是根据主键顺序组织存放的,这种存储方式的表称为索引组织表(Index organized table, IOT)
页分裂:页可以为空,也可以填充一般,也可以填充100%,每个页包含了2-N行数据(如果一行数据过大,会行溢出),根据主键排列。
页合并:当删除一行记录时,实际上记录并没有被物理删除,只是记录被标记(flaged)为删除并且它的空间变得允许被其他记录声明使用。当页中删除的记录到达 MERGE_THRESHOLD(默认为页的50%),InnoDB会开始寻找最靠近的页(前后)看看是否可以将这两个页合并以优化空间使用。
MERGE_THRESHOLD:合并页的阈值,可以自己设置,在创建表或创建索引时指定
主键设计原则:
满足业务需求的情况下,尽量降低主键的长度
插入数据时,尽量选择顺序插入,选择使用 AUTO_INCREMENT 自增主键
尽量不要使用 UUID 做主键或者是其他的自然主键,如身份证号
业务操作时,避免对主键的修改
满足业务需求的情况下,尽量降低主键的长度
插入数据时,尽量选择顺序插入,选择使用 AUTO_INCREMENT 自增主键
尽量不要使用 UUID 做主键或者是其他的自然主键,如身份证号
业务操作时,避免对主键的修改
order by优化
如果order by字段全部使用升序排序或者降序排序,则都会走索引,但是如果一个字段升序排序,另一个字段降序排序,则不会走索引,explain的extra信息显示的是Using index, Using filesort,如果要优化掉Using filesort,则需要另外再创建一个索引,如:create index idx_user_age_phone_ad on tb_user(age asc, phone desc);,此时使用select id, age, phone from tb_user order by age asc, phone desc;会全部走索引
group by优化
在分组操作时,可以通过索引来提高效率
分组操作时,索引的使用也是满足最左前缀法则的
如索引为idx_user_pro_age_stat,则句式可以是select ... where profession order by age,这样也符合最左前缀法则
分组操作时,索引的使用也是满足最左前缀法则的
如索引为idx_user_pro_age_stat,则句式可以是select ... where profession order by age,这样也符合最左前缀法则
limit优化
常见的问题如limit 2000000, 10,此时需要 MySQL 排序前2000000条记录,但仅仅返回2000000 - 2000010的记录,其他记录丢弃,查询排序的代价非常大。
优化方案:一般分页查询时,通过创建覆盖索引能够比较好地提高性能,可以通过覆盖索引加子查询形式进行优化
例如:
-- 此语句耗时很长
select * from tb_sku limit 9000000, 10;
-- 通过覆盖索引加快速度,直接通过主键索引进行排序及查询
select id from tb_sku order by id limit 9000000, 10;
-- 下面的语句是错误的,因为 MySQL 不支持 in 里面使用 limit
-- select * from tb_sku where id in (select id from tb_sku order by id limit 9000000, 10);
-- 通过连表查询即可实现第一句的效果,并且能达到第二句的速度
select * from tb_sku as s, (select id from tb_sku order by id limit 9000000, 10) as a where s.id = a.id;
优化方案:一般分页查询时,通过创建覆盖索引能够比较好地提高性能,可以通过覆盖索引加子查询形式进行优化
例如:
-- 此语句耗时很长
select * from tb_sku limit 9000000, 10;
-- 通过覆盖索引加快速度,直接通过主键索引进行排序及查询
select id from tb_sku order by id limit 9000000, 10;
-- 下面的语句是错误的,因为 MySQL 不支持 in 里面使用 limit
-- select * from tb_sku where id in (select id from tb_sku order by id limit 9000000, 10);
-- 通过连表查询即可实现第一句的效果,并且能达到第二句的速度
select * from tb_sku as s, (select id from tb_sku order by id limit 9000000, 10) as a where s.id = a.id;
count优化
各种用法的性能:
count(主键):InnoDB引擎会遍历整张表,把每行的主键id值都取出来,返回给服务层,服务层拿到主键后,直接按行进行累加(主键不可能为空)
count(字段):没有not null约束的话,InnoDB引擎会遍历整张表把每一行的字段值都取出来,返回给服务层,服务层判断是否为null,不为null,计数累加;有not null约束的话,InnoDB引擎会遍历整张表把每一行的字段值都取出来,返回给服务层,直接按行进行累加
count(1):InnoDB 引擎遍历整张表,但不取值。服务层对于返回的每一层,放一个数字 1 进去,直接按行进行累加
count(*):InnoDB 引擎并不会把全部字段取出来,而是专门做了优化,不取值,服务层直接按行进行累加
按效率排序:count(字段) < count(主键) < count(1) < count(*),所以尽量使用 count(*)
count(主键):InnoDB引擎会遍历整张表,把每行的主键id值都取出来,返回给服务层,服务层拿到主键后,直接按行进行累加(主键不可能为空)
count(字段):没有not null约束的话,InnoDB引擎会遍历整张表把每一行的字段值都取出来,返回给服务层,服务层判断是否为null,不为null,计数累加;有not null约束的话,InnoDB引擎会遍历整张表把每一行的字段值都取出来,返回给服务层,直接按行进行累加
count(1):InnoDB 引擎遍历整张表,但不取值。服务层对于返回的每一层,放一个数字 1 进去,直接按行进行累加
count(*):InnoDB 引擎并不会把全部字段取出来,而是专门做了优化,不取值,服务层直接按行进行累加
按效率排序:count(字段) < count(主键) < count(1) < count(*),所以尽量使用 count(*)
update优化(避免行锁升级为表锁)
InnoDB 的行锁是针对索引加的锁,不是针对记录加的锁,并且该索引不能失效,否则会从行锁升级为表锁。
如以下两条语句:
update student set no = '123' where id = 1;,这句由于id有主键索引,所以只会锁这一行;
update student set no = '123' where name = 'test';,这句由于name没有索引,所以会把整张表都锁住进行数据更新,解决方法是给name字段添加索引
如以下两条语句:
update student set no = '123' where id = 1;,这句由于id有主键索引,所以只会锁这一行;
update student set no = '123' where name = 'test';,这句由于name没有索引,所以会把整张表都锁住进行数据更新,解决方法是给name字段添加索引
InnoDB 引擎
逻辑存储结构
表空间(ibd文件),一个mysql实例可以对应多个表空间,用于存储记录、索引等数据。
段,分为数据段(Leaf node segment)、索引段(Non-leaf node segment)、回滚段(Rollback segment),InnoDB是索引组织表,数据段就是B+树的叶子节点,索引段即为B+树的非叶子节点。段用来管理多个Extent(区)。
区,表空间的单元结构,每个区的大小为1M。默认情况下,InnoDB存储引擎页大小为16K,即一个区中一共有64个连续的页。
页,是InnoDB存储引擎磁盘管理的最小单元,每个页的大小默认为16KB。为了保证页的连续性,InnoDB存储引擎每从磁盘申请4-5个区。一页包含若干行。
行,InnoDB存储引擎数据是按进行存放的。
段,分为数据段(Leaf node segment)、索引段(Non-leaf node segment)、回滚段(Rollback segment),InnoDB是索引组织表,数据段就是B+树的叶子节点,索引段即为B+树的非叶子节点。段用来管理多个Extent(区)。
区,表空间的单元结构,每个区的大小为1M。默认情况下,InnoDB存储引擎页大小为16K,即一个区中一共有64个连续的页。
页,是InnoDB存储引擎磁盘管理的最小单元,每个页的大小默认为16KB。为了保证页的连续性,InnoDB存储引擎每从磁盘申请4-5个区。一页包含若干行。
行,InnoDB存储引擎数据是按进行存放的。
InnoDB的整个体系结构为:
当业务操作的时候直接操作的是内存缓冲区,如果缓冲区当中没有数据,则会从磁盘中加载到缓冲区,增删改查都是在缓冲区的,后台线程以一定的速率刷新到磁盘。
当业务操作的时候直接操作的是内存缓冲区,如果缓冲区当中没有数据,则会从磁盘中加载到缓冲区,增删改查都是在缓冲区的,后台线程以一定的速率刷新到磁盘。
事务原理
事务是一组操作的集合,它是一个不可分割的工作单位,事务会把所有的操作作为一个整体一起向系统提交或撤销操作请求,即这些操作要么同时成功,要么同时败。具有ACID四大特征。
原子性,一致性,持久性这三大特性由 redo log 和 undo log 日志来保证的。 隔离性 是由锁机制和MVCC保证的。
原子性,一致性,持久性这三大特性由 redo log 和 undo log 日志来保证的。 隔离性 是由锁机制和MVCC保证的。
redo log:
重做日志,记录的是事务提交时数据页的物理修改,是用来实现事务的持久性。 该日志文件由两部分组成:重做日志缓冲(redo log buffer)以及重做日志文件(redo log file),前者是在内存中,后者在磁盘中。当事务提交之后会把所有修改信息都存到该日志文件中,用于在刷新脏页到磁盘,发生错误时,进行数据恢复使用。
个人理解: 事物每次提交的时候都会将数据刷到redo log中而不是直接将buffer pool中的数据直接刷到磁盘中(ibd文件中),是因为redo log 是顺序写,性能处理的够快,直接刷到ibd中,是随机写,性能慢。所以脏页是在下一次读的时候,或者后台线程采用一定的机制进行刷盘到ibd中。
个人理解: 事物每次提交的时候都会将数据刷到redo log中而不是直接将buffer pool中的数据直接刷到磁盘中(ibd文件中),是因为redo log 是顺序写,性能处理的够快,直接刷到ibd中,是随机写,性能慢。所以脏页是在下一次读的时候,或者后台线程采用一定的机制进行刷盘到ibd中。
undo log:
回滚日志,用于记录数据被修改前的信息,作用包含两个:提供回滚和MVCC(多版本并发控制)。 undo log和redo log记录物理日志不一样,它是逻辑日志。可以认为当delete一条记录,undo log中会记录一条对应的insert记录,反之亦然,当update一条记录时,它记录一条对应相反的update记录。当执行rollback时,就可以从undo log中的逻辑记录读取到相应的内容并进行回滚。
Undo log销毁:undo log在事务执行时产生,事务提交时,并不会立即删除undo log,因为这些日志可能还用于MVCC。 Undo log存储:undo log采用段的方式进行管理和记录,存放在前面介绍的rollback segment回滚段中,内部包含1024个undo log segment。
Undo log销毁:undo log在事务执行时产生,事务提交时,并不会立即删除undo log,因为这些日志可能还用于MVCC。 Undo log存储:undo log采用段的方式进行管理和记录,存放在前面介绍的rollback segment回滚段中,内部包含1024个undo log segment。
MVCC
简介
全称Multi-Version Concurrency Control,多版本并发控制。指维护一个数据的多个版本,使得读写操作没有冲突,快照读为MySQL实现MVCC提供了一个非阻塞读功能。MVCC的具体实现,还需要依赖于数据库记录中的三个隐式字段、undo log日志、readView。
当前读:
读取的是记录的最新版本,读取时还要保证其他并发事务不能修改当前记录,会对读取的记录进行加锁。对于我们日常的操作,如:
select...lock in share mode(共享锁)。
select..…for update、update、insert、delete(排他锁)都是一种当前读。
快照读:
简单的select(不加锁)就是快照读,快照读,读取的是记录数据的可见版本,有可能是历史数据,不加锁,是非阻塞读。
Read Committed:每次select,都生成一个快照读。
Repeatable Read:开启事务后第一个select语句才是快照读的地方。
Serializable:快照读会退化为当前读。
读取的是记录的最新版本,读取时还要保证其他并发事务不能修改当前记录,会对读取的记录进行加锁。对于我们日常的操作,如:
select...lock in share mode(共享锁)。
select..…for update、update、insert、delete(排他锁)都是一种当前读。
快照读:
简单的select(不加锁)就是快照读,快照读,读取的是记录数据的可见版本,有可能是历史数据,不加锁,是非阻塞读。
Read Committed:每次select,都生成一个快照读。
Repeatable Read:开启事务后第一个select语句才是快照读的地方。
Serializable:快照读会退化为当前读。
MVCC 实现原理
有三个隐藏的字段
undo log 版本链
read view
总结
MVCC 靠 隐藏字段 , undo log 版本链 , read view 实现的。
原子性-undo log
持久性-redo log
一致性-undo log + redo log
隔离性-锁 + MVCC
原子性-undo log
持久性-redo log
一致性-undo log + redo log
隔离性-锁 + MVCC
运维
日志
错误日志
错误日志是 MySQL 中 重要的日志之一,它记录了当 mysqld启动和停止时,以及服务器在运行过程中发生任何严重错误时的相关信息。当数据库出现任何故障导致无法正常使用时,建议首先查看此日志。
该日志是默认开启的,默认存放目录 /var/log/,默认的日志文件名为
mysqld.log 。查看日志位置:
show variables like '%log_error%';
该日志是默认开启的,默认存放目录 /var/log/,默认的日志文件名为
mysqld.log 。查看日志位置:
show variables like '%log_error%';
二进制日志
二进制日志(BINLOG)记录了所有的 DDL(数据定义语言)语句和
DML(数据操纵语言)语句,但不包括数据查询(SELECT、SHOW)语句。
作用:①. 灾难时的数据恢复;②.MySQL的主从复制。在MySQL8版本中,默认二进制日志是开启着的,涉及到的参数如下:
show variables like '%log_bin%';
DML(数据操纵语言)语句,但不包括数据查询(SELECT、SHOW)语句。
作用:①. 灾难时的数据恢复;②.MySQL的主从复制。在MySQL8版本中,默认二进制日志是开启着的,涉及到的参数如下:
show variables like '%log_bin%';
查看
删除
查询日志
查询日志中记录了客户端的所有操作语句,而二进制日志不包含查询数据的SQL语句。默认情况下,查询日志是未开启的。
如果需要开启查询日志,可以修改MySQL的配置文件 /etc/my.cnf
文件,添加如下内容:
#该选项用来开启查询日志 , 可选值 : 0 或者 1 ; 0 代表关闭, 1 代表开启
general_log=1
#设置日志的文件名 , 如果没有指定, 默认的文件名为 host_name.log
general_log_file=mysql_query.log
开启了查询日志之后,在MySQL的数据存放目录,也就是 /var/lib/mysql/目录下就会出现mysql_query.log文件。之后所有的客户端的增删改查操作都会记录在该日志文件之中,长时间运行后,该日志文件将会非常大。
如果需要开启查询日志,可以修改MySQL的配置文件 /etc/my.cnf
文件,添加如下内容:
#该选项用来开启查询日志 , 可选值 : 0 或者 1 ; 0 代表关闭, 1 代表开启
general_log=1
#设置日志的文件名 , 如果没有指定, 默认的文件名为 host_name.log
general_log_file=mysql_query.log
开启了查询日志之后,在MySQL的数据存放目录,也就是 /var/lib/mysql/目录下就会出现mysql_query.log文件。之后所有的客户端的增删改查操作都会记录在该日志文件之中,长时间运行后,该日志文件将会非常大。
慢查询日志
慢查询日志记录了所有执行时间超过参数 long_query_time设置值并且扫描记录数不小于min_examined_row_limit的所有的SQL语句的日志,默认未开启。long_query_time 默认为10 秒, 小为 0, 精度可以到微秒。
如果需要开启慢查询日志,需要在MySQL的配置文件 /etc/my.cnf中配置如下参数:
#慢查询日志
slow_query_log=1
#执行时间参数
long_query_time=2
默认情况下,不会记录管理语句,也不会记录不使用索引进行查找的查询。可以使用log_slow_admin_statements和 更改此行为log_queries_not_using_indexes,如下所述。
#记录执行较慢的管理语句
log_slow_admin_statements =1
#记录执行较慢的未使用索引的语句
log_queries_not_using_indexes = 1
上述所有的参数配置完成之后,都需要重新启动MySQL服务器才可以生效。
如果需要开启慢查询日志,需要在MySQL的配置文件 /etc/my.cnf中配置如下参数:
#慢查询日志
slow_query_log=1
#执行时间参数
long_query_time=2
默认情况下,不会记录管理语句,也不会记录不使用索引进行查找的查询。可以使用log_slow_admin_statements和 更改此行为log_queries_not_using_indexes,如下所述。
#记录执行较慢的管理语句
log_slow_admin_statements =1
#记录执行较慢的未使用索引的语句
log_queries_not_using_indexes = 1
上述所有的参数配置完成之后,都需要重新启动MySQL服务器才可以生效。
binlog redolog undolog有什么区别
MySQL中的binlog、redolog和undolog是三个不同的日志文件,它们分别用于不同的目的。
binlog(二进制日志):记录所有对数据库结构或数据进行更改的SQL语句,包括增加、删除、修改等操作。binlog可以用来实现数据备份、恢复和复制等功能。
redolog(重做日志):也叫事务日志,记录了数据库引擎执行插入、更新、删除等修改操作所产生的信息。在事务提交之前,redolog会将这些修改操作记录下来。如果在事务提交时出现异常情况,可以利用redolog回滚到事务开始时的状态。
undolog(回滚日志):记录了在事务执行过程中所执行的每一个操作的反向操作。当一条事务被回滚时,系统会根据undolog把事务执行的所有操作都撤销,恢复到事务开始之前的状态。
总之,binlog主要用于备份和复制,redolog主要用于故障恢复,而undolog主要用于回滚操作。
binlog记录的是逻辑日志,而redolog和undolog记录的是物理日志。逻辑日志指的是对数据库进行操作的逻辑顺序,而物理日志指的是数据库内部数据文件的物理结构。
binlog是在服务器层记录的,可以跨多个数据库实例,而redolog和undolog是在存储引擎层记录的,只能在单个实例中使用。
binlog可以被用于MySQL主从复制以及高可用性(HA)架构,而redolog和undolog只是在单个实例的故障恢复中起作用。
对于高更新负载的系统,redolog和undolog的写入速度通常比binlog更快,因为它们是在内存中直接处理的。
redolog(重做日志)和undolog(撤销日志)是MySQL中两种不同的日志类型,用于记录数据库操作的详细信息。
Redo log记录了所有修改数据的操作,在事务提交之前将这些操作记录下来。如果在事务提交时出现异常,可以利用redolog重新执行先前记录的修改操作,从而恢复到事务提交之前的状态。也就是说,redo log主要用于故障恢复。
Undo log则记录了事务执行过程中所进行的每一个操作的反向操作。当一条事务被回滚时,系统会根据undo log把事务执行的所有操作都撤销,恢复到事务开始之前的状态。因此,undo log主要用于回滚操作。
换句话说,redo log记录了未来的操作,以便在发生故障时进行回滚恢复,而undo log记录了过去的操作,以便在需要回滚时进行操作撤销。同时,redo log在发生故障时可能会丢失部分数据,因为它只记录了尚未提交的操作,而undo log不会丢失任何数据,因为它记录了所有的操作。
在MySQL中,redo log和undo log通常是与存储引擎紧密集成的,不同存储引擎的实现方式可能会略微不同。但总体来说,它们的作用和区别都类似。
binlog(二进制日志):记录所有对数据库结构或数据进行更改的SQL语句,包括增加、删除、修改等操作。binlog可以用来实现数据备份、恢复和复制等功能。
redolog(重做日志):也叫事务日志,记录了数据库引擎执行插入、更新、删除等修改操作所产生的信息。在事务提交之前,redolog会将这些修改操作记录下来。如果在事务提交时出现异常情况,可以利用redolog回滚到事务开始时的状态。
undolog(回滚日志):记录了在事务执行过程中所执行的每一个操作的反向操作。当一条事务被回滚时,系统会根据undolog把事务执行的所有操作都撤销,恢复到事务开始之前的状态。
总之,binlog主要用于备份和复制,redolog主要用于故障恢复,而undolog主要用于回滚操作。
binlog记录的是逻辑日志,而redolog和undolog记录的是物理日志。逻辑日志指的是对数据库进行操作的逻辑顺序,而物理日志指的是数据库内部数据文件的物理结构。
binlog是在服务器层记录的,可以跨多个数据库实例,而redolog和undolog是在存储引擎层记录的,只能在单个实例中使用。
binlog可以被用于MySQL主从复制以及高可用性(HA)架构,而redolog和undolog只是在单个实例的故障恢复中起作用。
对于高更新负载的系统,redolog和undolog的写入速度通常比binlog更快,因为它们是在内存中直接处理的。
redolog(重做日志)和undolog(撤销日志)是MySQL中两种不同的日志类型,用于记录数据库操作的详细信息。
Redo log记录了所有修改数据的操作,在事务提交之前将这些操作记录下来。如果在事务提交时出现异常,可以利用redolog重新执行先前记录的修改操作,从而恢复到事务提交之前的状态。也就是说,redo log主要用于故障恢复。
Undo log则记录了事务执行过程中所进行的每一个操作的反向操作。当一条事务被回滚时,系统会根据undo log把事务执行的所有操作都撤销,恢复到事务开始之前的状态。因此,undo log主要用于回滚操作。
换句话说,redo log记录了未来的操作,以便在发生故障时进行回滚恢复,而undo log记录了过去的操作,以便在需要回滚时进行操作撤销。同时,redo log在发生故障时可能会丢失部分数据,因为它只记录了尚未提交的操作,而undo log不会丢失任何数据,因为它记录了所有的操作。
在MySQL中,redo log和undo log通常是与存储引擎紧密集成的,不同存储引擎的实现方式可能会略微不同。但总体来说,它们的作用和区别都类似。
主从复制
主从复制是指将主数据库的 DDL 和 DML操作通过二进制日志传到从库服务器中,然后在从库上对这些日志重新执行(也叫重做),从而使得从库和主库的数据保持同步。
MySQL支持一台主库同时向多台从库进行复制,从库同时也可以作为其他从服务器的主库,实现链状复制。
MySQL支持一台主库同时向多台从库进行复制,从库同时也可以作为其他从服务器的主库,实现链状复制。
MySQL 复制的优点主要包含以下三个方面:
主库出现问题,可以快速切换到从库提供服务。
实现读写分离,降低主库的访问压力。
可以在从库中执行备份,以避免备份期间影响主库服务。
主库出现问题,可以快速切换到从库提供服务。
实现读写分离,降低主库的访问压力。
可以在从库中执行备份,以避免备份期间影响主库服务。
原理
MySQL主从复制的核心就是 二进制日志,具体的过程如下:
从上图来看,复制分成三步:
Master 主库在事务提交时,会把数据变更记录在二进制日志文件Binlog中。
从库读取主库的二进制日志文件 Binlog ,写入到从库的中继日志 Relay Log。
slave重做中继日志中的事件,将改变反映它自己的数据。
从上图来看,复制分成三步:
Master 主库在事务提交时,会把数据变更记录在二进制日志文件Binlog中。
从库读取主库的二进制日志文件 Binlog ,写入到从库的中继日志 Relay Log。
slave重做中继日志中的事件,将改变反映它自己的数据。
分库分表
垂直拆分
垂直分库
垂直分库:以表为依据,根据业务将不同表拆分到不同库中。
特点:
每个库的表结构都不一样。
每个库的数据也不一样。
所有库的并集是全量数据。
特点:
每个库的表结构都不一样。
每个库的数据也不一样。
所有库的并集是全量数据。
垂直分表
垂直分表:以字段为依据,根据字段属性将不同字段拆分到不同表中。
特点:
每个表的结构都不一样。
每个表的数据也不一样,一般通过一列(主键/外键)关联。
所有表的并集是全量数据。
特点:
每个表的结构都不一样。
每个表的数据也不一样,一般通过一列(主键/外键)关联。
所有表的并集是全量数据。
水平拆分
水平分库
水平分库:以字段为依据,按照一定策略,将一个库的数据拆分到多个库中。
特点:
每个库的表结构都一样。
每个库的数据都不一样。
所有库的并集是全量数据。
特点:
每个库的表结构都一样。
每个库的数据都不一样。
所有库的并集是全量数据。
水平分表
水平分表:以字段为依据,按照一定策略,将一个表的数据拆分到多个表中。
特点:
每个表的表结构都一样。
每个表的数据都不一样。
所有表的并集是全量数据。
特点:
每个表的表结构都一样。
每个表的数据都不一样。
所有表的并集是全量数据。
实现技术
shardingJDBC:基于AOP原理,在应用程序中对本地执行的SQL进行拦截,解析、改写、路由处理。需要自行编码配置实现,只支持java语言,性能较高。
MyCat:数据库分库分表中间件,不用调整代码即可实现分库分表,支持多种语言,性能不及前者。
sharding-proxy
读写分离
一主一从
MySQL的主从复制,是基于二进制日志(binlog)实现的。
双主双从
一个主机 Master1 用于处理所有写请求,它的从机 Slave1 和另一台主机
Master2 还有它的从机 Slave2 负责所有读请求。当 Master1主机宕机后,Master2 主机负责写请求,Master1 、Master2 互为备机。
Master2 还有它的从机 Slave2 负责所有读请求。当 Master1主机宕机后,Master2 主机负责写请求,Master1 、Master2 互为备机。
RocketMQ
关于MQ常见问题汇总
消息丢失问题
在往MQ发送消息的时候,很可能会发送失败,原因有很多,比如:网络问题、broker挂了、mq服务端磁盘问题等。这些情况,都可能会造成消息丢失。
那么,如何防止消息丢失呢?
答:加一张消息发送表。
那么,如何防止消息丢失呢?
答:加一张消息发送表。
在生产者发送mq消息之前,先把该条消息写入消息发送表,初始状态是待处理,然后再发送mq消息。消费者消费消息时,处理完业务逻辑之后,再回调生产者的一个接口,修改消息状态为已处理。
如果生产者把消息写入消息发送表之后,再发送mq消息到mq服务端的过程中失败了,造成了消息丢失。 这时候,要如何处理呢?
答:使用定时任务xxl-job,增加重试机制。
如果生产者把消息写入消息发送表之后,再发送mq消息到mq服务端的过程中失败了,造成了消息丢失。 这时候,要如何处理呢?
答:使用定时任务xxl-job,增加重试机制。
用job每隔一段时间去查询消息发送表中状态为待处理的数据,然后重新发送mq消息。
重复消费问题
本来消费者消费消息时,在ack应答的时候,如果网络超时,本身就可能会消费重复的消息。但由于消息发送者增加了重试机制,会导致消费者重复消息的概率增大。
那么,如何解决重复消息问题呢? 答:加一张消息处理表。
那么,如何解决重复消息问题呢? 答:加一张消息处理表。
消费者读到消息之后,先判断一下消息处理表,是否存在该消息,如果存在,表示是重复消费,则直接返回。如果不存在,则进行下单操作,接着将该消息写入消息处理表中,再返回。
有个比较关键的点是:下单和写消息处理表,要放在同一个事务中,保证原子操作。
有个比较关键的点是:下单和写消息处理表,要放在同一个事务中,保证原子操作。
垃圾消息问题
这套方案表面上看起来没有问题,但如果出现了消息消费失败的情况。比如:由于某些原因,消息消费者下单一直失败,一直不能回调状态变更接口,这样job会不停的重试发消息。最后,会产生大量的垃圾消息。
那么,如何解决这个问题呢?
答:每次在job重试时,需要先判断一下消息发送表中该消息的发送次数是否达到最大限制,如果达到了,则直接返回。如果没有达到,则将次数加1,然后发送消息。
这样如果出现异常,只会产生少量的垃圾消息,不会影响到正常的业务。
那么,如何解决这个问题呢?
答:每次在job重试时,需要先判断一下消息发送表中该消息的发送次数是否达到最大限制,如果达到了,则直接返回。如果没有达到,则将次数加1,然后发送消息。
这样如果出现异常,只会产生少量的垃圾消息,不会影响到正常的业务。
延迟消费问题
通常情况下,如果用户秒杀成功了,下单之后,在15分钟之内还未完成支付的话,该订单会被自动取消,回退库存。
那么,在15分钟内未完成支付,订单被自动取消的功能,要如何实现呢? 我们首先想到的可能是job,因为它比较简单。
但job有个问题,需要每隔一段时间处理一次,实时性不太好。 还有更好的方案?
答:使用延迟队列。
我们都知道rocketmq,自带了延迟队列的功能。
下单时消息生产者会先生成订单,此时状态为待支付,然后会向延迟队列中发一条消息。达到了延迟时间,消息消费者读取消息之后,会查询该订单的状态是否为待支付。如果是待支付状态,则会更新订单状态为取消状态。如果不是待支付状态,说明该订单已经支付过了,则直接返回。
那么,在15分钟内未完成支付,订单被自动取消的功能,要如何实现呢? 我们首先想到的可能是job,因为它比较简单。
但job有个问题,需要每隔一段时间处理一次,实时性不太好。 还有更好的方案?
答:使用延迟队列。
我们都知道rocketmq,自带了延迟队列的功能。
下单时消息生产者会先生成订单,此时状态为待支付,然后会向延迟队列中发一条消息。达到了延迟时间,消息消费者读取消息之后,会查询该订单的状态是否为待支付。如果是待支付状态,则会更新订单状态为取消状态。如果不是待支付状态,说明该订单已经支付过了,则直接返回。
还有个关键点,用户完成支付之后,会修改订单状态为已支付。
作为互联网行业,肯定会涉及资金转账等要求事务一致的场景, Kafka 不支持分布式事务,RocketMQ 支持分布式事务,而且理论上不会丢消息,整体对比来看,还是RocketMQ更加优越。而且RocketMQ是经历了多次双十一考验的,可用性是毋庸置疑的!
NameServer:命名发现服务,更新和路由发现broker;其在RocketMQ中起着中转承接的作用,是一个无状态的服务,多个NameServer之间不通信。
Broker-Master:broker 消息主机服务器。
Broker-Slave:broker 消息从机服务器。
Producer:消息生产者。
Consumer:消息消费者。
Broker-Master:broker 消息主机服务器。
Broker-Slave:broker 消息从机服务器。
Producer:消息生产者。
Consumer:消息消费者。
服务启动时执行,初始化了发送消息、消费消息、清理过期请求等各种线程池和监听事件。
消息由客户端MQProducer发出,调用了通信层RemotingClient接口(实现类是NettyRemotingClient)的invokeAsync方法。
消息从Client调入通信层的NettyRemotingClient:写入通道后,就等待Netty的Selector轮询出来,调用后续broker处理任务。
发送的消息到达broker,调用了MessageStore接口的putMessage()方法,而MessageStore的实现类是DefaultMessageStore,该类是位于存储层store
存储层整体结构
业务层
存储逻辑
磁盘io交互
消息写入内存就算完成了,之后就是通过判断配置文件的主从同步类型和刷盘类型,进行刷盘和HA主从同步。
1、同步刷盘
每次发送消息,消息都直接存储在FileChannel中,使用的是(MapFile的MappdByteBuffer),然后直接调用force()方法刷写到磁盘,等到force刷盘成功后,再返回给调用方,(GroupCommitRequest.waitForFlush)就是其同步调用的实现。
2、异步刷盘
分为两种情况,是否开启堆内存缓存池,具体配置参数:MessageStoreConfig.transientStorePoolEnable
transientStorePoolEnable=true
先写入内存字节缓冲区(writeBuffer) ----> 从内存字节缓冲区(write buffer)提交(commit)到文件通道(fileChannel) ----> 文件通道(fileChannel)定时flush到磁盘
transientStorePoolEnable=false(默认)
写入映射文件字节缓冲区(mappedByteBuffer) ----> 映射文件字节缓冲区(mappedByteBuffer)定时flush
每次发送消息,消息都直接存储在FileChannel中,使用的是(MapFile的MappdByteBuffer),然后直接调用force()方法刷写到磁盘,等到force刷盘成功后,再返回给调用方,(GroupCommitRequest.waitForFlush)就是其同步调用的实现。
2、异步刷盘
分为两种情况,是否开启堆内存缓存池,具体配置参数:MessageStoreConfig.transientStorePoolEnable
transientStorePoolEnable=true
先写入内存字节缓冲区(writeBuffer) ----> 从内存字节缓冲区(write buffer)提交(commit)到文件通道(fileChannel) ----> 文件通道(fileChannel)定时flush到磁盘
transientStorePoolEnable=false(默认)
写入映射文件字节缓冲区(mappedByteBuffer) ----> 映射文件字节缓冲区(mappedByteBuffer)定时flush
如果是SYNC_MASTER模式,消息发送者将消息刷写到磁盘后,需要继续等待新数据被传输到从服务器,从服务器数据的复制是在另外一个线程HAConnection中去拉取,所以消息发送者在这里需要等待数据传输的结果,GroupTransferService就是实现该功能。而ASYNC_MASTER模式,消息在master写入成功,即会返回成功,无需等待slave。
所以,异步复制性能高于同步双写,业务没有主从强一致要求的话,推荐使用ASYNC_MASTER模式。
所以,异步复制性能高于同步双写,业务没有主从强一致要求的话,推荐使用ASYNC_MASTER模式。
Kafka
Kafka之所以可以实现高吞吐,主要依赖于以下5点:
Zero Copy(零拷贝)技术
Page Cache(页缓存)+磁盘顺序写
分区分段+索引
批量读写
批量压缩
Zero Copy(零拷贝)技术
Page Cache(页缓存)+磁盘顺序写
分区分段+索引
批量读写
批量压缩
Kafka消费模式
一对一
一对多
Kafka的基础架构
生产者
消费者
消费者组
Broker
topic
Partition分区
Replica副本
leader
follower
文件存储
子主题
ack
为保证producer发送的数据能够可靠的发送到指定的topic中,topic的每个partition收到producer发送的数据后,都需要向producer发送ackacknowledgement,如果producer收到ack就会进行下一轮的发送,否则重新发送数据。
发送ack的时机
确保有follower与leader同步完成,leader在发送ack,这样可以保证在leader挂掉之后,follower中可以选出新的leader(主要是确保follower中数据不丢失)
follower同步完成多少才发送ack
半数以上的follower同步完成,即可发送ack
全部的follower同步完成,才可以发送ack
全部的follower同步完成,才可以发送ack
producer返ack,0无落盘直接返,1只leader落盘然后返,-1全部落盘然后返
分区分配策略
round-robin轮训
range重新分配
range重新分配
ElasticSearch
ES在高并发下如何保证读写一致性
对于更新操作:可以通过版本号使用乐观并发控制,以确保新版本不会被旧版本覆盖
对于写操作,一致性级别支持 quorum/one/all,默认为 quorum,即只有当大多数分片可用时才允许写操作。但即使大多数可用,也可能存在因为网络等原因导致写入副本失败,这样该副本被认为故障,副本将会在一个不同的节点上重建。
对于读操作,可以设置 replication 为 sync(默认),这使得操作在主分片和副本分片都完成后才会返回;如果设置replication 为 async 时,也可以通过设置搜索请求参数 _preference 为 primary 来查询主分片,确保文档是最新版本。
ES集群 如何 选举 Master
当集群中 master 候选节点数量不小于3个时(node.master: true),可以通过设置最少投票通过数量(discovery.zen.minimum_master_nodes),设置超过所有候选节点一半以上来解决脑裂问题,即设置为 (N/2)+1;
Netty
Selector
Netty 基于 Selector 对象实现 I/O 多路复用,通过 Selector 一个线程可以监听多个连接的 Channel 事件。
NioEventLoop
其中维护了一个线程和任务队列,支持异步提交执行任务,线程启动时会调用 NioEventLoop 的 run 方法,执行 I/O 任务和非 I/O 任务。
NioEventLoopGroup
主要管理 eventLoop 的生命周期,可以理解为一个线程池,内部维护了一组线程,每个线程(NioEventLoop)负责处理多个 Channel 上的事件,而一个 Channel 只对应于一个线程。
ChannelHandler
是一个接口,处理 I/O 事件或拦截 I/O 操作,并将其转发到其 ChannelPipeline(业务处理链)中的下一个处理程序。
ChannelHandlerContext
保存 Channel 相关的所有上下文信息,同时关联一个 ChannelHandler 对象。
Netty 基于 Selector 对象实现 I/O 多路复用,通过 Selector 一个线程可以监听多个连接的 Channel 事件。
NioEventLoop
其中维护了一个线程和任务队列,支持异步提交执行任务,线程启动时会调用 NioEventLoop 的 run 方法,执行 I/O 任务和非 I/O 任务。
NioEventLoopGroup
主要管理 eventLoop 的生命周期,可以理解为一个线程池,内部维护了一组线程,每个线程(NioEventLoop)负责处理多个 Channel 上的事件,而一个 Channel 只对应于一个线程。
ChannelHandler
是一个接口,处理 I/O 事件或拦截 I/O 操作,并将其转发到其 ChannelPipeline(业务处理链)中的下一个处理程序。
ChannelHandlerContext
保存 Channel 相关的所有上下文信息,同时关联一个 ChannelHandler 对象。
ChannelPipeline 是保存 ChannelHandler 的 List,用于处理或拦截 Channel 的入站事件和出站操作。实现了一种高级形式的拦截过滤器模式,使用户可以完全控制事件的处理方式,以及 Channel 中各个的 ChannelHandler 如何相互交互。
Netty中的事件分为Inbond事件和Outbound事件。
Inbound事件通常由I/O线程触发,如TCP链路建立事件、链路关闭事件、读事件、异常通知事件等。
Outbound事件通常是用户主动发起的网络I/O操作,如用户发起的连接操作、绑定操作、消息发送等。
一个 Channel 包含了一个 ChannelPipeline,而 ChannelPipeline 中又维护了一个由 ChannelHandlerContext 组成的双向链表,并且每个 ChannelHandlerContext 中又关联着一个 ChannelHandler。
入站事件和出站事件在一个双向链表中,入站事件会从链表 head 往后传递到最后一个入站的 handler,出站事件会从链表 tail 往前传递到最前一个出站的 handler,两种类型的 handler 互不干扰。
Netty中的事件分为Inbond事件和Outbound事件。
Inbound事件通常由I/O线程触发,如TCP链路建立事件、链路关闭事件、读事件、异常通知事件等。
Outbound事件通常是用户主动发起的网络I/O操作,如用户发起的连接操作、绑定操作、消息发送等。
一个 Channel 包含了一个 ChannelPipeline,而 ChannelPipeline 中又维护了一个由 ChannelHandlerContext 组成的双向链表,并且每个 ChannelHandlerContext 中又关联着一个 ChannelHandler。
入站事件和出站事件在一个双向链表中,入站事件会从链表 head 往后传递到最后一个入站的 handler,出站事件会从链表 tail 往前传递到最前一个出站的 handler,两种类型的 handler 互不干扰。
Dubbo
Zookeeper
zab协议
脑裂问题
Nacos
服务注册
客户端
1.心跳参数校验
2.最终的服务名格式:serviceName@@groupName
3.如果是临时实例则会开启心跳包
4.服务注册 (参数组装调用API请求注册)
2.最终的服务名格式:serviceName@@groupName
3.如果是临时实例则会开启心跳包
4.服务注册 (参数组装调用API请求注册)
1.单机注册中心,失败重试(默认3次,前提是nacos异常)
2.集群注册中心,随机挑选一个注册,失败则轮询其他注册中心
3.最终调用callServer方法 (API_URL: IP:PORT/nacos/v1/ns/instance)
2.集群注册中心,随机挑选一个注册,失败则轮询其他注册中心
3.最终调用callServer方法 (API_URL: IP:PORT/nacos/v1/ns/instance)
服务端
注册表
为什么一个服务会有多个集群,不应该一个服务就一个集群吗?
这里可以理解为按机房划分集群,不管有多少个集群都属于你该服务的。比如上海机房有一个SH 集群,深圳机房有一个SZ集群,请求的时候你可以按地区请求最近的集群实例,如果整个地区集群都不可用那么可以请求其他地区的集群实例。
这里可以理解为按机房划分集群,不管有多少个集群都属于你该服务的。比如上海机房有一个SH 集群,深圳机房有一个SZ集群,请求的时候你可以按地区请求最近的集群实例,如果整个地区集群都不可用那么可以请求其他地区的集群实例。
注册流程
1.拿到创建好的service放入注册表,为其 开启一个心跳检测,并将这个service加入监听列表
2.完成实例的注册表更新,并完成nacos集群同步
2.完成实例的注册表更新,并完成nacos集群同步
consistencyService.put(key, instances)方法完成注册和集群同步
consistencyService有很多种实现,根据实例的类型来判断具体走哪种实现方式,这里我们以临时实例为例,主要看看DistroConsistencyServiceImpl
DistroConsistencyServiceImpl.put 临时实例的注册方法
临时实例的添加
onPut方法:
1.会将任务放入Notifier内部的阻塞队列中,Notifier是个Runnable(异步执行任务)
2.最后会回到Service.onChange方法更新实例,内部调用updateIPs方法,这里面需要注意更新后会触发一个服务变更事件(后面有用)
onPut方法:
1.会将任务放入Notifier内部的阻塞队列中,Notifier是个Runnable(异步执行任务)
2.最后会回到Service.onChange方法更新实例,内部调用updateIPs方法,这里面需要注意更新后会触发一个服务变更事件(后面有用)
Service.updateIPs:
public void updateIPs(Collection<Instance> instances, boolean ephemeral) {
// 准备一个Map,key是cluster,值是集群下的Instance集合
Map<String, List<Instance>> ipMap = new HashMap<>(clusterMap.size());
// 获取服务的所有cluster名称
for (String clusterName : clusterMap.keySet()) {
ipMap.put(clusterName, new ArrayList<>());
}
for (Instance instance : instances) {
try {
if (instance == null) {
Loggers.SRV_LOG.error("[NACOS-DOM] received malformed ip: null");
continue;
}
// 判断实例是否包含clusterName,没有的话用默认cluster
if (StringUtils.isEmpty(instance.getClusterName())) {
instance.setClusterName(UtilsAndCommons.DEFAULT_CLUSTER_NAME);
}
// 判断cluster是否存在,不存在则创建新的cluster
if (!clusterMap.containsKey(instance.getClusterName())) {
Loggers.SRV_LOG
.warn("cluster: {} not found, ip: {}, will create new cluster with default configuration.",
instance.getClusterName(), instance.toJson());
Cluster cluster = new Cluster(instance.getClusterName(), this);
cluster.init();
getClusterMap().put(instance.getClusterName(), cluster);
}
// 获取当前cluster实例的集合,不存在则创建新的
List<Instance> clusterIPs = ipMap.get(instance.getClusterName());
if (clusterIPs == null) {
clusterIPs = new LinkedList<>();
ipMap.put(instance.getClusterName(), clusterIPs);
}
// 添加新的实例到 Instance 集合
clusterIPs.add(instance);
} catch (Exception e) {
Loggers.SRV_LOG.error("[NACOS-DOM] failed to process ip: " + instance, e);
}
}
for (Map.Entry<String, List<Instance>> entry : ipMap.entrySet()) {
//make every ip mine
List<Instance> entryIPs = entry.getValue();
// 将实例集合更新到 clusterMap(注册表)
clusterMap.get(entry.getKey()).updateIps(entryIPs, ephemeral);
}
setLastModifiedMillis(System.currentTimeMillis());
//触发服务变更事件
getPushService().serviceChanged(this);
StringBuilder stringBuilder = new StringBuilder();
for (Instance instance : allIPs()) {
stringBuilder.append(instance.toIpAddr()).append("_").append(instance.isHealthy()).append(",");
}
Loggers.EVT_LOG.info("[IP-UPDATED] namespace: {}, service: {}, ips: {}", getNamespaceId(), getName(),
stringBuilder.toString());
}
————————————————
版权声明:本文为CSDN博主「Colins~」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/weixin_44102992/article/details/127760597
public void updateIPs(Collection<Instance> instances, boolean ephemeral) {
// 准备一个Map,key是cluster,值是集群下的Instance集合
Map<String, List<Instance>> ipMap = new HashMap<>(clusterMap.size());
// 获取服务的所有cluster名称
for (String clusterName : clusterMap.keySet()) {
ipMap.put(clusterName, new ArrayList<>());
}
for (Instance instance : instances) {
try {
if (instance == null) {
Loggers.SRV_LOG.error("[NACOS-DOM] received malformed ip: null");
continue;
}
// 判断实例是否包含clusterName,没有的话用默认cluster
if (StringUtils.isEmpty(instance.getClusterName())) {
instance.setClusterName(UtilsAndCommons.DEFAULT_CLUSTER_NAME);
}
// 判断cluster是否存在,不存在则创建新的cluster
if (!clusterMap.containsKey(instance.getClusterName())) {
Loggers.SRV_LOG
.warn("cluster: {} not found, ip: {}, will create new cluster with default configuration.",
instance.getClusterName(), instance.toJson());
Cluster cluster = new Cluster(instance.getClusterName(), this);
cluster.init();
getClusterMap().put(instance.getClusterName(), cluster);
}
// 获取当前cluster实例的集合,不存在则创建新的
List<Instance> clusterIPs = ipMap.get(instance.getClusterName());
if (clusterIPs == null) {
clusterIPs = new LinkedList<>();
ipMap.put(instance.getClusterName(), clusterIPs);
}
// 添加新的实例到 Instance 集合
clusterIPs.add(instance);
} catch (Exception e) {
Loggers.SRV_LOG.error("[NACOS-DOM] failed to process ip: " + instance, e);
}
}
for (Map.Entry<String, List<Instance>> entry : ipMap.entrySet()) {
//make every ip mine
List<Instance> entryIPs = entry.getValue();
// 将实例集合更新到 clusterMap(注册表)
clusterMap.get(entry.getKey()).updateIps(entryIPs, ephemeral);
}
setLastModifiedMillis(System.currentTimeMillis());
//触发服务变更事件
getPushService().serviceChanged(this);
StringBuilder stringBuilder = new StringBuilder();
for (Instance instance : allIPs()) {
stringBuilder.append(instance.toIpAddr()).append("_").append(instance.isHealthy()).append(",");
}
Loggers.EVT_LOG.info("[IP-UPDATED] namespace: {}, service: {}, ips: {}", getNamespaceId(), getName(),
stringBuilder.toString());
}
————————————————
版权声明:本文为CSDN博主「Colins~」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/weixin_44102992/article/details/127760597
临时实例的集群同步
distroProtocol.sync()临时实例集群同步:
distroProtocol.sync()临时实例集群同步:
遍历集群中其他节点
定义一个DistroDelayTask异步任务放入一个ConcurrentHashMap中,会有一个ScheduledExecutorService线程池定时从这个map中取任务执行
定义一个DistroDelayTask异步任务放入一个ConcurrentHashMap中,会有一个ScheduledExecutorService线程池定时从这个map中取任务执行
Distro协议的设计思想
Nacos 每个节点是平等的都可以处理写请求,同时把新数据同步到其他节点。
每个节点只负责部分数据,定时发送自己负责数据的校验值到其他节点来保持数据一致性。
每个节点独立处理读请求,及时从本地发出响应。
每个节点只负责部分数据,定时发送自己负责数据的校验值到其他节点来保持数据一致性。
每个节点独立处理读请求,及时从本地发出响应。
数据校验
在 Distro 集群启动之后,各台机器之间会定期的发送心跳。心跳信息主要为各个机器上的所有数据的元信息(之所以使用元信息,是因为需要保证网络中数据传输的量级维持在一个较低水平)。这种数据校验会以心跳的形式进行,即每台机器在固定时间间隔会向其他机器发起一次数据校验请求。
在 Distro 集群启动之后,各台机器之间会定期的发送心跳。心跳信息主要为各个机器上的所有数据的元信息(之所以使用元信息,是因为需要保证网络中数据传输的量级维持在一个较低水平)。这种数据校验会以心跳的形式进行,即每台机器在固定时间间隔会向其他机器发起一次数据校验请求。
写操作
前置的 Filter 拦截请求,并根据请求中包含的 IP 和 port 信息计算其所属的 Distro 责任节点,并将该请求转发到所属的 Distro 责任节点上。
责任节点上的 Controller 将写请求进行解析。
Distro 协议定期执行 Sync 任务,将本机所负责的所有的实例信息同步到其他节点上。
前置的 Filter 拦截请求,并根据请求中包含的 IP 和 port 信息计算其所属的 Distro 责任节点,并将该请求转发到所属的 Distro 责任节点上。
责任节点上的 Controller 将写请求进行解析。
Distro 协议定期执行 Sync 任务,将本机所负责的所有的实例信息同步到其他节点上。
读操作
由于每台机器上都存放了全量数据,因此在每一次读操作中,Distro 机器会直接从本地拉取数据。快速响应。
由于每台机器上都存放了全量数据,因此在每一次读操作中,Distro 机器会直接从本地拉取数据。快速响应。
总结
Distro 协议是 Nacos 对于临时实例数据开发的一致性协议。其数据存储在缓存中,并且会在启动时进行全量数据同步,并定期进行数据校验。
在 Distro 协议的设计思想下,每个 Distro 节点都可以接收到读写请求。所有的 Distro 协议的请求场景主要分为三种情况:
1、当该节点接收到属于该节点负责的实例的写请求时,直接写入。
2、当该节点接收到不属于该节点负责的实例的写请求时,将在集群内部路由,转发给对应的节点,从而完成读写。
3、当该节点接收到任何读请求时,都直接在本机查询并返回(因为所有实例都被同步到了每台机器上)。
Distro 协议作为 Nacos 的内嵌临时实例一致性协议,保证了在分布式环境下每个节点上面的服务信息的状态都能够及时地通知其他节点,可以维持数十万量级服务实例的存储和一致性。
Distro 协议是 Nacos 对于临时实例数据开发的一致性协议。其数据存储在缓存中,并且会在启动时进行全量数据同步,并定期进行数据校验。
在 Distro 协议的设计思想下,每个 Distro 节点都可以接收到读写请求。所有的 Distro 协议的请求场景主要分为三种情况:
1、当该节点接收到属于该节点负责的实例的写请求时,直接写入。
2、当该节点接收到不属于该节点负责的实例的写请求时,将在集群内部路由,转发给对应的节点,从而完成读写。
3、当该节点接收到任何读请求时,都直接在本机查询并返回(因为所有实例都被同步到了每台机器上)。
Distro 协议作为 Nacos 的内嵌临时实例一致性协议,保证了在分布式环境下每个节点上面的服务信息的状态都能够及时地通知其他节点,可以维持数十万量级服务实例的存储和一致性。
图解
是否临时节点
ap
cp
Raft设计思想
它这个raft协议最终一致实现原理其实很简单,就是leader在发送心跳的时候会将这个数据的key与timestamp(可以理解成版本号,我们上面也介绍过) 带给follower节点,然后follower节点收到leader 发过来的心跳,会将本地的key ,timestamp 与leader带过来的key,timestamp 进行比较,如果本地少了这个key ,或者是key对应的timestamp 低于leader的话,就会发送请求去leader那拉取不一致的数据
客户端:启动则获取自身配置信息,发起http请求注册,临时实例同时会开启心跳机制(下面会说),服务端是单机的情况下请求失败会重试三次,服务端是单机的集群的情况下请求失败会轮询请求
服务端:
本地通过一个Map保存所有服务信息,注册的实质就是往map里面添加信息
会先创建空的服务,后更新服务中的实例信息
服务创建后会初始化服务,启动心跳检测
往服务中添加实例的时候会判断实例是永久实例还是临时实例,不同类型的实例有不同的处理方式
注册后同时会发布服务变更事件(后面说,先记着这个事件)
服务端:
本地通过一个Map保存所有服务信息,注册的实质就是往map里面添加信息
会先创建空的服务,后更新服务中的实例信息
服务创建后会初始化服务,启动心跳检测
往服务中添加实例的时候会判断实例是永久实例还是临时实例,不同类型的实例有不同的处理方式
注册后同时会发布服务变更事件(后面说,先记着这个事件)
为什么客户端注册会先开启心跳后发起注册请求?
因为心跳是异步定时执行,就算后续的注册发生某意外注册失败,心跳机制还可以弥补注册(因为心跳也可以注册),如果是先发起注册后开启心跳,有可能注册发生某意外就直接终止了,心跳还没开启
服务端注册怎么保证线程安全?
服务器注册会先创建一个空的服务,后对该服务填充信息初始化,保存服务的map用ConcurrentHashMap修饰的,所以此过程是线程安全的,后续再对服务内实例更新的时候,采用synchronized对该服务做了加锁操作
服务端注册怎么保证性能?(临时实例)
前置操作时采用ConcurrentHashMap和synchronized锁服务,前者是最优的线程安全map,后者锁的是“服务”颗粒度一定程度的保证了性能,后续均采用了异步更新,如本地注册表更新采用了阻塞队列异步执行,临时实例集群同步过程中同样采用了阻塞队列异步执行机制,因为为阻塞的异步执行,所以保值性能的同时也保证了资源不会占用异常
心跳机制
临时实例
子主题
客户端
临时实例在注册的时候会开启心跳包,这个在前面有说(默认5s心跳)
服务端
1.从注册表中获取实例信息,若无则重新注册
2.从注册表中获取服务,若无则直接异常(实例都注册完了,服务还找不到是不合理的)
3.开启异步任务将临时实例状态 置为健康状态,然后返回
2.从注册表中获取服务,若无则直接异常(实例都注册完了,服务还找不到是不合理的)
3.开启异步任务将临时实例状态 置为健康状态,然后返回
心跳检测处理
实例如果宕机或者其他什么请求无法发送心跳,那么服务端自然也要对这个实例进行处理,就在服务初始化的时候,会开启会实例的心跳检测任务,上面也有提到过
找到心跳超时的实例,改变其健康状态,并发布serviceChange事件(后面说),还有实例心跳超时事件
找到满足删除条件的实例,从注册表中删除该实例信息(HTTP请求调用API,异步删除)
默认15s超时,30s剔除
找到满足删除条件的实例,从注册表中删除该实例信息(HTTP请求调用API,异步删除)
默认15s超时,30s剔除
永久实例
子主题
临时实例:
采用客户端心跳检测模式,心跳检测周期5秒
心跳间隔超过15秒(默认)则标记为不健康
心跳间隔超过30秒(默认)则从服务列表删除
永久实例:
采用服务端主动健康检测方式
周期为2000 + 5000毫秒内的随机数
检测异常只会标记为不健康,不会删除
采用客户端心跳检测模式,心跳检测周期5秒
心跳间隔超过15秒(默认)则标记为不健康
心跳间隔超过30秒(默认)则从服务列表删除
永久实例:
采用服务端主动健康检测方式
周期为2000 + 5000毫秒内的随机数
检测异常只会标记为不健康,不会删除
服务发现
实例是如何得知其他实例的信息呢?毕竟需要远程调用嘛
两种方式:1.客户端主动获取(定时更新)、2.服务端主动推送(长连接推送变更信息)
两种方式:1.客户端主动获取(定时更新)、2.服务端主动推送(长连接推送变更信息)
客户端主动获取
子主题
客户端
1.先是故障转移机制判断是否去本地文件中读取信息,读到则返回
2.再去本地服务列表读取信息(本地缓存),没读到则创建一个空的服务,然后立刻去nacos中读取更新
3.读到了就返回,同时开启定时更新,定时向服务端同步信息 (正常1s,异常最多60s一次)
2.再去本地服务列表读取信息(本地缓存),没读到则创建一个空的服务,然后立刻去nacos中读取更新
3.读到了就返回,同时开启定时更新,定时向服务端同步信息 (正常1s,异常最多60s一次)
服务端
服务端这边处理请求就比较简单了,除去参数获取以及相关校验就剩服务列表的获取了
服务端主动推送
子主题
既然是主动推送那么就需要两个条件:1.建立长连接2.触发推送的事件
客户端
服务端
服务变更就会触发事件让服务端主动推送服务变更信息
服务的发现有两种方式
客户端主动获取:
会先读取缓存,缓存内读取不到则会去服务端获取,同时开启一个定时任务定时更新
定时任务1s一次,异常时会延长时间最长60s
拉取URL:/nacos/v1/ns/instance/list
服务端主动推送
服务端和客户端在启动后会建立一个长连接
服务端服务变更后会发布服务变更事件ServiceChangeEvent,会通过长连接将变更后的信息发送给客户端
客户端更新的方式是hostReactor.processServiceJson方法,会写入缓存、发布实例变更事件、写入磁盘
客户端主动获取:
会先读取缓存,缓存内读取不到则会去服务端获取,同时开启一个定时任务定时更新
定时任务1s一次,异常时会延长时间最长60s
拉取URL:/nacos/v1/ns/instance/list
服务端主动推送
服务端和客户端在启动后会建立一个长连接
服务端服务变更后会发布服务变更事件ServiceChangeEvent,会通过长连接将变更后的信息发送给客户端
客户端更新的方式是hostReactor.processServiceJson方法,会写入缓存、发布实例变更事件、写入磁盘
寻址机制
Nacos 支持单机部署以及集群部署,针对单机模式,Nacos 只是自己和自己通信;对于集群模式,则集群内的每个 Nacos 成员都需要相互通信。因此这就带来一个问题,该以何种方式去管理集群内的 Nacos 成员节点信息,这就是 Nacos 内部的寻址机制。
单机寻址
单机寻址对应StandaloneMemberLookup类,查看核心的doStart方法。单机模式的寻址模式很简单,就是找到自己的IP:PORT组合信息,然后格式化为一个节点信息,调用afterLookup 然后将信息存储到 ServerMemberManager 中。
文件寻址
文件寻址模式就是每个 Nacos 节点需要维护一个叫做 cluster.conf 的文件,其中填写了每个成员节点的 IP 信息。
文件寻址对应FileConfigMemberLookup类,查看核心的doStart方法。调用readClusterConfFromDisk方法读取本节点的cluster.conf文件,获取保存的节点成员列表。并注册FileWatcher监听cluster.conf的变化,有变更会被监听并更新缓存地址列表。
文件寻址对应FileConfigMemberLookup类,查看核心的doStart方法。调用readClusterConfFromDisk方法读取本节点的cluster.conf文件,获取保存的节点成员列表。并注册FileWatcher监听cluster.conf的变化,有变更会被监听并更新缓存地址列表。
地址服务器寻址
地址服务器寻址模式是 Nacos 官方推荐的一种集群成员节点信息管理,该模式利用了一个简易的 web 服务器,用于管理 cluster.conf 文件的内容信息,这样,运维人员只需要管理这一份集群成员节点内容即可,而每个Nacos 成员节点,只需要向这个 web 节点定时请求当前最新的集群成员节点列表信息即可。
配置中心
1.在nacos上修改配置。
2.nacos客户端中ClientWorker会每隔10ms异步读取一次配置中心文件md5值。
3.和本地md5值比较,有变化的从服务器拉取。
4.将文件保存/缓存到本地。
5.通知NacosContextRefresher配置文件有变化。
6.NacosContextRefresher判断是否需要更新配置。
7.发送事件通知ContextRefresher去更新。
8.这里是更新配置的关键步骤。
9.准备一份before配置,然后通过构建新的Environment的方式拿到新的配置, 接着比较变化,得到有变化的keys。
10.构建Environment时会去读取配置文件,文件优先读本地,如果本地没有通过Http请求服务商。
11.构建NacosPropertiesSource,并重新生成ConfigurationProperties对象。
12.通知RefreshScope去更新。
13.销毁scope='refresh'的bean。
14.通知bean容器去构建新的bean(懒加载)。
15.将属性(@Value注解)注入到新的bean。
2.nacos客户端中ClientWorker会每隔10ms异步读取一次配置中心文件md5值。
3.和本地md5值比较,有变化的从服务器拉取。
4.将文件保存/缓存到本地。
5.通知NacosContextRefresher配置文件有变化。
6.NacosContextRefresher判断是否需要更新配置。
7.发送事件通知ContextRefresher去更新。
8.这里是更新配置的关键步骤。
9.准备一份before配置,然后通过构建新的Environment的方式拿到新的配置, 接着比较变化,得到有变化的keys。
10.构建Environment时会去读取配置文件,文件优先读本地,如果本地没有通过Http请求服务商。
11.构建NacosPropertiesSource,并重新生成ConfigurationProperties对象。
12.通知RefreshScope去更新。
13.销毁scope='refresh'的bean。
14.通知bean容器去构建新的bean(懒加载)。
15.将属性(@Value注解)注入到新的bean。
Sentinel
架构图
SpringBoot SPI 自动装配SentinelWebAutoConfiguration,然后将FilterRegistrationBean注入IOC,FilterRegistrationBean的作用就是为了注册CommonFilter,CommenFilter通过 filter 的方式将所有请求自动设置为 Sentinel 资源,从而达到限流目的。
以@SentinelResource作为切点
限流的判断逻辑是在 SphU.entry 方法中实现的
CtSph 是 Sph的默认实现类
校验全局上下文Context
通过lookProcessChain 方法获取一个 ProcessorSlot 链
这里的 chain 会构造一个 Slot 链,启动每一个 Slot 都有各自的责任
执行 chain.entry ,如果没有被限流,返回 entry 对象 ,否则抛出 BlockException
Seata
Seata的分布式事务解决方案是业务层面的解决方案,只依赖于单台数据库的事务能力。Seata框架中一个分布式事务包含3中角色:
Transaction Coordinator ( TC ): 事务协调器,维护全局事务的运行状态,负责协调并驱动全局事务的提交或回滚。
Transaction Manager( TM ): 控制全局事务的边界,负责开启一个全局事务,并最终发起全局提交或全局回滚的决议。
Resource Manager ( RM ): 控制分支事务,负责分支注册、状态汇报,并接收事务协调器的指令,驱动分支(本地)事务的提交和回滚。
其中,TM是一个分布式事务的发起者和终结者,TC负责维护分布式事务的运行状态,而RM则负责本地事务的运行。
Transaction Coordinator ( TC ): 事务协调器,维护全局事务的运行状态,负责协调并驱动全局事务的提交或回滚。
Transaction Manager( TM ): 控制全局事务的边界,负责开启一个全局事务,并最终发起全局提交或全局回滚的决议。
Resource Manager ( RM ): 控制分支事务,负责分支注册、状态汇报,并接收事务协调器的指令,驱动分支(本地)事务的提交和回滚。
其中,TM是一个分布式事务的发起者和终结者,TC负责维护分布式事务的运行状态,而RM则负责本地事务的运行。
TM 向 TC 申请开启一个全局事务,全局事务创建成功并生成一个全局唯一的 XID。
XID 在微服务调用链路的上下文中传播。
RM 向 TC 注册分支事务,接着执行这个分支事务并提交(重点:RM在第一阶段就已经执行了本地事务的提交/回滚),最后将执行结果汇报给TC。
TM 根据 TC 中所有的分支事务的执行情况,发起全局提交或回滚决议。
TC 调度 XID 下管辖的全部分支事务完成提交或回滚请求。
XID 在微服务调用链路的上下文中传播。
RM 向 TC 注册分支事务,接着执行这个分支事务并提交(重点:RM在第一阶段就已经执行了本地事务的提交/回滚),最后将执行结果汇报给TC。
TM 根据 TC 中所有的分支事务的执行情况,发起全局提交或回滚决议。
TC 调度 XID 下管辖的全部分支事务完成提交或回滚请求。
Seata在第一阶段就直接提交了分支事务
Seata能够在第一阶段直接提交事务,是因为Seata框架为每一个RM维护了一张UNDO_LOG表(这张表需要客户端自行创建),其中保存了每一次本地事务的回滚数据。因此,二阶段的回滚并不依赖于本地数据库事务的回滚,而是RM直接读取这张UNDO_LOG表,并将数据库中的数据更新为UNDO_LOG中存储的历史数据。这也是在使用seata作为分布式事务解决方案的时候,需要在参与分布式事务的每一个服务中加入UNDO_LOG表。
如果第二阶段是提交命令,那么RM事实上并不会对数据进行提交(因为一阶段已经提交了),而实发起一个异步请求删除UNDO_LOG中关于本事务的记录。
由于Seata一阶段直接提交了本地事务,因此会造成隔离性问题,因此Seata的默认隔离级别为Read Uncommitted。然而Seata也支持Read Committed的隔离级别,我们会在下文中介绍如何实现。
如果第二阶段是提交命令,那么RM事实上并不会对数据进行提交(因为一阶段已经提交了),而实发起一个异步请求删除UNDO_LOG中关于本事务的记录。
由于Seata一阶段直接提交了本地事务,因此会造成隔离性问题,因此Seata的默认隔离级别为Read Uncommitted。然而Seata也支持Read Committed的隔离级别,我们会在下文中介绍如何实现。
Spring Cloud Gateway
网关启动阶段
请求处理阶段
Openfeign
Feign组件初始化是从 @EnableFeignClients 注解开始的
@EnableFeignClients 核心有2个方法, basePackages 与 defaultConfiguration ,前者用于定义扫描包路径,后者用于定义@FeignClient组件的配置类,在配置类中可以自己定义Feign请求的 Decoder 解码器、 Encoder 编码器、 Contract 组件扫描构造器。 在注解上有一个关键注解 @Import(FeignClientsRegistrar.class) ,导入了Feign组件的注册器,用于扫描Feign组件与初始化Feign组件的Bean定义信息
创建一个 FeignClientFactoryBean 对象, BeanDefinitionBuilder.genericBeanDefinition 设置Url,Path,decode404,以及降级fallback等属性,返回 BeanDefinitionBuilder 的对象,可以看到, BeanDefinitionBuilder 的构造返回的是 FeignClientFactoryBean 的 getObject() 方法。
最核心的是 getObject 方法,方法中定义好了如何去初始化一个FeignClient组件,在代理Bean中织入了哪些方法,具体可以参见代码中文注释。 我们先暂时略过上述工厂Bean创建代理对象时,使用到Feign的其他组件,如:Encoder、Decoder、Contract等,后面再详细阐述。 当调用target方法时,会触发实际会触发FeignBuiler组件的newInstance
RequestTemplate模板需要经过一系列拦截器的处理。我们可以自定义请求拦截器,我们自定义的拦截器,也会在此时进行调用,所有实现了RequestTemplate接口的类,都会在这里被调用。比如我们可以自定义拦截器把全局事务id放在请求头里。
Feign的重试是通过配置Retryer来实现,在 FeignClientsConfiguration 自动配置类中,配置了一个默认的 Retryer.NEVER_RETRY ,表示用不重试。不重试和重试超过限制次数都是抛出异常来停止重试。可以通过自定义 Retryer 来覆盖默认的配置
@EnableFeignClients 核心有2个方法, basePackages 与 defaultConfiguration ,前者用于定义扫描包路径,后者用于定义@FeignClient组件的配置类,在配置类中可以自己定义Feign请求的 Decoder 解码器、 Encoder 编码器、 Contract 组件扫描构造器。 在注解上有一个关键注解 @Import(FeignClientsRegistrar.class) ,导入了Feign组件的注册器,用于扫描Feign组件与初始化Feign组件的Bean定义信息
创建一个 FeignClientFactoryBean 对象, BeanDefinitionBuilder.genericBeanDefinition 设置Url,Path,decode404,以及降级fallback等属性,返回 BeanDefinitionBuilder 的对象,可以看到, BeanDefinitionBuilder 的构造返回的是 FeignClientFactoryBean 的 getObject() 方法。
最核心的是 getObject 方法,方法中定义好了如何去初始化一个FeignClient组件,在代理Bean中织入了哪些方法,具体可以参见代码中文注释。 我们先暂时略过上述工厂Bean创建代理对象时,使用到Feign的其他组件,如:Encoder、Decoder、Contract等,后面再详细阐述。 当调用target方法时,会触发实际会触发FeignBuiler组件的newInstance
RequestTemplate模板需要经过一系列拦截器的处理。我们可以自定义请求拦截器,我们自定义的拦截器,也会在此时进行调用,所有实现了RequestTemplate接口的类,都会在这里被调用。比如我们可以自定义拦截器把全局事务id放在请求头里。
Feign的重试是通过配置Retryer来实现,在 FeignClientsConfiguration 自动配置类中,配置了一个默认的 Retryer.NEVER_RETRY ,表示用不重试。不重试和重试超过限制次数都是抛出异常来停止重试。可以通过自定义 Retryer 来覆盖默认的配置
基于JDK动态代理生成代理类。
根据接口类的注解声明规则,解析出底层MethodHandler
基于RequestBean动态生成request。
Encoder将bean包装成请求。
拦截器负责对请求和返回进行装饰处理。
日志记录。
基于重试器发送http请求,支持不同的http框架,默认使用的是HttpUrlConnection。
根据接口类的注解声明规则,解析出底层MethodHandler
基于RequestBean动态生成request。
Encoder将bean包装成请求。
拦截器负责对请求和返回进行装饰处理。
日志记录。
基于重试器发送http请求,支持不同的http框架,默认使用的是HttpUrlConnection。
Ribben
Ribbon的调用过程非常简单,使用RestTemplate加上@LoadBalanced注解就可以开启客户端的负载均衡
在Ribbon中有个非常重要的组件LoadBalancerClient,它是负载均衡的一个客户端
ILoadBalancer是Ribbon中最重要的一个组件,它起到了承上启下的作用,既要连接 Eureka获取服务地址,又要调用IRule利用负载均衡算法选择服务。
Ribbon在选择之前需要获取服务列表,而Ribbon本身不具有服务发现的功能,所以需要借助Eureka来解决获取服务列表的问题。
用新的服务列表更新了旧服务列表,因此当执行IPing的线程再执行时,服务列表中只剩下了一个服务实例。
综上可以发现,Ribbon为了解决服务列表的脏读现象,采用了两种手段:
更新列表
ping机制
在测试中发现,更新机制和ping机制功能基本重合,并且在ping的时候不能执行更新,在更新的时候不能运行ping,所以很难检测到ping失败的情况。
综上可以发现,Ribbon为了解决服务列表的脏读现象,采用了两种手段:
更新列表
ping机制
在测试中发现,更新机制和ping机制功能基本重合,并且在ping的时候不能执行更新,在更新的时候不能运行ping,所以很难检测到ping失败的情况。
Spring Cloud LoadBalancer
arthas
gnats
xxl-job
Spring Security
Mycat
Canal
SkyWalking
Sentry
ELK
Jenkins
Git
Maven
Docker
K8s
技术架构
pig商业版白皮书
奥里给
0 条评论
下一页