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

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


  • 首页

  • 归档

  • 搜索

面试必问题:缓存击穿、缓存穿透、缓存雪崩,你还傻傻分不清

发表于 2021-11-14

当我们服务器QPS比较高,并且对数据的实时性要求不高时,往往会接入缓存以达到快速Response、降低数据库压力的作用,常用来做缓存的中间件如Redis等,面试时经常会被面试官提问,作为里面最常见,几乎是必考题的缓存击穿、穿透、雪崩场景,你真的了解了吗?

​ 前端发起一个请求,经历过三次握手后连接到服务器,想要获取相应的数据,那么服务器接入了缓存中间件后,从接收到Request到最后的Response,到底是怎样的一个流程呢?以下探讨忽略掉参数校验等逻辑,直接讲最核心的链路。

调用链路

​ 一个请求Request过来,服务器首先和缓存中间件建立连接,传输对应key到缓存中间件中获取相对应的数据,服务器拿到返回的结果后,判断返回的结果是否有数据,如果有数据,则返回从缓存中拿到的结果。如果缓存中间件中没有数据,则建立数据库连接,访问数据库服务器,按照相应逻辑拿到返回结果,判断结果中是否有数据,如果有则返回对应数据,如果没有则按照业务场景要求,返回对应结果(一般为null或者new一个空对象)。

在这里插入图片描述

缓存击穿

含义:

​ 什么是缓存击穿?通俗的讲指的是缓存中没有数据,但数据库中有数据的场景。那为什么缓存中会没有数据呢?一般是由于设置了缓存时间导致缓存过期,所以没有数据。那缓存找不到数据去数据库查询就好了呀,为啥又叫击穿?是因为要查询这个key对应的数据是一个热点数据,并发访问的量大,同时去查询数据库,导致数据库压力骤增,严重会打崩数据库。

在这里插入图片描述

解决方案:

1、如果是不改变的数据,如一些常量值,则可以设置对应热点key永不过期。

2、加上互斥锁,防止同一台服务器同一时间有多个连接访问数据库。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
java复制代码// 伪代码
public class Main {
// 双重检测锁
public static String getHotData(String key) {
// 先从缓存中间件获取对应热点key数据
String response = redis.get(key);
// 缓存没有数据
if(Objects.isNull(response)) {
// 保证一台服务器同一时间只有一个线程访问
synchronized (Main.class) {
// 假设A线程访问进synchronized里,线程B, C阻塞在synchronsized外面
// 线程A退出synchronized后,线程B和C应该从redis中拿而不是再访问数据库
response = redis.get(key);
// 访问数据库 拿到数据后 写进redis中
if(Objects.isNull(response)) {
response = loadDataFromMySQL(key);
redis.set(key, response);
}
}
}
return response;
}
}

3、加上分布式锁,全局保证只有一个线程访问数据库。

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复制代码// 伪代码
public class Main {

// 分布式唯一key
public static String getHotData(String key, int tryTime) throws InterruptedException {
if(tryTime >= 4) {
return "";
}

// 先从缓存中间件获取对应热点key数据
String response = redis.get(key);
// 缓存没有数据
if(Objects.isNull(response)) {
// 保证整个服务集群同一时间只有一个线程访问
if (redis.tryLock()) {
try {
// 访问数据库 拿到数据后 写进redis中
if(Objects.isNull(response)) {
response = loadDataFromMySQL(key);
redis.set(key, response);
}
} finally {
redis.unlock();
}
} else {
TimeUnit.MILLISECONDS.sleep(100);
getHotData(key, tryTime + 1);
}
}

return response;
}
}

缓存穿透

含义:缓存穿透指的是缓存中间件和数据库都没有对应的数据,但是不断接收到请求获取该key的数据,导致数据库压力过大,甚至崩溃。

在这里插入图片描述

解决方案:

1、访问数据库也拿不到数据后,可以按照具体业务要求,在缓存层加上一个该key的值,设置一个过期时间,比如10s或者1min等。那为什么不设不过期呢?第一个是说因为该key可能有对应的业务含义,有可能只是该时间点还没有数据,所以不能设置不过期;第二个是说如果真的是恶意访问,那么可能过一段时间就没有类似请求,那么我们没有必要一直把该数据留在缓存里。

2、增加校验,如果是不符合预期的请求可以直接过滤,比如说缓存中存放了用户信息,对应的缓存key是和id有关系,那么如果你的id都是大于等于0的,对于小于0的id可以直接做过滤。

1
2
3
4
5
6
7
8
9
10
11
java复制代码@Controller
public class Controller {
@RequestMapping(value="/test")
public String printHello(Integer id) {
if(Objects.isNull(id) || id < 0) {
return null;
}

// 处理对应逻辑
}
}

缓存雪崩

含义:

缓存雪崩指的是在同一个时间点,缓存中的大批量数据过期,并且还都是热点数据,导致同一时间并发压力都打到了数据库中,导致数据库压力骤增,甚至宕机。有的人就会问了,这和缓存击穿不是一个意思吗?缓存击穿指的是并发查询某条热点key数据,缓存雪崩指的是大批量。出现场景之一是在某些核心页面,该页面的内容都放入了缓存,并且都设置了同样的缓存时间。

在这里插入图片描述

解决方案:

1、最简单的就是设置热点数据不过期,但要结合对应业务场景来看。

2、在给每个热点key设置过期时间时,加上一个随机值,使得热点数据离散开来,不会同一时间大批量过期。

3、使用缓存击穿场景讲到的互斥锁、分布式锁。

在这里插入图片描述

愿每个人都能带着怀疑的态度去阅读文章并探究其中原理。

道阻且长,往事作序,来日为章。

期待我们下一次相遇!

本文转载自: 掘金

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

9139 位艺人在 Python 面前不值一提

发表于 2021-11-14

「这是我参与11月更文挑战的第14天,活动详情查看:2021最后一次更文挑战」

本篇博客的最终目标是爬取世界上 9139 位艺人的身高、体重、生日、血型,当然有些数据目标网站没有提供,不在做过多的扩展。
9139 位艺人在 Python 面前不值一提 Python 爬虫小课 5-9

爬取前的分析工作

目标网址为:www.ylq.com/star/list-a…,数据量在 9000+位艺人,单纯从数据上看量不是很大,合计 153 页数据。

页面未通过接口返回数据,查看页面源码即可看到。

9139 位艺人在 Python 面前不值一提 Python 爬虫小课 5-9
列表页对应的数据如下图:
9139 位艺人在 Python 面前不值一提 Python 爬虫小课 5-9
从图片中可以看到内页地址,头像图片,姓名这些直观数据。点击进入内页,对应的数据如下图所示。
9139 位艺人在 Python 面前不值一提 Python 爬虫小课 5-9
整理爬取思路为,通过列表页面抓取所有的艺人内页,然后进入内页之后获取详细,将详细数据存档即可完成本案例。

编码时间

单线程爬取

本案例涉及的所有模块可以优先导入

1
2
3
4
5
6
7
8
python复制代码import requests
import re
import json
import threading
import csv
import codecs
import fcntl # 该模块未用到,可以暂时忽略
import time

对于页面的获取与解析相对比较简单,优先对这部分做出说明。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
python复制代码flag_page = 0
def get_list():
global flag_page
while flag_page < 154:
time.sleep(1)
flag_page += 1
print(f"正在爬取{flag_page}页数据")
url = f"http://www.ylq.com/star/list-all-------{flag_page}.html"
try:
r = requests.get(url=url, headers=headers)
pattern = re.compile(
r'<li>[.\s]*<a href="(.*?)" target="_blank">[.\s]*<img src="(.*?)" width="\d+" height="\d+" alt=".*?" /><span class="bg"></span><h2>(.*?)</h2>')
# 获取页面中的所有艺人
famous = pattern.findall(r.text)
print(famous)
except Exception as e:
print(e)

continue

if __name__ == "__main__":
get_list()

上述代码说明:

  • 本案例会利用到简单的多线程操作,所以提前声明一个全局变量 flag_page 方便后续使用
  • 正则表达式需要的反复练习,如果无法一次完整匹配,可以采用多次匹配的方式进行

优化成多线程模式

从现在的代码修改为多线程非常简单,只需要修改函数调用部分的代码即可。具体如下:

1
2
3
4
5
python复制代码if __name__ == "__main__":
for i in range(1, 6):
t = threading.Thread(target=get_list)
t.setName(f't{i}')
t.start()

循环创建 5 个线程,每个线程的名字设置为 tn,线程的初始化与使用代码如下:

1
2
python复制代码 t = threading.Thread(target=get_list) # 初始化
t.start() # 启动

此时代码会同时并发 5 个线程,速度会有极大的提高。

抓取内页

在每个线程获取到数据之后,就可以对内页进行分析了,从上面获取到的解析数据中提取出内页链接。

1
2
3
4
5
6
7
python复制代码# 获取页面中的所有艺人
famous = pattern.findall(r.text)
for user in famous:
# 内页地址
detail_url = user[0]
# print(detail_url)
data = get_detail(detail_url)

接下来扩展 get_detail 函数,该部分主要为正则表达式的应用,函数具体内容如下:

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
python复制代码def get_detail(detail_url):

r = requests.get(url=detail_url, headers=headers)
r.encoding = "utf-8"
html = r.text
# 截取字符串
start = html.find('<div class="sLeft">')
end = html.find('<div class="sRight">')
html = html[start:end]
# 获取姓名和职业
name_type = re.search(
r'<h1>(?P<name>.*?)<span>(?P<type>.*?)</span></h1>', html)
# 获取地区
city = re.search(r'<li><span>地区:</span>(?P<city>.*?)</li>', html)
high = re.search(
r'<li><span>身高:</span>(<a href="(.*?)" target="_blank" title="(.*?)">)?(?P<high>.*?)(</a>)?</li>', html)
weight = re.search(r'<li><span>体重:</span>(?P<weight>.*?)</li>', html)
birthday = re.search(r'<li><span>生日:</span>(?P<birthday>.*?)</li>', html)
star = re.search(
r'<li><span>星座:</span>(<a href="(.*?)" target="_blank" title="(.*?)">)?(?P<star>.*?)(</a>)?</li>', html)
blood = re.search(
r'<li><span>血型:</span>(<a href="(.*?)" target="_blank" title="(.*?)">)?(?P<blood>.*?)(</a>)?</li>', html)

