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

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


  • 首页

  • 归档

  • 搜索

详解Spring Boot默认异常处理(建议收藏)

发表于 2021-10-12

本文正在参与 「掘力星计划」 ,赢取创作大礼包,挑战创作激励金。

在日常的 Web 开发中,会经常遇到大大小小的异常,此时往往需要一个统一的异常处理机制,来保证客户端能接收较为友好的提示。Spring Boot 同样提供了一套默认的异常处理机制,本节将对它进行详细的介绍。

Spring Boot 默认异常处理机制

Spring Boot 提供了一套默认的异常处理机制,一旦程序中出现了异常,Spring Boot 会自动识别客户端的类型(浏览器客户端或机器客户端),并根据客户端的不同,以不同的形式展示异常信息。

  1. 对于浏览器客户端而言,Spring Boot 会响应一个“ whitelabel”错误视图,以 HTML 格式呈现错误信息,如图

​

图1:Spring Boot 默认错误白页
  1. 对于机器客户端而言,Spring Boot 将生成 JSON 响应,来展示异常消息。
1
2
3
4
5
6
7
json复制代码{
"timestamp": "2021-07-12T07:05:29.885+00:00",
"status": 404,
"error": "Not Found",
"message": "No message available",
"path": "/m1ain.html"
}

Spring Boot 异常处理自动配置原理

Spring Boot 通过配置类 ErrorMvcAutoConfiguration 对异常处理提供了自动配置,该配置类向容器中注入了以下 4 个组件。

  • ErrorPageCustomizer:该组件会在在系统发生异常后,默认将请求转发到“/error”上。
  • BasicErrorController:处理默认的“/error”请求。
  • DefaultErrorViewResolver:默认的错误视图解析器,将异常信息解析到相应的错误视图上。
  • DefaultErrorAttributes:用于页面上共享异常信息。

下面,我们依次对这四个组件进行详细的介绍。

ErrorPageCustomizer

ErrorMvcAutoConfiguration 向容器中注入了一个名为 ErrorPageCustomizer 的组件,它主要用于定制错误页面的响应规则。

1
2
3
4
java复制代码@Bean
public ErrorPageCustomizer errorPageCustomizer(DispatcherServletPath dispatcherServletPath) {
return new ErrorPageCustomizer(this.serverProperties, dispatcherServletPath);
}

ErrorPageCustomizer 通过 registerErrorPages() 方法来注册错误页面的响应规则。当系统中发生异常后,ErrorPageCustomizer 组件会自动生效,并将请求转发到 “/error”上,交给 BasicErrorController 进行处理,其部分代码如下。

1
2
3
4
5
6
7
java复制代码@Override
public void registerErrorPages(ErrorPageRegistry errorPageRegistry) {
//将请求转发到 /errror(this.properties.getError().getPath())上
ErrorPage errorPage = new ErrorPage(this.dispatcherServletPath.getRelativePath(this.properties.getError().getPath()));
// 注册错误页面
errorPageRegistry.addErrorPages(errorPage);
}

BasicErrorController

ErrorMvcAutoConfiguration 还向容器中注入了一个错误控制器组件 BasicErrorController,代码如下。

1
2
3
4
5
6
7
java复制代码@Bean
@ConditionalOnMissingBean(value = ErrorController.class, search = SearchStrategy.CURRENT)
public BasicErrorController basicErrorController(ErrorAttributes errorAttributes,
ObjectProvider<ErrorViewResolver> errorViewResolvers) {
return new BasicErrorController(errorAttributes, this.serverProperties.getError(),
errorViewResolvers.orderedStream().collect(Collectors.toList()));
}

BasicErrorController 的定义如下。

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
java复制代码//BasicErrorController 用于处理 “/error” 请求
@Controller
@RequestMapping("${server.error.path:${error.path:/error}}")
public class BasicErrorController extends AbstractErrorController {
......
/**
* 该方法用于处理浏览器客户端的请求发生的异常
* 生成 html 页面来展示异常信息
* @param request
* @param response
* @return
*/
@RequestMapping(produces = MediaType.TEXT_HTML_VALUE)
public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {
//获取错误状态码
HttpStatus status = getStatus(request);
//getErrorAttributes 根据错误信息来封装一些 model 数据,用于页面显示
Map<String, Object> model = Collections
.unmodifiableMap(getErrorAttributes(request, getErrorAttributeOptions(request, MediaType.TEXT_HTML)));
//为响应对象设置错误状态码
response.setStatus(status.value());
//调用 resolveErrorView() 方法,使用错误视图解析器生成 ModelAndView 对象(包含错误页面地址和页面内容)
ModelAndView modelAndView = resolveErrorView(request, response, status, model);
return (modelAndView != null) ? modelAndView : new ModelAndView("error", model);
}

/**
* 该方法用于处理机器客户端的请求发生的错误
* 产生 JSON 格式的数据展示错误信息
* @param request
* @return
*/
@RequestMapping
public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
HttpStatus status = getStatus(request);
if (status == HttpStatus.NO_CONTENT) {
return new ResponseEntity<>(status);
}
Map<String, Object> body = getErrorAttributes(request, getErrorAttributeOptions(request, MediaType.ALL));
return new ResponseEntity<>(body, status);
}
......
}
1
scss复制代码Spring Boot 通过 BasicErrorController 进行统一的错误处理(例如默认的“/error”请求)。Spring Boot 会自动识别发出请求的客户端的类型(浏览器客户端或机器客户端),并根据客户端类型,将请求分别交给 errorHtml() 和 error() 方法进行处理。
返回值类型 方法声明 客户端类型 错误信息返类型
ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) 浏览器客户端 text/html(错误页面)
ResponseEntity<Map<String, Object>> error(HttpServletRequest request) 机器客户端(例如安卓、IOS、Postman 等等) JSON

换句话说,当使用浏览器访问出现异常时,会进入 BasicErrorController 控制器中的 errorHtml() 方法进行处理,当使用安卓、IOS、Postman 等机器客户端访问出现异常时,就进入error() 方法处理。

在 errorHtml() 方法中会调用父类(AbstractErrorController)的 resolveErrorView() 方法,代码如下。

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码protected ModelAndView resolveErrorView(HttpServletRequest request, HttpServletResponse response, HttpStatus status,
Map<String, Object> model) {
//获取容器中的所有的错误视图解析器来处理该异常信息
for (ErrorViewResolver resolver : this.errorViewResolvers) {
//调用错误视图解析器的 resolveErrorView 解析到错误视图页面
ModelAndView modelAndView = resolver.resolveErrorView(request, status, model);
if (modelAndView != null) {
return modelAndView;
}
}
return null;
}
1
复制代码从上述源码可以看出,在响应页面的时候,会在父类的 resolveErrorView 方法中获取容器中所有的 ErrorViewResolver 对象(错误视图解析器,包括 DefaultErrorViewResolver 在内),一起来解析异常信息。

DefaultErrorViewResolver

1
java复制代码ErrorMvcAutoConfiguration 还向容器中注入了一个默认的错误视图解析器组件 DefaultErrorViewResolver,代码如下。
1
2
3
4
5
6
java复制代码@Bean
@ConditionalOnBean(DispatcherServlet.class)
@ConditionalOnMissingBean(ErrorViewResolver.class)
DefaultErrorViewResolver conventionErrorViewResolver() {
return new DefaultErrorViewResolver(this.applicationContext, this.resources);
}

当发出请求的客户端为浏览器时,Spring Boot 会获取容器中所有的 ErrorViewResolver 对象(错误视图解析器),并分别调用它们的 resolveErrorView() 方法对异常信息进行解析,其中自然也包括 DefaultErrorViewResolver(默认错误信息解析器)。

DefaultErrorViewResolver 的部分代码如下。

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
java复制代码public class DefaultErrorViewResolver implements ErrorViewResolver, Ordered {

private static final Map<HttpStatus.Series, String> SERIES_VIEWS;

static {
Map<HttpStatus.Series, String> views = new EnumMap<>(HttpStatus.Series.class);
views.put(Series.CLIENT_ERROR, "4xx");
views.put(Series.SERVER_ERROR, "5xx");
SERIES_VIEWS = Collections.unmodifiableMap(views);
}

......

@Override
public ModelAndView resolveErrorView(HttpServletRequest request, HttpStatus status, Map<String, Object> model) {
//尝试以错误状态码作为错误页面名进行解析
ModelAndView modelAndView = resolve(String.valueOf(status.value()), model);
if (modelAndView == null && SERIES_VIEWS.containsKey(status.series())) {
//尝试以 4xx 或 5xx 作为错误页面页面进行解析
modelAndView = resolve(SERIES_VIEWS.get(status.series()), model);
}
return modelAndView;
}

private ModelAndView resolve(String viewName, Map<String, Object> model) {
//错误模板页面,例如 error/404、error/4xx、error/500、error/5xx
String errorViewName = "error/" + viewName;
//当模板引擎可以解析这些模板页面时,就用模板引擎解析
TemplateAvailabilityProvider provider = this.templateAvailabilityProviders.getProvider(errorViewName,
this.applicationContext);
if (provider != null) {
//在模板能够解析到模板页面的情况下,返回 errorViewName 指定的视图
return new ModelAndView(errorViewName, model);
}
//若模板引擎不能解析,则去静态资源文件夹下查找 errorViewName 对应的页面
return resolveResource(errorViewName, model);
}

private ModelAndView resolveResource(String viewName, Map<String, Object> model) {
//遍历所有静态资源文件夹
for (String location : this.resources.getStaticLocations()) {
try {
Resource resource = this.applicationContext.getResource(location);
//静态资源文件夹下的错误页面,例如error/404.html、error/4xx.html、error/500.html、error/5xx.html
resource = resource.createRelative(viewName + ".html");
//若静态资源文件夹下存在以上错误页面,则直接返回
if (resource.exists()) {
return new ModelAndView(new DefaultErrorViewResolver.HtmlResourceView(resource), model);
}
} catch (Exception ex) {
}
}
return null;
}
......
}

DefaultErrorViewResolver 解析异常信息的步骤如下:

  1. 根据错误状态码(例如 404、500、400 等),生成一个错误视图 error/status,例如 error/404、error/500、error/400。
  2. 尝试使用模板引擎解析 error/status 视图,即尝试从 classpath 类路径下的 templates 目录下,查找 error/status.html,例如 error/404.html、error/500.html、error/400.html。
  3. 若模板引擎能够解析到 error/status 视图,则将视图和数据封装成 ModelAndView 返回并结束整个解析流程,否则跳转到第 4 步。
  4. 依次从各个静态资源文件夹中查找 error/status.html,若在静态文件夹中找到了该错误页面,则返回并结束整个解析流程,否则跳转到第 5 步。
  5. 将错误状态码(例如 404、500、400 等)转换为 4xx 或 5xx,然后重复前 4 个步骤,若解析成功则返回并结束整个解析流程,否则跳转第 6 步。
  6. 处理默认的 “/error ”请求,使用 Spring Boot 默认的错误页面(Whitelabel Error Page)。

