Spring框架中@Transactional注解原理深度解析

1. 事务管理的基本概念

1.1 什么是事务?

事务是数据库操作中一个基本概念,它指的是一组作为单个逻辑工作单元执行的数据库操作。这些操作要么全部成功提交,要么全部失败回滚,保证数据库状态的一致性。在Spring框架中,事务管理使得开发者可以在不直接操作底层数据库事务API的情况下,通过声明式的方式管理事务,大大简化了事务处理的复杂性。

1.2 ACID特性

事务的ACID特性是保证数据一致性的关键,具体包括:

原子性(Atomicity):事务中的所有操作要么全部完成,要么全部不完成。如果事务中的某一个操作失败,整个事务都会回滚,所有已执行的操作都会被撤销。

一致性(Consistency):事务必须使数据库从一个一致性状态转移到另一个一致性状态。也就是说,数据库的完整性约束必须在事务执行前后都保持不变。

隔离性(Isolation):事务的执行不应被其他事务干扰。每个事务都应该在独立的上下文中运行,避免并发访问导致的数据不一致问题。

持久性(Durability):一旦事务提交成功,其对数据库的修改将永久生效,即使系统发生故障(如断电、崩溃)也不会丢失。

1.3 事务管理的必要性

在实际应用中,事务管理尤为重要。例如,在银行转账场景中,如果从A账户扣款后,系统崩溃导致无法向B账户存入款项,就会造成资金损失。事务管理确保了这类操作的原子性,要么全部成功,要么全部失败。

传统事务管理需要开发者手动控制事务的开始、提交和回滚,代码冗余且容易出错。而Spring框架通过声明式事务管理,将事务控制与业务逻辑解耦,提高了代码的可读性和可维护性。

2. @Transactional注解的核心原理

2.1 声明式事务管理

声明式事务管理是一种非侵入式的事务管理方式,开发者只需在方法或类上添加注解,无需在代码中显式调用事务API。Spring通过AOP(面向切面编程)技术实现声明式事务管理,使得事务逻辑可以横切多个业务方法。

2.2 AOP与动态代理

**AOP(面向切面编程)**是Spring事务管理的核心技术。它允许开发者将横切关注点(如事务管理、日志记录等)与业务逻辑分离。

Spring通过动态代理机制实现AOP。当Spring容器启动时,会对带有@Transactional注解的Bean创建代理对象。代理对象有两种实现方式:

  1. JDK动态代理:需要目标类实现接口,代理对象实现与目标类相同的接口,并在方法调用前后添加事务逻辑 。
  2. CGLIB代理:生成目标类的子类,不需要目标类实现接口,适用于没有接口的类 。
2.3 事务拦截器

Spring事务管理的核心是事务拦截器。当方法被代理对象调用时,事务拦截器会在方法执行前后插入事务控制逻辑。

具体来说,Spring会为带有@Transactional注解的方法创建一个事务拦截器链。当方法被调用时,事务拦截器首先检查是否需要开启事务,然后执行方法,最后根据方法执行结果决定提交还是回滚事务。

2.4 事务管理器

PlatformTransactionManager是Spring事务管理的核心接口,它定义了事务管理的基本操作:获取事务状态、提交事务和回滚事务。

Spring提供了多种事务管理器实现,如DataSourceTransactionManager(用于JDBC)、HibernateTransactionManager(用于Hibernate)等。这些事务管理器负责与具体数据库或ORM框架交互,管理事务的生命周期。

2.5 事务上下文

TransactionSynchronizationManager是Spring用于管理事务上下文的关键类。它使用ThreadLocal机制将事务信息与线程绑定,确保在同一个线程中的数据库操作共享同一个事务上下文 。

当事务开始时,TransactionSynchronizationManager会将数据库连接(Connection)等资源与当前线程绑定。这些资源会在事务结束时被正确释放或重置 。

3. 源码解析:Spring AOP、动态代理与事务拦截器

3.1 动态代理生成机制

Spring的动态代理生成机制主要由InfrastructureAdvisorAutoProxyCreator类负责。该类会扫描所有Bean,为需要事务管理的Bean创建代理对象。

