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

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


  • 首页

  • 归档

  • 搜索

AWS Lambda无服务开发入门——手把手教你创建一个无服

发表于 2019-07-14

先决条件

  1. 创建AWS s3桶
    参考:docs.aws.amazon.com/zh_cn/Amazo…
  2. 创建Kinesis Firehose流
    参考:docs.aws.amazon.com/zh_cn/fireh…

实现功能

1、使用AWS API Gataway + AWS lambda获取访问者的Public IP ,并把它插入请求内容中,通过AWS Kinesis Firehose再传入AWS S3中

2、携带密钥对部署的该api进行访问

一、使用控制台创建 Lambda 函数

  1. 打开 AWS Lambda 控制台。

*进入控制台后请注意又上交的region,请选择选择与你业务对应的区域
2. 选择创建函数(Create a function),进入函数创建页面


3. 在函数名称中,输入 my-function。
4. 选择运行语言,语言支持请参考官方文档,本实例采用Noda.js作为运行环境
5. 选择 Create function。函数创建成功,进入lambda在线编译环境


6. 把index.js中的代码替换成下面的代码(本例子中采用node.js 为了让没有接触过node.js的小伙伴能看懂,重要的语句都会添加注释)

/**``* 导入aws开发模块``*/``const AWS = require(``'aws-sdk'``); /**``* 为aws模块配置认证信息``*/``AWS.config.update({``accessKeyId: 'AKI***********PY'``,``secretAccessKey: 'xle****************cIoq'``,``region: 'ap-southeast-1'``}); /**``* lambda核心模块,处理api请求的信息,如获取请求body,就在此模块中``* 请求信息通过event传递,如果想要拿到例如public ip这类请求附加信息,需要在AWS API Gateway中做映射,下面会将``*/``exports.handler = async (event) => {``let firehose = new AWS.Firehose(); let ip = event.context.sourceip; //public ip在API Gateway中被映射成了context.sourceip``if``(event.bodyjson){``event.bodyjson.publicip = ip; //请求体在API Gateway中被映射成了名字为bodyjson的json``} /**``* 为firehose设置参数``*/``let params = {``DeliveryStreamName: 'delay-collection-a'``,``Record: { /* required */``Data: JSON.stringify(event.bodyjson) }``}; /**``* 向firehose发送数据``*/``firehose.putRecord(params, function(err, data) {``if (err) console.log(err, err.stack); // an error occurred``else console.log(data); // successful response``}); /**``* 此处是为了测试,能把最终传递给firehose的数据返回给请求者``*/``return event.bodyjson;``};
7. 点击保存

二、使用控制台创建AWS API Gateway

  1. 打开 AWS API Gateway 控制台。
  2. 选择开始使用,进入API创建页面
  3. 在选择协议中,选择REST
  4. 在新建API中,选择新建API
  5. 在设置中输入API名称My-API
  6. 选择创建API,API创建成功,进入API管理界面

三、使用控制台编辑My-API

  1. 选中AWS Gateway API管理界面中已经创建好的My-API
  2. 为My-API创建资源,选择操作中的创建资源,进入资源创建页面
  3. 在资源名中输入my-resource,选择创建资源,资源创建成功
  4. 在操作中为新创建的资源my-resource创建方法
  5. 在方法下拉框中选择POST方法
  6. 选中POST方法后,勾选旁边的勾选按钮,进入my-resource - POST - 设置 页面
  7. 在集成环境中选择lambda函数
  8. 在lambda区域中选择第一步创建lambda中所选的region
  9. 在lambda函数中输任何字母,下拉框会出现你创建好的函数my-funciton,选它!选它!选它!
  10. 点击保存按钮,弹出权限确认框,点击确定,稍等片刻进入方法执行界面
  11. 在方法执行界面,选择集成请求
  12. 在集成请求页面中选择映射模板(用户请求体的信息,如请求头和publicIP就是他通过这儿获取),点击添加映射模板,并在输入框中输入application/json
  13. 在点击勾选按钮保存后弹出更改传递行为确认框,点击是,锁定此集成
  14. 点击后生成模板输入框弹出模板输入框
  15. 输入以下模板,并点击保存按钮
## See http:``//docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-mapping-template-reference.html``## This template will pass through all parameters including path, querystring, header, stage variables, and context through to the integration endpoint via the body/payload``#set($allParams = $input.params())``{``"bodyjson" : $input.json(``'$'``),``"params" : {``#foreach($type in $allParams.keySet())``#set($params = $allParams.get($type))``"$type" : {``#foreach($paramName in $params.keySet())``"$paramName" : "$util.escapeJavaScript($params.get($paramName))"``#``if``($foreach.hasNext),#end``#end``}``#``if``($foreach.hasNext),#end``#end``},``"stage-variables" : {``#foreach($key in $stageVariables.keySet())``"$key" : "$util.escapeJavaScript($stageVariables.get($key))"``#``if``($foreach.hasNext),#end``#end``},``"context" : {``"account-id" : "$context.identity.accountId"``,``"api-id" : "$context.apiId"``,``"api-key" : "$context.identity.apiKey"``,``"authorizer-principal-id" : "$context.authorizer.principalId"``,``"caller" : "$context.identity.caller"``,``"cognito-authentication-provider" : "$context.identity.cognitoAuthenticationProvider"``,``"cognito-authentication-type" : "$context.identity.cognitoAuthenticationType"``,``"cognito-identity-id" : "$context.identity.cognitoIdentityId"``,``"cognito-identity-pool-id" : "$context.identity.cognitoIdentityPoolId"``,``"http-method" : "$context.httpMethod"``,``"stage" : "$context.stage"``,``"sourceip" : "$context.identity.sourceIp"``,``"user" : "$context.identity.user"``,``"user-agent" : "$context.identity.userAgent"``,``"user-arn" : "$context.identity.userArn"``,``"request-id" : "$context.requestId"``,``"resource-id" : "$context.resourceId"``,``"resource-path" : "$context.resourcePath"``}``}

*其中获取请求体的语句为:”bodyjson” : $input.json(‘$’),

*获取public ip的语句为:”sourceip” : “$context.identity.sourceIp”,

四、使用控制台测试API

  1. 回到方法执行界面,点击测试按钮,尽然测试页面
  2. 在请求正文中输入以下json内容,点击测试
{``"test1"``:``"abcdef"``,``"test2"``:``"abcdef"``}
3. 请求成功后,将返回200,并且再响应参数中多了publicip(因为使用的是aws控制台,无法获取到准确的publicip ,需要本地进行调用才能获取),证明api,编写成功

五、使用控制台部署API

  1. 在API Gateway控制台中选中API MY-API中的资源/my-resource,并点击post,进入方法执行界面
  2. 在方法执行界面点击方法请求,尽然请求设置界面,并选中需要API键 ,设置为是
  3. 点击操作按钮,在下拉框中选中部署api
  4. 在弹出的部署api提示框,中,选中部署阶段下拉框,选择新阶段,在阶段名称中输入test
  5. 点击保存按钮后,跳转阶段界面,部署成功,其中调用 URL: pzoszxq868.execute-api.us-east-1.amazonaws.com/test 就是我们发布出去的api,/my-resource,就是我们调用的方法
  6. 因为我们为该api设置了访问需要密钥,所以接下来,为该api配置密钥

六、使用控制台为API配置密钥

  1. 在AWS API Gateway控制台中,选中API键,选中操作,在下拉框中选择创建API键
  2. 在姓名输入框中输入:taoli , API密钥选择自动生成,d点击保存按钮,api 键生成成功
  3. 在生成的API密钥管理界面中可以查看以生成的密钥,选中API密钥,点击显示,密钥将会出现
  4. 选中使用计划,并点击创建按钮创建一个使用计划,点击按钮后将尽然使用计划创建页面
  5. 在名称中填入my-test ,并根据实际需求填写限制和配额的数量,点击下一步
  6. 选中添加API存储,在API下拉框中选中My-API ,在阶段下拉框中选中test,勾选按钮后,点击下一步
  7. 选中向使用计划中订阅一个API键,然后再名称中输入t,会弹出已经创建好的api key taoli,选中它 ,勾选按钮后,点击完成
  8. 至此,所有api开发工作完成,下面我们将使用postman来测是我们所发布的api

七、使用Postman来测试部署的api

  1. 使用post方法对URL pzoszxq868.execute-api.us-east-1.amazonaws.com/test/my-resource做请求
  2. 在请求之前,在request headers中加入以下访问密钥,同时加入传输类型
    x-api-key : d2M16C6EXG6t8cR9buOuPBqHesLQwER7nhelyZk4
    Content-Type: application/json
  3. 点击send,请求成功,并返回结果
  4. 通过查询s3数据,数据正常进入s3

八、参考资料

  1. AWS Lambda
  2. Amazon API Gateway

本文转载自: 掘金

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

对接一个第三方服务接口要考虑什么?(一)

发表于 2019-07-14

通讯标准

  1. 确定协议 Http/Https
  2. 确定参数传输格式 json/x-www-form-urlencoded/…
  3. 具体接口参数的数据类型(弱类型对接强类型语言时容易踩坑)
  4. 确定身份校验方式
    • token
    • 参数签名:单一Key、交换公钥
    • 证书校验
  5. 确定响应结构(统一成功响应码等等)

异常处理

一般情况下,异常结构应该区分为三大类:

  1. RpcException:调用异常
  2. BusinessException:业务异常
  3. Exception:其他程序异常

对于不同的第三方服务,什么情况下对应什么异常,可能会有不同的划分标准,一般情况下有以下规则:

  1. Http StatusCode == 500 时为RpcException
  2. 业务响应码 != 统一成功响应码 时为BusinessException
  3. 其他异常不作归类

一致性

对于涉及到支付、退款等有下单概念的接口或涉及到状态问题时,则需要考虑到一致性的问题。一般情况下有以下要求(第三方服务也叫上游):

  1. 上游有的数据,本地一定要有
  2. 上游状态与本地每条数据的状态相同 (或者状态可以相对来说一一映射)

由于第三方服务一般不受控,这里说的一致性往往只能是最终一致性

事务发起

  1. 确定接口当中的唯一标识是哪个字段,通常是requestNo,这个字段的值将上游数据和本地数据进行一一对应,上游存在的requestNo,本地必须存在
  2. 划分状态:
  • PENDING:本地数据已创建,未发起接口请求
  • UNCONFIRMED:本地数据已创建,不知道接口请求发起了没有,等待回查
  • PROCESSING:接口请求已发起,并且上游已响应,等待回查确认最终状态
  • SUCCESS:终态,业务已成功
  • FAIL:终态,业务已失败
  • DEAD:终态,本地数据已创建,接口死活请求不了,上游也查不到对应数据,不要了,根据实际情况也可以归类为FAIL

这里的流程可以概述为:

  1. 发起上游接口前,生成一个全局唯一的requestNo,该条数据的状态为PENDING,并且入库提交。
  2. 请求上游接口没有异常的情况下:
    1. 如果允许的话,同步处理状态,更新状态入库。
    2. 否则,直接更新为PROCESSING,表示请求上游已成功,等待进一步确认状态。
  3. 请求上游接口遇到RpcException,更新为PROCESSING,表示请求上游已成功,等待进一步确认状态。
  4. 请求上游接口遇到BusinessException,更新为FAIL,表示请求上游已成功,等待进一步确认状态。(这里可能根据不同的业务返回码,处理为PROCESSING,等待进一步确认)
  5. 处理过程中遇到其他Exception,更新为UNCONFIRMED,不能确定是否已请求上游,等待进一步确认状态,这里可以概述为本地事务处理失败,即保存到本地数据库时失败。

如果你对上游的信任度较低,可以直接将PROCESSING状态也合并为UNCONFIRMED通一由事务回查处理

下面用一段伪代码来描述接口调用的流程:

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
复制代码// 开启本地事务
startTrans();
Order order = new Order();
// 唯一请求号
String requestNo = UUID();
order.setRequestNo(requestNo);
order.setState(OrderState.PENDING);
order.save();
// 提交本地事务
commit();

try {
startTrans();
RpcResponse rpcRes = rpcService.requestToRpcCreateOrder(...);
order.setState(OrderState.PROCESSING);

// 如果你的接口可以同步返回业务状态
if(rpcRes.getState() == 'SUCCESS') {
order.setState(OrderState.SUCCESS);
}
if(rpcRes.getState() == 'FAIL') {
order.setState(OrderState.FAIL);
}

order.save();
commit();
} catch(RpcException e) {
startTrans();
// 认为是处理中,等待后续回查
order.setState(OrderState.PROCESSING);
order.save();
commit();
} catch(BusinessException e) {
startTrans();
// 认为是失败
order.setState(OrderState.FAIL);
// 失败时,建议记录rpc响应参数
order.setRpcResponseCode(e.getCode());
order.setRpcResponseMsg(e.getMsg());
order.save();
commit();
} catch(Exception e) {
startTrans();
// 认为是待确认,等待后续回查
order.setState(OrderState.UNCONFIRMED);
order.save();
commit();
}

事务回查(重试)

经过上面的流程,数据会剩下UNCONFIRMED 和 PROCESSING 两种状态,因此对这两种状态进行进一步确认,保证数据到达终态。

事务回查有几种实现方式:

  1. 利用定时器扫描数据库状态为UNCONFIRMED或PROCESSING的数据
    • 保证数据库有索引
    • 如果requestNo字段也有索引,则可利用覆盖索引机制缩短查询时间,查询上游数据状态一般只需要requestNo
  2. 把UNCONFIRMED或PROCESSING数据的requestNo存入Redis,再利用定时器处理
  3. 利用队列,将UNCONFIRMED 和 PROCESSING塞在回查队列中

实际上,假如你的rpc请求不需同步返回出去,推荐使用具有事务机制的消息队列,否则利用队列方案需要考虑复杂度的上升程度

那么UNCONFIRMED 和 PROCESSING分别怎么处理呢

  • 对应PROCESSING,处理思路很简单,因为这种状态上游肯定能够返回对应的状态(实际上有的上游并不一定),只要查询到对应状态更新为SUCCESS或FAIL即可
  • 对应UNCONFIRMED需要区分上游数据不存在的情况,也就是说上面的事务发起流程当中,上游没有收到我们的请求,那么我们需要根据业务情况进行处理:
+ 重新发起这个请求(要确保上游接口是否幂等,否则要自己处理)
+ 更新为DEAD,抛弃这个请求如果上游存在该记录,则视为PROCESSING情况处理即可

如果你的上游提供异步处理通知,则可按照同样的思路完成事务回查这个阶段

总结与思考

本文简单总结了一下对接第三方服务接口时需要考虑的几个问题:通讯标准、异常处理、一致性,实际处理时通常会分为RPC层与Service层来处理,RPC封装通讯标准、异常处理的问题,Service层处理一致性问题。最后留下了一个问题还未进行讨论,在入库前、重新发起请求前、异步通知时都需要考虑幂等的问题,下一篇文章针对幂等再来分享几种处理方案吧。

本文转载自: 掘金

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

图解JVM垃圾回收

发表于 2019-07-12

一、垃圾收集器所属分代总览

1、各代采用的收集器

有连线的表示可搭配使用

HotSpot虚拟机各代对应的垃圾收集器

2、垃圾回收算法

2.1、标记-清除过程

第一阶段:标记出可回收对象
第二阶段:回收被标记的对象
缺点:产生大量的内存碎片

2.2、复制回收过程

内存被分为相等的两块(S0+S1),S0为使用内存,S1为保留内存,回收时将S0的存活对象一次性全部复制到S1,再一次性清空S0内存
缺点:内存使用率只有50%

2.3、标记整理过程

第一阶段:标记出存活对象
第二阶段:存活对象往一端移动,清理边界外内存

3、各收集器采用的收集算法

业界商用的JVM收集算法都是采用分代收集,按不同代的特点采用不同的收集算法。
通常情况,年轻代采用复制算法,年老代采用标记-整理或标记-清除算法。

G1整体上看基于 "标记-整理" 算法,而两个Region之间则基于 "复制" 算法

二、各垃圾收集器详解

1、Serial 和Serial Old组合收集

单线程串行收集器,垃圾收集时会“Stop The World”

Serial 和Serial Old组合收集示意图

2、ParNew 和Serial Old组合收集

ParNew收集器就是Serial收集器的多线程版本,收集算法和回收策略和Serial完全一样,也会Stop The World。
注意:ParNew是并行收集器,不是并发收集器

ParNew 和Serial Old组合收集示意图

3、Parallel Scavenge收集器

跟ParNew类似,是一种吞吐量优先收集器,即目标是达到一个可控制的吞吐量

1
复制代码吞吐量 = 运行用户代码时间 / (运行用户代码时间) + 垃圾收集时间

4、Parallel Scavenge和Parallel Old组合收集

Parallel Old是Parallel Scavenge收集器的老年代版本

Parallel Scavenge和Parallel Old组合收集示意图

5、CMS收集器

CMS收集器是一种并发收集器,目标时已达到最小回收停顿时间,采用标记清除算法。

CMS收集器示意图

6、G1收集器

G1内存布局中,保留了年轻代年老代的概念,但不是物理隔离,而是很多个大小相同的独立区域(Region),年轻代和年老代都是一部分Region(不需要连续)的集合
初始标记:标记GC Roots能直接关联到的对象
并发标记:从GC Roots中对堆对象进行可达性分析,找出存活对象
最终标记:修改并发期间变动的标记记录
筛选回收:根据用户指定的停顿时间制定回收计划

G1收集器示意图

G1内存布局示意图

三、存活对象的判断

不管是采用什么样的垃圾回收算法,都需要判断对象的存活状态,通常会采用两种方式判断

1、引用计数法

对象中增加一个引用计数器,每当有地方引用该对象时,其计数器+1,引用失效后计数器-1,当计数器为0的对象,即认为是可回收的对象,但是这种方式无法解决循环引用的问题,所以目前主流的垃圾回收器都没有采用这种方式。

2、可达性分析算法

通过GC Roots集合中的对象开始搜索,当某对象不能通过GCRoots到达时,判定为对象可回收。
对象1、2、3、4不可回收,对象5、6、7可回收

GCRoots.png

附一张之前画过的JVM内存模型

内存模型.jpeg

本文转载自: 掘金

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

深入理解SpringBoot核心原理(一)--------启

发表于 2019-07-10

一、前言

  使用过springboot的同学应该已经知道,springboot通过默认配置了很多框架的使用方式帮我们大大简化了项目初始搭建以及开发过程。本文的目的就是一步步分析springboot的启动过程,这次主要是分析springboot特性自动装配。

  那么首先带领大家回顾一下以往我们的web项目是如何搭建的,通常我们要搭建一个基于Spring的Web应用,我们需要做以下一些工作:

  1.pom文件中引入相关jar包,包括spring、springmvc、redis、mybaits、log4j、mysql-connector-java 等等相关jar …

  2.配置web.xml,Listener配置、Filter配置、Servlet配置、log4j配置、error配置 …

  3.配置数据库连接、配置spring事务

  4.配置视图解析器

  5.开启注解、自动扫描功能
  6.配置完成后部署tomcat、启动调试

  ……

  花在搭建一个初始项目,可能一个小时就过去了或者半天救过了,但是用了SpringBoot之后一切都会变得非常便捷,下面我们首先来分析一下SpringBoot的起步依赖以及自动配置。

二、起步依赖

  1.在我们的pom文件里面引入以下jar:

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
复制代码   <modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.4.RELEASE</version>
<relativePath /> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>demo</name>
<description>Demo project for Spring Boot</description>

<properties>
<java.version>1.8</java.version>
</properties>

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>

<!--mybatis 开发包 -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.3.2</version>
</dependency>
<!--springboot web模块支持 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>

spring-boot-starter-web包自动帮我们引入了web模块开发需要的相关jar包。

mybatis-spring-boot-starter帮我们引入了dao开发相关的jar包。

spring-boot-starter-xxx是官方提供的starter,xxx-spring-boot-starter是第三方提供的starter。

截图看一下我们的mybatis-spring-boot-starter

avatar

可以看出mybatis-spring-boot-starter并没有任何源码,只有一个pom文件,它的作用就是帮我们引入其它jar。
2.配置数据源

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
复制代码spring:
datasource:
type: com.zaxxer.hikari.HikariDataSource
url: jdbc:mysql://127.0.0.1:3306/mybatis_test
username: root
password: root
driver-class-name: com.mysql.jdbc.Driver
hikari:
# 最小空闲连接数量
minimum-idle: 5
# 连接池最大连接数,默认是10
maximum-pool-size: 60
# 此属性控制从池返回的连接的默认自动提交行为,默认值:true
auto-commit: true
# 一个连接idle状态的最大时长(毫秒),超时则被释放(retired),缺省:10分钟
idle-timeout: 600000
# 此属性控制池中连接的最长生命周期,值0表示无限生命周期,默认1800000即30分钟
max-lifetime: 1800000
# 数据库连接超时时间,默认30秒,即30000
connection-timeout: 60000

  stater机制帮我们完成了项目起步所需要的的相关jar包。那问题又来了,传统的spring应用中不是要在application.xml中配置很多bean的吗,比如dataSource的配置,transactionManager的配置 … springboot是如何帮我们完成这些bean的配置的?下面我们来分析这个过程

三、自动配置————

1.基于java代码的bean配置

以mybatis为例,在上面的截图中,我们发现mybatis-spring-boot-starter这个包帮我们引入了mybatis-spring-boot-autoconfigure这个包,如下图:

avatar

里面有MybatisAutoConfiguration这个类,打开这个类看看有些什么东西。
avatar

熟悉@Configuration&、@Bean这两个bean的同学或许已经知道了。这两个注解一起使用就可以创建一个基于java代码的配置类,可以用来替代相应的xml配置文件。

@Configuration注解的类可以看作是能生产让Spring IoC容器管理的Bean实例的工厂。

@Bean注解告诉Spring,一个带有@Bean的注解方法将返回一个对象,该对象应该被注册到spring容器中。

所以上面的MybatisAutoConfiguration这个类,自动帮我们生成了SqlSessionFactory这些Mybatis的重要实例并交给spring容器管理,从而完成bean的自动注册。

2.自动配置条件依赖

从MybatisAutoConfiguration这个类中使用的注解可以看出,要完成自动配置是有依赖条件的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
复制代码@org.springframework.context.annotation.Configuration
@ConditionalOnClass({ SqlSessionFactory.class, SqlSessionFactoryBean.class })
@ConditionalOnBean(DataSource.class)
@EnableConfigurationProperties(MybatisProperties.class)
@AutoConfigureAfter(DataSourceAutoConfiguration.class)
public class MybatisAutoConfiguration {

private static final Logger logger = LoggerFactory.getLogger(MybatisAutoConfiguration.class);

private final MybatisProperties properties;

private final Interceptor[] interceptors;

private final ResourceLoader resourceLoader;

private final DatabaseIdProvider databaseIdProvider;

private final List<ConfigurationCustomizer> configurationCustomizers;
......

首先预习一下Springboot是常用的条件依赖注解有:

@ConditionalOnBean,仅在当前上下文中存在某个bean时,才会实例化这个Bean。

@ConditionalOnClass,某个class位于类路径上,才会实例化这个Bean。

@ConditionalOnExpression,当表达式为true的时候,才会实例化这个Bean。

@ConditionalOnMissingBean,仅在当前上下文中不存在某个bean时,才会实例化这个Bean。

@ConditionalOnMissingClass,某个class在类路径上不存在的时候,才会实例化这个Bean。

@ConditionalOnNotWebApplication,不是web应用时才会实例化这个Bean。

@AutoConfigureAfter,在某个bean完成自动配置后实例化这个bean。

@AutoConfigureBefore,在某个bean完成自动配置前实例化这个bean。

所以要完成Mybatis的自动配置,需要在类路径中存在SqlSessionFactory.class、SqlSessionFactoryBean.class这两个类,需要存在DataSource这个bean且这个bean完成自动注册。

进入DataSourceAutoConfiguration这个类,可以看到这个类属于这个包:
org.springframework.boot.autoconfigure.jdbc
这个包又属于spring-boot-autoconfigure-2.0.4.RELEASE.jar这个包,自动配置这个包帮们引入了jdbc、kafka、logging、mail、mongo等包。很多包需要我们引入相应jar后自动配置才生效。

avatar

3.Bean参数的获取

到此我们已经知道了bean的配置过程,但是还没有看到springboot是如何读取yml或者properites配置文件的的属性来创建数据源的?

在DataSourceAutoConfiguration类里面,我们注意到使用了EnableConfigurationProperties这个注解。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
复制代码@Configuration
@ConditionalOnClass({ DataSource.class, EmbeddedDatabaseType.class })
@EnableConfigurationProperties(DataSourceProperties.class)
@Import({ DataSourcePoolMetadataProvidersConfiguration.class,
DataSourceInitializationConfiguration.class })
public class DataSourceAutoConfiguration {

@Configuration
@Conditional(EmbeddedDatabaseCondition.class)
@ConditionalOnMissingBean({ DataSource.class, XADataSource.class })
@Import(EmbeddedDataSourceConfiguration.class)
protected static class EmbeddedDatabaseConfiguration {

}
......

DataSourceProperties中封装了数据源的各个属性,且使用了注解ConfigurationProperties指定了配置文件的前缀。

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
复制代码@ConfigurationProperties(prefix = "spring.datasource")
public class DataSourceProperties implements BeanClassLoaderAware, InitializingBean {

private ClassLoader classLoader;

/**
* Name of the datasource. Default to "testdb" when using an embedded database.
*/
private String name;

/**
* Whether to generate a random datasource name.
*/
private boolean generateUniqueName;

/**
* Fully qualified name of the connection pool implementation to use. By default, it
* is auto-detected from the classpath.
*/
private Class<? extends DataSource> type;

/**
* Fully qualified name of the JDBC driver. Auto-detected based on the URL by default.
*/
private String driverClassName;

/**
* JDBC URL of the database.
*/
private String url;

/**
* Login username of the database.
*/
private String username;

/**
* Login password of the database.
*/
private String password;

/**
* JNDI location of the datasource. Class, url, username & password are ignored when
* set.
*/
private String jndiName;

/**
* Initialize the datasource with available DDL and DML scripts.
*/
private DataSourceInitializationMode initializationMode = DataSourceInitializationMode.EMBEDDED;

/**
* Platform to use in the DDL or DML scripts (such as schema-${platform}.sql or
* data-${platform}.sql).
*/
private String platform = "all";

/**
* Schema (DDL) script resource references.
*/
private List<String> schema;

/**
* Username of the database to execute DDL scripts (if different).
*/
private String schemaUsername;

/**
* Password of the database to execute DDL scripts (if different).
*/
private String schemaPassword;

/**
* Data (DML) script resource references.
*/
private List<String> data;

......

通过以上分析,我们可以得知:

@ConfigurationProperties注解的作用是把yml或者properties配置文件转化为bean。

@EnableConfigurationProperties注解的作用是使@ConfigurationProperties注解生效。如果只配置@ConfigurationProperties注解,在spring容器中是获取不到yml或者properties配置文件转化的bean的。

通过这种方式,把yml或者properties配置参数转化为bean,这些bean又是如何被发现与加载的?

3.Bean的发现

springboot默认扫描启动类所在的包下的主类与子类的所有组件,但并没有包括依赖包的中的类,那么依赖包中的bean是如何被发现和加载的?

我们通常在启动类中加@SpringBootApplication这个注解,点进去看

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
复制代码@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(excludeFilters = {
@Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
@Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
public @interface SpringBootApplication {

/**
* Exclude specific auto-configuration classes such that they will never be applied.
* @return the classes to exclude
*/
@AliasFor(annotation = EnableAutoConfiguration.class)
Class<?>[] exclude() default {};

/**
* Exclude specific auto-configuration class names such that they will never be
* applied.
* @return the class names to exclude
* @since 1.3.0
*/
......

实际上重要的只有三个Annotation:

@Configuration(@SpringBootConfiguration里面还是应用了@Configuration)

@EnableAutoConfiguration

@ComponentScan

@Configuration的作用上面我们已经知道了,被注解的类将成为一个bean配置类。

@ComponentScan的作用就是自动扫描并加载符合条件的组件,比如@Component和@Repository等,最终将这些bean定义加载到spring容器中。

@EnableAutoConfiguration 这个注解的功能很重要,借助@Import的支持,收集和注册依赖包中相关的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
复制代码@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {

String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration";

/**
* Exclude specific auto-configuration classes such that they will never be applied.
* @return the classes to exclude
*/
Class<?>[] exclude() default {};

/**
* Exclude specific auto-configuration class names such that they will never be
* applied.
* @return the class names to exclude
* @since 1.3.0
*/
String[] excludeName() default {};

}

如上源码,@EnableAutoConfiguration注解引入了@AutoConfigurationPackage和@Import这两个注解。@AutoConfigurationPackage的作用就是自动配置的包,@Import导入需要自动配置的组件。

1
2
3
4
5
6
7
8
复制代码@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Import(AutoConfigurationPackages.Registrar.class)
public @interface AutoConfigurationPackage {

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
复制代码/**
* {@link ImportBeanDefinitionRegistrar} to store the base package from the importing
* configuration.
*/
static class Registrar implements ImportBeanDefinitionRegistrar, DeterminableImports {

@Override
public void registerBeanDefinitions(AnnotationMetadata metadata,
BeanDefinitionRegistry registry) {
register(registry, new PackageImport(metadata).getPackageName());
}

@Override
public Set<Object> determineImports(AnnotationMetadata metadata) {
return Collections.singleton(new PackageImport(metadata));
}

}

new AutoConfigurationPackages.PackageImport(metadata)).getPackageName()

new AutoConfigurationPackages.PackageImport(metadata)

这两句代码的作用就是加载启动类所在的包下的主类与子类的所有组件注册到spring容器,这就是前文所说的springboot默认扫描启动类所在的包下的主类与子类的所有组件。

那问题又来了,要搜集并注册到spring容器的那些beans来自哪里?

进入 AutoConfigurationImportSelector类,

我们可以发现SpringFactoriesLoader.loadFactoryNames方法调用loadSpringFactories方法从所有的jar包中读取META-INF/spring.factories文件信息。

下面是spring-boot-autoconfigure这个jar中spring.factories文件部分内容,其中有一个key为org.springframework.boot.autoconfigure.EnableAutoConfiguration的值定义了需要自动配置的bean,通过读取这个配置获取一组@Configuration类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
复制代码org.springframework.boot.autoconfigure.AutoConfigurationImportListener=\
org.springframework.boot.autoconfigure.condition.ConditionEvaluationReportAutoConfigurationImportListener

# Auto Configuration Import Filters
org.springframework.boot.autoconfigure.AutoConfigurationImportFilter=\
org.springframework.boot.autoconfigure.condition.OnClassCondition

# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.boot.autoconfigure.admin.SpringApplicationAdminJmxAutoConfiguration,\
org.springframework.boot.autoconfigure.aop.AopAutoConfiguration,\
org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration,\
org.springframework.boot.autoconfigure.batch.BatchAutoConfiguration,\
org.springframework.boot.autoconfigure.cache.CacheAutoConfiguration,\

每个xxxAutoConfiguration都是一个基于java的bean配置类。实际上,这些xxxAutoConfiguratio不是所有都会被加载,会根据xxxAutoConfiguration上的@ConditionalOnClass等条件判断是否加载;通过反射机制将spring.factories中@Configuration类实例化为对应的java实列。

到此我们已经知道怎么发现要自动配置的bean了,最后一步就是怎么样将这些bean加载到spring容器。

4.Bean 加载

如果要让一个普通类交给Spring容器管理,通常有以下方法:

1、使用 @Configuration与@Bean 注解

2、使用@Controller @Service @Repository @Component 注解标注该类,然后启用@ComponentScan自动扫描

3、使用@Import 方法

springboot中使用了@Import 方法

@EnableAutoConfiguration注解中使用了@Import({AutoConfigurationImportSelector.class})注解,AutoConfigurationImportSelector实现了DeferredImportSelector接口,

DeferredImportSelector接口继承了ImportSelector接口,ImportSelector接口只有一个selectImports方法。

selectImports方法返回一组bean,@EnableAutoConfiguration注解借助@Import注解将这组bean注入到spring容器中,springboot正式通过这种机制来完成bean的注入的。

四、总结

我们可以将自动配置的关键几步以及相应的注解总结如下:

1、@Configuration&与@Bean——>>>基于java代码的bean配置

2、@Conditional——–>>>>>>设置自动配置条件依赖

3、@EnableConfigurationProperties与@ConfigurationProperties->读取配置文件转换为bean。

4、@EnableAutoConfiguration、@AutoConfigurationPackage 与@Import->实现bean发现与加载。

本文转载自: 掘金

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

我竟然不再抗拒 Java 的类加载机制了

发表于 2019-07-10

很长一段时间里,我对 Java 的类加载机制都非常的抗拒,因为我觉得太难理解了。但为了成为一名优秀的 Java 工程师,我决定硬着头皮研究一下。

01、字节码

在聊 Java 类加载机制之前,需要先了解一下 Java 字节码,因为它和类加载机制息息相关。

计算机只认识 0 和 1,所以任何语言编写的程序都需要编译成机器码才能被计算机理解,然后执行,Java 也不例外。

Java 在诞生的时候喊出了一个非常牛逼的口号:“Write Once, Run Anywhere”,为了达成这个目的,Sun 公司发布了许多可以在不同平台(Windows、Linux)上运行的 Java 虚拟机(JVM)——负责载入和执行 Java 编译后的字节码。

到底 Java 字节码是什么样子,我们借助一段简单的代码来看一看。

源码如下:

1
2
3
4
5
6
7
8
9
复制代码package com.cmower.java_demo;

public class Test {

public static void main(String[] args) {
System.out.println("沉默王二");
}

}

代码编译通过后,通过 xxd Test.class 命令查看一下这个字节码文件。

1
2
3
4
5
6
7
8
复制代码xxd Test.class
00000000: cafe babe 0000 0034 0022 0700 0201 0019 .......4."......
00000010: 636f 6d2f 636d 6f77 6572 2f6a 6176 615f com/cmower/java_
00000020: 6465 6d6f 2f54 6573 7407 0004 0100 106a demo/Test......j
00000030: 6176 612f 6c61 6e67 2f4f 626a 6563 7401 ava/lang/Object.
00000040: 0006 3c69 6e69 743e 0100 0328 2956 0100 ..<init>...()V..
00000050: 0443 6f64 650a 0003 0009 0c00 0500 0601 .Code...........
00000060: 000f 4c69 6e65 4e75 6d62 6572 5461 626c ..LineNumberTabl

感觉有点懵逼,对不对?

懵就对了。

这段字节码中的 cafe babe 被称为“魔数”,是 JVM 识别 .class 文件的标志。文件格式的定制者可以自由选择魔数值(只要没用过),比如说 .png 文件的魔数是 8950 4e47。

至于其他内容嘛,可以选择忘记了。

02、类加载过程

了解了 Java 字节码后,我们来聊聊 Java 的类加载过程。

Java 的类加载过程可以分为 5 个阶段:载入、验证、准备、解析和初始化。这 5 个阶段一般是顺序发生的,但在动态绑定的情况下,解析阶段发生在初始化阶段之后。

1)Loading(载入)

JVM 在该阶段的主要目的是将字节码从不同的数据源(可能是 class 文件、也可能是 jar 包,甚至网络)转化为二进制字节流加载到内存中,并生成一个代表该类的 java.lang.Class 对象。

2)Verification(验证)

JVM 会在该阶段对二进制字节流进行校验,只有符合 JVM 字节码规范的才能被 JVM 正确执行。该阶段是保证 JVM 安全的重要屏障,下面是一些主要的检查。

  • 确保二进制字节流格式符合预期(比如说是否以 cafe bene 开头)。
  • 是否所有方法都遵守访问控制关键字的限定。
  • 方法调用的参数个数和类型是否正确。
  • 确保变量在使用之前被正确初始化了。
  • 检查变量是否被赋予恰当类型的值。

3)Preparation(准备)

JVM 会在该阶段对类变量(也称为静态变量,static 关键字修饰的)分配内存并初始化(对应数据类型的默认初始值,如 0、0L、null、false 等)。

也就是说,假如有这样一段代码:

1
2
3
复制代码public String chenmo = "沉默";
public static String wanger = "王二";
public static final String cmower = "沉默王二";

chenmo 不会被分配内存,而 wanger 会;但 wanger 的初始值不是“王二”而是 null。

需要注意的是,static final 修饰的变量被称作为常量,和类变量不同。常量一旦赋值就不会改变了,所以 cmower 在准备阶段的值为“沉默王二”而不是 null。

4)Resolution(解析)

该阶段将常量池中的符号引用转化为直接引用。

what?符号引用,直接引用?

符号引用以一组符号(任何形式的字面量,只要在使用时能够无歧义的定位到目标即可)来描述所引用的目标。

在编译时,Java 类并不知道所引用的类的实际地址,因此只能使用符号引用来代替。比如 com.Wanger 类引用了 com.Chenmo 类,编译时 Wanger 类并不知道 Chenmo 类的实际内存地址,因此只能使用符号 com.Chenmo。

直接引用通过对符号引用进行解析,找到引用的实际内存地址。

5)Initialization(初始化)

该阶段是类加载过程的最后一步。在准备阶段,类变量已经被赋过默认初始值,而在初始化阶段,类变量将被赋值为代码期望赋的值。换句话说,初始化阶段是执行类构造器方法的过程。

oh,no,上面这段话说得很抽象,不好理解,对不对,我来举个例子。

1
复制代码String cmower = new String("沉默王二");

上面这段代码使用了 new 关键字来实例化一个字符串对象,那么这时候,就会调用 String 类的构造方法对 cmower 进行实例化。

03、类加载器

聊完类加载过程,就不得不聊聊类加载器。

一般来说,Java 程序员并不需要直接同类加载器进行交互。JVM 默认的行为就已经足够满足大多数情况的需求了。不过,如果遇到了需要和类加载器进行交互的情况,而对类加载器的机制又不是很了解的话,就不得不花大量的时间去调试
ClassNotFoundException 和 NoClassDefFoundError 等异常。

对于任意一个类,都需要由它的类加载器和这个类本身一同确定其在 JVM 中的唯一性。也就是说,如果两个类的加载器不同,即使两个类来源于同一个字节码文件,那这两个类就必定不相等(比如两个类的 Class 对象不 equals)。

站在程序员的角度来看,Java 类加载器可以分为三种。

1)启动类加载器(Bootstrap Class-Loader),加载 jre/lib 包下面的 jar 文件,比如说常见的 rt.jar。

2)扩展类加载器(Extension or Ext Class-Loader),加载 jre/lib/ext 包下面的 jar 文件。

