面试常见问题
其他一些面试问题
40亿个QQ号,限制1G内存,如何去重?
java和go语言中int类型占用的字节数如下:
- 在java中,int类型占4个字节(32位)。java的int类型按照二进制补码形式存储,范围是-2^31到2^31-1,也就是-2147483648到2147483647。
- 在go语言中,int类型也占4个字节(32位)。go中的int类型按照二进制补码形式存储,范围是-2^31到2^31-1,也就是-2147483648到2147483647,和java的int范围相同。
- go语言还提供了int8、int16、int32和int64这几种具体字节长度的整数类型。但一般使用int类型时,如果没有特别指明,编译器会根据架构默认int为32位。
所以综上,java和go语言中的int类型默认都是32位,占4个字节。
40亿个QQ号,每个QQ号使用int类型存储,则可以计算出总的内存占用:
- 每个QQ号使用int类型,在Java和Go语言中int类型占4个字节
- 共有40亿个QQ号
- 所以总内存占用 = 每个QQ号占用字节 * QQ号总数
- = 4 * 40亿 = 160亿字节
- =约合16GB
所以,如果使用int类型来存储40亿个QQ号,总共需要大约16GB的内存空间。
使用位图的话,一个数字只需要占用1个bit,那么40亿个数字也就是:
4000000000 * 1 /8 /1024/1024 = 476M

