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

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


  • 首页

  • 归档

  • 搜索

「Rust 重写 sqlite」SQL模块:CREATE

发表于 2021-11-21

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


SQL模块

到这正式进入sql模块。不过这实际与之前的 meta command 模块没有什么不同,至少在结构上是这样。我们有一个枚举,定义了我们最初计划支持的查询类型。然后是一个带有 fn new() 的 impl block,同样作为构造函数。

然后是一个 fn process_command(),返回 Result<String, SQLRiteError>,如果你还记得,这个函数是由 main.rs 调用的。在这个函数中,神奇的事情开始发生。

你会注意到,在 fn process_command() 的开头,我们使用了 sqlparser-rs crate,它为Rust建立了一个可扩展的SQL Lexer和解析器,并有许多不同的SQL方言,包括SQLite方言,所以我决定暂时使用它们而不是编写一个全新的SQL Lexer。

通过调用 Parser::parse_sql(),我得到了一个 Result<Vec<Statement>, ParserError>,我做了一些基本的检查,并把它传递给一个匹配语句,以确定输入的是哪种类型的SQL语句,或者在这个过程中是否有错误,如果有,我就返回这个错误。返回的语句是 sqlparser::ast::Statement,它是一个所有可能语句结果的枚举,你可以从 sqlparser文档 中看到。

目前,我实际建立的解析器的唯一SQL语句是 CREATE TABLE,对于其余的语句,我们只是识别SQL语句的类型并返回给用户。在与CREATE TABLE匹配的block中,我们调用另一个模块 Parser::create,它包含了CREATE TABLE的所有逻辑。

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
rust复制代码#[derive(Debug,PartialEq)]
pub enum SQLCommand {
Insert(String),
Delete(String),
Update(String),
CreateTable(String),
Select(String),
Unknown(String),
}

impl SQLCommand {
pub fn new(command: String) -> SQLCommand {
let v = command.split(" ").collect::<Vec<&str>>();
match v[0] {
"insert" => SQLCommand::Insert(command),
"update" => SQLCommand::Update(command),
"delete" => SQLCommand::Delete(command),
"create" => SQLCommand::CreateTable(command),
"select" => SQLCommand::Select(command),
_ => SQLCommand::Unknown(command),
}
}
}

// 使用sqlparser-rs执行SQL语句的初始解析
pub fn process_command(query: &str) -> Result<String> {
let dialect = SQLiteDialect{};
let message: String;
let mut ast = Parser::parse_sql(&dialect, &query).map_err(SQLRiteError::from)?;

if ast.len() > 1 {
return Err(SQLRiteError::SqlError(ParserError::ParserError(format!(
"Expected a single query statement, but there are {}",
ast.len()
))));
}

let query = ast.pop().unwrap();

// 最初只实现一些基本的SQL语句
match query {
Statement::CreateTable{..} => {
let result = CreateQuery::new(&query);
match result {
Ok(payload) => {
println!("Table name: {}", payload.table_name);
for col in payload.columns {
println!("Column Name: {}, Column Type: {}", col.name, col.datatype);
}
},
Err(err) => return Err(err),
}
message = String::from("CREATE TABLE Statement executed.");
// TODO: Push table to DB
},
Statement::Query(_query) => message = String::from("SELECT Statement executed."),
Statement::Insert {..} => message = String::from("INSERT Statement executed."),
Statement::Delete{..} => message = String::from("DELETE Statement executed."),
_ => {
return Err(SQLRiteError::NotImplemented(
"SQL Statement not supported yet.".to_string()))
}
};

Ok(message)
}

这是我们的 sql::parser::create 模块。

这里我们有两个结构类型的定义。第一个是 ParsedColumn:代表表中的一个列,第二个是 CreateQuery:代表一个表。正如你所看到的,CreateQuery有一个名为columns的属性,这是一个ParsedColumns的集合。我们在这个模块上的主要方法,也就是 fn new(),返回一个 Result<CreateTable, SQLRiteError>,然后将其插入到我们的数据库数据结构中,该结构仍有待于在代码中定义。

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
rust复制代码// Represents Columns in a table
#[derive(PartialEq, Debug)]
pub struct ParsedColumn {
pub name: String,
pub datatype: String,
pub is_pk: bool,
pub is_nullable: bool,
}

// Represents a SQL Statement CREATE TABLE
#[derive(Debug)]
pub struct CreateQuery {
pub table_name: String, // table name
pub columns: Vec<ParsedColumn>, // columns
}

impl CreateQuery {
pub fn new(statement: &Statement) -> Result<CreateQuery> {
match statement {
Statement::CreateTable {
name,
columns,
constraints: _constraints,
with_options: _with_options,
external: _external,
file_format: _file_format,
location: _location,
..
} => {
let table_name = name;
let mut parsed_columns: Vec<ParsedColumn> = vec![];

// 遍历 columns
for col in columns {
let name = col.name.to_string();
// TODO: 目前只能解析基础类型,没有时间戳和日期
let datatype = match &col.data_type {
DataType::SmallInt => "int",
DataType::Int => "int",
DataType::BigInt => "int",
DataType::Boolean => "bool",
DataType::Text => "string",
DataType::Varchar(_bytes) => "string",
DataType::Float(_precision) => "float",
DataType::Double => "float",
DataType::Decimal(_precision1, _precision2) => "float",
_ => {
println!("not matched on custom type");
"invalid"
}
};

let mut is_pk: bool = false;
for column_option in &col.options {
is_pk = match column_option.option {
ColumnOption::Unique { is_primary } => is_primary,
_ => false,
};
}

parsed_columns.push(ParsedColumn {
name,
datatype: datatype.to_string(),
is_pk,
is_nullable: false,
});
}
// TODO: 处理表约束,比如主键外键等
for constraint in _constraints {
println!("{:?}", constraint);
}
return Ok(CreateQuery {
table_name: table_name.to_string(),
columns: parsed_columns,
});
}

_ => return Err(SQLRiteError::Internal("Error parsing query".to_string())),
}
}
}

本文转载自: 掘金

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

Spring之AOP

发表于 2021-11-21

Spring之AOP

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

Spring的核心是控制反转(IoC)和面向切面(AOP)

前面我们已经了解了IOC,现在来了解什么是AOP!

一、AOP的概念

AOP(Aspect Oriented Programming)意为:面向切面编程,通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术。AOP是OOP的延续,是软件开发中的一个热点,也是Spring框架中的一个重要内容,是函数式编程的一种衍生范型。利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。

为什么说Spring是一个一站式的轻量级开源框架呢?JavaEE开发可分成三层架构,针对JavaEE的三层结构,每一层Spring都提供了不同的解决技术。

• WEB层:SpringMVC

• 业务层:Spring的IoC

• 持久层:Spring的JDBCTemplate(Spring的JDBC模板,ORM模板用于整合其他的持久层框架)

Spring AOP 模块提供拦截器来拦截一个应用程序,例如,当执行一个方法时,你可以在方法执行之前或之后添加额外的功能。

如下图:

image.png

在我们的开发中:dao、service、controller、view层都可以认为是一个切面,**对象与对象之间,方法与方法之间,模块与模块之间都是一个个切面。**我们不改变源代码的情况下,在执行某个切面里的某个方法的时候,在后期开发想添加一些功能,改动源代码代价太大,那么我们就可以通过切入点表达式,指定拦截哪些类的哪些方法, 给指定的类在运行的时候植入切面类代码。这就是面向切面编程。下图也是一个实现的流程。

image.png

