Java 后端面试 3
这个是某黄袍加身,下岗也可以去换部门的中厂。我自己还没面,因为听说9月下旬HC会多一点,进的概率大点,而且自己现在也抽不开身,在一个很不正规的厂里打工。
面试题目来自某客,应该靠谱。我先评论一下,如果所有非项目面试题都在了,那我只能说这什么垃圾面试官,问的垃圾题目,全是屁用没有的纯八股,真的烂。你这么问,怎么能有区分度,怎么能把人筛出来。选出来的人都是只会背书的文科生吧(无意冒犯文科生,这里指没有代码能力,只会背八股的公式人)。
-
登录如何实现登录,使用redis替代session,登录逻辑如何
答:首先用户输入手机号,并请求发送短信验证码,服务器通过阿里的接口发送短信验证码,并将验证码存入redis中(设置一个较短的过期时间),用户输入验证码,服务器验证验证码是否正确。若失败,则提示用户验证码错误;若正确,则使用JWT生成token,并返回给用户。用户登录成功后,将token存入redis中,并设置一个较长的过期时间,用来表征用户处于登录状态。此外,如果用户是第一次登录,也就是注册,那么我们会将用户信息存入数据库。
Session的缺点比较显著,一是加大服务器的存储负担,因为所有信息的存储都需要在服务器端,二是无法实现分布式,如果有多台服务器,则需要在每台服务器上都保存一份session,这会造成资源浪费。而Token的话,解决了前者的痛点,每次访问都是客户端携带Token,服务器只需要验证Token就能判断用户身份信息和是否登录,不需要在服务器端存储用户信息。而使用Redis则解决了后一问题,通过在多个服务器前加一个中间层来集中管理Token,可以较好地实现分布式。
-
秒杀服务如何保证高并发,怎么使用lua脚本,lua脚本的作用
答:秒杀服务的逻辑是这样的。首先我们需要一个统一的ID生成器,用来标识每一个订单号,既然是统一的,我们就不能分布离散地生成,而是要统一在redis进行生成。然后,我们需要一个秒杀队列,用来处理秒杀请求,队列中的请求会被逐个处理。对于已登录用户而言,第一点,需要先判断库存是否充足,这里可以使用CAS来实现,对于只读操作CAS是较为高效的解法;第二点,我们需要确认当前时间是否在秒杀时间段内,如果不在,则直接返回秒杀失败;第三点,我们需要检查用户是否已经下过单了(出于每个用户最多只能下n单的限制),这个也用CAS处理;最后我们进行订单的生成和仓库的扣减,并将订单信息存入数据库,这个处理需要用上Redisson和lua脚本来实现分布式锁,保证原子性。因为普通的synchronized锁是无法保证分布式下的原子性的,每个机器都会生成一个新的JVM,而lua脚本则可以在Redis中执行,可以保证原子性。
然后我们可以发现,对于整个秒杀业务其实大体上可以被分为两个部分,一个是秒杀资格判定(读),一个是更新(写),并且最重要的两点,一是前者快后者慢,而是前者通过才有后者。所以我们可以异步地处理,类似前者可以直接把所有的请求都处理完,放到一个队列里面进行排队,而后者则可以异步地处理,比如使用lua脚本来保证原子性。这也是引入kafka处理的原因。
对于lua脚本,我们首先要明确它是什么。它其实是用来实现原子性、事务性的一种编程语言,可以让我们在Redis中执行一些复杂的操作,它是单线程的所以比较安全,同时它要么做完要么完全不做。其次,再来说我们要怎么利用它,我们要用它来减少库存数量。
-
Redis的过期淘汰策略与内存淘汰策略
答:(这个就是纯纯的八股了)Redis当中的过期淘汰可以用
expire <key> <time>
来指定,也可以在创建的时候就直接指定过期时间。同时我们也可以用ttl <key>
来查看过期时间n,若大于0,则标识还有n秒过期;若等于-1,则标识永不过期,没有被设置过期时间;若等于-2,则标识已经过期。redis每隔一段时间,就会查看被设置expire的key,并将过期的key删除。而对于内存淘汰策略就有挺多的了:
no-eviction:当内存不足以容纳新写入数据时,新写入操作会报错。
allkeys-lru:当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的key。
allkeys-random:当内存不足以容纳新写入数据时,在键空间中,随机移除某个key。
volatile-lru:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,移除最近最少使用的key。
volatile-random:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,随机移除某个key。
volatile-ttl:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,有更早过期时间的key优先移除。
-
redis集群有哪些,介绍一下主从、分片、哨兵分别解决了什么问题
答:(我嘞个逆天,这种背下来有任何作用吗,不都是有需求就去用吗)Redis集群是一种通过将多个Redis节点连接在一起以实现高可用性、数据分片和负载均衡的技术。它允许Redis在不同节点上同时提供服务,提高整体性能和可靠性。Redis集群有主从模式、分片模式、哨兵模式。
主从模式:主从模式是Redis的一种集群架构,主节点负责处理写操作,从节点负责处理读操作。当主节点挂掉时,从节点会自动提升为主节点。主从模式下,读写分离,可以提高读的吞吐量。
分片模式:分片模式是Redis的另一种集群架构,它将数据分布到多个节点上,每个节点负责存储一部分数据。当数据量较大时,可以有效地解决单机内存不足的问题。分片模式下,数据存储在不同的节点上,可以有效地提高容量。
哨兵模式:哨兵模式是在主从复制基础上加入了哨兵节点,实现了自动故障转移。哨兵节点是一种特殊的Redis节点,它会监控主节点和从节点的运行状态。当主节点发生故障时,哨兵节点会自动从从节点中选举出一个新的主节点,并通知其他从节点和客户端,实现故障转移。
-
AOF和RDB持久化,这两者是否会阻塞主线程
答:(这种我看到就恶心,都是redis设计方面的了,作为一个使用者,在实际开发当中,我们根本不需要知道)由于Redis是存储在内存当中的,为了数据的持久化,我们还是需要一种方式存回到硬盘这种持久化存储器当中。Redis提供了两种持久化方式,第一种是RDB持久化,它会将内存中的数据以快照的方式,按照固定的时间间隔(如果修改操作越频繁,则该时间间隔越短)写入磁盘,恢复时会恢复到最近一次快照的状态。第二种是AOF持久化,它会将内存中的数据以日志的形式写入磁盘,恢复时会根据日志中的指令来恢复数据。
还可以通过save命令,手动触发RDB持久化,也可以通过bgsave命令,后台异步执行RDB持久化。
此外对于AOF持久化,它是通过追加的方式来实现的,所以不会阻塞主线程。但是对于RDB持久化,它是通过fork子进程来实现的,所以会阻塞主线程。
-
缓存一致性有哪些策略,分别怎么做,怎么优化
答:缓存一致性问题是指多个节点缓存的数据可能不一致,需要通过某种策略来解决。常见的策略有以下几种:
- 基于版本号:每个缓存都有自己的版本号,当数据更新时,版本号加1,缓存更新时,先比较版本号,若相同则更新缓存,否则丢弃缓存。
- 基于超时:设置缓存的超时时间,当缓存超时时,则更新缓存。
- 基于分布式锁:当多个客户端同时操作缓存时,通过分布式锁来避免缓存击穿。
- 基于消息队列:当缓存更新时,通过消息队列通知其他节点更新缓存。
对于基于版本号的策略,需要客户端和服务端都实现版本号的维护,比较麻烦。对于基于超时的策略,需要客户端和服务端都设置超时时间,比较麻烦。对于基于分布式锁的策略,需要客户端和服务端都实现分布式锁,比较麻烦。对于基于消息队列的策略,需要客户端和服务端都实现消息队列,比较麻烦。
-
缓存穿透问题:缓存和数据库都没有,每次查询都要去数据库,这样会导致数据库压力过大,造成系统崩溃。
- 缓存空值:在第一次到缓存未命中到达数据库后,发现符合缓存穿透条件,设置一个空值缓存(有过期时间),当查询不到数据时,直接返回空值。
- 布隆过滤器: 这样的话结构本来是发出查询->redis缓存->数据库,要在查询和缓存间加一层BloomFilter。如果检测到查询的key不存在于数据库中,则直接报错;如果在数据库中,则更布隆过滤器。
-
缓存雪崩问题:缓存服务器宕机或同一时段大量缓存失效,导致大量请求直接落到数据库,数据库压力过大,造成系统崩溃。
- 缓存失效时间设置随机值:随机设置缓存过期时间,避免缓存雪崩。
- Redis集群: 避免缓存雪崩,可以将缓存分布到多个Redis节点上,避免单点故障。
- 降级限流: 直接限制对服务器的查询请求,返回错误,不对数据库产生进一步的压力。
- 添加多级缓存
-
缓存击穿问题:缓存击穿是指对于某个被高并发且缓存构建业务比较复杂的key,缓存中没有,但是数据库中有,每次查询都要去数据库,造成数据库压力过大,造成系统崩溃。
- 互斥锁: 对于查询某个key的请求,当缓存没有命中时,加互斥锁,查询数据库返回请求,并在缓存中设置这个key,再释放锁,避免其他线程在查询重建时期的多次访问。
- 逻辑过期: 对于那些已经判断为热点高并发的资源,直接把它定死在redis当中,保证热点资源每时每刻都在缓存中,虽然可能会有旧的没更新的。对于过期时间,直接以值的形式存到redis的value一栏当中,这样即使到了过期时间它也不会被redis删除。如果过期了,则开另外一个线程去查询数据库,更新缓存。
-
分布式锁
答:一个最基本的分布式锁需要满足:
- 互斥:任意一个时刻,锁只能被一个线程持有。
- 高可用:锁服务是高可用的,当一个锁服务出现问题,能够自动切换到另外一个锁服务。并且,即使客户端的释放锁的代码逻辑出现问题,锁最终一定还是会被释放,不会影响其他线程对共享资源的访问。这一般是通过超时机制实现的。
- 可重入:一个节点获取了锁之后,还可以再次获取锁。实现一般会通过一个计数器来实现。
异常情况(三个异常将隔离级别分成四种):
- 脏读(Dirty Read): 一个事务读了另一个事务还未提交的变更,如果另一个事务发生了回滚,就会导致事务一读取的结果无效。
- 不可重复读(Unrepeatable Read): 一个事务先后对进行读,但是在这之间有另一个事务进行写,导致两次重复读取的结果不一致。
- 幻读(Phantom Read): 一个事务进行全表操作,完成之后另一个事务进行了插入操作,导致事务一全表扫描发现多出了一个Tuple。
四种隔离级别(从上往下安全性逐渐增强):
- Read Uncommitted (RU): 允许脏读,也就是可能读到其他事务未提交的变更。放开索引锁,并且删除Shared锁(读锁)。
- Read Committed (RC): 禁止脏读,只能读到已经提交的事务,也就是只能读到其他事务提交的最新数据。放开索引锁,并且Shared锁(读锁)会立即解锁。
- Repeatable Read (RR): 幻读有可能发生,保证同一事务的多个实例在并发环境中返回同样的结果,禁止脏读和不可重复读,也就是在同一事务中,同样的查询语句返回的结果必须是一致的。放开索引锁。
- Serializable (S): 最严格的隔离级别,确保事务之间的数据完全一致,避免脏读、不可重复读、幻读。需要加所有锁,并执行严格2PL。