分布式事务解决方案 Seata - AT mode springboot

在这里插入图片描述

Seata 是一款开源的分布式事务解决方案,致力于在微服务架构下提供高性能和简单易用的分布式事务服务。在 Seata 开源之前,Seata
对应的内部版本在阿里经济体内部一直扮演着分布式一致性中间件的角色,帮助经济体平稳的度过历年的双11,对各BU业务进行了有力的支撑。经过多年沉淀与积累,商业化产品先后在阿里云、金融云进行售卖。2019.1
为了打造更加完善的技术生态和普惠技术成果,Seata 正式宣布对外开源,未来 Seata 将以社区共建的形式帮助其技术更加可靠与完备。

此前我们自己从头实现的分布式事务解决方案总是要耗费很多时间,seata的出现无疑给我们节省下很多时间,它的非侵入和侵入式的分布式事务解决方案很全面,但是在使用它的时候不要忽略seata的种种方案的理念要尽量做到知其所以然,这个对于搞技术的才是最重要的,毕竟没有什么工具是不可替代的。

seata 官网

seata 官方文档

下载安装Seata Server

Seata Server 下载中心

Seata-Server-1.2.0.zip 下载(推荐 速度快)

AT模式下的 undo_log 建表语句
undo_log是AT mode 下的回滚事务表,其作用是在一阶段每个子事务提交时记录事务的的信息和回滚sql,如果事务回滚就会使用这条记录,这个事务回滚记录也会在事务完毕时被删除。

Demo

这个Demo项目使用seata的AT做一个分布式事务的实例,实例不使用任何服务注册中心如(nacos、zookppers、erurka…)
,只要有一个支持ACID的数据库就行,这里我们使用mysql数据库。

我们会创建两个项目:AccountService、BillService,分别连着不同的数据库(AT模式下每个数据库都要有undo_log表哦),两个项目没有什么实际意义就是做个Example。

AT 模式

前提

基于支持本地 ACID 事务的关系型数据库。
Java 应用,通过 JDBC 访问数据库。

整体机制

两阶段提交协议的演变:

一阶段:业务数据和回滚日志记录在同一个本地事务中提交,释放本地锁和连接资源。

二阶段:

提交异步化,非常快速地完成。
回滚通过一阶段的回滚日志进行反向补偿。

写隔离

  • 一阶段本地事务提交前,需要确保先拿到 全局锁 。
  • 拿不到 全局锁 ,不能提交本地事务。
  • 拿 全局锁 的尝试被限制在一定范围内,超出范围将放弃,并回滚本地事务,释放本地锁。

读隔离

在数据库本地事务隔离级别 读已提交(Read Committed) 或以上的基础上,Seata(AT 模式)的默认全局隔离级别是 读未提交(Read Uncommitted) 。


Demo环境

工具版本
java1.8
spring boot2.1.2
gradle4.8
seata1.2.0
windows10
CentOS7

Demo项目仓库
在这里插入图片描述


Account服务

引入依赖

build.gradle

plugins {
    id 'java'
}

apply plugin: 'idea'

group 'cn.springboot'
version '1.0-SNAPSHOT'

sourceCompatibility = 1.8
targetCompatibility = 1.8

repositories {
    maven { url 'https://maven.aliyun.com/repository/public/' }
    mavenCentral()
}

