Spring注解配置事务管理

1 Sping事务简介

事务管理是应用系统开发中必不可少的一部分,Spring为事务管理提供了丰富的功能支持。Spring事务管理分为编程式事务管理和声明式事务管理的两种方式。

1.1 编程式事务

编程式事务指的是通过编码方式实现事务,编程式事务管理使用TransactionTemplate或者直接使用底层的PlatformTransactionManager。对于编程式事务管理,spring推荐使用TransactionTemplate。

1.2 声明式事务

声明式事务基于AOP,将具体业务逻辑与事务处理解耦,其本质是对方法前后进行拦截,然后在目标方法开始之前创建或者加入一个事务,在执行完目标方法之后根据执行情况提交或者发生异常时回滚事务。声明式事务管理使业务代码逻辑不受污染, 因此在实际使用中声明式事务用的比较多。

声明式事务有两种方式,一种是在配置文件(xml)中配置相关的事务规则声明,另一种是基于 @Transactional 注解的方式。本文将着重介绍基于 @Transactional注解的事务管理。

1.3 编程式事务与声明式事务的区别

声明式事务开始事务和提交事务都是固定的,不够灵活,而编程式事务通过代码在想要的地方开始事务,在想要的地方提交事务,更加灵活。

2 基于@Transactional的声明式事务

@Transactional可以作用于接口、接口方法、类以及类方法上。当作用于类上时,该类的所有public方法将都具有该类型的事务属性,同时,我们也可以在方法级别使用该标注来覆盖类级别的定义。

1、通过@Transactional,实现了事务操作,@Transactional注解在Service类上表示所有public方法都使用了事务;

2、 Spring的AOP即声明式事务管理默认是针对unchecked exception回滚。也就是默认对RuntimeException()异常或是其子类进行事务回滚;checked异常,即Exception可try{}捕获的不会回滚,因此对于我们自定义异常,通过rollbackFor进行设定,发生异常后进行回滚。

3、如果我们需要捕获异常后,同时进行回滚,我们可以通过代码进行手动回滚操作:

TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();。

4、通过使用Object savePoint = TransactionAspectSupport.currentTransactionStatus().createSavepoint() 设置回滚点,使用TransactionAspectSupport.currentTransactionStatus().rollbackToSavepoint(savePoint) 回滚到savePoint。

       5、需要明确以下两点:

       (1) 默认配置下 Spring只会回滚运行时、未检查异常(继承自RuntimeException的异常)或者发生了Error的代码。

参考:https://docs.spring.io/spring/docs/4.3.13.RELEASE/spring-framework-reference/htmlsingle/#transaction-declarative-rolling-back

       (2) @Transactional 注解只能应用到 public 方法才有效。

参考:https://docs.spring.io/spring/docs/4.3.13.RELEASE/spring-framework-reference/htmlsingle/#transaction-declarative-annotations

2.1 @Transactional注解

2.1.1 @Transactional注解定义

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Transactional {

    /**
     * 默认的事务管理器
     */
    @AliasFor("transactionManager")
    String value() default "";

    /**
     * 当配置了多个事务管理器时,可以使用该属性指定使用哪个事务管理器
     */
    @AliasFor("value")
    String transactionManager() default "";

    Propagation propagation() default Propagation.REQUIRED;

    Isolation isolation() default Isolation.DEFAULT;

    int timeout() default TransactionDefinition.TIMEOUT_DEFAULT;

    boolean readOnly() default false;

    Class<? extends Throwable>[] rollbackFor() default {};

    String[] rollbackForClassName() default {};

    Class<? extends Throwable>[] noRollbackFor() default {};

    String[] noRollbackForClassName() default {};
}

2.1.2 @Transactional常用配置

序号

参数名称

功能描述

1

readOnly

该属性用于设置当前事务是否为只读事务,设置为true表示只读,false则表示可读写,默认值为false。

例如:@Transactional(readOnly=true)

2

rollbackFor

该属性用于设置需要进行回滚的异常类数组,当方法中抛出指定异常数组中的异常时,则进行事务回滚。例如

指定单一异常类:

@Transactional(rollbackFor=RuntimeException.class)

指定多个异常类:

@Transactional(rollbackFor={RuntimeException.class, Exception.class})

3

