Spring事务源码解析

一、基本概念

使用事务的目的是保证数据一致性和操作隔离。

1、ACID属性

  • 原子性(Atomicity):事务作为一个整体被执行,包含在其中的对数据库的操作要么全部被执行,要么都不执行。
  • 一致性(Consistency):事务应确保数据库的状态从一个一致状态转变为另一个一致状态。事务开始前后,数据库的完整性没有被破坏
  • 隔离性(Isolation):多个事务并发执行时,一个事务的执行不应影响其他事务的执行。
  • 持久性(Durability):已被提交的事务对数据库的修改应该永久保存在数据库中。

2、事务的隔离级别

  • 读未提交(READ UNCOMMITTED):另一个事务修改了数据,但尚未提交,而本事务中的SELECT会读到这些未被提交的数据(脏读)。
    在这里插入图片描述

  • 读已提交(READ COMMITTED):本事务读取到的是最新的数据(其他事务提交后的)。问题是,在同一个事务里,前后两次相同的SELECT会读到不同的结果(不重复读)。
    在这里插入图片描述

  • 可重复读(REPEATABLE READ):在同一个事务里,SELECT的结果是事务开始时时间点的状态,因此,同样的SELECT操作读到的结果会是一致的。但是,会有幻读现象。
    在这里插入图片描述

  • 串行化(SERIALIZABLE):通过共享锁和排它锁实现,可以保证不同事务间的互斥,读读操作不会发生阻塞。
    在这里插入图片描述
    四个隔离级别逐渐增强,每一级解决一个问题,MySQL InnoDB的默认隔离级别是可重复读。
    脏读和不重复读比较好理解,这里主要解释下什么是幻读
    所谓幻读就是一个事务在前后两次查询(当前读/更新)同一范围数据数据时,后一次查询看到了前一次查询没有看到的行。在下图的例子中,由于事务B的插入,导致事务A第一次查询时只有两条数据,而在第二次查询时出现了三条数据,就像之前读到的数据像幻觉一样。
    在这里插入图片描述

3、事务行为

事务的开启:可用START TRANSACTION、BEGI命令显式开启事务
事务的提交:默认情况下,单条SQL执行成功后,MySQL会自动提交事务。开始事务后可以通过COMMIT命令提交;部分命令(DDL命令、管理数据库架构命令、管理命令)也会隐式执行COMMIT命令
事务的回滚:ROLLBACK命令

4、Spring事务的传播级别

传播级别定义
PROPAGATION_REQUIRED支持当前事务,如果当前没有事务,则新建一个事务
PROPAGATION_SUPPORTS支持当前事务,如果当前没有事务,则以非事务进行
PROPAGATION_MANDATORY支持当前事务,如果当前没有事务,则抛异常
PROPAGATION_REQUIRES_NEW新建事务,如果当前存在事务,则把当前事务挂起
PROPAGATION_NESTED如果当前存在事务,则在嵌套事务内执行。如果没有,则进行与PROPAGATION_REQUIRED类似操作
PROPAGATION_NOT_SUPPORTED以非事务进行,如果当前存在事务,则挂起事务,执行当前逻辑,结束后恢复上下文的事务
PROPAGATION_NEVER以非事务进行,如果当前存在事务,则抛异常

5、Spring事务支持方式

  • 声明式事务
    • @Transactional注解
  • 编程式事务
    • TransactionTemplate
    • TransactionManager
      编程式事务有两种方式:TransactionTemplate和TransactionManager
// TransactionTemplate
@Autowired
private TransactionTemplate transactionTemplate;
public void testTransaction() {
    transactionTemplate.execute(new TransactionCallbackWithoutResult() {
        @Override
        protected void doInTransactionWithoutResult(TransactionStatus transactionStatus) {
 
            try {
 
                // ....  业务代码
            } catch (Exception e){
                //回滚
                transactionStatus.setRollbackOnly();
            }
 
        }
    });
}
// TransactionManager
@Autowired
private PlatformTransactionManager transactionManager;
 
public void testTransaction() {
 
    TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition());
    try {
        // ....  业务代码
        transactionManager.commit(status);
    } catch (Exception e) {
        transactionManager.rollback(status);
    }
}

