SpringAlibaba-Seata


(来源于尚硅谷)

一、什么是Seata?

Seata 是一款开源的分布式事务解决方案,致力于在微服务架构下提供高性能和简单易用的分布式事务服务。(简单可扩展的自治事务框架)。分布式就是,通过一个全局事务,把多个分支事务构成一个整体,来完成分布式事务的管理。

二、Seata对分布式事务的协调和控制就是1+3

1:代表XID,XID是全局事务的唯一标识,它可以在服务的调用链路中传递,绑定到服务的事务上下文中。
3:表示TC、TM、RM。
TC:就是Seata,负责维护全局事务和分支事务的状态,驱动全局事务提交或回滚。
TM:标注全局@GlobalTransactional启动入口动作的微服务模块(比如订单模块),它是事务的发起者,负责定义全局事务的范围,并根据
TC 维护的全局事务和分支事务状态,做出开始事务、提交事务、回滚事务的决议。
RM:就是mysql数据库本身,可以是多个RM,负责管理分支事务上的资源,向TC注册分支事务,汇报分支事务状态,驱动分支事务的提交或回滚。
在这里插入图片描述

三、下载、安装,配置

我们使用的是Seata AT,还有其他三个暂不学习。下载,安装均去官网。

1,建表

CREATE DATABASE seata;

USE seata;

2,建数据库(官网的)

-- -------------------------------- The script used when storeMode is 'db' --------------------------------

-- the table to store GlobalSession data

CREATE TABLE IF NOT EXISTS `global_table`

(

    `xid`                       VARCHAR(128) NOT NULL,

    `transaction_id`            BIGINT,

    `status`                    TINYINT      NOT NULL,

    `application_id`            VARCHAR(32),

    `transaction_service_group` VARCHAR(32),

    `transaction_name`          VARCHAR(128),

    `timeout`                   INT,

    `begin_time`                BIGINT,

    `application_data`          VARCHAR(2000),

    `gmt_create`                DATETIME,

    `gmt_modified`              DATETIME,

    PRIMARY KEY (`xid`),

    KEY `idx_status_gmt_modified` (`status` , `gmt_modified`),

    KEY `idx_transaction_id` (`transaction_id`)

) ENGINE = InnoDB

  DEFAULT CHARSET = utf8mb4;



-- the table to store BranchSession data

CREATE TABLE IF NOT EXISTS `branch_table`

(

    `branch_id`         BIGINT       NOT NULL,

    `xid`               VARCHAR(128) NOT NULL,

    `transaction_id`    BIGINT,

    `resource_group_id` VARCHAR(32),

    `resource_id`       VARCHAR(256),

    `branch_type`       VARCHAR(8),

    `status`            TINYINT,

    `client_id`         VARCHAR(64),

    `application_data`  VARCHAR(2000),

    `gmt_create`        DATETIME(6),

    `gmt_modified`      DATETIME(6),

    PRIMARY KEY (`branch_id`),

    KEY `idx_xid` (`xid`)

) ENGINE = InnoDB

  DEFAULT CHARSET = utf8mb4;



