问题-2021-09-21

xxl-job如何保证定时任务只执行一次

1、业务逻辑代码和定时任务逻辑完全分开部署
2、调度框架集群(rehash将不同任务注册到不同节点)
3、使用分布式锁解决本次定时任务未执行完,下一次定时任务开始执行的问题

Redis扩容机制

Redis常用方法

ArrayList扩容机制

构造时,容量为0,添加元素时才分配容量10;超过容量时,扩容1.5倍;System.arraycopy方法做一个整体的复制(排在这次添加元素后面的所有元素)

分布式事务-本地消息表+消息队列

a服务:落业务表、本地消息表(事件发布表,状态TODO)
a服务:定时任务(将事件发送到消息队列,确认收到(防止消息队列数据丢失),更新事件发布表状态PUBLISHED)
消息队列将消息推送到b服务的事件订阅表(重复则失败,幂等,状态TODO),消费者确认后,消息队列将消息删除
b服务:定时任务处理业务,处理成功,更新事件订阅表状态SUCC
注:MQ的Leader保存数据,follower未保存,然后Leader出现问题,导致消息丢失(集群、持久化)

本地服务调用其他服务日志问题

将本地服务注册到eurkea上,链路追踪,日志拦截包装(给上层服务)

TCC

杜绝a服务-b服务-c服务-d服务
清分业务拆分:转账(核心企业账户转银行内部户)、还款(内部户还款给银行/供应商)、核销(核心企业授信额度退还)
清分总控表
查询
转账定时任务:查询TODO的,转账,如果请求超时/报错/返回超时-TOCK,成功- SUCC
转账同步定时任务:查询转账记录,请求超时/返回超时-状态不变,空-FAIL,有数据-SUCC
转账异常查询-重试
无论是强一致性还是最终一致性,都不能保证100%的一致性,日终对账:当天内部户入账余额+昨天内部户入账余额是否等于查询到的余额

代理与装饰

装饰器模式关注于在一个对象上动态的添加方法,然而代理模式关注于控制对对象的访问
代理 非业务校验拦截(减少冗余代码,不必关心这些校验逻辑,缓存一些信息):api切换、日志、业务参考号、项目信息、客户信息、客户号校验 业务修饰(拓展自己,增强自身)
装饰 定制化 业务校验 主体逻辑相同 额外的装饰

Spring-IOC

1、解析注册:使用Resource定位xml配置;使用BeanDefinitionReader读取配置,并封装成BeanDefinition;使用BeanDefinitionRegistry将BeanDefinition注册到BeanDefinitionMap中
2、BeanFatory中单例bean的加载过程
a、转换对应的beanName:传入的参数可能不是bean的name,可能是别名、FactoryBean(&开头)
b、尝试从缓存(两级缓存)中获取单例bean
c、获取失败再尝试从singletonFactories中获取ObjectFatory,通过ObjectFatory去加载:

1、在创建单例bean时为解决依赖注入,不等bean创建完成就将创建bean的工厂ObjectFatory提早曝光并加入缓存中,其他单例bean创建时如果需要依赖该bean,直接从缓存中获取单例bean或获取ObjectFatory去创建
2、调用ObjectFatory的getObject方法先获取实例化但还未初始化的单例bean,加入到earlySingletonObjects缓存中,将单例工厂从singletonFactories中移除,返回单例bean(已经可以通过getBean方法获取到)
3、单例bean的转换:获取的bean可能是原始状态(有可能获取的是Factorybean,需要调用FactoryBean中的getObject方法获取单例bean)

d、如果缓存中没有,以下从头开始创建单例bean
e、根据beanName尝试从beanDefinitionMap中获取对应的beanDefinition中的配置,如果获取不到配置,尝试递归根据parentBeanFactory去加载(调用父类工厂的getBean方法)
f、前置处理:创建单例bean之前,记录单例bean正在创建状态,用于检测循环依赖
g、通过ObjectFatory创建单例bean,调用ObjectFatory的getObject方法获取提早曝光的单例bean(实例化还未初始化):

