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

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


  • 首页

  • 归档

  • 搜索

Spring中的RestController注解

发表于 2021-09-23

@RestController注解

在Spring中@RestController的作用等同于@Controller + @ResponseBody。
所以想要理解@RestController注解就要先了解@Controller和@ResponseBody注解。

@Controller注解

在一个类上添加@Controller注解,表明了这个类是一个控制器类。但想要让这个类成为一个处理请求的处理器光有@Controller注解是不够的,他还需要进一步修炼才能成为一个处理器。

1)在spring容器中创建该类的实例。创建实例的方式有两种:

1
xml复制代码<bean class = "test.controller.MyController"/>

上述这种方式是在spring容器中注入单个bean,当项目比较大,控制器类比较多时,用这种方式向Spring容器中注入bean非常的让人苦恼,索性有第二种方式。

1
xml复制代码<context:component-scan base-scan="test.controller"/>

这种方式会扫描指定包中的所有类,并生成相应的bean注入到spring容器中。使用这种方式当然能够极大提高我们的开发效率,但是有时候我们不想某一类型的类注入到spring容器中。
这个时候第二种方式也可以解决。

1
2
3
xml复制代码<context:component-scan base-package="test">
<context:include-filter type="annotation" expression="org.springframework.stereotype.Service"/>
</context:component-scan>

上述代码表示扫描test包中除有@Service注解之外的类。

2)将@Controller注解的类注入Spring容器中,只是该类成为处理器的第一步,想要修炼大成,还需要在该类中添加注解@RequestMapping。

@RequestMapping注解是用来映射请求的,即指明处理器可以处理哪些URL请求,该注解既可以用在类上,也可以用在方法上。当使用@RequestMapping标记控制器类时,方法的请求地址是相对类的请求地址而言的;当没有使用@RequestMapping标记类时,方法的请求地址是绝对路径。@RequestMapping的地址可以是uri变量,并且通过@PathVariable注解获取作为方法的参数。也可以是通配符来筛选请求地址。具体的使用方法不是本次的重点。

1
2
3
4
5
6
7
8
9
java复制代码@Controller
@RequestMapping("/user")
public class UserController{

@RequestMapping("/users")
public String users() {
return "users";
}
}

此时请求users方法的url路径就是:…/user/users。
可以看到上面users方法的返回值是字符串类型的,这个就是处理器在处理完任务后将要跳转的页面。如果想要方法直接返回结果,而不是跳转页面,这就要用到@ResponseBody注解了。

@ResponseBody注解

@ResponseBody表示方法的返回值直接以指定的格式写入Http response body中,而不是解析为跳转路径。
格式的转换是通过HttpMessageConverter中的方法实现的,因为它是一个接口,因此由其实现类完成转换。

如果要求方法返回的是json格式数据,而不是跳转页面,可以直接在类上标注@RestController,而不用在每个方法中标注@ResponseBody,简化了开发过程。

ref: @RestController注解初步理解

本文转载自: 掘金

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

某小厂面试:String 类型的变量和常量做“+”运算时发生

发表于 2021-09-23

本文已经收录进 Github 110k+ 点赞的 Java 知识点总结类开源项目JavaGuide,【Java学习+面试指南】 一份涵盖大部分Java程序员所需要掌握的核心知识。

前言

看到了一个球友分享的面试题,一定要分享一下。

这个面试题不论是面试还是笔试中都是非常常见的,搞懂原理非常重要!

球友的描述如下:

不过,这个问题我们在日常开发中不会遇到。

因为,比较 String 字符串的值是否相等,可以使用 equals() 方法。 String 中的 equals 方法是被重写过的。 Object 的 equals 方法是比较的对象的内存地址,而 String 的 equals 方法比较的是字符串的值是否相等。

不过,这个面试题会涉及到很多 Java 基础以及 JVM 相关的知识点。还是非常有必要搞懂的!

问题解答&原理分析

我对问题进行了完善了修改,我们先来看字符串不加 final 关键字拼接的情况。完善后的代码如下(JDK1.8):

1
2
3
4
5
6
7
8
9
java复制代码String str1 = "str";
String str2 = "ing";

String str3 = "str" + "ing";//常量池中的对象
String str4 = str1 + str2; //在堆上创建的新的对象
String str5 = "string";//常量池中的对象
System.out.println(str3 == str4);//false
System.out.println(str3 == str5);//true
System.out.println(str4 == str5);//false

对于基本数据类型来说,== 比较的是值。对于引用数据类型来说,==比较的是对象的内存地址。

对于编译期可以确定值的字符串,也就是常量字符串 ,jvm 会将其存入字符串常量池。

字符串常量池 是 JVM 为了提升性能和减少内存消耗针为字符串(String 类)专门开辟的一块区域,主要目的是为了避免字符串的重复创建。

1
2
3
4
5
> java复制代码String aa = "ab"; // 放在常量池中
> String bb = "ab"; // 从常量池中查找
> System.out.println("aa==bb");// true
>
>

JDK1.7 之前运行时常量池逻辑包含字符串常量池存放在方法区。JDK1.7 的时候,字符串常量池被从方法区拿到了堆中。

并且,字符串常量拼接得到的字符串常量在编译阶段就已经被存放字符串常量池,这个得益于编译器的优化。

在编译过程中,Javac 编译器(下文中统称为编译器)会进行一个叫做 常量折叠(Constant Folding) 的代码优化。《深入理解 Java 虚拟机》中是也有介绍到:

常量折叠会把常量表达式的值求出来作为常量嵌在最终生成的代码中,这是 Javac 编译器会对源代码做的极少量优化措施之一(代码优化几乎都在即时编译器中进行)。

对于 String str3 = "str" + "ing"; 编译器会给你优化成 String str3 = "string"; 。

并不是所有的常量都会进行折叠,只有编译器在程序编译期就可以确定值的常量才可以:

  1. 基本数据类型(byte、boolean、short、char、int、float、long、double)以及字符串常量
  2. final 修饰的基本数据类型和字符串变量
  3. 字符串通过 “+”拼接得到的字符串、基本数据类型之间算数运算(加减乘除)、基本数据类型的位运算(<<、>>、>>> )

因此,str1 、 str2 、 str3 都属于字符串常量池中的对象。

引用的值在程序编译期是无法确定的,编译器无法对其进行优化。

对象引用和“+”的字符串拼接方式,实际上是通过 StringBuilder 调用 append() 方法实现的,拼接完成之后调用 toString() 得到一个 String 对象 。

1
java复制代码String str4 = new StringBuilder().append(str1).append(str2).toString();

因此,str4 并不是字符串常量池中存在的对象,属于堆上的新对象。

我画了一个图帮助理解:

我们在平时写代码的时候,尽量避免多个字符串对象拼接,因为这样会重新创建对象。如果需要改变字符串的话,可以使用 StringBuilder 或者 StringBuffer。

不过,字符串使用 final 关键字声明之后,可以让编译器当做常量来处理。

1
2
3
4
5
6
java复制代码final String str1 = "str";
final String str2 = "ing";
// 下面两个表达式其实是等价的
String c = "str" + "str2";// 常量池中的对象
String d = str1 + str2; // 常量池中的对象
System.out.println(c == d);// true

被 final 关键字修改之后的 String 会被编译器当做常量来处理,编译器在程序编译期就可以确定它的值,其效果就想到于访问常量。

如果 ,编译器在运行时才能知道其确切值的话,就无法对其优化。

示例代码如下(str2 在运行时才能确定其值):

1
2
3
4
5
6
7
8
java复制代码final String str1 = "str";
final String str2 = getStr();
String c = "str" + "str2";// 常量池中的对象
String d = str1 + str2; // 常量池中的对象
System.out.println(c == d);// false
public static String getStr() {
return "ing";
}

我们再来看一个类似的问题!

1
2
3
4
5
java复制代码String str1 = "abcd";
String str2 = new String("abcd");
String str3 = new String("abcd");
System.out.println(str1==str2);
System.out.println(str2==str3);

上面的代码运行之后会输出什么呢?

答案是:

1
2
arduino复制代码false
false

这是为什么呢?

我们先来看下面这种创建字符串对象的方式:

1
2
java复制代码// 从字符串常量池中拿对象
String str1 = "abcd";

这种情况下,jvm 会先检查字符串常量池中有没有”abcd”,如果字符串常量池中没有,则创建一个,然后 str1 指向字符串常量池中的对象,如果有,则直接将 str1 指向”abcd””;

因此,str1 指向的是字符串常量池的对象。

我们再来看下面这种创建字符串对象的方式:

1
2
3
java复制代码// 直接在堆内存空间创建一个新的对象。
String str2 = new String("abcd");
String str3 = new String("abcd");

只要使用 new 的方式创建对象,便需要创建新的对象 。

使用 new 的方式创建对象的方式如下,可以简单概括为 3 步:

  1. 在堆中创建一个字符串对象
  2. 检查字符串常量池中是否有和 new 的字符串值相等的字符串常量
  3. 如果没有的话需要在字符串常量池中也创建一个值相等的字符串常量,如果有的话,就直接返回堆中的字符串实例对象地址。

因此,str2 和 str3 都是在堆中新创建的对象。

字符串常量池比较特殊,它的主要使用方法有两种:

  1. 直接使用双引号声明出来的 String 对象会直接存储在常量池中。
  2. 如果不是用双引号声明的 String 对象,使用 String 提供的 intern() 方法也有同样的效果。String.intern() 是一个 Native 方法,它的作用是:如果运行时常量池中已经包含一个等于此 String 对象内容的字符串,则返回常量池中该字符串的引用;如果没有,JDK1.7 之前(不包含 1.7)的处理方式是在常量池中创建与此 String 内容相同的字符串,并返回常量池中创建的字符串的引用,JDK1.7 以及之后,字符串常量池被从方法区拿到了堆中,jvm 不会在常量池中创建该对象,而是将堆中这个对象的引用直接放到常量池中,减少不必要的内存开销。

示例代码如下(JDK 1.8) :

1
2
3
4
5
6
7
8
9
10
java复制代码String s1 = "Javatpoint";  
String s2 = s1.intern();
String s3 = new String("Javatpoint");
String s4 = s3.intern();
System.out.println(s1==s2); // True
System.out.println(s1==s3); // False
System.out.println(s1==s4); // True
System.out.println(s2==s3); // False
System.out.println(s2==s4); // True
System.out.println(s3==s4); // False

推荐阅读

  • R 大(RednaxelaFX)关于常量折叠的回答:www.zhihu.com/question/55…
  • 《深入理解 Java 虚拟机》第 10 章程序编译与代码优化

