问题集锦-副本

Spring-IOC

1、解析注册:使用Resource定位xml配置;使用BeanDefinitionReader读取配置,并封装成BeanDefinition;使用BeanDefinitionRegistry将BeanDefinition注册到BeanDefinitionMap中
2、BeanFatory中bean的加载过程
**转换对应的beanName:传入的参数可能不是bean的name,可能是别名、FactoryBean(&开头)
**如果是单例,尝试从缓存中获取单例bean,获取失败再尝试从singletonFactories中获取单例工厂,通过单例工厂去加载(在创建单例bean时为解决依赖注入,不等bean创建完成就将创建bean的工厂提早曝光并加入缓存中,其他单例bean创建时如果需要依赖该bean,直接从缓存中获取单例bean或获取工厂去创建)(调用工厂的getObject方法先获取实例化但还未初始化的单例bean,加入到earlySingletonObjects缓存中,将单例工厂从singletonFactories中移除,返回单例bean),单例bean的转换:获取的bean可能是原始状态(有可能获取的是Factorybean,需要调用FactoryBean中的getObject方法获取单例bean)
**如果缓存中没有,以下开始创建单例bean
**根据beanName尝试从beanDefinitionMap中获取对应的beanDefinition中的配置,如果获取不到配置,尝试递归根据parentBeanFactory去加载(调用父类工厂的getBean方法)
**前置处理:创建单例bean之前,记录单例bean正在创建状态,用于检测循环依赖
**通过单例工厂创建单例bean,调用单例工厂的getObject方法获取提早曝光的单例bean(实例化还未初始化):处理override属性:为了后面实例化单例bean时更好的处理,这里先预先判断一下是否需要覆盖或重载,后面处理的原理就是在实例化bean时如果检测到methodOverrides时,会动态地为当前bean生成代理并使用对应的拦截器为bean做增强处理;实例化单例bean前处理;短路处理:Spring AOP代理实现,如果需要使用代理bean且代理bean已经创建,直接返回;实例化单例bean(将BeanDefinition转换为BeanWrapper(对反射相关API的简单封装,使得上层使用反射完成相关的业务逻辑大大的简化),需要选择不同的实例化策略:如果有需要覆盖或动态替换的方法,需要使用cglib进行动态代理,因为可以在创建代理的同时将动态方法织入类中,否则可以直接用反射;构造函数注入循环依赖问题spring不能解决 循环依赖是在实例化后处理的);实例化bean后处理;如果需要解决循环依赖(满足3个条件:单例、允许循环依赖、当前bean正在创建),则提早曝光单例工厂(将单例工厂放入工厂缓存singletonFactories中,其他单例bean在创建时调用getObject方法可以获取未创建好的单例bean,getObject方法中实现Spring AOP 的advice动态织入;属性注入(填充)(递归初始化);激活aware方法(通过aware方法可以获取对应的资源:BeanNameAware 获取bean名称,BeanClassLoaderAware 获取bean的类加载器;BeanFactory 获取bean的工厂,即加载到IOC容器中);初始化单例bean前处理;激活用户自定义的init方法:如afterPropertiesSet方法、init-method,afterPropertiesSet先执行,init-method后执行;初始化单例bean后处理(spring AOP 在这里实现);检测循环依赖;注册destroy-method销毁方法);
**后置处理:创建单例bean之后,移除单例bean正在创建状态,用于检测循环依赖
**将单例bean放入单例缓存singletonObjects,从单例工厂缓存singletonFactories中移除单例工厂,从单例bean缓存earlySingletonObjects中移除单例bean,保存已注册的单例bean
**类型转换:将bean转换为需要的类型
3、ApplicationContext
**环境准备:如系统属性或环境变量的准备及验证
**加载BeanFactory,并读取配置文件:创建BeanFactory(DefaultListableBeanFactory);定制BeanFactory(在基本容器的基础上,增加了是否允许覆盖是否允许扩展的设置,并提供了对注解@Qualifier、@Autowired的支持);加载beanDefinition,读取配置文件(通过beanDefinitionReader加载beanDefinition(并注册到beanFactory的BeanDefinitionMap中));使用全局变量记录beanFactory
**对BeanFactory进行功能填充:如对@Qualifier和@Autowired注解的支持;增加AspectJ支持;增加属性注册编辑器(Spring DI 依赖注入时 Date类型是无法识别的)
**子类通过覆盖方法做额外处理
**激活(调用)BeanFactory处理器(容器级),可以有多个,通过排序依次处理;beanFactory处理器可以在实例化任何bean之前获得配置元数据并修改BeanDefinition(如${}替换);@ComponentScan就是在这里实现的;注册bean处理器(BeanFactory没有注册(因此不能使用),需要手动注册),在bean创建时调用
**注册拦截bean创建的bean处理器,这里只是注册,真正调用是在getBean方法中
**国际化处理
**初始化应用消息广播器
**子类覆盖方法处理
**在所有注册的bean中查找要监听的bean,注册到消息广播器中(注册监听器,并在合适的时候通知监听器)
**通过beanFactory加载bean(非延迟加载):ApplicationContext在启动时会加载所有的单例bean,调用getBean方法(上面Spring中BeanFactory加载bean的过程)
**完成刷新,通知生命周期管理器lifecycleProcessor刷新过程,并通过事件通知监听者


