万字长文!详细解析字节跳动Java后端社招面试题(3年经验)

755 阅读39分钟

哈喽。我又来为大家更新面经资料了。今天继续为大家更新字节跳动社招的面经资料。 (我发现我特别喜欢整理字节的。。。)

这篇文章主要是分享给有面试需求的朋友们。我整理了多家互联网大厂的多年面试题,多位大佬的面经资料,以及Java学习资料。有需要的朋友可以点击进入获取。暗号:掘金。

面试题

面试时长2H。应用+原理理解;Java后端社招+3年经验

1、自我介绍

2、JVM组成部分

(1)堆

(2)方法区(元数据区)

(3)程序计数器

(4)本地方法栈

(5)虚拟机栈

3、虚拟机栈和本地方法栈区别(线程私有,每个线程都有一个)

(1)本地方法栈和虚拟机栈的作用相同,用来描述方法执行的内存模型

(2)即每个方法执行都会在栈上创建一个栈帧,存放方法的出入口,局部变量表和操作数等信息.当一个方法开始与执行完毕对应的栈帧就会入栈与出栈;

4、可以用堆代替栈嘛(区别)

(1)栈是描述该线程当前执行中方法的内存模型,堆存放程序运行时创建的对象;

(2)堆生命周期是和程序一致,线程共享,栈生命周期和执行线程一致,且只用于当前线程,所以内存大小比堆小得多;

(3)栈内存地址要求连续,统一分配好.并且栈后入先出,方法执行完毕栈帧弹出内存及时回收。保证栈的读取速度;

(4)堆内存地址不一定连续,并且内存回收由GC回收非实时回收;

(5)栈内存放入堆中由于大部分线程执行周期短会造成大量垃圾对象或者长期对象也会有内存泄露的问题,一直跟随垃圾回收进行迁移;

(6)所以堆和栈的内存管理方式不一样,用堆代替栈会增加堆内存回收的复杂性,分开更容易管理各自内存

(7)两者内存作用不同,栈中溢出会提示栈溢出错误,堆中溢出会提示内存溢出错误,如果放在一起则无法明确定位错误

分开的意义在于两个内存释放时机要求不同导致的内存管理方式不一样,如果放到一起会增加现有垃圾回收复杂度.并且堆栈内存的存取速率以及隔离性要求不一致,使用堆来存放也会增加堆内存管理复杂度以及降低线程执行效率。

5、JVM为啥要有垃圾回收器

JAVA语言未提供手动释放对象资源的方式,内存空间释放由JVM虚拟机自己判断与执行,所以提供垃圾回收器帮我们进行内存资源释放.这样也避免了我们申请完内存后如果忘了释放内存而造成的内存泄漏。

6、 如何定位垃圾,哪些是ROOT节点

(1)类常量和静态属性引用的对象(static,final);

(2)虚拟机栈和本地方法栈中引用的对象(局部变量表);

7、Java程序占用CPU100%如何排查解决

前言:CPU占用100则表示当前有计算密集类型线程正在执行并处于无法退出/执行计算难度很大的状态(比如死循环和大量对象未死亡时的GC扫描阶段)

(1)查看GC是否正常,当有大量小对象存在,则GC会非常占用资源;

(2)查看占用资源的线程信息,根据线程执行信息定位代码问题;

(3)查看占用最高进程: top查看占用最高PID;

(4)查看该进程的可疑线程: top -H -p PID 查看当前进程中占用最高线程;

(5)打印输出可疑线程16位字符: printf "%x\n" 线程PID或echo "obase=16;线程PID" | bc;

(6)打印程序线程并查询对应线程信息: jstack pid | grep -A num 16进制PID

(7)查看线程栈信息的调用路径以及当前所处方法,查看代码逻辑;

8、阻塞状态线程会让CPU100嘛

CPU使用率的飙升更多是由于执行计算密集型任务导致的,而处于阻塞状态的线程在等待时间分片尚未执行,所以不会导致CPU使用率上升

9、死锁为啥会让CPU占用100

(1)当出现锁竞争时,自旋锁会一直循环执行获取锁操作直至到达重试次数或者获取到锁,使得CPU陷入忙等待状态;

(2)适用于业务执行时间比较短的操作;

while(! lock.lock()){
    //...
}

10、Connection=keep-alive干嘛的

(1)HTTP每次请求都会执行TCP连接握手操作,比较消耗资源而且性能也不是很好,所以通过客户端告知服务端建立长连接方式,进行长连接复用.客户端的下次请求即可使用该长连接进行发送;

(2)HTTP1.0协议要求客户端增加Connection=keep-alive表明长连接请求,服务端也需要返回加Connection=keep-alive表示支持,然后双方建立长连接,客户端可复用该条连接请求;

(3)http1.1默认使用加Connection=keep-alive的长连接模式,默认的连接时长可以在服务器端设置不会让它长时间连接防止无效资源占用;

(4)长连接可以保证客户端可进行连接复用,但不能使服务器端主动向客户端发送请求(复用的是底层TCP的Socket连接,但需要和TCP的keep-alive区分开)

11、 为啥用Websocket做实时通信连接, JSONP了解吗.

(1)Websocket是基于TCP的全双工通信,可以做到客户端和服务器端实时发送信息,最主要解决服务器端主动向客户端推送信息的问题

(2)一个客户端建立一条websocket连接即可,不需要创建大量HTTP连接请求,减少服务端压力

(3)减少大量重复请求头的传输,减少网络带宽压力

(4)JSONP

12、长轮询机制不能做通信吗

(1)长轮询对于不同的实时信息传递需要有不同的请求连接,并且需要服务端HOLD住请求,会占用大量服务资源;而websocket可以将一条tcp通道复用来传递多种类型信息,传输信息再通过文本协议区分;

(2)Http请求的请求头每次都需要带很多信息,占用很多网络资源