总结

  1. 对于基本数据类型来说,==比较的是值。对于引用数据类型来说,==比较的是对象的内存地址。
  2. 在编译过程中,Javac 编译器(下文中统称为编译器)会进行一个叫做 常量折叠(Constant Folding) 的代码优化。常量折叠会把常量表达式的值求出来作为常量嵌在最终生成的代码中,这是 Javac 编译器会对源代码做的极少量优化措施之一(代码优化几乎都在即时编译器中进行)。
  3. 一般来说,我们要尽量避免通过 new 的方式创建字符串。使用双引号声明的 String 对象( String s1 = "java" )更利于让编译器有机会优化我们的代码,同时也更易于阅读。
  4. 被 final 关键字修改之后的 String 会被编译器当做常量来处理,编译器程序编译期就可以确定它的值,其效果就想到于访问常量。

本文转载自: 掘金

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

Spring Boot中如何配置线程池拒绝策略,妥善处理好溢

发表于 2021-09-23

通过之前三篇关于Spring Boot异步任务实现的博文,我们分别学会了用@Async创建异步任务、为异步任务配置线程池、使用多个线程池隔离不同的异步任务。今天这篇,我们继续对上面的知识进行完善和优化!

如果你已经看过上面几篇内容并已经掌握之后,一起来思考下面这个问题:

假设,线程池配置为核心线程数2、最大线程数2、缓冲队列长度2。此时,有5个异步任务同时开始,会发生什么?

场景重现

我们先来把上面的假设用代码实现一下:

第一步:创建Spring Boot应用,根据上面的假设写好线程池配置。

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
java复制代码@EnableAsync
@SpringBootApplication
public class Chapter78Application {

public static void main(String[] args) {
SpringApplication.run(Chapter78Application.class, args);
}

@EnableAsync
@Configuration
class TaskPoolConfig {

@Bean
public Executor taskExecutor1() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(2);
executor.setMaxPoolSize(2);
executor.setQueueCapacity(2);
executor.setKeepAliveSeconds(60);
executor.setThreadNamePrefix("executor-1-");
return executor;
}

}

}

第二步:用@Async注解实现一个部分任务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java复制代码@Slf4j
@Component
public class AsyncTasks {

public static Random random = new Random();

@Async("taskExecutor1")
public CompletableFuture<String> doTaskOne(String taskNo) throws Exception {
log.info("开始任务:{}", taskNo);
long start = System.currentTimeMillis();
Thread.sleep(random.nextInt(10000));
long end = System.currentTimeMillis();
log.info("完成任务:{},耗时:{} 毫秒", taskNo, end - start);
return CompletableFuture.completedFuture("任务完成");
}

}

第三步:编写测试用例

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
java复制代码@Slf4j
@SpringBootTest
public class Chapter78ApplicationTests {

@Autowired
private AsyncTasks asyncTasks;

@Test
public void test2() throws Exception {
// 线程池配置:core-2,max-2,queue=2,同时有5个任务,出现下面异常:
// org.springframework.core.task.TaskRejectedException: Executor [java.util.concurrent.ThreadPoolExecutor@59901c4d[Running, pool size = 2,
// active threads = 0, queued tasks = 2, completed tasks = 4]] did not accept task: java.util.concurrent.CompletableFuture$AsyncSupply@408e96d9

long start = System.currentTimeMillis();

// 线程池1
CompletableFuture<String> task1 = asyncTasks.doTaskOne("1");
CompletableFuture<String> task2 = asyncTasks.doTaskOne("2");
CompletableFuture<String> task3 = asyncTasks.doTaskOne("3");
CompletableFuture<String> task4 = asyncTasks.doTaskOne("4");
CompletableFuture<String> task5 = asyncTasks.doTaskOne("5");

// 一起执行
CompletableFuture.allOf(task1, task2, task3, task4, task5).join();

long end = System.currentTimeMillis();

log.info("任务全部完成,总耗时:" + (end - start) + "毫秒");
}

}

执行一下,可以类似下面这样的日志信息:

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
less复制代码2021-09-22 17:33:08.159  INFO 21119 --- [   executor-1-2] com.didispace.chapter78.AsyncTasks       : 开始任务:2
2021-09-22 17:33:08.159 INFO 21119 --- [ executor-1-1] com.didispace.chapter78.AsyncTasks : 开始任务:1

org.springframework.core.task.TaskRejectedException: Executor [java.util.concurrent.ThreadPoolExecutor@3e1a3801[Running, pool size = 2, active threads = 2, queued tasks = 2, completed tasks = 0]] did not accept task: java.util.concurrent.CompletableFuture$AsyncSupply@64968732

at org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor.execute(ThreadPoolTaskExecutor.java:324)
at java.util.concurrent.CompletableFuture.asyncSupplyStage(CompletableFuture.java:1604)
at java.util.concurrent.CompletableFuture.supplyAsync(CompletableFuture.java:1830)
at org.springframework.aop.interceptor.AsyncExecutionAspectSupport.doSubmit(AsyncExecutionAspectSupport.java:274)
at org.springframework.aop.interceptor.AsyncExecutionInterceptor.invoke(AsyncExecutionInterceptor.java:129)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:750)
at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:692)
at com.didispace.chapter78.AsyncTasks$$EnhancerBySpringCGLIB$$c7e8d57b.doTaskOne(<generated>)
at com.didispace.chapter78.Chapter78ApplicationTests.test2(Chapter78ApplicationTests.java:51)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at org.junit.platform.commons.util.ReflectionUtils.invokeMethod(ReflectionUtils.java:688)
at org.junit.jupiter.engine.execution.MethodInvocation.proceed(MethodInvocation.java:60)
at org.junit.jupiter.engine.execution.InvocationInterceptorChain$ValidatingInvocation.proceed(InvocationInterceptorChain.java:131)
at org.junit.jupiter.engine.extension.TimeoutExtension.intercept(TimeoutExtension.java:149)
at org.junit.jupiter.engine.extension.TimeoutExtension.interceptTestableMethod(TimeoutExtension.java:140)
at org.junit.jupiter.engine.extension.TimeoutExtension.interceptTestMethod(TimeoutExtension.java:84)
at org.junit.jupiter.engine.execution.ExecutableInvoker$ReflectiveInterceptorCall.lambda$ofVoidMethod$0(ExecutableInvoker.java:115)
at org.junit.jupiter.engine.execution.ExecutableInvoker.lambda$invoke$0(ExecutableInvoker.java:105)
at org.junit.jupiter.engine.execution.InvocationInterceptorChain$InterceptedInvocation.proceed(InvocationInterceptorChain.java:106)
at org.junit.jupiter.engine.execution.InvocationInterceptorChain.proceed(InvocationInterceptorChain.java:64)
at org.junit.jupiter.engine.execution.InvocationInterceptorChain.chainAndInvoke(InvocationInterceptorChain.java:45)
at org.junit.jupiter.engine.execution.InvocationInterceptorChain.invoke(InvocationInterceptorChain.java:37)
at org.junit.jupiter.engine.execution.ExecutableInvoker.invoke(ExecutableInvoker.java:104)
at org.junit.jupiter.engine.execution.ExecutableInvoker.invoke(ExecutableInvoker.java:98)
at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.lambda$invokeTestMethod$6(TestMethodTestDescriptor.java:210)
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.invokeTestMethod(TestMethodTestDescriptor.java:206)
at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:131)
at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:65)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$5(NodeTestTask.java:139)
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$7(NodeTestTask.java:129)
at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:127)
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:126)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:84)
at java.util.ArrayList.forEach(ArrayList.java:1255)
at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:38)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$5(NodeTestTask.java:143)
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$7(NodeTestTask.java:129)
at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:127)
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:126)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:84)
at java.util.ArrayList.forEach(ArrayList.java:1255)
at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:38)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$5(NodeTestTask.java:143)
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$7(NodeTestTask.java:129)
at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:127)
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:126)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:84)
at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.submit(SameThreadHierarchicalTestExecutorService.java:32)
at org.junit.platform.engine.support.hierarchical.HierarchicalTestExecutor.execute(HierarchicalTestExecutor.java:57)
at org.junit.platform.engine.support.hierarchical.HierarchicalTestEngine.execute(HierarchicalTestEngine.java:51)
at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:108)
at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:88)
at org.junit.platform.launcher.core.EngineExecutionOrchestrator.lambda$execute$0(EngineExecutionOrchestrator.java:54)
at org.junit.platform.launcher.core.EngineExecutionOrchestrator.withInterceptedStreams(EngineExecutionOrchestrator.java:67)
at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:52)
at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:96)
at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:75)
at com.intellij.junit5.JUnit5IdeaTestRunner.startRunnerWithArgs(JUnit5IdeaTestRunner.java:71)
at com.intellij.rt.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:33)
at com.intellij.rt.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:235)
at com.intellij.rt.junit.JUnitStarter.main(JUnitStarter.java:54)
Caused by: java.util.concurrent.RejectedExecutionException: Task java.util.concurrent.CompletableFuture$AsyncSupply@64968732 rejected from java.util.concurrent.ThreadPoolExecutor@3e1a3801[Running, pool size = 2, active threads = 2, queued tasks = 2, completed tasks = 0]
at java.util.concurrent.ThreadPoolExecutor$AbortPolicy.rejectedExecution(ThreadPoolExecutor.java:2063)
at java.util.concurrent.ThreadPoolExecutor.reject(ThreadPoolExecutor.java:830)
at java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1379)
at org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor.execute(ThreadPoolTaskExecutor.java:321)
... 74 more

从异常信息org.springframework.core.task.TaskRejectedException: Executor [java.util.concurrent.ThreadPoolExecutor@3e1a3801[Running, pool size = 2, active threads = 2, queued tasks = 2, completed tasks = 0]] did not accept task: 中,可以很明确的知道,第5个任务因为超过了执行线程+缓冲队列长度,而被拒绝了。

所有,默认情况下,线程池的拒绝策略是:当线程池队列满了,会丢弃这个任务,并抛出异常。

配置拒绝策略

虽然线程池有默认的拒绝策略,但实际开发过程中,有些业务场景,直接拒绝的策略往往并不适用,有时候我们可能会选择舍弃最早开始执行而未完成的任务、也可能会选择舍弃刚开始执行而未完成的任务等更贴近业务需要的策略。所以,为线程池配置其他拒绝策略或自定义拒绝策略是很常见的需求,那么这个要怎么实现呢?

下面就来具体说说今天的正题,如何为线程池配置拒绝策略、如何自定义拒绝策略。

看下面这段代码的最后一行,setRejectedExecutionHandler方法就是为线程池设置拒绝策略的方法:

1
2
3
4
5
java复制代码ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();

//...其他线程池配置

executor.setRejectedExecutionHandler(new ThreadPoolExecutor.AbortPolicy());

在ThreadPoolExecutor中提供了4种线程的策略可以供开发者直接使用,你只需要像下面这样设置即可:

