Spring源码系列【四】——Context初始化流程(下)

Context 初始化流程

接上一篇:Spring源码系列【三】——Context初始化流程(上)

二、方法详解

2.7 #initMessageSource()

初始化消息源,消息源可用来支持消息的国际化。

/**
 * Initialize the MessageSource.
 * Use parent's if none defined in this context.
 * 初始化MessageSource
 * 如果在上下文中未定义,则使用父项
 */
protected void initMessageSource() {
	ConfigurableListableBeanFactory beanFactory = getBeanFactory();
	if (beanFactory.containsLocalBean(MESSAGE_SOURCE_BEAN_NAME)) {
                // 如果在配置中已经配置了messageSource,那么将messageSource提取并记录在this.messageSource中
		this.messageSource = beanFactory.getBean(MESSAGE_SOURCE_BEAN_NAME, MessageSource.class);
		// Make MessageSource aware of parent MessageSource.
		if (this.parent != null && this.messageSource instanceof HierarchicalMessageSource) {
			HierarchicalMessageSource hms = (HierarchicalMessageSource) this.messageSource;
			if (hms.getParentMessageSource() == null) {
				// Only set parent context as parent MessageSource if no parent MessageSource
				// registered already.
				// 如果尚未注册父MessageSource,则仅将父上下文设置为父MessageSource
				hms.setParentMessageSource(getInternalParentMessageSource());
			}
		}
		if (logger.isTraceEnabled()) {
			logger.trace("Using MessageSource [" + this.messageSource + "]");
		}
	}
	else {
		// Use empty MessageSource to be able to accept getMessage calls.
		// 使用空的MessageSource可以接受getMessage调用(如果用户并没有定义配置文件,那么使用临时的DelegatingMessageSource以便于作为调用getMessage方法返回)
		DelegatingMessageSource dms = new DelegatingMessageSource();
		dms.setParentMessageSource(getInternalParentMessageSource());
		this.messageSource = dms;
		beanFactory.registerSingleton(MESSAGE_SOURCE_BEAN_NAME, this.messageSource);
		if (logger.isTraceEnabled()) {
			logger.trace("No '" + MESSAGE_SOURCE_BEAN_NAME + "' bean, using [" + this.messageSource + "]");
		}
	}
}

可以查看 MessageSource.java 接口:用于解析消息的策略界面,支持参数化以及此类信息的国际化。

如图getMessage():

大致意思就是,通过读取并将自定义资源文件配置记录在容器中,那么就可以在获取资源文件的时候使用了,例如在 AbstractApplicationContext 中的获取资源文件属性的方法就是:

return getMessageSource().getMessage(code, args, locale);

 

2.8 #initApplicationEventMulticaster()

初始化应用事件广播器。事件广播器用来向 ApplicationListener 通知各种应用产生的事件,是标准的观察者模式。

/**
 * Initialize the ApplicationEventMulticaster.
 * Uses SimpleApplicationEventMulticaster if none defined in the context.
 * 初始化ApplicationEventMulticaster
 * 如果上下文中未定义,则使用SimpleApplicationEventMulticaster
 * @see org.springframework.context.event.SimpleApplicationEventMulticaster
 */
protected void initApplicationEventMulticaster() {
	ConfigurableListableBeanFactory beanFactory = getBeanFactory();
	if (beanFactory.containsLocalBean(APPLICATION_EVENT_MULTICASTER_BEAN_NAME)) {
		this.applicationEventMulticaster =
				beanFactory.getBean(APPLICATION_EVENT_MULTICASTER_BEAN_NAME, ApplicationEventMulticaster.class);
		if (logger.isTraceEnabled()) {
			logger.trace("Using ApplicationEventMulticaster [" + this.applicationEventMulticaster + "]");
		}
	}
	else {
		this.applicationEventMulticaster = new SimpleApplicationEventMulticaster(beanFactory);
		beanFactory.registerSingleton(APPLICATION_EVENT_MULTICASTER_BEAN_NAME, this.applicationEventMulticaster);
		if (logger.isTraceEnabled()) {
			logger.trace("No '" + APPLICATION_EVENT_MULTICASTER_BEAN_NAME + "' bean, using " +
					"[" + this.applicationEventMulticaster.getClass().getSimpleName() + "]");
		}
	}
}

初始化广播器逻辑考虑两种情况:如果用户自定义了事件广播器,就使用自定义的事件广播器,如果用户没有定义,则使用默认的 ApplicationEventMulticaster。其中 new SimpleApplicationEventMulticaster(beanFactory); 就是默认广播器的实现。

 

2.9 #onRefresh()

该方法是留给子类的扩展步骤,用来让特定的 Context 子类初始化其他的 Bean。

/**
 * Template method which can be overridden to add context-specific refresh work.
 * Called on initialization of special beans, before instantiation of singletons.
 * 可以重写的模版方法以添加特定于上下文的刷新操作
 * 在所有单例bean初始化之前调用
 * <p>This implementation is empty.
 * @throws BeansException in case of errors
 * @see #refresh()
 */
protected void onRefresh() throws BeansException {
	// For subclasses: do nothing by default.
}

 

2.10 #registerListeners()

把实现了ApplicationListener 的 Bean 注册到事件广播器,并对广播器中的早期未广播事件进行通知。

/**
 * Add beans that implement ApplicationListener as listeners.
 * Doesn't affect other listeners, which can be added without being beans.
 */
protected void registerListeners() {
	// Register statically specified listeners first.
	// 首先注册静态指定的监听器
	for (ApplicationListener<?> listener : getApplicationListeners()) {
		getApplicationEventMulticaster().addApplicationListener(listener);
	}

	// Do not initialize FactoryBeans here: We need to leave all regular beans
	// uninitialized to let post-processors apply to them!
	String[] listenerBeanNames = getBeanNamesForType(ApplicationListener.class, true, false);
	for (String listenerBeanName : listenerBeanNames) {
		getApplicationEventMulticaster().addApplicationListenerBean(listenerBeanName);
	}

	// Publish early application events now that we finally have a multicaster...
	// 至此拥有了一个广播器,可以发布早起的应用程序事件
	Set<ApplicationEvent> earlyEventsToProcess = this.earlyApplicationEvents;
	this.earlyApplicationEvents = null;
	if (earlyEventsToProcess != null) {
		for (ApplicationEvent earlyEvent : earlyEventsToProcess) {
			getApplicationEventMulticaster().multicastEvent(earlyEvent);
		}
	}
}

 

2.11 #finishBeanFactoryInitialization()

完成 BeanFactory 的初始化工作,其中包括 ConversionService 的设置、配置冻结以及非延迟加载的 bean 的初始化工作。

/**
 * Finish the initialization of this context's bean factory,
 * initializing all remaining singleton beans.
 * 完成此上下文的BeanFactory的初始化
 * 初始化所有剩余的单例bean
 */
protected void finishBeanFactoryInitialization(ConfigurableListableBeanFactory beanFactory) {
	// Initialize conversion service for this context.
	// 为此上下文初始化转换服务
	if (beanFactory.containsBean(CONVERSION_SERVICE_BEAN_NAME) &&
			beanFactory.isTypeMatch(CONVERSION_SERVICE_BEAN_NAME, ConversionService.class)) {
		beanFactory.setConversionService(
				beanFactory.getBean(CONVERSION_SERVICE_BEAN_NAME, ConversionService.class));
	}

	// Register a default embedded value resolver if no bean post-processor
	// (such as a PropertyPlaceholderConfigurer bean) registered any before:
	// at this point, primarily for resolution in annotation attribute values.
	// 如果没有bean后处理器,则注册默认的嵌入式值解析器(例如PropertyPlaceholderConfigurer bean)之前注册过的任何东西:
	// 主要用于注释属性值的解析。
	if (!beanFactory.hasEmbeddedValueResolver()) {
		beanFactory.addEmbeddedValueResolver(strVal -> getEnvironment().resolvePlaceholders(strVal));
	}

	// Initialize LoadTimeWeaverAware beans early to allow for registering their transformers early.
	// 尽早初始化LoadTimeWeaverAware bean,以便尽早注册其转换器
	String[] weaverAwareNames = beanFactory.getBeanNamesForType(LoadTimeWeaverAware.class, false, false);
	for (String weaverAwareName : weaverAwareNames) {
		getBean(weaverAwareName);
	}

	// Stop using the temporary ClassLoader for type matching.
	// 停止使用临时的类加载器进行类型匹配
	beanFactory.setTempClassLoader(null);

	// Allow for caching all bean definition metadata, not expecting further changes.
	// 允许缓存所有bean定义元数据,而不期望进一步的更改
	beanFactory.freezeConfiguration();

	// Instantiate all remaining (non-lazy-init) singletons.
	// 初始化所有剩余的单例bean(非延迟初始化的)
	beanFactory.preInstantiateSingletons();
}

 

2.12 #finishRefresh()

完成上下文的 refresh 工作,调用 LifecycleProcessor 的 onFresh() 方法,以及发布 ContextRefreshEvent 事件。

/**
 * Finish the refresh of this context, invoking the LifecycleProcessor's
 * onRefresh() method and publishing the
 * {@link org.springframework.context.event.ContextRefreshedEvent}.
 * 完成此上下文的刷新,调用LifecycleProcessor的onRefresh()方法并发布
 */
protected void finishRefresh() {
	// Clear context-level resource caches (such as ASM metadata from scanning).
	// 清除上下文级别的资源缓存,例如来自扫描的ASM元数据
	clearResourceCaches();

	// Initialize lifecycle processor for this context.
	// 为此上下文初始化生命周期处理器
	initLifecycleProcessor();

	// Propagate refresh to lifecycle processor first.
	// 首先将刷新传播到生命周期处理器
	getLifecycleProcessor().onRefresh();

	// Publish the final event.
	// 发布最终事件
	publishEvent(new ContextRefreshedEvent(this));

	// Participate in LiveBeansView MBean, if active.
	// 如果是活动的,则加入到LiveBeansView MBean
	LiveBeansView.registerApplicationContext(this);
}

