Java精简攻略
2023-06-16 09:06:33 6 举报
AI智能生成
奥里给
作者其他创作
大纲/内容
CPU
LINUX
TCP、IP
反射
枚举
字节流
字符流
io
selector
channal
buffer
nio
jdk1.7初始化的时候就分配10个容量,jdk8初始化不分配,add元素时才会分配10个元素
扩容为1.5倍
ArrayList
链表,添加元素使用尾插法,不用考虑效率问题,保存了尾结点
LinkedList
在数据量大的情况下,因为ArrayList底层数组, LinkedList底层双向链表。ArrayList增删时,越靠前头部,增删效率越低,因为ArrayList增删的时候是需要拷贝数组的。而LinkedList当增删、查找效率都不是很高,特别是对象处于链表中部位置所以当插入删除元素在中间/或者随机查找的时候,数据量大的情况下,ArrayList可能会比LinkedList快。
ArrayList插入或删除元素一定比LinkedList慢么
论遍历ArrayList要⽐LinkedList快得多,ArrayList遍历最⼤的优势在于内存的连续性,CPU的内部缓存 结构会缓存连续的内存⽚段,可以⼤幅降低读取内存的性能开销
判断是链表的前半段还是后半段,再去遍历
LinkedList.get(index)具体是怎么实现的
List
每个节点非红即黑,根节点是黑色、红节点的子节点必须是黑色、叶子结点必须是黑的空节点、每天链路的黑色节点数目一致
数组 链表 红黑树
JDK1.7之前使用头插法、JDK1.8使用尾插法
太小了就有可能频繁发生扩容,影响效率。太大了又浪费空间,不划算。
默认容量16
扩容因子 0.75
扩容2倍,位置会重新计算,可能是原下表或者是原下标加旧数组长度
hashcode相等,两个对象不一定相等,node会存在链表中
node大于8,变成红黑树,小于6,变回链表,这样会减少变换的次数
通过hashCode()的高16位异或低16位实现的,果当哈希值的高位变化很大,低位变化很小,这样就很容易造成哈希冲突了,所以这里把高低位都利用起来
hash的实现
用异或运算符
使用红黑树的原因,是因为二叉树在极端情况变成链表,红黑树可以通过左旋 右旋和变色来保持二叉树的平衡,但是旋转和变色是需要消耗资源的
外框
hashmap
concurentHashmap
Java
程序计数器本地方法栈虚拟机栈堆方法区
内存模型
try关键字监控代码块,有异常会goto到指定的异常表中的异常,最后会执行finally代码,如果finally中有return关键字,会弹至操作栈顶。
try-catch-finally
会在栈中的局部变量表中和操作数栈来回操作
i++
1 + \"\" = \"1\"
占内存,浪费时间,循环引用
引用计数法
可达性分析法
对象回收算法
强、软、弱、虚
四种引用
标记清除
标记整理
复制
垃圾回收算法
首先对象会分配在eden区,年轻代空间不足时,会触发minor gc,将eden区和from区幸存的对象复制到to区,存活的年龄+1,再交换from和to,当然,minor gc会触发STW,存活的年龄到达阈值,对象晋升至老年代,如果老年代空间不足,首先触发minor gc,如果仍然不足,触发full gc。
gc大概流程
1)初始标记(CMSinitialmark)2)并发标记(CMSconcurrentmark)3)重新标记(CMSremark)4)并发清除(CMSconcurrentsweep)
CMS
G1
ZGC
jps 工具 查看当前系统中有哪些 java 进程jmap 工具 查看堆内存占用情况 jmap - heap 进程idjconsole 工具 图形界面的,多功能的监测工具,可以连续监测jvisualvmMAT
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 方法,否则不会走双亲委派机制
自定义类加载器
占16个字节,对象头64位就是8字节,类指针压缩是4字节,不压缩是8字节,默认是压缩的,对齐(被8整除)
new dup init
锁、线程、id
markword
new Object()
在代码块前后加了监控,步入和步出,在jvm内部维护了一个锁的模型,有owner,wait,entryList
01
无锁
偏向锁在mark world维护了一个偏向锁标识,0无锁,1有锁,存了线程id,在jdk15已经被移除(用锁就说明有并发场景),偏向锁默认程序启动一秒之后启动
00
轻量级锁,实现机制是用锁记录,自旋10次,锁膨胀,晋升
10
重量级锁
锁升级
synchronized
保证内存可见性和禁止指令重排序
cpu三级缓存
每次线程读取数据会从高速缓存中读取
缓存行的数据写回到内存
清除缓存了该地址的数据
hotspot是用lock关键字实现在volitile读写操作前后加读写屏障
volatile
比较并交换,c调用cpu指令cmpxchg
cas
JVM
继承Thread
实现Runable
实现Callable和Future
线程池
核心线程 最大线程 线程存活时间 时间单位 队列 线程工厂 拒绝策略
AbortPolicy - 抛出异常,中止任务(默认)
CallerRunsPolicy - 使用调用线程执行任务
DiscardPolicy - 直接丢弃
DiscardOldestPolicy - 丢弃队列最老任务,添加新任务
拒绝策略
自定义线程池
线程池执行流程
线程创建
new runable blocked wait timedwait terminated
线程状态
sleep 会让当前线程从 Running 进入 Timed Waiting 状态
调用 yield 会让当前线程从 Running 进入 Runnable 就绪状态
sleep 和 yield
几乎没用
线程优先级priority
join
// 打断当前线程,但是打断sleep,wait,join时,会清除打断状态。 thread.interrupt(); // 可清除打断标记 Thread.interrupted();
interrupt
守护线程
obj.wait() 让进入 object 监视器的线程到 waitSet 等待obj.notify() 在 object 上正在 waitSet 等待的线程中挑一个唤醒obj.notifyAll() 让 object 上正在 waitSet 等待的线程全部唤醒
wait notify notifyAll
LockSupport.park()可以被interrupt()打断,并且不会被清除打断标记,也可以通过LockSupport.unpark(thread1)方法结束LockSupport.park()并继续执行。
Park & Unpark
分段锁 + cell + cpu伪共享
LongAdder
同步工具,它提供了一种用于构建锁和同步器的框架。
AQS
ReentrantLock是一个可重入的独占锁
公平锁和非公平锁
条件变量
ReentrantLock
ReentrantReadWriteLock 是一个基于 AQS 实现的可重入读写锁
锁降级
ReentrantReadWriteLock
StampedLock还支持优化的读操作和乐观锁模式
StampedLock
计数器
CountdownLatch
CyclicBarrier的计数器用于等待所有线程都到达某一点后再继续执行,而Semaphore的计数器用于控制同时访问某一资源的线程数量。
CyclicBarrier和Semaphore
Phaser与CyclicBarrier和CountDownLatch等同步工具类似,但提供了更为灵活和高效的同步机制。
Phaser
把哈希桶分为多个段,每个段包含多个hashEntry,再把将每个段上锁
jdk1.7
Node数组+链表+红黑树
锁住这个链表头节点(红黑树的根节点)
根据key算出hash,定位到node,如果为空,则通过cas的方式添加,不满足则加synchronized方式去添加
put
根据key算出hash,定位node,如果是首节点,直接返回,不是就查找红黑树,如果不是红黑树就遍历链表
get
JDK1.8
因为并发情况下,不知道是value存的是null,还是没查到,对于hashmap为什么可以,因为hashmap可以用containsKey方法区判断是后存在key
ConcurrentHashMap 不支持 key 或者 value 为 null 的原因
在 JDK1.6 中,对 synchronized 锁的实现引入了大量的优化,并且 synchronized 有多种锁状态,会从无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁一步步转换。减少内存开销 。假设使用可重入锁来获得同步支持,那么每个节点都需要通过继承 AQS 来获得同步支持。但并不是每个节点都需要获得同步支持的,只有链表的头节点(红黑树的根节点)需要同步,这无疑带来了巨大内存浪费。
JDK1.8 中为什么使用内置锁 synchronized替换 可重入锁 ReentrantLock?
ConcurrentHashMap 迭代器是弱一致性
ConcurrentHashMap
JUC
Spring启动扫描(BenaFactoryPostProcessor)生成BeanDefinitionput进BeanDefinitionMap
构建BeanDrfinetionMap
BeanDefinitionRegistryPostProcessorBeanFactoryPostProcessors
实例化前
遍历BeanDefinitionMap筛选出单例的BeanDefinition
单例BeanDefinition
通过@Autowired等
推断创建Bean的构造方法
通过反射创建对象
实例化
BeanNameAware
BeanClassLoaderAware
BeanFactoryAware
Aware
BeanPostProcessor#postProcessBeforeInitialization
初始化前
@PostConstruct
InitializingBean
@Bean(initMethod = \"myInitMethod\")
BeanPostProcessor#postProcessAfterInitialization
@PreDestroy
Destroyable
@Bean(destroyMethod = \"myDestroyMethod\")
初始化后
Bean生命周期
SmartLifecycle -- isAutoStartup方法
SmartLifecycle -- getPhase方法
Lifecycle -- isRunning方法SmartLifecycle -- isAutoStartup方法
Lifecycle -- start方法
SmartLifecycle -- stop方法
Lifecycle -- stop方法
SmartLifecycle
容器生命周期
IOC
Spring5默认Aop代理为JDK动态代理;SpringBoot2.0默认代理为CgLib代理,可在配置文件配置spring.aop.proxy-target-class=false,将Cglib代理切换为Jdk动态代理
代理切换
1. 众所周知,AOP代理对象是在Bean的初始化之后创建
2. 初始化完毕后,执行后置处理器的postProcessAfterInitialization方法,其中AbstractAutoProxyCreator为核心处理器核心方法:获取这个bean的advice集合,判断是否需要代理,推断代理策略,创建代理
3. 获取这个bean的advice集合
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); }
3. 选择代理策略
初始化cglib的字节码增强器Enhancer后,就开始创建代理对象
4. 创建代理对象
创建代理对象
执行around拦截器,前置执行around拦截器,准备调用目标方法准备调用的这个目标方法中,会调用before拦截器紧接着会调用after拦截器,在after拦截器中,首先会真正的执行目标方法,然后在执行自己的after方法然后,将执行完的after方法、before方法依次弹栈,回到around方法,执行around方法的后置代码
文字描述
代理对象invoke流程图解
图解
代理对象invoke流程
@Around -- before
@Before
目标方法
@AfterThrowing
@AfterReturning
@After
@Around -- after
通知顺序
getArgs(): 返回连接点的参数。getKind(): 返回连接点的类型。getSignature(): 返回连接点的签名。getStaticPart(): 返回连接点静态表示的信息。getSourceLocation(): 返回连接点在源代码中的位置。getThis(): 返回当前正在执行的对象。
JoinPoint
getArgs(): 返回连接点参数。getKind(): 返回连接点类型。getSignature(): 返回连接点签名。getStaticPart(): 返回连接点静态表示的信息。getSourceLocation(): 返回连接点在源代码中的位置。getThis(): 返回当前正在执行的对象。proceed(): 使用连接点上的参数调用目标方法,并返回结果。proceed(Object[] args): 使用指定的参数数组调用目标方法,并返回结果。proceedWithInvocation(): 调用目标方法,使用连接点上的原始参数数组。toLongString(): 返回连接点详细描述。toShortString(): 返回连接点简短描述。
ProceedingJoinPoint
getDeclaringTypeName():获取定义该方法的类的名称。getMethod():获取被通知的方法对象。getName():获取被通知的方法的名称。getParameterTypes():获取被通知方法的参数类型数组。getReturnType():获取被通知方法的返回类型。toLongString():获取完整的方法签名,包括返回类型、方法名、参数类型等信息。toString():获取简要的方法签名,只包括方法名和参数类型。
Signature
api
AOP
首先获取切面列表(其中包含事务属性相关)创建事务代理对象
Spring容器初始化的时候,已经获取过切面列表,这里直接从缓存中获取即可
获取事务属性
获取数据库连接对象关闭事务自动提交激活事务设置超时时间
根据当前事务的传播行为来创建事务
执行目标方法
如果有异常抛出,根据异常判断事务是否需要回滚
回滚事务
执行事务
事务流程图解
执行流程
在SPRNG中,支持以下五种事务隔离级别:DEFAULT:使用底层数据存储的默认隔离级别。READ_UNCOMMITTED:允许脏读,也就是说,某个事务可以读取另一个事务尚未提交的数据。READ_COMMITTED:禁止脏读,但是允许不可重复读和幻读。也就是说,一个事务只能读取已经提交的数据,但是可能会看到其他事务已提交但还未提交的更改。REPEATABLE_READ:禁止脏读和不可重复读,但是允许幻读。也就是说,一个事务在读取某行数据后,不允许其他事务对该行进行修改,直到本事务结束。SERIALIZABLE:禁止脏读、不可重复读和幻读。也就是说,一个事务在读取某行数据后,其他事务不能对该行进行任何形式的修改,直到本事务结束。需要注意的是,隔离级别越高,事务的安全性越高,但是并发性越低,因为需要加锁来保证一致性。因此,在选择隔离级别时需要根据具体场景进行权衡。
隔离级别
事务传播行为定义了方法在执行过程中如何参与到已经存在的事务中,或者如何开始一个新的事务。Spring支持以下七种事务传播行为:REQUIRED(默认):如果当前存在事务,则加入该事务;如果不存在,则创建一个新的事务。REQUIRES_NEW:创建一个新的事务,并在它自己的事务中运行。如果当前存在事务,则将当前事务挂起。SUPPORTS:如果当前存在事务,则加入该事务;如果不存在,则以非事务方式运行。MANDATORY:如果当前存在事务,则加入该事务;如果不存在,则抛出异常。NOT_SUPPORTED:以非事务方式运行,如果当前存在事务,则将其挂起。NEVER:以非事务方式运行,如果当前存在事务,则抛出异常。NESTED:如果当前存在事务,则在嵌套事务中执行。如果不存在,则执行REQUIRED类似的操作。需要注意的是,不同的传播行为对应不同的事务行为,选择合适的事务传播行为可以保证业务的正确性和事务的一致性。同时,需要注意不同的传播行为可能会带来不同的性能开销,因此需要根据具体场景进行选择。
传播方式
事务
1. 在启动Tomcat过程中,会创建DispatcherServlet对象,并执行它的初始化逻辑2. DispatcherServlet初始化过程中会创建Spring容器(根据用户的Spring配置)3. 然后初始化过程中还是初始化HandlerMapping、HandlerAdapter等等4. SpringMVC中默认提供了好几个HandlerMapping,其中有一个为RequestMappingHandlerMapping5. RequestMappingHandlerMapping的作用是去寻找Spring容器中有哪些加了@RequestMapping的方法6. 找到这些方法后,就会解析该注解上的信息,包含了指定的path,然后就把path作为key,Method作为value存到一个map中7. 当DispatcherServlet接收到请求后,RequestMappingHandlerMapping就会负责根据请求路径从map中找到对应的Method8. 然后准备执行Method,只不过,在执行Method之前,会解析该方法的各个参数9. 比如参数前面加了@RequestParam注解,那SpringMVC就会解析该注解,并从请求中取出对应request param中的数据传给该参数10. 解析完各个参数并从请求中拿到了对应的值之后,就会执行方法了11. 执行完方法得到了方法返回值后,SpringMVC会进一步解析12. 比如方法上如果加了@ResponseBody,那么就直接把返回值返回给浏览器13. 如果方法上没有加@ResponseBody,那么就要根据返回值找到对应的页面,并进行服务端渲染,再把渲染结果返回给浏览器
在初始化bean之后// Instantiate all remaining (non-lazy-init) singletons. finishBeanFactoryInitialization(beanFactory);// Last step: publish corresponding event. finishRefresh();
tomcat启动时,servlet生命周期开始,调用init()方法,最终调用DispatcherServlet#onRefresh方法,完成初始化。
DispatcherServletDispatcherServlet 是整个 Spring MVC 框架的核心,它是一个 Servlet,所有的请求都会经过它。DispatcherServlet 的主要作用是将请求分派给相应的 HandlerMapping,然后将处理结果返回给客户端。DispatcherServlet 在初始化时会加载一些配置文件,比如 applicationContext.xml、spring-mvc.xml 等等。这些配置文件中定义了 Spring MVC 的各个组件,包括 HandlerMapping、HandlerAdapter、ViewResolver 等等。
SpringBoot自动装配的时候,会注入WebMvcAutoConfiguration类
@ConditionalOnMissingBean(WebMvcConfigurationSupport.class)public class WebMvcAutoConfiguration {
WebMvcConfigurationSupport内部注入了@Bean【RequestMappingHandlerMapping】
RequestMappingInfoHandlerMapping父类的父类实现了InitializingBean接口,RequestMappingInfoHandlerMapping实现了afterPropertiesSet方法
for (String beanName : getCandidateBeanNames()) { if (!beanName.startsWith(SCOPED_TARGET_NAME_PREFIX)) { // put进registry这个HashMap中 processCandidateBean(beanName); } }
重写了InitializingBean接口,就可以在bean初始化的时候,最终会保存进MappingRegistry#registry,在registry这个HashMap中:1. 请求方法和路径({GET [/user/test]})作为key2. 方法名(public java.lang.String com.ossa.web3.controller.TestController.transaction())作为value。
HandlerMappingHandlerMapping 用来映射请求到相应的处理器,它根据请求的 URL 和其他条件来确定最终的处理器。Spring MVC 中提供了多种 HandlerMapping 实现,比如 BeanNameUrlHandlerMapping、RequestMappingHandlerMapping、SimpleUrlHandlerMapping 等等。其中,RequestMappingHandlerMapping 是最常用的 HandlerMapping 实现,它会扫描应用程序中所有带有 @Controller 注解的类,并将其中所有带有 @RequestMapping 注解的方法注册为处理器。
RequestMappingHandlerAdapter实现了InitializingBean初始化时处理@ControllerAdvice注解
HandlerAdapterHandlerAdapter 用来确定请求处理器的类型,并调用相应的方法来处理请求。Spring MVC 中提供了多种 HandlerAdapter 实现,比如 HttpRequestHandlerAdapter、SimpleControllerHandlerAdapter、AnnotationMethodHandlerAdapter 等等。其中,AnnotationMethodHandlerAdapter 是最常用的 HandlerAdapter 实现,它会根据方法的参数类型和返回值类型动态地确定请求处理器的类型,并调用相应的方法来处理请求。
ViewResolverViewResolver 用来将逻辑视图名解析为实际的视图对象,它根据逻辑视图名和其他条件来确定最终的视图对象。Spring MVC 中提供了多种 ViewResolver 实现,比如 InternalResourceViewResolver、FreeMarkerViewResolver、VelocityViewResolver 等等。其中,InternalResourceViewResolver 是最常用的 ViewResolver 实现,它会将逻辑视图名解析为 JSP 文件名,并返回一个 InternalResourceView 对象。
ModelAndViewModelAndView 是 Spring MVC 中最常用的视图模型对象,它包含了视图名称和模型数据。在处理请求时,请求处理器将模型数据填充到 ModelAndView 对象中,并返回一个 ModelAndView 对象作为处理结果。DispatcherServlet 会将 ModelAndView 对象传递给 ViewResolver,ViewResolver 会使用视图名称和模型数据来渲染响应结果。
组件
简图
流程图
servlet > filter > interceptor
生命周期
MVC
实现拦截器,需要实现HandlerInterceptor接口,该接口内部含有三个默认的方法,前置和后置分别在目标方法的前后执行,afterCompletion方法,在最后执行并且prehandle方法返回true才会执行。
@Configurationpublic 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】,该类会解析所有拦截器,包括自定义的,然后缓存起来。
在发起一个请求时,会在真正执行目标方法的前后去执行拦截器对应的方法
原理
拦截器
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 === 初始化\
@Configurationpublic 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 === 初始化\
第二种
过滤器
init
service
distory
Servlet生命周期
内嵌Tomcat
自动装配
启动流程
SpringBoot
Java SPI
SpringBoot SPI
SPI机制
@AutoConfiguration
@EnableConfigurationProperties
注解
BeanFactoryUtils
ScopedProxyUtils
SecurityUtil
工具类
1. 推断构造方法2. 实例化3. 填充属性,也就是依赖注入4. 处理Aware回调5. 初始化前,处理@PostConstruct注解6. 初始化,处理InitializingBean接口7. 初始化后,进行AOP
Bean生命周期简图
Spring中的Bean生命周期的步骤
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等注解
Spring启动流程
Spring容器启动流程
Spring中的单例Bean也是一种单例模式,只不过范围比较小,范围是beanName,一个beanName对应同一个Bean对象,不同beanName可以对应不同的Bean对象(就算是同一个类也是可以的)。
单例Bean是单例模式吗?
Spring本身并没有针对Bean做线程安全的处理,所以,如果Bean是无状态的(无成员变量),那么Bean则是线程安全的,如果Bean是有状态的(有成员变量),那么Bean则不是线程安全的。另外,Bean是不是线程安全,跟Bean的作用域没有关系,Bean的作用域只是表示Bean的生命周期范围,对于任何生命周期的Bean都是一个对象,这个对象是不是线程安全的,还是得看这个Bean对象本身。
Spring中Bean是线程安全的吗?
BeanFactory是Spring中非常核心的组件,表示Bean工厂,可以生成Bean,维护Bean,而ApplicationContext继承了BeanFactory,所以ApplicationContext拥有BeanFactory所有的特点,也是一个Bean工厂,但是ApplicationContext除开继承了BeanFactory之外,还继承了诸如EnvironmentCapable、MessageSource、ApplicationEventPublisher等接口,从而ApplicationContext还有获取系统环境变量、国际化、事件发布等功能,这是BeanFactory所不具备的
ApplicationContext和BeanFactory有什么区别?
@Index
https://www.cnblogs.com/wangyunhong/articles/16833043.html
事务失效场景
面试
Spring
Mybatis-plus
会把参数解析成字符串,可以防止sql注入
#
直接显示传入的值,不能防止sql注入,一般结合order by用,正序倒序
$
#和$
JDK动态代理
代理对象
Mybatis
redis7
新特性
这里都是指的value,key都是String
十大数据类型
RDB
AOF
持久化
页缓存
减少用户态和内核态切换
0拷贝
批处理
kafka
减少上下文切换
单线程
epool
selecter
NIO
io多路复用
三主三从
集群
Redis
DDL: 数据定义语言,用来定义数据库对象(数据库、表、字段)
DML: 数据操作语言,用来对数据库表中的数据进行增删改
DQL: 数据查询语言,用来查询数据库中表的记录
DCL: 数据控制语言,用来创建数据库用户、控制数据库的控制权限
语法
InnoDB 是一种兼顾高可靠性和高性能的通用存储引擎,在 MySQL 5.5 之后,InnoDB 是默认的 MySQL 引擎。特点:DML 操作遵循 ACID 模型,支持事务行级锁,提高并发访问性能支持外键约束,保证数据的完整性和正确性
InnoDB
MyISAM 是 MySQL 早期的默认存储引擎。特点:不支持事务,不支持外键支持表锁,不支持行锁访问速度快
MyISAM
Memory 引擎的表数据是存储在内存中的,受硬件问题、断电问题的影响,只能将这些表作为临时表或缓存使用。
Memory
在选择存储引擎时,应该根据应用系统的特点选择合适的存储引擎。对于复杂的应用系统,还可以根据实际情况选择多种存储引擎进行组合。InnoDB: 如果应用对事物的完整性有比较高的要求,在并发条件下要求数据的一致性,数据操作除了插入和查询之外,还包含很多的更新、删除操作,则 InnoDB 是比较合适的选择MyISAM: 如果应用是以读操作和插入操作为主,只有很少的更新和删除操作,并且对事务的完整性、并发性要求不高,那这个存储引擎是非常合适的。Memory: 将所有数据保存在内存中,访问速度快,通常用于临时表及缓存。Memory 的缺陷是对表的大小有限制,太大的表无法缓存在内存中,而且无法保障数据的安全性电商中的足迹和评论适合使用 MyISAM 引擎,缓存适合使用 Memory 引擎。
存储引擎的选择
存储引擎
查看执行频次
慢查询日志记录了所有执行时间超过指定参数(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';
慢查询日志
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;
profile
EXPLAIN 或者 DESC 命令获取 MySQL 如何执行 SELECT 语句的信息,包括在 SELECT 语句执行过程中表如何连接和连接的顺序。语法:# 直接在select语句之前加上关键字 explain / descEXPLAIN 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、allpossible_key:可能应用在这张表上的索引,一个或多个Key:实际使用的索引,如果为 NULL,则没有使用索引Key_len:表示索引中使用的字节数,该值为索引字段最大可能长度,并非实际使用长度,在不损失精确性的前提下,长度越短越好rows:MySQL认为必须要执行的行数,在InnoDB引擎的表中,是一个估计值,可能并不总是准确的filtered:表示返回结果的行数占需读取行数的百分比,filtered的值越大越好
explain
索引是帮助 MySQL 高效获取数据的数据结构(有序)。在数据之外,数据库系统还维护着满足特定查找算法的数据结构,这些数据结构以某种方式引用(指向)数据,这样就可以在这些数据结构上实现高级查询算法,这种数据结构就是索引。优缺点:优点:提高数据检索效率,降低数据库的IO成本通过索引列对数据进行排序,降低数据排序的成本,降低CPU的消耗缺点:索引列也是要占用空间的索引大大提高了查询效率,但降低了更新的速度,比如 INSERT、UPDATE、DELETE
相对于二叉树,层级更少,搜索效率高对于 B-Tree,无论是叶子节点还是非叶子节点,都会保存数据,这样导致一页中存储的键值减少,指针也跟着减少,要同样保存大量数据,只能增加树的高度,导致性能降低相对于 Hash 索引,B+Tree 支持范围匹配及排序操作
为什么 InnoDB 存储引擎选择使用 B+Tree 索引结构?
答:第一条语句,因为第二条需要回表查询,相当于两个步骤。
1. 以下 SQL 语句,哪个执行效率高?为什么?select * from user where id = 10;select * from user where name = 'Arm';-- 备注:id为主键,name字段创建的有索引
假设一行数据大小为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。
InnoDB 主键索引的 B+Tree 高度为多少?
最左前缀法则
索引失效情况
是优化数据库的一个重要手段,简单来说,就是在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就是无论如何都强制使用该索引。
SQL 提示
覆盖索引&回表查询
前缀索引
单列索引&联合索引
针对于数据量较大,且查询比较频繁的表建立索引针对于常作为查询条件(where)、排序(order by)、分组(group by)操作的字段建立索引尽量选择区分度高的列作为索引,尽量建立唯一索引,区分度越高,使用索引的效率越高如果是字符串类型的字段,字段长度较长,可以针对于字段的特点,建立前缀索引尽量使用联合索引,减少单列索引,查询时,联合索引很多时候可以覆盖索引,节省存储空间,避免回表,提高查询效率要控制索引的数量,索引并不是多多益善,索引越多,维护索引结构的代价就越大,会影响增删改的效率如果索引列不能存储NULL值,请在创建表时使用NOT NULL约束它。当优化器知道每列是否包含NULL值时,它可以更好地确定哪个索引最有效地用于查询
设计原则
使用规则
普通插入:采用批量插入(一次插入的数据不建议超过1000条)手动提交事务主键顺序插入
插入数据
页分裂:页可以为空,也可以填充一般,也可以填充100%,每个页包含了2-N行数据(如果一行数据过大,会行溢出),根据主键排列。
页合并:当删除一行记录时,实际上记录并没有被物理删除,只是记录被标记(flaged)为删除并且它的空间变得允许被其他记录声明使用。当页中删除的记录到达 MERGE_THRESHOLD(默认为页的50%),InnoDB会开始寻找最靠近的页(前后)看看是否可以将这两个页合并以优化空间使用。
MERGE_THRESHOLD:合并页的阈值,可以自己设置,在创建表或创建索引时指定
主键设计原则:满足业务需求的情况下,尽量降低主键的长度插入数据时,尽量选择顺序插入,选择使用 AUTO_INCREMENT 自增主键尽量不要使用 UUID 做主键或者是其他的自然主键,如身份证号业务操作时,避免对主键的修改
主键优化
order by优化
在分组操作时,可以通过索引来提高效率分组操作时,索引的使用也是满足最左前缀法则的如索引为idx_user_pro_age_stat,则句式可以是select ... where profession order by age,这样也符合最左前缀法则
group by优化
limit优化
各种用法的性能: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 的行锁是针对索引加的锁,不是针对记录加的锁,并且该索引不能失效,否则会从行锁升级为表锁。如以下两条语句:update student set no = '123' where id = 1;,这句由于id有主键索引,所以只会锁这一行;update student set no = '123' where name = 'test';,这句由于name没有索引,所以会把整张表都锁住进行数据更新,解决方法是给name字段添加索引
update优化(避免行锁升级为表锁)
SQL 优化
索引
性能分析
表空间(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存储引擎数据是按进行存放的。
逻辑存储结构
InnoDB的整个体系结构为:当业务操作的时候直接操作的是内存缓冲区,如果缓冲区当中没有数据,则会从磁盘中加载到缓冲区,增删改查都是在缓冲区的,后台线程以一定的速率刷新到磁盘。
InnoDB 引擎
事务是一组操作的集合,它是一个不可分割的工作单位,事务会把所有的操作作为一个整体一起向系统提交或撤销操作请求,即这些操作要么同时成功,要么同时败。具有ACID四大特征。原子性,一致性,持久性这三大特性由 redo log 和 undo log 日志来保证的。 隔离性 是由锁机制和MVCC保证的。
重做日志,记录的是事务提交时数据页的物理修改,是用来实现事务的持久性。 该日志文件由两部分组成:重做日志缓冲(redo log buffer)以及重做日志文件(redo log file),前者是在内存中,后者在磁盘中。当事务提交之后会把所有修改信息都存到该日志文件中,用于在刷新脏页到磁盘,发生错误时,进行数据恢复使用。个人理解: 事物每次提交的时候都会将数据刷到redo log中而不是直接将buffer pool中的数据直接刷到磁盘中(ibd文件中),是因为redo log 是顺序写,性能处理的够快,直接刷到ibd中,是随机写,性能慢。所以脏页是在下一次读的时候,或者后台线程采用一定的机制进行刷盘到ibd中。
redo 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:
事务原理
全称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:快照读会退化为当前读。
简介
有三个隐藏的字段
undo log 版本链
read view
MVCC 实现原理
总结
MVCC
错误日志是 MySQL 中 重要的日志之一,它记录了当 mysqld启动和停止时,以及服务器在运行过程中发生任何严重错误时的相关信息。当数据库出现任何故障导致无法正常使用时,建议首先查看此日志。该日志是默认开启的,默认存放目录 /var/log/,默认的日志文件名为mysqld.log 。查看日志位置:show variables like '%log_error%';
错误日志
二进制日志(BINLOG)记录了所有的 DDL(数据定义语言)语句和DML(数据操纵语言)语句,但不包括数据查询(SELECT、SHOW)语句。作用:①. 灾难时的数据恢复;②.MySQL的主从复制。在MySQL8版本中,默认二进制日志是开启着的,涉及到的参数如下:show variables like '%log_bin%';
查看
删除
二进制日志
查询日志中记录了客户端的所有操作语句,而二进制日志不包含查询数据的SQL语句。默认情况下,查询日志是未开启的。如果需要开启查询日志,可以修改MySQL的配置文件 /etc/my.cnf文件,添加如下内容:#该选项用来开启查询日志 , 可选值 : 0 或者 1 ; 0 代表关闭, 1 代表开启general_log=1#设置日志的文件名 , 如果没有指定, 默认的文件名为 host_name.loggeneral_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中的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 redolog undolog有什么区别
日志
主从复制是指将主数据库的 DDL 和 DML操作通过二进制日志传到从库服务器中,然后在从库上对这些日志重新执行(也叫重做),从而使得从库和主库的数据保持同步。MySQL支持一台主库同时向多台从库进行复制,从库同时也可以作为其他从服务器的主库,实现链状复制。
MySQL 复制的优点主要包含以下三个方面:主库出现问题,可以快速切换到从库提供服务。实现读写分离,降低主库的访问压力。可以在从库中执行备份,以避免备份期间影响主库服务。
MySQL主从复制的核心就是 二进制日志,具体的过程如下:从上图来看,复制分成三步:Master 主库在事务提交时,会把数据变更记录在二进制日志文件Binlog中。从库读取主库的二进制日志文件 Binlog ,写入到从库的中继日志 Relay Log。slave重做中继日志中的事件,将改变反映它自己的数据。
主从复制
垂直分库:以表为依据,根据业务将不同表拆分到不同库中。特点:每个库的表结构都不一样。每个库的数据也不一样。所有库的并集是全量数据。
垂直分库
垂直分表:以字段为依据,根据字段属性将不同字段拆分到不同表中。特点:每个表的结构都不一样。每个表的数据也不一样,一般通过一列(主键/外键)关联。所有表的并集是全量数据。
垂直分表
垂直拆分
水平分库:以字段为依据,按照一定策略,将一个库的数据拆分到多个库中。特点:每个库的表结构都一样。每个库的数据都不一样。所有库的并集是全量数据。
水平分库
水平分表:以字段为依据,按照一定策略,将一个表的数据拆分到多个表中。特点:每个表的表结构都一样。每个表的数据都不一样。所有表的并集是全量数据。
水平分表
水平拆分
shardingJDBC:基于AOP原理,在应用程序中对本地执行的SQL进行拦截,解析、改写、路由处理。需要自行编码配置实现,只支持java语言,性能较高。
MyCat:数据库分库分表中间件,不用调整代码即可实现分库分表,支持多种语言,性能不及前者。
sharding-proxy
实现技术
分库分表
MySQL的主从复制,是基于二进制日志(binlog)实现的。
一主一从
一个主机 Master1 用于处理所有写请求,它的从机 Slave1 和另一台主机Master2 还有它的从机 Slave2 负责所有读请求。当 Master1主机宕机后,Master2 主机负责写请求,Master1 、Master2 互为备机。
双主双从
读写分离
运维
Mysql
在往MQ发送消息的时候,很可能会发送失败,原因有很多,比如:网络问题、broker挂了、mq服务端磁盘问题等。这些情况,都可能会造成消息丢失。那么,如何防止消息丢失呢?答:加一张消息发送表。
在生产者发送mq消息之前,先把该条消息写入消息发送表,初始状态是待处理,然后再发送mq消息。消费者消费消息时,处理完业务逻辑之后,再回调生产者的一个接口,修改消息状态为已处理。如果生产者把消息写入消息发送表之后,再发送mq消息到mq服务端的过程中失败了,造成了消息丢失。 这时候,要如何处理呢?答:使用定时任务xxl-job,增加重试机制。
用job每隔一段时间去查询消息发送表中状态为待处理的数据,然后重新发送mq消息。
消息丢失问题
本来消费者消费消息时,在ack应答的时候,如果网络超时,本身就可能会消费重复的消息。但由于消息发送者增加了重试机制,会导致消费者重复消息的概率增大。那么,如何解决重复消息问题呢? 答:加一张消息处理表。
消费者读到消息之后,先判断一下消息处理表,是否存在该消息,如果存在,表示是重复消费,则直接返回。如果不存在,则进行下单操作,接着将该消息写入消息处理表中,再返回。有个比较关键的点是:下单和写消息处理表,要放在同一个事务中,保证原子操作。
重复消费问题
这套方案表面上看起来没有问题,但如果出现了消息消费失败的情况。比如:由于某些原因,消息消费者下单一直失败,一直不能回调状态变更接口,这样job会不停的重试发消息。最后,会产生大量的垃圾消息。那么,如何解决这个问题呢?答:每次在job重试时,需要先判断一下消息发送表中该消息的发送次数是否达到最大限制,如果达到了,则直接返回。如果没有达到,则将次数加1,然后发送消息。这样如果出现异常,只会产生少量的垃圾消息,不会影响到正常的业务。
垃圾消息问题
通常情况下,如果用户秒杀成功了,下单之后,在15分钟之内还未完成支付的话,该订单会被自动取消,回退库存。那么,在15分钟内未完成支付,订单被自动取消的功能,要如何实现呢? 我们首先想到的可能是job,因为它比较简单。但job有个问题,需要每隔一段时间处理一次,实时性不太好。 还有更好的方案?答:使用延迟队列。我们都知道rocketmq,自带了延迟队列的功能。下单时消息生产者会先生成订单,此时状态为待支付,然后会向延迟队列中发一条消息。达到了延迟时间,消息消费者读取消息之后,会查询该订单的状态是否为待支付。如果是待支付状态,则会更新订单状态为取消状态。如果不是待支付状态,说明该订单已经支付过了,则直接返回。
还有个关键点,用户完成支付之后,会修改订单状态为已支付。
延迟消费问题
关于MQ常见问题汇总
作为互联网行业,肯定会涉及资金转账等要求事务一致的场景, Kafka 不支持分布式事务,RocketMQ 支持分布式事务,而且理论上不会丢消息,整体对比来看,还是RocketMQ更加优越。而且RocketMQ是经历了多次双十一考验的,可用性是毋庸置疑的!
NameServer:命名发现服务,更新和路由发现broker;其在RocketMQ中起着中转承接的作用,是一个无状态的服务,多个NameServer之间不通信。Broker-Master:broker 消息主机服务器。Broker-Slave:broker 消息从机服务器。Producer:消息生产者。Consumer:消息消费者。
服务启动时执行,初始化了发送消息、消费消息、清理过期请求等各种线程池和监听事件。
消息由客户端MQProducer发出,调用了通信层RemotingClient接口(实现类是NettyRemotingClient)的invokeAsync方法。
消息从Client调入通信层的NettyRemotingClient:写入通道后,就等待Netty的Selector轮询出来,调用后续broker处理任务。
业务层
存储逻辑
磁盘io交互
存储层整体结构
消息写入内存就算完成了,之后就是通过判断配置文件的主从同步类型和刷盘类型,进行刷盘和HA主从同步。
如果是SYNC_MASTER模式,消息发送者将消息刷写到磁盘后,需要继续等待新数据被传输到从服务器,从服务器数据的复制是在另外一个线程HAConnection中去拉取,所以消息发送者在这里需要等待数据传输的结果,GroupTransferService就是实现该功能。而ASYNC_MASTER模式,消息在master写入成功,即会返回成功,无需等待slave。所以,异步复制性能高于同步双写,业务没有主从强一致要求的话,推荐使用ASYNC_MASTER模式。
RocketMQ
Kafka之所以可以实现高吞吐,主要依赖于以下5点:Zero Copy(零拷贝)技术Page Cache(页缓存)+磁盘顺序写分区分段+索引批量读写批量压缩
一对一
一对多
Kafka消费模式
生产者
消费者
消费者组
Partition分区
leader
follower
Replica副本
topic
Broker
Kafka的基础架构
子主题
文件存储
为保证producer发送的数据能够可靠的发送到指定的topic中,topic的每个partition收到producer发送的数据后,都需要向producer发送ackacknowledgement,如果producer收到ack就会进行下一轮的发送,否则重新发送数据。
确保有follower与leader同步完成,leader在发送ack,这样可以保证在leader挂掉之后,follower中可以选出新的leader(主要是确保follower中数据不丢失)
发送ack的时机
半数以上的follower同步完成,即可发送ack全部的follower同步完成,才可以发送ack
follower同步完成多少才发送ack
producer返ack,0无落盘直接返,1只leader落盘然后返,-1全部落盘然后返
ack
round-robin轮训range重新分配
分区分配策略
Kafka
对于更新操作:可以通过版本号使用乐观并发控制,以确保新版本不会被旧版本覆盖
对于写操作,一致性级别支持 quorum/one/all,默认为 quorum,即只有当大多数分片可用时才允许写操作。但即使大多数可用,也可能存在因为网络等原因导致写入副本失败,这样该副本被认为故障,副本将会在一个不同的节点上重建。
对于读操作,可以设置 replication 为 sync(默认),这使得操作在主分片和副本分片都完成后才会返回;如果设置replication 为 async 时,也可以通过设置搜索请求参数 _preference 为 primary 来查询主分片,确保文档是最新版本。
ES在高并发下如何保证读写一致性
当集群中 master 候选节点数量不小于3个时(node.master: true),可以通过设置最少投票通过数量(discovery.zen.minimum_master_nodes),设置超过所有候选节点一半以上来解决脑裂问题,即设置为 (N/2)+1;
ES集群 如何 选举 Master
ElasticSearch
SelectorNetty 基于 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
Dubbo
zab协议
脑裂问题
Zookeeper
1.单机注册中心,失败重试(默认3次,前提是nacos异常)2.集群注册中心,随机挑选一个注册,失败则轮询其他注册中心3.最终调用callServer方法 (API_URL: IP:PORT/nacos/v1/ns/instance)
1.心跳参数校验2.最终的服务名格式:serviceName@@groupName3.如果是临时实例则会开启心跳包4.服务注册 (参数组装调用API请求注册)
客户端
为什么一个服务会有多个集群,不应该一个服务就一个集群吗?这里可以理解为按机房划分集群,不管有多少个集群都属于你该服务的。比如上海机房有一个SH 集群,深圳机房有一个SZ集群,请求的时候你可以按地区请求最近的集群实例,如果整个地区集群都不可用那么可以请求其他地区的集群实例。
1.拿到创建好的service放入注册表,为其 开启一个心跳检测,并将这个service加入监听列表2.完成实例的注册表更新,并完成nacos集群同步
consistencyService有很多种实现,根据实例的类型来判断具体走哪种实现方式,这里我们以临时实例为例,主要看看DistroConsistencyServiceImpl
临时实例的添加onPut方法:1.会将任务放入Notifier内部的阻塞队列中,Notifier是个Runnable(异步执行任务)2.最后会回到Service.onChange方法更新实例,内部调用updateIPs方法,这里面需要注意更新后会触发一个服务变更事件(后面有用)
遍历集群中其他节点定义一个DistroDelayTask异步任务放入一个ConcurrentHashMap中,会有一个ScheduledExecutorService线程池定时从这个map中取任务执行
临时实例的集群同步distroProtocol.sync()临时实例集群同步:
Nacos 每个节点是平等的都可以处理写请求,同时把新数据同步到其他节点。每个节点只负责部分数据,定时发送自己负责数据的校验值到其他节点来保持数据一致性。每个节点独立处理读请求,及时从本地发出响应。
数据校验在 Distro 集群启动之后,各台机器之间会定期的发送心跳。心跳信息主要为各个机器上的所有数据的元信息(之所以使用元信息,是因为需要保证网络中数据传输的量级维持在一个较低水平)。这种数据校验会以心跳的形式进行,即每台机器在固定时间间隔会向其他机器发起一次数据校验请求。
写操作前置的 Filter 拦截请求,并根据请求中包含的 IP 和 port 信息计算其所属的 Distro 责任节点,并将该请求转发到所属的 Distro 责任节点上。责任节点上的 Controller 将写请求进行解析。Distro 协议定期执行 Sync 任务,将本机所负责的所有的实例信息同步到其他节点上。
读操作由于每台机器上都存放了全量数据,因此在每一次读操作中,Distro 机器会直接从本地拉取数据。快速响应。
总结Distro 协议是 Nacos 对于临时实例数据开发的一致性协议。其数据存储在缓存中,并且会在启动时进行全量数据同步,并定期进行数据校验。在 Distro 协议的设计思想下,每个 Distro 节点都可以接收到读写请求。所有的 Distro 协议的请求场景主要分为三种情况:1、当该节点接收到属于该节点负责的实例的写请求时,直接写入。2、当该节点接收到不属于该节点负责的实例的写请求时,将在集群内部路由,转发给对应的节点,从而完成读写。3、当该节点接收到任何读请求时,都直接在本机查询并返回(因为所有实例都被同步到了每台机器上)。Distro 协议作为 Nacos 的内嵌临时实例一致性协议,保证了在分布式环境下每个节点上面的服务信息的状态都能够及时地通知其他节点,可以维持数十万量级服务实例的存储和一致性。
Distro协议的设计思想
DistroConsistencyServiceImpl.put 临时实例的注册方法
注册流程
注册表
ap
它这个raft协议最终一致实现原理其实很简单,就是leader在发送心跳的时候会将这个数据的key与timestamp(可以理解成版本号,我们上面也介绍过) 带给follower节点,然后follower节点收到leader 发过来的心跳,会将本地的key ,timestamp 与leader带过来的key,timestamp 进行比较,如果本地少了这个key ,或者是key对应的timestamp 低于leader的话,就会发送请求去leader那拉取不一致的数据
Raft设计思想
cp
是否临时节点
服务端
客户端:启动则获取自身配置信息,发起http请求注册,临时实例同时会开启心跳机制(下面会说),服务端是单机的情况下请求失败会重试三次,服务端是单机的集群的情况下请求失败会轮询请求服务端:本地通过一个Map保存所有服务信息,注册的实质就是往map里面添加信息会先创建空的服务,后更新服务中的实例信息服务创建后会初始化服务,启动心跳检测往服务中添加实例的时候会判断实例是永久实例还是临时实例,不同类型的实例有不同的处理方式注册后同时会发布服务变更事件(后面说,先记着这个事件)
因为心跳是异步定时执行,就算后续的注册发生某意外注册失败,心跳机制还可以弥补注册(因为心跳也可以注册),如果是先发起注册后开启心跳,有可能注册发生某意外就直接终止了,心跳还没开启
为什么客户端注册会先开启心跳后发起注册请求?
服务器注册会先创建一个空的服务,后对该服务填充信息初始化,保存服务的map用ConcurrentHashMap修饰的,所以此过程是线程安全的,后续再对服务内实例更新的时候,采用synchronized对该服务做了加锁操作
服务端注册怎么保证线程安全?
前置操作时采用ConcurrentHashMap和synchronized锁服务,前者是最优的线程安全map,后者锁的是“服务”颗粒度一定程度的保证了性能,后续均采用了异步更新,如本地注册表更新采用了阻塞队列异步执行,临时实例集群同步过程中同样采用了阻塞队列异步执行机制,因为为阻塞的异步执行,所以保值性能的同时也保证了资源不会占用异常
服务端注册怎么保证性能?(临时实例)
服务注册
临时实例在注册的时候会开启心跳包,这个在前面有说(默认5s心跳)
1.从注册表中获取实例信息,若无则重新注册2.从注册表中获取服务,若无则直接异常(实例都注册完了,服务还找不到是不合理的)3.开启异步任务将临时实例状态 置为健康状态,然后返回
实例如果宕机或者其他什么请求无法发送心跳,那么服务端自然也要对这个实例进行处理,就在服务初始化的时候,会开启会实例的心跳检测任务,上面也有提到过
心跳检测处理
临时实例
永久实例
临时实例:采用客户端心跳检测模式,心跳检测周期5秒心跳间隔超过15秒(默认)则标记为不健康心跳间隔超过30秒(默认)则从服务列表删除永久实例:采用服务端主动健康检测方式周期为2000 + 5000毫秒内的随机数检测异常只会标记为不健康,不会删除
心跳机制
实例是如何得知其他实例的信息呢?毕竟需要远程调用嘛两种方式:1.客户端主动获取(定时更新)、2.服务端主动推送(长连接推送变更信息)
1.先是故障转移机制判断是否去本地文件中读取信息,读到则返回2.再去本地服务列表读取信息(本地缓存),没读到则创建一个空的服务,然后立刻去nacos中读取更新3.读到了就返回,同时开启定时更新,定时向服务端同步信息 (正常1s,异常最多60s一次)
服务端这边处理请求就比较简单了,除去参数获取以及相关校验就剩服务列表的获取了
客户端主动获取
既然是主动推送那么就需要两个条件:1.建立长连接2.触发推送的事件
服务变更就会触发事件让服务端主动推送服务变更信息
服务端主动推送
服务的发现有两种方式客户端主动获取:会先读取缓存,缓存内读取不到则会去服务端获取,同时开启一个定时任务定时更新定时任务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的变化,有变更会被监听并更新缓存地址列表。
文件寻址
地址服务器寻址模式是 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。
配置中心
Nacos
架构图
SpringBoot SPI 自动装配SentinelWebAutoConfiguration,然后将FilterRegistrationBean注入IOC,FilterRegistrationBean的作用就是为了注册CommonFilter,CommenFilter通过 filter 的方式将所有请求自动设置为 Sentinel 资源,从而达到限流目的。
以@SentinelResource作为切点
限流的判断逻辑是在 SphU.entry 方法中实现的
校验全局上下文Context
这里的 chain 会构造一个 Slot 链,启动每一个 Slot 都有各自的责任
通过lookProcessChain 方法获取一个 ProcessorSlot 链
执行 chain.entry ,如果没有被限流,返回 entry 对象 ,否则抛出 BlockException
CtSph 是 Sph的默认实现类
Sentinel
Seata的分布式事务解决方案是业务层面的解决方案,只依赖于单台数据库的事务能力。Seata框架中一个分布式事务包含3中角色:Transaction Coordinator ( TC ): 事务协调器,维护全局事务的运行状态,负责协调并驱动全局事务的提交或回滚。Transaction Manager( TM ): 控制全局事务的边界,负责开启一个全局事务,并最终发起全局提交或全局回滚的决议。Resource Manager ( RM ): 控制分支事务,负责分支注册、状态汇报,并接收事务协调器的指令,驱动分支(本地)事务的提交和回滚。其中,TM是一个分布式事务的发起者和终结者,TC负责维护分布式事务的运行状态,而RM则负责本地事务的运行。
TM 向 TC 申请开启一个全局事务,全局事务创建成功并生成一个全局唯一的 XID。XID 在微服务调用链路的上下文中传播。RM 向 TC 注册分支事务,接着执行这个分支事务并提交(重点:RM在第一阶段就已经执行了本地事务的提交/回滚),最后将执行结果汇报给TC。TM 根据 TC 中所有的分支事务的执行情况,发起全局提交或回滚决议。TC 调度 XID 下管辖的全部分支事务完成提交或回滚请求。
Seata能够在第一阶段直接提交事务,是因为Seata框架为每一个RM维护了一张UNDO_LOG表(这张表需要客户端自行创建),其中保存了每一次本地事务的回滚数据。因此,二阶段的回滚并不依赖于本地数据库事务的回滚,而是RM直接读取这张UNDO_LOG表,并将数据库中的数据更新为UNDO_LOG中存储的历史数据。这也是在使用seata作为分布式事务解决方案的时候,需要在参与分布式事务的每一个服务中加入UNDO_LOG表。如果第二阶段是提交命令,那么RM事实上并不会对数据进行提交(因为一阶段已经提交了),而实发起一个异步请求删除UNDO_LOG中关于本事务的记录。由于Seata一阶段直接提交了本地事务,因此会造成隔离性问题,因此Seata的默认隔离级别为Read Uncommitted。然而Seata也支持Read Committed的隔离级别,我们会在下文中介绍如何实现。
Seata在第一阶段就直接提交了分支事务
Seata
网关启动阶段
请求处理阶段
Spring Cloud Gateway
基于JDK动态代理生成代理类。根据接口类的注解声明规则,解析出底层MethodHandler基于RequestBean动态生成request。Encoder将bean包装成请求。拦截器负责对请求和返回进行装饰处理。日志记录。基于重试器发送http请求,支持不同的http框架,默认使用的是HttpUrlConnection。
Openfeign
Ribbon的调用过程非常简单,使用RestTemplate加上@LoadBalanced注解就可以开启客户端的负载均衡
在Ribbon中有个非常重要的组件LoadBalancerClient,它是负载均衡的一个客户端
ILoadBalancer是Ribbon中最重要的一个组件,它起到了承上启下的作用,既要连接 Eureka获取服务地址,又要调用IRule利用负载均衡算法选择服务。
Ribbon在选择之前需要获取服务列表,而Ribbon本身不具有服务发现的功能,所以需要借助Eureka来解决获取服务列表的问题。
用新的服务列表更新了旧服务列表,因此当执行IPing的线程再执行时,服务列表中只剩下了一个服务实例。综上可以发现,Ribbon为了解决服务列表的脏读现象,采用了两种手段:更新列表ping机制在测试中发现,更新机制和ping机制功能基本重合,并且在ping的时候不能执行更新,在更新的时候不能运行ping,所以很难检测到ping失败的情况。
Ribben
Spring Cloud LoadBalancer
gnats
xxl-job
Spring Security
Mycat
Canal
SkyWalking
Sentry
ELK
Jenkins
Git
Maven
Docker
K8s
技术栈
pig商业版白皮书
技术架构
奥里给
葵花宝典
0 条评论
下一页