1 SpringBoot 使用sharding jdbc进行分库分表

这一篇版本有的老,用新版的移步这一篇:https://tianyalei.blog.csdn.net/article/details/98057916

分库分表在数据量大的系统中比较常用,解决方案有Cobar,TDDL等,这次主要是拿当当网开源的Sharding-JDBC来做个小例子。
它的github地址为:https://github.com/dangdangdotcom/sharding-jdbc
简介:
Sharding-JDBC直接封装JDBC API,可以理解为增强版的JDBC驱动,旧代码迁移成本几乎为零:
可适用于任何基于java的ORM框架,如:JPA, Hibernate, Mybatis, Spring JDBC Template或直接使用JDBC。
可基于任何第三方的数据库连接池,如:DBCP, C3P0, BoneCP, Druid等。
理论上可支持任意实现JDBC规范的数据库。虽然目前仅支持MySQL,但已有支持Oracle,SQLServer,DB2等数据库的计划。
Sharding-JDBC定位为轻量级java框架,使用客户端直连数据库,以jar包形式提供服务,未使用中间层,无需额外部署,无其他依赖,DBA也无需改变原有的运维方式。SQL解析使用Druid解析器,是目前性能最高的SQL解析器。
具体的介绍可以上它的文档那里看看,简单归纳起来就是,它是一个增强版的JDBC,对使用者透明,逻辑代码什么的都不用动,它来完成分库分表的操作;然后它还支持分布式事务(不完善)。看起来很不错的样子。
下面用个小例子来看一下分库分表的使用。使用的是SpringBoot,JPA(hibernate),druid连接池。

使用Idea新建个Spring Boot项目

pom文件如下:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>

	<groupId>com.tianyalei</groupId>
	<artifactId>shardingtest</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<packaging>jar</packaging>

	<name>shardingtest</name>
	<description>Demo project for Spring Boot</description>

	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>1.5.2.RELEASE</version>
		<relativePath/> <!-- lookup parent from repository -->
	</parent>

	<properties>
		<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
		<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
		<java.version>1.8</java.version>
	</properties>

	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-data-jpa</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>

		<dependency>
			<groupId>mysql</groupId>
			<artifactId>mysql-connector-java</artifactId>
			<version>5.1.41</version>
		</dependency>
		<dependency>
			<groupId>com.dangdang</groupId>
			<artifactId>sharding-jdbc-core</artifactId>
			<version>1.4.2</version>
		</dependency>
		<dependency>
			<groupId>com.alibaba</groupId>
			<artifactId>druid</artifactId>
			<version>1.0.12</version>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>
	</dependencies>

	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
			</plugin>
		</plugins>
	</build>


</project>

目前最新的sharding jdbc是1.4.2,里面druid版本是1.0.12,这个是因为
这里写图片描述
虽然没试验,但是还是按他们的要求来吧。
在官方文档里能看到,配置sharding jdbc有三种方式,可以用java代码配置,YAML配置和Spring xml配置http://dangdangdotcom.github.io/sharding-jdbc/02-guide/configuration/
不同的配置需要引入不同的maven依赖。这里我采用java代码配置的方式,最简单。

在application.yml里配置一下jpa

如下:

spring:
  jpa:
    database: mysql
    show-sql: true
    hibernate:
      ddl-auto: none

指明jpa的数据库为mysql,hibernate的ddl-auto方式为none。
为毛为none?而不是update之类的,update的话hibernate就能自动帮我们建表了。
是因为,既然是分库分表,表名就是Order1,Order2之类的,hibernate只能建立个Order映射,是建不出来多个分表的,所以表就自己建。

建domain

import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;

/**
 * Created by wuwf on 17/4/19.
 */
@Entity
@Table(name = "t_order")
public class Order {
    @Id
    private Long orderId;

    private Long userId;

    public Long getOrderId() {
        return orderId;
    }

    public void setOrderId(Long orderId) {
        this.orderId = orderId;
    }

