Spring Boot+MyBatis 动态数据源切换、多数据源,读写分离

本项目使用 Spring Boot 和 MyBatis 实现多数据源,动态数据源的切换;有多种不同的实现方式,在学习的过程中发现没有文章将这些方式和常见的问题集中处理,所以将常用的方式和常见的问题都写在了在本项目的不同分支上:

项目地址: 

https://github.com/helloworlde/SpringBoot-DynamicDataSource

master: 使用了多数据源的 RESTful API 接口,使用 Druid 实现了 DAO 层数据源动态切换和只读数据源负载均衡

dev: 最简单的切面和注解方式实现的动态数据源切换

druid: 通过切面和注解方式实现的使用 Druid 连接池的动态数据源切换

aspect_dao: 通过切面实现的 DAO 层的动态数据源切换

roundrobin: 通过切面使用轮询方式实现的只读数据源负载均衡

hikari: 升级到SpringBoot 2.0, 数据源使用 Hikari

以上分支都是基于 dev 分支修改或扩充而来,基本涵盖了常用的多数据源动态切换的方式,基本的原理都一样,都是通过切面根据不同的条件在执行数据库操作前切换数据源。

在使用的过程中基本踩遍了所有动态数据源切换的坑,将常见的一些坑和解决方法写在了 Issues 里面。

该项目使用了一个可写数据源和多个只读数据源,为了减少数据库压力,使用轮循的方式选择只读数据源;考虑到在一个 Service 中同时会有读和写的操作,所以本应用使用 AOP 切面通过 DAO 层的方法名切换只读数据源;但这种方式要求数据源主从一致,并且应当避免在同一个 Service 方法中写入后立即查询,如果必须在执行写入操作后立即读取,应当在 Service 方法上添加 @Transactional 注解以保证使用主数据源。

需要注意的是,使用 DAO 层切面后不应该在 Service 类层面上加 @Transactional 注解,而应该添加在方法上,这也是 Spring 推荐的做法

动态切换数据源依赖 configuration 包下的4个类来实现,分别是:

DataSourceRoutingDataSource.java

DataSourceConfigurer.java

DynamicDataSourceContextHolder.java

DynamicDataSourceAspect.java

添加依赖

 
  1. dependencies {

  2. compile('org.mybatis.spring.boot:mybatis-spring-boot-starter:1.3.1')

  3. compile('org.springframework.boot:spring-boot-starter-web')

  4. compile('org.springframework.boot:spring-boot-starter-aop')

  5. compile('com.alibaba:druid-spring-boot-starter:1.1.6')

  6. runtime('mysql:mysql-connector-java')

  7. testCompile('org.springframework.boot:spring-boot-starter-test')

  8. }

创建数据库及表

product_master, product_slave_alpha, product_slave_beta, product_slave_gamma

在以上数据库中分别创建表 product,并插入不同数据

 
  1. DROP DATABASE IF EXISTS product_master;

  2. CREATE DATABASE product_master;

  3. CREATE TABLE product_master.product(

  4. id INT PRIMARY KEY AUTO_INCREMENT,

  5. name VARCHAR(50) NOT NULL,

  6. price DOUBLE(10,2) NOT NULL DEFAULT 0);

  7. INSERT INTO product_master.product (name, price) VALUES('master', '1');

  8.  

  9.  

  10. DROP DATABASE IF EXISTS product_slave_alpha;

  11. CREATE DATABASE product_slave_alpha;

  12. CREATE TABLE product_slave_alpha.product(

  13. id INT PRIMARY KEY AUTO_INCREMENT,

  14. name VARCHAR(50) NOT NULL,

  15. price DOUBLE(10,2) NOT NULL DEFAULT 0);

  16. INSERT INTO product_slave_alpha.product (name, price) VALUES('slaveAlpha', '1');

  17.  

  18. DROP DATABASE IF EXISTS product_slave_beta;

  19. CREATE DATABASE product_slave_beta;

  20. CREATE TABLE product_slave_beta.product(

  21. id INT PRIMARY KEY AUTO_INCREMENT,

  22. name VARCHAR(50) NOT NULL,

  23. price DOUBLE(10,2) NOT NULL DEFAULT 0);

  24. INSERT INTO product_slave_beta.product (name, price) VALUES('slaveBeta', '1');

  25.  

  26. DROP DATABASE IF EXISTS product_slave_gamma;

  27. CREATE DATABASE product_slave_gamma;

  28. CREATE TABLE product_slave_gamma.product(

  29. id INT PRIMARY KEY AUTO_INCREMENT,

  30. name VARCHAR(50) NOT NULL,

  31. price DOUBLE(10,2) NOT NULL DEFAULT 0);

  32. INSERT INTO product_slave_gamma.product (name, price) VALUES('slaveGamma', '1');