我们常用的是声明式事务,Spring事务的核心流程在抽象类AbstractPlatformTransactionManager中,其基于模版方法的设计模式,事务的getTransaction、suspend、startTransaction、rollback、commit等都由各框架的事务管理器实现。下面主要结合**JDBC(DataSourceTransactionManager)**的事务实现进行Spring事务源码的分析。

二、Spring事务的执行源码

1、事务AOP

代码在执行@Transcational所注解的目标方法之前,会首先执行CglibAopProxy.DynamicAdvisedInterceptor#intercept方法,在这个方法中主要是寻找所执行目标方法的拦截器列表,也就是目标方法上对应注解的拦截器,如果有拦截器则先以链式的方式执行拦截器方法,否则执行目标方法。
在这里插入图片描述
对于@Transactional注解,其对应的拦截器为TransactionInterceptor

2、事务处理拦截器TransactionInterceptor

TransactionInterceptor类是Advice实现类,用于对事务性的方法进行拦截,并通过Spring的事务管理器(PlatformTransactionManager)进行事务管理,PlatformTransactionManager有三个方法,通过 PlatformTransactionManager 这个接口,Spring 为各个平台如 JDBC(DataSourceTransactionManager)、Hibernate(HibernateTransactionManager)、JPA(JpaTransactionManager)等都提供了对应的事务管理器。

interface PlatformTransactionManager extends TransactionManager{
    // 根据事务定义获取事务状态
    TransactionStatus getTransaction(TransactionDefinition definition)
            throws TransactionException;
 
    // 提交事务
    void commit(TransactionStatus status) throws TransactionException;
 
    // 事务回滚
    void rollback(TransactionStatus status) throws TransactionException;
}

2.1 主要流程

TransactionInterceptor的核心方法是invokeWithinTransaction,主要流程
在这里插入图片描述
源码如下:

@Override
@Nullable
public Object invoke(MethodInvocation invocation) throws Throwable {
    // 获取目标类,即执行方法的类
    Class<?> targetClass = (invocation.getThis() != null ? AopUtils.getTargetClass(invocation.getThis()) : null);
 
    // 事务处理逻辑
    return invokeWithinTransaction(invocation.getMethod(), targetClass, invocation::proceed);
}
 
@Nullable
protected Object invokeWithinTransaction(Method method, @Nullable Class<?> targetClass,
        final InvocationCallback invocation) throws Throwable {
 
    // 获取事务属性源
    // 该类有一个属性publicMethodsOnly,为true,限制了只有public方法,@Transcational注解才能生效
    TransactionAttributeSource tas = getTransactionAttributeSource();
    // 获取事务属性,包括传播级别、隔离级别、回滚规则等
    final TransactionAttribute txAttr = (tas != null ? tas.getTransactionAttribute(method, targetClass) : null);
    // 获取事务管理类,一般用的是JDBC的DataSourceTransactionManager
    // 这里有本地缓存,只有第一次获取才会加载bean
    final PlatformTransactionManager tm = determineTransactionManager(txAttr);
    // 获取全路径方法名,用于监控和记录
    final String joinpointIdentification = methodIdentification(method, targetClass, txAttr);
 
    if (txAttr == null || !(tm instanceof CallbackPreferringPlatformTransactionManager)) {
        // 创建TransactionInfo(这里的TransactionInfo是TransactionAspectSupport的内部类)
        // 这里会处理事务的传播级别,同时将事务和线程绑定
        TransactionInfo txInfo = createTransactionIfNecessary(tm, txAttr, joinpointIdentification);
 
        Object retVal;
        try {
            // 执行目标方法
            retVal = invocation.proceedWithInvocation();
        }
        catch (Throwable ex) {
            // 目标方法执行异常,事务回滚
            completeTransactionAfterThrowing(txInfo, ex);
            throw ex;
        }
        finally {
            // 重置线程的事务信息
            cleanupTransactionInfo(txInfo);
        }
        // 事务提交
        commitTransactionAfterReturning(txInfo);
        return retVal;
    }
 
    else {
        // 省略CallbackPreferringPlatformTransactionManager的处理
        // 这部分是编程式事务的处理
        ......
    }
}

2.2 尝试创建事务

