开发者博客 – IT技术 尽在开发者博客

开发者博客 – 科技是第一生产力


  • 首页

  • 归档

  • 搜索

Node中processnextTick(),定时器和se

发表于 2019-07-30

最近在学习nodejs,看到了异步这块,就整理了下Node中几个异步API

定时器:

setTimeout和setInterval与浏览器中的API是一致的,分别用于单次和多次定时执行任务。他们的实现原理和异步I/O比较类似,只是不需要I/O线程池的参与。调用他们创建的定时器会被插入到定时器观察者内部的一个红黑树中。每次Tick执行的时候,会从该红黑树中迭代取出定时器对象,检查是否超过定时时间,如果超过,就形成一个事件,它的回调函数将立即执行。

问题:并非精确,尽管事件循环非常快,但是如果某一次循环占用的时间比较多,那么下次循环时,它也许已经超时很久了。

process.nextTick()

很多人会为了立即异步执行一个任务,会这样调用setTimeout()来达到所需的效果。

1
2
3
复制代码setTimeout(function () {
// TODO
}, 0)

由于事件循环的特点,定时器的精确度不够。而事实上,采用定时器需要动用红黑树,创建定时器对象和迭代等操作,而setTimeout(fn, 0)较为浪费性能。而process.nextTick()方法的操作相对较为轻量,具体代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
复制代码process.nextTick = function (callback) {
if( process._exiting ) return;
if( tickDepth >= process.maxTickDepth ){
maxTickWarn();
}
var tock = { callback: callback};
if( process.domain ) tock.domain = process.domain;
nextTickQueue.push( tock );
if( nextTickQueue.length ) {
process._needTickCallback();
}
}

每次调用process.nextTick()方法,只会将回调函数放入队列中,在下一轮Tick时取出执行。定时器中采用红黑树的操作时间复杂度为O(lg(n)), nextTick()的时间复杂度为O(1)。相对之下,process.nextTick()更加有效。

setImmediate()

setImmdiate()方法与process.nextTick()方法类似,都是将回调函数延迟执行。

1
2
3
4
5
6
7
8
复制代码process.nextTick( function () {
console.log("延迟执行!");
});
console.log("正常执行!");

// 结果:
// 正常执行
// 延迟执行

用setImmediate()实现:

1
2
3
4
5
6
7
8
复制代码setImmediate( function () {
console.log("延迟执行")
});
console.log("正常执行");

// 结果:
// 正常执行
// 延迟执行

放在一起执行:

1
2
3
4
5
6
7
8
9
10
11
12
13
复制代码proccess.nextTick( function () {
console.log("nextTick");
});
setImmediate( function () {
console.log("immediate");
});
console.log("正常执行");


// 结果:
// 正常执行
// nextTick
// immediate

process.nextTick()中的回调函数优先级高于setImmediate().
process.nextTick()属于idle观察者,setImmediate()属于check观察者。在每一个轮循环检查中,idle观察者先于I/O观察者,I/O观察者先于check观察者。

三者的执行顺序为process.next() 先于 定时器 先于 setImmediate()

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

深入理解SpringBoot核心原理(三)--------

发表于 2019-07-29

一、前言

  在上一篇我们了解到 new SpringApplication(primarySources)实例初始化源码的加载过程,通过走跟源码分析了基本初始化过程如下:

1.资源初始化资源加载器为 null

2.断言主要加载资源类不能为 null,否则报错

3.初始化主要加载资源类集合并去重

4.推断当前 WEB 应用类型

5.设置应用上下文初始化器

6.设置监听器

7.推断主入口应用类

如果,各位同学有遗忘的,可以去复习一下上篇文章深入理解SpringBoot核心原理(二)——–初始化流程(run方法)。
那么,这篇我们继续往下面分析其核心 run 方法。

二、SpringApplication 实例 run 方法运行过程

下面继续来分析SpringApplication对象的run方法实现过程以及运行原理。
还是跟之前的分析流程一样,先来看一下run方法里面总体的流程实现:

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
复制代码public ConfigurableApplicationContext run(String... args) {
// 1、创建并启动计时监控类
StopWatch stopWatch = new StopWatch();
stopWatch.start();

// 2、初始化应用上下文和异常报告集合
ConfigurableApplicationContext context = null;
Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>();

// 3、设置系统属性 `java.awt.headless` 的值,默认值为:true
configureHeadlessProperty();

// 4、创建所有 Spring 运行监听器并发布应用启动事件
SpringApplicationRunListeners listeners = getRunListeners(args);
listeners.starting();
try {

// 5、初始化默认应用参数类
ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);

// 6、根据运行监听器和应用参数来准备 Spring 环境
ConfigurableEnvironment environment = prepareEnvironment(listeners, applicationArguments);
configureIgnoreBeanInfo(environment);

// 7、创建 Banner 打印类
Banner printedBanner = printBanner(environment);

// 8、创建应用上下文
context = createApplicationContext();

// 9、准备异常报告器
exceptionReporters = getSpringFactoriesInstances(SpringBootExceptionReporter.class,
new Class[] { ConfigurableApplicationContext.class }, context);

// 10、准备应用上下文
prepareContext(context, environment, listeners, applicationArguments, printedBanner);

// 11、刷新应用上下文
refreshContext(context);

// 12、应用上下文刷新后置处理
afterRefresh(context, applicationArguments);

// 13、停止计时监控类
stopWatch.stop();

// 14、输出日志记录执行主类名、时间信息
if (this.logStartupInfo) {
new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), stopWatch);
}

// 15、发布应用上下文启动完成事件
listeners.started(context);

// 16、执行所有 Runner 运行器
callRunners(context, applicationArguments);
}
catch (Throwable ex) {
handleRunFailure(context, ex, exceptionReporters, listeners);
throw new IllegalStateException(ex);
}

try {

// 17、发布应用上下文就绪事件
listeners.running(context);
}
catch (Throwable ex) {
handleRunFailure(context, ex, exceptionReporters, null);
throw new IllegalStateException(ex);
}
// 18、返回应用上下文
return context;
}

三、run 方法运行过程分解

3.1 创建并启动计时监控类

1
2
复制代码		StopWatch stopWatch = new StopWatch();
stopWatch.start();

进入start()方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
复制代码	/**
* Start an unnamed task. The results are undefined if {@link #stop()}
* or timing methods are called without invoking this method.
* @see #stop()
*/
public void start() throws IllegalStateException {
start("");
}

/**
* Start a named task. The results are undefined if {@link #stop()}
* or timing methods are called without invoking this method.
* @param taskName the name of the task to start
* @see #stop()
*/
public void start(String taskName) throws IllegalStateException {
if (this.currentTaskName != null) {
throw new IllegalStateException("Can't start StopWatch: it's already running");
}
this.currentTaskName = taskName;
this.startTimeMillis = System.currentTimeMillis();
}

首先记录了当前任务的名称,默认为空字符串,然后记录当前 Spring Boot 应用启动的开始时间。

3.2 初始化应用上下文和异常报告集合

1
2
复制代码		ConfigurableApplicationContext context = null;
Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>();

3.3 设置系统属性 java.awt.headless 的值

1
复制代码        configureHeadlessProperty();

至于为什么设置这个属性值为true,可以参考下面这篇文章:www.cnblogs.com/princessd82…

3.4 创建所有 Spring 运行监听器并发布应用启动事件

1
2
复制代码		SpringApplicationRunListeners listeners = getRunListeners(args);
listeners.starting();

进去看一下创建spring运行监听器的相关源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
复制代码	private SpringApplicationRunListeners getRunListeners(String[] args) {
Class<?>[] types = new Class<?>[] { SpringApplication.class, String[].class };
return new SpringApplicationRunListeners(logger,
getSpringFactoriesInstances(SpringApplicationRunListener.class, types, this, args));
}
SpringApplicationRunListeners {
......
SpringApplicationRunListeners(Log log, Collection<? extends SpringApplicationRunListener> listeners) {
this.log = log;
this.listeners = new ArrayList<>(listeners);
}
......
}

创建逻辑和之前实例化初始化器和监听器的一样,一样调用的是getSpringFactoriesInstances 方法来获取配置的监听器名称并实例化所有的类。SpringApplicationRunListener所有监听器配置在 spring-boot-2.0.4.RELEASE.jar!/META-INF/spring.factories 这个配置文件里面:

1
2
3
复制代码# Run Listeners
org.springframework.boot.SpringApplicationRunListener=\
org.springframework.boot.context.event.EventPublishingRunListener

3.5 初始化默认应用参数类

1
复制代码			ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);

3.6 根据运行监听器和应用参数来准备 Spring 环境

1
2
复制代码			ConfigurableEnvironment environment = prepareEnvironment(listeners, applicationArguments);
configureIgnoreBeanInfo(environment);

下面我们主要来看下准备环境的 prepareEnvironment 源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
复制代码	private ConfigurableEnvironment prepareEnvironment(SpringApplicationRunListeners listeners,
ApplicationArguments applicationArguments) {
// 1.Create the environment
ConfigurableEnvironment environment = getOrCreateEnvironment();
// 2.Configure the environment
configureEnvironment(environment, applicationArguments.getSourceArgs());
listeners.environmentPrepared(environment);
bindToSpringApplication(environment);
if (!this.isCustomEnvironment) {
environment = new EnvironmentConverter(getClassLoader()).convertEnvironmentIfNecessary(environment,
deduceEnvironmentClass());
}
ConfigurationPropertySources.attach(environment);
return environment;
}

3.7 创建 Banner 打印类

1
复制代码			Banner printedBanner = printBanner(environment);

3.8 创建应用上下文

1
复制代码			context = createApplicationContext();

进去源码,可以知道这里主要是根据不同的应用类型初始化不同的上下文应用类。

3.9 准备异常报告器

1
2
复制代码			exceptionReporters = getSpringFactoriesInstances(SpringBootExceptionReporter.class,
new Class[] { ConfigurableApplicationContext.class }, context);

getSpringFactoriesInstances ——>>createSpringFactoriesInstances ——->>>逻辑和之前实例化初始化器和监听器的一样,一样调用的是 getSpringFactoriesInstances 方法来获取配置的异常类名称并实例化所有的异常处理类。

该异常报告处理类配置在 spring-boot-2.0.4.RELEASE.jar!/META-INF/spring.factories 这个配置文件里面。

1
2
3
复制代码# Error Reporters
org.springframework.boot.SpringBootExceptionReporter=\
org.springframework.boot.diagnostics.FailureAnalyzers

3.10 准备应用上下文

1
复制代码			prepareContext(context, environment, listeners, applicationArguments, printedBanner);

接下来进入prepareContext方法:

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
复制代码	private void prepareContext(ConfigurableApplicationContext context, ConfigurableEnvironment environment,
SpringApplicationRunListeners listeners, ApplicationArguments applicationArguments, Banner printedBanner) {
context.setEnvironment(environment);
// 配置上下文的 bean 生成器及资源加载器
postProcessApplicationContext(context);
// 为上下文应用所有初始化器
applyInitializers(context);
// 触发所有 SpringApplicationRunListener 监听器的 contextPrepared 事件方法
listeners.contextPrepared(context);
// 记录日志
if (this.logStartupInfo) {
logStartupInfo(context.getParent() == null);
logStartupProfileInfo(context);
}

// Add boot specific singleton beans 启动两个特殊的单例bean
context.getBeanFactory().registerSingleton("springApplicationArguments", applicationArguments);
if (printedBanner != null) {
context.getBeanFactory().registerSingleton("springBootBanner", printedBanner);
}

// Load the sources 加载所有资源
Set<Object> sources = getAllSources();
Assert.notEmpty(sources, "Sources must not be empty");
load(context, sources.toArray(new Object[0]));
// 触发所有 SpringApplicationRunListener 监听器的 contextLoaded 事件方法
listeners.contextLoaded(context);
}

3.11 刷新应用上下文

1
复制代码			refreshContext(context);

3.12 应用上下文刷新后,自定义处理

1
2
3
4
复制代码			afterRefresh(context, applicationArguments);

protected void afterRefresh(ConfigurableApplicationContext context, ApplicationArguments args) {
}

3.13 停止计时监控类

1
复制代码			stopWatch.stop();

计时监听器停止,并统计一些任务执行信息。

3.14 输出日志记录执行主类名、时间信息

1
2
3
复制代码			if (this.logStartupInfo) {
new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), stopWatch);
}

3.15 发布应用上下文启动完成事件

1
复制代码			listeners.started(context);

这里会触发所有 SpringApplicationRunListener 监听器的 started 事件方法。

3.16 执行所有 Runner 运行器

1
复制代码			callRunners(context, applicationArguments);

执行所有ApplicationRunner以及CommandLineRunner执行器

3.17 发布应用上下文就绪事件

1
复制代码			listeners.running(context);

触发所有 SpringApplicationRunListener 监听器的 running 事件方法。

3.18 返回应用上下文

1
复制代码		return context;

四、总结

  关于SpringBootApplication.run()启动实例初始化以及实例加载run方法的源码分析到此结束,分析源码是件有点痛苦的事情,不过分析完源码后,你会对SpringBoot是如何加载以及初始化有更全面的了解,当然其中也有其它的一些东西值得学习,比如Spring事件监听,如何使用单例,自动化配置等等,最后,希望给各位同学在学习SpringBoot的路上提供一点帮助。看完,如果觉得有收获,希望点个赞。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

如何写出优雅的开源项目文档

发表于 2019-07-29

摘要

mall项目是我去年写的SpringBoot实战电商项目,现在在Github上面已经有18k+star。去年12月份的时候,mall项目只有一些必要的说明文档和部署文档。mall项目涉及到的技术栈比较广泛,业务也比较复杂,却没有系统的学习教程。今年5月份的时候,我开始完善整套学习教程,目前已经更新了三十余篇。最近使用docsify搭建了一个小型的文档网站,希望大家能有更好的阅读体验。本文将介绍如何使用docsify来写开源项目文档。

项目文档演示

展示图片

使用docsify来写项目文档

docsify简介

