为了账号安全,请及时绑定邮箱和手机立即绑定

Seata四种模式详解与入门教程

概述

Seata提供了一个易于使用的分布式事务框架,支持微服务架构下的事务管理。Seata支持四种不同的事务模式:AT模式、TCC模式、SAGA模式和XA模式,每种模式有其特定的工作原理和适用场景。这些模式帮助开发者在处理复杂的分布式事务时更加得心应手。

Seata简介

Seata是一个开源的分布式事务解决方案,旨在提供一个易于使用的分布式事务框架,以支持微服务架构下的分布式事务管理。Seata的目标是帮助开发者构建可靠且一致的分布式应用。

Seata的基本概念

在Seata中,主要有以下三个角色:

  • TM(Transaction Manager):事务管理器,控制全局事务的生命周期。它可以开始一个新的全局事务,提交或回滚事务。
  • RM(Resource Manager):资源管理器,管理分支事务的执行。一个RM实例通常与一个数据库或服务相关联,负责管理该资源的事务状态。
  • TC(Transaction Coordination):事务协调器,负责维护全局事务的运行状态。TC通过调用TM提供的接口来控制全局事务的提交或回滚,并通过与各个RM进行通信来管理分支事务。
Seata的作用与应用场景

Seata的作用在于提供一套分布式事务解决方案,帮助企业开发者在微服务架构中实现一致性的事务管理。它广泛应用于以下场景:

  • 跨服务的事务管理:当一个业务逻辑涉及多个服务时,Seata可以帮助实现这些服务之间的事务一致性。
  • 数据库事务:对于连接到不同数据库的微服务,Seata可以确保这些数据库之间的事务一致性。
  • 混合事务:当业务逻辑需要同时处理SQL和消息队列等异步操作时,Seata可以保证这些操作的事务一致性。

Seata简化了分布式事务的实现,使得开发者可以更加专注于业务逻辑的实现,而无需关心底层事务的具体实现细节。

Seata四种模式概述

Seata支持四种不同的事务模式:AT模式、TCC模式、SAGA模式和XA模式。每种模式都有其特定的工作原理和适用场景。

AT模式

AT模式的工作原理

AT(Automatic Transaction)模式是Seata中最常用的一种模式,它通过数据库的Undo Log来实现分布式事务的补偿。具体来说,AT模式的工作流程如下:

  1. 全局事务开始:当全局事务开始时,TM会向TC发送一个全局事务的开始请求。TC会返回一个全局事务ID(XID),表示这个全局事务的唯一标识。
  2. 资源加入:RM会在本地事务中设置XID,将自己注册到TM中,并获取到全局事务ID。
  3. 事务提交检查:当参与方的本地事务提交时,RM会将本地事务的提交信息上报给TC。TC会检查所有参与方的提交状态。
  4. 全局提交:如果所有参与方都成功提交,TC会通知所有RM进行全局提交。RM会清除本地事务的XID,并提交本地事务。
  5. 全局回滚:如果某个参与方提交失败,TC会通知所有参与方进行全局回滚。RM会从Undo Log中恢复原始状态,并执行回滚操作。

AT模式的使用场景

AT模式适用于大多数传统的事务场景,特别是那些使用关系型数据库的应用。例如,当一个微服务需要操作多个数据库时,可以使用AT模式来确保这些数据库之间的事务一致性。

TCC模式

TCC模式的工作原理

TCC(Try-Confirm-Cancel)模式是一种两阶段提交的事务模式,它将事务的执行分为Try、Confirm和Cancel三个阶段。具体来说,TCC模式的工作流程如下:

  1. 全局事务开始:当全局事务开始时,TM会向TC发送一个全局事务的开始请求。TC会返回一个全局事务ID(XID),表示这个全局事务的唯一标识。
  2. Try阶段:在Try阶段,服务会尝试执行本地事务并预留资源。如果Try阶段成功,服务会返回成功状态;如果Try阶段失败,服务会返回失败状态。
  3. 资源加入:RM会在本地事务中设置XID,将自己注册到TM中,并获取到全局事务ID。
  4. 事务提交检查:当Try阶段成功时,TM会向TC发送一个全局事务的提交请求。TC会检查所有服务的Try阶段状态。
  5. Confirm阶段:如果所有服务的Try阶段都成功,TM会通知所有服务进行Confirm操作。Confirm阶段将完成本地事务的提交。
  6. Cancel阶段:如果任何服务的Try阶段失败,TM会通知所有服务进行Cancel操作。Cancel阶段将回滚本地事务。