注意:AOP不是一种技术,实际上是编程的思想。

二、AOP组成

可以通过预编译方式和运行期动态代理实现在不修改源代码的情况下给程序动态统一添加功能的一种技术。

主要的功能是:日志记录,性能统计,安全控制,事务处理,异常处理等等。

在我们开始使用 AOP 工作之前,让我们熟悉一下 AOP 概念和术语。这些术语并不特定于 Spring,而是与 AOP 有关的。

项 描述
Aspect(切面) 一个模块具有一组提供横切需求的 APIs。例如,一个日志模块为了记录日志将被 AOP 方面调用。应用程序可以拥有任意数量的方面,这取决于需求。
Join point(切入点) 在你的应用程序中它代表一个点,你可以在插件 AOP 方面。你也能说,它是在实际的应用程序中,其中一个操作将使用 Spring AOP 框架。
Advice(通知) 这是实际行动之前或之后执行的方法。这是在程序执行期间通过 Spring AOP 框架实际被调用的代码。
Pointcut(切入短) 这是一组一个或多个连接点,通知应该被执行。你可以使用表达式或模式指定切入点正如我们将在 AOP 的例子中看到的。
Introduction 引用允许你添加新方法或属性到现有的类中。
Target object(目标) 被一个或者多个方面所通知的对象,这个对象永远是一个被代理对象。也称为被通知对象。
Weaving(织入) Weaving 把方面连接到其它的应用程序类型或者对象上,并创建一个被通知的对象。这些可以在编译时,类加载时和运行时完成。

通知的类型

SpringAOP中,通过Advice可以使用下面提到的五种通知工作:

通知 描述
前置通知 在一个方法执行之前,执行通知。
后置通知 在一个方法执行之后,不考虑其结果,执行通知。
返回后通知 在一个方法执行之后,只有在方法成功完成时,才能执行通知。
抛出异常后通知 在一个方法执行之后,只有在方法退出抛出异常时,才能执行通知。
环绕通知 在建议方法调用之前和之后,执行通知。

三、AOP的实现

  • 通过 Spring API 实现
  • 自定义类来实现Aop
  • 通过注解实现

项目结构:

image.png

需要导入一个AOP织入依赖包!

1
2
3
4
5
6
xml复制代码<!-- https://mvnrepository.com/artifact/org.aspectj/aspectjweaver -->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.4</version>
</dependency>

1、通过 Spring API 实现

编写业务接口

1
2
3
4
5
6
java复制代码public interface UserService {
public void add();
public void delete();
public void update();
public void search();
}

编写业务接口实现类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
java复制代码public class UserServiceImpl implements UserService{
@Override
public void add() {
System.out.println("xiaoqian增加用户");
}
@Override
public void delete() {
System.out.println("xiaoqian删除用户");
}
@Override
public void update() {
System.out.println("xiaoqian更新用户");
}
@Override
public void search() {
System.out.println("xiaoqian查询用户");
}
}

编写两个日志增强类

前置增强

1
2
3
4
5
6
7
8
java复制代码public class BeforeLog implements MethodBeforeAdvice {

//method : 要执行的目标对象的方法 objects : 被调用的方法的参数 Object : 目标对象
@Override
public void before(Method method, Object[] objects, Object o) throws Throwable {
System.out.println( o.getClass().getName() + "的" + method.getName() + "方法被执行了");
}
}

后置增强

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码public class AfterLog implements AfterReturningAdvice {
//returnValue 返回值
//method被调用的方法
//args 被调用的方法的对象的参数
//target 被调用的目标对象
@Override
public void afterReturning(Object returnValue, Method method, Object[] args, Object target) throws Throwable {
System.out.println("执行了" + target.getClass().getName()
+"的"+method.getName()+"方法,"
+"返回值:"+returnValue);
}
}

编写applicationContext.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
xml复制代码<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop
https://www.springframework.org/schema/aop/spring-aop.xsd">

<!--注册bean-->
<bean id="userService" class="com.mq.service.UserServiceImpl"/>
<bean id="before" class="com.mq.log.AfterLog"/>
<bean id="afterLog" class="com.mq.log.BeforeLog"/>


<!--aop的配置 需要导入aop的约束-->
<aop:config>
<!--切入点 expression:表达式匹配要执行的方法-->
<aop:pointcut id="pointcut" expression="execution(* com.mq.service.UserServiceImpl.*(..))"/>
<!--执行环绕; advice-ref执行方法 . pointcut-ref切入点-->
<aop:advisor advice-ref="before" pointcut-ref="pointcut"/>
<aop:advisor advice-ref="afterLog" pointcut-ref="pointcut"/>
</aop:config>
</beans>

这里用的是 通过类定义切点

拓展:execution表达式的使用:

详细的使用可以参照这篇博客execution表达式
测试类

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

@Test
public void test(){
ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
UserService userService = (UserService) context.getBean("userService");
userService.search();
}

}

结果:

image.png

2、自定义类来实现Aop

自定义一个DiypointCut,写上自己想执行的方法

1
2
3
4
5
6
7
8
9
java复制代码public class DiypointCut {

public void before(){
System.out.println("=========before方法执行前========");
}
public void after(){
System.out.println("=========after方法执行后=========");
}
}

编写applicationContext.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
xml复制代码 <!--第二种方式自定义实现-->
<!--注册bean-->
<bean id="userService" class="com.mq.service.UserServiceImpl"/>
<bean id="diy" class="com.mq.diy.DiypointCut"/>
<!--aop的配置-->
<aop:config>
<!--第二种方式:使用AOP的标签实现-->
<aop:aspect ref="diy">
<aop:pointcut id="diyPonitcut" expression="execution(* com.mq.service.UserServiceImpl.*(..))"/>
<aop:before pointcut-ref="diyPonitcut" method="before"/>
<aop:after pointcut-ref="diyPonitcut" method="after"/>
</aop:aspect>
</aop:config>

测试类

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

@Test
public void test(){
ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
UserService userService = (UserService) context.getBean("userService");
userService.delete();
}

}

结果:

image.png

3、注解实现

编写AnnotationPointcut注解实现的增强类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
java复制代码@Aspect  //标注这个类是一个切面
public class AnnotationPointcut {

@Before("execution(* com.mq.service.UserServiceImpl.*(..))")
public void before(){
System.out.println("---------方法执行前---------");
}
@After("execution(* com.mq.service.UserServiceImpl.*(..))")
public void after(){
System.out.println("---------方法执行后---------");
}

//在环绕增强中,我们可以给定一个参数,代表我们要获取处理切入的点
@Around("execution(* com.mq.service.UserServiceImpl.*(..))")
public void around(ProceedingJoinPoint jp) throws Throwable {
System.out.println("环绕前");
System.out.println("签名:"+jp.getSignature());
//执行目标方法proceed
Object proceed = jp.proceed();
System.out.println("环绕后");
System.out.println(proceed);
}
}

在Spring配置文件中,注册bean,并增加支持注解的配置

1
2
3
4
5
6
7
8
9
xml复制代码    <!--第三种方式:注解实现-->
<bean id="annotationPointcut" class="com.mq.diy.AnnotationPointcut"/>
<bean id="userService" class="com.mq.service.UserServiceImpl"/>
<!-- proxy-target-class属性,默认为false,表示使用jdk动态代理织入增强,
当配为<aop:aspectj-autoproxy poxy-target-class="true"/>时,
表示使用CGLib动态代理技术织入增强。不过即使proxy-target-class设置为false,
如果目标类没有声明接口,则spring将自动使用CGLib动态代理。-->
<!--开启注解支持-->
<aop:aspectj-autoproxy proxy-target-class="true"/>