根据给定的属性尝试创建一个事务,并将相关的事务信息TransactionStatus绑定到TransactionInfo返回,同时会将当前的TransactionInfo对象绑定到当前线程中,将该线程之前的事务信息记录到TransactionInfo的oldTransactionInfo属性中,也就是记录外层事务,可以形成不同方法之间的事务信息链表。

protected TransactionInfo createTransactionIfNecessary(@Nullable PlatformTransactionManager tm,
        @Nullable TransactionAttribute txAttr, final String joinpointIdentification) {
 
    //如果没有名称,将方法全路径作为事务名称
    if (txAttr != null && txAttr.getName() == null) {
        txAttr = new DelegatingTransactionAttribute(txAttr) {
            @Override
            public String getName() {
                return joinpointIdentification;
            }
        };
    }
     
    TransactionStatus status = null;
    if (txAttr != null) {
        if (tm != null) {
            // 在该方法决定是否开启一个事务,
            status = tm.getTransaction(txAttr);
        }
        else {
            if (logger.isDebugEnabled()) {
                logger.debug("Skipping transactional joinpoint [" + joinpointIdentification +
                        "] because no transaction manager has been configured");
            }
        }
    }
 
    // 通过各参数生成TransactionInfo,并记录线程中上个方法的TransactionInfo,将新TransactionInfo和线程绑定
    return prepareTransactionInfo(tm, txAttr, joinpointIdentification, status);
}
 
public final TransactionStatus getTransaction(@Nullable TransactionDefinition definition) throws TransactionException {
    // 获取事务的数据源连接
    // 设置是否允许保存点
    Object transaction = doGetTransaction();
 
    // Cache debug flag to avoid repeated checks.
    boolean debugEnabled = logger.isDebugEnabled();
 
    if (definition == null) {
        // Use defaults if no transaction definition given.
        definition = new DefaultTransactionDefinition();
    }
 
    if (isExistingTransaction(transaction)) {
        // 如果事务对象的是否存在数据库连接且已经开启过事务,则说明存在外层事务
        // 根据不同的事务传播级别进行处理
        return handleExistingTransaction(definition, transaction, debugEnabled);
    }
 
    // 若不存在外层事务,开始创建新事务
 
    // 判断是否超时
    if (definition.getTimeout() < TransactionDefinition.TIMEOUT_DEFAULT) {
        throw new InvalidTimeoutException("Invalid transaction timeout", definition.getTimeout());
    }
 
    // PROPAGATION_MANDATORY传播级别是:如果存在事务,则将当前方法加入到该事务中,如果不存在事务则当前方法抛出异常
    // 这里由于不存在外层事务,所以直接抛出异常
    if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_MANDATORY) {
        throw new IllegalTransactionStateException(
                "No existing transaction found for transaction marked with propagation 'mandatory'");
    }
    // 处理PROPAGATION_REQUIRED、PROPAGATION_REQUIRES_NEW、PROPAGATION_NESTED三种传播级别的事务
    // 这三种传播级别的共同点是如果当前不存在事务,则创建一个新的事务
    else if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_REQUIRED ||
            definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_REQUIRES_NEW ||
            definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_NESTED) {
        // 挂起给定的事务
        // 首先挂起事务同步器,清空TransactionSynchronizationManager中保存的当前线程的事务信息,返回被挂起的资源信息
        // 核心方法是doSuspend,由不同子类实现,DataSourceTransactionManage的实现就是将当前线程的连接资源解绑
        SuspendedResourcesHolder suspendedResources = suspend(null);
        if (debugEnabled) {
            logger.debug("Creating new transaction with name [" + definition.getName() + "]: " + definition);
        }
        try {
            // 判断是否需要开启事务同步,默认是开启
            boolean newSynchronization = (getTransactionSynchronization() != SYNCHRONIZATION_NEVER);
            DefaultTransactionStatus status = newTransactionStatus(
                    definition, transaction, true, newSynchronization, debugEnabled, suspendedResources);
            // 真正开启事务
            doBegin(transaction, definition);
            // 根据需要初始化事务同步器
            prepareSynchronization(status, definition);
            return status;
        }
        catch (RuntimeException | Error ex) {
            resume(null, suspendedResources);
            throw ex;
        }
    }
    else {
        // 处理PROPAGATION_SUPPORTS、PROPAGATION_NEVER、PROPAGATION_NOT_SUPPORTED三种传播级别的事务
        if (definition.getIsolationLevel() != TransactionDefinition.ISOLATION_DEFAULT && logger.isWarnEnabled()) {
            logger.warn("Custom isolation level specified but no actual transaction initiated; " +
                    "isolation level will effectively be ignored: " + definition);
        }
         
        boolean newSynchronization = (getTransactionSynchronization() == SYNCHRONIZATION_ALWAYS);
        // 这里并没有真正开启一个事务,只创建了一个DefaultTransactionStatus对象用于记录和初始化事务同步器
        return prepareTransactionStatus(definition, null, true, newSynchronization, debugEnabled, null);
    }
}

