java面试重点(概览篇)
- Lang
- Container
- Concurrency
- IO
- JVM
- Spring
- SpringBoot
- SpringWeb
- SpringData
- SpringSecurity
- SpringCloud
- Mysql
- Redis
- MQ
- Network
- Web
- Distributed
java面试重点(语言篇)
Lang
- 字符串
- 数值
- 对象
Container
- 容器遍历
- 容器排序
- 容器空值
- 容器安全
- 容器原理
- 容器扩容
Concurrency
- 线程基础(Thread)
- 线程变量(ThreadLocal)
- 线程安全(Safe)
- 线程并发控制(Lock)
- 任务(Task)
- 线程池(Executor)
- 线程通信(wait/notify、await/signal)
- 线程协作(Semaphore、CountDownLatch、CyclicBarrier)
IO
- IO体系(流、缓冲区)
- IO模型(同步、异步、阻塞、非阻塞、BIO、NIO、AIO)
- 多路复用(C10K、epoll)
- 并发模型(Reactor、Proactor)
- 设计模式(装饰器、适配器、观察者)
JVM
- JVM主要组成
- JVM核心功能
- JVM运行数据分区
- JVM内存分配策略
- JVM垃圾数据回收(分代、标记、回收算法、回收时机、回收过程、回收器分类、回收器搭配)
- JVM类(类加载机制、双亲委派机制)
- JVM对象(对象生命周期、对象初始化过程、对象内存结构)
- JVM问题排查(CPU占用高、内存OOM和泄露、线程死锁)
- JMM(三大原则、as-if-serial和happens-before、内存屏障)
java面试重点(通用篇)
Spring
Spring
- IOC(注册方法、注入方法、懒加载、循环依赖、Bean的生命周期、IOC容器启动流程)
- AOP(使用场景、通知顺序、失效场景)
- Proxy(静态代理和动态代理、代理的生成时机)
SpringBoot
- 起步依赖
- 自动配置
SpringWeb
- Mvc(流程、Filter和Interceptor、跨域)
- Web(幂等、防重复提交)
SpringData
- Mybatis(分页、缓存)
- Transaction(事务失效场景、事务传播级别)
SpringSecurity
- 单点登录流程(SSO)
- 扫码登录流程
SpringCloud
- SpringCloud组件(降级、熔断、限流)
Mysql
- mysql索引(索引分类、索引原则、索引失效、索引优化、索引结构、B+索引、Hash索引)
- mysql事务(四大特性、MVCC、Lock、undo日志、redo日志)
- mysql问题(并发操作、读写分离、数据丢失)
- mysql性能(性能优化、Explain)
- mysql架构(主从复制、分表分库)
Redis
- redis数据类型
- redis数据结构
- redis过期策略(惰性删除、定期删除)
- redis淘汰策略(不淘汰、淘汰最先过期的、random、lru、lfu)
- redis更新策略(旁路缓存、穿透写入、异步回写)
- redis使用场景(分布式锁、限流器、延时队列)
- redis事务(持久化)
- redis问题(缓存穿透、缓存击穿、缓存雪崩、缓存一致性、数据丢失)
- redis性能(性能优化、为什么很快,单线程、多线程、大key,热key)
- redis架构(主从复制、主从、哨兵、集群、hash槽)
- redis分布式锁(分布式锁、红锁)
MQ
- 消息使用(消息延时、消息过期、消费分组、消费策略、死信队列)
- 消息可靠性(可靠性、Ack、Retry、Qos、持久化)
- 消息问题(消息丢失、消息重复、消息乱序、消息积压、消息溢出)
Network
- TCP和UDP的区别
- TCP之三次握手和四次挥手
- TCP之标志位
- TCP之序列号和确认号
- TCP之数据校验
- TCP之超时重传
- TCP之流量控制(滑动窗口)
- TCP之拥塞控制(拥塞窗口)
Web
- Http和Https
- Cookie和Session
- Http之url(输入URL到显示页面的全过程)
- Http之GET和POST(GET和POST的区别)
- Http之Status(常见的Http状态码)
- Https之握手过程
Distributed
- 分布式id(雪花id + 时钟回拨)
- 分布式锁(redis分布式锁 + redis红锁)
- 分布式事务(2PC、3PC、TCC、SAGA、本地消息表、MQ事务消息、最大努力通知)
java面试难点(语言篇)
Lang
Container
- ArrayList扩容
- ConcurrentHashMap扩容
- ConcurrentHashMap线程安全
- ConcurrentHashMap并发操作
- ConcurrentHashMap并发扩容
Concurrency
- volatile关键字
IO
JVM
- 三色标记算法
java面试难点(通用篇)
Spring
- bean的生命周期
- IOC容器的初始化流程
Mysql
- 性能优化
- 主从复制
Redis
- 性能优化
- 主从复制
MQ
- 可靠性
Network
- Tcp
Web
Distributed
- 分布式id
- 分布式锁
- 分布式事务
java面试总结(语言篇)
Lang
String
String为什么是不可变的
- 字符数组私有且被final修饰,并且没有提供修改的方法,保证不会被外部修改
- 类被final修饰,使得不可被继承,保证不会被子类修改
String为什么要设计成不可变的
- String不可变时可以放到常量池里缓存并重复使用
- String不可变时容器可以缓存String的hashCode并重复使用
- String不可变时可以保证线程安全
String和StringBuilder、StringBuffer的区别
- String是不可变的,StringBuilder、StringBuffer是可变的
- String和StringBuffer是线程安全的,StringBuilder是线程不安全的
Integer
Integer为什么不能使用==进行比较
- ==比较的是两个对象的地址是否相等,即是否是同一个对象
- 用数值字面量赋值给Integer对象时,会进行装箱操作
- 数值字面量小于等于127时会从缓存中取对象,这时候地址相等会返回true
- 数值字面量大于127时会新建对象,这时候地址不相等会返回false
- 所以Integer用==比较时会导致数值相等但结果为false的错误问题
- Integer对象的比较需要用equals方法
Object
Object的基本方法
getClass、hashCode、equals、toString、clone、wait、notify、notifyAll、finalize
equals和==的区别
- equals:逻辑相等,比较对象是否是相同的对象
- ==:引用相等,比较对象是否是同一个对象
注意:数值类对象比较使用 equals 可以避免以下问题
- 用==比较时,值相同但地址不同,带来的逻辑判断错误的问题
- 用==比较时,包装类缓存,带来的逻辑判断错误的问题
- 用==比较时,包装类拆箱,带来的空指针异常问题
Container
Container
List
Array和ArrayList的区别
- Array的占用空间是固定的,ArrayList的占用空间是动态的
- Array支持原始类型,ArrayList只支持对象类型
- Array的元素可以直接使用子类,ArrayList的元素需要使用类型通配符才能使用子类
- Array获取大小是通过length属性,ArrayList获取大小是通过size方法
- Array没有删除元素的方法,ArrayList有删除元素的方法
ArrayList和LinkedList的区别
- 结构:
- ArrayList是基于数组实现的
- LinkedList是基于链表实现的
- 性能:
- ArrayList读取时使用索引直接定位,所以读取(随机访问)较快,写入时需要移动元素,所以写入(插入和删除)较慢
- LinkedList读取时需从头开始遍历,所以读取(随机访问)较慢,写入时不需要移动元素,所以写入(插入和删除)较快
- 空间:
- LinkedList使用Node结构还需要存储前指针和后指针,比ArrayList更占内存
ArrayList和Vector的区别
Collections.synchronizedList和Vector的区别
CopyOnWriteArrayList和Vector的区别
CopyOnWriteArrayList和Collections.synchronizedList的区别
为什么ArrayList实现了RandomAccess接口而LinkedList却没有
Map
HashMap和ConcurrentHashMap的区别
HashMap和Hashtable的区别
Collections.synchronizedMap和Hashtable的区别
ConcurrentHashMap和Hashtable的区别
ConcurrentHashMap和Collections.synchronizedMap的区别
Queue
Queue方法的区别
按照操作分
- add/offer/put:添加尾部元素
- remove/poll/take:移除头部元素
- element/peek:查询头部元素
按照结果分
- add/remove/element:操作元素失败的时候抛出异常
- offer/poll/peek:操作元素失败的时候会返回值
- put/take:操作元素失败的时候会阻塞
ps:put/take是BlockingQueue接口特有的阻塞方法,Queue接口里没有
双端队列的使用场景
- 模拟栈
- 实现消息消费失败时回退的功能
ArrayDeque和LinkedList的区别
- 结构:
- ArrayDeque是基于数组实现的
- LinkedList是基于链表实现的
- 性能:
- ArrayDeque读取时使用索引直接定位,所以读取(随机访问)较快,写入时需要移动元素,所以写入(插入和删除)较慢
- LinkedList读取时需从头开始遍历,所以读取(随机访问)较慢,写入时不需要移动元素,所以写入(插入和删除)较快
- 空间:
- LinkedList使用Node结构还需要存储前指针和后指针,比ArrayDeque更占内存
ArrayBlockingQueue和LinkedBlockingQueue的区别
- 结构:
- ArrayBlockingQueue是基于循环数组实现的,LinkedBlockingQueue基于链表实现的
- ArrayBlockingQueue必须设置边界,LinkedBlockingQueue默认无界,但边界可配
- 性能:
- ArrayBlockingQueue出队和入队使用同一把锁,LinkedBlockingQueue出队和入队使用不同的锁,因此LinkedBlockingQueue的并发支持更高
- 空间:
- ArrayBlockingQueue因为使用循环数组会分配固定的内存,LinkedBlockingQueue按需使用内存,因此ArrayBlockingQueue在滞留元素较少时容易浪费空间
- LinkedBlockingQueue使用Node结构还需要存储指针,比ArrayBlockingQueue更占内存
- LinkedBlockingQueue会频繁的创建和销毁额外的Node对象,对GC存在较大影响
LinkedBlockingQueue和ConcurrentLinkedQueue的区别
- LinkedBlockingQueue是
阻塞队列
- ConcurrentLinkedQueue是
非阻塞队列
SynchronousQueue和TransferQueue的区别
- SynchronousQueue并发控制时使用的是锁,性能相对较低(会被阻塞)
- TransferQueue并发控制时使用的是CAS,性能相对较高(不会阻塞)
PriorityBlockingQueue和DelayQueue的区别
- PriorityBlockingQueue:队列不为空时取元素时可以立即获取
- DelayQueue:队列不为空时获取元素需要等待延时到达后才可以获取
ps:PriorityBlockingQueue和DelayQueue都是基于PriorityQueue实现的,都会在队列为空或者为满时支持阻塞
DelayQueue和DelayedWorkQueue的区别
- DelayQueue是通用的
延时消息队列
(基于PriorityQueue实现的),支持的元素的类型是Delayed
- DelayedWorkQueue是专用的
延时任务队列
(是ScheduledThreadPoolExecutor的内部类),支持的元素类型是RunnableScheduledFuture
Traverse
Iterator和Itreable的区别
- Iterator:迭代器,负责进行遍历
- Itreable:可迭代,负责返回Iterator
Iterator和ListIterator的区别
- Iterator只能向后遍历
- ListIterator还可以向前遍历
如何高效的遍历容器
- List使用fori的遍历方式最高效
- Set和Map使用size + fori + iterator.next的遍历方式效率比foreach更高
List遍历时调用list.remove删除元素会有什么问题
- 迭代器会检查到迭代器的expectedModCount和list的modCount不一致而抛出ConcurrentModificationException异常
List遍历时怎么安全的删除元素
- 使用for遍历时需要从后向前遍历并可以删除当前及当前之后的元素
- 使用迭代器遍历时需要使用迭代器的删除方法
- 任何方式遍历时可以将不需要删除的元素添加到新的列表中去
Sorting
Comparator和Comparable的区别
- Comparator:比较器,负责对任意两个对象进行比较
- Comparable:可比较,负责比较当前对象和其他对象
Nullable
ConcurrentHashMap为什么key和value不能为null
- 如果允许value为null,当get返回null时无法区分value是不存在还是value为null
- 这时候就存在了存在二义性
- 需要用containsKey来判断value是否存在
- 如果value本来是不存在,当其他线程在当前线程的get和containsKey操作之间put了一个key相同的value,会导致当前线程判断value为存在,和实际情况不符而出现了bug
- 如果value本来是存在的,当其他线程在当前线程的get和containsKey操作之间remove了一个key相同的value,会导致当前线程判断value为不存在,和实际情况不符而出现了bug
Theory
List
ArrayList是如何扩容的
- 初始容量:10(可通过initialCapacity设置)
- 负载因子:1
- 扩容时机:存入数据前
- 扩容条件:
size + 1 > capacity
- 扩容计算:
capacity + capacity >> 1
- 扩容迁移:不需要迁移
ArrayList为什么按大约1.5倍扩容
- 小于1.5倍时太小,会导致频繁扩容
- 大于1.5倍时太大,会导致内存浪费
Map
Map
为什么重写equals时要重写hashCode
- 为了避免equals相等而hashCode不相等时导致map可以将两个相等的key存在不同的槽位而违背了key不重复的原则
- 为了避免用另一个和A对象相等的B对象去map中找A时由于hashCode不相等而找不到A的情况
重写equals和hashCode的设计原则
- equals相等,hashCode一定相等
- equals不相等,hashCode不一定不相等
- hashCode相等,equals不一定相等
- hashCode不相等,equals一定不相等
hash冲突如何解决
- 开放定址法:冲突后寻找下一个空位置插入,查找时会顺着冲突位置往下查找
- 线性探测法
- 平方探测法
- 再哈希探测法
- 链地址法:冲突的部分在同一个位置用链表链接起来,查找时会在链表中查找
- 公共溢出区:冲突的部分都放到另一个列表中,找不到时会在溢出表中顺序查找
hash方法为何要进行位移后异或
是为了让hash值更加分散,减少hash冲突
Map的容量大小为什么必须是2的幂次方
- 加快索引计算:计算索引位时可以使用按位与运算代替求余运算来加快运算速度
- 加快索引计算:扩容后重新计算索引位时可以通过hash的第n位来快速确定新位置
- 减少哈希冲突:按位与运算时其中一个数都是1,增大了hash分布范围,减少了hash冲突
HashMap
HashMap的实现原理和核心逻辑
java7和java8的HashMap区别
区别点 | jdk7 | jdk8 |
---|---|---|
存储结构 | 数组 + 链表 | 数组 + 链表 + 红黑树 |
逻辑结构 | Entry | Node |
结点插法 | 头插法 | 尾插法 |
哈希(hash)算法 | 较复杂 | 较简单(冲突了可以树化) |
下标(index)算法 | 较复杂 | 较简单(使用了按位与运算代替求余运算) |
扩容时机 | 存入数据前判断 | 存入数据后判断 |
扩容条件 | 大小超过阈值且存在hash冲突 | 大小超过阈值 或者 链表大小大于8且桶个数小于64 |
扩容迁移 | 所有元素重新求槽位 | 用新容量的最高有效位与hash中的相同位进行异或来快速确定新槽位 |
ps:大小超过阈值 =》
size > threshold
ps:hash冲突 =》
entry != null
java8的HashMap为什么要用红黑树
红黑树是查找树,可以使用二分查找,查找次数比链表少
- 跳表需要数据有序
- HashMap中查找使用的hashcode是散列无序的
- 跳表占用的空间比红黑树多
- 跳表需要维护额外的多层链表,需要占用额外的空间
- 红黑树不需要占用额外的空间
java8的HashMap中的红黑树是排序规则
- 实现了Comparable接口则用Comparable接口排序
- 否则用类的名字的自然顺序排序
- 类名相同的使用对象的hashcode的自然顺序排序
java8的HashMap中链表和红黑树的转换条件
树化:链表大小大于8,且桶个数大于或等于64(小于64时会扩容而不会树化)
链化:链表大小小于或等于6
HashMap是如何扩容的
- 初始容量:16(可通过initialCapacity设置)
- 负载因子:0.75
- 扩容时机:
- java7:存入数据前
- java8:存入数据后
- 扩容条件
- java7:大小超过阈值且存在hash冲突
- java8:大小超过阈值 或者 链表大小大于8且桶个数小于64
- 扩容计算:
- java7:扩容到原来的2倍
- java8:扩容到原来的2倍
- 扩容迁移:
- java7:所有元素重新求槽位
- java8:用新容量的最高有效位与hash中的相同位进行异或来快速确定新槽位
HashMap有哪些并发问题
- 并发更新时导致更新丢失
- 并发扩容时导致链表插入死循环(java8时已通过尾插法解决)
TODO:HashMap插入死循环
HashMap使用时如何保证线程安全
ConcurrentHashMap
ConcurrentHashMap的实现原理和核心逻辑
java7和java8的ConcurrentHashMap区别
区别点 | jdk7 | jdk8 |
---|---|---|
存储结构 | 数组 + 链表 | 数组 + 链表 + 红黑树 |
逻辑结构 | Segment + HashEntry + lock(segment) | Node + cas + synchronized(head) |
结点插法 | 头插法 | 尾插法 |
哈希(hash)算法 | 和HashMap一样 | 和HashMap一样 |
下标(index)算法 | 需要index两次 | 只需要index一次 |
扩容时机 | 和HashMap一样 | 和HashMap一样 |
扩容条件 | 和HashMap一样 | 和HashMap一样 |
扩容迁移 | 单线程扩容迁移 | 多线程扩容迁移 |
大小计算 | 先无锁计算三次,如果结果一样则返回计算结果,否则就会锁住所有的Segment求和 | 通过baseCount和遍历CounterCell数组计算出size |
java7的ConcurrentHashMap的并发度是什么
ConcurrentHashMap的并发度是指ConcurrentHashMap支持同时读写的线程数
java8的ConcurrentHashMap为什么放弃了分段锁
ConcurrentHashMap是如何计算size的
java7通过比较前后两次的所有Segment的size之和,如果结果一样则返回计算结果,如果不一样则继续计算并比较最多计算3次后还不一样,就会锁住所有的Segment求和
java8通过baseCount和遍历CounterCell数组计算出size
ConcurrentHashMap是如何扩容的
整体流程和HashMap一样,并支持多线程扩容和分桶迁移
- 写入和迁移的流程
- 读取头结点并判断是否为null,为null则cas
- 写入时cas的新值是新结点
- 迁移时cas的新值是fwd结点
- 头结点不为null时,判断是否是迁移中(头结点的hash等于MOVED代表迁移中)
- 写入时如果是迁移中就帮助迁移(helpTransfer)
- 迁移时如果是迁移中就什么也不做
- 否则就锁住头结点进行操作
- 读取头结点并判断是否为null,为null则cas
ps:迁移时是按照桶为单位分配给线程进行迁移的
ps:迁移中是指整个表还在迁移中,而不是某个桶还在迁移中
ps:头结点不为null且头结点的hash等于MOVED代表迁移中
- 写入时的迁移操作
- 写入时如果桶为空,由于CAS写入时是原子操作,则迁移操作会在写入完成之后执行
- 写入时如果桶不为空,由于写入时会锁住头结点,则迁移操作获取头结点的锁时会被阻塞
- 迁移时的读操作
- 如果桶的头结点是fwd结点,则表示迁移完成,可以通过fwd结点的nextTable找到新表进行读取
- 如果检测到正在迁移中,由于迁移时是复制结点的引用而不是删除,所以在原表中还可以读到结点
- 迁移时的写操作
- 如果桶的头结点是fwd结点,则表示迁移完成,可以通过fwd结点的nextTable找到新表进行写入
- 如果检测到正在迁移中,则先放弃写入操作并帮助扩容迁移,扩容迁移完后再写入
扩容相关的属性
- table:当前表(旧表)
- nextTable: 新表
- sizeCtl: 表状态
- sizeCtl = 0:表示没有指定初始容量
- sizeCtl > 0:表示初始容量(可以使用阶段)
- sizeCtl = -1, 标记作用,告知其他线程,正在初始化
- sizeCtl = 0.75n , 扩容阈值
- sizeCtl < 0 : 表示有其他线程正在执行扩容or初始化(不能使用阶段)
- sizeCtl = (resizeStamp(n) << RESIZE_STAMP_SHIFT) + 2 : 表示此时只有一个线程在执行扩容
- transferIndex: 正在迁移的桶索引
- ForwardingNode结点: fwd结点,标记此结点的数据已经迁移完毕
ConcurrentHashMap是如何保证线程安全的
cas(槽位值为null) + synchronized(锁链表头部)
ConcurrentHashMap还有并发问题吗
有,单个操作是安全的,复合操作不是安全的
Queue
ArrayBlockingQueue是如何实现阻塞功能的
使用了ReentrantLock的Condition来实现阻塞和唤醒的
- notEmptyCondition:入队时signal,出队时如果为空则await
- notFullCondition:出队时signal,入队时如果为满则await
Concurrency
Thread
线程的创建方式
- 继承Thread,并实现run方法,然后调用start启动
- 实现Runnable,并作为target传递给线程进行启动
- 实现Callable,并用FutureTask包装后作为target传递给线程进行启动
- 使用线程池的execute方法执行Runnable
- 使用线程池的submit方法提交Runnable或者Callable
- 使用CompletableFuture类
线程的状态变化
NEW、READY、RUNNING、WAITING、TIMED_WAITING、BLOCKED、TERMINATED
- NEW(新建):创建线程后进入此状态
- READY(就绪):创建后调用Thread.start或者运行时调用Thread.yield进入此状态
- RUNNING(运行):获取资源(cpu和lock)成功后进入此状态
- WAITING(等待):调用Thread.join、Object.wait、LockSupport.park的非超时方法后进入此状态
- TIMED_WAITING(超时等待):调用Thread.sleep、Thread.join、Object.wait、LockSupport.park的超时方法后进入此状态
- BLOCKED(阻塞):获取lock失败后进入此状态
- TERMINATED(死亡):结束线程后进入此状态
线程的控制方法
- Thread.yield:让出cpu资源并进入
就绪状态
,不释放锁资源
- Thread.sleep:让出cpu资源并进入
等待状态
,不释放锁资源
- Thread.join:让出cpu资源并进入
等待状态
,会释放锁资源
线程的废弃方法
suspend/resume、stop、destroy
- suspend:不会释放锁,会导致死锁问题
- stop:会立即终止线程,导致操作被中断,会导致数据不一致和资源未释放等问题
- destroy:从来都没有被实现过,且已经被废弃了(相当于suspend且没有后续的resume)
线程的相关疑问
线程和进程的区别
- 线程是任务调度和指令执行的最小单元,进程是资源分配的最小单元
- 线程开销小,进程开销大,线程又被称作轻量级进程
- 进程可以包含多个线程,至少包含一个线程
- 同一个进程的线程共享资源,不同进程之间的资源相互独立
- 线程异常如果不处理会导致线程所属的进程挂掉,进程挂掉则不会影响其他进程
Runnable和Callable的区别
- Runnable不能获取返回值
- Callable和Future、FutureTask配合可以获取返回值
Thread的start和run的区别
- start是本地方法,会启动线程
- run是普通方法,不会启动线程
start调用了start0,start0是本地方法
Thread的start能调用多次吗
不能,再次调用时会检测到状态变化后抛出 IllegalThreadStateException
异常
Thread的sleep和yield的区别
- 方法:都是Thread的静态方法
- 锁:都不释放锁
- 状态:sleep进入到等待状态,yield进入到就绪状态
Thread的sleep和join的区别
- 方法:sleep是Thread的静态方法,join是Thread的实例方法
- 锁:sleep不释放锁,join释放锁
- 状态:都是进入到等待状态
Thread的join和wait的区别
- 方法:join是Thread的实例方法,wait是Object的实例方法
- 锁:都会释放锁
- 状态:都是进入到等待状态
ps:join是通过wait来实现的,所以和wait没区别
ThreadLocal
ThreadLocal为什么会内存泄漏
- ThreadLocal是基于ThreadLocalMap实现的
- Thread引用了ThreadLocalMap
- ThreadLocalMap引用了key和value
- key弱引用了ThreadLocal
- 当ThreadLocal不再使用时,即没有强引用时
- 由于key是弱引用,所以ThreadLocal会被回收
- 此时无法通过ThreadLocal访问value,value应该被回收
- 由于Thread通过ThreadLocalMap间接强引用了value,所以要先回收Thread
- 但是如果Thread是线程池时,Thread不能被回收,所以value不能回收
- 此时value既不能访问又不能回收,就造成了内存泄漏
ps:ThreadLocal在get和set时会自动检测哪些key指向null的entry并清除,可以一定程度减轻内存泄漏的影响
ThreadLocalMap的key为什么是弱引用
- 如果ThreadLocalMap的key为强引用,此时Thread就会通过ThreadLocalMap间接强引用了key
- 如果Thread不回收,比如是线程池时,即使ThreadLocal不再使用了
- 由于ThreadLocal还被Thread引用着,所以ThreadLocal会无法回收而导致内存泄漏
ThreadLocalMap的value为什么是强引用
如果value是弱引用,垃圾回收后value指向了null,此时ThreadLocal还活着却获取不到value对象就不符合逻辑
ThreadLocalMap为什么不用Thread做key
- 如果用Thread做key(且ThreadLocalMap是ThreadLocal的实例),就会有多个线程访问map,就需要保证线程安全,复杂性会提高,并且并发性也会降低
- 如果用ThreadLocal做key(且ThreadLocalMap是Thread的实例),那么访问map的线程都是持有map的那一个线程,就不需要保证线程安全,复杂性会降低,并且并发性也会提高
- 如果是用二级map,那就会和ThreadLocalMap做key一样,就会有多个线程访问map,就需要保证线程安全,还需要两次寻址,复杂性会更高,并且并发性也会更低
ThreadLocal为什么要定义成静态变量
定义成实例变量,使用时会频繁创建threadLocal,导致垃圾回收频繁
定义成实例变量,使用时会重复创建value,导致内存浪费
定义成实例变量,threadLocal回收后,导致value内存泄漏
ThreadLocal和局部变量的区别
ThreadLocal是线程(隔离)变量,局部变量是方法(隔离)变量
ps:局部变量不溢出时本质上还是线程(隔离)变量,因为方法变量是线程私有的
Safe
什么是线程安全
当多个线程访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那这个对象就是线程安全的
如何保证线程安全
- 共享
- 不可变
- 不可变对象(Immutable):通过对象不可变机制实现
- 访问控制
- 悲观锁(LOCK):通过Mutex机制实现
- 乐观锁(CAS):通过CAS机制实现
- 读写分离
- 写时复制(COW):通过COW机制实现
- 不可变
- 私有
- 方法局部对象(LocalVariable):通过方法私有机制实现
- 线程隔离对象(ThreadLocal):通过线程私有机制实现
volatile能保证线程安全性吗
- volatile只能保证可见性和有序性,不能保证原子性
- volatile只能保证原子操作(比如读和写操作)的线程安全
- volatile不能保证非原子操作(比如自增和自减操作)的线程安全
ps:volatile不能保证原子性,只能用来修饰哪些已经保证了原子性的操作,比如
flag读写
和cas操作
ps:也可以使用
atomic类
来保证操作的安全 (atomic依赖volatile并使用了cas保证原子性)
volatile的底层实现原理
- 保证可见性:内存屏障
- 保证有序性:内存屏障
单例模式的双检查实现中volatile的作用
- 保证可见性:volatile会保证读最新值,避免双重判断时没有读到最新状态而重复创建对象
- 保证有序性:volatile会禁止指令重排,避免还没初始化完成的对象被提前暴露引用并使用
Lock
锁的作用
通过互斥机制和队列机制将并发操作变为串行操作从而保证并发操作的线程安全
并发操作的安全保障
- 悲观锁(LOCK):适合
低并发
、常规读写
、强一致性
的场景- 乐观锁(CAS ):适合
高并发
、读多写少
、弱一致性
的场景- 写时复制(COW):适合
高并发
、读多写少
、弱一致性
的场景
ps:CAS如果写多的话,竞争激烈时大量的失败导致cpu做了很多无用功从而占用和浪费cpu资源
ps:COW如果写多的话,频繁分配内存时来不及回收会造成内存占用过高
ps:CAS比COW的效率更高,但CAS支持设置操作却不支持插入和删除的操作
并发操作的读写控制
- 悲观锁(LOCK):写写互斥,读写互斥,读读互斥
- 乐观锁(CAS ):写写互斥,读写不互斥,读读不互斥
- 写时复制(COW):写写互斥,读写不互斥,读读不互斥
ps:读写不互斥时会存在弱一致性的问题
并发操作的常见问题
- 读问题
- 并发读:不存在问题
- 写后读:不一致性(脏读、不可重复读、幻读)
- 写问题
- 并发写:插入冲突、更新丢失
- 读后写:写入偏差
- 死锁
锁的分类
锁的类型分类
- 是否锁住资源:悲观锁(锁住资源)、乐观锁(不锁住资源)
- 是否独占资源:排他锁(独占资源)、共享锁(共享资源)
- 是否阻塞线程:同步锁(阻塞)、自旋锁(不阻塞)
ps:读写锁一般是写独占和读共享的,即写写互斥,读写互斥,读读不互斥
锁的实现分类
- 是否锁住资源:悲观锁(synchronized、ReentrantLock)、乐观锁(ReentrantReadWriteLock、StampedLock)
- 是否共享资源:排他锁(synchronized、ReentrantLock)、共享锁(Semaphore)、读写锁(ReentrantReadWriteLock、StampedLock)
- 是否阻塞线程:同步锁(synchronized、ReentrantLock、ReentrantReadWriteLock、StampedLock)、自旋锁(SpinLock)
ps:ReentrantReadWriteLock是
读写互斥
的,而StampedLock是读写不互斥
的
锁的范围分类
- 线程锁
- 进程锁
- 分布式锁
ps:java锁、redis锁、数据库锁
锁的原理
- 悲观锁:Mutex + 阻塞 + 唤醒
- 乐观锁:CAS + 自旋 + 重试
锁的特性
- 是否支持重入:同一个线程是否可以多次获取锁
- 是否支持中断:线程是否可以响应中断请求
- 是否支持公平:线程是否能够公平的处理请求
ps:公平(排队且先进先出)、非公平(先插队,如果失败后再排队)
锁的问题
死锁问题
死锁的形成条件
- 互斥使用:即当资源被一个线程占用时,别的线程不能使用
- 不可抢占:资源请求者不能强制从资源占有者手中抢夺资源,资源只能由占有者主动释放
- 资源保持:当资源请求者在请求其他资源的同时保持对现有资源的占有
- 循环等待:多个线程存在环路的锁依赖关系而永远等待下去(例如T1占有T2需要的资源,T2占有T3需要的资源,T3占有T1需要的资源,这种情况可能会形成一个等待环路)
死锁的解决办法
- 预防死锁
- 破坏不可抢占条件:当请求不到其他资源超时时释放自己持有的资源
- 破坏资源保持条件:申请资源时一次性申请全部所需的资源
- 破坏循环等待条件:给资源分配编号并按照编号顺序进行申请
- 避免死锁
- 银行家算法
- 检测和解除死锁
饥饿问题
饥饿的解决办法
- 分配资源时使用公平的算法
ps:公平(排队且先进先出)、非公平(先插队,如果失败后再排队)
synchronized同步锁
synchronized锁原理
- 锁方法:通过方法的
ACC_SYNCHRONIZED
标识实现 - 锁代码块:通过对象的
monitor
锁和系统的monitorenter
和monitorexit
指令实现
synchronized锁优化
- 锁膨胀
- 锁消除
- 锁粗化
- 自适应自旋锁
synchronized锁膨胀
- 无锁:没有线程获取锁
- 偏向锁:只有一个线程获取锁时进入此状态(通过CAS对象的标记头获取锁)
- 轻量级锁:多个线程获取锁时进入此状态(通过CAS对象的标记头获取锁,获取锁失败的线程需进行自旋)
- 重量级锁:多个线程获取锁并且有线程自旋失败(10次且可配置)时进入此状态(通过操作系统的Mutex机制获取锁)
synchronized锁消除
synchronized锁粗化
synchronized锁自旋
synchronized锁疑问
synchronized和volatile的区别
- synchronized可以修饰类、字段、方法,volatile只能修饰字段
- synchronized保证原子性、可见性、有序性,volatile只保证可见性、有序性
- synchronized会阻塞线程,volatile不会阻塞线程
synchronized和Lock的区别
- 位置:
- synchronized是java关键字,Lock是java类
- 实现:
- synchronized基于操作系统mutex机制实现,Lock基于java的AQS机制实现
- 操作:
- synchronized会自动释放锁,Lock需要手动释放
- 超时:
- synchronized不能设置等待超时时间,Lock可以设置等待超时时间
- 状态:
- synchronized无法判断是否获取了锁,Lock可以判断是否获取了锁
- 特性:
- synchronized支持重入,Lock也支持重入
- synchronized不支持中断,Lock可支持中断(也支持不可中断)
- synchronized不支持公平锁,Lock可支持公平锁(也支持非公平)
synchronized和ReentrantLock为什么默认是非公平的
因为非公平锁在释放后可以省去唤醒某个线程的开销直接让另一个线程获得锁,从而提高整体的效率
ps:但是非公平锁可能会导致饥饿问题
ReentrantLock是如何实现公平和非公平性的
- 公平:线程在竞争锁资源的时候先判断AQS同步队列里面有没有在等待的线程,如果有的话就加入到队列的尾部等待,没有的话就直接获取锁
- 非公平:线程在竞争锁资源的时候先尝试获取锁,失败后再加入到队列的尾部等待
synchronized是如何保证线程安全的
synchronized通过锁的互斥机制保证了原子性,使得同一时间只有一个线程能够操作资源来保证了线程安全,同时通过内存屏障来保证了线程安全中的可见性和有序性
synchronized的底层原理
- 锁方法:通过方法的
ACC_SYNCHRONIZED
标识实现 - 锁代码块:通过对象的
monitor
锁和系统的monitorenter
和monitorexit
指令实现
wait和notify为什么要位于synchronized代码块中
- wait和notify是用来实现线程间通信的,是基于共享变量实现的
- 为了保证共享变量的线程安全,需要用synchronized来对共享变量加锁
ReentrantLock和ReentrantReadWriteLock
lock.lock() 写在 try 代码块内部行吗
不能,如果写在try里面,当lock异常时,finally会执行unlock,unlock的时候检测到线程没有先持有锁会抛出 IllegalMonitorStateException
异常
如何安全的unlock
- 用try-catch包住异常,并且不处理任何异常,不打印日志
- 可以用ReentrantLock对象的isHeldByCurrentThread方法进行判断
ps:并不是所有的Lock实现类都有isHeldByCurrentThread方法,所以可以统一使用try-catch包住异常
CAS
CAS存在的问题
- ABA问题:可以加版本号或者时间戳解决
- 只能保证单个变量操作的原子性:可以合并多个变量为单个对象进行操作
- 竞争激烈时大量的失败导致cpu做了很多无用功从而占用和浪费cpu资源:可以限制重试次数
ps:cas需要配合volatile来实现线程安全,atomic类就是这样实现的
乐观锁的优点和缺点
- 优点:避免冲突时等待造成的耗时
- 缺点:失败后需要重新处理并重试
自旋锁的优点和缺点
- 优点:避免上下文切换带来的耗时
- 缺点:自旋循环时间长的话会占用和浪费cpu资源
AQS
抽象同步器负责通用的逻辑(阻塞和唤醒、入队和出队),具体同步器负责自定义逻辑(加锁和解锁)
AQS的核心对象
- state:资源
- CLH队列:双向链表实现的等待队列(链表结点中包含线程对象)
ps:CLH队列中的某个结点会自旋CAS检查前驱结点的locked状态,自旋失败后则进行阻塞并等待前驱结点唤醒
AQS的重写方法
- isHeldExclusively():该线程是否正在独占资源(只有用到Condition时才需要去实现它)
- tryAcquire(int):独占方式,成功则返回true,失败则返回false
- tryRelease(int):独占方式,成功则返回true,失败则返回false
- tryAcquireShared(int):共享方式,负数表示失败,0表示成功但没有剩余资源,正数表示成功且有剩余资源
- tryReleaseShared(int):共享方式,如果释放后允许唤醒后续等待对象则返回true,否则返回false
Task
FutureTask的状态变化
- NEW(已创建):创建FutureTask之后
- COMPLETING(完成中):设置结果开始时,是一个中间过渡态
- NORMAL(已正常完成):设置结果(正常完成)结束后
- EXCEPTIONAL(已异常完成):设置结果(异常完成)结束后
- CANCELLED(已取消):取消任务后
- INTERRUPTING(中断中):中断开始时,是一个中间过渡态
- INTERRUPTED(已断中):中断结束后
FutureTask为什么支持传递给线程
因为FutureTask实现了RunnableFuture接口,而RunnableFuture接口继承了Runnable接口,所以FutureTask支持传递给线程
Executor
线程池的创建方式
- 使用Executors快速创建
- 使用ExecutorService手动创建
使用Executors快速创建的方式如下
- Executors.newSingleThreadExecutor:队列容量无限大,容易导致内存耗尽引发OOM
- Executors.newFixedThreadPool:队列容量无限大,容易导致内存耗尽引发OOM
- Executors.newCachedThreadPool:线程数量无限大,容易导致内存耗尽引发OOM
- Executors.newScheduledThreadPool:线程数量无限大,容易导致内存耗尽引发OOM
- Executors.newSingleThreadScheduledExecutor:线程数量无限大,容易导致内存耗尽引发OOM
- Executors.newWorkStealingPool:队列容量无限大,容易导致内存耗尽引发OOM
线程池的核心参数
- corePoolSize:核心线程数
- maximumPoolSize:最大线程数
- keepAliveTime:线程保活时间大小
- unit:线程保活时间单位
- workQueue:工作队列
- threadFactory:线程工厂
- handler:任务饱和策略处理器
线程池的提交策略
poolSize < corePoolSize
时增加线程poolSize = corePoolSize
时放入队列- 队列满了之后再增加线程
- 线程达到
maximumPoolSize
后执行RejectedExecutionHandler - 根据RejectedExecutionHandler指定的拒绝策略来处理新的任务
线程池的线程数量
- 如果是CPU密集型应用,则线程池大小设置为CPU核心数+1
- 如果是IO密集型应用,则线程池大小设置为2*CPU核心数+1
线程池的工作队列
- BlockingQueue
- ArrayBlockingQueue
- LinkedBlockingQueue
- SynchronousQueue
- LinkedTransferQueue
- PriorityBlockingQueue
- DelayQueue
- DelayedWorkQueue
- BlockingDeque
- LinkedBlockingDeque
线程池的饱和策略
- AbortPolicy:丢弃任务并且抛出RejectedExecutionException异常(默认策略)
- DiscardPolicy:丢弃任务但不抛出异常
- DiscardOldestPolicy:丢弃最老的(队列头部)任务
- CallerRunsPolicy:在提交的线程中直接执行任务
线程池的提交方式
- 使用submit方法提交(普通任务有返回值时使用)
- 使用execute方法提交(普通任务无返回值时使用)
- 使用schedule方法提交(延时任务和定时任务时使用)
submit和execute的区别
- execute只支持Runnable,submit可以支持Runnable和Callable
- execute没有返回值,submit有返回值
- execute有异常会直接打印,submit在Future.get的时候才会打印异常
线程池的异常捕获
- execute方式提交时
- 在方法内部捕获异常并处理
- 在ThreadFactory里使用Thread.setUncaughtExceptionHandler拦截处理
- 在ThreadPoolExecutor.afterExecute里面处理
- submit方式提交时
- 在方法内部捕获异常并处理
- 调用Future.get的时候捕获异常并处理
ps:线程池里面的某个线程异常了,线程池会移除这个线程并创建一个新的线程
线程池的关闭方式
- shutdown:线程池的状态变为SHUTDOWN,不再接受新任务了,不会终止当前正在运行的任务,还会继续处理队列里剩余的任务
- shutdownNow:线程池的状态变为STOP,不再接受新任务了,会终止当前正在运行的任务,而且不会处理队列里剩余的任务并返回还未处理完成的任务列表
shutdown和shutdownNow的区别
- shutdown和shutdownNow之后都不会接收新任务了
- shutdown不会终止当前正在运行的任务,shutdownNow会终止当前正在运行的任务
- shutdown还会继续处理队列里剩余的任务,shutdownNow不会处理队列里剩余的任务
线程池的状态变化
RUNNING、SHUTDOWN、STOP、TIDYING、TERMINATED
- RUNNING(运行):创建线程池后的初始状态
- SHUTDOWN(关闭):执行shutdown()后,不接收新任务,但会继续处理当前任务和队列里剩余的任务
- STOP(停止):执行shutdownNow()后,不接收新任务,且会中断当前任务和丢弃并返回队列里剩余的任务
- TIDYING(清扫):线程池中的任务队列为空后
- TERMINATED(终结):执行terminated()后
线程池的相关疑问
线程池是如何保证核心线程不被销毁的
线程池里面的线程内部是无限循环,任务执行完后不会结束,而是继续去队列里面获取任务,如果没获取到任务,就会被队列阻塞直到有新的任务可以获取
如何知道线程池里的的任务已经完成了
- 线程池内部
- Runnable的run方法执行完毕时,这个任务就完成了
- Callable的call方法执行完毕时,这个任务就完成了
- 线程池外部
- 通过线程池的isTerminated方法可以用来判断所有的任务是否已经完成了
- 通过线程池返回的Future对象的isDone方法可以用来判断某个任务是否已经完成了
- 可以通过CountDownLatch计数器来等待相关的任务完成
IO
IO体系相关
核心接口
- InputStream
- OutputStream
- Reader
- Writer
操作分类
- 内存
- 文件
- 管道
- 终端
- 缓冲
- 转换
IO模型相关
BIO和NIO的区别
- BIO的操作是阻塞的,NIO的操作是非阻塞的
- BIO是面向流(Stream Oriented)的,NIO是面向缓冲区(Buffer Oriented)的
- BIO一次只能操作一个数据源,NIO一次可以操作多个数据源
Stream和Channel的区别
- Stream是单向的(只能读或者写),Channel是双向的(既能读又能写)
IO多路复用
C10K和epoll
- select
- 文件描述符个数最多只能有1024个,数量少
- 监听事件时每次都需要将所有的文件描述符拷贝到内核,开销大
- 查阅事件时每次都需要遍历所有的文件描述符,开销大
- poll
- 解决了文件描述符个数最多只能有1024个的限制
- epoll
- 解决了文件描述符个数最多只能有1024个的限制
- 监听事件时不需要将所有的文件描述符拷贝到内核
- 查阅事件时不需要遍历所有的文件描述符
IO并发模型
Reactor和Proactor
- Reactor:同步非阻塞IO
- Proactor:异步非阻塞IO
IO设计模式
- 装饰器模式:FilterInputStream和FilterOutputStream
- 适配器模式:InputStreamReader和OutputStreamWriter
- 观察者模式:WatchService和Watchable
JVM
JVM主要组成
- 运行时数据区(即JVM内存区域)
- 类加载子系统
- 执行引擎
- 本地库接口
- 本地方法库
JVM核心功能
- 执行指令:解释和执行字节码
- 管理内存:分配和回收内存块
JVM运行数据分区
- 线程共享
- 方法区:包含常量池和类的元数据(放在永久代或者元空间中)
- 堆区:包含对象的数据
- 线程私有
- 虚拟机栈:包含局部变量和对象的引用
- 本地方法栈:
- 程序计数器:
JVM内存分配策略
- 堆上分配
- 栈上分配
- TLAB上分配
ps:TLAB(Thread-local allocation buffer):线程私有的内存区域
JVM垃圾数据回收
垃圾分代技术
- 新生代(Young Generation):属于内存中的
堆区
,包含1个Eden
区和2个Survivor
区 - 老年代(Old/Tenured Generation):属于内存中的
堆区
- 永久代(Permanent Generation):属于内存中的
方法区
ps:如果只有一个Survivor区,Survivor区会存在内存碎片
ps:如果没有Survivor区,垃圾对象会直接进入老年代,而老年代的回收效率很低
ps:java8中永久代已经被元空间(Metaspace)取代了
永久代的缺点
- 动态生成的类及方法的信息等比较难确定其大小,容易超出永久代的限制而引发OOM
- 永久代的垃圾回收效率低下
- 永久代的字符串常量池无法回收
垃圾标记算法
对象存活判断方法
- 引用计数算法:不能解决循环引用的问题
- 可达性分析算法:可以解决循环引用的问题
对象存活标记算法
- 三色标记法(标记对象是否活着)
并发标记步骤
- 初始标记
- 并发标记
- 重新标记
- 并发清除
并发标记问题
- 多标:某个
灰色对象A
的所有引用被断开,因为A已经被标记为灰色了,所以标记完成后A会被标记为黑色且不可以回收
,然而此时A因引用被断开已变为不可达,造成了A应该回收却不会回收的问题 - 漏标:某个
白色对象A
的所有引用被断开,因为A的所有引用都被断开了,所以标记完成后A会被标记为白色且可以回收
,但是此时有一个被标记为黑色的对象B
重新引用了A,因为B为黑色代表已经标记完成,所以不会通过B重新标记A,然而此时A因被重新引用已变为可达,造成了A不该回收却被回收了的错误
ps:多标会造成应该回收的不被回收,问题不大,可以在下一次进行回收
ps:漏标会造成不该回收的被回收了,属于bug,jvm通过重新标记解决了
并发漏标解决
漏标形成条件
- 至少一个有黑色对象在自己被标记之后引用了白色对象A
- 所有的灰色对象在自己引用扫描完成之前删除了对白色对象A的引用
漏标解决方案
- 增量更新算法(IU:Incremental Update):破坏漏标形成条件1,将白色对象标记为灰色并放到Mod-Union Table中,以便重新标记阶段能够重新扫描这个对象
- 原始快照算法(SATB:Snapshot At The Beginning):破坏漏标形成条件2,在白色对象的所有引用被删除前将白色对象标记放到线程私有的SATB Queue并汇总到全局的SATB Queue中,以便重新标记阶段能够重新扫描这个对象
ps:SATB比IU算法更快,因为IU需要重新扫描,SATB则是用空间换时间
ps:CMS用的是IU算法,G1用的是SATB算法
垃圾回收算法
- 标记清除算法(MarkSweep):清除对应的内存块,有内存碎片
- 标记复制算法(MarkCopy):复制到另一个区域,适合新生代使用
- 标记整理算法(MarkCompact):移动到区域的一端,适合老年代使用
ps:新生代存活对象少,复制开销小,移动开销大,所以适合用标记复制算法
ps:老年代存活对象多,复制开销大,移动开销小,所以适合用标记整理算法
垃圾回收时机
- Minor GC:回收新生代的垃圾
- Major GC:回收老年代的垃圾
- Full GC:回收新生代、老年代和永久代(方法区)的垃圾
Minor GC的触发条件
- Eden区空间不足时会触发
ps:Survivor区空间不足不会触发Minor GC,而是将对象直接迁移到老年代
Major GC的触发条件
- 老年代空间不足时触发
ps:Major GC执行时通常会先执行一次着Minor GC
Full GC的触发条件
- 调用System.gc时,建议系统执行Full GC,但是不一定要执行
- 老年代空间不足
- 方法区空间不足
垃圾回收过程
- 新创建的对象会放到Eden区
- Eden区满了会触发Minor GC的触发条件
- 经过扫描和标记后,将Eden区和S0区中的存活对象的年龄加1并进行移动
- 如果对象的年龄超限了(默认为15),则将对象移动到老年代
- 如果S1区满了或者剩余空间不足,则将对象移动到老年代
- 如果S1区没有满并且剩余空间充足,则将对象移动到S1区,并调换S0和S1区
垃圾回收器分类
- 新生代
- Serial:
串行回收器
,使用标记复制算法
- PN(ParNew):
并行回收器
,使用标记复制算法
,Serial的多线程版本
- PS(Parallel Scavenge):
并行回收器
,使用标记复制算法
,追求最大吞吐量
- Serial:
- 老年代
- Serial Old:
串行回收器
,使用标记整理算法
,Serial的老年代版本
- CMS(Concurrent Mark Sweep):
并发回收器
,使用标记清除算法
,追求最短停顿时间
- Parallel Old:
并行回收器
,使用标记整理算法
,Parallel Scavenge的老年代版本
- Serial Old:
ps:
吞吐量
是指用户线程时间除以总时间(用户线程时间和GC线程时间之和)的比值,因为总时间固定,则GC线程时间越少,用户线程时间越多,吞吐量越高
ps:停顿时间
是指用户线程被暂停(STW)的时间
ps:Parallel Scavenge
是通过增大内存空间
从而减少垃圾回收次数
的方式来提高吞吐量
的
ps:Concurrent Mark Sweep
是通过并发标记
的方式来减少停顿时间
的
ps:前台交互型
应用追求的是最短停顿时间
,比如Web服务
应用
ps:后台计算型
应用追求的是最大吞吐量
,比如大数据计算
应用
新生代垃圾回收器对比
- Serial:单线程标记,单线程回收,全程STW
- PN(ParNew):多线程标记,多线程回收,全程STW,Serial的
多线程版本
- PS(Parallel Scavenge):多线程标记,多线程回收,全程STW,追求
最大吞吐量
老生代垃圾回收器对比
- Serial Old:单线程标记,单线程回收,全程STW
- CMS(Concurrent Mark Sweep):初始标记时单线程,并发标记时多线程,重新标记时单线程,并发清除时多线程,初始标记和重新标记时会STW,并发标记和并发清除时不会STW,追求
最短停顿时间
- Parallel Old:多线程标记,多线程回收,全程STW,Parallel Scavenge的
老年代版本
主要垃圾回收器的特点
- Parallel Scavenge:吞吐量优先垃圾回收器
- 优点
- 相比于ParNew支持控制停顿时间和吞吐量
- 相比于ParNew多了自适应调节策略
- 缺点
- 相比于ParNew需要消耗更多的系统资源
- 优点
- CMS:低延迟垃圾回收器
- 优点
- 相比于Parallel Scavenge的停顿时间更短
- 缺点
- 相比于Parallel Scavenge的吞吐量更低
- 并发标记时多标的问题会产生浮动垃圾
- 使用的标记清除回收算法会导致内存碎片
- 优点
- G1:分区垃圾回收器
- 优点
- 相比于CMS使用了分区和标记整理算法解决了内存碎片的问题
- 相比于CMS可以更好的控制停顿时间
- 相比于CMS可以处理更大的内存堆
- 缺点
- 相比于CMS需要消耗更多的系统资源
- 优点
垃圾回收器搭配
- Serial Old可以和Serial、PN(ParNew)、PS(Parallel Scavenge)搭配
- CMS(Concurrent Mark Sweep)可以和Serial、PN(ParNew)搭配
- Parallel Old只能和PS(Parallel Scavenge)搭配
JVM类
类加载机制
类加载器分类
- 自举加载器:Bootstrap ClassLoader,负责加载
JAVA_HOME/jre/lib
目录中的jar包(rt.jar) - 扩展加载器:Extension ClassLoader,负责加载
JAVA_HOME/jre/lib/ext
目录中的jar包 - 应用加载器:Application ClassLoader,负责加载
classpath
目录中的jar包 - 自定义加载器:Custom ClassLoader,可以自己定义加载逻辑
类加载过程
- 加载(load):将类文件的数据加载到内存中
- 链接(link):
- 验证(verify):检查类文件数据是否符合规范和要求
- 准备(prepare):为类成员分配内存和设置默认值
- 解析(resolve):将符号引用替换为地址引用
- 初始化(initiate):静态变量初始化和静态代码块初始化
- 使用(use):使用
- 卸载(unload):将类文件的数据从内存中卸载
双亲委派机制
双亲委派机制的作用
- 保证系统类先加载,避免攻击者替换系统类进行破坏活动
- 避免重复加载,前面的加载器加载后自己就不需要加载了
为什么有时候要打破双亲委派机制
- jdbc的DriverManager是由自举加载器加载的,而第三方厂商的DriverManager的实现类则是由应用加载器加载的,由于应用程序类加载器加载的类对启动类加载器是不可见的,导致找不到DriverManager的实现类,此时就需要打破双亲委派机制
- web容器中可以存放多个应用,而这些应用可能会使用版本不同的同名类,而双亲委派机制只会加载第一个同名类,所以就需要对应用的类进行隔离,此时就需要打破双亲委派机制
如何打破双亲委派机制
- 重写ClassLoader的loadClass自定义加载过程
- 利用Thread.currentThread().getContextClassLoader()返回的ClassLoader加载
- 利用java的SPI机制
ps:重写findClass方法可以自定义类加载过程,但不能打破双亲委派机制
JVM对象
对象生命周期
- 实例化(Instantiation)
- 初始化(Initialization)
- 构造(Constrction)
- 使用(Usage)
- 析构(Destruction)
ps:
实例化(instantiation)
和初始化(initialization)
不是同一个概念,实例化会分配内存
和设置默认值
,初始化会设置初始值
ps:
默认值(default)
和初始值(initial)
不是同一个概念,比如int的默认值
为0
,初始值
可以为100
对象初始化过程
- 父类静态变量和代码块
- 子类静态变量和代码块
- 父类非静态变量和代码块
- 父类构造函数
- 子类非静态变量和代码块
- 子类构造函数
对象内存结构
一个空Object对象占多大的空间
- 在开启了压缩指针的情况下,Object 默认会占用 12 个字节,但是为了避免伪共享问题,JVM 会按照 8 个字节的倍数进行填充,所以会填充 4 个字节变成 16 个字节长度。
- 在关闭了压缩指针的情况下,Object 默认会占用 16 个字节,16 个字节正好是 8 的整数倍,因此不需要填充
JVM问题排查
jvm常见问题
- OOM
- cpu占用高
- 死锁
ps:内存溢出(Out Of Memory)和内存泄漏(Memory Leak)不是一个概念
jvm问题排查工具
jmap:排查内存问题(OOM)
jstack:排查线程问题(CPU负载高和死锁)
- 命令行工具
- jps:java进程查看工具
- jmap:java内存诊断工具
- jstack:java线程诊断工具
- jstat:JVM运行状态查看工具
- jinfo:JVM参数查看和调整工具
- jcmd:JVM综合诊断工具
- jhsdb:JVM综合诊断工具
- jhat:java内存分析工具(网页版)
- 可视化工具
- jconsole:java自带工具(老版)
- jvisualvm:java自带工具(新版)
- jprofile:商业工具(可以在idea中使用)
ps:jhat不常用(因为分析工作比较耗性能,一般不在应用服务器上用jhat分析,而是在其他机器上用jvisualvm分析)
jvm问题排查过程
- OOM问题排查
- 查看内存的统计信息:
jmap -histo pid
,可以看出当前哪个对象最消耗内存 - 查看内存的详细信息:
jmap -heap pid
,可以查看内存的配置和使用情况 - 导出内存的详细信息:
jmap -dump:file=a.dump pid
,然后导入到jvisualvm
中进行分析
- 查看内存的统计信息:
- CPU负载高问题排查
- 找出cpu占用飚高进程:
top
,然后按P
键后按照CPU负载排序并找导致cpu占用飚高的进程id - 找出cpu占用飚高线程:
top -H -p pid
,找出导致cpu占用飚高的线程id - 将线程id转化为十六进制:使用python的
hex
函数或者其他支持进制转换工具 - 查看cpu占用飚高线程的堆栈信息:
jstack pid | grep -A 10 tid(十六进制的线程id)
- 堆栈信息中会显示线程停留在代码的哪一行
- 找出cpu占用飚高进程:
- 死锁问题排查
- 用jstack查看是否有deadlock字样
- 用jvisualvm查看是否有死锁
ps:pid(进程id),tid(线程id)
TODO:cpu占用高
TODO:内存泄露和内存溢出
TODO:死锁
JMM
JMM内存模型的作用:定义了线程和主内存之间的操作和关系
- 三大特性是基本原则
- as-if-serial和happens-before是指导思想
- 内存屏障是实现手段
TODO:as-if-serial
TODO:happens-before
TODO:内存屏障和volatile
线程安全的三大原则
- 原子性:通过互斥机制实现,来解决资源的互斥使用问题
- 可见性:通过内存屏障实现,解决数据的不一致性问题
- 有序性:通过内存屏障实现,解决指令的重排序问题
happens-before的8大原则
as-if-serial:单线程原则,无论如何重排序,程序执行的结果都应该与代码顺序执行的结果一致
happens-before:多线程原则,在发生操作B之前,操作A产生的影响都能被操作B观察到
happens-before的8大原则
- 单线程happen-before原则:在同一个线程中,书写在前面的操作happen-before后面的操作
- lock的happen-before原则:同一个锁的unlock操作happen-before此锁的lock操作
- volatile的happen-before原则: 对一个volatile变量的写操作happen-before对此变量的任意操作
- 线程启动的happen-before原则:同一个线程的start方法happen-before此线程的其它方法
- 线程中断的happen-before原则:对线程interrupt方法的调用happen-before被中断线程的检测到中断的代码
- 线程终结的happen-before原则:线程中的所有操作都happen-before线程的终止检测
- 对象创建的happen-before原则:一个对象的初始化完成先于他的finalize方法调用
- happen-before的传递性原则: 如果A操作 happen-before B操作,B操作happen-before C操作,那么A操作happen-before C操作
JMM的8种原子操作
JMM的8种原子操作
- lock(锁定):
- 对象:作用于
主内存
- 作用:对变量进行
加锁
- 对象:作用于
- unlock(解锁):
- 对象:作用于
主内存
- 作用:对变量进行
解锁
- 对象:作用于
- read(读取):
- 对象:作用于
主内存
- 作用:将变量的值从
主内存
传输到工作内存
中
- 对象:作用于
- load(载入):
- 对象:作用于
工作内存
- 作用:将变量的值
加载
到工作内存
中的变量副本中
- 对象:作用于
- use(使用):
- 对象:作用于
工作内存
- 作用:执行引擎
使用
变量的值
- 对象:作用于
- assign(赋值):
- 对象:作用于
工作内存
- 作用:执行引擎
设置
变量的值
- 对象:作用于
- store(存储):
- 对象:作用于
工作内存
- 作用:将变量的值从
工作内存
传输到主内存
中
- 对象:作用于
- write(写入):
- 对象:作用于
主内存
- 作用:将变量的值
写入
到主内存
中的变量中
- 对象:作用于
JMM的8种操作要求
- 操作要完整
- read和load操作必须同时出现且必须顺序执行(不是连续执行),store和write同理
- 即不允许变量从主存读取时工作内存却不接受或者从工作内存写入时主存却不接受的情况
- 变更必须同步
- 不允许线程丢弃它的最近的assign操作
- 即变量在工作内存中改变了就必须同步回主内存
- 未变更不允许同步
- 不允许线程无原因地(比如没有发生过assign操作)将数据从工作内存同步回主内存中
- 即变量在工作内存中未改变就不能同步回主内存
- 不能使用未初始化的变量
- 不允许在工作内存中直接使用一个未被初始化(load或assign)的变量
- 即对一个变量实施use和store操作之前必须先执行过了load和assign操作
- lock需要支持可重入
- 一个变量在同一个时刻只允许一条线程对其执行lock操作,但lock操作可以被同一个条线程重复执行多次,多次执行lock后,只有执行相同次数的unlock操作,变量才会被解锁
- lock时需要将工作内存中的变量副本清空
- 如果对一个变量执行lock操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量前,需要重新执行load或assign操作初始化变量的值
- unlock之前得先执行过lock
- 如果一个变量实现没有被lock操作锁定,则不允许对它执行unlock操作,也不允许去unlock一个被其他线程锁定的变量
- unlock时需要将工作内存中的变量副本同步回主内存中
- 对一个变量执行unlock操作之前,必须先把此变量同步回主内存(执行store和write操作)
java面试总结(通用篇)
Spring
Spring
TODO:Spring原理
TODO:Spring设计模式
IOC
DI
Spring的bean对象的三种注册方式
三种注册方式
- 基于xml文件:ApplicationContext类 + applicationContext.xml
- 基于java注解:@Component、@Controller、@Service、@Repository
- 基于java配置类:@Configuration + @Bean、@Configuration + @Import
- 使用ImportSelector进行注册
- 使用ImportBeanDefinitionRegistrar进行注册
- 使用FactoryBean进行注册
ps:@Configuration上有@Component,所以使用组件扫描(@ComponentScan)时会自动注册到容器
ps:@Configuration可以用来注册第三方的bean
Spring的bean依赖的三种注入方式
- field注入:字段注入(不推荐)
- setter注入:setter注入
- constructor注入:构造器注入
####### field注入的问题
- 破坏对象的隐私和封装特性
- 可能会导致循环依赖(Spring不能解决非单例bean的循环依赖)
- 无法对static变量进行注入(因为static变量属于类不属于对象)
- 无法对final变量进行注入(因为final变量只能在声明或者构造的时候赋值)
- 手动创建的对象的依赖不会注入,使用时会导致空指针错误
####### setter注入的问题
- 手动创建的对象的依赖不会注入,使用时会导致空指针错误
Spring自动装配
@Autowired的装配查找逻辑如下
- 先按照byType的方式进行匹配
- 如果匹配了多个,则又按照byName的方式进行匹配
- 如果没匹配到,则抛出异常
@Resource的装配查找逻辑如下
- 如果同时指定了name和type,name和type必须同时匹配
- 如果只指定了name,只会按照byName的方式进行匹配
- 如果只指定了type,只会按照byType的方式进行匹配
- 否则先按照byName的方式进行匹配,找不到时再按byType的方式进行匹配
- 如果匹配了多个或者没匹配到,则抛出异常
ps:byName的方式不会找到多个,因为name(bean id)在容器中必须是唯一的
Spring懒加载
懒加载实现的关键对象
- ProxyFactory
- TargetSource
ps:Lazy是通过代理实现的,调用方法时实际上调用的是代理对象的代理方法,代理方法会调用时会通过
targetSource.getTarget
触发beanFactory.doResolveDependency
的调用来完成目标对象的创建和缓存
Spring循环依赖
循环依赖解决的关键对象
- Map
- ObjectFactory
ps:单例对象创建后会缓存在Map中
####### 循环依赖和注入方式
单例bean
时字段注入
和setter注入
可以通过三级缓存解决循环依赖单例bean
时构造器注入
不能解决循环依赖,因为会导致构造函数的循环调用非单例bean
时任何注入方式都不能解决循环依赖,因为非单例bean不会进行缓存
####### 循环依赖和解决办法
构造器注入
可以改成setter注入
或者字段注入
- 使用
@Lazy延迟加载
####### 循环依赖和三级缓存
1 | /** Cache of singleton objects: bean name --> bean instance */ |
- 第三级缓存:存放bean对象的创建工厂(ObjectFactory)
- 第二级缓存:存放已
代理过
的bean对象(Object) - 第一级缓存:存放已
初始化
的bean对象(Object)
第二级缓存的作用
- 提前生成代理对象并注入到依赖中
ps:当存在
循环依赖时
需要提前生成代理对象进行注入,而不是注入目标对象
ps:当存在循环依赖时
会提前生成代理对象,否则则在初始化完成后
生成代理对象
Bean
bean的作用域
- singleton:单例模式,全局只有一个
- prototype:原型模式,每次使用时都创建一个新的Bean实例
- request:每个请求一个,Web应用中有效
- session:每个会话一个,Web应用中有效
- globalsession:类似session,只有对portlet才有意义
ps:requestScope的bean是基于ThreadLocal实现的
ps:sessionScope的bean是基于sessionId和Map实现的
bean的线程安全
bean不是线程安全的
bean的生命周期
####### bean的生命周期概览
- bean实例化
- bean属性填充
- bean初始化
- bean使用
- bean销毁
ps:bean实例化后会放入到(三级)缓存中
####### bean的生命周期详述
- instantiateBean:bean实例化
- populateBean:bean属性填充
- 调用BeanNameAware.setBeanName
- 调用BeanFactoryAware.setBeanFactory
- 调用ApplicationContextAware.setApplicationContext
- initializeBean:bean初始化
- 调用BeanPostProcessor.postProcessBeforeInitialization
- 调用@PostConstruct注解标注的方法
- 调用InitializingBean.afterPropertiesSet
- 调用bean的init-method
- 调用BeanPostProcessor.postProcessAfterInitialization
- useBean:bean使用
- destoryBean:bean销毁
- 调用@PreDestroy注解标注的方法
- 调用DisposableBean.destory
- 调用bean的destory-method
ps:bean实例化后会放入到(三级)缓存中
IOC
- IOC容器的接口类
- BeanFactory
- BeanDefinition
- IOC容器的实现类
- AnnotationConfigApplicationContext
- ClassPathXmlApplicationContext
- FileSystemXmlApplicationContext
ps:实现类分为独立版和web版
IOC容器的初始化流程
####### IOC容器的初始化概览
- 将bean的配置信息解析成BeanDefinition并放到BeanDefinitionMap中
- 根据BeanDefinition创建bean对象并进行依赖注入
####### IOC容器的初始化详述
- prepareRefresh:context刷新前的预处理
- obtainFreshBeanFactory:加载BeanFactory(默认实现是DefaultListableBeanFactory)
- refreshBeanFactory:刷新BeanFactory
- prepareBeanFactory:BeanFactory的预处理工作
- postProcessBeanFactory:BeanFactory的后处理工作
- invokeBeanFactoryPostProcessors:实例化实现了BeanFactoryPostProcessor接口的Bean并调用接口方法
- registerBeanPostProcessors:注册BeanPostProcessor(Bean的后置处理器,在创建Bean的前后等执行)
- initMessageSource:初始化MessageSource组件(做国际化功能:消息绑定,消息解析)
- initApplicationEventMulticaster:初始化事件派发器
- onRefresh:context的刷新回调,子类可以重写这个方法在容器刷新的时候可以自定义逻辑
- registerListeners:注册应用的监听器,就是注册实现了ApplicationListener接口的监听器Bean
- finishBeanFactoryInitialization:初始化所有的剩下的非懒加载的单例bean,并填充属性
- finishRefresh:完成context的刷新
BeanFactory和FactoryBean的区别
- BeanFactory:IOC容器的底层接口
- FactoryBean:一种特殊的bean,getBean时会返回FactoryBean持有的bean
ps:Spring默认使用反射机制来创建Bean,可以使用FactoryBean来自定义创建过程
ps:getBean时如果需要返回FactoryBean本身,id前面需要添加&
符号
BeanFactory和ApplicationContext的区别
- BeanFactory:IOC容器的底层接口
- ApplicationContext:IOC容器的高级接口,是BeanFactory的子接口,支持更多的功能
AOP
AOP的核心概念
AOP的核心概念
- JoinPoint(连接点):任何可以切入的地方(比如java对象中的字段和方法)
- Pointcut(切点):实际需要切入的地方的匹配规则
- Advice(通知):切入后要做的事情
- Aspect(切面):切点和通知的组合
- Target(目标):被代理的对象
- Proxy(代理):代理后的对象
- Weaving(织入):将通知添加到切点上的实现过程
- Introduction(引入):特殊的通知(可以为目标添加一些额外的属性和方法)
AOP的通知顺序
AOP的通知顺序(Spring5)
- @AroundBefore
- @Before
- doSomething
- @AfterReturning | @AfterThrowing
- @After
- @AroundAfter
AOP的使用场景
- 认证鉴权
- 日志记录
- 耗时统计
- 异常处理
- 重试管理
- 事务管理
- 异步管理
- 缓存管理
AOP的实现案例
- 事务管理:@Transactional
- 异步管理:@Async
AOP的失效场景
AOP的失效场景
- 代理的方法被同类的方法调用
- 代理的方法不能被外部访问(private方法、protected方法)
- 代理的方法不能被重写(final方法、static方法)
- 代理的类不能被继承(final类)
- 代理的类没有被Spring管理
- 代理的对象是用户手动创建的
ps:JDK动态代理和CGLIB动态代理不能够代理
不能继承的类
和不能重写的方法
,因为是通过生成子类并重写方法来实现的
ps:JDK动态代理和CGLIB动态代理不支持代理private方法,但AspectJ静态代理支持代理private方法
ps:JDK动态代理和CGLIB动态代理支持代理protected方法,但SpringAOP只会拦截public方法(因为AopUtils.canApply方法中使用的Class.getMethods只能获取public方法)
Proxy
Spring代理的实现方案
- 静态代理:编译时生成代理类
- AspectJ:可以代理所有类
- 动态代理:运行时生成代理类
- JDK动态代理:只能对基于接口的类进行代理,不能对没有基于接口的类进行代理
- CGLIB动态代理:可以代理所有类
ps:当
没有实现接口
或者optimize配置为true
或者proxy-target-class配置为true
时会使用CGLIB动态代理,否则使用JDK动态代理
JDK动态代理和CGLIB动态代理对比
- 代理对象
- JDK动态代理:实现了接口的类
- CGLIB动态代理:任何类
- 实现技术
- JDK动态代理:基于java的反射机制
- CGLIB动态代理:基于ASM字节码操作框架
- 实现方式
- JDK动态代理:通过反射生成接口的实现类
- CGLIB动态代理:通过继承生成目标类的子类
Spring代理的生成时机
- 提前生成代理对象:存在循环依赖时,在实例化(instantiateBean)的时候生成
- 非提前生成代理对象:不存在循环依赖时,在初始化(initializeBean)的时候生成
ps:代码入口为
AbstractAutowireCapableBeanFactory.doCreateBean
SpringBoot
SpringBoot和Spring的区别
SpringBoot新增主要特性
- 起步依赖:简化了pom文件中依赖的配置(利用maven的依赖传递通过Starter引入相关的依赖)
- 自动配置:简化了bean对象的声明和注入(基于Spring的Import机制实现)
- 条件配置:@Condition
SpringBootConfiguration
@SpringBootApplication由以下三个注解组成
- @SpringBootConfiguration:被@Configuration注解所标注,和@Configuration没区别
- @EnableAutoConfiguration:自动和有条件的加载工程里面的
依赖的
组件和配置 - @ComponentScan:加载自己工程里面的
自己的
组件和配置
ps:@Configuration是不会被@EnableAutoConfiguration和@ComponentScan重复加载,因为@EnableAutoConfiguration和@ComponentScan的加载范围不同
AutoConfiguration
@EnableAutoConfiguration
通过@Import(AutoConfigurationImportSelector.class)
自动和有条件的加载所有依赖包里的配置AutoConfigurationImportSelector
通过SpringFactoriesLoader.loadFactoryNames
加载META-INF/spring.factories
资源文件里面指定的配置类SpringFactoriesLoader.loadFactoryNames
通过ClassLoader.getResources
获取classpath中META-INF/spring.factories
资源文件
SpringBootStarter
- 起步依赖:利用maven的依赖传递通过Starter引入相关的依赖
- 自动配置:应用启动时会通过
@SpringBootApplication
里的@EnableAutoConfiguration
自动和有条件的加载所有依赖包里的组件和配置
SpringWeb
Mvc
请求流程描述
- web容器接收到请求后解析成Request对象
- web容器将所有请求交给DispatcherServlet处理
- DispatcherServlet通过HandlerMapping获取Handler
- 通过相应的适配类HandlerAdapter调用Handler的处理方法处理请求并返回ModelAndView
- 根据返回的ModelAndView选择一个适合的ViewResolver
- ViewResolver结合Model和View渲染视图
- 将结果放到Response对象中返回给web容器
请求过滤拦截
Filter(过滤器)和Interceptor(拦截器)对比
主要区别如下
- 使用范围不同:Filter是Servlet规范,只能用于web应用,Interceptor是Spring的规范,还可以用于非web应用
- 拦截对象不同:Filter在IOC初始化之前,Interceptor在IOC初始化之后,所以Filter中不能使用IOC中的对象
Filter(过滤器)和Interceptor(拦截器)顺序
执行顺序如下
- Filter(过滤器)
- Interceptor(拦截器)
- Advice
- Aop
ps:Filter在IOC之前,所以Filter中不能使用IOC
后端如何支持跨域功能
- 全局
- WebMvcConfigurer
- CorsFilter
- 自定义Filter添加响应头(Access-Control-Allow-Origin)
- 局部
- @CrossOrigin
- 手动在响应里面添加响应头(Access-Control-Allow-Origin)
Web
如何实现幂等
幂等和去重的区别
- 幂等:是一种概念,操作执行多次的结构都是一样的
- 去重:是一种方案,操作只会执行一次
ps:去重是实现幂等的一种方案(如果操作本身就是支持幂等的就不需要去重)
如何实现幂等
- 请求
- token令牌
- 数据
- 插入操作
- 去重表(mysql)
- 直接插入 + 唯一约束:适合有唯一约束的
- 使用
insert into
检测到插入失败(DuplicateKeyException)后换一个主键重试 - 使用
insert ignore into
检测到插入失败后换一个主键重试
- 使用
- 检测插入 + 加锁控制:适合没有唯一约束的
- 使用
select for update
检测到重复后换一个主键重试
- 使用
- 直接插入 + 唯一约束:适合有唯一约束的
- 去重表(redis)
- setnx
- 去重表(mysql)
- 更新操作
- 状态机 + CAS思想
- 版本号 + CAS思想
- 计数
- 时间戳
- 插入操作
ps:去重时还可以使用布隆过滤器来优化去重的效率
ps:数据库的插入和更新都会主动加锁,所以不需要额外加锁
幂等和去重都需要唯一标识
唯一标识来源
- 业务id(身份证号、手机号、设备指纹)
- 逻辑id(发号器生成)
- 顺序id
- 随机id
唯一标识实现
- 单机
- 顺序id
- mysql自增id
- 时间戳
- 随机id
- UUID
- 顺序id
- 分布式
- 顺序id
- mysql发号器
- redis发号器
- 随机id
- UUID
- SnowflakeId
- MongodbId
- 顺序id
如何防止重复提交
- 前端
- 进入页面时向后端请求一个去重token放入隐藏域中
- 点击提交按钮后立即禁用提交按钮
- 点击提交按钮后显示加载中或者跳转到其他页面
- 后端
- 取出前端提交的去重token
- 如果token存在,则执行正常逻辑,并销毁去重token
- 如果token不存在,则执行去重逻辑,即丢弃重复的请求
如何提高接口的性能
- 缓存加速
- 本地缓存
- 集中缓存
- 异步处理
- 子线程(多线程)
- 消息队列
- 并行处理
- 分解任务并进行并行处理
- 批量处理
- 聚合任务并进行批量处理
- 池化技术
异步处理时获取任务结果
- 轮询
- 回调(回调机制,耦合)
- 监听(发布和订阅,不耦合)
SpringData
Mybatis
${}和#{}的区别
- ${}是属性替换符,生成sql时会直接替换成属性的值(不会被转义)
- #{}是参数占位符,生成sql时会被处理为问号并通过PreparedStatement设置参数执行(会转义)
where中的1=1的作用
为了保证没有筛选条件时sql语句的正确性
插入时如何返回主键
返回主键方案
- 数据库支持主键自增时:useGeneratedKeys + keyProperty
- 数据库不支持主键自增时:selectKey
类的属性名和表的字段名不一样的解决办法
类的属性名和表的字段名不一样的解决办法
- 启用下划线转驼峰的配置
- 在sql中起别名保持一致
- 使用ResultMap自定义映射
分页查询的实现方案
分页查询方案
- 使用RowBounds来完成内存分页(它是针对ResultSet结果集执行的内存分页,而非物理分页)
- 直接在sql中写分页条件来完成物理分页
- 通过分页插件来完成物理分页
分页插件的基本原理
分页插件的基本原理是使用Mybatis提供的插件接口,实现自定义插件,在插件的拦截方法内拦截待执行的sql语句并改写sql,改写时会根据方言(dialect)添加对应的物理分页条件
关联查询的实现方案
- 一对一查询(selectOne): association + result、association + select
- 一对多查询(selectOne):collection + result、collection + select
- 多对一查询(selectMany): association + result、association + select
- 多对多查询(selectMany):collection + result、collection + select
ps:result方式是连接查询,需要在sql中进行join
ps:select方式是分步查询,不需要在sql中进行join
ps:多对一
本质上还是一对一
,多对多
本质上还是一对多
Mybatis一级缓存的缓存一致性问题
一级缓存是session范围的
因为一级缓存是session范围的,所以BSession的更新不会使ASession的缓存失效,ASession再次查询时还会读缓存,导致BSession数据的更新读不到
Mybatis二级缓存的缓存一致性问题
二级缓存是mapper范围的
因为二级缓存是mapper范围的,所以BMapper的更新不会使AMapper的缓存失效,AMapper再次查询时还会读缓存,导致BMapper数据的更新读不到
Transaction
事务的实现方式
- 编程式事务管理:TransactionTemplate、TransactionManager
- 声明式事务管理:@Transactional、TransactionProxyFactoryBean
- 配置式事务管理:Aspectj AOP
Spring事务实现为什么不推荐@Transactional注解
- 使用不当时事务不会生效,比如同类方法调用等
- 容易出现长事务问题,占用数据库连接
- 事务内的非数据库操作不能回滚,会导致不一致的问题
@Transactional使用时容易出现的问题
@Transactional + @Async
:异步方法会启动一个新的事务(因为事务是线程隔离的)@Transactional + @Retryable
:重试如果在事务开始之后可能会不起作用(隔离级别为可重复读时只在事务启动时生成快照)@Transactional + Lock
:解锁如果在事务提交之前可能会导致业务逻辑错误
事务的失效场景
事务的失效场景
- 所有AOP失效的场景(例外是事务对protected也不生效)
- 多线程或者异步调用(事务是线程隔离的)
- 事务传播方式使用不当
ps:@Transactional是通过AOP实现的,所以失效的场景包含AOP失效的场景
事务不回滚的场景
- 异常被吞掉了
- 未指定rollbackFor参数时抛出的异常不是RuntimeException
- 指定rollbackFor参数时抛出的异常和指定的异常不匹配
事务的传播方式
事务传播方式处理的是两个方法之间的事务关系,比如加入当前事务或者新建事务等等
事务传播方式
- PROPAGATION_REQUIRED:如果当前存在事务就
加入当前事务
,否则就以新建事务
的方式运行 - PROPAGATION_SUPPORTS:如果当前存在事务就
加入当前事务
,否则就以非事务
的方式运行 - PROPAGATION_MANDATORY:如果当前存在事务就
加入当前事务
,否则就抛出异常
- PROPAGATION_REQUIRES_NEW:以
新建事务
的方式运行,如果当前存在事务就挂起当前事务
- PROPAGATION_NOT_SUPPORTED:以
非事务
的方式运行,如果当前存在事务就挂起当前事务
- PROPAGATION_NEVER:以
非事务
的方式运行,如果当前存在事务就抛出异常
- PROPAGATION_NESTED:如果当前存在事务就
开启嵌套事务
运行,否则就新建事务
运行
ps:传播方式为PROPAGATION_REQUIRES_NEW和PROPAGATION_NOT_SUPPORTED时如果存在当前事务,当前事务会被挂起
ps:传播方式为PROPAGATION_NESTED时嵌套事务回滚不会影响主事务,但主事务回滚会将嵌套事务一起回滚了
事务传播处理
- 当前事务存在时
- 加入当前事务
- 挂起当前事务
- 抛出异常
- 当前事务不存在时
- 新建事务
- 非事务
- 抛出异常
SpringSecurity
单点登录流程(SSO)
TODO:单点登录流程
扫码登录流程
TODO:扫码登录流程
SpringCloud
微服务的核心组件
- 服务治理:注册中心(注册和发现)、配置中心、网关
- 服务调用:负载均衡
- 服务防护:重试、熔断、降级、限流、预热
服务治理
网关对比
- Nginx:支持所有语言的网关,可编程进行增强,也可以通过插件进行增强
- Zuul:支持java语言的网关,可编程进行增强
- SpringCloudGateway:支持java语言的网关,可编程进行增强
负载均衡对比
- Nginx:服务端负载均衡
- Ribbon:客户端负载均衡
服务调用
服务调用对比
- RestTemplate:Http协议,性能相对较低
- Feign:Http协议,性能相对较低
- Dubbo:RPC协议,性能相对较高
ps:Feign可以支持声明式调用
客户端组件作用
- Feign:客户端请求库(Request)
- Ribbon:客户端请求负载均衡器(LoadBalance)
- Hystrix:客户端请求断路器(Breaker)和限流器(Limiter)
ps:OpenFeign是Spring对Feign支持SpringMVC的封装,可以使用SpringMVC的注解
服务防护
TODO:降级
TODO:熔断
TODO:限流
微服务的实现方案
SpringCloudNetflix
- 注册中心:Eureka
- 配置中心:Archaius
- 网关中心:Zuul
- 服务调用:Feign + Ribbon
- 服务防护:Hystrix
SpringCloudAlibaba
- 注册中心:Nacos
- 配置中心:Nacos
- 网关中心:无(可使用Zuul或者SpringCloudGateway)
- 服务调用:Dubbo
- 服务防护:Sentinel
SpringCloudOfficial
- 注册中心:无(可使用Nacos)
- 配置中心:SpringCloudConfig、SpringCloudVault
- 网关中心:SpringCloudGateway
- 服务调用:SpringCloudOpenFeign
- 服务防护:无(可使用Sentinel)
Mysql
Basic
Design
TODO:数据库范式
Concept
Engine
MyISAM和InnoDB的区别
- MyISAM不支持事务,InnoDB支持事务
- MyISAM不支持外键,InnoDB支持外键
- MyISAM支持全文索引,InnoDB不支持全文索引(5.7以后的InnoDB支持全文索引了)
- MyISAM不要求有唯一索引,InnoDB要求有唯一索引(没指定的话会生成隐藏的ROW_ID)
- MyISAM用的是非聚簇索引,InnoDB用的是聚簇索引
- MyISAM只支持表锁,InnoDB支持表锁和行锁
- MyISAM保存了行数,InnoDB获取行数要全表扫描
Usage
DDL
Type
char和varchar对比和选择
varchar和text对比和选择
char和varchar的默认值
char和varchar的空格处理
char和varchar的长度选择
datetime和timestamp对比和选择
datetime和timestamp的默认值
datetime和timestamp的自动更新
Constraint
为什么不推荐外键
- 外键需要额外的检查,影响性能
- 外键需要锁住主记录,影响性能
- 外键不利于分表分库,影响扩展
ps:不使用外键是为了牺牲一致性来保证可用性(类似于CAP中C和A不能同时满足)
ps:不使用外键的话则需要在应用层面来做约束检查
Id
为什么不推荐uuid和雪花id
- uuid和雪花id不是单调递增的,不是直接插入到缓存页尾部,而是需要寻找合适的位置去插入,会导致性能变差
- uuid和雪花id不是单调递增的,会分散插入到多个缓存页中,会导致页分裂和频繁的换入换出,会导致性能变差
ps:虽然雪花id对于局部(某个机器内部)来说是单调递增的,但是对于全局(所有机器整体)来说却不是单调递增的
DQL
常规查询
sql语句书写顺序
- SELECT
- DISTINCT
- FROM
JOIN - ON
- WHERE
- GROUP BY
- HAVING
- ORDER BY
- LIMIT
sql语句执行顺序
- FROM
- ON
- JOIN
- WHERE
- GROUP BY
- HAVING
- SELECT
- DISTINCT
- ORDER BY
- LIMIT
大数据量如何深度分页
转化为子查询,子查询只查出满足条件的id集合,父查询则查出在id集合里面的记录,这样可以利用索引覆盖,减少无用记录带来的开销
union和union all的区别
- union会对重复记录进行去重,union all不会
- union会按照主键进行排序,union all不会
聚合查询
count(1)和count(*)的区别
- 当统计表的行数时
count(1)
和count(*)
没区别- 理论上列有索引时
count(column)
比count(*)
快(count(*)
需要全表扫描) - 理论上列无索引时
count(column)
和count(*)
一样快(都要全表扫描) - 但是优化器优化后
count(*)
会利用索引覆盖 - 所以实际上列有索引时
count(*)
和count(column)
一样快 - 所以实际上列无索引时
count(*)
比count(column)
快 - 所以统计表的行数时推荐
count(*)
(count(*)
是sql标准,而count(1)
不是sql标准)
关联查询
on和where的区别
on是join之前过滤,where是join之后过滤,on比where先执行
子查询
- 子查询按依赖分类
- 相关子查询 子查询依赖父查询
- 非相关子查询 子查询不依赖父查询
- 子查询按返回分类
- 表子查询 返回多行多列,即返回一张表格,用于父查询的FROM子句中
- 行子查询 返回一行多列,即返回一条记录,用于父查询的FROM、WHERE子句中
- 列子查询 返回多行一列,即返回一个集合,用于父查询的WHERE子句中
- 标量子查询 返回一行一列,即返回一个值,用于父查询的SELECT、FROM、WHERE子句中
- 子查询按类型分类
- in子查询:非相关子查询 + 行子查询
- all子查询:非相关子查询 + 行子查询
- any子查询:非相关子查询 + 行子查询
- some子查询(some是any的同义词):非相关子查询 + 行子查询
- exists子查询:相关子查询 + 任意子查询
in和exists的区别
- in是非相关子查询,会将条件带入主表中进行查询,适用于次表数据量小的场景
- exists相关子查询,会扫描主表的每行并去次表中匹配,适用于主表数据量小的场景
Index
索引分类
- 按索引功能分
- 主键索引
- 辅助索引(也叫二级索引)
- 按字段特性分
- 主键索引
- 唯一索引
- 普通索引
- 按字段个数分
- 单列索引
- 联合索引(也叫组合索引、复合索引)
- 按字段长度分
- 前缀索引
- 后缀索引
- 全键索引
- 按数据结构分
- B+tree索引
- Hash索引
- Full-text索引
- 按存储内容分
- 聚簇索引:主键索引时使用,索引结点中包含主键和记录
- 非聚簇索引:辅助索引时使用,索引结点中包含关键字和主键,不包含记录
ps:数据库的
辅助索引
和搜索引擎的倒排索引
都是关键字到主键
的反向映射关系
索引原则
- 从表的角度考虑
- 数据量超过300的表适合建索引
- 频繁更新的表不适合建索引
- 从字段的角度考虑
主键
和外键
必须建索引- 经常要
查询筛选
的字段适合建索引 - 多字段查询时尽量用
联合索引
代替单列索引
- 多字段查询时使用频繁的字段应该放在
联合索引
的左边 - 多字段查询时包含
NULL
值的字段不适合建联合索引
- 从数据的角度考虑
散列性
高的字段适合建索引字串类
字段适合建前缀索引文本类
字段不适合建索引
ps:还应该删除无用的索引,避免对执行计划造成负面影响
索引失效
- 单列索引时查询条件有问题导致索引失效
- 不等查询(!=)
- 取反查询(not in)
- like查询时使用百分号开头
- 联合索引时字段组合没遵循
最左前缀
原则导致索引失效 - 联合索引时查询条件有问题导致右边的索引失效
- 联合索引时左边使用了
不等查询(!=)
导致右边的索引失效 - 联合索引时左边使用了
取反查询(not in)
导致右边的索引失效 - 联合索引时左边使用了
like查询时使用百分号开头
导致右边的索引失效 - 联合索引时左边使用了
范围查询
导致右边的索引失效 - 联合索引时左边使用了
or查询
导致右边的索引失效
- 联合索引时左边使用了
- 查询时使用了
函数
导致索引失效 - 查询时进行了
计算
导致索引失效 - 查询时触发了
隐式转换
导致索引失效 - 排序时排序字段和查询字段不一致导致无法使用索引排序
- 优化器认为全表扫描比较快导致索引失效(数据少或者需要回表)
ps:not between会分成两个范围,本质上还是范围操作
索引优化
or查询优化为union查询
索引排序
索引覆盖和回表查询
索引下推和引擎过滤
索引合并和多个索引
索引结构
- B+树是一颗多叉平衡查找树,包含N个关键字和N+1个指针
- 枝干结点只包含关键字,不包含数据
- 叶子结点既包含关键字,又包含数据
- 叶子结点通过链表连接起来,有利于范围查询
- 高度2到3层,支持2000千万数据的快速查询
Transaction
ACID
- 原子性(atomicity):事务里的所有操作要么全部生效,要么全部不生效
- 一致性(consistency):事务的操作不会影响数据的正确性
- 隔离性(isolation):事务里的数据修改何时对其他事务可见
- 持久性(durability):事务对数据的更改会永久的保存到磁盘
ps:
ACID
中C
是目的
,AID
是措施
ps:
原子性
保障的是复合操作
,隔离性
保障的是并发操作
ps:
undo日志
保证原子性
,redo日志
保证持久性
,mvcc和lock机制
保证隔离性
事务的隔离级别
- 读未提交(RU -> Read Uncommitted):问题为脏读(一个事务读到另一个事务未提交的更新数据)
- 读已提交(RC -> Read Committed):问题为不可重复读(同一条记录前后发生了变化)
- 可重复读(RR -> Repeatable Read):问题为幻读(记录的数量前后发生了变化)
- 串行化(Serializable):没有问题,但性能差
隔离性解决的是数据库事务并发的读写问题,是通过MVCC(快照读)和Lock(当前读)来实现的
- 读问题
- 并发读:不存在问题
- 写后读:不一致性(脏读、不可重复读、幻读)
- 写问题
- 并发写:插入冲突、更新丢失
- 读后写:写入偏差
- 死锁
读操作的类型
快照读:写写冲突, 读写不冲突
(通过MVCC的写时复制机制实现),读读不冲突
当前读:写写冲突, 读写冲突
(通过读写锁的读写互斥机制实现),读读不冲突
- 快照读(不加锁)
select
(MVCC)
- 当前读(加锁)
select for update
(写锁)select lock in share mode
(读锁)insert
(写锁)delete
(写锁)update
(写锁)
ps:当前读只影响其他事务的当前读,不会影响其他事务的快照读,因为快照读不加锁
各种select的区别
- 读快照的数据时用
select
- 读最新的数据但不修改时用
select lock in share mode
- 读最新的数据且要修改时用
select for update
ps:
select for update
可以用来解决并发写入时的脏写问题
mysql事务隔离级别为什么默认是RR
是为了解决binlog为STATEMENT时主从执行的SQL顺序不一致导致主从数据不一致的问题,RC级不能解决,RR级别通过加锁解决了
- 事务A删除id为1的数据
- 事务B插入id为1的数据
- 事务B先提交
- 事务A后提交
- 此时主库会存在id为1的数据
- 此时从库不存在id为1的数据
- 因为binlog是按照提交顺序记录的,会记录成先插入后删除
ps:binlog是提交的时候按照提交顺序记录的,不是按照事务的执行顺序记录的
MVCC
MVCC实现原理
- 写时复制(CopyOnWrite)
- 快照读(ReadView)
- 版本链(UndoLog)
MVCC实现原理之隐藏字段
DB_ROW_ID:6字节,记录ID,没有主键时会自动用DB_ROW_ID生成一个聚簇索引
DB_TRX_ID:6字节,版本号,记录的是修改这条记录的事务的版本号
DB_ROLL_PTR:7字节,回滚指针,指向记录的上一个版本
MVCC实现原理之ReadView
隔离级别
- 读已提交:事务中每次查询时都生成新的ReadView
- 可重复读:事务中第一次查询时只生成一次ReadView
核心概念
- 当前事务id:creator_trx_id,创建ReadView的事务id
- 活跃事务id:m_ids,创建ReadView时未提交的事务id集合
- 最小事务id:min_trx_id,m_ids里的最小id
- 最大事务id:max_trx_id,系统分配给下一个事务的事务id
- 记录事务id:trx_id,修改记录时的事务的事务id
查找过程
- 判断记录是否可见
- trx_id等于creator_trx_id,当前事务修改过的数据,对于当前事务可见
- trx_id小于min_trx_id,当前事务开启前修改的数据,对于当前事务可见
- trx_id大于等于max_trx_id,当前事务开启后修改的数据,对于当前事务不可见
- trx_id在m_ids中,当前事务开启时未提交的数据,对于当前事务不可见
- trx_id不在m_ids中,当前事务开启时已提交的数据,对于当前事务可见
- 如果记录可见,就接受当前记录
- 如果记录不可见,则通过记录的roll_ptr找到上一个版本的记录继续判断,直到所有版本的记录都判断过
ps:删除的记录会有删除标记(delete_mark),会被过滤掉
MVCC和隔离级别的关系
隔离级别为 读已提交
和 可重复读
时会启用MVCC机制
- 读已提交:事务中每次查询时都生成新的ReadView
- 可重复读:事务中第一次查询时只生成一次ReadView
MVCC彻底解决幻读问题了吗
- MVCC可以解决
不可重复读
的问题 - 不可重复读是通过MVCC中的快照(ReadView)和版本链(UndoLog)解决的
- MVCC只能解决
幻读
的部分问题,当前读
时的幻读问题 - 幻读中的
记录数量变化
问题可以通过MVCC(快照读)来解决 - 幻读中的
记录新增和删除
问题需要通过LOCK(当前读)来解决
Lock
锁和隔离级别的关系
读未提交:只加record锁,操作完记录就释放锁
读已提交:只加record锁,事务提交时才释放锁
可重复读:加record锁或者gap锁或者Next-key锁,事务提交时才释放锁
锁和查询索引的关系
- 有索引:行锁
- 无索引:表锁
锁和查询类型的关系
- 有唯一索引:会先加
临键锁
- 等值查询:
- 命中索引时
临键锁
会退化成记录锁
- 没有命中索引时
临键锁
会退化成间隙锁
- 命中索引时
- 范围查询:
- 命中索引时
临键锁
会退化成间隙锁
- 没有命中索引时加
临键锁
,不退化
- 命中索引时
- 等值查询:
- 有非唯一索引:两端都会先加
临键锁
- 等值查询:
- 命中索引时除了
临键锁
还会额外添加一把间隙锁
(会加2把锁) - 没有命中索引时除了
临键锁
会退化成间隙锁
- 命中索引时除了
- 范围查询:
- 有没有命中索引都加
临键锁
,不退化
- 有没有命中索引都加
- 等值查询:
- 无索引:会导致全表扫描,行锁会升级为表锁
Log
mysql事务日志
- undolog:事务回滚数据时需要的日志
- redolog:宕机修复数据时需要的日志
- binlog:主从数据同步时需要的日志
ps:WAL(Write-Ahead Log):预写日志(顺序写磁盘,速度快),undo和redo都是预写日志
为什么需要undolog
undolog是用来支持事务回滚的
- undolog记录了数据的上一个版本
为什么需要redolog
redolog是用来支持数据恢复的
- redolog记录了数据的变更
- redolog是顺序写操作,比直接将数据写入磁盘时的随机写更快
- redolog的占用空间小,比直接将数据写入磁盘时的耗时更短
为什么需要binlog
binlog是用来支持主从复制和备份恢复的
- binlog记录了数据的变更操作(SQL语句)
undolog、redolog、binlog的区别
- 日志归属
- undolog是引擎层面的日志(InnoDB有而MyISAM没有)
- redolog是引擎层面的日志(InnoDB有而MyISAM没有)
- binlog是服务层面的日志(InnoDB和MyISAM都有)
- 日志类型
- undolog是物理日志,记录的是数据行的变更
- redolog是物理日志,记录的是数据页的变更
- binlog是逻辑日志,记录的是变更操作(SQL语句)
undolog、redolog、binlog的产生
- undolog:
- 事务执行前生成
- redolog:可以通过
innodb_flush_log_at_trx_commit
控制写入策略- 0:事务执行时写到redolog buffer,每隔1秒刷新到redolog file后立即调用fsync刷盘
- 1:事务执行时写到redolog buffer,事务提交时刷新到redolog file后立即调用fsync刷盘
- 2:事务执行时写到redolog buffer,事务提交时刷新到redolog file,每隔1秒调用fsync刷盘
- binlog:可以通过
sync_binlog
控制写入策略- 0:事务执行时写到binlog cache,事务提交时刷新到binlog file,何时刷盘由操作系统决定
- 1:事务执行时写到binlog cache,事务提交时刷新到binlog file,每次提交时刷盘
- N:事务执行时写到binlog cache,事务提交时刷新到binlog file,每N个事务提交后刷盘
ps:innodb_flush_log_at_trx_commit默认为1,sync_binlog默认为0
ps:undolog也会生成对应的redolog来保证undolog的日志数据的持久化
undolog、redolog、binlog的释放
- undolog:事务提交后不会立即释放,要等到没用的时候释放
- redolog:刷盘后就释放
- binlog:需要手动释放或者配置过期时间
redolog和binlog的区别
- redolog属于InnoDB引擎的功能,binlog属于MySQL-Server的功能
- redolog是物理日志,记录是数据页的修改,binlog是逻辑日志,记录的数据更改操作
- redolog是循环写入,日志空间是固定大小,binlog是追加写入,日志空间会持续增长
- redolog是作为服务器异常宕机后事务数据自动恢复使用的,binlog是作为主从复制和备份恢复使用的
- redolog有crash-safe的能力,binlog没有crash-safe的能力
ps:binlog时二进制文件,而不是文本文件
redolog和binlog的主从一致性
redolog和binlog其中有一个写盘失败,会导致主(redolog)从(binlog)数据不一致的问题
mysql使用两阶段提交来保证redolog和binlog的主从一致性
- 阶段1:redolog写盘后,修改事务的状态为prepare状态
- 阶段2:binlog写盘后,修改事务的状态为commit状态
写失败的场景分析
- redolog写盘时崩溃,此时redolog和binlog都没有新数据,不存在不一致性
- redolog写盘后修改事务状态时崩溃,此时binlog中没有数据且事务处于initial状态,恢复的时候会回滚事务
- binlog写盘时崩溃,此时binlog中没有数据且事务处于prepare状态,恢复的时候会回滚事务
- binlog写盘后修改事务状态时崩溃,此时binlog中有数据且事务处于prepare状态,恢复的时候会提交事务
binlog为什么没有crash-safe的能力
- 虽然binlog拥有全量的日志,但binlog没有一个标志能够判断哪些数据已经刷盘和哪些数据还没有
- redolog则不存在这种问题,因为redolog刷盘成功后已刷盘的部分会被清除
Scope
session和global控制的是新的设置生效的范围
- session:会话,也就是当前连接立即生效
- global:全局,不包含当前连接,之后新获取的连接都会生效
Problem
并发操作
- 读问题
- 并发读:不存在问题
- 写后读:不一致性(脏读、不可重复读、幻读)
- 写问题
- 并发写:插入冲突、更新丢失
- 读后写:写入偏差
- 死锁
数据丢失
- 持久化时innodb_flush_log_at_trx_commit设置为0或2会丢数据(未刷盘的数据在宕机的时候会丢数据)
- 主从切换时主从同步不完整时会丢数据
- 程序有bug误操作删除数据
- 人为误操作删除数据
主从延迟
主从延迟会导致读写分离时不能立马读到最新的数据
Performance
TODO:mysql性能优化
查询优化
- 索引
- 常用的查询条件要添加索引
- 避免单列索引时查询条件有问题导致索引失效
- 避免不等查询(!=)
- 避免取反查询(not in)
- 避免like查询时使用百分号开头
- 避免联合索引时字段组合没遵循
最左前缀
原则导致索引失效 - 避免联合索引时查询条件有问题导致右边的索引失效
- 避免联合索引时左边使用了
不等查询(!=)
导致右边的索引失效 - 避免联合索引时左边使用了
取反查询(not in)
导致右边的索引失效 - 避免联合索引时左边使用了
like查询时使用百分号开头
导致右边的索引失效 - 避免联合索引时左边使用了
范围查询
导致右边的索引失效 - 避免联合索引时左边使用了
or查询
导致右边的索引失效
- 避免联合索引时左边使用了
- 避免查询时使用了
函数
导致索引失效 - 避免查询时进行了
计算
导致索引失效 - 避免查询时触发了
隐式转换
导致索引失效
- 查询
- select时尽量只选择需要的字段
- or查询优化为in或者union
- 排序时注意要利用索引排序
- 分页时可以使用子查询进行索引覆盖
- 子查询和关联查询时注意要用小表驱动大表
- 查询时避免使用长事务
- 大数据
- 预处理
- 预统计 + 物化
- 预查询 + 缓存
- 分库分表
- 使用数据仓库和大数据组件
- 预处理
性能优化
- 使用更好的服务器
- 增大缓冲池
- 增大连接数
操作优化
- 多次的操作优化为批量处理
- 长时间操作优化为分批处理
explain
explain的输出说明
- id:每个select子句的标识id(值越大越先被执行)
- select_type:查询类型
- table:当前表名(有时不是真实的表名)
- type:查询方式
- possible_keys:可能使用的索引
- key:实际使用的索引
- key_length:使用的索引长度
- ref:索引的哪一列被使用了
- rows:可能需要扫描的行数
- Extra:额外的信息说明
explain的Type说明
- system:只有一条数据的表
- const:主键索引,根据键直接查询到记录(常量级别)
- eq_ref:唯一索引,查出关联的一个主键后,根据主键回表查询记录
- ref:普通索引,查出关联的多个主键后,根据主键回表查询记录
- range:范围查找
- index:索引覆盖(select的字段在索引里就有)
- all:全表扫描
explain的Extra说明
- Using temporary:使用了临时表
- Using filesort:使用了文件排序
- Using index:使用了索引覆盖
- Using index condition: 使用了索引下推(Index Condition Pushdown)
- Using where: 使用了引擎过滤
- Using join buffer:使用了连接缓冲(连接时没有使用索引)
- Using MRR:使用了MRR优化(先将主键排序后再回表,因为相近的索引可能在相同的页上)
Architecture
Replication
主从复制
主从复制过程
- master将DDL和DML操作记录到binlog文件中
- slave的IO线程负责从master的logdump线程那里接收binlog并写入relaylog文件中
- slave的SQL线程负责执行relaylog文件的sql语句
ps:主从同步是从库去主库拉取,而不是主库推给从库
主从复制模式
- 异步复制模式:主库写完binlog后不管binlog是否同步到了从库就返回
- 同步复制模式:主库写完binlog后等待binlog同步到了所有的从库才返回
- 半同步复制模式:主库写完binlog后等待binlog同步到了至少一个从库就返回
- 全局事务ID步复制模式:半同步复制并用全局事务ID来改善主从同步的一致性问题
ps:半同步:semi-sync
ps:全局事务ID:GTID
ps:半同步复制时master如果没有收到slave的ack,会降级为异步复制
主从复制文件
- binlog:master的日志文件
- relaylog:slave的日志文件
- master.info:master的信息文件,保存了slave读取binlog文件和位置信息
- relaylog.info:slave的信息文件,保存了slave应用relaylog的执行点信息
binlog的格式
- STATMENT(statement-based replication, SBR):记录sql语句
- ROW(row-based replication, RBR):记录每行的变更
- MIXED(mixed-based replication, MBR):混合使用SBR和RBR
ps:SBR在某些情况下会导致主从数据不一致,比如last_insert_id()、now()等函数
ps:MBR会根据sql语句选择格式,优先使用SBR,SBR不能用的话才使用RBR
binlog格式和隔离级别的关系
- RC:只支持row格式的binlog(如果指定了mixed格式也会自动使用row格式)
- RR:支持statement、row和mixed格式
ps:statement和mixed格式可能会存在主从数据不一致的问题
主从延迟
TODO:mysql主从延迟
Cluster
TODO:mysql Cluster
Distributed
分表分库的实现方案
- 分表
- 垂直拆分:将字段拆分到多个表中
- 水平拆分:将记录拆分成多个表中
- 分库
- 垂直拆分:将表格拆分到多个库中
- 水平拆分:将记录拆分到多个库中
分表分库的查询问题
- join
- group by
- order by
- 事务问题
- 非分区键查询
分表分库的分区策略
- 按照范围分
- 使用hash算法分
- 普通hash
- 一致性hash
- hash槽
ps:一致性hash和hash槽能够很好的支持节点的扩容和缩容
ps:节点路由(node route)问题是通过hash算法来解决的
ps:节点故障(node failure)问题则是通过主从架构来解决的
分表分库的查询过程
- 基于分区键的查询会直接找到对应的分区进行查询
- 不基于分区键的查询会在所有的分区中查询并合并结果
分区键的分区方式
- 单键分区:比如user_id
- 多键分区:比如user_id和order_time合成一个单键user_id:order_time后再分区
非分区键的查询优化
- 冗余法:根据user_id和order_id各做一个分表分库的实现,查询order_id时直接去order_id的分表分库中查
- 索引法:建立order_id和user_id的全量数据关联表,查询order_id时先去关联表里面查出user_id
- 基因法:order_id的低n位设置为user_id的低n位,这样order_id和user_id的hash取模结果就一样
Theory
mysql为什么用B+树而不是红黑树、B-树详解
mysql的耗时点为磁盘IO,这就要求IO的次数尽可能少,即结点的层级尽可能得少
- 红黑树是二叉树,结点的层级很高,排除
- B树是多叉树,结点的层级较低,但是B树的结点既包含索引所以又包含数据,导致结点的层级比B+树高,也排除
- 红黑树和B树的范围查找比B+树复杂(B+树的叶子结点通过指针连接起来)
Redis
Basic
Concept
Redis和Memcached的区别
memcached只支持字符串类型,redis支持更多的数据类型
Usage
Type
- String
- List
- Set
- SortedSet
- Hash
- Geo
- Bitmap
- BloomFilter
- HyperLogLog
Expire
缓存过期策略
- 惰性删除
- 定期删除
ps:字符串的
SET
和SETEX
也可以设置过期时间,过期的key会放在过期表里面
Eliminate
内存淘汰策略
- noeviction:不淘汰,内存不够了就报错
- volatile-ttl:从设置了过期时间的key里面选择最先过期的进行淘汰
- volatile-random:从设置了过期时间的key里面随机选择进行淘汰
- volatile-lru:从设置了过期时间的key里面使用lru算法进行淘汰
- volatile-lfu:从设置了过期时间的key里面使用lfu算法进行淘汰
- allkeys-random:从所有的key里面随机选择进行淘汰
- allkeys-lru:从所有的key里面使用lru算法进行淘汰
- allkeys-lfu:从所有的key里面使用lfu算法进行淘汰
FIFO(First In First Out):先进先出算法,基于位置,淘汰最前面的数据,可以使用队列实现
LRU(Least Recently Used):最久未使用算法,基于时间,淘汰闲置时间最长的数据,可以使用链表实现
LFU(Least Frequently Used):最不常使用算法,基于频率,淘汰使用频率最低的数据,可以使用小顶堆实现
ps:LRU保留的是新访数据,LFU保留的是热点数据
ps:java中LRU可以用LinkedHashMap,LFU可以用PriorityQueue
redis中lru的近似性:采样选择一部分key而不是所有key,按照lru算法进行淘汰
redis中lfu的近似性:记录的不是真实频率,而是数量级
Update
缓存更新策略
- 旁路缓存(Cache Aside Pattern):数据写到数据库并删除缓存,业务模块负责
稍后同步
数据到缓存 - 穿透写入(Write Through Pattern):数据写到缓存,缓存模块负责
立即同步
数据到数据库 - 异步回写(Write Back Pattern):数据写到缓存,缓存模块负责
延后同步
数据到数据库
ps:旁路缓存会先删除缓存,等到下次读取缓存时发现不存在就会从数据库同步到缓存中
ps:写入操作会先更新缓存,然后将数据立即或者稍后同步到数据库
ps:穿透写入类似于cpu缓存的写通策略,异步回写类似于cpu缓存的写回策略
- 旁路缓存是以数据为主,缓存为辅的策略,追求数据的完整
- 穿透写入是以缓存为主,数据为辅的策略,追求系统的性能
- 异步回写是以缓存为主,数据为辅的策略,追求系统的性能
ps:
穿透写入
适合写少
的场景,异步回写
适合写多
的场景
Scene
- Normal
- 缓存
- 分布式锁
- Number
- 计数器
- 限流器
- 发号器(生成全局序列号)
- List
- 时间轴
- 消息列表
- Set
- 随机抽奖
- 点赞信息
- 签到信息
- 打卡信息
- 商品标签
- 社交关系
- SortedSet
- 热搜榜(TopK)
- 排行榜(TopK)
- 延时队列
- Hash
- 购物车
Structure
数据结构和编码
- String:intstr、embstr、rawstr
- List:ziplist、linkedlist、quicklist
- Set:intset、hashtable
- SortedSet:ziplist、skiplist
- Hash:ziplist、hashtable
ps:embstr和rawstr的区别是embstr和redisObject的空间是连续在一起的
ps:quicklist是ziplist和linkedlist的合体,将多个ziplist用linkedlist链接起来
- ziplist:压缩列表
- linkedlist:双向链表
- quicklist:快速列表
- intset:整型集合
- skiplist:跳跃表
- hashtable:哈希表
渐进式rehash
- 为ht[1]分配空间,让字典同时持有ht[0]和ht[1]两个哈希表
- 在字典中维持一个索引计数器变量rehashidx,并将它的指设置为0,表示rehash工作正式开始
- 在rehash进行期间,每次对字典执行添加、删除、查找或者更新操作时,程序除了执行指定的操作以外,还会顺带将ht[0]哈希表在rehashidx索引上的所有键值对rehash到ht[1],当rehash工作完成之后,程序将rehashidx属性的值一
- 随着字典操作的不断执行,最终在某个时间点,ht[0]的所有键值对都会被rehash至ht[1],
- 这时程序将rehashidx属性设置为-1,表示rehash已经操作完成
Transaction
Atomicity
redis中单个操作是原子性的,复合操作不是原子性的(因为某个命令执行出错不会影响下一个命令的执行)
ps:redis事务中的命令如果有语法错误,事务会被取消,事务里的所有命令都不会执行
Isolation
redis执行命令是单线程的,不存在并发,所以不需要隔离性
Durability
- RDB快照:类似与mysql的dump文件
- AOF日志:类似于mysql的binlog文件
- RDB快照和AOF日志混合:AOF日志只记录上次RDB快照之后的变化,这次生成快照后会重写AOF日志
ps:非混合时启动时先考虑加载AOF文件,AOF文件不存在则加载RDB文件,因为AOF文件保存的数据比RDB文件更完整
ps:混合时启动会加载RDB文件,再加载AOF文件
RDB
- save:会阻塞其他操作直到RDB操作完成
- bgsave:fork出子进程在后台进行RDB操作
AOF
- Always:命令执行完立即将AOF日志同步写入磁盘
- Everysec:每隔一秒将AOF缓冲区中的内容写入磁盘
- No:由操作系统决定何时将AOF缓冲区中的内容写入磁盘
ps:appendfsync的默认值为Everysec
RDB和AOF混合
Problem
并发操作
- 读问题
- 并发读:不存在问题
- 写后读:不一致性(脏读、不可重复读、幻读)
- 写问题
- 并发写:插入冲突、更新丢失
- 读后写:写入偏差
- 死锁
数据丢失
- 持久化时appendfsync设置为Everysec或No会丢数据(未刷盘的数据在宕机的时候会丢数据)
- 主从切换时主从同步不完整时会丢数据
- 内存不足进行淘汰时会丢失
- 程序有bug误操作删除数据
- 人为误操作删除数据
缓存问题
缓存失效
- 缓存穿透:不存在的key(布隆过滤器、缓存空值、接口校验、IP黑名单)
- 缓存击穿:单个热点key失效了(热点key永不过期、监控热点key并延长过期时间、读数据库时加锁排队)
- 缓存雪崩:大量key同时失效了(过期时间随机、使用二级缓存、读数据库时加锁排队)
ps:二级缓存是指
一级本地进程缓存 + 二级redis缓存
,一级缓存的过期时间是随机的,二级缓存的过期时间比一级缓存的过期时间长
缓存一致性
缓存一致性方案
- 写缓存:写多时前面写入的缓存会被后面的覆盖从而浪费cpu资源,而且高并发写入会导致更新丢失的问题
- 先写数据库再写缓存
- 先写缓存再写数据库
- 删缓存:使用删除和懒加载的方式效率更高,而且删除比更新更快速和安全
- 先写数据库再删缓存
- 先删缓存再写数据库
ps:关键字:写浪费,写丢失,删除更快速和安全
综上所述应该选择 删缓存
的方案
主要的操作如下
写数据库 + 删缓存
或删缓存 + 写数据库
读数据库 + 写缓存
ps:读缓存读不到时(缓存已
删除
或缓存已失效
)会读数据库
和写缓存
缓存不一致的形成条件为
- B读数据库要在A写数据库之前(读脏数据)
- B写缓存要在A删缓存之后(写脏数据)
方案1:先写数据库再删缓存
- 场景1:写数据库成功,删缓存失败(会出现缓存不一致的情况,需要重试删除缓存操作)
- 场景2:B读数据库(缓存已失效),A写数据库,A删缓存,B写缓存
ps:A写数据库要在A删缓存之前(方案要求)
ps:B读数据库要在A写数据库之前(读脏数据)
ps:B写缓存要在A删缓存之后(写脏数据)
方案2:先删缓存再写数据库
- 场景3:删缓存成功,写数据库成功(不会出现缓存不一致的情况,缓存后面会被重新加载)
- 场景4:B读数据库(缓存已失效),A删缓存,A写数据库,B写缓存
- 场景5:B读数据库(缓存已失效),A删缓存,B写缓存,A写数据库
- 场景6:A删缓存,B读数据库(缓存已删除),A写数据库,B写缓存
- 场景7:A删缓存,B读数据库(缓存已删除),B写缓存,A写数据库
ps:A删缓存要在A写数据库之前(方案要求)
ps:B读数据库要在A写数据库之前(读脏数据)
ps:B写缓存要在A删缓存之后(写脏数据)
对比发现 先写数据库再删缓存
这种方案更优
- 因为方案1出现缓存一致性的场景比方案2更少
- 因为缓存已失效比缓存已删除低的出现概率低
- 因为B读数据库之后B写缓存出现在A写数据库之后的概率低
ps:一般来说B读数据库之后B写缓存的速度比A写数据库快
所以最终的方案是 先写数据库再删缓存
,再配合 延时双删
可以更好的解决缓存不一致的问题
Performance
TODO:redis性能优化
查询优化
- 避免使用很大的key和value
- 避免使用耗时长的命令
性能优化
- 为key设置过期时间来减少内存占用
- 避免大量的key同时过期
- 尽量使用批量操作
- 启用延迟删除的特性
- 启用自动整理碎片的功能
操作优化
- 多次的操作优化为批量处理
- 长时间操作优化为分批处理
为什么要使用redis
- 高性能:redis的性能很高,可以用作缓存加快访问速度
- 高并发:redis的并发很高,可以支持更高的并发请求
ps:系统三高指标,高性能,高并发,高可用
redis为什么这么快
数据读写
部分基于内存访问,速度快数据结构
部分基于优化后的数据结构,效率高命令处理
部分基于单线程来避免上下文切换,效率高网络请求
部分基于多路复用,性能高
ps:redis是IO密集型,因此cpu不是瓶颈而网络IO是瓶颈,所以命令处理部分使用单线程更好
redis为什么QPS这么高
QPS为每秒处理的请求数和并发数的乘积,操作的速度越快,每秒处理的请求数越高
- 内存操作:redis是基于内存的,操作速度快
- 高效的数据结构:高效的数据结构,使得操作速度更快
- 单线程和事件机制:cpu不是瓶颈,使用单线程和事件机制避免了加锁和线程切换,使得操作速度更快
- IO多路复用机制:使用了IO多路复用机制,支持的并发数很高
ps:redis的QPS是10w+(50000~300000)级别的,mysql的QPS是1k+(5000左右)级别的
redis6.0之前真的是单线程吗
网络请求部分和命令处理部分是单线程,还存在其他线程
- close_file:负责关闭资源文件(日志文件和网络套接字)
- aof_fsync:负责对aof文件刷盘
- lazy_free:负责释放大对象空间
redis6.0之前为什么不使用多线程
- 单线程实现比较简单
- 多线程不是迫切需求
- 因为内存访问很快,所以并发和吞吐量也能满足早期的需求
- 单线程无需加锁和无需线程切换的特性使得单线程的性能比多线程更高
- 早期的主要操作是IO读写,cpu不是瓶颈,IO读写在单线程中可以通过多路复用来解决
redis6.0之后为什么要引入多线程
- redis6.0之后的网络IO部分是多线程的,命令执行还是单线程的
- 使用多线程是为了提高网络IO部分的性能,支持更高的并发量
redis键名长度会影响性能吗
键名太长的话会导致以下问题从而影响性能
- 内存占用更多
- 读写更加耗时
- 传输更加耗时
redis大key(BigKey)问题如何优化
BigKey的危害
- 操作时间长,阻塞其他请求,性能变差
BigKey的发现
- redis的bigkeys选项
BigKey的解决
- 使用二级缓存(本地缓存)
- 拆分成多个key进行读写
redis热key(HotKey)问题如何优化
HotKey的危害
- 请求集中在某个节点上,导致节点的压力大
HotKey的发现
- 根据业务特点预判
- 客户端收集访问信息
- 代理层收集访问信息
- redis的hotkeys选项
- redis的monitor监控
- redis的网络数据抓包
HotKey的解决
- 使用二级缓存(本地缓存)
- 分布式时增加副本数量来分摊读取压力
Architecture
Replication
主从复制
主从复制过程
建立连接阶段
- 读取主节点地址信息:读取配置文件中slaveof中配置的ip和port
- 建立Socket连接:根据slaveof中配置的ip和port与master建立连接
- 发送PING命令:收到PONG响应后说明连接正常
- 验证用户身份:如果配置了masterauth就会请求验证slave的身份
- 发送从节点地址信息:slave发送自己的ip和port
数据同步阶段
- 全量同步:发送
PSYNC ? -1
命令 - 增量同步:发送
PSYNC <runid> <offset>
命令
ps:runid为主服务器的ID
主从复制模式
- 全量复制:第一次连接时
- 增量复制:非第一次连接时
主从延迟
TODO:redis主从延迟
Cluster
TODO:redis Cluster
Distributed
redis为什么使用的是hash槽而不是一致性hash
- 一致性hash不灵活
- 一致性hash无法灵活的手动设置数据分布,比如有些硬件差的节点希望少存一点数据
- hash槽可以灵活的手动设置数据分布,hash槽能够配置每个节点使用的哈希槽范围
- 一致性hash更复杂
- 相对于hash槽,一致性hash更复杂
redis的hash槽为什么是16384个
理论上crc16算法可以得到2^16个数值,其数值范围在0-65535之间,取模运算key的时候,应该是crc16(key)%65535
如果槽位为65536,发送心跳信息的消息头达8k,发送的心跳包过于庞大。
如上所述,在消息头中,最占空间的是 myslots[CLUSTER_SLOTS/8]。 当槽位为65536时,这块的大小是: 65536÷8÷1024=8kb因为每秒钟,redis节点需要发送一定数量的ping消息作为心跳包,如果槽位为65536,这个ping消息的消息头太大了,浪费带宽。redis的集群主节点数量基本不可能超过1000个。
如上所述,集群节点越多,心跳包的消息体内携带的数据越多。如果节点过1000个,也会导致网络拥堵。因此redis作者,不建议redis cluster节点数量超过1000个。 那么,对于节点数在1000以内的redis cluster集群,16384个槽位够用了。没有必要拓展到65536个。槽位越小,节点少的情况下,压缩率高
Redis主节点的配置信息中,它所负责的哈希槽是通过一张bitmap的形式来保存的,在传输过程中,会对bitmap进行压缩,但是如果bitmap的填充率slots / N很高的话(N表示节点数),bitmap的压缩率就很低。 如果节点数很少,而哈希槽数量很多的话,bitmap的压缩率就很低。16384÷8÷1024=2kb,怎么样,神奇不!综上所述,作者决定取16384个槽,不多不少,刚刚好!
Application
Lock
TODO:redis锁(红锁)
如何用redis实现分布式锁
redis实现分布式锁需要考虑的问题
- 锁的特性
- 锁需要超时释放:程序出错或者挂了不释放锁,会导致死锁,需要支持锁超时释放
- 锁不能提前释放:任务还没完成就释放了锁,会导致锁被重复获取,需要支持锁超时刷新
- 锁的可重入性:同一个使用者可以重复获取锁
- 锁的安全释放:锁只能被持有者释放
- 锁的安全
- 加锁和超时的原子性:
加锁
和设置超时
需要保证原子性 - 检查和加锁的原子性:检查加锁的操作者是否是持有者并允许
重入
需要保证原子性 - 检查和解锁的原子性:检查解锁的操作者是否是持有者并允许
删除
需要保证原子性
- 加锁和超时的原子性:
redis看门狗超时刷新原理:有一个后台线程定期(周期小于过期时间和延长时间)去延长锁的过期时间
ps:如果key不存在了,说明是主动释放了锁,这时候就不需要延长锁的过期时间
setnx
- 不支持锁超时释放
- 不支持锁的可重入性
- 不支持锁的安全释放
加锁
SETNX lock_key 1
解锁
DEL lock_key
setnx + expire
- 锁可能会提前释放
- 不支持锁的可重入性
- 不支持锁的安全释放
- 加锁和超时不具备原子性
加锁
SETNX lock_key 1
+ EXPIRE lock_key 10
解锁
DEL lock_key
set + ex + nx
- 锁可能会提前释放
- 不支持锁的可重入性
- 不支持锁的安全释放
加锁
SET lock_key 1 EX 10 NX
解锁
DEL lock_key
check + set(命令版)
- 锁可能会提前释放
- 检查和加锁不具备原子性
- 检查和解锁不具备原子性
加锁
1 | if (redis.get(lock_key) == unique_value) { |
解锁
1 | if (redis.get(lock_key) == unique_value) { |
check + set(脚本版)
- 锁可能会提前释放
加锁
1 | if redis.call("get", KEYS[1]) == ARGV[1] then |
解锁
1 | if redis.call("get", KEYS[1]) == ARGV[1] then |
check + set(看门狗)
脚本版 + 看门狗 就可以实现redis单机版的分布式锁了,redis的集群版分布式锁要用红锁
Limiter
- 计数
- 固定窗口计数:无法应对流量集中在窗口边界两侧的突发大流量
- 滑动窗口计数:可以应对流量集中在窗口边界两侧的突发大流量
- 漏桶:流量漏出速度恒定,多余的流量会被丢弃,无法应对突发大流量
- 令牌:令牌放入速度恒定,但取令牌的速度不限,可以应对突发大流量
DelayQueue
SortedSet + 轮询
Theory
redis为什么用跳表而不是红黑树详解
- 跳表插入和删除效率比红黑树高
- 红黑树的平衡操作会引起子树的操作,比较耗时
- 跳表只需要维护相邻节点,耗时较少
- 跳表的范围查询效率比红黑树高
- 红黑树通过二分法找到最小值后还需要中序遍历整棵树找到范围内的值,比较耗时
- 跳表通过二分法找到最小值通过向后遍历就能找到范围内的值,耗时较少
Mq
Reliability
可靠性的关键点
- 传输
- 确认机制
- 超时机制
- 重试机制
- 存储(持久化)
- 写盘策略
- 刷盘策略
- 同步策略(主从同步策略)
可靠性的保证
- 生产端:使用确认机制
- 同步发送:同步发送并ACK(sync)
- 异步发送:异步发送并ACK(async)
- 单向发送:异步发送且不ACK(oneway)
- 队列端:开启持久化机制(刷盘策略 + 主从同步策略)
- 写盘策略:
- 同步写盘:写到磁盘后才返回ACK
- 异步写盘:写到页缓存后就返回ACK
- 刷盘策略:
- 每次刷盘
- 每秒刷盘
- 系统刷盘
- 同步策略:
- 不同步:不需要同步到从节点
- 半同步:至少需要同步到一个从节点
- 全同步:需要同步到所有的从节点
- 写盘策略:
- 消费端:使用手动确认机制(不使用自动确认机制)
- 不确认:收到消息后不需要确认
- 自动确认:收到消息后先确认再消费
- 手动确认:收到消息后先消费再确认
RabbitMQ
- 生产端:使用确认机制(使用confirm机制)
- 队列端:开启持久化机制(交换机、队列、消息)
- 消费端:使用手动确认机制(basicAck)
ps:生产端建议使用确认(confirm)机制,不建议使用事务(transaction)机制
ps:确认机制是异步操作,性能好
ps:事务机制是同步操作,性能差
RocketMQ
- 生产端:使用确认机制(使用异步发送)
- 队列端:开启持久化机制(同步刷盘策略 + 同步主从同步策略)
- 消费端:使用手动确认机制(ConsumeStatus)
ps:生产端建议使用异步发送,而不是同步发送和单向发送
ps:异步发送是异步操作,性能好
ps:同步发送是同步操作,性能差
ps:单向发送无确认机制,会丢失消息
Kafka
- 生产端:使用确认机制(生产者的acks配置为1)
- 队列端:开启持久化机制(同步刷盘策略 + 同步主从同步策略)
- 消费端:使用手动确认机制(手动提交offset)
ps:acks含义 0:不关心结果 1:只需主节点写完成 -1:所有从节点都同步完成
Problem
消息丢失
TODO:消息丢失
问题描述:消息丢失
解决办法:确认机制 + 重试机制 + 持久化机制
- 生产时丢失
- 场景1
- 丢失原因:使用自动确认机制时,网络有问题导致生产端发送消息失败
- 解决办法:使用重试机制
- 场景2
- 丢失原因:使用自动确认机制时,网络有问题导致队列端返回ACK失败
- 解决办法:使用重试机制
- 场景3
- 丢失原因:使用自动确认机制时,队列端处理异常导致生产端发送消息失败
- 解决办法:使用重试机制
- 场景1
- 入队时丢失
- 场景1
- 丢失原因:队列端服务强停时,消息还在内存缓冲区中没有持久化到磁盘
- 解决办法:开启持久化机制
- 场景2
- 丢失原因:队列端服务崩溃时,消息还在内存缓冲区中没有持久化到磁盘
- 解决办法:开启持久化机制
- 场景3
- 丢失原因:队列端机器宕机时,消息还在内存缓冲区中没有持久化到磁盘
- 解决办法:开启持久化机制
- 场景1
- 消费时丢失
- 场景1
- 丢失原因:使用手动确认机制时,消费端收到消息后忘记了返回ACK
- 解决办法:需要返回ACK
- 场景2
- 丢失原因:使用自动确认机制时,消费端收到消息后不管是否处理成功就返回了ACK
- 解决办法:需要在处理成功后返回ACK
- 场景3
- 丢失原因:使用手动确认机制时,消费端收到消息后使用了异步处理并立即返回了ACK
- 解决办法:需要在异步处理成功后返回ACK
- 场景1
消息重复
TODO:消息重复
问题描述:消息重复
解决办法:幂等处理(数据库唯一性去重 或者 redis的setnx去重)
- 生产时重复
- 场景1
- 重复原因:生产时,队列端回复的ACK丢失了,生产端没有收到ACK会重发
- 解决办法:队列端进行幂等处理
- 场景1
- 消费时重复
- 场景1
- 重复原因:消费时,消费端回复的ACK丢失了,队列端没有收到ACK会重推
- 解决办法:消费端进行幂等处理
- 场景1
ps:反正最后都需要在消费端解决一下,所以只需要在消费端解决就行了
消息乱序
TODO:消息乱序
- 生产时乱序
- 场景1
- 乱序原因:因为网络的复杂性,先生产的消息可能因发送时间长而后到达
- 解决办法:
- 场景2
- 乱序原因:因为消息的大小不同,先生产的消息可能因发送时间长而后到达
- 解决办法:
- 场景1
- 消费时乱序
- 场景1
- 乱序原因:存在多分区时,消息投递到了消息积压多的分区,导致比后面的消息晚消费
- 解决办法:生产时保证分区有序 或者 只创建一个分区
- 场景2
- 乱序原因:存在多分区时,先消费的消息可能晚于后消费的消息完成
- 解决办法:
- 场景1
ps:总结为3种原因,1. 先发送但后到达 2. 先到达但后消费 3. 先消费但后完成
消息积压
TODO:消息积压
消息溢出
TODO:消息溢出
Performance
Kafka为什么这么快
TODO:页缓存
TODO:零拷贝
TODO:时间轮
- 顺序读写:寻址快
- 分区读写:并发读写
- 批量读写:减少网络耗时
- 数据压缩:减少数据量从而减少读写时间和传输时间
- 页缓存:避免实时写磁盘的慢操作
- 零拷贝:避免多次拷贝造成的时间浪费
Network
TODO:TCP
OSI
OSI的7层模型
TCP/IP的5层模型
TCP
TCP和UDP的区别
TCP如何保证可靠传输的
TCP的三次握手
TCP的四次挥手
TCP的重传机制
TCP的滑动窗口
TCP的流量控制
TCP的拥塞控制
TCP的TIME_WAIT和CLOSE_WAIT
Web
Http
GET和POST的区别
Cookie和Session的区别
输入url到显示页面的全过程
Http常用的状态码
Http和Https的区别
Distributed
CAP
- CAP
- C:强一致性:某个时刻读取所有节点的某个数据都是相同的(强一致性,不同于弱一致性和最终一致性)
- A:高可用性:在有限的时间返回数据(不能出现等待延迟或者访问超时的情况)
- P:分区容错性:网络故障出现分区后各个可用分区也能继续对外提供服务(而不是暂停服务去等待网络恢复)
ps:在分布式系统中,因为网络的复杂性,所以一定会出现因为网络故障导致分区的情况,如果此时不保证分区容错性,就意味着允许部分节点不可用导致服务不能提供完整的服务,但是大部分得场景是不能忍受不完整的服务的,所以分布式系统中一般会保证分区容错性
BASE
- BASE
- BA:基本可用
- S:软状态(允许有短暂的中间状态)
- E:最终一致性
- ACID
- A:原子性
- C:一致性
- I:隔离性
- D:持久性
ps:BASE理论的一致性是最终一致性,而CAP和ACID的一致性是强一致性
分布式id
TODO:分布式id
- 单机
- 顺序id
- mysql自增id
- 时间戳
- 随机id
- UUID
- 顺序id
- 分布式
- 顺序id
- mysql发号器
- redis发号器
- 随机id
- UUID
- SnowflakeId
- MongodbId
- 顺序id
SnowflakeId:1(符号位) + 41(时间戳) + 10(workerId) + 12(offset)
ps:workerId = 5(machineId) + 5(datacenterId)
ps:时间戳是一个偏移量不一定要从1970-1-1
开始,可以从第一次上线时间开始
ps:可以按照实际需求做调整,比如增加时间戳或者偏移量的位数
- workerId生成方案
- machineId + datacenterId
- redis自增
- machineId生成方案
- hostname(简单,固定,需要注意检查是不是localhost)
- ip(复杂,可能会变,需要注意排除localhost和虚拟机ip)
- datacenterId生成方案
- redis自增
如何解决时钟回拨
- UidGenerator
- 每次启动时生成新的机器id
- 运行时检测到时钟回拨则抛出异常
- Leaf
- 运行时检测到时钟回拨则阻塞等待直到时间到达
分布式锁
TODO:分布式锁(红锁)
分布式事务
TODO:分布式事务
事务的分类
- 本地事务(ACID理论)
- 2PC
- 3PC
- 分布式事务
- 刚性事务(CAP理论、强一致性)
- 2PC(Seata-XA/AT)
- 3PC(Seata-XA/AT)
- 柔性事务(BASE理论,最终一致性)
- 补偿型
- TCC(Seata-TCC)
- SAGA(Seata-SAGA)
- 通知型
- 本地消息表
- MQ事务消息
- 最大努力通知
- 补偿型
- 刚性事务(CAP理论、强一致性)
ps:XA/AT是通用的事务协议(AT是高性能版的XA),JTA和JTS是Java的事务协议
ps:TCC和2PC的流程相似,TCC适用于业务系统,2PC适用于存储系统
ps:最大努力通知也称定期校对,是对MQ事务方案的进一步优化,它在事务主动方增加了消息校对的接口,如果事务被动方没有接收到主动方发送的消息,此时可以调用事务主动方提供的消息校对的接口主动获取没有处理完成的消息
事务的概念
- TX:应用程序与事务管理器之间的接口协议
- XA:事务管理器与资源管理器之间的接口协议
- AP:应用程序
- RM:资源管理器
- TM:事务管理器
- 2PC:Prepare、Commit、Rollback,两阶段提交(Two-Phrase-Commit)
- 3PC:Can、Prepare、Commit、Rollback,三阶段提交(Three-Phrase-Commit)
- TCC:Try、Confirm、Cancel,两阶段提交(Two-Phrase-Commit)
ps:XA规范是X/Open组织定义的分布式事务处理(DTP,Distributed Transaction Processing)标准
事务的流程
本地消息表
本地消息表使用了两阶段提交的思想来保证本地事务和事务消息的一致性
- 事务主动方写业务表
- 事务主动方写消息表
- 事务主动方发送事务执行消息
- 事务被动方消费事务执行消息
- 事务被动方写业务表
- 事务被动方发送事务结果消息
- 事务主动方消费事务结果消息
- 事务主动方更新消息表的状态
ps:如果事务被动方执行成功,可以发成功消息让事务主动方提交
ps:如果事务被动方执行失败,可以发失败消息让事务主动方回滚
失败时的相关处理
- 第1步和第2步失败,事务主动方直接回滚本地事务就行了
- 其他步骤失败,事务主动方根据消息表状态重发就行了
MQ事务消息
MQ事务消息使用了两阶段提交的思想来保证本地事务和事务消息的一致性
- 事务主动方在事务执行前发送半消息(半消息对消费者不可见)
- 事务主动方执行事务
- 事务主动方在事务执行后发送事务消息
- 事务执行成功发送提交消息
- 事务执行失败发送回滚消息
失败时的相关处理
- 第1步失败,事务主动方重发就行了
- 第2步失败,事务主动方回滚就行了
- 第3步失败
- MQ发起回查消息
- 事务主动方检查事务状态并重发就行了
事务的问题
2PC的问题
- 性能问题:参与者获取被占用的资源时会被阻塞
- 协调问题:协调者挂掉后参与者不会释放占用的资源
- 网络问题
- 部分执行问题:因为网络问题导致只有部分参与者收到了Commit或者Rollback请求
- 重复执行问题:因为网络问题导致参与者重复收到了Commit或者Rollback请求
- 空回滚问题:因为网络问题导致Prepare丢失或者Prepare晚于Rollback达到,使得还没有执行过Prepare就执行Rollback
ps:问题的关键字:性能、协调、网络
ps:协调者有超时机制,参与者没有超时机制
3PC的问题
- 网络问题
- 部分执行问题:因为网络问题导致只有部分参与者收到了提交或者回滚请求
- 重复执行问题:因为网络问题导致参与者重复收到了提交或者回滚请求
- 空回滚问题:因为网络问题导致Prepare丢失或者Prepare晚于Rollback达到,使得还没有执行过Prepare就执行Rollback
ps:3PC新增了can阶段来判断是否能够执行事务从而减少了事务失败时的阻塞时间
ps:3PC新增了参与者超时机制来避免协调者挂掉后参与者不会释放占用的资源的问题
TCC的问题
- 每个操作都需要支持额外的确认和取消操作
- 每个操作都需要支持幂等
- 网络问题
- 部分执行问题:因为网络问题导致只有部分参与者收到了Confirm或者Cancel请求
- 重复执行问题:因为网络问题导致参与者重复收到了Confirm或者Cancel请求
- 空回滚问题:因为网络问题导致Try丢失或者Try晚于Cancel达到,使得还没有执行过Try就执行Cancel
事务的补偿
- 回滚
- 重试