3)应用类加载器(Application or App Clas-Loader),根据程序的类路径(classpath)来加载 Java 类。

来来来,通过一段简单的代码了解下。

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

public static void main(String[] args) {
ClassLoader loader = Test.class.getClassLoader();
while (loader != null) {
System.out.println(loader.toString());
loader = loader.getParent();
}
}

}

每个 Java 类都维护着一个指向定义它的类加载器的引用,通过 类名.class.getClassLoader() 可以获取到此引用;然后通过 loader.getParent() 可以获取类加载器的上层类加载器。

这段代码的输出结果如下:

1
2
复制代码sun.misc.Launcher$AppClassLoader@73d16e93
sun.misc.Launcher$ExtClassLoader@15db9742

第一行输出为 Test 的类加载器,即应用类加载器,它是 sun.misc.Launcher$AppClassLoader 类的实例;第二行输出为扩展类加载器,是 sun.misc.Launcher$ExtClassLoader 类的实例。那启动类加载器呢?

按理说,扩展类加载器的上层类加载器是启动类加载器,但在我这个版本的 JDK 中, 扩展类加载器的 getParent() 返回 null。所以没有输出。

04、双亲委派模型

如果以上三种类加载器不能满足要求的话,程序员还可以自定义类加载器(继承 java.lang.ClassLoader 类),它们之间的层级关系如下图所示。

