JAVA知识体系

5 篇文章 0 订阅

文章目录

项目

项目中你遇到了那些挑战

  1. 医保定时任务
    1.1 如何获取到哪些机构需要生成对账记录
            因为我们是saas服务嘛,所以你必须得找到当前池子下有哪些租户,然后使用当前租户登录获取token,拿着当前租户下的token去查询哪些机构配置了当前的医保机构,然后再进行生成记录。另外,基于这个,我使用策略模式将全租户执行与实际干的事进行了抽象,这样,当再有类似需要全租户执行的操作时,只需要继承我的抽象方法即可。
    1.2 如何保证数据不会重复插入
            要保证不会重复执行,必须得加锁,我们使用的是Redisson来进行加锁的,锁的key是租户id+医保中心id+业务key。执行前,先查询redis中是否有当前锁,如果有,则跳过,如果没有,则进行执行后续的操作。
            其实除了这种加锁的方案,还可以使用外部唤醒的方案,来实现同样的功能。
  2. 数据一致性方面的保证,除了结算时,尽量保证数据的一致性,那如果在结算出错时,只能依靠对对账来解决了。
  3. 系统响应时间,因为系统要满足三级医院的要求,1s内响应,所以对程序的响应速度上要求很高
  4. 读接口文档的能力,对接一家医保,你需要根据接口文档,来构思出你的程序代码要如何去实现,能否用现有的功能或者工具的组合来完成这个功能。
  5. 长连接模式下,如何保证微服务的集群,也就是有状态的服务集群。

表数据

我们的用户群体,一级医院或者卫生院,二级医院、三级医院
大部分群体是二级医院一级卫生院,卫生院的话有点类似于医共体这种的,虽然每一家的结算量不大,但是一天的结算量2000左右。
医保一天结算量,三级医院一家机构:300~500
然后数据量统计的话,平均单表数据在十几万到几十万左右吧到现在。
然后目录的数据会比较多,大概一百万左右的数据量吧,所以对于目录的话,我们是用的es。

项目中出现了什么jvm的问题

有一次,导入三目的数据,导致线上直接oom了,这个服务紧接着就挂了。因为当时导入的这个文件几十万条吧,我们用的是poi进行导入的,poi都知道,它是先把所有excel的数据都读取到内存中 ,然后才能查询每一行的数据,但是医保给的三目数据列太多了,所以直接导致读崩了。
没办法,那次手动用excel来编辑的sql,通过线上的jira任务来提交的。
后来我将这个导入换成了easyExcel,就没有再出现过类似的问题了。

线上出现过什么事故

这个是在去年的时候,当时大家还有权限去查询线上的库。有一次,实施同志执行了一个sql,具体的语句忘了,大概是select * 然后关联了几个大的表,然后也没有加where条件,导致数据库直接查崩了。当时影响挺大的,整个医院系统都用不了,过后还专门开会说了这个事,不允许查询全部,一定要加条件跟limit。而且后来,运维部门把查询线上库的权限也都收回了,如果线上排查问题,使用的是TIDB分析库,这样即使查崩了也不会对线上产生影响。

项目中,那些地方使用到了juc的工具

在数据库路由时,用到了ConcurrentHashMap

如果让你做技术选型,你怎么做

  1. 首先肯定是先看公司内部,有没有使用相关的方案,如果有的话,首先用公司内部的
  2. 如果公司内部没有的话,就搜下百度啊谷歌啊或者github这些技术论坛
  3. 另外,如果遇到问题以后,还可以问下马士兵那边,因为毕竟买的会员,会有这个服务
  4. 还有我们之前上家公司的架构师关系还不错,有问题也可以问他

结算时,幂等性如何保证

在预结算时,rcm会先生成一个settlementId,结算时,会根据这个settlementId来更新结算单状态。
如果出现异常,医保调用成功两次,但是insurance服务只调用了一次,那么到时候由对账的冲正来解决。

结算的数据一致性

  1. 调用医保,医保报错
  2. 医保返回了,但是没有传给后端
  3. 后端报错

有分布式事务吗,怎么做的?

项目后期的规划

insurance服务的拆分,因为insurance服务是所有的医保跟数据库打交道的服务,包括主业务与非主业务,如果把主业务与非主业务区分开来的话,那么,即使非主业务挂了,也不会影响主业务的运行,另外,还可以提升服务的响应时间。


java基础

jvm只会在堆上分配吗

不是,在以下情况会进行栈上分配

  1. 基本数据类型
  2. Integer -128~127范围内,会进行栈上分配
  3. jvm对进行内存逃逸分析,如果检测到一段执行了很多次,就会判定为热点代码,这时候,有些对象就会进行栈上分配。

网络

七层网络模型

  1. 应用层:代表应用程序或者应用软件
  2. 表示层:协议,语义,段落划分,字符串表示,加密等等
  3. 会话层:session
  4. 传输层:如何建立连接,如何传输,例如tcp udp
  5. 网络层:设备中如何去路由,数据包如何发送
  6. 数据链路层:点对点之间之间如何进行通信
  7. 物理层:wifi,光纤,4G等
    在这里插入图片描述
    image.png

数据传输顺序

三次握手–>数据传输–>四次挥手
最小粒度,不可被分割。例如,负载均衡,第一次与第三次的握手如果不是同一台机器,那么连接建立不起来。

为什么要三次握手

客户端要先发送跟接收一次消息,来确保输入输出没问题。
同样,服务端也需要发送跟接受一次消息,来确保输入输出没问题。
也就是,客户端先发送一次消息,服务端返回一个ack,那么对于客户端来说,确认了输入输出没有问题。
服务端返回ack的过程,其实也就相当于服务端发送了一次消息,那么客户端收到以后,再次发送一次ack。
那么此时,两端都确认了输入输出没有问题,开始开辟空间,创建线程,建立响应的资源,建立起连接进行通信。

为什么要四次挥手

第一次,客户端先跟服务端说,我要断开连接,服务端发送一个确认收到,服务端再次发送一个我也要分手,客户端再发送一个确认,所以一共需要四次。

一台设备能开启的端口号数量

65535

LVS

基于四层的负载均衡,数据包级别的转发,不会和客户端进行握手。因为是基于四层的协议,所以只能看到ip+端口。

高并发

nginx

根据网络层反向代理

4层反向代理: 可以根据ip、端口进行转发
7层反向代理: 可以根用户协议、方法、头、正文参数、cookie等进行转发
越往低层,掌握的信息越少,实现起来越简单,效率也越高

负载均衡算法

  1. 轮询: RR(round-robin)默认负载均衡算法,即以轮询的方式将请求转发到上游服务器,通过配合weight配置可以实现基于权重的轮询。
  2. 加权: WRR(Weight round robin)权重,weight用来配置权重,默认都是1,权重越高分配给这台服务器的请求就越多,需要根据服务器实际处理能力设置权重。
  3. 随机: Random
  4. IP哈希:(IP Hash)由请求客户端的IP地址决定请求发往哪台服务器,可以保证同一个IP地址的请求可以转发到同一台服务器。IPV4的前三位或者IPV6的全部地址参与哈希运算。
  5. 最少连接:(Least Connections)请求会转发到当前有效连接最少的服务器,可以配合权重使用。

并行与并发的区别

两个看的角度不同,并发是任务提交,并行是任务执行。
并发是从任务提交的角度看,多个任务可以同时运行,但是实际是不是同时运行不一定。
从行是从任务执行的角度看,多个任务同时处理。
并发包括并行。

集群

集群需要注意的点

  1. 存储问题:
    做集群时,不要在服务的内部进行存储客户端的数据,当需要存储时,使用外部的存储设备进行存储,例如mysql、redis,保证服务是无状态的,这样便于服务的水平扩展。
  2. 协作问题,例如定时任务:
    2.1 内部加锁:使用分布式锁来解决
    2.2 外部唤醒:外部有一个定时任务的服务,来调用需要执行的任务。
    2.3 使用公共存储的互斥性
  3. 单一服务节点集群(有状态的集群)
    虽然无状态的集群能满足大部分要求,但是如果涉及到长连接,服务端需要记录客户端执行到哪一步了,那么此时每个服务是有状态的。此时,需要建立用户与服务器之间的映射,有两种方案可选:
    用户手动选择服务器
    用户ip分配服务器(hash算法)
  4. 信息共享节点集群
    多个服务节点,共享存储
    缺点:受到共享存储的限制(存储容量、读写性能)
    运算能力分散到各个节点上,但是存储能力却集中在了共享存储中,这样会导致故障的单点和性能瓶颈。
  5. 信息一致节点集群
    读写分离(采用分流的思想)
    需要考虑数据一致性的问题
            强一致:
            弱一致,最终一致:

分布式系统

需要统一接口的定义,彼此当做的黑盒。

CAP理论

C:一致性:在同一时刻,获取到的数据都是最新的数据(所有节点的写操作都是原子操作)
        强一致性:写操作完成,后续所有的读操作都能获取到最新修改后的数据
        弱一致性:写操作完成后,可能一段时间内读取到的是旧值,但是最终要读取到新值。
A::可用性:不管成功与失败,都能立刻返回结果(只要有数据返回就行,不管新值旧值。)
P:分区容错性:
        分区:假如有四个服务,分别是ServiceA、ServiceB、ServiceC、ServiceD,此时,ServiceA与ServiceB能够正常通信,ServiceC与ServceD能够正常通信,但是ServiceA、ServiceB与ServiceC、ServiceD不能进行通信,这时候,这时候就产生了分区
        分区容错性:即使产生分区,各个分区也要正常返回结果。
三者只能满足其中两个,其中P是必须要满足的。即使网络出现问题,我们的系统也要能正常使用。
如何保证P:
        1. 尽量使用异步代替同步操作。
        2. 主节点挂了,从节点顶替上来
常用的解决方案:
        保证AP,兼顾C(最终一致性)
        如果保证强一致性,那么会影响吞吐量。
CP与AP如何选择:
        考虑业务对数据一致性的容忍程度
        业务的读写频率(是否是读多写少的业务)

服务内部的并发

多进程

相同的服务启动多个
优点:每个进程之间资源独立,具有很强的隔离性

多线程

  1. 请求的多线程、任务处理的多线程
  2. 使用异步操作,提前释放主线程
    优点:
    1. 提高吞吐量
    2. 节省CPU的资源消耗(使用异步可以使得请求不必一直等待阻塞中)
    3. 提高平均响应时间

网络8大谬误

  1. 网络是可靠的
  2. 没有延迟
  3. 带宽无限
  4. 网络总是安全的
  5. 网络拓扑不会改变
  6. 只有一个管理员
  7. 传输代价为0
  8. 网络是同构的

缓存设计

缓存设计的意义:以空间换时间

  1. 常见的缓存
    redis、memcache、localcache、guava、客户端缓存
  2. 缓存的位置
    位于请求方与提供方之间
  3. 缓存的成本:
    计算key的时间、查询key的时间、转换key的时间
  4. 所有数据的查询时间:假设缓存命中率为p
    缓存的成本+(1-p) * 原始查询的时间
  5. 何时使用缓存:
    1. (计算key的时间+查询key的时间+转换key的时间) 要远小于 (原始的查询时间)。
    2. 读多写少的场景
    3. 查询耗时长,写的频率小的场景。
  6. 常见存储结构
    一般采用key/value的方式来进行存储。
  7. key的生成
    在一些安全性的场景下,需要对key进行加密,那么需要保证:
            单向函数:给定输入,很容易能计算出结果,但是给定结果,很难计算出输入
            正向快速,逆向困难
            冲突的概率极低
    常见的加密策略:md5、sha-256
    越安全,时间越长。
    总结:无碰撞、高效生成
  8. 值的存储
    存储形式:直接存储对象、存储序列化后的值
  9. 实际key的设计:前缀+业务关键信息+后缀

使用缓存需要考虑哪些因素

  1. 缓存的业务场景(使用缓存来干嘛)
    1.1 对于数据库的缓存,读多写少的情况
    1.2 用作分布式锁
    1.3 排名的计算
  2. 缓存的实时性
  3. 缓存key如何设置:
    前缀+业务key+后缀
  4. 缓存的value
    直接存对象
    存储对象序列化后的数据
  5. 缓存的过期时间
  6. 缓存的更新机制

缓存的更新机制(双写一致性)

  1. 被动更新
    设置一个过期时间,当过期了再从数据库中获取。中途如果数据发生了改变,不去更新缓存
    适用场景:对数据准确性、实时性要求不高的场景。
  2. 主动更新
    2.1 先更新缓存,再更新数据库
            如果更新数据库发生错误,造成数据回滚,就会导致数据不一致,这种方案一般不采用
    2.2 先更新数据库,再更新缓存
            如果一个请求被阻塞,那么很可能数据库存储的与缓存中存储的数据不一致。
            另外,更新完数据以后,需要缓存需要重新计算,才可能完成操作
            所以,这种方案一般也不采用。
    2.3 先删除缓存,再更新数据库
            有可能在还没有保存数据库前,再来一个读请求,这时候,就会缓存旧的数据。
            解决办法:延迟双删(等待一段时间,再说删一次缓存),会降低吞吐量。
            这种方案一般也不采用。
    2.4 先更新数据库,再删除缓存 – cashe-aside模式
            异常流程:
            前提:缓存无数据,数据库有数据
            A: 查询 B: 更新
            A查询数据,发现缓存中没有,去数据库中查询
            B更新数据,删除缓存
            A查询完数据,写入缓存 – 旧值
            这种情况发生的概率极低,需要保证读比写要慢,而且缓存中必须无数据。

上面是说的延迟双删如果某一步失败了怎么办

以上操作无非三步

  1. 删除缓存
    如果这一步出错了,那么直接抛异常即可
  2. 更新数据
    这一步出错,会又数据库事务进行保证,错了以后直接回滚操作
  3. 删除缓存
    方案1. 回滚数据,代价太大,一般不考虑
    方案2.失败重试 可以使用消息队列来通知删除,当删除成功以后,就停止发送消息,如果一直删除不成功,进入死信队列,那么可以发消息给开发手动排查问题。
    方案3.开启数据库binlog,canal订阅binlog日志,更新数据后,发送消息进行删除缓存即可。这个方案的好处在于,与业务代码进行了解耦。

Read/Write Through

以缓存为主,数据进来先存缓存,再保存数据库。
需要保证程序启动时,先将数据的的数据放入缓存,不能等待启动完成后放入缓存。

Write Behind

以缓存为主,数据进来只存缓存,数据库通过异步或者消息队列的方式进行存储
优点:降低了写操作的时间,提高了系统吞吐量
缺点:如果缓存一旦挂了,那么整个服务就挂了
与上面的方案不同的是,上面的方案写数据库是同步的,这个是异步的

缓存清理机制

  1. 时效性清理机制
    设置一个过期时间,到时间自动去清理
    具体实现:定时任务去轮询
  2. 数据阈值式清理机制
    判断当缓存到达一定阈值以后,对缓存进行清理

缓存淘汰策略

fifo:先进先出
定长队列,当队列满时,从队尾删除数据
random:随机剔除
lru:剔除最近最少未使用的数据
ttl: 设置过期时间,到期自动清理

缓存异常情况

  1. 缓存穿透
    数据库中没有,缓存中也没有
    解决方案:
    1.1 缓存空值,一般时间比较短,5分钟以内。当有实际业务数据以后,将空值的缓存删除,但是会有内存压力。
    1.2 增加业务限制判断,例如年龄负数
    1.3 布隆过滤器,如果缓存中没有该数据,就查询布隆过滤器,相当于白名单,如果布隆过滤器中也查询不到,那么直接返回空值
    1.4 监控用户行为,如果发现有违规操作,直接将该用户强制下线并放入黑名单。
  2. 缓存雪崩
    大量缓存同时失效,导致数据库压力增大
    解决方案:
    2.1 过期时间随机,可以设置固定时间+随机时间来作为过期时间
    2.2 使用队列或锁,保证不会所有请求都一下打到数据库
    2.3 热点key不过期或者采用主备key
    2.4 服务熔断、限流、降级
  3. 缓存击穿
    高频缓存失效(热点key失效),与雪崩不同的是,击穿是单个热点key,而雪崩是多个key
    解决方案:
    3.1 热点数据不过期
    3.2 使用互斥锁,来保证一个key只查询一次
    3.3 守护线程去定时延长热点key的失效时间
    3.4 使用Read Write Through或者Write Behind

项目中,有没有处理过缓存异常情况

没有,因为以上出现的情况,一定是共享数据产生了并发压力,但是对于医院来说,每个患者的就诊的数据,是属于私有数据,不会造成大的并发,所以没有采用以上的处理方式。

如何对分布式锁进行优化

对于同一个key的请求,先要保证服务内部的排他(JVM锁),再保证服务外部的排他(分布式锁),这样就可以大大减少获取锁的数量。

缓存预热

如果是以缓存为主的业务,需要对缓存进行提前加载,另外还有一些热数据,也需要提前进行预热缓存。

读缓存与写缓存

写缓存:减少了客户响应时间,提高了系统吞吐量,增加了系统处理时间。
读缓存:减少了客户响应时间,提高了系统吞吐量,减少了系统处理时间。

如何设计一个高并发系统

  1. 分流:达到服务之前,减少服务的请求数
  2. 并发:到达服务之后,提升服务的请求数

应用保护

产生原因:系统压力大,负载过高,例如数据库慢查询,或者调用的服务出现问题
核心思想:优先保证核心业务,优先保证大部分用户。

服务降级

将非核心业务停用,来保证主业务可用。
前提:
代码提前规划好

实现方案:

  1. 提供后门接口,当调用这个接口后,关闭了一些业务的执行操作(在业务中,加if判断)
  2. 独立的降级系统,也就是对后门接口的封装。

触发条件:

  1. 超时
  2. 发生异常
  3. 业务访问过大需要限流,到达流量阈值以后,进行等待,如果等待的数量也超过以后,进行拒绝。

降级手段
减少不必要的操作,保留核心业务功能。

  1. 停止读数据库,转去读缓存
  2. 准确结果转化为近似结果(视频网站,高清流量过大,转换为流畅 定位不使用精准结果)。
  3. 返回静态结果(例如猜你喜欢 返回静态页面,而不是分析后的页面)
  4. 同步转异步(写多读少的业务)
  5. 不必要的功能停用、停用一些非核心业务的写操作(比如高峰期不允许修改用户信息)。
  6. 分用户降级(将一些频繁操作的用户,进行短时间降级,防止爬虫,但是可能会有误杀)
  7. 流量过大,工作量证明POW(例如验证码、数学题、拼图、滑块等),这样就可以让用户的操作慢下来,减轻服务的压力。

