Java面试理论知识
2024-06-06 11:44:23 1 举报
AI智能生成
Java面试理论知识全面梳理
作者其他创作
大纲/内容
基础
常识
Java特性
封装
Java权限(关键字)
private
protected
default
public
继承
抽象类与接口
多态
重载与重写
Java变量
类变量(静态变量)(静态属性)
成员变量(实例变量,属性)
局部变量(本地变量)
Java类
创建对象
1、使用new关键字 } → 调用了构造函数
2、使用Class类的newInstance方法 } → 调用了构造函数
3、使用Constructor类的newInstance方法 } → 调用了构造函数
4、使用clone方法 } → 没有调用构造函数
5、使用反序列化 } → 没有调用构造函数
2、使用Class类的newInstance方法 } → 调用了构造函数
3、使用Constructor类的newInstance方法 } → 调用了构造函数
4、使用clone方法 } → 没有调用构造函数
5、使用反序列化 } → 没有调用构造函数
Object类常用方法,分别怎么实现
常用方法:toString、getClass、equals、hashcode、clone、wait、notify、notifyall、finalize
hashcode
hashcode主要用于提高容器查找和存储的快捷性,如 HashSet, Hashtable,HashMap 等
hashCode是用来在散列存储结构中确定对象的存储地址的
重复的数据hashCode一定相等,但是hashCode相等的数据不一定相同
碰撞的概率与具体的算法有关
==与equals:其实它们两个都是通过比较内存地址(准确的说是堆内存地址)来比较两个数据是否相等,但是为什么实际使用时好像不一样呢,这是因为equals很重要的一个特点,可以被重写
String类
为什么设计成final类
字符串常量池实现(堆中)
安全性:不可变
多线程安全:
finalize:Object对象方法,gc回收对象的时候调用一次,可用在对象被回收时需要释放资源的场景下,例如关闭socket连接。
String str1="abc",String str2="abc",String str3=new String("abc"),String str4=new String("abc")
==:1和2相等,3和4不相等,1和3不相等
equals:四者相等
原因分析:Java String类的equals方法重写了,它比较的两个字符串的内容是否相等,符合大多数时候人们对于字符串比较的需求。Java中equals方法重写了的还有Integer, Data等等
String str3=new String("abc"),可能创建一个对象(堆中)也可能创建两个对象(字符串常量池中)
String、StringBuffer与StringBuilder
hashcode:String类中重写的hashCode()方法,以31为权,每一位为字符的ASCII值进行运算,用自然溢出来等效取模
equals:该方法去比较两个对象时,首先先去判断两个对象是否具有相同的地址,如果是同一个对象的引用,则直接放回true;如果地址不一样,则证明不是引用同一个对象,接下来就是挨个去比较两个字符串对象的内容是否一致,完全相等返回true,否则false
序列化
概念:将对象的状态信息转换为可以存储或传输的形式的过程
作用:(1)把内存中的对象写入到硬盘(2)用套接字在网络上传输对象
实现:(1)实现Sericalizable接口(2)实现Externalnalizable接口
反序列化漏洞
反序列化是一系列安全问题的根源:攻击者能够将恶意数据序列化并存储到数据库或内存中,当应用进行反序列化时,应用会执行到恶意代码
解决方案
1、对序列化对象执行完整性检查或加密
2、在创建对象之前强制执行严格的类型约束
3、隔离反序列化的代码,使其在非常低的特权环境中运行
反射
概念:在运行状态中,动态获取类的信息以及动态调用类对象的方法的功能
原理
获取反射中的Class对象
使用 Class.forName 静态方法,根据类的全路径名获取类的Class对象
使用类 .class 方法,只适合在编译前就知道操作的 Class
使用类对象的getClass() 方法
通过反射创建类对象
通过 Class 对象的 newInstance() 方法
通过 Constructor 对象的 newInstance() 方法
根据 Class 对象实例获取 Constructor 对象
使用 Constructor 对象的 newInstance 方法获取反射类对象
通过反射调用方法
获取方法的 Method 对象
利用 invoke 方法调用方法
应用
JDBC中,利用反射动态加载了数据库驱动程序
Web服务器中利用反射调用了Sevlet的服务方法
Spring框架用到反射机制,注入属性,调用方法
泛型
异常机制,项目怎么处理异常,自定义异常如何实现
final finally 和 finalize的区别
final:类不能继承,方法不能重载,变量不可改变(常量)
finally:异常处理的时候使用,经常被用在需要释放资源的情况下(try未执行或者执行try时程序中断都会导致finally不执行)
Cookie和Session
wait和sleep区别
原理不同:sleep()方法是Thread类的静态方法,是线程用来控制自身流程的,他会使此线程暂停执行一段时间,而把执行机会让给其他线程,等到计时时间一到,此线程会自动苏醒;而wait()方法是object类的方法,用于线程间通信,这个方法会使当前拥有该对象锁的进程等待,直到其他线程调用notify()方法或者notifyAll()时才醒来,不过开发人员也可以给他指定一个时间,自动醒来
对锁的处理机制不同:sleep()方法并不会释放锁,而wait()方法则不同,当调用wait()方法后,线程会释放掉他所占用的锁,从而使线程所在对象中的其他synchronized数据可以被其他线程使用。
使用区域不同:wait()方法必须放在同步控制方法和同步代码块中使用,sleep()方法则可以放在任何地方使用。
集合
List
ArrayList
底层数据结构:数组
Vector
底层数据结构:数组
LinkedList
底层数据结构:双向链表
Map
HashMap
底层数据结构:数组+链表+红黑树(Node数组+Node链表+TreeNode(红黑树),使用拉链法在Node数组中记录了每个链表的第一个节点)
如果链表的长度超过了8,那么链表转换为红黑树。(桶的数量必须大于64,小于64的时候只会扩容)
当链表长度小于6时,resize(扩容)的时候才会根据 UNTREEIFY_THRESHOLD 进行转换
原理:HasMap线程不安全,默认容量:16,默认加载因子:0.75,元素数超过容量*加载因子时,并且要存放的位置已经有元素了(hash碰撞),会对哈希表进行 rehash 操作,会将容量扩大为原来两倍。Put元素时,先计算key的hashcode值(key最好选择字符串【已保证同样的值hashcode相等】,其他对象类型需要重写hashcode方法),根据hashcode计算key的hash值(h=key.hashCode()) 与 (h >>> 16)(h的高16位) 异或,结果离散性更好,降低hash碰撞概率),根据hash值计算数组index(两种算法:取模运算(index=hash%length)和位运算(index=hash&(length-1)),判断当前数组位置是否有值,如果没有,直接插入;如果有值,需要根据==或者equals方法(String的equals方法)判断key是否已经存在,存在的话直接覆盖,返回旧value;不存在的话(hash冲突),判断点前数组位置节点是链表节点还是红黑树节点,根据==或者equals方法查询链表或者红黑树中的对象是否存在,存在的话进行覆盖,不存在的话进行插入。
默认加载因子是 0.75, 这是在时间和空间成本("冲突的机会"与"空间利用率"之间)上寻求一种折衷。
加载因子过高虽然减少了空间开销,但同时也增加了查询成本
加载因子过高虽然减少了空间开销,但同时也增加了查询成本
HashMap的方法不是线程安全的,HashMap在并发执行put操作时发生扩容,可能会导致节点丢失,产生环形链表等情况。
1.节点丢失,会导致数据不准
2.生成环形链表,会导致get()方法死循环
在jdk1.7中,由于扩容时使用头插法,在并发时可能会形成环状列表,导致死循环。
(这个原因主要是因为hashMap在resize过程中对链表进行了一次倒序处理。)
在jdk1.8中改为尾插法,可以避免这种问题,但是依然避免不了节点丢失的问题。
(多线程下put操作时,执行addEntry(hash, key, value, i),如果有产生哈希碰撞,
导致两个线程得到同样的bucketIndex去存储,就可能会出现覆盖丢失的情况)
HashMap的设计初衷就不是在并发情况下使用,如果有并发的场景,推荐使用ConcurrentHashMap
1.节点丢失,会导致数据不准
2.生成环形链表,会导致get()方法死循环
在jdk1.7中,由于扩容时使用头插法,在并发时可能会形成环状列表,导致死循环。
(这个原因主要是因为hashMap在resize过程中对链表进行了一次倒序处理。)
在jdk1.8中改为尾插法,可以避免这种问题,但是依然避免不了节点丢失的问题。
(多线程下put操作时,执行addEntry(hash, key, value, i),如果有产生哈希碰撞,
导致两个线程得到同样的bucketIndex去存储,就可能会出现覆盖丢失的情况)
HashMap的设计初衷就不是在并发情况下使用,如果有并发的场景,推荐使用ConcurrentHashMap
HashMap 和 Hashtable 有什么区别?
① HashMap 是线程不安全的, HashTable中的方法都有Synchronized修饰,线程安全。
② 由于线程安全,所以Hashtable 的效率比不上 HashMap。
③ HashMap最多只允许一条记录的键为 null,允许多条记录的值为null,而Hashtable 不允许。
④ HashMap 默认初始化数组的大小为16,Hashtable 为11,前者扩容时,扩大两倍,后者扩大 两倍 +1.
⑤ HashMap 需要重新计算 hash 值,而 Hashtable 直接使用对象的 hashcode
② 由于线程安全,所以Hashtable 的效率比不上 HashMap。
③ HashMap最多只允许一条记录的键为 null,允许多条记录的值为null,而Hashtable 不允许。
④ HashMap 默认初始化数组的大小为16,Hashtable 为11,前者扩容时,扩大两倍,后者扩大 两倍 +1.
⑤ HashMap 需要重新计算 hash 值,而 Hashtable 直接使用对象的 hashcode
HashMap & ConcurrentHashMap 的区别?
除了加锁,原理上无太大区别。另外,HashMap 的键值对允许有null,但是ConcurrentHashMap 都不允许。
Hashtable
底层是链地址法组成(拉链法)的哈希表(即数组+单向链表组成)(与HashMap原理相同)
ConcurrentHashMap
在 JDK1.7采用 分段锁的方式,JDK1.8 中直接采用了 CAS(无锁算法 ) + synchronized
TreeMap
底层红黑树,实现了SortMap接口,能够把保存的记录根据键排序,(默认是按键值升序排序,也可以指定排序的比较器)
Linkedhashmap
底层修改自HashMap,包含一个维护插入顺序的双向链表。保存了记录的插入顺序,用Iterator遍历时,先取到的记录肯定是先插入的,遍历比HashMap慢
Set
HashSet
底层HashMap
TreeSet
底层是TreeMap
LinkedHashSet
底层是LinkedHashMap
并发编程
JMM
定义:JMM即Java内存模型(Java memory model),在JSR133里指出了JMM是用来定义一个一致的、跨平台的内存模型,是缓存一致性协议,用来定义数据读写的规则
三要素
原子性:一个或者多个操作,要么全部执行并且在执行的过程中不被其他操作打断,要么就全部都不执行
可见性:多个线程操作一个共享变量时,其中一个线程对变量进行修改后,其他线程可以立即看到修改的结果
有序性:程序的执行顺序按照代码的先后顺序来执行
进程与线程
概念
进程概念:进程是是操作系统进行资源分配和调度的一个独立单位,是应用程序运行的载体
线程概念:线程是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。
通信方式
进程通信方式
管道(pipe):管道是一种半双工的通信方式,数据只能单向流动,而且只能在具有亲缘关系的进程间使用进程间的亲缘关系通常是指父子进程关系。
命名管道(named pipe/FIFO):命名管道也是半双工的通信方式,但是它允许无亲缘关系进程间的通信
信号量(semophonre):信号量是一个计数器,可以用来控制多个进程队共享资源的访问。它常作为一个锁机制,防止某进程在访问共享资源时,其他进程也访问此资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段
消息队列(message queue):消息队列是由消息的链表,存放在内核中并由消息队列标识符标识。消息队列克服了信号传递信息少,管道只能承载无格式字节流以及缓冲区大小受限等缺点
共享内存(shared memory):共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。
套接字(socket):套接字也是一种进程间通信机制,与其他通信机制不同的是,它可用于不同设备间的进程通信
全双工管道:共享内存、信号量、消息队列、管道和命名管道只适用于本地进程间通信,套接字和全双工管道可用于远程通信,因此可用于网络编程
线程通信方式
方式一:使用 volatile 关键字定义全局变量
方式二:使用Object类的wait() 和 notify() 方法
方式三:使用 ReentrantLock 结合 Condition
方式四:使用消息队列
线程
状态
新建状态(New): 线程对象被创建后,就进入了新建状态
就绪状态(Runnable): 也被称为“可执行状态”。线程对象被创建后,其它线程调用了该对象的start()方法,从而来启动该线程。
运行状态(Running): 线程获取CPU权限进行执行。需要注意的是,线程只能从就绪状态进入到运行状态。
阻塞状态(Blocked): 阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态
等待阻塞 -- 通过调用线程的wait()方法,让线程等待某工作的完成
同步阻塞 -- 线程在获取synchronized同步锁失败(因为锁被其它线程所占用),它会进入同步阻塞状态
其他阻塞 -- 通过调用线程的sleep()或join()或发出了I/O请求时,线程会进入到阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态
死亡状态(Dead): 线程执行完了或者因异常退出了run()方法,该线程结束生命周期
创建方式
继承Thread类(extends Thread)(lang)
实现Runnable接口(implements Runnable)(lang)
实现Callable接口(implements Callable)(JUC)
多线程
优点
发挥出多核CPU的优势来,达到充分利用CPU资源的目的
防止单线程阻塞导致业务无法进行的问题
缺点
线程的生命周期开销非常高,过多的线程会造成大量内存消耗,运行多线程导致线程上下文的切换,会降低程序执行的效率
降低稳定性,JVM在可创建线程的数量上存在一个限制,如果破坏了这些限制,那么可能抛出OutOfMemoryError异常
线程安全
Synchronized(关键字)
底层实现
synchronized有两种形式上锁,一个是同步方法,一个是同步代码块。同步块会生成对应的monitorenter和monitorexit指令分别对应synchronized同步块的进入和退出(有两个monitorexit指令的原因是:为了保证抛异常的情况下也能释放锁);同步方法则会生成ACC_SYNCHRONIZED标记符,在JVM进行方法调用时,发现调用的方法被ACCSYNCHRONIZED修饰,则会先尝试获得锁。
基本规则
当一个线程访问“某对象”的“synchronized方法”或者“synchronized代码块”时,其他线程对“该对象”的“synchronized方法”或者“synchronized代码块”的访问都将被阻塞;其他线程可以访问“该对象”的非同步方法或者代码块
Synchronized的三种使用方式
1.普通同步方法(实例方法):锁是当前实例对象 ,进入同步代码前要获得当前实例的锁
2.静态同步方法:锁是当前类的class对象 ,进入同步代码前要获得当前类对象的锁
3.同步方法块:锁是括号里面的对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁
锁状态:无锁、偏向锁、轻量级锁和重量级锁,可以看到锁信息也是存在于对象头的mark word中的。
当对象状态为偏向锁(biasable)时,mark word存储的是偏向的线程ID;
当状态为轻量级锁(lightweight locked)时,mark word存储的是指向线程栈中Lock Record的指针;
当状态为重量级锁(inflated)时,mark word存储的是指向堆中的monitor对象的指针。
当状态为轻量级锁(lightweight locked)时,mark word存储的是指向线程栈中Lock Record的指针;
当状态为重量级锁(inflated)时,mark word存储的是指向堆中的monitor对象的指针。
Java中的synchronized有偏向锁、轻量级锁、重量级锁三种形式,分别对应了锁只被一个线程持有、不同线程交替持有锁、多线程竞争锁三种情况。当条件不满足时,锁会按偏向锁->轻量级锁->重量级锁 的顺序升级。JVM种的锁也是能降级的,只不过条件很苛刻,不在我们讨论范围之内。
Synchronized和ReentrantLock区别
synchronized 是JVM层面的锁,是Java关键字,通过monitor对象来完成,
ReentrantLock 是API层面的锁
ReentrantLock 是API层面的锁
synchronized 的实现涉及到锁的升级,具体为无锁、偏向锁、自旋锁、向OS申请重量级锁,
ReentrantLock实现则是通过利用CAS(CompareAndSwap)自旋机制保证线程操作的原子性和volatile保证数据可见性以实现锁的功能
ReentrantLock实现则是通过利用CAS(CompareAndSwap)自旋机制保证线程操作的原子性和volatile保证数据可见性以实现锁的功能
synchronized 不需要用户去手动释放锁,synchronized 代码执行完后或者发生异常系统会自动释放线程占用的锁;
ReentrantLock则需要用户去手动释放锁,如果没有手动释放锁,就可能导致死锁现象。
ReentrantLock则需要用户去手动释放锁,如果没有手动释放锁,就可能导致死锁现象。
synchronized是不可中断类型的锁,除非加锁的代码中出现异常或正常执行完成;
ReentrantLock则可以中断,可通过trylock(long timeout,TimeUnit unit)设置超时方法或将lockInterruptibly()放到代码块中,调用interrupt方法进行中断
ReentrantLock则可以中断,可通过trylock(long timeout,TimeUnit unit)设置超时方法或将lockInterruptibly()放到代码块中,调用interrupt方法进行中断
synchronized为非公平锁 ,ReentrantLock则即可以选公平锁也可以选非公平锁,通过构造方法new ReentrantLock时传入boolean值进行选择,为空默认false非公平锁,true为公平锁。
synchronized不能绑定Condition; ReentrantLock通过绑定Condition结合await()/singal()方法实现线程的精确唤醒,而不是像synchronized通过Object类的wait()/notify()/notifyAll()方法要么随机唤醒一个线程要么唤醒全部线程
synchronzied锁的是对象,锁是保存在对象头里面的,根据对象头数据来标识是否有线程获得锁/争抢锁;ReentrantLock锁的是线程,根据进入的线程和int类型的state标识锁的获得/争抢
Volatile(关键字)
特性:可见性、原子性
内存语义
volatile写的内存语义:当线程写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量值刷新到主内存
volatile读的内存语义:当线程读一个volatile变量时,JMM会把该线程对应的本地内存置为无效,线程从主内存读取共享变量
底层实现
volatile可见性的实现就是借助了CPU的lock指令,通过在写volatile的机器指令前加上lock前缀,使写volatile具有以下两个原则:
1:写volatile时处理器会将缓存写回到主内存。
2:一个处理器的缓存写回到内存会导致其他处理器的缓存失效。
2:一个处理器的缓存写回到内存会导致其他处理器的缓存失效。
volatile有序性是通过加内存屏障禁止指令重排序来实现的。
JMM为volatile加内存屏障有以下4种情况:
1:在每个volatile写操作的前面插入一个StoreStore屏障,防止写volatile与后面的写操作重排序。
2:在每个volatile写操作的后面插入一个StoreLoad屏障,防止写volatile与后面的读操作重排序。
3:在每个volatile读操作的后面插入一个LoadLoad屏障,防止读volatile与后面的读操作重排序。
4:在每个volatile读操作的后面插入一个LoadStore屏障,防止读volatile与后面的写操作重排序。
1:在每个volatile写操作的前面插入一个StoreStore屏障,防止写volatile与后面的写操作重排序。
2:在每个volatile写操作的后面插入一个StoreLoad屏障,防止写volatile与后面的读操作重排序。
3:在每个volatile读操作的后面插入一个LoadLoad屏障,防止读volatile与后面的读操作重排序。
4:在每个volatile读操作的后面插入一个LoadStore屏障,防止读volatile与后面的写操作重排序。
ReentrantLock
底层实现
ReentrantLock底层使用了CAS+AQS队列实现,ReentrantLock先通过CAS尝试获取锁,如果此时锁已经被占用,该线程加入AQS队列并wait()
当锁被释放,挂在CLH队列为首的线程就会被notify(),然后继续CAS尝试获取锁,此时:
公平锁,只有在CLH队列头的线程才可以获取锁,新来的线程只能插入到队尾。
非公平锁,如果有其他线程尝试lock(),有可能被其他刚好申请锁的线程抢占。
当锁被释放,挂在CLH队列为首的线程就会被notify(),然后继续CAS尝试获取锁,此时:
公平锁,只有在CLH队列头的线程才可以获取锁,新来的线程只能插入到队尾。
非公平锁,如果有其他线程尝试lock(),有可能被其他刚好申请锁的线程抢占。
一般与Condition配合使用
Condition 是 Java 提供用来实现等待/通知的类,它由lock对象创建,调用await()使当前线程进入阻塞,调用signal()唤醒该线程继续执行
ThreadLocal
作用:实现线程范围内的局部变量,即ThreadLocal在一个线程中是共享的,在不同线程之间是隔离的
原理:每个线程对象都有一个ThreadLocalMap,ThreadLocal存入值时使用当前ThreadLocal实例作为key,存入当前线程对象的ThreadLocalMap中
注意事项
线程复用ThreadLock变量也会被复用,会造成脏数据
ThreadLocal变量通常使用关键字static修饰,其生命周期不会随着线程结束而结束,会造成内存泄漏
死锁
概念:一组相互竞争资源的线程因为互相等待,导致“永久”阻塞的现象
死锁发生的四个必要条件
互斥,共享资源只能被一个线程占用
占有且等待,线程 已经取得共享资源 a,在等待共享资源 b 的时候,不释放共享资源 a
不可抢占,其他线程不能强行抢占线程占有的资源
循环等待,线程a等待线程b占有的资源,线程b等待线程a占有的资源,就是循环等待
避免死锁
避免多次锁定。尽量避免同一个线程对多个 Lock 进行锁定。
具有相同的加锁顺序。如果多个线程需要对多个 Lock 进行锁定,则应该保证它们以相同的顺序请求加锁。
使用定时锁。超过指定时间后会自动释放对 Lock 的锁定,这样就可以解开死锁了
死锁检测。死锁检测是一种依靠算法机制来实现的死锁预防机制,它主要是针对那些不可能实现按序加锁,也不能使用定时锁的场景的
CAS
概念:CAS是一种无锁算法。有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做
底层实现:JAVA中的CAS操作是通过sun包下Unsafe类实现,而Unsafe类中的方法都是native方法,由JVM本地实现,调用UnSafe类中的CAS方法,JVM会帮我实现CAS汇编指令,通过它实现了原子操作
不足
ABA问题:在更新前的值是A,但在操作过程中被其他线程更新为B,又更新为 A
不能保证代码块的原子性:CAS机制能保证一个变量的原子性操作,但不能保证整个代码块的原子性
CAS造成CPU利用率增加:CAS里面是一个循环判断的过程,如果线程一直没有获取到状态,cpu资源会一直被占用
典例
JUC包下的Atomic原子操作类,通过CAS(compare and swap)乐观锁方式实现线程安全
锁分类
乐观锁 & 悲观锁
乐观锁
概念:乐观锁认为一个线程去拿数据的时候不会有其他线程对数据进行更改,所以不会上锁
实现方式:CAS机制、版本号机制
典例:
悲观锁
概念:悲观锁认为一个线程去拿数据时一定会有其他线程对数据进行更改,所以必须上锁
实现方式:加锁
典例:synchronized关键字的实现就是悲观锁
独占锁 & 共享锁
独占锁
概念:该锁一次只能被一个线程所持有。根据锁的获取机制,它又划分为“公平锁”和“非公平锁”。
公平锁:是按照通过CLH等待线程按照先来先得的规则,公平的获取锁
非公平锁:当线程要获取锁时,它会无视CLH等待队列而直接获取锁
实现方式:独享锁与共享锁通过AQS(AbstractQueuedSynchronizer)来实现的,通过实现不同的方法,来实现独享或者共享
典例:synchronized是独占锁,ReentrantLock,此外,ReentrantReadWriteLock.WriteLock也是独占锁
共享锁
概念:该锁可以被多个线程所持有
实现方式:独享锁与共享锁通过AQS(AbstractQueuedSynchronizer)来实现的,通过实现不同的方法,来实现独享或者共享
典例:JUC包中的ReentrantReadWriteLock.ReadLock,CyclicBarrier, CountDownLatch和Semaphore都是共享锁
互斥锁 & 读写锁
互斥锁的具体实现就是synchronized、ReentrantLock
读写锁的具体实现就是读写锁ReadWriteLock
可重入锁(优点:避免死锁)
概念:对于同一个线程在外层方法获取锁的时候,在进入内层方法时也会自动获取锁
典例:ReentrantLock、synchronized
分段锁
分段锁并不是具体的一种锁,只是一种锁的设计。分段锁的设计目的是细化锁的粒度
无锁、偏向锁、轻量级锁和重量级锁
锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了不让这个线程每次获得锁都需要CAS操作的性能消耗,就引入了偏向锁。当一个线程访问对象并获取锁时,会在对象头里存储锁偏向的这个线程的ID,以后该线程再访问该对象时只需判断对象头的Mark Word里是否有这个线程的ID,如果有就不需要进行CAS操作,这就是偏向锁。当线程竞争更激烈时,偏向锁就会升级为轻量级锁,轻量级锁认为虽然竞争是存在的,但是理想情况下竞争的程度很低,通过自旋方式等待一会儿上一个线程就会释放锁,但是当自旋超过了一定次数,或者一个线程持有锁,一个线程在自旋,又来了第三个线程访问时(反正就是竞争继续加大了),轻量级锁就会膨胀为重量级锁,重量级锁就是Synchronized,重量级锁会使除了此时拥有锁的线程以外的线程都阻塞
JUC(java.util.concurrent)
atomic包下的原子操作类
运用了CAS的AtomicBoolean、AtomicInteger、AtomicReference等原子变量类
locks包下的AbstractQueuedSynchronizer(AQS)类以及使用AQS的ReentantLock(显式锁)、ReentrantReadWriteLock等
AQS(AbstractQueuedSynchronizer类)抽象队列同步器,是java中管理“锁”的抽象类,锁的许多公共方法都是在这个类中实现。
AQS是独占锁(例如,ReentrantLock)和共享锁(例如,Semaphore)的公共父类。
AQS是独占锁(例如,ReentrantLock)和共享锁(例如,Semaphore)的公共父类。
运用了AQS的类还有:Semaphore、CountDownLatch、ReentantLock(显式锁)、ReentrantReadWriteLock
JUC下的一些Executor框架的相关类
线程池的工厂类->Executors 线程池的实现类->ThreadPoolExecutor/ForkJoinPool
JUC下的一些并发容器类
ConcurrentHashMap、CopyOnWriteArrayList
JUC下的一些同步工具类
CountDownLatch(闭锁):一个线程等待,直到他所等待的其他线程都执行完成并且调用countDown()方法发出通知后,当前线程才可以继续执行
CyclicBarrier(栅栏):所有线程都进行等待,直到所有线程都准备好进入await()方法之后,所有线程同时开始执行
Semaphore(信号量):Semaphore就是一个信号量,它的作用是限制某段代码块的并发数
FutureTask:异步获取执行结果
JUC下的一些阻塞队列实现类
ArrayBlockingQueue、LinkedBlockingQueue、PriorityBlockingQueue
线程池
优点:线程可以重复利用,减少在创建和销毁线程上所消耗的时间以及系统资源的开销;提供并发数控制、定时执行、定期执行等功能
JDK实现类
JDK线程池的三种实现类(JUC)
ThreadPoolExecutor (推荐)
更加明确线程池的运行规则,规避资源耗尽的风险
更加明确线程池的运行规则,规避资源耗尽的风险
参数:
corePoolSize - 线程池核心池的大小。
maximumPoolSize - 线程池的最大线程数。
keepAliveTime - 此为超出核心线程数的空闲线程被回收时间。
unit - keepAliveTime 的时间单位。
workQueue - 用来储存等待执行任务的队列。
threadFactory - 线程工厂。
handler - 拒绝策略。
corePoolSize - 线程池核心池的大小。
maximumPoolSize - 线程池的最大线程数。
keepAliveTime - 此为超出核心线程数的空闲线程被回收时间。
unit - keepAliveTime 的时间单位。
workQueue - 用来储存等待执行任务的队列。
threadFactory - 线程工厂。
handler - 拒绝策略。
当线程数小于核心线程数时,创建线程。
当线程数大于等于核心线程数,且任务队列未满时,将任务放入任务队列。
当线程数大于等于核心线程数,且任务队列已满若线程数小于最大线程数,创建线程
当线程数等于最大线程数,调用拒绝执行处理程序(默认效果为:抛出异常,拒绝任务)
当线程数大于等于核心线程数,且任务队列未满时,将任务放入任务队列。
当线程数大于等于核心线程数,且任务队列已满若线程数小于最大线程数,创建线程
当线程数等于最大线程数,调用拒绝执行处理程序(默认效果为:抛出异常,拒绝任务)
ForkJoinPool
ScheduledThreadPoolExecutor
实现方式
JDK的四种线程池实现(不推荐)(JUC)
Executors工厂类(工具类)创建线程池
Executors 返回的线程池对象的弊端如下:
1) FixedThreadPool 和 SingleThreadPool:
允许的请求队列长度为 Integer.MAX_VALUE(2^31-1),可能会堆积大量的请求,从而导致 OOM。
2) CachedThreadPool:
允许的创建线程数量为 Integer.MAX_VALUE,可能会创建大量的线程,从而导致 OOM。
Executors 返回的线程池对象的弊端如下:
1) FixedThreadPool 和 SingleThreadPool:
允许的请求队列长度为 Integer.MAX_VALUE(2^31-1),可能会堆积大量的请求,从而导致 OOM。
2) CachedThreadPool:
允许的创建线程数量为 Integer.MAX_VALUE,可能会创建大量的线程,从而导致 OOM。
newFixedThreadPool 创建一个定长线程池,可控制线程最大并发数。
核心线程数和最大线程数相等,超过核心线程数的空闲线程生存时间是0,队列是LinkedBlockingQueue(Integer.MAX_VALUE)
核心线程数和最大线程数相等,超过核心线程数的空闲线程生存时间是0,队列是LinkedBlockingQueue(Integer.MAX_VALUE)
特点:创建一个定长线程池,可控制线程最大并发数
场景:适合用在稳定且固定的并发场景
newCachedThreadPool创建一个可缓存线程池,线程池最大线程数目为最大整型
核心线程数是0,最大线程数是Integer.MAX_VALUE,空闲线程生存时间是1min,队列是SynchronousQueue
核心线程数是0,最大线程数是Integer.MAX_VALUE,空闲线程生存时间是1min,队列是SynchronousQueue
特点:
①这是一个可以无限扩大的线程池;
②适合处理执行时间比较小的任务;
③线程空闲时间超过60s就会被杀死,所以长时间处于空闲状态的时候,这种线程池几乎不占用资源;
④阻塞队列没有存储空间,只要请求到来,就必须找到一条空闲线程去处理这个请求,找不到则在线程池新开辟一条线程。
①这是一个可以无限扩大的线程池;
②适合处理执行时间比较小的任务;
③线程空闲时间超过60s就会被杀死,所以长时间处于空闲状态的时候,这种线程池几乎不占用资源;
④阻塞队列没有存储空间,只要请求到来,就必须找到一条空闲线程去处理这个请求,找不到则在线程池新开辟一条线程。
注意:使用该线程池时,一定要注意控制并发的任务数,否则创建大量的线程可能导致严重的性能问题
newScheduledThreadPool 创建一个定长线程池,支持定时及周期性任务执行
核心线程数是创建时设置,最大线程数是Integer.MAX_VALUE,空闲线程生存时间是10ms,队列是DelayedWorkQueue
核心线程数是创建时设置,最大线程数是Integer.MAX_VALUE,空闲线程生存时间是10ms,队列是DelayedWorkQueue
特点:
①可以执行延时任务
②也可以执行带有返回值的任务
①可以执行延时任务
②也可以执行带有返回值的任务
newSingleThreadExecutor 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务
核心线程数和最大线程数都是1,队列是LinkedBlockingQueue(Integer.MAX_VALUE)
核心线程数和最大线程数都是1,队列是LinkedBlockingQueue(Integer.MAX_VALUE)
保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行,所以这个比较适合那些需要按序执行任务的场景
其他线程池实现(推荐)
推荐方式1:spring线程池:ThreadPoolTaskExecutor(线程池参数均可配置)
推荐方式2:commons-lang3包
推荐方式 3:com.google.guava包
常见问题
多线程下 i++和++i 操作如何保证线程安全
使用JUC包下的Atomic原子操作类实现(CAS)
使用锁机制,ReentrantLock实现
使用synchronized关键字,同步代码块的方式实现
线程和进程的概念,常见的守护线程有哪些
守护线程,专门用于服务其他的线程,如果其他的线程(即用户自定义线程)都执行完毕,连main线程也执行完毕,那么jvm就会退出。
应用程序里的线程,一般都是用户自定义线程。垃圾回收线程就是典型的守护线程
应用程序里的线程,一般都是用户自定义线程。垃圾回收线程就是典型的守护线程
线程状态有哪些,怎么转换,JVM怎么查看
并发与并行的概念,Java实现并发的方式
并发:在一段时间内宏观上有多个程序在同时运行
并行:在同一个时间点有多个程序在同时运行
网络
网络模型
OSI七层模型
应用层
表示层
会话层
传输层
网络层
数据链路层
物理层
TCP/IP四层模型
应用层
DNS解析
传输层
网络层
网络接口层
DNS寻址
协议
TCP/UDP:
1.TCP面向连接,UDP无连接。
2.TCP面向字节流(文件传输),UDP是面向报文的,UDP没有拥塞控制,因此网络出现拥塞不会使源主机的发送速率降低(对IP电话,实时视频会议等)。
3.TCP首部开销20字节,UDP的首部开销小,只有8个字节。
4.TCP提供可靠的服务。也就是说,通过TCP连接传送的数据,无差错,不丢失,不重复,且按序到达;UDP尽最大努力交付,即不保证可靠交付。
5.每一条TCP连接只能是点到点的;UDP支持一对一,一对多,多对一和多对多的交互通信。
6.TCP的逻辑通信信道是全双工的可靠信道,UDP则是不可靠信道
TCP面向连接和字节流,通信通道是全双工的可靠通道,可实现点对点通信,传输速度较慢,效率较低
UDP无连接,面向报文,通信通道不可靠,可实现一对一、一对多和多对多的交互通信,传输速度快,效率高
1.TCP面向连接,UDP无连接。
2.TCP面向字节流(文件传输),UDP是面向报文的,UDP没有拥塞控制,因此网络出现拥塞不会使源主机的发送速率降低(对IP电话,实时视频会议等)。
3.TCP首部开销20字节,UDP的首部开销小,只有8个字节。
4.TCP提供可靠的服务。也就是说,通过TCP连接传送的数据,无差错,不丢失,不重复,且按序到达;UDP尽最大努力交付,即不保证可靠交付。
5.每一条TCP连接只能是点到点的;UDP支持一对一,一对多,多对一和多对多的交互通信。
6.TCP的逻辑通信信道是全双工的可靠信道,UDP则是不可靠信道
TCP面向连接和字节流,通信通道是全双工的可靠通道,可实现点对点通信,传输速度较慢,效率较低
UDP无连接,面向报文,通信通道不可靠,可实现一对一、一对多和多对多的交互通信,传输速度快,效率高
TCP
三次握手:
1、第一次握手(SYN=1, seq=x)
建立连接。客户端发送连接请求报文段,这是报文首部中的同步位SYN=1,同时选择一个初始序列号 seq=x ,此时,客户端进程进入了 SYN-SENT(同步已发送状态)状态。TCP规定,SYN报文段(SYN=1的报文段)不能携带数据,但需要消耗掉一个序号;
2、第二次握手(SYN=1, ACK=1, seq=y, ACKnum=x+1)
服务器收到客户端的SYN报文段,如果同意连接,则发出确认报文。确认报文中应该 ACK=1,SYN=1,确认号ACKnum=x+1,同时,自己还要发送SYN请求信息,SYN=1,为自己初始化一个序列号 seq=y,服务器端将上述所有信息放到一个报文段(即SYN+ACK报文段)中,一并发送给客户端,此时,TCP服务器进程进入了SYN-RCVD(同步收到)状态。这个报文也不能携带数据,但是同样要消耗一个序号
3、第三次握手(ACK=1,ACKnum=y+1)
客户端收到服务器的SYN+ACK报文段,再次发送确认包(ACK),SYN 标志位为0,ACK 标志位为1,确认号 ACKnum = y+1,这个报文段发送完毕以后,客户端和服务器端都进入ESTABLISHED(已建立连接)状态,完成TCP三次握手。
为什么需要三次握手呢?两次不行吗?
为了防止服务器端开启一些无用的连接增加服务器开销以及防止已失效的连接请求报文段突然又传送到了服务端,因而产生错误。
讲一讲SYN超时,洪泛攻击,以及解决策略
什么 SYN 是洪泛攻击? 在 TCP 的三次握手机制的第一步中,客户端会向服务器发送 SYN 报文段。服务器接收到 SYN 报文段后会为该TCP分配缓存和变量,如果攻击者大量地往服务器发送 SYN 报文段,服务器的连接资源终将被耗尽,导致内存溢出无法继续服务。
解决策略:
1、缩短SYN timeout时间,即从服务器收到一个SYN报文到确认这个SYN报文无效这个过程所花费的时间。
2、设置SYN Cookie,在TCP服务器收到TCP SYN包并返回TCP SYN+ACK包时,不分配一个专门的数据区,而是根据这个SYN包计算出一个cookie值。在收到TCP ACK包时,TCP服务器再根据那个cookie值检查这个TCP ACK包的合法性。如果合法,再分配专门的数据区进行处理未来的TCP连接
1、第一次握手(SYN=1, seq=x)
建立连接。客户端发送连接请求报文段,这是报文首部中的同步位SYN=1,同时选择一个初始序列号 seq=x ,此时,客户端进程进入了 SYN-SENT(同步已发送状态)状态。TCP规定,SYN报文段(SYN=1的报文段)不能携带数据,但需要消耗掉一个序号;
2、第二次握手(SYN=1, ACK=1, seq=y, ACKnum=x+1)
服务器收到客户端的SYN报文段,如果同意连接,则发出确认报文。确认报文中应该 ACK=1,SYN=1,确认号ACKnum=x+1,同时,自己还要发送SYN请求信息,SYN=1,为自己初始化一个序列号 seq=y,服务器端将上述所有信息放到一个报文段(即SYN+ACK报文段)中,一并发送给客户端,此时,TCP服务器进程进入了SYN-RCVD(同步收到)状态。这个报文也不能携带数据,但是同样要消耗一个序号
3、第三次握手(ACK=1,ACKnum=y+1)
客户端收到服务器的SYN+ACK报文段,再次发送确认包(ACK),SYN 标志位为0,ACK 标志位为1,确认号 ACKnum = y+1,这个报文段发送完毕以后,客户端和服务器端都进入ESTABLISHED(已建立连接)状态,完成TCP三次握手。
为什么需要三次握手呢?两次不行吗?
为了防止服务器端开启一些无用的连接增加服务器开销以及防止已失效的连接请求报文段突然又传送到了服务端,因而产生错误。
讲一讲SYN超时,洪泛攻击,以及解决策略
什么 SYN 是洪泛攻击? 在 TCP 的三次握手机制的第一步中,客户端会向服务器发送 SYN 报文段。服务器接收到 SYN 报文段后会为该TCP分配缓存和变量,如果攻击者大量地往服务器发送 SYN 报文段,服务器的连接资源终将被耗尽,导致内存溢出无法继续服务。
解决策略:
1、缩短SYN timeout时间,即从服务器收到一个SYN报文到确认这个SYN报文无效这个过程所花费的时间。
2、设置SYN Cookie,在TCP服务器收到TCP SYN包并返回TCP SYN+ACK包时,不分配一个专门的数据区,而是根据这个SYN包计算出一个cookie值。在收到TCP ACK包时,TCP服务器再根据那个cookie值检查这个TCP ACK包的合法性。如果合法,再分配专门的数据区进行处理未来的TCP连接
四次挥手:
1、第一次挥手(FIN=1,seq=x)
主机1(可以使客户端,也可以是服务器端),设置seq=x,向主机2发送一个FIN报文段;此时,主机1进入FIN_WAIT_1状态;这表示主机1没有数据要发送给主机2了;
2、第二次挥手(ACK=1,ACKnum=x+1)
主机2收到了主机1发送的FIN报文段,向主机1回一个ACK报文段,Acknnum=x+1,主机1进入FIN_WAIT_2状态;主机2告诉主机1,我“同意”你的关闭请求;
3、第三次挥手(FIN=1,seq=y)
主机2向主机1发送FIN报文段,请求关闭连接,同时主机2进入LAST_ACK 状态
4、第四次挥手(ACK=1,ACKnum=y+1)
主机1收到主机2发送的FIN报文段,向主机2发送ACK报文段,然后主机1进入TIME_WAIT状态;主机2收到主机1的ACK报文段以后,就关闭连接;此时,主机1等待2MSL(两个最大段生命周期,2MSL,2 Maximum Segment Lifetime)后依然没有收到回复,则证明Server端已正常关闭,那好,主机1也可以关闭连接了,进入 CLOSED 状态。
为什么连接的时候是三次握手,关闭的时候却是四次握手?
因为当Server端收到Client端的SYN连接请求报文后,可以直接发送SYN+ACK报文。其中ACK报文是用来应答的,SYN报文是用来同步的。但是关闭连接时,当Server端收到FIN报文时,很可能并不会立即关闭SOCKET,所以只能先回复一个ACK报文,告诉Client端,"你发的FIN报文我收到了"。只有等到我Server端所有的报文都发送完了,我才能发送FIN报文,因此不能一起发送。故需要四步握手。
为什么TIME_WAIT状态需要经过2MSL(最大报文段生存时间)才能返回到CLOSE状态?
1.防止发送的ACK丢失,等待着,如果对端重发FIN报文,可以重发ACK
2.防止“已失效的连接请求报文段”出现在连接中
服务端time_wait过多的处理办法?可以通过优化服务器参数得到解决
net.ipv4.tcp_tw_reuse=1
#表示开启重用。允许将TIME-WAIT sockets重新用于新的TCP连接,默认为0,表示关闭;
net.ipv4.tcp_tw_recycle=1
#表示开启TCP连接中TIME-WAIT sockets的快速回收,默认为0,表示关闭。
net.ipv4.tcp_timestamps=1
#表示开启TCP连接中TIME-WAIT sockets的快速回收,默认为0,表示关闭。
服务器出现了大量CLOSE_WAIT状态如何解决
大量 CLOSE_WAIT 表示程序出现了问题,对方的 socket 已经关闭连接,而我方忙于读或写没有及时关闭连接,需要检查代码,特别是释放资源的代码,或者是处理请求的线程配置。
1、第一次挥手(FIN=1,seq=x)
主机1(可以使客户端,也可以是服务器端),设置seq=x,向主机2发送一个FIN报文段;此时,主机1进入FIN_WAIT_1状态;这表示主机1没有数据要发送给主机2了;
2、第二次挥手(ACK=1,ACKnum=x+1)
主机2收到了主机1发送的FIN报文段,向主机1回一个ACK报文段,Acknnum=x+1,主机1进入FIN_WAIT_2状态;主机2告诉主机1,我“同意”你的关闭请求;
3、第三次挥手(FIN=1,seq=y)
主机2向主机1发送FIN报文段,请求关闭连接,同时主机2进入LAST_ACK 状态
4、第四次挥手(ACK=1,ACKnum=y+1)
主机1收到主机2发送的FIN报文段,向主机2发送ACK报文段,然后主机1进入TIME_WAIT状态;主机2收到主机1的ACK报文段以后,就关闭连接;此时,主机1等待2MSL(两个最大段生命周期,2MSL,2 Maximum Segment Lifetime)后依然没有收到回复,则证明Server端已正常关闭,那好,主机1也可以关闭连接了,进入 CLOSED 状态。
为什么连接的时候是三次握手,关闭的时候却是四次握手?
因为当Server端收到Client端的SYN连接请求报文后,可以直接发送SYN+ACK报文。其中ACK报文是用来应答的,SYN报文是用来同步的。但是关闭连接时,当Server端收到FIN报文时,很可能并不会立即关闭SOCKET,所以只能先回复一个ACK报文,告诉Client端,"你发的FIN报文我收到了"。只有等到我Server端所有的报文都发送完了,我才能发送FIN报文,因此不能一起发送。故需要四步握手。
为什么TIME_WAIT状态需要经过2MSL(最大报文段生存时间)才能返回到CLOSE状态?
1.防止发送的ACK丢失,等待着,如果对端重发FIN报文,可以重发ACK
2.防止“已失效的连接请求报文段”出现在连接中
服务端time_wait过多的处理办法?可以通过优化服务器参数得到解决
net.ipv4.tcp_tw_reuse=1
#表示开启重用。允许将TIME-WAIT sockets重新用于新的TCP连接,默认为0,表示关闭;
net.ipv4.tcp_tw_recycle=1
#表示开启TCP连接中TIME-WAIT sockets的快速回收,默认为0,表示关闭。
net.ipv4.tcp_timestamps=1
#表示开启TCP连接中TIME-WAIT sockets的快速回收,默认为0,表示关闭。
服务器出现了大量CLOSE_WAIT状态如何解决
大量 CLOSE_WAIT 表示程序出现了问题,对方的 socket 已经关闭连接,而我方忙于读或写没有及时关闭连接,需要检查代码,特别是释放资源的代码,或者是处理请求的线程配置。
如何做到可靠传输
数据包校验:目的是检测数据在传输过程中的变化,若校验出包有错,则丢弃报文段并且不给出响应,这时TCP发送数据端超时后会重发数据
对失序数据包重排序:既然TCP报文段作为IP数据报来传输,而IP数据报的到达可能会失序,因此TCP报文段的到达也可能会失序。TCP将对失序数据进行重新排序,然后才交给应用层
丢弃重复数据:对于重复数据,能够丢弃重复数据
超时重发:当TCP发出一个段后,它启动一个定时器,等待目的端确认收到这个报文段。如果不能及时收到一个确认,将重发这个报文段
流量控制:TCP连接的每一方都有固定大小的缓冲空间。TCP的接收端只允许另一端发送接收端缓冲区所能接纳的数据,这可以防止较快主机致使较慢主机的缓冲区溢出,这就是流量控制。TCP使用的流量控制协议是可变大小的滑动窗口协议
应答机制:当TCP收到发自TCP连接另一端的数据,它将发送一个确认。这个确认不是立即发送,通常将推迟几分之一秒
报文结构
TCP粘包
什么是TCP粘包问题?
TCP粘包就是指发送方发送的若干包数据到达接收方时粘成了一包,从接收缓冲区来看,后一包数据的头紧接着前一包数据的尾
造成TCP粘包的原因
发送方原因:TCP默认使用Nagle算法(主要作用:减少网络中报文段的数量),而Nagle算法主要做两件事:
(1)只有上一个分组得到确认,才会发送下一个分组
(2)收集多个小分组,在一个确认到来时一起发送
(1)只有上一个分组得到确认,才会发送下一个分组
(2)收集多个小分组,在一个确认到来时一起发送
接收方原因:TCP接收到数据包时,并不会马上交到应用层进行处理,或者说应用层并不会立即处理。实际上,TCP将接收到的数据包保存在接收缓存里,然后应用程序主动从缓存读取收到的分组。这样一来,如果TCP接收数据包到缓存的速度大于应用程序从缓存中读取数据包的速度,多个包就会被缓存,应用程序就有可能读取到多个首尾相接粘到一起的包。
什么时候需要处理粘包现象?
(1)如果发送方发送的多组数据本来就是同一块数据的不同部分,比如说一个文件被分成多个部分发送,这时当然不需要处理粘包现象
(2)如果多个分组毫不相干,甚至是并列关系,那么这个时候就一定要处理粘包现象了
(2)如果多个分组毫不相干,甚至是并列关系,那么这个时候就一定要处理粘包现象了
如何处理粘包现象?
发送方:对于发送方造成的粘包问题,可以通过关闭Nagle算法来解决,使用TCP_NODELAY选项来关闭算法
应用层:应用层的解决办法简单可行,不仅能解决接收方的粘包问题,还可以解决发送方的粘包问题。
解决办法:循环处理,应用程序从接收缓存中读取分组时,读完一条数据,就应该循环读取下一条数据,
直到所有数据都被处理完成,但是如何判断每条数据的长度呢?
解决办法:循环处理,应用程序从接收缓存中读取分组时,读完一条数据,就应该循环读取下一条数据,
直到所有数据都被处理完成,但是如何判断每条数据的长度呢?
格式化数据:每条数据有固定的格式(开始符,结束符),这种方法简单易行,但是选择开始符和结束符时一定要确保每条数据的内部不包含开始符和结束符
发送长度:发送每条数据时,将数据的长度一并发送,例如规定数据的前4位是数据的长度,应用层在处理时可以根据长度来判断每个分组的开始和结束位置
UDP会不会产生粘包问题呢?
不会,UDP面向报文段传输,数据有边界,接收方一次只接受一条独立的信息
UDP
如何实现可靠传输
传输层无法保证数据的可靠传输,只能通过应用层来实现了。实现的方式可以参照tcp可靠性传输的方式,只是实现不在传输层,实现转移到了应用层。
实现应答机制、重传机制、数据包排序机制等。
实现应答机制、重传机制、数据包排序机制等。
目前有如下开源程序利用udp实现了可靠的数据传输。分别为RUDP、RTP、UDT
报文结构
HTTP/HTTPS
HTTP
版本差异
HTTP1.0和HTTP1.1的区别
长连接:HTTP 1.0需要使用keep-alive参数来告知服务器端要建立一个长连接,而HTTP1.1默认支持长连接
HOST域:HTTP1.0是没有host域的,HTTP1.1才支持这个参数,host域可以给web servers设置虚拟站点
节约带宽:HTTP 1.1支持只发送header信息(不带任何body信息)
HTTP1.1和HTTP2.0的区别
多路复用:HTTP2.0的多路复用允许单一的 TCP连接同时发起多重的HTTP请求-响应消息,且多个请求可以被并行处理,而HTTP1.1的长连接虽然可以通过管道技术在一个TCP连接中发起多个HTTP请求-响应消息,但多个请求只能被串行处理,且存在HTTP队头阻塞问题
原因分析:因为HTTP1.1的数据基于文本,只能按顺序传输,所以多个请求只能被串行处理,如果一个请求响应延迟就会导致其他请求延迟,造成HTTP队头阻塞问题;HTTP2.0的传输是基于二进制帧的。每一个TCP连接中承载了多个双向流通的流,每一个流都有一个独一无二的标识和优先级,而流就是由二进制帧组成的。二进制帧的头部信息会标识自己属于哪一个流,所以这些帧是可以交错传输,然后在接收端通过帧头的信息组装成完整的数据。这样就解决了队头阻塞的问题,同时也提高了网络速度的利用率。
数据压缩:HTTP1.1不支持header数据的压缩,HTTP2.0使用HPACK算法对header的数据进行压缩,这样数据体积小了,在网络上传输就会更快
服务器推送:HTTP2.0引入了server push,它允许服务端推送资源给浏览器,在浏览器明确地请求之前
HTTP3.0又称为HTTP Over QUIC,其弃用TCP协议,改为使用基于UDP协议的QUIC协议来实现
状态码
200 OK:请求成功
300系列
301 Moved Permanently:永久性重定向
302 Found:临时性重定向
400系列
400 Bad Request:表示请求报文中存在语法错误
401 Unauthorized:未认证
403 Forbidden:权限不足,禁止访问
404 Not Found:页面无法访问
500系统
500 Inter Server Error:服务器内部错误
503 Server Unavailable:请求未完成,服务器临时过载或宕机
504 Gateway timeout:网关超时
头部参数
请求头
accept:表示当前浏览器可以接受的文件类型
accept-encoding:表示当前浏览器可以接受的数据编码
accept-language:表示当前使用的浏览语言
Cookie:很多和用户相关的信息都存在 Cookie 里,用户在向服务器发送请求数据时会带上
user-agent: user-agent
响应头
content-encoding:示返回内容的压缩编码类型
content-length:表示这次回包的数据大小,如果数据大小不匹配,要当作异常处理
content-type:表示数据的格式
set-cookie:服务器通知浏览器设置一个 Cookie
请求方式
GET、POST、PUT、DELETE、HEAD、CONNECT、OPTIONS、TRACE
HTTPS协议流
TLS协议
HTTP风险
窃听风险
篡改风险
冒充风险
TLS解决
信息加密
校验机制
身份证书
TLS握手过程
IO模型
BIO:一个连接一个线程,客户端有连接请求时服务器端就需要启动一个线程进行处理。线程开销大。
NIO:一个请求一个线程,但客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有 I/O 请求时才启动一个线程进行处理。
IO多路复用:单线程或单进程同时监测若干个文件描述符是否可以执行IO操作的能力
IO多路复用:单线程或单进程同时监测若干个文件描述符是否可以执行IO操作的能力
AIO:一个有效请求一个线程,客户端的 I/O 请求都是由 OS 先完成了再通知服务器应用去启动线程进行处理
缓存
Redis
Redis 是 C 语言开发的一个开源的(遵从 BSD 协议)高性能键值对(key-value)的内存数据库,
可以用作数据库、缓存、消息中间件等。
可以用作数据库、缓存、消息中间件等。
基于内存实现:数据在内存中,减少了一些不必要的I/O操作,读写速度快,性能比较优秀
高效的数据结构:Redis提供的多种数据类型采用了不同的数据结构,使得数据存储效率得到提升
合理的线程模型: IO 多路复用模型可以同时监听多个客户端的连接;单线程在执行过程中不需要进行上下文切换,减少了耗时
合理的数据编码:根据字符串的长度及元素的个数适配不同的编码格式
单线程模型:指的是执行 Redis 命令的核心模块(文件事件处理器)是单线程的
文件事件处理器是基于Reactor模式开发的,对于大量的Socket请求,Redis使用I/O多路复用程序同时监听多个套接字,
并将这些事件推送到队列里,由文件事件分派器选择对应的事件处理器进行处理,然后将结果返回给客户端
并将这些事件推送到队列里,由文件事件分派器选择对应的事件处理器进行处理,然后将结果返回给客户端
多个Socket
IO多路复用(又被称为“事件驱动”)
基于epoll实现
基于队列的文件事件分派器(单线程的工作方式)
事件处理器
连接应答处理器
命令请求处理器
命令回复处理器
优点
避免了不必要的上下文切换和竞争条件
不存在多进程或者多线程导致的切换而消耗 CPU
不用去考虑各种锁的问题,不存在加锁释放锁操作,没有因为可能出现死锁而导致的性能消耗
缺点
耗时的命令会导致并发的下降,不只是读并发,写并发也会下降
无法发挥多核CPU性能,不过可以通过在单机开多个Redis实例来完善
Redis不存在线程安全问题?
Redis采用了线程封闭的方式,把任务封闭在一个线程,自然避免了线程安全问题,不过对于需要依赖多个redis操作(即:多个Redis操作命令)的复合操作来说,依然需要锁,而且有可能是分布式锁。
数据类型
字符串(String)
数据结构:动态字符串SDS(Simple Dynamic String)
①常数时间内获得字符串长度
SDS 中 len 字段保存着字符串的长度,所以总能在常数时间内获取字符串长度,复杂度是 O(1)
②避免缓冲区溢出
SDS 的 API 对字符串修改时首先会检查空间是否足够,若不充足则会分配新空间,避免了缓冲区溢出问题
③减少字符串修改时带来的内存重新分配的次数
SDS 实现了空间预分配和惰性空间释放两种优化策略
④二进制安全
在 C 中遇到 '' 则表示字符串的结束,但在 SDS 中,标志字符串结束的是 len 属性
数据编码:存储数字的话,采用int类型的编码,如果是非数字的话,采用 raw 编码
应用:分布式锁、全局唯一ID、计数器、单值缓存、JSON对象缓存
列表(List)
数据结构
双向链表
(1)前后节点
(2)头尾节点
(3)链表长度
压缩列表(ziplist)
它是经过特殊编码,专门为了提升内存使用效率设计的。所有的操作都是通过指针与解码出来的偏移量进行的。
并且压缩列表的内存是连续分配的,遍历的速度很快
并且压缩列表的内存是连续分配的,遍历的速度很快
数据编码:字符串长度及元素个数小于一定范围使用 ziplist 编码,任意条件不满足,则转化为 linkedlist 编码
应用:消息队列、微信-订阅号消息
散列(Hash)
数据结构
哈希表(字典)
压缩列表(ziplist)
数据编码:Hash 对象保存的键值对内的键和值字符串长度小于一定值及键值对使用 ziplist 编码,任意条件不满足,则转化为 hashcode 编码
应用:对象类型存储
集合(Set)
数据结构
哈希表(字典)
数据编码:保存元素为整数及元素个数小于一定范围使用 intset 编码,任意条件不满足,则使用 hashtable 编码
应用:去重
有序集合(Zset)
数据结构
跳跃表(skiplist)
压缩列表(ziplist)
数据编码:zset 对象中保存的元素个数小于及成员长度小于一定值使用 ziplist 编码,任意条件不满足,则使用 skiplist 编码
应用:排行榜
持久化方式
RDB:将内存中的数据以快照的方式定时写入到二进制dump.rdb文件中。(bgsave)持久化时,Redis 会 fork 一个子进程,子进程将数据写到磁盘上一个临时 RDB 文件中。当子进程完成写临时文件后,将原来的 RDB 替换掉,这样的好处是可以实现写时拷贝,缺点的话是可能会造成丢失数据
优点
RDB文件紧凑,全量备份,非常适合用于进行备份和灾难恢复
RDB 在恢复大数据集时的速度比 AOF 的恢复速度要快
缺点
RDB快照持久化期间修改的数据不会被保存,可能丢失数据
AOF:以独立日志(appendonly.aof)的方式记录每次写命令, 重启时再重新执行AOF文件中的命令达到恢复数据的目的。Redis会将每一个收到的写命令都通过write函数追加到AOF文件中
优点
备份机制更稳健,丢失数据概率更低
可读的日志文本,通过操作AOF文件处理误操作
缺点
(1)比起RDB占用更多的磁盘空间
(2)恢复备份速度要慢
(3)每次读写都同步的话,有一定的性能压力
(2)恢复备份速度要慢
(3)每次读写都同步的话,有一定的性能压力
分布式锁(代码实现)
定义:保证同一时间只能有一个客户端对共享资源进行操作
要求
不能发生死锁
成功获取锁后, 执行流程时异常结束, 没有执行释放锁操作, 这样就会产生死锁
获得锁的线程在执行中, 服务被强制停止或服务器宕机, 锁依然不会得到释放
加锁和解锁必须是同一个客户端,客户端自己不能把别人加的锁给解了
线程1执行时间大于锁过期时间导致分布式锁超时释放
线程2获取超时释放的锁后,又被线程1释放
容错性:只要大部分的Redis节点正常运行,客户端就可以加锁和解锁
实现
Redis
官方推荐的客户端: Redission
Zookeeper
数据库
部署架构
单机模式
内存瓶颈,数据量大时一台节点的内存无法存储所有的数据
IO瓶颈,客户端数量较多时,同时处理的客户端数量有限,且是单线程处理,无法同时处理太多的客户端请求
可用性不高,一旦单节点宕机,就会导致整个redis不可用
主从模式
主从复制原理
1、主节点启动,提供正常的读写服务
2、从节点启动,和主节点建立TCP连接
3、从节点向主节点发生psync命令表示需要同步数据
4、主节点接收到psync之后执行bgsave命令生成RDB内存快照文件
5、主节点的RDB文件生成完成之后通过TCP连接发送给从节点,并且将之后的写命令存入缓存区
6、从节点接收到RDB文件之后开始写入本地内存中
7、主节点将缓冲区的写命令发生给从节点,从节点执行写命令(此时主从节点数据保持了一致)
8、主节点在执行写命令时会将写命令同步给从节点
2、从节点启动,和主节点建立TCP连接
3、从节点向主节点发生psync命令表示需要同步数据
4、主节点接收到psync之后执行bgsave命令生成RDB内存快照文件
5、主节点的RDB文件生成完成之后通过TCP连接发送给从节点,并且将之后的写命令存入缓存区
6、从节点接收到RDB文件之后开始写入本地内存中
7、主节点将缓冲区的写命令发生给从节点,从节点执行写命令(此时主从节点数据保持了一致)
8、主节点在执行写命令时会将写命令同步给从节点
优点
多个Redis数据库之间的数据同步
读写分离,读的压力分散到从节点,主节点宕机从节点仍然可以提供读服务
缺点
主节点依然承受所有的写压力
每个节点的数据都一模一样,可存储数据的总量和单机一样,依然没有解决单节点的数据容量问题
主机宕机,宕机前的未同步到slave节点的数据会丢失
主机宕机,需要手动配置新的master节点
哨兵模式
哨兵作用:Sentinel(哨兵)用于监控redis集群中节点的运行状态,通过监控master和slave节点的状态,并通过故障转移的方式保证了redis集群的高可用性。
哨兵功能
监控:哨兵会不断地检查你的主服务器和从服务器是否运作正常
提醒:当被监控的某个 Redis出现问题时, 哨兵可以通过 API 向管理员或者其他应用程序发送通知
自动故障迁移:当一个主服务器不能正常工作时,哨兵会开始一次自动故障迁移操作,它会将失效主服务器的其中一个Slave升级为新的主服务器, 并让失效主服务器的其他从服务器改为复制新的主服务器。
故障转移
1、从客观下线的master节点的所有slave节点中挑选一个新的Master节点
2、向所有slave节点发送新的Master节点的信息,所有Slave节点更新Master节点信息
3、下线的Master节点变成新Master节点的Slave节点,恢复正常时会变成Slave节点
2、向所有slave节点发送新的Master节点的信息,所有Slave节点更新Master节点信息
3、下线的Master节点变成新Master节点的Slave节点,恢复正常时会变成Slave节点
哨兵领导者选举
Redis新主库选举
1、配置项slave-priority最高的从库得分最高
2、和原主库数据同步进度最接近的从库得分最高
3、从库id最小的得分最高
集群模式
原理:集群模式最大的区别就在于master节点不止一个,而是可以有N个,每个master节点再分配N个slave节点,这样就相当于集群模式是有N个主从模式构成的。每个master节点存储的数据不一样,这样就间接的解决了单机master节点的存储容量问题,和达不到高可用的问题。集群模式下可以有多个master节点,存储容量相当于翻倍,另外某一个master节点宕机不可用,其他master节点仍然继续可以提供工作,使得整个集群仍然处于高可用状态。
与SpringBoot整合
直接通过 RedisTemplate 来使用
使用 Spring Cache 集成 Redis(也就是注解的方式)
@Cachable
@CachePut
@CacheEvict
常见问题
缓存和数据库数据一致性问题
存在问题(数据更新时)
先删缓存,在写数据库:如果删除了缓存Redis,还没有来得及写库MySQL,另一个线程就来读取,发现缓存为空,则去数据库中读取数据写入缓存,此时缓存中为脏数据
先写数据库,再删缓存:如果先写了库,在删除缓存前,写库的线程宕机了,没有删除掉缓存,则也会出现数据不一致情况
解决方案
采用延时双删策略:在写库前后都进行redis.del(key)操作,并且设定合理的超时时间
先删除缓存;
再写数据库;
休眠500毫秒(需要评估自己的项目的读数据业务逻辑的耗时,确保读请求结束,写请求可以删除读请求造成的缓存脏数据);
再次删除缓存。
再写数据库;
休眠500毫秒(需要评估自己的项目的读数据业务逻辑的耗时,确保读请求结束,写请求可以删除读请求造成的缓存脏数据);
再次删除缓存。
缺点:结合双删策略+缓存超时设置,这样最差的情况就是在超时时间内数据存在不一致,而且又增加了写请求的耗时
异步更新缓存(基于订阅binlog的同步机制):MySQL binlog增量订阅消费+消息队列+增量数据更新到redis
读Redis:热数据基本都在Redis
写MySQL:增删改都是操作MySQL
更新Redis数据:MySQL binlog增量订阅消费+消息队列+增量数据更新到redis(类似MySQL的主从备份机制)
写MySQL:增删改都是操作MySQL
更新Redis数据:MySQL binlog增量订阅消费+消息队列+增量数据更新到redis(类似MySQL的主从备份机制)
缓存击穿、穿透、崩溃
缓存穿透:查询一个根本不存在的数据,缓存层和持久层都不会命中,会导致数据库压力过大,严重会击垮数据库
缓存空对象
布隆过滤器
缓存击穿:热点Key数据失效,大并发的请求可能会瞬间把后端DB压垮
限流降级,在缓存失效后,通过加锁,对某个key只允许一个线程查询数据和写缓存,其他线程等待(分布式锁)
设置热点数据永远不过期
缓存雪崩:缓存层由于某些原因不可用(宕机)或者大量缓存由于超时时间相同在同一时间段失效(大批key失效/热点数据失效)
Redis高可用,利用sentinel或cluster实现
限流降级,在缓存失效后,通过加锁,对某个key只允许一个线程查询数据和写缓存,其他线程等待
数据预热,在即将发生大并发访问前手动触发加载缓存不同的key,设置不同的过期时间,让缓存失效的时间点尽量均匀
LRU缓存淘汰策略(代码实现):内存满了就优先删除那些很久没用过的数据
消息队列
功能
异步、削峰、解耦
Kafka
RocketMQ
设计模式
单例模式
1)饿汉式单例模式的写法:线程安全
2)懒汉式单例模式的写法:非线程安全
3)双检锁单例模式的写法:线程安全
2)懒汉式单例模式的写法:非线程安全
3)双检锁单例模式的写法:线程安全
子主题
子主题
MySQl数据库
索引
索引是什么?
索引是一种特殊的文件(InnoDB数据表上的索引是表空间的一个组成部分),它们包含着对数据库表里所有记录的引用指针。
索引也是一种数据结构。数据库索引,是数据库管理系统中一个排序的数据结构,以协助快速查找、更新数据表中的数据。
索引也是一种数据结构。数据库索引,是数据库管理系统中一个排序的数据结构,以协助快速查找、更新数据表中的数据。
优点:提高数据的检索速度
缺点:创建索引和维护索引需要耗费空间和时间,对数据进行增、删、改的时候,索引也要动态维护,会降低执行效率;索引需要占据物理空间
索引类型
数据结构维度
B+Tree索引(平衡查找树,非二叉树):非叶子节点是索引不保存数据,数据都在叶子节点,并增加了顺序访问指针,每个叶子节点都指向相邻的叶子节点的地址。叶子节点的这种链表结构更有利于提高范围查找的效率
优点
B+树的磁盘读写代价更低:
(1)非叶子节点不存储数据,能存储更多索引,一次性读入内存的索引越多,相对IO读写次数就降低了;
(2)B+树的高度低,IO代价低
(1)非叶子节点不存储数据,能存储更多索引,一次性读入内存的索引越多,相对IO读写次数就降低了;
(2)B+树的高度低,IO代价低
B+树更适合区间查询
Hash索引:基于哈希表实现,只有精确匹配索引所有列查询才有效,对于每一行数据,存储引擎都会对所有的索引列计算一个哈希码(hashcode),并且将所有的哈希码存储在索引中,同时在索引表中保存指向每个数据行的指针
特点
可以快速定位,但没有顺序,IO复杂度高
适合等值查询,检索效率高
如果存在大量重复键值的情况下,哈希索引的效率很低,因为存在哈希碰撞问题
全文索引:一般在文本类型
R-Tree索引:用来对GIS数据类型创建spatial索引
逻辑维度(应用层划分)
普通索引:一个索引只包含单个列,一个表可以有多个单列索引
最左前缀原则:最左优先,在创建多列索引时,要根据业务需求,where子句中使用最频繁的一列放在最左边
索引下推,可以减少回表次数
唯一索引:索引列的值必须唯一,允许有空值
联合索引(复合索引):多个列值组成一个索引,专门用于组合搜索,其效率大于索引合并
在联合索引中,如果想要命中索引,需要按照建立索引时的字段顺序挨个使用,否则无法命中索引。
联合索引的建立是按照索引顺序排序的,查询根据索引顺序进行等值查询,一般将查询需求频繁的索引列放在前面。
物理存储维度
聚簇索引(聚集索引):不是一种单独的索引类型,而是一种数据存储方式。在InnoDB里,索引B+树的叶子节点存储了整行数据的是主键索引,也被称为聚簇索引,即将数据存储与索引放在了一块,找到了索引也就找到了数据
非聚簇索引:不是聚簇索引,就是非聚簇索引
覆盖索引(索引包含所有需要查询的字段值)不会要进行回表查询
索引失效情况,原因分析
使用!=或者<>导致索引失效
类型不一致导致索引失效
函数导致的索引失效
运算符(+,-,*,/,! 等)导致的索引失效
OR引起的索引失效
模糊搜索导致索引失效
常见问题
官方建议使用自增长主键作为索引的原因:结合B+树的特点,自增主键是连续的,在插入过程中,可以减少页分裂和数据的移动
建索引的原则:最左前缀匹配原则、=和in可以乱序、尽量选择区分度高的列作为索引、尽量的扩展索引,不要新建索引
使用索引查询一定能提高查询性能吗?:不一定,通常使用索引查询数据比全表扫描要快,但是也必须注意到它的代价,索引需要空间来存储,也需要定期维护,每当表中数据增删或者索引列修改时,索引本身也会被修改,为此需要多付出磁盘IO
数据量增大后为什么性能会下降
阿里巴巴《Java 开发手册》提出单表行数超过 500 万行或者单表容量超过 2G
当单表数据库到达某个量级的上限时,导致内存无法存储其索引,使得之后的 SQL 查询会产生磁盘 IO,从而导致性能下降
存储引擎
InnoDB
B+树
和B树区别
MyISAM
事物
ACID四个特性
原子性(Atomicity)
事物作为一个整体被执行,包含在其中的对数据库的操作要么全部被执行,要么都不执行
一致性(Consistency)
事物应确保数据库的状态从一个一致状态转变为另一个一致性状态。一致状态的含义是数据库中的数据应满足完整性约束
隔离性(Isolation)
多个事物并发执行时,一个事物的执行不影响其他事物的执行
持久性(Durability)
已被提交的事物对数据库的修改应该永久的保存在数据库中
常见概念
脏读:指的是读到了其他事务未提交的数据,未提交意味着这些数据可能会回滚,也就是可能最终不会存到数据库中,也就是不存在的数据。
幻读:是针对数据插入(INSERT)操作来说的。假设事务A对某些行的内容作了更改,但是还未提交,此时事务B插入了与事务A更改前的记录相同的记录行,并且在事务A提交之前先提交了,而这时,在事务A中查询,会发现好像刚刚的更改对于某些数据未起作用,但其实是事务B刚插入进来的,让用户感觉很魔幻,感觉出现了幻觉,这就叫幻读。
可重复读:指的是在一个事务内,最开始读到的数据和事务结束前的任意时刻读到的同一批数据都是一致的。通常针对数据更新(UPDATE)操作。
不可重复读:指的是在同一事务内,不同的时刻读到的同一批数据可能是不一样的,可能会受到其他事务的影响,比如其他事务改了这批数据并提交了。通常针对数据更新(UPDATE)操作。
事务隔离级别(依靠锁来实现的)
读未提交(READ UNCOMMITTED):。任何事务对数据的修改都会第一时间暴露给其他事务,即使事务还没有提交。
原理:无锁,性能是好(没有加锁、解锁带来的性能开销),但是连脏读的问题都没办法解决。
原理:无锁,性能是好(没有加锁、解锁带来的性能开销),但是连脏读的问题都没办法解决。
读提交 (READ COMMITTED):读提交就是一个事务只能读到其他事务已经提交过的数据,也就是其他事务调用 commit 命令之后的数据。那脏数据问题迎刃而解了。这就出现了一个问题,在同一事务中(本例中的事务B),事务的不同时刻同样的查询条件,查询出来的记录内容是不一样的,事务A的提交影响了事务B的查询结果,这就是不可重复读()
原理:读提交解决了脏读问题,行锁解决了并发更新的问题
原理:读提交解决了脏读问题,行锁解决了并发更新的问题
可重复读 (REPEATABLE READ):可重复是对比不可重复而言的,上面说不可重复读是指同一事物不同时刻读到的数据值可能不一致。而可重复读是指,事务不会读到其他事务对已有数据的修改,即使其他事务已提交,也就是说,事务开始时读到的已有数据是什么,在事务提交前的任意时刻,这些数据的值都是一样的。但是,对于其他事务新插入的数据是可以读到的,这也就引发了幻读问题。(解决了幻读问题,是通过行锁和间隙锁的组合 Next-Key 锁实现的)
原理:行锁和间隙锁的组合 Next-Key 锁实现的
原理:行锁和间隙锁的组合 Next-Key 锁实现的
串行化 (SERIALIZABLE):串行化是4种事务隔离级别中隔离效果最好的,解决了脏读、可重复读、幻读的问题,但是性能最差,它将事务的执行变为顺序执行,与其他三个隔离级别相比,它就相当于单线程,后一个事务的执行必须等待前一个事务结束。
原理:读的时候加共享锁,也就是其他事务可以并发读,但是不能写。写的时候加排它锁,其他事务不能并发写也不能并发读。
原理:读的时候加共享锁,也就是其他事务可以并发读,但是不能写。写的时候加排它锁,其他事务不能并发写也不能并发读。
日志
事务日志
Redo log:防止在发生故障的时间点,尚有脏页未写入磁盘,在重启mysql服务的时候,根据redo log进行重做,从而达到事务的未入磁盘数据进行持久化这一特性。RedoLog是为了实现事务的持久性而出现的产物
Undo log:用来回滚行记录到某个版本。事务未提交之前,Undo保存了未提交之前的版本数据,Undo中的数据可作为数据旧版本快照供其他并发事务进行快照读。是为了实现事务的原子性而出现的产物,在Mysql innodb存储引擎中用来实现多版本并发控制
二进制日志(binlog)
主从复制
这里面涉及到三个线程,连接到 master 获取 binlog(记录所有数据库表结构变更(DDL数据定义语言:例如CREATE、ALTER TABLE…)以及表数据修改(DML数据操纵语言:INSERT、UPDATE、DELETE…)的二进制日志),并且解析 binlog 写入中继日 志,这个线程叫做 I/O 线程。Master 节点上有一个 log dump 线程,是用来发送 binlog 给 slave 的。从库的 SQL 线程,是用来读取 relay log,把数据写入到数据库的。
分库分表
如何实现
垂直拆分:根据业务进行拆分,比如可以将一张表中的多个字段拆成两张表,一张是不经常更改的,一张是经常改的
水平拆分:即根据表来进行分割:比如user表可以拆分为user0,、user1、user2、user3、user4等
如何跨表查询
可以使用第三方中间件来实现,比如:mycat、shading-jdbc。当客户端发送一条sql查询:select * from user;此时中间件会根据有几个子表,拆分成多个语句:select * from user1;select * from user2;select * from user3等多条语句查询,然后将查询的结果返回给中间件,然后汇总给客户端。这些语句是并发执行的,所以效率会很高
优化方案
查询性能优化
索引优化
数据类型优化
分布式理论
理论基础
CAP
一致性(C:Consistency):指数据在多个副本之间能够保持一致的特性
可用性(A: Availability):指系统提供的服务必须一直处于可用的状态,每次请求都能获取到非错的响应——但是不保证获取的数据为最新数据
分区容错性(P:Partition tolerance):分布式系统在遇到任何网络分区故障的时候,仍然能够对外提供满足一致性和可用性的服务,除非整个网络环境都发生了故障
BASE理论:BASE是Basically Available(基本可用)、Soft state(软状态)和Eventually consistent(最终一致性)三个短语的缩写。BASE理论是对CAP中一致性和可用性权衡的结果,其来源于对大规模互联网系统分布式实践的总结, 是基于CAP定理逐步演化而来的。BASE理论的核心思想是:即使无法做到强一致性,但每个应用都可以根据自身业务特点,采用适当的方式来使系统达到最终一致性。
一致性协议
分布式UUID
实现方案
分布式锁
实现方案
分布式事物
实现方案
分布式session
实现方案
接口幂等性
微服务系统
注册与发现
服务治理
负责均衡(算法)
服务熔断
监控运维
限流算法
令牌桶
漏斗算法
与SOA架构区别
数据结构和算法
数据结构
数组
链表
队列
栈
堆
树
B+树
红黑树
图
跳跃表
算法
排序算法
二分查找
动态规划
深度遍历
广度遍历
分治算法
回溯算法
布隆过滤器
原理:bit向量(二进制向量)或者bit数组+多个hash函数【最初所有的值均设置为 0,添加时使用多个不同的哈希函数生成多个哈希值,并对每个生成的哈希值指向的 bit 位置 1,查找时若值对应的索引位置存在0,则一定不存在;均为1则可能存在】
Hash函数推荐: MurmurHash、Fnv
如何选择哈希函数个数和布隆过滤器长度
布隆过滤器越长其误报率越小,存储空间占用越大
哈希函数的个数越多则布隆过滤器 bit 位置位 1 的速度越快,且布隆过滤器的效率越低;太少的话,误报率会变高
应用
亿级数据过滤算法(IP、URL...)
解决缓存穿透的问题
网页爬虫对 URL 去重,避免爬取相同的 URL 地址
反垃圾邮件,从数十亿个垃圾邮件列表中判断某邮箱是否垃圾邮箱
Medium 使用布隆过滤器避免推荐给用户已经读过的文章
Google Chrome 使用布隆过滤器识别恶意 URL
优缺点:它的优点是空间效率和查询时间都比一般的算法要好的多,缺点是有一定的误识别率和删除困难
LeetCode
代码实现
分布式锁
LRU(Least Recently Used)缓存淘汰算法
多线程
JVM
JVM模型
类加载过程(类的生命周期)
加载:将class文件加载到内存,并将这些数据转换成方法区中的运行数据(常量池等),在堆中生成一个class类对象代表这个类(反射原理),作为方法区类数据的访问入口
验证:确保加载的类信息符合JVM规范,没有安全方面问题
准备:正式为类变量(static变量)分配内存并设置类变量初始值阶段(初始值为默认值,具体赋值在初始化阶段完成)
解析:虚拟机常量池内的符号引用替换为直接引用(地址引用)的过程
初始化:初始化阶段是执行类构造器<clinit>()方法的过程。类构造器<clinit>()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static块)中的语句合并产生的
1、父类的静态变量
2、父类的静态代码块
3、子类的静态变量
4、子类的静态代码块
5、父类的非静态变量
6、父类的非静态代码块
7、父类的构造方法
8、子类的非静态变量
9、子类的非静态代码块
10、子类的构造方法
2、父类的静态代码块
3、子类的静态变量
4、子类的静态代码块
5、父类的非静态变量
6、父类的非静态代码块
7、父类的构造方法
8、子类的非静态变量
9、子类的非静态代码块
10、子类的构造方法
使用
卸载
类加载机制
双亲委派模型
自定义类加载器
应用:热加载和热部署
自定义类加载器,并重写ClassLoader的findClass方法
Tomcat多个应用程序之间的依赖隔离(打破双亲委派机制)
自定义类加载器,并重写ClassLoader的loadClass方法,移除loadClass方法中双亲委派相关的代码
内存模型
堆中的内存划分
堆大小(该值可以通过参数–Xms、-Xmx 来指定) = 新生代 + 老年代。
默认的,新生代 ( Young ) 与老年代 ( Old ) 的比例的值为 1:2 ( 该值可以通过参数 –XX:NewRatio 来指定 )
默认的,新生代 ( Young ) 与老年代 ( Old ) 的比例的值为 1:2 ( 该值可以通过参数 –XX:NewRatio 来指定 )
新生代 ( Young ) 被细分为 Eden 和 两个 Survivor 区域,这两个 Survivor 区域分别被命名为 from 和 to,以示区分。
默认的,Eden : from : to = 8 : 1 : 1 ( 可以通过参数 –XX:SurvivorRatio 来设定 )
默认的,Eden : from : to = 8 : 1 : 1 ( 可以通过参数 –XX:SurvivorRatio 来设定 )
垃圾回收
检测垃圾算法
引用计数算法
原理:给每个对象添加一引用计数器,每当有一个地方引用它,计数器+1 ,引用失效时就-1
缺点:很难解决对象之间互相引用的问题
可达性分析算法
原理:以GC Roots对象为起始点进行搜索,如果有对象不可达的话,即是垃圾对象。
GC Roots对象
虚拟机栈(栈帧中局部变量表)中引用的对象
方法区中类静态属性引用的对象
方法区中常量引用的对象
本地方法栈中JNI(即一般说的Native方法)引用的对象
对象的四种引用
强引用:创建一个对象并把这个对象直接赋给一个变量,不管系统资源多么紧张,强引用的对象都不会被回收,即使他以后不会再用到。
软引用:通过SoftReference修饰的类,内存非常紧张的时候会被回收,其他时候不会被回收,在使用之前要判断是否为null从而判断他是否已经被回收了
弱引用:通过WeakReference修饰的类,不管内存是否足够,系统垃圾回收时必定会回收
虚引用:不能单独使用,主要是用于追踪对象被垃圾回收的状态。通过PhantomReference修饰和引用队列ReferenceQueue类联合使用实现
垃圾回收算法
标记-清除(Mark-Sweep):GC分为两个阶段,标记和清除。首先标记所有可回收的对象,在标记完成后统一回收所有被标记的对象。同时会产生不连续的内存碎片。碎片过多会导致以后程序运行时需要分配较大对象时,无法找到足够的连续内存,而不得已再次触发GC
复制(Copy):将内存按容量划分为两块,每次只使用其中一块。当这一块内存用完了,就将存活的对象复制到另一块上,然后再把已使用的内存空间一次清理掉。这样使得每次都是对半个内存区回收,也不用考虑内存碎片问题,简单高效。缺点需要两倍的内存空间。
标记-整理(Mark-Compact):也分为两个阶段,首先标记可回收的对象,再将存活的对象都向一端移动,然后清理掉边界以外的内存。此方法避免标记-清除算法的碎片问题,同时也避免了复制算法的空间问题。
分代收集算法:一般年轻代中执行GC后,会有少量的对象存活,就会选用复制算法,只要付出少量的存活对象复制成本就可以完成收集。而老年代中因为对象存活率高,没有额外过多内存空间分配,就需要使用标记-清除或者标记-整理算法来进行回收
HotSpot虚拟机垃圾收集器
GC
新生代GC(Minor GC):指发生在新生代的垃圾回收动作,因为Java对象大多都具备朝生夕灭的特性,所以Minor GC非频繁,一般回收速度也比较快
老年代GC(Major GC或者Full GC):指发生在老年代的GC,经常会伴随至少一次的Minor GC(但非绝对的,在Parallel Scavenge收集器的收集策略里就有直接进行Major GC的策略选择过程)。Major GC的速度一般会比Minor GC慢10倍以上
垃圾收集器
Serial 收集器(老年代采用Serial Old收集器)的运行过程(适合单个CPU的环境,没有线程交互的开销)
新生代:Serial 收集器
垃圾收集算法:复制算法
特点:单线程收集器,只会使用一个CPU或一条收集线程去完成垃圾收集工作,更重要的是它在进行垃圾收集时,必须暂停其他所有的工作线程,直至Serial收集器收集结束为止(“Stop The World”)
老年代:Serial Old收集器
垃圾收集算法:标记-整理(Mark-Compact)算法
特点:同样是一个单线程收集器
ParNew收集器的工作过程(老年代采用Serial Old收集器)
新生代:ParNew收集器
垃圾收集算法:复制算法
特点:Serial收集器的多线程版本
老年代:Serial Old收集器
Parallel Scavenge/Parallel Old收集器配合使用的流程图
新生代:Parallel Scavenge收集器(“吞吐量优先”收集器)
垃圾收集算法:复制算法
特点:多线程收集器,Parallel Scavenge收集器的目标是达到一个可控制的吞吐量(Throughput),具备GC自适应的调节策略(GC Ergonomics)
老年代:Parallel Old收集器
垃圾收集算法:标记-整理算法
特点:Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法
CMS收集器
老年代:CMS收集器
垃圾收集算法:标记-清除算法
工作流程
(1)初始标记(CMS initial mark):仅仅只是标记一下GC Roots能直接关联到的对象,速度很快,需要“Stop The World”。
(2)并发标记(CMS concurrent mark):进行GC Roots Tracing的过程,在整个过程中耗时最长。
(3)重新标记(CMS remark):为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会 比初始标记阶段稍长一些,但远比并发标记的时间短。此阶段也需要“Stop The World”。
(4)并发清除(CMS concurrent sweep)
由于整个过程中耗时最长的并发标记和并发清除过程收集器线程都可以与用户线程一起工作。所以,从总体上来说,CMS收集器的内存回收过程是与用户线程一起并发执行的
(2)并发标记(CMS concurrent mark):进行GC Roots Tracing的过程,在整个过程中耗时最长。
(3)重新标记(CMS remark):为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会 比初始标记阶段稍长一些,但远比并发标记的时间短。此阶段也需要“Stop The World”。
(4)并发清除(CMS concurrent sweep)
由于整个过程中耗时最长的并发标记和并发清除过程收集器线程都可以与用户线程一起工作。所以,从总体上来说,CMS收集器的内存回收过程是与用户线程一起并发执行的
优缺点
优点:CMS是一款优秀的收集器,它的主要优点在名字上已经体现出来了:并发收集、低停顿,因此CMS收集器也被称为并发低停顿收集器(Concurrent Low Pause Collector)
缺点
浮动垃圾(Floating Garbage)
由于CMS并发清理阶段用户线程还在运行着,伴随程序运行自然就还会有新的垃圾不断产生。
这一部分垃圾出现在标记过程之后,CMS无法再当次收集中处理掉它们,只好留待下一次GC时再清理掉。
这一部分垃圾就被称为“浮动垃圾”。
这一部分垃圾出现在标记过程之后,CMS无法再当次收集中处理掉它们,只好留待下一次GC时再清理掉。
这一部分垃圾就被称为“浮动垃圾”。
内存碎片
标记-清除算法导致的空间碎片,收集结束时会有大量空间碎片产生。
空间碎片过多时,将会给大对象分配带来很大麻烦,往往出现老年代空间剩余,但无法找到足够大连续空间来分配当前对象。
空间碎片过多时,将会给大对象分配带来很大麻烦,往往出现老年代空间剩余,但无法找到足够大连续空间来分配当前对象。
特点:以获取最短回收停顿时间为目标的收集器,它非常符合那些集中在互联网站或者B/S系统的服务端上的Java应用,这些应用都非常重视服务的响应速度
G1收集器
新生代和老年代:G1收集器
垃圾收集算法:标记整理和复制算法
原理及工作流程
G1在使用时,Java堆的内存布局与其他收集器有很大区别,它将整个Java堆划分为多个大小相等的独立区域(Region),虽然还保留新生代和老年代的概念,但新生代和老年代不再是物理隔离的了,而都是一部分Region(不需要连续)的集合。
G1把Java堆分为多个Region,就是“化整为零”。但是Region不可能是孤立的,一个对象分配在某个Region中,可以与整个Java堆任意的对象发生引用关系。在做可达性分析确定对象是否存活的时候,需要扫描整个Java堆才能保证准确性,这显然是对GC效率的极大伤害。
为了避免全堆扫描的发生,虚拟机为G1中每个Region维护了一个与之对应的Remembered Set。虚拟机发现程序在对Reference类型的数据进行写操作时,会产生一个Write Barrier暂时中断写操作。
检查Reference引用的对象是否处于不同的Region之中(在分代的例子中就是检查是否老年代中的对象引用了新生代中的对象),如果是,便通过CardTable把相关引用信息记录到被引用对象所属的Region的Remembered Set之中。当进行内存回收时,在GC根节点的枚举范围中加入Remembered Set即可保证不对全堆扫描也不会有遗漏。
如果不计算维护Remembered Set的操作,G1收集器的运作大致可划分为以下几个步骤:
G1把Java堆分为多个Region,就是“化整为零”。但是Region不可能是孤立的,一个对象分配在某个Region中,可以与整个Java堆任意的对象发生引用关系。在做可达性分析确定对象是否存活的时候,需要扫描整个Java堆才能保证准确性,这显然是对GC效率的极大伤害。
为了避免全堆扫描的发生,虚拟机为G1中每个Region维护了一个与之对应的Remembered Set。虚拟机发现程序在对Reference类型的数据进行写操作时,会产生一个Write Barrier暂时中断写操作。
检查Reference引用的对象是否处于不同的Region之中(在分代的例子中就是检查是否老年代中的对象引用了新生代中的对象),如果是,便通过CardTable把相关引用信息记录到被引用对象所属的Region的Remembered Set之中。当进行内存回收时,在GC根节点的枚举范围中加入Remembered Set即可保证不对全堆扫描也不会有遗漏。
如果不计算维护Remembered Set的操作,G1收集器的运作大致可划分为以下几个步骤:
(1)初始标记(Initial Marking)仅仅只是标记一下GC Roots 能直接关联到的对象,并且修改TAMS(Nest Top Mark Start)的值,让下一阶段用户程序并发运行时,能在正确可以的Region中创建对象,此阶段需要停顿线程,但耗时很短。
(2)并发标记(Concurrent Marking) 从GC Root 开始对堆中对象进行可达性分析,找到存活对象,此阶段耗时较长,但可与用户程序并发执行。
(3)最终标记(Final Marking) 为了修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程的Remembered Set Logs里面,最终标记阶段需要把Remembered Set Logs的数据合并到Remembered Set中,这阶段需要停顿线程,但是可并行执行。
(4)筛选回收(Live Data Counting and Evacuation) 首先对各个Region中的回收价值和成本进行排序,根据用户所期望的GC 停顿是时间来制定回收计划。此阶段其实也可以做到与用户程序一起并发执行,但是因为只回收一部分Region,时间是用户可控制的,而且停顿用户线程将大幅度提高收集效率。
(2)并发标记(Concurrent Marking) 从GC Root 开始对堆中对象进行可达性分析,找到存活对象,此阶段耗时较长,但可与用户程序并发执行。
(3)最终标记(Final Marking) 为了修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程的Remembered Set Logs里面,最终标记阶段需要把Remembered Set Logs的数据合并到Remembered Set中,这阶段需要停顿线程,但是可并行执行。
(4)筛选回收(Live Data Counting and Evacuation) 首先对各个Region中的回收价值和成本进行排序,根据用户所期望的GC 停顿是时间来制定回收计划。此阶段其实也可以做到与用户程序一起并发执行,但是因为只回收一部分Region,时间是用户可控制的,而且停顿用户线程将大幅度提高收集效率。
特点
并行与并发 G1 能充分利用多CPU、多核环境下的硬件优势,使用多个CPU来缩短“Stop The World”停顿时间,部分其他收集器原本需要停顿Java线程执行的GC动作,G1收集器仍然可以通过并发的方式让Java程序继续执行
分代收集 与其他收集器一样,分代概念在G1中依然得以保留。
空间整合 G1从整体来看是基于“标记-整理”算法实现的收集器,从局部(两个Region之间)上来看是基于“复制”算法实现的。这意味着G1运行期间不会产生内存空间碎片,收集后能提供规整的可用内存。此特性有利于程序长时间运行,分配大对象时不会因为无法找到连续内存空间而提前触发下一次GC。
可预测的停顿 这是G1相对CMS的一大优势,降低停顿时间是G1和CMS共同的关注点,但G1除了降低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在GC上的时间不得超过N毫秒,这几乎已经是实时Java(RTSJ)的垃圾收集器的特征了
垃圾收集器比较
并行(Parallel):指多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态。并发(Concurrent):指用户线程与垃圾收集线程同时执行(但不一定是并行的,可能会交替执行),用户程序在继续运行。而垃圾收集程序运行在另一个CPU上。
OOM(内存溢出)
原因分析:(1)JVM本身可使用的内存太少(内存溢出)(2)应用使用太多,且没有释放,浪费了内存(内存泄漏)
溢出区域
堆溢出
原因:堆中存在大量对象,并且GC Roots到对象之间存在可达路径导致垃圾回收机制无法清除这些对象,那么在对象数量到达最大堆容量限制后就会产生内存溢出异常
现象:java.lang.OutOfMemoryError:Java heep space
分析:使用内存映像分析工具(JDK JMap或者Idea JProfiler)对Dump出来的堆转储快照进行分析,重点确认内存中的对象是否是必要的,也就是要分清楚到底是出现了内存泄漏(Memory Leak),还是内存溢出(Memory Overflow)
如果是内存泄漏,可进一步通过工具查看泄漏对象到GC Roots的引用链。于是就能找到泄漏对象是通过怎样的路径与GCRoots相关联并导致垃圾收集器无法自动回收他们的。掌握了泄漏对象的类型信息及GC Roots引用链的信息,就可以比较准确地定位出泄漏代码的位置。
如果是内存溢出,换句话说内存中的对象确实必须存活,那就应当检查虚拟机的堆参数(-Xmx与-Xms),与机器物理内存对比看是否还可以调大,从代码上检查是否存在某些对象的生命周期过长、持有时间过长的情况,尝试减少程序运行期的内存消耗。
虚拟机栈和本地方法栈溢出
两种异常
StackOverflowError:如果线程请求的栈深度大于虚拟机所允许的最大深度
栈帧太大(本地变量过多)
OutOfMemoryError:如果虚拟机在扩展栈时无法申请到足够的内存空间
虚拟机栈容量太小
过多线程导致的内存溢出问题
栈内存计算:虚拟机栈和本地方法栈=虚拟机内存-最大堆容量(Xmx)-最大方法区容量(MaxMetaspaceSize)-程序计数器(很小,可忽略 )
分析:在不能减少线程数或者更换64位虚拟机的情况下,就只能通过减少最大堆和减少栈容量来换取更多线程。(“减少内存”的手段解决内存溢出问题)
方法区和运行时常量池溢出
不同版本的JVM HotSpot分析
1.8之前,永久代实现,参数MaxPermSize
1.8及以后,元空间实现,参数MaxMetaspaceSize
现象:java.lang.OutOfMemoryError:MetaSpace
分析:(1)Metaspace 设置小了(2)加载到Metaspace的类太多或太大,导致Metaspace 元空间不够用
CGlib字节码增强
JVM上的动态语言(例如Groovy)
大量JSP或动态产生JSP文件的应用(JSP在第一次运行时需要编译为Java类)
本机直接内存溢出
由于申请直接内存不由虚拟机管理,所以由此导致的 OOM 是不会在 Heap Dump 文件中看出明显的异常。当 OOM 后发现 Dump 文件很小同时程序直接或间接使用了 NIO ,就可以考虑一下这方面的原因
性能优化
如何打印一个线程的堆栈信息
内存过高怎么分析
CPU过高怎么定位
JVM参数及作用
框架
Spring
IOC
AOP
JDK动态代理:
CGLib动态代理:
启动过程
如何解决循环依赖
Spring事物
声明式事务(基于@Transactional注解)
实现原理:AOP
失效原因
自己吞了异常
开发者在代码中手动try...catch了异常,如果没有抛异常,则spring认为程序是正常的。
手动抛了别的异常
开发人员自己捕获了异常,又手动抛出了异常:Exception,事务同样不会回滚。
因为spring事务,默认情况下只会回滚RuntimeException(运行时异常)和Error(错误),对于普通的Exception(非运行时异常),它不会回滚。
因为spring事务,默认情况下只会回滚RuntimeException(运行时异常)和Error(错误),对于普通的Exception(非运行时异常),它不会回滚。
自定义了错误的回滚异常(例如:BusinessException)
因为如果使用默认值,一旦程序抛出了Exception,事务不会回滚,这会出现很大的bug。所以,建议一般情况下,将该参数设置成:Exception或Throwable
方法访问权限不是public
spring事务的实现AbstractFallbackTransactionAttributeSource类的computeTransactionAttribute方法中有个判断,如果目标方法不是public,则TransactionAttribute返回null,即不支持事务
方法用final修饰
因为spring事务底层实现使用了AOP动态代理,通过jdk的动态代理或者cglib,生成了代理类,在代理类中实现了事务功能,如果方法被final修饰,无法重写该方法,也就无法添加事务的功能了
方法内部调用
由于spring的事务实现是因为aop生成代理,这样是直接调用了this对象,所以也不会生成事务。
没有被spring管理
在使用spring事务的时候,对象要被spring进行管理,也就是需要创建bean,一般我们都会加@Controller、@Service、@Component、@Repository等注解,可以自动实现bean实例化和依赖注入的功能。如果忘记加了,也会导致事务的失效
...
编程式事务(手动编写代码实现的事务)
实现原理:TransactionTemplate
优势
相较于@Transactional注解声明式事务,基于TransactionTemplate的编程式事务的优势如下:
1. 避免由于spring aop问题,导致事务失效的问题。
2. 能够更小粒度的控制事务的范围,更直观。
1. 避免由于spring aop问题,导致事务失效的问题。
2. 能够更小粒度的控制事务的范围,更直观。
SpringBoot
自动装配原理
启动过程
SpringSecurity
原理
基于责任链模式的过滤器链
启动配置
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
protected void configure(HttpSecurity httpSecurity) throws Exception {
...
}
...
}
@EnableWebSecurity
@RequiredArgsConstructor
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
protected void configure(HttpSecurity httpSecurity) throws Exception {
...
}
...
}
权限控制方式
表达式控制URL路径权限
// swagger 文档
.antMatchers("/swagger-ui.html").permitAll()
.antMatchers("/*/api-docs").permitAll()
表达式控制方法权限
@PreAuthorize:方法执行前进行权限检查
结合Spring EL表达式(自定义)
@PreAuthorize("@el.check('user:list')")
@PreAuthorize("@el.check('user:add')")
@Service(value = "el")
public class ElPermissionConfig {
public Boolean check(String ...permissions){
// 获取当前用户的所有权限
List<String> elPermissions = SecurityUtils.getCurrentUser().getAuthorities().stream().map(GrantedAuthority::getAuthority).collect(Collectors.toList());
// 判断当前用户的所有权限是否包含接口上定义的权限
return Arrays.stream(permissions).anyMatch(elPermissions::contains);
}
}
过滤器链
@PostAuthorize:方法执行后进行权限检查
@Secured:类似于 @PreAuthorize
使用过滤注解
两个过滤函数 @PreFilter 和 @PostFilter,可以根据给出的条件,自动移除集合中的元素
动态权限
主要通过重写拦截器和决策器来实现
AccessDecisionManager(决策管理器)、AuthenticationManager(认证管理器)、 SecurityMetadataSource(资源与权限的对应关系)
源码分析
过滤器链
SpringDataJPA
数据覆盖问题
场景:jpa 更新是先要查出整个entity,然后整个entity update。这样就会出现数据被覆盖
解决方案
不采用JPA操作数据库,手写SQL语句直接针对指定字段进行更新
将JPA实体映射类拆分成多个,根据不同的操作需求选择不同的JPA实体映射类
采用分布式锁,资产操作前后进行锁定和解锁
将数据表拆分成多个子表,将更新频繁且会造成数据覆盖问题的字段独立出来
事物隔离级别调整为串行化
Netty
设计模式、架构
Tomcat
项目
项目1
核心价值
技术架构
设计模式和业务
职责描述
疑难问题解决
数据最终一致性方案
重试 机制+口令回滚机制
补偿任务机制:上报端定时补偿上报,接收端定时主动获取
异步消息模式(MQ)
接口幂等性设计方案
幂等性
概念:多次调用方法或者接口不会改变业务的状态,可以保证重复调用的结果和单次调用的结果一致
核心思想:通过唯一的业务单号保证幂等
使用幂等的场景(业务场景例如转账、支付、提交订单接口)
前端重复提交
接口超时重试
消息重复消费
解决方案
token 机制实现(没有唯一业务单号)
基于Redis SETNX 命令实现的(有唯一业务单号)
利用数据库唯一索引的特性
系统高可用设计方案
子主题
职业规划
收藏
收藏
0 条评论
下一页