测试类:

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

@Test
public void test(){
ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
UserService userService = (UserService) context.getBean("userService");
userService.add();
}

}

结果:

image.png

学习一定要多看官方文档,多看源码,了解底层原理是怎么实现的,今日份学习!

本文转载自: 掘金

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

实现DevOps的三步工作法 第一步:流动原则 第二步:反馈

发表于 2021-11-21

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

你好,我是看山。

《凤凰项目-一个IT运维的传奇故事》是一本比较神奇的书,用讲故事的方式,展现了IT团队(开发、测试、运维)在开发效能低、系统交付慢的情况下,通过实践三步工作法,在团队中实现加快系统交付、提升开发效能,使团队走上DevOps之路。而且本书有一个值得称道的地方是,通过类比制造业的工作流程,可以直观发现技术团队工作过程中隐藏的问题。

这里需要提醒一下开发人员,看书的时候一定要佛系,因为这个故事是以运维角度展开的,有一些大骂开发的情节。如果是想找具体的DevOps工具的,建议不要看了,里面没有具体的工具介绍,是以最朴素的方式,讲述DevOps的优势和实践。

先说一下概念:

  • 价值流:一个组织基于客户的需求所执行的一系列有序的交付活动。或者,为了给客户设计、生产和提供产品或服务所需从事的一系列活动,它包含了信息流和物料流的双重价值。
  • 技术价值流:把业务构想转化为向客户交付价值的、有技术驱动的服务所需要的流程。流程的输入是需求,由开发部门完成开发,进行整体测试,部署到生产环境正常运行,并为客户提供服务,以产生价值。
  • 前置时间:从需求确认(开发接收需求)开始计时,到工作完成时结束
  • 处理时间:从实际开始处理工作开始计时,到工作完成结束
  • 等待时间:从需求确认(开发接收需求)开始计时,到实际开始处理工作时结束
  • 在制品/半成品:价值流里没有彻底完成的工作、处于队列中的工作。部分完成的工作会逐渐过期,随着时间推移到最终失去价值。
  • 约束点:价值流中的瓶颈,即整个价值流流速的上限点。

第一步:流动原则

图片

第一步流动原则是为了打通技术价值流通道,实现开发到运维的工作快速从左到右流动。通过加速技术价值流的流速,缩短满足客户需求的前置时间,提高工作质量和产量,并使企业具有更强的竞争力。相关实践包括:持续构建、集成、测试和部署,按需搭建环境,限制在制品数量,构建能够安全实施变更的系统和组织。

通过持续加强工作内容的可视化,减小批次大小和等待间隔,内建质量以防止缺陷向下游传递,

使工作可视

在制造业,原料或半成品的堆积、订单积压都是直观可见,产生阻塞的地方,就是约束点。但是在技术价值流中,很多问题是隐藏的,没有办法很明显的看到阻塞和约束点。同时,因为信息的不可见或者彼此信息不全,可能将问题传递到下一环节,甚至上线时才出现问题,或者根本没法交付。这就需要尽可能的将工作可视化,用来识别工作在哪里流动、排队、停滞。

一般,可以使用Kanban管理(来自日语,就是看板)或Sprint计划板作为工具。

图片

可视化管理中有一个需要注意的地方,要着眼全局目标,而不是局部目标。全局目标是增加系统质量,提升开发效能,局部目标是开发的完成率、测试的缺陷数、系统的可用性等。不是说局部目标不重要,这些局部目标需要其他方式来优化,我们现在需要提升整体的效率,一旦我们陷入细节中,就是一叶障目不见泰山,没有办法把握全局了。也就是吉恩•金所说的“不允许局部优化造成整体性能下降”。

限制在制品数

工作可视化之后,就可以开始有素质的找茬了。

第一步,限制并行任务。为什么?因为如果有并行任务,我们就需要花时间切换任务。有一种说法,如果同时进行两个任务,会有20%的时间用于切换任务,比如理清思路、进入状态、恢复工作环境等。如果是三个,那就会有40%的时间用于切换任务。并行任务越多,用于切换任务所花费的时间越多,造成的人力浪费越多。并行任务减少之后,浪费的时间减少,花费在工作上的时间就增加了,整体的交付效率也相应的提升。如果是用的是看板管理,可以通过限制每一列或每个工作中心在制品(并行任务)数量,并把卡片数量的上限标记在每一列上。

减小批量大小

这个就是敏捷中提倡的小步快跑,先交付,先尝试,就可以先试错,先改错,有问题可以及早暴露,不至于最后集成一个大疙瘩,无力回天。

这里不得不提持续部署,相信很多团队在使用持续部署工具,比如Jenkins,代码提交之后,触发Jenkins工作流,开始进行编译、测试、部署和发布。我们要做的就是小批次的提交代码,这部分代码被编译、测试,如果有问题,能够尽早发现,如果没有问题,经过测试发布到正式环境,就能够及早的呈现给客户。

图片

减少交接次数

在传统的IT团队中,代码从开发完成到部署上线需要经过N多个部门,每个部门有自己的KPI,有自己的任务排期,不同部门沟通需要成本,工单审批需要时间,这样就造成了交付时间的延长。另外,不同部门对于一个功能的认知有自己的认知陷阱,开发认为理所当然的时期,运维可能根本不了解。信息的隔离,可能造成一些已知的缺陷没有及时传递到下游,出现各种返工的情况。

为了减少这种交接,可以引入自动化方式完成大部分的操作,或者调整架构,让团队尽量少的依赖于其他人。

图片

持续识别和改善约束点

约束点就是瓶颈,如果整个团队中存在约束点,交付工作流就会有瓶颈。随着工作的优化,约束点之前的工作会堆积到约束点,而约束点之后的角色因为任务还没有到达,可能出现等待的情况。想要提升整体的效能,必须找到约束点,并进行拓宽,才能增加任务的吞吐量,任何不针对约束点进行的优化都是假象。

一般按照下面的步骤拓宽约束:

  1. 识别约束点,任务队列最长的角色,就是约束点;
  2. 根据找到的约束点,找到拓宽约束点的方式;
  3. 根据2中的决定,考虑全局工作;
  4. 改善系统的约束点;
  5. 如果约束已经拓宽,整个工作流中会出现新的约束点,重复上面的步骤。

消除价值流中的困境和浪费

想要提升交付效率,除了开源,还需要节流。减少任何超出客户需求和他们愿意支付范围的任何材料和资源:

  • 半成品:价值流里没有彻底完成的工作、处于队列中的工作。部分完成的工作会逐渐过期,随着时间推移到最终失去价值。比如没有确认的需求、等待评审的变更。
  • 额外工序:在交付过程中执行的、并未给客户增值的额外工作。比如对下游没有使用过的文档,或者对输出结果没有增值的评审。
  • 额外功能:在交付过程中构建那些组织和客户完全不需要的功能,还没有到镀金阶段,却要浪费时间在镀金上,镀金的功能会增加功能测试和管理的复杂度和工作量。
  • 任务切换:将人员分配到多个项目或价值流中,因为会出现任务切换,会在价值流中耗费额外的工作流和时间。
  • 等待:由于资源竞争产生的等待,这将增加周期时间,延迟向客户交付。比如等待其他部门配合。
  • 移动:信息或数据在工作中心之间移动的工作量。比如对于那种需要频繁沟通的人员不在一地办公,人员移动就产生浪费了。或者工作交接也会产生移动浪费,需要额外沟通。
  • 缺陷:由于信息、材料或产品的错误、残缺或模糊,需要一定的工作量来确认。缺陷的产生和被检测出来的时间间隔越长,解决问题就越困难。
  • 非标准或手工操作:需要依赖其他人的非标准或手动的工作,比如手动部署系统
  • 填坑侠:为了实现组织目标,不得不把有些人和团队置于不太合理的处境。