1
2
3
4
5
6
7
8
9
10
11
java复制代码// AbortPolicy策略
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.AbortPolicy());

// DiscardPolicy策略
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.DiscardPolicy());

// DiscardOldestPolicy策略
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.DiscardOldestPolicy());

// CallerRunsPolicy策略
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());

这四个策略对应的含义分别是:

  • AbortPolicy策略:默认策略,如果线程池队列满了丢掉这个任务并且抛出RejectedExecutionException异常。
  • DiscardPolicy策略:如果线程池队列满了,会直接丢掉这个任务并且不会有任何异常。
  • DiscardOldestPolicy策略:如果队列满了,会将最早进入队列的任务删掉腾出空间,再尝试加入队列。
  • CallerRunsPolicy策略:如果添加到线程池失败,那么主线程会自己去执行该任务,不会等待线程池中的线程去执行。

而如果你要自定义一个拒绝策略,那么可以这样写:

1
2
3
4
5
6
java复制代码executor.setRejectedExecutionHandler(new RejectedExecutionHandler() {
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
// 拒绝策略的逻辑
}
});

当然如果你喜欢用Lamba表达式,也可以这样写:

1
2
3
java复制代码executor.setRejectedExecutionHandler((r, executor1) -> {
// 拒绝策略的逻辑
});

好了,今天的学习就到这里!

如果您学习过程中如遇困难?可以加入我们超高质量的Spring技术交流群,参与交流与讨论,更好的学习与进步!更多Spring Boot教程可以点击直达!,欢迎收藏与转发支持!

代码示例

本文的完整工程可以查看下面仓库中2.x目录下的chapter7-8工程:

  • Github:github.com/dyc87112/Sp…
  • Gitee:gitee.com/didispace/S…

如果您觉得本文不错,欢迎Star支持,您的关注是我坚持的动力!

欢迎关注我的公众号:程序猿DD,分享外面看不到的干货与思考!

本文转载自: 掘金

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

小学生都能读懂的网络协议之 WebSocket 简介 web

发表于 2021-09-23

简介

服务端和客户端应该怎么进行通信呢?我们常见的方法就是客户端向服务器端发送一个请求,然后服务器端向客户端发送返回的响应。这种做法比较简单,逻辑也很清晰,但是在某些情况下,这种操作方式并不好使。

比如在服务器端的某些变动需要通知客户端的情况,因为客户端并不知道服务器端的变动是否完成,所以需要不停的使用轮循去检测服务器的状态。这种做法的缺点就是太过于浪费资源。如果希望及时性好的话,需要不断的减少轮循的时间间隔,导致极大的服务器压力和资源的浪费。

那么有没有好的解决办法呢?

既然不能使用查询,那么就改成服务器推送就行了。我们知道在HTTP/2中,提供了一种服务器推送的方式,但是这种方式是单向的,也就是说在同一个TCP连接之上,并不能实现客户端和服务器端的交互。

于是我们需要一个能够双向交互的网络协议,这个协议就是WebSocket。

webSocket vs HTTP

webSocket是一个基于底层TCP协议的一个双向通信网络协议。这个双向通信是通过一个TCP连接来实现的。webSocket于2011年以RFC 6455发布成为IETF的标准。

同样作为基于TCP协议的标准协议,它和HTTP有什么区别呢?

如果以OSI的七层模型来说,两者都位于七层协议的第四层。但是两者是两种不同的协议。鉴于HTTP已经如此流行了,为了保证webSocket的通用性,webSocket也对HTTP协议进行了兼容。也就是说能够使用HTTP协议的地方也就可以使用webScoket。

这个和之前讨论的HTTP3有点类似,虽然HTTP3是一个新的协议,但是为了保证其广泛的应用基础,HTTP3还是在现有的UDP协议上进行重写和构建。目的就是为了兼容。

实时上,webSocket使用的是HTTP upgrade header,从HTTP协议升级成为webSocket协议。

HTTP upgrade header

什么是HTTP upgrade header呢?

HTTP upgrade header是在HTTP1.1中引入的一个HTTP头。当客户端觉得需要升级HTTP协议的时候,会向服务器端发送一个升级请求,服务器端会做出相应的响应。

对于websocket来说,客户端在和服务器端建立连接之后,会首先发送给服务器端 Upgrade: WebSocket 和 Connection: Upgrade 头。服务器端接收到客户端的请求之后,如果支持webSocket协议,那么会返回同样的Upgrade: WebSocket和Connection: Upgrade 头到客户端。客户端接收到服务器端的响应之后,就知道服务器端支持websocket协议了,然后就可以使用WebSocket协议发送消息了。

websocket的优点

其实前面我们也讲过了,相对于传统的HTTP拉取,webSocket可以借助于一个TCP连接实现数据的实时传输。可以在减少服务器压力的同时,实现服务器和客户端的实时通信。

webScoket的应用

WebSocket使用的是ws和wss作为URI的标记符。其中ws表示的是websocket,而wss表示的是WebSocket Secure。

因为通常来说我们使用的web浏览器来和服务器进行通信。浏览器就是我们的web客户端,对于现代浏览器来说,基本上都支持WebSocket协议,所以大家可以放心应用,不用担心协议兼容的问题。

对于浏览器客户端来说,可以使用标准的浏览器WebSocket对象,来和服务器进行通信,我们看一个简单的javascript客户端使用webSocket进行通信的例子:

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
scss复制代码// 使用标准的WebSocket API创建一个socket连接
const socket = new WebSocket('ws://www.flydean.com:8000/webscoket');

// 监听webSocket的open事件
socket.onopen = function () {
setInterval(function() {
if (socket.bufferedAmount == 0)
socket.send(getUpdateData());
}, 50);
};

// 监听接收消息事件
socket.onmessage = function(event) {
handleUpdateData(event.data);
};

// 监听socket关闭事件
socket.onclose = function(event) {
onSocketClose(event);
};

// 监听error事件
socket.onerror = function(event) {
onSocketError(event);
};

上述代码主要就是各种监听socket的事件,然后进行处理,非常简单。

websocket的握手流程

上面我们讲过了,websocket是从HTTP协议升级的,客户端通过发送:

1
2
makefile复制代码Upgrade: websocket
Connection: Upgrade

到服务器端,对协议进行升级。我们举一个具体的例子:

1
2
3
4
5
6
7
8
makefile复制代码GET /webscoket HTTP/1.1
Host: www.flydean.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: x123455688xafe=
Sec-WebSocket-Protocol: chat, superchat
Sec-WebSocket-Version: 13
Origin: http://flydean.com

对应的server端的返回:

1
2
3
4
5
makefile复制代码HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: Qhfsfew12445m=
Sec-WebSocket-Protocol: chat

在上面的例子中,除了使用Upgrade头之外,客户端还向服务器端发送了Sec-WebSocket-Key header。这个header包含的是一个 base64 编码的随机字节。server对应的会返回这个key的hash值,并将其设置在Sec-WebSocket-Accept header中。

这里并不是为了安全操作,而是为了避免上一次的连接缓存情况。

WebSocket API

要想在浏览器端使用WebSocket,那么就需要用到客户端API,而客户端API中最主要的就是WebSocket。

它提供了对websocket的功能封装。它的构造函数是这样的:

1
scss复制代码WebSocket(url[, protocols])

url就是要连接的websocket的地址,那么可选的protocols是什么呢?protocols可以传入单个协议字符串或者是协议字符串数组。它指的是 WebSocket 服务器实现的子协议。

子协议是在WebSocket协议基础上发展出来的协议,主要用于具体的场景的处理,它是是在WebSocket协议之上,建立的更加严格的规范。

比如,客户端请求服务器时候,会将对应的协议放在Sec-WebSocket-Protocol头中:

1
2
3
bash复制代码GET /socket HTTP/1.1
...
Sec-WebSocket-Protocol: soap, wamp

服务器端会根据支持的类型,做对应的返回,如:

1
makefile复制代码Sec-WebSocket-Protocol: soap

WebSocket API有四种状态,分别是:

状态定义 取值
WebSocket.CONNECTING 0
WebSocket.OPEN 1
WebSocket.CLOSING 2
WebSocket.CLOSED 3

通过调用close或者Send方法,会触发相应的events事件,WebSocket API 的事件主要有:close,error,message,open这4种。

下面是一个具体使用的例子:

1
2
3
4
5
6
7
8
9
10
11
12
javascript复制代码// 创建连接
const socket = new WebSocket('ws://localhost:8000');

// 开启连接
socket.addEventListener('open', function (event) {
socket.send('没错,开启了!');
});

// 监听消息
socket.addEventListener('message', function (event) {
console.log('监听到服务器的消息 ', event.data);
});

总结

以上就是websocket的简单介绍和使用,有想知道Websocket到底是怎么进行消息传输的,敬请期待我的下一篇文章。

本文已收录于 www.flydean.com/06-websocke…

最通俗的解读,最深刻的干货,最简洁的教程,众多你不知道的小技巧等你来发现!

欢迎关注我的公众号:「程序那些事」,懂技术,更懂你!

本文转载自: 掘金

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

【建议收藏】毕设/私活/兼职大佬必备,一个挣钱的开源【Spr

发表于 2021-09-23

前言:

**前段时间也和大家分享了一个关于Springboot+vue的前后端分享的脚手架、最近刚好有同学找到我叫我帮他做一个简单的酒庄管理系统、于是就找到个这个开源框架来试试看、这是个什么项目呢,它就是是集SpringBoot、MyBatis、Shiro于一体的标准项目框架、让我们解放双手 ✋ 从现在开始看一下吧。不管是用来学习技术还是接私活/毕设/兼职挣钱、都是非常不错的哟、建议大家收藏起来、文末我把源码给大家。**

)​

**这岂不是太简单了 分分钟就能做好的吗、哈哈、不说废话了、今天就给大家演示一下利用一个开源框架写一个吧**

这个开源项目主要特点:

  • 这个框架采用SpringBoot、MyBatis、Shiro框架,开发的一套权限系统,极低门槛,拿来即用。设计之初,也非常注重安全性,为自己做学习使用以及简单企业系统都是可以的,让我们的开发变得很简单。
  • 灵活的权限控制,可控制到页面或按钮,满足绝大部分的权限需求、根据管理员灵活控制权限
  • 完善的部门管理及数据权限,通过注解实现数据权限的控制、具体到前端后代码
  • 完善的XSS防范及脚本过滤,彻底杜绝XSS攻击
  • 支持MySQL、Oracle、SQL Server、PostgreSQL等主流数据库
  • 后期推荐进行云服务器进行部署项目

内置功能模板:

**用户管理:用户是系统操作者,该功能主要完成系统用户配置。

