面试题总结

时间:2022-08-02 01:23:12

Spring如何解析配置类

解析配置类

所有注册到beanFactory中的beanDefinitions,并且带@Configuration注解或者@Import注解或者@ImportResource注解或者@Component注解或者@ComponentScan注解或者@Bean方法的都是配置类。

  • 先判断配置类上的@Conditional注解
  • 如果配置类带了@Component注解,并且它的内部类中有配置类,则先解析内部类。
  • 解析配置类上的@PropertySources注解或者@PropertySource注解,这些注解的location属性配置的资源文件的位置,会把对应的资源解析成PropertySource对象添加到Environment中的propertySources属性中。
  • 解析配置类上的@ComponentScans或@ComponentScan注解,扫描注解中配置的路径下配置了@Component、@Repository、@Service或@Controller注解的class,并且生成这些类的beanDefinition,注册到beanFactory中。如果这些类中有配置类,则先递归处理这些配置类,然后再往下处理。
  • 获取配置类上所有的@Import注解的value属性值对应的Class数组,遍历处理这个Class数组。
    1. 如果Class的类型是ImportSelector类型,则先实例化ImportSelector,然后调用String[] importClassNames = selector.selectImports(currentSourceClass.getMetadata())得到要import的importClassNames,然后递归处理importClassNames对应的class。
    2. 如果Class的类型是DeferredImportSelector,则把它先保存到一个集合中,等所有的配置类都处理完之后再处理DeferredImportSelector集合。
    3. 如果Class的类型是ImportBeanDefinitionRegistrar类型,则把这个类加到ConfigurationClass的importBeanDefinitionRegistrars集合属性中,后面会处理
    4. 否则就把Class当做一个配置类,递归调用处理配置类的方法处理。
  • 解析配置类上的@ImportResource注解,把注解的locations属性值配置的String类型的值添加到配置类的importedResources属性中。后续处理。
  • 处理配置类中的@Bean方法,拿到配置类中所有的@Bean方法的MethodMetadata集合,并且设置到配置类的beanMethods集合属性中
  • 递归遍历配置类的所有接口,把接口中的default修饰的非abstract的@Bean方法添加到配置类的beanMethods集合属性中。
  • 返回配置类的父类,递归处理父类。

处理上一步解析的ConfigurationClass列表,注册beanDefinition

  • 如果配置类是被import进来的,则先注册这个配置类的beanDefinition
  • 处理配置类的beanMethods,注册beanDefinition。通过@Bean注册的beanDefinition都会调用设置autowireMode属性的值为AUTOWIRE_CONSTRUCTOR,通过构造器注入。
  • 处理配置类的importedResources属性,注册beanDefinition
  • 处理配置类的importBeanDefinitionRegistrars集合属性,注册beanDefinition

Spring中的设计模式

  • 工厂模式
  • 单例模式
  • 工厂方法
  • 策略模式
    1. 从各种Advice中提取MethodInterceptor时,使用不同的策略类支持不同的Advice。遍历策略类接口,遍历处理Advice
    2. 容器实例化实例时,使用不同的实例化策略。
  • 监听者模式

Springboot功能

Spring Boot可以轻松创建独立的、生产级的基于Spring的应用程序,您可以直接运行这些应用程序。
我们对Spring平台和第三方库做了很多配置,因此您可以以最少的配置开始。大多数Spring Boot应用程序需要最少的Spring配置。

Springboot功能

  • 内嵌Tomcat、Jetty或Undertow(无需部署WAR文件)
  • 提供“starter”依赖项以简化构建配置
  • 尽可能自动配置Spring和第三方库
  • 提供生产就绪功能,如指标、运行状况检查和外部化配置
  • 不需要XML配置

mybatis

核心概念

  • 每个基于 MyBatis 的应用都是以一个 SqlSessionFactory 的实例为核心的。SqlSessionFactory 的实例可以通过 SqlSessionFactoryBuilder 获得。而 SqlSessionFactoryBuilder 则可以从 XML 配置文件或一个预先配置的 Configuration 实例来构建出 SqlSessionFactory 实例。
  • 从 XML 文件中构建 SqlSessionFactory 的实例非常简单,建议使用类路径下的资源文件进行配置。 但也可以使用任意的输入流(InputStream)实例,比如用文件路径字符串或 file:// URL 构造的输入流。MyBatis 包含一个名叫 Resources 的工具类,它包含一些实用方法,使得从类路径或其它位置加载资源文件更加容易
String resource = "org/mybatis/example/mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
  • XML 配置文件中包含了对 MyBatis 系统的核心设置,包括获取数据库连接实例的数据源(DataSource)以及决定事务作用域和控制方式的事务管理器(TransactionManager)。后面会再探讨 XML 配置文件的详细内容,这里先给出一个简单的示例:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
  PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
  "https://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
  <environments default="development">
    <environment id="development">
      <transactionManager type="JDBC"/>
      <dataSource type="POOLED">
        <property name="driver" value="${driver}"/>
        <property name="url" value="${url}"/>
        <property name="username" value="${username}"/>
        <property name="password" value="${password}"/>
      </dataSource>
    </environment>
  </environments>
  <mappers>
    <mapper resource="org/mybatis/example/BlogMapper.xml"/>
  </mappers>
</configuration>
  • 如果你更愿意直接从 Java 代码而不是 XML 文件中创建配置,或者想要创建你自己的配置构建器,MyBatis 也提供了完整的配置类,提供了所有与 XML 文件等价的配置项。
DataSource dataSource = BlogDataSourceFactory.getBlogDataSource();
TransactionFactory transactionFactory = new JdbcTransactionFactory();
Environment environment = new Environment("development", transactionFactory, dataSource);
Configuration configuration = new Configuration(environment);
configuration.addMapper(BlogMapper.class);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(configuration);
  • 注意该例中,configuration 添加了一个映射器类(mapper class)。映射器类是 Java 类,它们包含 SQL 映射注解从而避免依赖 XML 映射文件。不过,由于 Java 注解的一些限制以及某些 MyBatis 映射的复杂性,要使用大多数高级映射(比如:嵌套联合映射),仍然需要使用 XML 映射文件进行映射。有鉴于此,如果存在一个同名 XML 映射文件,MyBatis 会自动查找并加载它(在这个例子中,基于类路径和 BlogMapper.class 的类名,会加载 BlogMapper.xml)。具体细节稍后讨论。

从 SqlSessionFactory 中获取 SqlSession

既然有了 SqlSessionFactory,顾名思义,我们可以从中获得 SqlSession 的实例。SqlSession 提供了在数据库执行 SQL 命令所需的所有方法。你可以通过 SqlSession 实例来直接执行已映射的 SQL 语句。例如:

try (SqlSession session = sqlSessionFactory.openSession()) {
  Blog blog = (Blog) session.selectOne("org.mybatis.example.BlogMapper.selectBlog", 101);
}

诚然,这种方式能够正常工作,对使用旧版本 MyBatis 的用户来说也比较熟悉。但现在有了一种更简洁的方式——使用和指定语句的参数和返回值相匹配的接口(比如 BlogMapper.class),现在你的代码不仅更清晰,更加类型安全,还不用担心可能出错的字符串字面值以及强制类型转换。

try (SqlSession session = sqlSessionFactory.openSession()) {
  BlogMapper mapper = session.getMapper(BlogMapper.class);
  Blog blog = mapper.selectBlog(101);
}

作用域(Scope)和生命周期

  • 理解我们之前讨论过的不同作用域和生命周期类别是至关重要的,因为错误的使用会导致非常严重的并发问题。
  • SqlSessionFactoryBuilder
    这个类可以被实例化、使用和丢弃,一旦创建了 SqlSessionFactory,就不再需要它了。 因此 SqlSessionFactoryBuilder 实例的最佳作用域是方法作用域(也就是局部方法变量)。 你可以重用 SqlSessionFactoryBuilder 来创建多个 SqlSessionFactory 实例,但最好还是不要一直保留着它,以保证所有的 XML 解析资源可以被释放给更重要的事情。
  • SqlSessionFactory
    SqlSessionFactory 一旦被创建就应该在应用的运行期间一直存在,没有任何理由丢弃它或重新创建另一个实例。 使用 SqlSessionFactory 的最佳实践是在应用运行期间不要重复创建多次,多次重建 SqlSessionFactory 被视为一种代码“坏习惯”。因此 SqlSessionFactory 的最佳作用域是应用作用域。 有很多方法可以做到,最简单的就是使用单例模式或者静态单例模式。
  • SqlSession
    每个线程都应该有它自己的 SqlSession 实例。SqlSession 的实例不是线程安全的,因此是不能被共享的,所以它的最佳的作用域是请求或方法作用域。 绝对不能将 SqlSession 实例的引用放在一个类的静态域,甚至一个类的实例变量也不行。 也绝不能将 SqlSession 实例的引用放在任何类型的托管作用域中,比如 Servlet 框架中的 HttpSession。 如果你现在正在使用一种 Web 框架,考虑将 SqlSession 放在一个和 HTTP 请求相似的作用域中。 换句话说,每次收到 HTTP 请求,就可以打开一个 SqlSession,返回一个响应后,就关闭它。 这个关闭操作很重要,为了确保每次都能执行关闭操作,你应该把这个关闭操作放到 finally 块中。 下面的示例就是一个确保 SqlSession 关闭的标准模式:
try (SqlSession session = sqlSessionFactory.openSession()) {
  // 你的应用逻辑代码
}
在所有代码中都遵循这种使用模式,可以保证所有数据库资源都能被正确地关闭。
  • 映射器实例
    映射器是一些绑定映射语句的接口。映射器接口的实例是从 SqlSession 中获得的。虽然从技术层面上来讲,任何映射器实例的最大作用域与请求它们的 SqlSession 相同。但方法作用域才是映射器实例的最合适的作用域。 也就是说,映射器实例应该在调用它们的方法中被获取,使用完毕之后即可丢弃。 映射器实例并不需要被显式地关闭。尽管在整个请求作用域保留映射器实例不会有什么问题,但是你很快会发现,在这个作用域上管理太多像 SqlSession 的资源会让你忙不过来。 因此,最好将映射器放在方法作用域内。就像下面的例子一样:
