MyBatis数据源和数据库连接池源码解析

常见的数据源组件都实现了javax.sql.DataSource接口,MyBatis自身实现的数据源实现也不例外。MyBatis提供了两个javax.sql.DataSource接口实现,分别是PooledDataSource和UnpooledDataSource。MyBatis使用不同的DataSourceFactory接口实现创建不同类型的DataSource,如下图所示:

在这里插入图片描述

1、DataSourceFactory

DataSourceFactory是JndiDataSourceFactory和UnpooledDataSourceFactory两个工厂类的顶层接口,只定义了两个方法,如下所示:

public interface DataSourceFactory {

  //设置DataSource的相关属性,一般在初始化完成后进行设置
  void setProperties(Properties props);

  //获取DataSource对象
  DataSource getDataSource();

}

2、UnpooledDataSourceFactory

在UnpooledDataSourceFactory的构造函数中会直接创建UnpooledDataSource对象。setProperties()方法会完成对UnpooledDataSource对象的配置

public class UnpooledDataSourceFactory implements DataSourceFactory {

  private static final String DRIVER_PROPERTY_PREFIX = "driver.";
  private static final int DRIVER_PROPERTY_PREFIX_LENGTH = DRIVER_PROPERTY_PREFIX.length();
	
  //对应的数据源,即UnpooledDataSource
  protected DataSource dataSource;

  public UnpooledDataSourceFactory() {
    this.dataSource = new UnpooledDataSource();
  }

  @Override
  public void setProperties(Properties properties) {
    Properties driverProperties = new Properties();
    //创建DataSource相应的MetaObject
    MetaObject metaDataSource = SystemMetaObject.forObject(dataSource);
    //遍历properties集合,该集合中配置了数据源所需要的信息
    for (Object key : properties.keySet()) {
      String propertyName = (String) key;
      if (propertyName.startsWith(DRIVER_PROPERTY_PREFIX)) {
        String value = properties.getProperty(propertyName);
        //以driver.开头的配置项是对DataSource的配置,记录到driverProperties中保存
        driverProperties.setProperty(propertyName.substring(DRIVER_PROPERTY_PREFIX_LENGTH), value);
      } else if (metaDataSource.hasSetter(propertyName)) {
        String value = (String) properties.get(propertyName);
        //根据属性类型进行类型转换,主要是Integer、Long、Boolean三种类型的转换
        Object convertedValue = convertValue(metaDataSource, propertyName, value);
        //设置DataSource的相关属性值
        metaDataSource.setValue(propertyName, convertedValue);
      } else {
        throw new DataSourceException("Unknown DataSource property: " + propertyName);
      }
    }
    if (driverProperties.size() > 0) {
      //设置DataSource.driverProperties属性值
      metaDataSource.setValue("driverProperties", driverProperties);
    }
  }

  //返回数据源
  @Override
  public DataSource getDataSource() {
    return dataSource;
  }

  //类型转换
  private Object convertValue(MetaObject metaDataSource, String propertyName, String value) {
    Object convertedValue = value;
    Class<?> targetType = metaDataSource.getSetterType(propertyName);
    if (targetType == Integer.class || targetType == int.class) {
      convertedValue = Integer.valueOf(value);
    } else if (targetType == Long.class || targetType == long.class) {
      convertedValue = Long.valueOf(value);
    } else if (targetType == Boolean.class || targetType == boolean.class) {
      convertedValue = Boolean.valueOf(value);
    }
    return convertedValue;
  }

}

3、PooledDataSourceFactory

PooledDataSourceFactory继承了UnpooledDataSourceFactory,但并没有覆盖setProperties()getDataSource()方法。两者唯一的区别是PooledDataSourceFactory的构造函数会将其dataSource字段初始化为PooledDataSource对象

public class PooledDataSourceFactory extends UnpooledDataSourceFactory {

  public PooledDataSourceFactory() {
    this.dataSource = new PooledDataSource();
  }

}

4、不使用连接池的UnpooledDataSource

UnpooledDataSource实现了javax.sql.DataSource接口中定义的getConnection()方法,用于获取数据库连接。每次通过UnpooledDataSource.getConnection()方法获取数据库连接时都会创建一个新连接

