Redis八股文
我看你在做项目的时候都使用到redis,你在最近的项目中哪些场景下使用redis呢?
缓存和分布式锁都有使用到。
问:说说在缓存方面使用
1.在我最写的物流项目中就使用redis作为缓存,当然在业务中还是比较复杂的。
2.在物流信息查询模块使用中使用二级缓存,一级缓存使用的是Caffeine,二级缓存就是使用redis。
问:对缓存击穿,缓存雪崩,缓存穿透有了解过吗?说说者三个缓存问题的解决方案吧。
1.缓存击穿:某个热点key设置了过期时间,在高并发查询的情况下,该热点key过期了,导致大量的请求去访问数据库,最终压垮数据库。
解决方案:
(1)不给热点key设置过期时间(在redis中设置的过期时间都是逻辑过期时间,通过逻辑字段来判断key是否过期)。
(2)使用分布式锁,保证每次只有一个请求去访问数据库,在每次访问数据库前再做一次查询缓存的操作,然后获取锁并去数据库查询,将查到的数据重新缓存起来,下一次请求就会在缓存中查询到数据,就不会查询数据库,防止压垮数据库。
2.缓存雪崩:第一种情况:大量的key设置了相同的过期时间,在同一大量的key失效,导致大量的请求去访问数据库,导致压垮数据库。第二种情况:redis宕机。
解决方案:
(1) 错开key的过期时间(TTL):在每个key的统一过期时间上在随机加上1~5分钟的过期时间。
(2)设置服务降级,服务熔断,服务限流,到达阈值的时候直接返回自定义的错误信息。(作为系统的兜底策略)
(3)为redis搭建集群,就包括哨兵模式,集群模式。
(4)做二级缓存。(可以重点介绍,就引导面试官到运单信息查询模块,给他介绍二级缓存的实现还有缓存同步的问题,主要讲caffeine缓存同步的问题)
3.缓存穿透:查询的key在缓存和数据库中都存在,每次都会进入数据库查询,当大量的这种key访问数据库,就会导致缓存穿透。(这种情况多半上是恶意攻击)
解决方案:
(1)在缓存中储存空值:当在数据库中没有查到数据时,将key关联null的键值对存储到缓存中,后续就会走缓存,这种解决方案缺点很明显,就是存储大量无用的存储,浪费空间。
(2)使用布隆过滤器:在查询缓存的时候先去布隆过滤器中查询是否存在缓存,再去查询缓存。
问:具体讲讲你对这个布隆过滤器理解?
布隆过滤器类似hash表叫做bitmap(位图),在没有位置存放的是二进制的数据,1表示存在,0表示不存在。通过哈希函数计算储存位置。在后续查询缓存前先去布隆过滤器中查询数据是否存在,如果存在才去查缓存,如果不存在就直接返回空。
布隆过滤器的优点就是:储存二进制的数据,而非真实的数据,查询速度快。缺点:判断数据是否存在有误判率,不能做删除操作。
为了降低布隆过滤器的误判率,因为两个数据的hashCode可能相同,所以我们可以设置多哈希函数操作,较低过滤器的误判率。
我们主要是根据redisson设置布隆过滤器,可以设置其误判率。在高并发的场景下,一般误判率控制在5%之内就可以了。
问:说说分布式锁方面的使用
在项目的支付模块中的扫码支付为了保证交易单的状态不会被除当前业务的其他业务修改,我们就需要使用分布式锁。
问:你能简单的讲述一下分布式锁的实现吗?(引导面试官到你的项目中,去解释)
分布式锁
可以跟面试官说说优惠劵超卖的问题。
在redisson中的获取锁方法底层主要是通过Lua(能够调用redis命令,保证命令执行的原子性)实现的,如果获取锁方法没有设置过期时间,则分布式锁会有watch dog来保证延长锁的有效时间。
还可以通过setnx来实现分布式锁(因为redis是单线程的)。 通过 set If not exists并设置过期时间实现分布式锁,但是存在的问题就是无法确定要给锁设置多长的有效时间,所以在项目中比较少使用。
公平锁 (我们项目中使用的分布式锁,保证可重入性)
公平锁主要还是基于redisson实现的。锁的结构采用的是hash结构,以大小key的形式,在我业务中,使用订单id和交易单id进行拼接作为大key,使用当前的线程id作为小key,vlaue存放的就是上锁的次数。在业务修改交易单状态是需要先判断当前交易单是否被上锁(通过订单id,交易单id,进程id来获取对应的锁),如果被上锁,则当前线程的其他业务不能进行操作。保证订单状态的准确性。(判断幂等性) 保证可重入性,根据当前线程id来获取分布式锁,解决死锁的情况(分布式锁的底层实现)。
问:分布式锁可以实现重入吗?
重入的意思就是:在同一个线程中获取锁后继续获取锁。因为我们分布式锁中小key使用的是线程id,value就是上锁的次数,我们每次进行重入时就是value值+1,在释放锁的时候,就将对应的value-1,在value为0的时候就删除对应的信息。其他微服务就可以获取此分布式锁了。通过线程id的不同来保证分布式锁的互斥。(蓝字选答)
问:redisson实现的分布式锁,可以解决主从一致性的问题吗?
不能解决主从一致的问题,单master节点获取锁后就没释放锁就宕机了,此时slave节点变成了master节点,因为新的master没有上锁,所以新的master会进行上锁,破坏了锁的互斥性。为了解决这个问题我们可以使用红锁,也就是给一般以上的节点添加分布式锁,但是这样效率就变的很低,为了提高效率,建议采用zookeeper实现分布式锁。(听说过zookeeper实现分布式锁)
问:在集群的项目中为什么不能用关键字synchronized呢?
在两个微服务中,如果使用synchronized是无法达到同步上锁效果,因为两个微服务是两个单独的JVM。
问:了解过双写一致性吗?
双写一致性:当数据库中的数据发生修改的时候,我们需要修改缓存中的数据,保证数据库和缓存中的信息相同。
在读操作的时候,会先到缓存中查询数据,如果没有命中的话就到数据库中查询。
在写操作的时候,会采用延迟双删。
问:在延迟双删中为什么要延迟删除缓存?
数据库采用的是主从模式,遵循读写分离,需要一些时间来将主数据库中的数据同步到从数据库,但是还是可能出现脏读的情况。
问:在延迟双删中为什么要删除两次缓存呢?
如果只有一次删除缓存操作的话就会有两种情况。
1.情况1:先删除缓存,再修改数据库。线程1删除缓存。在线程1要修改数据库前。线程2去做查询操作,发现没有缓存,就去数据库中查询数据并做缓存,之后线程1修改了数据库中的信息,最终出现缓存数据和数据库数据不相同的情况。
2.情况2:先修改数据,再删除缓存。当前缓存中方没有数据,线程1做查询操作,准备将旧数据缓存前,线程2做了修改数据和删除缓存的操作(此时是没有缓存的),最终缓存中就存储了旧的数据,出现脏读的情况。
为了防脏读的缓存被使用,所以在数据同步的需要两次删除缓存的操作。
问:延迟双删是没法保证强一致性的,有什么强一致性的方法吗?
方法一:使用分布式锁,每次只有一个线程进行操作,效率比较低下。
方法二:因为缓存中的数据大多是读多写少,所以我们可以使用在读取数据的时候使用共享锁, 多个线程同时可以读取缓存但是其他线程不可以写,在修改数据的时候使用排他锁,会阻塞其他线程的读写操作。
排他锁和共享锁可以使用redisson实现,通过redissonClient获取对应的读写锁。
1.读锁也就是共享锁。
2.写锁也就是排他锁。
必须保证读写锁的名字相同。
问:那有了解过最终一致性的方法吗?
在我们支付模块中,每次支付都需要获取支付模板也就是支付宝支付和微信支付模板,这些数据我们基本上不会修改,会将其缓存起来,在修改mysql中模板的数据的时候,通过rabbitmq发送消息,异步修改缓存的数据,达到最终一致的效果。
为什么mq可以实现最终一致的效果呢?
mq中的消息都是按照顺序进行消费的,消息的消费是和事务绑定的,如果事务进行了回滚操作则被消费的消息也会被重新放回队列的原先位置中,并且接收到消息的服务会异步进行处理。
当然我们不仅仅可以使用rabbitmq实现最终一致的效果,我们还可以使用canal实现最终一致的效果。canal主要是通过mysql的主从同步实现的。通过监听bin Log日志的方式来修改缓存中的信息,达到最终一致的效果。(binLog日志主要是储存DDL(数据定义语句)和DML(数据操纵语句))
问:redis做为缓存,数据的持久化是怎么做的呢?
在持久化上用 RDB和AOF两种方式。
问:说说你对RDB和AOF的理解吧。
1.RDB:通过对数据做快照的方式做持久化,将快照存放到磁盘上,后续需要恢复数据的时间就使用该快照进行恢复。
RDB的执行原理:在主进程会有一个页表文件(用于映射数据在物理内存上的数据),通过复制该页表到子进程中,子进程通过页表找到数据并做快照。
当是如果在修改数据的过程中做RDB就会出现脏读的情况,RDB通过设置数据为只读,在修改数据的时候复制一份相同的数据进行修改如何修改页表的映射,最终解决脏读的问题。
2.AOF:在redis做操写的指令的时候,会将这些指令都存储到对应的AOF文件中,在后续数据需要恢复的时候就执行AOF文件中的所有指令。
AOF执行的原理:就是每次去记录操作写的指令到AOF文件中。
问:RDB和AOF有什么区别吗?
1.RDB是对整个内存做快照,而AOF是记录redis每一条执行的语句。
2.RDB的在两次备份间可能会出现数据丢失的情况(redis宕机)完整性低, 而AOF的完整性比较高,其取决去刷盘的策略。
3.RDB的文件比较小,而AOF会记录每日一条指令,所以AOF的文件比较大。
4.RDB的数据恢复比较快,因为AOF文件比较大,需要一条条的执行指令,恢复速度慢。
5.RDB系统占用高,需要大量的CPU和内存的消耗,而AOF系统占用比较小,只要是文件的IO操作,但是在AOF文件重写的时候需要大量的CPU和内存的消耗。
问:RDB和AOF谁的恢复速度更快,我们在平时要怎么选择呢?
RDB的快照文件本质是二进制文件,其体积比较小,而AOF文件需要保存redis中的写操作指令,在体积上比较大,所以在恢复速度上RDb比较快,但是RDB存在数据丢失的风险,在我的项目中只要主要还是使用AOF,像支付方式模板这种比较重要的数据,我们应当降低其丢失的风险,在刷盘策略上使用每秒进行一次刷盘,也就是每秒批量写入一次。
问:redis的key过期后,会立刻做删除操作吗?
redis有两种数据过期策略,分别是惰性删除和定期删除。
惰性删除:我们会为每个key设置一个过期时间,在每次获取数据的时候会先去判断该key是否过期,如果过期直接删除key,如果没有过期则直接返回,也就是说在没有使用数据时不会主动删除。(优点:对CPU友好,只在查询时才做过期判断。缺点:内存消耗大。)
定期删除:定期去判断一定数量的key是否过期,如果过期则直接进行删除操作,定期删除又分为SLOW模式和FAST模式。(优点:内存消耗小。缺点:定期查询key需要消耗大量时间。)
SLOW模式:默认的频率为10hz(一秒内进行十次),每次不大于25ms,我们可以通过配置文件中的hz修改频率。
FAST模式:执行频率是不固定的,两次删除的间隔不小于2ms,每次耗时小于1ms。
redis默认采用的数据过期删除策略:惰性删除+定期删除配合使用。
问:假如缓存过多,内存有限,内存满了怎么办呢?
在redis中提供了八种数据淘汰的策略,那默认的处理就是noeviction,就是直接报错。我们可以提供配置文件修改对应的淘汰策略。策略中有两种重要的思想,分别是LRU和LFU。
LRU:就是将当前使用间距最长的数据进行淘汰。
LFU:就是将当前使用频率最少的数据进行淘汰。
八个策略的不同就在于key的类型(全部key和设置过期时间的key)和使用的思想不同(LRU和LFU)。
在我的项目中主要使用的淘汰策略就是: allkey-lru,淘汰掉当前使用最少的数据。
问:数据库中有100w条数据,redis只能存储20w数据,如何保证redis中的数据是热点数据呢?
使用 allkey-lru策略,保证经常使用的数据不被淘汰。
问:redis缓存的空间用完了会怎么样?
如果是默认情况下的话,会直接报错,因为默认的淘汰策略就是noeviction,如果设置了其他的策略则会对数据进行淘汰。
问:能介绍一下redis的主从复制和主从复制的流程吗?
单个redis节点的并发能力是有限的,所以为了提高并发能力,我们需要搭建redis集群,就比如:主从复制。
主从复制的流程
主从复制主要分为:全量同步和增量同步。
全量同步:在salve请求数据同步的时候会携带application Id和offset,如果master判断出applid和自己的不一样,就认为slave是第一次进行同步,所以会进行全量同步。 master会执行bgsave生成RDB文件给slave,slave进行同步,在此过程中master可能会进行新的指令,master会将这些指令存储到日志文件,在加载RDB完后再将日志文件传给slave进行最终的同步,master同步applid和offset给slave。
增量同步: 在master判断出slave中的applid和自己一样就认为不是第一次同步,直接进行增量同步,从日志文件中获取到offset的位置,将offset之后的数据发送给slave,进行数据的同步。
问:那主节点宕机了又该怎么办呢?
为了提高redis集群的高可用,我们可以使用哨兵模式,解决主节点宕机的问题。
问:那说说你对哨兵模式的理解吧
哨兵模式:通过sentinel的心跳机制去监测master的状态,当然为了保证高可用,我们也需要对sentinel搭建集群。sentinel每隔1秒就会向集群的节点发送ping指令,当master失效后会就选择出新的master。
在sentinel中有两种概念:主观下线和客观下线。
主观下线:当有一个sentinel发现redis节点没有返回响应就认为其为主观下线。
客观下线:当有一半以上的sentinel节点发现redis节点没有返回响应就认为其为客观下线。
当master发生客观下线就会筛选slave作为新的master。
筛洗新master的优先级为下:
1.判断master与slave断开的时长,如果时长超过指指定值则直接排除。
2.判断slave的权重,如果权重越小优先级就越高。
3.如果权重相同的话,就比较slave的offst值也就是偏移量,如果offset越大优先级就越高。
4.判断slave运行id的大小,如果运行id越小则优先级越高。
问:哨兵模式可能会出现脑裂的情况,有了解过吗?
由于网络不稳定的因素,多个sentinel都没有ping到master,此时master是没有宕机的,而哨兵模式就会选择出新的master,就出现两个master的情况,而客户还是对旧的master进行数据的操作,在网络稳定后,旧的master就会变成新master的slave,最终导致数据操作的丢失。从而形成脑裂的现象。
解决脑裂的方案:
1.在redis的配置文件中设置最少的slave个数(对应的配置项:min-replicas-to-write)。当出现脑裂的情况时,旧的master会监测到没有slave,就不会做数据的操作直接返回错误信息,防止数据操作的丢失。
2.在resdis的配置文件中设置最大数据同步的延迟时间。(对应的配置项:min-replicas-max-lag)。当出现脑裂的情况时。旧的master在做数据的同步时一直找不到slave,当时间超过最大的延迟时间就会直接返回错误信息,防止数据操作的的丢失。
问:你们使用redis是单点还是集群?
我们的redis使用主从模式(一主一从)+ 哨兵模式。当然在容量不够的时候,会为不同的服务配置独立的redis主从节点。
问:redis分片集群有什么作用?
1,存在多个master,这些master存储不同的数据,多个master可以解决并发写的问题。
2.每一个master都可以有多个slave,解决并发读的问题。
3.在分片集群中,不再通过sentinel监测master的健康状态,而是通过master之间ping状态,判断各个master的健康状态,最终达到哨兵模式的效果。
4.客户端在访问对应的数据时会路由到对应的节点上。
问:redis分集群中的数据是如何存储和读取的呢?
redis分片集群采用的是哈希槽的结构实现的,哈希槽总共有16384个。master节点等量的获取哈希槽范围用于存储数据。
在存储数据的时候,通过有效部分取哈希槽总数的模计算出哈希值(这里的有效部分指key前大括号中的有效值(就比如: set {}key value),如果没有大括号key就是有效部分),找到master后做写操作。
在读数据的时候,通过计算出的哈希值确定存储数据的master位置,从该master对应的slave中读取数据,到达读写分离的效果。
问:redis是单线程的,为什么会那么快呢?
1.完全基于内存的,是C语言编写的。
2.采用单线程,避免了不必要的上下文切换可竞争条件,多线程还需要考虑线程问题。
3.采用多路IO复用模型,非阻塞IO模型。
问:说说对阻塞IO和非阻塞IO的理解吧。
我们先需要知道内存的使用情况为:用户空间和内核空间。
用户空间:只能执行受限制的指令,不能直接调用系统资源,需要通过内核提供的接口来调用系统资源。
内核空间:可以执行特权指令,可以直接调用系统资源。
阻塞IO:在用户线程要获取内核中获取数据,而此时内核中没有数据,用户线程就会等待从而导致用户线程阻塞,当内核中有数据后,数据需要重内核缓冲区复制到用户缓冲区,在这个过程中用户线程也需要等待从而导致线程堵塞。在这两个阶段中用户线程都是堵塞的,这就是堵塞IO。
非堵塞IO: 用户线程要从内核中获取数据,但内核中没有数据,此时内核直接返回错误信息给用户线程,反之线程堵塞,用户线程会循环的调用内核的方法直到内核中有数据,当内核中有数据后,用户线程就会等待内核缓冲区中的数据复制到用户缓冲区中,此时用户线程是阻塞的。在第一阶段是不阻塞的,而第二阶段是堵塞的,这就是非堵塞IO。比阻塞IO优化没多少,而且忙等机制可能会导致CPU的空转,CUP使用率暴增。
问:解释一下什么是多路IO复用模型?
单线程同时监听多个Socket(操作客户端)的状态,某个Socket可读可写时得到通知,防止出现忙等的情况,提高CPU的利用率,可能同时存在多个可用Socket,通过循环做读取数据的操作,多路IO复用主要通过epoll模式实现的,将已就绪的socket存到用户空间中,就不需要遍历判断socket是否就绪,从而提高性能。
问:有了解过redis的网络模型吗?
通过多路复用IO + 事件处理器实现的。事件处理器主要是:连接应答处理器,命令回复处理器,命令请求处理器。在redis6.0之后,使用多线程来处理命令的回复和命令的请求从而实现高效的网络请求,在命令执行的时候依旧是单线程(线程安全的)。
MySQL八股文
问:如何定位慢查询?
在我们的项目中,在上线时使用skywalking来定位慢的查询,如果发现是某个SQL执行速度慢,我们就可以使用skywalking的追踪功能,来确定SQL语句。
而在测试环境中,我们使用MySQL提供的慢日志来确定慢查询的位置。mysql是默认没有开启慢日志的,需要通过配置文件开启并设置快查询的最大时间,超过这个时间就认为其为慢查询,我们就可以在慢日志中找到慢查询的sql,在我们的项目中设置最大的时间为2秒。
#开启慢日志
slow_query_log=1
#设置快查询的最大时间
long_query_time=2
问:一个SQL语句执行很慢,应该如何分析呢?
我们可以借助Mysql提供的关键值 explain来展示出某个SQL语句的状态。
在该状态中包含属性 key和key_lenSQL中使用到的索引,如果提供我们的索引出现失效的情况就可以修改和SQL和添加索引。属性type表示SQL的性能,通常其值为const,提供type判断是否存在全索引扫描或全盘扫描。属性extra表示建议属性,提供该属性判断是否出现回表的情况。
问:有了解过索引吗?
1.索引是帮助Mysql高效查询数据的数据结构。
2.索引提高检索效率,大大降低IO成本。
3.通过索引列对数据排序,大大降低了排序的成本。
问:索引的底层数据结构有了解过吗?
MySQL中的InnoDB和Myisam的索引的底层都是B+树。
B+树的特点就是:
1.B+树层数较低路径更短,大大降低IO成本。
2.B+树只有在叶子节点上存储数据,在非叶子节点上存储指针,这个指针用于确定对应数据的叶子节点。
3.B+树非常适合做范围查询,因为叶子节点是有序的双向链表(左大于右)。
问:什么是聚簇索引和非聚簇索引?
聚簇索引:索引结构和数据是存放在一起的,也就是在B+树的叶子节点中存放整行的数据。
非聚簇索引(也叫二级索引):索引结构和数据不是存放在一起的,也就是在B+树的叶子节点上存放对应的主键且是不唯一的。我们为字段添加索引通常就是二级索引。
问:知道什么是回表操作吗?
通过二级索引查到的主键再去聚簇索引中查询数据行的过程就是回表。而直接查询聚簇索引则不会出现回表的情况。
问:有了过覆盖索引吗?
查询数据通过索引进行查询,返回列都可以在索引的数据中找到(包含在其中),就是覆盖查询。
使用id主键进行查询就是覆盖索引查询,因为聚簇索引的数据中包含id主键,性能高。
在做查询的时候如果返回列吧全部存在于索引中就会回表查询,所以尽量避免使用select *。
问:Mysql超大分页查询怎么进行优化?
Mysql做limit分页查询的时候,需要做排序,这个过程非常耗时。
优化方案:覆盖索引 + 子查询。先通过子查询出分页排序完后的id主键,因为是主键所以会直接进行覆盖索引查询。通过子查询的id关联表中的id查询出分页后的数据。
select * from table t
(select id from table limit 0 10 order by id) s
where t.id = s.id
问:索引的创建原则有哪些?
1.数据量大于十万且查询的频率表较高的表我们才会考虑创建索引。
2.如果一个表需要添加索引,我们应该选择作为查询字段,排序字段,分组字段的字段作为索引,且字段的区分度要高。
3.在添加索引的时候都使用复合索引来创建,尽量使用覆盖查询,降低回表的概率。
4.如果需要对长字符串添加索引我们可以使用前缀索引。
5.控制索引的数量,并不是越多越快,在增删改的时候我们也需要消耗时间来维护索引。
问:什么情况下索引会失效?
复合索引
1.在使用复合索引的时候不遵循最左前缀法则。在做条件查询的时候跳跃某一列字段导致索引失效。
2.在条件查询中的范围查询的右遍的列不能使用索引,使用也会失效。(如果三个都有效的话 key_len应该为六百多,说明此时address字段失效)
3.不能在索引列进行运算操作,这会导致索引失效。
4.在条件查询的时候如果没有加单引号也会导致索引失效。(就比如:0和'0',会进行类型转换,导致索引失效)
5. 在模糊查询的时候,如果字符串中是以%开头的就会导致索引失效。(就比如: "%abc")
在我遇到的随影失效问题就是没有遵循最左前缀原则,只要实在测试的时候通过explain查询SQL语句的执行状态来判断的。
问:谈谈你对SQL的优化经验?
在做SQL优化的时候主要从:建表时,使用索引时,sql语句编写,主从复制,读写分离的方面进行考虑,当数据量过大的时候考虑使用分库分表。
问:创建表的时候你是怎么优化的?
我们主要遵循阿里的开发手册,就比如在使用整数类型的时候就考虑使用:tinyInt,Int,bigInt,如果是逻辑字段就使用tinyInt,在使用字符串是考虑使用:char,varchar,text。
问:那在使用索引的时候如何进行优化?
讲出索引失效的五种情况,再使用SQL的时候避免使用select *,使用覆盖索引减少回表的操作。
问:你平时SQL语句是怎么优化的?
select 指明字段,不要使用select * from防止回表的操作。在使用聚合查询的时候尽量使用union all而不是union,union会多一次过滤,在效率上比较低。使用inner join 而不使用 left join/right join,如果必须使用的,一定要以小表为驱动。
问:事务的特性是什么?可以详细说一下?
这里你可以取钱的例子来引导模式官。
原子性:在事务中的语句要么都成功要么都失败。
一致性:在事务中数据的总量不会变。
持久性: 提交和会滚的数据都会持久化到数据库。
隔离性:事务中间是是相互隔离的,是不会相互影响的。
问:并发事务带来了哪些问题?
并发事务可能会出现三种问题。
1.脏读:事务1读取到事务2未提交的数据。
2.不可重复读:事务2先后读取事务1中的某合个数据,两次的结果不一样。
3.幻读:一个事务在按条件查询数据时没有查到数据吗,但是插入操作时,又发现该数据已经存在。因为其他事务在这个过程中插入数据(选答)
怎么解决解决这些问题?
通过设置过隔离级别来解决。隔离级别包括:
1.读取未提交,无法解决并发事务带来的问题。
2.读取已提交,可以解决脏读。
3.可重复读,可以解决脏读,不可重复读。
4.串行化,事务只能一个一个执行,可以解决脏读,不可重复读,幻读,隔离级别最高,效率最低。
MySQL默认的隔离级别是什么?
Mysql默认使用的隔离级别是:可重复读。
问:undo log和rado log有什么区别?
redo log:用于记录数据页的物理变化,当服务宕机的时候进行数据同步操作。保证了事务的持久性。
undo log:记录逻辑日志,就比如:当做插入操作时会在日志中记录逆向的操作也即是删除,在事务回滚的时候会执行逻辑日志中的指令。保证了事务的持久性和原子性。
问:隔离级别是怎么实现的?
排他锁+MVCC实现的。
问:说说你对MVCC的理解吧?
多版本并发控制。维护一个数据的多个版本,使得读写操作没有冲突。
mvcc主要有三个重点:
1.隐藏字段:trx_id(事务id):记录当前事务的id,其为自增的。 roll-pointer:指向上一个版本的事务记录地址。
2.undo log:回滚日志,存储老版本的数据,版本链:多个同时修改某条记录,产生多版本的数据,通过rool-pointer指针形成链表。
3.readview:解决一个事务查询选择版本的问题。
根据readView的匹配规则和当前事务id找到对应的版本信息。(问规则时答:1.判断是事务id是否为当前事务的id。2.是否是活跃事务id。3.判断事务是否是在readview创建后开启的,也就是事务id大于当前事务。4.判断事务中的数据是否已提交,事务id小于最小的事务id。)
不同的隔离级别快照读是不一样的,最终的访问结果也是不一样的。(问时答:当前读:读取的是最新的数据并且会加锁。快照读:读取的是记录数据的可见版本,不会加锁。)
读已提交:在每次快照读的时候都会生成readview。
可重复读:在有在第一次快照读的时候才会生成readview,后续的快照读都是使用该readview的复制,保证数据的一致性。
问:MySQL的主从同步有了解过吗?
Mysql主从同步的核心就是bin log(二进制日志),这个日志中主要记录 DDL(表的操作),DML(表中数据的操作)。
1.master中事务提交数据后,会将修改的数据保存到bin log中。
2.slave有个iothread线程会监控的bin log的变化,并将变化写入relay log中。
3.slave有个SQLthread线程会监控relay log,将改变的数据写入slave中。
问:你在项目中有使用过分库分表吗?
在物流项目中的订单服务的数据非常庞大,请求数多且业务累计大。差不多单表的数据有100w条,这时我们就使用分库分表。
分库分表有四种策略:
1.水平分库:通过将一个库中的数据拆分到多个库中,解决海量数据存储和高并发的问题。主要通过sharing-sphere和mtycat实现。
2.水平分表:解决单表存储和性能的问题。
3.垂直分库:根据业务来拆分库中的表,在高并发的情况下提高磁盘IO和网络连接数。每个微服务都有自己的表。
4.垂直分表:冷热数据分离,多表不会相互影响。就比如:表中字段为id,name,des,将id,name和des分离,id和name都是热数据而des为冷数据,访问频率较低。
框架八股文
问:spring框架的单例bean是线程安全的吗?
在spring框架中有个注解叫@Scope可以设置bean的状态,默认就是singleton也就是单例。
bean进行注入的时都是无状态的,其不会被修改的。所以没有线程安全的问题。但是如果bean中有成员变量时就可能会有线程安全的问题,因为该成员变量可能会被多个线程修改,为了解决这个问题我们可以加锁或将bean设置为多例。(@Scope设置为prototype)
问:什么是AOP?
面向切面编程,将那些于业务无关的复用性比较高的代码快抽取出来,较低代码的耦合度。
问:在你的项目中有使用过AOP吗?
在我的云盘项目中就使用到AOP,在记录日志的时候,我创建有个自定义注解,aop的切面就是这个注解,使用环绕通知在方法中,我们通过传入的参数(joinPoint)获取对应的类和方法,从而获取前端传来的参数和其他主要信息,实现记录日志的效果。
问:Spring中的事务是怎么实现的?
本质就是通过AOP实现的,通过环绕通知对应方法进行前后拦截,在方法执行前开启事务,在执行后提交事务,会对此过程进行try/catch,如果报错直接回滚。(就是@transactional)
问:spring中事务失效场景有哪些?
1.在出现异常后,方法中try/catch了该异常并且没有主动抛出异常,这时候就会导致事务失效。解决方法:在方法try/catch异常后手动的抛出异常。
2.抛出检查异常时会导致事务失效,spring中的事务只会对runtime异常进行回滚。就比如:Not found Exception就是检查异常。解决方法:在@transactional中设置属性 rollbackFor = Exception.class。使得事务会对所有的异常进行回滚。
3.非public方法会导致事务失效。解决方法:将方法的作用域该为public。
问:spring中bean的生命周期有了解过吗?
结构图:
生命周期的流程为下:
1.通过BeanDefinition获取bean的定义信息。
2.通过构造函数创建bean,可以将当前的bean理解为一个空壳。
3.进行依赖注入,对bean中的属性进行赋值。
4.处理Aware接口,也就是一些以Aware结尾的接口,就比如:beanNameAware,beanFactoryAware,applicationContextAware。如果实现了个接口的话,我们需要重写一些方法。
5.执行bean的后置处理器BeanPostProcessor-before(前置的后置处理器)。
6.执行初始化方法,包括 IntializingBean和自定义的初始化方法。
7.执行bean的后置处理器BeanPostProcessor-force(后置的后置处理器)。在此后置处理器中我们可以通过aop对原始的bean做增强也就是进行代理,代理就包括 JVM代理和CGLIB代理。
8.将bean进行销毁。
问:有了解过bean的循环依赖吗?
循环依赖也就是循环引用,两个或以上的bean同时相互依赖对方,最终形成闭环。就比如:A依赖B,B依赖A。
spring提供了解决方案:三级缓存。
1.一级缓存:单例池,存储已经初始化完成的单例bean。
2.二级缓存:缓存早期的bean单例对象,就bean只完成到执行构造方法。
3.三级缓存:缓存创建bean的factory,这些factory用于创建代理对象和普通对象。
三级缓存的解决流程(当前问题为A,B相互依赖):
实例化A,并将生成的A的objectFacttory将其存入三级缓存,因为A依赖B,B也会去实例化,创建对应的objectFactory存入三级缓存。
此时B依赖A,就会通过三级缓存中A的objectFactory生成早期A实例,并将该A实例存储到二级缓存中,将这个早期的A注入B中,此时B就创建完成了,将B实例存储到一级缓存中。
将完整的B注入A中,将A实例存储到一级缓存中,并将二级缓存中A的实例删除。
问:构造方法中出现循环依赖怎么办?
在构造函数中如果存在循环依赖我们在函数的参数上添加@Lazy就可以解决循环依赖的问题,保证bean在需要的时候才去加载。
问:SpringMVC的执行流程有了解过吗?
jsp版本:
1.用户发送请求到DispatchServlet。
2.dispatchServlet调用处理器映射器HandlerMappering,处理器就会去Controller中找到对应的方法通过映射的路径,然后将处理器执行链返回给dispatchServlet。
3.dispatchServlet调用处理器适配器HandlerAdaptor找到对应的处理器,该处理器就会处理对应的参数和处理返回值,最终会返回ModelAndView给DispatchServlet。
4.Dispatch调用ViewResolver视图解析器,并将ModelAndView传入,最终返回View给dispatchServlet,通过View渲染视图。(就比如:JSP)
前后端分离版本:
1.用户发送请求搭配DispatchServlet。
2.DispatchServlet调用处理器映射器HandlerMappering,会去Controller中找到对应的方法,最终处理器返回执行链给DispatchServlet。
3.DispatchServlet会调用处理器适配器HandlerAdaptor找到对应的处理器,该处理器就会处理对应的方法的参数和处理返回值,在方法上添加了@ReponseBody。
4.通过HttpMessageConverter将数据转为Json返回。
问:有了解过springboot的自动装配原理吗?
在启动类上有个注解的叫@SpringbootApplication,在该注解中包含springbootConfiguration表示该类为配置类,还有个注解叫@EnableAutoConfiguration,这个就是实现自动装配的核心注解。
在@EnableAutoConfiguration中使用注解@Import引入了一个自动自动装配的选择器。
该选择器会到jar中的 /META-INF/spring.factories中按照条件加载对应的类,并将该类配置到ioc中。这里面添加的注解就包括:@ConditionOnClass表示当某个类存在时才进行类加载。@ConditionOnMissingBean表示当某个bean不存在的时候才进行类加载。
问:spring中有那些常见的注解?
在Spring中主要注解有:@Componment,@Controller,@Service,@Repository将类配置到ioc中,@AutoWired(根据class),@Qualifier(根据名字)实现依赖注入。@scope设置bean的作用范围。@Configration设置配置类。@ComponetScan主键扫描。@Bean将某个方法的返回值配置到ioc中。@Import将某类导入ioc中。@Before,@After,@Aspecr,@Around,@Pointcut切面编程的注解。
在SpringMVC中就是一些关于请求的注解,@RequestMappering,@ReponseBody,@RestController,已经参数的注解 @RequestParam,@PathViriable,@RequestHeader。
在springboot的注解中就包括:@SpringBootConfiguration,@EnableAutoConfiguration自动装配注解,@ComponentScan之间扫描。
问:能说说你对Mybatis的执行流程的理解吗?
1.读取mybatis-config.xml文件,里面就是数据库的配置和mapper的地址。
2.创建SqlSessionFactory。
3.通过SqlSessionFactory创建对应的SqlSession,就是项目和数据库的会话,SqlSession包含执行sql语句的所有方法。
4.执行操作数据库的接口,Executor执行器,同时负责缓存的维护。
5.在Executor执行器中的MapperStatement对象,当操作数据库的时候会将Java的类型转为数据库的类型,当输出结果的时候会将数据库的类型转为Java的类型。
问:Mybatis支持延迟加载吗?
1.延迟加载就是当需要使用数据的时候才去加载数据,不用数据得时候不会主动加载。
2.Mybatis支持一对一关联对象和一对多关联集合的延迟加载。
3.在mybatis的配置文配置文件中的LazyloadEnabled设置为true就开启了全局延迟加载。
问:Mybatis延迟加载的底层有了解过吗?
1.通过GCLIB的代理实现的。
2.当调用目标函数的时候,会调用拦截器的invoke方法,如果发现目标方法中的值为null,则进行sql查询,在获取数据后通过set设置属性值,在后续调用目标方法时就有值了。