2024年7月

问题表现

在我的 OpenStack 集群上迁移了一批老旧的镜像(从其他三方云平台过来的)发现这批镜像在使用 ConfigDrive 的方式注入配置初始化时无法对非首张网卡镜像初始化(后经过测试非 ConfigDrive 的数据源也不行)。

排查路径

首先检查 cloud-init 是否是正常工作的
实例化镜像查看 cloud-init 服务, 以及相关日志。

systemctl status cloud-init
systemctl status cloud-init-local

服务均正常启用。
再查看 Cloud-init 的初始化日志

[   19.254076] cloud-init[1483]: Cloud-init v. 0.7.5 finished at Tue, 02 Jul 2024 06:28:30 +0000. Datasource DataSourceConfigDriveNet [net,ver=2][source=/dev/sr0].  Up 19.24 seconds

可以看到有类似读取到数据源并实例化的日志。基本上可以可以排除 cloud-init 运行不正常的情况。

cloud-init 调试

只能细化cloud-init初始化的流程。
关于cloud-init 运行阶段的讲述推荐这篇文章,我此处不多赘述。
https://www.cnblogs.com/frankming/p/16281447.html
此处给出快速重重跑初始化的脚本

# centos7
#! /bin/bash
cloud-init clean
rm -rf /var/run/cloud-init/
rm -rf /var/lib/cloud/
rm -rf /etc/sysconfig/network-scripts/ifcfg-*

# local 阶段数据源准备
cloud-init init --local

# 网络阶段, 渲染数据
cloud-init init

# 执行模块
cloud-init modules --mode=config
# centos6
#! /bin/bash
rm -rf /var/run/cloud-init/
rm -rf /var/lib/cloud/
rm -rf /etc/sysconfig/network-scripts/ifcfg-*

# local 阶段数据源准备
cloud-init init --local

# 网络阶段, 渲染数据
cloud-init init

# 执行模块
cloud-init modules --mode=config

很遗憾,在重跑初始化流程中未看出端疑,对比可以初始化多张网卡的日志(CentOS7系统上),总感觉 CentOS6 在网卡配置阶段无任务操作。于是拖下了 Cloud-init 的源码码进行静态审计+Print大发。
源码路径:

/usr/lib/python2.6/site-packages/cloudinit

定位到 Cloud-init 7.5 的这个位置

...
# sources/DataSourceConfigDrive.py +166
def read_config_drive(source_dir, version="2012-08-10"):
    reader = openstack.ConfigDriveReader(source_dir)
    finders = [
        (reader.read_v2, [], {'version': version}),
        (reader.read_v1, [], {}),
    ]
    excps = []
    for (functor, args, kwargs) in finders:
        try:
            return functor(*args, **kwargs)
        except openstack.NonReadable as e:
            excps.append(e)
    raise excps[-1]
...
...
# sources/DataSourceConfigDrive.py +59
    def get_data(self):
        found = None
        md = {}
        results = {}
        if os.path.isdir(self.seed_dir):
            try:
                results = read_config_drive(self.seed_dir)
                found = self.seed_dir
            except openstack.NonReadable:
                util.logexc(LOG, "Failed reading config drive from %s",
                            self.seed_dir)
        if not found:
            for dev in find_candidate_devs():
                try:
                    results = util.mount_cb(dev, read_config_drive)
                    found = dev
                except openstack.NonReadable:
...

可以看到在挂载 /dev/sr0 设备后,cloud-init 0.7.5 版本使用的是 2012-08-10 数据源
手动挂载并查看

[root@aa home]# mount /dev/sr0 /mnt/
mount: /dev/sr0 is write-protected, mounting read-only
[root@aa home]# ls /mnt/
ec2  openstack
[root@aa home]# ls /mnt/openstack/2012-08-10/
meta_data.json  user_data

好哇,根本没有network_data.json这种东西。看相关网络配置的初始化,验证网络初始逻辑只给ubuntu做了适配。得出结果,Cloud-init 0.7.5 版本过低,centos7支持差导致。

解决方案

解决方案大致有两种:

1. 升级 Cloud-init
2. 手工实现 Cloud-init 网络初始化部分的逻辑

解决方案一

升级的话首选需要升级Python版本,本人未采用升级的方案,所以不多赘述,但是是一定可行的,推荐手动升级Python 并源码安装 Cloud-init。

解决方案二

推荐像本人一样参考高版本的 cloud-init 驱动实现逻辑手工用 go 或者 C 语言这种重新写一个补丁,经测试可行。由于这个组件是为公司开发的,不方便开源,但是欢迎交流。

Guava Cache、EVCache、Tair、Aerospike 是不同类型的缓存解决方案,它们各有特点和应用场景。下面我会逐一分析这些缓存系统的优势、应用场景,并提供一些基本的代码示例。

Guava Cache

优势特点:

  • 内置在Guava库中,易于集成和使用。
  • 提供了丰富的缓存配置选项,如过期策略、缓存加载策略等。
  • 适用于单机应用内的缓存场景。

应用场景:

  • 本地缓存,用于提升Java应用的数据处理速度。
  • 适合数据量不大,需要快速访问的场合。

代码示例:

LoadingCache<String, Object> cache = CacheBuilder.newBuilder()
    .maximumSize(1000)
    .expireAfterWrite(10, TimeUnit.MINUTES)
    .build(
        new CacheLoader<String, Object>() {
            public Object load(String key) {
                // 缓存不存在时,加载缓存逻辑
                return fetchDataFromDatabase(key);
            }
        }
    );

EVCache

优势特点:

  • 基于Memcached,支持数据的分布式存储。
  • 高性能和高可用性。
  • 支持数据压缩和高效的序列化机制。

应用场景:

  • 分布式缓存,适用于大型应用和系统。
  • 高并发访问场景,如大型网站、社交网络等。

代码示例:

EVCache cache = EVCache.getCache("myCache", "myApp");
String value = cache.get("key");

Tair

优势特点:

  • 由阿里巴巴开发,支持分布式数据存储。
  • 提供丰富的数据结构支持,如K-V、列表、集合等。
  • 高可用性和可扩展性。

应用场景:

  • 适用于需要高可用、高并发、大数据量的场景。
  • 适合大规模分布式系统。

代码示例:
// 示例代码为伪代码,因为Tair的具体API依赖于客户端库

TairClient tairClient = new TairClient("config.properties");
tairClient.put(1, "key", "value");

Aerospike

优势特点:

  • 高性能的NoSQL数据库,支持缓存和持久化存储。
  • 支持数据备份和自动恢复。
  • 适用于大规模数据集和低延迟访问。