public class UnpooledDataSource implements DataSource {

  //加载Driver类的类加载器
  private ClassLoader driverClassLoader;
  //数据库连接驱动的相关配置
  private Properties driverProperties;
  //缓存所有已注册的数据库连接驱动
  private static Map<String, Driver> registeredDrivers = new ConcurrentHashMap<>();

  private String driver;
  private String url;
  private String username;
  private String password;

  //是否自动提交
  private Boolean autoCommit;
  //事务隔离级别
  private Integer defaultTransactionIsolationLevel;
  private Integer defaultNetworkTimeout;

  //静态代码块,初始化的时候,从DriverManager中获取所有已注册的驱动信息,并缓存到该类的registeredDrivers中
  static {
    Enumeration<Driver> drivers = DriverManager.getDrivers();
    while (drivers.hasMoreElements()) {
      Driver driver = drivers.nextElement();
      registeredDrivers.put(driver.getClass().getName(), driver);
    }
  }

  public UnpooledDataSource() {
  }

  public UnpooledDataSource(String driver, String url, String username, String password) {
    this.driver = driver;
    this.url = url;
    this.username = username;
    this.password = password;
  }

接下来看下获取连接的方法:

public class PooledDataSource implements DataSource {
  
	@Override
  public Connection getConnection() throws SQLException {
    return doGetConnection(username, password);
  }

  @Override
  public Connection getConnection(String username, String password) throws SQLException {
    return doGetConnection(username, password);
  }
  
  private Connection doGetConnection(String username, String password) throws SQLException {
    Properties props = new Properties();
    if (driverProperties != null) {
      props.putAll(driverProperties);
    }
    if (username != null) {
      props.setProperty("user", username);
    }
    if (password != null) {
      props.setProperty("password", password);
    }
    return doGetConnection(props);
  }
  
  //根据properties获取一个新的数据库连接
  private Connection doGetConnection(Properties properties) throws SQLException {
    //初始化数据库驱动
    initializeDriver();
    //通过DriverManager获取一个数据库连接
    Connection connection = DriverManager.getConnection(url, properties);
    //配置数据连接是否自动提交和事务隔离级别
    configureConnection(connection);
    return connection;
  }

  //初始化数据库驱动
  private synchronized void initializeDriver() throws SQLException {
    //如果当前驱动还没有注册,则进行注册
    if (!registeredDrivers.containsKey(driver)) {
      Class<?> driverType;
      try {
        if (driverClassLoader != null) {
          driverType = Class.forName(driver, true, driverClassLoader);
        } else {
          driverType = Resources.classForName(driver);
        }
        //创建驱动
        Driver driverInstance = (Driver) driverType.getDeclaredConstructor().newInstance();
        //向JDBC的DriverManager注册驱动
        DriverManager.registerDriver(new DriverProxy(driverInstance));
        //向本类的registeredDrivers注册驱动
        registeredDrivers.put(driver, driverInstance);
      } catch (Exception e) {
        throw new SQLException("Error setting driver on UnpooledDataSource. Cause: " + e);
      }
    }
  }

  //配置数据连接是否自动提交和事务隔离级别
  private void configureConnection(Connection conn) throws SQLException {
    if (defaultNetworkTimeout != null) {
      conn.setNetworkTimeout(Executors.newSingleThreadExecutor(), defaultNetworkTimeout);
    }
    if (autoCommit != null && autoCommit != conn.getAutoCommit()) {
      conn.setAutoCommit(autoCommit);
    }
    if (defaultTransactionIsolationLevel != null) {
      conn.setTransactionIsolation(defaultTransactionIsolationLevel);
    }
  }  

以上代码就是UnpooledDataSource类的主要实现逻辑,每次获取连接都是从数据库新创建一个连接进行返回,又因为数据库连接的创建是一个耗时的操作,且数据库连接是非常珍贵的资源,如果每次获取连接都创建一个,则可能会造成系统的瓶颈,拖垮响应速度等,这时就需要数据库连接池了,Mybatis也提供了自己数据库连接池的实现,就是PooledDataSource类

5、使用连接池的PooledDataSource

PooledDataSource实现了简易数据库连接池的功能,PooledDataSource新建数据库连接是使用UnpooledDataSource来实现的

在这里插入图片描述

1)、PooledConnection

UnpooledDataSource并不会管理java.sql.Connection对象,而是管理org.apache.ibatis.datasource.pooled.PooledConnection对象,在PooledConnection中封装了真正的数据库连接对象和其代理对象,这里的代理对象是通过JDK动态代理产生的。PooledConnection继承了InvocationHandler接口

class PooledConnection implements InvocationHandler {
  
