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

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


  • 首页

  • 归档

  • 搜索

Yarn 的安装与使用

发表于 2021-10-23

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。

大家好,我是一碗周,一个不想被喝(内卷)的前端。如果写的文章有幸可以得到你的青睐,万分有幸~

写在前面

Yarn 是一款 JavaScript 的包管理工具(npm 的代替方案),在 Yarn 的官网有着一句话:Safe, stable, reproducible projects 。

正如 Yarn 官网的介绍,Yarn 的具有速度快 、安全 、可靠 的优点,在功能上相比于 npm 优化了许多功能等,例如网络性能优化,安装依赖的方式相同等功能。具体可以参考Yarn 中文网。

Yarn 的安装

Yarn 的安装比较简单,直接使用npm命令即可,这样的前提是你已经安装了 Node.js,命令如下:

1
2
3
4
PowerShell复制代码# 检查是否具有node.js
node-v
# 安装yarn
npm install -g yarn

安装完成之后可以通过如下命令检测是否安装成功:

1
PowerShell复制代码yarn -v

如果提示版本号则安装完成,提示的版本号为1.X.X就表示安装成功了

然后我们设置一下yarn库的镜像源,命令如下:

1
PowerShell复制代码yarn config set npmRegistryServer https://registry.npm.taobao.org

Yarn 的常用命令

初始化

1
PowerShell复制代码yarn init

添加依赖包

1
2
3
4
PowerShell复制代码yarn add [package] # 会自动安装最新版本,会覆盖指定版本号
yarn add [package] [package] [package] # 一次性添加多个包
yarn add [package]@[version] # 添加指定版本的包
yarn add [package]@[tag] # 安装某个tag(比如beta,next或者latest)

将依赖项添加到不同依赖项类别

不指定依赖类型默认安装到dependencies里,你也可以指定依赖类型分别添加到devDependencies、peerDependencies和optionalDependencies。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
PowerShell复制代码# 加到 devDependencies
yarn add [package] --dev
#或
yarn add [package] -D

# 加到 peerDependencies
yarn add [package] --peer
#或
yarn add [package] -P

# 加到 optionalDependencies
yarn add [package] --optional
#或
yarn add [package] -O

升级依赖包

1
2
3
PowerShell复制代码yarn upgrade [package] # 升级到最新版本
yarn upgrade [package]@[version] # 升级到指定版本
yarn upgrade [package]@[tag] # 升级到指定tag

移除依赖包

1
PowerShell复制代码yarn remove [package] # 移除包

从 package.json 里安装依赖,并将依赖项保存进 yarn.lock

1
2
3
4
5
PowerShell复制代码yarn # 安装所有依赖
yarn install # 安装所有依赖
yarn install --flat # 安装一个包的单一版本
yarn install --force # 强制重新下载所有包
yarn install --production # 只安装生产环境依赖

发布包

1
PowerShell复制代码yarn publish

运行脚本

1
PowerShell复制代码yarn run # 用来执行在 package.json 中 scripts 属性下定义的脚本

显示某个包的信息

1
PowerShell复制代码yarn info [package] # 可以用来查看某个模块的最新版本信息

缓存

1
2
3
4
PowerShell复制代码yarn cache
yarn cache list # 列出已缓存的每个包
yarn cache dir # 返回全局缓存位置
yarn cache clean # 清除缓存

本文转载自: 掘金

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

