唯's Blog

笔者是一个热爱编程的 Java 程序员。

0%

  1. 利用 @RefreshScope,@RefreshScope 底层通过 @Scope(“refresh”)、ScopedProxyMode proxyMode() 来实现当前Bean 的配置刷新。底层会对当前Bean生成一个代理对象,@Scope(“refresh”) 的Bean 会有一个单独的Map进行缓存,独立于 singletonObjects ,

  2. 核心发送了一个RefreshEvent事件:applicationContext.publishEvent(new RefreshEvent(this, null, “Refresh Nacos config”));

  3. RefreshEventListener 监听该事件,利用ContextRefresher.refresh()刷新环境配置信息、清除 @RefresScope Bean缓存,注意:ContextRefresher refresh方法中 refreshEnvironment方法会重启Application容器

  4. 客户端拿到最新的配置后,将配置更新到Spring容器中的Environment对象中.

  5. 清除bean缓存,这里的缓存和通常说的三级缓存有点不一样,是BeanLifecycleWrapperCache,缓存了所有@RefreshScope 标注的对象.

  6. 对所有需要动态更新的bean重新执行实例化,初始化的过程,执行完毕重新放到缓存

Mbean

用JMX把需要配置的属性集中在一个类中,然后写一个MBean,再进行相关配置。另外JMX还提供了一个工具页,以方便我们对参数值进行修改。

1
2
3
4
5
6
7
8
9
10
11
12
13

final ObjectName beanConfigName = new ObjectName("com.zaxxer.hikari:type=PoolConfig (" + poolName + ")");

final ObjectName beanPoolName = new ObjectName("com.zaxxer.hikari:type=Pool (" + poolName + ")");

if (!mBeanServer.isRegistered(beanConfigName)) {

mBeanServer.registerMBean(config, beanConfigName);

mBeanServer.registerMBean(hikariPool, beanPoolName);

}

核心控制器 Controller

Broker Set -> Controller ,创建 Zookeeper 节点,利用 Zookeeper 实现 Broker 分布式协调。(后续新版本废弃 Zookeeper)

具体作用

  • Partition Leader 选举

  • 分区 ISR 集合发生变化,同步变化信息到其他 broker 节点

  • 当使用kafka-topics.sh脚本为某个topic增加分区数量时,同样还是由控制器负责让新分区被其他节点感知到

GroupCoordinator(组协调器)

消费者组加入

相关名词

HW

LEO

LSO

如何保证消息不丢失?

Producer 相关配置

acks

默认 acks = all

1
2
3
4
5
6
7

ack = all/-1:表示分区 isr 的所有副本同步成功以后才返回ack

ack = 0:表示客户端只发送消息,不管服务端是否保存消息成功

ack = 1:表示分区 Leader 节点保存消息成功后返回。

in.insync.replicas

Kafka ISR 列表中最小同步副本数

默认 in.insync.replicas = 1

根据这两个极端的情况可以看出min.insync.replicas的取值,是kafka系统可用性和数据可靠性的平衡!

  1. 减小 min.insync.replicas 的值,一定程度上增大了系统的可用性,允许kafka出现更多的副本broker crash并且服务正常运行;但是降低了数据可靠性,可能会丢数据(极端情况1)。

  2. 增大 min.insync.replicas 的值,一定程度上增大了数据的可靠性,允许一些broker crash掉,且不会丢失数据(只要再次选举的leader是从ISR中选举的就行);但是降低了系统的可用性,会允许更少的broker crash(极端情况2)。

1
2
3
4
5

当生产者将确认设置为“all”(或“-1”)时,min.insync.replicas 指定必须确认写入的最小副本数才能被视为成功。如果无法满足此最小值,则生产者将引发异常(NotEnoughReplicas 或 NotEnoughReplicasAfterAppend)。

当一起使用时,min.insync.replicas 和 acks 允许您强制执行更大的持久性保证。一个典型的场景是创建一个复制因子为 3 的主题,将 min.insync.replicas 设置为 2,并使用“all”的 acks 生成。如果大多数副本没有收到写入,这将确保生产者引发异常。