-- the table to store lock data

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` (`xid`)

) ENGINE = InnoDB

  DEFAULT CHARSET = utf8mb4;



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

  DEFAULT CHARSET = utf8mb4;



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);

3.配置Seata

F:\develop\seata-server-2.0.0\conf​ 在你的conf目录下,先备份application.yml,然后再进行配置。
在这里插入图片描述

4.运行

F:\develop\05_nacos-server2.2.3\nacos-server2.2.3\bin 首先启动我们的nacos 在bin 路径下运行cmd,输入startup.cmd -m standalone
F:\develop\06_seata-server-2.0.0\seata-server-2.0.0\bin 找到seata-server.bat 运行
访问:http://localhost:8848/nacos
在这里插入图片描述
再访问http://localhost:7091
在这里插入图片描述

四、实战demo

1.建数据库

扩展:因为当我们微服务哪一块出问题时,我们将会停止并且返回,所以我们需要定义一个回滚表undo_log用来返回上一次的数据,保证数据不会出错。
建seata order库+建t order表+undo log表

#order

CREATE DATABASE seata_order;

USE seata_order;

 

CREATE TABLE t_order(

`id` BIGINT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY,

`user_id` BIGINT(11) DEFAULT NULL COMMENT '用户id',

`product_id` BIGINT(11)DEFAULT NULL COMMENT '产品id',

`count` INT(11) DEFAULT NULL COMMENT '数量',

`money` DECIMAL(11,0) DEFAULT NULL COMMENT '金额',

`status` INT(1) DEFAULT NULL COMMENT '订单状态: 0:创建中; 1:已完结'

)ENGINE=INNODB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

 

SELECT * FROM t_order;



-- for AT mode you must to init this sql for you business database. the seata server not need it.

CREATE TABLE IF NOT EXISTS `undo_log`

(

    `branch_id`     BIGINT       NOT NULL COMMENT 'branch transaction id',

    `xid`           VARCHAR(128) NOT NULL COMMENT 'global transaction id',

    `context`       VARCHAR(128) 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 KEY `ux_undo_log` (`xid`, `branch_id`)

) ENGINE = InnoDB AUTO_INCREMENT = 1 DEFAULT CHARSET = utf8mb4 COMMENT ='AT transaction mode undo table';

ALTER TABLE `undo_log` ADD INDEX `ix_log_created` (`log_created`);

建seata storage库+建t storage表+undo log表

#storage

CREATE DATABASE seata_storage;

 

USE seata_storage;

 

CREATE TABLE t_storage(

`id` BIGINT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY,

`product_id` BIGINT(11) DEFAULT NULL COMMENT '产品id',

`total` INT(11) DEFAULT NULL COMMENT '总库存',

`used` INT(11) DEFAULT NULL COMMENT '已用库存',

`residue` INT(11) DEFAULT NULL COMMENT '剩余库存'

)ENGINE=INNODB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

 

INSERT INTO t_storage(`id`,`product_id`,`total`,`used`,`residue`)VALUES('1','1','100','0','100');

 

SELECT * FROM t_storage;



 -- for AT mode you must to init this sql for you business database. the seata server not need it.

CREATE TABLE IF NOT EXISTS `undo_log`

(

    `branch_id`     BIGINT       NOT NULL COMMENT 'branch transaction id',

    `xid`           VARCHAR(128) NOT NULL COMMENT 'global transaction id',

    `context`       VARCHAR(128) 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 KEY `ux_undo_log` (`xid`, `branch_id`)

) ENGINE = InnoDB AUTO_INCREMENT = 1 DEFAULT CHARSET = utf8mb4 COMMENT ='AT transaction mode undo table';

ALTER TABLE `undo_log` ADD INDEX `ix_log_created` (`log_created`);

建seata account库+建t account 表+undo log表

#account

create database seata_account;

 

use seata_account;

 

CREATE TABLE t_account(

`id` BIGINT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY COMMENT 'id',

`user_id` BIGINT(11) DEFAULT NULL COMMENT '用户id',

`total` DECIMAL(10,0) DEFAULT NULL COMMENT '总额度',

`used` DECIMAL(10,0) DEFAULT NULL COMMENT '已用余额',

`residue` DECIMAL(10,0) DEFAULT '0' COMMENT '剩余可用额度'

)ENGINE=INNODB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;

 

INSERT INTO t_account(`id`,`user_id`,`total`,`used`,`residue`)VALUES('1','1','1000','0','1000');

 

SELECT * FROM t_account;

 -- for AT mode you must to init this sql for you business database. the seata server not need it.

CREATE TABLE IF NOT EXISTS `undo_log`

(

    `branch_id`     BIGINT       NOT NULL COMMENT 'branch transaction id',

    `xid`           VARCHAR(128) NOT NULL COMMENT 'global transaction id',

    `context`       VARCHAR(128) 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 KEY `ux_undo_log` (`xid`, `branch_id`)

) ENGINE = InnoDB AUTO_INCREMENT = 1 DEFAULT CHARSET = utf8mb4 COMMENT ='AT transaction mode undo table';

ALTER TABLE `undo_log` ADD INDEX `ix_log_created` (`log_created`);
2.代码自动生成

使用mybatis一键生成(之后会专门有专门的介绍)
在cloud2024\mybatis_generator2024\src\main\resources\config.properties在这里插入图片描述
修cloud2024\mybatis_generator2024\src\main\resources\generatorConfig.xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE generatorConfiguration
        PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN"
        "http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd">

<generatorConfiguration>
    <properties resource="config.properties"/>

    <context id="Mysql" targetRuntime="MyBatis3Simple" defaultModelType="flat">
        <property name="beginningDelimiter" value="`"/>
        <property name="endingDelimiter" value="`"/>

        <plugin type="tk.mybatis.mapper.generator.MapperPlugin">
            <property name="mappers" value="tk.mybatis.mapper.common.Mapper"/>
            <property name="caseSensitive" value="true"/>
        </plugin>

        <jdbcConnection driverClass="${jdbc.driverClass}"
                        connectionURL="${jdbc.url}"
                        userId="${jdbc.user}"
                        password="${jdbc.password}">
        </jdbcConnection>
