进大厂必问的高频java高级面试题的总结

  1,为什么要用redis,redis集群怎么做的,redis如何实现负载均衡,Redis雪崩、穿透、热点key等优化

      用redis目的很简单,快,基于内存的,比读盘速度快出不止一个量级;其次设计上减轻后台压力,而且相比其他nosql产品,支持更多的数据类型,可以持久化,而且在某些场景下可以当队列来使用,非常优秀。Redis的内存结构作为扩展内容自行了解一下。

       负载均衡,一般有几种方案,简单的,通过客户端分片,优势是服务端redis相互无关联,单例服务器,容易线性扩展。客户端采用哈希算法将Redis数据的key进行散列,通过hash函数,特定的key会映射到特定的Redis节点上。

      第二种,官方的redis cluster,通过sharding(分片)进行数据共享,这个方案官方推出时间有限,目前应用并不广泛,可以先了解下。一个node就是一个redis服务器,整个集群有一个slot(插槽)的概念,一共有16383个slot(2^14),需要为每个节点分配负责的slot,在存取数据的时候,redis会用key根据crc16算法算出slot的位置,也就得到了数据存储的节点,然后再存取数据。

       还有一种,类似mycat,利用twemproxy代理中间件实现大规模Redis集群,twemproxy处于客户端和服务器的中间,将客户端发来的请求,进行一定的处理后(如sharding),再转发给后端真正的Redis服务器。

       穿透雪崩等问题,没有标准答案,只有常见方案,对于穿透,要缓存空值(注意设置过期时间和后台更新时对应更新缓存);雪崩,最简单的,设计个策略,确保不会某一刻出现大量失效key。比如起一个线程专门维护,要么延长,要么插入式就均匀分布,看业务,甚至多级缓存都可以。

2,nginx如何实现负载均衡

      nginx的均衡策略就那几种,轮询+权重,ip hash(可实现粘性session,缺点是单点故障后session丢失),url hash,最小连接数等,关键字配置就行,应用哪个,要针对架构看,上面的应该很好理解;

nginx除了上面的均衡策略,还有一些可用性策略的配置,比如(maxfail最大失败数,failtimeout等),热备策略(down,backup);直接看官方文档就行。但生产上,规模稍大的项目一般不会只用nginx来保证高可用,要么配合keepalived,要么配合LVS,具体百度搜一下,资料很多。

       这一块,针对大家就要面试了,只了解一下nginx即可,至少各种配置要说出来说明白,其他的是架构师的事。

3,你们项目中遇到哪些技术难点

给大家个建议,随便抓一个点搞透即可,毕竟简历对象是一年经验,要求多高不可能,哪怕你说你只负责redis策略的开发和维护,说透了,面试十拿九稳,说的太杂反而不好。比如上次问你们并发量多少,说10000,这里完全可以扩展,比如(活动峰值10000左右,日常峰值3000以内,热点时间500-1000,什么是热点时间?自己想;)

然后6个tomcat,怎么均衡不清楚,还是电商网站,除非专门做个秒杀,否则10000并发几乎是不可能的。这里随便说句,tomcat的并发量取决于业务场景和硬件配置。典型场景不同优化和设计可以有上十倍差距。通常情况下单个tomcat非静态资源请求并发也就几百,10000并发你想想什么概念。

5,集合有哪些?底层实现

基础问题不作科普了,我给个例子,这是阿里巴巴的一个面试题,追问式的,hashmap的数据结构是什么?回答上来后继续问:初始容量是多少?回答上来后继续问:我初始传1100会怎么样?回答上来后继续:为什么这样设计?回答上来再问:讲一下它的扩容过程;回答上来后继续问:hashmap在jdk7 8中的区别?回答上来后继续问:为什么要这样?最后继续问:你有没有更好的方案?

另一个,concurrenthashmap的结构是?为什么线程安全?与synchronized修饰的hashmap有什么区别?底层原理是?锁的机制是?

要全回答完是有难度的,答案我不说了留着自己钻研,当思考题。重点是这个思考过程,可以套到任何一个地方。

【百度内容】下面总结了几道关于hashmap的问题。