最核心的方法是doBegin,这里是真正开启一个事务,其主要操作是获取数据库连接资源,同时将自动提交关闭,以此来形成一个事务。

protected void doBegin(Object transaction, TransactionDefinition definition) {
    DataSourceTransactionObject txObject = (DataSourceTransactionObject) transaction;
    Connection con = null;
 
    try {
        // 事务没有连接资源或者资源被标记为和事务同步,则获取一个新的连接
        if (!txObject.hasConnectionHolder() ||
                txObject.getConnectionHolder().isSynchronizedWithTransaction()) {
            // 获取新的数据库连接
            Connection newCon = obtainDataSource().getConnection();
            if (logger.isDebugEnabled()) {
                logger.debug("Acquired Connection [" + newCon + "] for JDBC transaction");
            }
            // 将新的连接信息保存到事务对象中
            txObject.setConnectionHolder(new ConnectionHolder(newCon), true);
        }
        // 将synchronizedWithTransaction设置为true,即资源标记为与事务同步
        txObject.getConnectionHolder().setSynchronizedWithTransaction(true);
        con = txObject.getConnectionHolder().getConnection();
 
        // 设置数据库连接的隔离级别和只读标志
        Integer previousIsolationLevel = DataSourceUtils.prepareConnectionForTransaction(con, definition);
        // 设置事务的隔离级别属性
        txObject.setPreviousIsolationLevel(previousIsolationLevel);
 
        // 将自动提交设置为手动提交
        if (con.getAutoCommit()) {
            // 设置mustRestoreAutoCommit为true,用于事务提交后回复自动提交
            txObject.setMustRestoreAutoCommit(true);
            if (logger.isDebugEnabled()) {
                logger.debug("Switching JDBC Connection [" + con + "] to manual commit");
            }
            con.setAutoCommit(false);
        }
        // 到这里事务已经开启,后续的sql需要等待commit命令执行后才会提交
 
        // 对于只读事务,执行"SET TRANSACTION READ ONLY"优化
        prepareTransactionalConnection(con, definition);
        txObject.getConnectionHolder().setTransactionActive(true);
 
        // 设置超时时间
        int timeout = determineTimeout(definition);
        if (timeout != TransactionDefinition.TIMEOUT_DEFAULT) {
            txObject.getConnectionHolder().setTimeoutInSeconds(timeout);
        }
 
        // 将连接资源和当前线程绑定
        if (txObject.isNewConnectionHolder()) {
            TransactionSynchronizationManager.bindResource(obtainDataSource(), txObject.getConnectionHolder());
        }
    }
 
    catch (Throwable ex) {
        if (txObject.isNewConnectionHolder()) {
            DataSourceUtils.releaseConnection(con, obtainDataSource());
            txObject.setConnectionHolder(null, false);
        }
        throw new CannotCreateTransactionException("Could not open JDBC Connection for transaction", ex);
    }
}

2.3 清除线程事务信息

这里比较简单,本次事务结束后,将本次事务对象中的外层事务oldTransactionInfo重新和线程绑定在这里插入图片描述

2.4 事务提交

目标方法执行完成后,则调用事务管理器的commit方法进行事务提交。但是以下两种情况是不进行提交而进行事务回滚的:

  • 当前事务被标记为仅回滚,这里是一般是当前方法抛出异常后进行手动设置
  • 当前事务被标记为全局回滚,这里一般是内层方法设置了仅回滚,且该内部方法的事务传播级别是不需要创建新的事务的(即PROPAGATION_REQUIRED、PROPAGATION_SUPPORTS、PROPAGATION_MANDATORY),内部方法在进行内部提交时会将整个连接资源设置为仅回滚,也就是全局回滚。在这里插入图片描述

