0%

本章是整理知识内容,为强化知识长期更新。

整体架构

基础层

处理层

接口层

Mybatis执行流程

  • 获取sqlSessionFactory对象:解析文件的每一个信息保存在Configuration中,返回包含Configuration的DefaultSqlSession;注意:MappedStatement:代表一个增删改查的详细信息。
  • 获取sqlSession对象,返回一个DefaultSQlSession对象,包含Executor和Configuration;这一步会创建Executor对象;
  • 获取接口的代理对象(MapperProxy),getMapper,使用MapperProxyFactory创建一个MapperProxy的代理对象。

Mybatis拦截器

代理对象里面包含了,DefaultSqlSession(Executor)

  • 执行增删改查方法。
    • 调用DefaultSqlSession的增删改查(Executor)
    • 创建一个StatementHandler对象、且同时创建出ParameterHandler和ResultSetHandler。
    • 调用StatementHandler预编译参数以及设置参数值;使用ParameterHandler来给sql设置参数
    • 调用StatementHandler的增删改查方法;
    • ResultSetHandler封装结果
  • MyBatis 拦截签名 拦截器签名是一个名为 @Intercepts 的注解,该注解中可以通过 @Signature 配置多个签名。@Signature 注解中则包含三个属性
    • type: 拦截器需要拦截的接口,有 4 个可选项,分别是:Executor、ParameterHandler、ResultSetHandler 以及 StatementHandler。
    • method: 拦截器所拦截接口中的方法名,也就是前面四个接口中的方法名,接口和方法要对应上。
    • args: 拦截器所拦截方法的参数类型,通过方法名和参数类型可以锁定唯一一个方法。
  • 被拦截的对象
    • org.apache.ibatis.executor.Executor
    • org.apache.ibatis.executor.statement.StatementHandler
    • org.apache.ibatis.executor.statement.ParameterHandler
    • org.apache.ibatis.executor.resultset.ResultSetHandler
  • Executor
    • update:该方法会在所有的 INSERT、 UPDATE、 DELETE 执行时被调用,如果想要拦截这些操作,可以通过该方法实现。
    • query:该方法会在 SELECT 查询方法执行时被调用,方法参数携带了很多有用的信息,如果需要获取,可以通过该方法实现。
    • queryCursor:当 SELECT 的返回类型是 Cursor 时,该方法会被调用。
    • flushStatements:当 SqlSession 方法调用 flushStatements 方法或执行的接口方法中带有 @Flush 注解时该方法会被触发。
    • commit:当 SqlSession 方法调用 commit 方法时该方法会被触发。
    • rollback:当 SqlSession 方法调用 rollback 方法时该方法会被触发。
    • getTransaction:当 SqlSession 方法获取数据库连接时该方法会被触发。
    • close:该方法在懒加载获取新的 Executor 后会被触发。
    • isClosed:该方法在懒加载执行查询前会被触发。
  • StatementHandler
    • prepare:该方法在数据库执行前被触发。
    • parameterize:该方法在 prepare 方法之后执行,用来处理参数信息。
    • batch:如果 MyBatis 的全剧配置中配置了 defaultExecutorType=”BATCH”,执行数据操作时该方法会被调用。
    • update:更新操作时该方法会被触发。
    • query:该方法在 SELECT 方法执行时会被触发。
    • queryCursor:该方法在 SELECT 方法执行时,并且返回值为 Cursor 时会被触发。
  • ParameterHandler
    • getParameterObject:在执行存储过程处理出参的时候该方法会被触发。
    • setParameters:设置 SQL 参数时该方法会被触发。
  • ResultSetHandler
    • handleResultSets:该方法会在所有的查询方法中被触发(除去返回值类型为 Cursor 的查询方法),一般来说,如果我们想对查询结果进行二次处理,可以通过拦截该方法实现。
    • handleCursorResultSets:当查询方法的返回值类型为 Cursor 时,该方法会被触发。
    • handleOutputParameters:使用存储过程处理出参的时候该方法会被调用。

MyBatis插件

MyBatis 将插件单独分离出一个模块,位于 org.apache.ibatis.plugin 包中,在该模块中主要使用了两种设计模式:代理模式和责任链模式。

  • 插件接口

    • org.apache.ibatis.plugin.Interceptor

    • intercept:它将直接覆盖你所拦截的对象,有个参数Invocation对象,通过该对象,可以反射调度原来对象的方法;

    • plugin:target是被拦截的对象,它的作用是给被拦截对象生成一个代理对象;

    • setProperties:允许在plugin元素中配置所需参数,该方法在插件初始化的时候会被调用一次;

  • MyBatis允许我们自定义 Interceptor 拦截 SQL 语句执行过程中的某些关键逻辑,允许拦截的方法有:Executor 类中的 update()、query()、flushStatements()、commit()、rollback()、getTransaction()、close()、isClosed()方法,ParameterHandler 中的 setParameters()、getParameterObject() 方法,ResultSetHandler中的 handleOutputParameters()、handleResultSets()方法,以及StatementHandler 中的parameterize()、prepare()、batch()、update()、query()方法。