1、hashmap的主要参数都有哪些?

2、hashmap的数据结构是什么样子的?自己如何实现一个hashmap?

3、hash计算规则是什么?

4、说说hashmap的存取过程?

5、说说hashmap如何处理碰撞的,或者说说它的扩容?

解答:以1.7为例,也会掺杂一些1.8的不同点。

1、

1)桶(capacity)容量,即数组长度:DEFAULT_INITIAL_CAPACITY=1<<4;默认值为16

即在不提供有参构造的时候,声明的hashmap的桶容量;

2)MAXIMUM_CAPACITY = 1 << 30;

极限容量,表示hashmap能承受的最大桶容量为2的30次方,超过这个容量将不再扩容,让hash碰撞起来吧!

3)static final float DEFAULT_LOAD_FACTOR = 0.75f;

负载因子(loadfactor,默认0.75),负载因子有个奇特的效果,表示当当前容量大于(size/)时,将进行hashmap的扩容,扩容一般为扩容为原来的两倍。

4)int threshold;阈值

阈值算法为capacity*loadfactory,大致当map中entry数量大于此阈值时进行扩容(1.8)

5)transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;(默认为空{})

核心的数据结构,即所谓的数组+链表的部分。

2、hashmap的数据结构是什么样子的?自己如何实现一个hashmap?

主要数据结构即为数组+链表。

在hashmap中的主要表现形式为一个table,类型为Entry<K,V>[] table

首先是一个Entry型的数组,Entry为hashmap的内部类:

1 static class Entry<K,V> implements Map.Entry<K,V> {

2         final K key;

3         V value;

4         Entry<K,V> next;

5         int hash;

6 }

在这里可以看到,在Entry类中存在next,所以,它又是链表的形式。

这就是hashmap的主要数据结构。

3、hash的计算规则,这又要看源码了:

1 static final int hash(Object key) {

2         int h;

3         return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);

4     }

这是1.8的源码,1.7太复杂但原理是一致的,简单说这就是个“扰动函数”,最终的目的是让散列分布地更加均匀。

算法就是拿存储key的hashcode值先右移16位,再与hashcode值进行亦或操作,即不求进位只求按位相加的值:盗图:

最后是如何获得,本key在table中的位置呢?本身应该是取得了hash进行磨除取余运算,但是,源码:

1 static int indexFor(int h, int length) {

2         // assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2";

3         return h & (length-1);

4     }

为什么又做了个与运算求得位置呢?简单说,它的意义和取余一致。

不信可以自己算一下。

首先说,他利用了table的长度肯定是2的整数次幂的原理,假设当前length为16,2的4次方

而与&运算,又是只求进位运算,比如1111&110001结果为000001

只求进位运算(&),保证算出的结果一定在table的length之内,最大为1111。

故而,它的运算结果与价值等同于取余运算,并且即使不管hash值有多大都可以算出结果,并且在length之内。

并且,这种类型的运算,能够更加的节约计算机资源,少了加(计算机所有运算都是加运行)运算过程,更加地节省资源。

4、hashmap的存取过程

源码1.7:

1 /**

 2 *往hashmap中放数据

 3 */

 4 public V put(K key, V value) {

 5         if (table == EMPTY_TABLE) {

 6             inflateTable(threshold);//判断如果为空table,先对table进行构造

 7             //构造通过前面的几个参数

 8         }

 9         //首先判断key是否为null,为null也可以存

10         //这里需要记住,null的key一定放在table的0号位置

11         if (key == null)

12             return putForNullKey(value);

13         //算出key的hash值

14         int hash = hash(key);

15         //根据hash值算出在table中的位置

16         int i = indexFor(hash, table.length);

17         //放入K\V,遍历链表,如果位置上存在相同key,进行替换value为新的,且将替换的旧的value返回

18         for (Entry<K,V> e = table[i]; e != null; e = e.next) {

19             Object k;

20             if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {

21                 V oldValue = e.value;

22                 e.value = value;

23                 e.recordAccess(this);

24                 return oldValue;

25             }

26         }

27         modCount++;

28         //增加一个entry,有两种情况,1、如果此位置存在entry,将此位置变为插入的entry,且将插入entry的next节点变为原来的entry;2、如果此位置不存在entry则直接插入新的entry

29         addEntry(hash, key, value, i);

30         return null;

31     }