部门管理:配置系统组织机构(公司、部门、小组),树结构展现支持数据权限。

岗位管理:配置系统用户所属担任职务。

菜单管理:配置系统菜单,操作权限,按钮权限标识等。

角色管理:角色菜单权限分配、设置角色按机构进行数据范围权限划分。

字典管理:对系统中经常使用的一些较为固定的数据进行维护。

参数管理:对系统动态配置常用参数。

通知公告:系统通知公告信息发布维护。

操作日志:系统正常操作日志记录和查询;系统异常信息日志记录和查询。

登录日志:系统登录日志记录查询包含登录异常。

在线用户:当前系统中活跃用户状态监控。

定时任务:在线(添加、修改、删除)任务调度包含执行结果日志。

代码生成:前后端代码的生成(java、html、xml、sql)支持CRUD下载 。

系统接口:根据业务代码自动生成相关的api接口文档。

服务监控:监视当前系统CPU、内存、磁盘、堆栈等相关信息。

缓存监控:对系统的缓存信息查询,命令统计等。

在线构建器:拖动表单元素生成相应的HTML代码。

连接池监视:监视当期系统数据库连接池状态,可进行分析SQL找出系统性能瓶颈。**

项目介绍:

项目实际分为4个模块:

  • renren-common为公共模块,其他模块以jar包的形式引入进去,主要提供些工具类,以及renren-admin、renren-api模块公共的entity、mapper、dao、service服务,防止一个功能重复多次编写代码。
  • renren-admin为后台模块,也是系统的核心,用来开发后台管理系统,可以打包成jar,部署到服务器上运行,或者打包成war,放到Tomcat8.5+容器里运行。
  • renren-api为接口模块,主要是简化APP开发,如:为微信小程序、IOS、Android提供接口,拥有一套单独的用户体系,没有与renren-admin用户表共用,因为renren-admin用户表里存放的是企业内部人员账号,具有后台管理员权限,可以登录后台管理系统,而renren-api用户表里存放的是我们的真实用户,不具备登录后台管理系统的权限。renren-api主要是实现了用户注册、登录、接口权限认证、获取登录用户等功能,为APP接口的安全调用,提供一套优雅的解决方案,从而简化APP接口开发。
  • renren-generator为代码生成器模块,只需在MySQL数据库里,创建好表结构,就可以生成新增、修改、删除、查询、导出等操作的代码,包括entity、mapper、dao、service、controller、页面等所有代码,项目开发神器。

)​

这边由于我个人没有用到api/微信小程序、IOS、Android提供接口的业务以及generator代码生成器模块 、所以可以根据实际情况删除和添加模块

本地部署项目:

下载地址:

1
bash复制代码git clone https://gitee.com/renrenio/renren-security.git

  • 环境要求JDK1.8、Tomcat8.5+、MySQL5.5+
  • 通过git,下载renren-security源码,如下:

)​

  • 创建数据库 renren_security ,数据库编码为 UTF-8
  • 执行数据库脚本,如MySQL数据库,则执行 db/mysql.sql 文件,初始化数据
  • 修改application-dev.yml,更改数据库账号和密码
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
yaml复制代码spring:
datasource:
type: com.alibaba.druid.pool.DruidDataSource
druid:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/renren-chateau?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
username: root
password: 123456
initial-size: 10
max-active: 100
min-idle: 10
max-wait: 60000
pool-prepared-statements: true
max-pool-prepared-statement-per-connection-size: 20
time-between-eviction-runs-millis: 60000
min-evictable-idle-time-millis: 300000
#Oracle需要打开注释
#validation-query: SELECT 1 FROM DUAL
test-while-idle: true
test-on-borrow: false
test-on-return: false
stat-view-servlet:
enabled: true
url-pattern: /druid/*
#login-username: admin
#login-password: admin
filter:
stat:
log-slow-sql: true
slow-sql-millis: 1000
merge-sql: false
wall:
config:
multi-statement-allow: true


##多数据源的配置,需要引用renren-dynamic-datasource
#dynamic:
# datasource:
# slave1:
# driver-class-name: com.microsoft.sqlserver.jdbc.SQLServerDriver
# url: jdbc:sqlserver://localhost:1433;DatabaseName=renren_security
# username: sa
# password: 123456
# slave2:
# driver-class-name: org.postgresql.Driver
# url: jdbc:postgresql://localhost:5432/renren_security
# username: renren
# password: 123456

设置拦截放行:

静态资源文件以及登录和swagger-ui接口文档等设置放行

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
typescript复制代码@Bean("shiroFilter")
public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean();
shiroFilter.setSecurityManager(securityManager);
shiroFilter.setLoginUrl("/login.html");
shiroFilter.setUnauthorizedUrl("/");

Map<String, String> filterMap = new LinkedHashMap<>();
filterMap.put("/swagger/**", "anon");
filterMap.put("/v2/api-docs", "anon");
filterMap.put("/swagger-ui.html", "anon");
filterMap.put("/webjars/**", "anon");
filterMap.put("/swagger-resources/**", "anon");

filterMap.put("/statics/**", "anon");
filterMap.put("/login.html", "anon");
filterMap.put("/sys/login", "anon");
filterMap.put("/favicon.ico", "anon");
filterMap.put("/captcha.jpg", "anon");

filterMap.put("/**", "authc");
shiroFilter.setFilterChainDefinitionMap(filterMap);

return shiroFilter;
}

项目启动:

  • 运行io.renren.AdminApplication.java的main方法,则可启动renren-admin项目
  • 项目访问路径:http://localhost:8080/renren-admin

)​

  • 账号密码:admin/admin
  • Swagger路径:http://localhost:8080/renren-admin/swagger/index.html
  • Swagger注解路径:http://localhost:8080/renren-admin/swagger-ui.html

这是稍微经过改造之后的样子

)​

改造前:

输入图片说明​

改造后:

加一些业务表和删除隐藏掉一些不必要的功能模块

)​

)​

核心技术:

SpringBoot框架

Spring Boot是一款开箱即用框架,提供各种默认配置来简化项目配置。让我们的Spring应用变的更轻量化、更快的入门。 在主程序执行main函数就可以运行。你也可以打包你的应用为jar并通过使用java -jar来运行你的Web应用。它遵循”约定优先于配置”的原则, 使用SpringBoot只需很少的配置,大部分的时候直接使用默认的配置即可。同时可以与Spring Cloud的微服务无缝结合。

Spring Boot2.x版本环境要求必须是jdk8或以上版本,服务器Tomcat8或以上版本

优点

  • 使编码变得简单: 推荐使用注解。
  • 使配置变得简单: 自动配置、快速集成新技术能力 没有冗余代码生成和XML配置的要求
  • 使部署变得简单: 内嵌Tomcat、Jetty、Undertow等web容器,无需以war包形式部署
  • 使监控变得简单: 提供运行时的应用监控
  • 使集成变得简单: 对主流开发框架的无配置集成。
  • 使开发变得简单: 极大地提高了开发快速构建项目、部署效率。

Spring Security安全控制

1、介绍

Spring Security是一个能够为基于Spring的企业应用系统提供声明式的安全访问控制解决方案的安全框架。

2、功能

Authentication 认证,就是用户登录

Authorization 授权,判断用户拥有什么权限,可以访问什么资源

安全防护,跨站脚本攻击,session攻击等

非常容易结合Spring进行使用

3、Spring Security与Shiro的区别

相同点

1、认证功能 2、授权功能 3、加密功能

4、会话管理 5、缓存支持 6、rememberMe功能

不同点

优点:

1、Spring Security基于Spring开发,项目如果使用Spring作为基础,配合Spring Security做权限更加方便。而Shiro需要和Spring进行整合开发

2、Spring Security功能比Shiro更加丰富,例如安全防护方面

3、Spring Security社区资源相对比Shiro更加丰富

缺点:

1)Shiro的配置和使用比较简单,Spring Security上手复杂些

2)Shiro依赖性低,不需要依赖任何框架和容器,可以独立运行。Spring Security依赖Spring容器

今天推荐这个SpringBoot开源项目还是比较不错的、项目是快速开发脚手架,代码质量各方面的也还不错、适合用来做自己学习技术或者或自己**兼职私活接单**都是可以的哟、喜欢的朋友点一个一健三联支持下哟

本文转载自: 掘金

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

为什么 Vue2 this 能够直接获取到 data 和 m

发表于 2021-09-23
  1. 前言

大家好,我是若川。为了能帮助到更多对源码感兴趣、想学会看源码、提升自己前端技术能力的同学。我倾力组织了每周大家一起学习200行左右的源码共读活动,感兴趣的可以点此扫码加我微信 ruochuan02 参与。

之前写的《学习源码整体架构系列》 包含jQuery、underscore、lodash、vuex、sentry、axios、redux、koa、vue-devtools、vuex420余篇源码文章。其中最新的三篇是:

50行代码串行Promise,koa洋葱模型原来是这么实现?

Vue 3.2 发布了,那尤雨溪是怎么发布 Vue.js 的?

初学者也能看懂的 Vue3 源码中那些实用的基础工具函数

写相对很难的源码,耗费了自己的时间和精力,也没收获多少阅读点赞,其实是一件挺受打击的事情。从阅读量和读者受益方面来看,不能促进作者持续输出文章。
所以转变思路,写一些相对通俗易懂的文章。其实源码也不是想象的那么难,至少有很多看得懂。歌德曾说:读一本好书,就是在和高尚的人谈话。
同理可得:读源码,也算是和作者的一种学习交流的方式。

本文源于一次源码共读群里群友的提问,请问@若川,“为什么 data 中的数据可以用 this 直接获取到啊”,当时我翻阅源码做出了解答。想着如果下次有人再次问到,我还需要回答一次。当时打算有空写篇文章告诉读者自己探究原理,于是就有了这篇文章。

阅读本文,你将学到:

1
2
3
4
js复制代码1. 如何学习调试 vue2 源码
2. data 中的数据为什么可以用 this 直接获取到
3. methods 中的方法为什么可以用 this 直接获取到
4. 学习源码中优秀代码和思想,投入到自己的项目中

本文不难,用过 Vue 的都看得懂,希望大家动手调试和学会看源码。

看源码可以大胆猜测,最后小心求证。

  1. 示例:this 能够直接获取到 data 和 methods

众所周知,这样是可以输出我是若川的。好奇的人就会思考为啥 this 就能直接访问到呢。

1
2
3
4
5
6
7
8
9
10
11
12
js复制代码const vm = new Vue({
data: {
name: '我是若川',
},
methods: {
sayName(){
console.log(this.name);
}
},
});
console.log(vm.name); // 我是若川
console.log(vm.sayName()); // 我是若川

那么为什么 this.xxx 能获取到data里的数据,能获取到 methods 方法。

我们自己构造写的函数,如何做到类似Vue的效果呢。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
js复制代码function Person(options){

}

const p = new Person({
data: {
name: '若川'
},
methods: {
sayName(){
console.log(this.name);
}
}
});

console.log(p.name);
// undefined
console.log(p.sayName());
// Uncaught TypeError: p.sayName is not a function

如果是你,你会怎么去实现呢。带着问题,我们来调试 Vue2源码学习。

  1. 准备环境调试源码一探究竟

可以在本地新建一个文件夹examples,新建文件index.html文件。
在<body></body>中加上如下js。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
js复制代码<script src="https://unpkg.com/vue@2.6.14/dist/vue.js"></script>
<script>
const vm = new Vue({
data: {
name: '我是若川',
},
methods: {
sayName(){
console.log(this.name);
}
},
});
console.log(vm.name);
console.log(vm.sayName());
</script>

再全局安装npm i -g http-server启动服务。

1
2
3
4
5
js复制代码npm i -g http-server
cd examples
http-server .
// 如果碰到端口被占用,也可以指定端口
http-server -p 8081 .

这样就能在http://localhost:8080/打开刚写的index.html页面了。

对于调试还不是很熟悉的读者,可以看这篇文章《前端容易忽略的 debugger 调试技巧》,截图标注的很详细。

调试:在 F12 打开调试,source 面板,在例子中const vm = new Vue({打上断点。

如下图所示

刷新页面后按F11进入函数,这时断点就走进了 Vue 构造函数。

3.1 Vue 构造函数

1
2
3
4
5
6
7
8
9
10
11
12
13
js复制代码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);
stateMixin(Vue);
eventsMixin(Vue);
lifecycleMixin(Vue);
renderMixin(Vue);

值得一提的是:if (!(this instanceof Vue)){} 判断是不是用了 new 关键词调用构造函数。
一般而言,我们平时应该不会考虑写这个。

当然看源码库也可以自己函数内部调用 new 。但 vue 一般一个项目只需要 new Vue() 一次,所以没必要。

而 jQuery 源码的就是内部 new ,对于使用者来说就是无new构造。

1
2
3
4
js复制代码jQuery = function( selector, context ) {
// 返回new之后的对象
return new jQuery.fn.init( selector, context );
};

因为使用 jQuery 经常要调用。
其实 jQuery 也是可以 new 的。和不用 new 是一个效果。

如果不明白 new 操作符的用处,可以看我之前的文章。面试官问:能否模拟实现JS的new操作符

调试:继续在this._init(options);处打上断点,按F11进入函数。

3.2 _init 初始化函数

进入 _init 函数后,这个函数比较长,做了挺多事情,我们猜测跟data和methods相关的实现在initState(vm)函数里。

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
js复制代码// 代码有删减
function initMixin (Vue) {
Vue.prototype._init = function (options) {
var vm = this;
// a uid
vm._uid = uid$3++;

// a flag to avoid this being observed
vm._isVue = true;
// merge options
if (options && options._isComponent) {
// optimize internal component instantiation
// since dynamic options merging is pretty slow, and none of the
// internal component options needs special treatment.
initInternalComponent(vm, options);
} else {
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
);
}

// expose real self
vm._self = vm;
initLifecycle(vm);
initEvents(vm);
initRender(vm);
callHook(vm, 'beforeCreate');
initInjections(vm); // resolve injections before data/props
// 初始化状态
initState(vm);
initProvide(vm); // resolve provide after data/props
callHook(vm, 'created');
};
}

调试:接着我们在initState(vm)函数这里打算断点,按F8可以直接跳转到这个断点,然后按F11接着进入initState函数。

3.3 initState 初始化状态

从函数名来看,这个函数主要实现功能是:

1
2
3
4
5
bash复制代码初始化 props
初始化 methods
监测数据
初始化 computed
初始化 watch
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
js复制代码function initState (vm) {
vm._watchers = [];
var opts = vm.$options;
if (opts.props) { initProps(vm, opts.props); }
// 有传入 methods,初始化方法
if (opts.methods) { initMethods(vm, opts.methods); }
// 有传入 data,初始化 data
if (opts.data) {
initData(vm);
} else {
observe(vm._data = {}, true /* asRootData */);
}
if (opts.computed) { initComputed(vm, opts.computed); }
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch);
}
}