Spring 中提供了 Lifecycle 接口,该接口中包含 start 和 stop 方法,实现此接口后 Spring 会保证在启动的时候调用 start 方法开始生命周期,并在 Spring 关闭的时候调用 stop 方法来结束生命周期,通常用于配置后台程序,在启动后一直运行(比如对 MQ 进行轮询等)。ApplicationContext 的初始化最后正是保证了这一功能的实现。

  • initLifecycleProcessor

当 ApplicationContext 启动或停止时,它会通过 LifecycleProcessor 来与所有声明的 bean 的周期做状态更新,而在 LifecycleProcessor 使用之前需要初始化。

  • onRefresh

启动所有实现了 Lifecycle 接口的bean。

  • publishEvent

当完成 ApplicationContext 初始化的时候,要通过 Spring 中的事件发布机制发出 ContextRefreshedEvent 事件,以保证对应的监听器可以做进一步的逻辑处理。

 

2.13 #resetCommonCaches()

在 finally 中重置公共的缓存,比如 ReflectionUtils 中的缓存、AnnotationUtils 中的缓存。

/**
 * Reset Spring's common reflection metadata caches, in particular the
 * {@link ReflectionUtils}, {@link AnnotationUtils}, {@link ResolvableType}
 * and {@link CachedIntrospectionResults} caches.
 * @since 4.2
 * @see ReflectionUtils#clearCache()
 * @see AnnotationUtils#clearCache()
 * @see ResolvableType#clearCache()
 * @see CachedIntrospectionResults#clearClassLoader(ClassLoader)
 */
protected void resetCommonCaches() {
	ReflectionUtils.clearCache();
	AnnotationUtils.clearCache();
	ResolvableType.clearCache();
	CachedIntrospectionResults.clearClassLoader(getClassLoader());
}

 

三、Context初始化流程思维导图

至此,以上为代码、注释及说明,简明扼要的用思维导图概括如下图:

 

(本文完)

Spring源码系列【三】——Context初始化流程(上)

Context 初始化流程

Context 也即是常说的上下文,个人理解是程序执行的环境、一个容器,其初始化流程分12步(12个方法),也可以说是13步(外加 finally 代码块执行的那个方法)。

为便于查看每个方法的功能描述,后面我会用散装英语“翻译”一下,注释是在原英文注释下面。

一、Context 初始化流程(12个方法)

入口代码:AbstractApplicationContext.java#refresh() 方法:

@Override
public void refresh() throws BeansException, IllegalStateException {
	synchronized (this.startupShutdownMonitor) {
		// Prepare this context for refreshing.
		// 1.准备此上下文以进行刷新,即刷新前上下文环境准备
		prepareRefresh();

		// Tell the subclass to refresh the internal bean factory.
		// 2.告诉子类刷新内部bean工厂
		ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();

		// Prepare the bean factory for use in this context.
		// 3.准备在上下文中使用的bean工厂
		prepareBeanFactory(beanFactory);

		try {
			// Allows post-processing of the bean factory in context subclasses.
			// 4.允许在上下文子类中对bean工厂进行后处理
			postProcessBeanFactory(beanFactory);

			// Invoke factory processors registered as beans in the context.
			// 5.调用在上下文中注册为bean的工厂处理器
			invokeBeanFactoryPostProcessors(beanFactory);

			// Register bean processors that intercept bean creation.
			// 6.注册拦截bean创建的bean处理器,即注册BeanPostProcessors
			registerBeanPostProcessors(beanFactory);

			// Initialize message source for this context.
			// 7.为此上下文初始化消息源
			initMessageSource();

			// Initialize event multicaster for this context.
			// 8.为此上下文初始化事件多播器(广播器?)
			initApplicationEventMulticaster();

			// Initialize other special beans in specific context subclasses.
			// 9.在特定上下文子类中初始化其他特殊bean
			onRefresh();

			// Check for listener beans and register them.
			// 10.检查监听器bean并注册它们
			registerListeners();

			// Instantiate all remaining (non-lazy-init) singletons.
			// 11.实例化所有剩余的(非延迟初始化的)单例
			finishBeanFactoryInitialization(beanFactory);

			// Last step: publish corresponding event.
			// 12.最后一步:发布相应事件
			finishRefresh();
		}

		catch (BeansException ex) {
			if (logger.isWarnEnabled()) {
				logger.warn("Exception encountered during context initialization - " +
						"cancelling refresh attempt: " + ex);
			}

			// Destroy already created singletons to avoid dangling resources.
			// 销毁已创建的单例以避免资源悬空
			destroyBeans();

			// Reset 'active' flag.
			// 重置活动标识
			cancelRefresh(ex);

			// Propagate exception to caller.
			// 将异常抛给调用者
			throw ex;
		}

		finally {
			// Reset common introspection caches in Spring's core, since we
			// might not ever need metadata for singleton beans anymore...
			// 13.重置spring核心中常见的内省缓存,因为我们可能不再需要单例bean的元数据...
			resetCommonCaches();
		}
	}
}

二、方法详解

使用IDEA时,ctrl+alt+B 和 ctrl+shift+R 以及 ctrl+alt+←(→) 是很常用的跟踪代码的快捷键。

2.1 #prepareRefresh()

对 refresh 做准备,包括设置开始时间、设置激活状态、初始化 Context 环境中的占位符,这个动作根据子类的需求由子类来执行,然后验证是否缺失必要的 properties。

/**
 * Prepare this context for refreshing, setting its startup date and
 * active flag as well as performing any initialization of property sources.
 * 刷新上下文准备,设置启动时间和活动标识,以及执行属性源的任何初始化
 */
protected void prepareRefresh() {
	// Switch to active.
	// 切换到活动状态
	this.startupDate = System.currentTimeMillis();
	this.closed.set(false);
	this.active.set(true);

	if (logger.isDebugEnabled()) {
		if (logger.isTraceEnabled()) {
			logger.trace("Refreshing " + this);
		}
		else {
			logger.debug("Refreshing " + getDisplayName());
		}
	}

	// Initialize any placeholder property sources in the context environment.
	// 在上下文环境中初始化任何占位符属性源
	initPropertySources();

	// Validate that all properties marked as required are resolvable:
	// see ConfigurablePropertyResolver#setRequiredProperties
	// 验证所有标记为必需的属性都是可解析的
	// 参见ConfigurablePropertyResolver#setRequiredProperties
	getEnvironment().validateRequiredProperties();

	// Store pre-refresh ApplicationListeners...
	// 存储预刷新的ApplicationListeners
	if (this.earlyApplicationListeners == null) {
		this.earlyApplicationListeners = new LinkedHashSet<>(this.applicationListeners);
	}
	else {
		// Reset local application listeners to pre-refresh state.
		// 将本地程序监听器重置为预刷新状态
		this.applicationListeners.clear();
		this.applicationListeners.addAll(this.earlyApplicationListeners);
	}

	// Allow for the collection of early ApplicationEvents,
	// to be published once the multicaster is available...
	// 允许收集早期的ApplicationEvents,一旦多播器可用就将其发布
	this.earlyApplicationEvents = new LinkedHashSet<>();
}

关注 initPropertySources()getEnvironment().validateRequiredProperties()。initPropertySources() 方法内为空,这是 Spring 开放式架构设计,给用户扩展 Spring 的能力,也就是留给子类覆盖,当业务上有参数验证需求的时候,可以通过自定义类继承 ClassPathXMLApplicationContext 重写该方法,将参数验证逻辑写入方法内,并且在使用的时候替换原有的 ClassPathXMLApplicationContext。

2.2 #obtainFreshBeanFactory()

refresh 并获得内部的 Bean Factory。这方法名起的真有水平。

/**
 * Tell the subclass to refresh the internal bean factory.
 * @return the fresh BeanFactory instance
 * @see #refreshBeanFactory()
 * @see #getBeanFactory()
 */
protected ConfigurableListableBeanFactory obtainFreshBeanFactory() {
	refreshBeanFactory();
	return getBeanFactory();
}

其中 refreshBeanFactory() 方法:

/**
 * This implementation performs an actual refresh of this context's underlying
 * bean factory, shutting down the previous bean factory (if any) and
 * initializing a fresh bean factory for the next phase of the context's lifecycle.
 * 该实现会实际刷新此上下文的基础Bean工厂,关闭前一个Bean工厂(如果有)
 * 并为上下文的生命周期的下一个阶段初始化一个新的Bean工厂
 */
@Override
protected final void refreshBeanFactory() throws BeansException {
	if (hasBeanFactory()) {
		destroyBeans();
		closeBeanFactory();
	}
	try {
		// 1.为此上下文创建一个内部bean工厂
		DefaultListableBeanFactory beanFactory = createBeanFactory();
		// 2.指定一个ID以进行序列化,如果需要的话,允许将该BeanFactory从该ID反序列化回BeanFactory对象
		beanFactory.setSerializationId(getId());
		// 3.定制此上下文使用的内部bean工厂
		customizeBeanFactory(beanFactory);
		// 4.将bean定义加载到给定的bean工厂中
		loadBeanDefinitions(beanFactory);
		// 5.设置Context的beanFactory
		synchronized (this.beanFactoryMonitor) {
			this.beanFactory = beanFactory;
		}
	}
	catch (IOException ex) {
		throw new ApplicationContextException("I/O error parsing bean definition source for " + getDisplayName(), ex);
	}
}

关注 customizeBeanFactory(beanFactory) loadBeanDefinitions(beanFactory)

customizeBeanFactory(beanFactory) 定制beanFactory,设置相关属性,包括是否允许同名称的不同定义的对象以及是否允许 bean 之间存在循环依赖。

loadBeanDefinitions(beanFactory) 初始化 XmlBeanDefinitionReader ,设置环境变量(environment、resourceLoader、entityResolver)。在初始化 DefaultListableBeanFactory 和 XmlBeanDefinitionReader 后就可以进行配置文件的读取了。

2.3 #prepareBeanFactory()