取数据:

1 //根据key获得一个entry

 2 public V get(Object key) {

 3         //如果key为null,获取0号位的切key为null的值

 4         if (key == null)

 5             return getForNullKey();

 6         //如果不是,获取entry,在下面方法

 7         Entry<K,V> entry = getEntry(key);

 8         //合法性判断

 9         return null == entry ? null : entry.getValue();

10     }

11 //获取一个key不为null的entry

12 final Entry<K,V> getEntry(Object key) {

13         //如果table为null,则返回null

14         if (size == 0) {

15             return null;

16         }

17         //计算hash值

18         int hash = (key == null) ? 0 : hash(key);

19         //根据hash值获得table的下标,遍历链表,寻找key,找到则返回

20         for (Entry<K,V> e = table[indexFor(hash, table.length)];

21              e != null;

22              e = e.next) {

23             Object k;

24             if (e.hash == hash &&

25                 ((k = e.key) == key || (key != null && key.equals(k))))

26                 return e;

27         }

28         return null;

29     }

 5.扩容和碰撞

先说碰撞吧,由于hashmap在存值的时候并不是直接使用的key的hashcode,而是通过扰动函数算出了一个新的hash值,这个计算出的hash值可以明显的减少碰撞。

还有一种解决碰撞的方式就是扩容,扩容其实很好理解,就是将原来桶的容量扩为原来的两倍。这样争取散列的均匀,比如:

原来桶的长度为16,hash值为1和17的entry将会都在桶的0号位上,这样就出现了碰撞,而当桶扩容为原来的2倍时,hash值为1和17的entry分别在1和17号位上,整号岔开了碰撞。

下面说说何时扩容,扩容都做了什么。

1.7中,在put元素的过程中,判断table不为空、切新增的元素的key不与原来的重合之后,进行新增一个entry的逻辑。

复制代码

1 void addEntry(int hash, K key, V value, int bucketIndex) {

2         if ((size >= threshold) && (null != table[bucketIndex])) {

3             resize(2 * table.length);

4             hash = (null != key) ? hash(key) : 0;

5             bucketIndex = indexFor(hash, table.length);

6         }

7         createEntry(hash, key, value, bucketIndex);

8     }

由源代码可知,在新增元素时,会先判断:

1)当前的entry数量是否大于或者等于阈值(loadfactory*capacity);

2)判断当前table的位置是否存在entry。

经上两个条件联合判定,才会进行数组的扩容工作,最后扩容完成才会去创建新的entry。

而扩容的方法即为:resize()看代码

 1 void resize(int newCapacity) {

 2         //拿到原table对象

 3         Entry[] oldTable = table;

 4         //计算原table的桶长度

 5         int oldCapacity = oldTable.length;

 6         //先判定,当前容量是否已经是最大容量了(2的30次方)

 7         if (oldCapacity == MAXIMUM_CAPACITY) {

 8             //假如达到了,将阈值设为int的最大值2的31次方减1,返回

 9             threshold = Integer.MAX_VALUE;

10             return;

11         }

12         //创建新的table对象

13         Entry[] newTable = new Entry[newCapacity];

14         //将旧的table放入新的table中

15         transfer(newTable, initHashSeedAsNeeded(newCapacity));

16         //赋值新table

17         table = newTable;

18         //计算新的阈值

19         threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);

20     }

21 //具体的扩容过程

22 void transfer(Entry[] newTable, boolean rehash) {

23         int newCapacity = newTable.length;

24         //遍历原table,重新散列

25         for (Entry<K,V> e : table) {

26             while(null != e) {

27                 Entry<K,V> next = e.next;

28                 if (rehash) {

29                     e.hash = null == e.key ? 0 : hash(e.key);

30                 }

31                 int i = indexFor(e.hash, newCapacity);

32                 e.next = newTable[i];

33                 newTable[i] = e;

34                 e = next;

35             }

36         }

37     }