<!--    这就是我们在config.properties中定义的账号密码等-->

        <javaModelGenerator targetPackage="${package.name}.entities" targetProject="src/main/java"/>
<!--这个是生成一个实体类-->
        <sqlMapGenerator targetPackage="${package.name}.mapper" targetProject="src/main/java"/>
<!--这是mapper-->
        <javaClientGenerator targetPackage="${package.name}.mapper" targetProject="src/main/java" type="XMLMAPPER"/>
<!--这是mapper接口-->
       <!-- <table tableName="t_pay" domainObjectName="Pay">
            <generatedKey column="id" sqlStatement="JDBC"/>
        </table>-->
        <!--  seata_order -->
        <table tableName="t_order" domainObjectName="Order">
            <generatedKey column="id" sqlStatement="JDBC"/>
        </table>
    </context>
</generatorConfiguration>

点击插件,自动生成mapper和entities Order类
在这里插入图片描述

3.然后开始创建自己的业务模块和业务代码,
1.application.yml

application.yml中

server:
  port: 2001

spring:
  application:
    name: seata-order-service
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848         #Nacos服务注册中心地址
  # ==========applicationName + druid-mysql8 driver===================
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/seata_order?characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B8&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true
    username: root
    password: 123456
# ========================mybatis===================
mybatis:
  mapper-locations: classpath:mapper/*.xml
  type-aliases-package: com.atguigu.cloud.entities
  configuration:
    map-underscore-to-camel-case: true

# ========================seata===================
seata:
  registry:
    type: nacos
    nacos:
      server-addr: 127.0.0.1:8848
      namespace: ""
      group: SEATA_GROUP
      application: seata-server
  tx-service-group: default_tx_group # 事务组,由它获得TC服务的集群名称
  service:
    vgroup-mapping: # 点击源码分析
      default_tx_group: default # 事务组与TC服务集群的映射关系
  data-source-proxy-mode: AT
#seata的日志级别
logging:
  level:
    io:
      seata: info

对应说明:
在这里插入图片描述

2.主要代码和注解分析

使用:订单➡库存➡钱 这样的小型微服务来实现这个seata的功能实现。

3.openfeignAPI接口定义方法

首先我们要在我们对外暴露的openfeignAPI接口中创建需要的方法,让其他服务通过我们定义好的接口方法进行调用:

@FeignClient(value = "seata-account-service")
public interface AccountFeignApi
{
    //扣减账户余额
    @PostMapping("/account/decrease")
    ResultData decrease(@RequestParam("userId") Long userId, @RequestParam("money") Long money);
}
4.订单类

然后开始写订单类:
(Order,mapper,Orderservice省略,方法不限,可以用mybatis plus等方法优化,但这样更直观)

package com.atguigu.cloud.service.impl;

import com.atguigu.cloud.apis.AccountFeignApi;
import com.atguigu.cloud.apis.StorageFeignApi;
import com.atguigu.cloud.entities.Order;
import com.atguigu.cloud.mapper.OrderMapper;
import com.atguigu.cloud.service.OrderService;
import io.seata.core.context.RootContext;
import io.seata.spring.annotation.GlobalTransactional;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import tk.mybatis.mapper.entity.Example;

@Service
@Slf4j
public class OrderServiceImpl implements OrderService {
    @Resource
    private OrderMapper orderMapper;
    @Resource//订单微服务通过OpenFeign去调用库存微服务
    private StorageFeignApi  storageFeignApi;
    @Resource//订单微服务通过OpenFeign去调用账户微服务
    private AccountFeignApi accountFeignApi;

    @Override
    @GlobalTransactional(name = "zzyy-create-order",rollbackFor = Exception.class)//AT模式 全局事务注解,name是全局事务的名称,rollbackFor是回滚的异常类
    public void create(Order order) {
        //xid全局事务id的检查,重要
        String xid = RootContext.getXID();
        //1.新建订单
        log.info("==================>开始新建订单"+"\t"+"xid_order:" +xid);
        //订单新建时默认初始的订单状态为0
        order.setStatus(0);//因为当状态时0的时候,证明当前订单状态为未支付,在这设置初始
        int result = orderMapper.insertSelective(order);//insertSelective比insert更精细的控制,仅插入对象中非null的字段值,是一种更倾向于按需插入数据的策略。当插入成功时,会返回1,不成功则返回0。可以用来决定是否抛出异常、是否进行事务回滚等。
        //插入订单成功后获得插入mysql的实体对象
        Order orderFromDB=null;
        if(result > 0) {
            orderFromDB = orderMapper.selectOne(order);//根据提供的参数从数据库中查询并返回一条记录。
            //orderFromDB = orderMapper.selectByPrimaryKey(order.getId());//等价
            log.info("-------> 新建订单成功,orderFromDB info: " + orderFromDB);
            System.out.println();
            //2.扣减库存
            log.info("-------> 订单微服务开始调用Storage库存,做扣减count");
            storageFeignApi.decrease(orderFromDB.getProductId(),orderFromDB.getCount());//开始调用openfeign中的方法
            log.info("-------> 订单微服务结束调用Storage库存,做扣减完成");
            System.out.println();
            //3. 扣减账号余额
            log.info("-------> 订单微服务开始调用Account账号,做扣减money");
            accountFeignApi.decrease(orderFromDB.getUserId(), orderFromDB.getMoney());
            log.info("-------> 订单微服务结束调用Account账号,做扣减完成");
            System.out.println();
            //4. 修改订单状态
            //订单状态status:0:创建中;1:已完结
            log.info("-------> 修改订单状态");
            orderFromDB.setStatus(1);//如果所有的都正确,我们修改状态为1,表示已经完结

            Example whereCondition=new Example(Order.class);
            //创建了一个Example实例,指明我们即将进行的查询或更新操作是针对Order表的。Example类是MyBatis提供的一个工具,用于构建基于各种条件的动态查询。
            Example.Criteria criteria=whereCondition.createCriteria();
            //Criteria用于定义查询或更新的具体条件
            criteria.andEqualTo("userId",orderFromDB.getUserId());
            //只有当userId字段等于orderFromDB对象中的userId值时,这条记录才会被更新。这里使用了andEqualTo方法,意味着添加的是一个等于条件。
            criteria.andEqualTo("status",0);
            //只更新那些status字段值为0的记录。这意味着这次更新操作旨在修改那些属于特定用户(由userId确定)且状态为0的订单记录。
            int updateResult = orderMapper.updateByExampleSelective(orderFromDB, whereCondition);
            //通过orderMapper调用updateByExampleSelective方法执行更新操作。
            log.info("-------> 修改订单状态完成"+"\t"+updateResult);
            log.info("-------> orderFromDB info: "+orderFromDB);
        }
        System.out.println();
        log.info("==================>结束新建订单"+"\t"+"xid_order:" +xid);

    }
}

4.storage库存类

storage库存类:
(controller简单的调用不写,主要是mapper.xml中需要注意)StorageServiceImpl:

package com.atguigu.cloud.service.impl;

import com.atguigu.cloud.mapper.StorageMapper;
import com.atguigu.cloud.service.StorageService;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

@Service
@Slf4j
public class StorageServiceImpl implements StorageService {
    @Resource
    private StorageMapper storageMapper;

    /**
     * 扣减库存
     */
    @Override
    public void decrease(Long productId, Integer count) {
        log.info("------->storage-service中扣减库存开始");
        storageMapper.decrease(productId,count);
        log.info("------->storage-service中扣减库存结束");
    }
}