配置数据源

application.properties

 
  1. ### Master datasource config

  2. spring.datasource.druid.master.name=master

  3. spring.datasource.druid.master.driver-class-name=com.mysql.jdbc.Driver

  4. spring.datasource.druid.master.url=jdbc:mysql://localhost/product_master?useSSL=false

  5. spring.datasource.druid.master.port=3306

  6. spring.datasource.druid.master.username=root

  7. spring.datasource.druid.master.password=123456

  8.  

  9. # SlaveAlpha datasource config

  10. spring.datasource.druid.slave-alpha.name=SlaveAlpha

  11. spring.datasource.druid.slave-alpha.driver-class-name=com.mysql.jdbc.Driver

  12. spring.datasource.druid.slave-alpha.url=jdbc:mysql://localhost/product_slave_alpha?useSSL=false

  13. spring.datasource.druid.slave-alpha.port=3306

  14. spring.datasource.druid.slave-alpha.username=root

  15. spring.datasource.druid.slave-alpha.password=123456

  16.  

  17. # SlaveBeta datasource config

  18. spring.datasource.druid.slave-beta.name=SlaveBeta

  19. spring.datasource.druid.slave-beta.driver-class-name=com.mysql.jdbc.Driver

  20. spring.datasource.druid.slave-beta.url=jdbc:mysql://localhost/product_slave_beta?useSSL=false

  21. spring.datasource.druid.slave-beta.port=3306

  22. spring.datasource.druid.slave-beta.username=root

  23. spring.datasource.druid.slave-beta.password=123456

  24.  

  25. # SlaveGamma datasource config

  26. spring.datasource.druid.slave-gamma.name=SlaveGamma

  27. spring.datasource.druid.slave-gamma.driver-class-name=com.mysql.jdbc.Driver

  28. spring.datasource.druid.slave-gamma.url=jdbc:mysql://localhost/product_slave_gamma?useSSL=false

  29. spring.datasource.druid.slave-gamma.port=3306

  30. spring.datasource.druid.slave-gamma.username=root

  31. spring.datasource.druid.slave-gamma.password=123456

  32.  

  33. # Druid dataSource config

  34. spring.datasource.type=com.alibaba.druid.pool.DruidDataSource

  35. spring.datasource.druid.initial-size=5

  36. spring.datasource.druid.max-active=20

  37. spring.datasource.druid.min-idle=5

  38. spring.datasource.druid.max-wait=60000

  39. spring.datasource.druid.pool-prepared-statements=false

  40. spring.datasource.druid.validation-query=SELECT 1

  41. spring.datasource.druid.validation-query-timeout=30000

  42. spring.datasource.druid.test-on-borrow=false

  43. spring.datasource.druid.test-on-return=false

  44. spring.datasource.druid.test-while-idle=true

  45. #spring.datasource.druid.time-between-eviction-runs-millis=

  46. #spring.datasource.druid.min-evictable-idle-time-millis=

  47. #spring.datasource.druid.max-evictable-idle-time-millis=10000

  48.  

  49. # Druid stat filter config

  50. spring.datasource.druid.filters=stat,wall,log4j

  51. spring.datasource.druid.web-stat-filter.enabled=true

  52. spring.datasource.druid.web-stat-filter.url-pattern=/*

  53. spring.datasource.druid.web-stat-filter.exclusions=*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*

  54. spring.datasource.druid.web-stat-filter.session-stat-enable=true

  55. spring.datasource.druid.web-stat-filter.session-stat-max-count=10

  56. spring.datasource.druid.web-stat-filter.principal-session-name=user

  57. #spring.datasource.druid.web-stat-filter.principal-cookie-name=

  58. spring.datasource.druid.web-stat-filter.profile-enable=true

  59. spring.datasource.druid.filter.stat.db-type=mysql

  60. spring.datasource.druid.filter.stat.log-slow-sql=true

  61. spring.datasource.druid.filter.stat.slow-sql-millis=1000

  62. spring.datasource.druid.filter.stat.merge-sql=true

  63. spring.datasource.druid.filter.wall.enabled=true

  64. spring.datasource.druid.filter.wall.config.delete-allow=true

  65. spring.datasource.druid.filter.wall.config.drop-table-allow=false

  66. spring.datasource.druid.filter.slf4j.enabled=true

  67. # Druid manage page config

  68. spring.datasource.druid.stat-view-servlet.enabled=true

  69. spring.datasource.druid.stat-view-servlet.url-pattern=/druid/*

  70. spring.datasource.druid.stat-view-servlet.reset-enable=true

  71. spring.datasource.druid.stat-view-servlet.login-username=admin

  72. spring.datasource.druid.stat-view-servlet.login-password=admin

  73. #spring.datasource.druid.stat-view-servlet.allow=

  74. #spring.datasource.druid.stat-view-servlet.deny=

  75. spring.datasource.druid.use-global-data-source-stat=true

  76. # Druid AOP config

  77. spring.datasource.druid.aop-patterns=cn.com.hellowood.dynamicdatasource.service.*

  78. spring.aop.proxy-target-class=true

  79.  

  80. # MyBatis config

  81. mybatis.type-aliases-package=cn.com.hellowood.dynamicdatasource.mapper

  82. mybatis.mapper-locations=mappers/**Mapper.xml

  83. server.port=9999

配置数据源

DataSourceKey.java

 
  1. package cn.com.hellowood.dynamicdatasource.common;

  2.  

  3. public enum DataSourceKey {

  4. master,

  5. slaveAlpha,

  6. slaveBeta,

  7. slaveGamma

  8. }

DataSourceRoutingDataSource.java

该类继承自 AbstractRoutingDataSource 类,在访问数据库时会调用该类的 determineCurrentLookupKey() 方法获取数据库实例的 key

 
  1. package cn.com.hellowood.dynamicdatasource.configuration;

  2.  

  3. import org.slf4j.Logger;

  4. import org.slf4j.LoggerFactory;

  5. import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;

  6.  

  7. public class DynamicRoutingDataSource extends AbstractRoutingDataSource {

  8.  

  9. private final Logger logger = LoggerFactory.getLogger(getClass());

  10.  

  11. @Override

  12. protected Object determineCurrentLookupKey() {

  13. logger.info("Current DataSource is [{}]", DynamicDataSourceContextHolder.getDataSourceKey());

  14. return DynamicDataSourceContextHolder.getDataSourceKey();

  15. }

  16. }

DataSourceConfigurer.java

数据源配置类,在该类中生成多个数据源实例并将其注入到 ApplicationContext 中

 
  1. package cn.com.hellowood.dynamicdatasource.configuration;

  2.  

  3. import org.mybatis.spring.SqlSessionFactoryBean;

  4. import org.springframework.boot.autoconfigure.jdbc.DataSourceBuilder;

  5. import org.springframework.boot.context.properties.ConfigurationProperties;

  6. import org.springframework.context.annotation.Bean;

  7. import org.springframework.context.annotation.Configuration;

  8. import org.springframework.context.annotation.Primary;

  9.  

  10. import javax.sql.DataSource;

  11. import java.util.HashMap;

  12. import java.util.Map;

  13.  

  14. @Configuration

  15. public class DataSourceConfigurer {

  16.  

  17.  

  18. @Bean("master")

  19. @Primary

  20. @ConfigurationProperties(prefix = "spring.datasource.druid.master")

  21. public DataSource master() {

  22. return DruidDataSourceBuilder.create().build();

  23. }

  24.  

  25.  

  26. @Bean("slaveAlpha")

  27. @ConfigurationProperties(prefix = "spring.datasource.druid.slave-alpha")

  28. public DataSource slaveAlpha() {

  29. return DruidDataSourceBuilder.create().build();

  30. }

  31.  

  32.  

  33. @Bean("slaveBeta")

  34. @ConfigurationProperties(prefix = "spring.datasource.druid.slave-beta")

  35. public DataSource slaveBeta() {

  36. return DruidDataSourceBuilder.create().build();

  37. }

  38.  

  39.  

  40. @Bean("slaveGamma")

  41. @ConfigurationProperties(prefix = "spring.datasource.druid.slave-gamma")

  42. public DataSource slaveGamma() {

  43. return DruidDataSourceBuilder.create().build();

  44. }

  45.  

  46.  

  47. @Bean("dynamicDataSource")

  48. public DataSource dynamicDataSource() {

  49. DynamicRoutingDataSource dynamicRoutingDataSource = new DynamicRoutingDataSource();

  50. Map<Object, Object> dataSourceMap = new HashMap<>(4);

  51. dataSourceMap.put(DataSourceKey.master.name(), master());

  52. dataSourceMap.put(DataSourceKey.slaveAlpha.name(), slaveAlpha());

  53. dataSourceMap.put(DataSourceKey.slaveBeta.name(), slaveBeta());

  54. dataSourceMap.put(DataSourceKey.slaveGamma.name(), slaveGamma());

  55.  

  56.  

  57. dynamicRoutingDataSource.setDefaultTargetDataSource(master());

  58.  

  59. dynamicRoutingDataSource.setTargetDataSources(dataSourceMap);

  60.  

  61.  

  62. DynamicDataSourceContextHolder.dataSourceKeys.addAll(dataSourceMap.keySet());

  63.  

  64.  

  65. DynamicDataSourceContextHolder.slaveDataSourceKeys.addAll(dataSourceMap.keySet());

  66. DynamicDataSourceContextHolder.slaveDataSourceKeys.remove(DataSourceKey.master.name());

  67. return dynamicRoutingDataSource;

  68. }

  69.  

  70.  

  71. @Bean

  72. @ConfigurationProperties(prefix = "mybatis")

  73. public SqlSessionFactoryBean sqlSessionFactoryBean() {

  74. SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();

  75.  

  76. sqlSessionFactoryBean.setDataSource(dynamicDataSource());

  77. return sqlSessionFactoryBean;

  78. }

  79.  

  80.  

  81. @Bean

  82. public PlatformTransactionManager transactionManager() {

  83. return new DataSourceTransactionManager(dynamicDataSource());

  84. }

  85.  

  86. }

DynamicDataSourceContextHolder.java

该类为数据源上下文配置,用于切换数据源

 
  1. package cn.com.hellowood.dynamicdatasource.configuration;

  2.  

  3.  

  4. import cn.com.hellowood.dynamicdatasource.common.DataSourceKey;

  5. import org.slf4j.Logger;

  6. import org.slf4j.LoggerFactory;

  7.  

  8. import java.util.ArrayList;

  9. import java.util.List;

  10. import java.util.concurrent.locks.Lock;

  11. import java.util.concurrent.locks.ReentrantLock;

  12.  

  13. public class DynamicDataSourceContextHolder {

  14.  

  15. private static final Logger logger = LoggerFactory.getLogger(DynamicDataSourceContextHolder.class);

  16.  

  17.  

  18. private static Lock lock = new ReentrantLock();

  19.  

  20.  

  21. private static int counter = 0;

  22.  

  23.  

  24. private static final ThreadLocal<Object> CONTEXT_HOLDER = ThreadLocal.withInitial(DataSourceKey.master);

  25.  

  26.  

  27.  

  28. public static List<Object> dataSourceKeys = new ArrayList<>();

  29.  

  30.  

  31. public static List<Object> slaveDataSourceKeys = new ArrayList<>();

  32.  

  33.  

  34. public static void setDataSourceKey(String key) {

  35. CONTEXT_HOLDER.set(key);

  36. }

  37.  

  38.  

  39. public static void useMasterDataSource() {

  40. CONTEXT_HOLDER.set(DataSourceKey.master);

  41. }

  42.  

  43.  

  44. public static void useSlaveDataSource() {

  45. lock.lock();

  46.  

  47. try {

  48. int datasourceKeyIndex = counter % slaveDataSourceKeys.size();

  49. CONTEXT_HOLDER.set(String.valueOf(slaveDataSourceKeys.get(datasourceKeyIndex)));

  50. counter++;

  51. } catch (Exception e) {

  52. logger.error("Switch slave datasource failed, error message is {}", e.getMessage());

  53. useMasterDataSource();

  54. e.printStackTrace();

  55. } finally {

  56. lock.unlock();

  57. }

  58. }

  59.  

  60.  

  61. public static String getDataSourceKey() {

  62. return CONTEXT_HOLDER.get();

  63. }

  64.  

  65.  

  66. public static void clearDataSourceKey() {

  67. CONTEXT_HOLDER.remove();

  68. }

  69.  

  70.  

  71. public static boolean containDataSourceKey(String key) {

  72. return dataSourceKeys.contains(key);

  73. }

  74. }

DynamicDataSourceAspect.java

动态数据源切换的切面,切 DAO 层,通过 DAO 层方法名判断使用哪个数据源,实现数据源切换

 
  1. package cn.com.hellowood.dynamicdatasource.configuration;

  2.  

  3. import org.aspectj.lang.JoinPoint;

  4. import org.aspectj.lang.annotation.After;

  5. import org.aspectj.lang.annotation.Aspect;

  6. import org.aspectj.lang.annotation.Before;

  7. import org.aspectj.lang.annotation.Pointcut;

  8. import org.slf4j.Logger;

  9. import org.slf4j.LoggerFactory;

  10. import org.springframework.stereotype.Component;

  11.  

  12. @Aspect

  13. @Component

  14. public class DynamicDataSourceAspect {

  15. private static final Logger logger = LoggerFactory.getLogger(DynamicDataSourceAspect.class);

  16.  

  17. private final String[] QUERY_PREFIX = {"select"};

  18.  

  19. @Pointcut("execution( * cn.com.hellowood.dynamicdatasource.mapper.*.*(..))")

  20. public void daoAspect() {

  21. }

  22.  

  23. @Before("daoAspect()")

  24. public void switchDataSource(JoinPoint point) {

  25. Boolean isQueryMethod = isQueryMethod(point.getSignature().getName());

  26. if (isQueryMethod) {

  27. DynamicDataSourceContextHolder.useSlaveDataSource();

  28. logger.info("Switch DataSource to [{}] in Method [{}]",

  29. DynamicDataSourceContextHolder.getDataSourceKey(), point.getSignature());

  30. }

  31. }

  32.  

  33. @After("daoAspect()")

  34. public void restoreDataSource(JoinPoint point) {

  35. DynamicDataSourceContextHolder.clearDataSourceKey();

  36. logger.info("Restore DataSource to [{}] in Method [{}]",

  37. DynamicDataSourceContextHolder.getDataSourceKey(), point.getSignature());

  38. }

  39.  

  40. private Boolean isQueryMethod(String methodName) {

  41. for (String prefix : QUERY_PREFIX) {

  42. if (methodName.startsWith(prefix)) {

  43. return true;

  44. }

  45. }

  46. return false;

  47. }

  48.  

  49. }

配置 Product REST API 接口

ProductController.java

 
  1. package cn.com.hellowood.dynamicdatasource.controller;

  2.  

  3. import cn.com.hellowood.dynamicdatasource.common.CommonResponse;

  4. import cn.com.hellowood.dynamicdatasource.common.ResponseUtil;

  5. import cn.com.hellowood.dynamicdatasource.modal.Product;

  6. import cn.com.hellowood.dynamicdatasource.service.ProductService;

  7. import cn.com.hellowood.dynamicdatasource.utils.ServiceException;

  8. import org.springframework.beans.factory.annotation.Autowired;

  9. import org.springframework.web.bind.annotation.*;

  10.  

  11. @RestController

  12. @RequestMapping("/product")

  13. public class ProductController {

  14.  

  15. @Autowired

  16. private ProductService productService;

  17.  

  18. @GetMapping("/{id}")

  19. public CommonResponse getProduct(@PathVariable("id") Long productId) throws ServiceException {

  20. return ResponseUtil.generateResponse(productService.select(productId));

  21. }

  22.  

  23. @GetMapping

  24. public CommonResponse getAllProduct() {

  25. return ResponseUtil.generateResponse(productService.getAllProduct());

  26. }

  27.  

  28. @PutMapping("/{id}")

  29. public CommonResponse updateProduct(@PathVariable("id") Long productId, @RequestBody Product newProduct) throws ServiceException {

  30. return ResponseUtil.generateResponse(productService.update(productId, newProduct));

  31. }

  32.  

  33. @DeleteMapping("/{id}")

  34. public CommonResponse deleteProduct(@PathVariable("id") long productId) throws ServiceException {

  35. return ResponseUtil.generateResponse(productService.delete(productId));

  36. }

  37.  

  38. @PostMapping

  39. public CommonResponse addProduct(@RequestBody Product newProduct) throws ServiceException {

  40. return ResponseUtil.generateResponse(productService.add(newProduct));

  41. }

  42. }

ProductService.java

 
  1. package cn.com.hellowood.dynamicdatasource.service;

  2.  

  3. import cn.com.hellowood.dynamicdatasource.mapper.ProductDao;

  4. import cn.com.hellowood.dynamicdatasource.modal.Product;

  5. import cn.com.hellowood.dynamicdatasource.utils.ServiceException;

  6. import org.springframework.beans.factory.annotation.Autowired;

  7. import org.springframework.dao.DataAccessException;

  8. import org.springframework.stereotype.Service;

  9. import org.springframework.transaction.annotation.Transactional;

  10.  

  11. import java.util.List;

  12.  

  13. @Service

  14. public class ProductService {

  15.  

  16. @Autowired

  17. private ProductDao productDao;

  18.  

  19. public Product select(long productId) throws ServiceException {

  20. Product product = productDao.select(productId);

  21. if (product == null) {

  22. throw new ServiceException("Product:" + productId + " not found");

  23. }

  24. return product;

  25. }

  26.  

  27. @Transactional(rollbackFor = DataAccessException.class)

  28. public Product update(long productId, Product newProduct) throws ServiceException {

  29.  

  30. if (productDao.update(newProduct) <= 0) {

  31. throw new ServiceException("Update product:" + productId + "failed");

  32. }

  33. return newProduct;

  34. }

  35.  

  36. @Transactional(rollbackFor = DataAccessException.class)

  37. public boolean add(Product newProduct) throws ServiceException {

  38. Integer num = productDao.insert(newProduct);

  39. if (num <= 0) {

  40. throw new ServiceException("Add product failed");

  41. }

  42. return true;

  43. }

  44.  

  45. @Transactional(rollbackFor = DataAccessException.class)

  46. public boolean delete(long productId) throws ServiceException {

  47. Integer num = productDao.delete(productId);

  48. if (num <= 0) {

  49. throw new ServiceException("Delete product:" + productId + "failed");

  50. }

  51. return true;

  52. }

  53.  

  54. public List<Product> getAllProduct() {

  55. return productDao.getAllProduct();

  56. }

  57. }

ProductDao.java

 
  1. package cn.com.hellowood.dynamicdatasource.mapper;

  2.  

  3. import cn.com.hellowood.dynamicdatasource.modal.Product;

  4. import org.apache.ibatis.annotations.Mapper;

  5. import org.apache.ibatis.annotations.Param;

  6.  

  7. import java.util.List;

  8.  

  9. @Mapper

  10. public interface ProductDao {

  11. Product select(@Param("id") long id);

  12.  

  13. Integer update(Product product);

  14.  

  15. Integer insert(Product product);

  16.  

  17. Integer delete(long productId);

  18.  

  19. List<Product> getAllProduct();

  20. }

ProductMapper.xml

启动项目,此时访问 /product/1 会返回 productmaster 数据库中 product 表中的所有数据,多次访问 /product 会分别返回 productslavealpha、productslavebeta、productslave_gamma 数据库中 product 表中的数据,同时也可以在看到切换数据源的 log,说明动态切换数据源是有效的

注意

在该应用中因为使用了 DAO 层的切面切换数据源,所以 @Transactional 注解不能加在类上,只能用于方法;有 @Trasactional注解的方法无法切换数据源

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值