对 BeanFactory 进行准备工作,比如设置类加载器和后置处理器,配置不进行自动装配的类型、注册默认的环境 Bean。

/**
 * Configure the factory's standard context characteristics,
 * such as the context's ClassLoader and post-processors.
 * 配置工厂的标准上下文特征,例如上下文的类加载器和后置处理器
 * @param beanFactory the BeanFactory to configure
 */
protected void prepareBeanFactory(ConfigurableListableBeanFactory beanFactory) {
	// Tell the internal bean factory to use the context's class loader etc.
	// 告诉内部bean工厂使用上下文的类加载器等
	beanFactory.setBeanClassLoader(getClassLoader());
	beanFactory.setBeanExpressionResolver(new StandardBeanExpressionResolver(beanFactory.getBeanClassLoader()));
	beanFactory.addPropertyEditorRegistrar(new ResourceEditorRegistrar(this, getEnvironment()));

	// Configure the bean factory with context callbacks.
	// 使用上下文回调配置bean工厂
	beanFactory.addBeanPostProcessor(new ApplicationContextAwareProcessor(this));
	beanFactory.ignoreDependencyInterface(EnvironmentAware.class);
	beanFactory.ignoreDependencyInterface(EmbeddedValueResolverAware.class);
	beanFactory.ignoreDependencyInterface(ResourceLoaderAware.class);
	beanFactory.ignoreDependencyInterface(ApplicationEventPublisherAware.class);
	beanFactory.ignoreDependencyInterface(MessageSourceAware.class);
	beanFactory.ignoreDependencyInterface(ApplicationContextAware.class);

	// BeanFactory interface not registered as resolvable type in a plain factory.
	// MessageSource registered (and found for autowiring) as a bean
	// BeanFactory接口未在普通工厂中注册为可解析类型
	// MessageSource注册为bean(并发现用于自动装配)
	beanFactory.registerResolvableDependency(BeanFactory.class, beanFactory);
	beanFactory.registerResolvableDependency(ResourceLoader.class, this);
	beanFactory.registerResolvableDependency(ApplicationEventPublisher.class, this);
	beanFactory.registerResolvableDependency(ApplicationContext.class, this);

	// Register early post-processor for detecting inner beans as ApplicationListeners.
	// 注册早期的后置处理器以将内部bean检测为ApplicationListeners
	beanFactory.addBeanPostProcessor(new ApplicationListenerDetector(this));

	// Detect a LoadTimeWeaver and prepare for weaving, if found.
	// 如果发现LoadTimeWeaver准备编制
	if (beanFactory.containsBean(LOAD_TIME_WEAVER_BEAN_NAME)) {
		beanFactory.addBeanPostProcessor(new LoadTimeWeaverAwareProcessor(beanFactory));
		// Set a temporary ClassLoader for type matching.
		// 设置一个临时的ClassLoader以进行类型匹配
		beanFactory.setTempClassLoader(new ContextTypeMatchClassLoader(beanFactory.getBeanClassLoader()));
	}

	// Register default environment beans.
	// 注册默认环境bean
	if (!beanFactory.containsLocalBean(ENVIRONMENT_BEAN_NAME)) {
		beanFactory.registerSingleton(ENVIRONMENT_BEAN_NAME, getEnvironment());
	}
	if (!beanFactory.containsLocalBean(SYSTEM_PROPERTIES_BEAN_NAME)) {
		beanFactory.registerSingleton(SYSTEM_PROPERTIES_BEAN_NAME, getEnvironment().getSystemProperties());
	}
	if (!beanFactory.containsLocalBean(SYSTEM_ENVIRONMENT_BEAN_NAME)) {
		beanFactory.registerSingleton(SYSTEM_ENVIRONMENT_BEAN_NAME, getEnvironment().getSystemEnvironment());
	}
}

以上方法主要进行了几个方面的功能扩展:

  • 1. 增加对SpEL语言的支持

SpEL全称 Spring Expression Language,类似于 Struts2 中的 OGNL 表达式,能在运行时构建复杂表达式、存取对象属性、对象方法调用等,并且能与 Spring 功能完美整合,比如能用来配置 bean 定义。SpEL 是单独模块,只依赖于 core 模块,不依赖其他模块,可以单独使用。比如 value=“#{mobilephone}” 的解析。详细可以看 AbstractBeanFactory 中 #evaluateBeanDefinitionString() 方法,查看该方法的调用层次可以看出应用语言解析器的调用主要是在解析依赖注入 bean 的时候,以及在完成 bean 的初始化和属性获取后进行属性填充的时候。

  • 2. 增加对属性编辑器的支持

在 Spring DI 注入的时候可以把普通属性注入进来,但是像 Date 类型就无法被识别,针对类型转换不成功程序抛异常的处理,Spring 提供了两种解决办法:

使用自定义属性编辑器(通过继承 PropertyEditorSupport,重写 setAsText 方法,自定义属性编辑器)和注册 Spring 自带的属性编辑器 CustomDateEditor,此处不作扩展了。

  • 3. 增加对一些内置类,比如 EnvironmentAware、MessageSourceAware的信息注入

添加 ApplicationContextAwareProcessor 处理器。

  • 4. 设置了依赖功能可忽略的接口

当 Spring 将 ApplicationContextAwareProcessor 注册后,那么在 invokeAwareInterfaces 方法中间接调用的 Aware 类已经不是普通的 bean 了,需要在 Spring 做 bean 的依赖注入的时候忽略它们。

  • 5. 注册一些固定依赖的属性

当注册了依赖解析后,例如当注册了对 BeanFactory.class 的解析依赖后,当 bean 的属性注入的时候,一旦检测到属性为 BeanFactory 类型便会将 beanFactory 的实例注入进去。

  • 6. 增加 AspectJ 的支持
  • 7. 将相关环境变量及属性注册以单例模式注册

2.4 #postProcessBeanFactory()

为 Context 的子类提供后置处理 BeanFactory 的扩展能力,如果子类想在 Bean 定义加载完成后、开始初始化上下文之前做一些特殊处理,可以复写这个方法。

/**
 * Register request/session scopes, a {@link ServletContextAwareProcessor}, etc.
 */
@Override
protected void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) {
	// 1.添加ServletContextAwareProcessor到BeanFactory中,该processor实现BeanPostProcessor接口,主要用于将ServletContext传递给实现了ServletContextAware接口的bean
	beanFactory.addBeanPostProcessor(new ServletContextAwareProcessor(this.servletContext, this.servletConfig));
	// 2.忽略ServletContextAware和ServletConfigAware
	beanFactory.ignoreDependencyInterface(ServletContextAware.class);
	beanFactory.ignoreDependencyInterface(ServletConfigAware.class);

	// 3.注册特定于web的作用域例如"request", "session", "globalSession", "application"到给定的BeanFactory中,由Web应用程序上下文使用
	WebApplicationContextUtils.registerWebApplicationScopes(beanFactory, this.servletContext);
	// 4.注册特定于web环境的bean例如"contextParameters", "contextAttributes"到给定的BeanFactory中,由Web应用程序上下文使用
	WebApplicationContextUtils.registerEnvironmentBeans(beanFactory, this.servletContext, this.servletConfig);
}

 

2.5 #invokeBeanFactoryPostProcessors()

调用Context 中注册的 BeanFactory 后置处理器。这里有两种后置处理器,一种是可以注册 Bean 的后置处理器,另一种是针对 BeanFactory 进行处理的后置处理器。执行顺序是先按优先级执行可注册 Bean 的处理器,再按优先级执行针对 BeanFactory 的处理器。

对 Spring Boot 来说,这一步会进行注解 Bean Definition 的解析。流程:由 ConfigurationClassPostProcessor 触发、由 ClassPathBeanDefinitionScanner 解析并注册到 BeanFactory。

/**
 * Instantiate and invoke all registered BeanFactoryPostProcessor beans,
 * respecting explicit order if given.
 * <p>Must be called before singleton instantiation.
 * 实例化并调用所有已注册的BeanFactoryPostProcessor bean,遵守明确的命令(如果有)
 * 必须在单例实例化之前调用
 */
protected void invokeBeanFactoryPostProcessors(ConfigurableListableBeanFactory beanFactory) {
	PostProcessorRegistrationDelegate.invokeBeanFactoryPostProcessors(beanFactory, getBeanFactoryPostProcessors());

	// Detect a LoadTimeWeaver and prepare for weaving, if found in the meantime
	// (e.g. through an @Bean method registered by ConfigurationClassPostProcessor)
	// 检测LoadTimeWeaver并准备编织(如果在此期间发现)
	// 例如通过ConfigurationClassPostProcessor注册的@Bean方法
	if (beanFactory.getTempClassLoader() == null && beanFactory.containsBean(LOAD_TIME_WEAVER_BEAN_NAME)) {
		beanFactory.addBeanPostProcessor(new LoadTimeWeaverAwareProcessor(beanFactory));
		beanFactory.setTempClassLoader(new ContextTypeMatchClassLoader(beanFactory.getBeanClassLoader()));
	}
}

 

2.6 #registerBeanPostProcessors()

按优先级顺序在 BeanFactory 中注册 Bean 的后置处理器,Bean 后置处理器可以在 Bean 初始化前、后执行处理。

/**
 * Instantiate and register all BeanPostProcessor beans,
 * respecting explicit order if given.
 * <p>Must be called before any instantiation of application beans.
 * 实例化并注册所有BeanPostProcessor bean,遵守明确的命令(如果有)
 * 必须在应用程序bean的任何实例化之前调用
 */
protected void registerBeanPostProcessors(ConfigurableListableBeanFactory beanFactory) {
	PostProcessorRegistrationDelegate.registerBeanPostProcessors(beanFactory, this);
}

其中,PostProcessorRegistrationDelegate.java #registerBeanPostProcessors()方法:

public static void registerBeanPostProcessors(
		ConfigurableListableBeanFactory beanFactory, AbstractApplicationContext applicationContext) {

	String[] postProcessorNames = beanFactory.getBeanNamesForType(BeanPostProcessor.class, true, false);

	// Register BeanPostProcessorChecker that logs an info message when
	// a bean is created during BeanPostProcessor instantiation, i.e. when
	// a bean is not eligible for getting processed by all BeanPostProcessors.
	int beanProcessorTargetCount = beanFactory.getBeanPostProcessorCount() + 1 + postProcessorNames.length;
	beanFactory.addBeanPostProcessor(new BeanPostProcessorChecker(beanFactory, beanProcessorTargetCount));

	// Separate between BeanPostProcessors that implement PriorityOrdered,
	// Ordered, and the rest.
	List<BeanPostProcessor> priorityOrderedPostProcessors = new ArrayList<>();
	List<BeanPostProcessor> internalPostProcessors = new ArrayList<>();
	List<String> orderedPostProcessorNames = new ArrayList<>();
	List<String> nonOrderedPostProcessorNames = new ArrayList<>();
	for (String ppName : postProcessorNames) {
		if (beanFactory.isTypeMatch(ppName, PriorityOrdered.class)) {
			BeanPostProcessor pp = beanFactory.getBean(ppName, BeanPostProcessor.class);
			priorityOrderedPostProcessors.add(pp);
			if (pp instanceof MergedBeanDefinitionPostProcessor) {
				internalPostProcessors.add(pp);
			}
		}
		else if (beanFactory.isTypeMatch(ppName, Ordered.class)) {
			orderedPostProcessorNames.add(ppName);
		}
		else {
			nonOrderedPostProcessorNames.add(ppName);
		}
	}

	// First, register the BeanPostProcessors that implement PriorityOrdered.
	sortPostProcessors(priorityOrderedPostProcessors, beanFactory);
	registerBeanPostProcessors(beanFactory, priorityOrderedPostProcessors);

	// Next, register the BeanPostProcessors that implement Ordered.
	List<BeanPostProcessor> orderedPostProcessors = new ArrayList<>(orderedPostProcessorNames.size());
	for (String ppName : orderedPostProcessorNames) {
		BeanPostProcessor pp = beanFactory.getBean(ppName, BeanPostProcessor.class);
		orderedPostProcessors.add(pp);
		if (pp instanceof MergedBeanDefinitionPostProcessor) {
			internalPostProcessors.add(pp);
		}
	}
	sortPostProcessors(orderedPostProcessors, beanFactory);
	registerBeanPostProcessors(beanFactory, orderedPostProcessors);

	// Now, register all regular BeanPostProcessors.
	List<BeanPostProcessor> nonOrderedPostProcessors = new ArrayList<>(nonOrderedPostProcessorNames.size());
	for (String ppName : nonOrderedPostProcessorNames) {
		BeanPostProcessor pp = beanFactory.getBean(ppName, BeanPostProcessor.class);
		nonOrderedPostProcessors.add(pp);
		if (pp instanceof MergedBeanDefinitionPostProcessor) {
			internalPostProcessors.add(pp);
		}
	}
	registerBeanPostProcessors(beanFactory, nonOrderedPostProcessors);

	// Finally, re-register all internal BeanPostProcessors.
	sortPostProcessors(internalPostProcessors, beanFactory);
	registerBeanPostProcessors(beanFactory, internalPostProcessors);

	// Re-register post-processor for detecting inner beans as ApplicationListeners,
	// moving it to the end of the processor chain (for picking up proxies etc).
	beanFactory.addBeanPostProcessor(new ApplicationListenerDetector(applicationContext));
}

 

(本文未完待续,接下一篇)

Spring源码系列【二】——Spring框架概述

1.源码分析前的123
2.Spring框架组件
3.后续思路

一、写在前面的废话

上一篇搭建了 Spring 源码分析的环境,这一篇正式开始了,在进入正题之前先问自己几个问题:

1、为什么要看 Spring 源码?

2、想要分析到哪个层面?

3、如何看 Spring 源码?

……

问题1,不用说了,Spring 框架是众多优秀框架之一,也是 java 从业者必会框架,网上关于 Spring 的赞誉之词不胜枚举,既是用/学一个东西,在用/学之前总要问这个东西为什么会出现?是为了解决哪些问题?研究要有个整体思路,切忌上来就一头扎进代码中,容易“只见树木不见森林”。看源码是为了更深入的了解框架,当出现需求时能抓住切入点,更好的运用、扩展框架。

问题2,Spring 框架十多个组件,在这里并不对所有组件都详细了解,如果确实有需要钻研的当然就另说了,本人这轮打算重点了解常用几个组件实现,以及知道每个组件用于实现哪一类功能就 OK 了。希望做到“每有会意便欣然忘食”,常学常新。

问题3,目前个人功力不够,大局观有限,先抽丝剥茧地一点点看了,在看的过程中会不断更新这部分。

在阅读源码的过程中只需要关心核心的主流程,了解其工作原理,并在阅读的过程中感受它的代码风格以及设计理念就好了,如果真的追求理解每一行代码真的是非常耗时的一件事,毕竟阅读源码的目的大多数是成长而不是真的要去维护 Spring。

网上资料千千万,同一个东西每个人都有不同的见解,总结的一个比一个精彩。但是,古语云:纸上得来终觉浅,绝知此事要躬行。亲自下手带有目的性的研究印象更深、效果更好些。废话不多说了。

二、Spring 组件

2.1 spring-framework 代码

2.2 Spring 框架组件

上图是 Spring4.x 的文档,目前5.x版本中的 Portlet 组件已经被废弃掉了,同时增加了用于异步响应式处理的 WebFlux 组件。红框框住的是比较重要的组件:

Core 组件是 Spring 所有组件的核心;

Bean 组件和 Context 组件是实现 IoC依赖注入的基础;

AOP 组件用于实现切面编程;

Web 组件包括 SpringMVC,是 Web 服务的控制层实现。

三、后续思路

简单说 Spring 就 IoC、AOP,而 Context 和 Bean 又是实现 IoC 和 依赖注入的基础,所以在接下来的代码分析中也将围绕这几个重要的组件进行梳理,并从 Context 说起……

(本文完)

Spring源码系列【一】——调试环境搭建

1.获取仓库地址
2.配置Gradle
3.IDEA检出代码

一、操作环境

Win10,Jdk1.8,IDEA 2019,spring-framework Version 5.2.3

二、获取仓库地址

2.1 Spring 官方源码

源码下载地址:https://github.com/xiguanchendian/spring-framework

GitHub下载代码速度巨慢,此时有个曲线救国的骚操作:在“码云”新建仓库,导入GitHub已有仓库,然后从gitee检出效率就高多了。

我的仓库地址传送门:https://gitee.com/liangyadong/spring-framework

 

2.2 Spring 官方文档

Spring Projects:https://spring.io/docs/reference

Spring Framework :https://docs.spring.io/spring/docs/5.2.3.BUILD-SNAPSHOT/spring-framework-reference/

三、Gradle配置

3.1 下载Gradle5.6.4

官方地址:https://gradle.org/releases/

下载complete版,130M左右,解压到指定目录。

3.2 配置Gradle环境变量

比如我的:E:\Develop\gradle-5.6.4\bin添加到系统环境变量,通过命令 gradle -v 验证,出现以下信息证明已经配置成功了:

这里提一下,我开始下载的是Gradle6.0.1,结果检出代码后build时报版本不匹配问题。需要降一下gradle的版本。报错信息:

The build scan plugin is not compatible with Gradle 6.0 and later.
Please use the Gradle Enterprise plugin instead.

关于版本的问题据说还要考虑IDEA版本,心里有数就行,避免踩坑,实在不行IDEA配置gradle时先选择 Use default Gradle wrapper (recommended) 。

四、IDEA检出代码

“码云”添加本机的SSH公钥,和GitHub操作一样,不赘述了。

代码检出路径就是上面的传送门。

IDEA中gradle配置:

检出完毕截图:

 

五、运行示例

打开 IDEA Terminal ,输入如下命令,预编译 spring-oxm 项目:

gradle g-oxm:compileTestJava

结果如下:

Microsoft Windows [版本 10.0.17134.984]
(c) 2018 Microsoft Corporation。保留所有权利。

F:\IDEA\spring-framework>gradle :spring-oxm:compileTestJava

BUILD SUCCESSFUL in 5s
39 actionable tasks: 39 up-to-date
F:\IDEA\spring-framework>

5.1 解析 XML 配置文件成对应的 BeanDefinitions 的流程

可调试 org.springframework.beans.factory.xml.XmlBeanDefinitionReaderTests 的 withFreshInputStream() 和 withImport() 这两个单元测试,相比来说,后者比前者多了一个 <import /> 标签的解析。

这里只执行一次 withImport() 方法,验证环境有没有问题:

OK,环境搭建好,开始懵逼这么多模块从哪下手看呢?下一篇开始真正的 Spring 源码分析。

 

(本文完)

SQL性能优化系列——何为悲观锁和乐观锁[十一]

1.锁的划分方式有哪些?
2.为什么共享锁会发生死锁?
3.乐观锁和悲观锁的思想是什么?乐观锁的两种实现方式?
4.多个事务并发,发生死锁该如何解决?如何降低死锁发生的概率?

索引和锁是数据库中的两个核心知识点,之前从不同维度对索引进行了了解,比如 B+ 树、Hash 索引、页结构、缓冲池和索引原则等,了解它们的工作原理可以加深对索引的理解。同时事务的隔离级别的实现都是通过锁来完成的,为什么要给数据加锁?

实际上加锁是为了保证数据的一致性,这个思想在程序开发领域中同样很重要。在程序开发中也会存在多线程同步的问题。当多个线程并发访问某个数据的时候,尤其是针对一些敏感的数据(比如订单、金额等),就需要保证这个数据在任何时刻最多只有一个线程在进行访问,保证数据的完整性和一致性。

按照锁粒度划分

从锁定对象的粒度大小进行划分,分为行锁页锁表锁

行锁就是按照行的粒度对数据进行锁定。锁定力度小,发生锁冲突概率低,可以实现的并发度高,但是对于锁的开销比较大,加锁会比较慢,容易出现死锁情况。