如何在Java中避免创建不必要的对象(备战2022春招或暑期

发表于 2021-10-23

本文已参与 「掘力星计划」 ,赢取创作大礼包,挑战创作激励金。

  • 备战2022春招或暑期实习,祝大家每天进步亿点点!Day1
  • 本篇总结的是 如何在Java中避免创建不必要的对象,后续会每日更新~
  • 关于《Redis入门到精通》、《并发编程》等知识点可以参考我的往期博客:xxx
  • 相信自己,越活越坚强,活着就该逢山开路,遇水架桥!生活,你给我压力,我还你奇迹!

简介

在Java开发中,程序员要尽可能的避免创建相同的功能的对象,因为这样既消耗内存,又影响程序运行速度。在这种情况下可以考虑重复利用对象。

接下来举例几种对象重复利用的场景,看看我们是不是有中招了,如果有赶紧趁着还没被发现悄悄改掉,被发现了会被diss啦!

1、String和Boolean

如下两种写法看似没有什么区别,但是如果深入jvm底层了解,我们可以利用jvm运行时常量池的特性,避免创建具有相同功能的String对象(尤其是在循环内部创建)可以带来比较可观的性能优化以及节约内存。

错误写法

1
2
javascript复制代码// 每次都会创建一个新的String对象,且不会加入常量池
String name2 = new String("李子捌");

正确写法

1
2
ini复制代码// 正确写法
String name1 = "李子捌";

除此之外,刚写Java代码的程序员们,也要正确的选择String、StringBuilder、StringBuffer类的使用。String为不可变对象,通常用于定义不变字符串;StringBuilder、StringBuffer用于可变字符串操作场景,如字符串拼接;其中StringBuffer是线程安全的,它通过Synchronized关键字来实现线程同步。

1
2
3
4
5
6
7
8
9
10
11
12
typescript复制代码// StringBuffer中的append()方法
public synchronized StringBuffer append(String str) {
toStringCache = null;
super.append(str);
return this;
}

// StringBuilder中的append()方法
public StringBuilder append(String str) {
super.append(str);
return this;
}

Boolean是常用的类型,在开发中也应该使用Boolean.valueof()而不是new Boolean(),从Boolean的源码可以看出,Boolean类定义了两个final static的属性,而Boolean.valueof()直接返回的是定义的这两个属性,而new Boolean()却会创建新的对象。

1
2
3
sql复制代码public static final Boolean TRUE = new Boolean(true);

public static final Boolean FALSE = new Boolean(false);

2、自动拆箱和装箱

Java提供了基本数据类型的自动拆箱和装箱功能,那是不是意味着我们可以在代码中随意的使用这两个类型呢?其实理论上在代码层面是没得问题,不过在具体的性能方面还是有优化的空间啦!!!

我们来测试下性能

1
2
3
4
5
6
ini复制代码long start = System.currentTimeMillis();
Integer sum = 0;
for (int i = 0; i < 100000; i++) {
sum += i;
}
System.out.println(System.currentTimeMillis() - start);

使用Integer耗时3毫秒

1
2
3
4
5
6
7
ini复制代码long start = System.currentTimeMillis();
// 修改Integer 为 int
int sum = 0;
for (int i = 0; i < 100000; i++) {
sum += i;
}
System.out.println(System.currentTimeMillis() - start);

使用int耗时0毫秒

因此关于自动拆箱装箱的使用,我们其实也可以做适当的考虑,毕竟有时候代码性能就是一点点挤出来的嘛!!!

3、正则表达式

正则表达式我们经常用于字符串是否合法的校验,我们先来看一段简单的代码(大家有没有一眼看出问题呢?我想你肯定看出来了!!!):

1
2
3
4
5
6
7
8
9
10
11
12
13
ini复制代码public static void main(String[] args) {

String email = "1057301174@qq.com";
String regex = "^([a-z0-9A-Z]+[-|\.]?)+[a-z0-9A-Z]@([a-z0-9A-Z]+(-[a-z0-9A-Z]+)?\.)+[a-zA-Z]{2,}$";

long start = System.currentTimeMillis();
for (int i = 0; i < 10000; i++) {
email.matches(regex);
}

System.out.println(System.currentTimeMillis() - start);

}

执行这段代码的时间,一共耗时71毫秒,看似好像挺快的!

但是我们做个非常简单的优化,优化后的代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
ini复制代码public static void main(String[] args) {

String email = "1057301174@qq.com";
String regex = "^([a-z0-9A-Z]+[-|\.]?)+[a-z0-9A-Z]@([a-z0-9A-Z]+(-[a-z0-9A-Z]+)?\.)+[a-zA-Z]{2,}$";
Pattern pattern = Pattern.compile(regex);

long start = System.currentTimeMillis();
for (int i = 0; i < 10000; i++) {
//email.matches(regex);
pattern.matcher(email);
}

System.out.println(System.currentTimeMillis() - start);

}

再次执行代码,一共耗时1毫秒,快了70倍呀!!!

这是因为String.matches()方法在循环中创建时,每次都需要执行Pattern.compile(regex),而创建Patter实例的成本很高,因为需要将正则表达式编译成一个有限状态机( finite state machine)。这种我们经常会因为Java api提供了比较方便的方法调用而忽略了性能考究,往往不容易被发现。这个时候就需要优秀的你,去“咬文嚼字”啦!

「欢迎在评论区讨论,掘金官方将在掘力星计划活动结束后,在评论区抽送100份掘金周边,抽奖详情见活动文章」

本文转载自: 掘金

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

SpringBoot进阶(七)整合RocketMQ

发表于 2021-10-23

RocketMQ是阿里出的一个纯java的消息队列框架,其它的还有Kafka,Rabbitmq等,网上也有很多他们的对比,优缺点一目了然,国内可能RocketMQ使用居多,因为它抗住了全球最大的流量洪峰双十一,而且运行稳定!

GitHub:github.com/baiyuliang/…

下图为引用网上关于三者的对比情况图:

在这里插入图片描述

关于MQ的作用这里也不再讲了,自行Baidu,你会了解更多!如果你是安卓开发同学,那你就把他理解成EventBus吧,哈哈!

使用Docker安装RocketMQ:

你可以不使用docker,直接安装到linux服务器:Linux部署RocketMq,当然,这会相对麻烦一点!

首先,打开Xftp 6软件连接Linux服务器,在usr/local或者其它文件夹下,创建mq文件夹,再在mq文件夹下创建conf和data两个文件夹:

在这里插入图片描述

在conf文件夹下新建broker.conf文件,并编辑内容一下内容后保存:

1
2
3
4
5
6
7
8
bash复制代码brokerClusterName = DefaultCluster
brokerName = broker-a
brokerId = 0
deleteWhen = 04
fileReservedTime = 48
brokerRole = ASYNC_MASTER
flushDiskType = ASYNC_FLUSH
brokerIP1 = 39.xxx.x.xx

brokerIP1为你本机的外网地址!

准备就绪后,开始安装:
1.下载:
docker pull rocketmqinc/rocketmq
2.启动rocketmq:
docker run -d -p 9876:9876 -v /usr/local/mq/data/namesrv/logs:/root/logs -v /usr/local/mq/data/namesrv/store:/root/store --name rmqnamesrv -e "MAX_POSSIBLE_HEAP=100000000" rocketmqinc/rocketmq sh mqnamesrv
3.启动broker:
docker run -d -p 10911:10911 -p 10909:10909 -v /usr/local/mq/data/broker/logs:/root/logs -v /usr/local/mq/data/broker/store:/root/store -v /usr/local/mq/conf/broker.conf:/opt/rocketmq/conf/broker.conf --name rmqbroker --link rmqnamesrv:namesrv -e "NAMESRV_ADDR=namesrv:9876" -e "MAX_POSSIBLE_HEAP=200000000" rocketmqinc/rocketmq sh mqbroker autoCreateTopicEnable=true -c /opt/rocketmq/conf/broker.conf
4.安装控制台:
docker pull styletang/rocketmq-console-ng
7.启动控制台:
docker run -e "JAVA_OPTS=-Drocketmq.config.namesrvAddr=39.xxx.x.xx:9876 -Drocketmq.config.isVIPChannel=false" -p 8082:8080 -t styletang/rocketmq-console-ng

注意自己的外网ip地址不要写错,如果你更改了默认的端口映射,那么要注意你的端口保持一致!

浏览器打开:39.xxx.x.xx/8082

在这里插入图片描述

你可以在右侧选择中文语言,但到这里并不算成功,点击Cluster/集群:

在这里插入图片描述

出现broker-a,即你在broker.conf文件内声明的brokerName时,表明你的RockerMQ完全安装成功了!

整合进项目测试:

pom.xml引入:

1
2
3
4
5
6
xml复制代码<!--消息框架rocketmq-->
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-client</artifactId>
<version>4.7.1</version>
</dependency>

application.propertites中配置:

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
xml复制代码spring.application.name=springboottest
#配置项目根路径
server.servlet.context-path=/byl
spring.thymeleaf.cache=false
####RocketMQ相关配置
###producer
#该应用是否启用生产者
rocketmq.producer.isOnOff=on
#发送同一类消息的设置为同一个group
rocketmq.producer.groupName=${spring.application.name}
#mq的nameserver地址
rocketmq.producer.namesrvAddr=39.xxx.x.xx:9876
#主题
rocketmq.producer.topics=MyTopic;
#消息最大长度 默认1024*128(128M)
rocketmq.producer.maxMessageSize=131027
#发送消息超时时间,默认10000
rocketmq.producer.sendMsgTimeout=10000
#发送消息失败重试次数,默认2
rocketmq.producer.retryTimesWhenSendFailed=2
###consumer
##该应用是否启用消费者
rocketmq.consumer.isOnOff=on
rocketmq.consumer.groupName=${spring.application.name}
#mq的nameserver地址
rocketmq.consumer.namesrvAddr=39.xxx.x.xx:9876
#该消费者订阅的主题
rocketmq.consumer.topics=MyTopic;
rocketmq.consumer.consumeThreadMin=20
rocketmq.consumer.consumeThreadMax=64
#设置一次消费消息的条数,默认为1条
rocketmq.consumer.consumeMessageBatchMaxSize=1

rocketmq的三大件:Producer,Consumer,MessageListener,分别进行配置:

在这里插入图片描述

MQProducerConfig:

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
java复制代码package com.byl.springboottest.rocket;

import org.apache.rocketmq.client.exception.MQClientException;
import org.apache.rocketmq.client.producer.DefaultMQProducer;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
* 消息生产者配置
*/
@Configuration
public class MQProducerConfig {

@Value("${rocketmq.producer.groupName}")
private String groupName;
@Value("${rocketmq.producer.namesrvAddr}")
private String namesrvAddr;
@Value("${rocketmq.producer.maxMessageSize}")
private Integer maxMessageSize;
@Value("${rocketmq.producer.sendMsgTimeout}")
private Integer sendMsgTimeout;
@Value("${rocketmq.producer.retryTimesWhenSendFailed}")
private Integer retryTimesWhenSendFailed;

@Bean
public DefaultMQProducer defaultMQProducer() {
DefaultMQProducer producer = new DefaultMQProducer(this.groupName);
producer.setNamesrvAddr(this.namesrvAddr);
producer.setMaxMessageSize(this.maxMessageSize);
producer.setSendMsgTimeout(this.sendMsgTimeout);
producer.setRetryTimesWhenSendFailed(this.retryTimesWhenSendFailed);
try {
producer.start();
} catch (MQClientException e) {
e.printStackTrace();
}
return producer;
}
}

MQConsumerConfig:

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
java复制代码package com.byl.springboottest.rocket;


import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer;
import org.apache.rocketmq.client.exception.MQClientException;
import org.apache.rocketmq.common.consumer.ConsumeFromWhere;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.annotation.Resource;


/**
* 消费者配置
*/
@Configuration
public class MQConsumerConfig {
@Value("${rocketmq.consumer.namesrvAddr}")
private String namesrvAddr;
@Value("${rocketmq.consumer.groupName}")
private String groupName;
@Value("${rocketmq.consumer.consumeThreadMin}")
private int consumeThreadMin;
@Value("${rocketmq.consumer.consumeThreadMax}")
private int consumeThreadMax;
@Value("${rocketmq.consumer.topics}")
private String topics;
@Value("${rocketmq.consumer.consumeMessageBatchMaxSize}")
private int consumeMessageBatchMaxSize;

@Resource
private MQMsgListener mqMsgListener;

@Bean
public DefaultMQPushConsumer defaultMQPushConsumer() {
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer(groupName);
consumer.setNamesrvAddr(namesrvAddr);
consumer.setConsumeThreadMin(consumeThreadMin);
consumer.setConsumeThreadMax(consumeThreadMax);
consumer.registerMessageListener(mqMsgListener);
consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_LAST_OFFSET);
// consumer.setMessageModel(MessageModel.CLUSTERING);
consumer.setConsumeMessageBatchMaxSize(consumeMessageBatchMaxSize);
try {
consumer.subscribe(topics, "MyTag");
consumer.start();
} catch (MQClientException e) {
}
return consumer;
}
}

MQMsgListener:

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
java复制代码package com.byl.springboottest.rocket;

import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyContext;
import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyStatus;
import org.apache.rocketmq.client.consumer.listener.MessageListenerConcurrently;
import org.apache.rocketmq.common.message.MessageExt;
import org.springframework.stereotype.Component;

import java.util.List;

/**
* 消息接收监听
*/
@Component
public class MQMsgListener implements MessageListenerConcurrently {

@Override
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext context) {
if (msgs == null || msgs.size() == 0) {
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
MessageExt messageExt = msgs.get(0);
System.out.println("接受到的消息为:" + messageExt.toString());
if (messageExt.getTopic().equals("你的Topic")) {
if (messageExt.getTags().equals("你的Tag")) {
//判断该消息是否重复消费(RocketMQ不保证消息不重复,如果你的业务需要保证严格的不重复消息,需要你自己在业务端去重)
//获取该消息重试次数
int reconsume = messageExt.getReconsumeTimes();
if (reconsume == 3) {//消息已经重试了3次,如果不需要再次消费,则返回成功
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
//处理对应的业务逻辑

}
}
// 如果没有return success ,consumer会重新消费该消息,直到return success
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}

}

打开SpringboottestApplicationTests测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码    @Resource
DefaultMQProducer defaultMQProducer;

@Test
void testSendRocketMq() throws InterruptedException, RemotingException, MQClientException, MQBrokerException {
String msg = "rocketmq发送查询消息:查询成功";
Message sendMsg = new Message("MyTopic", "MyTag", msg.getBytes());
//默认3秒超时
SendResult sendResult = defaultMQProducer.send(sendMsg);
SendStatus sendStatus = sendResult.getSendStatus();
System.out.println("消息发送响应信息状态:" + sendStatus);
System.out.println("消息发送响应信息:" + sendResult.toString());
}

运行testSendRocketMq方法:

在这里插入图片描述

具体返回内容:

1
java复制代码SendResult [sendStatus=SEND_OK, msgId=C0A801184D2818B4AAC27A4996310000, offsetMsgId=2769096000002A9F0000000000000347, messageQueue=MessageQueue [topic=ConsumerTopic, brokerName=broker-a, queueId=2], queueOffset=0]

打开rocketmq控制台:

在这里插入图片描述

在这里插入图片描述

注意,经测试,在消费者订阅主题时:

1
java复制代码subscribe(String topic, String subExpression)

第二个参数,也就是发送消息时设置的Tag,必须指定,如:

1
java复制代码consumer.subscribe("MyTopic", "MyTag");

或

1
java复制代码consumer.subscribe("MyTopic", "MyTag||MyTag2");

如果设为consumer.subscribe(“MyTopic”, “*“)会导致无法收到消息!

本文转载自: 掘金

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

SpringBoot入门(三)Controller

发表于 2021-10-23

新建一个Controller类:

在这里插入图片描述

Controller的写法有两种:

1.@Controller+@RequestMapping+@ResponseBody方式:

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

@RequestMapping(value = "/hello", method = RequestMethod.GET)
@ResponseBody
public String hello() {
return "Hello World!";
}

}