detail = {
'name': name_type.group('name'),
'type': name_type.group('type'),
'city': city.group('city'),
'high': high.group('high'),
'weight': weight.group('weight'),
'birthday': birthday.group('birthday'),
'star': star.group('star'),
'blood': blood.group('blood')
}

return detail

上述内容会将匹配到的数据返回给主函数,代码运行之后,就可以打印出我们想要的信息了。
9139 位艺人在 Python 面前不值一提 Python 爬虫小课 5-9
最后,只需要将数据存在本地的 csv 文件即可,完整代码参照:

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
python复制代码import requests,re,json,threading,csv,codecs,time

headers = {
"user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.121 Safari/537.36"
}

flag_page = 0

def get_detail(detail_url):
r = requests.get(url=detail_url, headers=headers)
r.encoding = "utf-8"
html = r.text
# 截取字符串
start = html.find('<div class="sLeft">')
end = html.find('<div class="sRight">')
html = html[start:end]
# 获取姓名和职业
name_type = re.search(
r'<h1>(?P<name>.*?)<span>(?P<type>.*?)</span></h1>', html)

city = re.search(r'<li><span>地区:</span>(?P<city>.*?)</li>', html)
high = re.search(
r'<li><span>身高:</span>(<a href="(.*?)" target="_blank" title="(.*?)">)?(?P<high>.*?)(</a>)?</li>', html)
weight = re.search(r'<li><span>体重:</span>(?P<weight>.*?)</li>', html)
birthday = re.search(r'<li><span>生日:</span>(?P<birthday>.*?)</li>', html)
star = re.search(
r'<li><span>星座:</span>(<a href="(.*?)" target="_blank" title="(.*?)">)?(?P<star>.*?)(</a>)?</li>', html)
blood = re.search(
r'<li><span>血型:</span>(<a href="(.*?)" target="_blank" title="(.*?)">)?(?P<blood>.*?)(</a>)?</li>', html)

detail = {
'name': name_type.group('name'),
'type': name_type.group('type'),
'city': city.group('city'),
'high': high.group('high'),
'weight': weight.group('weight'),
'birthday': birthday.group('birthday'),
'star': star.group('star'),
'blood': blood.group('blood')
}

return detail

def save_face():
pass

def save(all_data):
# fcntl.flock(f.fileno(), fcntl.LOCK_EX) # 加锁
with open('users.csv', 'a+', newline='', encoding='utf-8-sig') as f:
fieldnames = {'name', 'type', 'city', "high",
'weight', 'birthday', 'star', 'blood'}
writer = csv.DictWriter(f, fieldnames=fieldnames)
for i in all_data:
writer.writerow(i)

def get_list():
global flag_page
# name = threading.currentThread().name
# print(f"当前线程名字为{name}")
while flag_page < 154:
time.sleep(1)
flag_page += 1
print(f"正在爬取{flag_page}页数据")
url = f"http://www.ylq.com/star/list-all-------{flag_page}.html"
try:
r = requests.get(url=url, headers=headers)
pattern = re.compile(
r'<li>[.\s]*<a href="(.*?)" target="_blank">[.\s]*<img src="(.*?)" width="\d+" height="\d+" alt=".*?" /><span class="bg"></span><h2>(.*?)</h2>')
famous = pattern.findall(r.text)
all_data = []
for user in famous:
detail_url = user[0]
# print(detail_url)
data = get_detail(detail_url)
all_data.append(data)

save(all_data)
except Exception as e:
print(e)
print(f"{detail_url}出现问题")
continue

if __name__ == "__main__":
with open('users.csv', 'w', newline='') as f:
fieldnames = {'name', 'type', 'city', "high",
'weight', 'birthday', 'star', 'blood'}
writer = csv.DictWriter(f, fieldnames=fieldnames)
writer.writeheader()
for i in range(1, 6):
t = threading.Thread(target=get_list)
t.setName(f't{i}')
t.start()

数据说明

数据爬取到本地为 500KB 整理量不大,存在很多不详,如果你想要该份数据用于分析,直接运行上述代码即可成功。
9139 位艺人在 Python 面前不值一提 Python 爬虫小课 5-9
代码中需要特别注意的是正则的泛应用,csv 文件的存储(带列头),简单多线程的应用。

本文转载自: 掘金

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

遇到生产bug时,你会慌嘛?

发表于 2021-11-14

前言

大家好,我是捡田螺的小男孩。大家周末愉快呀。

作为程序员,我们经常需要面对bug。刚毕业的孩子或者工作没多久的程序员,遇到生产问题,很容易就惊慌失措。其实,我们都知道,着急是没有用,因为急是解决不了问题的。

今天田螺哥跟大家一起来聊聊,如何应对生产问题。当然,这句话既是对你们说的,也是对我自己说的,我自己也在成长阶段啦。

  • 公众号:捡田螺的小男孩
  1. 调整好自己的心态。

遇到生产问题时,着急是人的一种正常反应,我们要做的是,冷静一下。一定要调整好自己的心态。

因为如果你急的像热锅上的蚂蚁,领导看在眼里,会觉得你并不靠谱,下次有比较重要的开发任务,相信他不会再交给你了,并且后面什么绩效等等,估计也好不了哪里去。

还有一点,着急的话,很容易就分散你解决问题的精力!所以遇到生产问题,需要先调整好自己心态,泰然处之。

  1. 专注于解决问题!

调整好心态之后呢,我们需要专注于如何解决问题。

如果你的问题,是监控报的异常,你需要去仔细查询日志,分析日志,是哪里出的问题。比如,简单的,是不是空指针异常了;又或者是不是没控制好接口幂等,唯一索引报的Duplicate key等等。

如果是CPU负载过高,那就可以打印堆栈信息分析一下。jstack生成线程快照,来分析程序异常,是Java程序员的必备技能。之前写过一篇jstack的文章,大家可以看下哈

给你的Java程序拍个片子吧:jstack命令解析

如果是慢SQL的原因,就分析如何优化,是加索引好呢,还是控制SQL的数据量好呢,等等。

如果是业务投诉,说程序哪里的业务功能实现有问题,那就要好好分析当前的一下关键日志以及,回头看看你的业务代码啦。反正就具体问题具体分析吧,你冷静下来,分析怎么解决你的问题就好了。

  1. 汇报解决方案,分析可能造成的影响

如果你找到了问题出在哪里,那就列出解决方案。比如是慢SQL的原因,那到底要在哪个字段加索引,还是加时间范围控制数据量等等,你就一一列出来,跟上级领导汇报解决方案。

除了汇报解决方案,还有一个很重要的点,那就是分析可能造成的影响!生产上的问题,是会影响到用户的,我们需要分析影响到哪些用户,影响的功能点是什么,以及补救方案。如你的一个空指针异常,影响到客户信息展示,然后你的补救方案,是不是补数就可以了。

  1. 乐于沟通!寻找领导帮助

虽然说,领导更愿意听到的是你的解决方案,而不是你给他抛出问题。但是呢,有时候,我们由于经验不够等原因,有些问题,一时半会确实找不出解决方案。这时候,你可以向有经验的程序员请教,向你的领导寻求帮助。

自己不知道怎么解决的时候,千万不要一个人闷声在那里瞎想!我们要的,就是快速解决问题!

  1. 技术输出,多总结生产问题

解决完生产问题后,建议大家可以写写技术总结,为什么会出现这个问题,如何规避解决这个问题等等。

比如这个问题是,因为delete in 子查询不走索引导致的,那你查下资料,原因是啥嘛。可以写篇技术博客分享出来,总结的时候,自己印象加深了,并且避免公司其他同事下次跟你犯一样的错误嘛。之前有篇生产问题的技术总结,大家可以看下哈:

生产问题分析!delete in子查询不走索引?!

  1. 平时开发设计时,统筹兼顾!

从出现的生产问题,可以反思我们平时开发设计时,要怎么想得周全一点。比如来个简单的例子,你需要查询个用户信息,假设查不到,你怎么处理,代码是不是需要做下非空判断。再比如,你在登陆注册加个通知功能,是不是要考虑异步呢。之前写过一篇如何减少bug的文章,大家有兴趣可以看下哈。

聊聊日常开发中,如何减少bug呢?

何为统筹兼顾呢?开发设计过程中,处理比较明显的bug需要考虑规避。并且需要考虑,假设你这样设计实现,可能会有什么问题呢,以及如果有问题,你的补救方案是怎样呢。

  • 比如你设计一个新功能开发,如果这个接口出问题了,是不是会导致老功能有问题呢?那你是不是可以加个开关什么的呢?
  • 比如对方发个广播消息,你是不是需要考虑,如果消息丢了,你没收到,怎么处理呢?业务可以接受嘛?是不是可以下次消息再来时,跟上次对比一下等等。

最后

好啦,今天就聊这么多哈,希望对大家有帮助呀。所以遇到生产问题,我们千万不能慌!不怕你说我啰嗦,总结一下:

  • 调整好自己的心态。
  • 专注于解决问题!
  • 跟领导汇报解决方案,分析可能造成的影响
  • 乐于沟通!寻找领导帮助
  • 技术输出,多总结生产问题
  • 平时开发设计时,统筹兼顾!

本文转载自: 掘金

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

【设计模式系列】通俗易懂的门面模式

发表于 2021-11-14

这是我参与11月更文挑战的第14天,活动详情查看:2021最后一次更文挑战

前言

