面试题:java后端

文章目录

1. 不用Https怎么保证被抓包后的安全性?

2. String, StringBuffer, StringBuilder 区别

String是Java中基础且重要的类,并且String也是Immutable类的典型实现,被声明为final class,除了hash这个属性其它属性都声明为final,因为它的不可变性,所以例如拼接字符串时候会产生很多无用的中间对象,如果频繁的进行这样的操作对性能有所影响。
StringBuffer就是为了解决大量拼接字符串时产生很多中间对象问题而提供的一个类,提供append和add方法,可以将字符串添加到已有序列的末尾或指定位置,它的本质是一个线程安全的可修改的字符序列,把所有修改数据的方法都加上synchronized。但是保证了线程安全是需要性能的代价的。
StringBuilder是JDK1.5发布的,它和StringBuffer本质上没什么区别,就是去掉了保证线程安全的那部分,减少了开销。

3. 项目做成集群后Session的保存问题?

4. 粘性Session、Redis共享Session、Tomcat的Session同步优缺点?

5. HTTP中用什么字段规定保存我的这个Cookie?

6. 请求头和响应头有哪些字段?

请求头:
在这里插入图片描述
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,/;q=0.8
表示客户端支持的数据格式,或者说客户端“希望”接受到的内容类型。
Accept-Encoding:gzip, deflate, br
表示客户端所支持的解码(解压缩)格式。
Accept-Language:zh-CN,zh;q=0.9
表示客户端支持的语言格式(不是编码格式),如中文/英文
Accept-Charset:gbk,utf-8;q=0.8
表示客户端支持编码格式
Referer:表示当前请求是从哪个资源发起的

响应头:
在这里插入图片描述
Last-Modified/If-Modified-Since 和 Etag/If-None-Match这两对头字段都是来标记缓存资源的,但是后者的优先级要高于前者。
Cache-Control:no-cache
字段的字面意思为“缓存-控制”,前面我们将了几个字段表面客户端/服务器如何使用缓存机制,而这个字段就是用来控制缓存的。
Content-Length:607
表示接收到的响应报文的总长度为607。
Accept-Ranges:bytes
表示服务器支持http中的Range功能,能够分段请求客户端能够分段请求服务器。

7. Http2.0和1.1的区别?

(1)、什么是HTTP 2.0

  • HTTP/2(超文本传输协议第2版,最初命名为HTTP 2.0),是HTTP协议的的第二个主要版本,使用于万维网。
  • HTTP/2是HTTP协议自1999年HTTP 1.1发布后的首个更新,主要基于SPDY协议(是Google开发的基于TCP的应用层协议,用以最小化网络延迟,提升网络速度,优化用户的网络使用验)。

(2)、与HTTP 1.1相比,主要区别包括

HTTP/2采用二进制格式而非文本格式;
HTTP/2是完全多路复用的,而非有序并阻塞的——只需一个连接即可实现并行;
使用报头压缩,HTTP/2降低了开销;
HTTP/2让服务器可以将响应主动“推送”到客户端缓存中。

8. HTTP和HTTPS的区别,tcp和udp的区别

在这里插入图片描述
OSI七层模型
HTTP和HTTPS是应用层协议,该层协议负责主机间数据传输;
TCP和UDP是传输层协议,该层协议负责网络连接。

HTTP和HTTPS的区别:
在这里插入图片描述
HTTPS = HTTP + SSL/TSL(安全层)

区别:1、https协议需要到ca申请证书,一般免费证书较少,因而需要一定费用。
2、http是超文本传输协议,信息是明文传输,https则是具有安全性的ssl加密传输协议。
3、http和https使用的是完全不同的连接方式,用的端口也不一样,前者是80,后者是443。
4、http的连接很简单,是无状态的;HTTPS协议是由SSL+HTTP协议构建的可进行加密传输、身份认证的网络协议,比http协议安全。

tcp和udp的区别:

1、tcp是面向连接的,udp是面向无连接的,所以tcp需要建立连接和断开连接,udp不需要,因此udp更加的高效;
2、tcp是流协议,udp是数据包协议,所以tcp数据没有大小限制,udp数据包有大小限制;
3、tcp是可靠的协议,udp是不可靠协议,所以tcp会处理数据包重发以及乱序等情况,udp则不会处理,因此容易造成丢包.

9. 为什么要用ES?

10. ES原理?

11. 什么是倒排索引?

12. 数据库新增数据时怎么和ES进行同步?

13. Redis的数据结构和常用场景

15. Json和XML有什么优缺点?

