MongoTemplate findOne方法底层还会加同步锁?

有一天我在观察阿里云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 接口源码解析的时候,也会使用递归,同时也会缓存已经解析过的对象信息,而不是每次都当作新对象解析一遍,目的都是为了加快映射、解析结果的速度。

总结

  • 一个MongoTemplateMappingContext(映射转换上下文)内部维护了一个多线程共享的持久化对象缓存,是一个HashMap<TypeInformation, PersistentEntity>
  • 内部使用ReentrantReadWriteLock维护缓存的读和写,从场景上来说,既然是缓存一定是读写比很高,使用读写锁是比较合理的
  • PersistentEntity的创建是“懒加载”的,通过org.springframework.data.util.Lazy包装实现
  • MongoTemplate在项目启动初期,查询相对较慢的原因可能与这个缓存有关;项目启动预热可以尝试在这里做点文章
  • 既然是java本地缓存设计,为什么没有使用软引用?而且也没有任何缓存淘汰策略。我想作者在设计之初,就默认这部分数据读写比非常非常高,且在这样的读写比下,一次写入的代价非常大,所以并不期望淘汰任何缓存的数据。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值