  //记录当前PooledConnection对象所在的PooledDataSource对象,当调用close()方法时会将PooledConnection放回该PooledDataSource中
  private final PooledDataSource dataSource;
  
  //真正的数据库连接
  private final Connection realConnection;
  
  //数据库连接的代理对象
  private final Connection proxyConnection;
  
  //从连接池中取出该连接的时间戳
  private long checkoutTimestamp;
  
  //该连接创建的时间戳
  private long createdTimestamp;
  
  //最后一次被使用的时间戳
  private long lastUsedTimestamp;
  
  //由数据库URL、用户名和密码计算出来的hash值,可用于标识该连接所在的连接池
  private int connectionTypeCode;
  
  //检测当前PooledConnection是否有效,主要是为了防止程序通过close()方法将连接归还给连接池之后,依然通过该连接操作数据库
  private boolean valid;
  
  @Override
  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    String methodName = method.getName();
    //如果调用close()方法,则将其重新放入连接池,而不是真正关闭数据库连接
    if (CLOSE.equals(methodName)) {
      dataSource.pushConnection(this);
      return null;
    }
    try {
      if (!Object.class.equals(method.getDeclaringClass())) {
        //通过valid字段检测连接是否有效
        checkConnection();
      }
      //调用真正数据库连接对象的对应方法
      return method.invoke(realConnection, args);
    } catch (Throwable t) {
      throw ExceptionUtil.unwrapThrowable(t);
    }

  }  
2)、PoolState

PoolState是用于管理PooledConnection对象状态的组件,它通过两个List<PooledConnection>集合分别管理空闲状态的连接和活跃状态的连接:

public class PoolState {

  protected PooledDataSource dataSource;

  //空闲的PooledConnection集合
  protected final List<PooledConnection> idleConnections = new ArrayList<>();
  
  //活跃的PooledConnection集合
  protected final List<PooledConnection> activeConnections = new ArrayList<>();
  
  //请求数据库连接的次数
  protected long requestCount = 0;
  
  //获取连接的累积时间
  protected long accumulatedRequestTime = 0;
  
  //checkoutTime表示应用从连接池中取出连接,到归还连接这段时长,accumulatedCheckoutTime记录了所有连接累积的checkoutTime时长
  protected long accumulatedCheckoutTime = 0;
  
  //当连接长时间未归还给连接池时,会被认为该连接超时,claimedOverdueConnectionCount记录了超时的连接个数
  protected long claimedOverdueConnectionCount = 0;
  
  //累积超时时间
  protected long accumulatedCheckoutTimeOfOverdueConnections = 0;
  
  //累积等待时间
  protected long accumulatedWaitTime = 0;
  
  //等待次数
  protected long hadToWaitCount = 0;
  
  //无效的连接数
  protected long badConnectionCount = 0;
3)、PooledDataSource

PooledDataSource新建数据库连接是使用UnpooledDataSource来实现的,PooledConnection用来管理连接池中的连接,PoolState是用于管理连接池的状态

PooledDataSource中核心属性如下:

public class PooledDataSource implements DataSource {
  
  //连接池的状态
  private final PoolState state = new PoolState(this);

  //用来创建真正的数据库连接对象
  private final UnpooledDataSource dataSource;

  //最大活跃的连接数,默认为10 
  protected int poolMaximumActiveConnections = 10;
  
  //最大空闲连接数,默认为5
  protected int poolMaximumIdleConnections = 5;
  
  //最大获取连接的时长
  protected int poolMaximumCheckoutTime = 20000;
  
  //在无法获取到连接时,最大等待的时间
  protected int poolTimeToWait = 20000;

  //在检测一个连接是否可用时,会向数据库发送一个测试SQL 
  protected String poolPingQuery = "NO PING QUERY SET";
  
