一些整理
clickhouse
Resilience4j源码解析-3 RateLimiter模块介绍及限流算法
Guava RateLimiter限流原理解析
Elasticsearch-基础介绍及索引原理分析
数据库整理(全)
幻读、间隙锁、行锁、next-key lock、加锁规则、间隙锁导致的死锁、隔离级别设置、for update的理解
应该如何合理的设置超时时间?
理解了RPC框架的超时实现原理和可能引入的副作用后,可以按照下面的方法进行超时设置:
-
设置调用方的超时时间之前,先了解清楚依赖服务的TP99响应时间是多少(如果依赖服务性能波动大,也可以看TP95),调用方的超时时间可以在此基础上加50%
-
如果RPC框架支持多粒度的超时设置,则:全局超时时间应该要略大于接口级别最长的耗时时间,每个接口的超时时间应该要略大于方法级别最长的耗时时间,每个方法的超时时间应该要略大于实际的方法执行时间
-
区分是可重试服务还是不可重试服务,如果接口没实现幂等则不允许设置重试次数。注意:读接口是天然幂等的,写接口则可以使用业务单据ID或者在调用方生成唯一ID传递给服务端,通过此ID进行防重避免引入脏数据
-
如果RPC框架支持服务端的超时设置,同样基于前面3条规则依次进行设置,这样能避免客户端不设置的情况下配置是合理的,减少隐患
-
如果从业务角度来看,服务可用性要求不用那么高(比如偏内部的应用系统),则可以不用设置超时重试次数,直接人工重试即可,这样能减少接口实现的复杂度,反而更利于后期维护
-
重试次数设置越大,服务可用性越高,业务损失也能进一步降低,但是性能隐患也会更大,这个需要综合考虑设置成几次(一般是2次,最多3次)
-
如果调用方是高QPS服务,则必须考虑服务方超时情况下的降级和熔断策略。(比如超过10%的请求出错,则停止重试机制直接熔断,改成调用其他服务、异步MQ机制、或者使用调用方的缓存数据)
-
深入了解Protocol Buffer 序列化原理大揭秘
为什么Protocol Buffer
的性能如此的好:
a. 序列化速度 & 反序列化速度快
b. 数据压缩效果好,即序列化后的数据量体积小
进程间通信,linux:
(1)管道(pipe)和有名管道(FIFO)
(2)信号(signal)
(3)消息队列
(4)共享内存
(5)信号量
(6)套接字(socket)
tcp连接建立的时候3次握手,断开连接的4次握手
建立:
client:我要连。
server:知道了,连上了。
client:好的,确实连上了!
断开:
client:我要断(仍可接数据)。TIME_WAIT开始,2倍MSL(最大报文段生存时间,可以保证本次连接的所有数据都从网络中消失,不被下一次连接接收。)。
server:知道了。(不再发数据)
server:断。
client:好的。或者超时后,自动断开。
epoll与select的区别
1. select在一个进程中打开的最大fd文件句柄是2048。 epoll则没有这个限制,1G内存都能达到大约10w左右。
2. select的fd轮询机制。epoll是一旦某个fd数据就绪时,内核会callback回调,迅速激活这个文件描述符。
3. 内核把FD消息通知给用户空间,select则做了不必要的拷贝。epoll是通过内核与用户空间mmap同一块内存实现的。
tcp和udp的区别
TCP:是面向连接。有验证重发机制,因此不会出现丢失或乱序。
UDP:是无连接的,不验重,无须等待对方的应答,会出现丢失、重复、乱序。UDP段结构比TCP的段结构简单,网络开销小。
Java NIO
IO与NIO区别
Leaf——美团点评分布式ID生成系统
MySQL主从复制–原理
Mysql主从分离介绍及实现
MySQL中性能瓶颈定位Show命令
Mysql> show status ——显示状态信息(扩展show status like ‘XXX’)
Mysql> show variables ——显示系统变量(扩展show variables like ‘XXX’)
Mysql> show innodb status ——显示InnoDB存储引擎的状态
Mysql> show processlist ——查看当前SQL执行,包括执行状态、是否锁表等
Shell> mysqladmin variables -u username -p password——显示系统变量
Shell> mysqladmin extended-status -u username -p password——显示状态信息
Mysql慢查询日志
/path/mysqldumpslow -s c -t 10 /database/mysql/slow-query.log
这会输出记录次数最多的10条SQL语句,其中:
-s, 是表示按照何种方式排序,c、t、l、r分别是按照记录次数、时间、查询时间、返回的记录数来排序,ac、at、al、ar,表示相应的倒叙;
-t, 是top n的意思,即为返回前面多少条的数据;
-g, 后边可以写一个正则匹配模式,大小写不敏感的;
MySQL中使用explain分析sql语句执行效率
通过explain命令可以得到:
– 表的读取顺序
– 数据读取操作的操作类型
– 哪些索引可以使用
– 哪些索引被实际使用
– 表之间的引用
– 每张表有多少行被优化器查询
显示项中:type:显示查询使用了何种类型。从最好到最差类型为system、const、eq_reg、ref、range、index和ALL
system、const:可以将查询的变量转为常量. 如id=1; id为 主键或唯一键.
eq_ref:访问索引,返回某单一行的数据.(通常在联接时出现,查询使用的索引为主键或惟一键)
ref:访问索引,返回某个值的数据.(可以返回多行) 通常使用=时发生
range:这个连接类型使用索引返回一个范围中的行,比如使用>或<查找东西,并且该字段上建有索引时发生的情况(注:不一定好于index)
index:以索引的顺序进行全表扫描,优点是不用排序,缺点是还要全表扫描
ALL:全表扫描,应该尽量避免
显示项中:Extra关于MYSQL如何解析查询的额外信息,主要有以下几种
using index:只用到索引,可以避免访问表.
using where:使用到where来过虑数据. 不是所有的where clause都要显示using where. 如以=方式访问索引.
using tmporary:用到临时表
using filesort:用到额外的排序. (当使用order by v1,而没用到索引时,就会使用额外的排序)
range checked for eache record(index map:N):没有好的索引.
MySQL中使用SHOW PROFILE命令分析性能的用法整理
MysqlSQL查询优化
1、FROM子句中顺序:数据库的解析器按照从右到左的顺序处理FROM子句中的表名,如果三个表是有关系的话,将引用最多的表,放在最后,
select emp.empno,emp.ename,emp.sal,salgrade.grade,dept.dname from salgrade,dept,emp where (emp.deptno = dept.deptno) and (emp.sal between salgrade.losal and salgrade.hisal)
2、WHERE子句中的连接顺序
数据库采用自右而左的顺序解析WHERE子句,根据这个原理,表之间的连接必须写在其他WHERE条件之左,那些可以过滤掉最大数量记录的条件必须写在WHERE子句的之右。
emp.sal可以过滤多条记录,写在WHERE字句的最右边
select emp.empno,emp.ename,emp.sal,dept.dnamefrom dept,empwhere (emp.deptno = dept.deptno) and (emp.sal > 1500)
3、SELECT子句中避免使用*号,*要通过查询数据字典完成
4、用TRUNCATE替代DELETE,DELETE是一条一条记录的删除,而Truncate是将整个表删除
5、多使用内部函数提高SQL效率,例如使用mysql的concat()函数会比使用|| 快,因为concat()函数已经被mysql优化过了。
6、避免在索引列上使用NOT、!、<>、LIKE关键字和%x%通配符,会执行全表扫描
7、避免在索引列上使用函数,WHERE子句中,如果索引列是函数的一部分,优化器将不使用索引而使用全表扫描
8、用>=
替代 >
低效:SELECT * FROM EMP WHERE DEPTNO > 3 首先定位到DEPTNO=3的记录并且扫描到第一个DEPT大于3的记录高效:SELECT * FROM EMP WHERE DEPTNO >= 4 直接跳到第一个DEPT等于4的记录
9、用IN替代OR
select * from emp where sal = 1500 or sal = 3000 or sal = 800;select * from emp where sal in (1500,3000,800);
10、使用索引的第一个列,最左前缀原则
11、使用连接(JOIN)来代替子查询(Sub-Queries),因为MySQL不需要在内存中创建临时表来完成这个逻辑
12、创建字段宽度合适,能用数值不用字符。
mysql性能优化-慢查询分析、优化索引和配置
关于distinct 和group by的去重逻辑
distinct需要将col列中的全部内容都存储在一个内存中,可以理解为一个hash结构,key为col的值,最后计算hash结构中有多少个key即可得到结果,内存占用大。
而group by的方式是先将col排序。而数据库中的group一般使用sort的方法,即数据库会先对col进行排序。而排序的基本理论是,时间复杂为nlogn,空间为1.,然后只要单纯的计数就可以了。优点是空间复杂度小,缺点是要进行一次排序
Mysql的InnoDB架构
InnoDB整体也分为三层:
(1)内存结构(In-Memory Structure),这一层在MySQL服务进程内;
(2)OS Cache,这一层属于内核态内存;
(3)磁盘结构(On-Disk Structure),这一层在文件系统上,主要包括日志与表空间;
InnoDB内存结构包含四大核心组件,分别是:
(1)缓冲池(Buffer Pool);加速读请求,避免每次数据访问都进行磁盘IO。技术点包括:预读,局部性原理,LRU,预读失败+缓冲池污染,新生代老生代双链LRU…细节参见《缓冲池(buffer pool),彻底懂了!》
(2)写缓冲(Change Buffer);加速写请求,避免每次写入都进行磁盘IO。细节参见《写缓冲(change buffer),彻底懂了!》
(3)自适应哈希索引(Adaptive Hash Index);加速读请求,减少索引查询的寻路路径。技术点包括:聚集索引,普通索引,哈希索引…细节参见《InnoDB到底支不支持哈希索引》。
(4)日志缓冲(Log Buffer);极大优化redo日志性能,并提供了高并发与强一致性的折衷方案。技术点包括:redo log作用,流程,三层架构,随机写优化为顺序写,次次写优化为批量写…细节参见《事务已提交,数据却丢了,赶紧检查下LogBuffer》。
PAXOS协议
一句话概括Paxos:将所有节点都写入同一个值,且被写入后不再更改。
角色:proposer、acceptor、Learner
1、proposer将发起提案(value,带proposer_id)给所有accpetor;
2、超过半数accpetor接收(proposer_id最大原则),且将自身议案信息(带议案id)返回给proposer;
3、proposer整合收集到的议案(议案id最大原则),将议案发accept请求给accpetor;
4、如果acceptor收到了proposer的accept请求时,还没有收到其他proposer更高编号的promise请求时,将会批准proposer的建议,并将自己决定告知learner;
5、Learner收到了某个acceptor的accept信息后,会统计一共收到了多少个acceptor发过来的该proposal的accept信息,若该proposal的acceptor数量达到了quorum,那么就认定该proposal对应的值为 X的值,且后续不允许再修改。
RAFT 与PAXOS区别
相同点:1、过半数同意。2、提案id最大原则
不同点:raft协议日志连续性
Raft协议强调日志的连续性,multi-paxos则允许日志有空洞。日志的连续性蕴含了这样一条性质:如果两个不同节点上相同序号的日志,term相同,那么这和这之前的日志必然也相同的。
两者的区别在于Leader确认提交和获取所有可以被提交日志的方式上,而方式上的区别又是由于是日志是否连续造成的,Raft协议利用日志连续性,简化了这个过程。
分布式理论(七)—— 一致性协议之 ZAB
J.U.C包
自旋锁:
1. 如果某个线程持有锁的时间过长,就会导致其它等待获取锁的线程进入循环等待,消耗CPU。使用不当会造成CPU使用率极高。
2. 上面Java实现的自旋锁不是公平的,即无法满足等待时间最长的线程优先获取锁。不公平的锁就会存在“线程饥饿”问题。
3. 自旋锁不会使线程状态发生切换,一直处于用户态,即线程一直都是active的;不会使线程进入阻塞状态,减少了不必要的上下文切换,执行速度快
线程池工作策略
(1)如果没有空闲的线程执行该任务且当前运行的线程数少于corePoolSize,则添加新的线程执行该任务。
(2)如果没有空闲的线程执行该任务且当前的线程数等于corePoolSize同时阻塞队列未满,则将任务入队列,而不添加新的线程。
(3)如果没有空闲的线程执行该任务且阻塞队列已满同时池中的线程数小于maximumPoolSize,则创建新的线程执行任务。
(4)如果没有空闲的线程执行该任务且阻塞队列已满同时池中的线程数等于maximumPoolSize,则根据构造函数中的handler指定的策略来拒绝新的任务。
不同线程池使用场景
newCachedThreadPool:使用SynchronousQueue作为阻塞队列,队列无界,线程数无上限,线程的空闲时限为60秒。这种类型的线程池非常适用IO密集的服务,因为IO请求具有密集、数量巨大、不持续、服务器端CPU等待IO响应时间长的特点。服务器端为了能提高CPU的使用率就应该为每个IO请求都创建一个线程,以免CPU因为等待IO响应而空闲。
public
static
ExecutorService newCachedThreadPool() {
return
new
ThreadPoolExecutor(
0
, Integer.MAX_VALUE,//
60L, TimeUnit.SECONDS,
new
SynchronousQueue<Runnable>());
}
newFixedThreadPool:需指定核心线程数,核心线程数和最大线程数相同,使用LinkedBlockingQueue 作为阻塞队列,队列无界,线程空闲时间0秒。这种类型的线程池可以适用CPU密集的工作,在这种工作中CPU忙于计算而很少空闲,由于CPU能真正并发的执行的线程数是一定的(比如四核八线程),所以对于那些需要CPU进行大量计算的线程,创建的线程数超过CPU能够真正并发执行的线程数就没有太大的意义。
newSingleThreadExecutor:池中只有一个线程工作,阻塞队列无界,它能保证按照任务提交的顺序来执行任务。
线程数公式
线程池设定最佳线程数目 = ((线程池设定的线程等待时间+线程 CPU 时间)/
线程 CPU 时间 )* CPU 数目
ThreadPoolExecutor 提 供 了 动 态 调 整 线 程 池 容 量 大 小 的 方 法 : setCorePoolSize() 和 setMaximumPoolSize()
线程拒绝策略
1、AbortPolicy:直接抛出异常,默认策略;
2、CallerRunsPolicy:用调用者所在的线程来执行任务;
3、DiscardOldestPolicy:丢弃阻塞队列中靠最老的任务,并执行当前任务;
4、DiscardPolicy:直接丢弃任务;
线程池的监控
如果在项目中大规模的使用了线程池,那么必须要有一套监控体系,来指导当前线程池的状态,当出现问题的时候可以快速定位到问题。而线程池提供了相应的扩展方法,我们通过重写线程池的 beforeExecute、afterExecute 和 shutdown 等方式就可以实现对线程的监控
public class Demo1 extends ThreadPoolExecutor {// 保存任务开始执行的时间,当任务结束时,用任务结束时间减去开始时间计算任务执行时间private ConcurrentHashMap<String, Date> startTimes;public Demo1(int corePoolSize, int maximumPoolSize, longkeepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue) {super(corePoolSize, maximumPoolSize, keepAliveTime, unit,workQueue);this.startTimes = new ConcurrentHashMap<>();}@Overridepublic void shutdown() {System.out.println("已经执行的任务数:"+this.getCompletedTaskCount()+", " +"当前活动线程数:" + this.getActiveCount() + ",当前排队线程数:"+this.getQueue().size());System.out.println();super.shutdown();}//任务开始之前记录任务开始时间@Overrideprotected void beforeExecute(Thread t, Runnable r) {startTimes.put(String.valueOf(r.hashCode()), new Date());super.beforeExecute(t, r);}@Overrideprotected void afterExecute(Runnable r, Throwable t) {Date startDate = startTimes.remove(String.valueOf(r.hashCode()));Date finishDate = new Date();long diff = finishDate.getTime() - startDate.getTime();// 统计任务耗时、初始线程数、核心线程数、正在执行的任务数量、// 已完成任务数量、任务总数、队列里缓存的任务数量、// 池中存在的最大线程数、最大允许的线程数、线程空闲时间、线程池是否关闭、线程池是否终止System.out.print("任务耗时:" + diff + "n");System.out.print("初始线程数:" + this.getPoolSize() + "n");System.out.print("核心线程数:" + this.getCorePoolSize() + "n");System.out.print("正在执行的任务数量:" + this.getActiveCount() + "n");System.out.print("已经执行的任务数:"+this.getCompletedTaskCount()+"n ");System.out.print("任务总数:" + this.getTaskCount() + "n");System.out.print("最大允许的线程数:" + this.getMaximumPoolSize() + "n");System.out.print("线程空闲时间:"+this.getKeepAliveTime(TimeUnit.MILLISECONDS)+"n ");System.out.println();super.afterExecute(r, t);}public static ExecutorService newCachedThreadPool() {return new Demo1(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, newSynchronousQueue());}/**
* 测试
*/public class Test implements Runnable{private static ExecutorService es =Demo1.newCachedThreadPool();@Overridepublic void run() {try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}public static void main(String[] args) throws Exception {for (int i = 0; i < 100; i++) {es.execute(new Test());}es.shutdown();}
}
AQS的基本原理
AQS就是基于CLH队列,用volatile修饰共享变量state,线程通过CAS去改变状态符,成功则获取锁成功,失败则进入等待队列,等待被唤醒。AQS 定义了两种资源共享方式:
1.Exclusive:独占,只有一个线程能执行,如ReentrantLock
2.Share:共享,多个线程可以同时执行,如Semaphore、CountDownLatch
AQS使用一个整数state以表示状态,并通过getState、setState及compareAndSetState等protected类型方法进行状态转换。巧妙的使用state,可以表示任何状态,如:
- ReentrantLock用state表示所有者线程已经重复获取该锁的次数。
- Semaphore用state表示剩余的许可数量。
- CountDownLatch用state表示闭锁的状态,如关闭、打开。
- FutureTask用state表示任务的状态,如尚未开始、正在运行、已完成、已取消。
//AQS中控制同步状态的state变量
public abstract class AbstractQueuedSynchronizerextends AbstractOwnableSynchronizer {private volatile int state;protected final int getState() {return state;}protected final void setState(int newState) {state = newState;}//对state变量进行CAS 操作protected final boolean compareAndSetState(int expect, int update) {return unsafe.compareAndSwapInt(this, stateOffset, expect, update);}}
boolean acquire () throws InterruptedException {while (当前状态不允许获取操作) {if (需要阻塞获取请求) {如果当前线程不在队列中,则将其插入队列阻塞当前新城}else返回失败}可能更新同步器的状态如果当前线程在队列中,则将其移出队列返回成功
}
void release () {更新同步器的状态if (新的状态允许某个被阻塞的线程获取成功)接触队列中一个或多个线程的阻塞状态
}
Java技术之AQS详解
AQS分析
深入剖析java并发之阻塞队列LinkedBlockingQueue与ArrayBlockingQueue
插入方法:
add(E e) : 添加成功返回true,失败抛IllegalStateException异常
offer(E e) : 成功返回 true,如果此队列已满,则返回 false。
put(E e) :将元素插入此队列的尾部,如果该队列已满,则一直阻塞
删除方法:
remove(Object o) :移除指定元素,成功返回true,失败返回false
poll() : 获取并移除此队列的头元素,若队列为空,则返回 null
take():获取并移除此队列头元素,若没有元素则一直阻塞。
检查方法:
element() :获取但不移除此队列的头元素,没有元素则抛异常
peek() :获取但不移除此队列的头;若队列为空,则返回 null。
————————————————
ArrayBlockingQueue
内部的阻塞队列是通过一个重入锁ReenterLock和两个Condition条件队列实现的,所以ArrayBlockingQueue中的元素存在公平访问与非公平访问的区别
/** 控制并非访问的锁 */
final ReentrantLock lock;
/**notEmpty条件对象,用于通知take方法队列已有元素,可执行获取操作 */
private final Condition notEmpty;
/**notFull条件对象,用于通知put方法队列未满,可执行添加操作 */
private final Condition notFull;
LinkedBlockingQueue
吞吐量要高于ArrayBlockingQueue,因为其内部实现添加和删除操作使用的两个ReenterLock来控制并发执行,而ArrayBlockingQueue内部只是使用一个ReenterLock控制并发
/** 获取并移除元素时使用的锁,如take, poll, etc */
private final ReentrantLock takeLock = new ReentrantLock();
/** notEmpty条件对象,当队列没有数据时用于挂起执行删除的线程 */
private final Condition notEmpty = takeLock.newCondition();
/** 添加元素时使用的锁如 put, offer, etc */
private final ReentrantLock putLock = new ReentrantLock();
/** notFull条件对象,当队列数据已满时用于挂起执行添加的线程 */
private final Condition notFull = putLock.newCondition();
————————————————
区别:
1.队列大小有所不同,ArrayBlockingQueue必须指定大小,而LinkedBlockingQueue可以是有界、也可以无界的(Integer.MAX_VALUE),对于后者而言,当添加速度大于移除速度时,在无界的情况下,可能会造成内存溢出等问题。
2.由于ArrayBlockingQueue采用的是数组的存储容器,因此在插入或删除元素时不会产生或销毁任何额外的对象实例,而LinkedBlockingQueue则会生成一个额外的Node对象。这可能在长时间内需要高效并发地处理大批量数据的时,对于GC可能存在较大影响。
3.两者的实现队列添加或移除的锁不一样,ArrayBlockingQueue实现的队列中的锁是没有分离的,即添加操作和移除操作采用的同一个ReenterLock锁,而LinkedBlockingQueue实现的队列中的锁是分离的,其添加采用的是putLock,移除采用的则是takeLock,这样能大大提高队列的吞吐量,也意味着在高并发的情况下生产者和消费者可以并行地操作队列中的数据,以此来提高整个队列的并发性能。
Fail-Fast 和 Fail-Safe
Fail Fast Iterator | Fail Safe Iterator | |
---|---|---|
Throw ConcurrentModification Exception | Yes | No |
Clone object | No | Yes |
Memory Overhead | No | Yes |
Examples | HashMap,Vector,ArrayList,HashSet | CopyOnWriteArrayList, ConcurrentHashMap |
spring-aop-proxy代码分析.png
spring容器-aop.png
Spring 启动流程refresh()源码解析
请别再问Spring Bean的生命周期了!
Spring Bean的生命周期分为四个阶段
和多个扩展点
。扩展点又可以分为影响多个Bean
和影响单个Bean
。整理如下:
四个阶段
- 实例化 Instantiation
- 属性赋值 Populate
- 初始化 Initialization
- 销毁 Destruction
多个扩展点
- 影响多个Bean
- BeanPostProcessor
- InstantiationAwareBeanPostProcessor
- 影响单个Bean
- Aware
- Aware Group1
- BeanNameAware
- BeanClassLoaderAware
- BeanFactoryAware
- Aware Group2
- EnvironmentAware
- EmbeddedValueResolverAware
- ApplicationContextAware(ResourceLoaderAwareApplicationEventPublisherAwareMessageSourceAware)
- Aware Group1
- 生命周期
- InitializingBean:执行配置的init-method
- DisposableBean:执行DisposableBean接口的destroy方法;
执行配置的destroy-method。
- Aware
Spring中BeanFactory和ApplicationContext的生命周期及其区别
springAOP的实现原理
1. @EnableAspectJAutoProxy开启注解功能,并在容器中注册一个组件——AnnotationAwareAspectJProxyCreator,这个组件实现了SmartInstantiationAwareBeanPostProcessor,是一个后置处理器;
2. 在创建springIOC容器时,有一个步骤是refresh()即刷新容器,方法中有一步是registerBeanPostProcessors,这一步会初始化所有的后置处理器,就是在这时生成了AnnotationAwareAspectJProxyCreator组件;
3. refresh后面还有一步,finishBeanFactoryInitilization,即初始化剩下的单实例bean,实例化目标类组件和切面类组件;
4. AnnotationAwareAspectJProxyCreator会对目标类组件和切面类组件进行拦截,即在这些组件创建完成并初始化之后,调用postProcessAfterInitialization方法,判断目标类组件是否需要增强,如果需要,会将切面类的通知方法包装成增强器(Advisor),然后用cglib动态代理(如果目标类实现了接口,也可以使用jdk动态代理)给目标类对象创建一个代理对象,这个代理对象中就有上述增强器;
5. 经过2-4步,容器就创建完毕,接下来代理对象执行目标方法,首先获取目标方法的拦截器链(即MethodInterceptor,由增强器包装而来),利用拦截器的链式机制依次进入每一个拦截器进行增强;
6. 拦截器链的执行效果
目标方法成功:前置通知 → 目标方法 → 后置通知 → 返回通知;
目标方法抛出异常:前置通知 → 目标方法 → 后置通知 → 异常通知。
基于注解的声明式事务的原理
1. 在容器配置类上使用@EnableTransactionManagement注解,该注解在容器中注册了两大组件——AutoProxyRegistrar、ProxyTransactionManagementConfiguration;
2. AutoProxyRegistrar通过导入方式在容器中注册了InfrastructureAdvisorAutoProxyCreator,后置处理器;
3. ProxyTransactionManagementConfiguration本身就是一个容器配置类,它注册了transactionAdvisor(事务增强器),然后又在这个事务增强器中注入了两个属性transactionAttributeSource、transactionInterceptor:
a. transactionAttributeSource用于解析@Transactional注解的各种属性;
b. transactionInterceptor实现了MethodInterceptor,是一个拦截器链,这个拦截器链会从容器中获取事务管理器,利用事务管理器,在目标方法发生异常时执行回滚,在目标发生正常完成后提交事务;
4. 第2步的InfrastructureAdvisorAutoProxyCreator后置处理器,会在目标对象创建完成之后调用postProcessAfterInitialization方法,对需要增强的代理对象用cglib或jdk动态代理,将其包装为代理对象;
5、代理对象在执行目标方法时,会首先获取拦截器链,这个拦截器链就是第3.b步的transactionInterceptor。
JDK和CGLIB动态代理区别
在Spring中循环依赖处理分为3种情况
spring对象三级缓存:
三级:singletonFactories : 单例对象工厂的cache
二级: earlySingletonObjects :提前暴光的单例对象的Cache 。【用于检测循环引用,与singletonFactories互斥】
一级: singletonObjects:单例对象的cache
1. 构造器循环依赖(无法解决)
因为加入singletonFactories三级缓存的前提是执行了构造器,所以构造器的循环依赖没法解决
2. setter循环依赖(可以解决)
解决方式:Spring容器提前暴露刚完成构造器注入,但未完成属性注入(setter方法)的bean来完成的。
3. prototype范围的依赖处理(无法解决)
因为spring容器不进行缓存prototype作用域的bean
40个Java集合面试问题和答案
java队列——queue详细分析
浅谈AVL树,红黑树,B树,B+树原理及应用
数据库分库分表详解
高并发下数据库分库分表面试题整理
关键字final的好处小结
- final关键字提高了性能。JVM和Java应用都会缓存final变量。
- final变量可以安全的在多线程环境下进行共享,而不需要额外的同步开销。
- 使用final关键字,JVM会对方法、变量及类进行优化。
- 对于不可变类,它的对象是只读的,可以在多线程环境下安全的共享,不用额外的同步开销。
jvm参数
参数 | 含义 |
---|---|
-Xms | 初始堆大小 |
-Xmx | 最大堆空间 |
-Xmn | 设置新生代大小 |
-XX:SurvivorRatio | 设置新生代eden空间和from/to空间的比例关系 |
-XX:PermSize | 方法区初始大小 |
-XX:MaxPermSize | 方法区最大大小 |
-XX:MetaspaceSize | 元空间GC阈值(JDK1.8) |
-XX:MaxMetaspaceSize | 最大元空间大小(JDK1.8) |
-Xss | 栈大小 |
-XX:MaxDirectMemorySize | 直接内存大小,默认为最大堆空间 |
Java各种反射性能对比
1. directGet 100000000 times using 37 ms
bean.getName();
2. reflectAsmGet 100000000 times using 39 ms
MethodAccess methodAccess = MethodAccess.get(SimpleBean.class);
methodAccess.invoke(bean, "getName");
3. javaReflectGet 100000000 times using 222 ms
Method getName = SimpleBean.class.getMethod("getName");
getName.invoke(bean);
4. propertyGet 100000000 times using 335 ms
BeanInfo beanInfo = Introspector.getBeanInfo(SimpleBean.class);
PropertyDescriptor[] propertyDescriptors = beanInfo.getPropertyDescriptors();
for (PropertyDescriptor propertyDescriptor : propertyDescriptors) {if (propertyDescriptor.getName().equals("name")) {method = propertyDescriptor.getReadMethod();break;}
}
method.invoke(bean);
5. beanUtilsGet 100000000 times using 20066 ms
BeanUtils.getProperty(bean, "name");
Tomcat 类加载器之为何违背双亲委派模型
启动类加载器(Bootstrap ClassLoader):加载JAVA_HOME/lib 目录
扩展类加载器(Extension ClassLoader):加载JAVA_HOME/lib/ext 目录
应用程序类加载器(Application ClassLoader)
双亲委派模型:所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个请求(他的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载。
如何破坏双亲委任模型
第一次:在双亲委派模型出现之前—–即JDK1.2发布之前。
第二次:典型的例子就是JNDI、JDBC,它的代码由启动类加载器去加载(在JDK1.3时就放进去的rt.jar),但它需要调用由独立厂商实现并部署在应用程序的ClassPath下的JNDI接口提供者(SPI, Service Provider Interface)的代码,但启动类加载器不可能“认识“这些代码啊。因为这些类不在rt.jar中,但是启动类加载器又需要加载。怎么办呢:线程上下文类加载器(Thread Context ClassLoader)。这个类加载器可以通过java.lang.Thread类的setContextClassLoader方法进行设置。如果创建线程时还未设置,它将会从父线程中继承一个,如果在应用程序的全局范围内都没有设置过多的话,那这个类加载器默认即使应用程序类加载器。
第三次,为了实现热插拔,热部署,模块化,意思是添加一个功能或减去一个功能不用重启,只需要把这模块连同类加载器一起换掉就实现了代码的热替换。
Tomcat 如何实现自己独特的类加载机制,tomcat 为了实现隔离性,没有遵守双亲委派的约定,每个webappClassLoader加载自己的目录下的class文件,不会传递给父类加载器。
ConcurrentHashMap 1.8为什么要使用CAS+Synchronized取代Segment+ReentrantLock
从 synchronized 到 CAS 和 AQS
jstack(查看线程)、jmap(查看内存)和jstat(性能分析)命令
jstack [ option ] pid
jstack [ option ] executable core
jstack [ option ] [server-id@]remote-hostname-or-IP
最常用的还是jstack pid
死锁,Deadlock(重点关注)
等待资源,Waiting on condition(重点关注)
• 等待获取监视器,Waiting on monitor entry(重点关注)
阻塞,Blocked(重点关注)
• 执行中,Runnable
• 暂停,Suspended
• 对象等待中,Object.wait() 或 TIMED_WAITING
• 停止,Parked
JVM内存逃逸
逃逸分析的基本行为就是分析对象动态作用域:当一个对象在方法中被定义后,它可能被外部方法所引用,例如作为调用参数传递到其他地方中,称为方法逃逸。
防止内存逃逸:
调整虚拟机参数,进行一些高效的优化。
1. 栈上分配
如果能够通过逃逸分析确定某些对象不会逃出方法之外,那就可以让这个对象在栈上分配内存,这样该对象所占用的内存空间就可以随栈帧出栈而销毁,就减轻了GC垃圾回收的压力。
2. 同步消除
线程同步本身比较耗时,如果确定一个变量不会逃逸出线程,无法被其它线程访问到,那这个变量的读写就不会存在竞争,对这个变量的同步措施可以清除。
3. 标量替换
Java虚拟机中的原始数据类型(int,long等数值类型以及reference类型等)都不能再进一步分解,它们就可以称为标量。相对的,如果一个数据可以继续分解,那它称为聚合量,Java中最典型的聚合量是对象。如果逃逸分析证明一个对象不会被外部访问,并且这个对象是可分解的,那程序真正执行的时候将可能不创建这个对象,而改为直接创建它的若干个被这个方法使用到的成员变量来代替。拆散后的变量便可以被单独分析与优化,可以各自分别在栈帧或寄存器上分配空间,原本的对象就无需整体分配空间了。
HotSpot虚拟机
由于HotSpot虚拟机目前的实现方法导致栈上分配实现起来比较复杂,因为在HotSpot中暂时还没有做这项优化。
相关JVM参数
-XX:+DoEscapeAnalysis 开启逃逸分析
-XX:+PrintEscapeAnalysis 开启逃逸分析后,可通过此参数查看分析结果。
-XX:+EliminateAllocations 开启标量替换
-XX:+EliminateLocks 开启同步消除
-XX:+PrintEliminateAllocations 开启标量替换后,查看标量替换情况。
常用的避免死锁方法
1、避免一个线程同时获取多个锁 ,尽量保证每个锁只占用一个资源
2、尝试使用定时锁,使用lock.tryLock(timeout)来替代使用内部锁机制
3、对于数据库锁,加锁和解锁必须在一个数据库连接里,否则会出现解锁失败的情况
1、对比
功能 | RocketMQ | Kafka | RabbitMQ |
---|---|---|---|
安全防护 | 支持 | 支持 | 支持 |
主子账号支持 | 支持 | 支持 | 不支持 |
可靠性 | – 同步刷盘 – 同步双写 – 超3份数据副本 – 99.99999999% |
– 同步刷盘 – 同步双写 – 超3份数据副本 – 99.99999999% |
同步刷盘 镜像模式集群 |
可用性 | – 非常好,99.95% – Always Writable |
– 非常好,99.95% – Always Writable |
好,主从模式,切换有6秒延时 |
横向扩展能力 | – 支持平滑扩展 – 支持百万级 QPS |
– 支持平滑扩展 – 支持百万级 QPS |
– 集群扩容依赖前端 – LVS 负载均衡调度 |
Low Latency | 支持 | 支持 | 不支持 |
消费模型 | Push / Pull | Push / Pull | Push / Pull |
定时消息 | 支持(可精确到秒级) | 暂不支持 | 支持 |
事务消息 | 支持 | 不支持 | 不支持 |
顺序消息 | 支持 | 暂不支持 | 不支持 |
全链路消息轨迹 | 支持 | 暂不支持 | 不支持 |
消息堆积能力 | 百亿级别 不影响性能 |
百亿级别 不影响性能 |
影响性能 |
消息堆积查询 | 支持 | 支持 | 不支持 |
消息回溯 | 支持 | 支持 | 不支持 |
消息重试 | 支持 | 不支持 | 支持 |
死信队列 | 支持 | 不支持 | 支持 |
性能(常规) | 百万级 QPS | 百万级 QPS | 万级 QPS |
性能(万级 Topic 场景) | 百万级 QPS | 百万级 QPS | 低 |
性能(海量消息堆积场景) | 百万级 QPS | 百万级 QPS | 低 |
延时Latency | ms毫秒级 | ms毫秒级 | μs微秒级 |
2、RabbitMQ结构
Exchange消息调度策略
1. Fanout (订阅模式|广播模式)
交换器会把所有发送到该交换器的消息路由到所有与该交换器绑定的消息队列中
2. Direct(路由模式)
RabbitMQ默认提供了一个Exchange,名字是空字符串,类型是Direct,绑定到所有的Queue(每一个Queue和这个无名Exchange之间的Binding Key是Queue的名字)
多个订阅方指定Queuename,则为争抢消费
多个订阅方不指定Queuename,则为重复消费
3.Topic (通配符模式)
按照正则表达式模糊匹配:用消息的Routing Key与 Exchange和Queue 之间的Binding Key进行模糊匹配,如果匹配成功,将消息分发到该Queue。
3、消费模式
a) 一种是通过basic.consume命令,订阅某一个队列中的消息,channel会自动在处理完上一条消息之后,接收下一条消息。(同一个channel消息处理是串行的)。除非关闭channel或者取消订阅,否则客户端将会一直接收队列的消息。
b) 另外一种方式是通过basic.get命令主动获取队列中的消息,但是绝对不可以通过循环调用basic.get来代替basic.consume,这是因为basic.get RabbitMQ在实际执行的时候,是首先consume某一个队列,然后检索第一条消息,然后再取消订阅。如果是高吞吐率的消费者,最好还是建议使用basic.consume。
4、集群模式
普通模式:默认的集群模式
rabbitmq集群中各个节点共享元数据,如user,vhost,exchange等,但不共享Queue。
当消息进入A节点的Queue中后,consumer从B节点拉取时,RabbitMQ会临时在A、B间进行消息传输,把A中的消息实体取出并经过B发送给consumer。当A节点故障后,B节点无法取到A节点中还未消费的消息实体。
镜像模式:把需要的队列做成镜像队列,存在于多个节点,属于RabbitMQ的HA方案
为了使队列称为镜像队列,你将会创建一个策略来匹配队列,设置策略有两个键“ha-mode和 ha-params(可选)”。ha-params根据ha-mode设置不同的值,下面表格说明这些key的选项。
ES调优
1、ulimit -a 命令,查看文件句柄(open files)的个数为1024,在ElasticSearch大量请求的情况下,这个句柄数量是不够的,可以改成655360:ulimit -n 655360
2、jvm老年代和新生代的内存比例为2:1是比较合适的
3、内存分配:官方给出了解决方案,把一半(少于)的内存分配给Luence,另外的内存分配给ElasticSearch。内存消耗大户 非堆内存 (off-heap):Lucene。Lucene 被设计为可以利用操作系统底层机制来缓存内存数据结构。
4、即便你有足够的内存,也尽量不要 超过 32 GB。因为它浪费了内存,降低了 CPU 的性能,还要让 GC 应对大内存。
Java 使用一个叫作 内存指针压缩(compressed oops)的技术。它的指针不再表示对象在内存中的精确位置,而是表示 偏移量,一旦你越过那个神奇的 ~32 GB 的边界,指针就会切回普通对象的指针。每个对象的指针都变长了,就会使用更多的 CPU 内存带宽。
5、关掉swap,内存交换 到磁盘对服务器性能来说是 致命 的。如果内存交换到磁盘上,一个 100 微秒的操作可能变成 10 毫秒。swapoff -a
6、Elasticsearch 默认的线程设置已经很合理了,搜索线程池可以设置的大一点,配置为 int(( 核心数 * 3 )/ 2 )+ 1
7、GC,Elasticsearch 默认的垃圾回收器( GC )是 CMS,低延迟需求软件的最佳垃圾回收器。官方建议使用。
8、最小主节点数:minimum_master_nodes 设置及其重要,为了防止集群脑裂,这个参数应该设置为法定个数就是 ( master 候选节点个数 / 2) + 1
9、集群分片数:一个分片实际上对应一个lucene 索引,而lucene索引的读写会占用很多的系统资源,因此,分片数不能设置过大。控制每个分片占用的硬盘容量不超过ES的最大JVM的堆空间设置(一般设置不超过32G)。
如果分片数过多,大大超过了节点数,很可能会导致一个节点上存在多个分片,一旦该节点故障,有可能会导致数据丢失。所以, 一般都设置分片数不超过节点数的3倍。
10、索引优化
a.修改index_buffer_size 的设置,可以设置成百分数,也可设置成具体的大小,大小可根据集群的规模做不同的设置测试。
indices.memory.index_buffer_size:10%(默认)
indices.memory.min_index_buffer_size:48mb(默认)
indices.memory.max_index_buffer_size: 32GB
b. _id字段的使用,应尽可能避免自定义_id, 以避免针对ID的版本管理;建议使用ES的默认ID生成策略或使用数字类型ID做为主键。
c. _all字段及_source字段的使用,应该注意场景和需要,_all字段包含了所有的索引字段,方便做全文检索,如果无此需求,可以禁用;_source存储了原始的document内容,如果没有获取原始文档数据的需求,可通过设置includes、excludes 属性来定义放入_source的字段。、
d. 合理的配置使用index属性,analyzed 和not_analyzed,根据业务需求来控制字段是否分词或不分词。只有 groupby需求的字段,配置时就设置成not_analyzed, 以提高查询或聚类的效率。
11、查询优化
a. 调整filter过滤顺序,把过滤效果明显的条件提前,按照过滤效果把过滤条件排序
b. 索引时间精度优化,时间粒度越大,搜索时间越短,所以时间精度越低越好;或者,增加冗余的时间字段,精确到天,带有时间范围的查询使用该字段进行查询。
c. 查询Fetch Source优化,不取非必须的字段。举例:只需要从es中查询id这一个字段,却把所有字段查询了出来
redis
类型 |
简介 |
特性 |
场景 |
命令 |
String(字符串) |
二进制安全 |
可以包含任何数据,比如jpg图片或者序列化的对象,一个键最大能存储512M |
value其实不仅是String,也可以是数字 |
set,get,decr,incr,mget |
Hash(字典) |
键值对集合,即编程语言中的Map类型 |
适合存储对象,并且可以像数据库中update一个属性一样只修改某一项属性值(Memcached中需要取出整个字符串反序列化成对象修改完再序列化存回去) |
存储、读取、修改用户属性 |
hget,hset,hgetall |
List(列表) |
链表(双向链表) |
增删快,提供了操作某一段元素的API |
1,最新消息排行等功能(比如朋友圈的时间线) 2,消息队列 |
lpush,rpush,lpop,rpop,lrange |
Set(集合) |
哈希表实现,元素不重复 set 的内部实现是一个 value永远为null的HashMap |
1、添加、删除,查找的复杂度都是O(1) 2、为集合提供了求交集、并集、差集等操作 |
1、将其所有好友存在一个集合。Redis还为集合提供了求交集、并集、差集等操作,可以非常方便的实现如共同关注、共同喜好、二度好友等功能 2、利用唯一性,统计访问网站的所有独立ip;
3、好友推荐时,根据tag求交集,大于某个阈值就可以推荐 |
sadd,spop,smembers,sunion |
Sorted Set(有序集合) |
内部使用HashMap和跳跃表(SkipList)来保证数据的存储和有序,HashMap里放的是成员到score的映射,而跳跃表里存放的是所有的成员,排序依据是HashMap里存的score |
数据插入集合时,已经进行天然排序 |
1、排行榜 2、带权重的消息队列 |
zadd,zrange,zrem,zcard |
pub/sub |
当一个key值上进行了消息发布后,所有订阅它的客户端都会收到相应的消息。 |
|
即时聊天,群聊 |
|
Watch、Transactions |
|
Redis 不支持回滚(roll back) |
Redis还提供了一个Watch功能,你可以对一个key进行Watch,然后再执行Transactions,在这过程中,如果这个Watched的值进行了修改,那么这个Transactions会发现并拒绝执行 |
|
setnx |
步骤: 1. setnx(lockkey, 当前时间+过期超时时间) ,如果返回1,则获取锁成功;如果返回0则没有获取到锁,转向2。 5. 在获取到锁之后,当前线程可以开始自己的业务处理,当处理完毕后,比较自己的处理时间和对于锁设置的超时时间,如果小于锁设置的超时时间,则直接执行delete释放锁;如果大于锁设置的超时时间,则不需要再锁进行处理。
|
|
|
加锁: jedis.set(String key, String value, String NX, String expx, int time)
释放锁:
为什么要使用Lua语言来实现呢?因为要确保上述操作是原子性的 |
位操作 |
|
|
几亿用户系统的签到,这个几亿长的大数组就能通过下标和元素值(0和1)来构建一个记忆系统 |
setbit、getbit、bitcount |
深入理解redis主从复制原理
redis优化
1、精简键名和键值、内部编码优化
(redis为每种数据类型都提供了两种内部编码方式,在不同的情况下redis会自动调整合适的编码方式。)、
2、修改linux内存分配策略:设置:sysctl vm.overcommit_memory=1(有三个可选值:0、1、2:
0, 内核将检查是否有足够的内存;如果有,内存申请允许;没有,内存申请失败,并把错误返回给应用进程。
1, 不管需要多少内存,都允许申请。
2, 只允许分配物理内存和交换内存的大小。(交换内存一般是物理内存的一半)
redis在备份数据的时候,会fork出一个子进程,理论上child进程所占用的内存和parent是一样的,比如parent占用的内存为8G,这个时候也要同样分配8G的内存给child,如果内存无法负担,往往会造成redis服务器的down机或者IO负载过高,效率下降。所以内存分配策略应该设置为 1(表示内核允许分配所有的物理内存,而不管当前的内存状态如何)
3、关闭Transparent Huge Pages(THP)
THP会造成内存锁影响redis性能,建议关闭。Transparent HugePages :用来提高内存管理的性能
使用root用户执行下面命令:echo never > /sys/kernel/mm/transparent_hugepage/enabled
4、修改linux中TCP 监听的最大容纳数量
在高并发环境下你需要一个高backlog值来避免慢客户端连接问题。注意Linux内核默默地将这个值减小到/proc/sys/net/core/somaxconn的值,所以需要确认增大somaxconn和tcp_max_syn_backlog两个值来达到想要的效果。
注意:这个参数并不是限制redis的最大链接数。如果想限制redis的最大连接数需要修改maxclients,默认最大连接数为10000。
5、限制redis的内存大小
- maxmemory:最大内存
- maxmemory-policy:内存不足时,数据清除策略
注意:如果不限制内存,当物理内存使用完之后,会使用swap分区,这样性能较低,如果限制了内存,当到达指定内存之后就不能添加数据了;如果没限制,会报OOM错误。
6、使用管道批量添加
redis5.0
- 新的流数据类型(Stream data type) https://redis.io/topics/streams-intro
- 新的 Redis 模块 API:定时器、集群和字典 API(Timers, Cluster and Dictionary APIs)
- RDB 增加 LFU 和 LRU 信息
- 集群管理器从 Ruby (redis-trib.rb) 移植到了redis-cli 中的 C 语言代码
- 新的有序集合(sorted set)命令:ZPOPMIN/MAX 和阻塞变体(blocking variants)
- 升级 Active defragmentation 至 v2 版本
- 增强 HyperLogLog 的实现
- 更好的内存统计报告
- 许多包含子命令的命令现在都有一个 HELP 子命令
- 客户端频繁连接和断开连接时,性能表现更好
- 许多错误修复和其他方面的改进
- 升级 Jemalloc 至 5.1 版本
- 引入 CLIENT UNBLOCK 和 CLIENT ID
- 新增 LOLWUT 命令 http://antirez.com/news/123
- 在不存在需要保持向后兼容性的地方,弃用 "slave" 术语
- 网络层中的差异优化
- Lua 相关的改进
- 引入动态的 HZ(Dynamic HZ) 以平衡空闲 CPU 使用率和响应性
- 对 Redis 核心代码进行了重构并在许多方面进行了改进
关于Redis RedLock算法的争论
- Martin上来就问,我们要锁来干啥呢?2个原因:
- 提升效率,用锁来保证一个任务没有必要被执行两次。比如(很昂贵的计算)
- 保证正确,使用锁来保证任务按照正常的步骤执行,防止两个节点同时操作一份数据,造成文件冲突,数据丢失。
- antirez 总结了 Martin 对 RedLock的指控:
- 分布式的锁具有一个自动释放的功能。锁的互斥性,只在过期时间之内有效,锁过期释放以后就会造成多个Client 持有锁。对于这个问题,其他带有自动释放的分布式锁也没有办法。
- RedLock 整个系统是建立在,一个在实际系统无法保证的系统模型上的。在这个例子中就是系统假设时间是同步且可信的。Martin认为系统时间的阶跃主要来自两个方面:
-
人为修改——无可应对。
-
从NTP服务收到了一个跳跃时时钟更新。——需要通过运维来保证:将阶跃的时间更新到服务器的时候,应当采取小步快跑的方式。多次修改,每次更新时间尽量小。
Linux下TCP最大连接数限制修改
1、修改
打开文件 /etc/sysctl.conf,增加以下设置
该参数设置系统的TIME_WAIT的数量,如果超过默认值则会被立即清除
net.ipv4.tcp_max_tw_buckets = 20000
定义了系统中每一个端口最大的监听队列的长度,这是个全局的参数
net.core.somaxconn = 65535
对于还未获得对方确认的连接请求,可保存在队列中的最大数目
net.ipv4.tcp_max_syn_backlog = 262144
在每个网络接口接收数据包的速率比内核处理这些包的速率快时,允许送到队列的数据包的最大数目
net.core.netdev_max_backlog = 30000
能够更快地回收TIME-WAIT套接字。此选项会导致处于NAT网络的客户端超时,建议为0
net.ipv4.tcp_tw_recycle = 0
系统所有进程一共可以打开的文件数量
fs.file-max = 6815744
防火墙跟踪表的大小。注意:如果防火墙没开则会提示error: "net.netfilter.nf_conntrack_max" is an unknown key,忽略即可
net.netfilter.nf_conntrack_max = 2621440
运行 sysctl -p即可生效。
2、修改打开文件限制
(1)ulimit -HSn 102400
这只是在当前终端有效,退出之后,open files 又变为默认值。
(2)将ulimit -HSn 102400写到/etc/profile中,这样每次登录终端时,都会自动执行/etc/profile。
(3)令修改open files的数值永久生效,则必须修改配置文件:/etc/security/limits.conf. 在这个文件后加上:
- soft nofile 1024000
- hard nofile 1024000
root soft nofile 1024000
root hard nofile 10240
深克隆和浅克隆
浅拷贝:仅仅克隆基本类型变量,而不克隆引用类型的变量
深克隆:既克隆基本类型变量,也克隆引用类型变量
1.浅克隆:只复制基本类型的数据,引用类型的数据只复制了引用的地址,引用的对象并没有复制,在新的对象中修改引用类型的数据会影响原对象中的引用。直接使用clone
方法,再嵌套的还是浅克隆,因为有些引用类型不能直接克隆。
-
@Override
-
public Object clone() throws CloneNotSupportedException {
-
// TODO Auto-generated method stub
-
//注意以下代码
-
Teacher teacher = (Teacher)super.clone();
-
return teacher;
-
}
2.深克隆:是在引用类型的类中也实现了clone
,是clone
的嵌套,并且在clone
方法中又对没有clone
方法的引用类型又做差异化复制,克隆后的对象与原对象之间完全不会影响,但是内容完全相同。
-
@Override
-
public Object clone() throws CloneNotSupportedException {
-
// TODO Auto-generated method stub
-
//注意以下代码
-
Teacher teacher = (Teacher)super.clone();
-
teacher.setStudent((Student)teacher.getStudent().clone());
-
return teacher;
-
}
3.使用序列化也能完成深复制的功能:对象序列化后写入流中,此时也就不存在引用什么的概念了,再从流中读取,生成新的对象,新对象和原对象之间也是完全互不影响的。非侵入的,不需要修改目标代码就可以实现
//将对象写到流里
ByteArrayOutputStream byteOut=new ByteArrayOutputStream();
ObjectOutputStream objOut=new ObjectOutputStream(byteOut);
objOut.writeObject(father);
//从流里读出来
ByteArrayInputStream byteIn=new ByteArrayInputStream(byteOut.toByteArray());
ObjectInputStream objInput=new ObjectInputStream(byteIn);
fatherCopy = (Son) objInput.readObject()
设计模式几大原则
开闭原则:当需求改变时,在不修改源代码的前提下,可以扩展模块的功能,满足新的需求。例如:Windows 的桌面主题设计
里氏替换原则:子类可以扩展父类的功能,但不能改变父类原有的功能。例如:企鹅属于鸟类;但它不飞,所以不能定义成“鸟”的子类。
依赖倒置原则:高层模块不应该依赖低层模块,两者都应该依赖其抽象;抽象不应该依赖细节,细节应该依赖抽象;其核心思想是:要面向接口编程。例如:顾客从多家商店购物,可将商店参数设置为interface Shop,代码中用Shop的实现类做参数。
单一职责原则:的核心就是控制类的粒度大小、将对象解耦、提高其内聚性。一个类只负责一项职责,提高类的可读性,提高系统的可维护性。例如:学校系统中,老师、学生的角色。
接口隔离原则:将臃肿庞大的接口拆分成更小的和更具体的接口,让接口中只包含客户感兴趣的方法。例如:学生成绩管理程序,将诸多功能分类放在输入、统计、打印 3 个接口中。
迪米特法则:又叫作最少知识原则。只依赖应该依赖的对象。只暴露应该暴露的方法。例如:明星与经纪人的关系实例;
合成复用原则:要尽量先使用组合或者聚合等,其次才考虑使用继承关系来实现。例如:汽车,分为:电动、汽油车;同时有白、红、黑颜色;
协程的4种状态
- Pending
- Running
- Done
- Cacelled
协程和系统线程之间的映射关系
go的协程本质上还是系统的线程调用,而Python中的协程是eventloop模型实现,所以虽然都叫协程,但并不是一个东西.
Python 中的协程是严格的 1:N 关系,也就是一个线程对应了多个协程。虽然可以实现异步I/O,但是不能有效利用多核(GIL)。
而 Go 中是 M:N 的关系,也就是 N 个协程会映射分配到 M 个线程上,这样带来了两点好处:
- 多个线程能分配到不同核心上,CPU 密集的应用使用 goroutine 也会获得加速.
- 即使有少量阻塞的操作,也只会阻塞某个 worker 线程,而不会把整个程序阻塞。
PS: Go中很少提及线程或进程,也就是因为上面的原因.
两种协程对比:
- async是非抢占式的,一旦开始采用 async 函数,那么你整个程序都必须是 async 的,不然总会有阻塞的地方(一遇阻塞对于没有实现异步特性的库就无法主动让调度器调度其他协程了),也就是说 async 具有传染性。
- Python 整个异步编程生态的问题,之前标准库和各种第三方库的阻塞性函数都不能用了,如:requests,redis.py,open 函数等。所以 Python3.5后加入协程的最大问题不是不好用,而是生态环境不好,历史包袱再次上演,动态语言基础上再加上多核之间的任务调度,应该是很难的技术吧,真心希望python4.0能优化或者放弃GIL锁,使用多核提升性能。
- goroutine 是 go 与生俱来的特性,所以几乎所有库都是可以直接用的,避免了 Python 中需要把所有库重写一遍的问题。
- goroutine 中不需要显式使用 await 交出控制权,但是 Go 也不会严格按照时间片去调度 goroutine,而是会在可能阻塞的地方插入调度。goroutine 的调度可以看做是半抢占式的。
Do not communicate by sharing memory; instead, share memory by communicating.(不要以共享内存的方式来通信,相反,要通过通信来共享内存) — CSP并发模型
Java编程详细解析—淘宝大秒杀系统是如何设计的?
如何设计一个 RPC 系统
史上最全DDoS攻击与防御教程
一文带你实现RPC框架