了解过Mybatis的同学,大概都知道Mybatis有一级缓存和二级缓存;但是我们使用Mybatis时一般使用默认的配置,对缓存的原理知之甚少。之前介绍Executor的文章中提到了Mybatis缓存相关的内容,但是比较琐碎,不成体系。
今天通过这篇文章对Mybatis的缓存机制做下详细解读,对一级缓存、二级缓存的执行流程、工作原理有个全面的认识,方便开发过程中合理使用。 ![image.png](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/76827989df754f16804d217e0ee00191~tplv-k3u1fbpfcp-zoom-1.image) 还是先通过一个“全景图”对Mybatis的缓存机制有个整体了解,然后结合整体流程,对每个环节逐个剖析。如图所示,Mybatis一级缓存是在BaseExecutor中实现,并且一级缓存仅在SqlSession生命周期内有效;二级缓存是在CachingExecutor实现,二级缓存是在namespace维度且全局共享。
若一二级缓存同时开启,当执行查询时:Mybatis先查询二级缓存;如果二级缓存未命中,则继续查询一级缓存;若一级缓存未命中,则查询数据库并更新一二级缓存。默认情况下,一级缓存是开启的,而且没有办法关闭;二级缓存默认关闭。大体了解Mybatis缓存机制后,我们逐个了解一下一二级缓存分别是怎么实现的。
## 一级缓存
先来看下比较简单的一级缓存。Mybatis一级缓存在BaseExecutor中实现,使用Cache接口实现类PerpetualCache作为缓存存储的容器,通过源码可知其内部就是一个HashMap。这就跟我们平时自己做K-V存储或者使用Redis做缓存的思路类似。那么,我们自然而然的需要弄清楚以下几个问题:
* Mybatis是如何写缓存、何时写缓存?
* Mybatis如何读缓存、何时读缓存?
* Mybatis何时使缓存失效,缓存失效采用了什么策略?
* Cache接口及实现类提供了哪些接口方法,Mybatis是如何使用它们的?
* 一级缓存是应用内的本地缓存,在分布式场景下会不会导致脏读等不一致问题?该如何解决?
呃,好像问题挺多的!前面三个是缓存的使用问题,第四个是源码层面的,最后一个是结合一级缓存特点如何使用的问题。带着这几个问题,我们进入正题。
### Cache接口及PerpetualCache
Cache接口位于org.apache.ibatis.cache包下,是Mybatis实现缓存的核心接口,Mybatis一级、二级缓存的都依赖于它,通过下面的类图简单了解一下继承关系,以体现Cache的重要地位: ![image.png](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/285015ab9d3e41468c802e5782dcd901~tplv-k3u1fbpfcp-zoom-1.image) 一级缓存仅用到了PerpetualCache,所以这个阶段我们先通过源码认识Cache接口及PerpetualCache实现。先看下Cache接口的注释:
~~~java
/**
* SPI for cache providers.
* One instance of cache will be created for each namespace.
* The cache implementation must have a constructor that receives the cache id as an String parameter.
* MyBatis will pass the namespace as id to the constructor.
*
* <pre>
* public MyCache(final String id) {
* if (id == null) {
* throw new IllegalArgumentException("Cache instances require an ID");
* }
* this.id = id;
* initialize();
* }
* </pre>
*
* @author Clinton Begin
*/
复制代码
~~~
作者Clinton Begin说,Cache接口定义了Mybatis cache体系的SPI,任何一个cache实例都是为namespace创建的,并且通过示例要求每个Cache接口实现类应该通过构造方法接收namespace作为cache的id作为唯一标识。这也就是说,每个Cache实例都是有唯一标识的(id)。再来看下Cache的接口定义:
~~~java
public interface Cache {
/**
* 获取缓存的唯一标识
* @return The identifier of this cache
*/
String getId();
/**
* 向缓存中添加内容,这里的key使用了CacheKey这个类
*
* @param key Can be any object but usually it is a {@link CacheKey}
* @param value The result of a select.
*/
void putObject(Object key, Object value);
/**
* 根据key从缓存中查询
* @param key The key
* @return The object stored in the cache.
*/
Object getObject(Object key);
/**
* 从缓存中移除key对应的内容
*
* @param key The key
* @return Not used
*/
Object removeObject(Object key);
/**
* Clears this cache instance,清空缓存
*/
void clear();
}
复制代码
~~~
Cache接口很少,它提供了对缓存添加、查询、删除、清空的基本操作方法,其中添加、查询、删除都需要使用类型为CacheKey的key来操作。CacheKey是Mybatis用来生成缓存索引的工具类,通过一定的规则来确保散列的鲁棒性。再看PerpetualCache,刚才也提到,它使用HashMap作为缓存的存储容器,再结合Cache接口定义的方法我们自然就可以知道它是对HashMap元素的操作了,节省篇幅,代码就不再贴了。
### 缓存管理
一级缓存管理涉及PerpetualCache创建、查询缓存、添加缓存、清空缓存几个过程,除了缓存创建外,其他几个过程都是由Executor的update/query/commit/rollback等方法执行时触发。
#### 缓存创建
PerpetualCache的初始化工作在BaseExecutor构造方法中完成,并指定id为`LocalCache`,所以一级缓存也叫本地缓存。初始化代码如下所示:
~~~java
protected BaseExecutor(Configuration configuration, Transaction transaction) {
this.transaction = transaction;
this.deferredLoads = new ConcurrentLinkedQueue<>();
// 缓存初始化
this.localCache = new PerpetualCache("LocalCache");
this.localOutputParameterCache = new PerpetualCache("LocalOutputParameterCache");
this.closed = false;
this.configuration = configuration;
this.wrapper = this;
}
复制代码
~~~
#### 缓存查询与添加
缓存的目的是为了提高查询效率,所以查询缓存必然是在BaseExecutor#query触发。如果查询过程中命中缓存,可直接把缓存内容返回;如果未命中,则需要从数据库中查询,然后再把数据库查询结果添加到缓存中,以保证下次的查询效率。查询过程中的缓存管理流程如下图所示: ![image.png](data:image/svg+xml;utf8,) 以上流程分别涉及了BaseExecutor#query()、BaseExecutor#queryFromDatabase两个方法,大家可以对照流程图及代码过一下:
~~~java
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId());
if (closed) {
throw new ExecutorException("Executor was closed.");
}
if (queryStack == 0 && ms.isFlushCacheRequired()) {
clearLocalCache();
}
List<E> list;
try {
queryStack++;
// 检查是否命中缓存,如果命中则取出结果
list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
if (list != null) {
handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
} else {
//未命中时,调用方法查询数据库
list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
} finally {
queryStack--;
}
if (queryStack == 0) {
for (DeferredLoad deferredLoad : deferredLoads) {
deferredLoad.load();
}
// issue #601
deferredLoads.clear();
// 如果本地缓存(一级缓存)的作用范围是STATEMENT,则清空缓存
if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
// issue #482
clearLocalCache();
}
}
return list;
}
private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
List<E> list;
//通过占位符预占缓存
localCache.putObject(key, EXECUTION_PLACEHOLDER);
try {
// 查询数据库
list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
} finally {
// 移除占位符
localCache.removeObject(key);
}
// 把数据库查询结果添加到缓存中
localCache.putObject(key, list);
if (ms.getStatementType() == StatementType.CALLABLE) {
localOutputParameterCache.putObject(key, parameter);
}
return list;
}
复制代码
~~~
#### 清空缓存
当数据源发生改变时,为了保证数据一致性,清空缓存中的失效数据,从而保证再次查询时从数据库加载最新数据。所以,引起数据源变更的操作即为清空缓存的时机。
从BaseExecutor的接口来看,update、commit、rollback等方法都会导致数据源的变更,通过源码来看,这些方法内部确实调用了clearLocalCache方法来清空缓存。代码如下所示:
~~~java
public int update(MappedStatement ms, Object parameter) throws SQLException {
ErrorContext.instance().resource(ms.getResource()).activity("executing an update").object(ms.getId());
if (closed) {
throw new ExecutorException("Executor was closed.");
}
clearLocalCache();
return doUpdate(ms, parameter);
}
public void commit(boolean required) throws SQLException {
if (closed) {
throw new ExecutorException("Cannot commit, transaction is already closed");
}
clearLocalCache();
flushStatements();
if (required) {
transaction.commit();
}
}
public void rollback(boolean required) throws SQLException {
if (!closed) {
try {
clearLocalCache();
flushStatements(true);
} finally {
if (required) {
transaction.rollback();
}
}
}
}
public void clearLocalCache() {
if (!closed) {
localCache.clear();
localOutputParameterCache.clear();
}
}
复制代码
~~~
这里需要注意的是:在update方法中,如果本地缓存(一级缓存)的作用范围是STATEMENT,则也会调用方法clearLocalCache来清空缓存。
### 一级缓存总结
#### 生命周期
在之前的文章中我们已经了解到,Executor被SqlSession持有,而一级缓存`LocalCache`只是BaseExecutor的一个字段。所以,当SqlSession的生命周期结束时,一级缓存也会被回收。也就是说,一级缓存仅能作用于同一个SqlSession的声明周期,不同SqlSession间不可共享。它们之间的关系如下图所示: ![image.png](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/f616507473e441699b4f0d04e4403c8e~tplv-k3u1fbpfcp-zoom-1.image)
#### 分布式系统如何避免数据不一致
既然一级缓存仅可在SqlSession内部共享,那么如果同时存在多个SqlSession或者分布式的环境下,update、commit等引起缓存失效的方法无法对其他SqlSession起作用,必然会带来脏读问题。这个问题可以通过更改一级缓存的作用范围(localCacheScope)为STATEMENT进行避免。
#### 缓存读写时机与原理
一级缓存由PerpetualCache实现,其内部通过HashMap完成对缓存的读写操作,所以本质上讲,一级缓存其实是通过HashMap实现的,Map中的key是由MappedStatement生成CacheKey,来确保查询的唯一性。
一级缓存是为了提高查询效率,在执行query操作前会先查询缓存:若命中缓存,则直接把缓存内容作为结果返回,不再查询数据库;若未命中缓存,则执行数据库查询,然后把查询结果加入缓存内,最终返回结果。
## 二级缓存
一级缓存生命周期仅限于SqlSession,然而实际使用时我们创建多个SqlSession对象,但是多个SqlSession之间无法共享缓存内容。为了解决这个问题,我们可以使用二级缓存。二级缓存由CachingExecutor实现,作用在一级缓存之前。开启二级缓存后,Executor的查询流程变为:`二级缓存->一级缓存->数据库`。
### 启用二级缓存
启用二级缓存,需要依次完成以下几个配置:
* 在Mybatis配置文件中配置cacheEnabled 设置(settings):全局性地开启或关闭所有映射器配置文件中已配置的任何缓存,默认值为true。
~~~xml
<setting name="cacheEnabled" value="true"/>
复制代码
~~~
* 在mapper配置文件中增加节点。
~~~xml
<!--默认情况下,这一行就可以-->
<cache/>
复制代码
~~~
这样会使用默认的缓存策略,cache节点有如下几个属性可以设置:
* eviction:清除策略。支持LRU(最近最少使用,默认)、FIFO(先进先出)、SOFT、WEAK。
* flushInterval:刷新间隔。可以被设置为任意的正整数,设置的值应该是一个以毫秒为单位的合理时间量。 默认情况是不设置,也就是没有刷新间隔,缓存仅仅会在调用语句时刷新。
* size:缓存数量,默认值是 1024。可以被设置为任意正整数,要注意欲缓存对象的大小和运行环境中可用的内存资源。
* readOnly(只读)属性可以被设置为 true 或 false。只读的缓存会给所有调用者返回缓存对象的相同实例。 因此这些对象不能被修改。这就提供了可观的性能提升。而可读写的缓存会(通过序列化)返回缓存对象的拷贝。 速度上会慢一些,但是更安全,因此默认值是 false。
默认情况下(如上配置)起到的效果如下:
* 映射语句文件中的所有 select 语句的结果将会被缓存。
* 映射语句文件中的所有 insert、update 和 delete 语句会刷新缓存。
* 缓存会使用最近最少使用算法(LRU, Least Recently Used)算法来清除不需要的缓存。
* 缓存不会定时进行刷新(也就是说,没有刷新间隔)。
* 缓存会保存列表或对象(无论查询方法返回哪种)的 1024 个引用。
* 缓存会被视为读/写缓存,这意味着获取到的对象并不是共享的,可以安全地被调用者修改,而不干扰其他调用者或线程所做的潜在修改。
### 二级缓存初始化
二级缓存初始化是在mapper对应的xml文件解析时执行的,可以按照以下链路查询解析流程,其中cacheElement方法读取了并为配置信息赋值为默认值,然后调用MapperBuilderAssistant#useNewCache初始化二级缓存对象。
* org.apache.ibatis.session.SqlSessionFactoryBuilder#build(java.io.InputStream, java.lang.String, java.util.Properties)
* org.apache.ibatis.builder.xml.XMLConfigBuilder#parse
* org.apache.ibatis.builder.xml.XMLConfigBuilder#parseConfiguration
* org.apache.ibatis.builder.xml.XMLConfigBuilder#mapperElement
* org.apache.ibatis.builder.xml.XMLMapperBuilder#parse
* org.apache.ibatis.builder.xml.XMLMapperBuilder#configurationElement
* org.apache.ibatis.builder.xml.XMLMapperBuilder#cacheElement
* org.apache.ibatis.builder.MapperBuilderAssistant#useNewCache
cacheElement方法读取配置并初始化配置信息,若未配置属性会采用默认值。
~~~java
private void cacheElement(XNode context) {
if (context != null) {
// 读取缓存类型,默认为PERPETUAL
String type = context.getStringAttribute("type", "PERPETUAL");
Class<? extends Cache> typeClass = typeAliasRegistry.resolveAlias(type);
// 读取数据淘汰策略并获取其类型,默认为LRU,默认大小1024
String eviction = context.getStringAttribute("eviction", "LRU");
Class<? extends Cache> evictionClass = typeAliasRegistry.resolveAlias(eviction);
// 刷新间隔,默认为空,即不会刷新。
Long flushInterval = context.getLongAttribute("flushInterval");
// 缓存数量:默认为空,会使用LRU的默认大小1024
Integer size = context.getIntAttribute("size");
boolean readWrite = !context.getBooleanAttribute("readOnly", false);
boolean blocking = context.getBooleanAttribute("blocking", false);
Properties props = context.getChildrenAsProperties();
// 初始化缓存对象
builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, blocking, props);
}
}
复制代码
~~~
useNewCache方法:依次创建缓存对象PerpetualCache,添加数据淘汰策略(装饰器),设置刷新间隔,设置缓存大小……,这一系列操作采用装饰器模式进行装配,最终得到的是一个层层嵌套的Cache对象。
~~~java
public Cache useNewCache(Class<? extends Cache> typeClass,
Class<? extends Cache> evictionClass,
Long flushInterval,
Integer size,
boolean readWrite,
boolean blocking,
Properties props) {
Cache cache = new CacheBuilder(currentNamespace)
.implementation(valueOrDefault(typeClass, PerpetualCache.class))
.addDecorator(valueOrDefault(evictionClass, LruCache.class))
.clearInterval(flushInterval)
.size(size)
.readWrite(readWrite)
.blocking(blocking)
.properties(props)
.build();
configuration.addCache(cache);
currentCache = cache;
return cache;
}
复制代码
~~~
Cache初始化时,使用currentNamespace(即:mapper文件的namespace)作为Cache的id,最终把该缓存对象存储在全局缓存Configuration中,以此实现了跨Session共享。
### 缓存使用
如前文所述,二级缓存在CachingExecutor中生效。查看org.apache.ibatis.executor.CachingExecutor#query方法可知,其执行流程如下:
* 根据MappedStatement、parameterObject等信息生成CacheKey;
* 获取MappedStatement中缓存的Cache对象;
* 通过TransactionalCacheManager尝试获取缓存数据:若缓存命中,则直接返回;若缓存未命中,则执行后续查询并把结果缓存在TransactionalCacheManager中。
query方法完成了缓存的查询与添加操作,具体过程可查看如下代码。
~~~java
@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
throws SQLException {
Cache cache = ms.getCache();
if (cache != null) {
flushCacheIfRequired(ms);
if (ms.isUseCache() && resultHandler == null) {
ensureNoOutParams(ms, boundSql);
@SuppressWarnings("unchecked")
List<E> list = (List<E>) tcm.getObject(cache, key);
if (list == null) {
list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
tcm.putObject(cache, key, list); // issue #578 and #116
}
return list;
}
}
return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
复制代码
~~~
### 缓存失效
为了避免脏读,当CachingExecutor执行update、commit、rollback等操作时,会执行缓存的删除或清空重置操作。
update源码:
~~~java
@Override
public int update(MappedStatement ms, Object parameterObject) throws SQLException {
flushCacheIfRequired(ms);
return delegate.update(ms, parameterObject);
}
private void flushCacheIfRequired(MappedStatement ms) {
Cache cache = ms.getCache();
if (cache != null && ms.isFlushCacheRequired()) {
tcm.clear(cache);
}
}
复制代码
~~~
commit源码:
~~~java
//org.apache.ibatis.executor.CachingExecutor#commit
@Override
public void commit(boolean required) throws SQLException {
delegate.commit(required);
tcm.commit();
}
//org.apache.ibatis.executor.CachingExecutor#rollback
@Override
public void rollback(boolean required) throws SQLException {
try {
delegate.rollback(required);
} finally {
if (required) {
tcm.rollback();
}
}
}
//org.apache.ibatis.cache.TransactionalCacheManager#commit
public void commit() {
for (TransactionalCache txCache : transactionalCaches.values()) {
txCache.commit();
}
}
//org.apache.ibatis.cache.decorators.TransactionalCache#commit
public void commit() {
if (clearOnCommit) {
delegate.clear();
}
flushPendingEntries();
reset();
}
private void reset() {
clearOnCommit = false;
entriesToAddOnCommit.clear();
entriesMissedInCache.clear();
}
复制代码
~~~
### 二级缓存总结
MyBatis的二级缓存相对于一级缓存来说,实现了SqlSession之间缓存数据的共享,同时粒度更加的细,能够到namespace级别,通过Cache接口实现类不同的组合,对Cache的可控性也更强。
在分布式环境下,由于默认的MyBatis Cache实现都是基于本地的,分布式环境下必然会出现读取到脏数据,需要使用集中式缓存将MyBatis的Cache接口实现,有一定的开发成本,直接使用Redis、Memcached等分布式缓存可能成本更低,安全性也更高。
## 全文总结
本文简单梳理了mybatis的缓存机制,主要在于了解其运行原理,虽然在实际工作中不会使用,但是某些参数或特性可能会影响我们应用的运行效果,了解其原理重点在于避坑。另外,其缓存原理也可以为我们实际开发工作提供一些好的思路。
作者:码路印记
链接:https://juejin.cn/post/6908703314867650567
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
- 一.JVM
- 1.1 java代码是怎么运行的
- 1.2 JVM的内存区域
- 1.3 JVM运行时内存
- 1.4 JVM内存分配策略
- 1.5 JVM类加载机制与对象的生命周期
- 1.6 常用的垃圾回收算法
- 1.7 JVM垃圾收集器
- 1.8 CMS垃圾收集器
- 1.9 G1垃圾收集器
- 2.面试相关文章
- 2.1 可能是把Java内存区域讲得最清楚的一篇文章
- 2.0 GC调优参数
- 2.1GC排查系列
- 2.2 内存泄漏和内存溢出
- 2.2.3 深入理解JVM-hotspot虚拟机对象探秘
- 1.10 并发的可达性分析相关问题
- 二.Java集合架构
- 1.ArrayList深入源码分析
- 2.Vector深入源码分析
- 3.LinkedList深入源码分析
- 4.HashMap深入源码分析
- 5.ConcurrentHashMap深入源码分析
- 6.HashSet,LinkedHashSet 和 LinkedHashMap
- 7.容器中的设计模式
- 8.集合架构之面试指南
- 9.TreeSet和TreeMap
- 三.Java基础
- 1.基础概念
- 1.1 Java程序初始化的顺序是怎么样的
- 1.2 Java和C++的区别
- 1.3 反射
- 1.4 注解
- 1.5 泛型
- 1.6 字节与字符的区别以及访问修饰符
- 1.7 深拷贝与浅拷贝
- 1.8 字符串常量池
- 2.面向对象
- 3.关键字
- 4.基本数据类型与运算
- 5.字符串与数组
- 6.异常处理
- 7.Object 通用方法
- 8.Java8
- 8.1 Java 8 Tutorial
- 8.2 Java 8 数据流(Stream)
- 8.3 Java 8 并发教程:线程和执行器
- 8.4 Java 8 并发教程:同步和锁
- 8.5 Java 8 并发教程:原子变量和 ConcurrentMap
- 8.6 Java 8 API 示例:字符串、数值、算术和文件
- 8.7 在 Java 8 中避免 Null 检查
- 8.8 使用 Intellij IDEA 解决 Java 8 的数据流问题
- 四.Java 并发编程
- 1.线程的实现/创建
- 2.线程生命周期/状态转换
- 3.线程池
- 4.线程中的协作、中断
- 5.Java锁
- 5.1 乐观锁、悲观锁和自旋锁
- 5.2 Synchronized
- 5.3 ReentrantLock
- 5.4 公平锁和非公平锁
- 5.3.1 说说ReentrantLock的实现原理,以及ReentrantLock的核心源码是如何实现的?
- 5.5 锁优化和升级
- 6.多线程的上下文切换
- 7.死锁的产生和解决
- 8.J.U.C(java.util.concurrent)
- 0.简化版(快速复习用)
- 9.锁优化
- 10.Java 内存模型(JMM)
- 11.ThreadLocal详解
- 12 CAS
- 13.AQS
- 0.ArrayBlockingQueue和LinkedBlockingQueue的实现原理
- 1.DelayQueue的实现原理
- 14.Thread.join()实现原理
- 15.PriorityQueue 的特性和原理
- 16.CyclicBarrier的实际使用场景
- 五.Java I/O NIO
- 1.I/O模型简述
- 2.Java NIO之缓冲区
- 3.JAVA NIO之文件通道
- 4.Java NIO之套接字通道
- 5.Java NIO之选择器
- 6.基于 Java NIO 实现简单的 HTTP 服务器
- 7.BIO-NIO-AIO
- 8.netty(一)
- 9.NIO面试题
- 六.Java设计模式
- 1.单例模式
- 2.策略模式
- 3.模板方法
- 4.适配器模式
- 5.简单工厂
- 6.门面模式
- 7.代理模式
- 七.数据结构和算法
- 1.什么是红黑树
- 2.二叉树
- 2.1 二叉树的前序、中序、后序遍历
- 3.排序算法汇总
- 4.java实现链表及链表的重用操作
- 4.1算法题-链表反转
- 5.图的概述
- 6.常见的几道字符串算法题
- 7.几道常见的链表算法题
- 8.leetcode常见算法题1
- 9.LRU缓存策略
- 10.二进制及位运算
- 10.1.二进制和十进制转换
- 10.2.位运算
- 11.常见链表算法题
- 12.算法好文推荐
- 13.跳表
- 八.Spring 全家桶
- 1.Spring IOC
- 2.Spring AOP
- 3.Spring 事务管理
- 4.SpringMVC 运行流程和手动实现
- 0.Spring 核心技术
- 5.spring如何解决循环依赖问题
- 6.springboot自动装配原理
- 7.Spring中的循环依赖解决机制中,为什么要三级缓存,用二级缓存不够吗
- 8.beanFactory和factoryBean有什么区别
- 九.数据库
- 1.mybatis
- 1.1 MyBatis-# 与 $ 区别以及 sql 预编译
- Mybatis系列1-Configuration
- Mybatis系列2-SQL执行过程
- Mybatis系列3-之SqlSession
- Mybatis系列4-之Executor
- Mybatis系列5-StatementHandler
- Mybatis系列6-MappedStatement
- Mybatis系列7-参数设置揭秘(ParameterHandler)
- Mybatis系列8-缓存机制
- 2.浅谈聚簇索引和非聚簇索引的区别
- 3.mysql 证明为什么用limit时,offset很大会影响性能
- 4.MySQL中的索引
- 5.数据库索引2
- 6.面试题收集
- 7.MySQL行锁、表锁、间隙锁详解
- 8.数据库MVCC详解
- 9.一条SQL查询语句是如何执行的
- 10.MySQL 的 crash-safe 原理解析
- 11.MySQL 性能优化神器 Explain 使用分析
- 12.mysql中,一条update语句执行的过程是怎么样的?期间用到了mysql的哪些log,分别有什么作用
- 十.Redis
- 0.快速复习回顾Redis
- 1.通俗易懂的Redis数据结构基础教程
- 2.分布式锁(一)
- 3.分布式锁(二)
- 4.延时队列
- 5.位图Bitmaps
- 6.Bitmaps(位图)的使用
- 7.Scan
- 8.redis缓存雪崩、缓存击穿、缓存穿透
- 9.Redis为什么是单线程、及高并发快的3大原因详解
- 10.布隆过滤器你值得拥有的开发利器
- 11.Redis哨兵、复制、集群的设计原理与区别
- 12.redis的IO多路复用
- 13.相关redis面试题
- 14.redis集群
- 十一.中间件
- 1.RabbitMQ
- 1.1 RabbitMQ实战,hello world
- 1.2 RabbitMQ 实战,工作队列
- 1.3 RabbitMQ 实战, 发布订阅
- 1.4 RabbitMQ 实战,路由
- 1.5 RabbitMQ 实战,主题
- 1.6 Spring AMQP 的 AMQP 抽象
- 1.7 Spring AMQP 实战 – 整合 RabbitMQ 发送邮件
- 1.8 RabbitMQ 的消息持久化与 Spring AMQP 的实现剖析
- 1.9 RabbitMQ必备核心知识
- 2.RocketMQ 的几个简单问题与答案
- 2.Kafka
- 2.1 kafka 基础概念和术语
- 2.2 Kafka的重平衡(Rebalance)
- 2.3.kafka日志机制
- 2.4 kafka是pull还是push的方式传递消息的?
- 2.5 Kafka的数据处理流程
- 2.6 Kafka的脑裂预防和处理机制
- 2.7 Kafka中partition副本的Leader选举机制
- 2.8 如果Leader挂了的时候,follower没来得及同步,是否会出现数据不一致
- 2.9 kafka的partition副本是否会出现脑裂情况
- 十二.Zookeeper
- 0.什么是Zookeeper(漫画)
- 1.使用docker安装Zookeeper伪集群
- 3.ZooKeeper-Plus
- 4.zk实现分布式锁
- 5.ZooKeeper之Watcher机制
- 6.Zookeeper之选举及数据一致性
- 十三.计算机网络
- 1.进制转换:二进制、八进制、十六进制、十进制之间的转换
- 2.位运算
- 3.计算机网络面试题汇总1
- 十四.Docker
- 100.面试题收集合集
- 1.美团面试常见问题总结
- 2.b站部分面试题
- 3.比心面试题
- 4.腾讯面试题
- 5.哈罗部分面试
- 6.笔记
- 十五.Storm
- 1.Storm和流处理简介
- 2.Storm 核心概念详解
- 3.Storm 单机版本环境搭建
- 4.Storm 集群环境搭建
- 5.Storm 编程模型详解
- 6.Storm 项目三种打包方式对比分析
- 7.Storm 集成 Redis 详解
- 8.Storm 集成 HDFS 和 HBase
- 9.Storm 集成 Kafka
- 十六.Elasticsearch
- 1.初识ElasticSearch
- 2.文档基本CRUD、集群健康检查
- 3.shard&replica
- 4.document核心元数据解析及ES的并发控制
- 5.document的批量操作及数据路由原理
- 6.倒排索引
- 十七.分布式相关
- 1.分布式事务解决方案一网打尽
- 2.关于xxx怎么保证高可用的问题
- 3.一致性hash原理与实现
- 4.微服务注册中心 Nacos 比 Eureka的优势
- 5.Raft 协议算法
- 6.为什么微服务架构中需要网关
- 0.CAP与BASE理论
- 十八.Dubbo
- 1.快速掌握Dubbo常规应用
- 2.Dubbo应用进阶
- 3.Dubbo调用模块详解
- 4.Dubbo调用模块源码分析
- 6.Dubbo协议模块