docsify是一个动态生成网站的工具,它不会将.md文件转化为.html文件从而污染你的Github提交记录,所有转化都将在运行时完成。如果你需要快速搭建一个小型文档网站,这将非常实用。

初始化项目

安装nodejs

  • 下载地址:nodejs.org/dist/v8.9.4…
  • 下载完成后直接安装即可。

安装docsify-cli工具

  • 在命令行中执行如下命令:
1
复制代码npm i docsify-cli -g
  • 安装完成后可以方便地在本地实时预览所编辑的文档。

初始化项目结构

  • 新建一个docs文件夹,然后执行如下命令:
1
复制代码docsify init ./docs
  • docsify会创建如下结构的目录:
1
2
3
4
复制代码  -| docs/
-| .nojekyll
-| index.html
-| README.md

实时预览

  • 在命令行中输入如下命令:
1
复制代码docsify serve docs
  • 访问该地址即可查看效果:http://localhost:3000/

定制侧边栏

  • 在index.html中添加侧边栏的配置:
1
2
3
4
5
6
7
8
9
10
11
复制代码  <script>
window.$docsify = {
loadSidebar: true,
maxLevel: 2,
subMaxLevel: 4,
alias: {
'/.*/_sidebar.md': '/_sidebar.md'//防止意外回退
}
}
</script>
<script src="//unpkg.com/docsify/lib/docsify.min.js"></script>
  • 添加_sidebar.md文件来配置侧边栏:
1
2
3
4
5
6
复制代码  * 序章
* [mall架构及功能概览](foreword/mall_foreword_01.md)
* [mall学习所需知识点](foreword/mall_foreword_02.md)
* 架构篇
* [mall整合SpringBoot+MyBatis搭建基本骨架](architect/mall_arch_01.md)
* [mall整合Swagger-UI实现在线API文档](architect/mall_arch_02.md)
  • 这样就可以生成一个二级的侧边栏:

展示图片

定制导航栏

  • 在index.html中添加导航栏的配置:
1
2
3
4
5
6
7
8
复制代码  <script>
window.$docsify = {
loadNavbar: true,
alias: {
'/.*/_navbar.md': '/_navbar.md'//防止意外回退
}
}
</script>
  • 添加_navbar.md文件来配置导航栏:
1
2
3
4
5
6
7
复制代码  * 演示
* [后台管理](http://39.98.190.128/index.html)
* [移动端](http://39.98.190.128/mall-app/mainpage.html)
* 项目地址
* [后台项目](https://github.com/macrozheng/mall)
* [前端项目](https://github.com/macrozheng/mall-admin-web)
* [学习教程](https://github.com/macrozheng/mall-learning)
  • 这样就可以在右上角生成两个导航栏:

展示图片

定制封面页

  • 在index.html中添加封面页的配置:
1
2
3
4
5
复制代码  <script>
window.$docsify = {
coverpage: true
}
</script>
  • 添加_coverpage.md文件来配置封面页:
1
2
3
4
5
6
7
复制代码  ![logo](images/mall.svg)
# mall-learning
> mall学习教程,架构、业务、技术要点全方位解析。

此处填写详细简介。
[GitHub](https://github.com/macrozheng/mall-learning)
[Get Started](README.md)
  • 查看封面页效果:

展示图片

添加全文搜索

  • 在index.html中添加全文搜索的配置:
1
2
3
4
5
6
7
8
9
10
复制代码  <script>
window.$docsify = {
search: {
placeholder: '搜索',
noData: '找不到结果!',
depth: 3
},
}
</script>
<script src="//unpkg.com/docsify/lib/plugins/search.js"></script>
  • 查看全文搜索效果:

展示图片

添加代码高亮

  • 在index.html中添加代码高亮的配置:
1
2
3
复制代码  <script src="//unpkg.com/prismjs/components/prism-bash.js"></script>
<script src="//unpkg.com/prismjs/components/prism-java.js"></script>
<script src="//unpkg.com/prismjs/components/prism-sql.js"></script>
  • 其他支持高亮语言请参考:github.com/PrismJS/pri…
  • 查看代码高亮效果:

展示图片

添加一键拷贝代码

  • 在index.html中添加一键拷贝代码的配置:
1
复制代码  <script src="//unpkg.com/docsify-copy-code"></script>
  • 查看一键拷贝代码效果:

展示图片

在Github上部署文档

  • 首先将你的代码提交到Github上去;
  • 然后点击项目的Settings按钮:

展示图片

  • 开启GitHub Pages服务:

展示图片

文档地址

macrozheng.github.io/mall-learni…

项目源码地址

github.com/macrozheng/…

公众号

mall项目全套学习教程连载中,关注公众号第一时间获取。

公众号图片

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

学习 jQuery 源码整体架构,打造属于自己的 js 类库

发表于 2019-07-29

前言

你好,我是若川。这是学习源码整体架构第一篇。整体架构这词语好像有点大,姑且就算是源码整体结构吧,主要就是学习是代码整体结构,不深究其他不是主线的具体函数的实现。本篇文章学习的是打包整合后的代码,不是实际仓库中的拆分的代码。

学习源码整体架构系列文章如下:

1.学习 jQuery 源码整体架构,打造属于自己的 js 类库

2.学习 underscore 源码整体架构,打造属于自己的函数式编程类库

3.学习 lodash 源码整体架构,打造属于自己的函数式编程类库

4.学习 sentry 源码整体架构,打造属于自己的前端异常监控SDK

5.学习 vuex 源码整体架构,打造属于自己的状态管理库

6.学习 axios 源码整体架构,打造属于自己的请求库

7.学习 koa 源码的整体架构,浅析koa洋葱模型原理和co原理

8.学习 redux 源码整体架构,深入理解 redux 及其中间件原理

感兴趣的读者可以点击阅读。

虽然现在基本不怎么使用jQuery了,但jQuery流行10多年的JS库,还是有必要学习它的源码的。也可以学着打造属于自己的js类库,求职面试时可以增色不少。

本文章学习的是v3.4.1版本。
unpkg.com源码地址:https://unpkg.com/jquery@3.4.1/dist/jquery.js

jQuery github仓库

自执行匿名函数

1
2
3
4
5
复制代码(function(global, factory){

})(typeof window !== "underfined" ? window: this, function(window, noGlobal){

});

外界访问不到里面的变量和函数,里面可以访问到外界的变量,但里面定义了自己的变量,则不会访问外界的变量。
匿名函数将代码包裹在里面,防止与其他代码冲突和污染全局环境。
关于自执行函数不是很了解的读者可以参看这篇文章。
[译] JavaScript:立即执行函数表达式(IIFE)

浏览器环境下,最后把$ 和 jQuery函数挂载到window上,所以在外界就可以访问到$和jQuery了。

1
2
3
4
复制代码if ( !noGlobal ) {
window.jQuery = window.$ = jQuery;
}
// 其中`noGlobal`参数只有在这里用到。

支持多种环境下使用 比如 commonjs、amd规范

commonjs 规范支持

commonjs实现 主要代表 nodejs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
复制代码// global是全局变量,factory 是函数
( function( global, factory ) {

// 使用严格模式
"use strict";
// Commonjs 或者 CommonJS-like 环境
if ( typeof module === "object" && typeof module.exports === "object" ) {
// 如果存在global.document 则返回factory(global, true);
module.exports = global.document ?
factory( global, true ) :
function( w ) {
if ( !w.document ) {
throw new Error( "jQuery requires a window with a document" );
}
return factory( w );
};
} else {
factory( global );
}

// Pass this if window is not defined yet
// 第一个参数判断window,存在返回window,不存在返回this
} )( typeof window !== "undefined" ? window : this, function( window, noGlobal ) {});

amd 规范 主要代表 requirejs

1
2
3
4
5
复制代码if ( typeof define === "function" && define.amd ) {
define( "jquery", [], function() {
return jQuery;
} );
}

cmd 规范 主要代表 seajs

很遗憾,jQuery源码里没有暴露对seajs的支持。但网上也有一些方案。这里就不具体提了。毕竟现在基本不用seajs了。

无 new 构造

实际上也是可以 new的,因为jQuery是函数。而且和不用new效果是一样的。
new显示返回对象,所以和直接调用jQuery函数作用效果是一样的。
如果对new操作符具体做了什么不明白。可以参看我之前写的文章。

面试官问:能否模拟实现JS的new操作符

源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
复制代码 var
version = "3.4.1",

// Define a local copy of jQuery
jQuery = function( selector, context ) {
// 返回new之后的对象
return new jQuery.fn.init( selector, context );
};
jQuery.fn = jQuery.prototype = {
// jQuery当前版本
jquery: version,
// 修正构造器为jQuery
constructor: jQuery,
length: 0,
};
init = jQuery.fn.init = function( selector, context, root ) {
// ...
if ( !selector ) {
return this;
}
// ...
};
init.prototype = jQuery.fn;
1
2
3
4
5
6
复制代码jQuery.fn === jQuery.prototype;  // true
init = jQuery.fn.init;
init.prototype = jQuery.fn;
// 也就是
jQuery.fn.init.prototype === jQuery.fn; // true
jQuery.fn.init.prototype === jQuery.prototype; // true

关于这个笔者画了一张jQuery原型关系图,所谓一图胜千言。
jQuery-v3.4.1原型关系图

1
2
3
4
复制代码<sciprt src="https://unpkg.com/jquery@3.4.1/dist/jquery.js">
</script>
console.log({jQuery});
// 在谷歌浏览器控制台,可以看到jQuery函数下挂载了很多静态属性和方法,在jQuery.fn 上也挂着很多属性和方法。

Vue源码中,也跟jQuery类似,执行的是Vue.prototype._init方法。

1
2
3
4
5
6
7
8
9
10
11
复制代码function Vue (options) {
if (!(this instanceof Vue)
) {
warn('Vue is a constructor and should be called with the `new` keyword');
}
this._init(options);
}
initMixin(Vue);
function initMixin (Vue) {
Vue.prototype._init = function (options) {};
};

核心函数之一 extend

用法:

1
2
3
复制代码jQuery.extend( target [, object1 ] [, objectN ] )        Returns: Object

jQuery.extend( [deep ], target, object1 [, objectN ] )

jQuery.extend API
jQuery.fn.extend API

看几个例子:
(例子可以我放到在线编辑代码的jQuery.extend例子codepen了,可以直接运行)。

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
复制代码// 1. jQuery.extend( target)
var result1 = $.extend({
job: '前端开发工程师',
});

console.log(result1, 'result1', result1.job); // $函数 加了一个属性 job // 前端开发工程师

// 2. jQuery.extend( target, object1)
var result2 = $.extend({
name: '若川',
},
{
job: '前端开发工程师',
});

console.log(result2, 'result2'); // { name: '若川', job: '前端开发工程师' }

// deep 深拷贝
// 3. jQuery.extend( [deep ], target, object1 [, objectN ] )
var result3 = $.extend(true, {
name: '若川',
other: {
mac: 0,
ubuntu: 1,
windows: 1,
},
}, {
job: '前端开发工程师',
other: {
mac: 1,
linux: 1,
windows: 0,
}
});
console.log(result3, 'result3');
// deep true
// {
// "name": "若川",
// "other": {
// "mac": 1,
// "ubuntu": 1,
// "windows": 0,
// "linux": 1
// },
// "job": "前端开发工程师"
// }
// deep false
// {
// "name": "若川",
// "other": {
// "mac": 1,
// "linux": 1,
// "windows": 0
// },
// "job": "前端开发工程师"
// }

结论:extend函数既可以实现给jQuery函数可以实现浅拷贝、也可以实现深拷贝。可以给jQuery上添加静态方法和属性,也可以像jQuery.fn(也就是jQuery.prototype)上添加属性和方法,这个功能归功于this,jQuery.extend调用时this指向是jQuery,jQuery.fn.extend调用时this指向则是jQuery.fn。

浅拷贝实现

知道这些,其实实现浅拷贝还是比较容易的:

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
复制代码// 浅拷贝实现
jQuery.extend = function(){
// options 是扩展的对象object1,object2...
var options,
// object对象上的键
name,
// copy object对象上的值,也就是是需要拷贝的值
copy,
// 扩展目标对象,可能不是对象,所以或空对象
target = arguments[0] || {},
// 定义i为1
i = 1,
// 定义实参个数length
length = arguments.length;
// 只有一个参数时
if(i === length){
target = this;
i--;
}
for(; i < length; i++){
// 不是underfined 也不是null
if((options = arguments[i]) != null){
for(name in options){
copy = options[name];
// 防止死循环,continue 跳出当前此次循环
if ( name === "__proto__" || target === copy ) {
continue;
}
if ( copy !== undefined ) {
target[ name ] = copy;
}
}
}

}
// 最后返回目标对象
return target;
}

深拷贝则主要是在以下这段代码做判断。可能是数组和对象引用类型的值,做判断。

1
2
3
复制代码if ( copy !== undefined ) {
target[ name ] = copy;
}

为了方便读者调试,代码同样放在jQuery.extend浅拷贝代码实现codepen,可在线运行。

深拷贝实现

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
复制代码$.extend = function(){
// options 是扩展的对象object1,object2...
var options,
// object对象上的键
name,
// copy object对象上的值,也就是是需要拷贝的值
copy,
// 深拷贝新增的四个变量 deep、src、copyIsArray、clone
deep = false,
// 源目标,需要往上面赋值的
src,
// 需要拷贝的值的类型是函数
copyIsArray,
//
clone,
// 扩展目标对象,可能不是对象,所以或空对象
target = arguments[0] || {},
// 定义i为1
i = 1,
// 定义实参个数length
length = arguments.length;

// 处理深拷贝情况
if ( typeof target === "boolean" ) {
deep = target;

// Skip the boolean and the target
// target目标对象开始后移
target = arguments[ i ] || {};
i++;
}

// Handle case when target is a string or something (possible in deep copy)
// target不等于对象,且target不是函数的情况下,强制将其赋值为空对象。
if ( typeof target !== "object" && !isFunction( target ) ) {
target = {};
}

// 只有一个参数时
if(i === length){
target = this;
i--;
}
for(; i < length; i++){
// 不是underfined 也不是null
if((options = arguments[i]) != null){
for(name in options){
copy = options[name];
// 防止死循环,continue 跳出当前此次循环
if ( name === "__proto__" || target === copy ) {
continue;
}

// Recurse if we're merging plain objects or arrays
// 这里deep为true,并且需要拷贝的值有值,并且是纯粹的对象
// 或者需拷贝的值是数组
if ( deep && copy && ( jQuery.isPlainObject( copy ) ||
( copyIsArray = Array.isArray( copy ) ) ) ) {

// 源目标,需要往上面赋值的
src = target[ name ];

// Ensure proper type for the source value
// 拷贝的值,并且src不是数组,clone对象改为空数组。
if ( copyIsArray && !Array.isArray( src ) ) {
clone = [];
// 拷贝的值不是数组,对象不是纯粹的对象。
} else if ( !copyIsArray && !jQuery.isPlainObject( src ) ) {
// clone 赋值为空对象
clone = {};
} else {
// 否则 clone = src
clone = src;
}
// 把下一次循环时,copyIsArray 需要重新赋值为false
copyIsArray = false;

// Never move original objects, clone them
// 递归调用自己
target[ name ] = jQuery.extend( deep, clone, copy );

// Don't bring in undefined values
}
else if ( copy !== undefined ) {
target[ name ] = copy;
}
}
}

}
// 最后返回目标对象
return target;
};

为了方便读者调试,这段代码同样放在jQuery.extend深拷贝代码实现codepen,可在线运行。

深拷贝衍生的函数 isFunction

判断参数是否是函数。

1
2
3
4
5
6
7
8
复制代码var isFunction = function isFunction( obj ) {

// Support: Chrome <=57, Firefox <=52
// In some browsers, typeof returns "function" for HTML <object> elements
// (i.e., `typeof document.createElement( "object" ) === "function"`).
// We don't want to classify *any* DOM node as a function.
return typeof obj === "function" && typeof obj.nodeType !== "number";
};

深拷贝衍生的函数 jQuery.isPlainObject

jQuery.isPlainObject(obj)
测试对象是否是纯粹的对象(通过 “{}” 或者 “new Object” 创建的)。

1
2
复制代码jQuery.isPlainObject({}) // true
jQuery.isPlainObject("test") // false
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
复制代码var getProto = Object.getPrototypeOf;
var class2type = {};
var toString = class2type.toString;
var hasOwn = class2type.hasOwnProperty;
var fnToString = hasOwn.toString;
var ObjectFunctionString = fnToString.call( Object );

jQuery.extend( {
isPlainObject: function( obj ) {
var proto, Ctor;

// Detect obvious negatives
// Use toString instead of jQuery.type to catch host objects
// !obj 为true或者 不为[object Object]
// 直接返回false
if ( !obj || toString.call( obj ) !== "[object Object]" ) {
return false;
}

proto = getProto( obj );

// Objects with no prototype (e.g., `Object.create( null )`) are plain
// 原型不存在 比如 Object.create(null) 直接返回 true;
if ( !proto ) {
return true;
}

// Objects with prototype are plain iff they were constructed by a global Object function
Ctor = hasOwn.call( proto, "constructor" ) && proto.constructor;
// 构造器是函数,并且 fnToString.call( Ctor ) === fnToString.call( Object );
return typeof Ctor === "function" && fnToString.call( Ctor ) === ObjectFunctionString;
},
});

extend函数,也可以自己删掉写一写,算是jQuery中一个比较核心的函数了。而且用途广泛,可以内部使用也可以,外部使用扩展 插件等。

链式调用

jQuery能够链式调用是因为一些函数执行结束后 return this。
比如
jQuery 源码中的addClass、removeClass、toggleClass。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
复制代码jQuery.fn.extend({
addClass: function(){
// ...
return this;
},
removeClass: function(){
// ...
return this;
},
toggleClass: function(){
// ...
return this;
},
});

jQuery.noConflict 很多js库都会有的防冲突函数

jQuery.noConflict API

用法:

1
2
3
4
5
6
7
8
9
复制代码 <script>
var $ = '我是其他的$,jQuery不要覆盖我';
</script>
<script src="./jquery-3.4.1.js">
</script>
<script>
$.noConflict();
console.log($); // 我是其他的$,jQuery不要覆盖我
</script>

jQuery.noConflict 源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
复制代码var

// Map over jQuery in case of overwrite
_jQuery = window.jQuery,

// Map over the $ in case of overwrite
_$ = window.$;

jQuery.noConflict = function( deep ) {
// 如果已经存在$ === jQuery;
// 把已存在的_$赋值给window.$;
if ( window.$ === jQuery ) {
window.$ = _$;
}

// 如果deep为 true, 并且已经存在jQuery === jQuery;
// 把已存在的_jQuery赋值给window.jQuery;
if ( deep && window.jQuery === jQuery ) {
window.jQuery = _jQuery;
}

// 最后返回jQuery
return jQuery;
};

总结

全文主要通过浅析了jQuery整体结构,自执行匿名函数、无new构造、支持多种规范(如commonjs、amd规范)、核心函数之extend、链式调用、jQuery.noConflict等方面。

重新梳理下文中学习的源码结构。

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
复制代码// 源码结构
( function( global, factory )
"use strict";
if ( typeof module === "object" && typeof module.exports === "object" ) {
module.exports = global.document ?
factory( global, true ) :
function( w ) {
if ( !w.document ) {
throw new Error( "jQuery requires a window with a document" );
}
return factory( w );
};
} else {
factory( global );
}

} )( typeof window !== "undefined" ? window : this, function( window, noGlobal ) {
var version = "3.4.1",

// Define a local copy of jQuery
jQuery = function( selector, context ) {
return new jQuery.fn.init( selector, context );
};

jQuery.fn = jQuery.prototype = {
jquery: version,
constructor: jQuery,
length: 0,
// ...
};

jQuery.extend = jQuery.fn.extend = function() {};

jQuery.extend( {
// ...
isPlainObject: function( obj ) {},
// ...
});

init = jQuery.fn.init = function( selector, context, root ) {};

init.prototype = jQuery.fn;

if ( typeof define === "function" && define.amd ) {
define( "jquery", [], function() {
return jQuery;
} );
}
jQuery.noConflict = function( deep ) {};

if ( !noGlobal ) {
window.jQuery = window.$ = jQuery;
}

return jQuery;
});

可以学习到jQuery巧妙的设计和架构,为自己所用,打造属于自己的js类库。
相关代码和资源放置在github blog中,需要的读者可以自取。

下一篇文章是学习underscorejs的源码整体架构。学习 underscore 源码整体架构,打造属于自己的函数式编程类库

读者发现有不妥或可改善之处,欢迎评论指出。另外觉得写得不错,可以点赞、评论、转发,也是对笔者的一种支持。

笔者往期文章

面试官问:JS的继承

面试官问:JS的this指向

面试官问:能否模拟实现JS的call和apply方法

面试官问:能否模拟实现JS的bind方法

面试官问:能否模拟实现JS的new操作符

前端使用puppeteer 爬虫生成《React.js 小书》PDF并合并

扩展阅读

chokcoco: jQuery- v1.10.2 源码解读

chokcoco:【深入浅出jQuery】源码浅析–整体架构

songjz :jQuery 源码系列(一)总体架构

笔者另一个系列

面试官问:JS的继承

面试官问:JS的this指向

面试官问:能否模拟实现JS的call和apply方法

面试官问:能否模拟实现JS的bind方法

面试官问:能否模拟实现JS的new操作符

关于

作者:常以若川为名混迹于江湖。前端路上 | PPT爱好者 | 所知甚少,唯善学。

若川的博客,使用vuepress重构了,阅读体验可能更好些

掘金专栏,欢迎关注~

segmentfault前端视野专栏,欢迎关注~

语雀前端视野专栏,新增语雀专栏,欢迎关注~

知乎前端视野专栏,欢迎关注~

github blog,相关源码和资源都放在这里,求个star^_^~

欢迎加微信交流 微信公众号

可能比较有趣的微信公众号,长按扫码关注。欢迎加笔者微信ruochuan12(注明来源,基本来者不拒),拉您进【前端视野交流群】,长期交流学习~

若川视野

若川视野

本文使用 mdnice 排版

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

MySQL锁机制——你想知道的都在这!

发表于 2019-07-29

一、锁的类型

行锁

  • 共享锁(S Lock)允许事务读一行数据
  • 排它锁 (X Lock) 允许事务写一行数据

表锁(意向锁)

锁定允许事务在行级上的锁和表级上的锁同时存在。为了支持在不同粒度上进行加锁操作,InnoDB存储引擎支持一种额外的锁方式

  • 意向共享锁(IS Lock)事务想要获得一张表中某几行的共享锁
  • 意向排他锁(IX Lock)事务想要获得一张表中某几行的排他锁

由于InnoDB存储引擎支持的是行级别的锁,因此意向锁其实不会阻塞除全表扫以外的任何请求。故表级意向锁与行级锁的兼容性如下所示

若将上锁的对象看成一棵树,那么对最下层的对象上锁,也就是对最细粒度的对象进行上锁,那么首先需要对粗粒度的对象上锁。例上图,如果需要对页上的记录r进行上X锁,那么分别需要对数据库A、表、页上意向锁IX,最后对记录r上X锁。若其中任何一个部分导致等待,那么该操作需要等待粗粒度锁的完成。举例来说,在对记录r加X锁之前,已经有事务对表1进行了S表锁,那么表1上已存在S锁,之后事务需要对记录r在表1上加上IX,由于不兼容,所以该事务需要等待表锁操作的完成。

意向锁到底有什么作用?

innodb的意向锁主要用户多粒度的锁并存的情况。比如事务A要在一个表上加S锁,如果表中的一行已被事务B加了X锁,那么该锁的申请也应被阻塞。如果表中的数据很多,逐行检查锁标志的开销将很大,系统的性能将会受到影响。为了解决这个问题,可以在表级上引入新的锁类型来表示其所属行的加锁情况,这就引出了“意向锁”的概念。

举个例子,如果表中记录1亿,事务A把其中有几条记录上了行锁了,这时事务B需要给这个表加表级锁,如果没有意向锁的话,那就要去表中查找这一亿条记录是否上锁了。如果存在意向锁,那么假如事务A在更新一条记录之前,先加意向锁,再加X锁,事务B先检查该表上是否存在意向锁,存在的意向锁是否与自己准备加的锁冲突,如果有冲突,则等待直到事务A释放,而无须逐条记录去检测。事务B更新表时,其实无须知道到底哪一行被锁了,它只要知道反正有一行被锁了就行了。

主要作用是处理行锁和表锁之间的矛盾,能够显示“某个事务正在某一行上持有了锁,或者准备去持有锁”

二、锁的算法

  • Record Lock:单个行记录上的锁
  • Gap Lock:间隙锁,锁定一个范围,但不包含记录本身
  • Next-Key Lock∶Gap Lock+Record Lock,锁定一个范围、索引之间的间隙,并且锁定记录本身;目的是为了防止幻读

三、mysql如何做到读写并行(多版本控制)?

多版本并发控制 MVCC,是行级锁的一个变种,通过保存数据在某个时间节点的快照(snapshot),类似实现了行级锁。由此不同事务对同一表,同一时刻看到的数据可能是不一样的。
实现上通过在不同的数据行后增加创建日期版本号和删除日期版本号,且版本号不断递增,进而实现了数据快照

读的类型

  • 一致性非锁定读(快照读)
    • 在事务隔离级别提交读(RC)和可重复读(RR)下,InnoDB存储引擎使用非锁定的一致性读
      • RC模式下,读取最新的快照
      • RR模式下,读取事务开始时的快照
  • 一致性锁定读 (当前读)
    • 隔离级别为未提交读(RN)时读取都是当前读
    • SELECT…FOR UPDATE (加写锁)
    • SELECT…LOCK IN SHARE MODE (加读锁)

四、加锁处理分析

下面两条简单的SQL,他们加什么锁?

select * from t1 where id = 10

delete from t1 where id = 10

如果要分析加锁情况,必须还要知道以下的一些前提,前提不同,加锁处理的方式也不同

  • 前提一:id列是不是主键?
  • 前提二:当前系统的隔离级别是什么?
  • 前提三:id列如果不是主键,那么id列上有索引吗?
  • 前提四:id列上如果有二级索引,那么这个索引是唯一索引吗?
  • 前提五:两个SQL的执行计划是什么?索引扫描?全表扫描?

根据上述情况,有以下几种组合

  • 组合一:id列是主键,RC隔离级别
  • 组合二:id列是二级唯一索引,RC隔离级别
  • 组合三:id列是二级非唯一索引,RC隔离级别
  • 组合四:id列上没有索引,RC隔离级别
  • 组合五:id列是主键,RR隔离级别
  • 组合六:id列是二级唯一索引,RR隔离级别
  • 组合七:id列是二级非唯一索引,RR隔离级别
  • 组合八:id列上没有索引,RR隔离级别
  • 组合九:Serializable隔离级别

排列组合还没有列举完全,但是看起来,已经很多了。真的有必要这么复杂吗?事实上,要分析加锁,就是需要这么复杂。但是从另一个角度来说,只要你选定了一种组合,SQL需要加哪些锁,其实也就确定了。接下来挑几个比较经典的组合

组合一:id主键+RC

  这个组合,是最简单,最容易分析的组合。id是主键,Read Committed隔离级别,给定SQL:delete from t1 where id = 10; 只需要将主键上,id = 10的记录加上X锁即可。如下图所示:            

      结论:id是主键时,此SQL只需要在id=10这条记录上加X锁即可。

组合二:id唯一索引+RC

  这个组合,id不是主键,而是一个Unique的二级索引键值。那么在RC隔离级别下,delete from t1 where id = 10; 需要加什么锁呢?见下图:        

  此组合中,id是unique索引,而主键是name列。此时,加锁的情况由于组合一有所不同。由于id是unique索引,因此delete语句会选择走id列的索引进行where条件的过滤,在找到id=10的记录后,首先会将unique索引上的id=10索引记录加上X锁,同时,会根据读取到的name列,回主键索引(聚簇索引),然后将聚簇索引上的name = ‘d’ 对应的主键索引项加X锁。为什么聚簇索引上的记录也要加锁?试想一下,如果并发的一个SQL,是通过主键索引来更新:update t1 set id = 100 where name = ‘d’; 此时,如果delete语句没有将主键索引上的记录加锁,那么并发的update就会感知不到delete语句的存在,违背了同一记录上的更新/删除需要串行执行的约束。

结论:若id列是unique列,其上有unique索引。那么SQL需要加两个X锁,一个对应于id unique索引上的id = 10的记录,另一把锁对应于聚簇索引上的[name=’d’,id=10]的记录。

组合三:id非唯一索引+RC

  相对于组合一、二,组合三又发生了变化,隔离级别仍旧是RC不变,但是id列上的约束又降低了,id列不再唯一,只有一个普通的索引。假设delete from t1 where id = 10; 语句,仍旧选择id列上的索引进行过滤where条件,那么此时会持有哪些锁?同样见下图:

  根据此图,可以看到,首先,id列索引上,满足id = 10查询条件的记录,均已加锁。同时,这些记录对应的主键索引上的记录也都加上了锁。与组合二唯一的区别在于,组合二最多只有一个满足等值查询的记录,而组合三会将所有满足查询条件的记录都加锁。

结论:若id列上有非唯一索引,那么对应的所有满足SQL查询条件的记录,都会被加锁。同时,这些记录在主键索引上的记录,也会被加锁。

组合四:id无索引+RC

  相对于前面三个组合,这是一个比较特殊的情况。id列上没有索引,where id = 10;这个过滤条件,没法通过索引进行过滤,那么只能走全表扫描做过滤。对应于这个组合,SQL会加什么锁?或者是换句话说,全表扫描时,会加什么锁?这个答案也有很多:有人说会在表上加X锁;有人说会将聚簇索引上,选择出来的id = 10;的记录加上X锁。那么实际情况呢?请看下图:

  由于id列上没有索引,因此只能走聚簇索引,进行全部扫描。从图中可以看到,满足删除条件的记录有两条,但是,聚簇索引上所有的记录,都被加上了X锁。无论记录是否满足条件,全部被加上X锁。既不是加表锁,也不是在满足条件的记录上加行锁。

  有人可能会问?为什么不是只在满足条件的记录上加锁呢?这是由于MySQL的实现决定的。如果一个条件无法通过索引快速过滤,那么存储引擎层面就会将所有记录加锁后返回,然后由MySQL Server层进行过滤。因此也就把所有的记录,都锁上了。

注:在实际的实现中,MySQL有一些改进,在MySQL Server过滤条件,发现不满足后,会调用unlock_row方法,把不满足条件的记录放锁 (违背了2PL的约束)。这样做,保证了最后只会持有满足条件记录上的锁,但是每条记录的加锁操作还是不能省略的。

结论:若id列上没有索引,SQL会走聚簇索引的全扫描进行过滤,由于过滤是由MySQL Server层面进行的。因此每条记录,无论是否满足条件,都会被加上X锁。但是,为了效率考量,MySQL做了优化,对于不满足条件的记录,会在判断后放锁,最终持有的,是满足条件的记录上的锁,但是不满足条件的记录上的加锁/放锁动作不会省略。同时,优化也违背了2PL的约束。

组合五:id主键+RR

  上面的四个组合,都是在Read Committed隔离级别下的加锁行为,接下来的四个组合,是在Repeatable Read隔离级别下的加锁行为。

  组合五,id列是主键列,Repeatable Read隔离级别,针对delete from t1 where id = 10; 这条SQL,加锁与组合一:[id主键,Read Committed]一致。

组合六:id唯一索引+RR

  与组合五类似,组合六的加锁,与组合二:[id唯一索引,Read Committed]一致。两个X锁,id唯一索引满足条件的记录上一个,对应的聚簇索引上的记录一个。

组合七:id非唯一索引+RR

  还记得前面提到的MySQL的四种隔离级别的区别吗?RC隔离级别允许幻读,而RR隔离级别,不允许存在幻读。但是在组合五、组合六中,加锁行为又是与RC下的加锁行为完全一致。那么RR隔离级别下,

  组合七,Repeatable Read隔离级别,id上有一个非唯一索引,执行delete from t1 where id = 10; 假设选择id列上的索引进行条件过滤,最后的加锁行为,是怎么样的呢?同样看下面这幅图:       

  此图,相对于组合三:[id列上非唯一锁,Read Committed]看似相同,其实却有很大的区别。最大的区别在于,这幅图中多了一个GAP锁,而且GAP锁看起来也不是加在记录上的,倒像是加载两条记录之间的位置,GAP锁有何用?

  其实这个多出来的GAP锁,就是RR隔离级别,相对于RC隔离级别,不会出现幻读的关键。确实,GAP锁锁住的位置,也不是记录本身,而是两条记录之间的GAP。

  如何保证两次当前读返回一致的记录,那就需要在第一次当前读与第二次当前读之间,其他的事务不会插入新的满足条件的记录并提交。为了实现这个功能,GAP锁应运而生。

  如图中所示,有哪些位置可以插入新的满足条件的项 (id = 10),考虑到B+树索引的有序性,满足条件的项一定是连续存放的。记录[6,c]之前,不会插入id=10的记录;[6,c]与[10,b]间可以插入[10, aa];[10,b]与[10,d]间,可以插入新的[10,bb],[10,c]等;[10,d]与[11,f]间可以插入满足条件的[10,e],[10,z]等;而[11,f]之后也不会插入满足条件的记录。因此,为了保证[6,c]与[10,b]间,[10,b]与[10,d]间,[10,d]与[11,f]不会插入新的满足条件的记录,MySQL选择了用GAP锁,将这三个GAP给锁起来。

  Insert操作,如insert [10,aa],首先会定位到[6,c]与[10,b]间,然后在插入前,会检查这个GAP是否已经被锁上,如果被锁上,则Insert不能插入记录。因此,通过第一遍的当前读,不仅将满足条件的记录锁上 (X锁),与组合三类似。同时还是增加3把GAP锁,将可能插入满足条件记录的3个GAP给锁上,保证后续的Insert不能插入新的id=10的记录,也就杜绝了同一事务的第二次当前读,出现幻象的情况。

  有心的朋友看到这儿,可以会问:既然防止幻读,需要靠GAP锁的保护,为什么组合五、组合六,也是RR隔离级别,却不需要加GAP锁呢?

  首先,这是一个好问题。其次,回答这个问题,也很简单。GAP锁的目的,是为了防止同一事务的两次当前读,出现幻读的情况。而组合五,id是主键;组合六,id是unique键,都能够保证唯一性。一个等值查询,最多只能返回一条记录,而且新的相同取值的记录,一定不会在新插入进来,因此也就避免了GAP锁的使用。其实,针对此问题,还有一个更深入的问题:如果组合五、组合六下,针对SQL:select * from t1 where id = 10 for update; 第一次查询,没有找到满足查询条件的记录,那么GAP锁是否还能够省略?此问题留给大家思考。

结论:Repeatable Read隔离级别下,id列上有一个非唯一索引,对应SQL:delete from t1 where id = 10; 首先,通过id索引定位到第一条满足查询条件的记录,加记录上的X锁,加GAP上的GAP锁,然后加主键聚簇索引上的记录X锁,然后返回;然后读取下一条,重复进行。直至进行到第一条不满足条件的记录[11,f],此时,不需要加记录X锁,但是仍旧需要加GAP锁,最后返回结束。

什么时候会取得gap lock或nextkey lock 这和隔离级别有关,只在REPEATABLE READ或以上的隔离级别下的特定操作才会取得gap lock或nextkey lock。

组合八:id无索引+RR

  组合八,Repeatable Read隔离级别下的最后一种情况,id列上没有索引。此时SQL:delete from t1 where id = 10; 没有其他的路径可以选择,只能进行全表扫描。最终的加锁情况,如下图所示:   

  如图,这是一个很恐怖的现象。首先,聚簇索引上的所有记录,都被加上了X锁。其次,聚簇索引每条记录间的间隙(GAP),也同时被加上了GAP锁。这个示例表,只有6条记录,一共需要6个记录锁,7个GAP锁。试想,如果表上有1000万条记录呢?

  在这种情况下,这个表上,除了不加锁的快照度,其他任何加锁的并发SQL,均不能执行,不能更新,不能删除,不能插入,全表被锁死。

  当然,跟组合四:[id无索引, Read Committed]类似,这个情况下,MySQL也做了一些优化,就是所谓的semi-consistent read。semi-consistent read开启的情况下,对于不满足查询条件的记录,MySQL会提前放锁。针对上面的这个用例,就是除了记录[d,10],[g,10]之外,所有的记录锁都会被释放,同时不加GAP锁。semi-consistent read如何触发:要么是read committed隔离级别;要么是Repeatable Read隔离级别,同时设置了innodb_locks_unsafe_for_binlog 参数。更详细的关于semi-consistent read的介绍,可参考我之前的一篇博客:MySQL+InnoDB semi-consitent read原理及实现分析 。

结论:在Repeatable Read隔离级别下,如果进行全表扫描的当前读,那么会锁上表中的所有记录,同时会锁上聚簇索引内的所有GAP,杜绝所有的并发 更新/删除/插入 操作。当然,也可以通过触发semi-consistent read,来缓解加锁开销与并发影响,但是semi-consistent read本身也会带来其他问题,不建议使用。

组合九:Serializable

  针对前面提到的简单的SQL,最后一个情况:Serializable隔离级别。对于SQL2:delete from t1 where id = 10; 来说,Serializable隔离级别与Repeatable Read隔离级别完全一致,因此不做介绍。

  Serializable隔离级别,影响的是SQL1:select * from t1 where id = 10; 这条SQL,在RC,RR隔离级别下,都是快照读,不加锁。但是在Serializable隔离级别,SQL1会加读锁,也就是说快照读不复存在,MVCC并发控制降级为Lock-Based CC。

结论:在MySQL/InnoDB中,所谓的读不加锁,并不适用于所有的情况,而是隔离级别相关的。Serializable隔离级别,读不加锁就不再成立,所有的读操作,都是当前读。

五、死锁案例

1. 不同表相同记录行锁冲突

这种情况很好理解,事务A和事务B操作两张表,但出现循环等待锁情况。

2. 相同表记录行锁冲突

这种情况比较常见,之前遇到两个job在执行数据批量更新时,jobA处理的的id列表为[1,2,3,4],而job处理的id列表为[8,9,10,4,2],这样就造成了死锁。

3. 不同索引锁冲突

这种情况比较隐晦,事务A在执行时,除了在二级索引加锁外,还会在聚簇索引上加锁,在聚簇索引上加锁的顺序是[1,4,2,3,5],而事务B执行时,只在聚簇索引上加锁,加锁顺序是[1,2,3,4,5],这样就造成了死锁的可能性。

4. gap锁冲突

innodb在RR级别下,如下的情况也会产生死锁,比较隐晦。不清楚的同学可以自行根据上节的gap锁原理分析下。

六、如何尽可能避免死锁

  1. 以固定的顺序访问表和行。比如对第2节两个job批量更新的情形,简单方法是对id列表先排序,后执行,这样就避免了交叉等待锁的情形;又比如对于3.1节的情形,将两个事务的sql顺序调整为一致,也能避免死锁。
  2. 大事务拆小。大事务更倾向于死锁,如果业务允许,将大事务拆小。
  3. 在同一个事务中,尽可能做到一次锁定所需要的所有资源,减少死锁概率。
  4. 降低隔离级别。如果业务允许,将隔离级别调低也是较好的选择,比如将隔离级别从RR调整为RC,可以避免掉很多因为gap锁造成的死锁。
  5. 为表添加合理的索引。可以看到如果不走索引将会为表的每一行记录添加上锁,死锁的概率大大增大。=

七、如何查看锁

从InnoDB1.0开始,在INFORMATION_SCHEMA架构下添加了表INNODB_TRX、INNODB_LOCKS、INNODB_LOCK_WAITS。(详情见附录)通过这三张表,用户可以更简单地监控当前事务并分析可能存在的锁问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
复制代码#全局分析系统上中行锁的争夺情况
show status like 'innodb_row_lock%';
#查看事务
SELECT * FROM information_schema.INNODB_TRX;
#查看锁
SELECT * FROM information_schema.INNODB_LOCKS;
#查看锁等待情况
SELECT * FROM information_schema.INNODB_LOCK_WAITS;
#通过联合查询可以比较直观的查看哪个事务阻塞了哪个事务
SELECT r.trx_id '等待事务ID',
r.trx_mysql_thread_id '等待线程ID',
r.trx_query '等待事务运行语句',
b.trx_id '阻塞事务ID',
b.trx_mysql_thread_id '阻塞线程ID',
b.trx_query '阻塞事务运行语句'
FROM information_schema.innodb_lock_waits w
INNER JOIN information_schema.innodb_trx b
ON b.trx_id = w.blocking_trx_id
INNER JOIN information_schema.innodb_trx r
ON r.trx_id = w.requesting_trx_id;

八、mysql是如何预防死锁的?

innodb_lock_wait_timeout 等待锁超时回滚事务

直观方法是在两个事务相互等待时,当一个等待时间超过设置的某一阀值时,对其中一个事务进行回滚,另一个事务就能继续执行。

wait-for graph算法来主动进行死锁检测

每当加锁请求无法立即满足需要并进入等待时,wait-for graph算法都会被触发。

wait-for graph要求数据库保存以下两种信息:

  • 锁的信息链表
  • 事务等待链表

通过上述链表可以构造出一张图,而在这个图中若存在回路,就代表存在死锁,因此资源间相互发生等待。在wait-for graph中,事务为图中的节点。而在图中,事务T1指向T2边的定义为:

  • 事务T1等待事务T2所占用的资源
  • 事务T1最终等待T2所占用的资源,也就是事务之间在等待相同的资源,而事务T1发生在事务T2的后面

示例事务状态和锁的信息

在Transaction Wait Lists中可以看到共有4个事务t1、t2、t3、t4,故在wait-for graph中应有4个节点。而事务t2对row1占用x锁,事务t1对row2占用s锁。事务t1需要等待事务t2中row1的资源,因此在wait-for graph中有条边从节点t1指向节点t2。事务t2需要等待事务t1、t4所占用的row2对象,故而存在节点t2到节点t1、t4的边。同样,存在节点t3到节点t1、t2、t4的边,因此最终的wait-for graph如下图所示。

ps:若存在则有死锁,通常来说InnoDB存储引擎选择回滚undo量最小的事务并从新开始

附录

INNODB_ROW_LOCK

列名 描述
innodb_row_lock_current_waits 当前正在等待锁定的数量
innodb_row_lock_time 从系统启动到现在锁定总时间长度
innodb_row_lock_time_avg 每次等待所花平均时间
innodb_row_lock_time_max 从系统启动到现在等待最常的一次所花的时间
innodb_row_lock_waits 系统启动后到现在总共等待的次数;直接决定优化的方向和策略

INNODB_TRX

提供有关当前正在内部执行的每个事务的信息 InnoDB,包括事务是否在等待锁定,事务何时启动以及事务正在执行的SQL语句(如果有)。详见dev.mysql.com/doc/refman/…

列名 描述
TRX_ID 事务Id
TRX_WEIGHT 事务的权重,反映(但不一定是确切的计数)更改的行数和事务锁定的行数。要解决死锁,请 InnoDB``选择权重最小的事务作为回滚的“ 受害者 ”。无论更改和锁定行的数量如何,已更改非事务表的事务都被认为比其他事务更重。
TRX_STATE 事务执行状态。允许值是 RUNNING,LOCK WAIT, ROLLING BACK,和 COMMITTING。
TRX_STARTED 交易开始时间。
TRX_REQUESTED_LOCK_ID 事务当前正在等待的锁的ID,如果TRX_STATE是LOCK WAIT; 否则NULL``。
TRX_WAIT_STARTED 交易开始等待锁定的时间,如果 TRX_STATE是LOCK WAIT; 否则NULL``。
TRX_MYSQL_THREAD_ID MySQL线程ID,与show processlist中的ID值相对应
TRX_QUERY 事务正在执行的SQL语句
TRX_OPERATION_STATE 交易的当前操作,如果有的话; 否则 NULL``。
TRX_TABLES_IN_USE InnoDB``处理此事务的当前SQL语句时使用 的表数。
TRX_TABLES_LOCKED InnoDB``当前SQL语句具有行锁定 的表的数量。(因为这些是行锁,而不是表锁,所以通常仍可以通过多个事务读取和写入表,尽管某些行被锁定。)
TRX_LOCK_STRUCTS 事务保留的锁数。
TRX_LOCK_MEMORY_BYTES 内存中此事务的锁结构占用的总大小
TRX_ROWS_LOCKED 此交易锁定的大致数字或行数。该值可能包括实际存在但对事务不可见的删除标记行
TRX_ROWS_MODIFIED 此事务中已修改和插入的行数。
TRX_CONCURRENCY_TICKETS 一个值,指示当前事务在被换出之前可以执行多少工作
TRX_ISOLATION_LEVEL 当前事务的隔离级别。
TRX_UNIQUE_CHECKS 是否为当前事务打开或关闭唯一检查。例如,在批量数据加载期间可能会关闭它们
TRX_FOREIGN_KEY_CHECKS 是否为当前事务打开或关闭外键检查。例如,在批量数据加载期间可能会关闭它们
TRX_LAST_FOREIGN_KEY_ERROR 最后一个外键错误的详细错误消息(如果有); 否则NULL``
TRX_ADAPTIVE_HASH_LATCHED 自适应哈希索引是否被当前事务锁定。当自适应哈希索引搜索系统被分区时,单个事务不会锁定整个自适应哈希索引。自适应哈希索引分区由innodb_adaptive_hash_index_parts``,默认设置为8。
TRX_ADAPTIVE_HASH_TIMEOUT 是否立即为自适应哈希索引放弃搜索锁存器,或者在MySQL的调用之间保留它。当没有自适应哈希索引争用时,该值保持为零,语句保留锁存器直到它们完成。在争用期间,它倒计时到零,并且语句在每次行查找后立即释放锁存器。当自适应散列索引搜索系统被分区(受控制 innodb_adaptive_hash_index_parts``)时,该值保持为0。
TRX_IS_READ_ONLY 值为1表示事务是只读的。
TRX_AUTOCOMMIT_NON_LOCKING 值为1表示事务是 SELECT](https://dev.mysql.com/doc/refman/5.7/en/select.html)不使用FOR UPDATEor或 LOCK IN SHARED MODE子句的语句,并且正在执行, [autocommit因此事务将仅包含此一个语句。当此列和TRX_IS_READ_ONLY都为1时,InnoDB优化事务以减少与更改表数据的事务关联的开销

INNODB_LOCKS

提供有关InnoDB 事务已请求但尚未获取的每个锁的信息,以及事务持有的阻止另一个事务的每个锁。详见dev.mysql.com/doc/refman/…

列名 描述
LOCK_ID 一个唯一的锁ID号,内部为 InnoDB``。
LOCK_TRX_ID 持有锁的交易的ID
LOCK_MODE 如何请求锁定。允许锁定模式描述符 S,X, IS,IX, GAP,AUTO_INC,和 UNKNOWN``。锁定模式描述符可以组合使用以识别特定的锁定模式。
LOCK_TYPE 锁的类型
LOCK_TABLE 已锁定或包含锁定记录的表的名称
LOCK_INDEX 索引的名称,如果LOCK_TYPE是 RECORD; 否则NULL
LOCK_SPACE 锁定记录的表空间ID,如果 LOCK_TYPE是RECORD; 否则NULL``
LOCK_PAGE 锁定记录的页码,如果 LOCK_TYPE是RECORD; 否则NULL``。
LOCK_REC 页面内锁定记录的堆号,如果 LOCK_TYPE是RECORD; 否则NULL``。
LOCK_DATA 与锁相关的数据(如果有)。如果 LOCK_TYPE是RECORD,是锁定的记录的主键值,否则NULL。此列包含锁定行中主键列的值,格式为有效的SQL字符串。如果没有主键,LOCK_DATA则是唯一的InnoDB内部行ID号。如果对键值或范围高于索引中的最大值的间隙锁定,则LOCK_DATA 报告_supremum_ pseudo-record。当包含锁定记录的页面不在缓冲池中时(如果在保持锁定时将其分页到磁盘),InnoDB不从磁盘获取页面,以避免不必要的磁盘操作。相反, LOCK_DATA设置为 NULL``。

INNODB_LOCK_WAITS

包含每个被阻止InnoDB 事务的一个或多个行,指示它已请求的锁以及阻止该请求的任何锁。详见dev.mysql.com/doc/refman/…

列名 描述
REQUESTING_TRX_ID 请求(阻止)事务的ID。
REQUESTED_LOCK_ID 事务正在等待的锁的ID。
BLOCKING_TRX_ID 阻止事务的ID。
BLOCKING_LOCK_ID 由阻止另一个事务继续进行的事务所持有的锁的ID

引用文献

《MySQL技术内幕:InnoDB存储引擎》

何登成MySQL 加锁处理分析

Mysql加锁过程详解

数据库事务和锁(三)

针对MySQL死锁问题的思路分析

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

多种方法重构if语句

发表于 2019-07-27

1. 前言

项目开发初期阶段if/else语句一般比较简单,然后随着时间的推移和业务量的增加,if/else分之会越来越长。下面对如何重构if/else做出了详细分析。

2. 案例研究

我们经常遇到涉及很多条件的业务逻辑,并且每个都需要不同的处理,我们以Calculator类作为演示样例。有一个方法,它接受两个数字和一个运算符作为输入项,并根据操作返回相应结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
复制代码public int calculate(int a, int b, String operator) {
int result = Integer.MIN_VALUE;

if ("add".equals(operator)) {
result = a + b;
} else if ("multiply".equals(operator)) {
result = a * b;
} else if ("divide".equals(operator)) {
result = a / b;
} else if ("subtract".equals(operator)) {
result = a - b;
}
return result;
}

我们也可以使用switch语句来实现它:

1
2
3
4
5
6
7
8
9
复制代码public int calculateUsingSwitch(int a, int b, String operator) {
switch (operator) {
case "add":
result = a + b;
break;
// other cases
}
return result;
}

在真正的项目开发中,if语句可能会变得更大,更复杂,这时候switch语句并不是很适合,下面将介绍更好模式来解决if/else问题。

  1. 重构

3.1 工厂类

if/else每个分支中执行类似的操作,我们通过工厂方法返回给指定类型的对象并基于具体对象行为执行操作。

让我们定义一个具有单个apply方法的Operation接口:

1
2
3
复制代码public interface Operation {
int apply(int a, int b);
}

该方法将两个数字作为输入并返回结果。让我们定义一个用于执行添加的类:

1
2
3
4
5
6
复制代码public class Addition implements Operation {
@Override
public int apply(int a, int b) {
return a + b;
}
}

我们现在将实现一个工厂类,它根据给定的运算符返回Operation的实例:

1
2
3
4
5
6
7
8
9
10
11
12
复制代码public class OperatorFactory {
static Map<String, Operation> operationMap = new HashMap<>();
static {
operationMap.put("add", new Addition());
operationMap.put("divide", new Division());
// more operators
}

public static Optional<Operation> getOperation(String operator) {
return Optional.ofNullable(operationMap.get(operator));
}
}

现在,在Calculator类中,我们可以查询工厂以获取相关操作:

1
2
3
4
5
6
复制代码public int calculateUsingFactory(int a, int b, String operator) {
Operation targetOperation = OperatorFactory
.getOperation(operator)
.orElseThrow(() -> new IllegalArgumentException("Invalid Operator"));
return targetOperation.apply(a, b);
}

在这个例子中,我们已经看到了如何将责任委托给工厂类提供的松散耦合对象,但是有可能嵌套的if语句只是转移到了工厂类,我们可以在Map中维护一个对象存储库,可以查询该存储库以进快速查找,所以设计了OperatorFactory中的operationMap对象。

3.2 使用枚举

除了使用Map之外,我们还可以使用Enum来标记特定的业务逻辑。之后,我们可以在嵌套的if语句或switch case语句中使用它们。或者,我们也可以将它们用作对象的工厂并制定策略以执行相关的业务逻辑。这样可以减少嵌套if语句的数量并委托给单个Enum值。

让我们看看我们如何实现它。首先,我们需要定义我们的枚举:

1
2
3
复制代码public enum Operator {
ADD, MULTIPLY, SUBTRACT, DIVIDE
}

我们可以选择在嵌套的if语句或switch case中使用枚举值作为不同的条件,下面将介绍一种将逻辑委托给Enum本身的替代方法。

首先为每个Enum值定义方法并进行计算。例如:

1
2
3
4
5
6
7
8
9
复制代码ADD {
@Override
public int apply(int a, int b) {
return a + b;
}
},
// other operators

public abstract int apply(int a, int b);

然后在Calculator类中,我们可以定义一个可执行操作的方法:

1
2
3
复制代码public int calculate(int a, int b, Operator operator) {
return operator.apply(a, b);
}

现在,我们可以通过使用Operator.valueOf()方法将String值转换为Operator来调用该方法:

1
2
3
4
5
6
复制代码@Test
public void whenCalculateUsingEnumOperator_thenReturnCorrectResult() {
Calculator calculator = new Calculator();
int result = calculator.calculate(3, 4, Operator.valueOf("ADD"));
assertEquals(7, result);
}

3.3 命令模式

命令模式是解决嵌套if语句的另一种方法,我们可以设计一个Calculator#calculate方法来接受可执行的命令。

我们首先定义我们的Command接口:

1
2
3
复制代码public interface Command {
Integer execute();
}

接下来,让我们实现一个AddCommand:

1
2
3
4
5
6
7
8
9
10
11
12
13
复制代码public class AddCommand implements Command {
// Instance variables

public AddCommand(int a, int b) {
this.a = a;
this.b = b;
}

@Override
public Integer execute() {
return a + b;
}
}

最后,让我们在Calculator中引入一个接受并执行Command的新方法:

1
2
3
复制代码public int calculate(Command command) {
return command.execute();
}

接下来,我们可以通过实例化AddCommand调用计算并将其发送到Calculator#calculate方法:

1
2
3
4
5
6
复制代码@Test
public void whenCalculateUsingCommand_thenReturnCorrectResult() {
Calculator calculator = new Calculator();
int result = calculator.calculate(new AddCommand(3, 7));
assertEquals(10, result);
}

3.4 规则引擎

当我们最终编写大量嵌套if语句时,每个条件都描述了一个业务规则,必须对其进行评估才能处理正确的逻辑。规则引擎从主代码中获取了这种复杂性。

让我们通过设计一个简单的RuleEngine来进行演示,该RuleEngine通过一组规则来处理Expression,并返回所选规则的结果。首先,我们将定义一个Rule接口:

1
2
3
4
复制代码public interface Rule {
boolean evaluate(Expression expression);
Result getResult();
}

其次,让我们实现一个RuleEngine:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
复制代码public class RuleEngine {
private static List<Rule> rules = new ArrayList<>();

static {
rules.add(new AddRule());
}

public Result process(Expression expression) {
Rule rule = rules
.stream()
.filter(r -> r.evaluate(expression))
.findFirst()
.orElseThrow(() -> new IllegalArgumentException("Expression does not matches any Rule"));
return rule.getResult();
}
}

所述RuleEngine接受一个Expression对象并返回结果。现在,让我们将Expression类设计为一组包含两个Integer对象的Operator:

1
2
3
4
5
复制代码public class Expression {
private Integer x;
private Integer y;
private Operator operator;
}

最后让我们定义一个自定义的AddRule类,该类仅在指定ADD操作时进行求值:

1
2
3
4
5
6
7
8
9
10
11
复制代码public class AddRule implements Rule {
@Override
public boolean evaluate(Expression expression) {
boolean evalResult = false;
if (expression.getOperator() == Operator.ADD) {
this.result = expression.getX() + expression.getY();
evalResult = true;
}
return evalResult;
}
}

我们现在将使用Expression调用RuleEngine:

1
2
3
4
5
6
7
8
9
复制代码@Test
public void whenNumbersGivenToRuleEngine_thenReturnCorrectResult() {
Expression expression = new Expression(5, 5, Operator.ADD);
RuleEngine engine = new RuleEngine();
Result result = engine.process(expression);

assertNotNull(result);
assertEquals(10, result.getValue());
}

4 结论

通过上述方法我们重构if/else语句不会在仅仅局限于switch/casef方式,也探索了用不同的方法来简化复杂的代码和通过使用有效的设计模式来替换嵌套的if语句。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Java秒杀系统实战系列~整合RabbitMQ实现消息异步发

发表于 2019-07-26

摘要:

本篇博文是“Java秒杀系统实战系列文章”的第八篇,在这篇文章中我们将整合消息中间件RabbitMQ,包括添加依赖、加入配置信息以及自定义注入相关操作组件,比如RabbitTemplate等等,最终初步实现消息的发送和接收,并在下一篇章将其与邮件服务整合,实现“用户秒杀成功发送邮件通知消息”的功能!

内容:

对于消息中间件RabbitMQ,想必各位小伙伴没有用过、也该有听过,它是一款目前市面上应用相当广泛的消息中间件,可以实现消息异步通信、业务服务模块解耦、接口限流、消息分发等功能,在微服务、分布式系统架构中可以说是充当着一名了不起的角色!(详细的介绍,Debug在这里就不赘述了,各位小伙伴可以上官网看看其更多的介绍及其典型的应用场景)!

在本篇博文中,我们将使用RabbitMQ充当消息发送的组件,将它与后面篇章介绍的“邮件服务”结合实现“用户秒杀成功后异步发送邮件通知消息,告知用户秒杀已经成功!”,下面我们一起进入代码实战吧。

(1)要使用RabbitMQ,前提得在本地开发环境或者服务器安装RabbitMQ服务,如下图所示为Debug在本地安装RabbitMQ服务成功后访问其后端控制台应用的首页:

之后我们开始将其与SpringBoot进行整合。首先需要加入其依赖,其版本号跟SpringBoot的版本一致,版本号为1.5.7.RELEASE:

1
2
3
4
5
6
复制代码<!--rabbitmq-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
<version>${spring-boot.version}</version>
</dependency>

然后需要在配置文件application.properties中加入RabbitMQ服务相关的配置,比如其服务所在的Host、端口Port等等:

1
2
3
4
5
6
7
8
9
10
复制代码#rabbitmq
spring.rabbitmq.virtual-host=/
spring.rabbitmq.host=127.0.0.1
spring.rabbitmq.port=5672
spring.rabbitmq.username=guest
spring.rabbitmq.password=guest

spring.rabbitmq.listener.simple.concurrency=5
spring.rabbitmq.listener.simple.max-concurrency=15
spring.rabbitmq.listener.simple.prefetch=10

(2)紧接着,我们借助SpringBoot天然具有的一些特性,自动注入RabbitMQ一些组件的配置,包括其“单一实例消费者”配置、“多实例消费者”配置以及用于发送消息的操作组件实例“RabbitTemplate”的配置:

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
复制代码//通用化 Rabbitmq 配置
@Configuration
public class RabbitmqConfig {
private final static Logger log = LoggerFactory.getLogger(RabbitmqConfig.class);

@Autowired
private Environment env;

@Autowired
private CachingConnectionFactory connectionFactory;

@Autowired
private SimpleRabbitListenerContainerFactoryConfigurer factoryConfigurer;

//单一消费者
@Bean(name = "singleListenerContainer")
public SimpleRabbitListenerContainerFactory listenerContainer(){
SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory();
factory.setConnectionFactory(connectionFactory);
factory.setMessageConverter(new Jackson2JsonMessageConverter());
factory.setConcurrentConsumers(1);
factory.setMaxConcurrentConsumers(1);
factory.setPrefetchCount(1);
factory.setTxSize(1);
return factory;
}

//多个消费者
@Bean(name = "multiListenerContainer")
public SimpleRabbitListenerContainerFactory multiListenerContainer(){
SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory();
factoryConfigurer.configure(factory,connectionFactory);
factory.setMessageConverter(new Jackson2JsonMessageConverter());
//确认消费模式-NONE
factory.setAcknowledgeMode(AcknowledgeMode.NONE);
factory.setConcurrentConsumers(env.getProperty("spring.rabbitmq.listener.simple.concurrency",int.class));
factory.setMaxConcurrentConsumers(env.getProperty("spring.rabbitmq.listener.simple.max-concurrency",int.class));
factory.setPrefetchCount(env.getProperty("spring.rabbitmq.listener.simple.prefetch",int.class));
return factory;
}

@Bean
public RabbitTemplate rabbitTemplate(){
connectionFactory.setPublisherConfirms(true);
connectionFactory.setPublisherReturns(true);
RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory);
rabbitTemplate.setMandatory(true);
rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() {
@Override
public void confirm(CorrelationData correlationData, boolean ack, String cause) {
log.info("消息发送成功:correlationData({}),ack({}),cause({})",correlationData,ack,cause);
}
});
rabbitTemplate.setReturnCallback(new RabbitTemplate.ReturnCallback() {
@Override
public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
log.warn("消息丢失:exchange({}),route({}),replyCode({}),replyText({}),message:{}",exchange,routingKey,replyCode,replyText,message);
}
});
return rabbitTemplate;
}
}

在RabbitMQ的消息发送组件RabbitTemplate的配置中,我们还特意加入了“消息发送确认”、“消息丢失回调”的输出配置,即当消息正确进入到队列后,即代表消息发送成功;当消息找不到对应的队列(在某种程度上,其实也就是找不到交换机和路由)时,会输出消息丢失。

(3)完了之后,我们准备开始使用RabbitMQ实现消息的发送和接收。首先,我们需要在RabbitmqConfig配置类中创建队列、交换机、路由以及绑定等Bean组件,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
复制代码//构建异步发送邮箱通知的消息模型
@Bean
public Queue successEmailQueue(){
return new Queue(env.getProperty("mq.kill.item.success.email.queue"),true);
}

@Bean
public TopicExchange successEmailExchange(){
return new TopicExchange(env.getProperty("mq.kill.item.success.email.exchange"),true,false);
}

@Bean
public Binding successEmailBinding(){
return BindingBuilder.bind(successEmailQueue()).to(successEmailExchange()).with(env.getProperty("mq.kill.item.success.email.routing.key"));
}

其中,环境变量实例env读取的那些属性我们是配置在application.properties文件中的,如下所示:

1
2
3
4
5
6
复制代码mq.env=test

#秒杀成功异步发送邮件的消息模型
mq.kill.item.success.email.queue=${mq.env}.kill.item.success.email.queue
mq.kill.item.success.email.exchange=${mq.env}.kill.item.success.email.exchange
mq.kill.item.success.email.routing.key=${mq.env}.kill.item.success.email.routing.key

紧接着,我们需要在通用的消息发送服务类 RabbitSenderService 中写一段发送消息的方法,该方法用于接收“订单编号”参数,然后在数据库中查询其对应的详细订单记录,将该记录充当“消息”并发送至RabbitMQ的队列中,等待被监听消费:

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
复制代码/**
* RabbitMQ通用的消息发送服务
* @Author:debug (SteadyJack)
* @Date: 2019/6/21 21:47
**/
@Service
public class RabbitSenderService {

public static final Logger log= LoggerFactory.getLogger(RabbitSenderService.class);

@Autowired
private RabbitTemplate rabbitTemplate;

@Autowired
private Environment env;

@Autowired
private ItemKillSuccessMapper itemKillSuccessMapper;

//秒杀成功异步发送邮件通知消息
public void sendKillSuccessEmailMsg(String orderNo){
log.info("秒杀成功异步发送邮件通知消息-准备发送消息:{}",orderNo);

try {
if (StringUtils.isNotBlank(orderNo)){
KillSuccessUserInfo info=itemKillSuccessMapper.selectByCode(orderNo);
if (info!=null){
//TODO:rabbitmq发送消息的逻辑
rabbitTemplate.setMessageConverter(new Jackson2JsonMessageConverter());
rabbitTemplate.setExchange(env.getProperty("mq.kill.item.success.email.exchange"));
rabbitTemplate.setRoutingKey(env.getProperty("mq.kill.item.success.email.routing.key"));

//TODO:将info充当消息发送至队列
rabbitTemplate.convertAndSend(info, new MessagePostProcessor() {
@Override
public Message postProcessMessage(Message message) throws AmqpException {
MessageProperties messageProperties=message.getMessageProperties();
messageProperties.setDeliveryMode(MessageDeliveryMode.PERSISTENT);
messageProperties.setHeader(AbstractJavaTypeMapper.DEFAULT_CONTENT_CLASSID_FIELD_NAME,KillSuccessUserInfo.class);
return message;
}
});
}
}
}catch (Exception e){
log.error("秒杀成功异步发送邮件通知消息-发生异常,消息为:{}",orderNo,e.fillInStackTrace());
}
}
}

(4)最后,是在通用的消息接收服务类RabbitReceiverService中实现消息的接收,其完整的源代码如下所示:

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
复制代码/**
* RabbitMQ通用的消息接收服务
* @Author:debug (SteadyJack)
* @Date: 2019/6/21 21:47
**/
@Service
public class RabbitReceiverService {

public static final Logger log= LoggerFactory.getLogger(RabbitReceiverService.class);

@Autowired
private MailService mailService;

@Autowired
private Environment env;

@Autowired
private ItemKillSuccessMapper itemKillSuccessMapper;

//秒杀异步邮件通知-接收消息
@RabbitListener(queues = {"${mq.kill.item.success.email.queue}"},containerFactory = "singleListenerContainer")
public void consumeEmailMsg(KillSuccessUserInfo info){
try {
log.info("秒杀异步邮件通知-接收消息:{}",info);
//到时候这里将整合邮件服务发送邮件通知消息的逻辑

}catch (Exception e){
log.error("秒杀异步邮件通知-接收消息-发生异常:",e.fillInStackTrace());
}
}
}

至此,关于SpringBoot整合消息中间件RabbitMQ的代码实战,本篇文章就介绍到这里了。

最后一点,我们需要进行测试,即用户在界面发起“抢购”的请求操作之后,如果能秒杀成功,则RabbitMQ会发送、接收一条消息,如下所示:

好了,关于RabbitMQ的使用,本文到此就暂且告一段落了,在下一篇文章中我们将把它与邮件服务进行整合,实现“用户秒杀成功后异步发送邮件通知消息给到用户邮箱”的功能!除此之外,我们还将在后面的篇章介绍“如何使用RabbitMQ的死信队列,处理用户下单成功后却超时未支付的订单~在那里我们将采取失效的操作”。

补充:

1、目前,这一秒杀系统的整体构建与代码实战已经全部完成了,完整的源代码数据库地址可以来这里下载:gitee.com/steadyjack/… 记得Fork跟Star啊!!

2、最后,不要忘记了关注一下Debug的技术微信公众号:

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

商品模块数据库表解析(二)

发表于 2019-07-25

SpringBoot实战电商项目mall(18k+star)地址:github.com/macrozheng/…

简介

接上一篇文章,本文主要对编辑商品、商品评价及回复、商品操作记录这三块功能的表进行解析,采用数据库表与功能对照的形式。

编辑商品

相关表结构

商品表

商品信息主要包括四部分:商品的基本信息、商品的促销信息、商品的属性信息、商品的关联,商品表是整个商品的基本信息部分。

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
复制代码create table pms_product
(
id bigint not null auto_increment,
brand_id bigint comment '品牌id',
product_category_id bigint comment '品牌分类id',
feight_template_id bigint comment '运费模版id',
product_attribute_category_id bigint comment '品牌属性分类id',
name varchar(64) not null comment '商品名称',
pic varchar(255) comment '图片',
product_sn varchar(64) not null comment '货号',
delete_status int(1) comment '删除状态:0->未删除;1->已删除',
publish_status int(1) comment '上架状态:0->下架;1->上架',
new_status int(1) comment '新品状态:0->不是新品;1->新品',
recommand_status int(1) comment '推荐状态;0->不推荐;1->推荐',
verify_status int(1) comment '审核状态:0->未审核;1->审核通过',
sort int comment '排序',
sale int comment '销量',
price decimal(10,2) comment '价格',
promotion_price decimal(10,2) comment '促销价格',
gift_growth int default 0 comment '赠送的成长值',
gift_point int default 0 comment '赠送的积分',
use_point_limit int comment '限制使用的积分数',
sub_title varchar(255) comment '副标题',
description text comment '商品描述',
original_price decimal(10,2) comment '市场价',
stock int comment '库存',
low_stock int comment '库存预警值',
unit varchar(16) comment '单位',
weight decimal(10,2) comment '商品重量,默认为克',
preview_status int(1) comment '是否为预告商品:0->不是;1->是',
service_ids varchar(64) comment '以逗号分割的产品服务:1->无忧退货;2->快速退款;3->免费包邮',
keywords varchar(255) comment '关键字',
note varchar(255) comment '备注',
album_pics varchar(255) comment '画册图片,连产品图片限制为5张,以逗号分割',
detail_title varchar(255) comment '详情标题',
detail_desc text comment '详情描述',
detail_html text comment '产品详情网页内容',
detail_mobile_html text comment '移动端网页详情',
promotion_start_time datetime comment '促销开始时间',
promotion_end_time datetime comment '促销结束时间',
promotion_per_limit int comment '活动限购数量',
promotion_type int(1) comment '促销类型:0->没有促销使用原价;1->使用促销价;2->使用会员价;3->使用阶梯价格;4->使用满减价格;5->限时购',
product_category_name varchar(255) comment '产品分类名称',
brand_name varchar(255) comment '品牌名称',
primary key (id)
);

商品SKU表

SKU(Stock Keeping Unit)是指库存量单位,SPU(Standard Product Unit)是指标准产品单位。举个例子:iphone xs是一个SPU,而iphone xs 公开版 64G 银色是一个SKU。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
复制代码create table pms_sku_stock
(
id bigint not null auto_increment,
product_id bigint comment '商品id',
sku_code varchar(64) not null comment 'sku编码',
price decimal(10,2) comment '价格',
stock int default 0 comment '库存',
low_stock int comment '预警库存',
sp1 varchar(64) comment '规格属性1',
sp2 varchar(64) comment '规格属性2',
sp3 varchar(64) comment '规格属性3',
pic varchar(255) comment '展示图片',
sale int comment '销量',
promotion_price decimal(10,2) comment '单品促销价格',
lock_stock int default 0 comment '锁定库存',
primary key (id)
);

商品阶梯价格表

商品优惠相关表,购买同商品满足一定数量后,可以使用打折价格进行购买。如:买两件商品可以打八折。

1
2
3
4
5
6
7
8
9
复制代码create table pms_product_ladder
(
id bigint not null auto_increment,
product_id bigint comment '商品id',
count int comment '满足的商品数量',
discount decimal(10,2) comment '折扣',
price decimal(10,2) comment '折后价格',
primary key (id)
);

商品满减表

商品优惠相关表,购买同商品满足一定金额后,可以减免一定金额。如:买满1000减100元。

1
2
3
4
5
6
7
8
复制代码create table pms_product_full_reduction
(
id bigint not null auto_increment,
product_id bigint comment '商品id',
full_price decimal(10,2) comment '商品满足金额',
reduce_price decimal(10,2) comment '商品减少金额',
primary key (id)
);

商品会员价格表

根据不同会员等级,可以以不同的会员价格购买。此处设计有缺陷,可以做成不同会员等级可以减免多少元或者按多少折扣进行购买。

1
2
3
4
5
6
7
8
9
复制代码create table pms_member_price
(
id bigint not null auto_increment,
product_id bigint comment '商品id',
member_level_id bigint comment '会员等级id',
member_price decimal(10,2) comment '会员价格',
member_level_name varchar(100) comment '会员等级名称',
primary key (id)
);

管理端展现

填写商品信息

展示图片

填写商品促销

展示图片

特惠促销

展示图片

会员价格

展示图片

阶梯价格

展示图片

满减价格

展示图片

填写商品属性

展示图片

展示图片

展示图片

选择商品关联

展示图片

移动端展现

商品介绍

展示图片

图文详情

展示图片

相关专题

展示图片

商品评价及回复

相关表结构

商品评价表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
复制代码create table pms_comment
(
id bigint not null auto_increment,
product_id bigint comment '商品id',
member_nick_name varchar(255) comment '会员昵称',
product_name varchar(255) comment '商品名称',
star int(3) comment '评价星数:0->5',
member_ip varchar(64) comment '评价的ip',
create_time datetime comment '创建时间',
show_status int(1) comment '是否显示',
product_attribute varchar(255) comment '购买时的商品属性',
collect_couont int comment '收藏数',
read_count int comment '阅读数',
content text comment '内容',
pics varchar(1000) comment '上传图片地址,以逗号隔开',
member_icon varchar(255) comment '评论用户头像',
replay_count int comment '回复数',
primary key (id)
);

产品评价回复表

1
2
3
4
5
6
7
8
9
10
11
复制代码create table pms_comment_replay
(
id bigint not null auto_increment,
comment_id bigint comment '评论id',
member_nick_name varchar(255) comment '会员昵称',
member_icon varchar(255) comment '会员头像',
content varchar(1000) comment '内容',
create_time datetime comment '创建时间',
type int(1) comment '评论人员类型;0->会员;1->管理员',
primary key (id)
);

移动端展现

商品评价列表

展示图片

商品评价详情

展示图片

商品回复列表

展示图片

商品审核及操作记录

相关表结构

商品审核记录表

用于记录商品审核记录

1
2
3
4
5
6
7
8
9
10
复制代码create table pms_product_vertify_record
(
id bigint not null auto_increment,
product_id bigint comment '商品id',
create_time datetime comment '创建时间',
vertify_man varchar(64) comment '审核人',
status int(1) comment '审核后的状态:0->未通过;2->已通过',
detail varchar(255) comment '反馈详情',
primary key (id)
);

商品操作记录表

用于记录商品操作记录

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
复制代码create table pms_product_operate_log
(
id bigint not null auto_increment,
product_id bigint comment '商品id',
price_old decimal(10,2) comment '改变前价格',
price_new decimal(10,2) comment '改变后价格',
sale_price_old decimal(10,2) comment '改变前优惠价',
sale_price_new decimal(10,2) comment '改变后优惠价',
gift_point_old int comment '改变前积分',
gift_point_new int comment '改变后积分',
use_point_limit_old int comment '改变前积分使用限制',
use_point_limit_new int comment '改变后积分使用限制',
operate_man varchar(64) comment '操作人',
create_time datetime comment '创建时间',
primary key (id)
);

公众号

mall项目全套学习教程连载中,关注公众号第一时间获取。

公众号图片

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

手把手带你入门 Spring Security! 1项目创

发表于 2019-07-25

Spring Security 是 Spring 家族中的一个安全管理框架,实际上,在 Spring Boot 出现之前,Spring Security 就已经发展了多年了,但是使用的并不多,安全管理这个领域,一直是 Shiro 的天下。

相对于 Shiro,在 SSM/SSH 中整合 Spring Security 都是比较麻烦的操作,所以,Spring Security 虽然功能比 Shiro 强大,但是使用反而没有 Shiro 多(Shiro 虽然功能没有 Spring Security 多,但是对于大部分项目而言,Shiro 也够用了)。

自从有了 Spring Boot 之后,Spring Boot 对于 Spring Security 提供了 自动化配置方案,可以零配置使用 Spring Security。

因此,一般来说,常见的安全管理技术栈的组合是这样的:

  • SSM + Shiro
  • Spring Boot/Spring Cloud + Spring Security

注意,这只是一个推荐的组合而已,如果单纯从技术上来说,无论怎么组合,都是可以运行的。

我们来看下具体使用。

1.项目创建

在 Spring Boot 中使用 Spring Security 非常容易,引入依赖即可:

pom.xml 中的 Spring Security 依赖:

1
2
3
4
复制代码<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>

只要加入依赖,项目的所有接口都会被自动保护起来。

2.初次体验

我们创建一个 HelloController:

1
2
3
4
5
6
7
复制代码@RestController
public class HelloController {
@GetMapping("/hello")
public String hello() {
return "hello";
}
}

访问 /hello ,需要登录之后才能访问。

当用户从浏览器发送请求访问 /hello 接口时,服务端会返回 302 响应码,让客户端重定向到 /login 页面,用户在 /login 页面登录,登陆成功之后,就会自动跳转到 /hello 接口。

另外,也可以使用 POSTMAN 来发送请求,使用 POSTMAN 发送请求时,可以将用户信息放在请求头中(这样可以避免重定向到登录页面):

通过以上两种不同的登录方式,可以看出,Spring Security 支持两种不同的认证方式:

  • 可以通过 form 表单来认证
  • 可以通过 HttpBasic 来认证

3.用户名配置

默认情况下,登录的用户名是 user ,密码则是项目启动时随机生成的字符串,可以从启动的控制台日志中看到默认密码:

这个随机生成的密码,每次启动时都会变。对登录的用户名/密码进行配置,有三种不同的方式:

  • 在 application.properties 中进行配置
  • 通过 Java 代码配置在内存中
  • 通过 Java 从数据库中加载

前两种比较简单,第三种代码量略大,本文就先来看看前两种,第三种后面再单独写文章介绍,也可以参考我的微人事项目。

3.1 配置文件配置用户名/密码

可以直接在 application.properties 文件中配置用户的基本信息:

1
2
复制代码spring.security.user.name=javaboy
spring.security.user.password=123

配置完成后,重启项目,就可以使用这里配置的用户名/密码登录了。

3.2 Java 配置用户名/密码

也可以在 Java 代码中配置用户名密码,首先需要我们创建一个 Spring Security 的配置类,集成自 WebSecurityConfigurerAdapter 类,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
复制代码@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//下面这两行配置表示在内存中配置了两个用户
auth.inMemoryAuthentication()
.withUser("javaboy").roles("admin").password("$2a$10$OR3VSksVAmCzc.7WeaRPR.t0wyCsIj24k0Bne8iKWV1o.V9wsP8Xe")
.and()
.withUser("lisi").roles("user").password("$2a$10$p1H8iWa8I4.CA.7Z8bwLjes91ZpY.rYREGHQEInNtAp4NzL6PLKxi");
}
@Bean
PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}