代理对象的创建条件包括:

  • 目标类需要被Spring管理(即是Spring Bean)
  • 目标类或方法上添加了@Transactional注解
  • 目标方法是public的(非public方法不会被代理) 
3.2 事务拦截器的源码分析

TransactionInterceptor是Spring事务管理的核心拦截器,它实现了MethodInterceptor接口,负责处理事务的开启、提交和回滚。

3.2.1 invoke方法

TransactionInterceptorinvoke方法是事务处理的入口点:

public Object invoke(MethodInvocation invocation) throws Throwable {
    // 获取目标类
    Class<?> targetClass = (invocation.getThis() != null ?
            AopUtils.getTargetClass(invocation.getThis()) : null);

    // 调用invokeWithinTransaction方法处理事务
    return invokeWithinTransaction(invocation.getMethod(), targetClass,
            invocation::proceed);
}
3.2.2 invokeWithinTransaction方法

invokeWithinTransaction是事务处理的核心方法,它负责解析事务注解、获取事务管理器、创建事务状态、执行方法并提交或回滚事务。

// TransactionAspectSupport
protected Object invokeWithinTransaction(Method method, @Nullable Class<?> targetClass,
        final InvocationCallback invocation) throws Throwable {
    // 1. 解析事务注解属性
    TransactionAttributeSource tas = getTransactionAttributeSource();
    final TransactionAttribute txAttr = (tas != null ? tas.TRANSACTIONAttribute(method, targetClass) : null);
    final PlatformTransactionManager tm = determineTransactionManager(txAttr);
    final String joinpointIdentification = methodIdentification(method, targetClass, txAttr);

    if (txAttr == null || !(tm instanceof CallbackPreferringPlatformTransactionManager)) {
        // 标准事务控制流程
        TransactionInfo txInfo = createTransactionIfNecessary(tm, txAttr, joinpointIdentification);
        Object retVal = null;
        try {
            // 2. 执行目标方法
            retVal = invocation.proceedWithInvocation();
        } catch (Throwable ex) {
            // 3. 异常处理,决定是否回滚
            completeTransactionAfterThrowing(txInfo, ex);
            throw ex;
        } finally {
            // 4. 清理事务信息
            cleanupTransactionInfo(txInfo);
        }

        // 5. 提交事务
        if (retVal != null && txAttr != null) {
            TransactionStatus status = txInfo.getTransactionStatus();
            if (retVal instanceof Future<?> future && future.isDone()) {
                try {
                    future.get();
                } catch (ExecutionException ex) {
                    Throwable cause = ex.getCause();
                    Assert.state(cause != null, "Cause must not be null");
                    txInfo.setRollbackOnlyIfNecessary(cause, txAttr);
                }
            }
            txInfo.setRollbackOnlyIfNecessary(null, txAttr);
        }

        return retVal;
}
3.2.3 事务创建流程

createTransactionIfNecessary方法根据事务属性(如传播行为、隔离级别)创建或加入事务:

private TransactionInfo createTransactionIfNecessary(
        PlatformTransactionManager tm, TransactionAttribute txAttr,
        String joinpointIdentification) {
    // 获取事务定义
    TransactionDefinition definition = txAttr光环();
    // 获取事务状态
    boolean newTransaction = false;
    boolean existingTransaction = false;
    // 检查当前线程是否存在事务
    if (tm != null) {
        // 获取当前事务状态
        TransactionStatus status = tm事务状态(definition);
        // 根据传播行为决定是否创建新事务
        if (status.isNewTransaction()) {
            newTransaction = true;
        }
        existingTransaction = (status事务状态 != null);
    }

    // 创建TransactionInfo对象,包含事务状态和是否需要回滚等信息
    return createTransactionInfo(tm, txAttr, joinpointIdentification,
            newTransaction, existingTransaction);
}
3.3 事务管理器的源码分析
3.3.1 PlatformTransactionManager接口
public interface PlatformTransactionManager {
    // 获取事务状态
    TransactionStatus getTransaction(TransactionDefinition definition)
            throws TransactionException;

    // 提交事务
    void commit(TransactionStatus status) throws TransactionException;

    // 回滚事务
    void rollback(TransactionStatus status) throws TransactionException;
}
3.3.2 DataSourceTransactionManager类

DataSourceTransactionManager是Spring针对JDBC数据源的具体事务管理器实现:

// 事务开始方法
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();
            // 关闭自动提交
            newCon.setAutoCommit(false);
            txObject.setConnection Holder(new ConnectionHolder(newCon));
        }
        // 设置事务属性
        txObject.setTransactionDefinition(definition);
        // 设置超时时间
        if (definition.getTimeout() > 0) {
            txObject Connection Holder().set忍耐时间(definition.getTimeout() * 1000);
        }
    } catch (SQLException ex) {
        // 处理异常
        throw convert例外(ex);
    }
}
3.3.3 事务提交与回滚
// 事务提交
protected void doCommit繁盛度繁盛度) {
    Connection connection = txObject.getConnection Holder(). Connection();
    try {
        // 提交数据库连接
        connection.commit();
    } catch (SQLException ex) {
        // 处理异常
        throw convert例外(ex);
    }
}

