前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Spring事务的实现源码分析,以及事务不起作用原因分析

Spring事务的实现源码分析,以及事务不起作用原因分析

作者头像
吴就业
发布2020-07-13 15:53:47
1.5K0
发布2020-07-13 15:53:47
举报
文章被收录于专栏:Java艺术Java艺术

这篇文章写完后我改了N次,反复的读,反复的改,为的是能让读者看懂。源码分析类的文章真的很难写,懂是一回事,写又是另一回事,能写给别人看得懂又是一回事。太多英文单词搞得排版有点乱。

本篇内容包括:

  • Spring注解事务的实现
  • mybatis-spring包为事务提供的支持
  • 动态数据源使用配置需要注意的问题
  • 动态数据源配置例子

事务不起作用原因有哪些?

我遇到过的就这两点:

  • 同一个bean中调用自身的添加事务注解的方法
  • 使用动态数据源配置不正确导致的

一个Service方法中直接调用另一个被声明事务的方法,因为是在this中调用的,就走不到事务的切面方法,也就直接导致事务不生效,对于此类问题,可以通过ApplicationContext获取Bean再调用,不要直接使用this调用。

关于第二点,使用动态数据源配置不正确导致的事务不起作用问题,我将留在文末分析,因为只有了解Spring事务的工作原理,才能真正的理解为什么会出现这样的问题。

Spring注解事务的实现

TransactionInterceptor是事务方法拦截器,或叫切面。TransactionInterceptor继承TransactionAspectSupport。本篇不分析Spring AOP部分的实现,只关注事务的实现。

当调用一个bean的被@Transaction注解注释的方法时,先走到TransactionInterceptor事务拦截器的invoke方法,因此事务拦截器的invoke方法就是分析注解事务实现的入口。

由于invoke方法调用invokeWithinTransaction,自身并不做任何事情,所以直接看invokeWithinTransaction方法。

【TransactionInterceptor#invokeWithinTransaction方法】

invokeWithinTransaction方法整体分为四块

1、初始化事务支持,根据注解的属性配置,处理事务传播机制、为事务创建连接、为连接设置事务隔离级别等;

2、调用目标方法;

3、方法执行异常完成事务回滚;

4、方法执行成功完成事务提交。

源码分析涉及到的一些类说明:

spring-tx:

  • TransactionInterceptor(事务方法拦截器、切面)
  • PlatformTransactionManager(平台事务管理器)
  • TransactionStatus(事务状态)
  • ConnectionHolder(连接持有者)
  • TransactionSynchronizationManager(事务同步管理者)

【一些关键对象的类图】

01

首先获取注解的属性配置信息,如事务隔离级别、是否只读事务、事务的传播机制、事务超时时间等(TransactionAttribute)。

接着获取事务管理器PlatformTransactionManager,如果@Transaction注解上指定了事务管理器则获取指定的事务管理器,否则使用默认的。一般我们只会注册一个事务管理器。除非配置了多数据源(非动态数据源),才会为每个数据源配置一个事务管理器。

初始化工作由createTransactionIfNecessary方法完成。

调用createTransactionIfNecessary方法创建事务(如果需要),IfNecessary说明并不一定会创建,比如当前已经存在一个事务,则根据事务的传播机制决定是否要创建。createTransactionIfNecessary方法返回一个TransactionInfo,TransactionInfo保存事务信息,如旧的事务的TransactionInfo、事务属性配置、事务管理器、当前事务方法的事务状态。

代码语言:javascript
复制
protected final class TransactionInfo {
   // 事务管理器
    @Nullable
    private final PlatformTransactionManager transactionManager;
    // 事务注解的属性配置
    @Nullable
    private final TransactionAttribute transactionAttribute;
    // 切入点标志
    private final String joinpointIdentification;
    // 事务状态
    @Nullable
    private TransactionStatus transactionStatus;
    // 前一个事务的事务信息
    @Nullable
    private TransactionInfo oldTransactionInfo;
}

调用平台事务管理器PlatformTransactionManager的getTransaction方法为当前事务方法创建一个TransactionStatus,TransactionStatus描述一个事务的状态,如该事务是否存在保存点、是否已完成等。