【摘录】(5)HashMap的时间复杂度

说了一大堆,还有一个问题没有说,就是HashMap的时间复杂度。

不用说,存数据的时间复杂度是O(1),因为只需要首先根据Key计算哈希值,其实就是数组的下标,找到存放的位置,然后把key-value链表节点链接上去就OK了。

取数据的时间复杂度最好是O(1),最差是O(n),为什么呢?因为如果根据key首先得到哈希地址,发现该下标处位置已经被占用,那么我们要找的节点到底在链表的哪个位置,如果该位置就只有一个节点或者要找的节点就在表头,那么就不用再往后遍历链表了,所以最好情况下时间复杂度是O(1);如果要找到的节点在链表表尾,那么就需要一个一个遍历链表,所以此时最差情况下为O(n)。但是如果我们的链表长度都很短,那么时间效率就会很高,其实即使是O(n)这个级别的复杂度正常也还是可以接受的。所以来总结一下,map的put方法的时间复杂度为O(1),get方法的时间复杂度为O(1)~ O(n)。

那么containKey()方法的时间复杂度呢,其实和get方法的时间复杂度是一样的,也是O(1)~O(n),首先我们也是要根据key计算出对应的哈希地址,如果该地址处没有占用,那么可以直接返回false,说明Map中没有这个key;如果已经被占用,那就开始在链表上遍历Entry,比较key,所以这和刚才的get方法是一样的,从原理上就是一样的,那么时间复杂度自然也是一样的。

containValue()的时间复杂度和containKey()的时间复杂度不是一个级别的,因为他要依赖于key,所以他的时间复杂度为O(n).

6,项目架构风格?

      这个我还在总结,总结完发给你们。

7,多线程?线程池?

基本的,了解一下队列BlockingQueue的几个实现类和线程池那几个参数的含义,synchronized特性(对象锁、类锁)。

最重要的JUC包里几个类的用法。尤其是Lock和concurrenthashmap机制;再深入就了解下CAS的原理,以及juc包底层的AQS实现。网上很多资料。

8,数据库的隔离级别

      百度一下记住概念。

9,数据查询优化?

不谈DBA层面的,那是另一个方向。对开发人员来说,查询优化有两个层面,第一是应用层面,就是后台代码层面的优化,比如是否可以减少查询次数,减少关联表等,合理使用缓存等。第二是sql层面,基本的索引(越多越好吗?),星号不谈;正确使用Explain,主要是看索引,比如:用合适的索引避免全表扫描,例子…where id is null时单键值索引不会生效,可以使用复合B树索引create index indexname tablename(column,0)来使索引生效。

用索引来避免不必要的排序,对经常需要排序的字段,建立索引。

还有函数索引,一个特例,对于like ‘%ABC’,百分号在前面的like查询走不了索引,利用reverse()函数,将字段和参数翻转,….where reverse(col) like reverse(‘%ABC’),再建立个函数索引create index idx on t(reverse(col));

10,悲观锁和乐观锁

      百度一下记住概念和一般的处理方式。

11,分表和分库的策略

这个那天说过,先说目的,并不是是个项目就要分表分库,他的目的为了提高数据量不断攀升情况下的数据库性能,分库一般的route规则有几种:

1,根据字段分区,但可能数据分布不均,

2,hash取模分区,数据均匀但数据迁移不能按照机器性能分摊,

3,单独建关系表,但要多查一次,影响性能。

分表分库原则上能不能就不分,因为一旦分了,会产生多数据源管理问题,跨库事务处理问题,join查询问题;如500万以下的量不分片,优先采用优化sql/索引,读写分离的方式。分片尽量少,分布尽量均匀,分片规则要根据业务特性,如数据增长模式等因素考虑;尽量不要在事务中跨分片,尽量避免select *;

12,索引类型

这块我也理解不深,举几个例子,Oracle索引:

B*Tree,默认索引类型,平衡二叉树结构,适合高基数的列(列唯一值较多),不能用包含or的查询。