Spring-FactoryBean、BeanFactory、ObjectFactory

1、FactoryBean:
一般情况下,Spring通过反射机制来实例化bean,而这样可能需要很多配置,可以通过实现FactoryBean接口以编码的方式来代替
在IOC容器的基础上给Bean的实现加上了一个简单工厂模式和装饰模式,是一个可以生产对象和装饰对象的工厂bean
它是泛型的,只能固定生产某一类对象,而不像BeanFactory那样可以生产多种类型的Bean
当bean实现FactoryBean接口时,通过工厂的getBean方法返回的是FactoryBean中的getObject方法返回的实例,如果想要返回当前bean,需要以&开头
2、BeanFactory:对象工厂,用于实例化和保存对象
3、ObjectFactory:某个特定的工厂,用于在项目启动时,延迟实例化对象,解决循环依赖问题, 调用它的getObject方法时,才会触发 Bean 实例化


Spring单例下如何解决循环依赖(三级缓存)

Spring中循环依赖包括构造器依赖和setter注入依赖,Spring只能解决单例setter注入依赖(注入时会返回提前暴露的创建中的bean),构造器依赖无法解决(因为只有实例化之后才能曝光,实例化前曝光是有风险的),对于原型模式,循环依赖也是无法解决的(因为不使用缓存)
bean什么时候才会提早曝光:单例、创建中、允许循环依赖
1、尝试从singletionObjects中获取实例
2、尝试从earlySingletionObjects中获取实例
3、根据beanName尝试从singletonFactories中获取ObjectFactory,调用getObject方法创建bean(这里只是实例化了bean),放到earlySingletionObjects中,并从singletonFactories中移除ObjectFactory(互斥操作)这时已经可以通过容器的getBean方法获取到bean


Spring-AOP

1、静态代理、动态代理:静态代理直接调用目标类方法;动态代理通过反射调用目标类方法
2、JDK动态代理、CGLIB动态代理:JDK是在运行期间创建接口的实现类来完成对目标对象的代理;CGLIB采用了非常底层的字节码技术,其原理是通过字节码技术为一个类创建子类(生成代理类Class的二进制字节码;通过Class.forName加载二进制字节码,生成Class对象;通过反射机制获取代理类实例构造,并初始化代理类对象),并在子类中采用方法拦截的技术拦截所有父类方法的调用,顺势织入横切逻辑
3、连接点(方法执行处)、切入点(何处织入通知)、通知(处理时机及处理逻辑)、切面(包括切入点和通知)
4、源码:
**解析器解析AOP代理注解,生成BeanDefination,并注册到BeanDefinitionMap中
**创建自动代理创建器(用来实现AOP)(AnnotationAwareAspectJAutoProxyCreator)
**选择代理实现方式:默认如果目标对象有实现接口,则使用JDK动态代理;否则使用CGLIB代理(无法覆写final方法,可以通过proxy-target-class属性强制使用CGLIB代理);expose-proxy属性是为了解决有时候目标对象内部的自我调用无法实现切面增强的问题(强制暴露代理,在代码中可以获取这个代理进行显式调用)
**创建AOP动态代理:自动代理创建器实现了BeanPostProcessor接口,Spring在目标bean初始化完成之后会调用其postProcessAfterInitialization方法来创建AOP动态代理;获取增强,获取所有增强中目标bean可用的增强;创建代理工厂,根据配置设置JDK动态代理,或者CGLIB代理,将目标bean及其增强添加到代理工厂,通过代理工厂创建代理并返回(将增强组成拦截器链,执行目标方法时,执行拦截器链,中间会调用目标方法)


Spring事务失效的场景

1、注解@Transactional配置的方法非public权限修饰(可以开启 AspectJ 代理模式解决)
2、注解@Transactional所在类非Spring容器管理的bean
3、注解@Transactional所在类中,注解修饰的方法被类内部方法调用(无事务方法调用有事务方法,事务失效,使用代理对象调用解决,且要在启动类上加注解@EnableAspectJAutoProxy(exposeProxy = true)):Spring在扫描Bean的时候会自动为标注了@Transactional注解的类生成一个代理对象(proxy),当有注解的方法被调用的时候,实际上是代理对象调用的,代理对象在调用之前会开启事务,执行事务的操作,但是同类中的方法互相调用,相当于this.B(),此时的B方法并非是代理对象调用,而是直接通过原有的Bean直接调用,所以注解会失效)
4、业务代码抛出异常类型非RuntimeException,事务失效
5、业务代码中存在异常时,使用try…catch…语句块捕获,而catch语句块没有throw new RuntimeExecption异常(最难被排查到问题且容易忽略)
6、注解@Transactional中Propagation属性值设置错误即Propagation.NOT_SUPPORTED(一般不会设置此种传播机制)
7、mysql关系型数据库,且存储引擎是MyISAM而非InnoDB,则事务会不起作用(基本开发中不会遇到)


