shardingsphere之sharding-proxy分库分表学习笔记

引言

随着业务数据量的变大,单库单表已经不能满足需求了。当单表数据量超过五百万行,查询性能急剧下降。分库分表迫在眉睫,寻找一个简单实用的解决方案相信是很多小伙伴的想法。
我在看了好多的博客之后遇到了开源数据库中间件mycat和shardingsphere(前身是sharding-jdbc),经过一番比较之后,我选了京东开源的shardingsphere作为我的解决方案。
写这篇文章的目的有两个,一来是帮助刚入门学习shardingsphere的童鞋快速上手,减少时间成本,先看下怎么用再去看官方文档可以达到事半功倍的效果;二来是记录自己在学习过程中遇到的问题,方便以后在项目中的使用。

重要提示

(1)版本信息:

  • 操作系统:windows 7
  • JDK1.8
  • mybatis-plus-generator 3.1.0
  • mybatis-plus-boot-starter 3.1.0
  • druid-spring-boot-starter 1.1.18
  • MYSQL 5.7.10

(2)用于演示的代码和重要的参考链接已经放到文章的末尾,有需要的童鞋可直接下载查看。其中
sharding_proxy文件夹存放代码,sharding-proxy-server存放proxy服务端文件。

sharding-proxy简介

概念

太多的理论知识我就不赘述了,麻烦自己到官网去看。
在这里插入图片描述
sharding-jdbc 和 sharding-proxy对比

在这里插入图片描述

特点

配置好之后可作为独立的数据源使用,一个逻辑数据库代理着几个真实数据库,可以用客户端软件比如Navicat Premium 直接去连接和操作。很好的帮助我们处理分库分表的问题,基本不需要对现有的业务代码修改,减少时间成本。

使用情况

目前生产环境已使用的公司
在这里插入图片描述

从零开始整合sharding-proxy

(1)其实大部分的工作都是在编写conf目录下的yml文件,配置好之后应用还是跟以前的方式一样连接和操作,基本不需要对代码进行修改。
(2)逻辑数据源可以配置多个,每个yml文件代表一个逻辑数据源,可手动创建yml文件和编写规则实现多个逻辑数据源的使用
(3)本文演示的是单个逻辑数据源的配置和使用。

整合前的思考

首先你要对业务需要用到的表有一个清晰的认识。哪些表不需要拆分,哪些表需要拆分,表跟表之间是否存在关联。通过阅读官网和我的理解,我觉得主要分为这几种表:

  • 单库单表

这种表数据量不大,小于十万这样,而且跟其他表没有关联。这样的表不需要拆分,放在一个默认库中即可。比如:配置表,地区编码表。

  • 广播表

这种表数据量不大,没有必要拆分;但是跟其他表有关联关系。在每个库都保存一个完整表,当读取数据的时候随机路由到任一库,当写入数据时每个库下的表都写入。

  • 逻辑表

数据量较大需要拆分的表。比如说订单数据根据主键尾数拆分为10张表,分别是t_order_0到t_order_9,他们的逻辑表名为t_order。

  • 绑定表

按我的理解就是父子表,常见的就是订单表和订单详情表,通过订单id关联。这种类型的表数据量大也是需要拆分的。

场景模拟

为了加深对sharding-proxy的理解,我在这里模拟了一个场景,基本涵盖了常见的情况,顺便把实现步骤和使用过程的问题也提一提。
在这里插入图片描述

搭建项目

1. 建库建表

按照前面表的关系图,我们可以划分一个默认库(存放单库单表和广播表)和三个库(存放逻辑表和广播表);如下所示。sql文件放在git地址的sql目录下。

data_source
	--area
	--config
	--factory
	--warehouse
	
data_source0
	--code_relate0
	--code_relate1
	--customer0
	--customer1
	--factory
	--indent_detail0
	--indent_detail1
	--indent0
	--indent1
	--task_upload0
	--task_upload1
	--task0
	--task1
	--warehouse
	
