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

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


  • 首页

  • 归档

  • 搜索

SpringSecurity认证与授权 实现自定义注解

发表于 2021-07-04

点关注不迷路,欢迎关注点赞评论!

最近在做一个关于认证与授权的功能模块,需要使用自定义注解完成匿名访问的接口放行功能。想必各位大佬都知道或者了解,springsecurity中三个configure方法

image.png

  1. configure(AuthenticationManagerBuilder auth)
  2. configure(HttpSecurity http)
  3. configure(WebSecurity web)
    使用web.ignoring().antMatchers()方法;,可以实现不走 Spring Security 过滤器链
1
2
3
4
5
6
7
java复制代码@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/login");
}
}

一、首先根据官网搭一个基础的环境,加入spring-boot-starter-security和spring-boot-starter-web依赖

修改pom.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
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
xml复制代码    <parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.8.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</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>

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

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

<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>
</dependencies>

二、配置基础的SecurityConfig和WebMvcConfig以及登录页

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
java复制代码@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

@Autowired
private RequestMappingHandlerMapping handlerMapping;

@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {

httpSecurity
.authorizeRequests()
.anyRequest()// 所有请求全部需要鉴权认证
.authenticated();
}

@Bean
@Override
public UserDetailsService userDetailsService() {
UserDetails user =
User.withDefaultPasswordEncoder()
.username("user")
.password("password")
.roles("USER")
.build();

return new InMemoryUserDetailsManager(user);
}

}
1
2
3
4
5
6
7
8
java复制代码@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/login").setViewName("login");
}

}

在templates下添加login.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
html复制代码<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="https://www.thymeleaf.org"
xmlns:sec="https://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
<title>Spring Security Example </title>
</head>
<body>
<div th:if="${param.error}">
Invalid username and password.
</div>
<div th:if="${param.logout}">
You have been logged out.
</div>
<form th:action="@{/login}" method="post">
<div><label> User Name : <input type="text" name="username"/> </label></div>
<div><label> Password: <input type="password" name="password"/> </label></div>
<div><input type="submit" value="Sign In"/></div>
</form>
</body>
</html>

三、新建controller测试一把

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码@RestController
@RequestMapping
public class TestController {

@GetMapping("test")
public String test() {
return "直接过滤";
}

@GetMapping("test1")
public String test1() {
return "需要登录";
}
}

四、访问一下localhost:8080/test和test1接口,可以看到会跳转到登录页,说明我们的接口都是被拦截了的

屏幕截图 2021-07-04 140120.png

五、现在我们通过 web.ignoring().antMatchers来把test放行,放行配置如下

1
2
3
4
5
java复制代码    @Override
public void configure(WebSecurity web) throws Exception {
// 此方法可以实现不走 Spring Security 过滤器链
web.ignoring().antMatchers(HttpMethod.GET, "/test");
}

六、再试下test接口访问,直接访问成功

屏幕截图 2021-07-04 141118.png

而test1接口这个时候访问还是需要登录,说明我们的放行配置起到了作用

image.png

这个时候我们又要思考了,当项目越来越大,接口越来越多的时候怎么办呢?难道一直改代码?这样的方式恐怕不妥啊!问题不大,我们现在试一试用注解的方式来放行接口!

思路:把所有带这个注解的方法放行了不就行了吗

开始我们的操作

新建一个IgnoreAuth的注解

1
2
3
4
5
java复制代码@Target(ElementType.METHOD) //注解放置的目标位置,METHOD是可注解在方法级别上
@Retention(RetentionPolicy.RUNTIME) //注解在哪个阶段执行
@Documented //生成文档
public @interface IgnoreAuth {
}

现在有了注解,怎么获取到所有带这个注解的方法呢?

spring提供了RequestMappingHandlerMapping类,可以帮助我们,来获得所有的URL
通过继承(这里就不给大家看源码了,感兴趣的可以自己去看),可以调用getHandlerMethods()来获取

先debug看下获取到的数据是什么样的

1
2
3
4
5
6
java复制代码@Override
public void configure(WebSecurity web) throws Exception {
WebSecurity and = web.ignoring().and();
Map<RequestMappingInfo, HandlerMethod> handlerMethods =
handlerMapping.getHandlerMethods();
}

启动项目,看到handlerMethods中包含了key(请求的类型,请求url)value(具体的方法)

屏幕截图 2021-07-04 144729.png

那现在有了所有的url方法,我们只需要将带有IgnoreAuth注解方法筛选出来,并且直接放行就行了

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
java复制代码handlerMethods.forEach((info, method) -> {
// 带IgnoreAuth注解的方法直接放行
if (!Objects.isNull(method.getMethodAnnotation(IgnoreAuth.class))) {
// 根据请求类型做不同的处理
info.getMethodsCondition().getMethods().forEach(requestMethod -> {
switch (requestMethod) {
case GET:
// getPatternsCondition得到请求url数组,遍历处理
info.getPatternsCondition().getPatterns().forEach(pattern -> {
// 放行
and.ignoring().antMatchers(HttpMethod.GET, pattern);
});
break;
case POST:
info.getPatternsCondition().getPatterns().forEach(pattern -> {
and.ignoring().antMatchers(HttpMethod.POST, pattern);
});
break;
case DELETE:
info.getPatternsCondition().getPatterns().forEach(pattern -> {
and.ignoring().antMatchers(HttpMethod.DELETE, pattern);
});
break;
case PUT:
info.getPatternsCondition().getPatterns().forEach(pattern -> {
and.ignoring().antMatchers(HttpMethod.PUT, pattern);
});
break;
default:
break;
}
....
});
}
});
}

在test方法上打上我们的自定义注解IgnoreAuth

1
2
3
4
5
java复制代码    @IgnoreAuth
@GetMapping("test")
public String test() {
return "直接过滤";
}

写完代码,重启项目,我们再测试下,搞定!
屏幕截图 2021-07-04 141118.png

当然,我这里只是其中一种实现方式,有哪儿不足的地方希望大家能在评论中指出来,谢谢大家!
image.png

写文真不容易,裂开!

本文转载自: 掘金

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

如何设计一个高可用的订单系统

发表于 2021-07-04

订单系统的基本架构:
image.png
前台有结算页提供用户去结算,当后台收到前台用户点击结算操作时就会开始处理下单服务,起初订单被写入到后台的数据库中,然后异构数据到缓存中以此提供用户在我的订单系统中进行订单查询,当用户支付完成后,收银台发送消息给下单的服务进行数据库和混存中订单状态的修改,这样简单的订单系统就完成了,但是真实的订单系统有更加复杂的业务,使系统更加复杂。
接下来,看看系统中哪些环节会出现丢单的问题

订单丢失:写数据库和接受和发送订单消息

1.关键逻辑不能使用读写分离的查询方式,避免从库同步延迟造成订单查询异常
image.png
2.关键逻辑不要使用缓存进行订单查询,由于缓存延迟造成订单反查询失败

image.png

3.订单补偿不要简单粗暴的使用消息队列的方式,避免中间件引发的订单丢失,比如在订单状态修改的时候,如果处理失败,就将这个订单信息插入到消息队列中重新消费,以此完成订单的补偿,这种方式在发送消息和接收消息时有可能存在丢消息的可能
image.png

4接收消息处理失败时一定要让消息重试,避免丢失尤其注意return,continue等关键字,比如一次消费多条记录一条条地进行处理,如果修改状态成功,就继续处理下一条,如果修改失败,可能会因为return或者continue等关键字将其余的消息都丢失掉了

image.png

如何设计一个支持日万级订单系统,考虑到前面可能丢单的问题,以及系统的稳定性和可用性,我们如何进行系统化的重构优化。之前的系统其实很容易处理日万级的订单,所以你只要注意几个关键点就可以了:
1.写数据库时,数据库事务的粒度不要太大,避免锁表,关注慢查询,比如最不要犯的错误就是在数据库事物里面同时去更新其他的数据源或者发送消息到消息中,这不仅不能保证数据的一致性还会把数据库的连接耗尽。

image.png
另外需要注意的是关注数据异构的性能和稳定性,尤其是在网路抖动的情况下,可能会影响用户体验
最后,要关注订单的幂等性,避免出现计费错误,影响后续的操作流程。
做好这几点前面的架构方案基本上能够满足日万级的订单系统了。

如何设计一个支撑日千万级的订单系统,与日万级的订单系统的区别在于量,由于量的增大造成系统负载过重,导致服务最终宕机,那么分析一下前面的系统架构中哪些是系统的瓶颈呢?

首先,前面的架构设计中过度的依赖于数据库,而且数据库还是订单库,持续的读写请求会给数据库造成很大的压力,比如修改订单状态的时候需要反查数据库并进行订单状态更新,这些操作在高并发请求时,会造成数据库资源的抢占,从而影响系统的稳定性,其次,为了避免数据不一致,请求访问主要集中在主库上,因此主库的压力比较大

image.png

因此在用分库分表的架构,下单服务为此也必须进行改造,支持分库分表的架构设计,但是由于热点数据的存在,可能导致数据库出现数据倾斜的问题,引发提早的数据库扩容问题

image.png

还有,由于下单服务耦合过重,使得即使是多集群的部署架构,也很难快速的处理业务响应,更何况不同业务的订单处理流程还是不一样的,使得系统的维护性越来越差,比如:创建订单时由于业务不同,数码,3c,图书等订单系统的信息时不一样的,这就需要特殊处理,这种特殊处理与创建订单耦合在一起,就会导致系统处理速度很慢,最后由于数据库的存储数据量增大,还会导致数据异构性能直线下降以及缓存容量的不断扩大,这都会极大的影响查询性能,而且可能出现业务间的互相影响等问题
总的来说,前面的系统出现问题:下单系统处理订单慢,数据库压力大,数据异构延迟高,缓存数据质量差
image.png

为了应对日千万级的订单量,我们对下单服务进行了拆分,使用的单独的接单服务处理订单,使用订单引擎和订单管道处理订单业务逻辑,改用双写和事物补偿的方式处理缓存,使用缓存过期的方式处理数据量,

image.png

接下来就具体的分析实现方案:当用户点击提交订单之后,接单服务就会在同一个事务里,将订单插入接单库,将首任务插入到任务库,再由订单引擎进行任务调度,什么是任务?任务就是执行订单操作的步骤,比如写订单缓存,减库存,发送订单通知等,以及前面提到的不同的特殊业务流程,这些都是一个个的任务,我们将整个订单处理流程分解成一个个的任务,逐个单独处理,来应对日千万级的订单处理压力。

image.png

其中接单库为多台数据库,通过随机的方式写入数据库中,之所以没有采用哈希等算法,其原因在于扩展能力更加灵活,当遇到流量洪峰来临时,新增数台数据库对写入逻辑是无感的,接单库采用一主多从的部署架构,当一台机器故障,可以通过快读切换主从或者摘除故障机等手段进行修复,

image.png
而其中任务库由订单引擎驱动执行,任务通过订单引擎的服务编排能力,生成任务队列,首任务执行成功之后,会插入第二个任务,或者同时插入第三个和第四个任务,如果任务插入失败,订单引擎会重新执行当前任务,执行成功之后,会继续执行插入操作,这里就需要每个任务的业务处理都需要保证幂等性,
image.png

接下来,说一下任务的线程调度方式,任务使用多线程的异步方式进行调度,并根据配置选择是串行执行还是并行执行,有一点注意的是:前面说的任务线程调度执行,那么如果任务执行失败么,订单引擎如何重新执行任务失败的任务呢?这就是任务状态机来实现的,任务状态机就如同一个系统的守护线程。

image.png

任务状态机通过识别任务状态来识别每个任务是执行完成还是执行失败,并根据状态进行任务调度,并且对多次执行失败的任务重试调度的频次也会逐渐减弱,当超过一定重试次数之后就会发出报警进行人工干预,
image.png
其实,订单引擎真正执行远程调度远程服务的并非订单引擎来执行的,而是由订单引擎调度订单管道,订单管道去调度远程真实的服务来执行的,其原因在于任务引擎本身就是多线程设计架构,对线程占用就比较高,而远程调度会注册很多服务,服务调度也会启动很多多线程去执行,如果共同部署在同一个系统里,就会出现线程数过多导致cpu飙升的情况,

image.png
接下来再来说一下订单缓存的实现策略:接单服务在处理完一些业务逻辑之后,最后调用下单服务提交订单到订单中心,而在此之前,为了保证订单的及时性,在插入订单和任务之后,接单服务会现将订单通过接口写入到订单中心的缓存中,以支持用户在支付之后,在我的订单列表中能立即查询到我的订单,总体来说,订单中心接到下单服务之后,会将订单落库,便同步到缓存中,在后续订单中心收到台帐的消息之后,也会同时更新数据库和缓存,将订单状态更新为订单完成。

image.png
最后,讲述日千万级的订单系统架构再概括的讲一下,用户在结算页点击结算,结算页调用后台的接单服务,接单服务接收到下单请求之后会负责接单,将订单插入到接单库,同时在一个事务里将首任务插入到任务库并通知调度起订单引擎开始执行任务,订单引擎根据任务编号依次执行任务调度并更新任务状态,并由状态机进行任务校验补偿处理,订单引擎通过调度订单管道实现真实远程调度,订单管道请求服务之后,将处理结果返回任务引擎,最后,订单中心会在接单服务创建订单时异步地写一份订单缓存到订单中心,然后再通过数据异构的方式再次写一份数据在订单缓存中,

image.png

了解完订单的处理流程之后,我们再整个流程中如何保证下单流程的高可用和高性能的,整个订单系统接单的核心流程及几乎为同步执行,只有少数任务,比如发送订单通知给下游,系统时采用消息异步的方式执行,以此来保证订单流程的高性能而整个处理过程,基于订单引擎的调度,通过服务流程编排确定一个订单的执行步骤,并有效地保证每个环节的正确执行避免订单丢单,卡单等异常问题出现,进而保证订单流程的高可用,

本文转载自: 掘金

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

leetcode每日一题系列-错误的集合 leetcode-