只有解决了上面的八种浪费,系统的改进,减轻或消除这些负担,实现快速流动的目标。

第二步:反馈原则

图片

第一步工作法是为了使工作能够在价值流能够从左向右流动,第二步工作法是创建从右到左的每个阶段能够快速、持续获得工作反馈的机制。该方法通过放大反馈环防止问题复发,并能够缩短问题检测周期,实现快速修复故障。我们的目标是从源头控制质量,并在流程中嵌入相关知识;创造更安全的工作系统,在故障或事故发生前检测到并解决它;最终建立安全和可靠的工作系统。

一般来说,发现和纠正问题最好的时机是发生故障时,只有发现问题,才能够解决问题。通过在整个工作流和组织中建立高质量的反馈机制,就可以在规模比较小、成本比较低的情况下修复系统。在灾难发生前消除问题,并创造出组织性学习氛围。

在复杂系统中安全地工作

复杂系统的一个重要特征是无法将系统视为一个整体,系统中的各个组件之间通常是紧耦合且紧密关联的,不能仅仅依据组件的行为解释系统的行为。复杂系统中故障存在且不可避免,所以我们需要设计一个安全的工作系统,可以让工程师们在系统中无所畏惧的开展工作,也就是各种折腾,这样才能在灾难发生前,快速检测出错误。可以采取下面4项措施让负载系统更加安全:

  • 管理复杂的工作,从中识别设计和操作的问题
  • 群策群力解决问题,从而快速构建新知识
  • 在整个组织中,将区域性的新知识应用到全局范围
  • 领导者要持续培养有以上才能的人

及时发现问题

想要及时发现问题,一般有两种做法:被动等待、主动试错。

通常,我们会搭建监控系统、设置多维度指标,对系统进行监控,当系统发生故障时,相关人员会收到报警信息,针对报警信息开始定位解决问题。这种方式属于被动等待的做法,因为要等待故障发生,故障发生的时机不可控,可能发生在上班的时候,更有可能发生在晚上睡觉时、周末休息时、休假旅游时,还有的会在结婚交拜的时,但是这种方式又不能没有,被动等待所搭建的监控系统是主动试错的基础。

主动试错就是在安全的工作系统中,不断对设计和假设进行验证,这种方式两个关键词是主动、安全。如果我们验证过程中把生产系统弄瘫了,那就笑话了。这样做的目标是更早、更快、以最低的成本、从尽可能多的维度增加系统的信息流,并尽可能清晰的确定问题的前因后果。能排除的假设越多,定位和解决问题的速度就越快。同时,这个过程也是练兵的过程,能很好的学习和创新。

群策群力,战胜问题获取新知

这个是承接“及时发现问题”的,因为发现问题之后,需要解决问题,我们需要发动所有相关人员,群策群力,解决问题。出现问题的时候,最忌讳的是绕开问题或者用“时间不够”这类理由搪塞。我们要做的是不惜全面停产,也要把问题解决。

至于为什么要相关人员都参与到解决问题中,理由如下:

  • 相关人员参与定位和处理问题,能够让大家更深入的理解系统,把无法规避、早期无知阶段变成学习的过程。
  • 能够防止把问题带入下游环节,一旦进入下游环节,修复成本和工作量将呈指数增加,还会欠下技术债
  • 阻止启动新的工作,问题不解决,就开始新的功能,就会引入新的问题
  • 不解决问题,故障就会再次发生,修复成本更高

图片

在源头保障质量

这点主要是针对QA和开发两个部门,有点类似国家政策:“谁污染谁治理”。在日常工作中,我们需要价值流中的每个人在他们的控制领域内发现并解决问题,通过这种方式,可以把质量控制、安全责任和决策制定都置于开展工作的场景里,而不是依赖于外围高层管理者的审批。

比如开发人员开发过程中,可以使用自动化测试,不依赖于测试团队,这样,开发人员就能够在任何需要的时候快速测试自己的代码,经过完善的自动化测试,就可以把代码部署到正式环境中。这样,自己对自己负责,同时也是对他人负责。

为下游工作进行优化

这点负责的是精益原则:我们最重要的客户是我们的下游,为下游优化我们的工作,需要我们对他们有同理心,更好的识别可能阻碍快速平滑流动的设计问题。比如开发需要为运维优化自己的工作,比如架构、性能、稳定性、可测试性、可配置性、安全性等一系列特性,这些优化工作,和给客户提供功能同样重要。

第三步:持续学习与实验原则

图片

第一步建立从左到右的工作流,第二步建立从右到左的反馈机制,第三步就是要建立持续学习与实验的文化,通过提升个人技能,进而转化为团队和组织的财富。

这一步的核心是建立高度信任的文化,这种文化强调每个人都是持续学习者,在日常工作中,主动承担风险;通过安全的方法改进工作和开发产品,从成功或失败中积累经验,从而识别有价值的想法,摒弃无价值的想法。个人的努力带动整体的进化,帮助整个团队尝试和实践新技术、新方法。

必要的做法包括营造一种勇于创新、敢于冒险(相对于畏惧或盲目服从命令)以及高信任度(相对于低信任度和命令控制)的文化,把至少20%的开发和IT运维周期划拨给非功能性需求,并且不断鼓励进行改进

建立学习型组织和安全文化

在复杂系统中,精确预测出结果是不现实的。也就是说,无论我们怎么小心,故障总是会发生。

Westrum模型提出组织文化的三种类型:

  • 病态型组织的特点是大量的恐惧和威胁,倾向于隐藏失败。
  • 官僚型组织的特点是严格的规则和流程,每个部门各扫门前雪,在这种组织中,通过评判系统处理事故,采用恩威并施的手段。
  • 生机型组织是积极探索和分享信息,在这种组织中,整个团队所有员工共同承担责任,对事故积极反思并找到根本原因。

第三步推崇的就是生机型组织,在故障发生时,团队关注的是如何设计安全的系统,防止事故复发,而不是追究人的问题。正如Etsy的工程师拜塞尼•马克里说的:“不指责,就没有恐惧;没有恐惧,就能够坦诚;坦诚能够有效的预防事故。”

将日常工作的改进制度化

在技术价值流中,为了防止灾难性事故的发生,团队会陷入实施各种临时解决方案的工作中,这样就没有时间去完成那些有价值的工作。所以,用临时方案解决问题的模式,会导致问题和技术债务的累积。所以,我们需要在日常工作中留出时间来改善日常工作,比如偿还技术债务、修复bug、重构和优化代码等,这就要求我们的团队在开发间歇中预留一段时间,可以让团队成员解决问题。一件事情的果总会是另一件事情的因。我们在把日常问题解决了,有助于发现和解决潜在风险,或者有更多的精力去做更多有意义的事情。

把局部发现转化为全局优化

局部发现转化为全局优化就是说要在团队中做到先富带动后富。单个团队或个人获得了某种独有的知识或经验,应该把这种隐性知识(难以通过文档或沟通方式传递的知识)转换为显性知识,建立全局知识库,形成集体智慧。当其他人做类似的工作时,只需要在知识库中搜索,就能够很快找到前辈的经验。

