【愚公系列】2023年12月 Java教学课程 217-分布式事务(Seata的四种事务模式)

🏆 作者简介,愚公搬代码
🏆《头衔》:华为云特约编辑,华为云云享专家,华为开发者专家,华为产品云测专家,CSDN博客专家,阿里云专家博主,阿里云签约作者,腾讯云优秀博主,腾讯云内容共创官,掘金优秀博主,51CTO博客专家等。
🏆《近期荣誉》:2022年CSDN博客之星TOP2,2022年华为云十佳博主等。
🏆《博客内容》:.NET、Java、Python、Go、Node、前端、IOS、Android、鸿蒙、Linux、物联网、网络安全、大数据、人工智能、U3D游戏、小程序等相关领域知识。
🏆🎉欢迎 👍点赞✍评论⭐收藏

文章目录

  • 🚀前言
  • 🚀一、Seata的四种事务模式
    • 🔎1.XA模式
      • 🦋1.1 两阶段提交
      • 🦋1.2 Seata的XA模型
      • 🦋1.3 优缺点
      • 🦋1.4 XA模型实战
    • 🔎2.AT模式
      • 🦋2.1 Seata的AT模型
      • 🦋2.2 脏写问题
      • 🦋2.3 优缺点
      • 🦋2.4 AT模型实战
    • 🔎3.TCC模式
      • 🦋3.1 流程分析
      • 🦋3.2 Seata的TCC模型
      • 🦋3.3 优缺点
      • 🦋3.4 事务悬挂和空回滚
        • ☀️3.4.1 空回滚
        • ☀️3.4.2 业务悬挂
      • 🦋3.5 TCC模型实战
    • 🔎4.SAGA模式
      • 🦋4.1 原理
      • 🦋4.2 Seata的XA模型
      • 🦋4.3 优缺点
      • 🦋4.4 SAGA模型实战
  • 🚀总结
  • 🚀感谢:给读者的一封信

🚀前言

Seata事务的作用是保证系统中的数据操作具有原子性、一致性、隔离性和持久性(ACID)。在分布式系统中,多个服务之间需要共享数据并保证数据一致性,Seata提供了一个分布式事务管理解决方案,可以有效地解决分布式事务的难题,确保所有操作都能够按照预期顺序执行,并在必要时回滚事务。

使用Seata可以将分布式服务中各个服务的本地事务进行协调合并,形成一个全局事务,从而保证全局事务的一致性。同时,Seata还可以进行分布式事务的监控和追踪,方便排查问题。使用Seata可以大大简化分布式事务管理的复杂度,提高系统的可靠性和稳定性。

🚀一、Seata的四种事务模式

Seata的四种事务模式是:AT(自动化事务模式)、XA(两阶段提交协议实现分布式事务)、TCC(Try-Confirm-Cancel事务模式)和Saga(分布式事务模式)。

事务模式特点
XA基于两阶段提交协议实现分布式事务,保证ACID特性,但性能较低,不适用于高并发、高吞吐量场景。需要每个参与者支持XA接口。
AT基于本地事务实现分布式事务,通过undo和redo操作来保证ACID特性,性能较XA高,但需要开发者自行实现undo和redo操作。
TCC基于try-confirm-cancel机制实现分布式事务,通过预留资源、确认执行和撤销操作来保证ACID特性,适用于高并发、分布式环境下的事务操作。
Saga基于事件流实现的分布式事务协调模式,通过补偿机制来保证ACID特性,适用于高可靠性、高灵活性、高可扩展性的分布式系统。但实现较为复杂,需要开发者具备一定的领域驱动设计和事件驱动编程经验。

🔎1.XA模式

在Seata框架中,XA模式是指分布式事务采用两阶段提交协议来保证事务的一致性和隔离性。 在XA模式下,Seata框架会为每个参与分布式事务的资源管理器(如数据库、MQ等)创建一个对应的事务管理器,通过事务管理器协调各个资源管理器的提交或回滚操作,保证分布式事务的原子性和持久性。 Seata框架提供了XA模式的分布式事务支持,可以方便地应用在各种场景中,如电商下单、金融交易等。

🦋1.1 两阶段提交

事务的两阶段提交(Two-Phase Commitment Protocol)是保证分布式事务原子性和一致性的常用协议。它由协调者和参与者两个角色组成。