发表于 2021-07-04

leetcode-645-错误的集合

[博客链接]

菜🐔的学习之路

掘金首页

[题目描述

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
java复制代码集合 s 包含从 1 到 n 的整数。不幸的是,因为数据错误,导致集合里面某一个数字复制了成了集合里面的另外一个数字的值,导致集合 丢失了一个数字 并且 有
一个数字重复 。

给定一个数组 nums 代表了集合 S 发生错误后的结果。

请你找出重复出现的整数,再找到丢失的整数,将它们以数组的形式返回。



示例 1:


输入:nums = [1,2,2,4]
输出:[2,3]


示例 2:


输入:nums = [1,1]
输出:[1,2]




提示:


2 <= nums.length <= 104
1 <= nums[i] <= 104

Related Topics 位运算 数组 哈希表 排序
👍 189 👎 0

[题目链接]

leetcode题目链接

[github地址]

代码链接

[思路介绍]

**思路一:hash存储+两次遍历

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
java复制代码public int[] findErrorNums(int[] nums) {
int[] temp = new int[nums.length + 1];
int index = 0, res = 0;
Set<Integer> set = new HashSet<>();
for (int i = 0; i < nums.length; i++) {
if (set.contains(nums[i])) {
index = nums[i];
}
set.add(nums[i]);
temp[nums[i]] = nums[i];
}
for (int i = 1; i < temp.length; i++) {
if (temp[i] == 0) {
res = i;
}
}
return new int[]{index, res};
}

时间复杂度O(n)时间复杂度O(n)时间复杂度O(n)


思路二:思路一的优化

  • 通过数组记录重复元素,减少set存储
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码public int[] findErrorNums(int[] nums) {
int[] temp = new int[nums.length + 1];
int index = 0, res = 0;
for (int i = 0; i < nums.length; i++) {
temp[nums[i]]++;
}
for (int i = 1; i < temp.length; i++) {
if (temp[i] == 0) {
res = i;
}
if (temp[i] == 2) {
index = i;
}
}
return new int[]{index, res};
}

时间复杂度O(n)时间复杂度O(n)时间复杂度O(n)


思路三:数学

  • 正常没有重复元素总和为等差数列求和total
  • 通过数组记录重复元素
  • 所有总和-非重复元素和为重复元素
  • 目标和-非重复元素和为丢失元素
1
2
3
4
5
6
7
8
9
10
11
12
java复制代码public int[] findErrorNums(int[] nums) {
int n = nums.length;
int total = (1 + n) * n / 2;
int[] temp = new int[n + 1];
int sum = 0, set = 0;
for (int x : nums) {
sum += x;
if (temp[x] == 0) set += x;
temp[x] = 1;
}
return new int[]{sum-set, total-set};
}

时间复杂度O(n)时间复杂度O(n)时间复杂度O(n)


思路四:桶排序

  • 将对应元素i放入nums[i-1]
  • 然后遍历一次,找到非对应元素
  • 可以节省存储空间
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
java复制代码public int[] findErrorNums(int[] nums) {
int n = nums.length;
for (int i = 0; i < n; i++) {
//一直循环直到找到对应元素
while (nums[i] != i + 1 && nums[nums[i] - 1] != nums[i]) {
swap(nums, i, nums[i] - 1);
}
}
int a = -1, b = -1;
for (int i = 0; i < n; i++) {
if (nums[i] != i + 1) {
a = nums[i];
b = i == 0 ? 1 : nums[i - 1] + 1;
}
}
return new int[]{a, b};
}

private void swap(int[] nums, int l, int r) {
int temp = nums[l];
nums[l] = nums[r];
nums[r] = temp;
}

时间复杂度O(n),空间复杂度O(1)

本文转载自: 掘金

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

字节跳动10万节点HDFS集群多机房架构演进之路

发表于 2021-07-04

背景

现状

HDFS 全称是 Hadoop Distributed File System,其本身是 Apache Hadoop 项目的一个模块,作为大数据存储的基石提供高吞吐的海量数据存储能力。自从 2006 年 4 月份发布以来,HDFS 目前依然有着非常广泛的应用,以字节跳动为例,随着公司业务的高速发展,目前 HDFS 服务的规模已经到达“双 10”的级别:

  • 单集群节点 10 万台级别
  • 单集群数据量达到 10EB 级别

主要使用场景包括

  • 离线
    • OLAP 查询引擎存储底座,包括 Hive/ClickHouse/Presto 等场景
    • 机器学习离线训练数据
  • 近线
    • ByteMQ
    • 流式任务 Checkpoint

业界很多公司在维护 HDFS 服务时,采用的都是小集群模式,即生产上部署多个隔离独立的 HDFS 集群满足业务的不同需求。字节跳动采用的是横跨多个机房的联邦大集群部署模式,即 HDFS 只有一个集群,这个集群有多个 nameservice,但是底层的 DN 是横跨 A/B/C 3 个机房的 ,由于社区版 HDFS 没有机房感知相关的支持,因此字节跳动 HDFS 团队在这个功能上做了专门的设计和实现,本文会介绍这部分的工作。

动机

业务的迅猛发展和业务场景的多样性给 HDFS 带来了很大的挑战,这里列几个比较有代表性的问题:

  • 如何在容量上满足业务的发展需求
  • 如何满足近线场景对低延迟的需求
  • 如何满足关键业务的机房级别容灾需求
  • 如何高效运维如此超大规模的集群

要回答这些问题需要 HDFS 从多个方向迭代优化,例如 DanceNN 的上线、运维平台的建设等,本文不会介绍字节跳动 HDFS 所有的演进方案,而是聚焦在 HDFS 多机房架构的演进策略上,它直接回答了上面提到的两个问题,即:

  • 如何在容量上满足业务的发展需求:数据如何合理地在多个机房之间存放以便能通过其他机房的资源进行快速扩容?
  • 如何满足关键业务的容灾需求:系统如何满足核心业务机房级别的容灾需求?

社区版架构

字节跳动的 HDFS 技术脱胎于 Apache 社区的 HDFS,为了方便大家理解内部版本的技术发展历程,本小节我们将先了解一下社区 HDFS 的架构。

图(1) Apache 社区 HDFS 架构

从图(1) 可以看出,社区 HDFS 从架构上划分可以分为 3 部分:

  • Client:访问 HDFS 的 client,主要通过 HDFS SDK 和 HDFS 进行交互,HDFS SDK 的实现比较重,很多 IO 处理逻辑都是在 SDK 实现,因此这里单独列为架构的一部分。
  • 元数据管理:即 NameNode,负责集群的元数据管理,包括目录树和数据块的位置信息。为了解决元数据膨胀问题,社区提供了 Federation 的功能,引入了 NameService 的概念,简单地说,每一个 NameService 提供一个 NameSpace,为了保证 NameNode 的高可用,一个 NameService 包含多个 NameNode 节点(一般是 2 个),这些 NameNode 节点以一主多备的模式工作。Federation 功能跟多机房架构并没有必要的关联,因此接下来讨论我们将不会涉及 Federation/NameService 等概念。
  • 数据管理:即 DataNode,负责存放用户的实际数据,前面提到 NameNode 一个功能是管理数据块的位置信息,在具体实现上,NameNode 不会持久化这些块的信息,而是靠 DataNode 主动汇报来维护。

到目前为止,HDFS 集群的多机房架构相关的方案基本都是元数据层完成的,因此接下来我们的讨论将会聚焦在元数据部分。在本文剩余篇幅里,除非特别声明,否则相关术语都是指字节跳动版的 HDFS。

字节版架构

图(2) 字节跳动 HDFS 架构

注:由于 BookKeeper 自身的架构设计,NameNode(DanceNN)实际上是需要通过 ZooKeeper 去发现 BookKeeper 的 endpoint 信息的,这里为了方便理解,没有把这部分通信关系画出来。

对比图(1) 和 图(2), 我们可以发现,字节跳动的 HDFS 依然保留了社区 HDFS 的核心架构,同时也加入了一些特有的功能,包括:

  • DanceNN,即字节跳动用 C++重新实现的 NameNode,协议上基本兼容社区版的 NameNode。除非特别说明,否则后面出现 DanceNN、NameNode 均指代 DanceNN。
  • NNProxy,即 NameNode Proxy,为 Federation 功能提供统一的 Namespace,由于跟多机房架构直接关系不大,这里不再详细展开。
  • BookKeeper, 即 Apache BookKeeper,其作用是跟社区的 JournaNode 是一样的,就是为 Active 和 Standby NameNode 提供一个共享的 editlog 存储方案,这是实现 NameNode 的 HA 方法的基础。

值得一提的是,BookKeeper 本身提供了机房级别的保存配置策略,这是 HDFS 多机房容灾方案的基础,这个特性确保了 HDFS NameNode 提供跨机房容灾能力,后面我们将继续深入讨论。

演进

双机房

前面提到当前 HDFS 的大集群是横跨 A/B/C 的多机房模式,具体的演进顺序是 A -> A,B -> A,B,C ,现在也保持了直接扩展到更多机房的能力。本小节将着重介绍 A -> A,B 的双机房演进过程,后面的多机房架构的设计思想主要还是双机房架构的扩展。

数据放置

图(3) 字节跳动 HDFS 双机房 DataNode 结构

HDFS 双机房数据放置方案在设计上总结起来可以描述如下:

  • A/B 机房的 DN 直接跨机房组成一个双机房集群,向相同的 NameNode 汇报。
  • 每一个文件在写的时候会保证每个机房至少存在一个副本,数据实时写到两个机房。
  • 每个 Client 在读取文件的时候,优先读取本机房的副本,避免产生大量的跨机房读带宽。

这个设计的好处就是存储层对上层应用屏蔽了集群细节,计算资源可以直接无感分配。该设计结合离线数据一写多读的特点,充分考虑跨机房带宽的合理使用。

  • 由于写带宽一般不会有突发,机房间的离线带宽可以支撑同步写的需求,因此数据可以两个机房同步放置至少一个副本。
  • 离线查询容易有大的突发请求,因此需要确保常规状态下没有突发的跨机房读带宽。

在实现上关键是 DanceNN 加入了机房的感知能力,DanceNN 在 client 进行数据操作时加入对机房拓扑的识别,由于 DanceNN 对外的协议没有改动,因此上层应用不需要做感知改动。

容灾设计

前面介绍了双机房架构里数据放置的设计,它解决了容量扩展的问题,但是并没有解决机房级别的容灾问题,尽管 NameNode 以一主多备的形式实现了高可用,但是所有 NameNode 还是放在一个机房,在字节跳动基础架构的容灾体系里,是需要做到机房级别的容灾。由于 HDFS 的数据已经实现了多机房数据副本的同步写入,为了达成容灾的目标,只需要把元数据也演进到双机房架构即可实现机房级别的容灾。前面我们所说的 HDFS 的元数据组件其实包含了两部分,即 NameNode 和 NameNode Proxy(NNProxy),由于 NNProxy 是无状态的转发服务,因此元数据的多机房架构我们只需要关注在 NameNode 设计上。

图(4) 字节跳动 HDFS NameNode 体系

从图(4) 可以看出 NameNode 包含了 3 个关键模块:

  • Apache ZooKeeper,为 Apache BookKeeper 提供元数据服务。
  • Apache BookKeeper,为 NameNode 的高可用方案提供 EditLog 共享存储方案。
  • DanceNN,即字节跳动自研的高性能 NameNode 实现。

这 3 者构成一个分层的单向依赖关系链, DanceNN -> BookKeeper -> ZooKeeper,因此这 3 者可以独立完成双机房的容灾方案,最终在整体上呈现一个双机房容灾的 NameNode 元数据服务。

组件 多机房方案
ZooKeeper 一个 ZK ensemble 由 5 台 server 组成,这 5 台 server 分布在 3 个机房,分布比例为 A:B:C = 2:2:1
BookKeeper 一个 BK cluster 通常由 14 台 server 组成,分布在 2 个机房,分布比例为 1:1
DanceNN 一个 NameService 包含 5 个 DanceNN,这个 5 个 DanceNN 分布在 2 个机房,分布比例为 3:2,工作模式为 1 active + 4standby

在实现上,这里面的关键就是 DanceNN 的 editlog 机房写策略,因为 DanceNN 在做主备切换的时候,如果 editlog 没法保持同步,那么服务是不可用的,得益于 BookKeeper 的机房感知的数据放置功能,DanceNN 可以通过这些策略来完成双机房容灾方案。

  • 常态下,editlog 会以 4 个副本存放到 BookKeeper 上,这 4 个副本的机房分布比例为 1:1。
  • 容灾场景下,DanceNN 可以快速切换成单机房模式,editlog 依然以 4 个副本存放,但是存储策略变为单机房存储,历史的 editlog 也能正常消费。

旁路系统

前面已经介绍完了 HDFS 双机房方案的主体设计,但是事实上一个方案的推进落地除了架构上的迭代演进之外,还需要一系列的旁路系统来配合支持,包括:

  • Balancer:需要感知机房放置
  • Mover:需要保证数据的实际放置满足多机房策略
  • 运维系统
    • 在 federation 架构下,多个 nameservice 需要保证切主的效率
    • 运维操作预案:提前预判相关可能的故障,并且能在运维系统上执行
  • 业务的平稳过渡方案,尽可能少地减少对业务干扰

限于篇幅,本文不会对这些展开细节描述,感兴趣的同学可以再交流。

多机房

HDFS 多机房架构是对双机房架构的扩展,其研发直接动机是机房的资源供应短缺问题,例如 2020 年 B 机房几乎就没有资源供应,但是在公司新的主机房 C 却有较为充裕的资源。一开始我们是尝试将 C 机房作为一个独立的集群提供服务,但是发现业务的血缘关系太过复杂,迁移成本太高,因此选择了基于双机房机房扩展到多机房的方法,该方案需要满足这些需求。

  • 合理使用跨机房带宽
  • 兼容已有的双机房方案
  • 迁移成本尽可能小
  • 符合字节跳动的机房级别容灾标准

最终的设计方案为:

  • 数据放置策略支持多机房,同时兼容已有的双机房放置策略
  • NameNode 的容灾方案策略不变,因为在多机房架构下,HDFS 依然只保证一个机房范围的故障容灾

相应的旁路系统也做相应的调整,尽管 HDFS 底层提供了数据放置在多个机房的策略,但是在离线场景中,用户只能选择 2 个机房存放,例如 A/B, B/C,A/B,这个运营上的策略选择是综合考虑了稳定性、带宽使用的合理性以及资源的合理利用之后确定的,核心目标还是保障业务的平稳发展,从后续实践下来看,这个策略是一个非常正确的选择。

结语

根据我们的不完全调研,字节跳动 HDFS 的多机房架构在业界中是有自己独特的路线,这个中原因主要还是公司业务高速发展和机房建设方向在业界中也是独树一帜的,这些因素驱动 HDFS 进行自己独特迭代演进,从结果来看是达到预期,例如 2020 年 C 机房的充分使用,在 B 机房没有资源供应的情况下依然保障了业务的平稳;2021 春晚活动,为近线业务例如 ByteMQ、流式 CheckPoint 等提供了多机房的容灾策略保障。

最后,HDFS 的多机房架构依然在持续迭代,中长期来看,不排除有更多新机房的出现,这些都给 HDFS 多机房架构提出更多的挑战,原来多机房方案的基础条件不再具备,因此 HDFS 团队已经开启相关功能的迭代,敬请期待!

加入我们

字节跳动大数据存储团队是大数据存储领域行业领军者,负责整个字节跳动全球大数据存储基础设施的建设,支持今日头条、抖音、西瓜视频,番茄小说、电商、游戏以及教育等众多产品线,管理数十EB的数据。我们长期致力于超大规模存储系统的技术演进,包括访问加速,数据流转、成本优化、高效运维和服务可用性等方向,欢迎更多同学加入我们,一起构建下一代10EB级别的存储系统,工作地点是上海,感兴趣可以联系 fandi.3@bytedance.com 并注明大数据存储方向。

本文转载自: 掘金

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

SpringBoot优雅的全局异常处理 前言 SpringB

发表于 2021-07-04

前言

在日常项目开发中,异常是常见的,但是如何更高效的处理好异常信息,让我们能快速定位到BUG,是很重要的,不仅能够提高我们的开发效率,还能让你代码看上去更舒服,SpringBoot的项目已经有一定的异常处理了,但是对于我们开发者而言可能就不太合适了,因此我们需要对这些异常进行统一的捕获并处理。

SpringBoot默认的错误处理机制

返回错误页面

默认返回 Whitelabel Error Page页面的样式太单调,用户体验不好。

如 果 我 们 需 要 将 所 有 的 异 常 同 一 跳 转 到 自 定 义 的 错 误 页 面 , 需 要 再 src/main/resources/templates 目录下创建 error.html 页面。

注意:名称必须叫 error

1
2
3
4
5
6
7
8
9
10
11
html复制代码<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
<!--SpringBoot默认存储异常信息的key为exception-->
<span th:text="${exception}" />
</body>
</html>

返回json格式api

Json格式的结果字符串不统一,与前端人员约定统一格式不一致

源码分析

SpringBoot在页面 发生异常的时候会自动把请求转到/error,SpringBoot内置了一个BasicErrorController对异常进行统一的处理,当然也可以自定义这个路径

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
java复制代码@RequestMapping(
produces = {"text/html"}
)
public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {
HttpStatus status = this.getStatus(request);
Map<String, Object> model = Collections.unmodifiableMap(this.getErrorAttributes(request, this.getErrorAttributeOptions(request, MediaType.TEXT_HTML)));
response.setStatus(status.value());
ModelAndView modelAndView = this.resolveErrorView(request, response, status, model);
return modelAndView != null ? modelAndView : new ModelAndView("error", model);
}

@RequestMapping
public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
HttpStatus status = this.getStatus(request);
if (status == HttpStatus.NO_CONTENT) {
return new ResponseEntity(status);
} else {
Map<String, Object> body = this.getErrorAttributes(request, this.getErrorAttributeOptions(request, MediaType.ALL));
return new ResponseEntity(body, status);
}
}

