1.单例模式
面试突击50:单例模式有几种写法?
2.Mybatis缓存机制
MyBatis的一、二级缓存查询关系
一级缓存是SqlSession级别,不能跨SqlSession共享,默认开启。
二级缓存是基于mapper namespace级别的,可以跨SqlSession共享,通过配置开启。在开启二级缓存的状况下,查询数据的顺序为二级缓存→一级缓存→数据库。
一级缓存实现原理:
- 当你执行查询后,查询的结果会被放在SqlSession创建时由MyBatis创建的一个本地缓存中。
- 当你执行其他查询,MyBatis会检查缓存中是否有相同的查询。
- 如果有,就会从缓存中拿结果,不会再去数据库查询。
- 当SqlSession调用commit()方法时,MyBatis会清空其缓存。
二级缓存实现原理:
- 二级缓存的作用范围是一个mapper namespace,多个SqlSession共享。
- 二级缓存会将查询结果存储到一个缓存区域,这个缓存区域是按namespace分的。
- 当一个SqlSession关闭或提交后,其中的查询结果会被存入二级缓存。
- 当另一个SqlSession执行相同的查询时,它会检查二级缓存,如果有就会从二级缓存中获取结果。
本地缓存是在Executor内部构建,Executor包含了三个实现类,SimpleExecutor,BatchExecutor以及CachingExecutor,其中CachingExecutor是开启了二级缓存才会用到的,这里主要是SimpleExecutor和BatchExecutor,他们都实现了BaseExecutor,而BaseExecutor中正是进行了一级缓存的处理。
- BaseExecutor:基础执行器,封装了子类的公共方法,包括一级缓存、延迟加载、回滚、关闭等功能;
- BatchExecutor : 通过批量操作来提高性能。(执行update(没有select,JDBC批处理不支持select),将所有sql都添加到批处理中(addBatch()),等待统一执行(executeBatch()),它缓存了多个Statement对象,每个Statement对象都是addBatch()完毕后,等待逐一执行executeBatch()批处理。与JDBC批处理相同。)
- ReuseExecutor: 重复使用执行,其定义了一个Map<String, Statement>,将执行的sql作为key,将执行的Statement作为value保存,这样执行相同的sql时就可以使用已经存在的Statement,就不需要新创建了。(执行update或select,以sql作为key查找Statement对象,存在就使用,不存在就创建,用完后,不关闭Statement对象,而是放置于Map内,供下一次使用。简言之,就是重复使用Statement对象。)
- SimpleExecutor通过类名可以看出,它是一个简单的执行类,并不会做一些处理就执行sql。(每执行一次update或select,就开启一个Statement对象,用完立刻关闭Statement对象)。
- CacheExecutor有一个重要属性delegate,它保存的是某类普通的Executor,值在构照时传入。执行数据库update操作时,它直接调用delegate的update方法,执行query方法时先尝试从cache中取值,取不到再调用delegate的查询方法,并将查询结果存入cache中。(上述三种模式的装饰器模式)。
Mybatis源码阅读之–本地(一级)缓存实现原理分析
mybatis 源码分析(三)Executor 详解
3.分布式环境下的缓存一致性问题
在分布式环境下建议禁用MyBatis的一级、二级缓存,否则可能出现数据一致性问题。二级缓存虽然默认关闭,建议设置settings中配置:cacheEnabled 为false,全局关闭此。一级缓存默认开启而且不能关闭,可以设置localCacheScope属性设置为STATEMENT,查询时清空一级缓存。
MyBatis之缓存避坑: MyBatis的一级缓存、二级缓存的实现分析与分布式环境下数据一致性问题
4.全面解析 Redis 持久化:RDB、AOF与混合持久化
RDB 持久化方式是 Redis 将当前内存中的数据快照(snapshot)保存到硬盘的过程。换句话说,Redis 会创建一个代表某一时刻的数据集的磁盘文件。
例子: 想象一下相机的快门点击。每当你点击快门,你都会捕捉到那个特定时刻的场景。RDB的工作方式很相似,只不过它捕捉的是数据的状态。
RDB触发条件:理解 RDB 的本质后,你可能会问,我们如何生成这个快照呢?使用 SAVE 和 BGSAVE 命令即可。
手动触发:通过执行 SAVE 或 BGSAVE 命令。
自动触发:基于 Redis 配置文件中的 save 指令设置的条件。(默认是通过 BGSAVE 命令来触发的)
redis 配置文件 save 指令设置:
save 3600 1 # 3600秒内如果超过1个key被修改则生成 RDB
save 300 100 # 300秒内如果超过100个key被修改则生成 RDB
save 60 10000 # 60秒内如果超过10000个key被修改则生成 RDB
AOF(Append Of File)
以日志的形式来记录每个写操作,将Redis执行过的所有写指令记录下来(读操作不记录),只许追加文件但不可以改写文件,Redis启动之初会读取该文件重新构建数据,换言之,Redis重启的话就根据日志文件的内容将写执行从前到后执行一次以完成数据的恢复工作。
AOF 持久化的实现主要是以上三步:命令追加、文件写入、文件同步
命令追加: 将 redis 写操作命令追加到 aof_buf 缓冲区
文件写入: 周期性地将 aof_buf 缓冲区的命令写入 AOF 文件的内核缓冲区。
文件同步:根据配置同步策略,将 AOF 文件缓冲区的内容同步到磁盘。
其中文件同步策略 redis 提供了三种,分别是以下三种:
always:每次有命令写入时都立即同步。这提供了最高的数据安全性,但效率最低。
everysec:每秒同步一次。这是一个权衡安全性和效率的策略。最多只丢失 1 秒 的数据
no:让操作系统决定最佳的同步时间。这可能导致数据丢失,但提供了最高的效率。
RDB和AOP混合模式
混合持久化在 AOF 重写时,会首先将当前数据集以 RDB 格式快照的形式写入新 AOF 文件的开始位置,然后再追加新的写命令到文件末尾。
复制代码
(1)生成混合持久化文件:
Redis 在某个时刻生成了一个 RDB 快照,将其保存到 AOF 文件的前缀部分。
之后的所有写操作(例如 SET、DEL 等)都记录在 AOF 文件的后半部分。
假设 AOF 文件如下表示:
(2)恢复过程:
Redis 启动时,首先读取 AOF 文件中的 RDB 快照部分,快速加载数据集。
然后,Redis 继续读取 AOF 文件中的写操作日志,并逐个重放这些操作,以确保数据集与持久化时一致。
恢复后的顺序:
Redis 持久化模式概述
全面解析 Redis 持久化:RDB、AOF与混合持久化
微服务的特性
微服务的主要特性包括:
- 粒度更细的服务:微服务架构强调按业务边界进行细粒度的服务拆分,每个服务的功能和职责单一。这要求工程师充分理解和洞察业务领域的边界,保证服务自包含、独立部署和独立演进。
- 松耦合:服务之间是松耦合的,即服务之间的相互依赖程度低。高内聚要求将相关的元素和行为聚集在一起,而低耦合则是降低服务之间的相互依赖和相互作用。
- 独立部署:每个服务可以单独独立部署,这意味着可以对单个服务进行更新和维护,而不影响其他服务。
- 技术栈的多样性:每个服务可以使用不同的技术栈,包括不同的数据库和数据管理模型。通过REST API、事件流和消息代理组合彼此通信。
微服务的优点包括:
- 提高系统的可伸缩性和可维护性:通过将大型单体应用拆分成多个小服务,可以更灵活地扩展和维护系统。每个服务可以独立部署和扩展,提高了系统的可伸缩性和可维护性。
- 促进快速开发和部署:由于每个服务相对独立,开发人员可以并行开发不同的服务,加快开发速度。同时,服务的独立部署也使得新功能的上线更加迅速。
- 提高系统的稳定性和可靠性:通过将系统拆分成多个服务,即使某个服务出现问题,也不会影响整个系统,提高了系统的稳定性和可靠性。
常见的阻塞队列?及其使用场景
JDK7提供了7个阻塞队列。分别是
ArrayBlockingQueue :一个由数组结构组成的有界阻塞队列。
LinkedBlockingQueue :一个由链表结构组成的有界阻塞队列。
PriorityBlockingQueue :一个支持优先级排序的无界阻塞队列。
DelayQueue:一个使用优先级队列实现的无界阻塞队列。
SynchronousQueue:一个不存储元素的阻塞队列。
LinkedTransferQueue:一个由链表结构组成的无界阻塞队列。
LinkedBlockingDeque:一个由链表结构组成的双向阻塞队列。
,ArrayBlockingQueue是一个用数组实现的有界阻塞队列。此队列按照先进先出(FIFO)的原则对元素进行排序。默认情况下不保证访问者公平的访问队列,所谓公平访问队列是指阻塞的所有生产者线程或消费者线程,当队列可用时,可以按照阻塞的先后顺序访问队列,即先阻塞的生产者线程,可以先往队列里插入元素,先阻塞的消费者线程,可以先从队列里获取元素。通常情况下为了保证公平性会降低吞吐量。
LinkedBlockingQueue是一个用链表实现的有界阻塞队列。此队列的默认和最大长度为Integer.MAX_VALUE。此队列按照先进先出的原则对元素进行排序。
PriorityBlockingQueue是一个支持优先级的无界队列。默认情况下元素采取自然顺序排列,也可以通过比较器comparator来指定元素的排序规则。元素按照升序排列。
DelayQueue是一个支持延时获取元素的无界阻塞队列。队列使用PriorityQueue来实现。队列中的元素必须实现Delayed接口,在创建元素时可以指定多久才能从队列中获取当前元素。只有在延迟期满时才能从队列中提取元素。我们可以将DelayQueue运用在以下应用场景:
- 缓存系统的设计:可以用DelayQueue保存缓存元素的有效期,使用一个线程循环查询DelayQueue,一旦能从DelayQueue中获取元素时,表示缓存有效期到了。
- 定时任务调度。使用DelayQueue保存当天将会执行的任务和执行时间,一旦从DelayQueue中获取到任务就开始执行,从比如TimerQueue就是使用DelayQueue实现的。
SynchronousQueue是一个不存储元素的阻塞队列。每一个put操作必须等待一个take操作,否则不能继续添加元素。SynchronousQueue可以看成是一个传球手,负责把生产者线程处理的数据直接传递给消费者线程。队列本身并不存储任何元素,非常适合于传递性场景,比如在一个线程中使用的数据,传递给另外一个线程使用,SynchronousQueue的吞吐量高于LinkedBlockingQueue 和 ArrayBlockingQueue。
LinkedTransferQueue是一个由链表结构组成的无界阻塞TransferQueue队列。相对于其他阻塞队列,LinkedTransferQueue多了tryTransfer和transfer方法。
- transfer方法。如果当前有消费者正在等待接收元素(消费者使用take()方法或带时间限制的poll()方法时),transfer方法可以把生产者传入的元素立刻transfer(传输)给消费者。如果没有消费者在等待接收元素,transfer方法会将元素存放在队列的tail节点,并等到该元素被消费者消费了才返回。
- tryTransfer方法。则是用来试探下生产者传入的元素是否能直接传给消费者。如果没有消费者等待接收元素,则返回false。和transfer方法的区别是tryTransfer方法无论消费者是否接收,方法立即返回。而transfer方法是必须等到消费者消费了才返回。
LinkedBlockingDeque是一个由链表结构组成的双向阻塞队列。所谓双向队列指的你可以从队列的两端插入和移出元素。双端队列因为多了一个操作队列的入口,在多线程同时入队时,也就减少了一半的竞争。相比其他的阻塞队列,LinkedBlockingDeque多了addFirst,addLast,offerFirst,offerLast,peekFirst,peekLast等方法,以First单词结尾的方法,表示插入,获取(peek)或移除双端队列的第一个元素。以Last单词结尾的方法,表示插入,获取或移除双端队列的最后一个元素。另外插入方法add等同于addLast,移除方法remove等效于removeFirst。但是take方法却等同于takeFirst,不知道是不是Jdk的bug,使用时还是用带有First和Last后缀的方法更清楚。
线程池原理?
1,为什么要使用线程池
- 降低创建线程和销毁线程的性能开销
- 提高响应速度,当有新任务需要执行是不需要等待线程创建就可以立马执行
- 合理的设置线程池大小可以避免因为线程数超过硬件资源瓶颈带来的问题
2,线程池有哪几种类型
Executors 的工厂方法,就可以使用线程池: - newFixedThreadPool:该方法返回一个固定数量的线程池,线程数不变,当有一个任务提交时,若线程池中空闲,则立即执行,若没有,则会被暂缓在一个任务队列中,等待有空闲的线程去执行。
- newSingleThreadExecutor: 创建一个线程的线程池,若空闲则执行,若没有空闲线程则暂缓在任务队列中。
- newCachedThreadPool:返回一个可根据实际情况调整线程个数的线程池,不限制最大线程数量,若用空闲的线程则执行任务,若无任务则不创建线程。并且每一个空闲线程会在 60 秒后自动回收
- newScheduledThreadPool: 创建一个可以指定线程的数量的线程池,但是这个线程池还带有延迟和周期性执行任务的功能,类似定时器。
3,线程池有哪几种工作队列?
ArrayBlockingQueue (有界队列):是一个基于数组结构的有界阻塞队列,此队列按 FIFO(先进先出)原则对元素进行排序。
LinkedBlockingQueue (无界队列):一个基于链表结构的阻塞队列,此队列按FIFO (先进先出) 排序元素,吞吐量通常要高于ArrayBlockingQueue。静态工厂方法Executors.newFixedThreadPool()使用了这个队列。
SynchronousQueue(同步队列): 一个不存储元素的阻塞队列。每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于LinkedBlockingQueue,静态工厂方法Executors.newCachedThreadPool使用了这个队列。
DelayQueue(延迟队列):一个任务定时周期的延迟执行的队列。根据指定的执行时间从小到大排序,否则根据插入到队列的先后排序。指定时间到了之后,才能出队列;队列头的离出队时间最近。
PriorityBlockingQueue(优先级队列): 一个具有优先级的无限阻塞队列。
ArrayListQueue、LinkedBlockingQueue、SynchronousQueue是阻塞队列
有界队列:ArrayBl
无界队列:
- 有界队列
ArrayBlockingQueue:一个由数组结构组成的有界阻塞队列。
LinkedBlockingQueue:一个由链表结构组成的有界阻塞队列。 - 无界队列
PriorityBlockingQueue:一个支持优先级排序的无界阻塞队列。
DelayQueue:一个使用优先级队列实现的延迟无界阻塞队列。
SynchronousQueue:一个内部只能包含一个元素的队列。。
4. 怎么理解无界队列和有界队列
有界队列即长度有限,满了以后ArrayBlockingQueue会插入阻塞。
无界队列就是里面能放无数的东西而不会因为队列长度限制被阻塞,但是可能会出现OOM异常。
5. 线程池中的几种重要的参数及流程
1.向线程池提交任务时,会首先判断线程池中的线程数是否大于设置的核心线程数,如果不大于,就创建一个核心线程来执行任务。
2.如果大于核心线程数,就会判断缓冲队列是否满了,如果没有满,则放入队列,等待线程空闲时执行任务。
3.如果队列已经满了,则判断是否达到了线程池设置的最大线程数,如果没有达到,就对线程做扩让,创建临时线程来执行任务。
4.如果已经达到了最大线程数,则执行指定的拒绝策略。这里需要注意队列的判断与最大线程数判断的顺序,不要搞反。
public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,BlockingQueue<Runnable> workQueue,ThreadFactory threadFactory,RejectedExecutionHandler handler) {if (corePoolSize < 0 ||maximumPoolSize <= 0 ||maximumPoolSize < corePoolSize ||keepAliveTime < 0)throw new IllegalArgumentException();if (workQueue == null || threadFactory == null || handler == null)throw new NullPointerException();this.corePoolSize = corePoolSize;this.maximumPoolSize = maximumPoolSize;this.workQueue = workQueue;this.keepAliveTime = unit.toNanos(keepAliveTime);this.threadFactory = threadFactory;this.handler = handler;
}
- corePoolSize:核心池的大小,在创建了线程池后,默认情况下,线程池中并没有任何线程,而是等待有任务到来才创建线程去执行任务,当有任务来之后,就会创建一个线程去执行任务,当线程池中的线程数目达到corePoolSize后,就会把到达的任务放到缓存队列当中
- maximumPoolSize:线程池最大线程数最大线程数
- keepAliveTime:表示线程没有任务执行时最多保持多久时间会终止
- unit:参数keepAliveTime的时间单位TimeUtil类的枚举类(DAYS、HOURS、MINUTES、SECONDS 等)
- workQueue:阻塞队列,用来存储等待执行的任务
- threadFactory:线程工厂,主要用来创建线程
- handler:拒绝处理任务的策略
----AbortPolicy:丢弃任务并抛出 RejectedExecutionException 异常。(默认这种)
----DiscardPolicy:也是丢弃任务,但是不抛出异常
----DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程)
-----CallerRunsPolicy:由调用线程处理该任务
6.线程池的数量如何确定?
在遇到这类问题时,先冷静下来分析
- 需要分析线程池执行的任务的特性: CPU 密集型还是 IO 密集型
- 每个任务执行的平均时长大概是多少,这个任务的执行时长可能还跟任务处理逻辑是否涉及到网络传输以及底层系统资源依赖有关系
如果是 CPU 密集型,主要是执行计算任务,响应时间很快,cpu 一直在运行,这种任务 cpu的利用率很高,那么线程数的配置应该根据 CPU 核心数来决定,CPU 核心数=最大同时执行线程数,加入 CPU 核心数为 4,那么服务器最多能同时执行 4 个线程。过多的线程会导致上下文切换反而使得效率降低。那线程池的最大线程数可以配置为 cpu 核心数+1
如果是 IO 密集型,主要是进行 IO 操作,执行 IO 操作的时间较长,这是 cpu 出于空闲状态,导致 cpu 的利用率不高,这种情况下可以增加线程池的大小。这种情况下可以结合线程的等待时长来做判断,等待时间越高,那么线程数也相对越多。一般可以配置 cpu 核心数的 2 倍。
一个公式:线程池设定最佳线程数目 = ((线程池设定的线程等待时间+线程 CPU 时间)/线程 CPU 时间 )* CPU 数目
这个公式的线程 cpu 时间是预估的程序单个线程在 cpu 上运行的时间(通常使用 loadrunner测试大量运行次数求出平均值)