    public Long getUserId() {
        return userId;
    }

    public void setUserId(Long userId) {
        this.userId = userId;
    }
}

一个订单表,表名为t_order,里面有个主键orderId和userId,这次userId还没用上,以后用多对一关联时再用。
可以看到,只在orderId上加了@Id2而没有加@GeneratedValue(strategy = GenerationType.AUTO)的主键生成策略,mysql一般用自增。
为什么不加呢?因为不能加,你分表了,主键如果还是自增,就会出现主键重复!!重复了,程序就不能识别数据唯一性了。
这里写图片描述
所以这个主键需要由我们自己来创建生成。
再创建一个最基本的repository

import com.tianyalei.domain.Order;
import org.springframework.data.repository.CrudRepository;

/**
 * Created by wuwf on 17/4/19.
 */
public interface OrderRepository extends CrudRepository<Order, Long> {
}

分库分表的配置

package com.tianyalei.config;

import com.alibaba.druid.pool.DruidDataSource;
import com.dangdang.ddframe.rdb.sharding.api.ShardingDataSourceFactory;
import com.dangdang.ddframe.rdb.sharding.api.rule.DataSourceRule;
import com.dangdang.ddframe.rdb.sharding.api.rule.ShardingRule;
import com.dangdang.ddframe.rdb.sharding.api.rule.TableRule;
import com.dangdang.ddframe.rdb.sharding.api.strategy.database.DatabaseShardingStrategy;
import com.dangdang.ddframe.rdb.sharding.api.strategy.table.TableShardingStrategy;
import com.mysql.jdbc.Driver;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.sql.DataSource;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;

/**
 * Created by wuwf on 17/4/19.
 */
@Configuration
public class DataSourceConfig {
    @Bean
    public DataSource getDataSource() {
        return buildDataSource();
    }

    private DataSource buildDataSource() {
        //设置分库映射
        Map<String, DataSource> dataSourceMap = new HashMap<>(2);
        //添加两个数据库ds_0,ds_1到map里
        dataSourceMap.put("ds_0", createDataSource("ds_0"));
        dataSourceMap.put("ds_1", createDataSource("ds_1"));
        //设置默认db为ds_0,也就是为那些没有配置分库分表策略的指定的默认库
        //如果只有一个库,也就是不需要分库的话,map里只放一个映射就行了,只有一个库时不需要指定默认库,但2个及以上时必须指定默认库,否则那些没有配置策略的表将无法操作数据
        DataSourceRule dataSourceRule = new DataSourceRule(dataSourceMap, "ds_0");

        //设置分表映射,将t_order_0和t_order_1两个实际的表映射到t_order逻辑表
        //0和1两个表是真实的表,t_order是个虚拟不存在的表,只是供使用。如查询所有数据就是select * from t_order就能查完0和1表的
        TableRule orderTableRule = TableRule.builder("t_order")
                .actualTables(Arrays.asList("t_order_0", "t_order_1"))
                .dataSourceRule(dataSourceRule)
                .build();

        //具体分库分表策略,按什么规则来分
        ShardingRule shardingRule = ShardingRule.builder()
                .dataSourceRule(dataSourceRule)
                .tableRules(Arrays.asList(orderTableRule))
                .databaseShardingStrategy(new DatabaseShardingStrategy("user_id", new ModuloDatabaseShardingAlgorithm()))
                .tableShardingStrategy(new TableShardingStrategy("order_id", new ModuloTableShardingAlgorithm())).build();

        DataSource dataSource = ShardingDataSourceFactory.createDataSource(shardingRule);

        return dataSource;
    }

    private static DataSource createDataSource(final String dataSourceName) {
        //使用druid连接数据库
        DruidDataSource result = new DruidDataSource();
        result.setDriverClassName(Driver.class.getName());
        result.setUrl(String.format("jdbc:mysql://localhost:3306/%s", dataSourceName));
        result.setUsername("root");
        result.setPassword("");
        return result;
    }
}