MyBatis中的设计模式

  • 工厂模式
    • 工厂模式在 MyBatis 中的典型代表是 SqlSessionFactory。
    • SqlSession 是 MyBatis 中的重要 Java 接口,可以通过该接口来执行 SQL 命令、获取映射器示例和管理事务,而 SqlSessionFactory 正是用来产生 SqlSession 对象的,所以它在 MyBatis 中是比较核心的接口之一。
    • 工厂模式应用解析:SqlSessionFactory 是一个接口类,它的子类 DefaultSqlSessionFactory 有一个 openSession(ExecutorType execType) 的方法,其中使用了工厂模式。
  • 建造者模式
    • MyBatis 中的典型代表是 SqlSessionFactoryBuilder。
    • 普通的对象都是通过 new 关键字直接创建的,但是如果创建对象需要的构造参数很多,且不能保证每个参数都是正确的或者不能一次性得到构建所需的所有参数,那么就需要将构建逻辑从对象本身抽离出来,让对象只关注功能,把构建交给构建类,这样可以简化对象的构建,也可以达到分步构建对象的目的,而 SqlSessionFactoryBuilder 的构建过程正是如此。
    • 在 SqlSessionFactoryBuilder 中构建 SqlSessionFactory 对象的过程是这样的,首先需要通过 XMLConfigBuilder 对象读取并解析 XML 的配置文件,然后再将读取到的配置信息存入到 Configuration 类中,然后再通过 build 方法生成我们需要的 DefaultSqlSessionFactory 对象。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {

try {
XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
return build(parser.parse());
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error building SqlSession.", e);
} finally {
ErrorContext.instance().reset();
try {
inputStream.close();
} catch (IOException e) {
// Intentionally ignore. Prefer previous error.
}
}
}

public SqlSessionFactory build(Configuration config) {
return new DefaultSqlSessionFactory(config);
}

  • 单例模式
    • 单例模式在 MyBatis 中的典型代表是 ErrorContext。
    • 使用 private 修饰的 ThreadLocal 来保证每个线程拥有一个 ErrorContext 对象,在调用 instance() 方法时再从 ThreadLocal 中获取此单例对象。
1
2
3
4
5
6
7
8
9
10
11
public class ErrorContext {

private static final String LINE_SEPARATOR = System.lineSeparator();
// 每个线程存储的容器
private static final ThreadLocal<ErrorContext> LOCAL = ThreadLocal.withInitial(ErrorContext::new);

public static ErrorContext instance() {
return LOCAL.get();
}
}

  • 适配器模式

    • MyBatis 中的典型代表是 Log。
      • SLF4J
      • Apache Commons Logging
      • Log4j 2
      • Log4j
      • JDK logging
  • 代理模式

    • 代理模式在 MyBatis 中的典型代表是 MapperProxyFactory。
  • 模版方法模式

    • 模板方法在 MyBatis 中的典型代表是 BaseExecutor。
  • 装饰器模式

    • 装饰器模式在 MyBatis 中的典型代表是 Cache。

Mybatis一些疑问

#{} 于 ${}

使用#{}可以有效的防止SQL注入,提高系统安全性。

  • #{}是预编译,${}是字符串替换。
    • Mybatis在处理#{}时,会将sql中的#{}替换为?号,调用PreparedStatement的set方法来赋值;
    • Mybatis在处理${}时,就是把${}替换成变量的值。

MyBatis Dao 接口的工作原理

  • Dao 接口的全限定名,就是映射文件中的 namespace 的值,接口的方法名,就是映射文件中 Mapper 的 Statement 的 id 值,接口方法内的参数,就是传递给 SQL 的参数。Mapper 接口是没有实现类的,当调用接口方法时,接口全限定名 + 方法名拼接字符串作为 key 值,可唯一定位一个 MapperStatement。在 MyBatis 中,每一个 select、insert、update、delete 标签,都会被解析为一个 MapperStatement 对象。

Dao 接口里的方法可以重载吗

  • Mapper 接口的工作原理是 JDK 动态代理,MyBatis 运行时会使用 JDK 动态代理为 Mapper 接口生成代理对象 proxy,代理对象会拦截接口方法,转而执行 MapperStatement 所代表的 SQL,然后将 SQL 执行结果返回。所以是不能重载的

本章是整理知识内容,为强化知识长期更新。

Spring

  • Spring是一个轻量级的IoC和AOP容器框架。是为Java应用程序提供基础性服务的一套框架,目的是用于简化企业应用程序的开发,它使得开发者只需要关心业务需求。

