代办
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)]
该项目是一个致力于魔方爱好者之间交流、分享经验的在线平台,主要功能包括用户注册和登录、魔方教程的
展示分享、魔方论坛互动、比赛和活动组织、用户管理等。
以下是该项目的主要特点和功能:
以上就是我项目的主要内容
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RI90XXJc-1681383922608)(https://raw.githubusercontent.com/viacheung/img/main/image/image-20230322005426563.png)]
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)]
用户使用用户名密码来请求服务器
服务器进行验证用户的信息
3.服务器通过验证,发送给用户一个token(header.payload(id 电话号码啥的).签证)
4.客户端存储token,并在每次请求时附送上这个token值(Authorization里面)
服务端验证token值,并返回数据,从中也可获取用户相关信息
jwt token生成和校验
使用私钥加密生成token 公钥解密获取token中的信息
删除redis里面的token即可
然后呢,JWT是个啥?其实就是把用户的用户信息,用密钥加密防篡改,然后放在请求头里。用户请求的时候呢,再解密。服务器就不需要保存用户的信息了。简单来说,这个加密之后的信息写的是啥,服务器就认为用户是啥。
美其名曰,无状态,不需要消耗服务器的存储,减轻服务器压力。但是反过来,却带来了无法注销,请求头体积大,加解密效率等其他问题。然后为了解决这些问题,把redis的那一套解决方案,再引过来,两种方式结合在一起。。。讲道理,我吐了。强行混着来,jwt的意义何在呢?
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。
1、采用更安全的传输协议https
2、加密传输
3、代码层面也可以做安全检测,比如ip地址发生变化,MAC地址发生变化等等,可以要求重新登录
4、使用私钥加密生成token 公钥解密获取token中的信息
1)系统不能把jwt作为唯一的身份识别条件,不然被别人拿到了jwt就相当于获得了所有相关账户的权限,但对于这一点我还在学习中。2)存储jwt、传输的方式应该再加强。
private static final String slat = "mszlu!@#";//加密盐 因为数据库不能给人看密码 每次都用这一个字符串用来加密
我的项目是token不过期、redis记录过期
https://juejin.cn/post/7126708538440679460
直接把token和存入redis里,验证有无token即可。踢人下线直接清除redis中的token。和笔者的思路是类似的。
目的
方便鉴权,查询是否在线,减少数据库读操作
token:sysUser
不想用session(服务器存储 分布式)
文章发布 需要用户id,直接拿,评论也是一样,方便
request获取? 但是从设计上、代码分层上来说,
并发问题
降低redis使用?
redis中可以获取用户信息,但是因为redis中的key是token,要先拿到token才能拿到用户信息,但是token不是每个类中都存在,想在每个类都获取到用户信息
一般我们需要获取当前用户信息,放到缓存?每次解析token,然后传递?我们可以使用ThreadLocal来解决。将用户信息保存在线程中,当请求结束后我们在把保存的信息清除掉。这样我们才开发的时候就可以直接从全局的ThreadLocal中很方便的获取用户信息。当前线程在任何地方需要时,都可以使用
步骤
preHandle()
(ThreadLocal.put())和afterCompletion()
(不用了手动remove)方法。Thread
类中,有个ThreadLocal.ThreadLocalMap
的成员变量。ThreadLocalMap
内部维护了Entry
数组,每个Entry
代表一个完整的对象,key
是ThreadLocal
本身,value
是ThreadLocal
的泛型对象值
并发多线程场景下,每个线程Thread
,在往ThreadLocal
里设置值的时候,都是往自己的ThreadLocalMap
里存,读也是以某个ThreadLocal
作为引用,在自己的map
里找对应的key
,从而可以实现了线程隔离。
用了两个ThreadLocal
成员变量的话。如果用线程id
作为ThreadLocalMap
的key
,怎么区分哪个ThreadLocal
成员变量呢?因此还是需要使用ThreadLocal
作为Key
来使用。每个ThreadLocal
对象,都可以由threadLocalHashCode
属性唯一区分的,每一个ThreadLocal对象都可以由这个对象的名字唯一区分
ThreadLocalMap
使用ThreadLocal
的弱引用作为key
,当ThreadLocal
变量被手动设置为null
,即一个ThreadLocal
没有外部强引用来引用它,当系统GC时,ThreadLocal
一定会被回收。这样的话,ThreadLocalMap
中就会出现key
为null
的Entry
,就没有办法访问这些key
为null
的Entry
的value
,如果当前线程再迟迟不结束的话(比如线程池的核心线程),这些key
为null
的Entry
的value
就会一直存在一条强引用链:Thread变量 -> Thread对象 -> ThreaLocalMap -> Entry -> value -> Object 永远无法回收,造成内存泄漏。
实际上,ThreadLocalMap
的设计中已经考虑到这种情况。所以也加上了一些防护措施:即在ThreadLocal
的get
,set
,remove
方法,都会清除线程ThreadLocalMap
里所有key
为null
的value
。
不会的,因为有ThreadLocal变量
引用着它,是不会被GC回收的,除非手动把ThreadLocal变量设置为null
用线程池,一直往里面放对象
因为我们使用了线程池,线程池有很长的生命周期,因此线程池会一直持有tianLuoClass
(ThreadLocal泛型值)对象的value
值,即使设置tianLuoClass = null;
引用还是存在的
当ThreadLocal
的对象被回收了,因为ThreadLocalMap
持有ThreadLocal的弱引用,即使没有手动remove删除,ThreadLocal也会被回收。value
则在下一次ThreadLocalMap
调用set,get,remove
的时候会被清除。
在子线程中,是可以获取到父线程的 InheritableThreadLocal 类型变量的值,但是不能获取到 ThreadLocal 类型变量的值(因为ThreadLocal
是线程隔离)。
在Thread
类中,除了成员变量threadLocals
之外,还有另一个成员变量:inheritableThreadLocals
。
当parent的inheritableThreadLocals
不为null
时,就会将parent
的inheritableThreadLocals
,赋值给前线程的inheritableThreadLocals
。说白了,就是如果当前线程的inheritableThreadLocals
不为null
,就从父线程哪里拷贝过来一个过来,类似于另外一个ThreadLocal
,但是数据从父线程那里来的。有兴趣的小伙伴们可以在去研究研究源码~
ThreadLocal
的很重要一个注意点,就是使用完,要手动调用remove()
。
而ThreadLocal
的应用场景主要有以下这几种:
SimpleDateFormat
,使用ThreadLocal保证线性安全ThreadLocal
,那么当前线程在任何地方需要时,都可以使用)Connection
是同一个,使用ThreadLocal
来解决线程安全的问题MDC
保存日志信息。线程并发:在多线程并发场景下使用
**传递数据:**可以通过ThreadLocal在同一线程,不同组件中传递公共变量(保存每个线程的数据,在需要的地方可以直接获取, 避免参数直接传递带来的代码耦合问题)
线程隔离:每个线程的变量都是独立的, 不会互相影响
ThreadLocal模式与Synchronized关键字都用于处理多线程并发访问变量的问题
ThreadLocal:以空间换取时间的思想, 为每一个线程都提供了一份变量的副本, 从而实现同访问而 互相不干扰。 多线程中让每个线程之间的数据相互隔离
Synchronized:以时间换取空间的思想,只提供了一份变量, 让不同的线程排队访问。多个线程之间访问资源的同步
子线程访问父线程的共享变量时候,是“引用传递”,多个子线程访问的话所以线程不安全
线性探测法顾名思义,就是解决冲突的函数是一个线性函数,最直接的就是在TreadLocal的代码中也用的是这样一个解决冲突的函数。
f(x)= x+1
但是要注意的是TreadLocal中,是一个环状的探测,如果到达边界就会直接跨越边界到另一头去。
线性探测法的优点:
缺点:
之前我们说过,线性探测法有个问题是,一旦发生碰撞,很可能之后每次都会产生碰撞,导致连环撞车。而使用0x61c88647这个值做一个hash的增长值就可以从一定程度上解决这个问题让生成出来的值较为均匀地分布在2的幂大小的数组中。也就是说当我们用0x61c88647作为步长累加为每个ThreadLocal分配各自的ID也就是threadLocalHashCode再与2的幂取模,得到的结果分布很均匀。
0x61c88647选取其实是与斐波那契散列有关,这个就是数学知识了,这里不展开。
不能让记录日志出现失误影响用户的登录
为了出现错误时候的排查
记录日志录入数据库时,脱离主线程,实现异步插入,这样不会拖延主线程的执行时间
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()
方法用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功与否;get(long timeout,TimeUnit unit)
方法则会阻塞当前线程一段时间后立即返回,这时候有可能任务没有执行完。资源消耗–重复利用 响应速度—立即执行 可管理性—线程池统一分配
1、构造⽅法
2、通过 Executor 框架的⼯具类 Executors 来实现
三种ThreadPoolExecutor:
fixedThreadPool() 固定线程数
SingleThreadExecutor 只有一个线程
CachedThreadPool 根据情况调整 数量不固定
corePoolSize : 核心线程大小。线程池一直运行,核心线程就不会停止。
maximumPoolSize :线程池最大线程数量。非核心线程数量=maximumPoolSize-corePoolSize
keepAliveTime :非核心线程的心跳时间。如果非核心线程在keepAliveTime内没有运行任务,非核心线程会消亡。
workQueue :阻塞队列。ArrayBlockingQueue,LinkedBlockingQueue等,用来存放线程任务。
defaultHandler :饱和策略。ThreadPoolExecutor类中一共有4种饱和策略。通过实现
RejectedExecutionHandler
接口。
饱和策略
ThreadFactory :线程工厂。新建线程工厂。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JL9eoruj-1681383922612)(https://raw.githubusercontent.com/viacheung/img/main/image/1460000039258685)]
1、newCachedThreadPool
创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
这种类型的线程池特点是:
工作线程的创建数量几乎没有限制(其实也有限制的,数目为Interger. MAX_VALUE), 这样可灵活的往线程池中添加线程。
不足:这种方式虽然可以根据业务场景自动的扩展线程数来处理我们的业务,但是最多需要多少个线程同时处理缺是我们无法控制的;
优点:如果当第二个任务开始,第一个任务已经执行结束,那么第二个任务会复用第一个任务创建的线程,并不会重新创建新的线程,提高了线程的复用率;
2、newFixedThreadPool
创建一个指定工作线程数量的线程池。每当提交一个任务就创建一个工作线程,如果工作线程数量达到线程池初始的最大数,则将提交的任务存入到池队列中。
**缺点:**它具有线程池提高程序效率和节省创建线程时所耗的开销的优点。但是,在线程池空闲时,即线程池中没有可运行任务时,它不会释放工作线程,还会占用一定的系统资源。
优点:newFixedThreadPool的线程数是可以进行控制的,因此我们可以通过控制最大线程来使我们的服务器打到最大的使用率,同事又可以保证及时流量突然增大也不会占用服务器过多的资源。
3、newSingleThreadExecutor
创建一个单线程化的Executor,只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。如果这个线程异常结束,会有另一个取代它,保证顺序执行。单工作线程最大的特点是可保证顺序地执行各个任务,并且在任意给定的时间不会有多个线程是活动的。
4、newScheduleThreadPool
创建一个定长的线程池,而且支持定时的以及周期性的任务执行,支持定时及周期性任务执行。
表格左侧是线程池,右侧为它们对应的阻塞队列,可以看到 5 种线程池对应了 3 种阻塞队列
LinkedBlockingQueue 对于 FixedThreadPool 和 SingleThreadExector 而言,它们使用的阻塞队列是容量为 Integer.MAX_VALUE 的 LinkedBlockingQueue,可以认为是无界队列。由于 FixedThreadPool 线程池的线程数是固定的,所以没有办法增加特别多的线程来处理任务,因此需要这样一个没有容量限制的阻塞队列来存放任务。
由于线程池的任务队列永远不会放满,所以线程池只会创建核心线程数量的线程,所以此时的最大线程数对线程池来说没有意义,因为并不会触发生成多于核心线程数的线程。
SynchronousQueue 第二种阻塞队列是 SynchronousQueue,对应的线程池是 CachedThreadPool。线程池 CachedThreadPool 的最大线程数是 Integer 的最大值,可以理解为线程数是可以无限扩展的。CachedThreadPool 和上一种线程池 FixedThreadPool 的情况恰恰相反,FixedThreadPool 的情况是阻塞队列的容量是无限的,而这里 CachedThreadPool 是线程数可以无限扩展,所以 CachedThreadPool 线程池并不需要一个任务队列来存储任务,因为一旦有任务被提交就直接转发给线程或者创建新线程来执行,而不需要另外保存它们。 我们自己创建使用 SynchronousQueue 的线程池时,如果不希望任务被拒绝,那么就需要注意设置最大线程数要尽可能大一些,以免发生任务数大于最大线程数时,没办法把任务放到队列中也没有足够线程来执行任务的情况。
DelayedWorkQueue 第三种阻塞队列是DelayedWorkQueue,它对应的线程池分别是 ScheduledThreadPool 和 SingleThreadScheduledExecutor,这两种线程池的最大特点就是可以延迟执行任务,比如说一定时间后执行任务或是每隔一定的时间执行一次任务。
DelayedWorkQueue 的特点是内部元素并不是按照放入的时间排序,而是会按照延迟的时间长短对任务进行排序,内部采用的是“堆”的数据结构。之所以线程池 ScheduledThreadPool 和 SingleThreadScheduledExecutor 选择 DelayedWorkQueue,是因为它们本身正是基于时间执行任务的,而延迟队列正好可以把任务按时间进行排序,方便任务的执行。
源码中ThreadPoolExecutor中有个内置对象Worker,每个worker都是一个线程,worker线程数量和参数有关,每个worker会while死循环从阻塞队列中取数据,通过置换worker中Runnable对象,运行其run方法起到线程置换的效果,这样做的好处是避免多线程频繁线程切换,提高程序运行性能。
选择的关键点是:
这种场景适合线程尽量少,因为如果线程太多,任务执行时间段很快就执行完了,有可能出现线程切换和管理多耗费的时间,大于任务执行的时间,这样效率就低了。线程池线程数可以设置为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核数
Executors :按照需求创建了不同的线程池,来满足业务的需求。
Executor :执行线程任务 获得任务执行的状态并且可以获取任务的返回值。
使用ThreadPoolExecutor 可以创建自定义线程池。Future 表示异步计算的结果,他提供了检查计算是否完成的方法,以等待计算的完成,并可以使用get()方法获取计算的结果
1、异步发送邮件通知
发送一个任务,然后注入到线程池中异步发送。
2、心跳请求任务
创建一个任务,然后定时发送请求到线程池中。
3、如果用户量比较大,导致占用过多的资源,可能会导致我们的服务由于资源不足而宕机;
3.1 线程池中线程的使用率提升,减少对象的创建、销毁;
3.2 线程池可以控制线程数,有效的提升服务器的使用资源,避免由于资源不足而发生宕机等问题;
1、导包 做配置
2、配置Document
先要配置实体和ES的映射,通过在实体类中加入注解的方式来自动映射跟索引,我这里是配置了product
索引和实体的映射
3、使用ElasticsearchRestTemplate
在Spring启动的时候自动注入了该Bean,它封装了操作Elasticsearch
的增删改查API
完全匹配查询条件 --> 按照阅读量排序—>高亮显示—>分页
1s 为什么要延长 es主要业务写日志
查看完文章了,新增阅读数,做了一个更新操作,更新时加写锁,阻塞其他的读操作,性能就会比较低(没办法解决,增加阅读数必然要加锁)
更新增加了此次接口的耗时(考虑减少耗时)如果一旦更新出问题,不能影响查看操作
线程池 可以把更新操作扔到 线程池中去执行和主线程就不相关了
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)先淘汰缓存
(2)再写数据库
(3)休眠1秒,再次淘汰缓存,这么做,可以将1秒内所造成的缓存脏数据,再次删除。确保读请求结束,写请求可以删除读请求造成的缓存脏数据。自行评估自己的项目的读数据业务逻辑的耗时,写数据的休眠时间则在读数据业务逻辑的耗时基础上,加几百ms即可。
(我的理解:请求A先删缓存再往DB写数据,就算这时B来查数据库,缓存没数据,然后查DB,此时查到的是旧数据,写到缓存,A等待B写完之和再删缓存,这样就缓存一致)
如果使用的是 Mysql 的读写分离的架构的话,那么其实主从同步之间也会有时间差。
此时来了两个请求,请求 A(更新操作) 和请求 B(查询操作)
此时的解决办法就是如果是对 Redis 进行填充数据的查询数据库操作,那么就强制将其指向主库进行查询。
采用更新与读取操作进行异步串行化
异步串行化
我在系统内部维护n个内存队列,更新数据的时候,根据数据的唯一标识,将该操作路由之后,发送到其中一个jvm内部的内存队列中(对同一数据的请求发送到同一个队列)。读取数据的时候,如果发现数据不在缓存中,并且此时队列里有更新库存的操作,那么将重新读取数据+更新缓存的操作,根据唯一标识路由之后,也将发送到同一个jvm内部的内存队列中。然后每个队列对应一个工作线程,每个工作线程串行地拿到对应的操作,然后一条一条的执行。
这样的话,一个数据变更的操作,先执行删除缓存,然后再去更新数据库,但是还没完成更新的时候,如果此时一个读请求过来,读到了空的缓存,那么可以先将缓存更新的请求发送到队列中,此时会在队列中积压,排在刚才更新库的操作之后,然后同步等待缓存更新完成,再读库。
读操作去重
多个读库更新缓存的请求串在同一个队列中是没意义的,因此可以做过滤,如果发现队列中已经有了该数据的更新缓存的请求了,那么就不用再放进去了,直接等待前面的更新操作请求完成即可,待那个队列对应的工作线程完成了上一个操作(数据库的修改)之后,才会去执行下一个操作(读库更新缓存),此时会从数据库中读取最新的值,然后写入缓存中。
如果请求还在等待时间范围内,不断轮询发现可以取到值了,那么就直接返回;如果请求等待的时间超过一定时长,那么这一次直接从数据库中读取当前的旧值。(返回旧值不是又导致缓存和数据库不一致了么?那至少可以减少这个情况发生,因为等待超时也不是每次都是,几率很小吧。这里我想的是,如果超时了就直接读旧值,这时候仅仅是读库后返回而不放缓存)
这一种情况也会出现问题,比如更新数据库成功了,但是在删除缓存的阶段出错了没有删除成功,那么此时再读取缓存的时候每次都是错误的数据了。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ygWZDQKt-1681383922614)(https://raw.githubusercontent.com/viacheung/img/main/image/1735bb5881fb4a1b~tplv-t2oaga2asx-watermark.awebp)]
此时解决方案就是利用消息队列进行删除的补偿。具体的业务逻辑用语言描述如下:
但是这个方案会有一个缺点就是会对业务代码造成大量的侵入,深深的耦合在一起,所以这时会有一个优化的方案,我们知道对 Mysql 数据库更新操作后在binlog 日志中我们都能够找到相应的操作,那么我们可以订阅 Mysql 数据库的 binlog 日志对缓存进行操作。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3LQvY5Dt-1681383922616)(https://raw.githubusercontent.com/viacheung/img/main/image/1735bb588215b298~tplv-t2oaga2asx-watermark.awebp)]
主要是增删改帖子
这里用的Direct队列
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调用不同的服务,获取服务方传输的数据,然后进行数据的同步。
实现消息的延迟投递,避免消息丢失或无限制的重试
当你在消费消息时,如果队列里的消息出现以下情况
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,消费者消费业务队列的消息,由于处理过程中发生异常,于是进行了nck或者reject操作
3,被nck或reject的消息由RabbitMQ投递到死信交换机中
4,死信交换机将消息投入相应的死信队列
5,死信队列的消费者消费死信消息
———————————————
生产者将数据发送到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机制的。
数据持久化:rabbitmq自己挂了,恢复之后会自动读取之前存储的数据,一般数据不会丢。除非极其罕见的是,rabbitmq还没持久化,自己就挂了,可能导致少量数据会丢失的,但是这个概率较小。
使用:跟生产者那边的confirm机制配合起来,只有消息被持久化到磁盘之后,才会通知生产者ack了,所以哪怕是在持久化到磁盘之前,rabbitmq挂了,数据丢了,生产者收不到ack,你也是可以自己重发的。
步骤: 第一个是创建queue的时候将其设置为持久化,这样就可以保证rabbitmq持久化queue的元数据。
第二个是发送消息的时候将消息的deliveryMode设置为2,就是将消息设置为持久化的
这样abbitmq哪怕是挂了,再次重启,也会从磁盘上重启恢复queue,恢复这个queue里的数据。
主要是因为你消费的时候,刚消费到,还没处理,结果进程挂了比如重启了,那么就尴尬了,RabbitMQ认为你都消费了,这数据就丢了。或者消费者拿到数据之后挂了,这时候需要MQ重新指派另一个消费者去执行任务
这个时候得用RabbitMQ提供的ack机制,也是一种处理完成发送回执确认的机制。如果MQ等待一段时间后你没有发送过来处理完成 那么RabbitMQ就认为你还没处理完,这个时候RabbitMQ会把这个消费分配给别的consumer去处理,消息是不会丢的。
https://segmentfault.com/a/1190000019125512
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HZDHh4XB-1681383922618)(https://raw.githubusercontent.com/viacheung/img/main/image/image-20230325004307581.png)]
使用MQ的场景很多,主要有三个:解耦、异步、削峰。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4vk2gmra-1681383922619)(https://raw.githubusercontent.com/viacheung/img/main/image/727602-20200108091722601-747710174.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Bq0Xl4Fe-1681383922620)(https://raw.githubusercontent.com/viacheung/img/main/image/727602-20200108091915241-1598228624.png)]
后面请求积压在MQ里面 不过是短暂的
1、 系统可用性降低
系统引入的外部依赖越多,越容易挂掉。
2、 系统复杂度提高
加入了消息队列,要多考虑很多方面的问题,比如:一致性问题、如何保证消息不被重复消费、如何保证消息可靠性传输等。因此,需要考虑的东西更多,复杂性增大。
3、 一致性问题
A 系统处理完了直接返回成功了,人都以为你这个请求就成功了;但是问题是,要是 BCD 三个系统那里,BD 两个系统写库成功了,结果 C 系统写库失败了,这就数据不一致了。
Producer
先连接到Broker,建立连接Connection,开启一个信道(Channel)。Producer
声明一个交换器并设置好相关属性。Producer
声明一个队列并设置好相关属性。Producer
通过路由键将交换器和队列绑定起来。Producer
发送消息到Broker
,其中包含路由键、交换器等信息。Consumer
先连接到Broker
,建立连接Connection
,开启一个信道(Channel
)。Broker
请求消费响应的队列中消息,可能会设置响应的回调函数。Broker
回应并投递相应队列中的消息,接收消息。ack
。RabbitMq
从队列中删除已经确定的消息。Producer
发送消息给MQProducer
,此处有可能因为网络问题导致Ack消息无法发送到Producer
,那么Producer
在等待超时后,会重传消息;Producer
收到Ack消息后,认为消息已经投递成功Consumer
(或Consumer
来pull消息)Consumer
得到消息并做完业务逻辑Consumer
发送Ack消息给MQ,通知MQ删除该消息,此处有可能因为网络问题导致Ack失败,那么Consumer
会重复消息,这里就引出消费幂等的问题;消息幂等
根据业务特性,选取业务中唯一的某个属性,比如订单号作为区分消息是否重复的属性。在进行插入订单之前,先从数据库查询一下该订单号的数据是否存在,如果存在说明是重复消费,如果不存在则插入。伪代码如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ODbMiyUn-1681383922622)(https://raw.githubusercontent.com/viacheung/img/main/image/image-20230322145324052.png)]
TINYTEXT(255长度)
TEXT(65535)
MEDIUMTEXT(int最大值16M)
LONGTEXT(long最大值4G)
Docker如何解决大型项目依赖关系复杂,不同组件依赖的兼容性问题?
Docker如何解决开发、测试、生产环境有差异的问题?
Docker是一个快速交付应用、运行应用的技术,具备下列优势:
Docker和虚拟机的差异:
镜像:
容器:
Docker结构:
服务端:接收命令或远程请求,操作镜像或容器
客户端:发送命令或者请求到Docker服务端
DockerHub:
docker run命令的常见参数有哪些?
查看容器日志的命令:
查看容器状态:
数据卷的作用:
数据卷操作:
docker run的命令中通过 -v 参数挂载文件或目录到容器中:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-aErY5ZA0-1681383922623)(https://raw.githubusercontent.com/viacheung/img/main/image/image-20230403002812590.png)]
数据卷挂载与目录直接挂载的
Docker Compose可以基于Compose文件帮我们快速的部署分布式应用,而无需手动一个个创建和运行容器!
反射: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
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)]
用户使用用户名密码来请求服务器
服务器进行验证用户的信息
3.服务器通过验证,发送给用户一个token(header.payload(id 电话号码啥的).签证)
4.客户端存储token,并在每次请求时附送上这个token值
服务端验证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.发布文章 目的 构建Article对象
2.作者id 当前的 登录用户
3.标签要将标签加入到关联列表当中
4.body 内容存储
上传图片:七牛云
登录认证
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,定时任务失败怎么办?
为什么做这个项目,技术选型为什么是这样的?
登录怎么做的?单点登录说说你的理解?
说说项目中的闪光点和亮点?
服务端真实存在并且能够直接展示的一些文件 html js css 图片 视频’
降低服务端压力
相对于Tomcat,Nginx处理静态资源的能力更加高效,放到html文件里面
代理服务器(正向代理代理客户端)
起到了安全防护作用
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AFLDzmJt-1681383922627)(https://raw.githubusercontent.com/viacheung/img/main/image/image-20230314140013128.png)]
业务功能复杂 —多集群—负载均衡算法(轮询 权重 响应时间)
nginx.conf
首先大家要搞清楚什么是跨域,为什么会有跨域情况的出现。哪些情况属于跨域?
跨域
:由于浏览器的同源策略,即属于不同域的页面之间不能相互访问各自的页面内容注
:同源策略,单说来就是同协议,同域名,同端口
出于安全考虑(比如csrf攻击),浏览器一般会禁止进行跨域访问,但是因为有时有相应需求,需要允许跨域访问,这时,我们就需要将跨域访问限制打开。 启动一个web服务,端口是8081
然后再开启一个web服务/前端服务都可以。端口是8082,然后再8082的服务中通过ajax来访问8081的服务,这就不满足同源策略,就会出现跨域问题
虽然jsonp也可以实现跨域,但是因为jsonp不支持post请求,应用场景受到很大限制,所以这里不对jsonp作介绍。
CORS 是w3c标准的方式,通过在web服务器端设置:响应头Access-Cntrol-Alow-Origin 来指定哪些域可以访问本域的数据,ie8&9(XDomainRequest),10+,chrom4,firefox3.5,safair4,opera12支持这种方式。
服务器代理,同源策略只存在浏览器端,通过服务器转发请求可以达到跨域请求的目的,劣势:增加服务器的负担,且访问速度慢。
首先配置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;
}
解决了跨域问题
服务器默认是不被允许跨域的。给Nginx服务器配置Access-Control-Allow-Origin *
后,表示服务器可以接受所有的请求源(Origin),即接受所有跨域的请求。
是为了防止出现以下错误:
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)]
是为了防止出现以下错误:
Content-Type is not allowed by Access-Control-Allow-Headers in preflight response.
是为了处理在发送POST请求时Nginx依然拒绝访问的错误,发送"预检请求"时,需要用到方法 OPTIONS ,所以服务器需要允许该方法。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-G5bLDSTj-1681383922631)(https://raw.githubusercontent.com/viacheung/img/main/image/image-20230410163455042.png)]
跨域资源共享(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。即出现以下错误:
jmeter
https://blog.csdn.net/m0_37679452/article/details/103895809
面向过程
优点:性能高,适合单片机嵌入式开发 缺点:没有⾯向对象易维护、易复⽤、易扩展。
⾯向对象 :
优点:⾯向对象易维护、易复⽤、易扩展 缺点:性能低
jdk包括jre
JDK用来创建和编译程序。
JRE 是 Java 运⾏时环境。包括(JVM), Java 类库等,但是,不能⽤于创建新程序。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kgmuYnb4-1681383922631)(https://raw.githubusercontent.com/viacheung/img/main/image/image-20230318182251008.png)]
OpenJDK 是⼀个参考模型、完全开源,⽽ Oracle JDK 是 OpenJDK 的⼀个实现,并不是完全开源的;
Oracle JDK 更稳定 性能更好
都是⾯向对象的语⾔,都⽀持封装、继承和多态
Java 不提供指针来直接访问内存,程序内存更加安全
Java 的类是单继承(接口多继承), C++ ⽀持多重继承;
Java 有⾃动内存管理机制,不需要程序员⼿动释放⽆⽤内存
在 C 语⾔中,字符串或字符数组最后都会有⼀个==额外的字符‘\0’==来表示结束。但是, Java 语
⾔中没有结束符这⼀概念。 这是⼀个值得深度思考的问题,具体原因推荐看这篇⽂章:
https://blog.csdn.net/sszgg2006/article/details/49148189
java皆对象 可以通过length()知道长度 没必要加一个额外字符
解决了传统解释型语言执行效率低的问题,同时又保留了解释型语言可移植的特点
Constructor 不能被 override(重写) ,但是可以 overload(重载) ,所以你可以看到⼀个类中有多个构造函数的情况
都是实现多态方式,重载时编译时多态,重写时运行时多态
重载就是同样的⼀个⽅法能够根据输⼊数据的不同,做出不同的处理 方法名一样 参数列表不同 返回值也可以不同 例如构造器重载
重载的方法能否根据返回值类型进行区分?不可以
重写就是当⼦类继承⾃⽗类的相同⽅法,输⼊数据⼀样,但要做出有别于⽗类的响应时,你就要覆盖⽗类⽅法 方法名一样 参数列表一样 返回类型一样 代码具体实现不一样
封装:对外界提供**方法属性 ** 隐藏不可信信息 数据方法让可信类对象操作
继承:复用代码 父类私有属性方法 子类只读不写
多态:表现不同行为,方法调用到底哪个类实现需要在运行时确定,实现管右边,运行管左边,向上向下转型
编译时多态:运行哪个方法在编译时侯确定了
运行时多态:运行的时候才能确定
多态三个条件:继承、重写和向上转型(需要将子类的引用赋给父类对象,这样该引用才既能可以调用父类的方法,又能调用子类的方法。)。
1、前两者可变 string不可变(final)(适用于操作少量数据)
2、string线程安全 stringbuffer线程安全(synchronized) 那个不安全
3、StringBuilder性能比stringbuffer多10-15%
1.便于实现字符串池(String pool)
堆中开辟一块存储空间String pool,当初始化一个String变量时,如果该字符串已经存在了,就不会去创建一个新的字符串变量,而是会返回已经存在了的字符串的引用。
2.多线程安全
3.避免安全 网络连接地址URL,文件路径path,反射机制所需要的String参数保证连接安全性
4.保证了hashcode的唯一性,因此创建对象即可缓存,Map将其当做key,速度块
形式:字符数量 单双引号
含义上: 字符常量:整型值( ASCII 值) 字符串常量:地址值 对象;
占内存大小:字符常量2个字节;字符串常量占若干个字节
jvm为了提升性能、减少内存开销,开辟字符串常量池 ;当使用字符串 常量池有 直接拿 ; 不存在 初始化 放到池子里面
jdk7 字符串常量池在永久代 常量池 存的是对象
jdk8 字符串常量池在堆 常量池 存的堆的引用
String a = “aaa” ;
,常量池中查找”aaa”字符串,若没有,会将”aaa”字符串放进常量池,再将其地址赋给a;若有,将找到的”aaa”字符串的地址赋给a。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对象的引用地址,因此一样)
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类型来表示
byte、short、char、int enum String
final: 变量:引用不可变 必须初始化 方法:不可重写 类不可继承
finally:异常执行,最后一定执行
finalize 方法回收之前对对象的一些操作
1、属于类 不创建对象就能用这块资源 2、用类直接调用
不行,那岂不是不用new 对象了?
代码块执行顺序静态代码块——> 构造代码块 ——> 构造函数——> 普通代码块
继承中:父类静态–子类静态–父类构造代码块–父类构造器—子类构造代码块—子类构造器
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方法并不会进行类型转换。
Integer b = new Integer(10000); (堆)
Integer c=10000; (常量池)
运行状态时对于一个类可以知道他的所有属性方法,对于任何一个对象,可调用任何方法属性,这种动态获取对象方法并调用的功能叫做反射
优点:能够在运行时动态获取类的实例,灵活性高;例如加载MySQL的驱动类。
缺点:性能较低,需要解析字节码,将内存中的对象进行解析。其解决方案是:通过setAccessible(true)关闭JDK的安全检查来提升反射速度;
多次创建一个类的实例时,有缓存会快很多;ReflflectASM工具类,通过字节码生成的方式加快反射速度。
1、Class.forName(“类的路径”);
2、类名.class
3、对象名.getClass()
获取想要操作的类的Class对象,这是反射的核心,通过Class对象我们可以任意调用类的方法。
调用 Class 类中的方法,既就是反射的使用阶段。
使用反射 API 来操作这些信息。
例如:获取对象实例–获取构造器对象–构造器newInstance获取反射对象–获取方法的Method对象–利用Invoke调用方法(方法.invoke(对象,值))
原因:
也就是说,Oracle 希望开发者将反射作为一个工具,用来帮助程序员实现本不可能实现的功能。
应用:
第一种:JDBC 的数据库的连接
第二种:Spring 框架的使用,最经典的就是xml的配置模式。
Spring 通过 XML 配置模式装载 Bean 的过程:
我的理解:
Spring这样做的好处是:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(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代码大致如下:
泛型是 JDK1.5 的一个新特性,**泛型就是将类型参数化,其在编译时才确定具体的参数。**这种参数类型可以用在类、接口和方法的创建中,分别称为泛型类、泛型接口、泛型方法。
远在 JDK 1.4 版本的时候,那时候是没有泛型的概念的,如果使用 Object 来实现通用、不同类型的处理,有这么两个缺点:
如这个例子:
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 编程思想》中的描述,泛型出现的动机在于:有许多原因促成了泛型的出现,而最引人注意的一个原因,就是为了创建容器类。
使用泛型的好处有以下几点:
泛型是一种语法糖,泛型这种语法糖的基本原理是类型擦除。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 指令用于强制类型转换。这一个过程就叫做『泛型翻译』。
限定通配符对类型进行了限制。有两种限定通配符,一种是 extends T>它通过确保类型必须是T的子类来设定类型的上界,另一种是 super T>==它通过确保类型必须是T的父类来设定类型的下界。==泛型类型必须用限定内的类型来进行初始化,否则会导致编译错误。
非限定通配符 ?,可以用任意类型来替代。如List>
的意思是这个集合是一个可以持有任意类型的集合,它可以是List
,也可以是List
,或者List
等等。
这两个List的声明都是限定通配符的例子,List extends T>可以接受任何继承自T的类型的List,而List super T>可以接受任何T的父类构成的List。例如List extends Number>可以接受List或List。
不可以。真这样做的话会导致编译错误。因为List可以存储任何类型的对象包括String, Integer等等,而这样的话List却只能用来存储String。
List<Object> objectList;
List<String> stringList;
objectList = stringList; //compilation error incompatible types
不可以。这也是为什么 Joshua Bloch 在 《Effective Java》一书中建议使用 List 来代替 Array,因为 List 可以提供编译期的类型安全保证,而 Array 却不能。
ArrayList
与ArrayList
是否相等?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对象是保存在JVM的堆内存中的,也就是说,如果JVM堆不存在了,那么对象也就跟着消失了。
而序列化提供了一种方案,可以让你在即使JVM停机的情况下也能把对象保存下来的方案。就像我们平时用的U盘一样。把Java对象序列化成可存储或传输的形式(如二进制流),比如保存在文件中。这样,当再次需要这个对象的时候,从文件中读取出二进制流,再从二进制流中反序列化出对象。
**反序列化:**客户端从文件中或网络上获得序列化后的对象字节流,根据字节流中所保存的对象状态及描述信息,通过反序列化重建对象。
简要描述:对内存中的对象进行持久化或网络传输, 这个时候都需要序列化和反序列化
深入描述:
主要应用例如:RMI(即远程调用Remote Method Invocation)要利用对象序列化运行远程主机上的服务,就像在本地机上运行对象时一样。
可以将整个对象层次写入字节流中,可以保存在文件中或在网络连接上传递。利用对象序列化可以进行对象的"深复制",即复制对象本身及引用的对象本身。序列化一个对象可能得到整个对象序列。
比如:将某个类序列化后存为文件,下次读取时只需将文件中的数据反序列化就可以将原先的类还原到内存中。也可以将类序列化为流数据进行传输。
总的来说就是将一个已经实例化的类转成文件存储,下次需要实例化的时候只要反序列化即可将类实例化到内存中并保留序列化时类中的所有变量和状态。
序列化以后就都是字节流了,无论原来是什么东西,都能变成一样的东西,就可以进行通用的格式传输或保存,传输结束以后,要再次使用,就进行反序列化还原,这样对象还是对象,文件还是文件。
实现Serializable接口或者Externalizable接口。
类通过实现 java.io.Serializable
接口以启用其序列化功能。可序列化类的所有子类型本身都是可序列化的。序列化接口没有方法或字段,仅用于标识可序列化的语义。
Externalizable
继承自Serializable
,该接口中定义了两个抽象方法:writeExternal()
与readExternal()
。
当使用Externalizable
接口来进行序列化与反序列化的时候需要开发人员重写writeExternal()
与readExternal()
方法。否则所有变量的值都会变成默认值。
实现Serializable接口 | 实现Externalizable接口 |
---|---|
系统自动存储必要的信息 | 程序员决定存储哪些信息 |
Java内建支持,易于实现,只需要实现该接口即可,无需任何代码支持 | 必须实现接口内的两个方法 |
性能略差 | 性能略好 |
serialVersionUID 用来表明类的不同版本间的兼容性
Java的序列化机制是通过在运行时判断类的serialVersionUID来验证版本一致性的。在进行反序列化时,JVM会把传来的字节流中的serialVersionUID与本地相应实体(类)的serialVersionUID进行比较,如果相同就认为是一致的,可以进行反序列化,否则就会出现序列化版本不一致的异常。
如果不显示指定serialVersionUID==, JVM在序列化时会根据属性自动生成一个serialVersionUID==, 然后与属性一起序列化, 再进行持久化或网络传输. 在反序列化时, JVM会再根据属性自动生成一个新版serialVersionUID, 然后将这个新版serialVersionUID与序列化时生成的旧版serialVersionUID进行比较, 如果相同则反序列化成功, 否则报错.
如果显示指定了, JVM在序列化和反序列化时仍然都会生成一个serialVersionUID, 但值为我们显示指定的值, 这样在反序列化时新旧版本的serialVersionUID就一致了.
在实际开发中, 不显示指定serialVersionUID的情况会导致什么问题? 如果我们的类写完后不再修改, 那当然不会有问题, 但这在实际开发中是不可能的, 我们的类会不断迭代, 一旦类被修改了, 那旧对象反序列化就会报错. 所以在实际开发中, 我们都会显示指定一个serialVersionUID, 值是多少无所谓, 只要不变就行。
《阿里巴巴Java开发手册》中有以下规定:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mw4uJpZf-1681383922639)(https://raw.githubusercontent.com/viacheung/img/main/image/image-20210226222339606.png)]
对于不想进行序列化的变量,使用 transient 关键字修饰。
transient
关键字的作用是控制变量的序列化,在变量声明前加上该关键字,可以阻止该变量被序列化到文件中,在被反序列化后,transient
变量的值被设为初始值,如 int 型的是 0,对象型的是 null。transient 只能修饰变量,不能修饰类和方法。
不会。因为序列化是针对对象而言的, 而静态变量优先于对象存在, 随着类的加载而加载, 所以不会被序列化.
看到这个结论, 是不是有人会问, serialVersionUID也被static修饰, 为什么serialVersionUID会被序列化? 其实serialVersionUID属性并没有被序列化, JVM在序列化对象时会自动生成一个serialVersionUID, 然后将我们显示指定的serialVersionUID属性值赋给自动生成的serialVersionUID。
Java 中,所有的异常都有一个共同的祖先 java.lang
包中的 Throwable
类。Throwable
类有两个重要的子类 Exception
(异常)和 Error
(错误)。
Exception
和 Error
二者都是 Java 异常处理的重要子类,各自都包含大量子类。
Exception
:程序本身可以处理的异常,可以通过 catch
来进行捕获,通常遇到这种错误,应对其进行处理,使应用程序可以继续正常运行。Exception
又可以分为运行时异常(RuntimeException, 又叫非受检查异常)和非运行时异常(又叫受检查异常) 。Error
:Error
属于程序无法处理的错误 ,我们没办法通过 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 相关的异常、ClassNotFoundException
、SQLException
等。
非受检查异常和受检查异常之间的区别:是否强制要求调用者必须处理此异常,如果强制要求调用者必须进行处理,那么就使用受检查异常,否则就选择非受检查异常。
Java 中的异常处理除了包括捕获异常和处理异常之外,还包括声明异常和拋出异常,可以通过 throws 关键字在方法上声明该方法要拋出的异常,或者在方法内部通过 throw 拋出异常对象。(方法上+方法内部)
throws 关键字和 throw 关键字在使用上的几点区别如下:
NoClassDefFoundError 是一个 Error 类型的异常,是由 JVM 引起的,不应该尝试捕获这个异常。引起该异常的原因是 JVM 或 ClassLoader 尝试加载某类时在内存中找不到该类的定义,该动作发生在运行期间,即编译时该类存在,但是在运行时却找不到了,可能是编译后被删除了等原因导致。
ClassNotFoundException 是一个受检查异常,需要显式地使用 try-catch 对其进行捕获和处理,或在方法签名中用 throws 关键字进行声明。当使用 Class.forName, ClassLoader.loadClass 或 ClassLoader.findSystemClass 动态加载类到内存的时候,通过传入的类路径参数没有找到该类,就会抛出该异常;另一种抛出该异常的可能原因是某个类已经由一个类加载器加载至内存中,另一个加载器又尝试去加载它。
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或大于等于序列大小时,抛出该异常。
catch 可以省略。更为严格的说法其实是:try只适合处理运行时异常,try+catch适合处理运行时异常+普通异常。也就是说,如果你只用try去处理普通异常却不加以catch处理,编译是通不过的,因为编译器硬性规定,普通异常如果选择捕获,则必须用catch显示声明以便进一步处理。而运行时异常在编译时没有如此规定,所以catch可以省略,你加上catch编译器也觉得无可厚非。(运行时异常不加catch 普通异常必须加)
理论上,编译器看任何代码都不顺眼,都觉得可能有潜在的问题,所以你即使对所有代码加上try,代码在运行期时也只不过是在正常运行的基础上加一层皮。但是你一旦对一段代码加上try,就等于显示地承诺编译器,对这段代码可能抛出的异常进行捕获而非向上抛出处理。如果是普通异常,编译器要求必须用catch捕获以便进一步处理;如果运行时异常,捕获然后丢弃并且+finally扫尾处理,或者加上catch捕获以便进一步处理。
至于加上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 的一部分),默认异常处理器打印出异常信息并终止应用程序。 想要深入了解的小伙伴可以看这篇文章:https://www.cnblogs.com/qdhxhz/p/10765839.html
(适配器)
字节输入流转字符输入流通过 InputStreamReader 实现,该类的构造函数可以传入 InputStream 对象。
字节输出流转字符输出流通过 OutputStreamWriter 实现,该类的构造函数可以传入 OutputStream 对象。
使用了适配器模式和装饰器模式
适配器模式:
Reader reader = new INputStreamReader(inputStream);
把一个类的接口变换成客户端所期待的另一种接口,从而使原本因接口不匹配而无法在一起工作的两个类能够在一起工作
装饰器模式:
new BufferedInputStream(new FileInputStream(inputStream));
一种动态地往一个类中添加新的行为的设计模式。就功能而言,装饰器模式相比生成子类更为灵活,这样可以给某个对象而不是整个类添加一些功能。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dIEEy4mb-1681383922642)(https://raw.githubusercontent.com/viacheung/img/main/image/image-20210227115040999.png)]
否则不就类.任何变量了?
⽗类中只定义了有参数的构造⽅法(一旦父类有了属于自己的非空参数构造方法后,系统将不会再赠送父类没有参数的构造方法),⽽在⼦类的构造⽅法中⼜没有⽤ super() 来调⽤⽗类中特定的构造⽅法,则编译时将发⽣错误,因为不写super的话 默认子类无参构造 所以父类要手写无参构造
1 变量 接口只能有public static final修饰变量 抽象类无所谓
2 实现继承 接口多实现 类单继承
3 设计层面 一个模板设计 一个行为抽象(接口)
4 方法 抽象类可以提供成员方法的具体细节 接口只能public abstract方法
抽象类能用final修饰吗? 不能 我这样搞就是让别人继承的 你干啥?
new 反射 clone 序列化
String Integer包装类 好处线程安全
创建一个包含可变对象的不可变对象? inal Person[] persons = new Persion[]{}
1、位置不同 成员是类的 局部变量是方法的
2、成员属于对象 在堆上 局部变量在方法上 栈帧里面
3、成员会自动以类型默认值赋初始值 (除开final) 局部变量必须显示赋值
\1. 名字与类名相同。
\2. 没有返回值
\3. ⽣成类的对象时⾃动执⾏,⽆需调⽤。
对象的相等,⽐的是内存中存放的内容是否相等。⽽引⽤相等,⽐较的是他们指向的内存地址是
否相等
==: 基本数据类型—内容 引用数据类型-- 地址
equals: 都是内容
equal特点:自反 传递 一致 对称
它实际上是返回一个int整数。这个哈希码的作用是确定该对象在哈希表中的索引位置
你重写过 hashcode 和 equals 么,为什么重写 equals 时必须重写hashCode ⽅法?
答案:减少equals次数,hashcode就是根据对象地址返回一个整数然后再对数组取余的数,这个数可能有冲突,所以需要用下equals比较一下
是否真的相等,不等的话插入链表,所以重写hashCode ⽅法相当于一个屏障了 大大提高速度
1、只重写equals的话,会造成俩对象hashcode的值不同,因为是先根据hashcode进行判断,那么本来相同对象(equals相同)是覆盖的,结果都插进去了
2、减少equals次数
值传递就是把参数的值给你,调用函数时将实际参数复制一份传递到函数中,这样函数内部对参数内部进行修改不会影响到实际参数;
而引用传递就不一样了,它直接把参数的实际地址给调用函数了,函数内部可直接修改该地址内容,会影响到实际参数
为什么?
基本类型作为参数被传递时肯定是值传递;引用类型作为参数被传递时也是值传递,只不过“值”为对应的引用。
那我说下值传递的特征
⼀个⽅法不能修改⼀个基本数据类型的参数(即数值型或布尔型)。(只是copy不影响原值)
⼀个⽅法可以改变⼀个对象参数的状态。(引用可改变值)
⼀个⽅法不能让对象参数引⽤⼀个新的对象。 (引用一旦传递,不可交换)
变量(数值引用不能改)、⽅法(锁定 不能改含义 效率高)、类(不能继承 成员方法均final)。
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。
对于不想进⾏序列化的变量(只能变量),使⽤ transient(转瞬即逝) 关键字修饰。
⽅法 1:通过 Scanner nextLine(); close
⽅法 2:通过 BufferedReader readline
按照流的流向分,可以分为输⼊流和输出流;
按照操作单元划分,可以划分为字节流和字符流;
按照流的⻆⾊划分为节点流和处理流。
字节:input (outsput)stream
字符 reader writer
字符可以用字节转换 但是比较耗时 不如直接来个字符流
场景:⾳频⽂件、图⽚等媒体⽂件⽤字节流⽐较好,如果涉及到字符的话使⽤字符流⽐较好。
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 模型
不广泛 基于事件和回调
浅拷贝:正常 基本数据类型进⾏值传递,对引⽤数据类型进⾏引⽤传递般
深拷贝:String 对基本数据类型进⾏值传递,对引⽤数据类型,创建⼀个新的对象,并复制其内容,
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pvRn5QWQ-1681383922644)(https://raw.githubusercontent.com/viacheung/img/main/image/image-20230310163934375.png)]
getclass equals hashcode notify wait finalize clone tostring
快速失败(fail—fast)
安全失败(fail—safe)
Java集合类主要由两个根接口Collection和Map派生出来的,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等实现类
线程安全的:
线性不安全的:
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,导致插入失败,这样就保证了数据的不可重复性。
1、底层一个数组 一个双向链表
2、增删 和查的效率问题
3、内存空间,Arraylist 预留空间 LinkedList指针
if 容量==0,第一次添加元素容量为 10
else 会将修改次数 modCount++,并且会将原数组中的元素,拷贝至新数组中,新数组的大小是
原数组的 1.5 倍
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pgLdHFyi-1681383922649)(https://raw.githubusercontent.com/viacheung/img/main/image/image-20230310165027434.png)]
hashcode—equals
三个 初始容量+默认加载因子
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ez6yXdXo-1681383922650)(https://raw.githubusercontent.com/viacheung/img/main/image/image-20230324151900931.png)]
数组 +链表+红黑树
扩容:
首先 HashMap 的初始容量是 16,并且每次对原数组长度 * 2 进行扩容,==HashMap 在容量超过负载因子所定义的容量之后,就会扩容,默认0.75,==构造函数可以调整,无参有参构造
当链表大于8,如果数组<64 先数组扩容,否则链表转为红黑树
HashMap扩容:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mZk9ERD5-1681383922651)(https://raw.githubusercontent.com/viacheung/img/main/image/image-20230324234844935.png)]
作为一般规则,默认负载因子(0.75)在时间和空间成本上提供了很好的折衷。较高的值会降低空间开销,但提高查找成本(体现在大多数的HashMap类的操作,包括get和put)。设置初始大小时,应该考虑预计的entry数在map及其负载系数,并且尽量减少rehash操作的次数。如果初始容量大于最大条目数除以负载因子,rehash操作将不会发生
取key的 hashCode 值、根据 hashcode 计算出hash值(hashcode 异或其右移十六位)、通过取模计算下标
右移后再亦或,高位和低位做了混合,在之后的hash & (length-1) 中高位就也参与进运算了,增加了散列程度。
由于和 (length -1) 运算,length 绝大多数情况小于 2 的 16 次方。 所以始终是 hashcode 的低 16 位(甚至更低) 参与运算。 但是这样高 16 位是用不到的,为了让得到的下标更加散列,需要让高16位也参与运算,所以就需要低16位和高16位进行 ^ 运算。
位运算快
充分散列
首先根据 key 的值计算 hash 值,找到该元素在数组中存储的下标;
如果数组是空的,则调用 resize 进行初始化;
如果没有哈希冲突直接放在对应的数组下标里;
如果冲突了,且 key 已经存在,就覆盖掉 value;
如果冲突后,发现该节点是红黑树,则判断TreeNode是否已存在,如果存在则直接返回oldnode并更新;不存在则直接插入红黑树,++size,超出threshold容量就扩容,然后将这个节点挂在树上;
如果是链表,则判断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冲突方法有:开放定址法(线性探测法)、再哈希法(多个hash函数算)、链地址法(拉链法)、建立公共溢出区。HashMap中采用的是 链地址法
线性探测法和
1.7头插会产生,1.8尾插没有了
线程2完成移动 线程1才开始移动 因此就会产生环形链表
A判断好这个地方没有数据,准备插入的时候,这时候B线程抢夺到时间片,来插入,然后A再插入就把B覆盖了
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xGqCFSiy-1681383922653)(https://raw.githubusercontent.com/viacheung/img/main/image/image-20230324212711323.png)]
链表阈值和产生冲突概率为泊松分布 选择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)]
1、2 的幂次可以用 与 的方式进行取余运算,效率更高;
2)在扩容移动链表节点时,节点在新数组中的位置只可能是原位置 i 或 i + oldCap 旧数组长度,扩容时效率更高
一般用Integer、String 这种不可变类当 HashMap 当 key,而且 String 最为常用。
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 倍
先来看JDK1.7
首先,会尝试获取锁,如果获取失败,利用自旋获取锁;如果自旋重试的次数超过 64 次,则改为阻塞获取锁。
获取到锁后:
再来看JDK1.8
大致可以分为以下步骤:
f.hash = MOVED = -1
,说明其他线程在扩容,参与一起扩容。get 方法不需要加锁。因为 Node 的元素 val 和指针 next 是用 volatile 修饰的,在多线程环境下线程A修改结点的val或者新增节点的时候是对线程B可见的。
这也是它比其他并发集合比如 Hashtable、用 Collections.synchronizedMap()包装的 HashMap 安全效率高的原因之一。
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存在
没有关系。哈希桶table
用volatile修饰主要是保证在数组扩容的时候保证可见性。
jdk1.7里面,程序在运行时能够同时更新ConcurrentHashMap且不产生锁竞争的最大线程数默认是16,这个值可以在构造函数中设置。如果自己设置了并发度,ConcurrentHashMap 会使用大于等于该值的最小的2的幂指数作为实际并发度,也就是比如你设置的值是17,那么实际并发度是32。
类似快速失败和安全失败
快速失败就是HashMap 安全失败是Con~
与HashMap迭代器是强一致性不同,ConcurrentHashMap 迭代器是弱一致性。
ConcurrentHashMap 的迭代器创建后,就会按照哈希表结构遍历每个元素,但在遍历过程中,内部元素可能会发生变化,如果变化发生在已遍历过的部分,迭代器就不会反映出来,而如果变化发生在未遍历过的部分,迭代器就会发现并反映出来,这就是弱一致性。
这样迭代器线程可以使用原来老的数据,而写线程也可以并发的完成改变,更重要的,这保证了多个线程并发执行的连续性和扩展性,是性能提升的关键。想要深入了解的小伙伴,可以看这篇文章[为什么ConcurrentHashMap 是弱一致的](http://ifeve.com/ConcurrentHashMap -weakly-consistent/)
ConcurrentHashMap 的效率要高于Hashtable,因为Hashtable给整个哈希表加了一把大锁从而实现线程安全。而ConcurrentHashMap 的锁粒度更低,在JDK1.7中采用分段锁实现线程安全,在JDK1.8 中采用CAS+Synchronized
实现线程安全。
Hashtable是使用Synchronized来实现线程安全的,给整个哈希表加了一把大锁,多线程访问时候,只要有一个线程访问或操作该对象,那其他线程只能阻塞等待需要的锁被释放,在竞争激烈的多线程场景中性能就会非常差!
还可以使用Collections.synchronizedMap
方法,对方法进行加同步锁
如果传入的是 HashMap 对象,其实也是对 HashMap 做的方法做了一层包装,里面使用对象锁来保证多线程场景下,线程安全,本质也是对 HashMap 进行全表锁。在竞争激烈的多线程环境下性能依然也非常差,不推荐使用!
HashSet 是 Set 接⼝的主要实现类 , HashSet 的底层是 HashMap ,线程不安全的,可以存储 null 值;
LinkedHashSet 按照添加的顺序遍历;
TreeSet 底层红⿊树
Map
collection:set list
第一种,实体类实现Comparable接口,并实现 compareTo(T t) 方法,称为内部比较器。
第二种,创建一个外部比较器,这个外部比较器要实现Comparator接口的 compare(T t1, T t2)方法。
使用ListIterator,只能遍历List实现的对象,但可以向前和向后遍历集合中的元素。
根本区别:进程是操作系统资源分配的基本单位,而线程是处理器任务调度和执行的基本单位
资源开销:进程独享内存空间,进程之间的切换会有较大的开销;而线程有自己独立的运行栈和程序计数器(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、按序申请资源 反序释放
破坏死锁条件:
命令
jps -l 查看当前进程运行状况
jstack 进程编号 查看该进程信息
图形化
jconsole 打开线程 ,点击 检测死锁
原理:
区别:sleep() ⽅法没有释放锁,⽽ wait() ⽅法释放了锁
sleep之后自动苏醒,wait需要notify()唤醒
共同点:两者都可以暂停线程的执⾏。
直接调用就是main线程的一个普通方法,并不会在某个线程里面执行
而new 一个 Thread,线程进入了新建状态; 调用start() ,线程进入了就绪状态,然后获得时间片就可以运行了
暂停当前正在执行的线程对象,让其它有相同优先级的线程执行。它是一个静态方法而且只保证当前线程放弃CPU占用而不能保证使其它线程一定能占用CPU,执行yield()的线程有可能在进入到暂停状态后马上又被执行(我暂停了我又回来了吼吼)。
run()
run()
方在调用 start()
方法后被执行,而且一旦线程启动后 start()
方法后就会立即返回,而不是等到 run()
方法执行完毕后再返回。
在新建类时实现 Runnable
接口,然后在 Thread
类的构造函数(new Thread的时候传参)中传入 MyRunnable
的实例对象,最后执行 start()
方法即可;
RUNNING
状态的线程执行 Object.wait()
方法后,JVM 会将线程放入等待序列(waitting queue);
RUNNING
状态的线程在获取对象的同步锁时,若该 同步锁被其他线程占用,则 JVM 将该线程放入锁池(lock pool)中;
RUNNING
状态的线程执行 Thread.sleep(long ms)
或 Thread.join()
方法,或发出 I/O 请求时,JVM 会将该线程置为阻塞状态。当 sleep()
状态超时,join()
等待线程终止或超时. 或者 I/O 处理完毕时,线程重新转入可运行状态(RUNNABLE
);
run()
或者 call()
方法执行完成后,线程正常结束;
线程抛出一个未捕获的 Exception
或 Error
,导致线程异常结束;
直接调用线程的 stop()
方法来结束该线程,但是一般不推荐使用该种方式,因为该方法通常容易导致死锁;
运行在后台的一种特殊进程,在 Java 中垃圾回收线程就是特殊的守护线程。
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接口
互补 存在
1、volatile 只能用于变量,syn可以变量、方法、类级别、代码块
2、volatile 关键字能保证数据的可⻅性,但不能保证数据的原⼦性。 synchronized 关键字两者都能保证
3、volatile 不会造成线程的阻塞;synchronized 可能会造成线程的阻塞。
4、volatile 关键字主要⽤于解决变量在多个线程之间的可⻅性,⽽ synchronized 关键字解决的是多个线程之间访问资源的同步性
5、volatile 本质是告诉当前变量工作内存中的值是不确定的,需要从主存中读取; synchronized 则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。
1.两者都是可重入锁
递归锁,指的是在一个线程中可以多次获取同一把锁,比如: 一个线程在执行一个带锁的方法,该方法中又调用了另一个需要相同锁的方法,则该线程可以直接执行调用的方法,而无需重新获得锁, 两者都是同一个线程每进入一次,锁的计数器都自增1,所以要等到锁的计数器下降为0时才能释放锁。
2.synchronized 依赖于 JVM 而 ReentrantLock 依赖于 API
3.ReentrantLock 比 synchronized 增加了一些高级功能
相比synchronized,ReentrantLock增加了一些高级功能。主要来说主要有三点:①等待可中断;②可实现公平锁;③可实现选择性通知(锁可以绑定多个条件)
4.使用选择
特别注意:
①如果一个线程A调用一个实例对象的非静态 synchronized 方法,而线程B需要调用这个实例对象所属类的静态 synchronized 方法,是允许的,不会发生互斥现象,因为访问静态 synchronized 方法占用的锁是当前类的锁(因为加的不是一把锁)
②尽量不要使用 synchronized(String s) ,因为JVM中,字符串常量池具有缓冲功能
(就是说由于字符串常量池的原因不同的变量可能引用着同一个对象,锁不同变量的时候会锁成同一个对象,从而造成意料之外的同步,降低效率)
属于重量级锁,效率低,线程之间的切换通过操作系统层面,需要从⽤户态转换到内核态,这俩状态之间的转换成本高
1.6之后对synchronized引入大量优化,自旋 锁消除(每个线程一把锁)锁粗化(锁的都是一个对象) 偏向 轻量级锁来减少开销
两个判断:防止加锁过程对象被其他线程实例化
uniqueInstance = new Singleton();
1、分配内存空间
2、初始化
3、指向分配地址
jvm指令重排 导致线程获得还没初始化的实例,解决办法:volatile 保证多线程无指令重排
不可
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(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不成对,单线程情况下问题不大,但多线程下出问题
反编译带syn的代码块,可以看到,同步代码块开始结束位置有个monitorenter 和 monitorexit,到monitorenter这个指令时,会先尝试获取对象的锁,本质上来说,Synchronized其实是通过在对象头上设置标记,锁的计数器就会+1,而当执行到monitorexit这个指令时,锁计数器就会-1,直到减到0,这个锁也就被释放了。如果获取对象锁失败,那当前线程就要阻塞等待,直到锁被另外一个线程释放为止
synchronized 修饰的方法并没有 monitorenter 指令和 monitorexit 指令,取得代之的确实是 ACC_SYNCHRONIZED 标识,该标识指明了该方法是一个同步方法,JVM 通过该 ACC_SYNCHRONIZED 访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。
追溯底层可以发现每个对象天生都带着一个对象监视器: ObjectMonitor :记录线程获取锁次数,记录哪个线程持有我
synchronized 同步语句块的实现使⽤的是 monitorenter 和 monitorexit 指令,其中monitorenter 指令指向同步代码块的开始位置, monitorexit 指令则指明同步代码块的结束位置。当执⾏ monitorenter 指令时,线程试图获取锁也就是获取 对象监视器 monitor 的持有权。
moniter enter -->exit
objectmoniter 类owner谁持有谁记录
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)]
当一段同步代码一直被同一个线程多次访问,由于只有一个线程访问那么该线程在后续访问时便会自动获得锁。连CAS都无,(目的:防止不停的在用户态和内核态之间切换)
在锁对象的对象头里面MarkWord里面存的有当前线程id,第一次进,为空,把id设为自己的,以后每次进的时候没有加锁解锁,直接会去检查锁的MarkWord里面是不是放的自己的线程ID ,是的话,直接进,无CAS,如果不一致,尝试使用CAS来替换MarkWord里面的线程ID为新线程的ID ,竞争成功,表示之前的线程不存在了,MarkWord里面的线程ID为新线程的ID,锁不会升级,仍然为偏向锁;
(续上)如果竞争失败,这时候会等待一个全局安全点,也就是没有代码执行,暂停原来持有偏向锁的线程,检查偏向锁线程是否处于代码块,处于代码块的话,升级为轻量,此时持有线程的还是之前原持有偏向锁的线程,线程B自旋等待;如果已经退出代码块了,锁设为无锁状态。
作用:有线程来参与锁的竞争,但是获取锁的冲突时间极短,本质就是自选锁CAS
轻量级锁是为了在线程近乎交替执行同步块时提高性能 ,说白了先自旋,不行才升级阻塞。
若一个线程获得锁时发现是轻量级锁,会把锁的MarkWord复制到自己的Displaced Mark Word(JVM会为每个线程在 当前线程的栈帧中创建用于存储锁记录的空间 )里面。然后线程尝试用CAS将锁的MarkWord替换为指向锁记录的指针。 总结:就是这里MarkWord存的是指问线程栈中Lock Record的指针
java6之后有个【自适应自选锁】:
线程如果自旋成功了,那下次自旋的最大次数会增加,因为JVM认为既然上次成功了,那么这一次也很大概率会成功。
反之如果很少会自旋成功,那么下次会减少自旋的次数甚至不自旋,避免CPU空转。总之,自适应意味着自选的次数不是固定不变的,而是根据:同一个锁上一次自旋的时间和拥有锁线程的状态来决定。
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年龄了,那么这些信息被移动到哪里去了呢
·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 的非公平其实在源码中应该有不少地方,因为设计者就没按公平锁来设计,核心有以下几个点:
1)当持有锁的线程释放锁时,该线程会执行以下两个重要操作:
在1和2之间,如果有其他线程刚好在尝试获取锁(例如自旋),则可以马上获取到锁。
2)当线程尝试获取锁失败,进入阻塞时,放入链表的顺序,和最终被唤醒的顺序是不一致的,也就是说你先进入链表,不代表你就会先被唤醒。
上面讲到锁有四种状态,并且会因实际情况进行膨胀升级,其膨胀方向是:无锁——>偏向锁——>轻量级锁——>重量级锁,并且膨胀方向不可逆。
每个线程一把锁 等于没有 直接消除
锁同一个对象 合并
4.自适应自旋锁
轻量级锁失败后,因为一个线程持有一把锁的时间并不长,切换线程不值得,因此就自旋等待
自适应自旋锁属于进一步优化,它的自旋的次数不再固定,其自旋的次数由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定,这就解决了自旋锁带来的缺点。
可以的。
具体的触发时机:在全局安全点(safepoint)中,执行清理任务的时候会触发尝试降级锁。
当锁降级时,主要进行了以下操作:
1)恢复锁对象的 markword 对象头;(哦哦 原来所说的markword都是锁对象的呀)
2)重置 ObjectMonitor,然后将该 ObjectMonitor 放入全局空闲列表,等待后续使用。
一种协作协商机制 ,中断的过程完全需要程序员自己实现
① 通过一个volatile变量实现
volatile保证了可见性,t2修改了标志位后能马上被t1看到
② 通过AtomicBoolean(原子布尔型)
③ 通过Thread类自带的中断api方法实现
interrupt() :处于正常活动状态,那么会将该线程的中断标志设置为 true 仅此而已
如果线程处于被阻塞状态,在别的线程中调用当前线程对象的interrupt方法,那么线程将立即退出被阻塞状态(中断状态将被
清除),并抛出一个InterruptedException异常
interrupted():1 返回当前线程的中断状态2 将当前线程的中断状态设为false
isinterrupt 只是判断
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RDWGUSOn-1681383922662)(https://raw.githubusercontent.com/viacheung/img/main/image/image-20230314220933990.png)]
1、wait和notify方法必须要在同步块或者方法里面,且成对出现使用,先wait后notify才OK,顺序
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无效
因为unpark获得了一个凭证, 之后再调用park方法, 就可以名正言顺的凭证消费, 故不会阻塞。 先发放了凭证后续可以畅通无阻。
因为凭证的数量最多为1, 连续调用两次un park和调用一次un park效果一样, 只会增加一个凭证; 而调用两次park却需要消费两个凭
证, 证不够, 不能放行。
CPU 缓存 解决 CPU 处理速度和内存处理速度不对等的问题。
如何解决内存缓存不⼀致性问题?
通过制定缓存⼀致协议
每个线程都有自己的本地内存,读变量从自己内存里面,写变量,自己先改,改完刷回主存
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 关系:
八条原则:1次序(unlock lock) 2传递 3线程启动(先写再读) (先start) 4 线程中断规则 (先interrupt() ,再Thread.interrupted()检测中断 ) 5线程终止规则(线程中的所有操作都先行发生于对此线程的终止检测 ) 6对象终结规则 (对象初始化先于finalize)
Java 内存模型描述的是多线程对共享内存修改后彼此之间的可见性
volatile只能保证可见性和有序性
底层就是内存屏障 ,使得之前的所有读写操作都执行后才可以开始执行此点之后的操作 JVM指令
写指令后加store屏障 读指令前加load屏障
内存屏障之前的所有写操作都要回写到主内存, 内存屏障之后的所有读操作都能获得内存屏障之前的所有写操作的最新结果(实现了可见性)。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pPsFGFAR-1681383922665)(https://raw.githubusercontent.com/viacheung/img/main/image/image-20230314225624869.png)]
volatile的写内存语义是直接刷新到主内存中,读的内存语义是直接从主内存中读取。
一句话,volatile修饰的变量在某个工作内存修改后立刻会刷新会主内存,并把其他工作内存的该变量设置为无效。
写屏障:把存储在缓存的数据写回主内存 写屏障之前的写指令全部执行后面指令才能执行
读屏障:之后的读操作都需要在读屏障之后操作 保证读最新
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pUIGSIPQ-1681383922666)(https://raw.githubusercontent.com/viacheung/img/main/image/image-20230314223713230.png)]
我先写 你们后面先别读
隔断!我重新读主存
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RtDjXnxq-1681383922667)(https://raw.githubusercontent.com/viacheung/img/main/image/image-20230314223831844.png)]
1读2写
i++
大家一起读,一起加一,就看谁提交的快了。提交快的直接让另一个计算失效
比如说你在计算的时候,别的线程已经提交了,所以你的计算直接失效了
本来是6 变成5
总结:第二个线程在第一个线程读取旧值和写回新值期间读取i的阈值,也就造成了线程安全问题
读屏障
在每个volatile读操作的后面插入一个LoadLoad屏障
在每个volatile读操作的后面插入一个LoadStore屏障
写屏障
在每个volatile写操作的前面插入一个StoreStore屏障
在每个volatile写操作的后面插入一个StoreLoad屏障
互补 存在
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)]
字节码层面javap -c xx.class
它其实添加了一个ACC_VOLATILE
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类中的方法都直接调用操作系统底层资源执行相应任务
尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试*获取锁
1. ABA 问题
并发环境下,假设初始条件是A,去修改数据时,发现是A就会执行修改。但是看到的虽然是A,中间可能发生了A变B,B又变回A的情况。此时A已经非彼A,数据即使成功修改,也可能有问题。
1、时间戳 2、版本号
可以通过AtomicStampedReference解决ABA问题,它,一个带有标记的原子引用类,通过控制变量值的版本来保证CAS的正确性。
2. 循环时间长开销
自旋CAS,如果一直循环执行,一直不成功,会给CPU带来非常大的执行开销。
很多时候,CAS思想体现,是有个自旋次数的,就是为了避开这个耗时问题~
3. 只能保证一个变量的原子操作。
CAS 保证的是对一个变量执行操作的原子性,如果对多个变量操作时,CAS 目前无法直接保证操作的原子性的。
可以通过这两个方式解决这个问题:
ThreadLocal,即线程本地变量。如果你创建了一个ThreadLocal变量,那么访问这个变量的每个线程都会有这个变量的一个本地拷贝,多个线程操作这个变量的时候,实际是操作自己本地内存里面的变量,从而起到线程隔离的作用,避免了线程安全问题。
ThreadLocal的应用场景有
每⼀个线程都有⾃⼰的专属本地变量
可以使⽤ get 和 set ⽅法来获取默认值或将其值更改为当前线程所存的副本的值
最终变量存在ThreadLocalMap 中,key 为当前对象的 Thread 对象,值为 Object 对象 (ThreadLocalMap 是 ThreadLocal 的静态内部类。 )
ThreadLocalMap 中使⽤的 key 为 ThreadLocal 的弱引⽤,⽽ value 是强引⽤。
GC的时候,key变成Null,value无了,ThreadLocalMap 实现中已经考虑了这种情况,在调⽤ set() 、 get() 、 remove() ⽅法的时候,会清理掉 key 为 null的记录。使⽤完 ThreadLocal ⽅法后 最好⼿动调⽤ remove() ⽅法 最好需要手动调用remove方法。
ReetrantLock是一个可重入的独占锁,主要有两个特性,一个是支持公平锁和非公平锁,一个是可重入。 ReetrantLock实现依赖于AQS(AbstractQueuedSynchronizer)。
ReetrantLock主要依靠AQS维护一个阻塞队列,多个线程对加锁时,失败则会进入阻塞队列。等待唤醒,重新尝试加锁。
一句话 读读不影响
首先ReentrantLock某些时候有局限,如果使用ReentrantLock,可能本身是为了防止线程A在写数据、线程B在读数据造成的数据不一致,但这样,如果线程C在读数据、线程D也在读数据,读数据是不会改变数据的,没有必要加锁,但是还是加锁了,降低了程序的性能。
因为这个,才诞生了读写锁ReadWriteLock。ReadWriteLock是一个读写锁接口,ReentrantReadWriteLock是ReadWriteLock接口的一个具体实现,实现了读写的分离,读锁是共享的,写锁是独占的,读和读之间不会互斥,读和写、写和读、写和写之间才会互斥,提升了读写的性能
(线程池、数据库连接池、 Http 连接池 )
主要是为了减少每次获取资源的消耗,提⾼对资源的利⽤率。
1、降低资源消耗:服用线程
2、提高速度:任务来的时候不用创建直接执行
3、使⽤线程池可以进⾏统⼀的分配,调优和监控。
具有原⼦/原⼦操作特征的类
4个类型 基本 数组 引用 属性修改
基本:Integer Long Boolean
数组: 后面加个Array
引用:去掉基本类型 加reference stampleReference
属性修改:基本后面加FileldUpdater
AtomicInteger :整形原⼦类
AtomicLong :⻓整型原⼦类
AtomicBoolean :布尔型原⼦类
使⽤原⼦的⽅式更新数组⾥的某个元素
AtomicIntegerArray :整形数组原⼦类
AtomicLongArray :⻓整形数组原⼦类
AtomicReferenceArray :引⽤类型数组原⼦类
AtomicReference :引⽤类型原⼦类
AtomicStampedReference :原⼦更新带有版本号的引⽤类型。该类将整数值与引⽤关联起来,可⽤于解决原⼦的更新数据和数据的版本号,可以解决使⽤ CAS 进⾏原⼦更新时可能出现的 ABA 问题。
AtomicMarkableReference :原⼦更新带有标记位的引⽤类型
AtomicIntegerFieldUpdater :原⼦更新整形字段的更新器
AtomicLongFieldUpdater :原⼦更新⻓整形字段的更新器
AtomicReferenceFieldUpdater :原⼦更新引⽤类型字段的更新器
AtomicLong
线程安全,可允许一些性能损耗,要求高精度时可使用
AtomicLong是多个线程针对单个热点值value进行原子操作
LongAdder
当需要在高并发下有较好的性能表现,且对值的精确度要求不高时,可以使用
保证性能,精度代价
LongAdder是每个线程拥有自己的槽,各个线程一般只对自己槽中的那个值进行CAS操作
小总结
AtomicLong
原理:
CAS+自旋
incrementAndGet
场景:
低并发下的全局计算
AtomicLong能保证并发情况下计数的准确性,其内部通过CAS来解决并发安全性的问题
缺陷
高并发后性能急剧下降
why?AtomicLong的自旋会称为瓶颈(N个线程CAS操作修改线程的值,每次只有一个成功过,其它N -
1失败,失败的不停的自旋直到成功,这样大量失败自旋的情况,一下子cpu就打高了。)
LongAdder
原理
CAS+Base+Cell数组分散
空间换时间并分散了热点数据
场景
高并发的全局计算
缺陷
sum求和后还有计算线程修改结果的话,最后结果不够准确
AbstractQueuedSynchronizer
AbstractQueuedSynchronizer(AQS)
提供了一套可用于实现锁同步机制的框架,不夸张地说,AQS
是JUC
同步框架的基石。AQS
通过一个FIFO
队列维护线程同步状态,实现类只需要继承该类,并重写指定方法即可实现一套线程同步机制。
AQS
根据资源互斥级别提供了独占和共享两种资源访问模式;同时其定义Condition
结构提供了wait/signal
等待唤醒机制。在JUC
中,诸如ReentrantLock
、CountDownLatch
等都基于AQS
实现。
抽象(基石)队列Queue()同步器
如果当前线程访问的资源空闲,将线程设置为有效工作线程,否则需要一套线程阻塞等待唤醒的机制,这个机制是由CLH队列(双向队列)实现的,即将暂时获取不到锁的线程加⼊到队列中。
AQS
的原理并不复杂,AQS
维护了一个volatile int state
变量和一个CLH(三个人名缩写)双向队列
,队列中的节点持有线程引用,每个节点均可通过getState()
、setState()
和compareAndSetState()
对state
进行修改和访问。
当线程获取锁时,即试图对state
变量做修改,如修改成功则获取锁;如修改失败则包装为节点挂载到队列中,等待持有锁的线程释放锁并唤醒队列中的节点。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-U5JqeMXD-1681383922669)(https://raw.githubusercontent.com/viacheung/img/main/image/image-20230310225028080.png)]
如reentrantLock
分为非公平(谁抢到是谁)和公平(排队顺序)
CountDownLatch 、 Semaphore
ReentrantReadWriteLock 可以看成是组合式,因为 ReentrantReadWriteLock 也就是读写锁允许多个线程同时对某⼀资源进⾏读。
⾃定义同步器时 ,使⽤者继承 AbstractQueuedSynchronizer 并重写指定的⽅法
将AQS组合在自定义同步组件的实现中,并调用其模板方法,而这些模板方法会调用使用者重写的方法。
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)]
允许 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)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(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
1、因为原空间用的是本地内存,溢出几率低
2、加载类变多
方法区一部分
class文件:方法 接口 常量池表 编译器生成字面量、符号引用
1.8之后运行时常量池在堆里面
JVM 常量池中存储的是对象还是引⽤呢 ? 引用
NIO:基于通道和缓存区
java堆里面DiectByteBuffer作为Native堆外内存的引用,避免Java堆和Native堆的切换
明确指定了一组排序规则,来保证线程间的可见性
即:要想保证 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语言的运行时绑定(也称为动态绑定或晚期绑定)
类加载过程如下:
总结:加载 验证(是不是对的) 准备 (类变量赋初始值)解析(符号引用–直接引用) 初始化 (构造器)
类加载器是指:通过一个类的全限定性类名获取该类的二进制字节流叫做类加载器;类加载器分为以下四种:
总结: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,并不是先委派给父加载器,加载不了才委派给父加载器。
在JVM中表示两个class对象是否为同一个类存在两个必要条件:
1、类的完整类名必须一致,包括包名。
2、加载这个类的ClassLoader(指ClassLoader实例对象)必须相同
类加载检查–分配内存—初始化0值–设置对象头(juc的java内存布局 对象头 实例数据 对齐填充)–init方法
先看有么有被加载 加载过了就不加载 没加载再加载
遇到⼀条 new 指令时,⾸先检查是否能在常量池中定位到这个类的符号引⽤,检查这个类是否已被加载过、解析和初始化过。如果没有,那必须先执⾏相应的类加载过程
对象所需的内存⼤⼩在类加载完成后便可确定,为对象分配空间的任务等同于把⼀块确定⼤⼩的内存从 Java 堆中划分出来。 分配⽅式有 “指针碰撞”(复制 标记压缩) 和 “空闲列表” (标记清除)两种, 选择哪种分配⽅式由 Java 堆是否规整决定,⽽ Java堆是否规整⼜由所采⽤的垃圾收集器是否带有压缩整理功能决定。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Pwsb4rYe-1681383922677)(https://raw.githubusercontent.com/viacheung/img/main/image/image-20230311204655913.png)]
1、CAS+失败重试: 虚拟机采⽤ CAS配上失败重试的⽅式保证更新操作的原⼦性。
TLAB: 为每⼀个线程(做了线程隔离)预先在 Eden 区分配⼀块⼉内存, JVM给对象分配内存时候,先TLAB分配,不够了再用CAS+失败重试
初始化0值
hashcode Gc信息 对象标记 (mark word) 类元信息(class point)
按照代码逻辑初始化
1、句柄
堆有个句柄池 存了实例对象和类的信息
2、直接指针
存储的直接就是对象的地址
比较:
指针速度快 只需要一次定位即可
句柄能保证引用指向不会改,只改句柄里面实例数据的地址就可
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(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)]
1. System.gc()方法的调用(系统建议执行,但是不必然执行)
2. 老年代不足
3. 永久代不足
4. concurrent mode failure
5**.minor gc时年轻代的存活区空间不足而晋升老年代,老年代又空间不足而触发full gc。**
6. 统计得到的Minor GC晋升到旧生代的平均大小大于老年代的剩余空间
**Minor GC触发条件:**当Eden区满时,触发Minor GC。
对象优先eden
分配担保机制:eden满了触发MinorGc survivor存不下 直接进老年代
大对象直接进老年代
字符串 数组
长期存活的从年轻代到进老年代(15)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(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. 并发回收导致CPU资源紧张:
在并发阶段,它虽然不会导致用户线程停顿,但却会因为占用了一部分线程而导致应用程序变慢,降低程序总吞吐量。
2. 无法清理浮动垃圾:
并发清除阶段,用户线程还在继续运行,就还会伴随有新的垃圾对象不断产生,只好留到下一次垃圾收集时再清理掉。这一部分垃圾称为“浮动垃圾”。
3、碎片问题
通常来说,我们的 JVM 参数配置大多还是会遵循 JVM 官方的建议,例如:
典型的有:死循环、使用无界队列。
不合理的JVM参数配置:优化 JVM 参数配置。典型的有:年轻代内存配置过小、堆内存配置过小、元空间配置过小
JConsole Jprolie dump文件
支持 Lamda 表达式、集合的 stream 操作、提升HashMap性能
如果一个实例发生了问题,根据情况选择,要不要着急去重启。如果出现的CPU、内存飙高或者日志里出现了OOM异常
第一步是隔离(nginx 权重为0),第二步是保留现场(网络 IO cpu ),第三步才是问题排查(ThreadLocal里面的GC Roots,内存泄漏的根本就是,这些对象并没有切断和 GC Roots 的关系,可通过一些工具,能够看到它们的联系。)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(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
三次握手机制:
理想状态下,TCP连接一旦建立,在通信双方中的任何一方主动关闭连接之前,TCP 连接都将被一直保持下去。
确认号=序列号+1;
总结:
1、客户端发送带有syn(请求同步)的数据包到服务端 选择一个随机数seq=x
服务端发送带有syn+ack的数据包
客户端发送带有ack标志数据包
目的:建立可靠传输,双方确认自己与对方的发送接受都正常
确保双方都是能发能接
主要有三个原因:
防止已过期的连接请求报文突然又传送到服务器,因而产生错误和资源浪费。(A网络错误没到服务器 客户端超时重传B报文段 然后顺利到达 假如说是两次握手,那么现在客户端已经close了,A后面如果再到达服务器 服务器返回确认报文 但此时客户端不会有相应 那么会导致服务端长时间等待 造成资源浪费)
在双方两次握手即可建立连接的情况下,假设客户端发送 A 报文段请求建立连接,由于网络原因造成 A 暂时无法到达服务器,服务器接收不到请求报文段就不会返回确认报文段。
客户端在长时间得不到应答的情况下重新发送请求报文段 B,这次 B 顺利到达服务器,服务器随即返回确认报文并进入 ESTABLISHED 状态,客户端在收到 确认报文后也进入 ESTABLISHED 状态,双方建立连接并传输数据,之后正常断开连接。
此时姗姗来迟的 A 报文段才到达服务器,服务器随即返回确认报文并进入 ESTABLISHED 状态,但是已经进入 CLOSED 状态的客户端无法再接受确认报文段,更无法进入 ESTABLISHED 状态,这将导致服务器长时间单方面等待,造成资源浪费。
三次握手才能让双方均确认自己和对方的发送和接收能力都正常。
第一次握手:客户端只是发送处请求报文段,什么都无法确认,而服务器可以确认自己的接收能力和对方的发送能力正常;
第二次握手:客户端可以确认自己发送能力和接收能力正常,对方发送能力和接收能力正常;
第三次握手:服务器可以确认自己发送能力和接收能力正常,对方发送能力和接收能力正常;
可见三次握手才能让双方都确认自己和对方的发送和接收能力全部正常,这样就可以愉快地进行通信了。
告知对方自己的初始序号值,并确认收到对方的初始序号值。
TCP 实现了可靠的数据传输,原因之一就是 TCP 报文段中维护了序号字段和确认序号字段,通过这两个字段双方都可以知道在自己发出的数据中,哪些是已经被对方确认接收的。这两个字段的值会在初始序号值得基础递增,如果是两次握手,只有发起方的初始序号可以得到确认,而另一方的初始序号则得不到确认。
因为三次握手已经可以确认双方的发送接收能力正常,双方都知道彼此已经准备好,而且也可以完成对双方初始序号值得确认,也就无需再第四次握手了。
SYN洪泛攻击属于 DOS 攻击的一种,它利用 TCP 协议缺陷,通过发送大量的半连接请求,耗费 CPU 和内存资源。
原理:
[SYN/ACK]
包(第二个包)之后、收到客户端的 [ACK]
包(第三个包)之前的 TCP 连接称为半连接(half-open connect),此时服务器处于 SYN_RECV
(等待客户端响应)状态。如果接收到客户端的 [ACK]
,则 TCP 连接成功,如果未接受到,则会不断重发请求直至成功。[SYN]
包,服务器回复 [SYN/ACK]
包,并等待客户的确认。由于源地址是不存在的,服务器需要不断的重发直至超时。[SYN]
包将长时间占用未连接队列,影响了正常的 SYN,导致目标系统运行缓慢、网络堵塞甚至系统瘫痪。检测:当在服务器上看到大量的半连接状态时,特别是源 IP 地址是随机的,基本上可以断定这是一次 SYN 攻击。
防范:
服务端:
(服务端超时重传 重传指定次数后 服务器自动关连接)
客户端:
(后面发数据 会发现三次握手失败)
客户端认为这个连接已经建立,如果客户端向服务端发送数据,服务端将以RST包(Reset,标示复位,用于异常的关闭连接)响应。此时,客户端知道第三次握手失败。
第一次挥手:客户端向服务端发送连接释放报文(FIN=1,ACK=1),主动关闭连接,同时等待服务端的确认。
第二次挥手:服务端收到连接释放报文后,立即发出确认报文(ACK=1),序列号 seq = k,确认号 ack = u + 1。
这时 TCP 连接处于半关闭状态,即客户端到服务端的连接已经释放了,但是服务端到客户端的连接还未释放。这表示客户端已经没有数据发送了,但是服务端可能还要给客户端发送数据。
第三次挥手:服务端向客户端发送连接释放报文(FIN=1,ACK=1),主动关闭连接,同时等待 A 的确认。
第四次挥手:客户端收到服务端的连接释放报文后,立即发出确认报文(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一般都会分开发送,从而导致多了一次,因此一共需要四次挥手。
主要有两个原因:
确保 ACK 报文能够到达服务端,从而使服务端正常关闭连接。
第四次挥手时,客户端第四次挥手的 ACK 报文不一定会到达服务端。服务端会超时重传 FIN/ACK 报文,此时如果客户端已经断开了连接,那么就无法响应服务端的二次请求,这样服务端迟迟收不到 FIN/ACK 报文的确认,就无法正常断开连接。
MSL 是报文段在网络上存活的最长时间。客户端等待 2MSL 时间,即「客户端 ACK 报文 1MSL 超时 + 服务端 FIN 报文 1MSL 传输」,就能够收到服务端重传的 FIN/ACK 报文,然后客户端重传一次 ACK 报文,并重新启动 2MSL 计时器。如此保证服务端能够正常关闭。
如果服务端重发的 FIN 没有成功地在 2MSL 时间里传给客户端,服务端则会继续超时重试直到断开连接。
防止已失效的连接请求报文段出现在之后的连接中。
TCP 要求在 2MSL 内不使用相同的序列号。客户端在发送完最后一个 ACK 报文段后,再经过时间 2MSL,就可以保证本连接持续的时间内产生的所有报文段都从网络中消失。这样就可以使下一个连接中不会出现这种旧的连接请求报文段。或者即使收到这些过时的报文,也可以不处理它。
或者说,如果三次握手阶段、四次挥手阶段的包丢失了怎么办?如“服务端重发 FIN丢失”的问题。
简而言之,通过定时器 + 超时重试机制,尝试获取确认,直到最后会自动断开连接。
具体而言,TCP 设有一个保活计时器。服务器每收到一次客户端的数据,都会重新复位这个计时器,以Linux服务器为例,时间通常是设置为 2 小时。若 2 小时还没有收到客户端的任何数据,服务器就开始重试:每隔 75 秒(默认)发送一个探测报文段,若一共发送 10 个探测报文后客户端依然没有回应,那么服务器就认为连接已经断开了。
附:Linux服务器系统内核参数配置
从服务器来讲,短时间内关闭了大量的Client连接,就会造成服务器上出现大量的TIME_WAIT连接,严重消耗着服务器的资源,此时部分客户端就会显示连接不上。
从客户端来讲,客户端TIME_WAIT过多,就会导致端口资源被占用,因为端口就65536个,被占满就会导致无法创建新的连接。
解决办法:
net.ipv4.tcp_tw_reuse 和 tcp_timestamps
TIME_WAIT 是主动断开连接的一方会进入的状态,一般情况下,都是客户端所处的状态;服务器端一般设置不主动关闭连接。
TIME_WAIT 需要等待 2MSL,在大量短连接的情况下,TIME_WAIT会太多,这也会消耗很多系统资源。对于服务器来说,在 HTTP 协议里指定 KeepAlive(浏览器重用一个 TCP 连接来处理多个 HTTP 请求),由浏览器来主动断开连接,可以一定程度上减少服务器的这个问题
在进行数据传输时,如果传输的数据比较大,就需要拆分为多个数据包进行发送。TCP 协议需要对数据进行确认后,才可以发送下一个数据包。这样一来,就会在等待确认应答包环节浪费时间。
为了避免这种情况,TCP引入了窗口概念。窗口大小指的是不需要等待确认应答包而可以继续发送数据包的最大值。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KEdN8ujj-1681383922682)(https://raw.githubusercontent.com/viacheung/img/main/image/image-20210520214432214.png)]
从上面的图可以看到滑动窗口左边的是已发送并且被确认的分组,滑动窗口右边是还没有轮到的分组。
滑动窗口里面也分为两块,一块是已经发送但是未被确认的分组,另一块是窗口内等待发送的分组。随着已发送的分组不断被确认,窗口内等待发送的分组也会不断被发送。整个窗口就会往右移动,让还没轮到的分组进入窗口内。
可以看到滑动窗口起到了一个限流的作用,也就是说当前滑动窗口的大小决定了当前 TCP 发送包的速率,而滑动窗口的大小取决于拥塞控制窗口和流量控制窗口的两者间的最小值。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1Ta6DuJf-1681383922683)(https://raw.githubusercontent.com/viacheung/img/main/image/image-20230318181625164.png)]
TCP 用于在传输层有必要实现可靠传输的情况,UDP 用于对高速传输和实时性有较高要求的通信。TCP 和 UDP 应该根据应用目的按需使用。
TCP 是面向连接,能保证数据的可靠性交付,因此经常用于:
UDP 面向无连接,它可以随时发送数据,再加上UDP本身的处理既简单又高效,因此经常用于:
重传、流量控制、拥塞控制、序列号与确认应达号、校验和
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协议
优点: 简单
缺点: 信道利⽤率低,等待时间⻓
1、⽆差错情况:
发送⽅发送分组,接收⽅在规定时间内收到,并且回复确认.发送⽅再次发送。
2、出现差错情况(超时重传) :
停⽌等待协议中超时重传是指只要超过⼀段时间仍然没有收到确认,就重传前⾯发送过的分组(认为刚才发送过的分组丢失了)。因此每发送完⼀个分组需要设置⼀个超时计时器。
3、确认丢失和确认迟到
确认丢失 :syn --ack丢失 客户端没收到ack 超时计时后 再syn
因此服务端收到俩syn,处理有2种:丢弃这个重复的syn和向A发送
确认消息。
连续 ARQ 协议可提⾼信道利⽤率。发送⽅维持⼀个发送窗⼝,位于窗口内的分组可以连续发送,接受端对到达的最后⼀个分组发
送确认,表明所有分组都已经正确收到了。
优点: 信道利⽤率⾼,容易实现,即使确认丢失,也不必重传。
缺点: 不能向发送⽅反映出接收⽅已经正确收到的所有分组的信息。 ⽐如:发送⽅发送了 5条消息,中间第三条丢失(3号),这时接收⽅只能对前两个发送确认。发送⽅⽆法知道后三个分组的下落,⽽只好把后三个全部重传⼀次。这也叫 Go-Back-N
TCP 利⽤滑动窗⼝实现流量控制。流量控制是为了控制发送⽅发送速率,保证接收⽅来得及接收。
拥塞:某段时间,若对⽹络中某⼀资源的需求超过了该资源所能提供的可⽤部分,⽹络的性能就要变坏。这种情况就叫拥塞。
拥塞控制:拥塞控制就是为了防⽌过多的数据注⼊到⽹络中,这样就可以使⽹络中的路由器或链路不致过载。
vs流量控制:拥塞是全局(涉及到所有主机路由器 )流量是端到端
实操:维持⼀个 拥塞窗⼝
TCP 一共使用了四种算法来实现拥塞控制:
慢开始:不要一开始就发送大量的数据,由小到大逐渐增加拥塞窗口的大小。(每一个传播轮次加倍)
拥塞避免:拥塞避免算法让拥塞窗口缓慢增长,即每经过一个往返时间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 ,当接收端收到不按序的数据段 连发三个确认 那么发送端就立即重发 不等计时器
常见状态码:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UmC3ylfp-1681383922686)(https://raw.githubusercontent.com/viacheung/img/main/image/image-20210525114439748.png)]
共同点:301和302状态码都表示重定向,就是说浏览器在拿到服务器返回的这个状态码后会自动跳转到一个新的URL地址,这个地址可以从响应的Location首部中获取(用户看到的效果就是他输入的地址A瞬间变成了另一个地址B)。
不同点:
301表示旧地址A的资源已经被永久地移除了(这个资源不可访问了),搜索引擎在抓取新内容的同时也将旧的网址交换为重定向之后的网址;
302表示旧地址A的资源还在(仍然可以访问),这个重定向只是临时地从旧地址A跳转到地址B,搜索引擎会抓取新的内容而保存旧的网址。 SEO中302好于301。
补充,重定向原因:
使用上的区别:
本质区别
GET和POST最大的区别主要是GET请求是幂等性的,POST请求不是。这个是它们本质区别。
幂等性是指一次和多次请求某一个资源应该具有同样的副作用。简单来说意味着对同一URL的多个请求应该返回同样的结果。
DNS解析(浏览器搜索自己的DNS缓存(维护一张域名与IP的对应表);若没有,则搜索操作系统的DNS缓存(维护一张域名与IP的对应表);若没有,则搜索操作系统的hosts文件(维护一张域名与IP的对应表)。
若都没有,则找 tcp/ip 参数中设置的首选 dns 服务器,即本地 dns 服务器(递归查询),本地域名服务器查询自己的dns缓存,如果没有,则进行迭代查询。将本地dns服务器将IP返回给操作系统,同时缓存IP。)
TCP连接(发起 tcp 的三次握手,建立 tcp 连接。浏览器会以一个随机端口(1024-65535)向服务端的 web 程序 80 端口发起 tcp 的连接。)
发送HTTP请求。
服务器处理,客户端得到 html 代码。服务器 web 应用程序收到 http 请求后,就开始处理请求,处理之后就返回给浏览器 html 文件。
浏览器解析,解析 html 代码,并请求 html 中的资源。浏览器对页面进行渲染,并呈现给用户
连接结束
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Ynox7lcW-1681383922687)(https://raw.githubusercontent.com/viacheung/img/main/image/image-20230313160513726.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6c4vYDbZ-1681383922688)(https://raw.githubusercontent.com/viacheung/img/main/image/image-20230313160546396.png)]
**HTTP/1.0中默认使⽤短连接 **,每次建立连接之后都中断
HTTP/1.1里面默认使⽤⻓连接 , 响应头加入Connection:keep-alive ,再次访问这个服务器会继续使用已经建立的连接,但有时间限制,且客户端服务端都要支持长连接
HTTP协议的⻓连接和短连接,实质上是TCP协议的⻓连接和短连接。
请求报文格式:
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 请求主体
响应报文:
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>
服务端放一个session记录用户状态,客户端我们通过在 Cookie 中附加⼀个 Session ID 来⽅式来跟踪。
最常⽤的就是利⽤ URL 重写把 Session ID 直接附加在URL路径的后⾯
什么是 Cookie
HTTP Cookie(也叫 Web Cookie或浏览器 Cookie)是服务器发送到用户浏览器并保存在本地的一小块数据,它会在浏览器下次向同一服务器再发起请求时被携带并发送到服务器上。通常,它用于告知服务端两个请求是否来自同一浏览器,如保持用户的登录状态。Cookie 使基于无状态的 HTTP 协议记录稳定的状态信息成为了可能。
Cookie 主要用于以下三个方面:
什么是 Session
Session 代表着服务器和客户端一次会话的过程。Session 对象存储特定用户会话所需的属性及配置信息。这样,当用户在应用程序的 Web 页之间跳转时,存储在 Session 对象中的变量将不会丢失,而是在整个用户会话中一直存在下去。当客户端关闭会话,或者 Session 超时失效时会话结束。
总结
作用:保存⽤户信息 ,帮你登录的⼀些基本信息给填了 ,cookie里面存放了⼀个Token ,下次登录的时候只需要根据 Token 值来查找⽤户即可
区别:Cookie 数据保存在客户端(浏览器端), Session 数据保存在服务器端。 session安全些
用户第一次请求服务器的时候,服务器根据用户提交的相关信息,创建对应的 Session ,请求返回时将此 Session 的唯一标识信息 SessionID 返回给浏览器,浏览器接收到服务器返回的 SessionID 信息后,会将此信息存入到 Cookie 中,同时 Cookie 记录此 SessionID 属于哪个域名。
当用户第二次访问服务器的时候,请求会自动判断此域名下是否存在 Cookie 信息,如果存在自动将 Cookie 信息也发送给服务端,服务端会从 Cookie 中获取 SessionID,再根据 SessionID 查找对应的 Session 信息,如果没有找到说明用户没有登录或者登录失效,如果找到 Session 证明用户已经登录可执行后面操作。
根据以上流程可知,SessionID 是连接 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全称Distributed Denial of Service,分布式拒绝服务攻击。最基本的DOS攻击过程如下:
DDoS则是采用分布式的方法,通过在网络上占领多台“肉鸡”,用多台计算机发起攻击。
DOS攻击现在基本没啥作用了,因为服务器的性能都很好,而且是多台服务器共同作用,1V1的模式黑客无法占上风。对于DDOS攻击,预防方法有:
XSS也称 cross-site scripting,跨站脚本。这种攻击是由于服务器将攻击者存储的数据原原本本地显示给其他用户所致的。比如一个存在XSS漏洞的论坛,用户发帖时就可以引入带有<script>标签的代码,导致恶意代码的执行。
预防措施有:
select distinct * from company where id='1' OR '1' = '1'
SQL 注入就是在用户输入的字符串中加入 SQL 语句,如果在设计不良的程序中忽略了检查,那么这些注入进去的 SQL 语句就会被数据库服务器误认为是正常的 SQL 语句而运行,攻击者就可以执行计划外的命令或访问未被授权的数据。
SQL注入的原理主要有以下 4 点
避免SQL注入的一些方法:
多台服务器以对称的方式组成一个服务器集合,每台服务器都具有等价的地位,能互相分担负载。
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)。
HTTP2.0相比HTTP1.1支持的特性:
都可以标识一个资源 但URL可以定位到这个资源
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(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等
优点:
缺点:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XQzMlnBx-1681383922689)(https://raw.githubusercontent.com/viacheung/img/main/image/image-20210525160006424.png)]
图片来源:https://segmentfault.com/a/1190000021494676
加密流程按图中的序号分为:
客户端请求 HTTPS 网址,然后连接到 server 的 443 端口 (HTTPS 默认端口,类似于 HTTP 的80端口)。
采用 HTTPS 协议的服务器必须要有一套数字 CA (Certification Authority)证书。颁发证书的同时会产生一个私钥和公钥。私钥由服务端自己保存,不可泄漏。公钥则是附带在证书的信息中,可以公开的。证书本身也附带一个证书电子签名,这个签名用来验证证书的完整性和真实性,可以防止证书被篡改。
服务器响应客户端请求,将证书传递给客户端,证书包含公钥和大量其他信息,比如证书颁发机构信息,公司信息和证书有效期等。
客户端解析证书并对其进行验证。如果证书不是可信机构颁布,或者证书中的域名与实际域名不一致,或者证书已经过期,就会向访问者显示一个警告,由其选择是否还要继续通信。
如果证书没有问题,客户端就会从服务器证书中取出服务器的公钥A。然后客户端还会生成一个随机码 KEY,并使用公钥A将其加密。
客户端把加密后的随机码 KEY 发送给服务器,作为后面对称加密的密钥。
服务器在收到随机码 KEY 之后会使用私钥B将其解密。经过以上这些步骤,客户端和服务器终于建立了安全连接,完美解决了对称加密的密钥泄露问题,接下来就可以用对称加密愉快地进行通信了。
服务器使用密钥 (随机码 KEY)对数据进行对称加密并发送给客户端,客户端使用相同的密钥 (随机码 KEY)解密数据。
双方使用对称加密愉快地传输所有数据。
管理计算机硬件与软件资源的程序
屏蔽了硬件层的复杂性
内核负责系统的内存管理,硬件设备的管理,⽂件系统的管理以及应⽤程序的管理。
用户态内核态
⽤户程序中,凡是与系统态级别的资源有关的操作(如⽂件管理、进程控制、内存管理等),都必须通过系统调⽤⽅式向操作系统提出服务请求,并由操作系统代为完成。
并发:1、一段时间,有多个任务在执行; 但某一时刻 ,只有一个任务在执行 本质是因为进程切换时间片足够块来达到同时多个程序在运行的错觉
并行:2、同一时刻,确实有多个任务在执行。需要多核处理器才能完成,不同的程序被放到不同的处理器上运行
进程切换分两步:
1、切换页表以使用新的地址空间(换其他线程的虚拟地址空间),一旦去切换上下文,处理器中所有已经缓存的内存地址一瞬间都作废了。
2、切换内核栈和硬件上下文。
线程只有
2、切换内核栈和硬件上下文。
因为每个进程都有自己的虚拟地址空间,而线程是共享所在进程的虚拟地址空间的,因此同一个进程中的线程进行线程切换时不涉及虚拟地址空间的转换。
进程都有自己的虚拟地址空间,把虚拟地址转换为物理地址需要查找页表,页表查找是一个很慢的过程,因此通常使用Cache来缓存常用的地址映射,这样可以加速页表查找,这个Cache就是TLB(translation Lookaside Buffer,TLB本质上就是一个Cache,是用来加速页表查找的)。
由于每个进程都有自己的虚拟地址空间,那么显然每个进程都有自己的页表,那么当进程切换后页表也要进行切换,页表切换后TLB就失效了,Cache失效导致命中率降低,那么虚拟地址转换为物理地址就会变慢,表现出来的就是程序运行会变慢,而线程切换则不会导致TLB失效,因为线程无需切换地址空间,因此我们通常说线程切换要比较进程切换块,原因就在这里。
总结:虚拟地址到物理地址需要用到页表进行转换,为了加速转换,一般把页表放到Cache(TLB)里面 进程切换后页表也要进行切换,页表切换后TLB就失效了,所以这个转换就变慢了
管道:管道这种通讯方式有两种限制,一是半双工的通信,数据只能单向流动,二是只能在具有亲缘关系的进程间(父子进程关系)。
管道可以分为两类:匿名管道和命名管道。匿名管道是单向的,只能在有亲缘关系的进程间通信;
命名管道以磁盘文件的方式存在,可以实现本机任意两个进程通信。
信号 : 信号是一种比较复杂的通信方式,信号可以在任何时候发给某一进程,而无需知道该进程的状态。
Socket:与其他通信机制不同的是,它可用于不同机器间的进程通信。
消息队列:消息队列克服了信号承载信息量少,管道只能承载无格式字节流以及缓冲区大小受限等缺点。
共享内存:共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。共享内存是最快的 IPC 方式,它是针对其他进程间通信方式运行效率低而专门设计的。它往往与其他通信机制,如信号量,配合使用,来实现进程间的同步和通信。
信号量:信号量是一个计数器,可以用来控制多个进程对共享资源的访问。它常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段。
优缺点:
管道/匿名管道(Pipes)
信号(Signal)
消息队列(Message Queuing)
信号量(Semaphores) :计数器,⽤于多进程对共享数据的访问
共享内存(Shared memory)
套接字(Sockets) : 客户端和服务器之间通过⽹络进⾏通信
1、临界区:通过多线程的串行化来访问公共资源或一段代码,速度快,适合控制数据访问。(只能同步本进程内的线程)
优点:保证在某一时刻只有一个线程能访问数据的简便办法。
缺点:虽然临界区同步速度很快,但却只能用来同步本进程内的线程,而不可用来同步多个进程中的线程。
2、互斥量:为协调共同对一个共享资源的单独访问而设计的。互斥量跟临界区很相似,比临界区复杂,互斥对象只有一个,只有拥有互斥对象的线程才具有访问资源的权限。(可以在不同进程的线程之间进行同步)
优点:使用互斥不仅仅能够在同一应用程序不同线程中实现资源的安全共享,而且可以在不同应用程序的线程之间实现对资源的安全共享。
缺点:
3、信号量:为控制一个具有有限数量用户资源而设计。它允许多个线程在同一时刻访问同一资源,但是需要限制在同一时刻访问此资源的最大线程数目。互斥量是信号量的一种特殊情况,当信号量的最大资源数=1就是互斥量了。
优点:适用于对Socket(套接字)程序中线程的同步。
缺点:
4、事件: 用来通知线程有一些事件已发生,从而启动后继任务的开始。
优点:事件对象通过通知操作的方式来保持线程的同步,并且可以实现不同进程中的线程同步操作。
1、临界区:当多个线程访问一个独占性共享资源时,可以使用临界区对象。拥有临界区的线程可以访问被保护起来的资源或代码段,其他线程若想访问,则被挂起,直到拥有临界区的线程放弃临界区为止,以此达到用原子方式操 作共享资源的目的。
2、事件:事件机制,则允许一个线程在处理完一个任务后,主动唤醒另外一个线程执行任务。
3、互斥量:互斥对象和临界区对象非常相似,只是其允许在进程间使用,而临界区只限制与同一进程的各个线程之间使用,但是更节省资源,更有效率。
( Java 中的synchronized 关键词和各种 Lock 都是这种机制。)
4、信号量:当需要一个计数器来限制可以使用某共享资源的线程数目时,可以使用“信号量”对象。
区别:
从线程的运行空间来说,分为用户级线程(user-level thread, ULT)和内核级线程(kernel-level, KLT)
内核级线程:这类线程依赖于内核,又称为内核支持的线程或轻量级进程。无论是在用户程序中的线程还是系统进程中的线程,它们的创建、撤销和切换都由内核实现。比如英特尔i5-8250U是4核8线程,这里的线程就是内核级线程
用户级线程:它仅存在于用户级中,这种线程是不依赖于操作系统核心的。应用进程利用线程库来完成其创建和管理,速度比较快,操作系统内核无法感知用户级线程的存在。
每个进程中访问临界资源的那段程序称为临界区,一次仅允许一个进程使用的资源称为临界资源。
解决冲突的办法:
什么是死锁:
两个或者多个进程持有某种资源而又等待其它进程释放现在保持着的资源,然后形成一种无限期的阻塞、相互等待的一种状态。
死锁产生的四个必要条件:(有一个条件不成立,则不会产生死锁)
常用的处理死锁的方法有:死锁预防、死锁避免、死锁检测、死锁解除、鸵鸟策略。
**(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个位置。
而分段的因为每段的长度不一样,必须给出段码和段内地址
操作系统把物理内存(physical RAM)分成一块一块的小内存,每一块内存被称为页(page)。当内存资源不足时,Linux把某些页的内容转移至硬盘上的一块空间上,以释放内存空间。硬盘上的那块空间叫做交换空间**(swap space),而这一过程被称为交换(swapping)。**物理内存和交换空间的总容量就是虚拟内存的可用容量。
用途:
物理地址就是内存中真正的地址,具有唯一性。不管哪种地址,最终都会映射为物理地址。
有效地址=逻辑地址 线性地址=逻辑地址
在实模式
下,段基址 + 段内偏移经过地址加法器的处理,经过地址总线传输,最终也会转换为物理地址
。
但是在保护模式
下,段基址 + 段内偏移被称为线性地址
,不过此时的段基址不能称为真正的地址,而是会被称作为一个选择子
的东西,选择子就是个索引,相当于数组的下标,通过这个索引能够在 GDT 中找到相应的段描述符,段描述符记录了段的起始、段的大小等信息,这样便得到了基地址。如果此时没有开启内存分页功能,那么这个线性地址可以直接当做物理地址来使用,直接访问内存。如果开启了分页功能,那么这个线性地址又多了一个名字,这个名字就是虚拟地址
。
不论在实模式还是保护模式下,段内偏移地址都叫做有效地址
。有效地址也是逻辑地址。
线性地址可以看作是虚拟地址
,虚拟地址不是真正的物理地址,但是虚拟地址会最终被映射为物理地址。下面是虚拟地址 -> 物理地址的映射。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Al5U1jiF-1681383922694)(https://raw.githubusercontent.com/viacheung/img/main/image/image-20210807152300643.png)]
缓冲区溢出是指当计算机向缓冲区填充数据时超出了缓冲区本身的容量,溢出的数据覆盖在合法数据上。
危害有以下两点:
造成缓冲区溢出的主要原因是程序中没有仔细检查用户输入。
因为只加载进程的一部分进入内存 然后用页面置换技术就完事
虚拟内存就是说,让物理内存扩充成更大的逻辑内存,从而让程序获得更多的可用内存。虚拟内存使用部分加载的技术,==让一个进程或者资源的某些页面加载进内存,从而能够加载更多的进程,甚至能加载比内存大的进程,==这样看起来好像内存变大了,这部分内存其实包含了磁盘或者硬盘,并且就叫做虚拟内存。
虚拟内存中,允许将一个作业分多次调入内存。釆用连续分配方式时,会使相当一部分内存空间都处于暂时或永久
的空闲状态,造成内存资源的严重浪费,而且也无法从逻辑上扩大内存容量。因此,虚拟内存的实需要建立在离散分配的内存管理方式的基础上。虚拟内存的实现有以下三种方式:
肯定要离散呀 连续的话 那相当于没有虚拟内存 使用起始位置+偏移量
https://juejin.cn/post/6882984260672847879
IO多路复用是指内核一旦发现进程指定的一个或者多个IO条件准备读取,它就通知该进程。IO多路复用适用如下场合:
inode 是一个描述文件或目录属性的数据库,如元信息、硬盘物理地址等。通过 inode,操作系统可以检索文件权限信息、物理地址等信息。当一个文件从一个文件夹移到另一个文件夹,文件将被移动到硬盘的另一个位置,文件的 inode 值也会自动发生变化。
硬连接直接通过 inode 引用文件。硬连接只能用于文件,而不能用于目录。
硬连接(Hard Link)扮演着源文件拷贝或镜像的角色。可以访问源文件的数据,如果源文件被删除,硬连接依然可以访问源文件的数据。
软连接本质上是源文件的一个快捷方式,指向源文件本身,而不是源文件的 inode 值。软连接可以同时用于文件和目录,也可以在不同的硬盘或容器之间使用。
软连接(Soft Link 或 Symbolic Link)扮演着源文件指针的角色。不可以访问源文件数据,如果源文件被删除,软连接将会指向一个不再存在的文件地址。
硬连接直接引用源文件引用的 inode,软连接则直接引用源文件。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-aRDMVrv9-1681383922695)(https://raw.githubusercontent.com/viacheung/img/main/image/image-20230331214416885.png)]
用户态和系统态是操作系统的两种运行状态:
- 内核态:内核态运行的程序可以访问计算机的任何数据和资源,不受限制,包括外围设备,比如网卡、硬盘等。处于内核态的 CPU 可以从一个程序切换到另外一个程序,并且占用 CPU 不会发生抢占情况。
- 用户态:用户态运行的程序只能受限地访问内存,只能直接读取用户程序的数据,并且不允许访问外围设备,用户态下的 CPU 不允许独占,也就是说 CPU 能够被其他程序获取。
将操作系统的运行状态分为用户态和内核态,主要是为了对访问能力进行限制,防止随意进行一些比较危险的操作导致系统的崩溃,比如设置时钟、内存清理,这些都需要在内核态下完成 。
所有的用户进程都是运行在用户态的,但是我们上面也说了,用户程序的访问能力有限,一些比较重要的比如从硬盘读取数据,从键盘获取数据的操作则是内核态才能做的事情,而这些数据却又对用户程序来说非常重要。所以就涉及到两种模式下的转换,即用户态 -> 内核态 -> 用户态,而唯一能够做这些操作的只有 系统调用
,而能够执行系统调用的就只有 操作系统
。
一般用户态 -> 内核态的转换我们都称之为 trap 进内核,也被称之为 陷阱指令(trap instruction)
。
他们的工作流程如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Cf80Tdpd-1681383922695)(https://raw.githubusercontent.com/viacheung/img/main/image/image-20210807152619210.png)]
glibc
库,glibc 是一个标准库,同时也是一套核心库,库中定义了很多关键 API。系统调用
的正确方法,它会根据体系结构应用程序的二进制接口设置用户进程传递的参数,来准备系统调用。软件中断指令(SWI)
,这个指令通过更新 CPSR
寄存器将模式改为超级用户模式,然后跳转到地址 0x08
处。vector_swi()
。sys_call_table
的索引,调转到系统调用函数。对于一次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有数据到达了,就通知用户进程。该模型的优势并不是对于单个连接能处理得更快,而是在于能处理更多的连接。
(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. 如果该⻚在快表中,直接从快表中读取相应的物理地址;
\3. 如果该⻚不在快表中,就访问内存中的⻚表,再从⻚表中得到物理地址,同时将⻚表中的该映射表项添加到快表中;
\4. 当快表填满后,⼜要登记新⻚时,就按照⼀定的淘汰策略淘汰掉快表中的⼀个⻚。
多级⻚表
时间换空间
共同点 :
1、都是为了提⾼内存利⽤率,较少内存碎⽚。
2、每个⻚和段中的内存是连续的。
区别 :
1、⻚的⼤⼩是固定的,由操作系统决定;⽽段的⼤⼩不固定,取决于我们当前运⾏的程序。
2、分⻚仅仅是为了满⾜操作系统内存管理的需求,⽽段是逻辑信息的单位,
逻辑地址:指针⾥⾯存储的数值
物理地址指的是真实物理内存中地址 (内存地址寄存器 )
如果直接把物理地址暴露出来的话会带来严重问题,⽐如可能对操作系统造成伤害以及给同时运⾏多个程序造成困难。
只是多个进程被分割为多个物理内存块(还有部分暂时存储在外部磁盘存储器上,在需要时进⾏数据交换。 ) 感觉上是独享主存
时间局部:如果程序中的某条指令⼀旦执⾏,不久以后该指令可能再次执⾏
空间局部性 :⼀旦程序访问了某个存储单元,在不久之后,其附近的存储单元也将被访问,
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(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)⻚⾯置换算法(最少使⽤⻚⾯置换算法)
超键:在关系中能唯一标识元组的属性集称为关系模式的超键。一个属性可以为作为一个超键,多个属性组合在一起也可以作为一个超键。
超键包含候选键和主键。
候选键:是最小超键,即没有冗余元素的超键。
主键:数据库表中对储存数据对象予以唯一和完整标识的数据列或属性的组合。一个数据列只能有一个主键,且主键的取值不能缺失,即不能为空值(Null)。
外键:在一个表中存在的另一个表的主键称此表的外键。
NOT NULL: 用于控制字段的内容一定不能为空(NULL)。
UNIQUE: 控件字段内容不能重复,一个表允许有多个 Unique 约束。
PRIMARY KEY: 也是用于控件字段内容不能重复,但它在一个表只允许出现一个。
FOREIGN KEY: 用于预防破坏表之间连接的动作,也能防止非法数据插入外键列,因为它必须是它指向的那个表中的值之一。
CHECK: 用于控制字段的值范围。
定长vs 不定长
char 是一个定长字段,假如申请了char(10)的空间,那么无论实际存储多少内容.该字段都占用 10 个字符,而 varchar 是变长的,也就是说申请的只是最大长度,占用的空间为实际字符长度+1,最后一个字符存储使用了多长的空间.
在检索效率上来讲,char > varchar,因此在使用中,如果确定某个字段的值的长度,可以使用 char,否则应该尽量使用 varchar.
例如存储用户 MD5 加密后的密码,则应该使用 char。
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要快。
概念:对数据表里所有记录的引用指针。类似目录 查字典
优点:加快数据的检索速度 在查询的过程中,使用优化隐藏器,提高系统的性能。 缺点:索引的维护成本(增删改的时候要维护)+占物理空间
B+树索引:所有数据存储在叶子节点,复杂度为O(logn)
,适合范围查询。
哈希索引: 适合等值查询,检索效率高,一次到位。
全文索引:MyISAM
和InnoDB
中都支持使用全文索引,一般在文本类型char,text,varchar
类型上创建。
R-Tree
索引: 用来对GIS
数据类型创建SPATIAL
索引
应用层:普通(单个列) 唯一 (值必须唯一,但允许有空值) 复合(组合搜索) 聚簇 非聚簇
物理存储维度
Innodb
存储引擎)Innodb
存储引擎)逻辑维度:
MySQL中
基本索引类型,允许空值和重复值。适合等值查询,检索效率高,一次到位。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-p5scv3nn-1681383922697)(https://raw.githubusercontent.com/viacheung/img/main/image/image-20230314141544547.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(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性质:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gQjBRiHc-1681383922699)(https://raw.githubusercontent.com/viacheung/img/main/image/image-20230314141916699.png)]
B+非叶子节点不存数据,占用空间小,磁盘读写代价低
B+适合区间查询,扫一遍叶子节点就行,但B-需要中序2遍历 所以B+树更加适合在区间查询
的情况,所以通常B+树用于数据库索引。
Hash:
二叉树: 二叉树树化为链表变成线性查询 树的高度不均匀,不能自平衡,查找效率跟数据有关(树的高度),并且IO代价高。
平衡二叉树:只能存储两个节点 多叉树可以存更多 树高降低
红黑树: 树的高度随着数据量增加而增加,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字段用做索引查找,以此类推。因此在建立联合索引的时候应该注意索引列的顺序,一般情况下,==将查询需求频繁或者字段选择性高的列放在前面。==此外可以根据特例的查询或者表结构进行单独的调整。
最左前缀原则就是最左优先,在创建多列索引时,要根据业务需求,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';
其中,name
和age
为联合索引(idx_name_age
)。
如果是Mysql5.6之前,在idx_name_age
索引树,找出所有名字第一个字是“小”
的人,拿到它们的主键id
,然后回表找出数据行,再去对比年龄和性别等其他字段。如图:
有些朋友可能觉得奇怪,idx_name_age(name,age)
不是联合索引嘛?为什么选出包含“小”
字后,不再顺便看下年龄age
再回表呢,不是更高效嘛?所以呀,MySQL 5.6
就引入了索引下推优化,可以在索引遍历过程中,对索引中包含的字段先做判断,直接过滤掉不满足条件的记录,减少回表次数。
因此,MySQL5.6版本之后,选出包含“小”
字后,顺表过滤age=28
如果一张表数据量级是千万级别以上的,那么,如何给这张表添加索引?
我们需要知道一点,给表添加索引的时候,是会对表加锁的。如果不谨慎操作,有可能出现生产事故的。可以参考以下方法:
A
数据结构相同的新表B
。B
添加需要加上的新索引。A
数据导到新表B
rename
新表B
为原表的表名A
,原表A
换别的表名;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 的实现,是通过保存数据在某个时间点的快照来实现的。根据事务开始的时间不同,每个事务对同一张表,同一时刻看到的数据可能是不一样的。
前置知识
快照读:读取的是记录数据的可见版本(有旧的版本)。不加锁,普通的select语句都是快照读。
当前读:读取的是记录数据的最新版本,显式加锁的都是当前读。
ROW ID:隐藏的自增 ID,如果表没有主键,InnoDB 会自动按 ROW ID 产生一个聚集索引树。
事务 ID:记录最后一次修改该记录的事务 ID。
回滚指针:多个事务并行操作某一行数据时,不同事务对该行数据的修改会产生多个版本然后通过回滚指针(roll_pointer),连成一个链表,这个链表就称为版本链。如下:
InnoDB
存储引擎,每一行记录都有两个隐藏列trx_id(当前事务id)、roll_pointer
,如果表中没有主键和非NULL唯一键时,则还会有第三个隐藏的主键列row_id
。当delete
一条记录时,undo log
中会记录一条对应的insert
记录,当update
一条记录时,它记录一条对应相反的update
记录。
1、事务回滚时,保证原子性和一致性。
2、用于MVCC快照读。
什么是Read View
Read View是什么呢? 它就是事务执行SQL语句时,产生的读视图。每个SQL语句执行前都会得到一个Read View
。它主要是用来做可见性判断的,即判断当前事务可见哪个版本的数据~
在Read View
中,有这几个重要的属性。
Read view 匹配条件规则(很重要)
trx_id < min_limit_id
,表明生成该版本的事务在生成Read View前,已经提交(因为事务ID是递增的),所以该版本可以被当前事务访问。trx_id>= max_limit_id
,表明生成该版本的事务在生成Read View后才生成,所以该版本不可以被当前事务访问。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生成之前就已经提交了,修改的结果,当前事务是能看见的。
事务ID(trx_id)
Read View
的可见性规则, 即就需要Undo log
中历史快照;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)]
事务AB先后开启事务 trx_id 为100 101
事务A第一次查到name是孙权 (trx_id=最新trx_id 符合可见性原则)
事务B把name改为曹操 最新版本链101
事务A再查(trx_id=101 但此时因为B提交了 m_ids里面已经没101了 所以依旧符合可见性原则 查出来是曹操) 出现不可重读的问题
RR级别下:
分析一下:
主要是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 时,大致步骤如下:
总结:
InnoDB 每一行数据都有一个隐藏的回滚指针,用于指向该行修改前的最后一个历史版本,这个历史版本存放在 undo log 中。如果要执行更新操作,会将原记录放入 undo log 中,并通过隐藏的回滚指针指向 undo log 中的原记录。其它事务此时需要查询时,就是查询 undo log 中这行数据的最后一个历史版本。
MVCC 最大的好处是读不加锁,读写不冲突,极大地增加了 MySQL 的并发性。通过 MVCC,保证了事务 ACID 中的 I(隔离性)特性。
处理大事务和长事务是数据库设计和优化中非常重要的一部分,以下是一些常用的处理方法:
事务是一个不可分割的数据库操作序列 事务是逻辑上的⼀组操作,要么都执⾏,要么都不执⾏
事务,由一个有限的数据库操作序列构成,这些操作要么全部执行,要么全部不执行,是一个不可分割的工作单位。
原子性: 事务作为一个整体被执行,包含在其中的对数据库的操作要么全部都执行,要么都不执行。
一致性: 指在事务开始之前和事务结束以后,数据不会被破坏,假如A账户给B账户转10块钱,不管成功与否,A和B的总金额是不变的。
隔离性: 多个事务并发访问时,事务之间是相互隔离的,一个事务不应该被其他事务干扰,多个并发事务之间要相互隔离。
持久性: 表示事务完成提交后,该事务对数据库所作的操作更改,将持久地保存在数据库之中
重做日志文件(redo log)和回滚日志(undo log)实现的。
提交一个事务必须先将该事务的所有日志写入到redo log进行持久化,数据库就可以通过重做日志来保证事务的原子性和持久性。
每当有修改事务时,还会产生 undo log,如果需要回滚,则根据 undo log 的反向语句进行逻辑操作,比如 insert 一条记录就 delete 一条记录。undo log 主要实现数据库的一致性。
包括二进制日志binlog
(归档日志)、事务日志redo log
(重做日志)和undo log
(回滚日志)。
InnoDB
存储引擎独有的,它让MySQL
有了崩溃恢复的能力
当MySQL
实例挂了或者宕机了,重启的时候InnoDB
存储引擎会使用rede log
日志恢复数据,保证事务的持久性和完整性。如下图:
MySQL
中数据是以页存储,当查询一条记录时,硬盘会把一整页的数据加载出来(数据页)放到Buffer Pool
中。后续的查询都是先从Buffer Pool
中找,没有找到再去硬盘加载其他的数据页直到命中,这样子可以减少磁盘IO
的次数,提高性能。更新数据的时候也是一样,优先去Buffer Pool
中找,如果存在需要更新的数据就直接更新。然后会把“在某个数据页做了什么修改”记录到重做日志缓存(redo log buffer
)里,在刷盘的时候会写入redo log
日志文件里。
读取页 操作:
修改页操作:
脏页:就发生在修改这个操作中,如果缓冲池中的页已经被修改了,但是还没有刷新到磁盘上,那么我们就称缓冲池中的这页是 ”脏页“,即缓冲池中的页的版本要比磁盘的新。
缓冲池的大小直接影响着数据库的整体性能。
| 每条redo记录由“表空间号+数据页号+偏移量+修改数据长度+具体修改的数据”组成|
后台线程的主要作用就是刷新内存池中的数据,保证内存池中缓存的是最近的数据;此外将已修改的数据文件刷新到磁盘文件,同时保证在数据库发生异常的情况下 InnoDB 能恢复到正常运行状态。
当缓冲池中的某页数据被修改后,该页就被标记为 ”脏页“,脏页的数据会被定期刷新到磁盘上。
倘若每次一个页发生变化,就将新页的版本刷新到磁盘,那么这个开销是非常大的。并且,如果热点数据都集中在某几个页中,那么数据库的性能将变得非常差。另外,如果在从缓冲池将页的新版本刷新到磁盘时发生了宕机,那么这个数据就不能恢复了。
所以,为了避免发生数据丢失的问题,当前事务数据库系统(并非 MySQL 所独有)普遍都采用了 WAL(Write Ahead Log
,预写日志)策略:即当事务提交时,先写重做日志(redo log),再修改页(先修改缓冲池,再刷新到磁盘);当由于发生宕机而导致数据丢失时,通过 redo log 来完成数据的恢复。这也是事务 ACID 中 D(Durability 持久性)的要求。
有了 redo log,InnoDB 就可以保证即使数据库发生异常重启,之前提交的记录都不会丢失,这个能力称为 crash-safe。
理想情况下,事务一提交就会进行刷盘操作,但是实际上是刷盘的时机是根据策略来决定的。
InnoDB
存储引擎为redo log
的刷盘策略提供了innodb_flush_log_at_trx_commit
参数,它支持三种策略:
redo log buffer
写入page cache
。innodb_flush_log_at_trx_commit
参数默认为1,当事务提交的时候会调用fsync
对redo log
进行刷盘,将redo log buffer
写入redo log
文件中。
另外,Innodb
存储引擎有一个后台线程,每隔1
秒,就会把会redo log buffer
中的内容写入到文件系统缓存page cache
,然后调用fsync
刷盘。(因此这三种策略都会有刷盘)
1、innodb_flush_log_at_trx_commit = 0
如果宕机了或者MySQL
挂了可能造成1
秒内的数据丢失。
2、innodb_flush_log_at_trx_commit = 1
只要事务提交成功,redo log
记录就一定在磁盘里,不会有任务数据丢失。
如果执行事务的时候MySQL
挂了或者宕机了,这部分日志丢失了,但是因为事务没有提交,所以日志丢了也不会有损失。
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
文件大小都是一样的(至少需要两个)。它采用的是环形数组形式,从头开始写,写到末尾回到头循环写,如下图所示:
在日志文件组中有两个重要的属性,分别是witre 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 pos
和 checkpoint
之间的还空着的部分可以用来写入新的 redo log
记录。
如果 witre pos
追上checkpoint
,表示日志文件组满了,这时候不能再写入新的redo log
记录,MySQL
得停下来,清空一些记录,把checkpoint
推进一下。
有了 redo log 我们仍然面临这样 3 个问题:
1)缓冲池不是无限大的,也就是说不能没完没了的存储我们的数据等待一起刷新到磁盘
2)redo log 是循环使用而不是无限大的(也许可以,但是成本太高,同时不便于运维),那么当所有的 redo log file 都写满了怎么办?
3)当数据库运行了几个月甚至几年时,这时如果发生宕机,重新应用 redo log 的时间会非常久,此时恢复的代价将会非常大。
因此 Checkpoint 技术的目的就是解决上述问题:
(最频繁使用的页在 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
刷盘?不都是刷盘吗?有什么区别?
实际上,数据页大小是16KB
,刷盘比较耗时,可能就修改了数据页的几byte
数据,没有必要把整页的数据刷盘。而且数据页刷盘都是随机写,因为一个数据页对应的位置可能是在硬盘文件的随机位置,所以性能很差。
如果是写redo log
,一行记录就占了几十byte
,只要包含了表空间号、数据页号、磁盘文件偏移量、修改值,再加上是顺序写,所以刷盘效率很高。
所以用 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 是循环写入的。
总结:写的内容少,目的一样, 性能好
redo log
是物理日志,记录的是“在某个数据页做了什么修改”,属于Innodb
存储引擎。
而binlog
日志是逻辑日志,记录内容是语句的原始逻辑,属于MySQL Server
层。所有的存储引擎只要发生了数据更新,都会产生binlog
日志。
binlog
日志的作用可以说MySQL
数据库的数据备份、主备、主主、住从都离不开binlog
,需要依赖binlog
来同步数据,保证数据一致性。binlog
会记录所有涉及更新数据的逻辑规则,并且按顺序写。
可以通过binlog_format
参数设置,有以下三种:
1、statement记录SQL语句原文,但是有个问题,比如update T set update_time = now() where id = 1,更新的是当前系统的时间,可能和原来数据库的数据不一样,所以->row
2、记录的不再是简单的SQL
语句了,还包含了操作的具体数据,记录内容如下
但是这种格式需要大量的容量来记录,比较占用空间,恢复与同步时会更消耗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
日志刷盘流程如下:
write
,是指把日志写入到文件系统的page cache
,并没有把数据持久化硬盘,所以速度比较快。 fsync
才是将数据库持久化到硬盘的操作。write
和fsync
的时机可以由参数sync_binlog
控制,可以配置成0、1、N(N>1)
。
write
,由系统自行判断什么时候执行fsync
。fsync
,就和redo log
日志刷盘流程一样。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 log
与binlog
两块日志,以基本的事务为单位,redo log
在事务执行过程中可以不断写入(提交事务后刷盘),而binlog
日志只有在提交事务的时候才会写入,所以它们写入的时机不一样。
思考一个问题,如果redo log和binlog两份日志之间的逻辑不一样,会出现什么问题呢?MySQL是怎么解决这个问题的呢?
比如有这样一个场景,假设有这么一条语句update T set c = 1 where id = 2
(c原值为0),假如执行过程中写完redo log
日志后,在写入binlog
的时候发生了异常,会出现什么情况呢?
如下图:
由于binlog
日志没写完就异常,这个时候binlog
日志里面没有对应的修改记录,之后使用binlog
同步的数据的时候就会少这一次的更新,这一行数据c = 0
,而原库使用redo log
日志恢复(恢复数据库),这一行数据c = 1
,最终数据不一致。如下图:
为了解决两份日志之间的逻辑不一致的问题,InnoDB
存储引擎使用两阶段提交方案。
将redo log
日志的写入拆分成两个步骤prepare
和commit
,如下图:
使用两阶段提交后,写入binlog
时发生异常也没关系,因为MySQL
根据redo log
日志恢复数据时,发现redo log
日志处于prepare
阶段,并且没有对应binlog
日志(根据事务id对应),所以就会回滚事务。
本质就是把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
认为是完整的,所以不会回滚事务。
想要保证事务的原子性,就需要在发生异常时,对已经执行的操作进行回滚,在MySQL
中恢复机制是通过undo log
(回滚日志)实现的,所有事务进行的修改都会先被记录到这个回滚日志,然后再执行其他相关的操作。如果执行过程中遇到异常的话,我们直接利用回滚日志中的信息将数据回滚到修改之前的样子。并且,回滚日志会先于数据持久化到磁盘上。这样就保证了即使遇到数据库突然宕机等情况,当用户再次启动数据库的时候,数据库还能够通过查询回滚日志来回滚将之前未完成的事务。
另外,MVCC
的实现依赖:隐藏字段、Read View
、undo 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 在可重复读级别解决了幻读问题,是通过行锁和间隙锁的组合 Next-Key 锁实现的。
InnoDB是基于索引来完成行锁
\1. 原⼦性(Atomicity): 事务要么全部发生,要么全部不发生
\2. ⼀致性(Consistency): 执⾏事务前后,数据保持⼀致
\3. 隔离性(Isolation): ,⼀个⽤户的事务不被其他事务所⼲扰
\4. 持久性(Durability): ⼀个事务被提交之后。它对数据库中数据的改变是持久的,
脏读 写中读 事务 A 读取了事务 B 更新的数据,然后 B 回滚操作,那么 A 读取到的数据是脏数据
丢失修改 写写
不可重复读:两次读的不一样 读写读
幻读:突然有数据插入
实际上就是数据控制和数据一致性的一个平衡
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数据库一般都是集群部署的,会有主库、从库。主库负责写,从库负责读。主库写入之后,会进行主从复制,把数据同步到从库。
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
隔离级别,并发度是会比RR
更好的,为什么呢?
因为RC隔离级别,加锁过程中,只需要对修改的记录加行锁。而RR隔离级别,还需要加Gap Lock
和Next-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的隔离级别是通过MVCC和锁机制来实现的。
MVCC
来实现。多用户环境下保证数据库完整性和一致性。
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也会隐式的创建一个索引,并使用这个索引实施记录锁。它会阻塞其他事务对这行记录的插入、更新、删除。
为了解决幻读问题,InnoDB引入了间隙锁(Gap Lock)
。间隙锁是一种加在两个索引之间的锁,或者加在第一个索引之前,或最后一个索引之后的间隙。它锁住的是一个区间,而不仅仅是这个区间中的每一条数据
Next-key锁是记录锁和间隙锁的组合,它指的是加在某条记录以及这条记录前面间隙上的锁。说得更具体一点就是:临键锁会封锁索引记录本身,以及索引记录之前的区间,即它的锁区间是前开后闭,比如(5,10]
。
如果一个会话占有了索引记录R的共享/排他锁,其他会话不能立刻在R之前的区间插入新的索引记录。
插入意向锁,是插入一行记录操作之前设置的一种间隙锁,这个锁释放了一种插入方式的信号。 它解决的问题:多个事务,在同一个索引,同一个范围区间插入记录时,如果插入的位置不冲突,不会阻塞彼此。
假设有索引值4、7,几个不同的事务准备插入5、6,每个锁都在获得插入行的独占锁之前用插入意向锁各自锁住了4、7之间的间隙,但是不阻塞对方因为插入行不冲突。以下就是一个插入意向锁的日志:
自增锁是一种特殊的表级别锁。它是专门针对AUTO_INCREMENT
类型的列,对于这种列,如果表中新增数据时就会去持有自增锁。简言之,如果一个事务正在往表中插入记录,所有其他事务的插入必须等待,以便第一个事务插入的行,是连续的主键值。
一个事务拥有(获得)悲观锁后,其他任何事务都不能对数据进行修改啦,只能等待锁被释放才可以执行。 select…for update
就是MySQL悲观锁的应用。
乐观锁的“乐观情绪”体现在,它认为数据的变动不会太频繁。因此,它允许多个事务同时对数据进行变动。实现方式:乐观锁一般会通过version版本号/时间戳判断记录是否被更改过,一般配合CAS算法实现。
这道面试题,一般需要分两种数据库隔离级别(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
排他锁(行锁,会锁住主键索引那一行)。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 来分,好处在于说,扩容的时候很简单,因为你只要预备好,给每个月都准备一个库就可以了,到了一个新的月份的时候,自然而然,就会写新的库了;缺点,但是大部分的请求,都是访问最新的数据。实际生产用 range,要看场景。
hash 分发,好处在于说,可以平均分配每个库的数据量和请求压力;坏处在于说扩容起来比较麻烦,会有一个数据迁移的过程,之前的数据需要重新计算 hash 值重新分配到不同的库或表
使得数据可以从一个数据库服务器复制到其他服务器上,在复制数据时,一个服务器充当主服务器(master),其余的服务器充当从服务器(slave)。
搞一个主库,挂多个从库,然后我们只写主库,读从库,然后主库会自动把数据给同步到从库上去。
基本原理流程,是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事件,从而与主数据库中的数据保持一致。
两个同步机制,一个是半同步复制,用来 解决主库数据丢失问题;
一个是并行复制,用来 解决主从同步延时问题。
半同步复制,也叫 semi-sync 复制,指的就是主库写入 binlog 日志之后,就会将强制此时立即将数据同步到从库,从库将日志写入自己本地的 relay log 之后,接着会返回一个 ack 给主库,主库接收到至少一个从库的 ack 之后才会认为写操作完成了。
**并行复制,**指的是从库开启多个线程,并行读取 relay log 中不同库的日志,然后并行重放不同库的日志,这是库级别的并行。
DDL:数据定义语言 CREATE(建表)、ALTER(增删字段)、DROP和TRUNCATE(删除表)
DML:数据操纵语言(insert、update、delete)
DQL:数据查询语言
explain:是否使用索引、使用什么索引、使用索引相关信息
1、加缓存 redis
2、sql语句+索引
3、主从复制、读写分离
4、垂直拆分 水平拆分
limit:1000000开始取10条
select * from table where age > 20 limit 1000000,10优化为=》
我们先来看下这个SQL的执行流程:
SQL变慢原因有两个:
limit 100000,10
,就会扫描100010行,而limit 0,10
,只扫描10行。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),所以速度会很快。
1、Load额外数据-2、修改索引,尽量命中3、数据量大?分表
1、将字段很多的表分解成多个表
因为当一个表的数据量很大时,会由于使用频率低的字段的存在而变慢。
2、增加中间表
对于需要经常联合查询的表,可以建立中间表以提高查询效率。
通过建立中间表,将需要通过联合查询的数据插入到中间表中,然后将原来的联合查询改为对中间表的查询。
3、增加冗余字段
合理的加入冗余字段可以提高查询速度。
表的规范化程度越高,表和表之间的关系越多,需要连接查询的情况也就越多,性能也就越差。
注意:
冗余字段的值在一个表中修改了,就要想办法在其他表中更新,否则就会导致数据不一致的问题。
top 命令观察是不是 MySQLd 占用导致的,不是,占用高进程杀死
是的话, show processlist,看看里面跑的 session 情况,找出消耗高的 sql,看是不是Index少了或者数据量大
kill掉,加索引、改sql、改内存参数,重新跑
session变多,限制连接数
1、限定数据范围
2、读写分离 主库负责写,从库负责读
3、垂直分区 优点:列数据变小 缺点:主键冗余,产生join
4、水平分区 每⼀⽚数据分散到不同的表或者库中,达到了分布式的⽬的。 ⽔平拆分可以⽀撑⾮常⼤的数据量
数据库分⽚的两种常⻅⽅案:
客户端代理: 分⽚逻辑在应⽤端,封装在jar包中,通过修改或者封装JDBC层来实现。
中间件代理: 在应⽤和数据中间加了⼀个代理层。分⽚逻辑统⼀维护在中间件服务中。
java线程池、 jdbc连接池、 redis连接池
数据库连接池:多个socket 的连接
为什么需要?:减少用户等待时间
自增 设置不同步长 有序 不好部署
redis生成
leaf分布式id 保证证全局唯⼀性、趋势递增、单调递增、信息安全
雪花算法 :分布式id mp
查询:
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。
总结:redolog—redolog_prepare----binlog—redolog_commit
这个就要依赖于 MySQL 的处理机制了,MySQL 的处理过程如下:
•判断 redo log 是否完整,如果判断是完整的,就立即提交。
•如果 redo log 只是预提交但不是 commit 状态,这个时候就会去判断 binlog 是否完整,如果完整就提交 redo log, 不完整就回滚事务。
这样就解决了数据一致性的问题
整个数据库加载在内存当中操作,定期通过异步操作把数据库中的数据flush到硬盘上进行保存,每秒10w次读写操作
优点:
读写性能高 Redis能读的速度是110000次/s,写的速度是81000次/s。
支持数据持久化 AOF RDB
支持事务
数据结构丰富:hash set zset list string
支持主从复制和读写分离 主机会自动将数据同步到从机,可以进行读写分离。
支持发布订阅 pub-sub,通知,key过期等特性
缺点:
数据库容量受到物理内存的限制,不能用作海量数据的高性能读写,因此Redis适合的场景主要局限在较小数据量的高性能操作和运算上。
主机宕机,宕机前有部分数据未能及时同步到从机,切换IP后还会引入数据不一致的问题,降低了系统的可用性。
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,而是自己实现。
共同点 :
基于内存 过期策略 性能
区别 :
数据类型: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会是不错的选择。
用户请求数据在缓存直接范围,不在,查数据库,数据库在,更新缓存,不在,返回空
从高并发上来说:
从高性能上来说:
总结:
提升⽤户体验以及应对更多的⽤户。------》 ⾼性能”和“⾼并发
高性能:从硬盘取太慢 从缓存快
⾼并发: MySQL QPS (服务器每秒可以执⾏的查询次数 )⼤概都在 1w 左右(4 核 8g) ,但是使⽤ Redis 缓存之后
单机 10w- 30w, redis 集群的话会更⾼。
缓存分为本地缓存和分布式缓存。以java为例,使用自带的map或者guava实现的是本地缓存,最主要的特点是轻量以及快速,生命周期随着jvm的销毁而结束,并且在多实例的情况下,每个实例都需要各自保存一份缓存,缓存不具有一致性。
使用Redis或memcached之类的称为分布式缓存,在多实例的情况下,各实例共用一份缓存数据,缓存具有一致性。缺点是需要保持Redis或memcached服务的高可用,整个程序架构上较为复杂。
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) :礼物排⾏榜,弹幕消息
有五种常用数据类型: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中的数据写入到磁盘空间中,即持久化。
快照(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
从不同步具体地:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JxJ2wtT7-1681383922713)(https://raw.githubusercontent.com/viacheung/img/main/image/98987d9417b2bab43087f45fc959d32a-20230309232253633.png)]
劣势
aof
文件要远大于rdb
文件,恢复速度慢于rdb
aof
运行效率要慢于rdb
,每秒同步策略效率较好,不同步效率和rdb
相同https://juejin.cn/post/6844903750860013576
一致性hash算法可以保证当机器增加或者减少时,节点之间的数据迁移只限于两个节点之间,不会造成全局的网络问题。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0P8L2t2I-1681383922713)(https://raw.githubusercontent.com/viacheung/img/main/image/1545973609449.png)]
删除key常见的三种处理方式:
1、定时删除
在设置某个key 的过期时间同时,我们创建一个定时器,让定时器在该过期时间到来时,立即执行对其进行删除的操作。
优点:定时删除对内存是最友好的,能够保存内存的key一旦过期就能立即从内存中删除。
缺点:对CPU最不友好,在过期键比较多的时候,删除过期键会占用一部分 CPU 时间,对服务器的响应时间和吞吐量造成影响。
2、惰性删除
设置该key 过期时间后,我们不去管它,当需要该key时,我们在检查其是否过期,如果过期,我们就删掉它,反之返回该key。(代码层面)
优点:对 CPU友好,我们只会在使用该键时才会进行过期检查,对于很多用不到的key不用浪费时间进行过期检查。
缺点:对内存不友好,如果一个键已经过期,但是一直没有使用,那么该键就会一直存在内存中,如果数据库中有很多这种使用不到的过期键,这些键便永远不会被删除,内存永远不会释放。从而造成内存泄漏。(不用但存在)
3、定期删除
每隔一段时间,我们就对一些key进行检查,删除里面过期的key。
优点:可以通过限制删除操作执行的时长和频率来==减少删除操作对 CPU 的影响。==另外定期删除,也能有效释放过期键占用的内存。
缺点:难以确定删除操作执行的时长和频率。如果执行的太频繁,定期删除策略变得和定时删除策略一样,对CPU不友好。如果执行的太少,那又和惰性删除一样了,过期键占用的内存不会及时得到释放。
另外最重要的是,在获取某个键时,如果某个键的过期时间已经到了,但是还没执行定期删除,那么就会返回这个键的值,这是业务不能忍受的错误。
通过expire或pexpire命令,客户端可以以秒或毫秒的精度为数据库中的某个键设置生存时间。
与expire和pexpire命令类似,客户端可以通过expireat和pexpireat命令,以秒或毫秒精度给数据库中的某个键设置过期时间,可以理解为:让某个键在某个时间点过期。
命令:字符串:setex 其他 expire
有助于缓解内存的消耗
业务场景就是需要某个数据只在某⼀时间段内存在 ⽐如我们的短信验证码可能只在1分钟内有效,⽤户登录的 token 可能只在 1 天内有效。
如果使⽤传统的数据库来处理的话,⼀般都是⾃⼰判断过期,这样更麻烦并且性能要差很多。
过期字典 (hash表)键指向Redis数据库中的某个key(键),值是⼀个long long类型的整数(数据库键的过期时间)
仅仅通过给 key 设置过期时间不太够,还是有很多key没删掉,报oom,因此要用内存淘汰机制
内存淘汰策略可以通过配置文件来修改,Redis.conf对应的配置项是maxmemory-policy 修改对应的值就行,默认是noeviction。
Redis事务提供了⼀种将多个命令请求打包的功能。然后,再按顺序执⾏打包的所有命令,并且不会被中途打断。
Redis 是不⽀持 roll back 的,因⽽不满⾜原⼦性的(⽽且不满⾜持久性)。
为啥不支持回滚?
更简单便捷并且性能更好。 即使命令执⾏错误也应该在开发过程中就被发现⽽不是⽣产过程中。
缓存异常有四种类型,分别是缓存和数据库的数据不一致、缓存雪崩、缓存击穿和缓存穿透。
背景:使用到缓存,无论是本地内存做缓存还是使用 Redis 做缓存,那么就会存在数据同步的问题,因为配置信息缓存在内存中,而内存时无法感知到数据在数据库的修改。这样就会造成数据库中的数据与缓存中数据不一致的问题。
共有四种方案:
第一种和第二种方案,没有人使用的,因为第一种方案存在问题是:并发更新数据库场景下,会将脏数据刷到缓存。
第二种方案存在的问题是:如果先更新缓存成功,但是数据库更新失败,则肯定会造成数据不一致。
目前主要用第三和第四种方案。
该方案也会出问题,此时来了两个请求,请求 A(更新操作) 和请求 B(查询操作)
上述情况就会导致不一致的情形出现。而且,如果不采用给缓存设置过期时间策略,该数据永远都是脏数据。
(1)先淘汰缓存
(2)再写数据库(这两步和原来一样)
(3)休眠1秒,再次淘汰缓存,这么做,可以将1秒内所造成的缓存脏数据,再次删除。确保读请求结束,写请求可以删除读请求造成的缓存脏数据。自行评估自己的项目的读数据业务逻辑的耗时,写数据的休眠时间则在读数据业务逻辑的耗时基础上,加几百ms即可。
(我的理解:请求A先删缓存再往DB写数据,就算这时B来查数据库,缓存没数据,然后查DB,此时查到的是旧数据,写到缓存,A等待B写完之和再删缓存,这样就缓存一致)
如果使用的是 Mysql 的读写分离的架构的话,那么其实主从同步之间也会有时间差。
此时来了两个请求,请求 A(更新操作) 和请求 B(查询操作)
此时的解决办法就是如果是对 Redis 进行填充数据的查询数据库操作,那么就强制将其指向主库进行查询。
采用更新与读取操作进行异步串行化
异步串行化
我在系统内部维护n个内存队列,更新数据的时候,根据数据的唯一标识,将该操作路由之后,发送到其中一个jvm内部的内存队列中(对同一数据的请求发送到同一个队列)。读取数据的时候,如果发现数据不在缓存中,并且此时队列里有更新库存的操作,那么将重新读取数据+更新缓存的操作,根据唯一标识路由之后,也将发送到同一个jvm内部的内存队列中。然后每个队列对应一个工作线程,每个工作线程串行地拿到对应的操作,然后一条一条的执行。
这样的话,一个数据变更的操作,先执行删除缓存,然后再去更新数据库,但是还没完成更新的时候,如果此时一个读请求过来,读到了空的缓存,那么可以先将缓存更新的请求发送到队列中,此时会在队列中积压,排在刚才更新库的操作之后,然后同步等待缓存更新完成,再读库。
读操作去重
多个读库更新缓存的请求串在同一个队列中是没意义的,因此可以做过滤,如果发现队列中已经有了该数据的更新缓存的请求了,那么就不用再放进去了,直接等待前面的更新操作请求完成即可,待那个队列对应的工作线程完成了上一个操作(数据库的修改)之后,才会去执行下一个操作(读库更新缓存),此时会从数据库中读取最新的值,然后写入缓存中。
如果请求还在等待时间范围内,不断轮询发现可以取到值了,那么就直接返回;如果请求等待的时间超过一定时长,那么这一次直接从数据库中读取当前的旧值。(返回旧值不是又导致缓存和数据库不一致了么?那至少可以减少这个情况发生,因为等待超时也不是每次都是,几率很小吧。这里我想的是,如果超时了就直接读旧值,这时候仅仅是读库后返回而不放缓存)
这一种情况也会出现问题,比如更新数据库成功了,但是在删除缓存的阶段出错了没有删除成功,那么此时再读取缓存的时候每次都是错误的数据了。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IrOU7BJN-1681383922714)(https://raw.githubusercontent.com/viacheung/img/main/image/1735bb5881fb4a1b~tplv-t2oaga2asx-watermark.awebp)]
此时解决方案就是利用消息队列进行删除的补偿。具体的业务逻辑用语言描述如下:
但是这个方案会有一个缺点就是会对业务代码造成大量的侵入,深深的耦合在一起,所以这时会有一个优化的方案,我们知道对 Mysql 数据库更新操作后在binlog 日志中我们都能够找到相应的操作,那么我们可以订阅 Mysql 数据库的 binlog 日志对缓存进行操作。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5ClWLCY3-1681383922715)(https://raw.githubusercontent.com/viacheung/img/main/image/1735bb588215b298~tplv-t2oaga2asx-watermark.awebp)]
(热点key失效)
缓存击穿跟缓存雪崩有点类似,缓存雪崩是大规模的key失效,而缓存击穿是某个热点的key失效(及你太美),大并发集中对其进行请求,就会造成大量请求读缓存没读到数据,从而导致高并发访问数据库,引起数据库压力剧增。这种现象就叫做缓存击穿。
从两个方面解决,第一是否可以考虑热点key不设置过期时间,第二是否可以考虑降低打在数据库上的请求数量。
解决方案:
(缓存数据库都无)
缓存穿透是指用户请求的数据在缓存中不存在即没有命中,同时在数据库中也不存在,导致用户每次请求该数据都要去数据库中查询一遍。如果有恶意攻击者不断请求系统中不存在的数据,会导致短时间大量请求落在数据库上,造成数据库压力过大,甚至导致数据库承受不住而宕机崩溃。
缓存穿透的关键在于在Redis中查不到key值,它和缓存击穿的根本区别在于传进来的key在Redis中是不存在的。假如有黑客传进大量的不存在的key,那么大量的请求打在数据库上是很致命的问题,所以在日常开发中要对参数做好校验,一些非法的参数,不可能存在的key就直接返回错误提示。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lZinelA6-1681383922716)(https://raw.githubusercontent.com/viacheung/img/main/image/2021013117512340.png)]
解决方法:
当出现Redis查不到数据,数据库也查不到数据的情况,我们就把这个key保存到Redis中,设置value=“null”,并设置其过期时间极短,后面再出现查询这个key的请求的时候,直接返回null,就不需要再查询数据库了。但这种处理方式是有问题的,假如传进来的这个不存在的Key值每次都是随机的,那存进Redis也没有意义。
(准确度换空间?)
如果布隆过滤器==判定某个 key 不存在布隆过滤器中,那么就一定不存在,如果判定某个 key 存在,那么很大可能是存在(存在一定的误判率)。==于是我们可以在缓存之前再加一个布隆过滤器,将数据库中的所有key都存储在布隆过滤器中,在查询Redis前先去布隆过滤器查询 key 是否存在,如果不存在就直接返回请求参数错误信息给客户端,不让其访问数据库,从而避免了对底层存储系统的查询压力。
如何选择:针对一些恶意攻击,攻击带过来的大量key是随机,那么我们采用第一种方案就会缓存大量不存在key的数据。那么这种方案就不合适了,我们可以先对使用布隆过滤器方案进行过滤掉这些key。所以,针对这种key异常多、请求重复率比较低的数据,优先使用第二种方案直接过滤掉。而对于空数据的key有限的,重复率比较高的,则可优先采用第一种方式进行缓存。
(大规模的key失效)
如果缓在某一个时刻出现大规模的key失效,那么就会导致大量的请求打在了数据库上面,导致数据库压力巨大,如果在高并发的情况下,可能瞬间就会导致数据库宕机。这时候如果运维马上又重启数据库,马上又会有新的流量把数据库打死。这就是缓存雪崩。
造成缓存雪崩的关键在于同一时间的大规模的key失效,主要有两种可能:
例子:秒杀开始 12 个⼩时之前,我们统⼀存放了⼀批商品到 Redis 中,设置的缓存过期时间也是 12 个⼩时,那么秒杀开始的时候,这些秒杀的商品的访问直接就失效了。导致的情况就是,相应的请求直接就落到了数据库上,就像雪崩⼀样可怕。
解决方案:
1、事前:
setRedis(Key,value,time + Math.random() * 10000);
,保证数据不会在同一时间大面积失效。2、事中:
3、事后:
开启Redis持久化机制,尽快恢复缓存数据,一旦重启,就能从磁盘上自动加载数据恢复内存中的数据。
缓存预热是指系统上线后,提前将相关的缓存数据加载到缓存系统。避免在用户请求的时候,先查询数据库,然后再将数据缓存的问题,用户直接查询事先被预热的缓存数据。
如果不进行预热,那么Redis初始状态数据为空,系统上线初期,对于高并发的流量,都会访问到数据库中, 对数据库造成流量的压力。
缓存预热解决方案:
缓存降级是指==缓存失效或缓存服务器挂掉的情况下,不去访问数据库,直接返回默认数据或访问服务的内存数据。==降级一般是有损的操作,所以尽量减少降级对于业务的影响程度。
在进行降级之前要对系统进行梳理,看看系统是不是可以丢卒保帅;从而梳理出哪些必须誓死保护,哪些可降级;比如可以参考日志级别设置预案:
缓存失效时间变短(不推荐,治标不治本):我们让缓存数据的过期时间变短,这样的话缓存就会从数据库中加载数据。另外,这种解决办法对于先操作缓存后操作数据库的场景不适⽤
增加cache更新重试机制(常⽤) : 如果 cache 服务当前不可⽤导致缓存删除失败的话,我们就隔⼀段时间进⾏重试,重试次数可以⾃⼰定。如果多次重试还是失败的话,我们可以把当前更新失败的 key 存⼊队列中,等缓存服务可⽤之后,再将 缓存中对应的 key 删除即可
在Redis 6.0以前,Redis的核心网络模型选择用单线程来实现。先来看下官方的回答:
核心意思就是,对于一个 DB 来说,CPU 通常不会是瓶颈,因为大多数请求不会是 CPU 密集型的,而是 I/O 密集型。具体到 Redis的话,如果不考虑 RDB/AOF 等持久化方案,Redis是完全的纯内存操作,执行速度是非常快的,因此这部分操作通常不会是性能瓶颈,==Redis真正的性能瓶颈在于网络 I/O,也就是客户端和服务端之间的网络传输延迟,==因此 Redis选择了单线程的 I/O 多路复用来实现它的核心网络模型。
实际上更加具体的选择单线程的原因如下:
总而言之,Redis选择单线程可以说是多方博弈之后的一种权衡:在保证足够的性能表现之下,使用单线程保持代码的简单和可维护性。
讨论 这个问题前,先看下 Redis的版本中两个重要的节点:
所以,网络上说的Redis是单线程,通常是指在Redis 6.0之前,其核心网络模型使用的是单线程。
且Redis6.0引入多线程I/O,只是用来处理网络数据的读写和协议的解析,而执行命令依旧是单线程。
Redis在 v4.0 版本的时候就已经引入了的多线程来做一些异步操作,此举主要针对的是那些非常耗时的命令,通过将这些命令的执行进行异步化,避免阻塞单线程的事件循环。
在 Redisv4.0 之后增加了一些的非阻塞命令如
UNLINK
、FLUSHALL ASYNC
、FLUSHDB ASYNC
。
很简单,就是 Redis的网络 I/O 瓶颈已经越来越明显了。
随着互联网的飞速发展,互联网业务系统所要处理的线上流量越来越大,Redis的单线程模式会导致系统消耗很多 CPU 时间在网络 I/O 上从而降低吞吐量,要提升 Redis的性能有两个方向:
后者依赖于硬件的发展,暂时无解。所以只能从前者下手,网络 I/O 的优化又可以分为两个方向:
零拷贝技术有其局限性,无法完全适配 Redis这一类复杂的网络 I/O 场景,更多网络 I/O 对 CPU 时间的消耗和 Linux 零拷贝技术。而 DPDK 技术通过旁路网卡 I/O 绕过内核协议栈的方式又太过于复杂以及需要内核甚至是硬件的支持。
总结起来,Redis支持多线程主要就是两个原因:
Redis 作者 antirez 在 RedisConf 2019 分享时曾提到:Redis 6 引入的多线程 IO 特性对性能提升至少是一倍以上。
国内也有大牛曾使用 unstable 版本在阿里云 esc 进行过测试,GET/SET 命令在 4 线程 IO 时性能相比单线程是几乎是翻倍了。
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 会产生不同的事件,不同的事件对应着不同的操作,IO 多路复用程序监听着这些 Socket,当这些 Socket 产生了事件,IO 多路复用程序会将这些事件放到一个队列中,通过这个队列,以有序、同步、每次一个事件的方式向文件时间分派器中传送。当事件处理器处理完一个事件后,IO 多路复用程序才会继续向文件分派器传送下一个事件。
下图是客户端与 Redis 通信的一次完整的流程:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-10d3zS9p-1681383922718)(https://raw.githubusercontent.com/viacheung/img/main/image/202105092153019692.png)]
流程简述如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qpNdICUp-1681383922719)(https://raw.githubusercontent.com/viacheung/img/main/image/image-20210828175543973.png)]
从实现机制可以看出,Redis 的多线程部分只是用来处理网络数据的读写和协议解析,执行命令仍然是单线程顺序执行。
所以我们不需要去考虑控制 Key、Lua、事务,LPUSH/LPOP 等等的并发及线程安全问题。
**相同点:**都采用了 Master 线程 -Worker 线程的模型。
不同点:Memcached 执行主逻辑也是在 Worker 线程里,模型更加简单,实现了真正的线程隔离,符合我们对线程隔离的常规理解。
而 Redis 把处理逻辑交还给 Master 线程,虽然一定程度上增加了模型复杂度,但也解决了线程并发安全等问题。
单线程如何监听来⾃客户端的⼤量连接?
答:Redis 通过IO 多路复⽤程序 来监听来⾃客户端的⼤量连接
好处:I/O 多路复⽤技术的使⽤让 Redis 不需要额外创建多余的线程来监听客户端的⼤量连接,降低了资源的消耗
为了提⾼⽹络 IO 读写性能 但也只是在⽹络数据的读写这类耗时操作上使⽤了, 执⾏命令仍然是单线程顺序执⾏
默认禁用,修改 redis 配置⽂件 redis.conf 开启
Redis的事务并不是我们传统意义上理解的事务,我们都知道 单个 Redis 命令的执行是原子性的,但 Redis 没有在事务上增加任何维持原子性的机制,所以 Redis 事务的执行并不是原子性的。
事务可以理解为一个打包的批量执行脚本,但批量指令并非原子化的操作,中间某条指令的失败不会导致前面已做指令的回滚,也不会造成后续的指令不做。(没回滚)
总结:
1. Redis事务中如果有某一条命令执行失败,之前的命令不会回滚,其后的命令仍然会被继续执行。鉴于这个原因,所以说Redis的事务严格意义上来说是不具备原子性的。
2. Redis事务中所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。
3. 在事务开启之前,如果客户端与服务器之间出现通讯故障并导致网络断开,其后所有待执行的语句都将不会被服务器执行。然而如果网络中断事件是发生在客户端执行EXEC命令之后,那么该事务中的所有命令都会被服务器执行。
当使用Append-Only模式时,Redis会通过调用系统函数write将该事务内的所有写操作在本次调用中全部写入磁盘。然而如果在写入的过程中出现系统崩溃,如电源故障导致的宕机,那么此时也许只有部分数据被写入到磁盘,而另外一部分数据却已经丢失。Redis服务器会在重新启动时执行一系列必要的一致性检测,一旦发现类似问题,就会立即退出并给出相应的错误提示。此时,我们就要充分利用Redis工具包中提供的Redis-check-aof工具,**该工具可以帮助我们定位到数据不一致的错误,并将已经写入的部分数据进行回滚。**修复之后我们就可以再次重新启动Redis服务器了。
事务执行过程中,如果服务端收到有EXEC、DISCARD、WATCH、MULTI之外的请求,将会把请求放入队列中排队.
Redis事务功能是通过MULTI、EXEC、DISCARD和WATCH 四个原语实现的
Redis 是单进程程序,并且它保证在执行事-务时,不会对事务进行中断,事务可以运行直到执行完所有事务队列中的命令为止。因此,Redis 的事务是总是带有隔离性的。
Redis的几种常见使用方式包括:
使用场景:
如果数据量很少,主要是承载高并发高性能的场景,比如缓存一般就几个G的话,单机足够了。
主从模式:master 节点挂掉后,需要手动指定新的 master,可用性不高,基本不用。
哨兵模式:master 节点挂掉后,哨兵进程会主动选举新的 master,可用性高,但是每个节点存储的数据是一样的,浪费内存空间。数据量不是很多,集群规模不是很大,需要自动容错容灾的时候使用。
Redis cluster 主要是针对海量数据+高并发+高可用的场景,如果是海量数据,如果你的数据量很大,那么建议就用Redis cluster,所有master的容量总和就是Redis cluster可缓存的数据容量。
Redis单副本,采用单个Redis节点部署架构,没有备用节点实时同步数据,不提供数据持久化和备份策略,适用于数据可靠性要求不高的纯缓存业务场景。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9W9wWm73-1681383922720)(https://raw.githubusercontent.com/viacheung/img/main/image/image-20210829103307048.png)]
优点:
缺点:
方便解耦,简化开发
AOP 编程的支持
声明式事务的支持
方便程序的测试
方便集成各种优秀框架
Spring的开发步骤 用于配置对象交由Spring 来创建。默认情况下它调用的是类中的无参构造函数,如果没有无参构造函数则不能创建成功。 id: Bean实例在Spring容器中 默认值,单例的 init-method:指定类中的初始化方法名称 destroy-method:指定类中销毁方法名称 无参构造方法实例化 通过控制反转,把对象的创建交给了 Spring,但IOC 解耦只是降低他们的依赖关系,但不会消除。例如:业务层仍会调用持久层的方法。 对象注入哪几种方式? [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(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)] [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(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)] 普通属性呢? 普通数据类型 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AjhQafH8-1681383922725)(https://raw.githubusercontent.com/viacheung/img/main/image/image-20230317210715447.png)] 一样 也是 set方法 this.age=age,然后直接输出age 容器中某一类型的Bean有多个–用id 有一个–用class BeanFactory和ApplicationContext的优缺点分析: BeanFactory的优缺点: ApplicationContext的优缺点: bean 所需的依赖项和服务在 XML 格式的配置文件中指定。这些配置文件通常包含许多 bean 定义和特定于应用程序的配置选项。它们通常以 bean 标签开头。 通过在相关的类,方法或字段声明上使用注解,将 bean 配置为组件类本身,而不是使用 XML 来描述 bean 装配。默认情况下,Spring 容器中未打开注解装配。因此需要在使用它之前在 Spring 配置文件中启用它。例如: Spring 的 Java 配置是通过使用 @Bean 和 @Configuration 来实现。 IOC 就是控制反转,通俗的说就是我们不用自己创建实例对象,这些都交给Spring的bean工厂帮我们创建管理。 这也是Spring的核心思想,通过面向接口编程的方式来是实现对业务组件的动态依赖。这就意味着IOC是Spring针对解决程序耦合而存在的。在实际应用中,Spring通过配置文件(xml或者properties)指定需要实例化的java类(类名的完整字符串),包括这些java类的一组初始化值,通过加载读取配置文件,用Spring提供的方法(getBean())就可以获取到我们想要的根据指定配置进行初始化的实例对象。 DI:DI—Dependency Injection,即“依赖注入”:组件之间依赖关系由容器在运行期决定,形象的说,即由容器动态的将某个依赖关系注入到组件之中。依赖注入的目的并非为软件系统带来更多功能,而是为了提升组件重用的频率,并为系统搭建一个灵活、可扩展的平台。通过依赖注入机制,我们只需要通过简单的配置,而无需任何代码就可指定目标需要的资源,完成自身的业务逻辑,而不需要关心具体的资源来自何处,由谁实现。 核⼼技术 :依赖注⼊(DI), AOP,事件(events),资源, 验证,数据绑定, Spring Core: 基础,可以说 Spring 其他所有的功能都需要依赖于该类库。主要提供 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 的底层是通过 Spring 提供的的动态代理技术实现的。在运行期间, Spring通过动态代理技术动态的生成代理对象,代理对象方法执行时进行增强功能的介入,在去调用目标对象的方法,从而完成功能的增强 JDK 代理 : 基于接口的动态代理技术 采用哪种? 在 spring 中,框架会根据目标类是否实现了接口来决定采用哪种动态代理的方式 面向切面编程,将公共的代码逻辑抽象出来变成一个切面,然后注入到目标对象(具体业务)中去,通过动态代理的方式,将需要注入切面的对象进行代理,在进行调用的时候,将公共的逻辑直接添加进去,而不需要修改原有业务的逻辑代码,只需要在原来的业务逻辑基础之上做一些增强功能即可。 Spring AOP就是基于动态代理的, 如果要代理的对象,实现了某个接⼝,那么Spring AOP会使(基于代理对象的接口) 会使⽤ Cglib ⽣成⼀个被代理对象的⼦类来作为代理,如下图所示: (基于代理对象的子类) ① 创建目标接口和目标类(内部有切点)(一个接口 一个实现类) 面向切面编程, 它与 OOP( Object-Oriented Programming, 面向对象编程) 相辅相成, 提供了与 OOP 不同的抽象软件结构的视角. 在 OOP 中, 我们以类(class)作为我们的基本单元, 而 AOP 中的基本单元是 Aspect(切面) 主要分为两大类: Spring AOP 属于运⾏时增强,⽽ AspectJ 是编译时增强。 singleton : 唯⼀ bean 实例, Spring 中的 bean 默认都是单例的。 主要是因为当多个线程操作同⼀个对象的时候,对这个对象的⾮静态成员变量的写操作会存在线程安全问题。 在类中定义⼀个ThreadLocal成员变量,将⾮静态成员变量保存在 ThreadLocal 中(推荐的⼀种⽅式)。 作线程隔离 @Component 注解作⽤于类,⽽ @Bean 注解作⽤于⽅法。 @Component 通常是通过类路径扫描⾃动装配到Spring容器中( @ComponentScan 注解定义要扫描的路径) 引⽤第三⽅库中的类需要装配到 Spring 容器时,则只能通过@Bean 来实现 resource 按name autowired 按类型 +quirfy 按name 我们一般使用 @Autowired 注解自动装配 bean,要想把类标识成可用于 @Autowired 注解自动装配的 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方法,则调用它,如果实现了自定义的销毁方法,则调用之。 客户端–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 对各个组件的职责划分的比较清晰。 将 bean 用作另一个 bean 的属性时,才能将 bean 声明为内部 bean,假设我们有一个 Student 类,其中引用了 Person 类,就可以这样。 当 bean 在 Spring 容器中组合在一起时,它被称为装配或 bean 装配。 Spring 容器需要知道需要什么 bean 以及容器应该如何使用依赖注入来将 bean 绑定在一起,同时装配 bean。 Spring 容器能够自动装配 bean。也就是说,可以通过检查 BeanFactory 的内容让 Spring 自动解析 bean 的协作者。 自动装配的不同模式: 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的实例对象”这种循环依赖的情况。 A首先完成了初始化的第一步(createBeanINstance实例化),并且将自己提前曝光到singletonFactories(三级缓存)中。 此时进行初始化的第二步,发现自己依赖对象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中。 此时返回A中,A此时能拿到B的对象顺利完成自己的初始化阶段2、3,最终A也完成了初始化,进去了一级缓存singletonObjects中,而且更加幸运的是,由于B拿到了A的对象引用,所以B现在hold住的A对象完成了初始化。 当多个用户同时请求一个服务时,容器会给每一个请求分配一个线程,这时多个线程会并发执行该请求对应的业务逻辑(成员方法),此时就要注意了,如果该处理逻辑中有对单例状态的修改(体现为该单例的成员属性),则必须考虑线程同步问题。 线程安全问题都是由全局变量及静态变量引起的。 若每个线程中对全局变量、静态变量只有读操作,而无写操作,一般来说,这个全局变量是线程安全的;若有多个线程同时执行写操作,一般都需要考虑线程同步,否则就可能影响线程安全. 无状态bean和有状态bean 在spring中无状态的Bean适合用不变模式,就是单例模式,这样可以共享实例提高性能。有状态的Bean在多线程环境下不安全,适合用Prototype原型模式。 Spring使用ThreadLocal解决线程安全问题。如果你的Bean有多种状态的话(比如 View Model 对象),就需要自行保证线程安全 。 ⼯⼚设计模式 : Spring使⽤⼯⼚模式通过 BeanFactory 、 ApplicationContext(应用上下文) 创建 bean 对象。 PlatformTransactionManager :是 spring 的事务管理器,它里面提供了我们常用的操作事务的方法 getTransaction commit rollback 编程式事务,在代码中硬编码。 (不推荐使⽤) 声明式作用:事务管理不侵入开发的组件 事务管理是属于系统层面的服务,而不是业务逻辑的一部分 在不需要事务管理的时候,只要在设定文件上修改一下,即可移去事务管理服务,无需改变代码重新编译,这样维护起来极其方便 注意: Spring 声明式事务控制底层就是AOP。 基于xml 平台事务管理器配置 基于注解 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GyTlBd1C-1681383922731)(https://raw.githubusercontent.com/viacheung/img/main/image/image-20230317221224522.png)] 平台事务管理器配置(xml方式) 1、使⽤后端数据库默认的隔离级别 2、读未提交 3、读已提交 4、可重复读 5、串行化(将严重影响程序的性能 ) ⽀持当前事务的情况 : 如果当前存在事务 加入该事务,否则(1、创建⼀个新的事务 2、以⾮事务的⽅式继续运⾏ 3、抛出异常) 不⽀持当前事务 : 把当前事务挂起 然后: 1、创建⼀个新的事务 2、以⾮事务的⽅式继续运⾏ 3、抛出异常 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YfjVVHfQ-1681383922732)(https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/7e92221a76ba45aca924d0ce87c3dea6~tplv-k3u1fbpfcp-zoom-in-crop-mark:1512:0:0:0.awebp)] 如果一个方法被声明为 事务不生效的原因: 事务是通过 Spring事务的底层,还是依赖于数据库本身的事务支持。在 事务不生效的原因: 解决方案:选择正确的事务传播机制。 事务失效原因:这是因为 在Spring事务管理器中,通过 标记一个类为 Spring Web MVC 控制器 Controller,springmvc扫描到有该注解的类,然后这个类有@RequestMapping注解的方法,为这个方法生成一个处理器对象 将请求和方法进行映射,可以作用于类也可以作用于方法,作用于类的话一般就是控制器URI前缀、 当然,返回什么样的数据格式,根据客户端的 两个注解都用于方法参数,获取参数值的方式不同, 参数绑定注解@requestParam [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-So6mkgGr-1681383922733)(https://raw.githubusercontent.com/viacheung/img/main/image/image-20230317213825953.png)] @Controller 返回⼀个⻚⾯ 只返回视图 属于⽐较传统的Spring MVC 的应⽤,对应于前后端不分离的情况 @RestController 返回JSON 或 XML 形式数据 只返回对象,以 JSON 或 XML 形式写⼊ Response中,(前后端分离)。 @Controller +@ResponseBody =@RestController **@ResponseBody ** :Controller 的⽅法返回的对象通过适当的转换器转换为指定的格式(json xml)之后,写⼊到HTTP 响应(Response)对象的 body 中, GET:用于获取资源 当 @Transactional 注解作⽤于类上时,该类的所有 public ⽅法将都具有该类型的事务属性,如果类或者⽅法加了这个注解,那 如果不配置 rollbackFor 属性,那么事物只会在遇到 RuntimeException 的时候才会回滚,加上 rollbackFor=Exception.class ,可以让事物在遇到⾮运⾏时异常时也回滚。 替代xml里面的配置 xml里面一个个都是容器 然后注入就是拿容器返回的值(getBean) 获取容器 ------也就是获取上下文 Spring的处理程序映射机制包括处理程序拦截器,当你希望将特定功能应用于某些请求时,例如,检查用户主题时,这些拦截器非常有用。拦截器必须实现org.springframework.web.servlet包的HandlerInterceptor。此接口定义了三种方法: 入口不同 配置映射不同, 视图不同 REST 代表着抽象状态转移,它是根据 HTTP 协议从客户端发送数据到服务端,例如:服务端的一本书可以以 XML 或 JSON 格式传递到客户端 REST 接口是通过 HTTP 方法完成操作 所以,是否安全的界限,在于是否修改服务端的资源 是的,REST API 应该是无状态的,因为它是基于 HTTP 的,它也是无状态的 REST API 中的请求应该包含处理它所需的所有细节。它不应该依赖于以前或下一个请求或服务器端维护的一些数据,例如会话 REST 规范为使其无状态设置了一个约束,在设计 REST API 时,你应该记住这一点 安全是一个宽泛的术语。它可能意味着消息的安全性,这是通过认证和授权提供的加密或访问限制提供的 REST 通常不是安全的,需要开发人员自己实现安全机制 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、自定义线程池 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:反射创建的对象返回数据 优点: 缺点: ${} 是 Properties ⽂件中的变量占位符 #{} 是 sql 的参数占位符 Dao接口即Mapper接口。接口的全限名就是映射文件中的namespace的值;接口的方法名,就是映射文件中Mapper的Statement的id值;接口方法内的参数,就是传递给sql的参数。Mapper接口是没有实现类的,当调用接口方法时,接口全限名+方法名的拼接字符串作为key值,可唯一定位一个MapperStatement。 Dao接口里的方法,是不能重载的,因为是全限名+方法名的保存和寻找策略。 Dao接口的工作原理是JDK动态代理,Mybatis运行时会使用JDK动态代理为Dao接口生成代理proxy对象,代理对象proxy会拦截接口方法,转而执行MappedStatement所代表的sql,然后将sql执行结果返回。 、 、 、 、 、、、、 例: com.mybatis3.mappers.StudentDao.findStudentById ,可以唯⼀找到 namespace(接口全限定名)为 com.mybatis3.mappers.StudentDao 下⾯ id = findStudentById 的 MappedStatement (接⼝⽅法内的参数,就是传递给 sql 的参数。 ) 在 Mybatis中,每⼀个 、 、 、 标签,都会被解析为⼀个 MappedStatement 对象 Dao 接⼝的⼯作原理是 JDK 动态代理, Mybatis 运⾏时会使⽤ JDK 动态代理为 Dao 接⼝⽣成代 1、若Dao层函数有多个参数,那么其对应的xml中,#{0}代表接收的是Dao层中的第一个参数,#{1}代表Dao中的第二个参数,以此类推。 2、使用@Param注解:在Dao层的参数中前加@Param注解,注解内的参数名为传递到Mapper中的参数名。 3、多个参数封装成Map,以HashMap的形式传递到Mapper中。 Dao 接⼝⾥的⽅法,是不能重载的,因为是全限名+⽅法名的保存和寻找策略 使用基于jdk的动态代理,为需要拦截的接口生成代理对象以实现接口方法拦截功能, 当执行这4种接口方法时,会进入拦截方法,invoke()方法 如何编写? 实现 Mybatis 的 Interceptor 接⼝并复写 intercept() ⽅法,然后在给插件编写注解,指定要拦截哪⼀个接⼝的哪些⽅法即可,记住,别忘了在配置⽂件中配置你编写的插件。 可以 Mybatis 动态 sql 可以让我们在 Xml 映射⽂件内,以标签的形式编写动态 sql,完成逻辑判断 执行原理:是根据表达式的值完成逻辑判断,并动态拼接sql的功能。 第⼀种:是使⽤ 标签,逐⼀定义列名和对象属性名之间的映射关系。 第⼆种:使⽤ sql 列的别名功能,将列别名书写为对象属性名,⽐如 T_NAME AS NAME, 有了列名与属性名的映射关系后, Mybatis 通过反射创建对象,同时使⽤反射给对象的属性逐⼀赋值并返回,那些找不到映射关系的属性,是⽆法完成赋值的 可以,把 selectOne() 修改为 selectList() 即可 关联对象查询,有两种实现⽅式,一种是两条sql 一种是嵌套(join) 具体如下: 去重复的原理是 标签内的 ⼦标签 1、 一级缓存:基于PerpetualCache的HashMap本地缓存,其存储作用域为Session,当Session flush或close之后,该Session中的所有Cache就将清空,默认打开一级缓存。 2、 二级缓存与一级缓存机制相同,默认也是采用PerpetualCache,HashMap存储,不同在于其存储作用域为Mapper(namespace),并且可自定义存储源,如Ehcache。默认打不开二级缓存,要开启二级缓存,使用二级缓存属性类需要实现Serializable序列化接口(可用来保存对象的状态),可在它的映射文件中配置。 对于缓存数据更新机制,当某一个作用域(一级缓存Session/二级缓存Namespace)进行了增/删/改操作后,默认该作用域下所有select中的缓存将被clear。 1、Mapper接口方法名和mapper.xml中定义的每个sql的id相同; 2、Mapper接口方法的输入参数类型和mapper.xml中定义的每个sql的parameterType类型相同; 3、Mapper接口方法的输出参数类型和mapper.xml中定义的每个sql的resultType的类型相同; 4、Mapper.xml文件中的namespace即是mapper接口的类路径。 使⽤ CGLIB 创建⽬标对象的代理对象,当调⽤⽬标⽅法时,进⼊拦截器⽅法,⽐ 意思就是set我先不做,等你get的时候我再带调用set 然后再get,比较懒 如果配置了 namespace,那么 id 可以重复;如果没有配置namespace,那么 id 不能重复; 原因就是 namespace+id 是作为 Map BatchExecutor 完成批处理。 ctional(rollbackFor = Exception.class)注解了解吗? 当 @Transactional 注解作⽤于类上时,该类的所有 public ⽅法将都具有该类型的事务属性,如果类或者⽅法加了这个注解,那 如果不配置 rollbackFor 属性,那么事物只会在遇到 RuntimeException 的时候才会回滚,加上 rollbackFor=Exception.class ,可以让事物在遇到⾮运⾏时异常时也回滚。 替代xml里面的配置 xml里面一个个都是容器 然后注入就是拿容器返回的值(getBean) 获取容器 ------也就是获取上下文 Spring的处理程序映射机制包括处理程序拦截器,当你希望将特定功能应用于某些请求时,例如,检查用户主题时,这些拦截器非常有用。拦截器必须实现org.springframework.web.servlet包的HandlerInterceptor。此接口定义了三种方法: 入口不同 配置映射不同, 视图不同 REST 代表着抽象状态转移,它是根据 HTTP 协议从客户端发送数据到服务端,例如:服务端的一本书可以以 XML 或 JSON 格式传递到客户端 REST 接口是通过 HTTP 方法完成操作 所以,是否安全的界限,在于是否修改服务端的资源 是的,REST API 应该是无状态的,因为它是基于 HTTP 的,它也是无状态的 REST API 中的请求应该包含处理它所需的所有细节。它不应该依赖于以前或下一个请求或服务器端维护的一些数据,例如会话 REST 规范为使其无状态设置了一个约束,在设计 REST API 时,你应该记住这一点 安全是一个宽泛的术语。它可能意味着消息的安全性,这是通过认证和授权提供的加密或访问限制提供的 REST 通常不是安全的,需要开发人员自己实现安全机制 1、注解(我项目里面用的) 配置类上加上@EnableAsync启动异步调用,使用了@Async标记的异步方法,可以带参可以带返回值 返回值必须以下类型: [外链图片转存中…(img-2oLwFZ4j-1681383922733)] 2、内置线程池 [外链图片转存中…(img-Ez6pyXni-1681383922734)] 3、自定义线程池 1、Mybatis是一个半ORM(对象关系映射)框架,它内部封装了JDBC,加载驱动、创建连接、创建statement等繁杂的过程,开发者开发时只需要关注如何编写SQL语句,可以严格控制sql执行性能,灵活度高 2、作为一个半ORM框架,MyBatis 可以使用 XML 或注解来配置和映射原
① 导入坐标(context )
② 创建Bean(编写Dao接口和实现类 )
③ 创建applicationContext.xml(类路径下(resources) )
④ 在配置文件中进行配置(Bean
class: Bean的全限定名称Bean实例化三种方式
工厂静态方法实例化
工厂实例方法实例化Bean的依赖注入
那这种业务层和持久层的依赖关系,在使用 Spring 之后,就让 Spring 来维护了。简单的说,就是坐等框架把持久层对象传入业务层,而不用我们自己去获取
private IUserDao userDao1;
public void setUserDao(IUserDao userDao1) {//这里注意,name方法与类中成员变量名和方法的参数名都无关,只与set方法名有关
this.userDao1 = userDao1;
}
构造注入vs setter注入
构造函数注入
setter 注入
没有部分注入
有部分注入
不会覆盖 setter 属性
会覆盖 setter 属性
任意修改都会创建一个新实例
任意修改不会创建一个新实例
适用于设置很多属性
适用于设置少量属性
引用数据类型
集合数据类型getBean
区分 BeanFactory 和 ApplicationContext?
BeanFactory
ApplicationContext
它使用懒加载
它使用即时加载
它使用语法显式提供资源对象
它自己创建和管理资源对象
不支持国际化
支持国际化
不支持基于依赖的注解
支持基于依赖的注解
spring提供了哪些配置方式
<bean id="studentbean" class="org.edureka.firstSpring.StudentBean">
<property name="name" value="Edureka">property>
bean>
<beans>
<context:annotation-config/>
beans>
元素相同的角色。@Configuration
public class StudentConfig {
@Bean
public StudentBean myStudent() {
return new StudentBean();
}
}
如何理解IoC和DI?
spring特征
测试 : Spring MVC 测试,
数据访问 :事务, DAO⽀持, JDBC, ORM,XML。
Web⽀持 : Spring MVC和Spring WebFlux Web框架。
集成 :远程处理, 电⼦邮件,任务,调度,缓存。列举⼀些重要的Spring模块?
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
种对象。aop
cglib 代理:基于父类的动态代理技术
⽤JDK Proxy,去创建代理对象,⽽对于没有实现接⼝的对象,基于注解的aop开发步骤:
② 创建切面类(内部有增强方法)
③ 将目标类和切面类的对象创建权交给 spring(xml 或者 注解)
④ 在切面类中使用注解配置织入关系(定义是哪个切面 目标类的方法用的切面类的哪个增强方法)
⑤ 在配置文件中开启组件扫描和 AOP 的自动代理
⑥ 测试什么是 AOP?
AOP 有哪些实现方式?
JDK
动态代理:通过反射来接收被代理的类,并且要求被代理的类必须实现一个接口 。JDK 动态代理的核心是 InvocationHandler 接口和 Proxy 类 。CGLIB
动态代理: 如果目标类没有实现接口,那么 Spring AOP
会选择使用 CGLIB
来动态代理目标类 。CGLIB
( Code Generation Library ),是一个代码生成的类库,可以在运行时动态的生成某个类的子类,注意, CGLIB
是通过继承的方式做的动态代理,因此如果某个类被标记为 final
,那么它是无法使用 CGLIB
做动态代理的。Spring AOP 和 AspectJ AOP 有什么区别?
Spring 中的 bean 的作⽤域有哪些?
prototype : 每次请求都会创建⼀个新的 bean 实例。
request : 每⼀次HTTP请求都会产⽣⼀个新的bean,该bean仅在当前HTTP request内有效。
session : 每⼀次HTTP请求都会产⽣⼀个新的 bean,该bean仅在当前 HTTP session 内有效。Spring 中的单例 bean 的线程安全问题了解吗(Threadlocal)?
常⻅的有两种解决办法:@Component 和 @Bean 的区别是什么?
Spring 的 bean 容器中)自动注入
将⼀个类声明为Spring的 bean 的注解有哪些?
Spring 中的 bean ⽣命周期?
创建过程
销毁过程
总结
Spring MVC ⼯作原理了解吗?
简单介绍 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)
DispatcherServlet
负责协调,其他组件则各自做分内之事,互不干扰。什么是 spring 的内部 bean?
什么是 spring 装配?
自动装配有什么局限?
和
设置指定依赖项,这将覆盖自动装配。Spring中出现同名bean怎么办?
Spring 怎么解决循环依赖问题?
Spring 中的单例 bean 的线程安全问题?
Spring 框架中⽤到了哪些设计模式?
代理设计模式 : Spring AOP 功能(动态代理)的实现。
单例设计模式 : Spring 中的 Bean 默认都是单例的。
包装器设计模式 : 我们的项⽬需要连接多个数据库,⽽且不同的客户在每次访问中根据需要会去访问不同的数据库。这种模式让我们可以根据客户的需求能够动态切换不同的数据源。
观察者模式: Spring 事件驱动模型就是观察者模式很经典的⼀个应⽤。(消息队列的发布订阅)
适配器模式 ==:Spring AOP 的增强或通知(Advice)==使⽤到了适配器模式、 spring MVC 中也是⽤到了适配器模式适配 Controller 。Spring 事务
Spring 管理事务的⽅式有⼏种?
声明式事务,在配置⽂件中配置(推荐使⽤) (分为基于xml 和基于注解)
事务通知的配置
事务aop织入的配置
事务通知的配置(@Transactional注解配置)
事务注解驱动的配置 tx:annotation-driven/Spring 事务中的隔离级别有哪⼏种?
Spring框架的事务管理有哪些优点?
Spring 事务中哪⼏种事务传播⾏为?
spring事务不生效的场景
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、 数据库的存储引擎不支持事务
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
)来实现线程安全。大家有兴趣的话,可以去看下源码哈.TransactionSynchronizationManager
类来管理事务上下文。TransactionSynchronizationManager
内部维护了一个ThreadLocal
对象,用来存储当前线程的事务上下文。在事务开始时,TransactionSynchronizationManager
会将事务上下文绑定到当前线程的ThreadLocal
对象中,当事务结束时,TransactionSynchronizationManager
会将事务上下文从ThreadLocal
对象中移除。@Controller 注解有什么用?
@RequestMapping 注解有什么用?
@RestController 和 @Controller 有什么区别?
@RestController
=@Controller
+ @ResponseBody
,更加适合目前前后端分离的架构下,提供 Restful API ,返回例如 JSON 数据格式。ACCEPT
请求头来决定。@RequestMapping 和 @GetMapping 注解的不同之处在哪里?
@RequestMapping
:可注解在类和方法上;@GetMapping
仅可注册在方法上@RequestMapping
: GET、POST、PUT、DELETE 等请求方法都可以用@GetMapping
是 @RequestMapping
的 GET 请求方法的特例,目的是为了提高区分度。@RequestParam 和 @PathVariable 两个注解的区别
@RequestParam
注解的参数从请求携带的参数中获取(请求头),而 @PathVariable
注解从请求的 URI 中(?后面的参数)@RestController vs @Controller
restful
POST:用于新建资源
PUT:用于更新资源
DELETE:用于删除资源返回 JSON 格式使用什么注解?
@ResponseBody
注解,or @RestController
(ResponseBody+Controller)@Transactional(rollbackFor = Exception.class)注解了解吗?
么这个类⾥⾯的⽅法抛出异常,就会回滚,数据库⾥⾯的数据也会回滚。注解开发
ApplicationContext ac = new ClassPathXmlApplicationContext("bean.xml");
IAccountService as = ac.getBean("accountService",IAccountService.class);
什么是springmvc拦截器以及如何使用它?
Spring MVC 和 Struts2 的异同?
REST
REST 代表着什么?
什么是安全的 REST 操作?
REST API 是无状态的吗?
REST安全吗? 你能做什么来保护它?
补充
spring中出现异步调用的方式?
MyBatis
MyBatis是什么?
Mybaits的优缺点
#{}和${}的区别是什么?
通常一个Xml映射文件,都会写一个Dao接口与之对应,那么这个Dao接口的工作原理是什么?Dao接口里的方法、参数不同时,方法能重载吗?
Xml 映射⽂件中有哪些标签?
Mapper接⼝的⼯作原理是什么?
理 proxy 对象,代理对象 proxy 会拦截接⼝⽅法,转⽽执⾏ MappedStatement 所代表的 sql,然后
将 sql 执⾏结果返回。在Mapper中如何传递多个参数?
Dao (Mapper)接⼝⾥的⽅法,参数不同时,⽅法能重载吗?
Mybatisplus 是如何进⾏分⻚的?分⻚插件的原理是什么?
简述 Mybatis 的插件运⾏原理,以及如何编写⼀个插件
Mybatis 执⾏批量插⼊,能返回数据库主键列表吗?
Mybatis 动态 sql 是做什么的?都有哪些动态 sql?能简述⼀下动态 sql 的执⾏原理不?
和动态拼接 sql 的功能, Mybatis 提供了 9 种动态 sql 标签
trim|where|set|foreach|if|choose|when|otherwise|bind 。
其执⾏原理为,使⽤ OGNL 从 sql 参数对象中计算表达式的值,根据表达式的值动态拼接 sql,以此来完成动态 sql 的功能。Mybatis 是如何将 sql 执⾏结果封装为⽬标对象并返回的?都有哪些映射形式?
Mybatis 能执⾏⼀对⼀、⼀对多的关联查询吗?都有哪些实现⽅式,以及它们之间的区别。
有联合查询和嵌套查询两种方式。
联合查询是几个表联合查询,通过在resultMap里面配置association节点配置一对一的类就可以完成;
嵌套查询是先查一个表,根据这个表里面的结果的外键id,再去另外一个表里面查询数据,也是通过association配置,但另外一个表的查询是通过select配置的。
Mybatis的一级、二级缓存(?)
使用MyBatis的Mapper接口调用时有哪些要求?
Mybatis 是否⽀持延迟加载(懒加载)?如果⽀持,它的实现原理是什么?
如调⽤ a.getB().getName() ,拦截器 invoke() ⽅法发现 a.getB() 是 null 值,那么就会单独发送事先保存好的查询关联 B 对象的 sql,把 B 查询上来,然后调⽤ a.setB(b),于是 a 的对象 b 属性就有值了,接着完成 a.getB().getName() ⽅法的调⽤。这就是延迟加载的基本原理。Mybatis 的 Xml 映射⽂件中,不同的 Xml 映射⽂件, id 是否可以重复?
Mybatis 中如何执⾏批处理?
么这个类⾥⾯的⽅法抛出异常,就会回滚,数据库⾥⾯的数据也会回滚。注解开发
ApplicationContext ac = new ClassPathXmlApplicationContext("bean.xml");
IAccountService as = ac.getBean("accountService",IAccountService.class);
什么是springmvc拦截器以及如何使用它?
Spring MVC 和 Struts2 的异同?
REST
REST 代表着什么?
什么是安全的 REST 操作?
REST API 是无状态的吗?
REST安全吗? 你能做什么来保护它?
补充
spring中出现异步调用的方式?
MyBatis
MyBatis是什么?