bitmap,适合低基数列(大量重复值列(如性别))来用,它以压缩格式来存放,比较节省空间,适合查询较多的场景,不适合插入和修改,非常耗资源,可以用and或or较多时比较有优势。

函数索引,应用于查询语句条件列上包含函数的情况,索引中储存了经过函数计算的索引码值。可以在不修改应用程序的基础上能提高查询效率。

13,设计模式有哪些

      这个我已经给大家讲过了,至少记住常用的几个,另外知道spring框架在设计的时候使用了哪些设计模式。

14,tcp/ ip协议?

这太大了,说几个基本的:OSI七层模型要有个概念,物理层,数据链路层,网络层(IP),传输层,会话层(SSL,RPC),表示层,应用层(HTTP,FTP);

然后记住请求响应基本结构:

http请求包含三部分:请求行(请求方法POST/GET、URI、请求协议版本);请求头(各种参数如cache很多),请求正文(需要传到服务器的数据);

响应也包含三部分:状态行(包含状态码,400语法错误,401请求未经授权,403服务器拒绝服务,503暂时不能处理请求),消息报头(服务器各种状态,传输方式,数据大小,资源修改时间等),响应正文。

然后握手过程,百度搜,配图看,关键要记住握手挥手的目的:

三次握手原因:防止已过期(网络延迟等)的客户端连接请求,服务端收到后若不确认则会直接发送数据,造成网络资源浪费。

四次挥手原因:确保数据能够完成传输。

再深入的看一下滑动窗口概念,还有和UDP区别等。

15,消息队列使用场景

这个应用还是比较广的,最终一致性的分布式事务处理。发布订阅业务。系统业务解耦、通讯等等。

16,有两个链表如何知道这两个链表中哪一个是循环链表

      算法问题,参考快慢指针。

17,java虚拟机包含那些部分

Jvm参考群里发的那个pdf,有空看看,简单说下。

Jvm组件包括类加载器,执行引擎,运行时数据区,本地接口库。

基本运行流程:classFile->classLoader->运行时数据区->执行引擎->本地库接口->本地方法库。

重点是运行时数据区,包含的几个部分比如方法区(永久代),堆,虚拟机栈最重要,其他几个本地方法栈,计数器了解即可。

1,计数器,线程私有,是当前线程所执行字节码行号指示器,不会oom。

2,虚拟机栈,线程私有,生命周期和线程绑定,表示java方法执行内存模型,每个方法执行时创建一个栈帧(stack frame)用来存储局部变量表(基本类型,引用类型(指针),return address类型),操作数栈,动态链接,方法出口等。

局部变量表的大小在进入方法前就确定,会抛出StackOverflowError和oom。-Xss设置虚拟机栈容量

3,本地方法栈,线程私有,作用同虚拟机栈,用来执行native方法,抛出异常同虚拟机栈。

4, Java堆,线程共享,主要用来存放对象实例,gc的主要管理区域,会抛出oom,堆内存分为新生代与老年代,新生代又分为eden区和两个survivor区。

5,方法区,线程共享,主要存储被虚拟机加载的类信息,常量,静态变量,即时编译器编译后的代码等,该区域较少出现gc行为,同样会抛出oom。

a)  运行时常量池,存在于方法区中,用于存放类中的常量和符号引用,特点是在运行期也可以将新常量放到池中(intern()),同样会抛出oom。

-MaxPermSize设置方法区。

6,直接内存,不属于jvm运行时数据区,在NIO场景很常用。

18,垃圾回收算法

JVM中使用的:

1, 标记-清除算法,先标记可回收的对象,再对标记的对象进行回收,问题在于效率不高,空间碎片多。