Spring IOC = Spring Bean + Spring Context特性

  • 主要由以下几个模块组成:
    • Spring Core:核心类库,提供IOC服务
    • Spring Context:提供框架式的Bean访问方式,以及企业级功能(JNDI、定时任务等)
    • Spring AOP:AOP服务切面编程
    • Spring Type Conversion:类型转换
    • Spring Data Binding :数据绑定
    • Spring Express Language:Spring 表达式
    • Spring Resources:资源管理
    • Spring Events:事件
    • Spring i18n:国际化
    • Spring Validation:校验
    • Spring Data Access:数据存储
      • JDBC Java API:对JDBC的抽象,简化了数据访问异常的处理
      • Transactions:事务抽象 EJB简化版本
      • O/R Mapping:映射
      • DAO Support:对JDBC的抽象,简化了数据访问异常的处理
      • XML Marshalling 编列
    • Spring Integration
      • Remoting:远程调用
      • JMS:Java消息服务
      • JCA:Java连接架构
      • JMS:Java管理扩展
      • Email:Java 邮件客户端
      • Tasks:本地任务
      • Scheduling:本地调度
      • Caching:缓存抽象
      • Test : Spring 测试
        • Mock Objects:模拟对象框架
        • Test Context Framework:TestContext框架
        • Spring Mvc Test:Spring MVC 测试
        • WebTestClient :Web 测试客户端
    • Spring Web:提供了基本的面向Web的综合特性,例如多方文件上传
      • Spring Servlet Support
        • Spring MVC:提供面向Web应用的Model-View-Controller实现
        • WebSocket
        • SockJS
      • Spring Reactive
        • Spring WebFlux
        • WebClient
        • WebSocket
阅读全文 »

本章是整理知识内容,为强化知识长期更新。

Spring Web MVC

Spring Web 模型-视图-控制器 (MVC) 框架围绕DispatcherServlet将请求分派给处理程序而设计。

常用注解

  • @Controller

    • 声明在类上,该注解表明该类扮演控制器的角色,类似Action。
  • @RestController

    • RestController是Controller超子集,相当于@RequestMapping方法默认采用@ResponseBody注解。
  • @RequestMapping

    • 该注解是用来映射一个URL到一个类或一个特定的方处理法上。
    • RequestMapping属性
      • path / method 指定方法的映射路径
      • params / headers 请求映射范围
      • consumes / produces 请求与响应格式的限制范围
    • Restfull风格的使用。
      • restfull 支持的请求头GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS, TRACE 。
      • 通常情况下只使用 GET,PUT,POST,DELETE。
        • GET 通常用来获取数据。
        • PUT 通常用来新增数据。
        • POST 通常用来更新数据。
        • DELETE 通常用来删除数据。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    @RequestMapping(value = "/get", method = RequestMethod.GET)
    public Object get(){
    return "200";
    }

    @RequestMapping(value = "/post", method = RequestMethod.POST)
    public Object post(){
    return "200";
    }

    @RequestMapping(value = "/delete", method = RequestMethod.DELETE)
    public Object delete(){
    return "200";
    }

    @RequestMapping(value = "/put", method = RequestMethod.PUT)
    public Object put(){
    return "200";
    }

DispatcherServlet

  • Root WebApplicationcontext 是可以在不同的 Servlet WebApplicationcontext共享,但是反来不醒,因此通常将web相关的代码放到Servlet WebApplicationcontext一些基础代码放到Root WebApplicationcontext 。也可以让Root WebApplicationcontext 托管所有的bean,这样可以避免Servlet WebApplicationcontext未初始化全部bean的问题。

源码分析

  • DispatcherServlet是继承了FrameworkServlet,而FrameworkServlet又继承了HttpServletBean。追溯到最上层其实就是Servlet。通过源码可以发现最核心的一个方法doService()

  • 处理流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
/**
* Exposes the DispatcherServlet-specific request attributes and delegates to {@link #doDispatch}
* for the actual dispatching.
*/
@Override
protected void doService(HttpServletRequest request, HttpServletResponse response) throws Exception {
logRequest(request);

// Keep a snapshot of the request attributes in case of an include,
// to be able to restore the original attributes after the include.
Map<String, Object> attributesSnapshot = null;
if (WebUtils.isIncludeRequest(request)) {
attributesSnapshot = new HashMap<>();
Enumeration<?> attrNames = request.getAttributeNames();
while (attrNames.hasMoreElements()) {
String attrName = (String) attrNames.nextElement();
if (this.cleanupAfterInclude || attrName.startsWith(DEFAULT_STRATEGIES_PREFIX)) {
attributesSnapshot.put(attrName, request.getAttribute(attrName));
}
}
}

// Make framework objects available to handlers and view objects.
request.setAttribute(WEB_APPLICATION_CONTEXT_ATTRIBUTE, getWebApplicationContext());
request.setAttribute(LOCALE_RESOLVER_ATTRIBUTE, this.localeResolver);
request.setAttribute(THEME_RESOLVER_ATTRIBUTE, this.themeResolver);
request.setAttribute(THEME_SOURCE_ATTRIBUTE, getThemeSource());

if (this.flashMapManager != null) {
FlashMap inputFlashMap = this.flashMapManager.retrieveAndUpdate(request, response);
if (inputFlashMap != null) {
request.setAttribute(INPUT_FLASH_MAP_ATTRIBUTE, Collections.unmodifiableMap(inputFlashMap));
}
request.setAttribute(OUTPUT_FLASH_MAP_ATTRIBUTE, new FlashMap());
request.setAttribute(FLASH_MAP_MANAGER_ATTRIBUTE, this.flashMapManager);
}

try {
//进入doDispatch
doDispatch(request, response);
}
finally {
if (!WebAsyncUtils.getAsyncManager(request).isConcurrentHandlingStarted()) {
// Restore the original attribute snapshot, in case of an include.
if (attributesSnapshot != null) {
restoreAttributesAfterInclude(request, attributesSnapshot);
}
}
}
}