最近同事二狗子要装修房子,他装修之前呢,得先找个设计师设计,然后还需要找个水电工给新房子改水电,再然后,还得找泥瓦工,刷墙贴瓷砖之类的,这个过程中要招人,谈价钱,跟进度,二狗子一个写代码的程序员,他哪会这些呀,太麻烦了,要是有人能帮他干就好了。

image-20211114124413210

哎,这事儿好办呀,找个装修公司呀。

有了装修公司,二狗子就只需要和装修公司谈好价钱,然后等房子装修就好,不用再操心一些细节。

这和我们今天讲的门面设计模式是一个道理。

门面模式定义

GoF门面设计模式定义:为子系统中的一组接口提供统一的接口。门面模式定义了使子系统更易于使用的高级接口。

门面设计模式(Facade Design Pattern)是结构设计模式中的一种,门面模式的作用是为了帮助客户端更容易地与系统交互。

门面模式结构

我们来看一下,在上面的例子中,都需要有些什么元素。

子系统

在上面的例子中,我们首先需要有设计师,水电工,泥瓦匠这些功能对应的接口,供门面(装修公司)来使用。

设计师

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码/**
* @author 小黑说Java
* @ClassName Designer
* @Description 设计师
* @date 2021/11/14
**/
public class Designer {

public void design() {
System.out.println("设计师设计");
}
}

水电工

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码/**
* @author 小黑说Java
* @ClassName WaterElectricWorker
* @Description 水电工
* @date 2021/11/14
**/
public class WaterElectricWorker {

public void work() {
System.out.println("水电工修水电");
}
}

泥瓦工

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码/**
* @author 小黑说Java
* @ClassName bricklayer
* @Description 泥瓦工
* @date 2021/11/14
**/
public class Bricklayer {

public void work() {
System.out.println("泥瓦匠贴瓷砖");
}
}

门面

然后我们建立一个门面类,也就是装修公司。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码/**
* @author 小黑说Java
* @ClassName DecorationCompany
* @Description 装修公司
* @date 2021/11/14
**/
public class DecorationCompany {

public static void decorate() {
System.out.println("装修公司整合~");
new Designer().design();
new WaterElectricWorker().work();
new Bricklayer().work();
System.out.println("装修完毕");
}
}

在我的同事二狗子就可以来进行装修了,我们来看一下二狗子使用门面和不使用门面的不同实现方式。

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码public class DecorationTest {

public static void main(String[] args) {
// 不使用门面模式
new Designer().design();
new WaterElectricWorker().work();
new Bricklayer().work();

// 使用门面模式
DecorationCompany.decorate();
}
}

如你所见,使用门面模式可以避免在客户端有大量的实现逻辑,客户端可以更简单清晰的使用服务。

使用门面模式要遵循以下特点。

  • 门面设计模式更像是客户端应用程序的助手,它不会对客户端隐藏子系统接口,是否使用门面完全取决于客户端代码;
  • 门面设计模式可以应用于开发的任何时刻,通常是在接口数量增加和系统变得复杂时;
  • 子系统接口不知道门面,它们不应该有任何对门面接口的引用;

门面模式的优缺点

优点

  • 松散耦合: 门面模式松散了客户端与子系统的耦合关系,让子系统内部的模块能更容易扩展和维护。
  • 简单易用: 门面模式让子系统更加易用,客户端不再需要了解子系统内部的实现,也不需要跟众多子系统内部的模块进行交互,只需要跟门面类交互就可以了。:门面模式让子系统更加易用,客户端不再需要了解子系统内部的实现,也不需要跟众多子系统内部的模块进行交互,只需要跟门面类交互就可以了。

缺点

  • 当增加子系统时可能带来未知风险;
  • 门面不符合开闭原则;

门面模式与代理模式的区别

代理模式侧重于对原对象的访问控制;

代理模式会实现原对象的接口;

代理模式只代理一个接口;

门面模式更侧重于整合子系统资源;

门面模式和子系统对象具有不同的接口抽象;

门面模式代理的是一系列接口。


以上就是门面模式的内容,如果对你有所帮助,点赞是对我最大的鼓励!

本文转载自: 掘金

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

IPVlan 源码探秘

发表于 2021-11-14

ipvlan是Linux提供的一个内核网络虚拟化的机制,最近在支持集团内某些业务的时候直接使用了ipvlan的网络方案,在解决问题和方案设计的时候也深入看了一下这块源码,看了一下并不复杂,索性写下来记录一下,以飨读者。

本文会省略一些具体的模拟环境的测试结论,因此更适合有ipvlan使用和调戏经验的同学阅读。

总论

ipvlan的使用体验实际上跟bridge是比较接近的,在配置好子网卡和IP地址后,就可以借助underlay网络互通了,在Linux Howto上有配置ipvlan的详细指引:

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
bash复制代码  +=============================================================+
| Host: host1 |
| |
| +----------------------+ +----------------------+ |
| | NS:ns0 | | NS:ns1 | |
| | | | | |
| | | | | |
| | ipvl0 | | ipvl1 | |
| +----------#-----------+ +-----------#----------+ |
| # # |
| ################################ |
| # eth0 |
+==============================#==============================+


(a) Create two network namespaces - ns0, ns1
ip netns add ns0
ip netns add ns1

(b) Create two ipvlan slaves on eth0 (master device)
ip link add link eth0 ipvl0 type ipvlan mode l2
ip link add link eth0 ipvl1 type ipvlan mode l2

(c) Assign slaves to the respective network namespaces
ip link set dev ipvl0 netns ns0
ip link set dev ipvl1 netns ns1

(d) Now switch to the namespace (ns0 or ns1) to configure the slave devices
- For ns0
(1) ip netns exec ns0 bash
(2) ip link set dev ipvl0 up
(3) ip link set dev lo up
(4) ip -4 addr add 127.0.0.1 dev lo
(5) ip -4 addr add $IPADDR dev ipvl0
(6) ip -4 route add default via $ROUTER dev ipvl0
- For ns1
(1) ip netns exec ns1 bash
(2) ip link set dev ipvl1 up
(3) ip link set dev lo up
(4) ip -4 addr add 127.0.0.1 dev lo
(5) ip -4 addr add $IPADDR dev ipvl1
(6) ip -4 route add default via $ROUTER dev ipvl1

具体说来,每一个ipvlan网卡都会挂在一个父网卡上面,同一个父网卡下面挂的ipvlan默认是三层互通的(除非配置了private模式)并且使用相同的mac地址,而不同父网卡之间的子网卡则在ipvlan这一层相互隔离,从而形成了一种类似于vlan的隔离机制,因此取名叫做ipvlan。

但是这种隔离的关系仅仅是在Linux系统内部生效的,报文在出ipvlan子网卡之后会因ipvlan的各种配置和外部网络因素形成各种复杂的关系,本文就先从源码角度入手看一下ipvlan的内部实现,再配合外部网络常见的各种环境,对ipvlan可能的业务场景和技术方案做一些分析。

ipvlan基本的转发思路以每个父网卡作为一个隔离域配置一个hash表,每个子网卡在更新IP的时候把IP地址注册到这个hash表中,ipvlan再转发流程里面以这个ip-网卡之间的hash作为转发依据。

L2模式

对于L2模式而言,其与L3模式的主要区别在于这种模式下,还是保留了一些二层相关的流程到达ipvlan子网卡这里的,主要包括ARP流程和广播报文。

话不多说,我们可以先看下对于L2模式而言的关键转发逻辑:

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
c复制代码static int ipvlan_xmit_mode_l2(struct sk_buff *skb, struct net_device *dev)
{
const struct ipvl_dev *ipvlan = netdev_priv(dev);
struct ethhdr *eth = eth_hdr(skb);
struct ipvl_addr *addr;
void *lyr3h;
int addr_type;

if (!ipvlan_is_vepa(ipvlan->port) &&
ether_addr_equal(eth->h_dest, eth->h_source)) {
lyr3h = ipvlan_get_L3_hdr(ipvlan->port, skb, &addr_type);
if (lyr3h) {
addr = ipvlan_addr_lookup(ipvlan->port, lyr3h, addr_type, true);
if (addr) {
if (ipvlan_is_private(ipvlan->port)) {
consume_skb(skb);
return NET_XMIT_DROP;
}
return ipvlan_rcv_frame(addr, &skb, true);
}
}
skb = skb_share_check(skb, GFP_ATOMIC);
if (!skb)
return NET_XMIT_DROP;

/* Packet definitely does not belong to any of the
* virtual devices, but the dest is local. So forward
* the skb for the main-dev. At the RX side we just return
* RX_PASS for it to be processed further on the stack.
*/
return dev_forward_skb(ipvlan->phy_dev, skb);

} else if (is_multicast_ether_addr(eth->h_dest)) {
ipvlan_skb_crossing_ns(skb, NULL);
ipvlan_multicast_enqueue(ipvlan->port, skb, true);
return NET_XMIT_SUCCESS;
}

skb->dev = ipvlan->phy_dev;
return dev_queue_xmit(skb);
}

兄弟网卡间的转发逻辑

这段代码比较清晰的描述了对于L2模式的转发逻辑,在不考虑vepa模式的情况下(这个模式会在4.1这个部分简单讲下),会首先判断一下报文的源mac和目的mac是否一致,如果一致则认为是ipvlan内部转发转流程,判断是否有三层头,有三层头自然就是带有IP层的报文,然后就会寻找挂在这个父网卡下的目的IP所在的子网卡,直接调用该网卡的ipvlan_rcv_frame函数,送到对应的子网卡下。

跨主机的转发逻辑

实际上对于ipvlan而言,还有一种重要场景就是跨linux之间的转发,对于这种情况,l2模式采用的方法是简单粗暴的:直接调用skb->dev = ipvlan->phy_dev; dev_queue_xmit(skb);通过父网卡直接送出去。

\

对于多播报文的处理

多播报文的处理其实也不需要太多关注,对于多播报文而言,只要ipvlan发现目的mac是一个多播地址,就会直接把他塞到backlog队列里去做转发。