我们可以看到刚好对照两个方法一个返回错误页面,一个返回错误字符,默认错误路径是/error如果有自定义就用自定义的

1
properties复制代码server.error.path=/custom/error

自定义错误处理

SpringBoot提供了ErrorAttribute类型
自定义ErrorAttribute类型的bean还是默认的两种响应方式,只不过改变了响应内容项而已

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
java复制代码package cn.soboys.core;


import cn.hutool.core.bean.BeanUtil;
import cn.soboys.core.ret.Result;
import cn.soboys.core.ret.ResultCode;
import cn.soboys.core.ret.ResultResponse;
import org.springframework.boot.web.error.ErrorAttributeOptions;
import org.springframework.boot.web.servlet.error.DefaultErrorAttributes;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Map;

/**
* @author kenx
* @version 1.0
* @date 2021/6/18 14:14
* 全局错误
*/
@Component
public class GlobalErrorHandler extends DefaultErrorAttributes {


/**
* 自定义错误返回页面
* @param request
* @param response
* @param handler
* @param ex
* @return
*/
@Override
public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
return super.resolveException(request, response, handler, ex);
}

/**
* 自定义错误返回格式
*
* @param webRequest
* @param options
* @return
*/
@Override
public Map<String, Object> getErrorAttributes(WebRequest webRequest, ErrorAttributeOptions options) {
Map<String, Object> errorAttributes = super.getErrorAttributes(webRequest, options);
Result result = ResultResponse.failure(ResultCode.NOT_FOUND, errorAttributes.get("path"));
Map map = BeanUtil.beanToMap(result, true, true);
return map;
}
}

自定义业务异常类

继承RuntimeException

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
java复制代码package cn.soboys.core.authentication;

import cn.soboys.core.ret.ResultCode;
import lombok.Data;

/**
* @author kenx
* @version 1.0
* @date 2021/6/22 13:58
* 认证异常
*/
@Data
public class AuthenticationException extends RuntimeException {

public AuthenticationException(String message) {
super(message);
}

}

全局捕获异常

通过SpringBoot提供的@RestControllerAdvice和@ControllerAdvice 结合@ExceptionHandler使用

@RestControllerAdvice和@ControllerAdvice区别和@RestController,@Controller一样如果想返回json格式也可以单独使用@ResponseBody注解在方法上

需要捕获什么异常通过@ExceptionHandler来指定对应异常类就可以了这里原则是按照从小到大异常进行依次执行

通俗来讲就是当小的异常没有指定捕获时,大的异常包含了此异常就会被执行比如Exception 异常包含了所有异常类,是所有异常超级父类,当出现没有指定异常时此时对应捕获了Exception异常的方法会执行

@ExceptionHandler注解处理异常

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
java复制代码@Controller
public class DemoController {
@RequestMapping("/show")
public String showInfo() {
String str = null;
str.length();
return "index";
}

@RequestMapping("/show2")
public String showInfo2() {
int a = 10 / 0;
return "index";
}

/**
* java.lang.ArithmeticException 该方法需要返回一个 ModelAndView:目的是可以让我们封装异常信息以及视
* 图的指定 参数 Exception e:会将产生异常对象注入到方法中
*/
@ExceptionHandler(value = { java.lang.ArithmeticException.class })
public ModelAndView arithmeticExceptionHandler(Exception e) {
ModelAndView mv = new ModelAndView();
mv.addObject("error", e.toString());
mv.setViewName("error1");
return mv;
}

/**
* java.lang.NullPointerException 该方法需要返回一个 ModelAndView:目的是可以让我们封装异常信息以及视
* 图的指定 参数 Exception e:会将产生异常对象注入到方法中
*/
@ExceptionHandler(value = { java.lang.NullPointerException.class })
public ModelAndView nullPointerExceptionHandler(Exception e) {
ModelAndView mv = new ModelAndView();
mv.addObject("error", e.toString());
mv.setViewName("error2");
return mv;
}
}

优点:可以自定义异常信息存储的key,自定义跳转视图的名称

缺点:需要编写大量的异常处理方法,不能跨controller,如果两个controller中出现同样的异常,需要重新编写异常处理的方法

@ControllerAdvice+@ExceptionHandler 注解处理异常

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
java复制代码/**
* @author kenx
* @version 1.0
* @date 2021/6/17 20:19
* 全局异常统一处理
*/
@RestControllerAdvice
public class GlobalExceptionHandler {
/**
* 认证异常
* @param e
* @return
*/
@ExceptionHandler(AuthenticationException.class)
public Result UnNoException(AuthenticationException e) {
return ResultResponse.failure(ResultCode.UNAUTHORIZED,e.getMessage());
}

/**
*
* @param e 未知异常捕获
* @return
*/
@ExceptionHandler(Exception.class)
public Result UnNoException(Exception e) {
return ResultResponse.failure(ResultCode.INTERNAL_SERVER_ERROR, e.getMessage());
}
}

优点:可以自定义异常信息存储的key,自定义跳转视图的名称,跨controller统一拦截统一捕获,一般都是使用这种

关注公众号猿人生了解更多好文

本文转载自: 掘金

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

计算机组成原理保姆级复习资料 一、计算机系统概论 二、数据的

发表于 2021-07-04

@[TOC]

一、计算机系统概论

看书看书,都是文字,(想拿高分)多了解

二、数据的表示和运算

各种进制及其转换

进制 举例
二进制: 0,1 二进制: 101.1 —> 1 × 2! + 0 × 2” + 1 × 2# + 1 × 2%” = 5.5
八进制: 0,1,2,3,4,5,6,7 八进制: 5.4 —> 5 × 8# + 4 × 8%” = 5.5
十进制: 0,1,2,3,4,5,6,7,8,9 十进制: 5.5 —> 5 × 10# + 5 × 10%” = 5.5
十六进制: 0,1,2,3,4,5,6,7,8,9,A,B,C,D,E,F 十六进制: 5.8 —> 5 × 16# + 8 × 16%” = 5.5

各种进制的常见书写方式

在这里插入图片描述

二进制优越性

①可使用两个稳定状态的物理器件表示
②0,1 正好对应逻辑值 假、真。方便实现逻辑运算
③可很方便地使用逻辑门电路实现算术运算

任意进制→十进制

采用r 进制计数法每个位数的基数×该进制的位权次幂依次相加就可以啦

r 进制计数法

在这里插入图片描述
==基数==:每个数码位所用到的不同符号的个数,r 进制的基数为 r

举例

进制 计算举例
二进制:1 0 0 1 0 0 1 0 . 1 1 0 1 * 2^7 + 1 * 2^4 + 1 * 2^1 + 1 * 2^-1 + 1 * 2^-2 = 146.75
八进制:251.5 2 * 8^2 + 5 * 8^1 + 1 * 8^0 + 5 * 8^-1 = 168.625
十六进制:AE86.1 10 * 16^3 + 14 * 16^2 + 8 * 16^1 + 6 * 16^0 + 1 * 16^-1 = 44678.0625

大家也可以记住常见的二进制各个位的十进制值(右击保存图片收藏)
在这里插入图片描述

二进制↔八进制、十六进制

二进制 —> 八进制

3位一组,毎组转换成对应的八进制符号
在这里插入图片描述

八进制—> 二进制

每位八进制对应的3位二进制
在这里插入图片描述

二进制 —> 十六进制

4位一组,毎组转换成对应的十六进制符号
在这里插入图片描述

二进制 —> 十六进制

4位一组,毎组转换成对应的十六进制符号

在这里插入图片描述

十进制→任意进制

除 x 取余倒排法(x 代表进制数)

完整内容可以参考这个回答:zhidao.baidu.com/question/37…
在这里插入图片描述
如:75.3 小数部分=0.3
在这里插入图片描述
那么我们转化成二进制就是这样

  • 整数部分
    在这里插入图片描述- 小数部分
    在这里插入图片描述

十进制→二进制(拼凑法)

用这个图直接凑
在这里插入图片描述

真值和机器数

  • 真值:符合人类习惯的数字
  • 机器数:数字实际存到机器里的形式,正负号需要被“数字化”

例如下面两个数
在这里插入图片描述

定点数与浮点数的举例

举例
定点数:小数点的位置固定 Eg:996.007 ——常规计数
浮点数:小数点的位置不固定 Eg:9.96007*102 ——科学计数法

无符号数

概念

==无符号数==:整个机器字长的全部二进制位均为数值位,没有符号位,相当于数的绝对值。

例如:
在这里插入图片描述

表示范围

8位二进制数有 2^8 种不同的状态
在这里插入图片描述
n位的无符号数表示范围为:0 ~ 2^n -1

有符号数

有符号数的表示

例如
在这里插入图片描述
但是这样小数点的位置会不固定,我们在面对所有数据都不固定小数位的时候我们的心情会是这样

在这里插入图片描述
所以就有了有符号数的定点表示的规定

有符号数的定点表示

定点整数

在这里插入图片描述

定点小数

在这里插入图片描述

注意:

  • 可用 原码、反码、补码 三种方式来表示定点整数和定点小数。
  • 还可用 移码 表示定点整数。
  • 若真值为 x,则用 [x]原、[x]反、[x]补、[x]移 分别表示真值所对应的原码、反码、补码、移码

原码、反码、补码、移码

原码

在这里插入图片描述

反码

  • 若符号位为==0==,则==反码与原码相同==
  • 若符号位为==1==,则==数值位全部取反==

补码

  • 正数的补码 = 原码
  • 负数的补码 = 反码末位+1(要考虑进位)
  • 将负数补码转回原码的方法相同:尾数取反,末位+1

移码

  • 移码: 补码的基础上将符号位取反。

注意:移码只能用于表示整数

用几种码表示定点整数

在这里插入图片描述

各种码的真值0

