项目八股 面经简历整理
抽奖大转盘
一般项目中都是MVC,做起来简单,随着项目变大,service层,很多不同业务的接口和实现类,导致项目结构很乱,dto,vo系统内部数据流转类,维护和迭代不太好做,DDD在一定程度上解决了这个问题,domain 领域层,把不同业务以合理的角度区分为不同的领域,也就是不同包,不同的包下面维护自己的service和系统内部流转类,杜绝相互调用,比如A领域 不能调用B领域,解耦,维护和扩展。
有一个抽象类,他可能有一个或者多个子类,然后将一个流程或者一个算法的通用流程定义在模板中,也就是抽象类中这个方法里边,然后子类可以按照这个流程去实现,当迭代或者扩展的时候,程序员可以直接看到这个流程,方便迭代和扩展,在我的系统中就是定义了一个规范的抽奖流程,我这个是给之后扩展用的,然后在后续的增加新功能的时候只需要继承这个抽象类,比如我使用的抽奖策略,,比如底层的抽奖算法需要修改的时候,可以换个抽奖算法。有很好的扩展性和可维护性
责任链模式通俗讲就是 收到请求后,把请求放到一个链中去处理,包含很多处理者,前一个处理后给下一个去处理,流程审批,过滤器,方便扩展,在我的抽奖系统里,在抽奖之前,需要对我们这次使用的转盘的一些玩法进行过滤,首先是查询用户是否在黑名单里,是否是多少积分抽更好的奖品,在执行这个过滤之前,要先装配责任链,首先就查询这个抽奖转盘的所有玩法信息,然后根据这个标识去装配,就是把spring容器中对应的处理者装配到责任链上,存到一个ConcurrentHashMap里面,方便下次使用,最后返回的结果是这个责任链的头节点,后面就开始执行过滤,比如查询该用户是否是黑名单 是的话直接返回给用户一个保底的奖品,不是就继续交给下一个节点处理,判断该抽奖转盘这次抽奖是否是权重抽奖,是的话就进行抽奖,不是就进行默认抽奖
工厂模式是一种创建型设计模式,其主要目的是提供一个创建对象的接口,在我的项目中责任链和规则树的装配都是在工厂中生产的,隐藏了对象的具体实现细节,封装对象的创建过程,也是一个解耦的过程, 提高了代码灵活性和可维护性。我这个抽奖项目中的责任链和规则树都是通过工厂去装配的
首先就是我先讲一下之前的抽奖算法的设计,在这个抽奖转盘里有很多奖品 不同概率,举例奖品ABC 20% 30% 50 % 生成一个随机数,两个问题就是if太多 然后可读性差,扩展性差,第二就是 每次请求的时间不一样,造成系统不稳定,用户体验问题,然后就是我这个新的抽奖设计, 首先是在抽奖活动上架之后进行一个装配,装配的原理就是在redis里存一个Hash类型的数据结构 key 就是一个 举个例子就是 举例奖品ABC 20% 30% 50 %,对应的奖品id,根据redis的key 加上一个随机数 就能获取到对应的奖品id,。 装配的流程就是查询该奖盘的所有奖品,找到里面的最小概率,比如一个概率是1%,然后就创建一个100大小的集合,然后遍历所有的集合,向集合中填充数据,然后将这个范围大小存入redis中,在将这个map存入redis中,在使用的时候首先将这个范围查出来, 然后根据这个范围生成随机数,完成抽奖。让后我这个抽奖还配置了对应的权重抽奖玩法,比如我的积分到了4000分,我就可以抽更好的奖品,这个时候也是需要装配的,这个装配和上面差不多,只不过是装配的时候过滤掉了一些用不到的奖品,装配好之后用的rediskey也不同,这个就是整个的抽奖算法层面
在整个抽奖活动中,我将抽奖流程分隔为了抽奖前和抽奖后,抽奖前是使用了责任链模式进行前置处理,抽奖后创建了一个规则树进行抽奖的后置处理,然后我这个抽奖活动中有两个玩法 就是黑名单过滤玩法,就是用户可能会有薅羊毛或者做了不好的事情,会把这个用户加入黑名单,还有一个玩法就是权重抽奖,当用户有4000积分 就不会抽到哪些差的奖品,然后就是责任链这这块,我先创建了一个接口用于 里面有一个执行的方法,用于让责任链上的每个节点都实现这个方法,然后定义了一个抽象类抽象类定义了每个节点的next也就是下一个节点,最后让黑名单过滤,权重过滤和正常抽奖三个节点放到了里面,真正装配责任链时我是配置了写了一个工程类,首先根据该策略id查询出 工厂类的一个map中是否有该责任链有就直接返回,没有就根据该策略id查询出对应的玩法标识,然后根据该玩法标识查询出spring中每一个责任链节点,遍历把他们连接起来,存入map中,然后返回该头节点,使用的时候就直接调用头节点的处理方法 处理完成后会默认调用下一个,除非中间有返回的方法。
在抽奖活动完成后,会生成一个奖品的id,抽奖后我设计了一个规则树,对奖品进行校验,对应有1个玩法,就是这个奖品要抽奖多少次后才能解锁,如果次数不够的话,还抽到了这个奖品就返回一个保底的奖品,如果次数够了, 再判断该奖品的库存是否有剩余,有剩余就直接返回抽奖结果,没有剩余就就也返回一个保底奖品,反正这个过程是用户感知不到的吗, 首先就是装配规则树,然后我这里设计了三个数据库表,一个是树的整体信息,一个树树的节点信息,一个是线,有from 和 to两个字段,用于连接两个节点的线,根据数据库的内容构建一颗规则树,当然这个信息也是优先从缓存里面查,里面没有的话就从数据库里面构建,构建好之后就开始过滤,首先根据根节点的标识,查询出spring中装配的对应的树节点,然后开始进行处理,过滤完成后,根据过滤的结果,获取到该节点对应的线,根据里面的form to找到需要过滤的下一个节点,知道为空。完成规则树的操作
在我的抽奖活动之前,有一个装配操作,同时还会将该活动 的库存存入redis,也就是这个抽奖活动对外能用多少次,抽奖时就扣减redis中的库存,第一个就是减缓数据库的压力吗,然后因为是redis 还能保证,用的decr的命令,还能保证不超卖的一个问题,让后redis和数据库的数据同步我用的是使用,当扣减库存后将扣减库存的信息加入一个延迟队列,让后又定义 了一个定时任务定时去消耗延迟队列的消息,去扣减库存,极大的减少了数据库的压力,最后当库存扣减为0时,使用mq发送一个消息,收到消息后 清空延迟队列,将数据库进行更新,两个方面保证数据库和缓存的一致性,
在抽中之后进行发奖 ,首先将用户的中奖信息存入数据库,使用Rabbitmq发送发奖信息,然后再将该任务存入任务表,定义消费者去发奖,当Rabbitmq成功发送信息后不做处理,mq发送信息失败时,将任务表的任务状态标识为失败,使用定时任务定时去扫描任务表,将失败的信息重新发送,保证发奖的成功。幂等性的话mq自己会做处理
URL链接重定向系统
首先用户输入原始链接之后 ,根据原始链接生成一段随机的6位的短链接,然后使用布隆过滤器判断短链接是否存在,但是布隆过滤器可能会存在一个误判问题,然后我在介绍一下这个误判问题,布隆过滤器就是用于检测一个元素是否存在于一个集合中,我使用的是redission实现的布隆过滤器,他底层就是先去初始化一个比较大的数组,里面存放 的是二进制的0或者1,一开始都是0,当一个key来了之后经过三次hash计算,模数组的长度然后将这三个位置的0改为1,三个位置就能表明一个元素的存在,这个误判产生于比如A,B已经存在于布隆过滤器,然后对C进行判断计算的时候,刚好,c的三个位置已经都被a,b改为了1 ,这样实际上c并不存在,但是返回的结果是存在,这样就产生了误判的问题,但是如果判断结果是c不存在那么一定是不存在的,如果判断结果是c存在,c大概率是存在,小概率不存在,所有当布隆过滤器判断该短链接不存在时就将该短链接的数据直接存入数据库并加入到布隆过滤器中,如果判断结果时短链接已经存在就不断循环遍历直到能存入数据库,当然这样的概率是非常小的,然后我还做了个控制,当次数大于10的时候直接返回给用户创建失败请重新创建,保证系统执行时间的稳定。
在用户在浏览器地址栏输入创建后返回的短链接时,因为这个短链接有可能并发量比较高,所以这个短链接是使用了redis缓存增加的响应速度,具体的流程就是首先查询redis是否有记录,有就直接重定向到原始链接,没有就继续向下运行, 然后这个时候就会出现一个缓存穿透问题,就是redis里没有这个数据,mysql里面也没有这个数据,导致有些人可能会恶意攻击数据库,造成缓存穿透的问题,在我的设计中,使用了布隆过滤,将短链接加入到布隆过滤器中,当查询到短链接不存在时,直接返回一个找不到该短链接的信息,同时我还在redis里面缓存了空值,来解决其中的误判的问题,当然这个概率时非常小的,后面就是短链接存在于数据库了,存在数据库的情况下,首先加了一个分布式锁,获取到锁的线程首先去重建缓存,这个双重判定第一个就是判断是否获取到锁,然后第二个就是重建缓存之后释放锁,后面的线程也获取到 了锁,首先判断缓存是否已经重建,避免多次重建缓存。造成没必要的内存占用
当用户在使用短链接时,有一个功能时记录用户的网络还是数据流量,设备pc还是手机,访问的ip,ip的位置等各种日志信息,因为我使用了mq进行日志信息的记录,异步处理日志信息,提高系统地响应速度
我的短链接系统中使用了shardingsphere进行分库分表,首先我有三个表一个时group分组表分库分表标识用的是username,一个是link链接表,使用的分组标识的groupid也就是分组id,还有一个是路由表,分库分表标识是完整短链接。首先就是根据该用户创建的完整短链接,在缓存重建的过程中,根据完整短链接查询出对应的分组,在根据对应的分组查询出对应的原始链接链接。然后将该短链接和原始链接的映射缓存入数据库中。完成数据的重建过程。如果没有会怎么样。。。后管用户可以看到所有的短链接,当要查询短链接的详细信息是,需要查出其对应的原始链接,和更多的想关信息。
使用Sentinel对某些需要的接口进行限流,然后流量过高会进行降级处理,并且还使用了lua脚本操作redis,单个用户一段时间内只能访问多少次进行限制,用户每访问一次,该用户的使用次数就增加一次,当超过一定次数就进行单独处理。我是生成了一个uuid存入到cookie中,用来区分每个用户,然后以cookie加上一个固定的字符串存入redis key是次数,然后还有一个超时时间,以此来保证单个终端的访问次数。
基础八股 视频面经简历整理
集合
基础知识
List: 存储的元素是有序的、可重复的。(vector 线程安全 )(ArrayList非线程安全,LinkList非线程安全)
Set:无序,存储的元素不可重复的。(HashSet底层是hashmap,TreeSet底层是红黑树)
Queue: 按特定的排队规则来确定先后顺序,存储的元素是有序的、可重复的。
Map:使用键值对(key-value)存储,key 是无序的、不可重复的,value 是无序的、可重复的,每个键最多映射到一个值 (HashMap非线程安全)(concurrentHashMap线程安全)
ArrayList
ArrayList在添加元素时,如果当前元素的个数达到了内部数组容量的上限,会触发扩容操作,首先计算出新的容量,创建新的数组,1.5倍扩容,然后将原来的数据复制过去,最后更新引用
数组转list,修改原数组集合也会改变,反之集合转数组,修改集合,数组不会改变。
是否保证线程安全: ArrayList 和 LinkedList 都是不同步的,也就是不保证线程安全;底层数据结构: ArrayList 底层使用的是 Object 数组;LinkedList 底层使用的是 双向链表 数据结构插入和删除是否受元素位置的影响:ArrayList 采用数组存储,所以插入和删除元素的时间复杂度受元素位置的影响。 比如:执行add(E e)方法的时候, ArrayList 会默认在将指定的元素追加到此列表的末尾,这种情况时间复杂度就是 O(1)。但是如果要在指定位置 i 插入和删除元素的话(add(int index, E element)),时间复杂度就为 O(n)。因为在进行上述操作的时候集合中第 i 和第 i 个元素之后的(n-i)个元素都要执行向后位/向前移一位的操作。LinkedList 采用链表存储,所以在头尾插入或者删除元素不受元素位置的影响(add(E e)、addFirst(E e)、addLast(E e)、removeFirst()、 removeLast()),时间复杂度为 O(1),如果是要在指定位置 i 插入和删除元素的话(add(int index, E element),remove(Object o),remove(int index)), 时间复杂度为 O(n) ,因为需要先移动到指定位置再插入和删除。是否支持快速随机访问: LinkedList 不支持高效的随机元素访问,而 ArrayList(实现了 RandomAccess 接口) 支持。快速随机访问就是通过元素的序号快速获取元素对象(对应于get(int index)方法)。内存空间占用: ArrayList 的空间浪费主要体现在在 list 列表的结尾会预留一定的容量空间,而 LinkedList 的空间花费则体现在它的每一个元素都需要消耗比 ArrayList 更多的空间(因为要存放直接后继和直接前驱以及数据)。
线程安全的arraylist 。CopyOnWriteArrayList 或者自己加锁或者Collections.synchronizedList(new ArrayList)底层使用的时synchronized效率不高
CopyOnWriteArrayList写时复制,读操作时不做处理,当写操作时会首先获取到锁,然后复制出来一个备份,对备份的数据进行操作,读操作读到的还是旧的数据,内存占用问题,因为需要复制一份出来,造成内存占用,造成频繁垃圾回收,数据一致性问题,会导致读写时,刚刚写入的数据不能及时被读到,写完数据之后,引用指向新的数据时,才能被读到。
HashMap
添加数据时,首先根据key计算数组的下标,没有数据就直接加入,有数据,key相同则替换,不同则加入红黑树或者链表中,插入成功后如果size的大小大于数组长度 * 0.75加载因子计算出的扩容阈值,就进行扩容。
默认大小是16,两倍扩容,hashMap默认的负载因子是0.75,如果hashmap中的元素个数超过了总容量75%,则会触发扩容,扩容之后会创建一个新数组,将老数据放入新数组中,计算新的节点位置,如果时红黑树直接走红黑树的添加,链表就走链表添加。
寻址算法,首先计算hashcode,然后二次哈希,让其分布更均匀。
数组长度为什么2的n次幂:在扩容时,通过hash计算索引位置的时候,如果时二的n次幂的话,刚好就是在二进制的hash码多算了一位,如果多算的这一位时0,那么位置不变,如果多算的这一位时1,则加上一个原来的大小。省去了重新计算hash的时间,并且新增的0或者1还可以认为时随机的。
在jdk1.7时进行扩容后复制链表时采用的头插法,有可能导致死循环,线程不安全导致的形成了一个带环的链表,jdk8尾插法解决了这个问题。
HashMap:JDK1.8 之前 HashMap 由数组+链表组成的,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的(“拉链法”解决冲突)。JDK1.8 以后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为 8)(将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树)时,将链表转化为红黑树,以减少搜索时间。
ConcurrentHashMap
ConcurrentHashMap:jdk1.7数组加链表,jdk1.8数组加链表加红黑树,jdk1.7当一个key需要被存入时,首先有一个Segment数组,然后获取这个数组元素关联的ReentrantLock,通过cas尝试获取锁,获取到锁之后再将数据添加到数组或链表中,这样通过数组将锁的粒度缩小了,减少了竞争,不同位置上添加数据时都不构成竞争关系,jdk1.8中和hashmap一样,舍弃了之前的设计,使用cas控制数组节点的添加,synchor锁锁住了链表或者红黑树的首节,只要hash不冲突,就不会产生并发冲突。
JVM
内存结构
主要是分为两类,
线程私有
虚拟机栈:就是每个线程运行时需要的内存。因为他是栈吗 ,所以是先进后出的,由多个栈帧组成,对应着每次方法调用所占用的内存,每个线程只有一个活动栈帧,对应着正在执行的方法 递归调用会导致栈内存溢出
本地方法栈 :与虚拟机栈相似,区别在于本地方法栈调用本地的native方法,由c/c++编写。当调用native方法时,会使用本地方法栈来执行这个方法
程序计数器 :每个线程一份 用于记录正在执行的字节码指令的地址
线程共享
方法区:jdk1.7有一个永久代类信息,静态变量,常量(运行时常量池),和编译后的代码,在jdk1.8之后移除了永久代,将这些数据存到了本地内存的元空间,防止内存溢出。是各个线程共享的内存区域 jvm启动时创建,关闭时释放
堆 :一块线程共享的区域,主要用来存储对象实例和数组等,包括年轻代 伊甸园区和两个幸存者区和老年代,我们jvm的垃圾回收就是对堆的垃圾回收
双亲委派机制
类加载器有,启动类加载器,扩展类加载器,应用类加载器,自定义类加载器,类加载器的作用是将字节码文件加载到jvm中,就是当一个类收到了类加载请求的时候,他首先不会尝试自己去加载这个类,而是把这个请求委托给父类去完成,每一个层次类加载器都是如此,。直到最顶层的启动类加载器,就是说他自底向上查找是否加载过这个类,如果加载过就直接返回,一直到顶然后自顶向下进行加载;作用时确保核心的安全性和完整性,避免一个类被重复加载。自上向下谁能加载就直接加载,上面的优先级高,由上面的类加载器先加载。
实现自定义类加载器,重写defineClass方法,将双亲委派机制的代码去除,来打破双亲委派机制
初始化类的时候优先
父类静态代码块
子类静态代码块
父类构造代码块
父类构造函数
子类构造代码块
子类构造函数
静态变量和静态代码块,则按照自上而下的顺序依次执行
垃圾回收算法
在讲垃圾回收之前,需要判断哪些对象时垃圾,有两种方式来确定,一个是引用计数法,一个是可达性分析算法,一个对象被引用了一次,他的引用次数就加一, 当引用次数为0时代表这个对象可回收,但是当对象出现循环引入时,也就是在类里面的属性值互相引用时,引用计数法会失效,所以还有一个算法时可达性分析算法来判断哪些内容时垃圾,过程是扫描堆中的对象,看是否能沿着gcroot对象为起点的一个引用链找到该对象,找不到,表示可以回收,虚拟机栈 (栈帧中的本地变量表)中的引用对象,方法区中类静态属性引用的对象,方法区中常量引用的对象 本地方法栈中native方法引用的对象
标记清除算法:分为两个阶段标记和清除,首先根据可达性分析算法垃圾,然后清除 速度快,内存碎片化严重。
复制算法:讲内存分为两半,使用其中一半,然后讲不是垃圾的对象复制到另一半中,之前使用的一般全部回收掉,效率高,无碎片,内存使用率低
标记整理算法:在标记清除算法的基础上加了一步整理,解决了标记清除算法内存碎片化严重的问题
JVM的分代回收算法:将内存分为新生代和老年代 内存比为1:2
新生代分为伊甸园区两个幸存者区,伊甸园区存储新创建的对象,两个幸存者区分为相同的from 和 to两块 8:1:1
伊甸园内存不足时,垃圾回收时将伊甸园区和幸存者区的from 使用复制算法存入to中,释放伊甸园区和from区的内存,经过一段时间 伊甸园区满了,标记伊甸园区和to区的对象,放入到from区,to和伊甸园区清空。幸存过15次的对象或者幸存区内存不足,或大对象,会晋升到老年代
minorgc也叫younggc 发生在新生代stw 暂停时间短 ,mixedgc新生代加老年代部分区域,g1收集器 fullgc新生代加老年代完整垃圾回收,暂停时间长stw应尽量避免。
CMS G1工作原理
串行垃圾收集器:单线程垃圾回收,堆内存小,适合个人电脑 Serial 作用于新生代,采用复制算法,Serial作用于老年代,采用标记整理算法,垃圾回收时只有一个线程工作,其他线程都要停止stw
并行垃圾收集器:Parallel New作用于新生代,Parallel Old作用于老年代,与串行垃圾收集器不同的是,垃圾回收时多个线程同时区回收垃圾,并且所以线程都要stw。等待垃圾回收的完成。
CMS垃圾收集器:并发的,标记清除算法的垃圾收集器,针对老年代的垃圾回收,时间停顿短,用户体验好,最大特点时垃圾回收时,仍然可以正常运行,初始标记 其他线程阻塞,,并发标记 不阻塞,重新标记的时候其他线程不运行,最后并发清理, jdk8默认
G1垃圾收集器;作用于新生代和老年代时jdk9默认的垃圾收集器,划分多个区域,每个区域都可以充当,伊甸园区,幸存者区,老年代,和大对象区,采用的是复制算法,响应时间和吞吐量兼顾,分为三个阶段,新生代回收 并发标记 混合收集,
如果回收速度赶不上创建新对象的速度,触发fullgc。
年轻代垃圾回收,复制算法 暂停用户线程。。。 当老年代占用内存超过阈值45%之后,触发并发标记,不暂停用户线程,然后时重新标记阶段,需要暂停用户线程,然后进入混合收集阶段,根据暂停时间,优先回收存活对象少的区域,不会全部回收老年代区域。 进入下一轮,
强引用:只有所有GCRoot 不通过强引用引用一个对象,这个对象才能被回收
软引用:在垃圾回收后,也就是内存溢出前,内存仍然不足,会回收掉该软引用的对象
弱引用:无论内存是否充足,都会回收掉该弱引用的对象。
虚引用:最弱的一种引用关系,不能单独使用,需要配合引用队列使用,主要作用时跟踪对象被垃圾回收的状态
并发编程
基础知识
当一个程序运行起来,把这个程序的代码从磁盘加载到内存,就开启了一个进程,进程包含了线程,每个线程执行不同的任务,不同的进程使用不同的内存空间,同一个进程下所有的线程可以共享内存,线程更轻量级,上下文切换成本低
并发是一段时间内处理多个事件,就比如说多个线程轮询使用一个cpu,并行是同一时间,多个cpu同时处理多个线程
创建线程的方式有继承Thread类,实现runnable接口,实现callable接口,线程池创建线程,runnable接口run方法没有返回值,callable的call方法有返回值,使用Future和future Task可以异步获取执行的结果,而且call方法可以抛出异常,run方法只能在内部处理,不能向上抛出。start方法才是真正的开启了一个线程,如果调用run就是一个普通的方法
线程状态:新建创建出来,可运行调用start,阻塞(获取不到锁)等待(wait)时间等待(sleep)终止运行完
如何保证三个线程按顺序执行 使用join方法 该线程进入阻塞,等待其他线程执行, notify,随机唤醒一个wait线程,notifyall唤醒所有wait线程,
wait和sleep都是暂时放弃cpu使用权,进入阻塞状态,sleep是Thread的静态方法,wait是成员方法,都是等待相应毫秒后醒来,wait可以被唤醒,如果没设置时间并且没被唤醒就一直阻塞下去,都可以被 打断和唤醒,wait方法必须先获取到wait对象的锁,执行wait后会释放对象锁,而sleep执行后不会释放对象锁。
停止一个正在执行的线程;正常退出,stop强行终止,interrupt方法中断线程打断阻塞的线程会抛出异常
JMM定义了共享内存中多线程程序读写操作的行为规范,保证指令的正确性,把内存分为两块,工作区域和共享区域,线程之间相互隔离,交互时需要通过主内存
各种锁
乐观锁:是一种思想,即认为读多写少,认为遇到并发的可能性低,每次操作数据都不会上锁,但是在更新时候会判断在此期间有没有其他人去操作这个数据,比较上一次的版本号,一样则更新成功,java中的乐观锁操作基本都是通过cas实现的,比较当前值和传入值是否一样,一样才更新。
悲观锁:就是认为并发的操作比较多,每次操作都先去获取锁,获取完成 进行操作 之后才释放锁。
上面两种锁是两种思想
公平锁:加锁前检验是否有排队等待的线程,优先排队等待的线程,先到先得
非公平锁:不考虑排队问题, 直接尝试获取锁,获取不到自动到队尾等待,
非公平锁效率高,因为公平锁还维护了一个线程共享的队列,synchronized时非公平锁,ReentrantLock 默认的lock()方法采用的是非公平锁
上面两种锁时两种锁的类型
synchronized锁:
synchronized锁底层是Monitor实现的,他是jvm提供的由c++实现,Monitor的结构包括,owner,entryList,waitset,当一个线程加锁之后,这个锁对象会关联上一个monitor,而且将owner写入存储当前获取锁的线程,只有一个线程可以获取,没抢到锁的线程,阻塞的线程进入entrylist,当锁释放后就竞争锁,线程调用wait方法之后,会将其关联到waitset中。
synchronized底层是一个重量级锁,涉及到用户态和内核态的切换,成本较高,使用该锁时,有一个操作时 锁对象会关联monitor,对象的对象头里有一个MarkWord,里的信息指向了重量级锁,但是很多情况下,同步代码块可能不存在竞争,只是交替执行,这种情况下没必要用重量级锁,因此一般使用轻量级锁,当线程获取锁时,会在线程的栈帧中存储一个锁记录,指向锁对象,还会发生一次cas操作,交换锁记录和锁对象的数据,如果有可重入的锁,再次获取锁,会再次发生cas操作,这个过程中一旦产生竞争,会升级为重量级锁,每次都发生cas操作,还可以优化为偏向锁, 第一次获得锁时有一个cas操作,之后再获取锁时,只需要判断markword是否是自己的线程id即可,而不是开销较大的cas命令
轻量级锁:线程之间没有竞争操作,用重量级锁性能不好,可以使用轻量级锁优化,修改了对象头的锁标志,每次操作都是cas,保证原子性
重量级锁:使用Monitor实现,涉及到了用户态和内核态的切换,进程上下文切换,性能较差
偏向锁:很长时间内锁都是由一个线程使用,第一次获得锁时有一个cas操作,之后再获取锁时,只需要判断markword是否是自己的线程id即可,而不是开销较大的cas命令
可重入锁:相对于synchronized 他可中断,可设置超时时间,默认是非公平锁,可设置为公平锁,支持多个条件变量,支持重入与sync一样,底层主要使用cas+aqs来实现,公平锁的效率往往没有非公平锁效率高,非公平锁继承自aqs,有字段有状态,头节点,尾部节点和当前正在占用的线程,使用cas修改state表示后,表示获取锁成功,exclusiveOwnerThread表示获取到锁的线程,获取不到锁的线程进入队列,这是一个双向链表,公平锁体现在双向链表的按顺序获取锁,非公平体现在不排队的线程也可以去抢锁,
共享锁(信号量):信号量是基于aqs的共享锁,一般用于限流,支持非公平和公平锁,但不支持可重入锁
读写锁:比较适合读多写少的场景,读锁属于共享锁,写锁属于独占锁,读写互斥,写写互斥,读读不互斥。
自旋锁:如果持有锁的线程能在很短时间内 释放锁资源,那么等待竞争锁的线程就不需要,做内核态和用户态的切换,进入阻塞状态,只需要等一等,避免切换的消耗,就是一个while循环,不断去尝试,当然有一个最大的等待时间,如果再这个时间内获取不到,就会进入阻塞状态。一般用于竞争不激烈,很短时间内释放资源地线程任务。
CAS
基于乐观锁的思想,最乐观的估计,就算想要的数据被修改了就不断尝试,在无锁状态下保证操作数据的原子性,
线程池
线程池的核心参数:核心线程数,最大线程数,生存时间(救急线程数的生存时间,没有任务,线程会释放),时间单位,没有空闲线程时,新来任务的队列,线程工程,拒绝策略(没有空闲线程,队列已经满了会触发拒绝策略),一个时抛异常,一个是丢弃该任务,使用调用的原来线程执行,丢弃队列中最老的,尝试执行新任务。当一个任务来的时候,首先查看核心线程是否已满,没满就执行,满了就查看阻塞队列是否已满,然后查看线程数是否小于最大线程数,创建非核心线程执行任务,非核心线程执行完之后,查询阻塞队列中的任务,执行。
常见的阻塞队列:基于数组的先入先出队列,基于链表的先入先出队列,优先级队列,保证每次出队的任务都是执行时间最靠前的,不存储元素的队列,每次插入操作都必须等待一个移除操作,
前两个阻塞队列链表默认无界支持有界,两把锁,数组的强制有界,一把锁,队列为空时,读取队列会阻塞
高并发任务 cpu核数+1 减少线程上下文切换
并发不高,任务时间长,
io密集型:文件读写,db操作,网络请求,核心线程数一般设置为2N+1 不需要占用太多cpu N为cpu核心数
cpu密集型:计算型代码,bitmap转换,核心线程数设置为N+1 尽量减少线程之间的切换
并发高、业务执行时间长这个关键在于整体的结构设计,缓存增加服务器,线程池的设计。
线程池的种类:固定线程数的线程池,无救急线程,底层时双向链表的阻塞队列,适用于任务量已知,相对耗时,
单线程的线程池,适用于按顺序执行任务, 可缓存的线程池,阻塞队列不存储元素,适用于任务密集,每个任务执行时间较短,
可以执行延迟任务的线程池,支持定时和周期执行任务,‘
阿里开发手册 线程池不允许使用Executors创建,避免请求队列过长,导致oom的问题,
ThreadLocal
ThreadLocal为每个线程都分配了一个独立的副本,他会对每个线程都维护一个ThreadLocalMap,里面存了我们放入的数据,key是threadlocal,资源对象是value,key是弱引用,value是强引用,会产生一个内存泄漏问题,key会被gc回收,value不会被内存回收,建议,remove释放key,value
常用工具类及其实现原理
volatile关键字修饰的变量,一个是保证线程间的可见性,让一个线程对共享变量的修改对另一个线程可见,一个是禁止指令重排,在读写变量时加入屏障,阻止指令重排,jit会优化,组织他优化
AQS是构建锁的基础框架,他里面有一个volatile修饰的状态信息0表示无锁,1表示有锁,两个线程竞争是使用cas保证原子性,或者不到锁的线程会存入进一个先入先出的队列,使用双向链表链接,AQS既可以实现公平锁,也能实现非公平锁
synchronized和lock区别,synchronized是关键字,源码在jvm中c++实现,lock接口是java实现,lock需要手动释放锁,lock功能更全面,公平锁,可打断,超时时间,读写锁等等,竞争少或者没竞争synchronize性能好,竞争激烈时lock性能好
死锁产生的条件,一个线程需要获取多把锁时,出现死锁使用jps(进程信息),jstack(线程的堆栈信息)进行诊断,jconsole内存,线程类监控工具,
多个线程操作共享的变量就会出现并发问题
CountDownLatch用来进行线程协作,其中构造参数用来初始化等待的计数值,await用来等待计数归零,countDown()用来让计数器减一。
比如我们平时使用的controller查询一个详细信息时, 需要查询多个数据库表进行汇总,这个时候需要使用多个线程去同时去完成任务,使用线程池+CountDownLatch去完成操作,提高响应速度,或者使用future也能完成类似的效果,线程池异步调用也可以完成类似的效果。
Semaphore:底层是aqs,常用于限流,在并发情况下,控制方法访问量,acquire获取信号量+1,release释放,信号量减一。
MySQL
基础知识
如何定位慢查询:第一可以使用一些开源工具Arthas,lPrometheus 、Skywalking,压测,第二就是MySQL自带一个慢查询的日志,在配置文件中配置,慢日志的开关,sql执行时间超过多少秒视为慢查询,记录日志,重启服务。
第三,使用explain或者desc获取SQL语句执行计划,找到慢的原因,里面有很多信息,包括当前sql可能使用到的索引,当前sql实际命中的索引,索引占用的大小,还有优化建议,是否回表查询,type这条sql连接的类型,(性能由好到差为NULL、system查询系统中的表(mysql内置)、const根据主键查询(单表)、eq_ref主键索引查询唯一索引查询(多表)、ref索引查询、range索引范围、 index索引树扫描、all全盘扫描) 如果存在索引树扫描或者全盘扫描那么效率就不高。
事务
事务是一组操作的集合,是一个整体,要么同时成功要么同时失败,事务由acid四个特性,原子性(最小的不可分隔的单元,要么都成功要么都失败),隔离性(不受其他事务和并发线程的干扰),一致性(事务完成时,所有的数据保持一致状态,我+500,你-500)。持久性(一单提交或者回滚,对数据库的操作改变时永久的)。
并发事务问题有三个,脏读:读到另一个事务还没有提交的数据。不可重复读:读取同一条数据,但是两次读取的结果不一样。幻读,读取一个数据时,发现没有,但是插入数据时这条数据已经存在,再次查询,这个数据还是不存在的。
四个隔离级别来解决这几个问题,读未提交(能看到其他事务没提交的数据)没有解决问题,读已提交(能看到其他事务已经提交的数据)解决了脏读的问题,可重复读(事务执行过程中,一直是事务启动时看到的数据)也就是mysql默认的隔离级别,解决了不可重复读的问题,但是还存在幻读的问题,串行化(对记录加锁,完全解决了并发问题)解决了所有的问题,因为时串行的所以性能太低不使用。
日志
redo log:当我们执行一条增删改的sql语句时,首先会把先操作内存的缓冲池,如果缓冲池中没有对应的数据,就会读取磁盘中的数据页,也就是innodb的最小存储单元,默认大小16kb存储的是行数据,将这个读取到内存中的缓冲池中进行操作,以一定的频率刷新磁盘,减少磁盘io,加快处理速度(WAL预写日志技术),在操作缓冲池中的同时还会将该操作的日志写入缓冲池的redologbuffer中,事务提交时这个会buffer的信息会被同步到磁盘上的redolog里,当发生错误时,进行数据恢复使用,保证事务的持久性,redolog记录的是数据更新后的状态,undolog记录的是日志更i性能前的状态。事务提交前崩溃会通过undolog回滚,事务提交后崩溃会通过redolog恢复数据。这个能力被称为崩溃恢复能力。redolog是追加操作,顺序写,数据写入是随机写,顺序写效率更高,磁盘开销小。mysql更新将内存的数据更新到磁盘时会记录一个lsn日志序列号,通过这个恢复数据时,来确定从那个位置开始redolog
undolog:回滚日志,当操作一条数据时,undolog会记录一条相反的数据,比如删除,这个日志会记录一个插入的数据,为回滚提供服务,保证事务的一致性和原子性。在mvcc多版本并发控制中也有重要作用。和回滚操作
binlog二进制日志:server层的日志,主要时数据备份和主从复制
relay log:主从复制时,从节点拷贝主机点binlog生成的本地日志。
binlog是执行更新操作后,生成binlog,事务提交时,将所有binlog写入binlog文件,别的存储引擎也能用,是追加写,写满一文件后创建一个新文件写,用于备份数据,主从复制,不记录查询操作。有三种格式类型:STATEMENT,记录逻辑操作,根据sql重现,但是如果使用了uuid这种会出现数据不一致问题;row:不会出现上面的问题, 所有的数据变化结果都记录,缺点是binlog文件会很大。mined:根据情况自动选择上面的两种方式。
两阶段提交:事务提交后,redolog和binlog都要存入磁盘,为了保证这两个日志的一致性,mysql内部开启了XA事务。当客户端提交或者开启自动提交时,会开启一个xa事务,将日志写入redolog分为两个步骤,prepare,commit。中间穿插写入binlog。prepare,将内部xa事务id写入redolog,事务状态设置为prepare,然后rodolog持久化到磁盘。commit阶段:将id写入binlog 并写入磁盘,接着将redolog状态设置为commit,该状态不需要持久到磁盘,只需要写入缓存。只要binlog成功就视为成功。如果mysql在这中间宕机,redolog都处于prepare状态,mysql会扫描redolog的状态,如果有prepare的,就会拿着这个事务id,查询binlog是否有这个id,没有就说明binlog还没记录信息,回滚事务。有就说明binlog有信息,提交事务。以此来保证事务的一致性。需要两次刷盘操作,性能较差。
存储引擎
存储引擎就是存储数据,建立索引,操作数据的实现方式,存储引擎是基于表的,MySQL5.5之后默认使用的存储引擎是InnoDB,支持事务安全,支持锁,表锁和行锁,支持外键,通过redolog日志实现崩溃恢复,其他的就支持一个表锁,MyISAM 不支持。。。用的不多,memory数据存在内存,服务器重启崩溃数据会丢失,不支持。。。用的也不多
索引
索引是一种帮助mysql高效获取数据的数据结构,在数据之外,数据库维护了满足查找算法的b+树,指向数据,提高检索效率,降低了io成本,降低了排序成本,降低了cpu消耗,这种数据结构就是索引。首先就是二叉搜索树,但是在特点情况下,最坏的二叉搜索树是一个链表,效率不稳定,avl在加入数据时为了保证平衡可能又需要多次自旋 ,插入删除代价大, 红黑树因为是二叉树,当数据量大时,会导致树太高,效率低的问题,b树是多叉树,B+树在b树的基础上又做了优化, b+树只有叶子节点存储数据,磁盘读写的压力更小,因为都在叶子节点,也就是在同一层,所以,查询时间更稳定,并且便于扫库和区间查询,如果b树要实现范围查询要多次io操作,因为叶子节点是一个双向链表。
主键索引(聚集索引)就是b+树的叶子节点存放的实际所有数据,一个表只能由一个,效率高。非聚集索引存放的是主键值,可能会需要回表操作
默认会使用主键作为聚集索引,没有就用数据库的唯一字段,如果都没有会生成一个隐藏字段,自增id作为聚集索引
主键索引(一个表最多一个),唯一索引(可以多个),普通索引,前缀索引(字段的一部分,可以减少索引字段的大小,有效提高索引的查询速度)
覆盖索引就是使用了聚集索引或者非聚集索引后,所需要的数据不需要回表查询,在索引的结果里面就能直接找到的索引。比如使用id查询,走聚集索引,所有的数据都能找到,或者使用名称查询,需要的数据是id和name这样也能找,尽量避免select*或者尽量避免触发回表查询。
mysql超大分页使用覆盖所有加子查询解决问题,先通过id进行排序,操作一个id比操作一行数据快。然后覆盖索引加子查询,速度提高
索引的创建原则:数据量大并且需要频繁查询的表,常作为查询条件,排序分组的表,字段内容区分度高,可以创建唯一索引,内容较长,可以创建前缀索引,尽量使用联合索引,控制索引数量,索引列如果不能为空,使用非空约束。主键最好是自增的,每次操作数据时,b+树都是追加操作。
索引失效:如果索引了多个列,使用时要遵循最左前缀法则,按顺序从最左列开始,并且不跳过 索引中间的列,如果有三个索引,按顺序使用,中间的一个索引没有使用,那么按照规则只有第一个索引有作用。范围查询右边的列不能使用索引,最右边会失效。使用函数,在索引列上增加运算操作,索引会失效。字符串不加单引号会索引失效(整型转字符串会失效,字符串转整型不会失效)。因为mysql查询优化器会自动进行类型转换,造成索引失效。%开头的模糊查询会失效,结尾不会失效。where 中使用or左边是索引,右边不是索引会导致索引失效 创建联合索引时,尽量把区分度大的字段放在前面,因为时先比较前面的字段。索引 a,b,c, a的顺序不重要,优化器会优化a的顺序。当使用%x模糊查询时,数据库中只有主键加二级索引,二级索引使用了%x也会走索引。
SQL优化:表的字段要选择合适的数据类型,设置合适的字符串类型,比如char定长效率高,varchar可变长度,效率低。避免使用select*,避免索引失效,尽量使用union all代替union 少一次过滤,效率高,避免在where子句对字段表达式操作。尽量使用innerjoin不用left和right,因为优先把小表放到外面,这样优化效率高,返回的结果集中数据少一点,以小表为驱动表,就行mysql建立连接数量少一点一样,MySQL底层也需要建立连接越小,内存判断计算的次数就越小。如果读多写少,还可以采用读写分离,主从复制的操作,写操作是主表,读操作时从表,当然需要一致性需求不高的业务。可能会出现不一致。分析查询语句。创建优化索引,
MVCC
是mysql的多版本并发控制,用于并发控制时访问到哪个版本的数据。维护一个数据的多个版本,使得读写操作没有冲突,主要有三个内容,一个是隐式字段,undolog日志,readview。首先就是隐藏字段,,每行数据都隐藏了三个字段,最后一次修改该事务的id,是自增的,用来标识事务,回滚指针,指向这条记录的上一个版本,配合undolog。隐藏主键,如果表中没有主键,就会有这么一个隐藏的主键。第二个内容就是undolog日志,也就是回滚日志,在增删改数据的时候,undolog中回记录反向的操作,比如。。。。不仅在回滚时需要,在mvcc版本并发控制时也需要,undolog有一个undolog版本链,记录的是不同或者相同的事务对数据的修改的信息,链表头部是最新的旧的记录,链表尾部是老的旧记录。然后一个隐藏的字段是回滚指针指向这个版本链的头部,然后第三个内容就是readview,就是确定当前这个事务读取到哪个版本的数据,readview包含四个字段,当前活跃的事务id,最小活跃事务id,预分配的当前最大的事务id+1.readview创建者的事务id。根据不同的隔离级别和readview版本链数据访问规则,找到特点版本的数据。在rc也就是 读已提交,读取到的数据可能是不同的,每次select前都会生成一个readview,在mysql默认的隔离级别可重复读,多次读取到的数据是相同的,第一条select时生成一个readview,事务期间都使用这个readview。根据上面三个内容完成了mvcc提供的功能服务。
分库分表
主从复制核心就是mysql的binlog也就是二进制日志,包括所以的数据库定义语句和数据库操作语句,不包括一些查询的语句select show。主库提交事务时,将数据变更记录进二进制文件,从库读取主库的二进制文件,写入到从库的一个中继日志,从节点根据中继日志,更新自己的数据
主从可以读写分离,效率高。大事务或者资源密集操作走主库,避免主从延迟。
当业务数据逐渐增多,业务发展迅速,优化已经解决不了问题,io和cpu瓶颈时,就可以考虑分库分表。分库分表有几个不同的策略,垂直分库:按照业务,将不同表拆分到不同的库中可以提高数据库连接数,垂直分表,以字段为依据,将不同的字段拆分到不同的表中,,冷热数据分离,将一些不常用的数据放到附表中。然后就是水平分库将一个库的数据拆分到多个库中,根据id取模,解决了单库大数量的问题,高并发性能瓶颈问题,水平分表,和分库类似,将一个表的数据分到多个表中,也是根据id取模一般常用的分库分表组件有sharding-sphere,lmycat,
/我之前也自己手写过一个springbootstart,分库分表组件,比较简单就是根据 aop切面,获取到一个业务id,然后根据这个id,取hash再取模。再将这个id加个下划线拼到sql语句里面。达到分库的效果,简单尝试过/
增加内容
关系型数据库和非关系型数据库:关系型数据库有aicd四个特性,然后nosql采用更宽松的模型,基本可用,软状态,最终一致性,扩展性更好,要根据实际业务来选择使用哪种数据库。aicd是否是必须的。
数据库三范式:每一列都不可分隔,每一列都和主键相关,不能只与一部分相关,每一列和主键直接相关,不能间接相关。
mysql连表查询:内连接,左外连接,右外连接,全外连接mysql不支持必须使用union配合完成
如何避免插入重复数据:使用unique约束,还有就插入时冲突可以选择更新数据,忽略错误,如果已存在,这条语句忽略
char和varchar:char定长,末尾补足空格,短字符效率高,varchar可变长度字符串,指定最大长度,存可变长度数据。text类型的也有长度限制
外键约束:维护表和表之间的关系,保证数据的完整性和一致性,但是一般不用
mysql的in和exist:in用来检验左边的数据是否出现在右边的集合中,exist用来检验右边是否能返回一行数据,exist性能好,in更直观,in可用处理空值
count(*) = count(1) >count(主键字段) > count(字段) 效率 遍历时优先使用二级索引,因为成本开销小,count(1)不会读取记录中字段的值,比count(主键字段)少一个步骤,count(字段)用的是全表扫描,还可以通过explain对数据进行估值。
执行一条sql的过程:首先就是先建立连接,连接的过程需要TCP三次握手,然后验证用户名密码,然后讲用户的权限保存起来,空闲的连接到一定时间会自动断开,(使用长连接减少建立连接和断开连接的过程,定期断开长连接,客户端主动重置连接)第二步就是去查询缓存,如果是select就先查缓存,缓存是key-value形式的,命中就直接返回,没命中就查询,但是对于更新频繁的表,缓存命中率低,这个表更新,这个表缓存会清空,8.0开始就不走缓存了。第三步解析sql。词法分析和语法分析构建语法树,第四步就是执行sql:prepare阶段,预处理阶段将 select*转换为所有列,检测字段是否存在,然后优化阶段,选择查询成本最小的计划,比如有索引就用索引查询,尽量避免回表查询。覆盖索引优化,然后是执行器,真正执行sql语句,有主键索引就主键索引查询,没有索引就全表扫描,还有一个优化处理就是索引下推,就是联合索引中使用的时候,第一个条件成立了,然后就去回表查询第二个条件看是否成立,当使用索引下推时,第一个条件成立,不去回表,直接查询第二个条件是否成立,成立直接返回。
数据存储:每个数据库都有一个以数据库名字的目录,表结构和表数据都在这个文件里,有db.opt,存储数据库的默认字符集和字符校验规则,t_order.frm,存表的结构信息,t_order.ibd表的数据
mysql为什么不用眺表,因为b+树3层的使用数据可能已经千万,这么大数据量使用眺表的话b+树io次数更少。眺表就是一个链表,然后随机出几个数据再生成一个链表,也可能生成多级多个链表,根据生成的链表先去遍历,减少遍历的次数
如果一个列既是单独索引又是联合索引,单独查询时,优化器会分析查询成本,选择成本低的方式。
索引已经创建好了再插入一条数据,为了保持b+树的平衡,如果一个叶子节点满了会触发叶子节点分裂操作,。如果时修改数据,不改索引数据,b+树的结构不会变,改索引数据b+树的结构会变。
索引不是越多越好,写入频繁的场景中,b+树维护的消耗也会变大,占用物理空间,需要维护,每次增删改索引都需要维护b+树
mysql解决并发问题:提供了多种锁,行级锁,表级锁,页级锁。读写操作时加锁。事务隔离级别。mvcc
可重复读没有完全解决幻读的问题,但是可以尽量事务开启后,执行select ... for update语句会加一个next-key-lock锁,避免了幻读
串行化是通过加行级锁实现的,会对记录加S型的行级锁,next-key锁。避免了所有的事务问题
update语句是原子性的,会加一个行级锁,保证事务在更新记录的时候原子性,会生成undolog,执行失败会回滚。
事务里sql太多会导致,锁定数据太多,导致大量死锁和锁超时;回滚日志占用空间太大,回滚时间长;造成主从延迟,主库等事务完成才会写binlog日志,传给从库。
mysql的锁分为全局锁,表级锁和行锁
全局锁锁住之后就只读了,增删改都会阻塞,主要用于全库备份。可能会造成性能问题,可以用可重复读隔离级别优化,mvcc的readview。
表级锁:lock tables语句加上表锁,会限制其他线程的读写操作;元数据锁,对数据库表数据操作时(读锁)和数据库表结构操作时(写锁)会加上这个mdl锁,防止在对表的数据操作时,其他线程对这个表结构进行操作。申请mdl得锁会形成一个队列,队列中写锁的优先级高于读锁,一旦出现写锁,后面的读操作会阻塞,意向锁,当插入更新删除数据时,加上意向独占锁,就是对行记录加锁之前先对表加一个意向独占锁或者意向共享锁,快速判断表里是否有记录正在使用。如果没有意向锁就要对表进行遍历查询是否有记录被加锁了。AUTO-INC锁:当主键自增时,插入数据不指定主键值,通过这个锁实现的,执行完插入语句后会释放,大量数据插入时性能差,在mysql5开始,会加一个轻量级锁,获取自增值之后直接释放这个锁,不会等插入语句执行完才释放。在mysql中这两种方式可以选择一种,mysql还提供了另外一种方式,普通插入语句,获取值之后就释放,批量插入语句,等语句结束才释放。当使用第一种方式时,可能会导致一个批量的插入语句生成的id不连续,在主从复制过程中,binlog只记录原始语句,记录顺序要么先a要么先b,这样会导致主从不一致,设置日志的格式或者使用第三种方式。
行级锁:记录锁,有s锁和x锁之分,读写互斥,写写互斥。行锁有共享锁和独占锁,s是共享锁,x是独占锁。间隙锁,只存在于可重复读的隔离级别,目的解决可重复读下的幻读问题比如我锁了3,5这个区间,那么4这个记录就不能被插入了;next-key-lock,临建锁,记录锁和间隙锁的组合,锁定一个范围和记录本身。插入意向锁:一个事务在插入数据时会判断该位置是否已经被其他事务加了间隙锁,如果有会发生阻塞,直到持有锁的事务释放锁为止,在阻塞期间会生成一个插入意向锁,表明要在某个区间插入数据,然后将插入意向锁的状态改为等待状态,等占有锁的事务处理完之后,锁变为正常状态。
mysql是怎么加行级锁的:加锁的对象是索引,加锁的基本单位是next-key lock临键锁,临键锁是前开后闭合,间隙锁是前开后开,能使用记录锁或者间隙锁解决幻读问题的场景下,临键锁会退化成记录锁或者间隙锁,
唯一索引等值查询:在事务中,当记录存在时,这个临键锁会退化成X(互斥)记录锁,当查询记录不存在时,这个锁会退化成间隙锁,使用一条命令可以查询事务执行sql中的锁,在等值查询的时候只使用记录锁就可以解决幻读的情况下会退化。当记录不存在时,查询的id=2,最近的两边的id是(1,5),这个时候会加上一个间隙锁锁住1,5,如果有记录插入2,3,4就会发生阻塞,在这种情况下仅仅使用间隙锁就能解决问题,锁是加在索引上的,不能对不存在的记录加锁,所以只能锁住这个区间。
唯一索引范围查询:扫描时对扫描到的每一个记录都加临建锁,针对大于等于的范围查询,如果等值记录存在会退化为记录锁。针对小于或者小于等于,当这个记录不在表中的时候,前面的都加临键锁,如果时id<6 (1,5,10)最后会在5,10加上间隙锁,如果时小于等于并且这个记录值存在,那么就是两个临键锁(-∞, 1] (1, 5 ] ;小于的范围查询,如果存在,第一个加临键锁,第二个加间隙锁,
非唯一索引等值查询:如果存在两个索引,当加锁的时候,会对两个索引都加锁,但是当主键索引加锁时,只有满足条件才会加锁,。当查询的记录不存在时:二级索引会在两侧挨着的地方加间隙锁,二级索引在值相同的情况下,再安装主键id的顺序存放,如果插入的主键id在间隙锁之前,可以插入成功,如果在间隙锁之间会插入失败。然后就是记录存在的情况,首先对查询到的二级索引加临键锁,然后对主键加记录锁,二级索引后面一个数据加间隙锁,为了锁住后面的id 插入 会加上一个间隙锁。
非唯一索引范围查询:加的锁全都是临键锁,没有加索引的查询也都是全部是临键锁。
死锁:两个事务即使生成的间隙锁的范围是一样的,也不会发生冲突,因为间隙锁目的是为了防止其他事务插入数据,因此间隙锁与间隙锁之间是相互兼容的。
在执行插入语句时,如果插入的记录在其他事务持有间隙锁范围内,插入语句就会被阻塞,因为插入语句在碰到间隙锁时,会生成一个插入意向锁,然后插入意向锁和间隙锁之间是互斥的关系。
如果两个事务分别向对方持有的间隙锁范围内插入一条记录,而插入操作为了获取到插入意向锁,都在等待对方事务的间隙锁释放,于是就造成了循环等待,满足了死锁的四个条件:互斥、占有且等待、不可强占用、循环等待,因此发生了死锁。
表锁主要用于控制整个表的操作。粒度大,适用于大批量操作;行锁主要用于细粒度控制,减少锁冲突,适用于频繁单行操作。
两个update语句处理同一条语句会阻塞,会加行级锁,处理不同范围,不会阻塞,锁住的范围不同。如果范围查询不是索引,会触发全表扫描,都会加上行级锁,会阻塞。
update语句执行流程:首先查询这个id的数据页是否在缓冲池中,在就直接用, 没有就将这个磁盘上的数据页从磁盘读到缓冲池,查询更新前后数据是否一样,不一样就传参个innodb更新数据。开启事务,在更新数据前,记录undolog日志,记录redologbuffer,然后更新记录,提交事务时,redolog更新到磁盘,缓冲池的数据页会用wal技术,选择合适实际更新到磁盘上。当事务提交时,开启事务的两阶段提交,这时的binlog是在缓存中,只有事务统一提交才会刷新到磁盘。然我完成所有操作。
如果语句不走索引可以加forceindex强制走索引。
MySQL 的 NULL 值是怎么存放的?
MySQL 的 Compact 行格式中会用「NULL值列表」来标记值为 NULL 的列,NULL 值并不会存储在行格式中的真实数据部分。NULL值列表会占用 1 字节空间,当表中所有字段都定义成 NOT NULL,行格式中就不会有 NULL值列表,这样可节省 1 字节的空间。
MySQL 的 Compact 行格式中会用「变长字段长度列表」存储变长字段实际占用的数据大小。也就是varchar的长度
如果一个数据页存储不了一条数据,会将溢出数据存入溢出页,真实数据20字节指向溢出页地址。
缓冲池默认大小128MB,可调整,存的是页,为了更好管理缓存页,innodb为每个缓存页创建了一个控制块,控制块指向缓存页,两个之间会有碎片空间,将空闲缓存页的控制块用链表连接起来,叫空闲链表,空闲链表的每一个节点都对应一个控制块,每当磁盘中需要加载一个页时,从free取出一个空闲缓存页,并把该缓存页对应的控制块信息填上,然后把该缓存页对应的控制块从free链表中移除。
更新数据时,将对应的缓存页标记为脏页,后台线程将脏页信息刷入磁盘,同样页维护了一个flush链表,根据这个链表刷入磁盘。
提高缓存命中率使用的时lru算法但是mysql用的不是这个:两个问题;预读失败;。mysql加载数据时,会加载相邻的数据,但是没有被访问还要放在链表头部,降低了命中率。Buffer Pool 污染:mysql扫描了大量的数据,导致把大量热数据替换出去了,再次访问时产生大量的磁盘io,叫做缓冲池污染。
lrulist 管理脏页+干净页:将链表分为old区和young区。加入缓冲池的页先进入old区,页被访问时才加入young区,解决预读失败的问题
当页被访问,old区域超过一定时间才加入到young区,解决缓冲池污染的问题
脏页什么时候刷入磁盘;当 redo log 日志满了的情况下,会主动触发脏页刷新到磁盘;
Buffer Pool 空间不足时,需要将一部分数据页淘汰掉,如果淘汰的是脏页,需要先将脏页同步到磁盘;
MySQL 认为空闲时,后台线程会定期将适量的脏页刷入到磁盘;
MySQL 正常关闭之前,会把所有的脏页刷入到磁盘;
Redis
基础知识
常用的数据类型就是
String就是最简单的key value,常用的 场景就是缓存对象,常规的计数呀,分布式锁,和常用的登录用户状态信息的存储这些
Hash 就是一个key然后value里面存很多个keyvalue ,也是缓存对象,只不过是更加细分了,可以通过redis直接获取到具体的数据
List,数据结构是一个链表,可以在双端进行push和pop操作,常用的场景有可以做消息队列,和一些适应的业务。
set,是一个无序的集合,常用场景有并集,交集,差集的场景,比如点赞,共同关注等
Zset 存储的是键值对,他的value是score也就是分数,排序由分数决定,常用的场景就是一些排行榜
redis zset是一种有序集合,底层是压缩列表或者眺表实现的,当元素个数小于128并且每个元素值小于64字节的时候,redis使用压缩列表作为底层数据结构,如果不满足上面的条件会使用眺表作为底层数据结构。在redis 7.0全部使用listpack实现。眺表的时间复杂度是o(logN);
眺表在创建节点的时候,生成一个0-1的随机数,如果随机数小于0.25就增加一层。然后继续生成新的随机数,层数越高概率越低。层数最高时64
眺表对于b+树的优点:从内存上看,眺表比平衡树更灵活一些,在范围查询时,眺表比平衡树更简单一点,算法实现上,眺表比平衡树更简单一点
压缩列表底层时连续的内存块组成,有点像数组,表头有3个字段:整个压缩列表占用字节数,尾部距离起始节点偏移量,包含的节点数量,队尾有一个节点,记录结束点。查询第一个或者最后一个o1,查询中间的时on,每个节点包含三个元素,前一个节点的长度,当前节点的类型和长度,当前节点的数据。当插入数据时,会根据是字符串还是整数,分配内存,缺点是会发生连锁更新(如果前一个节点的长度小于254字节,用一字节保存长度,大于等于254字节,要用5字节保存这个长度,如果一个数据更新可能会导致连锁更新),一旦发生,内存要重新分配,适用于结点数量少的情况。
quicklist是双向链表+压缩列表,减少了连锁更新带来的影响,每一个节点都带着一个压缩列表,加入元素是会检测对应位置的压缩列表能不能容纳该元素,能就加入,不能就新建一个节点,尽量避免了连锁更新,当时没有完全解决这个问题,
listpack紧凑列表:采用了压缩列表很多优秀的设计,也是连续空间保存数据,不同编码方式表示不同大小的数据。头包含两个属性,总字节数和元素数量,末尾也有结尾的标识,每个节点有编码类型,实际数据,总长度前面两个元素的长度。完全避免了连锁更新,
在最新版本的redis中,又加入了BitMap 适用于二进制状态统计的场景,一般用于签到和连续签到的场景,HyperLogLog海量数据统计场景,用于百万数据的网页的uv统计,GEO存储地理位置信息的场景,一般用于附件的商铺,或者和地图打车,等有关场景。Stream消息队列,相对于list实现的消息队列,自动生成全局唯一消息id支持以消费组的形式消费数据。
先操作数据库在删除缓存的方式和先删除缓存再操作数据库都可能会出现数据不一致的问题,所以保证双写一致有几个解决方案,第一个就是延迟双删:先删除缓存,然后修改数据库,然后延时一定的时间再删除缓存,这样的话可以尽量保证缓存中和数据库的一致性,会有脏数据的风险,再修改数据库之前又同步了缓存,这样很长一段时间都是脏数据。
如果要保证强一致性就读写锁,因为缓存肯定时读多写少吗,所以读数据的时候使用读锁,写数据的时候使用写锁,这样强一致性,但是性能不高。
还有一种解决方案就是保证数据的最终一致性,当修改数据库之后,发送一条mq消息,去更新缓存,还有就是利用canal中间件,伪装成一个mysql的中间节点,监听binlog日志,然后去 更新缓存。可以保证数据的最终一致性。
Redis单线程很快的原因,首先就是redis 是纯内存操作,执行速度非常快,采用单线程,避免了不必要的线程上下文切换,没有线程安全问题,使用的是io多路复用模型,非阻塞io。他的瓶颈不是执行速度而是网络延迟,io多路复用主要就是实现了高效的网络请求。
linux一个进程使用内存的情况分为用户空间和内核空间,用户空间只能执行受限的命令,而不能直接调用系统资源,必须通过内核提供的接口来访问,内核空间可以执行特权命令,可以调用一切的系统资源,linux为了提高io效率,会在用户空间和内核空间都使用一个缓冲区,写数据时,把用户缓冲区拷贝到内核缓冲区再写入设备,读数据时和写数据想反,先读取设备数据到内核缓冲区再拷贝到用户缓冲区。
阻塞io就是等待数据也就是用户读取数据时,如果没有数据要阻塞等待和数据拷贝到用户空间都是阻塞的,数据拷贝完成后才不阻塞。
非阻塞io 只是第一阶段不阻塞了,当没有数据时,直接返回错误,但是还是会不断尝试去获取数据,虽然是非阻塞的,但是性能并没有得到提高,而且可能会导致cpu空转,cpu使用率增加,
io多路复用,利用单个线程监听多个socket,并在socket可以操作是发送通知,避免无效等待,是阻塞的,充分利用了cpu资源。优点是可以监听多个socket。当监听到可以操作的socket时,通知的方式又有多种,select和poll通知方式只通知有socket就绪,但是不能定位到具体的socket需要循环去遍历,epoll在通知的同时还会把已经就绪的socket写入用户空间,可以直接找到并操作
Redis网络模型就是使用io多路复用来实现网络io的高性能,使用io多路复用加事件派发,将不同类型的任务给不同的处理器,有连接应答处理器(用来处理连接请求),命令回复处理器,和命令请求处理器(接收数据转为redis命令然后把执行结果写入缓冲区,交给命令回复处理器回复),redis6.0之后还对 命令回复处理器和 接收数据转换为redis命令做了多线程优化,速度更快。
持久化策略
RDB持久化策略:就是把redis中的数据直接存到磁盘上,redis故障重启,直接从磁盘读取数据,恢复数据。触发机制可以再配置文件中修改。RDB的执行原理就是fork出一个子进程,首先主进程访问内存的时候使用的是一个虚拟地址和物理地址的映射,叫页表,他会把这个拷贝给子进程,然后子进程根据这个页表将数据存储到磁盘上,替换旧的RDB文件,但是当子进程工作时,数据是只读的状态,主进程可能会往内存里写数据,这时使用的是copy on write技术,主进程写时,会拷贝一份操作的数据,并且读写都是再拷贝的数据中。不会影响子进程的操作。
AOF持久化策略:redis的每一个命令都会记录再AOF中,就是一个命令日志文件,可以通过配置文件开启并选择配置,直接写入AOF文件, 命令先被存入缓冲区,然后再写入磁盘;ALways同步刷盘性能差,每秒刷盘(默认最多丢失1s数据)和操作系统控制可靠性差,可能丢失大量数据。因为是记录命令,所以AOF文件比RDB大的多。当AOF触发阈值后会重写AOF文件,减少文件的体积。AOF数据完整性高,但是启动速度比RDB慢
缓存三个问题
缓存穿透:就是当查询一个数据的时候,redis和数据库里都没有,这样会导致每次查询都会查数据库,造成数据库压力过大。解决方案:缓存空数据可以解决这个问题,查询数据库为空,就把空存入redis。这样操作简单,但是消耗内存,可能会导致不一致的问题。另外一个解决方案就是布隆过滤器,布隆过滤器就是一个以bit位位单位的数组,数组中只存储0或者1,用于检索一个元素是否存在。一个集合中,他的原理是举个例子,有一个id为1的数据,获取其多次hash的值,然后模数组的长度,将对应的值改为1,查询的时候,如果这几个位置的数据都为1就表示这个数据存在,但是有一定的误判可能。这个误判产生于比如A,B已经存在于布隆过滤器,然后对C进行判断计算的时候,刚好,c的三个位置已经都被a,b改为了1 ,这样实际上c并不存在,但是返回的结果是存在,这样就产生了误判的问题,但是如果判断结果是c不存在那么一定是不存在的,如果判断结果是c存在,c大概率是存在,小概率不存在。我们在查询redis之前再加一层布隆过滤器,如果数据不存在,就直接返回。这个方案内存占用少但是有误判的概率。
缓存击穿:当给一个key设置了过期时间,key过期时,恰好有大量的请求发过来,可能瞬间会把数据压垮。第一个解决方案就是加分布式锁去重建缓存,这个方案一致性强,但是性能可能稍差。第二个解决方案就是设置逻辑过期,就是没有设置过期时间,只有一个理论上的过期时间,发现过期之后首先获取分布式锁,获取到锁就创建一个新的线程去异步重建缓存,然后将这个已经过期的数据直接返回。获取不到锁也直接返回旧数据,这样性能高,但是数据同步做不到强一致性。
缓存雪崩:同一时间段内大量的缓存key失效,或者redis宕机,大量的请求到达数据库,解决方案:可以给key在一定的范围内给出随机的过期时间,还可以使用redis集群的方式,一个redis宕机后,其他的还可以使用。
上面的三个问题都可以使用 当请求过多时进行降级或者限流,给业务添加多级缓存都能在一定程度上解决缓存的问题,
数据淘汰过期策略
数据过期策略 已经过期
第一个就是惰性删除,给一个数据设置过期时间后不去管他,等查询的时候检测其是否过期,如果过期了旧删掉。反之返回该数据。有点是对cpu友好,只有使用时才会检查是否过期,缺点对内存不友好,如果一个key一直没有使用会一直存在内存中。
第二个就是定期删除策略。每隔一段时间,从一定数量的数据库里随机取出一些key检查,删除过期的key,有两种模式,slow模式,定时任务定时去按照一定频率执行,fast是频率不固定,间隔时间和每次操作时间都较短。有点是可以限制调整删除操作的时长减少对cpu的影响,有效释放内存,缺点就是时长和频率不好把握,。redis是两种方式配合使用的。
数据淘汰策略 数据没有过期
当redis的内存不够用时,如果这个时候在向redis中添加数据,就会按照数据淘汰策略进行操作。然后redis支持8种策略来选择要进行的操作。
第一种时不淘汰任何的key,但是不允许写入新的数据,redis默认就是这种操作,
第二种是对设置了过期时间的值,比较剩余的过期时间,剩余时间越少越先淘汰,
第三种是对全体的key随机进行淘汰
第四种是对设置的过期时间的key随机进行淘汰
第五种是对全体key使用lru算法进行淘汰,最近最少使用,用当前时间减去最后一次访问的时间,越久的就先淘汰。
第六种是对设置了TTL的key使用lru算法进行淘汰
第七种是全体key使用lfu算法进行淘汰 ,最少使用频率。,会统计每个key的使用频率,频率越小就越先淘汰,
第八种是对设置了ttl的进行lfu算法进行淘汰
如果业务有明显的冷热区分,优先使用全体key使用lru算法进行淘汰
如果业务访问频率差别不大,没有明显的冷热数据区分,使用随机淘汰,
如果业务有有置顶的需求,也就是不设置过期时间的数据,使用对全体设置了过期时间的key进行lru淘汰
如果业务中有短时间高频的数据访问,可以使用lfu进行淘汰
1000w数据只能存20w 保证热点数据,使用lru算法,默认内存满了再加入会直接报错。
Redis集群问题
在集群中,如果我们只使用普通的jvm里的锁同一个jvm只能解决自己jvm线程里的互斥,解决不了多个jvm线程的互斥,在集群的情况下,就需要分布式锁解决问题。分布式锁在加锁成功之后会有一个看门狗每隔过期时间的三分之一做一次过期时间的续期,保证在业务完成之前不释放锁,当其他线程 在加锁的时候,加锁如果失败会进入一个while循环不断获取锁,使分布式锁在高并发的情况下有更好的性能,当然这个while循环也有等待的时间,防止cpu的消耗。Redission分布式锁是可重入的冲入一次,该分布式锁的重入次数就会加一只有重入次数为0的时候才可以释放。但是在集群的情况下,主节点宕机,数据不同步,可能会导致两个线程获取到同一个锁的情况,为了避免这种情况,redis还有一个红锁,在多个redis实例上创建锁,只有获取到n/2+1个节点的锁,才能真正加上锁,虽然红锁解决了上面的问题,但是其实现复杂,性能差,运维繁琐,对运维也有压力。如果非要保持集群的强一致性,可以使用zookeeper实现的分布式锁。
redis的集群方案有三种
主从复制
一般有一个主节点和多个从节点,主节点用于写操作,从节点用于读操作,从而实现读写分离,提高redis的并发能力,主从数据同步一般有两种方式,第一种方式就是全量同步,首先从节点会发送建立连接的请求,判断数据集id是否一致,每一个主节点都有一个数据集id,从节点继承主节点的数据集id,如果数据集id为空说明是第一次请求同步,然后主节点将这个id和一个偏移量发给从节点,从节点保存这个信息,主节点执行bgsave生成一个RDB文件,将文件同步给从节点用来同步数据,这个过程中如果有新数据写入主节点,主节点会将新来的数据存入repl_backlog一个日志中,然后发送给从节点,从节点进行数据更新
还有就是增量同步,当从节点重启或者主节点数据变化是,主节点会将数据集id和偏移量发给主节点,主节点去一个用于同步给从节点的日志中根据传入的偏移量将数据同步给从节点。
哨兵模式
主从模式有几个缺点,不能保证高可用,主节点宕机后就不能正常工作了,然后就有了这个哨兵模式,可以实现集群的故障恢复,Sentinel也就是哨兵可以监控主从节点是否正常工作,当主节点宕机后,哨兵会找一个从节点作为新的主节点,故障恢复后,也以新的master为主,当集群更换新的主节点之后,哨兵会将服务变更通知给redis客户端,哨兵每隔1s向每个实例发送ping命令当接收到pong之后表示正常工作。如果某个实例未在规定时间内响应,主观下线,当一定数量的哨兵查询到节点下线,则表示该节点客观下线。
当主节点宕机后选择新的主节点规则:判断从节点与主节点断开时间的长度,时间太长就排除该节点,判断从节点的一个权重优先级的值,优先级值越小,优先级越高,如果优先级一样,就看offset,越大数据越完整,优先级越高,最后是salve节点的id,越小优先级越高。
脑裂问题,当哨兵和主节点直接的网络不通畅时,会新选择一个从节点,会把新节点选为主节点,这个过程中redis客户端还是继续往之前的节点存储数据,当网络恢复之后,之前的主节点会被降级为从节点,将新的主节点的信息同步过去,会导致数据的丢失。解决方案就是主节点最少有一个从节点(老节点没有从节点就不能用了),数据复制和同步的延迟不能超过5s(老节点向从节点同步数据时间很长,所以也不能用了)
分片集群
主从和哨兵模式解决了高可用,高并发读的问题,使用分片集群才能解决海量数据存储和高并发写的问题,特征:集群中有多个master,每个master都能保存不同的数据,每个主节点都能有多个从节点,master直接通过ping监测彼此的状态 ,客户端可以访问任意的主节点,最终会被路由到正确的节点。集群会被分为16384个哈希槽,每个key通过crc校验对这个数取模来决定放哪个槽,每个节点都负责一部分哈希槽。
Spring
Spring基本知识
spring中的bean默认时单例的,可以加@scope注解将其设置为多例。
当bean中定义了可修改的成员变量的时候就要考虑线程安全问题,可以加锁解决,还可以将bean改为多例,每次请求都会创建一个新对象。
IOC主要就是IOC容器,控制反转和依赖注入,ioc就是通过包扫描将相关的bean封装成BeanDefinition,然后存放到BeanDefinitionMap,需要的对象可以根据这个map拿出BeanDefinition通过反射创建对象返回,控制反转就是对象主动创建变为被动创建,全部交给BeanFactory统一创建。依赖注入就是IOC容器会将对象的依赖关系动态注入
AOP就是面向切面编程,一般用于将公共逻辑和业务逻辑进行拆分,减少代码的耦合性。主要实现就是cglib动态代理和jdk动态代理,cglib动态代理是基于父子类实现的,生成一个代理子类重写父类方法,jdk的动态代理是基于接口实现的。AOP常用场景有日志处理,限流处理。
Spring中的事务支持编程式事务和声明式事务,编程式事务就是TransactionTemplate实现的,对代码有侵入性,很少使用,常用的就是声明式事务,本质就是AOP的功能,对方法进行前后拦截,根据方法的执行结果确认提交还是回滚
spring中的事务失效场景:异常捕获处理,如果我们的方法里自己处理了异常,事务会失效,抛出检查异常,Spring默认只回滚非检查异常,可以设置所以的异常都检查。非public方法,spring方法创建代理,添加事务前提式方法必须式public的
SpringBean的生命周期:首先一个bean会被spring扫描到,然后封装成一个BeanDefinition,根据这个信息构造函数通过反射创建出bean对象,然后就是bean的初始化过程,首先进行依赖注入,然后查询是否实现了aware接口,并调用对应的方法,然后就是beanpostprocesser前置处理,然后通过InitializingBean或者init-method进⾏初始化Bean,然后就是beanpostprocesser后置处理,最后不使用的时候通过destroymethod或DisposableBean进⾏销毁Bean。
spring中的循环引用问题:当对象a初始化的时候注入了对象b同时对象b又在初始化对象的时候注入了a,这个时候就出现了循环引用问题。spring使用的式三级缓存解决的循环依赖问题,一级缓存就单例池,存放的是经历了完整的生命周期,一级完成初始化的bean,二级缓存就是创建了对象但是还没初始化的bean,存放bean的半成品,三级缓存就是存放的对象工厂,用来创建某个对象的。我来讲一下这个流程,首先就是执行a的构造函数,然后就有了a对象的半成品,将这个半成品放入到二级缓存中,然后继续走后面的流程发现需要注入对象b,但是b不存在,就先初始化b,将b存入二级缓存,当需要注入a的时候,从二级缓存获取a的半成品注入到b中,b初始化成功将b存入一级缓存,删除二级缓存中的b,然后将b注入给a,a创建成功存入一级缓存,删除二级缓存的a。
但是当需要a的代理对象是,二级缓存就不能够完成了,这个时候就需要三级缓存来处理aop,三级缓存存放的是对象工厂,发现产生循环依赖时就用对象工厂创建动态代理类,对应的一级缓存存放的也是代理对象。
前面都是说的初始化中循环依赖的解决方法,在构造方法中出现循环依赖,使用@Lazy注解进行懒加载,需要对象的时候在创建
SpringMVC执行流程
在前后端分离的项目中,浏览器首先发起请求,DispatcherServlet接收到请求,就会到HandlerMapping处理器映射器里面找到对应处理的方法,返回一个处理器链,因为这个过程可能会有一些拦截器,返回给DispatcherServlet,再去找到对应的处理器适配器HandlerAdaptor(处理参数和返回值),然后真正执行方法,处理请求响应,最后一步步返回,由DispatcherServlet返回给前端。
在前后端不分离的项目中,与前后端分离的项目不同的是DispatcherServlet接收到处理器的响应之后会把信息给视图解析器,返回一个view对象,然后渲染视图,最后将结果返回给前端
SpringBoot自动配置原理:springboot启动类上由一个@SpringBootApplication注解,这个注解里面封装了三个注解,@SpringBootConfiguration就是当前这个类是一个配置类@ComponentScan 用来扫描包的注解,用于将对应的类加入spring容器
@EnableAutoConfiguration,是实现自动配置核心,这个注解使用@import注解导入一个配置选择器,将其放在最后进行加载,然后会去引入的jar包的classpath类路径下的META-INF/spring.factories读取到配置类的全类名,把需要装配的类根据条件装配到spring容器中。
常用注解:@Component、@Controller、@Service、@Repository都是在类上使用用于将对象注入spring容器。
@Autowired 在类属性上,用于将该对象注入进来,@Qualifier配合autowired一起使用,根据名称进行依赖注入@Scope标注bean的作用范围
@Configuration表示当前类是一个配置类@ComponentScan用于指定spring扫描的包。,@Bean用在方法上,将该方法返回值注入到spring容器中,@Import导入的类会加入到spring容器中@Aspect、@Before、@After、@Around、@Pointcut用于切面编程,aop。
@RequestMapping可以用在类或者方法上,用于映射请求路径@RequestBody接收post请求然后将其转换为java对象@RequestParam接收前端请求参数并指定名称@PathViriable用于请求路径下的参选reslful@ResponseBody将响应转换为json返回@RequestHeader获取请求头信息@RestController@Controller+@ResponseBody。
@SpringBootConfiguration组合了-@Configuration注解,是一个配置类,@EnableAutoConfiguration开启自动配置功能@ComponentScan扫描spring组件注入到容器中
Mybatis基本知识
执行流程
首先读取mybatis的配置文件,加载运行环境,然后构建会话工厂SqlSessionFactory,然后使用会话工厂创建会话,sqlsession包含了执行sql语句的所有方法,然后再操作数据库接口,使用的是Executor执行器去操作数据库,同时负责查询缓存,执行器里有一个MappedStatement,封装了我们要操作的sql的所有信息,封装了输入参数,最后返回输出结果。
延迟加载就是比如一个用户类,然后用户类里面由一个订单集合的属性,当我们查用户的时候,用不到订单但是也把订单查出来,这样会导致,查询时间变长,造成不必要的cpu使用,这个时候就可以使用延迟加载,只有用到这个订单的时候再去数据库查询对应的信息。首先使用cglib创建目标对象的代理对象,调用目标的方法,然后判断该数据是否为空,如果为空就去查询,然后将数据封装到对象的属性中,,然后再去获取结果。
mybatis的一级缓存和二级缓存首先,一级缓存和二级缓存都是基于本地缓存的,PerpetualCache,本质是一个hashmap,然后一级缓存的作用域是session级别的,默认打开,当session刷新或关闭之后缓存会消失。二级缓存是基于mapper和namespace的默认关闭,不受会话的影响,需要在配置文件中打开。二级缓存的数据需要实现Serializable接口。
SpringCloud基本知识
springcloud5大组件,Eureka 注册中心,Ribbon 负载均衡,Feign远程调用,Hystrix服务熔断,zuul/getway网关
springcloudalibaba 注册中心和配置中心/nacos 负载均衡Ribbon 服务调用Feign 服务保护sentinel 服务网关getway
如何实现服务的注册发现:Eureka首先在服务启动时,服务会把自己的信息注册到eureka,由注册中心保存这些信息,消费者会像eureka定时拉取服务列表信息,调用,如果服务提供者由多个会使用负载均衡算法进行调用。服务提供者会30s发送一次心跳表面自己的健康状态,如果90s没有收到心跳,则从注册中心剔除。
nacos多加了一个非临时实例,临时实例心跳不正常会被踢出,非临时实例不会,临时实例采用心跳模式,非临时实例采用主动检测模式。当服务提供者信息修改是,nacos会主动将信息同步给消费者,nacos默认采用的是ap的方式,当集群中有非实例变量是,采用cp模式,eureka采用的是ap模式
当使用负载均衡是会将注册中心的服务列表拉取,然后使用负载均衡策略到一个服务。简单的轮询;按照权重选择服务器,响应时间越短,权重越大;随机选择一个;忽略连接不到的,选择并发数较低的服务器;首先按照轮询选择服务,服务不能用就重试;先过滤不健康的,然后选择连接数较少的;优先选择地点近的区域的服务器进行连接,选择一个区域轮询,默认就是这个;自定义负载均衡策略,实现IRule接口;通过配置类(全局)和配置文件进行配置(局部)
服务雪崩就是一个服务失败导致整条链路的服务全都失败的情况。
服务降级就是在一个服务中如果一个服务变得不可以,为了确保服务不崩溃,不影响整个业务,做了降级处理,在开发中一般与feign接口一起使用,
服务熔断,默认是不会熔断的,当降级的次数过多时,超过了一定的阈值,就会触发服务熔断,之后每隔5s重新尝试请求服务,如果不响应继续熔断,如果有响应则关闭熔断。
在庞大的微服务架构下,有很多的问题出现,比如问题定位,性能分析,服务关系,服务警告,这几个问题,可以使用分布式应用程序性能监控工具skywalking,可以监控端点(请求的路径对应的对外暴露的功能接口)服务(微服务中的服务),实例(物理服务器)进行监控可以看到哪些服务运行比较慢。针对性进行优化和分析。设置了警告规则,当上线出现问题之后,给相关负责人发邮件短信,第一时间解决。
SpringCloud业务知识
限流是怎么做的
tomcat设置最大连接数
nginx漏桶算法 比如一个漏桶大小为20,每秒可以处理10个请求,请求来了会进入漏桶,当漏桶满了多余的请求会等待或者抛弃(有规则可以设置)
还可以控制ip所持有的连接数进行限制和虚拟主机能同时并发处理的连接来限流
网关令牌桶算法 令牌桶的大小时固定的,以一定的速率生成令牌,满了会停止生成,只有请求申请到令牌才能继续向下运行,没有请求到的阻塞或者丢弃。
自定义拦截器 使用redis对单个用户进行限制。
CAP理论就是一致性,可用性和分区容错性,分区容错性必然存在,所以知识CP(强一致性,等待彼此的结果,等统一提交或者回滚)或者AP(就是最终一致性,如果不一致,想办法恢复数据)
Bash理论是对CAP的一种解决思路,允许损失部分可用性,保证核心可用,允许出现软状态也就是临时不一致,保证最终一致性。
分布式事务一般使用Seata解决 。事务协调者用来协调全局事务提交或者回滚,维护所有事务的状态。事务管理器,定义全局事务范围,提交回滚全局事务,资源管理器,一个服务就是一个资源管理器管理分支事务提交或者回滚
XA模式,执行业务sql但是不提交,将状态报告给事务协调者,如果都成功都提交,反之回滚。 一致性强性能差
AT模式。执行sql直接提交,记录undolog日志,状态直接提交给事务管理器,成功就删除日志,失败就回滚 一致性差性能强
TCC模式。执行sql资源预留,成就直接confirm 失败cancel,性能较好,需要编码,一致性强
MQ实现 A事务操作的时候, 发送mq消息到另一个事务,异步操作,性能最好,一致性最差。
分布式服务的接口幂等怎么实现:保证每次调用接口和单次调用结果一致。
新增数据可用使用数据库唯一索引解决,新增或修改数据可用使用redis加token,进入页面时生成一个唯一token存入redis返回给前端,真正请求时,携带之前的token到redis进行验证,如果存在执行业务删除token,不存在不处理业务。
使用的分布式任务调度xxl-job
提供了很多路由策略,任务去找机器运行的方式就是路由策略,我们常用的就是轮询,故障转移,分片广播
任务执行失败选择故障转移,选择健康的实例来执行任务,尝试重试,查看日志然后发送邮件给相关负责人去解决,
如果有大量的任务需要执行可以部署集群,分片广播,可以查询获取总分片数,和当前分片,这个时是按照取模的方式分摊到各个实例执行。
RabbitMQ && Kafka
消息队列模型
包括生产者,生产者生成消息后发送到交换机,然后发送到绑定的消息队列,然后将消息发送到信道里给消费者使用,
消息可靠性
为了保证消息的可靠性,首先就是生产者确认机制,如果是交换机没收到,publish_confim类型的nack,如果是队列没收到publish_return类型nack,到了直接返回ack。消息失败后首先进行重发,然后记录日志,还可以保持到数据库中,然后定时任务重发,发送成功后删除表中的数据。
还可以采用消息持久化的方式保证可靠性,包括交换机的持久化,队列的持久化,消息的持久化,
还有就是消费者确认机制,消费者处理消息之后,像mq发送ack回执,mq收到之后才删除消息,整合spring有三者确认模式,手动ack,自动ack和关闭ack,mq投递消息之后直接删除。当消费者出现异常的时候,重试,设置重试次数,如果消息依然失败,将消息发送到异常交换机,交给其他消费者或者人工处理。
消息的幂等性,比如消费者消费消息后,网络延迟中断,然后交换机没有收到ack就会重试,导致重复消费,这个时候可用使用给每条消息设置一个唯一标识,当我们在处理消息的时候,先查数据库看这个消息有没有,不存在才进行处理。当然还可以使用分布式锁,redis,解决幂等性问题。
kafka保证消息不丢失
kafka集群有很多个broker 每个broker可能有多个topic
生成者发送到broker 设置异步发送,发送失败进行重试
kafka中会选择一个broker为leader,其他未follower
当设置ack为0时,生产者不会等待任何返回消息,为1时只要leader收到消息就表示成功,设置为2时,只有全部参与的节点收到消息才表示成功。
kafka中每个topic都会多个分区,不同的分区给不同的消费者去处理,一个消费者组去消费一个topic,消费者自动提交已经消费的偏移量,每5s提交一次,但是可能有重复消费的风险,这个时候改为手动提交,同步和异步结合使用手动提交。消费成功以后再上报,消费的位置,尽量避免重复消费为和消息丢失。
如何保证消息的顺序性
因为一个topic数据保存再不同的分区,如果要保证顺序性,需要保证消息放在同一个分区,再发送消息的,指定key,因为分区时使用的key的hashcode来指定的,只要保证key相同就可以保证相同分区,从而保证顺序性。
死信队列死信交换机
当一个消息被消费之nack拒绝了之后,消费失败,或者消息过期了,或者消息队列堆积满了,这样的消息会被送入死信交换机,死信交换机也绑定了一个死信队列然后找到消费者去处理这些消息。
如果队列和消息都设置了过期时间,以小的过期时间为标准。
延迟队列
延迟队列的一种实现就是死信交换机加上消息设置过期时间的方式, 还可以使用rabbitmq的一个延迟队列插件,进行相关配置之后,定义出一个延迟队列,在发送消息时,设置一个延迟时间,就可以完成延迟消息
高可用机制
消息堆积怎么处理
第一就是增加更多的消费者,第二就是消费之使用线程池,尽量最大限度使用cpu。第三就是扩大队列的容积,提高堆积的上限。扩大队列容积可使用惰性队列,定义队列的时候设置一个属性为lazy,收到消息存入磁盘而不是内存,消息消费的时候从磁盘加入内存,支持很大数据量的消息存储。性能稳定,但是基于磁盘,受限磁盘io效率会低。
rabbitmq的集群
首先就标准集群,每个节点都会存放某个节点的队列的信息,但是不包括队列里的消息,当消费者随便找到一个节点之后,会根据队列的信息找到真正能处理的节点,去处理队列里的消息。一旦宕机数据会丢失
镜像集群,与标准集群不同的的,每个节点都会在其他节点存一个镜像并且同步数据,创建队列的叫主节点,存镜像的叫从节点,主从节点都可能是。当主节点的队列宕机后,存镜像的节点会成为新的主节点提供服务。主节点数据还没同步就宕机可能会导致数据丢失。
仲裁队列与镜像队列一样但是基于Raft协议,强一致性。
kafka如何保证高可用,
首先就是他的集群模式,当一个节点挂掉之后,其他的节点还能继续对外提供服务,一个topic有多个分区,然后 会存储在不同的节点上,并且还有很多副本存储在不同的节点上,使用的时leader,副本就是follower,leader故障时就会找一个副本作为leader,leader同步数据时,有同步的复制数据和异步的复制数据,这个时候同步一致性强,异步性能好,当leader挂掉的时候,优先选则同步复制的副本作为leader。如果同步复制(isr)的都不行,只能选择异步复制(普通)的副本作为leader;
kafka中日志文件的清理,
每个分区又分了多个段用来存储数据,这样删除数据方便,查找数据便捷,又索引文件,数据文件和时间索引文件。删除的策略有两个一个时间超过一定时间删除(默认策略 7天),一个时topic日志文件大小大于一点的值,按照时间最久的顺序进行删除。
kafka中高性能的设计
消息分区。不受单台服务器的限制
顺序读写,读写效率高
页缓存,将磁盘数据读取到内存中,提高操作速度
消息压缩:减少磁盘io和网络io
分批发送:减少网络开销。批量打包发送
最重要的是零拷贝;不使用0拷贝时需要磁盘文件复制到缓存,再复制到用户空间,复制到socket缓存区,再复制到网卡进行发送,使用0拷贝的设计。复制到页缓存就直接复制到网卡。减少复制次数,效率更好。
几个设计模式
单例模式
单例模式(Singleton Pattern)是一种创建型设计模式,它确保一个类只有一个实例,并提供一个全局访问点来获取该实例。
常见的实现有饿汉式和懒汉式
-
懒汉式单例(Lazy Initialization)在实际使用时才创建实例,这种实现方式需要考虑线程安全问题
-
懒汉2(线程安全),因此一般会带上synchronized关键字,
-
懒汉3:双重检查锁定(Double-Checked Locking)保证线程安全,同时又减少了同步的开销,主要是用 synchronized 同步代码块来替代同步方法。减少了部分的消耗
-
枚举单例模式线程安全,写法简洁,面对反射攻击可以解决,其他懒汉式方式都不能解决反射创建的问题。
-
饿汉式单例(Eager Initialization)在类加载时就急切地创建实例,不管你后续用不用得到,这也是饿汉式的来源,简单但不支持延迟加载实例。线程安全
-
使用内部类线程安全同时不会因为加锁影响性能,类的构造方法一定线程安全的
单例模式有 5 种实现方式,常见的有饿汉式、懒汉式、双重检查锁定、静态内部类和枚举。
工厂模式
工厂模式是一种创建型设计模式,其主要目的是提供一个创建对象的接口,但将具体类的实例化延迟到子类中。这样,客户端代码就不需要知道要实例化的具体类,只需要知道使用的工厂接口。用户只需要知道具体工厂的名称就可得到所要的产品,无须知道产品的具体创建过程;方便,解耦
策略模式
策略模式(Strategy Pattern)是一种行为型设计模式,它定义了一系列的算法,将每个算法封装起来,使得它们可以相互替换。这种模式通常用于实现不同的业务规则或策略,其中每种策略封装了特定的行为或算法。
以前我们写代码的时候会经常用到if esle if,比如要判断十多种类型,每个类型都要使用if else if来判断,判断里面又有写大量的逻辑,代码耦合性太高,不利于后期拓展。策略模式就是解决这个问题的,讲每个判断逻辑单独抽取出来,通过类型直接去执行这个逻辑的代码块。
面试算法错题整理
stream流排序 (普通,绝对值,从大到小,从小到大)
nums = IntStream.of(nums)
.boxed()
.sorted((o1, o2) -> Math.abs(o2) - Math.abs(o1))
.mapToInt(Integer::intValue).toArray
();
RabbitMQ幂等性处理
数组排序
public int[][] reconstructQueue(int[][] people) {
// 身高从大到小排(身高相同k小的站前面)
Arrays.sort(people, (a, b) -> {
if (a[0] == b[0]) return a[1] - b[1]; // a - b 是升序排列,故在a[0] == b[0]的狀況下,會根據k值升序排列
return b[0] - a[0]; //b - a 是降序排列,在a[0] != b[0],的狀況會根據h值降序排列
});
// 使用Integer内置比较方法,不会溢出
Arrays.sort(points, (a, b) -> Integer.compare(a[0], b[0]));
PriorityQueue<Integer> queue = new PriorityQueue<Integer>(new Comparator<Integer>(){
public int compare(Integer num1,Integer num2){
return num2 - num1; //大顶堆
}
});
标签:总结,缓存,八股文,一个,索引,线程,数据,节点
From: https://www.cnblogs.com/zwmBlog/p/18395749