rollbackForClassName

该属性用于设置需要进行回滚的异常类名称数组,当方法中抛出指定异常名称数组中的异常时,则进行事务回滚。例如

指定单一异常类名称:

@Transactional(rollbackForClassName=”RuntimeException”)

指定多个异常类名称:

@Transactional(rollbackForClassName={“RuntimeException”,”Exception”})

4

noRollbackFor

该属性用于设置不需要进行回滚的异常类数组,当方法中抛出指定异常数组中的异常时,不进行事务回滚。例如

指定单一异常类:

@Transactional(noRollbackFor=RuntimeException.class)

指定多个异常类:

@Transactional(noRollbackFor={RuntimeException.class, Exception.class})

5

noRollbackForClassName

该属性用于设置不需要进行回滚的异常类名称数组,当方法中抛出指定异常名称数组中的异常时,不进行事务回滚。例如

指定单一异常类名称:

@Transactional(noRollbackForClassName=”RuntimeException”)

指定多个异常类名称:

@Transactional(noRollbackForClassName={“RuntimeException”,”Exception”})

6

propagation

该属性用于设置事务的传播行为。例如:

@Transactional(propagation=Propagation.NOT_SUPPORTED,readOnly=true)

7

isolation

该属性用于设置底层数据库的事务隔离级别,事务隔离级别用于处理多事务并发的情况,通常使用数据库的默认隔离级别即可,基本不需要进行设置

8

timeout

该属性用于设置事务的超时秒数,默认值为-1表示永不超时

2.1.3 事务传播行为

所谓事务的传播行为,是指如果在开始当前事务之前,一个事务上下文已经存在,此时有若干选项可以指定一个事务性方法的执行行为。事务的传播行为,默认值为 Propagation.REQUIRED。

事务传播行为的配置值如下表所示:

序号

配置值

描述

1

Propagation.REQUIRED

如果当前存在事务,则加入该事务,如果当前不存在事务,则创建一个新的事务。

2

Propagation.SUPPORTS

如果当前存在事务,则加入该事务;如果当前不存在事务,则以非事务的方式继续运行。

3

Propagation.MANDATORY

如果当前存在事务,则加入该事务;如果当前不存在事务,则抛出异常。

4

Propagation.REQUIRES_NEW

重新创建一个新的事务,如果当前存在事务,暂停当前的事务。

5

Propagation.NOT_SUPPORTED

以非事务的方式运行,如果当前存在事务,暂停当前的事务。

6

Propagation.NEVER

以非事务的方式运行,如果当前存在事务,则抛出异常。

7

Propagation.NESTED

和 Propagation.REQUIRED 效果一样。

2.1.4 事务超时

事务超时就是指一个事务所允许执行的最长时间,如果超过该时间限制但事务还没有完成,则自动回滚事务。在 TransactionDefinition中以 int 的值来表示超时时间,单位是秒。 默认设置为底层事务系统的超时值,如果底层数据库事务系统没有设置超时值,那么就是none,没有超时限制。

2.1.5 事务隔离级别

2.1.5.1 事务的ACID特性

事务的重要特性,是指保证数据的(ACID)特性。所谓的ACID是指:

  1. 原子性(Atomicity)
事物是数据库的逻辑工作单位,事务中的诸多操作要么全做要么全不做。
  1. 一致性(Consistency)
事务执行结果必须是使数据库从一个一致性状态变到另一个一致性状态。
  1. 隔离性(Isolation)
一个数据的执行不能被其他事务干扰。
  1. 持续性/永久性(Durability)  
一个事务一旦提交,它对数据库中的数据的改变是永久性的。隔离级别与并发性是互为矛盾的:隔离程度越高,数据库的并发性越差;隔离程度越低,数据库的并发性越好,这点很好理解

2.1.5.2 事务的隔离级别

事务的隔离级别有一下四种:

       1)ReadUncommitted

表示:未提交读。当事务A更新某条数据的时候,不容许其他事务来更新该数据,但可以进行读取操作。

      2)ReadCommitted

表示:提交读。当事务A更新数据时,不容许其他事务进行任何的操作包括读取,但事务A读取时,其他事务可以进行读取、更新。

     3)RepeatableRead