我们没有在配置文件里指明数据库的DataSource,就需要在java代码里来配置DataSource。普通情况不分库时,只需要在getDataSource方法直接返回createDataSource方法就行了,里面指定了使用druidDataSource。
现在分库了,我们就要用Sharding JDBC封装的DataSource了,由它来接管数据库连接。
也就是DataSource dataSource = ShardingDataSourceFactory.createDataSource(shardingRule);
可以看到,Sharding JDBC封装的DataSource主要是需要构造一个shardingRule参数。
这个类也主要就是构造这个规则,注释里面写的比较清晰了。差不多流程就是创建个Map<String, DataSource>赋给DataSourceRule,然后创建1个或多个TableRule,主要是配置多表映射的逻辑表,然后生成ShardingRule,参数有dataSourceRule、1或多个TableRule(在.tableRules(Arrays.asList(orderTableRule, XXXXTableRule))里添加)、然后是databaseShardingStrategy来指明按照什么字段的什么规则来决定使用哪个库、tableShardingStrategy同上。
譬如库的选择的话,我们可以使用user_id来做个算法来分流,使用user_id % 2 = 0的放到库1,其他的放库2。表的话可以用order_id % 2 = 0的来划分到不同表。规则可以随意指定,比较常见是按查询规则,譬如经常查询某个用户的所有订单,那就可以把user_id相等的放到一个表里去。

具体的策略算法

在上面的代码里,分别使用了ModuloDatabaseShardingAlgorithm和ModuloTableShardingAlgorithm来分别指定库和表的分流策略。
现在来看看这两个类。

import com.dangdang.ddframe.rdb.sharding.api.ShardingValue;
import com.dangdang.ddframe.rdb.sharding.api.strategy.database.SingleKeyDatabaseShardingAlgorithm;
import com.google.common.collect.Range;

import java.util.Collection;
import java.util.LinkedHashSet;

/**
 * Created by wuwf on 17/4/19.
 */
public class ModuloDatabaseShardingAlgorithm implements SingleKeyDatabaseShardingAlgorithm<Long> {

    @Override
    public String doEqualSharding(Collection<String> availableTargetNames, ShardingValue<Long> shardingValue) {
        for (String each : availableTargetNames) {
            if (each.endsWith(shardingValue.getValue() % 2 + "")) {
                return each;
            }
        }
        throw new IllegalArgumentException();
    }

    @Override
    public Collection<String> doInSharding(Collection<String> availableTargetNames, ShardingValue<Long> shardingValue) {
        Collection<String> result = new LinkedHashSet<>(availableTargetNames.size());
        for (Long value : shardingValue.getValues()) {
            for (String tableName : availableTargetNames) {
                if (tableName.endsWith(value % 2 + "")) {
                    result.add(tableName);
                }
            }
        }
        return result;
    }

    @Override
    public Collection<String> doBetweenSharding(Collection<String> availableTargetNames,
                                                ShardingValue<Long> shardingValue) {
        Collection<String> result = new LinkedHashSet<>(availableTargetNames.size());
        Range<Long> range = shardingValue.getValueRange();
        for (Long i = range.lowerEndpoint(); i <= range.upperEndpoint(); i++) {
            for (String each : availableTargetNames) {
                if (each.endsWith(i % 2 + "")) {
                    result.add(each);
                }
            }
        }
        return result;
    }

}
import com.dangdang.ddframe.rdb.sharding.api.ShardingValue;
import com.dangdang.ddframe.rdb.sharding.api.strategy.table.SingleKeyTableShardingAlgorithm;
import com.google.common.collect.Range;

import java.util.Collection;
import java.util.LinkedHashSet;

/**
 * Created by wuwf on 17/4/19.
 */
public final class ModuloTableShardingAlgorithm implements SingleKeyTableShardingAlgorithm<Long> {