代码语言:javascript
复制
public interface TransactionStatus extends SavepointManager {
  // 是否是新创建的事务
  boolean isNewTransaction();
  // 是否存在保存点
  boolean hasSavepoint();
  void setRollbackOnly();
  // 是否只回滚
  boolean isRollbackOnly();
  // 事务是否为已完成,即已提交或已回滚。
  boolean isCompleted();
}

接着分析PlatformTransactionManager的getTransaction方法,其实就是分析DataSourceTransactionManager的getTransaction方法。

在分析方法之前,先认识两个对象DataSourceTransactionObject与ConnectionHolder:

【DataSourceTransactionObject继承JdbcTransactionObjectSupport】

代码语言:javascript
复制
public abstract class JdbcTransactionObjectSupport 
        implements SavepointManager, SmartTransactionObject {
   // 持有数据库连接
  @Nullable
  private ConnectionHolder connectionHolder;
  // 之前的事务隔离级别,用于当事务退出时,还原Connection的事务隔离级别
  @Nullable
  private Integer previousIsolationLevel;
  // 是否允许使用保存点
  private boolean savepointAllowed = false;
 }

【DataSourceTransactionObject】

代码语言:javascript
复制
 private static class DataSourceTransactionObject 
                  extends JdbcTransactionObjectSupport {
        // 持有的ConnectionHolder是否是新创建的
        private boolean newConnectionHolder;
        // 事务结束时是否需要重置连接为自动提交
        private boolean mustRestoreAutoCommit;
 }

【ConnectionHolder】

代码语言:javascript
复制
public class ConnectionHolder extends ResourceHolderSupport {
  @Nullable
  private ConnectionHandle connectionHandle;
  // 当前数据库连接
  @Nullable
  private Connection currentConnection;
  // 当前事务状态
  private boolean transactionActive = false;
  // 是否支持保存点
  @Nullable
  private Boolean savepointsSupported;
  // 当前连接的事务的保存点总数
  private int savepointCounter = 0;
  }

1、调用doGetTransaction创建事务对象DataSourceTransactionObject。它继承JdbcTransactionObjectSupport实现的接口是SavepointManager,为当前事务提供创建保存点、回退到保存点、释放保存点继续执行的支持,而具体的创建保存点这些操作会交给ConnectionHolder完成的。DataSourceTransactionObject还会保存之前的事务隔离级别,用于当前事务退出时,还原Connection的事务隔离级别,否则当连接放入连接池被复用时就可能出现问题。

如果当前已经存在一个事务,则根据自己持有的数据源从TransactionSynchronizationManager中能拿到ConnectionHolder,如果当前未存在事务,则TransactionSynchronizationManager的getResource返回Null。如果拿到ConnectionHolder,则设置给DataSourceTransactionObject对象,并标志这不是一个新的连接。

2、调用isExistingTransaction方法判断当前是否已经有事务存在了,如果当前存在事务,则调用handleExistingTransaction方法返回一个TransactionStatus,根据配置的事务传播机制处理。

代码语言:javascript
复制
// 判断当前是否已经存在事务,其实是判断DataSourceTransactionObject
// 是否已经从TransactionSynchronizationManager
// 拿到了一个ConnectionHolder,且ConnectionHolder的transactionActive状态是否为true
@Override
  protected boolean isExistingTransaction(Object transaction) {
    DataSourceTransactionObject txObject = (DataSourceTransactionObject) transaction;
    return (txObject.hasConnectionHolder() && txObject.getConnectionHolder().isTransactionActive());
  }

这里会涉及到几种传播机制的处理,此处我只介绍其中的一种传播机制的处理。PROPAGATION_NESTED:如果当前存在事务,则在嵌套事务内执行;如果当前没有事务,则进行与PROPAGATION_REQUIRED类似的操作。

【handleExistingTransaction方法部分代码】

case 1

创建TransactionStatus,为当前连接创建保存点Savapoint。当方法执行过程中发生异常时,从TransactionStatus拿到保存点,回滚到保存点;当执行成功则移除保存点,回到上一个事务方法继续执行。

case 2

走正常的事务逻辑。

3、如果不走步骤2,则当前不存在事务。为当前方法创建一个TransactionStatus,并调用doBean方法。doBean方法由子类DataSourceTransactionManager实现。