1、处理override属性:为了后面实例化单例bean时更好的处理,这里先预先判断一下是否需要覆盖或重载,后面处理的原理就是在实例化单例bean时如果检测到methodOverrides时,会动态地为当前bean生成代理并使用对应的拦截器为bean做增强处理
2、实例化单例bean前置处理,短路处理:Spring AOP代理相关,如果需要使用代理bean且代理bean已经创建,直接返回
3、实例化单例bean
a、将BeanDefinition转换为BeanWrapper(对反射相关API的简单封装,使得上层使用反射完成相关的业务逻辑大大的简化
b、需要选择不同的实例化策略:如果有需要覆盖或动态替换的方法,需要使用cglib进行动态代理,因为可以在创建代理的同时将动态方法织入类中,否则可以直接用反射
4、实例化bean后置处理
5、如果需要解决循环依赖(满足3个条件:单例、允许循环依赖、当前bean正在创建),则提早曝光单例工厂(将ObjectFactory放入工厂缓存singletonFactories中,其他单例bean在创建时调用getObject方法可以获取未创建好的单例bean,getObject方法中实现Spring AOP 的advice动态织入)
(构造函数注入循环依赖问题spring不能解决 循环依赖是在实例化后处理的)
6、属性注入(填充)(递归初始化)
7、激活aware方法(通过aware方法可以获取对应的资源:BeanNameAware 获取bean名称,BeanClassLoaderAware 获取bean的类加载器;BeanFactory 获取bean的工厂,即加载到IOC容器中)
8、初始化单例bean前置处理
9、激活用户自定义的init方法:如afterPropertiesSet方法、init-method,afterPropertiesSet先执行,init-method后执行
10、初始化单例bean后置处理(spring AOP 在这里实现:拦截器生成代理对象)
11、检测循环依赖
12、注册destroy-method销毁方法

h、后置处理:创建单例bean之后,移除单例bean正在创建状态,用于检测循环依赖
i、将单例bean放入单例缓存singletonObjects,从单例工厂缓存singletonFactories中移除单例工厂,从单例bean缓存earlySingletonObjects中移除单例bean,保存已注册的单例bean
j、类型转换:将bean转换为需要的类型
3、ApplicationContext
a、环境准备:如系统属性或环境变量的准备及验证
b、加载BeanFactory,并读取配置文件:

创建BeanFactory(DefaultListableBeanFactory)
定制BeanFactory(在基本容器的基础上,增加了是否允许覆盖是否允许扩展的设置,并提供了对注解@Qualifier、@Autowired的支持)
加载beanDefinition,读取配置文件(通过beanDefinitionReader加载beanDefinition(并注册到beanFactory的BeanDefinitionMap中))
使用全局变量记录beanFactory

c、对BeanFactory进行功能填充:

如对@Qualifier和@Autowired注解的支持
增加AspectJ支持
增加属性注册编辑器(Spring DI 依赖注入时 Date类型是无法识别的)

d、子类通过覆盖方法做额外处理
e、激活(调用)BeanFactory处理器(容器级),可以有多个,通过排序依次处理:

beanFactory处理器可以在实例化任何bean之前获得配置元数据并修改BeanDefinition(如${}替换)
@ComponentScan就是在这里实现的
注册bean处理器(BeanFactory没有注册(因此不能使用),需要手动注册),在bean创建时调用

f、注册拦截bean创建的bean处理器,这里只是注册,真正调用是在getBean方法中
g、国际化处理
h、初始化应用消息广播器
i、子类覆盖方法处理
j、在所有注册的bean中查找要监听的bean,注册到消息广播器中(注册监听器,并在合适的时候通知监听器)
k、通过beanFactory加载bean(非延迟加载):ApplicationContext在启动时会加载所有的单例bean,调用getBean方法(上面Spring中BeanFactory加载bean的过程)
l、完成刷新,通知生命周期管理器lifecycleProcessor刷新过程,并通过事件通知监听者


Spring-FactoryBean、BeanFactory、ObjectFactory

1、FactoryBean:

1、一般情况下,Spring通过反射机制来实例化bean,而这样可能需要很多配置,可以通过实现FactoryBean接口以编码的方式来代替
2、在IOC容器的基础上给Bean的实现加上了一个简单工厂模式和装饰模式,是一个可以生产对象和装饰对象的工厂bean
它是泛型的,只能固定生产某一类对象,而不像BeanFactory那样可以生产多种类型的Bean
3、当bean实现FactoryBean接口时,通过工厂的getBean方法返回的是FactoryBean中的getObject方法返回的实例,如果想要返回当前bean,需要以&开头

2、BeanFactory:对象工厂,用于实例化和保存对象
3、ObjectFactory:某个特定的工厂,用于在项目启动时,延迟实例化对象,解决循环依赖问题, 调用它的getObject方法时,才会触发 Bean 实例化


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

Spring中循环依赖包括构造器依赖和setter注入依赖
Spring只能解决单例setter注入依赖(注入时会返回提前暴露的创建中的bean)
构造器依赖无法解决(因为只有实例化之后才能曝光,实例化前曝光是有风险的)
对于原型模式,循环依赖也是无法解决的(因为不使用缓存)

bean什么时候才会提早曝光:单例、创建中、允许循环依赖

1、根据beanName尝试从singletionObjects中获取实例
2、根据beanName尝试从earlySingletionObjects中获取实例
3、根据beanName尝试从singletonFactories中获取ObjectFactory,调用getObject方法创建bean(这里只是实例化了bean),放到earlySingletionObjects中,并从singletonFactories中移除ObjectFactory(互斥操作)这时已经可以通过容器的getBean方法获取到bean

setter循环依赖解决过程:

1、创建a时,暴露ObjectFactory,标记a在创建中,根据构造器实例化bean,setter注入b
2、创建b时,暴露ObjectFactory,标记b在创建中,根据构造器实例化bean,setter注入a


Spring-AOP

静态代理、动态代理:

1、静态代理直接调用目标类方法
2、动态代理通过反射调用目标类方法

JDK动态代理、CGLIB动态代理:

JDK是在运行期间创建接口的实现类来完成对目标对象的代理
CGLIB采用了非常底层的字节码技术,其原理是通过字节码技术为目标类创建子类:
1、生成代理类Class的二进制字节码
2、通过Class.forName加载二进制字节码,生成Class对象
3、通过反射机制获取代理类实例构造,并初始化代理类对象
4、在子类中采用方法拦截的技术拦截所有父类方法的调用,顺势织入横切逻辑

要素:连接点(方法执行处)、切入点(何处织入通知)、通知(处理时机及处理逻辑)、切面(包括切入点和通知)
源码:
1、解析器解析AOP代理注解,生成BeanDefination,并注册到BeanDefinitionMap中
2、创建自动代理创建器(用来实现AOP)(AnnotationAwareAspectJAutoProxyCreator)
3、选择代理实现方式:

1、默认如果目标对象有实现接口,则使用JDK动态代理
2、否则使用CGLIB代理(无法覆写final方法,可以通过proxy-target-class属性强制使用CGLIB代理)
3、expose-proxy属性是为了解决有时候目标对象内部的自我调用无法实现切面增强的问题(强制暴露代理,在代码中可以获取这个代理进行显式调用)

4、创建AOP动态代理:

1、自动代理创建器实现了BeanPostProcessor接口,Spring在目标bean初始化完成之后会调用其postProcessAfterInitialization方法来创建AOP动态代理
2、获取增强,获取所有增强中目标bean可用的增强
3、创建代理工厂,根据配置设置JDK动态代理,或者CGLIB代理,将目标bean及其增强添加到代理工厂,通过代理工厂创建代理并返回(将增强组成拦截器链,执行目标方法时,执行拦截器链,中间会调用目标方法)


Spring事务失效的场景

1、注解@Transactional配置的方法非public权限修饰

可以开启 AspectJ 代理模式解决

2、注解@Transactional所在类非Spring容器管理的bean
3、注解@Transactional所在类中,注解修饰的方法被类内部方法调用

场景:无事务方法调用有事务方法,事务失效
解决:使用代理对象调用解决,且要在启动类上加注解@EnableAspectJAutoProxy(exposeProxy = true)):
1、Spring在扫描Bean的时候会自动为标注了@Transactional注解的类生成一个代理对象(proxy)
2、当有注解的方法被调用的时候,实际上是调用代理对象的方法
3、代理对象在调用之前会开启事务,执行事务的操作
4、但是同类中的方法互相调用,相当于this.B(),此时的B方法并非是代理对象的方法,而是直接通过原有的Bean直接调用,所以注解会失效