我们重点来看初始化 methods,之后再看初始化 data。

调试:在 initMethods 这句打上断点,同时在initData(vm)处打上断点,看完initMethods函数后,可以直接按F8回到initData(vm)函数。
继续按F11,先进入initMethods函数。

3.4 initMethods 初始化方法

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
js复制代码function initMethods (vm, methods) {
var props = vm.$options.props;
for (var key in methods) {
{
if (typeof methods[key] !== 'function') {
warn(
"Method \"" + key + "\" has type \"" + (typeof methods[key]) + "\" in the component definition. " +
"Did you reference the function correctly?",
vm
);
}
if (props && hasOwn(props, key)) {
warn(
("Method \"" + key + "\" has already been defined as a prop."),
vm
);
}
if ((key in vm) && isReserved(key)) {
warn(
"Method \"" + key + "\" conflicts with an existing Vue instance method. " +
"Avoid defining component methods that start with _ or $."
);
}
}
vm[key] = typeof methods[key] !== 'function' ? noop : bind(methods[key], vm);
}
}

initMethods函数,主要有一些判断。

1
2
3
js复制代码判断 methods 中的每一项是不是函数,如果不是警告。
判断 methods 中的每一项是不是和 props 冲突了,如果是,警告。
判断 methods 中的每一项是不是已经在 new Vue实例 vm 上存在,而且是方法名是保留的 _ $ (在JS中一般指内部变量标识)开头,如果是警告。

除去这些判断,我们可以看出initMethods函数其实就是遍历传入的methods对象,并且使用bind绑定函数的this指向为vm,也就是new Vue的实例对象。

这就是为什么我们可以通过this直接访问到methods里面的函数的原因。

我们可以把鼠标移上 bind 变量,按alt键,可以看到函数定义的地方,这里是218行,点击跳转到这里看 bind 的实现。

3.4.1 bind 返回一个函数,修改 this 指向

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
js复制代码function polyfillBind (fn, ctx) {
function boundFn (a) {
var l = arguments.length;
return l
? l > 1
? fn.apply(ctx, arguments)
: fn.call(ctx, a)
: fn.call(ctx)
}

boundFn._length = fn.length;
return boundFn
}

function nativeBind (fn, ctx) {
return fn.bind(ctx)
}

var bind = Function.prototype.bind
? nativeBind
: polyfillBind;

简单来说就是兼容了老版本不支持 原生的bind函数。同时兼容写法,对参数多少做出了判断,使用call和apply实现,据说是因为性能问题。

如果对于call、apply、bind的用法和实现不熟悉,可以查看我在面试官问系列中写的面试官问:能否模拟实现JS的call和apply方法
面试官问:能否模拟实现JS的bind方法

调试:看完了initMethods函数,按F8回到上文提到的initData(vm)函数断点处。

3.5 initData 初始化 data

initData 函数也是一些判断。主要做了如下事情:

1
2
3
4
5
6
7
bash复制代码先给 _data 赋值,以备后用。
最终获取到的 data 不是对象给出警告。
遍历 data ,其中每一项:
如果和 methods 冲突了,报警告。
如果和 props 冲突了,报警告。
不是内部私有的保留属性,做一层代理,代理到 _data 上。
最后监测 data,使之成为响应式的数据。
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
js复制代码function initData (vm) {
var data = vm.$options.data;
data = vm._data = typeof data === 'function'
? getData(data, vm)
: data || {};
if (!isPlainObject(data)) {
data = {};
warn(
'data functions should return an object:\n' +
'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',
vm
);
}
// proxy data on instance
var keys = Object.keys(data);
var props = vm.$options.props;
var methods = vm.$options.methods;
var i = keys.length;
while (i--) {
var key = keys[i];
{
if (methods && hasOwn(methods, key)) {
warn(
("Method \"" + key + "\" has already been defined as a data property."),
vm
);
}
}
if (props && hasOwn(props, key)) {
warn(
"The data property \"" + key + "\" is already declared as a prop. " +
"Use prop default value instead.",
vm
);
} else if (!isReserved(key)) {
proxy(vm, "_data", key);
}
}
// observe data
observe(data, true /* asRootData */);
}

3.5.1 getData 获取数据

是函数时调用函数,执行获取到对象。

1
2
3
4
5
6
7
8
9
10
11
12
js复制代码function getData (data, vm) {
// #7573 disable dep collection when invoking data getters
pushTarget();
try {
return data.call(vm, vm)
} catch (e) {
handleError(e, vm, "data()");
return {}
} finally {
popTarget();
}
}

3.5.2 proxy 代理

其实就是用 Object.defineProperty 定义对象

这里用处是:this.xxx 则是访问的 this._data.xxx。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
js复制代码/**
* Perform no operation.
* Stubbing args to make Flow happy without leaving useless transpiled code
* with ...rest (https://flow.org/blog/2017/05/07/Strict-Function-Call-Arity/).
*/
function noop (a, b, c) {}
var sharedPropertyDefinition = {
enumerable: true,
configurable: true,
get: noop,
set: noop
};

function proxy (target, sourceKey, key) {
sharedPropertyDefinition.get = function proxyGetter () {
return this[sourceKey][key]
};
sharedPropertyDefinition.set = function proxySetter (val) {
this[sourceKey][key] = val;
};
Object.defineProperty(target, key, sharedPropertyDefinition);
}

3.5.3 Object.defineProperty 定义对象属性

Object.defineProperty 算是一个非常重要的API。还有一个定义多个属性的API:Object.defineProperties(obj, props) (ES5)

Object.defineProperty 涉及到比较重要的知识点,面试也常考。

1
2
3
4
5
6
bash复制代码value——当试图获取属性时所返回的值。
writable——该属性是否可写。
enumerable——该属性在for in循环中是否会被枚举。
configurable——该属性是否可被删除。
set()——该属性的更新操作所调用的函数。
get()——获取属性值时所调用的函数。

详细举例见此链接

3.6 文中出现的一些函数,最后统一解释下

3.6.1 hasOwn 是否是对象本身拥有的属性

调试模式下,按alt键,把鼠标移到方法名上,可以看到函数定义的地方。点击可以跳转。

1
2
3
4
5
6
7
8
9
10
11
12
13
js复制代码/**
* Check whether an object has the property.
*/
var hasOwnProperty = Object.prototype.hasOwnProperty;
function hasOwn (obj, key) {
return hasOwnProperty.call(obj, key)
}

hasOwn({ a: undefined }, 'a') // true
hasOwn({}, 'a') // false
hasOwn({}, 'hasOwnProperty') // false
hasOwn({}, 'toString') // false
// 是自己的本身拥有的属性,不是通过原型链向上查找的。