在日常工作中注入弹性模式

这样做的目标是为了给团队或系统增加弹力,提升抗脆弱性。想要抗脆弱,就要知道脆弱点在哪。根据前人的经验,我们可以通过缩短部署实践、提高测试覆盖率、缩短测试执行时间、系统解耦等方式,提升系统的弹力。我们可以通过故障演练,比如随机的拔网线、关电源、杀进程(比如Netflix的Chaos Monkey)等,能够验证系统的恢复能够力。我们还可以通过压测(单接口、全链路)来测试系统的瓶颈和上限。

图片

领导层强化学习文化

这点是说给领导听的,优秀的领导力不是体现在做出所有的决定都是正确的,而是能够为团队创造条件,让团队在日常工作中感受到这种卓越。因为领导者不会亲自参与到一线工作,一线工作者也不了解大的组织环境或者不具备工作领域以外做出改变的权利,所以领导者与一线工作者之间是互补关系,必须互相尊重。

最后

DevOps三步工作法作为支撑DevOps的基础原则,也衍生出了DevOps的行为和模式。相信很多团队已经开始走DevOps之路了,下面列出来4个阶段:

  1. 只有Dev没有Ops,所有的事情开发自己搞定。
  2. 有Dev也有Ops,他们相互独立,Ops承接了开发代码之外所有的工作。
  3. Dev+Ops,Ops做了一些自动化的工具提升效率,但主要是给自己去用,开发不用。
  4. DevOps,在上游工作的开发愿意使用下游的运维提供的系统或平台,通过API自助、自动的完成相应的工作。

可以看下自己处于那种阶段,如果没有达到,可以参考上面的三步工作法,一步一步的实现DevOps。

推荐阅读

  • 什么是微服务?
  • 微服务编程范式
  • 微服务的基建工作
  • 微服务中服务注册和发现的可行性方案
  • 从单体架构到微服务架构
  • 如何在微服务团队中高效使用 Git 管理代码?
  • 关于微服务系统中数据一致性的总结
  • 实现DevOps的三步工作法
  • 系统设计系列之如何设计一个短链服务
  • 系统设计系列之任务队列
  • 软件架构-缓存技术
  • 软件架构-事件驱动架构

你好,我是看山。游于码界,戏享人生。如果文章对您有帮助,请点赞、收藏、关注。欢迎关注公众号「看山的小屋」,发现不一样的世界。

本文转载自: 掘金

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

Java 中的抽象类和接口

发表于 2021-11-21

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

抽象类

抽象类不允许被实例化,只能被继承,所以我们不能通过 new 来创建抽象类的实例。

抽象类可以包含属性和方法,方法必须要有抽象方法,只有含有抽象方法的类才叫做抽象类。

子类继承抽象类,必须实现抽象类中所有的抽象方法。

接口

抽象类本身也是类,只不过是更加抽象的类,而接口就更加牛逼了,比抽象类还要抽象,很多地方都会说抽象类和接口的区别在于 is-a 和 has-a 的区别,大家自己感受一下即可。

接口可以理解为是一种协议,一种标准,我只负责定义,不负责实现,然而在 Java 8 接口中也能定义 default 的方法了。

像我们常见用 repository 层,就会定义一套接口,而 service 层在使用的时候不需要知道具体的实现是怎么样的,底层的数据库是 MySQL,Oracle ,甚至是 Redis 这些都不重要。

抽象类和接口的使用

抽象类在使用的时候需要注意,因为所有的子类都需要实现其所有的抽象方法,所以在定义的时候,一定要保证是父子类的关系,且抽象类中的方法不易过多,而接口的定义,就是为了定义某种标准,可能是数据交互的标准,子类需要实现接口就代表子类拥有了某种能力。

Java 中虽然不支持多重继承,但是我们可以通过多重实现来达到相似的功能。抽象类多用于模板方法,对模板中某一部分进行抽象,而接口则是完全的抽象。

抽象类的应用 - 模板设计模式

模板设计模式就是指固定的套路 。假如你有如下需求 ,计算代码的运行时间 。套路就是记录开始时间 ,运行方法 ,记录结束时间 ,最后一减得出结果 。可以像这样实现 。

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
csharp复制代码public abstract class SupClass {
​
   public abstract void run();
​
   //这就是一个模板,限定为 final 防止子类重写该方法。
   public final void calculateTime(){
       long l1 = System.currentTimeMillis();
       run();
       long l2 = System.currentTimeMillis();
​
       long time = l2 - l1;
       System.out.println("运行时间为 " + time + " 毫秒。");
  }
}
​
​
​
public class Sub1Class extends SupClass{
​
   @Override
   public void run() {
       System.out.println("在计算 Sub1Class 的 run 方法执行时间。。。");
       long sum = 0;
       for (int i = 0; i < 1000000000; i++) {
           sum = sum + i;
      }
  }
}
​
​
​
public class Sub2Class extends SupClass{
​
   @Override
   public void run() {
       System.out.println("在计算 Sub2Class 的 run 方法执行时间。。。");
       long sum = 0;
       for (int i = 0; i < 10000000; i++) {
           sum = sum + i;
      }
  }
}
​
​
// 测试代码
public class TemplateTest {
​
   public static void main(String[] args) {
       SupClass class1 = new Sub1Class();
       class1.calculateTime();
​
       SupClass class2 = new Sub2Class();
       class2.calculateTime();
​
  }
}

接口的应用 - 工厂模式

在工厂模式中 ,我们在创建对象时不会对客户端暴露创建逻辑 ,并且是通过使用一个共同的接口( Car )来指向新创建的对象 。这就是一种创建类的方法 。主要体现在工厂中获取对象的方法的内部逻辑 。像下面示例中的 getCar 方法 。

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
typescript复制代码public interface Car {
   void run();
}
​
public class BaoMa implements Car{
​
   @Override
   public void run() {
       System.out.println("我在 BaoMa 里笑。");
  }
​
}
​
public class BigCar implements Car{
​
   @Override
   public void run() {
       System.out.println("我是 BigCar ,我不怕撞!");
  }
​
}
​
public class SmallCar implements Car {
​
   @Override
   public void run() {
       System.out.println("我是 SmallCar ,看起来精致!");
  }
​
}
​
/**
* 工厂决定以何种形式创建对象,为什么叫工厂,也就是因为,对象如何产生是在这里决定的。
*
*/
public class CarFactory {
​
   // 这是重点呀,返回的都是同一个接口。这也是多态的体现 向上转型。
   public Car getCar(String type){
         if(type == null){
            return null;
        }        
         if(type.equalsIgnoreCase("SMALLCAR")){
            return new SmallCar();
        } else if(type.equalsIgnoreCase("BIGCAR")){
            return new BigCar();
        } else if(type.equalsIgnoreCase("BAOMA")){
            return new BaoMa();
        }
         return null;
      }
}
​
// 测试类
public class FactoryTest {
​
   public static void main(String[] args) {
​
       CarFactory factory = new CarFactory();
​
       Car car = factory.getCar("smallcar");
       car.run();
​
       Car car2 = factory.getCar("bigcar");
       car2.run();
​
       Car car3 = factory.getCar("baoma");
       car3.run();
  }
}
​
我是 SmallCar ,看起来精致!
我是 BigCar ,我不怕撞!
我在 BaoMa 里笑。

总结

抽象类和接口的区别

本文转载自: 掘金

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

Python logging日志错误和exception异常

发表于 2021-11-21