doBean方法中判断当前DataSourceTransactionObject是否持有ConnectionHandler,如果有,则说明从TransactionSynchronizationManager中能拿到ConnectionHolder,已经存在一个事务了。如果没有,则从数据源中获取一个连接,并创建ConnectionHolder,赋值给DataSourceTransactionObject,并标志这是一个新创建的连接。

获取到连接之后,需要为当前事务对象,设置事务信息,分以下几步解析:

step 1

调用DataSourceUtils的prepareConnectionForTransaction方法为连接设置事务。如果当前事务属性配置为只读事务,则设置当前连接只能执行读操作;为获取到的连接设置数据库事务隔离级别。保存原来的隔离级别到事务对象DataSourceTransactionObject中,对于事务结束时恢复Connection的事务隔离级别。

step 2

如果当前连接是自动提交的,取消连接的自动提交,否则事务不生效。并且也要保存是否需要在事务结束时,将Connection的auto commit恢复为true。

step 3

调用自身的prepareTransactionalConnection方法,判断是否只读事务,如果是则执行一条sql为当前连接设置事务为只读事务。

step 4

设置当前事务对象持有的ConnectionHolder的事务状态为Active,标志Connection事务已经开启。

step 5

如果需要,为Connection设置事务超时时间。事务注解上的超时属性,如果是-1则不设置。

step 6

判断当前事务对象持有的ConnectionHolder是否是新创建的,如果是,则说明当前线程的方法调用栈上并未有任何方法开启事务,将ConnectionHolder绑定到TransactionSynchronizationManager的resources上,下次有事务进来就可能拿到这个ConnectionHolder。

TransactionSynchronizationManager的resources静态字段类型为ThreadLocal,所以同一个线程上的事务方法都能获取到同一个连接。用一个Map存储线程数据。如果是存储ConnectionHodler的,则Key为数据源DataSouce(如果使用动态数据源则为动态数据源)。后面分析SqlSession时,也是用这个字段存储的,所以Key定义为Object类型。

02

完成事务的初始化工作之后,接着就是执行目标方法

代码语言:javascript
复制
retVal = invocation.proceedWithInvocation();

03

如果方法执行出现异常,则执行回滚逻辑。

1)、从TracsactionInfo中拿到事务注解的属性配置,判断@Transaction注解是否指定当遇到某种异常时才回滚,如果指定了,先判断当前异常类型是否匹配,如果不匹配,则走正常提交逻辑。

2)、否则从事务信息TransactionInfo中获取事务状态TransactionStatus,调用事务管理器的rollback方法完成回滚。

事务管理器的rollback方法分析:

1)、根据事务状态TransactionStatus,判断当前事务是否有保存点Savepoint,如果有,则回滚到保存点,然后释放保存点。

2)、否则如果是个新事务则整个事务回滚(要判断是否是新事务,因为事务的传播机制)。

04

否则如果方法执行正常,则执行提交逻辑。

从事务信息TransactionInfo中获取事务状态TransactionStatus,调用事务管理器的commit方法完成提交。

方法执行分析:

1)、如果当前TracsactionStatus有保存点,则释放保存点;事务还不能提交,因为前一个事务方法被挂起了,还没有执行完成。

2)、否则如果是个新事务,提交事务。

mybatis-spring为事务提供的支持

mybatis-spring-boot-autoconfigure包下的MybatisAutoConfiguration 会自动完成一些配置工作,如创建SqlSessionTemplate这个bean。

源码分析涉及到的一些类说明

mybatis-spring:

  • SqlSessionTemplate(SqlSession的代理+委托)
  • SqlSessionInterceptor(SqlSession代理拦截器)
  • SpringManagedTransaction(mybatis-spring实现的mybatis的Transaction)
  • SpringManagedTransactionFactory(mybatis-spring实现的mybatis的TransactionFactory)

SqlSessionTemplate这是一个神奇的SqlSession,即有委托又有代理。SqlSessionTemplate也是实现SqlSession接口的,支持SqlSession的所有方法,同时它又不会去执行,而是交给一个SqlSession代理对象去执行,这个代理对象是在SqlSessionTemplate的构造方法中创建的,使用jdk动态代理。而InvocationHandler正是SqlSessionInterceptor。