一些衍生的问题

这里面有几个比较值得玩味的细节:

  • 为什么源mac和目的mac地址一致就认为是ipvlan内部转发流程呢?
    • 因为ipvlan这个神奇的模式,ipvlan子网卡会继承父网卡的mac地址,这也是他和macvlan模式的明显区别之一,所以兄弟网卡之间的mac地址必然是相同的。
  • l2模式在跨主机转发的场景下是直接使用父网卡link的dev_queue_xmit发送的,bypass了所有的三四层的协议栈,这也就意味着ipvlan子网卡和父网卡在二层上是隔离的,和宿主机上的其他网卡更加是隔离的,这可能会给某些需要父子网卡互通的场景带来一些奇妙的麻烦,当然,你也不能指望L3模式帮你避开这个问题。

L3模式

对于L3模式而言,会给网卡做一些比较奇妙的配置,首先所有的多播和广播报文会在L3模式的网卡里面直接丢弃,等于多播完全在这种模式里面废除了;另外就是网卡会配置成noarp模式,不会响应和发出任何arp请求。那么就由小朋友要问了:那mac地址可怎么填呀?答案是对于子网卡而言,其发出的三层报文,源mac和目的mac都会设置成自己的mac地址。

简单过一下L3模式的转发代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
c复制代码static int ipvlan_xmit_mode_l3(struct sk_buff *skb, struct net_device *dev)
{
const struct ipvl_dev *ipvlan = netdev_priv(dev);
void *lyr3h;
struct ipvl_addr *addr;
int addr_type;

lyr3h = ipvlan_get_L3_hdr(ipvlan->port, skb, &addr_type);
if (!lyr3h)
goto out;

if (!ipvlan_is_vepa(ipvlan->port)) {
addr = ipvlan_addr_lookup(ipvlan->port, lyr3h, addr_type, true);
if (addr) {
if (ipvlan_is_private(ipvlan->port)) {
consume_skb(skb);
return NET_XMIT_DROP;
}
return ipvlan_rcv_frame(addr, &skb, true);
}
}
out:
ipvlan_skb_crossing_ns(skb, ipvlan->phy_dev);
return ipvlan_process_outbound(skb);
}


// 哎呀,他还调用了ipvlan_process_outbound,那ipvlan_process_outbound在下面了:
static int ipvlan_process_outbound(struct sk_buff *skb)
{
struct ethhdr *ethh = eth_hdr(skb);
int ret = NET_XMIT_DROP;

/* The ipvlan is a pseudo-L2 device, so the packets that we receive
* will have L2; which need to discarded and processed further
* in the net-ns of the main-device.
*/
if (skb_mac_header_was_set(skb)) {
/* In this mode we dont care about
* multicast and broadcast traffic */
if (is_multicast_ether_addr(ethh->h_dest)) {
pr_debug_ratelimited(
"Dropped {multi|broad}cast of type=[%x]\n",
ntohs(skb->protocol));
kfree_skb(skb);
goto out;
}

skb_pull(skb, sizeof(*ethh));
skb->mac_header = (typeof(skb->mac_header))~0U;
skb_reset_network_header(skb);
}

if (skb->protocol == htons(ETH_P_IPV6))
ret = ipvlan_process_v6_outbound(skb);
else if (skb->protocol == htons(ETH_P_IP))
ret = ipvlan_process_v4_outbound(skb);
else {
pr_warn_ratelimited("Dropped outbound packet type=%x\n",
ntohs(skb->protocol));
kfree_skb(skb);
}
out:
return ret;
}

// 还有一层?没关系,我们继续贴代码
static int ipvlan_process_v4_outbound(struct sk_buff *skb)
{
const struct iphdr *ip4h = ip_hdr(skb);
struct net_device *dev = skb->dev;
struct net *net = dev_net(dev);
struct rtable *rt;
int err, ret = NET_XMIT_DROP;
struct flowi4 fl4 = {
.flowi4_oif = dev->ifindex,
.flowi4_tos = RT_TOS(ip4h->tos),
.flowi4_flags = FLOWI_FLAG_ANYSRC,
.flowi4_mark = skb->mark,
.daddr = ip4h->daddr,
.saddr = ip4h->saddr,
};

rt = ip_route_output_flow(net, &fl4, NULL);
if (IS_ERR(rt))
goto err;

if (rt->rt_type != RTN_UNICAST && rt->rt_type != RTN_LOCAL) {
ip_rt_put(rt);
goto err;
}
skb_dst_set(skb, &rt->dst);
err = ip_local_out(net, skb->sk, skb);
if (unlikely(net_xmit_eval(err)))
dev->stats.tx_errors++;
else
ret = NET_XMIT_SUCCESS;
goto out;
err:
dev->stats.tx_errors++;
kfree_skb(skb);
out:
return ret;
}

兄弟网卡之间的转发逻辑

这个转发代码是不是有点过于简单?可以看出,兄弟网卡的判断逻辑跟L2是有所区别的,不能通过目的mac来判断了,因为不管是不是给兄弟发,目的mac都是一样的,那只能直接根据目的IP查表了,能查到就是兄弟的,直接调用兄弟的ipvlan_rcv_frame给他送过去,不用谢。

跨主机的转发逻辑

对于跨主机的转发, 和L2模式有显著的不同,不同点在于:

  1. 在outbound函数中增加了对mac_header的处理,对于目的MAC为多播的报文直接丢弃,然后把skbuf的mac_header直接清空,这一步应该是为了防止mac地址的配置影响路由子系统对发包的判断。
  2. 欸……等等,你刚才说了“路由子系统”对吧?嗯是的,可以看出,对于ipv4而言,发送的核心代码是ip_route_output_flow 和 ip_local_out ,这也就意味着,对于L3模式而言,报文的发送是经过了父网卡的路由子系统的,也就是最终网卡的发包是由父网卡路由子系统决定的下一跳……这是和L2模式的核心区别。

多播报文的处理

无论是收还是发,都是直接丢弃了事,就是这么残忍。

一些衍生的思考

  • 在2.2中说到,在L2模式下父子网卡之间二层隔离,那这个问题在L3模式下是不是就不存在了?
    • 是的,如果你直接在ipvlan子网卡ping父网卡,那父网卡是会在协议栈处理这个报文,并且能够回包的……但是你需要把子网卡的IP段设置在Host里面配置Link路由,报文才能正确地回到子网卡,这对于绝大多数容器网络应用来说并不是一个很明智的做法——不管你的host和容器是不是处于同一个网段。

关于那些flag

4.1 vepa这玩意

vepa这玩意是802.1Qbg提出的一种虚拟网络方案,其有独特的背景还是值得说一说的。

在古老的计算机网络体系里面,“交换”一般是指的基于MAC地址的二层转发,其对于未知主机的触达主要是依赖于“泛洪”,即对于未知目的MAC的报文,就很暴力地转发到全部物理端口,但是这个泛洪和转发,通常都是不包含这个报文的源物理端口的,原因嘛,也很简单,一个报文从这个口子发出来了,我再给他转发回去,万一下面也是个交换机,也给我原样转回来,这流量岂不是原地爆炸?不行不行。STP原理上也是不支持这样的转发模式。

但是随着云网络这玩意的兴起,交换机下面挂着的物理机也会有很多VM、容器之类的,有时候同一台物理机的VM之间互相通信,报文也直接怼给交换机了,这就是vepa模式。

那交换机转吗?转的话,那就开启一个hairpin模式,老老实实的转回去。

所以可以看出,vepa模式就是不管是不是同一物理机,我协议栈也不管,就是往外发,就是莽,交换机来给我处理好。所以这种模式通常是需要交换机配置来配合的,一般场景下,还真的用不到。

看ipvlan源代码也看出来,对于vepa模式,不管l2还是l3,都直接发出去了。

4.2 关于L3S和L3模式之间的差异

对于L3和L3s之间的差别,主要体现在接收流程上,2和3两节基本上没有讲接受逻辑,因为基本上乏善可陈,但是对于L3和L3s之间的差异,主要就体现在这个接收流程了,还是值得看一下的,关注一下rx_handle的相关逻辑:

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
c复制代码rx_handler_result_t ipvlan_handle_frame(struct sk_buff **pskb)
{
struct sk_buff *skb = *pskb;
struct ipvl_port *port = ipvlan_port_get_rcu(skb->dev);

if (!port)
return RX_HANDLER_PASS;

switch (port->mode) {
case IPVLAN_MODE_L2:
return ipvlan_handle_mode_l2(pskb, port);
case IPVLAN_MODE_L3:
return ipvlan_handle_mode_l3(pskb, port);
case IPVLAN_MODE_L3S:
return RX_HANDLER_PASS;
}

/* Should not reach here */
WARN_ONCE(true, "ipvlan_handle_frame() called for mode = [%hx]\n",
port->mode);
kfree_skb(skb);
return RX_HANDLER_CONSUMED;
}

static rx_handler_result_t ipvlan_handle_mode_l3(struct sk_buff **pskb,
struct ipvl_port *port)
{
void *lyr3h;
int addr_type;
struct ipvl_addr *addr;
struct sk_buff *skb = *pskb;
rx_handler_result_t ret = RX_HANDLER_PASS;

lyr3h = ipvlan_get_L3_hdr(port, skb, &addr_type);
if (!lyr3h)
goto out;

addr = ipvlan_addr_lookup(port, lyr3h, addr_type, true);
if (addr)
ret = ipvlan_rcv_frame(addr, pskb, false);

out:
return ret;
}

可以看出来,l3模式的收包基本上走的还是查IP->确定子网卡->转发的老套路,而L3S却在ipvlan_handle_frame的流程里直接返回了RX_HANDLER_PASS,走了Host的协议栈,欸,这是几个意思?
不过可以看出,在L3S的网卡初始化过程中,ipvlan悄悄配置了几个回调进去:

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
c复制代码static const struct l3mdev_ops ipvl_l3mdev_ops = {
.l3mdev_l3_rcv = ipvlan_l3_rcv,
};