2.@RestController+@GetMapping(PostMapping、PutMapping…)方式:

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

@GetMapping("/hello")
public String hello() {
return "Hello World!";
}

}

运行项目,在地址栏输入:http://localhost:8080/hello

得到结果:

在这里插入图片描述

可以每种方式都试一遍!

然后我们创建success.html文件,再来说他们的区别:

在这里插入图片描述
首先用第一种方式,添加success()方法:

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

@RequestMapping(value = "/hello",method = RequestMethod.GET)
@ResponseBody
public String hello() {
return "Hello World!";
}

@RequestMapping(value = "/success",method = RequestMethod.GET)
public String success() {
return "success";
}

}

浏览器访问:http://localhost:8080/success
在这里插入图片描述
报错,控制台信息:

1
powershell复制代码javax.servlet.ServletException: Circular view path [success]: would dispatch back to the current handler URL [/success] again. Check your ViewResolver setup! (Hint: This may be the result of an unspecified view, due to default view name generation.)

意思就是无法识别方法中返回的success,此时我们需要引入thymeleaf模板引擎,在pom.xml中引入:

1
2
3
4
xml复制代码<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>

重启,再次打开http://localhost:8080/success:

在这里插入图片描述

成功进入success.html!

关于thymeleaf作用,大家应该很容易猜到,不明白的可以去看下他的文档,很简单,springboot中常用,使用方法类似于Vue的v-xxx指令,或者微信小程序的wx:xxx指令,简单使用方法:

1
html复制代码<html xmlns:th="http://www.thymeleaf.org">
1
html复制代码<span th:text="${param.id}"></span>

我们在访问http://xxx?id=xxx时,就可以直接在通过上面的代码获取到id!

言归正传,继续说说Controller两种写法的区别:

如果使用 @Controller,那就必须指明方法中返回数据类型,如@ResponseBody,表明返回的是具体的数据如字符串,json等(如果我们直接返回一个对象,SpringBoot会自动为我们转成json格式),如上面的hello方法:

1
2
3
4
5
java复制代码 @RequestMapping(value = "/hello",method = RequestMethod.GET)
@ResponseBody
public String hello() {
return "Hello World!";
}

如果不指明,则代表返回的是一个网页路径,如上面的success方法:

1
2
3
4
java复制代码@RequestMapping(value = "/success",method = RequestMethod.GET)
public String success() {
return "success";
}

我们可以按着Ctrl鼠标点击return的”success”,会直接跳转到对应的success.html界面!

所以@Controller既可以返回指定数据格式数据,也可以返回具体网页路径!
而 @RestController则只能返回json等固定数据格式数据,不能再返回html,jsp页面等,因为它是@Controller+@ResponseBody的组合(可以点击@RestController注解进入内部查看),所以我们在为前端写接口时,就经常使用@RestController了!

我们在实际开发过程中,经常会划分业务模块,因为在书写Controller时,往往会为Controller里的所有方法定义一个父级RequestMapping来区分业务模块:

1
2
3
4
5
6
7
8
9
10
java复制代码@RestController
@RequestMapping("/test")
public class TestController {

@GetMapping("/hello")
public String hello() {
return "Hello World!";
}

}

浏览器访问:http://localhost:8080/test/hello

在这里插入图片描述

安卓或前端同学会深有感触了!

另外,我们往往也会为项目声明一个总的路径名,如:

1
xml复制代码server.servlet.context-path=/byl

那么此时,在访问时就需要加上该路径:http://localhost:8080/byl/test/hello

在这里插入图片描述

本文转载自: 掘金

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

【Maven从入门到精通】 01-自动化构建工具:Maven

发表于 2021-10-22

笔记来源:Maven零基础入门教程(一套轻松搞定maven工具)

[TOC]

自动化构建工具:Maven

目前掌握的技术

软件分层

目前技术在开发中存在的问题

  1. 一个项目就是一个工程
* **产生的问题**:项目非常庞大时,就不适合使用 package 划分模块。最好是一个模块对应一个工程,这样利于分工协作
* **借助 Maven**:可以将一个项目拆分成多个工程
  1. 项目所需 Jar 包需要手动复制粘贴至WEB-INF/lib下
* **产生的问题**:同样的 Jar 包重复出现在不同的项目工程中,一来占用存储空间,二来显得臃肿
* **借助 Maven**:可以将 Jar 存储在 *仓库* 中,需使用的项目工程中只需 *引用* 即可,无需真正地复制 Jar 包
  1. Jar 包需要别人替我们准备好,或是官网下载
* **产生的问题**:不同技术的官网提供 Jar 包下载的形式是五花八门的,有些官网就是通过 Maven 或 SVN 等专门的工具提供下载的。如果下载的 Jar 包 *来路不正* ,那么很可能其中的内容也是不正规的
* **借助 Maven**:可以以规范的方式下载 Jar 包。因为所有知名框架或是第三方工具的 Jar 包已经按照统一规范存放在了 Maven 的中央仓库中,其内容也是可靠的
* **Tips**:统一规范不仅对于 IT 开发领域非常之重要,对于整个人类社会也是非常之重要
  1. 一个 Jar 包所依赖的其他 Jar 包需要手动加入项目中
* **产生的问题**:如果所有的 Jar 包依赖关系都需要程序员自己了解的特别清楚,就会极大地增加学习和开发成本
* **借助 Maven**:自动将 Jar 包所需的依赖包导入进来


FileUpload 组件 → IO 组件;commons-fileupload-1.3.jar 依赖于 commons-io-2.0.1.jar