应用场景:

  • 适用于需要高性能、低延迟的数据访问场景。
  • 适合大规模分布式系统,如实时广告投放、用户行为分析等。

代码示例:

// 示例代码为伪代码,因为Aerospike的具体API依赖于客户端库
AerospikeClient client = new AerospikeClient("localhost", 3000);
Key key = new Key("test", "demo", "key");
Record record = client.get(null, key);

在选择适合的缓存系统时,需要根据具体的应用场景、性能要求、数据规模等因素进行综合考虑。每种缓存系统都有其独特的优势,选择合适的缓存策略对提升系统性能至关重要。

下面 V 哥来一一说一说各自的实现原理,方更兄弟们更好的理解。

实现原理

Guava Cache

Guava Cache是Google Guava库提供的一种基于内存的缓存实现,它主要用于提升数据访问速度,减少对底层存储系统(如数据库)的访问。Guava Cache的实现原理主要包括以下几个方面:

1. 缓存加载策略:

  • 懒加载:当缓存项不存在时,可以通过定义的加载函数(CacheLoader)来异步加载缓存值。
  • 预加载:可以在创建缓存时通过CacheBuilder的build方法传入一个CacheLoader,实现缓存的预加载。

2. 缓存回收策略:

  • 基于大小回收:可以设置缓存的最大条目数(maximumSize)或最大权重(maximumWeight),当达到限制时,会根据一定的策略(如最少使用(LRU)、先进先出(FIFO)等)回收缓存项。
  • 基于时间回收:可以设置缓存项在写入后多久(expireAfterWrite)或访问后多久(expireAfterAccess)失效。

3. 缓存刷新策略:

  • 可以设置缓存项在多久之后自动刷新(refreshAfterWrite),这通常用于数据可能会更新的场景。

4. 并发控制:

  • Guava Cache内部使用了锁机制来保证并发访问时的线程安全。

5. 统计功能:

  • Guava Cache提供了缓存命中、未命中、加载时间等统计信息,便于监控和调优。

6. 移除监听器:

  • 可以添加移除监听器(RemovalListener),当缓存项被移除时执行特定的逻辑,如资源清理等。

7. 缓存键和缓存值:

  • 缓存键可以是任意不可变对象,缓存值可以是任意对象。

Guava Cache的实现原理基于以上几个核心概念,通过组合这些概念,可以创建出适合不同场景的缓存策略。例如,可以创建一个有限大小的缓存,当缓存达到最大容量时,会根据访问频率回收不常用的缓存项,同时,当缓存项长时间未被访问时,缓存会自动将其移除。

以下是Guava Cache的一个简单示例:

LoadingCache<String, String> cache = CacheBuilder.newBuilder()
    .maximumSize(100) // 设置最大缓存条目数
    .expireAfterWrite(10, TimeUnit.MINUTES) // 设置写入10分钟后过期
    .build(
        new CacheLoader<String, String>() {
            @Override
            public String load(String key) {
                // 缓存项不存在时,加载缓存值的逻辑
                return getDataFromDatabase(key);
            }
        }
    );

String value = cache.getUnchecked("key"); // 获取缓存值

在这个示例中,cache是一个LoadingCache实例,它会在缓存项不存在时通过load方法加载数据。缓存设置了最大条目数和写入过期时间,当缓存项达到最大数量或写入超过10分钟后,相应的缓存项会被回收。

EVCache

EVCache 是一个分布式数据缓存系统,基于 Memcached 协议构建。它主要用于缓存大量数据,以减少对后端数据库的访问,提高系统的响应速度。EVCache 的实现原理可以从以下几个方面来理解:

分布式存储:
EVCache 将数据存储在多个 Memcached 节点上,每个节点可以存储一部分数据。这种分布式存储机制可以提高数据的读取速度,因为请求可以并行发送到不同的节点。

数据一致性:
EVCache 通常使用一致性哈希(Consistent Hashing)算法来决定数据应该存储在哪个节点上。这种算法可以使得数据在节点之间均匀分布,并且当节点加入或离开集群时,能够最小化数据迁移的数量。

数据复制:
为了提高可用性和容错性,EVCache 支持数据复制。可以将数据的副本存储在多个节点上,这样即使某个节点发生故障,其他节点上的副本仍然可以提供服务。

缓存失效策略:
Memcached 内置了简单的缓存失效机制,例如 LRU(Least Recently Used)和到期失效(Time-to-Live, TTL)。EVCache 继承了这些特性,允许开发者设置数据的过期时间,以确保缓存数据的时效性和新鲜度。

客户端支持:
EVCache 提供了客户端库,使得开发者可以轻松地在应用程序中集成 EVCache。客户端库通常提供了丰富的 API,用于数据的存取、缓存管理和其他高级功能。

性能优化:
EVCache 优化了网络通信和数据序列化/反序列化过程,以减少延迟并提高吞吐量。它还支持数据压缩,以减少网络带宽的使用。

监控和管理:
EVCache 支持监控和管理功能,允许开发者监控缓存节点的健康状况、性能指标和统计数据。这有助于及时发现和解决问题,以及进行性能调优。

EVCache 的应用场景包括需要高速缓存的大型网站和服务,如 Amazon、Netflix 等公司就使用了 EVCache 来缓存大量的用户数据,以提高用户体验和系统性能。

由于 EVCache 是一个复杂的系统,它通常需要与 Memcached 服务器、客户端库和应用程序集成在一起。因此,它的实现细节可能会因具体的部署和配置而有所不同。

Tair

Tair 是一个分布式高性能缓存系统,由阿里巴巴集团开发。它支持多种数据结构,如键值对(K-V)、列表、集合等,并且提供了高可用性和可扩展性。Tair 的实现原理可以从以下几个方面来理解:

数据分片:
Tair 将数据分散存储在多个节点上,每个节点负责存储一部分数据。这种分片机制允许系统水平扩展,通过增加节点来提高存储容量和吞吐量。

一致性哈希:
Tair 使用一致性哈希算法来决定数据应该存储在哪个节点上。这种算法可以使得数据在节点之间均匀分布,并且在节点加入或离开集群时,能够最小化数据迁移的数量。

数据复制:
为了提高数据的可靠性和可用性,Tair 支持数据复制。可以将数据的副本存储在多个节点上,这样即使某个节点发生故障,其他节点上的副本仍然可以提供服务。

多种数据结构支持:
Tair 不仅支持简单的键值对存储,还支持列表、集合等复杂数据结构,这使得它能够适用于多种不同的业务场景。