@Transactional原理

@Transactional是基于Spring AOP的,以@Transactional注解为连接点,@Transactional注解的切面逻辑类似于@Around


Spring Boot-启动原理

@SpringBootApplication注解:
1、@SpringBootConfiguration注解:继承Configuration,表示启动类是IOC容器的配置类
2、@EnableAutoConfiguration注解:通过@AutoConfigurationPackage注解获取自动配置包,返回当前主类的同级以及子级的中断自动配置组件;开启springboot的配置功能,借助@Import({EnableAutoConfigurationImportSelector.class})实现,将自动配置组件对应的bean定义都加载到IoC容器中,通过Spring的SpringFactoriesLoader(Spring工厂加载器)去读取META-INF/spring.factories中的配置类信息,通过反射生成一个配置类(里面有许多bean定义),最后将这些bean定义加载到IOC容器中(但是不是所有存在于spring.factories中的配置都进行加载,而是通过@ConditionalOnClass注解进行判断条件是否成立(只要导入相应的stater,条件就能成立),如果条件成立则加载配置类,否则不加载该配置类)
https://www.cnblogs.com/xiaopotian/p/11052917.html
3、@ComponentScan注解:自动扫描并加载符合条件的组件(如@Component和@Repository等)或者bean定义,最终将这些bean加载到IoC容器中,在beanFactoryProcessor中调用
run方法:
1、创建监听器
2、加载配置环境
3、创建ConfigurableApplicationContext
4、spring.factories加载,bean实例化

image.png


Spring Cloud

分布式事务

CAP:一致性、可用性、容错性
BASE:可用性、容错性、最终一致性(可能会存在中间状态如:处理中)
1、两阶段提交(2PC)(基于数据库,MySQL和Oracle支持):准备阶段(资源锁定,执行本地事务,并写日志undo(修改后数据)/redo日志(修改前数据))、提交/回滚阶段,中间由事务管理器控制全局事务,资源锁需要等到两个阶段结束才释放,性能较差,会出现死锁问题
2、seata改进2PC:事务管理器(事务发起服务引入,负责发起全局事务,发起全局提交或全局回滚的指令)、事务协调器(单独的服务,控制,维护全局事务状态,协调各分支事务提交/回滚)、资源管理器(控制每个分支事务,使用DataSourceProxy连接数据库,使用ConnectionProxy操作数据库,目的就是在第一阶段执行本地事务的同时,写入undo_log表(保存修改前和修改后的数据),因此第一阶段就能进行事务提交,并释放资源;第二阶段提交时只需要删除undo_log表数据,回滚时反向执行即可)
3、TCC:预处理Try(业务检查(一致性)及资源预留(隔离)执行)、确认 Confirm(确认提交)、撤销Cancel(回滚);如处理表(中间有状态、流水号)、被调用方保持幂等、并提供查询接口/回调
4、可靠消息最终一致性:本地消息表+消息中间件(通过本地事务保证数据业务操作和消息的一致性,然后通过定时任务将消息发送至消息中间件,待确认消息发送给消费方成功再将消息删除)
要解决以下问题:
**本地事务与消息发送的原子性问题
**事务参与方接收消息的可靠性(一定能够接收到消息)
**消息重复消费的问题(幂等性)
MQ的ack(即消息确认)机制,消费者监听MQ,如果消费者接收到消息并且业务处理完成后向MQ 发送ack(即消息确认),此时说明消费者正常消费消息完成,MQ将不再向消费者推送消息,否则消费者会不断重试向消费者来发送消息
5、最大努力通知


zookeeper

zookeeper是一个典型的分布式数据一致性解决方案
节点类型:持久节点、持久顺序节点、临时节点(客户端会话,非TCP连接,只能作为叶子节点)、临时顺序节点
集群:Leader、Follower、Observer(不参与Leader选举、也不参与写操作的“过半写成功”策略)
Leader选举:

分布式锁