第一阶段:准备阶段

  1. 协调者向所有参与者发出 prepare 消息,请求参与者准备提交事务。
  2. 参与者执行事务,并记录 undo 日志和 redo 日志。undo 日志是回滚日志,用于在事务失败时回滚事务,redo 日志是重做日志,用于在事务提交时提交事务。
  3. 参与者向协调者发送响应消息,表示已准备就绪。

第二阶段:提交阶段

  1. 协调者向所有参与者发出 commit 消息,请求参与者提交事务。
  2. 参与者收到协调者的 commit 消息后,如果事务正常执行,则提交事务并释放资源;否则回滚事务并释放资源。
  3. 参与者向协调者发送响应消息,表示已提交或已回滚事务。
  4. 当协调者收到所有参与者的响应消息后,如果所有参与者都已提交事务,则它向所有参与者发送“commit”消息,否则它向所有参与者发送“rollback”消息通知它们回滚事务。

其中在第二阶段,如果有任何一个参与者出现问题,整个事务都将回滚。两阶段提交协议虽然可以保证事务的原子性和一致性,但是由于需要等待所有参与者的响应消息,因此在高并发和分布式系统中其性能较低,同时也有单点故障问题。因此,在大规模分布式系统中,通常采用类似Seata框架提供的“补偿事务”等其他方案来保证分布式事务的一致性和可靠性。

正常情况:
异常情况:

🦋1.2 Seata的XA模型

下面是Seata的XA模型:

  1. 分布式事务协调器(TC)

TC是Seata的核心模块之一,负责协调分布式事务的各个参与者,实现全局事务的一致性和隔离性。

  1. 事务管理器(TM)

TM是分布式事务的发起者,负责启动、提交或回滚全局事务,并协调各个分支事务的提交和回滚。

  1. 资源管理器(RM)

RM是分布式事务的参与者,负责管理和操作局部资源。当RM需要参与分布式事务时,需要向TM注册,并接受TM的指令执行相应的操作。

  1. 分支事务(Branch Transaction)

对于每一个参与分布式事务的RM,都会有一个对应的分支事务。分支事务负责管理和操作局部资源,在TM的指令下执行提交和回滚。

  1. 全局事务(Global Transaction)

全局事务是一个跨越多个RM的分布式事务,包括多个分支事务。全局事务的一致性和隔离性是由TC和TM协同工作来保障的。

🦋1.3 优缺点

XA模式的优点是什么?
1. 事务的强一致性,满足ACID原则。
2. 常用数据库都支持,实现简单,并且没有代码侵入。
XA模式的缺点是什么?
1. 因为一阶段需要锁定数据库资源,等待二阶段结束才释放,性能较差。
2. 依赖关系型数据库实现事务。

🦋1.4 XA模型实战

Seata的starter已经完成了XA模式的自动装配,实现非常简单,步骤如下:

1)修改application.yml文件(每个参与事务的微服务),开启XA模式:

seata:
  data-source-proxy-mode: XA

2)给发起全局事务的入口方法添加@GlobalTransactional注解:

本例中是OrderServiceImpl中的create方法.

3)重启服务并测试

重启order-service,再次测试,发现无论怎样,三个微服务都能成功回滚。

🔎2.AT模式

🦋2.1 Seata的AT模型

Seata的AT和XA模型类似,模型图如下:

  • XA模式一阶段不提交事务,锁定资源;AT模式一阶段直接提交,不锁定资源。
  • XA模式依赖数据库机制实现回滚;AT模式利用数据快照实现数据回滚。
  • XA模式强一致;AT模式最终一致

🦋2.2 脏写问题

在多线程并发访问AT模式的分布式事务时,有可能出现脏写问题

解决思路就是引入了全局锁的概念。在释放DB锁之前,先拿到全局锁。避免同一时刻有另外一个事务来操作当前数据。

🦋2.3 优缺点

AT模式的优点
1. 一阶段完成直接提交事务,释放数据库资源,性能比较好
2. 利用全局锁实现读写隔离
3. 没有代码侵入,框架自动完成回滚和提交
AT模式的缺点
1. 两阶段之间属于软状态,属于最终一致
2. 框架的快照功能会影响性能,但比XA模式要好很多

🦋2.4 AT模型实战

AT模式需要一个表来记录全局锁、另一张表来记录数据快照undo_log。