![image-20211019214158362](https://gitee.com/songjianzaina/juejin_p11/raw/master/img/c1c275e724e2da79ed841ead255e2bfab4ec095207bbde3942d23d374c9880de)

1、Maven 到底是啥?

  • 概念:Maven 是 Java 平台上的自动化构建工具(Maven 本身也是使用 Java 编写的)
  • 构建工具发展历程:Make :arrow_right: Ant :arrow_right: Maven :arrow_right: Gradle

2、什么是构建?

以 Java 源文件、框架配置文件、HTML/CSS/JS/JSP、图片等资源为 原材料*,去 *生产 一个可以运行的工程项目的过程

  • 编译:Java 源文件 :arrow_right: ^编译^ :arrow_right: Class 字节码文件
  • 部署:一个 BS 项目最终运行的并不是动态 Web 工程本身,而是动态 Web 工程编译 后的结果

要深入地理解构建的含义,可以从以下三个层面来看:

  1. 纯 Java 代码

大家都知道,我们Java是一门编译型语言,.java扩展名的源文件需要编译成.class扩展名的字节码文件才能够执行。所以编写任何 Java 代码想要执行的话就必须经过编译得到对应的.class文件
2. Web 工程

当我们需要通过浏览器访问 Java 程序时就必须将包含 Java 程序的 Web 工程编译的结果 拿 到服务器上的指定目录,并启动服务器才行。这个 拿 的过程我们叫部署。我们可以将未编译的 Web 工程比喻为一只生的鸡,编译好的 Web 工程是一只煮熟的鸡,编译部署的过程就是将鸡炖熟。即:动态 Web 工程 :arrow_right: ^编译、部署^ :arrow_right: 编译结果 <=> 生鸡 :arrow_right: ^煮熟^ :arrow_right: 熟鸡

Web 工程和其编译结果的目录结构对比见下图

image-20211020211940092

开发过程中,所有的路径或配置文件中的类路径都是以编译结果的目录结构为标准的

Tips:运行时环境

image-20211019224029879

其实是一组 jar 包的引用,并没有把 jar 包本身复制到工程中,所以并不是目录
3. 实际项目

在实际项目中整合第三方框架, Web 工程中除了 Java 程序和 JSP 页面、图片等静态资源之外,还包括第三方框架的 jar 包以及各种各样的配置文件。所有这些资源都必须按照正确的目录结构部署到服务器上,项目才可以运行

所以综上所述:构建就是以我们编写的 Java 代码、框架配置文件、国际化等其他资源文件、JSP 页面和图片等静态资源作为 原材料*,去 *生产 出一个可以运行的项目的过程

3、构建过程中的各个环节

  • :one: 清理:讲之前编译得到的旧的.class字节码文件删除,为下一次编译做准备
  • :two: 编译:将 Java 源程序编译成 Class 字节码文件
  • :three: 测试:自动测试,调用 Junit 程序
  • :four: 报告:测试程序执行的结果
  • :five: 打包:动态 Web 工程打成 War 包,Java 工程打成 Jar 包
  • :six: 安装:讲打包得到的文件 复制 到 仓库 中的特定位置
  • :seven: 部署:将动态 Web 工程生成的 War 包 复制 到 Servlet 容器中的指定目录下,使其可以运行

4、自动化构建

其实上述环节我们在 Eclipse 中都可以找到对应的操作,只是不太标准。那么既然 IDE 已经可以进行构建了我们为什么还要使用 Maven 这样的构建工具呢?我们来看一个小故事:

这是阳光明媚的一天。托马斯向往常一样早早的来到了公司,冲好一杯咖啡,进入了自己的邮箱——很不幸,QA 小组发来了一封邮件,报告了他昨天提交的模块的测试结果——有BUG。

“好吧,反正也不是第一次”,托马斯摇摇头,进入 IDE,运行自己的程序,编译、打包、部署到服务器上,然后按照邮件中的操作路径进行测试。

“嗯,没错,这个地方确实有问题”,托马斯说道。于是托马斯开始尝试修复这个BUG,当他差不多有眉目的时候已经到了午饭时间。

下午继续工作。BUG很快被修正了,接着托马斯对模块重新进行了编译、打包、部署,测试之后确认没有问题了,回复了QA小组的邮件。

一天就这样过去了,明媚的阳光化作了美丽的晚霞,托马斯却觉得生活并不像晚霞那样美好啊。

让我们来梳理一下托马斯这一天中的工作内容

image-20211020220633809

从中我们发现,托马斯的很大一部分时间花在了“编译、打包、部署、测试”这些程式化的工作上面,而真正需要由“人”的智慧实现的分析问题和编码却只占了很少一部分

能否将这些程式化的工作交给机器自动完成呢?——当然可以!这就是自动化构建

image-20211020220810813

此时 Maven 的意义就体现出来了,它可以自动地从构建过程的起点一直执行到终点

5、安装 Maven 核心程序

  1. 检查 JAVA_HOME 环境变量

image-20211020221238991
2. 解压 Maven 核心程序的压缩包,放在一个非中文无空格路径下
3. 配置 Maven 相关的环境变量

* `MAVEN_HOME`或`M2_HOME`![image-20211020222833121](https://gitee.com/songjianzaina/juejin_p11/raw/master/img/15aed0a1f096848ec8a5b5e1bf2be48ea56325cf75b98fc2bc1b0db412bd3c18)


* `PATH`![image-20211020222907269](https://gitee.com/songjianzaina/juejin_p11/raw/master/img/92559943716a17aa20ed4de9f27abfc5ed3916b7c023b222008aa673a7f19bfb)
  1. 验证:运行mvn -v查看 Maven 版本

image-20211020223150834

6、Maven 的核心概念

  • :one: 约定的目录结构
  • :two: POM
  • :three: 坐标
  • :four: 依赖
  • :five: 仓库
  • :six: 生命周期 / 插件 / 目标
  • :seven: 继承
  • :eight: 聚合

7、约定的目录结构

  • 根目录:工程名
  • |—— src 目录:源码
  • |—— pom.xml文件:Maven 工程的核心配置文件
  • |———— main 目录:存放主程序
  • |———————— java 目录:存放 Java 源文件
  • |———————— resource 目录:存放框架或其他工具的配置文件
  • |———— test 目录:存放测试程序
  • |———————— java 目录:存放 Java 测试源文件
  • |———————— resource 目录:存放框架或其他工具的测试配置文件

为什么要遵守约定的目录结构呢?

  • 巧妇难为无米之炊:Maven 要负责我们项目的自动化构建,以编译为例,Maven 要想自动进行编译,就必须要知道 Java 源文件的位置
  • 如果想让框架或工具知道我们自定义的东西,方法有二:
    1. 以配置的方式指定:如<param-value>classpath:spring-context.xml</param-value>
    2. 遵守框架内部约定:如log4j.properties/log4j.xml
  • JavaEE 的开发共识:约定 > 配置 > 编码

8、常见的 Maven 命令

:heavy_exclamation_mark: :heavy_exclamation_mark: :heavy_exclamation_mark: 注意:执行与构建过程相关的 Maven 命令,必须进入 pom.xml 所在的目录

与构建过程相关:编译、测试、打包、.……

  • mvn clean:清理
  • mvn compile :编译主程序
  • mvn test-compile:编译测试程序
  • mvn test:执行测试
  • mvn package:打包

9、关于联网问题

Maven 核心程序仅仅是定义了抽象的生命周期,但是具体的工作必须要由插件来完成。而插件本身并不包含在 Maven 核心程序中

  1. 执行的 Maven 命令需要用到某些插件时,Maven 核心程序会首先在本地仓库中查找
    • 本地仓库的默认位置:【系统当前用户的家目录】\.m2\repository(windows:C:\Users\[用户名]\.m2\repository)
  2. 修改本地仓库的默认位置,Maven 核心程序会到我们事先准备好的目录下查找插件
    • 打开[Maven解压目录]\conf\settings.xml,找到localRepository标签,修改目录
  3. Maven 核心程序如果在本地仓库中找不到需要的插件,会自动到中央仓库下载。如果此时无法连接外网,则构建失败

10、第一个 Maven 程序

目录结构

image-20211022200617690

编写代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码public class Hello {
public String sayHello(String name) {
return "Hello " + name + "!";
}
}

public class HelloTest {
@Test
public void testHello() {
Hello hello = new Hello();
String results = hello.sayHello("world");
assertEquals("Hello world!", results);
}
}

编译

1
shell复制代码mvn compile

执行过程

image-20211022200853734

目录内容:

  • classes目录

image-20211022202257765

测试编译

1
shell复制代码mvn test-compile

执行过程

image-20211022202142335

目录内容:

  • classes目录:主程序编译后的字节码文件
  • test-classes目录:测试程序编译后的字节码文件

image-20211022202245195

测试

1
shell复制代码mvn test

执行过程

image-20211022202514540

目录结构

  • surefire-reports:测试报告,本例为com.vectorx.maven.HelloTest.txt
  • 测试报告会统计所有测试方法执行的最终结果

image-20211022202427603

image-20211022202839723

本例只有一个测试方法,并且最终运行结果为:运行成功数:1,运行失败数:0,运行错误数:0,跳过:0,总耗时:0.103 秒,说明本例中testHello测试方法断言正确,验证通过

打包

1
go复制代码mvn package

执行过程

image-20211022200943539

目录内容:

  • classes/test-classes:主程序 / 测试程序编译的字节码文件
  • surefire-reports:测试报告
  • Hello-0.0.1-SNAPSHOT.jar:jar 包文件
  • maven-archiver/maven-status等其他额外文件

image-20211022203445618

清理

1
shell复制代码mvn clean

执行过程

image-20211022200830744

目录内容:

  • 清理会清空整个target目录及其内容

image-20211022202049176

本文转载自: 掘金

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

RxJava 的基本概念和基本实现v 四个基本概念 回调方法

发表于 2021-10-22

RxJava 是一个在 Java VM 上使用可观测的序列来组成异步的、基于事件的程序的库。总之一句话就是异步。
它是一种扩展的观察者模式。

四个基本概念

  • Observable:被观察者
  • Observer:观察者
  • subscribe:订阅
  • 事件

回调方法

  • onNext():相当于 onClick() / onEvent()
  • onCompleted():当不会有新的 onNext()方法触发时调用
  • onError():时间队列异常,同时队列自动终止,且和onCompleted() 方法只能触发其中一个。

基本实现

Observer 观察者

1
2
3
4
5
6
7
8
java复制代码Observer<String> observer = new Observer<String>() { 
@Override
public void onNext(String s) { Log.d(tag, "Item: " + s); }
@Override
public void onCompleted() { Log.d(tag, "Completed!"); }
@Override
public void onError(Throwable e) { Log.d(tag, "Error!"); }
};

Subscriber 订阅者

Subscriber 对 Observer 接口进行了一些扩展,但他们的基本使用方式是完全一样的

1
2
3
4
5
6
7
8
java复制代码Subscriber<String> subscriber = new Subscriber<String>() { 
@Override
public void onNext(String s) { Log.d(tag, "Item: " + s); }
@Override
public void onCompleted() { Log.d(tag, "Completed!"); }
@Override
public void onError(Throwable e) { Log.d(tag, "Error!"); }
};

额外方法:

  • onStart():这是 Subscriber 新增方法,会在 subscribe 刚开始,而事件还未发送之前被调用,可以用于做一些准备工作,如果有线程要求,则不适用,因为总是在subscibe发生的线程被调用。
  • unsubscribe():用于取消订阅。在这个方法被调用后,Subscriber将不再接收事件;可用 isUnsubscribed() 判断是否订阅。在 subscribe() 之后, Observable 会持有 Subscriber 的引用,所以在合适的地方调用该方法,防止内存泄漏。

Observable 被观察者

  • create()创建
1
2
3
4
5
6
7
8
9
10
11
12
java复制代码Observable observable = Observable.create(new Observable.OnSubscribe<String>() { 
@Override
public void call(Subscriber<? super String> subscriber) {
subscriber.onNext("Hello");
subscriber.onNext("Hi");
subscriber.onNext("Aloha");
subscriber.onCompleted();
}
});
//这里传入一个 OnSubscribe 对象作为参数,OnSubscribe 会被存储在返回的
// Observable 对象中,相当一个计划表。Observable 被订阅的时候,
// OnSubscribe 的 call() 方法会自动被调用,事件序列就会依照设定依次触发。
  • just(T...) 将传入的参数依次发送过来
1
2
3
4
5
6
java复制代码Observable observable = Observable.just("Hello", "Hi", "Aloha");
// 将会依次调用:
// onNext("Hello");
// onNext("Hi");
// onNext("Aloha");
// onCompleted();
  • from(T[])/from(Iterable<? extends T>) 将传入的数组或 Iterable 拆分成具体对象后,依次发送出来
1
2
3
4
5
6
7
java复制代码String[] words = {"Hello", "Hi", "Aloha"};
Observable observable = Observable.from(words);
// 将会依次调用:
// onNext("Hello");
// onNext("Hi");
// onNext("Aloha");
// onCompleted();

其中 just(T…) 和 from(T[]) 和 create(OnSubscribe)是等价的。

Subscribe (订阅)

创建 Observable 和 Observer 之后,再用 subscribe() 方法链接起来

1
2
3
java复制代码observable.subscribe(observer);
// 或者:
observable.subscribe(subscriber);

被观察者,订阅观察者。当被订阅的时候,是 subscribe() 方法执行的时候

除了 subscribe(Observer) 和 subscribe(Subscriber) ,subscribe() 还支持不完整定义的回调:

Action

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复制代码Action1<String> onNextAction = new Action1<String>() {
// onNext()
@Override
public void call(String s) {
Log.d(tag, s);
}
};
Action1<Throwable> onErrorAction = new Action1<Throwable>() {
// onError()
@Override
public void call(Throwable throwable) {
// Error handling
}
};
Action0 onCompletedAction = new Action0() {
// onCompleted()
@Override
public void call() {
Log.d(tag, "completed");
}
};

// 自动创建 Subscriber ,并使用 onNextAction 来定义 onNext()
observable.subscribe(onNextAction);
// 自动创建 Subscriber ,并使用 onNextAction 和 onErrorAction 来定义 onNext() 和 onError()
observable.subscribe(onNextAction, onErrorAction);
// 自动创建 Subscriber ,并使用 onNextAction、 onErrorAction 和 onCompletedAction 来定义 onNext()、 onError() 和 onCompleted()
observable.subscribe(onNextAction, onErrorAction, onCompletedAction);
  • Action0
    只有 call() 一个回调方法,且是无参无返回值的。

onCompleted() 是无参无返回的函数,所以 Action0 可以定义不完整回调

  • Action1
    只有 call(T param) 一个回调方法,且是有一个参数 and 无返回值的。

onNext(T obj) 和 onError(Throwable error) 是有参数的,所以 Action1 可以定义不完整回调

总结

RXjava 是个好东西,但是学习起来还是有些难度,想要使用好它,只有在实战中不断的去实践。
以上这些是我以前整理的知识,希望对大家有用。最后推荐一个博客: 干货集中营

本文转载自: 掘金

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

【Go开源宝藏】CORS 跨域 与 CSRF攻击 中间件

发表于 2021-10-22

本文已参与 「掘力星计划」 ,赢取创作大礼包,挑战创作激励金

在这里插入图片描述

  1. 什么是跨域

当一个请求url的协议、域名、端口三者之间任意一个与当前页面url不同即为跨域

当前页面url 被请求页面url 是否跨域 原因
www.test.com/ www.test.com/index.html 否 同源(协议、域名、端口号相同)
www.test.com/ www.test.com/index.html 跨域 协议不同(http/https)
www.test.com/ www.baidu.com/ 跨域 主域名不同(test/baidu)
www.test.com/ blog.test.com/ 跨域 子域名不同(www/blog)
www.test.com:8080/ www.test.com:7001/ 跨域 端口号不同(8080/7001)

因为域的不一致,与此同时由于安全问题,请求就会受到同源策略限制

通常,浏览器会对跨域请求作出限制。
浏览器之所以要对跨域请求作出限制,是出于安全方面的考虑,因为跨域请求有可能被不法分子利用来发动 CSRF攻击。

  1. CSRF攻击

2.1 CSRF说明

CSRF(Cross-site request forgery)中文名称:跨站请求伪造,也被称为:one click attack/session riding,缩写为:CSRF/XSRF。
CSRF攻击者在用户已经登录目标网站之后,诱使用户访问一个攻击页面,利用目标网站对用户的信任,以用户身份在攻击页面对目标网站发起伪造用户操作的请求,达到攻击目的。

2.1 原理

​ CSRF 攻击的原理大致描述如下:

  1. 有两个网站,其中A网站是真实受信任的网站,而B网站是危险网站。
  2. 在用户登陆了受信任的A网站是,本地会存储A网站相关的Cookie,并且浏览器也维护这一个Session会话。
  3. 这时,如果用户在没有登出A网站的情况下访问危险网站B,那么危险网站B就可以模拟发出一个对A网站的请求(跨域请求)对A网站进行操作
  4. 而在A网站的角度来看是并不知道请求是由B网站发出来的(Session和Cookie均为A网站的),这时便成功发动一次 CSRF 攻击。

​ 因而 CSRF 攻击可以简单理解为:攻击者盗用了你的身份,以你的名义发送请求。

CSRF能够做的事情包括:以你名义发送邮件,发消息,盗取你的账号,甚至于购买商品,虚拟货币转账……造成的问题包括:个人隐私泄露以及财产安全。

  1. CORS

3.1 简介

跨源资源共享 Cross-Origin Resource Sharing(CORS) 是一个新的 W3C 标准,它新增的一组HTTP首部字段,允许服务端其声明哪些源站有权限访问哪些资源。

换言之,它允许浏览器向声明了 CORS 的跨域服务器,发出 XMLHttpReuest 请求,从而克服 Ajax 只能同源使用的限制

简单介绍一下CORS中新增的 HTTP 首部字段

  • Access-Control-Allow-Origin
    响应首部中可以携带这个头部表示服务器允许哪些域可以访问该资源
1
go复制代码c.Header("Access-Control-Allow-Origin", "*")
  • Access-Control-Allow-Methods
    预检请求的响应,指明实际请求所允许使用的HTTP方法
1
go复制代码c.Header("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE,UPDATE")
  • Access-Control-Allow-Headers
    首部字段用于预检请求的响应,指明了实际请求中允许携带的首部字段
1
go复制代码c.Header("Access-Control-Allow-Headers", "Authorization, Content-Length, X-CSRF-Token, Token,session,X_Requested_With,Accept, Origin, Host, Connection, Accept-Encoding, Accept-Language,DNT, X-CustomHeader, Keep-Alive, User-Agent, X-Requested-With, If-Modified-Since, Cache-Control, Content-Type, Pragma")
  • Access-Control-Max-Age
    首部字段用于预检请求的响应,指定了预检请求能够被缓存多久。
1
go复制代码c.Header("Access-Control-Max-Age", "172800")
  • Access-Control-Allow-Credentials
    首部字段用于预检请求的响应,指明实际请求所允许使用的HTTP方法。
1
go复制代码c.Header("Access-Control-Allow-Credentials", "false")

3.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
go复制代码func Cors() gin.HandlerFunc {
return func(c *gin.Context) {
method := c.Request.Method //请求方法
origin := c.Request.Header.Get("Origin") //请求头部
var headerKeys []string // 声明请求头keys
for k := range c.Request.Header {
headerKeys = append(headerKeys, k)
}
headerStr := strings.Join(headerKeys, ", ")
if headerStr != "" {
headerStr = fmt.Sprintf("access-control-allow-origin, access-control-allow-headers, %s", headerStr)
} else {
headerStr = "access-control-allow-origin, access-control-allow-headers"
}
if origin != "" {
c.Writer.Header().Set("Access-Control-Allow-Origin", "*")
c.Header("Access-Control-Allow-Origin", "*")
// 这是允许访问所有域
c.Header("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE,UPDATE")
//服务器支持的所有跨域请求的方法,为了避免浏览次请求的多次'预检'请求
// header的类型
c.Header("Access-Control-Allow-Headers", "Authorization, Content-Length, X-CSRF-Token, Token,session,X_Requested_With,Accept, Origin, Host, Connection, Accept-Encoding, Accept-Language,DNT, X-CustomHeader, Keep-Alive, User-Agent, X-Requested-With, If-Modified-Since, Cache-Control, Content-Type, Pragma")
// 允许跨域设置,可以返回其他子段
c.Header("Access-Control-Expose-Headers", "Content-Length, Access-Control-Allow-Origin, Access-Control-Allow-Headers,Cache-Control,Content-Language,Content-Type,Expires,Last-Modified,Pragma,FooBar")
// 跨域关键设置 让浏览器可以解析
c.Header("Access-Control-Max-Age", "172800")
// 缓存请求信息 单位为秒
c.Header("Access-Control-Allow-Credentials", "false")
// 跨域请求是否需要带cookie信息 默认设置为true
c.Set("content-type", "application/json")
// 设置返回格式是json
}
//放行所有OPTIONS方法
if method == "OPTIONS" {
c.JSON(http.StatusOK, "Options Request!")
}
c.Next() // 处理请求
}
}

配合gin框架使用

1
2
go复制代码	r:=gin.Default()
r.Use(middleware.Cors())

参考文献
1、www.jianshu.com/p/f880878c1…
2、blog.csdn.net/qq_38128179…

本文转载自: 掘金

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

手把手,带你从零封装Gin框架(六):初始化 Validat

发表于 2021-10-22

项目源码

地址: github.com/jassue/jass…

前言

Gin 自带验证器返回的错误信息格式不太友好,本篇将进行调整,实现自定义错误信息,并规范接口返回的数据格式,分别为每种类型的错误定义错误码,前端可以根据对应的错误码实现后续不同的逻辑操作,篇末会使用自定义的 Validator 和 Response 实现第一个接口

自定义验证器错误信息

新建 app/common/request/validator.go 文件,编写:

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

import (
"github.com/go-playground/validator/v10"
)

type Validator interface {
GetMessages() ValidatorMessages
}

type ValidatorMessages map[string]string

// GetErrorMsg 获取错误信息
func GetErrorMsg(request interface{}, err error) string {
if _, isValidatorErrors := err.(validator.ValidationErrors); isValidatorErrors {
_, isValidator := request.(Validator)

for _, v := range err.(validator.ValidationErrors) {
// 若 request 结构体实现 Validator 接口即可实现自定义错误信息
if isValidator {
if message, exist := request.(Validator).GetMessages()[v.Field() + "." + v.Tag()]; exist {
return message
}
}
return v.Error()
}
}

return "Parameter error"
}

新建 app/common/request/user.go 文件,用来存放所有用户相关的请求结构体,并实现 Validator 接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
go复制代码package request

type Register struct {
Name string `form:"name" json:"name" binding:"required"`
Mobile string `form:"mobile" json:"mobile" binding:"required"`
Password string `form:"password" json:"password" binding:"required"`
}

// 自定义错误信息
func (register Register) GetMessages() ValidatorMessages {
return ValidatorMessages{
"Name.required": "用户名称不能为空",
"Mobile.required": "手机号码不能为空",
"Password.required": "用户密码不能为空",
}
}

在 routes/api.go 中编写测试代码

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 routes

import (
"github.com/gin-gonic/gin"
"jassue-gin/app/common/request"
"net/http"
"time"
)

// SetApiGroupRoutes 定义 api 分组路由
func SetApiGroupRoutes(router *gin.RouterGroup) {
//...
router.POST("/user/register", func(c *gin.Context) {
var form request.Register
if err := c.ShouldBindJSON(&form); err != nil {
c.JSON(http.StatusOK, gin.H{
"error": request.GetErrorMsg(form, err),
})
return
}
c.JSON(http.StatusOK, gin.H{
"message": "success",
})
})
}

启动服务器,使用 Postman 测试,如下图所示,自定义错误信息成功

image-20211018192056332.png

自定义验证器

有一些验证规则在 Gin 框架中是没有的,这个时候我们就需要自定义验证器

新建 utils/validator.go 文件,定义验证规则,后续有其他的验证规则将统一存放在这里

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
go复制代码package utils

import (
"github.com/go-playground/validator/v10"
"regexp"
)

// ValidateMobile 校验手机号
func ValidateMobile(fl validator.FieldLevel) bool {
mobile := fl.Field().String()
ok, _ := regexp.MatchString(`^(13[0-9]|14[01456879]|15[0-35-9]|16[2567]|17[0-8]|18[0-9]|19[0-35-9])\d{8}$`, mobile)
if !ok {
return false
}
return true
}

新建 bootstrap/validator.go 文件,定制 Gin 框架 Validator 的属性

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 bootstrap

import (
"github.com/gin-gonic/gin/binding"
"github.com/go-playground/validator/v10"
"jassue-gin/utils"
"reflect"
"strings"
)

func InitializeValidator() {
if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
// 注册自定义验证器
_ = v.RegisterValidation("mobile", utils.ValidateMobile)

// 注册自定义 json tag 函数
v.RegisterTagNameFunc(func(fld reflect.StructField) string {
name := strings.SplitN(fld.Tag.Get("json"), ",", 2)[0]
if name == "-" {
return ""
}
return name
})
}
}

在 main.go 中调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
go复制代码package main

import (
"jassue-gin/bootstrap"
"jassue-gin/global"
)

func main() {
// ...

// 初始化验证器
bootstrap.InitializeValidator()

// 启动服务器
bootstrap.RunServer()
}

在 app/common/request/user.go 文件,增加 Resister 请求结构体中 Mobile 属性的验证 tag

注:由于在 InitializeValidator() 方法中,使用 RegisterTagNameFunc() 注册了自定义 json tag, 所以在 GetMessages() 中自定义错误信息 key 值时,需使用 json tag 名称

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
go复制代码package request

type Register struct {
Name string `form:"name" json:"name" binding:"required"`
Mobile string `form:"mobile" json:"mobile" binding:"required,mobile"`
Password string `form:"password" json:"password" binding:"required"`
}


func (register Register) GetMessages() ValidatorMessages {
return ValidatorMessages{
"name.required": "用户名称不能为空",
"mobile.required": "手机号码不能为空",
"mobile.mobile": "手机号码格式不正确",
"password.required": "用户密码不能为空",
}
}

重启服务器,使用 PostMan 测试,如下图所示,自定义验证器成功

image-20211018194531068.png

自定义错误码

新建 global/error.go 文件,将项目中可能存在的错误都统一存放到这里,为每一种类型错误都定义一个错误码,便于在开发过程快速定位错误,前端也可以根据不同错误码实现不同逻辑的页面交互

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
go复制代码package global

type CustomError struct {
ErrorCode int
ErrorMsg string
}

type CustomErrors struct {
BusinessError CustomError
ValidateError CustomError
}

var Errors = CustomErrors{
BusinessError: CustomError{40000, "业务错误"},
ValidateError: CustomError{42200, "请求参数错误"},
}

封装 Response

新建 app/common/response/response.go 文件,编写:

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

import (
"github.com/gin-gonic/gin"
"jassue-gin/global"
"net/http"
)

// 响应结构体
type Response struct {
ErrorCode int `json:"error_code"` // 自定义错误码
Data interface{} `json:"data"` // 数据
Message string `json:"message"` // 信息
}

// Success 响应成功 ErrorCode 为 0 表示成功
func Success(c *gin.Context, data interface{}) {
c.JSON(http.StatusOK, Response{
0,
data,
"ok",
})
}

// Fail 响应失败 ErrorCode 不为 0 表示失败
func Fail(c *gin.Context, errorCode int, msg string) {
c.JSON(http.StatusOK, Response{
errorCode,
nil,
msg,
})
}

// FailByError 失败响应 返回自定义错误的错误码、错误信息
func FailByError(c *gin.Context, error global.CustomError) {
Fail(c, error.ErrorCode, error.ErrorMsg)
}

// ValidateFail 请求参数验证失败
func ValidateFail(c *gin.Context, msg string) {
Fail(c, global.Errors.ValidateError.ErrorCode, msg)
}

// BusinessFail 业务逻辑失败
func BusinessFail(c *gin.Context, msg string) {
Fail(c, global.Errors.BusinessError.ErrorCode, msg)
}

实现用户注册接口

新建 utils/bcrypt.go 文件,编写密码加密及验证密码的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
go复制代码package utils

import (
"golang.org/x/crypto/bcrypt"
"log"
)

func BcryptMake(pwd []byte) string {
hash, err := bcrypt.GenerateFromPassword(pwd, bcrypt.MinCost)
if err != nil {
log.Println(err)
}
return string(hash)
}

func BcryptMakeCheck(pwd []byte, hashedPwd string) bool {
byteHash := []byte(hashedPwd)
err := bcrypt.CompareHashAndPassword(byteHash, pwd)
if err != nil {
return false
}
return true
}

新建 app/services/user.go 文件,编写用户注册逻辑

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

import (
"errors"
"jassue-gin/app/common/request"
"jassue-gin/app/models"
"jassue-gin/global"
"jassue-gin/utils"
)

type userService struct {
}

var UserService = new(userService)

// Register 注册
func (userService *userService) Register(params request.Register) (err error, user models.User) {
var result = global.App.DB.Where("mobile = ?", params.Mobile).Select("id").First(&models.User{})
if result.RowsAffected != 0 {
err = errors.New("手机号已存在")
return
}
user = models.User{Name: params.Name, Mobile: params.Mobile, Password: utils.BcryptMake([]byte(params.Password))}
err = global.App.DB.Create(&user).Error
return
}

新建 app/controllers/app/user.go 文件,校验入参,调用 UserService 注册逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
go复制代码package app

import (
"github.com/gin-gonic/gin"
"jassue-gin/app/common/request"
"jassue-gin/app/common/response"
"jassue-gin/app/services"
)

// Register 用户注册
func Register(c *gin.Context) {
var form request.Register
if err := c.ShouldBindJSON(&form); err != nil {
response.ValidateFail(c, request.GetErrorMsg(form, err))
return
}

if err, user := services.UserService.Register(form); err != nil {
response.BusinessFail(c, err.Error())
} else {
response.Success(c, user)
}
}

在 routes/api.go 中,添加路由

1
2
3
4
5
6
7
8
9
10
11
go复制代码package routes

import (
"github.com/gin-gonic/gin"
"jassue-gin/app/controllers/app"
)

// SetApiGroupRoutes 定义 api 分组路由
func SetApiGroupRoutes(router *gin.RouterGroup) {
router.POST("/auth/register", app.Register)
}

使用 Postman 调用接口 http://localhost:8888/api/auth/register ,如下图所示,接口返回成功

image-20211022184652754.png

查看数据库 users 表,数据已成功写入

image-20211022184849385.png

本文转载自: 掘金

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

redis 十一、redis之Bitmaps 一、Bit

发表于 2021-10-22

redis系列文章:

liudongdong.top/categories/…

本篇来源:

liudongdong.top/archives/re…

公众号:雨中散步撒哈拉

备注:欢迎关注公众号,一起学习,共同进步!

一、Bitmaps(位图)

Bitmaps 并不是实际的数据类型,而是定义在String类型上的一个面向字节操作的集合。因为字符串是二进制安全的块,他们的最大长度是512M,最适合设置成2^32个不同字节。

Bitmaps 的最大优势之一在存储信息时极其节约空间。例如,在一个以增量用户ID来标识不同用户的系统中,记录用户的四十亿的一个单独bit信息(例如,要知道用户是否想要接收最新的来信)仅仅使用512M内存。

  1. getbit key offset

获取位图指定索引的值

1
2
3
4
5
6
7
8
9
makefile复制代码127.0.0.1:6379> set bitmap big
OK
127.0.0.1:6379> getbit bitmap 0
(integer) 0
127.0.0.1:6379> getbit bitmap 1
(integer) 1
127.0.0.1:6379> getbit bitmap 2
(integer) 1
127.0.0.1:6379>

image.png

  1. setbit key offset value

给位图指定索引设置值,返回该索引位置的原始值

1
2
3
4
5
makefile复制代码127.0.0.1:6379> setbit bitmap 7 1
(integer) 0
127.0.0.1:6379> get bitmap
"cig"
127.0.0.1:6379>
  1. bitcount key [start end]

获取位图指定范围(start到end,单位为字节,如果不指定就是获取全部)位值为1的个数。

1
2
3
4
5
6
7
makefile复制代码127.0.0.1:6379> bitcount bitmap
(integer) 13
127.0.0.1:6379> setbit bitmap 8 1
(integer) 0
127.0.0.1:6379> bitcount bitmap
(integer) 14
127.0.0.1:6379>
  1. bitop and|or|not|xor destkey key [key…]

做多个bitmap的and(交集)、or(并集)、not(非)、xor(异或)操作并将结果保存到destkey中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
makefile复制代码127.0.0.1:6379> set hello good
OK
127.0.0.1:6379> set world good
OK
127.0.0.1:6379> bitop and hello_world hello world
(integer) 4
127.0.0.1:6379> get hello_world
"good"
127.0.0.1:6379> bitop or hello_world hello world
(integer) 4
127.0.0.1:6379> get hello_world
"good"
127.0.0.1:6379> bitop not hello_world hello
(integer) 4
127.0.0.1:6379> get hello_world
"\x98\x90\x90\x9b"
127.0.0.1:6379> bitop xor hello_world hello world
(integer) 4
127.0.0.1:6379> get hello_world
"\x00\x00\x00\x00"
127.0.0.1:6379>
  1. bitpos key targetBit [start] [end] (起始版本:2.8.7)

计算位图指定范围(start到end,单位为字节,如果不指定就是获取全部)第一个偏移量对应的值等于targetBit的位置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
bash复制代码127.0.0.1:6379> set hello big
OK
127.0.0.1:6379> bitpos hello 1
(integer) 1
127.0.0.1:6379> bitpos hello 0
(integer) 0
127.0.0.1:6379> bitpos hello 1 2 2
(integer) 17
127.0.0.1:6379> bitpos hello 1 2 3
(integer) 17
127.0.0.1:6379> bitpos hello 0 2 3
(integer) 16
127.0.0.1:6379> bitpos hello 0 0 3
(integer) 0
127.0.0.1:6379> bitpos hello 1 0 3
(integer) 1

实战应用

独立用户访问统计

  1. 使用 set 和 Bitmap (前提是用户的ID必须是整型)
  2. 1亿用户,五千万独立
数据类型 每个userId占用空间 需要存储的用户量 内存使用总量
set 32位(假设userId用的是integer) 50,000,000 32位*50,000,000=200MB
Bitmap 1位 100,000,000 1位*100,000,000=12.5MB
  1. 若只有10万独立用户
数据类型 每个userId占用空间 需要存储的用户量 内存使用总量
set 32位(假设userId用的是整型) 100,000 32位*100,000=4MB
Bitmap 1位 100,000,000 1位*100,000,000=12.5MB

本文转载自: 掘金

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

Oracle 19c ADG Swithover 切换手册

发表于 2021-10-22

作者 | JiekeXu

来源 | JiekeXu DBA之路(ID: JiekeXu_IT)

大家好,我是 JiekeXu,很高兴又和大家见面了,今天和大家一起来看看 Oracle 19c ADG Swithover 切换流程,欢迎点击上方蓝字关注我,标星或置顶,更多干货第一时间到达!

本文档主要是根据前面文档中搭建的 Oracle 19c MAA 架构的备库进行 Swithover 切换,然后将备库变为主库,主库变为备库,达到一个迁移的目的。

源地址IP为 192.168.0.86/87,目标端IP地址段为:192.168.21.81-87。规则如下:其中81/82/83为SCAN,86/84为一节点的PUB/VIP,87/85为二节点的PUB/VIP。

其中IP地址列表如下:

源端IP 192.168.0.86/87 目标端IP 192.168.21.86/87
源端 版本 Linux7 RAC 19.4.0 目标端 版本 Linux7 RAC 19.4.0
源端 字符集 AL32UTF8 目标端 字符集 AL32UTF8
源端 db edw 目标端 db edwstb

文章标题《Oracle 19c ADG Swithover 切换手册》那必定可以当手册使用,文档也已经整理好了,在本公众号后台回复关键字【A DG 切换 手册】获取本文文档版本。

1、切换前准备工作

Ø 检查集群状态,监听状态

1
2
lua复制代码crsctl status  res -t
lsnrctl status

Ø 检查主备库打开状态,模式

1
2
3
4
5
6
7
sql复制代码检查数据库实例状态,正常状态为open
select instance_name,status from gv$instance;
检查数据库打开模式
OPEN_MODE正常状态为READ WRITE
LOG_MODE正常状态为ARCHIVELOG
DATABASE_ROLE正常状态为PRIMARY
select INST_ID,NAME,open_mode,LOG_MODE,DATABASE_ROLE,PROTECTION_MODE,DB_UNIQUE_NAME from gv$database;

Ø 主备库的 DG 参数检查确认

1
2
3
4
5
6
7
8
sql复制代码show parameter db_file_name_convert
show parameter log_file_name_convert
show parameter standby_file_management
show parameter fal_client
show parameter fal_server
show parameter log_archive_config
show parameter log_archive_dest_2
show parameter log_archive_dest_2_state

Ø 检查主库

1
2
3
4
5
6
7
sql复制代码col DEST_NAME for a30
select DEST_ID,DEST_NAME,STATUS,RECOVERY_MODE from V$ARCHIVE_DEST_STATUS where DEST_NAME='LOG_ARCHIVE_DEST_2';
status正常状态为VALID ,如果备库停止日志传输,设置defer则status为BAD PARAM RECOVERY_MODE正常状态为 MANAGED REAL TIME APPLY
如果备库恢复模式不是“REAL TIME APPLY”,备库重启日志恢复进程,起用“REAL TIME APPLY”
[备库][oracle用户][节点1]
ALTER DATABASE RECOVER MANAGED STANDBY DATABASE CANCEL;
ALTER DATABASE RECOVER MANAGED STANDBY DATABASE USING CURRENT LOGFILE DISCONNECT;

Ø 检查主库、备库确定有足够的归档进程

1
2
sql复制代码log_archive_max_processes值需大于等于4,但也不会太大。
show parameter  LOG_ARCHIVE_MAX_PROCESSES

Ø 检查standby redo是否创建了

1
csharp复制代码select * from v$logfile;

Ø 备库检查redo logs是否需要清理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
sql复制代码[备库][oracle用户][节点1][SQL]
备库查询redo logs是否需要清理,有数据则说明需要清理
SQL>SELECT DISTINCT L.GROUP# FROM V$LOG L, V$LOGFILE LF
WHERE L.GROUP# = LF.GROUP#
AND L.STATUS NOT IN ('UNUSED','CLEARING','CLEARING_CURRENT');

如果存在需要清理的redo,可通过以下两种方式处理:
1)备库设置LOG_FILE_NAME_CONVERT参数,主备切换时会自动清理redo logs,该参数为静态参数,如搭建时未设置,设置需重启数据库生效。如该参数已设置,官方仍建议切换前清理redo logs
show parameter LOG_FILE_NAME_CONVERT;
设置方法:
alter system set log_file_name_convert='+ARCH','+ARCH' scope=spfile;
shutdown immediate;
startup;
2)需要停止日志应用,通过group#清理redo logs
取消日志应用
ALTER DATABASE RECOVER MANAGED STANDBY DATABASE CANCEL;
清理redo logs
SQL> ALTER DATABASE CLEAR LOGFILE GROUP <需要清理的日志组号>;
重新应用日志
ALTER DATABASE RECOVER MANAGED STANDBY DATABASE USING CURRENT LOGFILE DISCONNECT;