页锁就是在页的粒度上进行锁定,锁定的数据资源比行锁要多,因为一个页中可以有多个行记录。当使用页锁的时候,会出现数据浪费的现象,但这样的浪费最多也就是一个页上的数据行。页锁的开销介于表锁和行锁之间,会出现死锁。锁定粒度介于表锁和行锁之间,并发度一般。

表锁就是对数据表进行锁定,锁定粒度很大,同时发生锁冲突的概率也会较高,数据访问的并发度低。不过好处在于对锁的使用开销小,加锁会很快。

行锁、页锁和表锁是相对常见的三种锁,除此以外还可以在区和数据库的粒度上锁定数据,对应区锁和数据库锁。不同的数据库和存储引擎支持的锁粒度不同,InnoDB 和 Oracle 支持行锁和表锁。而 MyISAM 只支持表锁,MySQL 中的 BDB 存储引擎支持页锁和表锁。SQL Server 可以同时支持行锁、页锁和表锁,如下表所示:

需要说明的是,每个层级的锁数量是有限制的,因为锁会占用内存空间,锁空间的大小是有限的。当某个层级的锁数量超过了这个层级的阈值时,就会进行锁升级。锁升级就是用更大粒度的锁替代多个更小粒度的锁,比如 InnoDB 中行锁升级为表锁,这样做的好处是占用的锁空间降低了,但同时数据的并发度也下降了。

从数据库管理角度对锁划分

从数据库管理的角度对锁可以分为共享锁排他锁

共享锁

共享锁也叫读锁或 S 锁,共享锁锁定的资源可以被其他用户读取,但不能修改。在进行SELECT的时候,会将对象进行共享锁锁定,当数据读取完毕之后,就会释放共享锁,这样就可以保证数据在读取时不被修改。

比如我们想给 product_comment 在表上加共享锁,可以使用下面这行命令:

LOCK TABLE product_comment READ;

当对数据表加上共享锁的时候,该数据表就变成了只读模式,此时想要更新 product_comment 表中的数据,比如下面这样:

UPDATE product_comment SET product_id = 10002 WHERE user_id = 912178;

系统会做如下提示:

ERROR 1099 (HY000): Table 'product_comment' was locked with a READ lock and can't be updated

也就是当共享锁没有释放时,不能对锁住的数据进行修改。

如果我们想要对表上的共享锁进行解锁,可以使用下面这行命令:

UNLOCK TABLE;

如果想要给某一行加上共享锁呢,比如想对 user_id=912178 的数据行加上共享锁,可以像下面这样:

SELECT comment_id, product_id, comment_text, user_id FROM product_comment WHERE user_id = 912178 LOCK IN SHARE MODE

排他锁

排它锁也叫独占锁、写锁或 X 锁。排它锁锁定的数据只允许进行锁定操作的事务使用,其他事务无法对已锁定的数据进行查询或修改。

如果想给 product_comment 数据表添加排它锁,可以使用下面这行命令:

LOCK TABLE product_comment WRITE;

这时只有获得排它锁的事务可以对 product_comment 进行查询或修改,其他事务如果想要在 product_comment 表上查询数据,则需要等待。可以自己开两个 MySQL 客户端来模拟下。

释放排他锁命令:

UNLOCK TABLE;

同样的,如果想要在某个数据行上添加排它锁,比如针对 user_id=912178 的数据行,则写成如下这样:

SELECT comment_id, product_id, comment_text, user_id FROM product_comment WHERE user_id = 912178 FOR UPDATE;

另外当我们对数据进行更新的时候,也就是INSERT、DELETE、UPDATE时,数据库也会自动使用排它锁,防止其他事务对该数据行进行操作。

当想要获取某个数据表的排它锁的时候,需要先看下这张数据表有没有上了排它锁。如果这个数据表中的某个数据行被上了行锁,就无法获取排它锁。这时需要对数据表中的行逐一排查,检查是否有行锁,如果没有,才可以获取这张数据表的排它锁。这个过程是不是有些麻烦?这里就需要用到意向锁。

意向锁

意向锁(Intent Lock),简单来说就是给更大一级别的空间示意里面是否已经上过锁。举个例子,你可以给整个房子设置一个标识,告诉它里面有人,即使你只是获取了房子中某一个房间的锁。这样其他人如果想要获取整个房子的控制权,只需要看这个房子的标识即可,不需要再对房子中的每个房间进行查找。这样是不是很方便?

返回数据表的场景,如果给某一行数据加上了排它锁,数据库会自动给更大一级的空间,比如数据页或数据表加上意向锁,告诉其他人这个数据页或数据表已经有人上过排它锁了,这样当其他人想要获取数据表排它锁的时候,只需要了解是否有人已经获取了这个数据表的意向排他锁即可。

如果事务想要获得数据表中某些记录的共享锁,就需要在数据表上添加意向共享锁。同理,事务想要获得数据表中某些记录的排他锁,就需要在数据表上添加意向排他锁。这时,意向锁会告诉其他事务已经有人锁定了表中的某些记录,不能对整个表进行全表扫描。

为什么共享锁会发生死锁情况

当使用共享锁的时候会出现死锁的风险,下面用两个 MySQL 客户端来模拟一下事务查询。

首先客户端 1 开启事务,然后采用读锁的方式对user_id=912178的数据行进行查询,这时事务没有提交的时候,这两行数据行上了读锁。

然后我们用客户端 2 开启事务,同样对user_id=912178获取读锁,理论上获取读锁后还可以对数据进行修改,比如执行下面这条语句:

UPDATE product_comment SET product_i = 10002 WHERE user_id = 912178;

当执行的时候客户端 2 会一直等待,因为客户端 1 也获取了该数据的读锁,不需要客户端 2 对该数据进行修改。这时客户端 2 会提示等待超时,重新执行事务。

当有多个事务对同一数据获得读锁的时候,可能会出现死锁的情况

从程序员的角度划分

从程序猿的角度可以分为乐观锁和悲观锁。

乐观锁

乐观锁(Optimistic Locking)认为对同一数据的并发操作不会总发生,属于小概率事件,不用每次都对数据上锁,也就是不采用数据库自身的锁机制,而是通过程序来实现。在程序上,我们可以采用版本号机制或者时间戳机制实现。

乐观锁的版本号机制

在表中设计一个版本字段 version,第一次读的时候,会获取 version 字段的取值。然后对数据进行更新或删除操作时,会执行UPDATE … SET version=version+1 WHERE version=version。此时如果已经有事务对这条数据进行了更改,修改就不会成功。

这种方式类似 SVN、CVS 版本管理系统,当修改了代码进行提交时,首先会检查当前版本号与服务器上的版本号是否一致,如果一致就可以直接提交,如果不一致就需要更新服务器上的最新代码,然后再进行提交。

乐观锁的时间戳机制

时间戳和版本号机制一样,也是在更新提交的时候,将当前数据的时间戳和更新之前取得的时间戳进行比较,如果两者一致则更新成功,否则就是版本冲突。

你能看到乐观锁就是程序员自己控制数据并发操作的权限,基本是通过给数据行增加一个戳(版本号或者时间戳),从而证明当前拿到的数据是否最新。

悲观锁

悲观锁(Pessimistic Locking)也是一种思想,对数据被其他事务的修改持保守态度,会通过数据库自身的锁机制来实现,从而保证数据操作的排它性。

从这两种锁的设计思想中,能看出乐观锁和悲观锁的适用场景:

1.乐观锁适合读操作多的场景,相对来说写的操作比较少。它的优点在于程序实现,不存在死锁问题,不过适用场景也会相对乐观,因为它阻止不了除了程序以外的数据库操作。

2.悲观锁适合写操作多的场景,因为写的操作具有排它性。采用悲观锁的方式,可以在数据库层面阻止其他事务对该数据的操作权限,防止读 – 写和写 – 写的冲突。

总结

从不同维度都可以对锁进行划分,需要注意的是,乐观锁和悲观锁并不是锁,而是锁的设计思想

既然有锁的存在,就有可能发生死锁的情况。死锁就是多个事务(如果是在程序层面就是多个进程)在执行过程中,因为竞争某个相同的资源而造成阻塞的现象。发生死锁,往往是因为在事务中,锁的获取是逐步进行的。

上面的例子,在客户端 1 获取某数据行共享锁的同时,另一个客户端 2 也获取了该数据行的共享锁,这时任何一个客户端都没法对这个数据进行更新,因为共享锁会阻止其他事务对数据的更新,当某个客户端想要对锁定的数据进行更新的时候,就出现了死锁的情况。当死锁发生的时候,就需要一个事务进行回滚,另一个事务获取锁完成事务,然后将锁释放掉,很像交通堵塞时候的解决方案。

如何避免死锁发生

1.如果事务涉及多个表,操作比较复杂,那么可以尽量一次锁定所有的资源,而不是逐步来获取,这样可以减少死锁发生的概率;

2.如果事务需要更新数据表中的大部分数据,数据表又比较大,这时可以采用锁升级的方式,比如将行级锁升级为表级锁,从而减少死锁产生的概率;

3.不同事务并发读写多张数据表,可以约定访问表的顺序,采用相同的顺序降低死锁发生的概率。

当然在数据库中,也有一些情况是不会发生死锁的,比如采用乐观锁的方式。另外在 MySQL MyISAM 存储引擎中也不会出现死锁,这是因为 MyISAM 总是一次性获得全部的锁,这样的话要么全部满足可以执行,要么就需要全部等待。

思考题

使用 MySQL InnoDB 存储引擎时,为什么对某行数据添加排它锁之前,会在数据表上添加意向排他锁呢?

(本文完)

SQL性能优化系列——为何没有理想的索引[十]

1. 什么是索引片?如何计算过滤因子?
2.设计索引可遵循的原则有哪些?
3.为什么理想的索引在实际工作中很难应用起来?

索引片和过滤因子