2,复制算法(JVM年轻代使用):适用于对象存活率很低的情况(新生代),将内存分为两块,一次使用一半,使用完后将存活的对象复制到另一半,再把当前的一半清空,问题在于内存等于一次只能用一半。目前大部分虚拟机对 新生代 的gc采用该算法,(hotspot中,新生代内存分为一个eden,两个survivor,比例为8:1:1,可以使用–XX:SurvivorRatio调整,每次使用eden和一个survivor,另一个用来替换,在回收后,将存活的对象放到另一个survivor,再清空刚才的eden和survivor,如果在移动时该survivor空间不足,会放入老年代区域。。

3, 标记-整理算法(JVM老年代使用),针对对象存活率较高的情况,在标记后将所有存活对象向一端移动,然后清理以外区域。

19,分布式事务解决方案

事务一旦涉及到分布式,复杂度都会上升很大,这里只说基本思路和常见处理方法,不做标准答案。

在业务层面,常见的分布式事务实现方式有2PC二阶段提交,TCC补偿型提交,异步消息,最大努力通知型。

对于有强一致性要求的业务场景,尽量避免分布式而采用本地事务,对于最终一致性,最好采用基于异步消息来解决,如果即要求强一致性又必须分布式,最好采用TCC,其次考虑2PC。TCC原理:try,先完成业务一致性检测并预留业务资源(如锁住客户和商户的账户),confirm,使用try阶段预留的资源执行业务(幂等),如果失败要重试(客户扣款商户加款);cancel,释放try预留的资源(释放客户商户的账户锁);如果任一业务在confirm阶段失败,则要对其他业务进行补偿,如果补偿也失败就要重试,若实在无法成功,则事务管理器要解析log进行人工或其他方式处理。

tcc方式中的事务管理器必须高可用,且使用多数派算法。

在数据库层面,通常采用两种办法:

1,数据分区,采用一致性哈希来访问,即用hash算法来将对应的key哈希到一个具有2^32次方个桶的空间中,即0~(2^32)-1的数字空间中。将这些数字头尾相连,成一个闭合的环形,把数据通过一定的hash算法处理后映射到环上,这种设计无法解决单点故障导致的数据丢失。

2,数据镜像,多个服务器保存相同的数据,提供相同的服务,但这样会让逻辑变得复杂,如跨服务器事务。

这样,在典型的转账案例中,在1方案中,A扣钱成功B加钱失败时,跨机器操作就会比较复杂;在2方案中,如果A并发给BC转钱,如果在不同的机器上,需要保证数据的一致性。所以问题就是如果要保证高可用,就要有数据冗余,有冗余就会出现一致性问题,一致性问题又引起性能问题。

一致性根据业务可以制定不同的强度,如弱一致性,写入后可能读出来也可能读不出来;最终一致性,写入后在某段时间后一定能读出来;强一致性,写入后立刻可以读出来。前两种是异步冗余,后一种是同步冗余;解决以上问题的常用方案有以下几个:

master/slave,读写都由master处理,slave周期性同步master的数据,这样保证了最终一致性,可能出现同步时master挂掉,则该时间片内数据丢失,slave只能readonly等待master启动;如果强调强一致性,则可以采用在master和slave都写成功才返回成功。

master/master,每个master都提供读写服务,数据异步同步,同样是最终一致性,好处是挂一个master不影响业务,但如果多master对同一数据进行修改就非常棘手,数据会出现冲突,一般只能手动处理。

2pc,两阶段提交,强一致性,需要一个中间件来参与协调跨节点的事务处理,第一阶段,中间件首先通知各节点准备事务操作,各节点锁定资源并处理事务逻辑,如果成功返回可以提交,失败则返回拒绝提交。第二阶段,如果中间件收到有一个节点拒绝提交,则通知所有节点回滚操作。如果所有所有节点全部可以提交,则通知各节点进行正式提交操作。

该方式有几个缺点:1,第一步是同步阻塞,影响性能;2,超时问题,节点未收到通知或中间件未收到回应,则中间件要么需要重试要么回滚;3,在二阶段中间件发出可以提交后,某节点未收到该信息,或事物提交/回滚后确认信息未发送到中间件,则中间件要么需要重试要么回滚;4,二阶段中如果节点没收到commit或rollback,节点将不知道该如何处理,只能等待中间件重发命令,整个事物也被锁住。

3pc,三阶段提交,将2pc的第一阶段拆分为两步,先询问,都同意后再锁定资源。

  • 3
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

jason的java世界

不要吝啬你的赞赏哦~~~

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

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

打赏作者

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

抵扣说明:

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

余额充值