Redis分布式锁:
1、加锁:set 命令要用 set key value px milliseconds nx,替代 setnx + expire 需要分两次执行命令的方式,保证了原子性;给锁加上一个过期时间,即使Redis客户端中间出现异常(来不及调用lua脚本释放锁)也可以保证过期时锁会自动释放(但是如果Redis服务端异常就没办法解决);超时问题解决:lua脚本+额外线程进行锁延时
2、解锁:将lua脚本传到jedis.eval()方法里,并使参数KEYS[1]赋值为lockKey(锁标志),ARGV[1]赋值为requestId(Redis客户端标志);在执行的时候,首先会获取锁对应的value值,检查是否与requestId相等,如果相等则解锁(删除key);比较requestId是为了解决超时问题:如果在加锁和释放锁之间的逻辑执行的太长,以至于超出了锁的超时限制,就会出现问题,因为这时候锁过期了,第二个线程重新持有了这把锁,但是紧接着第一个线程执行完了业务逻辑,就把锁给释放了,第三个线程就会在第二个线程逻辑执行完之前拿到了锁
3、可重入性:对客户端的 set 方法进行包装,使用线程的 Threadlocal 变量存储当前线程持有锁的计数,还需要考虑内存锁计数的过期时间
问题:如果存储锁对应key的那个节点挂了的话,就可能存在丢失锁的风险,导致出现多个客户端持有锁的情况,这样就不能实现资源的独占了(即Redis服务端出现问题),即使是Redis主从也不能解决问题(Redis的主从同步通常是异步的)
解决:
1、Redlock算法:轮流尝试在每个节点上创建锁,过期时间较短,一般就几十毫秒,至少要在大多数节点上成功创建锁,才说明获取到锁,客户端计算创建锁的时间,如果创建锁的时间小于超时时间,就是创建成功了;如果创建锁失败了,那么就依次删除以前创建过的锁;如果其他客户端已经创建锁,就得不断轮询去尝试获取锁
2、Redisson:RedissonLock 同样没有解决节点挂掉的时候,存在丢失锁的风险的问题;Redisson 提供了实现了redlock算法的 RedissonRedLock,RedissonRedLock 真正解决了单点失败的问题,代价是需要额外的为 RedissonRedLock 搭建Redis环境
zookeeper分布式锁:
获取锁:客户端获取锁时,调用create方法创建临时顺序节点(Zookeeper会保证所有的客户端中,最终只有一个客户端能够创建成功,没有获取到锁的客户端需要创建一个节点去Watch监听锁节点)
释放锁:当前获取锁的客户端宕机/业务逻辑执行完都会移除临时顺序锁节点,并通知所有Watch节点去重新尝试获取锁

Redis-基础数据结构

1、String:类似于ArrayList,字节数组,用途:缓存用户信息(序列化和反序列化)
2、List:类似于LinkedList,链表+压缩列表(数据量少时,只用压缩列表),增删快,查询慢,用途:异步队列
3、Hash:类似于HashMap,数组+链表,渐进式reHash(中间新旧数据都会读),用途:缓存用户信息
4、Set:类似于HashSet,value值为空,用途:去重
5、ZSet: 类似于SortedSet 和 HashMap 的结合体,跳跃列表,既要随机增删,又要排序,用途:核心企业/供应商列表
6、跳跃列表:每一层是一个单向链表,每一层有一个额外的节点去定位每一层的头节点


image.png

Redis-缓存一致性

1、先删除缓存,再更新数据库(缓存设置过期时间)
问题:如果两个并发操作,一个读操作,一个写操作,写操作删除缓存,读操作从缓存读取数据失败,从数据库读取数据成功,然后更新缓存,写操作更新数据库,无法避免这种情况的缓存一致性
解决:延迟双删:写操作更新数据库成功后,sleep(睡眠时间不好控制)一段时间,再删除一次缓存
2、先更新数据库,再删除缓存(缓存设置过期时间)(推荐)
原理:更新数据库时,会加锁,其他操作不能操作这条数据
问题:写操作时,更新数据库成功,删除缓存失败,读操作仍读取缓存中的旧数据
解决:
**消息队列:更新数据库,插入本地消息表,删除缓存,消息队列重试去删除缓存(需要考虑消息队列的一些常见问题)
**消息队列+binlog日志:更新数据库时,会插入binlog日志,通过canal读取binlog日志,推送给消息队列,消息队列重试去删除缓存(与业务代码解耦)
3、为什么是删除缓存,而不是更新缓存
**问题1:如果两个并发操作,一个写操作a,一个写操作b,a更新数据库,释放锁,b更新数据库,释放锁,b更新缓存完成,a更新缓存完成,无法避免这种情况的缓存一致性
**问题2:每次都更新缓存,会导致性能消耗

Redis-缓存穿透、缓存雪崩、缓存击穿

缓存穿透
定义:查找一定不存在的数据(如恶意攻击),在缓存中根据key查不到,在数据库中也查询不到,所以不会存到缓存中,每次都要查询数据库
解决:
1、布隆过滤:将一切可能查询的key存到map中,请求过来时先去map中查找,不存在直接丢弃
2、即使在数据库中查询不到,空值也存到缓存中,设置过期时间(浪费空间,且在有效期内可能数据不一致)
缓存雪崩
定义:缓存中大量的数据同时失效(如服务挂掉和同时过期),直接去数据库中查询
解决:
1、过期时间设置均匀(避免同时过期)(事前)
2、缓存服务高可用(主从+哨兵)(事前)
3、限流:缓存失效时,通过加锁或者队列来控制读数据库写缓存的线程数量(如对某个key只允许一个线程查询数据和写缓存,其他线程等待),避免数据库崩掉(事中)
4、Redis 持久化:一旦重启,自动从磁盘上加载数据,快速恢复缓存数据(事后)
缓存击穿
定义:大量请求同时访问一个或多个热点key,key如果瞬间失效,会直接请求数据库,导致数据库崩掉
解决:
1、若缓存的数据是基本不会发生更新的,则可尝试将该热点数据设置为永不过期
2、若缓存的数据更新不频繁,且缓存刷新的整个流程耗时较少的情况下,则可以采用基于 Redis、zookeeper 等分布式中间件的分布式互斥锁,或者本地互斥锁以保证仅少量的请求能请求数据库并重新构建缓存,其余线程则在锁释放后能访问到新缓存
3、若缓存的数据更新频繁或者缓存刷新的流程耗时较长的情况下,可以利用定时任务在缓存过期前主动的重新构建缓存或者延后缓存的过期时间,以保证所有的请求能一直访问到对应的缓存