代码语言:javascript
复制
public class SqlSessionTemplate implements SqlSession,    
                                DisposableBean {

  private final SqlSessionFactory sqlSessionFactory;
  // 执行器类型,通过解析Mapper标签的<insert>、<update>、<delete>、<select>标签获得
  private final ExecutorType executorType;
  // SqlSession代理类,jdk动态代理创建
  private final SqlSession sqlSessionProxy;
  private final PersistenceExceptionTranslator exceptionTranslator;

  public SqlSessionTemplate(SqlSessionFactory sqlSessionFactory, ExecutorType executorType) {
    this(sqlSessionFactory, executorType,
        new MyBatisExceptionTranslator(
            sqlSessionFactory.getConfiguration().getEnvironment().getDataSource(), true));
  }
  
  public SqlSessionTemplate(SqlSessionFactory sqlSessionFactory, ExecutorType executorType,
      PersistenceExceptionTranslator exceptionTranslator) {

    this.sqlSessionFactory = sqlSessionFactory;
    this.executorType = executorType;
    this.exceptionTranslator = exceptionTranslator;
     // 创建代理类
    this.sqlSessionProxy = (SqlSession) newProxyInstance(
        SqlSessionFactory.class.getClassLoader(),
        new Class[] { SqlSession.class },
        new SqlSessionInterceptor());
  }

}

Mapper接口中的每个方法都会通过解析对应Mapper配置文件的标签生成一个MapperMethod,解析完所有方法之后,会为每个Mapper接口都通过JDK动态代理生成一个MapperProxy对象。

代码语言:javascript
复制
public MapperProxy(
            // SqlSessionTemplate
             SqlSession sqlSession, 
             // Mapper接口
             Class<T> mapperInterface, 
             // 反射获取的Method与MapperMethod映射
             Map<Method, MapperMethod> methodCache){
}

当调用Mapper接口的方法时,都会进入MapperProxy的invoke方法.

代码语言:javascript
复制
public Object invoke(Object proxy, Method method, Object[] args){
}

通过method获取对应的MapperMethod,调用execute方法执行。

代码语言:javascript
复制
【org.apache.ibatis.binding.MapperMethod#execute】
public Object execute(SqlSession sqlSession, Object[] args){

}

MapperMethod的execute方法通过命令类型(insert、update、delete、select)调用SqlSession的insert、update、delete、selectList、selectOne方法。而此时的SqlSession正是单例的SqlSessionTemplate。

调用SqlSessionTemplate的方法会进入SqlSessionInterceptor的invoke方法(JDK动态代理,SqlSessionInterceptor是InvocationHandler)。此时才会真正的去获取一个SqlSession。

getSqlSession方法的执行步骤分析:

1、从TransactionSynchronizationManager中获取当前是否持有一个SqlSessionHolder,如果有则判断这个SqlSessionHolder的ExecutorType(CRUD)是否相同,相同才使用这个SqlSession。否则调用SqlSessionFactory的openSession方法获取一个SqlSession。

这个TransactionSynchronizationManager前面分析事务的时候已经很熟悉了,它用于存储当前线程数据,而且都是使用resources这个静态字段存储的,这是一个ThreadLocal。

  • 当key的类型为数据源时,存储的就是当前事务的连接持有者ConnectionHodler;
  • 当key的类型为SqlSession工厂时,存储的就是当前SqlSession持有者SqlSessionHodler;

可能还存在其它的,只是当前我发现的就这两种。所以resources的命名由此而来,也正是resource静态字段配置ThreadLocal的泛型类型为Map的原因。

为什么不使用多个ThreadLocal存储的?如果用多个ThreadLocal存储,容易由于疏忽忘了调用哪个ThreadLocal的remove导致内存泄露问题。如果resources不仅仅只是保持ConnectionHodler、SqlSession呢,使用多个ThreadLocal难以管理,最重要的就是代码难以阅读,会显得很乱。这是我的个人观察,看源码就是多吸收一些优秀的编程思想。

SqlSession的创建在SqlSessionFactory的openSessionFromDataSource方法中完成的。

先调用SpringManagedTransactionFactory对象的newTransaction创建一个SpringManagedTransaction(mybatis的Transaction接口的实现类,用于Spring与Mybatis整合提供事务的支持),再创建Executor,最后创建DefaultSqlSession。

