Seata简介;安装seata;应用seata

一,Seata简介;

用来管理分布式事务,由阿里巴巴出品。

【1、TC (Transaction Coordinator) - 事务协调者】

用来维护事务的,包括主事务和分支事务。

【2、TM (Transaction Manager) - 事务管理器】

管理事务的,决定了事务什么时候开启,什么时候结束,提交还是回滚。

TM用来做提交和回滚。

【3、RM (Resource Manager) - 资源管理器】

监视分支事务的状态,和TC做数据交互,把分支事务状态告诉TC。

应用场景:

不适合高并发项目,中间操作时有锁,会导致效率低下

“保证事务同时成功或同时失败” RM - TC - TM TC起到中间桥梁的作用

二,安装seata

1、基于操作系统环境安装

官网网址:Releases · apache/incubator-seata · GitHub

2、拉取docker镜像

docker pull seataio/seata-server:1.3.0

3、拷贝配置文件

先启动seata,将seata-server目录下的文件拷贝到虚拟机中

#执行命令1:
docker run -d --name seata -p 8091:8091 seataio/seata-server:1.3.0

虚拟机里,创建/usr/local/docker/seata目录。

然后执行拷贝任务,拷贝seata:/seata-server 里的资源到/usr/local/docker/seata目录下。

#执行命令2:
docker cp seata:/seata-server /usr/local/docker/seata

修改以下对应配置文件

【file.conf文件】

file.conf 文件,用来告诉seata 数据存放再哪里的。

修改store.mode为db,修改db相关配置

## transaction log store, only used in seata-server
store {
  ## store mode: file、db、redis
  mode = "db"

  ## file store property
  file {
    ## store location dir
    dir = "sessionStore"
    # branch session size , if exceeded first try compress lockkey, still exceeded throws exceptions
    maxBranchSessionSize = 16384
    # globe session size , if exceeded throws exceptions
    maxGlobalSessionSize = 512
    # file buffer size , if exceeded allocate new buffer
    fileWriteBufferCacheSize = 16384
    # when recover batch read size
    sessionReloadReadSize = 100
    # async, sync
    flushDiskMode = async
  }

  ## database store property
  db {
    ## the implement of javax.sql.DataSource, such as DruidDataSource(druid)/BasicDataSource(dbcp)/HikariDataSource(hikari) etc.
    datasource = "druid"
    ## mysql/oracle/postgresql/h2/oceanbase etc.
    dbType = "mysql"
    driverClassName = "com.mysql.cj.jdbc.Driver"
    url = "jdbc:mysql://192.168.81.31:3308/seata"
    user = "root"
    password = "123456"
    minConn = 5
    maxConn = 30
    globalTable = "global_table"
    branchTable = "branch_table"
    lockTable = "lock_table"
    queryLimit = 100
    maxWait = 5000
  }

  ## redis store property
  redis {
    host = "127.0.0.1"
    port = "6379"
    password = ""
    database = "0"
    minConn = 1
    maxConn = 10
    queryLimit = 100
  }

}

【registry.conf】

registry.conf 告诉seata,我们的注册中心和配置中心用的是什么。

修改类型为nacos,修改nacos相关配置,包括registry和config的两组。在nacos中为seata创建一个属于自己的命名空间,它的配置比较多,避免和其他配置公用。

[Linux启动nacos,配置新的命名空间]

registry {
  # file 、nacos 、eureka、redis、zk、consul、etcd3、sofa
  type = "nacos"

  nacos {
    application = "seata-server"
    serverAddr = "192.168.81.31:8848"
    group = "SEATA_GROUP"
    namespace = "64a747e2-c7b9-464b-b9d9-586fa185f1dc"
    cluster = "default"
    username = "nacos"
    password = "nacos"
  }
  eureka {
    serviceUrl = "http://localhost:8761/eureka"
    application = "default"
    weight = "1"
  }
  redis {
    serverAddr = "localhost:6379"
    db = 0
    password = ""
    cluster = "default"
    timeout = 0
  }
  zk {
    cluster = "default"
    serverAddr = "127.0.0.1:2181"
    sessionTimeout = 6000
    connectTimeout = 2000
    username = ""
    password = ""
  }
  consul {
    cluster = "default"
    serverAddr = "127.0.0.1:8500"
  }
  etcd3 {
    cluster = "default"
    serverAddr = "http://localhost:2379"
  }
  sofa {
    serverAddr = "127.0.0.1:9603"
    application = "default"
    region = "DEFAULT_ZONE"
    datacenter = "DefaultDataCenter"
    cluster = "default"
    group = "SEATA_GROUP"
    addressWaitTime = "3000"
  }
  file {
    name = "file.conf"
  }
}