3.6.2 isReserved 是否是内部私有保留的字符串$ 和 _ 开头

1
2
3
4
5
6
7
8
9
10
11
js复制代码/**
* Check if a string starts with $ or _
*/
function isReserved (str) {
var c = (str + '').charCodeAt(0);
return c === 0x24 || c === 0x5F
}
isReserved('_data'); // true
isReserved('$options'); // true
isReserved('data'); // false
isReserved('options'); // false
  1. 最后用60余行代码实现简化版

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
js复制代码function noop (a, b, c) {}
var sharedPropertyDefinition = {
enumerable: true,
configurable: true,
get: noop,
set: noop
};
function proxy (target, sourceKey, key) {
sharedPropertyDefinition.get = function proxyGetter () {
return this[sourceKey][key]
};
sharedPropertyDefinition.set = function proxySetter (val) {
this[sourceKey][key] = val;
};
Object.defineProperty(target, key, sharedPropertyDefinition);
}
function initData(vm){
const data = vm._data = vm.$options.data;
const keys = Object.keys(data);
var i = keys.length;
while (i--) {
var key = keys[i];
proxy(vm, '_data', key);
}
}
function initMethods(vm, methods){
for (var key in methods) {
vm[key] = typeof methods[key] !== 'function' ? noop : methods[key].bind(vm);
}
}

function Person(options){
let vm = this;
vm.$options = options;
var opts = vm.$options;
if(opts.data){
initData(vm);
}
if(opts.methods){
initMethods(vm, opts.methods)
}
}

const p = new Person({
data: {
name: '若川'
},
methods: {
sayName(){
console.log(this.name);
}
}
});

console.log(p.name);
// 未实现前: undefined
// '若川'
console.log(p.sayName());
// 未实现前:Uncaught TypeError: p.sayName is not a function
// '若川'
  1. 总结

本文涉及到的基础知识主要有如下:

1
2
3
4
5
js复制代码构造函数
this 指向
call、bind、apply
Object.defineProperty
等等基础知识。

本文源于解答源码共读群友的疑惑,通过详细的描述了如何调试 Vue 源码,来探寻答案。

解答文章开头提问:

通过this直接访问到methods里面的函数的原因是:因为methods里的方法通过 bind 指定了this为 new Vue的实例(vm)。

通过 this 直接访问到 data 里面的数据的原因是:data里的属性最终会存储到new Vue的实例(vm)上的 _data对象中,访问 this.xxx,是访问Object.defineProperty代理后的 this._data.xxx。

Vue的这种设计,好处在于便于获取。也有不方便的地方,就是props、methods 和 data三者容易产生冲突。

文章整体难度不大,但非常建议读者朋友们自己动手调试下。调试后,你可能会发现:原来 Vue 源码,也没有想象中的那么难,也能看懂一部分。

启发:我们工作使用常用的技术和框架或库时,保持好奇心,多思考内部原理。能够做到知其然,知其所以然。就能远超很多人。

你可能会思考,为什么模板语法中,可以省略this关键词写法呢,内部模板编译时其实是用了with。有余力的读者可以探究这一原理。

最后欢迎加我微信 ruochuan12 交流,参与 源码共读 活动,大家一起学习源码,共同进步。


关于 && 交流群

最近组织了源码共读活动,感兴趣的可以加我微信 ruochuan12 参与,长期交流学习。

作者:常以若川为名混迹于江湖。欢迎加我微信ruochuan12。前端路上 | 所知甚少,唯善学。

关注公众号若川视野,每周一起学源码,学会看源码,进阶高级前端。

若川的博客

segmentfault若川视野专栏,开通了若川视野专栏,欢迎关注~

掘金专栏,欢迎关注~

知乎若川视野专栏,开通了若川视野专栏,欢迎关注~

github blog,求个star^_^~

本文转载自: 掘金

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

一文搞懂锁知识

发表于 2021-09-23

一位风尘仆仆的男子走了过来,对我说:小伙子,我看你简历上写的精通mysql,那我就问问你mysql的知识吧!

我:好的(千万别多问,千万别多问~~)

面试官:对mysql锁了解的多吗?

我:还行(其实就是很好)。

面试官:那好,那我接下来主要问锁相关的知识

我:好的好的(放马过来吧,我也就是客气一下。)

面试官: mysql支持哪些级别的锁?

我:支持库锁、表锁、行锁。

面试官:那先说说库锁吧,锁库有几种方式?

我:两种,分别是FTWRL(Flush tables with read lock)和 set global readonly=true

面试官:这两种有什么区别?

我:首先不管是谁,只要锁库了,那么整个库都处于只读状态,所有的更新操作都将被阻塞,FTWRL模式风险稍微小点,如果客户端异常断开后,FTWRL锁会自动释放,但是global readonly=true不会自动释放锁。

面试官:那myisam和innodb的锁有什么区别。

我:myisam不支持行锁,只支持表级的锁,innodb支持更细粒度的行锁。

面试官:表级锁使用过吗?

我:没有用过,表锁性能差。

面试官:那你知道MDL锁吗?

我:了解,MDL(metadata lock)锁是server层的锁,表级锁,它是隐式的,不需要显式的使用,mysql每次读写数据时(insert、update、select、delete)都要先去获取MDL读锁,只有获取到了MDL读锁,才能进行接下来的操作,否则阻塞,其中MDL读锁之间是共享的,当对数据库进行表结构变更的时候,会获取MDL写锁,MDL写锁和任何MDL锁都是互斥的,不管是MDL读锁还是MDL写锁。

面试官:那MDL锁的作用是什么?

我:MDL锁是为了解决DDL和DML之间冲突的问题。

  1. 假设事务A先查询得到一个数据,然后事务B执行了字段修改,那么事务A再次去查的时候,发现数据对不上了。

  1. 事务A先更新数据未提交,事务B修改字段提交,slave就会先修改字段,再更新数据,那么就会有问题

当使用了MDL锁后,DDL操作必须要先获得MDL写锁,我们知道写锁和写锁,写锁和读锁是冲突的,那么在DDL之前如果有任何查询或者更新,都必须要阻塞等待,不会让DDL执行的,从而解决了冲突问题。

面试官:那有了MDL锁,在线DDL是不是就很安全?

我:不一定。

假设session1先执行查询,但是不提交。session2紧接着执行添加字段的DDL,最后session3再执行查询。此时会发现session2和session3都是阻塞的,如果后面还来了sessionN的查询,那么都将阻塞,严重时将造成大量线程阻塞。

面试官:那你能解释下为什么这种情况下会阻塞吗?

我:首先MDL锁一定是在事务提交后才释放的,session1在执行查询后,并没有commit,那么MDL读锁是没有释放的,session2紧接着执行DDL,执行DDL是要获取MDL写锁的,由于写锁和读锁是互斥的,那么session2是卡住的,它在等待session1释放读锁,session3在session2之后执行的,此时session3是需要一个读锁的,但是由于获取锁是有先后顺序的,它们要排队,并且写锁的优先级要高于读锁,这也是为什么session3会卡住。

面试官:所以是session1执行commit之后,然后session2先执行完,最后session3先执行完?我刚试了下你的例子,看着好像session2和session3几乎同时运行完,你能否具体说说为什么呢?(看你进不进坑)

我:(想给我挖坑,没门),不是的,并不是session2先执行完,其实是session3先执行完,但是session2会先获得MDL写锁,由于session3没有显式的开启一个事务,那么session3默认执行完毕之后自动commit,所以在session1 commit之后,看起来像session2和session3几乎同时进行的。如果让session3显式的开启事务,就能发现运行的细节了。

这样当session1 commit之后可以发现session3先执行,session2依然是卡住的,只有当session3 commit之后,发现session2才能运行。所以真实情况应该是session3先执行,然后session2。

面试官:那这和你上面说的获取MDL锁排队的问题是不是矛盾了?

我:不矛盾,这其实涉及到online DDL的知识了,我们知道mysql支持在线DDL,不阻塞用户操作,当执行一个DDL,它的流程大概是这样的:

  1. 拿MDL写锁
  2. 降级成MDL读锁
  3. 真正做DDL
  4. 升级成MDL写锁
  5. 释放MDL锁

其中session2在拿到MDL写锁后,会降级成MDL读锁,降级后,session3拿到MDL读锁,然后执行select,但是没有commit,这样MDL读锁就没释放,然后session2在升级成MDL写锁的时候因为session3没释放读锁从而导致session2阻塞。

面试官:(这小子不错呀),那你知道为什么DDL过程中1-2要降级,而3-4又要升级吗?

我:(早知道就要问,还好我有准备),

首先在MDL写锁期间,干的事就是创建临时的frm和idb文件,这个过程要安全,是排他的,同时这个过程也是快速的,在临时文件创建好之后,就不需要排他了,那么就降级为读锁,支持正常的增删改查,这也是为什么DDL支持online的原因之一。在新的数据文件写好之后,要替换老的数据文件,这个过程要安全,所以在3执行完后,会尝试升级成MDL写锁,这个过程也是快速的,也是支持online DDL的原因之二。

面试官:我们知道InnoDB支持行级锁,行锁还分两类你知道吗?

我:知道,S(共享锁)和X(排他锁),S锁和S锁是共享的,X锁和任意锁互斥。

面试官:既然X锁和任意锁互斥,那么如果存在两个事务,事务A更新数据后,不提交,那么事务B去查询这条数据是不是就阻塞?

我:不会的,因为InnoDB支持MVCC(多版本控制),当一个事务执行查询的时候,它可以通过undo log查询到一个快照,这样就不用锁。

面试官:那你知道IS(意向共享锁)和IX(意向排他锁)吗?

我:首先它俩都是表级别的锁,因为InnoDB是支持行锁的,当某些行已经上了X锁之后,再想对这个表上锁的话,就得确认当前表中没有任何X锁,在没有意向锁的情况下,就得一行一行去判断,这样效率会非常低下,在有了意向锁之后,就不需要一行一行判断了,举个例子:

1
sql复制代码select * from user where id=1 for update;

当一个事务对id=1这行数据上了X锁之后,就会对user表也加一个IX锁。

1
sql复制代码LOCK TABLES user READ;

这时想要给表加上读锁,但是发现表上有IX锁,所以会阻塞无法执行。类似的如果一个事务对一行数据加上共享锁

1
sql复制代码select * from user where id=1 lock in share mode;

就会给对应的表加上IS锁。这时候如果执行

1
sql复制代码LOCK TABLES user WRITE;

也会因为表上有IS锁而阻塞。

面试官:那我们来聊聊行锁吧,InnoDB支持哪些行锁?