data_source1
	--code_relate0
	--code_relate1
	--customer0
	--customer1
	--factory
	--indent_detail0
	--indent_detail1
	--indent0
	--indent1
	--task_upload0
	--task_upload1
	--task0
	--task1
	--warehouse

data_source2
	--code_relate0
	--code_relate1
	--customer0
	--customer1
	--factory
	--indent_detail0
	--indent_detail1
	--indent0
	--indent1
	--task_upload0
	--task_upload1
	--task0
	--task1
	--warehouse

2.下载sharding-proxy二进制压缩包

压缩包下载地址
在这里插入图片描述

3、解压zip包

注意事项:

(1)解压工具最好选择winrar,不然解压出来的lib文件夹下的jar包由于命名太长被截断,后面将会导致文件找不到,系统启动失败异常。

(2)解压后的文件的路径下最好不要有中文,否则也会导致启动失败。

4、下载mysql驱动包

MySQL Connector/J,下载之后放到lib文件夹下。

5、编写yml文件

(1)首先要编写的是server.yaml,配置全局信息

##治理中心
# orchestration:
#   name: orchestration_ds
#   overwrite: true
#   registry:
#     type: zookeeper
#     serverLists: localhost:2181
#     namespace: orchestration

#权限配置
authentication:
  users:
    root:               #用户名
      password: root	#密码
    sharding:
      password: sharding 
      authorizedSchemas: sharding_db	#只能访问的逻辑数据库

#Proxy属性
props:
  max.connections.size.per.query: 1
  acceptor.size: 16  #用于设置接收客户端请求的工作线程个数,默认为CPU核数*2
  executor.size: 16  # Infinite by default.
  proxy.frontend.flush.threshold: 128  # The default value is 128.
    # LOCAL: Proxy will run with LOCAL transaction.
    # XA: Proxy will run with XA transaction.
    # BASE: Proxy will run with B.A.S.E transaction.
  proxy.transaction.type: LOCAL 	#默认为LOCAL事务
  proxy.opentracing.enabled: false     #是否开启链路追踪功能,默认为不开启。
  query.with.cipher.column: true
  sql.show: true				#SQL打印
  check.table.metadata.enabled: true			#是否在启动时检查分表元数据一致性,默认值: false
# proxy.frontend.flush.threshold: 				# 对于单个大查询,每多少个网络包返回一次

注意事项:

(1)当你把注释的示例内容复制粘贴,然后用快捷键去掉‘#’注释符的时候,注意父子层的距离不要改变,比如下方父子层之间是两个空格。

props:
  max.connections.size.per.query: 1

(2) 在编写的时候不要用‘Tab’键拉开距离,应该使用空格键,否则启动的时候会报错。

小插曲

当你编写yml文件的时候,里面都会有这句话,一开始我以为还要在哪里引用这些要用的文件,后面才发现直接去掉注释编写就好,文件都是自动引用的。

If you want to configure orchestration, authorization and proxy properties, please refer to this file.

6.编写config-sharding.yaml配置文件

其实编写这个文件和sharding-jdbc的yml配置文件基本相同,区别就是对于组合单词jdbc用的是驼峰,proxy用的是“-”,如果之前玩过sharding-jdbc的话可以直接把配置文件复制过来,对应修改即可。

schemaName: sharding_db		#逻辑数据库名配置

dataSources:      #真实数据源配置
  db:
    url: jdbc:mysql://127.0.0.1:3306/data_source?serverTimezone=UTC&useSSL=false&characterEncoding=utf-8
    username: root
    password: root
    connectionTimeoutMilliseconds: 30000
    idleTimeoutMilliseconds: 60000
    maxLifetimeMilliseconds: 1800000
    maxPoolSize: 50
  db0:
    url: jdbc:mysql://127.0.0.1:3306/data_source0?serverTimezone=UTC&useSSL=false&characterEncoding=utf-8
    username: root
    password: root
    connectionTimeoutMilliseconds: 30000
    idleTimeoutMilliseconds: 60000
    maxLifetimeMilliseconds: 1800000
    maxPoolSize: 50
  db1:
    url: jdbc:mysql://127.0.0.1:3306/data_source1?serverTimezone=UTC&useSSL=false&characterEncoding=utf-8
    username: root
    password: root
    connectionTimeoutMilliseconds: 30000
    idleTimeoutMilliseconds: 60000
    maxLifetimeMilliseconds: 1800000
    maxPoolSize: 50
  db2:
    url: jdbc:mysql://127.0.0.1:3306/data_source2?serverTimezone=UTC&useSSL=false&characterEncoding=utf-8
    username: root
    password: root
    connectionTimeoutMilliseconds: 30000
    idleTimeoutMilliseconds: 60000
    maxLifetimeMilliseconds: 1800000
    maxPoolSize: 50


