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

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


  • 首页

  • 归档

  • 搜索

Mybatis 的缓存设置

发表于 2021-11-26

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


MyBatis包含一个非常强大的查询缓存特性,它可以非常方便地定制和配置缓存。缓存可以极大的提升查询效率。
MyBatis系统中默认定义了两级缓存:一级缓存和二级缓存

  • 默认情况下,只有一级缓存开启。(SqlSession级别的缓存,也称为本地缓存)
  • 二级缓存需要手动开启和配置,他是基于namespace级别的缓存。
  • 为了提高扩展性,MyBatis定义了缓存接口Cache。可以通过实现Cache接口来自定义二级缓存

一级缓存

一级缓存也叫本地缓存:

  • 与数据库同一次会话期间查询到的数据会放在本地缓存中。
  • 以后如果需要获取相同的数据,直接从缓存中拿,没必须再去查询数据库;

二级缓存

二级缓存也叫全局缓存,一级缓存作用域太低了,所以诞生了二级缓存

  • 基于namespace级别的缓存,一个名称空间,对应一个二级缓存
  • 工作机制
    • 一个会话查询一条数据,这个数据就会被放在当前会话的一级缓存中;
      • 如果当前会话关闭了,这个会话对应的一级缓存就没了;但是我们想要的是,会话关闭了,一级缓存中的数据被保存到二级缓存中;
      • 新的会话查询信息,就可以从二级缓存中获取内容;
      • 不同的 mapper 查出的数据会放在自己对应的缓存(map)中;

使用步骤:

  1. 在 核心配置文件中开启全局缓存
1
xml复制代码<setting name="cacheEnabled" value="true"/>
  1. 每个 mapper.xml 中配置使用二级缓存
1
xml复制代码<cache/>

高级配置

1
2
3
4
5
xml复制代码<cache
eviction="FIFO"
flushInterval="60000"
size="512"
readOnly="true"/>

这个更高级的配置创建了一个 FIFO 缓存,每隔 60 秒刷新,最多可以存储结果对象或列表的 512 个引用,而且返回的对象被认为是只读的,因此对它们进行修改可能会在不同线程中的调用者产生冲突。

可用的清除策略有:

* `LRU` – 最近最少使用:移除最长时间不被使用的对象。
* `FIFO` – 先进先出:按对象进入缓存的顺序来移除它们。
* `SOFT` – 软引用:基于垃圾回收器状态和软引用规则移除对象。
* `WEAK` – 弱引用:更积极地基于垃圾收集器状态和弱引用规则移除对象。默认的清除策略是 LRU。

flushInterval(刷新间隔)属性可以被设置为任意的正整数,设置的值应该是一个以毫秒为单位的合理时间量。 默认情况是不设置,也就是没有刷新间隔,缓存仅仅会在调用语句时刷新。

size(引用数目)属性可以被设置为任意正整数,要注意欲缓存对象的大小和运行环境中可用的内存资源。默认值是 1024。

readOnly(只读)属性可以被设置为 true 或 false。只读的缓存会给所有调用者返回缓存对象的相同实例。 因此这些对象不能被修改。这就提供了可观的性能提升。而可读写的缓存会(通过序列化)返回缓存对象的拷贝。 速度上会慢一些,但是更安全,因此默认值是 false。

提示: 二级缓存是事务性的。这意味着,当 SqlSession 完成并提交时,或是完成并回滚,但没有执行 flushCache=true 的 insert/delete/update 语句时,缓存会获得更新。


本文转载自: 掘金

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

一篇长文带你领略super 关键字

发表于 2021-11-26

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

super和 this 可以对比着学习:

1,this

  • this 是一个引用,保存内存地址指向自己。
  • this 出现在实例方法中,谁调用这个实例方法,this 就代表谁,this 代表当前正在执行 这个动作的对象。
  • this 不能出现在静态方法中。
  • this 大部分情况下可以省略,在方法中区分实例变量和局部变量的时候不能省略。
  • “this(实际参数列表)”出现在构造方法第一行,通过当前的构造方法去调用本类当中 其它的构造方法。

2,super

  • 严格来说,super 其实并不是一个引用,它只是一个关键字,super代表了当前对象中 从父类继承过来的那部分特征。this 指向一个独立的对象,Super 并不是指向某个“独立” 的对象,假设张大明是父亲,张小明是儿子,有这样一句话:大家都说张小明的眼睛、鼻子和父亲很像。和父亲的很像。那么也就是说儿子继承了父亲的眼睛和鼻子特征,那么眼睛和鼻子肯定最终 还是长在儿子的身上。假设 this 指向张小明,那么 super就代表张小明身上的眼睛和鼻子。 换句话说 super 其实是 this 的一部分。
  • 举个栗子:张大明和张小明其实是两个独立的对象,两个对象内存方面没有联系,super 只是代表张小明对象身上的眼睛和鼻子,因为这个是从父类中继承过来的,在内存方面使用了 super 关键字进行了标记,对于下图来说“this.眼睛”和“super 眼睛”都是访问的同一块内存空间。
  • super 和 this 都可以使用在实例方法当中。
  • super 不能使用在静态方法当中,因为 super代表了当前对象上的父类型特征,静态方 法中没有 this,肯定也是不能使用 super的。
  • super 也有这种用法:“super(实际参数列表);” 这种用法是通过当前的构造方法调用父类的构造方法。

super和this 都可以使用在实例方法中,并且都不能使用在静态方法中,“this”大部分情况下都是可以省略的,只有在方法中区分局部变量和实例变量的时候不能省略。