DefaultErrorAttributes

ErrorMvcAutoConfiguration 还向容器中注入了一个组件默认错误属性处理工具 DefaultErrorAttributes,代码如下。

1
2
3
4
5
6
java复制代码
@Bean
@ConditionalOnMissingBean(value = ErrorAttributes.class, search = SearchStrategy.CURRENT)
public DefaultErrorAttributes errorAttributes() {
return new DefaultErrorAttributes();
}

DefaultErrorAttributes 是 Spring Boot 的默认错误属性处理工具,它可以从请求中获取异常或错误信息,并将其封装为一个 Map 对象返回,其部分代码如下。

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
java复制代码public class DefaultErrorAttributes implements ErrorAttributes, HandlerExceptionResolver, Ordered {
......
@Override
public Map<String, Object> getErrorAttributes(WebRequest webRequest, ErrorAttributeOptions options) {
Map<String, Object> errorAttributes = getErrorAttributes(webRequest, options.isIncluded(Include.STACK_TRACE));
if (!options.isIncluded(Include.EXCEPTION)) {
errorAttributes.remove("exception");
}
if (!options.isIncluded(Include.STACK_TRACE)) {
errorAttributes.remove("trace");
}
if (!options.isIncluded(Include.MESSAGE) && errorAttributes.get("message") != null) {
errorAttributes.remove("message");
}
if (!options.isIncluded(Include.BINDING_ERRORS)) {
errorAttributes.remove("errors");
}
return errorAttributes;
}

private Map<String, Object> getErrorAttributes(WebRequest webRequest, boolean includeStackTrace) {
Map<String, Object> errorAttributes = new LinkedHashMap<>();
errorAttributes.put("timestamp", new Date());
addStatus(errorAttributes, webRequest);
addErrorDetails(errorAttributes, webRequest, includeStackTrace);
addPath(errorAttributes, webRequest);
return errorAttributes;
}
......
}
1
java复制代码在 Spring Boot 默认的 Error 控制器(BasicErrorController)处理错误时,会调用 DefaultErrorAttributes 的 getErrorAttributes() 方法获取错误或异常信息,并封装成 model 数据(Map 对象),返回到页面或 JSON 数据中。该 model 数据主要包含以下属性:
  • timestamp:时间戳;
  • status:错误状态码
  • error:错误的提示
  • exception:导致请求处理失败的异常对象
  • message:错误/异常消息
  • trace: 错误/异常栈信息
  • path:错误/异常抛出时所请求的URL路径

所有通过 DefaultErrorAttributes 封装到 model 数据中的属性,都可以直接在页面或 JSON 中获取。

好了、今天就分享到这儿吧,我是小奥、下期见~~

本文转载自: 掘金

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

看动画学算法之 栈stack 简介 栈的构成 栈的实现

发表于 2021-10-12

本文已参与 「掘力星计划」 ,赢取创作大礼包,挑战创作激励金

「欢迎在评论区讨论,掘金官方将在掘力星计划活动结束后,在评论区抽送100份掘金周边,抽奖详情见活动文章」

简介

栈应该是一种非常简单并且非常有用的数据结构了。栈的特点就是先进后出FILO或者后进先出LIFO。

实际上很多虚拟机的结构都是栈。因为栈在实现函数调用中非常的有效。

今天我们一起来看学习一下栈的结构和用法。

栈的构成

栈一种有序的线性表,只能在一端进行插入或者删除操作。这一端就叫做top端。

定义一个栈,我们需要实现两种功能,一种是push也就是入栈,一种是pop也就是出栈。

当然我们也可以定义一些其他的辅助功能,比如top:获取栈上最顶层的节点。isEmpty:判断栈是否为空。isFull:判断栈是否满了之类。

先看下入栈的动画:

再看下出栈的动画:

栈的实现

具有这样功能的栈是怎么实现呢?

一般来说栈可以用数组实现,也可以用链表来实现。

使用数组来实现栈

如果使用数组来实现栈的话,我们可以使用数组的最后一个节点作为栈的head。这样在push和pop栈的操作的时候,只需要修改数组中的最后一个节点即可。

我们还需要一个topIndex来保存最后一个节点的位置。

实现代码如下:

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
java复制代码public class ArrayStack {

//实际存储数据的数组
private int[] array;
//stack的容量
private int capacity;
//stack头部指针的位置
private int topIndex;

public ArrayStack(int capacity){
this.capacity= capacity;
array = new int[capacity];
//默认情况下topIndex是-1,表示stack是空
topIndex=-1;
}

/**
* stack 是否为空
* @return
*/
public boolean isEmpty(){
return topIndex == -1;
}

/**
* stack 是否满了
* @return
*/
public boolean isFull(){
return topIndex == array.length -1 ;
}

public void push(int data){
if(isFull()){
System.out.println("Stack已经满了,禁止插入");
}else{
array[++topIndex]=data;
}
}

public int pop(){
if(isEmpty()){
System.out.println("Stack是空的");
return -1;
}else{
return array[topIndex--];
}
}
}

使用动态数组来实现栈

上面的例子中,我们的数组大小是固定的。也就是说stack是有容量限制的。

如果我们想构建一个无限容量的栈应该怎么做呢?

很简单,在push的时候,如果栈满了,我们将底层的数组进行扩容就可以了。

实现代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码public void push(int data){
if(isFull()){
System.out.println("Stack已经满了,stack扩容");
expandStack();
}
array[++topIndex]=data;
}

//扩容stack,这里我们简单的使用倍增方式
private void expandStack(){
int[] expandedArray = new int[capacity* 2];
System.arraycopy(array,0, expandedArray,0, capacity);
capacity= capacity*2;
array= expandedArray;
}

当然,扩容数组有很多种方式,这里我们选择的是倍增方式。

使用链表来实现

除了使用数组,我们还可以使用链表来创建栈。

使用链表的时候,我们只需要对链表的head节点进行操作即可。插入和删除都是处理的head节点。

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
java复制代码public class LinkedListStack {

private Node headNode;

class Node {
int data;
Node next;
//Node的构造函数
Node(int d) {
data = d;
}
}

public void push(int data){
if(headNode == null){
headNode= new Node(data);
}else{
Node newNode= new Node(data);
newNode.next= headNode;
headNode= newNode;
}
}

public int top(){
if(headNode ==null){
return -1;
}else{
return headNode.data;
}
}

public int pop(){
if(headNode ==null){
System.out.println("Stack是空的");
return -1;
}else{
int data= headNode.data;
headNode= headNode.next;
return data;
}
}

public boolean isEmpty(){
return headNode==null;
}
}

本文的代码地址:

learn-algorithm

本文已收录于 www.flydean.com/10-algorith…

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

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

本文转载自: 掘金

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

一文搞懂python的内建函数,自己添加一个print函数

发表于 2021-10-12

小知识,大挑战!本文正在参与“ 程序员必备小知识

本文同时参与 「掘力星计划」 ,赢取创作大礼包,挑战创作激励金

写python的同学应该都用过print函数,这个函数我们没有定义为什么可以调用?

答案就是因为 print 是内建函数,python的内建函数式都是常用的工具函数,也是系统内置的函数,今天就说下内建函数,结合例子看下,废话不多说,开始

1、什么是内建函数?

python内建函数指的是python自带的函数,这种函数不需要定义,并且不同的内建函数具有不同的功能,可以直接使用。

1
2
3
4
5
6
7
8
9
10
python复制代码#!/usr/bin/python2.6
# -*- coding: utf-8 -*-
import builtins
def test():
  print("dsada")
if __name__ == '__main__':
  #   增加内建函数
  builtins.__dict__["testFunc"] = test
  testFunc()
  pass

2、内置的内建函数多有哪些?

官方的文档说明链接:docs.python.org/3.9/library…

这里我截图了函数,可以做一个概览,看名字也能猜出这些函数都是做什么的

image-20211010222503993.png

对上面的函数进行分组:

数学类:sum(),pow(),sum(),round()

随机类:choice(),random(),seed(),shuffle(),uniform()

数字类:abs(),min(),max(),divmod(),ascii()

系统类:xxxattr,xxxmethod,

数据类型:set(),map(),tuple(),list(),bool(),int(),str(),

综合类:其他的可以归于此类

3、特殊函数说明

3.1 exec

格式:exec obj

obj对象可以是字符串(如单一语句、语句块),文件对象,也可以是已经由compile预编译过的代码对象。

举个例子

1
python复制代码    exec( "print('香菜')")

3.2 eval

格式:eval( obj[, globals=globals(), locals=locals()] )

obj可以是字符串对象或者已经由compile编译过的代码对象。globals和locals是可选的,分别代表了全局和局部名称空间中的对象,其中globals必须是字典,而locals是任意的映射对象。

1
2
3
python复制代码x = 3
print(eval('3*x'))
​

输出结果是9 ,eval 可以引用上下文

3.3 compile

格式:compile( str, file, type )

compile语句是从type类型(包括’eval’: 配合eval使用,’single’: 配合单一语句的exec使用,’exec’: 配合多语句的exec使用)中将str里面的语句创建成代码对象。file是代码存放的地方,通常为”。

compile语句的目的是提供一次性的字节码编译,就不用在以后的每次调用中重新进行编译了。

compile()函数将string编译为代码对象,编译生成的代码对象接下来被exec语句执行,接着能利用eval()函数对其进行求值。filename参数应是代码从其中读出的文件名。如果内部生成文件名,filename参数值应是相应的标识符。kind参数指定string参数中所含代码的类别

3.4 globals和locals

globals()会以字典类型返回当前位置的全部全局变量。

locals()以字典类型返回当前位置的全部局部变量。

对于函数, 方法, lambda 函式, 类, 以及实现了 call 方法的类实例, 它都返回 True。

4、自己增加一个内置函数

在开发过程有些函数经常调用,但是每次都要导包什么的还挺麻烦的,就想着能不能把某几个工具函数加入到内建函数,只要有问题,其他人就会有同样的问题,其实解决的办法很简单,就是在builtins 中dict中添加函数就可以了,下面是个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
python复制代码#!/usr/bin/env python
# encoding: utf-8
​
"""
#Author: 香菜
@time: 2021/10/10 0010 下午 10:22
"""
import builtins
def test():
   print("dsada")
if __name__ == '__main__':
   #   增加内建函数
   builtins.__dict__["testFunc"] = test
   testFunc()
   pass
​

5、总结

python的内置函数并没有什么特殊的,只不过是系统提供的一些工具方法,实现也都很简单,记住这些常用的工具方法,在开发中一定可以事半功倍,