这里我们在 configure 方法中配置了两个用户,用户的密码都是加密之后的字符串(明文是 123),从 Spring5 开始,强制要求密码要加密,如果非不想加密,可以使用一个过期的 PasswordEncoder 的实例 NoOpPasswordEncoder,但是不建议这么做,毕竟不安全。

Spring Security 中提供了 BCryptPasswordEncoder 密码编码工具,可以非常方便的实现密码的加密加盐,相同明文加密出来的结果总是不同,这样就不需要用户去额外保存盐的字段了,这一点比 Shiro 要方便很多。

4.登录配置

对于登录接口,登录成功后的响应,登录失败后的响应,我们都可以在 WebSecurityConfigurerAdapter 的实现类中进行配置。例如下面这样:

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
复制代码@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
VerifyCodeFilter verifyCodeFilter;
@Override
protected void configure(HttpSecurity http) throws Exception {
http.addFilterBefore(verifyCodeFilter, UsernamePasswordAuthenticationFilter.class);
http
.authorizeRequests()//开启登录配置
.antMatchers("/hello").hasRole("admin")//表示访问 /hello 这个接口,需要具备 admin 这个角色
.anyRequest().authenticated()//表示剩余的其他接口,登录之后就能访问
.and()
.formLogin()
//定义登录页面,未登录时,访问一个需要登录之后才能访问的接口,会自动跳转到该页面
.loginPage("/login_p")
//登录处理接口
.loginProcessingUrl("/doLogin")
//定义登录时,用户名的 key,默认为 username
.usernameParameter("uname")
//定义登录时,用户密码的 key,默认为 password
.passwordParameter("passwd")
//登录成功的处理器
.successHandler(new AuthenticationSuccessHandler() {
@Override
public void onAuthenticationSuccess(HttpServletRequest req, HttpServletResponse resp, Authentication authentication) throws IOException, ServletException {
resp.setContentType("application/json;charset=utf-8");
PrintWriter out = resp.getWriter();
out.write("success");
out.flush();
}
})
.failureHandler(new AuthenticationFailureHandler() {
@Override
public void onAuthenticationFailure(HttpServletRequest req, HttpServletResponse resp, AuthenticationException exception) throws IOException, ServletException {
resp.setContentType("application/json;charset=utf-8");
PrintWriter out = resp.getWriter();
out.write("fail");
out.flush();
}
})
.permitAll()//和表单登录相关的接口统统都直接通过
.and()
.logout()
.logoutUrl("/logout")
.logoutSuccessHandler(new LogoutSuccessHandler() {
@Override
public void onLogoutSuccess(HttpServletRequest req, HttpServletResponse resp, Authentication authentication) throws IOException, ServletException {
resp.setContentType("application/json;charset=utf-8");
PrintWriter out = resp.getWriter();
out.write("logout success");
out.flush();
}
})
.permitAll()
.and()
.httpBasic()
.and()
.csrf().disable();
}
}