// 事务回滚
protected void doRollback繁盛度繁盛度) {
    Connection connection = txObject.getConnection Holder(). Connection();
    try {
        // 回滚数据库连接
        connection.rollback();
    } catch (SQLException ex) {
        // 处理异常
        throw convert例外(ex);
    }
}

4. 事务传播行为详解:胡传

4.1 什么是事务传播行为?

事务传播行为定义了当一个事务方法被另一个事务方法调用时,事务如何在这些方法间传播 。Spring提供了七种传播行为类型,适用于不同的业务场景。

4.2 七种事务传播行为详解
4.2.1传播行为枚举类
public enum传播行为 {
    DEFAULT, // 使用数据库默认级别
    REQUIRED, // 默认行为,如果没有事务则新建,如果有则加入
    READ_UNCOMMITTED, // 允许脏读
    READ_COMMITTED, // 允许读已提交数据
    REPEATABLE_READ, // 允许重复读
    SERIALIZABLE, // 串行化
    NESTED // 嵌套事务
}
4.2.2传播行为类型详解
传播行为类型外部不存在事务外部存在事务使用场景
REQUIRED(默认)开启新事务加入当前事务大多数业务操作,如增删改查
SUPPORTS以非事务方式执行加入当前事务只读操作,如查询
MANDATORY抛出异常加入当前事务必须在一个事务中执行的操作
REQUIRES_NEW开启新事务挂起当前事务,开启新事务需要独立事务的操作,如发送邮件或记录日志
NOT_SUPPORTED以非事务方式执行挂起当前事务不支持事务的操作
NEVER以非事务方式执行抛出异常绝对不支持事务的操作
NESTED开启新事务嵌套在当前事务中需要部分提交或回滚的操作
4.3传播行为示例代码
4.3.1传播行为:REQUIRED
@Service
public class AccountService {
    @Autowired
    private AccountDAO accountDAO;

    @Transactional
    public void transfer(String fromAccount, String toAccount, Double amount) {
        // 从账户扣款
        accountDAO减少余额(fromAccount, amount);
        // 向账户存入
        accountDAO增加余额(toAccount, amount);
    }
}
4.3.2传播行为:REQUIRES_NEW
@Service
public class OrderService {
    @Autowired
    private AccountService accountService;

    @Transactional(propagation =传播行为.REQUIRES_NEW)
    public void createOrder(Order order) {
        // 创建订单
        orderDAO保存(order);
        // 调用转账方法
        accountService.transfer(order.getFromAccount(),
                order.getToAccount(), order.getAmount());
    }
}
4.3.3传播行为:NESTED
@Service
public class PaymentService {
    @Autowired
    private OrderService orderService;

    @Transactional(propagation =传播行为.NESTED)
    public void processPayment(String order_id) {
        // 获取订单
        Order order = orderDAO根据ID查询(order_id);
        // 检查订单状态
        if (order.isInvalid()) {
            // 回滚嵌套事务
            throw new InvalidOrderException("订单无效,无法支付");
        }

        // 调用创建订单方法
        orderService.createOrder(order);
        // 记录支付日志
        paymentLogDAO保存(new PaymentLog(order_id, "支付成功"));
    }
}