消费者相关配置

enable-auto-commit = false

AckMode 手动提交 MANUAL和MANUAL_IMMEDIATE

MANUAL_IMMEDIATE是消费完一个消息就提交,MANUAL是处理完一批消息,在下一次拉取消息之前批量提交。拉取批量消息可以通过max.poll.record设置最大,默认是500条。前提是消息大小满足最大限制,否则一批也拉取不到最大的500条。

| 模式 | 描述 |

| —————- | ———————————————————— |

| MANUAL | poll()拉取一批消息,处理完业务后,手动调用Acknowledgment.acknowledge()先将offset存放到map本地缓存,在下一次poll之前从缓存拿出来批量提交 |

| MANUAL_IMMEDIATE | 每处理完业务手动调用Acknowledgment.acknowledge()后立即提交 |

| RECORD | 当每一条记录被消费者监听器(ListenerConsumer)处理之后提交 |

| BATCH | 当每一批poll()的数据被消费者监听器(ListenerConsumer)处理之后提交 |

| TIME | 当每一批poll()的数据被消费者监听器(ListenerConsumer)处理之后,距离上次提交时间大于TIME时提交 |

| COUNT | 当每一批poll()的数据被消费者监听器(ListenerConsumer)处理之后,被处理record数量大于等于COUNT时提交 |

| COUNT_TIME | TIME或COUNT满足其中一个时提交 |

auto.offset.reset

  • none

  • earliest

  • latest

    所有介绍的前提,同一个消费者组下

none

1
2
3

如果没有为消费者找到先前的offset的值即没有自动维护偏移量,也没有手动维护偏移量,则抛出异常

earliest

1
2
3
4
5

在各分区下有提交的offset时:从offset处开始消费

在各分区下无提交的offset时:从头开始消费

latest

1
2
3
4
5

在各分区下有提交的offset时:从offset处开始消费

在各分区下无提交的offset时:从最新的数据开始消费

isolation.level=read_committed

1
2
3

READ_UNCOMMITTED((byte) 0), READ_COMMITTED((byte) 1);

在评估无服务器和容器等选项时,需要继续考虑虚拟机的优势。

人们如今生活在一切都是云原生的时代,任何虚拟机的优势都容易被忽略。虚拟机越来越被视为一种遗留技术,缺乏诸如容器和无服务器功能等新型解决方案的多功能性和性能优势。如果企业如今要部署应用程序,则可能更倾向于在后一种类型的“下一代”平台上进行部署,而不是使用无聊的原有虚拟机。

214443DO-0.jpg

在某种程度上,这种趋势是公平的。与替代形式的技术相比,虚拟机在许多情况下是效率较低的解决方案。

但是,这并不意味着虚拟机已经完全失效。就像当今的裸机环境(虚拟机在20年前帮助虚拟机成为“传统”技术)一样,如今仍然有其用例,仍然有很多充分的理由考虑使用虚拟机代替容器、无服务器功能或虚拟机。其他一些新型的托管解决方案。

反对采用虚拟机

为了解释原因,首先概述与替代托管技术相比,虚拟机可能不是理想选择的原因。

避免虚拟机,而是选择诸如容器之类的东西来托管您的应用的最常见原因如下:

开销:虚拟机比容器消耗更多的资源。

速度:在某些方面,虚拟机速度较慢。它们需要更长的时间来启动(可能是一分钟或两分钟,而不是一个容器的几秒钟)。由于某些主机系统的资源被虚拟化虚拟机管理程序占用,因此它们托管的应用程序运行速度可能也不太快,因此可供应用程序使用的可用资源较少。

冗余:虚拟机是在假设每台计算机都驻留在单个服务器上的前提下设计的。尽管可以通过将虚拟机分布在服务器群集中来为虚拟机创建冗余,但是与使用容器在群集中分布应用程序相比,这样做需要更多的工作-并且是一个更笨拙的过程。

