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

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


  • 首页

  • 归档

  • 搜索

同事问我代码结构中 Manager层是干什么的

发表于 2021-11-20

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

写在前面

今天我们聊聊Manager,大家可能懵逼了,这是什么,这跟三层模式有关系,欢迎大家看我之前写过的一篇

周六加完班,聊聊MVC思想模式

聊manager之前再回顾下经典的三层模式

MVC(Model-View-Controller)架构。它将整体的系统分成了 Model(模型),View(视图)和 Controller(控制器)三个层次,也就是将用户视图和业务处理隔离开,并且通过控制器连接起来,很好地实现了表现和逻辑的解耦,是一种标准的软件分层架构。

image.png

MVC分层架构是架构上最简单的一种分层方式。为了遵循这种分层架构我们在构建项目时往往会建立这样三个目录:controller、service 和 dao,它们分别对应了表现层、逻辑层还有数据访问层。

这就是我们平常开发中一直遵循的开发原则,回顾完MVC分层方式,接下来进入正题讲讲Manager层。

Manager

最早定义该层的应该出自阿里开发规约中。

我们先来看副图,将Manager层融入到三层模式当中是怎样的一种结构(此图来源于阿里规约)

image.png

对比上述图我们一一作出解释

  • 开放接口层:可直接封装 Service 方法暴露成 RPC 接口;通过 Web 封装成 http 接口;网关控制层等。
  • 终端显示层:各个端的模板渲染并执行显示的层。当前主要是 velocity 渲染,JS 渲染,JSP 渲染,移动端展示等。
  • Web 层:主要是对访问控制进行转发,各类基本参数校验,或者不复用的业务简单处理等
  • Service 层:相对具体的业务逻辑服务层。

接下来解释Manager层

  • Manager 层:通用业务处理层,它有如下特征:

1)对第三方平台封装的层,预处理返回结果及转化异常信息。

2)对 Service 层通用能力的下沉,如缓存方案、中间件通用处理。

3)与 DAO 层交互,对多个 DAO 的组合复用。

  • DAO 层:数据访问层,与底层 MySQL、Oracle、Hbase、OB 等进行数据交互。
  • 外部接口或第三方平台:包括其它部门 RPC 开放接口,基础平台,其它公司的 HTTP 接口。

总之一句话:在这个分层架构中主要增加了 Manager 层,它与 Service 层的关系是:Manager 层提供原子的服务接口,Service 层负责依据业务逻辑来编排原子接口。

说到这里 先不往下讲。我们先思考一个问题 就是MVC模式有什么弊端,为什么或者说Manager出现的意义

Manager出现的意义

其实仔细想想,传统的MVC分层有以下几个很明显的问题:

1) 由于我们Controller层不写任何业务逻辑 慢慢的导致Service层代码臃肿

2) 逻辑多起来之后,Service层很容易出现大事务,事务嵌套,导致问题很多,而且极难排查

3)那么逻辑会出现下沉,dao层参杂业务逻辑

4)dao层sql语句复杂,关联查询比较多

针对这些问题。Manager层应运而生。

我在哪看到这样一句话,感觉适用于所有的难题:大概意思就是 没有解决不了的问题,如果有 那就再抽一层,没有什么是抽层解决不了的问题

下面我们用实战案例演示一把。

Manager使用案例

这里我们举个例子说明一下Manager层的使用场景:

假设你有一个用户系统,他有一个获取用户信息的接口,它调用逻辑Service层的 getUser 方法,getUser方法又和 User DB 交互获取数据。

这时,善变的产品提出一个需求,在 APP 中展示用户信息的时候,如果用户不存在,那么要自动给用户创建一个用户

1) 此时按照传统的三层架构,逻辑层的边界就变得不清晰,表现层也承担了一部分的业务逻辑,因为我们往往会在表现层Service中增加业务逻辑处理,将获取用户和创建用户接口编排起来。

2)而添加Manager层以后,Manager 层提供创建用户和获取用户信息的接口,而 Service 层负责将这两个接口组装起来。这样就把原先散布在表现层的业务逻辑都统一到了 Service 层,每一层的边界就非常清晰了。

接下来我们看一段实际代码说明一下Service层与Manager层如何进行区分?

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复制代码
@Transactional(rollbackFor = Throwable.class)
public Result<String> upOrDown(Long departmentId, Long swapId) {
// 验证 1
DepartmentEntity departmentEntity = departmentDao.selectById(departmentId);
if (departmentEntity == null) {
return Result.error("部门xxx不存在");
}
// 验证 2
DepartmentEntity swapEntity = departmentDao.selectById(swapId);
if (swapEntity == null) {
return Result.error("部门xxx不存在");
}
// 验证 3
Long count = employeeDao.countByDepartmentId(departmentId);
if (count != null && count > 0) {
return Result.error("员工不存在");
}

// 操作数据库 4
Long departmentSort = departmentEntity.getSort();
departmentEntity.setSort(swapEntity.getSort());
departmentDao.updateById(departmentEntity);
swapEntity.setSort(departmentSort);
departmentDao.updateById(swapEntity);
return Result.OK("success");
}

上面代码在我们在我们采用三层架构时经常会遇到,那么它有什么问题呢?

就如我们上面所说的,逻辑太多会Service会及其臃肿,而且会非常引起事务套引起事务失效问题,

我们在加入Manager层以后可以这样写:

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

public Result<String> upOrDown(Long departmentId, Long swapId) {
// 验证 1
DepartmentEntity departmentEntity = departmentDao.selectById(departmentId);
if (departmentEntity == null) {
return Result.error("部门xxx不存在");
}
// 验证 2
DepartmentEntity swapEntity = departmentDao.selectById(swapId);
if (swapEntity == null) {
return Result.error("部门xxx不存在");
}
// 验证 3
Long count = employeeDao.countByDepartmentId(departmentId);
if (count != null && count > 0) {
return Result.error("员工不存在");
}

// 操作数据库 4
departmentManager.upOrDown(departmentSort,swapEntity);

return Result.OK("success");
}
1
2
3
4
5
6
7
8
9
10
java复制代码DepartmentManager.java

@Transactional(rollbackFor = Throwable.class)
public void upOrDown(DepartmentEntity departmentEntity ,DepartmentEntity swapEntity){
Long departmentSort = departmentEntity.getSort();
departmentEntity.setSort(swapEntity.getSort());
departmentDao.updateById(departmentEntity);
swapEntity.setSort(departmentSort);
departmentDao.updateById(swapEntity);
}

将数据在 service 层准备好,然后传递给 manager 层,由 manager 层添加@Transactional事务注解进行数据库操作。

OK 今天我们关于Manager 就讲到这里,大家以后也可以在代码中运用起来。

总结

Manager 虽然让结构更清晰,但是不要一味的运用,逻辑复杂的时候可以考虑运用,我上面举例的可能相对简单 但是这是一种实践思想,合理运用,不要让代码显得更乱。

弦外之音

感谢你的阅读,如果你感觉学到了东西,您可以点赞,关注。也欢迎有问题我们下面评论交流

加油! 我们下期再见!

给大家分享几个我前面写的几篇骚操作

copy对象,这个操作有点骚!

干货!SpringBoot利用监听事件,实现异步操作

本文转载自: 掘金

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