(3)无法解决服务器端主动发送消息的问题

(4)短/长轮询和WebSocket的优缺点:

  • 短轮询

定义:其实就是普通的轮询。指在特定的的时间间隔(如每1秒),由浏览器对服务器发出HTTP request,然后由服务器返回最新的数据给客户端的浏览器。

应用场景:传统的web通信模式。后台处理数据,需要一定时间,前端想要知道后端的处理结果,就要不定时的向后端发出请求以获得最新情况。

优点:前后端程序编写比较容易。

缺点:请求中有大半是无用,难于维护,浪费带宽和服务器资源;响应的结果没有顺序(因为是异步请求,当发送的请求没有返回结果的时候,后面的请求又被发送。而此时如果后面的请求比前面的请 求要先返回结果,那么当前面的请求返回结果数据时已经是过时无效的数据了)。

实例:适于小型应用。

  • 长轮询

定义:客户端向服务器发送Ajax请求,服务器接到请求后hold住连接,直到有新消息才返回响应信息并关闭连接,客户端处理完响应信息后再向服务器发送新的请求。

优点:在无消息的情况下不会频繁的请求,耗费资源小。

缺点:服务器hold连接会消耗资源,返回数据顺序无保证,难于管理维护。

  • WebSocket

定义:Websocket是基于HTTP协议的,在和服务端建立了链接后,服务端有数据有了变化后会主动推送给前端。

优点:请求响应快,不浪费资源。(传统的http请求,其并发能力都是依赖同时发起多个TCP连接访问服务器实现的(因此并发数受限于浏览器允许的并发连接数),而websocket则允许我们在一条ws连接上同时并发多个请求,即在A请求发出后A响应还未到达,就可以继续发出B请求。由于TCP的慢启动特性(新连接速度上来是需要时间的),以及连接本身的握手损耗,都使得websocket协议的这一特性有很大的效率提升;http协议的头部太大,且每个请求携带的几百上千字节的头部大部分是重复的,websocket则因为复用长连接而没有这一问题。)

缺点:主流浏览器支持的Web Socket版本不一致;服务端没有标准的API,下图是各大主流对websocket的兼容性列表。

实例:实现即时通讯:如股票交易行情分析、聊天室、在线游戏等,替代轮询和长轮询

解决:解决了http协议的两个问题:

①服务端的被动性。http协议是只有客户端询问之后才回复。解决了同步有延迟的问题

②解决了服务器上消耗资源的问题

13、Kafka顺序消费如何保证

(1)一个队列只创建一个分区保证顺序消费

(2)一个队列上的消息投递利用key的hash结果,保证部分有序

14、Kafka中Partion和Consumer对应关系

(1)一个Partion只能被一个消费者分组中的一个消费者消费

(2)一个消费者可以消费队列中多个partion

15、Redis分布式锁先SETNX后,未来得及设置过期时间宕机了怎么办

(1)set key value nx ex seconds(原子操作)

(2)LUA脚本保证原子操作

16、Redis内存不足时怎么样

(1)执行淘汰策略.默认是返回错误;还有随即淘汰和最近最少使用淘汰策略(注意设置maxmemory,否则64位机器会用尽机器内存)

(2)使用集群模式,将数据分片存储(中间件解决分片/业务keyhash实现)

(3)减少大key产生,查询大key优化

17、Redis如何查看大KEY

(1)redis-cli --bigkeys 查看大Key;基于scan命令,不用担心阻塞问题.对String类型统计字节长度,对集合类型统计元素个数.

(2)debug object key 查看对应key的序列化后长度

(3)手动执行bgsave命令,生成rdb文件,通过redis rdb tools工具分析rdb文件

(4)memory usage key 查看对应key的内存使用(4.0+版本)

18、 LUA脚本有了解吗

- LUA教程

19、 Redis如果有大量Key同一时间失效怎么办

我们使用缓存的主要目是提升查询速度和保护数据库等稀缺资源不被占满。而缓存最常见的问题是缓存穿透、击穿和雪崩,在高并发下这三种情况都会有大量请求落到数据库,导致数据库资源占满,引起数据库故障。

(1)概念

  • 缓存穿透 在高并发下,查询一个不存在的值时,缓存不会被命中,导致大量请求直接落到数据库上,如活动系统里面查询一个不存在的活动。
  • 缓存击穿 在高并发下,对一个特定的值进行查询,但是这个时候缓存正好过期了,缓存没有命中,导致大量请求直接落到数据库上,如活动系统里面查询活动信息,但是在活动进行过程中活动缓存突然过期了。
  • 缓存雪崩 在高并发下,大量的缓存key在同一时间失效,导致大量的请求落到数据库上,如活动系统里面同时进行着非常多的活动,但是在某个时间点所有的活动缓存全部过期。

(2)常见解决方案

  • 直接缓存NULL值
  • 限流
  • 缓存预热
  • 分级缓存
  • 缓存永远不过期

(3)layering-cache实践

在layering-cache里面结合了缓存NULL值,缓存预热,限流、分级缓存和间接的实现"永不过期"等几种方案来应对缓存穿透、击穿和雪崩问题。

  • 直接缓存NULL值

应对缓存穿透最有效的方法是直接缓存NULL值,但是缓存NULL的时间不能太长,否则NULL数据长时间得不到更新,也不能太短,否则达不到防止缓存击穿的效果。 我们在layering-cache对NULL值进行了特殊处理,一级缓存不允许存NULL值,二级缓存可以配置缓存是否允许存NULL值,如果配置可以允许存NULL值,框架还支持配置缓存非空值和NULL值之间的过期时间倍率,这使得我们能精准的控制每一个缓存的NULL值过期时间,控制粒度非常细。当NULL缓存过期我还可以使用限流,缓存预热等手段来防止穿透。

示例:

@Cacheable(value = "people", key = "#person.id", depict = "用户信息缓存",
        firstCache = @FirstCache(expireTime = 10, timeUnit = TimeUnit.MINUTES),
        secondaryCache = @SecondaryCache(expireTime = 10, timeUnit = TimeUnit.HOURS,
                isAllowNullValue = true, magnification = 10))
public Person findOne(Person person) {
    Person p = personRepository.findOne(Example.of(person));
    logger.info("为id、key为:" + p.getId() + "数据做了缓存");
    return p;
}

在这个例子里面isAllowNullValue = true表示允许换存NULL值,magnification = 10表示NULL值和非NULL值之间的时间倍率是10,也就是说当缓存值为NULL是,二级缓存的有效时间将是1个小时。

  • 限流

应对缓存穿透的常用方法之一是限流,常见的限流算法有滑动窗口,令牌桶算法和漏桶算法,或者直接使用队列、加锁等,在layering-cache里面我主要使用分布式锁来做限流。

layering-cache数据读取流程: 下面是读取数据的核心代码:

private <T> T executeCacheMethod(RedisCacheKey redisCacheKey, Callable<T> valueLoader) {
    Lock redisLock = new Lock(redisTemplate, redisCacheKey.getKey() + "_sync_lock");
    // 同一个线程循环20次查询缓存,每次等待20毫秒,如果还是没有数据直接去执行被缓存的方法
    for (int i = 0; i < RETRY_COUNT; i++) {
        try {
            // 先取缓存,如果有直接返回,没有再去做拿锁操作
            Object result = redisTemplate.opsForValue().get(redisCacheKey.getKey());
            if (result != null) {
                logger.debug("redis缓存 key= {} 获取到锁后查询查询缓存命中,不需要执行被缓存的方法", redisCacheKey.getKey());
                return (T) fromStoreValue(result);
            }

            // 获取分布式锁去后台查询数据
            if (redisLock.lock()) {
                T t = loaderAndPutValue(redisCacheKey, valueLoader, true);
                logger.debug("redis缓存 key= {} 从数据库获取数据完毕,唤醒所有等待线程", redisCacheKey.getKey());
                // 唤醒线程
                container.signalAll(redisCacheKey.getKey());
                return t;
            }
            // 线程等待
            logger.debug("redis缓存 key= {} 从数据库获取数据未获取到锁,进入等待状态,等待{}毫秒", redisCacheKey.getKey(), WAIT_TIME);
            container.await(redisCacheKey.getKey(), WAIT_TIME);
        } catch (Exception e) {
            container.signalAll(redisCacheKey.getKey());
            throw new LoaderCacheValueException(redisCacheKey.getKey(), e);
        } finally {
            redisLock.unlock();
        }
    }
    logger.debug("redis缓存 key={} 等待{}次,共{}毫秒,任未获取到缓存,直接去执行被缓存的方法", redisCacheKey.getKey(), RETRY_COUNT, RETRY_COUNT * WAIT_TIME, WAIT_TIME);
    return loaderAndPutValue(redisCacheKey, valueLoader, true);
}

当需要加载缓存的时候,需要获取到锁才有权限到后台去加载缓存数据,否则就会等待(同一个线程循环20次查询缓存,每次等待20毫秒,如果还是没有数据直接去执行被缓存的方法,这个主要是为了防止获取到锁并且去加载缓存的线程出问题,没有返回而导致死锁)。当获取到锁的线程执行完成会将获取到的数据放到缓存中,并且唤醒所有等待线程。 这里需要注意一下让线程等待一定不能用Thread.sleep(),我在使用Spring Redis Cache的时候,我发现当并发达到300左右,缓存一旦过期就会引起死锁,原因是使用的是sleep方法来让没有获取到锁的线程等待,当等待的线程很多的时候会产生大量上下文切换,导致获取到锁的线程一直获取不到cpu的执行权,导致死锁。在layering-cache里面,我们使用的是LockSupport.parkNanos方法,它会释放cpu资源, 因为我们使用的是redis分布式锁,所以也不能使用wait-notify机制。

  • 缓存预热

有效应对缓存的击穿和雪崩的方式之一是缓存预加载。

@Cacheable(value = "people", key = "#person.id", depict = "用户信息缓存",
        firstCache = @FirstCache(expireTime = 10, timeUnit = TimeUnit.MINUTES),
        secondaryCache = @SecondaryCache(expireTime = 10, preloadTime = 2,timeUnit = TimeUnit.HOURS,))
public Person findOne(Person person) {
    Person p = personRepository.findOne(Example.of(person));
    logger.info("为id、key为:" + p.getId() + "数据做了缓存");
    return p;
}

在 layering-cache里面二级缓存会配置两个时间,expireTime是缓存的过期时间,preloadTime 是缓存的刷新时间(预加载时间)。每次二级缓存被命中都会去检查缓存的过去时间是否小于刷新时间,如果小于就会开启一个异步线程预先去更新缓存,并将新的值放到缓存中,有效的保证了热点数据"永不过期"。这里预先更新缓存也是需要加锁的,并不是所有的线程都会落到库上刷新缓存,如果没有获取到锁就直接结束当前线程。

 /**
 * 刷新缓存数据
 */
private <T> void refreshCache(RedisCacheKey redisCacheKey, Callable<T> valueLoader, Object result) {
    Long ttl = redisTemplate.getExpire(redisCacheKey.getKey());
    Long preload = preloadTime;
    // 允许缓存NULL值,则自动刷新时间也要除以倍数
    boolean flag = isAllowNullValues() && (result instanceof NullValue || result == null);
    if (flag) {
        preload = preload / getMagnification();
    }
    if (null != ttl && ttl > 0 && TimeUnit.SECONDS.toMillis(ttl) <= preload) {
        // 判断是否需要强制刷新在开启刷新线程
        if (!getForceRefresh()) {
            logger.debug("redis缓存 key={} 软刷新缓存模式", redisCacheKey.getKey());
            softRefresh(redisCacheKey);
        } else {
            logger.debug("redis缓存 key={} 强刷新缓存模式", redisCacheKey.getKey());
            forceRefresh(redisCacheKey, valueLoader);
        }
    }
}