dependencies {
    testCompile group: 'junit', name: 'junit', version: '4.12'

    // https://mvnrepository.com/artifact/org.springframework.cloud/spring-cloud-starter-oauth2
    compile group: 'org.springframework.cloud', name: 'spring-cloud-starter-oauth2', version: '2.1.2.RELEASE'
    // https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-web
    compile group: 'org.springframework.boot', name: 'spring-boot-starter-web', version: '2.1.2.RELEASE'

    // https://mvnrepository.com/artifact/org.projectlombok/lombok
    compile group: 'org.projectlombok', name: 'lombok', version: '1.18.12'

    compileOnly 'org.projectlombok:lombok'
//    developmentOnly 'org.springframework.boot:spring-boot-devtools'
    annotationProcessor 'org.projectlombok:lombok'
    // https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-thymeleaf
    compile group: 'org.springframework.boot', name: 'spring-boot-starter-thymeleaf', version: '2.1.2.RELEASE'
    // https://mvnrepository.com/artifact/com.alibaba/fastjson
    compile group: 'com.alibaba', name: 'fastjson', version: '1.2.56'
    // https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-actuator
    compile group: 'org.springframework.boot', name: 'spring-boot-starter-actuator', version: '2.1.2.RELEASE'
    // https://mvnrepository.com/artifact/mysql/mysql-connector-java
    compile group: 'mysql', name: 'mysql-connector-java', version: '5.1.47'
    // https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-data-jpa
    compile group: 'org.springframework.boot', name: 'spring-boot-starter-data-jpa', version: '2.1.2.RELEASE'
    
//    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    testImplementation 'org.springframework.security:spring-security-test'
    // https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-test
    testCompile group: 'org.springframework.boot', name: 'spring-boot-starter-test', version: '2.1.2.RELEASE'
// --------------------   seata 相关 ----------------------------------
    // https://mvnrepository.com/artifact/io.seata/seata-spring-boot-starter
    compile group: 'io.seata', name: 'seata-spring-boot-starter', version: '1.2.0'

}

项目配置

特别提一句File注册配置下也是可以连公网的。
之前好像在哪看到人说File注册配置只可以本地这是不对的。

grouplist:
  default: 117.23.23.1:8091

application.yml


spring:
  jpa:
    database: mysql
    show-sql: true
    hibernate:
      ddl-auto: update
      naming:
        physical-strategy: org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl
  datasource:
    url: jdbc:mysql://127.0.0.1:3306/exampledb
#    jdbcUrl: jdbc:mysql://192.168.84.135:3306/demodb
    password: 123456
    username: root
    driver-class-name: com.mysql.jdbc.Driver

server:
  port: 8081
  servlet:
    context-path: /ResourceServer


seata:
  enabled: true
  application-id: applicationName
  tx-service-group: my_test_tx_group
  enable-auto-data-source-proxy: true
  use-jdk-proxy: false
  excludes-for-auto-proxying: firstClassNameForExclude,secondClassNameForExclude
  client:
    rm:
      async-commit-buffer-limit: 1000
      report-retry-count: 5
      table-meta-check-enable: false
      report-success-enable: false
      saga-branch-register-enable: false
      lock:
        retry-interval: 10
        retry-times: 30
        retry-policy-branch-rollback-on-conflict: true
    tm:
      degrade-check: false
      degrade-check-period: 2000
      degrade-check-allow-times: 10
      commit-retry-count: 5
      rollback-retry-count: 5
    undo:
      data-validation: true
      log-serialization: jackson
      log-table: undo_log
      only-care-update-columns: true
    log:
      exceptionRate: 100
  service:
    vgroup-mapping:
      my_test_tx_group: default
    grouplist:
      default: 127.0.0.1:8091
    enable-degrade: false
    disable-global-transaction: false
  transport:
    shutdown:
      wait: 3
    thread-factory:
      boss-thread-prefix: NettyBoss
      worker-thread-prefix: NettyServerNIOWorker
      server-executor-thread-prefix: NettyServerBizHandler
      share-boss-worker: false
      client-selector-thread-prefix: NettyClientSelector
      client-selector-thread-size: 1
      client-worker-thread-prefix: NettyClientWorkerThread
      worker-thread-size: default
      boss-thread-size: 1
    type: TCP
    server: NIO
    heartbeat: true
    serialization: seata
    compressor: none
    enable-client-batch-send-request: true
  config:
    type: file
    consul:
      server-addr: 127.0.0.1:8500
    apollo:
      apollo-meta: http://192.168.1.204:8801
      app-id: seata-server
      namespace: application
    etcd3:
      server-addr: http://localhost:2379
    nacos:
      namespace:
      serverAddr: 127.0.0.1:8848
      group: SEATA_GROUP
      userName: ""
      password: ""
    zk:
      server-addr: 127.0.0.1:2181
      session-timeout: 6000
      connect-timeout: 2000
      username: ""
      password: ""
  registry:
    type: file
    consul:
      server-addr: 127.0.0.1:8500
    etcd3:
      serverAddr: http://localhost:2379
    eureka:
      weight: 1
      service-url: http://localhost:8761/eureka
    nacos:
      application: seata-server
      server-addr: 127.0.0.1:8848
      namespace:
      userName: ""
      password: ""
    redis:
      server-addr: localhost:6379
      db: 0
      password:
      timeout: 0
    sofa:
      server-addr: 127.0.0.1:9603
      region: DEFAULT_ZONE
      datacenter: DefaultDataCenter
      group: SEATA_GROUP
      addressWaitTime: 3000
      application: default
    zk:
      server-addr: 127.0.0.1:2181
      session-timeout: 6000
      connect-timeout: 2000
      username: ""
      password: ""