    /**
     *  select * from t_order from t_order where order_id = 11
     *          └── SELECT *  FROM t_order_1 WHERE order_id = 11
     *  select * from t_order from t_order where order_id = 44
     *          └── SELECT *  FROM t_order_0 WHERE order_id = 44
     */
    public String doEqualSharding(final Collection<String> tableNames, final ShardingValue<Long> shardingValue) {
        for (String each : tableNames) {
            if (each.endsWith(shardingValue.getValue() % 2 + "")) {
                return each;
            }
        }
        throw new IllegalArgumentException();
    }

    /**
     *  select * from t_order from t_order where order_id in (11,44)
     *          ├── SELECT *  FROM t_order_0 WHERE order_id IN (11,44)
     *          └── SELECT *  FROM t_order_1 WHERE order_id IN (11,44)
     *  select * from t_order from t_order where order_id in (11,13,15)
     *          └── SELECT *  FROM t_order_1 WHERE order_id IN (11,13,15)
     *  select * from t_order from t_order where order_id in (22,24,26)
     *          └──SELECT *  FROM t_order_0 WHERE order_id IN (22,24,26)
     */
    public Collection<String> doInSharding(final Collection<String> tableNames, final ShardingValue<Long> shardingValue) {
        Collection<String> result = new LinkedHashSet<>(tableNames.size());
        for (Long value : shardingValue.getValues()) {
            for (String tableName : tableNames) {
                if (tableName.endsWith(value % 2 + "")) {
                    result.add(tableName);
                }
            }
        }
        return result;
    }

    /**
     *  select * from t_order from t_order where order_id between 10 and 20
     *          ├── SELECT *  FROM t_order_0 WHERE order_id BETWEEN 10 AND 20
     *          └── SELECT *  FROM t_order_1 WHERE order_id BETWEEN 10 AND 20
     */
    public Collection<String> doBetweenSharding(final Collection<String> tableNames, final ShardingValue<Long> shardingValue) {
        Collection<String> result = new LinkedHashSet<>(tableNames.size());
        Range<Long> range = shardingValue.getValueRange();
        for (Long i = range.lowerEndpoint(); i <= range.upperEndpoint(); i++) {
            for (String each : tableNames) {
                if (each.endsWith(i % 2 + "")) {
                    result.add(each);
                }
            }
        }
        return result;
    }
}

主要看doEqualSharding方法(譬如select * from t_order where user_id = 11就是equal),availableTargetNames就是所有的库名(ds_0,ds_1),shardingValue就是在DataSourceConfig里指定的user_id,代码就是如果user_id是偶数就算到ds_0数据库,其他的就放ds_1数据库。而另外的两个方法,doIn和doBetween是用在如where user_id in (1,23,7)和where user_id between(1, 6)。
table的策略和db的策略是一样的,算法可以自己定。
上面两个都是实现的SingleKeyShardingAlgorithm,也就是单列策略,也可以使用多列策略,譬如user_id 和 order_id同时符合某个条件的,分到哪个表。
new TableShardingStrategy(Arrays.asList(“order_id”, “order_type”, “order_date”), new MultiKeyShardingAlgorithm()))

上面都是根据一列或多列来决定分库分表策略,官方还提供了不根据列的路由策略,参考 强制路由。http://dangdangdotcom.github.io/sharding-jdbc/02-guide/hint-sharding-value/

创建DB和表