[+0] [-0]
原码 [+0]原=00000000 [-0]原=10000000
反码 [+0]反=00000000 [-0]反=11111111
补码 [+0]补= [-0]补= 00000000 [+0]补= [-0]补= 00000000
反码 [+0]移= [-0]移= 10000000 [+0]移= [-0]移= 10000000

各种码转换图

在这里插入图片描述

各种码表示范围

在这里插入图片描述

算数移位运算

移位:通过改变各个数码位和小数点的相对位置,从而改变各数码位的位权。可用移位运算实现乘法、除法

原码的算数移位

例如原码为 10101000 进行算数移位
在这里插入图片描述
原码的算数移位——==符号位保持不变,仅对数值位进行移位。==

  • 右移:高位补0,低位舍弃。若舍弃的位=0,则相当于÷2;若舍弃的位≠0,则会丢失精度
  • 左移:低位补0,高位舍弃。若舍弃的位=0,则相当于×2;若舍弃的位≠0,则会出现严重误差

反码的算数移位

反码的算数移位——==正数的反码与原码相同,因此对正数反码的移位运算也和原码相同。==

  • 右移:高位补0,低位舍弃。
  • 左移:低位补0,高位舍弃

反码的算数移位——==负数的反码数值位与原码相反,因此负数反码的移位运算规则如下,==

  • 右移:高位补1,低位舍弃。
  • 左移:低位补1,高位舍弃。

补码的算数移位

补码的算数移位——==正数的补码与原码相同,因此对正数补码的移位运算也和原码相同。==

  • 右移:高位补0,低位舍弃。
  • 左移:低位补0,高位舍弃。

补码的算数移位——==负数补码===反码末位+1 导致反码最右边几个连续的1都因进位而变为0,直到进位碰到第一个0为止。

规律——==负数补码中,最右边的1及其右边同原码。最右边的1的左边同反码==
负数补码的算数移位规则如下:

  • 右移(同反码):高位补1,低位舍弃。
  • 左移(同原码):低位补0,高位舍弃。

逻辑移位

在这里插入图片描述

  • 逻辑右移:高位补0,低位舍弃。
  • 逻辑左移:低位补0,高位舍弃。

可以把逻辑移位看作是对“无符号数”的算数移位

逻辑移位的应用举例

例如颜色RGB分别存储的数据为:
R = 102 01100110
G = 139 10001011
B = 139 10001011

需要将三个灰度值合成一个才能成彩色图像
在这里插入图片描述

循环移位

在这里插入图片描述

原码的加减运算

原码的加法运算:

  1. 正+正 → 绝对值做加法,结果为正 (可能会溢出)
  2. 负+负 → 绝对值做加法,结果为负 (可能会溢出)
  3. 正+负 → 绝对值大的减绝对值小的,符号同绝对值大的数
  4. 负+正 → 绝对值大的减绝对值小的,符号同绝对值大的数

原码的减法运算,“减数”符号取反,转变为加法:

  • 正-负 → 正+正
  • 负-正 → 负+负
  • 正-正 → 正+负
  • 负+正 → 负-负

补码的加减运算

对于补码来说,无论加法还是减法,最后都会转变成加法,由加法器实现运算,符号位也参与运算。

补充:

1. 求[-B]补

[-B]补 : [B]补连同符号位一起取反加1

2. 负数补 → 原:

①数值位取反+1;
②负数补码中,最右边的1及其右边同原码。最右边的1的左边同反码

例题

我们先看一道例题:设机器字长为8位(含1位符号位),A = 15,B = -24,求[A+B]补和[A−B]补

先将A B的原码补码都求出来
在这里插入图片描述

[A+B]补 = [A]补 + [B]补 = 0,0001111 + 1,1101000 = 1,1110111
原码:1,0001001 真值-9

[A-B]补 = [A]补 + [-B]补 = 0,0001111 + 0,0011000 = 0,0100111
真值+39

我们将题改一下:
其中 C = 124,求[A+C]补和[B−C]补,按照上面方法求出可得:
[A+C]补 = 0,0001111 + 0,1111100 = 1,0001011 真值-117 溢出(实际应该是139,但是溢出后是 -117)
[B−C]补 = 1,1101000 + 1,0000100 =0,1101100 真值+108

溢出判断

溢出条件

  • 只有“正数+正数 ”才会上溢 —— 正+正=负
  • 只有“负数+负数 ”才会下溢 —— 负+负=正

溢出判断:采用双符号位

正数符号为00,负数符号为11
[A+C]补 = 00,0001111 + 00,1111100 = ==01==,0001011 上溢
[B−C]补 = 11,1101000 + 11,0000100 = ==10==,1101100 下溢

记两个符号位为S1 S2 ,则V=S1异或S2

  • 若V=0,表示无溢出;
  • 若V=1,表示有溢出。

乘法运算的思想

手算乘法(二进制)

例如: 算 0.1101×0.1011

列竖式

在这里插入图片描述

移位运算

在这里插入图片描述

原码的一位乘法

补充:运算器相关知识

运算器:用于实现算术运算(如:加减乘除)、逻辑运算(如:与或非)

在这里插入图片描述

  • ACC: 累加器,用于存放操作数,或运算结果。
  • MQ: 乘商寄存器,在乘、除运算时,用于存放操作数或运算结果。
  • X: 通用的操作数寄存器,用于存放操作数
  • ==ALU==: 算术逻辑单元,通过内部复杂的电路实现算数运算、逻辑运算
加 减 乘 除
ACC 被加数、和 被减数、差 乘积高位 被除数、余数
MQ 乘数、乘积低位 商
X 加数 减数 被乘数 除数

原码一位乘法实现方法:先加法再移位,重复n次

符号位通过==异或==确定;数值部分通过被乘数和乘数绝对值的 n 轮加法、移位完成根据当前乘数中参与运算的位确定(ACC)加什么。

  1. 若当前运算位 =1,则(ACC)+[|x|]原;
  2. 若当前运算位 =0,则(ACC)+0。

每轮加法后ACC、MQ的内容统一逻辑右移

手算模拟

tips

  • 乘数的符号位不参与运算,可以省略
  • 原码一位乘可以只用单符号位
  • 答题时最终结果最好写为原码机器数

例题

设机器字长为5位(含1位符号位,n=4),x = −0.1101,y = +0.1011,采用原码一位乘法求x·y

解:手动计算是这样
在这里插入图片描述
符号位:1与0进行异或运算,得0。

所以随后结果是:==x·y= -0.10001111==
在这里插入图片描述

补码的一位乘法(Booth算法)

  1. 进行 n 轮加法、移位,最后再多来一次加法
  2. 每次加法可能 +0 、+[x]补、+[-x]补
  3. 每次移位是“补码的算数右移”
  4. 符号位参与运算

在第二个步骤中,需要根据MQ中的最低位、辅助位 来确定加什么:

  • 辅助位 - MQ中最低位 = 1时,(ACC)+[x]补
  • 辅助位 - MQ中最低位 = 0时,(ACC)+0
  • 辅助位 - MQ中最低位 = -1时,(ACC)+[-x]补

手算模拟

例题

设机器字长为5位(含1位符号位,n=4),x = −0.1101,y = +0.1011,采用Booth算法求x·y

解:手动计算是这样
在这里插入图片描述
最后得 [x·y]补 = 11.01110001
即x·y = −0.10001111

做题总结

  1. n轮加法、算数右移,加法规则如下:
    辅助位 - MQ中最低位 = 1时,(ACC)+[x]补
    辅助位 - MQ中最低位 = 0时,(ACC)+0
    辅助位 - MQ中最低位 = -1时,(ACC)+[-x]补
  2. 补码的算数右移:
    符号位不动,数值位右移,正数右移补0,
    负数右移补1(符号位是啥就补啥)
  3. 一般来说,Booth算法的被乘数、部分积采用双符号位补码

原码,补码一位乘法的对比