索引片就是 SQL 查询语句在执行中需要扫描的一个索引片段,我们会根据索引片中包含的匹配列的数量不同,将索引分成窄索引(比如包含索引列数为 1 或 2)和宽索引(包含的索引列数大于 2)。

如果索引片越宽,那么需要顺序扫描的索引页就越多;如果索引片越窄,就会减少索引访问的开销。比如在 product_comment 数据表中,我们将 comment_id 设置为主键,然后执行下面的 SQL 查询语句:

SELECT comment_id, product_id, comment_text, user_id FROM product_comment WHERE user_id between 100001 and 100100

针对这条 SQL 查询语句,我们可以设置窄索引(user_id)。需要说明的是,每个非聚集索引保存的数据都会存储主键值,然后通过主键值,来回表查找相应的数据,因此每个索引都相当于包括了主键,也就是(comment_id, user_id)。

同样我们可以设置宽索引(user_id, product_id, comment_text),相当于包括了主键,也就是(comment_id, user_id, product_id, comment_text)。

如何通过宽索引避免回表

上面说了宽索引需要顺序扫描的索引页很多,不过它也可以避免通过索引找到主键,再通过主键回表进行数据查找的情况。回表指的就是数据库根据索引找到了数据行之后,还需要通过主键再次到数据表中读取数据的情况。

可以用不同索引片来运行下刚才的 SQL 语句,比如采用窄索引(user_id)的方式,来执行下面这条语句:

SELECT comment_id, product_id, comment_text, user_id FROM product_comment WHERE user_id between 100001 and 100100

运行结果(110 条记录,运行时间 0.062s)

同样,如果我们设置宽索引(user_id, product_id, comment_text),然后执行相同的 SQL 语句,运行结果相同,运行时间为 0.043s,你能看到查询效率有了一些提升。这就是因为我们可以通过宽索引将 SELECT 中需要用到的列(主键列可以除外)都设置在宽索引中,这样就避免了回表扫描的情况,从而提升 SQL 查询效率。

什么是过滤因子

在索引片的设计中,我们还需要考虑一个因素,那就是过滤因子,它描述了谓词的选择性。在 WHERE 条件语句中,每个条件都称为一个谓词,谓词的选择性也等于满足这个条件列的记录数除以总记录数的比例。

过滤因子就是过滤条件,联合过滤因子有更高的过滤能力,这里还需要注意一个条件,那就是条件列的关联性应该尽量相互独立,否则如果列与列之间具有相关性,联合过滤因子的能力就会下降很多。比如城市名称和电话区号就有强相关性,这两个列组合到一起不会加强过滤效果。

你能看到过滤因子决定了索引片的大小(注意这里不是窄索引和宽索引),过滤因子的条件过滤能力越强,满足条件的记录数就越少,SQL 查询需要扫描的索引片也就越小。同理,如果我们没有选择好索引片中的过滤因子,就会造成索引片中的记录数过多的情况。

针对SQL查询的理想索引设计:三星索引

刚才介绍了宽索引和窄索引,有些时候宽索引可以提升 SQL 的查询效率,那么,如果针对 SQL 查询来说,有没有一个标准能让 SQL 查询效率最大化呢?

实际上存在一个三星索引的标准,好比是数据表设计时提到的三范式一样:

1.在 WHERE 条件语句中,找到所有等值谓词中的条件列,将它们作为索引片中的开始列;

2.将 GROUP BY 和 ORDER BY 中的列加入到索引中;

3.将 SELECT 字段中剩余的列加入到索引片中。

这样操作下来,索引片基本上会变成一个宽索引,把能添加的相关列都加入其中。对于一条 SQL 查询来说,这样做的效率是最高的吗?(怎么理解上面3条呢?)

首先,如果要通过索引查找符合条件的记录,就需要将 WHERE 子句中的等值谓词列加入到索引片中,这样索引的过滤能力越强,最终扫描的数据行就越少。

另外,如果要对数据记录分组或者排序,都需要重新扫描数据记录。为了避免进行 file sort 排序,可以把 GROUP BY 和 ORDER BY 中涉及到的列加入到索引中,因为创建了索引就会按照索引的顺序来存储数据,这样再对这些数据按照某个字段进行分组或者排序的时候,就会提升效率。

三星索引的逻辑就是:最小化碎片、避免排序、避免回表查询。

最后,取数据的时候,可能会存在回表情况,这是因为 SELECT 所需的字段并不都保存在索引中,因此可以将 SELECT 中的字段都保存在索引中避免回表的情况,从而提升查询效率。

为什么很难存在理想的索引设计

从三星索引的创建过程中,你能看到三星索引实际上分析了在 SQL 查询过程中所有可能影响效率的环节,通过在索引片中添加索引的方式来提升效率。通过上面的原则,我们可以很快创建一个 SQL 查询语句的三星索引(有时候可能只有两星,比如同时拥有范围谓词和 ORDER BY 的时候)。

但就同三范式一样,很多时候并没有遵循三范式的设计原则,而是采用了反范式设计。同样,有时候并不能需要完全遵循三星索引的原则,原因主要有以下两点:

1.采用三星索引会让索引片变宽,这样每个页能够存储的索引数据就会变少,从而增加了页加载的数量。从另一个角度来看,如果数据量很大,比如有 1000 万行数据,过多索引所需要的磁盘空间可能会成为一个问题,对缓冲池所需空间的压力也会增加。

2.增加了索引维护的成本。如果为所有的查询语句都设计理想的三星索引,就会让数据表中的索引个数过多,这样索引维护的成本也会增加。举个例子,当添加一条记录的时候,就需要在每一个索引上都添加相应的行(存储对应的主键值),假设添加一行记录的时间成本是 10ms(磁盘随机读取一个页的时间),那么如果创建了 10 个索引,添加一条记录的时间就可能变成 0.1s,如果是添加 10 条记录呢?就会花费近 1s 的时间。从索引维护的成本来看消耗还是很高的。当然对于数据库来说,数据的更新不一定马上回写到磁盘上,但即使不及时将脏页进行回写,也会造成缓冲池中的空间占用过多,脏页过多的情况。

总结

针对一条 SQL 查询来说,三星索引是个理想的方式,但实际运行起来要考虑更多维护的成本,在索引效率和索引维护之间进行权衡。

三星索引会让索引变宽,好处就是不需要进行回表查询,减少了磁盘 I/O 的次数,弊端就是会造成频繁的页分裂和页合并,对于数据的插入和更新来说,效率会降低不少。

那如何设计索引?

首先,一张表的索引个数不宜过多,否则一条记录的增加和修改,会因为过多的索引造成额外的负担。针对这个情况,当你需要新建索引的时候,首先考虑在原有的索引片上增加索引,也就是采用复合索引的方式,而不是新建一个新的索引。另外可以定期检查索引的使用情况,对于很少使用到的索引可以及时删除,从而减少索引数量。

同时,在索引片中也需要控制索引列的数量,通常情况下我们将 WHERE 里的条件列添加到索引中,而 SELECT 中的非条件列则不需要添加。除非 SELECT 中的非条件列数少,并且该字段会经常使用到。

另外,单列索引和复合索引的长度也需要控制,在 MySQL InnoDB 中,系统默认单个索引长度最大为 767 bytes,如果单列索引长度超过了这个限制,就会取前缀索引,也就是取前 255 字符。这实际上也是告诉我们,字符列会占用较大的空间,在数据表设计的时候,尽量采用数值类型替代字符类型,尽量避免用字符类型做主键,同时针对字符字段最好只建前缀索引

思考题

针对下面的 SQL 语句,如果创建三星索引该如何创建?使用三星索引和不使用三星索引在查询效率上又有什么区别?

SELECT comment_id, comment_text, user_id FROM product_comment where user_id BETWEEN 100000 AND 200000

(本文完)

SQL性能优化系列——从磁盘I/O角度理解SQL查询成本[九]

1.数据库的缓冲池在数据库中起到了怎样的作用?如果对缓冲池内的数据进行更新,数据会直接更新到磁盘上么?
2.对数据页加载都有哪些方式?
3.如何查看一条SQL语句在缓冲池中进行加载的页的数量?

数据库存储的基本单元是页,即使是查询一行数据,但是对于磁盘I/O来说要加载该行数据所在的一页的信息,因为页是最小的存储单位。如果查询数据是多行,查询时间是否会成本增加呢?其实数据库会采用缓冲池的方式提升页的查询效率。

数据库缓冲池

磁盘I/O需要消耗很多时间,而在内存中操作则效率会提升很多,为了能让数据表或索引中的数据随时被我们所用,DBMS会申请占用内存来作为数据缓冲池,这样可以让磁盘活动最小化,从而减少与磁盘直接进行I/O的时间,这样访问成本会低很多。

缓冲池如何读取数据呢?

缓冲池管理器会尽量将经常使用的数据保存起来,当数据库进行页面读操作的时候首先会判断该页是否在缓冲池中,如果存在就直接读取,如果不在就通过内存或磁盘将页面保存到缓冲池中再进行读取。

如果执行SQL语句更新了缓冲池中的数据,这些数据会立即更新到磁盘上么?

事实上,当对数据库中的记录进行修改时,首先修改的是缓冲池中页里面的记录信息,然后数据库才会以一定的频率刷新到磁盘上。并不是每一次更新都会立即进行磁盘回写。缓冲池会采用一种叫做checkpoint的机制将数据回写到磁盘上,这样能提升数据库的整体性能。比如,当缓冲池不够用时,需要释放掉一些不常用的页,就采用checkpoint的方式将不常用的脏页回写到磁盘上,然后从缓冲池中释放掉这些页。脏页(dirty page)是指缓冲池中被修改过的页,与磁盘上的页不一致。

查看缓冲池大小

如果使用的是MySQL的 MyISAM 存储引擎,它只缓存索引,不缓存数据,对应的键缓存参数为 key_buffer_size 可以进行查看。

如果使用的是InnoDB 存储引擎,可以通过查看 innodb_buffer_size 变量查看缓冲池大小,如:

mysql > show variables like 'innodb_buffer_pool_size'

也可以通过命令修改缓冲池大小(如修改为128M):