表示:重复读。当事务A更新数据时,不容许其他事务进行任何的操作,但是当事务A进行读取的时候,其他事务只能读取,不能更新。

    4)Serializable

表示:序列化。最严格的隔离级别,当然并发性也是最差的,事务必须依次进行。

2.1.5.3 事务的读取现象

通过对事务操作的现象现象结果收集,可以反映出事务的隔离级别的现象。这些现象有:

  1. 更新丢失(lost update)

当系统允许两个事务同时更新同一数据时,发生更新丢失。

  1. 脏读(dirty read)

当一个事务读取另一个事务尚未提交的修改时,产生脏读。

  1. 不重复读(nonrepeatable read)

同一查询在同一事务中多次进行,由于其他提交事务所做的修改或删除,每次返回不同的结果集,此时发生非重复读。

  1. 幻读(phantom read)

同一查询在同一事务中多次进行,由于其他提交事务所做的插入操作,每次返回不同的结果集,此时发生幻像读。

2.1.5.4 隔离级别与读取现象

隔离级别

状态说明

脏读(Dirty Read)

不可重复读取 (NonRepeatable Read)

幻读(Phantom Read )

ReadUncommitted

未提交读

可能发生

可能发生

可能发生

ReadCommitted

提交读

--

可能发生

可能发生

RepeatableRead

重复读

--

--

可能发生

Serializable

序列化

--

--

--

注:“可能发生”表示这个隔离级别会发生对应的现象,“--”表示不会发生。

2.1.5.5 主流数据库的默认级别

序号

数据库

默认隔离级别

1

Oracle

ReadCommitted

2

SqlServer

ReadCommitted

3

MySQL(InnoDB)

RepeatableRead

2.2 @Transactional的代码实现

2.2.1 @Transactional的propagation属性代码示例

2.2.1.1 Propagation.REQUIRED示例

以下代码事务的传播行为使用Propagation.REQUIRED,即两次操作使用了同一个事务,当发生异常后数据进行了回滚,两次插入的数据均没有入库。

/**
 * 事务传播行为 Propagation.REQUIRED 测试
 */
@Transactional(propagation = Propagation.REQUIRED)
@Override
public boolean requiredTest() {
    this. saveMethod1 ();
    SysName sysName = new SysName ()
            .setName("name-" + System.currentTimeMillis())
            .setCreateTime(new Date());
    boolean result = this.insert(sysName );
    if (result) {
        throw new RuntimeException("抛一个异常出来看看数据有没有回滚?");
    }
    return result;
}


public void saveMethod1() {
    SysName sysName = new SysName ()
            .setName("name-" + System.currentTimeMillis())
            .setCreateTime(new Date());
    this.insert(sysName );
}

       以上代码是在同一个事务下进行操作的,即数据保存要么成功,要么失败。假如说现在有一个需求,要求操作1成功后不管后面是否有异常,均不影响操作1的数据存储。在这样的需求下,我们可能会想到在操作1加入一个新的事务,这样原来的事务就不会影响到新的事务了。比如 saveMethod1 ()方法上面再加入注解 @Transactional,设置 propagation 属性为 Propagation.REQUIRES_NEW来实现这个需求的功能。代码如下。

/**
 * 事务传播行为 Propagation.REQUIRES_NEW的不正确的使用测试
 */
@Transactional(propagation = Propagation.REQUIRED)
@Override
public boolean requiresNewWrongTest() {
    this.saveMethod1();
    SysName sysName = new SysName ()
            .setName("name2-" + System.currentTimeMillis())
            .setCreateTime(new Date());
    boolean result = this.insert(sysName );
    if (result) {
        throw new RuntimeException("[requiresNewWrongTest]抛一个异常出来看看数据有没有回滚?");
    }
    return result;
}


@Transactional(propagation = Propagation.REQUIRES_NEW)
public void saveMethod1() {
    SysName sysName = new SysName ()
            .setName("name1-" + System.currentTimeMillis())
            .setCreateTime(new Date());
    this.insert(sysName );
}

       运行后发现,数据也没有插入数据库中,这更我们的意想中的结果完全不一致,这看起来不科学啊?然而,查看日志发现,这两个方法都是出于同一个事务中,saveMethod1()方法并没有使用新的事务。

为什么会是这样呢?来看看官方的文档:

https://docs.spring.io/spring/docs/4.3.13.RELEASE/spring-framework-reference/htmlsingle/#transaction-declarative-annotations

(1)创建事务的代理实现图如下:

http://img.nextyu.com/2017-12-02-15121941694494.jpg

 

       大概的意思是:在默认的代理模式下,只有目标方法由外部调用,才能被Spring的事务拦截器拦截。在同一个类中的两个方法直接调用,是不会被Spring的事务拦截器拦截,就像上面的requiresNewWrongTest方法直接调用了同一个类中的 saveMethod1方法,saveMethod1方法不会被 Spring 的事务拦截器拦截。可以使用 AspectJ取代Spring AOP代理来解决这个问题

2.2.1.2 Propagation. REQUIRES_NEW的正确示例

       对于上述出现的问题及分析,我们可以创建一个新的类去实现上述的需求。创建一个新的类如下:

@Slf4j
@Service
public class RequiresNewServiceImpl extends ServiceImpl<SysName Mapper, SysName> {
    /**
     * 事务传播行为 Propagation.REQUIRES_NEW 测试
     */
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void saveMethod1() {
        SysName sysName = new SysName ()
                .setName("name1-" + System.currentTimeMillis())
                .setCreateTime(new Date());
        this.insert(sysName );
    }
}

       事务传播行为 Propagation.REQUIRES_NEW 的正确的使用代码如下:

/**
 * 事务传播行为 Propagation.REQUIRES_NEW 的正确的使用测试
 */
@Transactional(propagation = Propagation.REQUIRED)
@Override
public boolean requiresNewTest() {
    requiresNewServiceImpl.saveMethod1();
    SysName sysName  = new SysName ()
            .setName("name2-" + System.currentTimeMillis())
            .setCreateTime(new Date());
    boolean result = this.insert(sysName );
    if (result) {
        throw new RuntimeException("[requiredTest]抛一个异常出来看看数据有没有回滚?");
    }
    return result;
}

       从运行的结构可以看出,尽管requiresNewServiceImpl.saveMethod1()后面的代码抛出了异常,事务进行了回滚,但不影响saveMethod1()数据的存储。分析日志,这段代码执行中创建了两个事务。

      从日志中看出,首先创建了requiresNewTes()方法的事务,由于requiresNewServiceImpl.saveMethod1()方法的@Transactional的propagation属性为Propagation.REQUIRES_NEW,所以接着暂停了requiresNewTes()方法的事务,重新创建了requiresNewServiceImpl.saveMethod1()方法的事务,接着 requiresNewServiceImpl.saveMethod1()方法成功执行后事务提交,然后requiresNewTes()抛出异常进行回滚。这就印证了只有目标方法由外部调用,才能被Spring的事务拦截器拦截。

2.2.1.3 Propagation的另外几种实例

1) requiresNewTes()方法的@Transactional注解去掉,requiresNewServiceImpl.saveMethod1()的@Transactional 注解保持不变,从日志就可以看出,只会创建一个 requiresNewServiceImpl.saveMethod1()方法的事务,两条数据都会插入。

2) 把requiresNewTes()方法的@Transactional注解去掉,requiresNewTes()方法改为调用内部的 saveMethod1()方法,从日志就可以看出,完全没有创建任何事务,两条数据都会插入。

2.2.2 @Transactional的rollbackFor属性代码示例

2.2.2.1 用户转账的事务实现

/**
 * 用户转账
 *
 * @param userId   转账用户ID
 * @param toUserId 转账目标用户ID
 * @param amount   转账金额
 */
@Transactional
@Override
public String transfer(Long userId, Long toUserId, Long amount) throws Exception {
    Asset asset = this.selectAsset(userId);
    if (asset == null) {
        return "Transfer user asset is not exist.";
    }
    AssetUser toAsset = this.selectAsset(toUserId);
    if (toAsset == null) {
        return "Transfer to user asset is not exist.";
    }

    //扣减金额
    AssetUser assetUpdate = new AssetUser().setId(asset.getId())
            .setBalance(asset.getBalance() - amount)
            .setUpdateTime(new Date());
    this.updateById(assetUpdate);

    //写入流水
    assetStatementService.saveAssetStatement(userId, toUserId, -amount, assetUpdate.getBalance());

    //增加金额
    AssetUser toAssetUpdate = new AssetUser().setId(toAsset.getId())
            .setBalance(toAsset.getBalance() + amount)
            .setUpdateTime(new Date());
    this.updateById(toAssetUpdate);

    //写入流水
    assetStatementService.saveAssetStatement(toUserId, userId, amount, toAssetUpdate.getBalance());

    return "OK";
}