这种层次关系被称作为双亲委派模型:如果一个类加载器收到了加载类的请求,它会先把请求委托给上层加载器去完成,上层加载器又会委托上上层加载器,一直到最顶层的类加载器;如果上层加载器无法完成类的加载工作时,当前类加载器才会尝试自己去加载这个类。

PS:双亲委派模型突然让我联想到朱元璋同志,这个同志当上了皇帝之后连宰相都不要了,所有的事情都亲力亲为,只有自己没精力没时间做的事才交给大臣们去干。

使用双亲委派模型有一个很明显的好处,那就是 Java 类随着它的类加载器一起具备了一种带有优先级的层次关系,这对于保证 Java 程序的稳定运作很重要。

上文中曾提到,如果两个类的加载器不同,即使两个类来源于同一个字节码文件,那这两个类就必定不相等——双亲委派模型能够保证同一个类最终会被特定的类加载器加载。

05、最后

硬着头皮翻看了大量的资料,并且动手去研究以后,我发现自己竟然对 Java 类加载机制(JVM 将类的信息动态添加到内存并使用的一种机制)不那么抗拒了——真是蛮奇妙的一件事啊。

也许学习就应该是这样,只要你敢于挑战自己,就能收获知识——就像山就在那里,只要你肯攀登,就能到达山顶。