Redis-持久化

1、RDB全量快照(数据)直接复制,快
需要解决的问题:不能影响服务响应,如何保证边持久化边响应
解决:使用Copy On Write机制来实现快照持久化
原理:Redis 在持久化时会产生一个子进程,子进程刚刚产生时,和父进程共享内存里面的代码段和数据段,这是 Linux 操作系统的机制,在进程分离的一瞬间,内存的增长几乎没有明显变化;子进程只做数据持久化,不会修改现有的内存数据结构,只是对数据结构进行遍历读取,然后序列化写到磁盘中;当父进程对其中一个页面的数据进行修改时,会将被共享的页面复制一份分离出来,然后对这个复制的页面进行修改,这时子进程相应的页面是没有变化的,还是进程产生时那一瞬间的数据
2、AOF增量日志(指令)需要指令重放,慢,一般是先存到磁盘,然后再执行指令
需要解决的问题:文件会越来越大,需要定期进行AOF重写;服务宕机,数据丢失的问题
原理:创建子进程对内存遍历转换成指令,序列化到一个新的AOF日志文件中;序列化完毕后再将操作期间发生的增量AOF日志追加到这个新的AOF日志文件中,追加完毕后就立即替代旧的AOF日志文件,瘦身工作就完成了;AOF日志在内存缓存中,需要异步将数据刷回到磁盘,如果机器突然宕机,AOF日志内容可能还没有来得及完全刷到磁盘中,这个时候就会出现日志丢失,因此需要每隔 1s(可配置)左右执行一次 fsync 操作(强制刷新到缓存)
3、通常 Redis 的主节点不会进行持久化操作,持久化操作主要在从节点进行,从节点是备份节点,没有来自客户端请求的压力
4、混合持久化:重启 Redis 时,很少使用 RDB 来恢复内存状态,因为会丢失大量数据(重启期间的数据没有保存),通常使用 AOF 日志重放(重启期间的数据会追加),但是重放 AOF 日志性能相对 RDB 来说要慢很多,这样在 Redis 实例很大的情况下,启动需要花费很长的时间;可以将RDB的内容和增量的AOF存放在一起,这里的AOF不再是全量的日志,而是自持久化开始到持久化结束的这段时间发生的增量 AOF日志,通常这部分AOF日志很小,于是在Redis重启的时候,开启AOF,先加载RDB的内容,然后再重放增量AOF日志就可以完全替代之前的AOF全量文件重放,重启效率因此大幅得到提升

Redis-过期策略

Redis是单线的,删除也会占用线程的时间,Redis采用的是定期删除 + 懒惰删除策略(定期删除是集中处理,惰性删除是零散处理)
定期删除策略
Redis单线程默认会每秒进行十次过期扫描,过期扫描不会遍历过期字典中所有的 key,而是采用了一种简单的贪心策略
1、从过期字典中随机 20 个 key
2、删除这 20 个 key 中已经过期的 key
3、如果过期的 key 比率超过 1/4,那就重复步骤 1
4、同时,为了保证过期扫描不会出现循环过度,导致线程卡死现象,算法还增加了扫描时间的上限,默认不会超过 25ms
为什么不扫描所有的过期key?
1、会导致线上读写请求出现明显的卡顿现象(所以要尽量避免大量key同时过期)
2、即使扫描有 25ms 的时间上限:假如有 101 个客户端同时将请求发过来了,然后前 100 个请求的执行时间都是25ms,那么第 101 个指令需要等待多久才能执行?2500ms(因为单线程),这个就是客户端的卡顿时间
从库的过期策略
1、从库不会进行过期扫描,从库对过期的处理是被动的,主库在 key 到期时,会在 AOF 文件里增加一条 del 指令,同步到所有的从库,从库通过执行这条 del 指令来删除过期的key
2、因为指令同步是异步进行的,所以主库过期的 key 的 del 指令没有及时同步到从库的话,会出现主从数据的不一致,主库没有的数据在从库里还存在
懒惰删除策略
为什么要懒惰删除?
1、删除指令 del 会直接释放对象的内存,大部分情况下,这个指令非常快,没有明显延迟,不过如果删除的 key 是一个非常大的对象,比如一个包含了千万元素的 hash,那么删除操作就会导致单线程卡顿
2、Redis 内部实际上并不是只有一个主线程,它还有几个异步线程专门用来处理一些耗时的操作,可以用异步线程实现懒惰删除