事务提交的核心方法是processCommit,源码如下:

private void processCommit(DefaultTransactionStatus status) throws TransactionException {
    try {
        boolean beforeCompletionInvoked = false;
 
        try {
            boolean unexpectedRollback = false;
            // 事务提交前的操作,空方法,由子类实现
            prepareForCommit(status);
            // 执行所有已注册的事务同步器的beforeCommit回调
            triggerBeforeCommit(status);
            // 执行所有已注册的事务同步器的beforeCompletion回调
            triggerBeforeCompletion(status);
            beforeCompletionInvoked = true;
             
            // 存在保存点,则释放保存点,但是不提交事务,需要等待外层事务提交
            // PROPAGATION_NESTED传播级别会开启保存点
            if (status.hasSavepoint()) {
                if (status.isDebug()) {
                    logger.debug("Releasing transaction savepoint");
                }
                unexpectedRollback = status.isGlobalRollbackOnly();
                status.releaseHeldSavepoint();
            }
            // 如果是新的事务,则通过Connection的commit方法提交事务,具体由子类实现
            // 对于PROPAGATION_REQUIRES_NEW或者最外层PROPAGATION_REQUIRED都是开启的新事务
            else if (status.isNewTransaction()) {
                if (status.isDebug()) {
                    logger.debug("Initiating transaction commit");
                }
                unexpectedRollback = status.isGlobalRollbackOnly();
                doCommit(status);
            }
            // 剩余情况仅做rollbackOnly检查
            else if (isFailEarlyOnGlobalRollbackOnly()) {
                unexpectedRollback = status.isGlobalRollbackOnly();
            }
 
            // 有rollbackOnly标志,那么抛出UnexpectedRollbackException异常
            if (unexpectedRollback) {
                throw new UnexpectedRollbackException(
                        "Transaction silently rolled back because it has been marked as rollback-only");
            }
        }
        catch (UnexpectedRollbackException ex) {
            // 有rollback标志
            // 执行所有已注册的事务同步器的afterCompletion回调
            triggerAfterCompletion(status, TransactionSynchronization.STATUS_ROLLED_BACK);
            throw ex;
        }
        catch (TransactionException ex) {
            // 执行doCommit发生异常
 
            // 若设置了在doCommit执行异常时进行回滚,则需要进行回滚
            if (isRollbackOnCommitFailure()) {
                doRollbackOnCommitException(status, ex);
            }
            else {
                // 执行所有已注册的事务同步器的afterCompletion回调
                triggerAfterCompletion(status, TransactionSynchronization.STATUS_UNKNOWN);
            }
            throw ex;
        }
        catch (RuntimeException | Error ex) {
            // 如果还没有触发beforeCompletion方法回调,则进行回调
            if (!beforeCompletionInvoked) {
                triggerBeforeCompletion(status);
            }
            // 执行回滚,并执行所有已注册的事务同步器的afterCompletion回调
            doRollbackOnCommitException(status, ex);
            throw ex;
        }
 
        try {
            // 事务成功提交后调用,触发afterCommit回调
            triggerAfterCommit(status);
        }
        finally {
            //最终触发afterCompletion方法回调
            triggerAfterCompletion(status, TransactionSynchronization.STATUS_COMMITTED);
        }
 
    }
    finally {
        // 事务提交后进行清理工作
        //(1)如果开启了新的事务同步器,则清理事务同步器
        //(2)如果该事务是新开启的,则恢复事务的自动提交,重置事务使用数据库连接的属性,释放数据库连接
        //(3)如果存在被挂起的事务,则恢复挂起的事务
        cleanupAfterCompletion(status);
    }
}

在事务提交后的清理工作中,会恢复之前挂起的事务,那如何理解挂起和恢复
事务的挂起实际上是开启内层事务时,将外层事务的连接和当前线程解绑,并存储在内层事务的属性中,然后将新事务所获取的连接资源与当前线程绑定,后续的操作都是基于这个新的连接,也就是一个新的事务,所以之前的事务和数据库连接就是被挂起了,并没有执行最后的事务提交或回滚;
事务的恢复就是内层事务提交或者回滚后,当前线程再次和之前保存的外层事务资源进行绑定,后续的处理都是基于外层事务的连接资源进行。