/**
 * 硬刷新(执行被缓存的方法)
 *
 * @param redisCacheKey {@link RedisCacheKey}
 * @param valueLoader   数据加载器
 */
private <T> void forceRefresh(RedisCacheKey redisCacheKey, Callable<T> valueLoader) {
    // 尽量少的去开启线程,因为线程池是有限的
    ThreadTaskUtils.run(() -> {
        // 加一个分布式锁,只放一个请求去刷新缓存
        Lock redisLock = new Lock(redisTemplate, redisCacheKey.getKey() + "_lock");
        try {
            if (redisLock.lock()) {
                // 获取锁之后再判断一下过期时间,看是否需要加载数据
                Long ttl = redisTemplate.getExpire(redisCacheKey.getKey());
                if (null != ttl && ttl > 0 && TimeUnit.SECONDS.toMillis(ttl) <= preloadTime) {
                    // 加载数据并放到缓存
                    loaderAndPutValue(redisCacheKey, valueLoader, false);
                }
            }
        } catch (Exception e) {
            logger.error(e.getMessage(), e);
        } finally {
            redisLock.unlock();
        }
    });
}

在缓存总量和并发量都很大的时候,这个时候缓存如果同时失效,缓存预热将是一个非常慢长的过程,就比如说服务重启或新上线一个新的缓存。这个时候我们可以采用切流的方式,让缓存慢慢预热,如开始切10%流量,观察没有异常后,再切30%流量,观察没有异常后,再切60%流量,然后全量。这种方式虽然有点繁琐,但是一旦遇到异常我们可以快速的切回流量,让风险可控。

(4)总结

总体来说layering-cache在缓存穿透、击穿和雪崩上是以预防为主,补救为辅。而在应对缓存的这些问题上其实也没有一个完全完美的方案,只有最适合自己业务系统的方案。目前如果直接使用layering-cache缓存框架已经基本能应对大部分的缓存问题了

20、Redis Pipeline

(1)Redis Pipeline原理

  • 原理

大多数同学一直以来对 Redis 管道有一个误解,他们以为这是 Redis 服务器提供的一种特别的技术,有了这种技术就可以加速 Redis 的存取效率。但是实际上 Redis 管道 (Pipeline) 本身并不是 Redis 服务器直接提供的技术,这个技术本质上是由客户端提供的,跟服务器没有什么直接的关系。下面我们对这块做一个深入探究。

  • Redis的消息交互

当我们使用客户端对 Redis 进行一次操作时, 如下图所示,客户端将请求传送给服务器,服务器处理完毕后,再将响应回复给客户端。这要花费一个网络数据包来回的时间。 如果连续执行多条指令,那就会花费多个网络数据包来回的时间。如下图所示。 回到客户端代码层面,客户端是经历了读-写-读-写四个操作才完整地执行了两条指令 现在如果我们调整读写顺序,改成写-写-读-读,这两个指令同样可以正常完成 两个连续的写操作和两个连续的读操作总共只会花费一次网络来回,就好比连续的 write 操作合并了,连续的 read 操作也合并了一样。 这便是管道操作的本质,服务器根本没有任何区别对待,还是收到一条消息,执行一条消息,回复一条消息的正常的流程。客户端通过对管道中的指令列表改变读写顺序就可以大幅节省 IO 时间。管道中指令越多,效果越好。

  • 管道压力测试

接下来我们实践一下管道的力量。Redis 自带了一个压力测试工具 redis-benchmark,使用这个工具就可以进行管道测试。

首先我们对一个普通的 set 指令进行压测,QPS 大约 5w/s。

redis-benchmark -t set -q
SET: 51975.05 requests per second

我们加入管道选项-P参数,它表示单个管道内并行的请求数量,看下面P=2,QPS 达到了 9w/s。

redis-benchmark -t set -P 2 -q
SET: 91240.88 requests per second

再看看P=3,QPS达到了10w/s

 SET: 102354.15 requests per second

但如果再继续提升 P 参数,发现 QPS 已经上不去了。这是为什么呢?因为这里 CPU 处理能力已经达到了瓶颈,Redis 的单线程 CPU 已经飙到了 100%,所以无法再继续提升了。

  • 深入理解管道本质

接下来我们深入分析一个请求交互的流程,真实的情况是它很复杂,因为要经过网络协议栈,这个就得深入内核了。 上图就是一个完整的请求交互流程图。 我用文字来仔细描述一遍:

①客户端进程调用write将消息写到操作系统内核为套接字分配的发送缓冲send buffer。

②客户端操作系统内核将发送缓冲的内容发送到网卡,

网卡硬件将数据通过「网际路由」送到服务器的网卡。

③服务器操作系统内核将网卡的数据放到内核为套接字分配的接收缓冲recv buffer。

④服务器进程调用read从接收缓冲中取出消息进行处理。

⑤服务器进程调用write将响应消息写到内核为套接字分配的发送缓冲send buffer。

⑥服务器操作系统内核将发送缓冲的内容发送到网卡,

网卡硬件将数据通过「网际路由」送到客户端的网卡。

⑦客户端操作系统内核将网卡的数据放到内核为套接字分配的接收缓冲recv buffer。

⑧客户端进程调用read从接收缓冲中取出消息返回给上层业务逻辑进行处理。

⑨结束。

其中步骤 5—8和 1—4 是一样的, 只不过方向是反过来的,一个是请求,一个是响应。