Ø 检查主库standby日志情况

检查主库是否创建了standby的redo log日志

注意:standby的redo log日志要比源库的redo log日志多一组,大小和redo log大小一致,例如源库数据库的 redo log 为2组4个,那么standby 的redo log应该为3组6个

1)查看redo log情况

1
vbnet复制代码select group#,thread#,sequence#,members,archived,status,bytes/1024/1024  M from v$log order by 1;

2)查看standby日志情况

1
2
3
4
5
6
7
8
9
10
sql复制代码col MEMBER for a50
select GROUP#,THREAD#,BYTES/1024/1024 M,STATUS from v$STANDBY_log;
如果主库没有standby日志组,可使用如下语句添加
添加standby日志组:(比主库多添加一组)
alter database add standby logfile thread 1 size 512m;
alter database add standby logfile thread 1 size 512m;
alter database add standby logfile thread 1 size 512m;
alter database add standby logfile thread 2 size 512m;
alter database add standby logfile thread 2 size 512m;
alter database add standby logfile thread 2 size 512m;

Ø 备库查询应用到的日志REDO SEQUENCE

主库查询当前的REDO SEQUENCE

SELECT THREAD#, SEQUENCE# FROM V$THREAD;

备库查询应用到的日志REDO SEQUENCE ,正常查询结果和上面主库的REDO SEQUENCE 数值只差1~2个