//这段代码执行是否会有问题?

       运行这段代码,当运行的过程中发生了Exception异常的时候,数据并没有进行回滚。那是因为@Transactional这个注解只会在RuntimeException(运行时异常),这种异常时才会进行数据回滚,而Exception(受检异常)抛出的时候,是不会进行数据回滚的,这个时候我们希望在程序执行的过程中抛出Exception异常的时候能够进行数据回滚,该怎么办?

解决办法,只需要在@Transactional后面加上(rollbackFor = Exception.class)就能完美的解决这个问题了。

/**
 * 用户转账
 *
 * @param userId   转账用户ID
 * @param toUserId 转账目标用户ID
 * @param amount   转账金额
 */
@Transactional(rollbackFor = Exception.class)
@Override
public String transfer(Long userId, Long toUserId, Long amount) throws Exception {
    Asset asset = this.selectAsset(userId);
    if (asset == null) {
        return "Transfer user asset is not exist.";
    }
    Asset toAsset = this.selectAsset(toUserId);
    if (toAsset == null) {
        return "Transfer toUser asset is not exist.";
    }
        // 此处省略几百行代码
}

 

2.2.3 @Transactional事务的实现机制

在应用系统调用声明了@Transactional 的目标方法时,Spring Framework 默认使用 AOP代理,在代码运行时生成一个代理对象,根据@Transactional的属性配置信息,这个代理对象决定该声明@Transactional的目标方法是否由拦截器TransactionInterceptor来使用拦截,在TransactionInterceptor拦截时,会在目标方法开始执行之前创建并加入事务,并执行目标方法的逻辑, 最后根据执行情况是否出现异常,利用抽象事务管理器 AbstractPlatformTransactionManager 操作数据源 DataSource 提交或回滚事务。

Spring AOP代理有CglibAopProxy和JdkDynamicAopProxy两种,以CglibAopProxy为例,对于 CglibAopProxy,需要调用其内部类的DynamicAdvisedInterceptor的intercept方法。对于 JdkDynamicAopProxy,需要调用其 invoke 方法。

如下图所示:Spring事务实现机制

事务管理的框架是由抽象事务管理器AbstractPlatformTransactionManager来提供的,而具体的底层事务处理实现,由PlatformTransactionManager的具体实现类来实现,如事务管理器DataSourceTransactionManager。不同的事务管理器管理不同的数据资源DataSource,比如DataSourceTransactionManager管理JDBC的 Connection。

2.2.4 @Transactional事务的使用注意点

(1)@Transactional用来类上,不要用来接口上,声明在接口上可能注解会无效;

(2)@Transactional一般用在方法上,对于查询方法不需要使用事务,如果用在类上,影响查询方法的性能;

(3)@Transactional注解只能应用到public可见度的方法上。 如果你在protected,private或者package-visible 的方法上使用 @Transactional注解,它也不会报错, 但是这个被注解的方法将不会展示已配置的事务设置;

(4)外部调用某个类的没有使用@Transactional注解的方法,该方法内部调这个类的一个有事务注解的其它方法,则即使这个方法使用了事务也不会生效。比如方法requiresNewTest()requiresNewTest()再调用本类的方法saveMethod1()(不管saveMethod1()是否public还是private),但requiresNewTest()没有声明注解事务。则外部调用requiresNewTest()之后,saveMethod1()的事务是不会起作用的。

3 编程式事务

3.1 编程式事务接口定义

3.1.1 编程式事务常用接口及类的定义

(1)PlatformTransactionManager, 事务管理接口定义。

public interface PlatformTransactionManager {

    /**
     * 根据指定的传播行为,返回当前活动的事务或创建新的事务。
     *
     * @param transactionDefinition
     */
    TransactionStatus getTransaction(@Nullable TransactionDefinition transactionDefinition) throws TransactionException;