需求

  • 在Python 错误等级日志输出或是程序异常时做一些处理,如:日志告警,异常通知

实现

logging record

  • 通过重写 logging.Logger.callHandlers 方法可以在打印日志时做一些其他的处理
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
ruby复制代码import logging

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

old_callHandlers = logging.Logger.callHandlers # 复制原有的 callHandlers


def callHandlers(self, record):
try:
return old_callHandlers(self, record)
finally:
# 这里可以添加自定义业务逻辑, 每次打印log都会进来
if record.levelno >= logging.ERROR: # 大于等于ERROR等级的log才输出
logger.info("callHandlers: %s", record.message)


# 替换原来的callHandlers方法
logging.Logger.callHandlers = callHandlers

logger.info("info log")
logger.error("error log")
logger.error("error log001")


# 输出
INFO:__main__:info log
ERROR:__main__:error log
INFO:__main__:callHandlers: error log
ERROR:__main__:error log001
INFO:__main__:callHandlers: error log001

异常处理

  • 实现 sys.excepthook 方法,如果是线程需要实现 threading.excepthook
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
ruby复制代码import sys
import logging


def callback_func(exc_type, exc_value, exc_traceback):
logging.error("callback_func>>>>>>>>>>>>>>", exc_info=(
exc_type, exc_value, exc_traceback))


sys.excepthook = callback_func

1 / 0


# 输出
ERROR:root:callback_func>>>>>>>>>>>>>>
Traceback (most recent call last):
File "C:/Users/dong/PycharmProjects/err-callback-py/main.py", line 14, in <module>
1 / 0
ZeroDivisionError: division by zero

最后

  • 推荐一个封装好的库 err-callback-py, 那样直接写自己的业务逻辑就可以了

本文转载自: 掘金

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

5930 两栋颜色不同且距离最远的房子

发表于 2021-11-21

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

  1. 两栋颜色不同且距离最远的房子

街上有 n 栋房子整齐地排成一列,每栋房子都粉刷上了漂亮的颜色。给你一个下标从 0 开始且长度为 n 的整数数组 colors ,其中 colors[i] 表示第 i 栋房子的颜色。

返回 两栋 颜色 不同 房子之间的 最大 距离。

第 i 栋房子和第 j 栋房子之间的距离是 abs(i - j) ,其中 abs(x) 是 x 的绝对值。

示例 1:

图片.png

1
2
3
4
5
6
scss复制代码输入:colors = [1,1,1,6,1,1,1]
输出:3
解释:上图中,颜色 1 标识成蓝色,颜色 6 标识成红色。
两栋颜色不同且距离最远的房子是房子 0 和房子 3 。
房子 0 的颜色是颜色 1 ,房子 3 的颜色是颜色 6 。两栋房子之间的距离是 abs(0 - 3) = 3 。
注意,房子 3 和房子 6 也可以产生最佳答案。

示例 2:

图片.png

1
2
3
4
5
scss复制代码输入:colors = [1,8,3,8,3]
输出:4
解释:上图中,颜色 1 标识成蓝色,颜色 8 标识成黄色,颜色 3 标识成绿色。
两栋颜色不同且距离最远的房子是房子 0 和房子 4 。
房子 0 的颜色是颜色 1 ,房子 4 的颜色是颜色 3 。两栋房子之间的距离是 abs(0 - 4) = 4 。

示例 3:

1
2
3
4
scss复制代码输入:colors = [0,1]
输出:1
解释:两栋颜色不同且距离最远的房子是房子 0 和房子 1 。
房子 0 的颜色是颜色 0 ,房子 1 的颜色是颜色 1 。两栋房子之间的距离是 abs(0 - 1) = 1 。

提示:

  • n == colors.length
  • 2 <= n <= 100
  • 0 <= colors[i] <= 100
  • 生成的测试数据满足 至少 存在 2 栋颜色不同的房子

解题思路

看到数据量,就想到使用朴素的解法,枚举两栋房子所有的可能选择,计算所有颜色不同的房子的距离,找出两栋颜色不同房子之间的最大距离。

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
cpp复制代码class Solution {
public:
int maxDistance(vector<int>& colors) {

int res(0);
for (int i = 0; i < colors.size(); ++i) {
for (int j = 0; j < i; ++j) {
if (colors[i]!=colors[j])
{
res=max(res,i-j);
break;
}
}
}
return res;

}
};

本文转载自: 掘金

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

559 N 叉树的最大深度

发表于 2021-11-21

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

  1. N 叉树的最大深度

给定一个 N 叉树,找到其最大深度。

最大深度是指从根节点到最远叶子节点的最长路径上的节点总数。

N 叉树输入按层序遍历序列化表示,每组子节点由空值分隔(请参见示例)。

示例 1:

image.png

输入:root = [1,null,3,2,4,null,5,6]

输出:3

  • 示例 2:

image.png

输入:root =
[1,null,2,3,4,5,null,null,6,7,null,8,null,9,10,null,null,11,null,12,null,13,null,null,14]

输出:5

提示:

  • 树的深度不会超过 1000 。
  • 树的节点数目位于 [0, 104] 之间。

解题思路

使用递归,每个递归函数返回的是以输入参数root为根节点的子树,所具有的最大深度。每次递归计算所有的子节点,得出子节点中的最大深度,然后加上当前节点的深度一,返回给上层调用。

代码

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
cpp复制代码/*
// Definition for a Node.
class Node {
public:
int val;
vector<Node*> children;

Node() {}

Node(int _val) {
val = _val;
}

Node(int _val, vector<Node*> _children) {
val = _val;
children = _children;
}
};
*/

class Solution {
public:
int maxDepth(Node* root) {
if (root== nullptr) return 0;
int res(0);
for (auto c:root->children)
res=max(maxDepth(c),res);
return res+1;
}
};

时间复杂度:O(n)O(n)O(n),其中 n 为 N 叉树节点的个数。每个节点在递归中只被遍历一次。

空间复杂度:O(height)O(\textit{height})O(height),其中height \textit{height}height 表示 N 叉树的高度。递归函数需要栈空间,而栈空间取决于递归的深度,因此空间复杂度等价于 N 叉树的高度。

解题思路

利用队列实现对N叉树的层序遍历,并且记录下访问到的最大的层数,就是最大深度

代码

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
cpp复制代码/*
// Definition for a Node.
class Node {
public:
int val;
vector<Node*> children;

Node() {}

Node(int _val) {
val = _val;
}

Node(int _val, vector<Node*> _children) {
val = _val;
children = _children;
}
};
*/

class Solution {
public:
int maxDepth(Node *root) {
if (root == nullptr) return 0;
int res(0);
queue<Node *> q;
q.push(root);
while (!q.empty()) {

int s = q.size();
for (int i = 0; i < s; ++i) {
Node *cur = q.front();
q.pop();
for (auto c:cur->children) {
if (c != nullptr)
q.push(c);
}
}
res++;
}
return res;
}
};

本文转载自: 掘金

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

Electron+Nodejs+SpringBoot+Soc

发表于 2021-11-21

之前写的个人项目都是博客之类的,这次打算写一个新的东西,加上本身对网络编程不太熟悉,所以就打算写一个IM软件,在此过程中,来学习一下网络编程

PS:图片的水印带有CSDN字样是因为最先在CSDN发布的,然后直接复制发到掘金上,两个都是本人,不是抄袭…

2022-01-26更新
增加了创建群聊的功能
增加群聊聊天的功能以及群里离线消息的保存
优化若干界面