1
2
3
4
5
sql复制代码SELECT THREAD#, MAX(SEQUENCE#) FROM V$ARCHIVED_LOG
WHERE APPLIED = 'YES'
AND RESETLOGS_CHANGE# = (SELECT RESETLOGS_CHANGE#
FROM V$DATABASE_INCARNATION WHERE STATUS = 'CURRENT')
GROUP BY THREAD#;

Ø 备库检查数据库是否有gap

1
2
arduino复制代码检查数据库是否有gap,正常没有
select thread#,low_sequence#,high_sequence# from v$archive_gap;

Ø 检查下当前应用进程是否有延时

1
2
3
4
5
sql复制代码column name format a13;
column value format a20;
column unit format a30;
column TIME_COMPUTED format a30;
select name,value,unit,time_computed from v$dataguard_stats where name in ('transport lag','apply lag');

Ø 关闭应用并确认当前连接会话

1
2
3
csharp复制代码select username,sid,status,event,program,machine,sql_id from v$session where username !='SYS';
  
select username,sid,status,event,program,machine,sql_id,logon_time from gv$session where username !='SYS' order by logon_time desc;

Ø 主库、备库确定数据文件、临时文件状态

确定主备数据库临时文件一致,且所有的数据文件都是online的

1
2
3
4
5
6
sql复制代码col FILENAME for a50
SELECT TMP.NAME FILENAME, BYTES/1024/1024 M, TS.NAME TABLESPACE FROM V$TEMPFILE TMP, V$TABLESPACE TS WHERE TMP.TS#=TS.TS#;