  //是否允许发送测试SQL
  protected boolean poolPingEnabled;
  
  //当连接超过poolPingConnectionsNotUsedFor毫秒未使用时,会发送一次测试SQL语句,测试连接是否正常
  protected int poolPingConnectionsNotUsedFor;

  //标志着当前的连接池,是url+username+password的hash值
  private int expectedConnectionTypeCode;  

PooledDataSource主要有以下几个方法:获取数据库连接的方法popConnection(),把连接放回连接池的方法pushConnection(),检测数据库连接是否有效的方法pingConnection(),还有关闭连接池中所有连接的方法forceCloseAll()

1)popConnection()

在这里插入图片描述

PooledDataSource中getConnection()方法首先会调用popConnection()方法获PooledConnection对象,然后通过PooledConnection.getProxyConnection()方法获取数据库连接的代理对象

public class PooledDataSource implements DataSource {
  
  //获取连接
	@Override
  public Connection getConnection() throws SQLException {
    return popConnection(dataSource.getUsername(), dataSource.getPassword()).getProxyConnection();
  }  

  //从连接池中获取连接
	private PooledConnection popConnection(String username, String password) throws SQLException {
    //等待的个数
    boolean countedWait = false;
    //PooledConnection对象
    PooledConnection conn = null;
    long t = System.currentTimeMillis();
    //无效的连接个数
    int localBadConnectionCount = 0;

    while (conn == null) {
      synchronized (state) {
        //检测是否还有空闲的连接
        if (!state.idleConnections.isEmpty()) {
          //连接池中还有空闲的连接,则直接获取连接返回
          conn = state.idleConnections.remove(0);
          if (log.isDebugEnabled()) {
            log.debug("Checked out connection " + conn.getRealHashCode() + " from pool.");
          }
        } else {
          //连接池中已经没有空闲连接了
          if (state.activeConnections.size() < poolMaximumActiveConnections) {
            //活跃的连接数没有达到最大值,则创建一个新的数据库连接
            conn = new PooledConnection(dataSource.getConnection(), this);
            if (log.isDebugEnabled()) {
              log.debug("Created connection " + conn.getRealHashCode() + ".");
            }
          } else {
            //如果活跃的连接数已经达到允许的最大值了,则不能创建新的数据库连接
            //获取最先创建的那个活跃的连接
            PooledConnection oldestActiveConnection = state.activeConnections.get(0);
            long longestCheckoutTime = oldestActiveConnection.getCheckoutTime();
            //检测该连接是否超时
            if (longestCheckoutTime > poolMaximumCheckoutTime) {
              //如果该连接超时,则进行相应的统计
              state.claimedOverdueConnectionCount++;
              state.accumulatedCheckoutTimeOfOverdueConnections += longestCheckoutTime;
              state.accumulatedCheckoutTime += longestCheckoutTime;
              //将超时连接移出activeConnections集合
              state.activeConnections.remove(oldestActiveConnection);
              if (!oldestActiveConnection.getRealConnection().getAutoCommit()) {
                try {
                  //如果超时未提交,则自动回滚
                  oldestActiveConnection.getRealConnection().rollback();
                } catch (SQLException e) {
                  log.debug("Bad connection. Could not roll back");
                }
              }
              //创建新的PooledConnection对象,但是真正的数据库连接并没有创建
              conn = new PooledConnection(oldestActiveConnection.getRealConnection(), this);
              conn.setCreatedTimestamp(oldestActiveConnection.getCreatedTimestamp());
              conn.setLastUsedTimestamp(oldestActiveConnection.getLastUsedTimestamp());
              //设置该超时的连接为无效
              oldestActiveConnection.invalidate();
              if (log.isDebugEnabled()) {
                log.debug("Claimed overdue connection " + conn.getRealHashCode() + ".");
              }
            } else {
              //如果无空闲连接,无法创建新的连接且无超时连接,则只能阻塞等待
              try {
                if (!countedWait) {
                  //等待次数
                  state.hadToWaitCount++;
                  countedWait = true;
                }
                if (log.isDebugEnabled()) {
                  log.debug("Waiting as long as " + poolTimeToWait + " milliseconds for connection.");
                }
                long wt = System.currentTimeMillis();
                //阻塞等待
                state.wait(poolTimeToWait);
                state.accumulatedWaitTime += System.currentTimeMillis() - wt;
              } catch (InterruptedException e) {
                break;
              }
            }
          }
        }
        //已经获取到连接
        if (conn != null) {
          if (conn.isValid()) {
            //如果连接有效,事务未提交则回滚
            if (!conn.getRealConnection().getAutoCommit()) {
              conn.getRealConnection().rollback();
            }
            conn.setConnectionTypeCode(assembleConnectionTypeCode(dataSource.getUrl(), username, password));
            conn.setCheckoutTimestamp(System.currentTimeMillis());
            conn.setLastUsedTimestamp(System.currentTimeMillis());
            //把连接加入到活跃集合中去
            state.activeConnections.add(conn);
            state.requestCount++;
            state.accumulatedRequestTime += System.currentTimeMillis() - t;
          } else {
            if (log.isDebugEnabled()) {
              log.debug("A bad connection (" + conn.getRealHashCode() + ") was returned from the pool, getting another connection.");
            }
            state.badConnectionCount++;
            localBadConnectionCount++;
            conn = null;
            if (localBadConnectionCount > (poolMaximumIdleConnections + poolMaximumLocalBadConnectionTolerance)) {
              if (log.isDebugEnabled()) {
                log.debug("PooledDataSource: Could not get a good connection to the database.");
              }
              throw new SQLException("PooledDataSource: Could not get a good connection to the database.");
            }
          }
        }
      }

    }

    if (conn == null) {
      if (log.isDebugEnabled()) {
        log.debug("PooledDataSource: Unknown severe error condition.  The connection pool returned a null connection.");
      }
      throw new SQLException("PooledDataSource: Unknown severe error condition.  The connection pool returned a null connection.");
    }

    return conn;
  }
2)pushConnection()