    /**
     * 事务提交
     *
     * @param transactionStatus 事务状态
     */
    void commit(TransactionStatus transactionStatus) throws TransactionException;


    /**
     * 事务回滚
     *
     * @param transactionStatus 事务状态
     */
    void rollback(TransactionStatus transactionStatus) throws TransactionException;
}

(2)DataSourceTransactionManager,数据源事务管理实现类。

public class DataSourceTransactionManager extends AbstractPlatformTransactionManager implements ResourceTransactionManager, InitializingBean {

    @Nullable
    private DataSource dataSource;
    private boolean enforceReadOnly;


    public DataSourceTransactionManager() {
        this.enforceReadOnly = false;
        this.setNestedTransactionAllowed(true);
    }

    public DataSourceTransactionManager(DataSource dataSource) {
        this();
        this.setDataSource(dataSource);
        this.afterPropertiesSet();
    }
}

(3) TransactionDefinition:事务传播行为及事务隔离级别接口定义。

public interface TransactionDefinition {

    int PROPAGATION_REQUIRED = 0;

    int PROPAGATION_SUPPORTS = 1;

    int PROPAGATION_MANDATORY = 2;

    int PROPAGATION_REQUIRES_NEW = 3;

    int PROPAGATION_NOT_SUPPORTED = 4;

    int PROPAGATION_NEVER = 5;

    int PROPAGATION_NESTED = 6;

    int ISOLATION_DEFAULT = -1;

    int ISOLATION_READ_UNCOMMITTED = 1;

    int ISOLATION_READ_COMMITTED = 2;

    int ISOLATION_REPEATABLE_READ = 4;

    int ISOLATION_SERIALIZABLE = 8;

    int TIMEOUT_DEFAULT = -1;

    int getPropagationBehavior();

    int getIsolationLevel();

    int getTimeout();

    boolean isReadOnly();

    @Nullable
    String getName();
}

(4) DefaultTransactionDefinition:事务传播行为及事务隔离级别的实现类。

(5) TransactionTemplate:事务模板类定义。

(6) TransactionStatus:事务状态接口接口定义。

public interface TransactionStatus extends SavepointManager, Flushable {

    boolean isNewTransaction();

    boolean hasSavepoint();

    void setRollbackOnly();

    boolean isRollbackOnly();

    void flush();

    boolean isCompleted();
}

3.2 编程式事务的代码实现

3.2.1 使用TransactionTemplate 执行具有返回值的事务,代码如下:

/**
 * TransactionTemplate 具有返回值的事务执行
 */
@Override
public int transactionWithResultTest() {
    Object result = this.transactionTemplate.execute(new TransactionCallback<Object>() {
        @Override
        public Object doInTransaction(TransactionStatus status) {
            try {
                saveMethod1();
                saveMethod2();
                return 0;
            } catch (Exception e) {
                status.setRollbackOnly();
            }
            return null;
        }
    });
    return 0;
}

3.2.2 TransactionTemplate 执行没有返回值的事务,代码如下:

/**
 * TransactionTemplate 没有返回值的事务执行
 */
@Override
public boolean transactionWithoutResultTest() {
    Holder<Boolean> result = new Holder<>(true);
    this.transactionTemplate.execute(new TransactionCallbackWithoutResult() {
        @Override
        protected void doInTransactionWithoutResult(TransactionStatus status) {
            try {
                saveMethod1();
                saveMethod2();

            } catch (Exception e) {
                status.setRollbackOnly();
                result.value = false;
            }
        }
    });
    return result.value;
}

3.2.3 使用transactionManager事务管理器来管理事务,代码如下:

/**
 * 直接使用transactionManager事务管理器来管理事务测试
 */
@Override
public boolean transactionManagerTest() {
    DefaultTransactionDefinition definition = new DefaultTransactionDefinition();
    PlatformTransactionManager transactionManager = new DataSourceTransactionManager(this.dataSource);
    TransactionStatus transactionStatus = transactionManager.getTransaction(definition);
    try {
        this.saveMethod1();
        this.saveMethod2();
        transactionManager.commit(transactionStatus);
        return true;
    } catch (Exception ex) {
        log.error("Transaction manager execute exception.", ex);
        transactionManager.rollback(transactionStatus);
        return false;
    }
}

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值