config {
  # file、nacos 、apollo、zk、consul、etcd3
  type = "nacos"

  nacos {
    serverAddr = "192.168.81.31:8848"
    group = "SEATA_GROUP"
    namespace = "64a747e2-c7b9-464b-b9d9-586fa185f1dc"
    username = "nacos"
    password = "nacos"
  }
  consul {
    serverAddr = "127.0.0.1:8500"
  }
  apollo {
    appId = "seata-server"
    apolloMeta = "http://192.168.1.204:8801"
    namespace = "application"
  }
  zk {
    serverAddr = "127.0.0.1:2181"
    sessionTimeout = 6000
    connectTimeout = 2000
    username = ""
    password = ""
  }
  etcd3 {
    serverAddr = "http://localhost:2379"
  }
  file {
    name = "file.conf"
  }
}

4、创建seata在nacos配置中心的配置文件

官方文件模板所在路径: incubator-seata/script/config-center/config.txt at develop · apache/incubator-seata · GitHub

创建一个config.txt文件,将以下内容改成自己的信息,粘贴进去保存。

#For details about configuration items, see https://seata.io/zh-cn/docs/user/configurations.html
#Transport configuration, for client and server
transport.type=TCP
transport.server=NIO
transport.heartbeat=true
transport.enableTmClientBatchSendRequest=false
transport.enableRmClientBatchSendRequest=true
transport.enableTcServerBatchSendResponse=false
transport.rpcRmRequestTimeout=30000
transport.rpcTmRequestTimeout=30000
transport.rpcTcRequestTimeout=30000
transport.threadFactory.bossThreadPrefix=NettyBoss
transport.threadFactory.workerThreadPrefix=NettyServerNIOWorker
transport.threadFactory.serverExecutorThreadPrefix=NettyServerBizHandler
transport.threadFactory.shareBossWorker=false
transport.threadFactory.clientSelectorThreadPrefix=NettyClientSelector
transport.threadFactory.clientSelectorThreadSize=1
transport.threadFactory.clientWorkerThreadPrefix=NettyClientWorkerThread
transport.threadFactory.bossThreadSize=1
transport.threadFactory.workerThreadSize=default
transport.shutdown.wait=3
transport.serialization=seata
transport.compressor=none

#Transaction routing rules configuration, only for the client
# ---------------------------------------------------
service.vgroupMapping.my_tx_group=default
# ---------------------------------------------------
#If you use a registry, you can ignore it
service.default.grouplist=127.0.0.1:8091
service.enableDegrade=false
service.disableGlobalTransaction=false

#Transaction rule configuration, only for the client
client.rm.asyncCommitBufferLimit=10000
client.rm.lock.retryInterval=10
client.rm.lock.retryTimes=30
client.rm.lock.retryPolicyBranchRollbackOnConflict=true
client.rm.reportRetryCount=5
client.rm.tableMetaCheckEnable=true
client.rm.tableMetaCheckerInterval=60000
client.rm.sqlParserType=druid
client.rm.reportSuccessEnable=false
client.rm.sagaBranchRegisterEnable=false
client.rm.sagaJsonParser=fastjson
client.rm.tccActionInterceptorOrder=-2147482648
client.tm.commitRetryCount=5
client.tm.rollbackRetryCount=5
client.tm.defaultGlobalTransactionTimeout=60000
client.tm.degradeCheck=false
client.tm.degradeCheckAllowTimes=10
client.tm.degradeCheckPeriod=2000
client.tm.interceptorOrder=-2147482648
client.undo.dataValidation=true
client.undo.logSerialization=jackson
client.undo.onlyCareUpdateColumns=true
server.undo.logSaveDays=7
server.undo.logDeletePeriod=86400000
client.undo.logTable=undo_log
client.undo.compress.enable=true
client.undo.compress.type=zip
client.undo.compress.threshold=64k
#For TCC transaction mode
tcc.fence.logTableName=tcc_fence_log
tcc.fence.cleanPeriod=1h

