文章目录
- 1. Redis数据库篇(忽略)
- 1.1 简单介绍一下redis
- 1.2 单线程的redis为什么读写速度快?
- 1.3 redis为什么是单线程的?
- 1.4 redis服务器的的内存是多大?
- 1.5 为什么Redis的操作是原子性的,怎么保证原子性的?
- 1.6 你还用过其他的缓存吗?这些缓存有什么区别?都在什么场景下去用?
- 1.7 Redis在你们项目中是怎么用的?
- 1.8 对redis的持久化了解不?
- 1.9 redis的过期策略以及内存淘汰机制有了解过吗
- 1.10 做过redis的集群吗?你们做集群的时候搭建了几台,都是怎么搭建的?
- 1.11 说说Redis哈希槽的概念?
- 1.12 redis有事务吗?
- 1.13 是否了解过redis的安全机制?
- 1.14 你对redis的哨兵机制了解多少?
- 1.15 redis缓存与mysql数据库之间的数据一致性问题?
- 1.16 什么是redis的缓存穿透?如何防止穿透?
- 1.17 什么是redis的缓存雪崩?如何防止?
- 1.18 什么是redis的缓存击穿?如何防止?
- 1.19 redis中对于生存时间的应用
- 2. MQ消息队列(忽略!!!)
- 2.1 为什么使用rabbitmq?
- 2.2 如何保证消息的可靠性传输/如何处理消息丢失问题?
- 2.3 如何保证消息不被重复消费?
- 2.4 消息积压问题
- 2.5 消息在什么时候会变成Dead Letter(死信)?
- 2.6 RabbitMQ如何实现延时队列?
- 3. kafka
- 3.1 消息队列的作用
- 3.2 Kafka的主要组件
- 3.3 Kafka如何保证消息可靠性(消息不丢失)
- 3.4 Kafka为什么要分区,都有哪些分区策略?
- 3.5 Kafka消费者的幂等性如何保证
- 3.6 Kafka如何保证消息有序性
- 生产有序性
- 消费有序性
- 3.7 Kafka如何避免消息堆积
- 3.8 为什么使用Kafka
- 4. SpringBoot+SpringCloud
- 4.1 为什么要用 Spring Boot?
- 4.2 Spring Boot 的核心注解是哪个?它主要由哪几个注解组成的?
- 4.3 Spring Boot 自动配置原理是什么?
- 4.4 你如何理解 Spring Boot 中的 Starters?
- 4.5 Spring Boot、Spring 和Spring Cloud什么关系
- 4.6 Ribbon和Feign的区别?
- 4.7 LoadBalancer/Ribbon负载均衡能干嘛?
- 4.8 你所知道的微服务技术栈有哪些?请列举一二
- 4.7 Nacos
- 4.7.1 什么是Nacos,主要用来作什么?
- 4.7.2 Nacos是AP的还是CP的?
- 4.8 Spring Cloud Alibaba断路器Sentinel的作用是什么?
- 4.9 什么是服务熔断?什么是服务降级?
- 5. Mysql
- 5.1 MySQL多表连接有哪些方式?怎么用的?这些连接都有什么区别?
- 5.2 说一下索引的优势和劣势?
- 5.3 MySQL聚簇和非聚簇索引的区别
- 5.4 MySQL中B+树和B树的区别
- 5.5 Mysql有哪些锁?
- 1、基于锁的属性分类:
- 2、基于锁的粒度分类:
- 5.6 关心过业务系统里面的sql耗时吗?统计过慢查询吗?对慢查询都怎么优化过?
- 5.7 MySQL事务的基本特性和隔离级别
- 5.8 Mysql的MVCC是什么
- 5.9 Mysql常见优化手段
- 5.10 sql题
- 1
- 2
- 3
- 6. JVM&JUC
- 6.1 说一下JVM的主要组成部分?及其作用?
- 6.2 说一下类装载的过程
- 6.3 怎么判断对象是否可以被回收(垃圾判断算法)
- 6.4 说一下JVM有哪些垃圾回收算法
- 6.5 说一下JVM有哪些垃圾回收器
- 6.6 说一下JVM调优的工具
- 6.7 常用的JVM调优的参数都有哪些
- 6.8 什么是线程池,JDK提供的线程池有哪些
- 6.9 线程池底层工作原理
- 6.10 ThreadPoolExecutor对象有哪些参数 怎么设定核心线程数和最大线程数 拒绝策略有哪些
- 6.11 常见线程安全的并发容器有哪些
- 6.12 synchronized底层实现是什么 lock底层是什么 有什么区别
- 6.13 ConcurrentHashMap为什么性能比HashTable高,底层原理是什么?
- 6.14 了解volatile关键字不,synchronized和volatile有什么区别
- 6.15 线程通信
- 7. 其它
- 7.1 Elasticsearch中的倒排索引是什么?
- 7.2 git如何解决代码冲突
- 7.3 Linux
- 7.3.1 Linux常用命令
- 7.3.2 如何查看测试项目的日志
- 7.3.3 LINUX中如何查看某个端口是否被占用
- 7.3.4 vim(vi)编辑器
- 7.4 写出Docker关于镜像容器的命令操作至少5个
- 7.5 什么是IaaS、PaaS、SaaS?
- 7.6 请画出云上高并发架构图
- 7.7 Nginx中的负载均衡算法有哪些 并做出解释
- 7.8 如何在 Nginx 中实现 IP 黑名单?
- 7.9 解释 Nginx 的 Master-Worker 架构。
- 7.10 冒泡排序(Bubble Sort)
- 7.11 快速排序(Quick Sort)
- 7.12 二分查找(Binary Search)
- 7.13 二叉搜索树的遍历
- 7.14 如何理解时间复杂度和空间复杂度
1. Redis数据库篇(忽略)
1.1 简单介绍一下redis
为什么不直接操作内存呢?
【答案解析】
(1)redis是一个key-value类型的非关系型数据库,基于内存也可持久化的数据库,相对于关系型数据库(数据主要存在硬盘中),性能高,因此我们一般用redis来做缓存使用;并且redis支持丰富的数据类型,比较容易解决各种问题
(2)Redis的Value支持5种数据类型,string、hash、list、set、zset(sorted set);
以下为5中类型比较经典的使用场景
类型 | 使用场景 |
---|---|
string | String类型是最简单的类型,一个key对应一个value,项目中我们主要利用单点登录中的token用string类型来存储;商品详情 |
hash | Hash类型中的key是string类型,value又是一个map(key-value),针对这种数据特性,比较适合存储对象,在我们项目中由于购物车是用redis来存储的,因此选择redis的散列(hash)来存储; |
list | List类型是按照插入顺序的字符串链表(双向链表),主要命令是LPOP和RPUSH,能够支持反向查找和遍历,如果使用的话主要存储商品评论列表,key是该商品的ID,value是商品评论信息列表;消息队列 |
set | Set类型是用哈希表类型的字符串序列,没有顺序,集合成员是唯一的,没有重复数据,底层主要是由一个value永远为null的hashmap来实现的。可以基于 Set 玩儿交集、并集、差集的操作,比如交集吧,我们可以把两个人的好友列表整一个交集,看看俩人的共同好友是谁? |
zset | zset(sorted set)类型和set类型基本是一致的,不同的是zset这种类型会给每个元素关联一个****double****类型的分数(score),这样就可以为成员排序,并且插入是有序的。这种数据类型如果使用的话主要用来统计商品的销售排行榜,比如:items:sellsort 10 1001 20 1002 这个代表编号是1001的商品销售数量为10,编号为1002的商品销售数量为20/附件的人 |
1.2 单线程的redis为什么读写速度快?
-
纯内存操作
-
单线程操作,避免了频繁的上下文切换
-
采用了非阻塞I/O多路复用机制
1.3 redis为什么是单线程的?
官方FAQ表示,因为Redis是基于内存的操作,CPU不是Redis的瓶颈,Redis的瓶颈最有可能是机器内存的大小或者网络带宽。既然单线程容易实现,而且CPU不会成为瓶颈,那就顺理成章地采用单线程的方案了Redis利用队列技术将并发访问变为串行访问
1)绝大部分请求是纯粹的内存操作
2)采用单线程,避免了不必要的上下文切换和竞争条件
1.4 redis服务器的的内存是多大?
配置文件中设置redis内存的参数:maxmemory
该参数如果不设置或者设置为0,则redis默认的内存大小为:
32位下默认是3G
64位下不受限制
一般推荐Redis设置内存为最大物理内存的四分之三,也就是0.75
命令行设置config set maxmemory <内存大小,单位字节>,服务器重启失效
config get maxmemory获取当前内存大小
永久则需要设置maxmemory参数,maxmemory是bytes字节类型,注意转换
1.5 为什么Redis的操作是原子性的,怎么保证原子性的?
对于Redis而言,命令的原子性指的是:一个操作的不可以再分,操作要么执行,要么不执行。
Redis的操作之所以是原子性的,是因为Redis是单线程的。
Redis本身提供的所有API都是原子操作,Redis中的事务其实是要保证批量操作的原子性。
多个命令在并发中也是原子性的吗?
不一定, 将get和set改成单命令操作,incr 。使用Redis的事务,或者使用Redis+Lua==的方式实现.
1.6 你还用过其他的缓存吗?这些缓存有什么区别?都在什么场景下去用?
对于缓存了解过redis和memcache,redis我们在项目中用的比较多,memcache没用过,但是了解过一点;
Memcache和redis的区别:
比较项 | reids | memcache |
---|---|---|
存储方式 | redis可以持久化其数据 | Memecache把数据全部存在内存之中,断电后会挂掉,数据不能超过内存大小 |
数据支持 | 支持丰富的数据类型,提供list,set,zset,hash等数据结构的存储 | memcached所有的值均是简单的字符串 |
底层模型 | Redis直接自己构建了VM 机制 | 无 |
value值大小 | Redis 最大可以达到 512M | value 不能超过 1M 字节 |
速度 | Redis 采用单线程模式处理请求。这样做的原因有 2 个:一个是因为采用了非阻塞的异步事件处理机制;另一个是缓存数据都是内存操作 IO 时间不会太长,单线程可以避免线程上下文切换产生的代价 | MC 处理请求时使用多线程异步 IO 的方式,可以合理利用 CPU 多核的优势,性能非常优秀 |
数据备份 | Redis支持数据的备份,即master-slave模式的数据备份,能够提供高可用服务 | 当容量存满时,会对缓存中的数据进行剔除,剔除时除了会对过期 key 进行清理,还会按 LRU 策略对数据进行剔除。 |
应用场景 | 适用于对读写效率要求高、数据处理业务复杂、安全性要求较高的系统 | 适合多读少写,大数据量的情况(一些官网的文章信息等) |
1.7 Redis在你们项目中是怎么用的?
【答案解析】
(1)门户(首页)系统中的首页内容信息的展示。(商品类目、广告、热门商品等信息)门户系统的首页是用户访问量最大的,而且这些数据一般不会经常修改,因此为了提高用户的体验,我们选择将这些内容放在缓存中;
(2)单点登录系统中也用到了redis。因为我们是分布式系统,存在session之间的共享问题,因此在做单点登录的时候,我们利用redis来模拟了session的共享,来存储用户的信息,实现不同系统的session共享;
(3)我们项目中同时也将购物车的信息设计存储在redis中,购物车在数据库中没有对应的表,用户登录之后将商品添加到购物车后存储到redis中,key是用户id,value是购物车对象;
(4)因为针对评论这块,我们需要一个商品对应多个用户评论,并且按照时间顺序显示评论,为了提高查询效率,因此我们选择了redis的list类型将商品评论放在缓存中;
(5)在统计模块中,我们有个功能是做商品销售的排行榜,因此选择redis的zset结构来实现;
还有一些其他的应用场景,主要就是用来作为缓存使用。
1.8 对redis的持久化了解不?
Redis是内存型数据库,同时它也可以持久化到硬盘中,redis的持久化方式有两种:
(1)RDB(半持久化方式):
按照配置不定期的通过异步的方式、快照的形式直接把内存中的数据持久化到磁盘的一个dump.rdb文件(二进制文件)中;
这种方式是redis默认的持久化方式,它在配置文件(redis.conf)中的格式是:save N M,表示的是在N秒之内发生M次修改,则redis抓快照到磁盘中;
原理:当redis需要持久化的时候,redis会fork一个子进程,这个子进程会将数据写到一个临时文件中;当子进程完成写临时文件后,会将原来的.rdb文件替换掉,这样的好处是写时拷贝技术(copy-on-write),可以参考下面的流程图;
优点:只包含一个文件,对于文件备份、灾难恢复而言,比较实用。因为我们可以轻松的将一个单独的文件转移到其他存储媒介上;性能最大化,因为对于这种半持久化方式,使用的是写时拷贝技术,可以极大的避免服务进程执行IO操作;相对于AOF来说,如果数据集很大,RDB的启动效率就会很高
缺点:如果想保证数据的高可用(最大限度的包装数据丢失),那么RDB这种半持久化方式不是一个很好的选择,因为系统一旦在持久化策略之前出现宕机现象,此前没有来得及持久化的数据将会产生丢失;rdb是通过fork进程来协助完成持久化的,因此当数据集较大的时候,我们就需要等待服务器停止几百毫秒甚至一秒;
(2)AOF(全持久化的方式)
把每一次数据变化都通过write()函数将你所执行的命令追加到一个appendonly.aof文件里面;
Redis默认是不支持这种全持久化方式的,需要将no改成yes
实现文件刷新的三种方式:
no:不会自动同步到磁盘上,需要依靠OS(操作系统)进行刷新,效率快,但是安全性就比较差;
always:每提交一个命令都调用fsync刷新到aof文件,非常慢,但是安全;
everysec:每秒钟都调用fsync刷新到aof文件中,很快,但是可能丢失一秒内的数据,推荐使用,兼顾了速度和安全;
原理:redis需要持久化的时候,fork出一个子进程,子进程根据内存中的数据库快照,往临时文件中写入重建数据库状态的命令;父进程会继续处理客户端的请求,除了把写命令写到原来的aof中,同时把收到的写命令缓存起来,这样包装如果子进程重写失败的话不会出问题;当子进程把快照内容以命令方式写入临时文件中后,子进程会发送信号给父进程,父进程会把缓存的写命令写入到临时文件中;接下来父进程可以使用临时的aof文件替换原来的aof文件,并重命名,后面收到的写命令也开始往新的aof文件中追加。下面的图为最简单的方式,其实也是利用写时复制原则。
优点:
数据安全性高
该机制对日志文件的写入操作采用的是append模式,因此在写入过程中即使出现宕机问题,也不会破坏日志文件中已经存在的内容;
缺点:
对于数量相同的数据集来说,aof文件通常要比rdb文件大,因此rdb在恢复大数据集时的速度大于AOF;
根据同步策略的不同,AOF在运行效率上往往慢于RDB,每秒同步策略的效率是比较高的,同步禁用策略的效率和RDB一样高效;
针对以上两种不同的持久化方式,如果缓存数据安全性要求比较高的话,用aof这种持久化方式(比如项目中的购物车);如果对于大数据集要求效率高的话,就可以使用默认的。而且这两种持久化方式可以同时使用。
1.9 redis的过期策略以及内存淘汰机制有了解过吗
redis采用的是定期删除+惰性删除策略。
为什么不用定时删除策略?
定时删除,用一个定时器来负责监视key,过期则自动删除。虽然内存及时释放,但是十分消耗CPU资源。在大并发请求下,CPU要将时间应用在处理请求,而不是删除key,因此没有采用这一策略.
定期删除+惰性删除是如何工作的呢?
定期删除,redis默认每个100ms检查,是否有过期的key,有过期key则删除。需要说明的是,redis不是每个100ms将所有的key检查一次,而是随机抽取进行检查(如果每隔100ms,全部key进行检查,redis岂不是卡死)。因此,如果只采用定期删除策略,会导致很多key到时间没有删除。
于是,惰性删除派上用场。也就是说在你获取某个key的时候,redis会检查一下,这个key如果设置了过期时间那么是否过期了?如果过期了此时就会删除。
采用定期删除+惰性删除就没其他问题了么?
不是的,如果定期删除没删除key。然后你也没即时去请求key,也就是说惰性删除也没生效。这样,redis的内存会越来越高。那么就应该采用内存淘汰机制。
在redis.conf中有一行配置
maxmemory-policy volatile-lru
该配置就是配内存淘汰策略的(什么,你没配过?好好反省一下自己)
volatile-lru:从已设置过期时间的数据集(server.db[i].expires)中挑选最近最少使用的数据淘汰
volatile-ttl:从已设置过期时间的数据集(server.db[i].expires)中挑选将要过期的数据淘汰
volatile-random:从已设置过期时间的数据集(server.db[i].expires)中任意选择数据淘汰
allkeys-lru:从数据集(server.db[i].dict)中挑选最近最少使用的数据淘汰
allkeys-random:从数据集(server.db[i].dict)中任意选择数据淘汰
no-enviction(驱逐):禁止驱逐数据,新写入操作会报错
ps:如果没有设置 expire 的key, 不满足先决条件(prerequisites); 那么 volatile-lru, volatile-random 和 volatile-ttl 策略的行为, 和 noeviction(不删除) 基本上一致。
1.10 做过redis的集群吗?你们做集群的时候搭建了几台,都是怎么搭建的?
针对这类问题,我们首先考虑的是为什么要搭建集群?(这个需要针对我们的项目来说)
Redis的数据是存放在内存中的,这就意味着redis不适合存储大数据,大数据存储一般公司常用hadoop中的Hbase或者MogoDB。因此redis主要用来处理高并发的,用我们的项目来说,电商项目如果并发大的话,一台单独的redis是不能足够支持我们的并发,这就需要我们扩展多台设备协同合作,即用到集群。
Redis搭建集群的方式有多种,例如:客户端分片、Twemproxy、Codis等,但是redis3.0之后就支持redis-cluster集群,这种方式采用的是无中心结构,每个节点保存数据和整个集群的状态,每个节点都和其他所有节点连接。如果使用的话就用redis-cluster集群。
集群这块直接说是公司运维搭建的,小公司的话也有可能由我们自己搭建,开发环境我们也可以直接用单机版的。但是可以了解一下redis的集群版。搭建redis集群的时候,对于用到多少台服务器,每家公司都不一样,大家针对自己项目的大小去衡量。举个简单的例子:
我们项目中redis集群主要搭建了6台,3主(为了保证redis的投票机制)3从(
【扩展】高可用),每个主服务器都有一个从服务器,作为备份机。
1、架构图如下:
(1)所有的节点都通过PING-PONG机制彼此互相连接;
(2)每个节点的fail是通过集群中超过半数的节点检测失效时才生效;
(3)客户端与redis集群连接,只需要连接集群中的任何一个节点即可;
(4)Redis-cluster把所有的物理节点映射到【0-16383】slot上,负责维护
2、容错机制(投票机制)
(1)选举过程是集群中的所有master都参与,如果半数以上master节点与故障节点连接超过时间,则认为该节点故障,自动会触发故障转移操作;
(2)集群不可用?
a:如果集群任意master挂掉,并且当前的master没有slave,集群就会fail;
b:如果集群超过半数以上master挂掉,无论是否有slave,整个集群都会fail;
1.11 说说Redis哈希槽的概念?
Redis集群没有使用一致性hash,而是引入了哈希槽的概念,Redis集群有16384个哈希槽,每个key通 过CRC16校验后对16384取模来决定放置哪个槽,集群的每个节点负责一部分hash槽。
1.12 redis有事务吗?
Redis是有事务的,redis中的事务是一组命令的集合,这组命令要么都执行,要不都不执行,redis事务的实现,需要用到MULTI(事务的开始)和EXEC(事务的结束)命令 ;
当输入MULTI命令后,服务器返回OK表示事务开始成功,然后依次输入需要在本次事务中执行的所有命令,每次输入一个命令服务器并不会马上执行,而是返回”QUEUED”,这表示命令已经被服务器接受并且暂时保存起来,最后输入EXEC命令后,本次事务中的所有命令才会被依次执行,可以看到最后服务器一次性返回了两个OK,这里返回的结果与发送的命令是按顺序一一对应的,这说明这次事务中的命令全都执行成功了。
Redis的事务除了保证所有命令要不全部执行,要不全部不执行外,还能保证一个事务中的命令依次执行而不被其他命令插入。同时,redis的事务是不支持回滚操作的。
【扩展】
Redis的事务中存在一个问题,如果一个事务中的B命令依赖上一个命令A怎么办?
这会涉及到redis中的WATCH命令:可以监控一个或多个键,一旦其中有一个键被修改(或删除),之后的事务就不会执行,监控一直持续到EXEC命令(事务中的命令是在EXEC之后才执行的,EXEC命令执行完之后被监控的键会自动被UNWATCH)。
应用场景:待定
1.13 是否了解过redis的安全机制?
1、redis的安全机制(你们公司redis的安全这方面怎么考虑的?)
漏洞介绍:redis默认情况下,会绑定在bind 0.0.0.0:6379,这样就会将redis的服务暴露到公网上,如果在没有开启认证的情况下,可以导致任意用户在访问目标服务器的情况下未授权访问redis以及读取redis的数据,攻击者就可以在未授权访问redis的情况下可以利用redis的相关方法,成功在redis服务器上写入公钥,进而可以直接使用私钥进行直接登录目标主机;
比如:可以使用FLUSHALL方法,整个redis数据库将被清空
解决方案:
(1)禁止一些高危命令。修改redis.conf文件,用来禁止远程修改DB文件地址,比如 rename-command FLUSHALL “” 、rename-command CONFIG"" 、rename-command EVAL “”等;
(2)以低权限运行redis服务。为redis服务创建单独的用户和根目录,并且配置禁止登录;
(3)为redis添加密码验证。修改redis.conf文件,添加
requirepass mypassword;
(4)禁止外网访问redis。修改redis.conf文件,添加或修改 bind 127.0.0.1,使得redis服务只在当前主机使用;
(5)做log监控,及时发现攻击;
(6)服务器不安装
1.14 你对redis的哨兵机制了解多少?
哨兵机制:
监控:监控主数据库和从数据库是否正常运行;
提醒:当被监控的某个redis出现问题的时候,哨兵可以通过API向管理员或者其他应用程序发送通知;
自动故障迁移:主数据库出现故障时,可以自动将从数据库转化为主数据库,实现自动切换;
具体的配置步骤面试中可以说参考的网上的文档。要注意的是,如果master主服务器设置了密码,记得在哨兵的配置文件(sentinel.conf)里面配置访问密码
1.15 redis缓存与mysql数据库之间的数据一致性问题?
不管先保存到MySQL,还是先保存到Redis都面临着一个保存成功而另外一个保存失败的情况。
不管是先写MySQL数据库,再删除Redis缓存;还是先删除缓存,再写库,都有可能出现数据不一致的情况。举一个例子:
1.如果删除了缓存Redis,还没有来得及写库MySQL,另一个线程就来读取,发现缓存为空,则去数据库中读取数据写入缓存,此时缓存中为脏数据。
2.如果先写了库,在删除缓存前,写库的线程宕机了,没有删除掉缓存,则也会出现数据不一致情况。
因为写和读是并发的,没法保证顺序,就会出现缓存和数据库的数据不一致的问题。
解决:
基于mysql的binlog日志(canal)
消息队列(双删)
1.16 什么是redis的缓存穿透?如何防止穿透?
缓存穿透是指查询一个不存在的数据,由于缓存无法命中,将去查询数据库,但是数据库也无此记录,并且出于容错考虑,我们没有将这次查询的null写入缓存,这将导致这个不存在的数据每次请求都要到存储层去查询,失去了缓存的意义。在流量大时,可能DB就挂掉了,要是有人利用不存在的key频繁攻击我们的应用,这就是漏洞。
解决:空结果也进行缓存,但它的过期时间会很短,最长不超过五分钟。
1.17 什么是redis的缓存雪崩?如何防止?
缓存雪崩是指在我们设置缓存时采用了相同的过期时间,导致缓存在某一时刻同时失效,请求全部转发到DB,DB瞬时压力过重雪崩。
解决:原有的失效时间基础上增加一个随机值,比如1-5分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件。
1.18 什么是redis的缓存击穿?如何防止?
缓存击穿是指对于一些设置了过期时间的key,如果这些key可能会在某些时间点被超高并发地访问,是一种非常“热点”的数据。这个时候,需要考虑一个问题:如果这个key在大量请求同时进来之前正好失效,那么所有对这个key的数据查询都落到db,我们称为缓存击穿。
与缓存雪崩的区别:
-
击穿是一个热点key失效
-
雪崩是很多key集体失效
解决:锁
1.19 redis中对于生存时间的应用
Redis中可以使用expire命令设置一个键的生存时间,到时间后redis会自动删除;
应用场景:
(1)设置限制的优惠活动的信息;
(2)一些及时需要更新的数据,积分排行榜;
(3)手机验证码的时间;
(4)限制网站访客访问频率;
2. MQ消息队列(忽略!!!)
2.1 为什么使用rabbitmq?
削峰填谷、异步、解耦合
消息类型较多:五种(simple、work、fanout、direct、topic)
对比只允许基于JAVA实现的消息平台的之间进行通信的JMS,AMQP(rabbitmq)允许多种消息协议进行通信)
2.2 如何保证消息的可靠性传输/如何处理消息丢失问题?
考虑维度 | 分析 |
---|---|
生产者 | 原因:网络中断解决1:可以使用rabbitmq提供的事务功能 就是生产者发送数据之前开启rabbitmq事务(channel.txSelect),然后发送消息,如果消息没有成功被rabbitmq接收到,那么生产者会收到异常报错,此时就可以回滚事务(channel.txRollback),然后重试发送消息;如果收到了消息,那么可以提交事务(channel.txCommit)此方法会严重降低系统的吞吐量,性能消耗太大解决2:生产者开发confirm模式之后 你每次写的消息都会分配一个唯一的id,然后如果写入了rabbitmq中,rabbitmq会给你回传一个ack消息,告诉你说这个消息ok了。如果rabbitmq没能处理这个消息,会回调你一个nack接口,告诉你这个消息接收失败,你可以重试。而且你可以结合这个机制自己在内存里维护每个消息id的状态,如果超过一定时间还没接收到这个消息的回调,那么你可以重发。 |
rabbitMQ本身 | 原因:MQ宕机解决:开启rabbitmq的持久化 就是消息写入之后会持久化到磁盘,哪怕是rabbitmq自己挂了, 恢复之后会自动读取之前存储的数据,一般数据不会丢。除非极其罕见的是,rabbitmq还没持久化,自己就挂了,可能导致少量数据会丢失的,但是这个概率较小。设置持久化有两个步骤,第一个是创建queue的时候将其设置为持久化的,这样就可以保证rabbitmq持久化queue的元数据,但是不会持久化queue里的数据;第二个是发送消息的时候将消息的deliveryMode设置为2,就是将消息设置为持久化的,此时rabbitmq就会将消息持久化到磁盘上去。必须要同时设置这两个持久化才行,rabbitmq哪怕是挂了,再次重启,也会从磁盘上重启恢复queue,恢复这个queue里的数据。而且持久化可以跟生产者那边的confirm机制配合起来,只有消息被持久化到磁盘之后,才会通知生产者ack了,所以哪怕是在持久化到磁盘之前,rabbitmq挂了,数据丢了,生产者收不到ack,你也是可以自己重发的。 |
消费者 | 原因:刚消费到,还没处理,结果进程就挂了解决: 用rabbitmq提供的ack机制,简单来说,就是你关闭rabbitmq自动ack,可以通过一个api来调用就行,然后每次你自己代码里确保处理完的时候,再程序里ack一把。这样的话,如果你还没处理完,不就没有ack?那rabbitmq就认为你还没处理完,这个时候rabbitmq会把这个消费分配给别的consumer去处理,消息是不会丢的。 |
2.3 如何保证消息不被重复消费?
这个很常见的一个问题,2.1和2.2两个问题基本可以连起来问。
既然是消费消息,那肯定要考虑考虑会不会重复消费?能不能避免重复消费?或者重复消费了也别造成系统异常可以吗?这个是MQ领域的基本问题,其实本质上还是问你使用消息队列如何保证幂等性,这个是你架构里要考虑的一个问题。
一般情况下,任何一种消息队列中间件都会出现消息的重复消费问题,因为这个问题不是mq能够保证的,需要程序员结合实际业务场景来控制的,所以回答这个问题最好是结合实际业务场景阐述
(1)数据要写库操作,最好先根据主键查一下,如果这数据都有了,就不再执行insert操作,可以update
(2)写入redis,redis的set操作天然幂等性
(3)你需要让生产者发送每条数据的时候,里面加一个全局唯一的id,类似订单id之类的东西,然后你这里消费到了之后,先根据这个id去比如redis里查一下,之前消费过吗?如果没有消费过,你就处理,然后这个id写redis。如果消费过了,那你就别处理了,保证别重复处理相同的消息即可。(防止订单重复提交)
2.4 消息积压问题
如何解决消息队列的延时以及过期失效问题?消息队列满了之后该如何处理?有几百万的消息持续积压几小时,说说如何解决?
方案分析
该问题,其本质针对的场景,都是说,可能你的消费端出了问题,不消费了,或者消费的极其极其慢。另外还有可能你的消息队列集群的磁盘都快写满了,都没人消费,这个时候怎么办?或者是整个这就积压了几个小时,你这个时候怎么办?或者是你积压的时间太长了,导致比如rabbitmq设置了消息过期时间后就没了怎么办?
所以这种问题线上常见的,一般不出,一出就是大问题,一般常见于,举个例子,消费端每次消费之后要写mysql,结果mysql挂了,消费端挂掉了。导致消费速度极其慢。
分析1+话术
这个是我们真实遇到过的一个场景,确实是线上故障了,这个时候要不然就是修复consumer的问题,让他恢复消费速度,然后傻傻的等待几个小时消费完毕。(可行,但是不建议 在面试的时候说)
一个消费者一秒是1000条,一秒3个消费者是3000条,一分钟是18万条,1000多万条
所以如果你积压了几百万到上千万的数据,即使消费者恢复了,也需要大概1小时的时间才能恢复过来
一般这个时候,只能操作临时紧急扩容了,具体操作步骤和思路如下:
1)先修复consumer的问题,确保其恢复消费速度,然后将现有cnosumer都停掉
2)新建一个topic,partition是原来的10倍,临时建立好原先10倍或者20倍的queue数量
3)然后写一个临时的分发数据的consumer程序,这个程序部署上去消费积压的数据,消费之后不做耗时的处理,直接均匀轮询写入临时建立好的10倍数量的queue
4)接着临时征用10倍的机器来部署consumer,每一批consumer消费一个临时queue的数据
5)这种做法相当于是临时将queue资源和consumer资源扩大10倍,以正常的10倍速度来消费数据
6)等快速消费完积压数据之后,得恢复原先部署架构,重新用原先的consumer机器来消费消息
分析2+话术
rabbitmq是可以设置过期时间的,就是TTL,如果消息在queue中积压超过一定的时间就会被rabbitmq给清理掉,这个数据就没了。那这就是第二个坑了。这就不是说数据会大量积压在mq里,而是大量的数据会直接搞丢。
这个情况下,就不是说要增加consumer消费积压的消息,因为实际上没啥积压,而是丢了大量的消息。我们可以采取一个方案,就是批量重导,这个我们之前线上也有类似的场景干过。就是大量积压的时候,我们当时就直接丢弃数据了,然后等过了高峰期以后,比如大家一起喝咖啡熬夜到晚上12点以后,用户都睡觉了。
这个时候我们就开始写程序,将丢失的那批数据,写个临时程序,一点一点的查出来,然后重新灌入mq里面去,把白天丢的数据给他补回来。也只能是这样了。
假设1万个订单积压在mq里面,没有处理,其中1000个订单都丢了,你只能手动写程序把那1000个订单给查出来,手动发到mq里去再补一次
分析3+话术
如果走的方式是消息积压在mq里,那么如果你很长时间都没处理掉,此时导致mq都快写满了,咋办?这个还有别的办法吗?没有,谁让你第一个方案执行的太慢了,你临时写程序,接入数据来消费,消费一个丢弃一个,都不要了,快速消费掉所有的消息。然后走第二个方案,到了晚上再补数据吧。
2.5 消息在什么时候会变成Dead Letter(死信)?
声明队列的时候,指定一个Dead Letter Exchange,来实现Dead Letter的转发。
队列中的消息出现以下情况会变成DL:
1.消息被拒绝并且没有设置重新入队:(NACK || Reject)&&requeue == false
2.消息过期(消息或者队列的TTL设置)
3.消息堆积,并且队列达到最大长度,先入队的消息会变成DL。
rabbitmq消息队列的长度或者大小配置:
可以通过为队列声明参数 x-max-length 提供一个非负整数值来设置最大消息数。
可以通过为队列声明参数 x-max-length-bytes 提供一个非负整数值,设置最大字节长度。
如果设置了两个参数,那么两个参数都将适用;无论先达到哪个限制,都将强制执行。
2.6 RabbitMQ如何实现延时队列?
利用TTL(队列的消息存活时间或消息的存活时间),加上死信交换机。
队列中的消息过期时会被丢到绑定的死信队列。
3. kafka
3.1 消息队列的作用
异步处理
用户注册后,需要发注册邮件和注册短信。传统的串行方式会等所有业务执行完成后再返回结果。并行方式将注册信息写入数据库成功后,发送注册邮件的同时,发送注册短信,可以加快响应速度。
应用解耦
利用消息队列存储消息,需要消费消息的系统只需要订阅消息队列即可,有新的业务加入时无需修改原本的代码。
流量削锋
高并发请求同时对数据库执行写操作,可能会导致数据库卡死导致宕机。将请求消息保存到消息队列,系统可以按照自己的能力来消费消息。
3.2 Kafka的主要组件
-
Broker:Kafka集群中的单个服务器节点,负责数据存储和处理。
-
Topic:消息发布的类别或者主题。
-
Partition:每个Topic被划分为多个不同的分区,分区内的数据有序。
-
Producer:生产者,负责向Kafka的Topic发送消息。
-
Consumer:消费者,负责从Kafka的Topic接收和处理消息。
-
Consumer Group:一组消费者的集合,用于实现消费者的负载均衡和故障转移。
-
ZooKeeper:Kafka使用ZooKeeper来进行集群管理、协调和元数据存储。
3.3 Kafka如何保证消息可靠性(消息不丢失)
-
持久化:Kafka将消息持久化到磁盘上,以保证即使在发生故障时也不会丢失。
-
复制:Kafka允许将消息分布到多个Broker上的副本。当一个Broker故障或不可用时,其他副本仍然可以使用。
-
批量发送:Kafka允许消息在一批中发送,在网络传输中可以降低开销,提高效率。
-
确认机制:Kafka通过Producer的确认机制保证消息的可靠传递。Producer可以选择等待Broker的确认,以确保消息成功写入。
3.4 Kafka为什么要分区,都有哪些分区策略?
分区为了提供负载均衡的能力,实现系统的高伸缩性。分区策略是决定生产者将消息发送到哪个分区的算法,避免造成数据“倾斜”
Kafka提供了默认的分区策略,同时也支持自定义的分区策略
-
默认分区策略:如果指定了 Key,那么默认实现按消息键保序策略;如果没有指定 Key,则使用轮询策略。
-
自定义分区策略(限制配置生产者端的参数 partitioner.class,编写一个具体的类实现org.apache.Kafka.clients.producer.Partitioner接口):
轮询策略(Round-robin)
随机策略(Randomness)
按消息键保序策略(Key-ordering):保证同一个 Key 的所有消息都进入到相同的分区里
3.5 Kafka消费者的幂等性如何保证
幂等简单点讲,就是用户对于同一操作发起的一次请求或者多次请求的结果是一致的,不会产生任何副作用。
在一些特殊情况下(如网络不稳定),生产者可能重复发送消息,消费者也可能会重复消费消息。
解决方案:
- 生产者发送消息时添加唯一标识符
- 消费者获取消息时通过消息唯一标识符判断是否消费过消息(可利用redis缓存)
3.6 Kafka如何保证消息有序性
生产有序性
Kafka 最多只保证单分区内的消息是有序的,所以如果要保证业务全局严格有序,就要设置 Topic 为单分区。通过指定key将消息发送到同一个分区。
kafka默认开启幂等性,服务端会缓存producer发来的最近5个request的元数据,故无论如何,都可以保证最近5个request的数据都是有序的。
消费有序性
不要使用多线程消费,它会破坏有序性
消费者手动同步提交ACK,确保消息一条一条的被消费
3.7 Kafka如何避免消息堆积
-
增加消费者数量及分区数量:两者数量相等。
-
消费者中使用多线程消费。
-
增加批量接收消息的大小, 减少网络IO次数。
-
消费者选择自动提交,但是可能会导致消息丢失。
-
提升物理硬件性能。
3.8 为什么使用Kafka
-
分区机制:消息会被发送到不同分区中,由不同消费者组进行消费。
-
顺序写
-
批量发送
-
批量接收
-
消息压缩:提高消息吞吐量
4. SpringBoot+SpringCloud
4.1 为什么要用 Spring Boot?
Spring Boot的优点
独立运行
Spring Boot而且内嵌了各种servlet容器,Tomcat、Jetty等,现在不再需要打成war包部署到容器中,Spring Boot只要打成一个可执行的jar包就能独立运行,所有的依赖包都在一个jar包内。
简化配置
spring-boot-starter-web启动器自动依赖其他组件,简少了maven的配置。除此之外,还提供了各种启动器,开发者能快速上手。
自动配置
Spring Boot能根据当前类路径下的类、jar包来自动配置bean,如添加一个spring-boot-starter-web启动器就能拥有web的功能,无需其他配置。
无代码生成和XML配置
Spring Boot配置过程中无代码生成,也无需XML配置文件就能完成所有配置工作,这一切都是借助于条件注解完成的,这也是Spring4.x的核心功能之一。
应用监控
Spring Boot提供一系列端点可以监控服务及应用,做健康检测。
4.2 Spring Boot 的核心注解是哪个?它主要由哪几个注解组成的?
启动类上面的注解是@SpringBootApplication,它也是 Spring Boot 的核心注解,主要组合包含了以下 3 个注解:
@SpringBootConfiguration:
组合了 @Configuration 注解,实现配置文件的功能。
@EnableAutoConfiguration:
打开自动配置的功能,也可以关闭某个自动配置的选项
如关闭数据源自动配置功能: @SpringBootApplication(exclude = { DataSourceAutoConfiguration.class })
@ComponentScan:
Spring组件扫描。
4.3 Spring Boot 自动配置原理是什么?
启动类上的注解@SpringBootApplication是一个组合注解,组成它的@SpringBootConfiguration、@EnableAutoConfiguration、@ComponentScan三个注解比较重要
@EnableAutoConfiguration注解
开启自动配置,对jar包下的spring.factories文件进行扫描,这个文件中包含了可以进行自动配置的类,当满足@Condition注解指定的条件时,便在依赖的支持下进行实例化,注册到Spring容器中。
4.4 你如何理解 Spring Boot 中的 Starters?
Starters可以理解为启动器,它包含了一系列可以集成到应用里面的依赖包,你可以一站式集成 Spring 及其他技术,而不需要到处找示例代码和依赖包。如你想使用 Spring JPA 访问数据库,只要加入 spring-boot-starter-data-jpa 启动器,相关依赖就可以引入到项目中了。
4.5 Spring Boot、Spring 和Spring Cloud什么关系
SpringBoot底层就是Spring,简化使用Spring的方式而已,多加了好多的自动配置
Spring Boot 是 Spring 的一套快速配置脚手架,可以基于Spring Boot 快速开发单个微服务
Spring Cloud是一个基于Spring Boot实现的开发工具;
Spring Boot专注于快速、方便集成的单个微服务个体,Spring Cloud关注全局的服务治理框架;
Spring Boot使用了默认大于配置的理念,很多集成方案已经帮你选择好了,能不配置就不配置
Spring Cloud很大的一部分是基于Spring Boot来实现,必须基于Spring Boot开发。
4.6 Ribbon和Feign的区别?
Ribbon
Ribbon 是 Netflix开源的基于HTTP和TCP等协议负载均衡组件
Ribbon 可以用来做客户端负载均衡,调用注册中心的服务
Ribbon的使用需要代码里手动调用目标服务
Feign
Feign是Spring Cloud组件中的一个轻量级RESTful的HTTP服务客户端。
Feign内置了Ribbon,用来做客户端负载均衡,去调用服务注册中心的服务。
Feign的使用方式是:使用Feign的注解定义接口,调用这个接口,就可以调用服务注册中心的服务。
Feign本身不支持Spring MVC的注解,它有一套自己的注解。
OpenFeign
OpenFeign是Spring Cloud 在Feign的基础上支持了Spring MVC的注解,如@RequesMapping等等。
OpenFeign的@FeignClient可以解析SpringMVC的@RequestMapping注解下的接口,并通过动态代理的方式产生实现类,实现类中做负载均衡并调用其他服务。
4.7 LoadBalancer/Ribbon负载均衡能干嘛?
LB(负载均衡LB,即负载均衡(Load Balance),在微服务或分布式集群中经常用的一种应用。负载均衡简单的说就是将用户的请求平摊的分配到多个服务上,从而达到系统的HA。
Ribbon/LoadBalancer属于进程内LB,它只是一个类库,集成于消费方进程,消费方通过它来获取到服务提供方的地址。
4.8 你所知道的微服务技术栈有哪些?请列举一二
服务注册与发现:Eureka、Nacos、Consul、Zookeeper等
服务调用:Rest(Openfeign)、RPC、gRPC
服务熔断器: Hystrix、Sentinel、Envoy等
负载均衡:Ribbon、Openfeign、Nginx等
消息队列:Kafka、RabbitMQ、ActiveMQ等
服务配置中心管理:SpringCloudConfig、Nacos、Apollo等
服务路由(API网关):Gateway、Zuul等
分布式链路追踪:zipkin+Sleuth 、skywalking等
4.7 Nacos
4.7.1 什么是Nacos,主要用来作什么?
Nacos是一个开源的分布式服务注册和配置中心。它提供了服务注册、发现、配置和管理的能力,可以帮助开发者构建和管理微服务架构。
主要功能包括:
-
服务注册与发现:Nacos充当了服务注册中心的角色,服务提供者通过向Nacos注册自己的服务,使得服务消费者能够方便地发现和调用服务。
-
动态配置管理:Nacos提供了统一的配置管理功能,可以集中管理应用程序的配置信息。它支持动态刷新配置,可以在运行时动态修改应用程序的配置,而无需重启应用。
-
服务路由与负载均衡:Nacos可以根据服务的健康状况和负载情况,动态地进行服务路由和负载均衡,以提供更好的服务质量和可用性。
-
服务共享与版本管理:Nacos支持多租户的服务共享,不同的租户可以共享同一个服务。同时,Nacos还提供了版本管理功能,可以管理不同版本的服务。
-
服务监控与治理:Nacos提供了服务的健康检查和监控功能,可以实时监控服务的状态和性能指标。此外,Nacos还提供了服务熔断、限流等治理能力,以提高系统的稳定性和可靠性。
4.7.2 Nacos是AP的还是CP的?
Nacos是一个AP(可用性和分区容忍性)的系统。在分布式系统中,CAP定理指出,一个系统无法同时满足一致性(Consistency)、可用性(Availability)和分区容忍性(Partition tolerance)这三个特性。Nacos选择了AP模型,即在面对网络分区时,为了保证系统的可用性和分区容忍性,它牺牲了一致性。这意味着在网络分区的情况下,Nacos可能会出现数据的不一致性,但它能够保证服务的可用性和高效性。对于服务注册发现、配置管理等功能,Nacos提供了强大的可用性和灵活性,使得开发者能够便捷地构建和管理分布式系统。然而,在某些特定情况下,如果一致性是系统中最重要的特性,那么可能需要考虑选择其他CP模型的系统。
4.8 Spring Cloud Alibaba断路器Sentinel的作用是什么?
Spring Cloud Alibaba 中的断路器组件 Sentinel 主要用于保护和控制微服务的稳定性和可靠性。下面是 Sentinel 的主要作用:
-
流量控制:Sentinel 可以通过流量控制功能限制微服务的请求量,防止系统因过高的请求负载而崩溃或响应变慢。它通过实时监控和统计请求的 QPS(每秒钟的请求数量)、线程数、响应时间等指标,根据预先设置的规则,对请求进行限流,保证服务在承受范围内的正常运行。
-
熔断降级:当微服务出现故障或不可用时,Sentinel 可以自动地将请求快速熔断,避免故障的传播,从而减少对其他服务的影响。通过设置熔断规则,当服务调用失败或超时达到一定阈值时,Sentinel 可以自动触发熔断,进入预设的降级逻辑,比如返回默认值或执行备选逻辑,以保证系统的可用性。
-
服务监控:Sentinel 提供实时的监控和统计功能,可以对微服务的请求流量、各项指标进行实时监控和报警。通过控制台或仪表盘,开发者可以实时查看和分析系统的运行状况,了解请求的分布、成功率、响应时间等关键指标,帮助定位问题,进行性能优化和故障排查。
通过流量控制、熔断降级和服务监控,Sentinel 能够帮助开发者保护和控制微服务的稳定性和可靠性。它可以有效地防止潜在的请求过载、服务雪崩等问题,提高系统的容错能力和稳定性,同时也为开发者提供了实时监控和报警的能力,帮助快速定位和解决问题,保障微服务架构的可用性和性能。
4.9 什么是服务熔断?什么是服务降级?
在复杂的分布式系统中,微服务之间的相互呼叫可能会导致服务堵塞的各种原因。在高并发场景下,服务堵塞意味着线程堵塞,导致当前线程不可用,服务器线程全部堵塞,导致服务器崩溃。由于服务之间的呼叫关系是同步的,它将导致整个微服务系统的服务雪崩。为了解决微服务的调用响应时间过长或不可用,占用越来越多的系统资源,导致雪崩效应,需要进行服务熔断和服务降级。
所谓服务熔断,是指某个服务故障或异常在一起,类似于显示世界“保险丝”当异常情况被触发时,整个服务将被直接熔断,而不是等到服务加班。
服务熔断相当于我们电闸的保险丝,一旦发生服务雪崩,整个服务将被熔断。通过维护自己的线程池,当线程达到阈值时,将启动服务降级。如果其他请求继续访问,则直接返回fallback的默认值。
5. Mysql
5.1 MySQL多表连接有哪些方式?怎么用的?这些连接都有什么区别?
连接方式:左连接、右连接、内连接
使用方法:
左连接:select * from A LEFT JOIN B on A.id=B.id;
右连接:select * from A RIGHT JOIN B on A.id=B.id;
内连接:select * from A inner join B on a.xx=b.xx;(其中inner可以省略)
区别:
Inner join 内连接,在两张表进行连接查询时,只保留两张表中完全匹配的结果集
left join 在两张表进行连接查询时,会返回左表所有的行,即使在右表中没有匹配的记录。
right join 在两张表进行连接查询时,会返回右表所有的行,即使在左表中没有匹配的记录。
5.2 说一下索引的优势和劣势?
优势:
唯一索引可以保证数据库表中每一行数据的唯一性索引可以加快数据查询速度,减少查询时间
劣势:
创建索引和维护索引要耗费时间索引需要占物理空间,除了数据表占用数据空间之外,每一个索引还要占用一定的物理空间给表中的数据进行增、删、改的时候,索引也要动态的维护。
5.3 MySQL聚簇和非聚簇索引的区别
都是B+树的数据结构
**聚簇索引:**将数据存储与索引放到了一块、并且是按照一定的顺序组织的,找到索引也就找到了数 据,数据的物理存放顺序与索引顺序是一致的,即:只要索引是相邻的,那么对应的数据一定也是 相邻地存放在磁盘上的。
**非聚簇索引:**叶子节点不存储数据、存储的是数据行地址,也就是说根据索引查找到数据行的位置 再去磁盘查找数据,这个就有点类似一本树的目录,比如我们要找第三章第一节,那我们先在这个目录里面找,找到对应的页码后再去对应的页码看文章。
-
优势:
1、查询通过聚簇索引可以直接获取数据,相比非聚簇索引需要第二次查询(非覆盖索引的情况下)效率要高
2、聚簇索引对于范围查询的效率很高,因为其数据是按照大小排列的。
3、聚簇索引适合用在排序的场合,非聚簇索引不适合。
-
劣势:
1、维护索引很昂贵,特别是插入新行或者主键被更新导至要分页(page split)的时候。建议在大量插入新行后,选在负载较低的时间段,通过OPTIMIZE TABLE优化表,因为必须被移动的行数据可能造成碎片。使用独享表空间可以弱化碎片。
2、表因为使用UUId(随机ID)作为主键,使数据存储稀疏,这就会出现聚簇索引有可能有比全表扫面更慢,所以建议使用int的auto_increment作为主键
3、如果主键比较大的话,那辅助索引将会变的更大,因为辅助索引的叶子存储的是主键值;过长的主键值,会导致非叶子节点占用占用更多的物理空间。
InnoDB中一定有主键,主键一定是聚簇索引,不手动设置、则会使用unique索引,没有unique索引,则会使用数据库内部的一个行的隐藏id来当作主键索引。在聚簇索引之上创建的索引称之为辅助索引,辅助索引访问数据总是需要二次查找,非聚簇索引都是辅助索引,像复合索引、前缀索引、唯一索引,辅助索引叶子节点存储的不再是行的物理位置,而是主键值.
MyISAM使用的是非聚簇索引,没有聚簇索引,非聚簇索引的两棵B+树看上去没什么不同,节点的结构完全一致只是存储的内容不同而已,主键索引B+树的节点存储了主键,辅助键索引B+树存储了辅助键。表数据存储在独立的地方,这两颗B+树的叶子节点都使用一个地址指向真正的表数据,对于表数据来说,这两个键没有任何差别。由于索引树是独立的,通过辅助键检索无需访问主键的索引树。
如果涉及到大数据量的排序、全表扫描、count之类的操作的话,还是MyISAM占优势些,因为索引所占空间小,这些操作是需要在内存中完成的。
5.4 MySQL中B+树和B树的区别
1、非叶子节点数据不同:
B+树的非叶子节点的数据都在叶子节点中出现过,也就是叶子节点中的数据都在非叶子节点冗余一份。B树中非叶子节点中元素不会冗余。
B+树非叶子节点只存放指针,不存放数据,B树所有节点(叶子节点)都存放数据。
2、叶子节点数据不同:
B+树叶子节点存放数据,B树所有节点(非叶子节)点存放数据。数据遍布整个树结构。
3、时间复杂度不同:
由于B+树的数据都存在叶子节点,因此B+树的时间复杂度固定为o(log n),而B树的数据分布在每个节点中,因此时间复杂度不固定,最好为o(1).
4、叶子节点连接不同:
B+树的叶子节点通过有序的双向链表相连,B树叶子节点不相连。
5、区间查询效率不同:
因为第4点的原因,所以B+树去范围查询效率更快,而B树范围查询比较慢。
因此,存在大量范围查询的场景,适合使用B+树
而对大量单个key查询的场景,可以考虑B树
5.5 Mysql有哪些锁?
1、基于锁的属性分类:
共享锁、排他锁。
共享锁(Share Lock):
共享锁又称读锁,简称S锁;当一个事务为数据加上读锁之后,其他事务只能对该数据加读锁,而不能对数据加写锁,直到所有的读锁释放之后其他事务才能对其进行加持写锁。共享锁的特性主要是为了支持并发的读取数据,读取数据的时候不支持修改,避免出现重复读的问题。
排他锁(Exclusive Lock):
排他锁又称写锁,简称X锁;当一个事务为数据加上写锁时,其他请求将不能再为数据加任何锁,直到该锁释放之后,其他事务才能对数据进行加锁。排他锁的目的是在数据修改时候,不允许其他人同时修改,也不允许其他人读取。避免了出现脏数据和脏读的问题.
2、基于锁的粒度分类:
行级锁(INNODB)、表级锁(INNODB、MYISAM)、页级锁(BDB引擎 )、记录锁、间隙锁、临键锁。
表锁:
表锁是指上锁的时候锁住的是整个表,当下一个事务访问该表的时候,必须等前一个事务释放了锁才能进行对表进行访问; 特点: 粒度大,加锁简单,容易冲突
行锁:
行锁是指上锁的时候锁住的是表的某一行或多行记录,其他事务访问同一张表时,只有被锁住的记录不能访问,其他的记录可正常访问; 特点:粒度小,加锁比表锁麻烦,不容易冲突,相比表锁支持的并发要高。
记录锁(Record Lock)
记录锁也属于行锁中的一种,只不过记录锁的范围只是表中的某一条记录,记录锁是说事务在加锁后锁住的只是表的某一条记录。 精准条件命中,并且命中的条件字段是唯一索引 加了记录锁之后数据可以避免数据在查询的时候被修改的重复读问题,也避免了在修改的事务未提交前被其他事务读取的脏读问题。
页锁:
页级锁是MySQL中锁定粒度介于行级锁和表级锁中间的一种锁。表级锁速度快,但冲突多,行级冲突少,但速度慢。所以取了折衷的页级,一次锁定相邻的一组记录。 特点:开销和加锁时间界于表锁和行锁之间;会出现死锁;锁定粒度界于表锁和行锁之间,并发度一般。
间隙锁(Gap Lock)
属于行锁中的一种,间隙锁是在事务加锁后其锁住的是表记录的某一个区间,当表的相邻ID之间出现空隙则会形成一个区间,遵循左开右闭原则。 间隙锁只会出现在REPEATABLE_READ(可重复读)的事务级别中。 触发条件:防止幻读问题,事务并发的时候,如果没有间隙锁,就会发生如下图的问题,在同一个事务里,A事务的两次查询出的结果会不一样。 比如表里面的数据ID 为 1,4,5,7,10 ,那么会形成以下几个间隙区间,-n-1区间,1-4区间,5-7区间,7-10区间,10-n区间 (-n代表负无穷大,n代表正无穷大)。
临建锁(Next-Key Lock):
也属于行锁的一种,并且它是INNODB的行锁默认算法,总结来说它就是记录锁和间隙锁的组合,临键锁会把查询出来的记录锁住,同时也会把该范围查询内的所有间隙空间也会锁住。 触发条件:范围查询并命中,查询命中了索引。 结合记录锁和间隙锁的特性,临键锁避免了在范围查询时出现脏读、重复读、幻读问题。加了临键锁之后,在范围区间内数据不允许被修改和插入。
5.6 关心过业务系统里面的sql耗时吗?统计过慢查询吗?对慢查询都怎么优化过?
在业务系统中,除了使用主键进行的查询,其他的都会在测试库上测试其耗时,慢查询的统计主要由运维在做,会定期将业务中的慢查询反馈给我们。
慢查询的优化首先要搞明白慢的原因是什么?
1、是查询条件没有命中索引?
2、是load了不需要的数据列?
3、还是数据量太大?
所以优化也是针对这三个方向来的:
1、首先分析语句,看看是否load了额外的数据,可能是查询了多余的行并且抛弃掉了,可能是加载了许多结果中并不需要的列,对语句进行分析以及重写。
2、分析语句的执行计划,然后获得其使用索引的情况,之后修改语句或者修改索引,使得语句可以尽可能的命中索引。
3、如果对语句的优化已经无法进行,可以考虑表中的数据量是否太大,如果是的话可以进行横向或者纵向的分表。
5.7 MySQL事务的基本特性和隔离级别
事务基本特性ACID分别是:
-
**原子性:**指的是一个事务中的操作要么全部成功,要么全部失败。
-
**一致性:**指的是数据库总是从一个一致性的状态转换到另外一个一致性的状态。比如A转账给B100块钱,假设A只有90块,支付之前我们数据库里的数据都是符合约束的,但是如果事务执行成功了,我们的数据库数据就破坏约束了,因此事务不能成功,这里我们说事务提供了一致性的保证。
-
**隔离性:**指的是一个事务的修改在最终提交前,对其他事务是不可见的。
-
**持久性:**指的是一旦事务提交,所做的修改就会永久保存到数据库中。
事务并发问题:
-
**脏读(Drity Read):**某个事务已更新一份数据,另一个事务在此时读取了同一份数据,由于某些原因,前一个RollBack了操作,则后一个事务所读取的数据就会是不正确的。
-
**不可重复读(Non-repeatable read)😗*在一个事务的两次查询之中数据不一致,这可能是两次查询过程中间插入了一个事务更新的原有的数据。
-
**幻读(Phantom Read)😗*在一个事务的两次查询中数据笔数不一致,例如有一个事务查询了几列(Row)数据,而另一个事务却在此时插入了新的几列数据,先前的事务在接下来的查询中,就会发现有几列数据是它先前所没有的。
隔离性有4个隔离级别,分别是:
-
read uncommit 读未提交,可能会读到其他事务未提交的数据,也叫做脏读。 用户本来应该读取到id=1的用户age应该是10,结果读取到了其他事务还没有提交的事务,结果读取结果age=20,这就是脏读。
-
**read commit **读已提交,两次读取结果不一致,叫做不可重复读。 不可重复读解决了脏读的问题,他只会读取已经提交的事务。 用户开启事务读取id=1用户,查询到age=10,再次读取发现结果=20,在同一个事务里同一个查询读取到不同的结果叫做不可重复读。
-
**repeatable read **可重复复读,这是mysql的默认级别,就是每次读取结果都一样,但是有可能产生幻读。
-
**serializable **串行,一般是不会使用的,他会给每一行读取的数据加锁,会导致大量超时和锁竞争 的问题。
5.8 Mysql的MVCC是什么
多版本并发控制:读取数据时通过一种类似快照的方式将数据保存下来,这样读锁就和写锁不冲突了,不同的事务会看到自己特定版本的数据,版本链.
MVCC只在 READ COMMITTED 和 REPEATABLE READ 两个隔离级别下工作。其他两个隔离级别和MVCC不兼容, 因为 READ UNCOMMITTED 总是读取最新的数据行, 而不是符合当前事务版本的数据行。而 SERIALIZABLE 则会对所有读取的行都加锁。
聚簇索引记录中有两个必要的隐藏列:
-
trx_id:用来存储每次对某条聚簇索引记录进行修改的时候的事务id。
-
roll_pointer:每次对哪条聚簇索引记录有修改的时候,都会把老版本写入undo日志中。这个 roll_pointer就是存了一个指针,它指向这条聚簇索引记录的上一个版本的位置,通过它来获得上一个版本的记录信息。(注意插入操作的undo日志没有这个属性,因为它没有老版本)
已提交读和可重复读的区别就在于它们生成ReadView的策略不同。
开始事务时创建ReadView,ReadView维护当前活动的事务id,即未提交的事务id,排序生成一个数组.
访问数据,获取数据中的事务id,对比ReadView:
如果在ReadView的左边(比ReadView都小),可以访问(在左边意味着该事务已经提交)
如果在ReadView的右边(比ReadView都大)或者就在ReadView中,不可以访问,获取roll_pointer,取上一版本重新对比(在右边意味着,该事务在ReadView生成之后出现,在ReadView中意味着该事务还未提交)
已提交读隔离级别下的事务在每次查询的开始都会生成一个独立的ReadView,而可重复读隔离级别则在第一次读的时候生成一个ReadView,之后的读都复用之前的ReadView。
这就是Mysql的MVCC,通过版本链,实现多版本,可并发读-写,写-读。通过ReadView生成策略的不同实现不同的隔离级别
5.9 Mysql常见优化手段
(1)where中用的比较频繁的字段建立索引,尽量选择较小的列
(2)select子句中避免使用‘*’
(3)避免在索引列上使用计算、函数处理、not in 和<>等操作
(4)当只需要一行数据的时候使用limit 1
(5)适时分割表。针对查询较慢的语句,可以使用explain 来分析该语句具体的执行情况。
(6)避免改变索引列的类型。
(7)选择最有效的表名顺序,from字句中写在最后的表是基础表,将被最先处理,在from子句中包含多个表的情况下,你必须选择记录条数最少的表作为基础表。
(8)能用关联查询的不要用子查询
(9)尽量缩小子查询的结果
5.10 sql题
1
学生表 如下:
自动编号 学号 姓名 课程编号 课程名称 分数
1 2005001 张三 0001 数学 69
2 2005002 李四 0001 数学 89
3 2005001 张三 0001 数学 69
删除除了自动编号不同, 其他都相同的学生冗余信息
delete tablename where 自动编号 not in(select min( 自动编号) from tablename group by学号, 姓名, 课程编号, 课程名称, 分数)
2
一个叫 team 的表,里面只有一个字段name, 一共有4 条纪录,分别是a,b,c,d, 对应四个球对,现在四个球对进行比赛,用一条sql 语句显示所有可能的比赛组合.
select a.name, b.name
from team a, team b
where a.name < b.name
3
怎么把这样一个表儿
year month amount
1991 1 1.1
1991 2 1.2
1991 3 1.3
1991 4 1.4
1992 1 2.1
1992 2 2.2
1992 3 2.3
1992 4 2.4
查成这样一个结果
year m1 m2 m3 m4
1991 1.1 1.2 1.3 1.4
1992 2.1 2.2 2.3 2.4
select year,
(select amount from aaa m where month=1 and m.year=aaa.year) as m1,
(select amount from aaa m where month=2 and m.year=aaa.year) as m2,
(select amount from aaa m where month=3 and m.year=aaa.year) as m3,
(select amount from aaa m where month=4 and m.year=aaa.year) as m4
from aaa group by year
6. JVM&JUC
6.1 说一下JVM的主要组成部分?及其作用?
**class loader 类加载器:**加载类文件到内存。Class loader只管加载,只要符合文件结构就加载,至于能否运行,它不负责,那是有Exectution Engine负责的。
**exection engine :**执行引擎也叫解释器,负责解释命令,交由操作系统执行。
**native interface:**本地接口。本地接口的作用是融合不同的语言为java所用。
**Runtimedata area 运行数据区:**运行数据区是jvm的重点,我们所有所写的程序都被加载到这里,之后才开始运行。
**stack:**栈也叫栈内存,是java程序的运行区,是在线程创建时创建,它的生命周期跟随线程的生命周期,线程结束栈内存释放;对于栈来说不存在垃圾回收的问题,只要线程一结束,该栈就结束。栈中的数据以栈帧的格式存在,栈帧是一个内存区块,是一个数据集,是一个有关方法和运行期数据的集合,当一个方法A被调用时就产生了一个栈帧F1,并被压入到栈中,A方法又调用了B方法,于是产生栈帧F2也被压入栈,执行完毕后,先弹出F2栈帧,再弹出F1栈帧,遵循“先进后出”原则。
**堆内存:**一个JVM实例只存在一个堆内存,堆内存的大小是可以调节的。类的加载器读取了类文件之后,需要把类、方法、常变量放到堆内存中,以方便执行器执行,堆内存分三部分:永久存储(用于存放jdk自身携带的class,interface的元数据,也就是说它存储的是运行环境必须的类信息,被装载至此区域的数据是不会被垃圾回收掉的,只有关闭jvm释放此区域所占用的内存)区、新生区、老年代
**method area方法区:**方法去是被所有线程共享,该区域保存的所有字段和字节方法码以及一些特殊方法如构造函数,接口代码也在此定义。
**PC Register 程序计数器:**每个线程都有一个程序计数器,就是一个指针,指向方法区中的方法字节码,由执行引擎读取下一条指令
6.2 说一下类装载的过程
类装载分为以下 5 个步骤:
-
**加载:**根据查找路径找到相应的 class 文件然后导入;
-
**检查:**检查加载的 class 文件的正确性;
-
**准备:**给类中的静态变量分配内存空间;
-
**解析:**虚拟机将常量池中的符号引用替换成直接引用的过程。符号引用就理解为一个标示,而在直接引用直接指向内存中的地址;
-
**初始化:**对静态变量和静态代码块执行初始化工作。
6.3 怎么判断对象是否可以被回收(垃圾判断算法)
一般有两种方法来判断:
-
**引用计数器:**为每个对象创建一个引用计数,有对象引用时计数器 +1,引用被释放时计数 -1,当计数器为 0 时就可以被回收。它有一个缺点不能解决循环引用的问题;
-
**可达性分析:**从 GC Roots 开始向下搜索,搜索所走过的路径称为引用链。当一个对象到 GC Roots 没有任何引用链相连时,则证明此对象是可以被回收的。
6.4 说一下JVM有哪些垃圾回收算法
-
**标记-清除算法:**标记无用对象,然后进行清除回收。
缺点:效率不高,无法清除垃圾碎片。
-
**标记-整理算法:**标记无用对象,让所有存活的对象都向一端移动,然后直接清除掉端边界以外的内存。
-
**复制算法:**按照容量划分二个大小相等的内存区域,当一块用完的时候将活着的对象复制到另一块上,然后再把已使用的内存空间一次清理掉。
缺点:内存使用率不高,只有原来的一半。
-
**分代算法:**根据对象存活周期的不同将内存划分为几块,一般是新生代和老年代,新生代基本采用复制算法,老年代采用标记整理算法。
6.5 说一下JVM有哪些垃圾回收器
-
Serial:最早的单线程串行垃圾回收器。
-
Serial Old:Serial 垃圾回收器的老年版本,同样也是单线程的,可以作为 CMS 垃圾回收器的备选预案。
-
ParNew:是 Serial 的多线程版本。
-
Parallel : ParNew 收集器类似是多线程的,但 Parallel 是吞吐量优先的收集器,可以牺牲等待时间换取系统的吞吐量。
-
Parallel Old : Parallel 的老生代版本,Parallel 使用的是复制的内存回收算法,Parallel Old 使用的是标记-整理的内存回收算法。
-
CMS:一种以获得最短停顿时间为目标的收集器,非常适用 B/S 系统。
-
G1:一种兼顾吞吐量和停顿时间的 GC 实现,是 JDK 9 以后的默认 GC 选项。
-
ZGC
6.6 说一下JVM调优的工具
JDK 自带了很多监控工具,都位于 JDK 的 bin 目录下,其中最常用的是 jconsole 和 jvisualvm 这两款视图监控工具。
**jconsole:**用于对 JVM 中的内存、线程和类等进行监控;
**jvisualvm:**JDK 自带的全能分析工具,可以分析:内存快照、线程快照、程序死锁、监控内存的变化、gc 变化等。
第三方的arthas
6.7 常用的JVM调优的参数都有哪些
-Xms :s为strating,表示堆内存起始大小
-Xmx : x为max,表示最大的堆内存
(一般来说-Xms和-Xmx的设置为相同大小,因为当heap自动扩容时,会发生内存抖动,影响程序的稳定性)
-Xmn : n为new,表示新生代大小
以下了解:
-XX:SurvivorRator=8表示堆内存中新生代、老年代和永久代的比为8:1:1
-XX:PretenureSizeThreshold=3145728表示当创建(new)的对象大于3M的时候直接进入老年代
-XX:MaxTenuringThreshold=15表示当对象的存活的年龄(minor gc一次加1)大于多少时,进入老年代
-XX:-DisableExplicirGC表示是否(+表示是,-表示否)打开GC日志
6.8 什么是线程池,JDK提供的线程池有哪些
线程池就是事先将多个线程对象放到一个容器中,当使用的时候就不用 new 线程而是直接去池中拿线程即可,节省了开辟子线程的时间,提高的代码执行效率。
作用:线程复用、控制最大并发数、管理线程。
第一:降低资源消耗。通过重复利用己创建的线程降低线程创建和销毁造成的消耗。
第二:提高响应速度。当任务到达时,任务可以不需要的等到线程创建就能立即执行。
第三:提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进 行统一的分配,调优和监控
在 JDK 的 java.util.concurrent.Executors 中提供了生成多种线程池的静态方法。
ExecutorService newCachedThreadPool = Executors.newCachedThreadPool();
ExecutorService newFixedThreadPool = Executors.newFixedThreadPool(4);
ScheduledExecutorService newScheduledThreadPool = Executors.newScheduledThreadPool(4);
ExecutorService newSingleThreadExecutor = Executors.newSingleThreadExecutor();
然后调用他们的 execute 方法即可。
这4种线程池底层 全部是ThreadPoolExecutor对象的实现,阿里规范手册中规定线程池采用ThreadPoolExecutor自定义的,实际开发也是。
newCachedThreadPool
创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。这种类型的线程池特点是:
工作线程的创建数量几乎没有限制(其实也有限制的,数目为Interger. MAX_VALUE), 这样可灵活的往线程池中添加线程。
如果长时间没有往线程池中提交任务,即如果工作线程空闲了指定的时间(默认为1分钟),则该工作线程将自动终止。终止后,如果你又提交了新的任务,则线程池重新创建一个工作线程。
在使用CachedThreadPool时,一定要注意控制任务的数量,否则,由于大量线程同时运行,很有会造成系统瘫痪。
newFixedThreadPool
创建一个指定工作线程数量的线程池。每当提交一个任务就创建一个工作线程,如果工作线程数量达到线程池初始的最大数,则将提交的任务存入到池队列中。FixedThreadPool是一个典型且优秀的线程池,它具有线程池提高程序效率和节省创建线程时所耗的开销的优点。但是,在线程池空闲时,即线程池中没有可运行任务时,它不会释放工作线程,还会占用一定的系统资源。
newSingleThreadExecutor
创建一个单线程化的Executor,即只创建唯一的工作者线程来执行任务,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。如果这个线程异常结束,会有另一个取代它,保证顺序执行。单工作线程最大的特点是可保证顺序地执行各个任务,并且在任意给定的时间不会有多个线程是活动的。
newScheduleThreadPool
创建一个定长的线程池,而且支持定时的以及周期性的任务执行。例如延迟3秒执行。
6.9 线程池底层工作原理
第一步:线程池刚创建的时候,里面没有任何线程,等到有任务过来的时候才会创建线程。当然也可以调用 prestartAllCoreThreads() 或者 prestartCoreThread() 方法预创建corePoolSize个线程
第二步:调用execute()提交一个任务时,如果当前的工作线程数<corePoolSize,直接创建新的线程执行这个任务
第三步:如果当时工作线程数量>=corePoolSize,会将任务放入任务队列中缓存
第四步:如果队列已满,并且线程池中工作线程的数量<maximumPoolSize,还是会创建线程执行这个任务
第五步:如果队列已满,并且线程池中的线程已达到maximumPoolSize,这个时候会执行拒绝策略,JAVA线程池默认的策略是AbortPolicy,即抛出RejectedExecutionException异常
6.10 ThreadPoolExecutor对象有哪些参数 怎么设定核心线程数和最大线程数 拒绝策略有哪些
**参数与作用:**共7个参数
-
**corePoolSize:**核心线程数,
在ThreadPoolExecutor中有一个与它相关的配置:allowCoreThreadTimeOut(默认为false),当allowCoreThreadTimeOut为false时,核心线程会一直存活,哪怕是一直空闲着。而当allowCoreThreadTimeOut为true时核心线程空闲时间超过keepAliveTime时会被回收。
-
**maximumPoolSize:**最大线程数
线程池能容纳的最大线程数,当线程池中的线程达到最大时,此时添加任务将会采用拒绝策略,默认的拒绝策略是抛出一个运行时错误(RejectedExecutionException)。值得一提的是,当初始化时用的工作队列为LinkedBlockingDeque时,这个值将无效。
-
**keepAliveTime:**存活时间,
当非核心空闲超过这个时间将被回收,同时空闲核心线程是否回收受allowCoreThreadTimeOut影响。
-
**unit:**keepAliveTime的单位。
-
**workQueue:**任务队列
常用有三种队列,即SynchronousQueue,LinkedBlockingDeque(无界队列),ArrayBlockingQueue(有界队列)。
-
**threadFactory:**线程工厂,
ThreadFactory是一个接口,用来创建worker。通过线程工厂可以对线程的一些属性进行定制。默认直接新建线程。
-
**RejectedExecutionHandler:**拒绝策略
也是一个接口,只有一个方法,当线程池中的资源已经全部使用,添加新线程被拒绝时,会调用RejectedExecutionHandler的rejectedExecution法。默认是抛出一个运行时异常。
-
线程池大小设置:
需要分析线程池执行的任务的特性: CPU 密集型还是 IO 密集型
每个任务执行的平均时长大概是多少,这个任务的执行时长可能还跟任务处理逻辑是否涉及到网络传输以及底层系统资源依赖有关系
如果是 CPU 密集型,主要是执行计算任务,响应时间很快,cpu 一直在运行,这种任务 cpu的利用率很高,那么线程数的配置应该根据 CPU 核心数来决定,CPU 核心数=最大同时执行线程数,加入 CPU 核心数为 4,那么服务器最多能同时执行 4 个线程。过多的线程会导致上下文切换反而使得效率降低。那线程池的最大线程数可以配置为 cpu 核心数+1 如果是 IO 密集型,主要是进行 IO 操作,执行 IO 操作的时间较长,这是 cpu 出于空闲状态,导致 cpu 的利用率不高,这种情况下可以增加线程池的大小。这种情况下可以结合线程的等待时长来做判断,等待时间越高,那么线程数也相对越多。一般可以配置 cpu 核心数的 2 倍。
一个公式:线程池设定最佳线程数目 = ((线程池设定的线程等待时间+线程 CPU 时间)/ 线程 CPU 时间 )* CPU 数目
这个公式的线程 cpu 时间是预估的程序单个线程在 cpu 上运行的时间(通常使用 loadrunner测试大量运行次数求出平均值)
拒绝策略:
AbortPolicy:直接抛出异常,默认策略;
CallerRunsPolicy:用调用者所在的线程来执行任务;
DiscardOldestPolicy:丢弃阻塞队列中靠最前的任务,并执行当前任务;
DiscardPolicy:直接丢弃任务;当然也可以根据应用场景实现 RejectedExecutionHandler 接口,自定义饱和策略,如记录日志或持久化存储不能处理的任务
6.11 常见线程安全的并发容器有哪些
-
ConcurrentHashMap:它是基于哈希表的线程安全的并发容器,用于多线程并发访问场景。它通过分段锁(Segment)和读写分离的策略来提高并发性能。ConcurrentHashMap允许多个线程同时读取,而写操作会锁住对应的段,只有一个线程能进行写操作,减少了锁的竞争。
-
ConcurrentSkipListMap:它是基于跳表的线程安全的并发容器,用于多线程并发访问场景。跳表是一种有序数据结构,通过多级索引来加速查找和插入操作。ConcurrentSkipListMap通过使用跳表数据结构实现有序映射,并通过锁分段机制来提高并发度。
-
ConcurrentLinkedQueue:它是无界、线程安全的并发队列,用于多线程并发访问场景。它使用无锁算法CAS(Compare and Swap)来实现高效的并发操作。ConcurrentLinkedQueue提供了高吞吐量的队列操作,支持并发的入队和出队操作。
-
CopyOnWriteArrayList:它是基于数组的线程安全的并发容器,用于多线程并发访问场景。它通过写时复制的策略,在修改操作时对原有数据进行复制,从而实现读写分离,保证了读操作的并发安全性。CopyOnWriteArrayList适用于多读少写的场景。
-
BlockingQueue:它是一个接口,表示阻塞队列,用于在多线程环境下进行线程安全的生产者-消费者模式的操作。常见的实现类有ArrayBlockingQueue、LinkedBlockingQueue、PriorityBlockingQueue等。阻塞队列提供了线程安全的入队和出队操作,并且在队列满或空时,能够自动阻塞等待或唤醒。
6.12 synchronized底层实现是什么 lock底层是什么 有什么区别
Synchronized原理:
方法级的同步是隐式,即无需通过字节码指令来控制的,它实现在方法调用和返回操作之中。JVM可以从方法常量池中的方法表结构(method_info Structure) 中的 ACC_SYNCHRONIZED 访问标志区分一个方法是否同步方法。当方法调用时,调用指令将会 检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先持有monitor(虚拟机规范中用的是管程一词),然后再执行方法,最后再方法完成(无论是正常完成还是非正常完成)时释放monitor。
代码块的同步是利用monitorenter和monitorexit这两个字节码指令。它们分别位于同步代码块的开始和结束位置。当jvm执行到monitorenter指令时,当前线程试图获取monitor对象的所有权,如果未加锁或者已经被当前线程所持有,就把锁的计数器+1;当执行monitorexit指令时,锁计数器-1;当锁计数器为0时,该锁就被释放了。如果获取monitor对象失败,该线程则会进入阻塞状态,直到其他线程释放锁。
参考:https://blog.csdn.net/ben040661/article/details/125697819
Lock原理:
Lock的存储结构:一个int类型状态值(用于锁的状态变更),一个双向链表(用于存储等待中的线程)
Lock获取锁的过程:本质上是通过CAS来获取状态值修改,如果当场没获取到,会将该线程放在线程等待链表中。
Lock释放锁的过程:修改状态值,调整等待链表。
Lock大量使用CAS+自旋。因此根据CAS特性,lock建议使用在低锁冲突的情况下。
Lock与synchronized的区别:
Lock的加锁和解锁都是由java代码配合native方法(调用操作系统的相关方法)实现的,而synchronize的加锁和解锁的过程是由JVM管理的
当一个线程使用synchronize获取锁时,若锁被其他线程占用着,那么当前只能被阻塞,直到成功获取锁。而Lock则提供超时锁和可中断等更加灵活的方式,在未能获取锁的 条件下提供一种退出的机制。
一个锁内部可以有多个Condition实例,即有多路条件队列,而synchronize只有一路条件队列;同样Condition也提供灵活的阻塞方式,在未获得通知之前可以通过中断线程以 及设置等待时限等方式退出条件队列。
synchronize对线程的同步仅提供独占模式,而Lock即可以提供独占模式,也可以提供共享模式
synchronized | Lock |
---|---|
关键字 | 类 |
自动加锁和释放锁 | 需要手动调用unlock方法释放锁 |
jvm层面的锁 | API层面的锁 |
非公平锁 | 可以选择公平或者非公平锁 |
锁是一个对象,并且锁的信息保存在了对象中 | 代码中通过int类型的state标识 |
有一个锁升级的过程 | 无 |
6.13 ConcurrentHashMap为什么性能比HashTable高,底层原理是什么?
ConcurrentHashMap是线程安全的Map容器,JDK8之前,ConcurrentHashMap使用锁分段技术,将数据分成一段段存储,每个数据段配置一把锁,即segment类,这个类继承ReentrantLock来保证线程安全,JKD8的版本取消Segment这个分段锁数据结构,底层也是使用Node数组+链表+红黑树,从而实现对每一段数据就行加锁,也减少了并发冲突的概率。
hashtable类基本上所有的方法都是采用synchronized进行线程安全控制,高并发情况下效率就降低 ,ConcurrentHashMap是采用了分段锁的思想提高性能,锁粒度更细化
Java8 中的 ConcurrentHashMap 使用的 Synchronized 锁加 CAS 的机制。结构Node 数组 + 链表 / 红黑树,Node 是类似于一个 HashEntry 的结构。它的冲突再达到一定大小时会转化成红黑树,在冲突小于一定数量时又退回链表。
public V put(K key, V value) {
return putVal(key, value, false);
}
/** Implementation for put and putIfAbsent */
final V putVal(K key, V value, boolean onlyIfAbsent) {
// key 和 value 不能为空
if (key == null || value == null) throw new NullPointerException();
int hash = spread(key.hashCode());
int binCount = 0;
for (Node<K,V>[] tab = table;;) {
// f = 目标位置元素
Node<K,V> f; int n, i, fh;// fh 后面存放目标位置的元素 hash 值
if (tab == null || (n = tab.length) == 0)
// 数组桶为空,初始化数组桶(自旋+CAS)
tab = initTable();
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
// 桶内为空,CAS 放入,不加锁,成功了就直接 break 跳出
if (casTabAt(tab, i, null,new Node<K,V>(hash, key, value, null)))
break; // no lock when adding to empty bin
}
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
else {
V oldVal = null;
// 使用 synchronized 加锁加入节点
synchronized (f) {
if (tabAt(tab, i) == f) {
// 说明是链表
if (fh >= 0) {
binCount = 1;
// 循环加入新的或者覆盖节点
for (Node<K,V> e = f;; ++binCount) {
K ek;
if (e.hash == hash &&
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) {
oldVal = e.val;
if (!onlyIfAbsent)
e.val = value;
break;
}
Node<K,V> pred = e;
if ((e = e.next) == null) {
pred.next = new Node<K,V>(hash, key,
value, null);
break;
}
}
}
else if (f instanceof TreeBin) {
// 红黑树
Node<K,V> p;
binCount = 2;
if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
value)) != null) {
oldVal = p.val;
if (!onlyIfAbsent)
p.val = value;
}
}
}
}
if (binCount != 0) {
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
}
addCount(1L, binCount);
return null;
}
6.14 了解volatile关键字不,synchronized和volatile有什么区别
volatile是Java提供的最轻量级的同步机制,保证了共享变量的可见性,被volatile关键字修饰的变量,如果值发生了变化,其他线程立刻可见,避免出现脏读现象。volatile禁止了指令重排,可以保证程序执行的有序性,但是由于禁止了指令重排,所以JVM相关的优化没了,效率会偏弱
volatile本质是告诉JVM当前变量在寄存器中的值是不确定的,需要从主存中读取,synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。
volatile仅能用在变量级别,而synchronized可以使用在变量、方法、类级别。
volatile仅能实现变量的修改可见性,不能保证原子性;而synchronized则可以保证变量的修改可见性和原子性。
volatile不会造成线程阻塞,synchronized可能会造成线程上阻塞。
volatile标记的变量不会被编译器优化,synchronized标记的变量可以被编译器优化。
6.15 线程通信
需求:两个线程,一个线程打印1-52,另一个打印字母A-Z打印顺序为12A34B…5152Z,要求用线程间通信
7. 其它
7.1 Elasticsearch中的倒排索引是什么?
倒排索引是搜索引擎的核心。搜索引擎的主要目标是在查找发生搜索条件的文档时提供快速搜索。
倒排索引是区别于正排索引的概念:
- 正排索引:是以文档对象的唯一 ID 作为索引,以文档内容作为记录。
- 倒排索引:指的是将文档内容中的词映射为索引,将包含该词的文档 ID 作为记录。
7.2 git如何解决代码冲突
协作开发时修改了文件的同一区域时,后推送代码的开发人员会出现冲突问题,因为Git 不会执行自动合并,它会提示冲突让开发者解决。
git push xx :推送代码时会出现冲突
git status: 可以查询冲突信息
git pull xx :拉取远程仓库最新代码
手动编辑冲突文件解决冲突
git add . :添加合并后的文件到暂存区
git commit -m 'xxx' :提交到本地库
git push xx :推送到远程仓库
7.3 Linux
7.3.1 Linux常用命令
序号 | 命令 | 命令解释 |
---|---|---|
1 | top | 查看内存 |
2 | df -h | 查看磁盘存储情况 |
3 | tail -f -n | 跟随查看文本 |
4 | find | 查找文件或目录 |
5 | netstat -tunlp | grep 端口号 | 查看端口占用情况 |
6 | uptime | 查看报告系统运行时长及平均负载 |
7 | ps -aux | 查看进程 |
7.3.2 如何查看测试项目的日志
一般测试的项目里面,有个logs的目录文件,会存放日志文件,有个xxx.out的文件,可以用tail -f 动态实时查看后端日志
先cd 到logs目录(里面有xx.out文件)
>tail -f xx.out
这时屏幕上会动态实时显示当前的日志,ctr+c停止
7.3.3 LINUX中如何查看某个端口是否被占用
netstat -anp | grep 端口号
如果需要解决端口占用,可以换端口号或者关闭/杀死占用端口的进程
systemctl stop xxx
kill pid
7.3.4 vim(vi)编辑器
有命令模式、输入模式、末行模式三种模式。
-
命令模式:
查找内容(/abc)、跳转到指定行(20gg)、跳转到尾行(G)、跳转到首行(gg)、删除行(dd)、插入行(o)、复制粘贴(yy,p)
-
输入模式:可以编辑文件内容
-
末行模式:保存退出(wq)、强制退出(q!)、显示文件行号(set number)
在命令模式下,输入a或i即可切换到输入模式,输入冒号(:)即可切换到末行模式;
在输入模式和末行模式下,按esc键切换到命令模式
7.4 写出Docker关于镜像容器的命令操作至少5个
- 镜像相关操作:
-
拉取镜像:
docker pull image_name:tag
-
查看已有镜像:
docker images
-
构建镜像:
docker build -t image_name:tag .
-
推送镜像到仓库:
docker push image_name:tag
-
删除镜像:
docker rmi image_name:tag
- 容器相关操作:
-
创建并启动容器:
docker run -d --name container_name image_name:tag
-
查看运行中的容器:
docker ps
-
查看所有容器(包括停止状态):
docker ps -a
-
启动容器:
docker start container_name
-
停止容器:
docker stop container_name
-
重启容器:
docker restart container_name
-
进入容器:
docker exec -it container_name bash
-
查看容器日志:
docker logs container_name
-
删除容器:
docker rm container_name
-
进入容器内部的交互式终端 (仅限于基于Linux的镜像):
docker exec -it container_name /bin/bash
-
将本地文件或目录挂载到容器中:
docker run -v /host/path:/container/path image_name:tag
7.5 什么是IaaS、PaaS、SaaS?
IaaS、PaaS 和 SaaS 是云计算中常见的服务模型,用于描述不同层次的云服务提供方式:
· IaaS(基础设施即服务,Infrastructure as a Service): 在这种模型下,提供的是基础的计算资源,如虚拟机、存储、网络等。用户可以在这些基础设施上构建、管理和运行自己的应用程序,拥有更高的灵活性和控制权。但用户需要自己管理操作系统、中间件、应用等层面的内容。
· PaaS(平台即服务,Platform as a Service): PaaS 提供了比 IaaS 更高层次的抽象,除了基础设施,还提供了开发、部署和管理应用程序所需的平台和工具。用户可以将注意力集中在应用程序的开发和部署上,而不必过多关注底层的基础设施管理。PaaS 通常包括运行时环境、开发工具、数据库管理等。
· SaaS(软件即服务,Software as a Service): 在这种模型下,提供的是完整的应用程序作为服务。用户无需关心底层的基础设施、平台,只需通过网络浏览器或其他客户端访问应用程序。常见的 SaaS 包括电子邮件服务、在线办公套件、客户关系管理系统等。
这些服务模型从底层基础设施到应用程序层面提供了不同层次的抽象和服务,使用户能够根据需求选择合适的模型来构建、部署和使用应用程序
7.6 请画出云上高并发架构图
7.7 Nginx中的负载均衡算法有哪些 并做出解释
名称 | 说明 | *特点* |
---|---|---|
round robin | 轮询方式 | 默认的负载均衡算法,按照请求的顺序依次分配给后端服务器。 |
random | 随机 | 随机选择一个后端服务器来处理请求 |
url_hash | 依据url分配方式 | 根据客户端请求url的hash值,来分发请求, 同一个url请求, 会发转发到同一个服务器上 |
ip_hash | 依据ip分配方式 | 根据客户端请求的IP地址计算hash值, 根据hash值来分发请求, 同一个IP发起的请求, 会发转发到同一个服务器上 |
weight | 权重方式 | 根据权重分发请求,权重大的分配到请求的概率大 |
least_conn | 依据最少连接方式 | 哪个服务器当前处理的连接少, 请求优先转发到这台服务器 |
7.8 如何在 Nginx 中实现 IP 黑名单?
在 Nginx 中实现 IP 黑名单可以通过配置 allow 和 deny 指令来完成。在需要限制的 location 或 server 块中,使用 deny 指令来指定不允许访问的 IP 地址,然后使用 allow 指令来指定允许访问的 IP 地址。
7.9 解释 Nginx 的 Master-Worker 架构。
Nginx 采用了 Master-Worker 的架构模式。Master 进程负责读取和验证配置文件、管理 worker 进程;而 Worker 进程则负责处理实际的客户端请求。这种架构模式利用了多核 CPU 的优势,提高了并发处理能力和稳定性。
7.10 冒泡排序(Bubble Sort)
算法描述:
比较相邻的元素。如果第一个比第二个大,就交换它们两个;
对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对,这样在最后的元素应该会是最大的数;
针对所有的元素重复以上的步骤,除了最后一个;
重复步骤1~3,直到排序完成。
如果两个元素相等,不会再交换位置,所以冒泡排序是一种稳定排序算法。
代码实现:
publc class BubbleSort {
/**
* @param data 被排序的数组
*/
public static void bubbleSort(int[] data) {
int arrayLength = data.length;
for (int i = 1; i < arrayLength; i++) {//第i次排序
for (int j = 0; j < arrayLength - i; j++) {//从索引为j的数开始
if (data[j] > data[j + 1]) { //相邻元素两两对比
int temp = data[j + 1]; // 元素交换
data[j + 1] = data[j];
data[j] = temp;
}
}
System.out.println("第" + i + "次排序:\n" + java.util.Arrays.toString(data));
}
}
}
7.11 快速排序(Quick Sort)
算法描述:
使用分治法来把一个串(list)分为两个子串(sub-lists)。具体算法描述如下:
从数列中挑出一个元素,称为 “基准”(pivot);
重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作;
递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序。
key值的选取可以有多种形式,例如中间数或者随机数,分别会对算法的复杂度产生不同的影响。
代码实现:
public class QuickSort {
public static void quickSort(int[] data, int low, int high) {
int i, j, temp, t;
if (low > high) {
return;
}
i = low;
j = high;
//temp就是基准位
temp = data[low];
System.out.println("基准位:" + temp);
while (i < j) {
//先看右边,依次往左递减
while (temp <= data[j] && i < j) {
j--;
}
//再看左边,依次往右递增
while (temp >= data[i] && i < j) {
i++;
}
//如果满足条件则交换
if (i < j) {
System.out.println("交换:" + data[i] + "和" + data[j]);
t = data[j];
data[j] = data[i];
data[i] = t;
System.out.println(java.util.Arrays.toString(data));
}
}
//最后将基准位与i和j相等位置的数字交换
System.out.println("基准位" + temp + "和i、j相遇的位置" + data[i] + "交换");
data[low] = data[i];
data[i] = temp;
System.out.println(java.util.Arrays.toString(data));
//递归调用左半数组
quickSort(data, low, j - 1);
//递归调用右半数组
quickSort(data, j + 1, high);
}
}
7.12 二分查找(Binary Search)
算法描述:
二分查找也称折半查找,它是一种效率较高的查找方法,要求列表中的元素首先要进行有序排列。
首先,假设表中元素是按升序排列,将表中间位置记录的关键字与查找关键字比较,如果两者相等,则查找成功;
否则利用中间位置记录将表分成前、后两个子表,如果中间位置记录的关键字大于查找关键字,则进一步查找前一子表,否则进一步查找后一子表。
重复以上过程,直到找到满足条件的记录,使查找成功,或直到子表不存在为止,此时查找不成功。
代码实现:
public class BinarySearch {
/**
* 二分查找 时间复杂度O(log2n);空间复杂度O(1)
*
* @param arr 被查找的数组
* @param left
* @param right
* @param findVal
* @return 返回元素的索引
*/
public static int binarySearch(int[] arr, int left, int right, int findVal) {
if (left > right) {//递归退出条件,找不到,返回-1
return -1;
}
int midIndex = (left + right) / 2;
if (findVal < arr[midIndex]) {//向左递归查找
return binarySearch(arr, left, midIndex, findVal);
} else if (findVal > arr[midIndex]) {//向右递归查找
return binarySearch(arr, midIndex, right, findVal);
} else {
return midIndex;
}
}
}
7.13 二叉搜索树的遍历
四种遍历
先序遍历:先访问根节点,再访问左子树,最后访问右子树。
后序遍历:先左子树,再右子树,最后根节点。
中序遍历:先左子树,再根节点,最后右子树。
层序遍历:每一层从左到右访问每一个节点。
每一个子树遍历时依然按照此时的遍历顺序。可以采用递归实现遍历。
7.14 如何理解时间复杂度和空间复杂度
时间复杂度和空间复杂度一般是针对算法而言,是衡量一个算法是否高效的重要标准。先纠正一个误区,时间复杂度并不是算法执行的时间,再纠正一个误区,算法不单单指冒泡排序之类的,一个循环甚至是一个判断都可以称之为算法。其实理解起来并不冲突,八大排序甚至更多的算法本质上也是通过各种循环判断来实现的。
**时间复杂度:**指算法语句的执行次数。O(1),O(n),O(logn),O(n2)
**空间复杂度:**就是一个算法在运行过程中临时占用的存储空间大小,换句话说就是被创建次数最多的变量,它被创建了多少次,那么这个算法的空间复杂度就是多少。有个规律,如果算法语句中就有创建对象,那么这个算法的时间复杂度和空间复杂度一般一致,很好理解,算法语句被执行了多少次就创建了多少对象。