java后端面试大全
2023-03-23 17:12:42 3 举报
AI智能生成
金三银四 只为冲刺
作者其他创作
大纲/内容
nginx
什么是Nginx?
它是一轻量级/高性能的反向代理服务器,能做反向代理,负载均衡,还支持2-3万并发连接数,官方测试5万并发量
为什么要用Nginx?
跨平台、配置简单、反向代理、高并发连接,处理2-3万并发连接数,官方测试5万并发量
Nginx它有个健康检查功能:如果有一个服务器宕机,它会做一个健康检查,再发送请求就
不会发送到宕机的服务器上,会发送到其他的节点上
节省带宽:支持GZIP压缩,可以添加浏览器本地缓存
稳定性高:宕机的概率非常小
接收用户请求是异步的
Nginx它有个健康检查功能:如果有一个服务器宕机,它会做一个健康检查,再发送请求就
不会发送到宕机的服务器上,会发送到其他的节点上
节省带宽:支持GZIP压缩,可以添加浏览器本地缓存
稳定性高:宕机的概率非常小
接收用户请求是异步的
为什么Nginx性能这么高?
他有事件处理机制:异步非阻塞事件处理机制:运用了epoll模型,提供了一个队列,排队解决
Nginx怎么处理请求的?
nginx接收一个请求后,首先有listen和server_name指令匹配server模块,再匹配server模块里的location,location就是实际地址
什么是正向代理和反向代理?
正向代理就是一个人发送一个请求直接就到达了目标的服务器
反方代理就是请求统一被Nginx接收,nginx反向代理服务器接收到之后,按照一定的规 则分发给了后端的业务处理服务器进行处理了
反方代理就是请求统一被Nginx接收,nginx反向代理服务器接收到之后,按照一定的规 则分发给了后端的业务处理服务器进行处理了
使用“反向代理服务器的优点是什么?
反向代理服务器可以隐藏源服务器的存在和特征。它充当互联网云和web服务器之间的中间层。
这对于安全方面来说是很好的,特别是当您使用web托管服务时。
这对于安全方面来说是很好的,特别是当您使用web托管服务时。
Nginx的优缺点?
优点:
占内存小,可实现高并发连接,处理响应快
可实现http服务器、虚拟主机、方向代理、负载均衡
Nginx配置简单
可以不暴露正式的服务器IP地址
缺点:
动态处理差:nginx处理静态文件好,耗费内存少,但是处理动态页面则很鸡肋,现在一般前端用nginx作为反向代理抗住压力
占内存小,可实现高并发连接,处理响应快
可实现http服务器、虚拟主机、方向代理、负载均衡
Nginx配置简单
可以不暴露正式的服务器IP地址
缺点:
动态处理差:nginx处理静态文件好,耗费内存少,但是处理动态页面则很鸡肋,现在一般前端用nginx作为反向代理抗住压力
Nginx应用场景?
http服务器。Nginx是一个http服务可以独立提供http服务。可以做网页静态服务器。
虚拟主机。可以实现在一台服务器虚拟出多个网站,例如个人网站使用的虚拟机。
反向代理,负载均衡。当网站的访问量达到一定程度后,单台服务器不能满足用户的请求时,需要用多台服务器集群可以使用nginx做反向代理。并且多台服务器可以平均分担负载,不会应为某台服务器负载高宕机而某台服务器闲置的情况。
nginx 中也可以配置安全管理、比如可以使用Nginx搭建API接口网关,对每个接口服务进行拦截。
虚拟主机。可以实现在一台服务器虚拟出多个网站,例如个人网站使用的虚拟机。
反向代理,负载均衡。当网站的访问量达到一定程度后,单台服务器不能满足用户的请求时,需要用多台服务器集群可以使用nginx做反向代理。并且多台服务器可以平均分担负载,不会应为某台服务器负载高宕机而某台服务器闲置的情况。
nginx 中也可以配置安全管理、比如可以使用Nginx搭建API接口网关,对每个接口服务进行拦截。
Nginx目录结构有哪些?
[root@localhost ~]# tree /usr/local/nginx
/usr/local/nginx
├── client_body_temp
├── conf # Nginx所有配置文件的目录
│ ├── fastcgi.conf # fastcgi相关参数的配置文件
│ ├── fastcgi.conf.default # fastcgi.conf的原始备份文件
│ ├── fastcgi_params # fastcgi的参数文件
│ ├── fastcgi_params.default
│ ├── koi-utf
│ ├── koi-win
│ ├── mime.types # 媒体类型
│ ├── mime.types.default
│ ├── nginx.conf # Nginx主配置文件
│ ├── nginx.conf.default
│ ├── scgi_params # scgi相关参数文件
│ ├── scgi_params.default
│ ├── uwsgi_params # uwsgi相关参数文件
│ ├── uwsgi_params.default
│ └── win-utf
├── fastcgi_temp # fastcgi临时数据目录
├── html # Nginx默认站点目录
│ ├── 50x.html # 错误页面优雅替代显示文件,例如当出现502错误时会调用此页面
│ └── index.html # 默认的首页文件
├── logs # Nginx日志目录
│ ├── access.log # 访问日志文件
│ ├── error.log # 错误日志文件
│ └── nginx.pid # pid文件,Nginx进程启动后,会把所有进程的ID号写到此文件
├── proxy_temp # 临时目录
├── sbin # Nginx命令目录
│ └── nginx # Nginx的启动命令
├── scgi_temp # 临时目录
└── uwsgi_temp # 临时目录
/usr/local/nginx
├── client_body_temp
├── conf # Nginx所有配置文件的目录
│ ├── fastcgi.conf # fastcgi相关参数的配置文件
│ ├── fastcgi.conf.default # fastcgi.conf的原始备份文件
│ ├── fastcgi_params # fastcgi的参数文件
│ ├── fastcgi_params.default
│ ├── koi-utf
│ ├── koi-win
│ ├── mime.types # 媒体类型
│ ├── mime.types.default
│ ├── nginx.conf # Nginx主配置文件
│ ├── nginx.conf.default
│ ├── scgi_params # scgi相关参数文件
│ ├── scgi_params.default
│ ├── uwsgi_params # uwsgi相关参数文件
│ ├── uwsgi_params.default
│ └── win-utf
├── fastcgi_temp # fastcgi临时数据目录
├── html # Nginx默认站点目录
│ ├── 50x.html # 错误页面优雅替代显示文件,例如当出现502错误时会调用此页面
│ └── index.html # 默认的首页文件
├── logs # Nginx日志目录
│ ├── access.log # 访问日志文件
│ ├── error.log # 错误日志文件
│ └── nginx.pid # pid文件,Nginx进程启动后,会把所有进程的ID号写到此文件
├── proxy_temp # 临时目录
├── sbin # Nginx命令目录
│ └── nginx # Nginx的启动命令
├── scgi_temp # 临时目录
└── uwsgi_temp # 临时目录
Nginx配置文件nginx.conf有哪些属性模块?
worker_processes 1; # worker进程的数量
events { # 事件区块开始
worker_connections 1024; # 每个worker进程支持的最大连接数
} # 事件区块结束
http { # HTTP区块开始
include mime.types; # Nginx支持的媒体类型库文件
default_type application/octet-stream; # 默认的媒体类型
sendfile on; # 开启高效传输模式
keepalive_timeout 65; # 连接超时
server { # 第一个Server区块开始,表示一个独立的虚拟主机站点
listen 80; # 提供服务的端口,默认80
server_name localhost; # 提供服务的域名主机名
location / { # 第一个location区块开始
root html; # 站点的根目录,相当于Nginx的安装目录
index index.html index.htm; # 默认的首页文件,多个用空格分开
} # 第一个location区块结果
error_page 500502503504 /50x.html; # 出现对应的http状态码时,使用50x.html回应客户
location = /50x.html { # location区块开始,访问50x.html
root html; # 指定对应的站点目录为html
}
}
events { # 事件区块开始
worker_connections 1024; # 每个worker进程支持的最大连接数
} # 事件区块结束
http { # HTTP区块开始
include mime.types; # Nginx支持的媒体类型库文件
default_type application/octet-stream; # 默认的媒体类型
sendfile on; # 开启高效传输模式
keepalive_timeout 65; # 连接超时
server { # 第一个Server区块开始,表示一个独立的虚拟主机站点
listen 80; # 提供服务的端口,默认80
server_name localhost; # 提供服务的域名主机名
location / { # 第一个location区块开始
root html; # 站点的根目录,相当于Nginx的安装目录
index index.html index.htm; # 默认的首页文件,多个用空格分开
} # 第一个location区块结果
error_page 500502503504 /50x.html; # 出现对应的http状态码时,使用50x.html回应客户
location = /50x.html { # location区块开始,访问50x.html
root html; # 指定对应的站点目录为html
}
}
Nginx静态资源?
静态资源访问,就是存放在nginx的html页面
如何使用Nginx解决前端跨域问题?
使用Nginx转发请求。把跨域的接口写成调用域的接口,然后将这些接口转发到真正的请求地址。
Nginx虚拟主机怎么配置?
1、基于域名的虚拟主机,通过域名来区分虚拟主机——应用:外部网站
2、基于端口的虚拟主机,通过端口来区分虚拟主机——应用:公司内部网站,外部网站的管理后台
3、基于ip的虚拟主机。
2、基于端口的虚拟主机,通过端口来区分虚拟主机——应用:公司内部网站,外部网站的管理后台
3、基于ip的虚拟主机。
location的作用是什么?
location指令的作用是根据用户请求的URI来执行不同的应用,也就是根据用户请求的网站URL进行匹配,匹配成功即进行相关的操作。
限流怎么做的?
Nginx限流就是限制用户请求速度,防止服务器受不了
限流有3种
正常限制访问频率(正常流量)
突发限制访问频率(突发流量)
限制并发连接数
Nginx的限流都是基于漏桶流算法,底下会说道什么是桶铜流
限流有3种
正常限制访问频率(正常流量)
突发限制访问频率(突发流量)
限制并发连接数
Nginx的限流都是基于漏桶流算法,底下会说道什么是桶铜流
为什么要做动静分离?
Nginx是当下最热的Web容器,网站优化的重要点在于静态化网站,网站静态化的关键点则是是动静分离,动静分离是让动态网站里的动态网页根据一定规则把不变的资源和经常变的资源区分开来,动静资源做好了拆分以后,我们则根据静态资源的特点将其做缓存操作。
让静态的资源只走静态资源服务器,动态的走动态的服务器
Nginx的静态处理能力很强,但是动态处理能力不足,因此,在企业中常用动静分离技术。
对于静态资源比如图片,js,css等文件,我们则在反向代理服务器nginx中进行缓存。这样浏览器在请求一个静态资源时,代理服务器nginx就可以直接处理,无需将请求转发给后端服务器tomcat。
若用户请求的动态文件,比如servlet,jsp则转发给Tomcat服务器处理,从而实现动静分离。这也是反向代理服务器的一个重要的作用。
让静态的资源只走静态资源服务器,动态的走动态的服务器
Nginx的静态处理能力很强,但是动态处理能力不足,因此,在企业中常用动静分离技术。
对于静态资源比如图片,js,css等文件,我们则在反向代理服务器nginx中进行缓存。这样浏览器在请求一个静态资源时,代理服务器nginx就可以直接处理,无需将请求转发给后端服务器tomcat。
若用户请求的动态文件,比如servlet,jsp则转发给Tomcat服务器处理,从而实现动静分离。这也是反向代理服务器的一个重要的作用。
Nginx怎么做的动静分离?
先设置location路径,然后把静态资源放在该路径下,然后重启nginx,重启好了后浏览器就可以直接访问该静态资源了
location的语法能说出来吗?
~代表自己输入的英文字母
Nginx负载均衡的算法怎么实现的?策略有哪些?
为了避免服务器崩溃,大家会通过负载均衡的方式来分担服务器压力。将对台服务器组成一个集群,
当用户访问时,先访问到一个转发服务器,再由转发服务器将访问分发到压力更小的服务器。
Nginx负载均衡实现的策略有以下五种:
1、轮询(默认):每个请求按时间顺序逐一分配到不同的后端服务器,如果后端某个服务器宕机,能自动剔除故障系统。
2、权重weight:weight的值越大分配到的访问概率越高,主要用于后端每台服务器性能不均衡的情况下。其次是为在主从的情况下设置不同的权值,达到合理有效的地利用主机资源。
3、ip_hash( IP绑定):每个请求按访问IP的哈希结果分配,使来自同一个IP的访客固定访问一台后端服务器,并且可以有效解决动态网页存在的session共享问题
4、fair(第三方插件):对比 weight、ip_hash更加智能的负载均衡算法,fair算法可以根据页面大小和加载时间长短智能地进行负载均衡,响应时间短的优先分配。
5、url_hash(第三方插件):按访问url的hash结果来分配请求,使每个url定向到同一个后端服务器,可以进一步提高后端缓存服务器的效率。
当用户访问时,先访问到一个转发服务器,再由转发服务器将访问分发到压力更小的服务器。
Nginx负载均衡实现的策略有以下五种:
1、轮询(默认):每个请求按时间顺序逐一分配到不同的后端服务器,如果后端某个服务器宕机,能自动剔除故障系统。
2、权重weight:weight的值越大分配到的访问概率越高,主要用于后端每台服务器性能不均衡的情况下。其次是为在主从的情况下设置不同的权值,达到合理有效的地利用主机资源。
3、ip_hash( IP绑定):每个请求按访问IP的哈希结果分配,使来自同一个IP的访客固定访问一台后端服务器,并且可以有效解决动态网页存在的session共享问题
4、fair(第三方插件):对比 weight、ip_hash更加智能的负载均衡算法,fair算法可以根据页面大小和加载时间长短智能地进行负载均衡,响应时间短的优先分配。
5、url_hash(第三方插件):按访问url的hash结果来分配请求,使每个url定向到同一个后端服务器,可以进一步提高后端缓存服务器的效率。
Nginx配置高可用性怎么配置?
1、指定上游服务器负载均衡服务器
2、设置nginx与上游服务器超时时间和后端服务器连接的超时时间
3、设置nginx发送给上游服务器超时时间
4、设置nginx接受上游服务器超时时间
2、设置nginx与上游服务器超时时间和后端服务器连接的超时时间
3、设置nginx发送给上游服务器超时时间
4、设置nginx接受上游服务器超时时间
Nginx怎么判断别IP不可访问?
如果访问的ip地址为xxxIP,则返回403
Rewrite全局变量是什么?
全局变量
怎么限制浏览器访问?
使用if判断$http_user_agent ~ +浏览器名,是就返回500
nginx能监听同一端口吗?
可以
nginx提供哪些监控参数?
1、Active connections 当前活跃的用户连接
2、accepts 接收到的用户连接总数
3、handled Nginx处理的用户连接总数
4、requests 用户请求总数
5、Reading 当前连接中Nginx读取请求首部的个数
6、Writing 当前连接中Nginx写返回给用户的个数
7、Waiting 当前没有请求的活跃用户连接数
2、accepts 接收到的用户连接总数
3、handled Nginx处理的用户连接总数
4、requests 用户请求总数
5、Reading 当前连接中Nginx读取请求首部的个数
6、Writing 当前连接中Nginx写返回给用户的个数
7、Waiting 当前没有请求的活跃用户连接数
如何获取Nginx性能监控参数?
Nginx网页显示监控参数,默认是关闭的需要使用使用ngx_http_stub_status_module来解锁,
使用Nginx -s reload命令查看监控参数
使用Nginx -s reload命令查看监控参数
nacos
1.服务注册
各服务通过nacos client向注册中心发送注册请求,发送的注册请求数据放在一个阻塞队列中(set集合)。客户端即刻返回注册成功
Nacos server通过一个异步任务去阻塞队列中进行消费,然后去写进注册表,最终完成注册。
2.注册过程中如何实现高并发?
解答:通过内存队列+异步任务的形式
3.心跳机制
1.nacos client通过一个心跳任务来保持与注册中心的连接,也就是保活
2.ZK、netty也是通过心跳机制进行保活,但是使用的是轻量级的socket实现;Nacos底层使用的是http接口;
3.心跳机制:通过线程池,延时启动,默认5秒里面有个方法是setBeat();
4.接收到心跳之后,服务端需要做什么?
服务端需要更新本次心跳的时间,也就是最后更新时间。
5.服务端如何进行感知心跳的?
Helthcheck()方法,进行健康检查任务,runnable 默认延时5秒处理
服务端拿到Instance的set集合后,遍历判断
使用(当前时间-最后更新时间>设置的健康超时时间(默认15秒)),如果超过了15秒,则将状态改为 不健康。
里面还有一个遍历判断:
使用(当前时间-最后更新时间>设置的健康超时时间(默认30秒)),如果超过了30秒,则认为服务挂掉了,则从注册表中将该服务剔除
使用(当前时间-最后更新时间>设置的健康超时时间(默认15秒)),如果超过了15秒,则将状态改为 不健康。
里面还有一个遍历判断:
使用(当前时间-最后更新时间>设置的健康超时时间(默认30秒)),如果超过了30秒,则认为服务挂掉了,则从注册表中将该服务剔除
4.Nacos如何处理注册表的读写高并发冲突?
使用COW机制,也就是写实复制技术Copy On Write
首先一个概念:注册表的结构是一个双层Map的结构
Map<String,Map<String,Service>>
Space--Group--Service--Cluster--Instance
例子:develop(开发版本)--组(订单服务组、积分服务组)--服务(订单服务)--地区(BJ、NJ)--实例(机器9001/9002)
首先一个概念:注册表的结构是一个双层Map的结构
Map<String,Map<String,Service>>
Space--Group--Service--Cluster--Instance
例子:develop(开发版本)--组(订单服务组、积分服务组)--服务(订单服务)--地区(BJ、NJ)--实例(机器9001/9002)
如何读写数据避免数据错乱?保持读写的一致性?
一种是加锁,但加锁无法实现读写的高并发。
一种是写时复制COW。也就是在注册时:复制一份注册表(根据Instance,复制其中的一部分),在副本中进行写,写完进行替换。复制的内容其实是一个set集合,也就是Map结构里面的内层结构,根据Instance名称进行定位,并非复制的整个注册表。
服务发现时,读取原数据,拉取到本地,然后经过frign接口去调用其他服务,当然frign调用的时候也要借助ribbon进行负载均衡,所以说ribbon实际上是做的客户端的负载均衡。实际上nacos是一个读写分离的机制。从而实现了高并发。
一种是加锁,但加锁无法实现读写的高并发。
一种是写时复制COW。也就是在注册时:复制一份注册表(根据Instance,复制其中的一部分),在副本中进行写,写完进行替换。复制的内容其实是一个set集合,也就是Map结构里面的内层结构,根据Instance名称进行定位,并非复制的整个注册表。
服务发现时,读取原数据,拉取到本地,然后经过frign接口去调用其他服务,当然frign调用的时候也要借助ribbon进行负载均衡,所以说ribbon实际上是做的客户端的负载均衡。实际上nacos是一个读写分离的机制。从而实现了高并发。
那么问题来了:假设两个服务同时注册、同时都复制了一份然后进行副本修改,替换的时候会产生并发覆盖问题吗?
解答:不会。因为消费的时候是从阻塞队列中进行消费,是一个单线程的消费,所以不会出现并发覆盖的问题。
解答:不会。因为消费的时候是从阻塞队列中进行消费,是一个单线程的消费,所以不会出现并发覆盖的问题。
那么问题又来了:既然是单线程的消费,会不会导致队列任务的积压,因为单线程毕竟消费慢?
解答:一、这是一个极小的概率事件,内存队列也比较的快,可以处理的来。二、就算出现了也无所谓,稍微延迟一点没有关系,eureka还延迟一分钟以上呢。除非你同时启动上千台才会出现这种情况。
注:nacos的TPS为13000/S.
TPS为吞吐量,吞吐量为每秒内的访问数值,
QPS为峰值时的每秒钟访问数值。
解答:一、这是一个极小的概率事件,内存队列也比较的快,可以处理的来。二、就算出现了也无所谓,稍微延迟一点没有关系,eureka还延迟一分钟以上呢。除非你同时启动上千台才会出现这种情况。
注:nacos的TPS为13000/S.
TPS为吞吐量,吞吐量为每秒内的访问数值,
QPS为峰值时的每秒钟访问数值。
5.nacos配置文件的加载顺序?
bootstrap.yml -> application.yml -> application-stg.yml -> order-service.yml -> order-service-stg.yaml
6.nacos如何配置不同环境或多环境
可以在bootstrap.yml中利用Spring.profiles进行配置
7.项目在启动中
Nacos的版本是多少?
我们使用的版本号1.4.1
其他版本:
1.4.2
2.0.3
2.1.0
其他版本:
1.4.2
2.0.3
2.1.0
Token鉴权
生成的token后在服务端有哪些存储方式?
(1)保存在redis,最常用,也是分布式下的验证token的解决方案,
(2)数据库存储,性能比redis稍差,速度稍慢
(3)不做保存,下次验证的时候直接用jwt.decode验证(服务端为express),存储的压力给到了客户端,但是每次从客户端传到服务器端的数据量会稍微大一些
(2)数据库存储,性能比redis稍差,速度稍慢
(3)不做保存,下次验证的时候直接用jwt.decode验证(服务端为express),存储的压力给到了客户端,但是每次从客户端传到服务器端的数据量会稍微大一些
token是有状态的还是无状态的?
无状态的
优点:可扩展性,安全性,多平台跨域
优点:可扩展性,安全性,多平台跨域
如何实现jwt续期
在jwt中保存过期时间,解析时进行判定,如果即将超时则重新设置过期时间返回一个新的jwt给客户端。
jwt和security的区别
jwt ( json web token)优点:
无状态,json格式简单,不需要在服务端存储
缺点
一旦创建无法销毁或者修改状态,没有办法做权限 控制
security优点:
和spring无缝结合,可以对用户状态坐控制,可以做权限控制
缺点
用起来比较复杂,权限控制需要一连串过滤连,消耗服务器内存空间
无状态,json格式简单,不需要在服务端存储
缺点
一旦创建无法销毁或者修改状态,没有办法做权限 控制
security优点:
和spring无缝结合,可以对用户状态坐控制,可以做权限控制
缺点
用起来比较复杂,权限控制需要一连串过滤连,消耗服务器内存空间
如何销毁jwt
生成token时,有效期设置短一些
可以和redis结合使用,退出时销毁redis的token。判断redis的token即可
还可以添加黑名单功能,对其进行拦截
可以和redis结合使用,退出时销毁redis的token。判断redis的token即可
还可以添加黑名单功能,对其进行拦截
既然token有效期短,怎么解决token失效后的续签问题?
在验证用户登录状态的代码中,添加一段逻辑:判断cookie即将到期时,重新生成一个token。比如token有效期为30分钟,当用户请求我们时,我们可以判断如果用户的token有效期还剩下10分钟,那么就重新生成token。因此用户只要在操作我们的网站,就会续签token
如何防止cookie被篡改
可以设置cokkie的状态为httponly
可以在请求中添加防盗链
可以把token存放在请求头中或者local storage中,由于无状态的token过长,很容易达到4kb
由于容易被篡改,payload中不要放用户重要信息,如密码
可以在请求中添加防盗链
可以把token存放在请求头中或者local storage中,由于无状态的token过长,很容易达到4kb
由于容易被篡改,payload中不要放用户重要信息,如密码
如果用户禁用cookie怎么办
普通用户一般不知道cookie,也不会禁用,如何禁用我们对其友好提示,让其开启
如何防止token异地登陆
token是没办法做异地的判断的,这时我们可以采取传统的session方式
你们怎么做权限控制的
我们有一套权限控制服务,保存用户的角色和路径信息、当用户发起请求的时候,通过网关的的全局pre过滤器拦截所有请求,获取请求信息,解析jwt参数,根据参数获取用户,根据用户明去查询数据库的用户是否存在,以及当前用户有无访问此地址的权限,没有即拦截
你们微服务地址对外暴漏了怎么办
首先因为用户访问的时候走的是我们nginx的代理,微服务对外暴漏的可能性很低,即使暴漏了,我们每个微服务也有一套的鉴权校验策略;首先有一张表存每个微服务的id,密钥;服务器启动的时候使用id和密钥去授权中心生成jwt令牌,去访问其他的时候访问携带令牌
如何防止token被篡改
jwt使用了header和payload参数来校验是否被篡改,header存放算法类型和类型是jwt方式的参数,payload是存放用户自定义的参数,还有签名结合来确认token是否被篡改
如何完成权限校验的?
首先我们有权限管理的服务,管理用户的各种权限,及可访问路径等
在网关zuul中利用Pre过滤器,拦截一切请求,在过滤器中,解析jwt,获取用户身份,查询用户权限,判断用户身份可以访问当前路径
在网关zuul中利用Pre过滤器,拦截一切请求,在过滤器中,解析jwt,获取用户身份,查询用户权限,判断用户身份可以访问当前路径
多次登录生成的token都是一样的吗?都是可用的吗?
可以再payload加上时间戳,来保证每次生成的token都不一样,都是可用的
手机号一天能进行多少次登录校验?
1、时间上:一般来说用户在获取验证码都设置有时间间隔,一般为60秒,如果用户在规定的时间内没有收到短信验证码,可以点击再次获取,来重新取得短信验证码。而验证码的有效时间限制是为了防止不法分子利用暴力手段破解后使用。一般短信服务商都会提醒企业设置验证码的有效时间,超过有效时间验证码即失效。
2、次数上:短信验证码虽然很方便,但是它的使用次数是有限制的,不是用户想使用几次就可以使用几次的,这也是为了避免一些无聊用户频繁获取短信验证码而设置的一道坎。正常讲,大多数人肯定不会在注册账号或者更改个人信息的次数很频繁,所以如果遇到用户频繁多次的向后台发送验证码请求(而且都是同一个手机号),达到限制要求后就不能再正常使用。一般来说是设置的是一个手机号同一天时间只能发送5次验证码请求。
3、频率上:一般用户在更改密码、注册某些帐号时,对于短信验证码总是不能正确输入,那么该验证就会视为无效。因为一般正常规范的验证码短信在极短时间内就能送达,有效时间内用户是完全能输入完成的。如果想再次输入短信验证码只能再次获取,再次录入。
2、次数上:短信验证码虽然很方便,但是它的使用次数是有限制的,不是用户想使用几次就可以使用几次的,这也是为了避免一些无聊用户频繁获取短信验证码而设置的一道坎。正常讲,大多数人肯定不会在注册账号或者更改个人信息的次数很频繁,所以如果遇到用户频繁多次的向后台发送验证码请求(而且都是同一个手机号),达到限制要求后就不能再正常使用。一般来说是设置的是一个手机号同一天时间只能发送5次验证码请求。
3、频率上:一般用户在更改密码、注册某些帐号时,对于短信验证码总是不能正确输入,那么该验证就会视为无效。因为一般正常规范的验证码短信在极短时间内就能送达,有效时间内用户是完全能输入完成的。如果想再次输入短信验证码只能再次获取,再次录入。
docker
1.Dockerfile的构建过程
(1) docker从基础镜像运行一个容器
(2)执行一条指令并对容器作出修改
(3)执行类似docker commit的操作提交一个 新的镜像层
(4) docker再基 于刚提交的镜像运行个新容器
(5)执行dockerfile中的下一 条指令 直到所有指令都执行完成
(2)执行一条指令并对容器作出修改
(3)执行类似docker commit的操作提交一个 新的镜像层
(4) docker再基 于刚提交的镜像运行个新容器
(5)执行dockerfile中的下一 条指令 直到所有指令都执行完成
2.docker组成
从应用软件的角度来看,Dockerfile、Docker镜像与Docker容器分别代表软件的三个不同阶段,
* Dockerfile是软件的原材料
* Docker镜像是软件的交付品
* Docker容器则可以认为是软件的运行态。
Dockerfile面向开发,Docker镜像成为交付标准,Docker容器则涉及部署与运维,三者缺一不可,合力充当Docker体系的基石。
* Dockerfile是软件的原材料
* Docker镜像是软件的交付品
* Docker容器则可以认为是软件的运行态。
Dockerfile面向开发,Docker镜像成为交付标准,Docker容器则涉及部署与运维,三者缺一不可,合力充当Docker体系的基石。
1.Dockerfile,需要定义一个Dockerfile,Dockerfile定义了进程需要的一切东西。Dockerfile涉及的内容包括执行代码或者是文件、环境变量、依赖包、运行时环境、动态链接库、操作系统的发行版、服务进程和内核进程(当应用进程需要和系统服务和内核进程打交道,这时需要考虑如何设计namespace的权限控制)等等;
2.Docker镜像,在用Dockerfile定义一个文件之后,docker build时会产生一个Docker镜像,当运行 Docker镜像时,会真正开始提供服务;
3.Docker容器,容器是直接提供服务的。
什么是 Docker 容器?
Docker 是一种流行的开源软件平台,可简化创建、管理、运行和分发应用程序的过程。它使用容器来打包应用程序及其依赖项。我们也可以将容器视为 Docker 镜像的运行时实例。
Docker 和虚拟机有什么不同?
Docker 是轻量级的沙盒,在其中运行的只是应用,虚拟机里面还有额外的系统。
什么是 DockerFile?
Dockerfile 是一个文本文件,其中包含我们需要运行以构建 Docker 镜像的所有命令,每一条指令构建一层,因此每一条指令的内容,就是描述该层应当如何构建。Docker 使用 Dockerfile 中的指令自动构建镜像。我们可以 docker build 用来创建按顺序执行多个命令行指令的自动构建。
使用Docker Compose时如何保证容器A先于容器B运行?
Docker Compose 是一个用来定义和运行复杂应用的Docker工具。一个使用Docker容器的应用,通常由多个容器组成。使用Docker Compose不再需要使用shell脚本来启动容器。Compose 通过一个配置文件来管理多个Docker容器。简单理解:Docker Compose 是docker的管理工具。
一个完整的Docker由哪些部分组成?
DockerClient 客户端
Docker Daemon 守护进程
Docker Image 镜像
DockerContainer 容器
Docker Daemon 守护进程
Docker Image 镜像
DockerContainer 容器
docker常用命令
查看本地主机的所用镜像:`docker images``
搜索镜像:`docker search mysql``
下载镜像:docker pull mysql,没写 tag 就默认下载最新的 lastest
下载指定版本的镜像:`docker pull mysql:5.7``
删除镜像:`docker rmi -f 镜像id 镜像id 镜像id``
docker --help查看帮助命令
搜索镜像:`docker search mysql``
下载镜像:docker pull mysql,没写 tag 就默认下载最新的 lastest
下载指定版本的镜像:`docker pull mysql:5.7``
删除镜像:`docker rmi -f 镜像id 镜像id 镜像id``
docker --help查看帮助命令
描述 Docker 容器的生命周期。
创建容器
运行容器
暂停容器(可选)
取消暂停容器(可选)
启动容器
停止容器
重启容器
杀死容器
销毁容器
运行容器
暂停容器(可选)
取消暂停容器(可选)
启动容器
停止容器
重启容器
杀死容器
销毁容器
Docker网络有哪几种?
host类型的网络
bridge类型的网络
overlay类型的网络
macvlan类型的网络
none类型的网络
bridge类型的网络
overlay类型的网络
macvlan类型的网络
none类型的网络
docker容器之间怎么隔离?
Linux中的PID、IPC、网络等资源是全局的,而Linux的NameSpace机制是一种资源隔离方案,在该机制下这些资源就不再是全局的了,而是属于某个特定的NameSpace,各个NameSpace下的资源互不干扰。
Namespace实际上修改了应用进程看待整个计算机“视图”,即它的“视线”被操作系统做了限制,只能“看到”某些指定的内容。对于宿主机来说,这些被“隔离”了的进程跟其他进程并没有区别。
虽然有了NameSpace技术可以实现资源隔离,但进程还是可以不受控的访问系统资源,比如CPU、内存、磁盘、网络等,为了控制容器中进程对资源的访问,Docker采用control groups技术(也就是cgroup),有了cgroup就可以控制容器中进程对系统资源的消耗了,比如你可以限制某个容器使用内存的上限、可以在哪些CPU上运行等等。
Namespace实际上修改了应用进程看待整个计算机“视图”,即它的“视线”被操作系统做了限制,只能“看到”某些指定的内容。对于宿主机来说,这些被“隔离”了的进程跟其他进程并没有区别。
虽然有了NameSpace技术可以实现资源隔离,但进程还是可以不受控的访问系统资源,比如CPU、内存、磁盘、网络等,为了控制容器中进程对资源的访问,Docker采用control groups技术(也就是cgroup),有了cgroup就可以控制容器中进程对系统资源的消耗了,比如你可以限制某个容器使用内存的上限、可以在哪些CPU上运行等等。
Docker 和虚拟机有啥不同?
Docker 是轻量级的沙盒,在其中运行的只是应用,虚拟机里面还有额外的系统。
Docker 安全么?
Docker 利用了 Linux 内核中很多安全特性来保证不同容器之间的隔离,并且通 过签名机制来对镜像进行验证。大量生产环境的部署证明,Docker 虽然隔离性无法与 虚拟机相比,但仍然具有极高的安全性。
构建 Docker 镜像应该遵循哪些原则?
整体远侧上,尽量保持镜像功能的明确和内容的精简,要点包括: l 尽量选取满足需求但较小的基础系统镜像,建议选择 debian:wheezy 镜像,仅有 86MB 大小 l 清理编译生成文件、安装包的缓存等临时文件 l 安装各个软件时候要指定准确的版本号,并避免引入不需要的依赖 l 从安全的角度考虑,应用尽量使用系统的库和依赖 l 使用 Dockerfile 创建镜像时候要添加.dockerignore 文件或使用干净的工作目录 容器相关
容器退出后,通过 docker ps 命令查看不到,数据会丢失么
容器退出后会处于终止(exited)状态,此时可以通过 docker ps -a 查看,其中 数据不会丢失,还可以通过 docker start 来启动,只有删除容器才会清除数据。
可以在一个容器中同时运行多个应用进程吗?
一般不推荐在同一个容器内运行多个应用进程,如果有类似需求,可以通过额外 的进程管理机制,比如 supervisord 来管理所运行的进程
开发环境中 Docker 与 Vagrant 该如何选择?
Docker 不是虚拟机,而是进程隔离,对于资源的消耗很少,单一开发环境下 Vagrant 是虚拟机上的封装,虚拟机本身会消耗资源。 Other FAQ
容器退出后,通过 docker ps 命令查看不到,数据会丢失么?
容器退出后会处于终止(exited)状态,此时可以通过 docker ps -a 查看,其中 数据不会丢失,还可以通过 docker start 来启动,只有删除容器才会清除数据
Docker 与 Vagrant 有何不同?
两者的定位完全不同 Vagrant 类似于 Boot2Docker(一款运行 Docker 的最小内核),是一套虚拟机的管 理环境,Vagrant 可以在多种系统上和虚拟机软件中运行,可以在 Windows。Mac 等非 Linux 平台上为 Docker 支持,自身具有较好的包装性和移植性。 原生 Docker 自身只能运行在 Linux 平台上,但启动和运行的性能都比虚拟机要快, 往往更适合快速开发和部署应用的场景。
如何将一台宿主机的 docker 环境迁移到另外一台宿主机?
停止 Docker 服务,将整个 docker 存储文件复制到另外一台宿主机上,然后调整 另外一台宿主机的配置即可
仓库(Repository)、注册服务器(Registry)、注册索引(Index)有何关系
首先,仓库是存放一组关联镜像的集合,比如同一个应用的不同版本的镜像,注 册服务器是存放实际的镜像的地方,注册索引则负责维护用户的账号,权限,搜索, 标签等管理。注册服务器利用注册索引来实现认证等管理。
MQ
MQ有什么用?
消息队列有很多使用场景,比较常见的有3个:解耦、异步、削峰。
解耦:传统的软件开发模式,各个模块之间相互调用,数据共享,每个模块都要时刻关注其他模块的是否更改或者是否挂掉等等,使用消息队列,可以避免模块之间直接调用,将所需共享的数据放在消息队列中,对于新增业务模块,只要对该类消息感兴趣,即可订阅该类消息,对原有系统和业务没有任何影响,降低了系统各个模块的耦合度,提高了系统的可扩展性。
异步:消息队列提供了异步处理机制,在很多时候应用不想也不需要立即处理消息,允许应用把一些消息放入消息中间件中,并不立即处理它,在之后需要的时候再慢慢处理
削峰:在访问量骤增的场景下,需要保证应用系统的平稳性,但是这样突发流量并不常见,如果以这类峰值的标准而投放资源的话,那无疑是巨大的浪费。使用消息队列能够使关键组件支撑突发访问压力,不会因为突发的超负荷请求而完全崩溃。消息队列的容量可以配置的很大,如果采用磁盘存储消息,则几乎等于“无限”容量,这样一来,高峰期的消息可以被积压起来,在随后的时间内进行平滑的处理完成,而不至于让系统短时间内无法承载而导致崩溃。在电商网站的秒杀抢购这种突发性流量很强的业务场景中,消息队列的强大缓冲能力可以很好的起到削峰作用。
缺点:
系统可用性降低
系统的复杂度提高了
一致性问题
系统的复杂度提高了
一致性问题
消息队列如何保证顺序消费?
方案一:拆分多个队列,每个队列一个消费者
方案二:一个队列对应一个消费者,然后这个消费者内部用内存队列做排队,然后分发给底层不同的worker来处理
方案二:一个队列对应一个消费者,然后这个消费者内部用内存队列做排队,然后分发给底层不同的worker来处理
消息队列如何保证消息不丢?
消息丢失可能发生在生产者发送消息、MQ本身丢失消息、消费者丢失消息3个方面。
RabbitMQ:
RabbitMQ丢失消息分为如下几种情况:
生产者丢消息:
生产者将数据发送到RabbitMQ的时候,可能在传输过程中因为网络等问题而将数据弄丢了。
RabbitMQ自己丢消息:
如果没有开启RabbitMQ的持久化,那么RabbitMQ一旦重启数据就丢了。所以必须开启持久化将消息持久化到磁盘,这样就算RabbitMQ挂了,恢复之后会自动读取之前存储的数据,一般数据不会丢失。除非极其罕见的情况,RabbitMQ还没来得及持久化自己就挂了,这样可能导致一部分数据丢失。
消费端丢消息:
主要是因为消费者消费时,刚消费到还没有处理,结果消费者就挂了,这样你重启之后,RabbitMQ就认为你已经消费过了,然后就丢了数据。
生产者丢消息:
生产者将数据发送到RabbitMQ的时候,可能在传输过程中因为网络等问题而将数据弄丢了。
RabbitMQ自己丢消息:
如果没有开启RabbitMQ的持久化,那么RabbitMQ一旦重启数据就丢了。所以必须开启持久化将消息持久化到磁盘,这样就算RabbitMQ挂了,恢复之后会自动读取之前存储的数据,一般数据不会丢失。除非极其罕见的情况,RabbitMQ还没来得及持久化自己就挂了,这样可能导致一部分数据丢失。
消费端丢消息:
主要是因为消费者消费时,刚消费到还没有处理,结果消费者就挂了,这样你重启之后,RabbitMQ就认为你已经消费过了,然后就丢了数据。
针对上述三种情况,RabbitMQ可以采用如下方式避免消息丢失:
生产者丢消息:
可以选择使用RabbitMQ提供是事务功能,就是生产者在发送数据之前开启事务,然后发送消息,如果消息没有成功被RabbitMQ接收到,那么生产者会受到异常报错,这时就可以回滚事务,然后尝试重新发送。如果收到了消息,那么就可以提交事务。这种方式有明显的缺点,即RabbitMQ事务开启后,就会变为同步阻塞操作,生产者会阻塞等待是否发送成功,太耗性能会造成吞吐量的下降。
可以开启confirm模式。在生产者那里设置开启了confirm模式之后,每次写的消息都会分配一个唯一的id,然后如何写入了RabbitMQ之中,RabbitMQ会给你回传一个ack消息,告诉你这个消息发送OK了。如果RabbitMQ没能处理这个消息,会回调你一个nack接口,告诉你这个消息失败了,你可以进行重试。而且你可以结合这个机制知道自己在内存里维护每个消息的id,如果超过一定时间还没接收到这个消息的回调,那么你可以进行重发。
事务机制是同步的,你提交了一个事物之后会阻塞住,但是confirm机制是异步的,发送消息之后可以接着发送下一个消息,然后RabbitMQ会回调告知成功与否。 一般在生产者这块避免丢失,都是用confirm机制。
RabbitMQ自己丢消息:
设置消息持久化到磁盘,设置持久化有两个步骤:
创建queue的时候将其设置为持久化的,这样就可以保证RabbitMQ持久化queue的元数据,但是不会持久化queue里面的数据。
发送消息的时候讲消息的deliveryMode设置为2,这样消息就会被设为持久化方式,此时RabbitMQ就会将消息持久化到磁盘上。 必须要同时开启这两个才可以。
而且持久化可以跟生产的confirm机制配合起来,只有消息持久化到了磁盘之后,才会通知生产者ack,这样就算是在持久化之前RabbitMQ挂了,数据丢了,生产者收不到ack回调也会进行消息重发。
消费端丢消息:
使用RabbitMQ提供的ack机制,首先关闭RabbitMQ的自动ack,然后每次在确保处理完这个消息之后,在代码里手动调用ack。这样就可以避免消息还没有处理完就ack。
生产者丢消息:
可以选择使用RabbitMQ提供是事务功能,就是生产者在发送数据之前开启事务,然后发送消息,如果消息没有成功被RabbitMQ接收到,那么生产者会受到异常报错,这时就可以回滚事务,然后尝试重新发送。如果收到了消息,那么就可以提交事务。这种方式有明显的缺点,即RabbitMQ事务开启后,就会变为同步阻塞操作,生产者会阻塞等待是否发送成功,太耗性能会造成吞吐量的下降。
可以开启confirm模式。在生产者那里设置开启了confirm模式之后,每次写的消息都会分配一个唯一的id,然后如何写入了RabbitMQ之中,RabbitMQ会给你回传一个ack消息,告诉你这个消息发送OK了。如果RabbitMQ没能处理这个消息,会回调你一个nack接口,告诉你这个消息失败了,你可以进行重试。而且你可以结合这个机制知道自己在内存里维护每个消息的id,如果超过一定时间还没接收到这个消息的回调,那么你可以进行重发。
事务机制是同步的,你提交了一个事物之后会阻塞住,但是confirm机制是异步的,发送消息之后可以接着发送下一个消息,然后RabbitMQ会回调告知成功与否。 一般在生产者这块避免丢失,都是用confirm机制。
RabbitMQ自己丢消息:
设置消息持久化到磁盘,设置持久化有两个步骤:
创建queue的时候将其设置为持久化的,这样就可以保证RabbitMQ持久化queue的元数据,但是不会持久化queue里面的数据。
发送消息的时候讲消息的deliveryMode设置为2,这样消息就会被设为持久化方式,此时RabbitMQ就会将消息持久化到磁盘上。 必须要同时开启这两个才可以。
而且持久化可以跟生产的confirm机制配合起来,只有消息持久化到了磁盘之后,才会通知生产者ack,这样就算是在持久化之前RabbitMQ挂了,数据丢了,生产者收不到ack回调也会进行消息重发。
消费端丢消息:
使用RabbitMQ提供的ack机制,首先关闭RabbitMQ的自动ack,然后每次在确保处理完这个消息之后,在代码里手动调用ack。这样就可以避免消息还没有处理完就ack。
Kafka:
Kafka丢失消息分为如下几种情况:
生产者丢消息:
生产者没有设置相应的策略,发送过程中丢失数据。
Kafka自己丢消息:
比较常见的一个场景,就是Kafka的某个broker宕机了,然后重新选举partition的leader时。如果此时follower还没来得及同步数据,leader就挂了,然后某个follower成为了leader,它就少了一部分数据。
消费端丢消息:
消费者消费到了这个数据,然后消费之自动提交了offset,让Kafka知道你已经消费了这个消息,当你准备处理这个消息时,自己挂掉了,那么这条消息就丢了。
生产者丢消息:
生产者没有设置相应的策略,发送过程中丢失数据。
Kafka自己丢消息:
比较常见的一个场景,就是Kafka的某个broker宕机了,然后重新选举partition的leader时。如果此时follower还没来得及同步数据,leader就挂了,然后某个follower成为了leader,它就少了一部分数据。
消费端丢消息:
消费者消费到了这个数据,然后消费之自动提交了offset,让Kafka知道你已经消费了这个消息,当你准备处理这个消息时,自己挂掉了,那么这条消息就丢了。
RabbitMQ有哪些工作模式?
简单队列
一个生成者对应一个消费者和一个队列
工作队列
发布/订阅模式
路由模式
RabbitMQ有哪几种交换机
交换机的类型:
交换机主要包括如下4种类型:
Direct exchange(直连交换机)
Fanout exchange(扇型交换机)
Topic exchange(主题交换机)
Headers exchange(头交换机)
Direct exchange(直连交换机)
直连型交换机(direct exchange)是根据消息携带的路由键(routing key)将消息投递给对应队列的,步骤如下:
将一个队列绑定到某个交换机上,同时赋予该绑定一个路由键(routing key)
当一个携带着路由值为R的消息被发送给直连交换机时,交换机会把它路由给绑定值同样为R的队列。
Fanout exchange(扇型交换机)
扇型交换机(funout exchange)将消息路由给绑定到它身上的所有队列。不同于直连交换机,路由键在此类型上不启任务作用。如果N个队列绑定到某个扇型交换机上,当有消息发送给此扇型交换机时,交换机会将消息的发送给这所有的N个队列
Topic exchange(主题交换机)
主题交换机(topic exchanges)中,队列通过路由键绑定到交换机上,然后,交换机根据消息里的路由值,将消息路由给一个或多个绑定队列。
扇型交换机和主题交换机异同:
对于扇型交换机路由键是没有意义的,只要有消息,它都发送到它绑定的所有队列上
对于主题交换机,路由规则由路由键决定,只有满足路由键的规则,消息才可以路由到对应的队列上
Headers exchange(头交换机)
类似主题交换机,但是头交换机使用多个消息属性来代替路由键建立路由规则。通过判断消息头的值能否与指定的绑定相匹配来确立路由规则。
此交换机有个重要参数:”x-match”
当”x-match”为“any”时,消息头的任意一个值被匹配就可以满足条件
当”x-match”设置为“all”的时候,就需要消息头的所有值都匹配成功
交换机主要包括如下4种类型:
Direct exchange(直连交换机)
Fanout exchange(扇型交换机)
Topic exchange(主题交换机)
Headers exchange(头交换机)
Direct exchange(直连交换机)
直连型交换机(direct exchange)是根据消息携带的路由键(routing key)将消息投递给对应队列的,步骤如下:
将一个队列绑定到某个交换机上,同时赋予该绑定一个路由键(routing key)
当一个携带着路由值为R的消息被发送给直连交换机时,交换机会把它路由给绑定值同样为R的队列。
Fanout exchange(扇型交换机)
扇型交换机(funout exchange)将消息路由给绑定到它身上的所有队列。不同于直连交换机,路由键在此类型上不启任务作用。如果N个队列绑定到某个扇型交换机上,当有消息发送给此扇型交换机时,交换机会将消息的发送给这所有的N个队列
Topic exchange(主题交换机)
主题交换机(topic exchanges)中,队列通过路由键绑定到交换机上,然后,交换机根据消息里的路由值,将消息路由给一个或多个绑定队列。
扇型交换机和主题交换机异同:
对于扇型交换机路由键是没有意义的,只要有消息,它都发送到它绑定的所有队列上
对于主题交换机,路由规则由路由键决定,只有满足路由键的规则,消息才可以路由到对应的队列上
Headers exchange(头交换机)
类似主题交换机,但是头交换机使用多个消息属性来代替路由键建立路由规则。通过判断消息头的值能否与指定的绑定相匹配来确立路由规则。
此交换机有个重要参数:”x-match”
当”x-match”为“any”时,消息头的任意一个值被匹配就可以满足条件
当”x-match”设置为“all”的时候,就需要消息头的所有值都匹配成功
那你们使用什么mq?基于什么做的选型?
RocketMq
由于我们系统的qps压力比较大,所以性能是首要考虑的要素。
开发语言,由于我们的开发语言是java,主要是为了方便二次开发。
对于高并发的业务场景是必须的,所以需要支持分布式架构的设计。
功能全面,由于不同的业务场景,可能会用到顺序消息、事务消息等。
由于我们系统的qps压力比较大,所以性能是首要考虑的要素。
开发语言,由于我们的开发语言是java,主要是为了方便二次开发。
对于高并发的业务场景是必须的,所以需要支持分布式架构的设计。
功能全面,由于不同的业务场景,可能会用到顺序消息、事务消息等。
说一说生产者与消费者模式
所谓生产者-消费者问题,实际上主要是包含了两类线程。一种是生产者线程用于生产数据,另一种是消费者线程用于消费数据,为了解耦生产者和消费者的关系,通常会采用共享的数据区域,就像是一个仓库。生产者生产数据之后直接放置在共享数据区中,并不需要关心消费者的行为。而消费者只需要从共享数据区中去获取数据,就不再需要关心生产者的行为。但是,这个共享数据区域中应该具备这样的线程间并发协作的功能:
1.如果共享数据区已满的话,阻塞生产者继续生产数据放置入内;
2.如果共享数据区为空的话,阻塞消费者继续消费数据。
在Java语言中,实现生产者消费者问题时,可以采用三种方式:
1.使用 Object 的 wait/notify 的消息通知机制;
2.使用 Lock 的 Condition 的 await/signal 的消息通知机制;
3.使用 BlockingQueue 实现。
1.如果共享数据区已满的话,阻塞生产者继续生产数据放置入内;
2.如果共享数据区为空的话,阻塞消费者继续消费数据。
在Java语言中,实现生产者消费者问题时,可以采用三种方式:
1.使用 Object 的 wait/notify 的消息通知机制;
2.使用 Lock 的 Condition 的 await/signal 的消息通知机制;
3.使用 BlockingQueue 实现。
你说到消费者消费失败的问题,那么如果一直消费失败导致消息积压怎么处理?
因为考虑到时消费者消费⼀直出错的问题,那么我们可以从以下⼏个⻆度来考虑:
1. 消费者出错,肯定是程序或者其他问题导致的,如果容易修复,先把问题修复,让consumer恢复
正常消费
2. 如果时间来不及处理很麻烦,做转发处理,写⼀个临时的consumer消费⽅案,先把消息消费,然
后再转发到⼀个新的topic和MQ资源,这个新的topic的机器资源单独申请,要能承载住当前积压的
消息
3. 处理完积压数据后,修复consumer,去消费新的MQ和现有的MQ数据,新MQ消费完成后恢复原
状
1. 消费者出错,肯定是程序或者其他问题导致的,如果容易修复,先把问题修复,让consumer恢复
正常消费
2. 如果时间来不及处理很麻烦,做转发处理,写⼀个临时的consumer消费⽅案,先把消息消费,然
后再转发到⼀个新的topic和MQ资源,这个新的topic的机器资源单独申请,要能承载住当前积压的
消息
3. 处理完积压数据后,修复consumer,去消费新的MQ和现有的MQ数据,新MQ消费完成后恢复原
状
那如果消息积压达到磁盘上限,消息被删除了怎么办?
最初,我们发送的消息记录是落库保存了的,⽽转发发送的数据也保存了,那么我们就可以通过这部分
数据来找到丢失的那部分数据,再单独跑个脚本重发就可以了。如果转发的程序没有落库,那就和消费
⽅的记录去做对⽐,只是过程会更艰难⼀点。
数据来找到丢失的那部分数据,再单独跑个脚本重发就可以了。如果转发的程序没有落库,那就和消费
⽅的记录去做对⽐,只是过程会更艰难⼀点。
说了这么多,那你说说RocketMQ实现原理吧?
RocketMQ由NameServer注册中心集群、Producer生产者集群、Consumer消费者集群和若干Broker(RocketMQ进程)组成,它的架构原理是这样的:
Broker在启动的时候去向所有的NameServer注册,并保持长连接,每30s发送一次心跳
Producer在发送消息的时候从NameServer获取Broker服务器地址,根据负载均衡算法选择一台服务器来发送消息
Conusmer消费消息的时候同样从NameServer获取Broker地址,然后主动拉取消息来消费
Broker在启动的时候去向所有的NameServer注册,并保持长连接,每30s发送一次心跳
Producer在发送消息的时候从NameServer获取Broker服务器地址,根据负载均衡算法选择一台服务器来发送消息
Conusmer消费消息的时候同样从NameServer获取Broker地址,然后主动拉取消息来消费
为什么RocketMQ不使用Zookeeper作为注册中心呢?
我认为有以下⼏个点是不使⽤zookeeper的原因:
1. 根据CAP理论,同时最多只能满⾜两个点,⽽zookeeper满⾜的是CP,也就是说zookeeper并不能
保证服务的可⽤性,zookeeper在进⾏选举的时候,整个选举的时间太⻓,期间整个集群都处于不
可⽤的状态,⽽这对于⼀个注册中⼼来说肯定是不能接受的,作为服务发现来说就应该是为可⽤性
⽽设计。
2. 基于性能的考虑,NameServer本身的实现⾮常轻量,⽽且可以通过增加机器的⽅式⽔平扩展,增
加集群的抗压能⼒,⽽zookeeper的写是不可扩展的,⽽zookeeper要解决这个问题只能通过划分
领域,划分多个zookeeper集群来解决,⾸先操作起来太复杂,其次这样还是⼜违反了CAP中的A
的设计,导致服务之间是不连通的。
3. 持久化的机制来带的问题,ZooKeeper 的 ZAB 协议对每⼀个写请求,会在每个 ZooKeeper 节点上
保持写⼀个事务⽇志,同时再加上定期的将内存数据镜像(Snapshot)到磁盘来保证数据的⼀致性
和持久性,⽽对于⼀个简单的服务发现的场景来说,这其实没有太⼤的必要,这个实现⽅案太重
了。⽽且本身存储的数据应该是⾼度定制化的。
4. 消息发送应该弱依赖注册中⼼,⽽RocketMQ的设计理念也正是基于此,⽣产者在第⼀次发送消息
的时候从NameServer获取到Broker地址后缓存到本地,如果NameServer整个集群不可⽤,短时
间内对于⽣产者和消费者并不会产⽣太⼤影响。
1. 根据CAP理论,同时最多只能满⾜两个点,⽽zookeeper满⾜的是CP,也就是说zookeeper并不能
保证服务的可⽤性,zookeeper在进⾏选举的时候,整个选举的时间太⻓,期间整个集群都处于不
可⽤的状态,⽽这对于⼀个注册中⼼来说肯定是不能接受的,作为服务发现来说就应该是为可⽤性
⽽设计。
2. 基于性能的考虑,NameServer本身的实现⾮常轻量,⽽且可以通过增加机器的⽅式⽔平扩展,增
加集群的抗压能⼒,⽽zookeeper的写是不可扩展的,⽽zookeeper要解决这个问题只能通过划分
领域,划分多个zookeeper集群来解决,⾸先操作起来太复杂,其次这样还是⼜违反了CAP中的A
的设计,导致服务之间是不连通的。
3. 持久化的机制来带的问题,ZooKeeper 的 ZAB 协议对每⼀个写请求,会在每个 ZooKeeper 节点上
保持写⼀个事务⽇志,同时再加上定期的将内存数据镜像(Snapshot)到磁盘来保证数据的⼀致性
和持久性,⽽对于⼀个简单的服务发现的场景来说,这其实没有太⼤的必要,这个实现⽅案太重
了。⽽且本身存储的数据应该是⾼度定制化的。
4. 消息发送应该弱依赖注册中⼼,⽽RocketMQ的设计理念也正是基于此,⽣产者在第⼀次发送消息
的时候从NameServer获取到Broker地址后缓存到本地,如果NameServer整个集群不可⽤,短时
间内对于⽣产者和消费者并不会产⽣太⼤影响。
Master和Slave之间是怎么同步数据的呢?
消息在master和slave之间的同步是根据raft协议来进⾏的:
1. 在broker收到消息后,会被标记为uncommitted状态
2. 然后会把消息发送给所有的slave
3. slave在收到消息之后返回ack响应给master
4. master在收到超过半数的ack之后,把消息标记为committed
5. 发送committed消息给所有slave,slave也修改状态为committed
1. 在broker收到消息后,会被标记为uncommitted状态
2. 然后会把消息发送给所有的slave
3. slave在收到消息之后返回ack响应给master
4. master在收到超过半数的ack之后,把消息标记为committed
5. 发送committed消息给所有slave,slave也修改状态为committed
你知道RocketMQ为什么速度快吗?
是因为使⽤了顺序存储、Page Cache和异步刷盘。
1. 我们在写⼊commitlog的时候是顺序写⼊的,这样⽐随机写⼊的性能就会提⾼很多
2. 写⼊commitlog的时候并不是直接写⼊磁盘,⽽是先写⼊操作系统的PageCache
3. 最后由操作系统异步将缓存中的数据刷到磁盘
1. 我们在写⼊commitlog的时候是顺序写⼊的,这样⽐随机写⼊的性能就会提⾼很多
2. 写⼊commitlog的时候并不是直接写⼊磁盘,⽽是先写⼊操作系统的PageCache
3. 最后由操作系统异步将缓存中的数据刷到磁盘
什么是事务、半事务消息?怎么实现的?
事务消息就是MQ提供的类似XA的分布式事务能⼒,通过事务消息可以达到分布式事务的最终⼀致性。
半事务消息就是MQ收到了⽣产者的消息,但是没有收到⼆次确认,不能投递的消息。
实现原理如下:
1. ⽣产者先发送⼀条半事务消息到MQ
2. MQ收到消息后返回ack确认
3. ⽣产者开始执⾏本地事务
4. 如果事务执⾏成功发送commit到MQ,失败发送rollback
5. 如果MQ⻓时间未收到⽣产者的⼆次确认commit或者rollback,MQ对⽣产者发起消息回查
6. ⽣产者查询事务执⾏最终状态
7. 根据查询事务状态再次提交⼆次确认
最终,如果MQ收到⼆次确认commit,就可以把消息投递给消费者,反之如果是rollback,消息会保存
下来并且在3天后被删除。
半事务消息就是MQ收到了⽣产者的消息,但是没有收到⼆次确认,不能投递的消息。
实现原理如下:
1. ⽣产者先发送⼀条半事务消息到MQ
2. MQ收到消息后返回ack确认
3. ⽣产者开始执⾏本地事务
4. 如果事务执⾏成功发送commit到MQ,失败发送rollback
5. 如果MQ⻓时间未收到⽣产者的⼆次确认commit或者rollback,MQ对⽣产者发起消息回查
6. ⽣产者查询事务执⾏最终状态
7. 根据查询事务状态再次提交⼆次确认
最终,如果MQ收到⼆次确认commit,就可以把消息投递给消费者,反之如果是rollback,消息会保存
下来并且在3天后被删除。
死信队列
ElasticSearch
为什么要使用ES
数据量增大是模糊查询会放弃索引,效率低下。而es做全文索引提高查询速度。
ES怎么做数据同步?
1.同步双写
数据写到mysql时,同时将数据写到ES,实现数据的双写。
优点:业务逻辑简单。
缺点:硬编码,业务强耦合,性能较差,代码的侵入性太强
2.异步双写(MQ方式)(程序到MQ,MQ再到MYSQL和ES)
改为写MQ,不直接写ES
优点:性能高(由于MQ的性能基本比mysql高出一个数量级);不存在丢数据问题
缺点:硬编码,业务强耦合,代码的侵入性太强,延时
3.异步双写(Worker方式)(定时任务)
让该程序按一定的时间周期扫描指定的表,把该时间段内发生变化的数据提取出来;逐条写入到ES中。
优点:不改变原来代码,没有侵入性、没有硬编码;没有业务强耦合;
缺点:时效性较差
4.Binlog 同步方式
利用mysql的binlog来进行同步
优点:没有代码侵入、没有硬编码;原有系统不需要任何变化,没有感知;性能高;业务解耦,不需要关注原来系统的业务逻辑。
缺点:构建Binlog系统复杂;也像方案二,存在MQ延时的风险
数据写到mysql时,同时将数据写到ES,实现数据的双写。
优点:业务逻辑简单。
缺点:硬编码,业务强耦合,性能较差,代码的侵入性太强
2.异步双写(MQ方式)(程序到MQ,MQ再到MYSQL和ES)
改为写MQ,不直接写ES
优点:性能高(由于MQ的性能基本比mysql高出一个数量级);不存在丢数据问题
缺点:硬编码,业务强耦合,代码的侵入性太强,延时
3.异步双写(Worker方式)(定时任务)
让该程序按一定的时间周期扫描指定的表,把该时间段内发生变化的数据提取出来;逐条写入到ES中。
优点:不改变原来代码,没有侵入性、没有硬编码;没有业务强耦合;
缺点:时效性较差
4.Binlog 同步方式
利用mysql的binlog来进行同步
优点:没有代码侵入、没有硬编码;原有系统不需要任何变化,没有感知;性能高;业务解耦,不需要关注原来系统的业务逻辑。
缺点:构建Binlog系统复杂;也像方案二,存在MQ延时的风险
ES有哪些查询方式?
全文检索、匹配查询、精准查询、过滤查询、组合查询、范围查询、模糊查询、排序查询、高亮查询、分页查询、聚合查询
1.查询所有文档
在 Postman 中,向 ES 服务器发 GET 请求 :http://127.0.0.1:9200/student/_search
{
"query": {
"match_all": {}
} }
# "query":这里的 query 代表一个查询对象,里面可以有不同的查询属性
# "match_all":查询类型,例如:match_all(代表查询所有), match,term , range 等等
# {查询条件}:查询条件会根据类型的不同,写法也有差异
{
"query": {
"match_all": {}
} }
# "query":这里的 query 代表一个查询对象,里面可以有不同的查询属性
# "match_all":查询类型,例如:match_all(代表查询所有), match,term , range 等等
# {查询条件}:查询条件会根据类型的不同,写法也有差异
2.匹配查询
match 匹配类型查询,会把查询条件进行分词,然后进行查询,多个词条之间是 or 的关系
在 Postman 中,向 ES 服务器发 GET 请求 :http://127.0.0.1:9200/student/_search
{
"query": {
"match": {
"name":"zhangsan"
}
} }
在 Postman 中,向 ES 服务器发 GET 请求 :http://127.0.0.1:9200/student/_search
{
"query": {
"match": {
"name":"zhangsan"
}
} }
3.字段匹配查询
multi_match 与 match 类似,不同的是它可以在多个字段中查询。
在 Postman 中,向 ES 服务器发 GET 请求 :http://127.0.0.1:9200/student/_search
{
"query": {
"multi_match": {
"query": "zhangsan",
"fields": ["name","nickname"]
}
} }
在 Postman 中,向 ES 服务器发 GET 请求 :http://127.0.0.1:9200/student/_search
{
"query": {
"multi_match": {
"query": "zhangsan",
"fields": ["name","nickname"]
}
} }
4.关键字精确查询
term 查询,精确的关键词匹配查询,不对查询条件进行分词。
在 Postman 中,向 ES 服务器发 GET 请求 :http://127.0.0.1:9200/student/_search
{
"query": {
"term": {
"name": {
"value": "zhangsan"
}
}
} }
在 Postman 中,向 ES 服务器发 GET 请求 :http://127.0.0.1:9200/student/_search
{
"query": {
"term": {
"name": {
"value": "zhangsan"
}
}
} }
5.多关键字精确查询
terms 查询和 term 查询一样,但它允许你指定多值进行匹配。
如果这个字段包含了指定值中的任何一个值,那么这个文档满足条件,类似于 mysql 的 in
在 Postman 中,向 ES 服务器发 GET 请求 :http://127.0.0.1:9200/student/_search
{
"query": {
"terms": {
"name": ["zhangsan","lisi"]
}
} }
如果这个字段包含了指定值中的任何一个值,那么这个文档满足条件,类似于 mysql 的 in
在 Postman 中,向 ES 服务器发 GET 请求 :http://127.0.0.1:9200/student/_search
{
"query": {
"terms": {
"name": ["zhangsan","lisi"]
}
} }
6.指定字段查询
默认情况下,Elasticsearch 在搜索的结果中,会把文档中保存在_source 的所有字段都返回。
如果我们只想获取其中的部分字段,我们可以添加_source 的过滤
在 Postman 中,向 ES 服务器发 GET 请求 :http://127.0.0.1:9200/student/_search
{
"_source": ["name","nickname"],
"query": {
"terms": {
"nickname": ["zhangsan"]
}
} }
如果我们只想获取其中的部分字段,我们可以添加_source 的过滤
在 Postman 中,向 ES 服务器发 GET 请求 :http://127.0.0.1:9200/student/_search
{
"_source": ["name","nickname"],
"query": {
"terms": {
"nickname": ["zhangsan"]
}
} }
7.过滤字段
includes:来指定想要显示的字段
excludes:来指定不想要显示的字段
在 Postman 中,向 ES 服务器发 GET 请求 :http://127.0.0.1:9200/student/_search
{
"_source": {
"includes": ["name","nickname"]
},
"query": {
"terms": {
"nickname": ["zhangsan"]
}
} }
excludes:来指定不想要显示的字段
在 Postman 中,向 ES 服务器发 GET 请求 :http://127.0.0.1:9200/student/_search
{
"_source": {
"includes": ["name","nickname"]
},
"query": {
"terms": {
"nickname": ["zhangsan"]
}
} }
8.组合查询
`bool`把各种其它查询通过`must`(必须 )、`must_not`(必须不)、`should`(应该)的方
式进行组合
在 Postman 中,向 ES 服务器发 GET 请求 :http://127.0.0.1:9200/student/_search
{
"query": {
"bool": {
"must": [
{
"match": {
"name": "zhangsan"
}
}
],
"must_not": [
{
"match": {
"age": "40"
}
}
],
"should": [
{
"match": {
"sex": "男"
}
}
]
}
} }
式进行组合
在 Postman 中,向 ES 服务器发 GET 请求 :http://127.0.0.1:9200/student/_search
{
"query": {
"bool": {
"must": [
{
"match": {
"name": "zhangsan"
}
}
],
"must_not": [
{
"match": {
"age": "40"
}
}
],
"should": [
{
"match": {
"sex": "男"
}
}
]
}
} }
9.范围查询
range 查询找出那些落在指定区间内的数字或者时间。
10.模糊查询
返回包含与搜索字词相似的字词的文档。
编辑距离是将一个术语转换为另一个术语所需的一个字符更改的次数。这些更改可以包括:
更改字符(box → fox) 删除字符(black → lack)
插入字符(sic → sick) 转置两个相邻字符(act → cat)
为了找到相似的术语,fuzzy 查询会在指定的编辑距离内创建一组搜索词的所有可能的变体
或扩展。然后查询返回每个扩展的完全匹配。
通过 fuzziness 修改编辑距离。一般使用默认值 AUTO,根据术语的长度生成编辑距离。
编辑距离是将一个术语转换为另一个术语所需的一个字符更改的次数。这些更改可以包括:
更改字符(box → fox) 删除字符(black → lack)
插入字符(sic → sick) 转置两个相邻字符(act → cat)
为了找到相似的术语,fuzzy 查询会在指定的编辑距离内创建一组搜索词的所有可能的变体
或扩展。然后查询返回每个扩展的完全匹配。
通过 fuzziness 修改编辑距离。一般使用默认值 AUTO,根据术语的长度生成编辑距离。
11.单字段排序
sort 可以让我们按照不同的字段进行排序,并且通过 order 指定排序的方式。desc 降序,asc
升序。
在 Postman 中,向 ES 服务器发 GET 请求 :http://127.0.0.1:9200/student/_search
{
"query": {
"match": {
"name":"zhangsan"
}
},
"sort": [{
"age": {
"order":"desc"
}
}]
}
升序。
在 Postman 中,向 ES 服务器发 GET 请求 :http://127.0.0.1:9200/student/_search
{
"query": {
"match": {
"name":"zhangsan"
}
},
"sort": [{
"age": {
"order":"desc"
}
}]
}
12.多字段排序
假定我们想要结合使用 age 和 _score 进行查询,并且匹配的结果首先按照年龄排序,然后
按照相关性得分排序
在 Postman 中,向 ES 服务器发 GET 请求 :http://127.0.0.1:9200/student/_search
{
"query": {
"match_all": {}
},
"sort": [
{
"age": {
"order": "desc"
}
},
{
"_score":{
"order": "desc"
}
}
] }
按照相关性得分排序
在 Postman 中,向 ES 服务器发 GET 请求 :http://127.0.0.1:9200/student/_search
{
"query": {
"match_all": {}
},
"sort": [
{
"age": {
"order": "desc"
}
},
{
"_score":{
"order": "desc"
}
}
] }
13.高亮查询
在进行关键字搜索时,搜索出的内容中的关键字会显示不同的颜色,称之为高亮。
Elasticsearch 可以对查询内容中的关键字部分,进行标签和样式(高亮)的设置。
在使用 match 查询的同时,加上一个 highlight 属性:
pre_tags:前置标签
post_tags:后置标签
fields:需要高亮的字段
title:这里声明 title 字段需要高亮,后面可以为这个字段设置特有配置,也可以空
在 Postman 中,向 ES 服务器发 GET 请求 :http://127.0.0.1:9200/student/_search
{
"query": {
"match": {
"name": "zhangsan"
}
},
"highlight": {
"pre_tags": "<font color='red'>",
"post_tags": "</font>",
"fields": {
"name": {}
}
} }
在使用 match 查询的同时,加上一个 highlight 属性:
pre_tags:前置标签
post_tags:后置标签
fields:需要高亮的字段
title:这里声明 title 字段需要高亮,后面可以为这个字段设置特有配置,也可以空
在 Postman 中,向 ES 服务器发 GET 请求 :http://127.0.0.1:9200/student/_search
{
"query": {
"match": {
"name": "zhangsan"
}
},
"highlight": {
"pre_tags": "<font color='red'>",
"post_tags": "</font>",
"fields": {
"name": {}
}
} }
14.分页查询
from:当前页的起始索引,默认从 0 开始。 from = (pageNum - 1) * size
size:每页显示多少条
在 Postman 中,向 ES 服务器发 GET 请求 :http://127.0.0.1:9200/student/_search
{
"query": {
"match_all": {}
},
"sort": [
{
"age": {
"order": "desc"
}
}
],
"from": 0,
"size": 2
}
size:每页显示多少条
在 Postman 中,向 ES 服务器发 GET 请求 :http://127.0.0.1:9200/student/_search
{
"query": {
"match_all": {}
},
"sort": [
{
"age": {
"order": "desc"
}
}
],
"from": 0,
"size": 2
}
15.聚合查询
聚合允许使用者对 es 文档进行统计分析,类似与关系型数据库中的 group by,当然还有很
多其他的聚合,例如取最大值、平均值等等。
对某个字段取最大值 max
在 Postman 中,向 ES 服务器发 GET 请求 :http://127.0.0.1:9200/student/_search
多其他的聚合,例如取最大值、平均值等等。
对某个字段取最大值 max
在 Postman 中,向 ES 服务器发 GET 请求 :http://127.0.0.1:9200/student/_search
ES的master选举流程
1.选主是ZenDiscovery模块负责,主要包含Ping和Unicast这两部分。
2.对所有可以成为master的节点(node.master:true)根据nodeId字典排序,每次选举每个节点都把自己所知道节点排一次序,然后选出第一个节点,暂且认为它是master节点。
3.如果某个节点的投票数达到一定的值(可以成为master节点数n/2+1)并且该节点自己也选举自己,那这个节点就是master.否则重新选举直到满足上述条件。
4.mast节点的职责主要包括集群,节点和索引的管理,不负责文档级别的管理;data节点可以关闭http功能。
2.对所有可以成为master的节点(node.master:true)根据nodeId字典排序,每次选举每个节点都把自己所知道节点排一次序,然后选出第一个节点,暂且认为它是master节点。
3.如果某个节点的投票数达到一定的值(可以成为master节点数n/2+1)并且该节点自己也选举自己,那这个节点就是master.否则重新选举直到满足上述条件。
4.mast节点的职责主要包括集群,节点和索引的管理,不负责文档级别的管理;data节点可以关闭http功能。
ES集群脑裂问题
**成因**
1.网络原因:集群间的网络延迟导致一些节点访问不到master,认为master挂掉了从而选举出新的master,并对master上的分片和副本标红,分配新的主分片。
2.节点负载:主节点的角色即为master又为data,访问量较大时可能会导致ES停止响应造成大面积延迟,此时其他节点得不到主节点的响应认为主节点挂掉了,会重新选取主节点。
3.内存回收:data节点上的es进程占用的内存较大,引发JVM的大规模内存回收,造成ES进程失去响应。
**解决方案**
1。减少误判:discover.zen.ping_timeout节点状态的响应时间默认为3s,可适当调大。
2.选举触发:discover.zen.minimum_master_nodes:1该参数是用于控制选举行为发生的最小集群主节点数量。当备选主节点的个数大于等于该参数值,且备选主节点中有该参数个节点认为主节点挂了,进行选举。官方建议(n/2)+1,n为主节点个数。
3.角色分离:即master节点与data节点分离,限制角色
主节点配置:node.master:true node.data:false;
从节点配置:
node.master:false
node.data:true;
1.网络原因:集群间的网络延迟导致一些节点访问不到master,认为master挂掉了从而选举出新的master,并对master上的分片和副本标红,分配新的主分片。
2.节点负载:主节点的角色即为master又为data,访问量较大时可能会导致ES停止响应造成大面积延迟,此时其他节点得不到主节点的响应认为主节点挂掉了,会重新选取主节点。
3.内存回收:data节点上的es进程占用的内存较大,引发JVM的大规模内存回收,造成ES进程失去响应。
**解决方案**
1。减少误判:discover.zen.ping_timeout节点状态的响应时间默认为3s,可适当调大。
2.选举触发:discover.zen.minimum_master_nodes:1该参数是用于控制选举行为发生的最小集群主节点数量。当备选主节点的个数大于等于该参数值,且备选主节点中有该参数个节点认为主节点挂了,进行选举。官方建议(n/2)+1,n为主节点个数。
3.角色分离:即master节点与data节点分离,限制角色
主节点配置:node.master:true node.data:false;
从节点配置:
node.master:false
node.data:true;
ES索引文档的流程
1.协调节点默认使用文档ID参与计算(也支持routing),以便为路由提供合适的分片:shard=hash(document_id)%(num_of_primary_shards)
2.当分片所在的节点接收到来自协调节点的请求后,会请求写入到Memory Buffer,然后定时(默认1s)写入到Filesystem Cache,这个从过程叫**refresh**;
3.在某些情况下,存在Momery Buffer和Filesystem Cache的数据可能会丢失,ES通过translog的机制来保证数据的可靠性,其实现机制就是接收到请求后,同时也会写入到translog中,当Filesystem Cache中的数据写入到磁盘中时,才会清除掉,这个过程叫做**flush**;
4.在flush过程中,内存中的缓冲将被清除,内容被写入一个新段,段的fsync将创建一个新的提交点,并将内容刷新到磁盘,旧的translog将被删除并开始一个新的translog.
5.flush触发的时机是定时触发(默认30分钟)或者translog变得太大(默认512M)时。
2.当分片所在的节点接收到来自协调节点的请求后,会请求写入到Memory Buffer,然后定时(默认1s)写入到Filesystem Cache,这个从过程叫**refresh**;
3.在某些情况下,存在Momery Buffer和Filesystem Cache的数据可能会丢失,ES通过translog的机制来保证数据的可靠性,其实现机制就是接收到请求后,同时也会写入到translog中,当Filesystem Cache中的数据写入到磁盘中时,才会清除掉,这个过程叫做**flush**;
4.在flush过程中,内存中的缓冲将被清除,内容被写入一个新段,段的fsync将创建一个新的提交点,并将内容刷新到磁盘,旧的translog将被删除并开始一个新的translog.
5.flush触发的时机是定时触发(默认30分钟)或者translog变得太大(默认512M)时。
ES跟新和删除文档的流程
1.删除和更新也都是写操作,但是 Elasticsearch 中的文档是不可变的,因此不能被删除或者改动以展示其变更;
2.磁盘上的每个段都有一个相应的.del 文件。当删除请求发送后,文档并没有真的被删除,而是在.del文件中被标记为删除。该文档依然能匹配查询,但是会在结果中被过滤掉。当段合并时,在.del 文件中被标记为删除的文档将不会被写入新段。
3.在新的文档被创建时,Elasticsearch 会为该文档指定一个版本号,当执行更新时,旧版本的文档在.del文件中被标记为删除,新版本的文档被索引到一个新段。旧版本的文档依然能匹配查询,但是会在结果中被过滤掉。
2.磁盘上的每个段都有一个相应的.del 文件。当删除请求发送后,文档并没有真的被删除,而是在.del文件中被标记为删除。该文档依然能匹配查询,但是会在结果中被过滤掉。当段合并时,在.del 文件中被标记为删除的文档将不会被写入新段。
3.在新的文档被创建时,Elasticsearch 会为该文档指定一个版本号,当执行更新时,旧版本的文档在.del文件中被标记为删除,新版本的文档被索引到一个新段。旧版本的文档依然能匹配查询,但是会在结果中被过滤掉。
ES搜索的流程
1.搜索被执行成一个两阶段过程,我们称之为 Query Then Fetch;
2. 在初始查询阶段时,查询会广播到索引中每一个分片拷贝(主分片或者副本分片)。 每个分片在本地执行搜索并构建一个匹配文档的大小为 from + size 的优先队列。PS:在搜索的时候是会查询Filesystem Cache 的,但是有部分数据还在 Memory Buffer,所以搜索是近实时的。
3.每个分片返回各自优先队列中 所有文档的 ID 和排序值 给协调节点,它合并这些值到自己的优先队列中来产生一个全局排序后的结果列表。 接下来就是取回阶段,协调节点辨别出哪些文档需要被取回并向相关的分片提交多个 GET 请求。每个分片加载并丰富文档,如果有需要的话,接着返回文档给协调节点。一旦所有的文档都被取回了,协调节点返回结果给客户端。
4.Query Then Fetch 的搜索类型在文档相关性打分的时候参考的是本分片的数据,这样在文档数量较少的时候可能不够准确,DFS Query Then Fetch 增加了一个预查询的处理,询问 Term 和 Document frequency,这个评分更准确,但是性能会变差。
2. 在初始查询阶段时,查询会广播到索引中每一个分片拷贝(主分片或者副本分片)。 每个分片在本地执行搜索并构建一个匹配文档的大小为 from + size 的优先队列。PS:在搜索的时候是会查询Filesystem Cache 的,但是有部分数据还在 Memory Buffer,所以搜索是近实时的。
3.每个分片返回各自优先队列中 所有文档的 ID 和排序值 给协调节点,它合并这些值到自己的优先队列中来产生一个全局排序后的结果列表。 接下来就是取回阶段,协调节点辨别出哪些文档需要被取回并向相关的分片提交多个 GET 请求。每个分片加载并丰富文档,如果有需要的话,接着返回文档给协调节点。一旦所有的文档都被取回了,协调节点返回结果给客户端。
4.Query Then Fetch 的搜索类型在文档相关性打分的时候参考的是本分片的数据,这样在文档数量较少的时候可能不够准确,DFS Query Then Fetch 增加了一个预查询的处理,询问 Term 和 Document frequency,这个评分更准确,但是性能会变差。
ES在部署时,对Linux的设置有哪些优化方法
1.**64 GB** 内存的机器是非常理想的,但是 32 GB 和 16 GB 机器也是很常见的。少于 8 GB 会适得其反。
2.如果你要在更快的 CPUs 和更多的核心之间选择,选择**更多的核心**更好。多个内核提供的额外并发远胜过稍微快一点点的时钟频率。
3.如果你负担得起 **SSD**,它将远远超出任何旋转介质。 基于 SSD 的节点,查询和索引性能都有提升。
4.即使数据中心们近在咫尺,也要避免集群跨越多个数据中心。绝对要避免集群跨越大的地理距离。
5.请确保运行你应用程序的 JVM 和服务器的 JVM 是完全一样的。 Elasticsearch 的几个地方,使用 Java 的本地序列化。
6.通过设置 gateway.recover_after_nodes、gateway.expected_nodes、gateway.recover_after_time 可以在集群
重启的时候避免过多的分片交换,这可能会让数据恢复从数个小时缩短为几秒钟。
7.Elasticsearch 默认被配置为使用单播发现,以防止节点无意中加入集群。只有在同一台机器上运行的节点才会自动组成集群。最好使用单播代替组播。
8.不要随意修改垃圾回收器(CMS)和各个线程池的大小。
9.把你的内存的(少于)一半给 Lucene(但不要超过 32 GB!),通过 ES_HEAP_SIZE 环境变量设置。
10.内存交换到磁盘对服务器性能来说是致命的。如果内存交换到磁盘上,一个 100 微秒的操作可能变成 10 毫秒。 再想想那么多 10 微秒的操作时延累加起来。 不难看出 swapping 对于性能是多么可
怕。
11.Lucene 使用了大量的文件。同时,Elasticsearch 在节点和 HTTP 客户端之间进行通信也使用了大量的套接字。 所有这一切都需要足够的文件描述符。你应该增加你的文件描述符,设置一个很大的值,
如 64,000
**补充:索引阶段性能提升方法**
使用批量请求并调整其大小:每次批量数据 5–15 MB 大是个不错的起始点。
存储:使用 SSD
段和合并:Elasticsearch 默认值是 20 MB/s,对机械磁盘应该是个不错的设置。如果你用的是 SSD,可以考虑提高到 100–200 MB/s。如果你在做批量导入,完全不在意搜索,你可以彻底关掉合并限流。
另外还可以增加 index.translog.flush_threshold_size 设置,从默认的 512 MB 到更大一些的值,比如 1 GB,这可以在一次清空触发的时候在事务日志里积累出更大的段。
如果你的搜索结果不需要近实时的准确度,考虑把每个索引的 index.refresh_interval 改到 30s。 如果你在做大批量导入,考虑通过设置 index.number_of_replicas: 0 关闭副本。
2.如果你要在更快的 CPUs 和更多的核心之间选择,选择**更多的核心**更好。多个内核提供的额外并发远胜过稍微快一点点的时钟频率。
3.如果你负担得起 **SSD**,它将远远超出任何旋转介质。 基于 SSD 的节点,查询和索引性能都有提升。
4.即使数据中心们近在咫尺,也要避免集群跨越多个数据中心。绝对要避免集群跨越大的地理距离。
5.请确保运行你应用程序的 JVM 和服务器的 JVM 是完全一样的。 Elasticsearch 的几个地方,使用 Java 的本地序列化。
6.通过设置 gateway.recover_after_nodes、gateway.expected_nodes、gateway.recover_after_time 可以在集群
重启的时候避免过多的分片交换,这可能会让数据恢复从数个小时缩短为几秒钟。
7.Elasticsearch 默认被配置为使用单播发现,以防止节点无意中加入集群。只有在同一台机器上运行的节点才会自动组成集群。最好使用单播代替组播。
8.不要随意修改垃圾回收器(CMS)和各个线程池的大小。
9.把你的内存的(少于)一半给 Lucene(但不要超过 32 GB!),通过 ES_HEAP_SIZE 环境变量设置。
10.内存交换到磁盘对服务器性能来说是致命的。如果内存交换到磁盘上,一个 100 微秒的操作可能变成 10 毫秒。 再想想那么多 10 微秒的操作时延累加起来。 不难看出 swapping 对于性能是多么可
怕。
11.Lucene 使用了大量的文件。同时,Elasticsearch 在节点和 HTTP 客户端之间进行通信也使用了大量的套接字。 所有这一切都需要足够的文件描述符。你应该增加你的文件描述符,设置一个很大的值,
如 64,000
**补充:索引阶段性能提升方法**
使用批量请求并调整其大小:每次批量数据 5–15 MB 大是个不错的起始点。
存储:使用 SSD
段和合并:Elasticsearch 默认值是 20 MB/s,对机械磁盘应该是个不错的设置。如果你用的是 SSD,可以考虑提高到 100–200 MB/s。如果你在做批量导入,完全不在意搜索,你可以彻底关掉合并限流。
另外还可以增加 index.translog.flush_threshold_size 设置,从默认的 512 MB 到更大一些的值,比如 1 GB,这可以在一次清空触发的时候在事务日志里积累出更大的段。
如果你的搜索结果不需要近实时的准确度,考虑把每个索引的 index.refresh_interval 改到 30s。 如果你在做大批量导入,考虑通过设置 index.number_of_replicas: 0 关闭副本。
GC方面,在使用ES时要注意什么?
1.倒排词典的索引需要常驻内存,无法 GC,需要监控 data node 上 segment memory 增长趋势。
2.各类缓存,field cache, filter cache, indexing cache, bulk queue 等等,要设置合理的大小,并且要应该根
据最坏的情况来看 heap 是否够用,也就是各类缓存全部占满的时候,还有 heap 空间可以分配给其他任务吗?避免采用 clear cache 等“自欺欺人”的方式来释放内存。
3.避免返回大量结果集的搜索与聚合。确实需要大量拉取数据的场景,可以采用 scan & scroll api 来实现。
4.cluster stats 驻留内存并无法水平扩展,超大规模集群可以考虑分拆成多个集群通过 tribe node 连接。
6.想知道 heap 够不够,必须结合实际应用场景,并对集群的 heap 使用情况做持续的监控。
2.各类缓存,field cache, filter cache, indexing cache, bulk queue 等等,要设置合理的大小,并且要应该根
据最坏的情况来看 heap 是否够用,也就是各类缓存全部占满的时候,还有 heap 空间可以分配给其他任务吗?避免采用 clear cache 等“自欺欺人”的方式来释放内存。
3.避免返回大量结果集的搜索与聚合。确实需要大量拉取数据的场景,可以采用 scan & scroll api 来实现。
4.cluster stats 驻留内存并无法水平扩展,超大规模集群可以考虑分拆成多个集群通过 tribe node 连接。
6.想知道 heap 够不够,必须结合实际应用场景,并对集群的 heap 使用情况做持续的监控。
ES对大数据量(上亿量级)的聚合如何实现?
Elasticsearch 提供的首个近似聚合是 cardinality 度量。它提供一个字段的基数,即该字段的 distinct或者 unique 值的数目。它是基于 HLL 算法的。HLL 会先对我们的输入作哈希运算,然后根据哈希运算的
结果中的 bits 做概率估算从而得到基数。其特点是:可配置的精度,用来控制内存的使用(更精确 = 更多内存);小的数据集精度是非常高的;我们可以通过配置参数,来设置去重需要的固定内存使用量。无
论数千还是数十亿的唯一值,内存使用量只与你配置的精确度相关
结果中的 bits 做概率估算从而得到基数。其特点是:可配置的精度,用来控制内存的使用(更精确 = 更多内存);小的数据集精度是非常高的;我们可以通过配置参数,来设置去重需要的固定内存使用量。无
论数千还是数十亿的唯一值,内存使用量只与你配置的精确度相关
在并发情况下,ES如何保证读写一致?
可以通过版本号使用乐观并发控制,以确保新版本不会被旧版本覆盖,由应用层来处理具体的冲突;
另外对于写操作,一致性级别支持 quorum/one/all,默认为 quorum,即只有当大多数分片可用时才允许写操作。但即使大多数可用,也可能存在因为网络等原因导致写入副本失败,这样该副本被认为故
障,分片将会在一个不同的节点上重建。
对于读操作,可以设置 replication 为 sync(默认),这使得操作在主分片和副本分片都完成后才会返回;
如果设置 replication 为 async 时,也可以通过设置搜索请求参数_preference 为 primary 来查询主分片,
确保文档是最新版本。
另外对于写操作,一致性级别支持 quorum/one/all,默认为 quorum,即只有当大多数分片可用时才允许写操作。但即使大多数可用,也可能存在因为网络等原因导致写入副本失败,这样该副本被认为故
障,分片将会在一个不同的节点上重建。
对于读操作,可以设置 replication 为 sync(默认),这使得操作在主分片和副本分片都完成后才会返回;
如果设置 replication 为 async 时,也可以通过设置搜索请求参数_preference 为 primary 来查询主分片,
确保文档是最新版本。
如何监控ES集群状态?
elasticsearch-head 插件
通过 Kibana 监控 Elasticsearch。你可以实时查看你的集群健康状态和性能,也可以分析过去的集群、
索引和节点指标
通过 Kibana 监控 Elasticsearch。你可以实时查看你的集群健康状态和性能,也可以分析过去的集群、
索引和节点指标
是否了解字典树?
字典树又称单词查找树,Trie 树,是一种树形结构,是一种哈希树的变种。典型应用是用于统计,排序和保存大量的字符串(但不仅限于字符串),所以经常被搜索引擎系统用于文本词频统计。
它的优点是:
利用字符串的公共前缀来减少查询时间,最大限度地减少无谓的字符串比较,查询效率比哈希树高。
Trie 的核心思想是空间换时间,利用字符串的公共前缀来降低查询时间的开销以达到提高效率的目的。
它有 3 个基本性质: 根节点不包含字符,除根节点外每一个节点都只包含一个字符。
从根节点到某一节点,路径上经过的字符连接起来,为该节点对应的字符串。
每个节点的所有子节点包含的字符都不相同。
对于中文的字典树,每个节点的子节点用一个哈希表存储,这样就不用浪费太大的空间,而且查询速度上
可以保留哈希的复杂度 O(1)。
它的优点是:
利用字符串的公共前缀来减少查询时间,最大限度地减少无谓的字符串比较,查询效率比哈希树高。
Trie 的核心思想是空间换时间,利用字符串的公共前缀来降低查询时间的开销以达到提高效率的目的。
它有 3 个基本性质: 根节点不包含字符,除根节点外每一个节点都只包含一个字符。
从根节点到某一节点,路径上经过的字符连接起来,为该节点对应的字符串。
每个节点的所有子节点包含的字符都不相同。
对于中文的字典树,每个节点的子节点用一个哈希表存储,这样就不用浪费太大的空间,而且查询速度上
可以保留哈希的复杂度 O(1)。
ES中的集群,节点,索引,文档,类型是什么?
1.集群是一个或多个节点(服务器)的集合,它们共同保存您的整个数据,并提供跨所有节点的联合索引和搜索功能。群集由唯一名称标识,默认情况下为“elasticsearch”。此名称很重要,因为如果节点设置为按名称加入群集,则该节点只能是群集的一部分。
2.节点是属于集群一部分的单个服务器。它存储数据并参与群集索引和搜索功能。
3.索引就像关系数据库中的“数据库”。它有一个定义多种类型的映射。索引是逻辑名称空间,映射到一个或多个主分片,并且可以有零个或多个副本分片。 MySQL =>数据库 Elasticsearch =>索引
4.文档类似于关系数据库中的一行。不同之处在于索引中的每个文档可以具有不同的结构(字段),但
是对于通用字段应该具有相同的数据类型。 MySQL => Databases => Tables => Columns / Rows
Elasticsearch => Indices => Types =>具有属性的文档
类型是索引的逻辑类别/分区,其语义完全取决于用户。
2.节点是属于集群一部分的单个服务器。它存储数据并参与群集索引和搜索功能。
3.索引就像关系数据库中的“数据库”。它有一个定义多种类型的映射。索引是逻辑名称空间,映射到一个或多个主分片,并且可以有零个或多个副本分片。 MySQL =>数据库 Elasticsearch =>索引
4.文档类似于关系数据库中的一行。不同之处在于索引中的每个文档可以具有不同的结构(字段),但
是对于通用字段应该具有相同的数据类型。 MySQL => Databases => Tables => Columns / Rows
Elasticsearch => Indices => Types =>具有属性的文档
类型是索引的逻辑类别/分区,其语义完全取决于用户。
ES中的倒排索引是什么?
倒排索引是搜索引擎的核心。搜索引擎的主要目标是在查找发生搜索条件的文档时提供快速搜索。ES中的倒排索引其实就是 lucene 的倒排索引,区别于传统的正向索引,倒排索引会再存储数据时将关键词和数据进行关联,保存到倒排表中,然后查询时,将查询内容进行分词后在倒排表中进行查询,最后匹配数据即可。
Elasticsearch 中的集群、节点、索引、文档、类型是什么?
集群是一个或多个节点(服务器)的集合,它们共同保存您的整个数据,并提供跨所有节点的联合索
引和搜索功能。群集由唯一名称标识,默认情况下为“elasticsearch”。此名称很重要,因为如果节点设
置为按名称加入群集,则该节点只能是群集的一部分。
引和搜索功能。群集由唯一名称标识,默认情况下为“elasticsearch”。此名称很重要,因为如果节点设
置为按名称加入群集,则该节点只能是群集的一部分。
节点是属于集群一部分的单个服务器。它存储数据并参与群集索引和搜索功能。
索引就像关系数据库中的“数据库”。它有一个定义多种类型的映射。索引是逻辑名称空间,映射到一
个或多个主分片,并且可以有零个或多个副本分片。 MySQL =>数据库 Elasticsearch =>索引
个或多个主分片,并且可以有零个或多个副本分片。 MySQL =>数据库 Elasticsearch =>索引
文档类似于关系数据库中的一行。不同之处在于索引中的每个文档可以具有不同的结构(字段),但
是对于通用字段应该具有相同的数据类型。 MySQL => Databases => Tables => Columns / Rows
Elasticsearch => Indices => Types =>具有属性的文档
是对于通用字段应该具有相同的数据类型。 MySQL => Databases => Tables => Columns / Rows
Elasticsearch => Indices => Types =>具有属性的文档
类型是索引的逻辑类别/分区,其语义完全取决于用户。
redis
什么是 redis?它能做什么?
redis: redis 即 Remote Dictionary Server,用中文翻译过来可以理解为远程数据服务或远程字典服务。其是使用 C 语言的编写的key-value存储系统
应用场景:缓存,数据库,消息队列,分布式锁,点赞列表,排行榜等等
应用场景:缓存,数据库,消息队列,分布式锁,点赞列表,排行榜等等
redis 有哪八种数据类型?有哪些应用场景?
redis 总共有八种数据结构,五种基本数据类型和三种特殊数据类型。
字符串:redis没有直接使⽤C语⾔传统的字符串表示,⽽是⾃⼰实现的叫做简单动态字符串SDS的
抽象类型。C语⾔的字符串不记录⾃身的⻓度信息,⽽SDS则保存了⻓度信息,这样将获取字符串
⻓度的时间由O(N)降低到了O(1),同时可以避免缓冲区溢出和减少修改字符串⻓度时所需的内存重
分配次数。
抽象类型。C语⾔的字符串不记录⾃身的⻓度信息,⽽SDS则保存了⻓度信息,这样将获取字符串
⻓度的时间由O(N)降低到了O(1),同时可以避免缓冲区溢出和减少修改字符串⻓度时所需的内存重
分配次数。
链表linkedlist:redis链表是⼀个双向⽆环链表结构,很多发布订阅、慢查询、监视器功能都是使
⽤到了链表来实现,每个链表的节点由⼀个listNode结构来表示,每个节点都有指向前置节点和后
置节点的指针,同时表头节点的前置和后置节点都指向NULL。
⽤到了链表来实现,每个链表的节点由⼀个listNode结构来表示,每个节点都有指向前置节点和后
置节点的指针,同时表头节点的前置和后置节点都指向NULL。
字典hashtable:⽤于保存键值对的抽象数据结构。redis使⽤hash表作为底层实现,每个字典带有
两个hash表,供平时使⽤和rehash时使⽤,hash表使⽤链地址法来解决键冲突,被分配到同⼀个
索引位置的多个键值对会形成⼀个单向链表,在对hash表进⾏扩容或者缩容的时候,为了服务的可
⽤性,rehash的过程不是⼀次性完成的,⽽是渐进式的。
两个hash表,供平时使⽤和rehash时使⽤,hash表使⽤链地址法来解决键冲突,被分配到同⼀个
索引位置的多个键值对会形成⼀个单向链表,在对hash表进⾏扩容或者缩容的时候,为了服务的可
⽤性,rehash的过程不是⼀次性完成的,⽽是渐进式的。
跳跃表skiplist:跳跃表是有序集合的底层实现之⼀,redis中在实现有序集合键和集群节点的内部
结构中都是⽤到了跳跃表。redis跳跃表由zskiplist和zskiplistNode组成,zskiplist⽤于保存跳跃表
信息(表头、表尾节点、⻓度等),zskiplistNode⽤于表示表跳跃节点,每个跳跃表的层⾼都是1-
32的随机数,在同⼀个跳跃表中,多个节点可以包含相同的分值,但是每个节点的成员对象必须是
唯⼀的,节点按照分值⼤⼩排序,如果分值相同,则按照成员对象的⼤⼩排序。
结构中都是⽤到了跳跃表。redis跳跃表由zskiplist和zskiplistNode组成,zskiplist⽤于保存跳跃表
信息(表头、表尾节点、⻓度等),zskiplistNode⽤于表示表跳跃节点,每个跳跃表的层⾼都是1-
32的随机数,在同⼀个跳跃表中,多个节点可以包含相同的分值,但是每个节点的成员对象必须是
唯⼀的,节点按照分值⼤⼩排序,如果分值相同,则按照成员对象的⼤⼩排序。
整数集合intset:⽤于保存整数值的集合抽象数据结构,不会出现重复元素,底层实现为数组。
压缩列表ziplist:压缩列表是为节约内存⽽开发的顺序性数据结构,他可以包含多个节点,每个节
点可以保存⼀个字节数组或者整数值。
点可以保存⼀个字节数组或者整数值。
基于这些基础的数据结构,redis封装了⾃⼰的对象系统,包含字符串对象string、列表对象list、哈希对
象hash、集合对象set、有序集合对象zset,每种对象都⽤到了⾄少⼀种基础的数据结构。
象hash、集合对象set、有序集合对象zset,每种对象都⽤到了⾄少⼀种基础的数据结构。
redis通过encoding属性设置对象的编码形式来提升灵活性和效率,基于不同的场景redis会⾃动做出优
化。不同对象的编码如下:
1. 字符串对象string:int整数、embstr编码的简单动态字符串、raw简单动态字符串
2. 列表对象list:ziplist、linkedlist
3. 哈希对象hash:ziplist、hashtable
4. 集合对象set:intset、hashtable
5. 有序集合对象zset:ziplist、skiplist
化。不同对象的编码如下:
1. 字符串对象string:int整数、embstr编码的简单动态字符串、raw简单动态字符串
2. 列表对象list:ziplist、linkedlist
3. 哈希对象hash:ziplist、hashtable
4. 集合对象set:intset、hashtable
5. 有序集合对象zset:ziplist、skiplist
五种基本数据类型:
1.string:字符串类型,常被用来存储计数器,粉丝数等,简单的分布式锁也会用到该类型
2.hashmap:key - value 形式的,value 是一个map
3.list:基本的数据类型,列表。在 Redis 中可以把 list 用作栈、队列、阻塞队列。
4.set:集合,不能有重复元素,可以做点赞,收藏等
5.zset:有序集合,不能有重复元素,有序集合中的每个元素都需要指定一个分数,根据分数对元素进行升序排序。可以做排行榜
1.string:字符串类型,常被用来存储计数器,粉丝数等,简单的分布式锁也会用到该类型
2.hashmap:key - value 形式的,value 是一个map
3.list:基本的数据类型,列表。在 Redis 中可以把 list 用作栈、队列、阻塞队列。
4.set:集合,不能有重复元素,可以做点赞,收藏等
5.zset:有序集合,不能有重复元素,有序集合中的每个元素都需要指定一个分数,根据分数对元素进行升序排序。可以做排行榜
三种特殊数据类型:
1.geospatial: Redis 在 3.2 推出 Geo 类型,该功能可以推算出地理位置信息,两地之间的距离。
2.hyperloglog:基数:数学上集合的元素个数,是不能重复的。这个数据结构常用于统计网站的 UV。
3.bitmap: bitmap 就是通过最小的单位 bit 来进行0或者1的设置,表示某个元素对应的值或者状态。一个 bit 的值,或者是0,或者是1;也就是说一个 bit 能存储的最多信息是2。bitmap 常用于统计用户信息比如活跃粉丝和不活跃粉丝、登录和未登录、是否打卡等。
1.geospatial: Redis 在 3.2 推出 Geo 类型,该功能可以推算出地理位置信息,两地之间的距离。
2.hyperloglog:基数:数学上集合的元素个数,是不能重复的。这个数据结构常用于统计网站的 UV。
3.bitmap: bitmap 就是通过最小的单位 bit 来进行0或者1的设置,表示某个元素对应的值或者状态。一个 bit 的值,或者是0,或者是1;也就是说一个 bit 能存储的最多信息是2。bitmap 常用于统计用户信息比如活跃粉丝和不活跃粉丝、登录和未登录、是否打卡等。
redis为什么这么快?
1:完全基于内存操作
2:使用单线程模型来处理客户端的请求,避免了上下文的切换
3:IO 多路复用机制
4:自身使用 C 语言编写,有很多优化机制,比如全局哈希表,动态字符串 sds
2:使用单线程模型来处理客户端的请求,避免了上下文的切换
3:IO 多路复用机制
4:自身使用 C 语言编写,有很多优化机制,比如全局哈希表,动态字符串 sds
这是IO模型的一种,即经典的Reactor设计模式,
I/O 多路复用,简单来说就是通过监测文件的读写事件再通知线程执行相关操作,保证 Redis 的非阻塞 I/O 能够顺利执行完成的机制。
多路指的是多个socket连接,
复用指的是复用一个线程。多路复用主要有三种技术:select,poll,epoll。
epoll是最新的也是目前最好的多路复用技术。采用多路 I/O 复用技术可以让单个线程高效的处理多个连接请求(尽量减少网络IO的时间消耗),且Redis在内存中操作数据的速度非常快(内存内的操作不会成为这里的性能瓶颈),主要以上两点造就了Redis具有很高的吞吐量。
I/O 多路复用,简单来说就是通过监测文件的读写事件再通知线程执行相关操作,保证 Redis 的非阻塞 I/O 能够顺利执行完成的机制。
多路指的是多个socket连接,
复用指的是复用一个线程。多路复用主要有三种技术:select,poll,epoll。
epoll是最新的也是目前最好的多路复用技术。采用多路 I/O 复用技术可以让单个线程高效的处理多个连接请求(尽量减少网络IO的时间消耗),且Redis在内存中操作数据的速度非常快(内存内的操作不会成为这里的性能瓶颈),主要以上两点造就了Redis具有很高的吞吐量。
Redis的线程模型?
Redis 内部使用文件事件处理器 file event handler ,这个文件事件处理器是单线程的,所以
Redis 才叫做单线程的模型。它采用 IO 多路复用机制同时监听多个 socket ,根据 socket 上的事
件来选择对应的事件处理器进行处理。
Redis 才叫做单线程的模型。它采用 IO 多路复用机制同时监听多个 socket ,根据 socket 上的事
件来选择对应的事件处理器进行处理。
文件事件处理器的结构包含 4 个部分:
1. 多个 socket 。 2. IO 多路复用程序。
3. 文件事件分派器。
4. 事件处理器(连接应答处理器、命令请求处理器、命令回复处理器)。
1. 多个 socket 。 2. IO 多路复用程序。
3. 文件事件分派器。
4. 事件处理器(连接应答处理器、命令请求处理器、命令回复处理器)。
多个 socket 可能会并发产生不同的操作,每个操作对应不同的文件事件,但是 IO 多路复用程序会
监听多个 socket,会将 socket 产生的事件放入队列中排队,事件分派器每次从队列中取出一个事
件,把该事件交给对应的事件处理器进行处理。
监听多个 socket,会将 socket 产生的事件放入队列中排队,事件分派器每次从队列中取出一个事
件,把该事件交给对应的事件处理器进行处理。
redis到底是单线程还是多线程?
Redis 6.0版本之前的单线程指的是其网络I/O和键值对读写是由一个线程完成的。
Redis6.0引入的多线程指的是网络请求过程采用了多线程,而键值对读写命令仍然是单线程 处理的,所以Redis依然是并发安全的。
听说 redis 6.0之后又使用了多线程,不会有线程安全的问题吗?
不会
其实 redis 还是使用单线程模型来处理客户端的请求,只是使用多线程来处理数据的读写和协议解析,执行命令还是使用单线程,所以是不会有线程安全的问题。
之所以加入了多线程因为 redis 的性能瓶颈在于网络IO而非CPU,使用多线程能提升IO读写的效率,从而整体提高redis的性能。
其实 redis 还是使用单线程模型来处理客户端的请求,只是使用多线程来处理数据的读写和协议解析,执行命令还是使用单线程,所以是不会有线程安全的问题。
之所以加入了多线程因为 redis 的性能瓶颈在于网络IO而非CPU,使用多线程能提升IO读写的效率,从而整体提高redis的性能。
redis 如何做持久化
redis持久化⽅案分为RDB和AOF两种。
AOF和RDB不同,AOF是通过保存redis服务器所执⾏的写命令来记录数据库状态的。
AOF通过追加、写⼊、同步三个步骤来实现持久化机制。
1. 当AOF持久化处于激活状态,服务器执⾏完写命令之后,写命令将会被追加append到aof_buf缓冲
区的末尾
2. 在服务器每结束⼀个事件循环之前,将会调⽤flushAppendOnlyFile函数决定是否要将aof_buf的内
容保存到AOF⽂件中,可以通过配置appendfsync来决定。
如果不设置,默认选项将会是everysec,因为always来说虽然最安全(只会丢失⼀次事件循环的写命
令),但是性能较差,⽽everysec模式只不过会可能丢失1秒钟的数据,⽽no模式的效率和everysec相
仿,但是会丢失上次同步AOF⽂件之后的所有写命令数据。
AOF通过追加、写⼊、同步三个步骤来实现持久化机制。
1. 当AOF持久化处于激活状态,服务器执⾏完写命令之后,写命令将会被追加append到aof_buf缓冲
区的末尾
2. 在服务器每结束⼀个事件循环之前,将会调⽤flushAppendOnlyFile函数决定是否要将aof_buf的内
容保存到AOF⽂件中,可以通过配置appendfsync来决定。
如果不设置,默认选项将会是everysec,因为always来说虽然最安全(只会丢失⼀次事件循环的写命
令),但是性能较差,⽽everysec模式只不过会可能丢失1秒钟的数据,⽽no模式的效率和everysec相
仿,但是会丢失上次同步AOF⽂件之后的所有写命令数据。
AOF重写:
1 # auto‐aof‐rewrite‐min‐size 64mb //aof文件至少要达到64M才会自动重写,文件太 小恢复速度本来就很快,重写的意义不大
2 # auto‐aof‐rewrite‐percentage 100 //aof文件自上一次重写后文件大小增长了100% 则再次触发重写
1 # auto‐aof‐rewrite‐min‐size 64mb //aof文件至少要达到64M才会自动重写,文件太 小恢复速度本来就很快,重写的意义不大
2 # auto‐aof‐rewrite‐percentage 100 //aof文件自上一次重写后文件大小增长了100% 则再次触发重写
RDB持久化可以⼿动执⾏也可以根据配置定期执⾏,它的作⽤是将某个时间点上的数据库状态保存到
RDB⽂件中,RDB⽂件是⼀个压缩的⼆进制⽂件,通过它可以还原某个时刻数据库的状态。由于RDB⽂
件是保存在硬盘上的,所以即使redis崩溃或者退出,只要RDB⽂件存在,就可以⽤它来恢复还原数据库
的状态。
可以通过SAVE或者BGSAVE来⽣成RDB⽂件。
SAVE命令会阻塞redis进程,直到RDB⽂件⽣成完毕,在进程阻塞期间,redis不能处理任何命令请求,
这显然是不合适的。
BGSAVE则是会fork出⼀个⼦进程,然后由⼦进程去负责⽣成RDB⽂件,⽗进程还可以继续处理命令请
求,不会阻塞进程。
RDB⽂件中,RDB⽂件是⼀个压缩的⼆进制⽂件,通过它可以还原某个时刻数据库的状态。由于RDB⽂
件是保存在硬盘上的,所以即使redis崩溃或者退出,只要RDB⽂件存在,就可以⽤它来恢复还原数据库
的状态。
可以通过SAVE或者BGSAVE来⽣成RDB⽂件。
SAVE命令会阻塞redis进程,直到RDB⽂件⽣成完毕,在进程阻塞期间,redis不能处理任何命令请求,
这显然是不合适的。
BGSAVE则是会fork出⼀个⼦进程,然后由⼦进程去负责⽣成RDB⽂件,⽗进程还可以继续处理命令请
求,不会阻塞进程。
知道什么时热key吗?热 key 问题怎么解决?
所谓热key问题就是,突然有⼏⼗万的请求去访问redis上的某个特定key,那么这样会造成流量过于集
中,达到物理⽹卡上限,从⽽导致这台redis的服务器宕机引发雪崩。
中,达到物理⽹卡上限,从⽽导致这台redis的服务器宕机引发雪崩。
解决方案:
可以将结果缓存到本地内存中
将热 key 分散到不同的服务器中
设置永不过期
可以将结果缓存到本地内存中
将热 key 分散到不同的服务器中
设置永不过期
缓存击穿、缓存穿透、缓存雪崩是什么?怎么解决呢?
缓存击穿:就是单个key并发访问过⾼,过期时导致所有请求直接打到db上,这个和热key的问题⽐
较类似,只是说的点在于过期导致请求全部打到DB上⽽已。
较类似,只是说的点在于过期导致请求全部打到DB上⽽已。
1. 加锁更新,⽐如请求查询A,发现缓存中没有,对A这个key加锁,同时去数据库查询数据,写⼊缓
存,再返回给⽤户,这样后⾯的请求就可以从缓存中拿到数据了。(分布式锁/sychronized单机锁)
2. 永不过期
存,再返回给⽤户,这样后⾯的请求就可以从缓存中拿到数据了。(分布式锁/sychronized单机锁)
2. 永不过期
缓存穿透:缓存穿透是指用户请求的数据在缓存中不存在并且在数据库中也不存在,导致用户每次请求该数据都要去数据库中查询一遍,然后返回空。
1.加锁(类似击穿)
2.返回空对象
2.返回空对象
缓存雪崩:当某⼀时刻发⽣⼤规模的缓存失效的情况,⽐如你的缓存服务宕机了,会有⼤量的请求进来直接打到DB
上,这样可能导致整个系统的崩溃,称为雪崩。雪崩和击穿、热key的问题不太⼀样的是,他是指⼤规模
的缓存都过期失效了。
上,这样可能导致整个系统的崩溃,称为雪崩。雪崩和击穿、热key的问题不太⼀样的是,他是指⼤规模
的缓存都过期失效了。
1. 针对不同key设置不同的过期时间,避免同时过期
2. 限流,如果redis宕机,可以限流,避免同时刻⼤量请求打崩DB
3. ⼆级缓存,同热key的⽅案。
2. 限流,如果redis宕机,可以限流,避免同时刻⼤量请求打崩DB
3. ⼆级缓存,同热key的⽅案。
Redis集群数据hash分片算法是怎么回事?
Redis Cluster 将所有数据划分为 16384 个 slots(槽位),每个节点负责其中一部分槽位。 槽位的信息存储于每个节点中。 当 Redis Cluster 的客户端来连接集群时,它也会得到一份集群的槽位配置信息并将其缓存 在客户端本地。这样当客户端要查找某个 key 时,可以根据槽位定位算法定位到目标节 点
槽位定位算法:
Cluster 默认会对 key 值使用 crc16 算法进行 hash 得到一个整数值,然后用这个整数值对 16384 进行取模来得到具体槽位。 HASH_SLOT = CRC16(key) mod 16384 再根据槽位值和Redis节点的对应关系就可以定位到key具体是落在哪个Redis节点上的。
Cluster 默认会对 key 值使用 crc16 算法进行 hash 得到一个整数值,然后用这个整数值对 16384 进行取模来得到具体槽位。 HASH_SLOT = CRC16(key) mod 16384 再根据槽位值和Redis节点的对应关系就可以定位到key具体是落在哪个Redis节点上的。
Redis集群网络抖动导致频繁主从切换怎么处理?
真实世界的机房网络往往并不是风平浪静的,它们经常会发生各种各样的小问题。比如网络 抖动就是非常常见的一种现象,突然之间部分连接变得不可访问,然后很快又恢复正常。 为解决这种问题,Redis Cluster 提供了一种选项clusternodetimeout,表示当某个 节点持续 timeout 的时间失联时,才可以认定该节点出现故障,需要进行主从切换。如果没有这个选项,网络抖动会导致主从频繁切换 (数据的重新复制)
Redis集群为什么至少需要三个master节点?
因为新master的选举需要大于半数的集群master节点同意才能选举成功,如果只有两个 master节点,当其中一个挂了,是达不到选举新master的条件的。
Redis集群为什么推荐奇数个节点?
因为新master的选举需要大于半数的集群master节点同意才能选举成功,奇数个master节 点可以在满足选举该条件的基础上节省一个节点,比如三个master节点和四个master节点 的集群相比,大家如果都挂了一个master节点都能选举新master节点,如果都挂了两个 master节点都没法选举新master节点了,所以奇数的master节点更多的是从节省机器资源 角度出发说的。
Redis集群支持批量操作命令吗?
对于类似mset,mget这样的多个key的原生批量操作命令,redis集群只支持所有key落在 同一slot的情况,如果有多个key一定要用mset命令在redis集群上操作,则可以在key的前 面加上{XXX},这样参数数据分片hash计算的只会是大括号里的值,这样能确保不同的key 能落到同一slot里去,示例如下: 1 mset {user1}:1:name zhuge {user1}:1:age 18 假设name和age计算的hash slot值不一样,但是这条命令在集群下执行,redis只会用大括 号里的 user1 做hash slot计算,所以算出来的slot值肯定相同,最后都能落在同一slot。
Lua脚本能在Redis集群里执行吗?
Redis官方规定Lua脚本如果想在Redis集群里执行,需要Lua脚本里操作的所有Redis Key 落在集群的同一个节点上,这种的话我们可以给Lua脚本的Key前面加一个相同的hash tag,就是{XXX},这样就能保证Lua脚本里所有Key落在相同的节点上了。
主从同步原理是怎样的?
1.当一个从数据库启动时,它会向主数据库发送一个SYNC命令,master收到后,在后台保存快照,也就是我们说的RDB持久化,当然保存快照是需要消耗时间的,并且redis是单线程的,在保存快照期间redis收到的命令会缓存起来
2.快照完成后会将缓存的命令以及快照一起打包发给slave节点,从而保证主从数据库的一致性。
3.从数据库接受到快照以及缓存的命令后会将这部分数据写入到硬盘上的临时文件当中,写入完成后会用这份文件去替换掉RDB快照文件,当然,这个操作是不会阻塞的,可以继续接收命令执行,具体原因其实就是fork了一个子进程,用子进程去完成了这些功能。
2.快照完成后会将缓存的命令以及快照一起打包发给slave节点,从而保证主从数据库的一致性。
3.从数据库接受到快照以及缓存的命令后会将这部分数据写入到硬盘上的临时文件当中,写入完成后会用这份文件去替换掉RDB快照文件,当然,这个操作是不会阻塞的,可以继续接收命令执行,具体原因其实就是fork了一个子进程,用子进程去完成了这些功能。
因为不会阻塞,所以,这部分初始化完成后,当主数据库执行了改变数据的命令后,会异步的给slave,这也就是我们说的复制同步阶段,这个阶段会贯穿在整个主从同步的过程中,直到主从同步结束后,复制同步才会终止。
Redis主从复制风暴是怎么回事?
如果Redis主节点有很多从节点,在某一时刻如果所有从节点都同时连接主节点,那么主节 点会同时把内存快照RDB发给多个从节点,这样会导致Redis主节点压力非常大,这就是所 谓的Redis主从复制风暴问题。
Redis 常见性能问题和解决方案有哪些?
Master 最好不要做任何持久化工作,如 RDB 内存快照和 AOF 日志文件;
如果数据比较重要,某个 Slave 开启 AOF 备份数据,策略设置为每秒同步一次;
为了主从复制的速度和连接的稳定性,Master 和 Slave 最好在同一个局域网内;
尽量避免在压力很大的主库上增加从库;
主从复制不要用图状结构,用单向链表结构更为稳定,即:Master <- Slave1 <- Slave2 <-
Slave3….;这样的结构方便解决单点故障问题,实现 Slave 对 Master 的替换。如果 Master 挂
了,可以立刻启用 Slave1 做 Master,其他不变。
如果数据比较重要,某个 Slave 开启 AOF 备份数据,策略设置为每秒同步一次;
为了主从复制的速度和连接的稳定性,Master 和 Slave 最好在同一个局域网内;
尽量避免在压力很大的主库上增加从库;
主从复制不要用图状结构,用单向链表结构更为稳定,即:Master <- Slave1 <- Slave2 <-
Slave3….;这样的结构方便解决单点故障问题,实现 Slave 对 Master 的替换。如果 Master 挂
了,可以立刻启用 Slave1 做 Master,其他不变。
如何保证Redis和数据库数据一致性?
想要保证缓存与数据库的双写一致,一共有4种方式,即4种同步策略:
先更新缓存,再更新数据库;
先更新数据库,再更新缓存;
先删除缓存,再更新数据库;
先更新数据库,再删除缓存。
第一种:先删除缓存,再更新数据库
在出现失败时可能出现的问题:
1:线程A删除缓存成功,线程A更新数据库失败;
2 :线程B从缓存中读取数据;由于缓存被删,进程B无法从缓存中得到数据,进而从数据库读取数据;此时数据库中的数据更新失败,线程B从数据库成功获取旧的数据,然后将数据更新到了缓存。
最终,缓存和数据库的数据是一致的,但仍然是旧的数据。
第二种:先更新数据库,再删除缓存
假设这会有两个请求,一个请求A做查询操作,一个请求B做更新操作,那么会有如下情形产生
(1)缓存刚好失效
(2)请求A查询数据库,得一个旧值
(3)请求B将新值写入数据库
(4)请求B删除缓存
(5)请求A将查到的旧值写入缓存
如果发生上述情况,确实是会发生脏数据。
然而,发生这种情况的概率又有多少呢?
发生上述情况有一个先天性条件,就是步骤(3)的写数据库操作比步骤(2)的读数据库操作耗时更短,才有可能使得步骤(4)先于步骤(5)。
数据库的读操作的速度远快于写操作的(不然做读写分离干嘛,做读写分离的意义就是因为读操作比较快,耗资源少),因此步骤(3)耗时比步骤(2)更短,这一情形很难出现。
先更新数据库,再删缓存依然会有问题,不过,问题出现的可能性会因为上面说的原因,变得比较低。
第三种:给所有的缓存一个失效期
第三种方案可以说是一个大杀器,任何不一致,都可以靠失效期解决,失效期越短,数据一致性越高。但是失效期越短,查数据库就会越频繁。因此失效期应该根据业务来定。
1.并发不高的情况:
读: 读redis->没有,读mysql->把mysql数据写回redis,有的话直接从redis中取;
写: 写mysql->成功,再写redis;
2.并发高的情况:
读: 读redis->没有,读mysql->把mysql数据写回redis,有的话直接从redis中取;
写:异步话,先写入redis的缓存,就直接返回;定期或特定动作将数据保存到mysql,可以做到多次更新,一次保存;
第四种:加锁,使线程顺序执行
如果一个服务部署到了多个机器,就变成了分布式锁,或者是分布式队列按顺序去操作数据库或者 Redis,带来的副作用就是:数据库本来是并发的,现在变成串行的了,加锁或者排队执行的方案降低了系统性能,所以这个方案看起来不太可行。
第五种:采用双删
先删除缓存,再更新数据库,当更新数据后休眠一段时间再删除一次缓存。
方案推荐两种:
1:项目整合quartz等定时任务框架,去实现延时3–5s再去执行最后一步任务 。(推荐使用)
2:创建线程池,线程池中拿一个线程,线程体中延时3-5s再去执行最后一步任务(不能忘了启动线程)
第六种:异步更新缓存(基于订阅binlog的同步机制)
MySQL binlog增量订阅消费+消息队列+增量数据更新到redis读Redis
热数据基本都在Redis写MySQL:增删改都是操作MySQL更新Redis数据:MySQ的数据操作binlog,来更新到Redis:
1)数据操作主要分为两大块:一个是全量(将全部数据一次写入到redis)一个是增量(实时更新)。
这里说的是增量,指的是mysql的update、insert、delate变更数据。
2)读取binlog后分析 ,利用消息队列,推送更新各台的redis缓存数据。
这样一旦MySQL中产生了新的写入、更新、删除等操作,就可以把binlog相关的消息推送至Redis,Redis再根据binlog中的记录,对Redis进行更新。
其实这种机制,很类似MySQL的主从备份机制,因为MySQL的主备也是通过binlog来实现的数据一致性。
这里可以结合使用canal(阿里的一款开源框架),通过该框架可以对MySQL的binlog进行订阅,而canal正是模仿了mysql的slave数据库的备份请求,使得Redis的数据更新达到了相同的效果。
当然,这里的消息推送工具你也可以采用别的第三方:kafka、rabbitMQ等来实现推送更新Redis。
先更新缓存,再更新数据库;
先更新数据库,再更新缓存;
先删除缓存,再更新数据库;
先更新数据库,再删除缓存。
第一种:先删除缓存,再更新数据库
在出现失败时可能出现的问题:
1:线程A删除缓存成功,线程A更新数据库失败;
2 :线程B从缓存中读取数据;由于缓存被删,进程B无法从缓存中得到数据,进而从数据库读取数据;此时数据库中的数据更新失败,线程B从数据库成功获取旧的数据,然后将数据更新到了缓存。
最终,缓存和数据库的数据是一致的,但仍然是旧的数据。
第二种:先更新数据库,再删除缓存
假设这会有两个请求,一个请求A做查询操作,一个请求B做更新操作,那么会有如下情形产生
(1)缓存刚好失效
(2)请求A查询数据库,得一个旧值
(3)请求B将新值写入数据库
(4)请求B删除缓存
(5)请求A将查到的旧值写入缓存
如果发生上述情况,确实是会发生脏数据。
然而,发生这种情况的概率又有多少呢?
发生上述情况有一个先天性条件,就是步骤(3)的写数据库操作比步骤(2)的读数据库操作耗时更短,才有可能使得步骤(4)先于步骤(5)。
数据库的读操作的速度远快于写操作的(不然做读写分离干嘛,做读写分离的意义就是因为读操作比较快,耗资源少),因此步骤(3)耗时比步骤(2)更短,这一情形很难出现。
先更新数据库,再删缓存依然会有问题,不过,问题出现的可能性会因为上面说的原因,变得比较低。
第三种:给所有的缓存一个失效期
第三种方案可以说是一个大杀器,任何不一致,都可以靠失效期解决,失效期越短,数据一致性越高。但是失效期越短,查数据库就会越频繁。因此失效期应该根据业务来定。
1.并发不高的情况:
读: 读redis->没有,读mysql->把mysql数据写回redis,有的话直接从redis中取;
写: 写mysql->成功,再写redis;
2.并发高的情况:
读: 读redis->没有,读mysql->把mysql数据写回redis,有的话直接从redis中取;
写:异步话,先写入redis的缓存,就直接返回;定期或特定动作将数据保存到mysql,可以做到多次更新,一次保存;
第四种:加锁,使线程顺序执行
如果一个服务部署到了多个机器,就变成了分布式锁,或者是分布式队列按顺序去操作数据库或者 Redis,带来的副作用就是:数据库本来是并发的,现在变成串行的了,加锁或者排队执行的方案降低了系统性能,所以这个方案看起来不太可行。
第五种:采用双删
先删除缓存,再更新数据库,当更新数据后休眠一段时间再删除一次缓存。
方案推荐两种:
1:项目整合quartz等定时任务框架,去实现延时3–5s再去执行最后一步任务 。(推荐使用)
2:创建线程池,线程池中拿一个线程,线程体中延时3-5s再去执行最后一步任务(不能忘了启动线程)
第六种:异步更新缓存(基于订阅binlog的同步机制)
MySQL binlog增量订阅消费+消息队列+增量数据更新到redis读Redis
热数据基本都在Redis写MySQL:增删改都是操作MySQL更新Redis数据:MySQ的数据操作binlog,来更新到Redis:
1)数据操作主要分为两大块:一个是全量(将全部数据一次写入到redis)一个是增量(实时更新)。
这里说的是增量,指的是mysql的update、insert、delate变更数据。
2)读取binlog后分析 ,利用消息队列,推送更新各台的redis缓存数据。
这样一旦MySQL中产生了新的写入、更新、删除等操作,就可以把binlog相关的消息推送至Redis,Redis再根据binlog中的记录,对Redis进行更新。
其实这种机制,很类似MySQL的主从备份机制,因为MySQL的主备也是通过binlog来实现的数据一致性。
这里可以结合使用canal(阿里的一款开源框架),通过该框架可以对MySQL的binlog进行订阅,而canal正是模仿了mysql的slave数据库的备份请求,使得Redis的数据更新达到了相同的效果。
当然,这里的消息推送工具你也可以采用别的第三方:kafka、rabbitMQ等来实现推送更新Redis。
redis的数据结构?
总览:
SDS(简单动态字符串)
len,记录了字符串长度。这样获取字符串长度的时候,只需要返回这个成员变量值就行,时间复杂度只需要 O(1)。
alloc,分配给字符数组的空间长度。这样在修改字符串的时候,可以通过 alloc - len 计算出剩余的空间大小,可以用来判断空间是否满足修改需求,如果不满足的话,就会自动将 SDS 的空间扩展至执行修改所需的大小,然后才执行实际的修改操作,所以使用 SDS 既不需要手动修改 SDS 的空间大小,也不会出现前面所说的缓冲区溢出的问题。
flags,用来表示不同类型的 SDS。一共设计了 5 种类型,分别是 sdshdr5、sdshdr8、sdshdr16、sdshdr32 和 sdshdr64,后面在说明区别之处。
buf[],字符数组,用来保存实际数据。不仅可以保存字符串,也可以保存二进制数据。
优点:
O(1)复杂度获取字符串长度
二进制安全
因为 SDS 不需要用 “\0” 字符来标识字符串结尾了,而是有个专门的 len 成员变量来记录长度,所以可存储包含 “\0” 的数据。但是 SDS 为了兼容部分 C 语言标准库的函数, SDS 字符串结尾还是会加上 “\0” 字符。
因此, SDS 的 API 都是以处理二进制的方式来处理 SDS 存放在 buf[] 里的数据,程序不会对其中的数据做任何限制,数据写入的时候时什么样的,它被读取时就是什么样的。
通过使用二进制安全的 SDS,而不是 C 字符串,使得 Redis 不仅可以保存文本数据,也可以保存任意格式的二进制数据。
因此, SDS 的 API 都是以处理二进制的方式来处理 SDS 存放在 buf[] 里的数据,程序不会对其中的数据做任何限制,数据写入的时候时什么样的,它被读取时就是什么样的。
通过使用二进制安全的 SDS,而不是 C 字符串,使得 Redis 不仅可以保存文本数据,也可以保存任意格式的二进制数据。
不会发生缓冲区溢出
C 语言的字符串标准库提供的字符串操作函数,大多数(比如 strcat 追加字符串函数)都是不安全的,因为这些函数把缓冲区大小是否满足操作需求的工作交由开发者来保证,程序内部并不会判断缓冲区大小是否足够用,当发生了缓冲区溢出就有可能造成程序异常结束。
所以,Redis 的 SDS 结构里引入了 alloc 和 len 成员变量,这样 SDS API 通过 alloc - len 计算,可以算出剩余可用的空间大小,这样在对字符串做修改操作的时候,就可以由程序内部判断缓冲区大小是否足够用。
而且,当判断出缓冲区大小不够用时,Redis 会自动将扩大 SDS 的空间大小(小于 1MB 翻倍扩容,大于 1MB 按 1MB 扩容),以满足修改所需的大小。
在扩展 SDS 空间之前,SDS API 会优先检查未使用空间是否足够,如果不够的话,API 不仅会为 SDS 分配修改所必须要的空间,还会给 SDS 分配额外的「未使用空间」。
这样的好处是,下次在操作 SDS 时,如果 SDS 空间够的话,API 就会直接使用「未使用空间」,而无须执行内存分配,有效的减少内存分配次数。
所以,使用 SDS 即不需要手动修改 SDS 的空间大小,也不会出现缓冲区溢出的问题。
#节省内存空间
所以,Redis 的 SDS 结构里引入了 alloc 和 len 成员变量,这样 SDS API 通过 alloc - len 计算,可以算出剩余可用的空间大小,这样在对字符串做修改操作的时候,就可以由程序内部判断缓冲区大小是否足够用。
而且,当判断出缓冲区大小不够用时,Redis 会自动将扩大 SDS 的空间大小(小于 1MB 翻倍扩容,大于 1MB 按 1MB 扩容),以满足修改所需的大小。
在扩展 SDS 空间之前,SDS API 会优先检查未使用空间是否足够,如果不够的话,API 不仅会为 SDS 分配修改所必须要的空间,还会给 SDS 分配额外的「未使用空间」。
这样的好处是,下次在操作 SDS 时,如果 SDS 空间够的话,API 就会直接使用「未使用空间」,而无须执行内存分配,有效的减少内存分配次数。
所以,使用 SDS 即不需要手动修改 SDS 的空间大小,也不会出现缓冲区溢出的问题。
#节省内存空间
双向链表
优点:
listNode 链表节点的结构里带有 prev 和 next 指针,获取某个节点的前置节点或后置节点的时间复杂度只需O(1),而且这两个指针都可以指向 NULL,所以链表是无环链表;
list 结构因为提供了表头指针 head 和表尾节点 tail,所以获取链表的表头节点和表尾节点的时间复杂度只需O(1);
list 结构因为提供了链表节点数量 len,所以获取链表中的节点数量的时间复杂度只需O(1);
listNode 链表节使用 void* 指针保存节点值,并且可以通过 list 结构的 dup、free、match 函数指针为节点设置该节点类型特定的函数,因此链表节点可以保存各种不同类型的值;
list 结构因为提供了表头指针 head 和表尾节点 tail,所以获取链表的表头节点和表尾节点的时间复杂度只需O(1);
list 结构因为提供了链表节点数量 len,所以获取链表中的节点数量的时间复杂度只需O(1);
listNode 链表节使用 void* 指针保存节点值,并且可以通过 list 结构的 dup、free、match 函数指针为节点设置该节点类型特定的函数,因此链表节点可以保存各种不同类型的值;
缺点:
链表每个节点之间的内存都是不连续的,意味着无法很好利用 CPU 缓存。能很好利用 CPU 缓存的数据结构就是数组,因为数组的内存是连续的,这样就可以充分利用 CPU 缓存来加速访问。
还有一点,保存一个链表节点的值都需要一个链表节点结构头的分配,内存开销较大。
还有一点,保存一个链表节点的值都需要一个链表节点结构头的分配,内存开销较大。
未完。。。
redis是线程安全的吗?
Redis底层数据是如何用跳表来存储的?
跳表:将有序链表改造为支持近似“折半查找”算法,可以进行快速的插入、删除、查找操 作。
Redis持久化RDB、AOF、混合持久化是怎么回事?
Redis的过期键的删除策略有哪些?
定时删除:每个设置过期时间的key都需要创建一个定时器,到过期时间就会立即清除。该策略可以立即清除过期的数据,对内存很友好;但是会占用大量的CPU资源去处理过期的数据,从而影响缓存的响应时间和吞吐量。
惰性删除:只有当访问一个key时,才会判断该key是否已过期,过期则清除。该策略可以最大化地节省CPU资源,却对内存非常不友好。极端情况可能出现大量的过期key没有再次被访问,从而不会被清除,占用大量内存。
定期删除:默认每隔100ms,会扫描一定数量的数据库的expires字典中一定数量的key,并清除其中已过期的key。该策略是前两者的一个折中方案。通过调整定时扫描的时间间隔和每次扫描的限定耗时,可以在不同情况下使得CPU和内存资源达到最优的平衡效果。
Redis的内存满了怎么办?
实际上Redis定义了「8种内存淘汰策略」用来处理redis内存满的情况:
1.noeviction:直接返回错误,不淘汰任何已经存在的redis键
2.allkeys-lru:所有的键使用lru算法进行淘汰
3.volatile-lru:有过期时间的使用lru算法进行淘汰
4.allkeys-random:随机删除redis键
5.volatile-random:随机删除有过期时间的redis键
6.volatile-ttl:删除快过期的redis键
7.volatile-lfu:根据lfu算法从有过期时间的键删除
8.allkeys-lfu:根据lfu算法从所有键删除
1.noeviction:直接返回错误,不淘汰任何已经存在的redis键
2.allkeys-lru:所有的键使用lru算法进行淘汰
3.volatile-lru:有过期时间的使用lru算法进行淘汰
4.allkeys-random:随机删除redis键
5.volatile-random:随机删除有过期时间的redis键
6.volatile-ttl:删除快过期的redis键
7.volatile-lfu:根据lfu算法从有过期时间的键删除
8.allkeys-lfu:根据lfu算法从所有键删除
Redis淘汰Key的算法LRU与LFU区别?
LRU 算法(Least Recently Used,最近最少使用):淘汰很久没被访问过的数据,以最近一 次访问时间作为参考。
LFU 算法(Least Frequently Used,最不经常使用):淘汰最近一段时间被访问次数最少的 数据,以次数作为参考。
绝大多数情况我们都可以用LRU策略,当存在大量的热点缓存数据时,LFU可能更好点。
LFU 算法(Least Frequently Used,最不经常使用):淘汰最近一段时间被访问次数最少的 数据,以次数作为参考。
绝大多数情况我们都可以用LRU策略,当存在大量的热点缓存数据时,LFU可能更好点。
删除Key的命令会阻塞Redis吗?
有可能的,我们看下DEL Key命令的时间复杂度:
删除单个字符串类型的 key ,时间复杂度为 O(1)。
删除单个列表、集合、有序集合或哈希表类型的 key ,时间复杂度为 O(M), M 为以上数据结构内的元素数量。
删除单个字符串类型的 key ,时间复杂度为 O(1)。
删除单个列表、集合、有序集合或哈希表类型的 key ,时间复杂度为 O(M), M 为以上数据结构内的元素数量。
如果删除的是列表、集合、有序集合或哈希表类型的 key,如果集合元素过多,是会阻塞 Redis的。对于这种情况我们可以借助scan这样的命令循环删除元素。
如果删除的是字符串类型的 key,但是key对应value比较大,比如有几百M,那么也是会 阻塞Redis的。这种bigkey是我们要尽量减少出现的情况。
Redis 有哪些部署方式?
单机模式:这也是最基本的部署方式,只需要一台机器,负责读写,一般只用于开发人员自己测试
主从复制:在主从复制这种集群部署模式中,我们会将数据库分为两类,第一种称为主数据库(master),另一种称为从数据库(slave)。主数据库会负责我们整个系统中的读写操作,从数据库会负责我们整个数据库中的读操作。其中在职场开发中的真实情况是,我们会让主数据库只负责写操作,让从数据库只负责读操作,就是为了读写分离,减轻服务器的压力。
哨兵模式:哨兵模式是一种特殊的模式,首先Redis提供了哨兵的命令,哨兵是一个独立的进程,作为进程,它会独立运行。其原理是哨兵通过发送命令,等待Redis服务器响应,从而监控运行的多个Redis实例。它具备自动故障转移、集群监控、消息通知等功能。
cluster集群模式:在redis3.0版本中支持了cluster集群部署的方式,这种集群部署的方式能自动将数据进行分片,每个master上放一部分数据,提供了内置的高可用服务,即使某个master挂了,服务还可以正常地提供。
哨兵的作用和选举过程是怎么样的?
哨兵可以同时监视多个主从服务器,并且在被监视的master下线时,⾃动将某个slave提升为master,
然后由新的master继续接收命令
然后由新的master继续接收命令
整个过程如下:
1. 初始化sentinel,将普通的redis代码替换成sentinel专⽤代码
2. 初始化masters字典和服务器信息,服务器信息主要保存ip:port,并记录实例的地址和ID
3. 创建和master的两个连接,命令连接和订阅连接,并且订阅sentinel:hello频道
4. 每隔10秒向master发送info命令,获取master和它下⾯所有slave的当前信息
5. 当发现master有新的slave之后,sentinel和新的slave同样建⽴两个连接,同时每个10秒发送info
命令,更新master信息
6. sentinel每隔1秒向所有服务器发送ping命令,如果某台服务器在配置的响应时间内连续返回⽆效回
复,将会被标记为下线状态
7. 选举出领头sentinel,领头sentinel需要半数以上的sentinel同意
8. 领头sentinel从已下线的的master所有slave中挑选⼀个,将其转换为master
9. 让所有的slave改为从新的master复制数据
10. 将原来的master设置为新的master的从服务器,当原来master重新回复连接时,就变成了新
master的从服务器
sentinel会每隔1秒向所有实例(包括主从服务器和其他sentinel)发送ping命令,并且根据回复判断是
否已经下线,这种⽅式叫做主观下线。当判断为主观下线时,就会向其他监视的sentinel询问,如果超过
半数的投票认为已经是下线状态,则会标记为客观下线状态,同时触发故障转移。
1. 初始化sentinel,将普通的redis代码替换成sentinel专⽤代码
2. 初始化masters字典和服务器信息,服务器信息主要保存ip:port,并记录实例的地址和ID
3. 创建和master的两个连接,命令连接和订阅连接,并且订阅sentinel:hello频道
4. 每隔10秒向master发送info命令,获取master和它下⾯所有slave的当前信息
5. 当发现master有新的slave之后,sentinel和新的slave同样建⽴两个连接,同时每个10秒发送info
命令,更新master信息
6. sentinel每隔1秒向所有服务器发送ping命令,如果某台服务器在配置的响应时间内连续返回⽆效回
复,将会被标记为下线状态
7. 选举出领头sentinel,领头sentinel需要半数以上的sentinel同意
8. 领头sentinel从已下线的的master所有slave中挑选⼀个,将其转换为master
9. 让所有的slave改为从新的master复制数据
10. 将原来的master设置为新的master的从服务器,当原来master重新回复连接时,就变成了新
master的从服务器
sentinel会每隔1秒向所有实例(包括主从服务器和其他sentinel)发送ping命令,并且根据回复判断是
否已经下线,这种⽅式叫做主观下线。当判断为主观下线时,就会向其他监视的sentinel询问,如果超过
半数的投票认为已经是下线状态,则会标记为客观下线状态,同时触发故障转移。
cluster集群模式是怎么存放数据的?
一个cluster集群中总共有16384个节点,集群会将这16384个节点平均分配给每个节点
Redis执行命令竟然有死循环阻塞Bug?
如果你想随机查看 Redis 中的一个 key,Redis里有一个 RANDOMKEY 命令可以从 Redis 中随机取出一个 key,这个命令可能导致Redis死循环阻塞。
前面的面试题讲过Redis对于过期Key的清理策略是定时删除与惰性删除两种方式结合来做 的,而 RANDOMKEY 在随机拿出一个 key 后,首先会先检查这个 key 是否已过期,如果 该 key 已经过期,那么 Redis 会删除它,这个过程就是惰性删除。但清理完了还不能结 束,Redis 还要找出一个没过期的 key,返回给客户端。 此时,Redis 则会继续随机拿出一个 key,然后再判断它是否过期,直到找出一个没过期的 key 返回给客户端。 这里就有一个问题了,如果此时 Redis 中,有大量 key 已经过期,但还未来得及被清理 掉,那这个循环就会持续很久才能结束,而且,这个耗时都花费在了清理过期 key 以及寻 找不过期 key 上,导致的结果就是,RANDOMKEY 执行耗时变长,影响 Redis 性能。
以上流程,其实是在 master 上执行的。 如果在 slave 上执行 RANDOMEKY,那么问题会更严重。 slave 自己是不会清理过期 key,当一个 key 要过期时,master 会先清理删除它,之后 master 向 slave 发送一个 DEL 命令,告知 slave 也删除这个 key,以此达到主从库的数据
一致性。 假设Redis 中存在大量已过期还未被清理的 key,那在 slave 上执行 RANDOMKEY 时,就 会发生以下问题: 1、slave 随机取出一个 key,判断是否已过期。 2、key 已过期,但 slave 不会删除它,而是继续随机寻找不过期的 key。 3、由于大量 key 都已过期,那 slave 就会寻找不到符合条件的 key,此时就会陷入死循 环。也就是说,在 slave 上执行 RANDOMKEY,有可能会造成整个 Redis 实例卡死。 这其实是 Redis 的一个 Bug,这个 Bug 一直持续到 5.0 才被修复,修复的解决方案就是在 slave中最多找一定的次数,无论是否能找到,都会退出循环。
以上流程,其实是在 master 上执行的。 如果在 slave 上执行 RANDOMEKY,那么问题会更严重。 slave 自己是不会清理过期 key,当一个 key 要过期时,master 会先清理删除它,之后 master 向 slave 发送一个 DEL 命令,告知 slave 也删除这个 key,以此达到主从库的数据
一致性。 假设Redis 中存在大量已过期还未被清理的 key,那在 slave 上执行 RANDOMKEY 时,就 会发生以下问题: 1、slave 随机取出一个 key,判断是否已过期。 2、key 已过期,但 slave 不会删除它,而是继续随机寻找不过期的 key。 3、由于大量 key 都已过期,那 slave 就会寻找不到符合条件的 key,此时就会陷入死循 环。也就是说,在 slave 上执行 RANDOMKEY,有可能会造成整个 Redis 实例卡死。 这其实是 Redis 的一个 Bug,这个 Bug 一直持续到 5.0 才被修复,修复的解决方案就是在 slave中最多找一定的次数,无论是否能找到,都会退出循环。
一次线上事故,Redis主从切换导致了缓存雪崩
我们假设,slave 的机器时钟比 master 走得快很多。 此时,Redis master里设置了过期时间的key,从 slave 角度来看,可能会有很多在 master 里没过期的数据其实已经过期了。 如果此时操作主从切换,把 slave 提升为新的 master。 它成为 master 后,就会开始大量清理过期 key,此时就会导致以下结果: 1. master 大量清理过期 key,主线程可能会发生阻塞,无法及时处理客户端请 求。2. Redis 中数据大量过期,引发缓存雪崩。 当 master与slave 机器时钟严重不一致时,对业务的影响非常大。 所以,我们一定要保证主从库的机器时钟一致性,避免发生这些问题。
cluster的故障恢复是怎么做的?
判断故障的逻辑其实与哨兵模式有点类似,在集群中,每个节点都会定期的向其他节点发送ping命令,通过有没有收到回复来判断其他节点是否已经下线。
如果长时间没有回复,那么发起ping命令的节点就会认为目标节点疑似下线,也可以和哨兵一样称作主观下线,当然也需要集群中一定数量的节点都认为该节点下线才可以
1.当A节点发现目标节点疑似下线,就会向集群中的其他节点散播消息,其他节点就会向目标节点发送命令,判断目标节点是否下线
2.如果集群中半数以上的节点都认为目标节点下线,就会对目标节点标记为下线,从而告诉其他节点,让目标节点在整个集群中都下线
2.如果集群中半数以上的节点都认为目标节点下线,就会对目标节点标记为下线,从而告诉其他节点,让目标节点在整个集群中都下线
无硬盘复制是什么?
1.master禁用了RDB快照时,发生了主从同步(复制初始化)操作,也会生成RDB快照,但是之后如果master发成了重启,就会用RDB快照去恢复数据,这份数据可能已经很久了,中间就会丢失数据
2.在这种一主多从的结构中,master每次和slave同步数据都要进行一次快照,从而在硬盘中生成RDB文件,会影响性能
2.在这种一主多从的结构中,master每次和slave同步数据都要进行一次快照,从而在硬盘中生成RDB文件,会影响性能
为了解决这种问题,redis在后续的更新中也加入了无硬盘复制功能,也就是说直接通过网络发送给slave,避免了和硬盘交互,但是也是有io消耗
安全问题
SQL注入
SQL注入是比较常见的网络攻击方式之一,它不是利用操作系统的BUG来实现攻击,而是针对程序员编写时的疏忽,通过SQL语句,实现无账号登录,甚至篡改数据库。
Sql 注入攻击是通过将恶意的 Sql 查询或添加语句插入到应用的输入参数中,再在后台 Sql 服务器上解析执行进行的攻击,它目前黑客对数据库进行攻击的最常用手段之一
Sql 注入攻击是通过将恶意的 Sql 查询或添加语句插入到应用的输入参数中,再在后台 Sql 服务器上解析执行进行的攻击,它目前黑客对数据库进行攻击的最常用手段之一
sql 注入产生原因及威胁
当我们访问动态网页时, Web 服务器会向数据访问层发起 Sql 查询请求,如果权限验证通过就会执行 Sql 语句。这种网站内部直接发送的Sql请求一般不会有危险,但实际情况是很多时候需要结合用户的输入数据动态构造 Sql 语句,如果用户输入的数据被构造成恶意 Sql 代码,Web 应用又未对动态构造的 Sql 语句使用的参数进行审查,则会带来意想不到的危险。
Sql 注入带来的威胁主要有如下几点
猜解后台数据库,这是利用最多的方式,盗取网站的敏感信息。
绕过认证,列如绕过验证登录网站后台。
注入可以借助数据库的存储过程进行提权等操作
Sql 注入带来的威胁主要有如下几点
猜解后台数据库,这是利用最多的方式,盗取网站的敏感信息。
绕过认证,列如绕过验证登录网站后台。
注入可以借助数据库的存储过程进行提权等操作
SQL注入漏洞对于数据安全的影响
数据库信息泄漏:数据库中存放的用户的隐私信息的泄露。
网页篡改:通过操作数据库对特定网页进行篡改。
网站被挂马,传播恶意软件:修改数据库一些字段的值,嵌入网马链接,进行挂马攻击。
数据库被恶意操作:数据库服务器被攻击,数据库的系统管理员帐户被窜改。
服务器被远程控制,被安装后门。经由数据库服务器提供的操作系统支持,让黑客得以修改或控制操作系统。
破坏硬盘数据,瘫痪全系统。
网页篡改:通过操作数据库对特定网页进行篡改。
网站被挂马,传播恶意软件:修改数据库一些字段的值,嵌入网马链接,进行挂马攻击。
数据库被恶意操作:数据库服务器被攻击,数据库的系统管理员帐户被窜改。
服务器被远程控制,被安装后门。经由数据库服务器提供的操作系统支持,让黑客得以修改或控制操作系统。
破坏硬盘数据,瘫痪全系统。
mysql
为什么推荐使用自增 id 作为主键?
1.普通索引的 B+ 树上存放的是主键索引的值,如果该值较大,会「导致普通索引的存储空间较大」
2.使用自增 id 做主键索引新插入数据只要放在该页的最尾端就可以,直接「按照顺序插入」,不用刻意维护
3.页分裂容易维护,当插入数据的当前页快满时,会发生页分裂的现象,如果主键索引不为自增 id,那么数据就可能从页的中间插入,页的数据会频繁的变动,「导致页分裂维护成本较高」
什么是索引?以及好处和坏处
索引是一种帮助快速查找数据的数据结构,可以把它理解为书的目录,通过索引能够快速找到数据所在位置。
索引数据结构有:
Hash表(通过hash算法快速定位数据,但不适合范围查询,因为需要每个key都进行一次hash)、二叉树(查找和修改效率都比较高),但是在InnoDB引擎中使用的索引是B+Tree,相较于二叉树,B+Tree这种多叉树,更加矮宽,更适合存储在磁盘中。使用索引增加了数据查找的效率,但是相对的由于索引也需要存储到磁盘,所以增加了存储的压力,并且新增数据时需要同步维护索引。但是合理的使用索引能够极大提高我们的效率!
Hash表(通过hash算法快速定位数据,但不适合范围查询,因为需要每个key都进行一次hash)、二叉树(查找和修改效率都比较高),但是在InnoDB引擎中使用的索引是B+Tree,相较于二叉树,B+Tree这种多叉树,更加矮宽,更适合存储在磁盘中。使用索引增加了数据查找的效率,但是相对的由于索引也需要存储到磁盘,所以增加了存储的压力,并且新增数据时需要同步维护索引。但是合理的使用索引能够极大提高我们的效率!
MyISAM 与 InnoDB 的区别是什么?
「InnoDB 必须有唯一索引(主键)」,如果没有指定的话 InnoDB 会自己生成一个隐藏列Row_id来充当默认主键,「MyISAM 可以没有」
「InnoDB支持事务,MyISAM不支持」。
「InnoDB 支持外键,而 MyISAM 不支持」。
「InnoDB是聚集索引」,使用B+Tree作为索引结构,数据文件是和索引绑在一起的,必须要有主键。「MyISAM是非聚集索引」,也是使用B+Tree作为索引结构,索引和数据文件是分离的,索引保存的是数据文件的指针。主键索引和辅助索引是独立的。
「InnoDB 不保存表的具体行数」。「MyISAM 用一个变量保存了整个表的行数」。
Innodb 有 「redolog」 日志文件,MyISAM 没有
「InnoDB 支持表、行锁,而 MyISAM 支持表级锁」
「Innodb存储文件有frm、ibd,而Myisam是frm、MYD、MYI」
Innodb:frm是表定义文件,ibd是数据文件
Myisam:frm是表定义文件,myd是数据文件,myi是索引文件
Innodb:frm是表定义文件,ibd是数据文件
Myisam:frm是表定义文件,myd是数据文件,myi是索引文件
mysql中有哪些索引以及使用场景?
聚集索引:指索引项的排序方式和表中数据记录排序方式一致的索引。聚簇索引并不是一种单独的索引类型,而是一种数据存储方式。术语“聚簇”表示数据行和相邻的键值紧凑的存储在一起。
也就是说聚集索引的顺序就是数据的物理存储顺序。它会根据聚集索引键的顺序来存储表中的数据,即对表的数据按索引键的顺序进行排序,然后重新存储到磁盘上。因为数据在物理存放时只能有一种排列方式,所以一个表只能有一个聚集索引。
聚集索引的使用场合为:
a.查询命令的回传结果是以该字段为排序依据的;
b.查询的结果返回一个区间的值;
c.查询的结果返回某值相同的大量结果集。
也就是说聚集索引的顺序就是数据的物理存储顺序。它会根据聚集索引键的顺序来存储表中的数据,即对表的数据按索引键的顺序进行排序,然后重新存储到磁盘上。因为数据在物理存放时只能有一种排列方式,所以一个表只能有一个聚集索引。
聚集索引的使用场合为:
a.查询命令的回传结果是以该字段为排序依据的;
b.查询的结果返回一个区间的值;
c.查询的结果返回某值相同的大量结果集。
非聚集索引: 索引顺序与物理存储顺序不同
非聚集索引的使用场合为:
a.查询所获数据量较少时;
b.某字段中的数据的唯一性比较高时;
非聚集索引的使用场合为:
a.查询所获数据量较少时;
b.某字段中的数据的唯一性比较高时;
稠密索引:每个索引键值都对应有一个索引项
稠密索引的优点是:它所占空间更小,且插入和删除时的维护开销也小。
稠密索引的优点是:它所占空间更小,且插入和删除时的维护开销也小。
索引有哪几种类型?
主键索引: 数据列不允许重复,不允许为NULL,一个表只能有一个主键。
唯一索引: 数据列不允许重复,允许为NULL值,一个表允许多个列创建唯一索引。
普通索引: 基本的索引类型,没有唯一性的限制,允许为NULL值。
全文索引:是目前搜索引擎使用的一种关键技术,对文本的内容进行分词、搜索。
覆盖索引:查询列要被所建的索引覆盖,不必读取数据行
组合索引:多列值组成一个索引,用于组合搜索,效率大于索引合并
唯一索引: 数据列不允许重复,允许为NULL值,一个表允许多个列创建唯一索引。
普通索引: 基本的索引类型,没有唯一性的限制,允许为NULL值。
全文索引:是目前搜索引擎使用的一种关键技术,对文本的内容进行分词、搜索。
覆盖索引:查询列要被所建的索引覆盖,不必读取数据行
组合索引:多列值组成一个索引,用于组合搜索,效率大于索引合并
适合添加索引的情况
业务上具有唯一特性的字段,即使是组合字段,也必须建成唯一索引
不要以为唯一索引影响了insert的速度,这个速度损耗可以忽略,但提高查询速度是明显的
频繁作为where查询条件的字段添加index普通索引
经常group by 和order by的列(同时存在使用联合索引,group by 索引放前面,单独就单独创建)
更新update,删除delete的where条件列
去重字段需要创建索引
多表join字段需要创建索引
首先join表的数量尽量不要超过三张,其次对where条件创建索引,对于连接的字段创建索引
使用类型小的列创建索引
只用字符串前缀添加索引
区分度高(散列性高) 的列适合做索引
使用最频繁的列放到联合索引的最左侧
在多个字段都要创建索引联合索引优于单值索引
不要以为唯一索引影响了insert的速度,这个速度损耗可以忽略,但提高查询速度是明显的
频繁作为where查询条件的字段添加index普通索引
经常group by 和order by的列(同时存在使用联合索引,group by 索引放前面,单独就单独创建)
更新update,删除delete的where条件列
去重字段需要创建索引
多表join字段需要创建索引
首先join表的数量尽量不要超过三张,其次对where条件创建索引,对于连接的字段创建索引
使用类型小的列创建索引
只用字符串前缀添加索引
区分度高(散列性高) 的列适合做索引
使用最频繁的列放到联合索引的最左侧
在多个字段都要创建索引联合索引优于单值索引
索引失效的场景有哪些?
联合索引非最左匹配
对索引使用函数/表达式计算
不能继续使用索引中范围条件(bettween、<、>、in等)右边的列
索引字段上使用(!= 或者 < >)判断时,会导致索引失效而转向全表扫描
索引字段上使用 is null / is not null 判断时,会导致索引失效而转向全表扫描。
索引字段使用like以通配符开头(‘%字符串’)时,会导致索引失效而转向全表扫描,也是最左前缀原则。
索引字段是字符串,但查询时不加单引号,会导致索引失效而转向全表扫描
索引字段使用 or 时,会导致索引失效而转向全表扫描
索引的分类?
按「数据结构」分类:B+tree索引、Hash索引、Full-text索引。
按「物理存储」分类:聚集索引(主键索引)、二级索引(辅助索引)。
按「字段特性」分类:主键索引、唯一索引、普通索引、前缀索引。
按「字段个数」分类:单列索引、联合索引。
按「物理存储」分类:聚集索引(主键索引)、二级索引(辅助索引)。
按「字段特性」分类:主键索引、唯一索引、普通索引、前缀索引。
按「字段个数」分类:单列索引、联合索引。
创建索引有什么原则呢?
最左前缀匹配原则
频繁作为查询条件的字段才去创建索引
频繁更新的字段不适合创建索引
索引列不能参与计算,不能有函数操作
优先考虑扩展索引,而不是新建索引,避免不必要的索引
在order by或者group by子句中,创建索引需要注意顺序
区分度低的数据列不适合做索引列(如性别)
定义有外键的数据列一定要建立索引。
对于定义为text、image数据类型的列不要建立索引。
删除不再使用或者很少使用的索引
频繁作为查询条件的字段才去创建索引
频繁更新的字段不适合创建索引
索引列不能参与计算,不能有函数操作
优先考虑扩展索引,而不是新建索引,避免不必要的索引
在order by或者group by子句中,创建索引需要注意顺序
区分度低的数据列不适合做索引列(如性别)
定义有外键的数据列一定要建立索引。
对于定义为text、image数据类型的列不要建立索引。
删除不再使用或者很少使用的索引
创建索引的三种方式
1、在执行create table时创建索引
2、使用alter table命令添加索引
3、使用create index命令创建
2、使用alter table命令添加索引
3、使用create index命令创建
什么是最左前缀原则?什么是最左匹配原则?
最左前缀原则,就是最左优先,在创建多列索引时,要根据业务需求,where子句中使用最频繁的一列放在最左边。
当我们创建一个组合索引的时候,如(k1,k2,k3),相当于创建了(k1)、(k1,k2)和(k1,k2,k3)三个索引,这就是最左匹配原则。。
当我们创建一个组合索引的时候,如(k1,k2,k3),相当于创建了(k1)、(k1,k2)和(k1,k2,k3)三个索引,这就是最左匹配原则。。
什么情况下不合适场景索引?
where,group by,order by中使用不到的字段不需要创建索引
数据量小(少于1000行)的表最好不要创建索引
大量重复数据的列不要创建索引
避免对更新的字段创建索引
不建议用无序的字段值创建索引
不要定义冗余重复的索引
数据量小(少于1000行)的表最好不要创建索引
大量重复数据的列不要创建索引
避免对更新的字段创建索引
不建议用无序的字段值创建索引
不要定义冗余重复的索引
count(*)和count(1)有什么区别?
count(*)=count(1)>count(主键)>count(字段)
说一说三大范式
原子性:列或者字段不能再分,要求属性具有原子性,不可再分解
唯一性:一张表只说一件事,是对记录的唯一性约束,要求记录有唯一标识
直接性:数据不能存在传递关系,即每个属性都跟主键有直接关系,而不是间接关系
唯一性:一张表只说一件事,是对记录的唯一性约束,要求记录有唯一标识
直接性:数据不能存在传递关系,即每个属性都跟主键有直接关系,而不是间接关系
B树和B+数的区别
B树与B+树的区别有两个:
1、B树的非叶子节点是存储数据的,而B+树的非叶子节点只存储索引信息
2、B+树的非最右侧的叶子节点向右会指向右侧的叶子节点,形成一个有序的连表
在查找数据的时候,B树需要根据数据是比较查找查询次数比较多,
由于B+树存储的是数据的索引,所以只有一次IO就可以查找到数据
而且B树非叶子节点只存储索引,所以能存储更多的索引,使树的高度降低
1、B树的非叶子节点是存储数据的,而B+树的非叶子节点只存储索引信息
2、B+树的非最右侧的叶子节点向右会指向右侧的叶子节点,形成一个有序的连表
在查找数据的时候,B树需要根据数据是比较查找查询次数比较多,
由于B+树存储的是数据的索引,所以只有一次IO就可以查找到数据
而且B树非叶子节点只存储索引,所以能存储更多的索引,使树的高度降低
什么是幻读,脏读,不可重复读呢?
事务A、B交替执行,事务A被事务B干扰到了,因为事务A读取到事务B未提交的数据,这就是脏读
在一个事务范围内,两个相同的查询,读取同一条记录,却返回了不同的数据,这就是不可重复读。
事务A查询一个范围的结果集,另一个并发事务B修改了数据,然后事务A再次查询相同的范围时发现两次读取到的结果集不一样了,这就是幻读
在一个事务范围内,两个相同的查询,读取同一条记录,却返回了不同的数据,这就是不可重复读。
事务A查询一个范围的结果集,另一个并发事务B修改了数据,然后事务A再次查询相同的范围时发现两次读取到的结果集不一样了,这就是幻读
什么是覆盖索引?
覆盖索引(covering index)指一个查询语句的执行只用从索引中就能够取得,不必从数据表中读取,可以减少回表的次数
SQL优化?
1、尽量避免全表扫描,使用where及order by涉及的列上建立索引
2、尽量避免在where语句中对字段进行null值判断
3、尽量避免在where语句中使用!=或<>操作符
4、尽量避免在where语句中使用or来连接条件
5、in 和 not in 会导致全表扫描
6、使用模糊查询时也会导致全表扫描
7、尽量避免在where语句中对字段进行表达式操作
8、尽量避免在where语句中对字段进行函数操作
9、不要在where语句中的“=”左边进行函数、算术运算或表达式运算
10、使用in的时候用exists代替in
2、尽量避免在where语句中对字段进行null值判断
3、尽量避免在where语句中使用!=或<>操作符
4、尽量避免在where语句中使用or来连接条件
5、in 和 not in 会导致全表扫描
6、使用模糊查询时也会导致全表扫描
7、尽量避免在where语句中对字段进行表达式操作
8、尽量避免在where语句中对字段进行函数操作
9、不要在where语句中的“=”左边进行函数、算术运算或表达式运算
10、使用in的时候用exists代替in
事务的隔离级别?
1.「读提交」:即能够「读取到那些已经提交」的数据
2.「读未提交」:即能够「读取到没有被提交」的数据
3.「可重复读」:可重复读指的是在一个事务内,最开始读到的数据和事务结束前的「任意时刻读到的同一批数据都是一致的」
4.「可串行化」:最高事务隔离级别,不管多少事务,都是「依次按序一个一个执行」
2.「读未提交」:即能够「读取到没有被提交」的数据
3.「可重复读」:可重复读指的是在一个事务内,最开始读到的数据和事务结束前的「任意时刻读到的同一批数据都是一致的」
4.「可串行化」:最高事务隔离级别,不管多少事务,都是「依次按序一个一个执行」
「脏读」
脏读指的是「读到了其他事务未提交的数据」,未提交意味着这些数据可能会回滚,也就是可能最终不会存到数据库中,也就是不存在的数据。读到了并一定最终存在的数据,这就是脏读
「不可重复读」
对比可重复读,不可重复读指的是在同一事务内,「不同的时刻读到的同一批数据可能是不一样的」。
「幻读」
幻读是针对数据插入(INSERT)操作来说的。假设事务A对某些行的内容作了更改,但是还未提交,此时事务B插入了与事务A更改前的记录相同的记录行,并且在事务A提交之前先提交了,而这时,在事务A中查询,会发现「好像刚刚的更改对于某些数据未起作用」,但其实是事务B刚插入进来的这就叫幻读
脏读指的是「读到了其他事务未提交的数据」,未提交意味着这些数据可能会回滚,也就是可能最终不会存到数据库中,也就是不存在的数据。读到了并一定最终存在的数据,这就是脏读
「不可重复读」
对比可重复读,不可重复读指的是在同一事务内,「不同的时刻读到的同一批数据可能是不一样的」。
「幻读」
幻读是针对数据插入(INSERT)操作来说的。假设事务A对某些行的内容作了更改,但是还未提交,此时事务B插入了与事务A更改前的记录相同的记录行,并且在事务A提交之前先提交了,而这时,在事务A中查询,会发现「好像刚刚的更改对于某些数据未起作用」,但其实是事务B刚插入进来的这就叫幻读
优化慢查询?
分析语句,是否加载了不必要的字段/数据。
分析SQl执行句话,是否命中索引等。
如果SQL很复杂,优化SQL结构
如果表数据量太大,考虑分表
分析SQl执行句话,是否命中索引等。
如果SQL很复杂,优化SQL结构
如果表数据量太大,考虑分表
一条 Sql 语句查询一直慢会是什么原因?
「1.没有用到索引」
比如函数导致的索引失效,或者本身就没有加索引
比如函数导致的索引失效,或者本身就没有加索引
「2.表数据量太大」
考虑分库分表吧
考虑分库分表吧
「3.优化器选错了索引」
「考虑使用」 force index 强制走索引
「考虑使用」 force index 强制走索引
说说你的 Sql 调优思路吧
1.「表结构优化」
1.1拆分字段
1.2字段类型的选择
1.3字段类型大小的限制
1.4合理的增加冗余字段
1.5新建字段一定要有默认值
1.1拆分字段
1.2字段类型的选择
1.3字段类型大小的限制
1.4合理的增加冗余字段
1.5新建字段一定要有默认值
2.「索引方面」
2.1索引字段的选择
2.2利用好mysql支持的索引下推,覆盖索引等功能
2.3唯一索引和普通索引的选择
2.1索引字段的选择
2.2利用好mysql支持的索引下推,覆盖索引等功能
2.3唯一索引和普通索引的选择
3.「查询语句方面」
3.1避免索引失效
3.2合理的书写where条件字段顺序
3.3小表驱动大表
3.4可以使用force index()防止优化器选错索引
3.1避免索引失效
3.2合理的书写where条件字段顺序
3.3小表驱动大表
3.4可以使用force index()防止优化器选错索引
4.「分库分表」
分布式式事务怎么实现?
1.「本地消息表」
2.「消息事务」
3.「二阶段提交」
4.「三阶段提交」
5.「TCC」
6.「最大努力通知」
7.「Seata 框架」
2.「消息事务」
3.「二阶段提交」
4.「三阶段提交」
5.「TCC」
6.「最大努力通知」
7.「Seata 框架」
主从复制步骤
步骤一:主库的更新事件(update、insert、delete)被写到binlog
步骤二:从库发起连接,连接到主库。
步骤三:此时主库创建一个binlog dump thread,把binlog的内容发送到从库。
步骤四:从库启动之后,创建一个I/O线程,读取主库传过来的binlog内容并写入到relay log
步骤五:还会创建一个SQL线程,从relay log里面读取内容,从Exec_Master_Log_Pos位置开始执行读取到的更新事件,将更新内容写入到slave的db
步骤二:从库发起连接,连接到主库。
步骤三:此时主库创建一个binlog dump thread,把binlog的内容发送到从库。
步骤四:从库启动之后,创建一个I/O线程,读取主库传过来的binlog内容并写入到relay log
步骤五:还会创建一个SQL线程,从relay log里面读取内容,从Exec_Master_Log_Pos位置开始执行读取到的更新事件,将更新内容写入到slave的db
主从同步延迟问题以及解决办法
主从同步延迟的原因
一个服务器开放N个链接给客户端来连接的,这样有会有大并发的更新操作, 但是从服务器的里面读取binlog的线程仅有一个,当某个SQL在从服务器上执行的时间稍长 或者由于某个SQL要进行锁表就会导致,主服务器的SQL大量积压,未被同步到从服务器里。这就导致了主从不一致, 也就是主从延迟。
主从同步延迟的解决办法
主服务器要负责更新操作,对安全性的要求比从服务器要高,所以有些设置参数可以修改,比如sync_binlog=1,innodb_flush_log_at_trx_commit = 1 之类的设置等。
选择更好的硬件设备作为slave。
把一台从服务器当度作为备份使用, 而不提供查询, 那边他的负载下来了, 执行relay log 里面的SQL效率自然就高了。
增加从服务器喽,这个目的还是分散读的压力,从而降低服务器负载。
一个服务器开放N个链接给客户端来连接的,这样有会有大并发的更新操作, 但是从服务器的里面读取binlog的线程仅有一个,当某个SQL在从服务器上执行的时间稍长 或者由于某个SQL要进行锁表就会导致,主服务器的SQL大量积压,未被同步到从服务器里。这就导致了主从不一致, 也就是主从延迟。
主从同步延迟的解决办法
主服务器要负责更新操作,对安全性的要求比从服务器要高,所以有些设置参数可以修改,比如sync_binlog=1,innodb_flush_log_at_trx_commit = 1 之类的设置等。
选择更好的硬件设备作为slave。
把一台从服务器当度作为备份使用, 而不提供查询, 那边他的负载下来了, 执行relay log 里面的SQL效率自然就高了。
增加从服务器喽,这个目的还是分散读的压力,从而降低服务器负载。
什么是最左前缀原则?
最左前缀其实说的是,在 where 条件中出现的字段,「如果只有组合索引中的部分列,则这部分列的触发索引顺序」,是按照定义索引的时候的顺序从前到后触发,最左面一个列触发不了,之后的所有列索引都无法触发。
mysql 的内连接、左连接、右连接有什么区别?
Inner join 内连接,在两张表进行连接查询时,只保留两张表中完全匹配的结果集
left join 在两张表进行连接查询时,会返回左表所有的行,即使在右表中没有匹配的记录。
right join 在两张表进行连接查询时,会返回右表所有的行,即使在左表中没有匹配的记录。
left join 在两张表进行连接查询时,会返回左表所有的行,即使在右表中没有匹配的记录。
right join 在两张表进行连接查询时,会返回右表所有的行,即使在左表中没有匹配的记录。
什么是内连接、外连接、交叉连接、笛卡尔积呢?
内连接(inner join):取得两张表中满足存在连接匹配关系的记录。
外连接(outer join):取得两张表中满足存在连接匹配关系的记录,以及某张表(或两张表)中不满足匹配关系的记录。
交叉连接(cross join):显示两张表所有记录一一对应,没有匹配关系进行筛选,也被称为:笛卡尔积。
外连接(outer join):取得两张表中满足存在连接匹配关系的记录,以及某张表(或两张表)中不满足匹配关系的记录。
交叉连接(cross join):显示两张表所有记录一一对应,没有匹配关系进行筛选,也被称为:笛卡尔积。
一条Sql的执行顺序?
from<左表的名字>
on<join的条件>
<join的类型><右表的名字>
where<where的条件>
group by<group by的字段>
Having<having的条件>
select
distinct<要查询的字段>
order by<order by的条件>
limit<limit的数字>
on<join的条件>
<join的类型><右表的名字>
where<where的条件>
group by<group by的字段>
Having<having的条件>
select
distinct<要查询的字段>
order by<order by的条件>
limit<limit的数字>
使用Mysql内存飙到100%怎么排查?
排查过程:
使用top 命令观察,确定是mysqld导致还是其他原因。
如果是mysqld导致的,show processlist,查看session情况,确定是不是有消耗资源的sql在运行。
找出消耗高的 sql,看看执行计划是否准确, 索引是否缺失,数据量是否太大。
处理:
kill 掉这些线程(同时观察 cpu 使用率是否下降),
进行相应的调整(比如说加索引、改 sql、改内存参数)
重新跑这些 SQL。
其他情况:
也有可能是每个 sql 消耗资源并不多,但是突然之间,有大量的 session 连进来导致 cpu 飙升,这种情况就需要跟应用一起来分析为何连接数会激增,再做出相应的调整,比如说限制连接数等
使用top 命令观察,确定是mysqld导致还是其他原因。
如果是mysqld导致的,show processlist,查看session情况,确定是不是有消耗资源的sql在运行。
找出消耗高的 sql,看看执行计划是否准确, 索引是否缺失,数据量是否太大。
处理:
kill 掉这些线程(同时观察 cpu 使用率是否下降),
进行相应的调整(比如说加索引、改 sql、改内存参数)
重新跑这些 SQL。
其他情况:
也有可能是每个 sql 消耗资源并不多,但是突然之间,有大量的 session 连进来导致 cpu 飙升,这种情况就需要跟应用一起来分析为何连接数会激增,再做出相应的调整,比如说限制连接数等
什么是索引下推?
如果存在某些被索引的列的判断条件时,MySQL 将这一部分判断条件传递给存储引擎,然后由存储引擎通过判断索引是否符合 MySQL 服务器传递的条件,「只有当索引符合条件时才会将数据检索出来返回给 MySQL 服务器」 。
Innodb 事务为什么要两阶段提交?
先写 redolog 后写binlog。假设在 redolog 写完,binlog 还没有写完的时候,MySQL 进程异常重启,这时候 binlog 里面就没有记录这个语句。然后你会发现,如果需要用这个 binlog 来恢复临时库的话,由于这个语句的 「binlog 丢失」,这个临时库就会少了这一次更新,恢复出来的这一行 c 的值就是 0,与原库的值不同。
先写 binlog 后写 redolog。如果在 binlog 写完之后 crash,由于 redolog 还没写,崩溃恢复以后这个事务无效,所以这一行c的值是0。但是 binlog 里面已经记录了“把c从0改成1”这个日志。所以,在之后用 binlog 来恢复的时候就「多了一个事务出来」,恢复出来的这一行 c 的值就是 1,与原库的值不同。
可以看到,「如果不使用“两阶段提交”,那么数据库的状态就有可能和用它的日志恢复出来的库的状态不一致」。
为什么采用 B+ 树,而不是 B-树
B+ 树只在叶子结点储存数据,非叶子结点不存具体数据,只存 key,查询更稳定,增大了广度,而一个节点就是磁盘一个内存页,内存页大小固定,那么相比 B 树,B- 树这些「可以存更多的索引结点」,宽度更大,树高矮,节点小,拉取一次数据的磁盘 IO 次数少,并且 B+ 树只需要去遍历叶子节点就可以实现整棵树的遍历。而且在数据库中基于范围的查询是非常频繁的,效率更高。
mysql的索引有哪些,聚簇和⾮聚簇索引⼜是什么?
普通索引 最基本的索引,仅加速查询 CREATE INDEX idx_name ON table_name(filed_name)
唯一索引 加速查询,列值唯一,允许为空;
组合索引则列值的组合必须唯一 CREATE UNIQUE INDEX idx_name ON table_name(filed_name_1,filed_name_2)
组合索引则列值的组合必须唯一 CREATE UNIQUE INDEX idx_name ON table_name(filed_name_1,filed_name_2)
主键索引 加速查询,列值唯一,
一个表只有1个,不允许有空值 ALTER TABLE table_name ADD PRIMARY KEY ( filed_name )
一个表只有1个,不允许有空值 ALTER TABLE table_name ADD PRIMARY KEY ( filed_name )
组合索引 加速查询,多条件组合查询 CREATE INDEX idx_name ON table_name(filed_name_1,filed_name_2);
覆盖索引 索引包含所需要的值,不需要“回表”查询,
比如查询 两个字段,刚好是 组合索引 的两个字段
比如查询 两个字段,刚好是 组合索引 的两个字段
全文索引 对内容进行分词搜索,仅可用于Myisam, 更多用ElasticSearch做搜索 ALTER TABLE table_name ADD FULLTEXT ( filed_name )
聚簇索引:B+树是左⼩右⼤的顺序存储结构,节点只包含id索引列,⽽叶⼦节点包含索引列和数据,这种数据和索
引在⼀起存储的索引⽅式叫做聚簇索引,⼀张表只能有⼀个聚簇索引。假设没有定义主键,InnoDB会选
择⼀个唯⼀的⾮空索引代替,如果没有的话则会隐式定义⼀个主键作为聚簇索引。
引在⼀起存储的索引⽅式叫做聚簇索引,⼀张表只能有⼀个聚簇索引。假设没有定义主键,InnoDB会选
择⼀个唯⼀的⾮空索引代替,如果没有的话则会隐式定义⼀个主键作为聚簇索引。
非聚簇索引:⾮聚簇索引(⼆级索引)保存的是主
键id值,这⼀点和myisam保存的是数据地址是不同的
键id值,这⼀点和myisam保存的是数据地址是不同的
什么是回表?
回表就是先通过数据库索引扫描出该索引树中数据所在的行,取到主键 id,再通过主键 id 取出主键索引数中的数据,即基于非主键索引的查询需要多扫描一棵索引树.
普通索引和唯一索引该怎么选择?
查询
当普通索引为条件时查询到数据会一直扫描,直到扫完整张表
当唯一索引为查询条件时,查到该数据会直接返回,不会继续扫表
当普通索引为条件时查询到数据会一直扫描,直到扫完整张表
当唯一索引为查询条件时,查到该数据会直接返回,不会继续扫表
更新
普通索引会直接将操作更新到 change buffer 中,然后结束
唯一索引需要判断数据是否冲突
普通索引会直接将操作更新到 change buffer 中,然后结束
唯一索引需要判断数据是否冲突
所以「唯一索引更加适合查询的场景,普通索引更适合插入的场景」
什么是事务?其特性是什么?
事务是指是程序中一系列操作必须全部成功完成,有一个失败则全部失败。
A原⼦性由undo log⽇志保证,它记录了需要回滚的⽇志信息,事务回滚时撤销已经执⾏成功的sql
C⼀致性⼀般由代码层⾯来保证
I隔离性由MVCC来保证
D持久性由内存+redo log来保证,mysql修改数据同时在内存和redo log记录这次操作,事务提交的时
候通过redo log刷盘,宕机的时候可以从redo log恢复
候通过redo log刷盘,宕机的时候可以从redo log恢复
redolog 是怎么记录日志的?
InnoDB 的 redo log 是固定大小的,比如可以配置为一组4个文件,每个文件的大小是1GB,那么总共就可以记录4GB的操作。「从头开始写,写到末尾就又回到开头循环写」。
所以,如果数据写满了但是还没有来得及将数据真正的刷入磁盘当中,那么就会发生「内存抖动」现象,从肉眼的角度来观察会发现 mysql 会宕机一会儿,此时就是正在刷盘了。
一条 Sql 语句查询偶尔慢会是什么原因?
「1. 数据库在刷新脏页」
比如 「redolog 写满了」,「内存不够用了」释放内存如果是脏页也需要刷,mysql 「正常空闲状态刷脏页」
比如 「redolog 写满了」,「内存不够用了」释放内存如果是脏页也需要刷,mysql 「正常空闲状态刷脏页」
「2. 没有拿到锁」
针对线上的数据库,你会做哪些监控,业务性能 + 数据安全 角度分析
业务性能
1、应用上线前会审查业务新增的sql,和分析sql执行计划 比如是否存在 select * ,索引建立是否合理
2、开启慢查询日志,定期分析慢查询日志
3、监控CPU/内存利用率,读写、网关IO、流量带宽 随着时间的变化统计图
4、吞吐量QPS/TPS,一天内读写随着时间的变化统计图
1、应用上线前会审查业务新增的sql,和分析sql执行计划 比如是否存在 select * ,索引建立是否合理
2、开启慢查询日志,定期分析慢查询日志
3、监控CPU/内存利用率,读写、网关IO、流量带宽 随着时间的变化统计图
4、吞吐量QPS/TPS,一天内读写随着时间的变化统计图
数据安全
1、短期增量备份,比如一周一次。 定期全量备份,比如一月一次
2、检查是否有非授权用户,是否存在弱口令,网络防火墙检查
3、导出数据是否进行脱敏,防止数据泄露或者黑产利用
4、数据库 全量操作日志审计,防止数据泄露
5、数据库账号密码 业务独立,权限独立控制,防止多库共用同个账号密码
6、高可用 主从架构,多机房部署
1、短期增量备份,比如一周一次。 定期全量备份,比如一月一次
2、检查是否有非授权用户,是否存在弱口令,网络防火墙检查
3、导出数据是否进行脱敏,防止数据泄露或者黑产利用
4、数据库 全量操作日志审计,防止数据泄露
5、数据库账号密码 业务独立,权限独立控制,防止多库共用同个账号密码
6、高可用 主从架构,多机房部署
在高并发情况下,如何做到安全的修改同一行数据?
要安全的修改同一行数据,就要保证一个线程在修改时其它线程无法更新这行记录。一般有悲观锁和乐观锁两种方案~
使用悲观锁
悲观锁思想就是,当前线程要进来修改数据时,别的线程都得拒之门外~
比如,可以使用select…for update ~
select * from User where name=‘jay’ for update
以上这条sql语句会锁定了User表中所有符合检索条件(name=‘jay’)的记录。本次事务提交之前,别的线程都无法修改这些记录。
使用乐观锁
乐观锁思想就是,有线程过来,先放过去修改,如果看到别的线程没修改过,就可以修改成功,如果别的线程修改过,就修改失败或者重试。实现方式:乐观锁一般会使用版本号机制或CAS算法实现。
使用悲观锁
悲观锁思想就是,当前线程要进来修改数据时,别的线程都得拒之门外~
比如,可以使用select…for update ~
select * from User where name=‘jay’ for update
以上这条sql语句会锁定了User表中所有符合检索条件(name=‘jay’)的记录。本次事务提交之前,别的线程都无法修改这些记录。
使用乐观锁
乐观锁思想就是,有线程过来,先放过去修改,如果看到别的线程没修改过,就可以修改成功,如果别的线程修改过,就修改失败或者重试。实现方式:乐观锁一般会使用版本号机制或CAS算法实现。
mysql事务的四大特性
原子性: 事务作为一个整体被执行,包含在其中的对数据库的操作要么全部被执行,要么都不执行。
一致性: 指在事务开始之前和事务结束以后,数据不会被破坏,假如A账户给B账户转10块钱,不管成功与否,A和B的总金额是不变的。
隔离性: 多个事务并发访问时,事务之间是相互隔离的,即一个事务不影响其它事务运行效果。简言之,就是事务之间是进水不犯河水的。
持久性: 表示事务完成以后,该事务对数据库所作的操作更改,将持久地保存在数据库之中。
一致性: 指在事务开始之前和事务结束以后,数据不会被破坏,假如A账户给B账户转10块钱,不管成功与否,A和B的总金额是不变的。
隔离性: 多个事务并发访问时,事务之间是相互隔离的,即一个事务不影响其它事务运行效果。简言之,就是事务之间是进水不犯河水的。
持久性: 表示事务完成以后,该事务对数据库所作的操作更改,将持久地保存在数据库之中。
一条sql执行过长的时间,你如何优化,从哪些方面入手?
查看是否涉及多表和子查询,优化Sql结构,如去除冗余字段,是否可拆表等
优化索引结构,看是否可以适当添加索引
数量大的表,可以考虑进行分离/分表(如交易流水表)
数据库主从分离,读写分离
explain分析sql语句,查看执行计划,优化sql
查看mysql执行日志,分析是否有其他方面的问题
优化索引结构,看是否可以适当添加索引
数量大的表,可以考虑进行分离/分表(如交易流水表)
数据库主从分离,读写分离
explain分析sql语句,查看执行计划,优化sql
查看mysql执行日志,分析是否有其他方面的问题
InnoDB引擎的4大特性,了解过吗
插入缓冲(insert buffer)
二次写(double write)
自适应哈希索引(ahi)
预读(read ahead)
二次写(double write)
自适应哈希索引(ahi)
预读(read ahead)
什么是数据库事务?
数据库事务(简称:事务),是数据库管理系统执行过程中的一个逻辑单位,由一个有限的数据库操作序列构成,这些操作要么全部执行,要么全部不执行,是一个不可分割的工作单位。
一个6亿的表a,一个3亿的表b,通过外间tid关联,你如何最快的查询出满足条件的第50000到第50200中的这200条数据记录。
1、如果A表TID是自增长,并且是连续的,B表的ID为索引
select * from a,b where a.tid = b.id and a.tid>500000 limit 200;
2、如果A表的TID不是连续的,那么就需要使用覆盖索引.TID要么是主键,要么是辅助索引,B表ID也需要有索引。
select * from b , (select tid from a limit 50000,200) a where b.id = a .tid;
select * from a,b where a.tid = b.id and a.tid>500000 limit 200;
2、如果A表的TID不是连续的,那么就需要使用覆盖索引.TID要么是主键,要么是辅助索引,B表ID也需要有索引。
select * from b , (select tid from a limit 50000,200) a where b.id = a .tid;
执行一条 select 语句,期间发生了什么?
图解:
总结:
连接器:建立连接,管理连接、校验用户身份;
查询缓存:查询语句如果命中查询缓存则直接返回,否则继续往下执行。MySQL 8.0 已删除该模块;
解析 SQL:通过解析器对 SQL 查询语句进行词法分析、语法分析,然后构建语法树,方便后续模块读取表名、字段、语句类型;
执行 SQL:执行 SQL 共有三个阶段:
**预处理阶段:检查表或字段是否存在;将 select * 中的 * 符号扩展为表上的所有列。
**优化阶段:基于查询成本的考虑, 选择查询成本最小的执行计划;
**执行阶段:根据执行计划执行 SQL 查询语句,从存储引擎读取记录,返回给客户端;
查询缓存:查询语句如果命中查询缓存则直接返回,否则继续往下执行。MySQL 8.0 已删除该模块;
解析 SQL:通过解析器对 SQL 查询语句进行词法分析、语法分析,然后构建语法树,方便后续模块读取表名、字段、语句类型;
执行 SQL:执行 SQL 共有三个阶段:
**预处理阶段:检查表或字段是否存在;将 select * 中的 * 符号扩展为表上的所有列。
**优化阶段:基于查询成本的考虑, 选择查询成本最小的执行计划;
**执行阶段:根据执行计划执行 SQL 查询语句,从存储引擎读取记录,返回给客户端;
使用 Innodb 的情况下,一条更新语句是怎么执行的?
用以下语句来举例,c 字段无索引,id 为主键索引
update T set c=c+1 where id=2;
update T set c=c+1 where id=2;
1.执行器先找引擎取 id=2 这一行。id 是主键,引擎直接用树搜索找到这一行
如果 id=2 这一行所在的数据页本来就「在内存中」,就「直接返回」给执行器
「不在内存」中,需要先从磁盘「读入内存」,然后再「返回」
如果 id=2 这一行所在的数据页本来就「在内存中」,就「直接返回」给执行器
「不在内存」中,需要先从磁盘「读入内存」,然后再「返回」
2.执行器拿到引擎给的行数据,把这个值加上 1,比如原来是 N,现在就是 N+1,得到新的一行数据,再调用引擎接口「写入这行新数据」
3.引擎将这行新数据更新到内存中,同时将这个更新操作「记录到 redo log 里面」,此时 redo log 处于 「prepare」 状态。然后告知执行器执行完成了,随时可以提交事务
4.执行器「生成这个操作的 binlog」,并把 binlog 「写入磁盘」
5.执行器调用引擎的「提交事务」接口,引擎把刚刚写入的 redo log 改成提交(commit)状态,「更新完成」
什么时候适合用索引?什么时候不适合?
适合:
字段有唯一性
where条件经常用到的字段
group by和order by经常用到的字段
where条件经常用到的字段
group by和order by经常用到的字段
不适合:
字段频繁变化
字段数据大量重复
不经常用的字段
数据太少
字段数据大量重复
不经常用的字段
数据太少
执行计划的参数有哪些?
possible_keys 字段表示可能用到的索引;
key 字段表示实际用的索引,如果这一项为 NULL,说明没有使用索引;
key_len 表示索引的长度;
rows 表示扫描的数据行数。
type 表示数据扫描类型
key 字段表示实际用的索引,如果这一项为 NULL,说明没有使用索引;
key_len 表示索引的长度;
rows 表示扫描的数据行数。
type 表示数据扫描类型
type 字段就是描述了找到所需数据时使用的扫描方式是什么,常见扫描类型的执行效率从低到高的顺序为:
ALL(全表扫描);
index(全索引扫描);
range(索引范围扫描);
ref(非唯一索引扫描);
eq_ref(唯一索引扫描);
const(结果只有一条的主键或唯一索引扫描)。
ALL(全表扫描);
index(全索引扫描);
range(索引范围扫描);
ref(非唯一索引扫描);
eq_ref(唯一索引扫描);
const(结果只有一条的主键或唯一索引扫描)。
WAl 是什么?有什么好处?
WAL 就是 Write-Ahead Logging,其实就是「所有的修改都先被写入到日志中,然后再写磁盘」,用于保证数据操作的原子性和持久性。
好处:
1.「读和写可以完全地并发执行」,不会互相阻塞
2.先写入 log 中,磁盘写入从「随机写变为顺序写」,降低了 client 端的延迟就。并且,由于顺序写入大概率是在一个磁盘块内,这样产生的 io 次数也大大降低
3.写入日志当数据库崩溃的时候「可以使用日志来恢复磁盘数据」
1.「读和写可以完全地并发执行」,不会互相阻塞
2.先写入 log 中,磁盘写入从「随机写变为顺序写」,降低了 client 端的延迟就。并且,由于顺序写入大概率是在一个磁盘块内,这样产生的 io 次数也大大降低
3.写入日志当数据库崩溃的时候「可以使用日志来恢复磁盘数据」
mysql索引优化?
SQL书写规范?
1. 基本规则
SQL 可以写在一行或者多行。为了提高可读性,各子句分行写,必要时使用缩进
每条命令以 ; 或 \g 或 \G 结束
关键字不能被缩写也不能分行
关于标点符号
必须保证所有的()、单引号、双引号是成对结束的
必须使用英文状态下的半角输入方式
字符串型和日期时间类型的数据可以使用单引号(' ')表示
列的别名,尽量使用双引号(" "),而且不建议省略as
2. SQL大小写规范
MySQL 在 Windows 环境下是大小写不敏感的
MySQL 在 Linux 环境下是大小写敏感的
数据库名、表名、表的别名、变量名是严格区分大小写的
关键字、函数名、列名(或字段名)、列的别名(字段的别名) 是忽略大小写的。
推荐采用统一的书写规范:
数据库名、表名、表别名、字段名、字段别名等都小写
SQL 关键字、函数名、绑定变量等都大写
3. 命名规则
数据库、表名不得超过30个字符,变量名限制为29个
必须只能包含 A–Z, a–z, 0–9, _共63个字符
数据库名、表名、字段名等对象名中间不要包含空格
同一个MySQL软件中,数据库不能同名;同一个库中,表不能重名同一个表中,字段不能重名
必须保证你的字段没有和保留字、数据库系统或常用方法冲突。如果坚持使用,请在SQL语句中使 用`(着重号)引起来
保持字段名和类型的一致性,在命名字段并为其指定数据类型的时候一定要保证一致性。假如数据 类型在一个表里是整数,那在另一个表里可就别变成字符型了
SQL 可以写在一行或者多行。为了提高可读性,各子句分行写,必要时使用缩进
每条命令以 ; 或 \g 或 \G 结束
关键字不能被缩写也不能分行
关于标点符号
必须保证所有的()、单引号、双引号是成对结束的
必须使用英文状态下的半角输入方式
字符串型和日期时间类型的数据可以使用单引号(' ')表示
列的别名,尽量使用双引号(" "),而且不建议省略as
2. SQL大小写规范
MySQL 在 Windows 环境下是大小写不敏感的
MySQL 在 Linux 环境下是大小写敏感的
数据库名、表名、表的别名、变量名是严格区分大小写的
关键字、函数名、列名(或字段名)、列的别名(字段的别名) 是忽略大小写的。
推荐采用统一的书写规范:
数据库名、表名、表别名、字段名、字段别名等都小写
SQL 关键字、函数名、绑定变量等都大写
3. 命名规则
数据库、表名不得超过30个字符,变量名限制为29个
必须只能包含 A–Z, a–z, 0–9, _共63个字符
数据库名、表名、字段名等对象名中间不要包含空格
同一个MySQL软件中,数据库不能同名;同一个库中,表不能重名同一个表中,字段不能重名
必须保证你的字段没有和保留字、数据库系统或常用方法冲突。如果坚持使用,请在SQL语句中使 用`(着重号)引起来
保持字段名和类型的一致性,在命名字段并为其指定数据类型的时候一定要保证一致性。假如数据 类型在一个表里是整数,那在另一个表里可就别变成字符型了
系统内存飙到100%怎么去排查?
binlog 是做什么的?
binlog 是归档日志,属于 Server 层的日志,是一个二进制格式的文件,用于「记录用户对数据库更新的SQL语句信息」。
主要作用
主从复制
数据恢复
主从复制
数据恢复
undolog 是做什么的?
undolog 是 InnoDB 存储引擎的日志,用于保证数据的原子性,「保存了事务发生之前的数据的一个版本,也就是说记录的是数据是修改之前的数据,可以用于回滚」,同时可以提供多版本并发控制下的读(MVCC)。
主要作用
事务回滚
实现多版本控制(MVCC)
事务回滚
实现多版本控制(MVCC)
relaylog 是做什么的?
relaylog 是中继日志,「在主从同步的时候使用到」,它是一个中介临时的日志文件,用于存储从master节点同步过来的binlog日志内容。
master 主节点的 binlog 传到 slave 从节点后,被写入 relay log 里,从节点的 slave sql 线程从 relaylog 里读取日志然后应用到 slave 从节点本地。从服务器 I/O 线程将主服务器的二进制日志读取过来记录到从服务器本地文件,然后 SQL 线程会读取 relay-log 日志的内容并应用到从服务器,从而「使从服务器和主服务器的数据保持一致」。
redolog 是做什么的?
redolog 是 「InnoDB 存储引擎所特有的一种日志」,用于记录事务操作的变化,记录的是数据修改之后的值,不管事务是否提交都会记录下来。
可以做「数据恢复并且提供 crash-safe 能力」
当有增删改相关的操作时,会先记录到 Innodb 中,并修改缓存页中的数据,「等到 mysql 闲下来的时候才会真正的将 redolog 中的数据写入到磁盘当中」。
redolog 和 binlog 的区别是什么?
1.「redolog」 是 「Innodb」 独有的日志,而 「binlog」 是 「server」 层的,所有的存储引擎都有使用到
2.「redolog」 记录了「具体的数值」,对某个页做了什么修改,「binlog」 记录的「操作内容」
3.「binlog」 大小达到上限或者 flush log 「会生成一个新的文件」,而 「redolog」 有固定大小「只能循环利用」
4.「binlog 日志没有 crash-safe 的能力」,只能用于归档。而 redo log 有 crash-safe 能力。
说一说 mvcc 吧,有什么作用?
MVCC:多版本并发控制,是现代数据库(包括 MySQL、Oracle、PostgreSQL 等)引擎实现中常用的处理读写冲突的手段,目的在于「提高数据库高并发场景下的吞吐性能」。
在 MVCC 协议下,每个读操作会看到一个一致性的快照,「这个快照是基于整个库的」,并且可以实现非阻塞的读,用于「支持读提交和可重复读隔离级别的实现」
MVCC 允许数据具有多个版本,这个版本可以是时间戳或者是全局递增的事务 ID,在同一个时间点,不同的事务看到的数据是不同的,这个修改的数据是「记录在 undolog 中」的。
失效场景:两个连续的快照读中间有个当前读
当前读和快照读?
快照读就是最普通的Select查询语句
当前读指执行insert,update,delete,select 。。。for update,select。。。lock in share mode语句时进行读取数据的方式
那你知道什么是间隙锁吗?
间隙锁是可重复读级别下才会有的锁,结合MVCC和间隙锁可以解决幻读的问题。
需要注意的是唯⼀索引是不会有间隙索引的。
Mysql 主从之间是怎么同步数据的?
1.master 主库将此次更新的事件类型「写入到主库的 binlog 文件」中
2.master 「创建 log dump 线程通知 slave」 需要更新数据
3.「slave」 向 master 节点发送请求,「将该 binlog 文件内容存到本地的 relaylog 中」
4.「slave 开启 sql 线程」读取 relaylog 中的内容,「将其中的内容在本地重新执行一遍」,完成主从数据同步
2.master 「创建 log dump 线程通知 slave」 需要更新数据
3.「slave」 向 master 节点发送请求,「将该 binlog 文件内容存到本地的 relaylog 中」
4.「slave 开启 sql 线程」读取 relaylog 中的内容,「将其中的内容在本地重新执行一遍」,完成主从数据同步
「同步策略」:
1.「全同步复制」:主库强制同步日志到从库,等全部从库执行完才返回客户端,性能差
2.「半同步复制」:主库收到至少一个从库确认就认为操作成功,从库写入日志成功返回ack确认
1.「全同步复制」:主库强制同步日志到从库,等全部从库执行完才返回客户端,性能差
2.「半同步复制」:主库收到至少一个从库确认就认为操作成功,从库写入日志成功返回ack确认
主从延迟要怎么解决?
1.MySQL 5.6 版本以后,提供了一种「并行复制」的方式,通过将 SQL 线程转换为多个 work 线程来进行重放
2.「提高机器配置」(王道)
3.在业务初期就选择合适的分库、分表策略,「避免单表单库过大」带来额外的复制压力
4.「避免长事务」
5.「避免让数据库进行各种大量运算」
6.对于一些对延迟很敏感的业务「直接使用主库读」
2.「提高机器配置」(王道)
3.在业务初期就选择合适的分库、分表策略,「避免单表单库过大」带来额外的复制压力
4.「避免长事务」
5.「避免让数据库进行各种大量运算」
6.对于一些对延迟很敏感的业务「直接使用主库读」
删除表数据后表的大小却没有变动,这是为什么?
在使用 delete 删除数据时,其实对应的数据行并不是真正的删除,是「逻辑删除」,InnoDB 仅仅是将其「标记成可复用的状态」,所以表空间不会变小
为什么 VarChar 建议不要超过255?
当定义varchar长度小于等于255时,长度标识位需要一个字节(utf-8编码)
当大于255时,长度标识位需要两个字节,并且建立的「索引也会失效」
Mysql 中有哪些锁?
mysql锁分为共享锁和排他锁,也叫做读锁和写锁。
读锁是共享的,可以通过lock in share mode实现,这时候只能读不能写。
写锁是排他的,它会阻塞其他的写锁和读锁。从颗粒度来区分,可以分为表锁和⾏锁两种。
表锁会锁定整张表并且阻塞其他⽤户对该表的所有读写操作,⽐如alter修改表结构的时候会锁表。
⾏锁⼜可以分为乐观锁和悲观锁,悲观锁可以通过for update实现,乐观锁则通过版本号实现。
读锁是共享的,可以通过lock in share mode实现,这时候只能读不能写。
写锁是排他的,它会阻塞其他的写锁和读锁。从颗粒度来区分,可以分为表锁和⾏锁两种。
表锁会锁定整张表并且阻塞其他⽤户对该表的所有读写操作,⽐如alter修改表结构的时候会锁表。
⾏锁⼜可以分为乐观锁和悲观锁,悲观锁可以通过for update实现,乐观锁则通过版本号实现。
为什么不要使用长事务?
1.并发情况下,数据库「连接池容易被撑爆」
2.「容易造成大量的阻塞和锁超时」
长事务还占用锁资源,也可能拖垮整个库,
3.执行时间长,容易造成「主从延迟」
4.「回滚所需要的时间比较长」
事务越长整个时间段内的事务也就越多
5.「undolog 日志越来越大」
长事务意味着系统里面会存在很老的事务视图。由于这些事务随时可能访问数据库里面的任何数据,所以这个事务提交之前,数据库里面它可能用到的回滚记录都必须保留,这就会导致大量占用存储空间
2.「容易造成大量的阻塞和锁超时」
长事务还占用锁资源,也可能拖垮整个库,
3.执行时间长,容易造成「主从延迟」
4.「回滚所需要的时间比较长」
事务越长整个时间段内的事务也就越多
5.「undolog 日志越来越大」
长事务意味着系统里面会存在很老的事务视图。由于这些事务随时可能访问数据库里面的任何数据,所以这个事务提交之前,数据库里面它可能用到的回滚记录都必须保留,这就会导致大量占用存储空间
buffer pool 是做什么的?
buffer pool 是一块内存区域,为了「提高数据库的性能」,当数据库操作数据的时候,把硬盘上的数据加载到 buffer pool,不直接和硬盘打交道,操作的是 buffer pool 里面的数据,数据库的增删改查都是在 buffer pool 上进行
buffer pool 里面缓存的数据内容也是一个个数据页
其中「有三大双向链表」:
「free 链表」
用于帮助我们找到空闲的缓存页
「flush 链表」
用于找到脏缓存页,也就是需要刷盘的缓存页
「lru 链表」
用来淘汰不常被访问的缓存页,分为热数据区和冷数据区,冷数据区主要存放那些不常被用到的数据
「free 链表」
用于帮助我们找到空闲的缓存页
「flush 链表」
用于找到脏缓存页,也就是需要刷盘的缓存页
「lru 链表」
用来淘汰不常被访问的缓存页,分为热数据区和冷数据区,冷数据区主要存放那些不常被用到的数据
预读机制:
Buffer Pool 有一项特技叫预读,存储引擎的接口在被 Server 层调用时,会在响应的同时进行预判,将下次可能用到的数据和索引加载到 Buffer Pool
Buffer Pool 有一项特技叫预读,存储引擎的接口在被 Server 层调用时,会在响应的同时进行预判,将下次可能用到的数据和索引加载到 Buffer Pool
分表后⾮sharding_key的查询怎么处理呢?
1. 可以做⼀个mapping表,⽐如这时候商家要查询订单列表怎么办呢?不带user_id查询的话你总不
能扫全表吧?所以我们可以做⼀个映射关系表,保存商家和⽤户的关系,查询的时候先通过商家查
询到⽤户列表,再通过user_id去查询。
2. 打宽表,⼀般⽽⾔,商户端对数据实时性要求并不是很⾼,⽐如查询订单列表,可以把订单表同步
到离线(实时)数仓,再基于数仓去做成⼀张宽表,再基于其他如es提供查询服务。
3. 数据量不是很⼤的话,⽐如后台的⼀些查询之类的,也可以通过多线程扫表,然后再聚合结果的⽅
式来做。或者异步的形式也是可以的。
能扫全表吧?所以我们可以做⼀个映射关系表,保存商家和⽤户的关系,查询的时候先通过商家查
询到⽤户列表,再通过user_id去查询。
2. 打宽表,⼀般⽽⾔,商户端对数据实时性要求并不是很⾼,⽐如查询订单列表,可以把订单表同步
到离线(实时)数仓,再基于数仓去做成⼀张宽表,再基于其他如es提供查询服务。
3. 数据量不是很⼤的话,⽐如后台的⼀些查询之类的,也可以通过多线程扫表,然后再聚合结果的⽅
式来做。或者异步的形式也是可以的。
Mysql存储流程?
MyBatis
什么是MyBatis
(1)Mybatis是一个半ORM(对象关系映射)框架,它内部封装了JDBC,开发时只需要关注SQL
语句本身,不需要花费精力去处理加载驱动、创建连接、创建statement等繁杂的过程。程序员直
接编写原生态sql,可以严格控制sql执行性能,灵活度高。
(2)MyBatis 可以使用 XML 或注解来配置和映射原生信息,将 POJO映射成数据库中的记录,避
免了几乎所有的 JDBC 代码和手动设置参数以及获取结果集。
(3)通过xml 文件或注解的方式将要执行的各种 statement 配置起来,并通过java对象和
statement中sql的动态参数进行映射生成最终执行的sql语句,最后由mybatis框架执行sql并将结果
映射为java对象并返回。(从执行sql到返回result的过程)
语句本身,不需要花费精力去处理加载驱动、创建连接、创建statement等繁杂的过程。程序员直
接编写原生态sql,可以严格控制sql执行性能,灵活度高。
(2)MyBatis 可以使用 XML 或注解来配置和映射原生信息,将 POJO映射成数据库中的记录,避
免了几乎所有的 JDBC 代码和手动设置参数以及获取结果集。
(3)通过xml 文件或注解的方式将要执行的各种 statement 配置起来,并通过java对象和
statement中sql的动态参数进行映射生成最终执行的sql语句,最后由mybatis框架执行sql并将结果
映射为java对象并返回。(从执行sql到返回result的过程)
说说MyBatis的优点和缺点
优点:
(1)基于SQL语句编程,相当灵活,不会对应用程序或者数据库的现有设计造成任何影响,SQL写 在XML里,解除sql与程序代码的耦合,便于统一管理;提供XML标签,支持编写动态SQL语句,并
可重用。
(2)与JDBC相比,减少了50%以上的代码量,消除了JDBC大量冗余的代码,不需要手动开关连
接;
(3)很好的与各种数据库兼容(因为MyBatis使用JDBC来连接数据库,所以只要JDBC支持的数据
库MyBatis都支持)。
(4)能够与Spring很好的集成;
(5)提供映射标签,支持对象与数据库的ORM字段关系映射;提供对象关系映射标签,支持对象
关系组件维护。
缺点
(1)SQL语句的编写工作量较大,尤其当字段多、关联表多时,对开发人员编写SQL语句的功底有
一定要求。
(2)SQL语句依赖于数据库,导致数据库移植性差,不能随意更换数据库。
(1)基于SQL语句编程,相当灵活,不会对应用程序或者数据库的现有设计造成任何影响,SQL写 在XML里,解除sql与程序代码的耦合,便于统一管理;提供XML标签,支持编写动态SQL语句,并
可重用。
(2)与JDBC相比,减少了50%以上的代码量,消除了JDBC大量冗余的代码,不需要手动开关连
接;
(3)很好的与各种数据库兼容(因为MyBatis使用JDBC来连接数据库,所以只要JDBC支持的数据
库MyBatis都支持)。
(4)能够与Spring很好的集成;
(5)提供映射标签,支持对象与数据库的ORM字段关系映射;提供对象关系映射标签,支持对象
关系组件维护。
缺点
(1)SQL语句的编写工作量较大,尤其当字段多、关联表多时,对开发人员编写SQL语句的功底有
一定要求。
(2)SQL语句依赖于数据库,导致数据库移植性差,不能随意更换数据库。
#{}和${}的区别是什么?
#{}是预编译处理,${}是字符串替换。
Mybatis在处理#{}时,会将sql中的#{}替换为?号,调用PreparedStatement的set方法来赋值;
Mybatis在处理${}时,就是把${}替换成变量的值。
使用#{}可以有效的防止SQL注入,提高系统安全性。
Mybatis在处理#{}时,会将sql中的#{}替换为?号,调用PreparedStatement的set方法来赋值;
Mybatis在处理${}时,就是把${}替换成变量的值。
使用#{}可以有效的防止SQL注入,提高系统安全性。
当实体类中的属性名和表中的字段名不一样 ,怎么办 ?
第1种: 通过在查询的sql语句中定义字段名的别名,让字段名的别名和实体类的属性名一致。
第2种: 通过来映射字段名和实体类属性名的一一对应的关系。
Mybatis是如何进行分页的?分页插件的原理是什么?
Mybatis使用RowBounds对象进行分页,它是针对ResultSet结果集执行的内存分页,而非物理分
页。可以在sql内直接拼写带有物理分页的参数来完成物理分页功能,也可以使用分页插件来完成物
理分页,比如:MySQL数据的时候,在原有SQL后面拼写limit。
分页插件的基本原理是使用Mybatis提供的插件接口,实现自定义插件,在插件的拦截方法内拦截
待执行的sql,然后重写sql,根据dialect方言,添加对应的物理分页语句和物理分页参数。
页。可以在sql内直接拼写带有物理分页的参数来完成物理分页功能,也可以使用分页插件来完成物
理分页,比如:MySQL数据的时候,在原有SQL后面拼写limit。
分页插件的基本原理是使用Mybatis提供的插件接口,实现自定义插件,在插件的拦截方法内拦截
待执行的sql,然后重写sql,根据dialect方言,添加对应的物理分页语句和物理分页参数。
Mybatis是如何将sql执行结果封装为目标对象并返回的?都有
哪些映射形式?
哪些映射形式?
第一种是使用标签,逐一定义数据库列名和对象属性名之间的映射关系。
第二种是使用sql列的别名功能,将列的别名书写为对象属性名。
有了列名与属性名的映射关系后,Mybatis通过反射创建对象,同时使用反射给对象的属性逐一赋
值并返回,那些找不到映射关系的属性,是无法完成赋值的。
第二种是使用sql列的别名功能,将列的别名书写为对象属性名。
有了列名与属性名的映射关系后,Mybatis通过反射创建对象,同时使用反射给对象的属性逐一赋
值并返回,那些找不到映射关系的属性,是无法完成赋值的。
Xml映射文件中,除了常见的select|insert|updae|delete
标签之外,还有哪些标签?
标签之外,还有哪些标签?
加上动态sql的9个标签,其中为sql片段标签,通过标签引入sql片段,为不支持自增的主键生成策
略标签
略标签
MyBatis实现一对一有几种方式?具体怎么操作的?
有联合查询和嵌套查询,联合查询是几个表联合查询,只查询一次, 通过在resultMap里面配置
association节点配置一对一的类就可以完成;
嵌套查询是先查一个表,根据这个表里面的结果的 外键id,去再另外一个表里面查询数据,也是通过
association配置,但另外一个表的查询通过select属性配置。
association节点配置一对一的类就可以完成;
嵌套查询是先查一个表,根据这个表里面的结果的 外键id,去再另外一个表里面查询数据,也是通过
association配置,但另外一个表的查询通过select属性配置。
Mybatis是否支持延迟加载?如果支持,它的实现原理是什
么?
么?
Mybatis仅支持association关联对象和collection关联集合对象的延迟加载,association指的就是一
对一,collection指的就是一对多查询。在Mybatis配置文件中,可以配置是否启用延迟加载
lazyLoadingEnabled=true|false。
它的原理是,使用CGLIB创建目标对象的代理对象,当调用目标方法时,进入拦截器方法,比如调
用a.getB().getName(),拦截器invoke()方法发现a.getB()是null值,那么就会单独发送事先保存好的
查询关联B对象的sql,把B查询上来,然后调用a.setB(b),于是a的对象b属性就有值了,接着完成
a.getB().getName()方法的调用。这就是延迟加载的基本原理。
当然了,不光是Mybatis,几乎所有的包括Hibernate,支持延迟加载的原理都是一样的。
对一,collection指的就是一对多查询。在Mybatis配置文件中,可以配置是否启用延迟加载
lazyLoadingEnabled=true|false。
它的原理是,使用CGLIB创建目标对象的代理对象,当调用目标方法时,进入拦截器方法,比如调
用a.getB().getName(),拦截器invoke()方法发现a.getB()是null值,那么就会单独发送事先保存好的
查询关联B对象的sql,把B查询上来,然后调用a.setB(b),于是a的对象b属性就有值了,接着完成
a.getB().getName()方法的调用。这就是延迟加载的基本原理。
当然了,不光是Mybatis,几乎所有的包括Hibernate,支持延迟加载的原理都是一样的。
说说Mybatis的缓存机制
一级缓存localCache
在应用运行过程中,我们有可能在一次数据库会话中,执行多次查询条件完全相同的 SQL,
MyBatis 提供了一级缓存的方案优化这部分场景,如果是相同的 SQL 语句,会优先命中一级缓存,
避免直接对数据库进行查询,提高性能。
每个 SqlSession 中持有了 Executor,每个 Executor 中有一个 LocalCache。当用户发起查询时,
MyBatis 根据当前执行的语句生成 MappedStatement,在 Local Cache 进行查询,如果缓存命中
的话,直接返回结果给用户,如果缓存没有命中的话,查询数据库,结果写入 Local Cache,最后
返回结果给用户
在应用运行过程中,我们有可能在一次数据库会话中,执行多次查询条件完全相同的 SQL,
MyBatis 提供了一级缓存的方案优化这部分场景,如果是相同的 SQL 语句,会优先命中一级缓存,
避免直接对数据库进行查询,提高性能。
每个 SqlSession 中持有了 Executor,每个 Executor 中有一个 LocalCache。当用户发起查询时,
MyBatis 根据当前执行的语句生成 MappedStatement,在 Local Cache 进行查询,如果缓存命中
的话,直接返回结果给用户,如果缓存没有命中的话,查询数据库,结果写入 Local Cache,最后
返回结果给用户
1. MyBatis 一级缓存的生命周期和 SqlSession 一致。
2. MyBatis 一级缓存内部设计简单,只是一个没有容量限定的 HashMap,在缓存的功能性上有
所欠缺。
3. MyBatis 的一级缓存最大范围是 SqlSession 内部,有多个 SqlSession 或者分布式的环境下,
数据库写操作会引起脏数据,建议设定缓存级别为 Statement。
2. MyBatis 一级缓存内部设计简单,只是一个没有容量限定的 HashMap,在缓存的功能性上有
所欠缺。
3. MyBatis 的一级缓存最大范围是 SqlSession 内部,有多个 SqlSession 或者分布式的环境下,
数据库写操作会引起脏数据,建议设定缓存级别为 Statement。
二级缓存
在上文中提到的一级缓存中,其最大的共享范围就是一个 SqlSession 内部,如果多个 SqlSession
之间需要共享缓存,则需要使用到二级缓存。开启二级缓存后,会使用 CachingExecutor 装饰
Executor,进入一级缓存的查询流程前,先在 CachingExecutor 进行二级缓存的查询
在上文中提到的一级缓存中,其最大的共享范围就是一个 SqlSession 内部,如果多个 SqlSession
之间需要共享缓存,则需要使用到二级缓存。开启二级缓存后,会使用 CachingExecutor 装饰
Executor,进入一级缓存的查询流程前,先在 CachingExecutor 进行二级缓存的查询
二级缓存开启后,同一个 namespace 下的所有操作语句,都影响着同一个 Cache,即二级缓存被
多个 SqlSession 共享,是一个全局的变量。
当开启缓存后,数据的查询执行的流程为:
二级缓存 -> 一级缓存 -> 数据库
1. MyBatis 的二级缓存相对于一级缓存来说,实现了 SqlSession 之间缓存数据的共享,同时粒度
更加细,能够到 namespace 级别,通过 Cache 接口实现类不同的组合,对 Cache 的可控性
也更强。
2. MyBatis 在多表查询时,极大可能会出现脏数据,有设计上的缺陷,安全使用二级缓存的条件
比较苛刻。
3. 在分布式环境下,由于默认的 MyBatis Cache 实现都是基于本地的,分布式环境下必然会出现
读取到脏数据,需要使用集中式缓存将 MyBatis 的 Cache 接口实现,有一定的开发成本,直
接使用 Redis、Memcached 等分布式缓存可能成本更低,安全性也更高。
多个 SqlSession 共享,是一个全局的变量。
当开启缓存后,数据的查询执行的流程为:
二级缓存 -> 一级缓存 -> 数据库
1. MyBatis 的二级缓存相对于一级缓存来说,实现了 SqlSession 之间缓存数据的共享,同时粒度
更加细,能够到 namespace 级别,通过 Cache 接口实现类不同的组合,对 Cache 的可控性
也更强。
2. MyBatis 在多表查询时,极大可能会出现脏数据,有设计上的缺陷,安全使用二级缓存的条件
比较苛刻。
3. 在分布式环境下,由于默认的 MyBatis Cache 实现都是基于本地的,分布式环境下必然会出现
读取到脏数据,需要使用集中式缓存将 MyBatis 的 Cache 接口实现,有一定的开发成本,直
接使用 Redis、Memcached 等分布式缓存可能成本更低,安全性也更高。
JDBC 编程有哪些步骤?
1.装载相应的数据库的 JDBC 驱动并进行初始化:
2.建立 JDBC 和数据库之间的 Connection 连接:
Class.forName("com.mysql.jdbc.Driver");
Connection c = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/test?
characterEncoding=UTF-8", "root", "123456");
3.创建 Statement 或者 PreparedStatement 接口,执行 SQL 语句。
4.处理和显示结果。
5.释放资源
2.建立 JDBC 和数据库之间的 Connection 连接:
Class.forName("com.mysql.jdbc.Driver");
Connection c = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/test?
characterEncoding=UTF-8", "root", "123456");
3.创建 Statement 或者 PreparedStatement 接口,执行 SQL 语句。
4.处理和显示结果。
5.释放资源
MyBatis 中比如 UserMapper.java 是接口,为什么没有实
现类还能调用?
现类还能调用?
使用JDK动态代理+MapperProxy。本质上调用的是MapperProxy的invoke方法。
MyBatis的执行流程?
读取MyBatis的配置文件。mybatis-config.xml为MyBatis的全局配置文件,用于配置数据库连接信息。
加载映射文件。映射文件即SQL映射文件,该文件中配置了操作数据库的SQL语句,需要在MyBatis配置文件mybatis-config.xml中加载。mybatis-config.xml 文件可以加载多个映射文件,每个文件对应数据库中的一张表。
构造会话工厂。通过MyBatis的环境配置信息构建会话工厂SqlSessionFactory。
创建会话对象。由会话工厂创建SqlSession对象,该对象中包含了执行SQL语句的所有方法。
Executor执行器。MyBatis底层定义了一个Executor接口来操作数据库,它将根据SqlSession传递的参数动态地生成需要执行的SQL语句,同时负责查询缓存的维护。
MappedStatement对象。在Executor接口的执行方法中有一个MappedStatement类型的参数,该参数是对映射信息的封装,用于存储要映射的SQL语句的id、参数等信息。
输入参数映射。输入参数类型可以是Map、List等集合类型,也可以是基本数据类型和POJO类型。输入参数映射过程类似于JDBC对preparedStatement对象设置参数的过程。
输出结果映射。输出结果类型可以是Map、List等集合类型,也可以是基本数据类型和POJO类型。输出结果映射过程类似于JDBC对结果集的解析过程。
加载映射文件。映射文件即SQL映射文件,该文件中配置了操作数据库的SQL语句,需要在MyBatis配置文件mybatis-config.xml中加载。mybatis-config.xml 文件可以加载多个映射文件,每个文件对应数据库中的一张表。
构造会话工厂。通过MyBatis的环境配置信息构建会话工厂SqlSessionFactory。
创建会话对象。由会话工厂创建SqlSession对象,该对象中包含了执行SQL语句的所有方法。
Executor执行器。MyBatis底层定义了一个Executor接口来操作数据库,它将根据SqlSession传递的参数动态地生成需要执行的SQL语句,同时负责查询缓存的维护。
MappedStatement对象。在Executor接口的执行方法中有一个MappedStatement类型的参数,该参数是对映射信息的封装,用于存储要映射的SQL语句的id、参数等信息。
输入参数映射。输入参数类型可以是Map、List等集合类型,也可以是基本数据类型和POJO类型。输入参数映射过程类似于JDBC对preparedStatement对象设置参数的过程。
输出结果映射。输出结果类型可以是Map、List等集合类型,也可以是基本数据类型和POJO类型。输出结果映射过程类似于JDBC对结果集的解析过程。
springCloud
springCloud的组键有哪些?
Eureka:注册中心,服务注册和发现
Ribbon:负载均衡,实现服务调用的负载均衡
Hystrix:熔断器
Feign:远程调用
Zuul:网关
Spring Cloud Config:配置中心
Ribbon:负载均衡,实现服务调用的负载均衡
Hystrix:熔断器
Feign:远程调用
Zuul:网关
Spring Cloud Config:配置中心
SpringCloud使用的是那个版本?
我们使用的是:Hoxton.SR10,对应springboot对应的版本是2.3.9
springboot
1.SpringBoot启动类注解?它是由哪些注解组成?
@SpringBootConfiguration:组合了 @Configuration 注解,实现配置文件的功能。
@EnableAutoConfiguration:打开自动配置的功能,也可以关闭某个自动配置的选项。
@SpringBootApplication(exclude = { DataSourceAutoConfiguration.class })
@ComponentScan:Spring组件扫描
@EnableAutoConfiguration:打开自动配置的功能,也可以关闭某个自动配置的选项。
@SpringBootApplication(exclude = { DataSourceAutoConfiguration.class })
@ComponentScan:Spring组件扫描
2.SpringBoot启动方式?
1. 直接执行 main 方法运行
2. 命令行 java -jar 的方式 打包用命令或者放到容器中运行
3.用 Maven/ Gradle 插件运行
2. 命令行 java -jar 的方式 打包用命令或者放到容器中运行
3.用 Maven/ Gradle 插件运行
3.SpringBoot需要独立的容器运行?
不需要,内置了 Tomcat/Jetty。
4.SpringBoot自动配置原理?
@EnableAutoConfiguration (开启自动配置) 该注解引入了AutoConfigurationImportSelector,该类中
的方法会扫描所有存在META-INF/spring.factories的jar包。
的方法会扫描所有存在META-INF/spring.factories的jar包。
5.SpringBoot如何兼容Spring项目?
在启动类加:
@ImportResource(locations = {"classpath:spring.xml"})
@ImportResource(locations = {"classpath:spring.xml"})
SpringBoot生命周期
1、初始化环境变量
2、初始化环境变量完成
3、应用启动
4、应用已启动完成
5、应用刷新
6、应用停止
7、应用关闭
2、初始化环境变量完成
3、应用启动
4、应用已启动完成
5、应用刷新
6、应用停止
7、应用关闭
SpringBoot三大核心注解
Springboot三大核心注解是:Configuration,EnableAuto,ComponentScan
6.针对请求访问的几个组合注解?
@PatchMapping
@PostMapping
@GetMapping
@PutMapping
@DeleteMapping
@PostMapping
@GetMapping
@PutMapping
@DeleteMapping
7.编写测试用例的注解?
@SpringBootTest
8.SpringBoot异常处理相关注解?
@ControllerAdvice
@ExceptionHandler
@ExceptionHandler
9.SpringBoot读取配置相关注解有?
@PropertySource
@Value
@Environment
@ConfigurationProperties
@Value
@Environment
@ConfigurationProperties
10.SpringBoot Starter的工作原理
我个人理解SpringBoot就是由各种Starter组合起来的,我们自己也可以开发Starter
在sprinBoot启动时由@SpringBootApplication注解会自动去maven中读取每个starter中的spring.factories文件,该文件里配置了所有需要被创建spring容器中的bean,并且进行自动配置把bean注入SpringContext中 //(SpringContext是Spring的配置文件)
在sprinBoot启动时由@SpringBootApplication注解会自动去maven中读取每个starter中的spring.factories文件,该文件里配置了所有需要被创建spring容器中的bean,并且进行自动配置把bean注入SpringContext中 //(SpringContext是Spring的配置文件)
11.Spring Boot 中如何解决跨域问题 ?
跨域可以在前端通过 JSONP 来解决,但是 JSONP 只可以发送 GET 请求,无法发送其他类型的请求,在 RESTful 风格的应用中,就显得非常鸡肋,因此我们推荐在后端通过 (CORS,Crossorigin resource sharing) 来解决跨域问题。这种解决方案并非 Spring Boot 特有的,在传统的SSM 框架中,就可以通过 CORS 来解决跨域问题,只不过之前我们是在 XML 文件中配置 CORS ,现在可以通过实现WebMvcConfigurer接口然后重写addCorsMappings方法解决跨域问题。
如何快速搭建
springMVC
SpringMVC常用的注解?
第一个:@RequestParam注解
作用:是将请求参数绑定到你的控制器的方法参数上,是springmvc中的接收普通参数的注解,要好好的理解,在实际开发中,很常用的注解之一。
属性:value是请求参数中的名称。required是请求参数是否必须提供参数,它的默认是true,意思是表示必须提供,假如你不提供就会报错啦。
第二个:@RequestBody注解作用:如果作用在方法上,就表示该方法的返回结果是直接按写入的Http responsebody中(一般在异步获取数据时使用的注解)。
属性:required是否必须有请求体,它的默认也是true,在使用该注解时,值得注意的当为true时get的请求方式是报错的,如果你取值为false的话,get的请求是空的null。
第三个:@PathVaribale注解
作用:该注解是用于绑定url中的占位符,但是注意,spring3.0以后,url才开始支持占位符的,它是springmvc支持的rest风格url的一个重要的标志,什么是占位符呢,我去个例子比如:请求的url中/user/{id},这个{id}就是url的占位符。
第四个:@Controller注解
作用:该注解是用来标记一个类的,如果被一个类被标注为Controller的话,那么它就会被spring扫面机制扫面到,然后会自动将其注册为spring应用程序的上下文里的一个Bean,controller的真正作用是负责是处理由DispatcherServlet分发的请求,它把用户请求的数据经过业务处理层处理的,然后我们通过调用对应的Service,最后封装成一个Model,然后把Model返回给View显示到前端给用户的。
第五个:@RequsetMappring注解
作用:该注解的作用就是用来处理请求地址映射的,也就是说将其中的所有处理器方法都映射到url路径上,他有6个属性。
属性:
method:是让你指定请求的method的类型,比如常用的有get和post。value:是指请求的实际地址,如果是多个地址就用{}来指定就可以啦。produces:指定返回的内容类型,当request请求头中的Accept类型中包含指定的类型才可以返回的。consumes:指定处理请求的提交内容类型,比如一些json、html、text等的类型。headers:指定request中必须包含那些的headed值时,它才会用该方法处理请求的。params:该属性是指定request中一定要有的参数值,它才会使用该方法处理请求
作用:是将请求参数绑定到你的控制器的方法参数上,是springmvc中的接收普通参数的注解,要好好的理解,在实际开发中,很常用的注解之一。
属性:value是请求参数中的名称。required是请求参数是否必须提供参数,它的默认是true,意思是表示必须提供,假如你不提供就会报错啦。
第二个:@RequestBody注解作用:如果作用在方法上,就表示该方法的返回结果是直接按写入的Http responsebody中(一般在异步获取数据时使用的注解)。
属性:required是否必须有请求体,它的默认也是true,在使用该注解时,值得注意的当为true时get的请求方式是报错的,如果你取值为false的话,get的请求是空的null。
第三个:@PathVaribale注解
作用:该注解是用于绑定url中的占位符,但是注意,spring3.0以后,url才开始支持占位符的,它是springmvc支持的rest风格url的一个重要的标志,什么是占位符呢,我去个例子比如:请求的url中/user/{id},这个{id}就是url的占位符。
第四个:@Controller注解
作用:该注解是用来标记一个类的,如果被一个类被标注为Controller的话,那么它就会被spring扫面机制扫面到,然后会自动将其注册为spring应用程序的上下文里的一个Bean,controller的真正作用是负责是处理由DispatcherServlet分发的请求,它把用户请求的数据经过业务处理层处理的,然后我们通过调用对应的Service,最后封装成一个Model,然后把Model返回给View显示到前端给用户的。
第五个:@RequsetMappring注解
作用:该注解的作用就是用来处理请求地址映射的,也就是说将其中的所有处理器方法都映射到url路径上,他有6个属性。
属性:
method:是让你指定请求的method的类型,比如常用的有get和post。value:是指请求的实际地址,如果是多个地址就用{}来指定就可以啦。produces:指定返回的内容类型,当request请求头中的Accept类型中包含指定的类型才可以返回的。consumes:指定处理请求的提交内容类型,比如一些json、html、text等的类型。headers:指定request中必须包含那些的headed值时,它才会用该方法处理请求的。params:该属性是指定request中一定要有的参数值,它才会使用该方法处理请求
SpringMVC有哪些组件?
1:前端控制器(DispatcherServlet)
本质上是一个Servlet,相当于一个中转站,所有的访问都会走到这个Servlet中,再根据配置进行中转到
相应的Handler中进行处理,获取到数据和视图后,在使用相应视图做出响应。
2:处理器映射器(HandlerMapping)
本质上就是一段映射关系,将访问路径和对应的Handler存储为映射关系,在需要时供前端控制器查阅。
3:处理器适配器(HandlerAdapter)
本质上是一个适配器,可以根据要求找到对应的Handler来运行。前端控制器通过处理器映射器找到对应
的Handler信息之后,将请求响应和对应的Handler信息交由处理器适配器处理,处理器适配器找到真正
handler执行后,将结果即model和view返回给前端控制器
4:视图解析器(ViewResolver)
本质上也是一种映射关系,可以将视图名称映射到真正的视图地址。前端控制器调用处理器适配完成后得
到model和view,将view信息传给视图解析器得到真正的view。
5:视图渲染(View)
本质上就是将handler处理器中返回的model数据嵌入到视图解析器解析后得到的jsp页面中,向客户端做出响应。
本质上是一个Servlet,相当于一个中转站,所有的访问都会走到这个Servlet中,再根据配置进行中转到
相应的Handler中进行处理,获取到数据和视图后,在使用相应视图做出响应。
2:处理器映射器(HandlerMapping)
本质上就是一段映射关系,将访问路径和对应的Handler存储为映射关系,在需要时供前端控制器查阅。
3:处理器适配器(HandlerAdapter)
本质上是一个适配器,可以根据要求找到对应的Handler来运行。前端控制器通过处理器映射器找到对应
的Handler信息之后,将请求响应和对应的Handler信息交由处理器适配器处理,处理器适配器找到真正
handler执行后,将结果即model和view返回给前端控制器
4:视图解析器(ViewResolver)
本质上也是一种映射关系,可以将视图名称映射到真正的视图地址。前端控制器调用处理器适配完成后得
到model和view,将view信息传给视图解析器得到真正的view。
5:视图渲染(View)
本质上就是将handler处理器中返回的model数据嵌入到视图解析器解析后得到的jsp页面中,向客户端做出响应。
SpringMVC执行流程
1、用户发送请求到前端控制器(DispatcherServlet)。
2、前端控制器请求处理器映射器(HandlerMapping)去查找处理器(Handler)。
3、找到以后处理器映射器(HandlerMappering)向前端控制器返回执行链(HandlerExecutionChain)。
4、前端控制器(DispatcherServlet)调用处理器适配器(HandlerAdapter)去执行处理器(Handler)。
5、处理器适配器去执行Handler。
6、处理器执行完给处理器适配器返回ModelAndView。
7、处理器适配器向前端控制器返回ModelAndView。
8、前端控制器请求视图解析器(ViewResolver)去进行视图解析。
9、视图解析器向前端控制器返回View。
10、前端控制器对视图进行渲染。
11、前端控制器向用户响应结果。
2、前端控制器请求处理器映射器(HandlerMapping)去查找处理器(Handler)。
3、找到以后处理器映射器(HandlerMappering)向前端控制器返回执行链(HandlerExecutionChain)。
4、前端控制器(DispatcherServlet)调用处理器适配器(HandlerAdapter)去执行处理器(Handler)。
5、处理器适配器去执行Handler。
6、处理器执行完给处理器适配器返回ModelAndView。
7、处理器适配器向前端控制器返回ModelAndView。
8、前端控制器请求视图解析器(ViewResolver)去进行视图解析。
9、视图解析器向前端控制器返回View。
10、前端控制器对视图进行渲染。
11、前端控制器向用户响应结果。
spring
spring的启动流程?
笼统的说法
启动Spring时:
1. ⾸先会进⾏扫描,扫描得到所有的BeanDefinition对象,并存在⼀个Map中
2. 然后筛选出⾮懒加载的单例BeanDefinition进⾏创建Bean,对于多例Bean不需要在启动过程中去 进⾏创建,对于多例Bean会在每次获取Bean时利⽤BeanDefinition去创建
3. 利⽤BeanDefinition创建Bean就是Bean的创建⽣命周期,这期间包括了合并BeanDefinition、推 断构造⽅法、实例化、属性填充、初始化前、初始化、初始化后等步骤,其中AOP就是发⽣在初始 化后这⼀步骤中
4. 单例Bean创建完了之后,Spring会发布⼀个容器启动事件
5. Spring启动结束
启动Spring时:
1. ⾸先会进⾏扫描,扫描得到所有的BeanDefinition对象,并存在⼀个Map中
2. 然后筛选出⾮懒加载的单例BeanDefinition进⾏创建Bean,对于多例Bean不需要在启动过程中去 进⾏创建,对于多例Bean会在每次获取Bean时利⽤BeanDefinition去创建
3. 利⽤BeanDefinition创建Bean就是Bean的创建⽣命周期,这期间包括了合并BeanDefinition、推 断构造⽅法、实例化、属性填充、初始化前、初始化、初始化后等步骤,其中AOP就是发⽣在初始 化后这⼀步骤中
4. 单例Bean创建完了之后,Spring会发布⼀个容器启动事件
5. Spring启动结束
说一下你理解的 IOC 是什么?
首先 IOC 是一个「容器」,是用来装载对象的,它的核心思想就是「控制反转」
那么究竟「什么是控制反转」?
控制反转就是说,「把对象的控制权交给了 spring,由 spring 容器进行管理」,我们不进行任何操作
那么为「什么需要控制反转」?
我们想象一下,没有控制反转的时候,我们需要「自己去创建对象,配置对象」,还要「人工去处理对象与对象之间的各种复杂的依赖关系」,当一个工程的量起来之后,这种关系的维护是非常令人头痛的,所以就有了控制反转这个概念,将对象的创建、配置等一系列操作交给 spring 去管理,我们在使用的时候只要去取就好了
那么究竟「什么是控制反转」?
控制反转就是说,「把对象的控制权交给了 spring,由 spring 容器进行管理」,我们不进行任何操作
那么为「什么需要控制反转」?
我们想象一下,没有控制反转的时候,我们需要「自己去创建对象,配置对象」,还要「人工去处理对象与对象之间的各种复杂的依赖关系」,当一个工程的量起来之后,这种关系的维护是非常令人头痛的,所以就有了控制反转这个概念,将对象的创建、配置等一系列操作交给 spring 去管理,我们在使用的时候只要去取就好了
那么 DI 又是什么?
DI 就是依赖注入,其实和 IOC 大致相同,只不过是「同一个概念使用了不同的角度去阐述」
DI 所描述的「重点是在于依赖」,我们说了 「IOC 的核心功能就是在于在程序运行时动态的向某个对象提供其他的依赖对象」,而这个功能就是依靠 DI 去完成的,比如我们需要注入一个对象 A,而这个对象 A 依赖一个对象 B,那么我们就需要把这个对象 B 注入到对象 A 中,这就是依赖注入
依赖注入是指在程序运行期间,由外部容器动态地将依赖对象注入到组件中
spring 中有三种注入方式
接口注入
构造器注入
set注入
DI 所描述的「重点是在于依赖」,我们说了 「IOC 的核心功能就是在于在程序运行时动态的向某个对象提供其他的依赖对象」,而这个功能就是依靠 DI 去完成的,比如我们需要注入一个对象 A,而这个对象 A 依赖一个对象 B,那么我们就需要把这个对象 B 注入到对象 A 中,这就是依赖注入
依赖注入是指在程序运行期间,由外部容器动态地将依赖对象注入到组件中
spring 中有三种注入方式
接口注入
构造器注入
set注入
说说 AOP 是什么?
AOP面向切面编程,简单的理解就是,在不修改原有代码的基础上对某些功能进行增强。
通常用于,事务,日志,权限等
AOP 意为:「面向切面编程,通过预编译方式和运行期间动态代理实现程序功能的统一维护的一种技术」。
AOP 是 「OOP(面向对象编程) 的延续」,是 Spring 框架中的一个重要内容,是函数式编程的一种衍生范型。利用 AOP 可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。
「AOP 实现主要分为两类:」
「静态 AOP 实现」, AOP 框架「在编译阶段」对程序源代码进行修改,生成了静态的 AOP 代理类(生成的 *.class 文件已经被改掉了,需要使用特定的编译器),比如 AspectJ
「动态 AOP 实现」, AOP 框架「在运行阶段」对动态生成代理对象(在内存中以 JDK 动态代理,或 CGlib 动态地生成 AOP 代理类),如 SpringAOP
通常用于,事务,日志,权限等
AOP 意为:「面向切面编程,通过预编译方式和运行期间动态代理实现程序功能的统一维护的一种技术」。
AOP 是 「OOP(面向对象编程) 的延续」,是 Spring 框架中的一个重要内容,是函数式编程的一种衍生范型。利用 AOP 可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。
「AOP 实现主要分为两类:」
「静态 AOP 实现」, AOP 框架「在编译阶段」对程序源代码进行修改,生成了静态的 AOP 代理类(生成的 *.class 文件已经被改掉了,需要使用特定的编译器),比如 AspectJ
「动态 AOP 实现」, AOP 框架「在运行阶段」对动态生成代理对象(在内存中以 JDK 动态代理,或 CGlib 动态地生成 AOP 代理类),如 SpringAOP
spring 中 AOP 的实现是「通过动态代理实现的」,如果是实现了接口就会使用 JDK 动态代理,否则就使用 CGLIB 代理。
JDK动态代理:只能代理接口,不能代理类
CGLIB代理:是通过继承的方式做的动态代理,如果类被final修饰它就无法使用CGLIB做动态代理
JDK动态代理:只能代理接口,不能代理类
CGLIB代理:是通过继承的方式做的动态代理,如果类被final修饰它就无法使用CGLIB做动态代理
「有 5 种通知类型:」
前置通知 Before advice:在连接点前面执行,前置通知不会影响连接点的执行,除非此处抛出异常
后置通知 After returning advice:在连接点正常执行完成后执行,如果连接点抛出异常,则不会执行
异常通知 After throwing advice:在连接点抛出异常后执行
最终通知 After (finally) advice:在连接点执行完成后执行,不管是正常执行完成,还是抛出异常,都会执行返回通知中的内容
环绕通知 Around advice:环绕通知围绕在连接点前后,能在方法调用前后自定义一些操作,还需要负责决定是继续处理 join point (调用 ProceedingJoinPoint 的 proceed 方法)还是中断执行
前置通知 Before advice:在连接点前面执行,前置通知不会影响连接点的执行,除非此处抛出异常
后置通知 After returning advice:在连接点正常执行完成后执行,如果连接点抛出异常,则不会执行
异常通知 After throwing advice:在连接点抛出异常后执行
最终通知 After (finally) advice:在连接点执行完成后执行,不管是正常执行完成,还是抛出异常,都会执行返回通知中的内容
环绕通知 Around advice:环绕通知围绕在连接点前后,能在方法调用前后自定义一些操作,还需要负责决定是继续处理 join point (调用 ProceedingJoinPoint 的 proceed 方法)还是中断执行
Spring中Bean的生命周期
它有四个阶段:实例化 -》属性赋值 -》 初始化 -》销毁
spring事务失效场景
1、service没有托管给spring
失效原因: spring事务生效的前提是,service必须是一个bean对象
解决方案: 将service注入spring
2、抛出受检异常
失效原因: spring默认只会回滚非检查异常和error异常
解决方案: 配置rollbackFor
3、业务自己捕获了异常
失效原因: spring事务只有捕捉到了业务抛出去的异常,才能进行后续的处理,如果业务自己捕获了异常,则事务无法感知
解决方案:
1、将异常原样抛出;
2、设置TransactionAspectSupport.currentTransactionStatus().setR ollbackOnly();
4、切面顺序导致
失效原因: spring事务切面的优先级顺序最低,但如果自定义的切面优先级和他一样,且自定义的切面没有正确处理异常,则会同业务自己捕获异常的那种场景一样
解决方案:
1、在切面中将异常原样抛出;
2、在切面中设置TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
5、非public方法
失效原因: spring事务默认生效的方法权限都必须为public
解决方案:
1、将方法改为public;
2、修改TansactionAttributeSource,将publicMethodsOnly改为false【这个从源码跟踪得出结论】
3、开启 AspectJ 代理模式【从spring文档得出结论】
6、父子容器
失效原因: 子容器扫描范围过大,将未加事务配置的serivce扫描进来
解决方案:
1、父子容器个扫个的范围;
2、不用父子容器,所有bean都交给同一容器管理
7、方法用final修饰
失效原因: 因为spring事务是用动态代理实现,因此如果方法使用了final修饰,则代理类无法对目标方法进行重写,植入事务功能
解决方案:
1、方法不要用final修饰
8、方法用static修饰
失效原因: 原因和final一样
解决方案:
1、方法不要用static修饰
9、调用本类方法
失效原因: 本类方法不经过代理,无法进行增强
解决方案:
1、注入自己来调用;
2、使用@EnableAspectJAutoProxy(exposeProxy = true) + AopContext.currentProxy()
10、场景十:多线程调用
失效原因: 因为spring的事务是通过数据库连接来实现,而数据库连接spring是放在threadLocal里面。同一个事务,只能用同一个数据库连接。而多线程场景下,拿到的数据库连接是不一样的,即是属于不同事务
11、场景十一:错误的传播行为
失效原因: 使用的传播特性不支持事务
12、场景十二:使用了不支持事务的存储引擎
失效原因: 使用了不支持事务的存储引擎。比如mysql中的MyISAM
13、场景十三:数据源没有配置事务管理器
注: 因为springboot,他默认已经开启事务管理器。org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration。因此示例略过
14、被代理的类过早实例化
失效原因: 当代理类的实例化早于AbstractAutoProxyCreator后置处理器,就无法被AbstractAutoProxyCreator后置处理器增强
失效原因: spring事务生效的前提是,service必须是一个bean对象
解决方案: 将service注入spring
2、抛出受检异常
失效原因: spring默认只会回滚非检查异常和error异常
解决方案: 配置rollbackFor
3、业务自己捕获了异常
失效原因: spring事务只有捕捉到了业务抛出去的异常,才能进行后续的处理,如果业务自己捕获了异常,则事务无法感知
解决方案:
1、将异常原样抛出;
2、设置TransactionAspectSupport.currentTransactionStatus().setR ollbackOnly();
4、切面顺序导致
失效原因: spring事务切面的优先级顺序最低,但如果自定义的切面优先级和他一样,且自定义的切面没有正确处理异常,则会同业务自己捕获异常的那种场景一样
解决方案:
1、在切面中将异常原样抛出;
2、在切面中设置TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
5、非public方法
失效原因: spring事务默认生效的方法权限都必须为public
解决方案:
1、将方法改为public;
2、修改TansactionAttributeSource,将publicMethodsOnly改为false【这个从源码跟踪得出结论】
3、开启 AspectJ 代理模式【从spring文档得出结论】
6、父子容器
失效原因: 子容器扫描范围过大,将未加事务配置的serivce扫描进来
解决方案:
1、父子容器个扫个的范围;
2、不用父子容器,所有bean都交给同一容器管理
7、方法用final修饰
失效原因: 因为spring事务是用动态代理实现,因此如果方法使用了final修饰,则代理类无法对目标方法进行重写,植入事务功能
解决方案:
1、方法不要用final修饰
8、方法用static修饰
失效原因: 原因和final一样
解决方案:
1、方法不要用static修饰
9、调用本类方法
失效原因: 本类方法不经过代理,无法进行增强
解决方案:
1、注入自己来调用;
2、使用@EnableAspectJAutoProxy(exposeProxy = true) + AopContext.currentProxy()
10、场景十:多线程调用
失效原因: 因为spring的事务是通过数据库连接来实现,而数据库连接spring是放在threadLocal里面。同一个事务,只能用同一个数据库连接。而多线程场景下,拿到的数据库连接是不一样的,即是属于不同事务
11、场景十一:错误的传播行为
失效原因: 使用的传播特性不支持事务
12、场景十二:使用了不支持事务的存储引擎
失效原因: 使用了不支持事务的存储引擎。比如mysql中的MyISAM
13、场景十三:数据源没有配置事务管理器
注: 因为springboot,他默认已经开启事务管理器。org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration。因此示例略过
14、被代理的类过早实例化
失效原因: 当代理类的实例化早于AbstractAutoProxyCreator后置处理器,就无法被AbstractAutoProxyCreator后置处理器增强
你们项目中为什么使用Spring框架?
轻量:Spring 是轻量的,基本的版本大约2MB。
控制反转:Spring通过控制反转实现了松散耦合,对象们给出它们的依赖,而不是创建或查找
依赖的对象们。
面向切面的编程(AOP):Spring支持面向切面的编程,并且把应用业务逻辑和系统服务分开。
容器:Spring 包含并管理应用中对象的生命周期和配置。
MVC框架:Spring的WEB框架是个精心设计的框架,是Web框架的一个很好的替代品。
事务管理:Spring 提供一个持续的事务管理接口,可以扩展到上至本地事务下至全局事务
(JTA)。
异常处理:Spring 提供方便的API把具体技术相关的异常(比如由JDBC,Hibernate or JDO抛
出的)转化为一致的unchecked 异常。
控制反转:Spring通过控制反转实现了松散耦合,对象们给出它们的依赖,而不是创建或查找
依赖的对象们。
面向切面的编程(AOP):Spring支持面向切面的编程,并且把应用业务逻辑和系统服务分开。
容器:Spring 包含并管理应用中对象的生命周期和配置。
MVC框架:Spring的WEB框架是个精心设计的框架,是Web框架的一个很好的替代品。
事务管理:Spring 提供一个持续的事务管理接口,可以扩展到上至本地事务下至全局事务
(JTA)。
异常处理:Spring 提供方便的API把具体技术相关的异常(比如由JDBC,Hibernate or JDO抛
出的)转化为一致的unchecked 异常。
spring 中都用到了哪些设计模式?
1、工厂模式:BeanFactory就是简单工厂模式的体现,用来创建对象的实例;
2、单例模式:Bean默认为单例模式。
3、代理模式:Spring的AOP功能用到了JDK的动态代理和CGLIB字节码生成技术;
4、模板方法:用来解决代码重复的问题。比如. RestTemplate, JmsTemplate, JpaTemplate。
5、观察者模式:定义对象键一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都会得到通知被制动更新,如Spring中listener的实现–ApplicationListener。
2、单例模式:Bean默认为单例模式。
3、代理模式:Spring的AOP功能用到了JDK的动态代理和CGLIB字节码生成技术;
4、模板方法:用来解决代码重复的问题。比如. RestTemplate, JmsTemplate, JpaTemplate。
5、观察者模式:定义对象键一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都会得到通知被制动更新,如Spring中listener的实现–ApplicationListener。
工厂模式
比如通过 BeanFactory 和 ApplicationContext 来生产 Bean 对象
适配器模式
Spring 中的 AOP 中 AdvisorAdapter 类,它有三个实现:
MethodBeforAdviceAdapter、AfterReturnningAdviceAdapter、ThrowsAdviceAdapter。Spring
会根据不同的 AOP 配置来使用对应的 Advice,与策略模式不同的是,一个方法可以同时拥有多个
Advice。Spring 存在很多以 Adapter 结尾的,大多数都是适配器模式。
MethodBeforAdviceAdapter、AfterReturnningAdviceAdapter、ThrowsAdviceAdapter。Spring
会根据不同的 AOP 配置来使用对应的 Advice,与策略模式不同的是,一个方法可以同时拥有多个
Advice。Spring 存在很多以 Adapter 结尾的,大多数都是适配器模式。
模板方法模式
Spring 中 jdbcTemplate 等以 Template 结尾的对数据库操作的类,都会使用到模板方法设计模式,一些通用的功能
包装器模式
我们的项目需要连接多个数据库,而且不同的客户在每次访问中根据需要会去访问不同的数据库。这种模式让我们可以根据客户的需求能够动态切换不同的数据源
在你项目中使用到了哪些设计模式?使用在了哪里?为什么使用它?
spring怎么提交事务的?
1.判断事务是否已经完成,如果完成抛出异常
2.判断事务是否已经被标记成回滚,则执行回滚操作
3.嵌入事务标记回滚,如果嵌入事务抛出了异常执行了回滚,但是在调用方把嵌入事务的异常个捕获没有抛出,就会执行这一步。
4.提交事务
2.判断事务是否已经被标记成回滚,则执行回滚操作
3.嵌入事务标记回滚,如果嵌入事务抛出了异常执行了回滚,但是在调用方把嵌入事务的异常个捕获没有抛出,就会执行这一步。
4.提交事务
那 BeanFactory 和 FactoryBean 又有什么区别?
这两个是「不同的产物」
「BeanFactory 是 IOC 容器」,是用来承载对象的
「FactoryBean 是一个接口」,为 Bean 提供了更加灵活的方式,通过代理一个Bean对象,对方法前后做一些操作。
「BeanFactory 是 IOC 容器」,是用来承载对象的
「FactoryBean 是一个接口」,为 Bean 提供了更加灵活的方式,通过代理一个Bean对象,对方法前后做一些操作。
BeanFactory和ApplicationContext的区别?
BeanFactory在启动的时候不会去实例化Bean,从容器中拿Bean的时候才会去是实例化
ApplicationContext在启动的时候就把所有的Bean全部实例化了
ApplicationContext在启动的时候就把所有的Bean全部实例化了
@Repository、@Service、@Compent、@Controller它们有什么区别?
这四个注解的「本质都是一样的,都是将被该注解标识的对象放入 spring 容器当中,只是为了在使用上区分不同的应用分层」
@Repository:dao层
@Service:service层
@Controller:controller层
@Compent:其他不属于以上三层的统一使用该注解
@Repository:dao层
@Service:service层
@Controller:controller层
@Compent:其他不属于以上三层的统一使用该注解
动态代理和静态代理有什么区别?
「静态代理」
由程序员创建或由特定工具自动生成源代码,再对其编译。在程序运行前,代理类的.class文件就已经存在了
静态代理通常只代理一个类
静态代理事先知道要代理的是什么
「动态代理」
在程序运行时,运用反射机制动态创建而成
动态代理是代理一个接口下的多个实现类
动态代理不知道要代理什么东西,只有在运行时才知道
由程序员创建或由特定工具自动生成源代码,再对其编译。在程序运行前,代理类的.class文件就已经存在了
静态代理通常只代理一个类
静态代理事先知道要代理的是什么
「动态代理」
在程序运行时,运用反射机制动态创建而成
动态代理是代理一个接口下的多个实现类
动态代理不知道要代理什么东西,只有在运行时才知道
三种实现指定初始化方法
使用@PostConstruct注解,该注解作用于void方法上
在配文件中配置init-method方法
将类实现InitializingBean接口,实现afterPropertiesSet方法
在配文件中配置init-method方法
将类实现InitializingBean接口,实现afterPropertiesSet方法
如何实现AOP,项⽬哪些地⽅⽤到了AOP?
利⽤动态代理技术来实现AOP,⽐如JDK动态代理或Cglib动态代理,利⽤动态代理技术,可以针对某个类 ⽣成代理对象,当调⽤代理对象的某个⽅法时,可以任意控制该⽅法的执⾏,⽐如可以先打印执⾏时间, 再执⾏该⽅法,并且该⽅法执⾏完成后,再次打印执⾏时间。
项⽬中,⽐如事务、权限控制、⽅法执⾏时⻓⽇志都是通过AOP技术来实现的,凡是需要对某些⽅法做统 ⼀处理的都可以⽤AOP来实现,利⽤AOP可以做到业务⽆侵⼊。
项⽬中,⽐如事务、权限控制、⽅法执⾏时⻓⽇志都是通过AOP技术来实现的,凡是需要对某些⽅法做统 ⼀处理的都可以⽤AOP来实现,利⽤AOP可以做到业务⽆侵⼊。
Spring的对象默认是单例的还是多例的?单例bean存不存在线程安全问题?
在Spring中的对象默认是单例的,但是也可以配置为多例
单例bean对象对应的类存在可变的成员变量并且其中存在改变这个变量的线程时,多线程操作该bean对象是会出现线程安全问题
原因是:多线程操作如果改变成员变量,其他线程无法访问该bean对象,造成数据混乱
解决办法:在bean对象中避免定义可变成员变量;
在bean对象中定义一个ThreadLocal成员变量,将要的可变成员变量保存在ThreadLocal中
单例bean对象对应的类存在可变的成员变量并且其中存在改变这个变量的线程时,多线程操作该bean对象是会出现线程安全问题
原因是:多线程操作如果改变成员变量,其他线程无法访问该bean对象,造成数据混乱
解决办法:在bean对象中避免定义可变成员变量;
在bean对象中定义一个ThreadLocal成员变量,将要的可变成员变量保存在ThreadLocal中
Spring如果解决循环依赖?
通过三级缓存:
一级缓存,用于保存实例化,注入,初始化完成的bean实例
二级缓存,用于保存实例化完成的bean实例
三级缓存,用于保存bean创建工厂
一级缓存,用于保存实例化,注入,初始化完成的bean实例
二级缓存,用于保存实例化完成的bean实例
三级缓存,用于保存bean创建工厂
依赖注入的方式有几种,各是什么?
构造器注入
setter注入
接口注入
setter注入
接口注入
@Autowired 和 @Resource 有什么区别?
「@Resource 是 Java 自己的注解」,@Resource 有两个属性是比较重要的,分是 name 和 type;Spring 将 @Resource 注解的 name 属性解析为 bean 的名字,而 type 属性则解析为 bean 的类型。所以如果使用 name 属性,则使用 byName 的自动注入策略,而使用 type 属性时则使用 byType 自动注入策略。如果既不指定 name 也不指定 type 属性,这时将通过反射机制使用 byName 自动注入策略。
「@Autowired 是spring 的注解」,是 spring2.5 版本引入的,Autowired 只根据 type 进行注入,「不会去匹配 name」。如果涉及到 type 无法辨别注入对象时,那需要依赖 @Qualifier 或 @Primary 注解一起来修饰。
「@Autowired 是spring 的注解」,是 spring2.5 版本引入的,Autowired 只根据 type 进行注入,「不会去匹配 name」。如果涉及到 type 无法辨别注入对象时,那需要依赖 @Qualifier 或 @Primary 注解一起来修饰。
解释Spring支持的几种bean的作用域?
Spring容器中的bean可以分为5个范围:
(1)singleton:默认,每个容器中只有一个bean的实例,单例的模式由BeanFactory自身来维
护。
(2)prototype:为每一个bean请求提供一个实例。
(3)request:为每一个网络请求创建一个实例,在请求完成以后,bean会失效并被垃圾回收器回
收。
(4)session:与request范围类似,确保每个session中有一个bean的实例,在session过期后,
bean会随之失效。
(5)global-session:全局作用域,global-session和Portlet应用相关。当你的应用部署在Portlet
容器中工作时,它包含很多portlet。如果你想要声明让所有的portlet共用全局的存储变量的话,那
么这全局变量需要存储在global-session中。全局作用域与Servlet中的session作用域效果相同。
(1)singleton:默认,每个容器中只有一个bean的实例,单例的模式由BeanFactory自身来维
护。
(2)prototype:为每一个bean请求提供一个实例。
(3)request:为每一个网络请求创建一个实例,在请求完成以后,bean会失效并被垃圾回收器回
收。
(4)session:与request范围类似,确保每个session中有一个bean的实例,在session过期后,
bean会随之失效。
(5)global-session:全局作用域,global-session和Portlet应用相关。当你的应用部署在Portlet
容器中工作时,它包含很多portlet。如果你想要声明让所有的portlet共用全局的存储变量的话,那
么这全局变量需要存储在global-session中。全局作用域与Servlet中的session作用域效果相同。
说一下spring的事务机制?
1. Spring事务底层是基于数据库事务和AOP机制的
2. ⾸先对于使⽤了@Transactional注解的Bean,Spring会创建⼀个代理对象作为Bean
3. 当调⽤代理对象的⽅法时,会先判断该⽅法上是否加了@Transactional注解
4. 如果加了,那么则利⽤事务管理器创建⼀个数据库连接
5. 并且修改数据库连接的autocommit属性为false,禁⽌此连接的⾃动提交,这是实现Spring事务⾮常重 要的⼀步
6. 然后执⾏当前⽅法,⽅法中会执⾏sql
7. 执⾏完当前⽅法后,如果没有出现异常就直接提交事务
8. 如果出现了异常,并且这个异常是需要回滚的就会回滚事务,否则仍然提交事务
9. Spring事务的隔离级别对应的就是数据库的隔离级别
10. Spring事务的传播机制是Spring事务⾃⼰实现的,也是Spring事务中最复杂的
11. Spring事务的传播机制是基于数据库连接来做的,⼀个数据库连接⼀个事务,如果传播机制配置为需要 新开⼀个事务,那么实际上就是先建⽴⼀个数据库连接,在此新数据库连接上执⾏sql
2. ⾸先对于使⽤了@Transactional注解的Bean,Spring会创建⼀个代理对象作为Bean
3. 当调⽤代理对象的⽅法时,会先判断该⽅法上是否加了@Transactional注解
4. 如果加了,那么则利⽤事务管理器创建⼀个数据库连接
5. 并且修改数据库连接的autocommit属性为false,禁⽌此连接的⾃动提交,这是实现Spring事务⾮常重 要的⼀步
6. 然后执⾏当前⽅法,⽅法中会执⾏sql
7. 执⾏完当前⽅法后,如果没有出现异常就直接提交事务
8. 如果出现了异常,并且这个异常是需要回滚的就会回滚事务,否则仍然提交事务
9. Spring事务的隔离级别对应的就是数据库的隔离级别
10. Spring事务的传播机制是Spring事务⾃⼰实现的,也是Spring事务中最复杂的
11. Spring事务的传播机制是基于数据库连接来做的,⼀个数据库连接⼀个事务,如果传播机制配置为需要 新开⼀个事务,那么实际上就是先建⽴⼀个数据库连接,在此新数据库连接上执⾏sql
spring 事务隔离级别有哪些?
DEFAULT:采用 DB 默认的事务隔离级别
READ_UNCOMMITTED:读未提交
READ_COMMITTED:读已提交
REPEATABLE_READ:可重复读
SERIALIZABLE:串行化
READ_UNCOMMITTED:读未提交
READ_COMMITTED:读已提交
REPEATABLE_READ:可重复读
SERIALIZABLE:串行化
spirng的事务管理?
1、编程式事务:
beginTransaction()、commit()、rollback()等事务管理相关的方法
2、声明式事务
利用注解Transactional或者aop配置
beginTransaction()、commit()、rollback()等事务管理相关的方法
2、声明式事务
利用注解Transactional或者aop配置
Spring事务的传播行为
它有七种传播行为:
propagation_required:如果当前没有事务,就创建一个新事务,如果当前存在事务,就加入该事务
propagation_supports:如果当前存在事务,就加入该事务,如果当前不存在事务,就以非事务执行
propagaion_mandatory:如果当前存在事务,就加入该事务,如果当前不存在事务,就抛出异常
propagation_requires_new:无论当前存不存在事务,都创建新事务。
propagation_not_supported:以非事务方式执行操作,如果当前存在事务,就把当前事务挂起
propagation_never:以非事务方式执行,如果当前存在事务,则抛出异常
propagation_nested:如果当前存在事务,则在嵌套事务内执行,如果当前没有事务,则按required属性执行
propagation_required:如果当前没有事务,就创建一个新事务,如果当前存在事务,就加入该事务
propagation_supports:如果当前存在事务,就加入该事务,如果当前不存在事务,就以非事务执行
propagaion_mandatory:如果当前存在事务,就加入该事务,如果当前不存在事务,就抛出异常
propagation_requires_new:无论当前存不存在事务,都创建新事务。
propagation_not_supported:以非事务方式执行操作,如果当前存在事务,就把当前事务挂起
propagation_never:以非事务方式执行,如果当前存在事务,则抛出异常
propagation_nested:如果当前存在事务,则在嵌套事务内执行,如果当前没有事务,则按required属性执行
spring事务是怎么回滚的?
判断是否存在事务,只有存在事务才执行回滚,即是否有@Transactional事务注解或相关事务切面
根据异常类型判断是否回滚。如果异常类型不符合,仍然会提交事务
根据@Transactional注解中rollbackFor、rollbackForClassName、noRollbackForClassName配置的值,找到最符合ex的异常类型,如果符合的异常类型不是NoRollbackRuleAttribute,则可以执行回滚。
如果@Transactional没有配置,则默认使用RuntimeException和Error异常。
回滚处理
如果存在安全点,则回滚事务至安全点,这个主要是处理嵌套事务,回滚安全点的操作还是交给了数据库处理.
当前事务是一个新事务时,那么直接回滚,使用的是DataSourceTransactionManager事务管理器,所以调用DataSourceTransactionManager#doRollback,直接调用数据库连接的回滚方法。
当前存在事务,但又不是一个新的事务,只把事务的状态标记为read-only,等到事务链执行完毕后,统一回滚,调用DataSourceTransactionManager#doSetRollbackOnly
清空记录的资源并将挂起的资源恢复
根据异常类型判断是否回滚。如果异常类型不符合,仍然会提交事务
根据@Transactional注解中rollbackFor、rollbackForClassName、noRollbackForClassName配置的值,找到最符合ex的异常类型,如果符合的异常类型不是NoRollbackRuleAttribute,则可以执行回滚。
如果@Transactional没有配置,则默认使用RuntimeException和Error异常。
回滚处理
如果存在安全点,则回滚事务至安全点,这个主要是处理嵌套事务,回滚安全点的操作还是交给了数据库处理.
当前事务是一个新事务时,那么直接回滚,使用的是DataSourceTransactionManager事务管理器,所以调用DataSourceTransactionManager#doRollback,直接调用数据库连接的回滚方法。
当前存在事务,但又不是一个新的事务,只把事务的状态标记为read-only,等到事务链执行完毕后,统一回滚,调用DataSourceTransactionManager#doSetRollbackOnly
清空记录的资源并将挂起的资源恢复
spring 事务的传播机制有哪些?
1.「propagation_required」
当前方法「必须在一个具有事务的上下文中运行」,如有客户端有事务在进行,那么被调用端将在该事务中运行,否则的话重新开启一个事务。(如果被调用端发生异常,那么调用端和被调用端事务都将回滚)
当前方法「必须在一个具有事务的上下文中运行」,如有客户端有事务在进行,那么被调用端将在该事务中运行,否则的话重新开启一个事务。(如果被调用端发生异常,那么调用端和被调用端事务都将回滚)
2.「propagation_supports」
当前方法不必需要具有一个事务上下文,但是如果有一个事务的话,它也可以在这个事务中运行
当前方法不必需要具有一个事务上下文,但是如果有一个事务的话,它也可以在这个事务中运行
3.「propagation_mandatory」
表示当前方法「必须在一个事务中运行」,如果没有事务,将抛出异常
表示当前方法「必须在一个事务中运行」,如果没有事务,将抛出异常
4.「propagation_nested」
如果当前方法正有一个事务在运行中,则该方法应该「运行在一个嵌套事务」中,被嵌套的事务可以独立于被封装的事务中进行提交或者回滚。如果封装事务存在,并且外层事务抛出异常回滚,那么内层事务必须回滚,反之,内层事务并不影响外层事务。如果封装事务不存在,则同propagation_required的一样
如果当前方法正有一个事务在运行中,则该方法应该「运行在一个嵌套事务」中,被嵌套的事务可以独立于被封装的事务中进行提交或者回滚。如果封装事务存在,并且外层事务抛出异常回滚,那么内层事务必须回滚,反之,内层事务并不影响外层事务。如果封装事务不存在,则同propagation_required的一样
5.「propagation_never」
当方法务不应该在一个事务中运行,如果「存在一个事务,则抛出异常」
当方法务不应该在一个事务中运行,如果「存在一个事务,则抛出异常」
6.「propagation_requires_new」
当前方法「必须运行在它自己的事务中」。一个新的事务将启动,而且如果有一个现有的事务在运行的话,则这个方法将在运行期被挂起,直到新的事务提交或者回滚才恢复执行。
当前方法「必须运行在它自己的事务中」。一个新的事务将启动,而且如果有一个现有的事务在运行的话,则这个方法将在运行期被挂起,直到新的事务提交或者回滚才恢复执行。
7.「propagation_not_supported」
方法不应该在一个事务中运行。「如果有一个事务正在运行,他将在运行期被挂起,直到这个事务提交或者回滚才恢复执行」
方法不应该在一个事务中运行。「如果有一个事务正在运行,他将在运行期被挂起,直到这个事务提交或者回滚才恢复执行」
事务三要素是什么?
数据源:表示具体的事务性资源,是事务的真正处理者,如MySQL等。
事务管理器:像一个大管家,从整体上管理事务的处理过程,如打开、提交、回滚等。
事务应用和属性配置:像一个标识符,表明哪些方法要参与事务,如何参与事务,以及一些相关属
性如隔离级别、超时时间等。
事务管理器:像一个大管家,从整体上管理事务的处理过程,如打开、提交、回滚等。
事务应用和属性配置:像一个标识符,表明哪些方法要参与事务,如何参与事务,以及一些相关属
性如隔离级别、超时时间等。
spring事务不回滚场景
1.错误的传播特性
2.自己吞了异常
3.手动抛了别的异常
4.自定义了回滚异常
5.嵌套事务回滚多了
2.spring中使用了哪些设计模式?
在spring中主要用到的设计模式有:工厂模式、单例模式、代理模式、模板模式、观察者模式、适配器模式。
1.工厂模式
IOC控制反转也叫依赖注入,它就是典型的工厂模式,通过sessionfactory去注入实例
解释:将对象交给容器管理,你只需要在spring配置文件总配置相应的bean,以及设置相关属性,让spring容器来生成类的实例对象以及管理对象。在spring容器启动的时候,spring会把你在配置文件中配置的bean初始化好,然后在你需要调用的时候,就把它已经初始化好的那些bean分配给你 需要调用这些bean的类(假设这个类名是A),分配的方法就是调用A的setter方法来注入,而不需要你在A类里面new这些bean了。
总结:对象实例化与初始化进行解耦
2.单例模式
Spring中JavaBean默认为单例,因为spring上下文中会有很多个dao\service\action对象,如果用多例模式的话,就每次要用到的时候,都会重新创建一个新的对象,内存中就会有很多重复的对象,所以单例模式的特点就是能减少我们的内存空间,节约性能。
还有常用Spring中 @Repository、@Component、@Configuration @Service注解作用下的类默认都是单例模式的,所以,我目前认为在Spring下使用单例最优的方式是将类@Component注册为组件。使用场景主要有:数据库配置、Redis配置、权限配置、Filter过滤、webMvcConfig、swagger及自定义的时间转换器、类型转换器、对接第三方硬件时,调用硬件的dll、so文件等。
单独使用@Component注解,只能控制到类上,使用@Configuration+@Bean可以控制到方法级别粒度,但是尽量避免@Component+@Bean组合使用,因为@Component+@Bean并不是单例,在调用过程中可能会出现多个Bean实例,导致蜜汁错误。
单独使用@Component注解,只能控制到类上,使用@Configuration+@Bean可以控制到方法级别粒度,但是尽量避免@Component+@Bean组合使用,因为@Component+@Bean并不是单例,在调用过程中可能会出现多个Bean实例,导致蜜汁错误。
并不是所有的注解默认都是单例模式,@RestController就是多例
3.代理模式
Spring中的AOP就是典型的代理模式,首先我们先聊聊AOP(面向切面编程)的的一个设计原理:
AOP可以说是对OOP的补充和完善 OOP引入了封装、继承和多态性等概念来建立一种对象层次结构,用以模拟公共行为的一个集合。当我们需要为分散的对象引入公共行为的时候,OOP则显得无能为力。也就是说,oop允许你定义从上到下的关系,但并不适合定义从左到右的关系。例如日志功能,日志代码往往水平地散布在所有对象层次中,而与它所散布到的对象的核心功能毫无关系。在OOP设计中,它导致大量代码重复,而不利于各个模块的重用。
实现AOP的技术,主要分为两大类:一是采用动态代理技术,利用截取消息的方式,对该消息进行装饰,以取代原有对象行为的执行;二是采用静态织入的方式,引入特定的语法创建“方面”,从而使得编译器可以在编译期间织入有关“方面”的代码
简单点解释,比方说你想在你的biz层所有类中都加上一个打印‘你好’的功能,这时就可以用AOP思想来做,你先写个类写个类方法,方法经实现打印‘你好’,然后IOC这个类ref="biz.* "让每个类都注入即可实现。
AOP可以说是对OOP的补充和完善 OOP引入了封装、继承和多态性等概念来建立一种对象层次结构,用以模拟公共行为的一个集合。当我们需要为分散的对象引入公共行为的时候,OOP则显得无能为力。也就是说,oop允许你定义从上到下的关系,但并不适合定义从左到右的关系。例如日志功能,日志代码往往水平地散布在所有对象层次中,而与它所散布到的对象的核心功能毫无关系。在OOP设计中,它导致大量代码重复,而不利于各个模块的重用。
实现AOP的技术,主要分为两大类:一是采用动态代理技术,利用截取消息的方式,对该消息进行装饰,以取代原有对象行为的执行;二是采用静态织入的方式,引入特定的语法创建“方面”,从而使得编译器可以在编译期间织入有关“方面”的代码
简单点解释,比方说你想在你的biz层所有类中都加上一个打印‘你好’的功能,这时就可以用AOP思想来做,你先写个类写个类方法,方法经实现打印‘你好’,然后IOC这个类ref="biz.* "让每个类都注入即可实现。
4.模板模式
定义:模板方法模式在一个方法中定义一个算法骨架,并将某些步骤推迟到子类中实现。模板方法模式可以让子类在不改变算法整体结构的情况下,重新定义算法中的某些步骤
目的:1.使用模版方法模式的目的是避免编写重复代码,以便开发人员可以专注于核心业务逻辑的实现
2.解决接口与接口实现类之间继承矛盾问题
目的:1.使用模版方法模式的目的是避免编写重复代码,以便开发人员可以专注于核心业务逻辑的实现
2.解决接口与接口实现类之间继承矛盾问题
5.观察者模式
定义对象键一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都会得到通知被制动更新
例如:
报社的业务就是出版报纸。
向某家报社订阅报纸,只要他们有新报纸出版,就会给你送来。只要你是他们的订户、你就会一直收到新报纸。
当你不想再看报纸的时候,取消订阅,他们就不会再送新报纸来。
只要报社还在运营,就会一直有人(或单位)向他们订阅报纸或取消订阅报纸。
报社:被观察者
订户:观察者
一个报社对应多个订户
例如:
报社的业务就是出版报纸。
向某家报社订阅报纸,只要他们有新报纸出版,就会给你送来。只要你是他们的订户、你就会一直收到新报纸。
当你不想再看报纸的时候,取消订阅,他们就不会再送新报纸来。
只要报社还在运营,就会一直有人(或单位)向他们订阅报纸或取消订阅报纸。
报社:被观察者
订户:观察者
一个报社对应多个订户
在Spring中有一个ApplicationListener,采用观察者模式来处理的,ApplicationEventMulticaster作为主题,里面有添加,删除,通知等。
spring有一些内置的事件,当完成某种操作时会发出某些事件动作,他的处理方式也就上面的这种模式,当然这里面还有很多,可以了解下spring的启动过程。
spring有一些内置的事件,当完成某种操作时会发出某些事件动作,他的处理方式也就上面的这种模式,当然这里面还有很多,可以了解下spring的启动过程。
在java.util 包下 除了常用的 集合 和map之外还有一个Observable类,他的实现方式其实就是观察者模式。里面也有添加、删除、通知等方法。
6.适配器模式
定义:将一个类的接口转接成用户所期待的。一个适配使得因接口不兼容而不能在一起工作的类能在一起工作,做法是将类自己的接口包裹在一个已存在的类中
例子:以手机充电为例,电压220V,手机支持5.5V,充电器相当于适配器
例子:以手机充电为例,电压220V,手机支持5.5V,充电器相当于适配器
AOP和MVC中,都有用到适配器模式。
spring aop框架对BeforeAdvice、AfterAdvice、ThrowsAdvice三种通知类型的支持实际上是借助适配器模式来实现的,这样的好处是使得框架允许用户向框架中加入自己想要支持的任何一种通知类型,上述三种通知类型是spring aop框架定义的,它们是aop联盟定义的Advice的子类型。
Spring中的AOP中AdvisorAdapter类,它有三个实现:MethodBeforAdviceAdapter、AfterReturnningAdviceAdapter、ThrowsAdviceAdapter。
spring aop框架对BeforeAdvice、AfterAdvice、ThrowsAdvice三种通知类型的支持实际上是借助适配器模式来实现的,这样的好处是使得框架允许用户向框架中加入自己想要支持的任何一种通知类型,上述三种通知类型是spring aop框架定义的,它们是aop联盟定义的Advice的子类型。
Spring中的AOP中AdvisorAdapter类,它有三个实现:MethodBeforAdviceAdapter、AfterReturnningAdviceAdapter、ThrowsAdviceAdapter。
说说你对Spring MVC的理解
MVC:MVC是一种设计模式
M-Model 模型(完成业务逻辑:有javaBean构成,service+dao+entity)
V-View 视图(做界面的展示 jsp,html……)
C-Controller 控制器(接收请求—>调用模型—>根据结果派发页面)
V-View 视图(做界面的展示 jsp,html……)
C-Controller 控制器(接收请求—>调用模型—>根据结果派发页面)
工作原理:
1、 用户发送请求至前端控制器DispatcherServlet。
2、 DispatcherServlet收到请求调用HandlerMapping处理器映射器。
3、 处理器映射器找到具体的处理器(可以根据xml配置、注解进行查找),生成处理器对象及处理器
拦截器(如果有则生成)一并返回给DispatcherServlet。
4、 DispatcherServlet调用HandlerAdapter处理器适配器。
5、 HandlerAdapter经过适配调用具体的处理器(Controller,也叫后端控制器)。
6、 Controller执行完成返回ModelAndView。
7、 HandlerAdapter将controller执行结果ModelAndView返回给DispatcherServlet。
8、 DispatcherServlet将ModelAndView传给ViewReslover视图解析器。
9、 ViewReslover解析后返回具体View。
10、DispatcherServlet根据View进行渲染视图(即将模型数据填充至视图中)。
11、 DispatcherServlet响应用户。
2、 DispatcherServlet收到请求调用HandlerMapping处理器映射器。
3、 处理器映射器找到具体的处理器(可以根据xml配置、注解进行查找),生成处理器对象及处理器
拦截器(如果有则生成)一并返回给DispatcherServlet。
4、 DispatcherServlet调用HandlerAdapter处理器适配器。
5、 HandlerAdapter经过适配调用具体的处理器(Controller,也叫后端控制器)。
6、 Controller执行完成返回ModelAndView。
7、 HandlerAdapter将controller执行结果ModelAndView返回给DispatcherServlet。
8、 DispatcherServlet将ModelAndView传给ViewReslover视图解析器。
9、 ViewReslover解析后返回具体View。
10、DispatcherServlet根据View进行渲染视图(即将模型数据填充至视图中)。
11、 DispatcherServlet响应用户。
前端控制器(DispatcherServlet):接收请求,响应结果,相当于电脑的CPU。
处理器映射器(HandlerMapping):根据URL去查找处理器。
处理器(Handler):需要程序员去写代码处理逻辑的。
处理器适配器(HandlerAdapter):会把处理器包装成适配器,这样就可以支持多种类型的处理
器,类比笔记本的适配器(适配器模式的应用)。
视图解析器(ViewResovler):进行视图解析,多返回的字符串,进行处理,可以解析成对应的页
面。
处理器映射器(HandlerMapping):根据URL去查找处理器。
处理器(Handler):需要程序员去写代码处理逻辑的。
处理器适配器(HandlerAdapter):会把处理器包装成适配器,这样就可以支持多种类型的处理
器,类比笔记本的适配器(适配器模式的应用)。
视图解析器(ViewResovler):进行视图解析,多返回的字符串,进行处理,可以解析成对应的页
面。
SpringMVC常用的注解有哪些?
@RequestMapping:用于处理请求 url 映射的注解,可用于类或方法上。用于类上,则表示类中
的所有响应请求的方法都是以该地址作为父路径。
@RequestBody:注解实现接收http请求的json数据,将json转换为java对象。
@ResponseBody:注解实现将conreoller方法返回对象转化为json对象响应给客户。
的所有响应请求的方法都是以该地址作为父路径。
@RequestBody:注解实现接收http请求的json数据,将json转换为java对象。
@ResponseBody:注解实现将conreoller方法返回对象转化为json对象响应给客户。
什么是spring?
Spring 是个java企业级应用的开源开发框架。Spring主要用来开发Java应用,但是有些扩展是针对
构建J2EE平台的web应用。Spring 框架目标是简化Java企业级应用开发,并通过POJO为基础的编程
模型促进良好的编程习惯。
构建J2EE平台的web应用。Spring 框架目标是简化Java企业级应用开发,并通过POJO为基础的编程
模型促进良好的编程习惯。
spring 中有哪些核心模块?
1.「Spring Core」:Spring核心,它是框架最基础的部分,提供IOC和依赖注入DI特性
2.「Spring Context」:Spring上下文容器,它是 BeanFactory 功能加强的一个子接口
3.「Spring Web」:它提供Web应用开发的支持
4.「Spring MVC」:它针对Web应用中MVC思想的实现
5.「Spring DAO」:提供对JDBC抽象层,简化了JDBC编码,同时,编码更具有健壮性
6.「Spring ORM」:它支持用于流行的ORM框架的整合,比如:Spring + Hibernate、Spring + iBatis、Spring + JDO的整合等
7.「Spring AOP」:即面向切面编程,它提供了与AOP联盟兼容的编程实现
spring 中的 IOC 容器有哪些?有什么区别?
spring 主要提供了「两种 IOC 容器」,一种是 「BeanFactory」,还有一种是 「ApplicationContext」
它们的区别就在于,BeanFactory 「只提供了最基本的实例化对象和拿对象的功能」,而 ApplicationContext 是继承了 BeanFactory 所派生出来的产物,是其子类,它的作用更加的强大,比如支持注解注入、国际化等功能
它们的区别就在于,BeanFactory 「只提供了最基本的实例化对象和拿对象的功能」,而 ApplicationContext 是继承了 BeanFactory 所派生出来的产物,是其子类,它的作用更加的强大,比如支持注解注入、国际化等功能
ApplicationContext 继承了 BeanFactory,BeanFactory 是 Spring 中比较原始的
Factory,它不支持 AOP、Web 等 Spring 插件。而 ApplicationContext 不仅包含了 BeanFactory
的所有功能,还支持 Spring 的各种插件,还以一种面向框架的方式工作以及对上下文进行分层和实
现继承。
BeanFactory 是 Spring 框架的基础设施,面向 Spring 本身;而 ApplicationContext 面向使用
Spring 的开发者,相比 BeanFactory 提供了更多面向实际应用的功能,几乎所有场合都可以直接使
用 ApplicationContext,而不是底层的 BeanFactory。
Factory,它不支持 AOP、Web 等 Spring 插件。而 ApplicationContext 不仅包含了 BeanFactory
的所有功能,还支持 Spring 的各种插件,还以一种面向框架的方式工作以及对上下文进行分层和实
现继承。
BeanFactory 是 Spring 框架的基础设施,面向 Spring 本身;而 ApplicationContext 面向使用
Spring 的开发者,相比 BeanFactory 提供了更多面向实际应用的功能,几乎所有场合都可以直接使
用 ApplicationContext,而不是底层的 BeanFactory。
实现AOP的步骤?
JDK 动态代理和 CGLIB 代理有什么区别?
JDK 动态代理时业务类「必须要实现某个接口」,它是「基于反射的机制实现的」,生成一个实现同样接口的一个代理类,然后通过重写方法的方式,实现对代码的增强。
CGLIB 动态代理是使用字节码处理框架 ASM,其原理是通过字节码技术为一个类「创建子类,然后重写父类的方法」,实现对代码的增强。
CGLIB 动态代理是使用字节码处理框架 ASM,其原理是通过字节码技术为一个类「创建子类,然后重写父类的方法」,实现对代码的增强。
Spring AOP 和 AspectJ AOP 有什么区别?
Spring AOP 是运行时增强,是通过「动态代理实现」的
AspectJ AOP 是编译时增强,需要特殊的编译器才可以完成,是通过「修改代码来实现」的,支持「三种织入方式」
「编译时织入」:就是在编译字节码的时候织入相关代理类
「编译后织入」:编译完初始类后发现需要 AOP 增强,然后织入相关代码
「类加载时织入」:指在加载器加载类的时候织入
AspectJ AOP 是编译时增强,需要特殊的编译器才可以完成,是通过「修改代码来实现」的,支持「三种织入方式」
「编译时织入」:就是在编译字节码的时候织入相关代理类
「编译后织入」:编译完初始类后发现需要 AOP 增强,然后织入相关代码
「类加载时织入」:指在加载器加载类的时候织入
主要区别 Spring AOP AspecjtJ AOP
增强方式 运行时增强 编译时增强
实现方式 动态代理 修改代码
编译器 javac 特殊的编译器 ajc
效率 较低(运行时反射损耗性能) 较高
织入方式 运行时 编译时、编译后、类加载时
增强方式 运行时增强 编译时增强
实现方式 动态代理 修改代码
编译器 javac 特殊的编译器 ajc
效率 较低(运行时反射损耗性能) 较高
织入方式 运行时 编译时、编译后、类加载时
spring 中 Bean 的生命周期是怎样的?
(1)实例化Bean:
对于BeanFactory容器,当客户向容器请求一个尚未初始化的bean时,或初始化bean的时候需要注
入另一个尚未初始化的依赖时,容器就会调用createBean进行实例化。对于ApplicationContext容
器,当容器启动结束后,通过获取BeanDefinition对象中的信息,实例化所有的bean。
(2)设置对象属性(依赖注入):
实例化后的对象被封装在BeanWrapper对象中,紧接着,Spring根据BeanDefinition中的信息 以 及 通过BeanWrapper提供的设置属性的接口完成依赖注入。
(3)处理Aware接口:
接着,Spring会检测该对象是否实现了xxxAware接口,并将相关的xxxAware实例注入给Bean:
①如果这个Bean已经实现了BeanNameAware接口,会调用它实现的setBeanName(String
beanId)方法,此处传递的就是Spring配置文件中Bean的id值;
②如果这个Bean已经实现了BeanFactoryAware接口,会调用它实现的setBeanFactory()方法,传
递的是Spring工厂自身。
③如果这个Bean已经实现了ApplicationContextAware接口,会调用
setApplicationContext(ApplicationContext)方法,传入Spring上下文;
(4)BeanPostProcessor:
如果想对Bean进行一些自定义的处理,那么可以让Bean实现了BeanPostProcessor接口,那将会
调用postProcessBeforeInitialization(Object obj, String s)方法。
(5)InitializingBean 与 init-method:
如果Bean在Spring配置文件中配置了 init-method 属性,则会自动调用其配置的初始化方法。
(6)如果这个Bean实现了BeanPostProcessor接口,将会调用
postProcessAfterInitialization(Object obj, String s)方法;由于这个方法是在Bean初始化结束时调
用的,所以可以被应用于内存或缓存技术;
以上几个步骤完成后,Bean就已经被正确创建了,之后就可以使用这个Bean了。
(7)DisposableBean: 当Bean不再需要时,会经过清理阶段,如果Bean实现了DisposableBean这个接口,会调用其实现
的destroy()方法;
(8)destroy-method:
最后,如果这个Bean的Spring配置中配置了destroy-method属性,会自动调用其配置的销毁方
法。
对于BeanFactory容器,当客户向容器请求一个尚未初始化的bean时,或初始化bean的时候需要注
入另一个尚未初始化的依赖时,容器就会调用createBean进行实例化。对于ApplicationContext容
器,当容器启动结束后,通过获取BeanDefinition对象中的信息,实例化所有的bean。
(2)设置对象属性(依赖注入):
实例化后的对象被封装在BeanWrapper对象中,紧接着,Spring根据BeanDefinition中的信息 以 及 通过BeanWrapper提供的设置属性的接口完成依赖注入。
(3)处理Aware接口:
接着,Spring会检测该对象是否实现了xxxAware接口,并将相关的xxxAware实例注入给Bean:
①如果这个Bean已经实现了BeanNameAware接口,会调用它实现的setBeanName(String
beanId)方法,此处传递的就是Spring配置文件中Bean的id值;
②如果这个Bean已经实现了BeanFactoryAware接口,会调用它实现的setBeanFactory()方法,传
递的是Spring工厂自身。
③如果这个Bean已经实现了ApplicationContextAware接口,会调用
setApplicationContext(ApplicationContext)方法,传入Spring上下文;
(4)BeanPostProcessor:
如果想对Bean进行一些自定义的处理,那么可以让Bean实现了BeanPostProcessor接口,那将会
调用postProcessBeforeInitialization(Object obj, String s)方法。
(5)InitializingBean 与 init-method:
如果Bean在Spring配置文件中配置了 init-method 属性,则会自动调用其配置的初始化方法。
(6)如果这个Bean实现了BeanPostProcessor接口,将会调用
postProcessAfterInitialization(Object obj, String s)方法;由于这个方法是在Bean初始化结束时调
用的,所以可以被应用于内存或缓存技术;
以上几个步骤完成后,Bean就已经被正确创建了,之后就可以使用这个Bean了。
(7)DisposableBean: 当Bean不再需要时,会经过清理阶段,如果Bean实现了DisposableBean这个接口,会调用其实现
的destroy()方法;
(8)destroy-method:
最后,如果这个Bean的Spring配置中配置了destroy-method属性,会自动调用其配置的销毁方
法。
spring 是怎么解决循环依赖的?
循环依赖就是说两个对象相互依赖,形成了一个环形的调用链路
spring 使用三级缓存去解决循环依赖的,其「核心逻辑就是把实例化和初始化的步骤分开,然后放入缓存中」,供另一个对象调用
「第一级缓存」:用来保存实例化、初始化都完成的对象
「第二级缓存」:用来保存实例化完成,但是未初始化完成的对象
「第三级缓存」:用来保存一个对象工厂,提供一个匿名内部类,用于创建二级缓存中的对象
spring 使用三级缓存去解决循环依赖的,其「核心逻辑就是把实例化和初始化的步骤分开,然后放入缓存中」,供另一个对象调用
「第一级缓存」:用来保存实例化、初始化都完成的对象
「第二级缓存」:用来保存实例化完成,但是未初始化完成的对象
「第三级缓存」:用来保存一个对象工厂,提供一个匿名内部类,用于创建二级缓存中的对象
当 A、B 两个类发生循环引用时 大致流程
1.A 完成实例化后,去「创建一个对象工厂,并放入三级缓存」当中
如果 A 被 AOP 代理,那么通过这个工厂获取到的就是 A 代理后的对象
如果 A 没有被 AOP 代理,那么这个工厂获取到的就是 A 实例化的对象
2.A 进行属性注入时,去「创建 B」
3.B 进行属性注入,需要 A ,则「从三级缓存中去取 A 工厂代理对象」并注入,然后删除三级缓存中的 A 工厂,将 A 对象放入二级缓存
4.B 完成后续属性注入,直到初始化结束,将 B 放入一级缓存
5.「A 从一级缓存中取到 B 并且注入 B」, 直到完成后续操作,将 A 从二级缓存删除并且放入一级缓存,循环依赖结束
1.A 完成实例化后,去「创建一个对象工厂,并放入三级缓存」当中
如果 A 被 AOP 代理,那么通过这个工厂获取到的就是 A 代理后的对象
如果 A 没有被 AOP 代理,那么这个工厂获取到的就是 A 实例化的对象
2.A 进行属性注入时,去「创建 B」
3.B 进行属性注入,需要 A ,则「从三级缓存中去取 A 工厂代理对象」并注入,然后删除三级缓存中的 A 工厂,将 A 对象放入二级缓存
4.B 完成后续属性注入,直到初始化结束,将 B 放入一级缓存
5.「A 从一级缓存中取到 B 并且注入 B」, 直到完成后续操作,将 A 从二级缓存删除并且放入一级缓存,循环依赖结束
spring 解决循环依赖有两个前提条件:
1.「不全是构造器方式」的循环依赖(否则无法分离初始化和实例化的操作)
2.「必须是单例」(否则无法保证是同一对象)
1.「不全是构造器方式」的循环依赖(否则无法分离初始化和实例化的操作)
2.「必须是单例」(否则无法保证是同一对象)
为什么要使用三级缓存,二级缓存不能解决吗?
不可以,主要是为了⽣成代理对象。
因为三级缓存中放的是⽣成具体对象的匿名内部类,他可以⽣成代理对象,也可以是普通的实例对象。
使⽤三级缓存主要是为了保证不管什么时候使⽤的都是⼀个对象。
假设只有⼆级缓存的情况,往⼆级缓存中放的显示⼀个普通的Bean对象, BeanPostProcessor 去⽣成
代理对象之后,覆盖掉⼆级缓存中的普通Bean对象,那么多线程环境下可能取到的对象就不⼀致了。
因为三级缓存中放的是⽣成具体对象的匿名内部类,他可以⽣成代理对象,也可以是普通的实例对象。
使⽤三级缓存主要是为了保证不管什么时候使⽤的都是⼀个对象。
假设只有⼆级缓存的情况,往⼆级缓存中放的显示⼀个普通的Bean对象, BeanPostProcessor 去⽣成
代理对象之后,覆盖掉⼆级缓存中的普通Bean对象,那么多线程环境下可能取到的对象就不⼀致了。
Spring中后置处理器的作⽤?
Spring中的后置处理器分为BeanFactory后置处理器和Bean后置处理器,它们是Spring底层源码架构设计 中⾮常重要的⼀种机制,同时开发者也可以利⽤这两种后置处理器来进⾏扩展。
BeanFactory后置处理器 表示针对BeanFactory的处理器,Spring启动过程中,会先创建出BeanFactory实例,然后利⽤ BeanFactory处理器来加⼯BeanFactory,⽐如Spring的扫描就是基于BeanFactory后置处理器来实现的;
Bean后置处理器也类似,Spring在创建⼀个Bean的过程中,⾸先会实例化得到⼀个对象,然后再 利⽤Bean后置处理器来对该实例对象进⾏加⼯,⽐如我们常说的依赖注⼊就是基于⼀个Bean后置处理器 来实现的,通过该Bean后置处理器来给实例对象中加了@Autowired注解的属性⾃动赋值,还⽐如我们常 说的AOP,也是利⽤⼀个Bean后置处理器来实现的,基于原实例对象,判断是否需要进⾏AOP,如果需 要,那么就基于原实例对象进⾏动态代理,⽣成⼀个代理对象。
BeanFactory后置处理器 表示针对BeanFactory的处理器,Spring启动过程中,会先创建出BeanFactory实例,然后利⽤ BeanFactory处理器来加⼯BeanFactory,⽐如Spring的扫描就是基于BeanFactory后置处理器来实现的;
Bean后置处理器也类似,Spring在创建⼀个Bean的过程中,⾸先会实例化得到⼀个对象,然后再 利⽤Bean后置处理器来对该实例对象进⾏加⼯,⽐如我们常说的依赖注⼊就是基于⼀个Bean后置处理器 来实现的,通过该Bean后置处理器来给实例对象中加了@Autowired注解的属性⾃动赋值,还⽐如我们常 说的AOP,也是利⽤⼀个Bean后置处理器来实现的,基于原实例对象,判断是否需要进⾏AOP,如果需 要,那么就基于原实例对象进⾏动态代理,⽣成⼀个代理对象。
为什么要用SpringBoot?
一、独立运行
Spring Boot而且内嵌了各种servlet容器,Tomcat、Jetty等,现在不再需要打成war包部署到容器
中,Spring Boot只要打成一个可执行的jar包就能独立运行,所有的依赖包都在一个jar包内。
二、简化配置
spring-boot-starter-web启动器自动依赖其他组件,简少了maven的配置。
三、自动配置
Spring Boot能根据当前类路径下的类、jar包来自动配置bean,如添加一个spring-boot-starterweb启动器就能拥有web的功能,无需其他配置。
四、无代码生成和XML配置
Spring Boot配置过程中无代码生成,也无需XML配置文件就能完成所有配置工作,这一切都是借助
于条件注解完成的,这也是Spring4.x的核心功能之一。
五、应用监控
Spring Boot提供一系列端点可以监控服务及应用,做健康检测。
Spring Boot而且内嵌了各种servlet容器,Tomcat、Jetty等,现在不再需要打成war包部署到容器
中,Spring Boot只要打成一个可执行的jar包就能独立运行,所有的依赖包都在一个jar包内。
二、简化配置
spring-boot-starter-web启动器自动依赖其他组件,简少了maven的配置。
三、自动配置
Spring Boot能根据当前类路径下的类、jar包来自动配置bean,如添加一个spring-boot-starterweb启动器就能拥有web的功能,无需其他配置。
四、无代码生成和XML配置
Spring Boot配置过程中无代码生成,也无需XML配置文件就能完成所有配置工作,这一切都是借助
于条件注解完成的,这也是Spring4.x的核心功能之一。
五、应用监控
Spring Boot提供一系列端点可以监控服务及应用,做健康检测。
说说常⽤的SpringBoot注解,及其实现
1. @SpringBootApplication注解:这个注解标识了⼀个SpringBoot⼯程,它实际上是另外三个注解的组 合,这三个注解是:
--a. @SpringBootConfiguration:这个注解实际就是⼀个@Configuration,表示启动类也是⼀个配 置类
--b. @EnableAutoConfiguration:向Spring容器中导⼊了⼀个Selector,⽤来加载ClassPath下 SpringFactories中所定义的⾃动配置类,将这些⾃动加载为配置Bean
--c. @ComponentScan:标识扫描路径,因为默认是没有配置实际扫描路径,所以SpringBoot扫描的 路径是启动类所在的当前⽬录
2. @Bean注解:⽤来定义Bean,类似于XML中的<bean>标签,Spring在启动时,会对加了@Bean注解 的⽅法进⾏解析,将⽅法的名字做为beanName,并通过执⾏⽅法得到bean对象
3. @Controller、@Service、@ResponseBody、@Autowired都可以说
--a. @SpringBootConfiguration:这个注解实际就是⼀个@Configuration,表示启动类也是⼀个配 置类
--b. @EnableAutoConfiguration:向Spring容器中导⼊了⼀个Selector,⽤来加载ClassPath下 SpringFactories中所定义的⾃动配置类,将这些⾃动加载为配置Bean
--c. @ComponentScan:标识扫描路径,因为默认是没有配置实际扫描路径,所以SpringBoot扫描的 路径是启动类所在的当前⽬录
2. @Bean注解:⽤来定义Bean,类似于XML中的<bean>标签,Spring在启动时,会对加了@Bean注解 的⽅法进⾏解析,将⽅法的名字做为beanName,并通过执⾏⽅法得到bean对象
3. @Controller、@Service、@ResponseBody、@Autowired都可以说
springBoot 自动装配原理?
1.容器在启动的时候会调用 EnableAutoConfigurationImportSelector.class 的 selectImports方法「获取一个全面的常用 BeanConfiguration 列表」
2.之后会读取 spring-boot-autoconfigure.jar 下面的spring.factories,「获取到所有的 Spring 相关的 Bean 的全限定名 ClassName」
3.之后继续「调用 filter 来一一筛选」,过滤掉一些我们不需要不符合条件的 Bean
4.最后把符合条件的 BeanConfiguration 注入默认的 EnableConfigurationPropertie 类里面的属性值,并且「注入到 IOC 环境当中」
2.之后会读取 spring-boot-autoconfigure.jar 下面的spring.factories,「获取到所有的 Spring 相关的 Bean 的全限定名 ClassName」
3.之后继续「调用 filter 来一一筛选」,过滤掉一些我们不需要不符合条件的 Bean
4.最后把符合条件的 BeanConfiguration 注入默认的 EnableConfigurationPropertie 类里面的属性值,并且「注入到 IOC 环境当中」
最后,说说Spring Boot 启动流程吧?
1. 准备环境,根据不同的环境创建不同的Environment
2. 准备、加载上下⽂,为不同的环境选择不同的Spring Context,然后加载资源,配置Bean
3. 初始化,这个阶段刷新Spring Context,启动应⽤
4. 最后结束流程
2. 准备、加载上下⽂,为不同的环境选择不同的Spring Context,然后加载资源,配置Bean
3. 初始化,这个阶段刷新Spring Context,启动应⽤
4. 最后结束流程
JVM
介绍一下双亲委派模型,它的好处是什么?
类加载器⾃顶向下分为:
1. Bootstrap ClassLoader启动类加载器:默认会去加载JAVA_HOME/lib⽬录下的jar
2. Extention ClassLoader扩展类加载器:默认去加载JAVA_HOME/lib/ext⽬录下的jar
3. Application ClassLoader应⽤程序类加载器:⽐如我们的web应⽤,会加载web程序中ClassPath
下的类
4. User ClassLoader⽤户⾃定义类加载器:由⽤户⾃⼰定义
1. Bootstrap ClassLoader启动类加载器:默认会去加载JAVA_HOME/lib⽬录下的jar
2. Extention ClassLoader扩展类加载器:默认去加载JAVA_HOME/lib/ext⽬录下的jar
3. Application ClassLoader应⽤程序类加载器:⽐如我们的web应⽤,会加载web程序中ClassPath
下的类
4. User ClassLoader⽤户⾃定义类加载器:由⽤户⾃⼰定义
当我们在加载类的时候,⾸先都会向上询问⾃⼰的⽗加载器是否已经加载,如果没有则依次向上询问,
如果没有加载,则从上到下依次尝试是否能加载当前类,直到加载成功。
如果没有加载,则从上到下依次尝试是否能加载当前类,直到加载成功。
「好处:」
说这个问题前我要先和大家说一个概念,「Jvm 中类的唯一性是由类本身和加载这个类的类加载器决定的」,简单的说,如果有个a类,如果被两个不同的类加载器加载,那么他们必不相等。你看到这里会不会想到所有类的父类都是 Object 是怎么实现的了吗?是因为无论哪一个类加载器加载 Object 类,都会交给最顶层的启动类加载器去加载,这样就「保证了 Object 类在 Jvm 中是唯一的」
说这个问题前我要先和大家说一个概念,「Jvm 中类的唯一性是由类本身和加载这个类的类加载器决定的」,简单的说,如果有个a类,如果被两个不同的类加载器加载,那么他们必不相等。你看到这里会不会想到所有类的父类都是 Object 是怎么实现的了吗?是因为无论哪一个类加载器加载 Object 类,都会交给最顶层的启动类加载器去加载,这样就「保证了 Object 类在 Jvm 中是唯一的」
说说 JVM 内存区域
「1.程序计数器」
程序计数器是「程序控制流的指示器,循环,跳转,异常处理,线程的恢复等工作都需要依赖程序计数器去完成」。程序计数器是「线程私有」的,它的「生命周期是和线程保持一致」的,我们知道,N 个核心数的 CPU 在同一时刻,最多有 N个线程同时运行,在我们真实的使用过程中可能会创建很多线程,JVM 的多线程其实是通过线程轮流切换,分配处理器执行时间来实现的。既然涉及的线程切换,所以每条线程必须有一个独立的程序计数器。
程序计数器是「程序控制流的指示器,循环,跳转,异常处理,线程的恢复等工作都需要依赖程序计数器去完成」。程序计数器是「线程私有」的,它的「生命周期是和线程保持一致」的,我们知道,N 个核心数的 CPU 在同一时刻,最多有 N个线程同时运行,在我们真实的使用过程中可能会创建很多线程,JVM 的多线程其实是通过线程轮流切换,分配处理器执行时间来实现的。既然涉及的线程切换,所以每条线程必须有一个独立的程序计数器。
「2.虚拟机栈」
虚拟机栈,其描述的就是线程内存模型,「也可以称作线程栈」,也是每个「线程私有」的,「生命周期与线程保持一致」。在每个方法执行的时候,jvm 都会同步创建一个栈帧去存储局部变量表,操作数栈,动态连接,方法出口等信息。一个方法的生命周期就贯彻了一个栈帧从入栈到出栈的全部过程。
虚拟机栈,其描述的就是线程内存模型,「也可以称作线程栈」,也是每个「线程私有」的,「生命周期与线程保持一致」。在每个方法执行的时候,jvm 都会同步创建一个栈帧去存储局部变量表,操作数栈,动态连接,方法出口等信息。一个方法的生命周期就贯彻了一个栈帧从入栈到出栈的全部过程。
「3.本地方法栈」本地方法栈的概念很好理解,我们知道,java底层用了很多c的代码去实现,而其调用c端的方法上都会有native,代表本地方法服务,而本地方法栈是为本地方法服务的
「4.堆」Java中的堆是用来存储对象本身的以及数组(数组引用是存放在Java栈中的)。堆是被所有线程共享的,在JVM中只有一个堆。
「5.方法区」
方法区也是所有「线程共享」的区域,它「存储」了被 jvm 加载的「类型信息、常量、静态变量等数据」。运行时常量池就是方法区的一部分,编译期生成的各种字面量与符号引用就存储在常量池中。
方法区也是所有「线程共享」的区域,它「存储」了被 jvm 加载的「类型信息、常量、静态变量等数据」。运行时常量池就是方法区的一部分,编译期生成的各种字面量与符号引用就存储在常量池中。
「6.直接内存」
这部分数据并「不是 jvm 运行时数据区的一部分」,nio 就会使用到直接内存,也可以说「堆外内存」,通常会「配合虚引用一起去使用」,就是为了资源释放,会将堆外内存开辟空间的信息存储到一个队列中,然后GC会去清理这部分空间。堆外内存优势在 IO 操作上,对于网络 IO,使用 Socket 发送数据时,能够节省堆内存到堆外内存的数据拷贝,所以性能更高。看过 Netty 源码的同学应该了解,Netty 使用堆外内存池来实现零拷贝技术。对于磁盘 IO 时,也可以使用内存映射,来提升性能。另外,更重要的几乎不用考虑堆内存烦人的 GC 问题。但是既然是内存。也会受到本机总内存的限制,
这部分数据并「不是 jvm 运行时数据区的一部分」,nio 就会使用到直接内存,也可以说「堆外内存」,通常会「配合虚引用一起去使用」,就是为了资源释放,会将堆外内存开辟空间的信息存储到一个队列中,然后GC会去清理这部分空间。堆外内存优势在 IO 操作上,对于网络 IO,使用 Socket 发送数据时,能够节省堆内存到堆外内存的数据拷贝,所以性能更高。看过 Netty 源码的同学应该了解,Netty 使用堆外内存池来实现零拷贝技术。对于磁盘 IO 时,也可以使用内存映射,来提升性能。另外,更重要的几乎不用考虑堆内存烦人的 GC 问题。但是既然是内存。也会受到本机总内存的限制,
JVM生命周期
启动。启动一个Java程序时,一个JVM实例就产生了,任何一个拥有public static void
main(String[] args)函数的class都可以作为JVM实例运行的起点
运行。main()作为该程序初始线程的起点,任何其他线程均由该线程启动。JVM内部有两种线程:守护线程和非守护线程,main()属于非守护线程,守护线程通常由JVM自己使用,java程序也可以表明自己创建的线程是守护线程
消亡。当程序中的所有非守护线程都终止时,JVM才退出;若安全管理器允许,程序也可以使用Runtime类或者System.exit()来退出
main(String[] args)函数的class都可以作为JVM实例运行的起点
运行。main()作为该程序初始线程的起点,任何其他线程均由该线程启动。JVM内部有两种线程:守护线程和非守护线程,main()属于非守护线程,守护线程通常由JVM自己使用,java程序也可以表明自己创建的线程是守护线程
消亡。当程序中的所有非守护线程都终止时,JVM才退出;若安全管理器允许,程序也可以使用Runtime类或者System.exit()来退出
调优命令有哪些?
Sun JDK监控和故障处理命令有jps jstat jmap jhat jstack jinfo
jps,JVM Process Status Tool,显示指定系统内所有的HotSpot虚拟机进程。
jstat,JVM statistics Monitoring是用于监视虚拟机运行时状态信息的命令,它可以显示出虚拟
机进程中的类装载、内存、垃圾收集、JIT编译等运行数据。
jmap,JVM Memory Map命令用于生成heap dump文件
jhat,JVM Heap Analysis Tool命令是与jmap搭配使用,用来分析jmap生成的dump,jhat内
置了一个微型的HTTP/HTML服务器,生成dump的分析结果后,可以在浏览器中查看
jstack,用于生成java虚拟机当前时刻的线程快照。
jinfo,JVM Configuration info 这个命令作用是实时查看和调整虚拟机运行参数。
jps,JVM Process Status Tool,显示指定系统内所有的HotSpot虚拟机进程。
jstat,JVM statistics Monitoring是用于监视虚拟机运行时状态信息的命令,它可以显示出虚拟
机进程中的类装载、内存、垃圾收集、JIT编译等运行数据。
jmap,JVM Memory Map命令用于生成heap dump文件
jhat,JVM Heap Analysis Tool命令是与jmap搭配使用,用来分析jmap生成的dump,jhat内
置了一个微型的HTTP/HTML服务器,生成dump的分析结果后,可以在浏览器中查看
jstack,用于生成java虚拟机当前时刻的线程快照。
jinfo,JVM Configuration info 这个命令作用是实时查看和调整虚拟机运行参数。
常用的 jvm 调优的参数都有哪些?
-Xms2g:初始化推大小为 2g;
-Xmx2g:堆最大内存为 2g;
-XX:NewRatio=4:设置年轻的和老年代的内存比例为 1:4;
-XX:SurvivorRatio=8:设置新生代 Eden 和 Survivor 比例为 8:2;
–XX:+UseParNewGC:指定使用 ParNew + Serial Old 垃圾回收器组合;
-XX:+UseParallelOldGC:指定使用 ParNew + ParNew Old 垃圾回收器组合;
-XX:+UseConcMarkSweepGC:指定使用 CMS + Serial Old 垃圾回收器组合;
-XX:+PrintGC:开启打印 gc 信息;
-XX:+PrintGCDetails:打印 gc 详细信息。
-Xmx2g:堆最大内存为 2g;
-XX:NewRatio=4:设置年轻的和老年代的内存比例为 1:4;
-XX:SurvivorRatio=8:设置新生代 Eden 和 Survivor 比例为 8:2;
–XX:+UseParNewGC:指定使用 ParNew + Serial Old 垃圾回收器组合;
-XX:+UseParallelOldGC:指定使用 ParNew + ParNew Old 垃圾回收器组合;
-XX:+UseConcMarkSweepGC:指定使用 CMS + Serial Old 垃圾回收器组合;
-XX:+PrintGC:开启打印 gc 信息;
-XX:+PrintGCDetails:打印 gc 详细信息。
jvm调优步骤?
分析系统系统运行情况:分析GC日志及dump文件,判断是否需要优化,确定瓶颈问题点;
确定JVM调优量化目标;
确定JVM调优参数(根据历史JVM参数来调整);
依次确定调优内存、延迟、吞吐量等指标;
对比观察调优前后的差异;
不断的分析和调整,直到找到合适的JVM参数配置;
找到最合适的参数,将这些参数应用到所有服务器,并进行后续跟踪。
确定JVM调优量化目标;
确定JVM调优参数(根据历史JVM参数来调整);
依次确定调优内存、延迟、吞吐量等指标;
对比观察调优前后的差异;
不断的分析和调整,直到找到合适的JVM参数配置;
找到最合适的参数,将这些参数应用到所有服务器,并进行后续跟踪。
项目如何排查JVM的问题?
分两种情况:
1.系统还在运行:
通过公司的监控平台查看jvm的运行情况,比如gc频率,线程数等等
--每次gc回收的垃圾多不,如果回收的多,可以尝试把新生代的内存调大
--系统此时的访问量大不大,系统压力如何
--每次fullgc只回收一点内存,看下dump文件,看下内存里都是有哪些对象,看下代码是不是有问题,比如说system.gc()
具体场景具体分析
2.系统挂了:
一般都会设置当OOM生成dump文件,然后找运维要dump⽂件(生产机器开发一般没权限);然后把dump文件放到mat工具中进行分析,mat工具挺好用的,会直接提示哪里的代码可能会有问题,先按提示排查问题
总之,调优不是⼀蹴⽽就的,需要分析、推理、实践、总结、再分析,最终定位到具体的问题
1.系统还在运行:
通过公司的监控平台查看jvm的运行情况,比如gc频率,线程数等等
--每次gc回收的垃圾多不,如果回收的多,可以尝试把新生代的内存调大
--系统此时的访问量大不大,系统压力如何
--每次fullgc只回收一点内存,看下dump文件,看下内存里都是有哪些对象,看下代码是不是有问题,比如说system.gc()
具体场景具体分析
2.系统挂了:
一般都会设置当OOM生成dump文件,然后找运维要dump⽂件(生产机器开发一般没权限);然后把dump文件放到mat工具中进行分析,mat工具挺好用的,会直接提示哪里的代码可能会有问题,先按提示排查问题
总之,调优不是⼀蹴⽽就的,需要分析、推理、实践、总结、再分析,最终定位到具体的问题
垃圾回收器是怎样寻找 GC Roots 的?
我们在前面说明了根可达算法是通过 GC Roots 来找到存活的对象的,也定义了 GC Roots,那么垃圾回收器是怎样寻找GC Roots 的呢?首先,「「为了保证结果的准确性,GC Roots枚举时是要在STW的情况下进行的」」,但是由于 JAVA 应用越来越大,所以也不能逐个检查每个对象是否为 GC Root,那将消耗大量的时间。一个很自然的想法是,能不能用空间换时间,在某个时候把栈上代表引用的位置全部记录下来,这样到真正 GC 的时候就可以直接读取,而不用再一点一点的扫描了。事实上,大部分主流的虚拟机也正是这么做的,比如 HotSpot ,它使用一种叫做 「「OopMap」」 的数据结构来记录这类信息。
什么是Stop The World ? 什么是OopMap?什么是安全
点?
点?
进行垃圾回收的过程中,会涉及对象的移动。为了保证对象引用更新的正确性,必须暂停所有的用
户线程,像这样的停顿,虚拟机设计者形象描述为「Stop The World」。也简称为STW。
户线程,像这样的停顿,虚拟机设计者形象描述为「Stop The World」。也简称为STW。
在HotSpot中,有个数据结构(映射表)称为「OopMap」。一旦类加载动作完成的时候,
HotSpot就会把对象内什么偏移量上是什么类型的数据计算出来,记录到OopMap。在即时编译过
程中,也会在「特定的位置」生成 OopMap,记录下栈上和寄存器里哪些位置是引用。
HotSpot就会把对象内什么偏移量上是什么类型的数据计算出来,记录到OopMap。在即时编译过
程中,也会在「特定的位置」生成 OopMap,记录下栈上和寄存器里哪些位置是引用。
这些特定的位置主要在:
1.循环的末尾(非 counted 循环)
2.方法临返回前 / 调用方法的call指令后
3.可能抛异常的位置
这些位置就叫作「安全点(safepoint)。」 用户程序执行时并非在代码指令流的任意位置都能够在
停顿下来开始垃圾收集,而是必须是执行到安全点才能够暂停。
1.循环的末尾(非 counted 循环)
2.方法临返回前 / 调用方法的call指令后
3.可能抛异常的位置
这些位置就叫作「安全点(safepoint)。」 用户程序执行时并非在代码指令流的任意位置都能够在
停顿下来开始垃圾收集,而是必须是执行到安全点才能够暂停。
从线程角度看,安全点可以理解成是在「「代码执行过程中」」的一些「「特殊位置」」,当线程执行到这些位置的时候,说明「「虚拟机当前的状态是安全」」的。比如:「「方法调用、循环跳转、异常跳转等这些地方才会产生安全点」」。如果有需要,可以在这个位置暂停,比如发生GC时,需要暂停所有活动线程,但是线程在这个时刻,还没有执行到一个安全点,所以该线程应该继续执行,到达下一个安全点的时候暂停,等待 GC 结束。那么如何让线程在垃圾回收的时候都跑到最近的安全点呢?这里有「「两种方式」」:
「抢先式中断」
抢先式中断:就是在stw的时候,先让所有线程「「完全中断」」,如果中断的地方不在安全点上,然后「「再激活」」,「「直到运行到安全点的位置」」再中断。
「主动式中断」
主动式中断:在安全点的位置打一个标志位,每个线程执行都去轮询这个标志位,如果为真,就在最近的安全点挂起。
抢先式中断:就是在stw的时候,先让所有线程「「完全中断」」,如果中断的地方不在安全点上,然后「「再激活」」,「「直到运行到安全点的位置」」再中断。
「主动式中断」
主动式中断:在安全点的位置打一个标志位,每个线程执行都去轮询这个标志位,如果为真,就在最近的安全点挂起。
什么是逃逸分析?
逃逸分析是指分析指针动态范围的方法,它同编译器优化原理的指针分析和外形分析相关联。当变
量(或者对象)在方法中分配后,其指针有可能被返回或者被全局引用,这样就会被其他方法或者
线程所引用,这种现象称作指针(或者引用)的逃逸(Escape)。通俗点讲,如果一个对象的指针被
多个方法或者线程引用时,那么我们就称这个对象的指针发生了逃逸。
量(或者对象)在方法中分配后,其指针有可能被返回或者被全局引用,这样就会被其他方法或者
线程所引用,这种现象称作指针(或者引用)的逃逸(Escape)。通俗点讲,如果一个对象的指针被
多个方法或者线程引用时,那么我们就称这个对象的指针发生了逃逸。
「逃逸分析的好处」
栈上分配,可以降低垃圾收集器运行的频率。
同步消除,如果发现某个对象只能从一个线程可访问,那么在这个对象上的操作可以不需要同
步。
标量替换,把对象分解成一个个基本类型,并且内存分配不再是分配在堆上,而是分配在栈
上。这样的好处有,一、减少内存使用,因为不用生成对象头。二、程序内存回收效率高,并
且GC频率也会减少。
栈上分配,可以降低垃圾收集器运行的频率。
同步消除,如果发现某个对象只能从一个线程可访问,那么在这个对象上的操作可以不需要同
步。
标量替换,把对象分解成一个个基本类型,并且内存分配不再是分配在堆上,而是分配在栈
上。这样的好处有,一、减少内存使用,因为不用生成对象头。二、程序内存回收效率高,并
且GC频率也会减少。
JVM为什么能跨平台?
两大原因:
(1)Java 文件经过编译后生成和平台无关的 class 文件。
(2)而Java虚拟机(JVM)是不跨平台的,java工具会把统一的class文件,加载到对应的JVM,又因为该JVM是和这个系统是对应的,所以就可以运行。
(1)Java 文件经过编译后生成和平台无关的 class 文件。
(2)而Java虚拟机(JVM)是不跨平台的,java工具会把统一的class文件,加载到对应的JVM,又因为该JVM是和这个系统是对应的,所以就可以运行。
说一下 jvm 的主要组成部分?及其作用?
类加载器(ClassLoader)
运行时数据区(Runtime Data Area)
执行引擎(Execution Engine)
本地库接口(Native Interface)
运行时数据区(Runtime Data Area)
执行引擎(Execution Engine)
本地库接口(Native Interface)
「Class loader(类装载):」 根据给定的全限定名类名(如:java.lang.Object)来装载class文件
到运行时数据区的方法区中。
「Execution engine(执行引擎)」:执行class的指令。
「Native Interface(本地接口):」 与native lib交互,是其它编程语言交互的接口。
「Runtime data area(运行时数据区域)」:即我们常说的JVM的内存。
到运行时数据区的方法区中。
「Execution engine(执行引擎)」:执行class的指令。
「Native Interface(本地接口):」 与native lib交互,是其它编程语言交互的接口。
「Runtime data area(运行时数据区域)」:即我们常说的JVM的内存。
常见的垃圾回收器?
Serial是一个「「单线程」」的垃圾回收器,「「采用复制算法负责新生代」」的垃圾回收工作,可以与 CMS 垃圾回收器一起搭配工作。
ParNew:Serial的多线程版本,⽤于和CMS配合使⽤
CMS收集器是以获取最短停顿时间为⽬标的收集器,相对于其他
的收集器STW的时间更短暂,可以并⾏收集是他的特点,同时他基于标记-清除算法,整个GC的过程分
为4步。
1. 初始标记:标记GC ROOT能关联到的对象,需要STW
2. 并发标记:从GCRoots的直接关联对象开始遍历整个对象图的过程,不需要STW
3. 重新标记:为了修正并发标记期间,因⽤户程序继续运作⽽导致标记产⽣改变的标记,需要STW
4. 并发清除:清理删除掉标记阶段判断的已经死亡的对象,不需要STW
从整个过程来看,并发标记和并发清除的耗时最⻓,但是不需要停⽌⽤户线程,⽽初始标记和重新标记
的耗时较短,但是需要停⽌⽤户线程,总体⽽⾔,整个过程造成的停顿时间较短,⼤部分时候是可以和
⽤户线程⼀起⼯作的。
的收集器STW的时间更短暂,可以并⾏收集是他的特点,同时他基于标记-清除算法,整个GC的过程分
为4步。
1. 初始标记:标记GC ROOT能关联到的对象,需要STW
2. 并发标记:从GCRoots的直接关联对象开始遍历整个对象图的过程,不需要STW
3. 重新标记:为了修正并发标记期间,因⽤户程序继续运作⽽导致标记产⽣改变的标记,需要STW
4. 并发清除:清理删除掉标记阶段判断的已经死亡的对象,不需要STW
从整个过程来看,并发标记和并发清除的耗时最⻓,但是不需要停⽌⽤户线程,⽽初始标记和重新标记
的耗时较短,但是需要停⽌⽤户线程,总体⽽⾔,整个过程造成的停顿时间较短,⼤部分时候是可以和
⽤户线程⼀起⼯作的。
「1.初始标记」
初始标记只是标记出来「「和 GC Roots 直接关联」」的对象,整个速度是非常快的,为了保证标记的准确,这部分会在 「「STW」」 的状态下运行。
「2.并发标记」
并发标记这个阶段会直接根据第一步关联的对象找到「「所有的引用」」关系,这一部分时刻用户线程「「并发运行」」的,虽然耗时较长,但是不会有很大的影响。
「3.重新标记」
重新标记是为了解决第二步并发标记所导致的标错情况,这里简单举个例子:并发标记时a没有被任何对象引用,此时垃圾回收器将该对象标位垃圾,在之后的标记过程中,a又被其他对象引用了,这时候如果不进行重新标记就会发生「「误清除」」。这部分内容也是在「「STW」」的情况下去标记的。
「4.并发清除」
这一步就是最后的清除阶段了,将之前「「真正确认为垃圾的对象回收」」,这部分会和用户线程一起并发执行。
初始标记只是标记出来「「和 GC Roots 直接关联」」的对象,整个速度是非常快的,为了保证标记的准确,这部分会在 「「STW」」 的状态下运行。
「2.并发标记」
并发标记这个阶段会直接根据第一步关联的对象找到「「所有的引用」」关系,这一部分时刻用户线程「「并发运行」」的,虽然耗时较长,但是不会有很大的影响。
「3.重新标记」
重新标记是为了解决第二步并发标记所导致的标错情况,这里简单举个例子:并发标记时a没有被任何对象引用,此时垃圾回收器将该对象标位垃圾,在之后的标记过程中,a又被其他对象引用了,这时候如果不进行重新标记就会发生「「误清除」」。这部分内容也是在「「STW」」的情况下去标记的。
「4.并发清除」
这一步就是最后的清除阶段了,将之前「「真正确认为垃圾的对象回收」」,这部分会和用户线程一起并发执行。
CMS的「「三个缺点」」:
「1.影响用户线程的执行效率」
CMS默认启动的回收线程数是(处理器核心数 + 3)/ 4 ,由于是和用户线程一起并发清理,那么势必会影响到用户线程的执行速度,并且这个影响「「随着核心线程数的递减而增加」」。所以 JVM 提供了一种 "「「增量式并发收集器」」"的 CMS 变种,主要是用来减少垃圾回收线程独占资源的时间,所以会感觉到回收时间变长,这样的话「「单位时间内处理垃圾的效率就会降低」」,也是一种缓和的方案。
「2.会产生"浮动垃圾"」
之前说到 CMS 真正清理垃圾是和用户线程一起进行的,在「「清理」」这部分垃圾的时候「「用户线程会产生新的垃圾」」,这部分垃圾就叫做浮动垃圾,并且只能等着下一次的垃圾回收再清除。
「3.会产生碎片化的空间」
CMS 是使用了标记删除的算法去清理垃圾的,而这种算法的缺点就是会产生「「碎片化」」,后续可能会「「导致大对象无法分配」」从而触发「「和 Serial Old 一起配合使用」」来处理碎片化的问题,当然这也处于 「「STW」」的情况下,所以当 java 应用非常庞大时,如果采用了 CMS 垃圾回收器,产生了碎片化,那么在 STW 来处理碎片化的时间会非常之久。
「1.影响用户线程的执行效率」
CMS默认启动的回收线程数是(处理器核心数 + 3)/ 4 ,由于是和用户线程一起并发清理,那么势必会影响到用户线程的执行速度,并且这个影响「「随着核心线程数的递减而增加」」。所以 JVM 提供了一种 "「「增量式并发收集器」」"的 CMS 变种,主要是用来减少垃圾回收线程独占资源的时间,所以会感觉到回收时间变长,这样的话「「单位时间内处理垃圾的效率就会降低」」,也是一种缓和的方案。
「2.会产生"浮动垃圾"」
之前说到 CMS 真正清理垃圾是和用户线程一起进行的,在「「清理」」这部分垃圾的时候「「用户线程会产生新的垃圾」」,这部分垃圾就叫做浮动垃圾,并且只能等着下一次的垃圾回收再清除。
「3.会产生碎片化的空间」
CMS 是使用了标记删除的算法去清理垃圾的,而这种算法的缺点就是会产生「「碎片化」」,后续可能会「「导致大对象无法分配」」从而触发「「和 Serial Old 一起配合使用」」来处理碎片化的问题,当然这也处于 「「STW」」的情况下,所以当 java 应用非常庞大时,如果采用了 CMS 垃圾回收器,产生了碎片化,那么在 STW 来处理碎片化的时间会非常之久。
G1作为JDK9之后的服务端默认收集器,且不再区分年轻代和⽼年代进⾏垃圾回收,他把内存划分为多个
Region,每个Region的⼤⼩可以通过-XX:G1HeapRegionSize设置,⼤⼩为1~32M,对于⼤对象的存
储则衍⽣出Humongous的概念,超过Region⼤⼩⼀半的对象会被认为是⼤对象,⽽超过整个Region⼤
⼩的对象被认为是超级⼤对象,将会被存储在连续的N个Humongous Region中,G1在进⾏回收的时候
会在后台维护⼀个优先级列表,每次根据⽤户设定允许的收集停顿时间优先回收收益最⼤的Region。
Region,每个Region的⼤⼩可以通过-XX:G1HeapRegionSize设置,⼤⼩为1~32M,对于⼤对象的存
储则衍⽣出Humongous的概念,超过Region⼤⼩⼀半的对象会被认为是⼤对象,⽽超过整个Region⼤
⼩的对象被认为是超级⼤对象,将会被存储在连续的N个Humongous Region中,G1在进⾏回收的时候
会在后台维护⼀个优先级列表,每次根据⽤户设定允许的收集停顿时间优先回收收益最⼤的Region。
G1的回收过程分为以下四个步骤:
1. 初始标记:标记GC ROOT能关联到的对象,需要STW
2. 并发标记:从GCRoots的直接关联对象开始遍历整个对象图的过程,扫描完成后还会重新处理并发
标记过程中产⽣变动的对象
3. 最终标记:短暂暂停⽤户线程,再处理⼀次,需要STW
4. 筛选回收:更新Region的统计数据,对每个Region的回收价值和成本排序,根据⽤户设置的停顿
时间制定回收计划。再把需要回收的Region中存活对象复制到空的Region,同时清理旧的
Region。需要STW
1. 初始标记:标记GC ROOT能关联到的对象,需要STW
2. 并发标记:从GCRoots的直接关联对象开始遍历整个对象图的过程,扫描完成后还会重新处理并发
标记过程中产⽣变动的对象
3. 最终标记:短暂暂停⽤户线程,再处理⼀次,需要STW
4. 筛选回收:更新Region的统计数据,对每个Region的回收价值和成本排序,根据⽤户设置的停顿
时间制定回收计划。再把需要回收的Region中存活对象复制到空的Region,同时清理旧的
Region。需要STW
总的来说除了并发标记之外,其他⼏个过程也还是需要短暂的STW,G1的⽬标是在停顿和延迟可控的情
况下尽可能提⾼吞吐量。
况下尽可能提⾼吞吐量。
什么情况下内存对象会从新生代进入老年代?
年龄达到15岁
默认是15岁,年龄在对象头中记录
动态年龄判断
当前放对象的Survivor区域里,一批对象的总大小大于这块区域的50%,那么此时大于等于这批年龄的对象直接进入老年代
大对象直接进入老年代
老年代空间分配担保机制
java 有哪四种引用类型?
「1.强引用」
"Object o = new Object()" 就是一种强引用关系,这也是我们在代码中最常用的一种引用关系。无论任何情况下,只要强引用关系还存在,垃圾回收器就不会回收掉被引用的对象。
"Object o = new Object()" 就是一种强引用关系,这也是我们在代码中最常用的一种引用关系。无论任何情况下,只要强引用关系还存在,垃圾回收器就不会回收掉被引用的对象。
「2.软引用」
当内存空间不足时,就会回收软引用对象。
当内存空间不足时,就会回收软引用对象。
「3.弱引用」
弱引用要比软引用更弱一点,它「「只能够存活到下次垃圾回收之前」」。也就是说,垃圾回收器开始工作,会回收掉所有只被弱引用关联的对象。
弱引用要比软引用更弱一点,它「「只能够存活到下次垃圾回收之前」」。也就是说,垃圾回收器开始工作,会回收掉所有只被弱引用关联的对象。
「4.虚引用」
虚引用是最弱的一种引用关系,它的唯一作用是用来作为一种通知。如零拷贝(Zero Copy),开辟了堆外内存,虚引用在这里使用,会将这部分信息存储到一个队列中,以便于后续对堆外内存的回收管理。
虚引用是最弱的一种引用关系,它的唯一作用是用来作为一种通知。如零拷贝(Zero Copy),开辟了堆外内存,虚引用在这里使用,会将这部分信息存储到一个队列中,以便于后续对堆外内存的回收管理。
说一下堆栈的区别?
栈是运行时单位,代表着逻辑,内含基本数据类型和堆中对象引用,所在区域连续,没有碎片;堆
是存储单位,代表着数据,可被多个栈共享(包括成员中基本数据类型、引用和引用对象),所在
区域不连续,会有碎片。
1、功能不同
栈内存用来存储局部变量和方法调用,而堆内存用来存储Java中的对象。无论是成员变量,局部变
量,还是类变量,它们指向的对象都存储在堆内存中。
2、共享性不同
栈内存是线程私有的。 堆内存是所有线程共有的。
3、异常错误不同
如果栈内存或者堆内存不足都会抛出异常。 栈空间不足:java.lang.StackOverFlowError。 堆空间
不足:java.lang.OutOfMemoryError。
4、空间大小
栈的空间大小远远小于堆的。
是存储单位,代表着数据,可被多个栈共享(包括成员中基本数据类型、引用和引用对象),所在
区域不连续,会有碎片。
1、功能不同
栈内存用来存储局部变量和方法调用,而堆内存用来存储Java中的对象。无论是成员变量,局部变
量,还是类变量,它们指向的对象都存储在堆内存中。
2、共享性不同
栈内存是线程私有的。 堆内存是所有线程共有的。
3、异常错误不同
如果栈内存或者堆内存不足都会抛出异常。 栈空间不足:java.lang.StackOverFlowError。 堆空间
不足:java.lang.OutOfMemoryError。
4、空间大小
栈的空间大小远远小于堆的。
队列和栈是什么?有什么区别?
队列和栈都是被用来预存储数据的。
队列允许先进先出检索元素,但也有例外的情况,Deque 接口允许从两端检索元素。
栈和队列很相似,但它运行对元素进行后进先出进行检索。
队列允许先进先出检索元素,但也有例外的情况,Deque 接口允许从两端检索元素。
栈和队列很相似,但它运行对元素进行后进先出进行检索。
知道new⼀个对象的过程吗?
当虚拟机遇⻅new关键字时候,实现判断当前类是否已经加载,如果类没有加载,⾸先执⾏类的加载机
制,加载完成后再为对象分配空间、初始化等
制,加载完成后再为对象分配空间、初始化等
加载:根据查找路径找到相应的 class 文件然后导入;
验证:检查加载的 class 文件的正确性;
准备:给类中的静态变量分配内存空间;
解析:虚拟机将常量池中的符号引用替换成直接引用的过程。符号引用就理解为一个标示,而在直接引用直接指向内存中的地址;
初始化:对静态变量和静态代码块执行初始化工作,如果存在⽗类,先对⽗类进⾏初始化
Ps:静态代码块是绝对线程安全的,只能隐式被java虚拟机在类加载过程中初始化调⽤!
验证:检查加载的 class 文件的正确性;
准备:给类中的静态变量分配内存空间;
解析:虚拟机将常量池中的符号引用替换成直接引用的过程。符号引用就理解为一个标示,而在直接引用直接指向内存中的地址;
初始化:对静态变量和静态代码块执行初始化工作,如果存在⽗类,先对⽗类进⾏初始化
Ps:静态代码块是绝对线程安全的,只能隐式被java虚拟机在类加载过程中初始化调⽤!
当类加载完成之后,紧接着就是对象分配内存空间和初始化的过程
1. ⾸先为对象分配合适⼤⼩的内存空间
2. 接着为实例变量赋默认值
3. 设置对象的头信息,对象hash码、GC分代年龄、元数据信息等
4. 执⾏构造函数(init)初始化
2. 接着为实例变量赋默认值
3. 设置对象的头信息,对象hash码、GC分代年龄、元数据信息等
4. 执⾏构造函数(init)初始化
如何自定义类加载器?
在自定义ClassLoader的子类时,两种方式:
重写loadClass方法(是实现双亲委派逻辑的地方,修改他会破坏双亲委派机制,不推荐)
重写findClass方法 (推荐)
重写loadClass方法(是实现双亲委派逻辑的地方,修改他会破坏双亲委派机制,不推荐)
重写findClass方法 (推荐)
垃圾对象是怎么找到的?
「1.引用计数算法」
「2.根可达算法」
这也是「「JVM 默认使用」」的寻找垃圾算法它的原理就是定义了一系列的根,我们把它称为 「「"GC Roots"」」 ,从 「「"GC Roots"」」 开始往下进行搜索,走过的路径我们把它称为 「「"引用链"」」 ,当一个对象到 「「"GC Roots"」」 之间没有任何引用链相连时,那么这个对象就可以被当做垃圾回收了。
GC Roots 有哪些?
两个栈: Java栈 和 Native 栈中所有引用的对象;
两个方法区:方法区中的常量和静态变量;
所有线程对象;
所有跨代引用对象;
和已知 GCRoots 对象同属一个CardTable 的其他对象
两个方法区:方法区中的常量和静态变量;
所有线程对象;
所有跨代引用对象;
和已知 GCRoots 对象同属一个CardTable 的其他对象
说一说分代收集理论
大多数的垃圾回收器都遵循了分代收集的理论进行设计,它建立在两个分代假说之上:
「「弱分代假说」」:绝大多数对象都是朝生夕灭的。
「「强分代假说」」:熬过越多次数垃圾回收过程的对象就越难消亡。
「「弱分代假说」」:绝大多数对象都是朝生夕灭的。
「「强分代假说」」:熬过越多次数垃圾回收过程的对象就越难消亡。
这两种假说的设计原则都是相同的:垃圾收集器「「应该将jvm划分出不同的区域」」,把那些较难回收的对象放在一起(一般指老年代),这个区域的垃圾回收频率就可以降低,减少垃圾回收的开销。剩下的区域(一般指新生代)可以用较高的频率去回收,并且只需要去关心那些存活的对象,也不用标记出需要回收的垃圾,这样就能够以较低的代价去完成垃圾回收。
什么时候会触发FullGC?
除了system.gc()外;
1.老年代内存不足或者达到阈值
2.方法区内存不足
3.survivor放不下,老年代也放不下
4.young gc时统计得到进入老年代的平均大小,大于老年代的剩余大小
1.老年代内存不足或者达到阈值
2.方法区内存不足
3.survivor放不下,老年代也放不下
4.young gc时统计得到进入老年代的平均大小,大于老年代的剩余大小
垃圾收集算法有哪些?
「1.标记清除算法」
这种算法的实现是很简单的,有两种方式
1.标记出垃圾,然后清理掉
2.标记出存货的对象,回收其他空间
这种算法的实现是很简单的,有两种方式
1.标记出垃圾,然后清理掉
2.标记出存货的对象,回收其他空间
这种算法有两个「缺点」
1.随着对象越来越多,那么所需要消耗的时间就会越来越多
2.标记清除后会导致碎片化,如果有大对象分配很有可能分配不下而出发另一次的垃圾收集动作
1.随着对象越来越多,那么所需要消耗的时间就会越来越多
2.标记清除后会导致碎片化,如果有大对象分配很有可能分配不下而出发另一次的垃圾收集动作
「2.标记复制算法」
这种算法解决了第一种算法碎片化的问题。就是「「开辟两块完全相同的区域」」,对象只在其中一篇区域内分配,然后「「标记」」出那些「「存活的对象,按顺序整体移到另外一个空间」」,如下图,可以看到回收后的对象是排列有序的,这种操作只需要移动指针就可以完成,效率很高,「「之后就回收移除前的空间」」。
这种算法解决了第一种算法碎片化的问题。就是「「开辟两块完全相同的区域」」,对象只在其中一篇区域内分配,然后「「标记」」出那些「「存活的对象,按顺序整体移到另外一个空间」」,如下图,可以看到回收后的对象是排列有序的,这种操作只需要移动指针就可以完成,效率很高,「「之后就回收移除前的空间」」。
这种算法的缺点也是很明显的
浪费过多的内存,使现有的「「可用空间变为」」原先的「「一半」」
浪费过多的内存,使现有的「「可用空间变为」」原先的「「一半」」
「3.标记整理算法」
这种算法可以说是结合了前两种算法,既有标记删除,又有整理功能。
这种算法可以说是结合了前两种算法,既有标记删除,又有整理功能。
这种算法就是通过标记清除算法找到存活的对象,然后将所有「「存活的对象,向空间的一端移动」」,然后回收掉其他的内存。
常见调优工具有哪些
常用调优工具分为两类,jdk自带监控工具:jconsole和jvisualvm,第三方有:MAT(Memory
Analyzer Tool)、GChisto。
jconsole,Java Monitoring and Management Console是从java5开始,在JDK中自带的java监
控和管理控制台,用于对JVM中内存,线程和类等的监控
jvisualvm,jdk自带全能工具,可以分析内存快照、线程快照;监控内存变化、GC变化等。
MAT,Memory Analyzer Tool,一个基于Eclipse的内存分析工具,是一个快速、功能丰富的
Java heap分析工具,它可以帮助我们查找内存泄漏和减少内存消耗
GChisto,一款专业分析gc日志的工具
Analyzer Tool)、GChisto。
jconsole,Java Monitoring and Management Console是从java5开始,在JDK中自带的java监
控和管理控制台,用于对JVM中内存,线程和类等的监控
jvisualvm,jdk自带全能工具,可以分析内存快照、线程快照;监控内存变化、GC变化等。
MAT,Memory Analyzer Tool,一个基于Eclipse的内存分析工具,是一个快速、功能丰富的
Java heap分析工具,它可以帮助我们查找内存泄漏和减少内存消耗
GChisto,一款专业分析gc日志的工具
什么是 STW ?
Java 中「「Stop-The-World机制简称 STW」」 ,是在执行垃圾收集算法时,Java 应用程序的其他所有线程都被挂起(除了垃圾收集帮助器之外)。「Java 中一种全局暂停现象,全局停顿」,所有 Java 代码停止,native 代码可以执行,但不能与 JVM 交互。
为什么需要 STW?
在 java 应用程序中「「引用关系」」是不断发生「「变化」」的,那么就会有会有很多种情况来导致「「垃圾标识」」出错。想想一下如果 Object a 目前是个垃圾,GC 把它标记为垃圾,但是在清除前又有其他对象指向了 Object a,那么此刻 Object a 又不是垃圾了,那么如果没有 STW 就要去无限维护这种关系来去采集正确的信息。再举个例子,到了秋天,道路上洒满了金色的落叶,环卫工人在打扫街道,却永远也无法打扫干净,因为总会有不断的落叶。
如何排查 OOM 的问题?
1.增加两个参数 -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp/heapdump.hprof,当 OOM 发生时自动 dump 堆内存信息到指定目录;
2.同时 jstat 查看监控 JVM 的内存和 GC 情况,先观察问题大概出在什么区域;
3.使用工具载入到 dump 文件,分析大对象的占用情况。
2.同时 jstat 查看监控 JVM 的内存和 GC 情况,先观察问题大概出在什么区域;
3.使用工具载入到 dump 文件,分析大对象的占用情况。
安全区域是什么?解决了什么问题
刚刚说到了主动式中断,但是如果有些线程处于sleep状态怎么办呢?
为了解决这种问题,又引入了安全区域的概念安全区域是指「「在一段代码片中,引用关系不会发生改变」」,实际上就是一个安全点的拓展。当线程执行到安全区域时,首先标识自己已进入安全区域,那样,当在这段时间里 JVM 要发起 GC 时,就不用管标识自己为“安全区域”状态的线程了,该线程只能乖乖的等待根节点枚举完或者整个GC过程完成之后才能继续执行。
为了解决这种问题,又引入了安全区域的概念安全区域是指「「在一段代码片中,引用关系不会发生改变」」,实际上就是一个安全点的拓展。当线程执行到安全区域时,首先标识自己已进入安全区域,那样,当在这段时间里 JVM 要发起 GC 时,就不用管标识自己为“安全区域”状态的线程了,该线程只能乖乖的等待根节点枚举完或者整个GC过程完成之后才能继续执行。
说说三色标记
这里我们又提到了一个概念叫做 「「SATB 原始快照」」,关于SATB会延伸出有一个概念,「「三色标记算法」」,也就是垃圾回收器标记垃圾的时候使用的算法,这里我们简单说下:将对象分为「「三种颜色」」:
白色:没被 GC 访问过的对象(被 GC 标记完后还是白色代表是垃圾)
黑丝:存活的对象
灰色:被 GC 访问过的对象,但是对象引用链上至少还有一个引用没被扫描过
我们知道在 「「并发标记」」 的时候 「「可能会」」 出现 「「误标」」 的情况,这里举两个例子:
1.刚开始标记为 「「垃圾」」 的对象,但是在并发标记过程中 「「变为了存活对象」」
2.刚开始标记为 「「存活」」 的对象,但是在并发标记过程中 「「变为了垃圾对象」」
第一种情况影响还不算很大,只是相当于垃圾没有清理干净,待下一次清理的时候再清理一下就好了。第二种情况就危险了,正在使 「「用的对象的突然被清理掉」」 了,后果会很严重。那么 「「产生上述第二种情况的原因」」 是什么呢?
1.「「新增」」 一条或多条 「「黑色到白色」」 对象的新引用
2.删除 「「了」」 灰色 「「对象」」 到该白色对象 「「的直接」」 引用或间接引用。
当这两种情况 「「都满足」」 的时候就会出现这种问题了。所以为了解决这个问题,引入了 「「增量更新」」 (Incremental Update)和 「「原始快照」」 (SATB)的方案:
增量更新破坏了第一个条件:「「增加新引用时记录」」 该引用信息,在后续 STW 扫描中重新扫描(CMS的使用方案)。
原始快照破坏了第二个条件:「「删除引用时记录下来」」,在后续 STW 扫描时将这些记录过的灰色对象为根再扫描一次(G1的使用方案)。
白色:没被 GC 访问过的对象(被 GC 标记完后还是白色代表是垃圾)
黑丝:存活的对象
灰色:被 GC 访问过的对象,但是对象引用链上至少还有一个引用没被扫描过
我们知道在 「「并发标记」」 的时候 「「可能会」」 出现 「「误标」」 的情况,这里举两个例子:
1.刚开始标记为 「「垃圾」」 的对象,但是在并发标记过程中 「「变为了存活对象」」
2.刚开始标记为 「「存活」」 的对象,但是在并发标记过程中 「「变为了垃圾对象」」
第一种情况影响还不算很大,只是相当于垃圾没有清理干净,待下一次清理的时候再清理一下就好了。第二种情况就危险了,正在使 「「用的对象的突然被清理掉」」 了,后果会很严重。那么 「「产生上述第二种情况的原因」」 是什么呢?
1.「「新增」」 一条或多条 「「黑色到白色」」 对象的新引用
2.删除 「「了」」 灰色 「「对象」」 到该白色对象 「「的直接」」 引用或间接引用。
当这两种情况 「「都满足」」 的时候就会出现这种问题了。所以为了解决这个问题,引入了 「「增量更新」」 (Incremental Update)和 「「原始快照」」 (SATB)的方案:
增量更新破坏了第一个条件:「「增加新引用时记录」」 该引用信息,在后续 STW 扫描中重新扫描(CMS的使用方案)。
原始快照破坏了第二个条件:「「删除引用时记录下来」」,在后续 STW 扫描时将这些记录过的灰色对象为根再扫描一次(G1的使用方案)。
什么情况下会发生栈内存溢出?
Java 栈内存溢出可能抛出两种异常,两种异常虽然都发生在栈内存,但是两者导致内存溢出的根本原因是不一样的:
1.「如果线程请求分配的栈容量超过 Java 虚拟机栈允许的最大容量的时候」,Java 虚拟机将抛出一个 StackOverFlowError 异常。
2.如果 Java 虚拟机栈可以动态拓展,并且扩展的动作已经尝试过,但是目前「无法申请到足够的内存去完成拓展,或者在建立新线程的时候没有足够的内存去创建对应的虚拟机栈」,那 Java 虚拟机将会抛出一个 OutOfMemoryError 异常。
1.「如果线程请求分配的栈容量超过 Java 虚拟机栈允许的最大容量的时候」,Java 虚拟机将抛出一个 StackOverFlowError 异常。
2.如果 Java 虚拟机栈可以动态拓展,并且扩展的动作已经尝试过,但是目前「无法申请到足够的内存去完成拓展,或者在建立新线程的时候没有足够的内存去创建对应的虚拟机栈」,那 Java 虚拟机将会抛出一个 OutOfMemoryError 异常。
对象一定分配在堆中吗?有没有了解逃逸分析技术?
不一定的,JVM通过「逃逸分析」,那些逃不出方法的对象会在栈上
分配。
分配。
说一说对象的栈上分配吧?
如果所有对象都分配在堆中那么会给 GC 带来许多不必要的压力,比如有些对象的生命周期只是在当前线程中,为了减少临时对象在堆内分配的数量,就「可以在在栈上分配」,随着线程的消亡而消亡。当然栈上空间必须充足,否则也无法分配,在判断是否能分配到栈上的另一条件就是要经过逃逸分析,
「逃逸分析(Escape Analysis)」:
简单来讲就是:Java Hotspot 虚拟机判断这个新对象是否只会被当前线程引用,并且决定是否能够在 Java 堆上分配内存。
「逃逸分析(Escape Analysis)」:
简单来讲就是:Java Hotspot 虚拟机判断这个新对象是否只会被当前线程引用,并且决定是否能够在 Java 堆上分配内存。
什么是指针碰撞?
一般情况下,JVM的对象都放在堆内存中(发生逃逸分析除外)。当类加载检查通过后,Java虚拟
机开始为新生对象分配内存。如果Java堆中内存是绝对规整的,所有被使用过的的内存都被放到一
边,空闲的内存放到另外一边,中间放着一个指针作为分界点的指示器,所分配内存仅仅是把那个
指针向空闲空间方向挪动一段与对象大小相等的实例,这种分配方式就是 指针碰撞。
机开始为新生对象分配内存。如果Java堆中内存是绝对规整的,所有被使用过的的内存都被放到一
边,空闲的内存放到另外一边,中间放着一个指针作为分界点的指示器,所分配内存仅仅是把那个
指针向空闲空间方向挪动一段与对象大小相等的实例,这种分配方式就是 指针碰撞。
什么是空闲列表?
如果Java堆内存中的内存并不是规整的,已被使用的内存和空闲的内存相互交错在一起,不可以进
行指针碰撞啦,虚拟机必须维护一个列表,记录哪些内存是可用的,在分配的时候从列表找到一块
大的空间分配给对象实例,并更新列表上的记录,这种分配方式就是空闲列表。
行指针碰撞啦,虚拟机必须维护一个列表,记录哪些内存是可用的,在分配的时候从列表找到一块
大的空间分配给对象实例,并更新列表上的记录,这种分配方式就是空闲列表。
什么是TLAB?
可以把内存分配的动作按照线程划分在不同的空间之中进行,每个线程在Java堆中预先分配一小块
内存,这就是TLAB(Thread Local Allocation Buffer,本地线程分配缓存) 。虚拟机通过 -
XX:UseTLAB 设定它的。
内存,这就是TLAB(Thread Local Allocation Buffer,本地线程分配缓存) 。虚拟机通过 -
XX:UseTLAB 设定它的。
锁
多线程锁的升级原理是什么?
在Java中,锁共有4种状态,级别从低到高依次为:无状态锁,偏向锁,轻量级锁和重量级锁状态,这几个状态会随着竞争情况逐渐升级。锁可以升级但不能降级。
什么是死锁?
死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。是操作系统层面的一个错误,是进程死锁的简称,最早在 1965 年由 Dijkstra 在研究银行家算法时提出的,它是计算机操作系统乃至整个并发程序设计领域最难处理的问题之一。
怎么防止死锁?
互斥条件:进程对所分配到的资源不允许其他进程进行访问,若其他进程访问该资源,只能等待,直至占有该资源的进程使用完成后释放该资源
请求和保持条件:进程获得一定的资源之后,又对其他资源发出请求,但是该资源可能被其他进程占有,此事请求阻塞,但又对自己获得的资源保持不放
不可剥夺条件:是指进程已获得的资源,在未完成使用之前,不可被剥夺,只能在使用完后自己释放
环路等待条件:是指进程发生死锁后,若干进程之间形成一种头尾相接的循环等待资源关系
请求和保持条件:进程获得一定的资源之后,又对其他资源发出请求,但是该资源可能被其他进程占有,此事请求阻塞,但又对自己获得的资源保持不放
不可剥夺条件:是指进程已获得的资源,在未完成使用之前,不可被剥夺,只能在使用完后自己释放
环路等待条件:是指进程发生死锁后,若干进程之间形成一种头尾相接的循环等待资源关系
这四个条件是死锁的必要条件,只要系统发生死锁,这些条件必然成立,而只要上述条件之 一不满足,就不会发生死锁。
产生死锁的四个必要条件?
1. 互斥条件:一个资源每次只能被一个线程使用
2. 请求与保持条件:一个线程因请求资源而阻塞时,对已获得的资源保持不放
3. 不剥夺条件:进程已经获得的资源,在未使用完之前,不能强行剥夺
4. 循环等待条件:若干线程之间形成一种头尾相接的循环等待资源关系
2. 请求与保持条件:一个线程因请求资源而阻塞时,对已获得的资源保持不放
3. 不剥夺条件:进程已经获得的资源,在未使用完之前,不能强行剥夺
4. 循环等待条件:若干线程之间形成一种头尾相接的循环等待资源关系
知道synchronized原理吗?
synchronized是java提供的原⼦性内置锁,这种内置的并且使⽤者看不到的锁也被称为监视器锁,使⽤
synchronized之后,会在编译之后在同步的代码块前后加上monitorenter和monitorexit字节码指令,
他依赖操作系统底层互斥锁实现。他的作⽤主要就是实现原⼦性操作和解决共享变量的内存可⻅性问
题。
执⾏monitorenter指令时会尝试获取对象锁,如果对象没有被锁定或者已经获得了锁,锁的计数器+1。
此时其他竞争锁的线程则会进⼊等待队列中。
执⾏monitorexit指令时则会把计数器-1,当计数器值为0时,则锁释放,处于等待队列中的线程再继续
竞争锁。
synchronized是排它锁,当⼀个线程获得锁之后,其他线程必须等待该线程释放锁后才能获得锁,⽽且
由于Java中的线程和操作系统原⽣线程是⼀⼀对应的,线程被阻塞或者唤醒时时会从⽤户态切换到内核
态,这种转换⾮常消耗性能。
从内存语义来说,加锁的过程会清除⼯作内存中的共享变量,再从主内存读取,⽽释放锁的过程则是将
⼯作内存中的共享变量写回主内存。
synchronized之后,会在编译之后在同步的代码块前后加上monitorenter和monitorexit字节码指令,
他依赖操作系统底层互斥锁实现。他的作⽤主要就是实现原⼦性操作和解决共享变量的内存可⻅性问
题。
执⾏monitorenter指令时会尝试获取对象锁,如果对象没有被锁定或者已经获得了锁,锁的计数器+1。
此时其他竞争锁的线程则会进⼊等待队列中。
执⾏monitorexit指令时则会把计数器-1,当计数器值为0时,则锁释放,处于等待队列中的线程再继续
竞争锁。
synchronized是排它锁,当⼀个线程获得锁之后,其他线程必须等待该线程释放锁后才能获得锁,⽽且
由于Java中的线程和操作系统原⽣线程是⼀⼀对应的,线程被阻塞或者唤醒时时会从⽤户态切换到内核
态,这种转换⾮常消耗性能。
从内存语义来说,加锁的过程会清除⼯作内存中的共享变量,再从主内存读取,⽽释放锁的过程则是将
⼯作内存中的共享变量写回主内存。
如果再深⼊到源码来说,synchronized实际上有两个队列waitSet和entryList。
1. 当多个线程进⼊同步代码块时,⾸先进⼊entryList
2. 有⼀个线程获取到monitor锁后,就赋值给当前线程,并且计数器+1
3. 如果线程调⽤wait⽅法,将释放锁,当前线程置为null,计数器-1,同时进⼊waitSet等待被唤醒,
调⽤notify或者notifyAll之后⼜会进⼊entryList竞争锁
4. 如果线程执⾏完毕,同样释放锁,计数器-1,当前线程置为null
1. 当多个线程进⼊同步代码块时,⾸先进⼊entryList
2. 有⼀个线程获取到monitor锁后,就赋值给当前线程,并且计数器+1
3. 如果线程调⽤wait⽅法,将释放锁,当前线程置为null,计数器-1,同时进⼊waitSet等待被唤醒,
调⽤notify或者notifyAll之后⼜会进⼊entryList竞争锁
4. 如果线程执⾏完毕,同样释放锁,计数器-1,当前线程置为null
那锁的优化机制了解吗?
从JDK1.6版本之后,synchronized本身也在不断优化锁的机制,有些情况下他并不会是⼀个很重量级的
锁了。优化机制包括⾃适应锁、⾃旋锁、锁消除、锁粗化、轻量级锁和偏向锁。
锁的状态从低到⾼依次为⽆锁->偏向锁->轻量级锁->重量级锁,升级的过程就是从低到⾼,降级在⼀定条
件也是有可能发⽣的
锁了。优化机制包括⾃适应锁、⾃旋锁、锁消除、锁粗化、轻量级锁和偏向锁。
锁的状态从低到⾼依次为⽆锁->偏向锁->轻量级锁->重量级锁,升级的过程就是从低到⾼,降级在⼀定条
件也是有可能发⽣的
⾃旋锁:由于⼤部分时候,锁被占⽤的时间很短,共享变量的锁定时间也很短,所有没有必要挂起线
程,⽤户态和内核态的来回上下⽂切换严重影响性能。⾃旋的概念就是让线程执⾏⼀个忙循环,可以理
解为就是啥也不⼲,防⽌从⽤户态转⼊内核态,⾃旋锁可以通过设置-XX:+UseSpining来开启,⾃旋的默
认次数是10次,可以使⽤-XX:PreBlockSpin设置。
当一个线程尝试去获取某一把锁的时候,如果这个锁此时已经被别人获取(占用),那么此线程就无法获取到这把锁,该线程将会等待,间隔一段时间后会再次尝试获取
程,⽤户态和内核态的来回上下⽂切换严重影响性能。⾃旋的概念就是让线程执⾏⼀个忙循环,可以理
解为就是啥也不⼲,防⽌从⽤户态转⼊内核态,⾃旋锁可以通过设置-XX:+UseSpining来开启,⾃旋的默
认次数是10次,可以使⽤-XX:PreBlockSpin设置。
当一个线程尝试去获取某一把锁的时候,如果这个锁此时已经被别人获取(占用),那么此线程就无法获取到这把锁,该线程将会等待,间隔一段时间后会再次尝试获取
⾃适应锁:⾃适应锁就是⾃适应的⾃旋锁,⾃旋的时间不是固定时间,⽽是由前⼀次在同⼀个锁上的⾃
旋时间和锁的持有者状态来决定。
旋时间和锁的持有者状态来决定。
- 锁消除:锁消除指的是JVM检测到⼀些同步的代码块,完全不存在数据竞争的场景,也就是不需要加
锁粗化:锁粗化指的是有很多操作都是对同⼀个对象进⾏加锁,就会把锁的同步范围扩展到整个操作序
列之外。
列之外。
偏向锁:当线程访问同步块获取锁时,会在对象头和栈帧中的锁记录⾥存储偏向锁的线程ID,之后这个
线程再次进⼊同步块时都不需要CAS来加锁和解锁了,偏向锁会永远偏向第⼀个获得锁的线程,如果后
续没有其他线程获得过这个锁,持有锁的线程就永远不需要进⾏同步,反之,当有其他线程竞争偏向锁
时,持有偏向锁的线程就会释放偏向锁。可以⽤过设置-XX:+UseBiasedLocking开启偏向锁。
线程再次进⼊同步块时都不需要CAS来加锁和解锁了,偏向锁会永远偏向第⼀个获得锁的线程,如果后
续没有其他线程获得过这个锁,持有锁的线程就永远不需要进⾏同步,反之,当有其他线程竞争偏向锁
时,持有偏向锁的线程就会释放偏向锁。可以⽤过设置-XX:+UseBiasedLocking开启偏向锁。
轻量级锁:JVM的对象的对象头中包含有⼀些锁的标志位,代码进⼊同步块的时候,JVM将会使⽤CAS⽅
式来尝试获取锁,如果更新成功则会把对象头中的状态位标记为轻量级锁,如果更新失败,当前线程就
尝试⾃旋来获得锁。
式来尝试获取锁,如果更新成功则会把对象头中的状态位标记为轻量级锁,如果更新失败,当前线程就
尝试⾃旋来获得锁。
简单点说,偏向锁就是通过对象头的偏向线程ID来对⽐,甚⾄都不需要CAS了,⽽轻量级锁主要就是通
过CAS修改对象头锁记录和⾃旋来实现,重量级锁则是除了拥有锁的线程其他全部阻塞。
过CAS修改对象头锁记录和⾃旋来实现,重量级锁则是除了拥有锁的线程其他全部阻塞。
CAS的原理呢?
CAS叫做CompareAndSwap,⽐较并交换,主要是通过处理器的指令来保证操作的原⼦性,它包含三个
操作数:
1. 变量内存地址,V表示
2. 旧的预期值,A表示
3. 准备设置的新值,B表示
当执⾏CAS指令时,只有当V等于A时,才会⽤B去更新V的值,否则就不会执⾏更新操作。
操作数:
1. 变量内存地址,V表示
2. 旧的预期值,A表示
3. 准备设置的新值,B表示
当执⾏CAS指令时,只有当V等于A时,才会⽤B去更新V的值,否则就不会执⾏更新操作。
synchronized 和 ReentrantLock 有什么不同?
synchronized是java关键字,lock是java类
synchronized无法判断是否获取锁的状态,lock可以判断是否获取到锁的状态
synchronized会自动释放锁,lock需要手工释放锁
synchronized不可中断、非公平锁,lock可中断、公平锁和非公共锁
synchronized无法判断是否获取锁的状态,lock可以判断是否获取到锁的状态
synchronized会自动释放锁,lock需要手工释放锁
synchronized不可中断、非公平锁,lock可中断、公平锁和非公共锁
什么是分布式锁
分布式锁,即分布式系统中的锁。在单体应用中我们通过锁解决的是控制共享资源访问的问题,而分布式锁,就是解决了分布式系统中控制共享资源访问的问题。与单体应用不同的是,分布式系统中竞争共享资源的最小粒度从线程升级成了进程。
什么是分布式锁?
处于不同节点上不同的服务,它们可能需要顺序的访问一些资源,这里需要一把分布式的锁。
分布式锁具有以下特性:写锁、读锁、时序锁。
写锁:在zk上创建的一个临时的无编号的节点。由于是无序编号,在创建时不会自动编号,导致只
能客户端有一个客户端得到锁,然后进行写入。
读锁:在zk上创建一个临时的有编号的节点,这样即使下次有客户端加入是同时创建相同的节点
时,他也会自动编号,也可以获得锁对象,然后对其进行读取。
时序锁:在zk上创建的一个临时的有编号的节点根据编号的大小控制锁。
分布式锁具有以下特性:写锁、读锁、时序锁。
写锁:在zk上创建的一个临时的无编号的节点。由于是无序编号,在创建时不会自动编号,导致只
能客户端有一个客户端得到锁,然后进行写入。
读锁:在zk上创建一个临时的有编号的节点,这样即使下次有客户端加入是同时创建相同的节点
时,他也会自动编号,也可以获得锁对象,然后对其进行读取。
时序锁:在zk上创建的一个临时的有编号的节点根据编号的大小控制锁。
分布式锁应该具备哪些条件
在分布式系统环境下,一个方法在同一时间只能被一个机器的一个线程执行
高可用的获取锁与释放锁
高性能的获取锁与释放锁
具备可重入特性(可理解为重新进入,由多于一个任务并发使用,而不必担心数据错误)
具备锁失效机制,即自动解锁,防止死锁
具备非阻塞锁特性,即没有获取到锁将直接返回获取锁失败
高可用的获取锁与释放锁
高性能的获取锁与释放锁
具备可重入特性(可理解为重新进入,由多于一个任务并发使用,而不必担心数据错误)
具备锁失效机制,即自动解锁,防止死锁
具备非阻塞锁特性,即没有获取到锁将直接返回获取锁失败
一基于数据库的分布式锁
基于数据库的分布式锁
基于数据库的锁实现也有两种方式,一是基于数据库表的增删,另一种是基于数据库排他锁。
1、基于数据库表的增删:
基于数据库表增删是最简单的方式,首先创建一张锁的表主要包含下列字段:类的全路径名+方法名,时间戳等字段。
具体的使用方式:当需要锁住某个方法时,往该表中插入一条相关的记录。类的全路径名+方法名是有唯一性约束的,如果有多个请求同时提交到数据库的话,数据库会保证只有一个操作可以成功,那么我们就认为操作成功的那个线程获得了该方法的锁,可以执行方法体内容。执行完毕之后,需要delete该记录。
(这里只是简单介绍一下,对于上述方案可以进行优化,如:应用主从数据库,数据之间双向同步;一旦挂掉快速切换到备库上;做一个定时任务,每隔一定时间把数据库中的超时数据清理一遍;使用while循环,直到insert成功再返回成功;记录当前获得锁的机器的主机信息和线程信息,下次再获取锁的时候先查询数据库,如果当前机器的主机信息和线程信息在数据库可以查到的话,直接把锁分配给他就可以了,实现可重入锁)
2、基于数据库排他锁:
基于MySql的InnoDB引擎,在查询语句后面增加for update,数据库会在查询过程中给数据库表增加排他锁。获得排它锁的线程即可获得分布式锁,当获得锁之后,可以执行方法的业务逻辑,执行完方法之后,释放锁connection.commit()。当某条记录被加上排他锁之后,其他线程无法获取排他锁并被阻塞。
3、基于数据库锁的优缺点:
上面两种方式都是依赖数据库表,一种是通过表中的记录判断当前是否有锁存在,另外一种是通过数据库的排他锁来实现分布式锁。
优点是直接借助数据库,简单容易理解。
缺点是操作数据库需要一定的开销,性能问题需要考虑。
基于数据库的锁实现也有两种方式,一是基于数据库表的增删,另一种是基于数据库排他锁。
1、基于数据库表的增删:
基于数据库表增删是最简单的方式,首先创建一张锁的表主要包含下列字段:类的全路径名+方法名,时间戳等字段。
具体的使用方式:当需要锁住某个方法时,往该表中插入一条相关的记录。类的全路径名+方法名是有唯一性约束的,如果有多个请求同时提交到数据库的话,数据库会保证只有一个操作可以成功,那么我们就认为操作成功的那个线程获得了该方法的锁,可以执行方法体内容。执行完毕之后,需要delete该记录。
(这里只是简单介绍一下,对于上述方案可以进行优化,如:应用主从数据库,数据之间双向同步;一旦挂掉快速切换到备库上;做一个定时任务,每隔一定时间把数据库中的超时数据清理一遍;使用while循环,直到insert成功再返回成功;记录当前获得锁的机器的主机信息和线程信息,下次再获取锁的时候先查询数据库,如果当前机器的主机信息和线程信息在数据库可以查到的话,直接把锁分配给他就可以了,实现可重入锁)
2、基于数据库排他锁:
基于MySql的InnoDB引擎,在查询语句后面增加for update,数据库会在查询过程中给数据库表增加排他锁。获得排它锁的线程即可获得分布式锁,当获得锁之后,可以执行方法的业务逻辑,执行完方法之后,释放锁connection.commit()。当某条记录被加上排他锁之后,其他线程无法获取排他锁并被阻塞。
3、基于数据库锁的优缺点:
上面两种方式都是依赖数据库表,一种是通过表中的记录判断当前是否有锁存在,另外一种是通过数据库的排他锁来实现分布式锁。
优点是直接借助数据库,简单容易理解。
缺点是操作数据库需要一定的开销,性能问题需要考虑。
二基于Zookeeper的分布式锁
基于Zookeeper的分布式锁
基于zookeeper临时有序节点可以实现的分布式锁。每个客户端对某个方法加锁时,在zookeeper上的与该方法对应的指定节点的目录下,生成一个唯一的瞬时有序节点。 判断是否获取锁的方式很简单,只需要判断有序节点中序号最小的一个。 当释放锁的时候,只需将这个瞬时节点删除即可。同时,其可以避免服务宕机导致的锁无法释放,而产生的死锁问题。 (第三方库有 Curator,Curator提供的InterProcessMutex是分布式锁的实现)
Zookeeper实现的分布式锁存在两个个缺点:
(1)性能上可能并没有缓存服务那么高,因为每次在创建锁和释放锁的过程中,都要动态创建、销毁瞬时节点来实现锁功能。ZK中创建和删除节点只能通过Leader服务器来执行,然后将数据同步到所有的Follower机器上。
(2)zookeeper的并发安全问题:因为可能存在网络抖动,客户端和ZK集群的session连接断了,zk集群以为客户端挂了,就会删除临时节点,这时候其他客户端就可以获取到分布式锁了。
基于zookeeper临时有序节点可以实现的分布式锁。每个客户端对某个方法加锁时,在zookeeper上的与该方法对应的指定节点的目录下,生成一个唯一的瞬时有序节点。 判断是否获取锁的方式很简单,只需要判断有序节点中序号最小的一个。 当释放锁的时候,只需将这个瞬时节点删除即可。同时,其可以避免服务宕机导致的锁无法释放,而产生的死锁问题。 (第三方库有 Curator,Curator提供的InterProcessMutex是分布式锁的实现)
Zookeeper实现的分布式锁存在两个个缺点:
(1)性能上可能并没有缓存服务那么高,因为每次在创建锁和释放锁的过程中,都要动态创建、销毁瞬时节点来实现锁功能。ZK中创建和删除节点只能通过Leader服务器来执行,然后将数据同步到所有的Follower机器上。
(2)zookeeper的并发安全问题:因为可能存在网络抖动,客户端和ZK集群的session连接断了,zk集群以为客户端挂了,就会删除临时节点,这时候其他客户端就可以获取到分布式锁了。
三基于Redis的分布式锁
基于redis的分布式锁:
redis命令说明:
(1)setnx命令:set if not exists,当且仅当 key 不存在时,将 key 的值设为 value。若给定的 key 已经存在,则 SETNX 不做任何动作。
返回1,说明该进程获得锁,将 key 的值设为 value
返回0,说明其他进程已经获得了锁,进程不能进入临界区。
命令格式:setnx lock.key lock.value
(2)get命令:获取key的值,如果存在,则返回;如果不存在,则返回nil
命令格式:get lock.key
(3)getset命令:该方法是原子的,对key设置newValue这个值,并且返回key原来的旧值。
命令格式:getset lock.key newValue
(4)del命令:删除redis中指定的key
命令格式:del lock.key
方案一:基于set命令的分布式锁
1、加锁:使用setnx进行加锁,当该指令返回1时,说明成功获得锁
2、解锁:当得到锁的线程执行完任务之后,使用del命令释放锁,以便其他线程可以继续执行setnx命令来获得锁
(1)存在的问题:假设线程获取了锁之后,在执行任务的过程中挂掉,来不及显示地执行del命令释放锁,那么竞争该锁的线程都会执行不了,产生死锁的情况。
(2)解决方案:设置锁超时时间
3、设置锁超时时间:setnx 的 key 必须设置一个超时时间,以保证即使没有被显式释放,这把锁也要在一定时间后自动释放。可以使用expire命令设置锁超时时间
(1)存在问题:
setnx 和 expire 不是原子性的操作,假设某个线程执行setnx 命令,成功获得了锁,但是还没来得及执行expire 命令,服务器就挂掉了,这样一来,这把锁就没有设置过期时间了,变成了死锁,别的线程再也没有办法获得锁了。
(2)解决方案:redis的set命令支持在获取锁的同时设置key的过期时间
4、使用set命令加锁并设置锁过期时间:
命令格式:set <lock.key> <lock.value> nx ex <expireTime>
(1)存在问题:
① 假如线程A成功得到了锁,并且设置的超时时间是 30 秒。如果某些原因导致线程 A 执行的很慢,过了 30 秒都没执行完,这时候锁过期自动释放,线程 B 得到了锁。
② 随后,线程A执行完任务,接着执行del指令来释放锁。但这时候线程 B 还没执行完,线程A实际上删除的是线程B加的锁。
(2)解决方案:
可以在 del 释放锁之前做一个判断,验证当前的锁是不是自己加的锁。在加锁的时候把当前的线程 ID 当做value,并在删除之前验证 key 对应的 value 是不是自己线程的 ID。但是,这样做其实隐含了一个新的问题,get操作、判断和释放锁是两个独立操作,不是原子性。对于非原子性的问题,我们可以使用Lua脚本来确保操作的原子性
5、锁续期:(这种机制类似于redisson的看门狗机制,文章后面会详细说明)
虽然步骤4避免了线程A误删掉key的情况,但是同一时间有 A,B 两个线程在访问代码块,仍然是不完美的。怎么办呢?我们可以让获得锁的线程开启一个守护线程,用来给快要过期的锁“续期”。
① 假设线程A执行了29 秒后还没执行完,这时候守护线程会执行 expire 指令,为这把锁续期 20 秒。守护线程从第 29 秒开始执行,每 20 秒执行一次。
② 情况一:当线程A执行完任务,会显式关掉守护线程。
③ 情况二:如果服务器忽然断电,由于线程 A 和守护线程在同一个进程,守护线程也会停下。这把锁到了超时的时候,没人给它续命,也就自动释放了。
方案二:基于setnx、get、getset的分布式锁
1、实现原理:
(1)setnx(lockkey, 当前时间+过期超时时间) ,如果返回1,则获取锁成功;如果返回0则没有获取到锁,转向步骤(2)
(2)get(lockkey)获取值oldExpireTime ,并将这个value值与当前的系统时间进行比较,如果小于当前系统时间,则认为这个锁已经超时,可以允许别的请求重新获取,转向步骤(3)
(3)计算新的过期时间 newExpireTime=当前时间+锁超时时间,然后getset(lockkey, newExpireTime) 会返回当前lockkey的值currentExpireTime
(4)判断 currentExpireTime 与 oldExpireTime 是否相等,如果相等,说明当前getset设置成功,获取到了锁。如果不相等,说明这个锁又被别的请求获取走了,那么当前请求可以直接返回失败,或者继续重试。
(5)在获取到锁之后,当前线程可以开始自己的业务处理,当处理完毕后,比较自己的处理时间和对于锁设置的超时时间,如果小于锁设置的超时时间,则直接执行del命令释放锁(释放锁之前需要判断持有锁的线程是不是当前线程);如果大于锁设置的超时时间,则不需要再锁进行处理。
2、代码实现:
(1)获取锁的实现方式:
tryLock方法中,主要逻辑如下:lock调用tryLock方法,参数为获取的超时时间与单位,线程在超时时间内,获取锁操作将自旋在那里,直到该自旋锁的保持者释放了锁。
(2)释放锁的实现方式:
存在问题:
(1)这个锁的核心是基于System.currentTimeMillis(),如果多台服务器时间不一致,那么问题就出现了,但是这个bug完全可以从服务器运维层面规避的,而且如果服务器时间不一样的话,只要和时间相关的逻辑都是会出问题的
(2)如果前一个锁超时的时候,刚好有多台服务器去请求获取锁,那么就会出现同时执行redis.getset()而导致出现过期时间覆盖问题,不过这种情况并不会对正确结果造成影响
(3)存在多个线程同时持有锁的情况:如果线程A执行任务的时间超过锁的过期时间,这时另一个线程就可以获得这个锁了,造成多个线程同时持有锁的情况。类似于方案一,可以使用“锁续期”的方式来解决。
前两种redis分布式锁的存在的问题
前面两种redis分布式锁的实现方式,如果从“高可用”的层面来看,仍然是有所欠缺,也就是说当 redis 是单点的情况下,当发生故障时,则整个业务的分布式锁都将无法使用。
为了提高可用性,我们可以使用主从模式或者哨兵模式,但在这种情况下仍然存在问题,在主从模式或者哨兵模式下,正常情况下,如果加锁成功了,那么master节点会异步复制给对应的slave节点。但是如果在这个过程中发生master节点宕机,主备切换,slave节点从变为了 master节点,而锁还没从旧master节点同步过来,这就发生了锁丢失,会导致多个客户端可以同时持有同一把锁的问题。来看个图来想下这个过程:
那么,如何避免这种情况呢?redis 官方给出了基于多个 redis 集群部署的高可用分布式锁解决方案:RedLock,在方案三我们就来详细介绍一下。(备注:如果master节点宕机期间,可以容忍多个客户端同时持有锁,那么就不需要redLock)
方案三:基于RedLock的分布式锁
Redlock算法是Redis的作者 Antirez 在单Redis节点基础上引入的高可用模式。Redlock的加锁要结合单节点分布式锁算法共同实现,因为它是RedLock的基础
1、加锁实现原理:
现在假设有5个Redis主节点(大于3的奇数个),这样基本保证他们不会同时都宕掉,获取锁和释放锁的过程中,客户端会执行以下操作:
(1)获取当前Unix时间,以毫秒为单位,并设置超时时间TTL
TTL 要大于 正常业务执行的时间 + 获取所有redis服务消耗时间 + 时钟漂移
(2)依次尝试从5个实例,使用相同的key和具有唯一性的value获取锁,当向Redis请求获取锁时,客户端应该设置一个网络连接和响应超时时间,这个超时时间应该小于锁的失效时间TTL,这样可以避免客户端死等。比如:TTL为5s,设置获取锁最多用1s,所以如果一秒内无法获取锁,就放弃获取这个锁,从而尝试获取下个锁
(3)客户端 获取所有能获取的锁后的时间 减去 第(1)步的时间,就得到锁的获取时间。锁的获取时间要小于锁失效时间TTL,并且至少从半数以上的Redis节点取到锁,才算获取成功锁
(4)如果成功获得锁,key的真正有效时间 = TTL - 锁的获取时间 - 时钟漂移。比如:TTL 是5s,获取所有锁用了2s,则真正锁有效时间为3s
(5)如果因为某些原因,获取锁失败(没有在半数以上实例取到锁或者取锁时间已经超过了有效时间),客户端应该在所有的Redis实例上进行解锁,无论Redis实例是否加锁成功,因为可能服务端响应消息丢失了但是实际成功了。
设想这样一种情况:客户端发给某个Redis节点的获取锁的请求成功到达了该Redis节点,这个节点也成功执行了SET操作,但是它返回给客户端的响应包却丢失了。这在客户端看来,获取锁的请求由于超时而失败了,但在Redis这边看来,加锁已经成功了。因此,释放锁的时候,客户端也应该对当时获取锁失败的那些Redis节点同样发起请求。实际上,这种情况在异步通信模型中是有可能发生的:客户端向服务器通信是正常的,但反方向却是有问题的。
(6)失败重试:当client不能获取锁时,应该在随机时间后重试获取锁;同时重试获取锁要有一定次数限制;
在随机时间后进行重试,主要是防止过多的客户端同时尝试去获取锁,导致彼此都获取锁失败的问题。
2、RedLock性能及崩溃恢复的相关解决方法:
由于N个Redis节点中的大多数能正常工作就能保证Redlock正常工作,因此理论上它的可用性更高。前面我们说的主从架构下存在的安全性问题,在RedLock中已经不存在了,但如果有节点发生崩溃重启,还是会对锁的安全性有影响的,具体的影响程度跟Redis持久化配置有关:
(1)如果redis没有持久化功能,在clientA获取锁成功后,所有redis重启,clientB能够再次获取到锁,这样违法了锁的排他互斥性;
(2)如果启动AOF永久化存储,事情会好些, 举例:当我们重启redis后,由于redis过期机制是按照unix时间戳走的,所以在重启后,然后会按照规定的时间过期,不影响业务;但是由于AOF同步到磁盘的方式默认是每秒一次,如果在一秒内断电,会导致数据丢失,立即重启会造成锁互斥性失效;但如果同步磁盘方式使用Always(每一个写命令都同步到硬盘)造成性能急剧下降;所以在锁完全有效性和性能方面要有所取舍;
(3)为了有效解决既保证锁完全有效性 和 性能高效问题:antirez又提出了“延迟重启”的概念,redis同步到磁盘方式保持默认的每秒1次,在redis崩溃单机后(无论是一个还是所有),先不立即重启它,而是等待TTL时间后再重启,这样的话,这个节点在重启前所参与的锁都会过期,它在重启后就不会对现有的锁造成影响,缺点是在TTL时间内服务相当于暂停状态;
3、Redisson中RedLock的实现:
在JAVA的redisson包已经实现了对RedLock的封装,主要是通过 redisClient 与 lua 脚本实现的,之所以使用 lua 脚本,是为了实现加解锁校验与执行的事务性。
同样,基于RedLock实现的分布式锁也存在 client 获取锁之后,在 TTL 时间内没有完成业务逻辑的处理,而此时锁会被自动释放,造成多个线程同时持有锁的问题。而Redisson 在实现的过程中,自然也考虑到了这一问题,redisson 提供了一个“看门狗”的特性,当锁即将过期还没有释放时,不断的延长锁key的生存时间。(具体实现原理会在方案四进行介绍)
方案四:基于Redisson看门狗的分布式锁
前面说了,如果某些原因导致持有锁的线程在锁过期时间内,还没执行完任务,而锁因为还没超时被自动释放了,那么就会导致多个线程同时持有锁的现象出现,而为了解决这个问题,可以进行“锁续期”。其实,在JAVA的Redisson包中有一个"看门狗"机制,已经帮我们实现了这个功能。
1、redisson原理:
redisson在获取锁之后,会维护一个看门狗线程,当锁即将过期还没有释放时,不断的延长锁key的生存时间
2、加锁机制:
线程去获取锁,获取成功:执行lua脚本,保存数据到redis数据库。
线程去获取锁,获取失败:一直通过while循环尝试获取锁,获取成功后,执行lua脚本,保存数据到redis数据库。
3、watch dog自动延期机制:
看门狗启动后,对整体性能也会有一定影响,默认情况下看门狗线程是不启动的。如果使用redisson进行加锁的同时设置了锁的过期时间,也会导致看门狗机制失效。
redisson在获取锁之后,会维护一个看门狗线程,在每一个锁设置的过期时间的1/3处,如果线程还没执行完任务,则不断延长锁的有效期。看门狗的检查锁超时时间默认是30秒,可以通过 lockWactchdogTimeout 参数来改变。
加锁的时间默认是30秒,如果加锁的业务没有执行完,那么每隔 30 ÷ 3 = 10秒,就会进行一次续期,把锁重置成30秒,保证解锁前锁不会自动失效。
那万一业务的机器宕机了呢?如果宕机了,那看门狗线程就执行不了了,就续不了期,那自然30秒之后锁就解开了呗。
4、redisson分布式锁的关键点:
a. 对key不设置过期时间,由Redisson在加锁成功后给维护一个watchdog看门狗,watchdog负责定时监听并处理,在锁没有被释放且快要过期的时候自动对锁进行续期,保证解锁前锁不会自动失效
b. 通过Lua脚本实现了加锁和解锁的原子操作
c. 通过记录获取锁的客户端id,每次加锁时判断是否是当前客户端已经获得锁,实现了可重入锁。
5、Redisson的使用:
在方案三中,我们已经演示了基于Redisson的RedLock的使用案例,其实 Redisson 也封装 可重入锁(Reentrant Lock)、公平锁(Fair Lock)、联锁(MultiLock)、红锁(RedLock)、读写锁(ReadWriteLock)、 信号量(Semaphore)、可过期性信号量(PermitExpirableSemaphore)、 闭锁(CountDownLatch)等,具体使用说明可以参考官方文档:Redisson的分布式锁和同步器
redis命令说明:
(1)setnx命令:set if not exists,当且仅当 key 不存在时,将 key 的值设为 value。若给定的 key 已经存在,则 SETNX 不做任何动作。
返回1,说明该进程获得锁,将 key 的值设为 value
返回0,说明其他进程已经获得了锁,进程不能进入临界区。
命令格式:setnx lock.key lock.value
(2)get命令:获取key的值,如果存在,则返回;如果不存在,则返回nil
命令格式:get lock.key
(3)getset命令:该方法是原子的,对key设置newValue这个值,并且返回key原来的旧值。
命令格式:getset lock.key newValue
(4)del命令:删除redis中指定的key
命令格式:del lock.key
方案一:基于set命令的分布式锁
1、加锁:使用setnx进行加锁,当该指令返回1时,说明成功获得锁
2、解锁:当得到锁的线程执行完任务之后,使用del命令释放锁,以便其他线程可以继续执行setnx命令来获得锁
(1)存在的问题:假设线程获取了锁之后,在执行任务的过程中挂掉,来不及显示地执行del命令释放锁,那么竞争该锁的线程都会执行不了,产生死锁的情况。
(2)解决方案:设置锁超时时间
3、设置锁超时时间:setnx 的 key 必须设置一个超时时间,以保证即使没有被显式释放,这把锁也要在一定时间后自动释放。可以使用expire命令设置锁超时时间
(1)存在问题:
setnx 和 expire 不是原子性的操作,假设某个线程执行setnx 命令,成功获得了锁,但是还没来得及执行expire 命令,服务器就挂掉了,这样一来,这把锁就没有设置过期时间了,变成了死锁,别的线程再也没有办法获得锁了。
(2)解决方案:redis的set命令支持在获取锁的同时设置key的过期时间
4、使用set命令加锁并设置锁过期时间:
命令格式:set <lock.key> <lock.value> nx ex <expireTime>
(1)存在问题:
① 假如线程A成功得到了锁,并且设置的超时时间是 30 秒。如果某些原因导致线程 A 执行的很慢,过了 30 秒都没执行完,这时候锁过期自动释放,线程 B 得到了锁。
② 随后,线程A执行完任务,接着执行del指令来释放锁。但这时候线程 B 还没执行完,线程A实际上删除的是线程B加的锁。
(2)解决方案:
可以在 del 释放锁之前做一个判断,验证当前的锁是不是自己加的锁。在加锁的时候把当前的线程 ID 当做value,并在删除之前验证 key 对应的 value 是不是自己线程的 ID。但是,这样做其实隐含了一个新的问题,get操作、判断和释放锁是两个独立操作,不是原子性。对于非原子性的问题,我们可以使用Lua脚本来确保操作的原子性
5、锁续期:(这种机制类似于redisson的看门狗机制,文章后面会详细说明)
虽然步骤4避免了线程A误删掉key的情况,但是同一时间有 A,B 两个线程在访问代码块,仍然是不完美的。怎么办呢?我们可以让获得锁的线程开启一个守护线程,用来给快要过期的锁“续期”。
① 假设线程A执行了29 秒后还没执行完,这时候守护线程会执行 expire 指令,为这把锁续期 20 秒。守护线程从第 29 秒开始执行,每 20 秒执行一次。
② 情况一:当线程A执行完任务,会显式关掉守护线程。
③ 情况二:如果服务器忽然断电,由于线程 A 和守护线程在同一个进程,守护线程也会停下。这把锁到了超时的时候,没人给它续命,也就自动释放了。
方案二:基于setnx、get、getset的分布式锁
1、实现原理:
(1)setnx(lockkey, 当前时间+过期超时时间) ,如果返回1,则获取锁成功;如果返回0则没有获取到锁,转向步骤(2)
(2)get(lockkey)获取值oldExpireTime ,并将这个value值与当前的系统时间进行比较,如果小于当前系统时间,则认为这个锁已经超时,可以允许别的请求重新获取,转向步骤(3)
(3)计算新的过期时间 newExpireTime=当前时间+锁超时时间,然后getset(lockkey, newExpireTime) 会返回当前lockkey的值currentExpireTime
(4)判断 currentExpireTime 与 oldExpireTime 是否相等,如果相等,说明当前getset设置成功,获取到了锁。如果不相等,说明这个锁又被别的请求获取走了,那么当前请求可以直接返回失败,或者继续重试。
(5)在获取到锁之后,当前线程可以开始自己的业务处理,当处理完毕后,比较自己的处理时间和对于锁设置的超时时间,如果小于锁设置的超时时间,则直接执行del命令释放锁(释放锁之前需要判断持有锁的线程是不是当前线程);如果大于锁设置的超时时间,则不需要再锁进行处理。
2、代码实现:
(1)获取锁的实现方式:
tryLock方法中,主要逻辑如下:lock调用tryLock方法,参数为获取的超时时间与单位,线程在超时时间内,获取锁操作将自旋在那里,直到该自旋锁的保持者释放了锁。
(2)释放锁的实现方式:
存在问题:
(1)这个锁的核心是基于System.currentTimeMillis(),如果多台服务器时间不一致,那么问题就出现了,但是这个bug完全可以从服务器运维层面规避的,而且如果服务器时间不一样的话,只要和时间相关的逻辑都是会出问题的
(2)如果前一个锁超时的时候,刚好有多台服务器去请求获取锁,那么就会出现同时执行redis.getset()而导致出现过期时间覆盖问题,不过这种情况并不会对正确结果造成影响
(3)存在多个线程同时持有锁的情况:如果线程A执行任务的时间超过锁的过期时间,这时另一个线程就可以获得这个锁了,造成多个线程同时持有锁的情况。类似于方案一,可以使用“锁续期”的方式来解决。
前两种redis分布式锁的存在的问题
前面两种redis分布式锁的实现方式,如果从“高可用”的层面来看,仍然是有所欠缺,也就是说当 redis 是单点的情况下,当发生故障时,则整个业务的分布式锁都将无法使用。
为了提高可用性,我们可以使用主从模式或者哨兵模式,但在这种情况下仍然存在问题,在主从模式或者哨兵模式下,正常情况下,如果加锁成功了,那么master节点会异步复制给对应的slave节点。但是如果在这个过程中发生master节点宕机,主备切换,slave节点从变为了 master节点,而锁还没从旧master节点同步过来,这就发生了锁丢失,会导致多个客户端可以同时持有同一把锁的问题。来看个图来想下这个过程:
那么,如何避免这种情况呢?redis 官方给出了基于多个 redis 集群部署的高可用分布式锁解决方案:RedLock,在方案三我们就来详细介绍一下。(备注:如果master节点宕机期间,可以容忍多个客户端同时持有锁,那么就不需要redLock)
方案三:基于RedLock的分布式锁
Redlock算法是Redis的作者 Antirez 在单Redis节点基础上引入的高可用模式。Redlock的加锁要结合单节点分布式锁算法共同实现,因为它是RedLock的基础
1、加锁实现原理:
现在假设有5个Redis主节点(大于3的奇数个),这样基本保证他们不会同时都宕掉,获取锁和释放锁的过程中,客户端会执行以下操作:
(1)获取当前Unix时间,以毫秒为单位,并设置超时时间TTL
TTL 要大于 正常业务执行的时间 + 获取所有redis服务消耗时间 + 时钟漂移
(2)依次尝试从5个实例,使用相同的key和具有唯一性的value获取锁,当向Redis请求获取锁时,客户端应该设置一个网络连接和响应超时时间,这个超时时间应该小于锁的失效时间TTL,这样可以避免客户端死等。比如:TTL为5s,设置获取锁最多用1s,所以如果一秒内无法获取锁,就放弃获取这个锁,从而尝试获取下个锁
(3)客户端 获取所有能获取的锁后的时间 减去 第(1)步的时间,就得到锁的获取时间。锁的获取时间要小于锁失效时间TTL,并且至少从半数以上的Redis节点取到锁,才算获取成功锁
(4)如果成功获得锁,key的真正有效时间 = TTL - 锁的获取时间 - 时钟漂移。比如:TTL 是5s,获取所有锁用了2s,则真正锁有效时间为3s
(5)如果因为某些原因,获取锁失败(没有在半数以上实例取到锁或者取锁时间已经超过了有效时间),客户端应该在所有的Redis实例上进行解锁,无论Redis实例是否加锁成功,因为可能服务端响应消息丢失了但是实际成功了。
设想这样一种情况:客户端发给某个Redis节点的获取锁的请求成功到达了该Redis节点,这个节点也成功执行了SET操作,但是它返回给客户端的响应包却丢失了。这在客户端看来,获取锁的请求由于超时而失败了,但在Redis这边看来,加锁已经成功了。因此,释放锁的时候,客户端也应该对当时获取锁失败的那些Redis节点同样发起请求。实际上,这种情况在异步通信模型中是有可能发生的:客户端向服务器通信是正常的,但反方向却是有问题的。
(6)失败重试:当client不能获取锁时,应该在随机时间后重试获取锁;同时重试获取锁要有一定次数限制;
在随机时间后进行重试,主要是防止过多的客户端同时尝试去获取锁,导致彼此都获取锁失败的问题。
2、RedLock性能及崩溃恢复的相关解决方法:
由于N个Redis节点中的大多数能正常工作就能保证Redlock正常工作,因此理论上它的可用性更高。前面我们说的主从架构下存在的安全性问题,在RedLock中已经不存在了,但如果有节点发生崩溃重启,还是会对锁的安全性有影响的,具体的影响程度跟Redis持久化配置有关:
(1)如果redis没有持久化功能,在clientA获取锁成功后,所有redis重启,clientB能够再次获取到锁,这样违法了锁的排他互斥性;
(2)如果启动AOF永久化存储,事情会好些, 举例:当我们重启redis后,由于redis过期机制是按照unix时间戳走的,所以在重启后,然后会按照规定的时间过期,不影响业务;但是由于AOF同步到磁盘的方式默认是每秒一次,如果在一秒内断电,会导致数据丢失,立即重启会造成锁互斥性失效;但如果同步磁盘方式使用Always(每一个写命令都同步到硬盘)造成性能急剧下降;所以在锁完全有效性和性能方面要有所取舍;
(3)为了有效解决既保证锁完全有效性 和 性能高效问题:antirez又提出了“延迟重启”的概念,redis同步到磁盘方式保持默认的每秒1次,在redis崩溃单机后(无论是一个还是所有),先不立即重启它,而是等待TTL时间后再重启,这样的话,这个节点在重启前所参与的锁都会过期,它在重启后就不会对现有的锁造成影响,缺点是在TTL时间内服务相当于暂停状态;
3、Redisson中RedLock的实现:
在JAVA的redisson包已经实现了对RedLock的封装,主要是通过 redisClient 与 lua 脚本实现的,之所以使用 lua 脚本,是为了实现加解锁校验与执行的事务性。
同样,基于RedLock实现的分布式锁也存在 client 获取锁之后,在 TTL 时间内没有完成业务逻辑的处理,而此时锁会被自动释放,造成多个线程同时持有锁的问题。而Redisson 在实现的过程中,自然也考虑到了这一问题,redisson 提供了一个“看门狗”的特性,当锁即将过期还没有释放时,不断的延长锁key的生存时间。(具体实现原理会在方案四进行介绍)
方案四:基于Redisson看门狗的分布式锁
前面说了,如果某些原因导致持有锁的线程在锁过期时间内,还没执行完任务,而锁因为还没超时被自动释放了,那么就会导致多个线程同时持有锁的现象出现,而为了解决这个问题,可以进行“锁续期”。其实,在JAVA的Redisson包中有一个"看门狗"机制,已经帮我们实现了这个功能。
1、redisson原理:
redisson在获取锁之后,会维护一个看门狗线程,当锁即将过期还没有释放时,不断的延长锁key的生存时间
2、加锁机制:
线程去获取锁,获取成功:执行lua脚本,保存数据到redis数据库。
线程去获取锁,获取失败:一直通过while循环尝试获取锁,获取成功后,执行lua脚本,保存数据到redis数据库。
3、watch dog自动延期机制:
看门狗启动后,对整体性能也会有一定影响,默认情况下看门狗线程是不启动的。如果使用redisson进行加锁的同时设置了锁的过期时间,也会导致看门狗机制失效。
redisson在获取锁之后,会维护一个看门狗线程,在每一个锁设置的过期时间的1/3处,如果线程还没执行完任务,则不断延长锁的有效期。看门狗的检查锁超时时间默认是30秒,可以通过 lockWactchdogTimeout 参数来改变。
加锁的时间默认是30秒,如果加锁的业务没有执行完,那么每隔 30 ÷ 3 = 10秒,就会进行一次续期,把锁重置成30秒,保证解锁前锁不会自动失效。
那万一业务的机器宕机了呢?如果宕机了,那看门狗线程就执行不了了,就续不了期,那自然30秒之后锁就解开了呗。
4、redisson分布式锁的关键点:
a. 对key不设置过期时间,由Redisson在加锁成功后给维护一个watchdog看门狗,watchdog负责定时监听并处理,在锁没有被释放且快要过期的时候自动对锁进行续期,保证解锁前锁不会自动失效
b. 通过Lua脚本实现了加锁和解锁的原子操作
c. 通过记录获取锁的客户端id,每次加锁时判断是否是当前客户端已经获得锁,实现了可重入锁。
5、Redisson的使用:
在方案三中,我们已经演示了基于Redisson的RedLock的使用案例,其实 Redisson 也封装 可重入锁(Reentrant Lock)、公平锁(Fair Lock)、联锁(MultiLock)、红锁(RedLock)、读写锁(ReadWriteLock)、 信号量(Semaphore)、可过期性信号量(PermitExpirableSemaphore)、 闭锁(CountDownLatch)等,具体使用说明可以参考官方文档:Redisson的分布式锁和同步器
分布式锁三种方式的一个比较
从理解的难易程度角度(从低到高)
数据库 > 缓存 > Zookeeper
从实现的复杂性角度(从低到高)
Zookeeper >= 缓存 > 数据库
从性能角度(从高到低)
缓存 > Zookeeper >= 数据库
从可靠性角度(从高到低)
Zookeeper > 缓存 > 数据库
数据库 > 缓存 > Zookeeper
从实现的复杂性角度(从低到高)
Zookeeper >= 缓存 > 数据库
从性能角度(从高到低)
缓存 > Zookeeper >= 数据库
从可靠性角度(从高到低)
Zookeeper > 缓存 > 数据库
多线程
并行和并发有什么区别?
并行是指两个或者多个事件在同一时刻发生;而并发是指两个或多个事件在同一时间间隔发生。
并行是在不同实体上的多个事件,并发是在同一实体上的多个事件。
在一台处理器上“同时”处理多个任务,在多台处理器上同时处理多个任务。如hadoop分布式集群。
并行是在不同实体上的多个事件,并发是在同一实体上的多个事件。
在一台处理器上“同时”处理多个任务,在多台处理器上同时处理多个任务。如hadoop分布式集群。
线程和进程的区别?
简而言之,进程是程序运行和资源分配的基本单位,一个程序至少有一个进程,一个进程至少有一个线程。进程在执行过程中拥有独立的内存单元,而多个线程共享内存资源,减少切换次数,从而效率更高。线程是进程的一个实体,是cpu调度和分派的基本单位,是比程序更小的能独立运行的基本单位。同一进程中的多个线程之间可以并发执行。
sleep() 和 wait() 有什么区别?
wait来自Object类,sleep来自Thread类
wait等待的过程中会释放锁,sleep等待的过程中不会释放锁
wait必须在同步代码块中使用,sleep可以在任何地方使用
wait不需要捕获异常,sleep需要捕获异常
wait等待的过程中会释放锁,sleep等待的过程中不会释放锁
wait必须在同步代码块中使用,sleep可以在任何地方使用
wait不需要捕获异常,sleep需要捕获异常
创建线程有哪几种方式?
继承Thread类创建线程类,实现run方法
定义Thread类的子类,并重写该类的run方法,该run方法的方法体就代表了线程要完成的任务。因此把run()方法称为执行体。
创建Thread子类的实例,即创建了线程对象。
调用线程对象的start()方法来启动该线程。
创建Thread子类的实例,即创建了线程对象。
调用线程对象的start()方法来启动该线程。
通过Runnable接口创建线程类,实现run方法
定义runnable接口的实现类,并重写该接口的run()方法,该run()方法的方法体同样是该线程的线程执行体。
创建 Runnable实现类的实例,并依此实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象。
调用线程对象的start()方法来启动该线程。
创建 Runnable实现类的实例,并依此实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象。
调用线程对象的start()方法来启动该线程。
通过Callable和Future创建线程,实现call方法
创建Callable接口的实现类,并实现call()方法,该call()方法将作为线程执行体,并且有返回值。
创建Callable实现类的实例,使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法的返回值。
使用FutureTask对象作为Thread对象的target创建并启动新线程。
调用FutureTask对象的get()方法来获得子线程执行结束后的返回值。
创建Callable实现类的实例,使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法的返回值。
使用FutureTask对象作为Thread对象的target创建并启动新线程。
调用FutureTask对象的get()方法来获得子线程执行结束后的返回值。
通过线程池进行创建
四种创建线程的区别?
1.线程只是实现Runnable接口或Callable接口,还可以继承其他类(有点像接口和抽象类的区别,java是单继承的,但可以实现多个接口)
2.实现接口的方式多个线程可以共享一个target对象,非常适合多线程处理同一份资源的情形
3.如果需要访问当前线程,必须调用Thread.currentThread方法
4.继承Thread类的线程类不能再继承其他父类(java单继承决定)
因此,一般推荐采用实现接口的方式来创建线程。
2.实现接口的方式多个线程可以共享一个target对象,非常适合多线程处理同一份资源的情形
3.如果需要访问当前线程,必须调用Thread.currentThread方法
4.继承Thread类的线程类不能再继承其他父类(java单继承决定)
因此,一般推荐采用实现接口的方式来创建线程。
说一下 runnable 和 callable 有什么区别?
Runnable接口run方法无返回值,Callable接口call方法有返回值,支持泛型
Runnable接口run方法只能抛出运行时异常,且无法捕获处理;Callable接口call方法允许抛出异常,可以捕获异常信息
Runnable接口run方法只能抛出运行时异常,且无法捕获处理;Callable接口call方法允许抛出异常,可以捕获异常信息
线程有哪些状态?
线程通常都有五种状态,创建、就绪、运行、阻塞和死亡。
创建状态。在生成线程对象,并没有调用该对象的start方法,这是线程处于创建状态。
就绪状态。当调用了线程对象的start方法之后,该线程就进入了就绪状态,但是此时线程调度程序还没有把该线程设置为当前线程,此时处于就绪状态。在线程运行之后,从等待或者睡眠中回来之后,也会处于就绪状态。
运行状态。线程调度程序将处于就绪状态的线程设置为当前线程,此时线程就进入了运行状态,开始运行run函数当中的代码。
阻塞状态。线程正在运行的时候,被暂停,通常是为了等待某个时间的发生(比如说某项资源就绪)之后再继续运行。sleep,suspend,wait等方法都可以导致线程阻塞。
死亡状态。如果一个线程的run方法执行结束或者调用stop方法后,该线程就会死亡。对于已经死亡的线程,无法再使用start方法令其进入就绪
就绪状态。当调用了线程对象的start方法之后,该线程就进入了就绪状态,但是此时线程调度程序还没有把该线程设置为当前线程,此时处于就绪状态。在线程运行之后,从等待或者睡眠中回来之后,也会处于就绪状态。
运行状态。线程调度程序将处于就绪状态的线程设置为当前线程,此时线程就进入了运行状态,开始运行run函数当中的代码。
阻塞状态。线程正在运行的时候,被暂停,通常是为了等待某个时间的发生(比如说某项资源就绪)之后再继续运行。sleep,suspend,wait等方法都可以导致线程阻塞。
死亡状态。如果一个线程的run方法执行结束或者调用stop方法后,该线程就会死亡。对于已经死亡的线程,无法再使用start方法令其进入就绪
Java线程池中队列常用类型有哪些?
ArrayBlockingQueue 是一个基于数组结构的有界阻塞队列,此队列按 FIFO(先进先出)原则对元素进行排序。
LinkedBlockingQueue 一个基于链表结构的阻塞队列,此队列按FIFO (先进先出) 排序元素,吞吐量通常要高于 ArrayBlockingQueue 。
SynchronousQueue 一个不存储元素的阻塞队列。
PriorityBlockingQueue 一个具有优先级的无限阻塞队列。 PriorityBlockingQueue 也是基于最小二叉堆实现
DelayQueue只有当其指定的延迟时间到了,才能够从队列中获取到该元素。
DelayQueue 是一个没有大小限制的队列,因此往队列中插入数据的操作(生产者)永远不会被阻塞,而只有获取数据的操作(消费
者)才会被阻塞。
LinkedBlockingQueue 一个基于链表结构的阻塞队列,此队列按FIFO (先进先出) 排序元素,吞吐量通常要高于 ArrayBlockingQueue 。
SynchronousQueue 一个不存储元素的阻塞队列。
PriorityBlockingQueue 一个具有优先级的无限阻塞队列。 PriorityBlockingQueue 也是基于最小二叉堆实现
DelayQueue只有当其指定的延迟时间到了,才能够从队列中获取到该元素。
DelayQueue 是一个没有大小限制的队列,因此往队列中插入数据的操作(生产者)永远不会被阻塞,而只有获取数据的操作(消费
者)才会被阻塞。
在 java 程序中怎么保证多线程的运行安全?
线程安全在三个方面体现:
原子性:提供互斥访问,同一时刻只能有一个线程对数据进行操作,(atomic,synchronized);
可见性:一个线程对主内存的修改可以及时地被其他线程看到,(synchronized,volatile);
有序性:一个线程观察其他线程中的指令执行顺序,由于指令重排序,该观察结果一般杂乱无序,(happens-before原则)。
原子性:提供互斥访问,同一时刻只能有一个线程对数据进行操作,(atomic,synchronized);
可见性:一个线程对主内存的修改可以及时地被其他线程看到,(synchronized,volatile);
有序性:一个线程观察其他线程中的指令执行顺序,由于指令重排序,该观察结果一般杂乱无序,(happens-before原则)。
CAS的原理呢?
CAS叫做CompareAndSwap,⽐较并交换,主要是通过处理器的指令来保证操作的原⼦性,它包含三个
操作数:
1. 变量内存地址,V表示
2. 旧的预期值,A表示
3. 准备设置的新值,B表示
当执⾏CAS指令时,只有当V等于A时,才会⽤B去更新V的值,否则就不会执⾏更新操作。
操作数:
1. 变量内存地址,V表示
2. 旧的预期值,A表示
3. 准备设置的新值,B表示
当执⾏CAS指令时,只有当V等于A时,才会⽤B去更新V的值,否则就不会执⾏更新操作。
ThreadLocal 是什么?有哪些使用场景?
线程局部变量是局限于线程内部的变量,属于线程自身所有,不在多个线程间共享。Java提供ThreadLocal类来支持线程局部变量,是一种实现线程安全的方式。
ThreadLocal 有一个「静态内部类 ThreadLocalMap」,ThreadLocalMap 又包含了一个 Entry 数组,「Entry 本身是一个弱引用」,他的 key 是指向 ThreadLocal 的弱引用,「弱引用的目的是为了防止内存泄露」,如果是强引用那么除非线程结束,否则无法终止,可能会有内存泄漏的风险。
但是这样还是会存在内存泄露的问题,假如 key 和 ThreadLocal 对象被回收之后,entry 中就存在 key 为 null ,但是 value 有值的 entry 对象,但是永远没办法被访问到,同样除非线程结束运行。「解决方法就是调用 remove 方法删除 entry 对象」。
notify()和 notifyAll()有什么区别?
notify可能会导致死锁,而notifyAll则不会
任何时候只有一个线程可以获得锁,也就是说只有一个线程可以运行synchronized 中的代码
使用notifyall,可以唤醒 所有处于wait状态的线程,使其重新进入锁的争夺队列中,而notify只能唤
醒一个。
wait() 应配合while循环使用,不应使用if,务必在wait()调用前后都检查条件,如果不满足,必须调
用notify()唤醒另外的线程来处理,自己继续wait()直至条件满足再往下执行。
notify() 是对notifyAll()的一个优化,但它有很精确的应用场景,并且要求正确使用。不然可能导致
死锁。正确的场景应该是 WaitSet中等待的是相同的条件,唤醒任一个都能正确处理接下来的事
项,如果唤醒的线程无法正确处理,务必确保继续notify()下一个线程,并且自身需要重新回到
WaitSet中.
任何时候只有一个线程可以获得锁,也就是说只有一个线程可以运行synchronized 中的代码
使用notifyall,可以唤醒 所有处于wait状态的线程,使其重新进入锁的争夺队列中,而notify只能唤
醒一个。
wait() 应配合while循环使用,不应使用if,务必在wait()调用前后都检查条件,如果不满足,必须调
用notify()唤醒另外的线程来处理,自己继续wait()直至条件满足再往下执行。
notify() 是对notifyAll()的一个优化,但它有很精确的应用场景,并且要求正确使用。不然可能导致
死锁。正确的场景应该是 WaitSet中等待的是相同的条件,唤醒任一个都能正确处理接下来的事
项,如果唤醒的线程无法正确处理,务必确保继续notify()下一个线程,并且自身需要重新回到
WaitSet中.
volatile原理知道吗?
相⽐synchronized的加锁⽅式来解决共享变量的内存可⻅性问题,volatile就是更轻量的选择,他没有上
下⽂切换的额外开销成本。使⽤volatile声明的变量,可以确保值被更新的时候对其他线程⽴刻可⻅(缓存一致性协议)。
volatile使⽤内存屏障来保证不会发⽣指令重排,解决了内存可⻅性的问题。
下⽂切换的额外开销成本。使⽤volatile声明的变量,可以确保值被更新的时候对其他线程⽴刻可⻅(缓存一致性协议)。
volatile使⽤内存屏障来保证不会发⽣指令重排,解决了内存可⻅性的问题。
说内存屏障的问题,volatile修饰之后会加⼊不同的内存屏障来保证可⻅性的问题能正确执⾏。这⾥
写的屏障基于书中提供的内容,但是实际上由于CPU架构不同,重排序的策略不同,提供的内存屏障也
不⼀样,⽐如x86平台上,只有StoreLoad⼀种内存屏障。
1. StoreStore屏障,保证上⾯的普通写不和volatile写发⽣重排序
2. StoreLoad屏障,保证volatile写与后⾯可能的volatile读写不发⽣重排序
3. LoadLoad屏障,禁⽌volatile读与后⾯的普通读重排序
4. LoadStore屏障,禁⽌volatile读和后⾯的普通写重排序
写的屏障基于书中提供的内容,但是实际上由于CPU架构不同,重排序的策略不同,提供的内存屏障也
不⼀样,⽐如x86平台上,只有StoreLoad⼀种内存屏障。
1. StoreStore屏障,保证上⾯的普通写不和volatile写发⽣重排序
2. StoreLoad屏障,保证volatile写与后⾯可能的volatile读写不发⽣重排序
3. LoadLoad屏障,禁⽌volatile读与后⾯的普通读重排序
4. LoadStore屏障,禁⽌volatile读和后⾯的普通写重排序
volatile是什么?可以保证有序性吗?
一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,那么就具备了两层语
义:
1)保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对
其他线程来说是立即可见的,volatile关键字会强制将修改的值立即写入主存。
2)禁止进行指令重排序。
volatile 不是原子性操作
什么叫保证部分有序性?
当程序执行到volatile变量的读操作或者写操作时,在其前面的操作的更改肯定全部已经进行,且结
果已经对后面的操作可见
使用volatile 一般用于 状态标记量 和 单例模式的双检锁。
义:
1)保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对
其他线程来说是立即可见的,volatile关键字会强制将修改的值立即写入主存。
2)禁止进行指令重排序。
volatile 不是原子性操作
什么叫保证部分有序性?
当程序执行到volatile变量的读操作或者写操作时,在其前面的操作的更改肯定全部已经进行,且结
果已经对后面的操作可见
使用volatile 一般用于 状态标记量 和 单例模式的双检锁。
synchronized 和 ReentrantLock 有什么不同?
synchronized是java关键字,lock是java类
synchronized无法判断是否获取锁的状态,lock可以判断是否获取到锁的状态
synchronized会自动释放锁,lock需要手工释放锁
synchronized不可中断、非公平锁,lock可中断、公平锁和非公共锁
synchronized无法判断是否获取锁的状态,lock可以判断是否获取到锁的状态
synchronized会自动释放锁,lock需要手工释放锁
synchronized不可中断、非公平锁,lock可中断、公平锁和非公共锁
有三个线程T1,T2,T3,如何保证顺序执行?
可以用线程类的join方法在一个线程中启动另一个线程,另外一个线程完成该线程继续执行。为了确保三个线程的顺序你应该先启动最后一个(T3调
用T2,T2调用T1),这样T1就会先完成而T3最后完成。
用T2,T2调用T1),这样T1就会先完成而T3最后完成。
线程安全需要保证几个基本特征?
原子性,简单说就是相关操作不会中途被其他线程干扰,一般通过同步机制实现。
可见性,是一个线程修改了某个共享变量,其状态能够立即被其他线程知晓,通常被解释为将
线程本地状态反映到主内存上,volatile 就是负责保证可见性的。
有序性,是保证线程内串行语义,避免指令重排等。
可见性,是一个线程修改了某个共享变量,其状态能够立即被其他线程知晓,通常被解释为将
线程本地状态反映到主内存上,volatile 就是负责保证可见性的。
有序性,是保证线程内串行语义,避免指令重排等。
说一下线程之间是如何通信的?
线程之间的通信有两种方式:wait和notify或者共享变量
线程时的拒绝策略有哪些?
1. AbortPolicy:直接丢弃任务,抛出异常,这是默认策略
2. CallerRunsPolicy:只用调用者所在的线程来处理任务
3. DiscardOldestPolicy:丢弃等待队列中最旧的任务,并执行当前任务
4. DiscardPolicy:直接丢弃任务,也不抛出异常
2. CallerRunsPolicy:只用调用者所在的线程来处理任务
3. DiscardOldestPolicy:丢弃等待队列中最旧的任务,并执行当前任务
4. DiscardPolicy:直接丢弃任务,也不抛出异常
什么是AQS?
简单说一下AQS,AQS全称为AbstractQueuedSychronizer,翻译过来应该是抽象队列同步器。
如果说java.util.concurrent的基础是CAS的话,那么AQS就是整个Java并发包的核心了,
ReentrantLock、CountDownLatch、Semaphore等等都用到了它。AQS实际上以双向队列的形式
连接所有的Entry,比方说ReentrantLock,所有等待的线程都被放在一个Entry中并连成双向队
列,前面一个线程使用ReentrantLock好了,则双向队列实际上的第一个Entry开始运行。
AQS定义了对双向队列所有的操作,而只开放了tryLock和tryRelease方法给开发者使用,开发者可
以根据自己的实现重写tryLock和tryRelease方法,以实现自己的并发功能。
如果说java.util.concurrent的基础是CAS的话,那么AQS就是整个Java并发包的核心了,
ReentrantLock、CountDownLatch、Semaphore等等都用到了它。AQS实际上以双向队列的形式
连接所有的Entry,比方说ReentrantLock,所有等待的线程都被放在一个Entry中并连成双向队
列,前面一个线程使用ReentrantLock好了,则双向队列实际上的第一个Entry开始运行。
AQS定义了对双向队列所有的操作,而只开放了tryLock和tryRelease方法给开发者使用,开发者可
以根据自己的实现重写tryLock和tryRelease方法,以实现自己的并发功能。
介绍一下四种引用类型?
「强引用 StrongReference」
Object obj = new Object();
//只要obj还指向Object对象,Object对象就不会被回收
垃圾回收器不会回收被引用的对象,哪怕内存不足时,JVM 也会直接抛出 OutOfMemoryError,除非赋值为 null。
Object obj = new Object();
//只要obj还指向Object对象,Object对象就不会被回收
垃圾回收器不会回收被引用的对象,哪怕内存不足时,JVM 也会直接抛出 OutOfMemoryError,除非赋值为 null。
「软引用 SoftReference」
软引用是用来描述一些非必需但仍有用的对象。在内存足够的时候,软引用对象不会被回收,只有在内存不足时,系统则会回收软引用对象,如果回收了软引用对象之后仍然没有足够的内存,才会抛出内存溢出异常。
软引用是用来描述一些非必需但仍有用的对象。在内存足够的时候,软引用对象不会被回收,只有在内存不足时,系统则会回收软引用对象,如果回收了软引用对象之后仍然没有足够的内存,才会抛出内存溢出异常。
「弱引用 WeakReference」
弱引用的引用强度比软引用要更弱一些,无论内存是否足够,只要 JVM 开始进行垃圾回收,那些被弱引用关联的对象都会被回收。
弱引用的引用强度比软引用要更弱一些,无论内存是否足够,只要 JVM 开始进行垃圾回收,那些被弱引用关联的对象都会被回收。
「虚引用 PhantomReference」
虚引用是最弱的一种引用关系,如果一个对象仅持有虚引用,那么它就和没有任何引用一样,它随时可能会被回收,在 JDK1.2 之后,用 PhantomReference 类来表示,通过查看这个类的源码,发现它只有一个构造函数和一个 get() 方法,而且它的 get() 方法仅仅是返回一个null,也就是说将永远无法通过虚引用来获取对象,虚引用必须要和 ReferenceQueue 引用队列一起使用,NIO 的堆外内存就是靠其管理
虚引用是最弱的一种引用关系,如果一个对象仅持有虚引用,那么它就和没有任何引用一样,它随时可能会被回收,在 JDK1.2 之后,用 PhantomReference 类来表示,通过查看这个类的源码,发现它只有一个构造函数和一个 get() 方法,而且它的 get() 方法仅仅是返回一个null,也就是说将永远无法通过虚引用来获取对象,虚引用必须要和 ReferenceQueue 引用队列一起使用,NIO 的堆外内存就是靠其管理
线程的 run()和 start()有什么区别?
每个线程都是通过某个特定Thread对象所对应的方法run()来完成其操作的,方法run()称为线程体。通过调用Thread类的start()方法来启动一个线程。
start()方法来启动一个线程,真正实现了多线程运行。这时无需等待run方法体代码执行完毕,可以直接继续执行下面的代码; 这时此线程是处于就绪状态, 并没有运行。 然后通过此Thread类调用方法run()来完成其运行状态, 这里方法run()称为线程体,它包含了要执行的这个线程的内容, Run方法运行结束, 此线程终止。然后CPU再调度其它线程。
run()方法是在本线程里的,只是线程里的一个函数,而不是多线程的。 如果直接调用run(),其实就相当于是调用了一个普通函数而已,直接待用run()方法必须等待run()方法执行完毕才能执行下面的代码,所以执行路径还是只有一条,根本就没有线程的特征,所以在多线程执行时要使用start()方法而不是run()方法。
线程池的原理/执行流程/拒绝策略?
1. 最⼤线程数maximumPoolSize
2. 核⼼线程数corePoolSize
3. 活跃时间keepAliveTime
4. 阻塞队列workQueue
5. 拒绝策略RejectedExecutionHandler
2. 核⼼线程数corePoolSize
3. 活跃时间keepAliveTime
4. 阻塞队列workQueue
5. 拒绝策略RejectedExecutionHandler
当提交⼀个新任务到线程池时,具体的执⾏流程如下:
1. 当我们提交任务,线程池会根据corePoolSize⼤⼩创建若⼲任务数量线程执⾏任务
2. 当任务的数量超过corePoolSize数量,后续的任务将会进⼊阻塞队列阻塞排队
3. 当阻塞队列也满了之后,那么将会继续创建(maximumPoolSize-corePoolSize)个数量的线程来执
⾏任务,如果任务处理完成,maximumPoolSize-corePoolSize额外创建的线程等待
keepAliveTime之后被⾃动销毁
4. 如果达到maximumPoolSize,阻塞队列还是满的状态,那么将根据不同的拒绝策略对应处理
1. 当我们提交任务,线程池会根据corePoolSize⼤⼩创建若⼲任务数量线程执⾏任务
2. 当任务的数量超过corePoolSize数量,后续的任务将会进⼊阻塞队列阻塞排队
3. 当阻塞队列也满了之后,那么将会继续创建(maximumPoolSize-corePoolSize)个数量的线程来执
⾏任务,如果任务处理完成,maximumPoolSize-corePoolSize额外创建的线程等待
keepAliveTime之后被⾃动销毁
4. 如果达到maximumPoolSize,阻塞队列还是满的状态,那么将根据不同的拒绝策略对应处理
主要有4种拒绝策略:
1. AbortPolicy:直接丢弃任务,抛出异常,这是默认策略
2. CallerRunsPolicy:只⽤调⽤者所在的线程来处理任务
3. DiscardOldestPolicy:丢弃等待队列中最旧的任务,并执⾏当前任务
4. DiscardPolicy:直接丢弃任务,也不抛出异常
1. AbortPolicy:直接丢弃任务,抛出异常,这是默认策略
2. CallerRunsPolicy:只⽤调⽤者所在的线程来处理任务
3. DiscardOldestPolicy:丢弃等待队列中最旧的任务,并执⾏当前任务
4. DiscardPolicy:直接丢弃任务,也不抛出异常
线程和协程的区别?
线程是操作系统调度执行的最小单位,协程是比线程更加轻量级的存在,区别在于协程不是被操作系统内核所管理,而完全被程序进行控制。协程在一个线程中执行,消除了线程切换的开销,性能优势明显。协程不需要多线程的锁机制,具有极高的执行效率
1) 一个线程可以多个协程,一个进程也可以单独拥有多个协程。
2) 线程进程都是同步机制,而协程则是异步。
3) 协程能保留上一次调用时的状态,每次过程重入时,就相当于进入上一次调用的状态。
4)线程是抢占式,而协程是非抢占式的,所以需要用户自己释放使用权来切换到其他协程,因此同一时间其实只有一个协程拥有运行权,相当于单线程的能 力。
5)协程并不是取代线程, 而且抽象于线程之上, 线程是被分割的CPU资源, 协程是组织好的代码流程, 协程需要线程来承载运行, 线程是协程的资源, 但协程不会直接使用线程, 协程直接利用的是执行器(Interceptor), 执行器可以关联任意线程或线程池, 可以使当前线程, UI线程, 或新建新程.。
6)线程是协程的资源。协程通过Interceptor来间接使用线程这个资源
2) 线程进程都是同步机制,而协程则是异步。
3) 协程能保留上一次调用时的状态,每次过程重入时,就相当于进入上一次调用的状态。
4)线程是抢占式,而协程是非抢占式的,所以需要用户自己释放使用权来切换到其他协程,因此同一时间其实只有一个协程拥有运行权,相当于单线程的能 力。
5)协程并不是取代线程, 而且抽象于线程之上, 线程是被分割的CPU资源, 协程是组织好的代码流程, 协程需要线程来承载运行, 线程是协程的资源, 但协程不会直接使用线程, 协程直接利用的是执行器(Interceptor), 执行器可以关联任意线程或线程池, 可以使当前线程, UI线程, 或新建新程.。
6)线程是协程的资源。协程通过Interceptor来间接使用线程这个资源
守护线程是什么?
守护线程(即daemon thread),是个服务线程,准确地来说就是服务其他的线程。
如何查看线程死锁?
命令: jstack pid
为什么wait, notify 和 notifyAll这些方法不在thread类里
面?
面?
明显的原因是JAVA提供的锁是对象级的而不是线程级的,每个对象都有锁,通过线程获得。如果线
程需要等待某些锁那么调用对象中的wait()方法就有意义了。如果wait()方法定义在Thread类中,线
程正在等待的是哪个锁就不明显了。简单的说,由于wait,notify和notifyAll都是锁级别的操作,所
以把他们定义在Object类中因为锁属于对象
程需要等待某些锁那么调用对象中的wait()方法就有意义了。如果wait()方法定义在Thread类中,线
程正在等待的是哪个锁就不明显了。简单的说,由于wait,notify和notifyAll都是锁级别的操作,所
以把他们定义在Object类中因为锁属于对象
Java中interrupted 和 isInterruptedd方法的区别?
interrupted() 和 isInterrupted()的主要区别是前者会将中断状态清除而后者不会。Java多线程的中
断机制是用内部标识来实现的,调用Thread.interrupt()来中断一个线程就会设置中断标识为true。
当中断线程调用静态方法Thread.interrupted()来检查中断状态时,中断状态会被清零。而非静态方
法isInterrupted()用来查询其它线程的中断状态且不会改变中断状态标识。简单的说就是任何抛出
InterruptedException异常的方法都会将中断状态清零。无论如何,一个线程的中断状态有有可能
被其它线程调用中断来改变。
断机制是用内部标识来实现的,调用Thread.interrupt()来中断一个线程就会设置中断标识为true。
当中断线程调用静态方法Thread.interrupted()来检查中断状态时,中断状态会被清零。而非静态方
法isInterrupted()用来查询其它线程的中断状态且不会改变中断状态标识。简单的说就是任何抛出
InterruptedException异常的方法都会将中断状态清零。无论如何,一个线程的中断状态有有可能
被其它线程调用中断来改变。
创建线程池有哪几种方式?
newFixedThreadPool(int nThreads)
创建一个固定长度的线程池,每当提交一个任务就创建一个线程,直到达到线程池的最大数量,这时线程规模将不再变化,当线程发生未预期的错误而结束时,线程池会补充一个新的线程
newCachedThreadPool()
创建一个固定长度的线程池,每当提交一个任务就创建一个线程,直到达到线程池的最大数量,这时线程规模将不再变化,当线程发生未预期的错误而结束时,线程池会补充一个新的线程
newSingleThreadExecutor()
这是一个单线程的Executor,它创建单个工作线程来执行任务,如果这个线程异常结束,会创建一个新的来替代它;它的特点是能确保依照任务在队列中的顺序来串行执行。
newScheduledThreadPool(int corePoolSize)
创建了一个固定长度的线程池,而且以延迟或定时的方式来执行任务,类似于Timer。
线程池都有哪些状态?
线程池有5种状态:Running、ShutDown、Stop、Tidying、Terminated。
1.RUNNING:这是最正常的状态,接受新的任务,处理等待队列中的任务。线程池的初始化状态是RUNNING。线程池被一旦被创建,就处于RUNNING状态,并且线程池中的任务数为0。
2.SHUTDOWN:不接受新的任务提交,但是会继续处理等待队列中的任务。调用线程池的shutdown()方法时,线程池由RUNNING -> SHUTDOWN。
3.STOP:不接受新的任务提交,不再处理等待队列中的任务,中断正在执行任务的线程。调用线程池的shutdownNow()方法时,线程池由(RUNNING or SHUTDOWN ) -> STOP。
4.TIDYING:所有的任务都销毁了,workCount 为 0,线程池的状态在转换为 TIDYING 状态时,会执行钩子方法 terminated()。因为terminated()在ThreadPoolExecutor类中是空的,所以用户想在线程池变为TIDYING时进行相应的处理;可以通过重载terminated()函数来实现。
当线程池在SHUTDOWN状态下,阻塞队列为空并且线程池中执行的任务也为空时,就会由 SHUTDOWN -> TIDYING。
当线程池在STOP状态下,线程池中执行的任务为空时,就会由STOP -> TIDYING。
5.TERMINATED:线程池处在TIDYING状态时,执行完terminated()之后,就会由 TIDYING -> TERMINATED。
2.SHUTDOWN:不接受新的任务提交,但是会继续处理等待队列中的任务。调用线程池的shutdown()方法时,线程池由RUNNING -> SHUTDOWN。
3.STOP:不接受新的任务提交,不再处理等待队列中的任务,中断正在执行任务的线程。调用线程池的shutdownNow()方法时,线程池由(RUNNING or SHUTDOWN ) -> STOP。
4.TIDYING:所有的任务都销毁了,workCount 为 0,线程池的状态在转换为 TIDYING 状态时,会执行钩子方法 terminated()。因为terminated()在ThreadPoolExecutor类中是空的,所以用户想在线程池变为TIDYING时进行相应的处理;可以通过重载terminated()函数来实现。
当线程池在SHUTDOWN状态下,阻塞队列为空并且线程池中执行的任务也为空时,就会由 SHUTDOWN -> TIDYING。
当线程池在STOP状态下,线程池中执行的任务为空时,就会由STOP -> TIDYING。
5.TERMINATED:线程池处在TIDYING状态时,执行完terminated()之后,就会由 TIDYING -> TERMINATED。
线程池中 submit()和 execute()方法有什么区别?
接收的参数不一样
submit有返回值,而execute没有
submit方便Exception处理
submit有返回值,而execute没有
submit方便Exception处理
那对象头具体都包含哪些内容?
在我们常⽤的Hotspot虚拟机中,对象在内存中布局实际包含3个部分:
1. 对象头
2. 实例数据
3. 对⻬填充
1. 对象头
2. 实例数据
3. 对⻬填充
对象头包含两部分内容,Mark Word中的内容会随着锁标志位⽽发⽣变化,所以只说存储结构就好
了。
1. 对象⾃身运⾏时所需的数据,也被称为Mark Word,也就是⽤于轻量级锁和偏向锁的关键点。具体
的内容包含对象的hashcode、分代年龄、轻量级锁指针、重量级锁指针、GC标记、偏向锁线程
ID、偏向锁时间戳。
2. 存储类型指针,也就是指向类的元数据的指针,通过这个指针才能确定对象是属于哪个类的实例。
如果是数组的话,则还包含了数组的⻓度(占用4字节)
了。
1. 对象⾃身运⾏时所需的数据,也被称为Mark Word,也就是⽤于轻量级锁和偏向锁的关键点。具体
的内容包含对象的hashcode、分代年龄、轻量级锁指针、重量级锁指针、GC标记、偏向锁线程
ID、偏向锁时间戳。
2. 存储类型指针,也就是指向类的元数据的指针,通过这个指针才能确定对象是属于哪个类的实例。
如果是数组的话,则还包含了数组的⻓度(占用4字节)
实例数据: 对象实际数据,对象实际数据包括了对象的所有成员变量,其大小由各个成员变量的大小决定。(这里不包括静态成员变量,因为其是在方法区维护的)
对齐填充:Java 对象占用空间是 8 字节对齐的,即所有 Java 对象占用 bytes 数必须是 8 的倍数,是因为当我们从磁盘中取一个数据时,不会说我想取一个字节就是一个字节,都是按照一块儿一块儿来取的,这一块大小是 8 个字节,所以为了完整,padding 的作用就是补充字节,「保证对象是 8 字节的整数倍」。
那么CAS有什么缺点吗?
CAS的缺点主要有3点:
ABA问题:ABA的问题指的是在CAS更新的过程中,当读取到的值是A,然后准备赋值的时候仍然是A,
但是实际上有可能A的值被改成了B,然后⼜被改回了A,这个CAS更新的漏洞就叫做ABA。只是ABA的问
题⼤部分场景下都不影响并发的最终效果。
Java中有AtomicStampedReference来解决这个问题,他加⼊了预期标志和更新后标志两个字段,更新
时不光检查值,还要检查当前的标志是否等于预期标志,全部相等的话才会更新。
循环时间⻓开销⼤:⾃旋CAS的⽅式如果⻓时间不成功,会给CPU带来很⼤的开销。
只能保证⼀个共享变量的原⼦操作:只对⼀个共享变量操作可以保证原⼦性,但是多个则不⾏,多个可
以通过AtomicReference来处理或者使⽤锁synchronized实现。
ABA问题:ABA的问题指的是在CAS更新的过程中,当读取到的值是A,然后准备赋值的时候仍然是A,
但是实际上有可能A的值被改成了B,然后⼜被改回了A,这个CAS更新的漏洞就叫做ABA。只是ABA的问
题⼤部分场景下都不影响并发的最终效果。
Java中有AtomicStampedReference来解决这个问题,他加⼊了预期标志和更新后标志两个字段,更新
时不光检查值,还要检查当前的标志是否等于预期标志,全部相等的话才会更新。
循环时间⻓开销⼤:⾃旋CAS的⽅式如果⻓时间不成功,会给CPU带来很⼤的开销。
只能保证⼀个共享变量的原⼦操作:只对⼀个共享变量操作可以保证原⼦性,但是多个则不⾏,多个可
以通过AtomicReference来处理或者使⽤锁synchronized实现。
那多线程环境怎么使⽤Map呢?ConcurrentHashmap了
解过吗?
解过吗?
多线程环境可以使⽤Collections.synchronizedMap同步加锁的⽅式,还可以使⽤HashTable,但是同步
的⽅式显然性能不达标,⽽ConurrentHashMap更适合⾼并发场景使⽤。
ConcurrentHashmap在JDK1.7和1.8的版本改动⽐较⼤,1.7使⽤Segment+HashEntry分段锁的⽅式实
现,1.8则抛弃了Segment,改为使⽤CAS+synchronized+Node实现,同样也加⼊了红⿊树,避免链表
过⻓导致性能的问题
的⽅式显然性能不达标,⽽ConurrentHashMap更适合⾼并发场景使⽤。
ConcurrentHashmap在JDK1.7和1.8的版本改动⽐较⼤,1.7使⽤Segment+HashEntry分段锁的⽅式实
现,1.8则抛弃了Segment,改为使⽤CAS+synchronized+Node实现,同样也加⼊了红⿊树,避免链表
过⻓导致性能的问题
1.7分段锁
从结构上说,1.7版本的ConcurrentHashMap采⽤分段锁机制,⾥⾯包含⼀个Segment数组,Segment
继承与ReentrantLock,Segment则包含HashEntry的数组,HashEntry本身就是⼀个链表的结构,具有
保存key、value的能⼒能指向下⼀个节点的指针。
实际上就是相当于每个Segment都是⼀个HashMap,默认的Segment⻓度是16,也就是⽀持16个线程
的并发写,Segment之间相互不会受到影响。
从结构上说,1.7版本的ConcurrentHashMap采⽤分段锁机制,⾥⾯包含⼀个Segment数组,Segment
继承与ReentrantLock,Segment则包含HashEntry的数组,HashEntry本身就是⼀个链表的结构,具有
保存key、value的能⼒能指向下⼀个节点的指针。
实际上就是相当于每个Segment都是⼀个HashMap,默认的Segment⻓度是16,也就是⽀持16个线程
的并发写,Segment之间相互不会受到影响。
1.8CAS+synchronized
1.8抛弃分段锁,转为⽤CAS+synchronized来实现,同样HashEntry改为Node,也加⼊了红⿊树的实
现
1.8抛弃分段锁,转为⽤CAS+synchronized来实现,同样HashEntry改为Node,也加⼊了红⿊树的实
现
put流程
1. ⾸先计算hash,遍历node数组,如果node是空的话,就通过CAS+⾃旋的⽅式初始化
2. 如果当前数组位置是空则直接通过CAS⾃旋写⼊数据
3. 如果hash==MOVED,说明需要扩容,执⾏扩容
4. 如果都不满⾜,就使⽤synchronized写⼊数据,写⼊数据同样判断链表、红⿊树,链表写⼊和
HashMap的⽅式⼀样,key hash⼀样就覆盖,反之就尾插法,链表⻓度超过8就转换成红⿊树
1. ⾸先计算hash,遍历node数组,如果node是空的话,就通过CAS+⾃旋的⽅式初始化
2. 如果当前数组位置是空则直接通过CAS⾃旋写⼊数据
3. 如果hash==MOVED,说明需要扩容,执⾏扩容
4. 如果都不满⾜,就使⽤synchronized写⼊数据,写⼊数据同样判断链表、红⿊树,链表写⼊和
HashMap的⽅式⼀样,key hash⼀样就覆盖,反之就尾插法,链表⻓度超过8就转换成红⿊树
get查询
get很简单,通过key计算hash,如果key hash相同就返回,如果是红⿊树按照红⿊树获取,都不是就遍
历链表获取。
get很简单,通过key计算hash,如果key hash相同就返回,如果是红⿊树按照红⿊树获取,都不是就遍
历链表获取。
那么说说你对JMM内存模型的理解?为什么需要JMM?
JMM 就是 「Java内存模型」(java memory model)。因为在不同的硬件生产商和不同的操作系统下,内存的访问有一定的差异,所以会造成相同的代码运行在不同的系统上会出现各种问题。所以java内存模型(JMM)「屏蔽掉各种硬件和操作系统的内存访问差异,以实现让java程序在各种平台下都能达到一致的并发效果」。
Java内存模型规定所有的变量都存储在主内存中,包括实例变量,静态变量,但是不包括局部变量和方法参数。每个线程都有自己的工作内存,线程的工作内存保存了该线程用到的变量和主内存的副本拷贝,线程对变量的操作都在工作内存中进行。「线程不能直接读写主内存中的变量」。
每个线程的工作内存都是独立的,「线程操作数据只能在工作内存中进行,然后刷回到主存」。这是 Java 内存模型定义的线程基本工作方式。
Java内存模型规定所有的变量都存储在主内存中,包括实例变量,静态变量,但是不包括局部变量和方法参数。每个线程都有自己的工作内存,线程的工作内存保存了该线程用到的变量和主内存的副本拷贝,线程对变量的操作都在工作内存中进行。「线程不能直接读写主内存中的变量」。
每个线程的工作内存都是独立的,「线程操作数据只能在工作内存中进行,然后刷回到主存」。这是 Java 内存模型定义的线程基本工作方式。
原⼦性:Java内存模型通过read、load、assign、use、store、write来保证原⼦性操作,此外还有lock
和unlock,直接对应着synchronized关键字的monitorenter和monitorexit字节码指令。
可⻅性:Java保证可⻅性可以认为通过volatile、synchronized、
final来实现。
有序性:由于处理器和编译器的重排序导致的有序性问题,Java通过volatile、synchronized来保证。
和unlock,直接对应着synchronized关键字的monitorenter和monitorexit字节码指令。
可⻅性:Java保证可⻅性可以认为通过volatile、synchronized、
final来实现。
有序性:由于处理器和编译器的重排序导致的有序性问题,Java通过volatile、synchronized来保证。
happen-before规则
(1)程序顺序规则:一个线程中的每个操作,happens-before于该线程中的任意后续操作。
(2)监视器锁规则:对一个锁的解锁,happens-before于随后对这个锁的加锁。
(3)volatile变量规则:对一个volatile域的写,happens-before于任意后续对这个volatile域的读。
(4)传递性:如果A happens-before B,且B happens-before C,那么A happens-before C。
(5)start()规则:如果线程A执行操作ThreadB.start()(启动线程B),那么A线程的ThreadB.start()操作happens-before于线程B中的任意操作。
(6)Join()规则:如果线程A执行操作ThreadB.join()并成功返回,那么线程B中的任意操作happens-before于线程A从ThreadB.join()操作成功返回。
(7)程序中断规则:对线程interrupted()方法的调用先行于被中断线程的代码检测到中断时间的发生。
(8)对象finalize规则:一个对象的初始化完成(构造函数执行结束)先行于发生它的finalize()方法的开始。
(2)监视器锁规则:对一个锁的解锁,happens-before于随后对这个锁的加锁。
(3)volatile变量规则:对一个volatile域的写,happens-before于任意后续对这个volatile域的读。
(4)传递性:如果A happens-before B,且B happens-before C,那么A happens-before C。
(5)start()规则:如果线程A执行操作ThreadB.start()(启动线程B),那么A线程的ThreadB.start()操作happens-before于线程B中的任意操作。
(6)Join()规则:如果线程A执行操作ThreadB.join()并成功返回,那么线程B中的任意操作happens-before于线程A从ThreadB.join()操作成功返回。
(7)程序中断规则:对线程interrupted()方法的调用先行于被中断线程的代码检测到中断时间的发生。
(8)对象finalize规则:一个对象的初始化完成(构造函数执行结束)先行于发生它的finalize()方法的开始。
说一下 atomic 的原理?
Atomic包中的类基本的特性就是在多线程环境下,当有多个线程同时对单个(包括基本类型及引用类型)变量进行操作时,具有排他性,即当多个线程同时对该变量的值进行更新时,仅有一个线程能成功,而未成功的线程可以向自旋锁一样,继续尝试,一直等到执行成功。
Atomic系列的类中的核心方法都会调用unsafe类中的几个本地方法。我们需要先知道一个东西就是Unsafe类,全名为:sun.misc.Unsafe,这个类包含了大量的对C代码的操作,包括很多直接内存分配以及原子操作的调用,而它之所以标记为非安全的,是告诉你这个里面大量的方法调用都会存在安全隐患,需要小心使用,否则会导致严重的后果,例如在通过unsafe分配内存的时候,如果自己指定某些区域可能会导致一些类似C++一样的指针越界到其他进程的问题。
Java基础
八种基本数据类型的大小,以及他们的封装类?
1.int是基本数据类型,Integer是int的封装类,是引用类型。int默认值是0,而Integer默认值
是null,所以Integer能区分出0和null的情况。一旦java看到null,就知道这个引用还没有指向某个
对象,再任何引用使用前,必须为其指定一个对象,否则会报错。
是null,所以Integer能区分出0和null的情况。一旦java看到null,就知道这个引用还没有指向某个
对象,再任何引用使用前,必须为其指定一个对象,否则会报错。
2.基本数据类型在声明时系统会自动给它分配空间,而引用类型声明时只是分配了引用空间,
必须通过实例化开辟数据空间之后才可以赋值。数组对象也是一个引用对象,将一个数组赋值给另
一个数组时只是复制了一个引用,所以通过某一个数组所做的修改在另一个数组中也看的见。
必须通过实例化开辟数据空间之后才可以赋值。数组对象也是一个引用对象,将一个数组赋值给另
一个数组时只是复制了一个引用,所以通过某一个数组所做的修改在另一个数组中也看的见。
虽然定义了boolean这种数据类型,但是只对它提供了非常有限的支持。在Java虚拟机中没有
任何供boolean值专用的字节码指令,Java语言表达式所操作的boolean值,在编译之后都使用Java
虚拟机中的int数据类型来代替,而boolean数组将会被编码成Java虚拟机的byte数组,每个元素
boolean元素占8位。这样我们可以得出boolean类型占了单独使用是4个字节,在数组中又是1个字
节。使用int的原因是,对于当下32位的处理器(CPU)来说,一次处理数据是32位(这里不是指的
是32/64位系统,而是指CPU硬件层面),具有高效存取的特点。
任何供boolean值专用的字节码指令,Java语言表达式所操作的boolean值,在编译之后都使用Java
虚拟机中的int数据类型来代替,而boolean数组将会被编码成Java虚拟机的byte数组,每个元素
boolean元素占8位。这样我们可以得出boolean类型占了单独使用是4个字节,在数组中又是1个字
节。使用int的原因是,对于当下32位的处理器(CPU)来说,一次处理数据是32位(这里不是指的
是32/64位系统,而是指CPU硬件层面),具有高效存取的特点。
String中abc怎么变成cba?
可以使用StringBuffer调用reverse方法
String它是final修饰怎么进行a+b?
当我们使用“+”拼接字符串的时候,实际上是创建了一个StringBuilder对象,并使用该对象的append方法进行字符串拼接,然后将得到的字符串通过toString方法返回给字符串对象。
final有哪些用法?
1.被final修饰的类不能被继承
2.被final修饰的方法不能被重写
3.被final修饰的变量不能被更改,引用类型是引用不能改
4.被final修饰的常量会在编译阶段放入常量池
2.被final修饰的方法不能被重写
3.被final修饰的变量不能被更改,引用类型是引用不能改
4.被final修饰的常量会在编译阶段放入常量池
static都有哪些用法?
1.静态变量
2.静态方法
3.静态代码块
4.静态内部类
5.静态导包
2.静态方法
3.静态代码块
4.静态内部类
5.静态导包
普通内部类和静态内部类的区别?
1、普通内部类和静态内部类的区别:
a)普通内部类持有对外部类的引用,静态内部类没有持有外部类的引用。
b)普通内部类能够访问外部类的静态和非静态成员,静态内部类不能访问外部类的非静态成员,他只能访问外部类的静态成员。
c)一个普通内部类不能脱离外部类实体被创建,且可以访问外部类的数据和方法,因为他就在外部类里面。
2、区别还有
a)第一,内部类可以访问其所在类的属性(包括所在类的私有属性),内部类创建自身对象需要先创建其所在类的对象
b)第二,可以定义内部接口,且可以定义另外一个内部类实现这个内部接口
c)第三,可以在方法体内定义一个内部类,方法体内的内部类可以完成一个基于虚方法形式的回调操作
d)第四,内部类不能定义static元素
e)第五,内部类可以多嵌套
f)static内部类是内部类中一个比较特殊的情况,Java文档中是这样描述static内部类的:一旦内部类使用static修饰,那么此时这个内部类就升级为顶类。也就是说,除了写在一个类的内部以外,static内部类具备所有外部类的特性(和外部类完全一样)。
a)普通内部类持有对外部类的引用,静态内部类没有持有外部类的引用。
b)普通内部类能够访问外部类的静态和非静态成员,静态内部类不能访问外部类的非静态成员,他只能访问外部类的静态成员。
c)一个普通内部类不能脱离外部类实体被创建,且可以访问外部类的数据和方法,因为他就在外部类里面。
2、区别还有
a)第一,内部类可以访问其所在类的属性(包括所在类的私有属性),内部类创建自身对象需要先创建其所在类的对象
b)第二,可以定义内部接口,且可以定义另外一个内部类实现这个内部接口
c)第三,可以在方法体内定义一个内部类,方法体内的内部类可以完成一个基于虚方法形式的回调操作
d)第四,内部类不能定义static元素
e)第五,内部类可以多嵌套
f)static内部类是内部类中一个比较特殊的情况,Java文档中是这样描述static内部类的:一旦内部类使用static修饰,那么此时这个内部类就升级为顶类。也就是说,除了写在一个类的内部以外,static内部类具备所有外部类的特性(和外部类完全一样)。
如何将字符串反转?
使用 StringBuilder 或者 stringBuffer 的 reverse() 方法。
String 类的常用方法都有那些?
indexOf():返回指定字符的索引。
charAt():返回指定索引处的字符。
replace():字符串替换。
trim():去除字符串两端空白。
split():分割字符串,返回一个分割后的字符串数组。
getBytes():返回字符串的 byte 类型数组。
length():返回字符串长度。
toLowerCase():将字符串转成小写字母。
toUpperCase():将字符串转成大写字符。
substring():截取字符串。
equals():字符串比较。
charAt():返回指定索引处的字符。
replace():字符串替换。
trim():去除字符串两端空白。
split():分割字符串,返回一个分割后的字符串数组。
getBytes():返回字符串的 byte 类型数组。
length():返回字符串长度。
toLowerCase():将字符串转成小写字母。
toUpperCase():将字符串转成大写字符。
substring():截取字符串。
equals():字符串比较。
String、Stringuffer、StringBuilder三者之间的区别?
String类中使用Final关键字修饰,对象是不可变的,线程安全的
StringBuffer对方法加了同步锁或者对调用的方法加了同步锁,所以是线程安全的
StringBuilder并没有对方法进行加同步锁,所以是非线程安全的
StringBuffer对方法加了同步锁或者对调用的方法加了同步锁,所以是线程安全的
StringBuilder并没有对方法进行加同步锁,所以是非线程安全的
接口和抽象类有什么区别?
1.接口是抽象类的变体,「接口中所有的方法都是抽象的」。而抽象类是声明方法的存在而不去实现它的类。
2.接口可以多继承,抽象类只能单继承。
3.接口中方法不能实现,默认是 「public abstract」,而抽象类可以实现部分方法。
4.接口中基本数据类型为 「public static final」 并且需要给出初始值,而抽类象不用。
2.接口可以多继承,抽象类只能单继承。
3.接口中方法不能实现,默认是 「public abstract」,而抽象类可以实现部分方法。
4.接口中基本数据类型为 「public static final」 并且需要给出初始值,而抽类象不用。
重载和重写什么区别?
重写:
发生在父子类中
1.参数列表必须「完全与被重写的方法」相同,否则不能称其为重写而是重载.
2.「返回的类型必须一致与被重写的方法的返回类型相同」,否则不能称其为重写而是重载。
3.访问「修饰符的限制一定要大于被重写方法的访问修饰符」
4.重写方法一定「不能抛出新的检查异常或者比被重写方法申明更加宽泛的检查型异常」。
发生在父子类中
1.参数列表必须「完全与被重写的方法」相同,否则不能称其为重写而是重载.
2.「返回的类型必须一致与被重写的方法的返回类型相同」,否则不能称其为重写而是重载。
3.访问「修饰符的限制一定要大于被重写方法的访问修饰符」
4.重写方法一定「不能抛出新的检查异常或者比被重写方法申明更加宽泛的检查型异常」。
重载:
发生在同一类中
1.必须具有「不同的参数列表」;
2.可以有不同的返回类型,只要参数列表不同就可以了;
3.可以有「不同的访问修饰符」;
4.可以抛出「不同的异常」;
发生在同一类中
1.必须具有「不同的参数列表」;
2.可以有不同的返回类型,只要参数列表不同就可以了;
3.可以有「不同的访问修饰符」;
4.可以抛出「不同的异常」;
BIO、NIO、AIO 有什么区别?
BIO:Block IO 同步阻塞式 IO,就是我们平常使用的传统 IO,它的特点是模式简单使用方便,并发处理能力低。
NIO:New IO 同步非阻塞 IO,是传统 IO 的升级,客户端和服务器端通过 Channel(通道)通讯,实现了多路复用。
AIO:Asynchronous IO 是 NIO 的升级,也叫 NIO2,实现了异步非堵塞 IO ,异步 IO 的操作基于事件和回调机制。
NIO:New IO 同步非阻塞 IO,是传统 IO 的升级,客户端和服务器端通过 Channel(通道)通讯,实现了多路复用。
AIO:Asynchronous IO 是 NIO 的升级,也叫 NIO2,实现了异步非堵塞 IO ,异步 IO 的操作基于事件和回调机制。
java中有哪些集合?
Collection
Collection是单列集合包含List和Set
List
List包含ArrayList、LinkList和Vector
Set
Set包含HashSet、LinkedHashSet和TreeSet
Map
Map包含HashTable、HashMap、LinkedHashMap、WeakHashMap、TreeMap和IdentifyHashMap
Collection是单列集合包含List和Set
List
List包含ArrayList、LinkList和Vector
Set
Set包含HashSet、LinkedHashSet和TreeSet
Map
Map包含HashTable、HashMap、LinkedHashMap、WeakHashMap、TreeMap和IdentifyHashMap
HashMap 和 Hashtable 有什么区别?
HashMap是非线程安全的,HashTable是线程安全的
HashMap的键和值都允许有null值存在,HashTable不允许
HashMap效率比HashTable高
Hashtable是同步的,HashMap不同步
HashMap的键和值都允许有null值存在,HashTable不允许
HashMap效率比HashTable高
Hashtable是同步的,HashMap不同步
list、set和Map的区别?
list:有序,可重复
set:无序,不可重复
Map:无序,它的键不允许重复,它的值允许重复
set:无序,不可重复
Map:无序,它的键不允许重复,它的值允许重复
List,set和Map的实现类
ArrayList:
那么 hashMap 线程不安全怎么解决?
一.给 hashMap 「直接加锁」,来保证线程安全
二.使用 「hashTable」,比方法一效率高,其实就是在其方法上加了 synchronized 锁
三.使用 「concurrentHashMap」 , 不管是其 1.7 还是 1.8 版本,本质都是「减小了锁的粒度,减少线程竞争」来保证高效.
二.使用 「hashTable」,比方法一效率高,其实就是在其方法上加了 synchronized 锁
三.使用 「concurrentHashMap」 , 不管是其 1.7 还是 1.8 版本,本质都是「减小了锁的粒度,减少线程竞争」来保证高效.
hashMap 线程不安全体现在哪里?
在 「hashMap1.7 中扩容」的时候,因为采用的是头插法,所以会可能会有循环链表产生,导致数据有问题,在 1.8 版本已修复,改为了尾插法
在任意版本的 hashMap 中,如果在「插入数据时多个线程命中了同一个槽」,可能会有数据覆盖的情况发生,导致线程不安全。
在任意版本的 hashMap 中,如果在「插入数据时多个线程命中了同一个槽」,可能会有数据覆盖的情况发生,导致线程不安全。
HashMap底层原理?
JDK1.8之前数组+链表,JDK1.8之后是数组+链表+红黑树,当链表中的元素超过了8个以后,就会将链表转为红黑树,当红黑数节点小于等于6个的时候又会退化成链表结构
首次创建HashMap的长度为16,HashMap中负载因子大小为0.75,所以HashMap中元素个数达到12个时它就会进行扩容,扩容为它的两倍。
首次创建HashMap的长度为16,HashMap中负载因子大小为0.75,所以HashMap中元素个数达到12个时它就会进行扩容,扩容为它的两倍。
说一下HashMap的put方法?
1. 根据Key通过哈希算法与与运算得出数组下标
2. 如果数组下标位置元素为空,则将key和value封装为Entry对象(JDK1.7中是Entry对象,JDK1.8中是 Node对象)并放⼊该位置
3. 如果数组下标位置元素不为空,则要分情况讨论
--a. 如果是JDK1.7,则先判断是否需要扩容,如果要扩容就进⾏扩容,如果不⽤扩容就⽣成Entry对 象,并使⽤头插法添加到当前位置的链表中
--b. 如果是JDK1.8,则会先判断当前位置上的Node的类型,看是红⿊树Node,还是链表Node
---i. 如果是红⿊树Node,则将key和value封装为⼀个红⿊树节点并添加到红⿊树中去,在这个过 程中会判断红⿊树中是否存在当前key,如果存在则更新value
---ii. 如果此位置上的Node对象是链表节点,则将key和value封装为⼀个链表Node并通过尾插法插 ⼊到链表的最后位置去,因为是尾插法,所以需要遍历链表,在遍历链表的过程中会判断是否
2. 如果数组下标位置元素为空,则将key和value封装为Entry对象(JDK1.7中是Entry对象,JDK1.8中是 Node对象)并放⼊该位置
3. 如果数组下标位置元素不为空,则要分情况讨论
--a. 如果是JDK1.7,则先判断是否需要扩容,如果要扩容就进⾏扩容,如果不⽤扩容就⽣成Entry对 象,并使⽤头插法添加到当前位置的链表中
--b. 如果是JDK1.8,则会先判断当前位置上的Node的类型,看是红⿊树Node,还是链表Node
---i. 如果是红⿊树Node,则将key和value封装为⼀个红⿊树节点并添加到红⿊树中去,在这个过 程中会判断红⿊树中是否存在当前key,如果存在则更新value
---ii. 如果此位置上的Node对象是链表节点,则将key和value封装为⼀个链表Node并通过尾插法插 ⼊到链表的最后位置去,因为是尾插法,所以需要遍历链表,在遍历链表的过程中会判断是否
HashMap的get方法?
1.根据key通过HashCode方法得出哈希值,并通过哈希算法转换成数组的下标
2.判断该数组下标是否有数据,什么都没有就返回null,有它就会拿着参数key和单向列表上的每一个节点的Key进行equals比较,如果为false就返回null,如果其中一个节点返回true,就把该value进行返回。
2.判断该数组下标是否有数据,什么都没有就返回null,有它就会拿着参数key和单向列表上的每一个节点的Key进行equals比较,如果为false就返回null,如果其中一个节点返回true,就把该value进行返回。
ArrayList 和 LinkedList 的区别?
ArrayList底层是数组结构,LinkedList底层是链表结构
ArrayList查询快,增删慢,LinkedList查询慢,增删快
ArrayList查询快,增删慢,LinkedList查询慢,增删快
ArrayList 和 Vector 的区别是什么?
Vector是同步的,而ArrayList不同步。
ArrayList比Vector快,它因为有同步,不会过载。
Vector扩容是2倍,ArrayList扩容是1.5倍
ArrayList比Vector快,它因为有同步,不会过载。
Vector扩容是2倍,ArrayList扩容是1.5倍
hashMap 1.7 和 hashMap 1.8 的区别?
ArrayList默认长度?
ArrayList默认大小是10个元素
HashMap默认长度?
HashMap默认大小是16个元素
== 和 equals 的区别是什么?
==比较基本类型:比较的是值,比较引用类型:比较的就是地址值
equals比较引用类型比较的是地址值,方法被重写后比较的是类容
equals比较引用类型比较的是地址值,方法被重写后比较的是类容
ArrayList底层是怎么进行扩容的?
如果创建一个list没有指定元素大小,会在add元素的时候会通过ensureCapacityInternal()方法确认大小,一般是指定为10的大小。然后再通过ensureExplicitCapacity()方法确认list的显式大小。如果需要的最小容量大于list集合的大小时,会调用grow()再进行扩容,新的数组大小是原来数组的1.5倍。如果扩容后的新数组大小小于最小容量大小,则将数组的大小设定为最小容量大小。如果扩容后的新数组容量大小大于数组最大容量大小Integer.max.array.value,则调用hugeCapacity方法(),判断所需最小容量大小是否大于Integer.max.array.value。大于返回最大容量大小Integer.max.value。否则将容量大小设置为Integer.max.array.value.
有哪些数据结构?
数组、链表、栈、队列、树、堆、图、哈希表
说说进程和线程的区别?
进程是程序的⼀次执⾏,是系统进⾏资源分配和调度的独⽴单位,他的作⽤是是程序能够并发执⾏提⾼
资源利⽤率和吞吐率。
由于进程是资源分配和调度的基本单位,因为进程的创建、销毁、切换产⽣⼤量的时间和空间的开销,
进程的数量不能太多,⽽线程是⽐进程更⼩的能独⽴运⾏的基本单位,他是进程的⼀个实体,可以减少
程序并发执⾏时的时间和空间开销,使得操作系统具有更好的并发性。
线程基本不拥有系统资源,只有⼀些运⾏时必不可少的资源,⽐如程序计数器、寄存器和栈,进程则占
有堆、栈。
资源利⽤率和吞吐率。
由于进程是资源分配和调度的基本单位,因为进程的创建、销毁、切换产⽣⼤量的时间和空间的开销,
进程的数量不能太多,⽽线程是⽐进程更⼩的能独⽴运⾏的基本单位,他是进程的⼀个实体,可以减少
程序并发执⾏时的时间和空间开销,使得操作系统具有更好的并发性。
线程基本不拥有系统资源,只有⼀些运⾏时必不可少的资源,⽐如程序计数器、寄存器和栈,进程则占
有堆、栈。
反射的作用和原理?
反射机制是在运行时,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意个对象,
都能够调用它的任意一个方法。在java中,只要给定类的名字,就可以通过反射机制来获得类的所
有信息。
都能够调用它的任意一个方法。在java中,只要给定类的名字,就可以通过反射机制来获得类的所
有信息。
反射的实现方式:
第一步:获取Class对象,有4中方法: 1)Class.forName(“类的路径”); 2)类名.class 3)对象
名.getClass() 4)基本类型的包装类,可以调用包装类的Type属性来获得该包装类的Class对象
第一步:获取Class对象,有4中方法: 1)Class.forName(“类的路径”); 2)类名.class 3)对象
名.getClass() 4)基本类型的包装类,可以调用包装类的Type属性来获得该包装类的Class对象
反射机制的优缺点:
优点: 1)能够运行时动态获取类的实例,提高灵活性; 2)与动态编译结合 缺点: 1)使用反射
性能较低,需要解析字节码,将内存中的对象进行解析。 解决方案: 1、通过setAccessible(true)
关闭JDK的安全检查来提升反射速度; 2、多次创建一个类的实例时,有缓存会快很多 3、
ReflectASM工具类,通过字节码生成的方式加快反射速度 2)相对不安全,破坏了封装性(因为通
过反射可以获得私有方法和属性)
优点: 1)能够运行时动态获取类的实例,提高灵活性; 2)与动态编译结合 缺点: 1)使用反射
性能较低,需要解析字节码,将内存中的对象进行解析。 解决方案: 1、通过setAccessible(true)
关闭JDK的安全检查来提升反射速度; 2、多次创建一个类的实例时,有缓存会快很多 3、
ReflectASM工具类,通过字节码生成的方式加快反射速度 2)相对不安全,破坏了封装性(因为通
过反射可以获得私有方法和属性)
接口的幂等性?
token机制
1、服务端提供了发送token的接口。我们在分析业务的时候,哪些业务是存在幂等问题的,就必须在执行业务前,先去获取token,服务器会把token保存到redis中。
2、然后调用业务接口请求时,把token携带过去,一般放在请求头部。
3、服务器判断token是否存在redis中,存在表示第一次请求,然后删除token,继续执行业务。
4、如果判断token不存在redis中,就表示是重复操作,直接返回重复标记给client,这样就保证了业务代码,不被重复执行。
关键点 先删除token,还是后删除token。
后删除token:如果进行业务处理成功后,删除redis中的token失败了,这样就导致了有可能会发生重复请求,因为token没有被删除。这个问题其实是数据库和缓存redis数据不一致问题,后续会写文章进行讲解。
先删除token:如果系统出现问题导致业务处理出现异常,业务处理没有成功,接口调用方也没有获取到明确的结果,然后进行重试,但token已经删除掉了,服务端判断token不存在,认为是重复请求,就直接返回了,无法进行业务处理了。
先删除token可以保证不会因为重复请求,业务数据出现问题。出现业务异常,可以让调用方配合处理一下,重新获取新的token,再次由业务调用方发起重试请求就ok了。
token机制缺点
业务请求每次请求,都会有额外的请求(一次获取token请求、判断token是否存在的业务)。其实真实的生产环境中,1万请求也许只会存在10个左右的请求会发生重试,为了这10个请求,我们让9990个请求都发生了额外的请求。
乐观锁机制
这种方法适合在更新的场景中,update t_goods set count = count -1 , version = version + 1 where good_id=2 and version = 1
根据version版本,也就是在操作库存前先获取当前商品的version版本号,然后操作的时候带上此version号。我们梳理下,我们第一次操作库存时,得到version为1,调用库存服务version变成了2;但返回给订单服务出现了问题,订单服务又一次发起调用库存服务,当订单服务传如的version还是1,再执行上面的sql语句时,就不会执行;因为version已经变为2了,where条件就不成立。这样就保证了不管调用几次,只会真正的处理一次。
乐观锁主要使用于处理读多写少的问题
唯一主键
这个机制是利用了数据库的主键唯一约束的特性,解决了在insert场景时幂等问题。但主键的要求不是自增的主键,这样就需要业务生成全局唯一的主键。
如果是分库分表场景下,路由规则要保证相同请求下,落地在同一个数据库和同一表中,要不然数据库主键约束就不起效果了,因为是不同的数据库和表主键不相关。
防重表
使用订单号orderNo做为去重表的唯一索引,把唯一索引插入去重表,再进行业务操作,且他们在同一个事务中。这个保证了重复请求时,因为去重表有唯一约束,导致请求失败,避免了幂等问题。这里要注意的是,去重表和业务表应该在同一库中,这样就保证了在同一个事务,即使业务操作失败了,也会把去重表的数据回滚。这个很好的保证了数据一致性。
唯一ID
调用接口时,生成一个唯一id,redis将数据保存到集合中(去重),存在即处理过。
1、服务端提供了发送token的接口。我们在分析业务的时候,哪些业务是存在幂等问题的,就必须在执行业务前,先去获取token,服务器会把token保存到redis中。
2、然后调用业务接口请求时,把token携带过去,一般放在请求头部。
3、服务器判断token是否存在redis中,存在表示第一次请求,然后删除token,继续执行业务。
4、如果判断token不存在redis中,就表示是重复操作,直接返回重复标记给client,这样就保证了业务代码,不被重复执行。
关键点 先删除token,还是后删除token。
后删除token:如果进行业务处理成功后,删除redis中的token失败了,这样就导致了有可能会发生重复请求,因为token没有被删除。这个问题其实是数据库和缓存redis数据不一致问题,后续会写文章进行讲解。
先删除token:如果系统出现问题导致业务处理出现异常,业务处理没有成功,接口调用方也没有获取到明确的结果,然后进行重试,但token已经删除掉了,服务端判断token不存在,认为是重复请求,就直接返回了,无法进行业务处理了。
先删除token可以保证不会因为重复请求,业务数据出现问题。出现业务异常,可以让调用方配合处理一下,重新获取新的token,再次由业务调用方发起重试请求就ok了。
token机制缺点
业务请求每次请求,都会有额外的请求(一次获取token请求、判断token是否存在的业务)。其实真实的生产环境中,1万请求也许只会存在10个左右的请求会发生重试,为了这10个请求,我们让9990个请求都发生了额外的请求。
乐观锁机制
这种方法适合在更新的场景中,update t_goods set count = count -1 , version = version + 1 where good_id=2 and version = 1
根据version版本,也就是在操作库存前先获取当前商品的version版本号,然后操作的时候带上此version号。我们梳理下,我们第一次操作库存时,得到version为1,调用库存服务version变成了2;但返回给订单服务出现了问题,订单服务又一次发起调用库存服务,当订单服务传如的version还是1,再执行上面的sql语句时,就不会执行;因为version已经变为2了,where条件就不成立。这样就保证了不管调用几次,只会真正的处理一次。
乐观锁主要使用于处理读多写少的问题
唯一主键
这个机制是利用了数据库的主键唯一约束的特性,解决了在insert场景时幂等问题。但主键的要求不是自增的主键,这样就需要业务生成全局唯一的主键。
如果是分库分表场景下,路由规则要保证相同请求下,落地在同一个数据库和同一表中,要不然数据库主键约束就不起效果了,因为是不同的数据库和表主键不相关。
防重表
使用订单号orderNo做为去重表的唯一索引,把唯一索引插入去重表,再进行业务操作,且他们在同一个事务中。这个保证了重复请求时,因为去重表有唯一约束,导致请求失败,避免了幂等问题。这里要注意的是,去重表和业务表应该在同一库中,这样就保证了在同一个事务,即使业务操作失败了,也会把去重表的数据回滚。这个很好的保证了数据一致性。
唯一ID
调用接口时,生成一个唯一id,redis将数据保存到集合中(去重),存在即处理过。
介绍一下 java 吧
java 是一门开源的跨平台的面向对象的计算机语言.
跨平台是因为 java 的 class 文件是运行在虚拟机上的,其跨平台是指虚拟机在不同平台有不同版本」,所以说 java 是跨平台的.
面向对象有几个特点:
1.「封装」
两层含义:一层含义是把对象的属性和行为看成一个密不可分的整体,将这两者'封装'在一个不可分割的「独立单元」(即对象)中
另一层含义指'信息隐藏,把不需要让外界知道的信息隐藏起来,有些对象的属性及行为允许外界用户知道或使用,但不允许更改,而另一些属性或行为,则不允许外界知晓,或只允许使用对象的功能,而尽可能「隐藏对象的功能实现细节」。
「优点」:
1.良好的封装能够「减少耦合」,符合程序设计追求'高内聚,低耦合'
2.「类内部的结构可以自由修改」
3.可以对成员变量进行更「精确的控制」
4.「隐藏信息」实现细节
两层含义:一层含义是把对象的属性和行为看成一个密不可分的整体,将这两者'封装'在一个不可分割的「独立单元」(即对象)中
另一层含义指'信息隐藏,把不需要让外界知道的信息隐藏起来,有些对象的属性及行为允许外界用户知道或使用,但不允许更改,而另一些属性或行为,则不允许外界知晓,或只允许使用对象的功能,而尽可能「隐藏对象的功能实现细节」。
「优点」:
1.良好的封装能够「减少耦合」,符合程序设计追求'高内聚,低耦合'
2.「类内部的结构可以自由修改」
3.可以对成员变量进行更「精确的控制」
4.「隐藏信息」实现细节
2.「继承」
继承就是子类继承父类的特征和行为,使得子类对象(实例)具有父类的实例域和方法,或子类从父类继承方法,使得子类具有父类相同的行为。
「优点」:
1.提高类代码的「复用性」
2.提高了代码的「维护性」
继承就是子类继承父类的特征和行为,使得子类对象(实例)具有父类的实例域和方法,或子类从父类继承方法,使得子类具有父类相同的行为。
「优点」:
1.提高类代码的「复用性」
2.提高了代码的「维护性」
3.「多态」
1.「方法重载」:在一个类中,允许多个方法使用同一个名字,但方法的参数不同,完成的功能也不同。
2.「对象多态」:子类对象可以与父类对象进行转换,而且根据其使用的子类不同完成的功能也不同(重写父类的方法)。
多态是同一个行为具有多个不同表现形式或形态的能力。Java语言中含有方法重载与对象多态两种形式的多态:
「优点」
「消除类型之间的耦合关系」
「可替换性」
「可扩充性」
「接口性」
「灵活性」
「简化性」
1.「方法重载」:在一个类中,允许多个方法使用同一个名字,但方法的参数不同,完成的功能也不同。
2.「对象多态」:子类对象可以与父类对象进行转换,而且根据其使用的子类不同完成的功能也不同(重写父类的方法)。
多态是同一个行为具有多个不同表现形式或形态的能力。Java语言中含有方法重载与对象多态两种形式的多态:
「优点」
「消除类型之间的耦合关系」
「可替换性」
「可扩充性」
「接口性」
「灵活性」
「简化性」
面向对象和面向过程的区别?
面向过程:是分析解决问题的步骤,然后用函数把这些步骤一步一步地实现,然后在使用的时候一
一调用则可。性能较高,所以单片机、嵌入式开发等一般采用面向过程开发
一调用则可。性能较高,所以单片机、嵌入式开发等一般采用面向过程开发
面向对象:是把构成问题的事务分解成各个对象,而建立对象的目的也不是为了完成一个个步骤,
而是为了描述某个事物在解决整个问题的过程中所发生的行为。面向对象有封装、继承、多态的特
性,所以易维护、易复用、易扩展。可以设计出低耦合的系统。 但是性能上来说,比面向过程要
低。
而是为了描述某个事物在解决整个问题的过程中所发生的行为。面向对象有封装、继承、多态的特
性,所以易维护、易复用、易扩展。可以设计出低耦合的系统。 但是性能上来说,比面向过程要
低。
JDK 和 JRE 有什么区别?
JDK:Java Development Kit 的简称,java 开发工具包,提供了 java 的开发环境和运行环境。
JRE:Java Runtime Environment 的简称,java 运行环境,为 java 的运行提供了所需环境。
具体来说 JDK 其实包含了 JRE,同时还包含了编译 java 源码的编译器 javac,还包含了很多 java 程序调试和分析的工具。简单来说:如果你需要运行
java 程序,只需安装 JRE 就可以了,如果你需要编写 java 程序,需要安装 JDK。
java 程序,只需安装 JRE 就可以了,如果你需要编写 java 程序,需要安装 JDK。
什么是包装类?为什么需要包装类?
「Java 中有 8 个基本类型,分别对应的 8 个包装类」
byte -- Byte
boolean -- Boolean
short -- Short
char -- Character
int -- Integer
long -- Long
float -- Float
double -- Double
byte -- Byte
boolean -- Boolean
short -- Short
char -- Character
int -- Integer
long -- Long
float -- Float
double -- Double
「为什么需要包装类」:
基本数据类型方便、简单、高效,但泛型不支持、集合元素不支持
不符合面向对象思维
包装类提供很多方法,方便使用,如 Integer 类 toHexString(int i)、parseInt(String s) 方法等等
基本数据类型方便、简单、高效,但泛型不支持、集合元素不支持
不符合面向对象思维
包装类提供很多方法,方便使用,如 Integer 类 toHexString(int i)、parseInt(String s) 方法等等
Integer a = 1000,Integer b = 1000,a==b 的结果是什么?那如果 a,b 都为1,结果又是什么?
Integer a = 1000,Integer b = 1000,a==b 结果为「false」
Integer a = 1,Integer b = 1,a==b 结果为「true」
这道题主要考察 Integer 包装类缓存的范围,「在-128~127之间会缓存起来」,比较的是直接缓存的数据,在此之外比较的是对象
Integer a = 1,Integer b = 1,a==b 结果为「true」
这道题主要考察 Integer 包装类缓存的范围,「在-128~127之间会缓存起来」,比较的是直接缓存的数据,在此之外比较的是对象
3*0.1 == 0.3返回值是什么?
false,因为有些浮点数不能完全精确的表示出来.
a=a+b与a+=b有什么区别吗?
+= 操作符会进行隐式自动类型转换,此处a+=b隐式的将加操作的结果类型强制转换为持有结果的类
型,而a=a+b则不会自动进行类型转换
型,而a=a+b则不会自动进行类型转换
标识符的命名规则?
标识符的含义: 是指在程序中,我们自己定义的内容,譬如,类的名字,方法名称以及变量名称等
等,都是标识符。
命名规则:(硬性要求) 标识符可以包含英文字母,0-9的数字,$以及_ 标识符不能以数字开头 标
识符不是关键字
命名规范:(非硬性要求) 类名规范:首字符大写,后面每个单词首字母大写(大驼峰式)。 变量
名规范:首字母小写,后面每个单词首字母大写(小驼峰式)。 方法名规范:同变量名。
等,都是标识符。
命名规则:(硬性要求) 标识符可以包含英文字母,0-9的数字,$以及_ 标识符不能以数字开头 标
识符不是关键字
命名规范:(非硬性要求) 类名规范:首字符大写,后面每个单词首字母大写(大驼峰式)。 变量
名规范:首字母小写,后面每个单词首字母大写(小驼峰式)。 方法名规范:同变量名。
Java自动装箱与拆箱
装箱就是自动将基本数据类型转换为包装器类型(int-->Integer);调用方法:Integer的
valueOf(int) 方法
拆箱就是自动将包装器类型转换为基本数据类型(Integer-->int)。调用方法:Integer的
intValue方法
valueOf(int) 方法
拆箱就是自动将包装器类型转换为基本数据类型(Integer-->int)。调用方法:Integer的
intValue方法
try catch finally,try里有return,finally还执行么?
1、不管有木有出现异常,finally块中代码都会执行;
2、当try和catch中有return时,finally仍然会执行;
3、finally是在return后面的表达式运算后执行的(此时并没有返回运算后的值,而是先把要返回的
值保存起来,管finally中的代码怎么样,返回的值都不会改变,任然是之前保存的值),所以函数
返回值是在finally执行前确定的;
4、finally中最好不要包含return,否则程序会提前退出,返回值不是try或catch中保存的返回值。
2、当try和catch中有return时,finally仍然会执行;
3、finally是在return后面的表达式运算后执行的(此时并没有返回运算后的值,而是先把要返回的
值保存起来,管finally中的代码怎么样,返回的值都不会改变,任然是之前保存的值),所以函数
返回值是在finally执行前确定的;
4、finally中最好不要包含return,否则程序会提前退出,返回值不是try或catch中保存的返回值。
hashcode的作用?
java的集合有两类,一类是List,还有一类是Set。前者有序可重复,后者无序不重复。当我们在set
中插入的时候怎么判断是否已经存在该元素呢,可以通过equals方法。但是如果元素太多,用这样
的方法就会比较满。
于是有人发明了哈希算法来提高集合中查找元素的效率。 这种方式将集合分成若干个存储区域,每
个对象可以计算出一个哈希码,可以将哈希码分组,每组分别对应某个存储区域,根据一个对象的
哈希码就可以确定该对象应该存储的那个区域。
hashCode方法可以这样理解:它返回的就是根据对象的内存地址换算出的一个值。这样一来,当
集合要添加新的元素时,先调用这个元素的hashCode方法,就一下子能定位到它应该放置的物理
位置上。如果这个位置上没有元素,它就可以直接存储在这个位置上,不用再进行任何比较了;如
果这个位置上已经有元素了,就调用它的equals方法与新元素进行比较,相同的话就不存了,不相
同就散列其它的地址。这样一来实际调用equals方法的次数就大大降低了,几乎只需要一两次。
中插入的时候怎么判断是否已经存在该元素呢,可以通过equals方法。但是如果元素太多,用这样
的方法就会比较满。
于是有人发明了哈希算法来提高集合中查找元素的效率。 这种方式将集合分成若干个存储区域,每
个对象可以计算出一个哈希码,可以将哈希码分组,每组分别对应某个存储区域,根据一个对象的
哈希码就可以确定该对象应该存储的那个区域。
hashCode方法可以这样理解:它返回的就是根据对象的内存地址换算出的一个值。这样一来,当
集合要添加新的元素时,先调用这个元素的hashCode方法,就一下子能定位到它应该放置的物理
位置上。如果这个位置上没有元素,它就可以直接存储在这个位置上,不用再进行任何比较了;如
果这个位置上已经有元素了,就调用它的equals方法与新元素进行比较,相同的话就不存了,不相
同就散列其它的地址。这样一来实际调用equals方法的次数就大大降低了,几乎只需要一两次。
为什么重写equals方法也要重写hashcode方法
java中操作字符串都有哪些类,他们的区别?
String,StringBuffer,StringBuilder
String 和 StringBuffer、StringBuilder 的区别在于 String 声明的是不可变的对象,每次操作都会生成新的 String 对象,然后将指针指向新的 String 对象,而 StringBuffer、StringBuilder 可以在原有对象的基础上进行操作,所以在经常改变字符串内容的情况下最好不要使用 String。
StringBuffer 和 StringBuilder 最大的区别在于,StringBuffer 是线程安全的,而 StringBuilder 是非线程安全的,但 StringBuilder 的性能却高于 StringBuffer,所以在单线程环境下推荐使用 StringBuilder,多线程环境下推荐使用 StringBuffer。
String str="i"与 String str=new String("i")一样吗?
不一样,因为内存的分配方式不一样。String str="i"的方式,java 虚拟机会将其分配到常量池中;而 String str=new String("i") 则会被分到堆内存中。
java 有哪些数据类型?
1.「基本数据类型」
byte,short,int,long属于数值型中的整数型
float,double属于数值型中的浮点型
char属于字符型
boolean属于布尔型
基本数据有「八个」,
byte,short,int,long属于数值型中的整数型
float,double属于数值型中的浮点型
char属于字符型
boolean属于布尔型
基本数据有「八个」,
「引用数据类型」
引用数据类型有「三个」,分别是类,接口和数组
引用数据类型有「三个」,分别是类,接口和数组
抽象类必须要有抽象方法吗?
不需要,抽象类不一定非要有抽象方法。
普通类和抽象类有什么区别
普通类不能包含抽象方法,抽象类可以包含抽象方法。
抽象类不能直接实例化,普通类可以直接实例化。
抽象类不能直接实例化,普通类可以直接实例化。
Excption与Error包结构?
Java可抛出(Throwable)的结构分为三种类型:被检查的异常(CheckedException),运行时异常
(RuntimeException),错误(Error)。
(RuntimeException),错误(Error)。
常见的异常有哪些?
NullPointerException 空指针异常
ArrayIndexOutOfBoundsException 索引越界异常
InputFormatException 输入类型不匹配
SQLException SQL异常
IllegalArgumentException 非法参数
NumberFormatException 类型转换异常 等等....
ArrayIndexOutOfBoundsException 索引越界异常
InputFormatException 输入类型不匹配
SQLException SQL异常
IllegalArgumentException 非法参数
NumberFormatException 类型转换异常 等等....
异常要怎么解决?
Java标准库内建了一些通用的异常,这些类以Throwable为顶层父类。
Throwable又派生出「Error类和Exception类」。
错误:Error类以及他的子类的实例,代表了JVM本身的错误。错误不能被程序员通过代码处理,Error很少出现。因此,程序员应该关注Exception为父类的分支下的各种异常类。
异常:Exception以及他的子类,代表程序运行时发送的各种不期望发生的事件。可以被Java异常处理机制使用,是异常处理的核心。
Throwable又派生出「Error类和Exception类」。
错误:Error类以及他的子类的实例,代表了JVM本身的错误。错误不能被程序员通过代码处理,Error很少出现。因此,程序员应该关注Exception为父类的分支下的各种异常类。
异常:Exception以及他的子类,代表程序运行时发送的各种不期望发生的事件。可以被Java异常处理机制使用,是异常处理的核心。
处理方法:
1.try catch
2.throw
3.throws
1.try catch
2.throw
3.throws
java 中 IO 流分为几种?
按功能来分:输入流(input)、输出流(output)。
按类型来分:字节流和字符流。
按类型来分:字节流和字符流。
字节流和字符流的区别是:字节流按 8 位传输以字节为单位输入输出数据,字符流按 16 位传输以字符为单位输入输出数据。
Files的常用方法都有哪些?
Files.exists():检测文件路径是否存在。
Files.createFile():创建文件。
Files.createDirectory():创建文件夹。
Files.delete():删除一个文件或目录。
Files.copy():复制文件。
Files.move():移动文件。
Files.size():查看文件个数。
Files.read():读取文件。
Files.write():写入文件。
Files.createFile():创建文件。
Files.createDirectory():创建文件夹。
Files.delete():删除一个文件或目录。
Files.copy():复制文件。
Files.move():移动文件。
Files.size():查看文件个数。
Files.read():读取文件。
Files.write():写入文件。
java 容器都有哪些?
Collection
List
Queue
Set
Map
HashMap
Collection 和 Collections 有什么区别?
java.util.Collection 是一个集合接口(集合类的一个顶级接口)。它提供了对集合对象进行基本操作的通用接口方法。Collection接口在Java 类库中有很多具体的实现。Collection接口的意义是为各种具体的集合提供了最大化的统一操作方式,其直接继承接口有List与Set。
Collections则是集合类的一个工具类/帮助类,其中提供了一系列静态方法,用于对集合中元素进行排序、搜索以及线程安全等各种操作。
List、Set、Map 之间的区别是什么?
如何决定使用 HashMap 还是 TreeMap?
对于在Map中插入、删除和定位元素这类操作,HashMap是最好的选择。然而,假如你需要对一个有序的key集合进行遍历,TreeMap是更好的选择。基于你的collection的大小,也许向HashMap中添加元素会更快,将map换为TreeMap进行有序key的遍历。
说一下 HashSet 的实现原理?
HashSet底层由HashMap实现
HashSet的值存放于HashMap的key上
HashMap的value统一为PRESENT
HashSet的值存放于HashMap的key上
HashMap的value统一为PRESENT
Array 和 ArrayList 有何区别?
Array可以容纳基本类型和对象,而ArrayList只能容纳对象。
Array是指定大小的,而ArrayList大小是固定的。
Array没有提供ArrayList那么多功能,比如addAll、removeAll和iterator等
Array是指定大小的,而ArrayList大小是固定的。
Array没有提供ArrayList那么多功能,比如addAll、removeAll和iterator等
如何实现数组和 List 之间的转换?
List转换成为数组:调用ArrayList的toArray方法。
数组转换成为List:调用Arrays的asList方法。
数组转换成为List:调用Arrays的asList方法。
在 Queue 中 poll()和 remove()有什么区别?
poll() 和 remove() 都是从队列中取出一个元素,但是 poll() 在获取元素失败的时候会返回空,但是 remove() 失败的时候会抛出异常。
迭代器 Iterator 是什么?
迭代器是一种设计模式,它是一个对象,它可以遍历并选择序列中的对象,而开发人员不需要了解该序列的底层结构。迭代器通常被称为“轻量级”对象,因为创建它的代价小。
Iterator 怎么使用?有什么特点?
Java中的Iterator功能比较简单,并且只能单向移动:
(1) 使用方法iterator()要求容器返回一个Iterator。第一次调用Iterator的next()方法时,它返回序列的第一个元素。注意:iterator()方法是java.lang.Iterable接口,被Collection继承。
(2) 使用next()获得序列中的下一个元素。
(3) 使用hasNext()检查序列中是否还有元素。
(4) 使用remove()将迭代器新返回的元素删除。
Iterator是Java迭代器最简单的实现,为List设计的ListIterator具有更多的功能,它可以从两个方向遍历List,也可以从List中插入和删除元素。
(1) 使用方法iterator()要求容器返回一个Iterator。第一次调用Iterator的next()方法时,它返回序列的第一个元素。注意:iterator()方法是java.lang.Iterable接口,被Collection继承。
(2) 使用next()获得序列中的下一个元素。
(3) 使用hasNext()检查序列中是否还有元素。
(4) 使用remove()将迭代器新返回的元素删除。
Iterator是Java迭代器最简单的实现,为List设计的ListIterator具有更多的功能,它可以从两个方向遍历List,也可以从List中插入和删除元素。
Iterator 和 ListIterator 有什么区别?
Iterator可用来遍历Set和List集合,但是ListIterator只能用来遍历List。
Iterator对集合只能是前向遍历,ListIterator既可以前向也可以后向。
ListIterator实现了Iterator接口,并包含其他的功能,比如:增加元素,替换元素,获取前一个和后一个元素的索引,等等。
Iterator对集合只能是前向遍历,ListIterator既可以前向也可以后向。
ListIterator实现了Iterator接口,并包含其他的功能,比如:增加元素,替换元素,获取前一个和后一个元素的索引,等等。
介绍一下 hashset 吧
set 继承于 Collection 接口,是一个「不允许出现重复元素,并且无序的集合」.
HashSet 是「基于 HashMap 实现」的,底层「采用 HashMap 来保存元素」
元素的哈希值是通过元素的 hashcode 方法 来获取的, HashSet 首先判断两个元素的哈希值,如果哈希值一样,接着会比较 equals 方法 如果 equls 结果为 true ,HashSet 就视为同一个元素。如果 equals 为 false 就不是同一个元素。
HashSet 是「基于 HashMap 实现」的,底层「采用 HashMap 来保存元素」
元素的哈希值是通过元素的 hashcode 方法 来获取的, HashSet 首先判断两个元素的哈希值,如果哈希值一样,接着会比较 equals 方法 如果 equls 结果为 true ,HashSet 就视为同一个元素。如果 equals 为 false 就不是同一个元素。
什么是泛型?
泛型:「把类型明确的工作推迟到创建对象或调用方法的时候才去明确的特殊的类型」
使用泛型的好处?
以集合来举例,使用泛型的好处是我们不必因为添加元素类型的不同而定义不同类型的集合,如整
型集合类,浮点型集合类,字符串集合类,我们可以定义一个集合来存放整型、浮点型,字符串型
数据,而这并不是最重要的,因为我们只要把底层存储设置了Object即可,添加的数据全部都可向
上转型为Object。 更重要的是我们可以通过规则按照自己的想法控制存储的数据类型。
以集合来举例,使用泛型的好处是我们不必因为添加元素类型的不同而定义不同类型的集合,如整
型集合类,浮点型集合类,字符串集合类,我们可以定义一个集合来存放整型、浮点型,字符串型
数据,而这并不是最重要的,因为我们只要把底层存储设置了Object即可,添加的数据全部都可向
上转型为Object。 更重要的是我们可以通过规则按照自己的想法控制存储的数据类型。
泛型擦除是什么?
因为泛型其实只是在编译器中实现的而虚拟机并不认识泛型类项,所以要在虚拟机中将泛型类型进行擦除。也就是说,「在编译阶段使用泛型,运行阶段取消泛型,即擦除」。擦除是将泛型类型以其父类代替,如String 变成了Object等。其实在使用的时候还是进行带强制类型的转化,只不过这是比较安全的转换,因为在编译阶段已经确保了数据的一致性。
创建对象有哪些方式
有「五种创建对象的方式」
1、new关键字
Person p1 = new Person();
2.Class.newInstance
Person p1 = Person.class.newInstance();
3.Constructor.newInstance
Constructor<Person> constructor = Person.class.getConstructor();
Person p1 = constructor.newInstance();
4.clone
Person p1 = new Person();
Person p2 = p1.clone();
5.反序列化
Person p1 = new Person();
byte[] bytes = SerializationUtils.serialize(p1);
Person p2 = (Person)SerializationUtils.deserialize(bytes);
1、new关键字
Person p1 = new Person();
2.Class.newInstance
Person p1 = Person.class.newInstance();
3.Constructor.newInstance
Constructor<Person> constructor = Person.class.getConstructor();
Person p1 = constructor.newInstance();
4.clone
Person p1 = new Person();
Person p2 = p1.clone();
5.反序列化
Person p1 = new Person();
byte[] bytes = SerializationUtils.serialize(p1);
Person p2 = (Person)SerializationUtils.deserialize(bytes);
讲讲单例模式懒汉式吧
// 懒汉式
public class Singleton {
// 延迟加载保证多线程安全
Private volatile static Singleton singleton;
private Singleton(){}
public static Singleton getInstance(){
if(singleton == null){
synchronized(Singleton.class){
if(singleton == null){
singleton = new Singleton();
}
}
}
return singleton;
}
}
使用 volatile 是「防止指令重排序,保证对象可见」,防止读到半初始化状态的对象
第一层if(singleton == null) 是为了防止有多个线程同时创建
synchronized 是加锁防止多个线程同时进入该方法创建对象
第二层if(singleton == null) 是防止有多个线程同时等待锁,一个执行完了后面一个又继续执行的情况
public class Singleton {
// 延迟加载保证多线程安全
Private volatile static Singleton singleton;
private Singleton(){}
public static Singleton getInstance(){
if(singleton == null){
synchronized(Singleton.class){
if(singleton == null){
singleton = new Singleton();
}
}
}
return singleton;
}
}
使用 volatile 是「防止指令重排序,保证对象可见」,防止读到半初始化状态的对象
第一层if(singleton == null) 是为了防止有多个线程同时创建
synchronized 是加锁防止多个线程同时进入该方法创建对象
第二层if(singleton == null) 是防止有多个线程同时等待锁,一个执行完了后面一个又继续执行的情况
深拷贝、浅拷贝是什么?
浅拷贝并不是真的拷贝,只是「复制指向某个对象的指针」,而不复制对象本身,新旧对象还是共享同一块内存。
深拷贝会另外「创造一个一模一样的对象」,新对象跟原对象不共享内存,修改新对象不会改到原对象。
xxljob
1.框架系统组成
1.调度中心:负责管理调度信息,按照调度配置发出调度请求,自身不承担业务代码。
2.执行模块(执行器):负责接收调度请求并执行任务逻辑。任务模块专注于任务的执行等操作,开发和维护更加简单和高效;接收“调度中心”的执行请求、终止请求和日志请求等。
注:生产中使用xxl-job2.2.0版本。使用注解:@XxlJob("userStatisticsTiming")
2.问题总结
1.如何避免任务重复执行
调度密集或者耗时任务可能会导致任务阻塞,集群情况下调度组件小概率情况下会重复触发;
针对上述情况,可以通过结合 “单机路由策略(如:第一台、一致性哈希)” + “阻塞策略(如:单机串行、丢弃后续调度)” 来规避,最终避免任务重复执行。
针对上述情况,可以通过结合 “单机路由策略(如:第一台、一致性哈希)” + “阻塞策略(如:单机串行、丢弃后续调度)” 来规避,最终避免任务重复执行。
2.分片任务:一个任务在多台服务器上同时都执行,降低任务处理时间,调度器会调用配置的所有机器
3.执行器用的端口和该执行器本身的端口没有关系,在启动时可以指定执行器端口,xxljob会自己启一个服务用于调度,如果不指定该端口,默认为9999
3.遇到的问题
4.1 老版本自有bug,句柄数过多导致任务调度失败,修改源码修复
老版本GULE(shell)模式,调用远程接口时,打开连接,没有关闭资源,随着任务的执行,未关闭的句柄数越来越多,最高为65535,达到后就无法继续调度任务,重启可以解决,但是每隔一段时间就会出现该问题。
4.2 任务重复执行,有可能的原因,
1.tigger重复调度(老版本使用quartz,quartz本身bug,单台机器也可能出现重复调度)。新版本摒弃quartz,自己解析cron表达式,计算下次执行时间,单机不会重复调度;集群使用行锁避免重复调度(需要mysql的引擎为InnoDB,myisam不支持行锁)
2.glue模式,curl调用定时任务接口,会走域名,走ng,ng默认配置请求服务器发生错误或者超时,会尝试调用别的机器,导致重复调用;解决方式,使用BEAN模式配置定时任务
老版本GULE(shell)模式,调用远程接口时,打开连接,没有关闭资源,随着任务的执行,未关闭的句柄数越来越多,最高为65535,达到后就无法继续调度任务,重启可以解决,但是每隔一段时间就会出现该问题。
4.2 任务重复执行,有可能的原因,
1.tigger重复调度(老版本使用quartz,quartz本身bug,单台机器也可能出现重复调度)。新版本摒弃quartz,自己解析cron表达式,计算下次执行时间,单机不会重复调度;集群使用行锁避免重复调度(需要mysql的引擎为InnoDB,myisam不支持行锁)
2.glue模式,curl调用定时任务接口,会走域名,走ng,ng默认配置请求服务器发生错误或者超时,会尝试调用别的机器,导致重复调用;解决方式,使用BEAN模式配置定时任务
4.新版本特性
5.1 自己解析cron表达式,自己计算下次调度时间
5.2 调度策略:
触发任务(tigger)使用线程池,维护了两个线程池(快、慢),正常调度都会走快的线程池(默认 core:10 max:200 queue:1000 丢弃策略:默认,抛出异常),当有任务调度时间超过500ms,并且出现10次,则会将该任务放入慢的线程池,目的是为了不影响其他任务调度
5.2 调度策略:
触发任务(tigger)使用线程池,维护了两个线程池(快、慢),正常调度都会走快的线程池(默认 core:10 max:200 queue:1000 丢弃策略:默认,抛出异常),当有任务调度时间超过500ms,并且出现10次,则会将该任务放入慢的线程池,目的是为了不影响其他任务调度
5.我们做的改造
1.丰富告警通道
2.加入prometheus埋点,记录不同时刻所有的调度任务数
3.调度线程池满,默认会抛出异常丢弃任务,我们捕获异常,发送告警
4.勾子机制,发布重启时先让线程池中的任务执行完,再关闭服务
7.注册和销毁
没有使用zk,而是使用了DB,每30s将注册信息写入到DB,admin每30s读取数据库,获取当前可用的执行器;执行器关闭时会调用destory方法,更新数据库,删除对应的记录
2.加入prometheus埋点,记录不同时刻所有的调度任务数
3.调度线程池满,默认会抛出异常丢弃任务,我们捕获异常,发送告警
4.勾子机制,发布重启时先让线程池中的任务执行完,再关闭服务
7.注册和销毁
没有使用zk,而是使用了DB,每30s将注册信息写入到DB,admin每30s读取数据库,获取当前可用的执行器;执行器关闭时会调用destory方法,更新数据库,删除对应的记录
6.一致性保证
为了避免多个服务器同时调度任务, 通过mysql悲观锁实现分布式锁(for update语句)
1 setAutoCommit(false)关闭隐式自动提交事务,
2 启动事务select lock for update(排他锁)
3 读db任务信息 -> 拉任务到内存时间轮 -> 更新db任务信息
4 commit提交事务,同时会释放for update的排他锁(悲观锁)
任务处理完毕后,释放悲观锁,准备等待下一次循环。
2 启动事务select lock for update(排他锁)
3 读db任务信息 -> 拉任务到内存时间轮 -> 更新db任务信息
4 commit提交事务,同时会释放for update的排他锁(悲观锁)
任务处理完毕后,释放悲观锁,准备等待下一次循环。
7.如何触发
1.早期:基于quartz.现在:timewheel时间轮,这个时间轮本质就是一个Map<Integer, List>
2.触发算法:
拿到了距now 5秒内的任务列表数据:scheduleList,分三种情况处理:for循环遍历scheduleList集合
(1)对到达now时间后的任务:(任务下一次触发时间+5s<now):直接跳过不执行; 重置trigger_next_time;
(2)对到达now时间后的任务:(任务下一次触发时间<now<任务下一次触发时间+5s):线程执行触发逻辑; 若任务下一次触发时间是在5秒内, 则放到时间轮内(Map<Integer, List> 秒数(1-60) => 任务id列表);再 重置trigger_next_time
(3)对未到达now时间的任务(任务下一次触发时间>now):直接放到时间轮内;重置trigger_next_time 。
拿到了距now 5秒内的任务列表数据:scheduleList,分三种情况处理:for循环遍历scheduleList集合
(1)对到达now时间后的任务:(任务下一次触发时间+5s<now):直接跳过不执行; 重置trigger_next_time;
(2)对到达now时间后的任务:(任务下一次触发时间<now<任务下一次触发时间+5s):线程执行触发逻辑; 若任务下一次触发时间是在5秒内, 则放到时间轮内(Map<Integer, List> 秒数(1-60) => 任务id列表);再 重置trigger_next_time
(3)对未到达now时间的任务(任务下一次触发时间>now):直接放到时间轮内;重置trigger_next_time 。
hbase
1.hbase特点
1.海量存储
Hbase 适合存储 PB 级别的海量数据,在 PB 级别的数据以及采用廉价 PC 存储的情况下,能在几十到百毫秒内返回数据。这与 Hbase 的极易扩展性息息相关。正式因为 Hbase 良好的扩展性,才为海量数据的存储提供了便利。
2.列式存储
这里的列式存储其实说的是列族存储,Hbase 是根据列族来存储数据的。列族下面可以有非常多的列,列族在创建表的时候就必须指定。
3.极易扩展
Hbase 的扩展性主要体现在两个方面,一个是基于上层处理能力(RegionServer)的扩展,一个是基于存储的扩展(HDFS)
通过横向添加 RegionSever 的机器,进行水平扩展,提升 Hbase 上层的处理能力,提升 Hbsae服务更多 Region 的能力
4.高并发
由于目前大部分使用 Hbase 的架构,都是采用的廉价 PC,因此单个 IO 的延迟其实并不小,一般在几十到上百 ms 之间。这里说的高并发,主要是在并发的情况下,Hbase 的单个IO 延迟下降并不多。能获得高并发、低延迟的服务。
5.稀疏
稀疏主要是针对 Hbase 列的灵活性,在列族中,你可以指定任意多的列,在列数据为空的情况下,是不会占用存储空间的
2.hbase架构
Hbase 是由 Client、Zookeeper、Master、HRegionServer、HDFS 等几个组件组成,下面来介绍一下几个组件的相关功能:
1.client
Client 包含了访问 Hbase 的接口,另外 Client 还维护了对应的 cache 来加速 Hbase 的访
问,比如 cache 的.META.元数据的信息
问,比如 cache 的.META.元数据的信息
2.Zookeeper
HBase 通过 Zookeeper 来做 master 的高可用、RegionServer 的监控、元数据的入口以及集群配置的维护等工作。具体工作如下:
1.通过 Zoopkeeper 来保证集群中只有 1 个 master 在运行,如果 master 异常,会通过竞争机制产生新的 master 提供服务
2.通过 Zoopkeeper 来监控 RegionServer 的状态,当 RegionSevrer 有异常的时候,通过回调的形式通知 Master RegionServer 上下线的信息
3.通过 Zoopkeeper 存储元数据的统一入口地址
3.Hmaster
master 节点的主要职责如下:
1.为 RegionServer 分配 Region
2.维护整个集群的负载均衡
3.维护集群的元数据信息
4.发现失效的 Region,并将失效的 Region 分配到正常的 RegionServer 上
5.当 RegionSever 失效的时候,协调对应 Hlog 的拆分
1.为 RegionServer 分配 Region
2.维护整个集群的负载均衡
3.维护集群的元数据信息
4.发现失效的 Region,并将失效的 Region 分配到正常的 RegionServer 上
5.当 RegionSever 失效的时候,协调对应 Hlog 的拆分
4.HregionServer
HregionServer 直接对接用户的读写请求,是真正的“干活”的节点。它的功能概括如下:
1.管理 master 为其分配的 Region
2.处理来自客户端的读写请求
3.负责和底层 HDFS 的交互,存储数据到 HDFS
4.负责 Region 变大以后的拆分
5.负责 Storefile 的合并工作
1.管理 master 为其分配的 Region
2.处理来自客户端的读写请求
3.负责和底层 HDFS 的交互,存储数据到 HDFS
4.负责 Region 变大以后的拆分
5.负责 Storefile 的合并工作
5.HDFS
HDFS 为 Hbase 提供最终的底层数据存储服务,同时为 HBase 提供高可用(Hlog 存储在HDFS)的支持,具体功能概括如下:
提供元数据和表数据的底层分布式存储服务;数据多副本,保证的高可靠和高可用性
提供元数据和表数据的底层分布式存储服务;数据多副本,保证的高可靠和高可用性
3.HBase 中的角色
1.HMaster
1.监控 RegionServer
2.处理 RegionServer 故障转移
3.处理元数据的变更
4.处理 region 的分配或转移
5.在空闲时间进行数据的负载均衡
6.通过 Zookeeper 发布自己的位置给客户端
2.处理 RegionServer 故障转移
3.处理元数据的变更
4.处理 region 的分配或转移
5.在空闲时间进行数据的负载均衡
6.通过 Zookeeper 发布自己的位置给客户端
2.RegionServer
1.负责存储 HBase 的实际数据
2.处理分配给它的 Region
3.刷新缓存到 HDFS
4.维护 Hlog
5.执行压缩
6.负责处理 Region 分片
2.处理分配给它的 Region
3.刷新缓存到 HDFS
4.维护 Hlog
5.执行压缩
6.负责处理 Region 分片
3.其他组件
1.Write-Ahead logs
2.Region
Hbase 表的分片,HBase 表会根据 RowKey值被切分成不同的 region 存储在 RegionServer中,在一个 RegionServer 中可以有多个不同的 region。
3.Store
HFile 存储在 Store 中,一个 Store 对应 HBase 表中的一个列族
4.MemStore
顾名思义,就是内存存储,位于内存中,用来保存当前的数据操作,所以当数据保存在WAL 中之后,RegsionServer 会在内存中存储键值对
5.HFile
这是在磁盘上保存原始数据的实际的物理文件,是实际的存储文件。StoreFile 是以 Hfile的形式存储在 HDFS 的。
4.HBase Shell 操作
1.进入 HBase 客户端命令行:bin/hbase shell
2.查看当前数据库中有哪些表:list
3.创建表:create 'student','info'
4.插入数据到表:hbase(main):003:0> put 'student','1001','info:sex','male'
hbase(main):004:0> put 'student','1001','info:age','18'
hbase(main):005:0> put 'student','1002','info:name','Janna'
hbase(main):004:0> put 'student','1001','info:age','18'
hbase(main):005:0> put 'student','1002','info:name','Janna'
5.扫描查看表数据:hbase(main):008:0> scan 'student'
hbase(main):009:0> scan 'student',{STARTROW => '1001', STOPROW => '1001'}
hbase(main):010:0> scan 'student',{STARTROW => '1001'}
hbase(main):009:0> scan 'student',{STARTROW => '1001', STOPROW => '1001'}
hbase(main):010:0> scan 'student',{STARTROW => '1001'}
6.查看表结构:describe ‘student’
7.更新指定字段的数据:hbase(main):012:0> put 'student','1001','info:name','Nick'
hbase(main):013:0> put 'student','1001','info:age','100'
hbase(main):013:0> put 'student','1001','info:age','100'
8.统计表数据行数:count 'student'
9.删除数据:
删除某 rowkey 的全部数据:deleteall 'student','1001'
删除某 rowkey 的某一列数据:delete 'student','1002','info:sex'
10.清空表数据:truncate 'student'提示:清空表的操作顺序为先 disable,然后再 truncate。
11.删除表
首先需要先让该表为 disable 状态:
hbase(main):019:0> disable 'student'
然后才能 drop 这个表:
hbase(main):020:0> drop 'student'
提示:如果直接 drop 表,会报错:ERROR: Table student is enabled. Disable it first.
hbase(main):019:0> disable 'student'
然后才能 drop 这个表:
hbase(main):020:0> drop 'student'
提示:如果直接 drop 表,会报错:ERROR: Table student is enabled. Disable it first.
12.变更表信息
将 info 列族中的数据存放 3 个版本:
hbase(main):022:0> alter 'student',{NAME=>'info',VERSIONS=>3}
hbase(main):022:0> get
'student','1001',{COLUMN=>'info:name',VERSIONS=>3}
hbase(main):022:0> alter 'student',{NAME=>'info',VERSIONS=>3}
hbase(main):022:0> get
'student','1001',{COLUMN=>'info:name',VERSIONS=>3}
5.HBase 数据结构
1.RowKey
1.与 nosql 数据库们一样,RowKey 是用来检索记录的主键
2.访问 HBASE table 中的行,只有三种方式:
1.通过单个 RowKey 访问
2.通过 RowKey 的 range(正则)
3.全表扫描
3.RowKey 行键 (RowKey)可以是任意字符串(最大长度是 64KB,实际应用中长度一般为10-100bytes),在 HBASE 内部,RowKey 保存为字节数组。存储时,数据按照 RowKey 的字典序(byte order)排序存储。设计 RowKey 时,要充分排序存储这个特性,将经常一起读取的行存储放到一起。(位置相关性)
2.Column Family:列族
列族:HBASE 表中的每个列,都归属于某个列族。列族是表的 schema 的一部 分(而列不是),必须在使用表之前定义。列名都以列族作为前缀。例如 courses:history,courses:math都属于 courses 这个列族。
3.Cell
由{rowkey, column Family:columu, version} 唯一确定的单元。cell 中的数据是没有类的,全部是字节码形式存贮。
关键字:无类型、字节码
4.Time Stamp
HBASE 中通过 rowkey和 columns 确定的为一个存贮单元称为cell。每个 cell都保存 着同一份数据的多个版本。版本通过时间戳来索引。时间戳的类型是 64 位整型。时间戳可以由 HBASE(在数据写入时自动 )赋值,此时时间戳是精确到毫秒 的当前系统时间。时间戳也可以由客户显式赋值。如果应用程序要避免数据版 本冲突,就必须自己生成具有唯一性的时间戳。每个 cell 中,不同版本的数据按照时间倒序排序,即最新的数据排在最前面。
版本回收:为了避免数据存在过多版本造成的的管理 (包括存贮和索引)负担,HBASE 提供 了两种数据版本回收方式。一是保存数据的最后 n 个版本,二是保存最近一段 时间内的版本(比如最近七天)。用户可以针对每个列族进行设置。
5.命名空间(HBase NameSpaces)
1.Table:表,所有的表都是命名空间的成员,即表必属于某个命名空间,如果没有指定,则在 default 默认的命名空间中。
2.RegionServer group:一个命名空间包含了默认的 RegionServer Group
3.Permission:权限,命名空间能够让我们来定义访问控制列表 ACL(Access Control List)。例如,创建表,读取表,删除,更新等等操作。
4.Quota:限额,可以强制一个命名空间可包含的 region 的数量。
6.Hbase原理
1.读流程
1.Client 先访问 zookeeper,从 meta 表读取 region 的位置,然后读取 meta 表中的数据。meta中又存储了用户表的 region 信息
2.根据 namespace、表名和 rowkey 在 meta 表中找到对应的 region 信息;
3.找到这个 region 对应的 regionserver;
4.查找对应的 region;
5.先从 MemStore 找数据,如果没有,再到 BlockCache 里面读;
6.BlockCache 还没有,再到 StoreFile 上读(为了读取的效率);
7.如果是从 StoreFile 里面读取的数据,不是直接返回给客户端,而是先写入 BlockCache,再返回给客户端
2.写流程
1.Client 向 HregionServer 发送写请求;
2.HregionServer 将数据写到 HLog(write ahead log)。为了数据的持久化和恢复;
3.HregionServer 将数据写到内存(MemStore);
4.反馈 Client 写成功。
3.数据 flush 过程
1.当 MemStore 数据达到阈值(默认是 128M,老版本是 64M),将数据刷到硬盘,将内存中的数据删除,同时删除 HLog 中的历史数据;
2.并将数据存储到 HDFS 中;
3.在 HLog 中做标记点。
4.数据合并过程
1.当数据块达到 4 块,Hmaster 触发合并操作,Region 将数据块加载到本地,进行合并;
2.当合并的数据超过 256M,进行拆分,将拆分后的 Region 分配给不同的 HregionServer管理
3.当HregionServer宕机后,将HregionServer上的hlog拆分,然后分配给不同的HregionServer加载,修改.META.;
4.注意:HLog 会同步到 HDFS。
7.HBase优化
1.高可用
在 HBase 中 Hmaster 负责监控 RegionServer 的生命周期,均衡 RegionServer 的负载,如果 Hmaster 挂掉了,那么整个 HBase 集群将陷入不健康的状态,并且此时的工作状态并不会维持太久。所以 HBase 支持对 Hmaster 的高可用配置
1.关闭 HBase 集群(如果没有开启则跳过此步)bin/stop-hbase.sh
2.在 conf 目录下创建 backup-masters 文件 touch conf/backup-masters
3.在 backup-masters 文件中配置高可用 HMaster 节点:echo hadoop103 > conf/backup-masters
4.将整个 conf 目录 scp 到其他节点:[atguigu@hadoop102 hbase]$ scp -r conf/ hadoop103:/opt/module/hbase/
[atguigu@hadoop102 hbase]$ scp -r conf/ hadoop104:/opt/module/hbase/
[atguigu@hadoop102 hbase]$ scp -r conf/ hadoop104:/opt/module/hbase/
5.打开页面测试查看: http://hadooo102:16010
2.预分区
每一个 region 维护着 startRow 与 endRowKey,如果加入的数据符合某个 region 维护的rowKey 范围,则该数据交给这个 region 维护。那么依照这个原则,我们可以将数据所要投放的分区提前大致的规划好,以提高 HBase 性能。
3.RowKey 设计
一条数据的唯一标识就是 rowkey,那么这条数据存储于哪个分区,取决于 rowkey 处于哪个一个预分区的区间内,设计 rowkey的主要目的 ,就是让数据均匀的分布于所有的 region中,在一定程度上防止数据倾斜。接下来我们就谈一谈 rowkey 常用的设计方案
1.生成随机数、hash、散列值
比如:原 本 rowKey 为 1001 的 , SHA1 后变成:dd01903921ea24941c26a48f2cec24e0bb0e8cc7
原 本 rowKey 为 3001 的 , SHA1 后变成:49042c54de64a1e9bf0b33e00245660ef92dc7bd
原 本 rowKey 为 5001 的 , SHA1 后变成:7b61dec07e02c188790670af43e717f0f46e8913
在做此操作之前,一般我们会选择从数据集中抽取样本,来决定什么样的 rowKey 来 Hash后作为每个分区的临界值
原 本 rowKey 为 3001 的 , SHA1 后变成:49042c54de64a1e9bf0b33e00245660ef92dc7bd
原 本 rowKey 为 5001 的 , SHA1 后变成:7b61dec07e02c188790670af43e717f0f46e8913
在做此操作之前,一般我们会选择从数据集中抽取样本,来决定什么样的 rowKey 来 Hash后作为每个分区的临界值
2.字符串反转
20170524000001 转成 10000042507102
20170524000002 转成 20000042507102
这样也可以在一定程度上散列逐步 put 进来的数据。
20170524000002 转成 20000042507102
这样也可以在一定程度上散列逐步 put 进来的数据。
3.字符串拼接
20170524000001_a12e
20170524000001_93i7
20170524000001_93i7
RowKey散列原则
如果RowKey是按时间戳的方式递增,不要将时间放在二进制码的前面,建议将RowKey的高位作为散列字段,由程序循环生成,低位放时间字段,这样将提高数据均衡分布在每个RegionServer实现负载均衡的几率,如果没有散列字段,首字段直接是时间信息,将产生所有数据都在一个RegionServer上堆积的热点现象,这样在做数据检索的时候负载将会集中在个别RegionServer,降低查询效率。
RowKey唯一原则
RowKey是按照字典排序存储的,因此,设计RowKey时候,要充分利用这个排序特点,将经常一起读取的数据存储到一块,将最近可能会被访问的数据放在一块。
举个例子:如果最近写入HBase表中的数据是最可能被访问的,可以考虑将时间戳作为RowKey的一部分,由于是字段排序,所以可以使用Long.MAX_VALUE-timeStamp作为RowKey,这样能保证新写入的数据在读取时可以别快速命中。
案例分析:
用户订单列表查询RowKey设计。
##需求场景
某用户根据查询条件查询历史订单列表
##查询条件
开始结束时间(orderTime)-----必选,
订单号(seriaNum),
状态(status),游戏号(gameID)
##结果显示要求
结果按照时间倒叙排列。
#解答
RowKey可以设计为:userNumo r d e r T i m e orderTimeorderTimeseriaNum
注:这样设计已经可以唯一标识一条记录了,订单详情都是可以根据订单号seriaNum来确定。在模糊匹配查询的时候startRow和endRow只需要设置到userNum$orderTime即可,如下:
startRow=userNum$maxvalue-stopTime
endRow=userNum$maxvalue-startTime
其他字段用filter实现。
用户订单列表查询RowKey设计。
##需求场景
某用户根据查询条件查询历史订单列表
##查询条件
开始结束时间(orderTime)-----必选,
订单号(seriaNum),
状态(status),游戏号(gameID)
##结果显示要求
结果按照时间倒叙排列。
#解答
RowKey可以设计为:userNumo r d e r T i m e orderTimeorderTimeseriaNum
注:这样设计已经可以唯一标识一条记录了,订单详情都是可以根据订单号seriaNum来确定。在模糊匹配查询的时候startRow和endRow只需要设置到userNum$orderTime即可,如下:
startRow=userNum$maxvalue-stopTime
endRow=userNum$maxvalue-startTime
其他字段用filter实现。
4.内存优化
HBase 操作过程中需要大量的内存开销,毕竟 Table 是可以缓存在内存中的,一般会分配整个可用内存的 70%给 HBase 的 Java 堆。但是不建议分配非常大的堆内存,因为 GC 过程持续太久会导致 RegionServer 处于长期不可用状态,一般 16~48G 内存就可以了,如果因为框架占用内存过高导致系统内存不足,框架一样会被系统服务拖死。
5.基础优化
1.允许在 HDFS 的文件中追加内容
2.优化 DataNode 允许的最大文件打开数
3.优化延迟高的数据操作的等待时间
4.优化数据的写入效率
5.设置 RPC 监听数量
6.优化 HStore 文件大小
7.优化 hbase 客户端缓存
8.指定 scan.next 扫描 HBase 所获取的行数
9.flush、compact、split 机制
数据结构与算法
1.java算法
1.冒泡排序
1.比较相邻的元素。如果前一个元素比后一个元素大 ,就交换这两个元素的位置。
2.对每一对相邻元素做同样的工作 ,从开始第一对元素到结尾的最后一对元素。最终最后位置的元素就是最大值。
3.时间复杂度:O(N^2)
2.选择排序
1.每一次遍历的过程中,都假定第一个索引处的元素是 最小值,和其他索引处的值依次进行比较,如果当前索引处的值大于其他某个索引处的值,则假定其他某个索弓出的值为最小值,最后可以找到最小值所在的索引
2.交换第一个索|处和最小值所在的索引|处的值
3.时间复杂度:O(N^2)
3.插入排序
1.把所有的元素分为两组,已经排序的和未排序的;
2.找到未排序的组中的第一个元素,向已经排序的组中进行插入;
3.倒叙遍历已经排序的元素,依次和待插入的元素进行比较,直到找到一个元素小于等于待插入元素,那么就把待插入元素放到这个位置,其他的元素向后移动-位;
4.时间复杂度:O(N^2)
4.希尔排序
1.选定一个增长量h ,按照增长量h作为数据分组的依据,对数据进行分组;
2.对分好组的每一组数据完成插入排序;
3.减小增长量,最小减为1 ,重复第二步操作。
5.归并排序
1.尽可能的一组数据拆分成两个元索相等的子组,并对每一个子组继续拆分 ,直到拆分后的每个子组的元素个数是1为止。
2.将相邻的两个子组进行合并成一个有序的大组;
3.不断的重复步骤2 ,直到最终只有一个组为止。
2.将相邻的两个子组进行合并成一个有序的大组;
3.不断的重复步骤2 ,直到最终只有一个组为止。
4.时间复杂度:O(nlogn)
归并排序的缺点:需要申请额外的数组空间,导致空间复杂度提升,是典型的以空间换时间的造作。
6.快速排序
1.首先设定一个分界值 ,通过该分界值将数组分成左右两部分;
2.将大于或等于分界值的数据放到到数组右边,小于分界值的数据放到数组的左边。此时左边部分中各元素都小于或等于分界值,而右边部分中各元素都大于或等于分界值;
3.然后,左边和右边的数据可以独立排序。对于左侧的数组数据,又可以取一个分界值,将该部分数据分成左右两部分,同样在左边放置较小值,右边放置较大值。右侧的数组数据也可以做类似处理。
4.重复上述过程,可以看出,这是一个递归定义。通过递归将左侧部分排好序后,再递归排好右侧部分的顺序。当左侧和右侧两个部分的数据排完序后,整个数组的排序也就完成了。
2.将大于或等于分界值的数据放到到数组右边,小于分界值的数据放到数组的左边。此时左边部分中各元素都小于或等于分界值,而右边部分中各元素都大于或等于分界值;
3.然后,左边和右边的数据可以独立排序。对于左侧的数组数据,又可以取一个分界值,将该部分数据分成左右两部分,同样在左边放置较小值,右边放置较大值。右侧的数组数据也可以做类似处理。
4.重复上述过程,可以看出,这是一个递归定义。通过递归将左侧部分排好序后,再递归排好右侧部分的顺序。当左侧和右侧两个部分的数据排完序后,整个数组的排序也就完成了。
2.雪花算法
3.Leaf算法(美团)
网络
谈⼀谈你对TCP/IP四层模型,OSI七层模型的理解?
为了增强通⽤性和兼容性,计算机⽹络都被设计成层次机构,每⼀层都遵守⼀定的规则。
因此有了OSI这样⼀个抽象的⽹络通信参考模型,按照这个标准使计算机⽹络系统可以互相连接。
物理层:通过⽹线、光缆等这种物理⽅式将电脑连接起来。传递的数据是⽐特流,0101010100。
数据链路层: ⾸先,把⽐特流封装成数据帧的格式,对0、1进⾏分组。电脑连接起来之后,数据都经过
⽹卡来传输,⽽⽹卡上定义了全世界唯⼀的MAC地址。然后再通过⼴播的形式向局域⽹内所有电脑发送
数据,再根据数据中MAC地址和⾃身对⽐判断是否是发给⾃⼰的。
⽹络层:⼴播的形式太低效,为了区分哪些MAC地址属于同⼀个⼦⽹,⽹络层定义了IP和⼦⽹掩码,通
过对IP和⼦⽹掩码进⾏与运算就知道是否是同⼀个⼦⽹,再通过路由器和交换机进⾏传输。IP协议属于
⽹络层的协议。
传输层:有了⽹络层的MAC+IP地址之后,为了确定数据包是从哪个进程发送过来的,就需要端⼝号,通
过端⼝来建⽴通信,⽐如TCP和UDP属于这⼀层的协议。
会话层:负责建⽴和断开连接
表示层:为了使得数据能够被其他的计算机理解,再次将数据转换成另外⼀种格式,⽐如⽂字、视频、
图⽚等。
应⽤层:最⾼层,⾯对⽤户,提供计算机⽹络与最终呈现给⽤户的界⾯
因此有了OSI这样⼀个抽象的⽹络通信参考模型,按照这个标准使计算机⽹络系统可以互相连接。
物理层:通过⽹线、光缆等这种物理⽅式将电脑连接起来。传递的数据是⽐特流,0101010100。
数据链路层: ⾸先,把⽐特流封装成数据帧的格式,对0、1进⾏分组。电脑连接起来之后,数据都经过
⽹卡来传输,⽽⽹卡上定义了全世界唯⼀的MAC地址。然后再通过⼴播的形式向局域⽹内所有电脑发送
数据,再根据数据中MAC地址和⾃身对⽐判断是否是发给⾃⼰的。
⽹络层:⼴播的形式太低效,为了区分哪些MAC地址属于同⼀个⼦⽹,⽹络层定义了IP和⼦⽹掩码,通
过对IP和⼦⽹掩码进⾏与运算就知道是否是同⼀个⼦⽹,再通过路由器和交换机进⾏传输。IP协议属于
⽹络层的协议。
传输层:有了⽹络层的MAC+IP地址之后,为了确定数据包是从哪个进程发送过来的,就需要端⼝号,通
过端⼝来建⽴通信,⽐如TCP和UDP属于这⼀层的协议。
会话层:负责建⽴和断开连接
表示层:为了使得数据能够被其他的计算机理解,再次将数据转换成另外⼀种格式,⽐如⽂字、视频、
图⽚等。
应⽤层:最⾼层,⾯对⽤户,提供计算机⽹络与最终呈现给⽤户的界⾯
TCP/IP则是四层的结构,相当于是对OSI模型的简化。
1. 数据链路层,也有称作⽹络访问层、⽹络接⼝层。他包含了OSI模型的物理层和数据链路层,把电
脑连接起来。
2. ⽹络层,也叫做IP层,处理IP数据包的传输、路由,建⽴主机间的通信。
3. 传输层,就是为两台主机设备提供端到端的通信。
4. 应⽤层,包含OSI的会话层、表示层和应⽤层,提供了⼀些常⽤的协议规范,⽐如FTP、SMPT、
HTTP等。
1. 数据链路层,也有称作⽹络访问层、⽹络接⼝层。他包含了OSI模型的物理层和数据链路层,把电
脑连接起来。
2. ⽹络层,也叫做IP层,处理IP数据包的传输、路由,建⽴主机间的通信。
3. 传输层,就是为两台主机设备提供端到端的通信。
4. 应⽤层,包含OSI的会话层、表示层和应⽤层,提供了⼀些常⽤的协议规范,⽐如FTP、SMPT、
HTTP等。
总结下来,就是物理层通过物理⼿段把电脑连接起来,数据链路层则对⽐特流的数据进⾏分组,⽹络层
来建⽴主机到主机的通信,传输层建⽴端⼝到端⼝的通信,应⽤层最终负责建⽴连接,数据格式转换,
最终呈现给⽤户。
来建⽴主机到主机的通信,传输层建⽴端⼝到端⼝的通信,应⽤层最终负责建⽴连接,数据格式转换,
最终呈现给⽤户。
说说TCP 3次握⼿的过程?
建⽴连接前server端需要监听端⼝,所以初始状态是LISTEN。
1. client端建⽴连接,发送⼀个SYN同步包,发送之后状态变成SYN_SENT
2. server端收到SYN之后,同意建⽴连接,返回⼀个ACK响应,同时也会给client发送⼀个SYN包,发
送完成之后状态变为SYN_RCVD
3. client端收到server的ACK之后,状态变为ESTABLISHED,返回ACK给server端。server收到之后状
态也变为ESTABLISHED,连接建⽴完成。
1. client端建⽴连接,发送⼀个SYN同步包,发送之后状态变成SYN_SENT
2. server端收到SYN之后,同意建⽴连接,返回⼀个ACK响应,同时也会给client发送⼀个SYN包,发
送完成之后状态变为SYN_RCVD
3. client端收到server的ACK之后,状态变为ESTABLISHED,返回ACK给server端。server收到之后状
态也变为ESTABLISHED,连接建⽴完成。
为什么要3次?2次,4次不⾏吗?
因为TCP是双⼯传输模式,不区分客户端和服务端,连接的建⽴是双向的过程。
如果只有两次,⽆法做到双向连接的建⽴,从建⽴连接server回复的SYN和ACK合并成⼀次可以看出来,
他也不需要4次。
挥⼿为什么要四次?因为挥⼿的ACK和FIN不能同时发送,因为数据发送的截⽌时间不同。
如果只有两次,⽆法做到双向连接的建⽴,从建⽴连接server回复的SYN和ACK合并成⼀次可以看出来,
他也不需要4次。
挥⼿为什么要四次?因为挥⼿的ACK和FIN不能同时发送,因为数据发送的截⽌时间不同。
那么四次挥⼿的过程呢?
1. client端向server发送FIN包,进⼊FIN_WAIT_1状态,这代表client端已经没有数据要发送了
2. server端收到之后,返回⼀个ACK,进⼊CLOSE_WAIT等待关闭的状态,因为server端可能还有没
有发送完成的数据
3. 等到server端数据都发送完毕之后,server端就向client发送FIN,进⼊LAST_ACK状态
4. client收到ACK之后,进⼊TIME_WAIT的状态,同时回复ACK,server收到之后直接进⼊CLOSED状
态,连接关闭。但是client要等待2MSL(报⽂最⼤⽣存时间)的时间,才会进⼊CLOSED状态。
2. server端收到之后,返回⼀个ACK,进⼊CLOSE_WAIT等待关闭的状态,因为server端可能还有没
有发送完成的数据
3. 等到server端数据都发送完毕之后,server端就向client发送FIN,进⼊LAST_ACK状态
4. client收到ACK之后,进⼊TIME_WAIT的状态,同时回复ACK,server收到之后直接进⼊CLOSED状
态,连接关闭。但是client要等待2MSL(报⽂最⼤⽣存时间)的时间,才会进⼊CLOSED状态。
第四次挥手为什么要等待2MSL(60s)
1. 为了保证连接的可靠关闭。如果server没有收到最后⼀个ACK,那么就会重发FIN。
2. 为了避免端⼝重⽤带来的数据混淆。如果client直接进⼊CLOSED状态,⼜⽤相同端⼝号向server建
⽴⼀个连接,上⼀次连接的部分数据在⽹络中延迟到达server,数据就可能发⽣混淆了。
2. 为了避免端⼝重⽤带来的数据混淆。如果client直接进⼊CLOSED状态,⼜⽤相同端⼝号向server建
⽴⼀个连接,上⼀次连接的部分数据在⽹络中延迟到达server,数据就可能发⽣混淆了。
TCP怎么保证传输过程的可靠性?
校验和:发送⽅在发送数据之前计算校验和,接收⽅收到数据后同样计算,如果不⼀致,那么传输有
误。
确认应答,序列号:TCP进⾏传输时数据都进⾏了编号,每次接收⽅返回ACK都有确认序列号。
超时重传:如果发送⽅发送数据⼀段时间后没有收到ACK,那么就重发数据。
连接管理:三次握⼿和四次挥⼿的过程。
流量控制:TCP协议报头包含16位的窗⼝⼤⼩,接收⽅会在返回ACK时同时把⾃⼰的即时窗⼝填⼊,发
送⽅就根据报⽂中窗⼝的⼤⼩控制发送速度。
拥塞控制:刚开始发送数据的时候,拥塞窗⼝是1,以后每次收到ACK,则拥塞窗⼝+1,然后将拥塞窗⼝
和收到的窗⼝取较⼩值作为实际发送的窗⼝,如果发⽣超时重传,拥塞窗⼝重置为1。这样做的⽬的就是
为了保证传输过程的⾼效性和可靠性。
误。
确认应答,序列号:TCP进⾏传输时数据都进⾏了编号,每次接收⽅返回ACK都有确认序列号。
超时重传:如果发送⽅发送数据⼀段时间后没有收到ACK,那么就重发数据。
连接管理:三次握⼿和四次挥⼿的过程。
流量控制:TCP协议报头包含16位的窗⼝⼤⼩,接收⽅会在返回ACK时同时把⾃⼰的即时窗⼝填⼊,发
送⽅就根据报⽂中窗⼝的⼤⼩控制发送速度。
拥塞控制:刚开始发送数据的时候,拥塞窗⼝是1,以后每次收到ACK,则拥塞窗⼝+1,然后将拥塞窗⼝
和收到的窗⼝取较⼩值作为实际发送的窗⼝,如果发⽣超时重传,拥塞窗⼝重置为1。这样做的⽬的就是
为了保证传输过程的⾼效性和可靠性。
负载均衡有哪些实现⽅式?
DNS:这是最简单的负载均衡的⽅式,⼀般⽤于实现地理级别的负载均衡,不同地域的⽤户通过DNS的
解析可以返回不同的IP地址,这种⽅式的负载均衡简单,但是扩展性太差,控制权在域名服务商。
Http重定向:通过修改Http响应头的Location达到负载均衡的⽬的,Http的302重定向。这种⽅式对性
能有影响,⽽且增加请求耗时。
反向代理:作⽤于应⽤层的模式,也被称作为七层负载均衡,⽐如常⻅的Nginx,性能⼀般可以达到万
级。这种⽅式部署简单,成本低,⽽且容易扩展。
IP:作⽤于⽹络层的和传输层的模式,也被称作四层负载均衡,通过对数据包的IP地址和端⼝进⾏修改
来达到负载均衡的效果。常⻅的有LVS(Linux Virtual Server),通常性能可以⽀持10万级并发。
按照类型来划分的话,还可以分成DNS负载均衡、硬件负载均衡、软件负载均衡。
其中硬件负载均衡价格昂贵,性能最好,能达到百万级,软件负载均衡包括Nginx、LVS这种。
解析可以返回不同的IP地址,这种⽅式的负载均衡简单,但是扩展性太差,控制权在域名服务商。
Http重定向:通过修改Http响应头的Location达到负载均衡的⽬的,Http的302重定向。这种⽅式对性
能有影响,⽽且增加请求耗时。
反向代理:作⽤于应⽤层的模式,也被称作为七层负载均衡,⽐如常⻅的Nginx,性能⼀般可以达到万
级。这种⽅式部署简单,成本低,⽽且容易扩展。
IP:作⽤于⽹络层的和传输层的模式,也被称作四层负载均衡,通过对数据包的IP地址和端⼝进⾏修改
来达到负载均衡的效果。常⻅的有LVS(Linux Virtual Server),通常性能可以⽀持10万级并发。
按照类型来划分的话,还可以分成DNS负载均衡、硬件负载均衡、软件负载均衡。
其中硬件负载均衡价格昂贵,性能最好,能达到百万级,软件负载均衡包括Nginx、LVS这种。
说说BIO/NIO/AIO的区别?
BIO:同步阻塞IO,每⼀个客户端连接,服务端都会对应⼀个处理线程,对于没有分配到处理线程的连
接就会被阻塞或者拒绝。相当于是⼀个连接⼀个线程。
接就会被阻塞或者拒绝。相当于是⼀个连接⼀个线程。
NIO:同步⾮阻塞IO,基于Reactor模型,客户端和channel进⾏通信,channel可以进⾏读写操作,通
过多路复⽤器selector来轮询注册在其上的channel,⽽后再进⾏IO操作。这样的话,在进⾏IO操作的时
候再⽤⼀个线程去处理就可以了,也就是⼀个请求⼀个线程。
过多路复⽤器selector来轮询注册在其上的channel,⽽后再进⾏IO操作。这样的话,在进⾏IO操作的时
候再⽤⼀个线程去处理就可以了,也就是⼀个请求⼀个线程。
AIO:异步⾮阻塞IO,相⽐NIO更进⼀步,完全由操作系统来完成请求的处理,然后通知服务端开启线程
去进⾏处理,因此是⼀个有效请求⼀个线程。
去进⾏处理,因此是⼀个有效请求⼀个线程。
那么你怎么理解同步和阻塞?
⾸先,可以认为⼀个IO操作包含两个部分:
1. 发起IO请求
2. 实际的IO读写操作
同步和异步在于第⼆个,实际的IO读写操作,如果操作系统帮你完成了再通知你,那就是异步,否则都
叫做同步。
1. 发起IO请求
2. 实际的IO读写操作
同步和异步在于第⼆个,实际的IO读写操作,如果操作系统帮你完成了再通知你,那就是异步,否则都
叫做同步。
阻塞和⾮阻塞在于第⼀个,发起IO请求,对于NIO来说通过channel发起IO操作请求后,其实就返回了,
所以是⾮阻塞。
所以是⾮阻塞。
谈⼀下你对Reactor模型的理解?
Reactor模型包含两个组件:
1. Reactor:负责查询、响应IO事件,当检测到IO事件时,分发给Handlers处理。
2. Handler:与IO事件绑定,负责IO事件的处理
1. Reactor:负责查询、响应IO事件,当检测到IO事件时,分发给Handlers处理。
2. Handler:与IO事件绑定,负责IO事件的处理
单线程Reactor
这个模式reactor和handler在⼀个线程中,如果某个handler阻塞的话,会导致其他所有的handler⽆法
执⾏,⽽且⽆法充分利⽤多核的性能。
这个模式reactor和handler在⼀个线程中,如果某个handler阻塞的话,会导致其他所有的handler⽆法
执⾏,⽽且⽆法充分利⽤多核的性能。
单Reactor多线程
由于decode、compute、encode的操作并⾮IO的操作,多线程Reactor的思路就是充分发挥多核的特
性,同时把⾮IO的操作剥离开。
但是,单个Reactor承担了所有的事件监听、响应⼯作,如果连接过多,还是可能存在性能问题。
由于decode、compute、encode的操作并⾮IO的操作,多线程Reactor的思路就是充分发挥多核的特
性,同时把⾮IO的操作剥离开。
但是,单个Reactor承担了所有的事件监听、响应⼯作,如果连接过多,还是可能存在性能问题。
多Reactor多线程
为了解决单Reactor的性能问题,就产⽣了多Reactor的模式。其中mainReactor建⽴连接,多个
subReactor则负责数据读写。
为了解决单Reactor的性能问题,就产⽣了多Reactor的模式。其中mainReactor建⽴连接,多个
subReactor则负责数据读写。
介绍一下 HTTP 协议吧
HTTP 协议是基于 TCP 协议实现的,它是一个超文本传输协议,其实就是一个简单的请求-响应协议,它指定了客户端可能发送给服务器什么样的消息以及得到什么样的响应。
它主要是负责点对点之间通信的。
超文本就是用超链接的方法,将各种不同空间的文字信息组织在一起的网状文本。比如说html,内部定义了很多图片视频的链接,放在浏览器上就呈现出了画面。
协议就是约定俗称的东西,比如说 moon 要给读者送一本书,读者那里只接受顺丰快递,那么 moon 觉得可以,发快递的时候选择的顺丰,那么我们彼此之间共同约定好的就叫做协议。
传输这个就很好理解了,比如刚才举的例子,将书发给读者,要通过骑车或者飞机的方式,传递的这个过程就是运输。
它主要是负责点对点之间通信的。
超文本就是用超链接的方法,将各种不同空间的文字信息组织在一起的网状文本。比如说html,内部定义了很多图片视频的链接,放在浏览器上就呈现出了画面。
协议就是约定俗称的东西,比如说 moon 要给读者送一本书,读者那里只接受顺丰快递,那么 moon 觉得可以,发快递的时候选择的顺丰,那么我们彼此之间共同约定好的就叫做协议。
传输这个就很好理解了,比如刚才举的例子,将书发给读者,要通过骑车或者飞机的方式,传递的这个过程就是运输。
GET 和 POST有什么区别?
GET 和 POST 本质上就是 TCP 链接,并无差别。
但是由于 HTTP 的规定和浏览器/服务器的限制,导致他们在应用过程中体现出一些不同。
但是由于 HTTP 的规定和浏览器/服务器的限制,导致他们在应用过程中体现出一些不同。
PING 的作用?
PING 主要的作用就是测试在两台主机之间能否建立连接,如果 PING 不通就无法建立连接。
它其实就是向目的主机发送多个 ICMP 回送请求报文
如果没有响应则无法建立连接
如果有响应就可以根据目的主机返回的回送报文的时间和成功响应的次数估算出数据包往返时间及丢包率
它其实就是向目的主机发送多个 ICMP 回送请求报文
如果没有响应则无法建立连接
如果有响应就可以根据目的主机返回的回送报文的时间和成功响应的次数估算出数据包往返时间及丢包率
常见的 HTTP 状态码有哪些
1xx 信息,服务器收到请求,需要请求者继续执行操作
2xx 成功,操作被成功接收并处理
3xx 重定向,需要进一步的操作以完成请求
4xx 客户端错误,请求包含语法错误或无法完成请求
5xx 服务器错误,服务器在处理请求的过程中发生了错误
2xx 成功,操作被成功接收并处理
3xx 重定向,需要进一步的操作以完成请求
4xx 客户端错误,请求包含语法错误或无法完成请求
5xx 服务器错误,服务器在处理请求的过程中发生了错误
HTTP1.1 和 HTTP1.0 的区别有哪些?
1.长链接
早期 HTTP1.0 的每一次请求都伴随着一次三次握手的过程,并且是串行的请求,增加了不必要的性能开销
HTTP1.1 新增了长链接的通讯方式,减少了性能损耗
2.管道
HTTP1.0 只有串行发送,没有管道
HTTP1.1 增加了管道的概念,使得在同一个 TCP 链接当中可以同时发出多个请求
3.断点续传
HTTP1.0 不支持断点续传
HTTP1.1 新增了 range 字段,用来指定数据字节位置,开启了断点续传的时代
4.Host头处理
HTTP1.0 任务主机只有一个节点,所以并没有传 HOST
HTTP1.1 时代,虚拟机技术越来越发达,一台机器上也有可能有很多节点,故增加了 HOST 信息
5.缓存处理
在HTTP1.0中主要使用header里的If-Modified-Since,Expires来做为缓存判断的标准
HTTP1.1则引入了更多的缓存控制策略例如Entity tag,If-Unmodified-Since, If-Match, If-None-Match等更多可供选择的缓存头来控制缓存策略。
6.错误状态响应码
在HTTP1.1中新增了24个错误状态响应码,如410(Gone)表示服务器上的某个资源被永久性的删除等。
早期 HTTP1.0 的每一次请求都伴随着一次三次握手的过程,并且是串行的请求,增加了不必要的性能开销
HTTP1.1 新增了长链接的通讯方式,减少了性能损耗
2.管道
HTTP1.0 只有串行发送,没有管道
HTTP1.1 增加了管道的概念,使得在同一个 TCP 链接当中可以同时发出多个请求
3.断点续传
HTTP1.0 不支持断点续传
HTTP1.1 新增了 range 字段,用来指定数据字节位置,开启了断点续传的时代
4.Host头处理
HTTP1.0 任务主机只有一个节点,所以并没有传 HOST
HTTP1.1 时代,虚拟机技术越来越发达,一台机器上也有可能有很多节点,故增加了 HOST 信息
5.缓存处理
在HTTP1.0中主要使用header里的If-Modified-Since,Expires来做为缓存判断的标准
HTTP1.1则引入了更多的缓存控制策略例如Entity tag,If-Unmodified-Since, If-Match, If-None-Match等更多可供选择的缓存头来控制缓存策略。
6.错误状态响应码
在HTTP1.1中新增了24个错误状态响应码,如410(Gone)表示服务器上的某个资源被永久性的删除等。
HTTPS 和 HTTP 的区别是什么?
1.SSL安全协议
HTTP 是超⽂本传输协议,信息是明⽂传输,存在安全⻛险的问题。
HTTPS 则解决 HTTP 不安全的缺陷,在TCP 和 HTTP ⽹络层之间加⼊了 SSL/TLS 安全协议,使得报⽂能够加密传输。
2.建立连接
HTTP 连接建⽴相对简单, TCP 三次握⼿之后便可进⾏ HTTP 的报⽂传输。
HTTPS 在 TCP 三次握⼿之后,还需进⾏ SSL/TLS 的握⼿过程,才可进⼊加密报⽂传输。
3.端口号
HTTP 的端⼝号是 80。
HTTPS 的端⼝号是 443。
4.CA证书
HTTPS 协议需要向 CA(证书权威。机构)申请数字证书来保证服务器的身份是可信的。
HTTP 是超⽂本传输协议,信息是明⽂传输,存在安全⻛险的问题。
HTTPS 则解决 HTTP 不安全的缺陷,在TCP 和 HTTP ⽹络层之间加⼊了 SSL/TLS 安全协议,使得报⽂能够加密传输。
2.建立连接
HTTP 连接建⽴相对简单, TCP 三次握⼿之后便可进⾏ HTTP 的报⽂传输。
HTTPS 在 TCP 三次握⼿之后,还需进⾏ SSL/TLS 的握⼿过程,才可进⼊加密报⽂传输。
3.端口号
HTTP 的端⼝号是 80。
HTTPS 的端⼝号是 443。
4.CA证书
HTTPS 协议需要向 CA(证书权威。机构)申请数字证书来保证服务器的身份是可信的。
知道HTTPS的⼯作原理吗?
1. ⽤户通过浏览器请求https⽹站,服务器收到请求,选择浏览器⽀持的加密和hash算法,同时返回
数字证书给浏览器,包含颁发机构、⽹址、公钥、证书有效期等信息。
2. 浏览器对证书的内容进⾏校验,如果有问题,则会有⼀个提示警告。否则,就⽣成⼀个随机数X,
同时使⽤证书中的公钥进⾏加密,并且发送给服务器。
3. 服务器收到之后,使⽤私钥解密,得到随机数X,然后使⽤X对⽹⻚内容进⾏加密,返回给浏览器
4. 浏览器则使⽤X和之前约定的加密算法进⾏解密,得到最终的⽹⻚内容
数字证书给浏览器,包含颁发机构、⽹址、公钥、证书有效期等信息。
2. 浏览器对证书的内容进⾏校验,如果有问题,则会有⼀个提示警告。否则,就⽣成⼀个随机数X,
同时使⽤证书中的公钥进⾏加密,并且发送给服务器。
3. 服务器收到之后,使⽤私钥解密,得到随机数X,然后使⽤X对⽹⻚内容进⾏加密,返回给浏览器
4. 浏览器则使⽤X和之前约定的加密算法进⾏解密,得到最终的⽹⻚内容
HTTP2 和 HTTP1.1 的区别是什么?
1.头部压缩
在 HTTP2 当中,如果你发出了多个请求,并且它们的头部(header)是相同的,那么 HTTP2 协议会帮你消除同样的部分。(其实就是在客户端和服务端维护一张索引表来实现)
2.二进制格式
HTTP1.1 采用明文的形式
HTTP/2 全⾯采⽤了⼆进制格式,头信息和数据体都是⼆进制
3.数据流
HTTP/2 的数据包不是按顺序发送的,同⼀个连接⾥⾯连续的数据包,可能属于不同的回应。(对数据包做了标记,标志其属于哪一个请求,其中规定客户端发出的数据流编号为奇数,服务器发出的数据流编号为偶数。客户端还可以指定数据流的优先级,优先级⾼的请求,服务器就先响应该请求)
4.IO多路复用
如:在⼀个连接中,服务器收到了客户端 A 和 B 的两个请求,但是发现在处理 A 的过程中⾮常耗时,索性就先回应 A 已经处理好的部分,再接着回应 B 请求,最后再回应 A 请求剩下的部分。
HTTP/2 可以在⼀个连接中并发多个请求或回应。
5.服务器推送
服务器可以主动向客户端发送请求
在 HTTP2 当中,如果你发出了多个请求,并且它们的头部(header)是相同的,那么 HTTP2 协议会帮你消除同样的部分。(其实就是在客户端和服务端维护一张索引表来实现)
2.二进制格式
HTTP1.1 采用明文的形式
HTTP/2 全⾯采⽤了⼆进制格式,头信息和数据体都是⼆进制
3.数据流
HTTP/2 的数据包不是按顺序发送的,同⼀个连接⾥⾯连续的数据包,可能属于不同的回应。(对数据包做了标记,标志其属于哪一个请求,其中规定客户端发出的数据流编号为奇数,服务器发出的数据流编号为偶数。客户端还可以指定数据流的优先级,优先级⾼的请求,服务器就先响应该请求)
4.IO多路复用
如:在⼀个连接中,服务器收到了客户端 A 和 B 的两个请求,但是发现在处理 A 的过程中⾮常耗时,索性就先回应 A 已经处理好的部分,再接着回应 B 请求,最后再回应 A 请求剩下的部分。
HTTP/2 可以在⼀个连接中并发多个请求或回应。
5.服务器推送
服务器可以主动向客户端发送请求
HTTP3 和 HTTP2 的区别是什么?
1.协议不同
HTTP2 是基于 TCP 协议实现的
HTTP3 是基于 UDP 协议实现的
2.QUIC
HTTP3 新增了 QUIC 协议来实现可靠性的传输
3.握手次数
HTTP2 是基于 HTTPS 实现的,建立连接需要先进行 TCP 3次握手,然后再进行 TLS 3次握手,总共6次握手
HTTP3 只需要 QUIC 的3次握手
HTTP2 是基于 TCP 协议实现的
HTTP3 是基于 UDP 协议实现的
2.QUIC
HTTP3 新增了 QUIC 协议来实现可靠性的传输
3.握手次数
HTTP2 是基于 HTTPS 实现的,建立连接需要先进行 TCP 3次握手,然后再进行 TLS 3次握手,总共6次握手
HTTP3 只需要 QUIC 的3次握手
TCP 滑动窗⼝是什么?
TCP 是每发送⼀个数据,都要进⾏⼀次确认应答。只有上一个收到了回应才发送下一个,这样效率会非常低,因此引进了滑动窗口的概念.
其实就是在发送方设立一个缓存区间,将已发送但未收到确认的消息缓存起来,假如一个窗口可以发送 5 个 TCP 段,那么发送方就可以连续发送 5 个 TCP 段,然后就会将这 5 个 TCP 段的数据缓存起来,这 5 个 TCP 段是有序的,只要后面的消息收到了 ACK ,那么不管前面的是否有收到 ACK,都代表成功,窗⼝⼤⼩是由接收方决定的。
窗⼝⼤⼩就是指不需要等待应答,还可以发送数据的大小。
其实就是在发送方设立一个缓存区间,将已发送但未收到确认的消息缓存起来,假如一个窗口可以发送 5 个 TCP 段,那么发送方就可以连续发送 5 个 TCP 段,然后就会将这 5 个 TCP 段的数据缓存起来,这 5 个 TCP 段是有序的,只要后面的消息收到了 ACK ,那么不管前面的是否有收到 ACK,都代表成功,窗⼝⼤⼩是由接收方决定的。
窗⼝⼤⼩就是指不需要等待应答,还可以发送数据的大小。
TCP和UDP的区别
TCP:面向链接,UDP:源端和终端不需要建立链接
对系统资源的要求(TCP较多,UDP少)
UDP程序结构较简单;
流模式与数据报模式 ;
TCP保证数据正确性,UDP可能丢包
TCP保证数据顺序,UDP不保证。
发送方一直发送数据,但是接收方处理不过来怎么办?(流量控制)
如果接收方处理不过来,发送方就会触发重试机制再次发送数据,然而这个是有性能损耗的,为了解决这个问题,TCP 就提出了流量控制,为的就是让发送方知道接受方的处理能力。
也就是说,每次接收方接受到数据后会将剩余可处理数据的大小告诉发送方。
比如接受方滑动窗口可用大小为400字节,发送方发送过来100字节的数据,那么接收方剩余可用滑动窗口大小就为300字节,这是发送方就知道下次返送数据的大小范围了。
但是这里有一个问题,数据会存放在缓冲区,但是这个缓冲区是操作系统控制的,当系统繁忙的时候,会缩减缓冲区减小,可能就会造成丢包的问题。
也就是说,每次接收方接受到数据后会将剩余可处理数据的大小告诉发送方。
比如接受方滑动窗口可用大小为400字节,发送方发送过来100字节的数据,那么接收方剩余可用滑动窗口大小就为300字节,这是发送方就知道下次返送数据的大小范围了。
但是这里有一个问题,数据会存放在缓冲区,但是这个缓冲区是操作系统控制的,当系统繁忙的时候,会缩减缓冲区减小,可能就会造成丢包的问题。
TCP 半连接队列和全连接队列是什么?
服务端收到客户端发出的 SYN 请求后,会把这个连接信息存储到半链接队列(SYN 队列)。
服务端收到第三次握⼿的 ACK 后,内核会把连接从半连接队列移除,然后创建新的完全的连接,并将其添加到全连接队列(accept 队列),等待进程调⽤ accept 函数时把连接取出来。
这两个队列都是有大小限制的,当超过容量后就会将链接丢弃,或者返回 RST 包。
服务端收到第三次握⼿的 ACK 后,内核会把连接从半连接队列移除,然后创建新的完全的连接,并将其添加到全连接队列(accept 队列),等待进程调⽤ accept 函数时把连接取出来。
这两个队列都是有大小限制的,当超过容量后就会将链接丢弃,或者返回 RST 包。
粘包/拆包是怎么发生的?怎么解决这个问题?
TCP 发送数据时会根据 TCP 缓冲区的实际情况进行包的划分,一个完整的包可能会被 TCP 拆分成多个包进行发送,也有可能把多个小的包封装成一个大的数据包发送,这就是 TCP 粘包和拆包问题。
发生 TCP 粘包的原因:
1.发送的数据小于 TCP 缓冲区大小,TCP将缓冲区中的数据(数据属于多条业务内容)一次发送出去可能就会发生粘包。
2.接收数据端的应用层没有及时读取接收缓冲区中的数据,将发生粘包。
发生 TCP 拆包的原因:
1.待发送数据大于最大报文长度,TCP 在传输前将进行拆包。
2.发送的数据大于 TCP 发送缓冲区剩余空间大小,将会发生拆包。
1.发送的数据小于 TCP 缓冲区大小,TCP将缓冲区中的数据(数据属于多条业务内容)一次发送出去可能就会发生粘包。
2.接收数据端的应用层没有及时读取接收缓冲区中的数据,将发生粘包。
发生 TCP 拆包的原因:
1.待发送数据大于最大报文长度,TCP 在传输前将进行拆包。
2.发送的数据大于 TCP 发送缓冲区剩余空间大小,将会发生拆包。
1.发送端给每个数据包添加包首部,首部中包含数据包的长度,这样接收端在接收到数据后,通过该字段就可以知道每个数据包的实际长度了。
2.发送端将每个数据包设置固定长度,这样接收端每次从读取固定长度的数据把每个数据包拆分开。
3.可以在数据包之间设置边界,如添加特殊符号,接收端可以通过这个特殊符号来拆分包。
2.发送端将每个数据包设置固定长度,这样接收端每次从读取固定长度的数据把每个数据包拆分开。
3.可以在数据包之间设置边界,如添加特殊符号,接收端可以通过这个特殊符号来拆分包。
浏览器地址栏输入网站按回车后发生了什么?
1:解析网址,生成 HTTP 请求信息
2:根据 DNS 服务器查询真实请求的 IP 地址,如果本地服务器有缓存则直接返回
3:得到了 IP 以后,向服务器发送 TCP 连接,TCP 连接经过三次握手。
4:接受 TCP 报文后,对连接进行处理,对 HTTP 协议解析
5:服务器返回响应
6:浏览器接受响应,显示页面,渲染页面
2:根据 DNS 服务器查询真实请求的 IP 地址,如果本地服务器有缓存则直接返回
3:得到了 IP 以后,向服务器发送 TCP 连接,TCP 连接经过三次握手。
4:接受 TCP 报文后,对连接进行处理,对 HTTP 协议解析
5:服务器返回响应
6:浏览器接受响应,显示页面,渲染页面
SpringSecurity安全框架
linux
linux常用命令
tar:创建一个新的tar文件
创建:tar cvf 文件名 路径
解压:tar xvf 文件名
查看:tar tvf 文件名
grep:在文件中查找字符串
find:查找指定文件名的文件
ssh:登录到远程主机
vim:打开文件
sort:以升序对文件内容排序
ls:以易读的方式显示文件大小
pwd:输出当前工作目录
ps:显示正在运行中的进程的信息
top:当前系统中占用资源最最多的一些进程
创建:tar cvf 文件名 路径
解压:tar xvf 文件名
查看:tar tvf 文件名
grep:在文件中查找字符串
find:查找指定文件名的文件
ssh:登录到远程主机
vim:打开文件
sort:以升序对文件内容排序
ls:以易读的方式显示文件大小
pwd:输出当前工作目录
ps:显示正在运行中的进程的信息
top:当前系统中占用资源最最多的一些进程
vim命令
vim [文件名] 文本编辑器vim 四种模式:命令模式、插入模式、编辑模式、普通模式
:set nu 设置行号
:set nonu 取消行号
:wq 保存退出
a 在光标所在字符后插入 x 删除光标所在处字符
A 在光标所在行尾插入 nx 删除光标所在处后n个字符
i 在光标所在字符前插入 dd 删除光标所在行,ndd删除n行
I 在光标所在行行首插入 dG 删除光标所在行到文件末尾内容
o 在光标下插入新行 D 删除光标所在处到行尾内容
O 在光标上插入新行 :n1,n2d 删除指定范围的行
gg 到第一行 yy 复制当前行
G 到最后一行 nyy 复制当前行以下n行
nG 到第n行 dd 剪切当前行
:n 到第n行 ndd 剪切当前行以下n行
$ 移至行尾 p、P 粘贴在当前光标所在行下或行上
0 移至行首 R 从光标所在处开始替换子符,按Esc结束
u 取消上一步操作 /string 搜索指定字符串,搜索时忽略大小写 :set ic
n 搜索指定字符串的下一个出现位置 :%s/old/nwe/g 全文替换指定字符串
:n1,n2s/old/new/g 在一定范围内替换指定字符串
:w 保存修改 :w new_filename 另存为指定文件
:wq 保存修改并退出 ZZ 快捷键,保存修改并退出
:q! 不保存修改并退出 :wq! 保存修改并退出(文件所有者及root可使用)
:r !命令 导入命令执行结果 :map 定义快捷键
:n1,n2s/^/#/g
:n1,n2s/^#//g
:n1,n2s/^/\/\//g 连续行注释
:ab mymail samlee@lampbrother.net 替换
:set nu 设置行号
:set nonu 取消行号
:wq 保存退出
a 在光标所在字符后插入 x 删除光标所在处字符
A 在光标所在行尾插入 nx 删除光标所在处后n个字符
i 在光标所在字符前插入 dd 删除光标所在行,ndd删除n行
I 在光标所在行行首插入 dG 删除光标所在行到文件末尾内容
o 在光标下插入新行 D 删除光标所在处到行尾内容
O 在光标上插入新行 :n1,n2d 删除指定范围的行
gg 到第一行 yy 复制当前行
G 到最后一行 nyy 复制当前行以下n行
nG 到第n行 dd 剪切当前行
:n 到第n行 ndd 剪切当前行以下n行
$ 移至行尾 p、P 粘贴在当前光标所在行下或行上
0 移至行首 R 从光标所在处开始替换子符,按Esc结束
u 取消上一步操作 /string 搜索指定字符串,搜索时忽略大小写 :set ic
n 搜索指定字符串的下一个出现位置 :%s/old/nwe/g 全文替换指定字符串
:n1,n2s/old/new/g 在一定范围内替换指定字符串
:w 保存修改 :w new_filename 另存为指定文件
:wq 保存修改并退出 ZZ 快捷键,保存修改并退出
:q! 不保存修改并退出 :wq! 保存修改并退出(文件所有者及root可使用)
:r !命令 导入命令执行结果 :map 定义快捷键
:n1,n2s/^/#/g
:n1,n2s/^#//g
:n1,n2s/^/\/\//g 连续行注释
:ab mymail samlee@lampbrother.net 替换
ps命令
ps [-Aau] 查看当前系统后台运行状况
-A 查看所有进程
-au 显示所有包含其他使用者的进程
-aux a所有进程 u用户 x 不连续的终端,系统中
-le 查看系统中所有进程,使用Linux标准命令格式
-A 查看所有进程
-au 显示所有包含其他使用者的进程
-aux a所有进程 u用户 x 不连续的终端,系统中
-le 查看系统中所有进程,使用Linux标准命令格式
grep命令
grep -iv [指定字串] [文件/搜索内容] 在文件中搜索字串匹配的行并输出
-i 不区分大小写
-v 排除指定字串 ^# 表示#不管在前面、中间、后面都排除
-n 输出行号
--color=auto 搜索出的关键字用
-i 不区分大小写
-v 排除指定字串 ^# 表示#不管在前面、中间、后面都排除
-n 输出行号
--color=auto 搜索出的关键字用
Git
什么是 Git 复刻(fork)?复刻(fork)、分支(branch)和克隆(clone)之间有什么区别?
复刻(fork) 是对存储仓库(repository)进行的远程的、服务器端的拷贝,从源头上就有所区别。复刻实际上不是 Git 的范畴。它更像是个政治/社会概念。
克隆(clone) 不是复刻,克隆是个对某个远程仓库的本地拷贝。克隆时,实际上是拷贝整个源存储仓库,包括所有历史记录和分支。
分支(branch) 是一种机制,用于处理单一存储仓库中的变更,并最终目的是用于与其他部分代码合并。
克隆(clone) 不是复刻,克隆是个对某个远程仓库的本地拷贝。克隆时,实际上是拷贝整个源存储仓库,包括所有历史记录和分支。
分支(branch) 是一种机制,用于处理单一存储仓库中的变更,并最终目的是用于与其他部分代码合并。
“拉取请求(pull request)”和“分支(branch)”之间有什么区别?
分支(branch) 是代码的一个独立版本。
拉取请求(pull request) 是当有人用仓库,建立了自己的分支,做了些修改并合并到该分支(把自己修改应用到别人的代码仓库)。
拉取请求(pull request) 是当有人用仓库,建立了自己的分支,做了些修改并合并到该分支(把自己修改应用到别人的代码仓库)。
“git pull”和“git fetch”之间有什么区别?
当你使用 pull,Git 会试着自动为你完成工作。它是上下文(工作环境)敏感的,所以 Git 会把所有拉取的提交合并到你当前处理的分支中。pull 则是 自动合并提交而没有让你复查的过程。如果你没有细心管理你的分支,你可能会频繁遇到冲突。
当你 fetch,Git 会收集目标分支中的所有不存在的提交,并将这些提交存储到本地仓库中。但Git 不会把这些提交合并到当前分支中。这种处理逻辑在当你需要保持仓库更新,在更新文件时又希望处理可能中断的事情时,这将非常实用。而将提交合并到主分支中,则该使用 merge。
当你 fetch,Git 会收集目标分支中的所有不存在的提交,并将这些提交存储到本地仓库中。但Git 不会把这些提交合并到当前分支中。这种处理逻辑在当你需要保持仓库更新,在更新文件时又希望处理可能中断的事情时,这将非常实用。而将提交合并到主分支中,则该使用 merge。
如果分支是否已合并为master,你可以通过什么手段知道?
要知道某个分支是否已合并为master,你可以使用以下命令:
git branch –merged 它列出了已合并到当前分支的分支。
git branch –no-merged 它列出了尚未合并的分支。
git branch –merged 它列出了已合并到当前分支的分支。
git branch –no-merged 它列出了尚未合并的分支。
描述一下你所使用的分支策略?
功能分支(Feature branching)
要素分支模型将特定要素的所有更改保留在分支内。当通过自动化测试对功能进行全面测试和验证时,该分支将合并到主服务器中。
任务分支(Task branching)
在此模型中,每个任务都在其自己的分支上实现,任务键包含在分支名称中。很容易看出哪个代码实现了哪个任务,只需在分支名称中查找任务键。
发布分支(Release branching)
一旦开发分支获得了足够的发布功能,你就可以克隆该分支来形成发布分支。创建该分支将会启动下一个发布周期,所以在此之后不能再添加任何新功能,只有错误修复,文档生成和其他面向发布的任务应该包含在此分支中。一旦准备好发布,该版本将合并到主服务器并标记版本号。此外,它还应该再将自发布以来已经取得的进展合并回开发分支。
要素分支模型将特定要素的所有更改保留在分支内。当通过自动化测试对功能进行全面测试和验证时,该分支将合并到主服务器中。
任务分支(Task branching)
在此模型中,每个任务都在其自己的分支上实现,任务键包含在分支名称中。很容易看出哪个代码实现了哪个任务,只需在分支名称中查找任务键。
发布分支(Release branching)
一旦开发分支获得了足够的发布功能,你就可以克隆该分支来形成发布分支。创建该分支将会启动下一个发布周期,所以在此之后不能再添加任何新功能,只有错误修复,文档生成和其他面向发布的任务应该包含在此分支中。一旦准备好发布,该版本将合并到主服务器并标记版本号。此外,它还应该再将自发布以来已经取得的进展合并回开发分支。
如何在Git中创建存储库?
要创建存储库,先为项目创建一个目录(如果该目录不存在),然后运行命令 git init。通过运行此命令,将在项目的目录中创建 .git 目录。
列举工作中常用的git命令
1.新增文件的命令:git add file或者git add .
2.提交文件的命令:git commit –m或者git commit –a
3.查看工作区状况:git status –s
4.拉取合并远程分支的操作:git fetch/git merge或者git pull
5.查看提交记录命令:git reflog
6.创建仓库:git init
7.查看仓库的状态:git status
8.这次相较上次修改了哪些内容:git diff
9.将添加的文件放到栈存区中:git add
10.将栈存区内容提交到代码区中:git commit
11.将远程仓库的代码克隆到本地:git clone git地址
12.查看当前分支:git branch
13.切换分支:git checkout
2.提交文件的命令:git commit –m或者git commit –a
3.查看工作区状况:git status –s
4.拉取合并远程分支的操作:git fetch/git merge或者git pull
5.查看提交记录命令:git reflog
6.创建仓库:git init
7.查看仓库的状态:git status
8.这次相较上次修改了哪些内容:git diff
9.将添加的文件放到栈存区中:git add
10.将栈存区内容提交到代码区中:git commit
11.将远程仓库的代码克隆到本地:git clone git地址
12.查看当前分支:git branch
13.切换分支:git checkout
提交时发生冲突,你能解释冲突是如何产生的吗?你是如何解决的?
因为在合并分支的时候,master分支和dev分支恰好有人都修改了同一个文件,GIT不知道应该以哪一个人的文件为准,所以就产生了冲突了。
我是直接在js代码中手动解决冲突,将冲突的代码进行手动合并
我是直接在js代码中手动解决冲突,将冲突的代码进行手动合并
如果本次提交误操作,如何撤销
可以先用git reflog查看历史提交记录
1.软撤销
本地代码不会变化,只是 git 转改会恢复为 commit 之前的状态
不删除工作空间改动代码,撤销 commit,不撤销 git add .
git reset --soft HEAD~1 //表示撤销最后一次的 commit ,1 可以换成其他更早的数字
2.硬撤销
本地代码会直接变更为指定的提交版本,慎用
删除工作空间改动代码,撤销 commit,撤销 git add .
git reset --hard HEAD~1 //注意完成这个操作后,就恢复到了上一次的commit状态
如果仅仅是 commit 的消息内容填错了
输入git commit --amend
进入 vim 模式,对 message 进行更改
还有一个 --mixed
git reset --mixed HEAD~1
意思是:不删除工作空间改动代码,撤销commit,并且撤销git add . 操作
这个为默认参数,git reset --mixed HEAD~1 和 git reset HEAD~1 效果是一样的。
1.软撤销
本地代码不会变化,只是 git 转改会恢复为 commit 之前的状态
不删除工作空间改动代码,撤销 commit,不撤销 git add .
git reset --soft HEAD~1 //表示撤销最后一次的 commit ,1 可以换成其他更早的数字
2.硬撤销
本地代码会直接变更为指定的提交版本,慎用
删除工作空间改动代码,撤销 commit,撤销 git add .
git reset --hard HEAD~1 //注意完成这个操作后,就恢复到了上一次的commit状态
如果仅仅是 commit 的消息内容填错了
输入git commit --amend
进入 vim 模式,对 message 进行更改
还有一个 --mixed
git reset --mixed HEAD~1
意思是:不删除工作空间改动代码,撤销commit,并且撤销git add . 操作
这个为默认参数,git reset --mixed HEAD~1 和 git reset HEAD~1 效果是一样的。
如何把本地仓库的内容推向一个空的远程仓库?
1.首先确保本地仓库与远程之间是连同的。如果提交失败,则需要进行下面的命令进行连通:
git remote add origin XXXX 注意:XXXX是你的远程仓库地址。
2.如果是第一次推送,则进行下面命令:
git push -u origin master 注意:-u 是指定origin为默认主分支
3.之后的提交,只需要下面的命令:
git push origin master
git remote add origin XXXX 注意:XXXX是你的远程仓库地址。
2.如果是第一次推送,则进行下面命令:
git push -u origin master 注意:-u 是指定origin为默认主分支
3.之后的提交,只需要下面的命令:
git push origin master
请说一下团队内部协作开发的流程
首先在github上创建一个项目,然后为小组成员添加权限,给小组长搭好框架,把本地项目上传到远程项目 ,新建一个开发分支,所有成员都切换到开发分支中,在开发分支中进行开发,最后小组长将开发分支合并到master分支上,并解决一些冲突
请说一下远程跨团队协作开发的流程
首先需要在其他团队里申请到访问权限,然后在其他的团队那里克隆一下文件,然后在本地运行这个文件,创建一个分支,在这个分支里做进一步的操作,最后合并分支解决冲突提交到远程仓库就行
你所参与的多人协同开发时候,项目都有哪些分支,分支名是什么,每个分支代表什么,以及分支是由谁合并
mster 主分支用来发布
dev 日常开发用的分支
test 测试用的分支
分支最后会由小组长合并
dev 日常开发用的分支
test 测试用的分支
分支最后会由小组长合并
请写出将工作区文件推送到远程仓库的思路?
方案一:
用命令行将本地仓库推送到远程仓库。
首先先创建一个远程仓库待用,然后在本地建立git版本管理,在项目目录下进行以下操作
git init 项目目录下会多一个.git文件
git add . 添加所有文件
git commit -m“first commit” 提交文件
git remote add origin //添加到远程仓库
git push -u origin master //第一次推送到远程仓库需要 -u,将本地master推送到远程master,以后就直接Git push 就好了
方案二:
用命令行创建一个新的仓库
如果已经拉取了git仓库,那我们只需要在该仓库下进行项目的新建等操作即可
首先先创建一个本地文件
然后git init 初始化git
git add 文件 添加本地文件到暂存区
git commit -m“first commit” 提交文件
git remote add origin //添加到远程仓库
git push -u origin master //第一次推送到远程仓库需要 -u,将本地master推送到远程master,以后就直接Git push 就好了
用命令行将本地仓库推送到远程仓库。
首先先创建一个远程仓库待用,然后在本地建立git版本管理,在项目目录下进行以下操作
git init 项目目录下会多一个.git文件
git add . 添加所有文件
git commit -m“first commit” 提交文件
git remote add origin //添加到远程仓库
git push -u origin master //第一次推送到远程仓库需要 -u,将本地master推送到远程master,以后就直接Git push 就好了
方案二:
用命令行创建一个新的仓库
如果已经拉取了git仓库,那我们只需要在该仓库下进行项目的新建等操作即可
首先先创建一个本地文件
然后git init 初始化git
git add 文件 添加本地文件到暂存区
git commit -m“first commit” 提交文件
git remote add origin //添加到远程仓库
git push -u origin master //第一次推送到远程仓库需要 -u,将本地master推送到远程master,以后就直接Git push 就好了
Kafka
简述Kafka的架构设计?
总的来说,Kafka是分为三个角色:Producer、Kafka集群以及Consumer,生产者将消息发送到Kafka集群,然后消费者再去Kafka集群进行消息的消费
Consumer Group:消费者组,消费者组内每个消费者负责消费不同分区的数据,提高消费能力。逻
辑上的一个订阅者。
Topic:可以理解为一个队列,Topic 将消息分类,生产者和消费者面向的是同一个 Topic。
Partition:为了实现扩展性,提高并发能力,一个Topic 以多个Partition的方式分布到多个 Broker
上,每个 Partition 是一个 有序的队列。一个 Topic 的每个Partition都有若干个副本(Replica),一个
Leader 和若干个 Follower。生产者发送数据的对象,以及消费者消费数据的对象,都是 Leader。
Follower负责实时从 Leader 中同步数据,保持和 Leader 数据的同步。Leader 发生故障时,某个
Follower 还会成为新的 Leader。
Offset:消费者消费的位置信息,监控数据消费到什么位置,当消费者挂掉再重新恢复的时候,可以从
消费位置继续消费。
Zookeeper:Kafka 集群能够正常工作,需要依赖于 Zookeeper,Zookeeper 帮助 Kafka 存储和管理
集群信息。
辑上的一个订阅者。
Topic:可以理解为一个队列,Topic 将消息分类,生产者和消费者面向的是同一个 Topic。
Partition:为了实现扩展性,提高并发能力,一个Topic 以多个Partition的方式分布到多个 Broker
上,每个 Partition 是一个 有序的队列。一个 Topic 的每个Partition都有若干个副本(Replica),一个
Leader 和若干个 Follower。生产者发送数据的对象,以及消费者消费数据的对象,都是 Leader。
Follower负责实时从 Leader 中同步数据,保持和 Leader 数据的同步。Leader 发生故障时,某个
Follower 还会成为新的 Leader。
Offset:消费者消费的位置信息,监控数据消费到什么位置,当消费者挂掉再重新恢复的时候,可以从
消费位置继续消费。
Zookeeper:Kafka 集群能够正常工作,需要依赖于 Zookeeper,Zookeeper 帮助 Kafka 存储和管理
集群信息。
Kafka什么时候会出现消息丢失以及解决方案?
丢失的情况:
生产者
消费者
broker
生产者
消费者
broker
生产者:
1、ack=0,不重试 producer发送消息完,不管结果了,如果发送失败也就丢失了。
2、ack=1,leader crash producer发送消息完,只等待lead写入成功就返回了,leader crash了,这时follower没来及同步,消 息丢失。
3、unclean.leader.election.enable 配置true 允许选举ISR以外的副本作为leader,会导致数据丢失,默认为false。producer发送异步消息完,只等待 lead写入成功就返回了,leader crash了,这时ISR中没有follower,leader从OSR中选举,因为OSR 中本来落后于Leader造成消息丢失。
解决方案:
1、配置:ack=all / -1,tries > 1,unclean.leader.election.enable : false producer发送消息完,等待follower同步完再返回,如果异常则重试。副本的数量可能影响吞吐量。 不允许选举ISR以外的副本作为leader。
2、配置:min.insync.replicas > 1 副本指定必须确认写操作成功的最小副本数量。如果不能满足这个最小值,则生产者将引发一个异常(要么是 NotEnoughReplicas,要么是NotEnoughReplicasAfterAppend)。 min.insync.replicas和ack更大的持久性保证。确保如果大多数副本没有收到写操作,则生产者将引发异 常。
3、失败的offset单独记录 producer发送消息,会自动重试,遇到不可恢复异常会抛出,这时可以捕获异常记录到数据库或缓存,进行 单独处理。
1、ack=0,不重试 producer发送消息完,不管结果了,如果发送失败也就丢失了。
2、ack=1,leader crash producer发送消息完,只等待lead写入成功就返回了,leader crash了,这时follower没来及同步,消 息丢失。
3、unclean.leader.election.enable 配置true 允许选举ISR以外的副本作为leader,会导致数据丢失,默认为false。producer发送异步消息完,只等待 lead写入成功就返回了,leader crash了,这时ISR中没有follower,leader从OSR中选举,因为OSR 中本来落后于Leader造成消息丢失。
解决方案:
1、配置:ack=all / -1,tries > 1,unclean.leader.election.enable : false producer发送消息完,等待follower同步完再返回,如果异常则重试。副本的数量可能影响吞吐量。 不允许选举ISR以外的副本作为leader。
2、配置:min.insync.replicas > 1 副本指定必须确认写操作成功的最小副本数量。如果不能满足这个最小值,则生产者将引发一个异常(要么是 NotEnoughReplicas,要么是NotEnoughReplicasAfterAppend)。 min.insync.replicas和ack更大的持久性保证。确保如果大多数副本没有收到写操作,则生产者将引发异 常。
3、失败的offset单独记录 producer发送消息,会自动重试,遇到不可恢复异常会抛出,这时可以捕获异常记录到数据库或缓存,进行 单独处理。
消费者:
自动commit;
解决方案:
手动commit
自动commit;
解决方案:
手动commit
broker:
刷盘
解决方案:
减小刷盘间隔
刷盘
解决方案:
减小刷盘间隔
Kafka是pull还是push?
pull,默认一次最多500条
Kafka中zk的作用?
/brokers/ids:临时节点,保存所有broker节点信息,存储broker的物理地址、版本信息、启动时间
等,节点名称为brokerID,broker定时发送心跳到zk,如果断开则该brokerID会被删除
/brokers/topics:临时节点,节点保存broker节点下所有的topic信息,每一个topic节点下包含一个固
定的partitions节点,partitions的子节点就是topic的分区,每个分区下保存一个state节点、保存着当
前leader分区和ISR的brokerID,state节点由leader创建,若leader宕机该节点会被删除,直到有新的
leader选举产生、重新生成state节点
/consumers/[group_id]/owners/[topic]/[broker_id-partition_id]:维护消费者和分区的注册关系
/consumers/[group_id]/offsets/[topic]/[broker_id-partition_id]:分区消息的消费进度Offset
client通过topic找到topic树下的state节点、获取leader的brokerID,到broker树中找到broker的物理
地址,但是client不会直连zk,而是通过配置的broker获取到zk中的信息
等,节点名称为brokerID,broker定时发送心跳到zk,如果断开则该brokerID会被删除
/brokers/topics:临时节点,节点保存broker节点下所有的topic信息,每一个topic节点下包含一个固
定的partitions节点,partitions的子节点就是topic的分区,每个分区下保存一个state节点、保存着当
前leader分区和ISR的brokerID,state节点由leader创建,若leader宕机该节点会被删除,直到有新的
leader选举产生、重新生成state节点
/consumers/[group_id]/owners/[topic]/[broker_id-partition_id]:维护消费者和分区的注册关系
/consumers/[group_id]/offsets/[topic]/[broker_id-partition_id]:分区消息的消费进度Offset
client通过topic找到topic树下的state节点、获取leader的brokerID,到broker树中找到broker的物理
地址,但是client不会直连zk,而是通过配置的broker获取到zk中的信息
简单的说就是zk负责存储broker信息,topic信息,partitions信息,消费者和分区的注册关系,分区的消费进度
Kafka的reblance机制?
rebalance(重平衡)其实就是重新进行 partition 的分配,从而使得 partition 的分配重新达到平衡状态
触发条件:
当消费者组内的消费者数量发生变化(增加或者减少),就会产生重新分配patition
分区数量发生变化时(即 topic 的分区数量发生变化时)
当消费者组内的消费者数量发生变化(增加或者减少),就会产生重新分配patition
分区数量发生变化时(即 topic 的分区数量发生变化时)
Kafka的性能好在哪里?
kafka不基于内存,而是硬盘存储,因此消息堆积能力更强
顺序写:利用磁盘的顺序访问速度可以接近内存,kafka的消息都是append操作,partition是有序的,
节省了磁盘的寻道时间,同时通过批量操作、节省写入次数,partition物理上分为多个segment存储,
方便删除
节省了磁盘的寻道时间,同时通过批量操作、节省写入次数,partition物理上分为多个segment存储,
方便删除
零拷贝:
直接将内核缓冲区的数据发送到网卡传输
使用的是操作系统的指令支持
直接将内核缓冲区的数据发送到网卡传输
使用的是操作系统的指令支持
对应零拷贝技术有mmap及sendfile
mmap:小文件传输快
sendfile:大文件传输比mmap快
mmap:小文件传输快
sendfile:大文件传输比mmap快
kafka不太依赖jvm,主要理由操作系统的pageCache,如果生产消费速率相当,则直接用pageCache
交换数据,不需要经过磁盘IO
交换数据,不需要经过磁盘IO
Kafka如何应对大量的客户端连接?
Reactor多路复用
简单来说,就是搞⼀个 acceptor 线程,基于底层操作系统的⽀持,实现连接请求监听。 如果有某个设备发送了建⽴连接的请求过来,那么那个线程就把这个建⽴好的连接交给 processor 线程。 每个 processor 线程会被分配 N 多个连接,⼀个线程就可以负责维持 N 多个连接,他同样会基 于底层操作系统的⽀持监听 N 多连接的请求。 如果某个连接发送了请求过来,那么这个 processor 线程就会把请求放到⼀个请求队列⾥去。 接着后台有⼀个线程池,这个线程池⾥有⼯作线程,会从请求队列⾥获取请求,处理请求,接 着将请求对应的响应放到每个 processor 线程对应的⼀个响应队列⾥去。 最后,processor 线程会把⾃⼰的响应队列⾥的响应发送回给客户端。
Kafka的ISR机制是什么?
ISR 全称是 “In-Sync Replicas”,也就是保持同步的副本,他的含义就是,跟 Leader 始终保持同 步的 Follower 有哪些
Kafka与生产者的网络通信优化?
batch机制:多条消息打包成一个batch(默认16kb的大小)
request机制:多个batch打包成一个request
MinIO
MinIO怎么做分片上传?
1、数据库中存放文件路径,所有文件保存在 MINIO 中,文件名即是文件的 MD5。
2、当用户上传文件时,首先判断该文件信息是否存在在数据库中,如果存在则直接显示上传成功(急速上传),若不存在则执行上传操作。
3、文件在真正上传之前先判断文件大小,太小的不需要创建分片上传任务,一次性上传即可。
4、后台调用 MINIO 的 API 创建分片上传任务(得到一个任务 ID ),并为该任务生成分片上传链接(上传地址列表)后返回给前端,前端将对应分片按照到对应的连接传递到 MINIO 中。
5、分片上传成功后更新进度信息。
6、所有分片上传结束后,调用 MINIO 的 API 将当前任务的分片全部合并形成整个文件。
2、当用户上传文件时,首先判断该文件信息是否存在在数据库中,如果存在则直接显示上传成功(急速上传),若不存在则执行上传操作。
3、文件在真正上传之前先判断文件大小,太小的不需要创建分片上传任务,一次性上传即可。
4、后台调用 MINIO 的 API 创建分片上传任务(得到一个任务 ID ),并为该任务生成分片上传链接(上传地址列表)后返回给前端,前端将对应分片按照到对应的连接传递到 MINIO 中。
5、分片上传成功后更新进度信息。
6、所有分片上传结束后,调用 MINIO 的 API 将当前任务的分片全部合并形成整个文件。
MinIO怎么做断点续传?
MinIO如何保证高可用?
eureka
什么是Eureka?
Eureka作为SpringCloud的服务注册功能服务器,他是服务注册中心,系统中的其他服务使用Eureka的客户端将其连接到Eureka Service中,并且保持心跳,这样工作人员可以通过EurekaService来监控各个微服务是否运行正常。
Eureka怎么实现高可用?
集群吧,注册多台Eureka,然后把SpringCloud服务互相注册,客户端从Eureka获取信息时,按照Eureka的顺序来访问。
什么是Eureka的自我保护模式?
默认情况下,如果Eureka Service在一定时间内没有接收到某个微服务的心跳,Eureka Service会进入自我保护模式,在该模式下Eureka Service会保护服务注册表中的信息,不在删除注册表中的数据,当网络故障恢复后,Eureka Servic 节点会自动退出自我保护模式
DiscoveryClient的作用?
可以从注册中心中根据服务别名获取注册的服务器信息。
客户端向服务端注册Eureka它的注册请求会携带哪些信息?
Eureka它的服务端和客户端是如何保持续约的?
dubbo
dubbo怎么设置超时时间?
优先级
消费者Method>提供者method>消费者Reference>提供者Service>消费者全局配置provider>提供者全局配置consumer。
1:在服务提供者的实现累加 timeout的是ms
@DubboService(timeout = 10000)
public class ReportConfigServiceImpl implements ReportConfigService {
在消费者调用方加
@DubboReference(timeout = 10000)
ReportConfigService reportConfigService;
2:全局配置
生产者设置超时时间
dubbo.provider.timeout = 10000
消费者设置超时时间
dubbo.consumer.timeout = 10000
消费者Method>提供者method>消费者Reference>提供者Service>消费者全局配置provider>提供者全局配置consumer。
1:在服务提供者的实现累加 timeout的是ms
@DubboService(timeout = 10000)
public class ReportConfigServiceImpl implements ReportConfigService {
在消费者调用方加
@DubboReference(timeout = 10000)
ReportConfigService reportConfigService;
2:全局配置
生产者设置超时时间
dubbo.provider.timeout = 10000
消费者设置超时时间
dubbo.consumer.timeout = 10000
zookeeper
1.zookeeper工作机制
Zookeeper从设计模式角度来理解:是-一个基于观察者模式设计的分布式服务管理框架,它负责存储和管理大家都关心的数据,然后接受观察者的注册,一旦这些数据的状态发生变化,Zookeeper就将负责通知已经在Zookeeper.上注册的那些观察者做出相应的反应。
1.各服务启动的时候去zk注册信息(创建的都是临时节点)
2.客户端获取当前在线的服务列表,并注册监听事件
3.如果服务器节点下线
4.发送服务器节点上下线事件通知
5.客户端重新再去获取服务器列表,并注册监听
2.zookeeper的特点
1.Zookeeper: 一个领导者(Leader) ,多个跟随者(Follower) 组成的集群。
2.集群中只要有半数以上节点存活,Zookeeper集 群就能正常服务
3.全局数据一致:每个Server保存一份相同的数据副本,Client 无论连接到哪个Server,数据都是一致的。
4.更新请求顺序进行,来自同一个Clent的更新请求按其发送顺序依次执行。
5.数据更新原子性,一次数据更新要么成功,要么失败。
6.实时性,在一定时间范围内,Client 能读到最新数据。
3.zookeeper的数据结构
ZooKeeper数据模型的结构与Unix文件系统很类似,整体上可以看作是一棵树,每个节点称做一
个ZNode。每一个ZNode默认能够存储1MB的数据 ,每个ZNode都可以通过其路径唯一标识。
个ZNode。每一个ZNode默认能够存储1MB的数据 ,每个ZNode都可以通过其路径唯一标识。
4.zookeeper的应用场景
1.统一命名服务
在分布式环境下,经常需要对应用/服务进行统一命名 ,便于识别。
例如: IP不容易记住,而域名容易记住。
例如: IP不容易记住,而域名容易记住。
2.统一集群管理、统一配置管理
1.分布式环境下,配置文件同步非常常见。
( 1 )-般要求- -个集群中,所有节点的配置信息是一致的, 比如Kafka集群。
( 2)对配置文件修改后,希望能够快速同步到各个节点上。
( 1 )-般要求- -个集群中,所有节点的配置信息是一致的, 比如Kafka集群。
( 2)对配置文件修改后,希望能够快速同步到各个节点上。
2.配置管理可交由ZooKeeper实现。
( 1 )可将配置信息写入ZooKeeper上的一-个Znode.
(2)各个客户端服务器监听这个Znode.
( 3 ) - -旦Znode中的数据被修改, ZooKeeper将通知
各个客户端服务器。
( 1 )可将配置信息写入ZooKeeper上的一-个Znode.
(2)各个客户端服务器监听这个Znode.
( 3 ) - -旦Znode中的数据被修改, ZooKeeper将通知
各个客户端服务器。
ZooKeeper可以实现实时监控节点状态变化
( 1 )可将节点信息写入ZooKeeper,上的一一个ZNode.
(2)监听这个ZNode可获取它的实时状态变化。
( 1 )可将节点信息写入ZooKeeper,上的一一个ZNode.
(2)监听这个ZNode可获取它的实时状态变化。
3.服务动态上下线
4.软负载均衡
5.配置参数解读
Zookeeper中的配置文件zoo.cfg中
1.tickTime =2000:通信心跳数,Zookeeper 服务器与客户端心跳时间,单位毫秒
Zookeeper使用的基本时间,服务器之间或客户端与服务器之间维持心跳的时间间隔,也就是每个tickTime时间就会发送一个心跳,时间单位为毫秒。
它用于心跳机制,并且设置最小的session超时时间为两倍心跳时间。(session的最小超时时间是2*tickTime)
2.initLimit =10:LF 初始通信时限
集群中的Follower跟随者服务器与Leader领导者服务器之间初始连接时能容忍的最多心跳数(tickTime的数量),用它来限定集群中的Zookeeper服务器接到Leader的时限。
3.syncLimit =5:LF 同步通信时限
集群中Leader与Follower之间的最大响应时间单位,假如响应超过syncLimit * tickTime,Leader认为Follwer死掉,从服务器列表中删除Follwer。
4.dataDir:数据文件目录+数据持久化路径
主要用于保存 Zookeeper 中的数据。
5.clientPort =2181:客户端连接端口
6.zk内部原理
1.Zab协议
1.消息广播模式
如果你了解过2PC协议的话,理解起来就简单很多了,消息广播的过程实际上是一个简化版本的二阶段提交过程。
通俗的理解就比较简单了,我是领导,我要向各位传达指令,不过传达之前我先问一下大家支不支持我,若有一半以上的人支持我,那我就向各位传达指令了
通俗的理解就比较简单了,我是领导,我要向各位传达指令,不过传达之前我先问一下大家支不支持我,若有一半以上的人支持我,那我就向各位传达指令了
1.Leader将客户端的request转化成一个Proposal(提议),leader首先把proposal发送到FIFO队列里
2.FIFO取出队头proposal给Follower;Follower反馈一个ACK给队列;队列把ACK交给leader
3.leader收到半数以上ACK,就会发送commit指令给FIFO队列;FIFO队列把commit给Follower。
2.崩溃恢复模式
leader就是一个领导,既然领导挂了,整个组织肯定不会散架,毕竟离开谁都能活下去是不是,这时候我们只需要选举一个新的领导即可,而且还要把前leader还未完成的工作做完,也就是说不仅要进行leader服务器选取,而且还要进行崩溃恢复。我们一个一个来解决
1.leader选举
looking状态:也就是观望状态,这时候是由于组织出现内部问题,那就停下来,做一些其他的事。
following状态:自身是一个组织成员,做自己的事。
leading状态:自身是-个组织老大,做自己的事。
following状态:自身是一个组织成员,做自己的事。
leading状态:自身是-个组织老大,做自己的事。
这就是整个选举的过程。并且每个人的选举,都代表了一个事件,为了保证分布式系统的时间有序性,因此给每一个事件都分配了一个Zxid。相当于编了-个号。32位是按照数字递增,即每次客户端发起一个proposal, 低32位的数字简单加1。高32位是leader周期的epoch编号。
每当选举出一个新的eader时, 新的leader就从本地事物日志中取出ZXID然后解析出高32位的epoch编号,进行加1,再将32位的全部设置为0。这样就保证了每次新选举的leader后,保证了ZXID的唯一性而且是保证递增的。
2.崩溃恢复
既然要恢复,有些场景是不能恢复的,ZAB协议崩溃恢复要求满足如下2个要求:
第一:确保已经被leader 提交的proposal必须最终被所有的follower服务器提交。
第二:确保丢弃已经被leader出的但是没有被提交的proposal。
第一:确保已经被leader 提交的proposal必须最终被所有的follower服务器提交。
第二:确保丢弃已经被leader出的但是没有被提交的proposal。
第一步:选取当前取出最大的ZXID,代表当前的事件是最新的。
第二步:新leader把这个事件proposal提交给其他的follower节点
第三步: follower节点会根据leader的消息进行回退或者是数据同步操作。最终目的要保证集群中所有节点的数据副本保持一致。
这就是整个恢复的过程,其实就是相当于有个日志一样的东西, 记录每一次操作, 然后把出事前的最新操作恢复,然后进行同步即可。
第二步:新leader把这个事件proposal提交给其他的follower节点
第三步: follower节点会根据leader的消息进行回退或者是数据同步操作。最终目的要保证集群中所有节点的数据副本保持一致。
这就是整个恢复的过程,其实就是相当于有个日志一样的东西, 记录每一次操作, 然后把出事前的最新操作恢复,然后进行同步即可。
2.节点类型
持久(Persistent):客户端和服务器端断开连接后,创建的节点不删除
短暂(Ephemeral):客户端和服务器端断开连接后,创建的节点自己删除
说明:创建znode时设置顺序标识,znode名称后会附加一个值,顺序号是一个单调递增的计数器,由父节点维护
注意:在分布式系统中,顺序号可以被用于为所有的事件进行全局排序,这样客户端可以通过顺序号推断事件的顺序
3.选举机制(面试重点)
1.半数机制:集群中半数以上机器存活,集群可用。所以 Zookeeper 适合安装奇数台服务器
2.Zookeeper 虽然在配置文件中并没有指定 Master 和 Slave。但是,Zookeeper 工作时,是有一个节点为 Leader,其他则为 Follower,Leader 是通过内部的选举机制临时产生的。
3.选举机制举例
场景:假设有五台服务器组成的 Zookeeper 集群,它们的 id 从 1-5,同时它们都是最新启动的,也就是没有历史数据,在存放数据量这一点上,都是一样的。假设这些服务器依序启动,来看看会发生什么
1.服务器 1 启动,发起一次选举。服务器 1 投自己一票。此时服务器 1 票数一票,不够半数以上(3 票),选举无法完成,服务器 1 状态保持为 LOOKING
2.服务器 2 启动,再发起一次选举。服务器 1 和 2 分别投自己一票并交换选票信息:此时服务器 1 发现服务器 2 的 ID 比自己目前投票推举的(服务器 1)大,更改选票为推举服务器 2。此时服务器 1 票数 0 票,服务器 2 票数 2 票,没有半数以上结果,选举无法完成,服务器 1,2 状态保持 LOOKING
3.服务器 3 启动,发起一次选举。此时服务器 1 和 2 都会更改选票为服务器 3。此次投票结果:服务器 1 为 0 票,服务器 2 为 0 票,服务器 3 为 3 票。此时服务器 3 的票数已经超过半数,服务器 3 当选 Leader。服务器 1,2 更改状态为 FOLLOWING,服务器 3 更改状态为 LEADING;
4.服务器 4 启动,发起一次选举。此时服务器 1,2,3 已经不是 LOOKING 状态,不会更改选票信息。交换选票信息结果:服务器 3 为 3 票,服务器 4 为 1 票。此时服务器 4服从多数,更改选票信息为服务器 3,并更改状态为 FOLLOWING;
5.服务器 5 启动,同 4 一样当小弟。
4.监听器原理(面试重点)
1.首先要有一个main()线程
2.在main线程中创建Zookeeper客户端,这时就会创建两个线程,一个负责网络连接通信(connet),一个负责监听(listener)
3.通过connect线程将注册的监听事件发送给Zookeeper。
4.在Zookeeper的注册监听器列表中将注册的监听事件添加到列表中。
5.Zookeeper监听到有数据或路径变化,就会将这个消息发送给listener线程。
6.listener线程内部调用了process()方法。
注:常见的监听
1.监听节点数据的变化 get path[watch]
2.监听子节点增减的变化 ls path[watch]
5.写数据流程
1.Client向ZooKeeper的Server1上写数据,发送一个写请求。
2.如果Server1不是Leader,那么Server1会把接受到的请求进一步转发给Leader,因为每个ZooKeeper的Server里面有一个是Leader。这个Leader会将写请求广播给各个Server,比如Server1和Server2,各个Server会将该写请求加入待写队列,并向Leader发送成功信息。
3.当Leader收到半数以上Server的成功信息,说明该写操作可以执行。Leader会向各个Server发送提交信息,各个Server收到信息后会落实队列里的写请求,此时写成功。
4.Server1会进一步通知Client数据写成功了,这时就认为整个写操作成功。
7.zk的常用命令
ls path [watch] :使用 ls 命令来查看当前 znode 中所包含的内容
ls2 path [watch]:查看当前节点数据并能看到更新次数等数据
create:
普通创建
-s 含有序列
-e 临时(重启或者超时消失)
-s 含有序列
-e 临时(重启或者超时消失)
get path [watch]:获得节点的值
set:设置节点的具体值
stat:查看节点状态
delete:删除节点
rmr:递归删除节点
4.Eureka和ZooKeeper都可以提供服务注册
与发现的功能,请说说两个的区别?
与发现的功能,请说说两个的区别?
1. ZooKeeper中的节点服务挂了就要选举 在选举期间注册服务瘫痪,虽然服务最终会恢复,但是选举期间不可用的, 选举就是改微服务做了集群,必须有一台主其他的都是从
2. Eureka各个节点是平等关系,服务器挂了没关系,只要有一台Eureka就可以保证服务可用,数据都是最新的。 如果查询到的数据并不是最新的,就是因为Eureka的自我保护模式导致的
3. Eureka本质上是一个工程,而ZooKeeper只是一个进程
4. Eureka可以很好的应对因网络故障导致部分节点失去联系的情况,而不会像ZooKeeper 一样使得整个注册系统瘫痪
5. ZooKeeper保证的是CP,Eureka保证的是AP;CAP: C:一致性>Consistency; 取舍:(强一致性、单调一致性、会话一致性、最终一致性、弱一致性) A:可用性>Availability; P:分区容错性>Partition tolerance;
Config
1.什么是Spring Cloud Config?
Spring Cloud Config为分布式系统中的外部配置提供服务器和客户端支持,可以方便的对微服务各个环境下的配置进行集中式管理。Spring Cloud Config分为Config Server和Config Client两部分。Config Server负责读取配置文件,并且暴露Http API接口,Config Client通过调用ConfigServer的接口来读取配置文件。
2.分布式配置中心有那些框架?
Apollo、zookeeper、springcloud config、nacos
3.
动态变更项目配置信息而不必重新部署项目。
4.SpringCloud Config 可以实现实时刷新吗?
springcloud config实时刷新采用SpringCloud Bus消息总线。
Feign
什么是Feign?
Feign 是一个声明web服务客户端,这使得编写web服务客户端更容易
他将我们需要调用的服务方法定义成抽象方法保存在本地就可以了,不需要自己构建Http请求了,直接调用接口就行了,不过要注意,调用方法要和本地抽象方法的签名完全一致。
SpringCloud有几种调用接口方式?
RestTemplate
Feign
Ribbon和Feign调用服务的区别?
1.调用方式同:Ribbon需要我们自己构建Http请求,模拟Http请求然后通过RestTemplate发给其他服务,步骤相当繁琐
2.而Feign则是在Ribbon的基础上进行了一次改进,采用接口的形式,将我们需要调用的服务方法定义成抽象方法保存在本地就可以了,不需要自己构建Http请求了,直接调用接口就行了,不过要注意,调用方法要和本地抽象方法的签名完全一致。
Feign的远程调用的实现流程?
主程序入口添加了@EnableFeignClients注解开启对FeignClient扫描加载处理。根据Feign Client的开发规范,定义接口并加@FeignClientd注解。
当程序启动时,会进行包扫描,扫描所有@FeignClients的注解的类,并且将这些信息注入Spring IOC容器中,当定义的的Feign接口中的方法被调用时,通过JDK的代理方式,来生成具体的RequestTemplate.
当生成代理时,Feign会为每个接口方法创建一个RequestTemplate。当生成代理时,Feign会为每个接口方法创建一个RequestTemplate对象,该对象封装了HTTP请求需要的全部信息,如请求参数名,请求方法等信息都是在这个过程中确定的。
然后RequestTemplate生成Request,然后把Request交给Client去处理,这里指的是Client可以是JDK原生的URLConnection,Apache的HttpClient,也可以是OKhttp,最后Client被封装到LoadBalanceClient类,这个类结合Ribbon负载均衡发起服务之间的调用。
当程序启动时,会进行包扫描,扫描所有@FeignClients的注解的类,并且将这些信息注入Spring IOC容器中,当定义的的Feign接口中的方法被调用时,通过JDK的代理方式,来生成具体的RequestTemplate.
当生成代理时,Feign会为每个接口方法创建一个RequestTemplate。当生成代理时,Feign会为每个接口方法创建一个RequestTemplate对象,该对象封装了HTTP请求需要的全部信息,如请求参数名,请求方法等信息都是在这个过程中确定的。
然后RequestTemplate生成Request,然后把Request交给Client去处理,这里指的是Client可以是JDK原生的URLConnection,Apache的HttpClient,也可以是OKhttp,最后Client被封装到LoadBalanceClient类,这个类结合Ribbon负载均衡发起服务之间的调用。
Feign使用步骤?
添加feigh客户端依赖
启动类上添加feign注解:@EnableFeignClients
application.yml配置文件添加配置信息
创建一个feign客户端,用于引入远程模块的接口:@FeignClient
在需要进行远程调用的方法里注入该接口,并调用对应的api接口方法
启动类上添加feign注解:@EnableFeignClients
application.yml配置文件添加配置信息
创建一个feign客户端,用于引入远程模块的接口:@FeignClient
在需要进行远程调用的方法里注入该接口,并调用对应的api接口方法
Feign的四种日志级别?
- none:不记录任何日志信息,这是默认值。
- basic:仅记录请求的方法,URL以及响应状态码和执行时间
- headers:在BASIC的基础上,额外记录了请求和响应的头信息
- full:记录所有请求和响应的明细,包括头信息、请求体、元数据。
- basic:仅记录请求的方法,URL以及响应状态码和执行时间
- headers:在BASIC的基础上,额外记录了请求和响应的头信息
- full:记录所有请求和响应的明细,包括头信息、请求体、元数据。
Feign的调优
它默认使用的是URLConnection,它是没有连接池的
我们只需要使用HttpClient或者OKHttp带连接池的替换掉它就好了
日志级别尽量使用basic
步骤:
1.引入feign-httpClient依赖
2.配置文件开启httpClient功能,设置连接池参数,参数有:最大的连接数,每个路径的最大连接数
我们只需要使用HttpClient或者OKHttp带连接池的替换掉它就好了
日志级别尽量使用basic
步骤:
1.引入feign-httpClient依赖
2.配置文件开启httpClient功能,设置连接池参数,参数有:最大的连接数,每个路径的最大连接数
Hystrix
么是断路器?
当一个服务调用另一个服务由于网络原因或自身原因出现问题,调用者就会等待被调用者的响应当更多的服务请求到这些资源导致更多的请求等待,发生连锁效应(雪崩效应)
断路器有三种状态:
打开状态:一段时间内 达到一定的次数无法调用 并且多次监测没有恢复的迹象 断路器完全打开 那么下次请求就不会请求到该服务
半开状态:短时间内 有恢复迹象 断路器会将部分请求发给该服务,正常调用时 断路器关闭
关闭状态:当服务一直处于正常状态 能正常调用
打开状态:一段时间内 达到一定的次数无法调用 并且多次监测没有恢复的迹象 断路器完全打开 那么下次请求就不会请求到该服务
半开状态:短时间内 有恢复迹象 断路器会将部分请求发给该服务,正常调用时 断路器关闭
关闭状态:当服务一直处于正常状态 能正常调用
什么是 Hystrix?
在分布式系统,我们一定会依赖各种服务,那么这些个服务一定会出现失败的情况,就会导致雪崩,Hystrix就是这样的一个工具,防雪崩利器,它具有服务降级,服务熔断,服务隔离,监控等一些防止雪崩的技术。
Hystrix有四种防雪崩方式:
服务熔断:接口调用失败就会进入调用接口提前定义好的一个熔断的方法,返回错误信息
服务降级:接口调用失败就调用本地的方法返回一个空
服务隔离:隔离服务之间相互影响
服务监控:在服务发生调用时,会将每秒请求数、成功请求数等运行指标记录下来。
谈谈服务雪崩效应?
雪崩效应是在大型互联网项目中,当某个服务发生宕机时,调用这个服务的其他服务也会发生宕机,大型项目的微服务之间的调用是互通的,这样就会将服务的不可用逐步扩大到各个其他服务中,从而使整个项目的服务宕机崩溃.发生雪崩效应的原因有以下几点
1.单个服务的代码存在bug. 2请求访问量激增导致服务发生崩溃(如大型商城的枪红包,秒杀功能). 3.服务器的硬件故障也会导致部分服务不可用.
在微服务中,如何保护服务?
一般使用使用Hystrix框架,实现服务隔离来避免出现服务的雪崩效应,从而达到保护服务的效果。当微服务中,高并发的数据库访问量导致服务线程阻塞,使单个服务宕机,服务的不可用会蔓延到其他服务,引起整体服务灾难性后果,使用服务降级能有效为不同的服务分配资源,一旦服务不可用则返回友好提示,不占用其他服务资源,从而避免单个服务崩溃引发整体服务的不可用.
谈谈服务降级、服务熔断、服务隔离
服务降级:当客户端请求服务器端的时候,防止客户端一直等待,不会处理业务逻辑代码,直接返回一个友好的提示给客户端
服务熔断:是在服务降级的基础上更直接的一种保护方式,当在一个统计时间范围内的请求失败数量达到设定值(requestVolumeThreshold)或当前的请求错误率达到设定的错误率阈值(errorThresholdPercentage)时开启断路,之后的请求直接走fallback方法,在设定时间(sleepWindowInMilliseconds)后尝试恢复。
服务隔离:就是Hystrix为隔离的服务开启一个独立的线程池,这样在高并发的情况下不会影响其他服务。服务隔离有线程池和信号量两种实现方式,一般使用线程池方式
服务降级底层是如何实现的?
Hystrix实现服务降级的功能是通过重写HystrixCommand中的getFallback()方法,当Hystrix的run方法或construct执行发生错误时转而执行getFallback()方法。
Hystrix使用场景?
Ribbon
1.负载平衡的意义什么?
简单来说: 先将集群,集群就是把一个的事情交给多个人去做,假如要做1000个产品给一个人做要10天,我叫10个人做就是一天,这就是集群,负载均衡的话就是用来控制集群,他把做的最多的人让他慢慢做休息会,把做的最少的人让他加量让他做多点。
在计算中,负载平衡可以改善跨计算机,计算机集群,网络链接,中央处理单元或磁盘驱动器等多种计算资源的工作负载分布。负载平衡旨在优化资源使用,最大化吞吐量,最小化响应时间并避免任何单一资源的过载。使用多个组件进行负载平衡而不是单个组件可能会通过冗余来提高可靠性和可用性。负载平衡通常涉及专用软件或硬件,例如多层交换机或域名系统服务器进程。
2.Ribbon是什么?
Ribbon是Netflix发布的开源项目,主要功能是提供客户端的软件负载均衡算法
Ribbon客户端组件提供一系列完善的配置项,如连接超时,重试等。简单的说,就是在配置文件
中列出后面所有的机器,Ribbon会自动的帮助你基于某种规则(如简单轮询,随即连接等)去连
接这些机器。我们也很容易使用Ribbon实现自定义的负载均衡算法。(有点类似Nginx)
Ribbon客户端组件提供一系列完善的配置项,如连接超时,重试等。简单的说,就是在配置文件
中列出后面所有的机器,Ribbon会自动的帮助你基于某种规则(如简单轮询,随即连接等)去连
接这些机器。我们也很容易使用Ribbon实现自定义的负载均衡算法。(有点类似Nginx)
3.Nginx与Ribbon的区别?
答:Nginx是反向代理同时可以实现负载均衡,nginx拦截客户端请求采用负载均衡策略根据upstream配置进行转发,相当于请求通过nginx服务器进行转发。Ribbon是客户端负载均衡,从注册中心读取目标服务器信息,然后客户端采用轮询策略对服务直接访问,全程在客户端操作。
4.Ribbon底层实现原理?
答:Ribbon使用discoveryClient从注册中心读取目标服务信息,对同一接口请求进行计数,使用%取余算法获取目标服务集群索引,返回获取到的目标服务信息。
5.@LoadBalanced注解的作用?
答:开启客户端负载均衡
gateway网关
gateway介绍
Spring Cloud Gateway是Spring Cloud官方推出的第二代网关框架,取代Zuul网关。网关作为流量的,在微服务系统中有着非常作用,网关常见的功能有路由转发、权限校验、限流控制等作用。
使用了一个RouteLocatorBuilder的bean去创建路由,除了创建路由RouteLocatorBuilder可以让你添加各种predicates和filters,predicates断言的意思,顾名思义就是根据具体的请求的规则,由具体的route去处理,filters是各种过滤器,用来对请求做各种判断和修改。
网关的作用是什么?
统一管理微服务请求,权限控制、负载均衡、路由转发、监控、安全控制黑名单和白名单等
网关的应用场景有哪些?
对外暴露,权限校验,服务聚合,日志审计等
网关与过滤器有什么区别?
网关是对所有服务的请求进行分析过滤,过滤器是对单个服务而言。
如何实现动态gateway网关路由转发
通过path配置拦截请求,通过ServiceId到配置中心获取转发的服务列表,gateway内部使用Ribbon实
现本地负载均衡和转发。
现本地负载均衡和转发。
gateway如何实现鉴权检验?
1.客户端携带用户密码登录之后,在服务端进行账号密码的验证。
2.如果登录成功,则根据用户密码生成token,同时将token写入redis缓存,设置过期时间。
3.客户端携带token请求业务系统,经过网关服务,从redis缓存中读取并对token进行验证;
4.token验证成功,则路由到业务系统;验证失败则返回权限校验失败的错误码401;
gateway网关如何进行限流?
在 Spring Cloud Gateway 上实现限流是个不错的选择,只需要编写一个过滤器就可以了。Spring Cloud Gateway 已经内置了一个RequestRateLimiterGatewayFilterFactory,我们可以直接使用。目前RequestRateLimiterGatewayFilterFactory的实现依赖于 Redis,所以我们还要引入spring-boot-starter-data-redis-reactive。
在上面的配置文件,配置了 redis的信息,并配置了RequestRateLimiter的限流过滤器,该过滤器需要配置三个参数:
burstCapacity:令牌桶总容量。
replenishRate:令牌桶每秒填充平均速率。
key-resolver:用于限流的键的解析器的 Bean 对象的名字。它使用 SpEL 表达式根据#{@beanName}从 Spring 容器中获取 Bean 对象。
burstCapacity:令牌桶总容量。
replenishRate:令牌桶每秒填充平均速率。
key-resolver:用于限流的键的解析器的 Bean 对象的名字。它使用 SpEL 表达式根据#{@beanName}从 Spring 容器中获取 Bean 对象。
可以按照path接口地址限流、可以根据ip来限流或者自定义限流器
配置文件
spring:
cloud:
gateway:
discovery:
locator:
#是否与服务发现组件进行结合,通过serviceId转发到具体实例
#是否开启基于服务发现的路由规则
enabled: true
##表示将请求路径的服务名配置改成小写 ,因为服务注册的时候,向注册中心注册时将服务名转成大写的了
lowerCaseServiceId: true
routes:
- id: after_route
uri: lb://bit-msa-pasm-api
predicates:
- Path=/service/**
filters:
- name: RequestRateLimiter
args:
# 令牌桶每秒填充平均速率
redis-rate-limiter.replenishRate: 1
# 令牌桶的上限
redis-rate-limiter.burstCapacity: 2
# 使用SpEL表达式从Spring容器中获取Bean对象
key-resolver: "#{@pathKeyResolver}"
cloud:
gateway:
discovery:
locator:
#是否与服务发现组件进行结合,通过serviceId转发到具体实例
#是否开启基于服务发现的路由规则
enabled: true
##表示将请求路径的服务名配置改成小写 ,因为服务注册的时候,向注册中心注册时将服务名转成大写的了
lowerCaseServiceId: true
routes:
- id: after_route
uri: lb://bit-msa-pasm-api
predicates:
- Path=/service/**
filters:
- name: RequestRateLimiter
args:
# 令牌桶每秒填充平均速率
redis-rate-limiter.replenishRate: 1
# 令牌桶的上限
redis-rate-limiter.burstCapacity: 2
# 使用SpEL表达式从Spring容器中获取Bean对象
key-resolver: "#{@pathKeyResolver}"
持续高速访问某个路径,速度过快时,返回 HTTP ERROR 429 。
gateway网关如何进行黑名单、白名单设置?
自定义全局过滤器实现IP访问限制(黑白名单)
黑名单实际可以去数据库或者redis中查询
思路:获取客户端ip,判断是否在⿊名单中,在的话就拒绝访问,不在的话就放⾏
// 从上下⽂中取出request和response对象
// 从request对象中获取客户端ip
// 拿着clientIp去⿊名单中查询,存在的话就决绝访问
// 从上下⽂中取出request和response对象
// 从request对象中获取客户端ip
// 拿着clientIp去⿊名单中查询,存在的话就决绝访问
gateway的过滤器
gateway的过滤器使用场景
gateway网关中有哪些必须的配置?
Seata分布式
分布式幂等性如何设计?
在高并发场景的架构里,幂等性是必须得保证的。比如说支付功能,用户发起支付,如果后台没有
做幂等校验,刚好用户手抖多点了几下,于是后台就可能多次受到同一个订单请求,不做幂等很容
易就让用户重复支付了,这样用户是肯定不能忍的。
做幂等校验,刚好用户手抖多点了几下,于是后台就可能多次受到同一个订单请求,不做幂等很容
易就让用户重复支付了,这样用户是肯定不能忍的。
解决方案:
1,查询和删除不在幂等讨论范围,查询肯定没有幂等的说,删除:第一次删除成功后,后面来删
除直接返回0,也是返回成功。
2,建唯一索引:唯一索引或唯一组合索引来防止新增数据存在脏数据 (当表存在唯一索引,并发
时新增异常时,再查询一次就可以了,数据应该已经存在了,返回结果即可)。
3,token机制:由于重复点击或者网络重发,或者nginx重发等情况会导致数据被重复提交。前端
在数据提交前要向后端服务的申请token,token放到 Redis 或 JVM 内存,token有效时间。提交后
后台校验token,同时删除token,生成新的token返回。redis要用删除操作来判断token,删除成
功代表token校验通过,如果用select+delete来校验token,存在并发问题,不建议使用。
4,悲观锁
悲观锁使用时一般伴随事务一起使用,数据锁定时间可能会很长,根据实际情况选用(另外还要考
虑id是否为主键,如果id不是主键或者不是 InnoDB 存储引擎,那么就会出现锁全表)。
select id ,name from table_# where id='##' for update;
5,乐观锁,给数据库表增加一个version字段,可以通过这个字段来判断是否已经被修改了
update table_xxx set name=#name#,version=version+1 where version=#version#
6,分布式锁,比如 Redis 、 Zookeeper 的分布式锁。单号为key,然后给Key设置有效期(防止支
付失败后,锁一直不释放),来一个请求使用订单号生成一把锁,业务代码执行完成后再释放锁。
7,保底方案,先查询是否存在此单,不存在进行支付,存在就直接返回支付结果。
1,查询和删除不在幂等讨论范围,查询肯定没有幂等的说,删除:第一次删除成功后,后面来删
除直接返回0,也是返回成功。
2,建唯一索引:唯一索引或唯一组合索引来防止新增数据存在脏数据 (当表存在唯一索引,并发
时新增异常时,再查询一次就可以了,数据应该已经存在了,返回结果即可)。
3,token机制:由于重复点击或者网络重发,或者nginx重发等情况会导致数据被重复提交。前端
在数据提交前要向后端服务的申请token,token放到 Redis 或 JVM 内存,token有效时间。提交后
后台校验token,同时删除token,生成新的token返回。redis要用删除操作来判断token,删除成
功代表token校验通过,如果用select+delete来校验token,存在并发问题,不建议使用。
4,悲观锁
悲观锁使用时一般伴随事务一起使用,数据锁定时间可能会很长,根据实际情况选用(另外还要考
虑id是否为主键,如果id不是主键或者不是 InnoDB 存储引擎,那么就会出现锁全表)。
select id ,name from table_# where id='##' for update;
5,乐观锁,给数据库表增加一个version字段,可以通过这个字段来判断是否已经被修改了
update table_xxx set name=#name#,version=version+1 where version=#version#
6,分布式锁,比如 Redis 、 Zookeeper 的分布式锁。单号为key,然后给Key设置有效期(防止支
付失败后,锁一直不释放),来一个请求使用订单号生成一把锁,业务代码执行完成后再释放锁。
7,保底方案,先查询是否存在此单,不存在进行支付,存在就直接返回支付结果。
简单一次完整的 HTTP 请求所经历的步骤?
1、 DNS 解析(通过访问的域名找出其 IP 地址,递归搜索)。 2、HTTP 请求,当输入一个请求时,建立一个 Socket 连接发起 TCP的 3 次握手。
如果是 HTTPS 请求,会略微有不同。等到 HTTPS 小节,我们在来讲。
3.1、客户端向服务器发送请求命令(一般是 GET 或 POST 请求)。
这个是补充内容,面试一般不用回答。
客户端的网络层不用关心应用层或者传输层的东西,主要做的是通过查找路由表确定如何
到达服务器,期间可能经过多个路由器,这些都是由路由器来完成的工作,我不作过多的
描述,无非就是通过查找路由表决定通过那个路径到达服务器。
客户端的链路层,包通过链路层发送到路由器,通过邻居协议查找给定 IP 地址的 MAC 地
址,然后发送 ARP 请求查找目的地址,如果得到回应后就可以使用 ARP 的请求应答交换
的 IP 数据包现在就可以传输了,然后发送 IP 数据包到达服务器的地址。
3.2、客户端发送请求头信息和数据。
4.1、服务器发送应答头信息。
4.2、服务器向客户端发送数据。
5、服务器关闭 TCP 连接(4次挥手)。
这里是否关闭 TCP 连接,也根据 HTTP Keep-Alive 机制有关。
同时,客户端也可以主动发起关闭 TCP 连接。
6、客户端根据返回的 HTML 、 CSS 、 JS 进行渲染。
如果是 HTTPS 请求,会略微有不同。等到 HTTPS 小节,我们在来讲。
3.1、客户端向服务器发送请求命令(一般是 GET 或 POST 请求)。
这个是补充内容,面试一般不用回答。
客户端的网络层不用关心应用层或者传输层的东西,主要做的是通过查找路由表确定如何
到达服务器,期间可能经过多个路由器,这些都是由路由器来完成的工作,我不作过多的
描述,无非就是通过查找路由表决定通过那个路径到达服务器。
客户端的链路层,包通过链路层发送到路由器,通过邻居协议查找给定 IP 地址的 MAC 地
址,然后发送 ARP 请求查找目的地址,如果得到回应后就可以使用 ARP 的请求应答交换
的 IP 数据包现在就可以传输了,然后发送 IP 数据包到达服务器的地址。
3.2、客户端发送请求头信息和数据。
4.1、服务器发送应答头信息。
4.2、服务器向客户端发送数据。
5、服务器关闭 TCP 连接(4次挥手)。
这里是否关闭 TCP 连接,也根据 HTTP Keep-Alive 机制有关。
同时,客户端也可以主动发起关闭 TCP 连接。
6、客户端根据返回的 HTML 、 CSS 、 JS 进行渲染。
说说你对分布式事务的了解
ACID
指数据库事务正确执行的四个基本要素:
1. 原子性(Atomicity) 2. 一致性(Consistency) 3. 隔离性(Isolation) 4. 持久性(Durability)
1. 原子性(Atomicity) 2. 一致性(Consistency) 3. 隔离性(Isolation) 4. 持久性(Durability)
CAP
CAP原则又称CAP定理,指的是在一个分布式系统中,一致性(Consistency)、可用性
(Availability)、分区容忍性(Partition tolerance)。CAP 原则指的是,这三个要素最多只能同
时实现两点,不可能三者兼顾。
一致性:在分布式系统中的所有数据备份,在同一时刻是否同样的值。
可用性:在集群中一部分节点故障后,集群整体是否还能响应客户端的读写请求。
分区容忍性:以实际效果而言,分区相当于对通信的时限要求。系统如果不能在时限内达成数
据一致性,就意味着发生了分区的情况,必须就当前操作在C和A之间做出选择。
(Availability)、分区容忍性(Partition tolerance)。CAP 原则指的是,这三个要素最多只能同
时实现两点,不可能三者兼顾。
一致性:在分布式系统中的所有数据备份,在同一时刻是否同样的值。
可用性:在集群中一部分节点故障后,集群整体是否还能响应客户端的读写请求。
分区容忍性:以实际效果而言,分区相当于对通信的时限要求。系统如果不能在时限内达成数
据一致性,就意味着发生了分区的情况,必须就当前操作在C和A之间做出选择。
BASE理论
BASE理论是对CAP中的一致性和可用性进行一个权衡的结果,理论的核心思想就是:我们无法做到
强一致,但每个应用都可以根据自身的业务特点,采用适当的方式来使系统达到最终一致性。
Basically Available(基本可用)
Soft state(软状态)
Eventually consistent(最终一致性)
强一致,但每个应用都可以根据自身的业务特点,采用适当的方式来使系统达到最终一致性。
Basically Available(基本可用)
Soft state(软状态)
Eventually consistent(最终一致性)
你知道哪些分布式事务解决方案?
1. 两阶段提交(2PC)
2. 三阶段提交(3PC)
3. 补偿事务(TCC=Try-Confirm-Cancel)
4. 本地消息队列表(MQ)
1. 两阶段提交(2PC)
2. 三阶段提交(3PC)
3. 补偿事务(TCC=Try-Confirm-Cancel)
4. 本地消息队列表(MQ)
什么是二阶段提交?
两阶段提交2PC是分布式事务中最强大的事务类型之一,两段提交就是分两个阶段提交:
第一阶段询问各个事务数据源是否准备好。
第二阶段才真正将数据提交给事务数据源。
为了保证该事务可以满足ACID,就要引入一个协调者(Cooradinator)。其他的节点被称为参与者
(Participant)。协调者负责调度参与者的行为,并最终决定这些参与者是否要把事务进行提交。
第一阶段询问各个事务数据源是否准备好。
第二阶段才真正将数据提交给事务数据源。
为了保证该事务可以满足ACID,就要引入一个协调者(Cooradinator)。其他的节点被称为参与者
(Participant)。协调者负责调度参与者的行为,并最终决定这些参与者是否要把事务进行提交。
阶段一
a) 协调者向所有参与者发送事务内容,询问是否可以提交事务,并等待答复。
b) 各参与者执行事务操作,将 undo 和 redo 信息记入事务日志中(但不提交事务)。
c) 如参与者执行成功,给协调者反馈 yes,否则反馈 no。
阶段二
如果协调者收到了参与者的失败消息或者超时,直接给每个参与者发送回滚(rollback)消息;否则,
发送提交(commit)消息。两种情况处理如下:
情况1:当所有参与者均反馈 yes,提交事务
a) 协调者向所有参与者发出正式提交事务的请求(即 commit 请求)。
b) 参与者执行 commit 请求,并释放整个事务期间占用的资源。
c) 各参与者向协调者反馈 ack(应答)完成的消息。
d) 协调者收到所有参与者反馈的 ack 消息后,即完成事务提交。
情况2:当有一个参与者反馈 no,回滚事务
a) 协调者向所有参与者发出回滚请求(即 rollback 请求)。
b) 参与者使用阶段 1 中的 undo 信息执行回滚操作,并释放整个事务期间占用的资源。
c) 各参与者向协调者反馈 ack 完成的消息。
d) 协调者收到所有参与者反馈的 ack 消息后,即完成事务。
a) 协调者向所有参与者发送事务内容,询问是否可以提交事务,并等待答复。
b) 各参与者执行事务操作,将 undo 和 redo 信息记入事务日志中(但不提交事务)。
c) 如参与者执行成功,给协调者反馈 yes,否则反馈 no。
阶段二
如果协调者收到了参与者的失败消息或者超时,直接给每个参与者发送回滚(rollback)消息;否则,
发送提交(commit)消息。两种情况处理如下:
情况1:当所有参与者均反馈 yes,提交事务
a) 协调者向所有参与者发出正式提交事务的请求(即 commit 请求)。
b) 参与者执行 commit 请求,并释放整个事务期间占用的资源。
c) 各参与者向协调者反馈 ack(应答)完成的消息。
d) 协调者收到所有参与者反馈的 ack 消息后,即完成事务提交。
情况2:当有一个参与者反馈 no,回滚事务
a) 协调者向所有参与者发出回滚请求(即 rollback 请求)。
b) 参与者使用阶段 1 中的 undo 信息执行回滚操作,并释放整个事务期间占用的资源。
c) 各参与者向协调者反馈 ack 完成的消息。
d) 协调者收到所有参与者反馈的 ack 消息后,即完成事务。
问题
1) 性能问题:所有参与者在事务提交阶段处于同步阻塞状态,占用系统资源,容易导致性能瓶颈。
2) 可靠性问题:如果协调者存在单点故障问题,或出现故障,提供者将一直处于锁定状态。
3) 数据一致性问题:在阶段 2 中,如果出现协调者和参与者都挂了的情况,有可能导致数据不一
致。
优点:尽量保证了数据的强一致,适合对数据强一致要求很高的关键领域。(其实也不能100%保证
强一致)。
缺点:实现复杂,牺牲了可用性,对性能影响较大,不适合高并发高性能场景。
1) 性能问题:所有参与者在事务提交阶段处于同步阻塞状态,占用系统资源,容易导致性能瓶颈。
2) 可靠性问题:如果协调者存在单点故障问题,或出现故障,提供者将一直处于锁定状态。
3) 数据一致性问题:在阶段 2 中,如果出现协调者和参与者都挂了的情况,有可能导致数据不一
致。
优点:尽量保证了数据的强一致,适合对数据强一致要求很高的关键领域。(其实也不能100%保证
强一致)。
缺点:实现复杂,牺牲了可用性,对性能影响较大,不适合高并发高性能场景。
什么是三阶段提交?
三阶段提交是在二阶段提交上的改进版本,3PC最关键要解决的就是协调者和参与者同时挂掉的问
题,所以3PC把2PC的准备阶段再次一分为二,这样三阶段提交。
题,所以3PC把2PC的准备阶段再次一分为二,这样三阶段提交。
阶段一
a) 协调者向所有参与者发出包含事务内容的 canCommit 请求,询问是否可以提交事务,并等待所
有参与者答复。
b) 参与者收到 canCommit 请求后,如果认为可以执行事务操作,则反馈 yes 并进入预备状态,否
则反馈 no。
阶段二
协调者根据参与者响应情况,有以下两种可能。
情况1:所有参与者均反馈 yes,协调者预执行事务
a) 协调者向所有参与者发出 preCommit 请求,进入准备阶段。
b) 参与者收到 preCommit 请求后,执行事务操作,将 undo 和 redo 信息记入事务日志中(但不
提交事务)。
c) 各参与者向协调者反馈 ack 响应或 no 响应,并等待最终指令。
情况2:只要有一个参与者反馈 no,或者等待超时后协调者尚无法收到所有提供者的反馈,即中断
事务
a) 协调者向所有参与者发出 abort 请求。
b) 无论收到协调者发出的 abort 请求,或者在等待协调者请求过程中出现超时,参与者均会中断事
务。
阶段三
该阶段进行真正的事务提交,也可以分为以下两种情况。
情况 1:所有参与者均反馈 ack 响应,执行真正的事务提交
a) 如果协调者处于工作状态,则向所有参与者发出 do Commit 请求。
b) 参与者收到 do Commit 请求后,会正式执行事务提交,并释放整个事务期间占用的资源。
c) 各参与者向协调者反馈 ack 完成的消息。
d) 协调者收到所有参与者反馈的 ack 消息后,即完成事务提交。
情况2:只要有一个参与者反馈 no,或者等待超时后协调组尚无法收到所有提供者的反馈,即回滚
事务。
a) 如果协调者处于工作状态,向所有参与者发出 rollback 请求。
b) 参与者使用阶段 1 中的 undo 信息执行回滚操作,并释放整个事务期间占用的资源。
c) 各参与者向协调组反馈 ack 完成的消息。
d) 协调组收到所有参与者反馈的 ack 消息后,即完成事务回滚。
优点:相比二阶段提交,三阶段提交降低了阻塞范围,在等待超时后协调者或参与者会中断事务。
避免了协调者单点问题。阶段 3 中协调者出现问题时,参与者会继续提交事务。
缺点:数据不一致问题依然存在,当在参与者收到 preCommit 请求后等待 do commite 指令时,
此时如果协调者请求中断事务,而协调者无法与参与者正常通信,会导致参与者继续提交事务,造
成数据不一致。
a) 协调者向所有参与者发出包含事务内容的 canCommit 请求,询问是否可以提交事务,并等待所
有参与者答复。
b) 参与者收到 canCommit 请求后,如果认为可以执行事务操作,则反馈 yes 并进入预备状态,否
则反馈 no。
阶段二
协调者根据参与者响应情况,有以下两种可能。
情况1:所有参与者均反馈 yes,协调者预执行事务
a) 协调者向所有参与者发出 preCommit 请求,进入准备阶段。
b) 参与者收到 preCommit 请求后,执行事务操作,将 undo 和 redo 信息记入事务日志中(但不
提交事务)。
c) 各参与者向协调者反馈 ack 响应或 no 响应,并等待最终指令。
情况2:只要有一个参与者反馈 no,或者等待超时后协调者尚无法收到所有提供者的反馈,即中断
事务
a) 协调者向所有参与者发出 abort 请求。
b) 无论收到协调者发出的 abort 请求,或者在等待协调者请求过程中出现超时,参与者均会中断事
务。
阶段三
该阶段进行真正的事务提交,也可以分为以下两种情况。
情况 1:所有参与者均反馈 ack 响应,执行真正的事务提交
a) 如果协调者处于工作状态,则向所有参与者发出 do Commit 请求。
b) 参与者收到 do Commit 请求后,会正式执行事务提交,并释放整个事务期间占用的资源。
c) 各参与者向协调者反馈 ack 完成的消息。
d) 协调者收到所有参与者反馈的 ack 消息后,即完成事务提交。
情况2:只要有一个参与者反馈 no,或者等待超时后协调组尚无法收到所有提供者的反馈,即回滚
事务。
a) 如果协调者处于工作状态,向所有参与者发出 rollback 请求。
b) 参与者使用阶段 1 中的 undo 信息执行回滚操作,并释放整个事务期间占用的资源。
c) 各参与者向协调组反馈 ack 完成的消息。
d) 协调组收到所有参与者反馈的 ack 消息后,即完成事务回滚。
优点:相比二阶段提交,三阶段提交降低了阻塞范围,在等待超时后协调者或参与者会中断事务。
避免了协调者单点问题。阶段 3 中协调者出现问题时,参与者会继续提交事务。
缺点:数据不一致问题依然存在,当在参与者收到 preCommit 请求后等待 do commite 指令时,
此时如果协调者请求中断事务,而协调者无法与参与者正常通信,会导致参与者继续提交事务,造
成数据不一致。
什么是补偿事务?
TCC 其实就是采用的补偿机制,其核心思想是:针对每个操作,都要注册一个与其对应的确认和补
偿(撤销)操作。
它分为三个步骤:
Try 阶段主要是对业务系统做检测及资源预留。
Confirm 阶段主要是对业务系统做确认提交,Try阶段执行成功并开始执行 Confirm阶段时,默
认 Confirm阶段是不会出错的。即:只要Try成功,Confirm一定成功。
Cancel 阶段主要是在业务执行错误,需要回滚的状态下执行的业务取消,预留资源释放。
举个例子,假入你要向 老田 转账,思路大概是: 我们有一个本地方法,里面依次调用步骤: 1、
首先在 Try 阶段,要先调用远程接口把 你 和 老田 的钱给冻结起来。 2、在 Confirm 阶段,执行远
程调用的转账的操作,转账成功进行解冻。 3、如果第2步执行成功,那么转账成功,如果第二步执
行失败,则调用远程冻结接口对应的解冻方法 (Cancel)。
优点:
性能提升:具体业务来实现控制资源锁的粒度变小,不会锁定整个资源。
数据最终一致性:基于 Confirm 和 Cancel 的幂等性,保证事务最终完成确认或者取消,保证数据
的一致性。
可靠性:解决了 XA 协议的协调者单点故障问题,由主业务方发起并控制整个业务活动,业务活动
管理器也变成多点,引入集群。
缺点:TCC 的 Try、Confirm 和 Cancel 操作功能要按具体业务来实现,业务耦合度较高,提高了开
发成本。
偿(撤销)操作。
它分为三个步骤:
Try 阶段主要是对业务系统做检测及资源预留。
Confirm 阶段主要是对业务系统做确认提交,Try阶段执行成功并开始执行 Confirm阶段时,默
认 Confirm阶段是不会出错的。即:只要Try成功,Confirm一定成功。
Cancel 阶段主要是在业务执行错误,需要回滚的状态下执行的业务取消,预留资源释放。
举个例子,假入你要向 老田 转账,思路大概是: 我们有一个本地方法,里面依次调用步骤: 1、
首先在 Try 阶段,要先调用远程接口把 你 和 老田 的钱给冻结起来。 2、在 Confirm 阶段,执行远
程调用的转账的操作,转账成功进行解冻。 3、如果第2步执行成功,那么转账成功,如果第二步执
行失败,则调用远程冻结接口对应的解冻方法 (Cancel)。
优点:
性能提升:具体业务来实现控制资源锁的粒度变小,不会锁定整个资源。
数据最终一致性:基于 Confirm 和 Cancel 的幂等性,保证事务最终完成确认或者取消,保证数据
的一致性。
可靠性:解决了 XA 协议的协调者单点故障问题,由主业务方发起并控制整个业务活动,业务活动
管理器也变成多点,引入集群。
缺点:TCC 的 Try、Confirm 和 Cancel 操作功能要按具体业务来实现,业务耦合度较高,提高了开
发成本。
分布式ID生成有几种方案?
分布式ID的特性
唯一性:确保生成的ID是全网唯一的。
有序递增性:确保生成的ID是对于某个用户或者业务是按一定的数字有序递增的。
高可用性:确保任何时候都能正确的生成ID。
带时间:ID里面包含时间,一眼扫过去就知道哪天的交易。
唯一性:确保生成的ID是全网唯一的。
有序递增性:确保生成的ID是对于某个用户或者业务是按一定的数字有序递增的。
高可用性:确保任何时候都能正确的生成ID。
带时间:ID里面包含时间,一眼扫过去就知道哪天的交易。
UUID
算法的核心思想是结合机器的网卡、当地时间、一个随记数来生成UUID。
优点:本地生成,生成简单,性能好,没有高可用风险
缺点:长度过长,存储冗余,且无序不可读,查询效率低
算法的核心思想是结合机器的网卡、当地时间、一个随记数来生成UUID。
优点:本地生成,生成简单,性能好,没有高可用风险
缺点:长度过长,存储冗余,且无序不可读,查询效率低
数据库自增ID
使用数据库的id自增策略,如 MySQL 的 auto_increment。并且可以使用两台数据库分别设置不同
步长,生成不重复ID的策略来实现高可用。
优点:数据库生成的ID绝对有序,高可用实现方式简单
缺点:需要独立部署数据库实例,成本高,有性能瓶颈
使用数据库的id自增策略,如 MySQL 的 auto_increment。并且可以使用两台数据库分别设置不同
步长,生成不重复ID的策略来实现高可用。
优点:数据库生成的ID绝对有序,高可用实现方式简单
缺点:需要独立部署数据库实例,成本高,有性能瓶颈
批量生成ID
一次按需批量生成多个ID,每次生成都需要访问数据库,将数据库修改为最大的ID值,并在内存中
记录当前值及最大值。
优点:避免了每次生成ID都要访问数据库并带来压力,提高性能
缺点:属于本地生成策略,存在单点故障,服务重启造成ID不连续
一次按需批量生成多个ID,每次生成都需要访问数据库,将数据库修改为最大的ID值,并在内存中
记录当前值及最大值。
优点:避免了每次生成ID都要访问数据库并带来压力,提高性能
缺点:属于本地生成策略,存在单点故障,服务重启造成ID不连续
Redis生成ID
Redis的所有命令操作都是单线程的,本身提供像 incr 和 increby 这样的自增原子命令,所以能保
证生成的 ID 肯定是唯一有序的。
优点:不依赖于数据库,灵活方便,且性能优于数据库;数字ID天然排序,对分页或者需要排
序的结果很有帮助。
缺点:如果系统中没有Redis,还需要引入新的组件,增加系统复杂度;需要编码和配置的工作
量比较大。
考虑到单节点的性能瓶颈,可以使用 Redis 集群来获取更高的吞吐量。假如一个集群中有5台
Redis。可以初始化每台 Redis 的值分别是1, 2, 3, 4, 5,然后步长都是 5。
Redis的所有命令操作都是单线程的,本身提供像 incr 和 increby 这样的自增原子命令,所以能保
证生成的 ID 肯定是唯一有序的。
优点:不依赖于数据库,灵活方便,且性能优于数据库;数字ID天然排序,对分页或者需要排
序的结果很有帮助。
缺点:如果系统中没有Redis,还需要引入新的组件,增加系统复杂度;需要编码和配置的工作
量比较大。
考虑到单节点的性能瓶颈,可以使用 Redis 集群来获取更高的吞吐量。假如一个集群中有5台
Redis。可以初始化每台 Redis 的值分别是1, 2, 3, 4, 5,然后步长都是 5。
Twitter的snowflake算法(重点) Twitter 利用 zookeeper 实现了一个全局ID生成的服务 Snowflake
如上图的所示,Twitter 的 Snowflake 算法由下面几部分组成:
1位符号位:
由于 long 类型在 java 中带符号的,最高位为符号位,正数为 0,负数为 1,且实际系统中所使用
的ID一般都是正数,所以最高位为 0。
41位时间戳(毫秒级):
需要注意的是此处的 41 位时间戳并非存储当前时间的时间戳,而是存储时间戳的差值(当前时间
戳 - 起始时间戳),这里的起始时间戳一般是ID生成器开始使用的时间戳,由程序来指定,所以41
位毫秒时间戳最多可以使用 (1 << 41) / (1000x60x60x24x365) = 69年 。
10位数据机器位:
包括5位数据标识位和5位机器标识位,这10位决定了分布式系统中最多可以部署 1 << 10 = 1024
s个节点。超过这个数量,生成的ID就有可能会冲突。
12位毫秒内的序列:
这 12 位计数支持每个节点每毫秒(同一台机器,同一时刻)最多生成 1 << 12 = 4096个ID
加起来刚好64位,为一个Long型。
优点:高性能,低延迟,按时间有序,一般不会造成ID碰撞
缺点:需要独立的开发和部署,依赖于机器的时钟
如上图的所示,Twitter 的 Snowflake 算法由下面几部分组成:
1位符号位:
由于 long 类型在 java 中带符号的,最高位为符号位,正数为 0,负数为 1,且实际系统中所使用
的ID一般都是正数,所以最高位为 0。
41位时间戳(毫秒级):
需要注意的是此处的 41 位时间戳并非存储当前时间的时间戳,而是存储时间戳的差值(当前时间
戳 - 起始时间戳),这里的起始时间戳一般是ID生成器开始使用的时间戳,由程序来指定,所以41
位毫秒时间戳最多可以使用 (1 << 41) / (1000x60x60x24x365) = 69年 。
10位数据机器位:
包括5位数据标识位和5位机器标识位,这10位决定了分布式系统中最多可以部署 1 << 10 = 1024
s个节点。超过这个数量,生成的ID就有可能会冲突。
12位毫秒内的序列:
这 12 位计数支持每个节点每毫秒(同一台机器,同一时刻)最多生成 1 << 12 = 4096个ID
加起来刚好64位,为一个Long型。
优点:高性能,低延迟,按时间有序,一般不会造成ID碰撞
缺点:需要独立的开发和部署,依赖于机器的时钟
常见负载均衡算法有哪些?
轮询,加权轮询,随机,最少连接,源地址hash
你知道哪些限流算法?
计数器算法(固定窗口)
滑动窗口
漏桶算法
令牌桶算法
滑动窗口
漏桶算法
令牌桶算法
计数器算法是使用计数器在周期内累加访问次数,当达到设定的限流值时,触发限流策略。下一个
周期开始时,进行清零,重新计数。
此算法在单机还是分布式环境下实现都非常简单,使用redis的incr原子自增性和线程安全即可轻松
实现。
这个算法通常用于QPS限流和统计总访问量,对于秒级以上的时间周期来说,会存在一个非常严重
的问题,那就是临界问题
假设10S内服务器的负载能力为100,因此一个周期的访问量限制在100,然而在第一个周期的最
后5秒和下一个周期的开始5秒时间段内,分别涌入100的访问量,虽然没有超过每个周期的限制
量,但是整体上10秒内已达到200的访问量,已远远超过服务器的负载能力,由此可见,计数器算
法方式限流对于周期比较长的限流,存在很大的弊端。
周期开始时,进行清零,重新计数。
此算法在单机还是分布式环境下实现都非常简单,使用redis的incr原子自增性和线程安全即可轻松
实现。
这个算法通常用于QPS限流和统计总访问量,对于秒级以上的时间周期来说,会存在一个非常严重
的问题,那就是临界问题
假设10S内服务器的负载能力为100,因此一个周期的访问量限制在100,然而在第一个周期的最
后5秒和下一个周期的开始5秒时间段内,分别涌入100的访问量,虽然没有超过每个周期的限制
量,但是整体上10秒内已达到200的访问量,已远远超过服务器的负载能力,由此可见,计数器算
法方式限流对于周期比较长的限流,存在很大的弊端。
滑动窗口算法是将时间周期分为N个小周期,分别记录每个小周期内访问次数,并且根据时间滑动
删除过期的小周期。
当滑动窗口的格子划分的越多,那么滑动窗口的滚动就越平滑,限流的统计就会越精确。
此算法可以很好的解决固定窗口算法的临界问题。但是不能完全解决
删除过期的小周期。
当滑动窗口的格子划分的越多,那么滑动窗口的滚动就越平滑,限流的统计就会越精确。
此算法可以很好的解决固定窗口算法的临界问题。但是不能完全解决
漏桶算法是访问请求到达时直接放入漏桶,如当前容量已达到上限(限流值),则进行丢弃(触发
限流策略)。漏桶以固定的速率进行释放访问请求(即请求通过),直到漏桶为空。
限流策略)。漏桶以固定的速率进行释放访问请求(即请求通过),直到漏桶为空。
令牌桶算法是程序以r(r=时间周期/限流值)的速度向令牌桶中增加令牌,直到令牌桶满,请求到
达时向令牌桶请求令牌,如获取到令牌则通过请求,否则触发限流策略
达时向令牌桶请求令牌,如获取到令牌则通过请求,否则触发限流策略
如何提高系统的并发能力?
使用分布式系统。
部署多台服务器,并做负载均衡。
使用缓存(Redis)集群。
数据库分库分表 + 读写分离。
引入消息中间件集群。
部署多台服务器,并做负载均衡。
使用缓存(Redis)集群。
数据库分库分表 + 读写分离。
引入消息中间件集群。
注册中心
注册表结构
eureka
ConcurrentHashMap<String, Map<String, Lease<InstanceInfo>>>
nacos
Map(namespace, Map(group::serviceName, Service))
Service-->Map<String, Cluster> clusterMap
Cluster
-->Set<Instance> persistentInstances
-->Set<Instance> ephemeralInstances
-->Set<Instance> persistentInstances
-->Set<Instance> ephemeralInstances
服务注册
eureka
服务端
1.修改客户端续约数量和预期每分钟的续约阈值
2.实例信息放入注册表
3.将变动放入最近变化队列
4.清除读写缓存
2.实例信息放入注册表
3.将变动放入最近变化队列
4.清除读写缓存
nacos
客户端
注册数据
serviceName,groupName,ip,port,clusterName,weight,enable,healthy,ephemeral,metadata
1.构建注册数据,并且为临时节点启动定时续约任务,若续约表示未注册,则发起注册请求
2.向服务端发起注册请求
2.向服务端发起注册请求
服务端
1.第一次注册会先创建service,同时service内部启动一个定时任务,每5秒检查一次心跳,若15秒未续约则把健康状态改为false,若30秒为续约则删除实例
2.待续
2.待续
服务续约
eureka
客户端
注册时,启动一个异步任务,每30秒向服务端发送一次心跳
nacos
客户端
注册时,启动一个异步任务,每5秒向服务端发送一次心跳
Redis分布式锁如何防止多重死锁
Seata三大模块?
TC (Transaction Coordinator) - 事务协调者: 维护全局和分支事务的状态,驱动全局事务提交或回滚。
TM (Transaction Manager) - 事务管理器: 定义全局事务的范围:开始全局事务、提交或回滚全局事务。
RM (Resource Manager) - 资源管理器: 管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。
TM (Transaction Manager) - 事务管理器: 定义全局事务的范围:开始全局事务、提交或回滚全局事务。
RM (Resource Manager) - 资源管理器: 管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。
Seata有哪些模式?
AT模式
优缺点
优点
不受数据影响
性能会比XA好一些
无代码侵入性
缺点
最终一致性的
有可能会导致数据无法回滚
原理流程
它分为两个阶段:
第一阶段:
TM开启全局事务
RM注册各个分支事务
分支事务做完提交生成一个快照
第二阶段:
TC判断各个分支事务的执行状态
都成功异步删除快照
有一个失败基于快照恢复数据删除快照
第一阶段:
TM开启全局事务
RM注册各个分支事务
分支事务做完提交生成一个快照
第二阶段:
TC判断各个分支事务的执行状态
都成功异步删除快照
有一个失败基于快照恢复数据删除快照
一阶段
在一阶段,Seata 会拦截“业务 SQL”,首先解析 SQL 语义,找到“业务 SQL”要更新的业务数据,在业务数据被更新前,将其保存成“before image”,然后执行“业务 SQL”更新业务数据,在业务数据更新之后,再将其保存成“after image”,最后生成行锁。以上操作全部在一个数据库事务内完成,这样保证了一阶段操作的原子性。
二阶段提交
二阶段如果是提交的话,因为“业务 SQL”在一阶段已经提交至数据库, 所以 Seata 框架只需将一阶段保存的快照数据和行锁删掉,完成数据清理即可。
二阶段回滚
二阶段如果是回滚的话,Seata 就需要回滚一阶段已经执行的“业务 SQL”,还原业务数据。回滚方式便是用“before image”还原业务数据;但在还原前要首先要校验脏写,对比“数据库当前业务数据”和 “after image”,如果两份数据完全一致就说明没有脏写,可以还原业务数据,如果不一致就说明有脏写,出现脏写就需要转人工处理。
在一阶段,Seata 会拦截“业务 SQL”,首先解析 SQL 语义,找到“业务 SQL”要更新的业务数据,在业务数据被更新前,将其保存成“before image”,然后执行“业务 SQL”更新业务数据,在业务数据更新之后,再将其保存成“after image”,最后生成行锁。以上操作全部在一个数据库事务内完成,这样保证了一阶段操作的原子性。
二阶段提交
二阶段如果是提交的话,因为“业务 SQL”在一阶段已经提交至数据库, 所以 Seata 框架只需将一阶段保存的快照数据和行锁删掉,完成数据清理即可。
二阶段回滚
二阶段如果是回滚的话,Seata 就需要回滚一阶段已经执行的“业务 SQL”,还原业务数据。回滚方式便是用“before image”还原业务数据;但在还原前要首先要校验脏写,对比“数据库当前业务数据”和 “after image”,如果两份数据完全一致就说明没有脏写,可以还原业务数据,如果不一致就说明有脏写,出现脏写就需要转人工处理。
使用方式
TC的数据库需要导入一张全局锁的表(lock_table)
有参与事务的每个微服务的数据库都要导入一张undo_log表(用来记录快照数据)
分布式事务的模式设置为:AT
分布式事务全局事务的方法入口添加一个注解
@GlobalTransational
XA模式
优缺点
优点
基于XA规范实现的,实现起来很简单
没有代码侵入
强一致性
缺点
数据库得支持XA规范
保证强一致性,意味着性能很差
原理流程
全局事务注册到事务协调者,开启全局事务
注册分支事务
执行分支事务
所有的事务都执行完后,有TC判断是提交还是回滚
注册分支事务
执行分支事务
所有的事务都执行完后,有TC判断是提交还是回滚
使用方式
给所有参与的微服务
修改配置文件,配置使用XA模式
给全局事务入口
@GlobalTransational
TCC模式
T
Try
资源的预留
C
Confrim
业务的提交
C
Cancel
资源的释放
AP
最终一致性
原理流程
第一阶段
TM开启全局事务
RM注册分支事务
Try进行资源预留
直接提交
第二阶段
TC检查各个分支事务
如果都成功
删除预留的资源
如果失败了
执行cancel
回滚资源
优缺点
优点
快
不依赖数据库,也不用快照,也不需要全局锁
缺点
费程序员
代码由侵入性,需要自己编码实现TCC过程
考虑接口的幂等性
最终一致性
空回滚
还没有进行资源预留
执行了回滚操作
做空回滚的时候,插入一条数据,标记状态是回滚状态
判断预留表里面,有没有资源预留的记录
业务悬挂
做了空回滚之后
来进行资源的预留
判断表里面,这个事务id有没有对应的数据
没有,可以预留
有,不能进行资源的预留
使用方式
需要编写三个接口
需要编写一个接口类
@LocalTCC
写三个方法
try
@TwoPhaseBusinessAction(name="try-method",commitMethod="confirm-method",rollbackMethod="cancel-method")
confirm
cancel
然后实现这些接口
获取全局事务id
RootConetxt
BussinessActionContext
cancel的时候,需要预留的记录删除掉吗?
不需要
留着这条记录,以后发生业务悬挂或者空回滚可以拿来判断
Sage模式
优缺点
优点
一阶段提交本地数据库事务,无锁,高薪能
补偿服务即正向服务的 “反向”,高吞吐
参与者可异步执行,高吞吐
缺点
有代码入侵
一阶段提交本地数据库事务,无锁,高薪能
补偿服务即正向服务的 “反向”,高吞吐
参与者可异步执行,高吞吐
缺点
有代码入侵
什么是CAP?
c:一致性;a:可用性;p:必须保证分区容错性
最多只能同时满足两个:cp或ap
什么是BASE理论?
BA:基本可用;S:软状态;E:最终一致性
CP模式
保证一致性和分区容错性
各个分支事务先不提交,等所有事务都处理完毕后一起提交,如果有一个事务执行失败就全部回滚
各个分支事务先不提交,等所有事务都处理完毕后一起提交,如果有一个事务执行失败就全部回滚
AP模式
保证了可用性和分区容错性
各个分支事务处理完成后直接提交,整个流程执行完毕后只要发现有一个事务执行失败就全部回滚
各个分支事务处理完成后直接提交,整个流程执行完毕后只要发现有一个事务执行失败就全部回滚
什么是事务原理?
TC:事务协调者
RM:资源管理器
TM:事务管理者
XA和AT的区别?
XA是强一致性,AT是最终一致性
XA需要依赖数据库,AT不需要依赖数据库
XA资源锁定,AT不会资源锁定
AT比XA性能更好
XA需要依赖数据库,AT不需要依赖数据库
XA资源锁定,AT不会资源锁定
AT比XA性能更好
收藏
0 条评论
下一页