本文转载自: 掘金

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

公司倒闭 1 年了,而我当年的项目上了 GitHub 热榜

发表于 2019-07-10

公司倒闭 1 年多了,而我在公司倒闭时候做的开源项目,最近却上了 GitHub Trending,看着这个数据,真是不胜唏嘘。

缘起

2017 年 11 月份的时候,松哥所在的公司因为经营不善要关门了,关门的是深圳分公司,北京总部还在正常运转。

然后就是北京那边来人,和深圳的员工挨个谈话,谈裁员和赔偿,公司制度还算完善,都按照劳动合同法走,有的同事担心公司最后不按劳动合同法走,因此觉得先拿钱先走比价划算。我当时主要考虑到两个原因,并不着急走:

  1. 公司毕竟是香港上市公司,跑的了和尚跑不了庙,深圳关门了,北京那边还在运转,所以我不太担心公司赖账的事。
  2. 年底工作不好找,11 月拿赔偿走人,还有俩月才过年,这个时候不太容易拿到满意的 offer,很多公司年底都关闭 HC 了。

基于上面两点考虑,我当时并不急着走人,当公司说还需要有人留下来善后一直到 2018 年 1 月 31 号的时候,我就争取了下,然后就给留下来了。

留下来后并没有太多事情要做。划水划了一周,同事在楼下叫我:“老王下来聊天”,于是下楼跟他们吹吹牛,虽然吹牛,不过大多数时候还是在筹划来年找工作的事,不过我觉得这样没什么用,与其天天规划,不如来点实实在在的东西,为来年找工作积累一点筹码。

第一次尝试

心里想着手上就开始行动了,技术栈就选择当时最流行的 Spring Boot + Vue 前后端分离,业务就打算先做一个简单的博客试试水,博客的业务比较简单,做起来快,于是,V部落项目就诞生了:

V 部落

一个简单的博客后台管理,集成了博客编辑、发表、排版引入了 md 编辑器,博客的分类展示等,记得不到一周时间就弄完了,毕竟还是非常容易的。

V 部落项目发布后,我认认真真的写了一个介绍的 README,README 和我以前的开源项目一样,就是展示了一下项目的效果图,然后说了下要如何部署运行就完了。虽然自我感觉良好,但是并没有引起太多人关注。

在为数不多的几个关注中,我发现小伙伴在运行项目时候总是会遇到各种各样的问题,很多人多前后端分离的这种开发方式非常陌生,很多后端工程师甚至不懂,没听说过前端工程化,很多小伙伴在 GitHub 上提了很多非常简单的 issue,他们在部署V 部落项目时老是出错。

另一方面,由于博客项目比较简单,Vue 中很多高级功能没用上,例如状态管理,还有前后端分离时的动态权限管理,这些都没有体现出来。再加上当时才是 12 月,离过年还早着,我心想着再做一个业务复杂点的,然后把这些之前没用到的技能点都给用上。于是就有了微人事项目,这也是我们今天的主角,上了 6 月份 GitHub Trending。

微人事

微人事项目,我就吸取 V 部落的经验,没有等项目完全发布后再上传到 GitHub 上,而是边做变更新,每做完一个功能,就写一个文档,把实现的思路,代码的原理等都记录下来,然后在打一个 tag ,发布到 GitHub 上,这样,即使是一些新手,跟着文档,也能完全做出来。

这是当时的一些提交记录:

基本上每隔一两天就能完成一个新功能,然后就提交一次,这样的更新频率一直持续到 2018 年 1 月 20 之前,1 月 21 号女票从昆士兰大学访学回来,陪她在深圳玩了几天,然后把女票送回家,耽搁了好几天没更新。

到了 1 月 31 号,公司正式关门,我也就回家了,先去了女票家,在她家里呆了十多天,顺便完成了用 WebSocket 实现在线聊天的功能,提交了两个版本。

下面这两个是在女票家里提交的:

这两次提交之后,差不多就回家过年了,我家在岭上没有网,因此过年期间就没再继续做这个项目了,年后从家里到深圳的当天就拿到 offer 了,上班后就比较忙了,这个项目也就更新的慢了,没有再提交比较大的版本了,主要是一些修修补补的操作。

ALL IN

现在不是流行一个词叫做 all in ,用来形容我当时的状态再贴切不过了。

兴趣来了,谁都拦不住。那一段时间晚上经常在家里搞到一两点,第二天正常上班。记得那一年平安夜那天是周日,我早上依然按时起床去了公司,因为当天我的 V 部落项目就要收尾了,在公司里搞到晚上 9 点多,终于弄完了,第二天就可以发布了。然后收拾东西,骑着摩拜从科兴科学园那里出发,先走北环大道,然后再下到大沙河边上,沿着大沙河骑到西丽大学城,因为是平安夜,一路上都没什么行人,到家后还没吃饭,去楼下的餐厅随便吃点,店主一家人正在准备他们的平安夜饭,见有客人,抽出一个人给我弄了一碗面,然后我就独自边玩手机边吃饭,他们一家人围在旁边的桌子上吃他们的平安夜饭,这种感觉很奇怪,孤独又充实。

吃完回到宿舍,和女票视频,心理盘算着女票再过 20 多天就从昆士兰回来了,慢慢就睡着了,第二天到公司,我的 V 部落项目就正式上线了。

当你沉迷于一件事情的时候,效率非常高。

那段时间,我每天骑车上下班,一边骑着自行车,心里就在想着这个功能要怎么做更好,那个功能得怎么样实现,一路上就这样不断的规划着,到了公司,放下书包,就赶紧打开电脑挨个试验路上的各种想法,做出来了就很开心,在已经没有几个人的公司走走转转休息下,然后继续坐下 coding。

有一个周末去公司做这两个开源项目的时候,中午去吃饭,两只狗懒洋洋的躺在马路中间晒太阳,我甚至有点羡慕(下图拍摄于南山科兴科学园附近,我吃饭喜欢走远一点,多溜达溜达,不知不觉就溜达到工地了,见到这么惬意一幕,冬天的太阳,真的舒服)。

项目在 GitHub 上开源之后,有好几位小伙伴发邮件希望能在毕设中引用这个项目,记得有一位是国内高校 top20 的研究生,我当时又诧异又激动。后来也有好几个小伙伴加松哥微信表示想将这个项目作为脚手架用在公司的项目中,虽然没能亲眼见见小伙伴的项目,但是想到自己的项目帮到了这么多人,还是挺开心的。

这两个开源项目也带给我不少收获,技术上的提升+认识很多优秀的小伙伴,并且因此还出了一本书,也算是收获满满吧。

上榜

前两天有个小伙伴发消息说是看到我的项目出现 GitHub Trending 上了,我点开看了下,果然是的。

我是在 2013 年 11 月份注册的 GitHub,但是很长一段时间都没啥活动,后来也断断续续做过几个开源项目,但是都没啥动静,我总结其中一个原因可能也是因为我没好好对待项目,README 只是随便写写,读者一般很难上手项目,从微人事开始,我懂得了,你想要让别人重视你的项目,你首先得自己重视自己的项目。

这里再向大家安利一波这个开源项目微人事,这是一个使用了 Spring Boot + Vue 开发的前后端分离的人力资源管理系统,有一个非常完整的文档:

项目地址:github.com/lenve/vhr

欢迎大家点击阅读原文查看本项目。

其他