/**
* Process the actual dispatching to the handler.
* <p>The handler will be obtained by applying the servlet's HandlerMappings in order.
* The HandlerAdapter will be obtained by querying the servlet's installed HandlerAdapters
* to find the first that supports the handler class.
* <p>All HTTP methods are handled by this method. It's up to HandlerAdapters or handlers
* themselves to decide which methods are acceptable.
* @param request current HTTP request
* @param response current HTTP response
* @throws Exception in case of any kind of processing failure
*/
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
HttpServletRequest processedRequest = request;
HandlerExecutionChain mappedHandler = null;
boolean multipartRequestParsed = false;

WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);

try {
ModelAndView mv = null;
Exception dispatchException = null;

try {
// 先处理是否Multipart的请求,如果是则会解析,并且返回一个解析后的请求。
processedRequest = checkMultipart(request);
multipartRequestParsed = (processedRequest != request);
// 寻找对应的handler
// Determine handler for the current request.
mappedHandler = getHandler(processedRequest);
if (mappedHandler == null) {
// 没有找到则直接返回默认404视图。
noHandlerFound(processedRequest, response);
return;
}

// Determine handler adapter for the current request.
HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());

// Process last-modified header, if supported by the handler.
String method = request.getMethod();
boolean isGet = "GET".equals(method);
if (isGet || "HEAD".equals(method)) {
long lastModified = ha.getLastModified(request, mappedHandler.getHandler());
if (new ServletWebRequest(request, response).checkNotModified(lastModified) && isGet) {
return;
}
}
// 前置拦截器
if (!mappedHandler.applyPreHandle(processedRequest, response)) {
return;
}

// Actually invoke the handler.
// 如果上面的流程都执行完成,在执行真实的handle
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());

// 如果是异步的就直接结束。
if (asyncManager.isConcurrentHandlingStarted()) {
return;
}
// 视图解析
applyDefaultViewName(processedRequest, mv);
//执行后置拦截器
mappedHandler.applyPostHandle(processedRequest, response, mv);
}
catch (Exception ex) {
dispatchException = ex;
}
catch (Throwable err) {
// As of 4.3, we're processing Errors thrown from handler methods as well,
// making them available for @ExceptionHandler methods and other scenarios.
dispatchException = new NestedServletException("Handler dispatch failed", err);
}

// 响应视图
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
}
catch (Exception ex) {
triggerAfterCompletion(processedRequest, response, mappedHandler, ex);
}
catch (Throwable err) {
triggerAfterCompletion(processedRequest, response, mappedHandler,
new NestedServletException("Handler processing failed", err));
}
finally {
// 没有抛异常也会判断是否异步,如果是异步也会执行拦截器。
if (asyncManager.isConcurrentHandlingStarted()) {
// Instead of postHandle and afterCompletion
if (mappedHandler != null) {
mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);
}
}
else {
// Clean up any resources used by a multipart request.
if (multipartRequestParsed) {
cleanupMultipart(processedRequest);
}
}
}
}


/**
* Return the HandlerExecutionChain for this request.
* <p>Tries all handler mappings in order.
* @param request current HTTP request
* @return the HandlerExecutionChain, or {@code null} if no handler could be found
*/
@Nullable
protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
if (this.handlerMappings != null) {
// 遍历所有的HandlerMapping找出对应的HandlerExecutionChain
for (HandlerMapping mapping : this.handlerMappings) {
HandlerExecutionChain handler = mapping.getHandler(request);
if (handler != null) {
return handler;
}
}
}
return null;
}




/**
* Apply postHandle methods of registered interceptors.
*/
void applyPostHandle(HttpServletRequest request, HttpServletResponse response, @Nullable ModelAndView mv)
throws Exception {
// 取出所有拦截器,遍历拦截器
HandlerInterceptor[] interceptors = getInterceptors();
if (!ObjectUtils.isEmpty(interceptors)) {
for (int i = interceptors.length - 1; i >= 0; i--) {
HandlerInterceptor interceptor = interceptors[i];
interceptor.postHandle(request, response, this.handler, mv);
}
}
}

