有一天我在观察阿里云ARMS线程栈监控时,发现一个WAITING状态的线程栈很有意思,看起来是MongoDAOSupport.getById()
导致线程进入WAITING状态,出乎意料,这就探究下是神马情况~
为什么加锁?
一般来说,多个线程执行的SQL查询互不相干,MongoTemplate的查询方法中为什么要加锁呢?到底要保护什么临界资源呢?
了解MongoTemplate查询结果的解析和转换
内部执行find one查询的方法调用时,new 了一个ReadDocumentCallback
对象,这个对象构造参数有三个
分别是:
MongoConverter mongoConverter
,Mongo数据转换器,在MongoTemplate构造初始化时被初始化(final类型不可更改),MongoConverter
实现了EntityReader
接口Class<T> entityClass
,目标entity class对象,即需要转换的目标类型String collectionName
,查询数据时的mongo集合名称
结论:一个MongoTemplate
实例对象共用一个MongoConverter
,多线程运行时MongoConverter.read()
方法会加锁。
接着从一些局部的代码片段,看MongoConverter
是如何将数据库的Document bson
数据转换为开发者定义的entity的。
part-1:通过简单工厂模式,将Class
对象解析为ClassTypeInformation
对象。同时,将解析到的结果缓存到全局缓存CACHE
对象中,缓存是一个ConcurrentReferenceHashMap
。
ClassTypeInformation
只有一个私有属性private final Class<S> type;
,但它继承了TypeDiscoverer
类,画一个类图简述一下。
前面说的ClassTypeInformation.from()
这个静态工厂方法,会缓存所代理的目标entity对象的Class描述信息
,主要记录在TypeDiscoverer<S>
对象的私有属性中,包括目标类的Type类型
属性的TypeMap
等等;具体解析的代码在org.springframework.data.util.TypeDiscoverer#TypeDiscoverer:Line-82
,代码比较复杂,我简单理解为——这些是目标对象反射实例化时所必需的解析数据。
part-2:创建entity实例对象,并填充对象属性值
part-3:根据查询结果类型,将结果转换为目标类型
这里findOne查询的结果是一个Document
类型。
part-4:判断合适的目标类型,映射为持久层对象
查询结果与持久层对象的映射转换核心方法,也是这里面用到了上文中的lock
.
part-5:读写锁,管理读多写少的持久化对象缓存
可以看到加锁的地方在mappingContext.getPersistentEntity()
方法中。
一个MongoTemplate
对象对应一个mappingContext
对象,代码声明如下:
public class MongoTemplate {
……
private final MappingContext<? extends MongoPersistentEntity<?>, MongoPersistentProperty> mappingContext;
……
}
多个业务线程在调用mappingContext.getPersistentEntity(typeToUse);
方法时,会遇到这段同步代码。
使用的是读写锁,要保护的临界资源是Map<TypeInformation<?>, Optional<E>> persistentEntities
对象。
代码是这样声明的,我加了一些注释,简单来说核心点有几个——
- 一个
MongoTemplate
中用一个私有的map来缓存持久化对象数据,这个map的key是TypeInformation
,value是MutablePersistentEntity
AbstractMappingContext#getPersistentEntity
在每次查询结果映射时,先尝试获取typeToUse
对应的持久化对象,如果获取不到则创建一个并缓存起来。
public abstract class AbstractMappingContext {
……
private final Map<TypeInformation<?>, Optional<E>> persistentEntities = new HashMap<>();
private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
private final Lock read = lock.readLock();
private final Lock write = lock.writeLock();
……
// 获取持久层对象
public E getPersistentEntity(TypeInformation<?> type) {
Assert.notNull(type, "Type must not be null!");
try {
// 读锁,与写互斥,读不互斥
read.lock();
Optional<E> entity = persistentEntities.get(type);
if (entity != null) {
return entity.orElse(null);
}
} finally {
read.unlock();
}
……
return addPersistentEntity(type).orElse(null);
}
// 创建持久层对象,并缓存起来
protected Optional<E> addPersistentEntity(TypeInformation<?> typeInformation) {
Class<?> type = typeInformation.getType();
E entity = null;
try {
// 写锁,与读、写互斥
write.lock();
// 创建entity对象
entity = createPersistentEntity(typeInformation);
// Eagerly cache the entity as we might have to find it during recursive lookups.
// 尽早的缓存entity对象,因为我们必须在递归映射期间找到该对象(否则就会产生死循环)
persistentEntities.put(typeInformation, Optional.of(entity));
……
} catch (BeansException e) {
throw new MappingException(e.getMessage(), e);
} finally {
write.unlock();
}
……
return Optional.of(entity);
}
}
P.S这部分代码我看起来很眼熟,因为我做 Smart API
接口源码解析的时候,也会使用递归,同时也会缓存已经解析过的对象信息,而不是每次都当作新对象解析一遍,目的都是为了加快映射、解析结果的速度。
总结
- 一个
MongoTemplate
的MappingContext(映射转换上下文)
内部维护了一个多线程共享的持久化对象缓存
,是一个HashMap<TypeInformation, PersistentEntity>
- 内部使用
ReentrantReadWriteLock
维护缓存的读和写,从场景上来说,既然是缓存一定是读写比很高,使用读写锁是比较合理的 PersistentEntity
的创建是“懒加载”的,通过org.springframework.data.util.Lazy
包装实现MongoTemplate
在项目启动初期,查询相对较慢的原因可能与这个缓存有关;项目启动预热可以尝试在这里做点文章- 既然是java本地缓存设计,为什么没有使用
软引用
?而且也没有任何缓存淘汰策略。我想作者在设计之初,就默认这部分数据读写比非常非常高,且在这样的读写比下,一次写入的代价非常大,所以并不期望淘汰任何缓存的数据。