庞大的映像:包含主机操作系统的虚拟机映像(大多数情况下)通常会占用至少几GB的空间,甚至可能更多。相比之下,容器镜像可能只有几兆字节,因为容器镜像不必打包完整的操作系统。

原生云:虚拟机是一项在数十年前(即云时代之前)广泛使用的技术。因此,与它们不同的是,由于与容器和无服务器的虚拟机不同,虚拟机不是云原生技术,因此对它们存在某种文化偏见。

所有这些观点都是真实有效的。对于许多现代应用程序部署,虚拟机不是优秀的选择。

虚拟机仍然很重要的原因

但是,在许多用例中,虚拟机以积极的方式在竞争中脱颖而出。考虑以下原因,您可能想要保留您的虚拟机,并避免诱惑跳上容器化的,云原生的潮流。

灵活性

灵活性也许是虚拟机的最大卖点,到最后,它们仍将提供最大程度的部署灵活性。虚拟机几乎可以部署在任何地方,而不管其操作系统或主机的配置如何。Windows系统可以托管基于Linux的虚拟机,反之亦然。

容器提供一定程度的灵活性。容器化的Linux应用程序不在乎是哪个Linux发行版托管它。但是,除非您使用虚拟机创建所需的其他抽象,否则您仍然无法运行Linux容器或Windows或Linux上的Windows容器。

安全与隔离

自Docker在2013年问世以来,容器的安全性得到了极大的提高。但是,它仍然值得关注。确实,对安全性的担忧是某些团队选择不使用容器的主要原因。

随着容器平台的不断成熟以及更多安全工具的全面支持,这些担忧可能会得到缓解。但是,从一个简单的事实来看,容器化的应用程序永远无法与虚拟机达到相同程度的隔离,因此从安全角度来看,容器不可能完全匹配虚拟机。虚拟机不会像容器那样共享彼此的内核或其他基本系统资源。

容器管理

的确,容器在许多方面都更自然地适合于分布式主机环境,但是此功能也会使它们更难管理。当数百个容器分布在数十个服务器上时,事情很快就变得难以控制。这就是为什么您使用Kubernetes之类的业务流程协调器来自动执行大部分管理工作的原因。但是,协调器本身增加了您必须设置,管理和保护的另一层复杂性。

大规模虚拟机部署也需要编排解决方案。但是,它们很少像容器部署那样复杂。使用虚拟机时,移动部件很少,基础架构的重叠层也更少。

虚拟机是原始云

最后,让我们解决针对虚拟机的文化偏见。虚拟机可能早于云,但这并不意味着它们对云是陌生的。基于虚拟机的IaaS服务是2000年代中期由AWS等公共云提供商推出的第一项主要的云计算服务。它们仍然是这些提供商所提供产品的关键部分。

如今,容器和其他所谓的云原生解决方案可能会越来越热。但是不要误以为虚拟机也不是云原生技术。没有虚拟机,云首先就不可能成为现实。

结论

对于许多IT团队来说,是放心的时候了,学会学习Docker(以及企业喜欢的其他任何现代,云原生应用托管技术)。但这并不意味着完全放弃虚拟机。虚拟机在许多云中仍然扮演着重要角色,基于简单的假设即将其注销是错误的,因为它们是“旧”技术。

SpringBoot启动流程

  1. 创建 SpringApplication :读取 spring.facories 中的 ApplicationContextInitializer、ApplicationListener、判断 Web容器类型(webflux、servlet)

  2. 执行 Run 方法

  3. 读取 环境配置信息、命令行参数

  4. 创建对应的ServletWebServerApplicationContext

  5. 预初始化 context:读取配置类

  6. 调用refresh加载 context(ioc容器)

6.1 加载所有自动配置类 (@SpringBootApplication、@EableAutoConfiguration)

6.2 创建容器

  1. 在这个启动流程中,会发布多个applicationEvent (ApplicationListener) ;调用监听器 ApplicationContextInitializer

Spirng扩展点

自动装配原理

原理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

// 标识 SpringBoot 应用注解

@SpringBootApplication