2.5 事务异常处理

当事务执行抛出异常时,会调用rollbackOn方法进行判断,当前出现的异常是否和配置的回滚异常匹配,若匹配则调用事务管理器的rollback方法进行回滚,否则调用事务管理器的commit方法进行事务提交。
对于rollbackOn的判断逻辑:

  • 默认是只有RuntimeException或者Error类型的异常才会回滚
  • 基于xml则是rollback-for和no-rollback-for属性
  • 基于@Transactional注解则是rollbackFor、rollbackForClassName、noRollbackFor、noRollbackForClassName属性
    在这里插入图片描述

事务提交在2.4中内容已经涉及,这里不再赘述。下面重点分析rollback方法,其核心方法是processRollback,源码如下:

private void processRollback(DefaultTransactionStatus status, boolean unexpected) {
    try {
        boolean unexpectedRollback = unexpected;
 
        try {
            // 执行所有已注册的事务同步器的beforeCompletion回调
            triggerBeforeCompletion(status);
 
            // 如果存在保存点,则回滚到保存点并释放保存点
            if (status.hasSavepoint()) {
                if (status.isDebug()) {
                    logger.debug("Rolling back transaction to savepoint");
                }
                status.rollbackToHeldSavepoint();
            }
            // 如果是新开启的事务,则通过Connection的rollback进行回滚,具体由子类实现
            else if (status.isNewTransaction()) {
                if (status.isDebug()) {
                    logger.debug("Initiating transaction rollback");
                }
                doRollback(status);
            }
            // 其他情况
            else {
                // 若存在事务且事务被标记为仅回滚或者globalRollbackOnParticipationFailure属性为true
                // globalRollbackOnParticipationFailure属性默认为true,表示只要你的参与事务失败了,就标记此事务为rollback-only
                if (status.hasTransaction()) {
                    if (status.isLocalRollbackOnly() || isGlobalRollbackOnParticipationFailure()) {
                        if (status.isDebug()) {
                            logger.debug("Participating transaction failed - marking existing transaction as rollback-only");
                        }
                        // 此处是将该事务的资源设置为仅回滚,这样即使没有抛出异常,外层事务在提交时也会进行回滚
                        // 具体见2.4节事务提交时进行回滚的情况
                        doSetRollbackOnly(status);
                    }
                    else {
                        if (status.isDebug()) {
                            logger.debug("Participating transaction failed - letting transaction originator decide on rollback");
                        }
                    }
                }
                else {
                    logger.debug("Should roll back transaction but cannot - no transaction available");
                }
                // Unexpected rollback only matters here if we're asked to fail early
                if (!isFailEarlyOnGlobalRollbackOnly()) {
                    unexpectedRollback = false;
                }
            }
        }
        catch (RuntimeException | Error ex) {
            // 执行所有已注册的事务同步器的afterCompletion回调
            triggerAfterCompletion(status, TransactionSynchronization.STATUS_UNKNOWN);
            throw ex;
        }
        // 执行所有已注册的事务同步器的afterCompletion回调
        triggerAfterCompletion(status, TransactionSynchronization.STATUS_ROLLED_BACK);
 
        // Raise UnexpectedRollbackException if we had a global rollback-only marker
        if (unexpectedRollback) {
            throw new UnexpectedRollbackException(
                    "Transaction rolled back because it has been marked as rollback-only");
        }
    }
    finally {
        // 清除事务信息,同processCommit中的cleanupAfterCompletion方法
        cleanupAfterCompletion(status);
    }
}

三、总结

在我看来,对于spring事务的源码,搞清楚一下几个问题即可:

  • 不同的传播级别Spring是怎么处理的?
  • 在什么时候真正开启一个事务?
  • Spring事务是如何通过“挂起”和“恢复”实现外层事务和内层事务的提交和回滚的?

另外,Spring事务的实现使用了AOP的逻辑实现,以及其采用了模板方法的设计模式,将核心处理流程交给各事务管理器实现,这种设计模式是非常值得我们学习的。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值