TCC模式的使用场景

TCC模式适用于那些需要严格控制资源预留的应用场景。例如,当一个微服务需要操作多个第三方服务时,可以使用TCC模式来确保这些服务之间的事务一致性。TCC模式也可以适用于那些需要实现业务逻辑的复杂操作的应用场景。

示例代码

// TCC模式的示例代码

public class OrderService {
    @Autowired
    private OrderRepository orderRepository;

    public boolean tryCreateOrder(Order order) {
        // 尝试创建订单
        Order savedOrder = orderRepository.save(order);
        return savedOrder != null;
    }

    public void confirmCreateOrder(Order order) {
        // 确认订单创建
        orderRepository.confirmOrder(order.getId());
    }

    public void cancelCreateOrder(Order order) {
        // 取消订单创建
        orderRepository.cancelOrder(order.getId());
    }
}

public class StockService {
    @Autowired
    private StockRepository stockRepository;

    public boolean tryDeductStock(Stock stock) {
        // 尝试扣减库存
        Stock savedStock = stockRepository.save(stock);
        return savedStock != null;
    }

    public void confirmDeductStock(Stock stock) {
        // 确认库存扣减
        stockRepository.confirmDeduct(stock.getId());
    }

    public void cancelDeductStock(Stock stock) {
        // 取消库存扣减
        stockRepository.cancelDeduct(stock.getId());
    }
}
SAGA模式

SAGA模式的工作原理

SAGA模式是一种基于消息队列的分布式事务模式。它通过将事务的执行过程拆分成一系列本地事务,并通过消息队列来协调这些本地事务的执行。具体来说,SAGA模式的工作流程如下:

  1. 全局事务开始:当全局事务开始时,TM会向TC发送一个全局事务的开始请求。TC会返回一个全局事务ID(XID),表示这个全局事务的唯一标识。
  2. 资源加入:RM会在本地事务中设置XID,将自己注册到TM中,并获取到全局事务ID。
  3. 事务提交检查:当参与方的本地事务提交时,RM会将本地事务的提交信息上报给TC。TC会检查所有参与方的提交状态。
  4. 全局提交:如果所有参与方都成功提交,TC会通知所有RM进行全局提交。RM会清除本地事务的XID,并提交本地事务。
  5. 全局回滚:如果某个参与方提交失败,TC会通知所有参与方进行全局回滚。RM会通过消息队列来协调回滚操作,确保回滚操作的顺序性和一致性。

SAGA模式的使用场景

SAGA模式适用于那些需要实现复杂业务逻辑的应用场景。例如,当一个微服务需要操作多个异步服务时,可以使用SAGA模式来确保这些服务之间的事务一致性。

示例代码

// SAGA模式的示例代码

public class OrderService {
    @Autowired
    private OrderRepository orderRepository;
    @Autowired
    private StockService stockService;

    public void orderService() {
        // 创建订单
        Order savedOrder = orderRepository.save(order);

        // 调用库存服务扣减库存
        stockService.deductStock(order.getProduct().getId(), order.getQuantity());
    }
}

public class StockService {
    @Autowired
    private StockRepository stockRepository;

    @Async
    public void deductStock(long productId, int quantity) {
        // 扣减库存
        Stock stock = stockRepository.findByProductId(productId);
        if (stock == null || stock.getQuantity() < quantity) {
            throw new RuntimeException("库存不足");
        }
        stock.setQuantity(stock.getQuantity() - quantity);
        stockRepository.save(stock);
    }
}
XA模式

XA模式的工作原理