// 自动装配的核心注解

@EnableAutoConfiguration

// 默认注册当前 Springboot类包下和子包下的所有类

@AutoConfigurationPackage

// 读取 META-INF/spring.factories 下的自动配置类 (starter 自动装配核心)

// 利用 import 导入类的特性,注册配置类

@Import(AutoConfigurationImportSelector.class)

List<String> configurations = SpringFactoriesLoader.loadFactoryNames(

getSpringFactoriesLoaderFactoryClass(), getBeanClassLoader());



结论

SpringBoot不需要写配置文件的原因是,SpringBoot所有配置都是在启动的时候进行扫描并加载,SpringBoot的所有自动配置类都在Spring.factories里面,但是不一定会生效,生效前要判断条件是否成立,只要导入了对应的start,就有对应的启动器,有了启动器就能帮我们进行自动配置类。

总结

1、注解@SpringBootApplication中含有三个注解,其中@EnabelAutoConfiguration和自动配置有关;

2、@EnableAutoConfiguration会读取所有jar下META-INF/spring.factories文件的内容,获取”org.springframework.boot.autoconfigure.EnableAutoConfiguration“的配置,把这些配置注入到容器;

3、@EnableAutoConfiguration注入的类是否生效,需要看其上面的注解,主要配合@ConditionalXXX注解使用;

SpringBoot 怎么集成 Tomcat

SpringBoot 启动过程中会创建 ServletWebServerContext ,一般使用注解方式启动,创建的是 AnotationServletWebServerContext (extends ServletWebServerContext) ,该对象下有个成员变量 webServer ,具体的实现类:TomcatWebServer

TomcatWebServer 是 Spring 对原生 Tomcat 的包装类,其中就包含了 Tomcat 的功能。

基于插口 BeanPostProcessor 实现的一些 Spring 高级特性

  • AnnotationAwareAspectJAutoProxyCreator -> AOP 接入 Spring 核心扩展点,带有 @Aspect 将在这里 InstantiationAwareBeanPostProcessor 生成代理对象
1
2
3
4
5
6
7
8
9

>AnnotationAwareAspectJAutoProxyCreator extends... AbstractAutoProxyCreator

>AbstractAutoProxyCreator implements SmartInstantiationAwareBeanPostProcessor

>SmartInstantiationAwareBeanPostProcessor extends InstantiationAwareBeanPostProcessor



InstanitiationAwareBeanPostProcessor 含有 6 个扩展点

  • postProcessBeforeInstantiation // 实例化之前
  • postProcessAfterInstantiation // 实例化之后
  • postProcessProperties // 依赖自动注入之前
  • postProcessPropertyValues // 依赖自动注入之后
  • postProcessBeforeInitialization // 初始化之前
  • postProcessAfterInitialization // 初始化之后

SmartInitializingSingleton (since spring 4.1)

  • afterSingletonsInstantiated // 在容器非LazySingleton都已被初始化完成后执行
  • AsyncAnnotationBeanPostProcessor (extends AdviceModeImportSelector)

    @Async -> @import -> AsyncConfigurationSelector -> ProxyAsyncConfiguration -> @Bean -> AsyncAnnotationBeanPostProcessor

  • ProxyTransactionManagementConfiguration (extends AdviceModeImportSelector) -> @Transcation

  • @Transcation 声明式事务依赖 AOP, 核心原理是通过 @EnableTransactionManagement-> import(TransactionManagementConfigurationSelector->)ProxyTransactionManagementConfiguration -> 注入声明式事务的advisor BeanFactoryTransactionAttributeSourceAdvisor -> 通过advisor aop 完成事务的添加 BeanFactoryTransactionAttributeSourceAdvisor

AOP

Spring AOP 的实现是通过动态代理。核心是添加一堆的advisor (切点+通知)(@Aspect 切面会被解析成List), 声明式事务 本质 添加一个 advisor

代码

spring中expose-proxy的作用与原理

先看一下几个问题,下面是段代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

public interface UserService{

public void a();

public void a();

}