另外再给大家一个小小建议,如果你是初次接触前后端分离,可以先看 V 部落项目,这个项目不论从技术点还是业务上来说,都要比微人事简单, V 部落搞懂之后,再来学习微人事就会容易很多了。

如果你只是刚刚接触 Java,那么松哥也有一个前后端不分开源项目可以给你练手:

  • github.com/lenve/CoolM…

这个项目做的比较早,功能做的相对较全,但是介绍文档没有微人事那么详细,大家可以参考。

关注公众号【江南一点雨】,专注于 Spring Boot+微服务以及前后端分离等全栈技术,定期视频教程分享,关注后回复 Java ,领取松哥为你精心准备的 Java 干货!

本文转载自: 掘金

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

Django搭建个人博客:自动化测试

发表于 2019-07-07

测试是伴随着开发进行的,开发有多久,测试就要多久。本教程已经进行了30多章了,都是如何测试的?当然是runserver啦!每当开发新功能后,都需要运行服务器,假装自己就是用户,测试是否运行正常。

这样的人工测试优点是非常直观,你看到的和用户看到的是完全相同的。但是缺点也很明显:

  • **效率低。**在开发时可能你需要反复的修改代码、测试功能,这样重复查看几十次甚至几百次网页时会相当的让人烦躁。
  • **容易遗漏bug。**随着你的项目越来越复杂,组件之间的交互也更加复杂。修改某一个组件可能会导致另一个组件出现意想不到的bug,但是在人工测试时却很难检查出来,总不能每写几行代码就把整个网站统统检查一遍吧。过了很久之后你终于发现了这个bug,但此时你已经搞不清它来源于什么地方了。
  • **有的测试不方便进行。**比如说有个功能,限制每个用户每天发表评论不能超过10条,人工测试就显得比较麻烦,特别是需要反复调试的时候。

为了解决人工测试的种种问题,Django引入了Python标准库的单元测试模块,也就是自动化测试了:你可以写一段代码,让代码帮你测试!(程序员是最会偷懒的职业..)代码会忠实的完成测试任务,帮助你从繁重的测试工作中解脱出来。除此之外,自动化测试还有以下优点:

  • **预防错误。**当应用过于复杂时,代码的意图会变得非常不清晰,甚至你都看不懂自己写的代码,这是很常见的。而测试就好像是从内部审查代码一样,可以帮助你发现微小的错误。
  • **有利于团队协作。**良好的测试保证其他人不会不小心破坏了你的代码(也保证你不会不小心弄坏别人的..)。现在已经不是单打独斗出英雄的年代了,想要成为优秀的Django程序员,你必须擅长编写测试!

虽然学习自动化测试不会让你的博客增加一丝丝的功能,但是可以让代码更加强壮,所以我觉得很有必要拿出一章来专门讲讲。

Django官方文档的第5部分讲测试讲得非常的好,并且有中文版本。本章节就大量借鉴了官方文档,也非常非常推荐读者去拜读。

第一个测试

给我bug!

为了演示测试是如何工作的,让我们首先在文章模型中写个有bug的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
复制代码article/models.py

from django.utils import timezone

class ArticlePost(models.Model):
...

def was_created_recently(self):
# 若文章是"最近"发表的,则返回 True
diff = timezone.now() - self.created
if diff.days <= 0 and diff.seconds < 60:
return True
else:
return False

这个方法用于检测当前文章是否是最近发表的。

这个方法稍微扩展一下就会变得非常实用。比如可以将博文的发表日期显示为“刚刚”、“3分钟前”、“5小时前”等相对时间,用户体验将大有提升。

仔细看看,它是没办法正确判断“未来”的文章的:

1
2
3
4
5
6
7
8
9
10
11
复制代码>>> import datetime
>>> from django.utils import timezone
>>> from article.models import ArticlePost
>>> from django.contrib.auth.models import User

# 创建一篇"未来"的文章
>>> future_article = ArticlePost(author=User(username='user'), title='test',body='test', created=timezone.now() + datetime.timedelta(days=30))

# 是否是“最近”发表的?
>>> future_article.was_created_recently()
True

未来发生的肯定不是最近发生的,因此代码是错误的。

写个测试暴露bug

接下来就要写测试用例,将测试转为自动化。

还记得最初生成文章app时候的目录结构吗?

1
2
3
4
5
6
7
8
9
10
复制代码article
│ admin.py
│ apps.py
│ models.py
│ tests.py
│ views.py
│ __init__.py
│
└─migrations
└─ __init__.py

这个tests.py就是留给你写测试用例的地方了:

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
复制代码article/tests.py

from django.test import TestCase

import datetime
from django.utils import timezone
from article.models import ArticlePost
from django.contrib.auth.models import User


class ArticlePostModelTests(TestCase):

def test_was_created_recently_with_future_article(self):
# 若文章创建时间为未来,返回 False
author = User(username='user', password='test_password')
author.save()

future_article = ArticlePost(
author=author,
title='test',
body='test',
created=timezone.now() + datetime.timedelta(days=30)
)

self.assertIs(future_article.was_created_recently(), False)

基本就是把刚才在Shell中的测试代码抄了过来。有点不同的是末尾这个assertIs方法,了解**“断言”**的同学会对它很熟悉:它的作用是检测方法内的两个参数是否完全一致,如果不是则抛出异常,提醒你这个地方是有问题滴。

接下来运行测试:

1
复制代码(env) > python manage.py test

运行结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
复制代码Creating test database for alias 'default'...
System check identified no issues (0 silenced).
F
======================================================================
FAIL: test_was_created_recently_with_future_article (article.tests.ArticlePostModelTests)
----------------------------------------------------------------------
Traceback (most recent call last):
File "E:\django_project\my_blog\article\tests.py", line 19, in test_was_created_recently_with_future_article
self.assertIs(future_article.was_created_recently(), False)
AssertionError: True is not False

----------------------------------------------------------------------
Ran 1 test in 0.000s

FAILED (failures=1)
Destroying test database for alias 'default'...

这里面名堂就很多了:

  • 首先测试系统会在所有以tests开头的文件中寻找测试代码
  • 所有TestCase的子类都被认为是测试代码
  • 系统创建了一个特殊的数据库供测试使用,即所有测试产生的数据不会对你自己的数据库造成影响
  • 类中所有以test开头的方法会被认为是测试用例
  • 在运行测试用例时,assertIs抛出异常,因为True is not False
  • 完成测试后,自动销毁测试数据库

测试系统明确指明了错误的数量、位置和种类等信息,请读者细细品尝。

修正bug

既然通过测试找到了bug,那接下来就要把代码进行修正:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
复制代码article/models.py

from django.utils import timezone

class ArticlePost(models.Model):
...

def was_created_recently(self):
diff = timezone.now() - self.created

# if diff.days <= 0 and diff.seconds < 60:
if diff.days == 0 and diff.seconds >= 0 and diff.seconds < 60:
return True
else:
return False

重新运行测试:

1
2
3
4
5
6
7
8
9
10
复制代码(env) > python manage.py test

Creating test database for alias 'default'...
System check identified no issues (0 silenced).
.
----------------------------------------------------------------------
Ran 1 test in 0.000s

OK
Destroying test database for alias 'default'...

这次代码顺利通过了测试。

可以肯定的是,在往后的开发中,这个bug不会再出现了,因为你只需要运行一遍测试,就会立即得到警告。可以认为项目的这一小部分代码永远是安全的。

更全面的测试

既然一个测试用例就可以保证一小段代码永远安全,那我写一堆测试岂不是可以保证整个项目永远安全吗?确实如此,这个买卖绝对是不亏的。

因此我们继续再增加几个测试,全面强化代码:

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
复制代码article/tests.py

...

from django.test import TestCase

import datetime
from django.utils import timezone
from article.models import ArticlePost
from django.contrib.auth.models import User


class ArticlePostModelTests(TestCase):

def test_was_created_recently_with_future_article(self):
# 若文章创建时间为未来,返回 False
...

def test_was_created_recently_with_seconds_before_article(self):
# 若文章创建时间为 1 分钟内,返回 True
author = User(username='user1', password='test_password')
author.save()
seconds_before_article = ArticlePost(
author=author,
title='test1',
body='test1',
created=timezone.now() - datetime.timedelta(seconds=45)
)
self.assertIs(seconds_before_article.was_created_recently(), True)

def test_was_created_recently_with_hours_before_article(self):
# 若文章创建时间为几小时前,返回 False
author = User(username='user2', password='test_password')
author.save()
hours_before_article = ArticlePost(
author=author,
title='test2',
body='test2',
created=timezone.now() - datetime.timedelta(hours=3)
)
self.assertIs(hours_before_article.was_created_recently(), False)

def test_was_created_recently_with_days_before_article(self):
# 若文章创建时间为几天前,返回 False
author = User(username='user3', password='test_password')
author.save()
months_before_article = ArticlePost(
author=author,
title='test3',
body='test3',
created=timezone.now() - datetime.timedelta(days=5)
)
self.assertIs(months_before_article.was_created_recently(), False)

现在我们拥有了4个测试,来保证was_created_recently()方法对于过去、最近、未来中的4种情况都返回正确的值。你还可以继续扩展,直到你觉得完全没有任何bug藏匿的可能性为止。

在实际的开发中,有些难缠的bug会把自己伪装得非常的好,而不是像教程这样明确的知道它就在那里。有了自动化测试,无论以后你的项目怎么变化、app交互多么的复杂,只要在测试中写好的逻辑就一定是符合预期的,而你所需要做的只是运行一条测试指令而已。

虽然教程中仅使用了assertIs,但实际上Django中的断言有大概几十种之多,比如assertEqual、assertContains等,并且还在不断更新。详见Python标准断言和Django扩展断言

测试视图

上面的测试都是针对模型的。视图该怎么测试?如何通过测试系统模拟出用户的请求呢?

答案是TestCase类提供了一个供测试使用的Client来模拟用户通过请求和视图层代码的交互。

以文章详情视图的浏览量统计为例,比较容易出现的潜在bug有:

  • 增加的浏览量未能正常保存进数据库(即每次请求则浏览量+1)
  • 增加浏览量的同时,updated字段也错误的一并更新

所以有针对的写2条测试。新写一个专门测试视图的类,与前面的测试模型的类区分开:

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
复制代码article/tests.py

...
from time import sleep
from django.urls import reverse


class ArticlePostModelTests(TestCase):
...


class ArtitclePostViewTests(TestCase):

def test_increase_views(self):
# 请求详情视图时,阅读量 +1
author = User(username='user4', password='test_password')
author.save()
article = ArticlePost(
author=author,
title='test4',
body='test4',
)
article.save()
self.assertIs(article.total_views, 0)

url = reverse('article:article_detail', args=(article.id,))
response = self.client.get(url)

viewed_article = ArticlePost.objects.get(id=article.id)
self.assertIs(viewed_article.total_views, 1)

def test_increase_views_but_not_change_updated_field(self):
# 请求详情视图时,不改变 updated 字段
author = User(username='user5', password='test_password')
author.save()
article = ArticlePost(
author=author,
title='test5',
body='test5',
)
article.save()

sleep(0.5)

url = reverse('article:article_detail', args=(article.id,))
response = self.client.get(url)

viewed_article = ArticlePost.objects.get(id=article.id)
self.assertIs(viewed_article.updated - viewed_article.created < timezone.timedelta(seconds=0.1), True)

注意看代码是如何与视图层交互的:response = self.client.get(url)向视图发起请求并获得了响应,剩下的就是从数据库中取出更新后的数据,并用断言语句来判断代码是否符合预期了。

运行测试:

1
2
3
4
5
6
7
8
9
10
复制代码(env) > python manage.py test

Creating test database for alias 'default'...
System check identified no issues (0 silenced).
......
----------------------------------------------------------------------
Ran 6 tests in 0.617s

OK
Destroying test database for alias 'default'...

6条测试用例全部通过。

越多越好的测试

仅仅是app中的两个非常小的功能,就已经写了6条测试用例了,并且还可以继续扩展。除此之外,其他的每个模型、视图都可以扩展出几十甚至上百条测试,这样下去代码总量很快就要失去控制了,并且相对于业务代码来说,测试代码显得繁琐且不够优雅。