XA模式是一种传统的分布式事务模式,它通过XA协议来协调分布式事务的执行。具体来说,XA模式的工作流程如下:

  1. 全局事务开始:当全局事务开始时,TM会向TC发送一个全局事务的开始请求。TC会返回一个全局事务ID(XID),表示这个全局事务的唯一标识。
  2. 资源加入:RM会在本地事务中设置XID,将自己注册到TM中,并获取到全局事务ID。
  3. 事务提交检查:当参与方的本地事务提交时,RM会将本地事务的提交信息上报给TC。TC会检查所有参与方的提交状态。
  4. 全局提交:如果所有参与方都成功提交,TC会通知所有RM进行全局提交。RM会清除本地事务的XID,并提交本地事务。
  5. 全局回滚:如果某个参与方提交失败,TC会通知所有参与方进行全局回滚。RM会通过XA协议来协调回滚操作,确保回滚操作的顺序性和一致性。

XA模式的使用场景

XA模式适用于那些需要实现严格一致性的应用场景。例如,当一个微服务需要操作多个数据库时,可以使用XA模式来确保这些数据库之间的事务一致性。XA模式也可以适用于那些需要实现业务逻辑的复杂操作的应用场景。

示例代码

// XA模式的示例代码

public class OrderService {
    @Autowired
    private DataSource db1;
    @Autowired
    private DataSource db2;

    public void orderService() {
        // 开始全局事务
        UserTransaction utx = (UserTransaction) new InitialContext().lookup("java:comp/UserTransaction");
        utx.begin();

        // 创建订单
        Connection conn1 = db1.getConnection();
        try (PreparedStatement ps1 = conn1.prepareStatement("INSERT INTO order (user_id, product_id, quantity) VALUES (?, ?, ?)")) {
            ps1.setLong(1, 1L);
            ps1.setLong(2, 1L);
            ps1.setInt(3, 1);
            ps1.executeUpdate();
        }
        conn1.commit();

        // 扣减库存
        Connection conn2 = db2.getConnection();
        try (PreparedStatement ps2 = conn2.prepareStatement("UPDATE stock SET quantity = quantity - ? WHERE product_id = ?")) {
            ps2.setInt(1, 1);
            ps2.setLong(2, 1L);
            ps2.executeUpdate();
        }
        conn2.commit();

        // 提交全局事务
        utx.commit();
    }
}
AT模式详解
AT模式的工作原理

AT模式通过数据库的Undo Log来实现分布式事务的补偿。具体来说,AT模式的工作流程如下:

  1. 全局事务开始:当全局事务开始时,TM会向TC发送一个全局事务的开始请求。TC会返回一个全局事务ID(XID),表示这个全局事务的唯一标识。
  2. 资源加入:RM会在本地事务中设置XID,将自己注册到TM中,并获取到全局事务ID。
  3. 事务提交检查:当参与方的本地事务提交时,RM会将本地事务的提交信息上报给TC。TC会检查所有参与方的提交状态。
  4. 全局提交:如果所有参与方都成功提交,TC会通知所有RM进行全局提交。RM会清除本地事务的XID,并提交本地事务。
  5. 全局回滚:如果某个参与方提交失败,TC会通知所有参与方进行全局回滚。RM会从Undo Log中恢复原始状态,并执行回滚操作。

在AT模式中,每个数据库都有一个Undo Log,用来记录事务的变更操作。当事务需要回滚时,RM会从Undo Log中读取变更记录,并执行反向操作来恢复事务的初始状态。这种机制使得AT模式能够很好地支持多种数据库,并且不需要对数据库进行任何特殊的配置。

AT模式的使用场景

AT模式适用于大多数传统的事务场景,特别是那些使用关系型数据库的应用。例如,当一个微服务需要操作多个数据库时,可以使用AT模式来确保这些数据库之间的事务一致性。

示例代码

假设我们有两个数据库表orderstock,我们需要在一个全局事务中完成订单的创建和库存的扣减。下面是一个简单的示例代码:

-- 创建订单表
CREATE TABLE `order` (
  `id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '订单ID',
  `user_id` BIGINT(20) NOT NULL COMMENT '用户ID',
  `product_id` BIGINT(20) NOT NULL COMMENT '商品ID',
  `quantity` INT(11) NOT NULL COMMENT '数量',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单表';

-- 创建库存表
CREATE TABLE `stock` (
  `id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '库存ID',
  `product_id` BIGINT(20) NOT NULL COMMENT '商品ID',
  `quantity` INT(11) NOT NULL COMMENT '数量',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='库存表';
// 创建库存扣减的Java代码
public class StockService {
  @Transactional
  public void deductStock(long productId, int quantity) {
    Stock stock = stockMapper.selectByProductId(productId);
    if (stock != null && stock.getQuantity() >= quantity) {
      stock.setQuantity(stock.getQuantity() - quantity);
      stockMapper.updateStock(stock);
    } else {
      throw new RuntimeException("库存不足");
    }
  }
}

// 创建订单创建的Java代码
public class OrderService {
  @Transactional
  public void createOrder(long userId, long productId, int quantity) {
    Order order = new Order();
    order.setUserId(userId);
    order.setProductId(productId);
    order.setQuantity(quantity);
    orderMapper.insert(order);
    stockService.deductStock(productId, quantity);
  }
}

在这个示例中,我们使用了Spring的@Transactional注解来开启全局事务。当createOrder方法执行时,会创建一个新的订单,并调用deductStock方法来扣减库存。如果任何一步操作失败,全局事务将会被回滚,确保订单创建和库存扣减的原子性。

TCC模式详解
TCC模式的工作原理

TCC模式是一种两阶段提交的事务模式,它将事务的执行分为Try、Confirm和Cancel三个阶段。具体来说,TCC模式的工作流程如下:

  1. 全局事务开始:当全局事务开始时,TM会向TC发送一个全局事务的开始请求。TC会返回一个全局事务ID(XID),表示这个全局事务的唯一标识。
  2. Try阶段:在Try阶段,服务会尝试执行本地事务并预留资源。如果Try阶段成功,服务会返回成功状态;如果Try阶段失败,服务会返回失败状态。
  3. 资源加入:RM会在本地事务中设置XID,将自己注册到TM中,并获取到全局事务ID。
  4. 事务提交检查:当Try阶段成功时,TM会向TC发送一个全局事务的提交请求。TC会检查所有服务的Try阶段状态。
  5. Confirm阶段:如果所有服务的Try阶段都成功,TM会通知所有服务进行Confirm操作。Confirm阶段将完成本地事务的提交。
  6. Cancel阶段:如果任何服务的Try阶段失败,TM会通知所有服务进行Cancel操作。Cancel阶段将回滚本地事务。

在TCC模式中,每个服务都需要实现Try、Confirm和Cancel三个方法。Try阶段会尝试执行本地事务并预留资源,但不会真正提交事务。Confirm阶段会确认本地事务的提交,而Cancel阶段会取消本地事务的执行。这种设计使得TCC模式能够更好地支持复杂业务逻辑,并且可以在某些情况下实现更细粒度的事务控制。

TCC模式的使用场景

TCC模式适用于那些需要严格控制资源预留的应用场景。例如,当一个微服务需要操作多个第三方服务时,可以使用TCC模式来确保这些服务之间的事务一致性。TCC模式也可以适用于那些需要实现业务逻辑的复杂操作的应用场景。

示例代码

假设我们有两个微服务OrderServiceStockService,我们需要在一个全局事务中完成订单的创建和库存的扣减。下面是一个简单的示例代码:

// 创建订单服务
public class OrderService {
  @Autowired
  private OrderRepository orderRepository;

  public boolean tryCreateOrder(Order order) {
    // 尝试创建订单
    Order savedOrder = orderRepository.save(order);
    return savedOrder != null;
  }

  public void confirmCreateOrder(Order order) {
    // 确认订单创建
    orderRepository.confirmOrder(order.getId());
  }

  public void cancelCreateOrder(Order order) {
    // 取消订单创建
    orderRepository.cancelOrder(order.getId());
  }
}

// 创建库存服务
public class StockService {
  @Autowired
  private StockRepository stockRepository;

  public boolean tryDeductStock(Stock stock) {
    // 尝试扣减库存
    Stock savedStock = stockRepository.save(stock);
    return savedStock != null;
  }

  public void confirmDeductStock(Stock stock) {
    // 确认库存扣减
    stockRepository.confirmDeduct(stock.getId());
  }

  public void cancelDeductStock(Stock stock) {
    // 取消库存扣减
    stockRepository.cancelDeduct(stock.getId());
  }
}

在这个示例中,我们首先定义了OrderServiceStockService两个服务,它们分别负责订单的创建和库存的扣减。在OrderService中,我们定义了tryCreateOrderconfirmCreateOrdercancelCreateOrder三个方法,用来实现Try、Confirm和Cancel三个阶段的操作。同样,在StockService中,我们定义了tryDeductStockconfirmDeductStockcancelDeductStock三个方法,用来实现Try、Confirm和Cancel三个阶段的操作。

当全局事务开始时,OrderService会调用tryCreateOrder方法来尝试创建订单,并检查Try阶段是否成功。如果Try阶段成功,则会调用confirmCreateOrder方法来确认订单的创建。如果Try阶段失败,则会抛出异常,导致全局事务回滚。

同样地,当全局事务开始时,StockService会调用tryDeductStock方法来尝试扣减库存,并检查Try阶段是否成功。如果Try阶段成功,则会调用confirmDeductStock方法来确认库存的扣减。如果Try阶段失败,则会抛出异常,导致全局事务回滚。

通过这种方式,TCC模式可以实现更加严格的资源预留控制,并且可以在某些情况下实现更细粒度的事务控制。

SAGA模式详解
SAGA模式的工作原理

SAGA模式是一种基于消息队列的分布式事务模式。它通过将事务的执行过程拆分成一系列本地事务,并通过消息队列来协调这些本地事务的执行。具体来说,SAGA模式的工作流程如下:

  1. 全局事务开始:当全局事务开始时,TM会向TC发送一个全局事务的开始请求。TC会返回一个全局事务ID(XID),表示这个全局事务的唯一标识。
  2. 资源加入:RM会在本地事务中设置XID,将自己注册到TM中,并获取到全局事务ID。
  3. 事务提交检查:当参与方的本地事务提交时,RM会将本地事务的提交信息上报给TC。TC会检查所有参与方的提交状态。
  4. 全局提交:如果所有参与方都成功提交,TC会通知所有RM进行全局提交。RM会清除本地事务的XID,并提交本地事务。
  5. 全局回滚:如果某个参与方提交失败,TC会通知所有参与方进行全局回滚。RM会通过消息队列来协调回滚操作,确保回滚操作的顺序性和一致性。

在SAGA模式中,事务的执行过程被拆分成一系列本地事务,并通过消息队列来协调这些本地事务的执行。每个本地事务都会执行一个具体的业务操作,并通过消息队列来协调这些操作的顺序性。当全局事务提交时,所有本地事务都会被提交。当全局事务回滚时,所有本地事务都会被回滚。这种设计使得SAGA模式可以更好地支持复杂业务逻辑,并且可以在某些情况下实现更细粒度的事务控制。

SAGA模式的使用场景

SAGA模式适用于那些需要实现复杂业务逻辑的应用场景。例如,当一个微服务需要操作多个异步服务时,可以使用SAGA模式来确保这些服务之间的事务一致性。

示例代码

假设我们有两个微服务OrderServiceStockService,我们需要在一个全局事务中完成订单的创建和库存的扣减。下面是一个简单的示例代码:

// 创建订单服务
public class OrderService {
  @Autowired
  private OrderRepository orderRepository;
  @Autowired
  private StockService stockService;

  public void orderService() {
    // 创建订单
    Order savedOrder = orderRepository.save(order);

    // 调用库存服务扣减库存
    stockService.deductStock(order.getProduct().getId(), order.getQuantity());
  }
}

// 创建库存服务
public class StockService {
  @Autowired
  private StockRepository stockRepository;

  @Async
  public void deductStock(long productId, int quantity) {
    // 扣减库存
    Stock stock = stockRepository.findByProductId(productId);
    if (stock == null || stock.getQuantity() < quantity) {
      throw new RuntimeException("库存不足");
    }
    stock.setQuantity(stock.getQuantity() - quantity);
    stockRepository.save(stock);
  }
}

在这个示例中,我们首先定义了OrderServiceStockService两个服务,它们分别负责订单的创建和库存的扣减。在OrderService中,我们定义了orderService方法,用来创建订单并调用库存服务扣减库存。在StockService中,我们定义了deductStock方法,用来扣减库存。

当全局事务开始时,OrderService会调用orderService方法来创建订单,并调用库存服务扣减库存。由于库存服务是异步的,所以当库存服务扣减库存时,订单服务会通过消息队列来协调这些操作的顺序性。当全局事务提交时,所有本地事务都会被提交。当全局事务回滚时,所有本地事务都会被回滚。

通过这种方式,SAGA模式可以实现更细粒度的事务控制,并且可以在某些情况下实现更复杂的业务逻辑。

XA模式详解
XA模式的工作原理

XA模式是一种传统的分布式事务模式,它通过XA协议来协调分布式事务的执行。具体来说,XA模式的工作流程如下:

  1. 全局事务开始:当全局事务开始时,TM会向TC发送一个全局事务的开始请求。TC会返回一个全局事务ID(XID),表示这个全局事务的唯一标识。
  2. 资源加入:RM会在本地事务中设置XID,将自己注册到TM中,并获取到全局事务ID。
  3. 事务提交检查:当参与方的本地事务提交时,RM会将本地事务的提交信息上报给TC。TC会检查所有参与方的提交状态。
  4. 全局提交:如果所有参与方都成功提交,TC会通知所有RM进行全局提交。RM会清除本地事务的XID,并提交本地事务。
  5. 全局回滚:如果某个参与方提交失败,TC会通知所有参与方进行全局回滚。RM会通过XA协议来协调回滚操作,确保回滚操作的顺序性和一致性。

在XA模式中,每个数据库都有一个RM,用来管理本地事务的执行。当全局事务开始时,TM会向所有RM发送一个全局事务的开始请求。RM会设置本地事务的XID,并将自己注册到TM中。当参与方的本地事务提交时,RM会将本地事务的提交信息上报给TC。TC会检查所有参与方的提交状态,并决定全局事务的提交或回滚。当全局事务提交时,所有RM都会进行全局提交,并清除本地事务的XID。当全局事务回滚时,所有RM都会进行全局回滚,并清除本地事务的XID。

XA模式的使用场景

XA模式适用于那些需要实现严格一致性的应用场景。例如,当一个微服务需要操作多个数据库时,可以使用XA模式来确保这些数据库之间的事务一致性。XA模式也可以适用于那些需要实现业务逻辑的复杂操作的应用场景。

示例代码

假设我们有两个数据库db1db2,我们需要在一个全局事务中完成订单的创建和库存的扣减。下面是一个简单的示例代码:

public class OrderService {
  @Autowired
  private DataSource db1;
  @Autowired
  private DataSource db2;

  public void orderService() {
    // 开始全局事务
    UserTransaction utx = (UserTransaction) new InitialContext().lookup("java:comp/UserTransaction");
    utx.begin();

    // 创建订单
    Connection conn1 = db1.getConnection();
    try (PreparedStatement ps1 = conn1.prepareStatement("INSERT INTO order (user_id, product_id, quantity) VALUES (?, ?, ?)")) {
      ps1.setLong(1, 1L);
      ps1.setLong(2, 1L);
      ps1.setInt(3, 1);
      ps1.executeUpdate();
    }
    conn1.commit();

    // 扣减库存
    Connection conn2 = db2.getConnection();
    try (PreparedStatement ps2 = conn2.prepareStatement("UPDATE stock SET quantity = quantity - ? WHERE product_id = ?")) {
      ps2.setInt(1, 1);
      ps2.setLong(2, 1L);
      ps2.executeUpdate();
    }
    conn2.commit();

    // 提交全局事务
    utx.commit();
  }
}

在这个示例中,我们首先定义了orderService方法,用来创建订单和扣减库存。在orderService方法中,我们首先通过JNDI获取到全局事务的UserTransaction对象,并开始全局事务。然后,我们分别通过两个数据库连接来创建订单和扣减库存。当创建订单和扣减库存完成后,我们提交全局事务,确保两个操作的原子性。

通过这种方式,XA模式可以实现更严格的事务控制,并且可以在某些情况下实现更复杂的业务逻辑。

结语

通过上述对Seata四种模式的介绍和示例代码的演示,我们希望能够帮助读者更好地理解Seata的工作原理和应用场景。Seata为分布式系统提供了一种强大的事务管理解决方案,使得开发者可以更加专注于业务逻辑的实现,而无需关心底层事务的具体实现细节。希望读者能够结合实际业务场景,灵活运用Seata的各种模式,构建可靠的分布式系统。

推荐编程学习网站:

点击查看更多内容
TA 点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
  • 推荐
  • 评论
  • 收藏
  • 共同学习,写下你的评论
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消