本文转载自: 掘金

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

实战!拿着造假的简历领了人生中第一个需求 【评论区抽掘金周边

发表于 2021-10-12

本文已参与「掘力星计划」,赢取创作大礼包,挑战创作激励金。

🧧 评论抽奖

  • 抽奖礼物:100份,掘金官方提供,包括掘金徽章、拖鞋、马克杯、帆布袋等,随机发放
  • 抽奖时间:「掘力星计划」活动结束后,预计3个工作日内,官方将在所有符合规则的活动文章评论区抽出
  • 抽奖方式:掘金官方随机抽奖+人工核实
  • 评论内容:与文章内容相关的评论、建议、讨论等,「踩踩」「学习了」等泛泛类的评论无法获奖

🎀 前言

最近有一个朋友,拿着包装的简历去到了公司干货,虽然不是一个大厂,但是也领到了自己人生中第一个需求,虽然说这个需求我看起来不是很难,但是对于我朋友那种自学转行的人来说还是有一定难度的,这个需求我们来看看是什么需求把,其实也很简单:**利用java代码根据文字生成随机浅色背景的图片,而且字体也要可变换**。


我滴乖乖,这个需求给一个刚进来公司的人直接就给👨整不会了。那么接下来看看我是怎么做这个需求的吧,其实就几个关键字:**根据文字生成图片、字体可变、浅色背景**。

💎代码

话不多说,我们直接开始撸码。首先我们先在D盘创建一个`name.txt`的文件,我们等会需要将这里面的文字读出来生成图片。

image.png

读取文件中的名字

接下来我们要写一个方法来读取文件中的名字。我们利用一个InputStreamReader去读取,然后返回一个List。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java复制代码    /**
* @Description: 将文件中的文字一行一行读取出来并存放在List中返回
* @Param: [filename] 文件名
* @return java.util.List<java.lang.String>
*/
public static List<String> readFileByLine(String filename) throws IOException, FileNotFoundException {
List<String> stringList=new ArrayList<String>();
File file=new File(filename);
InputStreamReader isr=new InputStreamReader(new FileInputStream(file),"UTF-8");
BufferedReader reader=new BufferedReader(isr);
String tmp;
while((tmp=reader.readLine())!=null){
stringList.add(tmp);
}
reader.close();
return stringList;
}

生成图片

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
java复制代码
/**
* @Description: 生成图片的方法
* @Param: [string] 生成图片的文字内容
* @return void
*/
public static void generateImg(String string) throws IOException{
// 设置图片宽高
int width=400;
int height=400;
// 设置图片的路径
String filename="D:/"+string+".jpg";
System.out.println(filename);
File file=new File(filename);
// 不需要引入外部字体
Font font=new Font("黑体",Font.BOLD,60);
// 引入外部字体
//Font font = getSelfDefinedFont("E:\PangMenZhengDaoBiaoTiTi\思源宋体.ttf");
BufferedImage bi =new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
Graphics2D g2=(Graphics2D) bi.getGraphics();
g2.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
g2.setBackground(getRandomColor());
g2.clearRect(0, 0, width, height);
g2.setFont(font);
FontMetrics fm = g2.getFontMetrics(font);
int textWidth = fm.stringWidth(string);
g2.setPaint(new Color(0,0,128));
// 设置图片水平居中
int widthX = (width - textWidth) / 2;
// y设置高度,目前还没有研究出居中的方法
g2.drawString(string, widthX , 220);
ImageIO.write(bi,"jpg", file);
}
这里需要注意的是:
  1. 我这里有两种方式,第一种是不引入外部字体,可以直接使用内置字体。
  2. 我们可以自己下载字体,然后通过绝度路径的方式进行引入。字体文件格式一定要是ttf,其他我试了一下是不生效的,还有一定要用没有版权或者可以免费商用的字体。
  3. 我这里做了水平居中,但是垂直居中还没有研究出来,只可以根据实际效果自己慢慢调整。

生成浅色背景

其实生成浅色背景我能想到的是利用rgb在一定范围内随机生成来生成一个浅色背景。
1
2
3
4
5
6
7
8
9
10
java复制代码/**
* @Description: 设置随机浅色颜色
* @Param: []
* @return java.awt.Color
*/
private static Color getRandomColor() {
Random random=new Random();
// 我这边设置了返回浅色的图片,排除了深色
return new Color(random.nextInt(255)%(255-230+1) + 200,random.nextInt(255)%(255-230+1) + 200,random.nextInt(255)%(255-230+1) + 200);
}

写主方法

主体代码写完啦,接下来我们来写主方法进行测试。
1
2
3
4
5
java复制代码String fileName="D:/name.txt";
List<String> nameList = readFileByLine(fileName);
for (int i = 0; i < nameList.size(); i++) {
generateImg(nameList.get(i));
}

🎉测试

image.png

image.png

还可以把!嘻嘻

本文转载自: 掘金

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

Sentinel全系列之六 —— 滑动窗口在sentinel

发表于 2021-10-12
  1. 滑动窗口简介

  当下主流的限流方法主要有滑动窗口、和漏桶、令牌桶,而在 Sentinel 中主要使用了滑动窗口作为限流算法。下图参考于blog.csdn.net/weixin_4331…

  我们会发现,虽然滑动窗口在流量处理上分布不均匀,但是在突发的大流量面前,他能够最为从容的应对,并且他拥有最为轻便的配置调整性,因此对于电商抢购,商品浏览等场景的处理上,滑动窗口无疑是最优的。

  滑动窗口算法,其实理解起来不难,滑动窗口更多的是一种思想,在拥塞控制和限流上被广泛使用,话不多说,直接先上一道题玩一玩。加深印象。

题目:

  给定你一个整数数组,给定一个给定整数n,让你求出这个数组中连续n个数相加的最大值。

  本题是一个滑动窗口的典型例题,大致思路就是,维护一个窗口,窗口会沿着数组向前滑动,窗口长度为n,窗口会定时统计和更新数据,在这道题里,数据就是滑动窗口内部的数组的和。如下图所示:

image.png

解题代码

  代码相对比较简单,此处不过多介绍,主要就是了解滑动窗口是个什么玩意,代码如下:

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
java复制代码public class WindowDemo {

public static void solution(int n){
//这里模拟了一个滑动窗口数据模拟的过程,WindowData代表滑窗内维护的数据
int WindowData = 0;
if(n<=0)
n=2;
//在该数据上滑动
int a[] = {-1,2,3,7,-10,6};

//初始化,默认n为2
for(int i = 0;i < n; i++)
WindowData = WindowData+a[i];

System.out.println("窗内数据为:"+WindowData);

int max = WindowData;

//模拟滑动统计的过程
for(int i = n ; i < a.length ; i++){
//向前滑动一格
WindowData = WindowData+a[i]-a[i-n];

System.out.println("窗内数据为:"+WindowData);

//更新最大值
if(max < WindowData)
max = WindowData;

}

System.out.println("最大值为"+max);
}

public static void main(String[] args) {
solution(2);

}
}

输出结果

image.png

2.滑动窗口在sentinel中的形式

  进入源码分析之前,一定要搞懂的,是滑动窗口在sentinel应用中的形式和使用方式,以及一些关键名词的解释。下面不多说,直接结合源码进行分析。

2.1 样本窗口 WindowWrap

  在 sentinel 中,整个滑动窗口,可以理解成一个在时间轴上滑动的窗口,一个滑动窗口会被拆分成许多的时间样本窗口,样本窗口的数量默认是 10 个,每个样本窗口,会被分配作为某一段时间的数据统计(请求通过数目,线程数统计等),随着时间的推移,会有新的样本窗口被创建,也会有老的窗口被删除。

  在 sentinel 中,样本窗口由一个 WindowWrap 类表示,内部含有窗口开始时间(窗口被分配到的某个时间段的开始时间),窗口数据,窗口分配到的时间段长度(窗口跨越了多少时间),源码如下:(可以先跳过阅读)

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
java复制代码
public class WindowWrap<T> {


//窗口分配到的时间段长度
private final long windowLengthInMs;

//窗口开始时间
private long windowStart;

//窗口数据
private T value;

//构造方法
public WindowWrap(long windowLengthInMs, long windowStart, T value) {
this.windowLengthInMs = windowLengthInMs;
this.windowStart = windowStart;
this.value = value;
}


public long windowLength() {
return windowLengthInMs;
}

public long windowStart() {
return windowStart;
}

public T value() {
return value;
}

public void setValue(T value) {
this.value = value;
}


public WindowWrap<T> resetTo(long startTime) {
this.windowStart = startTime;
return this;
}

//判断是否为时间样本窗口,窗口开始时间+窗口时间长度小于当前时间
public boolean isTimeInWindow(long timeMillis) {
return windowStart <= timeMillis && timeMillis < windowStart + windowLengthInMs;
}

@Override
public String toString() {
return "WindowWrap{" +
"windowLengthInMs=" + windowLengthInMs +
", windowStart=" + windowStart +
", value=" + value +
'}';
}
}

2.2 滑动窗口 LeapArray

  刚说到样本窗口负责了某个时间段的数据统计和存储,而滑动窗口由多个样本窗口统计而成,在 sentinel 的源码中一个滑动窗口就是由一个 leapArray 维护的,下面是他的部分源码,我先看源码了解一下他是如何存储样本窗口的。

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
java复制代码
public abstract class LeapArray<T> {


//每个样本窗口的长度
protected int windowLengthInMs;
//样本窗口数
protected int sampleCount;
//毫秒为单位一个滑动窗口跨越多少时间长度
protected int intervalInMs;
//秒为单位一个滑动窗口跨越多少时间长度
private double intervalInSecond;

//样本窗口的集合,用AtomicReferenceArray存储,长度为sampleCount(样本窗口的数目)
protected final AtomicReferenceArray<WindowWrap<T>> array;

private final ReentrantLock updateLock = new ReentrantLock();
//进行参数的初始化
public LeapArray(int sampleCount, int intervalInMs) {
AssertUtil.isTrue(sampleCount > 0, "bucket count is invalid: " + sampleCount);
AssertUtil.isTrue(intervalInMs > 0, "total time interval of the sliding window should be positive");
AssertUtil.isTrue(intervalInMs % sampleCount == 0, "time span needs to be evenly divided");

this.windowLengthInMs = intervalInMs / sampleCount;
this.intervalInMs = intervalInMs;
this.intervalInSecond = intervalInMs / 1000.0;
this.sampleCount = sampleCount;

this.array = new AtomicReferenceArray<>(sampleCount);
}

//...省略其他代码

}

  从这段代码中可以看出,样本窗口在 LeapArray 中,用一个长度为 sampleCount 的 AtomicReferenceArray 存储。这个大概就是滑动窗口在 sentinel 中的表现形式,因此滑动窗口其实更多的是思想,具体如何使用和实现,可以有多种形式。