扩展标记语言 (Extensible Markup Language, XML) ,用于标记电子文件使其具有结构性的标记语言,可以用来标记数据、定义数据类型,是一种允许用户对自己的标记语言进行定义的源语言。 XML使用DTD(document type definition)文档类型定义来组织数据;格式统一,跨平台和语言,早已成为业界公认的标准。
XML是标准通用标记语言 (SGML) 的子集,非常适合 Web 传输。XML 提供统一的方法来描述和交换独立于应用程序或供应商的结构化数据。
1.2 JSON定义
JSON(JavaScript Object Notation)一种轻量级的数据交换格式,具有良好的可读和便于快速编写的特性。可在不同平台之间进行数据交换。JSON采用兼容性很高的、完全独立于语言文本格式,同时也具备类似于C语言的习惯(包括C, C++, C#, Java, JavaScript, Perl, Python等)体系的行为。这些特性使JSON成为理想的数据交换语言。
JSON基于JavaScript Programming Language , Standard ECMA-262 3rd Edition - December 1999 的一个子集。
2. XML和JSON优缺点
2.1 XML的优缺点
XML的优点:
  A.格式统一,符合标准;
  B.容易与其他系统进行远程交互,数据共享比较方便。
XML的缺点:
  A.XML文件庞大,文件格式复杂,传输占带宽;
  B.服务器端和客户端都需要花费大量代码来解析XML,导致服务器端和客户端代码变得异常复杂且不易维护;
  C.客户端不同浏览器之间解析XML的方式不一致,需要重复编写很多代码;
  D.服务器端和客户端解析XML花费较多的资源和时间。
2.2 JSON的优缺点
JSON的优点:
  A.数据格式比较简单,易于读写,格式都是压缩的,占用带宽小;
  B.易于解析,客户端JavaScript可以简单的通过eval()进行JSON数据的读取;
  C.支持多种语言,包括ActionScript, C, C#, ColdFusion, Java, JavaScript, Perl, PHP, Python, Ruby等服务器端语言,便于服务器端的解析;
  D.在PHP世界,已经有PHP-JSON和JSON-PHP出现了,偏于PHP序列化后的程序直接调用,PHP服务器端的对象、数组等能直接生成JSON格式,便于客户端的访问提取;
  E.因为JSON格式能直接为服务器端代码使用,大大简化了服务器端和客户端的代码开发量,且完成任务不变,并且易于维护。
JSON的缺点:
  A.没有XML格式这么推广的深入人心和喜用广泛,没有XML那么通用性;
  B.JSON格式目前在Web Service中推广还属于初级阶段。

16. 有了解哪些分布式框架?

17. Java线程池分类?

在这里插入图片描述

18. 说一下线程池的底层实现?

当调用线程池的 execute() 方法时,线程池会做出以下判断:

如果当前运行的线程小于线程池的核心线程数,那么马上创建线程完成这个任务。
如果运行中的线程数大于等于线程池的核心线程数,那么将线程放进任务队列等待。
如果此时任务队列已满,且正在运行的线程数小于最大线程数,立即创建非核心线程执行这个任务。
如果此时任务队列已满,且正在运行的线程数等于最大线程数,则线程池会启动饱和拒绝策略来执行。

当一个任务完成时,它会从任务队列的对头去出下一个任务来执行。
当一个线程空闲超过一定时间时,线程池会判断当前运行线程数是否大于核心线程数,如果大于核心线程数,该线程就会被停掉,直至当前线程数等于核心线程数。

19. 进程和线程的区别

根本区别:进程是操作系统资源分配的基本单位,而线程是任务调度和执行的基本单位
包含关系:没有线程的进程可以看做是单线程的,如果一个进程内有多个线程,则执行过程不是一条线的,而是多条线(线程)共同完成的;线程是进程的一部分,所以线程也被称为轻权进程或者轻量级进程
所处环境:在操作系统中能同时运行多个进程(程序);而在同一个进程(程序)中有多个线程同时执行(通过CPU调度,在每个时间片中只有一个线程执行)

20. 常用线程池,线程池有哪几个参数

一:常用线程池四类,在上面线程池的分类
二:线程池的七大参数:
corePoolSize:线程池中的常驻核心线程数
在创建了线程池后,当有请求任务来之后,就会排池中的线程去执行请求任务,近似理解为今日当值线程。
当线程池中的线程数目达到corePoolSize后,就会把到达的任务放到缓存队列当中。
maximumPoolSize:线程池能够容纳同时执行的最大线程数,此值必须大于等于1
keepAliveTime:多余的空闲线程的存活时间
当空闲时间达到keepAIiveTime值时,多余空闲线程会被销毁直到只剩下corePoolSize个线程为止
unit:keepAIiveTime的单位
workQueue:任务队列,被提交但尚未被执行的任务。
threadFactory: 表示生成线程池中工作线程的线程工厂,用于创建线程一般用默认的即可。
handIer:拒绝策略,表示当队列满了并且工作线程大于等于线程池的最大线程数(maximumPoolSize)

21. 你觉得一个线程池需要有哪几个部分组成?

1、线程池管理器(ThreadPoolManager):用于创建并管理线程池
2、工作线程(WorkThread): 线程池中线程
3、任务接口(Task):每个任务必须实现的接口,以供工作线程调度任务的执行。
4、任务队列:用于存放没有处理的任务。提供一种缓冲机制。

22. 如果让你实现线程池,你会怎么做?

1、定义一个接口TPool,需要添加任务的方法和关闭线程池的方法。
在这里插入图片描述
2、线程池的实现类,实现思路:①定义一个队列,存储任务列表,②定义一个集合存储工作线程,③定义布尔变量running作为线程池的执行状态,false是关闭所有线程,默认是true。④初始化的时候创建指定数量的线程并放入集合中,每个工作线程会从队列中获取任务并执行。⑤每次添加任务只放到队列中,等待线程获取。
在这里插入图片描述
3、做个测试,验证线程池能否使用,在这里插入图片描述

23. 如果让你实现List接口,你会怎么做?

1.ArrayList底层是用数组实现的存储。
特点:查询效率高,增删效率低,线程不安全。我们一般使用它。
2.LinkedList底层用双向链表实现的存储。
特点:查询效率低,增删效率高,线程不安全。
双向链表也叫双链表,是链表的一种,它的每个数据节点中都有两个指针,分别指向前一个节点和后一个节点。 所以,从双向链表中的任意一个节点开始,都可以很方便地找到所有节点。
3.Vector底层用数组实现的List,相关的方法都加了同步检查,因此“线程安全,效率低”。 比如,indexOf方法就增加了synchronized同步标记。

·线程安全时,用Vector。
· 局部变量不存在线程安全问题时,并且查找较多用ArrayList(一般使用它)
·局部变量不存在线程安全问题时,增加或删除元素较多用LinkedList。

24. 怎么用List接口去实现一个队列?

队列实现
java中虽然有Queue接口,单java并没有给出具体的队列实现类,而Java中让LinkedList类实现了Queue接口,所以使用队列的时候,一般采用LinkedList。因为LinkedList是双向链表,可以很方便的实现队列的所有功能。

Queue使用时要尽量避免Collection的add()和remove()方法,而是要使用offer()来加入元素,使用poll()来获取并移出元素。它们的优点是通过返回值可以判断成功与否,add()和remove()方法在失败的时候会抛出异常。 如果要使用前端而不移出该元素,使用element()或者peek()方法。

java中定义队列 一般这样定义: Queue queue = new LinkedList();

栈的实现
栈的实现,有两个方法:一个是用java本身的集合类型Stack类型;另一个是借用LinkedList来间接实现Stack

25. 如果让你用数组实现一个队列,什么时候进行初始化,初始化的容量你觉得多大比较合适?

队列是“先进先出”,有初始化队列、进队列,出队列等操作,题目大意就是要求我们用数组实现队列的这些功能。

初始化队列:给定一个初始大小,创建一个队列。

定义变量 start 和 end,初始化为 0,用来跟踪队列中的数。

定义变量 size,初始化为 0,用来记录队列中的元素数目。

进队列:如果 size 小于队列的大小,将要入队的数放到数组中 start 的位置上,start + 1;否则报错。如果 start 等于 size,start 重新变为 0。

出队列:如果 size 大于 0,数组中 end 位置上的数就是出队的数,end 加 1;否则报错,如果 end 等于 size,end 重新变为 0。

26. 说一说数组动态扩容的思路,怎么防止复杂度动荡?

扩容:

import java.util.Arrays;//在这里需要调用的函数
class Demo{
public static void main(String[] args){
int[] a={1,2,3,4,5,6,7,9};

//第一种方法,建立一个新的数组,通过for循环来进行拷贝扩容
int[] b=new int[a.length*2];//a.length:长度,a数组的长度,即数组中数据的个数
for(int i=0;i<a.length;i++){
b[i]=a[i];
}	
System.out.println(Arrays.toString(b));//这个函数就是将数组b进行遍历并输出
//如果不明白遍历什么意思,建议先学习遍历后再来看此篇文章
//第二种方法,固定的写法,System.Arrays.copy(a,0,b,0,a.length);
int[] c=new int[20];
System.arraycopy(a,0,c,0,a.length);
//a,需要需要复制的内容,第一个0(零):在a中开始复制的内容位置
//c,要复制的载体,这里写c就是将a中需要复制的内容赋值给c
//第二个0(零):在c中开始复制的位置
//a.length:要复制的元素量
System.out.println(Arrays.toString(c));

利用Arrays中的函数进行扩容

//方法三。利用方法函数直接
//原理,利用Arrays中的函数进行扩容
int[] d=Arrays.copyOf(a,22);//此函数的作用就是复制a的值,定义d的长度
//	Arrays.copyOf(a,22);a,需要复制的内容(a数组),22:数组长度
System.out.println(Arrays.toString(d));

27. BIO、NIO、AIO的区别及优缺点?

简答 区别:
1.BIO:同步并阻塞,服务器的实现模式是一个连接一个线程,这样的模式很明显的一个缺陷是:由于客户端连接数与服务器线程数成正比关系,可能造成不必要的线程开销,严重的还将导致服务器内存溢出。当然,这种情况可以通过线程池机制改善,但并不能从本质上消除这个弊端。

2.NIO:在JDK1.4以前,Java的IO模型一直是BIO,但从JDK1.4开始,JDK引入的新的IO模型NIO,它是同步非阻塞的。而服务器的实现模式是多个请求一个线程,即请求会注册到多路复用器Selector上,多路复用器轮询到连接有IO请求时才启动一个线程处理。

3.AIO:JDK1.7发布了NIO2.0,这就是真正意义上的异步非阻塞,服务器的实现模式为多个有效请求一个线程,客户端的IO请求都是由OS先完成再通知服务器应用去启动线程处理(回调)。

应用场景:并发连接数不多时采用BIO,因为它编程和调试都非常简单,但如果涉及到高并发的情况,应选择NIO或AIO,更好的建议是采用成熟的网络通信框架Netty。

28. SQL语句查询非常慢,怎么去排查?

29. 如果你发现用explain发现type字段的值是all,分析一下为什么是全表扫描,没有走索引(索引失效)?

type的连接类型:all,index,range,ref,eq_ref,const。从左到右,它们的效率依次是增强的;
all是全表扫描的,效率低下且耗时,有很大优化空间。

30. 介绍一下你用过的锁?

31. volatile(原理)和synchronized使用场景举例

(1)volatile原理 :
32.
volatile使用场景:

  • 根据经验总结,volatile最适合使用的地方是一个线程写、其它线程读的场合,如果有多个线程并发写操作,仍然需要使用锁或者线程安全的容器或者原子变量来代替。
  • 假如一个线程写、一个线程读,根据前面针对volatile的应用总结,此时可以使用volatile来代替传统的synchronized关键字提升并发访问的性能。
  • Netty中大量使用了volatile来修改成员变量,如果理解了volatile的应用场景,读懂Netty volatile的相关代码还是比较容易的。

(2)synchronized使用场景:
Synchronized是一个同步关键字,在某些多线程场景下,如果不进行同步会导致数据不安全(脏读现象),而Synchronized关键字就是用于代码同步。什么情况下会数据不安全呢,要满足两个条件:一是数据共享(临界资源),二是多线程同时访问并改变该数据。

注意:

  • 使用synchronized修饰非静态方法或者使用synchronized修饰代码块时制定的为实例对象时,同一个类的不同对象拥有自己的锁,因此不会相互阻塞。
  • 使用synchronized修饰类和对象时,由于类对象和实例对象分别拥有自己的监视器锁,因此不会相互阻塞。
  • 使用synchronized修饰实例对象时,如果一个线程正在访问实例对象的一个synchronized方法时,其它线程不仅不能访问该synchronized方法,该对象的其它synchronized方法也不能访问,因为一个对象只有一个监视器锁对象,但是其它线程可以访问该对象的非synchronized方法。
  • 线程A访问实例对象的非static synchronized方法时,线程B也可以同时访问实例对象的static synchronized方法,因为前者获取的是实例对象的监视器锁,而后者获取的是类对象的监视器锁,两者不存在互斥关系。

Synchronized锁的3种使用形式(使用场景):

  • Synchronized修饰普通同步方法:锁对象当前实例对象;
  • Synchronized修饰静态同步方法:锁对象是当前的类Class对象;
  • Synchronized修饰同步代码块:锁对象是Synchronized后面括号里配置的对象,这个对象可以是某个对象(xlock),也可以是某个类(Xlock.class);

33. synchronized和lock的区别,在spring的bean中能否使用lock保证线程安全

(1)区别:
在这里插入图片描述

34. synchronized, Lock, 行锁,表锁等各种锁的区别?如何根据场景选择?

35. synchronized原理

从JVM规范中可以了解到,无论是synchronized修饰方法(实例/静态方法)还是代码块都是基于进入(entry)和退出(exit)monitor对象来实现,但是两种修饰方式在字节码层面实现上有着很大区别。下面我们通过javap -verbose XXX.class命令查看class文件信息来具体分析两者实现上的差异。

  • 同步代码块:使用synchronized修饰代码块会在同步代码块之前加monitorenter指令,同时在代码块正常退出(15行)和异常退出(21行)的地方插入monitorexit指令,从而保证monitorenter和monitorexit的成对执行(保证同步代码块执行结束的同时释放锁资源)。可以把monitorenter看作lock.lock(),monitorexit看作lock.unlock(),
  • 方法(静态和实例):synchronized修饰方法并没有通过插入monitorentry和monitorexit指令来实现,而是在方法表结构中的访问标志(access_flags)设置ACC_SYNCHRONIZED标志来实现。线程在执行方法前先判断access_flags是否标记ACC_SYNCHRONIZED,如果标记则在执行方法前先去获取monitor对象,获取成功则执行方法代码且执行完毕后释放monitor对象,获取失败则表示monitor对象被其他线程获取从而阻塞当前线程。

36. synchronized锁升级过程

synchronized在修饰方法和代码块在字节码上实现方式有很大差异,但是内部实现还是基于对象头的MarkWord来实现的。JDK1.6之前synchronized使用的是重量级锁,JDK1.6之后进行了优化,拥有了无锁->偏向锁->轻量级锁->重量级锁的升级过程,而不是无论什么情况都使用重量级锁。锁只可以升级不可以降级,但是偏向锁可以被重置为无锁状态。
在这里插入图片描述
升级过程:锁升级的优化是针对于不同同步场景进行的优化,无锁是没有线程执行同步方法/代码块时的状态;在不存在锁竞争的时候进入同步方法/代码块则使用偏向锁;存在竞争时升级为轻量级锁,轻量级锁采用的是自旋锁,如果同步方法/代码块执行时间很短的话,采用轻量级锁虽然会占用cpu资源但是相对比使用重量级锁还是更高效的;但是如果同步方法/代码块执行时间很长,那么使用轻量级锁自旋带来的性能消耗就比使用重量级锁更严重,这时候就需要升级为重量级锁

37. mysql 索引类型,隔离级别,事务回滚

(1).mysql索引类型:
索引的数据结构和具体存储引擎的实现有关,在MySQL中使用较多的索引有Hash索引B+树索引等,而我们经常使用的InnoDB存储引擎的默认索引实现为:B+树索引。对于哈希索引来说,底层的数据结构就是哈希表,因此在绝大多数需求为单条记录查询的时候,可以选择哈希索引,查询性能最快;其余大部分场景,建议选择BTree索引。
Hash索引:
hash索引底层就是hash表。我们在mysql中用哈希索引时,主要就是通过Hash算法(常见的Hash算法有直接定址法、平方取中法、折叠法、除数取余法、随机数法),将数据库字段数据转换成定长的Hash值,与这条数据的行指针一并存入Hash表的对应位置;如果发生Hash碰撞(两个不同关键字的Hash值相同),则在对应Hash键下以链表形式存储。
B+树索引:
B+树底层实现是多路平衡查找树。B树索引是Mysql数据库中使用最频繁的索引类型,基本所有存储引擎都支持BTree索引。通常我们说的索引不出意外指的就是(B树)索引(实际是用B+树实现的,因为在查看表索引时,mysql一律打印BTREE,所以简称为B树索引)

(2)mysql隔离级别:

  • 第一级别:读未提交(read uncommitted)
    对方事务还没有提交,我们当前事务可以读取到对方未提交的数据。
    读未提交存在脏读(Dirty Read)现象:表示读到了脏的数。
  • 第二级别:读已提交(read committed)
    对方事务提交之后的数据我方可以读取到。
    这种隔离级别解决了: 脏读现象没有了。
    读已提交存在的问题是:不可重复读。
  • 第三级别:可重复读(repeatable read)
    这种隔离级别解决了:不可重复读问题。
    这种隔离级别存在的问题是:读取到的数据是幻象。
  • 第四级别:序列化读/串行化读(serializable)
    解决了所有问题。
    效率低。需要事务排队。
    (3) 事务回滚
    事务是用户定义的一个数据库操作序列,这些操作要么全做要么全不做,是一个不可分割的工作单位,事务回滚是指将该事务已经完成的对数据库的更新操作撤销,在事务中,每个正确的原子
    操作都会被顺序执行,直到遇到错误的原子操作。回滚的意思其实即使如果之前是插入操作的话,那么会执行删除之前插入的记录,如果是修改操作的话,那么会执行将update之前的记录还原。
    因此,正确的原子操作是真正被执行过的,是物理执行。
    事务是由一条或者多条sql语句组成,在事务的操作中,要么这些sql语句都执行,要么都不执行。

38.mysql聚簇和非聚簇

聚簇索引:将数据存储与索引放到了一块,找到索引也就找到了数据;
非聚簇索引:将数据存储于索引分开结构,索引结构的叶子节点指向了数据的对应行,myisam通过key_buffer把索引先缓存到内存中,当需要访问数据时(通过索引访问数据),在内存中直接搜索索引,然后通过索引找到磁盘相应数据,这也就是为什么索引不在key buffer命中时,速度慢的原因。

38.1 MySQL的锁,悲观锁和乐观锁

(1)mysql的锁:

  • 按照锁的粒度把数据库锁分为行级锁(INNODB引擎)、表级锁(MYISAM引擎)和页级锁(BDB引擎 )。
  • 行级锁 行级锁是Mysql中锁定粒度最细的一种锁,表示只针对当前操作的行进行加锁。行级锁能大大减少数据库操作的冲突。其加锁粒度最小,但加锁的开销也最大。行级锁分为共享锁 和 排他锁。
    特点:开销大,加锁慢;会出现死锁;锁定粒度最小,发生锁冲突的概率最低,并发度也最高。
  • 表级锁 表级锁是MySQL中锁定粒度最大的一种锁,表示对当前操作的整张表加锁,它实现简单,资源消耗较少,被大部分MySQL引擎支持。最常使用的MYISAM与INNODB都支持表级锁定。表级锁定分为表共享读锁(共享锁)与表独占写锁(排他锁)。
    特点:开销小,加锁快;不会出现死锁;锁定粒度大,发出锁冲突的概率最高,并发度最低。
  • 页级锁 页级锁是MySQL中锁定粒度介于行级锁和表级锁中间的一种锁。表级锁速度快,但冲突多,行级冲突少,但速度慢。所以取了折衷的页级,一次锁定相邻的一组记录。
    特点:开销和加锁时间界于表锁和行锁之间;会出现死锁;锁定粒度界于表锁和行锁之间,并发度一般
  • 从锁的类别上来讲,有共享锁和排他锁。
    共享锁: 又叫做读锁。 当用户要进行数据的读取时,对数据加上共享锁。共享锁可以同时加上多个。
    排他锁: 又叫做写锁。 当用户要进行数据的写入时,对数据加上排他锁。排他锁只可以加一个,他和其他的排他锁,共享锁都相斥。

(2) 悲观锁和乐观锁:
悲观锁:假定会发生并发冲突,屏蔽一切可能违反数据完整性的操作。在查询完数据的时候就把事务锁起来,直到提交事务。实现方式:使用数据库中的锁机制
乐观锁:假设不会发生并发冲突,只在提交操作时检查是否违反数据完整性。在修改数据的时候把事务锁起来,通过version的方式来进行锁定。实现方式:乐一般会使用版本号机制或CAS算法实现。
两种锁的使用场景
从上面对两种锁的介绍,我们知道两种锁各有优缺点,不可认为一种好于另一种,像乐观锁适用于写比较少的情况下(多读场景),即冲突真的很少发生的时候,这样可以省去了锁的开销,加大了系统的整个吞吐量。
但如果是多写的情况,一般会经常产生冲突,这就会导致上层应用会不断的进行retry,这样反倒是降低了性能,所以一般多写的场景下用悲观锁就比较合适。

39. TCP三次握手,四次挥手,timewait状态,SYN

tcp连接的三次握手:
1、第一次握手,建立连接时,客户端发送syn包到服务器,并进入SYN_SEND状态,等待服务器确认
2、第二次握手:服务器收到syn包,必须确认客户syn,同时自己也发送一个syn包,此时服务器处于SYC_RECV状态
3、客户端收到服务器发送的syn+ack包,向服务器发送确认包ack,此包发送完毕,客户端与服务器进入ESTABLISHED状态,完成三次握手,客户端和服务器开始传送数据
四次挥手:
第一次挥手:TCP发送一个FIN,用来关闭客户端和服务端的连接
第二次挥手:服务端收到FIN,发回一个ACK确认。
第三次挥手:服务端发送一个FIN到客户端,服务端关闭客户端连接
第四次挥手:客户端发送ack报文确认,并将确认的序号+1,这样关闭完成
timewait状态:
根据TCP协议定义的3次握手断开连接规定,发起socket主动关闭的一方 socket将进入TIME_WAIT状态。TIME_WAIT状态将持续2个MSL(Max Segment Lifetime),在Windows下默认为4分钟,即240秒。TIME_WAIT状态下的socket不能被回收使用. 具体现象是对于一个处理大量短连接的服务器,如果是由服务器主动关闭客户端的连接,将导致服务器端存在大量的处于TIME_WAIT状态的socket, 甚至比处于Established状态下的socket多的多,严重影响服务器的处理能力,甚至耗尽可用的socket,停止服务。
为什么需要TIME_WAIT?TIME_WAIT是TCP协议用以保证被重新分配的socket不会受到之前残留的延迟重发报文影响的机制,是必要的逻辑保证。
SYN:
同步序列编号(Synchronize Sequence Numbers)。是TCP/IP建立连接时使用的握手信号。在客户机和服务器之间建立正常的TCP网络连接时,客户机首先发出一个SYN消息,服务器使用SYN+ACK应答表示接收到了这个消息,最后客户机再以ACK消息响应。这样在客户机和服务器之间才能建立起可靠的TCP连接,数据才可以在客户机和服务器之间传递。

40. Synchronized和ReentrantLock有什么区别?

两者的共同点:
协调多线程对共享对象、变量的访问
可重入,同一线程可以多次获得同一个锁
都保证了可见性和互斥性
两者的不同点:
ReentrantLock显示获得、释放锁,synchronized隐式获得释放锁
ReentrantLock可响应中断、可轮回,synchronized是不可以响应中断的,为处理锁的不可用性提供了更高的灵活性
ReentrantLock是API级别的,synchronized是JVM级别的
ReentrantLock可以实现公平锁
ReentrantLock通过Condition可以绑定多个条件
底层实现不一样, synchronized是同步阻塞,使用的是悲观并发策略,lock是同步非阻塞,采用的是乐观并发策略

虽然ReentrantLock可以提供比synchronized更高级的功能,但是仍不能替换synchronized
为什么呢?
《java并发编程实战》上说是因为如果使用reentrantlock时,你没有释放锁,很难追踪到最初发生错误的位置,因为没有记录应该释放锁的位置和时间。

41. 为什么Synchronized没有被优化之前性能差?

早期 synchronized 中的传统锁有哪些不足点,这里的传统锁就是经常听到的重量锁。重量锁性能差,竞争重量锁的基本情况,我们可以知道实现的关键在于monitor。而monitor是依赖于底层的操作系统的Mutex Lock来实现的,而操作系统实现线程之间的切换时需要从用户态转换到核心态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高,这也是为什么早期的synchronized效率低的原因。

42. hashmap原理?

“你用过HashMap吗?” “什么是HashMap?你为什么用到它?”

几乎每个人都会回答“是的”,然后回答HashMap的一些特性,譬如HashMap可以接受null键值和值,而Hashtable则不能;HashMap是非synchronized;HashMap很快;以及HashMap储存的是键值对等等。这显示出你已经用过HashMap,而且对它相当的熟悉。但是面试官来个急转直下,从此刻开始问出一些刁钻的问题,关于HashMap的更多基础的细节。面试官可能会问出下面的问题:

“你知道HashMap的工作原理吗?” “你知道HashMap的get()方法的工作原理吗?”

你也许会回答“我没有详查标准的Java API,你可以看看Java源代码或者Open JDK。”“我可以用Google找到答案。”

但一些面试者可能可以给出答案,“HashMap是基于hashing的原理,我们使用put(key, value)存储对象到HashMap中,使用get(key)从HashMap中获取对象。当我们给put()方法传递键和值时,我们先对键调用hashCode()方法,返回的hashCode用于找到bucket位置来储存Entry对象。”这里关键点在于指出,HashMap是在bucket中储存键对象和值对象,作为Map.Entry。这一点有助于理解获取对象的逻辑。如果你没有意识到这一点,或者错误的认为仅仅只在bucket中存储值的话,你将不会回答如何从HashMap中获取对象的逻辑。这个答案相当的正确,也显示出面试者确实知道hashing以及HashMap的工作原理。但是这仅仅是故事的开始,当面试官加入一些Java程序员每天要碰到的实际场景的时候,错误的答案频现。下个问题可能是关于HashMap中的碰撞探测(collision detection)以及碰撞的解决方法:

“当两个对象的hashcode相同会发生什么?” 从这里开始,真正的困惑开始了,一些面试者会回答因为hashcode相同,所以两个对象是相等的,HashMap将会抛出异常,或者不会存储它们。然后面试官可能会提醒他们有equals()和hashCode()两个方法,并告诉他们两个对象就算hashcode相同,但是它们可能并不相等。一些面试者可能就此放弃,而另外一些还能继续挺进,他们回答“因为hashcode相同,所以它们的bucket位置相同,‘碰撞’会发生。因为HashMap使用链表存储对象,这个Entry(包含有键值对的Map.Entry对象)会存储在链表中。”这个答案非常的合理,虽然有很多种处理碰撞的方法,这种方法是最简单的,也正是HashMap的处理方法。但故事还没有完结,面试官会继续问:

“如果两个键的hashcode相同,你如何获取值对象?” 面试者会回答:当我们调用get()方法,HashMap会使用键对象的hashcode找到bucket位置,然后获取值对象。面试官提醒他如果有两个值对象储存在同一个bucket,他给出答案:将会遍历链表直到找到值对象。面试官会问因为你并没有值对象去比较,你是如何确定确定找到值对象的?除非面试者直到HashMap在链表中存储的是键值对,否则他们不可能回答出这一题。

其中一些记得这个重要知识点的面试者会说,找到bucket位置之后,会调用keys.equals()方法去找到链表中正确的节点,最终找到要找的值对象。完美的答案!

许多情况下,面试者会在这个环节中出错,因为他们混淆了hashCode()和equals()方法。因为在此之前hashCode()屡屡出现,而equals()方法仅仅在获取值对象的时候才出现。一些优秀的开发者会指出使用不可变的、声明作final的对象,并且采用合适的equals()和hashCode()方法的话,将会减少碰撞的发生,提高效率。不可变性使得能够缓存不同键的hashcode,这将提高整个获取对象的速度,使用String,Interger这样的wrapper类作为键是非常好的选择。

如果你认为到这里已经完结了,那么听到下面这个问题的时候,你会大吃一惊。“如果HashMap的大小超过了负载因子(load factor)定义的容量,怎么办?”除非你真正知道HashMap的工作原理,否则你将回答不出这道题。默认的负载因子大小为0.75,也就是说,当一个map填满了75%的bucket时候,和其它集合类(如ArrayList等)一样,将会创建原来HashMap大小的两倍的bucket数组,来重新调整map的大小,并将原来的对象放入新的bucket数组中。这个过程叫作rehashing,因为它调用hash方法找到新的bucket位置。

如果你能够回答这道问题,下面的问题来了:“你了解重新调整HashMap大小存在什么问题吗?”你可能回答不上来,这时面试官会提醒你当多线程的情况下,可能产生条件竞争(race condition)。

当重新调整HashMap大小的时候,确实存在条件竞争,因为如果两个线程都发现HashMap需要重新调整大小了,它们会同时试着调整大小。在调整大小的过程中,存储在链表中的元素的次序会反过来,因为移动到新的bucket位置的时候,HashMap并不会将元素放在链表的尾部,而是放在头部,这是为了避免尾部遍历(tail traversing)。如果条件竞争发生了,那么就死循环了。这个时候,你可以质问面试官,为什么这么奇怪,要在多线程的环境下使用HashMap呢?:)

43. 红黑树、b+,以及红黑树性质,为什么非黑即红?

在这里插入图片描述
B+ 树是一种树数据结构,是一个n叉树,每个节点通常有多个孩子,一颗B+树包含根节点、内部节点和叶子节点。B+ 树通常用于数据库和操作系统的文件系统中。 B+ 树的特点是能够保持数据稳定有序,其插入与修改拥有较稳定的对数时间复杂度。 B+ 树元素自底向上插入。

44. spring源码,生命周期

在这里插入图片描述

45. 类加载机制

在代码编译后,就会生成JVM(Java虚拟机)能够识别的二进制字节流文件(*.class)。而JVM把Class文件中的类描述数据从文件加载到内存,并对数据进行校验、转换解析、初始化,使这些数据最终成为可以被JVM直接使用的Java类型,这个说来简单但实际复杂的过程叫做JVM的类加载机制
一、类的加载
我们平常说的加载大多不是指的类加载机制,只是类加载机制中的第一步加载。

1、通过一个类的全限定名(包名与类名)来获取定义此类的二进制字节流(Class文件)。而获取的方式,可以通过jar包、war包、网络中获取、JSP文件生成等方式。
2、将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。这里只是转化了数据结构,并未合并数据。(方法区就是用来存放已被加载的类信息,常量,静态变量,编译后的代码的运行时内存区域)
3、在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。这个Class对象并没有规定是在Java堆内存中,它比较特殊,虽为对象,但存放在方法区中。

二、类的连接
类的加载过程后生成了类的java.lang.Class对象,接着会进入连接阶段,连接阶段负责将类的二进制数据合并入JRE(Java运行时环境)中。类的连接大致分三个阶段。

1、验证:验证被加载后的类是否有正确的结构,类数据是否会符合虚拟机的要求,确保不会危害虚拟机安全。
2、准备:为类的静态变量(static filed)在方法区分配内存,并赋默认初值(0值或null值)。如static int a = 100;
静态变量a就会在准备阶段被赋默认值0。
对于一般的成员变量是在类实例化时候,随对象一起分配在堆内存中。
另外,静态常量(static final filed)会在准备阶段赋程序设定的初值,如static final int a = 666; 静态常量a就会在准备阶段被直接赋值为666,对于静态变量,这个操作是在初始化阶段进行的。
3、解析:将类的二进制数据中的符号引用换为直接引用。

三、类的初始化
类初始化是类加载的最后一步,除了加载阶段,用户可以通过自定义的类加载器参与,其他阶段都完全由虚拟机主导和控制。到了初始化阶段才真正执行Java代码。
类的初始化的主要工作是为静态变量赋程序设定的初值。
如static int a = 100;在准备阶段,a被赋默认值0,在初始化阶段就会被赋值为100

46. jvm内存模型

首先要说一下JVM内存空间分为五部分,分别是:方法区、堆、Java虚拟机栈、本地方法栈、程序计数器

方法区主要用来存放类信息、类的静态变量、常量、运行时常量池等,方法区的大小是可以动态扩展的,

堆主要存放的是数组、类的实例对象、字符串常量池等。
Java虚拟机栈是描述JAVA方法运行过程的内存模型,Java虚拟机栈会为每一个即将执行的方法创建一个叫做“栈帧”的区域,该区域用来存储该方法运行时需要的一些信息,包括:局部变量表、操作数栈、动态链接、方法返回地址等。比如我们方法执行过程中需要创建变量时,就会将局部变量插入到局部变量表中,局部变量的运算、传递等在操作数栈中进行,当方法执行结束后,这个方法对应的栈帧将出栈,并释放内存空间。栈中会发生的两种异常,StackOverFlowError和OutOfMemoryError,StackOverFlowError表示当前线程申请的栈超过了事先定好的栈的最大深度,但内存空间可能还有很多。 而OutOfMemoryError是指当线程申请栈时发现栈已经满了,而且内存也全都用光了。

本地方法栈结构上和Java虚拟机栈一样,只不过Java虚拟机栈是运行Java方法的区域,而本地方法栈是运行本地方法的内存模型。运行本地方法时也会创建栈帧,同样栈帧里也有局部变量表、操作数栈、动态链接和方法返回地址等,在本地方法执行结束后栈帧也会出栈并释放内存资源,也会发生OutOfMemoryError。

最后是程序计数器,程序计数器是一个比较小的内存空间,用来记录当前线程正在执行的那一条字节码指令的地址。如果当前线程正在执行的是本地方法,那么此时程序计数器为空。程序计数器有两个作用,1、字节码解释器通过改变程序计数器来一次读取指令,从而实现代码的流程控制,比如我们常见的顺序、循环、选择、异常处理等。2、在多线程的情况下,程序计数器用来记录当前线程执行的位置,当线程切换回来的时候仍然可以知道该线程上次执行到了哪里。而且程序计数器是唯一一个不会出现OutOfMeroryError的内存区域。

方法区和堆都是线程共享的,在JVM启动时创建,在JVM停止时销毁,而Java虚拟机栈、本地方法栈、程序计数器是线程私有的,随线程的创建而创建,随线程的结束而死亡

47. AOP实现的方式?

Spring提供了4种实现AOP的方式:
1.经典的基于代理的AOP
2.@AspectJ注解驱动的切面
3.纯POJO切面
4.注入式AspectJ切面

48. JDK动态代理和Cglib代理的区别、底层是怎么实现的、哪个性能更好?

1、JDK动态代理具体实现原理:
通过实现InvocationHandlet接口创建自己的调用处理器;
通过为Proxy类指定ClassLoader对象和一组interface来创建动态代理;
通过反射机制获取动态代理类的构造函数,其唯一参数类型就是调用处理器接口类型;
通过构造函数创建动态代理类实例,构造时调用处理器对象作为参数参入;
JDK动态代理是面向接口的代理模式,如果被代理目标没有接口那么Spring也无能为力,Spring通过Java的反射机制生产被代理接口的新的匿名实现类,重写了其中AOP的增强方法。
2、CGLib动态代理:
CGLib是一个强大、高性能的Code生产类库,可以实现运行期动态扩展java类,Spring在运行期间通过 CGlib继承要被动态代理的类,重写父类的方法,实现AOP面向切面编程呢。
3、两者对比:
JDK动态代理是面向接口的。
CGLib动态代理是通过字节码底层继承要代理类来实现(如果被代理类被final关键字所修饰,那么抱歉会失败)
1、如果目标对象实现了接口,默认情况下会采用JDK的动态代理实现AOP
2、如果目标对象实现了接口,可以强制使用CGLIB实现AOP
3、如果目标对象没有实现了接口,必须采用CGLIB库,spring会自动在JDK动态代理和CGLIB之间转换

50. 前后端怎么测试数据的?

1、前端请求数据URL由谁来写
在开发中,URL主要是由后台来写好给前端。
若后台在查询数据,需要借助查询条件才能查询到前端需要的数据时,这时后台会要求前端提供相关的查询参数(即URL请求的参数)。
2、接口文档主要由谁来写
接口文档主要由后台设计和修改。
后台直接跟数据打交道,最清楚数据库里有什么数据,能返回什么数据。
前端只是数据的被动接受者,只是接口文档的使用者。
使用过程中,发现返回的数据部队,则跟后台商量,由后台修改。
切记:前端不能随意更改接口文档,除非取得后台同意。
3、前端与后台交互的数据格式
主要是JSON,XML现在用的不多
JSON 通常用于与服务端交换数据。
在接收服务器数据时一般是字符串。
我们可以使用 JSON.parse() 方法将数据转换为 JavaScript 对象。
4、前端与后台的交互原理
关注点:接口地址、前端请求的参数、后端返回的参数。
调一下接口,看一下返回的数据。
5、前端请求参数的形式
GET和POST两种方式
GET从指定的服务器中获取数据,POST提交数据给指定的服务器处理
6、前端应该告知后台那些有效信息,后台才能返回前端想要的数据
先将要展示的页面内容进行模块划分,将模块的内容提取出来,以及方便前端的一些标志值等,将所有想要的内容和逻辑告知后端
后端从数据库里面去查询相应的数据表以获得相应的内容或者图片地址信息
URL中的参数主要是根据后台需要,若后台需要一个参数作为查询的辅助条件,前端在URL数据请求时就传递参数
7、前端如何把页面信息有效传达给后台,以及后台如何获取到这些数据
所有前端请求的URL后面的参数都是辅助后台数据查询的
若不需要参数,那后台就会直接给个URL给前端
8、前端应该如何回拒一些本不属于自己做的一些功能需求或任务
前端负责把数据展示在页面上
清晰的认识自己需要做的需求和任务
9、当前端在调用数据接口时,发现有些数据不是我们想要的,那么前端应该怎么办
把请求的URL和返回的数据以及在页面的展示的情况给后台看【后台查询数据、取数据、封装数据方面等蛮难处理的】
10、为什么需要在请求的时候传入参数
后台在查询数据库的时候需要条件查询

51. 什么是MyBits三剑客?

MyBatis Generator,MyBatis Pager,Mybatis Plugin
1.通过generator产生与数据库对应的mapper文件和java pojo,里面包含了各种常用的sql操作在pom.xml中:
2.通过pager进行页面分页
原理是spring对sql操作进行切面配置,计算count,做相应的分页操作在pom.xml中
3.通过mybatis plugin提高开发效率

52. 什么是跨域问题?

在浏览器端进行 Ajax 请求时会出现跨域问题,那么什么是跨域,如何解决跨域呢?先看浏览器端出现跨域问题的现象。
跨域,指的是浏览器不能执行其他网站的脚本。它是由浏览器的同源策略造成的,是浏览器对 JavaScript 施加的安全限制。

53. 前后端怎么解决跨域请求问题?

方案一、使用Ajax的jsonp来解决(只能使用get请求)
方案二、使用JQurey的jsonp插件(对于get、post请求不做要求,但是从后台发来的消息依旧是jsonp格式的数据)插件去官网下载即可
方案三、使用cors

54. 说一说你了解的垃圾收集器?

55. 为什么默认使用G1垃圾收集器?

56. 垃圾回收的时机?

57. 对象的四种引用的区别?

强引用(StrongReference)软引用(SoftReference)弱引用(WeakReference)虚引用(PhantomReference),引用强度由强到弱。通过提供这四种引用类型的主要目的:1、通过代码的方式决定某些对象的声明周期 2、有利于JVM进行垃圾回收。
⑴强引用(StrongReference)
强引用是使用最普遍的引用。如果一个对象具有强引用,那垃圾回收器绝不会回收它。当内存空间不足,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足的问题。 ps:强引用其实也就是我们平时A a = new A()这个意思。

⑵软引用(SoftReference)
如果一个对象只具有软引用,则内存空间足够,垃圾回收器就不会回收它;如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。软引用可用来实现内存敏感的高速缓存(下文给出示例)。
软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收器回收,Java虚拟机就会把这个软引用加入到与之关联的引用队列中。

⑶弱引用(WeakReference)
弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。
弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java虚拟机就会把这个弱引用加入到与之关联的引用队列中。

⑷虚引用(PhantomReference)
“虚引用”顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。
虚引用主要用来跟踪对象被垃圾回收器回收的活动。虚引用与软引用和弱引用的一个区别在于:虚引用必须和引用队列 (ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之 关联的引用队列中。
ReferenceQueue queue = new ReferenceQueue ();

58. 虚引用的使用场景?

可以用来跟踪对象被垃圾回收的活动。一般可以通过虚引用达到回收一些非java内的一些资源比如堆外内存的行为。例如:在 DirectByteBuffer 中,会创建一个 PhantomReference 的子类Cleaner的虚引用实例用来引用该 DirectByteBuffer 实例,Cleaner 创建时会添加一个 Runnable 实例,当被引用的 DirectByteBuffer 对象不可达被垃圾回收时,将会执行 Cleaner 实例内部的 Runnable 实例的 run 方法,用来回收堆外资源。

59. 订单生成的过程?

60. 估算一下,一张表字段有多少字节?

MySQL Server最多只允许4096个字段● InnoDB 最多只能有1000个字段● 字段长度加起来如果超过65535,MySQL server层就会拒绝创建表● 字段长度加起来(根据溢出页指针来计算字段长度,大于40的,溢出,只算40个字节)如果超过8126,InnoDB拒绝创建表

61. 一张表数据量大的时候怎么办?

1、索引优化和SQL语句优化是必须的,避免模糊查询和非索引查询,删改操作根据聚集索引进行,删改操作太频繁的话还是需要考虑分表
2、看需求,如果需求不限制,那就分表
分区会增加管理复杂度和成本这个很难理解,分区增加不了多少工作,如果需求要求必须单表,分区是解决在千万到几亿数据量的比较合适的方法

62. 表拆分有什么纬度?

63. 基于什么业务进行水平拆分和竖直拆分?

1. 水平分区:
保持数据表结构不变,通过某种策略存储数据分片。这样每一片数据分散到不同的表或者库中,达到了分布式的目的。 水平拆分可以支撑非常大的数据量。

水平拆分是指数据表行的拆分,表的行数超过200万行时,就会变慢,这时可以把一张的表的数据拆成多张表来存放。举个例子:我们可以将用户信息表拆分成多个用户信息表,这样就可以避免单一表数据量过大对性能造成影响。

适用场景
1、表中的数据本身就有独立性,例如表中分表记录各个地区的数据或者不同时期的数据,特别是有些数据常用,有些不常用。
2、需要把数据存放在多个介质上。

2.垂直分区:

根据数据库里面数据表的相关性进行拆分。 例如,用户表中既有用户的登录信息又有用户的基本信息,可以将用户表拆分成两个单独的表,甚至放到单独的库做分库。

简单来说垂直拆分是指数据表列的拆分,把一张列比较多的表拆分为多张表

适用场景
1、如果一个表中某些列常用,另外一些列不常用
2、可以使数据行变小,一个数据页能存储更多数据,查询时减少I/O次数

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值