static int ipvlan_set_port_mode(struct ipvl_port *port, u16 nval)
{
struct ipvl_dev *ipvlan;
struct net_device *mdev = port->dev;
unsigned int flags;
int err;

ASSERT_RTNL();
// .... 省略一些不太关心的代码
if (nval == IPVLAN_MODE_L3S) {
/* New mode is L3S */
err = ipvlan_register_nf_hook(read_pnet(&port->pnet));
if (!err) {
mdev->l3mdev_ops = &ipvl_l3mdev_ops;
mdev->priv_flags |= IFF_L3MDEV_RX_HANDLER;
} else
goto fail;
} else if (port->mode == IPVLAN_MODE_L3S) {
/* Old mode was L3S */
mdev->priv_flags &= ~IFF_L3MDEV_RX_HANDLER;
ipvlan_unregister_nf_hook(read_pnet(&port->pnet));
mdev->l3mdev_ops = NULL;
}

// ....

}

oh,他在netfilter的l3mdev_l3_rcv注册了一个ipvlan_l3_rcv的回调,结合上面的分析,l3s的收包流程会一路走到PREROUTING hook点,找到要转发的子网卡后,再通过子网卡的LOCAL_IN hook点到达。

这也就意味着,在ipvlan子网卡的PreRouting里面常做的一些配置是可能被L3S模式绕过的,比如nat。目前我还没有遇到过需要使用L3S模式的应用场景。

4.3 关于private

private主要用于隔离兄弟网卡之间的报文转发。对于一些只提供南北向流量服务,又对隔离性有需求的应用可以使用private模式。

  1. 总结一下

通过上面的代码分析,可以看出ipvlan这玩意,提供了最基本的转发和隔离能力,并且代码这块短小精悍,省略了很多麻烦的处理流程,我们实际测试下来性能也相当可观。其基于网卡粒度的隔离思路,配合vlan子网卡这个模式可以在IDC配合交换机配置内轻松地实现vlan隔离。其与三层转发强关联、子网卡共享mac地址的方式也更适合带有三层交换网关的业务隔离系统,与使用去堆叠架构或者云上网络的纯三层转发系统相性非常好,是云上容器网络的一个绝佳方案。

但是可以看出ipvlan是一个underlay的解决方案,因此也需要上联的云网络或者物理网络的配置配合,考虑到物理网络更新的复杂性和风险,其实更适合做云上容器网络的解决方案。

本文转载自: 掘金

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

请问你是学Java的吗?那Jwt你知道吗?

发表于 2021-11-14

「这是我参与11月更文挑战的第14天,活动详情查看:2021最后一次更文挑战」

Jwt是什么?

概述:

Json web token (JWT), 是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准((RFC 7519).该token被设计为紧凑且安全的,特别适用于分布式站点的单点登录(SSO)场景。JWT的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源,也可以增加一些额外的其它业务逻辑所必须的声明信息,该token也可直接被用于认证,也可被加密。

Jwt能干什么?

场景: 可用于跨域身份验证。

传统身份验证过程如下:

  1. 用户向服务器发送用户名和密码。
  2. 验证服务器后,相关数据(如用户角色,登录时间等)将保存在当前会话中。
  3. 服务器向用户返回session_id,session信息都会写入到用户的Cookie。
  4. 用户的每个后续请求都将通过在Cookie中取出session_id传给服务器。
  5. 服务器收到session_id并对比之前保存的数据,确认用户的身份。

缺点: 此方案会导致逐渐增大服务器的负荷,并且cookie存储在客户端中容易被高手截获进而进行伪造,导致信息安全的问题发生。

相比较传统的session广播存入cookie中的解决方案,Jwt更加的灵活通过客户端保存数据,而服务器不用保存会话的数据,且自身的设计可以有效的防止信息被非法伪造。

Jwt怎么使用?

1、首先看看Jwt的组成

典型的,一个JWT看起来如下图:

image.png

该对象为一个很长的字符串,字符之间通过”.”分隔符分为三个子串。

每一个子串表示了一个功能块,总共有以下三个部分:JWT头、有效载荷和签名

JWT头:JWT头部分是一个描述JWT元数据的JSON对象,通常如下所示。

1
2
3
4
json复制代码    {
"alg": "HS256",
"typ": "JWT"
}

在上面的代码中,alg属性表示签名使用的算法,默认为HMAC SHA256(写为HS256);typ属性表示令牌的类型,JWT令牌统一写为JWT。最后,使用Base64 URL算法将上述JSON对象转换为字符串保存。

有效载荷:有效载荷部分,是JWT的主体内容部分,也是一个JSON对象,包含需要传递的数据。 JWT指定七个默认字段供选择。(都为可选设置)

1
2
3
4
5
6
7
lua复制代码    iss:发行人
exp:到期时间
sub:主题
aud:用户
nbf:在此之前不可用
iat:发布时间
jti:JWT ID用于标识该JWT

除以上默认字段外,我们还可以自定义私有字段,如下例:

1
2
3
4
5
json复制代码    {
"sub": "123456",
"name": "mingru",
"acl": true
}

请注意,默认情况下JWT是未加密的,任何人都可以解读其内容,因此不要构建隐私信息字段,存放保密信息,以防止信息泄露。

JSON对象也使用Base64 URL算法转换为字符串保存。

签名哈希

  1. 签名哈希部分是对上面两部分数据签名,通过指定的算法生成哈希,以确保数据不会被篡改。
  2. 首先,需要指定一个secret(密钥)。该密码仅仅为保存在服务器中,并且不能向用户公开。然后,使用Jwt头中指定的签名算法(默认情况下为HMAC SHA256)根据以下公式生成签名。
1
scss复制代码    HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(claims), secret)
  1. 在计算出签名哈希后,JWT头,有效载荷和签名哈希的三个部分组合成一个字符串,每个部分用”.”分隔,就构成整个JWT对象。

Base64URL算法

  1. 如前所述,JWT头和有效载荷序列化的算法都用到了Base64URL。该算法和常见Base64算法类似,稍有差别。
  2. 作为令牌的JWT可以放在URL中(例如index/?token=xxx)。 Base64中用的三个字符是”+”,”/“和”=”,由于在URL中有特殊含义,因此Base64URL中对他们做了替换:”=”去掉,”+”用”-“替换,”/“用”_“替换,这就是Base64URL算法。

2、Jwt的大体工作流程

  1. 用户在服务器身份验证之后,将生成一个JSON对象并将其发送回用户,之后,当用户与服务器通信时,客户在请求中发回JSON对象。
  2. 服务器仅依赖于这个JSON对象来标识用户。为了防止用户篡改数据,服务器将在生成对象时添加签名。而当用户再次访问同一个系统中的其他服务时请求头中便会携带一个token字符串,用户身份的验证通过解析token和之前合法的相对比,查看是否有被修改过。

服务器不保存任何会话数据,即服务器变为无状态,使其更容易扩展。

3、如何防止用户信息(有效载荷)被高手获取到并且被修改呢?

通过Jwt的token的生成就可以解释,再生成签名哈希时HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(claims), secret),在此步骤生成中,每一个数据体生成的签名哈希是独一无二的,而当你确实获取到用户的信息时,虽然可以至少其中的数据信息,但是如果想要对其进行非法的修改时,可想而知,一次对其进行验证时生成的token值肯定会产生改变,这样服务器就会对此次访问做出判断,阻止其继续操作。

Jwt的token字符串生成代码

1
2
3
4
5
xml复制代码//Jwt依赖
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
</dependency>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
arduino复制代码    public class JwtUtils {

//设置的密钥
private static final String SIGN="@das$25*j^";

//获取token字符串
public static String getToken(Map<String,String> map){
//设置token过期时间
Calendar instance = Calendar.getInstance();
instance.add(Calendar.DATE,7);

JWTCreator.Builder builder = JWT.create();
map.forEach((k,v)->{
builder.withClaim(k,v);
});
String token = builder.withExpiresAt(instance.getTime()) //指定过期时间
.sign(Algorithm.HMAC256(SIGN)); //指定加密算法

return token;
}
}

验证合法代码

1
2
3
4
typescript复制代码    //验证是否合法
public static DecodedJWT verify(String token){
return JWT.require(Algorithm.HMAC256(SIGN)).build().verify(token);
}

至此结束!希望本文能够对你有所帮助。。。

本文转载自: 掘金

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

dart系列之 在dart中使用packages 简介 pu

发表于 2021-11-14

「这是我参与11月更文挑战的第13天,活动详情查看:2021最后一次更文挑战」

简介

java中使用jar包来封装有用的功能,然后将其分发到maven仓库中,供其他人使用。同样的在dart中也有类似的概念叫做packages。packages就是可以用来共享的软件包,可以包含libraries和tools。

你可以在pub.dev网站中查到dart中所有的共享packages的信息。 那么怎么在一个dart项目中使用这些packages呢?

pubspec.yaml

简单点讲,一个dart的package就是包含pubspec.yaml的目录。pubspec.yaml是一个描述文件,用来表明该package的元信息,包括当前package的名字,版本号和依赖信息等。

要想使用pub.dev上的packages,只需要在pubspec.yaml引入对应的依赖即可。

我们举个例子:

1
2
3
4
5
6
7
8
9
10
yaml复制代码name: app2
description: a demo app
version: 1.0.0+1

environment:
sdk: ">=2.7.0 <3.0.0"

dependencies:
image_picker: ^0.6.7+22
video_player: ^0.10.12+5

这里我们的引入了两个依赖包,分别是image_picker和video_player。

get packages

当我们修改了pubspec.yaml之后,其实对应的package并没有下载到本地来,还需要通过下面的命令来下载对应的packages:

1
2
vbnet复制代码 cd <path-to-my_app>
dart pub get