我们可以在 successHandler 方法中,配置登录成功的回调,如果是前后端分离开发的话,登录成功后返回 JSON 即可,同理,failureHandler 方法中配置登录失败的回调,logoutSuccessHandler 中则配置注销成功的回调。

5.忽略拦截

如果某一个请求地址不需要拦截的话,有两种方式实现:

  • 设置该地址匿名访问
  • 直接过滤掉该地址,即该地址不走 Spring Security 过滤器链

推荐使用第二种方案,配置如下:

1
2
3
4
5
6
7
复制代码@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/vercode");
}
}

Spring Security 另外一个强大之处就是它可以结合 OAuth2 ,玩出更多的花样出来,这些我们在后面的文章中再和大家细细介绍。

本文就先说到这里,有问题欢迎留言讨论。

关注公众号【江南一点雨】,专注于 Spring Boot+微服务以及前后端分离等全栈技术,定期视频教程分享,关注后回复 Java ,领取松哥为你精心准备的 Java 干货!

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

面试必备:Java AQS 实现原理(图文)分析【精品长文】

发表于 2019-07-23

愿我所遇之人,所历之事,哪怕因为我有一点点变好,我就心满意足了。

AQS:AbstractQueuedSynchronizer

1、AQS设计简介

  • AQS的实现是基于一个FIFO的等待队列。
  • 使用单个原子变量来表示获取、释放锁状态(final int)改变该int值使用的是CAS。(思考:为什么一个int值可以保证内存可见性?)
  • 子类应该定义一个非公开的内部类继承AQS,并实现其中方法。
  • AQS支持exclusive与shared两种模式。
  • 内部类ConditionObject用于支持子类实现exclusive模式
  • 子类需要重写:
    • tryAcquire
    • tryRelease
    • tryReleaseShared
    • isHeldExclusively等方法,并确保是线程安全的。