看下面的代码进行体会学习:

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
typescript复制代码//书
public class Book {
//书名
String name;
//构造方法
public Book(){
super();
}
public Book(String name){
super();
this.name= name;
}
//纸质羽书
public class PaperBook extends Book{
//构造方法
public PaperBook(){
super();
}
public PaperBook(String name){
super();
this.name = name;
}
//打印书名
public void printName(){
System.out.println("this.name->书名:"+ this.name); System.out.println("super.name->书名:"+ super.name);
}
}
public class BookTest{
public static void main(String[] args){
paperBook book1=new PaperBook("零基础学Java");
book1.printName();
}
}
//程序运行结果如下:
this.name->书名:零基础学Java
super.name->书名:零基础学Java

我们发现printName()方法中super.name和this.name最终输出结果是一样的,这是为什么呢?

因为this.name和super.name实际上是同一块内存空间,所以它们的输出结果是完全一样的。

通过上面的例子,我们最终得出一个结论:父类和子类中有同名实例变量或者有同名的实例方法,想在子类中访问父类中的实例变量或实例方法,则super 是不能省略的,其他情况都可以省略。

super 使用在构造方法中,语法格式为:super(实际参数列表),这行代码和“this(实际参数列表)”都是只允许出现在构造方法的第一行(这一点记住就行了),所以这两行代码是无法共存的。“super(实际参数列表)”这种语法表示子类构造方法执行过程中调用父类的构造方法。

下面来看一段代码:

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
arduino复制代码public class People{
String idCand;
String name;
boalean sex;
public People(){

}
public People(String idCard,String name.,boolean sex ){
this.idCard = idCard;
this.name = name;
this.sex = sex;
}
public class Student extends People{
// 学号是子类特有的
int sno;
public Student(){

}
public Student(String idCard,String name,boolean sex,int sno){ this.idCard = idCard;
this.name = name;
this.sex=sex;
this.sno= sno;
}
}
public class StudentTest {
public static void main(Stringll args){
Student s = new Student("12345x","jack"true,100); System.out.printin("身份证号"+s.idCard); System.out.println("姓名"+s.name);
System.out.printin("性别“+ s.sex);
System.out.println("学号"+ s.sno);
}
}
//运行结果如下:
身份证号12345x
姓名jack
性别true
学号100

通过以上代码的学习,“super(实际参数列表);”语法表示调用父类的构造方法,代码复用性增强了 另外一方面也是模拟现实世界当中的“要想有儿子,必须先有父亲”的道理。

不过这里的“super(实际参数列表)”在调用父类构造方法的时候,从本质上来说并不是创建一个“ 立的父类对象”,而是为了完成当前对象的父类型特征的初始化操作。(或者说通过子类的构造方法 调用父类的构造方法,是为了让张小明身上长出具有他父亲特点的鼻子和眼睛,鼻子和眼睛初始化完毕之后,具有父亲的特点,但最终还是长在张小明的身上)。

接下来,再来看一段代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
css复制代码public class A{
public A(){
System.out.println("A 类的无参数构遗方法执行“;
}
}
public class B extends A{
public B(){
System.out.println("B 类的无参数构造方法执行");
}
}
public class C extends B{
public C(){
System.out.,printiln("C类的无参数构造方法执行");
}
}
public class Test {
public static void main(String[] args){
new C();
}
}
//程序运行结果如下:
A类的无参构造方法执行
B类的无参构造方法执行
C类的无参构造方法执行

通过以上内容的学习,super()的作用主要有以下两点:

  • 1,调用父类的构造方法,使用这个构造方法来给当前子类对象初始化父类型特征;
  • 2,代码复用

本文转载自: 掘金

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

「Rust 中方便和习惯性的转换」AsRef and AsM

发表于 2021-11-26

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


AsRef and AsMut

最后我们看看std::convert模块中剩下的trait,放到最后并非是它们不重要。AsRef和AsMut。像convert模块中的其他trait一样,它们被用来实现类型间的转换。

然而,其他特质会消耗数值,并可能执行重的操作,而 AsRef 和 AsMut 是用来实现轻便的,引用到引用的转换。

你可能已经从它们的名字中猜到了,AsRef将一个不可变的值的引用转换为另一个不可变的引用,而AsMut对可变的引用做同样的转换。

由于它们都非常相似,我们同时看看它们。让我们从它们的定义开始:

1
2
3
4
5
6
7
8
9
10
11
12
13
rust复制代码#[stable(feature = "rust1", since = "1.0.0")]
pub trait AsRef<T: ?Sized> {
/// Performs the conversion.
#[stable(feature = "rust1", since = "1.0.0")]
fn as_ref(&self) -> &T;
}

#[stable(feature = "rust1", since = "1.0.0")]
pub trait AsMut<T: ?Sized> {
/// Performs the conversion.
#[stable(feature = "rust1", since = "1.0.0")]
fn as_mut(&mut self) -> &mut T;
}

两者都接受对自引用,并返回对目标类型的引用,其可变性与自身相同。使用这些特性只需要在一个值上调用 as_ref() 或 as_mut() ,这取决于我们需要哪种转换,比如:value.as_ref()。

当源类型是目标类型的装箱时,实现AsRef和AsMut是简单的,就像我们之前使用的SortedVec<T> 例子。因为 SortedVec 依赖于 Vec,所以实现这两个特性不费力。

1
2
3
4
5
6
7
8
9
10
11
12
13
rust复制代码struct SortedVec<T>(Vec<T>);

impl<T> AsRef<Vec<T>> for SortedVec<T> {
fn as_ref(&self) -> &Vec<T> {
&self.0
}
}

impl<T> AsMut<Vec<T>> for SortedVec<T> {
fn as_mut(&mut self) -> &mut Vec<T> {
&mut self.0
}
}

AsRef和AsMut也允许我们将参数类型从特定的引用类型扩大到任何可以廉价转换为目标引用类型的类型,就像Into一样。

1
2
3
4
5
6
7
8
9
rust复制代码fn manipulate_vector<T, V: AsRef<Vec<T>>>(vec: V) -> Result<usize, ()> {
// ...
}

// converted to Vec<T>, such as SortedVec<T>.
let sorted_vec = SortedVec::from(vec![1u8, 2, 3]);
match manipulate_vector(sorted_vec) {
// ...
}

AsRef和AsMut与Borrow和BorrowMut非常相似,但在语义上有所不同。

Rust编程语言书详细讨论了这些区别,但作为经验,当我们想转换引用或编写通用代码时,我们选择AsRef和AsMut,而当我们想无视一个值是否是自有的或借用的时,我们选择Borrow和BorrowMut(例如,我们可能希望一个值具有相同的哈希值,而不管它是否为自有)。

对于AsRef和AsMut有一些有趣的通用实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
rust复制代码// As lifts over &
#[stable(feature = "rust1", since = "1.0.0")]
impl<'a, T: ?Sized, U: ?Sized> AsRef<U> for &'a T where T: AsRef<U> {
fn as_ref(&self) -> &U {
<T as AsRef<U>>::as_ref(*self)
}
}

// As lifts over &mut
#[stable(feature = "rust1", since = "1.0.0")]
impl<'a, T: ?Sized, U: ?Sized> AsRef<U> for &'a mut T where T: AsRef<U> {
fn as_ref(&self) -> &U {
<T as AsRef<U>>::as_ref(*self)
}
}

// AsMut lifts over &mut
#[stable(feature = "rust1", since = "1.0.0")]
impl<'a, T: ?Sized, U: ?Sized> AsMut<U> for &'a mut T where T: AsMut<U> {
fn as_mut(&mut self) -> &mut U {
(*self).as_mut()
}
}

本文转载自: 掘金

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

RabbitMQ 同步和异步通讯 Rabbit Spring

发表于 2021-11-26

同步和异步通讯

微服务间通讯有同步和异步两种方式

同步通讯

同步调用的优点:

  • 时效性较强,可以立即得到结果

同步调用的问题:

  • 耦合度高
  • 性能和吞吐能力下降
  • 有额外的资源消耗
  • 有级联失败问题

异步通讯

通过Broker实现,Broker 是一个像数据总线一样的东西,所有的服务要接收数据和发送数据都发到这个总线上,这个总线就像协议一样,让服务间的通讯变得标准和可控。

好处:

  • 吞吐量提升:无需等待订阅者处理完成,响应更快速
  • 故障隔离:服务没有直接调用,不存在级联失败问题
  • 调用间没有阻塞,不会造成无效的资源占用
  • 耦合度极低,每个服务都可以灵活插拔,可替换
  • 流量削峰:不管发布事件的流量波动多大,都由Broker接收,订阅者可以按照自己的速度去处理事件

缺点:

  • 架构复杂了,业务没有明显的流程线,不好管理
  • 需要依赖于Broker的可靠、安全、性能

MQ

MQ,中文是消息队列(MessageQueue),字面来看就是存放消息的队列。也就是事件驱动架构中的Broker。

比较常见的MQ实现:

  • ActiveMQ
  • RabbitMQ
  • RocketMQ
  • Kafka
    几种常见MQ的对比:
RabbitMQ ActiveMQ RocketMQ Kafka
公司/社区 Rabbit Apache 阿里 Apache
开发语言 Erlang Java Java Scala&Java
协议支持 AMQP,XMPP,SMTP,STOMP OpenWire,STOMP,REST,XMPP,AMQP 自定义协议 自定义协议
可用性 高 一般 高 高
单机吞吐量 一般 差 高 非常高
消息延迟 微秒级 毫秒级 毫秒级 毫秒以内
消息可靠性 高 一般 高 一般

追求可用性:Kafka、 RocketMQ 、RabbitMQ

追求可靠性:RabbitMQ、RocketMQ

追求吞吐能力:RocketMQ、Kafka

追求消息低延迟:RabbitMQ、Kafka

Rabbit

centos7 docker安装

  • 拉取镜像
    docker pull rabbitmq:management
  • 创建容器并启动
    docker run -id –name=rabbit –hostname=rabbit -p 15672:15672 -p 5672:5672 rabbitmq:management

RabbitMQ消息模型(队列消息 发布/订阅消息)

RabbitMQ中的几个概念:

  • channel:通道,操作MQ的工具
  • exchange:路由消息到队列中
  • queue:队列,缓存消息
  • virtual host:虚拟主机,是对queue、exchange等资源的逻辑分组

队列消息

  • 特点:
  1. 不会丢失数据且有序(先进先出)
  2. 仅有一个消费者能拿到
    • 模型:
    1. SimpleQueue(简单)
      1vs1 队列中待处理的消息少
    2. WorkQueue (工作)
      1vsN 队列中待处理的消息多,多个消费会平均分配,导致性能浪费,设置平均分配为能者多劳模式

订阅消息

  • 特点:
  1. 消息会丢失
  2. 强调的是多个消费者必须同时在线才能同时收到消息,不然消息会丢失
  3. 必须要有交换
    • 模型:
    1. fanout:
      1. 通过队列绑定交换机
      2. 数据通过交换机发给绑定的队列,所有消费者都能收到数据
    2. direct:在fanout的基础上可以通过routingkey(路由键)进行分类,指定了路由键的消费者才能收到数据
    3. topic:在direct的基础上可以对routingkey(路由键)进行通配简化
      1. *:可以匹配一个词 #:可以匹配多个词
      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
java复制代码public class PublisherTest {
@Test
public void testSendMessage() throws IOException, TimeoutException {
//1.建立连接
ConnectionFactory factory = new ConnectionFactory();
//1.1设置连接参数,分别是:主机名,端口号、vhost、用户名、密码
factory.setHost("192.168.190.131");
factory.setPort(5672);
factory.setVirtualHost("hawk-house");
factory.setUsername("hawk");
factory.setPassword("123456");
//1.2建立连接
Connection connection = factory.newConnection();
//2创建通道Channel
Channel channel = connection.createChannel();
//3创建队列queue
String queueName = "simple.queue";
channel.queueDeclare(queueName,false,false,false,null);
//4发送消息
String message = "hello,dover";
channel.basicPublish("",queueName,null,message.getBytes());
System.out.println("发送消息成功:【" + message + "】");
//5关闭通道和连接
channel.close();
connection.close();
}
}
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
java复制代码public class ConsumerTest {

public static void main(String[] args) throws IOException, TimeoutException {
// 1.建立连接
ConnectionFactory connectionFactory = new ConnectionFactory();
// 1.1.设置连接参数,分别是:主机名、端口号、vhost、用户名、密码
connectionFactory.setHost("192.168.190.131");
connectionFactory.setPort(5672);
connectionFactory.setVirtualHost("hawk-house");
connectionFactory.setUsername("hawk");
connectionFactory.setPassword("123456");
// 1.2.建立连接
Connection connection = connectionFactory.newConnection();
// 2.创建通道Channel
Channel channel = connection.createChannel();
// 3.创建队列
String queueName = "simple.queue";
channel.queueDeclare(queueName,false,false,false,null);
// 4.订阅消息
channel.basicConsume(queueName,true,new DefaultConsumer(channel){
@Override
public void handleDelivery(String consumerTag,
Envelope envelope,
AMQP.BasicProperties properties,
byte[] body) throws IOException {
//5.处理消息
String message = new String(body);
System.out.println("接收到消息:【" + message + "】");
}
});

System.out.println("等待接收消息。。。。");
}
}

SpringAMQP

什么是AMQP?
应用间消息通信的一种协议,与语言和平台无关。

SimpleQueue

消息发送

application.yml中添加配置

1
2
3
4
5
6
7
yml复制代码spring:
rabbitmq:
host: 192.168.190.131
port: 5672
virtual-host: hawk-house
username: hawk
password: 123456

利用RabbitTemplate实现消息发送:

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码@RunWith(SpringRunner.class)
@SpringBootTest
public class RabbitTemplateTest {
@Autowired
RabbitTemplate rabbitTemplate;

@Test
public void SingleAMOP(){
String queueName="simple.queue";
String message="hello,hawk";
rabbitTemplate.convertAndSend(queueName,message);
}
}

消息接收

application.yml中添加配置:同发送
自定义一个消息监听器

  • 此类交给容器管理
  • 编写方法:监听消息队列(队列中一旦有最新的消息,自动执行方法)
    • 方法没有返回值
    • 方法有参数(参数=发送的消息内容)
    • 参数类型和发送的消息类型一致
    • 在方法上通过@RabbitListener绑定队列
1
2
3
4
5
6
7
8
9
java复制代码@Component
public class MessageListener {

@RabbitListener(queues = "simple.queue")
public void SingleQueueGet(String message) throws InterruptedException {

System.out.println(message);
}
}

WorkQueue

Work queues,也被称为(Task queues),任务模型。简单来说就是让多个消费者绑定到一个队列,共同消费队列中的消息。
当消息处理比较耗时的时候,可能生产消息的速度会远远大于消息的消费速度,此时就可以使用work模型

消息发送

1
2
3
4
5
6
7
8
9
java复制代码@Test
public void sendWorkQueue(){
String queueName="simple.queue";
for (int i = 0; i < 50; i++) {
String message="hello,hawk"+"-"+i;
rabbitTemplate.convertAndSend(queueName,message);
}

}

消息接收

默认多个消费者轮询接收信息

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

@RabbitListener(queues = "simple.queue")
public void SingleQueueGet(String message) throws InterruptedException {

System.out.println("消费者1:"+message);
}
}
1
2
3
4
5
6
7
8
9
java复制代码@Component
public class MessageListener {

@RabbitListener(queues = "simple.queue")
public void SingleQueueGet(String message) throws InterruptedException {
Thread.sleep(1000);
System.out.println("消费者2:"+message);
}
}

能者多劳

application.yml文件,添加配置,注意所有消费者都要添加配置,只添加一个消费者的话该消费者处理一条信息后就会停止获取信息

1
2
3
4
5
yml复制代码spring:
rabbitmq:
listener: #将workqueue的默认平均分配处理改成能者多捞处理
simple:
prefetch: 1

发布/订阅

在订阅模型中,多了一个exchange角色,过程略有变化:

  • 生产者:也就是要发送消息的程序,但是不再发送到队列中,而是发给交换机
  • Exchange:交换机.一方面,接收生产者发送的消息。另一方面,知道如何处理消息,例如递交给某个特别队列、递交给所有队列、或是将消息丢弃。到底如何操作,取决于Exchange的类型。Exchange有以下3种类型:
    • Fanout:广播,将消息交给所有绑定到交换机的队列
    • Direct:定向,把消息交给符合指定routing key 的队列
    • Topic:通配符,把消息交给符合routing pattern(路由模式) 的队列
  • 消费者:与以前一样,订阅队列,没有变化
  • Queue:消息队列也与以前一样,接收消息、缓存消息。

Exchange(交换机)只负责转发消息,不具备存储消息的能力,因此如果没有任何队列与Exchange绑定,或者没有符合路由规则的队列,那么消息会丢失!

Fanout

消息发送流程:

  • 1) 可以有多个队列
  • 2) 每个队列都要绑定到Exchange(交换机)
  • 3) 生产者发送的消息,只能发送到交换机,交换机来决定要发给哪个队列,生产者无法决定
  • 4) 交换机把消息发送给绑定过的所有队列
  • 5) 订阅队列的消费者都能拿到消息