dart pub get会根据pubspec.yaml中配置的内容下载对应的包,并放置在系统缓存中。

在Mac或者Linux系统中,这个缓存目录的地址是:~/.pub-cache,在windows中这个目录地址是:%LOCALAPPDATA%\Pub\Cache。

当然,你也可以通过设置PUB_CACHE来更换这个地址。

如果你依赖的包又依赖其他的包的话,其他依赖包也会被下载下来。

当下载完依赖包之后,dart会在 .dart_tool/目录中创建一个 package_config.json文件,用来表示当前项目和系统缓存包的映射关系。

使用packages

万事俱备,只欠东风。现在包也有了,剩下就是使用了。

使用libary可以用关键字import。如果是dart SDK中的包,则以dart:开头:

1
arduino复制代码 import 'dart:html';

如果是第三方包,则以package: 开头:

1
arduino复制代码import 'package:test/test.dart';

引入的libary还可以被重命名:

1
2
3
4
5
6
7
8
dart复制代码import 'package:lib1/lib1.dart';
import 'package:lib2/lib2.dart' as lib2;

// Uses Element from lib1.
Element element1 = Element();

// Uses Element from lib2.
lib2.Element element2 = lib2.Element();

还可以使用show和hide引入部分library:

1
2
3
4
5
dart复制代码// Import only foo.
import 'package:lib1/lib1.dart' show foo;

// Import all names EXCEPT foo.
import 'package:lib2/lib2.dart' hide foo;

默认情况下,引入的包是初始加载的,如果某些包特别大,或者你想要在使用的时候再进行加载,则可以使用deferred关键字进行延时加载:

1
dart复制代码import 'package:greetings/hello.dart' deferred as hello;

在使用的时候,需要显示调用loadLibrary() 方法,来加载对应的library:

1
2
3
4
csharp复制代码Future<void> greet() async {
await hello.loadLibrary();
hello.printGreeting();
}

升级依赖

在第一次运行dart pub get 之后,dart会创建一个pubspec.lock文件,用来锁定依赖包的版本号,如果是在团队协作中,这个lock文件是特别有用的,它可以保证团队中所有成员使用的都是同一个版本的依赖包。

当你想升级对应的依赖的时候,可以使用dart pub upgrade命令,对依赖包进行升级。

dart pub upgrade会根据最新的可用包来生成最新的lock文件。

当然,你也可以指定升级某个特定的依赖包:

1
rust复制代码dart pub upgrade image_picker

要想查看最新依赖包的最新版本,可以使用:

1
rust复制代码dart pub outdated

总结

以上就是dart中packages的使用。

本文已收录于 www.flydean.com/09-dart-pac…

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

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

本文转载自: 掘金

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

Spring IOC容器初始化原理分析 (第六节上)

发表于 2021-11-14

「这是我参与11月更文挑战的第14天,活动详情查看:2021最后一次更文挑战」

1.前言

本文主要讲Spring IOC容器初始化过程中的 finishBeanFactoryInitialization(beanFactory) 方法,如想看之前的内容可查看 Spring IOC容器初始化原理分析 (第五节)

finishBeanFactoryInitialization(beanFactory) 是在Spring IOC 容器初始化过程中 refresh()中调用的,它的主要作用是实例化所有非懒加载的单实例bean

2.refresh()源码

如果需要复制代码,可前往Spring IOC容器初始化原理分析 (第一节)

refresh.jpg

3.finishBeanFactoryInitialization(beanFactory) 源码

2021-11-13_103149.jpg

3.1 第一步详解

这里先把变量的源码贴一下

图片.png
所以到这里 第一步的作用就很简单明了了,先判断 beanFactory 中 是否包含一个名为 conversionService 的bean 且 它的类型是 ConversionService.class 如果有的话,就把它设置到 beanFactory 的 conversionService属性中去。 ConversionService.class是一个用于系统类型转换的接口,我们可以实现它,自定义一些类型转换方式。

3.2 第二步详解

hasEmbeddedValueResolver 的默认实现如下图
图片.png
图片.png
其实这步的主要作用就是判断 embeddedValueResolvers 中是否有 StringValueResolver 对象,如果没有的话,给它注册一个默认的 StringValueResolver 对象,这里StringValueResolver 的作用主要用用来解析注解中字符串的属性值。

3.3第三步详解

这步的主要作用就是,初始化所有的 LoadTimeWeaverAware ,以便尽早的注册它们的转换器。LoadTimeWeaverAware 的主要作用是 在加载sring bean 织入第三方模块,这和spring aop 相关。这里就先不细讲了,想了解的话大家可以官网扒一扒。

3.4 第四步详解

这步作用比较直接:停止使用临时类加载器进行类型匹配,即把临时类型加载器设为null

3.5 第五步详解

缓存所有的bean定义的元数据,不希望后面还修改,即bean 即将被实例化,不希望它的数据被修改。源码如下:

图片.png

4.preInstantiateSingletons() (第六步)

这步的作用就是实例化所有剩余的 非懒加载的单实例bean,接下来我们一步步分析下它的实例化过程

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
scss复制代码public void preInstantiateSingletons() throws BeansException {
// 日志记录
if (logger.isTraceEnabled()) {
logger.trace("Pre-instantiating singletons in " + this);
}

// Iterate over a copy to allow for init methods which in turn register new bean definitions.
// While this may not be part of the regular factory bootstrap, it does otherwise work fine.
// 创建this.beanDefinitionNames 的副本用于后续的遍历,以允许 init方法注册新的bean定义
List<String> beanNames = new ArrayList<>(this.beanDefinitionNames);

//触发所有非懒加载的单例bean的初始化
// 遍历所有的 beanName
for (String beanName : beanNames) {
// 根据beanName获取对应的MergedBeanDefinition 这个方法作用 我在4.1中细讲,大家可以往下看
RootBeanDefinition bd = getMergedLocalBeanDefinition(beanName);
// 判断当它 非抽象,单例,非懒加载 的时候进入
if (!bd.isAbstract() && bd.isSingleton() && !bd.isLazyInit()) {
// 判断是否是FactoryBean 这个方法 4.2 中细讲
if (isFactoryBean(beanName)) {
// 得到对应的bean 这个我们会在4.4中 细讲
Object bean = getBean(FACTORY_BEAN_PREFIX + beanName);
// 当 bean是 FactoryBean 类型时,转换一下它的类型
if (bean instanceof FactoryBean) {
final FactoryBean<?> factory = (FactoryBean<?>) bean;
// 它表示,是否立即初始化的标识
boolean isEagerInit;
// 这一部分主要是判断它是否需要立即初始化。
if (System.getSecurityManager() != null && factory instanceof SmartFactoryBean) {
// 这个我们在 4.5 中说一下
isEagerInit = AccessController.doPrivileged((PrivilegedAction<Boolean>)
((SmartFactoryBean<?>) factory)::isEagerInit,
getAccessControlContext());
}
else {
isEagerInit = (factory instanceof SmartFactoryBean &&
((SmartFactoryBean<?>) factory).isEagerInit());
}
if (isEagerInit) {
getBean(beanName);
}
}
}
else {
getBean(beanName);
}
}
}

// Trigger post-initialization callback for all applicable beans...
// 遍历所有的 beanName , 为属于 SmartInitializingSingleton 这种类的bean 执行初始化后回调
// 这里bean 已经实例化完成
for (String beanName : beanNames) {
// 拿到beanName 的bean 实例 这里4.3 中细讲
Object singletonInstance = getSingleton(beanName);
// 判断是否属于 SmartInitializingSingleton
if (singletonInstance instanceof SmartInitializingSingleton) {
final SmartInitializingSingleton smartSingleton = (SmartInitializingSingleton) singletonInstance;
// 执行 SmartInitializingSingleton实现类的afterSingletonsInstantiated方法
// 实例化后的回调方法,
if (System.getSecurityManager() != null) {
AccessController.doPrivileged((PrivilegedAction<Object>) () -> {
smartSingleton.afterSingletonsInstantiated();
return null;
}, getAccessControlContext());
}
else {
smartSingleton.afterSingletonsInstantiated();
}
}
}
}

4.1 getMergedLocalBeanDefinition(beanName) 方法

在讲这个方法前,先简单了解一下 BeanDefintion,Spring容器启动的过程中,会将Bean解析成Spring内部的BeanDefinition结构。 BeanDefinition描述了一个bean的实例,包括属性值,构造方法参数值和继承自它的类的更多信息以及bean 的作用域,是否懒加载等 创建这个bean 所需要的所有信息。这里我就简单说一下。

图片.png

  • AbstractBeanDefinition:抽象类,实现了BeanDefinition的部分方法。方便子类继承
  • AnnotatedBeanDefinition:注解类型的 BeanDefinition
  • RootBeanDefinition:父级 BeanDefinition
  • ChildBeanDefinition:子级 BeanDefinition,必须设置 parentName 即父级 BeanDefinition的信息
  • GenericBeanDefinition:一般的 BeanDefinition

mergedBeanDefinitions:hashMap key是beanName,value是 RootBeanDefinition
图片.png
图片.png
这个方法的官方定义是:返回合并的RootBeanDefinition,如果指定的bean对应于子bean定义,则遍历父bean定义。

图片.png
这里先取到 RootBeanDefinition mbd,判断当他,不为空,且不需要重新合并定义的时候直接返回(ps: 这里的stale表示是否需要是否需要重新合并定义)
否则的话调用

图片.png
首先先说一下 getBeanDefinition(beanName) :即根据名称从 beanDefinitionMap 取出对应 BeanDefinition