1)导入数据库表,记录全局锁

其中lock_tabledistributed_lock导入到TC服务关联的数据库(seata),undo_log表导入到微服务关联的数据库(seata_demo):

SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
-- Table structure for undo_log
-- ----------------------------
DROP TABLE IF EXISTS `undo_log`;
CREATE TABLE `undo_log`  (
  `branch_id` bigint(20) NOT NULL COMMENT 'branch transaction id',
  `xid` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT 'global transaction id',
  `context` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT 'undo_log context,such as serialization',
  `rollback_info` longblob NOT NULL COMMENT 'rollback info',
  `log_status` int(11) NOT NULL COMMENT '0:normal status,1:defense status',
  `log_created` datetime(6) NOT NULL COMMENT 'create datetime',
  `log_modified` datetime(6) NOT NULL COMMENT 'modify datetime',
  UNIQUE INDEX `ux_undo_log`(`xid`, `branch_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = 'AT transaction mode undo table' ROW_FORMAT = Compact;

-- ----------------------------
-- Records of undo_log
-- ----------------------------

CREATE TABLE IF NOT EXISTS `lock_table`
(
    `row_key`        VARCHAR(128) NOT NULL,
    `xid`            VARCHAR(128),
    `transaction_id` BIGINT,
    `branch_id`      BIGINT       NOT NULL,
    `resource_id`    VARCHAR(256),
    `table_name`     VARCHAR(32),
    `pk`             VARCHAR(36),
    `status`         TINYINT      NOT NULL DEFAULT '0' COMMENT '0:locked ,1:rollbacking',
    `gmt_create`     DATETIME,
    `gmt_modified`   DATETIME,
    PRIMARY KEY (`row_key`),
    KEY `idx_status` (`status`),
    KEY `idx_branch_id` (`branch_id`),
    KEY `idx_xid_and_branch_id` (`xid` , `branch_id`)
) ENGINE = InnoDB;

CREATE TABLE IF NOT EXISTS `distributed_lock`
(
    `lock_key`       CHAR(20) NOT NULL,
    `lock_value`     VARCHAR(20) NOT NULL,
    `expire`         BIGINT,
    primary key (`lock_key`)
) ENGINE = InnoDB;

INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('AsyncCommitting', ' ', 0);
INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('RetryCommitting', ' ', 0);
INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('RetryRollbacking', ' ', 0);
INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('TxTimeoutCheck', ' ', 0);

2)修改application.yml文件,将事务模式修改为AT模式即可:

seata:
  data-source-proxy-mode: AT # 默认就是AT

3)重启服务并测试

🔎3.TCC模式

Seata中的TCC模式是一种基于“预留、确认、取消”三个步骤实现分布式事务的解决方案。TCC全称是“Try-Confirm-Cancel”,即尝试、确认和取消。

在TCC模式中,业务逻辑需要在三个阶段中进行处理,分别是:

  1. 尝试:在这个阶段中,资源被预留,锁定,或者资源被暂时保留。如果所有的资源都被成功预留,则TCC操作进入下一个阶段。否则,TCC操作将会进入cancel阶段。

  2. 确认:在这个阶段中,所有的资源将会被提交,TCC操作的所有的修改都将会生效。

  3. 取消:在这个阶段中,所有的资源将会被回滚,TCC操作的所有的修改都将会被撤回。

TCC模式需要业务逻辑进行事务的预留和回滚,因此实现过程相对于其他两种模式更为复杂。但是,TCC模式在实现起来相对较为灵活,在某些业务场景下可以实现更好的性能和可靠性。

🦋3.1 流程分析

Seata中TCC模式的流程可以分为以下几个步骤:

  1. 预留资源:TCC模式中的第一步即为尝试(Try)阶段,这个阶段的主要任务是预留各个资源。这一步操作一般和业务逻辑很相似,例如购买机票时,需要预留座位数和票号等信息。

  2. 确认操作:如果预留资源成功,那么下一步就是确认(Confirm)操作。该阶段会对之前预留的资源进行确认,保证所有资源都已准备就绪,可以进行下一步操作。例如完成支付等操作。

  3. 提交事务:确认操作完成之后,可以进行提交操作。这个阶段是将之前预留的资源进行提交和修改,包括更新数据库等操作。

  4. 完成事务:所有的资源提交完成后,整个分布式事务也就完成了。在这个阶段中,可以执行一些完成后的附加操作,例如发送通知等。

  5. 回滚事务:如果在任何一个阶段出现了错误或者异常情况,那么分布式事务需要回滚事务。回滚事务的操作和提交事务流程类似,但是操作的对象是预留的资源,将其还原至之前的状态。

在TCC模式下,业务逻辑需要自行实现“预留-确认-取消”三个阶段的操作,在实现过程中需要注意到每个阶段对应的事务语义。同时,调用方也需要实现分布式事务的补偿机制,保证在出现异常时能够进行事务的回滚和补偿操作。

🦋3.2 Seata的TCC模型

Seata的TCC模型类似,模型图如下:

🦋3.3 优缺点

TCC的优点是什么?
1. 一阶段完成直接提交事务,释放数据库资源,性能好
2. 相比AT模型,无需生成快照,无需使用全局锁,性能最强
3. 不依赖数据库事务,而是依赖补偿操作,可以用于非事务型数据库
TCC的缺点是什么?
1. 有代码侵入,需要人为编写try、Confirm和Cancel接口,太麻烦
2. 软状态,事务是最终一致
3. 需要考虑Confirm和Cancel的失败情况,做好幂等处理

🦋3.4 事务悬挂和空回滚

☀️3.4.1 空回滚

当一个参与者(比如数据库)接收到Seata的回滚请求时,它会将回滚操作记录下来,但不会实际执行。当所有参与者都记录完回滚操作后,Seata会向它们发送一条空操作请求,这时每个参与者只需要确认接收到了该请求,而不需要再执行之前记录的回滚操作。这样就可以避免由于网络延迟等原因导致的重复执行回滚操作,从而提高了分布式事务的效率和可靠性。

☀️3.4.2 业务悬挂

Seata 是一个分布式事务解决方案,它主要用于解决分布式系统中分布式事务的问题。Seata 的业务悬挂(Business Suspension)指的是在一个分布式事务过程中,如果一个业务出现问题或者等待某些资源导致无法继续执行,那么整个事务会被阻塞并且无法提交。这种情况称为业务悬挂。

业务悬挂是分布式事务中的常见问题之一,同时也是 Seata 作为事务解决方案所需要解决的问题之一。为了避免业务悬挂,Seata 采用了两种方法:

  1. 超时机制

Seata 使用超时机制来避免业务悬挂。当一个业务等待某些资源的时间超过了一定的时间界限,Seata 会自动回滚整个事务,避免业务悬挂的情况发生。

  1. 异步确保机制

Seata 还采用了异步确保机制来避免业务悬挂。当一个业务需要等待某些资源时,Seata 会将该业务置于一个异步队列中,并且继续执行其他可以执行的业务。当等待完毕后,Seata 再执行异步队列中的业务,确保整个事务可以成功提交。

🦋3.5 TCC模型实战

1)思路分析

这里我们定义一张表;在 seata_demo数据库中创建如下表:

CREATE TABLE `account_freeze_tbl`(
    `xid` varchar(128) NOT NULL,
    `user_id` varchar(255) DEFAULT NULL COMMENT'用户id',
    `freeze_money`int(11) unsigned DEFAULT'0'COMMENT'冻结金额',
    `state`int(1) DEFAULT NULL COMMENT'事务状态,0:try,1:confirm,2:cancel',
    PRIMARY KEY(`xid`)USING BTREE
)ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT;

流程如下:

  • Try业务:
    • 记录冻结金额和事务状态到account_freeze表
    • 扣减account表可用金额
  • Confirm业务
    • 根据xid删除account_freeze表的冻结记录
  • Cancel业务
    • 修改account_freeze表,冻结金额为0,state为2
    • 修改account表,恢复可用金额
  • 如何判断是否空回滚?
    • cancel业务中,根据xid查询account_freeze,如果为null则说明try还没做,需要空回滚
  • 如何避免业务悬挂?
    • try业务中,根据xid查询account_freeze ,如果已经存在则证明Cancel已经执行,拒绝执行try业务

2)声明TCC接口

account-service项目中的cn.itcast.account.service包中新建一个接口,声明TCC三个接口:

package cn.itcast.account.service;

import io.seata.rm.tcc.api.BusinessActionContext;
import io.seata.rm.tcc.api.BusinessActionContextParameter;
import io.seata.rm.tcc.api.LocalTCC;
import io.seata.rm.tcc.api.TwoPhaseBusinessAction;

@LocalTCC
public interface AccountTCCService {

    /**
     * 根据用户id扣减余额,记录冻结金额
     * commitMethod 提交时对于的方法;rollbackMethod回滚时对于的方法
     * @param userId 用户id
     * @param money 扣减金额
     */
    @TwoPhaseBusinessAction(name = "deduct", commitMethod = "confirm", rollbackMethod = "cancel")
    void deduct(@BusinessActionContextParameter("userId")String userId,
                @BusinessActionContextParameter("money")int money);

    /**
     * 执行TCC事务时,提交事务时执行的方法
     * @param ctx 上下文对象
     * @return 执行结果
     */
    Boolean confirm(BusinessActionContext ctx);
    /**
     * 执行TCC事务时,回滚事务时执行的方法
     * @param ctx 上下文对象
     * @return 执行结果
     */
    Boolean cancel(BusinessActionContext ctx);
}

3)编写实现类

account-service服务中的cn.itcast.account.service.impl包下新建一个类,实现TCC业务:

package cn.itcast.account.service.impl;

import cn.itcast.account.entity.AccountFreeze;
import cn.itcast.account.mapper.AccountFreezeMapper;
import cn.itcast.account.mapper.AccountMapper;
import cn.itcast.account.service.AccountTCCService;
import io.seata.core.context.RootContext;
import io.seata.rm.tcc.api.BusinessActionContext;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
public class AccountTCCServiceImpl implements AccountTCCService {

    @Autowired
    private AccountMapper accountMapper;

    @Autowired
    private AccountFreezeMapper accountFreezeMapper;

    @Override
    @Transactional
    public void deduct(String userId, int money) {
        //获取事务id
        String xid = RootContext.getXID();
        //处理业务悬挂
        AccountFreeze freeze = accountFreezeMapper.selectById(xid);
        if (freeze != null) {
            //已经cancel过,拒绝业务
            return;
        }

        //1 扣减余额
        accountMapper.deduct(userId, money);

        //2 记录冻结金额
        AccountFreeze accountFreeze = new AccountFreeze();
        accountFreeze.setXid(xid);
        accountFreeze.setUserId(userId);
        accountFreeze.setFreezeMoney(money);
        accountFreeze.setState(AccountFreeze.State.TRY);
        accountFreezeMapper.insert(accountFreeze);
    }

    @Override
    public Boolean confirm(BusinessActionContext ctx) {
        //删除冻结金额
        String xid = ctx.getXid();
        int count = accountFreezeMapper.deleteById(xid);
        return count==1;
    }

    @Override
    public Boolean cancel(BusinessActionContext ctx) {
        //获取全局事务id
        String xid = ctx.getXid();
        //用户id
        String userId = ctx.getActionContext("userId").toString();
        AccountFreeze accountFreeze = accountFreezeMapper.selectById(xid);

        //处理空回滚
        if (accountFreeze == null) {
            accountFreeze = new AccountFreeze();
            accountFreeze.setXid(xid);
            accountFreeze.setUserId(userId);
            accountFreeze.setFreezeMoney(0);
            accountFreeze.setState(AccountFreeze.State.CANCEL);
            accountFreezeMapper.insert(accountFreeze);
            return true;
        }

        //幂等处理
        if (accountFreeze.getState() == AccountFreeze.State.CANCEL) {
            //说明已经回滚过了;不需要再处理
            return true;
        }

        //1 回退金额
        accountMapper.refund(userId, accountFreeze.getFreezeMoney());

        //2 更新冻结金额为0和状态
        accountFreeze.setFreezeMoney(0);
        accountFreeze.setState(AccountFreeze.State.CANCEL);
        int count = accountFreezeMapper.updateById(accountFreeze);

        return count==1;
    }
}

改造 src/main/java/cn/itcast/account/web/AccountController.java 如下:

🔎4.SAGA模式

Saga官网:https://seata.io/zh-cn/docs/user/saga.html

🦋4.1 原理

在 Seata 中,SAGA 是一种分布式事务协议,其原理是将一个分布式事务分解为多个子事务,并且通过补偿机制来保证事务的正确性和可靠性。

具体来说,SAGA 的实现过程如下:

  1. SAGA 中的一个大事务被分解为多个 Sub-transaction 以及一个 orkestrator 。

  2. orkestrator 发起第一个 Sub-transaction 并在事务表中记录执行状态。

  3. 当第一个 Sub-transaction 执行成功时,orkestrator 发起下一个 Sub-transaction ,并继续记录执行状态。

  4. 当某个 Sub-transaction 失败时,orkestrator 根据预定义的补偿函数进行补偿,并将补偿操作记录在事务表中。

  5. orkestrator 根据 Sub-transaction 的执行状态来判断整个事务的状态,并根据事务的状态来提交或回滚事务。

  6. 如果整个事务执行成功,则所有的 Sub-transaction 对应的操作全部执行成功。如果某个 Sub-transaction 失败,则执行该 Sub-transaction 的补偿函数,使得整个事务回到正常状态。

SAGA 的优点是可以通过将大的事务分解为多个子事务来提升分布式事务的可靠性和性能,并且可以通过补偿机制来保证事务的正确性。Seata 将 SAGA 作为一种事务模型,可以在不同的业务场景中,通过 SAGA 来解决复杂的分布式事务问题。

🦋4.2 Seata的XA模型


Saga也分为两个阶段:

  • 一阶段:直接提交本地事务
  • 二阶段:成功则什么都不做;失败则通过编写补偿业务来回滚

🦋4.3 优缺点

优点缺点
事务参与者可以基于事件驱动实现异步调用,吞吐高软状态持续时间不确定,时效性差
一阶段直接提交事务,无锁,性能好没有锁,没有事务隔离,会有脏写
不用编写TCC中的三个阶段,实现简单

🦋4.4 SAGA模型实战

demo源码下载地址:https://download.csdn.net/download/aa2528877987/88591955

🚀总结

Seata是一款开源的分布式事务解决方案,支持多种分布式事务模式。其中,Seata提供了四种事务模式,分别是XA、AT、TCC和SAGA。

  1. XA事务模式

XA是一种比较传统的分布式事务模式,通过两阶段提交(2PC)机制来实现分布式事务的一致性,可以保证各个资源的数据一致性。但是,XA模式对数据库的支持比较好,对其他资源(如消息队列)的支持则比较有限,性能较差。

  1. AT事务模式

AT事务模式是一种轻量级的分布式事务模式,不依赖于任何分布式事务管理器,而是通过各个资源的本地事务来实现事务的一致性。在AT模式下,分布式事务的提交是通过分阶段的“尝试-确认”机制来实现的,可以对各个资源的性能影响较小。

  1. TCC事务模式

TCC事务模式是一种基于补偿机制的分布式事务模式,通过事务参与者的“预留-确认-撤销”三个步骤来实现分布式事务的一致性。在TCC模式下,每个事务参与者需要实现相应的预留、确认和撤销操作,相对较为复杂。

  1. SAGA事务模式

SAGA事务模式是一种基于状态机的分布式事务模式,通过事务流程中的一系列Saga事件来实现分布式事务的一致性。在SAGA模式下,每个Saga事件需要实现相应的正向和反向补偿操作,能够对各个资源的性能影响较小。

🚀感谢:给读者的一封信

亲爱的读者,

我在这篇文章中投入了大量的心血和时间,希望为您提供有价值的内容。这篇文章包含了深入的研究和个人经验,我相信这些信息对您非常有帮助。

如果您觉得这篇文章对您有所帮助,我诚恳地请求您考虑赞赏1元钱的支持。这个金额不会对您的财务状况造成负担,但它会对我继续创作高质量的内容产生积极的影响。

我之所以写这篇文章,是因为我热爱分享有用的知识和见解。您的支持将帮助我继续这个使命,也鼓励我花更多的时间和精力创作更多有价值的内容。

如果您愿意支持我的创作,请扫描下面二维码,您的支持将不胜感激。同时,如果您有任何反馈或建议,也欢迎与我分享。

再次感谢您的阅读和支持!

最诚挚的问候, “愚公搬代码”

文章出处登录后可见!

已经登录?立即刷新

共计人评分,平均

到目前为止还没有投票!成为第一位评论此文章。

(0)
心中带点小风骚的头像心中带点小风骚普通用户
上一篇 2023年12月11日
下一篇 2023年12月11日

相关推荐