消息发送

1
2
3
4
5
6
7
8
9
10
11
java复制代码@Test
public void Fanout(){
//交换机名称
String exchangeName="hawk-ex";
String message="hi,dover";
/*数据通过交换机给队列
交换机名称
路由键(路由模型下用)
数据*/
rabbitTemplate.convertAndSend(exchangeName,"",message);
}

消息接收

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
java复制代码@RabbitListener(
//绑定队列和交换机
bindings = @QueueBinding(
//指定队列
value = @Queue(
//队列名称
value ="fanout.queue1"
),
//指定交换机
exchange = @Exchange(
//交换机名称
value = "hawk-ex",
//交换机类型
type = ExchangeTypes.FANOUT
)
))
public void Fanout(String message) {
System.out.println("消费者1:"+message);
}

Direct

不同的消息被不同的队列消费。这时就要用到Direct类型的Exchange。
在Direct模型下:

  • 队列与交换机的绑定,不能是任意绑定了,而是要指定一个RoutingKey(路由key)
  • 消息的发送方在 向 Exchange发送消息时,也必须指定消息的 RoutingKey。
  • Exchange不再把消息交给每一个绑定的队列,而是根据消息的Routing Key进行判断,只有队列的Routingkey与消息的 Routing key完全一致,才会接收到消息

消息发送

1
2
3
4
5
6
7
java复制代码@Test
public void Direct(){
String exchangeName="hawk-dir";
String routingKey="9812";
String message="hawk,hawk,hawk";
rabbitTemplate.convertAndSend(exchangeName,routingKey,message);
}

消息接受

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
java复制代码@RabbitListener(
//绑定队列和交换机
bindings = @QueueBinding(
//指定队列
value = @Queue(
//队列名称
value ="direct.queue1"
),
//指定交换机
exchange = @Exchange(
//交换机名称
value = "hawk-dir",
//交换机类型
type = ExchangeTypes.DIRECT
),
key = {"0630","9812","0304"}
))
public void Direct(String message) {
System.out.println("消费者1:"+message);
}

Topic

Topic类型的Exchange与Direct相比,都是可以根据RoutingKey把消息路由到不同的队列。只不过Topic类型Exchange可以让队列在绑定Routing key 的时候使用通配符!

Routingkey 一般都是有一个或多个单词组成,多个单词之间以”.”分割,例如: item.insert

通配符规则:

#:匹配一个或多个词

*:匹配不多不少恰好1个词

消息发送

1
2
3
4
5
6
7
java复制代码@Test
public void topic(){
String exchangeName="hawk-dir";
String routingKey="hawk.dover.9999";
String message="hawk,hawk,hawk";
rabbitTemplate.convertAndSend(exchangeName,routingKey,message);
}

消息接收

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
java复制代码@RabbitListener(
//绑定队列和交换机
bindings = @QueueBinding(
//指定队列
value = @Queue(
//队列名称
value ="topic.queue1"
),
//指定交换机
exchange = @Exchange(
//交换机名称
value = "hawk-topic",
//交换机类型
type = ExchangeTypes.TOPIC
),
key = "hawk.#"
))
public void topic(String message) {
System.out.println("消费者1:"+message);
}

本文转载自: 掘金

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

JVM GC的心酸路程

发表于 2021-11-26

首先看一下JVM的内存分布

image.png

再复习一下 如何辨别垃圾

1.计数法 被引用了+1 不被引用了-1
缺陷: 无法解决循环问题。

image.png
这几个对象因为相互引用 永远无法被回收

2.GCRoot(可达性分析)
可达性算法的原理是以一系列叫做 GC Root 的对象为起点出发,引出它们指向的下一个节点,再以下个节点为起点,引出此节点指向的下一个结点,到下一节点,到下一节点,到下一节点,会形成一个链,最后那些没有出现在这些GCRoot链中的节点便是可以回收的节点

image.png
其中C便是可以认为被回收

GCRoot可以是
1.虚拟机栈中的引用对象(局部变量)
2.类静态成员变量
3.方法区中常量引用的对象
4.方法区中JNI引用的对象

JVM如何开始进行GC

1.OopMap: JVM(HotSpot为例)会报一些类信息放到OoMap这个数据结构里,主要记录着什么偏移量上是什么样的数据类型,是在类加载的时候放到这里的,这样就可以不用每次去扫所有的类挨个去找类的引用关系了。

2.安全点(SafePoint):有了OopMap,HotSpot就可以快速准确的完成GC Root枚举,JVM在进行GCRoot枚举以及进行垃圾回收的时候会Stop The World 为了防止用户线程在GC线程进行RCRoot枚举时或回收时 更改掉已经标记出的节点的引用,而设计的一种方案,当开始进行GCRoot枚举时,用户线程到达这个安全点变会暂停运行,这些安全点的选举基本上以“是否具有让程序长时间执行的特征”。