码代码

Controller

/**
 * 测试用controller
 */
@RestController
public class HomeController {
    @Autowired
    private GlobalService globalService;

    /**
     * 分布式事务开始
     * @param request
     * @param a
     * @param s
     * @return
     */
    @GetMapping("atstart")
    public JSONObject ATStart(HttpServletRequest request ,Integer a ,String s) {
        JSONObject result = new JSONObject();
        globalService.procFirst(a,s);
        result.put("status", "success");
        return result;
    }

}

Service

public interface GlobalService {
    void procFirst(Integer a, String s);

    void procSecond(Integer a, String s);
}

ServiceImpl

@Slf4j
@Service
public class GlobalServiceImpl implements GlobalService {
    @Autowired
    private AccountRepository accountRepository;

    /**
     * 应用seata分布式注解
     * 事务的第一个动作
     * @param a
     * @param s
     */
    @GlobalTransactional(name = "AT-First")
    @Override
    public void procFirst(Integer a, String s) {
        RestTemplate template = new RestTemplate();
        Account newAccount = new Account();
        newAccount.setAname(""+a);
        newAccount.setPassword(s);
        accountRepository.save(newAccount);

        try {
//            延迟5秒再去执行事务的下个工作,方便看到数据的变化
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        JSONObject jsonObject = template.getForObject("http://localhost:8083/ResourceServer/processContinue?a=" + a + "&s=" + s , JSONObject.class);

        if ( jsonObject.size()>0 && jsonObject.getBoolean("status")) {
            log.info("success");
            log.info(jsonObject.toJSONString());
        }else if(jsonObject != null){
            log.info(jsonObject.toJSONString());
        }

    }

    /**
     * 事务的第二个动作
     * 在第二个服务里实现
     * @param a
     * @param s
     */
    @Override
    public void procSecond(Integer a, String s) {

    }
}

Bill服务

gradle依赖项与第一个项目一致

配置方面与第一个服务不同的地方有数据库连接和web服务端口号。

spring:
  jpa:
    database: mysql
    show-sql: true
    hibernate:
      ddl-auto: update
      naming:
        physical-strategy: org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl
  datasource:
    url: jdbc:mysql://127.0.0.1:3306/demodb
#    jdbcUrl: jdbc:mysql://192.168.84.135:3306/demodb
    password: 123456
    username: root
    driver-class-name: com.mysql.jdbc.Driver

server:
  port: 8083
  servlet:
    context-path: /ResourceServer
码代码

Controller

/**
 * 测试用controller
 */
@RestController
public class HomeController {
    @Autowired
    private GlobalService globalService;

    /**
     * 分布式事务第二个动作的入口
     * @param request
     * @param a
     * @param s
     * @return
     */
    @GetMapping("processContinue")
    public JSONObject processContinue(HttpServletRequest request,Integer a ,String s) {
        JSONObject result = new JSONObject();

        globalService.procSecond(a,s);
        result.put("status", true);
        return result;
    }
}

Service

第二个服务的Service类与第一个项目一样

ServiceImpl

@Service
public class GlobalServiceImpl implements GlobalService {
    @Autowired
    private BillRepository billRepository;

    /**
     * 实现代码省略
     * @param a
     * @param s
     */
    @GlobalTransactional(name = "AT-First")
    @Override
    public void procFirst(Integer a, String s) {

    }