Redis-内存淘汰机制

1、noeviction:当内存超出最大内存,写入请求会报错,但是删除和读请求可以继续(一般不使用,但是是默认的)
2、allkeys-lru:当内存超出最大内存,在所有的key中,移除最少使用的key,只把Redis当作缓存时使用(推荐)
3、allkeys-random:当内存超出最大内存,在所有的key中,随机移除某个key(一般不使用)
4、volatile-lru:当内存超出最大内存,在设置了过期时间key的字典中,移除最少使用的key(不会移除没有设置过期时间的),把Redis既当缓存,又做持久化的时候使用
5、volatile-random:当内存超出最大内存,在设置了过期时间key的字典中,随机移除某个key(不会移除没有设置过期时间的)
6、volatile-ttl:当内存超出最大内存,在设置了过期时间key的字典中,优先移除剩余时间ttl 最少的(不会移除没有设置过期时间的)
LRU算法:
1、实现 LRU 算法除了需要 key/value 字典外,还需要附加一个链表,链表中的元素按照一定的顺序进行排列
2、当空间满的时候,会踢掉链表尾部的元素
3、当字典的某个元素被访问时,它在链表中的位置会被移动到表头,所以链表的元素排列顺序就是元素最近被访问的时间顺序
4、位于链表尾部的元素就是不被重用的元素,所以会被踢掉;位于表头的元素就是最近刚刚被人用过的元素,所以暂时不会被踢(双向链表)
近似 LRU 算法
1、Redis 使用的是一种近似 LRU 算法,之所以不使用 LRU 算法,是因为需要消耗大量的额外的内存,需要对现有的数据结构进行较大的改造
2、近似LRU 算法则很简单,在现有数据结构的基础上使用随机采样法+额外字段(最后一次被访问的时间戳)来淘汰元素,能达到和 LRU 算法非常近似的效果
LFU算法
1、Redis 4.0 里引入了一个新的淘汰策略 —— LFU(最近最少使用)算法
2、LFU 表示按最近的访问频率进行淘汰,它比 LRU 更加精准地表示了一个 key 被访问的热度
3、如果一个 key 长时间不被访问,只是刚刚偶然被用户访问了一下,那么在使用 LRU 算法下它是不容易被淘汰的,因为 LRU 算法认为当前这个 key 是很热的
4、而 LFU 是需要追踪最近一段时间的访问频率,如果某个 key 只是偶然被访问一次是不足以变得很热的,它需要在近期一段时间内被访问很多次才有机会被认为很热

Redis-哨兵

1、负责持续监控主从节点的健康,当主节点挂掉时,自动选择一个最优的从节点切换为主节点
2、客户端来连接主从时,会首先连接哨兵,通过哨兵来查询主节点的地址,然后再去连接主节点进行数据交互
3、当主节点发生故障时,客户端会重新向哨兵获取主节点地址,哨兵会将最新的主节点地址告诉客户端,无需重启即可自动完成节点切换
4、主节点挂掉了,原先的主从复制也断开了,客户端和损坏的主节点也断开了,从节点被提升为新的主节点,其它从节点开始和新的主节点建立复制关系,客户端通过新的主节点继续进行交互
5、哨兵会持续监控已经挂掉了主节点,待它恢复后,集群会进行调整,原先挂掉的主节点现在变成了从节点,从现在的主节点那里建立复制关系
6、哨兵进行主从切换时,客户端如何知道地址变更了 ? 在建立连接的时候进行了主节点地址变更判断,查询主节点地址,然后跟内存中的主节点地址进行比对,如果变更了,就断开所有连接,重新使用新地址建立新连接;如果是旧的主节点挂掉了,那么所有正在使用的连接都会被关闭,然后在重连时就会用上新地址

Redis-应用

1、Scan:扫描海量数据(有游标)
2、HyperLogLog:统计UV
3、布隆过滤器:推荐去重(布隆过滤器能准确过滤掉那些已经看过的内容,那些没有看过的新内容,它也会过滤掉极小一部分 (误判)),当布隆过滤器说某个值存在时,这个值可能不存在;当它说不存在时,那就肯定不存在
原理:对key多次无偏hash,每个hash取模数组长度,确定位置,将该位置置为1;查询是否存在时,再次对key多次无偏hash,取模,确定位置是否为1;位置可以重复利用,因此会有误差

HashMap

MySQL-索引