2.1 那么如何让线程(不包含JNI调用的线程)到指定的安全点停下来呢?
2.1.1 抢先式中断:在垃圾收集发生时,系统首先会把所有用户线程全部中断,如果发现用户线程中断不在安全点上,就恢复让该线程继续执行,直到跑到安全点(几乎没有虚拟机用)
2.1.2主动式中断:不直接对线程操作,仅仅设置一个标识位,各个线程回去loop这个标识位(使用内存保护陷阱的方式),一旦发现中断标识位为真时自己就在最近的安全点主动挂起

3.安全区:安全点过多也不是很好呀(可以看一下 有哪些东西可以作为安全点),所以又设计出安全区这个概念,就是用户线程执行到安全区里面的代码的时候,首先会标识自己进入安全区了,那么这段时间内如果虚拟机发生GC Root枚举或者进行垃圾收集的时候,便会跨过这些线程,当线程要离开安全区时,它会去检查虚拟机是否完成了GC Root枚举或者垃圾收集过程中其它需要暂停线程的阶段,如果完成了,那就啥事没有继续执行,否则就一直在等待。

image.png

JVM垃圾算法

1.标记清除
标记出来要清理的对象,在进行清除
缺点:效率比较低 会产生大量的内存碎片 这些碎片太多可能会造成之后的对象无法得到相应的内存而再造成GC

2.标记复制
该算法会把内存分为两个区,每次使用其中的一块,发生GC时,会把未回收的放到另一块内存中,之后剩余的进行清除 算法简单清晰
缺点:浪费空间 每次都只能使用一半内存 对象存活率较高的时候会很不划算

3.标记整理
使存活的对象往一段走 之后在把存活边界后面的全部清理掉 既解决了碎片化的问题又解决了复制算法的对象存活高情况下的内存浪费问题
缺点:很复杂 消耗资源 适合老年代

回头再说说JVM跨代引用问题

1.回顾一下现在JVM使用的分代回收算法
分代收集算法就是目前虚拟机使用的回收算法,它解决了标记整理不适用于老年代的问题,将内存分为各个年代,在不同年代使用不同的算法,从而使用最合适的算法,新生代存活率低,可以使用复制算法。而老年代对象存活率高,没有额外空间对它进行分配担保,所以使用标记整理算法。

2.跨代引用问题:
如果一个老年代的对象引用指向了年轻代,那年轻代的进行垃圾回收的时候,被引用的这个在年轻代的对象会怎样处理呢?
在进行GCRoot扫描的时候会再扫一遍老年区么?
答案是肯定不会的 因为这样的话意味着是不是每次进行Minor GC的时候 还要去扫一遍老年?那岂不是相当于进行了一次 次级别Full GC ,所以JVM是怎样解决这个问题的呢?答案是卡表。

先笼统地介绍下记忆集(基本思想):
记忆集是一种用于记录从非收集区域指向收集区域的指针集合的数据结构。JVM会把所有跨代的引用放到一起,年轻代发生GC时 被引用的对象不会被扫出去 而是会增加年龄 直至进入老年代 这样跨代的问题就没有了

卡表(记忆集的实现):
在hotspot虚拟机中卡表是一个字节数组,数组的每一项对应内存中某一块连续的地址区域,如果该区域中有引用只想了待回收区域的对象,卡表对应的元素将被设置成1,没有则设置成0

image.png

如图:每块连续的内存地址向右移动9位 便是它在卡表上的位置了

再说说写屏障:
卡表上的 这些数据是如何变脏的呢?
何时变脏很明确,有其它分代区域对象引用本区域对象时候,其对应卡表元素就应该变脏,变脏的时间点应该是发在引用赋值那一刻,所以这个之后就可以通过写屏障指令来维护卡表状态的 类似于AOP切面 。

卡表副作用:
1.写屏障的性能开销
2.内存“伪共享” 当多个线程修改相互独立的变量时,恰好这些变量都在一个缓存行,就会彼此影响(写回、无效化或者同步)导致性能降低

JDK 7 以后 HotSpot虚拟机增加一个新的参数 -XX:+UseCondCardMark 用来决定是否开启卡表更新。

引:深入理解java虚拟机 第三版 周志明

本文转载自: 掘金

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

《MySQL 入门教程》第 32 篇 存储过程(二)

发表于 2021-11-26

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

书接上文,本篇我们来详细介绍一下 MySQL 存储过程和函数支持的编程功能。

32.1 存储过程变量

变量(Variable)是存储过程和函数内部用于存储中间结果的对象。这些变量属于局部变量,作用域限定在存储过程中。在使用存储过程变量之前需要使用DECLARE语句声明变量:

1
sql复制代码DECLARE variable_name datatype(size) [DEFAULT default_value];

其中,DECLARE 表达定义变量,必须位于 BEGIN 和 END 之间并且是第一个语句;variable_name 是变量名;datatype(size) 是变量的类型和长度;DEFAULT default_value 用于为变量指定一个默认值,否则默认值为 NULL。例如,以下是一些变量的定义:

1
2
sql复制代码DECLARE salary DECIMAL(8, 2) DEFAULT 0.0;
DECLARE x, y INTEGER DEFAULT 0;

salary 是 DECIMAL(8, 2) 类型的变量,默认值为 0.0;x 和 y 是 INTEGER 类型变量,默认值为 0。

声明了变量之后,可以使用SET语句进行赋值:

1
sql复制代码SET variable_name = value;

或者使用SELECT INTO语句将查询的结果赋值给变量:

1
2
3
4
5
6
7
sql复制代码SELECT expression1, ...
INTO variable1, ...
FROM ...;

SELECT expression1, ...
FROM ...
INTO variable1, ...;

SELECT 返回的表达式个数和变量的个数相同,查询语句最多只能返回一行数据,可以使用 LIMIT 1 进行限定。

以下语句创建了一个存储过程 TestProc:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
sql复制代码DELIMITER $$

CREATE PROCEDURE TestProc()
BEGIN
DECLARE sal decimal(8, 2) DEFAULT 0.0;
DECLARE x, y integer DEFAULT 0;

SELECT salary
FROM employee
WHERE emp_name = '张飞'
INTO sal;

SET x = 6, y = 9;

SELECT sal, x, y;

END$$

DELIMITER ;

调用该存储过程查看变量的赋值结果:

1
2
3
4
5
sql复制代码CALL TestProc();

sal |x|y|
--------|-|-|
24000.00|6|9|

对于存储过程变量,它的作用域(生命周期)位于存储过程定义中的 BEGIN 和 END 语句之间。如果在嵌套的 BEGIN 和 END 语句之间定义的变量,作用域只在这个模块内部。如果不同作用域中存在同名的变量,在作用域内有效范围更小的变量优先级更高。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
sql复制代码DROP PROCEDURE TestProc;

DELIMITER $$

CREATE PROCEDURE TestProc()
BEGIN
DECLARE x integer DEFAULT 0;
SET x = 6;
SELECT 'outer', x;

BEGIN
DECLARE x integer DEFAULT 0;
SET x = 7;
SELECT 'inner', x;
END;

END$$

DELIMITER ;

存储过程中先定义了一个个变量 x,然后在嵌套模块中又定义了同名的变量。调用该存储过程的输出结果如下:

1
2
3
4
5
6
7
8
9
sql复制代码CALL TestProc();

outer|x|
-----|-|
outer|6|

inner|x|
-----|-|
inner|7|

在嵌套模块中查询返回的是该模块中的变量 x。虽然 MySQL 支持这种同名变量的使用,但是建议尽量不要这样使用,避免引起混淆。

📝关于 MySQL 系统变量和用户变量的介绍,可以参考本教程的第 17 篇。

32.2 条件控制语句

MySQL 提供了两种条件控制语句:IF 语句和 CASE 语句。

32.2.1 IF 语句

IF 语句可以实现基本的条件判断结构,语法如下:

1
2
3
4
5
sql复制代码IF search_condition THEN statement_list
[ELSEIF other_condition THEN other_statement_list]
...
[ELSE else_statement_list]
END IF;

如果 search_condition 的结果为 true,指定对应 THEN 之后的 statement_list 语句列表;否则,如果存在可选的 ELSEIF 并且 other_condition 结果为 true,执行对应的 other_statement_list 语句列表;依次类推;否则,如果存在可选的 ELSE,执行对应的 else_statement_list 语句列表。

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
sql复制代码DROP PROCEDURE TestProc;

DELIMITER $$

CREATE PROCEDURE TestProc()
BEGIN
DECLARE x, y integer DEFAULT 0;
SET x = 5, y = 10;

IF x = 5 THEN
SELECT 'x = 5';
END IF;

IF y < 10 THEN
SELECT 'y < 10';
ELSEIF y > 10 THEN
SELECT 'y > 10';
ELSE
SELECT 'y = 10';
END IF;

END$$

DELIMITER ;

调用该存储过程的输出结果如下:

1
2
3
4
5
6
7
8
9
sql复制代码CALL TestProc();

x = 5|
-----|
x = 5|

y = 10|
------|
y = 10|

32.2.2 CASE 语句

CASE 语句可以用于构造复杂的条件判断结构,支持两种形式的语法。第一种简单形式的 CASE 语句如下:

1
2
3
4
5
6
sql复制代码CASE case_value
WHEN when_value1 THEN statement_list1
[WHEN when_value2 THEN statement_list2]
...
[ELSE else_statement_list]
END CASE;

其中,case_value 是一个表达式;首先使用该表达式的值和 when_value1 进行比较,如果相等则执行 statement_list1 语句列表并结束 CASE 语句;否则,如果表达式等于可选的 when_value2,则执行 statement_list2 并结束 CASE 语句;依次类推;最后,如果存在可选的 ELSE,执行 else_statement_list 语句列表;如果此时没有定义 ELSE 子句,将会返回“Case not found for CASE statement”错误。

第二种形式的搜索 CASE 语句如下:

1
2
3
4
5
6
sql复制代码CASE
WHEN search_condition1 THEN statement_list1
[WHEN search_condition2 THEN statement_list2]
...
[ELSE else_statement_list]
END CASE;

首先,判断 search_condition1 是否为 true;如果是则执行 statement_list1 语句列表;否则,判断 search_condition2 是否为 true,如果是则执行 statement_list2 语句列表;依此类推;最后,如果存在可选的 ELSE,执行 else_statement_list 语句列表;如果此时没有定义 ELSE 子句,将会返回“Case not found for CASE statement”错误。

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
sql复制代码DROP PROCEDURE TestProc;

DELIMITER $$

CREATE PROCEDURE TestProc()
BEGIN
DECLARE x integer DEFAULT 0;
SET x = 5;

CASE
WHEN x < 0 THEN
SELECT 'x < 0';
WHEN x = 0 THEN
SELECT 'x = 0';
ELSE
SELECT 'x > 0';
END CASE;

END$$

DELIMITER ;

调用该存储过程的输出结果如下:

1
2
3
4
5
sql复制代码CALL TestProc();

x > 0|
-----|
x > 0|

⚠️第 13 篇我们介绍了 CASE 表达式与 IF 函数,和本篇的 CASE 语句和 IF 语句是不同的概念。

32.3 循环控制语句

MySQL 支持三种循环控制语句:LOOP、REPEAT 以及 WHILE 语句。

32.3.1 LOOP 语句

LOOP语句用于实现简单的循环结构,语法如下:

1
2
3
sql复制代码[begin_label:] LOOP
statement_list
END LOOP [end_label]

LOOP 语句重复执行 statement_list 直到循环被终止,通常使用LEAVE label语句退出循环,或者使用函数的 RETURN 语句退出整个函数。begin_label 和 end_label 是该循环语句的标签。例如:

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
sql复制代码DELIMITER $$

CREATE PROCEDURE TestLoop()
BEGIN
DECLARE x,sumx integer DEFAULT 0;

label1: LOOP
SET x = x + 1;

IF x > 10 THEN
LEAVE label1;
END IF;
IF x mod 2 THEN
ITERATE label1;
ELSE
SET sumx = sumx + x;
END IF;

END LOOP label1;

SELECT sumx;

END$$

DELIMITER ;

其中,LEAVE 表示 x 大于 10 时退出循环 label1;ITERATE 类似于 C++ 或者 Java 中的 CONTINUE,表示 x 为奇数则进入下一次循环,否则将其增加到变量 sumx 。存储过程的运行结果如下:

1
2
3
4
5
sql复制代码CALL TestLoop();

sumx|
----|
30|

⚠️如果 LOOP 循环语句中没有指定退出条件,将会进入死循环。

32.3.2 REPAET 语句

REPAET语句用于重复执行指定语句列表直到某个条件为真,语法如下:

1
2
3
4
sql复制代码[begin_label:] REPEAT
statement_list
UNTIL search_condition
END REPEAT [end_label];

REPEAT 语句首先执行 statement_list,然后判断 search_condition 是否为 true;如果是则终止循环,否则再次循环。REPAET 语句至少会执行一次。例如,上面的 LOOP 语句示例使用 REPAET 实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
sql复制代码DELIMITER $$

CREATE PROCEDURE TestRepeat()
BEGIN
DECLARE x,sumx integer DEFAULT 0;

label1: REPEAT
SET x = x + 1;

IF x mod 2 THEN
ITERATE label1;
ELSE
SET sumx = sumx + x;
END IF;

UNTIL x = 10 END REPEAT label1;

SELECT sumx;

END$$

DELIMITER ;

UNTIL 指定了 x = 10 时退出循环,执行该存储过程的结果和上一节相同。

32.3.3 WHILE 语句

WHILE语句基于某个条件为真时重复执行指定的语句列表,语法如下:

1
2
3
sql复制代码[begin_label:] WHILE search_condition DO
statement_list
END WHILE [end_label];

首先,判断 search_condition 是否为 true;如果是则执行 statement_list 语句列表,否则退出循环语句。WHILE 语句可能一次也不执行。例如,上面的 LOOP 语句示例使用 WHILE 实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
sql复制代码DELIMITER $$

CREATE PROCEDURE TestWhile()
BEGIN
DECLARE x,sumx integer DEFAULT 0;

label1: WHILE x < 10 DO
SET x = x + 1;

IF x mod 2 THEN
ITERATE label1;
ELSE
SET sumx = sumx + x;
END IF;

END WHILE label1;

SELECT sumx;

END$$

DELIMITER ;

32.4 错误处理

当存储过程或者函数在执行过程中出现某种错误条件(例如警告或者异常)时,需要进行特殊处理,例如退出当前程序模块或者继续执行,同时返回一个有用的错误信息。

32.4.1 定义错误条件

MySQL 提供了定义错误条件的DECLARE ... CONDITION语句:

1
2
sql复制代码DECLARE condition_name CONDITION 
FOR { mysql_error_code | SQLSTATE [VALUE] sqlstate_value }

其中,condition_name 是错误条件的名称,可以用于随后的错误处理器声明;mysql_error_code 表示 MySQL 错误码,例如 1062 代表了唯一键重复错误;SQLSTATE 表示使用 5 位字符串代表的 SQLSTATE 值,例如“42000” 和错误码 1062 一样代表了唯一键重复错误。

32.4.2 定义错误处理器

定义了错误条件之后,我们可以使用DECLARE ... HANDLER语句定义处理一个或多个错误条件的错误处理器:

1
2
3
4
5
6
7
8
9
10
11
12
sql复制代码DECLARE { CONTINUE | EXIT } HANDLER
FOR condition_value [, condition_value] ...
statement;

condition_value: {
mysql_error_code
| SQLSTATE [VALUE] sqlstate_value
| condition_name
| SQLWARNING
| NOT FOUND
| SQLEXCEPTION
}

该语句定义了一个错误处理器;当某个错误条件 condition_value 发生时,执行指定的 statement 语句,可以是单个语句或者 BEGIN/END 代码块;然后执行相应的操作,CONTINUE 表示继续执行当前程序,EXIT 表示终止执行错误处理器所在的 BEGIN/END 模块。

指定错误条件时,mysql_error_code 表示 MySQL 错误码,SQLSTATE 表示使用 5 位字符串代表的 SQLSTATE 值,condition_name 是使用 DECLARE … CONDITION 语句定义的错误条件名称,SQLWARNING 代表了以“01”开头的一类 SQLSTATE 值,NOT FOUND 代表了以“02”开头的一类 SQLSTATE 值,SQLEXCEPTION 代表了以“00”、“01”或者“02”开头的一类 SQLSTATE 值。

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
sql复制代码DELIMITER $$

CREATE PROCEDURE TestError()
BEGIN
DECLARE empid int DEFAULT 1;
DECLARE duplicate_employee CONDITION FOR 1062;

DECLARE EXIT HANDLER
FOR duplicate_employee
BEGIN
ROLLBACK;
SELECT CONCAT('重复的员工信息!', 'emp_id:', empid) Message;
END;

INSERT INTO employee
VALUES (empid, '刘备', '男', 1, NULL, DATE('2000-01-01'), 1, 30000, 10000, 'liubei@shuguo.com');
END$$

DELIMITER ;

首先,我们定义了一个错误条件 duplicate_employee,表示员工信息重复错误;然后定义了一个错误处理器,发生该错误时回滚数据修改并且输出一个消息,使用命名的错误条件可以提高代码的可读性。执行该存储过程的结果如下:

1
2
3
4
5
sql复制代码CALL TestError();

Message |
---------------------|
重复的员工信息!emp_id:1|

如果将错误处理器执行的语句稍加修改,就可以实现第 26 篇中的合并数据操作:

1
2
3
4
5
6
7
8
sql复制代码DECLARE EXIT HANDLER
FOR duplicate_employee
BEGIN
UPDATE employee
SET emp_name = '刘备'
...
WHERE emp_id = empid;
END;

📝如果定义了多个错误处理器,指定错误码的处理器优先级最高,然后是 SQLSTATE 处理器,然后是 SQLEXCEPTION 处理器,最后是 SQLWARNING 处理器以及 NOT FOUND 处理器。多个优先级相同的错误处理器的执行顺序不确定。

32.4.3 获取诊断信息