2.3 样本窗口数据 MetricBucket

  前面我们看到,样本窗口是用来统计某一段时间段数据的,刚在前面的源码阅读中,发现data定义的是泛型,那么他的内部数据是如何存储的呢,其实在 sentinel 源码中,大部分使用的是 MetricBucket 来进行存储。

1
kotlin复制代码private final LeapArray<MetricBucket> data;

  该类中使用了LongAdder[]类型来对当前请求通过数进行了一个计算,对于 LongAdder
感兴趣的可以上网进行查看,此处不过多介绍。下面代码只展示了对请求通过数目的一个统计,部分源码如下

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
java复制代码
public class MetricBucket {

//计数器,统计各中events的数目
private final LongAdder[] counters;

private volatile long minRt;

//构造方法
public MetricBucket() {
MetricEvent[] events = MetricEvent.values();
this.counters = new LongAdder[events.length];
for (MetricEvent event : events) {
counters[event.ordinal()] = new LongAdder();
}
initMinRt();
}

//...省略部分代码

public long get(MetricEvent event) {
return counters[event.ordinal()].sum();
}

//往计数器里的增加PASS的数量
public MetricBucket add(MetricEvent event, long n) {
counters[event.ordinal()].add(n);
return this;
}
//获得目前时间段通过的请求数目,用于跟规则比较,判断是否限流
public long pass() {
return get(MetricEvent.PASS);
}
//新的请求通过了,在此处添加统计
public void addPass(int n) {
add(MetricEvent.PASS, n);
}

//...省略部分代码

}
  1. 滑动窗口进行数据统计的全过程

  上述讲解了一下滑动窗口在 sentinel 中的大致存储形式,想必大家在心里也有了 sentinel 的滑动窗口如何存储的有了一定的雏形,但是光存没用呀,现在需要考虑的是,某个时间段来了个请求,如何判断这个请求位于哪个样本窗口,他的信息数据往哪进行统计,他所在的样本窗口的时间段是否请求过多,滑动窗口如何滑起来,这是我们后续考虑的重点。

  下面将为大家展示一下 sentinel 如何往某个时间样本窗口里增加请求通过数

3.1 数据统计全过程

  阅读过前面文章的小伙伴都知道,责任链来到 StatisticSlot 的时候会先 fire 掉,如果请求走完后续操作通过了,我们就要去增加当前时间段通过的请求数,作为后续请求能否通过的参考,源码如下(来自StatisticSlot类中代码):

1
2
3
4
5
6
java复制代码//执行链路的下一个slot判断是否限流
fireEntry(context, resourceWrapper, node, count, prioritized, args);
//增加线程数
node.increaseThreadNum();
//没有报错,执行到这里了说明完全通过了,增加请求通过数,往滑动窗口里更新
node.addPassRequest(count);

  接着我们往里跟进,进入到 node.addPassRequest(count) 方法中,发现来到了DefaultNode的类中,并调用了父类 StatisticNode 的 addPassRequest() 方法,这些都不重要,重点在于,此处建立了一个ArrayMetric的计数器,用于统计一秒和一分钟的请求通过数源码如下(来自StatisticNode类):

1
2
3
4
5
6
7
java复制代码private transient volatile Metric rollingCounterInSecond = new ArrayMetric(SampleCountProperty.SAMPLE_COUNT,
IntervalProperty.INTERVAL);
@Override
public void addPassRequest(int count) {
rollingCounterInSecond.addPass(count);
rollingCounterInMinute.addPass(count);
}

  我们继续往里跟进来到 ArrayMetric 的 addPass 方法,此处我们开始步入核心部分,首先我们看到该类下有一个data变量,一看类型,好家伙再熟悉不过了,就是我们前面介绍的滑动窗口类 LeapArray ,并用 MetricBucket 类型作为他的窗口数据类型,也就是我们进入主场了,部分源码如下(来自ArrayMetric):

1
2
3
4
5
6
7
8
9
java复制代码private final LeapArray<MetricBucket> data;

@Override
public void addPass(int count) {
//获取当前时间下的样本时间窗
WindowWrap<MetricBucket> wrap = data.currentWindow();
//增加请求通过数目
wrap.value().addPass(count);
}

  致此就走完了数据统计的全过程,往指定样本窗口里增加了请求通过数。

3.2 样本窗口的创建和更新

  前面我们看到 ArrayMeric 中的 addPass 方法中,利用 LeapArray 获取了当前请求时间所属于的样本窗口,那么这个步骤是怎么来的呢,因为滑动窗口肯定是随着时间推移而滑动的,因此我们接下来就是来讨论样本窗口的创建和更新。

  继续跟进源码,此时已经进入了滑动窗口LeapArray类中的方法了

1
2
3
4
java复制代码public WindowWrap<T> currentWindow() {
//获取当前时间点
return currentWindow(TimeUtil.currentTimeMillis());
}

  再次跟进就进入了本篇最为关键的代码部分,先上源码,慢慢分析

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
java复制代码public WindowWrap<T> currentWindow(long timeMillis) {
if (timeMillis < 0) {
return null;
}
//计算当前时间所在的样本窗口id,即在计算数据LeapArray中的索引
int idx = calculateTimeIdx(timeMillis);

//计算当前样本窗口的开始时间点
long windowStart = calculateWindowStart(timeMillis);

while (true) {
// 获取到当前时间所在的样本窗口
WindowWrap<T> old = array.get(idx);
//若当前时间所在样本窗口为null,说明还不存在,创建一个
if (old == null) {
//创建一个时间窗
WindowWrap<T> window = new WindowWrap<T>(windowLengthInMs, windowStart, newEmptyBucket(timeMillis));
if (array.compareAndSet(idx, null, window)) {
// 创建成功返回窗口
return window;
} else {
Thread.yield();
}
//若当前样本窗口的开始时间与计算出的样本窗口的开始时间相同
//则说明这两个是同一个样本窗口
} else if (windowStart == old.windowStart()) {
return old;
//若当前样本窗口的开始时间点大于计算出的样本窗口的开始时间
//则说明计算出的样本窗口已经过时了,需要将原来的样本窗口替换
} else if (windowStart > old.windowStart()) {

if (updateLock.tryLock()) {
try {
return resetWindowTo(old, windowStart);
} finally {
updateLock.unlock();
}
} else {
Thread.yield();
}
} else if (windowStart < old.windowStart()) {
return new WindowWrap<T>(windowLengthInMs, windowStart, newEmptyBucket(timeMillis));
}
}
}

  下面我们对其进行一个逐句的分析

3.2.1 calculateTimeIdx(timeMillis)

  首先引入眼帘的就是这个了,这个方法看似简单其实十分重要,源码如下:

1
2
3
4
5
6
java复制代码private int calculateTimeIdx(/*@Valid*/ long timeMillis) {
//当前时间除以样本窗口的长度
long timeId = timeMillis / windowLengthInMs;
// Calculate current index so we can map the timestamp to the leap array.
return (int)(timeId % array.length());
}

  初次见到一下子也会云里雾里,他到底在干嘛,先来看一张图

image.png

  看着这张图给大家解释一下,首先上方计算的 timeId 其实就是时间轴上根据样本窗口划分的时间区间,如图所示,0-10t 代表 timeId 为 0,10t-20t 代表 timeId 为 1 ,以此类推,那么后面一步在干嘛呢,其实就是在判定这一块时间段(timeId)分配给的样本窗口的 id。比如 timeId 为0的会被分配给 a0,timeId 为 3 的也会被分配给 a0。

calculateWindowStart(timeMillis);

  继续下一个步骤,这个步骤相对简单,知道了时间段id后,他的开始时间一下子就能知道,源码如下:

1
2
3
java复制代码protected long calculateWindowStart(/*@Valid*/ long timeMillis) {
return timeMillis - timeMillis % windowLengthInMs;
}

滑动窗口的滑动和更新

  前面计算出了 idx ,那么我们就知道了应该到哪个样本窗口了,因此现在会出现三种情况,如下所示:

  1. 当前窗口为空,也就是 old = array.get(idx) 的结果为空,这说明什么呢,说明当前窗口还未被创建,这种情况出现在限流前期,前几个流量来的时候出现的情况,也就是样本窗口刚被创建的时候,源码如下:
1
2
3
4
5
6
7
8
9
10
java复制代码if (old == null) {
//创建一个时间窗
WindowWrap<T> window = new WindowWrap<T>(windowLengthInMs, windowStart, newEmptyBucket(timeMillis));
if (array.compareAndSet(idx, null, window)) {
// 创建成功返回窗口
return window;
} else {
Thread.yield();
}
}
  1. 第二种情况,当发现当前样本时间段的开始时间和原来滑动窗口中的该 idx 下的样本窗口的开始时间是相通的,那么说明,该请求的时间段仍然在这个窗口内,计数应当仍然加在这个窗口内,大家可以继续看下面这个图,发现在 50t 和 60t 之间来了一个请求,该样本窗口已经创建,且已经统计了一定数据并且没有过时,则可以直接返回该窗口。源码如下
1
2
3
4
5
6
java复制代码        //若当前样本窗口的开始时间与计算出的样本窗口的开始时间相同
//则说明这两个是同一个样本窗口
else if (windowStart == old.windowStart()) {
return old;

}

image.png

  1. 第三种情况,最关键的情况,该情况会出现窗口滑动,见下图(结合上图查看变化)

image.png

  如图所示,当 60t-70t 来了一个请求,经过上述计算,60多t 整除单位时间间隔 10t 等到 timeId 为 6,6 取余滑动窗口中的样本窗口数目,得到0,也就是a0样本窗口,然而发现的关键点是a0目前已经在 30t-40t 时被创建,也就是 windowStart > old.windowStart() 这种情况,这说明了什么,说明前面那个窗口过时了,他里面的数据已经是一秒前甚至更久了,他内部的统计数据已经用不到了,因此将他内部数据清空,并重新指定 windowStart ,也就出现了如图中所示的情况,也就意味着滑动窗口向前移动了。源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码
//若当前样本窗口的开始时间点大于计算出的样本窗口的开始时间
//则说明计算出的样本窗口已经过时了,需要将原来的样本窗口替换
else if (windowStart > old.windowStart()) {

if (updateLock.tryLock()) {
try {
return resetWindowTo(old, windowStart);
} finally {
updateLock.unlock();
}
} else {
Thread.yield();
}
}

3.2.4 统计并维护

  上面我们已经获取到了当前请求所属于的样本窗口,接下来就是更新样本窗口的数据了,这个其实不难,源码如下:

1
2
3
4
5
6
7
8
9
java复制代码private final LeapArray<MetricBucket> data;