我们开始以为 write 操作是要等到对方收到消息才会返回,但实际上不是这样的。write 操作只负责将数据写到本地操作系统内核的发送缓冲然后就返回了。剩下的事交给操作系统内核异步将数据送到目标机器。但是如果发送缓冲满了,那么就需要等待缓冲空出空闲空间来,这个就是写操作 IO 操作的真正耗时。

我们开始以为 read 操作是从目标机器拉取数据,但实际上不是这样的。read 操作只负责将数据从本地操作系统内核的接收缓冲中取出来就了事了。但是如果缓冲是空的,那么就需要等待数据到来,这个就是读操作 IO 操作的真正耗时。所以对于value = redis.get(key)这样一个简单的请求来说,write操作几乎没有耗时,直接写到发送缓冲就返回,而read就会比较耗时了,因为它要等待消息经过网络路由到目标机器处理后的响应消息,再回送到当前的内核读缓冲才可以返回。这才是一个网络来回的真正开销。

而对于管道来说,连续的write操作根本就没有耗时,之后第一个read操作会等待一个网络的来回开销,然后所有的响应消息就都已经回送到内核的读缓冲了,后续的 read 操作直接就可以从缓冲拿到结果,瞬间就返回了。

  • 小结

这就是管道的本质了,它并不是服务器的什么特性,而是客户端通过改变了读写的顺序带来的性能的巨大提升。个人再总结一点就是:写入Redis的时候,一般情况我们写入一个数据,要等到服务器返回给我们ok,我们才写下一条数据,这样自然就慢了,但是pipeline则是:写入数据只要写到本地缓存就ok,就一直写,写到缓存满了才停,当然数据也是持续不断的往Redis 服务器走,来清空客户端的缓存,这样当然就快了,因为再也不用等服务器的返回了。

(2)Redis:Pipeline详解

  • pipeline出现的背景

redis客户端执行一条命令分4个过程 发送命令——>命令排队——>命令执行——>返回结果 这个过程称为Round trip time(简称RTT, 往返时间),mget mset有效节约了RTT,但大部分命令(如hgetall,并没有mhgetall)不支持批量操作,需要消耗N次RTT ,这个时候需要pipeline来解决这个问题

  • pipeline的性能 ①未使用pipeline执行N条命令

②使用了pipeline执行N条命令 ③两者性能对比

小结:这是一组统计数据出来的数据,使用Pipeline执行速度比逐条执行要快,特别是客户端与服务端的网络延迟越大,性能体能越明显。 下面贴出测试代码分析两者的性能差异:

	@Test
	public void pipeCompare() {
		Jedis redis = new Jedis("192.168.1.111", 6379);
		redis.auth("12345678");//授权密码 对应redis.conf的requirepass密码
		Map<String, String> data = new HashMap<String, String>();
		redis.select(8);//使用第8个库
		redis.flushDB();//清空第8个库所有数据
		// hmset
		long start = System.currentTimeMillis();
		// 直接hmset
		for (int i = 0; i < 10000; i++) {
			data.clear();  //清空map
			data.put("k_" + i, "v_" + i);
			redis.hmset("key_" + i, data); //循环执行10000条数据插入redis
		}
		long end = System.currentTimeMillis();
		System.out.println("    共插入:[" + redis.dbSize() + "]条 .. ");
		System.out.println("1,未使用PIPE批量设值耗时" + (end - start) / 1000 + "秒..");
		redis.select(8);
		redis.flushDB();
		// 使用pipeline hmset
		Pipeline pipe = redis.pipelined();
		start = System.currentTimeMillis();
		//
		for (int i = 0; i < 10000; i++) {
			data.clear();
			data.put("k_" + i, "v_" + i);
			pipe.hmset("key_" + i, data); //将值封装到PIPE对象,此时并未执行,还停留在客户端
		}
		pipe.sync(); //将封装后的PIPE一次性发给redis
		end = System.currentTimeMillis();
		System.out.println("    PIPE共插入:[" + redis.dbSize() + "]条 .. ");
		System.out.println("2,使用PIPE批量设值耗时" + (end - start) / 1000 + "秒 ..");
//--------------------------------------------------------------------------------------------------
		// hmget
		Set<String> keys = redis.keys("key_*"); //将上面设值所有结果键查询出来
		// 直接使用Jedis hgetall
		start = System.currentTimeMillis();
		Map<String, Map<String, String>> result = new HashMap<String, Map<String, String>>();
		for (String key : keys) {
			//此处keys根据以上的设值结果,共有10000个,循环10000次
			result.put(key, redis.hgetAll(key)); //使用redis对象根据键值去取值,将结果放入result对象
		}
		end = System.currentTimeMillis();
		System.out.println("    共取值:[" + redis.dbSize() + "]条 .. ");
		System.out.println("3,未使用PIPE批量取值耗时 " + (end - start) / 1000 + "秒 ..");

		// 使用pipeline hgetall
		result.clear();
		start = System.currentTimeMillis();
		for (String key : keys) {
			pipe.hgetAll(key); //使用PIPE封装需要取值的key,此时还停留在客户端,并未真正执行查询请求
		}
		pipe.sync();  //提交到redis进行查询
		
		end = System.currentTimeMillis();
		System.out.println("    PIPE共取值:[" + redis.dbSize() + "]条 .. ");
		System.out.println("4,使用PIPE批量取值耗时" + (end - start) / 1000 + "秒 ..");

		redis.disconnect();
	}

  • 原生批命令(mset,mget)与pipeline对比

①原生批命令是原子性,pipeline是非原子性 (原子性概念:一个事务是一个不可分割的最小工作单位,要么都成功要么都失败。原子操作是指你的一个业务逻辑必须是不可拆分的. 处理一件事情要么都成功,要么都失败,原子不可拆分) ②原生批命令一命令多个key, 但pipeline支持多命令(存在事务),非原子性 ③原生批命令是服务端实现,而pipeline需要服务端与客户端共同完成

  • pipeline正确使用方式