服务熔断

调用别的服务,别的服务挂了或者超时,为了当前服务不被拖垮,所以采用熔断策略。
熔断策略:
根据请求的失败率,如果到达阈值,则打开熔断开关,过一段时间,关闭熔断,放一个请求过去,如果还是不行,继续打开熔断(半开状态/Fail fast)。Hystrix
根据响应时间,如果服务响应时间超过指定的阈值,并且接下来5个请求都超过阈值,那么就开启熔断Sentinel

业务中哪些地方使用了降级

在对账中,我们使用了服务降级
因为前期是insurance与医保中心进行的对账,但是系统内与rcm没有进行校验,所以后期可能会出现系统内的不一致。所以后来加了与rcm对账,但是如果与rcm对账不通过,而医保又要求在规定时间内进行对账,所以在前端的菜单配置中,可以将于rcm对账去掉,来去掉与rcm对账的强校验。

业务中哪些地方使用了熔断

服务不可用时,返回一个临时的状态,Generate,例如在发版过程中访问业务。

限流

为了防止服务被拖垮,拒绝一些请求,来保证服务的正常运行。
触发情况:
外部:请求流量过大
内部:资源不够时

基于请求限流

  1. 请求的总量
    例如:同时在线人数限制
  2. 限制时间量
    一个时间窗口内,只允许最大阈值的请求

排队

当超过阈值后,进行排队

基于资源的限流

  1. 连接数达到阈值(tomcat连接数限流:配置connector,来配置连接数)
  2. 线程数达到阈值(设置线程数)
  3. cpu达到阈值,perl语言来读取cpu使用率,并操作redis即可。
  4. 请求队列达到阈值,进行拒绝

限流阈值如何确定

先设定一个数,然后上线观察。

限流算法

  1. 漏桶算法
    将请求放入队列进行缓存,以恒定的速率进行消费,如果超过队列大小,就执行拒绝。
  2. 令牌桶算法(Guava RateLimiter)
    有个单独线程以恒定的速率往桶中放令牌(一个一个放),服务来了以后,去拿令牌,如果能拿到令牌,就放行,如果拿不到令牌,就等待。令牌桶支持突发流量,例如指定1s放5个令牌,那么可以一次性拿50个,但是后面再取,需要等待10秒,由后面的请求去弥补之前的请求。
    与漏桶算法的区别是,漏桶出口速率是恒定的,但是令牌桶入口速率是恒定的

节流

只接受时间窗口内的最后一个请求(例如百度的搜索,只请求1s内最后一次输入的关键字)

数据不一致同步方案

  1. 中间件的主从复制 mysql redis
  2. 消息队列:订阅发布
  3. 二次读取
  4. 回源读取
  5. 重新生成
  6. 多通道同步:mysql主从复制+消息队列

数据需要考虑哪些方面

  1. 唯一性
  2. 实时性
  3. 可丢失性
  4. 可恢复性
  5. 异常处理

java基础

java集合

在这里插入图片描述

介绍下hashMap

  • 由数组加链表的数据结构组成,先对目标进行一次hash,获得一个hashcode值,根据hashcode对数组长度进行取模,获取到该槽位号,然后判断该槽中是否有数据,如果没有,直接存放,如果有,调用equals进行比较,如果不相等,那么使用链表的方式追加。
  • java8以后,如果链表长度大于8并且数组长度大于64,会将链表转换成红黑树,当红黑树长度小于6时,会将红黑树转换为链表。
  • java1.7采用头插法,1.8采用尾插法,头插法的缺点是扩容时,会改变链表原本的顺序,以至于在并发场景下,导致链表成环的问题
  • 1.7扩容时需要重新计算哈希值和索引位置,1.8并不重新计算哈希值,巧妙地采用和扩容后容量进行&操作来计算新的索引位置。
  • 默认的数组长度是16,扩容因子0.75,每次扩容一倍的长度
  • new HashMap时初始化的大小是必须是2的整数次幂,所以如果传20,实际创建的大小为32,原因:每次对key进行hash取模时是使用到了位运算,2的整数次幂的长度方便位运算的计算。

MQ

MQ如何保证消息成功消费

  1. 如果发送异常,重试
  2. 如果发送消息队列以后异常,持久化
  3. 如果消费异常,重发

JVM

类的加载过程

从总的阶段来看,一共分为五个动作,分别是加载、验证、准备、解析、初始化,当然,这几个动作不是依次执行的,像校验,是贯穿整个过程的。
第一步: 加载,这个过程主要完成3件事
    通过一个类的全限定名来获取此类的二进制流
    将二进制流中的静态存储结构,转化为方法区中的运行时数据结构
    在内存中生成一个代表此类的Class对象,作为方法区中访问该对象数据的入口
第二步: 验证,主要分为4个阶段
    文件格式验证:验证文件格式是否符合class文件格式规范(例如文件是否以0XCAFEBABE开头,版本号是否当前虚拟机能够解析等等)
    元数据验证:验证描述信息是否符合JAVA规范(比如这个类是否有父类,是否继承了final修饰的类等等)
    字节码验证:验证方法体的语义是否合法,符合逻辑
    符号引用验证:验证符号引用能否转换成直接引用,包括直接引用能否被当前类所访问
第三步:准备
    为类变量分配内存空间,并赋初始值。
第四步:解析
    将符号引用替换成直接引用
第五步:初始化
    初始化静态变量的值
    执行静态代码块
    初始化当前类的父类

类加载器有哪些

Bootstrap ClassLoader(启动类加载器)
Extension ClassLoader(扩展类加载器)
Application ClassLoader(应用程序加载器)
自定义类加载器

在这里插入图片描述

什么是双亲委派

当需要加载一个类时,先委托父类加载器去完成,如果父类加载器完成不了,才会尝试自己去加载。

双亲委派的好处

安全,防止核心类被外部篡改
避免类重复加载

如何打破双亲委派

重新loadClass方法
设置上下文类加载器

java内存模型

堆: 堆中存放所有new出来的对象
方法区: 类信息、静态变量、常量、即时编译的代码
程序计数器: 记录当前线程运行到哪一步了
本地方法栈: JVM执行native方法的栈
java虚拟机栈: JVM执行java程序的栈
其中,堆,方法区线程共享,其他的线程私有

在这里插入图片描述

字符串常量池位置

JDK1.6:永久代
JDK1.7以后:堆中

栈帧的结构

局部变量表: 方法中定义的局部变量以及方法的入参(局部变量表中的数据不能直接使用,如果要使用的话,必须调用相关指令将其加载到操作数栈中作为操作数使用)
操作数栈: 以压栈和出栈的形式存储操作数的
动态链接: 将常量池中的调用其他方法的符号引用转化为直接引用
方法返回地址

在这里插入图片描述

什么可以作为GC的Root

GC管理的是堆内存,一般只会对堆内存进行垃圾回收,方法区、虚拟机栈、本地方法栈不被GC管理,因而选择这些区域作为GC Root,被GC引用的对象不会被回收,一般情况下GC Root有以下几种
方法区中的静态变量、常量引用的对象
虚拟机栈中的局部变量表引用的对象
本地方法栈中的JNI引用的对象

java堆的分代设计

Young区:年轻代,包含Eden区和Survivor区
Old区: 老年代
在这里插入图片描述

对象内存分配

在这里插入图片描述

对应的GC

Young区: Young GC(minor GC)
Old区: Old GC(major GC)
Young区+Old区: Full GC,这个是当堆内存不足时触发

为什么需要Survivor区?只有Eden不行吗?

因为新生代使用的算法是复制回收算法,如果只有Survivor区,那么回收一次就会被送往Old区
这样会导致Old区很快被填满,触发Old GC(一般Old GC会伴随着Young GC,也就是Full GC)
老年代的空间一般大于新生代,所以消耗的时间比较长
另外老年代使用的回收算法是标记清除与标记压缩,不适合频繁的触发
所以,存在Survivor区的意义在于,对象不会很快被送到Old区,只有回收16次,才会被送往老年代

为什么要有两个Survivor区

其实是为了解决碎片化问题,因为复制算法,必须有有一块连续并空余的内存,Eden区回收一次后进入Survivor区
那么找不到一块连续的空间,去进行复制回收算法。

对象创建过程

  1. 先看该类是否被加载,如果没有被加载,先去加载(到常量池中查询是否有该类的符号引用,并且该Class类是否被初始化完毕)
  2. 分配内存空间
    2.1 分配内存的方式:
        内存连续: 指针碰撞(移动指针偏移位即可)
        内存不连续: 空闲列表(寻找一块能够创建该对象的区域)
        内存的是否连续,跟使用的垃圾回收器有关
    2.2 如果开辟内存期间,存在并发,怎么办
        CAS的方式
        本地线程分配缓存(每个线程有自己独立的空间,在自己独立空间内开辟内存)
  3. 成员变量赋初始值
  4. 设置对象头信息(markword,Class Point)在·
  5. 对象初始化

对象内存布局

对象头: markword、ClassPoint、length(数组独有)
实例数据: 成员变量
对其填充: 保证对象大小满足8字节的整数倍

在这里插入图片描述

对象大小

Mark Word: 8字节
Class Pointer: 不开启压缩8字节,开启压缩4字节
length: 4字节
实例数据: 引用类型不开启压缩8字节,不开启压缩4字节
padding: 8字节的倍数对其

对象头Mark Word

在这里插入图片描述

对象大小

名称大小
markword8
ClassPointer默认为4字节,关闭指针压缩为8字节
boolean1
byte1
short2
char2
int4
float4
long8
double8
数组size占4个字节,加上实例数据大小
引用类型开启指针压缩为4,不开启为8
padding8的倍数对齐

对象访问方式

主流的方式有使用句柄跟直接指针两种,HotSpot是使用的直接指针
句柄访问: 变量中存储的是句柄的地址,而句柄中分别存储了对象的类型数据地址(方法区)与对象的实例数据地址(堆)
直接访问: 变量中存储对象的实例数据地址
优缺点:
    句柄访问的方式,如果实例对象地址发生变化,不需要更新变量的地址,但是多了一层访问,访问速度低于直接访问
    直接访问的优点: 访问速度快

  1. 句柄池(先执行一块地址,存储的对象地址与class地址,访问这个对象的地址需要经过两步,但是在gc回收时,效率较高)
    在这里插入图片描述
  2. 直接指针(直接指向对象)
    在这里插入图片描述

JVM的GC执行时机是任何时候都可以吗?

程序执行时,并非所有地方都能停下来GC,只有在特定的位置,才会去去执行GC,这些特定的位置被称为安全点。
这些特定的位置,就是安全点,这些安全点的选定标准是"是否长时间执行"的特性,比如方法调用,循环跳转,异常跳转等
在GC的时候,有两种方案能够让线程准确的停留在安全点上
    抢占式中断: 先让所有线程中断,然后让那些停留在不安全点上的线程跑到安全点上。
    主动试中断: 当需要GC是,设置一个标志,线程执行过程中,当发现这个标志的时候,就会主动挂起线程。
除了在安全点上,还有一些情况,比如线程sleep或者blocked状态,那么安全区域来解决
只要在一个特定区域中,对象引用状态不会发生改变,就可以发起GC,当进入安全区域时,就标记自己已经进入了安全区域,那么,在这段时间发起GC时,就不用管是否在安全点上了

对象引用类型

强引用:指代码中普遍存在的赋值行为,如:Object o = new Object(),只要强引用关系还在,对象就永远不会被回收。
软引用:还有用处,但不是必须存活的对象,JVM会在内存溢出前对其进行回收,例如:缓存。
弱引用:非必须存活的对象,引用关系比软引用还弱,不管内存是否够用,下次GC一定回收。
虚引用:也称“幽灵引用”、“幻影引用”,最弱的引用关系,完全不影响对象的回收,等同于没有引用,虚引用的唯一的目的是对象被回收时会收到一个系统通知。
在这里插入图片描述

虚引用对象使用场景

堆外内存的垃圾回收。

常见的垃圾回收器

在这里插入图片描述

serial:

单线程的垃圾回收器
适合client端使用
优点: 单核效率最高,简单高效
在这里插入图片描述

ParNew:

多线程的垃圾回收器
适合service端使用
优点: 适合多线程使用
对于Serial来说,优化的是STW的时间

在这里插入图片描述

Parallel Scavenge

与Parnew类似,也是多线程的垃圾回收器
不同点在于,更加注重的是吞吐量,可以手动指定吞吐量,也可以自适应

Serial Old:

单线程的垃圾回收器
使用标记整理算法
JDK1.5之前的老年代垃圾回收器或者作为CMS的备选方案

在这里插入图片描述

Parallel Old

多线程的垃圾回收器
与Parallel Scavenge配合,JDK1.6推出。
使用标记整理算法
Parallel Scavenge与Parallel Old配合,用于注重吞吐量的场合

CMS

并发的垃圾回收器
主要是为了优化减少停顿时间
垃圾回收的过程分为了
    初始标记: 主要是找到所有的GC Root,这一步是STW的
    并发标记: 标记这条引用链上的所有对象,这一步是并发执行
    重新标记: 修正并发标记期间产生的变化,这一步是STW的,要比初始标记时间长点,但是远没有并发标记时间长
    并发清除: 并发去清理垃圾,这一步是并发执行的
使用CMS也会产生一些问题
    CMS的线程数的计算公式(CPU数量+3)/4,如果CPU线程数越少,工作线程执行效率越低,例如只有CPU数量只有两个的时候,那么用户线程的工作效率会降低50%
    CMS当老年代分配不下时,会触发Full GC,使用Serial Old单线程垃圾回收器来回收
    CMS采用的是标记清除,所以会产生浮动垃圾,由于工作线程与垃圾回收线程同时运行,那么很有可能会出现明明还有很大空间,但是却找不到一块连续的空间来放这个对象,这时候也会触发Full GC
    CMS的CPU建议在四核以上

在这里插入图片描述

G1

并发的垃圾回收器,可以由用户手动指定停顿时间
垃圾回收过程:
    初始标记
    并发标记
    重新标记
    筛选回收: 根据每个Regin区价值(回收获得的空间大小以及回收所需要的时间)排序,优先回收在用户指定时间内的垃圾

在这里插入图片描述
回收算法: 从两个Regin区间看的话,是采用的复制算法,如果从整体看的话,是标记压缩算法,可以减少内存碎片的产生。
逻辑分代,分为一个一个的Regin区,每一个Regin区可以是为Eden区、Survivor区、Old区,Humongouns可能跨好几个Regin区来存放大对象
G1分成了2048个Regin区,每一个Regin区大小1M-30M之间
Remembered Set中存放的是当前Regin区中,每个对象被哪些对象所引用,这个引用可能跨Rengin
引用关系的记录维护在Remembered Set中,判断存活对象,只需要扫描Remembered Set即可,就不需要扫描整个堆了

在这里插入图片描述

垃圾收集器分类

  • 串行收集器->Serial和Serial Old
    只能有一个垃圾回收线程执行,用户线程暂停。
    适用于内存比较小的嵌入式设备 。
  • 并行收集器[吞吐量优先]->Parallel Scanvenge、Parallel Old
    多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态。
    适用于科学计算、后台处理等若交互场景 。
  • 并发收集器[停顿时间优先]->CMS、G1
    用户线程和垃圾收集线程同时执行(但并不一定是并行的,可能是交替执行的),垃圾收集线程在执行的时
    候不会停顿用户线程的运行。
    适用于相对时间有要求的场景,比如Web 。

频繁的FullGC是怎么回事

首先发生Full GC说明是老年代满了,那么有如下可能性
    一般Full GC的原因是老年代满了,那么老年代满了又有很多种情况
    1. 年轻代满了,对象直接进入老年代,那么像这种情况,调大Young区
    2. 大对象直接进入老年代
    3. 内存泄漏
    4. 频繁调用System.gc()

CMS并发更新失败的原因

因为并发标记阶段,用户线程与垃圾回收线程同时在运行,那么如果此时新进来对象新生代老年代都放不下,那么就可能导致晋升失败
如果是这种情况,那么有如下几个解决办法
    1. 如果是年轻代设置的太小了,导致对象很容易进入老年代,那么年轻代空间设置的较大点即可
    2. 如果老年代设置的太小了,导致对象放不下,那么老年代设置的大一点
    3. 另外,增加老年代的回收频率

CMS与G1的区别

三色标记算法

白色: 未被标记过的对象
灰色: 自身被标记,子节点没有被标记
黑色: 自身与子节点都有被标记
漏标: 满足漏标,必须是黑色对象指向灰色对象,灰色对象指向白色对象,这时候,黑色对象指向白色对象,同时,灰色对象对白色对象的引用消失,这时候就会产生漏标的情况。
那么解决漏标的话,有两种解决方案
    CMS: increment update -> 关注引用增加,也就是将黑色对象重新标记成灰色对象
    G1: SATB -> 关注引用删除,引用删除时,将他加入到栈中,由于有Remembered Set的存在,就不需要扫描整个堆去查找指向白色的引用,效率较高
在这里插入图片描述

为什么G1三色标记要用SATB

SATB是关注的引用删除,当引用删除时,将他加入到一个栈中
当进行回收时,只需要将栈中数据拿出来遍历,并查询Remembered Set就可以解决漏标的问题了,这样就不用扫描整个堆了,效率比较高

JMM

JMM(Java Memory Model): java内存模型,虚拟机用来屏蔽各种硬件和操作系统的内存访问差异,以及如何保证原子性、可见性、有序性
硬件与java内存模型的关系
在这里插入图片描述

为什么会发生伪共享

读取缓存是以缓存行(cache line)为单位的,目前是64个字节
位于同一缓存行的不同数据,同时被两个不同的cpu锁定,相互产生的影响就叫伪共享
如何解决: 缓存行对其