@Override
public void addPass(int count) {
//获取当前时间下的样本时间窗
WindowWrap<MetricBucket> wrap = data.currentWindow();
//增加请求通过数目
wrap.value().addPass(count);
}

  我们知道上述代码中wrap的value其实就是 MetricBucket ,跟入进去我们发现,其实就是把 MetricBucket 里的 counters 计时器里某个 PASS 事件值增加了,代表通过的请求数增加了,是不是很简单。源码如下:

1
2
3
4
java复制代码public MetricBucket add(MetricEvent event, long n) {
counters[event.ordinal()].add(n);
return this;
}
  1. 总结

  本篇主要还是介绍了滑动窗口进行一个qps等数据的保存和维护的,后续我们将进入flowSlot的源码分析,看看限流是如何运用刚刚统计到的信息进行一个限流判定的。

本文转载自: 掘金

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

面试官问我MySQL调优,我真的是

发表于 2021-10-12

面试官:要不你来讲讲你们对MySQL是怎么调优的?

候选者:哇,这命题很大阿…我认为,对于开发者而言,对MySQL的调优重点一般是在「开发规范」、「数据库索引」又或者说解决线上慢查询上。

候选者:而对于MySQL内部的参数调优,由专业的DBA来搞。

面试官:扯了这么多,你就是想表达你不会MySQL参数调优,对吧

候选者:草,被发现了。

面试官:那你来聊聊你们平时开发的规范和索引这块,平时是怎么样的吧。