使用pipeline组装的命令个数不能太多,不然数据量过大,增加客户端的等待时间,还可能造成网络阻塞,可以将大量命令的拆分多个小的pipeline命令完成。

①Jedis中的pipeline使用方式大家知道redis提供了mset、mget方法,但没有提供mdel方法,如果想实现,可以借助pipeline实现。

②Jedis中的pipeline使用步骤:获取jedis对象(一般从连接池中获取)——>获取jedis对象的pipeline对象——>添加指令——>执行指令

测试类方法:

@Test
	public void testCommond() {
		// 工具类初始化
		JedisUtils jedis = new JedisUtils("192.168.1.111", 6379, "12345678");

		for (int i = 0; i < 100; i++) {
			// 设值
			jedis.set("n" + i, String.valueOf(i));
		}
		System.out.println("keys from redis return =======" + jedis.keys("*"));

	}

	// 使用pipeline批量删除
	 @Test
	public void testPipelineMdel() {
		// 工具类初始化
		JedisUtils jedis = new JedisUtils("192.168.1.111", 6379, "12345678");
		List<String> keys = new ArrayList<String>();
		for (int i = 0; i < 100; i++) {
			keys.add("n" + i);
		}
		jedis.mdel(keys);
		System.out.println("after mdel the redis return ---------" + jedis.keys("*"));
	}

JedisUtils下的mdel方法:

/**
	 * 删除多个字符串key 并释放连接
	 * 
	 * @param keys*
	 * @return 成功返回value 失败返回null
	 */
	public boolean mdel(List<String> keys) {
		Jedis jedis = null;
		boolean flag = false;
		try {
			jedis = pool.getResource();//从连接借用Jedis对象
			Pipeline pipe = jedis.pipelined();//获取jedis对象的pipeline对象
			for(String key:keys){
				pipe.del(key); //将多个key放入pipe删除指令中
			}
			pipe.sync(); //执行命令,完全此时pipeline对象的远程调用 
			flag = true;
		} catch (Exception e) {
			pool.returnBrokenResource(jedis);
			e.printStackTrace();
		} finally {
			returnResource(pool, jedis);
		}
		return flag;
	}

使用pipeline提交所有操作并返回执行结果:

@Test
	public void testPipelineSyncAll() {
		// 工具类初始化
		Jedis jedis = new Jedis("192.168.1.111", 6379);
		jedis.auth("12345678");
		// 获取pipeline对象
		Pipeline pipe = jedis.pipelined();
		pipe.multi();
		pipe.set("name", "james"); // 调值
		pipe.incr("age");// 自增
		pipe.get("name");
		pipe.discard();
		// 将不同类型的操作命令合并提交,并将操作操作以list返回
		List<Object> list = pipe.syncAndReturnAll();

		for (Object obj : list) {
			// 将操作结果打印出来
			System.out.println(obj);
		}
		// 断开连接,释放资源
		jedis.disconnect();
	}
  • redis事务

pipeline是多条命令的组合,为了保证它的原子性,redis提供了简单的事务。

①redis的简单事务

一组需要一起执行的命令放到multi和exec两个命令之间,其中multi代表事务开始,exec代表事务结束。

②停止事务discard

③命令错误,语法不正确,导致事务不能正常结束

④运行错误,语法正确,但类型错误,事务可以正常结束

⑤watch命令:使用watch后,multi失效,事务失效

WATCH的机制是:在事务EXEC命令执行时,Redis会检查被WATCH的key,只有被WATCH的key从WATCH起始时至今没有发生过变更,EXEC才会被执行。如果WATCH的key在WATCH命令到EXEC命令之间发生过变化,则EXEC命令会返回失败。 小结:redis提供了简单的事务,不支持事务回滚

21、Linux断开SSH连接(窗口),如何让进程继续执行

(1)nohup **** &

(2)解决Linux关闭终端窗口后运行的程序或者服务自动停止解决Linux关闭终端后运行的程序自动停止

22、Httpcookie客户端能修改吗,服务端能修改吗.做什么的

(1)cookie是Http请求头中的参数,是存放到客户端的.主要有kv对,过期时间,域信息和路径信息以及是否只读

(2)cookie是可以被客户端篡改的(覆盖旧值,删除的话过期时间设置为0),所以现在大部分存放的是非敏感数据或者登录信息.当然如果用户设置保存密码,有的也会保存到cookie中.在请求时带上就可以校验用户的登陆状态了.

(3)cookie是可以被客户端和服务端修改(set-cookie创建新的覆盖掉/有效期设为0使之删除),但是当用户知道cookie中数据作用,进行修改尝试获取别人信息也是很危险的,所以敏感的cookie数据要进行加密操作,防止用户解析/使修改不生效.

(4)cookie大小和同域名cookie数量限制不同浏览器有不同限制,和协议无关

23、Http连接Time wait是啥意思

(1)这个不知道.也没有查到.可以有大佬解释下吗?我知道的是TCP连接TIME_WAIT状态

(2)通信双方建立TCP连接后,主动关闭连接的一方就会进入TIME_WAIT状态,以用来在数据包生命周期内接收可能尚未发送到的信息

(3)引用: 看看TCP/IP协议组我们就知道,这样做是为了让在网络中残余的TCP包消失, 也就是说, 如果我们没有等到这个时间就让OS把这个端口释放给其他的进程使用,别的进程很有可能就会收到上一个会话的残余TCP包,这样就会出现一系列的不可预知的错误

(4)参考: ①http client 无限等待返回结果

TIME_WAIT状态原理

③TCP的三次握手与四次挥手

网络层,可以实现两个主机之间的通信。但是这并不具体,因为,真正进行通信的实体是在主机中的进程,是一个主机中的一个进程与另外一个主机中的一个进程在交换数据。IP协议虽然能把数据报文送到目的主机,但是并没有交付给主机的具体应用进程。而端到端的通信才应该是应用进程之间的通信。