通过分析PooledConnection的invoke()方法我们知道,当调用连接的代理对象的close()方法时,并未关闭真正的数据连接,而是调用 PooledDataSource的pushConnection()方法将PooledConnection对象归还给连接池,供之后重用

在这里插入图片描述

public class PooledDataSource implements DataSource {
  
  protected void pushConnection(PooledConnection conn) throws SQLException {

    synchronized (state) {
      //从活跃的集合中移除掉该连接
      state.activeConnections.remove(conn);
      //检测连接是否有效
      if (conn.isValid()) {
        //如果空闲连接数没有达到最大值,且PooledConnection为该连接池的连接
        if (state.idleConnections.size() < poolMaximumIdleConnections && conn.getConnectionTypeCode() == expectedConnectionTypeCode) {
          //累计checkout时长
          state.accumulatedCheckoutTime += conn.getCheckoutTime();
          //事务回滚
          if (!conn.getRealConnection().getAutoCommit()) {
            conn.getRealConnection().rollback();
          }
          //为返还的连接创建新的PooledConnection对象
          PooledConnection newConn = new PooledConnection(conn.getRealConnection(), this);
          //把该连接添加的空闲链表中
          state.idleConnections.add(newConn);
          newConn.setCreatedTimestamp(conn.getCreatedTimestamp());
          newConn.setLastUsedTimestamp(conn.getLastUsedTimestamp());
          //设置该连接为无效状态
          conn.invalidate();
          if (log.isDebugEnabled()) {
            log.debug("Returned connection " + newConn.getRealHashCode() + " to pool.");
          }
          //唤醒阻塞等待的线程
          state.notifyAll();
        } else {
          //如果空闲连接数已经达到最大值
          state.accumulatedCheckoutTime += conn.getCheckoutTime();
          if (!conn.getRealConnection().getAutoCommit()) {
            conn.getRealConnection().rollback();
          }
          //则关闭真正的数据库连接
          conn.getRealConnection().close();
          if (log.isDebugEnabled()) {
            log.debug("Closed connection " + conn.getRealHashCode() + ".");
          }
          //设置该连接为无效状态
          conn.invalidate();
        }
      } else {
        if (log.isDebugEnabled()) {
          log.debug("A bad connection (" + conn.getRealHashCode() + ") attempted to return to the pool, discarding connection.");
        }
        //无效连接个数加1
        state.badConnectionCount++;
      }
    }
  }
3)pingConnection()