Spring作用域。

  • 当定义一个Bean时,可以给这个Bean声明一个作用域。通过@Scope注解声明在类上。

    • Bean默认作用域是singleton 单例模式,在每个Spring Ioc容器中的一个Bean定义一个实例,也就是无状态Bean,也就是线程不安全的。这个单一实例会被存储到单例缓存(singleton cache)中,并且所有针对该bean的后续请求、访问和引用都将返回被缓存的对象实例。相当于一次创建多次使用。
    • prototype 原型模式,在每个Spring Ioc容器中的一个Bean定义多实例,就相当与Java 中new 操作。通常作为有状态的Bean。Prototype作用域的bean会导致在每次对该bean请求(将其注入到另一个bean中,或者以程序的方式调用容器的getBean()方法)时都会创建一个新的bean实例。
  • spring mvc增加的作用域。

    • request、session、global session 仅在基于web的应用中使用(不必关心你所采用的是什么web应用框架)。
      • 如果不是在web下实现而是通过ApplicationContext这种实现,尝试使用这些作用域将抛出异常IllegalStateException未知作用域。
      • request 对每一次HTTP请求都会产生一个新的bean。Spring容器会根据loginAction bean定义创建一个全新的LoginAction bean实例,且该loginAction bean实例仅在当前HTTP request内有效,因此可以根据需要放心的更改所建实例的内部状态,而其他请求中根据loginAction bean定义创建的实例,将不会看到这些特定于某个请求的状态变化。当处理请求结束,request作用域的bean实例将被销毁。
      • session 对每一次HTTP请求都会产生一个新的bean,Spring容器会根据userPreferences bean定义创建一个全新的userPreferences bean实例,且该userPreferences bean仅在当前HTTP Session内有效。与request作用域一样,你可以根据需要放心的更改所创建实例的内部状态,而别的HTTP Session中根据userPreferences创建的实例,将不会看到这些特定于某个HTTP Session的状态变化。当HTTP Session最终被废弃的时候,在该HTTP Session作用域内的bean也会被废弃掉。
      • ‍global session 作用域类似于标准的HTTP Session作用域,不过它仅仅在基于portlet的web应用中才有意义。Portlet规范定义了全局Session的概念,它被所有构成某个portlet web应用的各种不同的portlet所共享。在global session作用域中定义的bean被限定于全局portlet Session的生命周期范围内。在一个标准的基于Servlet的web应用,并且定义了一个或多个具有global session作用域的bean,系统会使用标准的HTTP Session作用域,并且不会引起任何错误。
    • 作用域依赖
      • Spring IoC容器除了管理对象(bean)的实例化,同时还负责协作者(或者叫依赖)的实例化。如果将一个Http request范围的bean注入到另一个bean中,那么需要注入一个AOP代理来替代被注入的作用域bean。也就是说需要注入一个代理对象,该对象具有与被代理对象一样的公共接口,而容器则可以足够智能的从相关作用域中(比如一个HTTP request)获取到真实的目标对象,并把方法调用委派给实际的对象。

阅读全文 »

本章是整理知识内容,为强化知识长期更新。

Mysql基础架构图

mysql基础架构图

简单介绍上图相关模块功能

  • 连接器:管理连接,权限验证

  • 查询缓存:命中缓存则直接返回结果

  • 语法解析:词法分析,语法分析

  • 查询优化:执行计划生成,索引选择

  • 执行器:操作引擎,返回结果

  • 存储引擎:存储数据,提供读写结构。

连接器

我们需要连接到mysql服务端才能进行各种操作。

1
2
# 使用密码登录到时候我们会使用这种格式。
mysql -h$ip -P$port -u$user -p

执行上述命令会提示输入密码。输入完密码后就进入了mysql。

  • 如果用户名或者密码输入错误,会提示一个Access denied for user到错误,然后客户端程序结束执行。

  • 如果用户名和密码认证通过,连接器会去权限表中查询该用户拥有的所有权限。此后该连接里面的权限逻辑判断都会依赖认证成功时候读取到的权限。

因此当修改用户的权限之后,如果被修改用户的在登录状态是不会修改已经连接的权限。需要让被修改权限用户从新登录。

  • 实际上连接的方式有多种,上面通过密码的方式只是其中的一种方式。当客户端与服务端mysql进程进程建立连接,服务器的进程就会创建一个单独的线程来专门处理与这个客户端的交互,当该客户断开与服务端连接时,服务端并不会马上销毁对应的交互线程,而是缓存起来。当另一个新的客户端连接进来的时候,在把这个缓存的线程分配给新的客户端。这样就避免的频繁创建和修改线程,节省系统的开销。但是连接线程多了也会影响服务端,所以也有默认的参数限制客户端的连接数量。
1
2
# 这里是查询服务端存在的连接。
SHOW PROCESSLIST;

我使用的阿里云的数据库,这里展示下结果集。这里需要注意,登录的账号需要有PROCESS的权限否则只能看到自己的连接信息(线程),我这里采用的root所以看到全部到。

连接完成后如果没有执行后续操作则 Command会显示Sleep的状态,长时间Sleep会导致连接自动断开。默认的时间是8小时,也就是参数 wait_timeout。当客户端连接断开后,若客户端再次发出请求就会提示一个

Lost connection to MySQL server during query,此时就只能重新建立连接了。

半双工
  • MySQL客户端/服务端通信协议是“半双工”的:在任一时刻,要么是服务器向客户端发送数据,要么是客户端向服务器发送数据,这两个动作不能同时发生。一旦一端开始发送消息,另一端要接收完整个消息才能响应它,所以我们无法也无须将一个消息切成小块独立发送,也没有办法进行流量控制。
  • 服务器响应给用户的数据通常会很多,由多个数据包组成。但是当服务器响应客户端请求时,客户端必须完整的接收整个返回结果,而不能简单的只取前面几条结果,然后让服务器停止发送。因而在实际开发中,尽量保持查询简单且只返回必需的数据,减小通信间数据包的大小和数量是一个非常好的习惯,这也是查询中尽量避免使用SELECT *以及加上LIMIT限制的原因之一。
