自我介绍+项目介绍:5分钟左右;
如果说不用Redis来做分布式锁,你刚刚提到的超卖问题有什么解决方案吗?基于乐观锁的CAS思想。
Redis的分布式锁你们是自己写的呢还是用的Redission?我们自己实现了的,共享对象作为lock_key,加锁的客户端的唯一标识作为value,配置上NX参数表示只在lock_key不存在时,才对lock_key进行设置操作,同时也需要配置一下EX也就是超时时间;
打断一下这里问两个问题,就是如果一个线程加锁成功了另一个线程加锁失败了那么会出现什么情况?通过while循环来进行重试。
那其实就是把等待转移到JVM里面来了,那么这个地方是只有拿到锁才考虑后面的逻辑是吧?是的。
超时的话可能会出现什么问题呢?如果锁的超时时间设置过长,会影响性能,如果设置的超时时间过短会保护不到共享资源。
那你们是怎么解决的这个问题呢?基于续约的方式设置超时时间:先给锁设置一个超时时间,然后启动一个守护线程,让守护线程在一段时间后,重新设置这个锁的超时时间还配置了集群(集群这个不该提的挖了个坑,其实点到为止就好了只说到守护线程,下次面试就假装只到续约吧)。
这个是你们项目里实际这样做的吗,也就是实际编码是这样写的吗?是的(他可能有点怀疑真实性了,毕竟这个功能是我包装的),其实还有一个问题就是删除lock_key的时候需要使用lua脚本来确保后面两步操作的原子性原子性,因为解锁的过程就是将lock_key键删除,但不能乱删,要保证执行操作的客户端就是加锁的客户端。所以,解锁的时候,我们要先判断锁的value是否为加锁客户端,是的话,才将 lock_key 键删除。
那你刚刚说引入集群版本的Redis是吧?是的,参考了Redlock也就是红锁的写法,借鉴了Github上一些项目的经验就是常用的做法是引入5个实例。
那你这里加锁的话相当于是先连第一个Redis进行加锁,然后连第二个Redis进行加锁,直到满足你提到的这个阈值是吗,也就是说是顺序的是吗?是的。
那怎么确保第一个线程给第一个Redis加了锁然后第二个线程遇到第一个Redis实例的时候就加不了锁了,岂不是没什么意义?就是如果某个线程加锁的数量达到一定阈值就认为加锁成功了。
恩恩我明白,主要是顺序加锁?请求是顺序的但是set操作可以看出来是否这个Redis实例已经加过锁了。
那怎么判断的呢?就是已经有锁对象了那么他就会去访问下一个Redis实例,这样如果有某个线程快速占据多个锁那么就认为加锁成功了。
其实就是说去模仿了一个Redlock的一个机制是吧,恩恩好的,自己做的话还是比较低效的,你刚刚提到了删除的时候用了lua脚本,那里面是怎么写的呀?用call操作get到线程id判断是否和redis里面的值一样。
刚刚提到了守护线程,有遇到过什么问题吗?定时续存功能,可能不一定能刚好续上。
等于说想要去模仿Redlock?是的。
其实守护线程是会遇到一些问题的,不知道你了解不了解,因为正常的容器应用里面就是一个线程池的模式,不是一对一的销毁了,引入守护线程去判断调用方的这个线程是否存在的话其实是不太准确的,因为比如Tomcat的一些核心线程它在处理完这个请求之后并没有销毁,而是去处理其他的任务,所以可能这个锁会不断续传?(我当然不了解,别骂了别骂了,我也是看文档偷学来的)确实没考虑到。
最后用的就是Redis这个版本吗?是的。
Redis的缓存穿透,缓存击穿的这个概念你能讲一讲吗?击穿:如果缓存中的某个热点数据过期了,此时大量的请求访问了该热点数据,就无法从缓存中读取,直接访问数据库,数据库很容易就被高并发的请求冲垮;穿透:当用户访问的数据,既不在缓存中,也不在数据库中,导致请求在访问缓存时,发现缓存缺失,再去访问数据库时,发现数据库中也没有要访问的数据,没办法构建缓存数据,来服务后续的请求。
怎么去解决这两种情况呢?穿透的话就是设置一个白名单对非法请求进行校验或者缓存空值或者NULL值;击穿的话就是去缓存热点数据,预先对热点数据进行缓存。
缓存空值或者NULL值会有什么问题吗?(我怎么知道!我也是看来的)不太清楚。
想一想呢,来了一个请求Redis和数据库里都没有那就变成了NULL,有什么想法吗?想不太出来。
比如说这时候数据库里面这个数据突然又有了呢?哦,那就是一致性问题,我觉得可以用超时机制来解决。
有没有更快的方案呢?更新数据库的时候把对应的缓存设置到数据库里面。
也可以,击穿你说的是对热点数据进行缓存,具体怎么实现呢?经常访问的数据根据频率来增加他的一个超时时间。
其实没有解决问题,因为有些数据可能前面几天平平无奇,但是今天突然数据量非常大,那你这个就没缓存到了,只能缓解不能解决?我只记得这一种了,不好意思。
你们这个项目里面用的技术栈来讲一讲?Nacos、GateWay、Feign。
对Feign了解过吗,他的底层原理怎么实现的呢?我记得通过动态代理在本地实例化远程接口,还有默认基于Ribbon的一个负载均衡,其他的不太记得了。
看你用到了对接微信支付,这边是怎么弄的呢?发送支付请求,调微信API,返回唯一凭证,然后进行轮询判断是否成功返回了,根据状态来修改数据库订单状态。
你们不断轮询是吗,我了解微信API有回调机制?没了解到(其实我看了下就是用的异步只不过轮询的是我们自己这边的数据库)。
线程池用过吗,详细讲讲?所有参数讲了一遍,任务的一个执行流程介绍了一遍。
拒绝策略有哪几种呢?把常见的几种讲了一下。
比如我新建了一个线程池,核心线程数是2,最大线程数是4,阻塞队列是5,那么来了好多个任务,新建了2个核心线程1/2,然后把队列也阻塞满了,接下来队列满了之后又新建了3/4两个线程,然后这个时候就等所有任务都执行完毕之后呢,哪些线程会被回收掉?3/4?
这里还可以再去看看,这里是3/4还是有不同情况?可能和实现方式有关,比如Fix、Single、Cached等等。
那你这个Fix和Single有什么区别?Single是只有一个核心线程来处理任务,Fix就是创建固定的。
什么固定的,参数是什么?(我没理解)。
就是Fix是什么意思,他底层是不是基于Java的一个原生线程池去做的?是的还是基于那三个参数。
那你觉得是怎么做的呢?哦想起来了,就是令核心线程数等于总线程数量。
比如我new了一个线程,那么我去调用start方法和run方法有什么区别?其实调用start本质上还是调用的run方法。
那它到底有什么区别呢?不太清楚了。
那你到时候可以再看看,然后那个new出来一个线程他的一个生命周期是怎么样的?指的是Java的线程吗(没听到new Thread再去问了一下,因为OS的线程模型和Java的有点区别)。
是的?NEW状态->RUNNABLE状态(包括了TIMED_WATING/WAITING/BLOCKED状态)->TERMINATED状态(期间眼神飘忽,他以为我在看资料...把电脑转了一圈,不小心把shutdown和shutdownNow的线程池状态和线程状态混了,尴尬)。
ThreadLocal了解吗?没用到过不太了解。
Java的synchronized和ReentrantLock有了解吗?锁升级详细过程讲述了一下(对象头的详细内容,偏向锁、轻量级锁LocalRecord、重量级锁Monitor、自旋),ReentrantLock的详细源码和底层原理结合一个具体的案例来讲解(获取锁,阻塞,解锁)。
ReentrantLock是公平的还是非公平的?这个是取决于我们的一个配置,不同的实现方式依赖于是否对阻塞队列中的Dummy节点的下一个节点进行探测。
那么在非公平的一个情况下,阻塞队列里面有AB,现在来了一个C,现在持有锁的线程释放了锁,那么谁会占有锁呢?只会考虑A和C的一个比较,看他们的抢占。
为什么synchronized需要从轻量级锁升级成重量级锁呢?开销问题。
那既然轻量级锁开销小且释放快那为什么还需要升级成重量级锁呢?不太清楚随便说了一下。
Spring的事务有没有用过呢,他的那个注解是什么怎么实现的?Transactional注解,基于AOP的思想使用动态代理来做的。
他的这个注解是怎么帮助我们处理事务的呢?基于TransactionalManager来切入,然后决定事务的开启、提交和回滚(记得不太清楚了,需要再看一下)。
比如说我新建了一个a,b两个字段的联合索引(当时没听到联合索引,声音有点小),你能说一说他的索引是怎么样的嘛?聚簇索引->二级索引->回表->联合索引。
那你说一说新建了a,b两个字段的联合索引,他的索引叶子节点存放的是哪些信息呢?我答的是聚簇索引里面的字段还需要更新(其实直接回答存的是主键就好了)。
那你说一说联合索引叶子节点存放的是什么(再提醒我一下,可能看出来我没听懂上面的问题)?就是主键的ID。
如果一个SQL比较慢的话你会怎么考虑?用explain去进行分析现在SQL可能涉及到的一些操作,比如索引可能失效了或者索引被MySQL的优化器错选了(包括扫描行数),还有判断order by是否是符合排序预期的。
反问环节问面试官反馈:
经过面试了解到我确实对Redis是有研究的,但是还需要去研究一些成熟的方案参考一下,比如Redission可以去学习一下,看看你们的方案和他们的方案的一个差别;然后Thread的状态机也可以再看看;锁还有MySQL的理解比较到位。
#java##后端##实习#