cpu层面是如何实现数据一致性的

  1. 总线锁
    在这里插入图片描述

  2. 缓存一致性协议
    这里列举一种协议MESI
    MESI协议又叫Illinois协议,MESI,“M”, “E”, “S”, "I"这4个字母代表了一个cache line的四种状态,分别是Modified,Exclusive,Shared和Invalid。

    Modified (M)
    cache line只被当前cache所有,并且是dirty的。

    Exclusive (E)
    cache line仅存在于当前缓存中,并且是clean的。

    Shared (S)
    cache line在其他Cache中也存在并且都是clean的。

    Invalid (I)
    cache line无效,即没有被任何Cache加载。

    有一个著名的状态标记图:
    在这里插入图片描述
    对同一个Cache line,

    我标记它为是M时,你只能标记为I

    我标记它为是E时,你只能标记为I

    我标记它为是S时,你只能标记为S或I

    我标记它为是I时,你能标记为MESI

volatile是如何实现的

首先字节码的变量上加一个ACC_VOLATILE标识

  1. 内存可见性
    cpu层面:总线锁或者缓存一致性协议
    jvm层面: Lock开头的指令,相当于将当前内存进行锁定,并写入到主内存中,同时,其他线程的该缓存标记为无效状态。
  2. 防止指令重排序
    cpu内存屏障:
            sfence: 指令前后的写操作不允许重排
            lfence: 指令前后的读操作不允许重排
            mfence: 指令前后的读写操作不允许重排
    jvm内存屏障:
            LoadLoad: Load LoadLoad Load,指令前后的load操作不能重排
            StoreStore: Store StoreStore Store,指令前后的Store操作不能重排
            LoadStore: Load LoadStore Store,指令前的Load操作与指令后的Store操作不能重排
            StoreLoad: Store StoreLoad Load,指令前的Store操作与指令后的Load操作不能重排
    jvm规范:
            LoadLoad
            读操作
            LoadStore
            
            StoreStore
            写操作
            StoreLoad
    具体字节码实现,是在写操作后,加入lock addl 0
    jvm规范是读写前后加屏障,那么具体实现是加lock指令。

DCL单例为什么要加volatile

jvm为了提高效率,允许指令重排序。而在字节码层面,new对象分为INVOKESPECIAL(调用构造方法) ASTORE(将引用指向该地址),

NEW java/lang/Object
DUP
INVOKESPECIAL java/lang/Object.<init> ()V
ASTORE 1

如果指令重排序后,将这两个步骤调换,那么就会先将引用指向该地址,这时候,其他线程来访问时,会得到一个半初始化的对象。

NEW java/lang/Object
DUP
ASTORE 1
INVOKESPECIAL java/lang/Object.<init> ()V

synchronized是怎么实现的

字节码层面:
        方法上: ACC_SYNCHRONIZED
        局部代码块: monitorenter monitorexit
JVM层面: 调用C/C++提供的同步机制
OS和操作系统层面: lock指令

synchronized为什么能够保证数据一致性

synchronized在字节码层面是monitorenter和monitorexit,对应硬件层面是lock与unlock指令,在unlock之前,会把新值同步到主内存中(store、write操作)

mysql

实际项目中,mysql如何优化的

  1. 在医保字典表、字典映射表,对要查询的字典key进行分区,因为一般查询是按照字典key来进行查询的。
  2. 表设计优化
    汇总信息优化:结算相关的数据,每天凌晨用定时任务进行汇总,这样查询汇总信息,只需要查询汇总表即可。

一条sql语句会使用到几个辅助索引

一般情况下,只会使用到一个辅助索引,即使创建了多个辅助索引,也只会选择一个。

mysql如何做优化

表设计优化

  1. 字段类型越小越简单越好,使用整形效率远远高于字符串。
  2. 实数来存储,既要确保精度,又要确保效率,可以使用bigint来代替decimal,因为decimal本质上是字符串
  3. 对于一些固定字符串的列,可以采用枚举类型来代替字符串
  4. 字符串的选择:char>varchar>text
  5. 日期的存储,尽量使用datetime来代替字符串,好处在于占用更少的空间,而且计算性能更高
  6. blob、text这些大字符串,尽量单独存储
  7. 有时为了提高查询性能,采用反范式设计,添加冗余字段,来减少关联查询
  8. 一些常用的汇总信息,单独用一张表查询。

sql语句优化

  • 请求了不需要的数据
    1. 表数据量大的情况下,必须加where条件
    2. 不要写select *,只查询需要的列
    3. 重复查询相同的数据
  1. 用left join代替子查询
  2. join操作尽量控制在三张表以内
  3. 尽量不要用数据库函数,因为不能使用索引
  4. 只返回一条数据时,使用limit 1,效率更高。
  5. 除非必须消除重复行,否则使用union all代替union,因为union需要依赖于临时表进行去重。
  6. 使用分页,当页数较多时,采用join的方式先查询id再进行关联,可以提高效率
  7. 对于一些汇总数量的字段(例如访问人数,下载量等),可以增加多个槽,来提升写的性能。
  8. 尽量不要存null,会降低查询效率
  9. 子查询,尽量不要与外层进行关联

主键设计

  1. 对于安全性不高的表,主键优先选择自增主键,这样可以减少B+树的页分裂合并
  2. 主键的类型越小效率越高

索引优化

  • 索引创建
    1. 索引个数不建议超过5个,复合索引的列不允许超过5个
    2. 索引列尽量不要空
    3. 索引列的类型应该尽量小。
    4. 索引列重复度要尽量低。
    5. 如果列特别长,而且前缀重复度低,可以采用前缀索引
    6. 多列索引的选择:
      如果是需要条件查询来创建索引列,将重复度低的列放在最前面,条件查询的sql顺序不会影响索引的使用。
      如果是order by、group by、distinct这些列创建索引,应该满足最左匹配原则。
    7. 在优化性能时,可以使用相同的列但顺序不同的索引来满足不同类型的查询需求
    8. 尽量满足三星索引
  • 索引使用
    1. 不要在索引列上做任何操作,例如表达式或者函数,这样会使得索引失效。
    2. 联合索引
      尽量全值匹配(联合索引)
      最左匹配原则
      对索引列的范围查询,要放到最后。例如 a b c三个联合索引,对b进行了范围查询,那么再对c进行范围查询,那么只能用到a b的索引。
    3. 尽量使用覆盖索引,避免回表操作。
    4. != <>l会让索引失效
    5. 类型转换不能使用索引,所以查询的值与类型要匹配
    6. like查询,只有前缀匹配才能生效,如果非要使用,覆盖索引会有优化。
    7. or查询最好两个是同一个列,不要不同的列。
      如果必须使用两个不同的列,那么可以将or转换成union all,这样就可以使用索引了。
      另外,使用覆盖索引也能进行优化。
    8. 排序
      查询顺序要一致,不要混用asc与desc
      排序列包含非同一个索引列,也可能会让索引失效
    9. 主键最好使用自增主键,避免使用uuid来做主键,因为使用uuid,可能会造成频繁的页分裂合并。
    10. limit查询如果偏移量过大,可以先查询id,在进行关联查询,效率可以提高
    11. 确保任何的group by和order by中的表达式只涉及到一个表中的列,这样mysql才有可能使用索引来优化这个过程
    12. union all、in、or都能使用索引,但是推荐使用in

其他调优

  1. buffer pool:为了提高读写效率,可以调大buffer pool的大小,存放更多的热点数据,从而提高缓存的命中率,一般生产环境可以调到内存的60%左右
  2. 在服务器允许的情况下,调大连接数,可以同时处理更多地请求(默认151个,最大连接数10w个)
  3. 如果一个方法中,有多个读请求,可以公用一个连接,方法上@Transition(readOnly=true)

mysql 数据查询慢如何解决

  1. 优化sql语句
  2. 加索引
  3. 进行表分区
  4. 读写分离
  5. 分库分表

数据库三范式

  1. 第一范式: 每个字段必须是原子的,不可拆分。
    反例: json字段

  2. 第二范式: 必须有主键(一个主键或者组合主键),非主属性,必须完全依赖于主键,不能依赖部分主键
    反例: 员工部门关系表 -> 主键(员工id, 部门id) 部门名称(只依赖于部门id)
    或者如下图,产品id并不完全依赖于id字段。
    在这里插入图片描述

  3. 第三范式: 没有传递依赖, 非主属性直接依赖主属性,而不能间接依赖主属性
    反例: 员工表 -> 主键(员工id) 员工姓名 所属部门id 部门名称(部门名称依赖于部门id,不依赖于主键)

计数器表设计

计数器表在Web应用中很常见。比如网站点击数、用户的朋友数、文件下载次数等。对于高并发下的处理,首先可以创建一张独立的表存储计数器,这样可使计数器表小且快,并且可以使用一些更高级的技巧。
image.pngimage.png

image.png

比如假设有一个计数器表,只有一行数据,记录网站的点击次数,网站的每次点击都会导致对计数器进行更新,问题在于,对于任何想要更新这一行的事务来说,这条记录上都有一个全局的互斥锁(mutex)。这会使得这些事务只能串行执行,会严重限制系统的并发能力。

怎么改进呢?可以将计数器保存在多行中,每次随机选择一行进行更新。在具体实现上,可以增加一个槽(slot)字段,然后预先在这张表增加100行或者更多数据,当对计数器更新时,选择一个随机的槽(slot)进行更新即可。

image.png

image.png

image.png

数据字段类型优化

  1. 更小的通常更好
  2. 简单就好
  3. 尽量避免NULL:null会占据额外的存储空间,以及索引、索引统计和值比较都更复杂,会让sql优化更加的复杂

整形

TINYINTSMALLINTMEDIUMINTINTBIGINT
8位16位24位32位64位
1字节2字节3字节4字节8字节

int(1)与int(11)有区别吗

存储上,没有区别,只是在可视化工具方面,展示的不同而已。

实数

decimal实际内部使用字符串存储的

floatdoubledecimal
4字节8字节65个数字

字符串类型

char定长,更省空间
varchar 不定长,需要额外1-2个字节来存储长度(255以内,1个字节,超过255两个字节)
同一个字符串,char、varchar占用的磁盘空间是差不多的,但是,varchar读取到内存,可能占用的空间会更大。
所以,对于一些操作,char的性能高于varchar
blob、text存储一个1-4个字节的指针,来指向外部的存储
blob内部使用二进制存储
text有字符集以及排序规则
如果是一些固定的字符串,可以使用枚举类型来存储,可以提升效率

charvarcharblobtextenumset
定长不定长1-4个字节存放指针1-4个字节存放指针内部使用整数存储

时间类型

只精确到秒

datetimetimestamp
1010年-9999年1970年-2038年

数据量特别大,实数来存储,确保精度,确保效率,怎么办?

使用bigint,将原先的数据以10的倍数放大。

text blob优化

一般像这种大的类型,索引处理会比较复杂,可以单独放一行,不要与其他列放在同一行中,这样可以提升查询效率。

命名优化

  1. 可读性原则
    业务名_表的作用
  2. 一般建议全部使用小写
    win不区分大小写,linux区分
  3. 不要使用复数的名字
  4. 不要使用保留字
  5. 索引命名
    主键:pk_字段名
    非主键:uk_字段名

InnoDB与MyISAM的区别

  1. 存储结构不同
    InnoDB只有ibd文件,
    MyISAM分别由.frm(表结构) .MYD(表数据) .MYI(表索引)三个文件组成
    在这里插入图片描述
    在这里插入图片描述

  2. InnoDB支持行锁,MyISAM只支持表锁

  3. InnoDB支持事务,但是MyISAM不支持事务

索引

目的:高效的获取数据的数据

mysql中的索引有哪些

InnoDB
哈希索引
全文索引

哈希索引

适合准确查询,不适合范围查询
不适合排序
不适合组合索引
hash冲突后,会使用链表,查询效率低
image.png

B树

叶子节点也会存储数据
image.png

B+树

B+树是通过二叉查找树、平衡二叉树、B树演化而来
特点:
左树都比小于头节点,右树大于等于头节点
叶子节点按照链表形式进行相连
一个节点有多个数据,也会进行排序
非叶子节点,只存储索引,叶子节点才会存储真实的数据
标准的B+树是叶子结点单链表,mysql是双向链表
在这里插入图片描述
image.png

B*树

在B树的基础上,叶子结点也进行相连
image.png

B树与B+树的区别

区别在于B树非叶子结点也存放数据,而B+树只有叶子节点才会存放数据
B+树的叶子节点会进行相连,可以进行顺序读取,适合范围查询,而b树不可以
因为每个节点存储的大小有限,而B+树的所有节点都是索引,所以每个节点存储的更多,树的高度对于B树来说更低,所以效率更高。

oracle与mysql用的什么数据结构来建立的索引

oracle:B*树
mysql:B+树

mysql的B+树

通过索引,可以找到一整列
image.png

聚簇索引与非聚簇索引

数据跟索引存储在一起,叫做聚簇索引,没有存储在一起叫做非聚簇索引

InnoDB中都是聚簇索引吗?

InnoDB中既有聚簇索引也有非聚簇索引
myisam中只有非聚簇索引

mysql如果没有定义主键,会不会生成索引

innoDB存储引擎在进行数据插入时,数据必须跟某一个索引列存储在一起,这个索引列可以是主键。如果没有主键,选择唯一键,如果没有唯一键,选择6字节的rowid作为主键进行存储。

辅助索引/二级索引

只要创建索引,mysql就会创建一棵B+树
辅助索引的叶子结点中存储的数据不再是整行的记录,而是索引值与主键值。

回表

因为辅助索引的叶子结点中存储的数据是主键值,所以当使用辅助索引时,需要先根据辅助索引查询主键值,然后根据主键值查询到完整的数据。相当于查询一条数据,走了两次索引查询,一次辅助索引,一次主键索引。

为什么辅助索引不存储所有的列呢

  1. 为了节省存储空间
  2. 如果修改了数据,那么需要在多个地方进行数据的修改

为什么有时候建立了索引,但是查询时不走索引

由于回表的存在,回表的记录越少,性能提升就越高,需要回表的记录越多,使用辅助索引的性能就越低,甚至让某些查询宁愿使用全表扫描也不使用辅助索引。
那什么时候采用全表扫描的方式,什么时候使用采用辅助索引 + 回表的方式去执行查询呢?这个就是查询优化器做的工作,查询优化器会事先对表中的记录计算一些统计数据,然后再利用这些统计数据根据查询的条件来计算一下需要回表的记录数,需要回表的记录数越多,就越倾向于使用全表扫描,反之倾向于使用辅助索引 + 回表的方式。

B+树的复合索引是如何存储的

与单列索引一样,只不过key的值换成了多个。
在这里插入图片描述

覆盖索引

从辅助索引中就可以查询到的记录,那么就不会走回表操作,这就叫做覆盖索引。
image.png

mysql自适应哈希索引

InnoDB存储引擎内部自己去监控索引表,如果监控到某个索引经常用,那么就认为是热数据,然后内部自己创建一个hash索引,称之为自适应哈希索引( Adaptive Hash Index,AHI),创建以后,如果下次又查询到这个索引,那么直接通过hash算法推导出记录的地址,直接一次就能查到数据,比重复去B+tree索引中查询三四次节点的效率高了不少。

索引列的选择

创建索引应该选择选择性/离散性高的列。索引的选择性/离散性是指,不重复的索引值(也称为基数,cardinality)和数据表的记录总数(N)的比值,范围从1/N到1之间。索引的选择性越高则查询效率越高,因为选择性高的索引可以让MySQL在查找时过滤掉更多的行。唯一索引的选择性是1,这是最好的索引选择性,性能也是最好的。

很差的索引选择性就是列中的数据重复度很高,比如性别字段,不考虑政治正确的情况下,只有两者可能,男或女。那么我们在查询时,即使使用这个索引,从概率的角度来说,依然可能查出一半的数据出来。

image.png

哪列做为索引字段最好?当然是姓名字段,因为里面的数据没有任何重复,性别字段是最不适合做索引的,因为数据的重复度非常高。

怎么算索引的选择性/离散性?比如person这个表:

SELECT count(DISTINCT name)/count() FROM person;
SELECT count(DISTINCT sex)/count(
) FROM person;
SELECT count(DISTINCT age)/count() FROM person;
SELECT count(DISTINCT area)/count(
) FROM person;

前缀索引

对于很长的字段(例如blob、text、varchar),mysql不支持索引他们的全部长度,需要建立前缀索引。
语法: alter table tableName add key/index (column(X))
X是前缀长度
当使用order by、group by无法使用索引。
如何选择索引长度

SELECT COUNT(DISTINCT LEFT(order_note,3))/COUNT(*) AS sel3,
COUNT(DISTINCT LEFT(order_note,4))/COUNT(*)AS sel4,
COUNT(DISTINCT LEFT(order_note,5))/COUNT(*) AS sel5,
COUNT(DISTINCT LEFT(order_note, 6))/COUNT(*) As sel6,
COUNT(DISTINCT LEFT(order_note, 7))/COUNT(*) As sel7,
COUNT(DISTINCT LEFT(order_note, 8))/COUNT(*) As sel8,
COUNT(DISTINCT LEFT(order_note, 9))/COUNT(*) As sel9,
COUNT(DISTINCT LEFT(order_note, 10))/COUNT(*) As sel10,
COUNT(DISTINCT LEFT(order_note, 11))/COUNT(*) As sel11,
COUNT(DISTINCT LEFT(order_note, 12))/COUNT(*) As sel12,
COUNT(DISTINCT LEFT(order_note, 13))/COUNT(*) As sel13,
COUNT(DISTINCT LEFT(order_note, 14))/COUNT(*) As sel14,
COUNT(DISTINCT LEFT(order_note, 15))/COUNT(*) As sel15,
COUNT(DISTINCT order_note)/COUNT(*) As total
FROM order_exp;

image.png
可以看见,从第10个开始选择性的增加值很高,随着前缀字符的越来越多,选择度也在不断上升,但是增长到第15时,已经和第14没太大差别了,选择性提升的幅度已经很小了,都非常接近整个列的选择性了。

那么针对这个字段做前缀索引的话,从第13到第15都是不错的选择
在上面的示例中,已经找到了合适的前缀长度,如何创建前缀索引:
ALTER TABLE order_exp ADD KEY (order_note(14));

加入需要使用后缀索引,应该怎么办

mysql只支持前缀索引,如果需要使用后缀索引,可以添加一个新列,保存当前列反转后的字符串,然后建立前缀索引。

三星索引

三星索引概念

对于一个查询而言,一个三星索引,可能是其最好的索引。

满足的条件如下:

  • 索引将相关的记录放到一起则获得一星 (比重27%)
  • 如果索引中的数据顺序和查找中的排列顺序一致则获得二星(排序星) (比重27%)
  • 如果索引中的列包含了查询中需要的全部列则获得三星(宽索引星) (比重50%)