try (SqlSession session = sqlSessionFactory.openSession()) {
  BlogMapper mapper = session.getMapper(BlogMapper.class);
  // 你的应用逻辑代码
}

configuration(配置)

MyBatis 的配置文件包含了会深深影响 MyBatis 行为的设置和属性信息。

settings(设置)

这是 MyBatis 中极为重要的调整设置,它们会改变 MyBatis 的运行时行为。 下表描述了设置中各项设置的含义、默认值等。

typeAliases(类型别名)

类型别名可为 Java 类型设置一个缩写名字。 它仅用于 XML 配置,意在降低冗余的全限定类名书写。
mybatis内置了一些常见的 Java 类型类型别名。

typeHandlers(类型处理器)

MyBatis 在设置预处理语句(PreparedStatement)中的参数或从结果集中取出一个值时, 都会用类型处理器将获取到的值以合适的方式转换成 Java 类型。mybatis内置了常见的java类型的类型处理器。

自定义类型处理器

  • 你可以重写已有的类型处理器或创建你自己的类型处理器来处理不支持的或非标准的类型。 具体做法为:实现 org.apache.ibatis.type.TypeHandler 接口, 或继承一个很便利的类 org.apache.ibatis.type.BaseTypeHandler, 并且可以(可选地)将它映射到一个 JDBC 类型。比如:
@MappedJdbcTypes(JdbcType.VARCHAR)
public class ExampleTypeHandler extends BaseTypeHandler<String> {

  @Override
  public void setNonNullParameter(PreparedStatement ps, int i, String parameter, JdbcType jdbcType) throws SQLException {
    ps.setString(i, parameter);
  }

  @Override
  public String getNullableResult(ResultSet rs, String columnName) throws SQLException {
    return rs.getString(columnName);
  }

  @Override
  public String getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
    return rs.getString(columnIndex);
  }

  @Override
  public String getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
    return cs.getString(columnIndex);
  }
}
<!-- mybatis-config.xml -->
<typeHandlers>
  <typeHandler handler="org.mybatis.example.ExampleTypeHandler"/>
</typeHandlers>
  • 使用上述的类型处理器将会覆盖已有的处理 Java String 类型的属性以及 VARCHAR 类型的参数和结果的类型处理器。 要注意 MyBatis 不会通过检测数据库元信息来决定使用哪种类型,所以你必须在参数和结果映射中指明字段是 VARCHAR 类型, 以使其能够绑定到正确的类型处理器上。这是因为 MyBatis 直到语句被执行时才清楚数据类型。

  • 通过类型处理器的泛型,MyBatis 可以得知该类型处理器处理的 Java 类型,不过这种行为可以通过两种方法改变:

    1. 在类型处理器的配置元素(typeHandler 元素)上增加一个 javaType 属性(比如:javaType=“String”);
    2. 在类型处理器的类上增加一个 @MappedTypes 注解指定与其关联的 Java 类型列表。 如果在 javaType 属性中也同时指定,则注解上的配置将被忽略。
  • 可以通过两种方式来指定关联的 JDBC 类型:

    1. 在类型处理器的配置元素上增加一个 jdbcType 属性(比如:jdbcType=“VARCHAR”);
    2. 在类型处理器的类上增加一个 @MappedJdbcTypes 注解指定与其关联的 JDBC 类型列表。 如果在 jdbcType 属性中也同时指定,则注解上的配置将被忽略。
  • 当在 ResultMap 中决定使用哪种类型处理器时,此时 Java 类型是已知的(从结果类型中获得),但是 JDBC 类型是未知的。 因此 Mybatis 使用 javaType=[Java 类型], jdbcType=null 的组合来选择一个类型处理器。 这意味着使用 @MappedJdbcTypes 注解可以限制类型处理器的作用范围,并且可以确保,除非显式地设置,否则类型处理器在 ResultMap 中将不会生效。 如果希望能在 ResultMap 中隐式地使用类型处理器,那么设置 @MappedJdbcTypes 注解的 includeNullJdbcType=true 即可。 然而从 Mybatis 3.4.0 开始,如果某个 Java 类型只有一个注册的类型处理器,即使没有设置 includeNullJdbcType=true,那么这个类型处理器也会是 ResultMap 使用 Java 类型时的默认处理器。

  • 最后,可以让 MyBatis 帮你查找类型处理器:

<!-- mybatis-config.xml -->
<typeHandlers>
  <package name="org.mybatis.example"/>
</typeHandlers>

注意在使用自动发现功能的时候,只能通过注解方式来指定 JDBC 的类型。

  • 你可以创建能够处理多个类的泛型类型处理器。为了使用泛型类型处理器, 需要增加一个接受该类的 class 作为参数的构造器,这样 MyBatis 会在构造一个类型处理器实例的时候传入一个具体的类
//GenericTypeHandler.java
public class GenericTypeHandler<E extends MyObject> extends BaseTypeHandler<E> {

  private Class<E> type;

  public GenericTypeHandler(Class<E> type) {
    if (type == null) throw new IllegalArgumentException("Type argument cannot be null");
    this.type = type;
  }
  ...

EnumTypeHandler 和 EnumOrdinalTypeHandler 都是泛型类型处理器,我们将会在接下来的部分详细探讨。

处理枚举类型

  • 若想映射枚举类型 Enum,则需要从 EnumTypeHandler 或者 EnumOrdinalTypeHandler 中选择一个来使用。
  • 比如说我们想存储取近似值时用到的舍入模式。默认情况下,MyBatis 会利用 EnumTypeHandler 来把 Enum 值转换成对应的名字。
  • 注意 EnumTypeHandler 在某种意义上来说是比较特别的,其它的处理器只针对某个特定的类,而它不同,它会处理任意继承了 Enum 的类。
    不过,我们可能不想存储名字,相反我们的 DBA 会坚持使用整形值代码。那也一样简单:在配置文件中把 EnumOrdinalTypeHandler 加到 typeHandlers 中即可, 这样每个 RoundingMode 将通过他们的序数值来映射成对应的整形数值
<!-- mybatis-config.xml -->
<typeHandlers>
  <typeHandler handler="org.apache.ibatis.type.EnumOrdinalTypeHandler" javaType="java.math.RoundingMode"/>
</typeHandlers>
  • 但要是你想在一个地方将 Enum 映射成字符串,在另外一个地方映射成整形值呢?
  • 自动映射器(auto-mapper)会自动地选用 EnumOrdinalTypeHandler 来处理枚举类型, 所以如果我们想用普通的 EnumTypeHandler,就必须要显式地为那些 SQL 语句设置要使用的类型处理器。

objectFactory(对象工厂)

  • 每次 MyBatis 创建结果对象的新实例时,它都会使用一个对象工厂(ObjectFactory)实例来完成实例化工作。 默认的对象工厂需要做的仅仅是实例化目标类,要么通过默认无参构造方法,要么通过存在的参数映射来调用带有参数的构造方法。 如果想覆盖对象工厂的默认行为,可以通过创建自己的对象工厂来实现。比如
// ExampleObjectFactory.java
public class ExampleObjectFactory extends DefaultObjectFactory {
  @Override
  public <T> T create(Class<T> type) {
    return super.create(type);
  }

  @Override
  public <T> T create(Class<T> type, List<Class<?>> constructorArgTypes, List<Object> constructorArgs) {
    return super.create(type, constructorArgTypes, constructorArgs);
  }

  @Override
  public void setProperties(Properties properties) {
    super.setProperties(properties);
  }

  @Override
  public <T> boolean isCollection(Class<T> type) {
    return Collection.class.isAssignableFrom(type);
  }}

<!-- mybatis-config.xml -->
<objectFactory type="org.mybatis.example.ExampleObjectFactory">
  <property name="someProperty" value="100"/>
</objectFactory>
  • ObjectFactory 接口很简单,它包含两个创建实例用的方法,一个是处理默认无参构造方法的,另外一个是处理带参数的构造方法的。 另外,setProperties 方法可以被用来配置 ObjectFactory,在初始化你的 ObjectFactory 实例后, objectFactory 元素体中定义的属性会被传递给 setProperties 方法。

plugins(插件)

  • MyBatis 允许你在映射语句执行过程中的某一点进行拦截调用。默认情况下,MyBatis 允许使用插件来拦截的方法调用包括:
    1. Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)
    2. ParameterHandler (getParameterObject, setParameters)
    3. ResultSetHandler (handleResultSets, handleOutputParameters)
    4. StatementHandler (prepare, parameterize, batch, update, query)
  • 这些类中方法的细节可以通过查看每个方法的签名来发现,或者直接查看 MyBatis 发行包中的源代码。 如果你想做的不仅仅是监控方法的调用,那么你最好相当了解要重写的方法的行为。 因为在试图修改或重写已有方法的行为时,很可能会破坏 MyBatis 的核心模块。 这些都是更底层的类和方法,所以使用插件的时候要特别当心
  • 通过 MyBatis 提供的强大机制,使用插件是非常简单的,只需实现 Interceptor 接口,并指定想要拦截的方法签名即可。
// ExamplePlugin.java
@Intercepts({@Signature(
  type= Executor.class,
  method = "update",
  args = {MappedStatement.class,Object.class})})
public class ExamplePlugin implements Interceptor {
  private Properties properties = new Properties();

  @Override
  public Object intercept(Invocation invocation) throws Throwable {
    // implement pre processing if need
    Object returnObject = invocation.proceed();
    // implement post processing if need
    return returnObject;
  }