在上面两个方法中,都调用了isValid()方法来检测连接是否可用,该方法除了检测valid字段外,还会调用pingConnection()方法来尝试让数据库执行测试SQL语句,从而检测真正的数据库连接对象是否依然正常可用

class PooledConnection implements InvocationHandler {
  
	public boolean isValid() {
    return valid && realConnection != null && dataSource.pingConnection(this);
  }
public class PooledDataSource implements DataSource {
  
  protected boolean pingConnection(PooledConnection conn) {
    boolean result = true;

    try {
      //检测真正的数据库连接是否已经关闭
      result = !conn.getRealConnection().isClosed();
    } catch (SQLException e) {
      if (log.isDebugEnabled()) {
        log.debug("Connection " + conn.getRealHashCode() + " is BAD: " + e.getMessage());
      }
      result = false;
    }

    //如果真正的数据库连接还没关闭,需要执行测试SQL语句,长时间(poolPingConnectionsNotUsedFor指定的时长)未使用的连接,才需要ping操作来检测连接是否正常
    if (result && poolPingEnabled && poolPingConnectionsNotUsedFor >= 0
        && conn.getTimeElapsedSinceLastUse() > poolPingConnectionsNotUsedFor) {
      try {
        if (log.isDebugEnabled()) {
          log.debug("Testing connection " + conn.getRealHashCode() + " ...");
        }
        //发送测试SQL语句执行
        Connection realConn = conn.getRealConnection();
        try (Statement statement = realConn.createStatement()) {
          statement.executeQuery(poolPingQuery).close();
        }
        if (!realConn.getAutoCommit()) {
          realConn.rollback();
        }
        result = true;
        if (log.isDebugEnabled()) {
          log.debug("Connection " + conn.getRealHashCode() + " is GOOD!");
        }
      } catch (Exception e) {
        log.warn("Execution of ping query '" + poolPingQuery + "' failed: " + e.getMessage());
        try {
          conn.getRealConnection().close();
        } catch (Exception e2) {
          // ignore
        }
        result = false;
        if (log.isDebugEnabled()) {
          log.debug("Connection " + conn.getRealHashCode() + " is BAD: " + e.getMessage());
        }
      }
    }
    return result;
  }
4)forceCloseAll()

当修改PooledDataSource相应的字段,如数据库的URL、用户名或密码等,需要将连接池中连接全部关闭,之后获取连接的时候从重新初始化。关闭连接池中全部连接的方法为forceCloseAll()

public class PooledDataSource implements DataSource {
  
  public void forceCloseAll() {
    synchronized (state) {
      expectedConnectionTypeCode = assembleConnectionTypeCode(dataSource.getUrl(), dataSource.getUsername(), dataSource.getPassword());
      //处理活跃的连接
      for (int i = state.activeConnections.size(); i > 0; i--) {
        try {
          PooledConnection conn = state.activeConnections.remove(i - 1);
          //设置连接为无效状态
          conn.invalidate();
					//获取数据库真正的连接
          Connection realConn = conn.getRealConnection();
          //事务回滚
          if (!realConn.getAutoCommit()) {
            realConn.rollback();
          }
          //关闭数据库连接
          realConn.close();
        } catch (Exception e) {
          // ignore
        }
      }
      //处理空闲的连接
      for (int i = state.idleConnections.size(); i > 0; i--) {
        try {
          PooledConnection conn = state.idleConnections.remove(i - 1);
          //设置为无效状态
          conn.invalidate();

          Connection realConn = conn.getRealConnection();
          if (!realConn.getAutoCommit()) {
            realConn.rollback();
          }
          realConn.close();
        } catch (Exception e) {
          // ignore
        }
      }
    }
    if (log.isDebugEnabled()) {
      log.debug("PooledDataSource forcefully closed/removed all connections.");
    }
  }  

参考:

《MyBatis技术内幕》

https://my.oschina.net/mengyuankan/blog/2664784

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

邋遢的流浪剑客

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

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

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

打赏作者

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

抵扣说明:

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

余额充值