贯穿全文的图(核心):

模板方法设计模式:定义一个操作中算法的骨架,而将一些步骤的实现延迟到子类中。

2、类结构

  • ConditionObject类
  • Node类
  • N多方法

3、FIFO队列

等待队列是CLH(Craig, Landin, and Hagersten)锁队列。

通过节点中的“状态”字段来判断一个线程是否应该阻塞。当该节点的前一个节点释放锁的时候,该节点会被唤醒。

1
2
3
4
5
复制代码private transient volatile Node head;
private transient volatile Node tail;
//The synchronization state.
//在互斥锁中它表示着线程是否已经获取了锁,0未获取,1已经获取了,大于1表示重入数。
private volatile int state;

AQS维护了一个volatile int state(代表共享资源)和一个FIFO线程等待队列(多线程争用资源被阻塞时会进入此队列)。

state的访问方式有三种:

  • getState()
  • setState()
  • compareAndSetState()

AQS定义两种资源共享方式:Exclusive(独占,只有一个线程能执行,如ReentrantLock)和Share(共享,多个线程可同时执行,如Semaphore/CountDownLatch)。

不同的自定义同步器争用共享资源的方式也不同。自定义同步器在实现时只需要实现共享资源state的获取与释放方式即可,至于具体线程等待队列的维护(如获取资源失败入队/唤醒出队等),AQS已经在顶层实现好了。