  @Override
  public void setProperties(Properties properties) {
    this.properties = properties;
  }
}
<!-- mybatis-config.xml -->
<plugins>
  <plugin interceptor="org.mybatis.example.ExamplePlugin">
    <property name="someProperty" value="100"/>
  </plugin>
</plugins>

上面的插件将会拦截在 Executor 实例中所有的 “update” 方法调用, 这里的 Executor 是负责执行底层映射语句的内部对象。

  • 除了用插件来修改 MyBatis 核心行为以外,还可以通过完全覆盖配置类来达到目的。只需继承配置类后覆盖其中的某个方法,再把它传递到 SqlSessionFactoryBuilder.build(myConfig) 方法即可。再次重申,这可能会极大影响 MyBatis 的行为,务请慎之又慎。

environments(环境配置)

  • MyBatis 可以配置成适应多种环境,这种机制有助于将 SQL 映射应用于多种数据库之中, 现实情况下有多种理由需要这么做。例如,开发、测试和生产环境需要有不同的配置;或者想在具有相同 Schema 的多个生产数据库中使用相同的 SQL 映射。还有许多类似的使用场景。
  • 不过要记住:尽管可以配置多个环境,但每个 SqlSessionFactory 实例只能选择一种环境。
    所以,如果你想连接两个数据库,就需要创建两个 SqlSessionFactory 实例,每个数据库对应一个。而如果是三个数据库,就需要三个实例,依此类推,记起来很简单:
    -为了指定创建哪种环境,只要将它作为可选的参数传递给 SqlSessionFactoryBuilder 即可。可以接受环境配置的两个方法签名是:
SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(reader, environment);
SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(reader, environment, properties);

如果忽略了环境参数,那么将会加载默认环境,如下所示:

SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(reader);
SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(reader, properties);

environments 元素定义了如何配置环境。

<environments default="development">
  <environment id="development">
    <transactionManager type="JDBC">
      <property name="..." value="..."/>
    </transactionManager>
    <dataSource type="POOLED">
      <property name="driver" value="${driver}"/>
      <property name="url" value="${url}"/>
      <property name="username" value="${username}"/>
      <property name="password" value="${password}"/>
    </dataSource>
  </environment>
</environments>

注意一些关键点:

  1. 默认使用的环境 ID(比如:default=“development”)。
  2. 每个 environment 元素定义的环境 ID(比如:id=“development”)。
  3. 事务管理器的配置(比如:type=“JDBC”)。
  4. 数据源的配置(比如:type=“POOLED”)。
    默认环境和环境 ID 顾名思义。 环境可以随意命名,但务必保证默认的环境 ID 要匹配其中一个环境 ID。

mappers(映射器)