B+树索引的优点:
1、索引按照顺序存储数据,可以用来做ORDER BY和GROUP BY操作
2、索引中存储了实际的索引列值,所以某些査询只使用索引就能够完成全部査询(非一级索引的叶子节点存储主键)
3、索引大大减少了服务器需要扫描的数据量
4、索引可以帮助服务器避免排序和临时表
5、索引可以将随机I/O变为顺序I/O
B+树索引的缺点:
1、如果不是按照索引的最左列开始査找,则无法使用索引
2、不能跳过索引中的列(只能使用跳过前的列)
3、如果查询中有某个列的范围査询,则其右边所有列都无法使用索引优化査找(但是可以作为值返回);如果范围査询列值的数量有限,那么可以使用多个等于条件来代替范围条件
索引策略
1、独立的列:索引列不能是表达式的一部分,也不能是函数的参数,因此要始终将索引列单独放在比较符号的一侧
2、前缀索引和索引选择性:前缀越长,选择性越好
3、聚合(多列)索引:
当出现服务器对多个索引做相交操作时(通常有多个AND条件)或对多个索引做联合操作时(通常有多个OR条件),通常意味着需要一个包含所有相关列的多列索引,而不是多个独立的单列索引(需要合并)
多列索引中索引的顺序(需要兼顾排序和分组):当不需要考虑排序和分组时,将选择性最高的列放在前面通常是很好的
4、聚簇索引:当表有聚簇索引时,它的数据行实际上存放在索引的叶子页中,因为无法同时把数据行存放在两个不同的地方,所以一个表只能有一个聚簇索引(覆盖索引可以模拟多个聚簇索引的情况)
5、二级索引:访问需要两次索引査找,而不是一次,因为二级索引叶子节点保存的不是指向行的物理位置的指针,而是行的主键值,这意味着通过二级索引查找行,存储引擎需要找到二级索引的叶子节点获得对应的主键值,然后根据这个值去聚簇索引中査找到对应的行,这里做了重复的工作:两次B+树査找而不是一次(回表查询)
6、普通索引和唯一索引:普通索引在查找到一条记录后会继续查找,而唯一索引会终止查找
使用索引排序
1、只有当索引的列顺序和ORDER BY子句的顺序完全一致,并且所有列的排序方向(倒序或正序)都一样时,MySQL才能够使用索引来对结果做排序
2、如果査询需要关联多张表,则只有当ORDER BY子句引用的字段全部为第一个表时,才能使用索引做排序
3、ORDER BY子句和查找型查询的限制是一样的:需要满足索引的最左前缀的要求;否则MySQL都需要执行排序操作,而无法利用索引排序
有一种情况下ORDER BY子句可以不满足索引的最左前缀的要求,就是前导列为常量的时候;即如果WHERE子句或者JOIN子句中对这些列指定了常量,就可以“弥补”索引的不足
B树与B+树的区别
1、B树可能在非叶子节点命中返回;B+不可能在非叶子结点命中
2、B+树叶子节点存放所有数据;B+树叶子节点之间又是一个链表

MySQL-事务

事务的特性
1、原子性:一个事务必须被视为一个不可分割的最小工作单元,整个事务中的所有操作要么全部提交成功,要么全部失败回滚
2、一致性:数据库总是从一个一致性的状态转换到另外一个一致性的状态
3、隔离性:通常来说(涉及到隔离级别),一个事务所做的修改在最终提交以前,对其他事务是不可见的
4、持久性:通常来说(涉及到持久级别),一旦事务提交,则其所做的修改就会永久保存到数据库中,此时即使系统崩溃,修改的数据也不会丢失
事务的隔离级别
1、读未提交:一个事务可以读取到其他事务未提交的数据(脏读)
2、读已提交(不可重复读):一个事务只能读取到其他事务此刻已提交的数据
3、可重复读:一个事务只能读取到其他事务在该事务开启时已提交的数据(快照读),但是无法避免幻读(单指插入,两次读取的结果不一致)
可重复读是MySQL的默认事务隔离级别,InnoDB通过MVCC(多版本并发控制)和next-key lock解决了幻读
4、串行读:强制事务串行执行,解决了幻读问题,在读取的每一行数据上都加锁,所以可能导致大量的超时和锁争用的问题
幻读:
1、幻读指的是一个事务在前后两次查询同一个范围的时候,后一次查询看到了前一次查询没有看到的行(单指插入);在可重复读隔离级别下,普通的查询是快照读,是不会看到别的事务插入的数据的(MVCC),幻读在“当前读”下仍会出现(加锁读时,只锁当前满足条件的行)
2、通过加间隙锁解决当前读导致的幻读,跟间隙锁存在冲突关系的,是跟 “往这个间隙中插入一个记录 ”这个操作,间隙锁之间都不存在冲突关系;间隙锁和行锁合称next-key lock,每个next-key lock是前开后闭区间
3、间隙锁是在可重复读隔离级别下才会生效的
4、一个并发问题:任意锁住一行,如果这一行不存在的话就插入,如果存在这一行就更新它的数据;因为锁的是间隙锁,并发时会锁竞争,发生死锁
MVCC-多版本并发控制(可重复读下)
查询(同时满足下面条件的,才作为结果返回):
a、査找版本早于当前事务版本的数据行(也就是行的系统版本号小于或等于事务的系统版本号),这样可以确保事务读取的行,要么是在事务开始前已经存在的,要么是事务自身插入或者修改过的
b、行的删除版本要么未定义,要么大于当前事务版本号,这可以确保事务读取到的行,在事务开始之前未被删除
插入:为新插入的每一行保存当前事务版本号作为行版本号
删除:为删除的每一行保存当前事务版本号作为行删除标识
修改:插入一行新记录,保存当前事务版本号作为行版本号,同时保存当前事务版本号到原来的行作为行删除标识
优点:保存这两个额外系统版本号,使大多数读操作都可以不用加锁,这样设计使得读数据操作很简单,性能很好,并且也能保证只会读取到符合标准的行
缺点:每行记录都需要额外的存储空间,需要做更多的行检査工作,以及一些额外的维护工作
MySQL没有完全解决幻读问题
如:事务a先查询(MVCC),事务b插入(next-key),事务a更新(next-key,会加版本号),事务a查询(MVCC),两次查询的结果不同
意向锁(表级锁):意向锁是由数据库自己维护的,一般来说,给一行数据加上共享锁之前,数据库会自动在这张表上面加一个意向共享锁(IS锁);给一行数据加上排他锁之前,数据库会自动在这张表上面加一个意向排他锁(IX锁)
意向锁可以认为是共享锁和互斥锁在数据表上的标识,通过意向锁可以快速判断表中是否有记录被上锁,从而避免通过遍历的方式来查看表中有没有记录被上锁,提升加锁效率
如要加表级别的互斥锁,这时候数据表里面如果存在行级别的互斥锁或者共享锁的,加锁就会失败,此时直接根据意向锁就能知道这张表是否有行级别的X锁或者S锁