UDP,在传送数据前不需要先建立连接,远地的主机在收到UDP报文后也不需要给出任何确认。虽然UDP不提供可靠交付,但是正是因为这样,省去和很多的开销,使得它的速度比较快,比如一些对实时性要求较高的服务,就常常使用的是UDP。对应的应用层的协议主要有 DNS,TFTP,DHCP,SNMP,NFS 等。

TCP,提供面向连接的服务,在传送数据之前必须先建立连接,数据传送完成后要释放连接。因此TCP是一种可靠的的运输服务,但是正因为这样,不可避免的增加了许多的开销,比如确认,流量控制等。对应的应用层的协议主要有 SMTP,TELNET,HTTP,FTP 等。

  • 常用的熟知端口号
  • TCP的概述

TCP把连接作为最基本的对象,每一条TCP连接都有两个端点,这种断点我们叫作套接字(socket),它的定义为端口号拼接到IP地址即构成了套接字,例如,若IP地址为192.3.4.16 而端口号为80,那么得到的套接字为192.3.4.16:80。

  • TCP报文首部

1、源端口和目的端口,各占2个字节,分别写入源端口和目的端口;

2、序号,占4个字节,TCP连接中传送的字节流中的每个字节都按顺序编号。例如,一段报文的序号字段值是 301 ,而携带的数据共有100字段,显然下一个报文段(如果还有的话)的数据序号应该从401开始;

3、确认号,占4个字节,是期望收到对方下一个报文的第一个数据字节的序号。例如,B收到了A发送过来的报文,其序列号字段是501,而数据长度是200字节,这表明B正确的收到了A发送的到序号700为止的数据。因此,B期望收到A的下一个数据序号是701,于是B在发送给A的确认报文段中把确认号置为701;

4、数据偏移,占4位,它指出TCP报文的数据距离TCP报文段的起始处有多远;

5、保留,占6位,保留今后使用,但目前应都位0;

6、紧急URG,当URG=1,表明紧急指针字段有效。告诉系统此报文段中有紧急数据;

7、确认ACK,仅当ACK=1时,确认号字段才有效。TCP规定,在连接建立后所有报文的传输都必须把ACK置1;

8、推送PSH,当两个应用进程进行交互式通信时,有时在一端的应用进程希望在键入一个命令后立即就能收到对方的响应,这时候就将PSH=1;

9、复位RST,当RST=1,表明TCP连接中出现严重差错,必须释放连接,然后再重新建立连接;

10、同步SYN,在连接建立时用来同步序号。当SYN=1,ACK=0,表明是连接请求报文,若同意连接,则响应报文中应该使SYN=1,ACK=1;

11、终止FIN,用来释放连接。当FIN=1,表明此报文的发送方的数据已经发送完毕,并且要求释放;

12、窗口,占2字节,指的是通知接收方,发送本报文你需要有多大的空间来接受;

13、检验和,占2字节,校验首部和数据这两部分;

14、紧急指针,占2字节,指出本报文段中的紧急数据的字节数;

15、选项,长度可变,定义一些其他的可选的参数。

  • TCP连接的建立(三次握手) 最开始的时候客户端和服务器都是处于CLOSED状态。主动打开连接的为客户端,被动打开连接的是服务器。

①TCP服务器进程先创建传输控制块TCB,时刻准备接受客户进程的连接请求,此时服务器就进入了LISTEN(监听)状态;

②TCP客户进程也是先创建传输控制块TCB,然后向服务器发出连接请求报文,这是报文首部中的同部位SYN=1,同时选择一个初始序列号 seq=x ,此时,TCP客户端进程进入了 SYN-SENT(同步已发送状态)状态。TCP规定,SYN报文段(SYN=1的报文段)不能携带数据,但需要消耗掉一个序号。

③TCP服务器收到请求报文后,如果同意连接,则发出确认报文。确认报文中应该 ACK=1,SYN=1,确认号是ack=x+1,同时也要为自己初始化一个序列号 seq=y,此时,TCP服务器进程进入了SYN-RCVD(同步收到)状态。这个报文也不能携带数据,但是同样要消耗一个序号。

④TCP客户进程收到确认后,还要向服务器给出确认。确认报文的ACK=1,ack=y+1,自己的序列号seq=x+1,此时,TCP连接建立,客户端进入ESTABLISHED(已建立连接)状态。TCP规定,ACK报文段可以携带数据,但是如果不携带数据则不消耗序号。

⑤当服务器收到客户端的确认后也进入ESTABLISHED状态,此后双方就可以开始通信了。

  • 为什么TCP客户端最后还要发送一次确认呢?

一句话,主要防止已经失效的连接请求报文突然又传送到了服务器,从而产生错误。 如果使用的是两次握手建立连接,假设有这样一种场景,客户端发送了第一个请求连接并且没有丢失,只是因为在网络结点中滞留的时间太长了,由于TCP的客户端迟迟没有收到确认报文,以为服务器没有收到,此时重新向服务器发送这条报文,此后客户端和服务器经过两次握手完成连接,传输数据,然后关闭连接。此时此前滞留的那一次请求连接,网络通畅了到达了服务器,这个报文本该是失效的,但是,两次握手的机制将会让客户端和服务器再次建立连接,这将导致不必要的错误和资源的浪费。 如果采用的是三次握手,就算是那一次失效的报文传送过来了,服务端接受到了那条失效报文并且回复了确认报文,但是客户端不会再次发出确认。由于服务器收不到确认,就知道客户端并没有请求连接。

  • TCP连接的释放(四次挥手)

数据传输完毕后,双方都可释放连接。最开始的时候,客户端和服务器都是处于ESTABLISHED状态,然后客户端主动关闭,服务器被动关闭。