长连接与短连接
  • 在数据库里面,长连接是指数据库建立连接,如果客户端有持续的请求则使用同一个连接,短连接是指每次执行几次短的查询就断开连接,下次查询再次创建连接。由于创建的过程复杂,一般来说尽量减少创建连接的动作,尽可能使用长连接。特别是在开发的时候出现大伙突然连接不上测试库,此时就检查下连接是不是太多了。因为连接多了会导致内存消耗特别快。

    • mysql的执行过程中临时使用的内存管理是在连接线程对象里面,服务端断开的时候才会释放,为了避免内存消耗过大,每次执行较大的操作之后,可以通过mysql_reset_connection来重新初始化连接资源。这个操作是不需要重新做权限,只是恢复到创建的初始状态。
  • MySQL mysql_reset_connection 官方文档

查询缓存

一个不被建议使用的功能,在新版的8.0中已经被删除了。总结就是弊大于利。

  • 建立完成后,假设执行一次SELECT语句,执行逻辑就会到第二部查询缓存。mysql拿到一个查询之后,会先到缓存中寻找释放有完全对应的,因为之前执行的查询语句和结果集会直接被缓存起来,以Keys-value的形式。keys是查询语句-value是查询结果集。如果能命中这个key则直接返回value。咋一看挺有用的,但实际上查询缓存的实效的太频繁。并且keys的命中条件太不聪明了,如果两次查询语句在任何的字符上存在不通(空格、大小写)都不会缓存命中。为什么会频繁失效,因为如果对查询语句中的表进行更新,就会导致缓存失效。比如INSERT、UPDATE 、DELETE 、TRUNCATE TABLE 、ALTER TABLE 等等就会导致缓存失效。

语法解析

如果没有命中查询缓存,就要开始执行sql语句了。mysql需要做什么,因此需要对sql进行解析。MySQL通过关键字将SQL语句进行解析,并生成一颗对应的解析树。这个过程解析器主要通过语法规则来验证和解析。比如SQL中是否使用了错误的关键字或者关键字的顺序是否正确等等。预处理则会根据MySQL规则进一步检查解析树是否合法。比如检查要查询的数据表和数据列是否存在等等。

  • 本质上发送过来的是一个文本信息,这里就需要对该文本信息进行编译,涉及到词法解析语法解析语义解析等阶段。需要注意的是,这里会对sql语句进行一些检查,比如设计的相关表、表字段。这里检查通过的才会进入查询优化模块。
  • 根据SQL语言的功能可以划分成4个部分
    • DDL,英文叫做 Data Definition Language,也就是数据定义语言,它用来定义我们的数据库对象,包括数据库、数据表和列。通过使用 DDL,我们可以创建,删除和修改数据库和表结构。
    • DML,英文叫做 Data Manipulation Language,数据操作语言,我们用它操作和数据库相关的记录,比如增加、删除、修改数据表中的记录。
    • DCL,英文叫做 Data Control Language,数据控制语言,我们用它来定义访问权限和安全级别。
    • DQL,英文叫做 Data Query Language,数据查询语言,我们用它查询想要的记录,它是 SQL 语言的重中之重。在实际的业务中,我们绝大多数情况下都是在和查询打交道,因此学会编写正确且高效的查询语句,是学习的重点。

查询优化