SELECT NAME FROM V$DATAFILE WHERE STATUS='OFFLINE';
如果存储offline的数据文件,且是切换为主库所需要的数据文件,需要online
SQL> ALTER DATABASE DATAFILE 'datafile-name' ONLINE;

Ø 主库验证当前是否可转换为备库

检查主库 switchover_status状态,正常结果为TO STANDBY或SESSION ACTIVE

1
csharp复制代码select switchover_status from v$database;

注意事项:

如果switchover_status为TO_STANDBY说明可以直接转换

alter database commit to switchover to physical standby;

如果switchover_status为SESSIONS ACTIVE ,但是查询V$SESSION会话,都是系统会话,可以通过如下命令在主库进行SWITCHOVER切换。

1
2
3
4
5
6
7
8
9
10
11
12
13
sql复制代码alter database commit to switchover to physical standby with session shutdown;

SQL>SELECT SWITCHOVER_STATUS FROM V$DATABASE;
SWITCHOVER_STATUS
----------------------------------
TO STANDBY 或者SESSIONS ACTIVE

--备库状态
SQL>SELECT SWITCHOVER_STATUS FROM V$DATABASE;

SWITCHOVER_STATUS
--------------------
NOT ALLOWED

Ø 再次确认下主库上面的crontab job是否转移到备库上了