把40亿个数字都放到Bitmap之后,所有位置上是1的表示存在,不为1的表示不存在,相同的QQ号只需要设置一次1就可以了,那么,最终就把所有是1的数字遍历出来就行了。
扩展比如用户登录在redis中使用bitmap实现
setbit user_loging_status 10023 1 //设置登录或退出 1/0
"0"
getbit user_loging_status 10023 //查询用户登录状态
"1"
bitcount user_loging_status //统计在线用户数量
"1"
1000瓶药水,1瓶有毒药,最少需要几只小白鼠一定能够找出?
为了找到1000瓶药水中有1瓶有毒的情况,最少需要多少只小白鼠呢?我们可以通过二分查找的思想来推导:
假设需要的最少小白鼠数量为n
-
先取500瓶药水让1只小白鼠喝,如果死了,那么有毒的药水在这500瓶里;如果没死,有毒的在另外500瓶里。
-
接着在上一步确定的500瓶里再取250瓶给1只小白鼠喝,重复这个过程,每次喂养一半数量的药水。
-
当样本数减少到1时,这最后1瓶就是有毒的药水,过程结束。
通过这种二分查找的思路,需要测试的样本量每次减少一半,那么总的测试轮数为 log1000,即约等于10。
因为每轮需要1只小白鼠,所以总共需要10只小白鼠。
所以对于1000瓶药水和1瓶有毒的情况,最少需要10只小白鼠就可以确定哪瓶是有毒的。这利用了二分查找算法的思想,能大幅减少测试次数,提高效率。
Cookie,Session,Token的区别是什么?
Cookie、Session和Token是用于在Web应用程序中管理用户状态和身份验证的技术。因为在Web应用中,HTTP的通信是无状态的,每个请求都是完全独立的,所以服务端无法确认当前访问者的身份信息,无法分辨上一次的请求发送者和这一次的发送者是不是同一个人。
Cookie是由服务器发送给用户浏览器的小型文本文件,存储在客户端的浏览器中。它会在浏览器下次向同一服务器再发起请求时被携带并发送到服务器上。服务器可以读取Cookie并使用其中的信息来进行识别和个性化处理。
每个 cookie 都会绑定单一的域名,无法在别的域名下获取使用,一级域名和二级域名之间是允许共享使用的。cookie 是不可跨域的,并且每个域名下面的Cookie的数量也是有限的。
Session是在服务器端创建和管理的一种会话机制。当用户首次访问网站时,服务器会为该用户创建一个唯一的Session ID,通常通过Cookie在客户端进行存储。会话标识符在后续的请求中用于标识具体是哪个用户。通常情况下,session 是基于 cookie 实现的,session 存储在服务器端,sessionId 会被存储到客户端的cookie 中。
Cookie 和 Session 的区别主要有以下几个
- 存储位置不同: Session 是存储在服务器端的,Cookie 是存储在客户端的。
- 安全性不同:因为Session存储在服务器端,所以Session 比 Cookie 安全。
- 存取值的类型不同:Cookie 只支持存字符串数据,想要设置其他类型的数据,需要将其转换成字符串,Session 可以存任意数据类型。
- 有效期不同: Cookie 可设置为长时间保持,比如我们经常使用的默认登录功能,Session 一般失效时间较短,客户端关闭(默认情况下)或者 Session 超时都会失效。
- 存储大小不同: 单个 Cookie 保存的数据不能超过 4K,Session 可存储数据远高于 Cookie,但是当访问量过多,会占用过多的服务器资源。
有了cookie和session之后,基本可以实现用户的各种验证、鉴权及身份识别了。但是,还是有一些场景中,不是特别适合这两种方案,或者说这两种方案的话还不够,那么就需要token上场了。
Token也是一种用于用户身份鉴权的手段。他其实是一种代表用户身份验证和授权的令牌。在Web应用程序中,常用的身份验证方案是基于令牌的身份验证(Token-based Authentication)。当用户成功登录时,服务器会生成一个Token并将其返回给客户端。客户端在后续的请求中将Token包含在请求头或请求参数中发送给服务器。服务器接收到Token后,会进行验证和解析,以确定用户的身份和权限。Token通常是基于某种加密算法生成的,因此具有一定的安全性。
主要由以下几个场景:
1、跨域请求:Cookie是不支持跨域的,当在不同的域名之间进行通信时,使用Token可以更方便地在跨域请求中传递身份验证信息,而不受Cookie限制。
2、分布式场景:Session是存储在服务器上的,但是随着现在很多都是集群部署,这就使得Session也需要实现分布式Session,而如果能用Token的话,就可以不用这么复杂。
3、API交互:当我们使用浏览器访问后端服务的时候,可以用cookie和session,但是如果是API调用,比如Dubbo交互,就没办法做cookie的存储和传递了,而使用Token是常见的身份验证方式。客户端通过提供Token来证明其身份,并获得对受保护资源的访问权限。
4、跨平台应用程序:Token可以轻松地在不同的平台和设备之间共享和传递,而无需依赖特定的会话机制或Cookie支持。
5、前后端分离项目:现在很多项目都是前后端分离的了,这种项目中,前端和后端之间通过API的方式交互,这种的话用Token也会更加方便一些。
总之,因为Cookie和Session存在各种限制,所以Token也是目前常见的身份验证和状态管理方式,它具有更大的灵活性和适用性,特别适用于现代的应用程序架构和需求。它提供了一种无状态、可扩展和安全的身份验证和授权机制。
而且,有了token之后,还可以基于Token做防重检测。
CPU飙高问题排查过程
登录服务器,执行top命令,查看CPU占用情况:
$top
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
1893 admin 20 0 7127m 2.6g 38m S 181.7 32.6 10:20.26 java
top命令是Linux下常用的性能分析工具,能够实时显示系统中各个进程的资源占用状况,类似于Windows的任务管理器。
通过以上命令,我们可以看到,进程ID为1893的Java进程的CPU占用率达到了181%,基本可以定位到是我们的Java应用导致整个服务器的CPU占用率飙升。
定位线程
我们知道,Java是单进程多线程的,那么,我们接下来看看PID=1893的这个Java进程中的各个线程的CPU使用情况,同样是用top命令:
$top -Hp 1893
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
4519 admin 20 0 7127m 2.6g 38m R 18.6 32.6 0:40.11 java
通过top -Hp 1893命令,我们可以发现,当前1893这个进程中,ID为4519的线程占用CPU最高。
定位代码
通过top命令,我们目前已经定位到导致CPU使用率较高的具体线程, 那么我么接下来就定位下到底是哪一行代码存在问题。
首先,我们需要把4519这个线程转成16进制:
$printf '%x\n' 4519
11a7
接下来,通过jstack命令,查看栈信息:
$sudo -u admin jstack 1893 |grep -A 200 11a7
"HSFBizProcessor-DEFAULT-8-thread-5" #500 daemon prio=10 os_prio=0 tid=0x00007f632314a800 nid=0x11a2 runnable [0x000000005442a000]
java.lang.Thread.State: RUNNABLE
at sun.misc.URLClassPath$Loader.findResource(URLClassPath.java:684)
at sun.misc.URLClassPath.findResource(URLClassPath.java:188)
at java.net.URLClassLoader$2.run(URLClassLoader.java:569)
at java.net.URLClassLoader$2.run(URLClassLoader.java:567)
at java.security.AccessController.doPrivileged(Native Method)
at java.net.URLClassLoader.findResource(URLClassLoader.java:566)
at java.lang.ClassLoader.getResource(ClassLoader.java:1093)
at java.net.URLClassLoader.getResourceAsStream(URLClassLoader.java:232)
at org.hibernate.validator.internal.xml.ValidationXmlParser.getInputStreamForPath(ValidationXmlParser.java:248)
at org.hibernate.validator.internal.xml.ValidationXmlParser.getValidationConfig(ValidationXmlParser.java:191)
at org.hibernate.validator.internal.xml.ValidationXmlParser.parseValidationXml(ValidationXmlParser.java:65)
at org.hibernate.validator.internal.engine.ConfigurationImpl.parseValidationXml(ConfigurationImpl.java:287)
at org.hibernate.validator.internal.engine.ConfigurationImpl.buildValidatorFactory(ConfigurationImpl.java:174)
at javax.validation.Validation.buildDefaultValidatorFactory(Validation.java:111)
at com.test.common.util.BeanValidator.validate(BeanValidator.java:30)
通过以上代码,我们可以清楚的看到,BeanValidator.java的第30行是有可能存在问题的。
接下来就是通过查看代码来解决问题了,我们发现,我们自定义了一个BeanValidator,封装了Hibernate的Validator,然后在validate方法中,通过Validation.buildDefaultValidatorFactory().getValidator()初始化一个Validator实例,通过分析发现这个实例化的过程比较耗时。
我们重构了一下代码,把Validator实例的初始化提到方法外,在类初始化的时候创建一次就解决了问题。
以上,展示了一次比较完成的线上问题定位过程。主要用到的命令有:top 、printf 和 jstack
另外,线上问题排查还可以使用Alibaba开源的工具Arthas进行排查,以上问题,可以使用一下命令定位:
thread -n 3 //查看cpu占比前三的线程
DDD的分层架构是怎么样的?
DDD的分层架构是一个四层架构,从上到下依次是:用户接口层、应用层、领域层和基础层。