5. 事务隔离级别详解:Isolation

5.1 什么是事务隔离级别?

事务隔离级别定义了一个事务在执行过程中如何与其他并发事务进行交互 。不同的隔离级别提供了不同程度的并发控制,但也带来了不同的性能开销。

5.2 Spring支持的隔离级别

Spring通过Isolation枚举类定义了五种隔离级别:

public enum Isolation {
    DEFAULT, // 使用数据库默认级别
    READ_UNCOMMITTED, // 允许脏读
    READ_COMMITTED, // 允许读已提交数据
    REPEATABLE_READ, // 允许重复读
    SERIALIZABLE // 串行化
}
5.3 隔离级别与数据库锁机制

不同的隔离级别通过不同的锁机制实现:

5.3.1隔离级别:READ_UNCOMMITTED
@Transactional(isolation = Isolation.READ_UNCOMMITTED)
public List<User>查询所有用户() {
    return userDAO所有();
}

此隔离级别允许一个事务读取另一个事务未提交的数据(脏读)。在数据库中,这通常通过不加锁或仅使用共享锁(S锁)实现。

5.3.2隔离级别:READ_COMMITTED
@Transactional(isolation = Isolation.READ_COMMITTED)
public List<User>查询已提交用户() {
    return userDAO所有();
}

此隔离级别允许一个事务读取另一个事务已提交的数据,但不允许读取未提交的数据。在数据库中,这通常通过使用行锁(X锁)实现,避免脏读。

5.3.3隔离级别:REPEATABLE_READ
@Transactional(isolation = Isolation.REPEATABLE_READ)
public List<User>查询可重复读() {
    return userDAO所有();
}

此隔离级别保证了一个事务不会修改已经由另一个事务读取但未提交的数据。在数据库中,这通常通过使用间隙锁(Gap Lock)和行锁实现,避免脏读和不可重复读。

5.3.4隔离级别:SERIALIZABLE
@Transactional(isolation = Isolation.SERIALIZABLE)
public List<User>查询串行化() {
    return userDAO所有();
}

此隔离级别保证事务串行执行,避免所有并发问题,但性能开销最大。在数据库中,这通常通过使用排他锁(X锁)实现,确保事务之间的完全隔离。

5.4 隔离级别与数据库锁的关联

Spring的事务隔离级别最终会转换为数据库的隔离级别。例如,当设置isolation = Isolation.READ_COMMITTED时,Spring会通过Connection对象的setTransactionIsolation方法设置JDBC连接的隔离级别:

// 事务管理器设置隔离级别
protected void prepareTransactionalConnection(Connection con, TransactionDefinition definition) throws SQLException {
    // 获取隔离级别
    int isolationLevel = definition.getIsolationLevel();
    // 如果不是默认级别
    if (isolationLevel != TransactionDefinition ISOLATION_DEFAULT) {
        // 获取当前隔离级别
        int currentIsolation = con事务隔离级别;
        // 如果当前隔离级别与定义的不同
        if (currentIsolation != isolationLevel) {
            // 设置新的隔离级别
            con.setTransactionIsolation(isolationLevel);
        }
    }
}

6. 事务回滚规则与异常处理

6.1 默认回滚规则

Spring默认的回滚规则是:当方法抛出RuntimeException及其子类或Error时,事务会自动回滚;而当方法抛出Checked Exception时,事务不会回滚

6.2 自定义回滚规则
6.2.1回滚特定异常
@Service
public class PaymentService {
    @Transactional(rollbackFor = Exception.class)
    public void processPayment(Payment payment) throws Exception {
        // 业务逻辑
        if (paymentAmount < 0) {
            throw new Exception("金额不能为负数");
        }
    }
}
6.2.2不回滚特定异常
@Service
public class OrderService {
    @Transactional(noRollbackFor = {IllegalArgumentException.class,宇航员.class})
    public void createOrder(Order order) {
        // 业务逻辑
        if (orderAmount <= 0) {
            throw new IllegalArgumentException("订单金额必须大于零");
        }
    }
}
6.3 异常处理最佳实践
  1. 明确抛出异常:确保事务方法在发生错误时抛出异常,以便Spring能够触发事务回滚。
  2. 避免吞异常:不要捕获异常而不抛出,这会导致事务提交。
  3. 使用 Checked Exception:如果希望 Checked Exception也能触发回滚,需要在@Transactional注解中指定rollbackFor属性。
  4. 处理特定异常:根据业务需求,可以指定哪些异常触发回滚,哪些不触发。