    /**
     * 分布式事务的第二个动作
     * @param a
     * @param s
     */
    @GlobalTransactional(name = "AT-First")
    @Override
    public void procSecond(Integer a, String s) {
        if (s.equals("break")) {
            throw new RuntimeException("ready to rollback");
        }
        Bill newBill = new Bill();
        newBill.setAmount(a);
        newBill.setBillNo(s);
        
        newBill.setNote(new Date().toLocaleString());
        this.billRepository.save(newBill);
    }
}

启动

在每个Service的数据库里建undo_log表(undo_log表是AT模式必需的)。
启动seata-server
Usage: sh seata-server.sh(for linux and mac) or cmd seata-server.bat(for windows) [options]
  Options:
    --host, -h
      The host to bind.
      Default: 0.0.0.0
    --port, -p
      The port to listen.
      Default: 8091
    --storeMode, -m
      log store mode : file、db
      Default: file
    --help

e.g.

sh seata-server.sh -p 8091 -h 127.0.0.1 -m file
启动Account和Bill项目

查看项目和seata-server日志

Account服务-启动日志

在这里插入代码片
Global事务客户端项目启动简述
  • Seata spring-boot自动配置
    在这里插入图片描述
  • 在这里插入图片描述
    初始化Global事务客户端
    在这里插入图片描述
    这里可以看到我们设置的异步事务提交的上限配置为1000
    在这里插入图片描述
    RM初始化完成、Global事务客户端初始化完成。
    在这里插入图片描述
    AT代理目标为数据源
    在这里插入图片描述

我们应用了@GlobalTransactional注解的Service方法已经被seata拦截到了
在这里插入图片描述

TM也注册成功了
在这里插入图片描述
一般看到以上这些日志说明Global事务客户端启动正常

seata-server日志
2020-05-30 22:45:42.941 INFO [ServerHandlerThread_1_500]io.seata.core.rpc.DefaultServerMessageListenerImpl.onRegRmMessage:127 -RM register success,message:RegisterRMRequest{resourceIds='jdbc:mysql://127.0.0.1:3306/demodb', applicationId='applicationName', transactionServiceGroup='my_test_tx_group'},channel:[id: 0x47e5634f, L:/127.0.0.1:8091 - R:/127.0.0.1:56683]
2020-05-30 22:46:41.449 INFO [NettyServerNIOWorker_1_16]io.seata.core.rpc.DefaultServerMessageListenerImpl.onRegTmMessage:153 -TM register success,message:RegisterTMRequest{applicationId='applicationName', transactionServiceGroup='my_test_tx_group'},channel:[id: 0x719e69f9, L:/127.0.0.1:8091 - R:/127.0.0.1:56700]
2020-05-30 22:48:14.114 INFO [ServerHandlerThread_1_500]io.seata.core.rpc.DefaultServerMessageListenerImpl.onRegRmMessage:127 -RM register success,message:RegisterRMRequest{resourceIds='jdbc:mysql://127.0.0.1:3306/exampledb', applicationId='applicationName', transactionServiceGroup='my_test_tx_group'},channel:[id: 0xaaadd3ae, L:/127.0.0.1:8091 - R:/127.0.0.1:56732]
2020-05-30 22:49:12.612 INFO [NettyServerNIOWorker_1_16]io.seata.core.rpc.DefaultServerMessageListenerImpl.onRegTmMessage:153 -TM register success,message:RegisterTMRequest{applicationId='applicationName', transactionServiceGroup='my_test_tx_group'},channel:[id: 0x32c93b17, L:/127.0.0.1:8091 - R:/127.0.0.1:56746]

执行事务测试

我们用3次http请求执行3个测试,其中两次为成功的事务,一个为失败的事务。