持久化存储:
Tair 支持将数据持久化存储到磁盘上,这样即使系统发生故障,数据也不会丢失。这使得 Tair 既可以作为缓存系统使用,也可以作为轻量级的分布式存储系统使用。

缓存失效策略:
Tair 支持多种缓存失效策略,如 LRU(Least Recently Used)、TTL(Time-to-Live)等,以确保缓存数据的时效性和新鲜度。

客户端支持:
Tair 提供了客户端库,使得开发者可以轻松地在应用程序中集成 Tair。客户端库通常提供了丰富的 API,用于数据的存取、缓存管理和其他高级功能。

高可用性和故障转移:
Tair 设计了故障转移机制,当某个节点发生故障时,系统可以自动将请求路由到其他健康的节点上,从而保证服务的高可用性。

Tair 的应用场景包括需要高并发、大数据量、低延迟访问的分布式系统。例如,它可以用于存储用户会话信息、商品信息、分布式锁等。由于 Tair 提供了多种数据结构支持,它能够满足不同业务场景的需求。

由于 Tair 是一个复杂的分布式系统,它的实现细节涉及到数据分片、复制、一致性保证、故障转移等多个方面。这些细节通常由 Tair 的内部架构和算法来处理,对用户来说是透明的。

Aerospike

Aerospike 是一个分布式NoSQL数据库,它专为高速、可扩展性、和高可用性而设计。Aerospike 的实现原理可以从以下几个方面来理解:

数据模型:
Aerospike 使用了一个简单的数据模型,由“namespace”、“set”和“record”组成。每个记录由一个唯一的键(key)和一个或多个二进制数据结构(bin)组成,其中bins存储实际的数据。

分布式架构:
Aerospike 设计为分布式系统,数据分布在多个服务器节点上。它使用了一种称为“哈希分区”(hash partitioning)的技术,将数据均匀地分布在集群中的所有节点上。

内存中存储:
Aerospike 主要将数据存储在内存中,以实现快速的数据访问。它使用了一种名为“内存中索引”(in-memory index)的技术,允许对数据进行快速查询。

持久化存储:
尽管数据主要存储在内存中,Aerospike 还支持将数据持久化到磁盘上。它使用了一种名为“写后日志”(write-ahead logging, WAL)的技术,确保数据的持久性和一致性。

数据复制:
Aerospike 支持数据复制,可以配置数据的副本数量。副本存储在不同的节点上,以提高数据的可靠性和可用性。

缓存和存储一体化:
Aerospike 结合了缓存和存储的特点,既可以作为高速缓存使用,也可以作为持久化存储使用。它通过自动管理内存中的数据和磁盘上的数据来实现这一点。

动态集群管理:
Aerospike 集群可以动态地添加或移除节点,而不会影响到正在进行的操作。这使得系统能够适应负载的变化和扩展需求。

强大的查询功能:
Aerospike 支持复杂的查询操作,包括二级索引、聚合查询和地理位置查询等。这些查询操作可以快速执行,因为它们主要在内存中完成。

事务支持:
Aerospike 支持原子性的事务操作,确保了多个操作在逻辑上的完整性和一致性。

Aerospike 的应用场景包括需要高速、低延迟数据访问的场合,如实时推荐、用户行为分析、在线广告等。它的设计目标是提供高吞吐量和低延迟,同时保持数据的持久性和一致性。

Aerospike 的实现细节涉及到数据分布、内存管理、磁盘I/O优化、数据复制和一致性保证等多个方面。这些技术确保了 Aerospike 能够在高速、可扩展和高可用的环境中稳定运行。

最后

除了Redis 框架,以上是对 GuavaCache、EVCache、Tair、Aerospike 缓存框架的比较,在实际项目中选持适合的才是最好的。关注威哥爱编程,让我们一起在技术海洋奔跑

本文分享自华为云社区
《【Spring Boot 源码学习】初识 SpringApplication》
,作者: Huazie。

引言

往期的博文,Huazie 围绕 Spring Boot 的核心功能,带大家从总整体上了解 Spring Boot 自动配置的原理以及自动配置核心组件的运作过程。这些内容大家需要重点关注,只有了解这些基础的组件和功能,我们在后续集成其他三方类库的 Starters 时,才能够更加清晰地了解它们都运用了自动配置的哪些功能。

在学习上述 Spring Boot 核心功能的过程中,相信大家可能都会尝试启动自己新建的 Spring Boot 的项目,并 Debug 看看具体的执行过程。本篇开始就将从 Spring Boot 的启动类
SpringApplication
上入手,带领大家了解 Spring Boot 启动过程中所涉及到的源码和知识点。

主要内容

1. Spring Boot 应用程序的启动

在 《
【Spring Boot 源码学习】@SpringBootApplication 注解
》这篇博文中,我们新建了一个基于 Spring Boot 的测试项目。

image.png

如上图中的
DemoApplication
就是我们这里 Spring Boot 项目的入口类。

同时,我们可以看到
DemoApplication

main
方法中,直接调用了
SpringApplication
的静态方法
run
,用于启动整个 Spring Boot 项目。

先来看看
run
方法的源码:

public static ConfigurableApplicationContext run(Class<?>primarySource, String... args) {return run(new Class<?>[] { primarySource }, args);
}
public static ConfigurableApplicationContext run(Class<?>[] primarySources, String[] args) {return newSpringApplication(primarySources).run(args);
}

阅读上述
run
方法,我们可以看到实际上是
new
了一个
SpringApplication
对象【其构造参数
primarySources
为加载的主要资源类,通常就是
SpringBoot
的入口类】,并调用其
run
方法【其参数
args
为传递给应用程序的参数信息】启动,然后返回一个应用上下文对象
ConfigurableApplicationContext

通过观察这个内部的
run
方法实现,我们也可以在自己的 Spring Boot 启动入口类中,像如下这样去写 :

@SpringBootApplicationpublic classDemoApplication {public static voidmain(String[] args) {
SpringApplication springApplication
= new SpringApplication(DemoApplication.class);//这里可以调用 SpringApplication 提供的 setXX 或 addXX 方法来定制化设置 springApplication.run(args);
}

}

2. SpringApplication 的实例化

上面已经看到我们在实例化
SpringApplication
了,废话不多说,直接翻看其源码【Spring Boot 2.7.9】:

    public SpringApplication(Class<?>... primarySources) {this(null, primarySources);
}