通过之前的语法解析,基本可以判定语法树是合法的,此时mysql就知道文本内容要做什么。

  • mysql会对查询语句进行优化,优化的结果就是生成一个执行计划,这个计划会表明使用了那些查询索引,表之间的连接是什么样子的。有时候出现多种执行方式,只是效率不通,优化器会决定使用哪一个执行方案,这个时候就涉及到EXPLAIN语句,该语句可以查看sql到执行计划,这将涉及到查询优化。
  • MySQL的查询优化器是一个非常复杂的部件,它使用了非常多的优化策略来生成一个最优的执行计划。
    • 重新定义表的关联顺序(多张表关联查询时,并不一定按照SQL中指定的顺序进行,但有一些技巧可以指定关联顺序)
    • 优化函数
    • 提前终止查询(比如:使用Limit时,查找到满足数量的结果集后会立即终止查询)
    • 优化排序(在老版本MySQL会使用两次传输排序,即先读取行指针和需要排序的字段在内存中对其排序,然后再根据排序结果去读取数据行,而新版本采用的是单次传输排序,也就是一次读取所有的数据行,然后根据给定的列排序。对于I/O密集型应用,效率会高很多。

执行器

进入到执行sql到环节。

  • 通过之前到查询优化,进入执行阶段,此时会先判断该连接用户是否有相关表的权限,如果没有会返回权限错误。如果有权限就会打开表,执行器会根据表定义的引擎去使用这个引擎提供的接口完成流程。

存储引擎

关于存储引擎,这里明确提醒。不是三种也不是四种。而是多种因为随时可能出现新的引擎。

  • 我们常用的就是InnoDBMyISAM,其它的不常见就不做过多介绍。mysql现在默认的引擎是InnoDB所以主要也是了解InnoDB
1
2
# 查看当前服务支持的存储引擎
SHOW ENGINES;

阅读全文 »

本章是整理知识内容,为强化知识长期更新。

Redis 简介

Redis 是完全开源免费的,遵守 BSD 协议,英文全称是Remote Dictionary Server(远程字典服务),是一个开源的使用ANSI C语言编写、支持网络、可基于内存亦可持久化的日志型、Key-Value数据库,并提供多种语言的API。

  • Redis 与 其他 key - value 缓存产品有以下三个特点:
    • Redis 支持数据持久化,可以将内存中的数据保存在磁盘中,重启的时候可以再次加载进行使用。
    • Redis 不仅仅支持简单的 key - value 类型的数据,同时还提供 list,set,zset,hash 等数据结构的存储
    • Redis 支持数据的备份,即 master - slave 模式的数据备份。
  • Redis是内存数据库,所有操作都在内存上完成,内存的访问速度本身就很快,读的速度是 110000 次 /s, 写的速度是 81000 次 /s。另一方面是因为它的数据结构。键值对是按一定的数据结构来组织的,操作键值对最终就是对数据结构进行增删改查操作,所以高效的数据结构是 Redis 快速处理数据的基础。
  • Redis使用基于哈西槽(slot)的数据划分方式。

Redis数据类型与数据结构

数据结构时间复杂度

名称 时间复杂度
哈希表 O(1)
跳表 O(logN)
双向链表 O(N)
压缩列表 O(N)
整数数组 O(N)

Redis事务

严格上来说redis是伪事物,

  • Redis事务功能是通过MULTI、EXEC、DISCARD和WATCH 四个原语实现的。Redis会将一个事务中的所有命令序列化,然后按顺序执行。
    1. redis 不支持回滚“Redis 在事务失败时不进行回滚,而是继续执行余下的命令”, 所以 Redis 的内部可以保持简单且快速。
    2. 如果在一个事务中的命令出现错误,那么所有的命令都不会执行;
    3. 如果在一个事务中出现运行错误,那么正确的命令会被执行。
  • MULTI命令用于开启一个事务,它总是返回OK。 MULTI执行之后,客户端可以继续向服务器发送任意多条命令,这些命令不会立即被执行,而是被放到一个队列中,当EXEC命令被调用时,所有队列中的命令才会被执行。
  • EXEC:执行所有事务块内的命令。返回事务块内所有命令的返回值,按命令执行的先后顺序排列。当操作被打断时,返回空值 nil 。
  • 通过调用DISCARD,客户端可以清空事务队列,并放弃执行事务, 并且客户端会从事务状态中退出。
  • WATCH 命令可以为 Redis 事务提供 check-and-set (CAS)行为。 可以监控一个或多个键,一旦其中有一个键被修改(或删除),之后的事务就不会执行,监控一直持续到EXEC命令。

Redis单线程

  • 我们通常说,Redis 是单线程,主要是指 Redis 的网络 IO 和键值对读写是由一个线程来完成的,这也是 Redis 对外提供键值存储服务的主要流程。但 Redis 的其他功能,比如持久化、异步删除、集群数据同步等,其实是由额外的线程执行的。Redis 的单线程设计机制以及多路复用机制
  • Linux 中的 IO 多路复用机制
    • Linux 中的 IO 多路复用机制是指一个线程处理多个 IO 流,就是我们经常听到的 select/epoll 机制。简单来说,在 Redis 只运行单线程的情况下,该机制允许内核中,同时存在多个监听套接字和已连接套接字。内核会一直监听这些套接字上的连接请求或数据请求。一旦有请求到达,就会交给 Redis 线程处理,这就实现了一个 Redis 线程处理多个 IO 流的效果。
  • Redis 的网络 IO 和键值对读写是由一个线程来完成的,这也是 Redis 对外提供键值存储服务的主要流程

Redis单线程处理IO请求性能瓶颈

  • 任意一个请求在server中一旦发生耗时,都会影响整个server的性能,也就是说后面的请求都要等前面这个耗时请求处理完成,自己才能被处理到。耗时的操作包括以下几种:
    • 操作bigkey:写入一个bigkey在分配内存时需要消耗更多的时间,同样,删除bigkey释放内存同样会产生耗时;
    • 使用复杂度过高的命令:例如SORT/SUNION/ZUNIONSTORE,或者O(N)命令,但是N很大,例如lrange key 0 -1一次查询全量数据;
    • 大量key集中过期:Redis的过期机制也是在主线程中执行的,大量key集中过期会导致处理一个请求时,耗时都在删除过期key,耗时变长;
    • 淘汰策略:淘汰策略也是在主线程执行的,当内存超过Redis内存上限后,每次写入都需要淘汰一些key,也会造成耗时变长;
    • AOF刷盘开启always机制:每次写入都需要把这个操作刷到磁盘,写磁盘的速度远比写内存慢,会拖慢Redis的性能;
    • 主从全量同步生成RDB:虽然采用fork子进程生成数据快照,但fork这一瞬间也是会阻塞整个线程的,实例越大,阻塞时间越久;
  • 并发量非常大时,单线程读写客户端IO数据存在性能瓶颈,虽然采用IO多路复用机制,但是读写客户端数据依旧是同步IO,只能单线程依次读取客户端的数据,无法利用到CPU多核。
  • Redis在4.0推出了lazy-free机制,把bigkey释放内存的耗时操作放在了异步线程中执行,降低对主线程的影响。
  • Redis在6.0推出了多线程,可以在高并发场景下利用CPU多核多线程读写客户端数据,进一步提升server性能,当然,只是针对客户端的读写是并行的,每个命令的真正操作依旧是单线程的。
阅读全文 »

本章属于持续学习、长期更修。

HashMap

特征

  • HashMap 允许null键和null值,null键哈西值为0.
  • HashMap 并不是有序的存放。在使用iterate迭代的时候,并不能得到存放顺序。
  • HashMap使用它的内部类Node <K,V>来存储映射。
  • HashMap将entries存储到多个单链表中,称为存储桶或存储桶。默认的箱数为16,默认负载因子0.75,它的扩展系数为2,当键值对数量大于阈值,则容量扩容到原来的2倍。
  • HashMap不是线程安全的,对于多线程环境,您应该使用ConcurrentHashMap类或使用Collections.synchronizedMap()方法获取同步映射。
  • 底层实现是链表,但是jdk1.8后添加了红黑谁的转换。
  • HashMap的Key用存放,所以key默认不允许重复的,如果想重复就重写key的hashcode和equals方法。
  • 查找方法,根据 hash 值我们能够快速定位到数组的具体下标,但是之后的话,需要顺着链表一个个比较下去才能找到我们需要的,时间复杂度取决于链表的长度,为 O(n)。为了降低这部分的开销,在 Java8 中,当链表中的元素超过了 8 个以后,会将链表转换为红黑树,在这些位置进行查找的时候可以降低时间复杂度为 O(logN)。

源码分析

HashMap中的常量以及变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// 未指定容量大小的情况下,默认初始化16。容量都是2的幂。第一次扩容大概率情况下是64。
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
// 容量最大长个数。
static final int MAXIMUM_CAPACITY = 1 << 30;
// 默认负载因子,
static final float DEFAULT_LOAD_FACTOR = 0.75f;
// HashMap使用单链表来存储元素,这些元素称为bin或buckets。当我们调用put方法时,key的hashCode用于确定将用于存储映射的存储区。
// 链表转换红黑树的阀值。当某个bin\buckets的长度大于8的时候进行转换。
static final int TREEIFY_THRESHOLD = 8;
// 红黑树转换链表的阀值。当某个bin\buckets的长度小于8的时候进行转换。
static final int UNTREEIFY_THRESHOLD = 6;
// 桶中bin最小hash容量,如果大于这个值会进行resize扩容操作,此值至少是TREEIFY_THRESHOLD的4倍
static final int MIN_TREEIFY_CAPACITY = 64;

// 被transient修饰的变量不回被序列化。
// HashMap内部类实现了Map的内部类Entry,用于存储K,V,第一次使用的时候被创建,根据需要可以进行resize。分配长度为2的冥次方
transient Node<K,V>[] table;
// 当被调用entrySet时被赋值。通过keySet()方法可以得到map key的集合,通过values方法可以得到map value的集合
transient Set<Map.Entry<K,V>> entrySet;

// size表示HashMap中存放KV的数量(为链表和树中的KV的总和)。
transient int size;

// 操作次数,通常用过fail-fast。每次扩容和更改map结构的计数器
transient int modCount;

// threshold=capacity*loadFactor threshold表示当HashMap的size大于threshold时会执行resize操作。
int threshold;

// Load Factor用于确定何时重新散列HashMap并增加存储桶大小。存储桶或容量的默认值为16,负载系数为0.75。通过乘以容量和负载因子来计算重新散列的阈值
final float loadFactor;

Node

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
static class Node<K,V> implements Map.Entry<K,V> {
final int hash; // node哈希值
final K key;
V value;
Node<K,V> next; // 下一个node的地址

Node(int hash, K key, V value, Node<K,V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}

public final K getKey() { return key; }
public final V getValue() { return value; }
// 这里重写方法,所以map.toString()不是内存地址。
public final String toString() { return key + "=" + value; }

// 这里重写了hashCode()方法
public final int hashCode() {
return Objects.hashCode(key) ^ Objects.hashCode(value);
}

public final V setValue(V newValue) {
V oldValue = value;
value = newValue;
return oldValue;
}

// 这里重写了equals()方法
public final boolean equals(Object o) {
if (o == this)
return true;
//Map.Entry 相等的条件:键相等、值相等、个数相等、顺序相等
if (o instanceof Map.Entry) {
Map.Entry<?,?> e = (Map.Entry<?,?>)o;
if (Objects.equals(key, e.getKey()) &&
Objects.equals(value, e.getValue()))
return true;
}
return false;
}
}
阅读全文 »