自定义同步器实现时主要实现以下几种方法:

  • isHeldExclusively():该线程是否正在独占资源。只有用到condition才需要去实现它。
  • tryAcquire(int):独占方式。尝试获取资源,成功则返回true,失败则返回false。
  • tryRelease(int):独占方式。尝试释放资源,成功则返回true,失败则返回false。
  • tryAcquireShared(int):共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
  • tryReleaseShared(int):共享方式。尝试释放资源,如果释放后允许唤醒后续等待结点返回true,否则返回false。

以ReentrantLock为例,state初始化为0,表示未锁定状态。A线程lock()时,会调用tryAcquire()独占该锁并将state+1。此后,其他线程再tryAcquire()时就会失败,直到A线程unlock()到state=0(即释放锁)为止,其它线程才有机会获取该锁。当然,释放锁之前,A线程自己是可以重复获取此锁的(state会累加),这就是可重入的概念。但要注意,获取多少次就要释放多么次,这样才能保证state是能回到零态的。

再以CountDownLatch以例,任务分为N个子线程去执行,state也初始化为N(注意N要与线程个数一致)。这N个子线程是并行执行的,每个子线程执行完后countDown()一次,state会CAS减1。等到所有子线程都执行完后(即state=0),会unpark()主调用线程,然后主调用线程就会从await()函数返回,继续后续动作。