这三颗星,哪颗最重要?第三颗星。因为将一个列排除在索引之外可能会导致很多磁盘随机读(回表操作)。第一和第二颗星重要性差不多,可以理解为第三颗星比重是50%,第一颗星为27%,第二颗星为23%,所以在大部分的情况下,会先考虑第一颗星,但会根据业务情况调整这两颗星的优先度。

一星:

一星的意思就是:如果一个查询相关的索引行是相邻的或者至少相距足够靠近的话,必须扫描的索引片宽度就会缩至最短,也就是说,让索引片尽量变窄,也就是我们所说的索引的扫描范围越小越好。

二星(排序星)

在满足一星的情况下,当查询需要排序,group by、 order by,如果查询所需的顺序与索引是一致的(索引本身是有序的),是不是就可以不用再另外排序了,一般来说排序可是影响性能的关键因素。

三星(宽索引星)

在满足了二星的情况下,如果索引中所包含了这个查询所需的所有列(包括 where 子句和 select 子句中所需的列,也就是覆盖索引),这样一来,查询就不再需要回表了,减少了查询的步骤和IO请求次数,性能几乎可以提升一倍。

explain执行计划

一般,我们要优化sql,是看type字段
常见的顺序 system>const>eq_ref>ref>range>index>ALL
一般来说,得保证查询至少达到range级别,最好能达到ref。

查询sql执行过程

  1. 先看缓存是否命中,如果命中,直接返回
  2. 解析查询sql
  3. 对sql进行优化
  4. 执行查询(优化后的sql)
    image.png

红黑树

高度差不超过一一半,它是一棵比较均衡的树,查找、插入、删除 三者操作平均 效率最高)

主从复制流程

在这里插入图片描述

表分区

默认一个表对应一个idb文件,data目录下
使用表分区,可以对数据按照规则进行水平分表,一个逻辑表对应多个idb文件。
优点:
    使用条件查询,当判定为某个区,直接在这个分区中查找即可,不需要扫描整个表
    业务代码不需要改动
    分区单独管理,备份、恢复
缺点:
    写入数据效率略低于不分区
    跨区查询效率较低
常用方式:
    parttition by range: 例如按照id数值分区 0-10 11-20
    parttition by list:根据某个字段值进行分区xxxx values in (1, 2)
    parttition by hash:根据hash函数进行分区
    parttition by key
注意事项:
    要根据查询规则进行分区,尽量保证查询落到一个分区中,尽量保证分区查询在where语句中。
    例如:根据学生姓名查询老师姓名,那么以学生姓名作为分区较为合理。

分库分表

划分规则

  1. 按照业务进行划分
  2. 按照读写评率进行划分,例如按照读多写少的数据还是读少写多的数据
  3. 按照核心与非核心业务进行划分,这样即使非核心库挂了,不影响核心库的使用。

分表规则

垂直分表
水平分表

分表方法

  1. 范围分表
    范围过小,子表数量增多,维护成本增大
    范围过大,依然存在单表性能问题。
    分表依据:分表后,能否满足系统要求。
    优点:可以平滑的扩充新表,只需要增加子表数据量,原有的数据不用动
    缺点:数据分配不均匀
  2. hash
    选取列,根据列进行hash取模
    优点:数据分配均匀
    缺点:缩容扩容比较麻烦,所有的需要重新算一遍hash

分库分表问题

  1. 分布式id问题,每个库的id不能重复
    解决办法:全局id生成服务(机器号+序列号+时间戳),高性能,高可用,易使用
    uuid、数据库自增、分号段、redis自增、雪花算法、滴滴的TinyId、百度的UIDGenerator,美团的leaf
  2. 拆分维度问题
    例如电商系统中,用户id 订单id 商品id 例如使用用户id查询的较为频繁,那么适合用用户id进行划分。
    如果是需要满足多个维度的查询,那么按照多个维度重复存储来解决。
    或者建立索引表(可以放redis中),比如划分规则是用户id,但是要用订单id进行查询,那么可以建立订单id与用户id的索引表,最终根据用户id来查询数据。
  3. join问题
    单库join没问题,多库join失败
    解决办法:代码层面使用join、导入es进行查询
    数据库sql越简单越好。禁止3张以上的表做关联,禁用存储过程,禁用触发器。
  4. 事务问题
    XA两阶段提交,不好用。
    ShardingSphere
  5. 成本问题
    非必要不分库,不要过度设计。

分库分表实现方案

Sharding-JDBC、mycat

读写分离

目的:分流
适用场景:读多写少的情况
原因:数据库的锁会对数据库并发产生影响
X锁-写锁,只能有一个线程去进行写操作,别的线程读写都进行阻塞
S锁-读锁,多个线程可以同时去读
一主(写)多从(读)
如何实现:
    根据sql语句进行自动路由
    select 路由到从库
    insert、update、delete路由到主库
主从同步问题:
    写主库,如何高效的同步到从库
    主库开启binlog日志,传给从库,写入从库的relayLog,解析relayLog,重新数据。
    时间差问题:
        写数据后的查询,指向到主库中去。
        先查询从库,如果从库没有再查询主库。
        主业务走主库,非主业务走从库。

索引合并

mysql在一般情况下,执行一个查询时最多只会用到单个二级索引,但也可能在一个查询中使用到多个二级索引。mysql将使用到多个索引来完成一次查询的执行方法称之为索引合并(index merge)

  • Intersection 合并
    两个索引使用and条件进行连接
    触发Intersection 合并的前提是必须等值匹配,另外,如果是组合索引,必须全部列都匹配的情况才可以。但是具体是否使用索引合并,还是看查询优化器。
  • Union合并
    两个索引使用or条件进行连接
    触发条件也跟Intersection 合并一样
  • Sort-Union合并
    两个索引中,其中有一个使用了范围查询,那么对于范围查询,可能会先对查询出来的主键进行排序后进行合并,然后再走回表操作。

对于 Intersection 合并可以使用联合索引来进行优化。

普通索引与唯一索引的区别

事务、事务隔离级别

事务特性

事务应该具有4个属性:原子性、一致性、隔离性、持久性。这四个属性通常称为ACID特性。

l 原子性(atomicity)
l 一致性(consistency)
l 隔离性(isolation)
l 持久性(durability)

事务并发引发的问题

  • 脏读

当一个事务读取到了另外一个事务修改但未提交的数据,被称为脏读。

image.png

1、在事务A执⾏过程中,事务A对数据资源进⾏了修改,事务B读取了事务A修改后的数据。
2、由于某些原因,事务A并没有完成提交,发⽣了RollBack操作,则事务B读取的数据就是脏数据。
这种读取到另⼀个事务未提交的数据的现象就是脏读(Dirty Read)。

  • 不可重复读

当事务内相同的记录被检索两次,且两次得到的结果不同时,此现象称为不可重复读。

image.png

事务B读取了两次数据资源,在这两次读取的过程中事务A修改了数据,导致事务B在这两次读取出来的
数据不⼀致。

  • 幻读

在事务执行过程中,另一个事务将新记录添加到正在读取的事务中时,会发生幻读。

image.png

事务B前后两次读取同⼀个范围的数据,在事务B两次读取的过程中事务A新增了数据,导致事务B后⼀
次读取到前⼀次查询没有看到的⾏。
幻读和不可重复读有些类似,但是幻读重点强调了读取到了之前读取没有获取到的记录。

并发事务执行的严重层度排序

脏读 > 不可重复读 > 幻读

四种隔离级别

  • SQL标准中的四种隔离级别
    READ UNCOMMITTED:未提交读。
    READ COMMITTED:已提交读。
    REPEATABLE READ:可重复读。
    SERIALIZABLE:可串行化。
    READ UNCOMMITTED隔离级别下,可能发生脏读、不可重复读和幻读问题。
    READ COMMITTED隔离级别下,可能发生不可重复读和幻读问题,但是不可以发生脏读问题。
    REPEATABLE READ隔离级别下,可能发生幻读问题,但是不可以发生脏读和不可重复读的问题。
    SERIALIZABLE隔离级别下,各种问题都不可以发生。
    image.png
  • MySQL中的隔离级别
    image.png
    MySQL的默认隔离级别为REPEATABLE READ,我们可以手动修改事务的隔离级别。

保存点

如果你开启了一个事务,执行了很多语句,忽然发现某条语句有点问题,你只好使用ROLLBACK语句来让数据库状态恢复到事务执行之前的样子,然后一切从头再来,但是可能根据业务和数据的变化,不需要全部回滚。所以MySQL里提出了一个保存点(英文:savepoint)的概念,就是在事务对应的数据库语句中打几个点,我们在调用ROLLBACK语句时可以指定会滚到哪个点,而不是回到最初的原点。定义保存点的语法如下:

SAVEPOINT 保存点名称;

当我们想回滚到某个保存点时,可以使用下边这个语句(下边语句中的单词WORK和SAVEPOINT是可有可无的):

ROLLBACK TO [SAVEPOINT] 保存点名称;

隐式提交

当我们使用START TRANSACTION或者BEGIN语句开启了一个事务,或者把系统变量autocommit的值设置为OFF时,事务就不会进行自动提交,但是如果我们输入了某些语句之后就会悄悄的提交掉,就像我们输入了COMMIT语句了一样,这种因为某些特殊的语句而导致事务提交的情况称为隐式提交,这些会导致事务隐式提交的语句包括:

  1. 执行DDL

  2. 隐式使用或修改mysql数据库中的表
    当我们使用ALTER USER、CREATE USER、DROP USER、GRANT、RENAME USER、REVOKE、SET PASSWORD等语句时也会隐式的提交前边语句所属于的事务。

  3. 事务控制或关于锁定的语句
    当我们在一个会话里,一个事务还没提交或者回滚时就又使用START TRANSACTION或者BEGIN语句开启了另一个事务时,会隐式的提交上一个事务,比如这样:

     BEGIN;
     SELECT ... # 事务中的一条语句
     UPDATE ... # 事务中的一条语句
     ... # 事务中的其它语句
     BEGIN; # 此语句会隐式的提交前边语句所属于的事务
    
  4. 加载数据的语句
    比如我们使用LOAD DATA语句来批量往数据库中导入数据时,也会隐式的提交前边语句所属的事务。

  5. 关于MySQL复制的一些语句
    使用START SLAVE、STOP SLAVE、RESET SLAVE、CHANGE MASTER TO等语句时也会隐式的提交前边语句所属的事务。

  6. 其它的一些语句
    使用ANALYZE TABLE、CACHE INDEX、CHECK TABLE、FLUSH、 LOAD INDEX INTO CACHE、OPTIMIZE TABLE、REPAIR TABLE、RESET等语句也会隐式的提交前边语句所属的事务。

MVCC原理

全称Multi-Version Concurrency Control,即多版本并发控制,主要是为了提高数据库的并发性能。
同一行数据平时发生读写请求时,会上锁阻塞住。但MVCC用更好的方式去处理读—写请求,做到在发生读—写请求冲突时不用加锁。
这个读是指的快照读,而不是当前读,当前读是一种加锁操作,是悲观锁。

版本链

我们知道,对于使用InnoDB存储引擎的表来说,它的聚簇索引记录中都包含两个必要的隐藏列(row_id并不是必要的,我们创建的表中有主键或者非NULL的UNIQUE键时都不会包含row_id列):
trx_id:每次一个事务对某条聚簇索引记录进行改动时,都会把该事务的事务id赋值给trx_id隐藏列。
roll_pointer:每次对某条聚簇索引记录进行改动时,都会把旧的版本写入到undo日志中,然后这个隐藏列就相当于一个指针,可以通过它来找到该记录修改前的信息。

(补充点:undo日志:为了实现事务的原子性,InnoDB存储引擎在实际进行增、删、改一条记录时,都需要先把对应的undo日志记下来。一般每对一条记录做一次改动,就对应着一条undo日志,但在某些更新记录的操作中,也可能会对应着2条undo日志。一个事务在执行过程中可能新增、删除、更新若干条记录,也就是说需要记录很多条对应的undo日志,这些undo日志会被从0开始编号,也就是说根据生成的顺序分别被称为第0号undo日志、第1号undo日志、…、第n号undo日志等,这个编号也被称之为undo no。)

每次对记录进行改动,都会记录一条undo日志,每条undo日志也都有一个roll_pointer属性(INSERT操作对应的undo日志没有该属性,因为该记录并没有更早的版本),可以将这些undo日志都连起来,串成一个链表,所以现在的情况就像下图一样:
在这里插入图片描述
对该记录每次更新后,都会将旧值放到一条undo日志中,就算是该记录的一个旧版本,随着更新次数的增多,所有的版本都会被roll_pointer属性连接成一个链表,我们把这个链表称之为版本链,版本链的头节点就是当前记录最新的值。另外,每个版本中还包含生成该版本时对应的事务id。于是可以利用这个记录的版本链来控制并发事务访问相同记录的行为,那么这种机制就被称之为多版本并发控制(Mulit-Version Concurrency Control MVCC)。

ReadView

对于使用READ UNCOMMITTED隔离级别的事务来说,由于可以读到未提交事务修改过的记录,所以直接读取记录的最新版本就好了(所以就会出现脏读、不可重复读、幻读)。
对于使用SERIALIZABLE隔离级别的事务来说,InnoDB使用加锁的方式来访问记录(也就是所有的事务都是串行的,当然不会出现脏读、不可重复读、幻读)。

对于使用READ COMMITTED和REPEATABLE READ隔离级别的事务来说,都必须保证读到已经提交了的事务修改过的记录,也就是说假如另一个事务已经修改了记录但是尚未提交,是不能直接读取最新版本的记录的,核心问题就是:READ COMMITTED和REPEATABLE READ隔离级别在脏读和不可重复读上的区别是从哪里来的,其实结合前面的知识,这两种隔离级别关键是需要判断一下版本链中的哪个版本是当前事务可见的
为此,InnoDB提出了一个ReadView的概念(作用于SQL查询语句),

这个ReadView中主要包含4个比较重要的内容:
**m_ids:**表示在生成ReadView时当前系统中活跃的读写事务的事务id列表。
**min_trx_id:**表示在生成ReadView时当前系统中活跃的读写事务中最小的事务id,也就是m_ids中的最小值。
**max_trx_id:**表示生成ReadView时系统中应该分配给下一个事务的id值。注意max_trx_id并不是m_ids中的最大值,事务id是递增分配的。比方说现在有id为1,2,3这三个事务,之后id为3的事务提交了。那么一个新的读事务在生成ReadView时,m_ids就包括1和2,min_trx_id的值就是1,max_trx_id的值就是4。
**creator_trx_id:**表示生成该ReadView的事务的事务id。

如果被访问版本的trx_id属性值与ReadView中的creator_trx_id值相同,意味着当前事务在访问它自己修改过的记录,所以该版本可以被当前事务访问。
如果被访问版本的trx_id属性值小于ReadView中的min_trx_id值,表明生成该版本的事务在当前事务生成ReadView前已经提交,所以该版本可以被当前事务访问。
如果访问的版本的trx_id属性值在min_trx_id与max_trx_id之间(或者在m_ids中存在),并且creator_trx_id值不相同,那么说明该版本不可以被当前事务访问。
在这里插入图片描述

如何解决脏读

READ COMMITTED隔离级别的事务在每次查询开始时都会生成一个独立的ReadView。

在这里插入图片描述

trx 30: begin;
trx 20: begin;
trx 20: update person set age=22 where id=1;
trx 30: select * from person where id=1;

在执行SELECT语句时会先生成一个ReadView:
ReadView的m_ids列表的内容就是[20, 30],min_trx_id为20,max_trx_id为40,creator_trx_id为30。

从版本链中挑选可见的记录,此时最新版本的列age的内容是’22’,该版本的trx_id值为20,在m_ids列表内,所以不符合可见性要求(trx_id属性值在ReadView的min_trx_id和max_trx_id之间说明创建ReadView时生成该版本的事务还是活跃的,该版本不可以被访问;如果不在,说明创建ReadView时生成该版本的事务已经被提交,该版本可以被访问),根据roll_pointer跳到下一个版本。

下一个版本的列age的内容是’20’,该版本的trx_id值为10,小于ReadView中的min_trx_id值,所以这个版本是符合要求的,最后返回给用户的版本就是这条列age为’20’的记录。

trx 20: update person set age=28 where id=1;
trx 20: commit;
trx 30: update person set name='王五' where id=1;
trx 30: select * from person where id=1;

在执行SELECT语句时会重新生成一个ReadView:
ReadView的m_ids列表的内容就是[30],min_trx_id为30,max_trx_id为40,creator_trx_id为30。

从版本链中挑选可见的记录,此时最新版本的列name的内容是’王五’,该版本的trx_id值为30,在m_ids列表内,所以不符合可见性要求(trx_id属性值在ReadView的min_trx_id和max_trx_id之间说明创建ReadView时生成该版本的事务还是活跃的,该版本不可以被访问;如果不在,说明创建ReadView时生成该版本的事务已经被提交,该版本可以被访问),根据roll_pointer跳到下一个版本。

下一个版本的列age的内容是’28’,该版本的trx_id值为20,小于ReadView中的min_trx_id值,所以这个版本是符合要求的,最后返回给用户的版本就是这条列age为’28’的记录。

所以有了这种机制,就不会发生脏读问题!因为会去判断活跃版本,必须是不在活跃版本的才能用,不可能读到没有 commit的记录。

但会出现不可重复读问题。

明显上面一个事务中两次
image.png

如何解决不可重复读

REPEATABLE READ解决不可重复读问题
对于使用REPEATABLE READ隔离级别的事务来说,只会在第一次执行查询语句时生成一个ReadView,之后的查询就不会重复生成了。
在这里插入图片描述
trx 30: begin;
trx 20: begin;
trx 20: update person set age=22 where id=1;
trx 30: select * from person where id=1;

在执行SELECT语句时会生成一个ReadView:
ReadView的m_ids列表的内容就是[20, 30],min_trx_id为20,max_trx_id为40,creator_trx_id为30。

从版本链中挑选可见的记录,此时最新版本的列age的内容是’22’,该版本的trx_id值为20,在m_ids列表内,所以不符合可见性要求(trx_id属性值在ReadView的min_trx_id和max_trx_id之间说明创建ReadView时生成该版本的事务还是活跃的,该版本不可以被访问;如果不在,说明创建ReadView时生成该版本的事务已经被提交,该版本可以被访问),根据roll_pointer跳到下一个版本。