GC常用相关参数和堆内存使用情况 常用vm相关参数(G1收集

发表于 2021-11-20

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

常用vm相关参数(G1收集器)

参数 意义 jdk9 之后
-Xms 堆初始大小
-Xmx / -XX:MaxHeapSize=size 堆最大大小
-Xmn 同时指定新生代初始和最大大小
-XX:NewSize=size+ 指定新生代初始大小
-XX:MaxNewSize=size 指定新生代最大大小
-XX:InitialSurvivorRation=ration 幸存区比例(动态调整幸存区与Eden比例)
-XX:SurvivorRation=ratio 幸存区比例(默认值是8)
-XX:MaxTenuringThreshold=threshold 晋升老年代阈值
-XX:+PrintTenuringDistribution 晋升详情
-XX:+PrintGCDetails -verbose:gc 查看gc详情 -Xlog:gc*
-XX:ScavengeBeforeFullGC 在FullGC前进行MinorGC
-XX:+PrintHeapAtGC 查看GC前后的堆、方法区可用容量变化 -Xlog:gc+heap=debug
-XX:+PrintGCApplicationConcurrentTime 查看GC过程中用户线程并发时间、停顿的时间 -Xlog:safepoint
-XX:+PrintTenuring-Distribution 查看收集后剩余对象的年龄分布信息 -Xlog:gc+age=trace
  • 幸存区比例 -XX:SurvivorRation=ratio 默认是8,假如新生代分配了10M的内存空间,那么8M会划分到Eden下,幸存区from和幸存区to各1M
  • 晋升详情 -XX:+PrintTenuringDistribution ,打印出对象的晋升信息
  • -XX:ScavengeBeforeFullGC 在FullGC前进行MinorGC,减少一些不必要的对象加速FullGC的进度,一般是默认是打开的。
  • 查看GC详情信息,在JDK 9之前使用-XX:+PrintGCDetails,在JDK 9之后可以使用-X-log:gc*,通配符 * 将GC下所有的细分过程都打印出来,如果调低日志级别还可以获得更多的GC信息。

VM的参数还有很多很多就不一一列举了,这些常用的VM参数可以满足很大部分的需求了

堆内存使用情况

以下这个案例主要演示查看堆内存使用情况

建立一个类叫DemoTest,添加main方法,然后启动该方法

image.png
在idea上对DemoTest配置

image.png

在jvm中添加:-Xms20M -Xmx20M -Xmn10M -XX:+UserSerialGC -xx:+PrintGCDetails -verbose:gc

参数意义:
设置最大堆空间为20M,新生代设置10M,设置垃圾回收器为:+UserSerialGC ,最后一个参数就是打印GC的详情

image.png

启动DemoTest方法,会出现以下信息,这个信息是程序运行完之后堆的情况,大致信息:

def new generation 新生代为9M,是因为我们给新生代分配了10M,Eden默认为8M,幸存区From和幸存区To各位1M,但是JVM会让幸存区To始终空着,所以这里只显示有9M左右。右侧信息说是已经使用的大小为3M,最后就是内存地址了。注意:就算再简单的JAVA程序也会加载一些类、创建一些对象,所以Eden就被占用了一部分空间。

tenured generation 老年代,空间大小10M,没有被使用,占用空间0K。

Metaspace 元空间,虽然元空间不属于堆这部分,但是**-xx:+PrintGCDetails -verbose:gc`** 把空间的信息打印出来。

image.png

本文转载自: 掘金

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

【设计模式27】访问者模式+源码分析:Eclipse JDT

发表于 2021-11-20
  1. 简介

1.1 定义

访问者模式(Visitor Pattern)定义:表示一个作用于某对象结构中的各元素的操作,它
使我们可以在不改变各元素的类的前提下定义作用于这些元素的新操作。访问者模式是一
种对象行为型模式。

1.2 结构模式

image.png
图1 浏览者模式结构图

1.3 模式分析

想象一个使用场景,有一张药单,里面包含药品、数量、价格等不同类型的集合。对于护士而言看到药品和数量信息就是去取并进行相关医疗工作,而对于缴费工作人员看到药品、数量、价格就是去核对。
这是一个

  • 背景:多种类型的物品继承同一个公共祖先,并且在一个集合中。
  • 需求:有多种类型的visitor会根据不同的实际类型,需要作出不同决策。
  1. 访问者模式实例

顾客在超市中将选择的商品,如苹果、图书等放在购物车中,然后到收银员处付款。在购物过程中,顾客需要对这些商品进行访问,以便确认这些商品的质量,之后收银员计算价格时也需要访问购物车内顾客所选择的商品。此时,购物车作为一个 ObjeetStructure(对象结构)用于存储各种类型的商品,而顾客和收银员作为访问这些商品的访问者,他们需要对商品进行检查和计价。不同类型的商品其访问形式也可能不同,如苹果需要过秤之后再计价,而图书不需要。使用访问者模式来设计该购物过程。
image.png
图2 购物车类图
​

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码public abstract class Visitor
{
protected String name;

public void setName(String name){
this. name = name;
}

public abstract void visit(Apple apple);

public abstract void visit(Book book);
}
1
2
3
4
5
6
7
8
9
java复制代码public class Customer extends Visitor {
public void visit(Apple apple) {
System. out. println("顾客"+ name + "选苹果。");
}

public void visit(Book book) {
System. out. println("顾客"+ name + "买书。”);
}
}
1
2
3
4
5
6
7
8
9
10
java复制代码public class Saler extends Visitor {
public void visit(Apple apple) {
System. out. println("收银员"+ name+"给苹果过秤,然后计算其价格。");
}

public void visit(Apple apple) {
System. out. print 1n("收银员"+ name +"直接计算书的价格。”);
}

}
1
2
3
java复制代码public interface Product {
void accept(Visitor visitor);
}
1
2
3
4
5
java复制代码public class Apple implements Product {
public void accept (Visitor visitor) {
visitor. visit(this);
}
}
1
2
3
4
5
java复制代码public class Book implements Product {
public void accept (Visitor visitor) {
visitor. visit(this);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
java复制代码import java. util.*

public class BuyBasket {
private ArrayList list = new ArrayList();

public void accept(Visitor visitor) {
Iterator i= list. iterator();

while(i.hasNext()) {
((Product)i.next()).accept(visitor);
}

}

public void addProduct (Product product) {
1ist.add(product);
}

public void removeProduct (Product product) {
list.remove(product);
}

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java复制代码public class Client {
public static void main(String a[]) {
Product b1 = new Book();
Product b2 = new Book();

Producr a1 = new Apple();

BuyBasket basket = new BuyBasket();
basket.addProduct(b1);
basket.addProduct(b2);
basket.addProduct(a1);

Visitor customer = new Customer();
basket.accept(visitor);

}
}
  1. 源码分析:Eclipse JDT AST中的访问者模式

在遍历语法树的时候,如果我们需要添加新的访问者,会重写一个新的类去继承 abstract class ASTVisitor,然后重写我们需要的方法,所有的visit方法默认 return true;。
现在我需要通过语法树去统计每个类被引用的次数,只需要写一个新的类去继承 ASTVisitor 抽象类,然后重写 public boolean visit(MethodDeclaration node) 和 public boolean visit(FieldDeclaration node)两个函数,统计语法树的子节点出现的被引用的类的情况。

1
2
3
4
5
6
7
8
9
10
11
java复制代码@Override
public class MyVisitor extends ASTVisitor {
public boolean visit(MethodDeclaration node) {
for (Object o : node.parameters()) {//遍历函数参数
//获取参数属于哪个类,然后为该类出现的次数+1
}

public boolean visit(FieldDeclaration node) {
//获取成员变量属于哪个类,然后为该类出现的次数+1
}
}

如何调用呢?使用 compilationUnit.accept(myVisitor);即可。
我们发现定义新的访问规则其实挺容易的,不用去修改 CompilationUnit 本身,这就是访问者模式的优点。
compilationUnit.accept(myVisitor);的函数执行时,调用的相关源码如下

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
java复制代码//ASTNode:
public final void accept(ASTVisitor visitor) {
if (visitor == null) {
throw new IllegalArgumentException();
} else {
if (visitor.preVisit2(this)) {
this.accept0(visitor);
}

visitor.postVisit(this);
}
}
//ASTVistor:
public boolean preVisit2(ASTNode node) {
this.preVisit(node);
return true;
}
}
//ASTVistor:
public void preVisit(ASTNode node) {
}
//ASTNode:
abstract void accept0(ASTVisitor var1);
//CompilationUnit:
void accept0(ASTVisitor visitor) {
boolean visitChildren = visitor.visit(this);
if (visitChildren) {
this.acceptChild(visitor, this.getPackage());
this.acceptChildren(visitor, this.imports);
this.acceptChildren(visitor, this.types);
}

visitor.endVisit(this);
}
//ASTNode:
final void acceptChild(ASTVisitor visitor, ASTNode child) {
if (child != null) {
child.accept(visitor);
}
}
//ASTNode:
final void acceptChildren(ASTVisitor visitor, ASTNode.NodeList children) {
ASTNode.NodeList.Cursor cursor = children.newCursor();

try {
while(cursor.hasNext()) {
ASTNode child = (ASTNode)cursor.next();
child.accept(visitor);
}
} finally {
children.releaseCursor(cursor);
}

}

//ASTVistor:
public void endVisit(ASTNode node) {
}

我们发现当调用accep方法时候,实际上是先调用visitor.preVisit(node),然后以前序遍历访问顺序分别访问Package、imports、types(该文件中的其他类),通过函数的重载实现了对于不同类型进行不同visit操作。
类图如图3所示:

image.png
图3 ASTNode、ASTVistor源码

总体上写的非常精妙的,但是如果有新的类 MyASTNode 实现了 ASTNode 抽象类,那么需要对ASTVistor 增加 preVisit(MyASTNode node),endVisit(MyASTNode node),visit(MyASTNode node) 三个函数了。不过介于目前Java语言已经趋于稳定,增加新的ASTNode的可能性不大。这里表现了一种 倾斜的开闭原则。

  1. 总结

  1. 访问者模式的优点
    1. 使得增加新的访问操作变得很容易。使用访问者模式,增加新的访问操作就意味着增加一个新的访问者类,无须修改现有类库代码,符合“开闭原则”的要求。
    2. 将有关元素对象的访问行为集中到一个访问者对象中,而不是分散到一个个的元素类中。类的职责更加清晰,有利于对象结构中元素对象的复用,相同的对象结构可以供多个不同的访问者访问。
  2. 访问者模式的缺点
    1. 在访问者模式中,每增加一个新的元素类都意味着要在抽象访问者角色中增加一个新的抽象操作,并在每一个具体访问者类中增加相应的具体操作,违背了“开闭原则”的要求。
    2. 破坏封裝。访问者模式要求访问者对象访问并调用每一个元素对象的操作,这意味着元素对象有时候必须暴露一些自己的内部操作和内部状态,否则无法供访问者访问。
  3. 适用场景
    1. 一个对象结构包含很多类型的对象,希望对这些对象实施一些依赖其具体类型的操作。在访问者中针对每一种具体的类型都提供了一个访问操作,不同类型的对象可以有不同的访问操作。
    2. 需要对一个对象结构中的对象进行很多不同的并且不相关的操作,而需要避免让这些操作“污染”这些对象的类,也不希望在增加新操作时修改这些类。访问者模式使得我们可以将相关的访问操作集中起来定义在访问者类中,对象结构可以被多个不同的访问者类所使用,将对象本身与对象的访问操作分离。
    3. 对象结构中对象对应的类很少改变,但经常需要在此对象结构上定义新的操作。

本文转载自: 掘金

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

和不安全访问 say goodbye,手把手教大家如何给域名

发表于 2021-11-20

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

之前分享的云服务器学习先导篇# 拥有一台服务器后,我竟然这么酷? 还挺受大家喜欢的,后面会多更新相关文章,今天这篇文章属于插曲,和云服务器也相关,希望对学习者有所帮助~记得点赞哦~

image.png

一、前情回顾

你是否会遇到这种情况,刚买的域名,搭上服务器后,兴致杠杠的将域名输入到浏览器访问,却提示你:不安全。

作为新手的你可能满脸问号,我,我发布的不是什么yyyy网站呀~

其实,这并不是说你的网站有问题,而是告诉你,你的网站或者你访问的这个网站不安全,存在数据泄露的风险,一般来说都是因为没有配置ssl证书的原因。

本文将手把手教大家如何给域名配置ssl证书。

本文是以阿里云为例子,教大家如何获取阿里云的免费证书,并且给域名配置上。

二、购买域名

既然是要给域名配置证书,那么首先我们就得有一个域名,这里新手的话,没特殊要求,可以买普通后缀的域名,下面我给大家找了阿里云最便宜的域名1元/年,大家点击即可查看

进入页面后,大家先要注册/登录,然后输入自己想要的域名即可,可以选一下后缀,感觉top是里面最酷的~因为我不是新用户,所以显示需要按原价购买~(这里我也建议,如果是新用户,还是去购买服务器来的划算~)

不过就算原价买,这个后缀的域名也只用5元/年~直接闭眼买就是了。

三、申请、下载免费证书

买完后,我们直接访问下面链接,进入免费证书的申请页面~

1
bash复制代码https://yundun.console.aliyun.com/?&p=cas#/certExtend/free

进入ssl免费申请页面后,点击页面中的立即购买(实际不要钱)。

完成上一步,会弹出一个购买界面,注释有说,每个人每年可以申请20张免费的证书,所以我们选20,然后直接付款即可(0元)。

支付成功后,我们返回ssl免费证书页面。

这时候点击创建证书(注意之前是0,现在有20个了),会生成一个证书。

新建成功后,先不急,先去新建一个联系人,在申请的时候需要,浏览器打开下面页面,即可直接进入:

1
bash复制代码https://yundun.console.aliyun.com/?p=cas#/informationManagement?tab=CONTACT_PERSON

联系人姓名、邮箱、手机号必填,我们申请的是DV证书,可以不填身份证信息(填下也没事)。

新建联系人后,再回到ssl证书页面,点击ssl证书页面的证书申请进入证书申请页面。

在证书申请页面,输入域名,联系人会自动选好我们刚刚新建的,其他的都默认即可,点击下一步。

按提示操作,去域名管理后台对应的域名下添加一条解析,记录类型、主机记录、记录值复制提示的内容即可。

如果发现自动设置了解析最好,发现没有自动帮我们设置,按下面方法手动设置即可。

  • 点击文字超链接或者新开一个面输入下面链接进入域名管理后台。
1
bash复制代码https://dc.console.aliyun.com/next/index#/domain/list/all-domain
  • 点击对应域名后面的解析按钮,然后添加一条解析即可,记录类型TXT,主机记录和记录值复制提示内的即可。

添加好后,如下截图中的ssl证书验证解析。

然后我们回到证书申请页面,可以点击下验证试试,如果显示验证成功,就说明应该没问题,直接再点击提交审核即可。(如果验证失败就检查下解析内容是否填写正确,另外解析配置后可能也会有点延迟,最晚10分钟后应该是肯定没问题的)

提交审核后会有个弹框提示提交成功,我这边是没有收到相关电话,应该一会就好了。

证书申请成功后,就可以点击下载按钮下载证书了。

你会发现不同服务器有不同的证书下载按钮,我们使用的Nginx,所以下载对应证书即可。

下载好的压缩包解压后包含两个文件pem(证书内容)和key(密钥),后面在宝塔直接建站后使用,关于宝塔相关介绍可以看这篇文章。

当然大家还可以下载后点帮助,官方也给出了各种代理服务器的配置部署方法。

到这里,阿里云免费SLL证书申请、配置就结束啦~我们下期见!

另外,域名购买后还得进行备案哦~

备案需要在域名购买并实名认证成功2-3天后再进行申请,到时候申请大家直接访问下方链接,点击新建备案即可进行备案申请:

1
bash复制代码https://beian.aliyun.com

没有备案的域名不能访问成功。

下期见,我是爱猫爱技术的老表,如果觉得本文对你学习有所帮助,欢迎点赞、评论、关注我!

本文转载自: 掘金

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

走进Linux内核之XFRM框架:应用demo

发表于 2021-11-20

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


笔者此前对Linux内核相关模块稍有研究,实现内核级通信加密、视频流加密等,涉及:Linux内核网络协议栈、Linux内核通信模块、Linux内核加密模块、秘钥生成分发等。

后续考虑开设Linux内核专栏。

掘金平台写过关于XFRM框架的分析,现简单对XFRM框架做简单的demo。

走进Linux内核之Netfilter框架

走进Linux内核之XFRM框架

一、Netfilter应用案例

如下为在网络上找到的一个内核模块 Demo,该模块的基本功能是将经过 IPv4 网络层 NF_INET_LOCAL_IN 节点的数据包的源 Mac 地址,目的 Mac 地址以及源 IP,目的 IP 打印出来,源码包下载.NF_INET_LOCAL_IN

代码如下所示:

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
c复制代码#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/types.h>
#include <linux/skbuff.h>
#include <linux/ip.h>
#include <linux/udp.h>
#include <linux/tcp.h>
#include <linux/netfilter.h>
#include <linux/netfilter_ipv4.h>


MODULE_LICENSE("GPLv3");
MODULE_AUTHOR("SHI");
MODULE_DESCRIPTION("Netfliter test");

static unsigned int
nf_test_in_hook(unsigned int hook, struct sk_buff *skb, const struct net_device *in,
const struct net_device *out, int (*okfn)(struct sk_buff*));

static struct nf_hook_ops nf_test_ops[] __read_mostly = {
{
.hook = nf_test_in_hook,
.owner = THIS_MODULE,
.pf = NFPROTO_IPV4,
.hooknum = NF_INET_LOCAL_IN,
.priority = NF_IP_PRI_FIRST,
},
};

void hdr_dump(struct ethhdr *ehdr) {
printk("[MAC_DES:%x,%x,%x,%x,%x,%x"
"MAC_SRC: %x,%x,%x,%x,%x,%x Prot:%x]\n",
ehdr->h_dest[0],ehdr->h_dest[1],ehdr->h_dest[2],ehdr->h_dest[3],
ehdr->h_dest[4],ehdr->h_dest[5],ehdr->h_source[0],ehdr->h_source[1],
ehdr->h_source[2],ehdr->h_source[3],ehdr->h_source[4],
ehdr->h_source[5],ehdr->h_proto);
}

#define NIPQUAD(addr) \
((unsigned char *)&addr)[0], \
((unsigned char *)&addr)[1], \
((unsigned char *)&addr)[2], \
((unsigned char *)&addr)[3]
#define NIPQUAD_FMT "%u.%u.%u.%u"

static unsigned int
nf_test_in_hook(unsigned int hook, struct sk_buff *skb, const struct net_device *in,
const struct net_device *out, int (*okfn)(struct sk_buff*)) {
struct ethhdr *eth_header;
struct iphdr *ip_header;
eth_header = (struct ethhdr *)(skb_mac_header(skb));
ip_header = (struct iphdr *)(skb_network_header(skb));
hdr_dump(eth_header);
printk("src IP:'"NIPQUAD_FMT"', dst IP:'"NIPQUAD_FMT"' \n",
NIPQUAD(ip_header->saddr), NIPQUAD(ip_header->daddr));
return NF_ACCEPT;
}

static int __init init_nf_test(void) {
int ret;
ret = nf_register_hooks(nf_test_ops, ARRAY_SIZE(nf_test_ops));
if (ret < 0) {
printk("register nf hook fail\n");
return ret;
}
printk(KERN_NOTICE "register nf test hook\n");
return 0;
}

static void __exit exit_nf_test(void) {
nf_unregister_hooks(nf_test_ops, ARRAY_SIZE(nf_test_ops));
}

module_init(init_nf_test);
module_exit(exit_nf_test);

dmesg | tail 后的结果:

1
2
3
4
5
6
7
8
9
10
csharp复制代码[452013.507230] [MAC_DES:70,f3,95,e,42,faMAC_SRC: 0,f,fe,f6,7c,13 Prot:8]
[452013.507237] src IP:'10.6.124.55', dst IP:'10.6.124.54'
[452013.944960] [MAC_DES:70,f3,95,e,42,faMAC_SRC: 0,f,fe,f6,7c,13 Prot:8]
[452013.944968] src IP:'10.6.124.55', dst IP:'10.6.124.54'
[452014.960934] [MAC_DES:70,f3,95,e,42,faMAC_SRC: 0,f,fe,f6,7c,13 Prot:8]
[452014.960941] src IP:'10.6.124.55', dst IP:'10.6.124.54'
[452015.476335] [MAC_DES:70,f3,95,e,42,faMAC_SRC: 0,f,fe,f6,7c,13 Prot:8]
[452015.476342] src IP:'10.6.124.55', dst IP:'10.6.124.54'
[452016.023311] [MAC_DES:70,f3,95,e,42,faMAC_SRC: 0,f,fe,f6,7c,13 Prot:8]
[452016.023318] src IP:'10.6.124.55', dst IP:'10.6.124.54'

这个 Demo 程序是个内核模块,模块入口为module_init传入的init_nf_test函数。

在init_nf_test函数中,其通过 Netfilter 提供的 nf_register_hooks 接口将自定义的nf_test_opt注册到钩子节点中。nf_test_opt为struct nf_hook_ops类型的结构体数组,其内部包含了所有关键元素,比如钩子函数的注册节点(此处为NF_INET_LOCAL_IN)以及钩子函数(nf_test_in_hook)。

在nf_test_in_hook函数内部,其检查每一个传递过来的数据包,并将其源 Mac 地址,目的 Mac 地址,源 IP 地址以及目的 IP 地址打印出来。最后返回NF_ACCEPT,将数据包交给下一个钩子函数处理。


二、Linux流量控制

Traffic Control HOWTO:大多利用Netfilter来实现流的控制.

比较详细的文档是 Linux Advanced Routing & Traffic Control HOWTO 和缩简版的 Traffic Control HOWTO.

三、扩展阅读

Monitoring and Tuning the Linux Networking Stack: Sending Data

Linux Netfilter and Traffic Control

Netfilter and iptables homepage

图解 Linux 网络包发送过程

网络基础–七层模型

OSI七层模型与TCP/IP五层模型

Linux 网络层收发包流程及 Netfilter 框架浅析

Netfilter & iptables 原理

Netfileter & iptables 实现(一)— Netfilter实现

本文转载自: 掘金

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

sockfwd 一个数据转发的小工具

发表于 2021-11-20

最近在看containerd的代码,上手试的时候才发现它监听的是unix socket,没法从外部访问containerd。
而我要验证的是从远端能不能访问containerd、管理containerd的容器,所以需要一个从远端访问unix socket的工具。

网上搜了一圈,没有现成的实现,就自己写了 sockfwd。

用法

1
2
3
4
5
6
7
vbnet复制代码Usage:
sockfwd [flags]

Flags:
-d, --destination string 目的地址,即要转发到的地址
-s, --source string 源地址,即接收请求的地址
-q, --quiet 静默模式

例子

将本地的containerd实例暴露到网络上:

./sockfwd -s tcp://127.0.0.1:8090 -d unix:///var/run/containerd.sock

将本地的127.0.0.1:8080端口暴露到0.0.0.0:8090端口上:

./sockfwd -s tcp://127.0.0.1:8090 -d unix://127.0.0.1:8090

另外,需要注意:将本地的服务暴露到网络上,需要格外注意是否有安全隐患!

其他碎碎念

最近在写golang,发现如果仅仅是处理数据面的话,golang提供的goroute再加channel就能够很优雅地处理数据转发,比如这篇文章写的sockfwd。

但是如果涉及管控面,比如复杂的状态管理,信息跨线程/goroute同步,那么channel的抽象能力还是不够的,需要写很多额外代码。

后续计划

可以加上支持tls认证来更加安全地转发数据。

本文转载自: 掘金

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

手把手教你认识下 JVM 的内存划分,再记不住就真的没办法了

发表于 2021-11-20

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

  1. 引言

Java 在运行时会将内存划分为若干个区域,粗略的可以将内存划分为堆区和栈区,堆区主要存储 Java 对象。栈区主要记录对象的引用地址。

其实还有更详细的划分。如下图所示:

20190321-1

  1. Java 虚拟机栈

Java 虚拟机栈就是我们之前将内存分为堆区和栈区,这里的栈区就是指的 Java 虚拟机栈。

每个方法在执行的同时都会创建一个栈帧用于存储局部变量表、操作数栈、动态链表、方法出口信息等。每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。主要用来存储局部变量表。

在这个区域会抛出两个异常,如果请求的栈的深度超过了虚拟机允许的最大深度,会抛出 StackOverflowError ,如果无法申请到足够的内存会抛出 OutOfMemoryError 异常。

  1. 本地方法栈

与 Java 虚拟机栈类似,本地方法栈是服务于 Native 方法的。为本地方法提供内存,同样也会抛出 StackOverflowError、OutOfMemoryError 异常。

  1. 程序计数器

程序计数器可以看做当前线程所执行的字节码的行号计数器。字节码的解释器需要通过改变这个值来选取下一条需要执行的字节码指令。程序中的分支、循环,跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。

  1. 方法区

方法区是用来存放 JVM 装载的 class 的类信息,包括:类的方法、静态变量、类型信息(接口/父类),我们使用反射,所需的信息就是从这里获取的。同样的,当方法区无法满足内存需要的时候会抛出 OutOfMemoryError 。

4.1 运行时常量池

常量池是方法区的一部分,用于记录编译期生成的各种字面量和符号引用。在 Java 类中使用 final 标识的字段都会放到常量池里。常量池在运行时不是一成不变的。常见的场景是 String 类的 intern 方法。当调用该方法时,JVM 会判断常量池里是否有该对象,有的话直接返回,没有的话,需要把该字符串放入常量池然后再返回。

  1. Java 堆

右边橙色部分的区域都是 Java 堆。这部分内存是 Java 虚拟机里最大的内存区域。我们之前将内存粗略的划分为堆区和栈区,其中栈区指的是 Java 虚拟机栈,而堆区说的就是 Java 堆。所有的 Java 对象都是在这个区域分配的内存,虚拟机的 GC 动作 大部分都是在这个区域进行的。

再细分一下可以将 Java 堆分为新生代和老年代。新生代又可以分为 From Survivor 区、To Survivor 区、Eden 区。

新生代的 Eden 区、From Survivor 区、To Survivor 区的比值是 8:1:1。

对象在第一次创建的时候会在新生代分配内存。准确的说是在 Eden 区分配内存。 经过一次 GC 后,没有被回收的对象年龄加 1 并且会从 Eden 区、From Survivor 区转移到 To Survivor 区。 To Survivor 区会成为新的 From Survivor 区。 同样 From Survivor 区会成为新的 To Survivor 区。

20190321-2

当年龄超过一定的值后(默认15)会将该对象转移到老年代。

PS:并不是所有对象在第一次创建的时候都会在新生代分配内存,有些对象大到超过新生代的内存时,会直接在老年代分配内存

本文转载自: 掘金

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

AUFS:多点合一,写时复制

发表于 2021-11-20

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

AUFS(全称:advanced multi-layered unification filesystem,高级多层统一文件系统),用于为 Linux 文件系统实现联合挂载。提到联合挂载,就要先解一下 Union File System。

Union File System

Union File System,简称 UnionFS,是一种为 Linux、FreeBSD 和 NetBSD 操作系统设计的文件系统,它能够多个其他文件系统联合到一个地方来挂载。换句话说,就是不过目录合并 mount 到同一个目录中。
它和核心原理有两个:

  • 分支管理:它使用 branch 把不同文件系统的文件和目录”透明地”覆盖,形成一个单一一致的文件系统。这些 branch 或者是 read-only 的,或者是 read-write 的,所以当对这个虚拟后的联合文件系统进行写操作的时候,系统是真正写到了一个新的文件中。
  • 写时复制:copy-on-write,简写为 CoW,也叫隐式共享,是一种提高资源使用效率的资源管理技术。它的思想是:如果一个资源是重复的,在没有对资源做出修改前,并不需要立即复制出一个新的资源实例,这个资源被不同的所有者共享使用。当任何一个所有者要对该资源做出修改时,复制出一个新的资源实例给该所有者进行修改,修改后的资源成为其所有者的私有资源。通过这种资源共享的方式,可以显著地减少复制相同资源带来的消耗,但是这样做也会在进行资源的修改时增加一部分开销。

AUFS

AUFS 又叫 Another UnionFS,后来叫 Alternative UnionFS,后来可能觉得不够霸气,叫成 Advance UnionFS。是个叫 Junjiro Okajima(岡島順治郎)在 2006 年开发的,AUFS 完全重写了早期的 UnionFS 1.x,其主要目的是为了可靠性和性能,并且引入了一些新的功能,比如可写分支的负载均衡。AUFS 在使用上全兼容 UnionFS,而且比之前的 UnionFS 在稳定性和性能上都要好很多,后来的 UnionFS 2.x 开始抄 AUFS 中的功能。

实战 AUFS

在 Ubuntu 中,我们可以通过 mount 命令手动来创建 AUFS,以此来体验一下 AUFS 的巧妙之处。

创建实验目录和文件

  1. 创建实验目录 aufs
1
shell复制代码$ mkdir aufs
  1. 在 aufs 目录下创建 mnt 目标作为文件系统的挂载点
1
shell复制代码$ mkdir aufs/mnt
  1. 在 aufs 目录下创建 container-layer 文件夹,用来模拟容器的读写层
1
2
3
shell复制代码$ mkdir aufs/container-layer
# 同时创建文件 container-layer.txt 并初始化内容
$ echo "I am container layer" > aufs/container-layer/container-layer.txt
  1. 在 aufs 目录下创建三个文件 image-layer1、image-layer2、image-layer3,用来模拟容器的镜像层
1
2
3
4
shell复制代码$ mkdir aufs/{image-layer1,image-layer2,image-layer3}
$ echo "I am image layer 1" > aufs/image-layer1/image-layer1.txt
$ echo "I am image layer 2" > aufs/image-layer2/image-layer2.txt
$ echo "I am image layer 3" > aufs/image-layer3/image-layer3.tx

准备好的实验目录和文件结构如下:
image

创建 AUFS 文件系统

  1. 通过 mount 把 container-layer、image-layer1、image-layer2、image-layer3 以 AUFS 的方式挂载到刚才创建的 mnt 目录下
1
2
shell复制代码$ cd aufs
$ sudo mount -t aufs -o dirs=./container-layer:./image-layer1:./image-layer2:./image-layer3 none ./mnt

挂载完成后,查看 mnt 目录的文件结构为:
image
在 mount 命令中我们没有指定要挂载的 4 个文件夹的权限信息,其默认行为是:dirs 指定的左边起第一个目录是 read-write 权限,后续目录都是 read-only 权限。我们可以通过下面的方式查看详情:
image
其中的 si_f7dd5867fe18e716 目录是系统为 mnt 这个挂载点创建的,从上图中我们可以清楚的看到各个目录的挂载权限。

验证写时复制

接下来我们向 mnt/image-layer3.txt 文件写入数据:

1
shell复制代码$ echo "I changed mnt/image-layer3.txt" >> mnt/image-layer3.txt

查看 mnt/image-layer3.txt 的内容,发现已经变化了:
image
那再去看看底层文件 image-layer3/image-layer3.txt,发现他的内容并没有改变:
image
那么,改变的内容被存储到了什么地方呢?去看看 container-layer 目录:
image
原来,当尝试向 mnt/image-layer3.txt 中写入文件时,系统首先在 mnt 目录下查找名为 image-layer3.txt 的文件,将其拷贝到 read-write 层的 container-layer 目录中,接着对 container-layer 目录中的 image-layer3.txt 的文件进行写操作。这个过程也就是 AUFS 的实际工作原理。

到这里,我们就完成了 AUFS 的实验,从中也学到了 AUFS 写时复制的技术原理。实验结束完,别忘了回收实验环境:

1
shell复制代码$ sudo umount aufs/mnt

image

Docker by UnionFS

虽然当前 docker 默认的存储驱动已经演进到了 overlay2,但是学习 AUFS 依然可以帮助我们深入理解 docker 中的文件系统。当我们以后在使用 Docker 容器时,我们在 build 镜像时,写时复制就在发挥着作用。

本文转载自: 掘金

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

每天一个 Linux 命令(15)—— vmstat 命令简

发表于 2021-11-20

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

命令简介

vmstat 是 Virtual Meomory Statistics(虚拟内存统计)的缩写,可对操作系统的虚拟内存、进程、CPU 活动进行监控。他是对系统的整体情况进行统计,不足之处是无法对某个进程进行深入分析。

vmstat命令提供的统计数据主要来自系统内核维护的 /proc/meminfo、/proc/stat 和 /proc/*/stat 等文件。

命令格式

1
2
3
4
5
6
7
less复制代码vmstat [-a] [-n] [-S unit] [delay [ count]]
vmstat [-s] [-n] [-S unit]
vmstat [-m] [-n] [delay [ count]]
vmstat [-d] [-n] [delay [ count]]
vmstat [-p disk partition] [-n] [delay [ count]]
vmstat [-f]
vmstat [-V]//原文出自【易百教程】,商业转载请联系作者获得授权,非商业请保留原文链接:https://www.yiibai.com/linux/vmstat.html

命令参数

参数 解释
-a 显示活动/不活动内存的统计数据。
-d
-f
-m 显示系统的缓存分配信息。
-n 仅显示一次字段标题信息,而非周期性地重复显示。
-p partition 显示指定设备分区的详细统计数据。
-s 以列表的方式显示各种事件计数器和内存的统计数据。注意,使用这个选项时不能连续地重复显示。
-S unit 按照指定的字节单位 k(1000)、K(1024)、m(10002)或 M(10242)显示统计数据。
-t 在显示的统计数据中增加时间信息。
-V 显示命令的版本信息,然后退出。
-w 增加显示字段的宽度,以便能够显示较大的内存数据。

VM 模式输出字段说明

Procs(进程状态信息)

字段 解释
r 运行队列中等待运行的进程数量。
b 因等待 I/O 资源或面页调度而处于不可中断睡眠状态的进程数量。

Memory(内存使用情况)

字段 解释
swpd 正在使用的虚拟内存数量。
free 空闲内存的数量。
buff 用作 buffer 的内存数量。
cache 用作 cache 的内存数量。
inact 不活动内存的数量(-a 选项)。
active 活动内存的数量(-a 选项)。

Swap(内存磁盘交换)

字段 解释
si 每秒从磁盘交换到内存的内存数量。
so 每秒交换到磁盘的内存数量。

IO

字段 解释
bi 每秒读取块设备的数据块数量。
bo 每秒写入块设备的数据块数量。

System

字段 解释
in 每秒产生的中断数量(包括时钟中断)。
cs 每秒上下文切换的数量。

CPU(整个CPU时间分配的百分比)

字段 解释
us 用户模式运行时间(用户时间)占用整个 CPU 时间的百分比。
sy 内核模式运行时间(系统时间)占用整个 CPU 时间的百分比。
id 系统空闲时间占用整个 CPU 时间的百分比。
wa 等待 I/O 时间占用整个 CPU 时间的百分比。
st 从虚拟机偷取的时间占用整个 CPU 时间的百分比。

磁盘模式输出字段说明

Reads

字段 解释
total 成功读取磁盘次数的总和。
merged 把多个逻辑读操作合并成一次读操作的数量。
sectors 成功读取的扇区数量。
ms 读磁盘花费的时间(毫秒)。

Writes

字段 解释
total 成功写入磁盘次数的总和。
merged 把多个逻辑写操作合并成一次写操作的数量。
sectors 成功写入磁盘的扇区数量。
ms 写磁盘花费的时间(毫秒)。

IO

字段 解释
cur 当前正在执行的 I/O 数量。
s 读写磁盘花费的时间(秒)。

磁盘分区模式输出字段说明

字段 解释
reads 读取磁盘分区次数的总和。
read sectors 读取磁盘分区的扇区总和。
writes 写入磁盘分区次数的总和。
requested writes 请求写磁盘分区次数的总和。

SLAB 模式输出字段说明

字段 解释
cache 缓存的名字。
num 当前活动对象的数量。
total 可用对象的总和。
size 每个对象的大小。
pages 具有至少一个活动对象的页面数量。
totpages 已分配的页面总和。
pslab 每个 slab 结构的页面数量。

物理内存和虚拟内存区别

本部分内容来自 vmstat命令

我们知道,直接从物理内存读写数据要比从硬盘读写数据要快的多,因此,我们希望所有数据的读取和写入都在内存完成,而内存是有限的,这样就引出了物理内存与虚拟内存的概念。物理内存就是系统硬件提供的内存大小,是真正的内存,相对于物理内存,在 linux 下还有一个虚拟内存的概念,虚拟内存就是为了满足物理内存的不足而提出的策略,它是利用磁盘空间虚拟出的一块逻辑内存,用作虚拟内存的磁盘空间被称为交换空间(Swap Space)。作为物理内存的扩展,linux 会在物理内存不足时,使用交换分区的虚拟内存,更详细的说,就是内核会将暂时不用的内存块信息写到交换空间,这样以来,物理内存得到了释放,这块内存就可以用于其它目的,当需要用到原始的内容时,这些信息会被重新从交换空间读入物理内存。linux 的内存管理采取的是分页存取机制,为了保证物理内存能得到充分的利用,内核会在适当的时候将物理内存中不经常使用的数据块自动交换到虚拟内存中,而将经常使用的信息保留到物理内存。

要深入了解 linux 内存运行机制,需要知道下面提到的几个方面:

  • 首先,Linux 系统会不时的进行页面交换操作,以保持尽可能多的空闲物理内存,即使并没有什么事情需要内存,Linux 也会交换出暂时不用的内存页面。这可以避免等待交换所需的时间。
  • 其次,linux 进行页面交换是有条件的,不是所有页面在不用时都交换到虚拟内存,linux 内核根据”最近最经常使用“算法,仅仅将一些不经常使用的页面文件交换到虚拟内存,有时我们会看到这么一个现象:linux 物理内存还有很多,但是交换空间也使用了很多。其实,这并不奇怪,例如,一个占用很大内存的进程运行时,需要耗费很多内存资源,此时就会有一些不常用页面文件被交换到虚拟内存中,但后来这个占用很多内存资源的进程结束并释放了很多内存时,刚才被交换出去的页面文件并不会自动的交换进物理内存,除非有这个必要,那么此刻系统物理内存就会空闲很多,同时交换空间也在被使用,就出现了刚才所说的现象了。关于这点,不用担心什么,只要知道是怎么一回事就可以了。
  • 最后,交换空间的页面在使用时会首先被交换到物理内存,如果此时没有足够的物理内存来容纳这些页面,它们又会被马上交换出去,如此以来,虚拟内存中可能没有足够空间来存储这些交换页面,最终会导致 linux 出现假死机、服务异常等问题,linux虽然可以在一段时间内自行恢复,但是恢复后的系统已经基本不可用了。因此,合理规划和设计 linux 内存的使用,是非常重要的。

虚拟内存原理

在系统中运行的每个进程都需要使用到内存,但不是每个进程都需要每时每刻使用系统分配的内存空间。当系统运行所需内存超过实际的物理内存,内核会释放某些进程所占用但未使用的部分或所有物理内存,将这部分资料存储在磁盘上直到进程下一次调用,并将释放出的内存提供给有需要的进程使用。

在 Linux 内存管理中,主要是通过“调页 Paging”和“交换 Swapping ”来完成上述的内存调度。调页算法是将内存中最近不常使用的页面换到磁盘上,把活动页面保留在内存中供进程使用。交换技术是将整个进程,而不是部分页面,全部交换到磁盘上。

分页(Page)写入磁盘的过程被称作 Page-Out,分页(Page)从磁盘重新回到内存的过程被称作 Page-In。当内核需要一个分页时,但发现此分页不在物理内存中(因为已经被 Page-Out了),此时就发生了分页错误(Page Fault)。

当系统内核发现可运行内存变少时,就会通过 Page-Out 来释放一部分物理内存。经管 Page-Out 不是经常发生,但是如果 Page-out 频繁不断的发生,直到当内核管理分页的时间超过运行程式的时间时,系统效能会急剧下降。这时的系统已经运行非常慢或进入暂停状态,这种状态亦被称作 thrashing。

参考文档

  • vmstat命令
  • 《Linux 常用命令简明手册》—— 邢国庆编著

本文转载自: 掘金

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

并发编程(二)

发表于 2021-11-20

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

本文主要讲BlockingQueue

BlockingQueue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码public interface BlockingQueue<E> extends Queue<E> {

boolean add(E e);
boolean offer(E e);
void put(E e) throws InterruptedException;
boolean offer(E e, long timeout, TimeUnit unit) throws InterruptedException;
E take() throws InterruptedException;
E poll(long timeout, TimeUnit unit) throws InterruptedException;
//剩余容量, 无界队列的时候是Integer.Max
int remainingCapacity();
boolean remove(Object o);
public boolean contains(Object o);
//批量从队列中取出元素,无阻塞
int drainTo(Collection<? super E> c);
int drainTo(Collection<? super E> c, int maxElements);
}

add,offer, put都是插入, add和offer无阻塞,但是在队列满时add会抛出异常,offer不会。put会阻塞,抛出中断异常。

remove非阻塞, poll和toke都会阻塞, 并抛出中断异常。

ArrayBlockingQueue

1
2
3
4
5
6
7
8
9
10
11
java复制代码public ArrayBlockingQueue(int capacity) {
this(capacity, false);
}
public ArrayBlockingQueue(int capacity, boolean fair) {
if (capacity <= 0)
throw new IllegalArgumentException();
this.items = new Object[capacity];
lock = new ReentrantLock(fair);
notEmpty = lock.newCondition();
notFull = lock.newCondition();
}

数组实现的环型队列,在构造函数中必须传入数组容量,分公平和非公平

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java复制代码
/** The queued items */
final Object[] items;

//队头指针
int takeIndex;

//队尾指针
int putIndex;

//当前队列中元素个数
int count;

//锁和条件
final ReentrantLock lock;
private final Condition notEmpty;
private final Condition notFull;
  • take
1
2
3
4
5
6
7
8
9
10
11
java复制代码public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
while (count == 0)
notEmpty.await();
return dequeue();
} finally {
lock.unlock();
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码private E dequeue() {
// assert lock.getHoldCount() == 1;
// assert items[takeIndex] != null;
final Object[] items = this.items;
@SuppressWarnings("unchecked")
E x = (E) items[takeIndex];
items[takeIndex] = null;
if (++takeIndex == items.length)
takeIndex = 0;
count--;
if (itrs != null)
itrs.elementDequeued();
notFull.signal();
return x;
}
  • put
1
2
3
4
5
6
7
8
9
10
11
12
java复制代码public void put(E e) throws InterruptedException {
checkNotNull(e);
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
while (count == items.length)
notFull.await();
enqueue(e);
} finally {
lock.unlock();
}
}
1
2
3
4
5
6
7
8
9
10
java复制代码private void enqueue(E x) {
// assert lock.getHoldCount() == 1;
// assert items[putIndex] == null;
final Object[] items = this.items;
items[putIndex] = x;
if (++putIndex == items.length)
putIndex = 0;
count++;
notEmpty.signal();
}

LinkedBlockingQueue

基于单向链表的阻塞队列,队头和队尾是两个指针分开操作的, 使用了2把锁和2个条件。同时有一个AtomicInteger的原子变量记录count数。 构造函数中可以指定总容量, 默认Integer.MAX_VALUE

1
2
3
4
5
6
7
8
java复制代码public LinkedBlockingQueue() {
this(Integer.MAX_VALUE);
}
public LinkedBlockingQueue(int capacity) {
if (capacity <= 0) throw new IllegalArgumentException();
this.capacity = capacity;
last = head = new Node<E>(null);
}
1
2
3
4
5
6
7
8
9
10
11
12
java复制代码private final int capacity;
//原子变量
private final AtomicInteger count = new AtomicInteger();
//单向链表头
transient Node<E> head;
//单向链表尾
private transient Node<E> last;
//两把锁 两个条件
private final ReentrantLock takeLock = new ReentrantLock();
private final Condition notEmpty = takeLock.newCondition();
private final ReentrantLock putLock = new ReentrantLock();
private final Condition notFull = putLock.newCondition();
  • take
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
java复制代码public E take() throws InterruptedException {
E x;
int c = -1;
final AtomicInteger count = this.count;
final ReentrantLock takeLock = this.takeLock;
takeLock.lockInterruptibly();
try {
while (count.get() == 0) {
notEmpty.await();
}
x = dequeue();
c = count.getAndDecrement();
if (c > 1)
notEmpty.signal(); //如果还有元素通知其他take线程
} finally {
takeLock.unlock();
}
if (c == capacity)
signalNotFull();
return x;
}
  • put
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
java复制代码public void put(E e) throws InterruptedException {
if (e == null) throw new NullPointerException();
int c = -1;
Node<E> node = new Node<E>(e);
final ReentrantLock putLock = this.putLock;
final AtomicInteger count = this.count;
putLock.lockInterruptibly();
try {
while (count.get() == capacity) {
notFull.await();
}
enqueue(node);
c = count.getAndIncrement();
if (c + 1 < capacity)
notFull.signal();//通知其他put线程
} finally {
putLock.unlock();
}
if (c == 0)
signalNotEmpty();
}

LinkedBlockingQueue和ArrayBlockingQueue的差异:

  • 为了提供并发度, LinkedBlockingQueue分别控制队头队尾操作, put和put之间, take和take之间是互斥的,put和take之间不互斥, 但是count变量必须是原子的
  • 因为各拿了一个锁, 当需要调用对方的signal时,必须加上对方的锁,比如signalNotEmpty
1
2
3
4
5
6
7
8
9
java复制代码private void signalNotEmpty() {
final ReentrantLock takeLock = this.takeLock;
takeLock.lock();
try {
notEmpty.signal();
} finally {
takeLock.unlock();
}
}

PriorityBlockingQueue

按照元素优先级从小到大出队列,元素要可以比较大小实现Comparable接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
java复制代码//数组实现二叉小根堆
private transient Object[] queue;
private transient int size;
private transient Comparator<? super E> comparator;
//锁和非空条件
private final ReentrantLock lock;
private final Condition notEmpty;
private transient volatile int allocationSpinLock;
private PriorityQueue<E> q;
public PriorityBlockingQueue() {
this(DEFAULT_INITIAL_CAPACITY, null);
}
public PriorityBlockingQueue(int initialCapacity) {
this(initialCapacity, null);
}
public PriorityBlockingQueue(int initialCapacity,
Comparator<? super E> comparator) {
if (initialCapacity < 1)
throw new IllegalArgumentException();
this.lock = new ReentrantLock();
this.notEmpty = lock.newCondition();
this.comparator = comparator;
this.queue = new Object[initialCapacity];
}

如果不指定大小,内部会设置11, 当超过11会自动扩容。

  • put
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
java复制代码public void put(E e) {
offer(e); // never need to block
}
public boolean offer(E e) {
if (e == null)
throw new NullPointerException();
final ReentrantLock lock = this.lock;
lock.lock();
int n, cap;
Object[] array;
while ((n = size) >= (cap = (array = queue).length))
tryGrow(array, cap); //扩容
try {
Comparator<? super E> cmp = comparator;
if (cmp == null)
siftUpComparable(n, e, array);//自带的comparable
else
siftUpUsingComparator(n, e, array, cmp);
size = n + 1;
notEmpty.signal();
} finally {
lock.unlock();
}
return true;
}
  • take
1
2
3
4
5
6
7
8
9
10
11
12
java复制代码public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
E result;
try {
while ( (result = dequeue()) == null)
notEmpty.await();
} finally {
lock.unlock();
}
return result;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
java复制代码private E dequeue() {
int n = size - 1;
if (n < 0)
return null;
else {
Object[] array = queue;
E result = (E) array[0]; //永远是0
E x = (E) array[n];
array[n] = null;
Comparator<? super E> cmp = comparator;
//调整二叉堆
if (cmp == null)
siftDownComparable(0, x, array, n);
else
siftDownUsingComparator(0, x, array, n, cmp);
size = n;
return result;
}
}

和ArrayBlockingQueue机制类似, 主要区别在于使用数组实现了二叉堆,并且没有notFull条件, 能执行自动扩容

DelayQueue

延迟队列, 按照时间从小到大出队的PriorityQueue。放入DelayQueue的元素必须实现Delayed接口。

1
2
3
java复制代码public interface Delayed extends Comparable<Delayed> {
long getDelay(TimeUnit unit);
}

如果getDelay的返回值小于或等于0,说明元素到期。Delayed接口实现Comparable接口, 基于getDelay的返回值比较元素的大小。

1
2
3
4
5
java复制代码//一把锁和一个非空条件
private final transient ReentrantLock lock = new ReentrantLock();
private final Condition available = lock.newCondition();
//优先级队列
private final PriorityQueue<E> q = new PriorityQueue<E>();
  • take
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 E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
for (;;) {
E first = q.peek(); //取出堆顶元素
if (first == null)
available.await();
else {
long delay = first.getDelay(NANOSECONDS);
if (delay <= 0)
return q.poll();
first = null; // don't retain ref while waiting
if (leader != null) //如果其他线程在等着,无限期等待。
available.await();
else {
Thread thisThread = Thread.currentThread();
leader = thisThread;
try {
available.awaitNanos(delay);
} finally {
if (leader == thisThread)
leader = null;
}
}
}
}
} finally {
if (leader == null && q.peek() != null)
available.signal();
lock.unlock();
}
}

只有在队列为空的时候才阻塞,如果堆顶元素的延迟时间没到,也会阻塞。使用Thread leader记录等待堆顶元素的第一个线程。

  • put
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java复制代码public void put(E e) {
offer(e);
}
public boolean offer(E e) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
q.offer(e);
if (q.peek() == e) {
leader = null;
available.signal();
}
return true;
} finally {
lock.unlock();
}
}

SynchronousQueue

本身没有容量,先调用put,线程会阻塞, 知道另一个线程调用了take,两个线程同时解锁。反之亦然。

1
2
3
4
5
6
7
java复制代码public SynchronousQueue() {
this(false);
}

public SynchronousQueue(boolean fair) {
transferer = fair ? new TransferQueue<E>() : new TransferStack<E>();
}

公平先进先出,不公平先进后出

  • put
1
2
3
4
5
6
7
java复制代码public void put(E e) throws InterruptedException {
if (e == null) throw new NullPointerException();
if (transferer.transfer(e, false, 0) == null) {
Thread.interrupted();
throw new InterruptedException();
}
}
  • take
1
2
3
4
5
6
7
java复制代码public E take() throws InterruptedException {
E e = transferer.transfer(null, false, 0);
if (e != null)
return e;
Thread.interrupted();
throw new InterruptedException();
}

核心是transfer接口。

1
2
3
java复制代码abstract static class Transferer<E> {
abstract E transfer(E e, boolean timed, long nanos);
}

根据e, 如果不是null,判定是消费者,否则是生产者。

如果是公平-队列模式

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
java复制代码E transfer(E e, boolean timed, long nanos) {
QNode s = null; // constructed/reused as needed
boolean isData = (e != null);

for (;;) {
QNode t = tail;
QNode h = head;
if (t == null || h == null) // saw uninitialized value
continue; // spin

//队列为空或者当前线程和队列中元素是同一个模式
if (h == t || t.isData == isData) { // empty or same-mode
QNode tn = t.next;
if (t != tail) // inconsistent read
continue;
if (tn != null) { // lagging tail
advanceTail(t, tn);
continue;
}
if (timed && nanos <= 0) // can't wait
return null;
if (s == null)
s = new QNode(e, isData); //新建节点
if (!t.casNext(null, s)) // failed to link in
continue;

advanceTail(t, s); // swing tail and wait 后移tail进入阻塞状态
Object x = awaitFulfill(s, e, timed, nanos);
if (x == s) { // wait was cancelled
clean(t, s);
return null;
}

if (!s.isOffList()) { // not already unlinked 确定处于队列中的第一个元素
advanceHead(t, s); // unlink if head
if (x != null) // and forget fields
s.item = s;
s.waiter = null;
}
return (x != null) ? (E)x : e;

} else { // complementary-mode
QNode m = h.next; // node to fulfill
if (t != tail || m == null || h != head)
continue; // inconsistent read

Object x = m.item;
if (isData == (x != null) || // m already fulfilled
x == m || // m cancelled
!m.casItem(x, e)) { // lost CAS
advanceHead(h, m); // dequeue and retry
continue;
}

advanceHead(h, m); // successfully fulfilled
LockSupport.unpark(m.waiter);
return (x != null) ? (E)x : e;
}
}
}

TransferQueue基于单向链表实现的队列,初始head和tail指向空节点

如果是非公平-栈模式

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
java复制代码 E transfer(E e, boolean timed, long nanos) {
SNode s = null; // constructed/reused as needed
int mode = (e == null) ? REQUEST : DATA;

for (;;) {
SNode h = head;
if (h == null || h.mode == mode) { // empty or same-mode
if (timed && nanos <= 0) { // can't wait
if (h != null && h.isCancelled())
casHead(h, h.next); // pop cancelled node
else
return null;
} else if (casHead(h, s = snode(s, e, h, mode))) {
SNode m = awaitFulfill(s, timed, nanos);
if (m == s) { // wait was cancelled
clean(s);
return null;
}
if ((h = head) != null && h.next == s)
casHead(h, s.next); // help s's fulfiller
return (E) ((mode == REQUEST) ? m.item : s.item);
}
} else if (!isFulfilling(h.mode)) { // try to fulfill 待匹配
if (h.isCancelled()) // already cancelled
casHead(h, h.next); // pop and retry
else if (casHead(h, s=snode(s, e, h, FULFILLING|mode))) {
for (;;) { // loop until matched or waiters disappear
SNode m = s.next; // m is s's match
if (m == null) { // all waiters are gone
casHead(s, null); // pop fulfill node
s = null; // use new node next time
break; // restart main loop
}
SNode mn = m.next;
if (m.tryMatch(s)) {
casHead(s, mn); // pop both s and m
return (E) ((mode == REQUEST) ? m.item : s.item);
} else // lost match
s.casNext(m, mn); // help unlink
}
}
} else { // help a fulfiller 待匹配
SNode m = h.next; // m is h's match
if (m == null) // waiter is gone
casHead(h, null); // pop fulfilling node
else {
SNode mn = m.next;
if (m.tryMatch(h)) // help match
casHead(h, mn); // pop both h and m
else // lost match
h.casNext(m, mn); // help unlink
}
}
}
}

本文转载自: 掘金

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

1…260261262…956

开发者博客

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