mysql > set global innodb_buffer_pool_size = 134217728;

另外,在InnoDB存储引擎中可以开启多个缓冲池,通过命令可以查看缓冲池的数量:

mysql > show variables like 'innodb_buffer_pool_instances'

实际上innodb_buffer_pool_instances默认情况下是8,为什么这里只显示为1?这是因为需要先将innodb_buffer_pool_size设置为大于1GB,缓冲池个数才会大于1。

数据页加载的三种方式

缓冲池中没有数据时,有三种读取数据方式,每种方式效率不同。

1.内存读取

如果数据存储在内存中,都取到缓冲池中约1ms,效率还是很高的。

2.随机读取

如果数据没有在内存中,就需要在磁盘上对该页进行查找,整体时间预估在 10ms 左右,这 10ms 中有 6ms 是磁盘的实际繁忙时间(包括了寻道和半圈旋转时间),有 3ms 是对可能发生的排队时间的估计值,另外还有 1ms 的传输时间,将页从磁盘服务器缓冲区传输到数据库缓冲区中。这 10ms 看起来很快,但实际上对于数据库来说消耗的时间已经非常长了,因为这还只是一个页的读取时间。

3.顺序读取

顺序读取其实是一种批量读取的方式,因为我们请求的数据在磁盘上往往都是相邻存储的,顺序读取可以帮我们批量读取页面,这样的话,一次性加载到缓冲池中就不需要再对其他页面单独进行磁盘 I/O 操作了。如果一个磁盘的吞吐量是 40MB/S,那么对于一个 16KB 大小的页来说,一次可以顺序读取 2560(40MB/16KB)个页,相当于一个页的读取时间为 0.4ms。采用批量读取的方式,即使是从磁盘上进行读取,效率也比从内存中只单独读取一个页的效率要高。

统计SQL语句的查询成本

前面说过,一条 SQL 查询语句在执行前需要确定查询计划,如果存在多种查询计划的话,MySQL 会计算每个查询计划所需要的成本,从中选择成本最小的一个作为最终执行的查询计划。

如果我们想要查看某条 SQL 语句的查询成本,可以在执行完这条 SQL 语句之后,通过查看当前会话中的 last_query_cost 变量值来得到当前查询的成本。这个查询成本对应的是 SQL 语句所需要读取的页的数量。

以 product_comment 表为例,如果我们想要查询 comment_id=900001 的记录,然后看下查询成本,我们可以直接在聚集索引上进行查找:

mysql> SELECT comment_id, product_id, comment_text, user_id FROM product_comment WHERE comment_id = 900001;

然后再看下查询优化器的成本,实际上我们只需要检索一个页即可:

mysql> SHOW STATUS LIKE 'last_query_cost';

如果我们想要查询 comment_id 在 900001 到 9000100 之间的评论记录呢?

mysql> SELECT comment_id, product_id, comment_text, user_id FROM product_comment WHERE comment_id BETWEEN 900001 AND 900100;

然后再看下查询优化器的成本,这时我们大概需要进行 20 个页的查询。

mysql> SHOW STATUS LIKE 'last_query_cost';

你能看到页的数量是刚才的 20 倍,但是查询的效率并没有明显的变化,实际上这两个 SQL 查询的时间基本上一样,就是因为采用了顺序读取的方式将页面一次性加载到缓冲池中,然后再进行查找。虽然页数量(last_query_cost)增加了不少,但是通过缓冲池的机制,并没有增加多少查询时间。

总结

SQL查询是一个动态过程,从页加载的角度看,可以得到以下两点结论:

1.位置决定效率。如果页就在数据库缓冲池中,那么效率是最高的,否则还需要从内存或者磁盘中进行读取,当然针对单个页的读取来说,如果页存在于内存中,会比在磁盘中读取效率高很多。

2.批量决定效率。如果我们从磁盘中对单一页进行随机读,那么效率是很低的(差不多 10ms),而采用顺序读取的方式,批量对页进行读取,平均一页的读取效率就会提升很多,甚至要快于单个页面在内存中的随机读取。

所以说,遇到 I/O 并不用担心,方法找对了,效率还是很高的。我们首先要考虑数据存放的位置,如果是经常使用的数据就要尽量放到缓冲池中,其次我们可以充分利用磁盘的吞吐能力,一次性批量读取数据,这样单个页的读取效率也就得到了提升。

(本文完)

SQL性能优化系列——从数据页角度理解B+树查询[八]

1.数据库中的存储结构是怎样的?
2.为什么页是数据库存储空间的基本单位?
3.从数据页角度看,B+树是怎样进行查询的?

B+树和 Hash 索引的原理前面已经说过了,这些索引结构给我们提供了高效的检索方式,不过这些索引信息及数据记录都是存储在文件上的,确切地说是存储在页结构中。对数据库存储结构以及页结构的底层了解可以加深对索引运行机制的认识,从而对索引的存储、查询原理、以及对SQL查询效率有更深的理解。

数据库中的存储结构

记录是按照行存储的,但是数据库读取并不以行为单位,否则一次读取(也就是一次I/O操作)只能处理一行数据,效率会非常低。因此在数据库中,不论读一行,还是读多行,都是将这些行所在的页进行加载。也即是说,数据库管理存储空间的基本单位是页(Page)

一个页中可以存储多个行记录(Row),同时数据库中还存在区(Extent)、段(Segment)和表空间(Tablespace)。行、页、区、段、表空间的关系如图所示:

从图可以看到一个表空间包括一个或多个段、一个段包括一个或多个区、一个区包括多个页、一个页又包含多行记录。

区(Extent)是比页大一级的存储结构,在 InnoDB 存储引擎中,一个区会分配 64 个连续的页。因为 InnoDB 中的页大小默认是 16KB,所以一个区的大小是 64*16KB=1MB。

段(Segment)由一个或多个区组成,区在文件系统是一个连续分配的空间(在 InnoDB 中是连续的 64 个页),不过在段中不要求区与区之间是相邻的。段是数据库中的分配单位,不同类型的数据库对象以不同的段形式存在。当我们创建数据表、索引的时候,就会相应创建对应的段,比如创建一张表时会创建一个表段,创建一个索引时会创建一个索引段。

表空间(Tablespace)是一个逻辑容器,表空间存储的对象是段,在一个表空间中可以有一个或多个段,但是一个段只能属于一个表空间。数据库由一个或多个表空间组成,表空间从管理上可以划分为系统表空间、用户表空间、撤销表空间、临时表空间等。

在 InnoDB 中存在两种表空间的类型:共享表空间和独立表空间。如果是共享表空间就意味着多张表共用一个表空间。如果是独立表空间,就意味着每张表有一个独立的表空间,也就是数据和索引信息都会保存在自己的表空间中。独立的表空间可以在不同的数据库之间进行迁移。

通过命令可以查看InnoDB表空间类型:

mysql > show variables like 'innodb_file_per_table';

你能看到 innodb_file_per_table=ON,这就意味着每张表都会单独保存为一个.ibd 文件。

数据页内的结构

页如果按类型分,常见的有数据页(保存B+树节点)、系统页、Undo页和事务数据页。其中数据页是最常用的页。

表页的大小限定了表行的最大长度,不同DBMS的表页大小不同,MySQL的InnoDB存储引擎中,默认页的大小是16KB,可以通过命令查看:

mysql> show variables like '%innodb_page_size%';

另,SQLServer的页大小为8KB,而在Oracle中用术语块(Block)代表页,支持的块大小为2、4、8、16、32和64KB。

数据库 I/O 操作的最小单位是页,与数据库相关的内容都会存储在页结构里。数据页包括七个部分,分别是文件头(File Header)、页头(Page Header)、最大最小记录(Infimum+supremum)、用户记录(User Records)、空闲空间(Free Space)、页目录(Page Directory)和文件尾(File Tailer)。

页结构的示意图如下所示:

各部分如下:

这7部分又可以分为3部分。

首先是文件通用部分,也就是文件头和文件尾。它们类似集装箱,将页的内容进行封装,通过文件头和文件尾校验的方式来确保页的传输是完整的。

在文件头中有两个字段,分别是 FIL_PAGE_PREV 和 FIL_PAGE_NEXT,它们的作用相当于指针,分别指向上一个数据页和下一个数据页。连接起来的页相当于一个双向的链表,如下图所示:

需要说明的是采用链表的结构让数据页之间不需要是物理上的连续,而是逻辑上的连续。

这里文件尾的校验方式就是采用 Hash 算法进行校验。举个例子,当进行页传输的时候,如果突然断电了,造成了该页传输的不完整,这时通过文件尾的校验和(checksum 值)与文件头的校验和做比对,如果两个值不相等则证明页的传输有问题,需要重新进行传输,否则认为页的传输已经完成。

第二个部分是记录部分,页的主要作用是存储记录,所以“最小和最大记录”和“用户记录”部分占了页结构的主要空间。另外空闲空间是个灵活的部分,当有新的记录插入时,会从空闲空间中进行分配用于存储新记录,如下图所示:

第三部分是索引部分,这部分重点指的是页目录,它起到了记录的索引作用,因为在页中,记录是以单向链表的形式进行存储的。单向链表的特点就是插入、删除非常方便,但是检索效率不高,最差的情况下需要遍历链表上的所有节点才能完成检索,因此在页目录中提供了二分查找的方式,用来提高记录的检索效率。这个过程就好比是给记录创建了一个目录:

1.将所有的记录分成几个组,这些记录包括最小记录和最大记录,但不包括标记为“已删除”的记录。

2.第 1 组,也就是最小记录所在的分组只有 1 个记录;最后一组,就是最大记录所在的分组,会有 1-8 条记录;其余的组记录数量在 4-8 条之间。这样做的好处是,除了第 1 组(最小记录所在组)以外,其余组的记录数会尽量平分。

3.在每个组中最后一条记录的头信息中会存储该组一共有多少条记录,作为 n_owned 字段。