候选者:嗯,首先,我们在生产环境下,创建数据库表,都是在工单系统下完成的(那就自然需要DBA审批)。如果在创建表时检测到没有创建索引,那就会直接提示warning(:

候选者:理论上来说,如果表有一定的数据量,那就应该要创建对应的索引。从数据库查询数据需要注意的地方还是蛮多的,其中很多都是平时积累来的。比如说:

候选者:1. 是否能使用「覆盖索引」,减少「回表」所消耗的时间。意味着,我们在select 的时候,一定要指明对应的列,而不是select *

候选者:2. 考虑是否组建「联合索引」,如果组建「联合索引」,尽量将区分度最高的放在最左边,并且需要考虑「最左匹配原则」

候选者:3.对索引进行函数操作或者表达式计算会导致索引失效

候选者:4.利用子查询优化超多分页场景。比如 limit offset , n 在MySQL是获取 offset + n的记录,再返回n条。而利用子查询则是查出n条,通过ID检索对应的记录出来,提高查询效率。

面试官:嗯…

候选者:5.通过explain命令来查看SQL的执行计划,看看自己写的SQL是否走了索引,走了什么索引。通过show profile 来查看SQL对系统资源的损耗情况(不过一般还是比较少用到的)

候选者:6.在开启事务后,在事务内尽可能只操作数据库,并有意识地减少锁的持有时间(比如在事务内需要插入&&修改数据,那可以先插入后修改。因为修改是更新操作,会加行锁。如果先更新,那并发下可能会导致多个事务的请求等待行锁释放)

面试官:嗯,你提到了事务,之前也讲过了事务的隔离级别嘛,那你线上用的是什么隔离级别?

候选者:嗯,我们这边用的是Read Commit(读已提交),MySQL默认用的是Repeatable read(可重复读)。选用什么隔离级别,主要看应用场景嘛,因为隔离级别越低,事务并发性能越高。

候选者:(一般互联网公司都选择Read Commit作为主要的隔离级别)

候选者:像Repeatable read(可重复读)隔离级别,就有可能因为「间隙锁」导致的死锁问题。

候选者:但可能你已经知道,MySQL默认的隔离级别为Repeatable read。很大一部分原因是在最开始的时候,MySQL的binlog没有row模式,在read commit隔离级别下会存在「主从数据不一致」的问题

候选者:binlog记录了数据库表结构和表数据「变更」,比如update/delete/insert/truncate/create。在MySQL中,主从同步实际上就是应用了binlog来实现的(:

候选者:有了该历史原因,所以MySQL就将默认的隔离级别设置为Repeatable read

面试官:嗯,那我顺便想问下,你们遇到过类似的问题吗:即便走对了索引,线上查询还是慢。

候选者:嗯嗯,当然遇到过了

面试官:那你们是怎么做的?

候选者:如果走对了索引,但查询还是慢,那一般来说就是表的数据量实在是太大了。

候选者:首先,考虑能不能把「旧的数据」给”删掉”,对于我们公司而言,我们都会把数据同步到Hive,说明已经离线存储了一份了。

候选者:那如果「旧的数据」已经没有查询的业务了,那最简单的办法肯定是”删掉”部分数据咯。数据量降低了,那自然,检索速度就快了…

面试官:嗯,但一般不会删的

候选者:没错,只有极少部分业务可以删掉数据(:

候选者:随后,就考虑另一种情况,能不能在查询之前,直接走一层缓存(Redis)。

候选者:而走缓存的话,又要看业务能不能忍受读取的「非真正实时」的数据(毕竟Redis和MySQL的数据一致性需要保证),如果查询条件相对复杂且多变的话(涉及各种group by 和sum),那走缓存也不是一种好的办法,维护起来就不方便了…

候选者:再看看是不是有「字符串」检索的场景导致查询低效,如果是的话,可以考虑把表的数据导入至Elasticsearch类的搜索引擎,后续的线上查询就直接走Elasticsearch了。

候选者:MySQL->Elasticsearch需要有对应的同步程序(一般就是监听MySQL的binlog,解析binlog后导入到Elasticsearch)

候选者:如果还不是的话,那考虑要不要根据查询条件的维度,做相对应的聚合表,线上的请求就查询聚合表的数据,不走原表。

候选者:比如,用户下单后,有一份订单明细,而订单明细表的量级太大。但在产品侧(前台)透出的查询功能是以「天」维度来展示的,那就可以将每个用户的每天数据聚合起来,在聚合表就是一个用户一天只有一条汇总后的数据。

候选者:查询走聚合后的表,那速度肯定杠杠的(聚合后的表数据量肯定比原始表要少很多)

候选者:思路大致的就是「以空间换时间」,相同的数据换别的地方也存储一份,提高查询效率

面试官:那我还想问下,除了读之外,写性能同样有瓶颈,怎么办?

候选者:你说到这个,我就不困了。

候选者:如果在MySQL读写都有瓶颈,那首先看下目前MySQL的架构是怎么样的。

候选者:如果是单库的,那是不是可以考虑升级至主从架构,实现读写分离。

候选者:简单理解就是:主库接收写请求,从库接收读请求。从库的数据由主库发送的binlog进而更新,实现主从数据一致(在一般场景下,主从的数据是通过异步来保证最终一致性的)

面试官:嗯…

候选者:如果在主从架构下,读写仍存在瓶颈,那就要考虑是否要分库分表了

候选者:至少在我前公司的架构下,业务是区分的。流量有流量数据库,广告有广告的数据库,商品有商品的数据库。所以,我这里讲的分库分表的含义是:在原来的某个库的某个表进而拆分。

候选者:比如,现在我有一张业务订单表,这张订单表在广告库中,假定这张业务订单表已经有1亿数据量了,现在我要分库分表

候选者:那就会将这张表的数据分至多个广告库以及多张表中(:

候选者:分库分表的最明显的好处就是把请求进行均摊(本来单个库单个表有一亿的数据,那假设我分开8个库,那每个库1200+W的数据量,每个库下分8张表,那每张表就150W的数据量)。

面试官:你们是以什么来作为分库键的?

候选者:按照我们这边的经验,一般来说是按照userId的(因为按照用户的维度查询比较多),如果要按照其他的维度进行查询,那还是参照上面的的思路(以空间换时间)。

面试官:那分库分表后的ID是怎么生成的?

候选者:这就涉及到分布式ID生成的方式了,思路有很多。有借助MySQL自增的,有借助Redis自增的,有基于「雪花算法」自增的。具体使用哪种方式,那就看公司的技术栈了,一般使用Redis和基于「雪花算法」实现用得比较多。

候选者:至于为什么强调自增(还是跟索引是有序有关,前面已经讲过了,你应该还记得)

面试官:嗯,那如果我要分库分表了,迁移的过程是怎么样的呢

候选者:我们一般采取「双写」的方式来进行迁移,大致步骤就是:

候选者:一、增量的消息各自往新表和旧表写一份

候选者:二、将旧表的数据迁移至新库

候选者:三、迟早新表的数据都会追得上旧表(在某个节点上数据是同步的)

候选者:四、校验新表和老表的数据是否正常(主要看能不能对得上)

候选者:五、开启双读(一部分流量走新表,一部分流量走老表),相当于灰度上线的过程

候选者:六、读流量全部切新表,停止老表的写入

候选者:七、提前准备回滚机制,临时切换失败能恢复正常业务以及有修数据的相关程序。

面试官:嗯…今天就到这吧

本文总结:

  • 数据库表存在一定数据量,就需要有对应的索引
  • 发现慢查询时,检查是否走对索引,是否能用更好的索引进行优化查询速度,查看使用索引的姿势有没有问题
  • 当索引解决不了慢查询时,一般由于业务表的数据量太大导致,利用空间换时间的思想
  • 当读写性能均遇到瓶颈时,先考虑能否升级数据库架构即可解决问题,若不能则需要考虑分库分表
  • 分库分表虽然能解决掉读写瓶颈,但同时会带来各种问题,需要提前调研解决方案和踩坑

线上不是给你炫技的地方,安稳才是硬道理。能用简单的方式去解决,不要用复杂的方式

欢迎关注我的微信公众号【Java3y】来聊聊Java面试,对线面试官系列持续更新中!

【对线面试官-移动端】系列 一周两篇持续更新中!

【对线面试官-电脑端】系列 一周两篇持续更新中!

原创不易!!求三连!!

本文转载自: 掘金

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

今日头条一面:十道经典面试题解析

发表于 2021-10-12

前言

大家好,我是捡田螺的小男孩。

有位朋友面试了宇宙条后端方向。整理了这几道面试真题以及答案,如有错误,欢迎大家指出哈。金九银十冲刺,面试的小伙伴加油呀。

1.http请求头里,expire和cache-control字段含义,说说HTTP状态码

1.1 expire和cache-control字段含义

  • Cache-Control是HTTP/1.1的头字段,用来区分对缓存机制的支持情况,请求头和响应头都支持这个属性。通过它提供的不同的值来定义缓存策略。主要有public、private、no-cache等值。
  • expires是http1.0的头字段,过期时间,如果设置了时间,则浏览器会在设置的时间内直接读取缓存,不再请求。

1.2 常见HTTP状态码

2.https原理,数字签名,数字证书。

2.1 https 原理

  • HTTPS = HTTP + SSL/TLS,即用SSL/TLS对数据进行加密和解密,Http进行传输。
  • SSL,即Secure Sockets Layer(安全套接层协议),是网络通信提供安全及数据完整性的一种安全协议。
  • TLS,即Transport Layer Security(安全传输层协议),它是SSL 3.0的后续版本。

Https工作流程

  1. 用户在浏览器里输入一个https网址,然后连接到server的443端口。
  2. 服务器必须要有一套数字证书,可以自己制作,也可以向组织申请,区别就是自己颁发的证书需要客户端验证通过。这套证书其实就是一对公钥和私钥。
  3. 服务器将自己的数字证书(含有公钥)发送给客户端。
  4. 客户端收到服务器端的数字证书之后,会对其进行检查,如果不通过,则弹出警告框。如果证书没问题,则生成一个密钥(对称加密),用证书的公钥对它加密。
  5. 客户端会发起HTTPS中的第二个HTTP请求,将加密之后的客户端密钥发送给服务器。
  6. 服务器接收到客户端发来的密文之后,会用自己的私钥对其进行非对称解密,解密之后得到客户端密钥,然后用客户端密钥对返回数据进行对称加密,这样数据就变成了密文。
  7. 服务器将加密后的密文返回给客户端。
  8. 客户端收到服务器发返回的密文,用自己的密钥(客户端密钥)对其进行对称解密,得到服务器返回的数据。

2.2 数字签名,数字证书

了解过Https原理的小伙伴,都知道数字证书这玩意。为了避免公钥被篡改,引入了数字证书,如下:

数字证书构成

  • 公钥和个人信息,经过Hash算法加密,形成消息摘要;将消息摘要拿到拥有公信力的认证中心(CA),用它的私钥对消息摘要加密,形成数字签名.
  • 公钥和个人信息、数字签名共同构成数字证书。

3.tcp连接client和server有哪些状态,time_wait状态

3.1 tcp 连接

tcp连接时,客户端client 有SYN_SEND、ESTABLISHED状态,服务端server有SYN_RCVD、ESTABLISHED状态。

tcp三次握手

开始客户端和服务器都处于CLOSED状态,然后服务端开始监听某个端口,进入LISTEN状态

  • 第一次握手(SYN=1, seq=x),发送完毕后,客户端进入 SYN_SEND 状态
  • 第二次握手(SYN=1, ACK=1, seq=y, ACKnum=x+1), 发送完毕后,服务器端进入 SYN_RCVD 状态。
  • 第三次握手(ACK=1,ACKnum=y+1),发送完毕后,客户端进入 ESTABLISHED 状态,当服务器端接收到这个包时,也进入 ESTABLISHED 状态,TCP 握手,即可以开始数据传输。

3.2 time_wait状态

可以先回忆下TCP的四次挥手哈,

TCP四次挥手

  • 第一次挥手(FIN=1,seq=u),发送完毕后,客户端进入FIN_WAIT_1状态
  • 第二次挥手(ACK=1,ack=u+1,seq =v),发送完毕后,服务器端进入CLOSE_WAIT状态,客户端接收到这个确认包之后,进入FIN_WAIT_2状态
  • 第三次挥手(FIN=1,ACK1,seq=w,ack=u+1),发送完毕后,服务器端进入LAST_ACK状态,等待来自客户端的最后一个ACK。
  • 第四次挥手(ACK=1,seq=u+1,ack=w+1),客户端接收到来自服务器端的关闭请求,发送一个确认包,并进入TIME_WAIT状态,等待了某个固定时间(两个最大段生命周期,2MSL,2 Maximum Segment Lifetime)之后,没有收到服务器端的 ACK ,认为服务器端已经正常关闭连接,于是自己也关闭连接,进入 CLOSED 状态。服务器端接收到这个确认包之后,关闭连接,进入 CLOSED 状态。

TIME-WAIT 状态为什么需要等待 2MSL

2MSL,2 Maximum Segment Lifetime,即两个最大段生命周期

  • 1个 MSL 保证四次挥手中主动关闭方最后的ACK 报文能最终到达对端
  • 1个 MSL 保证对端没有收到 ACK 那么进行重传的FIN报文能够到达

4.什么是虚拟内存? 什么是物理内存?

4.1 什么是虚拟内存?

虚拟内存,是虚拟出来的内存,它的核心思想就是确保每个程序拥有自己的地址空间,地址空间被分成多个块,每一块都有连续的地址空间。同时物理空间也分成多个块,块大小和虚拟地址空间的块大小一致,操作系统会自动将虚拟地址空间映射到物理地址空间,程序只需关注虚拟内存,请求的也是虚拟内存,真正使用却是物理内存。

4.2 什么是物理内存

物理内存,指通过物理内存条而获得的内存空间,而虚拟内存则是指将硬盘的一块区域划分来作为内存。

我们常说的物理内存大小,其实是指内存条的大小。一般买电脑时,我们都会看下内存条是多大容量的,话说如果内存条大小是100G,那这100G就都能够被使用吗?不一定的,更多的还是要看CPU地址总线的位数,如果地址总线只有20位,那么它的寻址空间就是1MB,即使可以安装100G的内存条也没有意义,也只能视物理内存大小为1MB。

4.3 虚拟内存如何映射到物理内存?

如下图,CPU里有一个内存管理单元(Memory Management Unit),简称为MMU,虚拟内存不是直接送到内存总线,而是先给到MMU,由MMU来把虚拟地址映射到物理地址,程序只需要管理虚拟内存就好,映射的逻辑自然有其它模块自动处理。

5.一台机器最多可以建立多少个tcp连接,client端,server端,超过了怎么办

  • TCP连接的客户端机:每一个ip可建立的TCP连接理论受限于ip_local_port_range参数,也受限于65535。但可以通过配置多ip的方式来加大自己的建立连接的能力。
  • TCP连接的服务器机:每一个监听的端口虽然理论值很大,但这个数字没有实际意义。最大并发数取决你的内存大小,每一条静止状态的TCP连接大约需要吃3
    .3K的内存。

6.Eureka原理,是否是强一致性,eureka集群。宕机了服务还能调用么?Eureka和ZooKeeper对比

6.1 eureka架构

注册中心是分布式开发的核心组件之一,而eureka是spring cloud推荐的注册中心实现。

架构图如下:

  • Eureka Server:提供服务注册和发现,多个Eureka Server之间会同步数据,做到状态一致
  • Service Provider:服务提供方,将自身服务注册到Eureka,从而使服务消费方能够找到
  • Service Consumer:服务消费方,从Eureka获取注册服务列表,从而能够消费服务

6.2 基于集群的Eureka架构图

Eureka server可以集群部署,多个节点之间会通过Replicate(异步方式)进行数据同步,保证数据最终一致性。Eureka Server作为一个开箱即用的服务注册中心,提供的功能包括:服务注册、接收服务心跳、服务剔除、服务下线等。

服务启动后向Eureka注册,Eureka Server会将注册信息向其他Eureka Server进行同步,当服务消费者要调用服务提供者,则向服务注册中心获取服务提供者地址,然后会将服务提供者地址缓存在本地,下次再调用时,则直接从本地缓存中取,完成一次调用。

6.3 宕机了服务还能调用么?

Eureka 挂了,微服务是可以调通的,不过有个前提:provider的地址没变!如果 provider换了一个 IP 地址或者端口,这个时候,consumer 就无法及时感知到这种变化,就会调不通。

6.4 Eureka和ZooKeeper对比

  • Zookeeper保证CP(一致性和分区容错性),但是不保证可用性,ZK的leader选举期间,是不可用的。
  • Eureka保证AP(可用性和分区容错性),它优先保证可用性,几个节点挂掉不会影响正常节点的工作。

7.Hystrix了解嘛?说说Hystrix的工作原理

Hystrix 工作流程图如下:

  1. 构建命令

Hystrix 提供了两个Command, HystrixCommand 和 HystrixObservableCommand,可以使用这两个对象来包裹待执行的任务。

  1. 执行命令

有四种方式执行command。分别是:

  • R execute():同步执行,从依赖服务得到单一结果对象
  • Future queue():异步执行,返回一个 Future 以便获取执行结果,也是单一结果对象
  • Observable observe():hot observable,创建Observable后会订阅Observable,可以返回多个结果
  • Observable toObservable():cold observable,返回一个Observable,只有订阅时才会执行,可以返回多个结果
  1. 检查缓存

如果启用了 Hystrix Cache,任务执行前将先判断是否有相同命令执行的缓存。如果有则直接返回缓存的结果;如果没有缓存的结果,但启动了缓存,将缓存本次执行结果以供后续使用。

4.检查断路器是否打开
断路器(circuit-breaker)和保险丝类似,保险丝在发生危险时将会烧断以保护电路,而断路器可以在达到我们设定的阀值时触发短路(比如请求失败率达到50%),拒绝执行任何请求。

如果断路器被打开,Hystrix 将不会执行命令,直接进入Fallback处理逻辑。

5.检查线程池/信号量情况
Hystrix 隔离方式有线程池隔离和信号量隔离。当使用Hystrix线程池时,Hystrix 默认为每个依赖服务分配10个线程,当10个线程都繁忙时,将拒绝执行命令。信号量同理。

6.执行具体的任务
通过HystrixObservableCommand.construct() 或者 HystrixCommand.run() 来运行用户真正的任务。

7.计算链路健康情况
每次开始执行command、结束执行command以及发生异常等情况时,都会记录执行情况,例如:成功、失败、拒绝以及超时等情况,会定期处理这些数据,再根据设定的条件来判断是否开启断路器。

8.命令失败时执行 Fallback 逻辑
在命令失败时执行用户指定的 Fallback 逻辑。上图中的断路、线程池拒绝、信号量拒绝、执行执行、执行超时都会进入 Fallback 处理。

9.返回执行结果
原始结果将以Observable形式返回,在返回给用户之前,会根据调用方式的不同做一些处理。

8.zookeeper一致性保证,zab协议原理,zookeeper属于哪种一致性,强一致性么,还是最终一致性

Zab协议,英文全称是Zookeeper Atomic Broadcast(Zookeeper原子广播)。Zookeeper是通过Zab协议来保证分布式事务的最终一致性。

Zab协议是为分布式协调服务Zookeeper专门设计的一种支持崩溃恢复的原子广播协议 ,是Zookeeper保证数据一致性的核心算法。Zab借鉴了Paxos算法,是一种通用的分布式一致性算法。

基于Zab协议,Zookeeper实现了一种主备模型(即Leader和Follower模型)的系统架构来保证集群中各个副本之间数据的一致性。就是指只有一台Leader节点负责处理外部的写事务请求,然后它(Leader)将数据同步到其他Follower节点。

Zookeeper 客户端会随机的链接到 zookeeper 集群中的一个节点,如果是读请求,就直接从当前节点中读取数据;如果是写请求,那么节点就会向Leader提交事务,Leader 接收到事务提交,会广播该事务,只要超过半数节点写入成功,该事务就会被提交。

Zab协议要求每个 Leader 都要经历三个阶段:发现,同步,广播。

  • 发现:要求zookeeper集群必须选举出一个 Leader 进程,同时 Leader 会维护一个 Follower 可用客户端列表。将来客户端可以和这些 Follower节点进行通信。
  • 同步:Leader 要负责将本身的数据与 Follower 完成同步,做到多副本存储。这样也是提现了CAP中的高可用和分区容错。Follower将队列中未处理完的请求消费完成后,写入本地事务日志中。
  • 广播:Leader 可以接受客户端新的事务Proposal请求,将新的Proposal请求广播给所有的 Follower。
  1. 聊聊zookeeper选举机制

服务器启动或者服务器运行期间(Leader挂了),都会进入Leader选举,我们来看一下~假设现在ZooKeeper集群有五台服务器,它们myid分别是服务器1、2、3、4、5,如图:

9.1 服务器启动的Leader选举

zookeeper集群初始化阶段,服务器(myid=1-5)依次启动,开始zookeeper选举Leader~

  1. 服务器1(myid=1)启动,当前只有一台服务器,无法完成Leader选举
  2. 服务器2(myid=2)启动,此时两台服务器能够相互通讯,开始进入Leader选举阶段
  • 2.1. 每个服务器发出一个投票

服务器1和服务器2都将自己作为Leader服务器进行投票,投票的基本元素包括:服务器的myid和ZXID,我们以(myid,ZXID)形式表示。初始阶段,服务器1和服务器2都会投给自己,即服务器1的投票为(1,0),服务器2的投票为(2,0),然后各自将这个投票发给集群中的其他所有机器。

  • 2.2 接受来自各个服务器的投票

每个服务器都会接受来自其他服务器的投票。同时,服务器会校验投票的有效性,是否本轮投票、是否来自LOOKING状态的服务器。

  • 2.3. 处理投票

收到其他服务器的投票,会将被人的投票跟自己的投票PK,PK规则如下:

  • 优先检查ZXID。ZXID比较大的服务器优先作为leader。
  • 如果ZXID相同的话,就比较myid,myid比较大的服务器作为leader。

服务器1的投票是(1,0),它收到投票是(2,0),两者zxid都是0,因为收到的myid=2,大于自己的myid=1,所以它更新自己的投票为(2,0),然后重新将投票发出去。对于服务器2呢,即不再需要更新自己的投票,把上一次的投票信息发出即可。

  • 2.4. 统计投票

每次投票后,服务器会统计所有投票,判断是否有过半的机器接受到相同的投票信息。服务器2收到两票,少于3(n/2+1,n为总服务器),所以继续保持LOOKING状态

  1. 服务器3(myid=3)启动,继续进入Leader选举阶段
  • 3.1 跟前面流程一致,服务器1和2先投自己一票,因为服务器3的myid最大,所以大家把票改投给它。此时,服务器为3票(大于等于n/2+1),所以服务器3当选为Leader。 服务器1,2更改状态为FOLLOWING,服务器3更改状态为LEADING;
  1. 服务器4启动,发起一次选举。
  • 4.1 此时服务器1,2,3已经不是LOOKING状态,不会更改选票信息。选票信息结果:服务器3为3票,服务器4为1票。服务器4并更改状态为FOLLOWING;
  1. 服务器5启动,发起一次选举。
  • 同理,服务器也是把票投给服务器3,服务器5并更改状态为FOLLOWING;
  1. 投票结束,服务器3当选为Leader

9.2 服务器运行期间的Leader选举

zookeeper集群的五台服务器(myid=1-5)正在运行中,突然某个瞬间,Leader服务器3挂了,这时候便开始Leader选举~

  1. 变更状态

Leader 服务器挂了之后,余下的非Observer服务器都会把自己的服务器状态更改为LOOKING,然后开始进入Leader选举流程。

  1. 每个服务器发起投票

每个服务器都把票投给自己,因为是运行期间,所以每台服务器的ZXID可能不相同。假设服务1,2,4,5的zxid分别为333,666,999,888,则分别产生投票(1,333),(2,666),(4,999)和(5,888),然后各自将这个投票发给集群中的其他所有机器。

  1. 接受来自各个服务器的投票
  2. 处理投票

投票规则是跟Zookeeper集群启动期间一致的,优先检查ZXID,大的优先作为Leader,所以显然服务器zxid=999具有优先权。

  1. 统计投票
  2. 改变服务器状态

10. 算法:给定一个字符串s ,请你找出其中不含有重复字符的最长连续子字符串的长度。

可以使用滑动窗口实现,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
ini复制代码public int lengthOfLongestSubstring2(String s) {
int n = s.length();
if (n <= 1) return n;
int maxLen = 1;

//左、右指针
int left = 0, right = 0;

Set<Character> window = new HashSet<>();
while (right < n) {
char rightChar = s.charAt(right);
while (window.contains(rightChar)) {
window.remove(s.charAt(left));
left++;
}
//最大长度对比
maxLen = Math.max(maxLen, right - left + 1);
window.add(rightChar);
right++;
}

return maxLen;
}

参考与感谢

  • Spring Cloud 源码学习之 Hystrix 工作原理
  • Zookeeper——一致性协议:Zab协议

本文转载自: 掘金

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

谈谈java中的死锁 什么是死锁 死锁产生原因 死锁的排查

发表于 2021-10-12

本文已参与「掘力星计划」,赢取创作大礼包,挑战创作激励金。

抽奖说明在文末!

什么是死锁

在使用多线程以及多进程时,两个或两个以上的运算单元(进程、线程或协程),各自占有一些共享资源,并且互相等待其他线程占有的资源才能进行,而导致两个或者多个线程都在等待对方释放资源,就称为死锁。

下面看个简单的例子:

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
java复制代码public class DeadLockTest {
public static void main(String[] args) {
Object lock1 = new Object(); // 锁1
Object lock2 = new Object(); // 锁2

Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
// 先获取锁1
synchronized (lock1) {
System.out.println("Thread 1:获取到锁1!");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//获取锁2
System.out.println("Thread 1:等待获取2...");
synchronized (lock2) {
System.out.println("Thread 1:获取到锁2!");
}
}
}
});
t1.start();

Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
// 先获取锁2
synchronized (lock2) {
System.out.println("Thread 2:获取到锁2!");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 获取锁1
System.out.println("Thread 2:等待获取1...");
synchronized (lock1) {
System.out.println("Thread 2:获取到锁1!");
}
}
}
});
t2.start();
}
}