目标:
db0
├── t_order_0 user_id为偶数 order_id为偶数
├── t_order_1 user_id为偶数 order_id为奇数
db1
├── t_order_0 user_id为奇数 order_id为偶数
├── t_order_1 user_id为奇数 order_id为奇数
先来创建两个db,ds_0和ds_1。然后分别在每个库里建表t_order_0和t_order_1。
建标语句:DROP TABLE IF EXISTS t_order_0;
CREATE TABLE t_order_0 (
order_id bigint(20) NOT NULL,
user_id bigint(20) NOT NULL,
PRIMARY KEY (order_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin;
再改个表名为1,然后分别在两个库里都执行一下。切记不能勾选auto_increment.
下面写个controller来试试添加和查询数据


import com.tianyalei.domain.Order;
import com.tianyalei.repository.OrderRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * Created by wuwf on 17/4/19.
 */
@RestController
@RequestMapping("/order")
public class OrderController {

    @Autowired
    private OrderRepository orderRepository;

    @RequestMapping("/add")
    public Object add() {
        for (int i = 0; i < 10; i++) {
            Order order = new Order();
            order.setUserId((long) i);
            order.setOrderId((long) i);
            orderRepository.save(order);
        }
        for (int i = 10; i < 20; i++) {
            Order order = new Order();
            order.setUserId((long) i + 1);
            order.setOrderId((long) i);
            orderRepository.save(order);
        }
        return "success";
    }

    @RequestMapping("query")
    private Object queryAll() {
        return orderRepository.findAll();
    }
}

启动Application,然后在浏览器访问http://localhost:8080/order/add
然后查看数据库,可以看到两个库共4个表,数据刚好是按配置的规则均匀分布的。
这里写图片描述
这样我们就完成了分库分表的操作。这里主键order_id我们是手工指定的,不能用mysql自增的那个。而在实际应用中,主键Id的生成唯一性是个比较麻烦的问题。

分布式主键

先看官方的说法,http://dangdangdotcom.github.io/sharding-jdbc/02-guide/id-generator/
传统数据库软件开发中,主键自动生成技术是基本需求。而各大数据库对于该需求也提供了相应的支持,比如MySQL的自增键。 对于MySQL而言,分库分表之后,不同表生成全局唯一的Id是非常棘手的问题。因为同一个逻辑表内的不同实际表之间的自增键是无法互相感知的, 这样会造成重复Id的生成。我们当然可以通过约束表生成键的规则来达到数据的不重复,但是这需要引入额外的运维力量来解决重复性问题,并使框架缺乏扩展性。
目前有许多第三方解决方案可以完美解决这个问题,比如UUID等依靠特定算法自生成不重复键,或者通过引入Id生成服务等。 但也正因为这种多样性导致了Sharding-JDBC如果强依赖于任何一种方案就会限制其自身的发展。
基于以上的原因,最终采用了以JDBC接口来实现对于生成Id的访问,而将底层具体的Id生成实现分离出来。
其实最终要解决的问题就是各库各表中的数据,主键不能重复。官方提供的statement什么的没看懂,我就直接用它提供的通用主键生成器来生成主键了。其实就是提供了一个类,这个类能生成一个保证不重复的Long型数字,我们就用它做主键。
添加pom依赖:

<dependency>
    <groupId>com.dangdang</groupId>
    <artifactId>sharding-jdbc-self-id-generator</artifactId>
    <version>${sharding-jdbc.version}</version>
</dependency>

该生成器生成的数据为64bit的long型数据。 在数据库中应该用大于等于64bit的数字类型的字段来保存该值,比如在MySQL中应该使用BIGINT。

唯一主键使用

在DataSourceConfig里加个bean

@Bean
    public IdGenerator getIdGenerator() {
        return new CommonSelfIdGenerator();
    }

采用CommonSelfIdGenerator来生成IdGenerator。然后在controller里使用它生成orderId即可。

@RestController
@RequestMapping("/order")
public class OrderController {

    @Autowired
    private OrderRepository orderRepository;
    @Autowired
    private IdGenerator idGenerator;

    @RequestMapping("/add")
    public Object add() {
//        for (int i = 0; i < 10; i++) {
//            Order order = new Order();
//            order.setUserId((long) i);
//            order.setOrderId((long) i);
//            orderRepository.save(order);
//        }
//        for (int i = 10; i < 20; i++) {
//            Order order = new Order();
//            order.setUserId((long) i + 1);
//            order.setOrderId((long) i);
//            orderRepository.save(order);
//        }
        Order order = new Order();
        order.setUserId(1L);
        order.setOrderId(idGenerator.generateId().longValue());
        orderRepository.save(order);
        return "success";
    }

    @RequestMapping("query")
    private Object queryAll() {
        return orderRepository.findAll();
    }
}

再访问add,看看生成的orderId。以后就可以靠这个类生成不重复ID了。

本节Over……

  • 10
    点赞
  • 88
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 23
    评论
### 回答1: 我可以为你提供一些关于使用SpringBoot ShardingJDBC MyBatis进行分库分表的建议,例如:1. 使用ShardingSphere框架定义数据库分片策略;2. 使用MyBatis实现MyBatis SQL脚本;3. 使用SpringBoot注解的方式定义数据源和ShardingJDBC的配置。 ### 回答2: 使用SpringBoot ShardingJDBC和MyBatis可以很方便地实现分库分表功能。首先,ShardingJDBC是一个轻量级的数据库中间件,可以将数据分散到不同的数据库实例中,从而实现分库的效果。其次,MyBatis是一个流行的持久层框架,可以通过XML或注解的方式与数据库进行交互。 在使用SpringBoot ShardingJDBC和MyBatis做分库分表时,首先需要配置ShardingJDBC的数据源和分片规则。可以通过编写一个配置类来配置分库分表的规则,例如可以根据某个字段的取值来确定数据应该分散到哪个库或表中。配置完成后,就可以在MyBatis的Mapper接口中直接使用分库分表的数据源,从而实现对不同数据库或表的访问。 在编写Mapper接口时,可以使用MyBatis提供的注解或XML方式来编写SQL语句。在SQL语句中,可以使用ShardingJDBC提供的分片键来实现对特定库或表的访问。例如,在需要查询特定表的数据时,可以使用ShardingJDBC提供的Hint注解将查询操作路由到相应的表上。 总的来说,使用SpringBoot ShardingJDBC和MyBatis可以实现简单、高效的分库分表功能。通过配置ShardingJDBC的分片规则和使用MyBatis编写SQL语句,可以将数据分散到不同的数据库实例和表中,从而实现了水平扩展和负载均衡的效果。这种方式能够帮助我们提高数据库的性能和容量,从而更好地应对大规模的数据存储需求。 ### 回答3: 使用SpringBoot ShardingJDBC MyBatis可以轻松实现分库分表。 首先,ShardingJDBC是一个分库分表的开源框架,它可以通过数据库中间件实现数据的分散存储。而SpringBoot是一个快速构建项目的框架,可以帮助开发者轻松集成各种组件。 使用SpringBoot ShardingJDBC MyBatis进行分库分表,首先需要配置ShardingJDBC的数据源、分片策略以及分表策略。可以通过配置文件或者编程方式来完成配置。配置数据源时,可以指定多个数据库的连接信息,并使用分片策略将数据分配到不同的数据库中。配置分表策略时,可以指定不同的分表规则,将数据根据一定的规则分散存储在不同的表中。 在具体的业务逻辑中,可以使用MyBatis来操作数据库。MyBatis是一个简化数据库访问的持久层框架,通过编写SQL语句和映射文件,可以轻松实现数据库的增删改查操作。 在访问数据库时,ShardingJDBC会根据配置的分片策略和分表策略,自动将数据路由到指定的数据库和表中。开发者不需要关心数据的具体存储位置,只需要使用MyBatis的API进行数据操作即可。 使用SpringBoot ShardingJDBC MyBatis进行分库分表,可以提高数据库的读写性能,增加数据的存储容量,并且可以实现数据的动态扩容和迁移。此外,由于SpringBoot和MyBatis的高度集成,开发者可以更加方便地进行开发和维护。 总之,使用SpringBoot ShardingJDBC MyBatis进行分库分表可以帮助开发者更好地管理数据,提升系统的性能和可扩展性。
评论 23
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

天涯泪小武

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值