shardingRule:      ##分库分表规则
  defaultDataSourceName: db    #默认数据源,放置不需要分片的表和广播表
  broadcastTables: 
    - factory
    - warehouse
  bindingTables: 
    - indent,indent_detail
    - task_upload,code_relate       ##绑定表配置
  defaultDatabaseStrategy:    #默认的分库规则,如果逻辑表没单独配置则使用这个
    inline:
      shardingColumn: customer_id    #默认按照customer_id分库
      algorithmExpression: db$->{customer_id % 3}
  tables:       #逻辑表配置
    config:               ###单库单表,使用UUID作为主键
      actualDataNodes: db.config
      keyGenerator:
        column: code
        type: UUID
    customer: 
      actualDataNodes: db$->{0..2}.customer$->{0..1}  #具体的数据节点
      tableStrategy:     ##分表策略
        inline:
          shardingColumn: customer_name     #根据hash值取模确定落在哪张表
          algorithmExpression: customer$->{Math.abs(customer_name.hashCode() % 2)}
      keyGenerator:    #配置主键生成策略,默认使用SNOWFLAKE
        column: customer_id
        type: SNOWFLAKE
        props:
          worker:
            id: 20200422
    indent:
      actualDataNodes: db$->{0..2}.indent$->{0..1}
      tableStrategy:
        inline: 
          shardingColumn: indent_id
          algorithmExpression: indent$->{indent_id % 2}
      keyGenerator:
        column: indent_id
        type: SNOWFLAKE 
    indent_detail:
      actualDataNodes: db$->{0..2}.indent_detail$->{0..1}
      tableStrategy:
        inline:
          shardingColumn: indent_id
          algorithmExpression: indent_detail$->{indent_id % 2}
      keyGenerator:
        column: detail_id
        type: SNOWFLAKE
    task:
      actualDataNodes: db$->{0..2}.task$->{0..1}  #具体的数据节点   
      databaseStrategy:   #分库规则 
        inline:
          shardingColumn: task_id
          algorithmExpression: db$->{task_id % 3}         
      tableStrategy:
        inline:
          shardingColumn: task_id
          algorithmExpression: task$->{task_id % 2}        
    task_upload:
      actualDataNodes: db$->{0..2}.task_upload$->{0..1}  #具体的数据节点   
      databaseStrategy:   #分库规则 
        inline:
          shardingColumn: task_id
          algorithmExpression: db$->{task_id % 3}         
      tableStrategy:
        inline:
          shardingColumn: stack_code
          algorithmExpression: task_upload$->{Math.abs(stack_code.hashCode() % 2)}        
      keyGenerator:
        column: upload_id
        type: SNOWFLAKE      
    code_relate:
      actualDataNodes: db$->{0..2}.code_relate$->{0..1}  #具体的数据节点   
      databaseStrategy:   #分库规则 
        inline:
          shardingColumn: task_id
          algorithmExpression: db$->{task_id % 3}         
      tableStrategy:
        inline:
          shardingColumn: stack_code
          algorithmExpression: code_relate$->{Math.abs(stack_code.hashCode() % 2)}        
      keyGenerator:
        column: relate_id
        type: SNOWFLAKE 

注意事项:

(1)数组的写法

  • 如果是广播表应该这么写
 broadcastTables: 
    - factory
    - warehouse
  • 如果是绑定表应该这么写