死锁产生原因

死锁只有同时满足以下四个条件才会发生:

互斥条件:线程对所分配到的资源具有排它性,在某个时间内锁资源只能被一个线程占用。如下图:

死锁1.PNG

线程1已经持有的资源,不能再同时被线程2持有,如果线程2请求获取线程1已经占用的资源,那线程2只能等待,直到线程1释放资源。如下图:

持有并请求条件:
持有.PNG

持有并请求条件是指,当线程1已经持有了资源1,又想申请资源2,而资源2已经被线程3持有了,所以线程1就会处于等待状态,但是线程1在等待资源2的同时并不会释放已经持有的资源1。

不可剥夺条件:当线程已经持有了资源 ,在未使用完之前,不能被剥夺。如下图:
不可.PNG

线程2如果也想使用此资源,只能等待线程1使用完并释放后才能获取。

环路等待条件:在死锁发生的时候,两个线程获取资源的顺序构成了环形链,如下图:

环形.PNG
线程1已经持有资源2,想获取资源1, 线程2已经获取了资源1,想请求资源2,这就形成资源请求等待的环形图。

死锁的排查

1.jstack

用法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
arduino复制代码/opt/java8/bin/jstack

Usage:
jstack [-l] <pid>
(to connect to running process) 连接活动线程
jstack -F [-m] [-l] <pid>
(to connect to a hung process) 连接阻塞线程
jstack [-m] [-l] <executable> <core>
(to connect to a core file) 连接dump的文件
jstack [-m] [-l] [server_id@]<remote server IP or hostname>
(to connect to a remote debug server) 连接远程服务器

Options:
-F to force a thread dump. Use when jstack <pid> does not respond (process is hung)
-m to print both java and native frames (mixed mode)
-l long listing. Prints additional information about locks
-h or -help to print this help message

死锁日志如下:

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
php复制代码"mythread2" #12 prio=5 os_prio=0 tid=0x0000000058ef7800 nid=0x1ab4 waiting on condition [0x0000000059f8f000]
java.lang.Thread.State: WAITING (parking)
at sun.misc.Unsafe.park(Native Method)
- parking to wait for <0x00000000d602d610> (a java.util.concurrent.lock
s.ReentrantLock$NonfairSync)
at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175)
at java.util.concurrent.locks.AbstractQueuedSynchronizer.parkAndCheckInt
errupt(AbstractQueuedSynchronizer.java:836)
at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireQueued(A
bstractQueuedSynchronizer.java:870)
at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquire(Abstrac
tQueuedSynchronizer.java:1199)
at java.util.concurrent.locks.ReentrantLock$NonfairSync.lock(ReentrantLo
ck.java:209)
at java.util.concurrent.locks.ReentrantLock.lock(ReentrantLock.java:285)

at DeathLock$2.run(DeathLock.java:34)

Locked ownable synchronizers:
- <0x00000000d602d640> (a java.util.concurrent.locks.ReentrantLock$Nonfa
irSync)

"mythread1" #11 prio=5 os_prio=0 tid=0x0000000058ef7000 nid=0x3e68 waiting on condition [0x000000005947f000]
java.lang.Thread.State: WAITING (parking)
at sun.misc.Unsafe.park(Native Method)
- parking to wait for <0x00000000d602d640> (a java.util.concurrent.lock
s.ReentrantLock$NonfairSync)
at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175)
at java.util.concurrent.locks.AbstractQueuedSynchronizer.parkAndCheckInt
errupt(AbstractQueuedSynchronizer.java:836)
at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireQueued(A
bstractQueuedSynchronizer.java:870)
at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquire(Abstrac
tQueuedSynchronizer.java:1199)
at java.util.concurrent.locks.ReentrantLock$NonfairSync.lock(ReentrantLo
ck.java:209)
at java.util.concurrent.locks.ReentrantLock.lock(ReentrantLock.java:285)

at DeathLock$1.run(DeathLock.java:22)

Locked ownable synchronizers:
- <0x00000000d602d610> (a java.util.concurrent.locks.ReentrantLock$Nonfa
irSync)