MySQL 在发生错误时会生成诊断信息,应用程序可以通过GET DIAGNOSTICS语句获取这些信息。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
sql复制代码mysql> DROP TABLE no_such_table;
ERROR 1051 (42S02): Unknown table 'hrdb.no_such_table'
mysql> GET DIAGNOSTICS CONDITION 1
-> @p1 = RETURNED_SQLSTATE, @p2 = MESSAGE_TEXT;
Query OK, 0 rows affected (0.00 sec)

mysql> SELECT @p1, @p2;
+-------+------------------------------------+
| @p1 | @p2 |
+-------+------------------------------------+
| 42S02 | Unknown table 'hrdb.no_such_table' |
+-------+------------------------------------+
1 row in set (0.00 sec)

另外,我们也可以使用 SHOW WARNINGS 或者 SHOW ERRORS 查看相关的错误信息。

GET DIAGNOSTICS 语句的完整语法如下:

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
sql复制代码GET [CURRENT | STACKED] DIAGNOSTICS {
target = NUMBER | target = ROW_COUNT,
...
}

GET [CURRENT | STACKED] DIAGNOSTICS
CONDITION condition_number {
target = condition_information_item_name,
...
}

condition_information_item_name: {
CLASS_ORIGIN
| SUBCLASS_ORIGIN
| RETURNED_SQLSTATE
| MESSAGE_TEXT
| MYSQL_ERRNO
| CONSTRAINT_CATALOG
| CONSTRAINT_SCHEMA
| CONSTRAINT_NAME
| CATALOG_NAME
| SCHEMA_NAME
| TABLE_NAME
| COLUMN_NAME
| CURSOR_NAME
}

其中,CURRENT 表示返回当前诊断区域中的信息,STACKED 表示返回第二诊断区域的信息(只能用于 DECLARE HANDLER 语句之中)。默认返回当前诊断区域中的信息。

简单来说,诊断区域中存储了两类信息:

  • 语句信息,包括错误条件的编号(NUMBER)和影响的行数(ROW_COUNT),对应上面的第一种语法。
  • 条件信息,例如错误码(MYSQL_ERRNO)和错误信息(MESSAGE_TEXT)等,对应上面的第二种语法。如果一个语句触发了多个错误条件,每个错误条件都提供了一个条件区域,编号从 1 到 NUMBER。

📝关于 MySQL 诊断区域的详细介绍,可以参考官方文档。

以下是一个在存储过程中获取诊断信息的示例:

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
sql复制代码CREATE TABLE t(id int NOT NULL);

DELIMITER $$

CREATE PROCEDURE do_insert(value INT)
BEGIN
-- 声明保存诊断信息的变量
DECLARE cno INT;
DECLARE code CHAR(5) DEFAULT '00000';
DECLARE msg TEXT;
DECLARE nrows INT;
DECLARE result TEXT;
-- 声明异常错误处理器
DECLARE CONTINUE HANDLER FOR SQLEXCEPTION
BEGIN
GET DIAGNOSTICS cno = NUMBER;
GET DIAGNOSTICS CONDITION cno
code = RETURNED_SQLSTATE, msg = MESSAGE_TEXT;
END;

-- 执行插入语句
INSERT INTO t(id) VALUES(value);
-- 检查插入是否成功
IF code = '00000' THEN
GET DIAGNOSTICS nrows = ROW_COUNT;
SET result = CONCAT('insert succeeded, row count = ',nrows);
ELSE
SET result = CONCAT('insert failed, error = ',code,', message = ',msg);
END IF;
-- 返回错误信息
SELECT result;
END$$

DELIMITER ;

其中,cno 是返回的错误条件编号,code 和 msg 是错误编码和信息,nrows 是成功插入的行数。运行以下测试语句:

1
2
3
4
5
6
7
8
9
sql复制代码CALL do_insert(1);
result |
-------------------------------|
insert succeeded, row count = 1|

CALL do_insert(NULL);
result |
------------------------------------------------------------------|
insert failed, error = 23000, message = Column 'id' cannot be null|

32.4.4 抛出错误条件

除了捕获和处理错误之外,MySQL 还提供了抛出错误的SIGNAL和RESIGNAL语句。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
sql复制代码SIGNAL { SQLSTATE [VALUE] sqlstate_value | condition_name }
[SET condition_information_item_name = simple_value, ...]

condition_information_item_name: {
CLASS_ORIGIN
| SUBCLASS_ORIGIN
| MESSAGE_TEXT
| MYSQL_ERRNO
| CONSTRAINT_CATALOG
| CONSTRAINT_SCHEMA
| CONSTRAINT_NAME
| CATALOG_NAME
| SCHEMA_NAME
| TABLE_NAME
| COLUMN_NAME
| CURSOR_NAME
}

SIGNAL 可以抛出一个 SQLSTATE 值或者命名的错误条件,SET 子句可以设置不同的信息,它们可以在调用程序中使用 GET DIAGNOSTICS 语句捕获。例如:

1
2
3
4
5
6
7
8
9
10
11
sql复制代码DELIMITER $$

CREATE PROCEDURE p(divisor INT)
BEGIN
IF divisor = 0 THEN
SIGNAL SQLSTATE '22012'
SET MESSAGE_TEXT = 'devided by zero!';
END IF;
END;

DELIMITER ;

存储过程 p 抛出了一个自定义的错误条件,指定了 SQLSTATE 值和错误消息。传入参数 0,测试抛出错误的结果:

1
2
sql复制代码CALL p(0);
ERROR 1644 (22012): devided by zero!

另外,RESIGNAL语句也可以抛出错误,语法和 SIGNAL 相同。不过,RESIGNAL 和 SIGNAL 存在一些差异:

  • RESIGNAL 语句只能用于错误处理器的处理语句中,可以修改某些错误信息后再次抛出;
  • RESIGNAL 语句的属性可以忽略,意味着将接收到的错误不经修改再次抛出。

32.5 游标的使用

游标(Cursor)可以支持对查询结果的遍历,从而实现数据集的逐行处理。MySQL 存储过程和函数支持游标,它们具有以下特性:

  • Asensitive 灵敏性,服务器可能直接遍历基础表的数据,也可能复制一份额外的临时数据;
  • 只读性(Read only),不能通过游标修改基础表中的数据;
  • 不可滚动性(Nonscrollable),只能按照查询结果的顺序访问数据,不能反向遍历,也不能跳跃访问数据行。

使用 MySQL 游标的过程如下:

  1. 通过DECLARE语句声明游标;
  2. 使用OPEN语句打开游标;
  3. 循环使用FETCH语句获取游标中的数据行;
  4. 使用CLOSE语句关闭游标并释放资源。

以下是一个使用游标的示例:

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
sql复制代码DELIMITER $$

CREATE PROCEDURE TestCurosr()
BEGIN
DECLARE done INT DEFAULT FALSE;
DECLARE id int;
DECLARE name varchar(50);
DECLARE namelist varchar(500) default '';
DECLARE cur1 CURSOR FOR
SELECT emp_id, emp_name FROM employee WHERE sex = '女';
DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = TRUE;

OPEN cur1;

getEmp: LOOP
FETCH cur1 INTO id, name;
IF done THEN
LEAVE getEmp;
ELSE
SET namelist = concat( name, ';', namelist);
END IF;
END LOOP;

CLOSE cur1;

SELECT namelist;
END$$

DELIMITER ;

该存储过程的执行结果如下:

1
2
3
4
sql复制代码CALL TestCurosr();
namelist |
------------------|
赵氏;孙丫鬟;孙尚香;|

下面我们基于该示例介绍使用游标过程中的语句。

32.5.1 声明游标

MySQL 使用 DECLARE 语句声明游标:

1
sql复制代码DECLARE cursor_name CURSOR FOR select_statement;

其中,cursor_name 是游标名称,select_statement 定义了与游标关联的查询语句。游标声明必须在变量和错误条件(CONDITION)声明之后,以及错误处理器(HANDLER)声明之前。在上面的示例中声明了一个游标 cur1:

1
2
sql复制代码 DECLARE cur1 CURSOR FOR 
SELECT emp_id, emp_name FROM employee WHERE sex = '女';

32.5.2 打开游标

声明之后,使用 OPEN 语句打开游标,也就是执行查询初始化结果集:

1
sql复制代码OPEN cur1;

32.5.3 遍历游标

接下来就是循环使用 FETCH 语句获取下一行数据,并且移动游标指针:

1
sql复制代码FETCH cur1 INTO id, name;

同时,在循环中检查是否还有更多数据行;如果没有则退出循环。变量 done 在预定义的错误处理器中进行设置,没有找到数据(NOT FOUND)时设置为 true。

32.5.4 关闭游标

最后,使用 CLOSE 语句关闭游标:

1
sql复制代码CLOSE cur1;

关闭游标可以释放相关的资源。

32.6 访问权限控制

在存储过程和函数的定义中,可以使用 SQL SECURITY 属性指定定义者权限(DEFINER )或者调用者权限(INVOKER )。

对于 SQL SECURITY DEFINER,存储过程使用定义者的权限执行,定义者可以通过 DEFINER 属性进行指定,默认为当前用户。

对于 SQL SECURITY INVOKER,存储过程将会使用调用者的权限执行,此时 DEFINER 属性不会对执行产生任何作用。如果调用者的权限较低,定义者的权限较高,调用者仍然能够获得存储过程内部操作所需的权限。