@SuppressWarnings({
"unchecked", "rawtypes"})public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {this.resourceLoader =resourceLoader;
Assert.notNull(primarySources,
"PrimarySources must not be null");this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources));//推断web应用类型 this.webApplicationType =WebApplicationType.deduceFromClasspath();//加载并初始化 BootstrapRegistryInitializer及其实现类 this.bootstrapRegistryInitializers = new ArrayList<>(
getSpringFactoriesInstances(BootstrapRegistryInitializer.
class));//加载并初始化 ApplicationContextInitializer及其实现类 setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));//加载并初始化ApplicationListener及其实现类 setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));//推断入口类 this.mainApplicationClass =deduceMainApplicationClass();
}

由上可知,
SpringApplication
提供了两个构造方法,而其核心的逻辑都在第二个构造方法中实现。

2.1 构造方法参数

我们从上述源码可知,
SpringApplication
的第二个构造方法有两个参数,分别是:

ResourceLoader resourceLoader

ResourceLoader
为资源加载的接口,它用于在Spring Boot 启动时打印对应的 banner 信息,默认采用的就是
DefaultResourceLoader
。实操过程中,如果未按照 Spring Boot 的 “约定” 将 banner 的内容放置于
classpath
下,或者文件名不是
banner.*
格式,默认资源加载器是无法加载到对应的 banner 信息的,此时则可通过
ResourceLoader
来指定需要加载的文件路径【这个后面我们专门来实操一下,敬请期待】。

Class<?>... primarySources
:主要的 bean 来源,该参数为可变参数,默认我们会传入 Spring Boot 的入口类【即
main
方法所在的类】,如上面我们的
DemoApplication
。如果作为项目的引导类,该类需要满足一个条件,就是被注解
@EnableAutoConfiguration
或其组合注解标注。在前面的《
【Spring Boot 源码学习】@SpringBootApplication 注解
》博文中,我们已经知道
@SpringBootApplication
注解中包含了
@EnableAutoConfiguration
注解,因此被
@SpringBootApplication
注解标注的类也可作为参数传入。当然,
primarySources
也可传入其他普通类,但只有传入被
@EnableAutoConfiguration
标注的类才能够开启 Spring Boot 的自动配置。

有些朋友,可能对
primarySources
这个可变参数的描述有点疑惑,下面我们就用实例来演示以其他引导类为入口类进行 Spring Boot 项目启动:

首先,我们在入口类
DemoApplication
的同级目录创建一个
SecondApplication
类,使用
@SpringBootApplication
进行注解。

@SpringBootApplicationpublic classSecondApplication {
}

然后,将
DemoApplication
修改成如下:

public classDemoApplication {public static voidmain(String[] args) {
SpringApplication.run(SecondApplication.
class, args);
}
}

最后,我们来运行
DemoApplication

main
方法。

image.png

从上图可以看出,我们的应用依然能正常启动,并完成自动配置。因此,决定 Spring Boot 启动的入口类并不是一定是
main
方法所在类,而是直接或间接被
@EnableAutoConfiguration
标注的类。

翻看
SpringApplication
的源码,我们在其中还能看到它提供了追加
primarySources
的方法,如下所示:

public void addPrimarySources(Collection<Class<?>>additionalPrimarySources) {this.primarySources.addAll(additionalPrimarySources);
}

如果采用 1 中最后的方式启动 Spring Boot ,我们就可以调用
addPrimarySources
方法来追加额外的
primarySources

我们继续回到
SpringApplication
的构造方法里,可以看到如下的代码:

this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources));

上述这里将
primarySources
参数转换为
LinkedHashSet
集合,并赋值给
SpringApplication
的私有成员变量
Set<Class<?>> primarySources

知识点:
LinkedHashSet
是 Java 集合框架中的类,它继承自
HashSet
,因此具有哈希表的查找性能。这是一个同时使用链表和哈希表特性的数据结构,其中链表用于维护元素的插入顺序。也即是说,当你向
LinkedHashSet
添加元素时,元素将按照添加的顺序被存储,并且能够被遍历输出。

此外,
LinkedHashSet
还确保了 元素的唯一性,即重复的元素在集合中只会存在一份。

如果需要频繁遍历集合,那么
LinkedHashSet
可能会比
HashSet
效率更高,因为其通过维护一个双向链表来记录元素的添加顺序,从而支持按照插入顺序排序的迭代。但需要注意的是,
LinkedHashSet
是非线程安全的,如果有多个线程同时访问该集合容器,可能会引发并发问题。

2.2 Web 应用类型推断

我们继续往下翻看源码,这里调用了
WebApplicationType

deduceFromClasspath
方法来进行 Web 应用类型的推断。

this.webApplicationType = WebApplicationType.deduceFromClasspath();

我们继续翻看
WebApplicationType
的源码:

public enumWebApplicationType {//非Web应用类型
NONE,//基于Servlet的Web应用类型
SERVLET,//基于reactive的Web应用类型
REACTIVE;private static final String[] SERVLET_INDICATOR_CLASSES = { "javax.servlet.Servlet","org.springframework.web.context.ConfigurableWebApplicationContext"};private static final String WEBMVC_INDICATOR_CLASS = "org.springframework.web.servlet.DispatcherServlet";private static final String WEBFLUX_INDICATOR_CLASS = "org.springframework.web.reactive.DispatcherHandler";private static final String JERSEY_INDICATOR_CLASS = "org.glassfish.jersey.servlet.ServletContainer";staticWebApplicationType deduceFromClasspath() {if (ClassUtils.isPresent(WEBFLUX_INDICATOR_CLASS, null) && !ClassUtils.isPresent(WEBMVC_INDICATOR_CLASS, null)&& !ClassUtils.isPresent(JERSEY_INDICATOR_CLASS, null)) {returnWebApplicationType.REACTIVE;
}
for(String className : SERVLET_INDICATOR_CLASSES) {if (!ClassUtils.isPresent(className, null)) {returnWebApplicationType.NONE;
}
}
returnWebApplicationType.SERVLET;
}
}

WebApplicationType
是一个定义了可能的Web应用类型的枚举类,该枚举类中包含了三块逻辑:

枚举类型 :非 Web 应用、基于 Servlet 的 Web 应用和基于 reactive 的 Web 应用。

用于下面推断的常量

推断类型的方法 deduceFromClasspath :


DispatcherHandler
存在,并且
DispatcherServlet

ServletContainer
都不存在,则返回类型为
WebApplicationType.REACTIVE


Servlet

ConfigurableWebApplicationContext
任何一个不存在时,则说明当前应用为非 Web 应用,返回
WebApplicationType.NONE

当应用不为 reactive Web 应用,并且
Servlet

ConfigurableWebApplicationContext
都存在的情况下,则返回
WebApplicationType.SERVLET

