能力说明:
精通JVM运行机制,包括类生命、内存模型、垃圾回收及JVM常见参数;能够熟练使用Runnable接口创建线程和使用ExecutorService并发执行任务、识别潜在的死锁线程问题;能够使用Synchronized关键字和atomic包控制线程的执行顺序,使用并行Fork/Join框架;能过开发使用原始版本函数式接口的代码。
暂时未有相关云产品技术能力~
1. 能说下myisam 和 innodb的区别吗?myisam引擎是5.1版本之前的默认引擎,支持全文检索、压缩、空间函数等,但是不支持事务和行级锁,所以一般用于有大量查询少量插入的场景来使用,而且myisam不支持外键,并且索引和数据是分开存储的。innodb是基于聚簇索引建立的,和myisam相反它支持事务、外键,并且通过MVCC来支持高并发,索引和数据存储在一起。2. 说下mysql的索引有哪些吧,聚簇和非聚簇索引又是什么?索引按照数据结构来说主要包含B+树和Hash索引。假设我们有张表,结构如下:create table user( id int(11) not null, age int(11) not null, primary key(id), key(age) );B+树是左小右大的顺序存储结构,节点只包含id索引列,而叶子节点包含索引列和数据,这种数据和索引在一起存储的索引方式叫做聚簇索引,一张表只能有一个聚簇索引。假设没有定义主键,InnoDB会选择一个唯一的非空索引代替,如果没有的话则会隐式定义一个主键作为聚簇索引。这是主键聚簇索引存储的结构,那么非聚簇索引的结构是什么样子呢?非聚簇索引(二级索引)保存的是主键id值,这一点和myisam保存的是数据地址是不同的。最终,我们一张图看看InnoDB和Myisam聚簇和非聚簇索引的区别3. 那你知道什么是覆盖索引和回表吗?覆盖索引指的是在一次查询中,如果一个索引包含或者说覆盖所有需要查询的字段的值,我们就称之为覆盖索引,而不再需要回表查询。而要确定一个查询是否是覆盖索引,我们只需要explain sql语句看Extra的结果是否是“Using index”即可。以上面的user表来举例,我们再增加一个name字段,然后做一些查询试试。explain select * from user where age=1; //查询的name无法从索引数据获取 explain select id,age from user where age=1; //可以直接从索引获取4. 锁的类型有哪些呢mysql锁分为共享锁和排他锁,也叫做读锁和写锁。读锁是共享的,可以通过lock in share mode实现,这时候只能读不能写。写锁是排他的,它会阻塞其他的写锁和读锁。从颗粒度来区分,可以分为表锁和行锁两种。表锁会锁定整张表并且阻塞其他用户对该表的所有读写操作,比如alter修改表结构的时候会锁表。行锁又可以分为乐观锁和悲观锁,悲观锁可以通过for update实现,乐观锁则通过版本号实现。5. 你能说下事务的基本特性和隔离级别吗?事务基本特性ACID分别是:原子性指的是一个事务中的操作要么全部成功,要么全部失败。一致性指的是数据库总是从一个一致性的状态转换到另外一个一致性的状态。比如A转账给B100块钱,假设中间sql执行过程中系统崩溃A也不会损失100块,因为事务没有提交,修改也就不会保存到数据库。隔离性指的是一个事务的修改在最终提交前,对其他事务是不可见的。持久性指的是一旦事务提交,所做的修改就会永久保存到数据库中。而隔离性有4个隔离级别,分别是:read uncommit 读未提交,可能会读到其他事务未提交的数据,也叫做脏读。用户本来应该读取到id=1的用户age应该是10,结果读取到了其他事务还没有提交的事务,结果读取结果age=20,这就是脏读。read commit 读已提交,两次读取结果不一致,叫做不可重复读。不可重复读解决了脏读的问题,他只会读取已经提交的事务。用户开启事务读取id=1用户,查询到age=10,再次读取发现结果=20,在同一个事务里同一个查询读取到不同的结果叫做不可重复读。repeatable read 可重复复读,这是mysql的默认级别,就是每次读取结果都一样,但是有可能产生幻读。serializable 串行,一般是不会使用的,他会给每一行读取的数据加锁,会导致大量超时和锁竞争的问题。6. 那ACID靠什么保证的呢?A原子性由undo log日志保证,它记录了需要回滚的日志信息,事务回滚时撤销已经执行成功的sqlC一致性一般由代码层面来保证I隔离性由MVCC来保证D持久性由内存+redo log来保证,mysql修改数据同时在内存和redo log记录这次操作,事务提交的时候通过redo log刷盘,宕机的时候可以从redo log恢复7. 那你说说什么是幻读,什么是MVCC?要说幻读,首先要了解MVCC,MVCC叫做多版本并发控制,实际上就是保存了数据在某个时间节点的快照。我们每行数据实际上隐藏了两列,创建时间版本号,过期(删除)时间版本号,每开始一个新的事务,版本号都会自动递增。还是拿上面的user表举例子,假设我们插入两条数据,他们实际上应该长这样。idnamecreate_versiondelete_version1张三1 2李四2 这时候假设小明去执行查询,此时current_version=3select * from user where id<=3;同时,小红在这时候开启事务去修改id=1的记录,current_version=4update user set name='张三三' where id=1;执行成功后的结果是这样的idnamecreate_versiondelete_version1张三1 2李四2 1张三三4 如果这时候还有小黑在删除id=2的数据,current_version=5,执行后结果是这样的。idnamecreate_versiondelete_version1张三1 2李四251张三三4 由于MVCC的原理是查找创建版本小于或等于当前事务版本,删除版本为空或者大于当前事务版本,小明的真实的查询应该是这样select * from user where id<=3 and create_version<=3 and (delete_version>3 or delete_version is null);所以小明最后查询到的id=1的名字还是'张三',并且id=2的记录也能查询到。这样做是为了保证事务读取的数据是在事务开始前就已经存在的,要么是事务自己插入或者修改的。明白MVCC原理,我们来说什么是幻读就简单多了。举一个常见的场景,用户注册时,我们先查询用户名是否存在,不存在就插入,假定用户名是唯一索引。小明开启事务current_version=6查询名字为'王五'的记录,发现不存在。小红开启事务current_version=7插入一条数据,结果是这样:idNamecreate_versiondelete_version1张三1 2李四2 3王五7 小明执行插入名字'王五'的记录,发现唯一索引冲突,无法插入,这就是幻读。8. 那你知道什么是间隙锁吗?间隙锁是可重复读级别下才会有的锁,结合MVCC和间隙锁可以解决幻读的问题。我们还是以user举例,假设现在user表有几条记录idAge110220330当我们执行:begin; select * from user where age=20 for update; begin; insert into user(age) values(10); #成功 insert into user(age) values(11); #失败 insert into user(age) values(20); #失败 insert into user(age) values(21); #失败 insert into user(age) values(30); #失败只有10可以插入成功,那么因为表的间隙mysql自动帮我们生成了区间(左开右闭)(negative infinity,10],(10,20],(20,30],(30,positive infinity)由于20存在记录,所以(10,20],(20,30]区间都被锁定了无法插入、删除。如果查询21呢?就会根据21定位到(20,30)的区间(都是开区间)。需要注意的是唯一索引是不会有间隙索引的。9. 你们数据量级多大?分库分表怎么做的?首先分库分表分为垂直和水平两个方式,一般来说我们拆分的顺序是先垂直后水平。垂直分库基于现在微服务拆分来说,都是已经做到了垂直分库了垂直分表如果表字段比较多,将不常用的、数据较大的等等做拆分水平分表首先根据业务场景来决定使用什么字段作为分表字段(sharding_key),比如我们现在日订单1000万,我们大部分的场景来源于C端,我们可以用user_id作为sharding_key,数据查询支持到最近3个月的订单,超过3个月的做归档处理,那么3个月的数据量就是9亿,可以分1024张表,那么每张表的数据大概就在100万左右。比如用户id为100,那我们都经过hash(100),然后对1024取模,就可以落到对应的表上了。10. 那分表后的ID怎么保证唯一性的呢?因为我们主键默认都是自增的,那么分表之后的主键在不同表就肯定会有冲突了。有几个办法考虑:设定步长,比如1-1024张表我们设定1024的基础步长,这样主键落到不同的表就不会冲突了。分布式ID,自己实现一套分布式ID生成算法或者使用开源的比如雪花算法这种分表后不使用主键作为查询依据,而是每张表单独新增一个字段作为唯一主键使用,比如订单表订单号是唯一的,不管最终落在哪张表都基于订单号作为查询依据,更新也一样。11. 分表后非sharding_key的查询怎么处理呢?可以做一个mapping表,比如这时候商家要查询订单列表怎么办呢?不带user_id查询的话你总不能扫全表吧?所以我们可以做一个映射关系表,保存商家和用户的关系,查询的时候先通过商家查询到用户列表,再通过user_id去查询。打宽表,一般而言,商户端对数据实时性要求并不是很高,比如查询订单列表,可以把订单表同步到离线(实时)数仓,再基于数仓去做成一张宽表,再基于其他如es提供查询服务。数据量不是很大的话,比如后台的一些查询之类的,也可以通过多线程扫表,然后再聚合结果的方式来做。或者异步的形式也是可以的。List<Callable<List<User>>> taskList = Lists.newArrayList(); for (int shardingIndex = 0; shardingIndex < 1024; shardingIndex++) { taskList.add(() -> (userMapper.getProcessingAccountList(shardingIndex))); } List<ThirdAccountInfo> list = null; try { list = taskExecutor.executeTask(taskList); } catch (Exception e) { //do something } public class TaskExecutor { public <T> List<T> executeTask(Collection<? extends Callable<T>> tasks) throws Exception { List<T> result = Lists.newArrayList(); List<Future<T>> futures = ExecutorUtil.invokeAll(tasks); for (Future<T> future : futures) { result.add(future.get()); } return result; } }12. 说说mysql主从同步怎么做的吧?首先先了解mysql主从同步的原理master提交完事务后,写入binlogslave连接到master,获取binlogmaster创建dump线程,推送binglog到slaveslave启动一个IO线程读取同步过来的master的binlog,记录到relay log中继日志中slave再开启一个sql线程读取relay log事件并在slave执行,完成同步slave记录自己的binglog由于mysql默认的复制方式是异步的,主库把日志发送给从库后不关心从库是否已经处理,这样会产生一个问题就是假设主库挂了,从库处理失败了,这时候从库升为主库后,日志就丢失了。由此产生两个概念。全同步复制主库写入binlog后强制同步日志到从库,所有的从库都执行完成后才返回给客户端,但是很显然这个方式的话性能会受到严重影响。半同步复制和全同步不同的是,半同步复制的逻辑是这样,从库写入日志成功后返回ACK确认给主库,主库收到至少一个从库的确认就认为写操作完成。13. 那主从的延迟怎么解决呢?针对特定的业务场景,读写请求都强制走主库读请求走从库,如果没有数据,去主库做二次查询
你们为什么使用mq?具体的使用场景是什么?mq的作用很简单,削峰填谷。以电商交易下单的场景来说,正向交易的过程可能涉及到创建订单、扣减库存、扣减活动预算、扣减积分等等。每个接口的耗时如果是100ms,那么理论上整个下单的链路就需要耗费400ms,这个时间显然是太长了。如果这些操作全部同步处理的话,首先调用链路太长影响接口性能,其次分布式事务的问题很难处理,这时候像扣减预算和积分这种对实时一致性要求没有那么高的请求,完全就可以通过mq异步的方式去处理了。同时,考虑到异步带来的不一致的问题,我们可以通过job去重试保证接口调用成功,而且一般公司都会有核对的平台,比如下单成功但是未扣减积分的这种问题可以通过核对作为兜底的处理方案。使用mq之后我们的链路变简单了,同时异步发送消息我们的整个系统的抗压能力也上升了。那你们使用什么mq?基于什么做的选型?我们主要调研了几个主流的mq,kafka、rabbitmq、rocketmq、activemq,选型我们主要基于以下几个点去考虑:由于我们系统的qps压力比较大,所以性能是首要考虑的要素。开发语言,由于我们的开发语言是java,主要是为了方便二次开发。对于高并发的业务场景是必须的,所以需要支持分布式架构的设计。功能全面,由于不同的业务场景,可能会用到顺序消息、事务消息等。基于以上几个考虑,我们最终选择了RocketMQ。你上面提到异步发送,那消息可靠性怎么保证?消息丢失可能发生在生产者发送消息、MQ本身丢失消息、消费者丢失消息3个方面。生产者丢失生产者丢失消息的可能点在于程序发送失败抛异常了没有重试处理,或者发送的过程成功但是过程中网络闪断MQ没收到,消息就丢失了。由于同步发送的一般不会出现这样使用方式,所以我们就不考虑同步发送的问题,我们基于异步发送的场景来说。异步发送分为两个方式:异步有回调和异步无回调,无回调的方式,生产者发送完后不管结果可能就会造成消息丢失,而通过异步发送+回调通知+本地消息表的形式我们就可以做出一个解决方案。以下单的场景举例。下单后先保存本地数据和MQ消息表,这时候消息的状态是发送中,如果本地事务失败,那么下单失败,事务回滚。下单成功,直接返回客户端成功,异步发送MQ消息MQ回调通知消息发送结果,对应更新数据库MQ发送状态JOB轮询超过一定时间(时间根据业务配置)还未发送成功的消息去重试在监控平台配置或者JOB程序处理超过一定次数一直发送不成功的消息,告警,人工介入。一般而言,对于大部分场景来说异步回调的形式就可以了,只有那种需要完全保证不能丢失消息的场景我们做一套完整的解决方案。MQ丢失如果生产者保证消息发送到MQ,而MQ收到消息后还在内存中,这时候宕机了又没来得及同步给从节点,就有可能导致消息丢失。比如RocketMQ:RocketMQ分为同步刷盘和异步刷盘两种方式,默认的是异步刷盘,就有可能导致消息还未刷到硬盘上就丢失了,可以通过设置为同步刷盘的方式来保证消息可靠性,这样即使MQ挂了,恢复的时候也可以从磁盘中去恢复消息。比如Kafka也可以通过配置做到:acks=all 只有参与复制的所有节点全部收到消息,才返回生产者成功。这样的话除非所有的节点都挂了,消息才会丢失。 replication.factor=N,设置大于1的数,这会要求每个partion至少有2个副本 min.insync.replicas=N,设置大于1的数,这会要求leader至少感知到一个follower还保持着连接 retries=N,设置一个非常大的值,让生产者发送失败一直重试虽然我们可以通过配置的方式来达到MQ本身高可用的目的,但是都对性能有损耗,怎样配置需要根据业务做出权衡。消费者丢失消费者丢失消息的场景:消费者刚收到消息,此时服务器宕机,MQ认为消费者已经消费,不会重复发送消息,消息丢失。RocketMQ默认是需要消费者回复ack确认,而kafka需要手动开启配置关闭自动offset。消费方不返回ack确认,重发的机制根据MQ类型的不同发送时间间隔、次数都不尽相同,如果重试超过次数之后会进入死信队列,需要手工来处理了。(Kafka没有这些)你说到消费者消费失败的问题,那么如果一直消费失败导致消息积压怎么处理?因为考虑到时消费者消费一直出错的问题,那么我们可以从以下几个角度来考虑:消费者出错,肯定是程序或者其他问题导致的,如果容易修复,先把问题修复,让consumer恢复正常消费如果时间来不及处理很麻烦,做转发处理,写一个临时的consumer消费方案,先把消息消费,然后再转发到一个新的topic和MQ资源,这个新的topic的机器资源单独申请,要能承载住当前积压的消息处理完积压数据后,修复consumer,去消费新的MQ和现有的MQ数据,新MQ消费完成后恢复原状那如果消息积压达到磁盘上限,消息被删除了怎么办?这。。。他妈都删除了我有啥办法啊。。。冷静,再想想。。有了。最初,我们发送的消息记录是落库保存了的,而转发发送的数据也保存了,那么我们就可以通过这部分数据来找到丢失的那部分数据,再单独跑个脚本重发就可以了。如果转发的程序没有落库,那就和消费方的记录去做对比,只是过程会更艰难一点。说了这么多,那你说说RocketMQ实现原理吧?RocketMQ由NameServer注册中心集群、Producer生产者集群、Consumer消费者集群和若干Broker(RocketMQ进程)组成,它的架构原理是这样的:Broker在启动的时候去向所有的NameServer注册,并保持长连接,每30s发送一次心跳Producer在发送消息的时候从NameServer获取Broker服务器地址,根据负载均衡算法选择一台服务器来发送消息Conusmer消费消息的时候同样从NameServer获取Broker地址,然后主动拉取消息来消费为什么RocketMQ不使用Zookeeper作为注册中心呢?我认为有以下几个点是不使用zookeeper的原因:根据CAP理论,同时最多只能满足两个点,而zookeeper满足的是CP,也就是说zookeeper并不能保证服务的可用性,zookeeper在进行选举的时候,整个选举的时间太长,期间整个集群都处于不可用的状态,而这对于一个注册中心来说肯定是不能接受的,作为服务发现来说就应该是为可用性而设计。基于性能的考虑,NameServer本身的实现非常轻量,而且可以通过增加机器的方式水平扩展,增加集群的抗压能力,而zookeeper的写是不可扩展的,而zookeeper要解决这个问题只能通过划分领域,划分多个zookeeper集群来解决,首先操作起来太复杂,其次这样还是又违反了CAP中的A的设计,导致服务之间是不连通的。持久化的机制来带的问题,ZooKeeper 的 ZAB 协议对每一个写请求,会在每个 ZooKeeper 节点上保持写一个事务日志,同时再加上定期的将内存数据镜像(Snapshot)到磁盘来保证数据的一致性和持久性,而对于一个简单的服务发现的场景来说,这其实没有太大的必要,这个实现方案太重了。而且本身存储的数据应该是高度定制化的。消息发送应该弱依赖注册中心,而RocketMQ的设计理念也正是基于此,生产者在第一次发送消息的时候从NameServer获取到Broker地址后缓存到本地,如果NameServer整个集群不可用,短时间内对于生产者和消费者并不会产生太大影响。那Broker是怎么保存数据的呢?RocketMQ主要的存储文件包括commitlog文件、consumequeue文件、indexfile文件。Broker在收到消息之后,会把消息保存到commitlog的文件当中,而同时在分布式的存储当中,每个broker都会保存一部分topic的数据,同时,每个topic对应的messagequeue下都会生成consumequeue文件用于保存commitlog的物理位置偏移量offset,indexfile中会保存key和offset的对应关系。CommitLog文件保存于${Rocket_Home}/store/commitlog目录中,从图中我们可以明显看出来文件名的偏移量,每个文件默认1G,写满后自动生成一个新的文件。由于同一个topic的消息并不是连续的存储在commitlog中,消费者如果直接从commitlog获取消息效率非常低,所以通过consumequeue保存commitlog中消息的偏移量的物理地址,这样消费者在消费的时候先从consumequeue中根据偏移量定位到具体的commitlog物理文件,然后根据一定的规则(offset和文件大小取模)在commitlog中快速定位。Master和Slave之间是怎么同步数据的呢?而消息在master和slave之间的同步是根据raft协议来进行的:在broker收到消息后,会被标记为uncommitted状态然后会把消息发送给所有的slaveslave在收到消息之后返回ack响应给mastermaster在收到超过半数的ack之后,把消息标记为committed发送committed消息给所有slave,slave也修改状态为committed你知道RocketMQ为什么速度快吗?是因为使用了顺序存储、Page Cache和异步刷盘。我们在写入commitlog的时候是顺序写入的,这样比随机写入的性能就会提高很多写入commitlog的时候并不是直接写入磁盘,而是先写入操作系统的PageCache最后由操作系统异步将缓存中的数据刷到磁盘什么是事务、半事务消息?怎么实现的?事务消息就是MQ提供的类似XA的分布式事务能力,通过事务消息可以达到分布式事务的最终一致性。半事务消息就是MQ收到了生产者的消息,但是没有收到二次确认,不能投递的消息。实现原理如下:生产者先发送一条半事务消息到MQMQ收到消息后返回ack确认生产者开始执行本地事务如果事务执行成功发送commit到MQ,失败发送rollback如果MQ长时间未收到生产者的二次确认commit或者rollback,MQ对生产者发起消息回查生产者查询事务执行最终状态根据查询事务状态再次提交二次确认最终,如果MQ收到二次确认commit,就可以把消息投递给消费者,反之如果是rollback,消息会保存下来并且在3天后被删除。
这是面试题系列第三篇--redis专题。说说Redis基本数据类型有哪些吧字符串:redis没有直接使用C语言传统的字符串表示,而是自己实现的叫做简单动态字符串SDS的抽象类型。C语言的字符串不记录自身的长度信息,而SDS则保存了长度信息,这样将获取字符串长度的时间由O(N)降低到了O(1),同时可以避免缓冲区溢出和减少修改字符串长度时所需的内存重分配次数。链表linkedlist:redis链表是一个双向无环链表结构,很多发布订阅、慢查询、监视器功能都是使用到了链表来实现,每个链表的节点由一个listNode结构来表示,每个节点都有指向前置节点和后置节点的指针,同时表头节点的前置和后置节点都指向NULL。字典hashtable:用于保存键值对的抽象数据结构。redis使用hash表作为底层实现,每个字典带有两个hash表,供平时使用和rehash时使用,hash表使用链地址法来解决键冲突,被分配到同一个索引位置的多个键值对会形成一个单向链表,在对hash表进行扩容或者缩容的时候,为了服务的可用性,rehash的过程不是一次性完成的,而是渐进式的。跳跃表skiplist:跳跃表是有序集合的底层实现之一,redis中在实现有序集合键和集群节点的内部结构中都是用到了跳跃表。redis跳跃表由zskiplist和zskiplistNode组成,zskiplist用于保存跳跃表信息(表头、表尾节点、长度等),zskiplistNode用于表示表跳跃节点,每个跳跃表的层高都是1-32的随机数,在同一个跳跃表中,多个节点可以包含相同的分值,但是每个节点的成员对象必须是唯一的,节点按照分值大小排序,如果分值相同,则按照成员对象的大小排序。整数集合intset:用于保存整数值的集合抽象数据结构,不会出现重复元素,底层实现为数组。压缩列表ziplist:压缩列表是为节约内存而开发的顺序性数据结构,他可以包含多个节点,每个节点可以保存一个字节数组或者整数值。基于这些基础的数据结构,redis封装了自己的对象系统,包含字符串对象string、列表对象list、哈希对象hash、集合对象set、有序集合对象zset,每种对象都用到了至少一种基础的数据结构。redis通过encoding属性设置对象的编码形式来提升灵活性和效率,基于不同的场景redis会自动做出优化。不同对象的编码如下:字符串对象string:int整数、embstr编码的简单动态字符串、raw简单动态字符串列表对象list:ziplist、linkedlist哈希对象hash:ziplist、hashtable集合对象set:intset、hashtable有序集合对象zset:ziplist、skiplistRedis为什么快呢?redis的速度非常的快,单机的redis就可以支撑每秒10几万的并发,相对于mysql来说,性能是mysql的几十倍。速度快的原因主要有几点:完全基于内存操作C语言实现,优化过的数据结构,基于几种基础的数据结构,redis做了大量的优化,性能极高使用单线程,无上下文的切换成本基于非阻塞的IO多路复用机制那为什么Redis6.0之后又改用多线程呢?redis使用多线程并非是完全摒弃单线程,redis还是使用单线程模型来处理客户端的请求,只是使用多线程来处理数据的读写和协议解析,执行命令还是使用单线程。这样做的目的是因为redis的性能瓶颈在于网络IO而非CPU,使用多线程能提升IO读写的效率,从而整体提高redis的性能。知道什么是热key吗?热key问题怎么解决?所谓热key问题就是,突然有几十万的请求去访问redis上的某个特定key,那么这样会造成流量过于集中,达到物理网卡上限,从而导致这台redis的服务器宕机引发雪崩。针对热key的解决方案:提前把热key打散到不同的服务器,降低压力加入二级缓存,提前加载热key数据到内存中,如果redis宕机,走内存查询什么是缓存击穿、缓存穿透、缓存雪崩?缓存击穿缓存击穿的概念就是单个key并发访问过高,过期时导致所有请求直接打到db上,这个和热key的问题比较类似,只是说的点在于过期导致请求全部打到DB上而已。解决方案:加锁更新,比如请求查询A,发现缓存中没有,对A这个key加锁,同时去数据库查询数据,写入缓存,再返回给用户,这样后面的请求就可以从缓存中拿到数据了。将过期时间组合写在value中,通过异步的方式不断的刷新过期时间,防止此类现象。https://tva缓存穿透缓存穿透是指查询不存在缓存中的数据,每次请求都会打到DB,就像缓存不存在一样。针对这个问题,加一层布隆过滤器。布隆过滤器的原理是在你存入数据的时候,会通过散列函数将它映射为一个位数组中的K个点,同时把他们置为1。这样当用户再次来查询A,而A在布隆过滤器值为0,直接返回,就不会产生击穿请求打到DB了。显然,使用布隆过滤器之后会有一个问题就是误判,因为它本身是一个数组,可能会有多个值落到同一个位置,那么理论上来说只要我们的数组长度够长,误判的概率就会越低,这种问题就根据实际情况来就好了。缓存雪崩当某一时刻发生大规模的缓存失效的情况,比如你的缓存服务宕机了,会有大量的请求进来直接打到DB上,这样可能导致整个系统的崩溃,称为雪崩。雪崩和击穿、热key的问题不太一样的是,他是指大规模的缓存都过期失效了。针对雪崩几个解决方案:针对不同key设置不同的过期时间,避免同时过期限流,如果redis宕机,可以限流,避免同时刻大量请求打崩DB二级缓存,同热key的方案。Redis的过期策略有哪些?redis主要有2种过期删除策略惰性删除惰性删除指的是当我们查询key的时候才对key进行检测,如果已经达到过期时间,则删除。显然,他有一个缺点就是如果这些过期的key没有被访问,那么他就一直无法被删除,而且一直占用内存。定期删除定期删除指的是redis每隔一段时间对数据库做一次检查,删除里面的过期key。由于不可能对所有key去做轮询来删除,所以redis会每次随机取一些key去做检查和删除。那么定期+惰性都没有删除过期的key怎么办?假设redis每次定期随机查询key的时候没有删掉,这些key也没有做查询的话,就会导致这些key一直保存在redis里面无法被删除,这时候就会走到redis的内存淘汰机制。volatile-lru:从已设置过期时间的key中,移除最近最少使用的key进行淘汰volatile-ttl:从已设置过期时间的key中,移除将要过期的keyvolatile-random:从已设置过期时间的key中随机选择key淘汰allkeys-lru:从key中选择最近最少使用的进行淘汰allkeys-random:从key中随机选择key进行淘汰noeviction:当内存达到阈值的时候,新写入操作报错持久化方式有哪些?有什么区别?redis持久化方案分为RDB和AOF两种。RDBRDB持久化可以手动执行也可以根据配置定期执行,它的作用是将某个时间点上的数据库状态保存到RDB文件中,RDB文件是一个压缩的二进制文件,通过它可以还原某个时刻数据库的状态。由于RDB文件是保存在硬盘上的,所以即使redis崩溃或者退出,只要RDB文件存在,就可以用它来恢复还原数据库的状态。可以通过SAVE或者BGSAVE来生成RDB文件。SAVE命令会阻塞redis进程,直到RDB文件生成完毕,在进程阻塞期间,redis不能处理任何命令请求,这显然是不合适的。BGSAVE则是会fork出一个子进程,然后由子进程去负责生成RDB文件,父进程还可以继续处理命令请求,不会阻塞进程。AOFAOF和RDB不同,AOF是通过保存redis服务器所执行的写命令来记录数据库状态的。AOF通过追加、写入、同步三个步骤来实现持久化机制。当AOF持久化处于激活状态,服务器执行完写命令之后,写命令将会被追加append到aof_buf缓冲区的末尾在服务器每结束一个事件循环之前,将会调用flushAppendOnlyFile函数决定是否要将aof_buf的内容保存到AOF文件中,可以通过配置appendfsync来决定。always ##aof_buf内容写入并同步到AOF文件 everysec ##将aof_buf中内容写入到AOF文件,如果上次同步AOF文件时间距离现在超过1秒,则再次对AOF文件进行同步 no ##将aof_buf内容写入AOF文件,但是并不对AOF文件进行同步,同步时间由操作系统决定如果不设置,默认选项将会是everysec,因为always来说虽然最安全(只会丢失一次事件循环的写命令),但是性能较差,而everysec模式只不过会可能丢失1秒钟的数据,而no模式的效率和everysec相仿,但是会丢失上次同步AOF文件之后的所有写命令数据。怎么实现Redis的高可用?要想实现高可用,一台机器肯定是不够的,而redis要保证高可用,有2个可选方案。主从架构主从模式是最简单的实现高可用的方案,核心就是主从同步。主从同步的原理如下:slave发送sync命令到mastermaster收到sync之后,执行bgsave,生成RDB全量文件master把slave的写命令记录到缓存bgsave执行完毕之后,发送RDB文件到slave,slave执行master发送缓存中的写命令到slave,slave执行这里我写的这个命令是sync,但是在redis2.8版本之后已经使用psync来替代sync了,原因是sync命令非常消耗系统资源,而psync的效率更高。哨兵基于主从方案的缺点还是很明显的,假设master宕机,那么就不能写入数据,那么slave也就失去了作用,整个架构就不可用了,除非你手动切换,主要原因就是因为没有自动故障转移机制。而哨兵(sentinel)的功能比单纯的主从架构全面的多了,它具备自动故障转移、集群监控、消息通知等功能。哨兵可以同时监视多个主从服务器,并且在被监视的master下线时,自动将某个slave提升为master,然后由新的master继续接收命令。整个过程如下:初始化sentinel,将普通的redis代码替换成sentinel专用代码初始化masters字典和服务器信息,服务器信息主要保存ip:port,并记录实例的地址和ID创建和master的两个连接,命令连接和订阅连接,并且订阅sentinel:hello频道每隔10秒向master发送info命令,获取master和它下面所有slave的当前信息当发现master有新的slave之后,sentinel和新的slave同样建立两个连接,同时每个10秒发送info命令,更新master信息sentinel每隔1秒向所有服务器发送ping命令,如果某台服务器在配置的响应时间内连续返回无效回复,将会被标记为下线状态选举出领头sentinel,领头sentinel需要半数以上的sentinel同意领头sentinel从已下线的的master所有slave中挑选一个,将其转换为master让所有的slave改为从新的master复制数据将原来的master设置为新的master的从服务器,当原来master重新回复连接时,就变成了新master的从服务器sentinel会每隔1秒向所有实例(包括主从服务器和其他sentinel)发送ping命令,并且根据回复判断是否已经下线,这种方式叫做主观下线。当判断为主观下线时,就会向其他监视的sentinel询问,如果超过半数的投票认为已经是下线状态,则会标记为客观下线状态,同时触发故障转移。能说说redis集群的原理吗?如果说依靠哨兵可以实现redis的高可用,如果还想在支持高并发同时容纳海量的数据,那就需要redis集群。redis集群是redis提供的分布式数据存储方案,集群通过数据分片sharding来进行数据的共享,同时提供复制和故障转移的功能。节点一个redis集群由多个节点node组成,而多个node之间通过cluster meet命令来进行连接,节点的握手过程:节点A收到客户端的cluster meet命令A根据收到的IP地址和端口号,向B发送一条meet消息节点B收到meet消息返回pongA知道B收到了meet消息,返回一条ping消息,握手成功最后,节点A将会通过gossip协议把节点B的信息传播给集群中的其他节点,其他节点也将和B进行握手槽slotredis通过集群分片的形式来保存数据,整个集群数据库被分为16384个slot,集群中的每个节点可以处理0-16383个slot,当数据库16384个slot都有节点在处理时,集群处于上线状态,反之只要有一个slot没有得到处理都会处理下线状态。通过cluster addslots命令可以将slot指派给对应节点处理。slot是一个位数组,数组的长度是16384/8=2048,而数组的每一位用1表示被节点处理,0表示不处理,如图所示的话表示A节点处理0-7的slot。当客户端向节点发送命令,如果刚好找到slot属于当前节点,那么节点就执行命令,反之,则会返回一个MOVED命令到客户端指引客户端转向正确的节点。(MOVED过程是自动的)如果增加或者移出节点,对于slot的重新分配也是非常方便的,redis提供了工具帮助实现slot的迁移,整个过程是完全在线的,不需要停止服务。故障转移如果节点A向节点B发送ping消息,节点B没有在规定的时间内响应pong,那么节点A会标记节点B为pfail疑似下线状态,同时把B的状态通过消息的形式发送给其他节点,如果超过半数以上的节点都标记B为pfail状态,B就会被标记为fail下线状态,此时将会发生故障转移,优先从复制数据较多的从节点选择一个成为主节点,并且接管下线节点的slot,整个过程和哨兵非常类似,都是基于Raft协议做选举。了解Redis事务机制吗?redis通过MULTI、EXEC、WATCH等命令来实现事务机制,事务执行过程将一系列多个命令按照顺序一次性执行,并且在执行期间,事务不会被中断,也不会去执行客户端的其他请求,直到所有命令执行完毕。事务的执行过程如下:服务端收到客户端请求,事务以MULTI开始如果客户端正处于事务状态,则会把事务放入队列同时返回给客户端QUEUED,反之则直接执行这个命令当收到客户端EXEC命令时,WATCH命令监视整个事务中的key是否有被修改,如果有则返回空回复到客户端表示失败,否则redis会遍历整个事务队列,执行队列中保存的所有命令,最后返回结果给客户端WATCH的机制本身是一个CAS的机制,被监视的key会被保存到一个链表中,如果某个key被修改,那么REDIS_DIRTY_CAS标志将会被打开,这时服务器会拒绝执行事务。
1.说说Spring 里用到了哪些设计模式?单例模式:Spring 中的 Bean 默认情况下都是单例的。无需多说。工厂模式:工厂模式主要是通过 BeanFactory 和 ApplicationContext 来生产 Bean 对象。代理模式:最常见的 AOP 的实现方式就是通过代理来实现,Spring主要是使用 JDK 动态代理和 CGLIB 代理。模板方法模式:主要是一些对数据库操作的类用到,比如 JdbcTemplate、JpaTemplate,因为查询数据库的建立连接、执行查询、关闭连接几个过程,非常适用于模板方法。2.谈谈你对IOC 和 AOP 的理解?他们的实现原理是什么?IOC 叫做控制反转,指的是通过Spring来管理对象的创建、配置和生命周期,这样相当于把控制权交给了Spring,不需要人工来管理对象之间复杂的依赖关系,这样做的好处就是解耦。在Spring里面,主要提供了 BeanFactory 和 ApplicationContext 两种 IOC 容器,通过他们来实现对 Bean 的管理。AOP 叫做面向切面编程,他是一个编程范式,目的就是提高代码的模块性。Spring AOP 基于动态代理的方式实现,如果是实现了接口的话就会使用 JDK 动态代理,反之则使用 CGLIB 代理,Spring中 AOP 的应用主要体现在 事务、日志、异常处理等方面,通过在代码的前后做一些增强处理,可以实现对业务逻辑的隔离,提高代码的模块化能力,同时也是解耦。Spring主要提供了 Aspect 切面、JoinPoint 连接点、PointCut 切入点、Advice 增强等实现方式。3. JDK 动态代理和 CGLIB 代理有什么区别?JDK 动态代理主要是针对类实现了某个接口,AOP 则会使用 JDK 动态代理。他基于反射的机制实现,生成一个实现同样接口的一个代理类,然后通过重写方法的方式,实现对代码的增强。而如果某个类没有实现接口,AOP 则会使用 CGLIB 代理。他的底层原理是基于 asm 第三方框架,通过修改字节码生成成成一个子类,然后重写父类的方法,实现对代码的增强。4. Spring AOP 和 AspectJ AOP 有什么区别?Spring AOP 基于动态代理实现,属于运行时增强。AspectJ 则属于编译时增强,主要有3种方式:编译时织入:指的是增强的代码和源代码我们都有,直接使用 AspectJ 编译器编译就行了,编译之后生成一个新的类,他也会作为一个正常的 Java 类装载到JVM。编译后织入:指的是代码已经被编译成 class 文件或者已经打成 jar 包,这时候要增强的话,就是编译后织入,比如你依赖了第三方的类库,又想对他增强的话,就可以通过这种方式。加载时织入:指的是在 JVM 加载类的时候进行织入。总结下来的话,就是 Spring AOP 只能在运行时织入,不需要单独编译,性能相比 AspectJ 编译织入的方式慢,而 AspectJ 只支持编译前后和类加载时织入,性能更好,功能更加强大。5. FactoryBean 和 BeanFactory有什么区别?BeanFactory 是 Bean 的工厂, ApplicationContext 的父类,IOC 容器的核心,负责生产和管理 Bean 对象。FactoryBean 是 Bean,可以通过实现 FactoryBean 接口定制实例化 Bean 的逻辑,通过代理一个Bean对象,对方法前后做一些操作。6.SpringBean的生命周期说说?SpringBean 生命周期简单概括为4个阶段:实例化,创建一个Bean对象填充属性,为属性赋值初始化如果实现了xxxAware接口,通过不同类型的Aware接口拿到Spring容器的资源如果实现了BeanPostProcessor接口,则会回调该接口的postProcessBeforeInitialzation和postProcessAfterInitialization方法如果配置了init-method方法,则会执行init-method配置的方法销毁容器关闭后,如果Bean实现了DisposableBean接口,则会回调该接口的destroy方法如果配置了destroy-method方法,则会执行destroy-method配置的方法7.Spring是怎么解决循环依赖的?首先,Spring 解决循环依赖有两个前提条件:不全是构造器方式的循环依赖必须是单例基于上面的问题,我们知道Bean的生命周期,本质上解决循环依赖的问题就是三级缓存,通过三级缓存提前拿到未初始化完全的对象。第一级缓存:用来保存实例化、初始化都完成的对象第二级缓存:用来保存实例化完成,但是未初始化完成的对象第三级缓存:用来保存一个对象工厂,提供一个匿名内部类,用于创建二级缓存中的对象假设一个简单的循环依赖场景,A、B互相依赖。A对象的创建过程:创建对象A,实例化的时候把A对象工厂放入三级缓存A注入属性时,发现依赖B,转而去实例化B同样创建对象B,注入属性时发现依赖A,一次从一级到三级缓存查询A,从三级缓存通过对象工厂拿到A,把A放入二级缓存,同时删除三级缓存中的A,此时,B已经实例化并且初始化完成,把B放入一级缓存。接着继续创建A,顺利从一级缓存拿到实例化且初始化完成的B对象,A对象创建也完成,删除二级缓存中的A,同时把A放入一级缓存最后,一级缓存中保存着实例化、初始化都完成的A、B对象因此,由于把实例化和初始化的流程分开了,所以如果都是用构造器的话,就没法分离这个操作,所以都是构造器的话就无法解决循环依赖的问题了。8. 为什么要三级缓存?二级不行吗?不可以,主要是为了生成代理对象。因为三级缓存中放的是生成具体对象的匿名内部类,他可以生成代理对象,也可以是普通的实例对象。使用三级缓存主要是为了保证不管什么时候使用的都是一个对象。假设只有二级缓存的情况,往二级缓存中放的显示一个普通的Bean对象,BeanPostProcessor去生成代理对象之后,覆盖掉二级缓存中的普通Bean对象,那么多线程环境下可能取到的对象就不一致了。9.Spring事务传播机制有哪些?PROPAGATION_REQUIRED:如果当前没有事务,就创建一个新事务,如果当前存在事务,就加入该事务,这也是通常我们的默认选择。PROPAGATION_REQUIRES_NEW:创建新事务,无论当前存不存在事务,都创建新事务。PROPAGATION_NESTED:如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则按REQUIRED属性执行。PROPAGATION_NOT_SUPPORTED:以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。PROPAGATION_NEVER:以非事务方式执行,如果当前存在事务,则抛出异常。PROPAGATION_MANDATORY:支持当前事务,如果当前存在事务,就加入该事务,如果当前不存在事务,就抛出异常。PROPAGATION_SUPPORTS:支持当前事务,如果当前存在事务,就加入该事务,如果当前不存在事务,就以非事务执行。‘10.最后,说说Spring Boot 启动流程吧?这个流程,网上一搜基本都是这张图了,我也不想再画一遍了。那其实主要的流程就几个步骤:准备环境,根据不同的环境创建不同的Environment准备、加载上下文,为不同的环境选择不同的Spring Context,然后加载资源,配置Bean初始化,这个阶段刷新Spring Context,启动应用最后结束流程
2022年06月