本文转载自: 掘金

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

基础数据结构--栈和队列的练习 1、如何用栈结构实现队列结构

发表于 2021-11-26

1、如何用栈结构实现队列结构

首先,用一个栈肯定是实现不了的。所以,考虑两个栈来实现。一个是push栈、一个是pop栈。

放数据的时候往push栈放,当取数据的时候,将数据全部倒到pop栈,再从pop栈取数据。

注意:

(1)倒数据的时候要一次性倒完

(2)如果pop栈没有拿完,不能倒数据

详细代码:

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
java复制代码package basic.stackqueue;

import java.util.Stack;

/**
 * 使用栈结构实现队列结构的功能 
 *
 * @author 周一
 */
public class TwoStacksImplementQueue {

    /**
     * 使用两个栈来实现队列结构
     */
    public static class TwoStackQueue<T> {
        // 用于存放添加数据的栈
        public Stack<T> stackPush;
        // 用于存放获取数据的栈
        public Stack<T> stackPop;

        public TwoStackQueue() {
            stackPush = new Stack<>();
            stackPop = new Stack<>();
        }

        /**
         * 倒数据
         */
        private void pushToPop() {
            // pop为空才倒数据
            if (stackPop.empty()) {
                // push有数据可倒,并且一次性倒完
                while (!stackPush.empty()) {
                    stackPop.push(stackPush.pop());
                }
            }
        }

        /**
         * 添加数据都往stackPush栈放
         */
        public void add(T data) {
            stackPush.push(data);
        }

        public T poll() {
            if (stackPush.empty() && stackPop.empty()) {
                throw new RuntimeException("Queue is empty");
            }
            // 拿数据前先执行倒数据方法
            pushToPop();
            // 拿数据都从stackPop栈拿
            return stackPop.pop();
        }

        public T peek() {
            if (stackPush.empty() && stackPop.empty()) {
                throw new RuntimeException("Queue is empty");
            }
            pushToPop();
            return stackPop.peek();
        }

    }

    public static void main(String[] args) {
        TwoStackQueue<Integer> test = new TwoStackQueue<>();
        test.add(1);
        test.add(4);
        test.add(3);
        test.add(2);

        System.out.println(test.peek());
        System.out.println(test.poll());

        System.out.println(test.peek());
        System.out.println(test.poll());

        System.out.println(test.peek());
        System.out.println(test.poll());

        System.out.println(test.peek());
        System.out.println(test.poll());
    }

}

2、如何用队列结构实现栈结构

同理,想用一个队列来实现,是不行的。所以,考虑两个队列来实现。

注意:

加数据的时候,往当前存在数据的队列加;

拿数据的时候,将所有数据移到空队列中,留下一个数返回即可。

详细代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
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
java复制代码package basic.stackqueue;

import java.util.LinkedList;
import java.util.Queue;
import java.util.Stack;

/**
 * 使用队列结构实现栈结构
 *
 * @author 周一
 */
public class TwoQueuesImplementStack {

    /**
     * 两个队列实现栈结构
     */
    public static class TwoQueueStack<T> {
        public Queue<T> queue;
        public Queue<T> help;

        public TwoQueueStack() {
            queue = new LinkedList<>();
            help = new LinkedList<>();
        }

        public void push(T data) {
            queue.offer(data);
        }

        public T poll() {
            while (queue.size() > 1) {
                // 将queue中数据倒入help中,留下一个
                help.offer(queue.poll());
            }
            // 将queue中剩下的一个数据取出后返回
            T res = queue.poll();

            // 交换queue和help的引用
            // 这样就不用判断当前哪个队列存在数据,每次都是从queue倒到help,取数据、交换引用,取后又是queue队列存放数据
            // 下次拿就直接从queue中拿,大大减少代码复杂程度
            Queue<T> tmp = queue;
            queue = help;
            help = tmp;

            return res;
        }

        public T peek() {
            while (queue.size() > 1) {
                // 将queue中数据倒入help中,留下一个
                help.offer(queue.poll());
            }
            // 将queue中剩下的一个数据取出后返回
            T res = queue.poll();
            // 由于是peek,所以将最后这一个数放到help队列中
            help.offer(res);

            // 交换queue和help的引用
            Queue<T> tmp = queue;
            queue = help;
            help = tmp;

            return res;
        }

        public boolean isEmpty() {
            return queue.isEmpty();
        }

    }

    public static void main(String[] args) {
        System.out.println("test begin");
        TwoQueueStack<Integer> myStack = new TwoQueueStack<>();
        Stack<Integer> stack = new Stack<>();

        int testTime = 100000;
        int maxValue = 100000;

        for (int i = 0; i < testTime; i++) {
            if (myStack.isEmpty()) {
                if (!stack.isEmpty()) {
                    System.out.println("error");
                }
                int value = (int) ((maxValue + 1) * Math.random());
                myStack.push(value);
                stack.push(value);
            } else {
                if (Math.random() < 0.25) {
                    int value = (int) ((maxValue + 1) * Math.random());
                    myStack.push(value);
                    stack.push(value);
                } else if (Math.random() < 0.5) {
                    if (!myStack.peek().equals(stack.peek())) {
                        System.out.println("error");
                    }
                } else if (Math.random() < 0.75) {
                    if (!myStack.poll().equals(stack.pop())) {
                        System.out.println("error");
                    }
                } else {
                    if (myStack.isEmpty() != stack.isEmpty()) {
                        System.out.println("error");
                    }
                }
            }
        }
        System.out.println("test end");
    }

}

本文转载自: 掘金

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