层次之间的调用关系是上层可以调用下层,即用户接口层可以调用应用层、领域层及基础层。应用层可以调用领域层和基础层,领域层可以调用基础层。
但是不能从下往上反向调用,各个层级之间是严格的单向调用的依赖关系。
除了这种简单的四层架构以外,DDD中还有比较典型的洋葱架构和六边形架构
洋葱架构,就是像洋葱一样的一层一层,从外到内的架构形式,如下图

他的依赖关系是从外到内的。
六边形架构和洋葱架构有点像,只不过不是圆形,而是六边形的:

虽然 DDD 分层架构、整洁架构、六边形架构的架构模型表现形式不一样,但是这三种架构模型的设计思想都是微服务架构高内聚低耦合原则的完美体现,都是以领域模型为中心的设计思想。

Git如何回滚代码?reset和revert什么区别?
首先,说本地文件修改,如果你只是想撤销本地文件的修改并恢复到上一次提交时的状态,可以使用git checkout命令。例如,如果你想撤销对文件example.txt的修改,可以运行以下命令:git checkout example.txt。
git rollback
如果你的代码本地修改后,使用了git commit或者push提交到了分支上,想要撤销这一次的提交的话,那么可以使用git revert和git reset命令。
例如,如果你想撤销指定的某一次提交,如这次提交的hash值是abcdefg,可以运行以下命令:git revert abcdefg或者 git reset –hard abcdefg。