代码语言:javascript
复制
// 三者的关系
DefaultSqlSession-> 调用Executor执行方法 -> 调用SpringManagedTransaction获取连接

newTransaction方法传入的数据源是配置SqlSessionFactory时注入的数据源,所以SqlSessionFactory与PlatformTransactionManager配置的数据源要相同,否则事务获取的连接与实际执行Sql获取到的连接将会不同。

2、将创建的SqlSession放到SqlSessionHolder中,将其绑定到TransactionSynchronizationManager(ThreadLocal)。一个事务中,如果执行的命令都是同一种类型,如update,则只会创建一个DefaultSqlSession。如果已经持有一个DefaultSqlSession则会直接使用。

DefaultSqlSession就是mybatis实现的SqlSession,所以往下走就是Mybatis的调用流程。在执行方法过程中,会调用Executor的getConnection方法获取连接,而Executor的getConnection方法会调用Transaction的getConnection方法。

这个Transaction就是SpringManagedTransaction。所以一个SqlSession不等于一个数据库连接。Mybatis中执行SQL之前再通过Transaction获取连接。

SpringManagedTransaction的getConnection方法如果当前已经打开一个连接,则返回当前连接,否则调用DataSourceUtils工具类的getConnection方法获取连接,而getConnection方法传递的参数是当前SpringManagedTransaction对象持有的数据源。根据数据源从TransactionSynchronizationManager中看看当前是否有事务打开了连接,如果有,则从TransactionSynchronizationManager拿到当前事务使用的连接。

如果能从TransactionSynchronizationManager中拿到连接,则说明当前执行的Mapper方法在事务中。如果拿不到则直接从数据源中获取一个连接,也就是走的非事务逻辑了。

动态数据源使用配置需要注意的问题

如果配置的PlatformTransactionManager事务管理器,使用的是一个动态数据源,那么TransactionSynchronizationManager会将这个动态数据源作为Key,因为多个数据源都是被动态数据源管理的,所以动态数据源内部怎么切换,一个事务中TransactionSynchronizationManager持有的都是第一次获取到的连接,整个事务中都会使用这个连接,因此在事务中切换数据源无效,应在事务之前完成数据源的切换。

SqlSessionFactory与PlatformTransactionManager配置的数据源一定要相同!SqlSessionFactory与PlatformTransactionManager配置的数据源一定要相同!SqlSessionFactory与PlatformTransactionManager配置的数据源一定要相同!

TransactionSynchronizationManager通过数据源作为key缓存事务持有的ConnectionHodler,由PlatformTransactionManager创建。SpringManagedTransaction是在DefaultSqlSessionFactory的openSession方法中调用SpringManagedTransactionFactory传入数据源创建的。因此,如果SqlSessionFactory与PlatformTransactionManager配置的数据源不同,ConnectionHodler所用数据源,将会与SpringManagedTransaction使用的数据源不同,ConnectionHodler所持有的连接就不会被Mybatis所使用,事务就不会生效。

动态数据源配置例子

示例一:

代码语言:javascript
复制
   @Bean(name = DsConstant.DEFAULT_MYSQL_DB_01)
    public DataSource mysqlDatabase01() {
        DruidDataSource druidDataSource = new DruidDataSource();
        ......
        return druidDataSource;
    }
    
    @Bean(name = DsConstant.DEFAULT_MYSQL_DB_02)
    public DataSource mysqlDatabase02() {
        DruidDataSource druidDataSource = new DruidDataSource();
        ......
        return druidDataSource;
    }
    
   /**
     * 动态数据源
     * 
     * @return
     */
    @Bean(name = DsConstant.DS)
    public DynamicDataSource dynamicDataSource(
                @Qualifier(DsConstant.DEFAULT_MYSQL_DB_01) DataSource db1,
                 @Qualifier(DsConstant.DEFAULT_MYSQL_DB_02) DataSource db2) {
        DynamicDataSource ds = new DynamicDataSource();
        ......
        return ds;
    }
    
    /**
     *  SqlSessionFactory配置
     */
    @Bean
    public SqlSessionFactoryBean platformSqlSessionFactory(@Qualifier(DsConstant.DS) DataSource dynamicDataSource,
                                                           @Autowired MybatisProperties mybatisProperties) {
        SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
        // 配置数据源
        sqlSessionFactoryBean.setDataSource(dataSource);
        PathMatchingResourcePatternResolver resourcePatternResolver = new PathMatchingResourcePatternResolver();
        // mapper文件位置
        sqlSessionFactoryBean.setMapperLocations(mybatisProperties.resolveMapperLocations());
        sqlSessionFactoryBean.setConfigLocation(resourcePatternResolver.getResource(mybatisProperties.getConfigLocation()));
        // 使用mybatis-spring提供的事务工厂
        TransactionFactory transactionFactory = new SpringManagedTransactionFactory();
        sqlSessionFactoryBean.setTransactionFactory(transactionFactory);
        return sqlSessionFactoryBean;
    }
    
    /**
     * 事务管理者
     *
     * @param platformDataSource
     * @return
     */
    @Bean
    public PlatformTransactionManager mysqlPlatformTransactionManager(@Qualifier(DsConstant.DS) DynamicDataSource dynamicDataSource) {
        return new DataSourceTransactionManager(dynamicDataSource);
    }