在上述的
deduceFromClasspath
方法中,我们可以看到,在判断的过程中使用到了
ClassUtils

isPresent
方法。该工具类方法就是通过反射创建指定的类,根据在创建过程中是否抛出异常来判断该类是否存在。

image.png

2.3 加载 BootstrapRegistryInitializer

this.bootstrapRegistryInitializers = new ArrayList<>(
getSpringFactoriesInstances(BootstrapRegistryInitializer.
class));

上述逻辑用于加载并初始化
BootstrapRegistryInitializer
及其相关的类。

BootstrapRegistryInitializer
是 Spring Cloud Config 的组件之一,它的作用是在应用程序启动时初始化 Spring Cloud Config 客户端。

在 Spring Cloud Config 中,客户端通过向配置中心(Config Server)发送请求来获取应用程序的配置信息。而
BootstrapRegistryInitializer
就是负责将配置中心的相关信息注册到 Spring 容器中的。

由于篇幅有限,有关
BootstrapRegistryInitializer
更详细的内容,笔者后续专门讲解。

2.4 加载 ApplicationContextInitializer

setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));

上述代码用于加载并初始化
ApplicationContextInitializer
及其相关的类。

ApplicationContextInitializer
是 Spring 框架中的一个接口,它的主要作用是在Spring容器刷新之前初始化
ConfigurableApplicationContext
。这个接口的实现类可以被视为回调函数,它们的
onApplicationEvent
方法会在Spring 容器启动时被自动调用,从而允许开发人员在容器刷新之前执行一些自定义的操作。

例如,我们可能需要在这个时刻加载一些配置信息,或者对某些 bean 进行预处理等。通过实现
ApplicationContextInitializer
接口并重写其
onApplicationEvent
方法,就可以完成这些定制化的需求。

由于篇幅有限,有关
ApplicationContextInitializer
更详细的内容,笔者后续专门讲解。

2.5 加载 ApplicationListener

setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));

上述代码用于加载并初始化
ApplicationListener
及其相关的类。

ApplicationListener
是 Spring 框架提供的一个事件监听机制,它是Spring 应用内部的事件驱动机制,通常被用于监控应用内部的运行状况。其实现的原理是 观察者设计模式,该设计模式的初衷是为了实现系统业务逻辑之间的解耦,从而提升系统的可扩展性和可维护性。

我们可以通过自定义一个类来实现
ApplicationListener
接口,然后在这个类中定义需要监听的事件处理方法。当被监听的事件发生时,Spring 会自动调用这个方法来处理事件。例如,在一个 Spring Boot 项目中,我们可能想要在容器启动时执行一些特定的操作,如加载配置等,就可以通过实现
ApplicationListener
接口来完成。

由于篇幅有限,有关
ApplicationListener
更详细的内容,笔者后续专门讲解。

2.6 推断应用入口类

最后一步,调用
SpringApplication

deduceMainApplicationClass
方法来进行入口类的推断:

private Class<?>deduceMainApplicationClass() {try{
StackTraceElement[] stackTrace
= newRuntimeException().getStackTrace();for(StackTraceElement stackTraceElement : stackTrace) {if ("main".equals(stackTraceElement.getMethodName())) {returnClass.forName(stackTraceElement.getClassName());
}
}
}
catch(ClassNotFoundException ex) {//这里捕获异常,并继续执行后续逻辑 }return null;
}

上述代码的思路就是:

  • 首先,创建一个运行时异常,并获得其堆栈数组。
  • 接着,遍历数组,判断类的方法中是否包含
    main
    方法。第一个被匹配的类会通过
    Class.forName
    方法创建对象,并将其被返回。
  • 最后,将上述创建的
    Class
    对象赋值给
    SpringApplication
    的成员变量
    mainApplicationClass

总结

本篇 Huazie 带大家初步了解了 SpringApplication 的实例化过程,当然由于篇幅受限,还有些内容暂时无法详解,Huazie 将在后续的博文中继续深入分析。

只有了解 Spring Boot 在启动时都做了些什么,我们才能在后续的实践的过程中更好地理解其运行机制,以便遇到问题能更快地定位和排查,使我们应用能够更容易、更方便地接入 Spring Boot 。

点击关注,第一时间了解华为云新鲜技术~


PaddleNLP UIE 实体关系抽取 -- 抽取药品说明书(名称、规格、用法、用量)

对于细分场景推荐使用轻定制功能(标注少量数据进行模型微调)以进一步提升效果

schema =['药品名称','用法','用量','频次']
ie = Taskflow('information_extraction',schema=schema)
pprint(ie("布洛芬分散片,口服或加水分散后服用。用于成人及12岁以上儿童,推荐剂里为一次0.2~0.4(1~2片)一日3次,或遵医嘱"))

如图:
默认模型只能提取出药品名称
,接下来,通过训练数据进行UIE模型微调
image

环境配置

自己创建项目有个好处,避免了好多版本问题带来的坑:
https://www.cnblogs.com/vipsoft/p/18265581#问题处理

  • Python 3.10.10
  • paddlepaddle-gpu Version: 2.5.2 [模型微调必须GPU,其它可以CPU]
  • PaddleNLP 2.6.1

image

创建项目

登录AI Studio 免费算力
获得免费算力
https://aistudio.baidu.com/personalcenter/thirdview/2631487
药品说明书信息抽取
image
选择环境 PaddlePaddle 2.5.2
image
启动时,可以选择环境,CPU的基础版不限时,每天有8个点的算力供学习使用
image

上传代码

项目地址:
https://aistudio.baidu.com/projectdetail/8126673?sUid=2631487&shared=1&ts=1719990517603
如果是 Fork 的项目, 直接跳至
模型微调

AI Studio 不能访问
gitee.com
,通过本地下载。再上传的方式操作
下载 V2.8:
https://gitee.com/paddlepaddle/PaddleNLP/branches
image
上传代码
image
解压代码
unzip PaddleNLP-release-2.8.zip
image

定制模型--训练

代码结构

model_zoo/uie
目录代码文件说明如下:

.
├── utils.py          # 数据处理工具
├── model.py          # 模型组网脚本
├── doccano.py        # 数据标注脚本 => 下面数据转换时会用到
├── doccano.md        # 数据标注文档
├── finetune.py       # 模型微调、压缩脚本
├── evaluate.py       # 模型评估脚本
└── README.md

数据标注

详细过程参考
数据标注工具 doccano | 命名实体识别(Named Entity Recognition,简称NER)

准备语料库

准备语料库、每一行为一条待标注文本,示例:corpus.txt

布洛芬分散片,口服或加水分散后服用。用于成人及12岁以上儿童,推荐剂里为一次0.2~0.4(1~2片)一日3次,或遵医嘱
白加黑(氨酚伪麻美芬片Ⅱ氨麻苯美片),口服。一次1~2片,一日3次(早、中各1~2白片,夜晚1~2片黑片),儿童遵医嘱
氯雷他定片,口服,规格为10mg的氯雷他定片,通常成人及12岁以上儿童1天1次,1次1片
扶他林(双氯芬酸二乙胺乳胶剂),外用。按照痛处面积大小,使用本品适量,轻轻揉搓,使本品渗透皮肤,一日3-4次
七叶洋地黄双苷,外用。用于黄斑变性时,每日3次,每次1滴,滴入眼结膜囊内(近耳侧外眼角)

数据标注

定义标签
Demo简单的定了 "药品名称、通用名、规格、用法、用量、频次"
image
数据标注
在doccano平台上
,创建一个类型为
序列标注
的标注项目。
定义实体标签类别,上例中需要定义的实体标签有[ 药品名称、通用名、规格、用法、用量、频次 ]。
使用以上定义的标签开始标注数据,下面展示了一个doccano标注示例:
image

导出数据

标注完成后,在doccano平台上导出文件,并将其重命名为doccano_ext.json后,放入./data目录下
image
image

数据转换

doccano

在 AI Studio 环境中创建 data 目录,将
doccano_ext.json
放入data目录中
image
执行以下脚本进行数据转换,执行后会在./data目录下生成训练/验证/测试集文件。

python doccano.py \
    --doccano_file ./data/doccano_ext.json \
    --task_type ext \
    --save_dir ./data \
    --splits 0.8 0.2 0 \
    --schema_lang ch
# 执行后会在./data目录下生成训练/验证/测试集文件。
[2024-06-26 09:48:38,269] [    INFO] - Save 24 examples to ./data/train.txt.
[2024-06-26 09:48:38,269] [    INFO] - Save 5 examples to ./data/dev.txt.
[2024-06-26 09:48:38,269] [    INFO] - Save 0 examples to ./data/test.txt.

image

可配置参数说明:

  • doccano_file
    : 从doccano导出的数据标注文件。
  • save_dir
    : 训练数据的保存目录,默认存储在
    data
    目录下。
  • negative_ratio
    : 最大负例比例,该参数只对抽取类型任务有效,适当构造负例可提升模型效果。负例数量和实际的标签数量有关,最大负例数量 = negative_ratio * 正例数量。该参数只对训练集有效,默认为5。为了保证评估指标的准确性,验证集和测试集默认构造全负例。
  • splits
    : 划分数据集时训练集、验证集所占的比例。默认为[0.8, 0.1, 0.1]表示按照
    8:1:1
    的比例将数据划分为训练集、验证集和测试集。
  • task_type
    : 选择任务类型,可选有抽取和分类两种类型的任务。
  • options
    : 指定分类任务的类别标签,该参数只对分类类型任务有效。默认为["正向", "负向"]。
  • prompt_prefix
    : 声明分类任务的prompt前缀信息,该参数只对分类类型任务有效。默认为"情感倾向"。
  • is_shuffle
    : 是否对数据集进行随机打散,默认为True。
  • seed
    : 随机种子,默认为1000.
  • separator
    : 实体类别/评价维度与分类标签的分隔符,该参数只对实体/评价维度级分类任务有效。默认为"##"。
  • schema_lang
    : 选择schema的语言,可选有
    ch

    en
    。默认为
    ch
    ,英文数据集请选择
    en

备注:

  • 默认情况下
    doccano.py
    脚本会按照比例将数据划分为 train/dev/test 数据集
  • 每次执行
    doccano.py
    脚本,将会覆盖已有的同名数据文件
  • 在模型训练阶段我们推荐构造一些负例以提升模型效果,在数据转换阶段我们内置了这一功能。可通过
    negative_ratio
    控制自动构造的负样本比例;负样本数量 = negative_ratio * 正样本数量。
  • 对于从doccano导出的文件,默认文件中的每条数据都是经过人工正确标注的。

Label Studio

也可以通过数据标注平台
Label Studio
进行数据标注。
labelstudio2doccano.py
脚本,将 label studio 导出的 JSON 数据文件格式转换成 doccano 导出的数据文件格式,后续的数据转换与模型微调等操作不变。

python labelstudio2doccano.py --labelstudio_file label-studio.json

可配置参数说明:

  • labelstudio_file
    : label studio 的导出文件路径(仅支持 JSON 格式)。
  • doccano_file
    : doccano 格式的数据文件保存路径,默认为 "doccano_ext.jsonl"。
  • task_type
    : 任务类型,可选有抽取("ext")和分类("cls")两种类型的任务,默认为 "ext"。

模型微调

注意使用GPU环境
image
启用GPU 环境
image

aistudio@jupyter-2631487-8126673:~$ python -V
Python 3.10.10
aistudio@jupyter-2631487-8126673:~$ pip show paddlepaddle-gpu
Name: paddlepaddle-gpu
Version: 2.5.2
Summary: Parallel Distributed Deep Learning
Home-page: https://www.paddlepaddle.org.cn/
Author: 
Author-email: Paddle-better@baidu.com
License: Apache Software License
Location: /opt/conda/envs/python35-paddle120-env/lib/python3.10/site-packages
Requires: astor, decorator, httpx, numpy, opt-einsum, Pillow, protobuf
Required-by: 
aistudio@jupyter-2631487-8126673:~$ pip show paddlenlp
Name: paddlenlp
Version: 2.6.1.post0
Summary: Easy-to-use and powerful NLP library with Awesome model zoo, supporting wide-range of NLP tasks from research to industrial applications, including Neural Search, Question Answering, Information Extraction and Sentiment Analysis end-to-end system.
Home-page: https://github.com/PaddlePaddle/PaddleNLP
Author: PaddleNLP Team
Author-email: paddlenlp@baidu.com
License: Apache 2.0
Location: /opt/conda/envs/python35-paddle120-env/lib/python3.10/site-packages
Requires: aistudio-sdk, colorama, colorlog, datasets, dill, fastapi, Flask-Babel, huggingface-hub, jieba, jinja2, multiprocess, onnx, paddle2onnx, paddlefsl, protobuf, rich, safetensors, sentencepiece, seqeval, tool-helpers, tqdm, typer, uvicorn, visualdl
Required-by: paddlehub
aistudio@jupyter-2631487-8126673:~$ 

推荐使用
Trainer API
对模型进行微调。只需输入模型、数据集等就可以使用 Trainer API 高效快速地进行预训练、微调和模型压缩等任务,可以一键启动多卡训练、混合精度训练、梯度累积、断点重启、日志显示等功能,Trainer API 还针对训练过程的通用训练配置做了封装,比如:优化器、学习率调度等。
使用下面的命令,使用
uie-base
作为预训练模型进行模型微调,将微调后的模型保存至
$finetuned_model

单卡启动:
cd PaddleNLP-release-2.8/model_zoo/uie/

export finetuned_model=./checkpoint/model_best

python finetune.py  \
    --device gpu \
    --logging_steps 10 \
    --save_steps 100 \
    --eval_steps 100 \
    --seed 42 \
    --model_name_or_path uie-base \
    --output_dir $finetuned_model \
    --train_path data/train.txt \
    --dev_path data/dev.txt  \
    --per_device_eval_batch_size 16 \
    --per_device_train_batch_size  16 \
    --num_train_epochs 20 \
    --learning_rate 1e-5 \
    --label_names "start_positions" "end_positions" \
    --do_train \
    --do_eval \
    --do_export \
    --export_model_dir $finetuned_model \
    --overwrite_output_dir \
    --disable_tqdm True \
    --metric_for_best_model eval_f1 \
    --load_best_model_at_end  True \
    --save_total_limit 1

注意
:如果模型是跨语言模型 UIE-M,还需设置
--multilingual

可配置参数说明:

  • model_name_or_path
    :必须,进行 few shot 训练使用的预训练模型。可选择的有 "uie-base"、 "uie-medium", "uie-mini", "uie-micro", "uie-nano", "uie-m-base", "uie-m-large"。
  • multilingual
    :是否是跨语言模型,用 "uie-m-base", "uie-m-large" 等模型进微调得到的模型也是多语言模型,需要设置为 True;默认为 False。
  • output_dir
    :必须,模型训练或压缩后保存的模型目录;默认为
    None
  • device
    : 训练设备,可选择 'cpu'、'gpu' 、'npu'其中的一种;默认为 GPU 训练。
  • per_device_train_batch_size
    :训练集训练过程批处理大小,请结合显存情况进行调整,若出现显存不足,请适当调低这一参数;默认为 32。
  • per_device_eval_batch_size
    :开发集评测过程批处理大小,请结合显存情况进行调整,若出现显存不足,请适当调低这一参数;默认为 32。
  • learning_rate
    :训练最大学习率,UIE 推荐设置为 1e-5;默认值为3e-5。
  • num_train_epochs
    : 训练轮次,使用早停法时可以选择 100;默认为10。
  • logging_steps
    : 训练过程中日志打印的间隔 steps 数,默认100。
  • save_steps
    : 训练过程中保存模型 checkpoint 的间隔 steps 数,默认100。
  • seed
    :全局随机种子,默认为 42。
  • weight_decay
    :除了所有 bias 和 LayerNorm 权重之外,应用于所有层的权重衰减数值。可选;默认为 0.0;
  • do_train
    :是否进行微调训练,设置该参数表示进行微调训练,默认不设置。
  • do_eval
    :是否进行评估,设置该参数表示进行评估。

该示例代码中由于设置了参数
--do_eval
,因此在训练完会自动进行评估。
image

模型评估

通过运行以下命令进行模型评估:

python evaluate.py \
    --model_path ./checkpoint/model_best \
    --test_path ./data/dev.txt \
    --batch_size 16 \
    --max_seq_len 512

输出:

[2024-07-03 14:14:16,345] [    INFO] - Class Name: all_classes
[2024-07-03 14:14:16,345] [    INFO] - Evaluation Precision: 1.00000 | Recall: 0.80000 | F1: 0.88889

通过运行以下命令对 UIE-M 进行模型评估:

python evaluate.py \
    --model_path ./checkpoint/model_best \
    --test_path ./data/dev.txt \
    --batch_size 16 \
    --max_seq_len 512 \
    --multilingual

评估方式说明:采用单阶段评价的方式,即关系抽取、事件抽取等需要分阶段预测的任务对每一阶段的预测结果进行分别评价。验证/测试集默认会利用同一层级的所有标签来构造出全部负例。

可开启
debug
模式对每个正例类别分别进行评估,该模式仅用于模型调试:

python evaluate.py \
    --model_path ./checkpoint/model_best \
    --test_path ./data/dev.txt \
    --debug

输出:

[2024-07-03 14:15:53,892] [    INFO] - -----------------------------
[2024-07-03 14:15:53,892] [    INFO] - Class Name: 通用名
[2024-07-03 14:15:53,892] [    INFO] - Evaluation Precision: 0.00000 | Recall: 0.00000 | F1: 0.00000
[2024-07-03 14:15:53,922] [    INFO] - -----------------------------
[2024-07-03 14:15:53,922] [    INFO] - Class Name: 药品名称
[2024-07-03 14:15:53,922] [    INFO] - Evaluation Precision: 1.00000 | Recall: 1.00000 | F1: 1.00000
[2024-07-03 14:15:54,039] [    INFO] - -----------------------------
[2024-07-03 14:15:54,039] [    INFO] - Class Name: 用法
[2024-07-03 14:15:54,039] [    INFO] - Evaluation Precision: 1.00000 | Recall: 1.00000 | F1: 1.00000
[2024-07-03 14:15:54,065] [    INFO] - -----------------------------
[2024-07-03 14:15:54,065] [    INFO] - Class Name: 用量
[2024-07-03 14:15:54,065] [    INFO] - Evaluation Precision: 1.00000 | Recall: 1.00000 | F1: 1.00000
[2024-07-03 14:15:54,091] [    INFO] - -----------------------------
[2024-07-03 14:15:54,091] [    INFO] - Class Name: 频次
[2024-07-03 14:15:54,091] [    INFO] - Evaluation Precision: 1.00000 | Recall: 1.00000 | F1: 1.00000

可配置参数说明:

  • model_path
    : 进行评估的模型文件夹路径,路径下需包含模型权重文件
    model_state.pdparams
    及配置文件
    model_config.json
  • test_path
    : 进行评估的测试集文件。
  • batch_size
    : 批处理大小,请结合机器情况进行调整,默认为16。
  • max_seq_len
    : 文本最大切分长度,输入超过最大长度时会对输入文本进行自动切分,默认为512。
  • debug
    : 是否开启debug模式对每个正例类别分别进行评估,该模式仅用于模型调试,默认关闭。
  • multilingual
    : 是否是跨语言模型,默认关闭。
  • schema_lang
    : 选择schema的语言,可选有
    ch

    en
    。默认为
    ch
    ,英文数据集请选择
    en

image

定制模型--预测

在 uie 目录下创建测试文件
drug.py
(也可直接在终端命令行中敲,个人比较喜欢用文件的方式执行)

from pprint import pprint
from paddlenlp import Taskflow

schema = ['药品名称','用法','用量','频次']

# 设定抽取目标和定制化模型权重路径
my_ie = Taskflow("information_extraction", schema=schema, task_path='./checkpoint/model_best')
pprint(my_ie("布洛芬分散片,口服或加水分散后服用。用于成人及12岁以上儿童,推荐剂里为一次0.2~0.4(1~2片)一日3次,或遵医嘱"))

# 不在语料库中的药品信息
pprint(my_ie("头孢克肟分散片,可用温开水融化后服用,或直接吞服。饭前饭后服用均可。成人及体重30公斤以上儿童:口服,每次50~100mg,每日2次;"))


注意到
drug.py
所在的目录
uie
下运行

aistudio@jupyter-2631487-8126673:~/PaddleNLP-release-2.8/model_zoo/uie$ python drug.py

输出

[{'用法': [{'end': 9,
          'probability': 0.9967116760425654,
          'start': 7,
          'text': '口服'}],
  '用量': [{'end': 50,
          'probability': 0.9942849459419811,
          'start': 46,
          'text': '1~2片'}],
  '药品名称': [{'end': 6,
            'probability': 0.9993706125082298,
            'start': 0,
            'text': '布洛芬分散片'}],
  '频次': [{'end': 55,
          'probability': 0.993725564192772,
          'start': 51,
          'text': '一日3次'}]}]
[{'用法': [{'end': 50,
          'probability': 0.9894656717712884,
          'start': 48,
          'text': '口服'},
         {'end': 24,
          'probability': 0.468091403198585,
          'start': 20,
          'text': '直接吞服'}],
  '用量': [{'end': 61,
          'probability': 0.7317206007179742,
          'start': 53,
          'text': '50~100mg'}],
  '药品名称': [{'end': 7,
            'probability': 0.999166781489965,
            'start': 0,
            'text': '头孢克肟分散片'}],
  '频次': [{'end': 66,
          'probability': 0.9919675951550744,
          'start': 62,
          'text': '每日2次'}]}]

效果

image
项目地址:
药品说明书信息抽取:
https://aistudio.baidu.com/projectdetail/8126673?sUid=2631487&shared=1&ts=1719990517603

交易流程

比特币的交易流程涉及多个步骤和参与方,包括发送方、接收方、矿工和比特币网络中的节点。以下是比特币交易的详细流程:

  1. 创建交易
    :
    • 生成比特币地址:比特币地址是一个由公钥生成的字符串,用于接收比特币。每个用户在创建比特币钱包时,都会生成一对公钥和私钥。公钥通过哈希算法生成比特币地址,私钥用于签名交易。
    • 创建交易信息:交易信息包括以下几个部分:
      • 输入(Inputs)
        :包含发送方的比特币地址和之前交易的输出(即发送方要花费的比特币)。
      • 输出(Outputs)
        :包含接收方的比特币地址和发送的比特币数量。
      • 交易额外信息
        :包括交易费等。
  2. 签名交易
    :发送方使用私钥对交易信息进行签名。签名的目的是证明交易的合法性和发送方对比特币的所有权。签名后的交易信息会被广播到比特币网络。
  3. 广播交易
    : 签名后的交易信息通过比特币网络广播到其他节点。每个节点都会验证交易的合法性,包括检查签名是否有效、输入是否未被双花(即同一笔比特币是否未被多次使用)等。
  4. 矿工打包交易
    : 矿工从比特币网络中收集未确认的交易,并将这些交易打包到一个新的区块中。矿工会优先选择交易费较高的交易进行打包。
  5. 工作量证明(PoW)
    : 矿工需要通过工作量证明(PoW)算法来解决一个复杂的数学问题。具体来说,矿工需要找到一个随机数(Nonce),使得整个区块的哈希值满足目标难度。这个过程称为挖矿。
  6. 广播新区块
    : 一旦矿工找到符合条件的哈希值,他们会将新区块广播到比特币网络。其他节点会验证新区块的合法性,包括检查哈希值是否符合目标难度、交易的合法性等。
  7. 区块链更新
    : 验证通过后,新区块会被添加到区块链中,成为区块链的最新部分。此时,交易被确认并记录在区块链中。
  8. 交易确认
    : 交易被打包到区块中并添加到区块链后,交易会获得一次确认。随着新区块的不断添加,交易会获得更多的确认。一般来说,6次确认被认为是安全的,意味着交易已经被多个区块确认,难以被篡改。
  9. 接收方验证交易
    : 接收方可以通过比特币网络查询交易状态,确认交易是否被记录在区块链中以及获得的确认次数。一旦交易获得足够的确认,接收方可以认为交易是安全的,已成功接收比特币。

广播交易

以下是一个比特币交易广播的示例流程图:

graph TD
A[用户创建并签名交易] --> B[发送交易到本地节点]
B --> C[本地节点验证交易]
C --> D[广播交易到邻近节点]
D --> E[邻近节点验证并广播交易]
E --> F[交易在网络中传播]
F --> G[交易进入内存池]
G --> H[矿工选择并打包交易]
H --> I[广播新区块]
I --> J[区块链更新]

广播过程涉及到
P2P网络

Gossip协议

  • P2P网络
    :比特币网络是一个P2P网络,由许多节点组成。每个节点都连接到其他节点,并通过P2P协议与其他节点交换信息。节点之间可以相互通信,交换交易、区块等信息。
  • Gossip协议
    :比特币网络使用一种称为Gossip协议的传播机制。每个节点会将接收到的新交易信息传递给它的所有邻近节点,邻近节点再继续传递,直到整个网络中的所有节点都接收到该交易信息。

交易验证过程包括

  • 检查输入的未花费交易输出(UTXO)
    :确保交易输入引用的比特币尚未被花费。
  • 签名验证
    :使用发送方的公钥验证交易的签名,确保发送方对交易输入的比特币拥有所有权。
  • 双花检查
    :确保交易输入的比特币未被其他交易使用。
  • 格式检查
    :确保交易的格式符合比特币协议的规范。


孟斯特

声明:本作品采用
署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0)
进行许可,使用时请注明出处。
Author:
mengbin
blog:
mengbin
Github:
mengbin92
cnblogs:
恋水无意
腾讯云开发者社区:
孟斯特