图片.png
图片.png
接着再来讲getMergedBeanDefinition(beanName, getBeanDefinition(beanName))
图片.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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
scss复制代码protected RootBeanDefinition getMergedBeanDefinition(
String beanName, BeanDefinition bd, @Nullable BeanDefinition containingBd)
throws BeanDefinitionStoreException {
// 首先加把锁 保证在执行这个方法时,别的线程不可以修改mergedBeanDefinitions
synchronized (this.mergedBeanDefinitions) {
RootBeanDefinition mbd = null;
RootBeanDefinition previous = null;

// Check with full lock now in order to enforce the same merged instance.
// 判断 containingBd为空时 进入
if (containingBd == null) {
// 从 mergedBeanDefinitions(Map) 中取出 RootBeanDefinition (此时mbd类型是RootBeanDefinition,也可能为null)
mbd = this.mergedBeanDefinitions.get(beanName);
}
// 当 mbd 为null 或者 需要重新合并定义时
if (mbd == null || mbd.stale) {
// 记录mbd 当前的引用信息
previous = mbd;
// 置为null
mbd = null;

//当bd.getParentName() == null时,如果bd 是 RootBeanDefinition,把bd拷贝给 mbd,
// 这里比 mbd = new RootBeanDefinition(bd); 多拷贝了一些属性 代码贴在 下面图1
if (bd.getParentName() == null) {
// Use copy of given root bean definition.
if (bd instanceof RootBeanDefinition) {
mbd = ((RootBeanDefinition) bd).cloneBeanDefinition();
}
else {
mbd = new RootBeanDefinition(bd);
}
}
// 当bd.getParentName() != null时,说明它是一个Child BeanDefinition 需要与父节点进行合并
else {
// Child bean definition: needs to be merged with parent.
BeanDefinition pbd;
try {
// transformedBeanName:返回bean名称,必要时去掉工厂解引用前缀,并将别名解析为规范名称。
String parentBeanName = transformedBeanName(bd.getParentName());
// 如果beanName不等于parentBeanName
if (!beanName.equals(parentBeanName)) {
// 返回给定bean名称的合并的 BeanDefinition,必要时将子BeanDefinition与其BeanDefinition合并。 这里放在 下面图二补充
pbd = getMergedBeanDefinition(parentBeanName);
}
else {
// 否则的话 先取得 当前对象的parentBeanFactory,判断当它属于 ConfigurableBeanFactory
BeanFactory parent = getParentBeanFactory();
if (parent instanceof ConfigurableBeanFactory) {
// 强转一下 调用 getMergedBeanDefinition 和上面相同 具体解释如下面图二
pbd = ((ConfigurableBeanFactory) parent).getMergedBeanDefinition(parentBeanName);
}
else {
// 否则的话 抛出异常
throw new NoSuchBeanDefinitionException(parentBeanName,
"Parent name '" + parentBeanName + "' is equal to bean name '" + beanName +
"': cannot be resolved without an AbstractBeanFactory parent");
}
}
}
catch (NoSuchBeanDefinitionException ex) {
throw new BeanDefinitionStoreException(bd.getResourceDescription(), beanName,
"Could not resolve parent bean definition '" + bd.getParentName() + "'", ex);
}
// Deep copy with overridden values.
//深层次复制,具体如图一
mbd = new RootBeanDefinition(pbd);
// 属性覆盖,具体看图三,使用原始的bd信息,覆盖父级信息
mbd.overrideFrom(bd);
}

// Set default singleton scope, if not configured before.
//判断 mbd的作用域 如果没有 默认给单例
if (!StringUtils.hasLength(mbd.getScope())) {
mbd.setScope(RootBeanDefinition.SCOPE_SINGLETON);
}

// A bean contained in a non-singleton bean cannot be a singleton itself.
// Let's correct this on the fly here, since this might be the result of
// parent-child merging for the outer bean, in which case the original inner bean
// definition will not have inherited the merged outer bean's singleton status.
// 包含在非单例Bean中的Bean本身不能是单例的。
// 让我们在此即时进行更正,因为这可能是外部bean的父子合并的结果,在这种情况下,
// 原始内部bean定义将不会继承合并的外部bean的单例状态。
// 如果有传包含bean定义且包含bean定义不是单例但mbd又是单例
if (containingBd != null && !containingBd.isSingleton() && mbd.isSingleton()) {
//设置mbd的作用域 和 containingBd的作用域 一样
mbd.setScope(containingBd.getScope());
}

// Cache the merged bean definition for the time being
// (it might still get re-merged later on in order to pick up metadata changes)
//暂时缓存合并bean定义
//稍后它可能仍然可以重新合并,以便拾取元数据变更
if (containingBd == null && isCacheBeanMetadata()) {
// 当containingBd为null 且 this.cacheBeanMetadata == true 把合并后的BeanDefinitions重新放到 this.mergedBeanDefinitions 中去
this.mergedBeanDefinitions.put(beanName, mbd);
}
}
if (previous != null) {
//如果previose 不为null , 把 previous 的部分属性 设置给 mbd ,具体属性看图四
copyRelevantMergedBeanDefinitionCaches(previous, mbd);
}
return mbd;
}
}

图一(多张):

2021-11-13_155417.jpg

2021-11-13_155640.jpg

2021-11-13_164115.jpg

图二(多张):
图片.png
图片.png
这里实际是通过 得到的 parentBeanName ,先判断当容器中的 beanDefinitionMap不包含它时且
this.parentBeanFactory instanceof ConfigurableBeanFactory 时 在调用本身。否则的话
再走一遍4.1的流程

图三:

2021-11-13_163742.jpg

图四:

图片.png

4.2 isFactoryBean(beanName) 源码如下

图片.png
1.先取得最终的beanName,然后拿着这个名字,调用getSingleton(beanNama,false)方法,返回单例对象。
这里 getSingleton 在 4.3 中有详细讲到

  1. beanInstance 不为null 时直接返回 beanInstance instanceof FactoryBean ,否则的话就说明这个beanName对应的bean还没有实例化,则先判断当容器中的 beanDefinitionMap不包含它时且 this.parentBeanFactory instanceof ConfigurableBeanFactory 时 接着判断。

3.isFactoryBean(beanName, getMergedLocalBeanDefinition(beanName))

图片.png
图片.png
图片.png
图片.png
先取出 mbd的isFactoryBean,当它不是null的时候直接返回,否则调用 predictBeanType

  • predictBeanType:获取 targetType
    • getTargetType: 如果已知,返回此bean定义的目标类型,当根据它的 targetType 返回
      如果targetType 不为null 直接返回,否则的话 判断 this.factoryMethodName 不为空时返回resolveBeanClass(mbd, beanName, typesToMatch),这个方法的主要作用:解析指定bean定义的bean类,将bean类名解析为class引用(如果需要),并将解析后的class存储在bean定义中以供进一步使用。这里就不往下继续讲了

4.3 getSingleton(beanName)

