分配单位:进程是操作系统分配资源的单位,线程是CPU调度的基本单位
占用资源:每个进程自己独立的代码段、数据段、堆栈段,而线程之间共享进程的资源,独享必不可少的寄存器和栈
通信:进程实现通信较为复杂,线程之间的通信可借助进程的共享资源轻松实现
开销:进程的创建和销毁都涉及到操作系统管理内存、文件等信息(如划分/收回内存空间、创建/删除PCB文件),线程的创建不会涉及到而是共享它们
进程之间的切换开销也比线程大,线程切换仅仅需要切换线程独享的寄存器和栈,而进程切换在前者的基础上还要切换代码段、数据段、堆栈端等共享内存,还涉
还涉及原进程的PCB文件的保存和新进程的PCB文件的读取
定义:就是当两个线程同时持有对方需要的互斥锁时,则这两个线程之间出现了死锁
发生的条件:
1 互斥资源:访问的是互斥资源
2 持有并等待:当一个线程获取到一个互斥资源后再获取下一个互斥资源时进入等待状态不会释放掉第一个资源
3 非抢占式获取:当一个线程获取的互斥资源被其他线程占有时无法强制占有,只能等待其他线程释放
4 循环等待:两个线程同时持有对象想要获取的互斥锁进入循环等待状态
public class DeadLock {
static Integer resource1 = new Integer(1);
static Integer resource2 = new Integer(1);
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
synchronized (resource1){
System.out.println("任务1获取到resource1");
try {
Thread.sleep(1000);
synchronized (resource2){
System.out.println("任务1获取到resource2");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
synchronized (resource2){
System.out.println("任务2获取到resource2");
try {
Thread.sleep(1000);
synchronized (resource1){
System.out.println("任务2获取到resource1");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}).start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
Jconsole
互斥资源有序分配法破坏循环等待的条件,即线程1和线程2获取的顺序都是先获取互斥资源1再获取互斥资源2
线程是分配CPU的基本单位,大致意思就是多个CPU同一个时间点可以并行多个线程,使用多线程能更好的发挥多核的优势,提高CPU的使用率
所谓的线程安全就是在多线程执行某一段程序时和单线程执行的结果一致,保证安全的方法以Java为例,Java中的synchronize、volatile、原子类、juc包等工具都可以作为实现线程安全的工具,比如在某一个场景下要有多个线程对Intger i进行++操作,这里就可能出现两个线程同时将加完的结果写回主内存,此时的两次加就变为了一次加,这里可以借助原子类实现线程安全
管道、消息队列、共享内存、信号量、信号、Socket
1 正常的拷贝
1.1 先read:数据先读到内核缓冲区,再由内核缓冲区传给用户缓冲区,这期间发生了两次上下文切换:用户态 ->内核态,内核态->用户态,两次拷贝:DMA负责将数据从磁盘里拷贝到内核缓冲区,CPU负责将数据从内核缓冲区拷贝到用户缓冲区
1.2 在writer:数据写到Socket缓冲区,再从socket缓冲区写到网卡,这期间也发生了两次上下文切换,两次拷贝
2 零拷贝
使用sendfile函数先将数据通过DMA拷贝到内核缓冲区,再将内核缓冲区里的数据起始位置和偏移量发送给socket缓冲区,网卡DMA通过这两个参数拷将内核中的数据拷贝到网卡缓冲区,这样直接通过两个DMA拷贝就实现了数据传输,不会使用CPU进行拷贝故称为0拷贝,而且上下文切换的次数也只是两次:从用户态->内核态,从内核态返回用户态
在传输大文件时如果还是按照零拷贝的思路的话,可能会导致内核缓冲区命中率降低,热点小数据被淘汰问题
所以拷贝大文件时使用异步IO+直接IO,异步IO就是用户发送文件请求后直接返回,当文件数据到达用户缓冲区时再通知用户
直接IO就是绕过内核缓冲区,直接将数据从磁盘缓冲区拷贝到用户缓冲区,这样就避免了前面的大文件沾满内核缓冲区问题、
即一个进程/线程监听多个Socket端口,Socket就是文件描述符
select:
poll:poll相对于select仅仅是将bitmap改成使用链表表示Socket状态,解决上面的第2个数量限制问题,但性能仍然较差
epoll:使用时间驱动,内核维护了一个链表记录就绪事件,当某个socket有事件发生时,通过回调函数内核会将其加入到就绪事件链表中,当用户调用epoll_wait只会返回有事件发生的Socket文件描述符,不会像select/poll返回全部,大大提高了性能
前提:tb_like表存在二级索引
问题起源,查第5000000页的10行:
select * from tb_like limit 5000000,10;
limit x, y 变慢原因:存储引擎无法直接定位到起始行,需要通过二级索引扫描x + y行再会给server层取最后y条,由于二级索引会有回标操作,所以这里就进行了 x次无用的回表,所以严重影响查询效率
子查询优化(借助索引覆盖):先将要查询的10行的id用子查询查出来,这里的子查询查询id的是二级索引树并出现了索引覆盖,所以会快一些
force index(PRIMARY)强制用主键索引,下面嵌套了一个select是因为in和limit不能连用,嵌套一个select隔离一下即可
select * from tb_like force index(PRIMARY) where id in (select id from (select id from tb_like LIMIT 5000000,10) as t);
游标优化:记录前一页的游标(最后一项)
4291546为第4999999行的id
select * from tb_like where id > 4291546 limit 10;
缺点就是只能一页一页的翻,无法跨多页
索引像是MySQL数据页的目录,使用索引能更快的定位到数据所在的数据页,以B+树索引为例(B+数存储在磁盘上),当我们查找一个数据走索引时先通过B+树的非叶字节点以二分查找的形式一直往叶字节点处查找,非叶字节点没有数据所以这里的IO量较少,到达叶字节点以后只需要加载叶字节点上的数据页即可找到目标数据,如果是范围查询的话B+树同一层之间是由双链表串联,通过双链表进行范围查询
好处:如果索引生效的话会极大减小IO次数降低数据库查询时间,加快通过索引字段的排序速度因为根本不用重排序
坏处:当对表中的数据进行增加、删除和修改的时候,索引也要动态的维护,这样就降低了数据的维护速度;索引还要占用物理空间
binlog就是一种逻辑日志,记录每条数据库的更新或新增语句,
主服务器先写入binlog再提交事务,然后返回给客户端,
从库会专门创建一个专门的IO线程接收主库的binlog日志,从库接收完binlog返回给主复制完成,再执行binlog中的更新语句
不一定。在MySQL中,order by后面的字段不一定要在select语句中,只要是表中的字段即可。
但是,如果你使用了distinct和order by一起用时,order by的字段必须在select中
先通过主键找到比其大且最接近的槽,通过该槽的上一个槽找到该槽的最小主键值
从最小的通过链表遍历直到找到目标主键,一个槽最大也就是一个长度为8的单向链表,遍历会很快
1 当插入的节点中数量< M 时,直接插入
2 当插入的节点中数量 = M时,则会将当前节点分为两个节点,一个结点包含 ⌈M/2⌉在左边,另一个结点包含 ⌊M/2⌋ 在右边,
将在左边的新节点的最大值加入到父节点,这里假设父节点的叉数小于 M
3 在前面2的前提下,当父节点的叉数等于M时,则父节点也要进行分裂,依次类推直到找到父节点小于M的
1 删除该关键字,如果不破坏 B+树本身的性质,直接完成删除操作(情况 1);
2 如果删除操作导致其该结点中最大(或最小)值改变,则应相应改动其父结点中的索引值(情况 2);
3 在删除关键字后,如果导致其结点中关键字个数不足,有两种方法:一种是向兄弟结点去借,另外一种是同兄弟结点合并(情况 3、4 和 5)。(注意这两种方式有时需要更改其父结点中的索引值。)
1 undo log:用于恢复上一条语句状态的日志,如果是插入操作,则undo log记录为删除操作,如果是删除操作则记录为新增日志,
在事务回滚时就是执行undo log里的逻辑日志,还有就是MVCC里也有用到undo log,undo log每个接单会指向其前一个版本的节点,
所以MVCC模式下通过当前读的版本号就可以顺着undo log的链找到小于等于当前读版本的记录,从而实现当前读
2 redo log:redo log是物理日志记录的是某个位置的值改为了多少之类的数据,
redo log的主要作用就是为了恢复MySQL在突然宕机时内存中的未刷盘的脏数据,redo log是一个环形的结构循环写,
如果redo log满了就会阻塞MySQL的更新操作,开启MySQL数据的刷盘
3 binlog:bin log是逻辑日志,记录每个更新操作语句,是追加写的操作,
主要用途就是备份数据库,主从复制,
主从复制就是从节点会有一个线程监听主节点的binlog日志的写操作,当主线程有新增bin log写日志时就会同步到从节点,从节点再回放从主节点上获取到的bin log就能实现主从数据同步了
4 两阶段提交:redo log和bin log两者没有什么关联,为了防止出现持久化不一致的情况,所以先准备好redo log,再提交bin log,最后提交redo log
MYSQL的事务是存储引擎InnoDB实现的,InnoDB实现的事务级别大概有四种:读未提交、读已提交、可重复读、串行化,读未提交会出现脏读、不可重复读、幻读,读已提交会出现不可重复读、幻读,可重复读用过MVCC + 行锁很大程度上解决了幻读,没有完全解决串行化不会出现问题,MySQL的默认隔离级别就是可重复读
索引的话通过数据结构可分为:B+树、哈希表、B树
物理结构:聚簇、非聚簇
索引类型:主键索引、唯一索引、前缀索引、普通索引
字段数量:单列索引、联合索引
聚簇索引叶子节点里存储的是数据,非聚簇索引里的叶子节点存储的是主键,
当走聚簇索引时查询到叶子节点直接返回数据即可,当走非聚簇索引时差到叶子节点获得主键后还要回表到聚簇索引里获取数据
聚簇索引
优点:查询速度快,支持范围、排序、分组等查询
缺点:占用空间大,插入时因为移动的数据较多所以插入速率较慢
非聚簇索引
优点:占用空间小,可以建立多列索引,多表查询时较快
缺点:查询速度慢
看过电子书说是2000万左右,主要由三个参数:x : B+数高度,y:一页16kb能包含索引树,z:一页所能存储的行数
得结果 = y ^ ( x - 1 ) * z,根据分析当 x 是 3,y 是 1300,z 是15最后算出2000多万
联合索引就是多个列的索引,假设创建a,b的联合索引,则索引中的排序方式就是先按a字段排序,当a字段相同时再按b字段排序
最左匹配就是where后的条件一定要是联合索引的左部,如a,b,c,用a或a,b都是最左匹配,用b,c或c或b则不符合
就是在有二级索引且sql语句的条件中存在二级索引字段时,就会先通过二级索引过滤,减少回表操作,将server层进行的非主键索引的过滤放到了引擎层
在内连接中由优化器决定,在左连接中左边的为驱动表,右边的为被驱动表
因为驱动表只会被全表扫描一次,而被驱动表会带上驱动表的条件被访问多次,所以大表可以通过主键优化每次为log级别
所以假设表A有100条数据,表B有10000条数据
则小表驱动大表复杂度为:100log(10000)
大表驱动小表复杂度为:10000log(100)
情况1:事务 A 查询 id > 0 的记录有 4 条(ids = 1、2、3、4) --> 事务B插入id=5的记录提交事务 --> 事务A直接更新id=5的记录,将此记录的事务id变为自己 --> 事务 A 查询 id > 0 的记录有 5 条出现幻读
情况2:事务 A 快照读id>0的记录数 --> 事务B插入id=5的记录提交事务 --> 事务A当前读id>0的记录数发现多了一条出现幻读
1 使用数据库的唯一索引
2 使用INSERT IGNORE语句:可以使用INSERT IGNORE语句插入数据,它会忽略插入时出现的重复值(主键冲突、或唯一索引冲突)
RC和RR唯一的区别就是Read View的时机,RR是在显示开启事务的时候Read View,RC是在每条语句的时候都会进行一次Read View,所以RC无法在事务中保证可重复读,所以RR和RC解决脏读的方式一样,就是只读取事务id小于当前活跃最小事务id或者是大于最小活跃事务id且小于当前事务id且不在活跃事务列表中的事务id,依次来实现只读取已经提交事务的数据
1 模糊匹配或联合索引查询时不满足最左匹配原则
2 对索引进行转换会导致索引失效,因为索引树中存储的是索引的原始数据而不是转换后的值,例如:where id + 1 = 11会失效,因为索引没有存储id+1的值,
还有就是对索引进行隐式转化,在MySQL中字符串和数字比较就会将字符串转为数字,如select * from tb where phone = 18679719965,如果phone为字符类型的话,则语句真实执行的是select * from tb where CAST(phone) = 18679719965
3 WHERE子句中 OR 的存在条件列不是索引就会走全表扫描
4 索引列数据中存在NULL值导致索引失效,NULL的特殊性无法直接通过索引进行定位,所以会导致索引失效
16KB是由设计者权衡多个因素后决定的
当数据页大于 16KB 时,每次查表读数据页到缓存的容量将增大,因为查数据都是先将数据页加载到内存,再在内存中的数据页查询要找的数据
当数据页小于 16KB 时,可能出现范围查询,数据页减小会增加查多个页的可能性,类似于空间局部性原则优化查询效率(自己想的)
B树的每个节点都会存储索引和具体数据,而B+树只有叶子节点会存储数据,其他非叶子节点只存储索引
索引存储:因为B树每个非叶子节点还会有数据存储,所以B+树可以存储的索引数量比B树更多,从而导致B+树每个非叶子节点更多,导致树的整体结构更加矮胖
单点查询:B树的最快时间复杂度为O(1),但也有可能到叶子节点才找到,整体速度不稳定,B+数是一定会查询到叶子节点速度更稳定,而且B+树不会冗余查询非叶子节点的数据,IO数量相对更少
范围查询:B+树通过叶子节点的双向链表实现范围查询,B树的范围查询需要进行树的中序遍历,涉及到多个节点的IO操作
像是K-V结构数据库的MongoDB就是使用的B树索引
方法一:实现Cloneable接口
这种方式要手动将其中的类型进行拷贝,如果该类中类过多,会很麻烦需要手动一个个拷
方法二:实现Serializable接口
序列化与反序列化,使用SerializationUtils的clone(Objectobj)方法,要求拷贝的对象实现了Serializable,Map不行,使用HashMap即可。
/**
* 通过字节流序列化实现深拷贝,需要深拷贝的对象必须实现Serializable接口
*
* @author Administrator
*/
public class SerializationUtils {
@SuppressWarnings("unchecked")
public static <T extends Serializable> T clone(T obj) {
T cloneObj = null;
try {
// 借助ObjectOutputStream将对象写入字节流
ByteArrayOutputStream out = new ByteArrayOutputStream();
ObjectOutputStream obs = new ObjectOutputStream(out);
obs.writeObject(obj);
obs.close();
// 借助ObjectInputStream将字节流里的对象读取出来
ByteArrayInputStream ios = new ByteArrayInputStream(out.toByteArray());
ObjectInputStream ois = new ObjectInputStream(ios);
// 返回生成的新对象
cloneObj = (T) ois.readObject();
ois.close();
} catch (Exception e) {
e.printStackTrace();
}
return cloneObj;
}
}
方法三:fastjson实现
用fastjson从Object转成json,然后转回object,本质上是反射
private Object deepCopyByJson(Object obj) {
String json = JSON.toJSONString(obj);
return JSON.parseObject(json, Object.class);
}
一般最好用方法二SerializationUtils,性能要求不高的情况下代码简洁也很重要。
hashmap的主要数据结构就是数组 + 链表/红黑树,每次put时通过哈希key值找到卡槽,使用拉链法解决hash冲突,判断两个key是否相等是先比较hashcode再用equals方法比较,所以加入hashmap的key一定要重写hashcode和equals方法,hashmap扩容时是乘原来的两倍,其容量一直保持为2n,因为hash数组长度为2n时可以通过位运算优化取余操作,扩容时会将一个链表的元素拆分为两个链表,所以其中一个位置不变,其中一个位置+原长度
HashMap的底层数据结构?
HashMap的底层数据时数组+链表/红黑树
HashMap怎么存储键值对的?
HashMap数组每个位置对应一个槽,用Hashcode定位槽,用拉链法解决hash冲突
HashMap中的数组存储的什么?链表的作用?红黑树的作用?
存储的是链表首节点或红黑树根节点,链表就是用于拉链法解决hash冲突,红黑树防止出现链表过长的情况导致查询速率为O(n)
HashMap的扩容机制,什么时候扩容?如何扩容?
HashMap里有一个负载=实际长度/容量因为,当这个负载因子大于0.75时就会触发扩容,或者在数组长度小于64且链表长度大于8页会触发扩容
扩容就是将开辟一个原来容量两倍大小的数组,遍历原来数组及其链表中所有的节点,通过重新计算每个节点的新的hash找到新的槽,在将节点通过尾部插法加入到新数组中
是的,Java中子类构造方法默认会调用父类构造方法。
1 如果父类没有构造方法,那么子类的构造方法中会自动进行调用;
2 如果父类有自己的构造方法,那么在子类的构造方法中,必须要调用父类的某个构造方法,而且必须是在构造方法的第一个语句中进行调用
1 toString
2 hashCode
3 equals
4 clone
5 wait
6 notify
7 notifyAll
8 getClass
9 finalize
因为在使用Map集合时,对某个key判断是否相等的逻辑时先判断两个key的hashCode是否相等,如果HashCode相等再使用equals方法比较
如果不重写hashCode则对象的hashcode返回的是地址的hash值,肯定不同,所以equals方法也不会生效,这样就可能导致出现重复的key
1 整型:byte、short、int、long
2 浮点型:float、double
3 字符型:char
4 布尔型:boolean
1 封装:将对象的属性隐藏,只暴露访问/修改方法,使对象的属性安全一些
2 继承:一个继承父类后,子类可直接使用父类中非私有的成员变量和成员方法,如果子类想要访问父类的私有属性或方法,则需要使用super关键字来访问
3 多态:就是由一个子类对象指向一个父类对象或接口
Set:HashSet、TreeSet
List:ArrayList、LinkedList
Map:HashMap、ConcurrentHashMap、HashTable
List有如数组实现的ArrayList,链表实现的LinkedList,Map的底层实现有HashMap的数组+链表/红黑树
List存的可重复的有序的单个元素,HashMap存储的是键值对,无序,且key不可重复
在ArrayList中当add时会判断是否需要扩容,如果容量不足则进行扩容,扩容的过程就是开辟一个原来容量1.5倍的数组,将原来得的元素复制到新数组中,
在将add的元素加入到新数组的尾部,最后将数组引用指向新数组
HashMap是线程不安全、ConcurrentHashMap线程安全
1.8以前HashMap是数组加链表实现,ConcurrentHashMap是通过分段锁实现,在对某一段进行写实会加写锁保证线程安全,HashMap因为是在扩容时头插法容易出现链表死循环和数据丢失
1.8后两者的数据结构则都是数组+链表或红黑树, HashMap该用尾插法解决了链表死循环但扩容时仍存在数据丢失问题,ConcurrentHashMap由分段锁改为锁数组节点,降低了锁的粒度,提高了一定的并发量
代理就是在被代理的方法的调用的前后包装一些特殊的操作
动态代理主要有JDK动态代理和CGLIB动态代理
JDK动态代理是通过实现同一个接口来实现代理
大部分的情况JDK动态代理效率更高
锁升级是一个过程,重在理解这个升级过程,分为三个阶段:偏向锁 --> 轻量级锁 --> 重量级锁
偏向锁:是指一个线程一直获取同一个对象的锁,即对象头中原来存储的获得锁线程和当前获取锁的线程一致
轻量级锁:当一把偏向锁被另一个线程获取时,偏向锁将会升级为轻量级锁,这个线程会通过自旋的方式获取锁,不会阻塞,性能较高
重量级锁:当一把轻量级锁,被另外的一个线程自旋获取多次后,还是没有获取到锁,则该线程将会进入一个阻塞状态,当前锁升级为重量级锁,重量级锁使后续获取的线程都阻塞,性能降低
synchronized(锁节点) + 自旋锁 + CAS
put操作:每次插入时会用synchronized锁住key对应的hash桶,put操作在一个死循环自旋进行直到put成功
Spring Task 底层是基于 JDK 的 ScheduledThreadPoolExecutor
线程池来实现的,默认的线程池配置是单线程,如果出现任务时间大于任务间隔的情况,则后面的任务将会等待前面的任务结束后,再延迟一定时间后再开始,适用于串行任务的情况,如果需求中的任务是并行的话,可以配置 Scheduled 线程池的线程参数改为非单线程
ScheduledThreadPoolExecutor的实现:是基于线程池,线程池的任务队列是延迟队列,由于时延迟队列,只有任务时间到达时才能获取到任务,如果任务队列最早的任务也还未到达则会阻塞。当任务执行完后会检查是否是一个周期性任务,如果是的话就会重新计算下一次执行的时间,再加入到任务队列。所以如下
综上所述,ScheduledThreadPoolExecutor
的单个任务都是串行执行的
多态是指允许不同子类型的对象对同一行为作出不同的响应。
特点
编译时重载:在编译时就确定要执行的方法内容,重载是一种编译时多态,还有就是子类虽然重写了父类的方法,但是new时引用类型和实例类型都是子类
运行时多态:就是常说的多态,也就是父类引用指向子类实例,多态调用的时候会去找子类有没有重写该方法,如果有则直接调用子类的方法,没有则调原父类的
只要参数类型或是数量不一样时就一定是重载,当只有返回值类型不一致时不是重载
public:可以被所有包中的所有类访问
private:只能被当前类访问
protected:可以被同一个包中的类访问,也可以被其他包下的子类访问
不加修饰符:只能被同一个包中的类访问
定义方式:抽象类是通过abstract关键词定义,接口通过interface定义
继承方式:可以实现多个接口,但是只能继承一个抽象类
方法实现:接口要实现所有方法,抽象类只需要实现抽象方法
实例化:抽象类可以实例化有构造函数,接口不能
成员变量:抽象类可以有实例成员变量,接口只能有常量
BIO:同步阻塞IO,应用程序发起IO请求后,需要阻塞等待IO直到内核将数据拷贝到用户空间,只实用于少量客户端数量的情况
NIO:同步非阻塞IO,相比于BIO,NIO通过一直轮询内核IO数据是否加载完的方式,避免一直阻塞,但是盲轮询会消耗大量CPU资源,所以通过IO多路的复用让一个线程管理多个客户端连接,当客户端数据加载完后在为其服务,优化了盲轮询带来的问题
AIO:异步IO,也就是操作之后直接返回,不会阻塞,当后台处理完成,操作会通知响应线程进行后续操作,目前使用不是很广泛
类加载过程依次为:
1 程序计数器
2 虚拟机栈,装线程执行函数、基本数据类型的栈
3 本地方法栈,装的是c++实现的直接操作内存的native方法
4 堆,主要存储的是对象、字符串常量池、静态变量,也是GC垃圾回收的重点对象
5 方法区,存储的是类加载的数据,运行时常量池:主要存的是类在初始化时的各种字面量和符号引用
JVM垃圾回收算法有
标记清除:通过可达性分析标记删除或保留的对象,然后对通过标记进行删除,缺点就是会出现内存碎片,主要用于回收老年代回收数量较少的操作
标记复制:通过将内存分成两块,每次只用一块,每次回收时将存活的放入到没有用的那一块上再清空当前块,优点是无内存碎片,缺点是内存使用率只有50%,主要用于回收新生代回收数量较多的操作,后安的优化后就是将新生代分为三块,一块是Eden和两块survivor,每次分配内存只分配Eden和一个survivor,当发送垃圾回收时将保留的对象放到另一个未使用的survivor中,如果另一个survivor不够用,则申请老年代空间进行兜底,也是这样导致老年代没有兜底方案无法使用标记复制算法
标记整理:就是将存活的对象向一端移动,然后回收掉所有边界以外的内存,期间会发生对象的移动,优点就是内存利用率高,无内存碎片,缺点就是需要移动对象速度变慢
serial:stop world时单线程回收垃圾
parlNew:stop world时多线程回收垃圾
Parallel Scavenge
serial Old
Parallel Old
cms
g1
内存泄漏就是使用了某部分空间后,后续不再使用的情况却没有释放导致的,如c++需要手动释放内存内存泄漏的情况较多,java将内存的管理交给了jvm虚拟机,所以内存泄漏的情况交少,但也存在,例如threadLocal使用不当就会出现内存泄漏,threadLocalMap中的key是弱引用,value是强引用,弱引用会在下一次gc的时候被回收,所以可能出现key不存在,而value存在的情况,不过java已经帮我考虑到这点了,每次调用set、get、remove方法时会清理掉key为null的数据,最好每次用完后手动remove
1 当直接手动System.gc()方法时会触发,这个不是立即触发
2 当新生代创建大对象或大数组时,直接进入到老年代中时,此时老年代的内存不够的话就会触发full gc,full gc以后还是不够则会报OOM heap space,大对象直接进入到老年代的原因就是新生代存在大量内存碎片不易开出一个连续的大内存,如果大对象在新生代会降低时长发生的MinorGc的效率
3 当系统要加载类、方法、常量超过本机内存时,就会触发full gc
4 当发生Minor gc时,Eden存活的对象和原Susvivor From区中部分对象加入到Survivor To中导致Survivor To内存不足时,Survivor To将会将部分对象转移到老年代中,如果此时的老年代空间不足也会触发full gc
主要回收的是新生代的对象,新生代分为Eden(装新对象),Survivor From(装上一次Minor gc是存活下来的对象),Survor To(用于装下一次Minor gc存活在新生代的对象)
当Eden区已经满了触发Minor gc,使用标记复制法,将Eden和Survivor From中未进入到老年代的对象复制到Survivor To中,复制完后清空Eden和Survivor From,期间会发生部分Survivor From中的对象进入到老年代的情况,最后将Survivor From和Survivor To置换
默认的是Parallel Scavenge(新生代)+ Parallel Old(老年代)
Parallel Scavenge 和 Parallel类似,不同点是吞吐量优先,吞吐量 = 执行代码时间 / (执行代码时间+垃圾回收时间),Parallel Scavenge有两个参数,一个可以调节垃圾回收最长时间,一个可以调整吞吐量,如果是交互性的服务的话就调整垃圾回收的最长时间,如果是后台任务类型化的服务的话就调节吞吐量擦参数,还有一个可以开启jvm自己根据实际情况进行堆内存的划分实现最大吞吐或最小时间间隔
Parallel Scavenge的收集流程和Parallel一样,
步骤为:用户线程正常访问 -> 开始收集,新生代多线程进行复制算法,暂停用户线线程 -> 老年代收集器工作
Parallel Old
回收步骤和Parallel Scavenge类似,也是吞吐量优先的垃圾回收器,多线程对老年进行标记整理操作
强引用:内存不足也不会回收,而是报OOM
软引用:当内存不足时可以进行回收的对象
弱引用:下一次垃圾回收时就会被回收
虚引用:被虚引用的对象可以看作没有被引用过,唯一的作用就是配合引用队列实现某个对象在被回收掉时的通知
通过引用队列,可以及时获取被垃圾回收器回收的对象,并进行一些必要的处理操作
使用可达性分写法,从GCRoot节点开始向下搜索,搜索路径就是引用链条,如果某个对象没有在引用链上的话就是可以清除的对象
GCRoot:栈中变量引用的对象,静态变量引用的对象,所有被同步锁持有的变量,常量池中引用的对象
新生代:Eden、From、To,当新生代内存不足时会触发Minor GC,通过标记复制法将Eden和From中存活的且未进入到老年代的对象复制到To中,To变为新的From,From清空后变为新的To
老年代:存储经历过多次MinorGc进入到老年代的或者是大对象,当方法区满了、大对象进入老年代内存不足、新生代的对象进入老年代内存不足都会触发Full Gc
双亲委派就是要加载一个类时,加载器会将加载类的任务先交给父类执行,父类已经加载过的就不会重复加载直接返回给子类,若父类无法加载该类再由子类加载器加载该类,主要目的就是保证某一个类只加载一次
当某个父类的加载需要调用到子类时,这不得不打破双亲委派模型,或者是如Tomcat一个类可能要加载多份时也会打破,Tomcat中可以部署多个web项目,为了保证每个web项目互相独立
怎么打破:自定义类加载器,继承ClassLoader类,重写loadClass方法和findClass方法。
从上之下有:
skiplist的的每个节点是具有层数的,每个节点有后向指针,和前向指针数组,通过分层的思路用数据冗余实现了双向链表的二分查找,
zrank的实现就是先用类二分查找到第一个节点,在一直用第0层的前向指针往前找即可,查询过程不可能跳到比它大的节点上
使用逻辑过期策略,就是在缓存的数据对象里加一个逻辑过期的时间,在Redis中不设置缓存过期,
当去到某个数据时使用存储的逻辑时间判断是否已经过期
如果未过期则直接返回给用户
如果已经逻辑过期了,则先将旧数据返回给用户,启用一个内部线程去完成缓存重建的工作
假设有两个线程A和B更新时,可能会出现 线程A更新缓存 -> B更新缓存 -> B更新数据库 -> A更新数据库
最后的状态时B的缓存和A的数据库造成了缓存不一致
为什么不先更新数据库再更新数据库再更新缓存?
AOF就是一种逻辑日志,当Redis开启AOF时,每次成功更新或添加以后,会写入一条命令到AOF文件,类似于mysql的binlog,AOF因为是增量式的,所以当AOF文件到达一定的大小时会触发AOF重写,AOF重写就是将set同一个key的多条命令合并为一条命令,在Redis中重写将数据库中所有的key都重写一遍,是非常耗时的,为了重写过程不影响主进程处理命令,开启后台子进程进行重写,子进程与父进程共享数据,当父进程发生修改时会触发写时复制,就是将要修改的key复制出来,父子进程共享的内存数据不变,在子进程重写过程中的的更新操作的aof命令会暂时存到重写aof缓冲区,当重写完以后再将重写aof缓冲区的aof数据写到新的aof文件中,最后用新的aof文件替换旧aof文件,重写过程中主进程执行命令要做三件事:执行命令,写入到aof缓冲区,写入到aof重写缓冲区
当某个从节点第一次向主节点发送同步请求,会发生全量备份,主节点向从节点发送自己的rdb文件,从节点收到rdb文件后清空自身数据,读取rdb文件,第一次全量同步以后每次主节点执行更新操作时会异步的将更新命令给从服务器
偏移量是用于主从服务断开时同步的,信号里有一个偏移量,会根据该偏移量去一个环形缓冲区找未同步的数据,主节点将未同步的数据以aof命令形式发送给从节点从而完成同步
缓存穿透就是某个热key失效以后,大量的请求打到了DB
解决方法就是,在缓存重建时设置一个分布式锁,当缓存失效以后只有一个线程能进行重建,
使用Redission设置一个获取锁时间,获取锁时间到了后判断缓存是否重建成功,如果成功直接返回类似于单例模式的双重检索
若没有重建成功,则没有获取到锁的用户返回服务器·繁忙,获取到锁的用户进行缓存重建后再返回
当Redis主服务完成一条革新操作时会将更新操作发给从服务器执行同步,
当主从之间断网又连通后,从服务器会发送一个偏移量,主服务通过该偏移量结合一个环状结构的缓冲区判断从服务器未同步的数据并将未同步的发给从服务器
如果环状结构已经满了则执行全量同步
具体操作:
先删除缓存 -> 更新数据库 -> 延迟大于数据库更新的时间再删缓存
因为在更新数据库期间可能存在重建缓存为旧数据的情况,所以要延迟
先删除缓存再更新数据库,多线程的情况下可能会重建旧数据库里的缓存
先更新数据库再删除缓存,多线程情况下可能会读到与数据库不一致的缓存
延迟双删相对于以前的能保证最终一致性
使用mq或者Redis的pub/sub模式进行异步通知删除本地缓存,Redis的pub/sub模式不同于mq的一点就是如果订阅节点已经下线了就不会再发过去,不负责存储
消息,仅仅是一个转发消息的作用,刚好适用于异步停止删除其他节点的本地缓存
在32位系统的默认最大内存是3G,64位是本机内存
Redis的内存淘汰机制可以分为三种
0 默认淘汰策略就是不淘汰,当内存已经满了后,可以正常读,新增则会报错
1 淘汰设置了过期时间的,有四种:随机淘汰、优先淘汰更早过期的、lru淘汰、lfu淘汰
2 还有一种就是直接淘汰全部key,有三种:随机、lru、lfu
具体可以通过命令或者配置文件修改淘汰策略
LRU:通常的LRU是链表加哈希的数据结构,每次访问或新增元素时会将该元素放到链表头,套太机制就是淘汰链表尾部的元素,但是通常的LRU的操作好几部不适合需要高性能的Redis,所以Redis的LRU算法实现维护一个最近访问时间的字段,当要淘汰时就会随机挑选几个(如5个)选出其中上次访问离现在最久的key淘汰掉,但是这种方法无法避免缓存污染,就是最近访问了很多key一次,当内存不足时可能会淘汰掉访问次数很多但是刚好最近未访问的热点元元素
LFU:即最近最不常用算法,它的实现是沿用了LRU存储最近访问时间的24位字段,它的左16位是存储最近访问时间,右8位存储访问频次,不过这个频次并不是访问次数而是访问频次,每次访问一个key会先进行衰减操作,即对频次进行衰减,距离上次访问时间越长衰减的越多,衰减后就是增长,但是增长也不是+1操作,而是根据根据概率增加,如果这个key的频次越大就越难增加
过期判断:Redis的过期策略的实现就是Redis的除了会存储键值对的dict,还有一个dict是存储设置了过期时间的key的过期时间,判断某个key是否过期就是查询这个过期时间dict,如果key不在dict中则默认是永久的,如果在dict中则会判断当前时间是否大于过期时间,如果则不返回数据
过期删除策略:Redis对于已经过期的数据采取的清除策略是懒惰删除+定时删除,懒惰删除就会在查询的时候如果判断为不存在的就会同步或异步去删除,定期删除的步骤就是随机抽取 x(可配置)个key检查是否过期,如果过期则删除,如果过期的key在抽取的占比中超过25%,则会再抽取一次走一次流程,当然这个流程也不是无限循环的也会有一个时间限制,默认时间超过25s就会终止流程
**Redis 集群没有使用一致性hash, 而是引入了哈希槽的概念。**相较于一致性哈希,分哈希槽实现简单更适合小而美的Redis,一共分了2^14个槽,每个分片负责一部分
扩容:首先将新节点加入到集群中,遍历其他分片,将属于新节点的key用pipeline模式迁移到新节点,迁移完成后更新所有节点的哈希槽数据,新节点正式工作
收缩:下线节点,将下线节点的key迁移到重新分片后的节点上,迁移完后将其他节点的哈希槽数据更新,删除下线节点后收缩完成
故障转移:当某个从节点无法ping通主节点则为主观下线,当超过半数的从节点无法ping主节点则为客观下线,从节点进行投票选出新的主节点
惰性删除 + 定期删除
惰性删除:就是在获取某个 key 是会查询 key 的过期时间,如果 key 是过期的则将 key 删除掉,清空其内存
定期删除:每隔一段随机时间,Redis 会随机取20个key检查是否过期,当检查出过期的则删除,如果20个中有25%以上的过期的话,就会再抽取20个key重复一轮,这个定时任务也是有时间上限的,如果一直再重复到到达了时间上限的话就不会再进行下一轮了
我感觉应该没法取代,只是二者的使用地方不同,如果要保证可靠交付那只能用tcp,如果追求性能和一对多的场景使用udp
TCP协议处于传输层
面向连接:只能是一对一数据传输,使用三次握手建立连接,四次挥手断开连接
可靠传输:使用重试、滑动窗口、流量控制、拥塞控制保证可靠传输
字节流:将数据分成多个tcp报文,如果一个tcp报文未收到,即使其后面的报文收到了也不会交给应用层处理
IP协议:
IP协议位于网路层
通过ip地址在庞大的互联网中定位到目标主机的地址,ip协议将数据打包成ip报文,通过路由器找到目标ip地址下一个要到的路由器,
通过多个路由器后到达目标ip主机
这两个协议都是互联网中最基本且重要的协议
当 A 向 B 发送一条消息时,这个过程可以被分为如下几个步骤:
应用层封装数据。创建一个应用级别消息,将其封装为一个传输报文(TCP 报文或 UDP 报文)。
传输层封装数据。将应用层报文加上源端口和目的端口号(如果使用 TCP 协议则还需要加上序号、确认机制、错误检测码等信息)。
网络层封装数据。将传输层报文加上源 IP 地址和目的 IP 地址,并进行分片等操作,生成一个 IP 数据报。
数据链路层封装数据。在网络层 IP 数据报的基础上,添加数据链路层的首部和尾部,形成一个帧。
物理层发送数据。帧在物理层被转变成需要传输的物理信号,进入物理媒介进行传输。
物理层到达接收方。被接收方的物理层检测并读取物理信号,将其解码为帧。
数据链路层接收数据,并确保数据的完整性。在接收到帧后,数据链路层会检查帧起始和结束的标志,以及时长和数据传输的正确性,确认数据无误后,将其剥离数据链路层的首部和尾部。
网络层接收数据。网络层检查接收到的 IP 数据报的目的 IP 地址和协议类型,将其从分片中重新组装还原,并将数据报传递给传输层。
传输层接收数据。传输层确定目的端口号,检查接收到的序号,确认机制和错误检测码等信息,将数据报传递给高层应用。
至此,A 向 B 发送的消息就成功地传输到了 B,可以被高层应用程序接收和处理了。
最开始时客户端关闭,服务端监听端口
第一次握手:客户端发送一个随机序列号syn给服务端
第二次握手:服务端收到客户端的syn后,将该syn+1作为ack,再加上服务端的一个随机序列号返回给客户端
第三次握手:客户端收到ack后,将服务端的syn+1作为ack返回给服务端,此时客户端已经处于连接状态,服务端收到ack后也处于连接状态
关于握手失败的情况只要记住ack不会重传,只会是未收到ack的一方重传序列号,而不是发送ack的重发ack
IP地址长度不同:IPv4是32位地址,IPv6是128位地址。
地址空间不同:IPv4只有约42亿个地址,IPv6则有无穷多个可能的地址。
存储效率不同:IPv4地址有点浪费空间,而IPv6更节省空间。
安全性不同:IPv6比IPv4更安全,支持IPSec协议的加密和身份验证。
网络协议不同:IPv4使用ARP和RARP协议来管理MAC地址,而IPv6使用邻居发现协议。
兼容性不同:IPv6能够与IPv4协议兼容,IPv4只能通过特殊的网关实现IPv6互联网的访问。
在执行CPU密集型任务时,单核CPU的多线程与单线程效果差不多,多线程因为有上下文切换导致效率更低
同步队列里的就是正在竞争锁的线程节点,等待队列存储在Condition中进行等待唤醒的线程
唤醒等待队列的线程不会马上获取到锁,而是将其加入到同步队列进行锁竞争
Condition是aqs中的一个类部类,Condition类通过Lock类获取,aqs中有一个同步队列和多个等待队列,因为可以new多个Condition对象,一个Condition对象有一个等待队列
Condition的await方法就是同步队列的首节点(持有锁的线程)释放锁并加入到对应Condition对象的等待队列中,当其他线程执行Condition的signal()方法时,会选中一个线程加入到同步队列,被选中的线程获取到锁以后才会从其await方法中返回
Condition的signalAll()方法就是将Condition的等待队列中的所有线程加入到同步队列
主要有synchronize,在jvm层面通过一个监视器monitor实现对象锁和类锁
还有就是JUC包下的Lock接口,在api层面实现的显示锁,实现类如ReenteryLock
用 ReentrantLock 获取到Condition,当某个线程获取到锁后,当前状态需要等待其他线程,则调用 Condition 的await方法将锁释放进入到Condition的等待队列
当其他线程完成某个任务时就是sigalAll唤醒Condition上的等待线程,以此类推来实现线程同步,类似于wait和notify配合使用的功能,
类加锁获取到的是全局唯一类的锁,只有获取到该类锁的线程才能执行该类被synchronized修饰的静态方法,其他线程需要等待当前线程释放类锁,
对象锁就是对当前实例对象加锁,只有获取到该实例锁的线程才能执行被synchronized修饰的实例方法,二者之间不会冲突,因为一个是锁多项一个锁实例,
类锁的锁粒度大于对象锁
1 支持公平锁和非公平锁
在公平锁机制先申请锁的线程先获取到锁
非公平锁时随机挑选申请的锁的线程,无关申请顺序,可能出现一个线程永远获取不到锁的情况
默认使用非平锁,除非在构造方法中传入参数true
2 可以关联多个等待队列
一个ReentrantLock可以new出多个Condition,每个Condition都有一个等待队列,这样一个锁就可以关联到多个等待队列
3 支持中断、超时、尝试获取锁
中断锁lockInterruptibly():就是如果一个线程尝试获取锁在同步队列时,后面操作可以直接接调用该线程的interrupt()方法发起中断信号,该线程接收到中断信号后放弃获取锁直接返回,这样是为了防止某个线程一直获取不到锁的情况
超时尝试获取锁tryLock,如果没有设置超时时间,只获取一次,获取不到则立刻返回false,获取到则返回true,设置时间则在给定时间内未获取到才返回false
单线程好比一个人干一件事,多线程就是将一件事拆分为几件小事分配给多个人干,这样完成一件事的效率就提高了,多线程出现的主要原因就是充分发挥当前时代多核服务器的性能,如果是单线程的话每个时间点只有一个cpu工作,就空闲了大量cpu资源
线程池好比一个池子,当有线程需求时就向线程池请求线程,如果请求成功则拿到线程执行后序操作,当线程用完以后再将其归还给线程池,优点就是省去了重复创建和销毁的过程,缺点就是线程池会有数量限制,需要考虑业务设置线程的数量,一般用new ThreadPool实现线程池,核心参数有:核心线程数、最大线程数、非核心线程空闲的最大时间,时间单位、任务等待队列、饱和策略
1 初始化:刚刚创建完成
2 就绪:已经准备好,等待执行
3 运行:执行线程任务
4 阻塞:由于某些原因无法继续执行,暂时阻塞
5 等待:wait或sleep
6 终止:
在JVM中不存在任何一个非守护线程时,线程就会自动退出,守护线程有自己结束的能力,这个能力非守护线程不具备
应用场景:主要应用场景就是后台任务,如垃圾回收线程,当JVM要退出时就会直接结束垃圾回收线程直接退出
如果要将做一个后台任务的线程时,就设置thread.setDaemon(true)
不一定,在频繁写的情况下,乐观锁的自旋操作会占用大量cpu所以反而没有悲观锁快
四种状态
无锁: 对象头中不存在线程
偏向锁: 对象头中刚好有本线程,同一个线程对某个偏向锁第二次加锁时如何对象头的线程id等于自己则不用重复加锁。
轻量级锁: 当前对象头的线程id和当前加锁线程不一致就从偏向锁升级为轻量级锁
重量级锁: 当某个线程自旋获取多次后还是失败,则轻量级锁升级为重量级锁,线程获取失败后等待唤醒不会自旋 ,这一套唤醒等待交给操作系统实习,涉及线程状态的切换,所以重量级锁占用资源更大
1 继承Thread类,重写run方法
2 实现Runnable接口
3 实现Callable接口
1 Runnable无返回值,Callable有返回值
2 Runnable抛出的异常只能再内部处理,无法抛出,Callable可以向上抛出异常也可以内部处理
初始化、就绪、 运行、阻塞、 等待、终止
wait属于Object,sleep属于Thread类,sleep不释放锁,wait释放锁,
sleep到达时间后线程会自动苏醒,wait需要其他线程notify唤醒类似于线程之间通信
阻塞就是当前线程不满足继续执行下去的条件,要先暂停线程直到条件满足,如获取一个已经被占用的重量级锁就是阻塞
等待是线程是因为某种原因暂停执行,并释放占用的资源,直到其他线程童子它继续执行,等待常常涉及到线程之间的协调和通信,比如等待某个线程完成某项任务再继续执行当前线程,Java中是用wait、notify实现等待唤醒的
核心线程数
最大线程数(当等待队列满了以后,再有任务加入时就会新开线程直到等于最大线程数)
大于核心线程数的空闲线程的最久存活时间值
大于核心线程数的空闲线程的最久存活时间单位
等待队列
创建线程的接口
饱和拒绝策略
1 直接抛出异常拒绝新任务
2 任务由提交任务的线程自己执行,这意味着提交任务的线程会阻塞,可能影响整体性能
3 不做任何处理,直接丢弃
4 丢弃最早的未处理的任务
由界队列(ArrayBlockingQueue):容量自己设置
无界队列(LinkedBlockingQueue):容量为Integer.MAX
同步队列:没有容量,无限创建线程执行任务
延迟阻塞队列:内部采用堆按照延迟时间长短进行排序,会自动扩容也是无界的容量为Integer.MAX
线程池初始化创建一定数量的线程,任务提交后,
如果存在空闲线程,直接将指派空闲线程取执行任务,
如果不存在空闲线程且阻塞队列未满则加入到阻塞队列,
如果阻塞队列也满了且当前线程总数小于最大线程,则新建线程执行任务
如果线程总数已经到达最大线程数且阻塞队列也是满的则会触发饱和策略
1 任务自己内部捕获异常不让其向上抛
2 submit提交的future返回值用try-catch捕获异常
3 重写线程池的afterExecute方法,用execute提交
1 ThreadLocalMap中的key为ThreadLocal,value为具体存储的对象,key为弱引用,value为强引用,在线程结束后如果没有手动remove的话就会出现在ThreadLocalMap中key为null,而value有值的情况造成内存泄漏,key的强引用改成弱引用的原因就是为了每次get或put时检查key为null的然后移除为内存泄漏增加一层保障,根本原因就是ThreadLocalMap被每一个Thread所引用,如果不手动移除的话都会出现内存泄漏
2 信息错乱:线程池中的线程是复用的,如果每次使用完后不及时清理可能会导致信息错乱问题,新任务使用上一次任务的ThreadLocal存储的值
内存泄漏:ThreadLocalMap对象和Thread是相同的生命周期,而线程池中的肯定存在一定量的Thread,如果ThreadLocal不及时remove肯定会出现内存泄漏
在父类中定义执行方法的模板和抽象方法,在子类中实现父类的抽象方法,执行时直接调用父类的模板方法操作其他方法完成目标
Java中的ReenteryLock就是就是继承了aqs类,实现了aqs的锁操作的抽象方法,调用时用模板方法结合父类的任务队列实现和子类的锁实现完成目标
2.1 单一职责:核心思想就是一个类只负责做一件事,从而形成高内聚、低耦合,如果出现一个类有承担多种职责的情况,当某个职责发生变更时,可能会影响到其他职责的正常工作
2.2 开发封闭:核心思想是对扩展开发,对修改封闭
对扩展开放,意味着有新的需求或变化时,可以对原来嘚代码进行扩展,以使用新的情况
对修改封闭,就是类一旦设计完成,就可以独立完成工作,而不需要对类进行任何修改
开放封闭是建立在里氏替换原则上的
2.3 里氏替换
任何基类可以出现的地方,子类一定可以出现
2.4 接口分离
接口分离原则指在设计时采用多个与特定客户类有关的接口比采用一个通用的接口要好。即,一个类要给多个客户使用,那么可以为每个客户创建一个接口,然后这个类实现所有的接口;而不要只创建一个接口,其中包含所有客户类需要的方法,然后这个类实现这个接口。
如果Client A类需要改变所使用的Service接口中的方法,那么不但要改动Service接口和ServiceImp类,还要对ClientB类和ClientC类重新编译。
2.5 依赖倒置
依赖倒置原则(Dependence Inversion Principle)是程序要依赖于抽象接口,不要依赖于具体实现。简单的说就是要求对抽象进行编程,不要对实现进行编程,这样就降低了客户与实现模块间的耦合。
面向过程的开发,上层调用下层,上层依赖于下层,当下层剧烈变动时上层也要跟着变动,这就会导致模块的复用性降低而且大大提高了开发的成本。
面向对象的开发很好的解决了这个问题,一般情况下抽象的变化概率很小,让用户程序依赖于抽象,实现的细节也依赖于抽象。即使实现细节不断变动,只要抽象不变,客户程序就不需要变化。这大大降低了客户程序与实现细节的耦合度。
1.1 介绍:
主要的作用就是解决在大V发布博客时,造成的系统性性能瓶颈,比如一个千万粉丝的博主发布了一篇博文,发布以后会推送到大量的粉丝,大量粉丝收到后又会引起该博文的大量访问,大量访问后又会引起大量的点赞评论操作,而且一个博文不仅要拉取博文数据,同时还要拉取关注数据和点赞数据,所以一个博客的发布可能会引发一系列的高并发操作,Feed流系统拆分为了用户、关系、点赞、博客、推送共五个模块,针对五个模块使用各种合适的方式进行了性能优化,降低了大V发布时导致系统奔溃的可能
1.2 难点
就是博客点赞数频繁更新导致博客的数据缓存频繁更新删除,导致缓存命中率极低,后面看在到一个博主说的好像很多公司都有一个计数中台,
一般的解决方案是基于点赞数通常情况都是增长的,所以可以10个点赞的++操作合并成一个+10操作,而且一般不要求强一致性性要求最终一致性即可,
所以如果接收到点赞时先将计数暂存在本地如ConcurrentHashMap,key为博客id,value为新增值,定时(如10s)将计数异步给博客服务,博客服务再更新博客缓存,这样不用频繁更新缓存了
2.1 介绍
Godis是模仿Redis用Go实现的kv数据库,主要就是想进步学一下Redis的数据结构如跳表、一致性hash、TTC分布式事务等知识
完善Redis没有本地事务回滚的缺点
大概归并排序的思路,拆成多个数据,取没分的前1000,合并后再取最后的1000
1 冒泡排序:通过不断的交换无序区最大的放入有序取的首部,O(n^2),稳定
2 选择排序:选择无序区中最小的一个元素插入到有序区的尾部,O(n^2),不稳定
3 插入排序:将无序区中的数字插入到有序区中合适的位置,O(n^2) 稳定
4 归并排序:先一直递归到左右端点相等后开始回溯合并,O(nlogn),稳定
5 快速排序:选择一个基准x,将大于x的放在x右边,小于x的放在x左边,再以 x 划分区间,直到 l == r, O(nlogn) ,不稳定
6 堆排序:通过建立最大/小堆,将每次将堆顶的元素移到有序区的首部,再调整堆,以此往复即可排序, O(nlogn) ,不稳定
日志记录:在调用切入方法之前或后记录一些日志
缓存管理:在获取某个数据时查看是否有缓存有就直接返回,没有再调用方法返回并重建缓存
事务管理:在切入点方法前开启事务,在调用完后并提交事务
性能监控
异常处理
权限控制
IOC:即控制反转,将创建和管理对象的权利交给了Spring的IOC容器,用户不用再考虑如何创建和管理对象
实现原理:
**依赖注入 :**指的是容器在实例化对象的时候把它依赖的类注入给它。
依赖注入可以通过三种方式完成:
Spring中的单例Bean不是线程安全的。
单例Bean线程安全问题怎么解决呢? ⭐
三级缓存:
一级缓存:单例Bean池,保存实例化、属性赋值、初始化完成的可用Bean
二级缓存:早起曝光对象,用于保存实例化完成的对象
三级缓存:保存
程序先执行**preHandle()**方法,如果该方法的返回值为true,则程序会继续向下执行处理器中的方法,否则将不再向下执行。
在业务处理器(即控制器Controller类)处理完请求后,会执行**postHandle()**方法,然后会通过DispatcherServlet向客户端返回响应。
在DispatcherServlet处理完请求后,才会执行**afterCompletion()**方法。
work 工作模式:让多个消费者绑定到一个队列,共同消费队列中的消息
pub/sub 发布订阅模式:将消息路由到所有与该交换机绑定的Queue中
Routing 路由模式:将消息路由到BindKey和消息的RountingKey完全匹配的Queue中
Topic 主题模式:也就是支持模糊匹配的路由模式
消费者保证业务幂等性
消费端处理消息的业务逻辑保持幂等性。
比如你拿个数据要写库,先根据主键查一下,如果这数据有了,就别插入了
消费者用消息日志保证幂等
利用一张日志表来记录已经处理成功的消息的 ID,如果新到的消息 ID 已经在日志表中,那么就不再处理这条消息。
比如你是写 Redis,那没问题了,反正每次都是 set,天然幂等性。
注意:此时需要保证每个消息都有唯一的id。
网络:负责与客户端创建网络连接,维护当前节点和集群中其他节点的网络连接池
指令:响应执行客户端的指令,并返回指令执行结果
数据结构:k-v存储结构、锁、跳表
数据类型:string、set、zset
数据库:单数据库、本地事务、aof
集群:一致性哈希、TCC分布式事务、mset分布式命令
前置知识:
RWMutex是Go语言中的一个读写锁。读写锁是一种常用的并发控制机制,可以同时允许多个读操作,但只允许一个写操作。
RWMutex提供了三种方法来控制对共享资源的并发访问:
RLock()
方法用于获取读锁,允许多个goroutine同时获得读取权限。在没有写操作时,读锁可以被多个goroutine同时获取。RUnlock()
方法用于释放读锁。Lock()
方法用于获取写锁,一次只能有一个goroutine获得写入权限。当有其他goroutine持有读锁或写锁时,请求写锁的goroutine会被阻塞。Unlock()
方法用于释放写锁。通过使用RWMutex,可以实现对共享资源的并发读写控制,从而提高程序的性能和并发能力。
为什么要用分段锁?
Go中的Map的底层实现逻辑是一个只读Map(read)+新数据Map(dirty),每次读的时候先读read中的数据,如果不存在再对dirty加锁读dirty中的Map,
在写的时候如果read中存在则直接替换后return,如果不存在再往dirty中写,其中还有一个miss参数,每次在read没读到且dirty中有新数据时miss会自增,
当miss到达一定的值后会将dirty提升为read,将read中未被删除的数据复制到dirty中,read = dirty,dirty = nil,如果原来的read数据量很大,这个复制操作将
会影响性能,还有当下一次写操作到dirty时会涉及dirty初始化,dirty初始化时会将当前read中未删除的数据复制到dirty也会影响性能,所以使用分段的思想减小
一个read里存在大量数据的情况
具体实现:
dict 结构的实现:map[](分段数组)、count(dict存储元素数量)、shardCount(分段数量)
单 key 加分段锁:获取 key 对应的分段数组位置,对该分段进行加锁
多 key 加分段锁:获取多个 key 的对应分段锁位置,并将要加锁的分段的index进行排序(不排序可能导致死锁发生),释放锁逆序释放(因为可能会出现获取锁 1,获取锁 2,释放锁 2,执行锁 1 函数后续操作,再释放锁 1,如果不是逆序释放锁可能出现问题)
用一个Map结构存储设置了过期时间的 key 对应的过期时间
判定是否过期流程:若 key 不在 ttlMap 中直接视为永久有效,若 key 在 ttlMap 中则判断该 key 的过期日期是否大于 now(),大于则未过期,否则就已经过期了
ThreadLocal的内存泄漏,以及和线程池共用时可能存在的问题
ThreadLocal是一个Map结构,其key为线程,value为存储的值,key为弱引用,value为强引用,
CAP理论是分布式架构中重要理论:
P是分布式的前提,C和A是互斥的,所以只有两种,即CP或AP架构
Zookeeper是CP,Nacos是AP
Zookeeper是基于CP模式的,所以当主节点挂了后进行重新选举主键的过程中是无法向外提供服务的,Zookeeper是基于推拉模式更新客户端的数据的,当某个服务发生了变化,Zookeeper通知客户端发生变化的服务端,客户端再向Zookeeper拉取新的服务端的配置
综上所述,Nacos 更加适合用于微服务架构,而 ZooKeeper 更加适合用于分布式系统。