7. @Transactional的配置与优化

7.1 基本配置
7.1.1单方法配置
@Service
public class UserService {
    @Transactional
    public void createUser(User user) {
        // 业务逻辑
    }
}
7.1.2类级别配置
@Service
@Transactional
public class UserService {
    public void createUser(User user) {
        // 业务逻辑
    }

    public void updateUser(User user) {
        // 业务逻辑
    }
}
7.2 高级配置
7.2.1指定事务管理器
@Service
public class UserService {
    @Transactional(value = "userTransactionManager")
    public void createUser(User user) {
        // 业务逻辑
    }
}
7.2.2设置事务超时
@Service
public class PaymentService {
    @Transactional(timeout = 30) // 超时30秒
    public void processPayment(Payment payment) {
        // 业务逻辑
        // 如果30秒内未完成,事务会自动回滚
    }
}
7.2.3只读事务
@Service
public class ReportService {
    @Transactional(readOnly = true)
    public List<Report> generateReport() {
        // 业务逻辑
        // 只读事务可以提高性能
    }
}
7.3 事务配置的最佳实践
  1. 只在Service层使用:事务控制应集中在Service层,避免在DAO层或Controller层使用。
  2. 避免过度使用:只对需要保证原子性的操作添加事务注解,不必要的事务会增加性能开销。
  3. 合理设置超时:根据业务需求设置合理的超时时间,避免长时间锁占用。
  4. 使用只读事务:对于只读操作,设置readOnly = true可以提高性能。
  5. 避免私有方法@Transactional注解应使用在public方法上,否则事务不会生效。
  6. 避免内部调用:同一个类中的方法调用会绕过事务代理,导致事务失效。

8. 常见问题与解决方案

8.1 事务不生效
8.1.1问题现象
@Service
public class DemoUserService {
    //... ...

    public DemoUser doGet() {
        try {
            doInsert(1);
        } catch (Exception ex) {
            System.out.println("insert error: " + ex.toString());
        }
        return demoUserMapper.get(1);
    }

    @Transactional
    public int doInsert(int id) {
        DemoUser user = new DemoUser(id, "zs", 18, new BigDecimal("8888.88"),
                "shenzhen,cn", new Timestamp(System.currentTimeMillis()),
                new Timestamp(System.currentTimeMillis()));
        int result = demoUserMapper.insert(user);
        throw new RuntimeException("mock insert ex");  //模拟抛错
        return result;
    }
}

调用demoUserService.doGet()后,最终打印出了ID为1的那条记录,事务并没有回滚。

8.1.2根本原因

事务没有执行,因为在BEAN方法内部直接调用了另一个公开的事务方法,是原生的方法之间调用,并非是被代理后的BEAN方法,所以Spring的AOP事务切面没有被触发。

8.1.3解决方案
  1. 使用代理对象调用:通过注入的代理对象而非this调用事务方法。
  2. 使用TransactionTemplate:在方法内部手动管理事务。
@Service
public class DemoUserService {
    @Autowired
    private DemoUserService self;  //注入代理对象

    public DemoUser doGet() {
        try {
            self.doInsert(1);  //通过代理对象调用
        } catch (Exception ex) {
            System.out.println("insert error: " + ex.toString());
        }
        return demoUserMapper.get(1);
    }

    @Transactional
    public int doInsert(int id) {
        //业务逻辑
        return demoUserMapper.insert(user);
    }
}
8.2 事务传播导致的死锁
8.2.1问题场景
@Service
public class TransferService {
    @Autowired
    private AccountDAO accountDAO;