原码一位乘法: 补码一位乘法:
进行 n 轮加法、移位 进行 n 轮加法、移位,最后再多来一次加法
每次加法可能 +0 、`+[ x
每次移位是“逻辑右移” 每次移位是“补码的算数右移”
符号位不参与运算 符号位参与运算

原码除法:恢复余数法(了解,不考)

思路图(打字打不清楚了是)
在这里插入图片描述

补充知识:大小端模式与边界对其

大小端模式

大家一定知道:多字节数据在内存里一定是占连续的几个字节

最高有效字节我们用MSB表示
最低有效字节我们用LSB表示

例如
在这里插入图片描述

  • 大端模式更便于人类阅读
    在这里插入图片描述
  • 小端模式更便于便于机器处理

在这里插入图片描述

边界对齐

现代计算机通常是按字节编址,即每个字节对应1个地址
通常也支持按字、按半字、按字节寻址。
假设存储字长为32位,则1个字=32bit,半字=16bit。

每次访存只能读/写1个字

  1. 下面是边界对其方式:不够四字节的会填充空的
    在这里插入图片描述
  2. 下面是不对齐方式,不够四字节的不填充在这里插入图片描述

浮点数的表示

定点数:如纯小数0.1011和纯整数11110

浮点数表示形式

在这里插入图片描述

==阶码==:常用补码或移码表示的定点整数
==尾数==:常用原码或补码表示的定点小数

浮点数的真值:

在这里插入图片描述
阶码E反映浮点数的表示范围及小数点的实际位置;
尾数M的数值部分的位数n反映浮点数的精度。

举个栗子

例题:阶码、尾数均用补码表示,求a、b的真值
a = 0,01;1.1001
b = 0,10;0.01001

解:
a: 阶码0,01对应真值+1
尾数1.1001对应真值-0.0111

a的真值 = 2^1^×(−0.0111) = −0.111
==(相当于尾数表示的定点小数算数左移一位,或小数点右移一位)==

b: 阶码0,10对应真值+2
尾数0.01001对应真值+0.01001

b的真值 = 2^2^×(+0.01001) = +1.001
==(相当于尾数表示的定点小数算数左移2位,或小数点右移2位)==

浮点数尾数的规格化

规格化浮点数:规定尾数的最高数值位必须是一个有效值 。

左归与右归

  • 左规:当浮点数运算的结果为非规格化时要进行规格化处理, 将尾数算数左移一位,阶码减1。
  • 右规:当浮点数运算的结果尾数出现溢出(双符号位为01或10)时, 将尾数算数右移一位,阶码加1。

说白了就是:

  • 左归就是通过算数左移、阶码减1 来规格化
  • 右归就是通过算数右移、阶码加1 来规格化

例题:浮点数加法

例:a = 010;00.1100,b = 010;00.1000,求a+b

解:a = 2^2^×00.1100 ,b = 2^2^×00.1000
a+b
= 2^2^×00.1100 + 2^2^×00.1000
= 2^2^×(00.1100 + 00.1000)
= 2^2^×01.0100
= 2^3^×00.1010

(注:采用“双符号位” ,当溢出发生时,可以挽救。更高的符号位是正确的符号位)

规格化浮点数的特点

  1. 用原码表示的尾数进行规格化:

  • 正数为0.1××…×的形式,其最大值表示为0.11…1;最小值表示为0.10…0。
    尾数的表示范围为1/2≤M≤(1−2^−n^)。
  • 负数为1.1××…×的形式,其最大值表示为1.10…0;最小值表示为1.11…1。
    尾数的表示范围为−(1−2^−n^)≤M≤−1/2。
  1. 用补码表示的尾数进行规格化:

  • 正数为0.1××…×的形式,其最大值表示为0.11…1;最小值表示为0.10…0。
    尾数的表示范围为1/2≤M≤(1−2−n)。
  • 负数为1.0××…×的形式,其最大值表示为1.01…1;最小值表示为1.00…0。
    尾数的表示范围为−1≤M≤−(1/2+2−n)
  1. 表示范围

在这里插入图片描述

  1. 注意事项(※)

  1. 规格化的原码尾数,最高数值位一定是1
  1. 规格化的补码尾数,符号位与最高数值位一定相反
  1. 补码算数左移,低位补0;补码算数右移,高位补1

浮点数的加减运算

我们可以先通过十进制的浮点数加减运算步骤来类推二进制的

十进制浮点数加减运算步骤:

浮点数加减运算包括五个步骤:① 对阶② 尾数加减③ 规格化④ 舍入⑤ 判溢出

例如:计算9.85211 × 10^12^ + 9.96007 × 10^10^

解:在这里插入图片描述

二进制浮点数的加减运算

上面我们进行了十进制的浮点数的加减运算,下面我们可以以此类推,也按照上面五个步骤来做

直接看一个例题:已知十进制数X=−5/256、Y=+59/1024,按机器补码浮点运算规则计算X−Y,结果用二进制表示,浮点数格式如下:==阶符取2位,阶码取3位,数符取2位,尾数取9位==

解:

首先我们先用补码表示阶码和尾数,

5D = 101B,1/256 = 2^-8^ → X = - 101 × 2^-8^ = - 0.101 × 2^-5^ = - 0.101 × 2^-101^
59D = 111011B,1/1024 = 2^-10^ → Y = + 111011 × 2^-10^ = + 0.111011 × 2^-4^ = + 0.111011 × 2^-100^

再转化成补码形式
X:11011,11.011000000
==(X是负数 转化成补码取反+1 阶码 尾数都一样操作)==
Y:11100,00.111011000

1. 对阶

使两个数的阶码相等,小阶向大阶看齐,尾数毎右移一位,阶码加1

① 求阶差:[ΔE]补=11011+00100=11111,知ΔE=−1
② 对阶:
X:11011,11.011000000 → 11100,11. 101100000
X = - 0.0101 × 2^-100^

2. 尾数加减

-Y:11100,11.000101000
==(求码的负数的方法:连符号位一块取反+1)==

然后让X加上-Y

1
2
3
c复制代码	11.101100000
+ 11.000101000
10.110001000

所以X-Y:11100, 10.110001000

3. 规格化

X-Y:11100, 10.110001000 à 11101,11.011000100

4. 舍入

无舍入

5. 判溢出

常阶码,无溢出,结果真值为2−3×(−0.1001111)2

浮点数的加减运算——舍入规则

“0”舍“1”入法:

类似于十进制数运算中的“四舍五入”法,即在尾数右移时,被移去的最高数值位为0,则舍去;被移去的最高数值位为1,则在尾数的末位加1。这样做可能会使尾数又溢出,此时需再做一次右规。

恒置“1”法:

尾数右移时,不论丢掉的最高数值位是“1”还是“0”,都使右移后的尾数末位恒置“1”。这种方法同样有使尾数变大和变小的两种可能。

例如
在这里插入图片描述

强制类型转换

在这里插入图片描述

转化的可操作性

char → int → long → double
float → double
int → float:可能损失精度
float → int:可能溢出及损失精度

结论:范围、精度从小到大,转换过程没有损失

原因:拿32位来说:
int:表示整数,范围 -2^31^ ~ 2^31^-1 ,有效数字32位
float:表示整数及小数,范围 ±[2^-126^ ~ 2^127^×(2−2^−23^)],有效数字23+1=24位

奇偶校验码

概念

由若干位代码组成的一个字叫==码字==。
将两个码字逐位进行对比,具有不同的位的个数称为==两个码字间的距离==。
一种编码方案可能有若干个合法码字,各合法码字间的最小距离称为“==码距==”。
例如:下面两组的码距分别是1和2

在这里插入图片描述
其中==码距==的能力范围是:

  • 当d=1时,无检错能力;
  • 当d=2时,有检错能力;
  • 当d≥3时,若设计合理,可能具有检错、纠错能力

奇偶校验码

  • 奇校验码:整个校验码(有效信息位和校验位)中“1”的个数为奇数。
  • 偶校验码:整个校验码(有效信息位和校验位)中“1”的个数为偶数。

在这里插入图片描述

例1: 给出两个编码1001101和1010111的奇校验码和偶校验码。
设最高位为校验位,余7位是信息位,则对应的奇偶校验码为:
奇校验: 11001101 01010111
偶校验: 01001101 11010111

1
0

例2: 给出两个编码1001101和1010111的奇校验码和偶校验码。
设最高位为校验位,余7位是信息位,则对应的奇偶校验码为:
奇校验: 11001101 01010111
偶校验: 01001101 11010111

偶校验的硬件实现:各信息进行异或(模2加)运算,得到的结果即为偶校验位

在这里插入图片描述
例如:将上述例子求偶校验位:
在这里插入图片描述

偶数个错误校验不出

例如
在这里插入图片描述

总结

在这里插入图片描述

三、主存储器

保留,我貌似没学过,不晓得,可能和后面存储系统混了,先看看书

四、指令系统

指令案例文章(必看):editor.csdn.net/md/?article…

现代计算机的结构

在这里插入图片描述
这次就开搞控制器!

学会指令系统就可以更精进之前搞的典型过程了:yangyongli.blog.csdn.net/article/det…

指令格式

指令的定义

==指令(又称机器指令)==:是指示计算机执行某种操作的命令,是计算机运行的最小功能单位。

一台计算机的所有指令的集合构成该机的指令系统,也称为==指令集==。

注:一台计算机只能执行自己指令系统中的指令,不能执行其他系统的指令。

例如:x86 架构、ARM架构之间不能互相执行对方架构系统的指令。

指令格式

一条指令就是机器语言的一个语句,它是一组有意义的二进制代码。

一条指令通常要包括操作码字段和地址码字段两部分(如下图所示):
在这里插入图片描述

==操作码==就是要表达用户要干什么?
比如:停机中断、求反求补、加减乘除……

==地址码==就是要说明对谁进行操作?
比如:不需要操作对象、需要一个操作对象、需要两个操作对象……


其中 一条指令可能包含 0个、1个、2个、3个、4个 地址码…

根据地址码数目不同,可以将指令分为 零地址指令、一地址指令、二地址指令…

零地址指令

在这里插入图片描述

  1. 不需要操作数,如空操作、停机、关中断等指令
  2. 堆栈计算机,两个操作数隐含存放在栈顶和次栈顶,计算结果压回栈顶

一地址指令

在这里插入图片描述

  1. 只需要单操作数,如加1、减1、取反、求补等
    指令含义:OP(A1)→A1 ,完成一条指令需要3次访存:取指→ 读A1 →写A1
  2. 需要两个操作数,但其中一个操作数隐含在某个寄存器(如隐含在ACC)
    指令含义: (ACC)OP(A1)→ACC,完成一条指令需要2次访存:取指→ 读A1

注:A1 指某个主存地址, (A1)表示 A1 所指向的地址中的内容

二、三地址指令

在这里插入图片描述
==常用于需要两个操作数的算术运算、逻辑运算相关指令==

指令含义:(A1)OP(A2)→A1
完成一条指令需要访存4次,取指→读A1→读A2→写A1

在这里插入图片描述
==常用于需要两个操作数的算术运算、逻辑运算相关指令==

指令含义:(A1)OP(A2)→A3
完成一条指令需要访存4次,取指→ 读A1→读A2 →写A3

四地址指令

在这里插入图片描述
指令含义:(A1)OP(A2)→A3,A4=下一条将要执行指令的地址

完成一条指令需要访存4次,取指 →读A1 →读A2 →写A3

正常情况下:取指令之后 PC+1,指向下一条指令
四地址指令:执行指令后,将PC的值修改位 A4 所指地址

小杨同学表示:事真多!
在这里插入图片描述

地址码的位数有什么影响?

n位地址码的直接寻址范围=2^n^,==若指令总长度固定不变,则地
址码数量越多,寻址能力越差==

分类

指令-按地址码数目分类

在这里插入图片描述

指令-按指令长度分类

可以分为:半字长指令、单字长指令、双字长指令 ——指令长度是机器字长的多少倍

==指令字长==:一条指令的总长度(可能会变)
==机器字长==:CPU进行一次整数运算所能处理的二进制数据的位数(通常和ALU直接相关)
==存储字长==:一个存储单元中的二进制代码位数(通常和MDR位数相同)

指令字长会影响取指令所需时间。如:机器字长=存储字长=16bit,则取一条双字长指令需要两次访存

定长指令字结构:指令系统中所有指令的长度都相等
变长指令字结构:指令系统中各种指令的长度不等

指令-按操作码长度分类

  1. 定长操作码:指令系统中所有指令的操作码长度都相同(n位 → 2^n^条指令)
    ——控制器的译码电路设计简单,但灵活性较低
  2. 可变长操作码:指令系统中各指令的操作码长度可变
    ——控制器的译码电路设计复杂, 但灵活性较高
  3. 扩展操作码指令格式:定长指令字结构+可变长操作码

指令—按操作类型分类

  1. 数据传送(==数据传送类:进行主存与CPU之间的数据传送==)
    LOAD 作用:把存储器(源)中的数据放到寄存器(目的)中
    STORE 作用:把寄存器(源)中的数据放到存储器(目的)中
  2. 算术逻辑操作
    算术:加、减、乘、除、增 1、减 1、求补、浮点运算、十进制运算
    逻辑:与、或、非、异或、位操作、位测试、位清除、位求反
  3. 移位操作
    算术移位、逻辑移位、循环移位(带进位和不带进位)
  4. 转移操作(==程序控制类:改变程序执行的顺序==)
    无条件转移 JMP
    条件转移 JZ:结果为0;JO:结果溢出;JC:结果有进位
    调用和返回 CALL和RETURN
    陷阱(Trap)与陷阱指令
  5. 输入输出操作(==输入输出类(I/O):进行CPU和I/O设备之间的数据传送==)
    CPU寄存器与IO端口之间的数据传送(端口即IO接口中的寄存器)

指令格式小结

在这里插入图片描述

扩展操作码

指令由操作码和若干个地址码组成。

PS:先回顾一下指令字结构与操作码的概念:

  • 定长指令字结构:指令系统中所有指令的长度都相等
  • 变长指令字结构:指令系统中各种指令的长度不等

  • 定长操作码:指令系统中所有指令的操作码长度都相同
  • 可变长操作码:指令系统中各指令的操作码长度可变

定长指令字结构+可变长操作码 → 扩展操作码指令格式(==即不同地址数的指令使用不同长度的操作码==)

扩展操作码举例

这只是一种设计方法:
在这里插入图片描述

设计扩展操作码需注意:

  1. 不允许短码是长码的前缀,即短操作码不能与长操作码的前面部分的代码相同。(==对比哈夫曼树“前缀编码”==)
  2. 各指令的操作码一定不能重复。

通常情况下,对使用频率较高的指令,分配较短的操作码;对使用频率较低的指令,分配较长的操作码,从而尽可能减少指令译码和分析的时间。

设计扩展操作码例题:

设指令字长固定为16位,试设计一套指令系统满足:
a) 有15条三地址指令
b) 有12条二地址指令
c) 有62条一地址指令
d) 有32条零地址指令

设地址长度为n,上一层留出m种状态,下一层可扩展出m×2!种状态

解:
a) 共2^4^=16种状态
留出16-15=1种

b) 共1 ×2^4^=16种
留出16-12=4种

c) 共4 ×2^4^=64种
留出64-62=2种

d) 共2 ×2^4^=32种

0000 -1110 A1(取的合法范围) A2 A3
1111 XXXX XXXXXXXX 1111 0000 -1011 A1 A2
1111 11XX XXXX XXXX 1111 1100 –1110 1111 0000 –1111 0000 –1101 A1
1111 1111 111X XXXX 1111 1111 1110 –1111 0000 -1111

指令操作码

==操作码==指出指令中该指令应该执行什么性质的操作和具有何种功能。

==操作码==是识别指令、了解指令功能与区分操作数地址内容的组成和使用方法等的==关键信息==。
例如,指出是算术加运算,还是减运算;是程序转移,还是返回操作。

操作码分类:

定长操作码:

==在指令字的最高位部分分配固定的若干位(定长)表示操作码。==

  • 一般n位操作码字段的指令系统最大能够表示2^n^条指令。
  • 优:定长操作码对于简化计算机硬件设计,提高指令译码和识别速度很有利; - 缺:指令数量增加时会占用更多固定位,留给表示操作数地址的位数受限。

扩展操作码(不定长操作码) :

==全部指令的操作码字段的位数不固定,且分散地放在指令字的不同位置上。==

  • 最常见的变长操作码方法是扩展操作码,使操作码的长度随地址码的减少而增加,不同地址数的
    指令可以具有不同长度的操作码,从而在满足需要的前提下,有效地缩短指令字长。
  • 优: 在指令字长有限的前提下仍保持比较丰富的指令种类;
  • 缺 :增加了指令译码和分析的难度,使控制器的设计复杂化。

首先 ,我们还得先回忆一下计算机的工作过程:yangyongli.blog.csdn.net/article/det…
在这里插入图片描述

指令寻址

==指令寻址==: ==下一条== 欲执行 ==指令== 的 ==地址==(始终由程序计数器PC给出)

即( PC ) + 1→ PC,如下面图片的例子
在这里插入图片描述

该系统采用==定长指令字结构==
指令字长=存储字长=16bit=2B(地址为16位)
主存==按字编址==

我们将上面例子拆分为: 指令地址、操作码、地址码。如下图形式

在这里插入图片描述

顺序寻址

( PC ) + “1” → PC

跳跃寻址

由转移指令指出

JMP:无条件转移把PC中的内容改成地址码数值

O(∩_∩)O哈哈~

例如在前面的例子中
在这里插入图片描述

小结

在这里插入图片描述

指令寻址 v.s. 数据寻址

寻址方式不同

在这里插入图片描述

寻址方式特征包括:(十种)

在这里插入图片描述

数据寻址

我们在原有的寻址方式上加上四个bit位,也就是==寻址特征==(告诉后面的形式地址用什么方式来解读),就构成了==数据寻址==。

一地址指令形式:

在这里插入图片描述
最终解读完后得到一条有效地址(求出操作数的真实地址,称为==有效地址(EA)==。)

二地址指令形式:

在这里插入图片描述

以下寻址方式前提:假设指令字长=机器字长=存储字长,假设操作数为3

十种寻址方式

直接寻址

在这里插入图片描述

间接寻址

在这里插入图片描述

寄存器寻址

在这里插入图片描述

寄存器间接寻址

在这里插入图片描述

隐含寻址

在这里插入图片描述

立即寻址

在这里插入图片描述

偏移寻址

内容太多:请移驾另一篇博客:
yangyongli.blog.csdn.net/article/det…

堆栈寻址

内容太多:请移驾另一篇博客:
yangyongli.blog.csdn.net/article/det…

数据寻址小结

在这里插入图片描述

CISC和RISC对比(整的少,要高分看书上的背)

CISC: Complex Instruction Set Computer RISC: RISC: Reduced Instruction Set Computer
类比 有很多库函数的C语言 没有库函数的C语言
设计思路 一条指令完成一个复杂的基本功能。 一条指令完成一个基本“动作”;多条指令组合完成一个复杂的基本功能。
CISC的思路:除了提供整数的加减乘指令除之外,还提供矩阵的加法指令、矩阵的减法指令、矩阵的乘法指令 RISC的思路:只提供整数的加减乘指令
代表 x86架构,主要用于笔记本、台式机等 ARM架构,主要用于手机、平板等
指令与电路 一条指令可以由一个专门的电路完成 一条指令一个电路,电路设计相对简单,功耗更低
实现 有的复杂指令用纯硬件实现很困难 → 采用“存储程序”的设计思想,由一个比较通用的电路配合存储部件完成一条指令 “并行”、“流水线”

将上面分的更细一下,如下表格
在这里插入图片描述

举个栗子

举个栗子,拿之前的计算机工作原理图来说,忘了的小伙伴可以点击下面链接再看一下:
blog.csdn.net/weixin_4552…
在这里插入图片描述
其中,乘法指令可以访存,一定是CISC
在这里插入图片描述

五、中央处理器

在这里插入图片描述

CPU的功能

  1. 指令控制。完成取指令、分析指令和执行指令的操作,即程序的顺序控制。
  2. 操作控制。一条指令的功能往往是由若干操作信号的组合来实现的。CPU管
    理并产生由内存取出的每条指令的操作信号,把各种操作信号送往相应的部件,
    从而控制这些部件按指令的要求进行动作。
  3. 时间控制。对各种操作加以时间上的控制。时间控制要为每条指令按时间
    顺序提供应有的控制信号。
  4. 数据加工。对数据进行算术和逻辑运算。
  5. 中断处理。对计算机运行过程中出现的异常情况和特殊请求进行处理。

运算器和控制器的功能

  • ==运算器==:对数据进行加工
  • ==控制器==
    协调并控制计算机各部件执行程序的指令序列,
    基本功能包括取指令、分析指令、执行指令
+ 取指令:自动形成指令地址;自动发出取指令的命令。
+ 分析指令:操作码译码(分析本条指令要完成什么操作);
+ 产生操作数的有效地址。
+ 执行指令:根据分析指令得到的“操作命令”和“操作数地址”,形成操作信号控制序列,控制运算器、存储器以及I/O设备完成相应的操作。
+ 中断处理:管理总线及输入输出;处理异常情况(如掉电)和特殊请
求(如打印机请求打印一行字符)

更形象点,如下图:
在这里插入图片描述

运算器的基本结构

运算器包括:==算术逻辑单元与通用寄存器组。==

  1. ==算术逻辑单元==:主要功能是进行算术/逻辑运算。
  2. ==通用寄存器组==:如AX、BX、CX、DX、SP等,用于存放操作数(包括源操作数、目的操作数及中间结果)和各种地址信息等。SP是堆栈指针,用于指示栈顶的地址。

例如下图:(当然没有这么简单,这只是个概图,肯定不是这么简单的了,大家可以先参考一下)

在这里插入图片描述

专用数据通路方式:根据指令执行过程中的数据和地址的流动方向安排连接线路。

问题探究

如果按照上图直接用导线连接,相当于多个寄存器同时并且一直向ALU传输数据???

那肯定是不行的呀,这样数据不就传输混乱的嘛,那么我们怎样解决呢?

解决方法1. 使用多路选择器

根据控制信号选择一路输出
在这里插入图片描述

解决方法2. 使用三态门

可以控制每一路是否输出
如:

1
2
c复制代码R0out为1时R$中的数据输出到A端,
R0out为0时R$中的数据无法输出到A端

优缺点:性能较高,基本不存在数据冲突现象,但结构复杂,硬件量大,不易实现。

运算器真正的基本结构

CPU采用内部单总线方式:==将所有寄存器的输入端和输出端都连接到一条公共的通路上。==

在这里插入图片描述

  1. ==算术逻辑单元==:主要功能是进行算术/逻辑运算。
  2. ==通用寄存器组==:如AX、BX、CX、DX、SP等,用于存放操作数(包括源操作数、目的操作数及中间结果)和各种地址信息等。SP是堆栈指针,用于指示栈顶的地址。
  3. ==暂存寄存器==:用于暂存从主存读来的数据,这个数据不能存放在通用寄存器中,否则会破坏其原有内容。
  4. ==累加寄存器==:它是一个通用寄存器,用于暂时存放ALU运算的结果信息,用于实现加法运算。
  5. ==程序状态字寄存器==:保留由算术逻辑运算指令或测试指令的结果而建立的各种状态信息,如溢出标志(OP)、符号标志(SF)、零标志(ZF)、进位标志(CF)等。PSW中的这些位参与并决定微操作的形成。
  6. ==移位器==:对运算结果进行移位运算。
  7. ==计数器==:控制乘除运算的操作步数。

优缺点

结构简单,容易实现,但数据传输存在较多冲突的现象,性能较低。

控制器的基本结构

在这里插入图片描述

  1. 程序计数器:用于指出下一条指令在主存中的存放地址。CPU就是根据PC的内容去主存中取指令的。因程序中指令(通常)是顺序执行的,所以PC有自增功能。
  2. 指令寄存器:用于保存当前正在执行的那条指令。
  3. 指令译码器:仅对操作码字段进行译码,向控制器提供特定的操作信号。
  4. 微操作信号发生器:根据IR的内容(指令)、PSW的内容(状态信息)及时序信号,产生控制整个计算机系统所需的各种控制信号,其结构有组合逻辑型和存储逻辑型两种。
  5. 时序系统:用于产生各种时序信号,它们都是由统一时钟(CLOCK)分频得到。
  6. 存储器地址寄存器:用于存放所要访问的主存单元的地址。
  7. 存储器数据寄存器:用于存放向主存写入的信息或从主存中读出的信息。

我们上面运算器与控制器合起来,构成一个CPU。

CPU整体的基本结构

在这里插入图片描述
我们将标注上ALU 寄存器 CU 中断系统后是这个样子。
在这里插入图片描述

小结

在这里插入图片描述

指令周期

==指令周期==:CPU从主存中每取出并执行一条指令所需的全部时间。
指令周期常常用若干机器周期来表示,机器周期又叫CPU周期

一个机器周期又包含若干时钟周期(也称为节拍、T周期或CPU时钟周期,它是CPU操作的最基本单位)。

每个指令周期内机器周期数可以不等,每个机器周期内的节拍数也可以不等。如下图可分为定长的机器周期与不定长的机器周期。
CLK:时钟脉冲
在这里插入图片描述

几种常见的指令周期

每个指令周期内机器周期数可以不等,每个机器周期内的节拍数也可以不等。

在这里插入图片描述

指令周期流程

在这里插入图片描述
四个工作周期都有CPU访存操作,只是访存的目的不同。

  1. 取指周期是为了==取指令==
  2. 间址周期是为了==取有效地址==
  3. 执行周期是为了==取操作数==
  4. 中断周期是为了==保存程序断点。==

这四个周期在计算机内部是用触发器控制的

触发器,可以存放1个二进制位。

CLK(时钟脉冲)通过判断四个触发器的状态来判断该指令执行的是哪个周期。具体状态判断如下图
在这里插入图片描述

指令周期的数据流

取指周期

取指周期步骤:

  1. 当前指令地址送至存储器地址寄存器,
    记做:(PC) → MAR
  2. 将MAR所指主存中的内容经数据总线
    送入MDR,记做:M(MAR) → MDR
  3. CU发出控制信号,经控制总线传到主
    存,这里是读信号,记做:1 → R
  4. 将MDR中的内容(此时是指令)送入IR,
    记做:(MDR) → IR
  5. CU发出控制信号,形成下一条指令地
    址,记做:(PC)+1 → PC

具体数据流向图如下图所示:
在这里插入图片描述

间址周期

间址周期步骤:

  1. 将指令的地址码送入MAR,
    记做:Ad(IR) → MAR
    或Ad(MDR) → MAR
  2. CU发出控制信号,启动主存做读操作,
    记做:1 → R
  3. 将MAR所指主存中的内容经数据总线
    送入MDR,记做:M(MAR) → MDR
  4. 将有效地址送至指令的地址码字段,
    记做:(MDR)→ Ad(IR)

具体数据流向图如下图所示:
在这里插入图片描述

执行周期

执行周期的任务是根据IR中的指令字的操作码和操作数通过ALU操作产生执行结果。

不同指令的执行周期操作不同,因此没有统一的数据流向。

中断周期

==中断==:暂停当前任务去完成其他任务。

为了能够恢复当前任务,需要保存断点。
一般使用堆栈来保存断点,这里用SP表示栈顶地址,假设SP指向栈顶元素,进栈操作是先修改指针,后存入数据。

中断周期步骤:

  1. CU控制将SP减1,修改后的地址送入MAR
    记做: (SP)-1 → SP,(SP) → MAR
    本质上是将断点存入某个存储单元,假设其
    地址为a,故可记做:a → MAR
  2. CU发出控制信号,启动主存做写操作,
    记做:1 → W
  3. 将断点(PC内容) 送入MDR,
    记做:(PC) → MDR
  4. CU控制将中断服务程序的入口地址
    (由向量地址形成部件产生)送入PC,
    记做:向量地址→ PC

具体数据流向图如下图所示:
在这里插入图片描述

指令执行方案

一个指令周期通常要包括几个时间段(执行步骤),每个步骤完成指令的一部分功能,几个依次执行的步骤完成这条指令的全部功能。

方案1.单指令周期

对所有指令都选用相同的执行时间来完成 。

==指令之间串行执行;==

指令周期取决于执行时间最长的指令的执行时间。

缺点:

对于那些本来可以在更短时间内完成的指令,要
使用这个较长的周期来完成,会降低整个系统的
运行速度。

方案2.多指令周期

对不同类型的指令选用不同的执行步骤来完成 。

==指令之间串行执行;==

可选用不同个数的时钟周期来完成不同指令的执行过程 。

缺点:

需要更复杂的硬件设计。

方案3.流水线方案

在每一个时钟周期启动一条指令,尽量让多条指令同时运行,但各自处在不同的执行步骤中 。

==指令之间并行执行==

例如这个样子
在这里插入图片描述

本章小结

在这里插入图片描述

微程序控制器

(如果我有时间,就整,建议看书,我的版本可能和学校教的不一样)

六、存储系统

都是概念,看书

七、IO(没学,不会)

本文转载自: 掘金

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

使用XXL-JOB时,如何避免多台服务器重复调度任务?|工作

发表于 2021-07-03

1.XXL-JOB介绍

  XXL-JOB是一个分布式任务调度平台,其核心设计目标是开发迅速、学习简单、轻量级、易扩展。支持通过Web页面对任务进行CRUD操作,操作简单,一分钟上手;支持动态修改任务状态、启动/停止任务,以及终止运行中任务,即时生效。

  官方地址中文版:www.xuxueli.com/xxl-job

  Gitee地址:gitee.com/xuxueli0323…

2.应用场景介绍

  我们将XXL-JOB调度系统部署在两台服务器实例上。使用XXL-JOB调度系统主要是执行各个业务系统的脚本,如发送短信、变更用户积分、生成BI报表等需求。

3.问题回顾

  job系统每天都正常的运行着,没发生过什么大问题。

  有天早上测试妹子向研发部门反馈问题:用户的每日积分、每周积分怎么变多了?刚好多了一倍,你们快看看吧,急!

  好家伙,这还了得,立马咨询坐在我旁边的大佬,看之前是否发生过这样的问题,大佬说很少发生,几乎没有。

  于是我找到处理每日积分的地方,发现有一个定时脚本在执行,每天晚上快凌晨时会执行一次,我随后登录job系统后台,查询该脚本,搜索该日期,发现了一个奇怪的问题,如图:

WX20210703-173403@2x.png

通过该截图发现了两个问题:

  1.该脚本任务在统一时间被调度了两次,为什么被调度了两次呢?因为我们有两台服务器实例,同时触发了调度任务。

  2.该脚本在间隔不到一分钟内重复执行了两次。

4.解决方案

  查看代码得知,这个脚本是每天只执行一次,但代码层面并没有判断是否已经执行过,多次调用就会多次计算。这就导致了计算数据时会出错,比如用户积分本应该减少10积分,由于调度系统调度了两次,这个用户的积分就减少了20积分。找到具体原因后就好解决了,其实就是避免脚本重复执行就可以了。

4.1使用XXL-JOB配置来避免重复调度(推荐)

  官网推荐的处理办法是利用“任务调度锁表“来避免集群同时调度的情况。

4.2在代码层面避免重复调度

方案1:

  在该脚本执行的地方判断下更新时间,判断今天是否已经执行过,这样不就可以搞定啦。
但这种处理会有另一个问题,还有一些脚本代码也没有判断是否已执行过。那需要修改的地方就很多了,需要向其他更便捷的办法。

方案2:

  先在配置文件中定义哪些脚本不能被重复调度、定义Redis过期时间。使用Redis记录该脚本已经执行过,第二次执行时判断是否已执行,如果已执行则退出执行流程。

实现流程:

1.先定义配置文件

1
2
3
4
5
6
7
8
9
10
11
12
php复制代码<?php  
return [
//定义延迟调用的接口
'job_delay_request_api_config' => [
'cache_time' => 1800,//过期时间 秒
'list' => [
'UserChangeLog/day',//每日积分统计
'UserChangeLog/week',//每周积分统计
],
],
];
?>

2.在父类控制器中判断脚本是否已执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
php复制代码<?php
public function __construct()
{
//获取当前调度的控制器和方法名称,用来当作key值
$apiUrl = CONTROLLER_NAME . '/' . ACTION_NAME;
//获取配置项中定义延迟调用的接口
$config = config('job_delay_request_api_config');
//判断当前调用的接口不在延迟脚本配置数组中,可以执行,返回即可
if (!in_array($apiUrl, $config['list'])) {
return;
}
//判断是否已调用过该接口
$key = 'job_delay_request_api:' . $apiUrl;
$redis = new Redis();
$result = $redis->get($key);
//判断已调用,则不能再次调用,需等待redis过期,此处停止脚本运行
if (!empty($result)) {
exit('该调度任务已执行,执行时间:' . date('Y-m-d H:i:s', $result));
}
//标记job任务已执行,记录执行时间
$redis->set($key, $config['cache_time'], time());
}
?>

5.总结

  目前采用的是4.2章节中的方案2,通过读取预先定义的配置,在代码层面判断做拦截,可以达到防止任务被重复调度的问题,并且可以指定哪些脚本需要被拦截。为什么没有采用4.1呢?由于我不是搞Java的哈哈,Java端的同事也对这个系统不太熟悉,所以没有采用4.1官网推荐的方式,后续会考虑4.1官网推荐的方式,会更加合理和稳定。感谢阅读,我们下次再见。

本文转载自: 掘金

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

pyOpenLooKeng——华为数据分析引擎OpenLoo

发表于 2021-07-03

本文正在参加「Python主题月」,详情查看 活动链接

前不久,2020 openEuler 高校开发者大赛暂告一段落。我们团队在这次比赛中承接了“为openLooKeng开发Python客户端”项目,成功进入决赛并拿到了二等奖的好成绩,也对开源社区有了新的认识和体会。应邀在此介绍一下我们的pyOpenLooKeng和参赛历程。

openLooKeng是什么?

openLooKeng,原名Hetu,是一个分布式、低延迟、可靠的数据引擎,提供了统一的SQL接口,具备高性能、跨数据中心/云查询、数据源扩展等功能,让大数据交互式分析变得更简单。

openLooKeng使用了FaceBook开源的分布式SQL引擎Presto来提供交互式的查询分析能力,除此之外还实现了许多features,例如动态过滤、位图索引、多缓存、跨DC(Data Center,数据中心)连接器等,这些让openLooKeng有了更好的性能、更强的扩展能力和可用性,真正实现了“SQL on Everything”这一理念。


我们的pyOpenLooKeng是怎么做的?


这是我们项目的仓库:gitee.com/openeuler20…

项目简析

OpenLooKeng采用REST通信,包括CLI、JDBC与 Coordinator, Coordinator与 Worker。Python client也可以使用REST通信,将Statement 按照 OpenLooKeng求封装成REST请求,发送恰 Coordinator执行,包含以下请求:

​ 1. 提交查询请求 POST /v1/statement

​ 2. 查询直至查询完成: GET /v1/statement/{ID}

​ 3. 删除某个查询:DELETE /v1/statement/{queryId}

同时,OpenLooKeng需要提供加密和认证功能,需要支持HTTPS加密以及Basic与Kerberos认证。 Python客户端作为OpenLooKeng的模块,需要符合OpenLooKeng的开发规范,同时客户端需要经过充分测试,单元测试覆盖率需要符合OpenLooKeng的要求,并且功能完成,能够正常访问OpenLooKeng的业务。

image-20210508102306307

架构介绍

  • auth: 架构用于获取请求会话,该会话由HTTPBasic或Kerberos验证;
  • err: 包含操作期间可能遇到的错误和警告 ,
  • common: 一些常见的DB-API逻辑的基类;
  • connections: 管理openLooKeng连接,用于获取游标,设置加密的身份验证信息等;
  • cursor: cursor表示数据库游标,用于管理提取操作的上下文。

arch

功能介绍

pyopenLooKeng采用了PEP-249v2.0 数据库API规范。PEP-249 DB-API为不同的数据库提供了一致的访问接口,比如我们可以很轻松地将访问的数据库从Mysql等移植到OpenLooKeng上,只需要进行少量的代码更改。

image-20210508103839914
类似于PyMysql,我们需要使用cur.execute()函数来提交查询请求。fetch函数族(fetchone() fetchmany() fetchall())来获取结果集;通过cancel()函数来取消查询。

pyOpenLooKeng还额外实现了一些其他的功能,比如Connection中可以获得集群信息,工作节点信息,以及查询的数据信息和阶段的数据信息。

在这一过程中,我们发现了openLooKeng的v1/stage的RESTful接口是无法使用的,我们在社区中进行了返回,提出了issue:gitee.com/openlookeng…

我们也支持HTTPS加密以及Basic与Kerberos认证,

详细代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
python复制代码from pyopenLooKeng import connections, auth

myAuth = auth.BasicAuthentication(username='user', password='password')
# Kerberos authentication can also be used
conn = connections.connect(host='host', port=8080, catalog='system', schema='runtime',
protocol="https", https_verify="trust", auth=myAuth)
print(conn.cluster())
print(conn.query())
# ...
cur = conn.cursor()
cur.execute("SHOW TABLES")
res = cur.fetchall()
print(res)
# (('nodes',), ('queries',), ('tasks',), ('transactions',))

在HTTPS加密的时候,我们也实现了两种方法:忽略证书验证,使用python第三方库certifi的证书;所以,我们可以把openLooKeng的公钥放到certifi.where()指向的文件中,提高了pyOpenLooKeng的安全性和便利性。

参赛体会

这是我们第一次参加开源社区的比赛,在此过程中我们有很多的感悟和体会。

  1. 开源社区很有帮助,最开始我们遇到过一个安装卡死在waiting cluster to start的问题,尝试过很多次无法解决,是社区微信群的一位前辈表示他遇到过,需要手动配置并修改一个配置文件。最后我们成功了。第二个就是stage接口的bug,最开始我以为是自己环境配置的问题,在与社区进行了沟通后确定,应该是系统的代码中出现了Bug。在社区许德智老师的引导下,我提交了openLooKeng的issue。
  2. 充分利用好工具,比如gitee的issue双周报和wiki,能够给使用者提供很多方便,也有利于开发者时间安排。
  3. 从开源软件中汲取经验,最开始我们面对PEP-249时是无从下手的,我们后来参考了pymysql的实现,从中获得了很多的帮助。
  4. 开源不只是把自己的代码发布出来,还要有清晰明了的文档,代码需要符合社区的开发规范,经过充分的测试,最重要的是要长时间的维护。

未来我们团队也会继续完善本项目,为openLooKeng的发展多助力,也祝愿我们的openLooKeng越来越好!

本文转载自: 掘金

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

使用gin封装一个web脚手架(六):实现session(中

发表于 2021-07-03

设计好session的数据结构后开始存放初始session值

先在context.go中定义好session的结构体

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
go复制代码package context

//context/context.go

import (
"github.com/gin-gonic/gin"
"strings"
)

type Context struct {
*gin.Context
}

type HandlerFunc func(*Context)

func (c *Context) Domain() string {

return c.Request.Host[:strings.Index(c.Request.Host, ":")]
}

type Session struct {
Cookie string `json:"cookie"`
ExpireTime int64 `json:"expire_time"`
SessionList map[string]interface{} `json:"session_list"`
}

接下来在session中间件中存储初始化数据

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
go复制代码package session

import (
context2 "context"
"encoding/json"
uuid "github.com/satori/go.uuid"
"myGin/context"
"myGin/redis"
"time"
)

var cookieName = "my_gin"

var lifeTime = 3600

func Session(c *context.Context) {

sessionKey := uuid.NewV4().String()

c.SetCookie(cookieName, sessionKey, lifeTime, "/", c.Domain(), false, true)

session := context.Session{
Cookie: sessionKey,
ExpireTime: time.Now().Unix() + int64(lifeTime),
SessionList: make(map[string]interface{}),
}

jsonString, _ := json.Marshal(session)

redis.Client().Set(context2.TODO(), sessionKey, jsonString, time.Second*time.Duration(lifeTime))

}

运行浏览器后,在redis中出现

image.png

问题出现了,虽然数据存储进去了,但是每刷新一次浏览器就会重新存储一次session,这显然不是我们想要的。正确的逻辑应该是,cookie存在并且能够在redis中找到这个值就不需要重新设置了。

修改一下代码

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
go复制代码package session

import (
context2 "context"
"encoding/json"
uuid "github.com/satori/go.uuid"
"myGin/context"
"myGin/redis"
"time"
)

var cookieName = "my_gin"

var lifeTime = 3600

func Session(c *context.Context) {

cookie, err := c.Cookie(cookieName)

if err == nil {

sessionString, err := redis.Client().Get(context2.TODO(), cookie).Result()

if err == nil {

var session context.Session

json.Unmarshal([]byte(sessionString), &session)

return
}

}

sessionKey := uuid.NewV4().String()

c.SetCookie(cookieName, sessionKey, lifeTime, "/", c.Domain(), false, true)

session := context.Session{
Cookie: sessionKey,
ExpireTime: time.Now().Unix() + int64(lifeTime),
SessionList: make(map[string]interface{}),
}

jsonString, _ := json.Marshal(session)

redis.Client().Set(context2.TODO(), sessionKey, jsonString, time.Second*time.Duration(lifeTime))

}

为了防止二次查询,将session结果存储到gin的context中

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
go复制代码package session

import (
context2 "context"
"encoding/json"
uuid "github.com/satori/go.uuid"
"myGin/context"
"myGin/redis"
"time"
)

var cookieName = "my_gin"

var lifeTime = 3600

func Session(c *context.Context) {

cookie, err := c.Cookie(cookieName)

if err == nil {

sessionString, err := redis.Client().Get(context2.TODO(), cookie).Result()

if err == nil {

var session context.Session

json.Unmarshal([]byte(sessionString), &session)

//存储到context中,方便当前请求中的其他函数好操作session
c.Set("_session", session)

return
}

}

sessionKey := uuid.NewV4().String()

c.SetCookie(cookieName, sessionKey, lifeTime, "/", c.Domain(), false, true)

session := context.Session{
Cookie: sessionKey,
ExpireTime: time.Now().Unix() + int64(lifeTime),
SessionList: make(map[string]interface{}),
}

//这里也要
c.Set("_session", session)

jsonString, _ := json.Marshal(session)

redis.Client().Set(context2.TODO(), sessionKey, jsonString, time.Second*time.Duration(lifeTime))

}

本文转载自: 掘金

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

手摸手,使用Dart语言开发后端应用,来吧!

发表于 2021-07-03

前言

这几天连续发了几篇关于 Dart 开发后端应用的文章,主要是介绍了 Dart 的一些优点,比如异步任务,并发处理,编译部署等等。

俗话说,光说不练假把式,今天我们来真正开始一个 Dart 后端应用。

我们要开发什么应用

假设我们现在要开发一个社区应用,类似于掘金,CSDN等等,基本的功能是用户发文章,发观点。

发文章,类似于传统的CMS系统

发观点,类似于现在的微博系统

围绕核心,还有标签,分类,评论等等。

我们用什么框架

既然打算使用 Dart 开发,有个开发框架还是有很大帮助的。 然而 Dart 的后端框架并不多,aqueduct, jaguar, DartMars 等等, 在这里,我们使用 DartMars。

源码在此 github.com/tangpanqing…

文档在此 tangpanqing.github.io/dart_mars_d…

打开文档首页,如此

微信图片_20210703095222.png

嗯嗯,浓浓的 vuepress 味道。

开始一个项目如此简单

根据DartMars的指引,在安装Dart 后,我们可以执行以下命令来创建项目

1
2
3
4
5
6
7
8
9
10
11
12
13
14
shell复制代码# 安装DartMars
dart pub global activate --source git https://github.com/tangpanqing/dart_mars.git

# 创建项目
dart pub global run dart_mars --create project_name

# 进入目录
cd project_name

# 获取依赖
dart pub global run dart_mars --get

# 启动项目
dart pub global run dart_mars --serve dev

手摸手,我们一步一步来

第一步,安装DartMars

打开命令行工具,执行

1
shell复制代码dart pub global activate --source git https://github.com/tangpanqing/dart_mars.git

感谢墙的存在,我等了将近1分钟,提示我如下:

1
shell复制代码Activated dart_mars 1.0.4 from Git repository "https://github.com/tangpanqing/dart_mars.git"

这就表示安装好了。

第二步,创建项目

项目暂定名称 community 社区,执行如下命令

1
shell复制代码dart pub global run dart_mars --create community

经过以上命令,DartMars 有了提示

1
2
3
4
shell复制代码project community has been created
you can change dir with command: cd community
and then get dependent with command: dart pub global run dart_mars --get
and then start it with command: dart pub global run dart_mars --serve dev

意思说,项目已经创建,接下来你需要进入目录,并且获取依赖,最后执行。

并且显示了相关命令,是不是很贴心? 谈恋爱的时候,一定是个暖男。

第三步,进入目录

执行命令

1
shell复制代码cd community

第四步,获取依赖

执行命令

1
shell复制代码dart pub global run dart_mars --get

经过以上命令,DartMars 有了提示

1
shell复制代码Got dependencies!

表示加载依赖完成

第五步,启动项目

1
shell复制代码dart pub global run dart_mars --serve dev

经过以上命令,DartMars 有了提示

1
2
3
4
5
shell复制代码route config file has been updated, see ./lib/config/route.dart
$ dart run bin\community.dart --serve dev
INFO::2021-07-03 10:14:13.601023::0::Server::Http Server has start, port=80
INFO::2021-07-03 10:14:13.608004::1::Server::Env type is dev
INFO::2021-07-03 10:14:13.624571::2::Server::Open browser and vist http://127.0.0.1:80 , you can see some info

启动成功,通过以上信息,我们可知:

  1. 路由配置文件已经更新,
  2. HTTP 服务已经开始,在80端口,目前使用的是开发环境

打开浏览器,访问 http://127.0.0.1:80 我们就看到了经典的

1
html复制代码hello world

按部就班地继续编码

先看一眼项目结构

微信图片_20210703115930.png

bin 目录是执行文件的入口

lib 目录是整个项目的开发目录

其他目录都是一些辅助性的,如名字所示。接下来,我们要按部就班的完成基本功能。

先完成第一个,用户的增查改删,并且做成标准,以后使用。

创建用户表

我已经提前准备好了相关的sql 语句

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
sql复制代码CREATE TABLE IF NOT EXISTS `user` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`user_id` varchar(40) COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '用户ID',
`user_mobile` varchar(11) COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '用户手机号',
`user_password` varchar(60) COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '用户密码',
`user_nickname` varchar(60) COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '用户昵称',
`user_avatar` varchar(60) COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '用户头像',
`user_description` varchar(120) COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '用户介绍',
`create_time` bigint(20) NOT NULL DEFAULT '0' COMMENT '创建时间',
`update_time` bigint(20) NOT NULL DEFAULT '0' COMMENT '更新时间',
`delete_time` bigint(20) NOT NULL DEFAULT '0' COMMENT '删除时间',
PRIMARY KEY (`id`),
UNIQUE KEY `user_id` (`user_id`),
KEY `user_mobile` (`user_mobile`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='用户表';

放到mysql 去执行

创建用户模型

用户模型用来与数据表进行对应的,方便面向对象开发。
在目录 lib/extend/model/ 下,新建模型文件 User.dart,键入如下内容

1
2
3
4
5
6
7
8
9
10
11
12
dart复制代码class User {
int id;
String userId;
String userMobile;
String userPassword;
String userNickname;
String userAvatar;
String userDescription;
int createTime;
int updateTime;
int deleteTime;
}

这里只是定义了类名,以及相关属性,还需要补充一些方法。补充模型类的方法,是一个枯燥的事情,建议使用工具。

如果你使用的是 VSCode,并且安装了 Dart Data Class Generator 插件,此时点击类名,将会出现帮助,点击下图红色框框内,将补充完成代码。

微信图片_20210703120712.png

我们将得到以下结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
dart复制代码import 'dart:convert';

class User {
int id;
String userId;
String userMobile;
String userPassword;
String userNickname;
String userAvatar;
String userDescription;
int createTime;
int updateTime;
int deleteTime;

User({
this.id,
this.userId,
this.userMobile,
this.userPassword,
this.userNickname,
this.userAvatar,
this.userDescription,
this.createTime,
this.updateTime,
this.deleteTime,
});

Map<String, dynamic> toMap() {
return {
'id': id,
'userId': userId,
'userMobile': userMobile,
'userPassword': userPassword,
'userNickname': userNickname,
'userAvatar': userAvatar,
'userDescription': userDescription,
'createTime': createTime,
'updateTime': updateTime,
'deleteTime': deleteTime,
};
}

factory User.fromMap(Map<String, dynamic> map) {
return User(
id: map['id'],
userId: map['userId'],
userMobile: map['userMobile'],
userPassword: map['userPassword'],
userNickname: map['userNickname'],
userAvatar: map['userAvatar'],
userDescription: map['userDescription'],
createTime: map['createTime'],
updateTime: map['updateTime'],
deleteTime: map['deleteTime'],
);
}

String toJson() => json.encode(toMap());

factory User.fromJson(String source) => User.fromMap(json.decode(source));

@override
String toString() {
return 'User(id: $id, userId: $userId, userMobile: $userMobile, userPassword: $userPassword, userNickname: $userNickname, userAvatar: $userAvatar, userDescription: $userDescription, createTime: $createTime, updateTime: $updateTime, deleteTime: $deleteTime)';
}
}

经过刚才的操作,可以看到

多了三个实例化函数 User, User.fromMap, User.fromJson

多了三个方法 toMap, toJson, toString

为什么要做这些,归根到底是因为 Dart 禁用反射,当我们从其他地方拿到数据,无法直接转成模型对象。只能先转成map,或者json字符串,然后再手工转成模型对象。

是稍稍复杂了点,为了更好的性能,不算大问题。

创建服务

服务用来处理实际业务,被控制器所调用。

在目录 lib/extend/service/ 下,新建服务文件 UserService.dart,键入如下内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
dart复制代码import 'package:community/bootstrap/db/Db.dart';
import 'package:community/bootstrap/db/DbColumn.dart';
import 'package:community/bootstrap/helper/ConvertHelper.dart';
import 'package:community/extend/helper/PasswordHelper.dart';
import 'package:community/extend/helper/TimeHelper.dart';
import 'package:community/extend/helper/UniqueHelper.dart';
import 'package:community/extend/model/Page.dart';
import 'package:community/extend/model/User.dart';

class UserService {
static String _table = "user";

/// 分页查询
static Future<Page<User>> query(
List<DbColumn> condition, int pageNum, int pageSize) async {
int totalCount = await Db(_table).where(condition).count('*');

List<Map<String, dynamic>> mapList = await Db(_table)
.where(condition)
.page(pageNum, pageSize)
.order("create_time desc")
.select();

List<User> list =
mapList.map((e) => User.fromMap(ConvertHelper.keyToHump(e))).toList();

return Page<User>(totalCount, pageNum, pageSize, list);
}

/// 根据用户ID查询
static Future<User> findById(String userId) async {
List<DbColumn> where = [
DbColumn.fieldToUnderLine("userId", "=", userId),
DbColumn.fieldToUnderLine("deleteTime", "=", 0),
];

Map<String, dynamic> map = await Db(_table).where(where).find();
if (null == map) throw "没有找到用户";

return User.fromMap(ConvertHelper.keyToHump(map));
}

/// 添加用户
static Future<User> add(
String userMobile,
String userPassword,
String userNickname,
String userAvatar,
String userDescription,
) async {
Map<String, dynamic> userMap = await _findByMobile(userMobile);

if (null != userMap) throw '该手机号已存在';

User user = User(
userId: UniqueHelper.userId(),
userMobile: userMobile,
userPassword: PasswordHelper.password(userPassword),
createTime: TimeHelper.timestamp(),
userNickname: userNickname,
userAvatar: userAvatar,
userDescription: userDescription,
updateTime: 0,
deleteTime: 0);

user.id = await Db(_table).insert(ConvertHelper.keyToUnderLine(user.toMap()));

return user;
}

/// 修改用户昵称
static Future<User> updateNickname(String userId, String userNickname) async {
User user = await findById(userId);
user.userNickname = userNickname;

await _updateField(user.toMap(), 'userId', ['userNickname']);

return user;
}

/// 根据用户ID删除,软删除
static Future<User> delete(String userId) async {
User user = await findById(userId);
user.deleteTime = TimeHelper.timestamp();

await _updateField(user.toMap(), 'userId', ['deleteTime']);

return user;
}

/// 根据用户手机号查询
static Future<Map<String, dynamic>> _findByMobile(String userMobile) async {
List<DbColumn> condition = [
DbColumn.fieldToUnderLine("userMobile", "=", userMobile),
DbColumn.fieldToUnderLine("deleteTime", "=", 0),
];

Map<String, dynamic> map = await Db(_table).where(condition).find();

return map;
}

/// 更新表字段
static Future<int> _updateField(
Map<String, dynamic> map, String keyName, List<String> fieldList) async {
List<DbColumn> condition = [
DbColumn.fieldToUnderLine(keyName, '=', map[keyName])
];

Map<String, dynamic> updateMap = {};
fieldList.forEach((fieldName) {
updateMap[fieldName] = map[fieldName];
});

return await Db(_table)
.where(condition)
.update(ConvertHelper.keyToUnderLine(updateMap));
}
}

上述代码,是对数据的增查改删,和其他语言的代码,大同小异,一些容易迷惑的地方,稍微解释下。

在分页查询中

1
2
dart复制代码List<User> list =
mapList.map((e) => User.fromMap(ConvertHelper.keyToHump(e))).toList();

这里主要的作用是,将 mapList 这个键值对的列表,转换成 User 对象列表。

另外,因为我们数据库的字段名是下划线格式的,而模型类的属性是驼峰格式的,所以需要一个转换过程。

ConvertHelper.keyToHump 的作用是将键名为 下划线格式 的键值对,转换成键名为 驼峰格式 的键值对。

创建控制器

控制器用于接收用户请求参数,并调用服务来处理业务,最后返回信息

在目录 lib/app/controller/ 下,新建模型文件 UserController.dart,键入如下内容

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
dart复制代码import 'package:community/bootstrap/Context.dart';
import 'package:community/bootstrap/db/DbColumn.dart';
import 'package:community/bootstrap/db/DbTrans.dart';
import 'package:community/bootstrap/helper/VerifyHelper.dart';
import 'package:community/bootstrap/meta/RouteMeta.dart';
import 'package:community/extend/model/Page.dart';
import 'package:community/extend/model/User.dart';
import 'package:community/extend/service/UserService.dart';

class UserController {
@RouteMeta('/home/user/query', 'GET|POST')
static void query(Context ctx) async {
int pageNum = ctx.getPositiveInt('pageNum', def: 1);
int pageSize = ctx.getPositiveInt('pageSize', def: 20);

await DbTrans.simple(ctx, () async {
List<DbColumn> condition = [];
Page<User> res = await UserService.query(condition, pageNum, pageSize);
ctx.showSuccess('已获取', res.toMap());
});
}

@RouteMeta('/home/user/findById', 'GET|POST')
static void findById(Context ctx) async {
String userId = ctx.getString('userId');
if (VerifyHelper.empty(userId)) return ctx.showError('用户ID不能为空');

await DbTrans.simple(ctx, () async {
User res = await UserService.findById(userId);
ctx.showSuccess('已获取', res.toMap());
});
}

@RouteMeta('/home/user/add', 'GET|POST')
static void add(Context ctx) async {
String userMobile = ctx.getString('userMobile');
String userPassword = ctx.getString('userPassword');
String userNickname = ctx.getString('userNickname');
String userAvatar = ctx.getString('userAvatar');
String userDescription = ctx.getString('userDescription');

if (VerifyHelper.empty(userMobile)) return ctx.showError('用户手机号不能为空');
if (VerifyHelper.empty(userPassword)) return ctx.showError('用户密码不能为空');
if (VerifyHelper.empty(userNickname)) return ctx.showError('用户昵称不能为空');
if (VerifyHelper.empty(userAvatar)) return ctx.showError('用户头像不能为空');
if (VerifyHelper.empty(userDescription)) return ctx.showError('用户描述不能为空');

await DbTrans.simple(ctx, () async {
User res = await UserService.add(
userMobile, userPassword, userNickname, userAvatar, userDescription);
ctx.showSuccess('已添加', res.toMap());
});
}

@RouteMeta('/home/user/updateNickname', 'GET|POST')
static void updateNickname(Context ctx) async {
String userId = ctx.getString('userId');
String userNickname = ctx.getString('userNickname');
if (VerifyHelper.empty(userId)) return ctx.showError('用户ID不能为空');
if (VerifyHelper.empty(userNickname)) return ctx.showError('用户昵称不能为空');

await DbTrans.simple(ctx, () async {
User res = await UserService.updateNickname(userId, userNickname);
ctx.showSuccess('已更改', res.toMap());
});
}

@RouteMeta('/home/user/delete', 'GET|POST')
static void delete(Context ctx) async {
String userId = ctx.getString('userId');
if (VerifyHelper.empty(userId)) return ctx.showError('用户ID不能为空');

await DbTrans.simple(ctx, () async {
User res = await UserService.delete(userId);
ctx.showSuccess('已删除', res.toMap());
});
}
}

有必要说明一下:

RouteMeta 是 DartMars 定义的路由元数据,类似于java 里的注解。

相同的作用是,可以对代码进行描述,让开发者知道所描述的代码的功能。

不同的是,因为 DartMars 没有反射,所以程序不能在运行的时候获取元数据或者说注解的信息,也就无法完成类似于java里注解生成代码的功能。

当然,既然运行的时候不能生成代码,我们另寻他图,在编译之前生成即可。

自动更新路由配置

接下来,我们启动项目,执行如下命令:

1
shell复制代码dart pub global run dart_mars --serve dev

请注意,控制台打印的有这样一句话

1
shell复制代码route config file has been updated, see ./lib/config/route.dart

说路由配置文件已经更新,地址是 ./lib/config/route.dart,我们看看去

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
dart复制代码import '../bootstrap/helper/RouteHelper.dart';
import '../app/controller/HomeController.dart' as app_controller_HomeController;
import '../app/controller/UserController.dart' as app_controller_UserController;

///
/// don't modify this file yourself, this file content will be replace by DartMars
///
/// for more infomation, see doc about Route
///
/// last replace time 2021-07-03 14:53:51.588722
///
void configRoute(){
RouteHelper.add('GET', '/', app_controller_HomeController.HomeController.index);
RouteHelper.add('GET', '/user', app_controller_HomeController.HomeController.user);
RouteHelper.add('GET', '/city/:cityName', app_controller_HomeController.HomeController.city);
RouteHelper.add('GET|POST', '/home/user/query', app_controller_UserController.UserController.query);
RouteHelper.add('GET|POST', '/home/user/findById', app_controller_UserController.UserController.findById);
RouteHelper.add('GET|POST', '/home/user/add', app_controller_UserController.UserController.add);
RouteHelper.add('GET|POST', '/home/user/updateNickname', app_controller_UserController.UserController.updateNickname);
RouteHelper.add('GET|POST', '/home/user/delete', app_controller_UserController.UserController.delete);
}

果然,最后面添加了 5 个路由规则,和我们刚才在 UserController 里定义的一样。

另外,如文件所提示的,这个文件不要手动更改,当你运行 --serve 命令时, DartMars会自动更新。

测试接口

测试接口的工作非常简单了,可以使用专业工具,也可以在浏览器中直接来。文章篇幅有限,我就测试 2 个,其他的接口,有兴趣的同学自己来。

测试添加用户接口

1
html复制代码http://127.0.0.1/home/user/add?userMobile=18512345679&userPassword=123456&userNickname=tang&userAvatar=http://www.test.com/1.jpg&userDescription=test

返回如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
json复制代码{
"code": 200,
"msg": "已添加",
"data": {
"id": 2,
"userId": "1625295731292004882",
"userMobile": "18512345679",
"userPassword": "4616221982a9d1759d1d0cec7249a6d71da960d3",
"userNickname": "tang",
"userAvatar": "http://www.test.com/1.jpg",
"userDescription": "test",
"createTime": 1625295731,
"updateTime": 0,
"deleteTime": 0
}
}

一切正常,非常棒。

测试查询单个用户接口

1
html复制代码http://127.0.0.1/home/user/findById?userId=1625295731292004882

返回如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
json复制代码{
"code": 200,
"msg": "已获取",
"data": {
"id": 2,
"userId": "1625295731292004882",
"userMobile": "18512345679",
"userPassword": "4616221982a9d1759d1d0cec7249a6d71da960d3",
"userNickname": "tang",
"userAvatar": "http://www.test.com/1.jpg",
"userDescription": "test",
"createTime": 1625295731,
"updateTime": 0,
"deleteTime": 0
}
}

一切正常,非常棒。

总结

能够看到这里的同学,想必都是真爱了。

由上述流程走下来,可以看出,用 Dart 开发后端应用,与其他语言开发,并无太大的区别。也说明一个事情,其他语言的开发者,想转用 Dart 开发后端应用程序,是一件很容易的事情。

加之 Dart 在客户端开发领域的成功, 一种语言完成客户端与服务端绝对不再是梦想。

That’s All, Enjoy.

本文转载自: 掘金

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

1…622623624…956

开发者博客

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