mapper:

package com.atguigu.cloud.mapper;
import com.atguigu.cloud.entities.Storage;
import org.apache.ibatis.annotations.Param;
import tk.mybatis.mapper.common.Mapper;
public interface StorageMapper extends Mapper<Storage>
{
    /**
     * 扣减库存
     */
    void decrease(@Param("productId") Long productId, @Param("count") Integer count);

}

mapper.xml:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.atguigu.cloud.mapper.StorageMapper">
  <resultMap id="BaseResultMap" type="com.atguigu.cloud.entities.Storage">
    <!--
      WARNING - @mbg.generated
    -->
    <id column="id" jdbcType="BIGINT" property="id" />
    <result column="product_id" jdbcType="BIGINT" property="productId" />
    <result column="total" jdbcType="INTEGER" property="total" />
    <result column="used" jdbcType="INTEGER" property="used" />
    <result column="residue" jdbcType="INTEGER" property="residue" />
  </resultMap>
  <update id="decrease">
    UPDATE
      t_storage
    SET
      used = used + #{count},//使用了多少
      residue = residue - #{count}//库存还剩多少
    WHERE product_id = #{productId}
  </update>
</mapper>
5.account余额表

account余额表:
和库存表逻辑是一样的
在service中加入测试,测试事件回滚:

package com.atguigu.cloud.service.impl;

import com.atguigu.cloud.mapper.AccountMapper;
import com.atguigu.cloud.service.AccountService;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

import java.util.concurrent.TimeUnit;

@Service
@Slf4j
public class AccountServiceImpl implements AccountService
{
    @Resource
    AccountMapper accountMapper;
    /**
     * 扣减账户余额
     */
    @Override
    public void decrease(Long userId, Long money) {
        log.info("------->account-service中扣减账户余额开始");
        accountMapper.decrease(userId,money);
         // myTimeOut();//当我们开启这两个错误时,首先,他们会进行数据库的操作,包括库存的减少,和余额的减少,但不过,当出错后,事件会发生回滚,他们都用Seate绑定的事务,所以一起回滚回操作开始的数据。
        //int age = 10/0;
        log.info("------->account-service中扣减账户余额结束");
    }
    /**
     * 模拟超时异常,全局事务回滚
     */
    private static void myTimeOut()
    {
        try { TimeUnit.SECONDS.sleep(65); } catch (InterruptedException e) { e.printStackTrace(); }
    }
}

mapper.xml:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.atguigu.cloud.mapper.AccountMapper">
  <resultMap id="BaseResultMap" type="com.atguigu.cloud.entities.Account">
    <!--
      WARNING - @mbg.generated
    -->
    <id column="id" jdbcType="BIGINT" property="id" />
    <result column="user_id" jdbcType="BIGINT" property="userId" />
    <result column="total" jdbcType="DECIMAL" property="total" />
    <result column="used" jdbcType="DECIMAL" property="used" />
    <result column="residue" jdbcType="DECIMAL" property="residue" />
  </resultMap>
  <update id="decrease">
    UPDATE
      t_account
    SET
      residue = residue - #{money},used = used + #{money}
    WHERE user_id = #{userId};
  </update>
</mapper>
6.在官网查看服务状态

当我们设置延时后,可以在http://localhost:7091/中查看状态
在这里插入图片描述
在这里插入图片描述

五、总结和错误

总结:我们需要先引入Seate的依赖,然后在我们需要绑定的业务上加入我们的@GlobalTransactional,并且定义我们此次操作的名字,方便查看。(如)

@GlobalTransactional(name = "zzyy-create-order",rollbackFor = Exception.class)

尽量不要特别多的功能写在一块,解耦。
pom中spring boot、spring cloud、spring Alibaba、Seata的版本一定要注意,不然就会导致版本冲突
暂定:

  <spring.boot.version>3.1.7</spring.boot.version>
 <spring.cloud.version>2022.0.4</spring.cloud.version>
  <spring.cloud.alibaba.version>2022.0.0.0-RC2</spring.cloud.alibaba.version>
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值