bindingTables: 
    - indent,indent_detail
    - task_upload,code_relate       ##绑定表配置

(2)分片键分为分库键和分表键。

(3)主键生成默认使用SNOWFLAKE算法,使用UUID主键的话需要配置。

(4)如果分片键的值为long型,分片规则为分片字段取模即可;如果是String型,分片规则为分片字段的哈希值取模再求绝对值,因为哈希值取模之后也许会出现负数。

(5)逻辑表和绑定表配置建议,尽可能的让同一类型的数据落在同一个库中。比如用户的信息和他产生的订单以及订单详情,可以通过consumer_id作为分库键,indent_id作为分表键存放,这样如果查询命中分片键的话可以提高查询效率(少查了不必要的表)。

(6)绑定表建表的时候,子表最好增加分库键字段便于新增数据时确定落到哪个库中。比如用户表、订单表和订单详情表,consumer_id作为分库键,订单表需要有这个字段,订单详情表也需要这个字段,否则订单详情新增数据的时候会在每个库都新增数据,很明显是不合理的情况。

7.编写logback.xml日志记录文件

日志路径默认在启动方式start.bat下创建个logs文件夹,所以只要填写相对路径就好。下面设置的是按级别按天输出日志,基本符合常规需求。

<?xml version="1.0"?>

<configuration>
  
    <!-- 控制台输出 -->
    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
        <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
            <!--格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度%msg:日志消息,%n是换行符-->
            <pattern>[%d{yyyy-MM-dd HH:mm:ss.SSS}] [%thread] %highlight([%-5level] %logger{50} - %msg%n)</pattern>
            <charset>UTF-8</charset>
        </encoder>
    </appender>
   
    <!-- 系统正常日志文件 -->
    <appender name="SYSTEM_INFO"  class="ch.qos.logback.core.rolling.RollingFileAppender">
        <!-- 过滤器,只打印INFO级别的日志 -->
        <filter class="ch.qos.logback.classic.filter.LevelFilter">
            <level>INFO</level>
            <onMatch>ACCEPT</onMatch>
            <onMismatch>DENY</onMismatch>
        </filter>
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <!--日志文件输出的文件名-->
            <fileNamePattern>../logs/%d{yyyy-MM-dd}/info.%i.log</fileNamePattern>
            <!--日志文件保留天数-->
            <!-- <MaxHistory>15</MaxHistory> -->
                <!-- 除按日志记录之外,还配置了日志文件不能超过2M,若超过2M,日志文件会以索引0开始,命名日志文件,例如log-error-2013-12-21.0.log -->
                <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
                    <maxFileSize>10MB</maxFileSize>
                </timeBasedFileNamingAndTriggeringPolicy>
        </rollingPolicy>
          <!-- 追加方式记录日志 -->
            <append>true</append>
      <!-- 日志文件的格式 -->
        <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
            <!--格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度%msg:日志消息,%n是换行符-->
            <pattern>[%d{yyyy-MM-dd HH:mm:ss.SSS}] [%thread] [%-5level] %logger{50} - %msg%n</pattern>
            <charset>UTF-8</charset>
        </encoder>
    </appender>
    
     <!-- 系统警告日志文件,记录用户输入不符合规则的数据 -->
    <appender name="SYSTEM_WARN"  class="ch.qos.logback.core.rolling.RollingFileAppender">
        <!-- 过滤器,只打印warn级别的日志 -->
        <filter class="ch.qos.logback.classic.filter.LevelFilter">
            <level>WARN</level>
            <onMatch>ACCEPT</onMatch>
            <onMismatch>DENY</onMismatch>
        </filter>
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <!--日志文件输出的文件名-->
            <fileNamePattern>../logs/%d{yyyy-MM-dd}/warn.%i.log</fileNamePattern>
                <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
                    <maxFileSize>10MB</maxFileSize>
                </timeBasedFileNamingAndTriggeringPolicy>
        </rollingPolicy>
          <!-- 追加方式记录日志 -->
            <append>true</append>
      <!-- 日志文件的格式 -->
        <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
            <!--格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度%msg:日志消息,%n是换行符-->
            <pattern>[%d{yyyy-MM-dd HH:mm:ss.SSS}] [%thread] [%-5level] %logger{50} - %msg%n</pattern>
            <charset>UTF-8</charset>
        </encoder>
    </appender>
    
    <!-- 系统错误日志文件 -->
    <appender name="SYSTEM_ERROR"  class="ch.qos.logback.core.rolling.RollingFileAppender">
        <!-- 过滤器,只打印ERROR级别的日志 -->
        <filter class="ch.qos.logback.classic.filter.LevelFilter">
            <level>ERROR</level>
            <onMatch>ACCEPT</onMatch>
            <onMismatch>DENY</onMismatch>
        </filter>
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <!--日志文件输出的文件名-->
            <fileNamePattern>../logs/%d{yyyy-MM-dd}/error.%i.log</fileNamePattern>
                <!-- 除按日志记录之外,还配置了日志文件不能超过2M,若超过2M,日志文件会以索引0开始,命名日志文件,例如log-error-2013-12-21.0.log -->
                <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
                    <MaxFileSize>10MB</MaxFileSize>
                </timeBasedFileNamingAndTriggeringPolicy>
        </rollingPolicy>
          <!-- 追加方式记录日志 -->
            <append>true</append>
      <!-- 日志文件的格式 -->
        <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
            <!--格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度%msg:日志消息,%n是换行符-->
            <pattern>[%d{yyyy-MM-dd HH:mm:ss.SSS}] [%thread] [%-5level] %logger{50} - %msg%n</pattern>
            <charset>UTF-8</charset>
        </encoder>
    </appender>
    
  
     <logger name="system_error" additivity="false">
        <appender-ref ref="SYSTEM_ERROR"/>
    </logger>
    
     <!-- 默认纪录级别 -->
    <root level="INFO">
        <appender-ref ref="CONSOLE" />
        <appender-ref ref="SYSTEM_INFO" />
        <appender-ref ref="SYSTEM_WARN" />
        <appender-ref ref="SYSTEM_ERROR" /> 
    </root>
  