4、业务代码抛出异常类型非RuntimeException,事务失效
5、业务代码中存在异常时,使用try…catch…语句块捕获,而catch语句块没有throw new RuntimeExecption异常(最难被排查到问题且容易忽略)
6、注解@Transactional中Propagation属性值设置错误即Propagation.NOT_SUPPORTED(一般不会设置此种传播机制)
7、mysql关系型数据库,且存储引擎是MyISAM而非InnoDB,则事务会不起作用(基本开发中不会遇到)


Spring Boot-启动原理

@SpringBootApplication注解:
1、@SpringBootConfiguration注解

继承Configuration,表示启动类是IOC容器的配置类

2、@EnableAutoConfiguration注解:

1、通过@AutoConfigurationPackage注解获取自动配置包,返回当前主类的同级以及子级的中断自动配置组件
2、开启springboot的配置功能,借助@Import({EnableAutoConfigurationImportSelector.class})实现,将自动配置组件对应的bean定义都加载到IoC容器中
3、通过Spring的SpringFactoriesLoader(Spring工厂加载器)去读取META-INF/spring.factories中的配置类信息,通过反射生成一个配置类(里面有许多bean定义)
4、将这些bean定义加载到IOC容器中(但是不是所有存在于spring.factories中的配置都进行加载,而是通过@ConditionalOnClass注解进行判断条件是否成立(只要导入相应的starter,条件就能成立),如果条件成立则加载配置类,否则不加载该配置类)
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

分布式事务

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

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会回收)


你可能感兴趣的:(问题-2021-09-21)