作者:牛客395930587号
链接:https://www.nowcoder.com/discuss/490309?channel=1009&source_id=discuss_terminal_discuss_history
来源:牛客网
口述怎么样判断一个链表是否成环。
答:快慢指针检测,快指针每次走两格,慢指针每次走一格,判断二者是否会相遇。
讲一下平衡树的特征。
答:空树或任意节点的子树的高度差都小于等于1.
mysql数据库的索引是用什么结构来实现的?它为什么要用这种结构?它利用了硬盘的哪些特点?
答:B+树;
要知道为什么用B+树,首先得知道B+树的特点;
1、B+节点关键字搜索采用闭合区间;
2、B+非叶子节点不保存数据相关信息,只保存关键字和子节点的引用;
3、B+关键字对应的数据保存在叶子节点中;
4、B+叶子节点是顺序排列的,并且相邻节点具有顺序引用的关系
B+树在MySQL中的优势:
1、B+树:多路绝对平衡查找树,拥有B-树的优势;
2、B+树扫库表能力更强;
3、B+树的磁盘读写能力更强;
4、B+树的排序能力更强;
5、B+树的查询效率更加稳定
原因:索引本身也很大,不可能全部存储在内存中,因此索引常常以文件的形式存储在磁盘上,这时索引的查找过程就要产生磁盘IO消耗,评价一个数据结构作为索引的优劣最重要的指标就是在查找过程中磁盘IO操作次数的渐进复杂度。
使用B+树的原因和磁盘存取原理有很大的关系,由于存储介质和机械运动耗费,磁盘的存取速度往往是主存的几百分之一,为了提高效率,磁盘往往不是严格按需读取,而是每次都会预读,即使只需要一个字节,磁盘也会从这个位置开始,顺序向后读取一定长度的数据放入内存。这是根据著名的局限性原理:当一个数据被用到时,其附近的数据通常会马上被用到。
预读可以提高IO效率,预读的长度一般为页的整数倍。页是计算机管理存储器的逻辑块,硬件及操作系统往往将主存和磁盘存储区分割为连续的大小相等的块,每个存储块称为一页(在许多操作系统中,页的大小通常称为4k),主存和磁盘以页为单位交换数据。当程序要读取的数据不在主存中时,会触发一个缺页异常,此时系统会向磁盘发出读盘信号,磁盘会找到数据的起始位置并向后连续读取一页或几页载入内存,然后异常返回;
数据库系统巧妙的利用了磁盘预读原理,将一个节点的大小设为等于一个页,这样每个节点只需要一次IO就可以完全载入;
每次新建节点时,直接申请一个页的空间,这样就保证了一个节点物理上也存储在一个页内,加上之前计算机存储分配都是按页对齐的,就实现了一个node只需要一次IO
一次检索最多需要h-1次IO,复杂度O(h)=O(logmN)而红黑树这种结构h要高很多,逻辑上很近的节点物理上可能很远效率太差
给定N个数字,快速找到最大的K个,怎么实现?那么时间复杂度是多少?
答:堆排,调整成大根堆形式,取堆顶,
O(nlog2n)
归并和快排的相似之处和不同之处?它们的稳定性呢?
答:相似之处:都运用分治策略实现;
快排:分解;解决;合并
归并:分解;解决;合并
时间复杂度都是O(nlogn)
不同之处:快排是一种不稳定的排序算法;
归并是一种稳定的排序算法(即相等的元素顺序不会改变)但归并排序的空间消耗更大;
有一个文件,然后文件中有几十亿个数字,怎么去重呢?
递归在实际中会出现什么问题?这里我只说了可能会爆栈,面试官很友善的提示我说那从时间的角度呢?
超时、栈溢出
讲一下常见的设计模式。
答:代理模式、简单工厂模式、工厂方法模式、
1、单例模式、适配器模式、策略模式;
代理模式为其他对象提供一种代理以控制这个对象的方法,代理类在运行时创建的代理称之为动态代理;(Spring AOP横向切面技术);
2、简单工厂模式:又称为静态工厂方法模式,在简单工厂模式中,可以根据参数的不同返回不同类的实例,简单工厂模式专门定义一个类来负责创建其他类的实例,被创建的实例通常都具有共同父类;包含以下角色:工厂角色(负责实现创建所有具体产品类的实例,直接被外界调用)、抽象产品角色、具体产品角色;优点:只需要传入一个正确的参数既可以得到结果无须知道细节;缺点:违背了开闭原则,增加一个产品需要修改工厂的逻辑;
3、工厂方法模式:通过定义工厂父类负责定义创建对象的公共方法,而子类负责生产具体对象,将类的实例化(具体产品的创建)延迟到工厂类的子类中完成;角色(抽象产品、具体产品、抽象工厂、具体工厂);不违背开闭原则但是每新增一个产品还需要增加与之对应的具体工厂,增加系统的复杂性;
4、单例模式:特点:单例模式只能有一个实例;必须自己创建自己的唯一实例;必须给其他对象提供这一唯一实例;饿汉、懒汉、全局锁式、静态代码块式、双重校验锁式、静态内部类式、枚举方式;
5、适配器模式:将一个接口转化为客户所需要的另一个接口,使接口不兼容的一些类可以一起工作;类适配器(使用继承方式)、对象适配器(组合模式);角色(目标角色、源角色、适配器角色);优点:复用性、扩展性;缺点:系统较凌乱
hashmap是线程安全的吗?那用什么代替呢,还有一些常见的hashmap的问题
答:不是;hashmap不是线程安全的原因:HashMap在put的时候,插入的元素超过了容量(由负载因子决定)的范围就会触发扩容操作,就是rehash,这个会重新将原数组的内容重新hash到新的扩容数组中,在多线程的环境下,存在同时其他的元素也在进行put操作,如果hash值相同,可能出现同时在同一数组下用链表表示,造成闭环,导致在get时会出现死循环,所以HashMap是线程不安全的;
用ConcurrentHashMap代替;
HashTable通过使用synchronized关键字修饰方法来实现线程同步,因此Hashtable的同步会锁住整个数组,在高并发的情况下,效率非常差;
ConcurrentHashMap作为高吞吐量的线程安全HashMap实现,采用了锁分离的技术允许多个修改操作并发进行。将Hash表默认分为16个桶(每一个桶看做一个Hahtable),对应的put,remove等操作只需要锁住当前线程需要用到的桶,不需要锁住整个数据。只有个别方法(size(),containsValue())可能需要锁住整个表而不是某个桶,在实现的时候需要按照顺序锁定所有的桶,操作完毕后又按顺序释放所有桶,按顺序可以防止死锁的发生。在读操作的时候,由于ConcurrentHashMap底层的HashEntry将元素设为final修饰(key,hash,next),几乎是不可变量,所以删除操作时需要把删除节点前面的所有节点都复制一遍,然后把复制后的Hash链的最后一个节点指向待删除节点的后继节点,比较耗时,value用volatile(不能保证原子性)修饰使这个值被修改后对所有线程都可见。
在Java语言中,多线程安全的容器主要分为两种:Synchronized和Concurrent,虽然它们都是线程安全的,但是它们在性能方面差距比较大。
Synchronized容器(同步容器)主要通过synchronized关键字来实现线程安全,在使用的时候会对所有的数据加锁。需要注意的是,由于同步容器将所有对容器状态的访问都串行化了,这样虽然保证了线程的安全性,但是这种方法的代价就是严重降低了并发性,当多个线程竞争容器时,吞吐量会严重降低。于是引入了Concurrent容器(并发容器),Concurrent容器采用了更加智能的方案,该方案不是对整个数据加锁,而是采取了更加细粒度的锁机制,因此,在大并发量的情况下,拥有更高的效率。
synchronized锁的对象是什么?
答:1、对于同步方法,锁当前对象;
2、对于静态同步方法,锁当前类的class对象
3、对于同步代码块,锁住的是synchronized括号中的对象
synchronized和lock的区别
答:1、存在层面:Synchronized是java中的一个关键字,存在于JVM层面,Lock是java中的一个接口;
2、锁的释放条件:(1)获取锁的线程执行完同步代码后,自动释放;(2)线程发生异常时,JVM会自动释放锁;Lock必须在finally关键字中释放锁,不然容易造成线程死锁;
3、锁的获取:在Syncronized中,假设线程A获得锁,B线程等待。如果A发生阻塞,那么B会一直等待。在Lock中会分情况而定,Lock中有尝试获取锁的方法,如果尝试获得锁,则不用一直等待;
4、锁的状态:Synchronized无法判断锁的状态,Lock则可以判断;
5、锁的类型:synchronized是可重入锁,不可中断,非公平锁;Lock锁则是可重入锁,可中断,可公平锁;
6、锁的性能:synchronized使用与少量同步的情况下,性能开销比较大。Lock锁使用于大量同步阶段:
(1)Lock锁可以提高多个线程进行读的效率(使用readWriteLock)
(2)在竞争不是很激烈的情况下,synchronized的性能要优于ReetrantLock,但是在资源竞争很激烈的情况下,synchronized的性能会下降几十倍,ReetrantLock的性能可以维持常态;
(3)ReetrantLock提供了多样化的同步,比如有时间限制的同步,可以被Interrupt的同步等;
select count(*) 和select count(某个字段) 有什么区别,我没回答出来…
答:两者的主要区别是
(1) count(1) 会统计表中的所有的记录数,包含字段为null 的记录
(2)count(字段) 会统计该字段在表中出现的次数,许罗字段为null 的情况。即不统计字段为null 的记录。
一般情况下, count(1)和count(#)两者返回的结果是一样的;
假如表没有主键,那么count(1)比count(*)快;
假如有主键的话,那么主键作为count的条件时候count(主键)最快;
假如表中只有一个字段那count(#)最快
count(#) 跟 count(1) 的结果一样,都包括对NULL的统计
count(column) 是不包括NULL的统计
两个字段做了联合索引(a,b),那么select * from xx where a= ‘xx’ 会走索引吗?那b = 'xx’呢?
答:会;不会
联合索引(a,b)实际建立了(a)(a,b);
a可以看成一级目录,b是一级目录下的二级目录;
a=‘xx’只使用了一级目录;
b=‘xx由于没有使用一级目录,二级目录也就没法用;
select效率低的原因:
①不需要的列会增加数据传输时间和网络开销
用“SELECT * ”数据库需要解析更多的对象、字段、权限、属性等相关内容,在 SQL 语句复杂,硬解析较多的情况下,会对数据库造成沉重的负担。
②对于无用的大字段,如 varchar、blob、text,会增加 IO 操作
③失去 MySQL 优化器“覆盖索引”策略优化的可能性
SELECT * 杜绝了覆盖索引的可能性,而基于 MySQL 优化器的“覆盖索引”策略又是速度极快,效率极高,业界极为推荐的查询优化方式。
例如,有一个表为 t(a,b,c,d,e,f),其中,a 为主键,b 列有索引。
那么,在磁盘上有两棵 B+ 树,即聚集索引和辅助索引(包括单列索引、联合索引),分别保存(a,b,c,d,e,f)和(a,b)。
如果查询条件中 where 条件可以通过 b 列的索引过滤掉一部分记录,查询就会先走辅助索引;如果用户只需要 a 列和 b 列的数据,直接通过辅助索引就可以知道用户查询的数据。
如果用户使用 SELECT *,获取了不需要的数据,则首先通过辅助索引过滤数据,然后再通过聚集索引获取所有的列,这就多了一次 B+ 树查询,速度必然会慢很多。
由于辅助索引的数据比聚集索引少很多,很多情况下,通过辅助索引进行覆盖索引(通过索引就能获取用户需要的所有列),都不需要读磁盘,直接从内存取。
而聚集索引很可能数据在磁盘(外存)中(取决于 buffer pool 的大小和命中率),这种情况下,一个是内存读,一个是磁盘读,速度差异就很显著了,几乎是数量级的差异。
联合索引的优势:
1、减少开销:建一个联合索引(a,b,c),实际相当于建了(a)、(a,b)、(a,b,c)三个索引。
每多一个索引,都会增加写操作的开销和磁盘空间的开销。对于大量数据的表,使用联合索引会大大的减少开销!
2、覆盖索引:对联合索引(a,b,c),如果有如下 SQL 的:
SELECT a,b,c from table where a=‘xx’ and b = ‘xx’;
那么 MySQL 可以直接通过遍历索引取得数据,而无需回表,这减少了很多的随机 IO 操作。
减少 IO 操作,特别是随机 IO 其实是 DBA 主要的优化策略。所以,在真正的实际应用中,覆盖索引是主要的提升性能的优化手段之一。
3、效率高
1、需要加索引的字段,要在where条件中
2、数据量少的字段不需要加索引
3、如果where条件中是OR关系,加索引不起作用
4、符合最左原则:mysql会一直向右匹配直到遇到范围查询(>、<、between、like)就停止匹配,比如a = 1 and b = 2 and c > 3 and d = 4 如果建立(a,b,c,d)顺序的索引,d是用不到索引的,如果建立(a,b,d,c)的索引则都可以用到,a,b,d的顺序可以任意调整”
Redis你用到了哪些功能呢?
答:
缓存是如何和数据库交互的,如何保持数据一致性的?
答:只有用缓存,就可能会涉及到缓存与数据库双存储双写,只要是双写,就一定会有数据一致性的问题;
做法1:最经典的缓存+数据库读写的模式,Cache Aside Pattern
读的时候,先读缓存,缓存没有的话,就读数据库,然后取出数据后放入缓存,同时返回响应;
更新的时候,更新数据库,然后删除缓存;
(其实删除缓存,而不是更新缓存,就是一个 lazy 计算的思想,不要每次都重新做复杂的计算,不管它会不会用到,而是让它到需要被使用的时候再重新计算。像 mybatis,hibernate,都有懒加载思想。查询一个部门,部门带了一个员工的 list,没有必要说每次查询部门,都里面的 1000 个员工的数据也同时查出来啊。80% 的情况,查这个部门,就只是要访问这个部门的信息就可以了。先查部门,同时要访问里面的员工,那么这个时候只有在你要访问里面的员工的时候,才会去数据库里面查询 1000 个员工。)
最初的缓存不一致问题及解决方案?
问题:先修改数据库,后更新缓存如果删除缓存失败了,那么会导致数据库中是新数据,缓存中是旧数据,会导致数据不一致问题;
解决:先删除缓存,后修改数据库。如果数据库修改失败了,那么数据库中是旧数据,缓存中是空的,那么数据不会不一致。因为读的时候缓存没有,则读数据库中旧数据,然后更新到缓存中;
比较复杂的数据不一致问题?
数据发生了变更,先删除了缓存,然后要去修改数据库,此时还没修改一个请求过来,去读缓存,发现缓存空了,去查询数据库,查到了修改前的旧数据,放到了缓存中,随后数据变更的程序完成了数据库的修改,此时数据库和缓存中的数据不一致;
这种情况可能会在高并发量的情况下发生;
解决方案:更新数据时,根据数据的唯一标识,将操作路由之后,发送到一个jvm内部队列中。读取数据的时候,如果发现数据不存在缓存中,那么将重新读取数据+更新缓存的操作,根据唯一标识路由之后,也发送同一个jvm内部队列中。
一个队列对应一个工作线程,每个工作线程串行拿到对应的操作,然后一条一条的执行。这样的话,一个数据变更的操作,先删除缓存,然后再去更新数据库,但是还没完成更新。此时如果一个读请求过来,读到了空的缓存,那么可以先将缓存更新的请求发送到队列中,此时会在队列中积压,然后同步等待缓存更新完成。
这里有一个优化点,一个队列中,其实多个更新缓存请求串在一起是没意义的,因此可以做过滤,如果发现队列中已经有一个更新缓存的请求了,那么就不用再放个更新请求操作进去了,直接等待前面的更新操作请求完成即可。
待那个队列对应的工作线程完成了上一个操作的数据库的修改之后,才会去执行下一个操作,也就是缓存更新的操作,此时会从数据库中读取最新的值,然后写入缓存中。
如果请求还在等待时间范围内,不断轮询发现可以取到值了,那么就直接返回;如果请求等待的时间超过一定时长,那么这一次直接从数据库中读取当前的旧值。
此解决方案存在的问题:
1、读请求长时阻塞,如果数据更新很频繁,导致队列中挤压了大量更新操作的里面,然后读请求会发生大量超时,最后导致很多请求直接走数据库;(解决方法:加机器)
2、读请求并发量过高
3、多服务实例部署的请求路由
可能这个服务部署了多个实例,那么必须保证说,执行数据更新操作,以及执行缓存更新操作的请求,都通过 Nginx 服务器路由到相同的服务实例上。
4、热点商品的路由问题,导致请求的倾斜
Redis如何保证并发的安全呢?
答:https://www.jianshu.com/p/76e82ae61066
用过的java框架有哪些?讲一下mvc里面,指的是什么呢?MVC指的是什么?
答:模型视图控制器