一些面试笔记
解释下Java线程池的各个参数的含义?
在JDK当中,创建线程值的API为ThreadPoolExector类,这个类的构成方法当中就包含了创建线程值的几个核心参数。第一个corePoolSize用于设置线程池里面的核心线程数量maxPoolSize用于设置线程池里面允许的最大线程数量keepAliveTime用于设置当线程数量大于corePoolSize多出来空闲的线程将会在keepAliveTime之后就会被释放掉。unit用于设置keepAliveTime时间单位,比如秒钟、分钟等。workQueen用于设置等待队列,ThreadFactory用于设置每当创建新的线程放入线程值的时候,就会是就是通过这个线程工厂来创建的。handler用于设置就是说当线程等待队列都满了之后采取的策略,比如抛出异常等策略。那我们假设一组参数值来说明一下这些参数的含义。假设我们设置corePoolSize为1,maxPoolSize为3,keepAliveTime为60秒,workQueen为ArrayBrokingQueen有界阻塞队列大小为4,handler了默认的策略抛出一个所的threadRejection异常。第一时刻一开始有一个线程变量poolSize维护当前线程数量,此时poolSize等于零。第二时刻此时来了一个任务需要创建线程,poolSize小于corePoolSize(1)。那么直接创建线程,第3时刻,此时又来了一个任务需要创建线程,poolSize大于等于corePoolSize,此时队列没有满,那么就会将任务丢到队列当中去,第四时刻如果队列也满了,但是poolSize小于maxPoolSize那么继续创建线程,第5时刻如果poolSize等于maxPoolSize,那么此时再提交一个任务就要执行handle了,默认就是抛出一个异常,第六时刻此时线程池里面有三个线程poolSize等于maxPoolSize等于3假设都处于空闲状态,但是corePoolSize等于一,那么就会有3减1等于2,那么超出的两个空行线程在空闲超过60秒以后就会被回收掉。
Java中线程的状态包括哪一些?
这个问题在很多的博客和专业书籍上面普遍的讲解是五种状态。其实这种说法是JDK1.5之前java 中线程状态的描述,而且这种状态是延续了操作系统的线程状态。而不同的操作系统对于线程状态的定义也有差异。故JDK1.5之后Java统一定义了线程状态。Thread.state枚举中,定义了六种线程状态的枚举。
一、初始状态new,新创建了一个线程对象,但还没有调用start()的方法。
二、运行状态runnable,Java线程中将就绪ready和运行中running 两种状态统称为运行。线程对象创建以后,其他线程,比如main线程调用了该对象的start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中。获取CPU的使用权,此时处于就绪状态ready,就绪状态的线程在获得cpu 时间片变为运行中的状态running.
三、阻塞状态blocked 表示线程阻塞与锁。
四、等待状态waiting,进入该状态的线程。需要等待其他线程做出一些特定的动作,比如通知或中断。
五、超时等待状态,time_waiting,该状态不等于不同于waiting,它可以在指定的时间以后自行返回。
六、终止状态,Terminated。表示该线程已经执行完毕。
阿里面试题:Redisson实现分布式锁的内部原理?
这个面试题考察的是面试者是否真正深入了解Redisson内部的机制。这个问题要从以下几个方面来回答。
- 一、客户端线程在底层是如何实现加锁的。
第一步、先定位master节点。通过key,也就是readison.getLock(“mylock”)的字符串参数mylock计算出循环冗余校验码的值。再用该循环冗余校验码,对16384取模得到哈希槽。通过这个哈希槽定位redis cluster集群当中的master节点。
第二步、加锁。加锁底层逻辑是通过Lua脚本来实现的。如果客户端线程第一次去加锁的话,会在key对应的哈希数据结构当中添加线程标识UUID:ThreadID 1。指定该线程当前对这个key 加锁一次了,并设置锁的过期时间为三十秒。
- 二、客户端线程是如何维持加锁的?
当加锁成功以后,此时会对加锁的结果设置一个监听器。如果监听到加锁成功了,也就是返回的结果为null,此时就会在后台通过watch dog 看门狗机制启动一个后台定时任务,每隔十秒执行一次检查。如果当前key依然存在。就重置key的存活时间为30秒,维持加锁底层就是通过后台这样一个线程定时刷新重合时间维持的。
- 三、相同的客户端线程是如何实现可重入加锁的?
第一次加锁时会往key 对应的哈希数据结构当中设置UUID:ThreadID 1。表示当前线程对k 加锁一次。如果相同线程来再次对这个key 加锁。只需要将UUID:ThreadID 持有锁的次数加一即可。就为UUID:ThreadID 2。内存底层就是通过这样的数据结构来表示重加锁的语义的。
- 四、其他线程加锁失败时,底层是如何实现阻塞的?
通过key 对应的哈希结构当中的UUID:ThreadID 判断是否为当前线程id 如果不是,则线程加锁失败。如果没有设置获取锁超时时间,此时就会进入一个while的死循环中,一直尝试加锁,直到加锁成功才会返回。
- 五、客户端宕机锁是如何释放的?
客户端宕机啊,相应的watchdog 后台定时任务当然也就没了,此时就无法对key 进行定时续期。那么当指定存活时间过后,key 就会自动失效,锁当然也就自动释放。
- 六、客户端如何主动释放持有的锁?
客户端主动释放锁,底层同样也是通过执行Lua脚本的方式来实现的。如果判断当前释放的key 存在。并且在key 的哈希结构当中存在当前线程的加锁信息,那么此时就会减扣当前线程对这个k 的重入次数。减扣线程的重入锁次数之后,如果当前线程在这个key的重入次数为0,此时就会直接释放锁。如果当前线程在这个重入锁次数依然大于0,此时就直接重置一下key的续期时间为30秒。
- 七、客户端尝试获取锁超时时间的机制,底层是如何实现的?
如果在加锁时就指定的尝试获取锁超时时间。如果获取锁失败,此时就不会永无止境的在while循环里面一直等待,而是根据你指定的锁超时时间,在这段时间范围内获取不到锁,那么就会标记为获取锁失败,直接返回false。
- 八、客户端锁超时自动释放机制在底层又是如何实现的?
如果在加锁时指定的锁超时时间,那么就算你获取锁成功了,也不会开启watch dog 的定时任务了。此时直接就将当前持有的这把锁的过期时间设置为你指定的超时时间。那么当你指定的时间到了之后,key失效被删除了,key对应的锁相应的也就自动释放了。
数据库面试题:分别谈谈联合索引生效条件和失效条件?
这道题目考察索引生效条件、失效条件。像这个问题才其实很有意义,比考察一般概念性的问题好多了。能够大概考察面试者对写的程序是有注重做优化,提高代码质量和程序的性能呢,还是指简单的CV了事。大家可以这样说,首先的话是联合索引生效的条件,联合索引又叫做复合索引。两个或更多列上的索引。被称之为复合索引。对于复合索引,MYSQL从左到右的使用使用索引当中的字段,一个查询可以使用索引的一部分,但只能是最左侧部分。例如索引是KEY INDEX(A,B,C) 可以支持A或者A,B或者A,B,C三种组合进行查询,但不支持B,C 进行查询。当最左侧字段是常量引用时,所以就十分有效。利用索引当中的附加列可以缩小搜索的范围,但使用一个具有两列的索引,不同于使用两个单独的索引,复合索引的结构和电话簿类似,人名有姓和名构成,电话簿首先按照姓氏进行排序,然后按照名字对有相同姓氏的人进行排序。如果您知道姓,电话簿将非常有用,如果您知道姓和名电话簿则更为有用。但如果您只知道名,不知道姓,电话簿将没有用处。所以说创建复合索引时,应仔细考虑列的顺序。对索引当中所有列执行的搜索或仅对前几列进行搜索复合索引非常有用,仅对后面的任意列执行搜索时,复合索引则没有用处。然后说一说索引失效的条件,一、不在索引列上做任何操作,包括但不限于计算、函数、自动或者手动类型转换会导致索引失效而转向全表扫描。二、存储引擎不能使用索引范围条件右侧的列。三、尽量使用索引覆盖。即只查询啊索引的查询,查询索引和查询列一致,减少select *。四、MYSQL在使用不等于的时候。无法使用索引会导致全表扫描。五、isNull,is not null也无法使用索引。六、like以通配符开头,即 ‘%ABC’,MYSQL索引失效会变成全表扫描。最后讲讲使用索引的一般性的建议。一、对于单键索引,尽量选择针对当前query过滤性更好的索引。二、在选择组合索引的时候,当前索引当中过滤性最好的字段在索引字段顺序中位置越靠前越好。三、在选择组合索引的时候,尽量选择可以包含当前query的where子句当中更多字段的索引。四、尽可能通过分析统计信息和调整query的写法来达到选择合适索引的目的。
老师讲的很好,个人稍微补充一下自己拙见:
1. MYSQL8中新增松散索引扫描,可以解决部分非最左前缀查询不能使用索引问题,因为老师也说了,复合索引先按第一个列排序,如果第一个列相同的话,会按照第二个列排序,假如索引是 KEY INDEX(A,B) 查询语句为 SELECT * FROM t WHERE B = 2,现在查找B=2的,假如A=1时,B为1,2,3,5,6多条记录。当判断B=3不满足时,没必要往后判断了。因为 A=1,B为3之后的B肯定大于3 不满足条件。所以直接在索引上推断出哪些行符合条件,在回表即可。
2. IS NULL、IS NOT NULL以及!=、OR、IN 这些条件都可能使用到索引的(因为可能会发生失效情况,所以网上很多资料都直接说这几种情况不会使用索引,会导致索引失效),MySQL优化器在执行查询之前会找出一个成本最低的执行方案执行(可能使用到哪些索引,这些索引各自的成本是多少,全表扫描的代价是多少) 如果索引列NULL值特别少 查询条件为IS NULL,则MSYQL可能会使用到这个索引。同理如果NULL值特别多,查询条件为IS NOT NULL,则MSYQL也可能会使用到这个索引。(以实际为准,测试情况可能情况不一样)再补充一句,默认NULL的二级索引记录都被放在了B+树的最左边,NULL值认为是列中最小的值。总结:MYSQL内部优化器会根据检索比例、表大小等多个因素整体评估是否使用索引。
3.如果列字段是整型的且加上索引,以字符串查询时也会走索引,如果列字段是varchar且加上索引,以整型查询(不带引号)时不会走索引,因为MYSQL在字符串与数字比较时会将字符串转换为数字譬如select * from t where A=1;如果t表中A字段为varchar类型,则此SQL相当于:select * from t where CAST(A AS signed int)=1;
4. MYSQL5.6之前 复合索引范围之后全失效,假如索引是 KEY INDEX(A,B) 查询语句为SELECT * FROM t WHERE A in (1,2) and B = 3; MySQL5.6之前,需要先使用索引查询A in (1,2)再全部回表验证B = 3,MySQL5.6之后,如果索引中可以判断,直接使用索引过滤,称为索引下推
大厂面试题:解释下什么是redis雪崩,穿透和击穿以及应对措施是什么?
第一个是缓存雪崩:对于系统A,假设每天高峰期每秒5000个请求。本来缓存在高峰期可以扛住每秒4000个请求,但是缓存的机器意外发生了宕机,缓存挂了。此时一秒5000的请求全部落到了数据库上,数据库必然扛不住。他会报一下警,然后就挂了。此时如果没有采取什么特别的方案。来处理这个故障,DBA很着急重启数据库,但是数据库立马又被新的流量给打死了。这就是缓存雪崩。大约在六年前国内比较知名的一个互联网公司,曾因为缓存事故导致雪崩,后台系统全部崩溃。事故从当天下午持续到晚上凌晨三到四点,公司损失了几千万。那缓存雪崩的解决方案是怎样的呢?大家可以这样,事前,Redis高可用可以通过主从加哨兵或redis cluster的搭建集群,避免全盘崩溃。事中,本地缓存延迟开启缓存加Hysteric限流,并且进行降级,避免mysql 被打死。事后,Redis持久化,一旦重启,自动从磁盘上加载数据,加速恢复缓存的数据。经过上述处理后的系统,当用户发送一个请求,系统A收到请求以后,先查本地Ehcache缓存。如果没有查到再查redis,如果Ehcache和redis 都没有查到,再查数据库,将数据库当中的结果写入到Ehcache和redis 中。限流组件可以设置每秒的请求有多少能够通过组件剩余未通过的请求怎么办呢?走降级可以返回一些默认的值,或者友情提示或者空白的字。那这样做的好处是什么呢?一、数据库绝对不会死,限流组件确保了每秒只有多少个请求能够通过。二、只要数据库不死,就是说对于用户来说,2/5的请求都可以被处理。三、只要有2/5的请求可以被处理,就是意味着你的系统没有死。对用户来说可能就是点击几次刷不出来页面,但是多点几次就可以刷出来一次。
第二个是缓存穿透:对于系统A假设每秒5000个请求,结果其中4000个请求是黑客发出来的恶意攻击。黑客发出来那4000个请求缓存中查不到,每次去数据库里面查也查不到。举个例子,数据库的ID是从1开始的。结果黑客发过来的请求ID全部都是负数。那这样的话,缓存每次都不会有,请求每次都“视缓存于无物“,直接查询数据库。这种恶意的攻击的场景,缓存穿透就会直接把数据库给打死。那解决方案的话也很简单,每次系统A从数据库当中只要没有查到,就会写一个空值到缓存当中。比如set -999 unknown,然后设置一个过期时间,这样的话下次有相同的key 来访问的时候,在缓存失效前都可以直接从缓存当中取数。
第三个就是缓存击穿:缓存击穿就是说某个key非常的热点,访问非常频繁,处于集中式高并发的访问的情况。当这个key在失效的瞬间。大量的请求就击穿了缓存。直接请求数据库,就像在一道屏障上面错开了一个洞,不同的场景下的解决方案如下:一、若缓存的数据是基本不会发生变更的,则可以尝试将热点的数据设置为永不过期。二、若缓存的数据更新不频繁,且缓存刷新的整个流程耗时比较少的情况下,则可以采用基于redis,zookeeper等分布式中间件的分布式互斥锁或者本地互斥锁,以保证进行少量的请求能请求数据库并重新构建缓存。其余的线程则在所释放以后只能访问到新的缓存。三、若缓存的数据更新频繁,或者缓存刷新的流程耗时比较长的情况下,可以利用定时线程在缓存过期前主动的重构缓存或者延后缓存的过期时间。以保证所有的请求一直访问相应的缓存。
数据库面试题:为什么要小表驱动大表?
小表驱动大表的优化主要有以下两个场景
- 第一,多表JOIN,类似于循环嵌套,外面的循环循环五次里面的循环循环一千次。如果小的循环在外层,对于数据库连接来说,就只连接五次,进行五千次的操作。如果一千在外,则需要进行一千次数据库连接,从而浪费资源,增加消耗。这就是JOIN的时候,为什么需要小表驱动大表。
- 第二种场景的话就是in和exist 子查询时也要使用小表驱动大表。因为in的子查询是先执行子查询后执行主查询。而exist 的是是先执行主查询,再将结果给子查询执行。所以对于in 和exist 的使用。in后面建议跟小表,exist 后面建议跟大表。简记:in小exist 大。
字节一面面试题:项目中是如何处理重复并发请求的?
对于一些用户请求在某些场景下是可以重复发送的。如果是查询类操作并无大碍,但其中有一些是涉及写入的操作的,一旦重复了,可能会导致很严重的后果。例如交易的接口,如果重复请求,可能会重复下单,那重复请求的场景有哪一些呢?比如黑客拦截的请求重放,还有前端。或者客户端因为某些原因请求重复发送了,或者用户在很短的时间内重复点击了。还有网关重发等等。那如何在服务端优雅的统一处理这种情况呢?
方案一:利用唯一请求编号去重,什么意思呢?就是只要请求有唯一的请求编号,那么就能借助redis 做这个去重。只要这个唯一的请求编号在redis 中存在证明处理过,那么就认为是重复的。具体的话可以这么做,每次写请求之前都是服务端返回一个唯一的编号,给客户端客户端带着这个唯一的请求号做请求,那服务端就可以完成去重拦截。
方案二:业务参数去重,上面的方案能够解决具备唯一请求编号的场景。但是很多场景下请求并不会带着一个唯一的编号。那么我们针对这种请求,我们可以用请求参数作为请求的一个标识。假设请求的参数只有一个字段reqParam。那我们可以利用 “用户ID接口名:请求参数” 这个标识去判断这个请求是否重复。问题是我们的接口通常不是这么简单。以目前主流我们的参数通常是一个JSON。那针对这种场景,我们应该如何去去重呢?
方案三:计算请求参数的摘要作为参数标识。假设我们请求参数JSON 按key做升序排序,排序以后拼成一个字符串作为key 值,但这可能非常的长。所以我们可以考虑对这个字符串求一个MD5作为参数的摘要,以这个摘要去取代reqParam判断的位置。这样请求的唯一标识就打上了。上面的方案其实已经是一个很不错的方案了,但是实际投入使用的时候,还可能出现一些问题。某些请求用户短时间内重复点击的,例如一千毫秒发送的三次请求,但绕过了上面的去重判断,即不同的key值,原因是这些请求参数字段里面是带时间字段的,这个字段标识用户请求的时间。这种请求我们也有可能需要挡住后面的重复请求。所以业务参数在摘要之前需要剔除啊这类时间字段,还有类似的字段,可能是GPS经纬度字段。
- 另外提一个问题给大家思考一下。如何去禁止用户重复点击等客户端操作
生成一个用户id加uuid生成一个防重token 用户点击一次就存redis 判断redis有没有这个token,如果有就报错返回,在逻辑处理完成以后删除这个token
进阶面试题:谈谈MySQL中的重做日志,回滚日志和二进制日志的作用以及区别?
在mysql 当中有六种日志文件,它们分别是。重做日志、回滚日志、二进制日志、错误日志、慢查询日志、一般查询日志、中继日志,其中重做日志和回滚日志与事务操作息息相关。二进制日志也与事务操作有一定的关系。这三组日志对理解mysql 中的事务操作有着重要的意义。下面我就这三种日志的文件的作用,内容、产生、释放的时机、以及对应的物理文件五个方面来给大家做说明。
重做日志redo log。
一、作用:确保事务的持久性,防止在发生故障的时间点,尚有脏页未写入磁盘。在重启mysql 服务的时候,根据redo log 进行重做,从而达到事务的持久性特征。二、内容:物理格式的日志,记录的是物理数据页面的修改信息。其Redo log 是顺序写Redo log file的物理文件当中的。三、什么时候产生:事务开始之后就会产生redo log,redo log 落盘并不是随着事务的提交才写入的,而是在事务的执行过程当中便开始写入redo log 文件中。四、什么时候释放:当对应的事务的脏页写入到磁盘之后,redo log 的使命已经完成了。重做日志占用的空间就可以重用,即被覆盖。五、对应的物理文件:默认情况下,对应的物理文件位于数据库的data目录下的iblogfile1 一和iblogfile2
回滚日志 undo log
一、作用:保存事务发生之前数据的一个版本,可以用于回滚,同时可以提供多个版本并发控制下的读,即MVCC也叫做非锁定读。二、内容:逻辑格式的日志在执行undo的时候,仅仅是将数据从逻辑上恢复至事务之前的状态,而不是从物理页面上的操作实现的。这一点不同于redo log。三、什么时候产生:事务开始之前将当前的版本生成undo log,undo也会产生redo 来保证undo log的可靠性。四、什么时候释放?当事务提交后,undo log并不能立马被删除,而是放入到一个待清理的链表,由purge线程判断是否由其他事务在undo段表中上一个事务之前的版本信息决定是否可以清理undo log 的日志空间。五、对应的物理文件:mysql5.6之前,undo段表空间位于共享表空间的回滚段中。共享表空间的默认的名称是ibdata,位于数据库文件目录。mysql5.6之后,undo 表空间可以配置成独立的文件,但是提前需要在配置文件当中配置完成数据库初始化后生效且不可改变,undo log文件的个数。如果初始化数据库之前没有进行相关的配置,那么将无法配置成独立的表空间。
二进制日志 binlog
一、作用:用于复制,在主从复制中从库利用主库上的binlog进行重播,实现主从同步。同时用于数据库的基于时间点的还原。二、内容: 逻辑格式的日志可以简单认为就是执行过的事务中的SQL语句,但又不完全是SQL语句这么简单,而是包括了执行SQL增删改的反向信息。也就意味着delete对应的delete本身和其反向的insert,update对应着update执行前后的版本的信息,insert 对应的insert和delete本身的信息。在使用mysqlbinlog解析binlog之后,一切都会真相大白。因此可以基于binlog做到类似于oracle 的闪回功能,其实都是依赖于binlog中的日志记录。三、什么时候产生:事务提交的时候,一次性将事务中的SQL语句按照一定的格式记录到binlog中。这里与redo log 很明显的差异就是redo log 并不一定是在事务提交的时候刷新到的磁盘。redo log 是在事务开始之后就开始逐步的写入磁盘。因此对于事务的提交,即便是较大的事务提交是很快的。但是在开启了binlog的情况下,对于较大的事务提交可能会变得慢一些。这是因为binlog在事务提交的时候一次性写入造成的。这些是可以通过测试来验证的。四、是什么时候释放?binlog默认保持时间由参数expire_log_days配置,也就是对于非活动的日志文件在生成时间超过expire_log_days配置的天数之后会被自动删除。五、对应的物理文件:配置文件的路径为log_bin_basename。binlog日志文件按照指定大小,当日志文件达到指定的最大的大小之后,进行滚动更新,生成新的日志软件。对于每个binlog日志文件,通过一个统一的index 文件来组织。
视频中老师提到 “redo log产生时机为:事务开始之后就会产生redo log,redo log 落盘并不是随着事务的提交才写入的,而是在事务的执行过程当中便开始写入redo log 文件中”。并且在 BV1wa411Z7FB 中也是提到了这一点。但是我看很多资料是如果一个事务会有多个增删改操作,那么会有多个redo log,多个SQL操作都执行完了,才把一组redo log给写入到redo log buffer中。然后根据一定的策略把redo日志从redo log buffer里刷入到磁盘文件里去。这个策略是通过innodb_flush_log_at_trx_commit来配置的,0 表示redo log thread每隔1s会将redo log buffer 中的数据写入redo log文件。 1 表示每次事务提交时,都会触发redo log thread将日志缓冲中的数据写入文件,并“flush”到磁盘。该设置下是最安全的模式。2 表示提交事务的时候,把redo日志写入磁盘文件对应的os cache缓存里,不刷新到磁盘,由MYSQL后台线程定时刷新等机制。不知道跟老师讲的是不是有冲突还是这是两个东西,没有关联。
进阶面试题:你有没有做过MySQL的读写分离?
问你有没有做过mysql 读写分离这个问题主要考察面试者是否具备高并发处理的经验。在高并发处理场景下,肯定是要做的,啥意思呢?因为实际上大部分的互联网公司啊。一些网站或者是APP,其实都是读多写少。所以针对这个情况,就是写一个主库,但是主库挂多个从库,然后从多个从库中来读,那不就可以支撑更高的读并发压力了吗?那如何实现mysql 的读写分离呢?其实很简单,就是基于主从复制架构,简单来说就是搞一个主库,挂多个从库。然后我们就可以单单只是写主库,然后主库会自动把数据同步到从库中去。那我们来说说主从复制的原理又是怎样的呢?主库将变更写入binlog的日志,然后从库连接到主库之后,从库有一个IO线程将主库的binlog日志拷贝到自己本地,写入一个relay中继日志。接着从库有一个SQL进程,可以从中继日志读取binlog日志,然后执行binlog日志当中的内容,也就是在自己本地再执行一遍SQL,这样就可以保证自己跟主库的数据是一样的。这里有一个非常重要的一点,就是从库同步主库数据过程的过程当中是串行化的。也就是说主库上并行的操作在主库上被串行执行,这也是一个非常重要的点。由于从库从主库拷贝日志以及串行上执行SQL的特点,在高并发场景下,从库的数据一定会比主库慢一些是有延时。所以经常出现刚写入主库的数据可能是读不到的,要过几十毫秒甚至几百毫秒才能读取到。而且这里还有另外一个问题。就是如果主库突然宕机,然后恰好数据还没有同步到从库,那有些数据可能在从库上是没有的,有些数据可能就丢失了。所以mysql 实际上在这一块有两个机制,一个是半同步复制,用来解决主库数据丢失问题,另一个是并行复制,用来解决主从同步延时问题。这个所谓的半同步复制也叫semi-sync复制。就是主库写入binlo日志之后就会强制实时立即将数据同步到从库。从库将日志写入到自己本地的relay-log之后,接着会返回一个ACK给主库,主库接收至少一个从库的ACK之后才会认为写操作完成了。所谓的并行复制,指的是从库开启多个线程并行读取relay-log中不同库的日志,然后并行存放不同库的日志,这是库级别的并行。你可以说以前线上确实处理过,因为主从同步延迟问题而导致的线上bug 属于小型的生产事故,是这么个场景。有个小伙伴是这样写代码逻辑的,先插入一条数据,然后把它查出来,然后更新这条数据。在生产环境高峰期写并发高达两千每秒。这个时候主从复制延迟大概是在小几十毫秒。线上会发现每天总有一些数据我们期望更新一些重要的数据的状态。但在高峰期的时候却发现没有更新。用户跟客服反馈,而客服就会反馈给我们。我们通过mysql 的命令,seconds_behind_master可以看到从库复制主库的数据落后了几毫秒。一般来说,如果主从延迟较为严重,有以下解决方案。一、分库:将主库拆分为多个主库,每个主库写并发就减少了几倍。此时主库延迟可以忽略不计。二、打开mysql支持的并行复制,多个库并行复制。如果说某个库的写入并发就是特别高,单库写并发高达两千每秒,并行复制还是没有意义。三、重写代码,写代码的同学要慎重,插入数据立马查询,可能查询不到。四、如果确实存在,必须先插入,立马要求就查询到,然后立马就反过来执行一些操作。对这个查询设置直连主库,不推荐这种做法,你要是这么搞,读写分离的意义就丧失了。
Java面试题:hash索引和B+树索引的区别是什么?
Hash索引和B+树索引的区别。首先要知道Hash索引和B+索引的索引底层实现原理。Hash索引底层就是Hash表进行查找的时候,调用一次Hash函数就可以获取到相应的键值之后进行回表查询获得实际的数据。B+树底层实现是多路平衡查找数对于每一次的查询都是从根结点出发,查找到叶子结点方可得到所查询的键值,然后根据查询判断是否需要回表查询数据。那么可以看出,它们之间有以下的不同。第一点,Hash索引进行等值查询更快,但是却无法进行范围查找。因为在Hash索引中,经过Hash函数建立索引之后,索引的顺序与原顺序无法保持一致,不能支持范围查询。B+树的所有节点皆遵循左节点小于父节点,右节点大于父节点多。多叉树也类似天然支持范围。第二:Hash索引不支持使用索引进行排序原理也是因为节点的顺序性。第三Hash索引不支持模糊查询以及多列索引的最左前缀匹配原理也是因为Hash函数是不可预测。AAAA和AAAABB的索引没有相关性。第四:Hash索引任何时候都避免不了回表查询数据。而B+在符合某些条件,如聚簇索引,索引覆盖等的时候可以只通过索引完成查询。第五,一个哈希索引虽然在等值查询上比较快,但是不稳定,性能不可预测。当某个键值存在大量重复的时候发生Hash冲突,此时效率可能极差,而B+数查询的效率比较稳定。对于所有的查询都是从根结点到叶子结点,且树的高度较低。因此大多数情况下直接选择B+树 索引,可以获得稳定且向往的查询速度,而不需要使用Hash索引。
中级面试题:BIO、NIO、AIO之间的区别以及各自的应用场景?
要回答这个问题。首先我们得了解IO模型的分类,他们主要有同步IO和异步IO阻塞IO和非阻塞IO同步阻塞,简称BIO,同步非阻塞简称NIO,异步非阻塞,简称AIO。它们之间的区别:第一个BIO同步阻塞IO模式,数据的读取和写入必须总是在一个线程内等待一起完成。这里使用那个经典的烧开水的例子,这里假设一个烧开水的场景,有一排水壶在烧开水。BIO的工作模式就是。叫一个线程停留在一个水壶,那直到这个水壶烧开才去处理下一个水壶。但是实际上线程在等待水壶烧开的时间段什么都没有做,第一个NIO同步非阻塞,同时支持阻塞和非阻塞模式。但这里我们从其同步非阻塞的IO模式来说明,那什么叫做同步非阻塞呢?如果还拿烧开水来说,NIO的做法是叫一个线程不停的轮询每一个水壶的状态,看看是否有水壶的状态发生了变化,从而进行下一步的操作。第三,AIO异步非阻塞IO模式。异步非阻塞与同步非阻塞的区别在哪里呢?异步非阻塞无需一个线程去轮询所有的IO操作的状态改变。在相应的状态改变以后系统会通知对应的线程来处理。对应到烧开水中,就是为每一个水壶上面装了一个开关。水烧开之后水会自动通知我们水烧开了,这是一个同步和异步的区别。同步发送一个请求,等待返回再发送下一个请求,同步可以避免出现死锁脏读。异步发送一个请求,不等待返回,随时可以再发送下一个请求,可以提高效率,保证并发。第五,阻塞与非阻塞。阻塞:传统的IO流都是阻塞的。也就是说当一个线程调用read 或write 方法时,该线程将被阻塞,直到有一些数据读取或写入。在此期间,该线程不能执行其他任何任务,在完成网络通讯进行IO时,由于线程会阻塞,所以服务端必须为每一个客户端都提供一个独立的线程进行处理。当服务端需要处理大量的客户端的时候,性能急剧下降,非阻塞:Java NIO是非阻塞的,当线程从某个通道进行读取数据的时候,若没有数据可用,该线程会去执行其他的任务。线程通常将非阻塞IO的空闲时间用于在其他通道上执行IO操作。所以单独的线程可以管理多个输入和输出。因此IO可以让服务器使用一个或者有限几个线程来同时处理连接到服务器上的所有的客户端。最后就是BIO、NIO、AIO的适应场景。BIO方式适用于连接数目比较小且固定的架构。这种方式对于服务器的资源要求比较高,并发局限在应用中。JDK1.4以前的唯一选择。NIO方式适用于连接数量多且连接比较短的架构,比如聊天服务器并发局限于应用中。编程比较复杂。AIO方式适用于数据连接数多且连接比较长的架构,比如相册服务器充分调用OS参与并发操作,编程比较复杂,JDK1.7以后开始支持。
Java高频面试题:谈谈你对spring aop的理解?
AOP的全称叫做面向切面编程。面向切面编程是面向对象编程的补充和升华。面向对象编程是采用纵向抽取,比如类A与类B他们存在大量的属性和方法的冗余。则我们可以通过找到其共同的父类,将冗余的方法和属性抽取到父类中子类继承父类,从而达到代码复用的目的。但是面向对象编程抽取是基于类级别的,也就是说在类级别存在代码冗余才能够更好的使用面向对象进行抽取。那如果在多个业务方法当中存在大量的冗余的代码逻辑,比如事务的回本和提交、操作流水等。这个时候我们就无法直接通过面向对象来抽取冗余的部分。然后AOP面向切面编程就可以解决这个问题,将冗余的代码抽取出来,形成增强。然后通过定义切点的方式就可以实现无侵入的方法级别的增强。最重要的是实现了代码的高度解耦,而切点和增强我们统称为切面。固我们称之为面向切面编程。大家可以看到AOP是基于方法级别的抽取,那么抽取的力度要比面向对象要小。其次的话就是如何实现面向切面编程,既AOP思想底层的话主要有两种技术,一种是通过JDK动态代理,一种是通过CGLIB动态代理。JDK动态代理的话是利用拦截器。必须实现invocationHandler接口加上反射机制,生成一个代理接口的匿名类,在调用具体的方法前调用,用invokeHandler来处理。而CGLIB动态代理则是利用ASM框架将代理类生成的class 文件加载进来,通过修改字节码,形成生成指令的方式来实现的。所以如果想要实现JDK动态代理,那么代理类必须实现接口,否则就不能使用。如果想要使用是CGLIB动态代理,那么代理类不能够用final修饰类和方法。当然我们也可以在spring 中通过配置其强制使用CGLIB动态代理
Spring Boot自动配置的原理?
学过Spring Boot的小伙伴都知道,当我们使用idea 工具创建Spring Boot项目的时候,在项目当中会生成一个全局的配置文件,application.properties 或application.yml。我们各种属性设置都是在这个文件当中去配置的。比如应用的名称、端口号等一系列的配置。那当我们去启动Spring Boot项目的时候,这些配置就都生效了。那这到底是怎么做到的呢?今天我们就来讲讲Spring Boot自动配置的原理。首先Spring Boot项目最重要的就是启动类上的注解。@SpringBootApplication. Spring Boot通过运行这个类的main方法来启动Spring Boot的应用。所以我们就得从这个注解入手,打开这个注解的源码。我们发现SpringBootApplication注解是一个组合注解,主要由原注解和三个注解组成。那这里我们主要跟大家讲解一下三个注解。第一个@SpringBootConfiguration。该注解表示,这是一个Spring Boot的配置类。第二个注解,@ComponentScan. 开启组件扫描。第三个注解@EnableAutoConfiguration,翻译过来的意思就是开启自动配置,重点就在这个配置当中打开@EnableAutoConfiguration注解的定义主要包括一个@AutoConfigurationPackage 注解。该注解主要作用就是将主程序所在的类以及主程序类所在的包以及所有子包下的组件扫描到Spring Boot容器中。另一个组件就是@Import({AutoConfigurationImportSelector.class})。该注解给当前配置类导入自动配置类,其导入的AutoConfigurationImportSelector类中的selecImports 方法,通过SpringFactoriesLoad.loadFactoryNames 扫描所有具有NETA-INF/spring.factories 下面的jar包,然后返回需要导入的组件的全类名。我们来看一下NETA-INF/spring.factories 这类文件。这个文件是以Map形式存放的,key 是EnableAutoConfiguration类的全类名value 是一个xxxAutoConfiguration的类名的列表。可以看到,EnableAutoConfiguration下有很多的类,这些是我们项目启动的时候进行自动配置的类。那总结一下呢,就是将类路径下面的NETA-INF/spring.factories 里面所有配置的EnableAutoConfiguration的值加入到spring 容器中。下面我们就以RedisAutoConfiguration,redis的自动配置类为例,说明一下自动配置的原理。该注解主要包括四个注解和两个Bean。第一个注解:Configuration(Proxy.BeanMethods=false )表示这是一个配置类与以前编写的配置文件一样,也可以给容器中添加组件。第二个注解:@ConditionalOnClass({RedisOperations.class})判断当前项目有没有这个类RedisOperations,没有则配置不生效。第三个注解:@EnableConfigurationProperties({RedisProperties.class})。将配置文件当中对应的值和RedisProperties类属性绑定起来,并将RedisProperties加入到IOC容器中。第四个注解:@Import({LettuceConnectionConfiguration.class, JedisConnectionConfiguration.class})。按需导入我们redis 客户端连接配置。同时将我们的RedisTemplate和StringRedisTemplate的这两个redis 模板类实例化到我们的Bean工厂。从而我们就可以通过注入的redis 模板类,对redis 进行相关的操作了。
Java面试题:谈谈Java中的类加载器以及类加载机制?
首先阐明一下类加载器的作用。类加载器负责将.class文件,不管跟class文件是在jar包当中还是在本地磁盘当中,还是通过网络获取等等,加载到内存当中,并为之生成对应的Java.lang.class对象。一个类被加载到JVM当中,就不会第二次加载了。那好奇的同学会问,那JVM是怎么判断是同一个类的呢?其实啊每个类在JVM当中是使用全类名与类加载器联合为唯一的i d。所以如果同一个类使用不同的类加载器,可以被加载到虚拟机中。但彼此不兼容。其次就是JVM当中主要包括三类类加载器。第一类加载器是BootStrap Classloader 根类加载器,它负责加载java 的核心类库。根类加载器不是Classloader的子类。是由实C++实现的。根类加载器负责加载JDK根目录下面的jre/lib下的jar包,以及由虚拟机参数-X bootclasspath指定类。第二类类加载器。Extension Classloader 扩展类加载器,负责加载JDK目录下jre/ext或java.ext.dirs 系统指定的目录的jar包。大家可以将自己写的工具包放到这个目录下,可以方便自己使用。第三类类加载器System Classloader系统类加载器又叫应用类加载器,负责加载来自java 命令的-Classpath选项,java.class path 系统属性或Class path环境变量所指定的jar包和类路径。程序可以通过ClassLoader.getSystemClassLoader()来获取系统类加载器。如果没有特别指定,则用户自定义的类加载器,默认都是以系统类加载器作为父加载器。最后呢就是类加载器的机制。一:全盘负责。当一个类加载器负责加载某个class 时,该class 所依赖和引用的其他class也由该类浏览器负责载入,除非显示使用另一个类浏览器来载入。二:父类委托也叫双亲委派,先让父加载器试图加载该class。只有在父类加载器无法加载时,该类加载器才会尝试从自己的类加载路径当中加载该类。三:缓存机制。缓存机制会将已经加载的class 缓存起来。当程序中需要使用某个class 时。类加载器先从缓存当中搜索该class。只有当缓存中不存在该class 时,系统才会读取该类的二进制数据,将其转换为class对象存入到缓存中。这就是为什么我们更改了Class以后需要重启JVM才生效的原因。
进阶面试题:谈谈MySQL中事务实现的原理?
最近有小伙伴在面试时遇到这样一个面试题,要去谈谈MySQL事务实现的原理。这个面试题主要考察面试者对事务的原理的了解的程度,当然可以从事务的ACID特征的底层原理来进行回答。ACID就是原子性、一致性、隔离性以及持久性。大家可以这样回答:
从宏观方面来讲,事务主要实现了两大效果,一个是可靠性,即数据库要保证当insert或update 操作时抛异常或者数据crash 的时候,需要保证数据的操作前后的状态。想要做到这个需要。我们知道我修改之前和修改之后的状态,所以就有了undo log 和redo log。那另一个就是并发处理,也就是说当多个并发请求过来的时候,并且其中有一个请求是对数据进行修改。为了避免读到脏数据,所以需要对事务之间的读写进行隔离。至于隔离到啥程度,就得看业务系统的场景了。实现这个的话就要用到mysql的隔离级别。
从微观方面来讲的话呢,就是ACID四个特征,各自底层的实现原理。第一个就是原子性的实现。一个事务必须视为不可分割的最小的工作单位。一个事务中所有的操作要么全部成功提交,要么全部呈失败回本。对于一个事务来说,不可能只执行其中的部分操作,这就是事务的原子性。相信大家都有了解。那么数据库是怎么实现的呢?就是通过回滚操作。所谓的回滚操作就是当发生错误异常或者显示执行rollback 语句,需要将把数据还原到原先的模样。这样这个时候就需要用到undo log进行回滚。其保存了事务发生之前数据的一个版本,可以用于回滚,同时提供了多版本并发控制下的读。既MVCC也叫做非锁定读。第二个特征,持久性,事务一旦提交其所做的修改。会永久性的保存在数据库当中。此时即使系统崩溃,修改的数据也不会丢失。根据mysql 数据存储的机制,mysql 表的数据是存储在磁盘上。因此,想要存取的时候都需要经过磁盘IO,然而即使是使用SSD,磁盘IO也是非常消耗性能的。为此呢为了提高性能,InnoDB提供了缓存池,Buffer Pool,Buffer Pool中包含了磁盘数据页的映射,可以当做缓存来使用。当我们读数据时,首先会从缓存池当中读取。如果缓存池当中没有,则从磁盘当中去读取,放入到缓存池中。当我们写数据时会首先写入缓存池。缓存池当中的数据会定期同步到磁盘中。上面这种缓存值的措施虽然在性能方面带来了质的飞跃,但是它也带来了新的问题。当MYSQL系统断机断电的时候,可能会丢数据。因为我们的数据已经提交了,但此时是在缓存池里面还没有来得及在磁盘持久化。所以我们急需一种机制来存一下已提交事务的数据。为了恢复数据使用这种机制就是redo log日志,其作用就是为了防止在发生故障的时间点尚有脏页未写入磁盘。在重启mysql 服务的时候,根据redo log进行重做,从而达到事务的持久性特征。具体的原因的话是事务开始之后就产生了redo log,redo log 落盘并不是随着事务的提交才写入的,而是在事务的执行过程当中便开始写入redo log文件中。第三个特征,隔离性的实现。隔离性是事务ACID阶段里面最复杂的一个。在SQL的标准定义里面定义了四种隔离级别。每一种隔离级别都规定了一个事务的修改,哪些是事务之间可见的,哪些是不可见的。级别越低,隔离级别可以执行越高的并发,但同时实现复杂度及其开销也就越大。mysql 隔离级别有以下四种,他们的级别由低到高分别是。read uncommited未提交读, read commited 提交读。还有一个就是repeatable read 可重复读,Serializable串行化。隔离性跟前面两个特征有所区别,原子性和持久性是为了要实现数据的可靠性保障。比如要做到宕机以后的恢复,以及错误以后的回滚。那么隔离性要做到什么呢?隔离性是要管理多个并发读写请求的访问顺序。这种顺序包括串行或者是并行。首先我们看一下未提交读的实现,在未提交读隔离级别下事务中的修改。即使没有提交对其他事务也是可见的,事务可以读取未提交的数据,造成脏读。因此。读不会加任何锁,因为读不会加任何锁,所以写操作在读过程当中修改数据会造成脏读。好处是可以提高并发的性能,能够做到读写并行。换句话说,读的操作不能排斥写操作。那第二个就是,提交读隔离级别。一个事务修改在他提交之前的所有修改对其他事务都是不可见的,其他的事务能得到已提交的修改的变化。在很多场景下,这种逻辑呢其实是可以接受的。InnoDB在read committed 使用排他锁,读取数据不加锁,而是使用了MVCC机制。换句话说就是它采用了读写分离机制。但是该级别会产生不可重复读以及幻读问题。第三个就是重复读。也就是MYSQL默认的隔离级别。mysql 有两种机制可以达到这种隔离级别的效果,分别是采用读写锁以及MVCC,读写锁实现是通过读读并行读写串行的机制来实现的。而MVCC是读写并行,但是通过多次读取只生成一个版本,读到的自然是相同的数据的方式来实现的。第四一个是串行化,是通过读写都加排他锁来实现的。那最后一个就是一致性的实现原理则是通过回滚以及恢复和在并发环境下隔离共同作用来做到一致性的。
Java面试百日百更MySQL中行锁和表锁的含义与区别?
mysql常用的引擎有myisam和innodb。innodb是mysql 默认的引擎。mysql 当中加锁的方式有哪一些?我们可以这样回答,先说明一下mysql 当中加锁的方式有哪一些。
第一种的话就是隐式加锁,myisam在执行查询语句前会自动给涉及所有的表加上读锁而执行增删改操作前会自动给涉及的表加解锁,这个过程并不需要用户干预,因此用户一般不需要直接用lock table 命令给myisam表显示加锁。第二个就是显示加锁啊。比如共享读,即读锁的写法,lock in share model, 例如如下的SQL语句:select * from t where A>10 lock in share model。排他锁记即读写锁的写法:for update,例如如下的SQL语句: select * from t where A>10 for update。 然后介绍一下表锁的特点,表锁不会出现死锁,发生锁冲突的几率高,并并发低,主要针对myisam引擎,myisam在执行查询操作前会自动给设计的所有的表加读锁。在执行增删改操作前会自动给涉及的表加写锁。mysql 的表级锁有两种模式。第一种表共享读锁,第二种表独占写锁。读锁会阻塞写,写锁会阻塞读和写。对于mysql的表的读操作不会阻塞其他进程对同一表的读请求,但是会阻塞对同一表的写请求。只有当读锁释放以后,才会执行其他进程的写操作。myisam表的写操作会阻塞其他进程对同一表的读和写操作,只有当写锁释放以后,才会执行其他进程的读写操作。myisam不适合做写为主的表的引擎。因为写锁后其他的线程不能做任何的操作,大量的更新会使得查询很难得到锁,从而造成永久阻塞。接着说一下行锁的特点,行锁会出现死锁,发生锁的几率低。但是并发高。在mysql的innodb引擎支持行锁,与oracle 不同,mysql 的行锁是通过索引加载的。也就是说行锁是加在索引相应的行上,要是对应的SQL语句没有做索引,那则会进行全面扫描。行锁就无法实现,取而代之的是表锁。那么此时其他事务无法对该表进行更新和插入操作,然后就是for update。如果在一条SQL语句后面加上for update。则查询到的数据会被加上一条排他锁。其他的事务可以读取,但是不能进行更新和插入操作。行数的使用需要注意哪些呢?第一个行数必须要有索引才能实现,否则会进行自动锁全表。那么就不能不叫行锁了。第二个就是两个事务不能锁同一个索引,第三个insert delete update 在事务当中会自动的默认加上排他锁。那么行锁的场景有哪些呢?
比如A用户进行消费操作,设置成先查询该用户的账户余额,如果余额充足,再进行后续的扣款操作,那么这种情况的查询就应该加上写锁。否则的话B用户在A用户查询以后,消费以前先一步将A用户的账上的钱转走。那么此时A用户已经进行了用户余额是否充足的判断,则可能会出现余额不足。但是扣款成功的情况。那么为了避免此情况,需要在A用户操作该记录的时候进行for update加锁。给大家留一个疑问,mysql 当中还有一种锁叫做间隙锁,大家知道它的含义吗?欢迎大家在评论区讨论。那最后说一下mysql 所使用的一般性的建议。一、尽可能的让所有的数据检索都通过索引来完成,避免无索引行锁升级为表锁。第二、一个合理的设计索引,尽量缩小锁的范围。第三、尽可能的减少索引条件,避免间隙锁。第四、尽量控制事务的大小,减少锁定资源的量和时间的长度。
MySQL按照锁粒度分可以分为全局锁、表级锁、行锁。全局锁命令是FTWRL (Flush tables with read lock)此命令使整个库处于只读状态,主要用途是保证备份的一致性,杀伤性极大,不要随意使用,要在备库使用。表级锁有数据锁和元数据锁(matadata lock,MDL锁)数据锁的语法是lock tables … read/write。可以用unlock tables主动释放锁。事务访问数据时,会自动给表加MDL读锁,事务修改元数据时,会自动给表加MDL写锁,读读不互斥,读写和写写互斥,保证变更表结构操作的安全性。临键锁(Next-key Locks)间隙锁(Gap Locks)记录锁(Record Locks)我认为是innodb行锁的三种实现,innodb能够在 REPEATABLE READ 隔离级别下是解决幻读的问题(不是完全解决,连续两个快照读中出现当前读可能会出现幻读)关键是靠MVCC和Next-key Locks来实现 快照读是通过MVCC解决幻读,当前读是通过Next-key Locks(间隙锁 + 记录锁)来实现。文中老师讲到了行锁是加在索引相应的行上(记录锁的情况),而幻读是一次事务中前后数据量发生变化,譬如事务A执行两次SQL语句 select * from t where id >10 and id <20 中间,事务B执行了一条 insert into t value(13,”zs”);事务A在第一次执行读取操作时,那些幻影记录尚不存在,我们无法给这些幻影记录加上记录锁 Innodb会使用Next-key Locks,具体实现:当SQL执行按照索引进行数据的检索时,且查询条件为范围查找(between and、<、>等),且有数据命中时,该SQL语句事务操作加上的行锁为Next-key locks,当查询范围或等值查询且记录不存在,临键锁退化成Gap锁。锁住数据不存在的区间 (左开右开);当sql执行按照唯一性(Primary key、Unique key)索引进行数据的检索时,查询条件等值匹配且查询的数据是存在,退化成Record锁。
Java面试百日百更:如何保障RabbitMQ的消息的可靠性?
RabbitMQ一个消息会经历四个节点。一、生产者发出去以后保证到达MQ二,MQ收到消息以后保证分发到消息对应的Exchange, 三、Exchange分发消息的入队以后保证消息的持久性。四、消费者收到消息以后,保证消息的正确消费。只有保证这四个节点的可靠性,才能够保证整个系统的可靠性,从而保证消息不会丢失。首先我们来看一下如何保证我们的生产者一定将消息发送到我们MQ的server端。生产者在发送消息之后,可能会由于网络闪断等各种原因导致我们的消息并没有发送到MQ的server端。但这个时候我们的生产端又不知道我们的消息没有发送出去,这就会造成消息丢失。为了解决这个问题,RabbitMQ引入了事务机制和发送方确认机制。由于事务机制过于消耗性能,所以一般不用,一般采用发送方确认机制。那什么是发送方确认机制呢?就是我们的消息发送到MQ的server端以后,我们的MQ会回一个确认收到的消息给我们。这样生产者就能够清楚的知道我们的消息到底有没有发送成功。具体的流程的话是这样的:首先在我们的生产端开启发送方确认机制,然后在生产者发送消息前,我们先将要发送的消息写入数据库的消息表当中,状态标记为发送中,然后发送者开始发送消息,当生产者收到MQ server端的确认回调时,我们将数据库当中消息的状态改为发送成功。然后我们还需要开启一个定时任务,扫描我们消息表当中未发送成功的消息,将未发送成功的消息重新投递。如果重试一定次数以后,仍然无法发送成功,则会标记消息的状态为发送失败,最后只能做兜底,做人工处理了。那么接下来的话就是如何保证MQ收到消息,保证分发到对应的Exchange以及exchange分发消息到队列以后,如何保证消息的持节性。这里的话主要会出现两类问题。第一个就是消息找到对找不到对应的Exchange,第二个找到了对应的Exchange但是找不到对应的Queue。这两种情况都可以用RabbitMQ提供的Mandatory参数来解决,它会设置消息投递失败的策略。有两种策略,一种是自动删除,一种是返回到客户端。我们既然要做可靠性,当然是要设置为返回的客户端。当我们拿到被退回的所有消息,然后再做进一步的处理。比如放入到一个新的对应单独处理。那这里还要考虑一个问题,就是消息入队列以后,如果MQ宕机了怎么办?虽然这是一个很小概率的问题啊,比如MQ宕机了或者关闭了,但也必须考虑要解决这个问题的话,就必须要对消息做持久化,以便我们的MQ重启以后,消息还能够重新恢复过来。消息的持久化要做的话,那不但要做消息的持久化,还要做队列的持久化和Exchange的持久化。那最后的话就是如何保证消费者正常的消费消息了。要解决这个问题的话,RabbitMQ也有对应的机制。那就是消费者消息确认机制,打开手动消息确认之后,只要我们的这条消息没有成功消费,无论中间出现消费者宕机还是代码异常,只要连接断开之后,那么这个消息还未被消费。那么这个消息就会被重新放入到队列再一次被消费。当然这也可能会出现重复消费的情况。这里可以通过redis 来保证我们消息的幂等性。所以一般重复的消息都会被幂等接口给拦截掉。
Java进阶面试题:谈谈OpenFeign的整体执行流程?
在我们使用OpenFeign作为远程调用客户端的时候,它的整体执行流程如下。一、通过启动类上的@EnableFeignClients注解开启Feign的装配和远程代理实例的创建。在@EnableFeignClients注解源码中可以看到它导入了一个FeignClientsRegister的类。该类的作用用于扫描@FeignClient注解过的RPC接口。二、通过对@FeignClient注解接口扫描创建远程调用的动态代理实例。FeignClientsRegister会进行包扫描,扫描所有包下面@FeignClient注解过的接口,创建RPC接口的factorybean工厂类实例,并将这些factorybean注入到Spring IOC容器中。如果应用的某些地方需要注入RPC接口的实例,比如被@Resource引用,Spring就会通过注册的factorybean工厂实例的getObject()方法获取RPC接口的动态实例。在创建RPC接口动态代理实例时,Fegin会为每一个RPC接口创建一个调用处理器,也会为每个接口的每一个RPC方法创建一个方法处理器,并且将方法处理器缓存在调用处理器的dispatch 映射成员中。在创建动态代理实例时,Fegin也会通过RPC方法的注解,为每一个RPC方法生成一个request Template 请求模板实例。request Template 中包含了请求的所有信息,如请求的URL、请求的类型,比如GET请求还是POST的请求,请求的参数等。三、发生RPC调用时,通过动态代理实例类完成远程的provider的HTTP 调用。当动态代理类的方法被调用的时候,Fegin会根据RPC方法反射实例,从调用处理器的dispatch 成员中获取方法处理器,然后由methodHandle 方法处理器开始HTTP请求处理。methodHandle会根据实际的调用参数通过request template 模板实例生成request请求实例。最后将request请求实例交给fegin.client 客户端进行进一步的完成HTTP请求处理。四、在完成远程HTTP调用前,需要进行客户端负载均衡的处理。在spring cloud微服务架构中,同一个provider 微服务一般都会运行多个实例。所以说客户端的负载均衡能力其实是必选项,而不是可选项。生产环境下fegin必须和ribbon 结合一起使用。所以方法处理器methodHandle的客户端client的成员必须具备负载均衡能力的LoadBalanceFeginClient的类型,而不是完成HTTP请求提交的ApacheHttpClient的等类型。只有在负载均衡计算出最佳的provider 实例之后,才能开始HTTP请求的提交。在LoadBalanceFeginClient内部有一个叫delegate 的委托成员,其类型可能为fegin.client.default或者ApacheHttpFeginClient、OkHttpClient 等,最终由delegate 客户端委托成员完成HTTP请求的提交。
Java面试百日百更:谈谈你对HashMap的理解?
HashMap是一个存储key value 键值对的容器,key 唯一,value 不唯一。其内部的数据结构采用的是散列表,即数组加链表。这么做的目的就是充分利用数组和链表各自的优点,规避其各自的缺点,从而实现查询效率无限接近O(1)。但是如果我们就直接采用数组加链表,不做任何的处理,是无法达到我们想要的查询效率O(1)。那到底要做哪一些处理,要解决什么问题呢?
- 第一个问题,如何减少哈希冲突
尽最大可能使得存在在散列表中的元素足够散列。因为哈希冲突越严重了,甚至说所有的元素都集中在某一个哈希索引处,那么严重影响性能。这里采取的第一个优化就是干扰函数的使用。通过干扰函数将元素哈希值进行高低位的异或,使得参与运算的哈希值同时具备了原哈希值高位和低位的特征,增大了随机性。那么第二个优化就是散列数组需要一个2的倍数的长度。那这样的数组长度会出现一个0111,除高位以外都是1的特征,也是为了散列。同时还提高了我们计算哈希效率。
- 那第二个问题,数组越小,碰撞越大,数组越大,碰撞越小,时间和空间如何取舍?
上面第一个的话其实也可以将数组定义的足够大,因为它其实是一个int类型,取值范围有将近四十亿的长度,所以不想把数组定义成这么大,内存也放不下。在HashMap当中默认储存的大小为16。那这里还有一个问题,就是随着HashMap当中数据的增加,在初始容量不变的情况下再怎么优化,其碰撞肯定也是越来越高。那这个需要我们在合适的时机对数组进行扩容,以减少哈希冲突。那么HashMap当中默认的扩容阀值为0.75,这只是一个理论的参考值,太小了扩容太频繁,太大了扩容成本太高,这里的话采取的是一个折中的阀值0.75,使用者可以根据自己的实际的使用场景进行修改。
- 第三个问题就是HashMap如何解决哈希冲突。
那么第三个问题就是HashMap当中出现哈希冲突。我们是通过拉链法来处理的,但是当拉链法最终列表的长度太长。同样性能还是会受到影响。因为如果需要从链表当中定位到某个数据,时间复杂度就是O(n) 。链表越长,性能越差。JDK1.8把过长的链表也就是8个优化为自平衡的红黑树结构,以此让定位元素的时间复杂度优化接近于O(logn) 那这样来提高元素查找的效率,但也不是完全摒弃链表。因为在元素相对不多的情况下,链表的插入速度更快。所以综合考虑下设定阀值为8或者元素个数达到64才进行红黑树的转换操作。当元素逐步减少,元素不多的情况下,元素个数为6时,红黑树退化为链表。之所以在链表转化为红黑树与红黑树退化为链表的阈值不同,主要是考虑到如果哈希碰撞次数在8附近徘徊,会一直发生链表与红黑树的相互转换。为了预防这种情况的发生,故设置为6。
- 那么最后就是JDK1.8对于HashMap都做了哪些优化?
一、数组加链表改成了数组加链表或红黑树。二、链表的插入方式,从头插法改成了尾插法。简单说就是插入时,如果数组位置已有元素,JDK1.7将先元素放到数组中,原始节点作为新节点的后继节点,JDK1.8遍历链表,将元素放置到链表的最后。三、扩容的时候JDK1.7需要对元素组当中的元素进行重新哈希,定位在新数组当中的位置。JDK1.8采用的是更简单的判断逻辑,位置不变或索引加旧容量大小。四、在插入时JDK1.7判断是否需要扩容再插入,JDK1.8先进行插入,插入完成以后再判断是否需要扩容。五、JDK1.7多线程下死循环、数据丢失、数据覆盖的问题,JDK1.8采用了尾插法解决JDK1.7死循环的问题,但是仍然有数据丢失和数据覆盖的问题。总而言之,言而总之,HashMap是线程不安全的,在多线程环境下不建议使用HashMap,尽量使用线程安全的HashMap。
3.16处 老师有个口误 ”所以综合考虑下,设定阀值为8或者元素个数达到64才进行红黑树的转换操作“ 。HashMap putVal()中有if (binCount >= TREEIFY_THRESHOLD - 1) treeifyBin(tab, hash); 表示链表元素个数超过8(划重点,是超过8,等于8不会执行后面方法)执行treeifyBin()方法,treeifyBin()中有if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY) resize(); 表示数组长度小于64执行扩容方法,不小于才进行树化。总结:HashMap当链表长度大于8且数组长度达到64才会将链表转换红黑树。
Java面试百日百更:项目中如何保障缓存与数据库的一致性?
java 面试百日百更第二天如何保障缓存与数据库的数据的一致性?首先在项目当中采用缓存机制的目的是为了提高系统的性能。这个在我们很多职业中的框架,包括操作系统、CPU等都有这种机制。但是由于请求的并发性和数据处理的顺序性更新数据是很有可能导致缓存和数据库的数据不一致的。那如何操作才能保证数据库和缓存的数据的一致性呢?在企业当中主要有以下几种方案。
方案一:我们可以采用双写模式,既更新数据库的同时也更新缓存。那不管是先更新数据库,还是先更新缓存,在并发请求下双写模式还是存在数据不一致的情况。
方案二:可以采用失效模式,即更新数据库时删除缓存。那如果是先删除缓存再更新数据库,在并发请求的情况下,缓存在被删除后更新数据库之前。被其他的请求更新了,则同样会出现数据不一致的情况。故这种方式不合适。那如果先更新数据库再删除缓存呢,在并发请求的情况下,理论上存在数据不一致的情况。但是缓存的更新常常要远远的快于数据库的更新。因此出现这种数据不一致的情况的几率是很低的。所以我们如果你想实现基础的数据缓存双写一致性的逻辑。那在大的绝大多数情况下,在不想做过多设计,增加太大工作量的情况下,建议先更新数据库,再删除缓存。那除了要考虑并发的情况以外,还得考虑更新数据库与删除缓存的原子性。如果删除缓存操作没有成功,那还会出现数据数据不一致的情况。所以可以兜底给数据加入过期时间。那么不一致时间最多只到过期,可以达到最终一致性。
方案三就是采用延迟双删,即删除缓存,更新数据库,延迟一段时间再删除缓存。或者更新数据库,删除缓存延迟一段时间再删除缓存,来解决双写模式中,由于并发问题导致的数据的不一致,那这里关键就在于延迟的时间不太好确定。另外还得考虑数据库读写分离时主从同步的时间。最后采用这种同步淘汰策略,吞吐量降低怎么办?当然可以把第二次删除作为异步的,自己起一个线程异步删除。这样写的请求就不用沉睡一段时间再返回。这么做加大了吞吐量,
方案四:使用canal定位binlog 日志异步删除缓存。先更新数据库,canal订阅binlog日志的变化,通过MQ消息队列,再通过一个非核心服务消费消息删除缓存。这样做即不影响核心业务的工作量,同时保障方案三中删除缓存一定能够成功。
Java面试百日百更:redis如何实现分布式锁?
Redis 如何实现分布式锁?在回答这个问题之前,我们首先搞清楚一下为什么需要使用分布式锁这个问题。我用这么一句话来概括,就是为了解决跨进程或跨JVM的线程安全问题,然后实现分布式锁的方式有redis 分布式锁、mysql 分布式锁,Zookeeper分布式锁。redis 分布式锁的本质就是在redis 中占坑。其次的话就是实现一个分布式锁,需要满足哪些条件呢?
首先第一个就是互斥性,在任何时刻都只能有一个客户端获得锁,第二个是健壮性。即使某一个客户端在获得锁的期间故障而没有释放锁,也要保证后续其他客户端可以获得出高。第三个高可用,只要大部分节点正常客户端就可以获得锁和释放锁。第四个唯一性,加锁和释放锁必须是同一个客户端的。那最后的话就*如何实现redis 分布式锁
- 如何实现redis 分布式锁。这里我们从两个方面来讲解。
第一个的话就是如何加锁。方式一:SETNX加DEL命令。SETNX是set if not exist 的缩写,通过set key 获得锁,使用完之后,通过delete 命令释放锁。如果遇到某个客户端获得锁期间出现了故障,DEL命令没有被调用怎么办?我们可以采用方式二,SETNX+expire。使用SETNX命令set key 之后,给key 设置一个过期时间。即使客户端在获得锁期间异常,也可以保证会在过期时间到来时自动释放。但是SETNX和expire是两个操作,不是原子性的。如果在SETNX和expire 期间,客户端异常了,还是会导致锁不会被释放,这个是什么意思呢?就是说我们的expire 是依赖于SETNX的执行结果。如果SETNX没有强到锁expire 是不应该执行的,事务里面没有if else 分支操作,要么全部执行,要么一个都不执行。所以此时不能使用redis 的事务解决。那应该如何解决呢?我们可以采取方式三:set 扩展参数的方式对set 命令加上扩展参数,使得SETNX加expire 操作能够一起执行。如果客户端在获得锁的期间故障了,那么也会在超时时间到来时释放。但是如果遇到SETNX+expire获得锁之后业务操作的执行时间超过了过期时间,就会导致锁释放之后业务操作还没有执行完,其他线程仍然可以获得锁,导致线程不安全。那这个该如何处理呢?那我们可以采用方式四:redisson。通过SETNX+Lua加锁,当遇到业务操作执行时间即将超过过期时间的话的时候,那么redis的看门狗机制会对过期时间进行续期。另外,使用redisson还能够解决单点故障问题,那这个是什么意思呢?就是说在我们的分布式情况下,客户端A在主节点set key 成功获得锁,主节点还未将锁同步到其他的从节点,主节点就故障了。那此时会选举出一个从节点升级为新的主节点。但是这个新的主节点并没有存在客户端A 获得成功的这个锁,此时客户端B请求获得锁成功,导致系统中同时有两个客户端持有锁,如何解决呢?那redisson通过redLock 红锁算法使用Lua脚本实现原子性的加锁和释放锁。在SETNX+Lua加锁时,它会向集群中的其他主节点发送SET命令。只要超过二分之一加一个节点设置成功,那么就认为加锁成功。释放锁时,需要向所有的节点发送DEL命令。
那第二个的话就是如何释放锁。方式一,我们可以用DEL命令,客户端加锁是set key。那释放锁直接用DEL命令将key 释放即可。那么假设线程A在获得锁操作期间锁超时释放了,此时线程B获得了这个锁。当线程A操作完成时,进行了一个DEL操作,将会把锁释放。线程B操作完成去释放锁的时候发现锁不存在的,或者释放了别的现成的锁,那这个该怎么办呢?我们可以采取方式二:GET+DEL命令。在设置锁的时候,将value 设置为一个唯一的客户端标识或者是UUID。客户端在释放锁的时候先get key 的value 校验加锁的线程是否为自己。如果是则释放锁,但是get 和delete 命令并不是原子操作,还还是有线程安全问题怎么办?那我们可以通过方式三:Lua脚本,使用Lua脚本,通过eval/evalsha命令执行get 和DEL的操作,Lua脚本可以保证我们的原子性操作。
Java面试百日百更:介绍下泛型以及对泛型擦除的理解?
最近有小伙伴频繁被面试官问到,要求介绍一下泛型以及对泛型擦除的理解。这个面试题主要考察面试者对于泛型的了解程度,大家可以这样回答。首先是泛型的概念,Java在1.5引入了泛型机制,泛型本质是参数化类型。也就是说变量的类型是一个参数,在使用时再指定具体的类型。然后就是泛型的分类可以分为泛型类、泛型接口、泛型方法。通过使用泛型,可以使代码更加的简单安全。但是Java中的泛型使用的类型擦除并不是C++、C#当中使用的真泛型,而是伪泛型。那么Java当中的泛型为伪泛型是个什么意思呢?比如有这样一种代码。在我们代码中,ArrayList 和ArrayList,在我们看来,它们的参数和类型不同,一个保存整形,一个保存字符串。但是通过比较它们的Class对象上面的代码输出是true。这说明在JVM看来,他们是同一个类。而在C++、C#这些支持真泛型的语言中,他们就是不同的类。就是说Java 当中的类型参数只存在于编译期,在运行时Java的虚拟机并不知道泛型的存在。这就是因为Java当中的泛型使用了类型擦除机制。那类型擦除机制到底又是怎么个过程呢?就是说在运行期泛型参数会被JVM擦除到它的第一个边界。比如说上面的hold 类参数类型是一个单独的T那么就会插入到Object。相当于所有出现T的地方都用Object替代。所以在JVM看来,保存的变量a 还是Object的类型。之所以取出来自动就是我们传入的参数类型。这是因为编译器在编译生成的字节码文件中插入了类型转换的代码,不需要我们手动转型了。如果参数类型有边界,那么就会插入到它的第一个边界。然而类型擦除导致泛型丧失了一些功能。在任何运行期需要知道,明确类型的代码都无法正常工作。比如就是在不能在泛型内的中通过new 泛型类的方式来创建对象。再比如使用泛型通过instanceof来判断参数类型或定义泛型类型的数组,比如下面的代码。从代码中我们可以看到new T创建对象是不行的。一是由于类型擦除,二是由于编译器不知道T是否有默认的构造器。再比如,不能调用泛型对象的某个方法。如下面的代码。上面的Manipulator是一个泛型类。内部用一个泛型化的变量obj。在Manipulator 方法中调用了obj的方法f(). 但这行代码无法编译,是因为类型擦除,编译器不能确定obj是否有f() 方法。那么解决这个问题的方法是通过给泛型通配符一个边界。如下面的代码五。但是大家有没有发现,这样定义真的有意义吗?不如直接指定HasF来个痛快。比如代码六。所以泛型只有在比较复杂的类中,才能体现出作业。但是像这种形式的东西,不是完全没有意义的。如果那种有一个返回T类型的方法泛型也有用的,因为这样就会返回准确类型。比如下面的例子片段二。大家可以看到这里的get 方法返回的是泛型参数的准确类型,而不是还是HasF。那最后的话就是叫泛型擦除是有问题的。所以在实际使用泛型的时,我们需要做一些个补偿措施。那么第一种补偿措施就是传递一个工程对象,并且通过它来创建新的实例例如片段七。那么另外一种解决方式就是利用模板设计模式例如片段八。大家可以看到所有的子类都必须按照父类的模板创建,具体类型放到我们子类当中。在create()方法中创建实际的类型并返回。对了,还有一点,面试官在你回答完上面的问题以后,马上就会直接问你在项目当中有用到泛型吗?具体是怎么使用的,欢迎大家评论区留言,发表一下自己项目当中泛型的使用场景。好了,今天的java 面试题分享就到这里了。
- 片段一
1 | public class ErasedTypeEquivalence { |
- 片段二
1 | public class Holder<T> { |
- 片段三
1 | public class Manipulator<T> { |
- 片段四-
1 | class HasF { |
- 片段五
1 | public class Manipulator<T extends HasF> { |
- 片段六
1 | public class Manipulator { |
- 片段七
1 | interface FactoryI<T> { |
- 片段八
1 | abstract class GenericwithCreate<T> { |
Java面试百日百更:项目中对外提供调用的API是如何设计?如何保证安全性?如何签名?如何防重的?
昨天有小伙伴在面试过程当中被面试官问到其项目中对外提供调用的API接口是如何设计的,如何保证安全性,如何签名如何防重。这个面试题主要考察面试者是否在实际的工作中关注过接口安全这一块的设计。在实际的业务中难免会跟第三方系统进行数据的交互与传递。那如何保证数据在传输过程的安全呢?除了HTTPS协议之外,能不能加上通用的一套算法以及规范来保证数据传输的安全性呢?下面就我自己工作中常见常用的一些API设计的安全方法,可能不一定是最好的,有更牛逼的实现方式。但是我自己的经验的一个分享。
一、token 机制:token 访问令牌access token 用于接口中,用于标识调用者的身份凭证,减少用户名和密码的传输次数。一般情况下,客户端需要先向服务器申请一个接口调用的账号,服务器会给出一个APPID和一个key, key 用于参数签名使用。注意key保存在客户端需要做一些安全处理,防止泄露。token 的值一般是UUID。服务端生成token 以后,需要将token 作为key,将一些和token关联的信息作为value保存到缓存服务器当中。比如redis。当一个请求过来以后,服务器就去缓存当中查询这个token 是否存在,存在则调用接口,不存在返回接口错误。一般通过拦截器或者过滤器来实现。token 的话主要分为两种,一种是接口令牌,API token 用于访问不需要登录的接口。如登录注册一些基本数据的获取。获取接口令牌需要拿APPID timestamp和sign 来换。sign 等于加密的timestamp 加key。另外一种是用户令牌userToken,用于访问需要用户登录之后的接口,如获取我的基本信息,保存、修改、删除等操作。获取用户令牌,需要拿用户名和密码来换。关于token 的时效性,token 可以是一次性的,也可以在一段时间范围内有效,具体使用哪一种就看业务的需要。一般情况下,接口最好使用HTTPS协议。如果使用HTTP协议token机制只是一种减少被黑的可能性,其实只能防君子不防小人。一般token timestamp和sign 三个参数会在接口当中同时作为参数传递,每个参数都有各自的用途。
二、timestamp 机制:timestamp 时间戳是客户端调用接口时对应的当前时间戳,时间戳用于防dos攻击。当黑客劫持了请求的药URL去DOS攻击,每次调用接口时,接口都可以判断服务器当前系统时间和接口传过来的timestamp的差值。如果这个差值超过某个设置的时间,例如五分钟,那么这个请求将会被拦截掉。如果在设置的超时时间范围之内,是不能阻止dos 攻击的。timestamp 机制只能减轻dos 攻击的时间,缩短攻击时间。如果黑客修改的时间戳的值,可以通过sign签名机制来进行处理。
三、sign 签名机制:sign 一般用于参数签名,防止参数被非法篡改,最常用的是修改金额等重要的这个敏感信息。sign 的值一般是将所有的非空参数按照升序排序以后,然后加上token+key+timestamp+nonce随机数拼接在一起,随后使用某种加密算法进行加密。作为接口的一个参数sign来传递,也可以将sign放在请求头中。接口在网络传输过程当中,如果被黑客劫持并修改其中的参数值,然后再继续调用接口。虽然参数值被修改了,但是因为黑客不知道sign 是如何计算出来的。不知道sign都有哪些值构成,不知道以怎么样的顺序拼接在一起。最重要的是不知道签名字符串中key 是什么,所以黑客可以篡改参数的值,但没法修改sign的值。当服务器调用接口前会按照sine 的规则重新计算出sign的值,然后和接口传输的sign参数值做比较。如果相等,则参数值没有被篡改。如果不等表示参数被非法篡改了,就不执行结果了。最后就是如何防止重复提交。对于一些重要的操作,需要防止客户端啊重复提交。如非幂等性的重要操作,具体的办法当请求第一次提交时,将sign作为key 保存在redis 并设置超时时间、超时时间和timestamp 当中的这个设置的差值相同。当同一个请求第二次访问时,先检验redis 是否存在该sign。如果存在,则证明重复提交了,接口就不再继续调用了。如果sign 在缓存服务器当中,因过期时间到了而被删除。此时当这个URL再一次请求服务器时,因token的过期时间和sign的过期时间一致。sign过期也意味着token 过期。那同样URL再次访问服务器,会因为token 错误会被拦截掉。这就是为什么sign和token 的时间要保持一致的原因,拒绝重复调用机制,保证URL被别人截获了,也无法使用。对于哪些接口需要防止重复提交,可以制定一个注解来进行标记。当然,所有的安全措施都用上的话,有的时候难免太过复杂。在实际的项目当中,要根据自身的情况做出裁剪,比如可以只使用签名机制,就可以保证信息不会被篡改或者定向的提供服务的时候只用token机制就可以了。如何拆解看项目的实际情况和对接口安全性的要求
Java高级面试题:有看过AQS的源码吗?说下他的原理是怎样的?
AQS它主要有两个关键点,第一个是一个叫state变量,再一个就是CLH双向列表。state变量它其实就是一个标志位,他用来标识哪个线程获取到了这个共享变量标识位,第二个他是把哪些加锁失败的线程然后直接放到这个队列当中进行等待,
那为什么这里需要使用双向链表呢?
它有两方面,第一个是方便那个新节点找到尾节点快速入队,那第二个就是说一些正常锁等待的节点,他可能需要中断或者说被唤醒,那其实通过这个双向队列他就可以很好的找到前后对应的节点提高效率。
然后就是AQS里面用到的设计模式
那最经典的就是模版模式,就是AQS它是一个抽象类,它给我们默认实现了一些方法,就是包括一个叫tryAquire() 以及enq()入队这种操作的方法,就是默认帮我们实现的,那也有一些获取锁或者是说正常逻辑 他写成了抽象方法,交给我们对应的子类去实现
还有一个就是AQS整体加锁的过程
假设一个线程A,是去争抢这个锁,那第一步是CAS修改它这个state变量,如果修改成功,那就直接抢到锁成功了。那如果修改失败,他会在判断这个持有这个锁的线程呢是不是自己,如果是自己就将状态加1,那就是涉及到一个可重入锁的概念,那这一步判断也失败了, 那会进入到一个入队的操作,会调用enq()方法,线程会把自己包装成一个Node节点,然后去入队操作,入队之后呢他如果发现自己的前节点是头节点,那其实很大概率他还会再进行一次锁的争抢,因为他如果前节点是头节点的话,其实大概率还是能够抢到锁的,如果这一步也失败了那就会进行一个等待机制了,然后去调用的那个Unsafe类的park()方法然后进入一个线程的休眠状态等待其他节点来唤醒他。
最后的话就是线程入队的详细过程
其实它底层啊开始也是一个CAS逻辑,大家可以看到 AQS里面其实频繁使用到CAS这个特性。是因为什么呢? 是因为他要通过CAS,来快速失败,然后他如果能够拿到锁的话那就直接成功了,他避免线程进入休眠等待时间,因为这个需要引起上下文的切换其实是很影响效率的。那如果他第一次CAS失败之后呢?然后他就会进行一个入队的操作,其实分三步,第一步是先修改他的前指针,然后再CAS修改这个尾节点的指针,最后再修改这个后指针变量。那最后就是入队成功了,那这个流程顺序可以变吗?,可不可以先把尾节点,或者先修改尾指针这种。啊答案是不可以的,因为你如果先修改尾节点,或者先改尾指针都有可能在并发情况下造成空指针异常。 以上就是我对这个问题的回答
Java进阶面试题:说下MySQL的两阶段提交?
那这个问题的话,大家可以先从它的那个一条s语句的执行流程说起。那它最开始的话,会写一个buffer pool,那就是我们MySQL的一个内存缓存,那第二个呢,它会写undo log啊,是我们一个事务的数据的版本链。那第三个呢,会到一个事务的两阶段提交啊,它会先写redo log,做一个事务的预处理。然后再写bin log等bin log写完之后呢,它会再进行一个redo log的事务的提交操作啊,这就是我们事务的两阶段提交。 那为什么需要写redo log啊?它为什么不直接写磁盘呢?那是因为我们的这个数据啊,它是随机分布在磁盘上面的。因为磁盘上面的那个随机写的效率很低。redo log它是一个顺序写的一个物理日志,所以它的效率啊是很高的。顺序写会比随机写的效率高几百倍啊,所以需要写read log以上就是我对这个问题的回答。
Java面试百日百更:介绍下MySql事务的隔离级别以及如何解决高并发问题?
MySql的隔离级别的话主要有读未提交,读已提交,可重复读,序列化。在高并发下,它们分别存在的问题,读未提交有一个脏读, 读已提交呢是不可重复读,可重复读会有一个幻读的问题。序列化就是效率太低了,那么解决这些问题的话,就是通过MVCC和加锁实现的。
加锁的话很简单,这里主要说一下MVCC。MVCC的话叫做多版本并发控制。它有一个最关键的东西叫read view读视图。它有四个核心参数,第一个呢就是当前最大的事务ID,第二个呢是当前最大的已提交事务ID。第三个是当前活跃的事务ID列表,第四个是当前的事务ID。它通过这个read view和它的一个undo log,一个事务的版本链进行一个对比,然后选择需要读取的数据。它就是一个MVCC机制。
MySql是否已经解决了幻读? 大部分人会说已经解决了啊,但是这个说法是不够精准的,应该来说MySql在可重复读隔离级别条件下。实现了快照读,解决了幻读问题。那如果是当前读的话,必须通过一个select for update这样的特殊的语法机制,也就是当前读。然后它会在这个间隙上加上一个行锁和一个间隙锁,组成一个next lock临界锁,那这样的话。可以避免其他事务在这个间隙插入数据,那数据插入不进去就没有换读的问题产生了。那以上就是我对这个问题的回答
Java面试百日百更:说说你对高并发的理解?
高并发的话就是考验我们一个服务在瞬时间对一个并发量请求的一个承载量。那它有两个核心参数,第一个是QPS,那第二个就是TPS,QPS的话是我们每秒能够承载的一个请求的数量。TPS的话就是每秒能够处理的事务数量,那一般来说一个事务会包含多个请求,就是TPS会比QPS小一点。那一般线上是如何处理这种场景的?分三个角度来说,事前事中和事后。
那事前的话,我们会考虑一个代码和架构层面的设计。包括批处理,异步处理,还有一个就是缓存处理,那事中的话,我们会建立一套完善的监控报警制度。以及我们对应的研发人员的一个响应措施,那第三个的话就是事后,好一点的公司的话,都会有一个不成文的规定,那每次出现线上事故的时候。都会进行组织一次复盘,那为什么呢?不是说公司要去追究责任啊,是因为大家想从这个事故当中吸取教训。然后下次的话,把这个事情做的更好,以上就是我对这个问题的回答。
Java面试百日百更:说下你对分布式事务的理解?
那首先的话是分布式事务的概念,其实就是在我们的分布式场景下。要对多实例对一个数据的事务的变更,要达到一个一致性的操作,那这个就叫做分布式事务。那目前的话,常有的分布分布式事务解决方案的话,第一个就是XA啊,就是它也是最开始的事务模型。它是追求一个强一致性,但是在2000年的时候CAP理论出现了,它告诉我们在分布式场景下,一致性,可用性,分区容错性这三个点是不可能完全达成一致的,所以说后面就有了两阶段,三阶段提交,也就最近两年阿里出的开源框架Seata。它里面的话也是支持一个AT和TCC模式,各有缺点。比如说TCC这个东西的话,需要我们自己手动硬编码来实现业务,比较复杂,那另一个就是说一些资源悬挂或者空回滚的问题,当然这个可以通过重试和本地记录分支事务状态的方式去解决。不过比较麻烦。 那如果生产环境当中,我们想要达到这个多服务数据的一致性,比较推荐落地的方案的话,可能会使用到一个本地消息表的方案,那说起来也比较简单,比如说我们的订单服务要去通知一个履约服务进行发货,那可以在本地事务里面插入一个本地消息表啊。然后再插入一条任务,然后通过这个异步线程的方式来消费这个任务,就是不断的重试。那就达到了一个最终一致性的效果,那这是一种比较省事的方案。
那如果在这个基础之上要继续优化的话啊,其实这个原理呢,就是一个异步重试嘛,那我们可以不直接在业务代码当中去处理本地消息表。业务代码只负责发布消息到消息队列,由消息队列触发后台线程来处理本地消息表。那这种方式呢,就降低了我们业务代码和本地消息表的偶合。那我们如果想更图省事的话,或者有一些开源框架可以去选择的话,那我们可以去选择RocketMQ。那它里面支持一个事务消息啊,其实也是类似于两阶段提交的原理,只不过它中间加了很多的什么重试啊,还有这个死信队列的机制,那这样的话呢,那我们只要我们有这个能力去维护这个RocketMQ的这个集群。然后或者是说有云服务的话啊,那这是一种比较推荐的方案,那以上就是我对这个问题的回答
Java面试百日百更第32天:spring是如何解决循环依赖的?
最近有小伙伴在面试过程当中被问到了Spring是如何解决循环依赖的这个面试题,也是spring面试过程当中必面的面试题。主要考察大家对循环依赖的理解以及Spring解决循环依赖的方式,今天老北鼻也给大家一个回答的参考。这个问题需要从以下三个方面进行回答,第一个就是什么是循环依赖,就是说类A内部依赖于类B,类B内部依赖于类A这种相互循环依赖。还有自身依赖于自身的循环依赖,就是说类A内部又依赖于类A。以及多组循环依赖。类A类内部依赖于类B,类B内部依赖于类C,类C内部依赖于类A,第二个就是依赖注入对象的两种方式。一个是构造器注入,另一个就是通过set注入。第三个就是Spring当中,主要解决了哪种方式的循环依赖,以及如何解决循环依赖的。
在Spring内部,主要解决了单例Bean的set注入方式的循环依赖。对于原型Bean的循环依赖,自依赖的循环依赖,构造器注入方式的循环依赖。Spring是不允许的,至于为什么这三种情况下不允许,主要是有套娃的风险。那Spring内部是如何处理这三种情况下是不允许的呢?其实在Spring内部是通过singletonsCurrentlyInCreation这个列表来发现循环依赖的。 这个列表会记录创建当中的Bean,然后发现Bean在这个列表当中存在了,说明有循环依赖,并且这个循环依赖是无法继续走下去。如果继续走下去,会进入死循环,此时Spring会抛出异常让系统终止,那在Spring内部采取了什么样的措施?又是怎么解决单例Bean的set注入的循环依赖的呢?
首先,在Spring当中使用了三个map来作为三级缓存,每一级对应一个Map,第一级singletonObjects用来存放已经完全创建好的单例Bean。BeanName为键,Bean实例为值。 第二级earlySingletonObjects用来存放早期的Bean。BeanName为键,Bean实例为值。第三级singleton factories用来存放单例Bean的ObjectFactory。BeanName为键,ObjectFactory实例为之值,当一个Bean早期被实例化完毕,但是还未填充属性初始化。此时Bean被放入到第三级缓存当中,并同时被加入到当前BeanName创建列表。但是这个时候并没有创建完毕。Bean被丢到一级缓存才算创建完毕,此时Bean还是个半成品。这个时候其他的Bean需要用到当前的Bean,那此时会从第三级缓存中获取Bean。此时的Bean是从第三级缓冲当中的ObjectFactory返回的并对象。丢到第二级缓存当中,同时删除第三级缓存当中的ObjectFactory,保证IOC的单例特性。当Bean实例化完毕,初始化完毕,属性注入完毕之后才会丢入到一级缓存当中,那这里就有一个问题。
为什么要使用三级缓存?在三级缓存中起到了什么样的作用?又解决了什么样的问题?
首先假设只有一级缓存,那整个Bean创建完毕以后将其完整的Bean放入到一级缓存当中,这样有什么问题呢?第一个Bean创建一共有三大步骤,实例化,属性赋值,初始化,等到整个过程都创建完毕,再存入到一级缓存。多线程了怎么办?第一个线程创建Bean的过程当中,又来了一个线程。当发现缓存当中,这个时候还没有就会再次去创建。那不就重复了吗?IOC要求Bean是单例的,加锁能解决这个问题吗?能,但是效率超级低, 对于一级缓存加锁,那么所有的对象创建过程当中都要等待,哪怕人家已经创建成功过,效率太低,不能接受,于是引入了二级缓存,二级缓存的引入可以解决一级缓存创建Bean链路长的问题。他在Bean一旦被创立,就立即放入到二级缓存整个Bean创建完毕以后,再放入到一级缓存,删除二级缓存。这样做可以解决多线程创建Bean的问题,缩短了整个链路。同时, 每次从缓存当中先获取Bean,如果一级缓存当中已经有了,那么就直接返回。不用执行后面的创建代码了,那么二级缓存有什么问题呢?这就需要知道一个问题,就是动态代理创建Bean,什么时候去使用动态代理来创建Bean呢?通常说我们在初始化之后调用Bean的后置处理器创建Bean,这只是大多数Bean创建动态代理的时候。那如果有循环依赖,有循环依赖在初始化之后创建Bean就完了,这是需要在实例化之后来创建这样动态代理的代码,就和创建Bean代码偶合了, 违背了单一原则,于是引入了三级缓存,三级缓存的引入是为了解决偶合问题。让每一个方法只做一件事情,巧妙的使用了函数结构,这个函数结构是什么用的呢?就是相当于js当中的回调函数。我们在前面定义好,但是不执行,直到满足条件了再执行,这个方法可以大方面的应用到实践工作当中,比如我们调用动态代理创建Bean。刚开始实例化完成,就赋予你这个能力,你可以去调用动态代理, 但是到后面你能否真的能够运用这个能力不一定只有满足条件才会运用这个能力。Spring内部就是通过这样的三级缓存来解决单例Bean的Set注入的循环依赖,以上就是我对这个问题的回答。
Java面试题:为什么!=会导致索引失效?
最近有小伙伴在面试过程当中谈到了MYSQL索引失效的场景的时候,说到了MYSQL在使用不等于会导致。索引失效,没想到面试官竟然不讲武德,问为什么不等于会导致索引失效,不等于一定不会使用索引吗?导致我们的小伙伴一时半会没招架住,今天老北鼻跟大家整理一个回答的参考,大家可以从以下三个方面进行说明。
第一呢,是MYSQL使用索引的查询有两个步骤,第一步查询索引数据获取主键ID,第二步根据主键ID,从表中获取数据。如果第一步在一个很大的表当中查询到少量的数据。那么,在第二步就会只需要很少的时间,例如对于等于这个比较,第一步读取索引数据效率。是O(LogN)速度很快,如果不了解,为什么等于是O(LogN)的话,大家可以去复习一下数据结构数的那一部分。那第一步和第二步都很快,所以走索引效率很好。
第二呢,是说 != 的话,第一步中读取索引数据的效率就不是O(LogN)了,需要全部读取索引的数据效率。一般情况下, !=操作会选中表中绝大部分的数据。假设一个字段有十个可选值。平均分布, !=操作就会选出其中90%的数据。第二步操作的时间和全表扫描的时间差不多。然而,还需要加上第一步,读取索引的时间。这样一来,所花的总时间就比不走索引还要多。对于这种情况,数据库底层就会优化为选择不走索引了。 那第三个呢,就是 !=不能使用索引,这个说法是错误的,在某种场景下不等于也是可以使用索引的。比如说我们查询的数据就是索引键,不用再读取表中的数据就可以获取所需要的数据的SQL。不等于是会使用索引的,换句话说就是满足索引覆盖使用条件的SQL, !=也是会使用索引。以上就是我对这个问题的回答,希望对大家的工作和面试有一定的帮助,谢谢大家。
Java面试题_谈谈你对Java中锁机制的理解
大家可以直接说问题场景,我们在多线程的情况下,同一时刻只有一个线程能够拿到这把锁。然后能够对某些变量进行一个更改或者是修改操作,那这个时候我们就需要用到锁了,那具体到锁的实现层面上面呢,主要有本地锁像synchronized,ReentrantLock,那如果是分布式锁层面的话呢啊,我们可以使用不同的中间件去实现。可以用Redis,zookeeper,甚至用Mysql也是可以的,那synchronized与ReentrantLock的区别的话。synchronized是一个JVM层面的关键字啊,它的底层的话是基于Mutex互斥变量,在JDK1.6这个性能优化之前呢,它的这个效率和性能都是很低的,然后的话就是那个ReentrantLock是一个JUC并发包下面提供的一个工具类,它的缺点的话就是需要你自己手动的去加锁和释放锁的逻辑。只不过用起来比较灵活。 然后ReentrantLock的话,其实底层呢是基于AQS啊,也叫做抽象队列同步器,那这个东西呢,它其实就是可以根据自己的一个需要实现。公平锁和非公平锁。对了,还有一点面试官在,你回答完上面的问题以后啊,马上就会追问啊,你是否看过AQS的源码,那么具体的原理是怎样的?欢迎大家在评论区留言发表一下自己对AQS的理解,下期跟大家说一下AQS的一个原理。以上就是我对这个问题的回答。