下一个版本的列age的内容是’20’,该版本的trx_id值为10,小于ReadView中的min_trx_id值,所以这个版本是符合要求的,返回给用户的版本就是这条列age为’20’的记录。

trx 20: update person set age=28 where id=1;
trx 20: commit;
trx 30: update person set name='王五' where id=1;
trx 30: select * from person where id=1;

执行SELECT语句时不会重新生成一个ReadView,而是使用第一次生成的:
ReadView的m_ids列表的内容还是[30],min_trx_id为20,max_trx_id为40,creator_trx_id为30。

从版本链中挑选可见的记录,此时最新版本的列name的内容是’王五’,该版本的trx_id值为30,在m_ids列表内,所以不符合可见性要求(trx_id属性值在ReadView的min_trx_id和max_trx_id之间说明创建ReadView时生成该版本的事务还是活跃的,该版本不可以被访问;如果不在,说明创建ReadView时生成该版本的事务已经被提交,该版本可以被访问),根据roll_pointer跳到下一个版本。

下一个版本的列age的内容是’28’,该版本的trx_id值为20,也在m_ids列表内,所以也不符合要求,继续跳到下一个版本。
下一个版本的列age的内容是’22’,该版本的trx_id值也为20,也在m_ids列表内,所以也不符合要求,继续跳到下一个版本。
下一个版本的列age的内容是’20’,该版本的trx_id值为10,小于ReadView中的min_trx_id值,所以这个版本是符合要求的,最后返回给用户的版本就是这条列age为’20’的记录。
根据前面的分析,返回的值还是age为’20’的这条记录。
也就是说两次SELECT查询得到的结果是重复的,记录的列age值都是’20’,这就是可重复读的含义。

能否解决幻读问题

幻读是一个事务按照某个相同条件多次读取记录时,后读取时读到了之前没有读到的记录,而这个记录来自另一个事务添加的新记录。
READ隔离级别下的事务T1先根据某个搜索条件读取到多条记录,然后事务T2插入一条符合相应搜索条件的记录并提交,然后事务T1再根据相同搜索条件执行查询。结果会是什么?按照ReadView中的比较规则(后两条):
3、如果被访问版本的trx_id属性值大于或等于ReadView中的max_trx_id值,表明生成该版本的事务在当前事务生成ReadView后才开启,所以该版本不可以被当前事务访问。
4、如果被访问版本的trx_id属性值在ReadView的min_trx_id和max_trx_id之间(min_trx_id < trx_id < max_trx_id),那就需要判断一下trx_id属性值是不是在m_ids列表中,如果在,说明创建ReadView时生成该版本的事务还是活跃的,该版本不可以被访问;如果不在,说明创建ReadView时生成该版本的事务已经被提交,该版本可以被访问。

不管事务T2比事务T1是否先开启,事务T1都是看不到T2的提交的。请自行按照上面介绍的版本链、ReadView以及判断可见性的规则来分析一下。
但是,在REPEATABLE READ隔离级别下InnoDB中的MVCC 可以很大程度地避免幻读现象,而不是完全禁止幻读
在这里插入图片描述

trx 30:begin;
trx 20: begin;
trx 30: select * from person where id=80;
trx 20: insert into person (id, name, age) values (80, '李四', '20');
trx 30: update person set age=28 where id=80;
trx 30: select * from person where id=80;

根据运行结果可以发现,在trx 30中,第二次读到了第一次没有读到的数据,产生了幻读。

mysql锁

锁分类

  1. 共享锁(Shared Locks),简称S锁。
  2. 独占锁(Exclusive Locks),也常称排他锁,简称X锁。

X 不兼容X 不兼容S
S 不兼容X 兼容S

锁定读操作的select

  1. 共享锁
    事务中开启共享锁

     SELECT * from test LOCK IN SHARE MODE;
    
  2. 排他锁(独占锁)
    事务中开启独占锁

     SELECT * from test FOR UPDATE;
    

写操作的锁

  • insert
    一般情况下,新插入一条记录的操作并不加锁,InnoDB通过一种称之为隐式锁来保护这条新插入的记录在本事务提交前不被别的事务访问。当然,在一些特殊情况下INSERT操作也是会获取锁的

  • delete
    对一条记录做DELETE操作的过程其实是先在B+树中定位到这条记录的位置,然后获取一下这条记录的X锁,然后再执行delete mark操作。我们也可以把这个定位待删除记录在B+树中位置的过程看成是一个获取X锁的锁定读。

  • update
    在对一条记录做UPDATE操作时分为三种情况:

    1. 如果未修改该记录的键值并且被更新的列占用的存储空间在修改前后未发生变化,则先在B+树中定位到这条记录的位置,然后再获取一下记录的X锁,最后在原记录的位置进行修改操作。其实我们也可以把这个定位待修改记录在B+树中位置的过程看成是一个获取X锁的锁定读
    2. 如果未修改该记录的键值并且至少有一个被更新的列占用的存储空间在修改前后发生变化,则先在B+树中定位到这条记录的位置,然后获取一下记录的X锁,将该记录彻底删除掉(就是把记录彻底移入垃圾链表),最后再插入一条新记录。这个定位待修改记录在B+树中位置的过程看成是一个获取X锁的锁定读,新插入的记录由INSERT操作提供的隐式锁进行保护
    3. 如果修改了该记录的键值,则相当于在原记录上做DELETE操作之后再来一次INSERT操作,加锁操作就需要按照DELETE和INSERT的规则进行了。

锁的粒度

  1. 表锁
  2. 行锁
  • 表锁与行锁的比较
    锁定粒度:表锁 > 行锁
    加锁效率:表锁 > 行锁
    冲突概率:表锁 > 行锁
    并发性能:表锁 < 行锁

为了解决表锁与行锁的互斥,所以产生了意向锁。

意向锁

如果要上行锁,首先要在表上,添加一个意向锁,来标识这行记录被上锁,这样当上表锁时,就不需要检查每一行的记录了。
意向共享锁 ,英文名:Intention Shared Lock,简称IS锁。当事务准备在某条记录上加S锁时,需要先在表级别加一个IS锁。
意向独占锁 ,英文名:Intention Exclusive Lock,简称IX锁。当事务准备在某条记录上加X锁时,需要先在表级别加一个IX锁。
表级别的各种锁的兼容性:

兼容性XIXSIS
X不兼容不兼容不兼容不兼容
IX不兼容不兼容
S不兼容不兼容
IS不兼容