public class UserServiceImpl implements UserService{

@Transactional(propagation = Propagation.REQUIRED)

public void a(){

this.b();

}

@Transactional(propagation = Propagation.REQUIRED_NEW)

public void b(){

System.out.println("b has been called");

}

}

ps:代码参照《Spring源码解析》p173页。

Q1:b中的事务会不会生效?

A1:不会,a的事务会生效,b中不会有事务,因为a中调用b属于内部调用,没有通过代理,所以不会有事务产生。

Q2:如果想要b中有事务存在,要如何做?

A2:<aop:aspectj-autoproxy expose-proxy=“true”> ,设置expose-proxy属性为true,或者在@EnableAspectJAutoProxy(exposeProxy = true)` 将代理暴露出来,使用AopContext.currentProxy()获取当前代理,将this.b()改为((UserService)AopContext.currentProxy()).b()

还要一个方法:

使用cglib的方式,是可以实现代理的,会执行成功!这个也是和jdk默认的动态代理的不同点

  • 在多线程访问同一份数据量时,会产生线程安全问题,ThreadLocal 以创建副本的方式来避免线程冲突。空间换时间的思想

Spring 中的应用

  • Spring 中事务功能便用到了 ThreadLocal,缓存事务数据库连接(ConnectionHodler)、事务信息(隔离级别、事务名、是否只读、事务激活状态)。用与实现事务传播行为。TransactionSynchronizationManager

  • Spring-Security 中的登录会话信息也是以 ThreadLocal 保存,在执行业务逻辑,使用异步线程处理IO逻辑时 会在异步线程找不到 会话信息

SqlSessionTemplate

  • 具体执行sql操作的代理对象sqlSessionProxy , 使用 JDK的动态代理
1
2
3
4
5
6
7

this.sqlSessionProxy = (SqlSession) newProxyInstance(SqlSessionFactory.class.getClassLoader(),

new Class[] { SqlSession.class }, new SqlSessionInterceptor());



  • 代理内容 获取一个SqlSession(DefalutSqlSession), TransactionSynchronizationManager (事务管理器(Spring-tx))

  • Mybatis 获取连接流程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15



SqlSession -> executor -> SpringManagedTransaction.getConnection() -> DataSourceUtil



SqlSession -> SqlSessionInterceptor -> DefaultSqlSession -> 执行对应模板方法(query)-> MybatisCachingExecutor.query() -> BaseExecutor.query() -> MybatisSimpleExecutor.doQuery() -> BaseExecutor.getConnection() -> SpringManagedTransaction.getConnection() -> DataSourceUtil.getConnection() ->



HikariPool(配置的数据库连接池)



1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

private class SqlSessionInterceptor implements InvocationHandler {

@Override

public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

SqlSession sqlSession = getSqlSession(SqlSessionTemplate.this.sqlSessionFactory,

SqlSessionTemplate.this.executorType, SqlSessionTemplate.this.exceptionTranslator);

try {

Object result = method.invoke(sqlSession, args);

if (!isSqlSessionTransactional(sqlSession, SqlSessionTemplate.this.sqlSessionFactory)) {

// force commit even on non-dirty sessions because some databases require

// a commit/rollback before calling close()

sqlSession.commit(true);

}

return result;

  • Spring 事务同步工具 TransactionSynchronizationManager

  • 动态代理

  • ThreadLocal 存放 SqlSessionFatory 与当前线程获取SqlSessionHolder(其中存放SqlSessiond的引用)绑定 (前提当前SqlSession开启了事务)

Spring Hikari 默认配置

  • CONNECTION_TIMEOUT (连接超时) 30s

  • minIdle (最小线程数) 10

  • maxPoolSize (最大线程数) 10

需求描述

撰写两个 API 接口:

  • 短域名存储接口:接受长域名信息,返回短域名信息

  • 短域名读取接口:接受短域名信息,返回长域名信息。

限制:

  • 短域名长度最大为 8 个字符

  • 采用SpringBoot,集成Swagger API文档;

  • JUnit编写单元测试, 使用Jacoco生成测试报告(测试报告提交截图);

  • 映射数据存储在JVM内存即可,防止内存溢出;

设计思路

数据存储

  • 由于项目要求映射数据存储在JVM内存即可,防止内存溢出。因此本次设计为求简化,采用HashMap进行存储(考虑并发可使用ConcurrentHashMap)。

  • 由于涉及从短url获取长url、从长url获取短url的双向操作,因此本项目采用两张哈希表存储,即一张表通过长域名查询存储短域名,另一张表实现短域名读取长域名时。

映射算法

生成短链接的本质是要确认一对短链接和长链接的唯一 KV 关系,也就是所以常见的生成方式有两种:Hash 和 自增 ID。 本项目默认采用自增ID方式。

Hash

此方案是通过 Hash 算法将任意长度的长链接计算输出得到一个固定长度的短码。好处就是实现简单,安全性高。但缺点也显而易见,再好的 Hash 算法也无法解决碰撞问题,且随着数量的增大碰撞概率也随之增大,所以此方案难点是如何处理碰撞冲突问题。

自增ID

自增 ID 可以确保全局唯一性,且市面上已有成熟的实现方案:

  • UUID

  • 数据库自增

  • Redis生成

  • 分布式算法(如雪花)

短链接一般由大小写字母和数字组成,共有 26+26+10 = 62 个字符。在得到一个十进制 Long 型 ID 后,可以通过 62 进制转成字符串。 但如果仅简单使用自增ID,会带来严重的安全问题,因为生成的短链接可以猜测和遍历。 后续可考虑以下方案提高安全性:

  • 可以对 ID 进行跳码计算,防止猜测

  • 结合长链接等信息进行安全位计算,并打乱顺序,防止遍历

  • 同时支持校验,防止恶意尝试直接回落 DB

本次方案设计中,为简化,采用项目中的自增ID实现。步骤如下:

  • 通过长url生成对应的ID

  • 将其转化为62进制的字符串。

后续优化

  • 存储采用redis + mysql

  • 分布式ID/数据库自增ID

  • 映射方案,可考虑策略模式,实现多种策略实现。

  • 优化映射策略的安全性。加入随机码、扰动等等

  1. 复制回环

  • 现象:A Set Key :A -> B,A -> C,B -> A, B -> C …
  • 网络风暴
  • 数据不一致
  • 解决方案:同步数据携带一些来源信息,根据来源信息来决策是否同步给其他服务
  1. Set Key 冲突

  • 现象: A set a v1 , B set a v2
  • 会存在数据不一致的情况
1
2
3
4
5
6
7
8
9

1. v1 v1

2. v2 v2

3. v1 v2

4. v2 v1

  • 解决方案:通过 LWW - Last-Writer-Wins , 通过时间 以最后写入时间为准

引申出:服务器 Clock 问题,时钟 – 分布式系统永远的痛

Linux 服务器依赖 NPT-Server 同步时间,但是服务器之间同步时间存在延时(时钟快慢)

Vector Clock (逻辑时钟)自定义向量来表示操作前后

  1. 删除下数据不一致

  • 现象:A set k1 v1, B del k1, 同步后便会出现不一致,LWW 方式 数据已经被删除便没办法比较

  • 解决方案:Tombstone , 执行 Del 命令 伪删除,将数据放置到 Tombstone,让其他后续的命令,能够和之前的命令有一个对比。数据的存留与否也就有了判定的依据

  1. 内存占用 GC

  • 现象 :因为使用了 Tombstone 导致一部分数据删除了,但是没有真正的移出内存

  • 解决方案:GC 利用 VectorClock 实现 GC, 判断数据存活:1. 引用计算 ; 2. GC Root

  1. Key Expire 冲突?
  • 现象:在双写的Redis 集群中,Key 是否要设置一致的过期时间?1. 设置一致 解决冲突成本很高; 2. 不一致,数据会不一致?不会,假死:A set key 30s ; B set key 60 s ,A 集群触发过期,同步给 B ,B 也就删除了