项目和简历
hr面试问题
自我介绍
面试官你好,我叫王首都,重庆邮电大学 计算机科学与技术专业研二在读,主要从事java后端开发,项目达人探店,它主要,实现了登录验证,缓存查询,优惠券秒杀,接口限流,以及签到打卡等功能。在上学期间获得了人民奖学金,新生奖学金,学业讲学金等。
你的项目是什么?能简单给我介绍一下么
达人探店平台是一个前后端分离项目,模仿大众点评,实现了发布查看商家,用户登录打卡,优惠券抢购等功能,业务可以帮助商家引流,增加曝光度,也可以为用户提供查看提供附近消费场所。
在线点餐平台是一款为餐饮企业定制的软件产品。该项目是一个在线外卖点餐订购系统,顾客可以通过网站订购点餐。
基于情感分析的社交网络影响力的传播研究:通过利用自然语言处理和图神经网络的社交检测算法,对社交网络进行研究,发现社交网络中的群体,以及群体中的情感值
遇到的最大的挫折是什么 ?
是我考研二战期间,我的母亲,摔伤,边复习一边陪着我的妈妈去市里做手术,在等待手术的过程中,一个人在外面焦急的等待,然后自己一个人照顾我妈,直到我妈出院。
平时有什么爱好
- 喜欢跑步,每周都会去跑步,现在坚持跑4公里,下下个月就要开始跑5公里
- 喜欢打乒乓球,我和我的师兄,师弟一起去打乒乓球,缓解生活
- 喜欢听歌,喜欢周杰伦的稻香,青花瓷,晴天。
你的优缺点是什么 ?
优点
- 坚持,比如现在坚持跑步,从开始2公里开始,到现在4公里。从7-8分钟每公里到现在5.30多每公里。在实习期间自己也会坚持跑步。
- 与人相处融洽:自己在上学的过程中,在初中,高中,大学,还有研究生期间,都和大家相处融洽,同时自己也很乐于助人,帮助对方。
- 乐于助人,在我上学的过程中,我都会帮助我的朋友,同学,他们提升他们的专业课,或者其他方面,有需要帮忙的地方,自己都会很积极帮助他们。
缺点
- 社会经验不够,公司技术流程不熟,所以想通过来实习弥补我的不足。我的学习能力不错,如果我有幸加入贵公司,相信我能够以最快的速度适应现在的工作。
- 之前不沉稳,但是经过考研和读研究生已经改了很多,之前代码出bug容易慌,但是现在很享受解决bug的过程以及解决之后的喜悦和成就感
实习的时长,最多可以实习到 10.1号
项目遇到的最大问题是什么?
我本科学的是信息安全,所以在做项目的时候,考虑到安全方面,在用redis缓存热门商品数据,如果存在非法的用户,构造非法的请求连接,访问不存在的数据,会导致redis缓存穿透,可能导致服务器宕机。
然后我通过查阅资料,了解布隆过滤器,利用布隆过滤器,来解决访问redis的穿透。当访问不存在的数据就返回null,为了防止服务器宕机,我又添加了一个保护措施,就是编写一个限流器,防止同一个ip短时间内大量非法请求.
项目问题一:session不共享问题
session 不共享
- 一台tomcat 中的所有请求是共享session 的,但是当访问另一台tomcat ,session 无法获取第一台tomcat 存储的数据。
解决办法1: 拷贝session:解决不共享问题
但是这种方案具有两个大问题
1、每台服务器中都有完整的一份session数据,服务器压力过大。
2、session拷贝数据时,可能会出现延迟
解决办法2: redis 共享
数据共享。
项目问题二:nignx 挂了怎么办
nginx 高可用
- 使用负载均衡:使用负载均衡器,将流量分发到多个Nginx服务器上,以实现负载均衡
- 使用主从复制:配置一个Nginx主服务器和多个Nginx从服务器,将主服务器上的配置文件和数据同步到从服务器上。主服务器处理请求,从服务器作为备份,当主服务器故障时,从服务器可以接管请求
- 使用热备份:配置一个Nginx主服务器和一个备份服务器,备份服务器实时同步主服务器的配置文件和数据。当主服务器故障时,备份服务器可以立即接管请求,实现高可用。
keepalive:集群管理nginx
keepalived 是集群管理中保证集群高可用的一个服务软件,用来防止单点故障。Keepalived的作用是检测 web 服务器的状态,如果有一台 web 服务器死机或工作出现故障,Keepalived 将能检测到,并将有故障的 web 服务器从系统中剔除,当web服务器工作正常后 Keepalived 会自动将该 web 服务器加入到服务器群中。
nginx 负载均衡算法
为了避免服务器崩溃,大家会通过负载均衡的方式来分担服务器压力。将对台服务器组成一个集群,当用户访问时,先访问到一个转发服务器,再由转发服务器将访问分发到压力更小的服务器
1 .轮询(默认)
每个请求按时间顺序逐一分配到不同的后端服务器。
2. 权重 weight
weight的值越大,分配到的访问概率越高,主要用于后端每台服务器性能不均衡的情况下
3. ip_hash( IP绑定)
每个请求按访问IP的哈希结果分配,使来自同一个IP的访客固定访问一台后端服务器,并且可以有效解决动态网页存在的session共享问题
Nginx解决前端跨域问题?
使用Nginx转发请求。把跨域的接口写成调本域的接口,然后将这些接口转发到真正的请求地址。
Nginx 代理解决前端跨域问题_ngix 代理解决跨域-CSDN博客
server {listen 80;server_name fe.server.com;location / {proxy_pass dev.server.com;}
}
限流算法
Nginx的限流都是基于漏桶流算法
项目问题 三:限流算法
漏桶算法
漏桶算法思路很简单,我们把水比作是请求,漏桶比作是系统处理能力极限,水先进入到漏桶里,漏桶里的水按一定速率流出,当流出的速率小于流入的速率时,由于漏桶容量有限,后续进入的水直接溢出(拒绝请求),以此实现限流
令牌桶算法
令牌桶算法的原理也比较简单,我们可以理解成医院的挂号看病,只有拿到号以后才可以进行诊病。
系统会维护一个令牌(token)桶,以一个恒定的速度往桶里放入令牌(token),这时如果有请求进来想要被处理,则需要先从桶里获取一个令牌(token),当桶里没有令牌(token)可取时,则该请求将被拒绝服务。令牌桶算法通过控制桶的容量、发放令牌的速率,来达到对请求的限制
项目问题4:全局唯一id
项目中唯一id 生成:64bit
ID的组成部分:符号位:1bit,永远为0
时间戳:31bit,以秒为单位,可以使用69年
序列号:32bit,秒内的计数器,支持每秒产生2^32个不同ID
uuid
UUID优点:
- 性能非常高:本地生成,没有网络消耗。
UUID缺点:
- 不易于存储:UUID太长,16字节128位,通常以36长度的字符串表示,很多场景不适用;
数据库自增id 和 redis 自增id
项目问题5:秒杀优惠
乐观锁解决超卖问题。:乐观锁适合读多写少。:更新数据
一人一单 加分布式锁。:悲观锁 适合读少写多。: 插入数据
秒杀流程: 时间是否开始-》 库存是否开始 -》根据 用户id和优惠id 查订单 -》扣减库存-》 创建订单。
项目问题6: 分布式锁
为什么用分布式锁
多个tomcat,每个tomcat都有一个属于自己的jvm
,那么假设在服务器A的tomcat内部,有两个线程,这两个线程由于使用的是同一份代码
,那么他们的锁对象是同一个,是可以实现互
。如果现在是服务器B的tomcat内部,又有两个线程
,但是他们的锁对象写的虽然和服务器A一样,但是锁对象却不是同一个
。
分布式锁条件
- 可见性
- 互斥
- 高可用
- 高性能
分布式锁实现3种方法
- mysql: 本身就带有锁机制,但是由于mysql性能本身一般mysql: 所以采用分布式锁的情况
- 利用setnx这个方法,如果插入key成功,则表示获得到了锁,如果有人插入成功,其他人插入失败则表示无法获得到锁,利用这套逻辑来实现分布式锁
redis 分布式锁误删除
持有锁的线程在锁的内部出现了阻塞,导致他的锁自动释放,这时其他线程,线程2来尝试获得锁,就拿到了这把锁,然后线程2在持有锁执行过程中**,线程1反应过来,继续执行,而线程1执行过程中,走到了删除锁逻辑,此时就会把本应该属于线程2的锁进行删除,这就是误删别人锁的情况说明。
解决方案:解决方案就是在每个线程释放锁的时候,去判断一下当前这把锁是否属于自己,如果属于自己,则不进行锁的删除,假设还是上边的情况,线程1卡顿,锁自动释放,线程2进入到锁的内部执行逻辑,此时线程1反应过来,然后删除锁, 但是线程1,一看当前这把锁不是属于自己,于是不进行删除锁逻辑,当线程2走到删除锁逻辑时,如果没有卡过自动释放锁的时间点,则判断当前这把锁是属于自己的,于是删除这把锁。
基础八股文
拦截器和过滤器 区别
- 出身不同
过滤器来自于 Servlet,而拦截器来自于 Spring 框架
- 触发时机不同
请求的执行顺序是:请求进入容器 > 进入过滤器 > 进入 Servlet > 进入拦截器 > 执行控制器(Controller)
-
实现原理
过滤器 是基于函数回调的,拦截器 则是基于Java的反射机制(动态代理)实现的。
-
拦截范围不同
拦截的请求范围不同:过滤器Filter执行了两次,拦截器Interceptor只执行了一次。这是因为过滤器几乎可以对所有进入容器的请
求起作用,而拦截器只会对Controller中请求或访问static目录下的资源请求起作用。
java 基础
集合类Collection和Map
1. 集合类组成
Java中的集合类主要由Collection和Map接口派生而出,Collection接口又派生出三个子接口,分别是Set、List、Queue 。Set、List、Queue、Map这四个接口的实现类
- Set代表无序的,元素不可重复的集合;
- List代表有序的,元素可以重复的集合;
- Queue代表先进先出(FIFO)的队列;
- Map代表具有映射关系(key-value)的集合
最常见的有:HashSet、TreeSet、ArrayList、LinkedList、ArrayDeque、HashMap、TreeMap
2. hashmap 和hashset 区别
1.HashMap实现了Map接口,而HashSet实现了Set接口。****
2.HashMap用于存储键值对,而HashSet用于存储对象。
3.HashMap不允许有重复的键,可以允许有重复的值。HashSet不允许有重复元素。
4.HashMap允许有一个键为空,多个值为空,HashSet允许有一个空值。
5.HashMap中使用put()将元素加入map中,而HashSet使用add()将元素放入set中。
6.HashMap比较快,因为其使用唯一的键来获取对象
HashMap和HashSet的区别-CSDN博客
hashMap面试题
0.请你说说HashMap和Hashtable的区别
-
Hashtable在实现Map接口时保证了线程安全性,而HashMap则是非线程安全的。所以,Hashtable的性能不如HashMap,因为为了保证线程安全它牺牲了一些性能。
-
Hashtable不允许存入null,无论是以null作为key或value,都会引发异常。而HashMap是允许存入null的,无论是以null作为key或value,都是可以的。 加分回答 虽然Hashtable是线程安全的,但仍然不建议在多线程环境下使用Hashtable。因为它是一个古老的API,从Java 1.0开始就出现了,它的同步方案还不成熟,性能不好。如果要在多线程环下使用HashMap,建议使用ConcurrentHashMap。它不但保证了线程安全,也通过降低锁的粒度提高了并发访问时的性能。
1. hashmap 的底层数据结构
jdk 1.7
由“数组+链表”组成,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的。
jdk 1.8
由“数组+链表+红黑树”组成。当链表过长,则会严重影响 HashMap 的性能,红黑树搜索时间复杂度是 O(logn),而链表是糟糕的 O(n)
链表和红黑树在达到一定条件会进行转换:
- 当链表超过 8 且数据总量超过 64 才会转红黑树。
- 将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树,以减少搜索时间。
Java集合 (javalearn.cn)
2. hashmap的扩容方式
什么时候进行扩容?
HashMap 中的元素个数超过 数组大小 * loadFactor 时,就会进行数组扩容,loadFactor(负载因子) 的默认值为 0.75
hashmap扩容机制
jdk 1.7
数组是无法自动扩容的,所以如果要扩容的话,就需要新建一个大的数组,然后把之前小的数组的元素复制过去,并且要重新计算哈希值和重新分配桶(重新散列)。
jdk 1.7 扩容过程
当超过 阈值, 通过resize()方法,新建一个数组new table, 是旧数组old table的2倍,通过transfer 方法,遍历旧数组元素,同时重新计算hash值,分配到新数组中,然后 将内部数组的引用指向新数组,重新计算阈值。
jdk 1.8 扩容机制修改
Java集合 (javalearn.cn)
3. 一般选什么作为hashmap 的key: 不变类
key 可以为null 吗?
必须可以,key为null的时候,hash算法最后的值以0来计算,也就是放在数组的第一个位置。
一般选用什么作为key
一般用Integer、String 这种不可变类当 HashMap 当 key,而且 String 最为常用
- 因为字符串是不可变的,所以在它创建的时候 hashcode 就被缓存了,不需要重新计算
- 因为获取对象的时候要用到 equals() 和 hashCode() 方法
用可变类当HashMap的key有什么问题?
hashcode可能发生改变,导致put进去的值,无法get出
一个自定义的class作为HashMap的key该如何实现?
- 重写hashcode 和equals 方法
- 设计一个不变类
原则1:
1)两个对象相等,hashcode一定相等
(2)两个对象不等,hashcode不一定不等
(3)hashcode相等,两个对象不一定相等
(4)hashcode不等,两个对象一定不等
设计不变类
(1) 类添加final修饰符,保证类不被继承。
(2)保证所有成员变量必须私有,并且加上final修饰
(3)不提供改变成员变量的方法,包括setter
(4)通过构造器初始化所有成员,进行深拷贝(deep copy)
(5)在getter方法中,不要直接返回对象本身,而是克隆对象,并返回对象的拷贝
HashMap详细20道面试题_hashmap面试题-CSDN博客
4. 解决hash 冲突的方法有哪些
- 开放定址法也称为
再散列法
:p=H(key)
出现冲突时,则以p
为基础,再次hash,直到不冲突为止 - 再哈希法:多个不同的hash函数,当
R1=H1(key1)
发生冲突时,再计算R2=H2(key1)
- 链地址法(拉链法):冲突建立 链表解决冲突。
- HashMap中采用的是 链地址法 。
5. 为什么在解决 hash 冲突的时候,不直接用红黑树?而选择先用链表,再转红黑树?
红黑树需要进行左旋,右旋,变色这些操作来保持平衡,而单链表不需要。当元素小于 8 个的时候,此时做查询操作,链表结构已经能保证查询性能。当元素大于 8 个的时候, 红黑树搜索时间复杂度是 O(logn),而链表是 O(n),此时需要红黑树来加快查询速度,但是新增节点的效率变慢了。
节点少时,红黑树的左右自旋,增加操作比较耗时,链表的增加操作简单,同时查找效率相差不大。所以选择链表。
6. HashMap默认加载因子是多少?为什么是 0.75,不是 0.6 或者 0.8 ?
- 如果加载因子比较大,扩容发生的频率比较低,浪费的空间比较少 ,发生 hash 冲突的几率比较大
- 如果加载因子比较小,扩容发生的频率比较高,浪费的空间比较多,发生 hash 冲突的几率比较小
综合时间和空间的考虑选择 负载因子为0.75.
7. 为什么数组的长度为什么是2的次幂
- 让 HashMap 存取高效,尽量较少碰撞,也就是要尽量把数据分配均匀。
- 当n为2次幂时,会满足:(n - 1) & hash = hash % n, &运算速度快,比%取模运算块
8. HashMap 中 key 的存储索引是怎么计算的?
1. key的值计算出hashcode的值
- hashcode计算出hash值,
- 最后通过hash&(length-1)计算得到存储的位置
为什么再次计算hash值
计算流程: hashcode 高16位与低 16位进行异或运算。
使得元素均匀分布, 减小hash碰撞的概率,避免长链表的产生
9. HashMap 的put方法流程?
- 首先根据 key 的值计算 hash 值,找到该元素在数组中存储的下标;
- 如果数组是空的,则调用 resize 进行初始化;
- 如果没有哈希冲突直接放在对应的数组下标里;
- 如果冲突了,且 key 已经存在,就覆盖掉 value;
- 如果冲突后,发现该节点是红黑树,就将这个节点挂在树上;
- 如果冲突后是链表,判断该链表是否大于 8 ,如果大于 8 并且数组容量小于 64,就进行扩容;如果链表节点大于 8 并且数组的容量大于 64,则将这个结构转换为红黑树;否则,链表插入键值对,若 key 存在,就覆盖掉 value
10. HashMap为什么线程不安全?
- 多线程下扩容死循环。JDK1.7中的 HashMap 使用头插法插入元素,在多线程的环境下,扩容的时候有可能导致环形链表的出现,形成死循环。因此,JDK1.8使用尾插法插入元素,在扩容时会保持链表元素原本的顺序,不会出现环形链表的问题。
- 多线程的put可能导致元素的丢失。 多线程同时执行 put 操作,如果计算出来的索引位置是相同的,那会造成前一个 key 被后一个 key 覆盖,从而导致元素的丢失。此问题在JDK 1.7和 JDK 1.8 中都存在。
- put和get并发时,可能导致get为null。 线程1执行put时,因为元素个数超出threshold而导致rehash,线程2此时执行get,有可能导致这个问题。此问题在JDK 1.7和 JDK 1.8 中都存在。
11.ConcurrentHashMap 线程安全的具体实现方式/底层具体实现
jdk 1.8 之前
数据分为一段一段(这个“段”就是 Segment
)的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据时,其他段的数据也能被其他线程访问。
ConcurrentHashMap
是由 Segment
数组结构和 HashEntry
数组结构组成。
Segment
继承了 ReentrantLock
,所以 Segment
是一种可重入锁,扮演锁的角色。
jdk 1.8 之后
ConcurrentHashMap
是 Node + CAS + synchronized来保证并发安全。数据结构跟
HashMap`1.8 的结构类似,数**组+链表/红黑二叉树。Java 8 在链表长度超过一定阈值(8)时将链表(寻址时间复杂度为 O(N))转换为红黑树(寻址时间复杂度为 O(log(N))
synchronized
只锁定当前链表或红黑二叉树的首节点,这样只要 hash 不冲突,就不会产生并发,就不会影响其他 Node 的读写
区别
线程安全实现方式:JDK 1.7 采用 Segment
分段锁来保证安全, Segment
是继承自 ReentrantLock
。JDK1.8 放弃了 Segment
分段锁的设计,采用 Node + CAS + synchronized
保证线程安全,锁粒度更细,synchronized
只锁定当前链表或红黑二叉树的首节点。
Hash 碰撞解决方法 : JDK 1.7 采用拉链法,JDK1.8 采用拉链法结合红黑树(链表长度超过一定阈值时,将链表转换为红黑树)。
并发度:JDK 1.7 最大并发度是 Segment 的个数,默认是 16。JDK 1.8 最大并发度是 Node 数组的大小,并发度更大。
12. ConcurrentHashMap 为什么 key 和 value 不能为 null?
因为ConcurrentHashMap
是用于多线程的 ,如果map.get(key)
得到了 null ,无法判断,是映射的value是 null ,还是没有找到对应的key而为 null ,这就有了二义性。
HashMap 面试题 11 问,看这篇就够了_hashmap扩容机制面试-CSDN博客
【硬核】HashMap最全面试题(附答案)_hashmap底层实现原理面试题-CSDN博客
13. 讲一讲快速失败(fail-fast)和安全失败(fail-safe)
快速失败(fail—fast)
- 在用迭代器遍历一个集合对象时,如果遍历过程中对集合对象的内容进行了修改(增加、删除、修改),则会抛出Concurrent Modification Exception。
原理:迭代器在遍历时直接访问集合中的内容,并且在遍历过程中使用一个 modCount 变量。集合在被遍历期间如果内容发生变化,就会改变modCount的值。每当迭代器使用hashNext()/next()遍历下一个元素之前,都会检测modCount变量是否为expectedmodCount值,是的话就返回遍历;否则抛出异常,终止遍历
安全失败(fail—safe)
- 采用安全失败机制的集合容器,在遍历时不是直接在集合内容上访问的,而是先复制原有集合内容,在拷贝的集合上进行遍历。
- 原理:由于迭代时是对原集合的拷贝进行遍历,所以在遍历过程中对原集合所作的修改并不能被迭代器检测到,所以不会触发Concurrent Modification Exception。
spring
1.Spring事务@Transaction失效原因 - 乐子不痞 - 博客园 (cnblogs.com)
Spring事务@Transaction失效原因 - 乐子不痞 - 博客园 (cnblogs.com)
1.访问权限问题**
如果事务方法的访问权限不是定义成public,这样会导致事务失效,因为spring要求被代理方法必须是public
的。
2. 方法用final修饰
如果事务方法用final修饰,将会导致事务失效。因为spring事务底层使用了aop,也就是通过jdk动态代理或者cglib,帮我们生成了代理类,在代理类中实现的事务功能。
但如果某个方法用final修饰了,那么在它的代理类中,就无法重写该方法,而添加事务功能。
同理,如果某个方法是static的,同样无法通过动态代理,变成事务方法。
3.对象没有被spring管理
使用spring事务的前提是:对象要被spring管理,需要创建bean实例。如果类没有加@Controller、@Service、@Component、@Repository等注解,即该类没有交给spring去管理,那么它的方法也不会生成事务。
4.表不支持事务
如果MySQL使用的存储引擎是myisam,这样的话是不支持事务的。因为myisam存储引擎不支持事务。
5.方法内部调用
如下代码所示,update方法上面没有加 @Transactional
注解,调用有 @Transactional
注解的 updateOrder 方法,updateOrder 方法上的事务会失效。
6.未开启事务
如果是spring项目,则需要在配置文件中手动配置事务相关参数。如果忘了配置,事务肯定是不会生效的。
2. spring Aop
1. aop实现方式
AOP有两种实现方式:静态代理和动态代理。
静态代理
静态代理:代理类在编译阶段生成,在编译阶段将通知织入Java字节码中,也称编译时增强。AspectJ使用的是静态代理。
缺点:代理对象需要与目标对象实现一样的接口,并且实现接口的方法,会有冗余代码。同时,一旦接口增加方法,目标对象与代理对象都要维护。
动态代理
动态代理:代理类在程序运行时创建,**AOP框架不会去修改字节码,而是在内存中临时生成一个代理对象,在运行期间对业务方法进行增强,不会生成新类。
2.Spring AOP的实现原理
Spring
的AOP
实现原理,就是通过动态代理实现的。如果我们为Spring
的某个bean
配置了切面,那么Spring
在创建这个bean
的时候,实际上创建的是这个bean
的一个代理对象,我们后续对bean
中方法的调用,实际上调用的是代理类重写的代理方法。而Spring
的AOP
使用了两种动态代理,分别是JDK的动态代理,以及CGLib的动态代理。
3. JDK动态代理和CGLIB动态代理的区别?
Spring AOP中的动态代理主要有两种方式:JDK动态代理和CGLIB动态代理。
JDK动态代理
如果目标类实现了接口,Spring AOP会选择使用JDK动态代理目标类。 代理类根据目标类实现的接口动态生成,不需要自己编写,生成的动态代理类和目标类都实现相同的接口。JDK动态代理的核心是InvocationHandler
接口和Proxy
类。
缺点:目标类必须有实现的接口。如果某个类没有实现接口,那么这个类就不能用JDK动态代理
CGLIB动态代理
通过继承实现。如果目标类没有实现接口,那么Spring AOP会选择使用CGLIB来动态代理目标类。CGLIB(Code Generation Library)可以在运行时动态生成类的字节码,动态创建目标类的子类对象,在子类对象中增强目标类。
CGLIB是通过继承的方式做的动态代理,因此如果某个类被标记为final
,那么它是无法使用CGLIB做动态代理的。
优点:目标类不需要实现特定的接口,更加灵活。
什么时候采用哪种动态代理?
- 如果目标对象实现了接口,默认情况下会采用JDK的动态代理实现AOP
- 如果目标对象实现了接口,可以强制使用CGLIB实现AOP
- 如果目标对象没有实现了接口,必须采用CGLIB库
两者的区别:
- jdk动态代理使用jdk中的类Proxy来创建代理对象,它使用反射技术来实现,不需要导入其他依赖。cglib需要引入相关依赖:
asm.jar
,它使用字节码增强技术来实现。 - 当目标类实现了接口的时候Spring Aop默认使用jdk动态代理方式来增强方法,没有实现接口的时候使用cglib动态代理方式增强方法。
4. ioc 容器初始化过程
- 从XML中读取配置文件。
- 将bean标签解析成 BeanDefinition,如解析 property 元素, 并注入到 BeanDefinition 实例中。
- 将 BeanDefinition 注册到容器 BeanDefinitionMap 中。
- BeanFactory 根据 BeanDefinition 的定义信息创建实例化和初始化 bean。
单例bean的初始化以及依赖注入一般都在容器初始化阶段进行,只有懒加载(lazy-init为true)的单例bean是在应用第一次调用getBean()时进行初始化和依赖注入。
多例bean 在容器启动时不实例化,即使设置 lazy-init 为 false 也没用,只有调用了getBean()才进行实例化。
3. bean 生命周期
1.调用bean的构造方法创建Bean
2.通过反射调用setter方法进行属性的依赖注入
3.如果Bean实现了BeanNameAware
接口,Spring将调用setBeanName
(),设置 Bean
的name(xml文件中bean标签的id)
4.如果Bean实现了BeanFactoryAware
接口,Spring将调用setBeanFactory()
把bean factory设置给Bean
5.如果存在BeanPostProcessor
,Spring将调用它们的postProcessBeforeInitialization
(预初始化)方法,在Bean初始化前对其进行处理
6.如果Bean实现了InitializingBean
接口,Spring将调用它的afterPropertiesSet
方法,然后调用xml定义的 init-method 方法,两个方法作用类似,都是在初始化 bean 的时候执行
7.如果存在BeanPostProcessor
,Spring将调用它们的postProcessAfterInitialization
(后初始化)方法,在Bean初始化后对其进行处理
8.Bean初始化完成,供应用使用,这里分两种情况:
8.1 如果Bean为单例的话,那么容器会返回Bean给用户,并存入缓存池。如果Bean实现了DisposableBean
接口,Spring将调用它的destory
方法,然后调用在xml中定义的 destory-method
方法,这两个方法作用类似,都是在Bean实例销毁前执行。
8.2 如果Bean是多例的话,容器将Bean返回给用户,剩下的生命周期由用户控制。
4. spring解决循环依赖
5. spring 设计模式
工厂设计模式 : Spring使用工厂模式通过 BeanFactory
、ApplicationContext
创建 bean 对象。
代理设计模式 : Spring AOP 功能的实现。
单例设计模式 : Spring 中的 Bean 默认都是单例的。
模板方法模式 : Spring 中 jdbcTemplate
Template 结尾的对数据库操作的类,它们就使用到了模板模式
适配器模式 :Spring AOP 的增强或通知(Advice)使用到了适配器模式、spring MVC 中也是用到了适配器模式适配Controller
6. spring mvc 工作原理或者工作流程是什么?
- 客户端(浏览器)发送请求,直接请求到
DispatcherServlet
。 DispatcherServlet
根据请求信息调用HandlerMapping
,解析请求对应的Handler
。- 解析到对应的
Handler
(也就是我们平常说的Controller
控制器)后,开始由HandlerAdapter
适配器处理。 HandlerAdapter
会根据Handler
来调用真正的处理器开处理请求,并处理相应的业务逻辑。- 处理器处理完业务后,会返回一个
ModelAndView
对象,Model
是返回的数据对象,View
是个逻辑上的View
。 ViewResolver
会根据逻辑View
查找实际的View
。DispaterServlet
把返回的Model
传给View
(视图渲染)。- 把
View
返回给请求者(浏览器)
springboot
1. 自动装配原理
springboot 自动装配的原理 - 醒醒起来 - 博客园 (cnblogs.com)
计算机网络
TCP当中的粘包和拆包的概念以及如何解决这两类问题
-
要发送的数据大于TCP发送缓冲区剩余空间大小,将会发生拆包
-
待发送数据大于MSS(最大报文长度),TCP在传输前将进行拆包
-
发送的数据小于TCP发送缓冲区的大小,TCP将多次写入缓冲区的数据一次发送出去,将会发生粘包。
-
接收数据端的应用层没有及时读取接收缓冲区中的数据,将发生粘包
解决办法:
发送端给每个数据包添加包首部,首部中应该至少包含数据包的长度,接收端在接收到数据后,通过读取包首部的长度字段,便知道每一个数据包的实际长度
可以在数据包之间设置边界,如添加特殊符号,这样,接收端通过这个边界就可以将不同的数据包拆分开
MySql
1. 数据库删表方式
第一种:drop table :
第二种:delete table;
第三种:truncate table;
drop table 表结构和数据都会被删除 。delete table和truncate table 只删除表中的数据
Delete table会记录保存删除日志,truncate table不会写日志。
Delete table效率低,数据可以恢复;truncate table 效率高,数据不可恢复。
2. mysql 索引类型
从存储结构上来划分:BTree索引(B-Tree或B+Tree索引),Hash索引,full-index全文索引,R-Tree索引。
从应用层次来分:普通索引,唯一索引,复合索引
根据中数据的物理顺序与键值的逻辑(索引)顺序关系: 聚集索引,非聚集索引。
3. mysql 的行级锁特点 和类型
特点
行锁:访问数据库时锁定整个行数据,防止并发错误
行锁:锁力度小,开销大,加锁慢,会出现死锁,发生锁冲突概率大,并发高
行锁适用于高并发环境下对于事物完整性高的系统
MySQL的行锁是通过索引加载的
MySQL行锁、表锁特点应用场景_行锁和表锁运用场景-CSDN博客
类型
行级锁的类型主要有三类:
Record Lock,记录锁,也就是仅仅把一条记录锁上;又分为共享锁和排他锁
Gap Lock,间隙锁,锁定一个范围,但是不包含记录本身,只存在于可重复读隔离级别,目的是为了解决可重复读隔离级别下幻读的现象。 左开右开()
Next-Key Lock 临键锁:Record Lock + Gap Lock 的组合,锁定一个范围,并且锁定记录本身。 左开 右闭合。(】
原文链接:https://blog.csdn.net/Chang_Yafei/article/details/131180391
4. mysql 索引什么时候失效?
- 带有运算,也就是在where子句中使用=,+,-,!=,<,>这些运算符号
- 使用not in,in,这些范围运算时,就会导致索引失效产生全表扫描
- Like模糊查询时候,使用%开头的左模糊查询,这样子会导致索引失效。
- 使用or的时候 ** or条件中有一个不是索引,就会导致索引失效**
- 字段类型与sql语句类型不匹配时,也会导致索引失效
【MySQL面试题】索引什么时候会失效_模糊查询会导致索引失效吗-CSDN博客
redis 和mysql 的一致性
1.延时双删 : 先删 redis 再写mysql 再删redis
1.延时双删: 第二次删除redis 间隔 300ms
缺点:串行化,风险不可控,redis 删除失败
优化方案:
rabbitmq消息队列异步删除。
键值删除失败:
- redis 设置过期时间
- rabbitmq重试机制
- mysql 设置一个表记录重试次数
2. 先更mysql, 在删除redis
- 存在问题
- 存在最前面一次不一致的问题:对强一致性要求不高的业务。
- 删除缓存失败的策略: 1. 设置键值过期时间,2. 使用rabbitmq异步删除,最大重试机制:失败进入死信队列,然后写入mysql。
3. 先写 MySQL,通过 Binlog,异步更新 Redis
主要是监听 MySQL 的 Binlog,然后通过异步的方式,将数据更新到 Redis,这种方案有个前提,查询的请求,不会回写 Redis
。
会保证 MySQL 和 Redis 的最终一致性,但是如果中途请求 B 需要查询数据,如果缓存无数据,就直接查 DB;如果缓存有数据,查询的数据也会存在不一致的情况。
所以这个方案,是实现最终一致性的终极解决方案,但是不能保证实时性。
一致性最终总结
- 先写 MySQL,再删除 Redis
- 比较推荐这种方式,删除 Redis 如果失败,可以再多重试几次,否则报警出来;
- 这个方案,是实时性中最好的方案,在一些高并发场景中,推荐这种。
- 先写 MySQL,通过 Binlog,异步更新 Redis
- 对于异地容灾、数据汇总等,建议会用这种方式,比如 binlog + kafka,数据的一致性也可以达到秒级;
- 纯粹的高并发场景,不建议用这种方案,比如抢购、秒杀等。
实时一致性和最终一致性
- 实时一致性方案:采用“先写 MySQL,再删除 Redis”的策略,这种情况虽然也会存在两者不一致,但是需要满足的条件有点苛刻,所以是满足实时性条件下,能尽量满足一致性的最优解。
- 最终一致性方案:采用“先写 MySQL,通过 Binlog,异步更新 Redis”,可以通过 Binlog,结合消息队列异步更新 Redis,是最终一致性的最优解。
一致性理论
强一致性:当更新操作完成之后,任何多个后续进程或者线程的访问都会返回最新的更新过的值
弱一致性:系统并不保证续进程或者线程的访问都会返回最新的更新过的值。系统在数据写入成功之后,不承诺立即可以读到最新写入的值,也不会具体的承诺多久之后可以读到
最终一致性:系统保证在没有后续更新的前提下,系统最终返回上一次更新操作的值。弱一致性特定形式。
如何保障MySQL和Redis的数据一致性? | 二哥的Java进阶之路 (javabetter.cn)
mysql 优化
1. 深度分页优化 limit offset, pagesize;
select * from user
where create_time>'2022-07-03'
limit 100000,10
深分页问题
- create_time是非聚簇索引,需要先查询出主键ID,再回表查询,通过主键ID查询出所有字段
1. 使用子查询,查询主键
子查询查出符合条件的主键,再用主键ID做条件查出所有字段
select * from user
where id in (select id from (select id from user where create_time>'2022-07-03' limit 100000,10) as t
);
优点:
- 维持了分页需求,适用于所有的limit offset场景,大大减少了随机IO,提高了性能。
- 二级索引上只查询id,传输数据包变小。
缺点:
二级索引还是会走下面的链表来遍历,这部分时间复杂度还是O(n)
2. inner join关联查询
子查询的结果当成一张临时表,然后和原表进行关联查询
select * from user
inner join (select id from user where create_time>'2022-07-03' limit 100000,10
) as t on user.id=t.id;
3. 使用分页游标(推荐)
实现方式就是:当我们查询第二页的时候,把第一页的查询结果放到第二页的查询条件中。
select * from user
where create_time>'2022-07-03' and id>10
limit 10;
查询效率提升10倍!3种优化方案,帮你解决MySQL深分页问题_mysql分页查询优化-CSDN博客
2. 慢mysql 如何优化
sql --- 慢sql定位及优化_慢sql怎么定位-CSDN博客
设计模式
单例模式
单例模式: 饿汉和懒汉模式。
懒汉式单例模式指的是在第一次访问时才创建唯一实例
饿汉式单例模式指的是在类加载时就创建唯一实例
//懒汉式多线程创建 线程安全
public class Single{private volatile static Single inStance=null;private Single(){}public static Single getInstance(){if(inStance==null){//1. 第一次检查初始化,后期不用锁,提高运行效率// 第二次检查: a线程运行着,然后b线程也运行到者,然后a加锁,b等待,//a 创建对象,b等待, a解锁,b加锁,然后继续创建对象。导致重复chuagn'jaisynchronized(Single.class){//2. 加锁为了并发安全if(isStance==null){//3. 第二次检查了,为了防止重复创建对象。inStance=new Single();}}}return inStance;}
}
**1. 私有构造函数 2. 私有静态 变量 不能实例化 3. 共有静态实例对象: DLC(双重检查)。 **
1. 第一次检查: 检查是否初始化,后期不用获得锁,提高效率。
2. 第二次检查: 防止重复创建对象。
3. synchronized: 加锁保证线程安全。
JVM
java 内存区域有哪些?
线程私有的:
- 程序计数器
- 虚拟机栈
- 本地方法栈
线程共享的:
- 堆
- 方法区
- 运行时常量池。
- 程序计数器:当前线程执行的字节码指令的地址。
- 本地方法栈:存储native方法执行时的数据和信息
- java虚拟机栈:用于存储方法执行时的局部变量表、操作数栈等
- Java堆:存储对象实例以及数组.:Java堆是所有线程共享的内存区域:Java堆可以细分为新生代和老年代,以及永久代
- 方法区: 被虚拟机加载的类信息、常量、静态变量.
- 运行时常量池: 方法区一部分,存放编译期生成的各种字面量和符号引用
jdk1.8 :使用元空间(Metaspace)用于替代方法区
JVM有哪些内存区域?经典面试题你会吗?_jvm内存区域-CSDN博客
类加载过程?
- 加载:根据查找路径找到相应的class文件然后导入;
- 验证:检查加载的class文件的正确性;
- 准备:给类中的静态标量分配内存空间;
- 解析:虚拟机将常量池中的符号引用替换成直接引用的过程。符号引用就理解为一个标示,而在直接引用直接指向内存中的地址;
- 初始化:对静态变量和静态代码块进行初始化工作
-
加载 2. 验证 3 准备 4 解析 5 初始化
十八道JVM面试题总汇(附解析) - 昨夜黄花今朝狼 - 博客园 (cnblogs.com)
jvm 加载class的原理机制?
虚拟机(jvm)把描述类的数据从Class文件加载到内存,并对数据进行校验,解析和初始化,最终形成可以被虚拟机直接使用的java类型。
java中的所有类,都需要有由类加载器装载到JVM中才能运行。类加载器本身也是一个类,而它的工作就是把Class文件从硬盘读取到内存中。在写程序的时候,我们几乎不需要关心类的加载,因为这些都是隐式装载的,除非我们有特殊的用法,像是反射,就需要显示的加载所需要的类。
类装载的方式,有两种:
1、隐式装载,程序在运行过程中当碰到通过new等方式生成对象时,隐式调用类装载器对应的类到jvm中
2、显示装载,通过class.forname()等方式,显示加载需要的类
java类的加载是动态的,它并不会一次性将所有类全部加载后再运行,而是保存程序运行的基础类(像是基类)完全加载到JVM中,至于其他类,则在需要的时候才才加载。这当然就是为了节省内存开销
什么是类加载器,类加载器有哪些?
通过类的权限定名获取该类的二进制字节流的代码块叫做类加载器
1、启动类加载器(Bootstrap ClassLoader)用来加载java核心类库,无法被java程序直接引用。
2、扩展类加载器(Extensions ClassLoader)用来加载java的扩展库。java虚拟机的实现会提供一个扩展库目录。该类加载器在此目录里面查找并加载java类,它的父类加载器是Bootrap。
3、系统类加载器(System ClassLoader)根据java应用的类路径(CLASSPATH)来加载java类。一般来说,java应用的类都是由它来完成加载的。可以通过ClassLoader.getSystemLoader()来获取它,它是应用最广泛的类加载器。。
4、用户自定义类加载器,通过继承java.lang.ClassLoader类的方式实现
JVM加载Class文件的原理机制 - 广大青年 - 博客园 (cnblogs.com)
双亲委派机制
1. 简述双亲委派机制
双亲委托机制是指当一个类加载器收到一个类加载请求时,该类加载器首先会把请求委派给父类加载器。每个类加载器都是如此,只有在父类加载器在自己的搜索范围内找不到指定类时,子类加载器才会尝试自己去加载。
2. 双亲委派机制好处
1.避免类的重复加载。一旦一个类被父类加载器加载之后,就不会再被委派给子类进行加载。
比如 A 类和 B 类都有一个父类 C 类,那么当 A 启动时就会将 C 类加载起来,那么在 B 类进行加载时就不需要在重复加载 C 类了。
2.保护程序安全,保证系统类不能被篡改:
如果没有使用双亲委派模型,而是每个类加载器加载自己的话就会出现一些问题,比如我们编写一个称为 java.lang.Object 类的话,那么程序运行的时候,系统就会出现多个不同的 Object 类,而有些 Object 类又是用户自己提供的因此安全性就不能得到保证了。
3. 如何打破双亲委派机制
11、重写 classloader 中的 loadClass方法,不再实现双亲委派机制。
2、线程上下文类加载器的传递性,让父类加载器中调用子类加载器的加载动作
3、OSGi实现了一整套类加载机制,允许同级类加载器之间互相调用。
4. tomcat 如何打破双亲委派机制
为什打破?
为了解决Web应用程序的类加载冲突问题。当使用双亲委派模型时,容器的类加载器会先尝试加载共享类库,导致Web应用程序中提供的同名类无法被正确加载,从而产生类加载冲突
如何打破?
WebAppClassLoader类加载器,通过重写classload 中loadclass 和findclass 方法。如果收到类加载的请求,它会先尝试自己去加载,如果找不到在交给父加载器去加载,这么做的目的就是为了优先加载Web应用程序自己定义的类来实现web应用程序相互隔离独立的。
Tomcat是如何打破"双亲委派"机制的?-阿里云开发者社区 (aliyun.com)
JVM第一讲 JVM双亲委派机制以及打破双亲委派_如何打破双亲委派机制-CSDN博客
堆栈的区别?
- 物理地址: 堆的物理地址分配对象是不连续的。栈使用的是数据结构中的栈,先进后出的原则,物理地址分配是连续的。
- 存放内容: 堆存放的是对象的实例和数组,更关注的是数据的存储 。 栈存放:局部变量,操作数栈,返回结果。该区更关注的是程序方法的执行。
- 可见性:堆对于整个应用程序都是共享、可见的 。 栈只对于线程是可见的 是线程私有。他的生命周期和线程相同。
Java虚拟机(JVM)超详细面试题_jvm笔试题-CSDN博客
内存溢出:OOM
什么情况下会发生栈内存溢出?
- 栈是线程私有的,栈的生命周期和线程一样,每个方法在执行的时候就会创建一个栈帧.
- 当**线程请求的栈深度超过了虚拟机允许的最大深度时, ** 方法递归调用肯可能会出现该问题
除了程序计数器,其他内存区域都有 OOM 的风险
- 栈一般爆出stackOverFlowError错误。:windows 系统单进程限制 2G 内存,无限创建线程就会发生栈的 OOM
- Java 8 常量池移到堆中,溢出会出 java.lang.OutOfMemoryError: Java heap space
- 方法区 OOM,经常会遇到的是动态生成大量的类、jsp 等
例子: HashMap 做缓存未清理,时间长了就会内存溢出,可以把改为弱引用 。
内存泄漏
内存泄漏是指不再被使用的对象或者变量一直被占据在内存中
对象不会再被程序用到了,但是GC又不能回收他们的情况,才叫内存泄漏。
长生命周期的对象持有短生命周期对象的引用就很可能发生内存泄露,尽管短生命周期对象已经不再需要,但是因为长生命周期对象持有它的引用而导致不能被回收,这就是java中内存泄露的发生场景。
ThreadLocal高频面试题-CSDN博客
1. ThreadLocal是什么?为什么要使用ThreadLocal?
ThreadLocal
,即线程本地变量,访问这个变量的每个线程都会有这个变量的一个本地拷贝,多个线程操作这个变量的时候,实际是在操本地拷贝: 线程内共享,线程间隔离
为什么要使用ThreadLocal
并发场景下,会存在多个线程同时修改一个共享变量的场景。这就可能会出现线性安全问题。
使用ThreadLocal
类访问共享变量时,会在每个线程的本地,都保存一份共享变量的拷贝副本。多线程对共享变量修改时,实际上操作的是这个变量副本,从而保证线性安全。
2. ThreadLocal的原理
Thread
线程类有一个类型为ThreadLocal.ThreadLocalMap
的实例变量threadLocals
,即每个线程都有一个属于自己的ThreadLocalMap
。ThreadLocalMap
内部维护着Entry
数组,每个Entry
代表一个完整的对象,key
是ThreadLocal
本身,value
是ThreadLocal
的泛型值。- 并发多线程场景下,每个线程
Thread
,在往ThreadLocal
里设置值的时候,都是往自己的ThreadLocalMap
里存,读也是以某个ThreadLocal
作为引用,在自己的map
里找对应的key
,从而可以实现了线程隔离。
3. TreadLocal为什么会导致内存泄漏呢?
ThreadLocalMap
使用ThreadLocal
的弱引用作为key
,当ThreadLocal
变量被手动设置为null
,当系统GC时,ThreadLocal
一定会被回收。就没有办法访问这些key
为null
的Entry
的value
,这些key
为null
的Entry
的value
就会一直存在一条强引用链:Thread变量 -> Thread对象 -> ThreaLocalMap -> Entry -> value -> Object 永远无法回收,造成内存泄漏。
即在ThreadLocal
的get
,set
,remove
方法,**都会清除线程ThreadLocalMap
里所有key
为null
的value
。
垃圾回收
1. 垃圾回收机制GC
程序员是不需要显示的去释放一个对象的内存的,而是由虚拟机自行执行。在JVM中,有一个垃圾回收线程,它是低优先级的,在正常情况下是不会执行的,只有在虚拟机空闲或者当前堆内存不足时,才会触发执行,扫面那些没有被任何引用的对象,并将它们添加到要回收的集合中,进行回收
2.垃圾回收算法
- 标记-清除算法:标记有用对象,然后进行清除未标记的对象。缺点:效率不高,无法清除垃圾碎片。
- 复制算法:按照容量划分二个大小相等的内存区域,当一块用完的时候将活着的对象复制到另一块上,然后再把已使用的内存空间一次清理掉。缺点:内存使用率不高,只有原来的一半,消耗内存。
- 标记-整理算法:标记无用对象,让所有存活的对象都向一端移动,然后直接清除掉端边界以外的内存。
- 分代算法:根据对象存活周期的不同将内存划分为几块,一般是新生代和老年代,新生代基本采用复制算法,老年代采用标记整理算法。
JVM (javalearn.cn)
3. 判断一个对象是否存活
- 引用计数法:
对象中添加一个引用计数器:
- 每当有一个地方引用它,计数器就加 1;
- 当引用失效,计数器就减 1;
- 任何时候计数器为 0 的对象就是不可能再被使用的
缺点:无法解决循环引用问题。
- 可达性分析算法:
“GC Roots” 的对象作为起点,从这些节点开始向下搜索,节点所走过的路径称为引用链,当一个对象到 GC Roots 没有任何引用链相连的话,则证明此对象是不可用的,需要被回收。
JVM垃圾回收详解(重点) | JavaGuide
4. Java 对象引用类型:4种 强,软,弱,虚
强引用(StrongReference):垃圾回收器绝不会回收它。当内存空间不足,虚拟机宁愿抛出 OutOfMemoryError 错误,使程序异常终止
软引用:只有在内存不足时,系统则会回收软引用对象
弱引用:更短的生命周期,当 JVM 进行垃圾回收时,无论内存是否充足,都会回收被弱引用关联的对象
虚引用:虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收。
软引用可以加速 JVM 对垃圾内存的回收速度,可以维护系统的运行安全,防止内存溢出(OutOfMemory)等问题的产生。
垃圾回收器
1. 12. 详细说一下CMS的回收过程?CMS的问题是什么?
CMS(Concurrent Mark Sweep,并发标记清除) 收集器是以获取最短回收停顿时间为目标的收集器(追求低停顿),它在垃圾收集时使得用户线程和 GC 线程并发执行,因此在垃圾收集过程中用户也不会感到明显的卡顿。
从名字就可以知道,CMS是基于“标记-清除”算法实现的。CMS 回收过程分为以下四步:
- 初始标记 (CMS initial mark):主要是标记 GC Root 开始的下级(注:仅下一级)对象,这个过程会 STW,但是跟 GC Root 直接关联的下级对象不会很多,因此这个过程其实很快。
- 并发标记 (CMS concurrent mark):根据上一步的结果,继续向下标识所有关联的对象,直到这条链上的最尽头。这个过程是多线程的,虽然耗时理论上会比较长,但是其它工作线程并不会阻塞,没有 STW。
- 重新标记(CMS remark):顾名思义,就是要再标记一次。为啥还要再标记一次?因为第 2 步并没有阻塞其它工作线程,其它线程在标识过程中,很有可能会产生新的垃圾。
- 并发清除(CMS concurrent sweep):清除阶段是清理删除掉标记阶段判断的已经死亡的对象,由于不需要移动存活对象,所以这个阶段也是可以与用户线程同时并发进行的。
CMS 的问题:
1. 并发回收导致CPU资源紧张:
在并发阶段,它虽然不会导致用户线程停顿,但却会因为占用了一部分线程而导致应用程序变慢,降低程序总吞吐量。CMS默认启动的回收线程数是:(CPU核数 + 3)/ 4,当CPU核数不足四个时,CMS对用户程序的影响就可能变得很大。
2. 无法清理浮动垃圾:
在CMS的并发标记和并发清理阶段,用户线程还在继续运行,就还会伴随有新的垃圾对象不断产生,但这一部分垃圾对象是出现在标记过程结束以后,CMS无法在当次收集中处理掉它们,只好留到下一次垃圾收集时再清理掉。这一部分垃圾称为“浮动垃圾”。
3. 并发失败(Concurrent Mode Failure):
由于在垃圾回收阶段用户线程还在并发运行,那就还需要预留足够的内存空间提供给用户线程使用,因此CMS不能像其他回收器那样等到老年代几乎完全被填满了再进行回收,必须预留一部分空间供并发回收时的程序运行使用。默认情况下,当老年代使用了 92% 的空间后就会触发 CMS 垃圾回收,这个值可以通过 -XX: CMSInitiatingOccupancyFraction 参数来设置。
这里会有一个风险:要是CMS运行期间预留的内存无法满足程序分配新对象的需要,就会出现一次“并发失败”(Concurrent Mode Failure),这时候虚拟机将不得不启动后备预案:Stop The World,临时启用 Serial Old 来重新进行老年代的垃圾回收,这样一来停顿时间就很长了。
4.内存碎片问题:
CMS是一款基于“标记-清除”算法实现的回收器,这意味着回收结束时会有内存碎片产生。内存碎片过多时,将会给大对象分配带来麻烦,往往会出现老年代还有很多剩余空间,但就是无法找到足够大的连续空间来分配当前对象,而不得不提前触发一次 Full GC 的情况。
为了解决这个问题,CMS收集器提供了一个 -XX:+UseCMSCompactAtFullCollection 开关参数(默认开启),用于在 Full GC 时开启内存碎片的合并整理过程,由于这个内存整理必须移动存活对象,是无法并发的,这样停顿时间就会变长。还有另外一个参数 -XX:CMSFullGCsBeforeCompaction,这个参数的作用是要求CMS在执行过若干次不整理空间的 Full GC 之后,下一次进入 Full GC 前会先进行碎片整理(默认值为0,表示每次进入 Full GC 时都进行碎片整理)。
2. 13. 详细说一下G1的回收过程?
G1(Garbage First)回收器采用面向局部收集的设计思路和基于Region的内存布局形式,是一款主要面向服务端应用的垃圾回收器。G1设计初衷就是替换 CMS,成为一种全功能收集器。G1 在JDK9 之后成为服务端模式下的默认垃圾回收器,取代了 Parallel Scavenge 加 Parallel Old 的默认组合,而 CMS 被声明为不推荐使用的垃圾回收器。G1从整体来看是基于 标记-整理 算法实现的回收器,但从局部(两个Region之间)上看又是基于 标记-复制 算法实现的。
G1 回收过程,G1 回收器的运作过程大致可分为四个步骤:
- 初始标记(会STW):仅仅只是标记一下 GC Roots 能直接关联到的对象,并且修改TAMS指针的值,让下一阶段用户线程并发运行时,能正确地在可用的Region中分配新对象。这个阶段需要停顿线程,但耗时很短,而且是借用进行Minor GC的时候同步完成的,所以G1收集器在这个阶段实际并没有额外的停顿。
- 并发标记:从 GC Roots 开始对堆中对象进行可达性分析,递归扫描整个堆里的对象图,找出要回收的对象,这阶段耗时较长,但可与用户程序并发执行。当对象图扫描完成以后,还要重新处理在并发时有引用变动的对象。
- 最终标记(会STW):对用户线程做短暂的暂停,处理并发阶段结束后仍有引用变动的对象。
- 清理阶段(会STW):更新Region的统计数据,对各个Region的回收价值和成本进行排序,根据用户所期望的停顿时间来制定回收计划,可以自由选择任意多个Region构成回收集,然后把决定回收的那一部分Region的存活对象复制到空的Region中,再清理掉整个旧Region的全部空间。这里的操作涉及存活对象的移动,必须暂停用户线程,由多条回收器线程并行完成的。
有哪几种垃圾回收器,各自的优缺点是什么?
Serial 垃圾回收器:单线程垃圾回收器,适用于小型应用场景。新生代采用标记-复制算法,老年代采用标记-整理算法。**
Parallel 垃圾回收器:多线程垃圾回收器,适用于中等大小的应用场景。新生代采用标记-复制算法,老年代采用标记-整理算法。
CMS(Concurrent Mark-Sweep)垃圾回收器:并发垃圾回收器,可以与应用程序并发执行,适用于大型应用场景。“标记-清除”算法
G1(Garbage First)垃圾回收器:基于区域的垃圾回收器,可以对堆内存进行划分并进行垃圾回收,适用于大型应用场景。
- CMS:是一种以获得最短回收停顿时间为目标的收集器,标记清除算法,运作过程:初始标记,并发标记,重新标记,并发清除,收集结束会产生大量空间碎片;
- G1:标记整理算法实现,运作流程主要包括以下:初始标记,并发标记,最终标记,筛选回收。不会产生空间碎片,可以精确地控制停顿;G1将整个堆分为大小相等的多个Region(区域),G1跟踪每个区域的垃圾大小,在后台维护一个优先级列表,每次根据允许的收集时间,优先回收价值最大的区域,已达到在有限时间内获取尽可能高的回收效率;
Java虚拟机(JVM)超详细面试题_jvm笔试题-CSDN博客
java 内存分配策略
- 对象优先在 Eden 区分配
多数情况,对象都在新生代 Eden 区分配。当 Eden 区分配没有足够的空间进行分配,虚拟机将会发起一次 Minor GC。如果本次 GC 后还是没有足够的空间,则将启用分配担保机制在老年代中分配内存。
-
大对象直接进入老年代:大量连续内存空间的对象
-
长期存活对象将进入老年代
Minor GC和Full GC 有什么区别?
Minor GC:只收集新生代的GC。
Full GC: 收集整个堆,包括 新生代,老年代,永久代(在 JDK 1.8及以后,永久代被移除,换为metaspace 元空间)等所有部分的模式。
**Minor GC触发条件: ** 当Eden区满时,触发Minor GC。
Full GC 触发条件:
- 老年代空间不够分配新的内存
- 调用System.gc时,系统建议执行Full GC
- 通过Minor GC后进入老年代的平均大小大于老年代的可用内存
简述分代垃圾回收器是怎么工作的?
分代回收器有两个分区:老生代和新生代,新生代默认的空间占比总空间的 1/3,老生代的默认占比是 2/3。
新生代使用的是复制算法,新生代里有 3 个分区:Eden、To Survivor、From Survivor,它们的默认占比是 8:1:1,它的执行流程如下:
- 把 Eden + From Survivor 存活的对象放入 To Survivor 区;
- 清空 Eden 和 From Survivor 分区;
- From Survivor 和 To Survivor 分区交换,From Survivor 变 To Survivor,To Survivor 变 From Survivor。
每次在 From Survivor 到 To Survivor 移动时都存活的对象,年龄就 +1,当年龄到达 15(默认配置是 15)时,升级为老生代。大对象也会直接进入老生代。****
老生代当空间占用到达某个值之后就会触发全局垃圾收回,一般使用标记整理的执行算法。以上这些循环往复就构成了**整个分代垃圾回收的整体执行流程。
JUC
synchronized和ReentLock的区别是什么?
相同点:
(1)都是可重入锁
(2)都保证了可见性和互斥性
(3)都可以用于控制多线程对共享对象的访问
不同点:
(1)ReentrantLock等待可中断
(2)synchronized中的锁是非公平的,ReentrantLock默认也是非公平的,但是可以通过修改参数来实现公平锁。
(3)ReentrantLock绑定多个条件
(4)synchronized是Java中的关键字是JVM级别的锁,而ReentrantLock是一个Lock接口下的实现类,是API层面的锁。
(5)synchronized不需要用户手动的释放锁,但是Lock需要手动释放,如果不释放有可能会造成死锁的发生
synchronized和volatile关键字 (cnblogs.com)
syn
synchronized关键字主要是用来解决多个线程访问资源的同步性,也就是保证被修饰的方法或者代码块任意时刻都只能有一个线程在执行。
实现原理:
底层实现是在修饰的前后加上minitorenter/minitorexit:
minitorenter:当需要获取锁的时候,会检查锁的计数器,如果是0,可以获取;如果是1,则不能获取。
minitorexit:当执行完了,会把锁的计数器设置为0.
使用:
volatile
第一个作用就是防止指令重排,创建一个新的对象,第一步,分配内存空间,第二步,初始化,第三步,指向分配的内存地址。在单线程的时候一般不会出现问题,但是在多线程的时候,有可能出现132的情况,而volatile就可以防止该可能的发生****(有序性)
防止指令重排它是通过内存屏障来实现的。
什么是内存屏障?硬件层面,内存屏障分两种:读屏障 (Load Barrier) 和写屏障 (Store Barrier) 。内存屏障有两个作用:
① 阻止屏障两侧的指令重排序;
② 强制把写缓冲区/高速缓存中的脏数据等写回主内存,或者让缓存中相应的数据失效。
原文链接:https://blog.csdn.net/weixin_43004044/article/details/129976736
第二个作用就是保证变量在多线程情况下的可见性。(可见性)
区别
第一:syn可以修饰方法,代码块,而volatile只能修饰变量
第二:正如上面所说syn可以保证原子性,可见性,但是volatile只能保证可见性
第三:二者的作用,syn是保证多个线程访问资源的同步性,volatile是解决变量在多个线程之间的可见性。
Java超高频面试题汇总!_牛客网 (nowcoder.com)
java多线程学习三:synchronized和volatile关键字 - Refuse_to_be_simple - 博客园 (cnblogs.com)
synchronize锁的作用范围
回答:
(1)synchronize作用于成员变量和非静态方法时,锁住的是对象的实例,即this对象。
(2)synchronize作用于静态方法时,锁住的是Class实例
(3)synchronize作用于一个代码块时,锁住的是所有代码块中配置的对象
synchronize 锁升级(偏向锁-》轻量锁-》重量锁)
为什么引入偏向锁
大多数时候是不存在锁竞争的,常常是一个线程多次获得同一个锁,因此如果每次都要竞争锁会增大很多没有必要付出的代价,为了降低获取锁的代价,才引入的偏向锁
偏向锁的升级
当线程1访问代码块并获取锁对象时,会在java对象头和栈帧中记录偏向的锁的threadID,因为偏向锁不会主动释放锁,因此以后线程1再次获取锁的时候,需要比较当前线程的threadID和Java对象头中的threadID是否一致,如果一致(还是线程1获取锁对象),则无需使用CAS来加锁、解锁;如果不一致(其他线程,如线程2要竞争锁对象,而偏向锁不会主动释放因此还是存储的线程1的threadID),那么需要查看Java对象头中记录的线程1是否存活,如果没有存活,那么锁对象被重置为无锁状态,其它线程(线程2)可以竞争将其设置为偏向锁;如果存活,那么立刻查找该线程(线程1)的栈帧信息,如果还是需要继续持有这个锁对象,那么暂停当前线程1,撤销偏向锁,升级为轻量级锁,如果线程1 不再使用该锁对象,那么将锁对象状态设为无锁状态,重新偏向新的线程。
为什么要引入轻量级锁?
轻量级锁考虑的是竞争锁对象的线程不多,而且线程持有锁的时间也不长的情景。因为阻塞线程需要CPU从用户态转到内核态,代价较大,如果刚刚阻塞不久这个锁就被释放了,那这个代价就有点得不偿失了,因此这个时候就干脆不阻塞这个线程,让它自旋这等待锁释放。
引入重量级锁
轻量级解锁时,会使用原子的CAS操作将Displaced **Mark Word
替换回到对象头中,如果成功,则表示没有发生竞争关系。如果失败,表示当前锁存在竞争关系。锁就会膨胀成重量级锁
偏向锁 (Biased Locking):偏向锁是 JVM 对 synchronized 关键字的一种优化,用于减少无竞争情况下的锁操作开销。偏向锁的核心思想是默认情况下,一个线程第一次获得锁时,将对象头中的标志设置为偏向锁,并将线程ID记录在对象头中,之后该线程再次请求锁时,无需进行同步操作即可获取锁。
轻量级锁 (Lightweight Locking):在多个线程之间进行竞争的情况下,JVM 将使用轻量级锁来提高性能。轻量级锁通过使用CAS(比较并交换)操作对对象头进行加锁和解锁,避免了重量级锁的开销。如果出现竞争,轻量级锁会升级为重量级锁。
自旋锁 (Spin Lock):自旋锁是在获取锁时,线程不会被阻塞,而是执行忙等待,不断尝试获取锁。自旋锁适用于锁持有者保持锁时间很短且线程间竞争不激烈的情况。如果锁竞争激烈或锁持有时间较长,自旋锁会浪费 CPU 时间。
锁粗化 (Lock Coarsening):锁粗化是将多个细粒度的连续加锁、解锁操作合并为一个更大范围的加锁、解锁操作,减少了加锁、解锁的次数和开销。这个优化避免了频繁的加锁、解锁对性能的影响。
锁消除 (Lock Elimination):锁消除是在编译器层面进行的优化,根据程序的静态分析,判断某些锁是多余的,可以被消除。例如,当编译器确定某个对象只在单线程中被访问时,就可以将其相关的锁消除。
原文链接:https://blog.csdn.net/weixin_43004044/article/details/129976736
CAS
CAS操作需要输入两个数值,一个旧值(期望操作前的值)和一个新值,在操作期间先比较下在旧值有没有发生变化,如果没有发生变化,才交换成新值,发生了变化则不交换。
CAS操作是原子性的,所以多线程并发使用CAS更新数据时,可以不使用锁
CAS会有哪些问题
- ABA问题
因为CAS需要在操作值的时候,检查值有没有发生变化,比如没有发生变化则更新,但是如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时则会发现它的值没有发生变化,但是实际上却变化了。
ABA问题的解决思路就是使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本号加1,那么A->B->A就会变成1A->2B->3A。
类AtomicStampedReference来解决ABA问题。这个类的compareAndSet方法的作用是首先检查当前引用是否等于预期引用,并且检查当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。
AtomicStampedReference主要维护包含一个对象引用以及一个可以自动更新的整数"stamp"的pair对象来解决ABA问题。
- 循环时间长开销大
自旋CAS如果长时间不成功,会给CPU带来非常大的执行开销
JVM能支持处理器提供的pause指令
- 只能保证一个共享变量的原子操作
当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁。
AtomicInteger底层实现?
- CAS+volatile
- volatile保证线程的可见性,多线程并发时,一个线程修改数据,可以保证其它线程立马看到**修改后的值CAS 保证数据更新的原子性。
await 和notify ,notifyall()
● wait() / wait(long timeout):让当前线程进⼊等待状态(释放锁)。:把线程放到等待队列中,释放的是自己的锁
● notify():唤醒当前对象上⼀个休眠的线程(随机)。*
● notifyAll():唤醒当前对象上的所有线程。
当调用了notify / notifyAll 之后,程序并不会立即恢复执行,而是尝试获取锁,只有得到锁之后才能继续执行
线程通讯(wait方法、notify方法、notifyAll方法)_wait notify notifyall-CSDN博客
join,yeild,sleep,await.
join:让主线程等待(WAITING状态),一直等到其他线程不再活动为止。
join()方法是用wait()
方法实现,但为什么没有通过notify()
系列方法唤醒呀,如果不唤醒,那不就一直等待下去了吗?:在java中,Thread类线程执行完run()方法后,一定会自动执行notifyAll()方法
yeild: 直接进入就绪态, 释放cpu执行权,但依然保存cpu的执行资格:马上释放了CPU的执行权,但是依然保留了CPU的执行资格,有可能在CPU下次进行线程调度时还让这个线程获取到执行权继续执行。
join:线程进入阻塞状态。:使用await实现的,但是主线程执行完后,会自动执行notiryALl()方法:例如在线程B中调用线程A的 join() 方法,那么线程B会进入到阻塞队列,直到线程A结束或者中断线程。:使得一个线程在另一个线程结束后再执行
sleep: 不会释放锁资源。
await:释放锁资源,进入锁等待队列。
wait、sleep、yield、join的区别 - 乐子不痞 - 博客园 (cnblogs.com)
wait 和 sleep 的区别
-
sleep 是 Thread 类的静态本地方法,wait 是 Object 类的本地方法。
-
sleep 不会释放锁资源,但wait 会释放锁资源,而且会加入到锁等待队列中。
sleep 就是把 CPU 的执行资格和执行权释放出去,不会再执行此线程,当定时时间结束后再取回CPU资源,参与CPU的调度,获取CPU资源后就可以继续运行、而如果 sleep 的时候该线程有锁,那么 sleep 不会释放这个锁,而是把锁带着一起进入睡眠状态,也就是说其他需要这个锁的线程根本不可能获得这个锁。如果在睡眠期间其他线程调用了这个线程 interrupt 方法,那么这个线程也会抛出 interruptException 异常返回,这点和 wait 是一样的。
-
sleep 方法不依赖于同步器 synchronized ,但是 wait 方法需要依赖 synchronized 关键字一起才能执行。
-
sleep 不需要被唤醒(休眠之后退出阻塞),但是 wait 需要被 notify() 或 notifyAll() 唤醒。
-
sleep 一般用于当前线程休眠,或者轮休暂停操作,而 wait 多用于多线程直接的通信。
-
sleep 会让出CPU执行时间且强制上下文切换,而 wait 则不一定,wait 后可能还是有机会重新竞争到锁继续执行。
中断线程
- 调用interrput(),通知线程应该中断了:1.如果线程出于被阻塞的状态,那么线程将立即退出被阻塞状态,并抛出一个InterruputedException异常。2.如果线程出于正常的活动状态,那么会将该线程的中断标志设置为true。被设置中断标志的线程将继续正常运行,不受影响。
- 需要被调用的线程配合中断:1.在正常运行任务时,经常检查本线程的中断标志位,如果被设置了中断标志就自行停止线程。2.如果线程处于正常活动状态,那么会将该线程的中断标志设置为true。被设置中断标志的线程将继续正常运行,不受影响。
CyclicBarrier 与 CountDownLatch
区别
CountDownLatch 是一次性的,CyclicBarrier 是可循环利用的
CountDownLatch 参与的线程的职责是不一样的,有的在倒计时,有的在等待倒计时结束。CyclicBarrier 参与的线程职责是一样的。
CountDownLatch 基于 AQS 的共享模式的使用,而 CyclicBarrier 基于 Condition 来实现的
CyclicBarrier原理:全部线程到达后才执行
CyclicBarrier类的内部有一个计数器,每个线程在到达屏障点的时候都会调用await方法将自己阻塞,此时计数器会减1,当计数器减为0的时候所有因调用await方法而被阻塞的线程将被唤醒
CyclicBarrier:让所有线程都等待完成后才会继续下一步行动(同步)。CyclicBarrier类可以实现一组线程相互等待,当所有线程都到达某个屏障点后再进行后续的操作
CyclicBarrier 原理(秒懂) - 疯狂创客圈 - 博客园 (cnblogs.com)
countDownLatch:一个线程必须等其他线程执行完后在执行
CountDownLatch对象设置一个初始的数字作为计数值,任何调用这个对象上的await()方法都会阻塞,直到这个计数器的计数值被其他的线程减为0为止。个线程调用了countDown()方法,则会使count-1;当count的值为0时,这时候阻塞队列中调用await()方法的线程便会逐个被唤醒
线程池
线程池的状态:
-
RUNNING:能接受新提交的任务,并且也能处理阻塞队列中的任务
-
SHUTDOWN;不再接受新提交的任务,但可以处理存量任务
-
STOP:不再接受新提交的任务,也不处理存量任务
-
TIDYING:所有的任务都已经终止
-
TERMINATED:teriminated()方法执行完后进入该状态
JWT
JWT (JSON Web Token) 是目前最流行的跨域认证解决方案,是一种基于 Token 的认证授权机制
jwt 组成
头部(Header)、载荷(Payload)与签名(signature)
header 包含两个部分
token 类型和采用的加密算法
{"alg": "HS256","typ": "JWT"
}payload 存放信息
过期时间,接收方,等等
Signature 签名
header 和payload 两部分都是使用 Base64 进行编码的,即前端可以解开知道里面的信息。
Signature 需要使用编码后的 header 和 payload 以及我们提供的一个密钥,然后使用 header 中指定的签名算法(HS256)进行签名.签名的作用是保证 JWT 没有被篡改过。
签名的过程,实际上是对头部以及负载内容进行签名,防止内容被窜改。如果有人对头部以及负载的内容解码之后进行修改,再进行编码,最后加上之前的签名组合形成新的JWT的话,那么服务器端会判断出新的头部和负载形成的签名和JWT附带上的签名是不一样的。****如果要对新的头部和负载进行签名,在不知道服务器加密时用的密钥的话,得出来的签名也是不一样的。
base64编码是可逆的,JWT中,不应该在负载里面加入任何敏感的数据。在上面的例子中,我们传输的是用户的User ID。这个值实际上不是什么敏感内容,一般情况下被知道也是安全的
面试-JWT认证 - FFLYY - 博客园 (cnblogs.com)
原理
JWT就是通过可逆加密算法,生成一串包含用户、过期时间等关键信息的Token,每次请求服务器拿到这个Token解密出来就能得到用户信息,从而判断用户状态。
如何基于 JWT 进行身份验证?
简化后的步骤如下:
- 用户向服务器发送用户名、密码以及验证码用于登陆系统。
- 如果用户用户名、密码以及验证码校验正确的话,服务端会返回已经签名的
Token
。 - 用户以后每次向后端发请求都在 Header 中带上这个
Token
。 - 服务端检查
Token
并从中获取用户相关信息。
两点建议:
- 建议将
Token
存放在 localStorage 中,放在 Cookie 中会有 CSRF 风险。 - 请求服务端并携带 Token 的常见做法是将
Token
放在 HTTP Header 的Authorization
字段中(Authorization: Bearer Token
)。
jwt 登录问题
使用JWT做登录凭证,如何解决token注销问题
原因:jwt的缺陷是token生成后无法修改,因此无法让token失效。
1)用户登录后,生成JWT,其中包含用户身份
2)以用户id为key,把JWT的id存入redis,只有redis中有id的JWT,才是有效的JWT
3)并且给Redis设置有效期,有效期到自动删除
4)退出登录时,把ID从Redis删除即可
在用户认证成功后,将用户uid和token保存在redis中,并设置token的过期时间,在续约的时候,要同步更新redis中token的过期时间。那么注销的时候,只需要将redis中的token移除即可。
校验的时候,只需要判断redis中token是否存在,即可判断用户是否注销
但是这样子不好的地方,就是每次都要花费一次网络开销去请求redis,还要用redis来存储token,违背了jwt的无状态特性
使用JWT做登录凭证,如何解决token注销问题
token 注销:黑名单机制
- 黑名单机制注销登录时,缓存JWT至Redis,且【缓存有效时间设置为JWT的有效期】,请求资源时判断是否存在缓存的黑名单中,存在则拒绝访问。
token注销:白名单机制
- 白名单机制认证通过时,把JWT存到Redis中。注销时,从缓存移除JWT。请求资源添加判断JWT在缓存中是否存在,不存在拒绝访问。
怎么解决登录超时后的登录续签问题?
答:判断登录是否超时的标准是redis,而不是JWT,因此每次用户访问网关,我们都会刷新redis的数据有效期,保证登录状态不断。
如何解决异地登录或跨设备登录问题?
方案一:不允许多端登录
如果账户在第二个设备登录,自然会将redis中的JWT覆盖,那么之前的登录凭证就成了无效凭证。
方案二:允许多端登录
存入redis时,redis的类型可以选择set,这样一个用户可以具备多个JWT的id,实现多端登录。
https://blog.csdn.net/weixin_51291483/article/details/109211659
jwt 适用场景
JWT 最适合的场景是不需要服务端保存用户状态的场景,但是如果考虑到 token 注销和 token 续签的场景话,没有特别好的解决方案,大部分解决方案都给 token 加上了状态
session
session 认证流程
- 客户端登录成功后,服务器会给每一台主机分配一个唯一的session_id,存入服务器的缓存,还会把这个session_id返回给相应的主机,
- 主机收到session_id后会存入cookie中,以后主机的每一次发送其他类型的请求的操作都会携带这个session_id
- 服务器会将客户端发来的这个session_id和服务端查到的session_id进行对比,如果匹配,则返回给对应主机所需要的资源,否则拒绝
session 缺点
- Session是存储在服务器端的,所以需要占用大量服务器内存
- 不能识别跨域请求
Session方式存储用户id的最大弊病在于Session是存储在服务器端的,所以需要占用大量服务器内存
对于有多个子域名的站点,每个子域名至少会对应一台不同的服务器,在其他的子域名下依然可以取到Session,这要求我们在多台服务器上同步Session。
虾皮二面:什么是 JWT? 如何基于 JWT 进行身份验证? - 简书 (jianshu.com)
redis
缓存穿透
什么是缓存穿透
缓存穿透表示查询一个一定不存在的数据,由于没有获取到缓存,所以没写入缓存,导致这个不存在的数据每次都需要去数据库查询
解决办法
1、对于像ID为负数的非法请求直接过滤掉,采用布隆过滤器(Bloom Filter)。
2、针对在数据库中找不到记录的,我们仍然将该空数据存入缓存中,当然一般会设置一个较短的过期时间。
布隆过滤器
布隆过滤器实现原理
-
假设有一个 bit数组,里面存的只有0和1;0代表不存在,1代表存在
-
我们假设有 多种不同的hash算法来计算同一个值的结果,并反应在改变数组对应下标的值;
-
当值存入时会进行多次不同的hash算法计算,多个计算结果都反应在数组下标上,都改变为1(存在)
-
在判断值是否存在时,就看对应算出的多个下标是否都为1(可能存在),或者任意一个为0(绝对不存在)
布隆过滤器:
-
查询的值都是1,可能存在,也可能不存在
-
如果不全是1,则一定不存在。
优缺点:
优点:不需要存储key,节省空间
缺点:算法判断key在集合中时,有一定的概率key其实不在集合中,已经映射的数据无法删除:存在误判率
- 布隆过滤器使用
【Redisson】Redisson--布隆(Bloom Filter)过滤器_redisson bloomfilter-CSDN博客
- 布隆过滤器的使用场景
- 检查单词拼写正确性
- 检测海量名单嫌疑人
- 垃圾邮件过滤
- 搜索爬虫URL去重
- 缓存穿透过滤
https://pdai.tech/md/algorithm/alg-domain-bigdata-bloom-filter.html
为什么布隆过滤器无法删除元素?
由于布隆过滤器的元素通过多个哈希函数映射到位数组中,如果要删除一个元素,我们需要将位数组中所有映射到的位置都置为0。然而,这样就会影响到其他元素的判断,因为可能其他元素也在相同的位置上。这就是为什么布隆过滤器通常被设计成不支持删除操作的原因。
解决方案:使用计数器布隆过滤器
为了解决无法删除元素的问题,可以引入计数器布隆过滤器。它在传统布隆过滤器的基础上,为每个元素维护一个计数器。当元素被加入时,计数器加1;当需要删除元素时,计数器减1。****只有当计数器归零时,才真正删除该元素。 这种方式虽然增加了一些复杂性,但使得删除操作成为可能。
https://www.bilibili.com/read/cv27737833/?jump_opus=1
如何解决布隆过滤器的判错
布隆过滤器存在的时候,会出现误判,怎么解决?
-
再次查询数据库。
-
如何降低误判率?
- 加大数组的长度,从而降低哈希碰撞的概率;
- 增加哈希函数的个数,从而降低哈希碰撞的概率;
误差率,以及hash个数计算
布隆过滤器现实场景模拟
class BitMap{public int n;public byte[]data;public BitMap(int n){this.n=n;this.data=new byte[(n+7)/8];}public void set(int num){if(num<0 || num>=n)return ;int byteIndex=num/8;//求出索引多少号:即那个字节byte b=data[byteIndex];int bitIndex=num%8;// 0000 0000 :0010 0000 求出那个字节,哪一位 bit位 为1data[byteIndex]=(byte)((1<<bitIndex)|b);}public boolean test(int num){if(num<0 || num>=n)return ;int byteIndex=num/8;//求出索引多少号:即那个字节byte b=data[byteIndex];int bitIndex=num%8;// 0000 0000 :0010 0000 求出那个字节,哪一位 bit位 为1return ((1<<bitIndex)&b)!=0;}}
位图 (BitMap)的代码实现_哔哩哔哩_bilibili
【C++】位图与布隆过滤器(内含相关高频面试题)-CSDN博客
如何快速判断一个 URL 是否存在于 50 亿个 URL 中?_哔哩哔哩_bilibili
位图只能映射整数,所以需要hash将string转成索引整数,然后在使用bitmap .就是布隆过滤器。
误判解决办法:
- 增加bitmap的size
- 增加hash映射函数的个数
hash+bitmap===布隆过滤器
内存限制 1 GB,如何快速判断一个数是否存在于 40 亿整数中?_哔哩哔哩_bilibili:位图(排序,去重,是否存在)
- 申请一个43亿(无符号整型大小4 字节:2^32 个整数)大小的二进制位数组(需要内存空间:43亿/8/1024/1024/1024== 512MB<1G)。: 二进制位数组 索引:40亿整数, value: 对应2进制位 0,1 (0表示不存在,1表示存在)
- 对磁盘中的40亿个整数进行预处理:读取整数将对应的二进制位设为1
- 拿到判断的整数,将它作为索引,去查看它对应的二进制位值。1 表示存在,0表示不存在。
内存限制 1GB,如何对 40 亿个整数进行快速去重呢?_哔哩哔哩_bilibili
位图: 排序( 采用上面判断一个数是否为存在于40亿整数1-2步骤),经历同样的处理,然后,从0开始遍历索引,将值为1的取出来。 这个实现了排序
去重:(( 采用上面判断一个数是否为存在于40亿整数1-2步骤),将索引值为1取出就可以去重。
- 申请一个43亿(无符号整型大小4 字节:2^32 个整数)大小的二进制位数组(需要内存空间:43亿/8/1024/1024/1024== 512MB<1G)。: 二进制位数组 索引:40亿整数, value: 对应2进制位 0,1 (0表示不存在,1表示存在)
- 对磁盘中的40亿个整数进行预处理:读取整数将对应的二进制位设为1
- 遍历二进位数组,取出索引值为1的进行去重。
亿级用户系统中,位图的两个应用场景_哔哩哔哩_bilibili:redis bitmap
1亿用户一个月 31 天签到情况:
1亿*31/8/1024/1024== 370MB 内存空间
连续登录统计
索引表示用户个数:
1表示当天签到
1 亿/8/1024/1024= 12MB 内存空间。快速统计
redisson 分布式锁
分布式锁
分布式锁满足要求
可见性:多个线程都能看到相同的结果,
互斥:互斥是分布式锁的最基本的条件,使得程序串行执行
高可用:程序不易崩溃,时时刻刻都保证较高的可用性
高性能:由于加锁本身就让性能降低,所有对于分布式锁本身需要他就较高的加锁性能和释放锁性能
安全性:安全也是程序中必不可少的一环
分布式锁可以用什么实现?
1.数据库乐观锁
2.基于Redis的分布式锁
3.基于Zookeeper的分布式锁
- 数据库实现分布式锁
1)思路:利用主键唯一的特性来保证互斥,当有多个请求同时提交到数据库的话,数据库会保证只有一个操作可以成功,那么我们可以认为操作成功的那个线程获得了该方法的锁,当方法执行完毕之后,想要释放锁的话,删除这条数据库记录。
缺点:强依赖数据库:数据库挂掉,整个服务就停止运行了
锁没有失效时间:一旦解锁失败,其它线程不能再获取到锁
不可重入:获取到锁后,没有解锁,无法再次获取到锁
2)思路:通过数据库的行锁(排他锁)来保证互斥
缺点:强依赖
不可重入
- redis 实现分布式锁
方法一:setnx(key,1) 实现加锁, del(key) 删除锁。
操作步骤:
1、setnx(lockkey, 1) 如果返回 0,则说明占位失败;如果返回 1,则说明占位成功
2、expire() 命令对 lockkey 设置超时时间,为的是避免死锁问题。
3、执行完业务代码后,可以通过 delete 命令删除 key。
容易出现,步骤1执行完,但是步骤2还未执行的时候,服务器宕机,则会出现死锁情况
方法二:setnx(lockkey, 当前时间+过期超时时间),del(key) 删除锁
1、setnx(lockkey, 当前时间+过期超时时间),如果返回 1,则获取锁成功,继续执行步骤2;如果返回 0 则没有获取到锁,转向 步骤4。
2、expire() 命令对 lockkey 设置超时时间。
3、执行完业务代码后,可以通过 delete 命令删除 key。流程结束
存在问题: 会出现误删锁的问题,线程1阻塞,导致锁自动释放,线程2 获得锁,线程1 到时间,删除2的锁。
解决办法: 增加线程标识:删除锁之前判断锁是不是自己的,如果是自己的锁,删除,不是不删除
以上还会存在问题: 拿锁,比锁,删除锁不是原子型操作,还会导致误删锁问题
解决办法:"lua 脚本实现,拿锁,比锁,删除锁的操作"
redis 实现分布式锁思路
- 利用setnx ex获取锁,并设置过期时间,保存线程标示
- 释放锁时先判断线程标示是否与自己一致,一致则删除锁
- 特性:
- 利用set nx满足互斥性
- 利用set ex保证故障时锁依然能释放,避免死锁,提高安全性
- 利用Redis集群保证高可用和高并发特性
- 特性:
存在问题:1. 重入问题2.不可重试3. 主从一致性
重入问题:重入问题是指 获得锁的线程可以再次进入到相同的锁的代码块中,可重入锁的意义在于防止死锁,
可重入锁他的主要意义是防止死锁,我们的synchronized和Lock锁都是可重入的。
不可重试:当线程在获得锁失败后,不能再次尝试获得锁
主从一致性: 如果Redis提供了主从集群,当我们向集群写数据时,主机需要异步的将数据同步给从机,而万一在同步过去之前,主机宕机了,就会出现死锁问题
lua 脚本如何保证 redis的原子性
Lua脚本能够保证原子性的主要原因还是Redis采用了单线程执行模型。也就是说,当Redis执行Lua脚本时,Redis会把Lua脚本作为一个整体并把它当作一个任务加入到一个队列中,然后单线程按照队列的顺序依次执行这些任务,在执行过程中Lua脚本是不会被其他命令或请求打断,因此可以保证每个任务的执行都是原子性的。
原文链接:https://blog.csdn.net/dl962454/article/details/132766344
Redission实现分布式锁的原理
- 可重入: 利用hash 结构记录线程id 和重入次数
- 可重试: 利用信号量和pubsub功能实现等待,唤醒,获取锁失败的重试机制
- 超时续约: 利用watchaDog,每隔一段时间 ,重置超时时间。
锁续期怎么实现(看门狗机制)
锁续约(看门狗机制)其实就是每次加锁成功后,会⻢上开启⼀个后台线程, 每隔10s检查⼀下key是否存在,如果存在就为key续期30s。
当前持有锁的线程被中断了,会停止锁续约,即杀死看门狗;
锁重入原理
*当调用该锁的时候,如果该锁存在,那么我就将里面的计数器数值加一,然后手动返回一个True,进行逻辑操作,而释放锁的操作就变更为计数器的数值减一,当计数器为零的时候就释放锁,如果锁不存在就会返回一个false*
Redisson可重入锁实现原理 + 步骤(图解)_可重入锁 redis 实现-CSDN博客
锁可重试
Redisson锁的超时重试代码尝试在给定的等待时间内获取锁,通过订阅锁状态变化的方式实现异步等待,如果在等待过程中锁未能成功获取,则通过取消订阅和执行获取锁失败的操作进行超时处理。在获取锁后,通过循环不断尝试续租锁,同时在等待期间通过异步消息通知机制等待锁释放或续租成功,确保在给定的总等待时间内获取或续租锁,最终返回获取锁的成功或失败状态。
Redisson分布式锁的可重入、重试、续约机制原理-CSDN博客
redis 和mysql 一致性
限流:防刷设计
- redis+lua+AOP 实现防刷机制
IP限流,即一定时间内同一IP访问的次数是有限的。
将用户的IP地址当Key,一段时间内访问次数为value,同时设置该Key过期时间
- 令牌桶算法
主要在于令牌桶算法能够在限制调用的平均速率的同时还允许一定程度的突发调用
- 系统以恒定的速率产生令牌,然后将令牌放入令牌桶中
- 令牌桶有一个容量,当令牌桶满了的时候,再向其中放入的令牌就会被丢弃
- 每次一个请求过来,需要从令牌桶中获取一个令牌,假设有令牌,那么提供服务;假设没有令牌,那么拒绝服务。
- 漏桶算法
漏桶算法原理:水(请求)先进入到漏桶里,人为设置一个最大出水速率,漏桶以<=最大出水速率的速度出水.
- 存下请求
- 匀速处理
- 多余丢弃
缺点:
无法面对突发的大流量
无法有效利用网络资源
漏桶算法和令牌桶算法 - harara - 博客园 (cnblogs.com)
限流算法(计数器、滑动时间窗口、漏斗、令牌)原理以及代码实现_时间滑动窗口限流原理-CSDN博客
redis 中的zset
zset 设计
有序集合 ZSet 来说,每个存储元素相当于有两个值组成的:一个是有序集合的元素值,一个是排序值:是一个没有重复元素的字符串集合:常用作排行榜等功能
底层原理:hash+跳表
hash,hash 的作用就是关联元素 value 和权重 score,保障元素 value 的唯一性,可以通过元素 value 找到相应的 score 值。
跳跃表,跳跃表的目的在于给元素 value 排序,根据 score 的范围获取元素列表
跳表:多级索引链表
跳跃表(skiplist)是一种有序数据结构,它通过在每个节点中维持多个指向其它节点的指针,从而达到快速访问节点的目的。链表加多级索引的的结构,就是跳跃表。
数组支持随机访问的线性结构,底层使用二分查找。链表不支持随机访问的线性结构。 跳跃表中查询任意数据的时间复杂度就是 O(logn)
Zset 为什么用跳表不用平衡树
从内存占用上来比较,跳表比平衡树更灵活一些。平衡树每个节点包含 2 个指针(分别指向左右子树),而跳表每个节点包含的指针数目平均为 1/(1-p),具体取决于参数 p 的大小。如果像 Redis里的实现一样,取 p=1/4,那么平均每个节点包含 1.33 个指针,比平衡树更有优势。
在做范围查找的时候,跳表比平衡树操作要简单。在平衡树上,我们找到指定范围的小值之后,还需要以中序遍历的顺序继续寻找其它不超过大值的节点。而在跳表上进行范围查找就非常简单,只需要在找到小值之后,对第 1 层链表进行若干步的遍历就可以实现。
从算法实现难度上来比较,跳表比平衡树要简单得多。平衡树的插入和删除操作可能引发子树的调整,逻辑复杂,而跳表的插入和删除只需要修改相邻节点的指针,操作简单又快速。
redis的基础底层篇 zset的详解_redis zset底层-CSDN博客
【Redis —ZSet介绍和应用场景】-CSDN博客
zset 设计延迟队列
基于 ZSet 实现延迟队列的原理是利用有序集合的特性
- 将延迟消息作为 ZSet 的成员,延迟时间作为成员的分数(score)。延迟时间可以是一个未来的时间戳,表示消息应该在该时间戳之后被处理。
- 将消息插入到 ZSet 中,使用ZADD命令可以将消息添加到 ZSet 中,并指定其延迟时间作为分数。
- 定期轮询 ZSet,检查是否有到期的延迟消息。可以使用ZRANGEBYSCORE命令来按照分数范围查询 ZSet 中的消息。
- 如果找到到期的消息,即分数小于当前时间的消息,就将其取出并进行相关处理。可以使用ZPOPMIN命令将最小的成员(即分数最小)移出 ZSet,然后进行消息的处理逻辑。
Redis 实现延迟队列 - 程序员老郑 - 博客园 (cnblogs.com)
redis 做消息队列
- 一般使用 list 结构作为队列,rpush 生产消息,lpop 消费消息。当 lpop 没有消息的时候,要适当 sleep 一会再重试
- 如果对方追问可不可以不用 sleep 呢?:list 还有个指令叫 blpop,在没有消息的时候,它会阻塞住直到消息到来。
- 如果对方追问能不能生产一次消费多次呢?:使用 pub/sub 主题订阅者模式,可以实现1:N 的消息队列
- 如果对方追问 pub/sub 有什么缺点?:在消费者下线的情况下,生产的消息会丢失,得使用专业的消息队列如 RabbitMQ等。
- 如果对方追问 redis 如何实现延时队列?:sortedset,拿时间戳作为score,消息内容作为 key 调用 zadd 来生产消息,消费者用 zrangebyscore 指令获取 N 秒之前的数据轮询进行处理。
redis I/0 多路复用
IO多路复用:单个线程同时操作多个IO请求。
- select调用:查询有多少个文件描述符需要进行IO操作,特点:轮询次数多,内存开销大,支持文件描述符的个数有限。底层采用的是数组要再O(N)的时间复杂度下对描述符进行遍历
- poll调用:和select几乎差不多。但是它的底层数据结构为链表,所以支持文件描述符的个数无上限。要再O(N)的时间复杂度下对描述符进行遍历
- epoll:更加高效的调用方式,底层的数据结构为红黑树加链表。避免大内存分配和轮询。使用O(1)的时间复杂度就能找到发送事件的描述符
rabbitmq 消息队列
MQ 使用场景
- 系统解耦
- 流量消峰
- 流量控制
- 异步处理
如何保证消息100%投递
1. 生产者:生产阶段
从消息在 Producer
创建出来,经过网络传输发送到 Broker
端
解决办法:
消息队列通过最常用的请求确认机制(事务机制,confirm确认机制),来保证消息的可靠传递。只要 **Producer**
收到了 **Broker**
的确认响应,就可以保证消息在生产阶段不会丢失
具体实现:
生产者那里设置开启 Confirm
模式之后,生产者每次发送的消息都会分配一个唯一的 ID,如果消息成功写入 Broker
中,Broker
会给生产者回传一个 ack
消息,告诉你说这个消息 ok
了
如果返回Non ,就会重试
2. broker:存储阶段
RabbitMQ收到消息后将这个消息暂时存在了内存中,那这就会有个问题,如果RabbitMQ挂了,那重启后数据就丢失了
message消息到达RabbitMQ后先是到exchange交换机中,然后路由给queue队列,最后发送给消费端。
因此要给exchange ,queue,message 都要持久化:即使RabbitMQ收到消息后挂了,重启后会自行恢复消息
还是存在一个问题:RabbitMQ收到消息还没来得及将消息持久化到硬盘时,RabbitMQ挂了,这样消息还是丢失了,或者RabbitMQ在发送确认消息给生产端的过程中,由于网络故障而导致生产端没有收到确认消息,这样生产端就不知道RabbitMQ到底有没有收到消息,就不好做接下来的处理。
解决办法:消息入库===将要发送的消息保存到数据库中。
发送消息前先将消息保存到数据库中,有一个状态字段status=0
,表示生产端将消息发送给了RabbitMQ但还没收到确认。在生产端收到确认后将status设为1,表示RabbitMQ已收到消息。
所以生产端这边开一个定时器,定时检索消息表,将status=0并且超过固定时间后,还没收到确认的消息取出重发。存在一个问题消息重复
为什么设置固定时间:消息刚发出去还没来得及确认这边定时器刚好检索到这条status=0的消息,所以给个时间)
第二种情况下这里会造成消息重复,消费者端要做幂等性
可能重发还会失败,所以可以做一个最大重发次数,超过就做另外的处理。
3. 消费者:消费阶段
Consumer
从 Broker
上拉取消息,经过网络传输发送到 Consumer
上。
因为RabbitMQ的自动ack机制,即默认RabbitMQ在消息发出后就立即将这条消息删除,而不管消费端是否接收到,是否处理完,导致消费端消息丢失时RabbitMQ自己又没有这条消息了: 解决办法 手动ack 机制。
字节二面:引入RabbitMQ后,你如何保证全链路数据100%不丢失? (qq.com)
消息队列之RabbitMQ (qq.com)
消息幂等性
请求多次执行和一次执行的结果或者影响是一样的。
为什么需要实现幂等性
前端重复提交表单
用户恶意进行刷单
接口超时重复提交
消息进行重复消费
幂等性解决办法
数据库唯一id:插入
唯一主键的实现主要是利用数据库中主键唯一约束的特性,一般来说唯一主键比较适用于“插入”时的幂等性,其能保证一张表中只能存在一条带该唯一主键的记录。
使用数据库唯一主键完成幂等性时需要注意的是,该主键一般来说并不是使用数据库中自增主键,而是使用分布式 ID 充当主键,这样才能能保证在分布式环境下 ID 的全局唯一性。
数据库乐观锁: 更新
数据库乐观锁方案一般只能适用于执行更新操作的过程,我们可以提前在对应的数据表中多添加一个字段,充当当前数据的版本标识。
这样每次对该数据库该表的这条数据执行更新时,都会将该版本标识作为一个条件,值为上次待更新数据中的版本标识的值
防重token:实现幂等性:插入,更新,删除
- 客户端请求服务器接口获取token。
- 服务器将token返给客户端的同时将信息(这里包括用户信息、和token)存储到redis中。
- 请求业务接口时,将token放入header中进行接口请求。
- 服务器通过用户信息和token检查token是否还存在,如果存在就删除,如果不存在直接返回结果。
- 响应服务器请求结果。
- 需要生成全局唯一
Token
串: - 需要使用第三方组件
Redis
进行数据效验
高并发下: lua或者redission 分布式锁实现:一口气说出四种幂等性解决方案,面试官露出了姨母笑~ - 掘金 (juejin.cn)
接口幂等性实现--Token令牌 - AmourLee - 博客园 (cnblogs.com)
一口气说出四种幂等性解决方案,面试官露出了姨母笑~ - 掘金 (juejin.cn)
消息积压
- 消息积压的原因
消费者处理能力不足 1. 解决办法, 增加消费者 2. 提高消费者的处理能力 3. 提高队列的容量。
消费者处理失败 2. 修复consumer 问题,让他恢复:临时紧急扩容, 停掉现有的消费者,上线新的消费者。
网络延迟或故障: 修复网络问题。
- 解决办法
-
增加消费者
-
提高消费者处理能力
-
增加队列容量
1. rabbitMQ中的消息过期失效,消息积压导致消息丢失?
- rabbitMQ是可以设置过期时间的。如果消息在queue里积压一定时间就会被rabbitMQ清掉,造成的问题是因积压超时而丢数据了
解决方法是: 重导入数据。手工查找丢的数据,重新灌入MQ里面去(一般不设rabbitMQ的过期时间)
消息积压在MQ里导致MQ快满了
解决方法:临时写程序接入消息来消费,消费一个丢一个(不写库了,快速把MQ里的消息处理掉)
然后到晚上再采用2的方法,补数据。(针对的是线上问题)重导入数据。手工查找丢的数据,重新灌入MQ里面去。
如何解决消息队列的延时及过期失效问题? - 黑子菜园 - 博客园 (cnblogs.com)
2. 消息积压数百万,解决办法
- 优先处理重要的消息:可以将重要的、紧急的消息先处理掉,以保证系统的稳定性和可靠性
- 增加处理能力:可以通过优化代码、升级硬件或者使用更强大的消息队列中间件来提高处理能力
- 使用消息持久化:如果积压的消息数量非常大,可能需要考虑使用消息持久化的方式来保存这些消息,避免因为系统故障等原因导致数据丢失。
死信队列
消息如何变成死信队列
- 消息被否定接收,消费者使用basic.reject ,并且requeue 重回队列属性设为false: 消费者拒绝接受
- 消息在队列里得时间超过了该消息设置的过期时间(TTL): 消息过期了
- 消息队列到达了它的最大长度,之后再收到的消息。:消息队列满了
死信队列原理
当一个消息再队列里变为死信时,它会被重新publish到另一个exchange交换机上,exchange就为DLX
,在声明正常的业务队列时添加一个可选的"x-dead-letter-exchange"参数,值为死信交换机,死信就会被rabbitmq重新publish到配置的这个交换机上
延迟队列 实现方法
方案一:基于死信队列中的消息TTL过期模式的进行改造,不监听对应队列,使消息过期后全部进入死信队列以达成延时效果,主要有队列TTL和消息TTL两种
方案二:使用延时队列插件,让交换机管理消息延时时间(常用)
重试队列:
重试队列: 在 RabbitMQ 中,可以通过设置消息的TTL(Time To Live)属性来实现重试队列。当消息在目标队列中消费失败时,可以将消息重新发送到绑定的重试队列,并设置一定的TTL,即重试的时间间隔。如果消息在重试队列中未被消费成功,则会再次被发送到重试队列,直到达到设置的重试次数。如果重试次数达到上限,消息会被丢弃或者发送到死信队列。
原文链接:https://blog.csdn.net/en_joker/article/details/130391769
rabbitmq实现顺序消费
顺序消费方法
- 单线程消费:rabbitmq 的队列有序,只要保证消费有序即可,即单线程消费保证有序消费。
- 有序分片消费: 将消费按照规则分割,对应一个queue,每一个queue对应一个消费者。
- 使用rabbitmq提供优先级队列: 优先级队列会按照消息的优先级进行排序,设置优先级保证消息的顺序。
spring boot rabbitmq 如何保持顺序消费_rabbitmq怎么保证顺序消费-CSDN博客
问题分析
顺序消费,即保证消息的发送顺序要与消息的消费顺序保持一致。比如数据库对一条数据依次进行了 插入->更新->删除操作,这个顺序必须是这样,如果在同步过程中,消息的顺序变成了删除->插入->更新,那么原本应该被删除的数据,就没有被删除,造成数据的不一致问题。
无序情景1: 一队列多消费者
一个queue,有多个consumer去消费,这样就会造成顺序的错误,consumer从MQ里面读取数据是有序的,但是每个consumer的执行时间是不固定的,无法保证先读到消息的consumer一定先完成操作,这样就会出现消息并没有按照顺序执行,造成数据顺序错误
解决办法: 队列与消费者一对一
将原来的一个queue拆分成多个queue,然后保证每个queue对应一个consumer,就是多一些queue而已,确实是麻烦点;这样也会造成吞吐量下降,可以在消费者内部采用多线程的方式取消费。
有序分片消费:
有序分片消费,可以先将消息队列按照一定的规则(如消息 ID、时间戳等)分成多个分片,然后每个分片使用一个单独的消费者线程消费消息。要保证消息的顺序,需要在分片规则上做额外的处理,确保分片规则是有序的,然后让每个消费者只消费自己所负责分片的消息。
无序2:一队列一消费者,消费者多线程消费
一个queue对应一个consumer,但是consumer里面进行了多线程消费,这样也会造成消息消费顺序错误
解决办法: 消费者使用内存队列
还是一个queue,但是也只对应一个consumer,然后这个consumer内部用内存队列做排队,然后分发给底层不同的worker来处理:
消费者不直接消费消息,而是将消息保存到内存队列中,根据关键值(比如订单id) 进行hash映射,将具有关键值相同的数据,放到相同的内存队列中,消费者开启对应线程消费。
RabbitMQ消息如何保证顺序消费_mq中间件如何保证消费顺序-CSDN博客
参考链接
消息队列之RabbitMQ (qq.com)
消息队列之RabbitMQ (qq.com)
场景设计
如何找出排名前500的数
有20个数组,每个数组有500个元素,并且是有序排列好的,现在如何在这20 * 500
个数中找出排名前500的数?
分析:
top K问题,可以设置一个大小为500的小顶堆,
第一次先读取其中1个数组的500个数存入小顶堆,
后续读取其他数组的数字,对每个数字,如果该数字大于堆顶元素,
则删除堆顶元素,添加当前数字,所有数字遍历完,
则堆中的500个元素,就是前500大的元素
原文链接:https://blog.csdn.net/qingyuanluofeng/article/details/97617389
如何找出排名前 500 的数? | Java学习&面试指南-程序员大彬 (topjavaer.cn)
出现频率最高的100个词 | Java学习&面试指南-程序员大彬 (topjavaer.cn)
给两个文件,分别由100亿个query(query是sql的查询语句或者是网络请求url,一般是字符串),我们只有1G内存,如何找到交集?给出精确算法和近似算法。
利用哈希分割 + 红黑树
-
将文件A和文件B分割成n个小文件,这里分割成1000个小文件。每个文件就是300~600M。
-
利用哈希分割:利用哈希字符串转整形函数:i = hashstr(query) % n,hashstr是字符串转整形函数,query是文件A和文件B中的query。n是文件个数,这里是1000。求出来的i是将对应的query保存到对应第i个文件中。
-
文件A和文件B求query放到哪个文件的hashstr用的是同一个。这时通过hashstr求出来的相同的值一定在A和B同一个编号小文件中,也就是Ai和Bi中。
文件A和文件B的交集一定都保存到了同样编号的小文件中,其中可能也保存了不是交集的值。
原文链接:https://blog.csdn.net/weixin_57023347/article/details/120247588
给一个超过100G的log file(日志文件),log里保存着IP地址,设计一算法找出出现次数最多的IP?与上题想同找到top K IP?
- 利用哈希切割,先将log file分割成多个小文件。分割的小文件只要可以保存到内存即可。假设我这里分割成1000个小文件,每个文件最小100M。
2. 在将每个小文件的IP保存到map<string,int>中来进行计数,设置一个遍历pair<string,int> max,来保存每个小文件中出现次数最多的键值对。就可以得到出现次数最多的IP。
原文链接:https://blog.csdn.net/weixin_57023347/article/details/120247588
面试题
牛客网 - 找工作神器|笔试题库|面试经验|实习招聘内推,求职就业一站解决_牛客网 (nowcoder.com)
海量数据处理博客
哈量数据处理面试题(哈希切割,位图,布隆过滤器)_面试 哈希切割-CSDN博客