我:有记录锁(Record Lock)、间隙锁(Gap Lock)、Next-Key Lock

面试官:假设有一张表,表里有10条记录,还有个字段user_id,并且user_id是普通索引。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
sql复制代码+----+---------+
| id | user_id |
+----+---------+
| 1 | 10 |
| 2 | 20 |
| 3 | 30 |
| 4 | 40 |
| 5 | 50 |
| 6 | 60 |
| 7 | 70 |
| 8 | 80 |
| 9 | 90 |
| 10 | 100 |
+----+---------+

如果事务A执行:

1
sql复制代码SELECT * FROM user WHERE user_id=50 FOR UPDATE;

紧接着事务B执行下面的sql会发生什么?:

1
sql复制代码INSERT INTO user set user_id=45;

我:阻塞。

面试官:能说下原因吗?

我:因为InnoDB的Next-Key Lock算法,不仅仅会锁住user_id=50这条记录,还会锁住50左右的间隙。Next-Key Lock锁定的范围是左开右闭的,那么理论上最终(40,50],(50,60]的区间数据会被锁定。

由于要插入的45在40-50之间,所以就会发生阻塞。

面试官:那按照你说的区间锁法,是包含60这条数据的是吧,所以如果插入一条60的数据,就会发生阻塞?

1
sql复制代码INSERT INTO user set user_id=60;

我:其实不会,这里涉及到next key lock的优化,在等值查询中,向右遍历时且最后一个值不满足等值条件的时候,Next-Key Lock会退化为Gap Lock,所以对于(50,60]这个区间最终会降级为(50,60),那么60这条数据就不会被锁住,是可以插入成功的。

面试官:那40这条数据是不在这个锁区间的,所以可以插入40这条数据?

1
sql复制代码INSERT INTO user set user_id=40;

我:其实也不会,40这条数据的插入也会阻塞,首先对于非聚集索引user_id它的叶子节点一定是排序的,大概就像(40,4),(50,5)这样,其次因为主键id是自增的,那么对于再插入一条40的数据,它的主键id一定是大于4的,就目前10条数据来说,下一次插入的id肯定是11,那(40,11)这条数据肯定是要在(40,4)后面的,这样的话,就落入到了间隙锁中,所以会阻塞,其实上面60那条数据可以插入也是同样的道理。

面试官:那如果我一开始不用select for update了,而用select lock in share mode,那么所有的插入会有什么变化吗?

我:没有变化,还是一样,因为插入需要X锁,X锁和任何锁都互斥。

面试官:如果user_id不是普通索引而是唯一索引,那会有什么变化?

我:当索引是唯一索引时,那么就会发生降级,Next Key Lock会降级成Record Lock,最终只会锁住50这条记录。

面试官:如果user_id没有索引怎么办?

我:那就所有的记录都会锁上,任何的插入都会阻塞。

面试官:那你知道为什么要有间隙锁这个东西吗?

我:为了解决幻读。比如当事务A执行以下查询时:

1
sql复制代码SELECT * FROM user WHERE id>=9 for update

应该返回两条记录(id=9和id=10),这时候如果另一个事务B执行

1
sql复制代码INSERT INTO user set user_id=110;

在没有间隙锁的情况下,那么事务A再次查询会发现多了一条记录,就出现了幻读,如果有了间隙锁,那么[9,+∞)这个区间都会被锁住,事务B的插入就会阻塞。但是只有在事务的隔离级别设置成可重复读的时候,才支持间隙锁。

面试官:如果某个上了锁的事务一直不提交,那么后面需要获取相关锁的事务就会阻塞,这样会有什么问题?

我:如果阻塞的事务越来越多,那么阻塞的线程也会越来越多,严重时会造成连接池满了,mysql不能提供服务了。但是InnoDB支持阻塞超时后,会自动放弃这个等待锁的sql命令,这个值默认是50s。

1
2
3
4
5
sql复制代码+--------------------------+-------+
| Variable_name | Value |
+--------------------------+-------+
| innodb_lock_wait_timeout | 50 |
+--------------------------+-------+

面试官看了看我:那你知道AUTO-INC Locking吗?

我:知道,自增长锁,在InnoDB引擎中,每个表都会维护一个表级别的自增长计数器,当对表进行插入的时候,会通过以下的命令来获取当前的自增长的值。

1
sql复制代码SELECT MAX(auto_inc_col)  FROM user FOR UPDATE;

插入操作会在这个基础上加1得到即将要插入的自增长id。

面试官:我们知道事务中的锁是在事务提交后才释放的,那么在更新自增长id后,当事务没来及提交,其它的事务获取自增长id就要等待吗?这样的话效率是不是有点低?

我:不用等待的,为了提高插入性能,自增长的锁不会等到事务提交之后才释放,而是在相关插入sql语句完成后立刻就释放的,这也是为什么一些事务回滚之后,发现id不连续的原因:

1
2
3
4
5
6
7
8
9
10
sql复制代码select * from user;
+----+---------+
| id | user_id |
+----+---------+
| .. | .. |
| 9 | 90 |
| 10 | 100 |
| 12 | 120 |
+----+---------+
# 11那条数据回滚了,但是id也被消耗了,id不会回滚。

面试官:虽然AUTO-INC Locking可以不用等事务提交就释放,但是在并发的时候,因为AUTO-INC Locking本身会对自增id上锁的,还是会影响效率,这个该怎么解决?

我:现在InnoDB支持互斥量的方式来实现自增长,通过互斥量可以对内存中的计数器进行累加操作,比AUTO-INC Locking要快些。

面试官:那你知道死锁吗?

我:知道。

面试官:什么情况下会出现死锁?

我:死锁出现的条件就是请求和保持,就是每一方都保持着对方的需要的资源同时请求对方占用的资源。比如:

事务1先锁id=1这条数据,紧接着事务2锁住id=2这条数据,然后事务1再次尝试锁住id=2这条数据,但是发现被事务2占着在,所以此时事务1会阻塞,最后事务2尝试获取id=1的锁,但是发现被事务1占着在,所以也会阻塞,那么此时就陷入僵局了,这就是死锁。

面试官:那如何解决死锁问题呢?

我:

  1. InnoDB提供锁超时的功能,当一个事务获取锁超时之后会自动放弃,另一个事务就可以执行。
1
2
3
4
5
sql复制代码+--------------------------+-------+
| Variable_name | Value |
+--------------------------+-------+
| innodb_lock_wait_timeout | 50 |
+--------------------------+-------+
  1. 可以让每次更新都按照约定的顺序去更新,这样也可以避免死锁。

  1. 通过死锁检测来提前判断,InnoDB默认是开启死锁检测的。
1
2
3
4
5
sql复制代码+------------------------+-------+
| Variable_name | Value |
+------------------------+-------+
| innodb_deadlock_detect | ON |
+------------------------+-------+

InnoDB通过等待图的方式来进行死锁检测的,这要求存储锁的信息链表和事务的等待链表,然后通过链表构造一张等待图,每次获取锁的时候会通过等待图来判断是否会造成死锁。

面试官看了看自己的劳力士:(看来今天锁不住这个小伙子了,时间也差不多了)那你在这稍等下,我喊下hr。

我:好的好的(终于结束了)。

往期精彩:

  • 一文搞懂回滚和持久化
  • redis IO模型的演进
  • 如何构建一个健壮性服务

微信搜【假装懂编程】,领取电子书,分享大厂面试经验

本文转载自: 掘金

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

实战!日志打印的15个好建议

发表于 2021-09-23

前言

大家好,我是捡田螺的小男孩。日志是快速定位问题的好帮手,是撕逼和甩锅的利器!打印好日志非常重要。今天我们来聊聊日志打印的15个好建议~

  • 公众号:捡田螺的小男孩
  • 我的github地址,感谢给个star
  1. 选择恰当的日志级别

常见的日志级别有5种,分别是error、warn、info、debug、trace。日常开发中,我们需要选择恰当的日志级别,不要反手就是打印info哈~

  • error:错误日志,指比较严重的错误,对正常业务有影响,需要运维配置监控的;
  • warn:警告日志,一般的错误,对业务影响不大,但是需要开发关注;
  • info:信息日志,记录排查问题的关键信息,如调用时间、出参入参等等;
  • debug:用于开发DEBUG的,关键逻辑里面的运行时数据;
  • trace:最详细的信息,一般这些信息只记录到日志文件中。
  1. 日志要打印出方法的入参、出参

我们并不需要打印很多很多日志,只需要打印可以快速定位问题的有效日志。有效的日志,是甩锅的利器!

哪些算得的上有效关键的日志呢?比如说,方法进来的时候,打印入参。再然后呢,在方法返回的时候,就是打印出参,返回值。入参的话,一般就是userId或者bizSeq这些关键信息。正例如下:

1
2
3
4
5
6
ini复制代码public String testLogMethod(Document doc, Mode mode){
log.debug(“method enter param:{}”,userId);
String id = "666";
log.debug(“method exit param:{}”,id);
return id;
}
  1. 选择合适的日志格式

理想的日志格式,应当包括这些最基本的信息:如当前时间戳(一般毫秒精确度)、日志级别,线程名字等等。在logback日志里可以这么配置:

1
2
3
4
5
xml复制代码<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{HH:mm:ss.SSS} %-5level [%thread][%logger{0}] %m%n</pattern>
</encoder>
</appender>

如果我们的日志格式,连当前时间都沒有记录,那连请求的时间点都不知道了?

  1. 遇到if…else…等条件时,每个分支首行都尽量打印日志

当你碰到if…else…或者switch这样的条件时,可以在分支的首行就打印日志,这样排查问题时,就可以通过日志,确定进入了哪个分支,代码逻辑更清晰,也更方便排查问题了。

正例:

1
2
3
4
5
6
7
c复制代码if(user.isVip()){
log.info("该用户是会员,Id:{},开始处理会员逻辑",user,getUserId());
//会员逻辑
}else{
log.info("该用户是非会员,Id:{},开始处理非会员逻辑",user,getUserId())
//非会员逻辑
}

5.日志级别比较低时,进行日志开关判断

对于trace/debug这些比较低的日志级别,必须进行日志级别的开关判断。

正例:

1
2
3
4
sql复制代码User user = new User(666L, "公众号", "捡田螺的小男孩");
if (log.isDebugEnabled()) {
log.debug("userId is: {}", user.getId());
}

因为当前有如下的日志代码:

1
bash复制代码logger.debug("Processing trade with id: " + id + " and symbol: " + symbol);

如果配置的日志级别是warn的话,上述日志不会打印,但是会执行字符串拼接操作,如果symbol是对象,
还会执行toString()方法,浪费了系统资源,执行了上述操作,最终日志却没有打印,因此建议加日志开关判断。

  1. 不能直接使用日志系统(Log4j、Logback)中的 API,而是使用日志框架SLF4J中的API。