1
复制代码crontab -l

2、正式切换 s witchover

  1. 修改主库ORA30A参数(当主备库的目录结构不一致时需要修改此参数)
1
2
3
sql复制代码#以下参数需要重启方能生效
alter system set db_file_name_convert='+DATA','+DATA' scope=spfile;
alter system set log_file_name_convert='+FRA','+FRA' scope=spfile;
  1. 修改备库ORA30A_JX参数(当主备库的目录结构不一致时需要修改此参数)
1
2
3
sql复制代码#以下参数需要重启方能生效
alter system set db_file_name_convert='+DG_DATA/ora30astd_jx' ,'+DG_DATA/ora30a_jx' scope=spfile;
alter system set log_file_name_convert='+DG_REDO/ora30astd_jx','+DG_REDO/ora30a_jx' scope=spfile;

本次 MAA 切换不需要修改此参数。

  1. edw主库关闭实例2
1
arduino复制代码srvctl stop instance -d edw -i edw2 -o immediate
  1. edwstb备库关闭实例2
1
arduino复制代码srvctl stop instance -d edwstb  -i edwstb2 -o immediate
  1. edw 主库切换到standby
1
sql复制代码SQL> ALTER DATABASE COMMIT TO SWITCHOVER TO PHYSICAL STANDBY WITH SESSION SHUTDOWN;

  1. 验证备库的切换状态
1
2
3
4
sql复制代码SQL>SELECT SWITCHOVER_STATUS FROM V$DATABASE;
SWITCHOVER_STATUS
--------------------
TO PRIMARY  --如果备库为 SESSIONS ACTIVE 但查询没有会话则可以重启一下
  1. 切换备库为主库
1
2
sql复制代码SQL>ALTER DATABASE COMMIT TO SWITCHOVER TO PRIMARY;
SQL>alter database open;

  1. 打开新备库的日志同步进程
1
2
3
sql复制代码SQL> startup mount

SQL>ALTER DATABASE RECOVER MANAGED STANDBY DATABASE USING CURRENT LOGFILE DISCONNECT FROM SESSION;

9. 验证切换后的结果

主库进行日志切换,查看备库的日志,看是否开始接收并应用。

经过检查多次切换日志,新备库均没有接受到日志,这个可能的原因比较多,按照之前说的检查备库 alert 日志,没有报错,只有一句提示:

2021-06-09T18:50:01.005734+08:00

TT02 (PID:26883): LOG_FILE_NAME_CONVERT is not defined, stop clearing ORLs

那么既然有这个提示就修改一下这个参数吧。只在备库修改,然后重启备库,应用日志后还是没有同步。

1
2
sql复制代码ALTER SYSTEM SET db_file_name_convert='+DATA','+DATA' SCOPE=SPFILE;
alter system set log_file_name_convert='+DATA','+DATA' scope=spfile;

不过终于在主库的 alert 日志中发现了错误 ORA-16047

ORA-16047: DGID mismatch between destination setting and target database

2021-06-09T19:27:17.609158+08:00

查看错误居然报备库 DB_UNIQUE_NAME 不匹配,检查备库参数后也没问题。

1
2
3
4
5
6
7
csharp复制代码[oracle]$ oerr ora 16047
16047, 00000, "DGID mismatch between destination setting and target database"
// *Cause: The DB_UNIQUE_NAME specified for the destination did not match
// the DB_UNIQUE_NAME at the target database.
// *Action: Ensure that the DB_UNIQUE_NAME specified in the LOG_ARCHIVE_DEST_n
// parameter matches the DB_UNIQUE_NAME parameter defined at the
//          destination.

然后通过以下视图查看时,在新主库上发现了错误。参数 LOG_ARCHIVE_DEST_2 配置错误,参数中 SERVICE 和 DB_UNIQUE_NAME 果然不匹配,重新修改后备库立马恢复正常,同步正常。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
sql复制代码select dest_name,status,error from v$archive_dest;

19:37:22 SQL> show parameter LOG_ARCHIVE_DEST_2

NAME TYPE VALUE
------------------------------------ ----------- ------------------------------
log_archive_dest_2 string SERVICE=EDWSTB LGWR ASYNC VALI
D_FOR=(ONLINE_LOGFILES,PRIMARY
_ROLE) DB_UNIQUE_NAME=edw

19:37:32 SQL> alter system set LOG_ARCHIVE_DEST_2='SERVICE=EDW LGWR ASYNC VALID_FOR=(ONLINE_LOGFILES,PRIMARY_ROLE) DB_UNIQUE_NAME=edw';

System altered.

Elapsed: 00:00:00.03
19:38:20 SQL> select dest_name,status,error from v$archive_dest;

DEST_NAME STATUS ERROR
------------------------------ --------- -----------------------------------------------------------------
LOG_ARCHIVE_DEST_1 VALID
LOG_ARCHIVE_DEST_2 VALID
LOG_ARCHIVE_DEST_3             INACTIVE
  1. 备库只读打开(可选)

可根据实际情况,选择是否打开备库。

1
2
3
sql复制代码SQL> ALTER DATABASE RECOVER MANAGED STANDBY DATABASE CANCEL;
SQL> alter database open;
SQL> ALTER DATABASE RECOVER MANAGED STANDBY DATABASE USING CURRENT LOGFILE DISCONNECT FROM SESSION;
  1. 打开RAC的另一个节点

主库和备库的另一个节点都可以打开。

1
复制代码startup

3、检查修改DBLINK

由于迁移主机发生变化了,因此需要检查确认是否有其他数据库配置了到本数据库的DBLINK连接。检查下是否存在其他库到本库的DBLINK,若有,则在那些库上面,修改dblink涉及到的本数据库服务器IP地址。

1
csharp复制代码select * from dba_db_links;

4、修改应用连接 IP 地址

由于数据库服务器IP地址发生了变化,因此需要将应用连接到数据库的IP地址也进行修改。

5、启应用

以上步骤全部完成之后,启应用,检查日志并确认下应用连接是否正常,业务测试。


本次分享到此结束啦~

❤️ 欢迎关注我的公众号,来一起玩耍吧!!!

——————————————————————–—–————

**公众号:JiekeXu DBA之路

墨天轮:www.modb.pro/u/4347

CSDN :blog.csdn.net/JiekeXu

腾讯云:cloud.tencent.com/developer/u…**

—————————————————————————-———

基于 VMWARE Oracle Linux7.9 安装 Oracle19c RAC 详细配置方案

爆肝一万字终于把 Oracle Data Guard 核心参数搞明白了

本文转载自: 掘金

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

1…476477478…956

开发者博客

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