MySQL-查询性能优化

从下面几点进行优化:
1、减少扫描行数(索引)
2、减少返回的行数或列数(limit或避免*)
如:
1、证件号码、证件名称、证件类型(聚合索引,冗余索引,顺序问题,减少回表)
2、select a.id,a.name from user a inner join (select b.id from user order by a.userId limit 1000,10) b on a.id = b.id
(原始的写法:select a.id,a.name from user a order by a.userId limit 1000,10)
(使用一级索引,减少回表)
join(小表驱动大表,大表用索引)
1、在可以使用被驱动表的索引(join字段)情况下,使用join语句,性能比强行拆成多个单表执行SQL语句的性能要好;如果使用join语句的话,需要让小表(根据条件查询出来少的表)做驱动表
2、在判断要不要使用join语句时,就是看explain结果里面,Extra字段里面有没有出现“Block Nested Loop”字样(出现则不用join)
3、如果用left join的话,左边的表一定是驱动表吗?不是
4、如果两个表的join包含多个条件的等值匹配,是都要写到on里面呢,还是只把一个条件写到on里面,其他条件写到where部分?写到on里面
5、在MySQL里,NULL跟任何值执行等值判断和不等值判断的结果,都是NULL;select NULL =NULL 的结果,也是返回 NULL

MySQL-执行流程

MySQL执行一个査询的过程:
1、客户端发送一条査询给服务器
2、服务器先检査査询缓存,如果命中了缓存,则立刻返回存储在缓存中的结果,否则进入下一阶段
3、服务器端进行SQL解析、预处理,再由优化器生成对应的执行计划
4、MySQL根据优化器生成的执行计划,调用存储引擎的API来执行査询
5、将结果返回给客户端
更新:先找到要更新的数据,从磁盘读入内存;在执行器中执行语句,调用引擎先把记录写到redo log(覆盖写,磁盘中,物理日志)里面(原先的记录会写到undo log中,用于回滚),并更新内存,此时还未提交事务,在适当的时候,再将记录更新到磁盘;执行器写到binlog(不覆盖写,磁盘中,逻辑日志(语句));引擎将redo log改成提交状态,更新完成,即两阶段提交

ThreadLocal

1、一个线程对应多个ThreadLocal,但只有一个ThreadLocalMap(在当前线程内)
2、多个线程可以使用同一个ThreadLocal,但是是隔离的
3、ThreadLocalMap是ThreadLocal的内部类
4、ThreadLocalMap的key为ThreadLocal(弱引用),value为存储的值
5、内存泄漏:当ThreadLocal被回收时,ThreadLocalMap中就可能出现key为null的Entry,没有任何办法访问这些key为null的Entry的value,如果当前线程再迟迟不结束的话,这些key为null的Entry的value就会一直存在一条强引用链:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value永远无法回收(没有调用remove),造成内存泄漏
6、为什么key使用弱引用:如果key使用强引用,如果当前线程再迟迟不结束的话(如线程池中复用线程),可能会出现整个Entry对象都不会被回收,也会出现内存泄漏问题(更难解决);如果key使用弱引用,即使没有手动删除,key也会被回收,但是会出现value不会被回收
7、内存泄漏的根源是:由于ThreadLocalMap的生命周期跟Thread一样长,如果没有手动删除对应key就会导致内存泄漏,而不是因为弱引用
8、内存泄漏解决:使用static修饰ThreadLocal引用(这样保证ThreadLocal始终保持被引用,不会被回收),但是最后还是要调用remove方法(ThreadLocal不会被回收,value会回收)


你可能感兴趣的:(问题集锦-副本)