    @Transactional
    public void transfer(String fromAccount, String toAccount, Double amount) {
        // 从账户扣款
        accountDAO减少余额(fromAccount, amount);
        // 向账户存入
        accountDAO增加余额(toAccount, amount);
    }
}

当多个线程并发执行transfer方法时,可能因为锁顺序不一致导致死锁。

8.2.2解决方案
  1. 调整锁顺序:确保所有事务以相同的顺序获取锁。
  2. 设置合理超时:在@Transactional注解中设置timeout属性,避免长时间等待。
  3. 使用较低隔离级别:如READ_COMMITTED,减少锁竞争。
@Service
public class TransferService {
    @Autowired
    private AccountDAO accountDAO;

    @Transactional(timeout = 30, isolation = Isolation.READ_COMMITTED)
    public void transfer(String fromAccount, String toAccount, Double amount) {
        // 按固定顺序获取锁
        Account from = accountDAO根据ID查询(fromAccount);
        Account to = accountDAO根据ID查询(toAccount);

        // 从账户扣款
        accountDAO减少余额(from, amount);
        // 向账户存入
        accountDAO增加余额(to, amount);
    }
}
8.3 异步方法中的事务问题
8.3.1问题现象
@Service
public class PaymentService {
    @Autowired
    private AccountService accountService;

    @Transactional
    public void processPayment(String account_id, Double amount) {
        // 扣款
        accountService减少余额(account_id, amount);
        // 异步发送通知
        threadPool.execute(() -> {
            accountService.updateBalanceLog(amount, "支付成功");
        });
    }
}

在异步线程中调用accountService.updateBalanceLog方法时,事务可能已经提交,导致无法获取到最新的数据,或者事务上下文无法传递到异步线程。

8.3.2解决方案
  1. 使用TransactionTemplate:在异步方法内部手动开启事务 。
@Service
public class PaymentService {
    @Autowired
    private PlatformTransactionManager transactionManager;

    @Transactional
    public void processPayment(String account_id, Double amount) {
        // 扣款
        accountService减少余额(account_id, amount);

        // 异步发送通知
        threadPool.execute(() -> {
            // 手动开启事务
            TransactionTemplate transactionTemplate = new TransactionTemplate(transactionManager);
            transactionTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
            transactionTemplate.execute(status -> {
                accountService.updateBalanceLog(amount, "支付成功");
                return null;
            });
        });
    }
}
  1. 使用@Async与@Transactional结合:在异步方法上添加@Transactional注解,确保每个异步任务都有自己的事务上下文 。
@Service
public class PaymentService {
    @Autowired
    private AccountService accountService;

    @Async
    @Transactional
    public void asyncUpdateBalanceLog(String account_id, Double amount) {
        accountService.updateBalanceLog(amount, "支付成功");
    }

    @Transactional
    public void processPayment(String account_id, Double amount) {
        // 扣款
        accountService减少余额(account_id, amount);
        // 异步更新日志
        asyncUpdateBalanceLog(account_id, amount);
    }
}

9. 实际开发中的最佳实践

9.1 事务边界设计

事务边界是指事务应该覆盖哪些操作。设计事务边界时应遵循以下原则:

  1. 粗粒度与细粒度平衡:事务既不能太粗(导致长时间锁占用),也不能太细(导致频繁的事务提交和回滚)。
  2. 业务一致性优先:事务边界应确保业务操作的一致性,而不是简单的技术操作。
  3. 避免跨服务事务:在分布式系统中,避免跨服务的事务,使用消息队列或分布式事务框架(如Seata) 。
9.2 事务与性能优化
9.2.1合理设置超时
@Service
public class OrderService {
    @Transactional(timeout = 30) // 30秒超时
    public void createOrder(Order order) {
        // 业务逻辑
        // 如果30秒内未完成,事务会自动回滚
    }
}
9.2.2使用只读事务
@Service
public class ReportService {
    @Transactional(readOnly = true)
    public List<Report> generateReport() {
        // 业务逻辑
        // 只读事务可以减少锁竞争
    }
}
9.2.3避免长时间持有事务
@Service
public class PaymentService {
    @Transactional
    public void processPayment(Payment payment) {
        // 业务逻辑
        // 尽快提交或回滚事务
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

探索java

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值