①客户端进程发出连接释放报文,并且停止发送数据。释放数据报文首部,FIN=1,其序列号为seq=u(等于前面已经传送过来的数据的最后一个字节的序号加1),此时,客户端进入FIN-WAIT-1(终止等待1)状态。 TCP规定,FIN报文段即使不携带数据,也要消耗一个序号。

②服务器收到连接释放报文,发出确认报文,ACK=1,ack=u+1,并且带上自己的序列号seq=v,此时,服务端就进入了CLOSE-WAIT(关闭等待)状态。TCP服务器通知高层的应用进程,客户端向服务器的方向就释放了,这时候处于半关闭状态,即客户端已经没有数据要发送了,但是服务器若发送数据,客户端依然要接受。这个状态还要持续一段时间,也就是整个CLOSE-WAIT状态持续的时间。

③客户端收到服务器的确认请求后,此时,客户端就进入FIN-WAIT-2(终止等待2)状态,等待服务器发送连接释放报文(在这之前还需要接受服务器发送的最后的数据)。

④服务器将最后的数据发送完毕后,就向客户端发送连接释放报文,FIN=1,ack=u+1,由于在半关闭状态,服务器很可能又发送了一些数据,假定此时的序列号为seq=w,此时,服务器就进入了LAST-ACK(最后确认)状态,等待客户端的确认。

⑤客户端收到服务器的连接释放报文后,必须发出确认,ACK=1,ack=w+1,而自己的序列号是seq=u+1,此时,客户端就进入了TIME-WAIT(时间等待)状态。注意此时TCP连接还没有释放,必须经过2 ∗ * ∗MSL(最长报文段寿命)的时间后,当客户端撤销相应的TCB后,才进入CLOSED状态。 ⑥服务器只要收到了客户端发出的确认,立即进入CLOSED状态。同样,撤销TCB后,就结束了这次的TCP连接。可以看到,服务器结束TCP连接的时间要比客户端早一些。

  • 为什么客户端最后还要等待2MSL

MSL(Maximum Segment Lifetime),TCP允许不同的实现可以设置不同的MSL值。 第一,保证客户端发送的最后一个ACK报文能够到达服务器,因为这个ACK报文可能丢失,站在服务器的角度看来,我已经发送了FIN+ACK报文请求断开了,客户端还没有给我回应,应该是我发送的请求断开报文它没有收到,于是服务器又会重新发送一次,而客户端就能在这个2MSL时间段内收到这个重传的报文,接着给出回应报文,并且会重启2MSL计时器。

第二,防止类似与“三次握手”中提到了的“已经失效的连接请求报文段”出现在本连接中。客户端发送完最后一个确认报文后,在这个2MSL时间中,就可以使本连接持续的时间内所产生的所有报文段都从网络中消失。这样新的连接中不会出现旧连接的请求报文。

为什么建立连接是三次握手,关闭连接确是四次挥手呢?

建立连接的时候, 服务器在LISTEN状态下,收到建立连接请求的SYN报文后,把ACK和SYN放在一个报文里发送给客户端。而关闭连接时,服务器收到对方的FIN报文时,仅仅表示对方不再发送数据了但是还能接收数据,而自己也未必全部数据都发送给对方了,所以己方可以立即关闭,也可以发送一些数据给对方后,再发送FIN报文给对方来表示同意现在关闭连接,因此,己方ACK和FIN一般都会分开发送,从而导致多了一次。

  • 如果已经建立了连接,但是客户端突然出现了故障怎么办?

TCP还设有一个保活计时器,显然,客户端如果出现故障,服务器不能一直等下去,白白浪费资源。服务器每收到一次客户端的请求后都会重新复位这个计时器,时间通常是设置为2小时,若两小时还没有收到客户端的任何数据,服务器就会发送一个探测报文段,以后每隔75秒发送一次。若一连发送10个探测报文仍然没反应,服务器就认为客户端出了故障,接着就关闭连接。

24、Integer赋值地址会变吗

这里是写算法题为了方便把传参是传值和传地址给忽略了,代码不用跑.

Integer当作参数传递.不会修改原来的变量,因为传递的是地址.对参数重新赋值是将地址变更了,而原来变量地址不变

25、算法:计算组成N的最小平方数的数量

  • 平方数是可重复的

给定正整数 n,找到若干个完全平方数(比如 1, 4, 9, 16, ...)使得它们的和等于 n。你需要让组成和的完全平方数的个数最少。给你一个整数 n ,返回和为 n 的完全平方数的 最少数量 。完全平方数 是一个整数,其值等于另一个整数的平方;换句话说,其值等于一个整数自乘的积。例如,1、4、9 和 16 都是完全平方数,而 3 和 11 不是。

  • 示例 1: 输入:n = 12 输出:3 解释:12 = 4 + 4 + 4
  • 示例 2: 输入:n = 13 输出:2 解释:13 = 4 + 9
  • 提示: 1 <= n <= 104

26、算法:查找最深层次子节点和

是最深层级所有子节点的和,不是所有子节点的和。

给你一棵二叉树的根节点 root ,请你返回 层数最深的叶子节点的和 。

  • 示例1:

输入:root = [1,2,3,4,5,null,6,7,null,null,null,null,8] 输出:15

  • 示例2: 输入:root = [6,7,8,2,7,1,3,9,null,1,4,null,null,null,5] 输出:19
  • 提示: 树中节点数目在范围 [1, 104] 之间。 1 <= Node.val <= 100

27、SQL:查找员工表第N高的工资

select salary from efm group by salary order by salary desc limit n-1, 1

写在最后

这篇文章到这里 终!于!接近结束。感谢各位耐心地看到这里(泪目......)如果这篇文章对你有帮助。别忘了点赞、转发、收藏(疯狂暗示)。你们的点赞就是我继续更新的动力,谢谢各位的观看。