4.页目录用来存储每组最后一条记录的地址偏移量,这些地址偏移量会按照先后顺序存储起来,每组的地址偏移量也被称之为槽(slot),每个槽相当于指针指向了不同组的最后一个记录。如下图所示:

页目录存储的是槽,槽相当于分组记录的索引。我们通过槽查找记录,实际上就是在做二分查找。这里我以上面的图示进行举例,5 个槽的编号分别为 0,1,2,3,4,我想查找主键为 9 的用户记录,我们初始化查找的槽的下限编号,设置为 low=0,然后设置查找的槽的上限编号 high=4,然后采用二分查找法进行查找。

首先找到槽的中间位置 p=(low+high)/2=(0+4)/2=2,这时我们取编号为 2 的槽对应的分组记录中最大的记录,取出关键字为 8。因为 9 大于 8,所以应该会在槽编号为 (p,high] 的范围进行查找

接着重新计算中间位置 p’=(p+high)/2=(2+4)/2=3,我们查找编号为 3 的槽对应的分组记录中最大的记录,取出关键字为 12。因为 9 小于 12,所以应该在槽 3 中进行查找。

遍历槽 3 中的所有记录,找到关键字为 9 的记录,取出该条记录的信息即为我们想要查找的内容。

从数据页的角度看 B+ 树是如何查询的

MySQL 的 InnoDB 存储引擎采用 B+ 树作为索引,而索引又可以分成聚集索引和非聚集索引(二级索引),这些索引都相当于一棵 B+ 树,如图所示。一棵 B+ 树按照节点类型可以分成两部分:

1.叶子节点,B+ 树最底层的节点,节点的高度为 0,存储行记录。

2.非叶子节点,节点的高度大于 0,存储索引键和页面指针,并不存储行记录本身。

可以用业结构对比,看下B+树的结构:

在一棵 B+ 树中,每个节点都是一个页,每次新建节点的时候,就会申请一个页空间。同一层上的节点之间,通过页的结构构成一个双向的链表(页文件头中的两个指针字段)。非叶子节点,包括了多个索引行,每个索引行里存储索引键和指向下一层页面的页面指针。最后是叶子节点,它存储了关键字和行记录,在节点内部(也就是页结构的内部)记录之间是一个单向的链表,但是对记录进行查找,则可以通过页目录采用二分查找的方式来进行。

当我们从页结构来理解 B+ 树的结构的时候,可以帮我们理解一些通过索引进行检索的原理:

1.B+树如何进行记录检索的?

如果通过B+树的索引查询行数据,首先是从B+树根节点开始,逐层检索,直到找到叶子节点,也就是找到对应的数据页为止,将数据页加载到内存中,页目录中的槽(slot)采用二分查找法的方式先找到一个粗略的记录分组,然后再在分组中通过链表遍历的方式查找记录。

2.普通索引和唯一索引在查询效率上有什么不同?

在查询效率上基本没有差别:唯一索引就是在普通索引上增加了约束性,也就是关键字唯一,找到了关键字就停止检索。而普通索引,可能会存在用户记录中的关键字相同的情况,根据页结构的原理,当我们读取一条记录的时候,不是单独将这条记录从磁盘中读出去,而是将这个记录所在的页加载到内存中进行读取。InnoDB 存储引擎的页大小为 16KB,在一个页中可能存储着上千个记录,因此在普通索引的字段上进行查找也就是在内存中多几次“判断下一条记录”的操作,对于 CPU 来说,这些操作所消耗的时间是可以忽略不计的。所以对一个索引字段进行检索,采用普通索引还是唯一索引在检索效率上基本上没有差别。

总结

数据库中的基本存储单位,也就是页(Page),磁盘 I/O 都是基于页来进行读取的,在页之上还有区、段和表空间,它们都是更大的存储单位。我们在分配空间的时候会按照页为单位来进行分配,同一棵树上同一层的页与页之间采用双向链表,而在页里面,记录之间采用的单向链表的方式。

链表这种数据结构的特点是增加、删除比较方便,所以在对记录进行删除的时候,有时候并不是真的删除了记录,而只是逻辑上的删除,也就是在标记为上标记为“已删除”。但链表还有个问题就是查找效率低,因此在页结构中还专门设计了页目录这个模块,专门给记录做一个目录,通过二分查找法的方式进行检索提升效率。

(本文完)

SQL性能优化系列——索引的使用原则[七]

1.什么情况使用索引?
2.什么情况不需要创建索引?
3.如何避免索引失效?

前面说到了索引的使用和底层原理,那么如何通过索引实现查询效率的最大化?

哪些情况可以创建索引

1.字段的数值有唯一性限制,比如用户名;

2.频繁作为 WHERE 查询条件的字段,尤其在数据表大的情况下;

3.需要经常 ORDER BY 和 GROUP BY 的列;

4.UPDATE、DELETE的 WHERE 条件列,一般也需要创建索引;

5.DISTINCT 字段需要创建索引;

6.做多表 JOIN 连接操作时,创建索引需要注意以下原则:

首先,连接表的数量尽量不要超过3张,因为每增加一张表就相当于增加了一次嵌套的循环,数量级增长非常快,严重影响查询效率。

其次,对 WHERE 条件创建索引,因为 WHERE 才是对数据条件的过滤。

最后,对用于连接的字段创建索引,并且该字段在多张表中的数据类型必须一致。

哪些情况不需要创建索引

1.WHERE 条件(包括 GROUP BY、ORDER BY)里用不到的字段不需要创建索引,索引的价值在于快速定位,如果起不到定位的字段不需要创建索引。

2.如果表记录太少,比如少于1000条,不需要创建索引,原因前面说到过。

3.如果字段中有大量重复数据也不需要创建索引,比如性别字段,原因前面也说到过。

4.频繁更新的字段不一定要创建索引。因为数据更新时,也需要更新索引,如果索引太多,在更新索引时也会造成负担,影响效率。

哪些情况下索引会失效

1.如果索引进行了表达式计算,则会失效

可以使用 EXPLAIN 关键字查看MySQL中一条SQL语句的执行计划,比如:

EXPLAIN SELECT comment_id, user_id, comment_text FROM product_comment WHERE comment_id+1 = 900001

运行结果:

+----+-------------+-----------------+------------+------+---------------+------+---------+------+--------+----------+-------------+
| id | select_type | table           | partitions | type | possible_keys | key  | key_len | ref  | rows   | filtered | Extra       |
+----+-------------+-----------------+------------+------+---------------+------+---------+------+--------+----------+-------------+
|  1 | SIMPLE      | product_comment | NULL       | ALL  | NULL          | NULL | NULL    | NULL | 996663 |   100.00 | Using where |
+----+-------------+-----------------+------------+------+---------------+------+---------+------+--------+----------+-------------+

可以看出如果对索引进行了表达式计算,索引就失效了。这是因为需要把索引字段的值都取出来,然后依次进行表达式计算进行条件判断,因此采用的是全表扫描的方式,运行会慢很多,最终运行时间为2.538秒。

为了避免索引失效,可以改造SQL:

SELECT comment_id, user_id, comment_text FROM product_comment WHERE comment_id = 900000

运行时间为0.039秒。

2.如果对索引使用函数,也会造成失效

比如要对 commont_text 的前三位为 abc 的内容进行条件筛选,执行计划:

EXPLAIN SELECT comment_id, user_id, comment_text FROM product_comment WHERE SUBSTRING(comment_text, 1,3)='abc'

运行结果:

+----+-------------+-----------------+------------+------+---------------+------+---------+------+--------+----------+-------------+
| id | select_type | table           | partitions | type | possible_keys | key  | key_len | ref  | rows   | filtered | Extra       |
+----+-------------+-----------------+------------+------+---------------+------+---------+------+--------+----------+-------------+
|  1 | SIMPLE      | product_comment | NULL       | ALL  | NULL          | NULL | NULL    | NULL | 996663 |   100.00 | Using where |
+----+-------------+-----------------+------------+------+---------------+------+---------+------+--------+----------+-------------+

可以看到索引失效,重写SQL:

SELECT comment_id, user_id, comment_text FROM product_comment WHERE comment_text LIKE 'abc%'

分析结果:

+----+-------------+-----------------+------------+-------+---------------+--------------+---------+------+------+----------+-----------------------+
| id | select_type | table           | partitions | type  | possible_keys | key          | key_len | ref  | rows | filtered | Extra                 |
+----+-------------+-----------------+------------+-------+---------------+--------------+---------+------+------+----------+-----------------------+
|  1 | SIMPLE      | product_comment | NULL       | range | comment_text  | comment_text | 767     | NULL |  213 |   100.00 | Using index condition |
+----+-------------+-----------------+------------+-------+---------------+--------------+---------+------+------+----------+-----------------------+

可以看到,经过查询重写后,可以使用索引进行范围检索,从而提高查询效率。

3.在 WHERE 子句中,如果在 OR 前的条件列进行了索引,而在 OR 后的条件列没有进行索引,那么索引会失效

4.当使用 LIKE 进行模糊查询的时候,后面不能是%

这个很好理解,如果一本字典按照字母顺序进行排序,我们会从首位进行匹配,而不会对中间位置进行匹配,否则索引就失效了。

5.索引列与 NULL 或 NOT NULL 进行判断的时候也会失效

这是因为索引并不存储空值,所以最好是在设计数据表的时候就将字段设置为 NOT NULL 约束,比如可以将 INT 类型的字段默认值设置为0,将字符类型的字段默认值设置为空字符串(”)。

6.在使用联合索引的时候要注意最左原则

最左原则就是需要从左到右的使用索引中的字段,一条SQL语句可以只使用联合索引的一部分,但是要从最左侧开始,否则就会失效。(联合索引有这段)

思考题

针对 product_comment 数据表,其中 comment_time 已经创建了普通索引。假设想查询在某段时间期间的记录:

SELECT comment_id, comment_text, comment_time FROM product_comment WHERE DATE(comment_time) >= '2018-10-01 10:00:00' AND comment_time <= '2018-10-02 10:00:00'

此时索引会失效吗?为什么?如果失效如何重写SQL?

(本文完)