#Log rule configuration, for client and server
log.exceptionRate=100

#Transaction storage configuration, only for the server. The file, db, and redis configuration values are optional.
# ---------------------------------------------------
store.mode=db
store.lock.mode=db
store.session.mode=db
# ---------------------------------------------------
#Used for password encryption
store.publicKey=

#If `store.mode,store.lock.mode,store.session.mode` are not equal to `file`, you can remove the configuration block.
store.file.dir=file_store/data
store.file.maxBranchSessionSize=16384
store.file.maxGlobalSessionSize=512
store.file.fileWriteBufferCacheSize=16384
store.file.flushDiskMode=async
store.file.sessionReloadReadSize=100

#These configurations are required if the `store mode` is `db`. If `store.mode,store.lock.mode,store.session.mode` are not equal to `db`, you can remove the configuration block.
store.db.datasource=druid
store.db.dbType=mysql
# ---------------------------------------------------
store.db.driverClassName=com.mysql.cj.jdbc.Driver
store.db.url=jdbc:mysql://192.168.81.31:3308/seata?useUnicode=true&rewriteBatchedStatements=true
store.db.user=root
store.db.password=123456
# ---------------------------------------------------
store.db.minConn=5
store.db.maxConn=30
store.db.globalTable=global_table
store.db.branchTable=branch_table
store.db.distributedLockTable=distributed_lock
store.db.queryLimit=100
store.db.lockTable=lock_table
store.db.maxWait=5000

#These configurations are required if the `store mode` is `redis`. If `store.mode,store.lock.mode,store.session.mode` are not equal to `redis`, you can remove the configuration block.
store.redis.mode=single
store.redis.single.host=127.0.0.1
store.redis.single.port=6379
store.redis.sentinel.masterName=
store.redis.sentinel.sentinelHosts=
store.redis.maxConn=10
store.redis.minConn=1
store.redis.maxTotal=100
store.redis.database=0
store.redis.password=
store.redis.queryLimit=100

#Transaction rule configuration, only for the server
server.recovery.committingRetryPeriod=1000
server.recovery.asynCommittingRetryPeriod=1000
server.recovery.rollbackingRetryPeriod=1000
server.recovery.timeoutRetryPeriod=1000
server.maxCommitRetryTimeout=-1
server.maxRollbackRetryTimeout=-1
server.rollbackRetryTimeoutUnlockEnable=false
server.distributedLockExpireTime=10000
server.xaerNotaRetryTimeout=60000
server.session.branchAsyncQueueSize=5000
server.session.enableBranchAsyncRemove=false
server.enableParallelRequestHandle=false

#Metrics configuration, only for the server
metrics.enabled=false
metrics.registryType=compact
metrics.exporterList=prometheus
metrics.exporterPrometheusPort=9898

修改的内容

service.vgroupMapping.my_tx_group=default
store.mode=db
store.lock.mode=db
store.session.mode=db
store.db.driverClassName=com.mysql.cj.jdbc.Driver
store.db.url=jdbc:mysql://192.168.43.8:3306/seata?useUnicode=true&rewriteBatchedStatements=true
store.db.user=root
store.db.password=123456

5、创建导入config.txt到nacos的脚本

官方文件模板所在路径: incubator-seata/script/config-center/nacos/nacos-config.sh at develop · apache/incubator-seata · GitHub

创建 nacos-config.sh 文件,粘贴以下内容,没有内容更改。

#!/bin/sh
# Copyright 1999-2019 Seata.io Group.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at、
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