既然 MyBatis 的行为已经由上述元素配置完了,我们现在就要来定义 SQL 映射语句了。 但首先,我们需要告诉 MyBatis 到哪里去找到这些语句。 在自动查找资源方面,Java 并没有提供一个很好的解决方案,所以最好的办法是直接告诉 MyBatis 到哪里去找映射文件。 你可以使用相对于类路径的资源引用,或完全限定资源定位符(包括 file:/// 形式的 URL),或类名和包名等。例如:

<!-- 使用相对于类路径的资源引用 -->
<mappers>
  <mapper resource="org/mybatis/builder/AuthorMapper.xml"/>
  <mapper resource="org/mybatis/builder/BlogMapper.xml"/>
  <mapper resource="org/mybatis/builder/PostMapper.xml"/>
</mappers>
<!-- 使用完全限定资源定位符(URL) -->
<mappers>
  <mapper url="file:///var/mappers/AuthorMapper.xml"/>
  <mapper url="file:///var/mappers/BlogMapper.xml"/>
  <mapper url="file:///var/mappers/PostMapper.xml"/>
</mappers>
<!-- 使用映射器接口实现类的完全限定类名 -->
<mappers>
  <mapper class="org.mybatis.builder.AuthorMapper"/>
  <mapper class="org.mybatis.builder.BlogMapper"/>
  <mapper class="org.mybatis.builder.PostMapper"/>
</mappers>
<!-- 将包内的映射器接口全部注册为映射器 -->
<mappers>
  <package name="org.mybatis.builder"/>
</mappers>

这些配置会告诉 MyBatis 去哪里找映射文件,剩下的细节就应该是每个 SQL 映射文件了,也就是接下来我们要讨论的。

mybatis缓存

  • MyBatis 内置了一个强大的事务性查询缓存机制,它可以非常方便地配置和定制。 为了使它更加强大而且易于配置,我们对 MyBatis 3 中的缓存实现进行了许多改进。
  • 默认情况下,只启用了本地的会话缓存,它仅仅对一个会话中的数据进行缓存。 要启用全局的二级缓存,只需要在你的 SQL 映射文件中添加一行:
<cache/>

基本上就是这样。这个简单语句的效果如下:

  • 射语句文件中的所有 select 语句的结果将会被缓存。
  • 映射语句文件中的所有 insert、update 和 delete 语句会刷新缓存。
  • 缓存会使用最近最少使用算法(LRU, Least Recently Used)算法来清除不需要的缓存。
  • 缓存不会定时进行刷新(也就是说,没有刷新间隔)。
  • 缓存会保存列表或对象(无论查询方法返回哪种)的 1024 个引用。
  • 缓存会被视为读/写缓存,这意味着获取到的对象并不是共享的,可以安全地被调用者修改,而不干扰其他调用者或线程所做的潜在修改。
    这些属性可以通过 cache 元素的属性来修改。比如:
<cache
  eviction="FIFO"
  flushInterval="60000"
  size="512"
  readOnly="true"/>

这个更高级的配置创建了一个 FIFO 缓存,每隔 60 秒刷新,最多可以存储结果对象或列表的 512 个引用,而且返回的对象被认为是只读的,因此对它们进行修改可能会在不同线程中的调用者产生冲突。

  • 二级缓存是事务性的。这意味着,当 SqlSession 完成并提交时,或是完成并回滚,但没有执行 flushCache=true 的 insert/delete/update 语句时,缓存会获得更新。

  • Mybatis 使用到了两种缓存:本地缓存(local cache)和二级缓存(second level cache)。

  • 每当一个新 session 被创建,MyBatis 就会创建一个与之相关联的本地缓存。任何在 session 执行过的查询结果都会被保存在本地缓存中,所以,当再次执行参数相同的相同查询时,就不需要实际查询数据库了。本地缓存将会在做出修改、事务提交或回滚,以及关闭 session 时清空。

  • 默认情况下,本地缓存数据的生命周期等同于整个 session 的周期。由于缓存会被用来解决循环引用问题和加快重复嵌套查询的速度,所以无法将其完全禁用。但是你可以通过设置 localCacheScope=STATEMENT 来只在语句执行时使用缓存。

  • 注意,如果 localCacheScope 被设置为 SESSION,对于某个对象,MyBatis 将返回在本地缓存中唯一对象的引用。对返回的对象(例如 list)做出的任何修改将会影响本地缓存的内容,进而将会影响到在本次 session 中从缓存返回的值。因此,不要对 MyBatis 所返回的对象作出更改,以防后患。

mybatis源码精髓

  • MybatisAutoConfiguration上的注解
@org.springframework.context.annotation.Configuration
@ConditionalOnClass({ SqlSessionFactory.class, SqlSessionFactoryBean.class })
@ConditionalOnSingleCandidate(DataSource.class)
@EnableConfigurationProperties(MybatisProperties.class)
@AutoConfigureAfter({ DataSourceAutoConfiguration.class, MybatisLanguageDriverAutoConfiguration.class })
public class MybatisAutoConfiguration implements InitializingBean 
  • mybatis-spring-boot-autoconfigure包中的spring.factories文件中配置了
# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.mybatis.spring.boot.autoconfigure.MybatisLanguageDriverAutoConfiguration,\
org.mybatis.spring.boot.autoconfigure.MybatisAutoConfiguration

# Depends On Database Initialization Detectors
org.springframework.boot.sql.init.dependency.DependsOnDatabaseInitializationDetector=\
org.mybatis.spring.boot.autoconfigure.MybatisDependsOnDatabaseInitializationDetector
  • MybatisAutoConfiguration中会注入SqlSessionFactory和SqlSessionTemplate的bean。主要的配置都在SqlSessionFactory中,@EnableConfigurationProperties(MybatisProperties.class)注解对应的MybatisProperties中的配置,都会被设置到SqlSessionFactory中。
  • 实例化SqlSessionFactory时,SqlSessionFactory主要有两个重要的属性,一个是DataSource属性,用于获取Session。一个是Configuration对象,Configuration对象包含了mybatis的各种配置,如plugin,类型处理器,mapper列表等等。
  • Configuration会根据配置,设置typeAliasesPackage,interceptorChain,注册类型处理器,设置默认的EnumTypeHandler,根据配置文件中设置的mapper-locations属性(比如classpath:mapper/*.xml),遍历扫描路径下的xml文件,解析xml文件中的cache-ref标签,cache标签,parameterMap标签,resultMap标签,sql标签,select|insert|update|delete标签。解析出来的对象都会设置到Configuration对象中。特别说明的每条select|insert|update|delete标签及其属性值都会被解析成一个MappedStatement对象,并被添加到Configuration的Map<String, MappedStatement> mappedStatements属性中,这个属性的key的结构是namespace.标签的id。namespace一般对应Mapper类的包名,id一般对应Mapper接口的方法名。然后会把namespace当做类名,解析对应的Mapper接口方法上的Select.class, Update.class, Insert.class, Delete.class, SelectProvider.class, UpdateProvider.class, InsertProvider.class, DeleteProvider.class对应的注解,这些注解都是用来写sql的,并且把注解对应的sql也放到mappedStatements属性中,key的结构是type.getName() + “.” + method.getName()。
    如果beanFactory中有Interceptor的实现类,则最终会被添加到Configuration的interceptorChain(拦截器链)属性中。如果beanFactory中有TypeHandler的实现类,则最终会被添加到Configuration的typeHandlerRegistry中。
  • 启动类上加了@MapperScan({“com.example.demo.mapper”})注解,这个注解上有元注解@Import(MapperScannerRegistrar.class),ConfigurationClassPostProcessor这个bfpp会处理@Import标签。MapperScannerRegistrar实现了ImportBeanDefinitionRegistrar接口,它的public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry)方法中可以从importingClassMetadata参数中获取到@MapperScan注解的basePackages的值,这个方法会向beanFactory中注册一个MapperScannerConfigurer的beanDefinition,MapperScannerConfigurer的beanDefinition中会设置annotationClass(扫描加了这个注解的Mapper接口),markerInterface(扫描继承了这个接口的Mapper接口),basePackage(扫描这个包下的接口)这几个属性,如果@MapperScan注解中annotationClass和markerInterface都没配置,则会扫描basePackage下的所有接口。MapperScannerConfigurer也是一个bfpp,它是真正负责扫描Mapper接口的。它内部是使用mybatis的ClassPathMapperScanner类来扫描的,这个类继承了ClassPathBeanDefinitionScanner,Spring处理@ComponentScan注解就是用的这个类。ClassPathMapperScanner可以添加IncludeFilter或者ExcludeFilter的,如果配置了annotationClass就会添加一个AnnotationTypeFilter的IncludeFilter,如果配置了markerInterface就添加一个AssignableTypeFilter的IncludeFilter,否则就添加一个始终返回true的IncludeFilter。
  • ClassPathMapperScanner会先调用父类ClassPathBeanDefinitionScanner的doScan()方法扫描beanDefinition,然后会把beanDefinition的beanClass替换成MapperFactoryBean这个类,这个类是一个FactoryBean。MapperFactoryBean这个类中有sqlSessionFactory,sqlSessionTemplate属性,实例化的时候会自动注入的,还会把实际的Mapper接口Class设置到mapperInterface属性上。所以beanFactory中对于Mapper接口实际生成的对象是MapperFactoryBean对象。每次调用getBean()方法时会调用MapperFactoryBean的getObject()方法获取实际的对象。
  • MapperFactoryBean的getObject()的逻辑是,先通过自己的sqlSessionTemplate属性中的sqlSessionFactory获取Configuration对象,然后通过Configuration对象的public T getMapper(Class type, SqlSession sqlSession)方法获取对应的Mapper实例。这个方法会通过前面步骤中通过解析xml文件的namespace解析的Mapper接口缓存,创建代理对象。创建代理对象的代码如下:
  public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
  //这是前面解析时,创建的缓存
    final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);
    if (mapperProxyFactory == null) {
      throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
    }
    return mapperProxyFactory.newInstance(sqlSession);
  }

代理对象的逻辑中可以通过mapperInterface和Method得到statementId,然后从Configuration中获得对应的MappedStatement,然后就知道MappedStatement的commandType, UNKNOWN, INSERT, UPDATE, DELETE, SELECT, FLUSH,根据commandType的类型调用SqlSessionTemplate不同的方法,比如如果是SELECT类型并且Method签名的返回值类型不是List,Map,Cursor,那么就调用SqlSessionTemplate的selectOne()方法,并且会把statementId也传过来。

  • SqlSessionTemplate的selectOne()方法的逻辑是:通过它的sqlSessionProxy调动对应的selectOne()方法,sqlSessionProxy的逻辑是先通过sqlSessionFactory获取底层的SqlSession,然后调用SqlSession的selectOne()方法。SqlSession内部会根据statementId,从configuration的mappedStatements缓存中获取对应的MappedStatement,然后调用Executor的query()方法执行这个语句。而通过sqlSessionFactory实例化SqlSession时,SqlSession中的Executor属性是通过Configuration的Executor newExecutor(Transaction transaction, ExecutorType executorType)方法得到的,而Configuration的这个方法内部在实例化Executor后,会调用 executor = (Executor)interceptorChain.pluginAll(executor),会生成Executor一个代理对象,后面自行executor的方法时就会调用拦截器链。
  • InterceptorChain的pluginAll方法定义如下
  public Object pluginAll(Object target) {
    for (Interceptor interceptor : interceptors) {
      target = interceptor.plugin(target);
    }
    return target;
  }

Interceptor的plugin()方法定义如下:

  default Object plugin(Object target) {
    return Plugin.wrap(target, this);
  }

Plugin.wrap的方法定义如下
Plugin本身实现了InvocationHandler接口。new Plugin它的invoke()方法的逻辑会判断

  public static Object wrap(Object target, Interceptor interceptor) {
  //这一步会解析Interceptor实现类上面的Intercepts标签,即其Signature[]属性,解析出来这个Interceptor可以拦截哪些类的哪些方法。后面执行代理类的invoke()方法时,如果目标类的目标方法不在signatureMap中,就不会进行拦截。
    Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
    Class<?> type = target.getClass();
    Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
    if (interfaces.length > 0) {
      return Proxy.newProxyInstance(
          type.getClassLoader(),
          interfaces,
          new Plugin(target, interceptor, signatureMap));
    }
    return target;
  }

Plugin的invoke方法定义如下:

  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    try {
      Set<Method> methods = signatureMap.get(method.getDeclaringClass());
      if (methods != null && methods.contains(method)) {
        return interceptor.intercept(new Invocation(target, method, args));
      }
      return method.invoke(target, args);
    } catch (Exception e) {
      throw ExceptionUtil.unwrapThrowable(e);
    }
  }
  • Executor的query()方法里面会生成BoundSql对象,判断是从缓存中获取还是从数据库获取,会prepareStatement,从ResultSet中映射数据到结果中,使用DefaultResultHandler和objectFactory生成结果对象。

  • beanFactory中的SqlSessionTemplate对象有四个属性,如下所示.

this.sqlSessionFactory = sqlSessionFactory;
    this.executorType = executorType;
    this.exceptionTranslator = exceptionTranslator;
    this.sqlSessionProxy = (SqlSession) newProxyInstance(SqlSessionFactory.class.getClassLoader(),
        new Class[] { SqlSession.class }, new SqlSessionInterceptor());

这个类的主要作用是获取SqlSession对象,用于执行sql语句。而通过SqlSessionTemplate获取SqlSession的时候实际获得的是sqlSessionProxy,这是一个代理对象。调用这个代理SqlSession对象的每个方法都会被拦截,它的InvocationHandler的拦截逻辑是先通过sqlSessionFactory获取实际的SqLSession,然后通过实际的SqLSession执行方法,然后处理事务的提交,session的关闭逻辑。这里值得借鉴的就是通过使用代理对象,用户可以不用关心如果获取SqlSession,以及关闭session,提交事务的逻辑。

Springboot

SpringBootApplication注解

SpringBootApplication注解主要有三个元注解,分别是

@SpringBootConfiguration

表明一个类是Spring Boot应用程序的@Configuration的。可以用作Spring的标准@Configuration注解的替代,以便可以自动找到configuration(例如在tests中)。
应用程序应该只包含一个@SpringBootConfiguration,大多数惯用的Spring Boot应用程序都会从@SpringBootApplication继承它。

@EnableAutoConfiguration

  • 允许Spring应用程序上下文的自动配置,尝试猜测和配置您可能需要的bean。自动配置类通常基于类路径和已经定义好的bean来应用。例如,如果您的类路径上有tomcat-embedded.jar,则可能需要一个TomcatServletWebServerFactory(除非您定义了自己的ServletWebSServerFactory bean)。
  • 使用@SpringBootApplication时,上下文的自动配置会自动启用,因此添加此注解不会产生额外的效果。
  • 自动配置尝试尽可能智能化,并在您定义更多自己的配置时回退。您始终可以exclude()任何您不想应用的配置(如果您没有访问权限,请使用excludeName())。您也可以通过spring.autoconfigure.exclude属性排除它们。自动配置总是在用户定义的bean注册后应用
  • 用@EnableAutoConfiguration(通常通过@SpringBootApplication)注解的类的包具有特定的意义,通常用作“默认值”。例如,它将在扫描@Entity类时使用。通常建议您把@EnableAutoConfiguration放置在根包中,以便可以搜索所有子包和类。
    自动配置的类是常规的Spring@configuration bean。它们使用ImportCandidate和SpringFactoriesLoader机制(针对该类)定位。通常,自动配置的bean是@Conditional bean(最常用的是@ConditionalOnClass和@ConditionalOnMissingBean注解)

@EnableAutoConfiguration注解自动配置原理

  • 这个注解继承了@Import注解,@Import注解会import AutoConfigurationImportSelector,这是一个DeferredImportSelector。ConfigurationClassPostProcessor的parse()阶段会处理AutoConfigurationImportSelector。
  • 处理DeferredImportSelector的步骤是先调用AutoConfigurationImportSelector的getImportGroup()方法返回AutoConfigurationGroup.class对象,然后实例化AutoConfigurationGroup对象,然后调用AutoConfigurationGroup的void process(AnnotationMetadata metadata, DeferredImportSelector selector),这个方法内部会查询要自动配置的类,解析的配置类会保存到AutoConfigurationGroup的autoConfigurationEntries属性和entries属性中。
  • 查询自动配置的类时,分两个来源,一个是查询spring.factories文件中,key为EnableAutoConfiguration全类名的值,另一个来源是META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports中配置的类。得到一个配置类类名的集合List configurations。
  • 然后解析配置类上的@EnableAutoConfiguration注解的exclude以及excludeName以及Environment中的spring.autoconfigure.exclude属性值中配置的排除的类,来排除一些配置类。
  • 然后解析spring.factories文件中key为AutoConfigurationImportFilter全类名的值,配置的是逗号分隔的AutoConfigurationImportFilter,然后遍历调用AutoConfigurationImportFilter的match()方法,过滤配置类。
  • 上面的步骤扫描完配置类集合后,然后调用AutoConfigurationImportSelector的public Iterable selectImports()方法获取配置类集合。这个方法主要目的是处理自动导入的配置类的排序,先根据类名的字母排序,然后根据类上的@AutoConfigureOrder注解的值排序。然后按@AutoConfigureBefore @AutoConfigureAfter排序。排序后返回配置类的名称集合。
  • 然后按顺序遍历配置类名称集合,递归调用processImports()方法,因为这些配置类上可能还会有@Import注解需要处理。
  • 所以@AutoConfigureBefore @AutoConfigureAfter和@AutoConfigureOrder注解只对自动导入的配置类有效。

ConditionalOnBean,ConditionalOnMissingBean,ConditionalOnSingleCandidate原理

  • ConditionalOnBean,ConditionalOnMissingBean,ConditionalOnSingleCandidate这些注解可以加在类上和方法上,所以有两个地方处理。
  • 处理类上的这种注解,是在ConfigurationClassPostProcessor的parse()阶段处理。处理@Bean方法上的Conditional注解是在ConfigurationClassPostProcessor的loadBeanDefinitions()阶段处理。
  • 不管是处理类上还是@bean方法上的注解,都是使用ConditionEvaluator的public boolean shouldSkip(@Nullable AnnotatedTypeMetadata metadata, @Nullable ConfigurationPhase phase)方法处理的。所以处理逻辑都是一样的。这个方法的phase参数的值,在ConfigurationClassPostProcessor的parse()阶段传的是PARSE_CONFIGURATION,在loadBeanDefinitions()阶段传的是REGISTER_BEAN。
  • 这个方法内部会获取配置类或方法上的@Conditiononal元注解的值,也就是一个Condition数组。然后就遍历这个Condition数组判断。遍历的逻辑是: 如果是ConfigurationCondition,并且getConfigurationPhase()返回了值,则阶段和参数传的phase要相同。然后再判断Condition的matches()方法判断,matches()方法是一个抽象方法需要由子类实现。
  • @ConditionalOnBean注解上就有@Conditional(OnBeanCondition.class)
    元注解。OnBeanCondition就实现了Condition接口,它的matches()方法会获取@ConditionalOnBean注解的Class<?>[] value()属性值,Class<? extends Annotation>[] annotation()属性值,String[] name()属性值。然后去beanFactory中查找beanDefinitions,有任何一个不存在,则认为不满足,就不会加载这个配置类。

@EnableConfigurationProperties

  • EnableConfigurationProperties注解上,加了@Import标签。
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(EnableConfigurationPropertiesRegistrar.class)
public @interface EnableConfigurationProperties {
  • EnableConfigurationPropertiesRegistrar实现了ImportBeanDefinitionRegistrar接口,ConfigurationClassPostProcessor的loadBeanDefinition()阶段会调用它的registerBeanDefinitions()方法。
  • registerBeanDefinitions()方法会注册ConfigurationPropertiesBinder的beanDefinition,这个类实现了ApplicationContextAware接口,实例化的时候会获取Environment中的propertySources属性值。
  • registerBeanDefinitions()方法会注册ConfigurationPropertiesBindingPostProcessor,这是一个bpp,它的postProcessBeforeInitialization()方法,会为bean填充属性值。
  • registerBeanDefinitions()方法会获取@EnableConfigurationProperties注解的Class<?>[] value() 属性,并且注册这些属性的beanDefinition。并且会校验这些Class上都有ConfigurationProperties注解。
  • ConfigurationPropertiesBindingPostProcessor填充属性值的逻辑是:如果bean上有ConfigurationProperties注解,才处理。遍历bean的属性,用ConfigurationProperties注解的prefix属性值 + 属性名,从ConfigurationPropertiesBinder中的propertySources解析属性值,填充到bean中。

spring kafka项目集成kafka原理

  • springboot自动加载配置时,会加载spring-boot-autoconfigure模块中的org.springframework.boot.autoconfigure.AutoConfiguration.imports文件中的类,这里面就配置了KafkaAutoConfiguration配置类。这个类有一系列的@Bean方法,会注册KafkaTemplate,DefaultKafkaConsumerFactory,DefaultKafkaProducerFactory等组件。
@AutoConfiguration
@ConditionalOnClass(KafkaTemplate.class)
@EnableConfigurationProperties(KafkaProperties.class)
@Import({ KafkaAnnotationDrivenConfiguration.class, KafkaStreamsAnnotationDrivenConfiguration.class })
public class KafkaAutoConfiguration {
  • KafkaAnnotationDrivenConfiguration这个类会注册ConcurrentKafkaListenerContainerFactory组件。
  • 想要启用对@KafkaListener的支持,要使用@EnableKafka注解。这个注解会import KafkaListenerConfigurationSelector,而KafkaListenerConfigurationSelector实现了DeferredImportSelector接口,会import KafkaBootstrapConfiguration配置类。而KafkaBootstrapConfiguration实现了ImportBeanDefinitionRegistrar接口,会注册KafkaListenerAnnotationBeanPostProcessor和KafkaListenerEndpointRegistry。
  • KafkaListenerAnnotationBeanPostProcessor是一个bpp,会处理带@KafkaListener注解的类和方法。并且解析注解中的topic,partition等属性,最后使用KafkaListenerEndpointRegistry注册这些endpoint。
  • 注册endpoint的时候,会创建listenerContainer,并且启动一个线程,循环从kafka拉取数据。如果处理数据成功,就会commit offset,下次从下个offset拉取,如果抛异常了,则回退到之前的offset,然后重试获取数据,默认重试九次。

Apolloconfig

阿波罗配置中心设计

面试题总结

Config Service

  • 提供配置获取接口
  • 提供配置更新推送接口(基于Http long polling)
    • 服务端使用Spring DeferredResult实现异步化,从而大大增加长连接数量
    • 目前使用的tomcat embed默认配置是最多10000个连接(可以调整),使用了4C8G的虚拟机实测可以支撑10000个连接,所以满足需求(一个应用实例只会发起一个长连接)。
  • 接口服务对象为Apollo客户端

Admin Service

  • 提供配置管理接口
  • 提供配置修改、发布等接口
  • 接口服务对象为Portal

Meta Server

  • Portal通过域名访问Meta Server获取Admin Service服务列表(IP+Port)
  • Client通过域名访问Meta Server获取Config Service服务列表(IP+Port)
  • Meta Server从Eureka获取Config Service和Admin Service的服务信息,相当于是一个Eureka Client
  • 增设一个Meta Server的角色主要是为了封装服务发现的细节,对Portal和Client而言,永远通过一个Http接口获取Admin Service和Config Service的服务信息,而不需要关心背后实际的服务注册和发现组件
  • Meta Server只是一个逻辑角色,在部署时和Config Service是在一个JVM进程中的,所以IP、端口和Config Service一致

Eureka

  • 基于Eureka和Spring Cloud Netflix提供服务注册和发现
  • Config Service和Admin Service会向Eureka注册服务,并保持心跳
  • 为了简单起见,目前Eureka在部署时和Config Service是在一个JVM进程中的(通过Spring Cloud Netflix)

Portal

  • 提供Web界面供用户管理配置
  • 通过Meta Server获取Admin Service服务列表(IP+Port),通过IP+Port访问服务
  • 在Portal侧做load balance、错误重试

Client

  • Apollo提供的客户端程序,为应用提供配置获取、实时更新等功能
  • 通过Meta Server获取Config Service服务列表(IP+Port),通过IP+Port访问服务
  • 在Client侧做load balance、错误重试

部署

为了简化部署,我们实际上会把Config Service、Eureka和Meta Server三个逻辑角色部署在同一个JVM进程中

配置更新实时推送原理

在配置中心中,一个重要的功能就是配置发布后实时推送到客户端。下面我们简要看一下这块是怎么设计实现的。
面试题总结

发送ReleaseMessage的实现方式

  • admin Service在配置发布后,需要通知所有的Config Service有配置发布,从而Config Service可以通知对应的客户端来拉取最新的配置。
  • 从概念上来看,这是一个典型的消息使用场景,Admin Service作为producer发出消息,各个Config Service作为consumer消费消息。通过一个消息组件(Message Queue)就能很好的实现Admin Service和Config Service的解耦。
  • 在实现上,考虑到Apollo的实际使用场景,以及为了尽可能减少外部依赖,我们没有采用外部的消息中间件,而是通过数据库实现了一个简单的消息队列。
  • 实现方式如下:
    1. Admin Service在配置发布后会往ReleaseMessage表插入一条消息记录,消息内容就是配置发布的AppId+Cluster+Namespace,参见DatabaseMessageSender
    2. Config Service有一个线程会每秒扫描一次ReleaseMessage表,看看是否有新的消息记录,参见ReleaseMessageScanner
    3. Config Service如果发现有新的消息记录,那么就会通知到所有的消息监听器(ReleaseMessageListener),如NotificationControllerV2,消息监听器的注册过程参见ConfigServiceAutoConfiguration
    4. NotificationControllerV2得到配置发布的AppId+Cluster+Namespace后,会通知对应的客户端

Config Service通知客户端的实现方式

  • 上一节中简要描述了NotificationControllerV2是如何得知有配置发布的,那NotificationControllerV2在得知有配置发布后是如何通知到客户端的呢?
  • 实现方式如下:
    1. 客户端会发起一个Http请求到Config Service的notifications/v2接口,也就是NotificationControllerV2,参见RemoteConfigLongPollService
      NotificationControllerV2不会立即返回结果,而是通过Spring DeferredResult把请求挂起
    2. 如果在60秒内没有该客户端关心的配置发布,那么会返回Http状态码304给客户端
    3. 如果有该客户端关心的配置发布,NotificationControllerV2会调用DeferredResult的setResult方法,传入有配置变化的namespace信息,同时该请求会立即返回。客户端从返回的结果中获取到配置变化的namespace后,会立即请求Config Service获取该namespace的最新配置。
    4. 客户端在收到服务端请求后会立即重新发起连接,回到第一步

客户端设计

面试题总结

  • 上图简要描述了Apollo客户端的实现原理:
    1. 客户端和服务端保持了一个长连接,从而能第一时间获得配置更新的推送。
    2. 客户端还会定时从Apollo配置中心服务端拉取应用的最新配置。
      • 这是一个fallback机制,为了防止推送机制失效导致配置不更新
      • 客户端定时拉取会上报本地版本,所以一般情况下,对于定时拉取的操作,服务端都会返回304 - Not Modified
      • 定时频率默认为每5分钟拉取一次,客户端也可以通过在运行时指定System Property: apollo.refreshInterval来覆盖,单位为分钟。
    3. 客户端从Apollo配置中心服务端获取到应用的最新配置后,会保存在内存中
    4. 客户端会把从服务端获取到的配置在本地文件系统缓存一份
      在遇到服务不可用,或网络不通的时候,依然能从本地恢复配置
    5. 应用程序从Apollo客户端获取最新的配置、订阅配置更新通知

和Spring集成的原理

  • Apollo除了支持API方式获取配置,也支持和Spring/Spring Boot集成,集成原理简述如下。
  • Spring从3.1版本开始增加了ConfigurableEnvironment和PropertySource:
    • ConfigurableEnvironment
      • Spring的ApplicationContext会包含一个Environment(实现ConfigurableEnvironment接口)
      • ConfigurableEnvironment自身包含了很多个PropertySource
  • 需要注意的是,PropertySource之间是有优先级顺序的,如果有一个Key在多个property source中都存在,那么在前面的property source优先
  • 在理解了上述原理后,Apollo和Spring/Spring Boot集成的手段就呼之欲出了:在应用启动阶段,Apollo从远端获取配置,然后组装成PropertySource并插入到第一个即可。
  • @EnableApolloConfig注解中有@Import元注解,会import ApolloConfigRegistrar类,这个类实现了ImportBeanDefinitionRegistrar接口,它的public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry)方法会注册PropertySourcesProcessor的beanDefinition,并且会把@EnableApolloConfig注解中配置的namespaces,存储到PropertySourcesProcessor的静态变量NAMESPACE_NAMES中,而PropertySourcesProcessor是一个bfpp,它会遍历NAMESPACE_NAMES中的namespace,使用ConfigService加载配置,加载的配置都放到CompositePropertySource中,并且放到Environment的第一个位置,名称是ApolloPropertySources。PropertySourcesProcessor还会向每个namespace对应的Config对象添加AutoUpdateConfigChangeListener监听器,在apollo配置发生变化时,可以实时得到更新。

实时更新bean中的@Value属性值的原理

  • ApolloConfigRegistrar会注册一个SpringValueProcessor类,这是一个bpp, 它的public Object postProcessBeforeInitialization(Object bean, String beanName)方法会提取bean中的每一个@Value属性,并且把创建一个new SpringValue(key, value.value(), bean, beanName, field, false)对象,其中key就是${}中包含的占位符,注意一个@Value值中可以提取出多个占位符,value.value()就是@Value注解的值,然后会把每个SpringValue对象通过springValueRegistry注册,保存在SpringValueRegistry中。SpringValueRegistry使用了google的guice框架,这是一个DI框架,和Spring类似,具有依赖注入的功能。所以SpringValueRegistry在其他任何地方获取都是单例的。
  • 而AutoUpdateConfigChangeListener监听器每次监听到ConfigChangeEvent变化时,就会遍历变化的key,然后调用springValueRegistry.get(beanFactory, key)的方法,就可以拿到所有引用了这个key的SpringValue集合。然后遍历SpringValue集合,对每个SpringValue中的value.value()值进行placeholder的解析以及类型转换得到最新的值。然后从SpringValue对象中获取bean以及filed,通过反射把最新的值设置到bean中。

Namespace

  • Namespace的获取权限分为两种:
    • private (私有的)
    • public (公共的)
      这里的获取权限是相对于Apollo客户端来说的。
  • Namespace类型有三种:
    • 私有类型, 具有私有权限
    • 公共类型,公共类型的Namespace具有public权限。公共类型的Namespace相当于游离于应用之外的配置,且通过Namespace的名称去标识公共Namespace,所以公共的Namespace的名称必须全局唯一。
    • 关联类型(继承类型),关联类型又可称为继承类型,关联类型具有private权限。关联类型的Namespace继承于公共类型的Namespace,用于覆盖公共Namespace的某些配置。

serviceLoader机制

  • apollo中使用了java自带的ServiceLoader机制来加载一些类。
  • service是指一个接口或者一个抽象类。而service provider是指接口或抽象类的实现类。
  • 当调用ServiceLoader.load(clazz)方法时,会搜索META-INF/services/目录下以clazz的全类名命名的文件,加载文件中的的类,并且实例化它们,然后返回它们的实例。这些文件中的类必须要有一个无参的构造函数。
  • 使用场景: 阿波罗的ApolloConfigRegistrar注册beanDefinition的时候,就是委托的ApolloConfigRegistrarHelper类,而ApolloConfigRegistrarHelper就是通过ServiceLoader机制获取的。

canal源码流程

seata

seata支持模式

  • Seata 将为用户提供了 AT、TCC、SAGA 和 XA 事务模式

AT模式

  • 前提
    • 基于支持本地 ACID 事务的关系型数据库。
    • Java 应用,通过 JDBC 访问数据库。
  • 整体机制
    • 两阶段提交协议的演变:
    • 一阶段:业务数据和回滚日志记录在同一个本地事务中提交,释放本地锁和连接资源。
    • 二阶段:提交异步化,非常快速地完成。回滚通过一阶段的回滚日志进行反向补偿

TCC 模式

  • TCC 模式,不依赖于底层数据资源的事务支持:
    • 一阶段 prepare 行为:调用 自定义 的 prepare 逻辑。
    • 二阶段 commit 行为:调用 自定义 的 commit 逻辑。
    • 二阶段 rollback 行为:调用 自定义 的 rollback 逻辑。
  • 所谓 TCC 模式,是指支持把 自定义 的分支事务纳入到全局事务的管理中。

Saga 模式

  • Saga模式是SEATA提供的长事务解决方案,在Saga模式中,业务流程中每个参与者都提交本地事务,当出现某一个参与者失败则补偿前面已经成功的参与者,一阶段正向服务和二阶段补偿服务都由业务开发实现。不需要获取全局锁的步骤
  • 适用场景
    • 业务流程长、业务流程多
    • 参与者包含其它公司或遗留系统服务,无法提供 TCC 模式要求的三个接口
  • 优势:
    • 一阶段提交本地事务,无锁,高性能
      -事件驱动架构,参与者可异步执行,高吞吐
    • 补偿服务易于实现
  • 缺点
    • 不保证隔离性(应对方案见用户文档)

seata中的几个角色

面试题总结

  1. TC (Transaction Coordinator) - 事务协调者
  • 维护全局和分支事务的状态,驱动全局事务提交或回滚。
  1. TM (Transaction Manager) - 事务管理器
  • 定义全局事务的范围:开始全局事务、提交或回滚全局事务。
  1. RM (Resource Manager) - 资源管理器
  • 管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。

seata的AT模式执行流程以及如何避免脏读和脏写

一阶段

  • 解析 SQL:得到 SQL 的类型(UPDATE),表(product),条件(where name = ‘TXC’)等相关的信息。
  • 查询前镜像:根据解析得到的条件信息,生成查询语句,定位数据。(select for update,因此此时获得本地锁)
  • 执行业务 SQL:更新这条记录的 name 为 ‘GTS’。
  • 查询后镜像:根据前镜像的结果,通过 主键 定位数据。
  • 插入回滚日志:把前后镜像数据以及业务 SQL 相关的信息组成一条回滚日志记录,插入到 UNDO_LOG 表中。
  • 提交前,向 TC 注册分支:申请 product 表中,主键值等于 1 的记录的 全局锁 。
  • 本地事务提交:业务数据的更新和前面步骤中生成的 UNDO LOG 一并提交。
  • 将本地事务提交的结果上报给 TC。

二阶段-回滚

  • 收到 TC 的分支回滚请求,开启一个本地事务,执行如下操作。
  • 通过 XID 和 Branch ID 查找到相应的 UNDO LOG 记录。
  • 数据校验:拿 UNDO LOG 中的后镜与当前数据进行比较,如果有不同,说明数据被当前全局事务之外的动作做了修改。这种情况,需要根据配置策略来做处理,详细的说明在另外的文档中介绍。
  • 根据 UNDO LOG 中的前镜像和业务 SQL 的相关信息生成并执行回滚的语句
  • 提交本地事务。并把本地事务的执行结果(即分支事务回滚的结果)上报给 TC。

二阶段-提交

  • 收到 TC 的分支提交请求,把请求放入一个异步任务的队列中,马上返回提交成功的结果给 TC。
  • 异步任务阶段的分支提交请求将异步和批量地删除相应 UNDO LOG 记录。

seata如何支持微服务间的事务传播

  • Seata 的事务上下文由 RootContext 来管理。
  • 应用开启一个全局事务后,RootContext 会自动绑定该事务的 XID,事务结束(提交或回滚完成),RootContext 会自动解绑 XID。

事务传播

  • Seata 全局事务的传播机制就是指事务上下文的传播,根本上,就是 XID 的应用运行时的传播方式。
  • 服务内部的事务传播
    • 默认的,RootContext 的实现是基于 ThreadLocal 的,即 XID 绑定在当前线程上下文中。
      -所以服务内部的 XID 传播通常是天然的通过同一个线程的调用链路串连起来的。默认不做任何处理,事务的上下文就是传播下去的。
  • 跨服务调用的事务传播
    • 跨服务调用场景下的事务传播,本质上就是要把 XID 通过服务调用传递到服务提供方,并绑定到 RootContext 中去。
    • 只要能做到这点,理论上 Seata 可以支持任意的微服务框架。

Seata事务隔离

  • 本文目标:帮助用户明白使用Seata AT模式时,该如何正确实现事务隔离,防止脏读脏写。
  • 首先请看这样的一段代码,尽管看着“初级”,但持久层框架实际上帮我们做的主要事情也就这样。
@Service
public class StorageService {

    @Autowired
    private DataSource dataSource;

    @GlobalTransactional
    public void batchUpdate() throws SQLException {
        Connection connection = null;
        PreparedStatement preparedStatement = null;
        try {
            connection = dataSource.getConnection();
            connection.setAutoCommit(false);
            String sql = "update storage_tbl set count = ?" +
                "    where id = ? and commodity_code = ?";
            preparedStatement = connection.prepareStatement(sql);
            preparedStatement.setInt(1, 100);
            preparedStatement.setLong(2, 1);
            preparedStatement.setString(3, "2001");
            preparedStatement.executeUpdate();
            connection.commit();
        } catch (Exception e) {
            throw e;
        } finally {
            IOutils.close(preparedStatement);
            IOutils.close(connection);
        }
    }
}

从代理数据源说起

  • 使用AT模式,最重要的事情便是代理数据源,那么用DataSourceProxy代理数据源有什么作用呢?
  • DataSourceProxy能帮助我们获得几个重要的代理对象
    • 通过DataSourceProxy.getConnection()获得ConnectionProxy
    • 通过ConnectionProxy.prepareStatement(…)获得StatementProxy
  • Seata的如何实现事务隔离,就藏在这2个Proxy中,我先概述下实现逻辑。
  • StatementProxy.executeXXX()的处理逻辑
    • 当调用io.seata.rm.datasource.StatementProxy.executeXXX()会将sql交给io.seata.rm.datasource.exec.ExecuteTemplate.execute(…)处理。
      • ExecuteTemplate.execute(…)方法中,Seata根据不同dbType和sql语句类型使用不同的Executer,调用io.seata.rm.datasource.exec.Executer类的execute(Object… args)。
      • 如果选了DML类型Executer,主要做了以下事情:
        • 查询前镜像(select for update,因此此时获得本地锁)
        • 执行业务sql
        • 查询后镜像
        • 准备undoLog
      • 如果你的sql是select for update则会使用SelectForUpdateExecutor(Seata代理了select for update),代理后处理的逻辑是这样的:
        • 先执行 select for update(获取数据库本地锁)
        • 如果处于@GlobalTransactional or @GlobalLock,检查是否有全局锁
        • 如果有全局锁,则未开启本地事务下会rollback本地事务,再重新争抢本地锁和全局锁,以此类推,除非拿到全局锁
  • ConnectionProxy.commit()的处理逻辑
  • 处于全局事务中(即,数据持久化方法带有@GlobalTransactional)
    • 注册分支事务,获取全局锁
    • undoLog数据入库
    • 让数据库commit本次事务
  • 处于@GlobalLock中(即,数据持久化方法带有@GlobalLock)
    • 向tc查询是否有全局锁存在,如存在,则抛出异常
    • 让数据库commit本次事务
  • 除了以上情况(else分支)
    • 让数据库commit本次事务

如何防止脏写

  • 假设你的业务代码是这样的:
    • updateAll()用来同时更新A和B表记录,updateA() updateB()则分别更新A、B表记录
    • updateAll()已经加上了@GlobalTransactional
      面试题总结

办法一:updateA()也加上@GlobalTransactional,此时Seata会如何保证事务隔离?

class DbServiceA {

    @GlobalTransactional
    @Transactional
    public boolean updateA(DTO dto) {
        serviceA.update(dto.getA());
    }
}

办法二: @GlobalLock + select for update

class DbServiceA {
    
    @GlobalLock
    @Transactional
    public boolean updateA(DTO dto) {
        serviceA.selectForUpdate(dto.getA());
        serviceA.update(dto.getA());
    }
}
  • 一定有人会问,“这里为什么要加上select for update? 只用@GlobalLock能不能防止脏写?” 能。但请再回看下上面的图,select for update能带来这么几个好处:
    • 锁冲突更“温柔”些。如果只有@GlobalLock,检查到全局锁,则立刻抛出异常,也许再“坚持”那么一下,全局锁就释放了,抛出异常岂不可惜了。
    • 在updateA()中可以通过select for update获得最新的A,接着再做更新。

如何防止脏读?

select for update + @GlobalLock。

spring事务

该类定义了7中传播行为的常量值:

  1. int PROPAGATION_REQUIRED = 0;
    支持当前事务,如果当前有事务就使用当前的事务;如果不存在,则创建一个新的。类似于同名的EJB事务属性。
    这通常是一个transaction definition的默认设置,并且通常会定义一个transaction synchronization scope。
  2. int PROPAGATION_SUPPORTS = 1;
    支持当前事务,如果当前有事务就使用当前的事务;如果不存在,则非事务性执行。类似于同名的EJB事务属性。
  3. int PROPAGATION_MANDATORY = 2;
    支持当前事务;如果不存在当前事务,则抛出异常。类似于同名的EJB事务属性。
    请注意,PROPAGATION_MANDORY scope内的transaction synchronization将始终由外围的事务驱动。
  4. int PROPAGATION_REQUIRES_NEW = 3;
    不支持当前事务,如果存在,则挂起。始终创建一个新事务。类似于同名的EJB事务属性。
    注意:实际事务暂停不是在所有transaction managers中都是可用的。这尤其适用于org.springframework.transaction.jta.JtaTransactionManager,它要求javax.transaction.TransactionManager对其可用(在标准Java EE中是特定于服务器的)。
    PROPAGATION_REQUIRES_NEW scope始终定义自己的transaction synchronizations。现有synchronizations将被暂停并在后面恢复。
  5. int PROPAGATION_NOT_SUPPORTED = 4;
    不支持当前事务,如果存在,则挂起。始终以非事务方式执行。类似于同名的EJB事务属性。
    注意:实际事务暂停不是在所有transaction managers中都是可用的。这尤其适用于org.springframework.transaction.jta.JtaTransactionManager,它要求javax.transaction.TransactionManager对其可用(在标准Java EE中是特定于服务器的)。
    请注意,transaction synchronizations在PROPAGATION_not_SUPPORTED scope内不可用。现有synchronizations将被暂停并在后续恢复
  6. int PROPAGATION_NEVER = 5;
    不支持当前事务,如果存在,则抛异常。类似于同名的EJB事务属性。
    请注意,transaction synchronizations在PROPAGATION_NEVER scope内不可用
  7. int PROPAGATION_NESTED = 6;
    如果当前事务存在,则在嵌套事务中执行。否则执行类似PROPAGATION_REQUIRED的行为,创建一个新的事务。EJB中没有类似的特性。
    注意:嵌套事务的实际创建仅适用于特定的事务管理器。在使用JDBC 3.0驱动程序时,这只适用于JDBC org.springframework.JDBC.datasource.DataSourceTransactionManager。一些JTA的事务管理器可能也支持嵌套事务。

该类定义了五种隔离级别

  1. int ISOLATION_DEFAULT = -1;
    使用底层数据库的默认隔离级别。所有其他的隔离级别都对应于JDBC隔离级别。
  2. int ISOLATION_READ_UNCOMMITTED = 1; // same as java.sql.Connection.TRANSACTION_READ_UNCOMMITTED
    此隔离级别可能发生dirty reads(脏读), non-repeatable reads(不可重复读) and phantom reads(幻读)。
    此隔离级别允许一个事务对行的更改在提交之前被另一个事务读取(“dirty read”)。如果回滚了更改,则第二个事务将检索到无效的行
  3. int ISOLATION_READ_COMMITTED = 2; // same as java.sql.Connection.TRANSACTION_READ_COMMITTED
    此隔离级别可以防止脏读;可能发生不可重复读和幻读。
    此隔离级别仅禁止读取有更改还未提交的行
  4. int ISOLATION_REPEATABLE_READ = 4; // same as java.sql.Connection.TRANSACTION_REPEATABLE_READ
    此隔离级别可以防止脏读和不可重复读;但是可能发生幻读。
    该隔离级别禁止事务读取未提交更改的行,也禁止这种场景: 一个事务读取行,第二个事务更改行,第一个事务重新读取行,在第二次读取时获得不同的值(“不可重复读取”)
  5. int ISOLATION_SERIALIZABLE = 8; // same as java.sql.Connection.TRANSACTION_SERIALIZABLE
    此隔离级别表示禁止脏读、不可重复读和幻读。
    该级别包括ISOLATION_REPEATABLE_READ隔离级别中的禁止,并进一步禁止这种场景: 一个事务读取满足where条件的所有行,第二个事务插入满足该where条件的行,第一个事务针对相同条件重新读取,检索第二次读取中的附加“幻象”行。

mysql索引优化

  • mysql架构
    面试题总结

  • mysql并发控制
    只要多线程在同一时刻操作同一条数据,就会有并发问题。mysql使用读写锁来防止并发。读锁可以共享,写锁是排他的。
    面试题总结

  • mysql事务
    ![-![](https://img-blog.csdnimg.cn/7548ce27dc6f4d77b97089b35c0073c2.png)

  • 死锁
    面试题总结

  • 自动提交
    面试题总结

  • MVCC 多版本并发控制
    面试题总结
    MVCC原理

  • 索引类型

    • normal 普通索引
    • unique 唯一索引
    • fulltext 全文索引
    • spatial 空间索引 (只有MYISAM引擎可以用)
  • 索引的实现数据结构

    • b±tree
    • hash(只有myisam引擎支持)
  • 索引失效的情况

    • 不符合最左前缀原则,where条件中的条件必须从最左边的一个索引开始
    • 范围查询后面的索引字段不会生效,如like
    • 非独立使用的字段不会使用索引,如函数中的参数
  • 索引优化

    • 索引长字符列的场景,使用前缀索引,并通过计算索引选择性确定索引的前缀长度,这样会降低索引空间,提高索引效率。
    • 使用索引覆盖查询,可以避免回表。explain命令返回的extra列的using index就表示使用了覆盖索引。下面是对于select * 类型语句优化使用覆盖索引的例子。
      面试题总结
    • 使用索引排序。当explain返回的extra列中没有using filesort时,就说明使用了索引排序。
      面试题总结
    • 索引优化案例
      面试题总结
  • 索引和锁
    只要存储引擎返回了的数据就会上锁。当在mysql服务器端应用where条件过滤之后才会释放锁。也就是说即使有些行在服务器端被where过滤掉了没有返回给客户端,但只要存储引擎查出了这些行,就会被锁定。所以如果索引能够完美的过滤出需要的行,那么就可以减少一些行锁。从而提高并发。当explain命令返回的extra列中有using where时说明没有使用存储引擎返回的数据在服务器端应用了where条件过滤。

为什么mysql索引使用B+树

为什么MySQL采用B+树作为索引

  • MySQL 是会将数据持久化在硬盘,而存储功能是由 MySQL 存储引擎实现的,所以讨论 MySQL 使用哪种数据结构作为索引,实际上是在讨论存储引擎使用哪种数据结构作为索引,InnoDB 是 MySQL 默认的存储引擎,它就是采用了 B+ 树作为索引的数据结构。

  • 要设计一个 MySQL 的索引数据结构,不仅仅考虑数据结构增删改的时间复杂度,更重要的是要考虑磁盘 I/0 的操作次数。因为索引和记录都是存放在硬盘,硬盘是一个非常慢的存储设备,我们在查询数据的时候,最好能在尽可能少的磁盘 I/0 的操作次数内完成。

  • 二分查找树虽然是一个天然的二分结构,能很好的利用二分查找快速定位数据,但是它存在一种极端的情况,每当插入的元素都是树内最大的元素,就会导致二分查找树退化成一个链表,此时查询复杂度就会从 O(logn)降低为 O(n)。

  • 为了解决二分查找树退化成链表的问题,就出现了自平衡二叉树,保证了查询操作的时间复杂度就会一直维持在 O(logn) 。自平衡二叉树中每个节点的左右子树高度差不能超过1。但是它本质上还是一个二叉树,每个节点只能有 2 个子节点,随着元素的增多,树的高度会越来越高。

  • 而树的高度决定于磁盘 I/O 操作的次数,因为树是存储在磁盘中的,访问每个节点,都对应一次磁盘 I/O 操作,也就是说树的高度就等于每次查询数据时磁盘 IO 操作的次数,所以树的高度越高,就会影响查询性能。

  • 为了解决降低树的高度的问题,后面就出来了 B 树,它不再限制一个节点就只能有 2 个子节点,而是允许 M 个子节点 (M>2),从而降低树的高度。B 树的每一个节点最多可以包括 M 个子节点,M 称为 B 树的阶,所以 B 树就是一个多叉树。假设 M = 3,那么就是一棵 3 阶的 B 树,特点就是每个节点最多有 2 个(M-1个)数据和最多有 3 个(M个)子节点,超过这些要求的话,就会分裂节点。

  • B树和B+树的区别

    • 叶子节点(最底部的节点)才会存放实际数据(索引+记录),非叶子节点只会存放索引;
    • 所有索引都会在叶子节点出现,叶子节点之间构成一个有序链表;
      非叶子节点的索引也会同时存在在子节点中,并且是在子节点中所有索引的最大(或最小)。
    • 非叶子节点中有多少个子节点,就有多少个索引;
      面试题总结
  • B 树和 B+ 都是通过多叉树的方式,会将树的高度变矮,所以这两个数据结构非常适合检索存于磁盘中的数据。

  • 但是 MySQL 默认的存储引擎 InnoDB 采用的是 B+ 作为索引的数据结构,原因有:

    • B+ 树的非叶子节点不存放实际的记录数据,仅存放索引,因此数据量相同的情况下,相比存储即存索引又存记录的 B 树,B+树的非叶子节点可以存放更多的索引,因此 B+ 树可以比 B 树更「矮胖」,查询底层节点的磁盘 I/O次数会更少。
    • B+ 树有大量的冗余节点(所有非叶子节点都是冗余索引),这些冗余索引让 B+ 树在插入、删除的效率都更高,比如删除根节点的时候,不会像 B 树那样会发生复杂的树的变化;
    • B+ 树叶子节点之间用链表连接了起来,有利于范围查询,而 B 树要实现范围查询,因此只能通过树的遍历来完成范围查询,这会涉及多个节点的磁盘 I/O 操作,范围查询效率不如 B+ 树。

mysql数据存储方式

从数据页的角度看B+树

kafka为什么这么快

参考链接

  • 读写的时候都是顺序读写

  • producter写入的时候写到page cace中就返回成功了,操作系统定时刷新page cache中的数据到磁盘,所以写入的时候和写内存一样。
    面试题总结

  • consumer读取的时候通过零拷贝,先到page cache中查找数据是否存在,如果不存在则将数据从磁盘读取到page cache中,然后把数据从page cache中通过内核的sendfile命令直接拷贝到socket buffer中,然后通过网卡发送出去。两次拷贝都在内核态完成,不用经过用户态,省去了用户态和内核态的两次转换。所以很快。
    面试题总结

  • 读取数据的时候,会先判断page cache中是否存在,存在就可以直接从page cache中消费,所以消费实时数据就会速度快很多。但是消费历史数据就不得不将历史数据重新加载到page cache,而且会污染掉page cache。PageCache技术在加载历史数据的时候,还会将你加载的数据块的临近的其他数据块也一起加载到PageCache里去,这其实就是一个预读过程,对于需要连续读取历史数据的,也是性能的不小优化。
    面试题总结

  • Mmap即是Memory Mapped Files内存文件映射,mmap其实就是把物理上的磁盘文件的一些地址和page cache地址进行一层映射。

  • 很多情况下,系统的瓶颈不是cpu,内存,磁盘io,而是网络IO。为了减少网络通信的次数,往往需要批量发送,kafka支持。batch.size消息条数积累到该阈值,立即发送。linger.ms不管消息有没有积累足够条数,超过该时间就立即发送。并且kafka发送消息的时候会批量压缩消息。

  • 分布式存储设计,消费者可以分布式消费数据。

    • topic进行分区 -->partition
    • partition为了方便超时删除等管理,又进一步划分segment
    • 每个saement.又包括了index文件和 log 文件,可以二分查找快速定位数据.
    • segment 数据只允许追加的形式.
    • offset是连续的支持预读和批量写.
  • 在 Kafka 中,大量使用了 PageCache, 这也是 Kafka 能实现高吞吐的重要因素之一, 当一个进程准备读取磁盘上的文件内容时,操作系统会先查看待读取的数据页是否在 PageCache 中,如果命中则直接返回数据,从而避免了对磁盘的 I/O 操作;如果没有命中,操作系统则会向磁盘发起读取请求并将读取的数据页存入 PageCache 中,之后再将数据返回给进程。同样,如果一个进程需要将数据写入磁盘,那么操作系统也会检查数据页是否在页缓存中,如果不存在,则 PageCache 中添加相应的数据页,最后将数据写入对应的数据页。被修改过后的数据页也就变成了脏页,操作系统会在合适的时间把脏页中的数据写入磁盘,以保持数据的一致性。

kafka存储机制

参考资料

  • 一个topic有多个partition,一个partition在文件系统中表现为一个目录,目录的名称为topic名称-partition编号。每个目录下有多个logSegment,logSegment是一个逻辑上的概念,被称为日志分段,是为了防止一个日志文件过大查找慢。每个LogSegment又包含.index文件、.log文件和.timeind文件。这几个文件的文件名称都是以LogSegment中的第一条记录的offset值来命名的。“.index” 文件存储大量的索引信息 , “.log” 文件存储大量的数据,索引文件中的元数据指向对应数据文件中 Message 的物理偏移量。
  • kafka 中消息是以主题 Topic 为基本单位进行归类的,这里的 Topic 是逻辑上的概念,实际上在磁盘存储是根据分区 Partition 存储的, 即每个 Topic 被分成多个 Partition,分区 Partition 的数量可以在主题 Topic 创建的时候进行指定。
  • Partition 分区主要是为了解决 Kafka 存储的水平扩展问题而设计的, 如果一个 Topic 的所有消息都只存储到一个 Kafka Broker上的话, 对于 Kafka 每秒写入几百万消息的高并发系统来说,这个 Broker 肯定会出现瓶颈, 故障时候不好进行恢复,所以 Kafka 将 Topic 的消息划分成多个 Partition, 然后均衡的分布到整个 Kafka Broker 集群中。
  • Partition 分区内每条消息都会被分配一个唯一的消息 id,即我们通常所说的偏移量 Offset, 因此 kafka 只能保证每个分区内部有序性,并不能保证全局有序性。
  • 然后每个 Partition 分区又被划分成了多个 LogSegment,这是为了防止 Log 日志过大,Kafka 又引入了日志分段(LogSegment)的概念,将 Log 切分为多个 LogSegement,相当于一个巨型文件被平均分割为一些相对较小的文件,这样也便于消息的查找、维护和清理。这样在做历史数据清理的时候,直接删除旧的 LogSegement 文件就可以了。
  • Log 日志在物理上只是以文件夹的形式存储,而每个 LogSegement 对应磁盘上的一个日志文件和两个索引文件,以及可能的其他文件(比如以".snapshot"为后缀的快照索引文件等)
    面试题总结