从零开始学设计模式(十九):责任链模式(Chain of R

发表于 2021-11-26

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

作者的其他平台:

| CSDN:blog.csdn.net/qq_41153943

| 掘金:juejin.cn/user/651387…

| 知乎:www.zhihu.com/people/1024…

| GitHub:github.com/JiangXia-10…

| 公众号:1024笔记

本文大概1940字,读完预计需要7分钟

定义

责任链模式(Chain of Responsibility Pattern)又叫也叫职责链模式,指的是为了避免请求发送者与多个请求处理者耦合在一起,于是将所有请求的处理者通过前一对象记住其下一个对象的引用而连成一条链;当有请求发生时,可将请求沿着这条链传递,直到有对象处理它为止。它是一种对象行为型模式。

组成部分

责任链模式包含以下两个主要部分:

1、抽象处理类(Handler):抽象处理类中主要包含一个指向下一处理类的成员变量nextHandler和一个抽象的处理请求的方法handRequest。handRequest方法由具体的处理子类进行实现,如果满足处理的条件,则该具体的处理子类就处理这个请求,否则由nextHandler来处理,以此类推。

2、具体处理类(Concrete Handler):实现抽象处理者的处理方法,判断能否处理本次请求,如果可以处理请求则处理,否则将该请求转给它的nextHandler处理,以此类推。

例子

首先声明一个抽象的处理类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
typescript复制代码public abstract  class Handler {
private Handler nextHandler;

public Handler getNextHandler() {
return nextHandler;
}

public void setNextHandler(Handler nextHandler) {
this.nextHandler = nextHandler;
}

//抽象的处理请求的方法
public abstract void handleRequest(String request);
}

再声明两个具体的处理类,继承抽象处理类并且实现抽象的处理请求方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
scala复制代码public class ConcreteHandler1 extends Handler{
@Override
public void handleRequest(String request) {
if (request.equals("做饭")) {
System.out.println("肚子饿了具体处理者1开始做饭");
} else {
if (getNextHandler() != null) {
getNextHandler().handleRequest(request);
} else {
System.out.println("没有人处理该请求!");
}
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
scala复制代码public class ConcreteHandler2 extends Handler{
@Override
public void handleRequest(String request) {
if (request.equals("渴了")) {
System.out.println("嘴巴渴了具体处理者2开始烧水!");
} else {
if (getNextHandler() != null) {
getNextHandler().handleRequest(request);
} else {
System.out.println("没有人处理该请求!");
}
}
}
}

测试方法如下:

1
2
3
4
5
6
7
8
9
10
java复制代码public class ChainOfResponsibilityPatternTest {
public static void main(String[] args) {
//责任链
Handler handler1 = new ConcreteHandler1();
Handler handler2 = new ConcreteHandler2();
handler1.setNextHandler(handler2);
//请求
handler1.handleRequest("渴了");
}
}

运行结果如下:

可以发现责任链模式其实就是对if else这类的条件语句的改版,将不同的请求的处理逻辑封装在具体的请求处理子类中。

责任链模式的优点

1、责任链模式是一种灵活版本的if else条件处理语句,它降低了对象之间的耦合度,每个具体的处理子类只需要负责处理自己的请求即可。

2、增加了拓展性,处理新的请求只需要增加具体的处理该请求的子类即可,符合了开闭原则。

3、每个对象只需保持一个指向其后继者的引用,不需保持其他所有处理者的引用,简化了对象之间的连接。

4、符合单一职责原则,每个类都有自己的明确职责方位,只需要处理自己该处理的工作,不该处理的传递给下一个对象完成。

责任链模式的缺点

1、性能较低,如果没有找到正确的处理类,那么所有的条件都需要执行,如果责任链过长,那么会导致性能较低。

2、因为没有明确的接受者,所以可能存在请求不被处理的情况,比如上面例子中的请求如果是困了,那么就没有合适的处理子类处理这个请求了。

应用场景

1、可以使用责任链模式来处理if else语句过于负责的情况。

2、如果发生了在不明确指定请求处理者的情况下,需要向多个处理者中的一个提交请求的情况,可以采用责任链模式。

总结

责任链模式指的是当有请求发生时,可将请求沿着这条责任链链传递,直到有对象处理它为止,即如果满足处理的条件,则该具体的处理子类就处理这个请求,否则由nextHandler来处理,以此类推。它的本质其实就是将请求与处理请求的逻辑进行解耦,让请求在处理链中能进行传递与被处理。

最后本文以及之前的所有的设计模式中的例子代码,都将同步至github,需要的欢迎下载star。

Github地址:

github.com/JiangXia-10…

相关推荐:

从零开始学设计模式(一):什么是设计模式

从零开始学设计模式(二):单例模式

从零开始学设计模式(三):原型模式(Prototype Pattern)

从零开始学设计模式(四):工厂模式(Factory Pattern)

从零开始学设计模式(五):建造者模式(Builder Pattern)

从零开始学设计模式(六):适配器模式(Adapter Pattern)

从零开始学设计模式(六):代理模式(Proxy Pattern)

从零开始学设计模式(八):装饰器模式(Decorator Pattern)

从零开始学设计模式(九):外观模式(Facade Pattern)

从零开始学设计模式(十):桥接模式(Bridge Pattern)

从零开始学设计模式(十一):组合模式(Composite Pattern)

从零开始学设计模式(十二):享元模式(Flyweight Pattern)

从零开始学设计模式(十三):访问者模式(Visitor Pattern)

从零开始学设计模式(十四):中介者模式(Mediator Pattern)

从零开始学设计模式(十五):模版方法模式(Template Method Pattern)

从零开始学设计模式(十六):策略模式(Strategy Pattern)

从零开始学设计模式(十七):备忘录设计模式(Memento Pattern)

从零开始学设计模式(十八):状态模式(State Pattern)

本文转载自: 掘金

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

「Rust 中方便和习惯性的转换」TryFrom和错误

发表于 2021-11-26

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


尝试:TryFrom

我们看到,由From和Into进行的转换不能失败。然而,有时我们处理的类型并不完全相互映射,所以我们需要这些trait的易错版本。

幸运的是,有 TryFrom 和 TryInto,它们返回一个 Result<TargetType, ErrorType>。这两个trait和它们NB的兄弟姐妹一起存在于std::convert中,但它们的确切细节和含义仍在讨论中,这意味着它们仍被标记为不稳定。为了使用它们,我们可以将自己限制在编译器的nightly版本中,使用 try_from,或者将它们的定义粘贴在我们的库中的某个文件中(它们真的很短)。

让我们来看看TryFrom的定义(截至Rust 1.10.0):

1
2
3
4
5
6
7
8
rust复制代码#[unstable(feature = "try_from", issue = "33417")]
pub trait TryFrom<T>: Sized {
/// The type returned in the event of a conversion error.
type Err;

/// Performs the conversion.
fn try_from(T) -> Result<Self, Self::Err>;
}

首先,我们有一个稳定性属性,标志着该特性是不稳定的,然后是特性定义本身。我们可以看到它有一个相关的类型,Err,用于转换失败的情况。正如预期的那样,我们有一个try_from方法而不是from,它返回Result<Self, Self::Err>而不是Self。

继续我们的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
rust复制代码impl TryFrom<u8> for PacketType {
type Err = ParseError;
fn try_from(original: u8) -> Result<Self, Self::Err> {
match original {
0 => Ok(PacketType::Data),
1 => Ok(PacketType::Fin),
2 => Ok(PacketType::State),
3 => Ok(PacketType::Reset),
4 => Ok(PacketType::Syn),
n => Err(ParseError::InvalidPacketType(n))
}
}
}

在这个例子中,对于可以映射的值,我们返回相应的 PacketType 变量,对于其余的值,我们返回一个错误。这个错误类型保留了原始值,这对调试有潜在的帮助,但其实我们可以直接丢弃它。

AsRef and AsMut

最后我们看看std::convert模块中剩下的trait,放到最后并非是它们不重要。AsRef和AsMut。像convert模块中的其他trait一样,它们被用来实现类型间的转换。

然而,其他特质会消耗数值,并可能执行重的操作,而 AsRef 和 AsMut 是用来实现轻便的,引用到引用的转换。

你可能已经从它们的名字中猜到了,AsRef将一个不可变的值的引用转换为另一个不可变的引用,而AsMut对可变的引用做同样的转换。

由于它们都非常相似,我们同时看看它们。让我们从它们的定义开始:

1
2
3
4
5
6
7
8
9
10
11
12
13
rust复制代码#[stable(feature = "rust1", since = "1.0.0")]
pub trait AsRef<T: ?Sized> {
/// Performs the conversion.
#[stable(feature = "rust1", since = "1.0.0")]
fn as_ref(&self) -> &T;
}

#[stable(feature = "rust1", since = "1.0.0")]
pub trait AsMut<T: ?Sized> {
/// Performs the conversion.
#[stable(feature = "rust1", since = "1.0.0")]
fn as_mut(&mut self) -> &mut T;
}

本文转载自: 掘金

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

TCP UDP的区别

发表于 2021-11-26

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

可以从 连接特点、交互通信、数据处理、传输服务、报头开销 五个方面进行对比

连接特点

UDP

UDP是一种无连接的传输层协议,因为在使用UDP发送报文段时,发送端和接收端的传输层实体之间没有进行握手。所谓的握手,就是发送端和接收端通过发送一些特定的报文段来互相确认,从而为发送做准备。由于UDP可以不用任何准备即可进行数据数据传输,因此UDP的数据传输速度会比TCP快。

TCP

TCP是一种面向连接的传输层协议,网络系统需要在两台计算机之间发送数据之前先建立连接。类似于我们打电话一样,通信之前需要呼叫和应答。其过程分为建立连接(三次握手)、使用连接(数据传输)、释放连接(四次挥手)三个过程。由于这些机制,TCP数据传输会比UDP可靠,即确保双方都互通后再发送数据,保证数据包能够完整的发送过去。

交互通信

UDP

UDP是无连接的传输协议,不需要维护连接状态,包括收发状态,可以实现一对一,一对多,多对一和多对多的交互通信。

TCP

TCP是面向连接的传输协议,发送数据需要双方建立连接,属于端到端的通信,实现的是一对一的交互通信。

数据处理

UDP

UDP是面向报文的。发送端的传输层对应用层交下来的报文,在添加报头后就向下交付给IP层。既不拆分,也不合并,而是保留这些报文的边界。接收端传输层接收到报文会去掉报头,将数据部分交给应用层。

TCP

TCP是面向字节流的。发送端的应用层将数据字节流交付到传输层的缓存区,根据发送策略对字节流分片,添加报头发送TCP报文。接收端传输层收到报文后,去掉报头存储到接收缓存。接收缓存将字节流片段交给应用层,应用层再将字节流片段重组还原为可用的数据。

传输服务

UDP

UDP提供无连接的不可靠服务。在发送端到接收端的传递过程中出现数据包丢失或接收误码的情况,协议本身并不能做出任何检测或提示。UDP只是尽可能快地把数据扔到网络上,并不保证数据包的完整性。因此UDP没有可靠性保证、顺序保证和流量控制字段。

TCP

TCP提供面向连接的可靠服务。在发送端到接收端的传递过程中出现数据包丢失或接收误码的情况,接收端在定时器超时后没有收到相应的确认,发送端会重新发送数据包。TCP连接每一方的接收缓冲空间大小都固定,接收端只允许另一端发送接收端缓冲区所能接纳的数据,TCP在此基础上提供流量控制,防止较快主机致使较慢主机的缓冲区溢出。

报头开销

UDP

UDP传输的段有8个字节的报头和有效载荷字段构成。UDP报头由4个域组成,其中每个域各占用2个字节,具体包括源端口号、目标端口号、数据报长度、校验和。

  • 端口号:使用端口号为不同的应用保留其各自的数据传输通道
  • 长度:数据报的长度是指包括报头和数据部分在内的总字节数
  • 校验和:使用报头中的校验值来保证数据的安全

TCP

TCP传输的段有最小20字节的报头和有效载荷字段构成。TCP具体组成包括端口号、序号、确认号、保留域、标志域、窗口、校验和、紧急指针构成,另外可扩展首部包括选项和填充。

总结

UDP

特点
  • 无连接
  • 支持一对一,一对多,多对一和多对多的通信
  • 面向报文
  • 不可靠传输,不使用流量控制和拥塞控制
  • 报头开销小,仅8字节
应用场景

常用于实时应用。例如视频直播、IP电话,QQ语音和QQ视频就是使用UDP的协议。

TCP

特点
  • 面向连接
  • 一对一通信
  • 面向字节流
  • 可靠传输,使用流量控制和拥塞控制
  • 报头最小20字节,最大60字节
应用场景

常用于对可靠性要求高的通信。例如文件传输。

本文转载自: 掘金

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

1…165166167…956

开发者博客

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