**但是没关系!**就让测试代码继续肆意增长吧。大部分情况下,你写完一个测试之后就可以忘掉它了。在你继续开发的过程中,它会一直默默无闻地为你做贡献的。最坏的情况是当你继续开发的时候,发现之前的一些测试现在看来是多余的。但是这也不是什么问题,多做些测试也不错。

深入代码测试

在前面的测试中,我们已经从模型层和视图层的角度检查了应用的输入输出,但是模板呢?虽然可以用assertInHTML、assertJSONEqual等断言大致检查模板中的某些内容,但更加近似于浏览器的检查就要使用Selenium等测试工具(毕竟Django的重点是后端而不是前端)。

Selenium不仅可以测试 Django 框架里的代码,甚至还可以检查 JavaScript代码。它假装成是一个正在和你站点进行交互的浏览器,就好像有个真人在访问网站一样。Django 提供了LiveServerTestCase来和Selenium这样的工具进行交互。

关于测试的话题这里只是开了个头,读者可以继续阅读下面的内容进一步了解:

  • Django: Writing and running tests
  • Django: Testing tools
  • Django: Advanced testing topics
  • Selenium官方文档

总结

有一帮崇尚“测试驱动”的开发者,他们开发时先写测试代码,然后才写业务代码。而普通开发者通常是先写业务代码,再写测试代码,这也是没问题的。但如果你已经写了很多业务代码了,再回头写测试确实有些无从下手,那么至少在以后写新功能时,记得加上测试。测试写得好不好,甚至比功能本身更能看出编程水平。

测试可以让代码更加强壮。项目没出bug时,皆大欢喜,有没有测试都一样;一旦出现难缠的bug,你就会无比想念一套完善的测试代码了。

博主写自己的网站时就没有对测试给与足够的重视,回想起来走了很多弯路。希望读者以前车之鉴,培养良好的编程习惯。


  • 有疑问请在杜赛的个人网站留言,我会尽快回复。
  • 或Email私信我:dusaiphoto@foxmail.com
  • 项目完整代码:Django_blog_tutorial

本文转载自: 掘金

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

CQRS之旅——旅程8(后记 经验教训)

发表于 2019-07-05

旅程8:后记:经验教训

1
2
3
ruby复制代码我们的地图有多好?我们走了多远?我们学到了什么?我们迷路了吗?

“这片土地可能对那些愿意冒险的人有益。”亨利.哈德逊

这一章总结了我们旅程中的发现。它强调了我们在这个过程中所学到的最重要的经验教训,提出了如果我们用新知识开始这段旅程,我们将以不同的方式做的一些事情,并指出了Contoso会议管理系统的一些未来道路。

你应该记住,这个总结反映的是我们的具体旅程,并非所有这些发现都适用于你自己的CQRS旅行。例如,我们的目标之一是探索如何在部署到Microsoft Azure并在利用云的可伸缩性和可靠性的应用程序中实现CQRS模式。对于我们的项目,这意味着使用消息传递来支持多个角色类型和实例之间的通信。您的项目可能不需要多个角色实例,或者没有部署到云中,因此可能不需要如此广泛地(或者根本不需要)使用消息传递。

我们希望这些发现能够被证明是有用的,特别是当您刚刚开始使用CQRS和事件源时。

我们学到了什么

本节描述了我们学到的主要经验教训。它们没有以任何特定的顺序呈现。

性能问题

在我们的旅程开始时,我们对CQRS模式的一个概念是,通过分离应用程序的读和写方面,我们可以优化每个方面的性能。CQRS社区的许多人都认同这一观点,例如:

“CQRS告诉我,我可以分别优化读和写,而且我不必总是手动的反规范化到平面表中。”

  • Kelly Sommers - CQRS顾问

这在我们的实践过程中得到了证实,当我们确实需要解决性能问题时,这种分离使我们受益匪浅。

在旅程的最后阶段,测试揭示了应用程序中的一组性能问题。当我们研究它们时,发现它们与我们实现CQRS模式的方式关系不大,而与我们使用基础设施的方式关系更大。发现这些问题的根源是困难的,由于应用程序中有如此多的活动部件,获得正确的跟踪和用于分析的正确数据是一项挑战。一旦我们确定了瓶颈,修复它们就相对容易了,这主要是因为CQRS模式使您能够清楚地分离系统的不同元素,比如读和写。尽管实现CQRS模式所导致的关注点分离会使识别问题变得更加困难,但是一旦您识别出一个问题,不仅更容易修复它,而且更容易防止它的重现。解耦的体系结构使得编写重现问题的单元测试更加简单。

我们在处理系统中的性能问题时遇到的挑战更多地是由于我们的系统是一个分布式的、基于消息的系统,而不是因为它实现了CQRS模式。

第7章,“添加弹性和优化性能”提供了关于我们处理系统中性能问题的方法的更多信息,并对我们想要进行但没有时间实现的额外更改提出了一些建议。

实现消息驱动系统远非易事

我们这个项目的基础设施是在旅程中根据需要开发它。我们没有预料(也没有预先警告)需要多少时间和精力来创建应用程序所需的健壮基础设施。我们在许多开发任务上花费的时间至少是最初计划的两倍,因为我们持续发现与基础设施相关的额外需求。特别是,我们从一开始就了解到拥有健壮的事件存储是至关重要的。我们从经验中得到的另一个关键思想是,消息总线上的所有I/O都应该是异步的。

Jana(软件架构师)发言:

虽然我们的事件存储还不是生产环境完备的,但是如果您决定实现自己的事件存储,那么当前的实现很好地指示了应该处理的问题类型。

尽管我们的应用程序并不大,但它向我们清楚地说明了end-to-end跟踪的重要性,以及帮助我们理解系统中所有消息流的工具的价值。第4章“扩展和增强订单和注册限界上下文”描述了测试在帮助我们理解系统方面的价值,并讨论了由我们的顾问之一Josh Elster创建的消息传递中间语言(messaging intermediate language, MIL)。

Gary(CQRS专家)发言:

如果我们有一个用于消息传递的标准符号,就可以帮助我们与领域专家和核心团队之外的人员沟通一些问题,这也会有所帮助。

总之,我们一路上遇到的许多问题都与CQRS模式没有特定的关系,而是与我们解决方案的分布式、消息驱动特性更相关。

Jana(软件架构师)发言:

我们发现,使用不同的Topic来传输由不同聚合发布的事件,通过这样来划分服务总线有助于实现可伸缩性。有关更多信息,请参见第7章“添加弹性和优化性能”。另外,请参阅这些博客文章:“Microsoft Azure Storage Abstractions and their Scalability Targets”和“Best Practices for Performance Improvements Using Service Bus Brokered Messaging”。

使用云带来的挑战

虽然云提供了很多好处,比如可靠的、可伸缩的、现成的服务,您只需单击几下鼠标就可以使用这些服务,但是云环境也带来了一些挑战:

  • 您可能无法在任何您想要的地方使用事务,因为云的分布式特性使得ACID(原子性、一致性、隔离性、持久性)事务在许多场景中不切实际。因此,您需要了解如何使用最终的一致性。例如,请参见第5章“准备发布V1版本”,以及第7章“添加弹性和优化性能”中减少UI延迟的部分章节。
  • 您可能需要重新检查关于如何将应用程序组织到不同层的假设。例如,参见第7章“添加弹性和优化性能”中关于进程内同步命令的讨论。
  • 您不仅必须考虑浏览器或内部环境与云之间的延迟,还必须考虑在云中运行的系统的不同部分之间的延迟。
  • 您必须考虑到瞬时错误,并了解不同的云服务可能如何实现节流。如果您的应用程序使用几个可能被节流的云服务,那么您必须协调应用程序如何处理不同服务在不同时间进行节流。

Markus(软件开发人员)发言:

我们发现,代码中只有一个总线抽象,这掩盖了这样一个事实,即有些消息是在本地进程内处理的,有些消息是在不同的角色实例中处理的。要查看这是如何实现的,请查看ICommandBus接口以及CommandBus和SynchronousCommandBusDecorator类。第七章“增加弹性和优化性能”包括了对SynchronousCommandBusDecorator类的讨论。

1
复制代码备注:我们的Visual Studio解决方案中的多个构建配置是为部分解决这个问题而设计的,也帮助人们下载和使用代码来快速入门。

CQRS是不同的

在我们的旅程开始时,有人警告我们,尽管CQRS模式看起来很简单,但实际上它要求您在考虑项目的许多方面时进行重大的转变。我们在旅途中的经历再次证明了这一点。您必须准备抛弃许多假设和预先设想的想法,在开始充分理解从模式中获得的好处之前,您可能需要先在几个限界上下文中实现CQRS模式。

这方面的一个例子是最终一致性的概念。如果您来自关系数据库背景,并且已经习惯了事务的ACID属性,那么在系统的所有级别上接受最终的一致性并理解其含义是一个很大的步骤。第5章“准备发布V1版本”和第7章“添加弹性和优化性能”都讨论了系统不同领域的最终一致性。

除了与您可能熟悉的不同之外,还没有一种正确的方法来实现CQRS模式。由于我们对模式和方法的不熟悉,我们在功能块上做了更多错误的开始,并且对所需的时间估计很差。随着我们对这种方法越来越熟悉,我们希望能够更快地确定如何在特定情况下实现模式,并提高我们估算的准确性。

Markus(软件开发人员)发言:

CQRS模式在概念上很简单,而细节才决定成败。

我们花了一些时间来理解CQRS方法及其含义的另一种情况是在限界上下文之间的集成期间。第5章“准备发布V1版本”详细讨论了团队如何处理会议管理与订单和注册上下文之间的集成问题。这部分旅程揭示了一些额外的复杂性,当您使用事件作为集成机制时,这些复杂性与限界上下文之间的耦合级别有关。我们的假设是,事件应该只包含关于聚合或限界上下文中变化的信息,但事实证明这种假设是没有帮助的,事件可以包含对一个或多个订阅者有用的附加信息,并有助于减少订阅者必须执行的工作量。

CQRS模式为如何划分系统引入了额外的思考。您不仅需要考虑如何将系统划分为层,还需要考虑如何将系统划分为限界上下文,其中一些上下文将包含CQRS模式的实现。在旅程的最后阶段,我们修改了关于层的一些假设,将一些处理从最初完成处理的工作者角色引入到web角色中。在第7章“增加弹性和优化性能”中讨论了如何在进程中发送和处理命令。应该根据领域模型将系统划分为限界上下文,每个限界上下文都有自己的领域模型和通用语言。一旦确定了限界上下文,就可以确定在哪些限界上下文中实现CQRS模式。这将影响如何以及在何处需要实现这些隔离限界上下文之间的集成。第二章“[分解领域]”介绍了我们对Contoso会议管理系统的所作的决策。

Gary(CQRS专家)发言:

单个进程(部署中的角色实例)可以承载多个限界上下文。在此场景中,您不需要为限界上下文使用服务总线来彼此通信。

实现CQRS模式比实现传统的(创建、读取、更新、删除)CRUD风格的系统更复杂。对于这个项目,第一次学习CQRS和创建分布式、异步消息传递基础设施的开销也很大。我们在此过程中的经验清楚地向我们证实了为什么CQRS模式不是顶级体系结构。您必须确保实现基于CQRS的限界上下文相关的成本是值得的,通常,您将在高竞争、高协作的领域中看到CQRS模式的好处。

Gary(CQRS专家)发言:

分析业务需求、构建有用的模型、维护模型、用代码表示它以及使用CQRS模式实现它都需要时间和金钱。如果这是您第一次实现CQRS模式,那么您还需要对基础设施元素(如消息总线和事件存储)进行开销投资。

事件源和事务日志

对于事件源和事务日志是否等同于同一件事,我们进行了一些讨论:它们都创建了所发生事情的记录,并且都允许您通过重播历史数据来重新创建系统的状态。结论是,事件的显著特征是除了记录所发生的事实之外,还能捕获意图。有关我们所说的意图的更多细节,请参阅参考指南中的第4章“深入CQRS和ES”。

涉及到领域专家的

实现CQRS模式鼓励领域专家的参与。该模式使您能够将写端上的领域和读端上的报告需求分离出来,并将它们与基础设施关注点分离开来。这种分离使领域专家更容易参与系统中他的专业知识最有价值的方面。使用领域驱动的设计概念,如限界上下文和通用语言,也有助于集中团队的注意力,并促进与领域专家的清晰沟通。