示例二:

代码语言:javascript
复制
   @Bean(name = DsConstant.DEFAULT_MYSQL_DB_01)
    public DataSource mysqlDatabase01() {
        DruidDataSource druidDataSource = new DruidDataSource();
        ......
        return druidDataSource;
    }
    
    @Bean(name = DsConstant.DEFAULT_MYSQL_DB_02)
    public DataSource mysqlDatabase02() {
        DruidDataSource druidDataSource = new DruidDataSource();
        ......
        return druidDataSource;
    }
    
   /**
     * 动态数据源
     *  
     *  @Primary注解:声明这个bean为同类型bean的高优先级bean,
     *   当自动注入不指定bean的name时,且存在多个同类型的bean时,
     *   自动注入会优先使用这个bean
     */
    @Primary
    @Bean(name = DsConstant.DS)
    public DynamicDataSource dynamicDataSource(
                @Qualifier(DsConstant.DEFAULT_MYSQL_DB_01) DataSource db1,
                 @Qualifier(DsConstant.DEFAULT_MYSQL_DB_02) DataSource db2) {
        DynamicDataSource ds = new DynamicDataSource();
        ......
        return ds;
    }
    
    /**
     * 事务管理者
     *
     * @param platformDataSource
     * @return
     */
    @Bean
    public PlatformTransactionManager mysqlPlatformTransactionManager(@Qualifier(DsConstant.DS) DynamicDataSource dynamicDataSource) {
        return new DataSourceTransactionManager(dynamicDataSource);
    }

示例二主要借助@Primary注解与mybatis-spring-boot-autoconfigure包下的MybatisAutoConfiguration自动配置类,实现自动配置SqlSessionFactory。

代码语言:javascript
复制
@org.springframework.context.annotation.Configuration
// 条件注入:存在指定的类型才注入这个配置类
@ConditionalOnClass({ SqlSessionFactory.class, SqlSessionFactoryBean.class })
// 条件注入:容器中存在数据源才注入这个配置类
@ConditionalOnBean(DataSource.class)
@EnableConfigurationProperties(MybatisProperties.class)
// 自动配置在DataSourceAutoConfiguration完成之后
// 如果项目中自己实现配置,则需要导出DataSourceAutoConfiguration这个自动配置
@AutoConfigureAfter(DataSourceAutoConfiguration.class)
public class MybatisAutoConfiguration {
.....
 // 当前未注册SqlSessionFactory,则自动创建SqlSessionFactory
 @Bean
 @ConditionalOnMissingBean
  public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
  }
  
  // SqlSessionTemplate也是由此自动创建的
  @Bean
  @ConditionalOnMissingBean
  public SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactory) {
  }
....
}

试一试,配置一个动态数据源,给动态数据源分配两个都是指向同一个数据库的数据源,然后配置SqlSessionFactoryBean(SqlSessionFactory)使用一个非动态数据源,而给PlatformTransactionManager事务管理器使用这个动态数据源,写一个事务例子,看看会发生什么?扫码关注最新动态

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2019-12-22,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 Java艺术 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
相关产品与服务
容器服务
腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档