while getopts ":h:p:g:t:u:w:" opt
do
  case $opt in
  h)
    host=$OPTARG
    ;;
  p)
    port=$OPTARG
    ;;
  g)
    group=$OPTARG
    ;;
  t)
    tenant=$OPTARG
    ;;
  u)
    username=$OPTARG
    ;;
  w)
    password=$OPTARG
    ;;
  ?)
    echo " USAGE OPTION: $0 [-h host] [-p port] [-g group] [-t tenant] [-u username] [-w password] "
    exit 1
    ;;
  esac
done

if [ -z ${host} ]; then
    host=localhost
fi
if [ -z ${port} ]; then
    port=8848
fi
if [ -z ${group} ]; then
    group="SEATA_GROUP"
fi
if [ -z ${tenant} ]; then
    tenant=""
fi
if [ -z ${username} ]; then
    username=""
fi
if [ -z ${password} ]; then
    password=""
fi

nacosAddr=$host:$port
contentType="content-type:application/json;charset=UTF-8"

echo "set nacosAddr=$nacosAddr"
echo "set group=$group"

urlencode() {
  length="${#1}"
  i=0
  while [ $length -gt $i ]; do
    char="${1:$i:1}"
    case $char in
    [a-zA-Z0-9.~_-]) printf $char ;;
    *) printf '%%%02X' "'$char" ;;
    esac
    i=`expr $i + 1`
  done
}

failCount=0
tempLog=$(mktemp -u)
function addConfig() {
  dataId=`urlencode $1`
  content=`urlencode $2`
  curl -X POST -H "${contentType}" "http://$nacosAddr/nacos/v1/cs/configs?dataId=$dataId&group=$group&content=$content&tenant=$tenant&username=$username&password=$password" >"${tempLog}" 2>/dev/null
  if [ -z $(cat "${tempLog}") ]; then
    echo " Please check the cluster status. "
    exit 1
  fi
  if [ "$(cat "${tempLog}")" == "true" ]; then
    echo "Set $1=$2 successfully "
  else
    echo "Set $1=$2 failure "
    failCount=`expr $failCount + 1`
  fi
}