</configuration> 

8.启动sharding-proxy

在cmd命令窗口中cd到bin目录下,然后直接运行start.bat,默认是3307端口;当然你也可以指定端口,在后面加上参数即可,比如start.bat 3309即可指定连接端口为3309。然后观察cmd窗口,如果没报出异常的话表示启动成功。
在这里插入图片描述

9.客户端连接

这里推荐使用Navicat Premium 11 去连接,使用Navicat Premium 12的话你会发现很多奇怪的问题,下面是官网的建议和成功连接的图。至此,proxy服务端搭建好了,接下来是编写代码。

在这里插入图片描述
在这里插入图片描述

10.基础CRUD代码生成

通过示例代码的工具类连接generator库可以快速生成基础的CRUD代码

src/test/java/com/project/generator/MybatisGenerator.java

注意事项

(1)实体主键类型的选择

  • 如果主键是long型的话,可以这么配置,个人建议选择type = IdType.ID_WORKER这样更直白明了。否则插入数据时会报错。
/**
     * id
     */
    @TableId(value = "id", type = IdType.ID_WORKER)
    private Long id;

或者

  /**
     * id
     */
    @TableId(value = "id", type = IdType.NONE)
    private Long id;

使用type= ID.AUTO的异常信息

Caused by: java.sql.SQLException: Field ‘id’ doesn’t have a default value
at com.mysql.jdbc.SQLError.createSQLException(SQLError.java:965)
at com.mysql.jdbc.MysqlIO.checkErrorPacket(MysqlIO.java:3978)
at com.mysql.jdbc.MysqlIO.checkErrorPacket(MysqlIO.java:3914)
at com.mysql.jdbc.MysqlIO.sendCommand(MysqlIO.java:2530)
at com.mysql.jdbc.MysqlIO.sqlQueryDirect(MysqlIO.java:2683)
at com.mysql.jdbc.ConnectionImpl.execSQL(ConnectionImpl.java:2495)
at com.mysql.jdbc.PreparedStatement.executeInternal(PreparedStatement.java:1903)
at com.mysql.jdbc.PreparedStatement.executeUpdateInternal(PreparedStatement.java:2124)
at com.mysql.jdbc.PreparedStatement.executeBatchSerially(PreparedStatement.java:1801)
… 89 common frames omitted

  • 如果主键是String型的话,可以这么配置。因为默认的是SNOWFLAKE生成,否则会插入一个long型的主键值导致报错。
   /**
     * 编号
     */
    @TableId(value = "code", type = IdType.AUTO)
    private String code;