在这里插入图片描述

在这里插入图片描述

2021-11-21更新
增加了发送图片的功能
增加对离线消息的保存
增加会话列表的显示
优化若干界面
在这里插入图片描述

Lchat

本次的项目取名为Lchat,没有什么具体含义。

所用到的技术

  • Electron: 因为之前写的项目都是网页版的,这次想弄点不一样的,在一次逛知乎的过程中发现了Electron,去简单了解了下,它使用 JavaScript,HTML 和 CSS 构建跨平台的桌面应用程序,总的来说就是它可以把网页变成桌面程序,还可以使用NodeJS,当时看到这个就觉得挺有意思,刚好我都会一些,桌面应用也是之前没接触过的。就选择用它来搭建IM的界面
  • VUE,Ant Design Vue:这个没啥好说的之前的项目都是用VUE来写前端
  • SpringBoot: SB搭建后台的服务
  • TCP: TCP其实不能说是技术,只是为了方便就放在这里了,Lchat使用的是TCP来进行网络通信,JSON作为传输格式
  • BIO: 这次使用的IO模型是BIO
    用的技术还有不少,比如Sqlite,MySQL,Redis,NodeJS,消息队列等就不一一列出来了
    对于IO模型的使用,我的计划是Lchat会有三个大版本的迭代,最开始的BIO也就是现在这个,然后是NIO,最后使用Netty,通过三个版本来学习网络编程,从简到难,同时顺便了解了解其他的技术

项目展示

第一版本目前还在开发中,完成了一些基础的,比如登录,添加好友,个人信息展示修改,单聊(目前仅能发送文字)等,后续功能会逐步完善

登录:
在这里插入图片描述
主界面:
在这里插入图片描述
个人信息:
在这里插入图片描述
添加好友:
在这里插入图片描述
在这里插入图片描述

单聊
在这里插入图片描述
这个项目是我一个人用业余时间做的,有很多地方还需要完善,尤其是界面,对于后端出生的我,界面的调试太痛苦了😂
代码我都放在了GITHUB上
Lchat界面
Lchat后端服务

本文转载自: 掘金

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

LeetCode 24 两两交换链表中的节点 【c++/j

发表于 2021-11-21

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

1、题目

给定一个链表,两两交换其中相邻的节点,并返回交换后的链表。

你不能只是单纯的改变节点内部的值,而是需要实际的进行节点交换。

示例 1:

1
2
ini复制代码输入:head = [1,2,3,4]
输出:[2,1,4,3]

示例 2:

1
2
ini复制代码输入:head = []
输出:[]

示例 3:

1
2
ini复制代码输入:head = [1]
输出:[1]

提示:

  • 链表中节点的数目在范围 [0, 100] 内
  • 0 <= Node.val <= 100

2、思路

(模拟) O(n)O(n)O(n)

题目给定一个链表,要求我们两两交换其中相邻的节点,并返回交换后的链表。由于可能会对头节点进行改变,因此需要建立一个虚拟头结点dummy,指向原来的头节点。


根据题意进行模拟迭代,两两交换相邻两个结点,如下图所示:


具体过程详解:

  • 1、首先定义p = dummy,a = p->next,b = a->next。
  • 2、遍历整个链表,第一步先将p的next指针指向b,即p->next = b。
  • 3、然后将a的next指向b->next,即a->next = b->next。
  • 4、最后将b的next指向a,即b->next = a。

经过上述操作以后,我们就成功交换了a,b节点,然后让p指向a节点,重复上述操作即可。

  • 5、最后返回虚拟头节点的下一个节点

时间复杂度分析: O(n)O(n)O(n),其中 nnn是链表的节点数量。

3、c++代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
c复制代码class Solution {
public:
ListNode* swapPairs(ListNode* head) {
ListNode* dummy = new ListNode(-1); //虚拟头节点
dummy->next = head;
for(auto p = dummy; p->next && p->next->next; )
{
auto a = p->next, b = a->next;
p->next = b;
a->next = b->next;
b->next = a;
p = a;
}
return dummy->next;
}
};

4、java代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码class Solution {
public ListNode swapPairs(ListNode head) {
ListNode dummy = new ListNode(0);
dummy.next = head;
for(ListNode p = dummy; p.next != null && p.next.next != null;)
{
ListNode a = p.next; //虚拟头节点
ListNode b = a.next;
p.next = b;
a.next = b.next;
b.next = a;
p = a;
}
return dummy.next;
}
}

原题链接: 24. 两两交换链表中的节点

在这里插入图片描述

本文转载自: 掘金

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

🏆【Alibaba中间件技术系列】「RocketMQ技术专题

发表于 2021-11-21

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

RocketMQ是一款分布式、队列模型的消息中间件,具有以下特点

  • 1、能够保证严格的消息顺序
  • 2、提供丰富的消息拉取模式
  • 3、高效的订阅者水平扩展能力
  • 4、实时的消息订阅机制
  • 5、亿级消息堆积能力

搭建一个双节点的RocketM

环境背景:

  • 虚拟机:vmware12
  • 操作系统:centos6.5
  • 内存:1G RAM
  • 硬盘:20G ROM

在WMWare虚拟机下实现,实现两台IP和资源的服务主机,IP分别是
192.168.1.12,192.168.1.13,分别在这两台机器的hosts文件中添加。

1
2
3
4
5
6
bash复制代码vim /etc/hosts
#rocketmq
192.168.1.12 rocketmq-nameserver1
192.168.1.12 rocketmq-master1
192.168.1.13 rocketmq-nameserver2
192.168.1.13 rocketmq-master2

下载安装RocketMQ

分别将alibaba-rocketmq-x.x.x.tar.gz使用rz命令上传到两台机器,也可以直接官网在线下载。

将RocketMq解压到/usr/local目录下:

1
perl复制代码root@localhost local]#tar -zxvf alibaba-rocketmq-x.x.x.tar.gz -C /usr/local/

建立alibaba-rocketmq到rocketmq软连接,如下:

1
perl复制代码[root@localhost local]#ln -s alibaba-rocketmq rocketmq

创建rocketmq存储的相关路径

1
2
3
4
perl复制代码[root@localhost local]# mkdir /usr/local/rocketmq/store
[root@localhost local]# mkdir /usr/local/rocketmq/store/commitlog
[root@localhost local]# mkdir /usr/local/rocketmq/store/consumequeue
[root@localhost local]# mkdir /usr/local/rocketmq/store/index

分别配置两台机器的broker-a.properties和broker-b.properties文件