锁的组合性:(意向锁没有行锁

组合性XIXSIS
表锁
行锁

当我们在对使用InnoDB存储引擎的表的某些记录加S锁之前,那就需要先在表级别加一个IS锁,当我们在对使用InnoDB存储引擎的表的某些记录加X锁之前,那就需要先在表级别加一个IX锁。

IS锁和IX锁的使命只是为了后续在加表级别的S锁和X锁时判断表中是否有已经被加锁的记录,以避免用遍历的方式来查看表中有没有上锁的记录。我们并不能手动添加意向锁,只能由InnoDB存储引擎自行添加。

表锁

  1. S锁
  2. X锁
  3. 元数据锁(Metadata Locks,简称MDL)
    执行DDL语句会上元数据锁。
  4. IS锁
    意向共享锁
  5. IX锁
    意向排他锁
  6. AUTO-INC锁
    表级别锁
    轻量级锁
    默认情况是混合这来,如果能确定条数,那么就用轻量级锁,如果不能确定条数,就用表级别锁。这两个的主要区别是,轻量级锁获取到自增值后,就会释放锁,但是表级别锁需要等到这条语句插入完后,才释放。

行锁

只有通过索引条件检索数据,InnoDB 才使用行级锁,否则,InnoDB 将使用表锁。

不论是使用主键索引、唯一索引或普通索引,InnoDB都会使用行锁来对数据加锁。
只有执行计划真正使用了索引,才能使用行锁

记录锁(Record Locks)

也叫记录锁,就是仅仅把一条记录锁上,官方的类型名称为:LOCK_REC_NOT_GAP。
在这里插入图片描述

记录锁是有S锁和X锁之分的,当一个事务获取了一条记录的S型记录锁后,其他事务也可以继续获取该记录的S型记录锁,但不可以继续获取X型记录锁;当一个事务获取了一条记录的X型记录锁后,其他事务既不可以继续获取该记录的S型记录锁,也不可以继续获取X型记录锁;

间隙锁

行锁的一种特殊情况:间隙锁:值在范围内,但却不存在
对于键值在条件范围内但并不存在的记录,叫做“间隙(GAP)”,InnoDB也会对这个“间隙”加锁,这种锁机制就是所谓的间隙锁(Next-Key锁)。
使用间隙锁,会对叶子节点的两端进行加锁
在这里插入图片描述
间隙锁的目的是为了防止幻读,以满足相关隔离级别的要求
以下sql会产生间隙锁:

-- 会对id大于10并且小于40之间进行加锁,加入这个时候,有id=20的想进行插入操作,那么会进行阻塞
update person set age =10 where id>10 and price<40;
-- 会对id在5跟7之间进行上锁
select * from person WHERE id BETWEEN 5 AND 7 FOR UPDATE;

在这里插入图片描述

-- 会对50这条记录页的上下进行上锁
insert into person (id, name, age) values (50, '张三', 20);

总结
有两种情况会产生间隙锁

  1. 为一个范围进行上锁。
  2. 插入一条记录,会对这个记录所在页的左右进行上锁。

死锁

产生情况:
两个事务互相抢占两个相同的资源,例如

事务A: select * from person where id = 1 for update;
事务B:  select * from person where id = 2 for update;
事务A: select * from person where id = 2 for update;
事务B:  select * from person where id = 1 for update;

mysql会自动处理死锁,这种情况,会将事务B进行回滚,并让事务A获取到id为2的资源。

两表连接的过程

  1. 确定驱动表
  2. 遍历驱动表,到被驱动表中查询匹配记录
    驱动表:连接查询中,第一个确定查询的表就是驱动表
    驱动表只需要访问一次,被驱动表可能被访问多次

优化:
被驱动表上的关联字段作为索引,就不需要进行全表扫描了,可以加快索引查询速度
如果没有索引,并且驱动表数据量过大,可以调大join buffer的大小,减少磁盘的IO次数。

join 连接原理

  1. Simple Nested-Loop Join(SNL,简单嵌套循环连接)
    驱动表中每一行对应被驱动表的一次全表扫描
    例如驱动表User,被驱动表UserInfo 的sql是 select * from User u left join User_info info on u.id = info.user_id,其实就是我们常用的for循环,伪代码的逻辑应该是

     for(User u:Users){
         for(UserInfo info:UserInfos){
             if(u.id == info.userId){
            	 //得到匹配数据
             }
         }
     }
    

    在这里插入图片描述
    简单粗暴的算法,每次从User表中取出一条数据,然后扫描User_info中的所有记录匹配,最后合并数据返回。

    假如驱动表User有10条数据,被驱动表UserInfo也有10条数据,那么实际上驱动表User会被扫描10次,而被驱动表会被扫描10*10=100次(每扫描一次驱动表,就会扫描全部的被驱动表),这种效率是很低的,对数据库的开销比较大,尤其是被驱动表。每一次扫描其实就是从硬盘中读取数据加载到内存中,也就是一次IO,目前IO是最大的瓶颈

  2. Index Nested-Loop Join(INL,索引嵌套循环连接)
    索引嵌套循环是使用索引减少扫描的次数来提高效率的,所以要求非驱动表上必须有索引才行。

    在查询的时候,驱动表(User) 会根据关联字段的索引进行查询,当索引上找到符合的值,才会进行回表查询。如果非驱动表(User_info)的关联字段(user_id)是主键的话,查询效率会非常高(主键索引结构的叶子结点包含了完整的行数据(InnoDB)),如果不是主键,每次匹配到索引后都需要进行一次回表查询(根据二级索引(非主键索引)的主键ID进行回表查询),性能肯定弱于主键的查询。
    在这里插入图片描述
    上图中的索引查询之后不一定会回表,什么情况下会回表,这个要看索引查询到的字段能不能满足查询需要的字段

  3. Block Nested-Loop Join(BNL,块嵌套循环连接)
    如果存在索引,那么会使用index的方式进行join,如果join的列没有索引,被驱动表要扫描的次数太多了,每次访问被驱动表,其表中的记录都会被加载到内存中,然后再从驱动表中取一条与其匹配,匹配结束后清除内存,然后再从驱动表中加载一条记录 然后把被驱动表的记录在加载到内存匹配,这样周而复始,大大增加了IO的次数。为了减少被驱动表的IO次数,就出现了Block Nested-Loop Join的方式。

    不再是逐条获取驱动表的数据,而是一块一块的获取,引入了join buffer缓冲区,将驱动表join相关的部分数据列(大小是join buffer的限制)缓存到join buffer中,然后全表扫描被驱动表,被驱动表的每一条记录一次性和join buffer中的所有驱动表记录进行匹配(内存中操作),将简单嵌套循环中的多次比较合并成一次,降低了被驱动表的访问频率。

    在这里插入图片描述

    驱动表能不能一次加载完,要看join buffer能不能存储所有的数据,默认情况下join_buffer_size=256k,查询的时候Join Buffer 会缓存所有参与查询的列而不是只有join关联的列,在一个有N个join关联的sql中会分配N-1个join buffer。所以查询的时候尽量减少不必要的字段,可以让join buffer中可以存放更多的列。

    可以调整join_buffer_size的缓存大小show variables like '%join_buffer%'这个值可以根据实际情况更改。
    在这里插入图片描述

日志

线上环境,一般需要开启慢日志查询,以及bin_log日志。
mysql日志常见的有哪些
错误日志
慢查询日志
bin_log日志:记录全量的ddl语句与dml语句。
redo日志:确保事务的持久性(确保commit)
undo日志:保证事务的原子性(确保rollback)
redo log节省随机写磁盘的IO消耗
change buffer 节省随机读磁盘的IO操作(如果该页在内存中,可以直接操作内存,后续执行merge操作即可,如果写请求后,接着来了一个读请求,也会触发merge操作)

redo log

redo log称为重做日志,每当有操作时,在数据变更之前将操作写入redo log,这样当发生掉电之类的情况时系统可以在重启后继续操作。
redo log 是配合buffer pool来使用的
redo log是InnoDB独有的
redo log是实现了ACID中的D(持久性)

binlog与redo log的区别

redo log是物理日志,所以恢复速度很快。redo log是mysql自己使用的,用于保证数据库崩溃时的事务持久性。
binlog 是逻辑日志,记录全量的ddl语句与dml语句,用来人工恢复数据使用

binlog 不能记录数据是否记录到磁盘中,但是redo log在执行完成以后,就会进行抹除。
binlog会记录所有存储引擎的日志,但是redo log只会记录InnoDB的日志。

redo日志格式

redo日志本质上只是记录了一下事务对数据库做了哪些修改。 InnoDB们针对事务对数据库的不同修改场景定义了多种类型的redo日志,但是绝大部分类型的redo日志都有下边这种通用的结构:
image.png
type:该条redo日志的类型,redo日志设计大约有53种不同的类型日志。
space ID:表空间ID。
page number:页号。
data:该条redo日志的具体内容。

undo log

undo log称为撤销日志,当一些变更执行到一半无法完成时,可以根据撤销日志恢复到变更之间的状态。
undo log是记录的数据修改之前的值

数据库表很大,性能下降?

如果表有索引
增删改变慢,因为要维护B+树
查询速度:

  1. 1个或者少量的查询依旧很快
  2. 并发大的时候,会受硬盘带宽影响速度(并发量大,以为这读取的数据大小会变大,但是硬盘的带宽有限,所以会对性能产生影响)。
    硬盘的读取速度要受到寻址速度与读取带宽的制约。

mysql查询执行过程

  1. 查询缓存
  2. 解析器
    1. 词法解析(将sql语句打碎成一个个的单词)
    2. 生成解析语法树,并且语法进行验证
    3. 预处理器(验证表名、列名、别名等是否存在问题,保证没有歧义)
  3. 查询优化
    1. 条件化简、sql重写
    2. 生成执行计划
  4. 查询执行引擎
    在这里插入图片描述

mysql更新流程

在这里插入图片描述

mysql执行成本

执行成本=I/O成本+CPU成本

  • I/O成本:MySQL规定读取一个页面花费的成本默认是1.0
  • CPU成本:读取以及检测一条记录是否符合搜索条件的成本默认是0.2(CPU成本)

执行成本优化步骤

  1. 根据搜索条件,找出所有可能使用的索引
  2. 计算全表扫描的代价
  3. 计算使用不同索引执行查询的代价
  4. 对比各种执行方案的代价,找出成本最低的那一个

连接查询的成本

  1. 单次查询驱动表的成本
  2. 多次查询被驱动表的成本(具体查询多少次取决于对驱动表查询的结果集中有多少条记录)

对驱动表进行查询后得到的记录条数称之为驱动表的 扇出 (英文名:fanout)。
很显然驱动表的扇出值越小,对被驱动表的查询次数也就越少,连接查询的总成本也就越低。
连接查询总成本 = 单次访问驱动表的成本 + 驱动表扇出数 x 单次访问被驱动表的成本

MySQL的查询重写规则

条件化简

  1. 移除无用的括号
    有时候表达式里有许多无用的括号,比如这样:

     ((a = 5 AND b =c) OR ((a > c) AND (c < 5)))
    

    看着就很烦,优化器会把那些用不到的括号给干掉,就是这样:

     (a = 5 and b =c) OR (a > c AND c < 5)
    
  2. 常量传递
    有时候某个表达式是某个列和某个常量做等值匹配,比如这样:

     a = 5
    

    当这个表达式和其他涉及列a的表达式使用AND连接起来时,可以将其他表达式中的a的值替换为5,比如这样:

     a = 5 AND b >a
    

    就可以被转换为:

     a = 5 AND b >5
    

    等值传递(equality_propagation)
    有时候多个列之间存在等值匹配的关系,比如这样:

     a = b and b = c and c = 5
    

    这个表达式可以被简化为:

     a = 5 and b = 5 and c = 5
    
  3. 移除没用的条件
    对于一些明显永远为TRUE或者FALSE的表达式,优化器会移除掉它们,比如这个表达式:

     (a < 1 and b= b) OR (a = 6 OR 5 != 5)
    

    很明显,b = b这个表达式永远为TRUE,5 != 5这个表达式永远为FALSE,所以简化后的表达式就是这样的:

     (a < 1 and TRUE) OR (a = 6 OR FALSE)
    

    可以继续被简化为

     a < 1 OR a =6
    
  4. 表达式计算
    在查询开始执行之前,如果表达式中只包含常量的话,它的值会被先计算出来,比如这个:

     a = 5 + 1
    

    因为5 + 1这个表达式只包含常量,所以就会被化简成:

     a = 6
    

    但是这里需要注意的是,如果某个列并不是以单独的形式作为表达式的操作数时,比如出现在函数中,出现在某个更复杂表达式中,就像这样:

     ABS(a) > 5
    

    或者:

     -a < -8
    

    优化器是不会尝试对这些表达式进行化简的。我们前边说过只有搜索条件中索引列和常数使用某些运算符连接起来才可能使用到索引,所以如果可以的话,最好让索引列以单独的形式出现在表达式中。

  5. 常量表检测
    MySQL觉得下边这种查询运行的特别快:
    使用主键等值匹配或者唯一二级索引列等值匹配作为搜索条件来查询某个表。
    MySQL觉得这两种查询花费的时间特别少,少到可以忽略,所以也把通过这两种方式查询的表称之为常量表(英文名:constant tables)。优化器在分析一个查询语句时,先首先执行常量表查询,然后把查询中涉及到该表的条件全部替换成常数,最后再分析其余表的查询成本,比方说这个查询语句:

     SELECT
     	*
     FROM
     	table1
     INNER JOIN table2 ON table1.column1 = table2.column2
     WHERE
     	table1.primary_key = 1;
    

    很明显,这个查询可以使用主键和常量值的等值匹配来查询table1表,也就是在这个查询中table1表相当于常量表,在分析对table2表的查询成本之前,就会执行对table1表的查询,并把查询中涉及table1表的条件都替换掉,也就是上边的语句会被转换成这样:

     SELECT
     	table1表记录的各个字段的常量值,
     	table2.*
     FROM
     	table1
     INNER JOIN table2 ON table1表column1列的常量值 = table2.column2;
    

外连接消除

我们前边说过,内连接的驱动表和被驱动表的位置可以相互转换,而左(外)连接和右(外)连接的驱动表和被驱动表是固定的。这就导致内连接可能通过优化表的连接顺序来降低整体的查询成本,而外连接却无法优化表的连接顺序。

我们之前说过,外连接和内连接的本质区别就是:对于外连接的驱动表的记录来说,如果无法在被驱动表中找到匹配ON子句中的过滤条件的记录,那么该记录仍然会被加入到结果集中,对应的被驱动表记录的各个字段使用NULL值填充;而内连接的驱动表的记录如果无法在被驱动表中找到匹配ON子句中的过滤条件的记录,那么该记录会被舍弃。查询效果就是这样:

SELECT * FROM e1 INNER JOIN e2 ON e1.m1 = e2.m2;

image.png
SELECT * FROM e1 LEFT JOIN e2 ON e1.m1 = e2.m2;
image.png
对于上边例子中的(左)外连接来说,由于驱动表e1中m1=1, n1='a’的记录无法在被驱动表e2中找到符合ON子句条件e1.m1 = e2.m2的记录,所以就直接把这条记录加入到结果集,对应的e2表的m2和n2列的值都设置为NULL。

因为凡是不符合WHERE子句中条件的记录都不会参与连接。只要我们在搜索条件中指定关于被驱动表相关列的值不为NULL,那么外连接中在被驱动表中找不到符合ON子句条件的驱动表记录也就被排除出最后的结果集了,也就是说:在这种情况下:外连接和内连接也就没有什么区别了!

另外再说下这个查询:

SELECT* FROM e1 LEFT JOIN e2 ON e1.m1 = e2.m2 WHERE e2.n2 IS NOT NULL

image.png

由于指定了被驱动表e2的n2列不允许为NULL,所以上边的e1和e2表的左(外)连接查询和内连接查询是一样的。当然,我们也可以不用显式的指定被驱动表的某个列IS NOT NULL,只要隐含的有这个意思就行了,比方说这样:

SELECT * FROM e1 LEFT JOIN e2 ON e1.m1 = e2.m2 WHERE e2.m2 = 2

image.png

在这个例子中,我们在WHERE子句中指定了被驱动表e2的m2列等于2,也就相当于间接的指定了m2列不为NULL值,所以上边的这个左(外)连接查询其实和下边这个内连接查询是等价的:

SELECT* FROM e1 INNER JOIN e2 ON e1.m1 = e2.m2 WHERE e2.m2 = 2

我们把这种在外连接查询中,指定的WHERE子句中包含被驱动表中的列不为NULL值的条件称之为空值拒绝(英文名:reject-NULL)。在被驱动表的WHERE子句符合空值拒绝的条件后,外连接和内连接可以相互转换。这种转换带来的好处就是查询优化器可以通过评估表的不同连接顺序的成本,选出成本最低的那种连接顺序来执行查询。

mysql执行原理

InnoDB的特性

  1. 双写缓冲区/双写机制
  2. Buffer Pool
  3. 自适应Hash索引

InnoDB的内存结构和磁盘存储结构图

image.png

InnoDB内存与磁盘交互方式

将数据划分为若干个页,以页作为磁盘和内存之间交互的基本单位,InnoDB中页的大小一般为 16 KB。也就是在一般情况下,一次最少从磁盘中读取16KB的内容到内存中,一次最少把内存中的16KB内容刷新到磁盘中。

记录行是如何定义的

image.png

  • NULL值列表是用二进制01来表示的
  • 记录头信息:
二进制位数解释
预留位11没有使用
预留位21没有使用
delete_mask1标记该记录是否被删除
min_rec_mask1B+树的每层非叶子节点中的最小记录都会添加该标记
n_owned4表示当前记录拥有的记录数
heap_no13表示当前记录在页的位置信息
record_type3表示当前记录的类型,
0表示普通记录,
1表示B+树非叶子节点记录,
2表示最小记录,
3表示最大记录
next_record16表示下一条记录的相对位置

隐藏列信息

MySQL会为每个记录默认的添加一些列(也称为隐藏列),包括:
DB_ROW_ID(row_id):非必须,6字节,表示行ID,唯一标识一条记录
InnoDB表对主键的生成策略是:优先使用用户自定义主键作为主键,如果用户没有定义主键,则选取一个Unique键作为主键,如果表中连Unique键都没有定义的话,则InnoDB会为表默认添加一个名为row_id的隐藏列作为主键。
DB_TRX_ID:必须,6字节,表示事务ID
DB_ROLL_PTR:必须,7字节,表示回滚指

索引页格式

它是InnoDB管理存储空间的基本单位,一个页的大小一般是16KB。

InnoDB表空间

表空间是一个抽象的概念,对于系统表空间来说,对应着文件系统中一个或多个实际文件;对于每个独立表空间来说,对应着文件系统中一个名为表名.ibd的实际文件。
image.png

  • 区(extent)

    表空间中的页可以达到2³²个页,实在是太多了,为了更好的管理这些页面,InnoDB中还有一个区(英文名:extent)的概念。对于16KB的页来说,连续的64个页就是一个区,也就是说一个区默认占用1MB空间大小。
    不论是系统表空间还是独立表空间,都可以看成是由若干个区组成的,每256个区又被划分成一个组。

引入区的主要目的是什么?

我们每向表中插入一条记录,本质上就是向该表的聚簇索引以及所有二级索引代表的B+树的节点中插入数据。而B+树的每一层中的页都会形成一个双向链表,如果是以页为单位来分配存储空间的话,双向链表相邻的两个页之间的物理位置可能离得非常远。

我们介绍B+树索引的适用场景的时候特别提到范围查询只需要定位到最左边的记录和最右边的记录,然后沿着双向链表一直扫描就可以了,而如果链表中相邻的两个页物理位置离得非常远,就是所谓的随机I/O。磁盘的速度和内存的速度差了好几个数量级,随机I/O是非常慢的,所以我们应该尽量让链表中相邻的页的物理位置也相邻,这样进行范围查询的时候才可以使用所谓的顺序I/O。

一个区就是在物理位置上连续的64个页。在表中数据量大的时候,为某个索引分配空间的时候就不再按照页为单位分配了,而是按照区为单位分配,甚至在表中的数据十分非常特别多的时候,可以一次性分配多个连续的区,从性能角度看,可以消除很多的随机I/O。

段(segment)

我们提到的范围查询,其实是对B+树叶子节点中的记录进行顺序扫描,而如果不区分叶子节点和非叶子节点,统统把节点代表的页面放到申请到的区中的话,进行范围扫描的效果就大打折扣了。所以InnoDB对B+树的叶子节点和非叶子节点进行了区别对待,也就是说叶子节点有自己独有的区,非叶子节点也有自己独有的区。

存放叶子节点的区的集合就算是一个段(segment),存放非叶子节点的区的集合也算是一个段。也就是说一个索引会生成2个段,一个叶子节点段,一个非叶子节点段。

段其实不对应表空间中某一个连续的物理区域,而是一个逻辑上的概念。

双写缓冲区/双写机制

它是一种特殊文件flush技术,带给InnoDB存储引擎的是数据页的可靠性。它的作用是,在把页写到数据文件之前,InnoDB先把它们写到一个叫doublewrite buffer(双写缓冲区)的连续区域内,在写doublewrite buffer完成后,InnoDB才会把页写到数据文件的适当的位置。如果在写页的过程中发生意外崩溃,InnoDB在稍后的恢复过程中在doublewrite buffer中找到完好的page副本用于恢复。

doublewrite buffer是InnoDB在系统表空间上的128个页(2个区,extend1和extend2),大小是2MB

所以在正常的情况下, MySQL写数据页时,会写两遍到磁盘上,第一遍是写到doublewrite buffer,第二遍是写到真正的数据文件中。如果发生了极端情况(断电),InnoDB再次启动后,发现了一个页数据已经损坏,那么此时就可以从doublewrite buffer中进行数据恢复了。

因为mysql是以页进行单位交互的,也就是16KB,但是实际操作系统是按照4k来进行内存与磁盘交互的,所以,引入了双写缓存区的概念。
另外,因为这个区的存储是连续的,所以效率要高于真实插入时的随机io。

已经有了redo log为什么还要引入双写缓冲区

因为redo log默认认为所有页是完整的,而不会校验页的完整性,但是双写缓冲区其实是为了保证页的完整性。

缓存的重要性(buffer pool)

我们知道,对于使用InnoDB作为存储引擎的表来说,不管是用于存储用户数据的索引(包括聚簇索引和二级索引),还是各种系统数据,都是以页的形式存放在表空间中的,而所谓的表空间只不过是InnoDB对文件系统上一个或几个实际文件的抽象,也就是说我们的数据说到底还是存储在磁盘上的。

但是磁盘的速度慢,所以InnoDB存储引擎在处理客户端的请求时,当需要访问某个页的数据时,就会把完整的页的数据全部加载到内存中,也就是说即使我们只需要访问一个页的一条记录,那也需要先把整个页的数据加载到内存中。将整个页加载到内存中后就可以进行读写访问了,在进行完读写访问之后并不着急把该页对应的内存空间释放掉,而是将其缓存起来,这样将来有请求再次访问该页面时,就可以省去磁盘IO的开销了。

buffer pool

InnoDB为了缓存磁盘中的页,在MySQL服务器启动的时候就向操作系统申请了一片连续的内存,他们给这片内存起了个名,叫做Buffer Pool(中文名是缓冲池)

Buffer Pool内部组成

Buffer Pool中默认的缓存页大小和在磁盘上默认的页大小是一样的,都是16KB。为了更好的管理这些在Buffer Pool中的缓存页,InnoDB为每一个缓存页都创建了一些所谓的控制信息,这些控制信息包括该页所属的表空间编号、页号、缓存页在Buffer Pool中的地址、链表节点信息、一些锁信息以及LSN信息,当然还有一些别的控制信息。
每个缓存页对应的控制信息占用的内存大小是相同的,我们称为控制块。控制块和缓存页是一一对应的,它们都被存放到 Buffer Pool 中,其中控制块被存放到 Buffer Pool 的前边,缓存页被存放到 Buffer Pool 后边,所以整个Buffer Pool对应的内存空间看起来就是这样的:

image.png
每个控制块大约占用缓存页大小的5%,而我们设置的innodb_buffer_pool_size并不包含这部分控制块占用的内存空间大小,也就是说InnoDB在为Buffer Pool向操作系统申请连续的内存空间时,这片连续的内存空间一般会比innodb_buffer_pool_size的值大5%左右。

free链表的管理

最初启动MySQL服务器的时候,需要完成对Buffer Pool的初始化过程,就是先向操作系统申请Buffer Pool的内存空间,然后把它划分成若干对控制块和缓存页。但是此时并没有真实的磁盘页被缓存到Buffer Pool中(因为还没有用到),之后随着程序的运行,会不断的有磁盘上的页被缓存到Buffer Pool中。

那么问题来了,从磁盘上读取一个页到Buffer Pool中的时候该放到哪个缓存页的位置呢?或者说怎么区分Buffer Pool中哪些缓存页是空闲的,哪些已经被使用了呢?最好在某个地方记录一下Buffer Pool中哪些缓存页是可用的,这个时候缓存页对应的控制块就派上大用场了,我们可以把所有空闲的缓存页对应的控制块作为一个节点放到一个链表中,这个链表也可以被称作free链表(或者说空闲链表)。刚刚完成初始化的Buffer Pool中所有的缓存页都是空闲的,所以每一个缓存页对应的控制块都会被加入到free链表中,假设该Buffer Pool中可容纳的缓存页数量为n,那增加了free链表的效果图就是这样的:

image.png
有了这个free链表之后,每当需要从磁盘中加载一个页到Buffer Pool中时,就从free链表中取一个空闲的缓存页,并且把该缓存页对应的控制块的信息填上(就是该页所在的表空间、页号之类的信息),然后把该缓存页对应的free链表节点从链表中移除,表示该缓存页已经被使用了。

如何知道该页数据是否在缓存中呢?

我们其实是根据表空间号 + 页号来定位一个页的,也就相当于表空间号 + 页号是一个key,缓存页就是对应的value,怎么通过一个key来快速找着一个value呢?

所以我们可以用表空间号 + 页号作为key,缓存页作为value创建一个哈希表,在需要访问某个页的数据时,先从哈希表中根据表空间号 + 页号看看有没有对应的缓存页,如果有,直接使用该缓存页就好,如果没有,那就从free链表中选一个空闲的缓存页,然后把磁盘中对应的页加载到该缓存页的位置。

flush链表的管理

如果我们修改了Buffer Pool中某个缓存页的数据,那它就和磁盘上的页不一致了,这样的缓存页也被称为脏页(英文名:dirty page)。当然,最简单的做法就是每发生一次修改就立即同步到磁盘上对应的页上,但是频繁的往磁盘中写数据会严重的影响程序的性能。所以每次修改缓存页后,我们并不着急立即把修改同步到磁盘上,而是在未来的某个时间点进行同步。

但是如果不立即同步到磁盘的话,那之后再同步的时候我们怎么知道Buffer Pool中哪些页是脏页,哪些页从来没被修改过呢?总不能把所有的缓存页都同步到磁盘上吧,假如Buffer Pool被设置的很大,比方说300G,那一次性同步会非常慢。

所以,需要再创建一个存储脏页的链表,凡是修改过的缓存页对应的控制块都会作为一个节点加入到一个链表中,因为这个链表节点对应的缓存页都是需要被刷新到磁盘上的,所以也叫flush链表。链表的构造和free链表差不多。

LRU链表的管理

Buffer Pool对应的内存大小毕竟是有限的,如果需要缓存的页占用的内存大小超过了Buffer Pool大小,那么就需要把某些旧的缓存页从Buffer Pool中移除,然后再把新的页放进来,那么这个淘汰机制就试采用的lru算法,这个链表与之前的free与flush链表不是同一个链表,而是单独一个链表,用来做淘汰使用

如何避免全表扫描时,将热点数据淘汰

如果非常多的使用频率偏低的页被同时加载到Buffer Pool时,可能会把那些使用频率非常高的页从Buffer Pool中淘汰掉。
因为有这两种情况的存在,所以InnoDB把这个LRU链表按照一定比例分成两截,分别是:
一部分存储使用频率非常高的缓存页,所以这一部分链表也叫做热数据,或者称young区域。
另一部分存储使用频率不是很高的缓存页,所以这一部分链表也叫做冷数据,或者称old区域。

默认情况下,old区域在LRU链表中所占的比例是37%,也就是说old区域大约占LRU链表的3/8。

  • 针对全表扫描时,短时间内访问大量使用频率非常低的页面情况的优化:
    全表扫描有一个特点,那就是它的执行频率非常低,出现了全表扫描的语句也是我们应该尽快优化的对象。而且在执行全表扫描的过程中,即使某个页面中有很多条记录,也就是去多次访问这个页面所花费的时间也是非常少的。
    所以在对某个处在old区域的缓存页进行第一次访问时就在它对应的控制块中记录下来这个访问时间,如果后续的访问时间与第一次访问的时间在某个时间间隔内,那么该页面就不会被从old区域移动到young区域的头部,否则将它移动到young区域的头部。默认间隔时间是1s

Buffer Pool中的数据如何防止丢失

使用redo log,将已提交的数据,存储到redo log以物理结构的形式存储在redo log中

http

http与https的区别

https为什么安全


多线程

多线程基础

进程、线程的区别

进程:资源分配的基本单位
线程:程序调度执行的最小单位
一个线程中可以有多个进程,多个线程共享进程中的资源。
(一个程序里不同的执行路劲就叫线程)

启动一个线程的方式

继承Thread类
实现Runnable接口
通过Callable和Future创建线程

sleep

当前线程进入阻塞状态

yield

让出当前线程资源,重新回到等待队列中去

join

将其他线程加入到当前线程,使得其他线程运行结束后,才能继续执行当前线程

线程的状态

  1. new: 初始状态

  2. runnable 运行状态
    ready: 就绪状态,等待被cpu调度
    running状态,运行状态

  3. blocked状态,阻塞状态 等待获取sychronized锁

  4. waiting状态,等待状态 调用了sleep()/join()/park()方法,没有固定时间的等待

  5. time_waiting 给定时间的等待状态,例如调用了sleep()方法

  6. terminated状态,死亡状态

在这里插入图片描述
image.png
在这里插入图片描述

synchronized

synchronized是如何实现的

在对象的markword中最后两位记录了锁状态,两位的不同组合来表示不同的锁的状态。
 Synchronized进过编译,会在同步块的前后分别形成monitorentermonitorexit这个两个字节码指令

synchronizedthis、synchronized方法、静态的synchronized方法

synchronized this与synchronized方法这两是一样的,都是使用当前对象作为锁
静态的synchronized方法,使用的是当前的class类作为锁对象。

可重入锁

一个synchronized方法调用另外一个synchronized方法

synchronized方法块中的程序报错会怎么样

synchronized方法快程序报错,会使得锁释放,这时候会造成程序的乱入,会使得数据出现问题。所以,需要处理好synchronized中的异常处理。

synchronized 锁升级

在JDK1.6之前,主要使用synchronized,那么jvm会直接向操作系统申请锁。由于申请系统锁需要用户态切换到内核态,而大部分情况下,是不需要直接切换到内核态的,所以在JDK1.6以后,为了提高效率,提出了锁升级的概念。

  1. 偏向锁:当只有一个对象访问时,只是在对象的markword上,记录当前的线程id。
  2. 轻量级锁:如果有线程争用,那么就采用cas的方式进行自旋
  3. 重量级锁:当满足一定条件时,就会升级为重量级锁,这时候,才会想向操作系统申请锁。
    老版本:当自旋次数超过10或者自旋线程的数超过了cpu核数的二分之一
    新版本:jvm自适应,由jvm自己决定什么时候升级为重量级锁
标志位状态
01未锁定/偏向锁
00轻量级锁
10重量级锁

什么情况下,适合使用重量级锁,什么情况适合轻量级锁

如果是执行时间长,或者等待线程比较多,那么适合使用重量级锁
否则,适合使用自旋锁。

String Integer Long为什么不能作为锁对象

包装类型内部使用了享元模式,所以如果用包装类型,会出问题
String不能使用的原因是使用String会使得字符串常量池有很多对象,极端情况下会发生oom

JUC

JUC下常见的类

  1. atomic
    1.1 Atomic开头的类,提供了一系列的原子操作,比如AtomicInteger的incrementAndGet()方法。
    1.2 LongAdder 内部使用分段锁机制,多线程情况下,效率高于Atomic(内部维护一个数组,数组中的每一个元素进行上锁递增,最后求总和,所以效率会更高。这个也是在juc的atomic包下)。
  2. lock
    2.1 ReentrantLock
            boolean tryLock() 尝试锁定
            boolean tryLock(long timeout, TimeUnit unit) 指定时间内尝试锁定
            lockInterruptibly() 尝试锁定过程中,可以被打断,并抛出异常
            void unlock() 解锁
            公平锁:new ReentrantLock(true);默认是非公平的,如果是公平锁,那么先检查等待队列中是否有其他线程,如果有,进行排队,如果是非公平锁,上来直接尝试上锁,如果上锁不成功,才会进入等待队列中去。synchronized只有非公平锁。
            可以创建多个condition,每个condition就是一个等待队列。
    2.2 ReadWriteLock: 读写锁(共享锁/排他锁 共享读锁,排他写锁)
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

/**
 * 读写锁
 * 排他锁/共享锁
 */
public class TestReadWriteLock {
    private static Lock lock = new ReentrantLock();
    private static ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
    private static Lock readLock = readWriteLock.readLock();
    private static Lock writeLock = readWriteLock.writeLock();

    private static void read(Lock lock) {
        lock.lock();
        sleep(1000);
        System.out.println("read over");
        lock.unlock();
    }

    private static void write(Lock lock) {
        lock.lock();
        sleep(1000);
        System.out.println("write over");
        lock.unlock();
    }

    public static void main(String[] args) {
//        Runnable read = () -> read(lock);
//        Runnable write = () -> write(lock);
        Runnable read = () -> read(readLock);
        Runnable write = () -> write(writeLock);

        for (int i=0; i<18; i++) {
            new Thread(read).start();
        }

        for (int i=0; i<2; i++) {
            new Thread(write).start();
        }
    }

    private static void sleep(int millis) {
        try {
            Thread.sleep(millis);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

    2.3 LockSupport: 锁支持,可以用来让一个线程在任意位置进入阻塞状态
        底层是调用了Unsafe的park()与unpark()方法实现的。


import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.LockSupport;

/**
 * 锁支持
 * 用于让一个线程在任意位置进入阻塞状态
 */
public class TestLockSupport {
    public static void main(String[] args) {
        Thread t = new Thread(() -> {
            for (int i=0; i<10; i++) {
                System.out.println(i);
                if (i == 5) {
                    LockSupport.park();
                }


                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

        t.start();
        // 如果先unpark,然后才上锁,那么这把锁还是解锁状态
        // unpark可以先与park调用。
//        LockSupport.unpark(t);
        try {
            TimeUnit.SECONDS.sleep(8);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("after 8 seconds!");

        LockSupport.unpark(t);
    }
}
  1. other
    3.1 CountDownLatch: 门栓,计数等待,到达次数以后自动释放锁。
            new CountDownLatch(length);
            countDown(); 计数减一
            await(); 阻塞方法
    3.2 CyclicBarrier: 栅栏,只要够了数量以后执行任务并释放锁,可以循环使用
            CyclicBarrier cyclicBarrier = new CyclicBarrier(20, () -> System.out.println(“满人发车”));
            cyclicBarrier.await();
    3.3 Phase: 阶段,栅栏的升级版,每一个阶段等到所有人都到齐后,进入下一个阶段。

import java.util.concurrent.Phaser;

/**
 * 阶段
 */
public class TestPhase {

    private static MarriagePhase phase = new MarriagePhase();

    public static void main(String[] args) {
        // 指定初始人数,也可以在new时指定
        phase.bulkRegister(7);

        for (int i=0; i<5; i++) {
            new Person("p"+i).start();
        }

        new Person("新郎").start();
        new Person("新娘").start();
    }

    static class Person extends Thread {
        private String name;

        public Person(String name) {
            this.name = name;
        }

        private void arrive() {
            System.out.println(name+"到达");
            phase.arriveAndAwaitAdvance();
        }

        private void eat() {
            System.out.println(name+"吃饭");
            phase.arriveAndAwaitAdvance();
        }

        private void leave() {
            System.out.println(name+"离开");
            phase.arriveAndAwaitAdvance();
        }

        private void hug() {
            if (name.equals("新郎") || name.equals("新娘")) {
                System.out.println(name+"拥抱");
                phase.arriveAndAwaitAdvance();
            } else {
                phase.arriveAndDeregister();
            }
        }

        @Override
        public void run() {
            arrive();

            eat();

            leave();

            hug();
        }
    }

    static class MarriagePhase extends Phaser {

        // 这里的phase代表的是步骤,固定从0开始
        // registeredParties目前的人数
        @Override
        protected boolean onAdvance(int phase, int registeredParties) {
            switch (phase) {
                case 0:
                    System.out.println("所有人到期,婚礼开始 人数"+registeredParties);
                    return false;
                case 1:
                    System.out.println("所有人到期,吃饭~ 人数"+registeredParties);
                    return false;
                case 2:
                    System.out.println("所有人离开! 人数"+registeredParties);
                    return false;
                case 3:
                    System.out.println("新郎新娘洞房! 人数"+registeredParties);
                    return true;
            }
            return true;
        }
    }
}

    3.4 Seamphore 信号量:可以指定最多有几个线程可以拿到这把锁,semaphore.acquire()获取锁,semaphore.release()释放锁。可以指定公平/非公平。

import java.util.concurrent.Semaphore;

/**
 * 信号量
 * 用作限流使用
 */
public class TestSemaphore {
    public static void main(String[] args) {
        Semaphore semaphore = new Semaphore(1);

        new Thread(() -> {
            try {
                semaphore.acquire();
                System.out.println("t1 Running");
                Thread.sleep(1000);
                System.out.println("t1 end");
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                semaphore.release();
            }
        }).start();

        new Thread(() -> {
            try {
                semaphore.acquire();
                System.out.println("t2 Running");
                Thread.sleep(1000);
                System.out.println("t2 end");
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                semaphore.release();
            }
        }).start();
    }
}

    3.5 Exchanger 交换器:用来两个线程之间交换数据。

import java.util.concurrent.Exchanger;

/**
 * 交换器
 * 用来线程之间交换两个数据
 */
public class TestExchanger {

    public static void main(String[] args) {
        Exchanger<String> exchanger = new Exchanger<>();

        new Thread(() -> {
            try {
                String s = exchanger.exchange("t1");
                System.out.println("t1 线程获取到值"+s);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();

        new Thread(() -> {
            try {
                String s = exchanger.exchange("t2");
                System.out.println("t2 线程获取到值"+s);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();
    }
}

synchronized与ReentrantLock的区别

  • 相似点:
    都是用来加锁的
    都是可重入锁
  • 不同点:
    sychronized是jvm提供实现的,ReentrantLock是jdk1.5以后出现的
    ReentrantLock可以等待中断,但是sychronized中断等待会抛出异常
    ReentrantLock支持公平、非公平锁,但是sychronized只支持非公平锁
    sychronized是自动上锁,自动解锁的,ReentrantLock需要手动上锁,手动解锁。
    ReentrantLock可以创建多个condition,每个condition代表的是不同的等待队列,可以单独对一个condition进行阻塞与唤醒,但是synchronized只能唤醒其中任意一个或者全部唤醒
    底层实现上,ReentrantLock是CAS的实现,sychronized是四种锁升级状态

CAS

CAS(compare and swap): 包含内存地址V,旧的值A,新的值B
如果内存地址V的值是等于旧值A,那么就将V更新成B,否则重新读取
因为CAS操作是属于CPU的原语操作,所以比较与交换的过程是原子操作。

ABA问题如何解决

假如内存地址V,旧的值A
在比较V跟A是否相等之前,V的值先由A变成了B,再由B变成了A,虽然看起来V的值还是A,与期望值相同,但是这个其实是已经发生改变后的值了。
那么这种情况如何避免呢?
加版本号,只要值发生改变,就累加版本号,比较时,不仅仅只比较值,也比较版本号。

AQS内部是如何实现的

AQS(AbstractQueuedSynchronizer),主要由volatile修饰的state变量与一个双向链表组成的等待队列。
state的值是通过CAS的方式来完成的,其具体含义由子类自己来定义实现。
例如:
ReentrantLock: state 的值表示是否有锁以及重入次数,state=0代表无锁,state>0表示有锁并且当前重入次数。
CountDownLatch:state的值代表还需要countDown几次进行就可以进行解锁了。
双向链表的node节点记录了当前的线程以及等待状态,本质上,是等待队列去争抢修改state的值。
对于非公平锁来说,来了一个线程,直接去尝试获得这把锁,如果这把锁获取不成功,那么就用cas的方式将自己插入到队列的尾部。
插入完成以后,循环去判断前一个节点是否是头结点,如果是,那么尝试获取锁,如果获取成功了,就将自己设置为头结点。
这样做的好处是不需要对整个链表进行加锁,效率高。
而对于公平锁,来了一个线程,是直接排队到tail后面。
在这里插入图片描述

ThreadLocal

ThreadLocal内部是如何实现的

ThreadLocal是将变量设置到当前线程的threadLocals中的,这是一个ThreadLocalMap对象,key存放的是当前ThreadLocal自身,value存放的是设置的值。

ThreadLocal中的Entry为什么要继承弱引用

为了节省系统资源,所以会将thread放入线程池当中,当有任务时,从线程池中拿到这个线程并运行,所以,有可能这个线程是一直使用的。而当运行当前任务时,在线程中设置了变量,如果当前任务运行完毕以后,由于这个线程没有被销毁,所以,这个值还一直存在,那么就会造成内存泄露。
在ThreadLocal中的Entry对象,继承了WeakReference,并且在设置值时,将当前的key(ThreadLocal对象)设置为弱引用,所以当线程运行完以后,当前对象自动被回收。但是value并不是弱引用,也就意味着,内存泄露依旧存在,所以,在使用完ThreadLocal以后,要将当前设置的值remove掉。
为什么放入ThreadLocalMap的弱引用不会被回收掉?
因为有个弱引用的对象是ThreadLocal对象,除了被ThreadLocalMap弱引用外,还有强引用指向这个对象,所以不会被gc回收,如果等到这个任务运行完毕,资源被回收,那么这个对象只有弱引用所指向了,所以可以被回收掉。

static class Entry extends WeakReference<ThreadLocal<?>> {
 	/** The value associated with this ThreadLocal. */
    Object value;

    Entry(ThreadLocal<?> k, Object v) {
        super(k);
        value = v;
    }
}

在这里插入图片描述

线程池

线程的运行方式

  1. 继承Thread类
  2. 实现Runnable接口,然后作为Thread的入参
  3. Callable Future方式

Callable

与Runnable类似,并且可以有返回值

Future

用于存储任务运行的结果,get()方法阻塞获取结果。

public static void main(String[] args) throws ExecutionException, InterruptedException {
        Callable<String> c = new Callable() {
            @Override
            public String call() throws Exception {
                return "Hello Callable";
            }
        };

        ExecutorService service = Executors.newCachedThreadPool();
        Future<String> future = service.submit(c); //异步

        System.out.println(future.get());//阻塞

        service.shutdown();
    }

FutureTask

相当于是Callable+Future,并且实现了Runnable接口可以放到Thread中执行。

public static void main(String[] args) throws InterruptedException, ExecutionException {
		
		FutureTask<Integer> task = new FutureTask<>(()->{
			TimeUnit.MILLISECONDS.sleep(500);
			return 1000;
		}); //new Callable () { Integer call();}
		
		new Thread(task).start();
		
		System.out.println(task.get()); //阻塞


	}

CompletableFuture

可以对多个Future进行管理,并且可以对返回结果进行统一处理。

import java.util.Random;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;

public class Test_CompletableFuture {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        CompletableFuture<Double> futureTM = CompletableFuture.supplyAsync(()->priceOfTM());
        CompletableFuture<Double> futureTB = CompletableFuture.supplyAsync(()->priceOfTB());
        CompletableFuture<Double> futureJD = CompletableFuture.supplyAsync(()->priceOfJD());

        CompletableFuture.allOf(futureTM, futureTB, futureJD)
                .thenApply(String::valueOf)
                .thenApply(str-> "price " + str)
                .join();
    }

    private static double priceOfTM() {
        delay();
        return 1.00;
    }

    private static double priceOfTB() {
        delay();
        return 2.00;
    }

    private static double priceOfJD() {
        delay();
        return 3.00;
    }

    private static void delay() {
        int time = new Random().nextInt(500);
        try {
            TimeUnit.MILLISECONDS.sleep(time);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.printf("After %s sleep!\n", time);
    }
}

输出:

After 81 sleep!
After 261 sleep!
After 285 sleep!

Executors提供的默认线程池

  1. newSingleThreadExecutor(): 只有一个核心线程
    corePoolSize : 1
    maximumPoolSize : 1
    keepAliveTime : 0
    TimeUnit : TimeUnit.MILLISECONDS
    BlockingQueue : LinkedBlockingQueue
    ThreadFactory : defaultThreadFactory
    RejectedExecutionHandler : defaultHandler
  2. newCachedThreadPool(): 全部是缓存的线程
    corePoolSize : 0
    maximumPoolSize : Integer.MAX_VALUE
    keepAliveTime : 60
    TimeUnit : TimeUnit.SECONDS
    BlockingQueue : SynchronousQueue
    ThreadFactory : defaultThreadFactory
    RejectedExecutionHandler : defaultHandler
  3. Executors.newFixedThreadPool(int nThreads):固定数量的线程池
    corePoolSize : nThreads
    maximumPoolSize : nThreads
    keepAliveTime : 0
    TimeUnit : TimeUnit.MILLISECONDS
    BlockingQueue : LinkedBlockingQueue
    ThreadFactory : defaultThreadFactory
    RejectedExecutionHandler : defaultHandler

ThreadPoolExecutor

核心组成是任务队列与多个线程,线程去执行任务队列中的任务。
在这里插入图片描述

ThreadPoolExecutor状态

image.png

ThreadPoolExecutor的7个参数

  1. 核心线程数
  2. 最大线程数
  3. 非核心线程数空闲时间
  4. 空闲时间单位
  5. 阻塞队列
  6. 线程工厂
  7. 拒绝策略

ThreadPoolExecutor的执行顺序

如果没有达到核心线程数,那么就去创建一个核心线程,这里不管核心线程有没有空闲。
如果核心线程数满了,那么加入任务队列。
如果任务队列满了,那么就创建临时线程。
临时线程满了,那么执行拒绝策略。
核心线程不会被回收,非核心线程会被回收。

public void execute(Runnable command) {
    // 非空!!
    if (command == null)
        throw new NullPointerException();
    // 拿到ctl
    int c = ctl.get();
    // 通过ctl获取当前工作线程个数
    if (workerCountOf(c) < corePoolSize) {
        // true:代表是核心线程,false:代表是非核心线程
        if (addWorker(command, true))
            // 如果添加核心线程成功,return结束掉
            return;
        // 如果添加失败,重新获取ctl
        c = ctl.get();
    }
    // 核心线程数已经到了最大值、添加时,线程池状态变为SHUTDOWN/STOP
    // 判断线程池是否是运行状态 && 添加任务到工作队列
    if (isRunning(c) && workQueue.offer(command)) {
        // 再次获取ctl的值
        int recheck = ctl.get();
        // 再次判断线程池状态。  DCL
        // 如果状态不是RUNNING,把任务从工作队列移除。
        if (! isRunning(recheck) && remove(command))
            // 走一波拒绝策略。
            reject(command);
        // 线程池状态是RUNNING。
        // 判断工作线程数是否是0个,防止放入队列后,没有线程去处理任务。
        // 可以将核心线程设置为0,所有工作线程都是非核心线程。
        // 核心线程也可以通过keepAlived超时被销毁,所以如果恰巧核心线程被销毁,也会出现当前效果
        else if (workerCountOf(recheck) == 0)
            // 添加空任务的非核心线程去处理工作队列中的任务
            addWorker(null, false);
    }
    // 可能工作队列中的任务存满了,没添加进去,到这就要添加非核心线程去处理任务
    else if (!addWorker(command, false))
        // 执行拒绝策略!
        reject(command);
}

jdk提供的4种拒绝策略

  1. Abort:抛出异常
  2. Discard:扔掉,不抛异常
  3. DiscardOldest:扔掉排队时间最久的
  4. CallerRuns:调用者处理任务

为什么要不建议使用Executors提供的线程池

因为Executors提供了很多默认的创建线程池的方式,而作为开发人员来说,最好了解线程池的原理以及对每个配置

submit()与execute()的区别

  • 相同点
    都是去执行线程任务的方法
  • 不同点
    submit可以获取到任务返回结果或异常信息
    execute不可以获取任务返回结果或异常信息
    submit内部也调用了execute方法

阿里手册如何创建线程池

  1. 创建线程或线程池,需要指定有意义的线程名称,方便出错时回溯。
  2. 线程资源必须通过线程池提供,不允许在应用中自行显示创建线程。
    防止无限制的创建线程而导致资源消耗完毕。
    防止线程频繁的创建销毁造成的资源浪费。
  3. 线程池不允许使用Executors去创建,而是通过ThreadPoolExecutor的方式,这样可以明确线程池的运行规则,避免资源耗尽的风险。
    FixedThreadPool与SingleThreadExecutor指定的阻塞队列是LinkedBlockingQueue,这个队列的最大长度是Integer.MAX_VALUE,可能会堆积大量请求,从而导致OOM。
    CachedThreadPool指定的最大线程数是Integer.MAX_VALUE,可能创建大量的线程,从而导致OOM

如何查看堆栈信息

jstack

线程数如何计算

公式1:cpu核心数 * cpu利用率 * (1+w/c)《java并发编程实践》
        cpu利用率:一般按照100%算即可
        w:等待时间(IO的时间或者程序阻塞的时间)
        c:计算时间(CPU参与计算的时间)
        等待越久,线程数越多。
公式2:cpu核心数 * (1-阻塞系数)《java虚拟机并发编程》
统一公式:cpu核心数 * cpu利用率 * (1+w/c)=cpu核心数 * (1-阻塞系数)
阻塞系数=w/(w+c)
实际以压测为准(线程数、qps、机器配置 以压测为准)。
IO密集型w/c预估为1,计算密集型的w/c预估为0

加入提供一个闹钟服务,订阅这个服务的人特别多,10亿人,怎么优化

  1. 将这些任务分发到多个服务器中
  2. 使用定时任务框架,用线程池+任务队列
  3. 长连接或者netty进行通知

ForkJoinPool

每个线程维护一个队列,当前线程来消费队列中的任务。
当自己队列中的任务都执行完成以后,会去别的队列获取任务进行执行。
在这里插入图片描述
另外还有一个功能是,将一个任务拆分成多个子任务,然后对任务的结果进行汇总。
在这里插入图片描述
定义任务:
ForkJoinTask< T >
常用的有两个实现
RecursiveAction 无返回值的任务
RecursiveTask< T > 带返回值的任务

Excutors提供的ForkJoinPool

Executors.newWorkStealingPool():任务窃取的线程池

java线程模型


Redis

redis项目中的使用场景

1、计数器:
在单位时间内,有且仅有N数量的请求能够访问我的接口。
比如我们需要在10秒内限定20个请求,那么我们在setnx的时候可以设置过期时间10,当请求的setnx数量达到20时候即达到了限流效果。
缺点:限流不均匀,粒度较粗,比如当统计1-10秒的时候,无法统计2-11秒之内,再有就是Redis中需要保持N个key等问题。

2、滑动窗口:
将请求包装成一个zset数组,当请求进来时,value中保存唯一标识,score中保存时间戳,用来计算当前时间戳内有多少个请求的数量。
zset的range方法可以轻松获取到两个时间戳之间有多少个请求,即每m秒内有n个请求。
缺点:zset的结构会越来越大。
解决方案:定时任务或mq消息异步删除时间戳较早的value数据。

3、令牌桶:
请求到来时先从redis中获取一个令牌,如果能拿到就放行,否则拦截返回。
可以使用list的结构,在Java代码中采用定时任务定时地往list中rightPush令牌,请求通过leftPot来获取令牌。
令牌需要保证唯一性,例如使用uuid生成。
Google开源项目Guava中的RateLimiter使用的就是令牌桶控制算法。

4、漏桶算法:
可以看作是令牌桶算法的一种特殊情况,可以使用list来实现,定时往list中rightPush前检查一下list中元素的数量,如果有一个就不执行rightPush操作了,没有再执行。

5、redis-cell:
是Redis4.0提供的一个限流Redis模块。该模块使用了漏桶算法,并提供了原子的限流指令。

Redis的多线程:

1、6.0版本之前Redis是单线程的,这指的是事件处理器在处理任务的时候是单线程的。
2、但在6.0开始,事件处理器变成了多线程模式,它是将work线程和io线程做了区分,work线程专注于计算工作,io线程则专注于io的读写(work线程可以有多个)。

Redis常用的数据结构

  • String
    1. 字符串类型
    2. 数值类型
    3. bitmap
  • hash
  • list
  • set
  • zset

世界上,有3中数据的表示

  1. k=a, k=1
  2. k=[1, 2, 3], k=[a, b, c]
  3. k={x=y}, k=[{}, {}]

memcache与redis的区别

memcache只有key-value一种数据结构,
redis支持5种数据类型,对每一种类型,都提供了对应的api,可以支持不同的应用场景。
一方面,使用redis可以减少对json的解析操作
另一方面,也可以减少网络的IO,不需要每次都拿全量的数据进行计算。

计算向数据移动

Redis是顺序的吗

一个连接中的多次请求,可以保证顺序处理,但是如果是多个连接,那么就不保证顺序了。

Redis分区

Redis默认分为16个库,0-15,每个库之间,数据相互隔离。
默认进入后,是在0号库。

各种数据类型常用api

  • 所有key通用
命令备注
keys pattern以正则表达式查询key,常用的如key *查询全部
del key删除指定key
exists key判定key是否存在
type key获取 key 的类型
expire key seconds设置key有效期为seconds秒
pexpire key milliseconds设置key有效期为milliseconds毫秒
expireat key timestamp设置key失效 的 秒级时间戳
pexpireat key milliseconds-timestamp设置key失效的 毫秒级时间戳
ttl key获取key的秒级有效时间
pttl key获取key的毫秒级有效时间
object encoding key返回具体的类型,例如string区分字符串与数字类型
  • string
命令备注
set key value设置值
get key获取值
set key value nx只有不存在,才设置(只能新建)
set key value xx只有存在,才设置(只能更新)
mset k1 v1 k2 v2设置多个key的值
mget k1 k2获取多个key的值
append k1 v追加值
getrange key start end截取字符串
getset取旧值,赋新值
setrange key offset value按照偏移量覆盖值
strlen key获取字符串长度
–num–
incr keyvalue自增
incrby key numvalue+num
decr keyvalue自减
decrby key numvalue-num
incrbyfloat key float加一个小数
–bitmap–
setbit key offset value设置二进制位的偏移量
bitpos key bit [start] [end]查找bit第一次出现的位置(位图中的位置) start、end是字节的位置
bitcount key [start] [end]查询二进制位1出现的次数 start、end是字节的位置
bitop op distkey k1 k2…k1 k2… 进行op操作,并把结果赋值到distkey中 op: and or

string num类型应用场景

抢购、秒杀访问人数、点赞、评论数、好友数
规避高并发下,对数据库事务操作,完全由redis内存操作代替。

位图应用场景

  1. 例如需要统计随机窗口用户登录次数(用户id作为key)
  2. 活跃用户统计(日期作为key,用户与数字做运算,日期做与运算,再通过bitcount来获取)

正反向索引

例如表示最后一个字母的偏移量,可以是正着数从0开始,也可以用-1来表示。

二进制安全

对于redis来说,是按照字节流来存储的,而不是按照字符
所以,像9999,实际上存储的是四个9,长度为4
另外,如果像使用了utf-8编码来存储了一个中文,那么长度就是3
这样做的好处是,对于多个平台数据交互,只要编码一致就可以正常读取。
而正因为有object encoding key 可以知道上一次key的类型,那么如果做计算操作,就不需要做类型判断了。

Redis的数据结构

redis 与mysql速度对比

redis可以达到秒级十万的存储速度
而mysql秒级大概在几千。

redis 分布式锁引发的问题

  1. 死锁(上锁后,客户端线程挂了的情况):设置过期时间 set nx ex
  2. 重复锁(上锁后,时间过期了,但是程序还没结束):watch dog 线程去续期
  3. 如何实现谁加的锁,只能谁来删:锁上带uuid属性,可以使用lua脚本来实现
  4. 如何基于以上问题,实现重复锁:使用hset来实现,锁的名称作为hset的key,value的key需要两个,第一个:uuid归属,第二个重入次数。
  5. 性能问题
    1. 轮询方式去抢锁 cas方式
    2. 回调方式 基于redis的发布订阅,订阅redis自身的key删除事件
  6. 单点故障,可靠性问题:集群
    1. 高可用,使用哨兵,主从同步,会有延迟,有不一致的可能(概率低,可能出现重复锁现象)
    2. 分片集群,解决压力问题
    3. 使用红锁(算法),实现是在client端。客户端去多个redis中去抢锁,过半通过即代表抢锁成功。3台容忍1台挂了,5台容忍2台挂了
      可能出现并发强占失败的问题,那么redlock会进行重试,在重试时,对多个redis进行排序,如果有一台抢成功了,就不去抢了。但是可能会出现网络分区问题,那么,又会重试,保证了CAP中的CP(保证一致)。
      另外,可能出现过期时间不一致情况,还需要计算对齐时间。
    4. 使用zookeeper分布式锁
      zookeeper刚启动属于无主模型,会触发ZAB协议(相当于简化版的paxos的实现),会推选出一个leader,后期的写,必须由主节点来完成(leader代替过半抢锁的过程)
      zookeeper的性能来自于单节点leader以及zk集群的2PC提交,损耗性能不如单节点的redis。

1-5问题,可以使用redison来实现。
1-4 redis 抢到锁自身出现的问题
5 redis 没抢到锁出现的问题
一般情况下,要么采用单节点redis的情况,忽略可靠性(性能更好)。
要么采用zookeeper情况,可靠性更好一些(更可靠)。

spring

@Transactional注解失效的原因

在spring中使用注解@Transitional可以添加事物管理,但是很多时候,似乎注解失效即发生了异常,却没有回滚了。这里列举一下失效的几种情况

  1. 数据库引擎不支持,mysql需要InnoDB
  2. 方法必须是public的
  3. @Transitional默认是捕获运行时异常(继承RuntimeException)才回滚,所以如果想要捕获所有异常都回滚,需要在@Transitional后面加上(rollbackFor=Exception.class)
  4. 需要抛出异常,才会回滚,如果你已经自己把异常捕获了,但是没有继续往外抛,那么也是不会回滚的
  5. 事务内重新创建的线程,线程中异常不会进行回滚
    常见的有这几种,但是也有其他的,参考:
    https://www.jb51.net/article/233682.htm

spring-cloud

eureka-service是如何被加载的

首先@EnableEurekaService会去创建一个Marker的标识类
spring.factories中指定的类中,有一个@ConditionalOnBean({Marker.class})的注解,会去检查这个类是否被创建,如果被创建,那么就执行eureka-service的加载逻辑。
在这里插入图片描述

eureka-service如何优化

  1. 如果是服务集群少,那么关闭自我保护
  2. 服务下线通知,实现listener
  3. 服务剔除时间设置3秒,可以快速下线
  4. 缓存优化
    默认是三级缓存,一级与二级缓存是实时的,二级跟三级缓存是30秒同步一次
    可以将三级缓存关闭
    或者减少三级缓存的拉取时间
    这样可以提高服务发现速度
  server:
  	# 自我保护,看服务多少。
    enable-self-preservation: false
    # 自我保护阈值
    renewal-percent-threshold: 0.85
    # 剔除服务时间间隔
    eviction-interval-timer-in-ms: 1000
    # 关闭从readOnly读注册表
    use-read-only-response-cache: false
    # readWrite 和 readOnly 同步时间间隔。
    response-cache-update-interval-ms: 1000

eureka-client

  1. 刷新注册表时间间隔
  2. 心跳间隔
  3. 注册中心的url每个服务打乱。

eureka哪些地方设置的不好

服务下线,使用的Timer来写的,Timer会有一个问题,加入执行多个任务,其中一个任务抛异常后,后面的任务将不会再去运行,建议使用ScheduledExecutorService-> ScheduledThreadPoolExecutor

为什么eureka在cap中只满足了AP

ap(可用性,分区容忍性),不保证c(一致性)
Eureka各个节点都是平等的,几个节点挂掉不会影响正常节点的工作,剩余的节点依然可以提供注册和查询服务。而Eureka的客户端在向某个Eureka注册时,如果发现连接失败,则会自动切换至其他节点,只要有一台Eureka还在,就能保住注册服务的可用性,只不过查到的信息可能不是最新的。

  1. 三级缓存,一级二级是实时的,二级跟三级30秒同步一次,所以不保证一致性,保证可用性
  2. 启动时,从其他(peer)节点拉取注册表, 但是启动到下次再次拉取之间,有新的服务进来是检测不到的。
  3. P:网络不好还是可以拉取到服务进行注册的。

eureka-service的作用

  1. 服务注册
  2. 服务续约
  3. 服务下线
  4. 获取服务注册列表
  5. 服务集群同步

eureka-client配置

  1. 注册
  2. 拉取
  3. 下线

mq

如何处理消息堆积

MQ消息堆积是指生产者发送的消息短时间内在Broker端大量堆积,无法被消费者及时消费,从而导致业务功能无法正常使用。
消息堆积常见于以下几种情况:
(1)新上线的消费者功能有BUG,消息无法被消费。
(2)消费者实例宕机或因网络问题暂时无法同Broker建立连接。
(3)生产者短时间内推送大量消息至Broker,消费者消费能力不足。
(4)生产者未感知Broker消费堆积持续向Broker推送消息。
解决上述问题就要做到:

(1)解决问题一,要做好 灰度发布。每次新功能上线前,选取一定比例的消费实例做灰度,若出现问题,及时回滚;若消费者消费正常,平稳运行一段时间后,再升级其它实例。如果需要按规则选出一部分账号做灰度,则需要做好消息过滤,让正常消费实例排除灰度消息,让灰度消费实例过滤出灰度消息。

(2)解决问题二,要做到 多活。极端情况下,当一个IDC内消费实例全部宕机时,需要做到让其他IDC内的消费实例正常消费消息。同时,若一个IDC内Broker全部宕机,需要支持生产者将消息发送至其它IDC的Broker。

(3)解决问题三,要 增强消费能力。增强消费能力,主要是增加消费者线程数或增加消费者实例个数。增加消费者线程数要注意消费者及其下游服务的消费能力,上线前就要将线程池参数调至最优状态。增加消费者实例个数,要注意Queue数量,消费实例的数量要与Queue数量相同,如果消费实例数量超过Queue数量,多出的消费实例分不到Queue,只增加消费实例是没用的,如果消费实例数量比Queue数量少,每个消费实例承载的流量是不同的。

(4)解决问题四,要做到 熔断与隔离。当一个Broker的队列出现消息积压时,要对其熔断,将其隔离,将新消息发送至其它队列,过一定的时间,再解除其隔离。

开放性问题

如何设计一个CDN服务器

  1. 如何确定用户在哪
    根据用户ip地址来判断用户的位置,例如(北京海淀联通)
  2. 如何做分发
    2.1 用户配置域名解析 域名->CDN域名
    2.2 根据用户的位置,指向离用户最近的CDN服务器
    2.3 这台CDN服务器查找是否有该数据,如果没有,去源站获取
  3. 内容管理
    3.1 是否满足用户设置的存储规则,如果不满足直接重定向到源站
    3.2 如果满足规则, 查询是否有该数据,如果有直接返回数据
    3.3 如果没有该数据,从源站中拉取数据存储,并返回给用户

如何定位cpu飚高

方法1:
1-启动:java -jar 2_cpu-0.0.1-SNAPSHOT.jar 8 > log.file 2>&1 &
2-一般来说,应用服务器通常只部署了java应用,可以top一下先确认,是否是java应用导致的:命令:top
3-如果是,查看java进场ID,命令:jps -l
4-找出该进程内最好非CPU的线程,命令:top -Hp pid 25128
5-将线程ID转化为16进制,命令:printf "%x\n" 线程ID 623c 25148
6-导出java堆栈信息,根据上一步的线程ID查找结果:命令:
jstack 11976 >stack.txt
grep 2ed7 stack.txt -A 20
方法2:
在线工具:https://gceasy.io/ft-index.jsp
1-方法1中导出的对快照文件,上传到该网站即可

如何导出堆栈信息:
https://blog.fastthread.io/2016/06/06/how-to-take-thread-dumps-7-options/

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Java知识体系非常广泛,涵盖了多个方面。以下是Java知识体系的一些主要方面: 1. Java基础:包括Java语言的基本语法、数据类型、运算符、流程控制等基础知识。 2. 面向对象编程:Java是一门面向对象的编程语言,因此掌握面向对象的概念、类与对象、继承、多态、封装等是非常重要的。 3. Java集合框架:Java提供了丰富的集合框架,包括List、Set、Map等,掌握集合框架的使用和常见操作是必备的。 4. 异常处理:Java中的异常处理机制非常重要,了解异常的分类、捕获和处理方式是编写健壮程序的关键。 5. IO流:Java提供了丰富的IO流类,包括字节流和字符流,了解IO流的使用和常见操作可以进行文件读写和网络通信等操作。 6. 多线程:Java支持多线程编程,掌握线程的创建、同步、通信等知识可以实现并发编程。 7. JDBC数据库操作:Java提供了JDBC接口用于与数据库进行交互,了解JDBC的使用可以进行数据库的增删改查操作。 8. Java Web开发:Java是一门广泛应用于Web开发的语言,掌握Java Web开发框架(如Servlet、JSP、Spring、SpringMVC等)和相关技术(如HTML、CSS、JavaScript、数据库等)可以进行Web应用的开发。 9. 设计模式:了解常见的设计模式,如单例模式、工厂模式、观察者模式等,可以提高代码的可维护性和可扩展性。 10. JVM和性能调优:了解Java虚拟机(JVM)的工作原理和调优技巧,可以优化程序的性能和内存管理。 以上是Java知识体系的一些主要方面,当然还有很多其他的知识点和技术。如果你对某个具体方面有更深入的问题,可以告诉我,我会尽力回答。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值