主要作用: 获取该beanName 下注册的单实例对象。
源码如下:
图片.png
图片.png
图片.png
图片.png
图片.png
图片.png
图片.png
this.singletonObjects(一级缓存)是一个Map 缓存了所有实例化好的单例对象,对象名–》对象,从里面拿出这个对象。

  • 如果不为null,则直接返回。
  • 否则判断当前正在创建的bean中是否有它(this.singletonsCurrentlyInCreation 保存当前正在创建的对象的名称,也就是没有初始化完成(比如A的构造器依赖了B对象所以得先去创建B对象, 或者在A的populateBean过程中依赖了B对象,得先去创建B对象,这时的A就是处于创建中的状态。)
    • 如果没有直接返回
    • 如果有则从this.earlySingletonObjects(二级缓存,保存提前曝光的单例对象:beanName –>bean实例) 中取出
      • 当 allowEarlyReference=false 直接返回.
      • 否则从 this.singletonFactories (三级缓存,保存的是要被实例化的对象的对象工厂 beanName –> ObjectFactory):取出 ObjectFactory 当它不为空的时候 从这里面取到 singletonObject,顺便把它从 singletonFactories 中移除,把它添加到 earlySingletonObjects 中,最终返回 singletonObject.

这里这种设计思想(三级缓存),实际上帮我们解决,单例模式下setter循环依赖的问题(即通过属性相互依赖的问题,构造器依赖spring解决不了)
例如有两个类,A和B ,A中有个属性是B,B中有个属性是A,spring 在初始的时候,先初始A 对象,当A完成初始化的第一步时(此时构造器放在已经执行完成),spring 会将它放在 singletonFactories 中 (具体的初始化过程下篇再讲),然后准备继续往下执行的时候,A 发现 它依赖B对象,A会从容器中尝试获取B,当A没有获取到的时候,就会转而先去创建B对象,B执行完构造器方法后,接着往下执行,发现自己依赖A。于是尝试获取A,从
一级缓存singletonObjects中获取(没有,因为A还没初始化完全),从二级缓存earlySingletonObjects中获取
(也没有),尝试三级缓存singletonFactories中获取,因为A通过ObjectFactory将自己提前曝光了,所以B可以获取到A(虽然没有初始化完全,但是能获取到),然后B完成自己的初始化过程,然后A接着完成自己的初始化过程

5.小结

本来想一篇弄完的,结果发现东西太多了,下篇文章我在继续补,下篇文章主要讲这个的后续的 4.3 getBean()和后面的方法。

本文转载自: 掘金

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

Java 中的多线程

发表于 2021-11-14

这是我参与11月更文挑战的第14天,活动详情查看:2021最后一次更文挑战

今天说多线程,若是你对进程和线程还有点迷糊,可以移步至 说说进程和线程 。

要想实现多线程,我们要先学会创建线程。创建线程有这么 3 种方式。

1 继承 Thread 类 。声明一个 Thread 的子类,该类需要重写 Thread 类的 run 方法。然后即可通过 start 方法来启动这个线程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
java复制代码public class Thread1 extends Thread{
    private String name;
    public Thread1(String name) {
        this.name = name;
    }
    @Override
    public void run() {
        for(int i = 0;i < 5;i++){
            System.out.println(name +"-----"+ i );
            try {
                sleep(1000); // 不睡也行,睡只是为了演示线程的切换效果。
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    public static void main(String[] args) {
        Thread1 t1 = new Thread1("1 号");
        Thread1 t2 = new Thread1("2 号");
        t1.start();
        t2.start();
    }
}

2 实现 Runnable 接口。声明一个实现 Runnable 接口的子类,重写 run 方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
typescript复制代码public class TaskNoResult implements Runnable{
    private String name;
    public TaskNoResult(String name) {
        this.name = name;
    }
    @Override
    public void run() {
        for(int i = 0;i < 5;i++){
            System.out.println(name +"-----"+ i );
            try {
                Thread.sleep(1000); // 不睡也行,睡只是为了演示线程的切换效果。
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) {
        new Thread(new TaskNoResult("任务1")).start();
        new Thread(new TaskNoResult("任务2")).start();
    }

}

本质上来说,继承 Thread 类也是在实现 Runnable 接口,启动线程的方式只有通过 Thread 类的 start 方法,start 方法是一个 native 方法,通过这个方法去执行 run 方法,run 方法里面的内容只是线程的执行体罢了。记住,启动线程的方式就一种,那就是通过 Thread 类的 start 方法。

实现 Runnable 接口的优点如下:

  1. 避免单继承的局限
  2. 线程代码可以被多个线程共享
  3. 适合多个线程处理同一个资源的情况
  4. 使用线程池时,只能放入 Runnable 或 Callable 类型线程。

3 实现 Callable 接口。Callable 接口是在 JDK1.5 中出现的,我们可以通过实现该接口并重写 call 方法来创建线程。

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
arduino复制代码public class TaskWithResult implements Callable<String>{
    private int id;
    public TaskWithResult(int id) {
        this.id = id;
    }
    @Override
    public String call() throws Exception {
        return id+"任务被线程驱动执行!";
    }
--------------------测试如下-----------------------
    public static void main(String[] args) throws InterruptedException, ExecutionException {
        // 使用Executors创建一个线程池来执行任务
        ExecutorService pool = Executors.newCachedThreadPool(); 
        //Future 相当于是用来存放Executor执行的结果的一种容器
        ArrayList<Future<String>> results = new ArrayList<Future<String>>();
        for (int i = 0; i < 10; i++) {
            results.add(pool.submit(new TaskWithResult(i)));
        }
        for (Future<String> fs : results) {
            if (fs.isDone()) {
                System.out.println(fs.get());// 返回任务执行结果
            } else {
                System.out.println("Future result is not yet complete");
            }
        }
        pool.shutdown();
    }
}

Runnable 和 Callable 的区别:

  1. Runnable 重写 run 方法,而 Callable 重写 call 方法。
  2. Runnable 没有返回值, Callable 有返回值。
  3. run 方法不能抛出异常,call 方法可以抛出异常。
  4. 运行 Callable 任务可以得到一个 Future 对象。
1
2
3
4
5
6
7
8
9
java复制代码public interface Future<V> {    
boolean  cancel(boolean mayInterruptIfRunning);
boolean  isCancelled(); 
boolean  isDone();
// 获得结果,一直等待
V  get() throws InterruptedException, ExecutionException; 
// 获得结果,等待一定的时间
V  get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException;
}

Future 是一个接口,他提供给我们方法来检测当前任务是否已经结束,还可以等待任务结束并且拿到一个结果。通过调用 Future 的 get 方法可以当任务结束后返回一个结果值,如果工作没有结束,则会阻塞当前线程,直到任务执行完毕,我们可以通过调用 cancel 方法来停止一个任务,如果任务已经停止,则cancel 方法会返回 true。如果任务已经完成或已经停止或这个任务无法停止,则 cancel 会返回一个 false。当一个任务被成功停止后,他无法再次执行。 isDone 和 isCancel 方法可以判断当前工作是否完成和是否取消。

我们可以这样理解,Runnable 和 Callable 都是用来创建任务,而我们用线程去驱动执行这个任务,常规的做法像这样:

1
2
sql复制代码new Thread(new TaskNoResult("任务1")).start();
new Thread(new TaskNoResult("任务2")).start();

但是并不推荐这样使用,推荐使用线程池来创建线程进而驱动任务执行。像这样:

1
2
3
vbscript复制代码ExecutorService pool = Executors.newCachedThreadPool();
pool.execute(new TaskNoResult("任务1"));
pool.execute(new TaskNoResult("任务2"));

下面奉上一张线程生命周期图,这张图值得好好看看。

图片

简单说一下线程中的几个方法。

start() :启动线程的方法,但是并不保证线程会立即执行。

sleep(long) :暂停线程一段时间,参数为毫秒数。

join() :把指定的线程加入到当前线程执行,等待其执行完毕,可用于控制线程的执行顺序。

yield() :线程让步,只会将 CPU 让给同优先级的线程,但是并不保证一定会让步成功。

最后说一个实际不建议使用的知识点,设置线程的优先级。因为优先级的高低不是决定线程执行顺序的决定因素,所以,千万不要指望设置优先级来控制线程的执行顺序。

1
2
3
scss复制代码t.setPriority(Thread.MIN_PRIORITY); // 最低 1
t.setPriority(Thread.NORM_PRIORITY); // 默认值 5 
t.setPriority(Thread.MAX_PRIORITY); //最高 10

本文转载自: 掘金

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

Java线程状态详解

发表于 2021-11-14

Java中线程状态

  1. NEW: 线程刚创建,还没有调用start()方法;
  2. RUNNABLE:线程准备就绪状(即:调用start()方法)或运行中;
  3. TIME_WAITING:等待隔一段时间自动唤醒;
  4. WAITING: 等待被唤醒;
  5. BLOCKED:阻塞,正在等待锁;
  6. TERMINATED:线程结束;

线程状态转换图

image.png

代码验证状态转换图

NEW、RUNNABLE、TERMINATED状态

1
2
3
4
5
6
7
8
9
10
11
12
csharp复制代码public class ThreadStatusTest {
public static void main(String[] args) throws Exception {
// NEW、RUNNABLE、TERMINATED
Thread t1 = new Thread(() -> {
System.out.println("2、"+Thread.currentThread().getState()); //线程在执行 输出: RUNNABLE
});
System.out.println("1、"+t1.getState()); //此时还没调用start() 输出: NEW
t1.start(); // 启动t1
t1.join(); // 等待t1线程执行完毕主线程再继续执行
System.out.println("3、"+t1.getState()); //此时t1线程一致性完毕 输出: TERMINATED
}
}

TIMED_WAITING状态

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
scss复制代码public class ThreadTimedWaitingTest {
public static void main(String[] args) throws Exception {
Object o = new Object();

// TIMED_WAITING
Thread t2 = new Thread(() -> {
try {
// 休眠3s
TimeUnit.SECONDS.sleep(3);

synchronized (o) {
// 等待3s
o.wait(3000);
}

Thread thread = new Thread(() -> {
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
thread.start(); // 创建一个线程并
thread.join(2000); // thread线程执行完本线程在继续执行

LockSupport.parkNanos(2000000000);

LockSupport.parkUntil(System.currentTimeMillis() + 2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
});

t2.start(); // 启动t2
TimeUnit.SECONDS.sleep(1); // 休眠1s保证t2线程启动
System.out.println("4、" + t2.getState()); //输出:4、TIMED_WAITING

TimeUnit.SECONDS.sleep(3); // 休眠2s保证t2线程 wait
System.out.println("5、" + t2.getState()); //输出:5、TIMED_WAITING

TimeUnit.SECONDS.sleep(3); // 休眠3s保证 t2在 join(2000)
System.out.println("6、" + t2.getState()); // 输出: TIMED_WAITING

TimeUnit.SECONDS.sleep(2); // 休眠2s保证 t2在 LockSupport.parkNanos(2000000000);
System.out.println("7、" + t2.getState()); // 输出: 7、TIMED_WAITING

TimeUnit.SECONDS.sleep(1); // 休眠2s保证 t2在 LockSupport.parkUntil(System.currentTimeMillis() + 2000);
System.out.println("8、" + t2.getState()); // 输出: 8、TIMED_WAITING
}
}

WAITING状态

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
scss复制代码public class ThreadWaitingTest {
public static void main(String[] args) throws Exception {
Object o = new Object();
ReentrantLock lock = new ReentrantLock();

Thread t1 = new Thread(() -> {
synchronized (o) {
try {
o.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}


Thread joinThread = new Thread(() -> {
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
});

try {
joinThread.start();
joinThread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}

LockSupport.park();

Thread lockThread = new Thread(() -> {
lock.lock();
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
lock.unlock();

}
});
lockThread.start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}

lock.lock();
lock.unlock();
});

t1.start();
TimeUnit.SECONDS.sleep(1); // 休眠1s,保证t1线程 wait()
System.out.println("1、" + t1.getState()); // 输出: 1、WAITING
synchronized (o) {
o.notify();
}

TimeUnit.SECONDS.sleep(1); // 休眠1s, 保证 thread.join();
System.out.println("2、" + t1.getState()); // 输出: 2、WAITING

TimeUnit.SECONDS.sleep(2);// 休眠2s,保证 LockSupport.park();
System.out.println("3、" + t1.getState()); // 输出: 3、WAITING
LockSupport.unpark(t1);

TimeUnit.SECONDS.sleep(2); // 休眠2s, 保证lock.lock();
System.out.println("4、"+t1.getState()); // 输出: 4、WAITING
}
}

BLOCKED

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

public static void main(String[] args) throws InterruptedException {
Object o = new Object();

Thread t1 = new Thread(() -> {
synchronized (o){
try {
TimeUnit.SECONDS.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});

Thread t2 = new Thread(() -> {
synchronized (o){

}
});
t1.start();
TimeUnit.SECONDS.sleep(1); // 休眠1s,保证t1执行
t2.start();
TimeUnit.SECONDS.sleep(1);// 休眠1s,保证t2执行
System.out.println(t2.getState()); // 输出: BLOCKED
}
}

本文转载自: 掘金

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

1…347348349…956

开发者博客

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