ZWY面试总结

代办

AQS

Concurrenthashmap

投递公司

公司 状态 链接 时间
腾讯
美团(张云峰) 笔试完成 https://zhaopin.meituan.com/web/personalCenter/deliveryRecord
字节 https://jobs.bytedance.com/experienced/position/application
阿里
小红书
腾讯云智 待面试 4.5
腾讯音乐 笔试完成
京东 测评完成 https://campus.jd.com/#/myDeliver?type=present
微众银行 一起触发更多可能 (webank.com)
阅文集团 https://www.nowcoder.com/careers/yuewen/122324
中国银行104573 https://applyjob.chinahr.com/page/job/success?projectId=63f47e7255cbed088c78eed1
中国邮政 https://xiaoyuan.zhaopin.com/scrd/postprocess?cid=58805&pid=101739970&productId=7&channelId=null&taskId=null
蚂蚁 挂到蚂蚁集团 https://talent.antgroup.com/personal/campus-application
特斯拉 https://app.mokahr.com/campus-recruitment/tesla/91939#/candidateHome/applications
58 https://campus.58.com/Portal/Apply/Index
立得空间 https://www.showmebug.com/written_pads/HRUHZN
恒生 https://campus.hundsun.com/personal/deliveryRecord
wind
银泰百货 https://app.mokahr.com/m/candidate/applications/deliver-query/intime
旷世 https://app.mokahr.com/campus-recruitment/megviihr/38642#/candidateHome/applications
腾讯音乐 https://join.tencentmusic.com/deliver
完美世界 https://app.mokahr.com/campus-recruitment/pwrd/45131#/candidateHome/applications
七牛云 https://app.mokahr.com/campus-recruitment/qiniuyun/73989#/candidateHome/applications
cider https://ciderglobal.jobs.feishu.cn/504718/position/application
360(运维开发) https://360campus.zhiye.com/personal/deliveryRecord
知乎(改简历) https://app.mokahr.com/campus_apply/zhihu/68321#/candidateHome/applications
猿辅导 https://hr.yuanfudao.com/campus-recruitment/fenbi/47742/#/candidateHome/applications
爱奇艺 https://careers.iqiyi.com/apply/iqiyi/39117#/jobs
百词斩 https://join.baicizhan.com/campus
昆仑万维 https://app.mokahr.com/campus-recruitment/klww/67963#/candidateHome/applications
bilibli https://jobs.bilibili.com/campus/records
geek https://app.mokahr.com/campus_apply/geekplus/98039#/candidateHome/applications
顺丰 https://campus.sf-express.com/index.html#/personalCenter
众安 https://app.mokahr.com/campus-recruitment/zhongan/71908?sourceToken=d895a22a006b8a6da61313d9b4091850#/candidateHome/applications
虎牙 https://app.mokahr.com/campus_apply/huya/4112?edit=1#/candidateHome/applications
oppo https://careers.oppo.com/campus/record
第四范式 https://app.mokahr.com/campus-recruitment/4paradigm/58145#/candidateHome/applications

代投:商汤 geek 雷火 oppo vivo 字节 阿里 腾讯 联想 西山居

自我介绍

我叫张维阳,是中国地质大学武汉的一名研二学生,以下是我的自我介绍:

在项目方面,为了河南高校魔方爱好者之间更好地交流与分享经验 ,为河南高校魔方联盟搭建了一个基于SpringBoot的魔方经验分享及管理平台,主要功能包括用户注册和登录、魔方教程和经验分享、论坛和社区互动、管理和监控等。 在这其中加入了一些中间件进行优化,最后做了上线部署。得到了魔方爱好者的一致认可。同时为了更好的了解RPC,自己手写了一个RPC框架,主要内容包括注册中心、网络传输、序列化与反序列化、动态代理、负载均衡等 ,从而加深了对RPC框架的理解。

比赛方面,参加过几次数学建模竞赛和数学竞赛,获得过第十二届全国大学生数学竞赛一等奖以及高教社全国大学生数学建模二等奖。学习之余,也喜欢通过博客整理分享自己所学知识,以上就是我的自我介绍。

优缺点

缺点: 缺少相关专业的实践、工作经验;遇事不够沉着冷静,容易紧张;

优点:有团队精神意识,善于沟通。大三参加数学建模时,协调组内其他成员,对编程遇到困难的组员提供帮助 ,最终顺利完成比赛并获得奖项 。

你还有什么要问的?

  • 部门的主营业务是什么?
  • 部门使用的技术栈、编程语言是什么、使用哪些框架、中间件?表达下自己对技术的好奇

项目

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PPOD9POX-1681383922602)(https://raw.githubusercontent.com/viacheung/img/main/image/image-20230413011206981.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KKm7TkAK-1681383922605)(https://raw.githubusercontent.com/viacheung/img/main/image/image-20230404225216774.png)]

魔方天地

介绍一下项目?

该项目是一个致力于魔方爱好者之间交流、分享经验的在线平台,主要功能包括用户注册和登录、魔方教程的
展示分享、魔方论坛互动、比赛和活动组织、用户管理等。

以下是该项目的主要特点和功能:

  1. 从用户系统:支持用户注册、登录,用户可以发布教程帖子、评论和点赞。
  2. 帖子管理:支持帖子的发布、编辑和删除等操作,帖子内容可以包含图片(七牛云)。
  3. 分类和标签:帖子可以被分配到不同的分类和标签,方便用户查找和阅读。
  4. 搜索和推荐:支持关键词搜索和帖子推荐功能,提高用户阅读体验。
  5. 安全和性能:采用Spring Security框架进行用户认证和授权

以上就是我项目的主要内容

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RI90XXJc-1681383922608)(https://raw.githubusercontent.com/viacheung/img/main/image/image-20230322005426563.png)]

1、JWT+token+redis

用户jwt鉴权流程

jwt 有三部分组成:A.B.C

A:Header,{“type”:“JWT”,“alg”:“HS256”} 固定 定义生成签名算法以及Token的类型

B:playload,存放实际需要传递的信息,比如,用户id,过期时间等等,可以被解密,不能存放敏感信息

C: 签证,A和B加上秘钥 加密而成,只要秘钥不丢失,可以认为是安全的。

jwt 验证,主要就是验证C部分 是否合法。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9p98Egvg-1681383922609)(https://raw.githubusercontent.com/viacheung/img/main/image/image-20230320181343651.png)]

  1. 用户使用用户名密码来请求服务器

  2. 服务器进行验证用户的信息

    3.服务器通过验证,发送给用户一个token(header.payload(id 电话号码啥的).签证)

    4.客户端存储token,并在每次请求时附送上这个token值(Authorization里面)

  3. 服务端验证token值,并返回数据,从中也可获取用户相关信息

jwt token生成和校验

使用私钥加密生成token 公钥解密获取token中的信息

jwt退出登录/修改密码时如何使原来的token失效

删除redis里面的token即可

redis or jwt?

然后呢,JWT是个啥?其实就是把用户的用户信息,用密钥加密防篡改,然后放在请求头里。用户请求的时候呢,再解密。服务器就不需要保存用户的信息了。简单来说,这个加密之后的信息写的是啥,服务器就认为用户是啥。

美其名曰,无状态,不需要消耗服务器的存储,减轻服务器压力。但是反过来,却带来了无法注销,请求头体积大,加解密效率等其他问题。然后为了解决这些问题,把redis的那一套解决方案,再引过来,两种方式结合在一起。。。讲道理,我吐了。强行混着来,jwt的意义何在呢?

用redis和不用的区别

1、用redis可以直接使jwt失效或者延期,方便

2、多个子系统可以访问redis数据库

就单点登录里的token而言,单点登录是为了实现一次登录其他相互信任的系统不用登陆就可访问的效果,既然如此就不止一个系统,每个系统的数据存在不同的数据库中,A系统不可以访问B系统的数据库,若将token存在于A系统数据库中,B系统登录时就访问不到token,但是所有系统都可以访问redis缓存数据库,而且token具有时效性,而redis天然支持设置过期时长【set(key,value,毫秒值)】

3、而且redis响应速度很快

登录时把jwt存进redis,设置成2倍的jwt有效期。登出删除redis保存的jwt。

如何防止jwt token被窃取

1、采用更安全的传输协议https

2、加密传输

3、代码层面也可以做安全检测,比如ip地址发生变化,MAC地址发生变化等等,可以要求重新登录

4、使用私钥加密生成token 公钥解密获取token中的信息

1)系统不能把jwt作为唯一的身份识别条件,不然被别人拿到了jwt就相当于获得了所有相关账户的权限,但对于这一点我还在学习中。2)存储jwt、传输的方式应该再加强。

private static final String slat = "mszlu!@#";//加密盐 因为数据库不能给人看密码  每次都用这一个字符串用来加密
token过期了怎么办?

我的项目是token不过期、redis记录过期

https://juejin.cn/post/7126708538440679460

如何踢人下线

直接把token和存入redis里,验证有无token即可。踢人下线直接清除redis中的token。和笔者的思路是类似的。

2、ThreadLocal保存用户信息

目的

方便鉴权,查询是否在线,减少数据库读操作

token:sysUser

不想用session(服务器存储 分布式)

文章发布 需要用户id,直接拿,评论也是一样,方便

request获取? 但是从设计上、代码分层上来说,

并发问题

降低redis使用?

redis中可以获取用户信息,但是因为redis中的key是token,要先拿到token才能拿到用户信息,但是token不是每个类中都存在,想在每个类都获取到用户信息

一般我们需要获取当前用户信息,放到缓存?每次解析token,然后传递?我们可以使用ThreadLocal来解决。将用户信息保存在线程中,当请求结束后我们在把保存的信息清除掉。这样我们才开发的时候就可以直接从全局的ThreadLocal中很方便的获取用户信息。当前线程在任何地方需要时,都可以使用

步骤

  • 创建ThreadLocal类,在其中设置相关的添加、获取以及删除方法。
  • 创建登录拦截器,重新其中的preHandle()(ThreadLocal.put())和afterCompletion()(不用了手动remove)方法。
  • webmvcConfig里面注册拦截器(告诉springmvc我们要拦截谁) 排除登录注册这种就行 其他都需要登录
原理

Thread类中,有个ThreadLocal.ThreadLocalMap 的成员变量。ThreadLocalMap内部维护了Entry数组,每个Entry代表一个完整的对象,keyThreadLocal本身,valueThreadLocal的泛型对象值

并发多线程场景下,每个线程Thread,在往ThreadLocal里设置值的时候,都是往自己的ThreadLocalMap里存,读也是以某个ThreadLocal作为引用,在自己的map里找对应的key,从而可以实现了线程隔离

为什么不直接用线程id作为ThreadLocalMap的key呢?

用了两个ThreadLocal成员变量的话。如果用线程id作为ThreadLocalMapkey,怎么区分哪个ThreadLocal成员变量呢?因此还是需要使用ThreadLocal作为Key来使用。每个ThreadLocal对象,都可以由threadLocalHashCode属性唯一区分的,每一个ThreadLocal对象都可以由这个对象的名字唯一区分

弱引用导致的内存泄漏呢?

ThreadLocalMap使用ThreadLocal弱引用作为key,当ThreadLocal变量被手动设置为null,即一个ThreadLocal没有外部强引用来引用它,当系统GC时,ThreadLocal一定会被回收。这样的话,ThreadLocalMap中就会出现keynullEntry,就没有办法访问这些keynullEntryvalue,如果当前线程再迟迟不结束的话(比如线程池的核心线程),这些keynullEntryvalue就会一直存在一条强引用链:Thread变量 -> Thread对象 -> ThreaLocalMap -> Entry -> value -> Object 永远无法回收,造成内存泄漏。

实际上,ThreadLocalMap的设计中已经考虑到这种情况。所以也加上了一些防护措施:即在ThreadLocalget,set,remove方法,都会清除线程ThreadLocalMap里所有keynullvalue

key是弱引用,GC回收会影响ThreadLocal的正常工作嘛?

不会的,因为有ThreadLocal变量引用着它,是不会被GC回收的,除非手动把ThreadLocal变量设置为null

ThreadLocal内存泄漏的demo

用线程池,一直往里面放对象

因为我们使用了线程池,线程池有很长的生命周期,因此线程池会一直持有tianLuoClass(ThreadLocal泛型值)对象的value值,即使设置tianLuoClass = null;引用还是存在的

为什么弱引用

ThreadLocal的对象被回收了,因为ThreadLocalMap持有ThreadLocal的弱引用,即使没有手动remove删除,ThreadLocal也会被回收。value则在下一次ThreadLocalMap调用set,get,remove的时候会被清除。

InheritableThreadLocal保证父子线程间的共享数据

在子线程中,是可以获取到父线程的 InheritableThreadLocal 类型变量的值,但是不能获取到 ThreadLocal 类型变量的值(因为ThreadLocal是线程隔离)。

Thread类中,除了成员变量threadLocals之外,还有另一个成员变量:inheritableThreadLocals

parent的inheritableThreadLocals不为null时,就会将parentinheritableThreadLocals,赋值给前线程的inheritableThreadLocals。说白了,就是如果当前线程的inheritableThreadLocals不为null,就从父线程哪里拷贝过来一个过来,类似于另外一个ThreadLocal,但是数据从父线程那里来的。有兴趣的小伙伴们可以在去研究研究源码~

ThreadLocal的应用场景和使用注意点

ThreadLocal很重要一个注意点,就是使用完,要手动调用remove()

ThreadLocal的应用场景主要有以下这几种:

  • 使用日期工具类,当用到SimpleDateFormat,使用ThreadLocal保证线性安全
  • 全局存储用户信息(用户信息存入ThreadLocal,那么当前线程在任何地方需要时,都可以使用)
  • 保证同一个线程,获取的数据库连接Connection是同一个,使用ThreadLocal来解决线程安全的问题
  • 使用MDC保存日志信息。
ThreadLocal特点

线程并发:在多线程并发场景下使用

**传递数据:**可以通过ThreadLocal在同一线程,不同组件中传递公共变量(保存每个线程的数据,在需要的地方可以直接获取, 避免参数直接传递带来的代码耦合问题)

线程隔离:每个线程的变量都是独立的, 不会互相影响

ThreadLocal 和Synchronized

ThreadLocal模式与Synchronized关键字都用于处理多线程并发访问变量的问题

ThreadLocal:以空间换取时间的思想, 为每一个线程都提供了一份变量的副本, 从而实现同访问而 互相不干扰。 多线程中让每个线程之间的数据相互隔离

Synchronized:以时间换取空间的思想,只提供了一份变量, 让不同的线程排队访问。多个线程之间访问资源的同步

ThreadLocal不能解决共享变量的线程安全问题

子线程访问父线程的共享变量时候,是“引用传递”,多个子线程访问的话所以线程不安全

  • 每个线程独享一份new出来的实例 -> 线程安全
  • 多个线程共享一份“引用类型”实例 -> 线程不安全
ThreadLocal与Thread同步机制的比较
线性探测法

线性探测法顾名思义,就是解决冲突的函数是一个线性函数,最直接的就是在TreadLocal的代码中也用的是这样一个解决冲突的函数。

 f(x)= x+1

但是要注意的是TreadLocal中,是一个环状的探测,如果到达边界就会直接跨越边界到另一头去。

线性探测法的优点:

  1. 不用额外的空间(对比拉链法,需要额外链表)
  2. 探测序列具有局部性,可以利用系统缓存,减少IO(连续的内存地址)

缺点:

  1. 耗费时间>O(1)(最差O(n))
  2. 冲突增多——以往的冲突会导致后续的连环冲突(时间复杂度趋近O(n))

之前我们说过,线性探测法有个问题是,一旦发生碰撞,很可能之后每次都会产生碰撞,导致连环撞车。而使用0x61c88647这个值做一个hash的增长值就可以从一定程度上解决这个问题让生成出来的值较为均匀地分布在2的幂大小的数组中。也就是说当我们用0x61c88647作为步长累加为每个ThreadLocal分配各自的ID也就是threadLocalHashCode再与2的幂取模,得到的结果分布很均匀

0x61c88647选取其实是与斐波那契散列有关,这个就是数学知识了,这里不展开。

3、日志记录放入线程池

原因

不能让记录日志出现失误影响用户的登录

为了出现错误时候的排查

记录日志录入数据库时,脱离主线程,实现异步插入,这样不会拖延主线程的执行时间
ps:记录日志,可以写在业务逻辑中,也可以利用aop自动记录。

我的线程池参数?

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DmLQyJqn-1681383922611)(https://raw.githubusercontent.com/viacheung/img/main/image/QFR%7D%7D83L%60%[email protected])]

这么设置也挺好,你的最佳核心线程数应该在cpuNum~cpuNum2 之间呢,cpuNum2 设置的太多了,cpu负载过大,速度反而会慢下来,cpuNum给的太少,也不能充分利用性能,利用队列满了之后溢出来的任务,被备用线程也就是核心线程之外的那些线程给处理了的这种机制,让他自动找补一下。

关于异步

如果是注册这类的功能不适合异步,肯定要同步的,注册成功才返回成功。我这个上传视频到OSS要异步的原因是因为用户要先把视频发到我的云服务器中,云服务器再去接收到的视频放到阿里云OSS中,是两个步骤。如果要两个操作成功才返回给用户上传成功的话太久了。所以在用户上传视频到云服务器成功后就可以返回成功信息给用户了,后面服务器再自己把视频上传到OSS(这个过程挺慢的)。具体逻辑还不够完善(主要是钱没到位)。不是什么情况下都可以用异步处理的。要一个操作设计多个步骤,并且后续的步骤都和用户没什么关系的情况下才会考虑要不要异步处理(可以提供用户体验,不用一直等)。比如银行的某些代付交易,在上游的数据检查完成正常后就会直接返回上游交易成功,用户直接就看到了交易结果,后续的调用银行核心记账这些操作其实还在跑,可能几秒甚至几分钟后才是真正的交易完成。

步骤

1.配置线程池(ThreadPoolTaskExecutor)
2.自定义一个异步任务管理器
3.自定义任务
4.指定地点处,调用执行任务管理器,传入指定的任务

execute() vs submit()

都是提交任务到线程池

  • execute() 方法用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功与否;
  • submit()方法用于提交需要返回值的任务。线程池会返回一个future类型的对象,通过这个future对象可以判断任务是否执行成功,并且可以通过future的get()方法来获取返回值,get()方法会阻塞当前线程直到任务完成,而使用 get(long timeout,TimeUnit unit)方法则会阻塞当前线程一段时间后立即返回,这时候有可能任务没有执行完。
为什么要用线程池?

资源消耗–重复利用 响应速度—立即执行 可管理性—线程池统一分配

  • 降低资源消耗。 通过重复利用已创建的线程降低线程创建和销毁造成的消耗
  • 提高响应速度。 当任务到达时,任务可以不需要的等到线程创建就能立即执行。
  • 提高线程的可管理性。 线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
如何创建线程池

1、构造⽅法

2、通过 Executor 框架的⼯具类 Executors 来实现

三种ThreadPoolExecutor:

fixedThreadPool() 固定线程数

SingleThreadExecutor 只有一个线程

CachedThreadPool 根据情况调整 数量不固定

ThreadPoolExecutor 类分析
ThreadPoolExecutor 构造函数七大参数分析

  • corePoolSize : 核心线程大小。线程池一直运行,核心线程就不会停止。

  • maximumPoolSize :线程池最大线程数量。非核心线程数量=maximumPoolSize-corePoolSize

  • keepAliveTime :非核心线程的心跳时间。如果非核心线程在keepAliveTime内没有运行任务,非核心线程会消亡。

  • workQueue :阻塞队列。ArrayBlockingQueue,LinkedBlockingQueue等,用来存放线程任务。

  • defaultHandler :饱和策略。ThreadPoolExecutor类中一共有4种饱和策略。通过实现

    RejectedExecutionHandler

    接口。

    饱和策略

    • AbortPolicy : 线程任务丢弃报错。默认饱和策略。
    • DiscardPolicy : 线程任务直接丢弃不报错。
    • DiscardOldestPolicy : 将workQueue队首任务丢弃,将最新线程任务重新加入队列执行。
    • CallerRunsPolicy :线程池之外的线程直接调用run方法执行。
  • ThreadFactory :线程工厂。新建线程工厂。

线程池原理分析

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JL9eoruj-1681383922612)(https://raw.githubusercontent.com/viacheung/img/main/image/1460000039258685)]

  1. 线程池执行execute/submit方法向线程池添加任务,当任务小于核心线程数corePoolSize,线程池中肯定可以创建新的线程。
  2. 当任务大于核心线程数corePoolSize,看看阻塞队列满了没,没满的话,就向阻塞队列添加任务,如果满的话,看看任务数和最大线程数的关系,如果还小于最大线程数的话,那我创建非核心线程。
  3. 如果线程数量大于maximumPoolSize,说明当前设置线程池中线程已经处理不了了,就会执行饱和策略。
常见java线程池

1、newCachedThreadPool

创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。

这种类型的线程池特点是:

工作线程的创建数量几乎没有限制(其实也有限制的,数目为Interger. MAX_VALUE), 这样可灵活的往线程池中添加线程。

不足:这种方式虽然可以根据业务场景自动的扩展线程数来处理我们的业务,但是最多需要多少个线程同时处理缺是我们无法控制的;

优点:如果当第二个任务开始,第一个任务已经执行结束,那么第二个任务会复用第一个任务创建的线程,并不会重新创建新的线程,提高了线程的复用率;

2、newFixedThreadPool

创建一个指定工作线程数量的线程池。每当提交一个任务就创建一个工作线程,如果工作线程数量达到线程池初始的最大数,则将提交的任务存入到池队列中。

**缺点:**它具有线程池提高程序效率和节省创建线程时所耗的开销的优点。但是,在线程池空闲时,即线程池中没有可运行任务时,它不会释放工作线程,还会占用一定的系统资源。

优点:newFixedThreadPool的线程数是可以进行控制的,因此我们可以通过控制最大线程来使我们的服务器打到最大的使用率,同事又可以保证及时流量突然增大也不会占用服务器过多的资源。

3、newSingleThreadExecutor

创建一个单线程化的Executor,只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。如果这个线程异常结束,会有另一个取代它,保证顺序执行。单工作线程最大的特点是可保证顺序地执行各个任务,并且在任意给定的时间不会有多个线程是活动的。

4、newScheduleThreadPool

创建一个定长的线程池,而且支持定时的以及周期性的任务执行,支持定时及周期性任务执行。

线程池常用的阻塞队列有哪些?

表格左侧是线程池,右侧为它们对应的阻塞队列,可以看到 5 种线程池对应了 3 种阻塞队列

  1. LinkedBlockingQueue 对于 FixedThreadPool 和 SingleThreadExector 而言,它们使用的阻塞队列是容量为 Integer.MAX_VALUE 的 LinkedBlockingQueue,可以认为是无界队列。由于 FixedThreadPool 线程池的线程数是固定的,所以没有办法增加特别多的线程来处理任务,因此需要这样一个没有容量限制的阻塞队列来存放任务。

    由于线程池的任务队列永远不会放满,所以线程池只会创建核心线程数量的线程,所以此时的最大线程数对线程池来说没有意义,因为并不会触发生成多于核心线程数的线程。

  2. SynchronousQueue 第二种阻塞队列是 SynchronousQueue,对应的线程池是 CachedThreadPool。线程池 CachedThreadPool 的最大线程数是 Integer 的最大值,可以理解为线程数是可以无限扩展的。CachedThreadPool 和上一种线程池 FixedThreadPool 的情况恰恰相反,FixedThreadPool 的情况是阻塞队列的容量是无限的,而这里 CachedThreadPool 是线程数可以无限扩展,所以 CachedThreadPool 线程池并不需要一个任务队列来存储任务,因为一旦有任务被提交就直接转发给线程或者创建新线程来执行,而不需要另外保存它们。 我们自己创建使用 SynchronousQueue 的线程池时,如果不希望任务被拒绝,那么就需要注意设置最大线程数要尽可能大一些,以免发生任务数大于最大线程数时,没办法把任务放到队列中也没有足够线程来执行任务的情况。

  3. DelayedWorkQueue 第三种阻塞队列是DelayedWorkQueue,它对应的线程池分别是 ScheduledThreadPool 和 SingleThreadScheduledExecutor,这两种线程池的最大特点就是可以延迟执行任务,比如说一定时间后执行任务或是每隔一定的时间执行一次任务。

DelayedWorkQueue 的特点是内部元素并不是按照放入的时间排序,而是会按照延迟的时间长短对任务进行排序,内部采用的是“堆”的数据结构。之所以线程池 ScheduledThreadPool 和 SingleThreadScheduledExecutor 选择 DelayedWorkQueue,是因为它们本身正是基于时间执行任务的,而延迟队列正好可以把任务按时间进行排序,方便任务的执行。

源码中线程池是怎么复用线程的?

源码中ThreadPoolExecutor中有个内置对象Worker,每个worker都是一个线程,worker线程数量和参数有关,每个worker会while死循环从阻塞队列中取数据,通过置换worker中Runnable对象,运行其run方法起到线程置换的效果,这样做的好处是避免多线程频繁线程切换,提高程序运行性能。

如何合理配置线程池参数

选择的关键点是:

  • 尽量减少线程切换和管理的开支
  • 最大化利用cpu
  1. 并发高 耗时短:
  • 这种场景适合线程尽量少,因为如果线程太多,任务执行时间段很快就执行完了,有可能出现线程切换和管理多耗费的时间,大于任务执行的时间,这样效率就低了。线程池线程数可以设置为CPU核数+1

    2.并发比较低,耗时比较长的任务

需要我们自己配置最大线程数 maximumPoolSize ,为了高效的并发运行,这时需要看我们的业务是IO密集型还是CPU密集型。

CPU密集型 CPU密集的意思是该任务需要最大的运算,而没有阻塞,CPU一直全速运行。CPU密集任务只有在真正的多核CPU上才能得到加速(通过多线程)。而在单核CPU上,无论你开几个模拟的多线程该任务都不可能得到加速,因为CPU总的运算能力就那么多。一般公式:CPU核数 + 1个线程数

IO密集型 IO密集型,即该任务需要大量的IO,即大量的阻塞在单线程上运行IO密集型的任务会导致大量的CPU运算能力浪费在等待。所以在IO密集型任务中使用多线程可以大大的加速程序运行,即使在单核CPU上这种加速主要就是利用了被浪费掉的阻塞时间

IO 密集型时,大部分线程都阻塞,故需要多配制线程数。公式为:

CPU核数*2
CPU核数/(1-阻塞系数) 阻塞系数在0.8~0.9之间
查看CPU核数:
System.out.println(Runtime.getRuntime().availableProcessors());
例如:8核CPU:8/ (1 - 0.9) = 80个线程数

注:IO密集型(某大厂实践经验)
       核心线程数 = CPU核数 / (1-阻塞系数)
或着
CPU密集型:核心线程数 = CPU核数 + 1
IO密集型:核心线程数 = CPU核数 * 2

也有说配置为cpu核数

Executor和Executors的区别?

Executors :按照需求创建了不同的线程池,来满足业务的需求。

Executor :执行线程任务 获得任务执行的状态并且可以获取任务的返回值。

使用ThreadPoolExecutor 可以创建自定义线程池。Future 表示异步计算的结果,他提供了检查计算是否完成的方法,以等待计算的完成,并可以使用get()方法获取计算的结果

线程池应用场景

1、异步发送邮件通知
发送一个任务,然后注入到线程池中异步发送。

2、心跳请求任务
创建一个任务,然后定时发送请求到线程池中。

3、如果用户量比较大,导致占用过多的资源,可能会导致我们的服务由于资源不足而宕机;

3.1 线程池中线程的使用率提升,减少对象的创建、销毁;

3.2 线程池可以控制线程数,有效的提升服务器的使用资源,避免由于资源不足而发生宕机等问题;

4、Elasticsearch搜索 +同步数据到索引

步骤

1、导包 做配置

2、配置Document

先要配置实体和ES的映射,通过在实体类中加入注解的方式来自动映射跟索引,我这里是配置了product索引和实体的映射

3、使用ElasticsearchRestTemplate

在Spring启动的时候自动注入了该Bean,它封装了操作Elasticsearch的增删改查API

完全匹配查询条件 --> 按照阅读量排序—>高亮显示—>分页

es刷新时间

1s 为什么要延长 es主要业务写日志

5、阅读评论数放入Redis中

查看完文章了,新增阅读数,做了一个更新操作,更新时加写锁,阻塞其他的读操作,性能就会比较低(没办法解决,增加阅读数必然要加锁)

更新增加了此次接口的耗时(考虑减少耗时)如果一旦更新出问题,不能影响查看操作
线程池 可以把更新操作扔到 线程池中去执行和主线程就不相关了
threadService.updateArticleViewCount(articleMapper, article);

在这里我们采用redis incr自增实现

redis定时任务自增实现阅读数和评论数更新

阅读数和评论数 ,考虑把阅读数和评论数 增加的时候 放入redis incr自增,使用定时任务 定时把数据固话到数据库当中

定时任务 :遍历redis中前缀是VIEW_COUNT的所有key,通过subString方法获取文章id,获取key存储的阅读数,把文章id和阅读数放入ViewCountQuery对象中,对象放入list集合中,批量更新

@Scheduled(cron = "0 30 4 ? * *")//每天凌晨四点半触发

https://blog.csdn.net/m0_52914401/article/details/124343310?utm_medium=distribute.pc_relevant.none-task-blog-2defaultbaidujs_baidulandingword~default-0-124343310-blog-125651961.pc_relevant_recovery_v2&spm=1001.2101.3001.4242.1&utm_relevant_index=3

出现缓存不一致问题?
1、先删除缓存,后更新数据库
答案一:延时双删

(1)先淘汰缓存

(2)再写数据库

(3)休眠1秒,再次淘汰缓存,这么做,可以将1秒内所造成的缓存脏数据,再次删除。确保读请求结束,写请求可以删除读请求造成的缓存脏数据。自行评估自己的项目的读数据业务逻辑的耗时,写数据的休眠时间则在读数据业务逻辑的耗时基础上,加几百ms即可。

(我的理解:请求A先删缓存再往DB写数据,就算这时B来查数据库,缓存没数据,然后查DB,此时查到的是旧数据,写到缓存,A等待B写完之和再删缓存,这样就缓存一致)

如果使用的是 Mysql 的读写分离的架构的话,那么其实主从同步之间也会有时间差。

此时来了两个请求,请求 A(更新操作) 和请求 B(查询操作)

  1. 请求 A 更新操作,删除了 Redis
  2. 请求主库进行更新操作,主库与从库进行同步数据的操作
  3. 请 B 查询操作,发现 Redis 中没有数据
  4. 去从库中拿去数据
  5. 此时同步数据(binlog没写完)还未完成,拿到的数据是旧数据

此时的解决办法就是如果是对 Redis 进行填充数据的查询数据库操作,那么就强制将其指向主库进行查询。

答案二: 更新与读取操作进行异步串行化

采用更新与读取操作进行异步串行化

异步串行化

我在系统内部维护n个内存队列,更新数据的时候,根据数据的唯一标识,将该操作路由之后,发送到其中一个jvm内部的内存队列中(对同一数据的请求发送到同一个队列)。读取数据的时候,如果发现数据不在缓存中,并且此时队列里有更新库存的操作,那么将重新读取数据+更新缓存的操作,根据唯一标识路由之后,也将发送到同一个jvm内部的内存队列中。然后每个队列对应一个工作线程,每个工作线程串行地拿到对应的操作,然后一条一条的执行。

这样的话,一个数据变更的操作,先执行删除缓存,然后再去更新数据库,但是还没完成更新的时候,如果此时一个读请求过来,读到了空的缓存,那么可以先将缓存更新的请求发送到队列中,此时会在队列中积压,排在刚才更新库的操作之后,然后同步等待缓存更新完成,再读库。

读操作去重

多个读库更新缓存的请求串在同一个队列中是没意义的,因此可以做过滤,如果发现队列中已经有了该数据的更新缓存的请求了,那么就不用再放进去了,直接等待前面的更新操作请求完成即可,待那个队列对应的工作线程完成了上一个操作(数据库的修改)之后,才会去执行下一个操作(读库更新缓存),此时会从数据库中读取最新的值,然后写入缓存中。

如果请求还在等待时间范围内,不断轮询发现可以取到值了,那么就直接返回;如果请求等待的时间超过一定时长,那么这一次直接从数据库中读取当前的旧值。(返回旧值不是又导致缓存和数据库不一致了么?那至少可以减少这个情况发生,因为等待超时也不是每次都是,几率很小吧。这里我想的是,如果超时了就直接读旧值,这时候仅仅是读库后返回而不放缓存)

2、先更新数据库,后删除缓存

这一种情况也会出现问题,比如更新数据库成功了,但是在删除缓存的阶段出错了没有删除成功,那么此时再读取缓存的时候每次都是错误的数据了。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ygWZDQKt-1681383922614)(https://raw.githubusercontent.com/viacheung/img/main/image/1735bb5881fb4a1b~tplv-t2oaga2asx-watermark.awebp)]

此时解决方案就是利用消息队列进行删除的补偿。具体的业务逻辑用语言描述如下:

  1. 请求 A 先对数据库进行更新操作
  2. 在对 Redis 进行删除操作的时候发现报错,删除失败
  3. 此时将Redis 的 key 作为消息体发送到消息队列中
  4. 系统接收到消息队列发送的消息后再次对 Redis 进行删除操作

但是这个方案会有一个缺点就是会对业务代码造成大量的侵入,深深的耦合在一起,所以这时会有一个优化的方案,我们知道对 Mysql 数据库更新操作后在binlog 日志中我们都能够找到相应的操作,那么我们可以订阅 Mysql 数据库的 binlog 日志对缓存进行操作。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3LQvY5Dt-1681383922616)(https://raw.githubusercontent.com/viacheung/img/main/image/1735bb588215b298~tplv-t2oaga2asx-watermark.awebp)]

6、RabbitMQ消息队列实现ES和数据库的数据同步

主要是增删改帖子

这里用的Direct队列

  • Fanout交换机将消息路由给每一个与之绑定的队列
  • Direct交换机根据RoutingKey判断路由给哪个队列
  • 如果多个队列具有相同的RoutingKey,则与Fanout功能类似
步骤

1、导包:spring-boot-starter-amqp

SpringAMQP是基于RabbitMQ封装的一套模板,并且还利用SpringBoot对其实现了自动装配,使用起来非常方便。

2、添加配置

spring:
rabbitmq:
host: localhost
port: 5672
username: admin
password: 123456
virtual-host: myHost

3、RabbitMQ的配置 声明Exchange、Queue、RoutingKey

4、生产者对应的增删改里面发送消息

所谓的生产者就是我们数据库的服务方,当我们对数据库的数据进行增删改的时候,我们应该像消息队列发送消息来通知ES我们进行了增删改操作,以便ES进行数据的同步。

5、消费者MQListener监听消息

所谓的消费者就是ES服务的操作方,通过实时的对消息队列的监听,通过消息队列对应的key值来进行选择服务的调用,不同的key调用不同的服务,获取服务方传输的数据,然后进行数据的同步。

7、死信交换机

目的

实现消息的延迟投递,避免消息丢失或无限制的重试

概念

当你在消费消息时,如果队列里的消息出现以下情况
1,消息被否定确认,使用 channel.basicNack 或channel.basicReject ,并且此时requeue 属性被设置为false。
2,消息在队列的存活时间超过设置的TTL时间
3,消息队列的消息数量已经超过最大队列长度

那么该消息将成为“死信”。“死信”消息会被RabbitMQ进行特殊处理,如果配置了死信队列信息,那么该消息将会被丢进死信队列中,如果没有配置,则该消息将会被丢弃。

步骤

大概可以分为以下步骤:

1,配置业务队列,绑定到业务交换机上
2,为业务队列配置死信交换机和路由key
3,为死信交换机配置死信队列

为每个需要使用死信的业务队列配置一个死信交换机,这里同一个项目的死信交换机可以共用一个,然后为每个业务队列分配一个单独的路由key。

有了死信交换机和路由key后,接下来,就像配置业务队列一样,配置死信队列,然后绑定在死信交换机上。也就是说,死信队列并不是什么特殊的队列,只不过是绑定在死信交换机上的队列。死信交换机也不是什么特殊的交换机,只不过是用来接受死信的交换机,所以可以为任何类型【Direct、Fanout、Topic】。一般来说,会为每个业务队列分配一个独有的路由key,并对应的配置一个死信队列进行监听,也就是说,一般会为每个重要的业务队列配置一个死信队列。

死信消息变化

如果队列配置了参数 x-dead-letter-routing-key 的话,“死信”的路由key将会被替换成该参数对应的值。如果没有设置,则保留该消息原有的路由key。

应用场景

确保未被正确消费的消息不被丢弃(更新删除修改帖子)

发生消费异常可能原因:

  1. 消息信息本身存在错误
  2. 处理过程中参数校验异常
  3. 网络波动导致的查询异常等等

当发生异常时,当然不能每次通过日志来获取原消息,然后让运维帮忙重新投递消息(没错,以前就是这么干的= =)。通过配置死信队列,可以让未正确处理的消息暂存到另一个队列中,待后续排查清楚问题后,编写相应的处理代码来处理死信消息,这样比手工恢复数据要好太多了。

总结

死信队列其实并没有什么神秘的地方,不过是绑定在死信交换机上的普通队列,而死信交换机也只是一个普通的交换机,不过是用来专门处理死信的交换机。

总结一下死信消息的生命周期:

1,业务消息被投入业务队列
2,消费者消费业务队列的消息,由于处理过程中发生异常,于是进行了nck或者reject操作
3,被nck或reject的消息由RabbitMQ投递到死信交换机中
4,死信交换机将消息投入相应的死信队列
5,死信队列的消费者消费死信消息
———————————————

8、RabbitMQ如何处理消息丢失

1)生产者弄丢了数据

生产者将数据发送到rabbitmq的时候,可能因为网络问题导致数据就在半路给搞丢了。

1.1 使用事务(不推荐)

生产者发送数据前开启事务,然后发送消息,如果消息没有成功被rabbitmq接收到,那么生产者会收到异常报错,此时就可以回滚事务(channel.txRollback),然后重试发送消息;如果收到了消息,那么可以提交事务(channel.txCommit)。但是问题是,开始rabbitmq事务机制,基本上吞吐量会下来,因为太耗性能

1.2 发送回执确认(推荐)

在生产者那里设置开启confirm模式之后,你每次写的消息都会分配一个唯一的id,然后如果写入了rabbitmq中,rabbitmq会给你回传一个ack消息,告诉你说这个消息ok了。如果rabbitmq没能处理这个消息,会回调你一个nack接口,告诉你这个消息接收失败,你可以重试。

但如果RabbitMQ服务端正常接收到了,把ack信息发送给生产者,结果这时网断了:

可以结合这个机制自己在内存里维护每个消息id的状态,如果超过一定时间还没接收到这个消息的回调,那么你可以重发。(消费者就要处理幂等问题,多次接收到同一条消息)

区别

事务机制是同步的,你提交一个事务之后会阻塞在那儿,但是confirm机制是异步的,你发送个消息之后就可以发送下一个消息,然后那个消息rabbitmq接收了之后会异步回调你一个接口通知你这个消息接收到了。

所以一般在生产者这块避免数据丢失,都是用confirm机制的。

2)RabbitMQ弄丢了数据-开启RabbitMQ的数据持久化

数据持久化:rabbitmq自己挂了,恢复之后会自动读取之前存储的数据,一般数据不会丢。除非极其罕见的是,rabbitmq还没持久化,自己就挂了,可能导致少量数据会丢失的,但是这个概率较小。

使用:跟生产者那边的confirm机制配合起来,只有消息被持久化到磁盘之后,才会通知生产者ack了,所以哪怕是在持久化到磁盘之前,rabbitmq挂了,数据丢了,生产者收不到ack,你也是可以自己重发的。

步骤: 第一个是创建queue的时候将其设置为持久化,这样就可以保证rabbitmq持久化queue的元数据。

第二个是发送消息的时候将消息的deliveryMode设置为2,就是将消息设置为持久化的

这样abbitmq哪怕是挂了,再次重启,也会从磁盘上重启恢复queue,恢复这个queue里的数据。

3)消费端弄丢了数据

主要是因为你消费的时候,刚消费到,还没处理,结果进程挂了比如重启了,那么就尴尬了,RabbitMQ认为你都消费了,这数据就丢了。或者消费者拿到数据之后挂了,这时候需要MQ重新指派另一个消费者去执行任务

这个时候得用RabbitMQ提供的ack机制,也是一种处理完成发送回执确认的机制。如果MQ等待一段时间后你没有发送过来处理完成 那么RabbitMQ就认为你还没处理完,这个时候RabbitMQ会把这个消费分配给别的consumer去处理,消息是不会丢的。

https://segmentfault.com/a/1190000019125512

RabbitMQ相关知识
多线程vsMQ

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HZDHh4XB-1681383922618)(https://raw.githubusercontent.com/viacheung/img/main/image/image-20230325004307581.png)]

为什么使用MQ?

使用MQ的场景很多,主要有三个:解耦、异步、削峰。

  • 解耦:假设现在,日志不光要插入到数据库里,还要在硬盘中增加文件类型的日志,同时,一些关键日志还要通过邮件的方式发送给指定的人。那么,如果按照原来的逻辑,A可能就需要在原来的代码上做扩展,除了B服务,还要加上日志文件的存储和日志邮件的发送。但是,如果你使用了MQ,那么,A服务是不需要做更改的,它还是将消息放到MQ中即可,其它的服务,无论是原来的B服务还是新增的日志文件存储服务或日志邮件发送服务,都直接从MQ中获取消息并处理即可。这就是解耦,它的好处是提高系统灵活性,扩展性。
  • 异步:可以将一些非核心流程,如日志,短信,邮件等,通过MQ的方式异步去处理。这样做的好处是缩短主流程的响应时间,提升用户体验。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4vk2gmra-1681383922619)(https://raw.githubusercontent.com/viacheung/img/main/image/727602-20200108091722601-747710174.png)]

  • 削峰:MQ的本质就是业务的排队。所以,面对突然到来的高并发,MQ也可以不用慌忙,先排好队,不要着急,一个一个来。削峰的好处就是避免高并发压垮系统的关键组件,如某个核心服务或数据库等。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Bq0Xl4Fe-1681383922620)(https://raw.githubusercontent.com/viacheung/img/main/image/727602-20200108091915241-1598228624.png)]

后面请求积压在MQ里面 不过是短暂的

消息队列的缺点

1、 系统可用性降低

系统引入的外部依赖越多,越容易挂掉。

2、 系统复杂度提高

加入了消息队列,要多考虑很多方面的问题,比如:一致性问题、如何保证消息不被重复消费、如何保证消息可靠性传输等。因此,需要考虑的东西更多,复杂性增大。

3、 一致性问题

A 系统处理完了直接返回成功了,人都以为你这个请求就成功了;但是问题是,要是 BCD 三个系统那里,BD 两个系统写库成功了,结果 C 系统写库失败了,这就数据不一致了。

生产者消息运转的流程
  1. Producer先连接到Broker,建立连接Connection,开启一个信道(Channel)。
  2. Producer声明一个交换器并设置好相关属性。
  3. Producer声明一个队列并设置好相关属性。
  4. Producer通过路由键将交换器和队列绑定起来。
  5. Producer发送消息到Broker,其中包含路由键、交换器等信息。
  6. 相应的交换器根据接收到的路由键查找匹配的队列。
  7. 如果找到,将消息存入对应的队列,如果没有找到,会根据生产者的配置丢弃或者退回给生产者。
  8. 关闭信道。
  9. 管理连接。
消费者接收消息过程?
  1. Consumer先连接到Broker,建立连接Connection,开启一个信道(Channel)。
  2. Broker请求消费响应的队列中消息,可能会设置响应的回调函数。
  3. 等待Broker回应并投递相应队列中的消息,接收消息。
  4. 消费者确认收到的消息,ack
  5. RabbitMq从队列中删除已经确定的消息。
  6. 关闭信道。
  7. 关闭连接。
生产者如何将消息可靠投递到RabbitMQ?
  1. Producer发送消息给MQ
  2. MQ将消息持久化后,发送Ack消息给Producer,此处有可能因为网络问题导致Ack消息无法发送到Producer,那么Producer在等待超时后,会重传消息;
  3. Producer收到Ack消息后,认为消息已经投递成功
RabbitMQ如何将消息可靠投递到消费者?
  1. MQ将消息push给Consumer(或Consumer来pull消息)
  2. Consumer得到消息并做完业务逻辑
  3. Consumer发送Ack消息给MQ,通知MQ删除该消息,此处有可能因为网络问题导致Ack失败,那么Consumer会重复消息,这里就引出消费幂等的问题;
  4. MQ将已消费的消息删除。

消息幂等

根据业务特性,选取业务中唯一的某个属性,比如订单号作为区分消息是否重复的属性。在进行插入订单之前,先从数据库查询一下该订单号的数据是否存在,如果存在说明是重复消费,如果不存在则插入。伪代码如下:

我遇到的问题

1、前后端交互问题

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ODbMiyUn-1681383922622)(https://raw.githubusercontent.com/viacheung/img/main/image/image-20230322145324052.png)]

其他问题

1、TEXT 存储文章内容

TINYTEXT(255长度)

TEXT(65535)

MEDIUMTEXT(int最大值16M)

LONGTEXT(long最大值4G)

2、Docker

Docker如何解决大型项目依赖关系复杂,不同组件依赖的兼容性问题?

  • Docker允许开发中将应用、依赖、函数库、配置一起打包,形成可移植镜像
  • Docker应用运行在容器中,使用沙箱机制,相互隔离

Docker如何解决开发、测试、生产环境有差异的问题?

  • Docker镜像中包含完整运行环境,包括系统函数库,仅依赖系统的Linux内核,因此可以在任意Linux操作系统上运行

Docker是一个快速交付应用、运行应用的技术,具备下列优势:

  • 可以将程序及其依赖、运行环境一起打包为一个镜像,可以迁移到任意Linux操作系统
  • 运行时利用沙箱机制形成隔离容器,各个应用互不干扰
  • 启动、移除都可以通过一行命令完成,方便快捷

Docker和虚拟机的差异:

  • docker是一个系统进程;虚拟机是在操作系统中的操作系统
  • docker体积小、启动速度快、性能好;虚拟机体积大、启动速度慢、性能一般
基本概念

镜像:

  • 将应用程序及其依赖、环境、配置打包在一起

容器:

  • 镜像运行起来就是容器,一个镜像可以运行多个容器 容器有自己独立的cpu 内存 文件系统 避免污染镜像

Docker结构:

  • 服务端:接收命令或远程请求,操作镜像或容器

  • 客户端:发送命令或者请求到Docker服务端

DockerHub:

  • 一个镜像托管的服务器,类似的还有阿里云镜像服务,统称为DockerRegistry
命令

docker run命令的常见参数有哪些?

  • –name:指定容器名称
  • -p:指定端口映射
  • -d:让容器后台运行

查看容器日志的命令:

  • docker logs
  • 添加 -f 参数可以持续查看日志

查看容器状态:

  • docker ps
  • docker ps -a 查看所有容器,包括已经停止的
数据卷

数据卷的作用:

  • 将容器与数据分离,解耦合,方便操作容器内数据,保证数据安全

数据卷操作:

  • docker volume create:创建数据卷
  • docker volume ls:查看所有数据卷
  • docker volume inspect:查看数据卷详细信息,包括关联的宿主机目录位置
  • docker volume rm:删除指定数据卷
  • docker volume prune:删除所有未使用的数据卷

docker run的命令中通过 -v 参数挂载文件或目录到容器中:

  • -v volume名称:容器内目录
  • -v 宿主机文件:容器内文件
  • -v 宿主机目录:容器内目录

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-aErY5ZA0-1681383922623)(https://raw.githubusercontent.com/viacheung/img/main/image/image-20230403002812590.png)]

数据卷挂载与目录直接挂载的

  • 数据卷挂载耦合度低,由docker来管理目录,但是目录较深,不好找
  • 目录挂载耦合度高,需要我们自己管理目录,不过目录容易寻找查看、
dockerfile
  1. Dockerfile的本质是一个文件,通过指令描述镜像的构建过程
  2. Dockerfile的第一行必须是FROM,从一个基础镜像来构建
  3. 基础镜像可以是基本操作系统,如Ubuntu。也可以是其他人制作好的镜像,例如:java:8-alpine
dockercompose

Docker Compose可以基于Compose文件帮我们快速的部署分布式应用,而无需手动一个个创建和运行容器!

3、linux命令

RPC

反射:newInstance() (实例化) getDeclaredMethod(public方法) invoke(方法输出)

蘑菇识别

上传图片到根目录:根目录 input文件夹存一下这个图片,然后python identify.py 路径+图片名称 识别出结果 各个种类的预测百分率(字符串) 对字符串进行一个处理

取前三,返回一个封装结果,同时还支持详细查询蘑菇的描述

论坛知识点(业务)

文章列表

通过传入的PageParams对象进行查询 调用service -> mapper ->xml->sql的select查询语句 ,传参包括分类,标签,年月(查询范围)返回IPage对象,copyList转化格式

最热标签

SELECT tag_id FROM ms_article_tag group by tag_id order by count(*) desc limit #{limit}

找ms_article_tag表查出对应article最多的tagid 然后再根据tagid查tag,封装到tagvo里面 返回名称+头像

最热文章

sevice层里面直接调用LambdaQueryWrapper

查询条件构造器 lamda表达式 链式查询

LambdaQueryWrapper
queryWrapper = new LambdaQueryWrapper<>(); queryWrapper.orderByDesc(Article::getViewCounts); queryWrapper.select(Article::getId,Article::getTitle); queryWrapper.last("limit " + limit); List
articles = articleMapper.selectList(queryWrapper); return Result.success(copyList(articles,false,false));

最新文章

一样只是

 queryWrapper.orderByDesc(Article::getCreateDate);

文章归档

 select year(create_date) as year,month(create_date) as month,count(*) as count from ms_article group by year,month

按照年月查询

登录

用户jwt鉴权流程

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5dMq4gbB-1681383922624)(https://raw.githubusercontent.com/viacheung/img/main/image/image-20230320181343651.png)]

  1. 用户使用用户名密码来请求服务器

  2. 服务器进行验证用户的信息

    3.服务器通过验证,发送给用户一个token(header.payload(id 电话号码啥的).签证)

    4.客户端存储token,并在每次请求时附送上这个token值

  3. 服务端验证token值,并返回数据

jwt token生成和校验

使用私钥加密生成token 公钥解密获取token中的信息

如何防止jwt token被窃取

1、采用更安全的传输协议https

2、加密传输

3、代码层面也可以做安全检测,比如ip地址发生变化,MAC地址发生变化等等,可以要求重新登录

4、使用私钥加密生成token 公钥解密获取token中的信息

jwt 有三部分组成:A.B.C

A:Header,{“type”:“JWT”,“alg”:“HS256”} 固定 定义生成签名算法以及Token的类型

B:playload,存放实际需要传递的信息,比如,用户id,过期时间等等,可以被解密,不能存放敏感信息

C: 签证,A和B加上秘钥 加密而成,只要秘钥不丢失,可以认为是安全的。

jwt 验证,主要就是验证C部分 是否合法。

private static final String slat = "mszlu!@#";//加密盐 因为数据库不能给人看密码  每次都用这一个字符串用来加密

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KzUuWOqi-1681383922626)(https://raw.githubusercontent.com/viacheung/img/main/image/image-20230316152438763.png)]

统一异常处理

不管是controller层还是service,dao层,都有可能报异常,如果是预料中的异常,可以直接捕获处理,如果是意料之外的异常,需要统一进行处理,进行记录,并给用户提示相对比较友好的信息。

package com.mszlu.blog.handler;

import com.mszlu.blog.vo.Result;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;

//对加了@Controller注解的方法进行拦截处理 AOP的实现
@ControllerAdvice
public class AllExceptionHandler {
    //进行异常处理,处理Exception.class的异常
    @ExceptionHandler(Exception.class)
    @ResponseBody //返回json数据
    public Result doException(Exception ex){
        ex.printStackTrace();
        return Result.fail(-999,"系统异常");
    }

}

编辑发表博客

写文章需要 三个接口:

  1. 获取所有文章类别
  2. 获取文章标签
  3. 发布文章 POST

下拉框文章类别、文章类别

1.发布文章 目的 构建Article对象

2.作者id 当前的 登录用户

3.标签要将标签加入到关联列表当中

4.body 内容存储

上传图片:七牛云

Security集成

登录认证

SecurityConfig配置类,硬编码,也就是在访问后台权限管理之前搞一个登录认证访问main.html的时候跳转login.html

    //当用户登录的时候,springSecurity 就会将请求 转发到此
    //根据用户名 查找用户,不存在 抛出异常,存在 将用户名,密码,授权列表 组装成springSecurity的User对象 并返回

剩下的认证 就由框架帮我们完成

权限认证

根据用户职能让其干对应的事情

admission表里面,每个用户对应一个权限路径,当用户访问的时候,拿到其权限路径和访问路径对比,相等就放行

只要你这个用户其中的一个权限路径是和requestURI一样的就

博客的评论与点赞

rocketmq

导依赖 做配置

@RocketMQMessageListener 发送一条消息给rocketmq 当前文章更新了,更新一下缓存吧 得到redisKey,更新查看文章详情的缓存

怎么分页的?

定义一个配置类,然后定义MyBatisPlus拦截器并将其设置为Spring管控的bean

先创建MyBatisPlus的拦截器栈,再初始化了分页拦截器,并添加到拦截器栈中。如果后期开发其他功能,需要添加全新的拦截器,按照第二行的格式继续add进去新的拦截器就可以了。

@Configuration
//扫包,将此包下的接口生成代理实现类,并且注册到spring容器中
@MapperScan("com.mszlu.blog.dao")
public class MybatisPlusConfig 

跨域问题?

比如论坛项目中评论是如何存储的?怎么展示所有的评论?

项目中框架或者中间件的使用细节。项目里怎么用ES的,ES怎么支持搜索的?缓存和DB是如何结合使用的?

2.1. 项日存在哪些问题,你准备怎么解决?
2.2. 项目的具体功能点如何优化?如论坛项目,查询评论是在DB里扫表查询吗?想要查询更快可以做哪些优化?
2.3. 项目中最有挑战的模块是哪个,你是怎么解决的?
项目要增大10倍的qps,你会怎么设计?
2.5. 项目上线后出现线上问题怎么解决?如频繁fullGc,定时任务失败怎么办?
为什么做这个项目,技术选型为什么是这样的?

登录怎么做的?单点登录说说你的理解?

说说项目中的闪光点和亮点?

Nginx

部署静态资源

服务端真实存在并且能够直接展示的一些文件 html js css 图片 视频’

降低服务端压力

相对于Tomcat,Nginx处理静态资源的能力更加高效,放到html文件里面

反向代理

代理服务器(正向代理代理客户端)

起到了安全防护作用

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AFLDzmJt-1681383922627)(https://raw.githubusercontent.com/viacheung/img/main/image/image-20230314140013128.png)]

负载均衡

业务功能复杂 —多集群—负载均衡算法(轮询 权重 响应时间)

nginx.conf

跨域问题

Nginx跨域实现

首先大家要搞清楚什么是跨域,为什么会有跨域情况的出现。哪些情况属于跨域?

跨域:由于浏览器的同源策略,即属于不同域的页面之间不能相互访问各自的页面内容 :同源策略,单说来就是同协议,同域名,同端口

跨域场景

出于安全考虑(比如csrf攻击),浏览器一般会禁止进行跨域访问,但是因为有时有相应需求,需要允许跨域访问,这时,我们就需要将跨域访问限制打开。   启动一个web服务,端口是8081

然后再开启一个web服务/前端服务都可以。端口是8082,然后再8082的服务中通过ajax来访问8081的服务,这就不满足同源策略,就会出现跨域问题

1、前后端结合(JsonP)

虽然jsonp也可以实现跨域,但是因为jsonp不支持post请求,应用场景受到很大限制,所以这里不对jsonp作介绍。

2、纯后端方式一(CORS方式)

CORS 是w3c标准的方式,通过在web服务器端设置:响应头Access-Cntrol-Alow-Origin 来指定哪些域可以访问本域的数据,ie8&9(XDomainRequest),10+,chrom4,firefox3.5,safair4,opera12支持这种方式。

服务器代理,同源策略只存在浏览器端,通过服务器转发请求可以达到跨域请求的目的,劣势:增加服务器的负担,且访问速度慢。

3.纯后端方式二(Nginx代理方式)【建议这种方式】

首先配置Nginx的反向代理方式

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zwgWmQrV-1681383922628)(https://raw.githubusercontent.com/viacheung/img/main/image/zp1p1bdlat.png)]

8082的服务访问Nginx,出现了跨域问题

Nginx配置跨域解决

location / {  
    add_header Access-Control-Allow-Origin *;
    add_header Access-Control-Allow-Methods 'GET, POST, OPTIONS';
    add_header Access-Control-Allow-Headers 'DNT,X-Mx-ReqToken,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization';

    if ($request_method = 'OPTIONS') {
        return 204;
}
proxy_pass http://192.168.12.1:8081;
} 

解决了跨域问题

Access-Control-Allow-Origin

服务器默认是不被允许跨域的。给Nginx服务器配置Access-Control-Allow-Origin *后,表示服务器可以接受所有的请求源(Origin),即接受所有跨域的请求。

Access-Control-Allow-Headers

是为了防止出现以下错误:

Request header field Content-Type is not allowed by Access-Control-Allow-Headers in preflight response.

这个错误表示当前请求Content-Type的值不被支持。其实是我们发起了"application/json"的类型请求导致的。这里涉及到一个概念:预检请求(preflight request),请看下面"预检请求"的介绍。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uNStXyYv-1681383922629)(https://raw.githubusercontent.com/viacheung/img/main/image/image-20230410163310797.png)]

Access-Control-Allow-Methods

是为了防止出现以下错误:

Content-Type is not allowed by Access-Control-Allow-Headers in preflight response.

给OPTIONS 添加 204的返回

是为了处理在发送POST请求时Nginx依然拒绝访问的错误,发送"预检请求"时,需要用到方法 OPTIONS ,所以服务器需要允许该方法。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-G5bLDSTj-1681383922631)(https://raw.githubusercontent.com/viacheung/img/main/image/image-20230410163455042.png)]

预检请求(preflight request)

跨域资源共享(CORS)标准新增了一组 HTTP 首部字段,允许服务器声明哪些源站有权限访问哪些资源。另外,规范要求,对那些可能对服务器数据产生副作用的HTTP 请求方法(特别是 GET 以外的 HTTP 请求,或者搭配某些 MIME 类型的 POST 请求),浏览器必须首先使用 OPTIONS 方法发起一个预检请求(preflight request),从而获知服务端是否允许该跨域请求。服务器确认允许之后,才发起实际的 HTTP 请求。在预检请求的返回中,服务器端也可以通知客户端,是否需要携带身份凭证(包括 Cookies 和 HTTP 认证相关数据)。   其实Content-Type字段的类型为application/json的请求就是上面所说的搭配某些 MIME 类型的 POST 请求,CORS规定,Content-Type不属于以下MIME类型的,都属于预检请求   所以 application/json的请求 会在正式通信之前,增加一次"预检"请求,这次"预检"请求会带上头部信息 Access-Control-Request-Headers: Content-Type:

OPTIONS /api/test HTTP/1.1
Origin: http://foo.example
Access-Control-Request-Method: POST
Access-Control-Request-Headers: Content-Type
... 省略了一些

复制

服务器回应时,返回的头部信息如果不包含Access-Control-Allow-Headers: Content-Type则表示不接受非默认的的Content-Type。即出现以下错误:

项目挑战?

SpringSecurity

压测

jmeter

https://blog.csdn.net/m0_37679452/article/details/103895809

Java 基础

面向对象和⾯向过程的区别

面向过程

优点:性能高,适合单片机嵌入式开发 缺点:没有⾯向对象易维护、易复⽤、易扩展。

⾯向对象 :

优点:⾯向对象易维护、易复⽤、易扩展 缺点:性能低

Java 语⾔有哪些特点?

  1. 简单易学;
  2. ⾯向对象(封装,继承,多态);
  3. 平台⽆关性( Java 虚拟机实现平台⽆关性) 一次编译 处处运行
  4. ⽀持多线程( C++ 语⾔没有内置的多线程机制,因此必须调⽤操作系统的多线程功能来进
    ⾏多线程程序设计,⽽ Java 语⾔却提供了多线程⽀持);
  5. ⽀持⽹络编程、⽅便 Java 语⾔诞⽣本身就是为简化⽹络编程设计的
  6. 半编译半解释;

JDK 和 JRE

jdk包括jre

JDK用来创建和编译程序。
JRE 是 Java 运⾏时环境。包括(JVM), Java 类库等,但是,不能⽤于创建新程序。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kgmuYnb4-1681383922631)(https://raw.githubusercontent.com/viacheung/img/main/image/image-20230318182251008.png)]

Oracle JDK 和 OpenJDK 的对⽐

OpenJDK 是⼀个参考模型、完全开源,⽽ Oracle JDK 是 OpenJDK 的⼀个实现,并不是完全开源的;

Oracle JDK 更稳定 性能更好

Java 和 C++的区别?

都是⾯向对象的语⾔,都⽀持封装、继承和多态
Java 不提供指针来直接访问内存,程序内存更加安全
Java 的类是单继承(接口多继承), C++ ⽀持多重继承;
Java 有⾃动内存管理机制,不需要程序员⼿动释放⽆⽤内存
在 C 语⾔中,字符串或字符数组最后都会有⼀个==额外的字符‘\0’==来表示结束。但是, Java 语
⾔中没有结束符这⼀概念。 这是⼀个值得深度思考的问题,具体原因推荐看这篇⽂章:
https://blog.csdn.net/sszgg2006/article/details/49148189

java皆对象 可以通过length()知道长度 没必要加一个额外字符

采用字节码的好处是什么?

解决了传统解释型语言执行效率低的问题,同时又保留了解释型语言可移植的特点

字符型常量和字符串常量的区别?

  1. 形式上: 字符常量是单引号引起的⼀个字符; 字符串常量是双引号引起的若⼲个字符
  2. 含义上: 字符常量相当于⼀个整型值( ASCII 值),可以参加表达式运算; 字符串常量代表⼀个
    址值(该字符串在内存中存放位置)
  3. 占内存⼤⼩ 字符常量只占 2 个字节; 字符串常量占若⼲个字节 (注意: char 在 Java 中占两
    个字节)

构造器 Constructor 是否可被 override?

Constructor 不能被 override(重写) ,但是可以 overload(重载) ,所以你可以看到⼀个类中有多个构造函数的情况

重载和重写的区别

都是实现多态方式,重载时编译时多态,重写时运行时多态

重载就是同样的⼀个⽅法能够根据输⼊数据的不同,做出不同的处理 方法名一样 参数列表不同 返回值也可以不同 例如构造器重载

重载的方法能否根据返回值类型进行区分?不可以

重写就是当⼦类继承⾃⽗类的相同⽅法,输⼊数据⼀样,但要做出有别于⽗类的响应时,你就要覆盖⽗类⽅法 方法名一样 参数列表一样 返回类型一样 代码具体实现不一样

Java ⾯向对象编程三⼤特性: 封装 继承 多态

封装:对外界提供**方法属性 ** 隐藏不可信信息 数据方法让可信类对象操作

继承:复用代码 父类私有属性方法 子类只读不写

多态:表现不同行为,方法调用到底哪个类实现需要在运行时确定,实现管右边,运行管左边,向上向下转型

如何实现多态?

编译时多态:运行哪个方法在编译时侯确定了

运行时多态:运行的时候才能确定

多态三个条件:继承、重写和向上转型(需要将子类的引用赋给父类对象,这样该引用才既能可以调用父类的方法,又能调用子类的方法。)。

String StringBuffer 和StringBuilder 的区别是什么?String 为什么是不可变的?

1、前两者可变 string不可变(final)(适用于操作少量数据)

2、string线程安全 stringbuffer线程安全(synchronized) 那个不安全

3、StringBuilder性能比stringbuffer多10-15%

String为什么要设计成不可变的?

1.便于实现字符串池(String pool)

堆中开辟一块存储空间String pool,当初始化一个String变量时,如果该字符串已经存在了,就不会去创建一个新的字符串变量,而是会返回已经存在了的字符串的引用。

2.多线程安全

3.避免安全 网络连接地址URL,文件路径path,反射机制所需要的String参数保证连接安全性

4.保证了hashcode的唯一性,因此创建对象即可缓存,Map将其当做key,速度块

String相关

字符串常量 vs 字符常量

形式:字符数量 单双引号

含义上: 字符常量:整型值( ASCII 值) 字符串常量:地址值 对象;

占内存大小:字符常量2个字节;字符串常量占若干个字节

什么是字符串常量池?

jvm为了提升性能、减少内存开销,开辟字符串常量池 ;当使用字符串 常量池有 直接拿 ; 不存在 初始化 放到池子里面

jdk7 字符串常量池在永久代 常量池 存的是对象

jdk8 字符串常量池在堆 常量池 存的堆的引用

String str="aaa"与 String str=new String(“aaa”)一样吗?new String(“aaa”);创建了几个字符串对象?

  • String a = “aaa” ;,常量池中查找”aaa”字符串,若没有,会将”aaa”字符串放进常量池,再将其地址赋给a;若有,将找到的”aaa”字符串的地址赋给a。
  • 使用String b = new String(“aaa”);`,程序会在堆内存中开辟一片新空间存放新对象,同时会将”aaa”字符串放入常量池,相当于创建了两个对象,无论常量池中有没有”aaa”字符串,程序都会在堆内存中开辟一片新空间存放新对象。

intern()函数

在JDK1.6中,intern的处理是 先判断字符串常量是否在字符串常量池中,如果存在直接返回该常量,如果没有找到,则将该字符串常量加入到字符串常量区,也就是在字符串常量区建立该常量(复制对象);

在JDK1.7中,intern的处理是 先判断字符串常量是否在字符串常量池中,如果存在直接返回该常量,如果没有找到,说明该字符串常量在堆中,则处理是把堆区该对象的引用加入到字符串常量池中,以后别人拿到的是该字符串常量的引用(复制对象引用地址),实际存在堆中

public void test(){
        String s = new String("2");
        s.intern();
        String s2 = "2";
        System.out.println(s == s2);


        String s3 = new String("3") + new String("3");
        s3.intern();
        String s4 = "33";
        System.out.println(s3 == s4);
    }
    
jdk6
false(没啥说的 一个堆对象  一个常量池对象)
false(没啥说的 一个堆对象  一个常量池对象)

jdk7
false(一个在堆中的StringObject对象,一个是在堆中的“2”对象 常量池存了一下”2“对象的引用地址,Intern之后,一样 还是存的"2"对象引用 不一样)
true(s3指向"33"  s3.intern 将s3对应的StringObject对象的地址保存到常量池中,返回StringObject对象的地址。String s4 = "33",去常量池找,发现有,返回2,也就是StringObject对象的引用地址,因此一样)

String特性

  • 不变性:当一个对象需要被多线程共享并频繁访问时,可以保证数据的一致性;
  • 常量池优化:String 对象创建之后,会在字符串常量池中进行缓存,如果下次创建同样的对象时,会直接返回缓存的引用;
  • final:使用 final 来定义 String 类,表示 String 类不能被继承,提高了系统的安全性。

在使用 HashMap 的时候,用 String 做 key 有什么好处?

HashMap 内部实现是通过 key 的 hashcode 来确定 value 的存储位置,因为字符串是不可变的,所以当创建字符串时,它的 hashcode 被缓存下来,不需要再次计算,所以相比于其他对象更快(存取快)。

八个基本数据类型

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BYz2c1uQ-1681383922632)(https://raw.githubusercontent.com/viacheung/img/main/image/image-20230318182611273.png)]

short int long float double char byte boolean

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mWqOQarw-1681383922633)(https://raw.githubusercontent.com/viacheung/img/main/image/image-20230318182624802.png)]

long后面要加L

boolean类型数据在经过编译后在JVM中会通过int类型来表示

switch

byte、short、char、int enum String

访问修饰符public、private、protected、以及不写(默认)时的区别?

  • default (即默认,什么也不写): 在同一包内,使用对象:类、接口、变量、方法。
  • private : 在同一类内。使用对象:变量、方法。 注意:不能修饰类(外部类)
  • public : 对所有类可见。使用对象:类、接口、变量、方法
  • protected : 对同一包内的类和所有子类(不同包也可)可见。使用对象:变量、方法。 注意:不能修饰类(外部类)

关键字

final、finally、finalize的区别?

final: 变量:引用不可变 必须初始化 方法:不可重写 类不可继承

finally:异常执行,最后一定执行

finalize 方法回收之前对对象的一些操作

static作用?

1、属于类 不创建对象就能用这块资源 2、用类直接调用

是否可以在static环境中访问非static变量?

不行,那岂不是不用new 对象了?

java静态变量、代码块、和静态方法的执行顺序是什么?

代码块执行顺序静态代码块——> 构造代码块 ——> 构造函数——> 普通代码块

继承中:父类静态–子类静态–父类构造代码块–父类构造器—子类构造代码块—子类构造器

包装类型

包装类型是什么?基本类型和包装类型有什么区别?

1、包装类型可以为 null,而基本类型不可以 pojo里面如果查询结果null 如果基本类型为null 直接空指针

2、包装类型可用于泛型,而基本类型不可以

3、基本类型比包装类型更高效 一个存数值 一个存堆的引用 总体包装占空间多

拆箱装箱

装箱就是 基本数据类型转换为(对象)包装器类型;拆箱 包装器类型(对象)转换为基本数据类型。

装箱过程:valueOf方法实现的,而拆箱过程: xxxValue方法

public` `class` `Main {
  ``public` `static` `void` `main(String[] args) {
    
    ``Integer i1 = ``100``;
    ``Integer i2 = ``100``;
    ``Integer i3 = ``200``;
    ``Integer i4 = ``200``;
    
    ``System.out.println(i1==i2);
    ``System.out.println(i3==i4);
  ``}
}
输出:
true
false

通过valueOf方法创建Integer对象的时候,如果数值在[-128,127]之间,便返回指向IntegerCache.cache中已经存在的对象的引用;否则创建一个新的Integer对象。上面的代码中i1和i2的数值为100,因此会直接从cache中取已经存在的对象,所以i1和i2指向的是同一个对象,而i3和i4则是分别指向不同的对象。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-a9sY6iCB-1681383922635)(https://raw.githubusercontent.com/viacheung/img/main/image/image-20230310154209872.png)]

谈谈Integer i = new Integer(xxx)和Integer i =xxx;这两种方式的区别。

当然,这个题目属于比较宽泛类型的。但是要点一定要答上,我总结一下主要有以下这两点区别:

1)第一种方式不会触发自动装箱的过程;而第二种方式会触发;

2)在执行效率和资源占用上的区别。第二种方式的执行效率和资源占用在一般性情况下要优于第一种情况(注意这并不是绝对的)。

当 "=="运算符的两个操作数都是 包装器类型的引用,则是比较指向的是否是同一个对象,而如果其中有一个操作数是表达式(即包含算术运算)则比较的是数值(即会触发自动拆箱的过程)。另外,对于包装器类型,equals方法并不会进行类型转换。

int 和 Integer 有什么区别?

  • Integer是int的包装类;int是基本数据类型;
  • Integer变量必须实例化后才能使用;int变量不需要;
  • Integer实际是对象的引用,指向此new的Integer对象;int是直接存储数据值 ;
  • Integer的默认值是null;int的默认值是0。
 Integer b = new Integer(10000);   (堆)

 Integer c=10000;    (常量池)

反射

反射是什么

运行状态时对于一个类可以知道他的所有属性方法,对于任何一个对象,可调用任何方法属性,这种动态获取对象方法并调用的功能叫做反射

反射有缺点

优点:能够在运行时动态获取类的实例,灵活性高;例如加载MySQL的驱动类。

缺点:性能较低,需要解析字节码,将内存中的对象进行解析。其解决方案是:通过setAccessible(true)关闭JDK的安全检查来提升反射速度;

多次创建一个类的实例时,有缓存会快很多;ReflflectASM工具类,通过字节码生成的方式加快反射速度。

如何获取反射的Class对象

1、Class.forName(“类的路径”);

2、类名.class

3、对象名.getClass()

Java反射API有几类

  • Class 类:类的属性,方法等信息。
  • Field 类:用来获取和设置类之中的属性值。
  • Method 类:获取类中的方法信息或者执行方法。
  • Constructor 类:表示类的构造方法。

反射使用的步骤?

获取想要操作的类的Class对象,这是反射的核心,通过Class对象我们可以任意调用类的方法。

调用 Class 类中的方法,既就是反射的使用阶段。

使用反射 API 来操作这些信息。

例如:获取对象实例–获取构造器对象–构造器newInstance获取反射对象–获取方法的Method对象–利用Invoke调用方法(方法.invoke(对象,值))

为什么引入反射?反射应用

原因:

  • 反射让开发人员可以通过外部类的全路径名创建对象,并使用这些类,实现一些扩展的功能。
  • 反射让开发人员可以枚举出类的全部成员,包括构造函数、属性、方法。以帮助开发者写出正确的代码。
  • 测试时可以利用反射 API 访问类的私有成员,以保证测试代码覆盖率。

也就是说,Oracle 希望开发者将反射作为一个工具,用来帮助程序员实现本不可能实现的功能。

应用:

第一种:JDBC 的数据库的连接

  1. 通过Class.forName()加载数据库的驱动程序((通过反射加载,前提是引入相关了Jar包)) ;
  2. 通过 DriverManager 类进行数据库的连接(输入数据库的连接地址、用户名、密码)
  3. 通过Connection 接口接收连接。

第二种:Spring 框架的使用,最经典的就是xml的配置模式

Spring 通过 XML 配置模式装载 Bean 的过程:

  1. 将程序内所有 XML 或 Properties 配置文件加载入内存中;
  2. Java类里面解析xml或properties里面的内容,得到对应实体类的字节码字符串以及相关的属性信息;
  3. 使用反射机制,根据这个字符串获得某个类的Class实例;
  4. 动态配置实例的属性。

我的理解:

  1. 读取xml properties文件
  2. 遍历Bean 获取其id(key)和class名称(反射拿对象)
  3. 对于每个Bean,遍历Property属性:name value,再用反射把这些值set到对象里面,最后把对象放到beanMap里面,key为id,value为对象

Spring这样做的好处是:

  • 不用每一次都要在代码里面去new或者做其他的事情;
  • 以后要改的话直接改配置文件,代码维护起来就很方便了;
  • 有时为了适应某些需求,Java类里面不一定能直接调用另外的方法,可以通过反射机制来实现。

反射机制的原理是什么?

  1. 反射获取类实例 Class.forName(),并没有将实现留给了java,而是交给了jvm去加载!主要是先获取 ClassLoader, 然后调用 native 方法,获取信息,加载类则是回调 java.lang.ClassLoader。最后,jvm又会回调 ClassLoader 进类加载!
  2. newInstance() 主要做了三件事:
    • 权限检测,如果不通过直接抛出异常;
    • 查找无参构造器,并将其缓存起来;
    • 调用具体方法的无参构造方法,生成实例并返回。
  3. 获取Method对象

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wLexOMJs-1681383922637)(https://raw.githubusercontent.com/viacheung/img/main/image/image-20210226195426092.png)]

上面的Class对象是在加载类时由JVM构造的,JVM为每个类管理一个独一无二的Class对象,这份Class对象里维护着该类的所有Method,Field,Constructor的cache,这份cache也可以被称作根对象

每次getMethod获取到的Method对象都持有对根对象的引用,因为一些重量级的Method的成员变量(主要是MethodAccessor),我们不希望每次创建Method对象都要重新初始化,于是所有代表同一个方法的Method对象都共享着根对象的MethodAccessor,每一次创建都会调用根对象的copy方法复制一份:

4.调用invoke()方法。调用invoke方法的流程如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pf8P1ECl-1681383922638)(http://blog-img.coolsen.cn/img/image-20210226195531619.png)]

调用Method.invoke之后,会直接去调MethodAccessor.invoke。MethodAccessor就是上面提到的所有同名method共享的一个实例,由ReflectionFactory创建。

创建机制采用了一种名为inflation的方式(JDK1.4之后):如果该方法的累计调用次数<=15,会创建出NativeMethodAccessorImpl,它的实现就是直接调用native方法实现反射;如果该方法的累计调用次数>15,会由java代码创建出字节码组装而成的MethodAccessorImpl。(是否采用inflation和15这个数字都可以在jvm参数中调整) 以调用MyClass.myMethod(String s)为例,生成出的MethodAccessorImpl字节码翻译成Java代码大致如下:

泛型

Java中的泛型是什么 ?

泛型是 JDK1.5 的一个新特性,**泛型就是将类型参数化,其在编译时才确定具体的参数。**这种参数类型可以用在类、接口和方法的创建中,分别称为泛型类、泛型接口、泛型方法。

使用泛型的好处是什么?

远在 JDK 1.4 版本的时候,那时候是没有泛型的概念的,如果使用 Object 来实现通用、不同类型的处理,有这么两个缺点:

  1. 每次使用时都需要强制转换成想要的类型
  2. 在编译时编译器并不知道类型转换是否正常,运行时才知道,不安全。

如这个例子:

List list = new ArrayList();
list.add("www.cnblogs.com");
list.add(23);
String name = (String)list.get(0);
String number = (String)list.get(1);    //ClassCastException

上面的代码在运行时会发生强制类型转换异常。这是因为我们在存入的时候,第二个是一个 Integer 类型,但是取出来的时候却将其强制转换为 String 类型了。Sun 公司为了使 Java 语言更加安全,减少运行时异常的发生。于是在 JDK 1.5 之后推出了泛型的概念。

根据《Java 编程思想》中的描述,泛型出现的动机在于:有许多原因促成了泛型的出现,而最引人注意的一个原因,就是为了创建容器类

使用泛型的好处有以下几点

  1. 类型安全
    • 泛型的主要目标是提高 Java 程序的类型安全
    • 编译时期就可以检查出因 Java 类型不正确导致的 ClassCastException 异常
    • 符合越早出错代价越小原则
  2. 消除强制类型转换
    • 泛型的一个附带好处是,使用时直接得到目标类型,消除许多强制类型转换
    • 所得即所需,这使得代码更加可读,并且减少了出错机会
  3. 潜在的性能收益
    • 由于泛型的实现方式,支持泛型(几乎)不需要 JVM 或类文件更改
    • 所有工作都在编译器中完成
    • 编译器生成的代码跟不使用泛型(和强制类型转换)时所写的代码几乎一致,只是更能确保类型安全而已

Java泛型的原理是什么 ? 什么是类型擦除 ?

泛型是一种语法糖,泛型这种语法糖的基本原理是类型擦除。Java中的泛型基本上都是在编译器这个层次来实现的,也就是说:**泛型只存在于编译阶段,而不存在于运行阶段。**在编译后的 class 文件中,是没有泛型这个概念的。

类型擦除:使用泛型的时候加上的类型参数,编译器在编译的时候去掉类型参数。

大部分情况下,泛型类型都会以 Object 进行替换,而有一种情况则不是。那就是使用到了extends和super语法的有界类型,如:

public class Caculate<T extends String> {
    private T num;
}

这种情况的泛型类型,num 会被替换为 String 而不再是 Object。这是一个类型限定的语法,它限定 T 是 String 或者 String 的子类,也就是你构建 Caculate 实例的时候只能限定 T 为 String 或者 String 的子类,所以无论你限定 T 为什么类型,String 都是父类,不会出现类型不匹配的问题,于是可以使用 String 进行类型擦除。

实际上编译器会正常的将使用泛型的地方编译并进行类型擦除,然后返回实例。但是除此之外的是,如果构建泛型实例时使用了泛型语法,那么编译器将标记该实例并关注该实例后续所有方法的调用,每次调用前都进行安全检查,非指定类型的方法都不能调用成功。

实际上编译器不仅关注一个泛型方法的调用,它还会为某些返回值为限定的泛型类型的方法进行强制类型转换,由于类型擦除,返回值为泛型类型的方法都会擦除成 Object 类型,当这些方法被调用后,编译器会额外插入一行 checkcast 指令用于强制类型转换。这一个过程就叫做『泛型翻译』。

什么是泛型中的限定通配符和非限定通配符 ?

限定通配符对类型进行了限制。有两种限定通配符,一种是它通过确保类型必须是T的子类来设定类型的上界,另一种是==它通过确保类型必须是T的父类来设定类型的下界。==泛型类型必须用限定内的类型来进行初始化,否则会导致编译错误。

非限定通配符 ,可以用任意类型来替代。如List 的意思是这个集合是一个可以持有任意类型的集合,它可以是List,也可以是List,或者List等等。

List和List 之间有什么区别 ?

这两个List的声明都是限定通配符的例子,List可以接受任何继承自T的类型的List,而List可以接受任何T的父类构成的List。例如List可以接受List或List。

可以把List传递给一个接受List参数的方法吗?

不可以。真这样做的话会导致编译错误。因为List可以存储任何类型的对象包括String, Integer等等,而这样的话List却只能用来存储String。

List<Object> objectList;
List<String> stringList;
objectList = stringList;  //compilation error incompatible types

Array中可以用泛型吗?

不可以。这也是为什么 Joshua Bloch 在 《Effective Java》一书中建议使用 List 来代替 Array,因为 List 可以提供编译期的类型安全保证,而 Array 却不能。

判断ArrayListArrayList是否相等?

ArrayList<String> a = new ArrayList<String>();
ArrayList<Integer> b = new ArrayList<Integer>();
Class c1 = a.getClass();
Class c2 = b.getClass();
System.out.println(c1 == c2); 

输出的结果是 true。因为无论对于 ArrayList还是 ArrayList,它们的 Class 类型都是一致的,都是 ArrayList.class。

那它们声明时指定的 String 和 Integer 到底体现在哪里呢?

**答案是体现在类编译的时候。**当 JVM 进行类编译时,会进行泛型检查,如果一个集合被声明为 String 类型,那么它往该集合存取数据的时候就会对数据进行判断,从而避免存入或取出错误的数据。

序列化

Java序列化与反序列化是什么?

Java序列化是指把Java对象转换为字节序列的过程,而Java反序列化是指把字节序列恢复为Java对象的过程:

  • 序列化:序列化是把对象转换成有序字节流,以便在网络上传输或者保存在本地文件中。核心作用是对象状态的保存与重建。我们都知道,Java对象是保存在JVM的堆内存中的,也就是说,如果JVM堆不存在了,那么对象也就跟着消失了。

    而序列化提供了一种方案,可以让你在即使JVM停机的情况下也能把对象保存下来的方案。就像我们平时用的U盘一样。把Java对象序列化成可存储或传输的形式(如二进制流),比如保存在文件中。这样,当再次需要这个对象的时候,从文件中读取出二进制流,再从二进制流中反序列化出对象。

  • **反序列化:**客户端从文件中或网络上获得序列化后的对象字节流,根据字节流中所保存的对象状态及描述信息,通过反序列化重建对象。

为什么需要序列化与反序列化?

简要描述:对内存中的对象进行持久化或网络传输, 这个时候都需要序列化和反序列化

深入描述:

  1. (分布式调用远程主机)对象序列化可以实现分布式对象。

主要应用例如:RMI(即远程调用Remote Method Invocation)要利用对象序列化运行远程主机上的服务,就像在本地机上运行对象时一样。

  1. (递归存对象序列)java对象序列化不仅保留一个对象的数据,而且递归保存对象引用的每个对象的数据。

可以将整个对象层次写入字节流中,可以保存在文件中或在网络连接上传递。利用对象序列化可以进行对象的"深复制",即复制对象本身及引用的对象本身。序列化一个对象可能得到整个对象序列。

  1. (固化)序列化可以将内存中的类写入文件或数据库中。

比如:将某个类序列化后存为文件,下次读取时只需将文件中的数据反序列化就可以将原先的类还原到内存中。也可以将类序列化为流数据进行传输。

总的来说就是将一个已经实例化的类转成文件存储,下次需要实例化的时候只要反序列化即可将类实例化到内存中并保留序列化时类中的所有变量和状态。

  1. (统一存储)对象、文件、数据,有许多不同的格式,很难统一传输和保存。

序列化以后就都是字节流了,无论原来是什么东西,都能变成一样的东西,就可以进行通用的格式传输或保存,传输结束以后,要再次使用,就进行反序列化还原,这样对象还是对象,文件还是文件。

序列化实现的方式有哪些?

实现Serializable接口或者Externalizable接口。

Serializable接口

类通过实现 java.io.Serializable 接口以启用其序列化功能。可序列化类的所有子类型本身都是可序列化的。序列化接口没有方法或字段,仅用于标识可序列化的语义。

Externalizable接口

Externalizable继承自Serializable,该接口中定义了两个抽象方法:writeExternal()readExternal()

当使用Externalizable接口来进行序列化与反序列化的时候需要开发人员重写writeExternal()readExternal()方法。否则所有变量的值都会变成默认值。

两种序列化的对比
实现Serializable接口 实现Externalizable接口
系统自动存储必要的信息 程序员决定存储哪些信息
Java内建支持,易于实现,只需要实现该接口即可,无需任何代码支持 必须实现接口内的两个方法
性能略差 性能略好

什么是serialVersionUID?

serialVersionUID 用来表明类的不同版本间的兼容性

Java的序列化机制是通过在运行时判断类的serialVersionUID来验证版本一致性的。在进行反序列化时,JVM会把传来的字节流中的serialVersionUID与本地相应实体(类)的serialVersionUID进行比较,如果相同就认为是一致的,可以进行反序列化,否则就会出现序列化版本不一致的异常。

为什么还要显示指定serialVersionUID的值?

如果不显示指定serialVersionUID==, JVM在序列化时会根据属性自动生成一个serialVersionUID==, 然后与属性一起序列化, 再进行持久化或网络传输. 在反序列化时, JVM会再根据属性自动生成一个新版serialVersionUID, 然后将这个新版serialVersionUID与序列化时生成的旧版serialVersionUID进行比较, 如果相同则反序列化成功, 否则报错.

如果显示指定了, JVM在序列化和反序列化时仍然都会生成一个serialVersionUID, 但值为我们显示指定的值, 这样在反序列化时新旧版本的serialVersionUID就一致了.

在实际开发中, 不显示指定serialVersionUID的情况会导致什么问题? 如果我们的类写完后不再修改, 那当然不会有问题, 但这在实际开发中是不可能的, 我们的类会不断迭代, 一旦类被修改了, 那旧对象反序列化就会报错. 所以在实际开发中, 我们都会显示指定一个serialVersionUID, 值是多少无所谓, 只要不变就行。

serialVersionUID什么时候修改?

《阿里巴巴Java开发手册》中有以下规定:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mw4uJpZf-1681383922639)(https://raw.githubusercontent.com/viacheung/img/main/image/image-20210226222339606.png)]

Java 序列化中如果有些字段不想进行序列化,怎么办?

对于不想进行序列化的变量,使用 transient 关键字修饰。

transient 关键字的作用是控制变量的序列化,在变量声明前加上该关键字,可以阻止该变量被序列化到文件中,在被反序列化后,transient 变量的值被设为初始值,如 int 型的是 0,对象型的是 null。transient 只能修饰变量,不能修饰类和方法。

静态变量会被序列化吗?

不会。因为序列化是针对对象而言的, 而静态变量优先于对象存在, 随着类的加载而加载, 所以不会被序列化.

看到这个结论, 是不是有人会问, serialVersionUID也被static修饰, 为什么serialVersionUID会被序列化? 其实serialVersionUID属性并没有被序列化, JVM在序列化对象时会自动生成一个serialVersionUID, 然后将我们显示指定的serialVersionUID属性值赋给自动生成的serialVersionUID。

异常

Error 和 Exception 区别是什么?

Java 中,所有的异常都有一个共同的祖先 java.lang 包中的 Throwable 类。Throwable 类有两个重要的子类 Exception(异常)和 Error(错误)。

ExceptionError 二者都是 Java 异常处理的重要子类,各自都包含大量子类。

  • Exception :程序本身可以处理的异常,可以通过 catch 来进行捕获,通常遇到这种错误,应对其进行处理,使应用程序可以继续正常运行。Exception 又可以分为运行时异常(RuntimeException, 又叫非受检查异常)和非运行时异常(又叫受检查异常) 。
  • ErrorError 属于程序无法处理的错误 ,我们没办法通过 catch 来进行捕获 。例如,系统崩溃,内存不足,堆栈溢出等,编译器不会对这类错误进行检测,一旦这类错误发生,通常应用程序会被终止,仅靠应用程序本身无法恢复。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZnwMY2cQ-1681383922639)(https://raw.githubusercontent.com/viacheung/img/main/image/image-20210227103256234.png)]

非受检查异常(运行时异常)和受检查异常(一般异常)区别是什么?

非受检查异常:包括 RuntimeException 类及其子类,表示 JVM 在运行期间可能出现的异常。 Java 编译器不会检查运行时异常。例如:NullPointException(空指针)NumberFormatException(字符串转换为数字)IndexOutOfBoundsException(数组越界)ClassCastException(类转换异常)ArrayStoreException(数据存储异常,操作数组时类型不一致)等。

受检查异常:是Exception 中除 RuntimeException 及其子类之外的异常。 Java 编译器会检查受检查异常。常见的受检查异常有: IO 相关的异常、ClassNotFoundExceptionSQLException等。

非受检查异常和受检查异常之间的区别:是否强制要求调用者必须处理此异常,如果强制要求调用者必须进行处理,那么就使用受检查异常,否则就选择非受检查异常。

throw 和 throws 的区别是什么?

Java 中的异常处理除了包括捕获异常和处理异常之外,还包括声明异常和拋出异常,可以通过 throws 关键字在方法上声明该方法要拋出的异常,或者在方法内部通过 throw 拋出异常对象。(方法上+方法内部)

throws 关键字和 throw 关键字在使用上的几点区别如下:

  • throw 关键字用在方法内部,只能用于抛出一种异常,用来抛出方法或代码块中的异常,受查异常和非受查异常都可以被抛出。
  • throws 关键字用在方法声明上,可以抛出多个异常,用来标识该方法可能抛出的异常列表。一个方法用 throws 标识了可能抛出的异常列表,调用该方法的方法中必须包含可处理异常的代码,否则也要在方法签名中用 throws 关键字声明相应的异常。

NoClassDefFoundError 和 ClassNotFoundException 区别?

NoClassDefFoundError 是一个 Error 类型的异常,是由 JVM 引起的,不应该尝试捕获这个异常。引起该异常的原因是 JVM 或 ClassLoader 尝试加载某类时在内存中找不到该类的定义,该动作发生在运行期间,即编译时该类存在,但是在运行时却找不到了,可能是编译后被删除了等原因导致。

ClassNotFoundException 是一个受检查异常,需要显式地使用 try-catch 对其进行捕获和处理,或在方法签名中用 throws 关键字进行声明。当使用 Class.forName, ClassLoader.loadClass 或 ClassLoader.findSystemClass 动态加载类到内存的时候,通过传入的类路径参数没有找到该类,就会抛出该异常;另一种抛出该异常的可能原因是某个类已经由一个类加载器加载至内存中,另一个加载器又尝试去加载它。

Java常见异常有哪些?

  • java.lang.IllegalAccessError:违法访问错误。当一个应用试图访问、修改某个类的域(Field)或者调用其方法,但是又违反域或方法的可见性声明,则抛出该异常。

  • java.lang.InstantiationError:实例化错误。当一个应用试图通过Java的new操作符构造一个抽象类或者接口时抛出该异常.

  • java.lang.OutOfMemoryError:内存不足错误。当可用内存不足以让Java虚拟机分配给一个对象时抛出该错误。

  • java.lang.StackOverflowError:堆栈溢出错误。当一个应用递归调用的层次太深而导致堆栈溢出或者陷入死循环时抛出该错误。


  • java.lang.ClassCastException:类造型异常。假设有类A和B(A不是B的父类或子类),O是A的实例,那么当强制将O构造为类B的实例时抛出该异常。该异常经常被称为强制类型转换异常。

  • java.lang.ClassNotFoundException:找不到类异常。当应用试图根据字符串形式的类名构造类,而在遍历CLASSPAH之后找不到对应名称的class文件时,抛出该异常。

  • java.lang.ArithmeticException:算术条件异常。譬如:整数除零等。

  • java.lang.ArrayIndexOutOfBoundsException:数组索引越界异常。当对数组的索引值为负数或大于等于数组大小时抛出。

  • java.lang.IndexOutOfBoundsException:索引越界异常。当访问某个序列的索引值小于0或大于等于序列大小时,抛出该异常。

  • java.lang.InstantiationException:实例化异常。当试图通过newInstance()方法创建某个类的实例,而该类是一个抽象类或接口时,抛出该异常。

  • java.lang.NoSuchFieldException:属性不存在异常。当访问某个类的不存在的属性时抛出该异常。

  • java.lang.NoSuchMethodException:方法不存在异常。当访问某个类的不存在的方法时抛出该异常。

  • java.lang.NullPointerException:空指针异常。当应用试图在要求使用对象的地方使用了null时,抛出该异常。譬如:调用null对象的实例方法、访问null对象的属性、计算null对象的长度、使用throw语句抛出null等等。

  • java.lang.NumberFormatException:数字格式异常。当试图将一个String转换为指定的数字类型,而该字符串确不满足数字类型要求的格式时,抛出该异常。

  • java.lang.StringIndexOutOfBoundsException:字符串索引越界异常。当使用索引值访问某个字符串中的字符,而该索引值小于0或大于等于序列大小时,抛出该异常。

try-catch-finally 中哪个部分可以省略?

catch 可以省略。更为严格的说法其实是:try只适合处理运行时异常,try+catch适合处理运行时异常+普通异常。也就是说,如果你只用try去处理普通异常却不加以catch处理,编译是通不过的,因为编译器硬性规定,普通异常如果选择捕获,则必须用catch显示声明以便进一步处理。而运行时异常在编译时没有如此规定,所以catch可以省略,你加上catch编译器也觉得无可厚非。(运行时异常不加catch 普通异常必须加)

理论上,编译器看任何代码都不顺眼,都觉得可能有潜在的问题,所以你即使对所有代码加上try,代码在运行期时也只不过是在正常运行的基础上加一层皮。但是你一旦对一段代码加上try,就等于显示地承诺编译器,对这段代码可能抛出的异常进行捕获而非向上抛出处理。如果是普通异常,编译器要求必须用catch捕获以便进一步处理;如果运行时异常,捕获然后丢弃并且+finally扫尾处理,或者加上catch捕获以便进一步处理。

至于加上finally,则是在不管有没捕获异常,都要进行的“扫尾”处理。

try-catch-finally 中,如果 catch 中 return 了,finally 还会执行吗?

会执行,在 return 前执行。catch有return的话,先finally再catch里面return 但如果finally里面有return catch不会执行

在 finally 中改变返回值的做法是不好的,因为如果存在 finally 代码块,try中的 return 语句不会立马返回调用者,而是记录下返回值待 finally 代码块执行完毕之后再向调用者返回其值,然后如果在 finally 中修改了返回值,就会返回修改后的值。显然,在 finally 中返回或者修改返回值会对程序造成很大的困扰,Java 中也可以通过提升编译器的语法检查级别来产生警告或错误。

//例1:
public static int getInt() {
    int a = 10;
    try {
        System.out.println(a / 0);
        a = 20;
    } catch (ArithmeticException e) {
        a = 30;
        return a;
        /*
         * return a 在程序执行到这一步的时候,这里不是return a 而是 return 30;这个返回路径就形成了
         * 但是呢,它发现后面还有finally,所以继续执行finally的内容,a=40
         * 再次回到以前的路径,继续走return 30,形成返回路径之后,这里的a就不是a变量了,而是常量30
         */
    } finally {
        a = 40;
    }
    return a;
}

//执行结果:30
//例2
public static int getInt() {
    int a = 10;
    try {
        System.out.println(a / 0);
        a = 20;
    } catch (ArithmeticException e) {
        a = 30;
        return a;
    } finally {
        a = 40;
        //如果这样,就又重新形成了一条返回路径,由于只能通过1个return返回,所以这里直接返回40
        return a; 
    }

}

// 执行结果:40

JVM 是如何处理异常的?

在一个方法中如果发生异常,这个方法会创建一个异常对象,并转交给 JVM,该异常对象包含异常名称,异常描述以及异常发生时应用程序的状态。创建异常对象并转交给 JVM 的过程称为抛出异常。可能有一系列的方法调用,最终才进入抛出异常的方法,这一系列方法调用的有序列表叫做调用栈

JVM 会顺着调用栈去查找看是否有可以处理异常的代码,如果有,则调用异常处理代码。当 JVM 发现可以处理异常的代码时,会把发生的异常传递给它。如果 JVM 没有找到可以处理该异常的代码块,JVM 就会将该异常转交给默认的异常处理器(默认处理器为 JVM 的一部分),默认异常处理器打印出异常信息并终止应用程序。 想要深入了解的小伙伴可以看这篇文章:https://www.cnblogs.com/qdhxhz/p/10765839.html

IO

Java的IO 流分为几种?

  • 按照流的方向:输入流(inputStream)和输出流(outputStream);
  • 按照实现功能分:节点流(可以从或向一个特定的地方读写数据,如 FileReader)和处理流(是对一个已存在的流的连接和封装,通过所封装的流的功能调用实现数据读写, BufferedReader);
  • 按照处理数据的单位: 字节流和字符流。分别由四个抽象类来表示(每种流包括输入和输出两种所以一共四个):InputStream,OutputStream,Reader,Writer。Java中其他多种多样变化的流均是由它们派生出来的。

字节流如何转为字符流?

(适配器)

字节输入流转字符输入流通过 InputStreamReader 实现,该类的构造函数可以传入 InputStream 对象。

字节输出流转字符输出流通过 OutputStreamWriter 实现,该类的构造函数可以传入 OutputStream 对象。

BIO、NIO、AIO的区别?

  • BIO:同步并阻塞,在服务器中实现的模式为一个连接一个线程。也就是说,客户端有连接请求的时候,服务器就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销,当然这也可以通过线程池机制改善。BIO一般适用于连接数目小且固定的架构,这种方式对于服务器资源要求比较高,而且并发局限于应用中,是JDK1.4之前的唯一选择,但好在程序直观简单,易理解。
  • NIO:同步并非阻塞,在服务器中实现的模式为一个请求一个线程,也就是说,客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到有连接IO请求时才会启动一个线程进行处理。NIO一般适用于连接数目多且连接比较短(轻操作)的架构,并发局限于应用中,编程比较复杂,从JDK1.4开始支持。
  • AIO:异步并非阻塞,在服务器中实现的模式为一个有效请求一个线程,也就是说,客户端的IO请求都是通过操作系统先完成之后,再通知服务器应用去启动线程进行处理。AIO一般适用于连接数目多且连接比较长(重操作)的架构,充分调用操作系统参与并发操作,编程比较复杂,从JDK1.7开始支持。

Java IO都有哪些设计模式

使用了适配器模式装饰器模式

适配器模式

Reader reader = new INputStreamReader(inputStream);

把一个类的接口变换成客户端所期待的另一种接口,从而使原本因接口不匹配而无法在一起工作的两个类能够在一起工作

  • 类适配器:Adapter类(适配器)继承Adaptee类(源角色)实现Target接口(目标角色)
  • 对象适配器:Adapter类(适配器)持有Adaptee类(源角色)对象实例,实现Target接口(目标角色) [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vpspfWbQ-1681383922641)(https://raw.githubusercontent.com/viacheung/img/main/image/image-20210227114919307.png)]

装饰器模式

new BufferedInputStream(new FileInputStream(inputStream));

一种动态地往一个类中添加新的行为的设计模式。就功能而言,装饰器模式相比生成子类更为灵活,这样可以给某个对象而不是整个类添加一些功能。

  • ConcreteComponent(具体对象)和Decorator(抽象装饰器)实现相同的Conponent(接口)并且Decorator(抽象装饰器)里面持有Conponent(接口)对象,可以传递请求。
  • ConcreteComponent(具体装饰器)覆盖Decorator(抽象装饰器)的方法并用super进行调用,传递请求。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dIEEy4mb-1681383922642)(https://raw.githubusercontent.com/viacheung/img/main/image/image-20210227115040999.png)]

在⼀个静态⽅法内调⽤⼀个⾮静态成员为什么是⾮法的?

否则不就类.任何变量了?

Java 中定义⼀个不做事且没有参数的构造⽅法的作⽤

⽗类中只定义了有参数的构造⽅法(一旦父类有了属于自己的非空参数构造方法后,系统将不会再赠送父类没有参数的构造方法),⽽在⼦类的构造⽅法中⼜没有⽤ super() 来调⽤⽗类中特定的构造⽅法,则编译时将发⽣错误,因为不写super的话 默认子类无参构造 所以父类要手写无参构造

接⼝和抽象类的区别是什么?

1 变量 接口只能有public static final修饰变量 抽象类无所谓

2 实现继承 接口多实现 类单继承

3 设计层面 一个模板设计 一个行为抽象(接口)

4 方法 抽象类可以提供成员方法的具体细节 接口只能public abstract方法

抽象类能用final修饰吗? 不能 我这样搞就是让别人继承的 你干啥?

Java创建对象方式

new 反射 clone 序列化

不可变对象

String Integer包装类 好处线程安全

创建一个包含可变对象的不可变对象? inal Person[] persons = new Persion[]{}

成员变量与局部变量的区别有哪些?

1、位置不同 成员是类的 局部变量是方法的

2、成员属于对象 在堆上 局部变量在方法上 栈帧里面

3、成员会自动以类型默认值赋初始值 (除开final) 局部变量必须显示赋值

若⼀个类没有声明构造⽅法,该程序能正确执⾏吗? (可以)为什么?(默认无参)

构造⽅法有哪些特性?

\1. 名字与类名相同。
\2. 没有返回值
\3. ⽣成类的对象时⾃动执⾏,⽆需调⽤。

对象的相等与指向他们的引⽤相等,两者有什么不同?

对象的相等,⽐的是内存中存放的内容是否相等。⽽引⽤相等,⽐较的是他们指向的内存地址是
否相等

== 与 equals(重要)

==: 基本数据类型—内容 引用数据类型-- 地址

equals: 都是内容

equal特点:自反 传递 一致 对称

hashcode?

它实际上是返回一个int整数。这个哈希码的作用是确定该对象在哈希表中的索引位置

hashCode 与 equals (重要)

你重写过 hashcode 和 equals 么,为什么重写 equals 时必须重写hashCode ⽅法?

答案:减少equals次数,hashcode就是根据对象地址返回一个整数然后再对数组取余的数,这个数可能有冲突,所以需要用下equals比较一下

是否真的相等,不等的话插入链表,所以重写hashCode ⽅法相当于一个屏障了 大大提高速度

为什么重写 equals 方法必须重写 hashcode 方法 ?

1、只重写equals的话,会造成俩对象hashcode的值不同,因为是先根据hashcode进行判断,那么本来相同对象(equals相同)是覆盖的,结果都插进去了

2、减少equals次数

为什么 Java 中只有值传递?

值传递就是把参数的值给你,调用函数时将实际参数复制一份传递到函数中,这样函数内部对参数内部进行修改不会影响到实际参数;

而引用传递就不一样了,它直接把参数的实际地址给调用函数了,函数内部可直接修改该地址内容,会影响到实际参数

为什么?

基本类型作为参数被传递时肯定是值传递;引用类型作为参数被传递时也是值传递,只不过“值”为对应的引用。

那我说下值传递的特征

⼀个⽅法不能修改⼀个基本数据类型的参数(即数值型或布尔型)。(只是copy不影响原值)
⼀个⽅法可以改变⼀个对象参数的状态。(引用可改变值)
⼀个⽅法不能让对象参数引⽤⼀个新的对象。 (引用一旦传递,不可交换)

关于 final 关键字的⼀些总结

变量(数值引用不能改)、⽅法(锁定 不能改含义 效率高)、类(不能继承 成员方法均final)。

Java 中的异常处理

Throwable:

编译时异常:找不到类 ioexception

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-chdfEJdU-1681383922643)(https://raw.githubusercontent.com/viacheung/img/main/image/image-20230310162625781.png)]

try捕获 catch处理 finally最终 (return之前)

如果finally和try都有return 以finally为主

在以下 3 种特殊情况下, finally 块不会被执⾏:
在 try 或 finally 块中⽤了 System.exit(int) 退出程序。但是,如果 System.exit(int) 在异常语句之后, finally 还是会被执⾏
线程死亡。
关闭 CPU。

Java 序列化中如果有些字段不想进⾏序列化,怎么办?

对于不想进⾏序列化的变量(只能变量),使⽤ transient(转瞬即逝) 关键字修饰。

获取⽤键盘输⼊常⽤的两种⽅

⽅法 1:通过 Scanner nextLine(); close
⽅法 2:通过 BufferedReader readline

Java 中 IO 流分为⼏种?

按照流的流向分,可以分为输⼊流和输出流;
按照操作单元划分,可以划分为字节流和字符流;
按照流的⻆⾊划分为节点流和处理流。

字节:input (outsput)stream

字符 reader writer

既然有了字节流,为什么还要有字符流?

字符可以用字节转换 但是比较耗时 不如直接来个字符流

场景:⾳频⽂件、图⽚等媒体⽂件⽤字节流⽐较好,如果涉及到字符的话使⽤字符流⽐较好。

BIO,NIO,AIO 有什么区别?

BIO (Blocking I/O): 同步阻塞 I/O 模式:

数据读取写入 必须阻塞在一个线程内等待完成 低并发 Socket 和 ServerSocket

NIO (Non-blocking/New I/O): NIO 是⼀种同步⾮阻塞的 I/O 模型

面向缓冲(buffer) 基于通道(channel)的I/O操作 高负载高并发 SocketChannel ServerSocketChannel

AIO 异步⾮阻塞的 IO 模型

不广泛 基于事件和回调

深拷⻉ vs 浅拷⻉

浅拷贝:正常 基本数据类型进⾏值传递,对引⽤数据类型进⾏引⽤传递般

深拷贝:String 对基本数据类型进⾏值传递,对引⽤数据类型,创建⼀个新的对象,并复制其内容,

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pvRn5QWQ-1681383922644)(https://raw.githubusercontent.com/viacheung/img/main/image/image-20230310163934375.png)]

Object方法

getclass equals hashcode notify wait finalize clone tostring

讲一讲快速失败(fail-fast)和安全失败(fail-safe)

快速失败(fail—fast)

  • 在用迭代器遍历一个集合对象时,如果遍历过程中对集合对象的内容进行了修改(增加、删除、修改),则会抛出Concurrent Modification Exception。
  • 原理:迭代器在遍历时使用一个 modCount 变量。集合在被遍历期间如果内容发生变化,就会改变modCount的值。每当迭代器使用hashNext()/next()遍历下一个元素之前,都会检测modCount变量是否为expectedmodCount值,是的话就返回遍历;否则抛出异常,终止遍历
  • 注意:这里异常的抛出条件是检测到 modCount!=expectedmodCount 这个条件。如果集合发生变化时修改modCount值刚好又设置为了expectedmodCount值,则异常不会抛出。因此,不能依赖于这个异常是否抛出而进行并发操作的编程,这个异常只建议用于检测并发修改的bug。
  • 场景:java.util包下的集合类都是快速失败的,不能在多线程下发生并发修改(迭代过程中被修改),比如HashMap、ArrayList 这些集合类。

安全失败(fail—safe)

  • 采用安全失败机制的集合容器,在遍历时不是直接在集合内容上访问的,而是先复制原有集合内容,在拷贝的集合上进行遍历
  • 原理:由于迭代时是对原集合的拷贝进行遍历,所以在遍历过程中对原集合所作的修改并不能被迭代器检测到,所以不会触发Concurrent Modification Exception。
  • 缺点:基于拷贝内容的优点是避免了Concurrent Modification Exception,但同样地,迭代器并不能访问到修改后的内容,即:迭代器遍历的是开始遍历那一刻拿到的集合拷贝,在遍历期间原集合发生的修改迭代器是不知道的。
  • 场景:java.util.concurrent包下的容器都是安全失败,可以在多线程下并发使用,并发修改,比如:ConcurrentHashMap。

转发 vs 重定向

  • [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-g7xFobqx-1681383922645)(https://raw.githubusercontent.com/viacheung/img/main/image/image-20230318004421380.png)]

Java集合

常见的集合有哪些?

Java集合类主要由两个根接口CollectionMap派生出来的,Collection派生出了三个子接口:List、Set、Queue(Java5新增的队列),因此Java集合大致也可分成List、Set、Queue、Map四种接口体系。

注意:Collection是一个接口,Collections是一个工具类,Map不是Collection的子接口

Java集合框架图如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WmHEtZJo-1681383922646)(https://raw.githubusercontent.com/viacheung/img/main/image/image-20210403163733569.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jJZNXGHu-1681383922647)(https://raw.githubusercontent.com/viacheung/img/main/image/image-20210403163751501.png)]

图中,List代表了有序可重复集合,可直接根据元素的索引来访问;Set代表无序不可重复集合,只能根据元素本身来访问;Queue是队列集合。

Map代表的是存储key-value对的集合,可根据元素的key来访问value。

上图中淡绿色背景覆盖的是集合体系中常用的实现类,分别是ArrayList、LinkedList、ArrayQueue、HashSet、TreeSet、HashMap、TreeMap等实现类

线程安全的集合有哪些?线程不安全的呢?

线程安全的:

  • Hashtable:比HashMap多了个线程安全。
  • ConcurrentHashMap:是一种高效但是线程安全的集合。
  • Vector:比Arraylist多了个同步化机制。
  • Stack:栈,也是线程安全的,继承于Vector。

线性不安全的:

  • HashMap
  • Arraylist
  • LinkedList
  • HashSet
  • TreeSet

说说List,Set,Map三者的区别?

List 有序的、可重复的。
Set⽆序的、不可重复的。
Map ⽆序的,key不可重复、value 可重复

HashSet 和 HashMap 区别?

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VpL5z6DS-1681383922648)(https://raw.githubusercontent.com/viacheung/img/main/image/image-20210403193010949.png)]

HashSet的底层其实就是HashMap,只不过我们HashSet是实现了Set接口并且把数据作为K值,而V值一直使用一个相同的虚值来保存

由于HashMap的K值本身就不允许重复,并且在HashMap中如果K/V相同时,会用新的V覆盖掉旧的V,然后返回旧的V,那么在HashSet中执行这一句话始终会返回一个false,导致插入失败,这样就保证了数据的不可重复性。

Arraylist 与 LinkedList 区别?

1、底层一个数组 一个双向链表

2、增删 和查的效率问题

3、内存空间,Arraylist 预留空间 LinkedList指针

说⼀说 ArrayList 的扩容机制吧

if 容量==0,第一次添加元素容量为 10

else 会将修改次数 modCount++,并且会将原数组中的元素,拷贝至新数组中,新数组的大小是

原数组的 1.5 倍

Array 和 ArrayList 有什么区别?什么时候该应 Array 而不是 ArrayList 呢?

  • Array 可以包含基本类型和对象类型,ArrayList 只能包含对象类型。
  • Array 大小是固定的,ArrayList 的大小是动态变化的。
  • ArrayList 提供了更多的方法和特性,比如:addAll(),removeAll(),iterator() 等等。

ArrayList 与 Vector 区别?

  • Vector是线程安全的,ArrayList不是线程安全的。其中,Vector在关键性的方法前面都加了synchronized关键字,来保证线程的安全性。如果有多个线程会访问到集合,那最好是使用 Vector,因为不需要我们自己再去考虑和编写线程安全的代码。
  • ArrayList在底层数组不够用时在原来的基础上扩展0.5倍,Vector是扩展1倍,这样ArrayList就有利于节约内存空间。

HashMap系列

HashMap 和 HashSet(底层HashMap)区别

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pgLdHFyi-1681383922649)(https://raw.githubusercontent.com/viacheung/img/main/image/image-20230310165027434.png)]

HashSet如何检查重复

hashcode—equals

HashMap的底层实现

HashMap构造函数

三个 初始容量+默认加载因子

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ez6yXdXo-1681383922650)(https://raw.githubusercontent.com/viacheung/img/main/image/image-20230324151900931.png)]

数组 +链表+红黑树

HashMap 的扩容方式?

扩容:

首先 HashMap 的初始容量是 16,并且每次对原数组长度 * 2 进行扩容,==HashMap 在容量超过负载因子所定义的容量之后,就会扩容,默认0.75,==构造函数可以调整,无参有参构造

当链表大于8,如果数组<64 先数组扩容,否则链表转为红黑树

HashMap扩容:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mZk9ERD5-1681383922651)(https://raw.githubusercontent.com/viacheung/img/main/image/image-20230324234844935.png)]

HashMap为什么默认加载因子是0.75?

作为一般规则,默认负载因子(0.75)在时间和空间成本上提供了很好的折衷。较高的值会降低空间开销,但提高查找成本(体现在大多数的HashMap类的操作,包括get和put)。设置初始大小时,应该考虑预计的entry数在map及其负载系数,并且尽量减少rehash操作的次数。如果初始容量大于最大条目数除以负载因子,rehash操作将不会发生

HashMap 中 key 的存储索引是怎么计算的?

取key的 hashCode 值、根据 hashcode 计算出hash值(hashcode 异或其右移十六位)、通过取模计算下标

1、JDK1.8 为什么要 hashcode 异或其右移十六位的值?

右移后再亦或,高位和低位做了混合,在之后的hash & (length-1) 中高位就也参与进运算了,增加了散列程度

由于和 (length -1) 运算,length 绝大多数情况小于 2 的 16 次方。 所以始终是 hashcode 的低 16 位(甚至更低) 参与运算。 但是这样高 16 位是用不到的,为了让得到的下标更加散列,需要让高16位也参与运算,所以就需要低16位和高16位进行 ^ 运算。

2、为什么 hash 值要与length-1相与?

位运算快

充分散列

HashMap 的put方法流程?

  1. 首先根据 key 的值计算 hash 值,找到该元素在数组中存储的下标;

  2. 如果数组是空的,则调用 resize 进行初始化;

  3. 如果没有哈希冲突直接放在对应的数组下标里;

  4. 如果冲突了,且 key 已经存在,就覆盖掉 value;

  5. 如果冲突后,发现该节点是红黑树,则判断TreeNode是否已存在,如果存在则直接返回oldnode并更新;不存在则直接插入红黑树,++size,超出threshold容量就扩容,然后将这个节点挂在树上;

  6. 如果是链表,则判断Node是否已存在,如果存在则直接返回oldnode并更新;不存在则直接插入链表尾部,判断链表长度,如果大于8则转为红黑树存储,++size,超出threshold容量就扩容;

    判断该链表是否大于 8 ,如果大于 8 并且数组容量小于 64,就进行扩容;如果链表节点大于 8 并且数组的容量大于 64,则将这个结构转换为红黑树;

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DVWkF82n-1681383922652)(https://raw.githubusercontent.com/viacheung/img/main/image/hashmap之put方法.jpg)]

解决hash冲突的办法有哪些?HashMap用的哪种?

解决Hash冲突方法有:开放定址法(线性探测法)、再哈希法(多个hash函数算)、链地址法(拉链法)、建立公共溢出区。HashMap中采用的是 链地址法

线性探测法和

HashMap 多线程操作死链问题

1.7头插会产生,1.8尾插没有了

线程2完成移动 线程1才开始移动 因此就会产生环形链表

HashMap为什么线程不安全

A判断好这个地方没有数据,准备插入的时候,这时候B线程抢夺到时间片,来插入,然后A再插入就把B覆盖了

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xGqCFSiy-1681383922653)(https://raw.githubusercontent.com/viacheung/img/main/image/image-20230324212711323.png)]

  • 多线程下扩容死循环。JDK1.7中的 HashMap 使用头插法插入元素,在多线程的环境下,扩容的时候有可能导致环形链表的出现,形成死循环。因此,JDK1.8使用尾插法插入元素,在扩容时会保持链表元素原本的顺序,不会出现环形链表的问题。
  • 多线程的put可能导致元素的丢失。多线程同时执行 put 操作,如果计算出来的索引位置是相同的,那会造成前一个 key 被后一个 key 覆盖,从而导致元素的丢失。此问题在JDK 1.7和 JDK 1.8 中都存在。
  • put和get并发时,可能导致get为null。线程1执行put时,因为元素个数超出threshold而导致rehash,线程2此时执行get,有可能导致这个问题。此问题在JDK 1.7和 JDK 1.8 中都存在。

为什么链表长度8树化

链表阈值和产生冲突概率为泊松分布 选择8是千万分之6 7是十万分之一,差1000倍

退化

当扩容后链表长度小于等于 6 进行树的退化 长度为6的话链表和红黑树查找效率忽略不计。此时维护红黑树的平衡反而加大开销,所以退化

红黑树:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Q97qoLaY-1681383922654)(https://raw.githubusercontent.com/viacheung/img/main/image/image-20230310165334906.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BYBs6zt7-1681383922655)(https://raw.githubusercontent.com/viacheung/img/main/image/image-20230310165618577.png)]

HashMap 的⻓度为什么是2的幂次⽅

1、2 的幂次可以用 与 的方式进行取余运算,效率更高;

2)在扩容移动链表节点时,节点在新数组中的位置只可能是原位置 i 或 i + oldCap 旧数组长度,扩容时效率更高

一般用什么作为HashMap的key?

一般用Integer、String 这种不可变类当 HashMap 当 key,而且 String 最为常用。

  • 因为字符串是不可变的,所以在它创建的时候 hashcode 就被缓存了,不需要重新计算。这就是 HashMap 中的键往往都使用字符串的原因。
  • 因为获取对象的时候要用到 equals() 和 hashCode() 方法,那么键对象正确的重写这两个方法是非常重要的,这些类已经很规范的重写了 hashCode() 以及 equals() 方法。

ConcurrentHashMap

ConcurrentHashMap线程安全的具体实现⽅式/底层具体实现

1.7: 分段锁 (可重入锁)Segment 数组 + HashEntry 数组 + 链表

JDK1.7中的ConcurrentHashMap 是由 Segment 数组结构和 HashEntry 数组结构组成,即ConcurrentHashMap 把哈希桶切分成小数组(Segment ),每个小数组有 n 个 HashEntry 组成。

其中,Segment 继承了 ReentrantLock,所以 Segment 是一种可重入锁,扮演锁的角色;HashEntry 用于存储键值对数据。

首先将数据分为一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据时,其他段的数据也能被其他线程访问,能够实现真正的并发访问。

1.8 CAS syn

在数据结构上, JDK1.8 中的ConcurrentHashMap 选择了与 HashMap 相同的数组+链表+红黑树结构;在锁的实现上,抛弃了原有的 Segment 分段锁,采用CAS + synchronized实现更加低粒度的锁。

将锁的级别控制在了更细粒度的哈希桶元素级别,也就是说只需要锁住这个链表头结点(红黑树的根节点),就不会影响其他的哈希桶元素的读写,大大提高了并发度。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CjkBfxEL-1681383922655)(https://raw.githubusercontent.com/viacheung/img/main/image/ConcurrentHashMap-jdk1.8.png)]

node数组+链表+红黑树

synchronized 只锁定当前链表或红⿊⼆叉树的⾸节点,这样只要 hash 不冲突,就不会产⽣并发,效率⼜提升 N 倍

ConcurrentHashMap 的 put 方法执行逻辑是什么?

先来看JDK1.7

首先,会尝试获取锁,如果获取失败,利用自旋获取锁;如果自旋重试的次数超过 64 次,则改为阻塞获取锁。

获取到锁后:

  1. 将当前 Segment 中的 table 通过 key 的 hashcode 定位到 HashEntry。
  2. 遍历该 HashEntry,如果不为空则判断传入的 key 和当前遍历的 key 是否相等,相等则覆盖旧的 value。
  3. 没有相等的话则需要新建一个 HashEntry 并加入到 Segment 中,同时会先判断是否需要扩容。
  4. 释放 Segment 的锁。

再来看JDK1.8

大致可以分为以下步骤:

  1. 根据 key 计算出 hash值。
  2. 判断是否需要进行初始化。
  3. 定位到 Node,拿到首节点 f,判断首节点 f:
    • 如果为 null ,则通过cas的方式尝试添加。
    • 如果为 f.hash = MOVED = -1 ,说明其他线程在扩容,参与一起扩容。
    • 如果都不满足 ,synchronized 锁住 f 节点,判断是链表还是红黑树,遍历插入。
  4. 当在链表长度达到8的时候,数组扩容或者将链表转换为红黑树。

ConcurrentHashMap 的 get 方法是否要加锁,为什么?

get 方法不需要加锁。因为 Node 的元素 val 和指针 next 是用 volatile 修饰的,在多线程环境下线程A修改结点的val或者新增节点的时候是对线程B可见的。

这也是它比其他并发集合比如 Hashtable、用 Collections.synchronizedMap()包装的 HashMap 安全效率高的原因之一。

ConcurrentHashMap 不支持 key 或者 value 为 null 的原因?

1、我们先来说value 为什么不能为 null ,因为ConcurrentHashMap 是用于多线程的 ,如果map.get(key)得到了 null ,无法判断,是映射的value是 null ,还是没有找到对应的key而为 null ,这就有了二义性。

追问:说说为什么ConcurrentHashMap判断不了呢?

此时如果有A、B两个线程,A线程调用ConcurrentHashMap.get(key)方法返回null,但是我们不知道这个null是因为key没有在map中映射还是本身存的value值就是null,此时我们假设有一个key没有在map中映射过,也就是map中不存在这个key,此时我们调用ConcurrentHashMap.containsKey(key)方法去做一个判断,我们期望的返回结果是false。但是恰好在A线程get(key)之后,调用constainsKey(key)方法之前B线程执行了ConcurrentHashMap.put(key,null),那么当A线程执行完containsKey(key)方法之后我们得到的结果是true,与我们预期的结果就不相符了。

而用于单线程状态的HashMap却可以用containsKey(key) 去判断到底是否包含了这个 null 。

2、至于ConcurrentHashMap 中的key为什么也不能为 null 的问题,源码就是这样写的,哈哈。就回答作者Doug不喜欢 null ,所以在设计之初就不允许了 null 的key存在

get方法不需要加锁与volatile修饰的哈希桶有关吗?

没有关系。哈希桶table用volatile修饰主要是保证在数组扩容的时候保证可见性。

ConcurrentHashMap 的并发度是多少?

jdk1.7里面,程序在运行时能够同时更新ConcurrentHashMap且不产生锁竞争的最大线程数默认是16,这个值可以在构造函数中设置。如果自己设置了并发度,ConcurrentHashMap 会使用大于等于该值的最小的2的幂指数作为实际并发度,也就是比如你设置的值是17,那么实际并发度是32。

ConcurrentHashMap 迭代器是强一致性还是弱一致性?

类似快速失败和安全失败

快速失败就是HashMap 安全失败是Con~

与HashMap迭代器是强一致性不同,ConcurrentHashMap 迭代器是弱一致性。

ConcurrentHashMap 的迭代器创建后,就会按照哈希表结构遍历每个元素,但在遍历过程中,内部元素可能会发生变化,如果变化发生在已遍历过的部分,迭代器就不会反映出来,而如果变化发生在未遍历过的部分,迭代器就会发现并反映出来,这就是弱一致性。

这样迭代器线程可以使用原来老的数据,而写线程也可以并发的完成改变,更重要的,这保证了多个线程并发执行的连续性和扩展性,是性能提升的关键。想要深入了解的小伙伴,可以看这篇文章[为什么ConcurrentHashMap 是弱一致的](http://ifeve.com/ConcurrentHashMap -weakly-consistent/)

JDK1.7与JDK1.8 中ConcurrentHashMap 的区别?

  • 数据结构:取消了Segment分段锁的数据结构,取而代之的是数组+链表+红黑树的结构。
  • 保证线程安全机制:JDK1.7采用Segment的分段锁机制实现线程安全,其中segment继承自ReentrantLock。JDK1.8 采用CAS+Synchronized保证线程安全。
  • 锁的粒度:原来是对需要进行数据操作的Segment加锁,现调整为对每个数组元素加锁(Node)。
  • 链表转化为红黑树:定位结点的hash算法简化会带来弊端,Hash冲突加剧,因此在链表节点数量大于8时,会将链表转化为红黑树进行存储。
  • 查询时间复杂度:从原来的遍历链表O(n),变成遍历红黑树O(logN)。

ConcurrentHashMap 和Hashtable的效率哪个更高?为什么?

ConcurrentHashMap 的效率要高于Hashtable,因为Hashtable给整个哈希表加了一把大锁从而实现线程安全。而ConcurrentHashMap 的锁粒度更低,在JDK1.7中采用分段锁实现线程安全,在JDK1.8 中采用CAS+Synchronized实现线程安全。

说一下Hashtable的锁机制 ?

Hashtable是使用Synchronized来实现线程安全的,给整个哈希表加了一把大锁,多线程访问时候,只要有一个线程访问或操作该对象,那其他线程只能阻塞等待需要的锁被释放,在竞争激烈的多线程场景中性能就会非常差!

img

多线程下安全的操作 map还有其他方法吗?

还可以使用Collections.synchronizedMap方法,对方法进行加同步锁

如果传入的是 HashMap 对象,其实也是对 HashMap 做的方法做了一层包装,里面使用对象锁来保证多线程场景下,线程安全,本质也是对 HashMap 进行全表锁。在竞争激烈的多线程环境下性能依然也非常差,不推荐使用!

⽐较 HashSet、 LinkedHashSet 和 TreeSet 三者的异同

HashSet 是 Set 接⼝的主要实现类 , HashSet 的底层是 HashMap ,线程不安全的,可以存储 null 值;
LinkedHashSet 按照添加的顺序遍历;
TreeSet 底层红⿊树

如何选⽤集合?

Map

collection:set list

collection框架中实现比较要怎么做?

第一种,实体类实现Comparable接口,并实现 compareTo(T t) 方法,称为内部比较器。

第二种,创建一个外部比较器,这个外部比较器要实现Comparator接口的 compare(T t1, T t2)方法。

Iterator 和 ListIterator 有什么区别?

  • 遍历。使用Iterator,可以遍历所有集合,如Map,List,Set;但只能在向前方向上遍历集合中的元素。

使用ListIterator,只能遍历List实现的对象,但可以向前和向后遍历集合中的元素。

  • 添加元素。Iterator无法向集合中添加元素;而,ListIteror可以向集合添加元素。
  • 修改元素。Iterator无法修改集合中的元素;而,ListIterator可以使用set()修改集合中的元素。
  • 索引。Iterator无法获取集合中元素的索引;而,使用ListIterator,可以获取集合中元素的索引。

多线程

请简要描述线程与进程的关系,区别及优缺点?

根本区别:进程是操作系统资源分配的基本单位,而线程是处理器任务调度和执行的基本单位

资源开销:进程独享内存空间,进程之间的切换会有较大的开销;而线程有自己独立的运行栈和程序计数器(PC),线程之间切换的开销小。

包含关系:线程是进程划分成更小的运行单位

影响关系:一个进程崩溃后,不会对其他进程产生影响,但是一个线程崩溃整个进程都死掉。所以多进程要比多线程健壮。

简述线程、程序、进程的基本概念。以及他们之间关系是什么?

程序: 含有数据和指令的静态 文件(存在磁盘)

进程:程序的一次执行过程

线程:进程划分为更小的运行单位 同类的多个线程共享进程的堆和⽅法区 但每个线程有⾃⼰的程序计数器、 虚拟机栈和本地⽅法栈

创建线程的三种方式的对比?

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hLZGdzgB-1681383922656)(https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/f79dc95501ff4f43a61fb415eab14506~tplv-k3u1fbpfcp-zoom-in-crop-mark:4536:0:0:0.awebp)]

  • 定义Thread类的子类,并重写该类的run方法(new完Thread,直接start)
  • 定义Runnable接口的实现类,并重写该接口的run()方法(new完Thread,再实现一个Runnable接口放到Thread里面)
  • 定义Callable接口的实现类,并重写该接口的call()方法,一般配合Future使用
  • 线程池的方式

1、采用实现Runnable. Callable接口的方式创建多线程:

优势:只是实现接口,还可以继承其他类,功能扩展好
劣势:要访问当前线程,则必须使用Thread.currentThread()

Runnable和Callable的区别

1、重写方法一个run 一个call

2、有无返回值 callable有

3、call可以抛出异常 run不可以

4、运行Callable任务可以拿到一个Future对象,表示异步计算的结果

2、使用继承Thread类的方式创建多线程

优势是:

如果需要访问当前线程,直接使用this即可获得当前线程。

劣势是:

已经继承了Thread类,所以不能再继承其他父类。

多线程的好处

1、基于高并发的需求

2、线程间的切换和调度的成本远远小于进程

程序计数器为什么是私有的?

程序计数器记录当前线程执行位置,主要是为了线程切换后能恢复到正确的执⾏位置。

**虚拟机栈和本地⽅法栈为什么是私有的? **

为了保证当前线程的局部变量不被别的线程访问

⼀句话简单了解堆和⽅法区

堆:对象

方法区:已经加载的类信息、常量、静态变量,JIT编译的代码

说说并发与并⾏的区别?

并发:一段时间

并行:同一时刻

为什么要使⽤多线程呢? (而不是多进程)

线程间切换调度成本小于进程

多核时代 利用多个cpu 提高利用率

使⽤多线程可能带来什么问题?

内存泄漏、 上下⽂切换、 死锁 。

说说线程的⽣命周期和状态?

新建-可运行-运行-等待-超时等待-阻塞-中止

什么是上下⽂切换?

每个线程分配时间片并轮转,当前任务切换到其他线程之前需要保存自己状态,以便回来时再继续加载之前状态

线程有哪些基本状态?

新建

可运行(就绪)

运行

阻塞

等待

超时等待

终止

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7nEpiUaP-1681383922657)(https://raw.githubusercontent.com/viacheung/img/main/image/image-20230310160402454.png)]

说下过程?

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YOkgx2Ma-1681383922658)(https://raw.githubusercontent.com/viacheung/img/main/image/image-20230310161133478.png)]

notify notifyAll

什么是线程死锁?如何避免死锁?

线程死锁:

两个或两个以上的线程在执行过程中,因争夺资源而造成的一种互相等待的现象

如何避免?:1、如果自己获取不到,主动释放自己占有的资源 2、按序申请资源 反序释放

破坏死锁条件:

  1. 互斥条件:该资源任意⼀个时刻只由⼀个线程占⽤。(解决:不能破坏)
  2. 请求与保持条件:⼀个进程因请求资源⽽阻塞时,对加锁资源保持不放。(一直阻塞在那里)(解决:一次性申请所有资源
  3. ==不剥夺条件:==线程已获得的资源在末使⽤完之前不能被其他线程强⾏剥夺,只有⾃⼰使⽤完毕(解决:如果自己获取不到,主动释放自己占有的资源)后才释放资源。(占着锁 非抢占式)
  4. 循环等待条件:若⼲进程之间形成⼀种头尾相接的循环等待资源关系。(解决:按序申请资源 反序释放

常见对比

Runnable VS Callable

  • Callable 接口可以返回结果或抛出检查异常
  • Runnable 接口不会
  • Executors 可以实现 Runnable 对象和 Callable 对象之间的相互转换

shutdown() VS shutdownNow()

  • shutdown() :关闭线程池,线程池的状态变为 SHUTDOWN。线程池不再接受新任务了,但是队列里的任务得执行完毕。
  • shutdownNow() :关闭线程池,线程的状态变为 STOP。线程池会终止当前正在运行的任务,并停止处理排队的任务并返回正在等待执行的 List。 shutdownNow的原理是遍历线程池中的工作线程,然后逐个调用线程的interrupt方法来中断线程,所以无法响应中断的任务可能永远无法终止

isTerminated() VS isShutdown()

  • isShutDown 当调用 shutdown() 方法后返回为 true。
  • isTerminated 当调用 shutdown() 方法后,并且所有提交的任务完成后返回为 true

排查死锁

命令

jps -l 查看当前进程运行状况
jstack 进程编号 查看该进程信息

图形化

jconsole 打开线程 ,点击 检测死锁

说说 sleep() ⽅法和 wait() ⽅法区别和共同点?

原理:

  • sleep方法:是Thread类的静态方法,当前线程将睡眠n毫秒,线程进入阻塞状态。当睡眠时间到了,会解除阻塞,进入可运行状态,等待CPU的到来。睡眠不释放锁(如果有的话)。
  • wait方法:是Object的方法,必须与synchronized关键字一起使用,线程进入阻塞状态,当notify或者notifyall被调用后,会解除阻塞。但是,只有重新占用互斥锁之后才会进入可运行状态。睡眠时,会释放互斥锁。

区别:sleep() ⽅法没有释放锁,⽽ wait() ⽅法释放了锁

sleep之后自动苏醒,wait需要notify()唤醒

共同点:两者都可以暂停线程的执⾏。

为什么我们调⽤ start() ⽅法时会执⾏ run() ⽅法,为什么我们不能直接调⽤ run() ⽅法

直接调用就是main线程的一个普通方法,并不会在某个线程里面执行

而new 一个 Thread,线程进入了新建状态; 调用start() ,线程进入了就绪状态,然后获得时间片就可以运行了

Thread类中的yield方法有什么作用?

暂停当前正在执行的线程对象,让其它有相同优先级的线程执行。它是一个静态方法而且只保证当前线程放弃CPU占用而不能保证使其它线程一定能占用CPU,执行yield()的线程有可能在进入到暂停状态后马上又被执行(我暂停了我又回来了吼吼)。

如何创建线程实例并运行

  1. 创建 Thread 的子类并重写 run()

run() 方在调用 start() 方法后被执行,而且一旦线程启动后 start() 方法后就会立即返回,而不是等到 run() 方法执行完毕后再返回。

  1. 实现 Runnable 接口

在新建类时实现 Runnable 接口,然后在 Thread 类的构造函数(new Thread的时候传参)中传入 MyRunnable 的实例对象,最后执行 start() 方法即可;

线程阻塞的三种情况

  1. 等待阻塞(Object.wait -> 等待队列)

RUNNING 状态的线程执行 Object.wait() 方法后,JVM 会将线程放入等待序列(waitting queue);

  1. 同步阻塞(lock -> 锁池)

RUNNING 状态的线程在获取对象的同步锁时,若该 同步锁被其他线程占用,则 JVM 将该线程放入锁池(lock pool)中

  1. 其他阻塞(sleep/join)

RUNNING 状态的线程执行 Thread.sleep(long ms)Thread.join() 方法,或发出 I/O 请求时,JVM 会将该线程置为阻塞状态。当 sleep() 状态超时,join() 等待线程终止或超时. 或者 I/O 处理完毕时,线程重新转入可运行状态(RUNNABLE);

线程死亡的三种方式

  1. 正常结束

run() 或者 call() 方法执行完成后,线程正常结束;

  1. 异常结束

线程抛出一个未捕获的 ExceptionError,导致线程异常结束;

  1. 调用 stop()

直接调用线程的 stop() 方法来结束该线程,但是一般不推荐使用该种方式,因为该方法通常容易导致死锁

守护线程是啥?

运行在后台的一种特殊进程,在 Java 中垃圾回收线程就是特殊的守护线程。

了解Fork/Join框架吗?

Java7提供 ,用于并行执行任务,把大任务分割成若干个小任务,最终汇总每个小任务结果后得到大任务结果的框架。

「分而治之」「工作窃取算法」

「分而治之」

「工作窃取算法」

把大任务拆分成小任务,放到不同队列执行,交由不同的线程分别执行时。有的线程优先把自己负责的任务执行完了,其他线程还在慢慢悠悠处理自己的任务,这时候为了充分提高效率,就需要工作盗窃算法啦~

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yutNkgRw-1681383922659)(data:image/gif;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVQImWNgYGBgAAAABQABh6FO1AAAAABJRU5ErkJggg==)]

工作盗窃算法就是,「某个线程从其他队列中窃取任务进行执行的过程」。一般就是指做得快的线程(盗窃线程)抢慢的线程的任务来做,同时为了减少锁竞争,通常使用双端队列,即快线程和慢线程各在一端。

补充

如何保证线程安全

1、原子:Atomic synchronized

2、可见:synchronized volatile

3、有序:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-i8EAeAaN-1681383922659)(https://raw.githubusercontent.com/viacheung/img/main/image/image-20230410165222752.png)]

synchronized lock接口

synchronized

说说 synchronized 关键字和 volatile 关键字的区别

互补 存在

1、volatile 只能用于变量,syn可以变量、方法、类级别、代码块

2、volatile 关键字能保证数据的可⻅性,但不能保证数据的原⼦性。 synchronized 关键字两者都能保证

3、volatile 不会造成线程的阻塞;synchronized 可能会造成线程的阻塞

4、volatile 关键字主要⽤于解决变量在多个线程之间的可⻅性,⽽ synchronized 关键字解决的是多个线程之间访问资源的同步性

5、volatile 本质是告诉当前变量工作内存中的值是不确定的需要从主存中读取; synchronized 则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。

synchronized 和 Lock 有什么区别?

  • synchronized 可以给类. 方法. 代码块加锁;而 lock 只能给代码块加锁。
  • synchronized 不需要手动获取锁和释放锁,使用简单,发生异常会自动释放锁,不会造成死锁;而 lock 需要自己加锁和释放锁,如果使用不当没有 unLock()去释放锁就会造成死锁。
  • 通过 Lock 可以知道有没有成功获取锁,而 synchronized 却无法办到。

synchronized 和 ReentrantLock 区别是什么?

1.两者都是可重入锁

递归锁,指的是在一个线程中可以多次获取同一把锁,比如: 一个线程在执行一个带锁的方法,该方法中又调用了另一个需要相同锁的方法,则该线程可以直接执行调用的方法,而无需重新获得锁, 两者都是同一个线程每进入一次,锁的计数器都自增1,所以要等到锁的计数器下降为0时才能释放锁。

2.synchronized 依赖于 JVM 而 ReentrantLock 依赖于 API

  • synchronized 是依赖于 JVM 实现的, 虚拟机团队在 JDK1.6 为 synchronized 关键字进行了很多优化,但是这些优化都是在虚拟机层面实现的
  • ReentrantLock 是 JDK 层面实现的(也就是 API 层面,需要 lock() 和 unlock() 方法配合 try/finally 语句块来完成)

3.ReentrantLock 比 synchronized 增加了一些高级功能

相比synchronized,ReentrantLock增加了一些高级功能。主要来说主要有三点:①等待可中断;②可实现公平锁;③可实现选择性通知(锁可以绑定多个条件)

  • 等待可中断.通过lock.lockInterruptibly()来实现这个机制。也就是说正在等待的线程可以选择放弃等待,改为处理其他事情。
  • ==ReentrantLock可以指定是公平锁还是非公平锁。而synchronized只能是非公平锁。==所谓的公平锁就是先等待的线程先获得锁。 ReentrantLock默认情况是非公平的,可以通过 ReentrantLock类的ReentrantLock(boolean fair)构造方法来制定是否是公平的。
  • ReentrantLock类线程对象可以注册在指定的Condition中,从而可以有选择性的进行线程通知,在调度线程上更加灵活。 在使用notify()/notifyAll()方法进行通知时,被通知的线程是由 JVM 选择的,用ReentrantLock类结合Condition实例可以实现“选择性通知”

4.使用选择

  • 除非需要使用 ReentrantLock 的高级功能,否则优先使用 synchronized。
  • synchronized 是 JVM 实现的一种锁机制,JVM 原生地支持它,而 ReentrantLock 不是所有的 JDK 版本都支持。并且使用 synchronized 不用担心没有释放锁而导致死锁问题,因为 JVM 会确保锁的释放

synchronized的用法有哪些?

  • 修饰普通方法:作用于当前对象实例,进入同步代码前要获得当前对象实例的锁
  • 修饰静态方法:作用于当前类,进入同步代码前要获得当前类对象的锁,synchronized 关键字加到 static 静态方法和 synchronized(class)代码块上都是是给 Class 类上锁
  • 修饰代码块:指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁(里面可以写.class(类锁)也可以写this(对象锁))

特别注意:

①如果一个线程A调用一个实例对象的非静态 synchronized 方法,而线程B需要调用这个实例对象所属类的静态 synchronized 方法,是允许的,不会发生互斥现象,因为访问静态 synchronized 方法占用的锁是当前类的锁(因为加的不是一把锁)

②尽量不要使用 synchronized(String s) ,因为JVM中,字符串常量池具有缓冲功能

(就是说由于字符串常量池的原因不同的变量可能引用着同一个对象,锁不同变量的时候会锁成同一个对象,从而造成意料之外的同步,降低效率)

Synchronized的作用有哪些?

  1. 原子性:确保线程互斥的访问同步代码;
  2. 可见性:保证共享变量的修改能够及时可见,其实是通过Java内存模型中的 “对一个变量unlock操作之前,必须要同步到主内存中;如果对一个变量进行lock操作,则将会清空工作内存中此变量的值,在执行引擎使用此变量前,需要重新从主内存中load操作或assign操作初始化变量值” 来保证的;
  3. 有序性:有效解决重排序问题,即 “一个unlock操作先行发生(happen-before)于后面对同一个锁的lock操作”。

说⼀说⾃⼰对于 synchronized 关键字的了解

属于重量级锁,效率低,线程之间的切换通过操作系统层面,需要从⽤户态转换到内核态,这俩状态之间的转换成本高

1.6之后对synchronized引入大量优化,自旋 锁消除(每个线程一把锁)锁粗化(锁的都是一个对象) 偏向 轻量级锁来减少开销

单例模式了解吗?来给我⼿写⼀下!给我解释⼀下双重检验锁⽅式实现单例模式的原理呗!

两个判断:防止加锁过程对象被其他线程实例化

uniqueInstance = new Singleton();

1、分配内存空间

2、初始化

3、指向分配地址

jvm指令重排 导致线程获得还没初始化的实例,解决办法:volatile 保证多线程无指令重排

构造⽅法可以使⽤ synchronized 关键字修饰么?

不可

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-E0QhfM6q-1681383922660)(https://raw.githubusercontent.com/viacheung/img/main/image/image-20230314215150551.png)]

对象锁是啥?

管程

什么是可重入锁

同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取(前提,锁对象得是同一个对象),不会因为之前已经获取过还没释放而阻塞。

执行monitorenter,如果锁对象计数器0,那么说明没有被其他线程持有,jvm把该锁对象持有线程设为当前线程,当计数器不为0,如果持有线程是当前线程,jvm把计数器+1,否则等待。执行monitorexit时,Java虚拟机则需将锁对象的计数器减1。计数器为零代表锁已被释放。

ReentrantLock和synchronized都是可重入锁 (可以避免死锁)
syn隐式,lock unlock显式 假如lock unlock不成对,单线程情况下问题不大,但多线程下出问题

讲⼀下 synchronized底层

反编译带syn的代码块,可以看到,同步代码块开始结束位置有个monitorenter 和 monitorexit,到monitorenter这个指令时,会先尝试获取对象的锁,本质上来说,Synchronized其实是通过在对象头上设置标记,锁的计数器就会+1,而当执行到monitorexit这个指令时,锁计数器就会-1,直到减到0,这个锁也就被释放了。如果获取对象锁失败,那当前线程就要阻塞等待,直到锁被另外一个线程释放为止

synchronized 修饰的方法并没有 monitorenter 指令和 monitorexit 指令,取得代之的确实是 ACC_SYNCHRONIZED 标识,该标识指明了该方法是一个同步方法,JVM 通过该 ACC_SYNCHRONIZED 访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。

为什么任何一个对象都可以成为一个锁?

追溯底层可以发现每个对象天生都带着一个对象监视器: ObjectMonitor :记录线程获取锁次数,记录哪个线程持有我

synchronized 同步语句块的情况

synchronized 同步语句块的实现使⽤的是 monitorenter 和 monitorexit 指令,其中monitorenter 指令指向同步代码块的开始位置, monitorexit 指令则指明同步代码块的结束位置。当执⾏ monitorenter 指令时,线程试图获取锁也就是获取 对象监视器 monitor 的持有权。

moniter enter -->exit

objectmoniter 类owner谁持有谁记录

synchronized 修饰⽅法的的情况

JVM 通过该ACC_SYNCHRONIZED (true or false)访问标志来辨别⼀个⽅法是否声明为同步⽅法,从⽽执⾏相应的同步调⽤。

为什么这俩不一样? 我的理解:代码块里面锁可以重入 方法不可

总结

synchronized 同步语句块的实现使⽤的是 monitorenter 和 monitorexit 指令,其中monitorenter 指令指向同步代码块的开始位置, monitorexit 指令则指明同步代码块的结束位置。 锁的计数器为 0 1 进+1 出 -1
synchronized 修饰的⽅法并没有 monitorenter 指令和 monitorexit 指令,取得代之的确实是ACC_SYNCHRONIZED 标识,该标识指明了该⽅法是⼀个同步⽅法。
不过两者的本质都是对对象监视器 monitor 的获取。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xHltHMrc-1681383922661)(https://raw.githubusercontent.com/viacheung/img/main/image/image-20230314220151803.png)]

多线程中 synchronized 锁升级的原理是什么?

1、偏向锁

当一段同步代码一直被同一个线程多次访问,由于只有一个线程访问那么该线程在后续访问时便会自动获得锁。连CAS都无,(目的:防止不停的在用户态和内核态之间切换

在锁对象的对象头里面MarkWord里面存的有当前线程id,第一次进,为空,把id设为自己的,以后每次进的时候没有加锁解锁,直接会去检查锁的MarkWord里面是不是放的自己的线程ID ,是的话,直接进,无CAS,如果不一致,尝试使用CAS来替换MarkWord里面的线程ID为新线程的ID ,竞争成功,表示之前的线程不存在了,MarkWord里面的线程ID为新线程的ID,锁不会升级,仍然为偏向锁;

2、轻量级锁

(续上)如果竞争失败,这时候会等待一个全局安全点,也就是没有代码执行,暂停原来持有偏向锁的线程,检查偏向锁线程是否处于代码块,处于代码块的话,升级为轻量,此时持有线程的还是之前原持有偏向锁的线程,线程B自旋等待;如果已经退出代码块了,锁设为无锁状态。

作用:有线程来参与锁的竞争,但是获取锁的冲突时间极短,本质就是自选锁CAS

轻量级锁是为了在线程近乎交替执行同步块时提高性能 ,说白了先自旋,不行才升级阻塞。

若一个线程获得锁时发现是轻量级锁,会把锁的MarkWord复制到自己的Displaced Mark Word(JVM会为每个线程在 当前线程的栈帧中创建用于存储锁记录的空间 )里面。然后线程尝试用CAS将锁的MarkWord替换为指向锁记录的指针。 总结:就是这里MarkWord存的是指问线程栈中Lock Record的指针

java6之后有个【自适应自选锁】:

线程如果自旋成功了,那下次自旋的最大次数会增加,因为JVM认为既然上次成功了,那么这一次也很大概率会成功。
反之如果很少会自旋成功,那么下次会减少自旋的次数甚至不自旋,避免CPU空转。总之,自适应意味着自选的次数不是固定不变的,而是根据:同一个锁上一次自旋的时间和拥有锁线程的状态来决定。

轻量锁和偏向锁的区别和不同:
  1. 争夺轻量级锁失败时,自旋尝试抢占锁
  2. 轻量级锁每次退出同步块都需要释放锁,而偏向锁是在竞争发生时才释放锁
3、重量级锁

Java中synchronized的重量级锁,是基于进入和退出Monitor对象实现的。在编译时会将同步块的开始位置插入monitor enter指令,在结束位置插入monitor exit指令。当线程执行到monitor enter指令时,会尝试获取对象所对应的Monitor所有权,如果获取到了,即获取到了锁,会在Monitor的owner中存放当前线程的id,这样它将处于锁定状态,除非退出同步块,否则其他线程无法获取到这个Monitor

锁的升级的目的:锁升级是为了减低了锁带来的性能消耗。在 Java 6 之后优化 synchronized 的实现方式,使用了偏向锁升级为轻量级锁再升级到重量级锁的方式,从而减低了锁带来的性能消耗。

此时Mark Word存的是指向互斥量的指针

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-k7GSE3XW-1681383922662)(https://raw.githubusercontent.com/viacheung/img/main/image/image-20230327213417566.png)]

锁升级为轻量级或重量级锁后,Mark Word中保存的分别是线程栈帧里的锁记录指针和重量级锁指针,
己经没有位置再保存哈希码,GC年龄了,那么这些信息被移动到哪里去了呢

锁升级发生后,hashcode去哪啦

·1、在无锁状态下,Mark Word中可以存储对象的hashcode值。当对象的hashCode()方法第一次被调用时,JVM会生成对应的identity hash code值并将该值存储到Mark Word中。
2、对于偏向锁,在线程获取偏向锁时,会用Thread ID和epoch值覆盖identity hash code所在的位置。如果一个对象的hashCode()方法己经被调用过一次之后,这个对象不能被设置偏向锁。因为如果可以的化,那Mark Word中的identity hash code必然会被偏向线程Id给覆盖,这就会造成同一个对象前后两次调用hashCode()方法得到的结果不一致。
3、升级为轻量级锁时,JVM会在当前线程的栈帧中创建一个锁记录(Lock Record)空间,用于存储锁对象的Mark Word拷贝,该拷贝中可以包含identity hash code,所以轻量级锁可以和identity hashcode共存,哈希码和GC年龄自然保存在此==,释放锁后会将这些信息写回到对象头。==(加锁肯定没法访问啊)
4、升级为重量级锁后,Mark Word保存的重量级锁指针,代表重量级锁的ObjectMonitor类里有字段记录非加锁状态下的Mark Word,锁释放后也会将信息写回到对象头。

synchronized 为什么是非公平锁?非公平体现在哪些地方?

synchronized 的非公平其实在源码中应该有不少地方,因为设计者就没按公平锁来设计,核心有以下几个点:

1)当持有锁的线程释放锁时,该线程会执行以下两个重要操作:

  1. 先将锁的持有者 owner 属性赋值为 null
  2. 唤醒等待链表中的一个线程(假定继承者)。

在1和2之间,如果有其他线程刚好在尝试获取锁(例如自旋),则可以马上获取到锁。

2)当线程尝试获取锁失败,进入阻塞时,放入链表的顺序,和最终被唤醒的顺序是不一致的,也就是说你先进入链表,不代表你就会先被唤醒

JVM对synchronized的优化有哪些?

1. 锁膨胀

上面讲到锁有四种状态,并且会因实际情况进行膨胀升级,其膨胀方向是:无锁——>偏向锁——>轻量级锁——>重量级锁,并且膨胀方向不可逆。

2.锁消除

每个线程一把锁 等于没有 直接消除

3.锁粗化

锁同一个对象 合并

4.自适应自旋锁

轻量级锁失败后,因为一个线程持有一把锁的时间并不长,切换线程不值得,因此就自旋等待

自适应自旋锁属于进一步优化,它的自旋的次数不再固定,其自旋的次数由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定,这就解决了自旋锁带来的缺点。

synchronized 锁能降级吗?

可以的。

具体的触发时机:在全局安全点(safepoint)中,执行清理任务的时候会触发尝试降级锁。

当锁降级时,主要进行了以下操作:

1)恢复锁对象的 markword 对象头;(哦哦 原来所说的markword都是锁对象的呀)

2)重置 ObjectMonitor,然后将该 ObjectMonitor 放入全局空闲列表,等待后续使用。

中断机制

一种协作协商机制 ,中断的过程完全需要程序员自己实现

如何使用中断标识停止线程?

① 通过一个volatile变量实现

volatile保证了可见性,t2修改了标志位后能马上被t1看到

② 通过AtomicBoolean(原子布尔型)

③ 通过Thread类自带的中断api方法实现

interrupt() :处于正常活动状态,那么会将该线程的中断标志设置为 true 仅此而已

如果线程处于被阻塞状态,在别的线程中调用当前线程对象的interrupt方法,那么线程将立即退出被阻塞状态(中断状态将被
清除),并抛出一个InterruptedException异常

interrupted():1 返回当前线程的中断状态2 将当前线程的中断状态设为false

isinterrupt 只是判断

3种让线程等待和唤醒的方法

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RDWGUSOn-1681383922662)(https://raw.githubusercontent.com/viacheung/img/main/image/image-20230314220933990.png)]

1、wait和notify方法必须要在同步块或者方法里面,且成对出现使用,先wait后notify才OK,顺序

LockSupport的阻塞唤醒

3、LockSupport用来创建锁和其他同步类的基本线程阻塞原语 ,Lock Support调用的Unsafe中的native代码 ,使用了一种名为Permit(许可) 的概念来做到阻塞和唤醒线程的功能, 每个线程都有一个许可(permit), -permit(许可)只有两个值1和0,默认是0。0 是阻塞,1是唤醒 - 可以把许可看成是一种(0,1)信号量(Semaphore),但与 Semaphore 不同的是,许可的累加上限是1

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-V3K9S8Gm-1681383922663)(https://raw.githubusercontent.com/viacheung/img/main/image/image-20230314221411811.png)]

许可证只需要一个 先 unpark 再 park无效

为什么可以突破wait/notify的原有调用顺序?

因为unpark获得了一个凭证, 之后再调用park方法, 就可以名正言顺的凭证消费, 故不会阻塞。 先发放了凭证后续可以畅通无阻。

为什么唤醒两次后阻塞两次,但最终结果还会阻塞线程?

因为凭证的数量最多为1, 连续调用两次un park和调用一次un park效果一样, 只会增加一个凭证; 而调用两次park却需要消费两个凭
证, 证不够, 不能放行。

JMM

为什么要弄⼀个 CPU ⾼速缓存呢?

CPU 缓存 解决 CPU 处理速度和内存处理速度不对等的问题。

如何解决内存缓存不⼀致性问题?

通过制定缓存⼀致协议

讲⼀下 JMM(Java 内存模型)

每个线程都有自己的本地内存,读变量从自己内存里面,写变量,自己先改,改完刷回主存

volatile 关键字 除了防⽌ JVM 的指令重排 ,还有⼀个重要的作⽤就是保证变量的可⻅性

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZkOLQNdK-1681383922664)(https://raw.githubusercontent.com/viacheung/img/main/image/image-20230314221553195.png)]

什么是JMM?

通过JMM来实现线程和主内存之间的读写关系,主要围绕三个特性展开

三大特性:原子性、可见性和有序性

原子性:一个操作是不可中断

可见:当一个线程修改了某一个共享变量的值,其他线程能够立即知道该变更 普通的共享变量不保证可见性 线程间变量值的传递均需要通过主内存来完成

有序性:指令重排 如果一个操作执行的结果需要对另一个操作可见性或者代码重新排序

happens-before:明确指定了一组排序规则,来保证线程间的可见性,这个规则就是happensbefore

如果一个操作happens-before另一个操作,那么第一个操作的执行结果将对第二个操作可见,
而且第一个操作的执行顺序排在第二个操作之前**

要想保证 B 操作能够看到 A 操作的结果(无论它们是否在同一个线程),那么 A 和 B 之间必须满足 Happens-Before 关系

  • 单线程规则:一个线程中的每个动作都 happens-before 该线程中后续的每个动作
  • 监视器锁定规则:监听器的解锁动作 happens-before 后续对这个监听器的锁定动作 (unlock lock)
  • volatile 变量规则:对 volatile 字段的写入动作 happens-before 后续对这个字段的每个读取动作
  • 线程 start 规则:线程 start() 方法的执行 happens-before 一个启动线程内的任意动作
  • 线程 join 规则:一个线程内的所有动作 happens-before 任意其他线程在该线程 join() 成功返回之前
  • 传递性:如果 A happens-before B, 且 B happens-before C, 那么 A happens-before C

八条原则:1次序(unlock lock) 2传递 3线程启动(先写再读) (先start) 4 线程中断规则 (先interrupt() ,再Thread.interrupted()检测中断 ) 5线程终止规则(线程中的所有操作都先行发生于对此线程的终止检测 ) 6对象终结规则 (对象初始化先于finalize)

Java 内存模型描述的是多线程对共享内存修改后彼此之间的可见性

volatile

volatile只能保证可见性和有序性

为什么volatile可以实现这些功能呢?(内存屏障)

底层就是内存屏障 ,使得之前的所有读写操作都执行后才可以开始执行此点之后的操作 JVM指令

写指令后加store屏障 读指令前加load屏障

内存屏障之前的所有写操作都要回写到主内存, 内存屏障之后的所有读操作都能获得内存屏障之前的所有写操作的最新结果(实现了可见性)。

内存屏障能干嘛?

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pPsFGFAR-1681383922665)(https://raw.githubusercontent.com/viacheung/img/main/image/image-20230314225624869.png)]

volatile的内存语义

volatile的写内存语义是直接刷新到主内存中,读的内存语义是直接从主内存中读取。
一句话,volatile修饰的变量在某个工作内存修改后立刻会刷新会主内存,并把其他工作内存的该变量设置为无效。

读屏障和写屏障

写屏障:把存储在缓存的数据写回主内存 写屏障之前的写指令全部执行后面指令才能执行

读屏障:之后的读操作都需要在读屏障之后操作 保证读最新

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pUIGSIPQ-1681383922666)(https://raw.githubusercontent.com/viacheung/img/main/image/image-20230314223713230.png)]

我先写 你们后面先别读

隔断!我重新读主存

happens-before之volatile变量规则

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RtDjXnxq-1681383922667)(https://raw.githubusercontent.com/viacheung/img/main/image/image-20230314223831844.png)]

1读2写

为什么volatile不能保证原子性

i++

大家一起读,一起加一,就看谁提交的快了。提交快的直接让另一个计算失效

比如说你在计算的时候,别的线程已经提交了,所以你的计算直接失效了

本来是6 变成5

总结:第二个线程在第一个线程读取旧值和写回新值期间读取i的阈值,也就造成了线程安全问题

内存屏障的四大指令

读屏障
在每个volatile读操作的后面插入一个LoadLoad屏障
在每个volatile读操作的后面插入一个LoadStore屏障

写屏障
在每个volatile写操作的前面插入一个StoreStore屏障
在每个volatile写操作的后面插入一个StoreLoad屏障

说说 synchronized 关键字和 volatile 关键字的区别

互补 存在

1、volatile 只能用于变量,syn可以变量、方法、类级别、代码块

2、volatile 关键字能保证数据的可⻅性,但不能保证数据的原⼦性。 synchronized 关键字两者都能保证

3、volatile 不会造成线程的阻塞;synchronized 可能会造成线程的阻塞

4、volatile 关键字主要⽤于解决变量在多个线程之间的可⻅性,⽽ synchronized 关键字解决的是多个线程之间访问资源的同步性

5、volatile 本质是告诉当前变量工作内存中的值是不确定的需要从主存中读取; synchronized 则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。

策略

是读用volatile,写用synchronized可以提高性能

案例

DCL双锁案例 多线程指令重排 new 一个对象 空间–对象 --对象指向空间 解决:volatile

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DOeMoAdp-1681383922668)(https://raw.githubusercontent.com/viacheung/img/main/image/image-20230410170353957.png)]

凭什么我们java写了一个volatile关键字系统底层加入内存屏障?两者关系怎么勾搭上的?

字节码层面javap -c xx.class
它其实添加了一个ACC_VOLATILE

CAS

cas原理

CAS (CompareAndSwap) CAS有3个操作数,位置内存值V,旧的预期值A,要修改的更新值B。 当且仅当旧的预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做或重来*当它重来重试的这种行为成为—自旋(do while)!

硬件级别保证

对总线加锁,效率比synchronized效率高

JDK提供的非阻塞原子性操作 Unsafe提供的CAS方法`(如compareAndSwapXXX)底层实现即为CPU指令cmpxchg。

CAS cpu并发原语 原子操作

AtomicInteger 类主要利用CAS (compare and swap) + volatile和 native 方法来保证原子操作,从而避免 synchronized 的高开销,执行效率大为提升。

执行cmpxchg指令的时候,会判断当前系统是否为多核系统,如果是就给总线加锁* ,**只有一个线程会对总线加锁成功* ,加锁成功之后会执行cas操作,也就是说CAS的原子性实际上是**CPU实现的

unsafe

Unsafe类中的方法都直接调用操作系统底层资源执行相应任务

自旋锁

尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试*获取锁

问题

1. ABA 问题

并发环境下,假设初始条件是A,去修改数据时,发现是A就会执行修改。但是看到的虽然是A,中间可能发生了A变B,B又变回A的情况。此时A已经非彼A,数据即使成功修改,也可能有问题。

1、时间戳 2、版本号

可以通过AtomicStampedReference解决ABA问题,它,一个带有标记的原子引用类,通过控制变量值的版本来保证CAS的正确性。

2. 循环时间长开销

自旋CAS,如果一直循环执行,一直不成功,会给CPU带来非常大的执行开销。

很多时候,CAS思想体现,是有个自旋次数的,就是为了避开这个耗时问题~

3. 只能保证一个变量的原子操作。

CAS 保证的是对一个变量执行操作的原子性,如果对多个变量操作时,CAS 目前无法直接保证操作的原子性的。

可以通过这两个方式解决这个问题

  • 使用互斥锁来保证原子性;
  • 将多个变量封装成对象,通过AtomicReference来保证原子性。

ThreadLocal

ThreadLocal,即线程本地变量。如果你创建了一个ThreadLocal变量,那么访问这个变量的每个线程都会有这个变量的一个本地拷贝,多个线程操作这个变量的时候,实际是操作自己本地内存里面的变量,从而起到线程隔离的作用,避免了线程安全问题。

ThreadLocal的应用场景有

  • 数据库连接池
  • 会话管理中使用

ThreadLocal 了解么?

  • Thread类有一个类型为ThreadLocal.ThreadLocalMap的实例变量threadLocals,即每个线程都有一个属于自己的ThreadLocalMap。

每⼀个线程都有⾃⼰的专属本地变量

可以使⽤ get 和 set ⽅法来获取默认值或将其值更改为当前线程所存的副本的值

最终变量存在ThreadLocalMap 中,key 为当前对象的 Thread 对象,值为 Object 对象 (ThreadLocalMap 是 ThreadLocal 的静态内部类。 )

ThreadLocal 内存泄露问题了解不?

ThreadLocalMap 中使⽤的 key 为 ThreadLocal 的弱引⽤,⽽ value 是强引⽤。

GC的时候,key变成Null,value无了,ThreadLocalMap 实现中已经考虑了这种情况,在调⽤ set() 、 get() 、 remove() ⽅法的时候,会清理掉 key 为 null的记录。使⽤完 ThreadLocal ⽅法后 最好⼿动调⽤ remove() ⽅法 最好需要手动调用remove方法。

ReentrantLock

了解ReentrantLock吗?

ReetrantLock是一个可重入的独占锁,主要有两个特性,一个是支持公平锁和非公平锁,一个是可重入。 ReetrantLock实现依赖于AQS(AbstractQueuedSynchronizer)。

ReetrantLock主要依靠AQS维护一个阻塞队列,多个线程对加锁时,失败则会进入阻塞队列。等待唤醒,重新尝试加锁。

ReadWriteLock是什么?

一句话 读读不影响

首先ReentrantLock某些时候有局限,如果使用ReentrantLock,可能本身是为了防止线程A在写数据、线程B在读数据造成的数据不一致,但这样,如果线程C在读数据、线程D也在读数据,读数据是不会改变数据的,没有必要加锁,但是还是加锁了,降低了程序的性能。

因为这个,才诞生了读写锁ReadWriteLock。ReadWriteLock是一个读写锁接口,ReentrantReadWriteLock是ReadWriteLock接口的一个具体实现,实现了读写的分离,读锁是共享的,写锁是独占的,读和读之间不会互斥,读和写、写和读、写和写之间才会互斥,提升了读写的性能

为什么要⽤线程池?

(线程池、数据库连接池、 Http 连接池 )

主要是为了减少每次获取资源的消耗,提⾼对资源的利⽤率。

1、降低资源消耗:服用线程

2、提高速度:任务来的时候不用创建直接执行

3、使⽤线程池可以进⾏统⼀的分配,调优和监控。

Atomic

介绍⼀下 Atomic 原⼦类

具有原⼦/原⼦操作特征的类

JUC 包中的原⼦类是哪 4 类?

4个类型 基本 数组 引用 属性修改

基本:Integer Long Boolean

数组: 后面加个Array

引用:去掉基本类型 加reference stampleReference

属性修改:基本后面加FileldUpdater

基本类型

AtomicInteger :整形原⼦类
AtomicLong :⻓整型原⼦类
AtomicBoolean :布尔型原⼦类

数组类型

使⽤原⼦的⽅式更新数组⾥的某个元素
AtomicIntegerArray :整形数组原⼦类
AtomicLongArray :⻓整形数组原⼦类
AtomicReferenceArray :引⽤类型数组原⼦类

引⽤类型

AtomicReference :引⽤类型原⼦类
AtomicStampedReference :原⼦更新带有版本号的引⽤类型。该类将整数值与引⽤关联起来,可⽤于解决原⼦的更新数据和数据的版本号,可以解决使⽤ CAS 进⾏原⼦更新时可能出现的 ABA 问题。
AtomicMarkableReference :原⼦更新带有标记位的引⽤类型

对象的属性修改类型

AtomicIntegerFieldUpdater :原⼦更新整形字段的更新器
AtomicLongFieldUpdater :原⼦更新⻓整形字段的更新器
AtomicReferenceFieldUpdater :原⼦更新引⽤类型字段的更新器

Longadder

AtomicLong
线程安全,可允许一些性能损耗,要求高精度时可使用
AtomicLong是多个线程针对单个热点值value进行原子操作
LongAdder
当需要在高并发下有较好的性能表现,且对值的精确度要求不高时,可以使用
保证性能,精度代价
LongAdder是每个线程拥有自己的槽,各个线程一般只对自己槽中的那个值进行CAS操作
小总结
AtomicLong
原理:
CAS+自旋
incrementAndGet
场景:
低并发下的全局计算
AtomicLong能保证并发情况下计数的准确性,其内部通过CAS来解决并发安全性的问题
缺陷
高并发后性能急剧下降
why?AtomicLong的自旋会称为瓶颈(N个线程CAS操作修改线程的值,每次只有一个成功过,其它N -
1失败,失败的不停的自旋直到成功,这样大量失败自旋的情况,一下子cpu就打高了。)
LongAdder
原理
CAS+Base+Cell数组分散
空间换时间并分散了热点数据
场景
高并发的全局计算
缺陷
sum求和后还有计算线程修改结果的话,最后结果不够准确

AQS

AQS 了解么?

AbstractQueuedSynchronizer

AbstractQueuedSynchronizer(AQS)提供了一套可用于实现锁同步机制的框架,不夸张地说,AQSJUC同步框架的基石。AQS通过一个FIFO队列维护线程同步状态,实现类只需要继承该类,并重写指定方法即可实现一套线程同步机制。

AQS根据资源互斥级别提供了独占和共享两种资源访问模式;同时其定义Condition结构提供了wait/signal等待唤醒机制。在JUC中,诸如ReentrantLockCountDownLatch等都基于AQS实现。

抽象(基石)队列Queue()同步器

AQS原理概况

如果当前线程访问的资源空闲,将线程设置为有效工作线程,否则需要一套线程阻塞等待唤醒的机制,这个机制是由CLH队列(双向队列)实现的,即将暂时获取不到锁的线程加⼊到队列中。

AQS的原理并不复杂,AQS维护了一个volatile int state变量和一个CLH(三个人名缩写)双向队列,队列中的节点持有线程引用,每个节点均可通过getState()setState()compareAndSetState()state进行修改和访问。

当线程获取锁时,即试图对state变量做修改,如修改成功则获取锁;如修改失败则包装为节点挂载到队列中,等待持有锁的线程释放锁并唤醒队列中的节点。

img

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-U5JqeMXD-1681383922669)(https://raw.githubusercontent.com/viacheung/img/main/image/image-20230310225028080.png)]

AQS 对资源的共享⽅式

  1. 独占

如reentrantLock

分为非公平(谁抢到是谁)和公平(排队顺序)

  1. 共享

CountDownLatch 、 Semaphore

ReentrantReadWriteLock 可以看成是组合式,因为 ReentrantReadWriteLock 也就是读写锁允许多个线程同时对某⼀资源进⾏读。

AQS 底层使⽤了模板⽅法模式

  1. ⾃定义同步器时 ,使⽤者继承 AbstractQueuedSynchronizer 并重写指定的⽅法

  2. 将AQS组合在自定义同步组件的实现中,并调用其模板方法,而这些模板方法会调用使用者重写的方法。

  3. AQS使用了模板方法模式,自定义同步器时需要重写下面几个AQS提供的模板方法:

    isHeldExclusively()//该线程是否正在独占资源。只有用到condition才需要去实现它。
    tryAcquire(int)//独占方式。尝试获取资源,成功则返回true,失败则返回false。
    tryRelease(int)//独占方式。尝试释放资源,成功则返回true,失败则返回false。
    tryAcquireShared(int)//共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
    tryReleaseShared(int)//共享方式。尝试释放资源,成功则返回true,失败则返回false。
    

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lMdz640Y-1681383922670)(https://raw.githubusercontent.com/viacheung/img/main/image/image-20230310225906988.png)]

⽤过 CountDownLatch 么?什么场景下⽤的?

允许 count 个线程阻塞在⼀个地⽅,直⾄所有线程的任务都执⾏完毕。

Service:十个service注入,多线程

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3ZFLdqbh-1681383922670)(https://raw.githubusercontent.com/viacheung/img/main/image/image-20230410170931418.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YWbUU90c-1681383922671)(https://raw.githubusercontent.com/viacheung/img/main/image/image-20230410170907476.png)]

[AQS 组件了解吗?

  • Semaphore(信号量)-允许多个线程同时访问: synchronized 和 ReentrantLock 都是一次只允许一个线程访问某个资源,Semaphore(信号量)可以指定多个线程同时访问某个资源。
  • CountDownLatch (倒计时器): CountDownLatch是一个同步工具类,用来协调多个线程之间的同步。这个工具通常用来控制线程等待,它可以让某一个线程等待直到倒计时结束,再开始执行。
  • CyclicBarrier(循环栅栏): CyclicBarrier 和 CountDownLatch 非常类似,它也可以实现线程间的技术等待,但是它的功能比 CountDownLatch 更加复杂和强大。主要应用场景和 CountDownLatch 类似。CyclicBarrier 的字面意思是可循环使用(Cyclic)的屏障(Barrier)。它要做的事情是,让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活。CyclicBarrier默认的构造方法是 CyclicBarrier(int parties),其参数表示屏障拦截的线程数量,每个线程调用await方法告诉 CyclicBarrier 我已经到达了屏障,然后当前线程被阻塞。

JVM

介绍下 Java 内存区域(运⾏时数据区)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vylwFlGD-1681383922672)(https://raw.githubusercontent.com/viacheung/img/main/image/image-20230324202124065.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MB3AVm5q-1681383922673)(https://raw.githubusercontent.com/viacheung/img/main/image/image-20230311003647862.png)]

线程私有:

程序计数器 本地方法栈 虚拟机栈

线程共享:

堆 方法区 直接内存

程序计数器

线程切换恢复

作用:代码流程控制 (多线程)记录恢复

当前虚拟机正在执行的线程指令地址

虚拟机栈

Java的基于栈指令。指令集小,编译器容易实现,缺点是性能下降,实现同样的功能需要更多的指令。

在Hotspot JVM里,每个线程都与操作系统的本地线程直接映射。

栈帧:局部变量表 操作数栈 动态链接 方法返回值

stackoverflow

局部变量表:各种数据类型 对象引用类型(指向对象的引用指针或者句柄)

调用进栈 结束(return或者异常)出栈

引申:

方法参数在哪个位置? 局部变量表

栈溢出? 当线程请求的栈深度超过了虚拟机允许的最大深度时,会抛出StackOverFlowError异常,方法递归调用肯可能会出现该问题; 3、调整参数-xss去调整jvm栈的大小

栈vs堆

1、申请空间:堆:空闲链表

本地方法栈

hotspot与虚拟机栈合1

和虚拟机栈不同的是:虚拟机栈执行的Java方法,本地方法栈是Native方法

线程共享 存放对象实例和数组

栈上分配:方法中对象没有返回,也就是没有被外界使用 那么栈上分配

新生代(eden suviver01(from to)) 老年代

eden先分配 一次回收后还存好 进s0或s1 对象年龄加一 年龄到15(–XX:MaxTenuringThreshold)

1.8之后方法区被原空间替换 原空间使用直接内存

MaxTenuringThreshold:当小于某个年龄的对象占survivor一半的时候,取这个年龄和MaxTenuringThreshold最小的那个

⽅法区

非堆

类信息、常量、静态变量、即时编译器编译后的代码数据

1.8之后分为两部分:1、加载的类信息(保存在元数据区),2、运行时常量(保存在堆中)

方法区是一个概念 永久代 原空间是实现

–XX:PermSize

-XX:MetaspaceSize

为什么要将永久代 (PermGen) 替换为元空间 (MetaSpace) 呢?

1、因为原空间用的是本地内存,溢出几率低

2、加载类变多

运行时常量池

方法区一部分

class文件:方法 接口 常量池表 编译器生成字面量、符号引用

1.8之后运行时常量池在堆里面

JVM 常量池中存储的是对象还是引⽤呢 ? 引用

直接内存

NIO:基于通道和缓存区

java堆里面DiectByteBuffer作为Native堆外内存的引用,避免Java堆和Native堆的切换

JMM内存模型

明确指定了一组排序规则,来保证线程间的可见性

即:要想保证 B 操作能够看到 A 操作的结果(无论它们是否在同一个线程),那么 A 和 B 之间必须满足 Happens-Before 关系

  • 线程规则:一个线程中的每个动作都 happens-before 该线程中后续的每个动作

  • 监视器锁定规则:监听器的解锁动作 happens-before 后续对这个监听器的锁定动作 加锁----解锁

  • volatile 变量规则:对 volatile 字段的写入动作 happens-before 后续对这个字段的每个读取动作 写----读

  • 线程 start 规则:线程 start() 方法的执行 happens-before 一个启动线程内的任意动作 start

  • 线程 join 规则:一个线程内的所有动作 happens-before 任意其他线程在该线程 join() 成功返回之前 Join在后面

  • 传递性:如果 A happens-before B, 且 B happens-before C, 那么 A happens-before C 传递

  • [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TXU6r5gB-1681383922674)(https://raw.githubusercontent.com/viacheung/img/main/image/image-20210329222941923.png)]

    可以这样理解,线程1释放锁退出同步块,线程2加锁进入同步块,那么线程2就能看见线程1对共享对象修改的结果。

    所以说,Java 内存模型描述的是多线程对共享内存修改后彼此之间的可见性

什么是类加载?类加载的过程?

虚拟机把描述类的数据加载到内存里面,并对数据进行校验、解析和初始化,最终变成可以被虚拟机直接使用的class对象;

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZvNMpRDZ-1681383922675)(https://raw.githubusercontent.com/viacheung/img/main/image/image-20210329231258940.png)]

加载、验证、准备、初始化和卸载这5个阶段的顺序是确定的,类的加载过程必须按照这种顺序按部就班地开始,而解析阶段则不一定:它在某些情况下可以在初始化阶段之后再开始,这是为了支持Java语言的运行时绑定(也称为动态绑定或晚期绑定)

类加载过程如下:

  1. 加载,加载分为三步: 1、通过类的全限定性类名获取该类的二进制流; 2、将该二进制流的静态存储结构转为方法区的运行时数据结构; 3、在堆中为该类生成一个class对象;
  2. 验证:验证该class文件中的字节流信息复合虚拟机的要求,不会威胁到jvm的安全;(文件格式 字节码 符号引用 元数据(从信息资源中抽取出来的用于说明其特征、内容的结构化的数据)验证)
  3. 准备:类变量赋0值 (除了final) 分配在方法区 这里不会为实例变量分配初始化值
  4. 解析:该阶段主要完成符号引用转化成直接引用;
  5. 初始化:到了初始化阶段,才开始执行类中定义的java代码;初始化阶段是调用类构造器的过程;

总结:加载 验证(是不是对的) 准备 (类变量赋初始值)解析(符号引用–直接引用) 初始化 (构造器)

类加载器

什么是类加载器,常见的类加载器有哪些?

类加载器是指:通过一个类的全限定性类名获取该类的二进制字节流叫做类加载器;类加载器分为以下四种:

  • 启动类加载器(也叫引导类加载器)(BootStrapClassLoader):用来加载java核心类库,无法被java程序直接引用;
  • 扩展类加载器(Extension ClassLoader):用来加载java的扩展库,java的虚拟机实现会提供一个扩展库目录,该类加载器在扩展库目录里面查找并加载java类;
  • 系统类加载器(AppClassLoader):它根据java的类路径来加载类,一般来说,java应用的类都是通过它来加载的;
  • 自定义类加载器:由java语言实现,继承自ClassLoader;(隔离加载类 修改类加载方式 )

总结:BootStrapClassLoader ,ExtensionClassLoader , ApplicationClassLoader

启动类 扩展类 应用程序类

记给定链表的长度为 ,注意到当向右移动的次数 k≥n 时,我们仅需要向右移动 k mod n次即可。因为每 nnn 次移动都会让链表变为原状。这样我们可以知道,新链表的最后一个节点为原链表的第 (n−1)−(k mod n)个节点(从 000 开始计数)。

这样,我们可以先将给定的链表连接成环,然后将指定位置断开。

具体代码中,我们首先计算出链表的长度 nnn,并找到该链表的末尾节点,将其与头节点相连。这样就得到了闭合为环的链表。然后我们找到新链表的最后一个节点(即原链表的第 (n−1)−(k mod n)个节点),将当前闭合为环的链表断开,即可得到我们所需要的结果。

特别地,当链表长度不大于 1,或者 k 为 n 的倍数时,新链表将与原链表相同,我们无需进行任何处理。

作者:力扣官方题解
链接:https://leetcode.cn/problems/rotate-list/solutions/681812/xuan-zhuan-lian-biao-by-leetcode-solutio-woq1/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

他们什么关系?

包含关系 不是上层下层 也不是子系统的继承关系

双亲委派机制

当一个类收到加载类请求,首先不会尝试自己去加载,而是将这个请求委派给父类加载器去加载,只有父类加载器在自己的搜索范围类查找不到给类时,子加载器才会尝试自己去加载该类

沙箱安全机制

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6rEGxK7E-1681383922676)(https://raw.githubusercontent.com/viacheung/img/main/image/image-20230324201252158.png)]

打破双亲委派的例子

打破: 自定义类加载器重写loadclass

Tomcat,应用的类加载器优先自行加载应用目录下的 class,并不是先委派给父加载器,加载不了才委派给父加载器。

如何判断两个class对象是否相同

在JVM中表示两个class对象是否为同一个类存在两个必要条件:
1、类的完整类名必须一致,包括包名。
2、加载这个类的ClassLoader(指ClassLoader实例对象)必须相同

说⼀下Java对象的创建过程

类加载检查–分配内存—初始化0值–设置对象头(juc的java内存布局 对象头 实例数据 对齐填充)–init方法

Step1:类加载检查

先看有么有被加载 加载过了就不加载 没加载再加载

遇到⼀条 new 指令时,⾸先检查是否能在常量池中定位到这个类的符号引⽤,检查这个类是否已被加载过、解析和初始化过。如果没有,那必须先执⾏相应的类加载过程

Step2:分配内存

对象所需的内存⼤⼩在类加载完成后便可确定,为对象分配空间的任务等同于把⼀块确定⼤⼩的内存从 Java 堆中划分出来。 分配⽅式有 “指针碰撞”(复制 标记压缩) 和 “空闲列表” (标记清除)两种, 选择哪种分配⽅式由 Java 堆是否规整决定,⽽ Java堆是否规整⼜由所采⽤的垃圾收集器是否带有压缩整理功能决定。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Pwsb4rYe-1681383922677)(https://raw.githubusercontent.com/viacheung/img/main/image/image-20230311204655913.png)]

Step3:将除对象头外的对象内存空间初始化为0

Step4:对对象头进行必要设置

内存分配并发问题

1、CAS+失败重试: 虚拟机采⽤ CAS配上失败重试的⽅式保证更新操作的原⼦性。
TLAB: 为每⼀个线程(做了线程隔离)预先在 Eden 区分配⼀块⼉内存, JVM给对象分配内存时候,先TLAB分配,不够了再用CAS+失败重试

初始化零值

初始化0值

设置对象头

hashcode Gc信息 对象标记 (mark word) 类元信息(class point)

执⾏ init ⽅法

按照代码逻辑初始化

对象的访问定位有哪两种⽅式?

1、句柄

堆有个句柄池 存了实例对象和类的信息

2、直接指针

存储的直接就是对象的地址

比较:

指针速度快 只需要一次定位即可

句柄能保证引用指向不会改,只改句柄里面实例数据的地址就可

简单聊聊 JVM 内存分配与回收

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GIIJSnbG-1681383922678)(https://raw.githubusercontent.com/viacheung/img/main/image/image-20210329225348086.png)]

eden先分配 一次回收后还存活 进s0或s1 对象年龄加一 年龄到15(–XX:MaxTenuringThreshold 可以配)

  • 在 Eden 区执行了第一次 GC 之后,存活的对象会被移动到其中一个 Survivor 分区;

  • Eden 区再次 GC,这时会采用复制算法,将 Eden 和 from 区一起清理,存活的对象会被复制到 to 区;

MaxTenuringThreshold:当小于某个年龄的对象占survivor一半的时候,取这个年龄和MaxTenuringThreshold最小的那个

一次Gc后,Eden和From空了,然后FROM和TO交换,保证To区空,当To区空,将对象移动到老年代

  • 动态对象年龄判定:Survivor 区相同年龄所有对象大小的总和 > (Survivor 区内存大小 * 这个目标使用率)时,大于或等于该年龄的对象直接进入老年代。其中这个使用率通过 -XX:TargetSurvivorRatio 指定,默认为 50%;

  • Survivor 区内存不足会发生担保分配,超过指定大小的对象可以直接进入老年代。

介绍下空间分配担保原则?

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Vkwq10YO-1681383922679)(https://raw.githubusercontent.com/viacheung/img/main/image/image-20210329230240201.png)]

何时FULLGC

1. System.gc()方法的调用(系统建议执行,但是不必然执行)

2. 老年代不足

3. 永久代不足

4. concurrent mode failure

5**.minor gc时年轻代的存活区空间不足而晋升老年代,老年代又空间不足而触发full gc。**

6. 统计得到的Minor GC晋升到旧生代的平均大小大于老年代的剩余空间

Minor Gc?

**Minor GC触发条件:**当Eden区满时,触发Minor GC。

堆内存中对象的分配的基本策略

对象优先eden

分配担保机制:eden满了触发MinorGc survivor存不下 直接进老年代

大对象直接进老年代

字符串 数组

长期存活的从年轻代到进老年代(15)

GC分类

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1vkqAh05-1681383922680)(https://raw.githubusercontent.com/viacheung/img/main/image/image-20230311222032342.png)]

如何判断对象是否死亡?(两种⽅法)

引⽤计数法

对象里有一个引用计数器,当有引用指向 +1,为0就是不再使用的

可达性分析算法

GC Roots的对象作为起点 ,向下搜索,没有与GcRoots相连或间接的对象就是不可用

GCRoots:

  • 虚拟机栈中引用的对象
  • 本地方法栈引用的对象
  • 方法区类静态属性引用的变量
  • 方法区常量池引用的对象

但一个对象满足上述条件的时候,不会马上被回收,还需要进行两次标记

第一次标记:判断当前对象是否有finalize()方法并且该方法没有被执行过,若不存在则标记为垃圾对象,等待回收;若有的话,则进行第二次标记;第二次标记将当前对象放入F-Queue队列,并生成一个finalize线程去执行该方法,虚拟机不保证该方法一定会被执行,这是因为如果线程执行缓慢或进入了死锁,会导致回收系统的崩溃;如果执行了finalize方法之后仍然没有与GC Roots有直接或者间接的引用,则该对象会被回收;

四种引用

强软弱虚

1、内存不足也不回收

2、内存不足即回收

3、GC就回收

4、与引用队列一起用 目的是这个对象回收的时候收到一个通知然后做进一步处理(比finalize更灵活)

软弱 可以用来存图片 HashMap 存路径和对象 内存不足 回收掉!具体选软还是弱看具体内存空间

如何判断⼀个常量是废弃常量?

没有对象引用

如何判断⼀个类是⽆⽤的类?

1、没有实例

3、类加载器回收了

2、没有被引用

垃圾收集有哪些算法,各⾃的特点?

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SaGuvcrW-1681383922681)(https://raw.githubusercontent.com/viacheung/img/main/image/image-20210329224002527.png)]

标记清除:cms

第一步:利用可达性去遍历内存,把存活对象和垃圾对象进行标记; 第二步:在遍历一遍,将所有标记的对象回收掉; 特点:效率不行,标记和清除的效率都不高;标记和清除后会产生大量的不连续的空间分片,可能会导致之后程序运行的时候需分配大对象而找不到连续分片而不得不触发一次GC;

缺点:内存碎片 效率

复制算法

内存一分为2 复制存活对象到一块 剩下一块直接清除

缺点:利用率只有一半

优点:没空间碎片

标记-整理算法

标记清除基础上进行整理

第一步:利用可达性去遍历内存,把存活对象和垃圾对象进行标记; 第二步:将所有的存活的对象向一段移动,将端边界以外的对象都回收掉; 特点:适用于存活对象多,垃圾少的情况;需要整理的过程,无空间碎片产生;

分代收集算法

⽐如在新⽣代中,每次收集都会有⼤量对象死去,所以可以选择复制算法,只需要付出少量对象的复制成本(因为移动的是存活对象)就可以完成每次垃圾收集。

⽽⽼年代的对象存活⼏率是⽐较⾼的,⽽且没有额外的空间对它进⾏分配担保,所以我们必须选择“标记-清除”或“标记-整理”算法进⾏垃圾收集

常⻅的垃圾回收器有那些?

性能指标:吞吐量(服务端) 暂停时间(响应)(客户端)

1、serial(Serial Old) :串行 stop the world cpu 单线程

收集垃圾时,必须stop the world,使用复制算法。它的最大特点是在进行垃圾回收时,需要对所有正在执行的线程暂停(stop the world),对于有些应用是难以接受的,但是如果应用的实时性要求不是那么高,只要停顿的时间控制在N毫秒之内,大多数应用还是可以接受的,是client级别的默认GC方式。

2、parnew 并行 Serial收集器的多线程版本,也需要stop the world

3、parallel(Parallel Old) Scavenge:并行 优先吞吐 服务端

新生代收集器,复制算法的收集器,并发的多线程收集器,目标是达到一个可控的吞吐量,和ParNew的最大区别是GC自动调节策略;虚拟机会根据系统的运行状态收集性能监控信息,动态设置这些参数,以提供最优停顿时间和最高的吞吐量;

4、cms

是⼀种以获取最短回收停顿时间为⽬标的收集器。它⾮常符合在注重⽤户体验的应⽤上使⽤。第⼀款真正意义上的并发收集器

cms 主打低延迟

步骤:初始标记–并发标记–重新标记–并发清楚 ,收集结束会产生大量空间碎片;

主要优点: 并发收集、低停顿。

缺点:
对 CPU 资源敏感;
⽆法处理浮动垃圾;
它使⽤的回收算法-“标记-清除”算法会导致收集结束时会有⼤量空间碎⽚产⽣。

cms为啥不用标记整理(并发)

5、G1 (Garbage-First) ⾯向服务器,主要针对配备多颗处理器及⼤容量内存的机器. 以极⾼概率满⾜ GC 停顿时间要求的同时,还具备⾼吞吐量性能特征

特点 并行并发 分代收集 整体标记整理 局部复制 可预测的停顿时间模型

G1 步骤
初始标记
并发标记
最终标记
筛选回收

G1 收集器在后台维护了⼀个优先列表,每次根据允许的收集时间,优先选择回收价值最⼤的
Region(这也就是它的名字 Garbage-First 的由来)

CMS回收过程

  1. 初始标记 (CMS initial mark):主要是标记 跟 GC Root 直接关联的下级对象,这个过程会 STW,但是跟 GC Root 直接关联的下级对象不会很多,因此这个过程其实很快。
  2. 并发标记 (CMS concurrent mark):根据上一步的结果,继续向下标识所有关联的对象,直到这条链上的最尽头。这个过程是多线程(和用户线程一起)的,虽然耗时理论上会比较长,但是其它工作线程并不会阻塞,没有 STW。
  3. 重新标记(CMS remark):顾名思义,就是要再标记一次。为啥还要再标记一次?因为第 2 步并没有阻塞其它工作线程,其它线程在标识过程中,很有可能会产生新的垃圾。
  4. 并发清除(CMS concurrent sweep):清除阶段是清理删除掉标记阶段判断的已经死亡的对象,由于不需要移动存活对象,所以这个阶段也是可以与用户线程同时并发进行的。

CMS 的问题:

1. 并发回收导致CPU资源紧张:

在并发阶段,它虽然不会导致用户线程停顿,但却会因为占用了一部分线程而导致应用程序变慢,降低程序总吞吐量。

2. 无法清理浮动垃圾:

并发清除阶段,用户线程还在继续运行,就还会伴随有新的垃圾对象不断产生,只好留到下一次垃圾收集时再清理掉。这一部分垃圾称为“浮动垃圾”。

3、碎片问题

jvm调优

命令:

  • jps:查HotSpot虚拟机进程id。
  • jstat:监视虚拟机运行时状态信息的命令
  • jmap:用于生成heap dump文件,内存映像
  • jhat:用来分析jmap生成的dump
  • jstack:jstack用于生成java虚拟机当前时刻的线程快照。知道java程序是如何崩溃和在程序何处发生问题。

通常来说,我们的 JVM 参数配置大多还是会遵循 JVM 官方的建议,例如:

  • -XX:NewRatio=2,年轻代:老年代=1:2
  • -XX:SurvivorRatio=8,eden:survivor=8:1
  • 堆内存设置为物理内存的3/4左右

典型的有:死循环、使用无界队列。
不合理的JVM参数配置:优化 JVM 参数配置。典型的有:年轻代内存配置过小、堆内存配置过小、元空间配置过小

JConsole Jprolie dump文件

JDK8

支持 Lamda 表达式、集合的 stream 操作、提升HashMap性能

线上故障排查

1、硬件故障排查

如果一个实例发生了问题,根据情况选择,要不要着急去重启。如果出现的CPU、内存飙高或者日志里出现了OOM异常

第一步是隔离(nginx 权重为0),第二步是保留现场(网络 IO cpu ),第三步才是问题排查(ThreadLocal里面的GC Roots,内存泄漏的根本就是,这些对象并没有切断和 GC Roots 的关系,可通过一些工具,能够看到它们的联系。)

CPU飙高

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pWyUf5AG-1681383922681)(https://raw.githubusercontent.com/viacheung/img/main/image/image-20230324212018061.png)]

计网

计算机网络的各层协议及作用?

基础

应用层:应用进程之间交互 DNS(域名系统) HTTP(超文本传输协议) SMTP(电子邮件)

表示层:数据格式的转换,如加密解密、转换翻译、压缩解压缩等。

会话层:网络中的两节点之间建立、维持和终止通信,如服务器验证用户登录

运输层:为应用层提供通用数据传输服务 TCP(面向连接 可靠) UDP(⽆连接 不保证数据传输的可靠性 )

网络层:选择合适路由节点 确保数据及时发送 IP协议

数据链路层:把网络层交付下来的ip数据报组装为帧 在两个相邻节点间链路传送帧(发数据) 帧包括数据+控制信息(同步信息、地址信息、差错控制–丢失帧)

物理层:数据单位bt 实现相邻节点比特流的透明传输 尽量屏蔽传输介质和物理设备差异

为什么分层?

1、易于实现和标准化各层独立,就可以把大问题分割成多个小问题,利于实现;

2、灵活性好:如果某一层发生变化,只要接口不变,不会影响其他层;

3、分层后,用户只关心用到的应用层,其他层用户可以复用;
4、各层之间相互独立:高层不需要知道底层的功能是采取硬件来实现的,只需要知道通过底层的接口来获得所需要的服务。

例子?

三层架构和MVC

三次握手 (建立连接)

三次握手机制:

  • 第一次握手:客户端请求建立连接,向服务端发送一个同步报文(SYN=1),同时选择一个随机数 seq = x 作为初始序列号,并进入SYN_SENT状态,等待服务器确认。
  • 第二次握手::服务端收到连接请求报文后,如果同意建立连接,则向客户端发送同步确认报文(SYN=1,ACK=1),确认号为 ack = x + 1,同时选择一个随机数 seq = y 作为初始序列号,此时服务器进入SYN_RECV状态。
  • 第三次握手:客户端收到服务端的确认后,向服务端发送一个确认报文(ACK=1),确认号为 ack = y + 1,序列号为 seq = x + 1,客户端和服务器进入ESTABLISHED状态,完成三次握手。

理想状态下,TCP连接一旦建立,在通信双方中的任何一方主动关闭连接之前,TCP 连接都将被一直保持下去。

确认号=序列号+1;

总结:

1、客户端发送带有syn(请求同步)的数据包到服务端 选择一个随机数seq=x

服务端发送带有syn+ack的数据包

客户端发送带有ack标志数据包

目的:建立可靠传输,双方确认自己与对方的发送接受都正常

确保双方都是能发能接

为什么需要三次握手,而不是两次?

主要有三个原因:

  1. 防止已过期的连接请求报文突然又传送到服务器,因而产生错误和资源浪费。(A网络错误没到服务器 客户端超时重传B报文段 然后顺利到达 假如说是两次握手,那么现在客户端已经close了,A后面如果再到达服务器 服务器返回确认报文 但此时客户端不会有相应 那么会导致服务端长时间等待 造成资源浪费)

    在双方两次握手即可建立连接的情况下,假设客户端发送 A 报文段请求建立连接,由于网络原因造成 A 暂时无法到达服务器,服务器接收不到请求报文段就不会返回确认报文段。

    客户端在长时间得不到应答的情况下重新发送请求报文段 B,这次 B 顺利到达服务器,服务器随即返回确认报文并进入 ESTABLISHED 状态,客户端在收到 确认报文后也进入 ESTABLISHED 状态,双方建立连接并传输数据,之后正常断开连接。

    此时姗姗来迟的 A 报文段才到达服务器,服务器随即返回确认报文并进入 ESTABLISHED 状态,但是已经进入 CLOSED 状态的客户端无法再接受确认报文段,更无法进入 ESTABLISHED 状态,这将导致服务器长时间单方面等待,造成资源浪费。

  2. 三次握手才能让双方均确认自己和对方的发送和接收能力都正常。

    第一次握手:客户端只是发送处请求报文段,什么都无法确认,而服务器可以确认自己的接收能力和对方的发送能力正常;

    第二次握手:客户端可以确认自己发送能力和接收能力正常,对方发送能力和接收能力正常;

    第三次握手:服务器可以确认自己发送能力和接收能力正常,对方发送能力和接收能力正常;

    可见三次握手才能让双方都确认自己和对方的发送和接收能力全部正常,这样就可以愉快地进行通信了。

  3. 告知对方自己的初始序号值,并确认收到对方的初始序号值。

    TCP 实现了可靠的数据传输,原因之一就是 TCP 报文段中维护了序号字段和确认序号字段,通过这两个字段双方都可以知道在自己发出的数据中,哪些是已经被对方确认接收的。这两个字段的值会在初始序号值得基础递增,如果是两次握手,只有发起方的初始序号可以得到确认,而另一方的初始序号则得不到确认。

为什么要三次握手,而不是四次?

因为三次握手已经可以确认双方的发送接收能力正常,双方都知道彼此已经准备好,而且也可以完成对双方初始序号值得确认,也就无需再第四次握手了。

  • 第一次握手:服务端确认“自己收、客户端发”报文功能正常。
  • 第二次握手:客户端确认“自己发、自己收、服务端收、客户端发”报文功能正常,客户端认为连接已建立。
  • 第三次握手:服务端确认“自己发、客户端收”报文功能正常,此时双方均建立连接,可以正常通信。

什么是 SYN洪泛攻击?如何防范?

SYN洪泛攻击属于 DOS 攻击的一种,它利用 TCP 协议缺陷,通过发送大量的半连接请求,耗费 CPU 和内存资源。

原理:

  • 在三次握手过程中,服务器发送 [SYN/ACK] 包(第二个包)之后、收到客户端的 [ACK] 包(第三个包)之前的 TCP 连接称为半连接(half-open connect),此时服务器处于 SYN_RECV(等待客户端响应)状态。如果接收到客户端的 [ACK],则 TCP 连接成功,如果未接受到,则会不断重发请求直至成功。
  • SYN 攻击的攻击者在短时间内伪造大量不存在的 IP 地址,向服务器不断地发送 [SYN] 包,服务器回复 [SYN/ACK] 包,并等待客户的确认。由于源地址是不存在的,服务器需要不断的重发直至超时。
  • 这些伪造的 [SYN] 包将长时间占用未连接队列,影响了正常的 SYN,导致目标系统运行缓慢、网络堵塞甚至系统瘫痪。

检测:当在服务器上看到大量的半连接状态时,特别是源 IP 地址是随机的,基本上可以断定这是一次 SYN 攻击。

防范:

  • 通过防火墙、路由器等过滤网关防护。
  • 通过加固 TCP/IP 协议栈防范,如增加最大半连接数,缩短超时时间。
  • SYN cookies技术。SYN Cookies 是对 TCP 服务器端的三次握手做一些修改,专门用来防范 SYN 洪泛攻击的一种手段。

三次握手连接阶段,最后一次ACK包丢失,会发生什么?

服务端:

(服务端超时重传 重传指定次数后 服务器自动关连接)

  • 第三次的ACK在网络中丢失,那么服务端该TCP连接的状态为SYN_RECV,并且会根据 TCP的超时重传机制,会等待3秒、6秒、12秒后重新发送SYN+ACK包,以便客户端重新发送ACK包。
  • 如果重发指定次数之后,仍然未收到 客户端的ACK应答,那么一段时间后,服务端自动关闭这个连接。

客户端:

(后面发数据 会发现三次握手失败)

客户端认为这个连接已经建立,如果客户端向服务端发送数据,服务端将以RST包(Reset,标示复位,用于异常的关闭连接)响应。此时,客户端知道第三次握手失败。

四次挥手(断开连接)

  • 第一次挥手:客户端向服务端发送连接释放报文(FIN=1,ACK=1),主动关闭连接,同时等待服务端的确认。

    • 序列号 seq = u,即客户端上次发送的报文的最后一个字节的序号 + 1
    • 确认号 ack = k, 即服务端上次发送的报文的最后一个字节的序号 + 1
  • 第二次挥手:服务端收到连接释放报文后,立即发出确认报文(ACK=1),序列号 seq = k,确认号 ack = u + 1。

    这时 TCP 连接处于半关闭状态,即客户端到服务端的连接已经释放了,但是服务端到客户端的连接还未释放。这表示客户端已经没有数据发送了,但是服务端可能还要给客户端发送数据。

  • 第三次挥手:服务端向客户端发送连接释放报文(FIN=1,ACK=1),主动关闭连接,同时等待 A 的确认。

    • 序列号 seq = w,即服务端上次发送的报文的最后一个字节的序号 + 1。
    • 确认号 ack = u + 1,与第二次挥手相同,因为这段时间客户端没有发送数据
  • 第四次挥手:客户端收到服务端的连接释放报文后,立即发出确认报文(ACK=1),序列号 seq = u + 1,确认号为 ack = w + 1。

    此时,客户端就进入了 TIME-WAIT 状态。注意此时客户端到 TCP 连接还没有释放,必须经过 2*MSL(最长报文段寿命)的时间后,才进入 CLOSED 状态。而服务端只要收到客户端发出的确认,就立即进入 CLOSED 状态。可以看到,服务端结束 TCP 连接的时间要比客户端早一些。

总结:

客户端发送一个fin,服务端回一个ack,然后客户端到服务端的数据传送关闭

服务端发送一个fin,客户端回一个ack,然后服务端到客户端的数据传送关闭

目的 :两边确认对方没有要发送的数据

为什么连接的时候是三次握手,关闭的时候却是四次握手?

服务器在收到客户端的 FIN 报文段后,可能还有一些数据要传输,所以不能马上关闭连接,但是会做出应答,返回 ACK 报文段.

接下来可能会继续发送数据,在数据发送完后,服务器会向客户单发送 FIN 报文,表示数据已经发送完毕,请求关闭连接。服务器的ACK和FIN一般都会分开发送,从而导致多了一次,因此一共需要四次挥手。

为什么客户端的 TIME-WAIT 状态必须等待 2MSL ?

主要有两个原因:

  1. 确保 ACK 报文能够到达服务端,从而使服务端正常关闭连接。

    第四次挥手时,客户端第四次挥手的 ACK 报文不一定会到达服务端。服务端会超时重传 FIN/ACK 报文,此时如果客户端已经断开了连接,那么就无法响应服务端的二次请求,这样服务端迟迟收不到 FIN/ACK 报文的确认,就无法正常断开连接。

    MSL 是报文段在网络上存活的最长时间。客户端等待 2MSL 时间,即「客户端 ACK 报文 1MSL 超时 + 服务端 FIN 报文 1MSL 传输」,就能够收到服务端重传的 FIN/ACK 报文,然后客户端重传一次 ACK 报文,并重新启动 2MSL 计时器。如此保证服务端能够正常关闭。

    如果服务端重发的 FIN 没有成功地在 2MSL 时间里传给客户端,服务端则会继续超时重试直到断开连接。

  2. 防止已失效的连接请求报文段出现在之后的连接中。

    TCP 要求在 2MSL 内不使用相同的序列号。客户端在发送完最后一个 ACK 报文段后,再经过时间 2MSL,就可以保证本连接持续的时间内产生的所有报文段都从网络中消失。这样就可以使下一个连接中不会出现这种旧的连接请求报文段。或者即使收到这些过时的报文,也可以不处理它。

如果已经建立了连接,但是客户端出现故障了怎么办?

或者说,如果三次握手阶段、四次挥手阶段的包丢失了怎么办?如“服务端重发 FIN丢失”的问题。

简而言之,通过定时器 + 超时重试机制,尝试获取确认,直到最后会自动断开连接。

具体而言,TCP 设有一个保活计时器。服务器每收到一次客户端的数据,都会重新复位这个计时器,以Linux服务器为例,时间通常是设置为 2 小时。若 2 小时还没有收到客户端的任何数据,服务器就开始重试:每隔 75 秒(默认)发送一个探测报文段,若一共发送 10 个探测报文后客户端依然没有回应,那么服务器就认为连接已经断开了。

附:Linux服务器系统内核参数配置

  1. tcp_keepalive_time,在TCP保活打开的情况下,最后一次数据交换到TCP发送第一个保活探测包的间隔,即允许的持续空闲时长,或者说每次正常发送心跳的周期,默认值为7200s(2h)。
  2. tcp_keepalive_probes 在tcp_keepalive_time之后,没有接收到对方确认,继续发送保活探测包次数,默认值为9(次)
  3. tcp_keepalive_intvl,在tcp_keepalive_time之后,没有接收到对方确认,继续发送保活探测包的发送频率,默认值为75s。 发送频率tcp_keepalive_intvl乘以发送次数tcp_keepalive_probes,就得到了从开始探测到放弃探测确定连接断开的时间; 举例:若设置,服务器在客户端连接空闲的时候,每90秒发送一次保活探测包到客户端,若没有及时收到客户端的TCP Keepalive ACK确认,将继续等待15秒*2=30秒。总之可以在90s+30s=120秒(两分钟)时间内可检测到连接失效与否。

TIME-WAIT 状态过多会产生什么后果?怎样处理?

从服务器来讲,短时间内关闭了大量的Client连接,就会造成服务器上出现大量的TIME_WAIT连接,严重消耗着服务器的资源,此时部分客户端就会显示连接不上。

从客户端来讲,客户端TIME_WAIT过多,就会导致端口资源被占用,因为端口就65536个,被占满就会导致无法创建新的连接。

解决办法:

  • 服务器可以设置 SO_REUSEADDR 套接字选项来避免 TIME_WAIT状态,此套接字选项告诉内核,即使此端口正忙(处于 TIME_WAIT状态),也请继续并重用它。
  • 调整系统内核参数,修改/etc/sysctl.conf文件,即修改net.ipv4.tcp_tw_reuse 和 tcp_timestamps
  • 强制关闭,发送 RST 包越过TIME_WAIT状态,直接进入CLOSED状态。

TIME_WAIT 是服务器端的状态?还是客户端的状态?

TIME_WAIT 是主动断开连接的一方会进入的状态,一般情况下,都是客户端所处的状态;服务器端一般设置不主动关闭连接。

TIME_WAIT 需要等待 2MSL,在大量短连接的情况下,TIME_WAIT会太多,这也会消耗很多系统资源。对于服务器来说,在 HTTP 协议里指定 KeepAlive(浏览器重用一个 TCP 连接来处理多个 HTTP 请求),由浏览器来主动断开连接,可以一定程度上减少服务器的这个问题

详细讲一下TCP的滑动窗口?

在进行数据传输时,如果传输的数据比较大,就需要拆分为多个数据包进行发送。TCP 协议需要对数据进行确认后,才可以发送下一个数据包。这样一来,就会在等待确认应答包环节浪费时间。

为了避免这种情况,TCP引入了窗口概念。窗口大小指的是不需要等待确认应答包而可以继续发送数据包的最大值。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KEdN8ujj-1681383922682)(https://raw.githubusercontent.com/viacheung/img/main/image/image-20210520214432214.png)]

从上面的图可以看到滑动窗口左边的是已发送并且被确认的分组,滑动窗口右边是还没有轮到的分组。

滑动窗口里面也分为两块,一块是已经发送但是未被确认的分组,另一块是窗口内等待发送的分组。随着已发送的分组不断被确认,窗口内等待发送的分组也会不断被发送。整个窗口就会往右移动,让还没轮到的分组进入窗口内。

可以看到滑动窗口起到了一个限流的作用,也就是说当前滑动窗口的大小决定了当前 TCP 发送包的速率,而滑动窗口的大小取决于拥塞控制窗口和流量控制窗口的两者间的最小值。

TCP VS UDP

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1Ta6DuJf-1681383922683)(https://raw.githubusercontent.com/viacheung/img/main/image/image-20230318181625164.png)]

TCP 用于在传输层有必要实现可靠传输的情况,UDP 用于对高速传输和实时性有较高要求的通信。TCP 和 UDP 应该根据应用目的按需使用。

UDP 和 TCP 对应的应用场景是什么?

TCP 是面向连接,能保证数据的可靠性交付,因此经常用于:

  • FTP文件传输
  • HTTP / HTTPS

UDP 面向无连接,它可以随时发送数据,再加上UDP本身的处理既简单又高效,因此经常用于:

  • 包总量较少的通信,如 DNS 、SNMP等
  • 视频、音频等多媒体通信
  • 广播通信

TCP 协议如何保证可靠传输

重传、流量控制、拥塞控制、序列号与确认应达号、校验和

1、合适地分割数据变成数据块

2、给包编号

3、校验和: 通过检验和的方式,接收端可以检测出来数据是否有差错和异常,假如有差错就会直接丢弃TCP段,重新发送

4、丢失重复

5、流量控制 (缓冲空间 利用滑动窗口实现):

如果主机A 一直向主机B发送数据,不考虑主机B的接受能力,则可能导致主机B的接受缓冲区满了而无法再接受数据,从而会导致大量的数据丢包,引发重传机制。而在重传的过程中,若主机B的接收缓冲区情况仍未好转,则会将大量的时间浪费在重传数据上,降低传送数据的效率。所以引入流量控制机制,主机B通过告诉主机A自己接收缓冲区的大小,来使主机A控制发送的数据量。流量控制与TCP协议报头中的窗口大小有关。

6、拥塞控制(拥塞时减少发送):

在数据传输过程中,可能由于网络状态的问题,造成网络拥堵,此时引入拥塞控制机制,在保证TCP可靠性的同时,提高性能。

7、ARQ

8、超时重传(有个定时器,等待接收端发ack,等不到就重发):

超时重传是指发送出去的数据包到接收到确认包之间的时间,如果超过了这个时间会被认为是丢包了,需要重传。最大超时时间是动态计算的。

9、滑动窗口:

滑动窗口既提高了报文传输的效率,也避免了发送方发送过多的数据而导致接收方无法正常处理的异常。

10、序列号/确认应答:

序列号的作用不仅仅是应答的作用,有了序列号能够将接收到的数据根据序列号排序,并且去掉重复序列号的数据。

TCP传输的过程中,每次接收方收到数据后,都会对传输方进行确认应答。也就是发送ACK报文,这个ACK报文当中带有对应的确认序列号,告诉发送方,接收到了哪些数据,下一次的数据从哪里发。

ARQ

⾃动重传请求,它通过使⽤确认和超时这两个机制,在不可靠服务的基础上实现可靠的信息传输。如果发送⽅在发送后⼀段时间之内没有收到确认帧,它通常会重新发送。

ARQ包括停⽌等待ARQ协议和连续ARQ协议

停⽌等待ARQ协议

优点: 简单
缺点: 信道利⽤率低,等待时间⻓

1、⽆差错情况:
发送⽅发送分组,接收⽅在规定时间内收到,并且回复确认.发送⽅再次发送。

2、出现差错情况(超时重传) :
停⽌等待协议中超时重传是指只要超过⼀段时间仍然没有收到确认,就重传前⾯发送过的分组(认为刚才发送过的分组丢失了)。因此每发送完⼀个分组需要设置⼀个超时计时器。

3、确认丢失和确认迟到
确认丢失 :syn --ack丢失 客户端没收到ack 超时计时后 再syn

因此服务端收到俩syn,处理有2种:丢弃这个重复的syn和向A发送
确认消息。

连续ARQ协议

连续 ARQ 协议可提⾼信道利⽤率。发送⽅维持⼀个发送窗⼝,位于窗口内的分组可以连续发送,接受端对到达的最后⼀个分组发
送确认,表明所有分组都已经正确收到了。

优点: 信道利⽤率⾼,容易实现,即使确认丢失,也不必重传。

缺点: 不能向发送⽅反映出接收⽅已经正确收到的所有分组的信息。 ⽐如:发送⽅发送了 5条消息,中间第三条丢失(3号),这时接收⽅只能对前两个发送确认。发送⽅⽆法知道后三个分组的下落,⽽只好把后三个全部重传⼀次。这也叫 Go-Back-N

滑动窗⼝和流量控制

TCP 利⽤滑动窗⼝实现流量控制。流量控制是为了控制发送⽅发送速率,保证接收⽅来得及接收。

拥塞控制

拥塞:某段时间,若对⽹络中某⼀资源的需求超过了该资源所能提供的可⽤部分,⽹络的性能就要变坏。这种情况就叫拥塞。

拥塞控制:拥塞控制就是为了防⽌过多的数据注⼊到⽹络中,这样就可以使⽹络中的路由器或链路不致过载。

vs流量控制:拥塞是全局(涉及到所有主机路由器 )流量是端到端

实操:维持⼀个 拥塞窗⼝

TCP 一共使用了四种算法来实现拥塞控制:

  • 慢开始 (slow-start);
  • 拥塞避免 (congestion avoidance);
  • 快速重传 (fast retransmit);
  • 快速恢复 (fast recovery)。

慢开始:不要一开始就发送大量的数据,由小到大逐渐增加拥塞窗口的大小。(每一个传播轮次加倍)

拥塞避免:拥塞避免算法让拥塞窗口缓慢增长,即每经过一个往返时间RTT就把发送方的拥塞窗口cwnd加1而不是加倍。这样拥塞窗口按线性规律缓慢增长。一个RTT cwnd+1)

快重传:我们可以剔除一些不必要的拥塞报文,提高网络吞吐量。比如接收方在收到一个失序的报文段后就立即发出重复确认,而不要等到自己发送数据时捎带确认。快重传规定:发送方只要一连收到三个重复确认就应当立即重传对方尚未收到的报文段,而不必继续等待设置的重传计时器时间到期。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-W41jJGAH-1681383922684)(https://raw.githubusercontent.com/viacheung/img/main/image/image-20210520214123058.png)]

快恢复:主要是配合快重传。当发送方连续收到三个重复确认时,就执行“乘法减小”算法,把ssthresh门限减半(为了预防网络发生拥塞),但接下来并不执行慢开始算法,因为如果网络出现拥塞的话就不会收到好几个重复的确认,收到三个重复确认说明网络状况还可以。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EUB7DYQH-1681383922685)(https://raw.githubusercontent.com/viacheung/img/main/image/image-20210520214146324.png)]

快重传与快恢复

之前的重传是数据丢失导致接收端收不到数据导致发送端收不到ack,然后超时就会重新发syn,耗时且中间不能发其他数据了

但是有了FRR ,当接收端收到不按序的数据段 连发三个确认 那么发送端就立即重发 不等计时器

HTTP常见的状态码有哪些?

常见状态码:

  • 200:服务器已成功处理了请求。 通常,这表示服务器提供了请求的网页。
  • 301 : (永久移动) 请求的网页已永久移动到新位置。 服务器返回此响应(对 GET 或 HEAD 请求的响应)时,会自动将请求者转到新位置。
  • 302:(临时移动) 服务器目前从不同位置的网页响应请求,但请求者应继续使用原有位置来进行以后的请求。
  • 400 :客户端请求有语法错误,不能被服务器所理解。
  • 403 :服务器收到请求,但是拒绝提供服务。
  • 404 :(未找到) 服务器找不到请求的网页。
  • 500: (服务器内部错误) 服务器遇到错误,无法完成请求。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UmC3ylfp-1681383922686)(https://raw.githubusercontent.com/viacheung/img/main/image/image-20210525114439748.png)]

状态码301和302的区别是什么?

共同点:301和302状态码都表示重定向,就是说浏览器在拿到服务器返回的这个状态码后会自动跳转到一个新的URL地址,这个地址可以从响应的Location首部中获取(用户看到的效果就是他输入的地址A瞬间变成了另一个地址B)。

不同点

301表示旧地址A的资源已经被永久地移除了(这个资源不可访问了),搜索引擎在抓取新内容的同时也将旧的网址交换为重定向之后的网址;

302表示旧地址A的资源还在(仍然可以访问),这个重定向只是临时地从旧地址A跳转到地址B,搜索引擎会抓取新的内容而保存旧的网址。 SEO中302好于301。

补充,重定向原因

  1. 网站调整(如改变网页目录结构);
  2. 网页被移到一个新地址;
  3. 网页扩展名改变(如应用需要把.php改成.Html或.shtml)。

HTTP 常用的请求方式?

  • PUT:上传文件,向服务器添加数据,可以看作增
  • DELETE:删除文件
  • POST:传输数据,向服务器提交数据,对服务器数据进行更新。
  • GET:获取资源,查询服务器资源

GET请求和POST请求的区别?

使用上的区别

  • GET使用URL或Cookie传参,而POST将数据放在BODY中”,这个是因为HTTP协议用法的约定。
  • GET方式提交的数据有长度限制,则POST的数据则可以非常大”,这个是因为它们使用的操作系统和浏览器设置的不同引起的区别。
  • POST比GET安全,因为数据在地址栏上不可见”,这个说法没毛病,但依然不是GET和POST本身的区别。

本质区别

GET和POST最大的区别主要是GET请求是幂等性的,POST请求不是。这个是它们本质区别。

幂等性是指一次和多次请求某一个资源应该具有同样的副作用。简单来说意味着对同一URL的多个请求应该返回同样的结果。

在浏览器中输⼊url地址 ->> 显示主⻚的过程

  1. DNS解析浏览器搜索自己的DNS缓存(维护一张域名与IP的对应表);若没有,则搜索操作系统的DNS缓存(维护一张域名与IP的对应表);若没有,则搜索操作系统的hosts文件(维护一张域名与IP的对应表)。

    若都没有,则找 tcp/ip 参数中设置的首选 dns 服务器,即本地 dns 服务器(递归查询),本地域名服务器查询自己的dns缓存,如果没有,则进行迭代查询。将本地dns服务器将IP返回给操作系统,同时缓存IP。)

  2. TCP连接(发起 tcp 的三次握手,建立 tcp 连接。浏览器会以一个随机端口(1024-65535)向服务端的 web 程序 80 端口发起 tcp 的连接。)

  3. 发送HTTP请求。

  4. 服务器处理,客户端得到 html 代码。服务器 web 应用程序收到 http 请求后,就开始处理请求,处理之后就返回给浏览器 html 文件。

  5. 浏览器解析,解析 html 代码,并请求 html 中的资源。浏览器对页面进行渲染,并呈现给用户

  6. 连接结束

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Ynox7lcW-1681383922687)(https://raw.githubusercontent.com/viacheung/img/main/image/image-20230313160513726.png)]

各种协议与HTTP协议之间的关系

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6c4vYDbZ-1681383922688)(https://raw.githubusercontent.com/viacheung/img/main/image/image-20230313160546396.png)]

HTTP⻓连接,短连接

**HTTP/1.0中默认使⽤短连接 **,每次建立连接之后都中断

HTTP/1.1里面默认使⽤⻓连接 , 响应头加入Connection:keep-alive ,再次访问这个服务器会继续使用已经建立的连接,但有时间限制,且客户端服务端都要支持长连接

HTTP协议的⻓连接和短连接,实质上是TCP协议的⻓连接和短连接。

HTTP请求报文和响应报文的格式?

请求报文格式

  1. 请求行(请求方法+URI协议+版本)
  2. 请求头部
  3. 空行
  4. 请求主体
GET/sample.jspHTTP/1.1 请求行
Accept:image/gif.image/jpeg, 请求头部
Accept-Language:zh-cn
Connection:Keep-Alive
Host:localhost
User-Agent:Mozila/4.0(compatible;MSIE5.01;Window NT5.0)
Accept-Encoding:gzip,deflate

username=jinqiao&password=1234 请求主体

响应报文

  1. 状态行(版本+状态码+原因短语)
  2. 响应首部
  3. 空行
  4. 响应主体
HTTP/1.1 200 OK
Server:Apache Tomcat/5.0.12
Date:Mon,6Oct2003 13:23:42 GMT
Content-Length:112

<html>
    <head>
        <title>HTTP响应示例<title>
    head>
    <body>
        Hello HTTP!
    body>
html>

HTTP是不保存状态的协议,如何保存⽤户状态?

服务端放一个session记录用户状态,客户端我们通过在 Cookie 中附加⼀个 Session ID 来⽅式来跟踪。

Cookie 被禁⽤怎么办?

最常⽤的就是利⽤ URL 重写把 Session ID 直接附加在URL路径的后⾯

什么是 Cookie 和 Session ?

什么是 Cookie

HTTP Cookie(也叫 Web Cookie或浏览器 Cookie)是服务器发送到用户浏览器并保存在本地的一小块数据,它会在浏览器下次向同一服务器再发起请求时被携带并发送到服务器上。通常,它用于告知服务端两个请求是否来自同一浏览器,如保持用户的登录状态。Cookie 使基于无状态的 HTTP 协议记录稳定的状态信息成为了可能。

Cookie 主要用于以下三个方面:

  • 会话状态管理(如用户登录状态、购物车、游戏分数或其它需要记录的信息)
  • 个性化设置(如用户自定义设置、主题等)
  • 浏览器行为跟踪(如跟踪分析用户行为等)

什么是 Session

Session 代表着服务器和客户端一次会话的过程。Session 对象存储特定用户会话所需的属性及配置信息。这样,当用户在应用程序的 Web 页之间跳转时,存储在 Session 对象中的变量将不会丢失,而是在整个用户会话中一直存在下去。当客户端关闭会话,或者 Session 超时失效时会话结束。

总结

作用:保存⽤户信息 ,帮你登录的⼀些基本信息给填了 ,cookie里面存放了⼀个Token ,下次登录的时候只需要根据 Token 值来查找⽤户即可

区别:Cookie 数据保存在客户端(浏览器端), Session 数据保存在服务器端。 session安全些

Cookie 和 Session 是如何配合的呢?

用户第一次请求服务器的时候,服务器根据用户提交的相关信息,创建对应的 Session ,请求返回时将此 Session 的唯一标识信息 SessionID 返回给浏览器,浏览器接收到服务器返回的 SessionID 信息后,会将此信息存入到 Cookie 中,同时 Cookie 记录此 SessionID 属于哪个域名。

当用户第二次访问服务器的时候,请求会自动判断此域名下是否存在 Cookie 信息,如果存在自动将 Cookie 信息也发送给服务端,服务端会从 Cookie 中获取 SessionID,再根据 SessionID 查找对应的 Session 信息,如果没有找到说明用户没有登录或者登录失效,如果找到 Session 证明用户已经登录可执行后面操作。

根据以上流程可知,SessionID 是连接 Cookie 和 Session 的一道桥梁,大部分系统也是根据此原理来验证用户登录状态。

Cookie和Session的区别?

  • 作用范围不同,Cookie 保存在客户端(浏览器),Session 保存在服务器端。
  • 存取方式的不同,Cookie 只能保存 ASCII,Session 可以存任意数据类型,一般情况下我们可以在 Session 中保持一些常用变量信息,比如说 UserId 等。
  • 有效期不同,Cookie 可设置为长时间保持,比如我们经常使用的默认登录功能,Session 一般失效时间较短,客户端关闭或者 Session 超时都会失效。
  • 隐私策略不同,Cookie 存储在客户端,比较容易遭到不法获取,早期有人将用户的登录名和密码存储在 Cookie 中导致信息被窃取;Session 存储在服务端,安全性相对 Cookie 要好一些。
  • 存储大小不同, 单个 Cookie 保存的数据不能超过 4K,Session 可存储数据远高于 Cookie。

如何考虑分布式 Session 问题?

在互联网公司为了可以支撑更大的流量,后端往往需要多台服务器共同来支撑前端用户请求,那如果用户在 A 服务器登录了,第二次请求跑到服务 B 就会出现登录失效问题。

分布式 Session 一般会有以下几种解决方案:

  • 客户端存储:直接将信息存储在cookie中,cookie是存储在客户端上的一小段数据,客户端通过http协议和服务器进行cookie交互,通常用来存储一些不敏感信息

  • Nginx ip_hash 策略:服务端使用 Nginx 代理,每个请求按访问 IP 的 hash 分配,这样来自同一 IP 固定访问一个后台服务器,避免了在服务器 A 创建 Session,第二次分发到服务器 B 的现象。

  • Session 复制:任何一个服务器上的 Session 发生改变(增删改),该节点会把这个 Session 的所有内容序列化,然后广播给所有其它节点。

  • 共享 Session(√):服务端无状态话,将用户的 Session 等信息使用缓存中间件(如Redis)来统一管理,保障分发到每一个服务器的响应结果都一致。

建议采用共享 Session的方案。

什么是DDos攻击?

DDos全称Distributed Denial of Service,分布式拒绝服务攻击。最基本的DOS攻击过程如下:

  1. 客户端向服务端发送请求链接数据包。
  2. 服务端向客户端发送确认数据包。
  3. 客户端不向服务端发送确认数据包,服务器一直等待来自客户端的确认

DDoS则是采用分布式的方法,通过在网络上占领多台“肉鸡”,用多台计算机发起攻击。

DOS攻击现在基本没啥作用了,因为服务器的性能都很好,而且是多台服务器共同作用,1V1的模式黑客无法占上风。对于DDOS攻击,预防方法有:

  • 减少SYN timeout时间。在握手的第三步,服务器会等待30秒-120秒的时间,减少这个等待时间就能释放更多的资源。
  • 限制同时打开的SYN半连接数目。

什么是XSS攻击?

XSS也称 cross-site scripting,跨站脚本。这种攻击是由于服务器将攻击者存储的数据原原本本地显示给其他用户所致的。比如一个存在XSS漏洞的论坛,用户发帖时就可以引入带有<script>标签的代码,导致恶意代码的执行。

预防措施有:

  • 前端:过滤。
  • 后端:转义,比如go自带的处理器就具有转义功能。

SQL注入是什么,如何避免SQL注入?

select distinct * from company where id='1' OR '1' = '1'

SQL 注入就是在用户输入的字符串中加入 SQL 语句,如果在设计不良的程序中忽略了检查,那么这些注入进去的 SQL 语句就会被数据库服务器误认为是正常的 SQL 语句而运行,攻击者就可以执行计划外的命令或访问未被授权的数据。

SQL注入的原理主要有以下 4 点

  • 恶意拼接查询
  • 利用注释执行非法命令
  • 传入非法参数
  • 添加额外条件

避免SQL注入的一些方法

  • 限制数据库权限,给用户提供仅仅能够满足其工作的最低权限。
  • 对进入数据库的特殊字符(’”\尖括号&*;等)转义处理
  • 提供参数化查询接口,不要直接使用原生SQL

负载均衡算法有哪些?

多台服务器以对称的方式组成一个服务器集合,每台服务器都具有等价的地位,能互相分担负载。

  • 轮询法:将请求按照顺序轮流的分配到服务器上。大锅饭,不能发挥某些高性能服务器的优势。
  • 随机法:随机获取一台,和轮询类似。
  • 哈希法:通过ip地址哈希化(我客户端的Ip地址 也就是每个客户端请求到的服务器固定)来确定要选择的服务器编号。好处是,每次客户端访问的服务器都是同一个服务器,能很好地利用session或者cookie。
  • 加权轮询:根据服务器性能不同加权

HTTP 1.0和HTTP 1.1的主要 区别

1、长连接

在一个TCP连接上可以传送多个HTTP请求和响应,减少了建立和关闭连接的消耗和延迟,在HTTP1.1中默认开启Connection: keep-alive,一定程度上弥补了HTTP1.0每次请求都要创建连接的缺点。

2、错误状态响应码多了

新增了24个错误状态响应码,如409(Conflict)表示请求的资源与资源的当前状态发生冲突;410(Gone)表示服务器上的某个资源被永久性的删除。

3、缓存策略多了

4、只请求资源一部分,优化带宽

5、Host头处理:在HTTP1.0中认为每台服务器都绑定一个唯一的IP地址,因此,请求消息中的URL并没有传递主机名(hostname)。但随着虚拟主机技术的发展,在一台物理服务器上可以存在多个虚拟主机(Multi-homed Web Servers),并且它们共享一个IP地址。HTTP1.1的请求消息和响应消息都应支持Host头域,且请求消息中如果没有Host头域会报告一个错误(400 Bad Request)。

HTTP1.1和 HTTP2.0的区别?

HTTP2.0相比HTTP1.1支持的特性:

  • 新的二进制格式:HTTP1.1的解析是基于文本。基于文本协议的格式解析存在天然缺陷,文本的表现形式有多样性,要做到健壮性考虑的场景必然很多,二进制则不同,只认0和1的组合。基于这种考虑HTTP2.0的协议解析决定采用二进制格式,实现方便且健壮。
  • 多路复用,即连接共享,即每一个request都是用作连接共享机制的。一个request对应一个id,这样一个连接上可以有多个request,每个连接的request可以随机的混杂在一起,接收方可以根据request的 id将request再归属到各自不同的服务端请求里面。
  • 头部压缩,HTTP1.1的头部(header)带有大量信息,而且每次都要重复发送;HTTP2.0使用encoder来减少需要传输的header大小,通讯双方各自cache一份header fields表,既避免了重复header的传输,又减小了需要传输的大小。
  • 服务端推送:服务器除了对最初请求的响应外,服务器还可以额外的向客户端推送资源,而无需客户端明确的请求。

URI和URL的区别是什么?

都可以标识一个资源 但URL可以定位到这个资源

HTTP 和 HTTPS 的区别?

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LCX3hYou-1681383922689)(https://raw.githubusercontent.com/viacheung/img/main/image/image-20230404211801661.png)]

1、端口 80-443 http://—>https://

2、安全性和资源消耗:HTTP协议运⾏在TCP之上,所有传输的内容都是明⽂,客户端和服务器端都⽆法验证对⽅的身份。 HTTPS是运⾏在SSL/TLS之上的HTTP协议, SSL/TLS 运⾏在TCP之上。所有传输的内容都经过加密,加密采⽤对称加密,但对称加密的密钥⽤服务器⽅的证书进⾏了⾮对称加密。所以说, HTTP 安全性没有 HTTPS⾼,但是 HTTPS ⽐HTTP耗费更多服务器资源

对称加密:密钥只有⼀个,加密解密为同⼀个密码 DES、 AES等;
⾮对称加密:密钥成对出现,加密解密使⽤不同密钥(公钥加密需要私钥解密,私钥加密需要公钥解密),相对对称加密速度较慢,典型的⾮对称加密算法有RSA、 DSA等

HTTPS 的优缺点?

优点

  • 安全性:
    • 使用HTTPS协议可认证用户和服务器,确保数据发送到正确的客户机和服务器;
    • HTTPS协议是由SSL+HTTP协议构建的可进行加密传输、身份认证的网络协议,要比http协议安全,可防止数据在传输过程中不被窃取、改变,确保数据的完整性。
    • HTTPS是现行架构下最安全的解决方案,虽然不是绝对安全,但它大幅增加了中间人攻击的成本。
  • SEO方面:谷歌曾在2014年8月份调整搜索引擎算法,并称“比起同等HTTP网站,采用HTTPS加密的网站在搜索结果中的排名将会更高”。

缺点

  • 在相同网络环境中,HTTPS 相比 HTTP 无论是响应时间还是耗电量都有大幅度上升。
  • HTTPS 的安全是有范围的,在黑客攻击、服务器劫持等情况下几乎起不到作用。
  • 在现有的证书机制下,中间人攻击依然有可能发生。
  • HTTPS 需要更多的服务器资源,也会导致成本的升高。

讲一讲HTTPS 的原理?

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XQzMlnBx-1681383922689)(https://raw.githubusercontent.com/viacheung/img/main/image/image-20210525160006424.png)]

图片来源:https://segmentfault.com/a/1190000021494676

加密流程按图中的序号分为:

  1. 客户端请求 HTTPS 网址,然后连接到 server 的 443 端口 (HTTPS 默认端口,类似于 HTTP 的80端口)。

  2. 采用 HTTPS 协议的服务器必须要有一套数字 CA (Certification Authority)证书。颁发证书的同时会产生一个私钥和公钥。私钥由服务端自己保存,不可泄漏。公钥则是附带在证书的信息中,可以公开的。证书本身也附带一个证书电子签名,这个签名用来验证证书的完整性和真实性,可以防止证书被篡改。

  3. 服务器响应客户端请求,将证书传递给客户端,证书包含公钥和大量其他信息,比如证书颁发机构信息,公司信息和证书有效期等。

  4. 客户端解析证书并对其进行验证。如果证书不是可信机构颁布,或者证书中的域名与实际域名不一致,或者证书已经过期,就会向访问者显示一个警告,由其选择是否还要继续通信。

    如果证书没有问题,客户端就会从服务器证书中取出服务器的公钥A。然后客户端还会生成一个随机码 KEY,并使用公钥A将其加密。

  5. 客户端把加密后的随机码 KEY 发送给服务器,作为后面对称加密的密钥。

  6. 服务器在收到随机码 KEY 之后会使用私钥B将其解密。经过以上这些步骤,客户端和服务器终于建立了安全连接,完美解决了对称加密的密钥泄露问题,接下来就可以用对称加密愉快地进行通信了。

  7. 服务器使用密钥 (随机码 KEY)对数据进行对称加密并发送给客户端,客户端使用相同的密钥 (随机码 KEY)解密数据。

  8. 双方使用对称加密愉快地传输所有数据。

操作系统

什么是操作系统?

管理计算机硬件与软件资源的程序

屏蔽了硬件层的复杂性

内核负责系统的内存管理,硬件设备的管理,⽂件系统的管理以及应⽤程序的管理。

系统调⽤

用户态内核态

⽤户程序中,凡是与系统态级别的资源有关的操作(如⽂件管理、进程控制、内存管理等),都必须通过系统调⽤⽅式向操作系统提出服务请求,并由操作系统代为完成。

进程和线程的区别

  • 调度:进程是资源管理的基本单位,线程是程序执行的基本单位。
  • 切换:线程上下文切换比进程上下文切换要快得多。
  • 拥有资源: 进程是拥有资源的一个独立单位,线程不拥有系统资源,但是可以访问隶属于进程的资源。
  • 系统开销: 创建或撤销进程时,系统都要为之分配或回收系统资源,如内存空间,I/O设备等,OS所付出的开销显著大于在创建或撤销线程时的开销,进程切换的开销也远大于线程切换的开销。

协程与线程的区别?

  • 线程和进程都是同步机制,而协程是异步机制。
  • 线程是抢占式,而协程是非抢占式的。需要用户释放使用权切换到其他协程,因此同一时间其实只有一个协程拥有运行权,相当于单线程的能力。
  • 一个线程可以有多个协程,一个进程也可以有多个协程(包含关系)。
  • 协程不被操作系统内核管理,而完全是由程序控制。线程是被分割的CPU资源,协程是组织好的代码流程,线程是协程的资源。但协程不会直接使用线程,协程直接利用的是执行器关联任意线程或线程池。
  • 协程能保留上一次调用时的状态。、

并发和并行有什么区别?

并发:1、一段时间,有多个任务在执行; 但某一时刻 ,只有一个任务在执行 本质是因为进程切换时间片足够块来达到同时多个程序在运行的错觉

并行:2、同一时刻,确实有多个任务在执行。需要多核处理器才能完成,不同的程序被放到不同的处理器上运行

进程与线程的切换流程?

进程切换分两步:

1、切换页表以使用新的地址空间(换其他线程的虚拟地址空间),一旦去切换上下文,处理器中所有已经缓存的内存地址一瞬间都作废了。

2、切换内核栈和硬件上下文。

线程只有

2、切换内核栈和硬件上下文。

因为每个进程都有自己的虚拟地址空间,而线程是共享所在进程的虚拟地址空间的,因此同一个进程中的线程进行线程切换时不涉及虚拟地址空间的转换。

为什么虚拟地址空间切换会比较耗时?

进程都有自己的虚拟地址空间,把虚拟地址转换为物理地址需要查找页表,页表查找是一个很慢的过程,因此通常使用Cache来缓存常用的地址映射,这样可以加速页表查找,这个Cache就是TLB(translation Lookaside Buffer,TLB本质上就是一个Cache,是用来加速页表查找的)。

由于每个进程都有自己的虚拟地址空间,那么显然每个进程都有自己的页表,那么当进程切换后页表也要进行切换,页表切换后TLB就失效了,Cache失效导致命中率降低,那么虚拟地址转换为物理地址就会变慢,表现出来的就是程序运行会变慢,而线程切换则不会导致TLB失效,因为线程无需切换地址空间,因此我们通常说线程切换要比较进程切换块,原因就在这里。

总结:虚拟地址到物理地址需要用到页表进行转换,为了加速转换,一般把页表放到Cache(TLB)里面 进程切换后页表也要进行切换,页表切换后TLB就失效了,所以这个转换就变慢了

进程间通信方式有哪些?

  • 管道:管道这种通讯方式有两种限制,一是半双工的通信,数据只能单向流动,二是只能在具有亲缘关系的进程间(父子进程关系)。

    管道可以分为两类:匿名管道和命名管道。匿名管道是单向的,只能在有亲缘关系的进程间通信;

    命名管道以磁盘文件的方式存在,可以实现本机任意两个进程通信。

  • 信号 : 信号是一种比较复杂的通信方式,信号可以在任何时候发给某一进程,而无需知道该进程的状态。

  • Socket:与其他通信机制不同的是,它可用于不同机器间的进程通信。

  • 消息队列:消息队列克服了信号承载信息量少,管道只能承载无格式字节流以及缓冲区大小受限等缺点。

  • 共享内存:共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。共享内存是最快的 IPC 方式,它是针对其他进程间通信方式运行效率低而专门设计的。它往往与其他通信机制,如信号量,配合使用,来实现进程间的同步和通信。

  • 信号量:信号量是一个计数器,可以用来控制多个进程对共享资源的访问。它常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段。

优缺点

  • 管道:速度慢,容量有限;
  • Socket:任何进程间都能通讯,但速度慢;
  • 消息队列:容量受到系统限制,且要注意第一次读的时候,要考虑上一次没有读完数据的问题;
  • 信号量:不能传递复杂消息,只能用来同步;
  • 共享内存区:能够很容易控制容量,速度快,但要保持同步,比如一个进程在写的时候,另一个进程要注意读写的问题,相当于线程中的线程安全,当然,共享内存区同样可以用作线程间通讯,不过没这个必要,线程间本来就已经共享了同一进程内的一块内存。

进程间的通信⽅式总结:

管道/匿名管道(Pipes)

信号(Signal)

消息队列(Message Queuing)

信号量(Semaphores) :计数器,⽤于多进程对共享数据的访问

共享内存(Shared memory)

套接字(Sockets) : 客户端和服务器之间通过⽹络进⾏通信

进程间同步的方式有哪些?

1、临界区:通过多线程的串行化来访问公共资源或一段代码,速度快,适合控制数据访问。(只能同步本进程内的线程)

优点:保证在某一时刻只有一个线程能访问数据的简便办法。

缺点:虽然临界区同步速度很快,但却只能用来同步本进程内的线程,而不可用来同步多个进程中的线程。

2、互斥量:为协调共同对一个共享资源的单独访问而设计的。互斥量跟临界区很相似,比临界区复杂,互斥对象只有一个,只有拥有互斥对象的线程才具有访问资源的权限。(可以在不同进程的线程之间进行同步

优点:使用互斥不仅仅能够在同一应用程序不同线程中实现资源的安全共享,而且可以在不同应用程序的线程之间实现对资源的安全共享。

缺点:

  • 互斥量是可以命名的,也就是说它可以跨越进程使用,所以创建互斥量需要的资源更多,所以如果只为了在进程内部是用的话使用临界区会带来速度上的优势并能够减少资源占用量。(跨进程 需要资源多
  • 通过互斥量可以指定资源被独占的方式使用,但如果有下面一种情况通过互斥量就无法处理,比如现在一位用户购买了一份三个并发访问许可的数据库系统,可以根据用户购买的访问许可数量来决定有多少个线程/进程能同时进行数据库操作,这时候如果利用互斥量就没有办法完成这个要求,信号量对象可以说是一种资源计数器。(无法处理多个线程一起同步的问题 要用到信号量

3、信号量:为控制一个具有有限数量用户资源而设计。它允许多个线程在同一时刻访问同一资源,但是需要限制在同一时刻访问此资源的最大线程数目。互斥量是信号量的一种特殊情况,当信号量的最大资源数=1就是互斥量了。

优点:适用于对Socket(套接字)程序中线程的同步。

缺点:

  • 信号量机制必须有公共内存,不能用于分布式操作系统,这是它最大的弱点;
  • 信号量机制功能强大,但使用时对信号量的操作分散, 而且难以控制,读写和维护都很困难,加重了程序员的编码负担;
  • 核心操作P-V分散在各用户程序的代码中,不易控制和管理,一旦错误,后果严重,且不易发现和纠正。

4、事件: 用来通知线程有一些事件已发生,从而启动后继任务的开始。

优点:事件对象通过通知操作的方式来保持线程的同步,并且可以实现不同进程中的线程同步操作

线程间的同步的⽅式

1、临界区:当多个线程访问一个独占性共享资源时,可以使用临界区对象。拥有临界区的线程可以访问被保护起来的资源或代码段,其他线程若想访问,则被挂起,直到拥有临界区的线程放弃临界区为止,以此达到用原子方式操 作共享资源的目的。

2、事件:事件机制,则允许一个线程在处理完一个任务后,主动唤醒另外一个线程执行任务。

3、互斥量:互斥对象和临界区对象非常相似,只是其允许在进程间使用,而临界区只限制与同一进程的各个线程之间使用,但是更节省资源,更有效率。

( Java 中的synchronized 关键词和各种 Lock 都是这种机制。)

4、信号量:当需要一个计数器来限制可以使用某共享资源的线程数目时,可以使用“信号量”对象。

区别:

  • 互斥量与临界区的作用非常相似,但互斥量是可以命名的,也就是说互斥量可以跨越进程使用,但创建互斥量需要的资源更多,所以如果只为了在进程内部是用的话使用临界区会带来速度上的优势并能够减少资源占用量 。因为互斥量是跨进程的互斥量一旦被创建,就可以通过名字打开它。
  • 互斥量,信号量,事件都可以被跨越进程使用来进行同步数据操作

线程的分类?

从线程的运行空间来说,分为用户级线程(user-level thread, ULT)和内核级线程(kernel-level, KLT)

内核级线程:这类线程依赖于内核,又称为内核支持的线程或轻量级进程。无论是在用户程序中的线程还是系统进程中的线程,它们的创建、撤销和切换都由内核实现。比如英特尔i5-8250U是4核8线程,这里的线程就是内核级线程

用户级线程:它仅存在于用户级中,这种线程是不依赖于操作系统核心的。应用进程利用线程库来完成其创建和管理,速度比较快,操作系统内核无法感知用户级线程的存在

什么是临界区,如何解决冲突?

每个进程中访问临界资源的那段程序称为临界区,一次仅允许一个进程使用的资源称为临界资源。

解决冲突的办法:

  • 【一次只允许一个进程进入,其他进程在外面等】如果有若干进程要求进入空闲的临界区,一次仅允许一个进程进入,如已有进程进入自己的临界区,则其它所有试图进入临界区的进程必须等待;
  • 【进入时间优先】进入临界区的进程要在有限时间内退出
  • 【进不去的话 让出CPU,避免出现忙等】如果进程不能进入自己的临界区,则应让出CPU,避免进程出现“忙等”现象。

什么是死锁?死锁产生的条件?

什么是死锁

两个或者多个进程持有某种资源而又等待其它进程释放现在保持着的资源,然后形成一种无限期的阻塞、相互等待的一种状态。

死锁产生的四个必要条件:(有一个条件不成立,则不会产生死锁)

  • 互斥条件:一个资源一次只能被一个进程使用
  • 请求与保持条件:一个进程因请求资源而阻塞时,对已获得资源保持不放
  • 不剥夺条件:进程获得的资源,在未完全使用完之前,不能强行剥夺
  • 循环等待条件:若干进程之间形成一种头尾相接的环形等待资源关系

如何处理死锁问题

常用的处理死锁的方法有:死锁预防、死锁避免、死锁检测、死锁解除、鸵鸟策略。

**(1)死锁的预防:**基本思想就是确保死锁发生的四个必要条件中至少有一个不成立:

  • ① 破除资源互斥条件:无法破坏
  • ② 破除“请求与保持”条件:进程在运行之前,必须一次性获取所有的资源。缺点:在很多情况下,无法预知进程执行前所需的全部资源,因为进程是动态执行的,同时也会降低资源利用率,导致降低了进程的并发性。
  • ③ 破除“不可剥夺”条件:允许进程强行从占有者那里夺取某些资源。当一个已经保持了某些不可被抢占资源的进程,提出新的资源请求而不能得到满足时,它必须释放已经保持的所有资源,待以后需要时再重新申请。这意味着进程已经占有的资源会被暂时被释放,或者说被抢占了。
  • ④ 破除“循环等待”条件:对所有资源排序编号,按照顺序获取资源,将紧缺的,稀少的采用较大的编号,在申请资源时必须按照编号的顺序进行,一个进程只有获得较小编号的进程才能申请较大编号的进程。(申请资源有序 释放资源反序)

(2)死锁避免:

死锁避免则允许前三个必要条件,但是通过动态地检测资源分配状态,以确保循环等待条件不成立,从而确保系统处于安全状态。

所谓安全状态是指:如果系统能按某个顺序为每个进程分配资源(不超过其最大值),那么系统状态是安全的,换句话说就是,如果存在一个安全序列,那么系统处于安全状态。银行家算法是经典的死锁避免的算法。

(3)死锁检测:

死锁预防策略是非常保守的,他们通过限制访问资源和在进程上强加约束来解决死锁的问题。死锁检测则是完全相反,它不限制资源访问或约束进程行为,只要有可能,被请求的资源就被授权给进程。但是操作系统会周期性地执行一个算法检测前面的循环等待的条件。死锁检测算法是通过资源分配图来检测是否存在环来实现,从一个节点出发进行深度优先搜索,对访问过的节点进行标记,如果访问了已经标记的节点,就表示有存在环,也就是检测到死锁的发生

dfs

  • (1)如果进程-资源分配图中无环路,此时系统没有死锁。
  • (2)如果进程-资源分配图中有环路,且每个资源类中只有一个资源,则系统发生死锁。
  • (3)如果进程-资源分配图中有环路,且所涉及的资源类有多个资源,则不一定会发生死锁。(因为不止一个资源嘛 你再访问也没事)

(4)死锁解除:

死锁解除的常用方法就是==终止进程和资源抢占,回滚。==所谓进程终止就是简单地终止一个或多个进程以打破循环等待,包括两种方式:终止所有死锁进程和一次只终止一个进程直到取消死锁循环为止;所谓资源抢占就是从一个或者多个死锁进程那里抢占一个或多个资源。

(5)鸵鸟策略:

把头埋在沙子里,假装根本没发生问题。因为解决死锁问题的代价很高,因此鸵鸟策略这种不采取任何措施的方案会获得更高的性能。当发生死锁时不会对用户造成多大影响,或发生死锁的概率很低,可以采用鸵鸟策略。大多数操作系统,包括 Unix,Linux 和 Windows,处理死锁问题的办法仅仅是忽略它。

进程的调度算法

  • 先来先服务:非抢占式的调度算法,按照请求的顺序进行调度。有利于长作业,但不利于短作业,因为短作业必须一直等待前面的长作业执行完毕才能执行,而长作业又需要执行很长时间,造成了短作业等待时间过长。另外,对I/O密集型进程也不利,因为这种进程每次进行I/O操作之后又得重新排队。

  • 短作业优先:非抢占式的调度算法,按估计运行时间最短的顺序进行调度。长作业有可能会饿死,处于一直等待短作业执行完毕的状态。因为如果一直有短作业到来,那么长作业永远得不到调度。

  • 最短剩余时间优先:最短作业优先的抢占式版本,按剩余运行时间的顺序进行调度。 当一个新的作业到达时,其整个运行时间与当前进程的剩余时间作比较。如果新的进程需要的时间更少,则挂起当前进程,运行新的进程。否则新的进程等待。

  • 时间片轮转:将所有就绪进程按 FCFS 的原则排成一个队列,每次调度时,把 CPU 时间分配给队首进程,该进程可以执行一个时间片。当时间片用完时,由计时器发出时钟中断,调度程序便停止该进程的执行,并将它送往就绪队列的末尾,同时继续把 CPU 时间分配给队首的进程。

    时间片轮转算法的效率和时间片的大小有很大关系:因为进程切换都要保存进程的信息并且载入新进程的信息,如果时间片太小,会导致进程切换得太频繁,在进程切换上就会花过多时间。 而如果时间片过长,那么实时性就不能得到保证。

  • 优先级调度:为每个进程分配一个优先级,按优先级进行调度。为了防止低优先级的进程永远等不到调度,可以随着时间的推移增加等待进程的优先级。

进程有哪些状态?

进程一共有5种状态,分别是创建、就绪、运行(执行)、终止、阻塞。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qk3s4r8p-1681383922691)(https://raw.githubusercontent.com/viacheung/img/main/image/A61F5B5322ED49038C64BDD82D341987)]

  • 运行状态就是进程正在CPU上运行。在单处理机环境下,每一时刻最多只有一个进程处于运行状态。
  • 就绪状态就是说进程已处于准备运行的状态,即进程获得了除CPU之外的一切所需资源,一旦得到CPU即可运行。
  • 阻塞状态就是进程正在等待某一事件而暂停运行,比如等待某资源为可用或等待I/O完成。即使CPU空闲,该进程也不能运行。

运行态→阻塞态:往往是由于等待外设,等待主存等资源分配或等待人工干预而引起的。

阻塞态→就绪态:则是等待的条件已满足,只需分配到处理器后就能运行。

运行态→就绪态:不是由于自身原因,而是由外界原因使运行状态的进程让出处理器,这时候就变成就绪态。例如时间片用完,或有更高优先级的进程来抢占处理器等。

就绪态→运行态:系统按某种策略选中就绪队列中的一个进程占用处理器,此时就变成了运行态。

什么是分页?

把内存空间划分为大小相等且固定的块,作为主存的基本单位。因为程序数据存储在不同的页面中,而页面又离散的分布在内存中,因此需要一个页表来记录映射关系,以实现从页号到物理块号的映射。

访问分页系统中内存数据需要两次的内存访问 (一次是从内存中访问页表,从中找到指定的物理块号,加上页内偏移得到实际物理地址==(页号*每页块数+页内偏移量=物理块号)==;第二次就是根据第一次得到的物理地址访问内存取出数据)。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KCNT6uK0-1681383922691)(https://raw.githubusercontent.com/viacheung/img/main/image/image-20210610173249387.png)]

什么是分段?

分页是为了提高内存利用率,而分段是为了满足程序员在编写代码的时候的一些逻辑需求(比如数据共享,数据保护,动态链接等)。

分段内存管理当中,地址是二维的,一维是段号,二维是段内地址;其中每个段的长度是不一样的,而且每个段内部都是从0开始编址的。由于分段管理中,每个段内部是连续内存分配,但是段和段之间是离散分配的,因此也存在一个逻辑地址到物理地址的映射关系,相应的就是段表机制。

基址+偏移量(看看有没有超过段长 超过说明越界了)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-25fusgIl-1681383922692)(https://raw.githubusercontent.com/viacheung/img/main/image/image-20230330225823075.png)]

分页和分段有什区别?

  • ==(分页透明 分段不透明 需要自己显式划分)==分页对程序员是透明的,但是分段需要程序员显式划分每个段。
  • 分页的地址空间是一维地址空间,分段是二维的。(一个给地址就行 一个需要给段码+段内地址)

解释:分页之所以是一维的,原因在于分页的大小是固定的,且页码之间是连续的,操作的时候只需给出一个地址,就能够根据所给地址的大小与页面大小计算出在页码和页内地址,粗略举例,比如页面大小是4KB,给一个地址为5000,可以算出所在页码是2,页内地址是5000-4000=1000,即在第二页的第1000个位置。
而分段的因为每段的长度不一样,必须给出段码和段内地址

  • 页的大小不可变,段的大小可以动态改变
  • 分页主要用于实现虚拟内存,从而获得更大的地址空间(for max);分段主要是为了使程序和数据可以被划分为逻辑上独立的地址空间并且有助于共享和保护(独立化)。

什么是交换空间?

操作系统把物理内存(physical RAM)分成一块一块的小内存,每一块内存被称为页(page)。当内存资源不足时,Linux把某些页的内容转移至硬盘上的一块空间上,以释放内存空间。硬盘上的那块空间叫做交换空间**(swap space),而这一过程被称为交换(swapping)。**物理内存和交换空间的总容量就是虚拟内存的可用容量。

用途:

  • 物理内存不足时一些不常用的页可以被交换出去,腾给系统。
  • 程序启动时很多内存页被用来初始化,之后便不再需要,可以交换出去。

物理地址、逻辑地址、有效地址、线性地址、虚拟地址的区别?

物理地址就是内存中真正的地址,具有唯一性。不管哪种地址,最终都会映射为物理地址

有效地址=逻辑地址 线性地址=逻辑地址

实模式下,段基址 + 段内偏移经过地址加法器的处理,经过地址总线传输,最终也会转换为物理地址

但是在保护模式下,段基址 + 段内偏移被称为线性地址,不过此时的段基址不能称为真正的地址,而是会被称作为一个选择子的东西,选择子就是个索引,相当于数组的下标,通过这个索引能够在 GDT 中找到相应的段描述符,段描述符记录了段的起始、段的大小等信息,这样便得到了基地址。如果此时没有开启内存分页功能,那么这个线性地址可以直接当做物理地址来使用,直接访问内存。如果开启了分页功能,那么这个线性地址又多了一个名字,这个名字就是虚拟地址

不论在实模式还是保护模式下,段内偏移地址都叫做有效地址。有效地址也是逻辑地址。

线性地址可以看作是虚拟地址,虚拟地址不是真正的物理地址,但是虚拟地址会最终被映射为物理地址。下面是虚拟地址 -> 物理地址的映射。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Al5U1jiF-1681383922694)(https://raw.githubusercontent.com/viacheung/img/main/image/image-20210807152300643.png)]

什么是缓冲区溢出?有什么危害?

缓冲区溢出是指当计算机向缓冲区填充数据时超出了缓冲区本身的容量,溢出的数据覆盖在合法数据上。

危害有以下两点:

  • 程序崩溃,导致拒绝服务
  • 跳转并且执行一段恶意代码

造成缓冲区溢出的主要原因是程序中没有仔细检查用户输入。

什么是虚拟内存?

因为只加载进程的一部分进入内存 然后用页面置换技术就完事

虚拟内存就是说,让物理内存扩充成更大的逻辑内存,从而让程序获得更多的可用内存。虚拟内存使用部分加载的技术,==让一个进程或者资源的某些页面加载进内存,从而能够加载更多的进程,甚至能加载比内存大的进程,==这样看起来好像内存变大了,这部分内存其实包含了磁盘或者硬盘,并且就叫做虚拟内存。

虚拟内存的实现方式有哪些?

虚拟内存中,允许将一个作业分多次调入内存。釆用连续分配方式时,会使相当一部分内存空间都处于暂时或永久的空闲状态,造成内存资源的严重浪费,而且也无法从逻辑上扩大内存容量。因此,虚拟内存的实需要建立在离散分配的内存管理方式的基础上。虚拟内存的实现有以下三种方式:

肯定要离散呀 连续的话 那相当于没有虚拟内存 使用起始位置+偏移量

  • 请求分页存储管理。(分页)
  • 请求分段存储管理。(分段)
  • 请求段页式存储管理。(段页)

讲一讲IO多路复用

https://juejin.cn/post/6882984260672847879

IO多路复用是指内核一旦发现进程指定的一个或者多个IO条件准备读取,它就通知该进程。IO多路复用适用如下场合

  • 当客户处理多个描述字时(一般是交互式输入和网络套接口),必须使用I/O复用。
  • 当一个客户同时处理多个套接口时,而这种情况是可能的,但很少出现。
  • 如果一个TCP服务器既要处理监听套接口,又要处理已连接套接口,一般也要用到I/O复用。
  • 如果一个服务器即要处理TCP,又要处理UDP,一般要使用I/O复用。
  • 如果一个服务器要处理多个服务或多个协议,一般要使用I/O复用。
  • 与多进程和多线程技术相比,I/O多路复用技术的最大优势是系统开销小,系统不必创建进程/线程,也不必维护这些进程/线程,从而大大减小了系统的开销

硬链接和软链接有什么区别?

inode

inode 是一个描述文件或目录属性的数据库,如元信息、硬盘物理地址等。通过 inode,操作系统可以检索文件权限信息、物理地址等信息。当一个文件从一个文件夹移到另一个文件夹,文件将被移动到硬盘的另一个位置,文件的 inode 值也会自动发生变化。

硬连接

硬连接直接通过 inode 引用文件。硬连接只能用于文件,而不能用于目录。

硬连接(Hard Link)扮演着源文件拷贝或镜像的角色。可以访问源文件的数据,如果源文件被删除,硬连接依然可以访问源文件的数据。

软连接

软连接本质上是源文件的一个快捷方式,指向源文件本身,而不是源文件的 inode 值。软连接可以同时用于文件和目录,也可以在不同的硬盘或容器之间使用。

软连接(Soft Link 或 Symbolic Link)扮演着源文件指针的角色。不可以访问源文件数据,如果源文件被删除,软连接将会指向一个不再存在的文件地址。

ZWY面试总结_第1张图片

硬连接直接引用源文件引用的 inode,软连接则直接引用源文件。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-aRDMVrv9-1681383922695)(https://raw.githubusercontent.com/viacheung/img/main/image/image-20230331214416885.png)]

中断的处理过程?

  1. 保护现场:将当前执行程序的相关数据保存在寄存器中,然后入栈。
  2. 开中断:以便执行中断时能响应较高级别的中断请求。
  3. 中断处理
  4. 关中断:保证恢复现场时不被新中断打扰
  5. 恢复现场:从堆栈中按序取出程序数据,恢复中断前的执行状态。

中断和轮询有什么区别?

  • 轮询:CPU对特定设备轮流询问。中断:通过特定事件提醒CPU。
  • 轮询:效率低等待时间长,CPU利用率不高。中断:容易遗漏问题,CPU利用率不高。

什么是用户态和内核态?

用户态和系统态是操作系统的两种运行状态:

  • 内核态:内核态运行的程序可以访问计算机的任何数据和资源,不受限制,包括外围设备,比如网卡、硬盘等。处于内核态的 CPU 可以从一个程序切换到另外一个程序,并且占用 CPU 不会发生抢占情况。
  • 用户态:用户态运行的程序只能受限地访问内存,只能直接读取用户程序的数据,并且不允许访问外围设备,用户态下的 CPU 不允许独占,也就是说 CPU 能够被其他程序获取。

将操作系统的运行状态分为用户态和内核态,主要是为了对访问能力进行限制,防止随意进行一些比较危险的操作导致系统的崩溃,比如设置时钟、内存清理,这些都需要在内核态下完成 。

用户态和内核态是如何切换的?

所有的用户进程都是运行在用户态的,但是我们上面也说了,用户程序的访问能力有限,一些比较重要的比如从硬盘读取数据,从键盘获取数据的操作则是内核态才能做的事情,而这些数据却又对用户程序来说非常重要。所以就涉及到两种模式下的转换,即用户态 -> 内核态 -> 用户态,而唯一能够做这些操作的只有 系统调用,而能够执行系统调用的就只有 操作系统

一般用户态 -> 内核态的转换我们都称之为 trap 进内核,也被称之为 陷阱指令(trap instruction)

他们的工作流程如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Cf80Tdpd-1681383922695)(https://raw.githubusercontent.com/viacheung/img/main/image/image-20210807152619210.png)]

  • 首先用户程序会调用 glibc 库,glibc 是一个标准库,同时也是一套核心库,库中定义了很多关键 API。
  • glibc 库知道针对不同体系结构调用系统调用的正确方法,它会根据体系结构应用程序的二进制接口设置用户进程传递的参数,来准备系统调用。
  • 然后,glibc 库调用软件中断指令(SWI) ,这个指令通过更新 CPSR 寄存器将模式改为超级用户模式,然后跳转到地址 0x08 处。
  • 到目前为止,整个过程仍处于用户态下,在执行 SWI 指令后,允许进程执行内核代码,MMU 现在允许内核虚拟内存访问
  • 从地址 0x08 开始,进程执行加载并跳转到中断处理程序,这个程序就是 ARM 中的 vector_swi()
  • 在 vector_swi() 处,从 SWI 指令中提取系统调用号 SCNO,然后使用 SCNO 作为系统调用表 sys_call_table 的索引,调转到系统调用函数。
  • 执行系统调用完成后,将还原用户模式寄存器,然后再以用户模式执行。

Unix 常见的IO模型

对于一次IO访问(以read举例),数据会先被拷贝到操作系统内核的缓冲区中,然后才会从操作系统内核的缓冲区拷贝到应用程序的地址空间。所以说,当一个read操作发生时,它会经历两个阶段:

  • 等待数据准备就绪 (Waiting for the data to be ready)
  • 将数据从内核拷贝到进程中 (Copying the data from the kernel to the process)

正式因为这两个阶段,linux系统产生了下面五种网络模式的方案:

  • 阻塞式IO模型(blocking IO model)
  • 非阻塞式IO模型(noblocking IO model)
  • IO复用式IO模型(IO multiplexing model)
  • 信号驱动式IO模型(signal-driven IO model)
  • 异步IO式IO模型(asynchronous IO model)

对于这几种 IO 模型的详细说明,可以参考这篇文章:https://juejin.cn/post/6942686874301857800#heading-13

其中,IO多路复用模型指的是:使用单个进程同时处理多个网络连接IO,他的原理就是select、poll、epoll 不断轮询所负责的所有 socket,当某个socket有数据到达了,就通知用户进程。该模型的优势并不是对于单个连接能处理得更快,而是在于能处理更多的连接。

select、poll 和 epoll 之间的区别?

(1)select:时间复杂度 O(n)

select 仅仅知道有 I/O 事件发生,但并不知道是哪几个流,所以只能无差别轮询所有流,找出能读出数据或者写入数据的流,并对其进行操作。所以 select 具有 O(n) 的无差别轮询复杂度,同时处理的流越多,无差别轮询时间就越长。

(2)poll:时间复杂度 O(n)

poll 本质上和 select 没有区别,它将用户传入的数组拷贝到内核空间,然后查询每个 fd 对应的设备状态, 但是它没有最大连接数的限制,原因是它是基于链表来存储的。

(3)epoll:时间复杂度 O(1)

epoll 可以理解为 event poll,不同于忙轮询和无差别轮询,epoll 会把哪个流发生了怎样的 I/O 事件通知我们。所以说 epoll 实际上是事件驱动(每个事件关联上 fd)的。

select,poll,epoll 都是 IO 多路复用的机制。I/O 多路复用就是通过一种机制监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),就通知程序进行相应的读写操作。但 select,poll,epoll 本质上都是同步 I/O,因为他们都需要在读写事件就绪后自己负责进行读写,也就是说这个读写过程是阻塞的,而异步 I/O 则无需自己负责进行读写,异步 I/O 的实现会负责把数据从内核拷贝到用户空间。

操作系统的内存管理主要是做什么

主要负责内存的分配与回收

常⻅的⼏种内存管理机制

块式管理 :碎⽚

⻚式管理 : 提⾼了内存利⽤率,减少了碎⽚

段式管理 :实际意义的

快表和多级⻚表

解决了⻚表管理中很重要的两个问题

  1. 虚拟地址到物理地址的转换要快。
  2. 解决虚拟地址空间⼤,⻚表也会很⼤的问题。

快表 (类似缓存)

\1.根据虚拟地址中的⻚号查快表;
\2. 如果该⻚在快表中,直接从快表中读取相应的物理地址;
\3. 如果该⻚不在快表中,就访问内存中的⻚表,再从⻚表中得到物理地址,同时将⻚表中的该映射表项添加到快表中;
\4. 当快表填满后,⼜要登记新⻚时,就按照⼀定的淘汰策略淘汰掉快表中的⼀个⻚。

多级⻚表

时间换空间

分⻚机制和分段机制的共同点和区别

共同点 :
1、都是为了提⾼内存利⽤率,较少内存碎⽚。
2、每个⻚和段中的内存是连续的。

区别 :

1、⻚的⼤⼩是固定的,由操作系统决定;⽽段的⼤⼩不固定,取决于我们当前运⾏的程序。

2、分⻚仅仅是为了满⾜操作系统内存管理的需求,⽽段是逻辑信息的单位,

逻辑(虚拟)地址和物理地址

逻辑地址:指针⾥⾯存储的数值

物理地址指的是真实物理内存中地址 (内存地址寄存器 )

CPU 寻址了解吗?为什么需要虚拟地址空间?

如果直接把物理地址暴露出来的话会带来严重问题,⽐如可能对操作系统造成伤害以及给同时运⾏多个程序造成困难。

虚拟内存

只是多个进程被分割为多个物理内存块(还有部分暂时存储在外部磁盘存储器上,在需要时进⾏数据交换。 ) 感觉上是独享主存

局部性原理

时间局部:如果程序中的某条指令⼀旦执⾏,不久以后该指令可能再次执⾏

空间局部性 :⼀旦程序访问了某个存储单元,在不久之后,其附近的存储单元也将被访问,

⻚⾯置换算法

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JxfI4XL8-1681383922696)(https://raw.githubusercontent.com/viacheung/img/main/image/image-20210807152232136.png)]

OPT ⻚⾯置换算法(最佳⻚⾯置换算法) :最⻓时间内不再被访问的⻚⾯

FIFO(First In First Out) ⻚⾯置换算法(先进先出⻚⾯置换算法) : 总是淘汰最先进⼊内存的⻚⾯,即选择在内存中驻留时间最久的⻚⾯进⾏淘汰

LRU (Least Currently Used)⻚⾯置换算法(最近最久未使⽤⻚⾯置换算法)

LFU (Least Frequently Used)⻚⾯置换算法(最少使⽤⻚⾯置换算法)

MySQL

数据库三范式

  • 第一范式:列的原子性,数据库表的每一列都不可分割
  • 第二范式:实体的属性完全依赖于主关键字 就是根据主键或者联合主键,将一个表最有关联的属性分别放在一起
  • 第三范式:任何非主属性不依赖于其它非主属性。消除传递依赖

MyISAM(Maiˈzæm) VS Innodb

  • InnoDB 支持事务,MyISAM 不支持
  • InnoDB 支持外键,而 MyISAM 不支持
  • InnoDB 是聚集索引,数据文件是和索引绑在一起的,必须要有主键,通过主键索引效率很高;MyISAM 是非聚集索引,数据文件是分离的,索引保存的是数据文件的指针,主键索引和辅助索引是独立的。
  • Innodb **不支持全文索引,**而 MyISAM 支持全文索引,查询效率上 MyISAM 要高;
  • InnoDB 不保存表的具体行数,MyISAM 用一个变量保存了整个表的行数。
  • MyISAM 采用表级锁(table-level locking);InnoDB 支持行级锁(row-level locking)和表级锁,默认为行级锁。

超键、候选键、主键、外键分别是什么?

超键:在关系中能唯一标识元组的属性集称为关系模式的超键。一个属性可以为作为一个超键,多个属性组合在一起也可以作为一个超键。

超键包含候选键和主键。
候选键:是最小超键,即没有冗余元素的超键。
主键:数据库表中对储存数据对象予以唯一和完整标识的数据列或属性的组合。一个数据列只能有一个主键,且主键的取值不能缺失,即不能为空值(Null)。
外键:在一个表中存在的另一个表的主键称此表的外键

SQL 约束有哪几种?

NOT NULL: 用于控制字段的内容一定不能为空(NULL)。
UNIQUE: 控件字段内容不能重复,一个表允许有多个 Unique 约束。
PRIMARY KEY: 也是用于控件字段内容不能重复,但它在一个表只允许出现一个
FOREIGN KEY: 用于预防破坏表之间连接的动作,也能防止非法数据插入外键列,因为它必须是它指向的那个表中的值之一。
CHECK: 用于控制字段的值范围。

MySQL 中的 varchar 和 char 有什么区别?

定长vs 不定长

char 是一个定长字段,假如申请了char(10)的空间,那么无论实际存储多少内容.该字段都占用 10 个字符,而 varchar 是变长的,也就是说申请的只是最大长度,占用的空间为实际字符长度+1,最后一个字符存储使用了多长的空间.

在检索效率上来讲,char > varchar,因此在使用中,如果确定某个字段的值的长度,可以使用 char,否则应该尽量使用 varchar.

例如存储用户 MD5 加密后的密码,则应该使用 char。

MySQL中 in 和 exists 区别

MySQL中的in语句是把外表和内表作hash 连接,而exists语句是对外表作loop循环,每次loop循环再对内表进行查询。

如果查询的两个表大小相当,那么用in和exists差别不大。 如果两个表中一个较小,一个是大表,则子查询表大的用exists,子查询表小的用in。 in用的A索引 (B直接写出来 In(1,2,3)=>or==1or ==2or ==3)exists用的B索引(从A逐条获取记录然后去B查)

not in 和not exists:如果查询语句使用了not in,那么内外表都进行全表扫描,没有用到索引;而not extsts的子查询依然能用到表上的索引。所以无论那个表大,用not exists都比not in要快。

drop、delete与truncate的区别

索引

img

概念:对数据表里所有记录的引用指针。类似目录 查字典

优点:加快数据的检索速度 在查询的过程中,使用优化隐藏器,提高系统的性能。 缺点:索引的维护成本(增删改的时候要维护)+占物理空间

索引类型?

B+树索引:所有数据存储在叶子节点,复杂度为O(logn),适合范围查询。

哈希索引: 适合等值查询,检索效率高,一次到位。

全文索引:MyISAMInnoDB中都支持使用全文索引,一般在文本类型char,text,varchar类型上创建。

R-Tree索引: 用来对GIS数据类型创建SPATIAL索引

应用层:普通(单个列) 唯一 (值必须唯一,但允许有空值) 复合(组合搜索) 聚簇 非聚簇

物理存储维度

  • 聚集索引:聚集索引就是以主键创建的索引,在叶子节点存储的是表中的数据。(Innodb存储引擎)
  • 非聚集索引:非聚集索引就是以非主键创建的索引,在叶子节点存储的是主键和索引列。(Innodb存储引擎)

逻辑维度:

  • 主键索引:一种特殊的唯一索引,不允许有空值。
  • 普通索引:MySQL中基本索引类型,允许空值和重复值。
  • 联合索引:多个字段创建的索引,使用时遵循最左前缀原则。
  • 唯一索引:索引列中的值必须是唯一的,但是允许为空值。

hash索引

适合等值查询,检索效率高,一次到位。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-p5scv3nn-1681383922697)(https://raw.githubusercontent.com/viacheung/img/main/image/image-20230314141544547.png)]

Hash 索引和 B+树区别是什么?你在设计索引是怎么抉择的?

  • B+树可以进行范围查询,Hash 索引不能。
  • B+树支持联合索引的最左侧原则,Hash 索引不支持。
  • B+树支持 order by 排序,Hash 索引不支持。
  • Hash 索引在等值查询上比 B+树效率更高。(但是索引列的重复值很多的话,Hash冲突,效率降低)。
  • B+树使用 like 进行模糊查询的时候,like 后面(比如%开头)的话可以起到优化的作用,Hash 索引根本无法进行模糊查询。

B+Tree

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-H85qRuTi-1681383922698)(https://raw.githubusercontent.com/viacheung/img/main/image/image-20230314141623990.png)]

B-Tree数据分布在各个节点之中。

B+Tree数据都在叶子节点上,并且增加了顺序访问指针,每个叶子节点都指向相邻的叶子节点的地址。相比B-Tree来说,进行范围查找时只需要查找两个节点,进行遍历即可。而B-Tree需要获取所有节点,相比之下B+Tree效率更高。

B+tree性质:

  • 叶子结点包含了数据+指针,且叶子结点本身自小而大顺序链接。
  • B+ 树中,数据对象的插入和删除仅在叶节点上进行。
  • B+树有2个头指针,一个是树的根节点,一个是最小关键码的叶节点。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gQjBRiHc-1681383922699)(https://raw.githubusercontent.com/viacheung/img/main/image/image-20230314141916699.png)]

为什么索引结构默认使用B+Tree,而不是B-Tree,Hash,二叉树,红黑树?

B+非叶子节点不存数据,占用空间小,磁盘读写代价低

B+适合区间查询,扫一遍叶子节点就行,但B-需要中序2遍历 所以B+树更加适合在区间查询的情况,所以通常B+树用于数据库索引。

Hash:

  • 虽然可以快速定位,但是没有顺序,IO复杂度高;
  • Hash碰撞
  • 索引的是列的全部内容 所以不支持匹配查找

二叉树: 二叉树树化为链表变成线性查询 树的高度不均匀,不能自平衡,查找效率跟数据有关(树的高度),并且IO代价高。

平衡二叉树:只能存储两个节点 多叉树可以存更多 树高降低

红黑树: 树的高度随着数据量增加而增加,IO代价高。

那为什么不是 B 树而是 B+树呢?(理解即可)

  • B+树非叶子节点上是不存储数据的,仅存储键值,而 B 树节点中不仅存储 键值,也会存储数据。innodb 中页的默认大小是 16KB,如果不存储数据,那 么就会存储更多的键值,相应的树的阶数(节点的子节点树)就会更大,树就 会更矮更胖,如此一来我们查找数据进行磁盘的 IO 次数有会再次减少,数据查 询的效率也会更快。
  • B+树索引的所有数据均存储在叶子节点,而且数据是按照顺序排列的,链 表连着的。那么 B+树使得范围查找,排序查找,分组查找以及去重查找变得 异常简单。

非聚集索引与聚集索引区别?

  • 非聚集索引的叶子节点不存储表中的数据,而是存储该列对应的主键(行号)右边这个就是

  • [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SgtHp5YZ-1681383922700)(https://raw.githubusercontent.com/viacheung/img/main/image/image-20230318215801811.png)]

  • 对于InnoDB来说,想要查找数据我们还需要根据主键再去聚集索引中进行查找,这个再根据聚集索引查找数据的过程,我们称为回表。第一次索引一般是顺序IO,回表的操作属于随机IO。需要回表的次数越多,即随机IO次数越多,我们就越倾向于使用全表扫描 。(回表:回表就是先通过数据库索引扫描出数据所在的行,再通过行主键id取出索引中未提供的数据,即基于非主键索引的查询需要多扫描一棵索引树)

  • 通常情况下, 主键索引(聚簇索引)查询只会查一次,而非主键索引(非聚簇索引)需要回表查询多次。当然,如果是覆盖索引的话,查一次即可(单列索引(name)升级为联合索引(name, sex),即可避免回表。)

  • 注意:MyISAM无论主键索引还是二级索引都是非聚簇索引,而InnoDB的主键索引是聚簇索引,二级索引是非聚簇索引。我们自己建的索引基本都是非聚簇索引。

非聚簇索引一定会回表查询吗?

不一定,这涉及到查询语句所要求的字段是否全部命中了索引,如果全部命中了索引,那么就不必再进行回表查询。一个索引包含(覆盖)所有需要查询字段的值,被称之为"覆盖索引"。

举个简单的例子,假设我们在员工表的年龄上建立了索引,那么当进行select score from student where score > 90的查询时,在索引的叶子节点上,已经包含了score 信息,不会再次进行回表查询。 如果select score sex from student where score > 90 需要回表

联合索引是什么?为什么需要注意联合索引中的顺序?

MySQL可以使用多个字段同时建立一个索引,叫做联合索引。在联合索引中,如果想要命中索引,==需要按照建立索引时的字段顺序挨个使用,==否则无法命中索引。

index(name,age,school)
)engine=innodb;

具体原因为:

MySQL使用索引时需要索引有序,假设现在建立了"name,age,school"的联合索引,那么索引的排序为: 先按照name排序,如果name相同,则按照age排序,如果age的值也相等,则按照school进行排序。

当进行查询时,此时索引仅仅按照name严格有序,因此必须首先使用name字段进行等值查询,之后对于匹配到的列而言,其按照age字段严格有序,此时可以使用age字段用做索引查找,以此类推。因此在建立联合索引的时候应该注意索引列的顺序,一般情况下,==将查询需求频繁或者字段选择性高的列放在前面。==此外可以根据特例的查询或者表结构进行单独的调整。

讲一讲MySQL的最左前缀原则?

最左前缀原则就是最左优先,在创建多列索引时,要根据业务需求,where子句中使用最频繁的一列放在最左边mysql会一直向右匹配直到遇到范围查询(>、<、between、like)就停止匹配,比如a = 1 and b = 2 and c > 3 and d = 4 如果建立(a,b,c,d)顺序的索引,d是用不到索引的,如果建立(a,b,d,c)的索引则都可以用到,a,b,d的顺序可以任意调整。

=和in可以乱序,比如a = 1 and b = 2 and c = 3 建立(a,b,c)索引可以任意顺序,mysql的查询优化器会帮你优化成索引可以识别的形式。

讲一讲前缀索引?

索引的字段非常长会占内存空间,也不利于维护。因此把很长字段的前面的公共部分作为一个索引,就会产生超级加倍的效果。但是,我们需要注意,order by不支持前缀索引 。

流程是:

先计算完整列的选择性 : select count(distinct col_1)/count(1) from table_1

再计算不同前缀长度的选择性 :select count(distinct left(col_1,4))/count(1) from table_1

找到最优长度之后,创建前缀索引 : create index idx_front on table_1 (col_1(4))

了解索引下推吗?

MySQL 5.6引入了索引下推优化。默认开启,使用SET optimizer_switch = ‘index_condition_pushdown=off’;可以将其关闭。

有了索引下推优化,可以减少回表次数

在InnoDB中只针对非聚集索引有效

总结:有一个是查数据 判断数据是不是复合条件

一个是查索引 判断索引是不是复合条件 如果符合 再去查数据

例子:

给你这个SQL:

select * from employee where name like '小%' and age=28 and sex='0';

其中,nameage为联合索引(idx_name_age)。

如果是Mysql5.6之前,在idx_name_age索引树,找出所有名字第一个字是“小”的人,拿到它们的主键id,然后回表找出数据行,再去对比年龄和性别等其他字段。如图:

ZWY面试总结_第2张图片

有些朋友可能觉得奇怪,idx_name_age(name,age)不是联合索引嘛?为什么选出包含“小”字后,不再顺便看下年龄age再回表呢,不是更高效嘛?所以呀,MySQL 5.6就引入了索引下推优化,可以在索引遍历过程中,对索引中包含的字段先做判断,直接过滤掉不满足条件的记录,减少回表次数。

因此,MySQL5.6版本之后,选出包含“小”字后,顺表过滤age=28

ZWY面试总结_第3张图片

大表如何添加索引

如果一张表数据量级是千万级别以上的,那么,如何给这张表添加索引?

我们需要知道一点,给表添加索引的时候是会对表加锁的。如果不谨慎操作,有可能出现生产事故的。可以参考以下方法:

  1. 先创建一张跟原表A数据结构相同的新表B
  2. 在新表B添加需要加上的新索引。
  3. 把原表A数据导到新表B
  4. rename新表B为原表的表名A,原表A换别的表名;

怎么查看MySQL语句有没有用到索引?

EXPLAIN SELECT * FROM employees.titles WHERE emp_no='10001' AND title='Senior Engineer' AND from_date='1986-06-26';
  • id:在⼀个⼤的查询语句中每个SELECT关键字都对应⼀个唯⼀的id ,如explain select * from s1 where id = (select id from s1 where name = ‘egon1’);第一个select的id是1,第二个select的id是2。有时候会出现两个select,但是id却都是1,这是因为优化器把子查询变成了连接查询 。

  • select_type:select关键字对应的那个查询的类型,如SIMPLE,PRIMARY,SUBQUERY,DEPENDENT,SNION 。

  • table:每个查询对应的表名 。

  • type:通过 type 字段, 我们判断此次查询是 全表扫描 还是 索引扫描

    通常来说, 不同的 type 类型的性能关系如下: ALL < index < range ~ index_merge < ref < eq_ref < const < system ALL 类型因为是全表扫描, 因此在相同的查询条件下, 它是速度最慢的. 而 index 类型的查询虽然不是全表扫描, 但是它扫描了所有的索引, 因此比 ALL 类型的稍快.

  • possible_key:查询中可能用到的索引*(可以把用不到的删掉,降低优化器的优化时间)* 。

  • key:此字段是 MySQL 在当前查询时所真正使用到的索引。

  • filtered:查询器预测满足下一次查询条件的百分比 。

  • rows 估算 SQL 要查找到结果集需要扫描读取的数据行数. 原则上 rows 越少越好。

  • extra:表示额外信息,如Using where,Start temporary,End temporary,Using temporary等。

为什么官方建议使用自增长主键作为索引?

结合B+Tree的特点,自增主键是连续的,在插入过程中尽量减少页分裂,即使要进行页(16kb)分裂,也只会分裂很少一部分。并且能减少数据的移动,每次插入都是插入到最后。总之就是减少分裂和移动的频率。

如何创建索引

1、 在执行CREATE TABLE时创建索引

2、 使用ALTER TABLE命令去增加索引。(普通索引、UNIQUE索引或PRIMARY KEY索引。)

table_name:增加索引的表名,

column_list指出对哪些列进行索引,多列时各列之间用逗号分隔。

索引名index_name可自己命名,缺省时,MySQL将根据第一个索引列赋一个名称。另外,ALTER TABLE允许在单个语句中更改多个表,因此可以在同时创建多个索引。

ALTER TABLE table_name ADD INDEX index_name (column_list);

3、 使用CREATE INDEX命令创建。

CREATE INDEX index_name ON table_name (column_list);

创建索引时需要注意什么?

非空字段:应该指定列为NOT NULL,除非你想存储NULL。在mysql中,含有空值的列很难进行查询优化,因为它们使得索引、索引的统计信息以及比较运算更加复杂。你应该用0、一个特殊的值或者一个空串代替空值;
取值离散大的字段:(变量各个取值之间的差异程度)的列放到联合索引的前面,可以通过count()函数查看字段的差异值,返回值越大说明字段的唯一值越多字段的离散程度高;
索引字段越小越好:数据库的数据存储以页为单位一页存储的数据越多一次IO操作获取的数据越大效率越高

建索引的原则有哪些?

1、最左前缀匹配原则,范围查询(>、<、between、like)停止匹配

2、=和in可以乱序,mysql的查询优化器会帮你优化成索引可以识别的形式。

3、尽量选择区分度高的列作为索引,区分度的公式是count(distinct col)/count(*),表示字段不重复的比例,比例越大我们扫描的记录数越少,唯一键的区分度是1,而一些状态、性别字段可能在大数据面前区分度就是0,那可能有人会问,这个比例有什么经验值吗?使用场景不同,这个值也很难确定,一般需要join的字段我们都要求是0.1以上,即平均1条扫描10条记录。

4、索引列不能参与计算,保持列“干净”,比如from_unixtime(create_time) = ’2014-05-29’就不能使用到索引,原因很简单,b+树中存的都是数据表中的字段值,但进行检索时,需要把所有元素都应用函数才能比较,显然成本太大。所以语句应该写成create_time = unix_timestamp(’2014-05-29’)。

from_unixtime:时间戳(1970-1-1至今的秒)->yyyy-MM-dd HH:mm:ss unix_timestamp->yyyy-MM-dd HH:mm:ss

5、尽量的扩展索引,不要新建索引。比如表中已经有a的索引,现在要加(a,b)的索引,那么只需要修改原来的索引即可。(联合索引)

使用索引查询一定能提高查询的性能吗?(优缺点)

优点:

1、通常通过索引查询数据比全表扫描要快。但是我们也必须注意到它的代价。

2、唯一索引可以保证数据库表中每一行的数据的唯一性

缺点:

  • 创建索引和维护索引要耗费时间
  • 索引需要占物理空间,除了数据表占用数据空间之外,每一个索引还要占用一定的物理空间
  • 以表中的数据进行增、删、改的时候,索引也要动态的维护。

什么情况下不走索引(索引失效)?

1、使用!= 或者 < >,not in NOT EXISTS 导致索引失效
2、类型不一致导致的索引失效
3、函数导致的索引失效
如:

SELECT * FROM user WHERE DATE(create_time) = ‘2020-09-03’;
如果使用函数在索引列,这是不走索引的。

4、运算符导致的索引失效
SELECT * FROM user WHERE age - 1 = 20;
如果你对列进行了(+,-,*,/,!), 那么都将不会走索引。

5、OR引起的索引失效
SELECT * FROM user WHERE name = ‘张三’ OR height = ‘175’;
OR导致索引是在特定情况下的,并不是所有的OR都是使索引失效,如果OR连接的是同一个字段,那么索引不会失效,反之索引失效。

6、模糊搜索导致的索引失效
SELECT * FROM user WHERE name LIKE ‘%冰’;
当%放在匹配字段前是不走索引的,放在后面才会走索引。

7、mysql 估计使用全表扫描要比使用索引快,则不使用索引。

8、 如果字段类型是字符串,where时一定用引号括起来,否则索引失效

9、索引字段上使用is null, is not null,可能导致索引失效。

事务

MVCC

多版本并发控制。MVCC 的实现,是通过保存数据在某个时间点的快照来实现的。根据事务开始的时间不同,每个事务对同一张表,同一时刻看到的数据可能是不一样的。

前置知识

  • 快照读:读取的是记录数据的可见版本(有旧的版本)。不加锁,普通的select语句都是快照读。

  • 当前读:读取的是记录数据的最新版本,显式加锁的都是当前读。

  • ROW ID:隐藏的自增 ID,如果表没有主键,InnoDB 会自动按 ROW ID 产生一个聚集索引树。

  • 事务 ID:记录最后一次修改该记录的事务 ID。

  • 回滚指针:多个事务并行操作某一行数据时,不同事务对该行数据的修改会产生多个版本然后通过回滚指针(roll_pointer),连成一个链表,这个链表就称为版本链。如下:

img
  • 隐式字段:对于InnoDB存储引擎,每一行记录都有两个隐藏列trx_id(当前事务id)、roll_pointer,如果表中没有主键和非NULL唯一键时,则还会有第三个隐藏的主键列row_id
  • undo log:回滚日志,用于记录数据被修改前的信息。在表记录修改之前,会先把数据拷贝到undo log里,如果事务回滚,即可以通过undo log来还原数据

delete一条记录时,undo log中会记录一条对应的insert记录,当update一条记录时,它记录一条对应相反的update记录。

1、事务回滚时,保证原子性和一致性。
2、用于MVCC快照读。

什么是Read View

Read View是什么呢? 它就是事务执行SQL语句时,产生的读视图。每个SQL语句执行前都会得到一个Read View。它主要是用来做可见性判断的,即判断当前事务可见哪个版本的数据~

Read View中,有这几个重要的属性。

  • m_ids:当前系统中,那些未提交的读写事务ID列表。
  • min_limit_id:表示在生成Read View时,当前系统中活跃的读写事务中最小的事务id,即m_ids中的最小值。
  • max_limit_id:表示生成Read View时,系统中应该分配给下一个事务的id值。
  • creator_trx_id: 创建当前Read View的事务ID

Read view 匹配条件规则(很重要)

  1. 如果数据事务ID trx_id < min_limit_id,表明生成该版本的事务在生成Read View前,已经提交(因为事务ID是递增的),所以该版本可以被当前事务访问。
  2. 如果trx_id>= max_limit_id,表明生成该版本的事务在生成Read View后才生成,所以该版本不可以被当前事务访问。
  3. 如果 min_limit_id =,需腰分3种情况讨论
  • (1).如果m_ids包含trx_id,则代表Read View生成时刻,这个事务还未提交,但是如果数据的trx_id等于creator_trx_id的话,表明数据是自己生成的,因此是可见的。
  • (2)如果m_ids包含trx_id,并且trx_id不等于creator_trx_id( 创建当前Read View的事务ID),则Read View生成时,事务未提交,并且不是自己生产的,所以当前事务也是看不见的;
  • (3).如果m_ids不包含trx_id,则说明你这个事务在Read View生成之前就已经提交了,修改的结果,当前事务是能看见的。
查询一条记录,基于MVCC,是怎样的流程
  1. 获取事务自己的版本号,即事务ID(trx_id)
  2. 获取Read View
  3. 查询得到的数据,然后Read View中的事务版本号进行比较。
  4. 如果不符合Read View的可见性规则, 即就需要Undo log中历史快照;
  5. 最后返回符合规则的数据

InnoDB 实现MVCC,是通过Read View+ Undo Log实现的,Undo Log保存了历史快照,Read View可见性规则帮助判断当前版本的数据是否可见。

举例:

RC级别下:

先插入一条这个数据

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-p5nQ3WXK-1681383922701)(https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/16de185a27a74c2a83674cc96eddea1c~tplv-k3u1fbpfcp-zoom-in-crop-mark:4536:0:0:0.awebp)]

ZWY面试总结_第4张图片

事务AB先后开启事务 trx_id 为100 101

事务A第一次查到name是孙权 (trx_id=最新trx_id 符合可见性原则)

事务B把name改为曹操 最新版本链101

事务A再查(trx_id=101 但此时因为B提交了 m_ids里面已经没101了 所以依旧符合可见性原则 查出来是曹操) 出现不可重读的问题

RR级别下:

  • 在读已提交(RC)隔离级别下,同一个事务里面,每一次查询都会产生一个新的Read View副本,这样就可能造成同一个事务里前后读取数据可能不一致的问题(不可重复读并发问题)。
  • 而在可重复读(RR)隔离级别下,一个事务里只会获取一次read view,都是副本共用的,从而保证每次查询的数据都是一样的。

分析一下:

主要是A再查这部分:

事务A再查(trx_id=101 因为A事务共用一个readview 所以m_ids里面有101 但creator_trx_id(100)!=trx_id(101) 不符合可见性规则,版本链roll_pointer跳到下一个版本,trx_id=100这个记录,再次校验是否可见,creator_trx_id (100) 等于trx_id(100),所以查到孙权这个记录) 出现不可重读的问题


如图,首先 insert 语句向表 t1 中插入了一条数据,a 字段为 1,b 字段为 1, ROW ID 也为 1 ,事务 ID 假设为 1,回滚指针假设为 null。当执行 update t1 set b=666 where a=1 时,大致步骤如下:

  • 数据库会先对满足 a=1 的行加排他锁;
  • 然后将原记录复制到 undo 表空间中;
  • 修改 b 字段的值为 666,修改事务 ID 为 2;
  • 并通过隐藏的回滚指针指向 undo log 中的历史记录;
  • 事务提交,释放前面对满足 a=1 的行所加的排他锁。

总结:

InnoDB 每一行数据都有一个隐藏的回滚指针,用于指向该行修改前的最后一个历史版本,这个历史版本存放在 undo log 中。如果要执行更新操作,会将原记录放入 undo log 中,并通过隐藏的回滚指针指向 undo log 中的原记录。其它事务此时需要查询时,就是查询 undo log 中这行数据的最后一个历史版本。

MVCC 最大的好处是读不加锁,读写不冲突,极大地增加了 MySQL 的并发性。通过 MVCC,保证了事务 ACID 中的 I(隔离性)特性。

如何处理大事务和长事务?请给出一些处理方法。

处理大事务和长事务是数据库设计和优化中非常重要的一部分,以下是一些常用的处理方法:

  • 大事务拆分为小事务:将大事务拆分为多个小事务,减少每个事务操作的数据量,可以减少锁竞争和死锁的风险,提高并发性能。
  • 优化查询语句:对于长事务中的查询操作,可以通过优化查询语句来提高查询性能,如添加索引、优化SQL结构等。
  • 避免长时间占用锁:长事务会占用锁资源,导致其他事务无法访问相应的数据,因此需要尽可能缩短事务的执行时间,避免长时间占用锁。
  • 避免长时间的事务等待:长事务可能会导致其他事务的等待时间过长,影响系统的性能和可用性,因此需要尽可能缩短事务的执行时间,避免长时间的事务等待。
  • 优化事务日志:长事务会占用大量的事务日志,导致数据库性能下降,因此需要通过优化事务日志的写入和刷盘策略来提高性能。
  • 使用定时任务:长时间运行的事务可以通过定时任务来定期执行,以避免长时间占用资源。
  • 适当增加硬件资源:如果以上方法不能解决问题,可以适当增加硬件资源,如增加内存、CPU、存储等,以提高系统性能。

如何优化 MySQL 事务的性能?请列举一些优化方法。

事务?

事务是一个不可分割的数据库操作序列 事务是逻辑上的⼀组操作,要么都执⾏,要么都不执⾏

事务的四大特性

事务,由一个有限的数据库操作序列构成,这些操作要么全部执行,要么全部不执行,是一个不可分割的工作单位。

原子性: 事务作为一个整体被执行,包含在其中的对数据库的操作要么全部都执行,要么都不执行。

一致性: 指在事务开始之前和事务结束以后,数据不会被破坏,假如A账户给B账户转10块钱,不管成功与否,A和B的总金额是不变的。

隔离性: 多个事务并发访问时,事务之间是相互隔离的,一个事务不应该被其他事务干扰,多个并发事务之间要相互隔离。

持久性: 表示事务完成提交后,该事务对数据库所作的操作更改,将持久地保存在数据库之中

事务的实现原理

重做日志文件(redo log)和回滚日志(undo log)实现的。

提交一个事务必须先将该事务的所有日志写入到redo log进行持久化,数据库就可以通过重做日志来保证事务的原子性和持久性。

每当有修改事务时,还会产生 undo log,如果需要回滚,则根据 undo log 的反向语句进行逻辑操作,比如 insert 一条记录就 delete 一条记录。undo log 主要实现数据库的一致性。

MySQL事务日志

包括二进制日志binlog(归档日志)、事务日志redo log(重做日志)和undo log(回滚日志)。

ZWY面试总结_第5张图片
redolog

InnoDB存储引擎独有的,它让MySQL有了崩溃恢复的能力

MySQL实例挂了或者宕机了,重启的时候InnoDB存储引擎会使用rede log日志恢复数据,保证事务的持久性和完整性。如下图:

ZWY面试总结_第6张图片

MySQL中数据是以页存储,当查询一条记录时,硬盘会把一整页的数据加载出来(数据页)放到Buffer Pool中。后续的查询都是先从Buffer Pool中找,没有找到再去硬盘加载其他的数据页直到命中,这样子可以减少磁盘IO的次数,提高性能。更新数据的时候也是一样,优先去Buffer Pool中找,如果存在需要更新的数据就直接更新。然后会把“在某个数据页做了什么修改”记录到重做日志缓存(redo log buffer)里,在刷盘的时候会写入redo log日志文件里。

ZWY面试总结_第7张图片
内存池

读取页 操作:

  • 首先将从磁盘读到的页存放在缓冲池中
  • 下一次再读相同的页时,首先判断该页是否在缓冲池中。若在缓冲池中,称该页在缓冲池中被命中,直接读取该页。否则,读取磁盘上的页。

修改页操作:

  • 首先修改在缓冲池中的页;然后再以一定的频率刷新到磁盘上。

脏页:就发生在修改这个操作中,如果缓冲池中的页已经被修改了,但是还没有刷新到磁盘上,那么我们就称缓冲池中的这页是 ”脏页“,即缓冲池中的页的版本要比磁盘的新。
缓冲池的大小直接影响着数据库的整体性能。

| 每条redo记录由“表空间号+数据页号+偏移量+修改数据长度+具体修改的数据”组成|

后台线程

后台线程的主要作用就是刷新内存池中的数据,保证内存池中缓存的是最近的数据;此外将已修改的数据文件刷新到磁盘文件,同时保证在数据库发生异常的情况下 InnoDB 能恢复到正常运行状态。

WAL 策略

当缓冲池中的某页数据被修改后,该页就被标记为 ”脏页“,脏页的数据会被定期刷新到磁盘上。

倘若每次一个页发生变化,就将新页的版本刷新到磁盘,那么这个开销是非常大的。并且,如果热点数据都集中在某几个页中,那么数据库的性能将变得非常差。另外,如果在从缓冲池将页的新版本刷新到磁盘时发生了宕机,那么这个数据就不能恢复了。

所以,为了避免发生数据丢失的问题,当前事务数据库系统(并非 MySQL 所独有)普遍都采用了 WAL(Write Ahead Log预写日志)策略:即当事务提交时,先写重做日志(redo log),再修改页(先修改缓冲池,再刷新到磁盘);当由于发生宕机而导致数据丢失时,通过 redo log 来完成数据的恢复。这也是事务 ACID 中 D(Durability 持久性)的要求。

有了 redo log,InnoDB 就可以保证即使数据库发生异常重启,之前提交的记录都不会丢失,这个能力称为 crash-safe

  • redo log file 不能设置得太大,如果设置得很大,在恢复时可能需要很长的时间
  • redo log file 又不能设置得太小了,否则可能导致一个事务的日志需要多次切换重做日志文件
刷盘时机

理想情况下,事务一提交就会进行刷盘操作,但是实际上是刷盘的时机是根据策略来决定的。

InnoDB存储引擎为redo log的刷盘策略提供了innodb_flush_log_at_trx_commit参数,它支持三种策略:

  • 0:设置为0的时候,每次提交事务时不刷盘。
  • 1:设置为1的时候,每次提交事务时刷盘。
  • 2:设置为2的时候,每次提交事务时都只把redo log buffer写入page cache

innodb_flush_log_at_trx_commit参数默认为1,当事务提交的时候会调用fsyncredo log进行刷盘,将redo log buffer写入redo log文件中。

另外,Innodb存储引擎有一个后台线程,每隔1秒,就会把会redo log buffer中的内容写入到文件系统缓存page cache,然后调用fsync刷盘。(因此这三种策略都会有刷盘)

img
三种情况:

1、innodb_flush_log_at_trx_commit = 0

img

如果宕机了或者MySQL挂了可能造成1秒内的数据丢失。

2、innodb_flush_log_at_trx_commit = 1

只要事务提交成功,redo log记录就一定在磁盘里,不会有任务数据丢失。

如果执行事务的时候MySQL挂了或者宕机了,这部分日志丢失了,但是因为事务没有提交,所以日志丢了也不会有损失。

ZWY面试总结_第8张图片

3、innodb_flush_log_at_trx_commit = 2

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VCH4Gh4X-1681383922703)(https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/965c0f87523b422784f475d68b6cfacc~tplv-k3u1fbpfcp-zoom-in-crop-mark:4536:0:0:0.awebp)]

当事务提交成功时,redo log buffer日志会被写入page cache,然后后台线程会刷盘写入redo log,由于后台线程是1秒执行一次所以宕机或者MySQL挂了可能造成1秒内的数据丢失。

1和3实际上都是事务提交了,但是没有被刷进盘,所以会造成1s的数据丢失,而2是事务提交就刷盘,不会丢失数据;

日志文件组

硬盘上存储redo log日志文件以一个日志文件组的形式出现,每个的redo log文件大小都是一样的(至少需要两个)。它采用的是环形数组形式,从头开始写,写到末尾回到头循环写,如下图所示:

ZWY面试总结_第9张图片

日志文件组中有两个重要的属性,分别是witre pos、checkpoint

  • wirte pos:是当前记录的位置,一边写一边后移。
  • checkpoint:是当前要擦除的位置,也是后台推移。

checkpoint:在 redo log file 中找到一个位置,将这个位置前的页都刷新到磁盘中去,这个位置就称为 CheckPoint(检查点)。举个例子来具体解释下:一组 4 个文件,每个文件的大小是 1GB,那么总共就有 4GB 的 redo log file 空间。write pos 是当前 redo log 记录的位置,随着不断地写入磁盘,write pos 也不断地往后移,就像我们上文说的,写到 file 3 末尾后就回到 file 0 开头。CheckPoint 是当前要擦除的位置(将 Checkpoint 之前的页刷新回磁盘),也是往后推移并且循环的:

每次刷盘redo log记录到日志文件组中,wirte log位置就会后移更新。

每次MySQL加载日志文件组恢复数据时,会清空加载过的redo log,并把checkpoint后移更新。

write poscheckpoint 之间的还空着的部分可以用来写入新的 redo log 记录。

如果 witre pos追上checkpoint,表示日志文件组满了,这时候不能再写入新的redo log记录,MySQL得停下来,清空一些记录,把checkpoint推进一下。

CheckPoint 技术

有了 redo log 我们仍然面临这样 3 个问题:

1)缓冲池不是无限大的,也就是说不能没完没了的存储我们的数据等待一起刷新到磁盘

2)redo log 是循环使用而不是无限大的(也许可以,但是成本太高,同时不便于运维),那么当所有的 redo log file 都写满了怎么办?

3)当数据库运行了几个月甚至几年时,这时如果发生宕机,重新应用 redo log 的时间会非常久,此时恢复的代价将会非常大。

因此 Checkpoint 技术的目的就是解决上述问题:

  • 缓冲池不够用时,将脏页刷新到磁盘缓冲池的空间无法存放新读取到的页,用LRU 算法)

(最频繁使用的页在 LRU 列表(LRU List)的前端,最少使用的页在 LRU 列表的尾端;当缓冲池的空间无法存放新读取到的页时,将首先释放 LRU 列表中尾端的页。这个被释放出来(溢出)的页,如果是脏页,那么就需要强制执行 CheckPoint,将脏页刷新到磁盘中去。)

  • redo log 不可用(没啥用)时,将脏页刷新到磁盘

  • 缩短数据库的恢复时间(当数据库宕机,不需要重做所有日志,只需要对Checkpoint后面的redolog进行恢复,缩短恢复时间)


所谓 CheckPoint 技术简单来说其实就是在 redo log file 中找到一个位置,将这个位置前的页都刷新到磁盘中去,这个位置就称为 CheckPoint(检查点)。

针对上面这三点我们依次来解释下:

1)缓冲池不够用时,将脏页刷新到磁盘:所谓缓冲池不够用的意思就是缓冲池的空间无法存放新读取到的页,这个时候 InnoDB 引擎会怎么办呢?LRU 算法。InnoDB 存储引擎对传统的 LRU 算法做了一些优化,用其来管理缓冲池这块空间。

总的思路还是传统 LRU 那套,具体的优化细节这里就不再赘述了:即最频繁使用的页在 LRU 列表(LRU List)的前端,最少使用的页在 LRU 列表的尾端;当缓冲池的空间无法存放新读取到的页时,将首先释放 LRU 列表中尾端的页。这个被释放出来(溢出)的页,如果是脏页,那么就需要强制执行 CheckPoint,将脏页刷新到磁盘中去。

2)redo log 不可用时,可以覆盖重用

所谓 redo log 不可用就是所有的 redo log file 都写满了。但事实上,其实 redo log 中的数据并不是时时刻刻都是有用的,那些已经不再需要的部分就称为 ”可以被重用的部分“,即当数据库发生宕机时,数据库恢复操作不需要这部分的 redo log,因此这部分就可以被覆盖重用(或者说被擦除)。

3)缩短数据库的恢复时间:当数据库发生宕机时,数据库不需要重做所有的日志,因为 Checkpoint 之前的页都已经刷新回磁盘。故数据库只需对 Checkpoint 后的 redo log 进行恢复就行了。这显然大大缩短了恢复的时间。

综上所述,Checkpoint 所做的事情无外乎是将缓冲池中的脏页刷新到磁盘。不同之处在于每次刷新多少页到磁盘,每次从哪里取脏页,以及什么时间触发 Checkpoint

redo log小结

redo log的作用和它的刷盘时机、存储形式。

可以思考一个问题:只要每次把修改后的数据页直接刷盘不就好了,为什么还要用redo log刷盘?不都是刷盘吗?有什么区别?

实际上,数据页大小是16KB,刷盘比较耗时,可能就修改了数据页的几byte数据,没有必要把整页的数据刷盘。而且数据页刷盘都是随机写,因为一个数据页对应的位置可能是在硬盘文件的随机位置,所以性能很差。

如果是写redo log,一行记录就占了几十byte,只要包含了表空间号、数据页号、磁盘文件偏移量、修改值,再加上是顺序写,所以刷盘效率很高。

所以用 redo log 形式记录修改内容,性能会远远超过刷数据页的方式,这也让数据库的并发能力更强。

有了 bin log 为什么还需要 redo log?

MySQL 架构可以分成俩层,一层是 Server 层,它主要做的是 MySQL 功能层面的事情;另一层就是存储引擎,负责存储与提取相关的具体事宜。

redo log 是 InnoDB 引擎特有的日志,而 Server 层也有自己的日志,包括错误日志(error log)、二进制日志(binlog)、慢查询日志(slow query log)、查询日志(log)。

binlog 日志只能用于归档,因此 binlog 也被称为归档日志,显然如果 MySQL 只依靠 binlog 等这四种日志是没有 crash-safe 能力的,所以为了弥补这种先天的不足,得益于 MySQL 可插拔的存储引擎架构,InnoDB 开发了另外一套日志系统 — 也就是 redo log 来实现 crash-safe 能力。

这就是为什么有了 bin log 为什么还需要 redo log 的答案。

回顾下 redo log 存储的东西,可以发现 redo log 是物理日志,记录的是 “在某个数据页上做了什么修改”。

另外,还有一点不同的是:binlog 是追加写入的,就是说 binlog 文件写到一定大小后会切换到下一个,并不会覆盖以前的日志;而 redo log 是循环写入的。

总结:写的内容少,目的一样, 性能好

binlog

redo log是物理日志,记录的是“在某个数据页做了什么修改”,属于Innodb存储引擎。

binlog日志是逻辑日志,记录内容是语句的原始逻辑,属于MySQL Server层。所有的存储引擎只要发生了数据更新,都会产生binlog日志。

binlog日志的作用

可以说MySQL数据库的数据备份、主备、主主、住从都离不开binlog,需要依赖binlog来同步数据,保证数据一致性。binlog会记录所有涉及更新数据的逻辑规则,并且按顺序写。

记录格式

可以通过binlog_format参数设置,有以下三种:

  • statement: 基于 SQL 语句的模式,某些语句和函数如 UUID, LOAD DATA INFILE 等在复制过程可能导致数据不一致甚至出错。
  • row: 基于行的模式,记录的是行的变化,很安全。但是 binlog 会大很多
  • mixed: 混合模式,根据语句来选用是 statement 还是 row 模式

1、statement记录SQL语句原文,但是有个问题,比如update T set update_time = now() where id = 1,更新的是当前系统的时间,可能和原来数据库的数据不一样,所以->row

2、记录的不再是简单的SQL语句了,还包含了操作的具体数据,记录内容如下

ZWY面试总结_第10张图片

但是这种格式需要大量的容量来记录,比较占用空间,恢复与同步时会更消耗IO资源,影响执行速度。->3

3、所以又有了一种折中方案,设置为mixed,记录的内容是前两者的混合。MySQL会判断这条SQL语句是否会引起数据不一致,如果是就用row格式,否则就用statement格式。

redo log 是在事务的执行过程中,开始写入 redo 中。防止在发生故障的时间点,尚有脏页(当内存数据页跟磁盘数据页内容不一致的时候,我们称这个内存页为“脏页”。)未写入磁盘,在重启 MySQL 服务的时候,根据 redo log 进行重做,从而达到事务的未入磁盘数据进行持久化这一特性。RedoLog 是为了实现事务的持久性而出现的产物。

写入机制

binlog的写入时机为事务执行过程中,先把日志写到binlog cache,事务提交的时候再把binlog cache写到binlog文件中(实际先会写入page cache,然后再由fsync写入binlog文件)。

因为一个事务的binlog不能被拆开,无论这个事务多大,也要确保一次性写入,所以系统会给每个线程分配一块内存作为binlog cache。可以通过binlog_cache_size参数控制单线程binlog_cache大小,如果存储内容超过了这个参数,就要暂存到磁盘。

binlog日志刷盘流程如下:

img
  • 上图的write,是指把日志写入到文件系统的page cache,并没有把数据持久化硬盘,所以速度比较快。
  • 上图的 fsync才是将数据库持久化到硬盘的操作。

writefsync的时机可以由参数sync_binlog控制,可以配置成0、1、N(N>1)

  • 设置成0时:表示每次提交事务都只会write,由系统自行判断什么时候执行fsync
  • 设置成1时:表示每次提交事务都会执行fsync,就和redo log日志刷盘流程一样。
  • 设置成N时:表示每次提交事务都会write,但是积累N个事务后才fsync

1、sync_bilog = 0设置成0,只把日志写入page cache虽然性能得到了提高,但是事务提交了fsync的时候宕机了,可能造成binlog日志的丢失。

2、在出现IO瓶颈的场景里,将sync_binlog设置成一个比较大的值,可以提升性能。同样的,如果机器宕机,会丢失最近N个事务的binlog日志。

3、不会出现日志丢失

和redolog一样,提交事务了但宕机了没有fsync就会丢失日志

两阶段提交

redo log(重做日志)让InnoDB存储引擎有了崩溃恢复的能力。

binlog(归档日志)保证了MySQL集群架构数据的一致性

虽然它们都属于持久化的保证,但是侧重点不一样。

在执行更新语句过程,会记录redo logbinlog两块日志,以基本的事务为单位,redo log在事务执行过程中可以不断写入(提交事务后刷盘),而binlog日志只有在提交事务的时候才会写入,所以它们写入的时机不一样。

ZWY面试总结_第11张图片

思考一个问题,如果redo log和binlog两份日志之间的逻辑不一样,会出现什么问题呢?MySQL是怎么解决这个问题的呢?

比如有这样一个场景,假设有这么一条语句update T set c = 1 where id = 2(c原值为0),假如执行过程中写完redo log日志后,在写入binlog的时候发生了异常,会出现什么情况呢?

如下图:

img

由于binlog日志没写完就异常,这个时候binlog日志里面没有对应的修改记录,之后使用binlog同步的数据的时候就会少这一次的更新,这一行数据c = 0,而原库使用redo log日志恢复(恢复数据库),这一行数据c = 1 ,最终数据不一致。如下图:

ZWY面试总结_第12张图片

为了解决两份日志之间的逻辑不一致的问题,InnoDB存储引擎使用两阶段提交方案。

redo log日志的写入拆分成两个步骤preparecommit,如下图: ZWY面试总结_第13张图片

使用两阶段提交后,写入binlog时发生异常也没关系,因为MySQL根据redo log日志恢复数据时,发现redo log日志处于prepare阶段,并且没有对应binlog日志(根据事务id对应),所以就会回滚事务。

ZWY面试总结_第14张图片

本质就是把redolog分为两个阶段(prepare+commit)把写入binlog插入其中,一旦写binlog失败,发生异常,也就没有commit了,也没有binlog了,那么直接判断redolog处于prepare以及binlog不存在条件成立,即可回滚事务。

再想一个场景,redo logo设置commit阶段发生异常,事务会不会回滚呢?

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mD45Y0FQ-1681383922704)(https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/9cb4826b521e4f018713e2f1fbfaca08~tplv-k3u1fbpfcp-zoom-in-crop-mark:4536:0:0:0.awebp)]

并不会回滚事务,虽然redo log是处于prepare阶段,但是存在对应的事务binlog日志,所以MySQL认为是完整的,所以不会回滚事务。

undo log

想要保证事务的原子性,就需要在发生异常时,对已经执行的操作进行回滚,在MySQL中恢复机制是通过undo log(回滚日志)实现的,所有事务进行的修改都会先被记录到这个回滚日志,然后再执行其他相关的操作。如果执行过程中遇到异常的话,我们直接利用回滚日志中的信息将数据回滚到修改之前的样子。并且,回滚日志会先于数据持久化到磁盘上。这样就保证了即使遇到数据库突然宕机等情况,当用户再次启动数据库的时候,数据库还能够通过查询回滚日志来回滚将之前未完成的事务。

另外,MVCC的实现依赖:隐藏字段、Read Viewundo log。在底层实现中,InnoDB通过数据行的DB_TRX_ID(事务Id)和Read View来判断数据的可见性,如不可见,则通过数据行DB_ROLL_PTR找到undo log中的历史版本。每个事务读到的数据版本可能是不一样的,在同一个事务里,用户只能看到该事务创建Read View之前已经提交的修改和该事务本身做的修改。

总结

MySQL InnoDB引擎使用redo log日志保证事务的持久性,使用undo log日志保证事务的原子性

MySQL数据库的数据备份、主备、主主、主从离不开binlog,需要依赖binlog来同步数据,保证数据的一致性。binlog 的主要目的是复制和恢复。

undo log 用来回滚行记录到某个版本。事务未提交之前,Undo 保存了未提交之前的版本数据(修改前的数据)Undo 中的数据可作为数据旧版本快照供其他并发事务进行快照读。是为了实现事务的原子性而出现的产物,在 MySQL innodb 存储引擎中用来实现多版本并发控制。

事务A修改时,这时B事务来读 直接读Undolog的内容

在事务中可以混合使用存储引擎吗?

尽量不要

如果该事务需要回滚,非事务型的表上的变更就无法撤销(无法通过undolog),这会导致数据库处于不一致的状态

MySQL中是如何实现事务隔离的?

读未提交和串行化基本上是不需要考虑的隔离级别,前者不加锁限制,后者相当于单线程执行,效率太差

MySQL 在可重复读级别解决了幻读问题,是通过行锁和间隙锁的组合 Next-Key 锁实现的。

数据库的乐观锁和悲观锁是什么?怎么实现的?

  • 悲观锁:假定会发生并发冲突,在查询完数据的时候就把事务锁起来,直到提交事务。实现方式:使用数据库中的锁机制(多写)
  • 乐观锁:假设不会发生并发冲突,只在提交操作时检查是否违反数据完整性。在修改数据的时候把事务锁起来,通过version的方式来进行锁定。实现方式:乐一般会使用版本号机制或CAS算法实现。(多读)

InnoDB引擎的行锁是怎么实现的?

InnoDB是基于索引来完成行锁

四⼤特性(ACID)

\1. 原⼦性(Atomicity): 事务要么全部发生,要么全部不发生
\2. ⼀致性(Consistency): 执⾏事务前后,数据保持⼀致
\3. 隔离性(Isolation): ,⼀个⽤户的事务不被其他事务所⼲扰
\4. 持久性(Durability): ⼀个事务被提交之后。它对数据库中数据的改变是持久的,

并发事务带来哪些问题

脏读 写中读 事务 A 读取了事务 B 更新的数据,然后 B 回滚操作,那么 A 读取到的数据是脏数据

丢失修改 写写

不可重复读:两次读的不一样 读写读

幻读:突然有数据插入

事务隔离级别有哪些?MySQL的默认隔离级别是?

实际上就是数据控制和数据一致性的一个平衡

READ-UNCOMMITTED(读取未提交) :最低

READ-COMMITTED(读取已提交) :防止脏读

REPEATABLE-READ(可重复读) :可以阻⽌脏读和不可重复读,但幻读仍有可能发⽣。

SERIALIZABLE(可串⾏化) :最⾼的隔离级别 ,该级别可以防⽌脏读、不可重复读以及幻读

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ob4lDbZp-1681383922705)(https://raw.githubusercontent.com/viacheung/img/main/image/image-20230313170422853.png)]

**脏读:**一个事务读取数据并且对数据进行了修改,这个修改对其他事务来说是可见的,即使当前事务没有提交。这时另外一个事务读取了这个还未提交的数据,但第一个事务突然回滚,导致数据并没有被提交到数据库,那第二个事务读取到的就是脏数据,这也就是脏读的由来。

**丢失修改:**第一个事务中修改了这个数据后,第二个事务也修改了这个数据。这样第一个事务内的修改结果就被丢失,因此称为丢失修改。

**不可重复读:**事务 1 读取某表中的数据 A=20,事务 2 也读取 A=20,事务 1 修改 A=A-1,事务 2 再次读取 A =19,此时读取的结果和第一次读取的结果不同。

**幻读:**事务 2 读取某个范围的数据,事务 1 在这个范围插入了新的数据,事务 2 再次读取这个范围的数据发现相比于第一次读取的结果多了新的数据。

事务隔离机制的实现基于锁机制和并发调度。其中并发调度使用的是MVVC(多版本并发控制),通过保存修改的旧版本信息来支持并发一致性读和回滚等特性。

mysql默认REPEATABLE-READ(可重读) 但mysql的InnoDB 存储引擎 使⽤的是Next-Key Lock 锁算法 ,因此可以避免幻读的产⽣ ,即达到了SQL标准的 SERIALIZABLE(可串⾏化) 隔离级别。

InnoDB 存储引擎在 分布式事务 的情况下⼀般会⽤到 SERIALIZABLE(可串⾏化) 隔离级别。

Mysql为什么会选择RR作为默认隔离级别呢?

我们的MySQL数据库一般都是集群部署的,会有主库、从库。主库负责写,从库负责读。主库写入之后,会进行主从复制,把数据同步到从库。

insert into t values(666,2),(233,1);

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xAeen8bQ-1681383922707)(https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/015816c04e4d438982116aa686177ffd~tplv-k3u1fbpfcp-zoom-in-crop-mark:4536:0:0:0.awebp)]

执行这两个事务,如果是RC级别,主库数据是(888,2)(233,2)但是binlog是根据事务提交顺序写进去,所以到从库是先B再A 那就数据不一致了

而在RR(可重复读的数据库隔离级别)下,因为会有间隙锁的存在,这种情况就不会发生,因此,Mysql默认选择RR作为隔离级别。

很多大厂为什么选择RC数据库隔离级别?

互联网大厂和一些传统企业,最明显的特点就是高并发。那么大厂就更倾向提高系统的并发读

RC隔离级别,并发度是会比RR更好的,为什么呢?

因为RC隔离级别,加锁过程中,只需要对修改的记录加行锁。而RR隔离级别,还需要加Gap LockNext-Key Lock,即RR隔离级别下,出现死锁的概率大很多。并且,RC还支持半一致读,可以大大的减少了更新语句时行锁的冲突;如果对于不满足更新条件的记录,就可以提前释放锁,提升并发度

解决幻读

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZJdZQqTR-1681383922708)(https://raw.githubusercontent.com/viacheung/img/main/image/image-20230319011414887.png)]

MVCC加上间隙锁的方式
(1)在快照读读情况下,mysql通过mvcc来避免幻读。(无锁化)
(2)在当前读读情况下,mysql通过next-key来避免幻读。锁住某个条件下的数据不能更改。

MySQL的隔离级别是如何实现的?

MySQL的隔离级别是通过MVCC和锁机制来实现的。

  • RU隔离级别最低,没有加锁,存在脏读问题。事务读不加锁,不阻塞其他事务的读和写
  • RC和RR隔离级别可以通过MVCC来实现。
  • 串行化是通过锁机制实现。读加共享锁,写加排他锁,读写互斥。如果有未提交的事务正在修改某些行,所有select这些行的语句都会阻塞。

为何加锁

多用户环境下保证数据库完整性和一致性。

ZWY面试总结_第15张图片

锁机制与InnoDB锁算法

MyISAM采⽤表级锁(table-level locking)。
InnoDB⽀持⾏级锁(row-level locking)和表级锁,默认为⾏级锁

表级行级对比(按照锁的粒度分数据库锁有哪些?)

表级锁: MySQL对当前操作的整张表加锁,实现简单,资源消耗也⽐较少,加锁快,不会出现死锁。,触发锁冲突的概率最⾼,并发度最低, MyISAM和 InnoDB引擎都⽀持表级锁。
⾏级锁: MySQL中锁定 粒度最⼩ 的⼀种锁,只针对当前操作的⾏进⾏加锁。 ⾏级锁能⼤⼤减少数据库操作的冲突。其加锁粒度最⼩,并发度⾼,但加锁的开销也最⼤,加锁慢,会出现死锁

锁的类别来分?

共享锁

其他用户可以并发读取数据,但任何事务都不能获取数据上的排他锁,直到已释放所有共享锁,其他事务只能加共享锁

简称为S锁,在事务要读取一条记录时,需要先获取该记录的S锁。

排他锁

若事务T对数据对象A加上X锁,则只允许T读取和修改A,其它任何事务都不能再对A加任何类型的锁,直到T释放A上的锁,更新操作始终用排他锁

简称X锁,在事务需要改动一条记录时,需要先获取该记录的X锁。

区别:共享锁上只能再加共享,且只能读不能写。排他锁加上之后其他不能再加任何锁,获取排他锁能写能读。

他们的加锁开销从大到小,并发能力也是从大到小。

InnoDB存储引擎的锁的算法有三种:
Record lock:单个⾏记录上的锁
Gap lock:间隙锁,锁定⼀个范围,不包括记录本身
Next-key lock: record+gap 锁定⼀个范围,包含记录本身,可解决幻读问题

相关知识点

innodb对于⾏的查询使⽤next-key lock
当查询的索引含有唯⼀属性时,将next-key lock降级为record key

意向锁

什么是意向锁呢?意向锁是一种不与行级锁冲突的表级锁。未来的某个时刻,事务可能要加共享或者排它锁时,先提前声明一个意向。注意一下,意向锁,是一个表级别的锁哈

  • 意向共享锁:简称IS锁,当事务准备在某些记录上加S锁时,需要现在表级别加一个IS锁。
  • 意向排他锁:简称IX锁,当事务准备在某条记录上加上X锁时,需要现在表级别加一个IX锁。

意向锁又是如何解决这个效率低的问题呢:

如果一个事务A获取到某一行的排他锁,并未提交,这时候表上就有意向排他锁和这一行的排他锁。这时候事务B想要获取这个表的共享锁,此时因为检测到事务A持有了表的意向排他锁,因此事务A必然持有某些行的排他锁,也就是说事务B对表的加锁请求需要阻塞等待,不再需要去检测表的每一行数据是否存在排他锁啦。

记录锁

记录锁是最简单的行锁,仅仅锁住一行。如:SELECT c1 FROM t WHERE c1 = 10 FOR UPDATE,如果C1字段是主键或者是唯一索引的话,这个SQL会加一个记录锁(Record Lock)

记录锁永远都是加在索引上的,即使一个表没有索引,InnoDB也会隐式的创建一个索引,并使用这个索引实施记录锁。它会阻塞其他事务对这行记录的插入、更新、删除。

间隙锁(Gap Lock)

为了解决幻读问题,InnoDB引入了间隙锁(Gap Lock)。间隙锁是一种加在两个索引之间的锁,或者加在第一个索引之前,或最后一个索引之后的间隙。它锁住的是一个区间,而不仅仅是这个区间中的每一条数据

临键锁(Next-Key Lock)

Next-key锁是记录锁和间隙锁的组合,它指的是加在某条记录以及这条记录前面间隙上的锁。说得更具体一点就是:临键锁会封锁索引记录本身,以及索引记录之前的区间,即它的锁区间是前开后闭,比如(5,10]

如果一个会话占有了索引记录R的共享/排他锁,其他会话不能立刻在R之前的区间插入新的索引记录。

插入意向锁

插入意向锁,是插入一行记录操作之前设置的一种间隙锁,这个锁释放了一种插入方式的信号。 它解决的问题:多个事务,在同一个索引,同一个范围区间插入记录时,如果插入的位置不冲突,不会阻塞彼此。

假设有索引值4、7,几个不同的事务准备插入5、6,每个锁都在获得插入行的独占锁之前用插入意向锁各自锁住了4、7之间的间隙,但是不阻塞对方因为插入行不冲突。以下就是一个插入意向锁的日志:

自增锁

自增锁是一种特殊的表级别锁。它是专门针对AUTO_INCREMENT类型的列,对于这种列,如果表中新增数据时就会去持有自增锁。简言之,如果一个事务正在往表中插入记录,所有其他事务的插入必须等待,以便第一个事务插入的行,是连续的主键值。

乐观悲观

一个事务拥有(获得)悲观锁后,其他任何事务都不能对数据进行修改啦,只能等待锁被释放才可以执行。 select…for update就是MySQL悲观锁的应用。

img ZWY面试总结_第16张图片

乐观锁的“乐观情绪”体现在,它认为数据的变动不会太频繁。因此,它允许多个事务同时对数据进行变动。实现方式:乐观锁一般会通过version版本号/时间戳判断记录是否被更改过,一般配合CAS算法实现。

select for update加的是表锁还是行锁

这道面试题,一般需要分两种数据库隔离级别(RR和RC),还需要分查询条件是唯一索引、主键、一般索引、无索引等几种情况分开讨论

在RC隔离级别下

  • 如果查询条件是唯一索引,会加IX意向排他锁(表级别的锁,不影响插入)、两把X排他锁(行锁,分别对应唯一索引,主键索引)

  • 如果查询条件是主键,会加IX意向排他锁(表级别的锁,不影响插入)、一把对应主键的X排他锁(行锁,会锁住主键索引那一行)。

  • 如果查询条件是普通索引,如果查询命中记录,会加IX意向排他锁(表锁)、两把X排他锁(行锁,分别对应普通索引的X锁,对应主键的X锁);如果没有命中数据库表的记录,只加了一把IX意向排他锁(表锁,不影响插入)

  • 如果查询条件是无索引,会加两把锁,IX意向排他锁(表锁)、一把X排他锁(行锁,对应主键的X锁)。

查询条件是无索引,为什么不锁表呢? MySQL会走聚簇(主键)索引进行全表扫描过滤。每条记录都会加上X锁。但是,为了效率考虑,MySQL在这方面进行了改进,在扫描过程中,若记录不满足过滤条件,会进行解锁操作。同时优化违背了2PL原则```。

引申:

为什么不是唯一索引上加X锁就可以了呢?为什么主键索引上的记录也要加锁呢?

如果并发的一个SQL,通过主键索引来更新update user_info_tab set user_name = '学友' where id = '1570068';此时,如果select...for update语句没有将主键索引上的记录加锁,那么并发的update就会感知不到select...for update语句的存在,违背了同一记录上的更新/删除需要串行执行的约束。

在RR隔离级别

  • 如果查询条件是唯一索引,命中数据库表记录时,一共会加三把锁:一把IX意向排他锁 (表锁,不影响插入),一把对应主键的X排他锁(行锁),一把对应唯一索引的X排他锁 (行锁)。
  • 如果查询条件是主键,会加IX意向排他锁(表级别的锁,不影响插入)、一把对应主键的X排他锁(行锁,会锁住主键索引那一行)。
  • 如果查询条件是普通索引,命中查询记录的话,除了会加X锁(行锁),IX锁(表锁,不影响插入),还会加Gap 锁(间隙锁,会影响插入)。
  • 如果查询条件是无索引,会加一个IX锁(表锁,不影响插入),每一行实际记录行的X锁,还有对应于supremum pseudo-record的虚拟全表行锁。这种场景,通俗点讲,其实就是锁表了。

InnoDB引擎的行锁是怎么实现的?

InnoDB是基于索引来完成行锁

例: select * from tab_with_index where id = 1 for update;

for update 可以根据条件来完成行锁锁定,并且 id 是有索引键的列,如果 id 不是索引键那么InnoDB将完成表锁,并发将无从谈起

死锁?避免死锁?

两个或多个事务在同一资源上相互占用,并请求锁定对方的资源,从而导致恶性循环的现象。

1、用表级锁

2、约定相同顺序

3、尽可能一次锁定所有资源

4、尽量避免大事务,建议拆成多个小事务。因为大事务占用的锁资源越多,越容易出现死锁。

5、降低数据库隔离级别,比如RR降低为RC,因为RR隔离级别,存在GAP锁,死锁概率大很多。

6、死锁与索引是密不可分的,合理优化你的索引,死锁概率降低。

7、如果业务处理不好可以用分布式事务锁或者使用乐观锁

隔离级别与锁的关系

在Read Uncommitted级别下,读取数据不需要加共享锁,这样就不会跟被修改的数据上的排他锁冲突

在Read Committed级别下,读操作需要加共享锁,但是在语句执行完以后释放共享锁;

在Repeatable Read级别下,读操作需要加共享锁,但是在事务提交之前并不释放共享锁,也就是必须等待事务执行完毕以后才释放共享锁。

SERIALIZABLE 是限制性最强的隔离级别,因为该级别锁定整个范围的键,并一直持有锁,直到事务完成。

数据库的乐观锁和悲观锁是什么?怎么实现的?

数据库管理系统(DBMS)中的并发控制的任务是确保在多个事务同时存取数据库中同一数据时不破坏事务的隔离性和统一性以及数据库的统一性。乐观并发控制(乐观锁)和悲观并发控制(悲观锁)是并发控制主要采用的技术手段。

悲观锁(多写):假定会发生并发冲突,在查询完数据的时候就把事务锁起来,直到提交事务。实现方式:使用数据库中的锁机制

乐观锁(多读):假设不会发生并发冲突,只在提交操作时检查是否违反数据完整性。在修改数据的时候把事务锁起来,实现方式:一般会使用版本号机制或CAS算法实现。

优化锁方面的意见?

较低的隔离级别
设计索引,尽量使用索引去访问数据

加锁更加精确,从而减少锁冲突
申请合适的锁。最好一次性请求足够级别的锁且不要申请超过实际需要的锁级别。列如,修改数据的话,最好申请排他锁,而不是先申请共享锁
不同的程序访问一组表的时候,应尽量约定一个相同的顺序访问各表,对于一个表而言,尽可能的固定顺序的获取表中的行。这样大大的减少死锁的机会。
尽量使用相等条件访问数据,这样可以避免间隙锁对并发插入的影响
数据查询的时候不是必要,不要使用加锁。MySQL的MVCC可以实现事务中的查询不用加锁,优化事务性能:MVCC只在committed read(读提交)和 repeatable read (可重复读)两种隔离级别
对于特定的事务,可以使用表锁来提高处理速度活着减少死锁的可能。

分库分表

为什么要分库分表

单表数据量太大,会极大影响你的 sql执行的性能

分表:

分表就是把一个表的数据放到多个表中,然后查询的时候你就查一个表。比如按照用户 id 来分表,将一个用户的数据就放在一个表中。然后操作的时候你对一个用户就操作那个表就好了。这样可以控制每个表的数据量在可控的范围内,比如每个表就固定在 200 万以内。

分库:

一个库一般我们经验而言,最多支撑到并发 2000,一定要扩容了,可以将一个库的数据拆分到多个库中,访问的时候就访问一个库好了。

如何对数据库如何进行垂直拆分或水平拆分的?

水平拆分的意思,就是把一个表的数据给弄到多个库的多个表里去,但是每个库的表结构都一样,水平拆分的意义,就是将数据均匀放更多的库里,然后用多个库来抗更高的并发

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-t0AINjLu-1681383922709)(https://raw.githubusercontent.com/viacheung/img/main/image/10089464-0e01dfe246b5c7ac.png)]

垂直拆分的意思,就是把一个有很多字段的表给拆分成多个表或者是多个库上去。每个库表的结构都不一样,每个库表都包含部分字段。一般来说,会将较少的访问频率很高的字段放到一个表里去,然后将较多的访问频率很低的字段放到另外一个表里去。因为数据库是有缓存的,你访问频率高的行字段越少,就可以在缓存里缓存更多的行,性能就越好。这个一般在表层面做的较多一些。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-d3jXbqUx-1681383922710)(https://raw.githubusercontent.com/viacheung/img/main/image/10089464-ab3069913c0f097c.png)]

两种分库分表的方式

  • 一种是按照 range 来分,就是每个库一段连续的数据,这个一般是按比如时间范围来的,但是这种一般较少用,因为很容易产生热点问题,大量的流量都打在最新的数据上了。
  • 或者是按照某个字段hash一下均匀分散,这个较为常用。

range 来分,好处在于说,扩容的时候很简单,因为你只要预备好,给每个月都准备一个库就可以了,到了一个新的月份的时候,自然而然,就会写新的库了;缺点,但是大部分的请求,都是访问最新的数据。实际生产用 range,要看场景。

hash 分发,好处在于说,可以平均分配每个库的数据量和请求压力;坏处在于说扩容起来比较麻烦,会有一个数据迁移的过程,之前的数据需要重新计算 hash 值重新分配到不同的库或表

读写分离、主从复制

什么是MySQL主从复制

使得数据可以从一个数据库服务器复制到其他服务器上,在复制数据时,一个服务器充当主服务器(master),其余的服务器充当从服务器(slave)。

主从复制目的?为什么主从复制

  1. **提高数据库的性能,***在主服务器上执行写入和更新,在从服务器上向外提供读功能
  2. 提高数据安全-从服务器上备份主服务器相应数据
  3. 在主服务器上生成实时数据,而在从服务器上分析这些数据,从而提高主服务器的性能
  4. 数据备份。

如何实现MySQL的读写分离?

搞一个主库,挂多个从库,然后我们只写主库,读从库,然后主库会自动把数据给同步到从库上去。

MySQL主从复制流程和原理?

基本原理流程,是3个线程以及之间的关联

主:binlog线程——记录下所有改变了数据库数据的语句,放进master上的binlog中;

从:io线程——在使用start slave 之后,负责从master上拉取 binlog 内容,放进自己的relay log中;

从:sql执行线程——执行relay log中的语句;

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0tjVSwRg-1681383922710)(https://raw.githubusercontent.com/viacheung/img/main/image/image-20230319151800745.png)]

Binary log:主数据库的二进制日志

Relay log:从服务器的中继日志

第一步:master在每个事务更新数据完成之前,将该操作记录串行地写入到binlog文件中。

第二步:salve开启一个I/O Thread,该线程在master打开一个普通连接,主要工作是binlog dump process。如果读取的进度已经跟上了master,就进入睡眠状态并等待master产生新的事件。I/O线程最终的目的是将这些事件写入到中继日志中。

第三步:SQL Thread会读取中继日志,并顺序执行该日志中的SQL事件,从而与主数据库中的数据保持一致。

MySQL主从同步延时问题如何解决?

两个同步机制,一个是半同步复制,用来 解决主库数据丢失问题;

一个是并行复制,用来 解决主从同步延时问题。

半同步复制,也叫 semi-sync 复制,指的就是主库写入 binlog 日志之后,就会将强制此时立即将数据同步到从库,从库将日志写入自己本地的 relay log 之后,接着会返回一个 ack 给主库,主库接收到至少一个从库的 ack 之后才会认为写操作完成了。
**并行复制,**指的是从库开启多个线程,并行读取 relay log 中不同库的日志,然后并行重放不同库的日志,这是库级别的并行。

DDL:数据定义语言 CREATE(建表)、ALTER(增删字段)、DROP和TRUNCATE(删除表)

DML:数据操纵语言(insert、update、delete)

DQL:数据查询语言

优化

如何定位及优化SQL语句的性能问题?

explain:是否使用索引、使用什么索引、使用索引相关信息

大表数据查询优化?

1、加缓存 redis

2、sql语句+索引

3、主从复制、读写分离

4、垂直拆分 水平拆分

超大分页怎么处理(MySQL深分页)

limit:1000000开始取10条

select * from table where age > 20 limit 1000000,10优化为=》

我们先来看下这个SQL的执行流程:

  1. 通过普通二级索引树idx_update_time,过滤update_time条件,找到满足条件的记录ID。
  2. 通过ID,回到主键索引树,找到满足记录的行,然后取出展示的列(回表
  3. 扫描满足条件的100010行,然后扔掉前100000行,返回。

SQL变慢原因有两个

  1. limit语句会先扫描offset+n行,然后再丢弃掉前offset行,返回后n行数据。也就是说limit 100000,10,就会扫描100010行,而limit 0,10,只扫描10行。
  2. limit 100000,10 扫描更多的行数,也意味着回表更多的次数。

通过子查询优化

select * from table where id in (select id from table where age > 20 limit 1000000,10)

子查询 table a查询是用到了idx_update_time索引。首先在索引上拿到了聚集索引的主键ID,省去了回表操作,然后第二查询直接根据第一个查询的 ID往后再去查10个就可以了!

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VnKFVZHz-1681383922711)(https://raw.githubusercontent.com/viacheung/img/main/image/1669949ca6818e4a0c87f754364a94d5.png)]

虽然也 load 了一百万的数据,但是由于索引覆盖,要查询的所有字段都在索引中(age),所以速度会很快。

统计过慢查询吗?慢查询怎么优化

  • 首先分析语句,看看是否load了额外的数据,可能是查询了多余的行并且抛弃掉了,可能是加载了许多结果中并不需要的列,对语句进行分析以及重写。
  • 分析语句的执行计划,然后获得其使用索引的情况,之后修改语句或者修改索引,使得语句可以尽可能的命中索引。
  • 如果对语句的优化已经无法进行,可以考虑表中的数据量是否太大,如果是的话可以进行横向或者纵向的分表。

1、Load额外数据-2、修改索引,尽量命中3、数据量大?分表

如何优化查询过程中的数据访问

  • 访问数据太多导致查询性能下降
  • 确定应用程序是否在检索大量超过需要的数据,可能是太多行或列
  • 确认MySQL服务器是否在分析大量不必要的数据行
  • 查询不需要的数据。解决办法:使用limit解决
  • 多表关联返回全部列。解决办法:指定列名
  • 总是返回全部列。解决办法:**避免使用SELECT ***
  • 重复查询相同的数据。解决办法:可以缓存数据,下次直接读取缓存
  • 是否在扫描额外的记录。解决办法: 使用explain进行分析,如果发现查询需要扫描大量的数据,但只返回少数的行,可以通过如下技巧去优化: 使用索引覆盖扫描,把所有的列都放到索引中,这样存储引擎不需要回表获取对应行就可以返回结果。
  • 改变数据库和表的结构,修改数据表范式
  • 重写SQL语句,让优化器可以以更优的方式执行查询。

优化关联查询

  • 确定ON或者USING子句中是否有索引。
  • 确保GROUP BY和ORDER BY只有一个表中的列,这样MySQL才有可能使用索引。

数据库结构优化

1、将字段很多的表分解成多个表

因为当一个表的数据量很大时,会由于使用频率低的字段的存在而变慢。

2、增加中间表
对于需要经常联合查询的表,可以建立中间表以提高查询效率。

通过建立中间表,将需要通过联合查询的数据插入到中间表中,然后将原来的联合查询改为对中间表的查询。

3、增加冗余字段
合理的加入冗余字段可以提高查询速度。

表的规范化程度越高,表和表之间的关系越多,需要连接查询的情况也就越多,性能也就越差。

注意:

冗余字段的值在一个表中修改了,就要想办法在其他表中更新,否则就会导致数据不一致的问题。

MySQL数据库cpu飙升到500%的话他怎么处理?

top 命令观察是不是 MySQLd 占用导致的,不是,占用高进程杀死

是的话, show processlist,看看里面跑的 session 情况,找出消耗高的 sql,看是不是Index少了或者数据量大

kill掉,加索引、改sql、改内存参数,重新跑

session变多,限制连接数

大表优化

1、限定数据范围

2、读写分离 主库负责写,从库负责读

3、垂直分区 优点:列数据变小 缺点:主键冗余,产生join

4、水平分区 每⼀⽚数据分散到不同的表或者库中,达到了分布式的⽬的。 ⽔平拆分可以⽀撑⾮常⼤的数据量

数据库分⽚的两种常⻅⽅案:
客户端代理: 分⽚逻辑在应⽤端,封装在jar包中,通过修改或者封装JDBC层来实现。
中间件代理: 在应⽤和数据中间加了⼀个代理层。分⽚逻辑统⼀维护在中间件服务中。

解释⼀下什么是池化设计思想。什么是数据库连接池?为什么需要数据库连接池?

java线程池、 jdbc连接池、 redis连接池

数据库连接池:多个socket 的连接

为什么需要?:减少用户等待时间

分库分表之后,id 主键如何处理?

自增 设置不同步长 有序 不好部署

redis生成

leaf分布式id 保证证全局唯⼀性、趋势递增、单调递增、信息安全

雪花算法 :分布式id mp

⼀条SQL语句在MySQL中如何执⾏的

查询:

1、客户端通过 TCP 连接发送连接请求到 ==MySQL连接器,==先检查该语句是否有权限,如果没有权限,返回错误信息,如果有权限,会以这条 sql 语句为 key先查询缓存, 如果有,直接返回,如果没有,执行下一步。

2、通过分析器进行词法分析,提取 sql 语句的关键元素,(表名 select啥的)然,判断 sql 语句是否有错误,如果检查没问题就执行下一步。

3、优化器根据自己的优化算法进行优化(比如先查谁再执行谁,优化器认为,有时候不一定最好),开始执行

4、交给执行器,将数据保存到结果集中,同时会逐步将数据缓存到查询缓存中,最终将结果集返回给客户端

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jsNPUyCx-1681383922712)(https://raw.githubusercontent.com/viacheung/img/main/image/4102b7d60fa20a0caabb127ecbb4d2f3.jpeg)]

更新:

需要检查表是否有排它锁,写 binlog,刷盘,是否执行 commit。

  1. 前面和查询一样,走缓存
  2. 拿到查询的语句,然后更新,然后调用引擎 API 接口,写入这一行数据,InnoDB 引擎把数据保存在内存中,同时记录 redo log,此时 redo log 进入 prepare 状态,然后告诉执行器,执行完成了,随时可以提交。
  3. 执行器收到通知后记录 binlog,然后调用引擎接口,提交 redo log 为提交状态,更新完成。

总结:redolog—redolog_prepare----binlog—redolog_commit

假设 redo log 处于预提交状态,binglog 也已经写完了,这个时候发生了异常重启会怎么样呢?

这个就要依赖于 MySQL 的处理机制了,MySQL 的处理过程如下:

•判断 redo log 是否完整,如果判断是完整的,就立即提交。

•如果 redo log 只是预提交但不是 commit 状态,这个时候就会去判断 binlog 是否完整,如果完整就提交 redo log, 不完整就回滚事务。

这样就解决了数据一致性的问题

Redis

Redis是什么以及优缺点

整个数据库加载在内存当中操作,定期通过异步操作把数据库中的数据flush到硬盘上进行保存,每秒10w次读写操作

优点:

读写性能高 Redis能读的速度是110000次/s,写的速度是81000次/s。

支持数据持久化 AOF RDB

支持事务

数据结构丰富:hash set zset list string

支持主从复制和读写分离 主机会自动将数据同步到从机,可以进行读写分离。

支持发布订阅 pub-sub,通知,key过期等特性

缺点:

  • 数据库容量受到物理内存的限制,不能用作海量数据的高性能读写,因此Redis适合的场景主要局限在较小数据量的高性能操作和运算上。

  • 主机宕机,宕机前有部分数据未能及时同步到从机,切换IP后还会引入数据不一致的问题,降低了系统的可用性。

Redis为什么快

1、内存存储:没有磁盘IO的开销 。数据存在内存中,类似于 HashMap,HashMap 的优势就是查找和操作的时间复杂度都是O(1)。

2、单线程:避免了多个线程之间线程切换和锁资源争用的开销。注意:单线程是指的是在核心网络模型中,网络请求模块使用一个线程来处理,即一个线程处理所有网络请求。

3、非阻塞IO:Redis使用多路复用IO技术,将epoll作为I/O多路复用技术的实现,再加上Redis自身的事件处理模型将epoll中的连接、读写、关闭都转换为事件,不在网络I/O上浪费过多的时间。

4、优化的数据结构,提升性能:Redis有诸多可以直接应用的优化数据结构的实现,应用层可以直接使用原生的数据结构提升性能。

5、使用底层模型不同:Redis直接自己构建了 VM (虚拟内存)机制 ,因为一般的系统调用系统函数的话,会浪费一定的时间去移动和请求。

内存数据库,读写速度快 分布式锁,甚⾄是消息队列。


Redis的VM(虚拟内存)机制就是暂时把不经常访问的数据(冷数据)从内存交换到磁盘中,从而腾出宝贵的内存空间用于其它需要访问的数据(热数据)。通过VM功能可以实现冷热数据分离,使热数据仍在内存中、冷数据保存到磁盘。这样就可以避免因为内存不足而造成访问速度下降的问题。

Redis提高数据库容量的办法有两种:一种是可以将数据分割到多个RedisServer上;另一种是使用虚拟内存把那些不经常访问的数据交换到磁盘上。需要特别注意的是Redis并没有使用OS提供的Swap,而是自己实现。


分布式缓存常⻅的技术选型⽅案有哪些?

说⼀下 Redis 和 Memcached 的区别和共同点

共同点 :
基于内存 过期策略 性能

区别 :

  1. 数据类型丰富 list set hash zset
  2. 数据的持久化
  3. 灾难恢复机制。 把缓存中的数据持久化到磁盘上。
  4. Redis ⽬前是⽀持 cluster 模式
  5. Memcached 多线程,⾮阻塞 IO 复⽤的⽹络模型; Redis 使⽤单线程的多路 IO 复⽤模
    型。
  6. Memcached过期数据的删除策略只⽤了惰性删除,⽽ Redis 同时使⽤了惰性删除与定期删
    除。

Redis相比Memcached有哪些优势?

  • 数据类型:Memcached所有的值均是简单的字符串,Redis支持更为丰富的数据类型,支持string(字符串),list(列表),Set(集合)、Sorted Set(有序集合)、Hash(哈希)等。

  • 持久化:Redis支持数据落地持久化存储,可以将内存中的数据保持在磁盘中,重启的时候可以再次加载进行使用。 memcache不支持数据持久存储 。

  • 集群模式:Redis提供主从同步机制,以及 Cluster集群部署能力,能够提供高可用服务。Memcached没有原生的集群模式,需要依靠客户端来实现往集群中分片写入数据

  • 性能对比:Redis的速度比Memcached快很多。

  • 网络IO模型:Redis使用单线程的多路 IO 复用模型,Memcached使用多线程的非阻塞IO模式。

  • Redis支持服务器端的数据操作:Redis相比Memcached来说,拥有更多的数据结构和并支持更丰富的数据操作,通常在Memcached里,你需要将数据拿到客户端来进行类似的修改再set回去。

    这大大增加了网络IO的次数和数据体积。在Redis中,这些复杂的操作通常和一般的GET/SET一样高效。所以,如果需要缓存能够支持更复杂的结构和操作,那么Redis会是不错的选择。

缓存数据的处理流程是怎样的?

用户请求数据在缓存直接范围,不在,查数据库,数据库在,更新缓存,不在,返回空

为什么要⽤ Redis/为什么要⽤缓存?

从高并发上来说:

  • 直接操作缓存能够承受的请求是远远大于直接访问数据库的,所以我们可以考虑把数据库中的部分数据转移到缓存中去,这样用户的一部分请求会直接到缓存这里而不用经过数据库。缓存能够承受的数据库请求数量是远远⼤于直接访问数据库的,也就提⾼的系统整体的并发。

从高性能上来说:

  • 用户第一次访问数据库中的某些数据。 因为是从硬盘上读取的所以这个过程会比较慢。将该用户访问的数据存在缓存中,下一次再访问这些数据的时候就可以直接从缓存中获取了。操作缓存就是直接操作内存,所以速度相当快。如果数据库中的对应数据改变的之后,同步改变缓存中相应的数据。

总结:

提升⽤户体验以及应对更多的⽤户。------》 ⾼性能”和“⾼并发

高性能:从硬盘取太慢 从缓存快

⾼并发: MySQL QPS (服务器每秒可以执⾏的查询次数 )⼤概都在 1w 左右(4 核 8g) ,但是使⽤ Redis 缓存之后
单机 10w- 30w, redis 集群的话会更⾼。

为什么用redis而不是map/guava做缓存

缓存分为本地缓存和分布式缓存。以java为例,使用自带的map或者guava实现的是本地缓存,最主要的特点是轻量以及快速,生命周期随着jvm的销毁而结束,并且在多实例的情况下,每个实例都需要各自保存一份缓存,缓存不具有一致性。

使用Redis或memcached之类的称为分布式缓存,在多实例的情况下,各实例共用一份缓存数据,缓存具有一致性。缺点是需要保持Redis或memcached服务的高可用,整个程序架构上较为复杂。

  • (缓存容量大)Redis 可以用几十 G 内存来做缓存,Map 不行,一般 JVM 也就分几个 G 数据就够大了;
  • (持久化)Redis 的缓存可以持久化,Map 是内存对象,程序一重启数据就没了;
  • (分布式缓存)Redis 可以实现分布式的缓存,Map 只能存在创建它的程序里;
  • (高并发)Redis 可以处理每秒百万级的并发,是专业的缓存服务,Map 只是一个普通的对象;
  • (缓存过期+丰富API)Redis 缓存有过期机制,Map 本身无此功能;Redis 有丰富的 API,Map 就简单太多了;
  • (分布式共享数据)Redis可单独部署,多个项目之间可以共享,本地内存无法共享;
  • (单独管理工具)Redis有专门的管理工具可以查看缓存数据。

Redis 常⻅数据结构以及使⽤场景分析

1、缓存

缓存现在几乎是所有中大型网站都在用的必杀技,合理的利用缓存不仅能够提升网站访问速度,还能大大降低数据库的压力。Redis提供了键过期功能,也提供了灵活的键淘汰策略,所以,现在Redis用在缓存的场合非常多。

2、排行榜

很多网站都有排行榜应用的,如京东的月度销量榜单、商品按时间的上新排行榜等。Redis提供的有序集合数据类构能实现各种复杂的排行榜应用。

3、计数器

什么是计数器,如电商网站商品的浏览量、视频网站视频的播放数等。为了保证数据实时效,每次浏览都得给+1,并发量高时如果每次都请求数据库操作无疑是种挑战和压力。Redis提供的incr命令来实现计数器功能,内存操作,性能非常好,非常适用于这些计数场景。

4、分布式会话

集群模式下,在应用不多的情况下一般使用容器自带的session复制功能就能满足,当应用增多相对复杂的系统中,一般都会搭建以Redis等内存数据库为中心的session服务,session不再由容器管理,而是由session服务及内存数据库管理

5、分布式锁

在很多互联网公司中都使用了分布式技术,分布式技术带来的技术挑战是对同一个资源的并发访问,如全局ID、减库存、秒杀等场景,==并发量不大的场景可以使用数据库的悲观锁、乐观锁来实现,==但在并发量高的场合中,利用数据库锁来控制资源的并发访问是不太理想的,大大影响了数据库的性能。可以利用Redis的setnx功能来编写分布式的锁,如果设置返回1说明获取锁成功,否则获取锁失败,实际应用中要考虑的细节要更多。

6、 社交网络

点赞、踩、关注/被关注、共同好友等是社交网站的基本功能,社交网站的访问量通常来说比较大,而且传统的关系数据库类型不适合存储这种类型的数据,Redis提供的哈希、集合等数据结构能很方便的的实现这些功能。如在微博中的共同好友,通过Redis的set能够很方便得出。

7、最新列表

Redis列表结构,LPUSH可以在列表头部插入一个内容ID作为关键字,LTRIM可用来限制列表的数量,这样列表永远为N个ID,无需查询最新的列表,直接根据ID去到对应的内容页即可。

8、消息系统

消息队列是大型网站必用中间件,如ActiveMQ、RabbitMQ、Kafka等流行的消息队列中间件,主要用于业务解耦、流量削峰及异步处理实时性低的业务。Redis提供了发布/订阅及阻塞队列功能,能实现一个简单的消息队列系统。另外,这个不能和专业的消息中间件相比。

string:计数器 过期

List:消息队列

incr命令:计数器

hash:存储对象数据 ⽤户信息,商品信息

set :获取多个数据源交集和并集 ,共同关注、共同粉丝、共同喜好

sorted set (zset) :礼物排⾏榜,弹幕消息

Redis的数据类型有哪些?

有五种常用数据类型:String、Hash、Set、List、SortedSet。以及三种特殊的数据类型:Bitmap、HyperLogLog、Geospatial ,其中HyperLogLog、Bitmap的底层都是 String 数据类型,Geospatial 的底层是 Sorted Set 数据类型。

五种常用的数据类型

1、String:String是最常用的一种数据类型,普通的key- value 存储都可以归为此类。其中Value既可以是数字也可以是字符串。使用场景:常规key-value缓存应用。常规计数: 微博数, 粉丝数。

2、Hash:Hash 是一个键值(key => value)对集合。Redishash 是一个 string 类型的 field 和 value 的映射表,hash 特别适合用于存储对象,并且可以像数据库中update一个属性一样只修改某一项属性值。

3、Set:Set是一个无序的天然去重的集合,即Key-Set。此外还提供了交集、并集等一系列直接操作集合的方法,对于求共同好友、共同关注什么的功能实现特别方便。

4、List:List是一个有序可重复的集合,其遵循FIFO的原则,底层是依赖双向链表实现的,因此支持正向、反向双重查找。通过List,我们可以很方面的获得类似于最新回复这类的功能实现。

5、SortedSet:类似于java中的TreeSet,是Set的可排序版。此外还支持优先级排序,维护了一个score的参数来实现。适用于排行榜和带权重的消息队列等场景。

三种特殊的数据类型

1、Bitmap:位图,Bitmap想象成一个以位为单位数组,数组中的每个单元只能存0或者1,数组的下标在Bitmap中叫做偏移量。使用Bitmap实现统计功能,更省空间。如果只需要统计数据的二值状态,例如商品有没有、用户在不在等,就可以使用 Bitmap,因为它只用一个 bit 位就能表示 0 或 1。

2、Hyperloglog。HyperLogLog 是一种用于统计基数的数据集合类型,HyperLogLog 的优点是,在输入元素的数量或者体积非常非常大时,计算基数所需的空间总是固定 的、并且是很小的。每个 HyperLogLog 键只需要花费 12 KB 内存,就可以计算接近 2^64 个不同元素的基 数。场景:统计网页的UV(即Unique Visitor,不重复访客,一个人访问某个网站多次,但是还是只计算为一次)。

要注意,HyperLogLog 的统计规则是基于概率完成的,所以它给出的统计结果是有一定误差的,标准误算率是 0.81%。

3、Geospatial :主要用于存储地理位置信息,并对存储的信息进行操作,适用场景如朋友的定位、附近的人、打车距离计算等。

持久化

Redis 持久化机制(怎么保证 Redis 挂掉之后再重启数据可以进⾏恢复)

为了能够重用Redis数据,或者防止系统故障,我们需要将Redis中的数据写入到磁盘空间中,即持久化。

快照(snapshotting)持久化(RDB(默认)

指定的时间间隔内将内存中的数据集快照写入磁盘(Snapshot),它恢复时是将快照文件直接读到内存里。

优势:适合大规模的数据恢复;对数据完整性和一致性要求不高

劣势:在一定间隔时间做一次备份,所以如果Redis意外down掉的话,就会丢失最后一次快照后的所有修改。(Redis 主从结构,主要⽤来提⾼ Redis 性能),还可以将快照留在原地以便重启服务器的时候使⽤

AOF(append-only file)持久化

每执⾏⼀条会更改 Redis 中的数据的命令, Redis 就会将该命令写⼊硬盘中的 AOF ⽂件。 Redis启动之初会读取该文件重新构建数据

AOF采用文件追加方式,==文件会越来越大,为避免出现此种情况,新增了重写机制,==当AOF文件的大小超过所设定的阈值时, Redis就会启动AOF文件的内容压缩,只保留可以恢复数据的最小指令集(比如两条数据取最新的那条就行了).。

优势

  • 每修改同步:appendfsync always 同步持久化,每次发生数据变更会被立即记录到磁盘,性能较差但数据完整性比较好
  • 每秒同步:appendfsync everysec 异步操作,每秒记录,如果一秒内宕机,有数据丢失
  • 不同步:appendfsync no 从不同步

具体地:

  • Always,这个单词的意思是「总是」,所以它的意思是每次写操作命令执行完后,同步将 AOF 日志数据写回硬盘;
  • Everysec,这个单词的意思是「每秒」,所以它的意思是每次写操作命令执行完后,先将命令写入到 AOF 文件的内核缓冲区,然后每隔一秒将缓冲区里的内容写回到硬盘;
  • No,意味着不由 Redis 控制写回硬盘的时机,转交给操作系统控制写回的时机,也就是每次写操作命令执行完后,先将命令写入到 AOF 文件的内核缓冲区,再由操作系统决定何时将缓冲区内容写回硬盘。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JxJ2wtT7-1681383922713)(https://raw.githubusercontent.com/viacheung/img/main/image/98987d9417b2bab43087f45fc959d32a-20230309232253633.png)]

劣势

  • 相同数据集的数据而言aof文件要远大于rdb文件,恢复速度慢于rdb
  • aof运行效率要慢于rdb,每秒同步策略效率较好,不同步效率和rdb相同

如何选择合适的持久化方式

  • 如果是数据不那么敏感,且可以从其他地方重新生成补回的,那么可以关闭持久化。
  • 如果是数据比较重要,不想再从其他地方获取,且可以承受数分钟的数据丢失,比如缓存等,那么可以只使用RDB。
  • 如果是用做内存数据库,要使用Redis的持久化,建议是RDB和AOF都开启,或者定期执行bgsave做快照备份,RDB方式更适合做数据的备份,AOF可以保证数据的不丢失。

Redis持久化数据和缓存怎么做扩容?

  • 如果Redis被当做缓存使用,使用一致性哈希实现动态扩容缩容。

https://juejin.cn/post/6844903750860013576

一致性hash算法可以保证当机器增加或者减少时,节点之间的数据迁移只限于两个节点之间,不会造成全局的网络问题。

  1. 传统hash算法增减服务器会导致大量key重定向到其他服务器
  2. 一致性hash算法有一个0-2^31的圆环(哈希值是32位无符号整形)值为Key的数据隶属于顺时针方向第一个服务器

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0P8L2t2I-1681383922713)(https://raw.githubusercontent.com/viacheung/img/main/image/1545973609449.png)]

  1. 当宕机和扩展服务器只会影响一部分数据
  1. 如果节点太少,会导致数据倾斜,也就是hash范围差距大 那么可以引入了虚拟节点机制,即对每个节点计算多个哈希值,每个计算结果位置都放置在对应节点中,这些节点称为虚拟节点
  • 如果Redis被当做一个持久化存储使用,必须使用固定的keys-to-nodes映射关系,节点的数量一旦确定不能变化。否则的话(即Redis节点需要动态变化的情况),必须使用可以在运行时进行数据再平衡的一套系统,而当前只有Redis集群可以做到这样。

过期键的删除策略、淘汰策略

  1. 惰性删除 :只会在取出key的时候才对数据进⾏过期检查。这样对CPU最友好,但是可能会造成太多过期 key 没有被删除。
  2. 定期删除 : 每隔⼀段时间抽取⼀批 key 执⾏删除过期key操作。并且, Redis 底层会通过限制删除操作执⾏的时⻓和频率来减少删除操作对CPU时间的影响。

删除key常见的三种处理方式:

1、定时删除

在设置某个key 的过期时间同时,我们创建一个定时器,让定时器在该过期时间到来时,立即执行对其进行删除的操作。

优点:定时删除对内存是最友好的,能够保存内存的key一旦过期就能立即从内存中删除。

缺点:对CPU最不友好,在过期键比较多的时候,删除过期键会占用一部分 CPU 时间,对服务器的响应时间和吞吐量造成影响。

2、惰性删除

设置该key 过期时间后,我们不去管它,当需要该key时,我们在检查其是否过期,如果过期,我们就删掉它,反之返回该key。(代码层面)

优点:对 CPU友好,我们只会在使用该键时才会进行过期检查,对于很多用不到的key不用浪费时间进行过期检查。

缺点:对内存不友好,如果一个键已经过期,但是一直没有使用,那么该键就会一直存在内存中,如果数据库中有很多这种使用不到的过期键,这些键便永远不会被删除,内存永远不会释放。从而造成内存泄漏。(不用但存在)

3、定期删除

每隔一段时间,我们就对一些key进行检查,删除里面过期的key。

优点:可以通过限制删除操作执行的时长和频率来==减少删除操作对 CPU 的影响。==另外定期删除,也能有效释放过期键占用的内存。

缺点:难以确定删除操作执行的时长和频率。如果执行的太频繁,定期删除策略变得和定时删除策略一样,对CPU不友好。如果执行的太少,那又和惰性删除一样了,过期键占用的内存不会及时得到释放。

另外最重要的是,在获取某个键时,如果某个键的过期时间已经到了,但是还没执行定期删除,那么就会返回这个键的值,这是业务不能忍受的错误。

Redis key的过期时间和永久有效分别怎么设置?

通过expirepexpire命令,客户端可以以秒或毫秒的精度为数据库中的某个键设置生存时间

与expire和pexpire命令类似,客户端可以通过expireat和pexpireat命令,以秒或毫秒精度给数据库中的某个键设置过期时间,可以理解为:让某个键在某个时间点过期。

Redis 给缓存数据设置过期时间有啥⽤?

命令:字符串:setex 其他 expire

有助于缓解内存的消耗

业务场景就是需要某个数据只在某⼀时间段内存在 ⽐如我们的短信验证码可能只在1分钟内有效,⽤户登录的 token 可能只在 1 天内有效。

如果使⽤传统的数据库来处理的话,⼀般都是⾃⼰判断过期,这样更麻烦并且性能要差很多。

Redis是如何判断数据是否过期的呢?

过期字典 (hash表)键指向Redis数据库中的某个key(键),值是⼀个long long类型的整数(数据库键的过期时间)

Redis 内存淘汰机制了解么?

仅仅通过给 key 设置过期时间不太够,还是有很多key没删掉,报oom,因此要用内存淘汰机制

  1. volatile-lru(least recently used) :从已设置过期时间的数据 中移除最近最少使⽤数据
  2. allkeys-lru(least recently used) :在键空间中,移除最近最少使⽤的 key(最常⽤
  3. volatile-lfu 从已设置过期时间的数据 中移除最不经常使⽤数据
  4. allkeys-lfu(least recently used) :在键空间中,移除最近最不经常用的 key(最常⽤
  5. volatile-random:从已设置过期时间的数据任意选择数据淘汰
  6. allkeys-random:从数据集中任意选择数据淘汰
  7. volatile-ttl:从已设置过期时间的数据挑选要过期的数据淘汰
  8. no-eviction:禁止驱逐数据,也就是说当内存不足以容纳新写入数据时,新写入操作会报错。这个应该没人使用吧!

内存淘汰策略可以通过配置文件来修改,Redis.conf对应的配置项是maxmemory-policy 修改对应的值就行,默认是noeviction。

事务

Redis事务提供了⼀种将多个命令请求打包的功能。然后,再按顺序执⾏打包的所有命令,并且不会被中途打断。

Redis 是不⽀持 roll back 的,因⽽不满⾜原⼦性的(⽽且不满⾜持久性)。

为啥不支持回滚?

更简单便捷并且性能更好。 即使命令执⾏错误也应该在开发过程中就被发现⽽不是⽣产过程中。

缓存异常

缓存异常有四种类型,分别是缓存和数据库的数据不一致、缓存雪崩、缓存击穿和缓存穿透。


如何保证缓存与数据库双写时的数据一致性?

背景:使用到缓存,无论是本地内存做缓存还是使用 Redis 做缓存,那么就会存在数据同步的问题,因为配置信息缓存在内存中,而内存时无法感知到数据在数据库的修改。这样就会造成数据库中的数据与缓存中数据不一致的问题。

共有四种方案:

  1. 先更新数据库,后更新缓存
  2. 先更新缓存,后更新数据库
  3. 先删除缓存,后更新数据库
  4. 先更新数据库,后删除缓存

第一种和第二种方案,没有人使用的,因为第一种方案存在问题是:并发更新数据库场景下,会将脏数据刷到缓存。

第二种方案存在的问题是:如果先更新缓存成功,但是数据库更新失败,则肯定会造成数据不一致。

目前主要用第三和第四种方案。

先删除缓存,后更新数据库

该方案也会出问题,此时来了两个请求,请求 A(更新操作) 和请求 B(查询操作)

  1. 请求A进行写操作,删除缓存
  2. 请求B查询发现缓存不存在
  3. 请求B去数据库查询得到旧值(此时)
  4. 请求B将旧值写入缓存
  5. 请求A将新值写入数据库

上述情况就会导致不一致的情形出现。而且,如果不采用给缓存设置过期时间策略,该数据永远都是脏数据。

答案一:延时双删

(1)先淘汰缓存

(2)再写数据库(这两步和原来一样)

(3)休眠1秒,再次淘汰缓存,这么做,可以将1秒内所造成的缓存脏数据,再次删除。确保读请求结束,写请求可以删除读请求造成的缓存脏数据。自行评估自己的项目的读数据业务逻辑的耗时,写数据的休眠时间则在读数据业务逻辑的耗时基础上,加几百ms即可。

(我的理解:请求A先删缓存再往DB写数据,就算这时B来查数据库,缓存没数据,然后查DB,此时查到的是旧数据,写到缓存,A等待B写完之和再删缓存,这样就缓存一致)

如果使用的是 Mysql 的读写分离的架构的话,那么其实主从同步之间也会有时间差。

此时来了两个请求,请求 A(更新操作) 和请求 B(查询操作)

  1. 请求 A 更新操作,删除了 Redis
  2. 请求主库进行更新操作,主库与从库进行同步数据的操作
  3. 请 B 查询操作,发现 Redis 中没有数据
  4. 去从库中拿去数据
  5. 此时同步数据(binlog没写完)还未完成,拿到的数据是旧数据

此时的解决办法就是如果是对 Redis 进行填充数据的查询数据库操作,那么就强制将其指向主库进行查询。

答案二: 更新与读取操作进行异步串行化

采用更新与读取操作进行异步串行化

异步串行化

我在系统内部维护n个内存队列,更新数据的时候,根据数据的唯一标识,将该操作路由之后,发送到其中一个jvm内部的内存队列中(对同一数据的请求发送到同一个队列)。读取数据的时候,如果发现数据不在缓存中,并且此时队列里有更新库存的操作,那么将重新读取数据+更新缓存的操作,根据唯一标识路由之后,也将发送到同一个jvm内部的内存队列中。然后每个队列对应一个工作线程,每个工作线程串行地拿到对应的操作,然后一条一条的执行。

这样的话,一个数据变更的操作,先执行删除缓存,然后再去更新数据库,但是还没完成更新的时候,如果此时一个读请求过来,读到了空的缓存,那么可以先将缓存更新的请求发送到队列中,此时会在队列中积压,排在刚才更新库的操作之后,然后同步等待缓存更新完成,再读库。

读操作去重

多个读库更新缓存的请求串在同一个队列中是没意义的,因此可以做过滤,如果发现队列中已经有了该数据的更新缓存的请求了,那么就不用再放进去了,直接等待前面的更新操作请求完成即可,待那个队列对应的工作线程完成了上一个操作(数据库的修改)之后,才会去执行下一个操作(读库更新缓存),此时会从数据库中读取最新的值,然后写入缓存中。

如果请求还在等待时间范围内,不断轮询发现可以取到值了,那么就直接返回;如果请求等待的时间超过一定时长,那么这一次直接从数据库中读取当前的旧值。(返回旧值不是又导致缓存和数据库不一致了么?那至少可以减少这个情况发生,因为等待超时也不是每次都是,几率很小吧。这里我想的是,如果超时了就直接读旧值,这时候仅仅是读库后返回而不放缓存)

先更新数据库,后删除缓存

这一种情况也会出现问题,比如更新数据库成功了,但是在删除缓存的阶段出错了没有删除成功,那么此时再读取缓存的时候每次都是错误的数据了。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IrOU7BJN-1681383922714)(https://raw.githubusercontent.com/viacheung/img/main/image/1735bb5881fb4a1b~tplv-t2oaga2asx-watermark.awebp)]

此时解决方案就是利用消息队列进行删除的补偿。具体的业务逻辑用语言描述如下:

  1. 请求 A 先对数据库进行更新操作
  2. 在对 Redis 进行删除操作的时候发现报错,删除失败
  3. 此时将Redis 的 key 作为消息体发送到消息队列中
  4. 系统接收到消息队列发送的消息后再次对 Redis 进行删除操作

但是这个方案会有一个缺点就是会对业务代码造成大量的侵入,深深的耦合在一起,所以这时会有一个优化的方案,我们知道对 Mysql 数据库更新操作后在binlog 日志中我们都能够找到相应的操作,那么我们可以订阅 Mysql 数据库的 binlog 日志对缓存进行操作。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5ClWLCY3-1681383922715)(https://raw.githubusercontent.com/viacheung/img/main/image/1735bb588215b298~tplv-t2oaga2asx-watermark.awebp)]

什么是缓存击穿?

(热点key失效)

缓存击穿跟缓存雪崩有点类似,缓存雪崩是大规模的key失效,而缓存击穿是某个热点的key失效(及你太美),大并发集中对其进行请求,就会造成大量请求读缓存没读到数据,从而导致高并发访问数据库,引起数据库压力剧增。这种现象就叫做缓存击穿。

从两个方面解决,第一是否可以考虑热点key不设置过期时间,第二是否可以考虑降低打在数据库上的请求数量。

解决方案:

  • 在缓存失效后,通过互斥锁或者队列来控制读数据写缓存的线程数量==,比如某个key只允许一个线程查询数据和写缓存,其他线程等待。这种方式会阻塞其他的线程,此时系统的吞吐量会下降
  • 热点数据缓存永远不过期。永不过期实际包含两层意思:
    • 物理不过期,针对热点key不设置过期时间
    • 逻辑过期,把过期时间存在key对应的value里,如果发现要过期了,通过一个后台的异步线程进行缓存的构建

什么是缓存穿透?

(缓存数据库都无)

缓存穿透是指用户请求的数据在缓存中不存在即没有命中,同时在数据库中也不存在,导致用户每次请求该数据都要去数据库中查询一遍。如果有恶意攻击者不断请求系统中不存在的数据,会导致短时间大量请求落在数据库上,造成数据库压力过大,甚至导致数据库承受不住而宕机崩溃。

缓存穿透的关键在于在Redis中查不到key值,它和缓存击穿的根本区别在于传进来的key在Redis中是不存在的。假如有黑客传进大量的不存在的key,那么大量的请求打在数据库上是很致命的问题,所以在日常开发中要对参数做好校验,一些非法的参数,不可能存在的key就直接返回错误提示。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lZinelA6-1681383922716)(https://raw.githubusercontent.com/viacheung/img/main/image/2021013117512340.png)]

解决方法:

  • 将无效的key存放进Redis中:

当出现Redis查不到数据,数据库也查不到数据的情况,我们就把这个key保存到Redis中,设置value=“null”,并设置其过期时间极短,后面再出现查询这个key的请求的时候,直接返回null,就不需要再查询数据库了。但这种处理方式是有问题的,假如传进来的这个不存在的Key值每次都是随机的,那存进Redis也没有意义。

  • 使用布隆过滤器:

(准确度换空间?)

如果布隆过滤器==判定某个 key 不存在布隆过滤器中,那么就一定不存在,如果判定某个 key 存在,那么很大可能是存在(存在一定的误判率)。==于是我们可以在缓存之前再加一个布隆过滤器,将数据库中的所有key都存储在布隆过滤器中,在查询Redis前先去布隆过滤器查询 key 是否存在,如果不存在就直接返回请求参数错误信息给客户端,不让其访问数据库,从而避免了对底层存储系统的查询压力。

如何选择:针对一些恶意攻击,攻击带过来的大量key是随机,那么我们采用第一种方案就会缓存大量不存在key的数据。那么这种方案就不合适了,我们可以先对使用布隆过滤器方案进行过滤掉这些key。所以,针对这种key异常多、请求重复率比较低的数据,优先使用第二种方案直接过滤掉。而对于空数据的key有限的,重复率比较高的,则可优先采用第一种方式进行缓存。

缓存雪崩

(大规模的key失效)

如果缓在某一个时刻出现大规模的key失效,那么就会导致大量的请求打在了数据库上面,导致数据库压力巨大,如果在高并发的情况下,可能瞬间就会导致数据库宕机。这时候如果运维马上又重启数据库,马上又会有新的流量把数据库打死。这就是缓存雪崩。

造成缓存雪崩的关键在于同一时间的大规模的key失效,主要有两种可能:

  • 第一种是Redis宕机
  • 第二种可能就是采用了相同的过期时间。

例子:秒杀开始 12 个⼩时之前,我们统⼀存放了⼀批商品到 Redis 中,设置的缓存过期时间也是 12 个⼩时,那么秒杀开始的时候,这些秒杀的商品的访问直接就失效了。导致的情况就是,相应的请求直接就落到了数据库上,就像雪崩⼀样可怕。

有哪些解决办法?

解决方案:

1、事前:

  • 均匀过期:设置不同的过期时间,让缓存失效的时间尽量均匀,避免相同的过期时间导致缓存雪崩,造成大量数据库的访问。如把每个Key的失效时间都加个随机值,setRedis(Key,value,time + Math.random() * 10000);,保证数据不会在同一时间大面积失效。
  • 分级缓存:第一级缓存失效的基础上,访问二级缓存,每一级缓存的失效时间都不同。
  • 热点数据缓存永远不过期。永不过期实际包含两层意思:
    • 物理不过期,针对热点key不设置过期时间
    • 逻辑过期,把过期时间存在key对应的value里,如果发现要过期了,通过一个后台的异步线程进行缓存的构建(定时任务)
  • 保证Redis缓存的高可用,防止Redis宕机导致缓存雪崩的问题。可以使用 主从+ 哨兵,Redis集群来避免 Redis 全盘崩溃的情况。

2、事中:

  • 互斥锁:在缓存失效后,通过互斥锁或者队列来控制读数据写缓存的线程数量,比如某个key只允许一个线程查询数据和写缓存,其他线程等待。这种方式会阻塞其他的线程,此时系统的吞吐量会下降
  • 使用熔断机制,限流降级。当流量达到一定的阈值,直接返回“系统拥挤”之类的提示,防止过多的请求打在数据库上将数据库击垮,至少能保证一部分用户是可以正常使用,其他用户多刷新几次也能得到结果。

3、事后:

开启Redis持久化机制,尽快恢复缓存数据,一旦重启,就能从磁盘上自动加载数据恢复内存中的数据。

什么是缓存预热?

缓存预热是指系统上线后,提前将相关的缓存数据加载到缓存系统。避免在用户请求的时候,先查询数据库,然后再将数据缓存的问题,用户直接查询事先被预热的缓存数据。

如果不进行预热,那么Redis初始状态数据为空,系统上线初期,对于高并发的流量,都会访问到数据库中, 对数据库造成流量的压力。

缓存预热解决方案:

  • 数据量不大的时候,工程启动的时候进行加载缓存动作;
  • 数据量大的时候,设置一个定时任务脚本,进行缓存的刷新;(防止启动太慢)
  • 数据量太大的时候,优先保证热点数据进行提前加载到缓存。

什么是缓存降级?

缓存降级是指==缓存失效或缓存服务器挂掉的情况下,不去访问数据库,直接返回默认数据或访问服务的内存数据。==降级一般是有损的操作,所以尽量减少降级对于业务的影响程度。

在进行降级之前要对系统进行梳理,看看系统是不是可以丢卒保帅;从而梳理出哪些必须誓死保护,哪些可降级;比如可以参考日志级别设置预案:

  • 一般:比如有些服务偶尔因为网络抖动或者服务正在上线而超时,可以自动降级;
  • 警告:有些服务在一段时间内成功率有波动(如在95~100%之间),可以自动降级或人工降级,并发送告警;
  • 错误:比如可用率低于90%,或者数据库连接池被打爆了,或者访问量突然猛增到系统能承受的最大阀值,此时可以根据情况自动降级或者人工降级;
  • 严重错误:比如因为特殊原因数据错误了,此时需要紧急人工降级。

如何保证缓存和数据库数据的⼀致性?

缓存失效时间变短(不推荐,治标不治本):我们让缓存数据的过期时间变短,这样的话缓存就会从数据库中加载数据。另外,这种解决办法对于先操作缓存后操作数据库的场景不适⽤

增加cache更新重试机制(常⽤) : 如果 cache 服务当前不可⽤导致缓存删除失败的话,我们就隔⼀段时间进⾏重试,重试次数可以⾃⼰定。如果多次重试还是失败的话,我们可以把当前更新失败的 key 存⼊队列中,等缓存服务可⽤之后,再将 缓存中对应的 key 删除即可

线程模型

Redis为何选择单线程?

Redis 6.0以前,Redis的核心网络模型选择用单线程来实现。先来看下官方的回答:

核心意思就是,对于一个 DB 来说,CPU 通常不会是瓶颈,因为大多数请求不会是 CPU 密集型的,而是 I/O 密集型。具体到 Redis的话,如果不考虑 RDB/AOF 等持久化方案,Redis是完全的纯内存操作,执行速度是非常快的,因此这部分操作通常不会是性能瓶颈,==Redis真正的性能瓶颈在于网络 I/O,也就是客户端和服务端之间的网络传输延迟,==因此 Redis选择了单线程的 I/O 多路复用来实现它的核心网络模型。

实际上更加具体的选择单线程的原因如下:

  • 避免过多的上下文切换开销:如果是单线程则可以规避进程内频繁的线程切换开销,因为程序始终运行在进程中单个线程内,没有多线程切换的场景。
  • 避免同步机制的开销:如果 Redis选择多线程模型,又因为 Redis是一个数据库,那么势必涉及到底层数据同步的问题,则必然会引入某些同步机制,比如锁,而我们知道 Redis不仅仅提供了简单的 key-value 数据结构,还有 list、set 和 hash 等等其他丰富的数据结构,而不同的数据结构对同步访问的加锁粒度又不尽相同,可能会导致在操作数据过程中带来很多加锁解锁的开销,增加程序复杂度的同时还会降低性能。
  • 简单可维护:如果 Redis使用多线程模式,那么所有的底层数据结构都必须实现成线程安全的,这无疑又使得 Redis的实现变得更加复杂。

总而言之,Redis选择单线程可以说是多方博弈之后的一种权衡:在保证足够的性能表现之下,使用单线程保持代码的简单和可维护性。

Redis真的是单线程?

讨论 这个问题前,先看下 Redis的版本中两个重要的节点:

  1. Redisv4.0(引入多线程处理异步任务)
  2. Redis 6.0(在网络模型中实现多线程 I/O )

所以,网络上说的Redis是单线程,通常是指在Redis 6.0之前,其核心网络模型使用的是单线程。

且Redis6.0引入多线程I/O,只是用来处理网络数据的读写和协议的解析,而执行命令依旧是单线程

Redis在 v4.0 版本的时候就已经引入了的多线程来做一些异步操作,此举主要针对的是那些非常耗时的命令,通过将这些命令的执行进行异步化,避免阻塞单线程的事件循环。

在 Redisv4.0 之后增加了一些的非阻塞命令如 UNLINKFLUSHALL ASYNCFLUSHDB ASYNC

Redis 6.0为何引入多线程?

很简单,就是 Redis的网络 I/O 瓶颈已经越来越明显了。

随着互联网的飞速发展,互联网业务系统所要处理的线上流量越来越大,Redis的单线程模式会导致系统消耗很多 CPU 时间在网络 I/O 上从而降低吞吐量,要提升 Redis的性能有两个方向:

  • 优化网络 I/O 模块
  • 提高机器内存读写的速度

后者依赖于硬件的发展,暂时无解。所以只能从前者下手,网络 I/O 的优化又可以分为两个方向:

  • 零拷贝技术或者 DPDK 技术
  • 利用多核优势

零拷贝技术有其局限性,无法完全适配 Redis这一类复杂的网络 I/O 场景,更多网络 I/O 对 CPU 时间的消耗和 Linux 零拷贝技术。而 DPDK 技术通过旁路网卡 I/O 绕过内核协议栈的方式又太过于复杂以及需要内核甚至是硬件的支持。

总结起来,Redis支持多线程主要就是两个原因:

  • 可以充分利用服务器 CPU 资源,目前主线程只能利用一个核
  • 多线程任务可以分摊 Redis 同步 IO 读写负荷

Redis 6.0 采用多线程后,性能的提升效果如何?

Redis 作者 antirez 在 RedisConf 2019 分享时曾提到:Redis 6 引入的多线程 IO 特性对性能提升至少是一倍以上

国内也有大牛曾使用 unstable 版本在阿里云 esc 进行过测试,GET/SET 命令在 4 线程 IO 时性能相比单线程是几乎是翻倍了。

介绍下Redis的线程模型

Redis的线程模型包括Redis 6.0之前和Redis 6.0。

下面介绍的是Redis 6.0之前。

Redis 是基于 reactor 模式开发了网络事件处理器,这个处理器叫做文件事件处理器(file event handler)。由于这个文件事件处理器是单线程的,所以 Redis 才叫做单线程的模型。采用 IO 多路复用机制同时监听多个 Socket,根据 socket 上的事件来选择对应的事件处理器来处理这个事件。

IO多路复用是 IO 模型的一种,有时也称为异步阻塞 IO,是基于经典的 Reactor 设计模式设计的。多路指的是多个 Socket 连接,复用指的是复用一个线程。多路复用主要有三种技术:Select,Poll,Epoll。

Epoll 是最新的也是目前最好的多路复用技术。

模型如下图:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2wm3mW42-1681383922717)(https://raw.githubusercontent.com/viacheung/img/main/image/202105092153018231.png)]

文件事件处理器的结构包含了四个部分:

  • 多个 Socket。Socket 会产生 AE_READABLE(读) 和 AE_WRITABLE (写)事件:
    • 当 socket 变得可读时或者有新的可以应答的 socket 出现时,socket 就会产生一个 AE_READABLE 事件
    • 当 socket 变得可写时,socket 就会产生一个 AE_WRITABLE(可写) 事件。
  • IO 多路复用程序
  • 文件事件分派器
  • 事件处理器。事件处理器包括:连接应答处理器、命令请求处理器、命令回复处理器,每个处理器对应不同的 socket 事件:
    • 如果是客户端要连接 Redis,那么会为 socket 关联连接应答处理器
    • 如果是客户端要写数据到 Redis(读、写请求命令),那么会为 socket 关联命令请求处理器
    • 如果是客户端要从 Redis 读数据,那么会为 socket 关联命令回复处理器

多个 socket 会产生不同的事件,不同的事件对应着不同的操作,IO 多路复用程序监听着这些 Socket,当这些 Socket 产生了事件,IO 多路复用程序会将这些事件放到一个队列中,通过这个队列,以有序、同步、每次一个事件的方式向文件时间分派器中传送。当事件处理器处理完一个事件后,IO 多路复用程序才会继续向文件分派器传送下一个事件。

下图是客户端与 Redis 通信的一次完整的流程:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-10d3zS9p-1681383922718)(https://raw.githubusercontent.com/viacheung/img/main/image/202105092153019692.png)]

  1. Redis 启动初始化的时候,Redis 会将连接应答处理器与 AE_READABLE 事件关联起来。
  2. 如果一个客户端跟 Redis 发起连接,此时 Redis 会产生一个 AE_READABLE 事件,由于开始之初 AE_READABLE 是与连接应答处理器关联,所以由连接应答处理器来处理该事件,这时连接应答处理器会与客户端建立连接,创建客户端响应的 socket同时将这个 socket 的 AE_READABLE 事件与命令请求处理器关联起来。
  3. 如果这个时间客户端向 Redis 发送一个命令(set k1 v1),这时 socket 会产生一个 AE_READABLE 事件,IO 多路复用程序会将该事件压入队列中,此时事件分派器从队列中取得该事件,由于该 socket 的 AE_READABLE 事件已经和命令请求处理器关联了,因此事件分派器会将该事件交给命令请求处理器处理,命令请求处理器读取事件中的命令并完成。操作完成后,Redis 会将该 socket 的 AE_WRITABLE 事件与命令回复处理器关联。
  4. 如果客户端已经准备好接受数据后,Redis 中的该 socket 会产生一个 AE_WRITABLE 事件,同样会压入队列然后被事件派发器取出交给相对应的命令回复处理器,由该命令回复处理器将准备好的响应数据写入 socket 中,供客户端读取。
  5. 命令回复处理器写完后,就会删除该 socket 的 AE_WRITABLE 事件与命令回复处理器的关联关系。

Redis 6.0 多线程的实现机制?

流程简述如下

  • 主线程负责接收建立连接请求,获取 Socket 放入全局等待读处理队列。
  • 主线程处理完读事件之后,通过 RR(Round Robin)将这些连接分配给这些 IO 线程。
  • 主线程阻塞等待 IO 线程读取 Socket 完毕。
  • 主线程通过单线程的方式执行请求命令,请求数据读取并解析完成,但并不执行。
  • 主线程阻塞等待 IO 线程将数据回写 Socket 完毕。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qpNdICUp-1681383922719)(https://raw.githubusercontent.com/viacheung/img/main/image/image-20210828175543973.png)]

Redis 6.0开启多线程后,是否会存在线程并发安全问题?

从实现机制可以看出,Redis 的多线程部分只是用来处理网络数据的读写和协议解析,执行命令仍然是单线程顺序执行。

所以我们不需要去考虑控制 Key、Lua、事务,LPUSH/LPOP 等等的并发及线程安全问题。

Redis 6.0 与 Memcached 多线程模型的对比

  • **相同点:**都采用了 Master 线程 -Worker 线程的模型。

  • 不同点:Memcached 执行主逻辑也是在 Worker 线程里,模型更加简单,实现了真正的线程隔离,符合我们对线程隔离的常规理解。

    而 Redis 把处理逻辑交还给 Master 线程,虽然一定程度上增加了模型复杂度,但也解决了线程并发安全等问题。

Redis 单线程模型详解

单线程如何监听来⾃客户端的⼤量连接?

答:Redis 通过IO 多路复⽤程序 来监听来⾃客户端的⼤量连接

好处:I/O 多路复⽤技术的使⽤让 Redis 不需要额外创建多余的线程来监听客户端的⼤量连接,降低了资源的消耗

Redis 没有使⽤多线程?为什么不使⽤多线程?

  1. 编程容易 容易维护;
  2. Redis 的性能瓶颈不在 CPU ,主要在内存和⽹络,没必要多线程;
  3. 多线程就会存在死锁、线程上下⽂切换等问题,甚⾄会影响性能。

Redis6.0 之后为何引⼊了多线程

为了提⾼⽹络 IO 读写性能 但也只是在⽹络数据的读写这类耗时操作上使⽤了, 执⾏命令仍然是单线程顺序执⾏

默认禁用,修改 redis 配置⽂件 redis.conf 开启

事务

Redis事务的概念

Redis的事务并不是我们传统意义上理解的事务,我们都知道 单个 Redis 命令的执行是原子性,但 Redis 没有在事务上增加任何维持原子性的机制,所以 Redis 事务的执行并不是原子性的

事务可以理解为一个打包的批量执行脚本,但批量指令并非原子化的操作,中间某条指令的失败不会导致前面已做指令的回滚,也不会造成后续的指令不做。(没回滚)

总结:

1. Redis事务中如果有某一条命令执行失败,之前的命令不会回滚,其后的命令仍然会被继续执行。鉴于这个原因,所以说Redis的事务严格意义上来说是不具备原子性的

2. Redis事务中所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。

3. 在事务开启之前,如果客户端与服务器之间出现通讯故障并导致网络断开,其后所有待执行的语句都将不会被服务器执行。然而如果网络中断事件是发生在客户端执行EXEC命令之后,那么该事务中的所有命令都会被服务器执行。

当使用Append-Only模式时,Redis会通过调用系统函数write将该事务内的所有写操作在本次调用中全部写入磁盘。然而如果在写入的过程中出现系统崩溃,如电源故障导致的宕机,那么此时也许只有部分数据被写入到磁盘,而另外一部分数据却已经丢失。Redis服务器会在重新启动时执行一系列必要的一致性检测,一旦发现类似问题,就会立即退出并给出相应的错误提示。此时,我们就要充分利用Redis工具包中提供的Redis-check-aof工具,**该工具可以帮助我们定位到数据不一致的错误,并将已经写入的部分数据进行回滚。**修复之后我们就可以再次重新启动Redis服务器了。

Redis事务的三个阶段

  1. multi 开启事务
  2. 大量指令入队
  3. exec执行事务块内命令,截止此处一个事务已经结束。
  4. discard 取消事务
  5. watch 监视一个或多个key,如果事务执行前key被改动,事务将打断。unwatch 取消监视。

事务执行过程中,如果服务端收到有EXEC、DISCARD、WATCH、MULTI之外的请求,将会把请求放入队列中排队.

Redis事务相关命令

Redis事务功能是通过MULTI、EXEC、DISCARD和WATCH 四个原语实现的

  • WATCH 命令是一个乐观锁,可以为 Redis 事务提供 check-and-set (CAS)行为。 可以监控一个或多个键,一旦其中有一个键被修改(或删除),之后的事务就不会执行,监控一直持续到EXEC命令。
  • MULTI命令用于开启一个事务,它总是返回OK。 MULTI执行之后,客户端可以继续向服务器发送任意多条命令,这些命令不会立即被执行,而是被放到一个队列中,当EXEC命令被调用时,所有队列中的命令才会被执行。
  • EXEC:执行所有事务块内的命令。返回事务块内所有命令的返回值,按命令执行的先后顺序排列。 当操作被打断时,返回空值 nil 。 通过调用DISCARD,客户端可以清空事务队列,并放弃执行事务, 并且客户端会从事务状态中退出。
  • UNWATCH命令可以取消watch对所有key的监控。

Redis事务支持隔离性吗?

Redis 是单进程程序,并且它保证在执行事-务时,不会对事务进行中断,事务可以运行直到执行完所有事务队列中的命令为止。因此,Redis 的事务是总是带有隔离性的

Redis为什么不支持事务回滚?

  • Redis 命令只会因为错误的语法而失败,或是命令用在了错误类型的键上面,这些问题不能在入队时发现,这也就是说,从实用性的角度来说,失败的命令是由编程错误造成的,而这些错误应该在开发的过程中被发现,而不应该出现在生产环境中.
  • 因为不需要对回滚进行支持,所以 Redis 的内部可以保持简单且快速。

Redis事务其他实现(?)

  • 基于Lua脚本,Redis可以保证脚本内的命令一次性、按顺序地执行, 其同时也不提供事务运行错误的回滚,执行过程中如果部分命令运行错误,剩下的命令还是会继续运行完。
  • 基于中间标记变量,通过另外的标记变量来标识事务是否执行完成,读取数据时先读取该标记变量判断是否事务执行完成。但这样会需要额外写代码实现,比较繁琐。

主从、哨兵、集群

36. Redis常见使用方式有哪些?

Redis的几种常见使用方式包括:

  • Redis单副本;
  • Redis多副本(主从);
  • Redis Sentinel(哨兵);
  • Redis Cluster;
  • Redis自研。

使用场景:

如果数据量很少,主要是承载高并发高性能的场景,比如缓存一般就几个G的话,单机足够了。

主从模式:master 节点挂掉后,需要手动指定新的 master,可用性不高,基本不用。

哨兵模式:master 节点挂掉后,哨兵进程会主动选举新的 master,可用性高,但是每个节点存储的数据是一样的,浪费内存空间。数据量不是很多,集群规模不是很大,需要自动容错容灾的时候使用。

Redis cluster 主要是针对海量数据+高并发+高可用的场景,如果是海量数据,如果你的数据量很大,那么建议就用Redis cluster,所有master的容量总和就是Redis cluster可缓存的数据容量。

介绍下Redis单副本

Redis单副本,采用单个Redis节点部署架构,没有备用节点实时同步数据,不提供数据持久化和备份策略,适用于数据可靠性要求不高的纯缓存业务场景。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9W9wWm73-1681383922720)(https://raw.githubusercontent.com/viacheung/img/main/image/image-20210829103307048.png)]

优点:

  • 架构简单,部署方便;
  • 高性价比:缓存使用时无需备用节点(单实例可用性可以用supervisor或crontab保证),当然为了满足业务的高可用性,也可以牺牲一个备用节点,但同时刻只有一个实例对外提供服务;
  • 高性能。

缺点:

  • 不保证数据的可靠性;
  • 在缓存使用,进程重启后,数据丢失,即使有备用的节点解决高可用性,但是仍然不能解决缓存预热问题,因此不适用于数据可靠性要求高的业务;
  • 高性能受限于单核CPU的处理能力(Redis是单线程机制),CPU为主要瓶颈,所以适合操作命令简单,排序、计算较少的场景。也可以考虑用Memcached替代。

spring

使用Spring框架的好处是什么?

  • **轻量:**Spring 是轻量的,基本的版本大约2MB
  • **控制反转:**Spring通过控制反转实现了松散耦合,对象们给出它们的依赖,而不是创建或查找依赖的对象们
  • **面向切面的编程(AOP):**Spring支持面向切面的编程,并且把应用业务逻辑和系统服务分开
  • **容器:**Spring 包含并管理应用中对象的生命周期和配置
  • **MVC框架:**Spring的WEB框架是个精心设计的框架,是Web框架的一个很好的替代品
  • **事务管理:**Spring 提供一个持续的事务管理接口,可以扩展到上至本地事务下至全局事务(JTA)
  • **异常处理:**Spring 提供方便的API把具体技术相关的异常(比如由JDBC,Hibernate or JDO抛出的)转化为一致的unchecked 异常。

特点

方便解耦,简化开发

AOP 编程的支持

声明式事务的支持

方便程序的测试

方便集成各种优秀框架

spring开发步骤

Spring的开发步骤
① 导入坐标(context )
② 创建Bean(编写Dao接口和实现类 )
③ 创建applicationContext.xml(类路径下(resources) )
④ 在配置文件中进行配置(

Bean

用于配置对象交由Spring 来创建。默认情况下它调用的是类中的无参构造函数,如果没有无参构造函数则不能创建成功。

id: Bean实例在Spring容器中
class: Bean的全限定名称

默认值,单例的

init-method:指定类中的初始化方法名称

destroy-method:指定类中销毁方法名称

Bean实例化三种方式

 无参构造方法实例化
 工厂静态方法实例化
 工厂实例方法实例化

Bean的依赖注入

通过控制反转,把对象的创建交给了 Spring,但IOC 解耦只是降低他们的依赖关系,但不会消除。例如:业务层仍会调用持久层的方法。
那这种业务层和持久层的依赖关系,在使用 Spring 之后,就让 Spring 来维护了。简单的说,就是坐等框架把持久层对象传入业务层,而不用我们自己去获取

对象注入哪几种方式?

  • 构造函数注入

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-E9F4kZNY-1681383922721)(https://raw.githubusercontent.com/viacheung/img/main/image/image-20230410165353388.png)]

好处:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TYgtynDb-1681383922722)(https://raw.githubusercontent.com/viacheung/img/main/image/image-20230410165421975.png)]

  • setter 注入(在XML中写入,然后在set方法中注入。
private IUserDao userDao1;
 
public void setUserDao(IUserDao userDao1) {//这里注意,name方法与类中成员变量名和方法的参数名都无关,只与set方法名有关
    this.userDao1 = userDao1;
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-p3ksEc0M-1681383922723)(https://raw.githubusercontent.com/viacheung/img/main/image/image-20230410165552733.png)]

  • 接口注入(注解注入)

    前面两种都可以检测出循环依赖

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9TxbmrDR-1681383922723)(https://raw.githubusercontent.com/viacheung/img/main/image/image-20230410165617392.png)]

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tDWMNW1J-1681383922724)(https://raw.githubusercontent.com/viacheung/img/main/image/image-20230410165642347.png)]

构造注入vs setter注入

构造函数注入 setter 注入
没有部分注入 有部分注入
不会覆盖 setter 属性 会覆盖 setter 属性
任意修改都会创建一个新实例 任意修改不会创建一个新实例
适用于设置很多属性 适用于设置少量属性

普通属性呢?

 普通数据类型
 引用数据类型
 集合数据类型

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AjhQafH8-1681383922725)(https://raw.githubusercontent.com/viacheung/img/main/image/image-20230317210715447.png)]

一样 也是 set方法 this.age=age,然后直接输出age

getBean

容器中某一类型的Bean有多个–用id

有一个–用class

区分 BeanFactory 和 ApplicationContext?

BeanFactory ApplicationContext
它使用懒加载 它使用即时加载
它使用语法显式提供资源对象 它自己创建和管理资源对象
不支持国际化 支持国际化
不支持基于依赖的注解 支持基于依赖的注解

BeanFactory和ApplicationContext的优缺点分析:

BeanFactory的优缺点:

  • 优点:应用启动的时候占用资源很少,对资源要求较高的应用,比较有优势;
  • 缺点:运行速度会相对来说慢一些。而且有可能会出现空指针异常的错误,而且通过Bean工厂创建的Bean生命周期会简单一些。

ApplicationContext的优缺点:

  • 优点
  • 1、启动即加载所有Bean
  • 2、系统运行的速度快
  • 3、在系统启动的时候,可以发现系统中的配置问题
  • 缺点
  • 1、所有的对象都预加载,缺点就是内存占用较大。

spring提供了哪些配置方式

  • 基于 xml 配置

bean 所需的依赖项和服务在 XML 格式的配置文件中指定。这些配置文件通常包含许多 bean 定义和特定于应用程序的配置选项。它们通常以 bean 标签开头。

<bean id="studentbean" class="org.edureka.firstSpring.StudentBean">
 <property name="name" value="Edureka">property>
bean>
  • 基于注解配置

通过在相关的类,方法或字段声明上使用注解,将 bean 配置为组件类本身,而不是使用 XML 来描述 bean 装配。默认情况下,Spring 容器中未打开注解装配。因此需要在使用它之前在 Spring 配置文件中启用它。例如:

<beans> 
    <context:annotation-config/> 
     
beans>
  • 基于 Java API 配置

Spring 的 Java 配置是通过使用 @Bean 和 @Configuration 来实现。

  1. @Bean 注解扮演与 元素相同的角色。
  2. @Configuration 类允许通过简单地调用同一个类中的其他 @Bean 方法来定义 bean 间依赖关系。
@Configuration
public class StudentConfig {
    @Bean
    public StudentBean myStudent() {
        return new StudentBean();
    }
}

如何理解IoC和DI?

IOC

就是控制反转,通俗的说就是我们不用自己创建实例对象,这些都交给Spring的bean工厂帮我们创建管理。

这也是Spring的核心思想,通过面向接口编程的方式来是实现对业务组件的动态依赖。这就意味着IOC是Spring针对解决程序耦合而存在的。在实际应用中,Spring通过配置文件(xml或者properties)指定需要实例化的java类(类名的完整字符串),包括这些java类的一组初始化值,通过加载读取配置文件,用Spring提供的方法(getBean())就可以获取到我们想要的根据指定配置进行初始化的实例对象。

  • 优点:IOC或依赖注入减少了应用程序的代码量。它使得应用程序的测试很简单,因为在单元测试中不再需要单例或JNDI查找机制。简单的实现以及较少的干扰机制使得松耦合得以实现。IOC容器支持勤性单例及延迟加载服务。

DI:DI—Dependency Injection,即“依赖注入”:组件之间依赖关系由容器在运行期决定,形象的说,即由容器动态的将某个依赖关系注入到组件之中。依赖注入的目的并非为软件系统带来更多功能,而是为了提升组件重用的频率,并为系统搭建一个灵活、可扩展的平台。通过依赖注入机制,我们只需要通过简单的配置,而无需任何代码就可指定目标需要的资源,完成自身的业务逻辑,而不需要关心具体的资源来自何处,由谁实现。

spring特征

核⼼技术 :依赖注⼊(DI), AOP,事件(events),资源, 验证,数据绑定,
测试 : Spring MVC 测试,
数据访问 :事务, DAO⽀持, JDBC, ORM,XML。
Web⽀持 : Spring MVC和Spring WebFlux Web框架。
集成 :远程处理, 电⼦邮件,任务,调度,缓存。

列举⼀些重要的Spring模块?

Spring Core: 基础,可以说 Spring 其他所有的功能都需要依赖于该类库。主要提供 IoC 依赖注⼊功能。
Spring Aspects : 该模块为与AspectJ的集成提供⽀持。
Spring AOP :提供了⾯向切⾯的编程实现。
Spring JDBC : Java数据库连接。
Spring JMS : Java消息服务。
Spring ORM : ⽤于⽀持Hibernate等ORM⼯具。
Spring Web : 为创建Web应⽤程序提供⽀持。
Spring Test : 提供了对 JUnit 和 TestNG 测试的⽀持。

Spring **IOC& AOP **

ioc

设计思想 :原本在程序中⼿动创建对象的控制权,交由Spring框架来管理

IoC 容器实际上就是个Map(key, value) ,Map 中存放的是各
种对象。

IoC 容器类似⼯⼚,当需要创建⼀个对象的时候,只需要配置好配置⽂件/注解即可

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-eRrtkubb-1681383922726)(https://raw.githubusercontent.com/viacheung/img/main/image/image-20230314005000935.png)]

解析xml注册到beanFactory

aop

AOP 的底层是通过 Spring 提供的的动态代理技术实现的。在运行期间, Spring通过动态代理技术动态的生成代理对象,代理对象方法执行时进行增强功能的介入,在去调用目标对象的方法,从而完成功能的增强

 JDK 代理 : 基于接口的动态代理技术
 cglib 代理:基于父类的动态代理技术

采用哪种?

在 spring 中,框架会根据目标类是否实现了接口来决定采用哪种动态代理的方式

面向切面编程,将公共的代码逻辑抽象出来变成一个切面,然后注入到目标对象(具体业务)中去,通过动态代理的方式,将需要注入切面的对象进行代理,在进行调用的时候,将公共的逻辑直接添加进去,而不需要修改原有业务的逻辑代码,只需要在原来的业务逻辑基础之上做一些增强功能即可。

Spring AOP就是基于动态代理的,

如果要代理的对象,实现了某个接⼝,那么Spring AOP会使(基于代理对象的接口)
⽤JDK Proxy,去创建代理对象,⽽对于没有实现接⼝的对象,

会使⽤ Cglib ⽣成⼀个被代理对象的⼦类来作为代理,如下图所示: (基于代理对象的子类)

基于注解的aop开发步骤:

① 创建目标接口和目标类(内部有切点)(一个接口 一个实现类)
② 创建切面类(内部有增强方法)
③ 将目标类和切面类的对象创建权交给 spring(xml 或者 注解)
④ 在切面类中使用注解配置织入关系(定义是哪个切面 目标类的方法用的切面类的哪个增强方法)
⑤ 在配置文件中开启组件扫描和 AOP 的自动代理
⑥ 测试

什么是 AOP?

面向切面编程, 它与 OOP( Object-Oriented Programming, 面向对象编程) 相辅相成, 提供了与 OOP 不同的抽象软件结构的视角. 在 OOP 中, 我们以类(class)作为我们的基本单元, 而 AOP 中的基本单元是 Aspect(切面)

AOP 有哪些实现方式?

主要分为两大类:

  • 静态代理 - 指使用 AOP 框架提供的命令进行编译,从而在编译阶段就可生成 AOP 代理类,因此也称为编译时增强;
    • 编译时编织(特殊编译器实现)
    • 类加载时编织(特殊的类加载器实现)。
  • 动态代理 - 在运行时在内存中“临时”生成 AOP 动态代理类,因此也被称为运行时增强。
    • JDK 动态代理:通过反射来接收被代理的类,并且要求被代理的类必须实现一个接口 。JDK 动态代理的核心是 InvocationHandler 接口和 Proxy 类 。
    • CGLIB动态代理: 如果目标类没有实现接口,那么 Spring AOP 会选择使用 CGLIB 来动态代理目标类 。CGLIB ( Code Generation Library ),是一个代码生成的类库,可以在运行时动态的生成某个类的子类,注意, CGLIB 是通过继承的方式做的动态代理,因此如果某个类被标记为 final ,那么它是无法使用 CGLIB 做动态代理的。

Spring AOP 和 AspectJ AOP 有什么区别?

Spring AOP 属于运⾏时增强,⽽ AspectJ 是编译时增强。

Spring 中的 bean 的作⽤域有哪些?

singleton : 唯⼀ bean 实例, Spring 中的 bean 默认都是单例的。
prototype : 每次请求都会创建⼀个新的 bean 实例。
request : 每⼀次HTTP请求都会产⽣⼀个新的bean,该bean仅在当前HTTP request内有效。
session : 每⼀次HTTP请求都会产⽣⼀个新的 bean,该bean仅在当前 HTTP session 内有效。

Spring 中的单例 bean 的线程安全问题了解吗(Threadlocal)?

主要是因为当多个线程操作同⼀个对象的时候,对这个对象的⾮静态成员变量的写操作会存在线程安全问题。
常⻅的有两种解决办法:

在类中定义⼀个ThreadLocal成员变量,将⾮静态成员变量保存在 ThreadLocal 中(推荐的⼀种⽅式)。 作线程隔离

@Component 和 @Bean 的区别是什么?

@Component 注解作⽤于类,⽽ @Bean 注解作⽤于⽅法。

@Component 通常是通过类路径扫描⾃动装配到Spring容器中( @ComponentScan 注解定义要扫描的路径)
Spring 的 bean 容器中)

引⽤第三⽅库中的类需要装配到 Spring 容器时,则只能通过@Bean 来实现

自动注入

resource 按name

autowired 按类型 +quirfy 按name

将⼀个类声明为Spring的 bean 的注解有哪些?

我们一般使用 @Autowired 注解自动装配 bean,要想把类标识成可用于 @Autowired 注解自动装配的 bean 的类,采用以下注解可实现:

  • @Component :通用的注解,可标注任意类为 Spring 组件。如果一个Bean不知道属于哪个层,可以使用@Component 注解标注。 8 @Repository : 对应持久层即 Dao 层,主要用于数据库相关操作。
  • @Service : 对应服务层,主要涉及一些复杂的逻辑,需要用到 Dao层。
  • @Controller : 对应 Spring MVC 控制层,主要用户接受用户请求并调用 Service 层返回数据给前端页面。

Spring 中的 bean ⽣命周期?

实例化----设置属性–前置处理 -init–后置处理–destory方法

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FireUwqM-1681383922727)(https://raw.githubusercontent.com/viacheung/img/main/image/image-20230314112152047.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mXKzsbfD-1681383922728)(https://raw.githubusercontent.com/viacheung/img/main/image/image-20230314112157474.png)]

创建过程

1,实例化bean对象,以及设置bean属性; 2,如果通过Aware接口声明了依赖关系,则会注入Bean对容器基础设施层面的依赖,Aware接口是为了感知到自身的一些属性。容器管理的Bean一般不需要知道容器的状态和直接使用容器。但是在某些情况下是需要在Bean中对IOC容器进行操作的。这时候需要在bean中设置对容器的感知。SpringIOC容器也提供了该功能,它是通过特定的Aware接口来完成的。 比如BeanNameAware接口,可以知道自己在容器中的名字。 如果这个Bean已经实现了BeanFactoryAware接口,可以用这个方式来获取其它Bean。 (如果Bean实现了BeanNameAware接口,调用setBeanName()方法,传入Bean的名字。 如果Bean实现了BeanClassLoaderAware接口,调用setBeanClassLoader()方法,传入ClassLoader对象的实例。 如果Bean实现了BeanFactoryAware接口,调用setBeanFactory()方法,传入BeanFactory对象的实例。) 3,紧接着会调用BeanPostProcess的前置初始化方法postProcessBeforeInitialization,主要作用是在Spring完成实例化之后,初始化之前,对Spring容器实例化的Bean添加自定义的处理逻辑。有点类似于AOP。 4,如果实现了BeanFactoryPostProcessor接口的afterPropertiesSet方法,做一些属性被设定后的自定义的事情。 5,调用Bean自身定义的init方法,去做一些初始化相关的工作。 6,调用BeanPostProcess的后置初始化方法,postProcessAfterInitialization去做一些bean初始化之后的自定义工作。 7,完成以上创建之后就可以在应用里使用这个Bean了

销毁过程

当Bean不再用到,便要销毁 1,若实现了DisposableBean接口,则会调用destroy方法; 2,若配置了destry-method属性,则会调用其配置的销毁方法;

总结

主要把握创建过程和销毁过程这两个大的方面; 创建过程:首先实例化Bean,并设置Bean的属性,根据其实现的Aware接口(主要是BeanFactoryAware接口,BeanFactoryAware,ApplicationContextAware)设置依赖信息, 接下来调用BeanPostProcess的postProcessBeforeInitialization方法,完成initial前的自定义逻辑;afterPropertiesSet方法做一些属性被设定后的自定义的事情;调用Bean自身定义的init方法,去做一些初始化相关的工作;然后再调用postProcessAfterInitialization去做一些bean初始化之后的自定义工作。这四个方法的调用有点类似AOP。 此时,Bean初始化完成,可以使用这个Bean了。 销毁过程:如果实现了DisposableBean的destroy方法,则调用它,如果实现了自定义的销毁方法,则调用之。

Spring MVC ⼯作原理了解吗?

客户端–request—disoatcherservelt–Handler–HandlerAdapter—由处理器适配器处理业务 返回一个modelandview对象,model返回数据 view视图渲染 返回浏览器

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CQAUrhSc-1681383922729)(https://raw.githubusercontent.com/viacheung/img/main/image/image-20230314112406771.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ywrZuucX-1681383922730)(https://raw.githubusercontent.com/viacheung/img/main/image/image-20230314112250270.png)]

HandlerMapping负责根据用户请求找到Handler即处理器,

SpringMVC基于Spring容器,所以在进行SpringMVC操作时,需要将Controller存储到Spring容器中

简单介绍 Spring MVC 的核心组件

组件 说明
DispatcherServlet Spring MVC 的核心组件,是请求的入口,负责协调各个组件工作
MultipartResolver 内容类型( Content-Type )为 multipart/* 的请求的解析器,例如解析处理文件上传的请求,便于获取参数信息以及上传的文件
HandlerMapping 请求的处理器匹配器,负责为请求找到合适的 HandlerExecutionChain 处理器执行链,包含处理器(handler)和拦截器们(interceptors
HandlerAdapter 处理器的适配器。因为处理器 handler 的类型是 Object 类型,需要有一个调用者来实现 handler 是怎么被执行。Spring 中的处理器的实现多变,比如用户处理器可以实现 Controller 接口、HttpRequestHandler 接口,也可以用 @RequestMapping 注解将方法作为一个处理器等,这就导致 Spring MVC 无法直接执行这个处理器。所以这里需要一个处理器适配器,由它去执行处理器
HandlerExceptionResolver 处理器异常解析器,将处理器( handler )执行时发生的异常,解析( 转换 )成对应的 ModelAndView 结果
RequestToViewNameTranslator 视图名称转换器,用于解析出请求的默认视图名
LocaleResolver 本地化(国际化)解析器,提供国际化支持
ThemeResolver 主题解析器,提供可设置应用整体样式风格的支持
ViewResolver 视图解析器,根据视图名和国际化,获得最终的视图 View 对象
FlashMapManager FlashMap 管理器,负责重定向时,保存参数至临时存储(默认 Session)

Spring MVC 对各个组件的职责划分的比较清晰。DispatcherServlet 负责协调,其他组件则各自做分内之事,互不干扰。

什么是 spring 的内部 bean?

将 bean 用作另一个 bean 的属性时,才能将 bean 声明为内部 bean,假设我们有一个 Student 类,其中引用了 Person 类,就可以这样。

什么是 spring 装配?

当 bean 在 Spring 容器中组合在一起时,它被称为装配或 bean 装配。 Spring 容器需要知道需要什么 bean 以及容器应该如何使用依赖注入来将 bean 绑定在一起,同时装配 bean。

Spring 容器能够自动装配 bean。也就是说,可以通过检查 BeanFactory 的内容让 Spring 自动解析 bean 的协作者。

自动装配的不同模式:

  • no - 这是默认设置,表示没有自动装配。应使用显式 bean 引用进行装配。
  • byName - 它根据 bean 的名称注入对象依赖项。它匹配并装配其属性与 XML 文件中由相同名称定义的 bean。
  • byType - 它根据类型注入对象依赖项。如果属性的类型与 XML 文件中的一个 bean 名称匹配,则匹配并装配属性。
  • 构造函数 - 它通过调用类的构造函数来注入依赖项。它有大量的参数。
  • autodetect - 首先容器尝试通过构造函数使用 autowire 装配,如果不能,则尝试通过 byType 自动装配。

自动装配有什么局限?

  • 覆盖的可能性 - 您始终可以使用 设置指定依赖项,这将覆盖自动装配。
  • 基本元数据类型 - 简单属性(如原数据类型,字符串和类)无法自动装配。
  • 令人困惑的性质 - 总是喜欢使用明确的装配,因为自动装配不太精确。

Spring中出现同名bean怎么办?

  • 同一个配置文件内同名的Bean,以最上面定义的为准
  • 不同配置文件中存在同名Bean,后解析的配置文件会覆盖先解析的配置文件
  • 同文件中ComponentScan和@Bean出现同名Bean。==同文件下@Bean的会生效,@ComponentScan扫描进来不会生效。==通过@ComponentScan扫描进来的优先级是最低的,原因就是它扫描进来的Bean定义是最先被注册的~

Spring 怎么解决循环依赖问题?

spring对循环依赖的处理有三种情况:

①构造器的循环依赖:这种依赖spring是处理不了的,直 接抛出BeanCurrentlylnCreationException异常。

②单例模式下的setter循环依赖:通过“三级缓存”处理循环依赖。

③非单例循环依赖:无法处理。

所以只针对2

spring对象实例三步

(1)createBeanInstance:实例化,其实也就是调用对象的构造方法实例化对象

(2)populateBean:填充属性,这一步主要是多bean的依赖属性进行填充

(3)initializeBean:调用spring xml中的init 方法。

三级缓存(对应上面进行哪一步了)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-b9KNvmR6-1681383922730)(https://raw.githubusercontent.com/viacheung/img/main/image/image-20230318003344460.png)]

A的某个field或者setter依赖了B的实例对象,同时B的某个field或者setter依赖了A的实例对象”这种循环依赖的情况。

  1. A首先完成了初始化的第一步(createBeanINstance实例化),并且将自己提前曝光到singletonFactories(三级缓存)中。

  2. 此时进行初始化的第二步,发现自己依赖对象B,此时就尝试去get(B),发现B还没有被create,所以走create流程,B在初始化第一步的时候发现自己依赖了对象A,于是尝试get(A),尝试一级缓存singletonObjects(肯定没有,因为A还没初始化完全),尝试二级缓存earlySingletonObjects(也没有),尝试三级缓存singletonFactories,由于A通过ObjectFactory将自己提前曝光了,所以B能够通过ObjectFactory.getObject拿到A对象(虽然A还没有初始化完全,但是总比没有好呀),B拿到A对象后顺利完成了初始化阶段1、2、3,完全初始化之后将自己放入到一级缓存singletonObjects中。

  3. 此时返回A中,A此时能拿到B的对象顺利完成自己的初始化阶段2、3,最终A也完成了初始化,进去了一级缓存singletonObjects中,而且更加幸运的是,由于B拿到了A的对象引用,所以B现在hold住的A对象完成了初始化。

Spring 中的单例 bean 的线程安全问题?

当多个用户同时请求一个服务时,容器会给每一个请求分配一个线程,这时多个线程会并发执行该请求对应的业务逻辑(成员方法),此时就要注意了,如果该处理逻辑中有对单例状态的修改(体现为该单例的成员属性),则必须考虑线程同步问题。 线程安全问题都是由全局变量及静态变量引起的。 若每个线程中对全局变量、静态变量只有读操作,而无写操作,一般来说,这个全局变量是线程安全的;若有多个线程同时执行写操作,一般都需要考虑线程同步,否则就可能影响线程安全.

无状态bean和有状态bean

  • 有状态就是有数据存储功能。有状态对象(Stateful Bean),就是有实例变量的对象,可以保存数据,是非线程安全的。在不同方法调用间不保留任何状态。
  • 无状态就是一次操作,不能保存数据。无状态对象(Stateless Bean),就是没有实例变量的对象 .不能保存数据,是不变类,是线程安全的。

在spring中无状态的Bean适合用不变模式,就是单例模式,这样可以共享实例提高性能。有状态的Bean在多线程环境下不安全,适合用Prototype原型模式。 Spring使用ThreadLocal解决线程安全问题。如果你的Bean有多种状态的话(比如 View Model 对象),就需要自行保证线程安全 。

Spring 框架中⽤到了哪些设计模式?

⼯⼚设计模式 : Spring使⽤⼯⼚模式通过 BeanFactoryApplicationContext(应用上下文) 创建 bean 对象。
代理设计模式 : Spring AOP 功能(动态代理)的实现。
单例设计模式 : Spring 中的 Bean 默认都是单例的。
包装器设计模式 : 我们的项⽬需要连接多个数据库,⽽且不同的客户在每次访问中根据需要会去访问不同的数据库。这种模式让我们可以根据客户的需求能够动态切换不同的数据源。
观察者模式: Spring 事件驱动模型就是观察者模式很经典的⼀个应⽤。(消息队列的发布订阅)
适配器模式 ==:Spring AOP 的增强或通知(Advice)==使⽤到了适配器模式、 spring MVC 中也是⽤到了适配器模式适配 Controller 。

Spring 事务

PlatformTransactionManager :是 spring 的事务管理器,它里面提供了我们常用的操作事务的方法 getTransaction commit rollback

Spring 管理事务的⽅式有⼏种?

编程式事务,在代码中硬编码。 (不推荐使⽤)
声明式事务,在配置⽂件中配置(推荐使⽤) (分为基于xml 和基于注解)

声明式作用:事务管理不侵入开发的组件 事务管理是属于系统层面的服务,而不是业务逻辑的一部分 在不需要事务管理的时候,只要在设定文件上修改一下,即可移去事务管理服务,无需改变代码重新编译,这样维护起来极其方便

注意: Spring 声明式事务控制底层就是AOP。

基于xml

 平台事务管理器配置
 事务通知的配置
 事务aop织入的配置

基于注解

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GyTlBd1C-1681383922731)(https://raw.githubusercontent.com/viacheung/img/main/image/image-20230317221224522.png)]

 平台事务管理器配置(xml方式)
 事务通知的配置(@Transactional注解配置)
 事务注解驱动的配置 tx:annotation-driven/

Spring 事务中的隔离级别有哪⼏种?

1、使⽤后端数据库默认的隔离级别

2、读未提交

3、读已提交

4、可重复读

5、串行化(将严重影响程序的性能 )

Spring框架的事务管理有哪些优点?

  • 它提供了跨不同事务api(如JTA、JDBC、Hibernate、JPA和JDO)的一致编程模型。
  • 它为编程事务管理提供了比JTA等许多复杂事务API更简单的API。
  • 它支持声明式事务管理。
  • 它很好地集成了Spring的各种数据访问抽象。

Spring 事务中哪⼏种事务传播⾏为?

⽀持当前事务的情况 :

如果当前存在事务 加入该事务,否则(1、创建⼀个新的事务 2、以⾮事务的⽅式继续运⾏ 3、抛出异常)

不⽀持当前事务 :

把当前事务挂起 然后:

1、创建⼀个新的事务 2、以⾮事务的⽅式继续运⾏ 3、抛出异常

spring事务不生效的场景

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YfjVVHfQ-1681383922732)(https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/7e92221a76ba45aca924d0ce87c3dea6~tplv-k3u1fbpfcp-zoom-in-crop-mark:1512:0:0:0.awebp)]

1、spring框架配置
的service类没有被Spring管理

@Service注解注释之后,spring事务(@Transactional)没有生效,因为Spring事务是由AOP机制实现的,也就是说从Spring IOC容器获取bean时,Spring会为目标类创建代理,来支持事务的。但是@Service被注释后,你的service类都不是spring管理的,那怎么创建代理类来支持事务呢

2、AOP代理
事务方法被final、static关键字修饰

如果一个方法被声明为final或者static,则该方法不能被子类重写,也就是说无法在该方法上进行动态代理,这会导致Spring无法生成事务代理对象来管理事务。

同一个类中,方法内部调用

事务不生效的原因: 事务是通过Spring AOP代理来实现的,而在同一个类中,一个方法调用另一个方法时,调用方法直接调用目标方法的代码,而不是通过代理类进行调用。即以上代码,调用目标executeAddTianLuo方法不是通过代理类进行的,因此事务不生效。

方法的访问权限不是public

spring事务方法addTianLuo的访问权限不是public,所以事务就不生效啦,因为Spring事务是由AOP机制实现的,AOP机制的本质就是动态代理,而代理的事务方法不是public的话,computeTransactionAttribute()就会返回null,也就是这时事务属性不存在了。

3、 数据库的存储引擎不支持事务

Spring事务的底层,还是依赖于数据库本身的事务支持。在MySQL中,MyISAM存储引擎是不支持事务的,InnoDB引擎才支持事务。因此开发阶段设计表的时候,确认你的选择的存储引擎是支持事务的

4、Transational配置问题
4.1 事务超时时间设置过短
@Transactional(timeout = 1)
public void doSomething() {
    //...
}
复制代码
  • 事务不生效的原因:在上面的例子中,timeout属性被设置为1秒,这意味着如果事务在1 秒内无法完成,则报事务超时了。
4.2 使用了错误的事务传播机制
@Service
public class TianLuoServiceImpl {
 
    @Autowired
    private TianLuoMapper tianLuoMapper;
    @Autowired
    private TianLuoFlowMapper tianLuoFlowMapper;
 
    @Transactional(propagation = Propagation.NOT_SUPPORTED)
    public  void doInsertTianluo(TianLuo tianluo) throws Exception {
        tianLuoMapper.save(tianluo);
        tianLuoFlowMapper.saveFlow(buildFlowByTianLuo(tianluo));
    }
}
复制代码
  • 事务不生效的原因Propagation.NOT_SUPPORTED传播特性不支持事务。

  • 解决方案:选择正确的事务传播机制。

5 事务多线程调用

事务失效原因:这是因为Spring事务是基于线程绑定的,每个线程都有自己的事务上下文而多线程环境下可能会存在多个线程共享同一个事务上下文的情况,导致事务失效。Spring事务管理器通过使用线程本地变量(ThreadLocal)来实现线程安全。大家有兴趣的话,可以去看下源码哈.

在Spring事务管理器中,通过TransactionSynchronizationManager类来管理事务上下文。TransactionSynchronizationManager内部维护了一个ThreadLocal对象,用来存储当前线程的事务上下文。在事务开始时,TransactionSynchronizationManager会将事务上下文绑定到当前线程的ThreadLocal对象中,当事务结束时,TransactionSynchronizationManager会将事务上下文从ThreadLocal对象中移除。

@Controller 注解有什么用?

标记一个类为 Spring Web MVC 控制器 Controller,springmvc扫描到有该注解的类,然后这个类有@RequestMapping注解的方法,为这个方法生成一个处理器对象

@RequestMapping 注解有什么用?

将请求和方法进行映射,可以作用于类也可以作用于方法,作用于类的话一般就是控制器URI前缀、

@RestController 和 @Controller 有什么区别?

@RestController =@Controller + @ResponseBody ,更加适合目前前后端分离的架构下,提供 Restful API ,返回例如 JSON 数据格式。

当然,返回什么样的数据格式,根据客户端的 ACCEPT 请求头来决定。

@RequestMapping 和 @GetMapping 注解的不同之处在哪里?

  1. @RequestMapping:可注解在类和方法上;@GetMapping 仅可注册在方法上

  2. @RequestMapping: GET、POST、PUT、DELETE 等请求方法都可以用

    @GetMapping@RequestMapping 的 GET 请求方法的特例,目的是为了提高区分度。

@RequestParam 和 @PathVariable 两个注解的区别

两个注解都用于方法参数,获取参数值的方式不同,@RequestParam 注解的参数从请求携带的参数中获取(请求头),而 @PathVariable 注解从请求的 URI 中(?后面的参数)

参数绑定注解@requestParam

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-So6mkgGr-1681383922733)(https://raw.githubusercontent.com/viacheung/img/main/image/image-20230317213825953.png)]

@RestController vs @Controller

@Controller 返回⼀个⻚⾯

只返回视图 属于⽐较传统的Spring MVC 的应⽤,对应于前后端不分离的情况

@RestController 返回JSON 或 XML 形式数据

只返回对象,以 JSON 或 XML 形式写⼊ Response中,(前后端分离)。

@Controller +@ResponseBody =@RestController

**@ResponseBody ** :Controller 的⽅法返回的对象通过适当的转换器转换为指定的格式(json xml)之后,写⼊到HTTP 响应(Response)对象的 body 中,

restful

 GET:用于获取资源
 POST:用于新建资源
 PUT:用于更新资源
 DELETE:用于删除资源

返回 JSON 格式使用什么注解?

@ResponseBody 注解,or @RestController(ResponseBody+Controller)

@Transactional(rollbackFor = Exception.class)注解了解吗?

当 @Transactional 注解作⽤于类上时,该类的所有 public ⽅法将都具有该类型的事务属性,如果类或者⽅法加了这个注解,那
么这个类⾥⾯的⽅法抛出异常,就会回滚,数据库⾥⾯的数据也会回滚。

如果不配置 rollbackFor 属性,那么事物只会在遇到 RuntimeException 的时候才会回滚,加上 rollbackFor=Exception.class ,可以让事物在遇到⾮运⾏时异常时也回滚。

注解开发

替代xml里面的配置 xml里面一个个都是容器 然后注入就是拿容器返回的值(getBean)

获取容器 ------也就是获取上下文

ApplicationContext ac = new ClassPathXmlApplicationContext("bean.xml");
IAccountService as = ac.getBean("accountService",IAccountService.class);  

什么是springmvc拦截器以及如何使用它?

Spring的处理程序映射机制包括处理程序拦截器,当你希望将特定功能应用于某些请求时,例如,检查用户主题时,这些拦截器非常有用。拦截器必须实现org.springframework.web.servlet包的HandlerInterceptor。此接口定义了三种方法:

  • preHandle:在执行实际处理程序之前调用。
  • postHandle:在执行完实际程序之后调用。
  • afterCompletion:在完成请求后调用。

Spring MVC 和 Struts2 的异同?

入口不同

  • Spring MVC 的入门是一个 Servlet 控制器
  • Struts2 入门是一个 Filter 过滤器

配置映射不同,

  • Spring MVC 是基于方法开发,传递参数是通过方法形参,一般设置为单例
  • Struts2 是基于开发,传递参数是通过类的属性,只能设计为多例

视图不同

  • Spring MVC 通过参数解析器是将 Request 对象内容进行解析成方法形参,将响应数据和页面封装成 ModelAndView 对象,最后又将模型数据通过 Request 对象传输到页面。其中,如果视图使用 JSP 时,默认使用 JSTL
  • Struts2 采用值栈存储请求和响应的数据,通过 OGNL 存取数据。

REST

REST 代表着什么?

REST 代表着抽象状态转移,它是根据 HTTP 协议从客户端发送数据到服务端,例如:服务端的一本书可以以 XML 或 JSON 格式传递到客户端

什么是安全的 REST 操作?

REST 接口是通过 HTTP 方法完成操作

  • GET 和 HEAD 安全,因为它不能在服务端修改资源
  • PUT、POST 和 DELETE 是不安全的,因为他们能修改服务端的资源

所以,是否安全的界限,在于是否修改服务端的资源

REST API 是无状态的吗?

是的,REST API 应该是无状态的,因为它是基于 HTTP 的,它也是无状态的

REST API 中的请求应该包含处理它所需的所有细节。它不应该依赖于以前或下一个请求或服务器端维护的一些数据,例如会话

REST 规范为使其无状态设置了一个约束,在设计 REST API 时,你应该记住这一点

REST安全吗? 你能做什么来保护它?

安全是一个宽泛的术语。它可能意味着消息的安全性,这是通过认证和授权提供的加密或访问限制提供的

REST 通常不是安全的,需要开发人员自己实现安全机制

补充

spring中出现异步调用的方式?

1、注解(我项目里面用的)

配置类上加上@EnableAsync启动异步调用,使用了@Async标记的异步方法,可以带参可以带返回值

返回值必须以下类型:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2oLwFZ4j-1681383922733)(https://raw.githubusercontent.com/viacheung/img/main/image/image-20230410171205483.png)]

2、内置线程池

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Ez6pyXni-1681383922734)(https://raw.githubusercontent.com/viacheung/img/main/image/image-20230410171626618.png)]

3、自定义线程池

MyBatis

MyBatis是什么?

1、Mybatis是一个半ORM(对象关系映射)框架,它内部封装了JDBC,加载驱动、创建连接、创建statement等繁杂的过程,开发者开发时只需要关注如何编写SQL语句,可以严格控制sql执行性能,灵活度高

2、作为一个半ORM框架,MyBatis 可以使用 XML 或注解来配置和映射原生信息,将 POJO映射成数据库中的记录,避免了几乎所有的 JDBC 代码和手动设置参数以及获取结果集。

3、通过xml 文件或注解的方式将要执行的各种 statement 配置起来,并通过java对象和 statement中sql的动态参数进行映射生成最终执行的sql语句,最后由mybatis框架执行sql并将结果映射为java对象并返回。(从执行sql到返回result的过程)。

4、由于MyBatis专注于SQL本身,灵活度高,所以比较适合对性能的要求很高,或者需求变化较多的项目,如互联网项目。

resultMap:反射创建的对象返回数据

Mybaits的优缺点

优点:

  • 基于SQL语句编程,相当灵活,不会对应用程序或者数据库的现有设计造成任何影响,SQL写在XML里,解除sql与程序代码的耦合,便于统一管理;提供XML标签,支持编写动态SQL语句,并可重用。
  • 与JDBC相比,减少大量代码量,消除了JDBC大量冗余的代码,不需要手动开关连接;
  • 与各种数据库兼容(因为MyBatis使用JDBC来连接数据库,所以只要JDBC支持的数据库MyBatis都支持)。 jdbc相当于接口 mysql类似于实现类
  • g很好的集成Spring;
  • 提供映射标签,支持对象与数据库的ORM字段关系映射;提供对象关系映射标签,支持对象关系组件维护。

缺点:

  • SQL语句的==编写工作量较大,==尤其当字段多、关联表多时,对开发人员编写SQL语句的功底有一定要求。
  • SQL语句依赖于数据库,导致数据库移植性差,不能随意更换数据库。

#{}和${}的区别是什么?

${} 是 Properties ⽂件中的变量占位符

#{} 是 sql 的参数占位符

  • #{}是占位符,预编译处理;${}是拼接符,字符串替换,没有预编译处理。
  • Mybatis在处理#{}时,#{}传入参数是以字符串传入,会将SQL中的#{}替换为?号,调用PreparedStatement的set方法来赋值。
  • 变量替换后,#{} 对应的变量自动加上单引号 ‘’;变量替换后,${} 对应的变量不会加上单引号 ‘’
  • #{} 可以有效的防止SQL注入,提高系统安全性;${} 不能防止SQL 注入
  • #{} 的变量替换是在DBMS 中;${} 的变量替换是在 DBMS 外

通常一个Xml映射文件,都会写一个Dao接口与之对应,那么这个Dao接口的工作原理是什么?Dao接口里的方法、参数不同时,方法能重载吗?

Dao接口即Mapper接口。接口的全限名就是映射文件中的namespace的值;接口的方法名,就是映射文件中Mapper的Statement的id值;接口方法内的参数,就是传递给sql的参数。Mapper接口是没有实现类的,当调用接口方法时,接口全限名+方法名的拼接字符串作为key值,可唯一定位一个MapperStatement。

Dao接口里的方法,是不能重载的,因为是全限名+方法名的保存和寻找策略。

Dao接口的工作原理是JDK动态代理,Mybatis运行时会使用JDK动态代理为Dao接口生成代理proxy对象,代理对象proxy会拦截接口方法,转而执行MappedStatement所代表的sql,然后将sql执行结果返回。

Xml 映射⽂件中有哪些标签?

、 、 、 、 、、、、

Mapper接⼝的⼯作原理是什么?

例: com.mybatis3.mappers.StudentDao.findStudentById ,可以唯⼀找到 namespace(接口全限定名)为 com.mybatis3.mappers.StudentDao 下⾯ id = findStudentById 的 MappedStatement (接⼝⽅法内的参数,就是传递给 sql 的参数。 )

在 Mybatis中,每⼀个 、 、 、 标签,都会被解析为⼀个 MappedStatement 对象

Dao 接⼝的⼯作原理是 JDK 动态代理Mybatis 运⾏时会使⽤ JDK 动态代理为 Dao 接⼝⽣成代
理 proxy 对象代理对象 proxy 会拦截接⼝⽅法,转⽽执⾏ MappedStatement 所代表的 sql,然后
将 sql 执⾏结果返回。

在Mapper中如何传递多个参数?

1、若Dao层函数有多个参数,那么其对应的xml中,#{0}代表接收的是Dao层中的第一个参数,#{1}代表Dao中的第二个参数,以此类推。

2、使用@Param注解:在Dao层的参数中前加@Param注解,注解内的参数名为传递到Mapper中的参数名。

3、多个参数封装成Map,以HashMap的形式传递到Mapper中。

Dao (Mapper)接⼝⾥的⽅法,参数不同时,⽅法能重载吗?

Dao 接⼝⾥的⽅法,是不能重载的,因为是全限名+⽅法名的保存和寻找策略

Mybatisplus 是如何进⾏分⻚的?分⻚插件的原理是什么?

简述 Mybatis 的插件运⾏原理,以及如何编写⼀个插件

使用基于jdk的动态代理,为需要拦截的接口生成代理对象以实现接口方法拦截功能,

当执行这4种接口方法时,会进入拦截方法,invoke()方法

如何编写?

实现 Mybatis 的 Interceptor 接⼝并复写 intercept() ⽅法,然后在给插件编写注解,指定要拦截哪⼀个接⼝的哪些⽅法即可,记住,别忘了在配置⽂件中配置你编写的插件。

Mybatis 执⾏批量插⼊,能返回数据库主键列表吗?

可以

Mybatis 动态 sql 是做什么的?都有哪些动态 sql?能简述⼀下动态 sql 的执⾏原理不?

Mybatis 动态 sql 可以让我们在 Xml 映射⽂件内,以标签的形式编写动态 sql,完成逻辑判断
和动态拼接 sql 的功能, Mybatis 提供了 9 种动态 sql 标签
trim|where|set|foreach|if|choose|when|otherwise|bind 。
其执⾏原理为,使⽤ OGNL 从 sql 参数对象中计算表达式的值,根据表达式的值动态拼接 sql,以此来完成动态 sql 的功能。

执行原理:是根据表达式的值完成逻辑判断,并动态拼接sql的功能。

Mybatis 是如何将 sql 执⾏结果封装为⽬标对象并返回的?都有哪些映射形式?

第⼀种:是使⽤ 标签,逐⼀定义列名和对象属性名之间的映射关系。

第⼆种:使⽤ sql 列的别名功能,将列别名书写为对象属性名,⽐如 T_NAME AS NAME,

有了列名与属性名的映射关系后, Mybatis 通过反射创建对象,同时使⽤反射给对象的属性逐⼀赋值并返回,那些找不到映射关系的属性,是⽆法完成赋值的

Mybatis 能执⾏⼀对⼀、⼀对多的关联查询吗?都有哪些实现⽅式,以及它们之间的区别。

可以,把 selectOne() 修改为 selectList() 即可

关联对象查询,有两种实现⽅式,一种是两条sql 一种是嵌套(join) 具体如下:

有联合查询和嵌套查询两种方式。
联合查询是几个表联合查询,通过在resultMap里面配置association节点配置一对一的类就可以完成;
嵌套查询是先查一个表,根据这个表里面的结果的外键id,再去另外一个表里面查询数据,也是通过association配置,但另外一个表的查询是通过select配置的。

去重复的原理是 标签内的 ⼦标签

Mybatis的一级、二级缓存(?)

1、 一级缓存:基于PerpetualCache的HashMap本地缓存,其存储作用域为Session,当Session flush或close之后,该Session中的所有Cache就将清空,默认打开一级缓存。

2、 二级缓存与一级缓存机制相同,默认也是采用PerpetualCache,HashMap存储,不同在于其存储作用域为Mapper(namespace),并且可自定义存储源,如Ehcache。默认打不开二级缓存,要开启二级缓存,使用二级缓存属性类需要实现Serializable序列化接口(可用来保存对象的状态),可在它的映射文件中配置。

对于缓存数据更新机制,当某一个作用域(一级缓存Session/二级缓存Namespace)进行了增/删/改操作后,默认该作用域下所有select中的缓存将被clear。

使用MyBatis的Mapper接口调用时有哪些要求?

1、Mapper接口方法名和mapper.xml中定义的每个sql的id相同;

2、Mapper接口方法的输入参数类型和mapper.xml中定义的每个sql的parameterType类型相同;

3、Mapper接口方法的输出参数类型和mapper.xml中定义的每个sql的resultType的类型相同;

4、Mapper.xml文件中的namespace即是mapper接口的类路径

Mybatis 是否⽀持延迟加载(懒加载)?如果⽀持,它的实现原理是什么?

使⽤ CGLIB 创建⽬标对象的代理对象,当调⽤⽬标⽅法时,进⼊拦截器⽅法,⽐
如调⽤ a.getB().getName() ,拦截器 invoke() ⽅法发现 a.getB() 是 null 值,那么就会单独发送事先保存好的查询关联 B 对象的 sql,把 B 查询上来,然后调⽤ a.setB(b),于是 a 的对象 b 属性就有值了,接着完成 a.getB().getName() ⽅法的调⽤。这就是延迟加载的基本原理。

意思就是set我先不做,等你get的时候我再带调用set 然后再get,比较懒

Mybatis 的 Xml 映射⽂件中,不同的 Xml 映射⽂件, id 是否可以重复?

如果配置了 namespace,那么 id 可以重复;如果没有配置namespace,那么 id 不能重复;

原因就是 namespace+id 是作为 Map 的 key 使⽤的

Mybatis 中如何执⾏批处理?

BatchExecutor 完成批处理。

ctional(rollbackFor = Exception.class)注解了解吗?

当 @Transactional 注解作⽤于类上时,该类的所有 public ⽅法将都具有该类型的事务属性,如果类或者⽅法加了这个注解,那
么这个类⾥⾯的⽅法抛出异常,就会回滚,数据库⾥⾯的数据也会回滚。

如果不配置 rollbackFor 属性,那么事物只会在遇到 RuntimeException 的时候才会回滚,加上 rollbackFor=Exception.class ,可以让事物在遇到⾮运⾏时异常时也回滚。

注解开发

替代xml里面的配置 xml里面一个个都是容器 然后注入就是拿容器返回的值(getBean)

获取容器 ------也就是获取上下文

ApplicationContext ac = new ClassPathXmlApplicationContext("bean.xml");
IAccountService as = ac.getBean("accountService",IAccountService.class);  

什么是springmvc拦截器以及如何使用它?

Spring的处理程序映射机制包括处理程序拦截器,当你希望将特定功能应用于某些请求时,例如,检查用户主题时,这些拦截器非常有用。拦截器必须实现org.springframework.web.servlet包的HandlerInterceptor。此接口定义了三种方法:

  • preHandle:在执行实际处理程序之前调用。
  • postHandle:在执行完实际程序之后调用。
  • afterCompletion:在完成请求后调用。

Spring MVC 和 Struts2 的异同?

入口不同

  • Spring MVC 的入门是一个 Servlet 控制器
  • Struts2 入门是一个 Filter 过滤器

配置映射不同,

  • Spring MVC 是基于方法开发,传递参数是通过方法形参,一般设置为单例
  • Struts2 是基于开发,传递参数是通过类的属性,只能设计为多例

视图不同

  • Spring MVC 通过参数解析器是将 Request 对象内容进行解析成方法形参,将响应数据和页面封装成 ModelAndView 对象,最后又将模型数据通过 Request 对象传输到页面。其中,如果视图使用 JSP 时,默认使用 JSTL
  • Struts2 采用值栈存储请求和响应的数据,通过 OGNL 存取数据。

REST

REST 代表着什么?

REST 代表着抽象状态转移,它是根据 HTTP 协议从客户端发送数据到服务端,例如:服务端的一本书可以以 XML 或 JSON 格式传递到客户端

什么是安全的 REST 操作?

REST 接口是通过 HTTP 方法完成操作

  • GET 和 HEAD 安全,因为它不能在服务端修改资源
  • PUT、POST 和 DELETE 是不安全的,因为他们能修改服务端的资源

所以,是否安全的界限,在于是否修改服务端的资源

REST API 是无状态的吗?

是的,REST API 应该是无状态的,因为它是基于 HTTP 的,它也是无状态的

REST API 中的请求应该包含处理它所需的所有细节。它不应该依赖于以前或下一个请求或服务器端维护的一些数据,例如会话

REST 规范为使其无状态设置了一个约束,在设计 REST API 时,你应该记住这一点

REST安全吗? 你能做什么来保护它?

安全是一个宽泛的术语。它可能意味着消息的安全性,这是通过认证和授权提供的加密或访问限制提供的

REST 通常不是安全的,需要开发人员自己实现安全机制

补充

spring中出现异步调用的方式?

1、注解(我项目里面用的)

配置类上加上@EnableAsync启动异步调用,使用了@Async标记的异步方法,可以带参可以带返回值

返回值必须以下类型:

[外链图片转存中…(img-2oLwFZ4j-1681383922733)]

2、内置线程池

[外链图片转存中…(img-Ez6pyXni-1681383922734)]

3、自定义线程池

MyBatis

MyBatis是什么?

1、Mybatis是一个半ORM(对象关系映射)框架,它内部封装了JDBC,加载驱动、创建连接、创建statement等繁杂的过程,开发者开发时只需要关注如何编写SQL语句,可以严格控制sql执行性能,灵活度高

2、作为一个半ORM框架,MyBatis 可以使用 XML 或注解来配置和映射原

你可能感兴趣的:(java)