SLF4J 是门面模式的日志框架,有利于维护和各个类的日志处理方式统一,并且可以在保证不修改代码的情况下,很方便的实现底层日志框架的更换。

正例:

1
2
3
4
arduino复制代码import org.slf4j.Logger; 
import org.slf4j.LoggerFactory;

private static final Logger logger = LoggerFactory.getLogger(TianLuoBoy.class);
  1. 建议使用参数占位{},而不是用+拼接。

反例:

1
bash复制代码logger.info("Processing trade with id: " + id + " and symbol: " + symbol);

上面的例子中,使用+操作符进行字符串的拼接,有一定的性能损耗。

正例如下:

1
bash复制代码logger.info("Processing trade with id: {} and symbol : {} ", id, symbol);

我们使用了大括号{}来作为日志中的占位符,比于使用+操作符,更加优雅简洁。并且,相对于反例,使用占位符仅是替换动作,可以有效提升性能。

  1. 建议使用异步的方式来输出日志。

  • 日志最终会输出到文件或者其它输出流中的,IO性能会有要求的。如果异步,就可以显著提升IO性能。
  • 除非有特殊要求,要不然建议使用异步的方式来输出日志。以logback为例吧,要配置异步很简单,使用AsyncAppender就行
1
2
3
ini复制代码<appender name="FILE_ASYNC" class="ch.qos.logback.classic.AsyncAppender">
<appender-ref ref="ASYNC"/>
</appender>
  1. 不要使用e.printStackTrace()

反例:

1
2
3
4
5
php复制代码try{
// 业务代码处理
}catch(Exception e){
e.printStackTrace();
}

正例:

1
2
3
4
5
php复制代码try{
// 业务代码处理
}catch(Exception e){
log.error("你的程序有异常啦",e);
}

理由:

  • e.printStackTrace()打印出的堆栈日志跟业务代码日志是交错混合在一起的,通常排查异常日志不太方便。
  • e.printStackTrace()语句产生的字符串记录的是堆栈信息,如果信息太长太多,字符串常量池所在的内存块没有空间了,即内存满了,那么,用户的请求就卡住啦~
  1. 异常日志不要只打一半,要输出全部错误信息

反例1:

1
2
3
4
5
6
php复制代码try {
//业务代码处理
} catch (Exception e) {
// 错误
LOG.error('你的程序有异常啦');
}
  • 异常e都没有打印出来,所以压根不知道出了什么类型的异常。

反例2:

1
2
3
4
5
6
php复制代码try {
//业务代码处理
} catch (Exception e) {
// 错误
LOG.error('你的程序有异常啦', e.getMessage());
}
  • e.getMessage()不会记录详细的堆栈异常信息,只会记录错误基本描述信息,不利于排查问题。

正例:

1
2
3
4
5
6
php复制代码try {
//业务代码处理
} catch (Exception e) {
// 错误
LOG.error('你的程序有异常啦', e);
}
  1. 禁止在线上环境开启 debug

禁止在线上环境开启debug,这一点非常重要。

因为一般系统的debug日志会很多,并且各种框架中也大量使用 debug的日志,线上开启debug不久可能会打满磁盘,影响业务系统的正常运行。

12.不要记录了异常,又抛出异常

反例如下:

1
2
vbnet复制代码log.error("IO exception", e);
throw new MyException(e);
  • 这样实现的话,通常会把栈信息打印两次。这是因为捕获了MyException异常的地方,还会再打印一次。
  • 这样的日志记录,或者包装后再抛出去,不要同时使用!否则你的日志看起来会让人很迷惑。

13.避免重复打印日志

避免重复打印日志,酱紫会浪费磁盘空间。如果你已经有一行日志清楚表达了意思,避免再冗余打印,反例如下:

1
2
3
4
5
6
7
8
c复制代码if(user.isVip()){
log.info("该用户是会员,Id:{}",user,getUserId());
//冗余,可以跟前面的日志合并一起
log.info("开始处理会员逻辑,id:{}",user,getUserId());
//会员逻辑
}else{
//非会员逻辑
}

如果你是使用log4j日志框架,务必在log4j.xml中设置 additivity=false,因为可以避免重复打印日志

正例:

1
ini复制代码<logger name="com.taobao.dubbo.config" additivity="false">

14.日志文件分离

  • 我们可以把不同类型的日志分离出去,比如access.log,或者error级别error.log,都可以单独打印到一个文件里面。
  • 当然,也可以根据不同的业务模块,打印到不同的日志文件里,这样我们排查问题和做数据统计的时候,都会比较方便啦。
  1. 核心功能模块,建议打印较完整的日志

  • 我们日常开发中,如果核心或者逻辑复杂的代码,建议添加详细的注释,以及较详细的日志。
  • 日志要多详细呢?脑洞一下,如果你的核心程序哪一步出错了,通过日志可以定位到,那就可以啦。

本文转载自: 掘金

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

元数据之-血缘分析实战

发表于 2021-09-22

引入

做过大数据或者接触过数仓的同学,相信都有听到过数据治理、血缘分析的专业术语。不知道大家有没有思考过以下几个问题:

1、什么是血缘分析?主要分析什么东西?

2、为什么要做血缘分析,主要是为了解决什么痛点?做出来之后有什么价值?如何衡量这些价值?

3、如何做血缘分析?

关于第1,2个问题是需要结合每个企业实际的情况来思考,当然分析其本质就是方便数据梳理。那么本篇主要侧重于第3个问题,通过工程+方法论的方式来为读者们揭开血缘分析功能的神秘面纱。

效果展示

关于如何做血缘分析,其实每个企业的做法都大差不差,主要差别在于实现的深度。例如:有的企业是直接引用现有的开源工具,有的企业是结合自身的产品进行自研,有的企业可能只做到表级别,有的企业做到字段级别。那么本篇将会为读者们提供一种表级别粒度的分析功能,并通过可视化的方式为大家展示,当然本篇文章是属于抛砖引玉,主要是给大家提供一种思路。先为读者们展示最终效果图:

执行底层

在数仓工作职责内,大部分都是SQL化,因此血缘分析大多数都是基于SQL解析来做。当然也有非SQL的场景,不过其思想和做法都是一样的,只是API层面的调用不同而已。本篇就以SparkSQL作为一种场景举例说明。

说到SparkSQL,那么不得不先来了解下其具体的底层流程(当然Hive的解析也是一样,同样要先熟悉其底层流程)。


如上图所示:

从整体可以分为以下两个流程:

1、逻辑计划:提交sql,spark进行解析生成逻辑计划,然后进行规则绑定,优化

2、物理计划:spark将优化后的逻辑计划进行切分生成物理计划,然后进行提交,生成job任务

从每个环节进行梳理的话,可以分为以下几个流程(当然这里也只是大概介绍一下,当大家有兴趣做这一块的时候可以深入研究):

1、当我们在client端提交一段sql后,spark会通过Antlr4进行词法、语法解析来校验SQl语法(当然这里会根据g4文件进行匹配)得到AST,生成未解析的逻辑计划Unresolved Logical Plan。此时的计划还不知道字段类型和表数据等信息

2、spark通过analyzer结合catalog对未解析的逻辑计划,进行一系列的规则绑定,应用数据信息,生成Resolved Logical Plan,此时的计划也是按照原来的节点原封不动进行绑定,并没有做任何的优化.

3、Spark中的Optimizer在保证数据结果正确的前提下,对解析后的逻辑计划进行优化,例如通常所提到的谓词下推、列裁剪、join优化、算子组合等多种优化手段会在该阶段使用,最后得到Optimized Logical Plan

4、当得到优化后的逻辑计划后,会通过Planner将各种策略应用到Logical Plan上,选择最优的物理计划进行提交执行。

注意:通过对上图sql流程的简单介绍,其中关于sparksql中的CBO,AE优化,相信读者们也有一个更清晰的理解。

源码分析

本篇选择用sparksql作为解析对象,其主要原因在于目前大多数企业在sparksql上的使用已经比较广泛且成熟,当然hive的解析也可以仿照本篇思想进行。那么结合上面的底层执行流程的理解,我们只需要解析到表依赖关系即可,所以只需要获取到Unresolved Logical Plan阶段的信息即可。那么问题来了,我们该从何处下手呢?这个时候我们可以先看下sparksql是如何做到的,那么不得不看下源码了。

首先我们知道提交sql的入口是通过SparkSession.sql方法进行执行的,那么就进入到该方法内查看。


其实你看到这一步的话,就已经可以了,直接调用parsePlan方法获取到Plan。不过我们这里再继续深入一下。


这里需要查看下有哪些实现类

如果你继续深入的话,会看到AbstractSqlParser.parse方法是具体解析sql生成AST的地方.

当然这也只是一个抽象类,那么我们最后会看到以下两个实现类。本篇是通过使用SparkSqlParser来实现sql解析.具体源码部分不在叙述,有兴趣的同学可以研究。

工程实现

在源码分析部分,大概引入了一下sparksql底层的解析入口。该篇作为举例,选择使用SparkSqlParser来对sparksql进行解析。当然读者们也可以使用CatalystSqlParser或者ASTBuilder直接进行解析,同时可以对比两者直接的区别。

该篇demo选用的技术主要为SparkSqlParser(spark3.X)+图数据库Neo4j。

主要实现的支持sql语法包括:

1、Create [temporary] table/view [AS] ….

2、Insert into/overwrite ….

3、CTE

sql解析伪代码示例如下图:

Neo4j代码如下图:

**注意:

1、关于Neo4j部分,大家有兴趣可以自行研究,本篇不做介绍

2、本篇仅是给读者们提供一种思路,具体实现可自行操作。授人以鱼不如授人以渔

3、关于选择Spark3.X作为解析工具,是相对于spark2.X,有部分解析语法和规则不够全面,例如[SPARK-30822][SQL]Remove semicolon at the end of a sql query。**

本文转载自: 掘金

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

centos 安装nginx 解决各种坑

发表于 2021-09-22

nginx 安装步骤

nginx 官网下载nginx安装包

image.png

上传到服务器

tar -zxvf nginx安装包

配置

./configure –prefix=/usr/local/nginx

编译

make
make install

安装ssl 和http v2模块

./configure –user=nginx –group=nginx –prefix=/usr/local/nginx –with-http_stub_status_module –with-http_ssl_module –with-http_v2_module

替换编译后的文件

cp ./objs/nginx /usr/local/nginx/sbin/

如果遇到下面的问题

解决nginx: [alert] kill(56, 1) failed (3: No such process)

/usr/local/nginx/sbin/nginx -c /usr/local/nginx/conf/nginx.conf

本文转载自: 掘金

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

1…522523524…956

开发者博客

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