Found one Java-level deadlock:
=============================
"mythread2":
waiting for ownable synchronizer 0x00000000d602d610, (a java.util.concurrent.l
ocks.ReentrantLock$NonfairSync),
which is held by "mythread1"
"mythread1":
waiting for ownable synchronizer 0x00000000d602d640, (a java.util.concurrent.l
ocks.ReentrantLock$NonfairSync),
which is held by "mythread2"

Java stack information for the threads listed above:
===================================================
"mythread2":
at sun.misc.Unsafe.park(Native Method)
- parking to wait for <0x00000000d602d610> (a java.util.concurrent.lock
s.ReentrantLock$NonfairSync)
at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175)
at java.util.concurrent.locks.AbstractQueuedSynchronizer.parkAndCheckInt
errupt(AbstractQueuedSynchronizer.java:836)
at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireQueued(A
bstractQueuedSynchronizer.java:870)
at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquire(Abstrac
tQueuedSynchronizer.java:1199)
at java.util.concurrent.locks.ReentrantLock$NonfairSync.lock(ReentrantLo
ck.java:209)
at java.util.concurrent.locks.ReentrantLock.lock(ReentrantLock.java:285)

at DeathLock$2.run(DeathLock.java:34)
"mythread1":
at sun.misc.Unsafe.park(Native Method)
- parking to wait for <0x00000000d602d640> (a java.util.concurrent.lock
s.ReentrantLock$NonfairSync)
at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175)
at java.util.concurrent.locks.AbstractQueuedSynchronizer.parkAndCheckInt
errupt(AbstractQueuedSynchronizer.java:836)
at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireQueued(A
bstractQueuedSynchronizer.java:870)
at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquire(Abstrac
tQueuedSynchronizer.java:1199)
at java.util.concurrent.locks.ReentrantLock$NonfairSync.lock(ReentrantLo
ck.java:209)
at java.util.concurrent.locks.ReentrantLock.lock(ReentrantLock.java:285)

at DeathLock$1.run(DeathLock.java:22)

Found 1 deadlock.

可以根据日志输出来直接定位到具体的死锁代码。

2.jmc

jmc是Oracle Java Mission Control的缩写,是一个对Java程序进行管理、监控、概要分析和故障排查的工具套件。在JDK的bin目录中,同样是双击启动,如下图所示:

mc.PNG
jmc打开如下:

11.PNG
需要选择死锁类,右键启动JMX控制台,然后就可以发现死锁和死锁具体信息。

常用工具还有jconsole,jvisualvm等不再一一介绍,感兴趣的可以自已查看一下。

避免死锁问题的发生

死锁分析

还要看死锁产生的4个条件,只要不满足条件就无法产生死锁了,互斥条件和不可剥夺条件是系统特性无法阻止。只能通过破坏请求和保持条件或者是环路等待条件,从而来解决死锁的问题。

避免死锁的方案

一、固定加锁顺序

即通过有顺序的获取锁,从而避免产生环路等待条件,从而解决死锁问题的。来看环路等待的例子:

22.PNG

线程1先获取了锁A,再请求获取锁B,线程2与线程1同时执行,线程2先获取锁B,再请求获取锁A,两个线程都先占用了各自的资源(锁A和锁B)之后,再尝试获取对方的锁,从而造成了环路等待问题,最后造成了死锁。

此时只需将线程1和线程2获取锁的顺序进行统一,也就是线程1和线程2同时执行之后,都先获取锁A,再获取锁B。因为只有一个线程能成功获取到锁A,没有获取到锁A的线程就会等待先获取锁A,此时得到锁A的线程继续获取锁 B,因为没有线程争抢和拥有锁B,那么得到锁A的线程就会顺利的拥有锁B,之后执行相应的代码再将锁资源全部释放,然后另一个等待获取锁A的线程就可以成功获取到锁资源,这样就不会出现死锁的问题了。

二、开放调用避免死锁

在协作对象之间发生死锁的场景中,主要是因为在调用某个方法时就需要持有锁,并且在方法内部也调用了其他带锁的方法,如果在调用某个方法时不需要持有锁,那么这种调用被称为开放调用,同步代码块最好仅被用于保护那些涉及共享状态的操作。

三、使用定时锁

使用显式Lock锁,在获取锁时使用tryLock()方法。当等待超过时限的时候,tryLock()不会一直等待,而是返回错误信息,能够有效避免死锁问题。

总结

简单来说,死锁问题的产生是由两个或者以上线程并行执行的时候,争夺资源而互相等待造成的。

死锁只有同时满足互斥、持有并等待、不可剥夺、环路等待这四个条件的时候才会发生。

所以要避免死锁问题,就是要破坏其中一个条件即可,最常用的方法就是使用资源有序分配法来破坏环路等待条件。

抽奖说明

1.本活动由掘金官方支持 详情可见juejin.cn/post/701221…

2.通过评论和文章有关的内容即可参加,要和文章内容有关哦!

3.本月的文章都会参与抽奖活动,欢迎大家多多互动!

4.除掘金官方抽奖外本人也将送出周边礼物(马克杯一个和掘金徽章若干,马克杯将送给走心评论,徽章随机抽取,数量视评论人数增加)。

本文转载自: 掘金

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

MySQL 双一原则为什么能保证数据不丢

发表于 2021-10-12

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。

在日常工作或者面试中,我们经常会谈到,即使 INNODB 事务提交了,数据还是有可能会丢,那么这是什么原因呢。

其实这就聊到了“双一原则”,即 sync_binlog = 1 && innodb_flush_log_at_trx_commit = 1。今天我们就来聊一聊为什么双一原则能保证数据不丢。

  • 先看一下 redo log 刷到磁盘的步骤:
1. 当事务提交时,会先将数据写入到 Log Buffer 中,这里调用的是 MySQL 自己的 WriteRedoLog;
2. 接着,之后当 MySQL 发起系统调用写文件 write 时,Log Buffer 中的日志才会写到操作系统缓存中(OS Cache);
3. 注意,当 MySQL 系统调用完 write 之后,就会认为文件已经写完,如果不 flush,什么时候落盘,是由操作系统决定的。
4. 最后,由操作系统将 OS Cache 中的数据刷到磁盘上。
  • 同样,再看一下 binlog 刷到磁盘的步骤:
1. 事务执行过程中,先把日志写到 binlog cache 中;
2. 事务提交,将 binlog cache 中的数据写入到操作系统缓存中;
3. 最后,由操作系统决定什么时候将数据刷到磁盘上;
  • 说一下 MySQL 的两阶段提交:
+ 先 prepare;
+ 写 binlog;
+ 写 redo log;
+ commit;
  • 为什么会丢数据?
+ binlog 认为写入到文件系统缓存就算成功了;(个人理解)
+ redo log 认为写入到 Log Buffer 中就算成功了;(个人理解)
+ 所以只要没有写入磁盘都会丢数据;
  • 如何保证不丢?
+ 写完缓存,再刷新到磁盘上,数据就能保证不丢;
+ 双一策略都要求落盘才会成功;
  • sync_binlog 参数配置:
+ `0`:每次提交事务只写入到文件系统缓存,并不会把数据持久化到磁盘,速度较快;
+ `1`:每次提交事务都持久化到磁盘;
+ `N`:(N>1)每次提交事务都把数据写入到文件系统缓存,但是累计 N 个事务后才把数据刷到磁盘
  • innodb_flush_log_at_trx_commit 参数配置:
+ `0`:每隔 1 秒,才会将 Log Buffer 中的数据批量写入到文件系统缓存,并刷到磁盘;(有可能会丢 1 秒数据)
+ `1`:每次事务提交,都会将 Log Buffer 中的数据写入到文件系统缓存,并刷到磁盘。(是 MySQL 默认配置,保证事务 ACID 特性)
+ `2`:每次事务提交,都将 Log Buffer 中的数据写到文件系统缓存,每隔 1 秒,MySQL 主动将文件系统缓存中数据批量刷到磁盘;

因此,我们可以看到,双一原则保证数据不丢的原因是:每次事务提交都将数据刷到磁盘中,这是非常重要的。

本文转载自: 掘金

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

鉴权界的后起之秀:Token鉴权方案

发表于 2021-10-11

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动

Token鉴权在产生之初就是为了打败Session/Cookie的,它是在前辈的基础上进行了变形改造,在数据传输中不仅提升了数据安全性,更是保证了服务端的高性能,带来了noSession的革命。

  1. Token是什么

Token,通常叫做令牌,是一种自定义实现的类似Session/Cookie机制的,用来代替传统Session/Cookie的新兴鉴权方案,当前很多的应用API鉴权就是使用的Token令牌。

Token是服务端生成的一串加密字符串,用户在用户登录成功后生成并返回给客户端,之后客户端的每次请求都会通过GET/POST/Header等方式携带Token,服务端通过验证Token的有效性来完成鉴权。

  1. Token带来了什么

基于Token的鉴权方式是无状态的,服务端将不再需要存储Session信息,这种noSession的方式让服务器扩展更加方便,并节省了大量的服务端内存空间。
Token鉴权方式优点:

  • 无状态、可扩展、适合分布式
  • 性能高、安全性好,能够防止CSRF(跨站请求伪造),解决跨域问题
  • Token鉴权时只需要客户端保存信息,减少了服务端对内存的消耗
  • 对于手机APP端适应性好
  1. Token鉴权流程

相比于Session/Cookie鉴权,Token验证变动最大的就是需要自定义完成Token的生成和Token解析认证,不再是针对SessionId的验证和获取信息。

基于Token的鉴权方式的完整流程为:

  1. 客户端用户通过用户和密码进行首次登录
  2. 服务端接收用户请求,并验证用户名和密码的正确性,登录验证成功后根据自定义规则生成Token信息
  3. 服务端将生成的Token通过响应返回给客户端
  4. 客户端将Token信息存储在本地
  5. 客户端在之后的每次请求中携带Token信息
  6. 服务端针获取请求中的Token,并根据定义的验证机制判断Token合法性,验证成功获取用户信息,保持用户状态
  7. Token存活时间达到设置的有效期后自动失效,此后用户请求时Token验证不通过,需要用户重新登录验证
  1. Token的技术实现

当然,Token的实现上似乎还存有一些疑问:

  • 类似Session/Cookie机制,但是所有都需要自己去实现,相当于从零开始,这也太复杂了
  • Token信息若是存在Cookie中,这样的方式和Session/Cookie没有区别,Token方案中怎么解决
  • 生成Token时如何保证数据的安全性

为了更好的解决这些问题,产生了一些基于Token方案的的技术实现,如:

  • JWT
  • OAuth
  1. 总结

Token势要打破Session/Cookie的鉴权机制,并形成一种自己的鉴权方案,为API接口鉴权提供更加安全、方便、高效、功能丰富的体验,后续我们就要慢慢揭开JWT这一基于Token的当下主流鉴权方案。

本文转载自: 掘金

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

1…497498499…956

开发者博客

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