我们的验收测试证明是一种有效的方法,可以让领域专家参与进来并获取他的知识。第4章“扩展和增强订单和注册有界上下文”详细描述了这种测试方法。

Jana(软件架构师)发言:

作为一个副作用,这些验收测试还有助于我们处理伪生产版本的快速发布,因为它们使我们能够在UI级别运行一组完整的测试,以验证除单元测试和集成测试之外的系统行为。

除了帮助团队定义系统的功能需求之外,领域专家还应该参与评估一致性、可用性、持久性和成本之间的权衡。例如,领域专家应该帮助确定什么时候手动流程是可接受的,以及在系统的不同区域中需要什么级别的一致性。

Gary(CQRS专家)发言:

开发人员倾向于将所有内容都锁定到事务中,以确保完全的一致性,但有时并不值得这样做。

何时使用CQRS

现在我们已经完成了我们的旅程,我们现在可以建议您应该评估的一些标准,以确定是否应该考虑在应用程序中的一个或多个限界上下文中实现CQRS模式。您能正面回答的问题越多,就越有可能将CQRS模式应用到给定的限界上下文中,从而使您的解决方案受益:

  • 限界上下文是否实现了业务功能的一个领域,这个领域是您的市场中的一个关键区别点?
  • 限界上下文本质上是否与可能在运行时具有高争用级别的元素协作?换句话说,多个用户是否会为了访问相同的资源而竞争?
  • 限界上下文是否可能经历不断变化的业务规则?
  • 您是否已经具备了健壮的、可伸缩的消息传递和持久性基础设施?
  • 可伸缩性是这个限界上下文面临的挑战之一吗?
  • 限界上下文中的业务逻辑复杂吗?
  • 您清楚CQRS模式将给这个限界上下文带来的好处吗?

Gary(CQRS专家)发言:

这些都是经验法则,不是硬性规定。

如果我们重新开始,会有什么不同?

本节是我们反思我们的旅程的结果,以及确定了一些我们想以不同方式去做的事情和一些我们希望追求的其他机会。如果在我们掌握了现在我们所了解的CQRS和ES知识之后重来一次的话。

从消息传递和持久性的坚实基础设施开始

我们将从一个可靠的消息传递和持久性基础设施开始。我们采取的方法是从简单的先开始,并根据需要建立基础设施,这意味着我们在旅程中积累了技术债务。我们还发现,采用这种方法意味着在某些情况下,我们对基础设施的选择影响了我们实现领域的方式。

Jana(软件架构师)发言:

从旅行的角度来看,如果我们从一个坚实的基础设施开始,我们将有时间处理领域中一些更复杂的部分,比如等待列表(Wating-list)。

从一个可靠的基础设施开始也能使我们更早地开始性能测试。我们还将进一步研究其他人如何在基于CQRS的系统上进行性能测试,并在其他系统上寻找性能基准,比如Jonathan Oliver的EventStore。

我们采取这种方法的原因之一是我们从顾问那里得到的建议:“不要担心基础设施。”

更多地利用基础设施的能力

从一个坚实的基础设施开始也将允许我们更多地利用基础设施的能力。例如,当我们发布一个事件时,我们使用消息发起者的ID作为会话ID在Azure服务总线传递,但从系统处理事件的部分来看,这并不总是最好的使用会话ID的方式。

作为其中的一部分,我们还将研究基础设施如何支持其他最终一致性的特殊情况,如时间一致性、单调一致性、“read my writes”和自我一致性。

我们想探讨的另一个想法是使用基础设施来支持版本之间的迁移。我们可以考虑使用基于消息的流程或实时通信流程来协调把新版本上线,而不是针对每个版本以特定的方式处理迁移。

采用更系统的方法来实现过程管理器

我们在旅程的早期就开始实现我们的过程管理器,并且仍然在强化它,并确保它的行为在旅程的最后阶段是幂等的。同样,从为流程管理人员提供一些坚实的基础设施支持开始,使他们更有弹性,这将对我们有所帮助。但是,如果我们要重新开始,我们也会等到过程的后期再实现流程管理器,而不是直接开始。

在旅程的第一阶段,我们开始实现RegistrationProcessManager类。第3章“订单和注册限界上下文”描述了初始实现。在旅程的每个后续阶段,我们都对流程管理器进行了更改。

以不同的方式划分应用程序

在项目开始时,我们会更仔细地考虑系统的分层。我们发现我们的划分的方式是把应用程序分到web角色和工作者角色中,这在第4章“扩展和增强订单和注册限界上下文“中进行了描述。但这不是最优的,在旅程的最后阶段,在第7章“增加弹性和优化性能”中,作为性能优化的一部分,我们对架构做了一些重大改变。

例如,在旅程的最后阶段,作为重新组织的一部分,我们在web应用程序中引入了同步命令处理,同时引入了已存在的异步命令处理。

以不同的方式组织开发团队

我们学习CQRS模式的方法是迭代开发、回顾、讨论,然后重构。但是,我们可以通过让几个开发人员在相同的特性上独立工作,然后比较结果,从而学到更多。这可能揭示了更广泛的解决方案和方法。

评估领域域和限界上下文是否适合使用CQRS模式

我们希望从一组更清晰的启发开始(如本章前面概述的启发),以确定特定的限界上下文是否会受益于CQRS模式。如果我们关注领域中更复杂的地方,比如等待列表(Wating-list),而不是订单、注册和支付的限界上下文,我们可能会学到更多。

性能计划

我们将在旅程的早期处理性能问题。我们尤其要:

  • 提前设定明确的性能目标。
  • 在过程中更早地运行性能测试。
  • 使用更大更实际的负载。

我们没有做任何性能测试,直到旅程的最后阶段。有关我们发现的问题以及如何解决这些问题的详细讨论,请参见第7章“添加弹性和优化性能”。

在旅程的最后阶段,我们在服务总线上引入了一些分区,以提高事件的吞吐量。此分区是基于事件的发布者完成的,因此由同一个聚合类型发布的事件将发布到同一个Topic。我们希望把当前使用一个Topic的扩展到使用多个Topic,可能会基于消息中OrderID的hash进行分区(这种方法通常称为分片)。这将为应用程序提供更大的扩展。

以不同的方式思考UI

我们认为UI与读写模型交互的方式,以及它处理最终一致性的方式都很好,并且满足了业务需求。特别是,UI检查预订是否可能成功并相应地修改其行为的方式,以及UI允许用户在等待更新读模型时继续输入数据的方式。有关当前解决方案如何工作的更多细节,请参见第7章“添加弹性和优化性能”中的“优化UI”一节。

我们想研究除非绝对需要,其他避免在UI中等待的方法,比如使用浏览器推送技术。在某些地方,当前系统中的UI仍然需要等待针对读模型的异步更新。

探索事件源的一些额外好处

我们发现在旅程的第三阶段,第5章“准备发布V1版本”中,修改订单和注册限界上下文来使用事件有助于简化这个限界上下文的实现,一部分是因为它已经使用了大量的事件。

在当前的旅程中,我们没有机会进一步探索灵活性的承诺,以及从事件源中挖掘过去事件以获得新的业务见解的能力。但是,我们确实确保系统保存了所有事件的副本(不仅仅是那些重建聚合状态所需的副本)和命令,以便在将来启用这些类型的场景。

Gary(CQRS专家)发言:

同样有趣的是,通过事件源或其他技术(如数据库事务日志或SQL Server的StreamInsight特性)来挖掘过去的事件流以获取新的业务洞察是否更容易实现?

探索关于限界上下文集成的相关问题

在我们的V3版本中,所有限界上下文都由同一个核心开发团队实现。我们希望研究在实践中,由不同开发团队实现的限界上下文与现有系统集成起来有多容易。

这是您为学习经验做出贡献的一个很好的机会:继续实现另一个限界上下文(请参阅产品backlog中的优秀用户故事),将它集成到Contoso会议管理系统中,并在旅程的另一章中描述您的经验。

本文转载自: 掘金

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

工具类算法--快排的优化(Java)

发表于 2019-07-05

快排简介

   快速排序(quick sort) 是算法题中经常遇到的工具类算法,所谓工具类算法就是存在很多的算法或者思考题是基于相同思想进行解答的,那么这类算法被考察的概率很高,对于这类算法的思考和探究也就十分有意义!

  快排也是在实践中应用非常广泛的一种排序算法,特别是在C++或对Java基本类型的排序中特别有效。

为什么说是基本类型? 这是在对比归并排序(merge sort)和快排时经常提的问题,原因在于两种排序方式各自的特点:

快速排序(quick sort)元素移动次数少,元素比较次数多;

归并排序(merge sort)元素移到次数多,元素比较次数最少。

而算法的运行时间耗费在:

1)比较元素;

2)移动元素。

所以快排更加适用于比较成本较低的基本类型,而对于耗时较长的泛型比较,例如实现comparator接口,就该考虑使用比较次数较少的归并排序了。

  它的平均运行时间是 O(N \log N)),但是不稳定,它的最坏情形时间复杂度为O(N^2),但是这种不稳地是可以通过对算法的优化避免的,接下来就讨论对算法的优化问题。

常见写法

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
复制代码    public static void QSort(int[] a, int left, int right) {
if(left >= right) {
return;
}
//选择最左边元素为基线值
int base = a[left];
int i = left;
int j = right;
//移动元素使得基线值
while(i < j) {
//左移放前面
while(i < j && base <= a[j]) {
j--;
}
//右移
while(i < j && base >= a[i]) {
i++;
}
if(i < j) {
swap(a, i, j);
}
}
//交换base和比base小的最后一个元素的值
swap(a, left, i);
QSort(a, left, i-1);//左边递归
QSort(a, i+1, right);
}

  快排是一种分治的递归算法,描述这种最常见的快排实现方式,
对数组S进行排序的基本算法由下面4步组成:

  • 如果S中的元素个数为0或者1,则返回;
  • 取S)中的任意元素u),称为枢纽元(pivot),上面实现采用的是数组的第一个元素作为枢纽元;
  • 将数组除u)的剩余部分划分为两个不相交的集合S_{1},S_{2}),一个由大于等于u)的元素组成,一个由小于等于u的元素组成;
  • 对集合S_{1},S_{2}重复前面步骤,递归进行快排。

  快排的过程参考下面GIF:

快排

快排动态图
不平衡的问题的出现


  前面提到快排存在不平衡的情况,但是这种不平衡可以通过对算法的优化来解决。那什么时候会引起不平衡的情况?

1)枢纽元选取引起

  快速排序的性能高度依赖于枢纽元的选择,对于常见写法中选择第一个元素作为枢纽元的策略是极其危险的,如果输入的是预排序或者是反排序的,那么枢纽元会产生极其不平衡的分割—元素全在S_{1})集合或者元素全划分到S_{2}集合。

  并且这种糟糕的情况会发生在所有的递归中,这种不平衡情况时间耗费是O(N^2),更为尴尬的是如果选取第一个元素作为枢纽元且输入是预先排序,时间消耗是二次的,但是结果却是什么也没做。

不平衡的快排调用栈
测试用例:
Leetcode 217.存在重复元素

217.存在重复元素

  这道题解题思路十分简单,先对整数数组进行快排,然后再判断排序后的数组的相邻元素是否相等,就可以得到解答,可以看出这也是一道可以使用快排算法的题。

1
2
3
4
5
6
7
8
9
10
11
12
复制代码    public boolean containsDuplicate(int[] nums) {
if (nums == null) {
return false;
}
QSort(nums, 0, nums.length - 1);
for (int i = 0; i < nums.length; i++) {
if (i + 1 < nums.length && nums[i] == nums[i + 1]) {
return true;
}
}
return false;
}

  当按照常见写法完成快排时,会发现这道题的最后一个测试用例是一个庞大的预先排序的整数数组,会导致超时,这就是由于不平衡导致的二次时间引起的超时。

  这种不平衡的情况会在后面枢纽元的选择的优化中进行处理。

2)分割策略引起

  我们再考虑由分割策略引起的不平衡,将快排步骤中的第三步元素的比较和交换的策略称为分割策略,可以理解为将数组按照与枢纽元大小关系分割成两个不相交子数组的策略,如下图:

分割策略
  图中的枢纽元为5,分割结果为5的左边为小于5的数右边为大于5的数。需要注意到数组中所有的元素互异,而分割策略的优化着重考虑的是数组中出现重复元素该怎么办。

  最优的分割策略,我们期待是将数组分割为元素个数相近的两个子数组,而坏的分割策略则会产生不均衡的两个子数组,即出现不平衡问题,极端情况结果就和预先排序且选取第一个元素作为枢纽元时的相同,时间复杂度O(N^2)。

  我们考虑一种极端情况,当数组所有元素的值都相等的情况,以常见写法为例,查看算法的分割策略:

1
2
3
4
5
6
7
8
复制代码            //左移
while(i < j && base <= a[j]) {
j--;
}
//右移
while(i < j && base >= a[i]) {
i++;
}

  首先R指针左移寻找到第一个小于枢纽值的元素,注意:对于和枢纽元相同的元素采用的策略是不停(遇到相等元素时继续移动),所以右移会一直左移直到L == R结束,如下图所示:

不平衡分割策略
  结果很明显,子数组S_{1})为空,S_{2}包含除了枢纽元外的其余5个元素,是极不均衡的分割策略。

枢纽元的选择的优化

  从上面描述的算法来看,枢纽元存在多种选择,无论选择数组内的哪个元素都能完成排序工作,但是前面也提到一些坏的选择会导致不平衡的问题,接下来讨论如下几种选择:

  • 一种错误的方法

  这种错误的选取方式就是把第一个元素或者最后一个元素用作枢纽元,如果输入的数组是随机的,那么这是可以接受的,但是如果输入是预排序或者是反序的,则会产生不平衡的问题,时间复杂度上升到O(N^2)。

  这种方法产生的糟糕结果在前面给出的Leetcode算法题中已经体现,会产生算法超时,所以我们应该避免这种方法。

  • 一种安全的方法

  一种安全的方针是随机选取枢纽元,一般来说这种策略非常安全,除非随机数发生器有问题,因为随机的枢纽元不可能总在连续不断的产生劣质的分割。但是随机数的生成一般开销很大有点得不偿失。

  • 三数中值分割法
      稍作思考,最优的枢纽元应该是将数组分成两个元素个数相近的子数组,其实也就是数组元素的中位数,即最优枢纽元是中位数,但是如果每一次选取的时候都计算出数组的中位数,又需要耗费大量时间,显然也不可取。

  在综合考虑后,提出一种中值估计的方法—-三数中值分割法,基本思路是:使用左端,右端和中心位置上的三个元素的中值作为枢纽元,其实是对中值的估计,选取过程:

三数中值分割法
  代码实现如下,参数为需要选取枢纽元的数组,返回枢纽元的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
复制代码    private int median3(int[] a,int i,int j) {
//对三个数组进行排序
int m = (i + j) >> 1;
if (a[m] < a[i]) {
swap(a, i, m);
}
if (a[j] < a[i]) {
swap(a, i, j);
}
if (a[j] < a[m]) {
swap(a, j, m);
}
//将枢纽值放在j - 1;
swap(a, m, j - 1);
return a[j - 1];
}

实现细节:

对左端a[left]右端a[right]和中心位置a[center]的元素进行排序,然后将枢纽元放在a[right-1]的位置。

好处一:a[left]和a[right]的位置是分割的正确的位置,所以在后序的需要分割的区间可以缩小到[left+1,right-2]。

好处二:枢纽元存储在a[right-1]可以充当警戒标记,防止越界。

分割策略的优化

  回顾前面由分割策略引起的不平衡,分割策略的细节在于如何处理那些等于枢纽元的元素,问题在于L指针和R指针在遇到等于枢纽元的元素是否停止,则存在如下三种策略:

  • L指针和R指针都不停;
  • L指针和R指针都停;
  • L指针和R指针其中一个停,一个不停。

  考虑元素全相等的极端情况,显然不停和其中一个停的策略其实结果都是产生不平衡情况,分割结果为极不平衡的两个数组。(参考前面不平衡的分割策略的图)

  所以考虑到我们追求的是平衡的一种策略,所以进行不必要的交换建立两个平衡的子数组要比冒险得到两个极不均衡的子数组要好。因此在LR遇到等于枢纽元的元素时,让两个指针都停下来,进而避免二次时间的出现。

优化后的快排

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
复制代码    public void QSort(int[] a, int left, int right) {
if(left >= right) {
return;
}
//三数中值分割法选取枢纽元
int base = median3(a, left, right);
int i = left;
int j = right - 1;
while(i < j) {
while(i < j && base > a[++i]) {}
while(i < j && base < a[--j]) {}
if(i < j) {
swap(a, i, j);
}
}
swap(a, i, right - 1);

QSort(a, left, i - 1);
QSort(a, i + 1, right);
}

private void swap(int[] a, int i, int j) {
int temp = a[i];
a[i] = a[j];
a[j] = temp;
}
//三数中值分割法
private int median3(int[] a,int i,int j) {
//对三个数进行排序
int m = (i + j) >> 1;
if (a[m] < a[i]) {
swap(a, i, m);
}
if (a[j] < a[i]) {
swap(a, i, j);
}
if (a[j] < a[m]) {
swap(a, j, m);
}
//将枢纽元放在j - 1;
swap(a, m, j - 1);
return a[j - 1];
}

小细节

对比常见写法和优后的快排,发现除了前面提到的优化策略外还有一些更改:

例如常见写法中是先R指针左移,后L指针右移,而优化后的是先L指针右移,为什么?

原因:

  在常见写法中枢纽元在最左边,右指针左移肯定停在一个小于或等于枢纽元的元素对应的位置(假设为index),紧接着是左指针右移假设一直小于枢纽值,则会停在和右指针相同位置(index),分割的最后一步是将枢纽元和左指针交换,而左指针指向的是一个小于或等于枢纽元的值(即右指针左移停的index),则因为最终分割结果是小于枢纽元的值在左边,所以完全没问题。

  再考虑如果在常见写法中,使用左指针先移的策略,那么左指针停的位置是一个大于或等于枢纽元的位置,如果进行最后的左指针和枢纽元的交换,就将一个大于枢纽元的值移到了左边,显然是不可行的。

结论:

  所以得出结论:左右指针的移动顺序是由要交换的枢纽元位置决定的,如果枢纽元在左边(常见写法的枢纽元)那么应该将一个小于或等于枢纽元的值和它交换,而右指针先左移肯定得到的是小于或等于枢纽元的值;

  而枢纽元在右边的(优化后的枢纽元)那么应该将一个大于或等于枢纽元的值和它交换,所以采用先左移的方案!

  可以将优化后的算法再次尝试解决存在不平衡的测试用例的例题:Leetcode 217.存在重复元素,执行结果。

最后

  这是在掘金的第一篇blog,写的不好的地方希望大家指正!

References:

《数据结构与算法分析Java语言描述》—7.7 快速排序

图片来源:

快排动态图

不平衡的快排调用栈

分割策略

本文转载自: 掘金

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

菜包大话-分布式事务-补偿事务-TCC

发表于 2019-07-04

大家好,我是培训时长N年半的培训生,菜..菜包。

喜欢 ..xx..xx..xx..篮球。

长时间培训,感觉需要写点东西,欢迎指正、交流。

概述

TCC是一种补偿型的事务,其核心思想是:对于每个操作,都要注册一个与其对应的确认和补偿(撤销)的操作。

TCC是一种应用层的、手动补偿的补偿型事务。

运行过程

  • Try 阶段主要是对业务系统做检测和资源预留。
  • Confirm 阶段主要是对业务系统做确认提交,Try 阶段执行成功并开始执行 Confirm 阶段时,默认 Confirm 阶段时不会出错的。即:只要 Try 成功,Confirm 一定成功;若Confirm失败,则一直重试直至成功。
  • Cancel 阶段主要是在业务执行错误,需要回滚的状态下执行的业务取消,预留资源的释放。

例子

隔壁老王给儿子转账,老王是农行的卡,儿子是工行的卡。

假设老王当前卡里有10000块,想转给儿子3000块,儿子目前卡里有2000块。

TCC理想成功情况下

  1. 农行检查老王账户状态,再将老王卡里的10000块扣减3000块,剩下7000块,往老王的预转出账户里增加3000块。(Try)
  2. 工行检查儿子账户状态,再将儿子的预转入账户里增加3000块。(Try)
  3. 以上步骤全部成功,农行将老王的预转出账户减少3000块,工行将儿子的预转入账户减少3000块,同时儿子的卡里增加3000块。(Confirm)
1
2
3
4
5
6
7
8
9
10
11
12
13
复制代码class Transfer{
String transfer(){
//农行预转出
nonghang.tryReduce(3000);
//工行预转入
gonghang.tryAdd(3000);
//农行确认转出
nonghang.confirmReduce(3000);
//工行确认转入
gonghang.confirmAdd(3000);
return "success";
}
}

TCC失败情况 – Try阶段失败

  1. 农行检查老王账户状态,再将老王卡里的10000块扣减3000块,剩下7000块,往老王的预转出账户里增加3000块。(Try)
  2. 工行检查儿子账户状态,发现儿子账户存在问题,转入金额失败。(Try)
  3. 儿子工行卡Try阶段失败,那么老王农行卡需要减去预扣减账户里的3000块,往卡里增加3000块。(Cancel)
  4. 若第3步Cancel失败,则需要重试。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
复制代码class Transfer{
String transfer(){
//农行预转出
boolean nhTrySuccess = nonghang.tryReduce(3000);
if (!nhTrySuccess) {
return "fail";
}
//工行预转入
boolean ghTrySuccess = gonghang.tryAdd(3000);;
if (!ghTrySuccess) {
boolean nhCancelSuccess = false;
while(!nhCancelSuccess){
//农行取消转出
nhCancelSuccess = nonghang.cancelReduce(3000);
}
return "fail";
}
//农行确认转出
nonghang.confirmReduce(3000);
//工行确认转入
gonghang.confirmAdd(3000);
return "success";
}
}

TCC失败情况 – Confirm阶段失败

  1. 农行检查老王账户状态,再将老王卡里的10000块扣减3000块,剩下7000块,往老王的预转出账户里增加3000块。(Try)
  2. 工行检查儿子账户状态,再将儿子的预转入账户里增加3000块。(Try)
  3. 以上步骤全部成功,农行将老王的预转出账户减少3000块,此时工行的服务器挂了,儿子卡里未增加3000块,那么我们认为交易是成功的,只是需要不断重试通知工行将儿子的预转入账户减少3000块并往卡里添加3000块。
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
复制代码class Transfer{
String transfer(){
//农行预转出
boolean nhTrySuccess = nonghang.tryReduce(3000);
if (!nhTrySuccess) {
return "fail";
}
//工行预转入
boolean ghTrySuccess = gonghang.tryAdd(3000);;
if (!ghTrySuccess) {
boolean nhCancelSuccess = false;
while(!nhCancelSuccess){
//农行取消转出
nhCancelSuccess = nonghang.cancelReduce(3000);
}
return "fail";
}
//农行确认转出
boolean nhConfirmSuccess = false;
while(!nhConfirmSuccess){
nhConfirmSuccess = nonghang.confirmReduce(3000);
}
//工行确认转入
boolean ghConfirmSuccess = false;
while(!ghConfirmSuccess){
ghConfirmSuccess = gonghang.confirmAdd(3000);
}
return "success";
}
}

总结

TCC事务与2PC事务对比,有什么区别?

从伪代码中我们不难看出,TCC实际上是将事务的提交和回滚方式搬到了应用代码层,将2PC的一个大的分布式事务,拆分成多个本地小事务执行,通过编码的方式来完成整个事务的生命周期。减少了对相关资源的长时间锁定,提高了可用性。

伪代码的TCC的实现方式,有什么缺陷?

  • 对真正业务代码入侵大,真正的业务只涉及到4行代码,其他的都是补偿和确认流程的控制。难以更加复杂的场景需要多大的代码量。
  • Confirm和Cancel接口方法需要做幂等性校验,保证数据一致性。

需要改进的地方

可将入侵的非业务代码,即补偿方式的核心思想抽象出来,形成类似2PC中协调器的组件,管控整个补偿流程,尽量减少代码的入侵。

TCC核心思想落地的框架

  • Hmily(源码简单,容易阅读)

本文转载自: 掘金

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

1…865866867…956

开发者博客

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