count=0
COMMENT_START="#"
for line in $(cat $(dirname "$PWD")/config.txt | sed s/[[:space:]]//g); do
    if [[ "$line" =~ ^"${COMMENT_START}".*  ]]; then
      continue
    fi
    count=`expr $count + 1`
	  key=${line%%=*}
    value=${line#*=}
	  addConfig "${key}" "${value}"
done

echo "========================================================================="
echo " Complete initialization parameters,  total-count:$count ,  failure-count:$failCount "
echo "========================================================================="

if [ ${failCount} -eq 0 ]; then
	echo " Init nacos config finished, please start seata-server. "
else
	echo " init nacos config fail. "
fi

6、导入nacos配置

将 config.txt文件添加到seata文件夹里,同时创建子目录nacos;将nacos-config.sh文件放到nacos目录里。

导入前要启动nacos并关闭防火墙,执行如下命令:为该文件分配一个执行的权限。

#命令1:为该文件分配一个执行的权限。
chmod +x nacos-config.sh

修改下面命令里的内容,包括ip和Id。

#命令2:修改ip地址和命名空间
sh nacos-config.sh -h 192.168.81.31 -p 8848 -g SEATA_GROUP -t 64a747e2-c7b9-464b-b9d9-586fa185f1dc -u nacos -w nacos

报错说含/r的错误,是因为windows下文件的换行符合Linux不同导致的,执行以下命令:

#命令3:
sed -i 's/\r//' nacos-config.sh
#然后再重新执行一遍命令2:
sh nacos-config.sh -h 192.168.81.31 -p 8848 -g SEATA_GROUP -t 6bb9bfd3-2af4-4fd8-8f75-c91482ba43f8 -u nacos -w nacos

7、复制配置文件

seata目录下原资源可以全部删掉了。 (改完配置文件执行命令后已经将改的配置激活,自己写的配置文件就没用了。这个步骤是因为docker对文件处理不方便)

将file.conf和registry.conf文件复制到/usr/local/docker/seata/config下,为再次创建seata容器做配置准备。

为config目录授权:

chmod 777 config

8、关闭并删除之前的seata容器

停掉刚才启动的seata,而且它以后也用不到了,可以删除。

9、数据库配置

启动seata需要有以下几张表的基础支持,所以在启动前,先创建如下数据库表。启动mysql数据库。

seata需要创建三张表:

全局事务---global_table

分支事务---branch_table

全局锁-----lock_table

建表语句

DROP TABLE
IF
	EXISTS `global_table`;
CREATE TABLE `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 ( 64 ),
	`timeout` INT,
	`begin_time` BIGINT,
	`application_data` VARCHAR ( 2000 ),
	`gmt_create` datetime,
	`gmt_modified` datetime,
	PRIMARY KEY ( `xid` ),
	KEY `idx_gmt_modified_status` ( `gmt_modified`, `status` ),
	KEY `idx_transaction_id` ( `transaction_id` ) 
);
DROP TABLE
IF
	EXISTS `branch_table`;
CREATE TABLE `branch_table` (
	`branch_id` BIGINT NOT NULL,
	`xid` VARCHAR ( 128 ) NOT NULL,
	`transaction_id` BIGINT,
	`resource_group_id` VARCHAR ( 32 ),
	`resource_id` VARCHAR ( 256 ),
	`lock_key` VARCHAR ( 128 ),
	`branch_type` VARCHAR ( 8 ),
	`status` TINYINT,
	`client_id` VARCHAR ( 64 ),
	`application_data` VARCHAR ( 2000 ),
	`gmt_create` datetime,
	`gmt_modified` datetime,
	PRIMARY KEY ( `branch_id` ),
	KEY `idx_xid` ( `xid` ) 
);
DROP TABLE
IF
	EXISTS `lock_table`;
CREATE TABLE `lock_table` (
	`row_key` VARCHAR ( 128 ) NOT NULL,
	`xid` VARCHAR ( 96 ),
	`transaction_id` LONG,
	`branch_id` LONG,
	`resource_id` VARCHAR ( 256 ),
	`table_name` VARCHAR ( 100 ),
	`pk` VARCHAR ( 32 ),
	`gmt_create` datetime,
	`gmt_modified` datetime,
	PRIMARY KEY ( `row_key` ) 
);

10、启动seata

在执行启动命令前要先配置seata数据库

docker run --name seata \
-p 8091:8091 \
-e SEATA_IP=192.168.81.31 \
-e SEATA_PORT=8091 \
-e SEATA_CONFIG_NAME=file:/root/seata-config/registry \
-v /usr/local/docker/seata/config:/root/seata-config  \
-d seataio/seata-server:1.3.0

查看日志信息,能看到端口号8091 就是启动成功了。

#查看上面的启动命令日志:
docker logs -f seata

三,应用seata

1.准备项目:

还是以openfeign工程做为例子,实现两张表的添加功能。

2.准备数据库表:

#建表代码
CREATE TABLE `score`  (
  `id` varchar(50)  NOT NULL,
  `name` varchar(50)  NULL,
  `score` double(7, 2) NULL ,
  PRIMARY KEY (`id`) USING BTREE
) 

CREATE TABLE `user`  (
  `id` varchar(50)  NOT NULL,
  `name` varchar(50)  NULL,
  `password` varchar(50)  NULL,
  PRIMARY KEY (`id`) USING BTREE
) 
CREATE TABLE `undo_log` (
	`id` BIGINT ( 20 ) NOT NULL AUTO_INCREMENT,
	`branch_id` BIGINT ( 20 ) NOT NULL,
	`xid` VARCHAR ( 100 ) NOT NULL,
	`context` VARCHAR ( 128 ) NOT NULL,
	`rollback_info` LONGBLOB NOT NULL,
	`log_status` INT ( 11 ) NOT NULL,
	`log_created` datetime NOT NULL,
	`log_modified` datetime NOT NULL,
	`ext` VARCHAR ( 100 ) DEFAULT NULL,
	PRIMARY KEY ( `id` ),
UNIQUE KEY `ux_undo_log` ( `xid`, `branch_id` ) 
) ENGINE = INNODB AUTO_INCREMENT = 1 DEFAULT CHARSET = utf8;

3.准备阶段,运行项目

【正常运行】

没有异常,成功运行,两张表里都有数据。

【添加异常】

添加异常,失败运行,两张表都添加数据成功了居然

【添加Spring事务】

两个工程添加事务注解,成绩添加数据成功,用户添加数据失败:Spring事务不能跨工程项目进行事务控制

===以上是分布式的情况下,事务没有统一管理。

4.应用seata

以下操作,每个工程项目里都操作一遍。

1、添加依赖

  <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
        </dependency>

2、各自数据库中创建UNDO_LOG表

给每个微服务的数据库都创建这张表才能应用seata

要求:具有InnoDB引擎的MySQL。

-- 注意此处0.3.0+ 增加唯一索引 ux_undo_log

CREATE TABLE `undo_log` (
	`id` BIGINT ( 20 ) NOT NULL AUTO_INCREMENT,
	`branch_id` BIGINT ( 20 ) NOT NULL,
	`xid` VARCHAR ( 100 ) NOT NULL,
	`context` VARCHAR ( 128 ) NOT NULL,
	`rollback_info` LONGBLOB NOT NULL,
	`log_status` INT ( 11 ) NOT NULL,
	`log_created` datetime NOT NULL,
	`log_modified` datetime NOT NULL,
	`ext` VARCHAR ( 100 ) DEFAULT NULL,
	PRIMARY KEY ( `id` ),
UNIQUE KEY `ux_undo_log` ( `xid`, `branch_id` ) 
) ENGINE = INNODB AUTO_INCREMENT = 1 DEFAULT CHARSET = utf8;

3、修改启动类

@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)

4、添加配置文件

在配置文件application.yml中,添加如下配置信息:

seata:
  application-id: ${spring.application.name}
  enabled: true
  tx-service-group: my_tx_group
  registry:
    type: nacos
    nacos:
      server-addr: 192.168.81.31:8848
      namespace: c05aaf00-2701-4d2e-8710-318752481731
      group: SEATA_GROUP
      username: nacos
      password: nacos
  config:
    type: nacos
    nacos:
      server-addr: 192.168.81.31:8848
      namespace: c05aaf00-2701-4d2e-8710-318752481731
      group: SEATA_GROUP
      username: nacos
      password: nacos
  service:
    vgroup-mapping:
      my_tx_group: default
#设置超时时间,默认是60秒,单位是毫秒       
feign:
  httpclient:
    connection-timeout: 600000
    connection-timer-repeat: 30000  

tx-service-group: my_tx_group

service: vgroup-mapping: my_tx_group: default

与nacos的配置要一致,nacos的配置是tx-service-group.my_tx_group=default

5、添加数据源配置

com.jr.config包下,创建下面的配置文件类。

import com.alibaba.druid.pool.DruidDataSource;
import io.seata.rm.datasource.DataSourceProxy;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;


@Configuration
public class DataSourceProxyConfig {

    @Bean
    @ConfigurationProperties(prefix = "spring.datasource")
    public DruidDataSource druidDataSource() {
        return new DruidDataSource();
    }

    @Bean
    @Primary
    public DataSourceProxy dataSourceProxy(DruidDataSource druidDataSource) {
        return new DataSourceProxy(druidDataSource);
    }

}

6、添加事务注解

更改之前ServletImpl类方法里的事务注解@Transactional(rollbackFor = Exception.class),改为下面注解:

@GlobalTransactional(rollbackFor = Exception.class,timeoutMills = 300000)

7、完成上述配置,运行结果

【有bug代码时】

两张表里都没有添加进数据,事务回滚。【一个错,两个一起回滚为原来的状态;没有事务,28行代码报错不影响27行代码执行结果】

【没有bug代码时】

数据添加成功

补:

如果报以下错误

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值