调整相关RocketMQ的broker的配置

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
ini复制代码[root@localhost local]# vim /usr/local/rocketmq/conf/2m-noslave/broker-a.properties 
#所属集群名字
brokerClusterName=rocketmq-cluster
#broker名字,注意此处不同的配置文件填写的不一样
brokerName=broker-a
#0 表示 Master,>0 表示 Slave
brokerId=0
#nameServer地址,分号分割
namesrvAddr=rocketmq-nameserver1:9876;rocketmq-nameserver2:9876
# 在发送消息时,自动创建服务器不存在的topic,默认创建的队列数
defaultTopicQueueNums=4
# 是否允许 Broker 自动创建Topic,建议线下开启,线上关闭
autoCreateTopicEnable=true
# 是否允许 Broker 自动创建订阅组,建议线下开启,线上关闭
autoCreateSubscriptionGroup=true
# Broker 对外服务的监听端口
listenPort=10911
# 删除文件时间点,默认凌晨 4点
deleteWhen=04
#文件保留时间,默认 48 小时
fileReservedTime=120
#commitLog每个文件的大小默认1G
mapedFileSizeCommitLog=1073741824
#ConsumeQueue每个文件默认存30W条,根据业务情况调整
mapedFileSizeConsumeQueue=300000
#destroyMapedFileIntervalForcibly=120000
#redeleteHangedFileInterval=120000
#检测物理文件磁盘空间
diskMaxUsedSpaceRatio=88
#存储路径
storePathRootDir= /usr/local/rocketmq/store
#commitLog 存储路径
storePathCommitLog= /usr/local/rocketmq/store/commitlog
#消费队列存储路径存储路径
storePathConsumeQueue=/usr/local/rocketmq/store/consumequeue
#消息索引存储路径
storePathIndex=/usr/local/rocketmq/store/index
#checkpoint 文件存储路径
storeCheckpoint=/usr/local/rocketmq/store/checkpoint
#abort 文件存储路径
abortFile=/usr/local/rocketmq/store/abort
#限制的消息大小
maxMessageSize=65536
#flushCommitLogLeastPages=4
#flushConsumeQueueLeastPages=2
#flushCommitLogThoroughInterval=10000
#flushConsumeQueueThoroughInterval=60000
#Broker 的角色
#- ASYNC_MASTER 异步复制Master
#- SYNC_MASTER 同步双写Master
#- SLAVE
brokerRole=ASYNC_MASTER
#刷盘方式
#- ASYNC_FLUSH 异步刷盘
#- SYNC_FLUSH 同步刷盘
flushDiskType=ASYNC_FLUSH
#checkTransactionMessageEnable=false
#发消息线程池数量
#sendMessageThreadPoolNums=128
#拉消息线程池数量
#pullMessageThreadPoolNums=128

启动nameserver

1
2
java复制代码[root@singlenode rocketmq]# cd /usr/local/rocketmq/bin/
[root@singlenode bin]# nohup sh mqnamesrv &

启动broker

1
2
3
4
java复制代码cd /usr/local/rocketmq/bin
[root@singlenode bin]# nohup sh mqbroker -c /usr/local/rocketmq/conf/2m-noslave/broker-a.properties >/dev/null 2>&1 &

[root@singlenode bin]# tail -f -n 500 /usr/local/rocketmq/logs/rocketmqlogs/broker.log

部署RocketMq管理界面

首先下载rocketmq-console.war和tomcat(地址github.com/apache/rock…

修改config.properties文件

1
2
ini复制代码rocketmq.namesrv.addr=192.168.1.12:9876;192.168.1.13:9876
throwDone=true

启动Tomcat,访问
http://192.168.1.128:8080/rocketmq-console

设定环境变量:

export NAMESRV_ADDR=192.168.169.128:9876\;192.168.169.129:9876

运行测试:

1
2
复制代码bash tools.sh com.alibaba.rocketmq.example.quickstart.Producer
bash tools.sh com.alibaba.rocketmq.example.quickstart.Consumer

关闭防火墙

1
2
csharp复制代码[root@singlenode bin]#service iptables status
[root@singlenode bin]#service iptables stop

如果是centos7以上使用

1
arduino复制代码systemctl stop firewalld.service

RocketMq的角色

  • producer
  • consumer
  • Broker
  • NameServer
创建topic
  • b broker地址
  • c Cluster名称
  • n nameserver地址列表
  • t topic名称
1
css复制代码updateTopic -b 192.168.0.1:10911 -c RocketMq-Cluster -n 192.168.0.1:9876;192.168.0.2:9876 -t order-topic
删除topic
1
css复制代码deleteTopic -c RocketMq-Cluster -n 192.168.0.1:9876;192.168.0.2:9876 -t order-topic
创建/修改订阅组

订阅组名称

1
css复制代码updateSubGroup -b 192.168.0.1:10911 -c RocketMq-Cluster -g subGroupName -n 192.168.0.1:9876;192.168.0.2:9876
删除订阅组
1
css复制代码deleteSubGroup -b 192.168.0.1:10911 -c RocketMq-Cluster -g subGroupName -n 192.168.0.1:9876;192.168.0.2:9876

更新broker配置

某些配置文件broker运行的时候可以动态修改,-k broker配置文件的key -v value

1
css复制代码updateBrokerConfig -b 192.168.0.1:10911 -c RocketMq-Cluster -n  192.168.0.1:9876;192.168.0.2:9876 -k deleteWhen -v 05

更新topic的读写权限

1
css复制代码updateTopicPerm -b 192.168.0.1:10911 -c RocketMq-Cluster -n  192.168.0.1:9876;192.168.0.2:9876 -t order-topic

查询topic路由信息

1
css复制代码TopicRoute -b 192.168.0.1:10911 -c RocketMq-Cluster -n  192.168.0.1:9876;192.168.0.2:9876 -t order-topic

查看topic路由信息

1
css复制代码TopicList -n  192.168.0.1:9876;192.168.0.2:9876

查看topic状态统计信息

1
css复制代码TopicStats -t order_topic -n  192.168.0.1:9876;192.168.0.2:9876

根据时间查询消息

1
css复制代码printMsg -t order_topic -n 192.168.0.1:9876;192.168.0.2:9876

根据Id查询消息

1
css复制代码queryMsgById -i msgId -n -n 192.168.0.1:9876;192.168.0.2:9876

查看集群信息

1
css复制代码clusterList -n 192.168.0.1:9876;192.168.0.2:9876

额外说明

如果没有安装中文语言包,出现乱码了,可以通过

1
perl复制代码[root@localhost local]# yum groupinstall chinese-support

可以修改两台机器的日志配置文件并且把conf目录下所有xml文件中的${user.home}替换为/usr/local/rocketmq

1
2
sql复制代码[root@localhost rocketmq]# mkdir -p /usr/local/rocketmq/logs
[root@localhost rocketmq]# cd /usr/local/rocketmq/conf && sed -i 's#${user.home}#/usr/local/rocketmq#g' *.xml

分别修改两台机器的rocketmq启动脚本

1
2
3
4
5
6
7
8
java复制代码[root@localhost rocketmq]# vim /usr/local/rocketmq/bin/runbroker.sh 
JAVA_OPT="${JAVA_OPT} -server -Xms1g -Xmx1g -Xmn800g -XX:PermSize=128m -XX:MaxPermSize=320m"
JAVA_OPT="${JAVA_OPT} -XX:+UseConcMarkSweepGC -XX:+UseCMSCompactAtFullCollection -XX:CMSInitiatingOccupancyFraction=70 -XX:+CMSParallelRemarkEnabled -XX:SoftRefLRUPolicyMSPerMB=0 -XX:+CMSClassUnloadingEnabled -XX:SurvivorRatio=8 -XX:+DisableExplicitGC"
JAVA_OPT="${JAVA_OPT} -verbose:gc -Xloggc:${HOME}/rmq_bk_gc.log -XX:+PrintGCDetails -XX:+PrintGCDateStamps"
JAVA_OPT="${JAVA_OPT} -XX:-OmitStackTraceInFastThrow"
JAVA_OPT="${JAVA_OPT} -Djava.ext.dirs=${BASE_DIR}/lib"
#JAVA_OPT="${JAVA_OPT} -Xdebug -Xrunjdwp:transport=dt_socket,address=9555,server=y,suspend=n"
JAVA_OPT="${JAVA_OPT} -cp ${CLASSPATH}"

本文转载自: 掘金

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

1…250251252…956

开发者博客

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