或者

  /**
     * 编号
     */
    @TableId(value = "code", type = IdType.UUID)
    private String code;

项目运行

测试代码已经写到里面了,通过发起请求和观察cmd窗口或者日志文件你会发现逻辑SQL和真实SQL,从而发现他的查询规则:

1、如果表没配置规则,那么直接到默认库去访问

2、如果访问的是广播表,那么读的时候是随机路由到一个库,写的时候是全部库都写数据。

3、逻辑表查询,查询字段命中了分库键,那么路由到指定库下的所有表查询;命中了分表键,到所有库下指定表查询。如果都没命中,那么将发生笛卡尔积,进行全路由所有的库和表都查询一遍,效率不高。所以合理的配置分片规则是很重要的。

分布式事务

由于这里只配置了一个逻辑数据源(包含多个真实数据源),不大清楚算不算分布式事务。不过写法跟原来是一样的,加上注解即可。运行下面的代码发生异常,数据将不会写进数据库,表示事务控制成功。

 /**
     * 测试事务管理
     * 
     * @date 2020年5月3日
     * @author huangjg
     */
    @Transactional(rollbackFor = Exception.class)
    @PostMapping("/save")
    public ResponseData<?> save() {
        customerService.saveBatch(list);
        int i = 5 / 0;
        return ResponseData.out(CodeEnum.SUCCESS, null);
    }

弹性伸缩

这个还没开始研究,估计在4.1.x版本会有这个功能。

配置zookeeper

目前我将zookeeper跑起来的时候不懂如何跟项目对接起来,如果有成功的同学麻烦将方法告知下。

结语

官网的文档比较详细和社区都是很活跃的,这些可以减少我们的学习成本,快速用于项目。如果在学习的过程中遇到问题可以多看看官方文档或者直接到github上面提issues,官方人员会很快给予答复的。

相关链接

演示代码地址

shardingsphere官网地址

shardingsphere github地址

Sharding-Proxy 是一个开源的分库分表中间件,它可以帮助应用程序实现无感知的分库分表操作。下面是一个简单的步骤来利用 Sharding-Proxy 进行分库分表: 1. 安装和配置 Sharding-Proxy:首先,你需要下载 Sharding-Proxy 的安装包,并解压到你的服务器上。然后,根据你的需求修改配置文件,配置数据源和分片规则等信息。 2. 创建数据库和表:在进行分库分表之前,你需要创建相应的数据库和表结构。你可以选择手动创建,或者使用 Sharding-Proxy 提供的自动建表功能。 3. 配置分片规则:在 Sharding-Proxy 的配置文件中,你需要定义分片规则,指定如何将数据分散到不同的数据库和表中。可以使用基于范围、哈希、精确等多种分片算法。 4. 连接到 Sharding-Proxy:在应用程序中,需要修改数据库连接信息,将原来连接数据库的地址改为连接 Sharding-Proxy 的地址。这样应用程序就可以通过 Sharding-Proxy 访问分片后的数据。 5. 进行分库分表操作:现在你可以在应用程序中执行正常的数据库操作,而无需关心具体的分库分表细节。Sharding-Proxy 会根据配置的规则自动将数据路由到正确的库和表中。 需要注意的是,使用 Sharding-Proxy 进行分库分表操作需要仔细考虑数据一致性、事务处理、跨库查询等问题。在配置和使用过程中,建议参考官方文档和示例来确保正确性和性能。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值