一般来说,自定义同步器要么是独占方法,要么是共享方式,他们也只需实现:
tryAcquire-tryRelease
tryAcquireShared-tryReleaseShared
中的一种即可。

当然AQS也支持自定义同步器同时实现独占和共享两种方式,如ReentrantReadWriteLock。

以下部分来自源码注释:

每次进入CLH队列时,需要对尾节点进入队列过程,是一个原子性操作。在出队列时,我们只需要更新head节点即可。在节点确定它的后继节点时, 需要花一些功夫,用于处理那些,由于等待超时时间结束或中断等原因, 而取消等待锁的线程。

节点的前驱指针,主要用于处理,取消等待锁的线程。如果一个节点取消等待锁,则此节点的前驱节点的后继指针,要指向,此节点后继节点中,非取消等待锁的线程(有效等待锁的线程节点)。

我们用next指针连接实现阻塞机制。每个节点均持有自己线程,节点通过节点的后继连接唤醒其后继节点。

CLH队列需要一个傀儡结点作为开始节点。我们不会再构造函数中创建它,因为如果没有线程竞争锁,那么,努力就白费了。取而代之的方案是,当有第一个竞争者时,我们才构造头指针和尾指针。

线程通过同一节点等待条件,但是用另外一个连接。条件只需要放在一个非并发的连接队列与节点关联,因为只有当线程独占持有锁的时候,才会去访问条件。当一个线程等待条件的时候,节点将会插入到条件队列中。当条件触发时,节点将会转移到主队列中。用一个状态值,描述节点在哪一个队列上。

4、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
复制代码static final class Node {
//该等待节点处于共享模式
static final Node SHARED = new Node();
//该等待节点处于独占模式
static final Node EXCLUSIVE = null;

//表示节点的线程是已被取消的
static final int CANCELLED = 1;
//表示当前节点的后继节点的线程需要被唤醒
static final int SIGNAL = -1;
//表示线程正在等待某个条件
static final int CONDITION = -2;
//表示下一个共享模式的节点应该无条件的传播下去
static final int PROPAGATE = -3;

//状态位 ,分别可以使CANCELLED、SINGNAL、CONDITION、PROPAGATE、0
volatile int waitStatus;

volatile Node prev;//前驱节点
volatile Node next;//后继节点
volatile Thread thread;//等待锁的线程

//ConditionObject链表的后继节点或者代表共享模式的节点。
//因为Condition队列只能在独占模式下被能被访问,我们只需要简单的使用链表队列来链接正在等待条件的节点。
//然后它们会被转移到同步队列(AQS队列)再次重新获取。
//由于条件队列只能在独占模式下使用,所以我们要表示共享模式的节点的话只要使用特殊值SHARED来标明即可。
Node nextWaiter;
//Returns true if node is waiting in shared mode
final boolean isShared() {
return nextWaiter == SHARED;
}
.......
}

waitStatus不同值含义:

  • SIGNAL(-1):当前节点的后继节点已经 (或即将)被阻塞(通过park) , 所以当当前节点释放或则被取消时候,一定要unpark它的后继节点。为了避免竞争,获取方法一定要首先设置node为signal,然后再次重新调用获取方法,如果失败,则阻塞。
  • CANCELLED(1):当前节点由于超时或者被中断而被取消。一旦节点被取消后,那么它的状态值不在会被改变,且当前节点的线程不会再次被阻塞。
  • CONDITION(-2) :该节点的线程处于等待条件状态,不会被当作是同步队列上的节点,直到被唤醒(signal),设置其值为0,重新进入阻塞状态.
  • PROPAGATE(-3:)共享模式下的释放操作应该被传播到其他节点。该状态值在doReleaseShared方法中被设置的。
  • 0:以上都不是

该状态值为了简便使用,所以使用了数值类型。非负数值意味着该节点不需要被唤醒。所以,大多数代码中不需要检查该状态值的确定值。

一个正常的Node,它的waitStatus初始化值是0。如果想要修改这个值,可以使用AQS提供CAS进行修改。

5、独占模式与共享模式

在锁的获取时,并不一定只有一个线程才能持有这个锁(或者称为同步状态),所以此时有了独占模式和共享模式的区别,也就是在Node节点中由nextWaiter来标识。比如ReentrantLock就是一个独占锁,只能有一个线程获得锁,而WriteAndReadLock的读锁则能由多个线程同时获取,但它的写锁则只能由一个线程持有。

5.1、独占模式

5.1.1 独占模式同步状态的获取

1
2
3
4
5
6
7
8
9
复制代码//忽略中断的(即不手动抛出InterruptedException异常)独占模式下的获取方法。
//该方法在成功返回前至少会调用一次tryAcquire()方法(该方法是子类重写的方法,如果返回true则代表能成功获取).
//否则当前线程会进入队列排队,重复的阻塞和唤醒等待再次成功获取后返回,
//该方法可以用来实现Lock.lock
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}

该方法首先尝试获取锁( tryAcquire(arg)的具体实现定义在了子类中),如果获取到,则执行完毕,否则通过addWaiter(Node.EXCLUSIVE), arg)方法把当前节点添加到等待队列末尾,并设置为独占模式。

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
复制代码private Node addWaiter(Node mode) {
//把当前线程包装为node,设为独占模式
Node node = new Node(Thread.currentThread(), mode);
// 尝试快速入队,即无竞争条件下肯定成功。如果失败,则进入enq自旋重试入队
Node pred = tail;
if (pred != null) {
node.prev = pred;
//CAS替换当前尾部。成功则返回
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
enq(node);
return node;
}
//插入节点到队列中,如果队列未初始化则初始化,然后再插入。
private Node enq(final Node node) {
for (;;) {
Node t = tail;
if (t == null) { // Must initialize
if (compareAndSetHead(new Node()))
tail = head;
} else {
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}

如果tail节点为空,执行enq(node);重新尝试,最终把node插入.在把node插入队列末尾后,它并不立即挂起该节点中线程,因为在插入它的过程中,前面的线程可能已经执行完成,所以它会先进行自旋操作acquireQueued(node, arg),尝试让该线程重新获取锁!当条件满足获取到了锁则可以从自旋过程中退出,否则继续。

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
复制代码final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
//如果它的前继节点为头结点,尝试获取锁,获取成功则返回
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
//判断当前节点的线程是否应该被挂起,如果应该被挂起则挂起。
//等待release唤醒释放
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
//在队列中取消当前节点
cancelAcquire(node);
}
}

如果没获取到锁,则判断是否应该挂起,而这个判断则得通过它的前驱节点的waitStatus来确定:

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
复制代码private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
//该节点如果状态如果为SIGNAL。则返回true,然后park挂起线程
if (ws == Node.SIGNAL)
return true;
//表明该节点已经被取消,向前循环重新调整链表节点
if (ws > 0) {
/*
* Predecessor was cancelled. Skip over predecessors and
* indicate retry.
*/
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
//执行到这里代表节点是0或者PROPAGATE,然后标记他们为SIGNAL,但是
//还不能park挂起线程。需要重试是否能获取,如果不能,则挂起。
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}

//挂起当前线程,且返回线程的中断状态
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}

最后,我们对获取独占式锁过程对做个总结:

AQS的模板方法acquire通过调用子类自定义实现的tryAcquire获取同步状态失败后->将线程构造成Node节点(addWaiter)->将Node节点添加到同步队列对尾(addWaiter)->节点以自旋的方法获取同步状态(acquirQueued)。在节点自旋获取同步状态时,只有其前驱节点是头节点的时候才会尝试获取同步状态,如果该节点的前驱不是头节点或者该节点的前驱节点是头节点单获取同步状态失败,则判断当前线程需要阻塞,如果需要阻塞则需要被唤醒过后才返回。

获取锁的过程:

  • 当线程调用acquire()申请获取锁资源,如果成功,则进入临界区。
  • 当获取锁失败时,则进入一个FIFO等待队列,然后被挂起等待唤醒。
  • 当队列中的等待线程被唤醒以后就重新尝试获取锁资源,如果成功则进入临界区,否则继续挂起等待。

5.1.2 独占模式同步状态的释放

既然是释放,那肯定是持有锁的该线程执行释放操作,即head节点中的线程释放锁.

AQS中的release释放同步状态和acquire获取同步状态一样,都是模板方法,tryRelease释放的具体操作都有子类去实现,父类AQS只提供一个算法骨架。

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
复制代码public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
//如果node的后继节点不为空且不是作废状态,则唤醒这个后继节点,
//否则从末尾开始寻找合适的节点,如果找到,则唤醒
private void unparkSuccessor(Node node) {
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
Node s = node.next;
if (s == null || s.waitStatus > 0) {
s = null;
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
if (s != null)
LockSupport.unpark(s.thread);
}

过程:首先调用子类的tryRelease()方法释放锁,然后唤醒后继节点,在唤醒的过程中,需要判断后继节点是否满足情况,如果后继节点不为空且不是作废状态,则唤醒这个后继节点,否则从tail节点向前寻找合适的节点,如果找到,则唤醒。

释放锁过程:

  • 当线程调用release()进行锁资源释放时,如果没有其他线程在等待锁资源,则释放完成。
  • 如果队列中有其他等待锁资源的线程需要唤醒,则唤醒队列中的第一个等待节点(先入先出)。

5.2、共享模式

5.2.1 共享模式同步状态的获取

  • 当线程调用acquireShared()申请获取锁资源时,如果成功,则进入临界区。
  • 当获取锁失败时,则创建一个共享类型的节点并进入一个FIFO等待队列,然后被挂起等待唤醒。
  • 当队列中的等待线程被唤醒以后就重新尝试获取锁资源,如果成功则唤醒后面还在等待的共享节点并把该唤醒事件传递下去,即会依次唤醒在该节点后面的所有共享节点,然后进入临界区,否则继续挂起等待。

5.2.2 共享模式同步状态的释放

  • 当线程调用releaseShared()进行锁资源释放时,如果释放成功,则唤醒队列中等待的节点,如果有的话。
  1. AQS小结

java.util.concurrent中的很多可阻塞类(比如ReentrantLock)都是基于AQS来实现的。AQS是一个同步框架,它提供通用机制来原子性管理同步状态、阻塞和唤醒线程,以及维护被阻塞线程的队列。

JDK中AQS被广泛使用,基于AQS实现的同步器包括:

  • ReentrantLock
  • Semaphore
  • ReentrantReadWriteLock(后续会出文章讲解)
  • CountDownLatch
  • FutureTask

每一个基于AQS实现的同步器都会包含两种类型的操作,如下:

  • 至少一个acquire操作。这个操作阻塞调用线程,除非/直到AQS的状态允许这个线程继续执行。
  • 至少一个release操作。这个操作改变AQS的状态,改变后的状态可允许一个或多个阻塞线程被解除阻塞。

基于“复合优先于继承”的原则,基于AQS实现的同步器一般都是:声明一个内部私有的继承于AQS的子类Sync,对同步器所有公有方法的调用都会委托给这个内部子类。

7.后续

后面会推出以下有关AQS的文章,已加深对于AQS的理解

  • AQS ConditionObject对象解析
  • AQS 应用案例 ReentrantReadWriteLock解析
  • Java volatile的内存语义与AQS锁内存可见性

8.感谢

本文很多内容整理自网络,参考文献:
segmentfault.com/a/119000001…
segmentfault.com/a/119000001…
zhuanlan.zhihu.com/p/27134110
blog.csdn.net/wojiaolinaa…
www.cnblogs.com/waterystone…

FIFO队列:www.cnblogs.com/waterystone…

9、博主信息

个人微信公众号:

个人博客

个人github

个人掘金博客

个人CSDN博客

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

1…863864865…956

开发者博客

9558 日志
1953 标签
RSS
© 2025 开发者博客
本站总访问量次
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4
0%