  • success http://localhost:8081/ResourceServer/atstart?a=1&s=hello
  • faill and rollback http://localhost:8081/ResourceServer/atstart?a=2&s=break
  • success http://localhost:8081/ResourceServer/atstart?a=3&s=world

第一次请求完成,这里没啥好看的。
第二次请求,account写入一条记录(break)

在这里插入图片描述
bill遇到break,抛异常,事务回滚。
在这里插入图片描述
account还剩一条记录。
在这里插入图片描述
执行第三次请求,成功。
最后数据都是完整的,是不是很方便呢。
在这里插入图片描述

在这里插入图片描述

日志输出

一个有两个动作的分布式事务成功执行的seata日志

2020-05-30 22:11:28.107 INFO [batchLoggerPrint_1]io.seata.core.rpc.DefaultServerMessageListenerImpl.run:214 -SeataMergeMessage timeout=60000,transactionName=AT-First
,clientIp:127.0.0.1,vgroup:my_test_tx_group
2020-05-30 22:11:28.107 INFO [ServerHandlerThread_1_500]io.seata.server.coordinator.DefaultCoordinator.doGlobalBegin:159 -Begin new global transaction applicationId: applicationName,transactionServiceGroup: my_test_tx_group, transactionName: AT-First,timeout:60000,xid:192.168.137.1:8091:2013030423
2020-05-30 22:11:28.117 INFO [batchLoggerPrint_1]io.seata.core.rpc.DefaultServerMessageListenerImpl.run:214 -SeataMergeMessage xid=192.168.137.1:8091:2013030423,branchType=AT,resourceId=jdbc:mysql://127.0.0.1:3306/exampledb,lockKey=account:402809817265e788017265ec5af10004
,clientIp:127.0.0.1,vgroup:my_test_tx_group
2020-05-30 22:11:28.117 INFO [ServerHandlerThread_1_500]io.seata.server.coordinator.AbstractCore.lambda$branchRegister$0:87 -Register branch successfully, xid = 192.168.137.1:8091:2013030423, branchId = 2013030424, resourceId = jdbc:mysql://127.0.0.1:3306/exampledb ,lockKeys = account:402809817265e788017265ec5af10004
2020-05-30 22:11:33.132 INFO [batchLoggerPrint_1]io.seata.core.rpc.DefaultServerMessageListenerImpl.run:214 -SeataMergeMessage timeout=60000,transactionName=AT-First
,clientIp:127.0.0.1,vgroup:my_test_tx_group
2020-05-30 22:11:33.132 INFO [ServerHandlerThread_1_500]io.seata.server.coordinator.DefaultCoordinator.doGlobalBegin:159 -Begin new global transaction applicationId: applicationName,transactionServiceGroup: my_test_tx_group, transactionName: AT-First,timeout:60000,xid:192.168.137.1:8091:2013030425
2020-05-30 22:11:33.140 INFO [batchLoggerPrint_1]io.seata.core.rpc.DefaultServerMessageListenerImpl.run:214 -SeataMergeMessage xid=192.168.137.1:8091:2013030425,branchType=AT,resourceId=jdbc:mysql://127.0.0.1:3306/demodb,lockKey=bill:break3
,clientIp:127.0.0.1,vgroup:my_test_tx_group
2020-05-30 22:11:33.141 INFO [ServerHandlerThread_1_500]io.seata.server.coordinator.AbstractCore.lambda$branchRegister$0:87 -Register branch successfully, xid = 192.168.137.1:8091:2013030425, branchId = 2013030426, resourceId = jdbc:mysql://127.0.0.1:3306/demodb ,lockKeys = bill:break3
2020-05-30 22:11:33.147 INFO [batchLoggerPrint_1]io.seata.core.rpc.DefaultServerMessageListenerImpl.run:214 -SeataMergeMessage xid=192.168.137.1:8091:2013030425,extraData=null
,clientIp:127.0.0.1,vgroup:my_test_tx_group
2020-05-30 22:11:33.154 INFO [batchLoggerPrint_1]io.seata.core.rpc.DefaultServerMessageListenerImpl.run:214 -SeataMergeMessage xid=192.168.137.1:8091:2013030423,extraData=null
,clientIp:127.0.0.1,vgroup:my_test_tx_group
2020-05-30 22:11:33.852 INFO [AsyncCommitting_1]io.seata.server.coordinator.DefaultCore.doGlobalCommit:240 -Committing global transaction is successfully done, xid = 192.168.137.1:8091:2013030425.
2020-05-30 22:11:33.861 INFO [AsyncCommitting_1]io.seata.server.coordinator.DefaultCore.doGlobalCommit:240 -Committing global transaction is successfully done, xid = 192.168.137.1:8091:2013030423.

Committing global transaction is successfully done
是在整个事务完成之后输出的,有几个事务动作就是几条输出。

下面是事务中第2个动作失败然后整个事务回滚的输出日志

2020-05-30 22:17:12.913 INFO [batchLoggerPrint_1]io.seata.core.rpc.DefaultServerMessageListenerImpl.run:214 -SeataMergeMessage timeout=60000,transactionName=AT-First
,clientIp:127.0.0.1,vgroup:my_test_tx_group
2020-05-30 22:17:12.913 INFO [ServerHandlerThread_1_500]io.seata.server.coordinator.DefaultCoordinator.doGlobalBegin:159 -Begin new global transaction applicationId: applicationName,transactionServiceGroup: my_test_tx_group, transactionName: AT-First,timeout:60000,xid:192.168.137.1:8091:2013030430
2020-05-30 22:17:12.921 INFO [batchLoggerPrint_1]io.seata.core.rpc.DefaultServerMessageListenerImpl.run:214 -SeataMergeMessage xid=192.168.137.1:8091:2013030430,branchType=AT,resourceId=jdbc:mysql://127.0.0.1:3306/exampledb,lockKey=account:402809817265e788017265f19dd60006
,clientIp:127.0.0.1,vgroup:my_test_tx_group
2020-05-30 22:17:12.921 INFO [ServerHandlerThread_1_500]io.seata.server.coordinator.AbstractCore.lambda$branchRegister$0:87 -Register branch successfully, xid = 192.168.137.1:8091:2013030430, branchId = 2013030431, resourceId = jdbc:mysql://127.0.0.1:3306/exampledb ,lockKeys = account:402809817265e788017265f19dd60006
2020-05-30 22:17:17.932 INFO [batchLoggerPrint_1]io.seata.core.rpc.DefaultServerMessageListenerImpl.run:214 -SeataMergeMessage timeout=60000,transactionName=AT-First
,clientIp:127.0.0.1,vgroup:my_test_tx_group
2020-05-30 22:17:17.932 INFO [ServerHandlerThread_1_500]io.seata.server.coordinator.DefaultCoordinator.doGlobalBegin:159 -Begin new global transaction applicationId: applicationName,transactionServiceGroup: my_test_tx_group, transactionName: AT-First,timeout:60000,xid:192.168.137.1:8091:2013030432
2020-05-30 22:17:17.937 INFO [ServerHandlerThread_1_500]io.seata.server.coordinator.DefaultCore.doGlobalRollback:334 -Rollback global transaction successfully, xid = 192.168.137.1:8091:2013030432.
2020-05-30 22:17:17.938 INFO [batchLoggerPrint_1]io.seata.core.rpc.DefaultServerMessageListenerImpl.run:214 -SeataMergeMessage xid=192.168.137.1:8091:2013030432,extraData=null
,clientIp:127.0.0.1,vgroup:my_test_tx_group
2020-05-30 22:17:17.945 INFO [batchLoggerPrint_1]io.seata.core.rpc.DefaultServerMessageListenerImpl.run:214 -SeataMergeMessage xid=192.168.137.1:8091:2013030430,extraData=null
,clientIp:127.0.0.1,vgroup:my_test_tx_group
2020-05-30 22:17:17.955 INFO [ServerHandlerThread_1_500]io.seata.server.coordinator.DefaultCore.doGlobalRollback:290 -Rollback branch transaction  successfully, xid = 192.168.137.1:8091:2013030430 branchId = 2013030431
2020-05-30 22:17:17.956 INFO [ServerHandlerThread_1_500]io.seata.server.coordinator.DefaultCore.doGlobalRollback:334 -Rollback global transaction successfully, xid = 192.168.137.1:8091:2013030430.

-Rollback global transaction successfully
相对于事务成功的输出日志,某个动作异常导致事务回滚的日志也是两个(跟事务的动作数量一致)。

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值