探索图像检索:从理论到实战的应用

一、引言

在当今数字化时代,图像成为了最直观、最丰富的信息载体之一。从社交媒体到电子商务平台,从云存储服务到内容发现应用,图像内容无处不在,它们的快速增长与管理已成为当代科技领域的一大挑战。在这个背景下,图像检索技术的发展与应用变得尤为重要。图像检索,即通过特定图像或图像特征,在大型数据库中查找并获取相关图像的技术,已成为智能信息检索领域的核心组成部分。

与传统的文本检索相比,图像检索面临着更多的挑战。图像的高维特性和视觉内容的多样性使得从海量图像数据中快速准确地提取信息成为一项复杂任务。这不仅要求算法能够处理高维数据,还需要具备理解图像内容和上下文的能力。为应对这些挑战,近年来,深度学习技术在图像检索领域的应用迅速发展。尤其是卷积神经网络(CNN)的应用,大幅提高了图像特征提取的准确性和效率。

在实际应用中,图像检索技术已被广泛应用于各种主流APP中。例如,在电子商务平台,用户可以通过上传商品图片来搜索相似商品;在社交媒体上,图像检索技术帮助用户发现和探索感兴趣的视觉内容;在云存储服务中,它则用于帮助用户组织和查找存储的图片。每一个应用场景都对图像检索技术提出了不同的需求和挑战,推动了这一领域的不断进步和创新。

本文旨在深入探讨图像检索技术及其在主流APP中的应用。我们将分析图像检索的关键技术,包括特征提取、相似度计算和索引技术等。同时,本文也将展示图像检索在不同应用场景下的具体实现方法,包括技术挑战和解决方案。此外,文章还将探讨图像检索技术面临的主要挑战和未来的发展趋势,旨在为AI领域的专业人士提供一份深入且全面的技术参考。

二、图像检索技术概述

图像检索技术,作为信息检索领域的一个重要分支,是指使用图像作为查询输入,在大型图像数据库中查找并检索出相关图像的技术。这项技术对于处理和管理海量图像数据至关重要,尤其在数字化和网络化日益发展的今天,它的应用价值日益凸显。

图像检索的基本概念

图像检索通常分为两大类:基于内容的图像检索(Content-Based Image Retrieval, CBIR)和基于文本的图像检索。基于内容的检索直接利用图像内容来进行检索,如颜色、纹理、形状或者更复杂的模式识别特征,而基于文本的检索则依赖于图像旁的文字描述或标签。在实际应用中,两者往往结合使用,但CBIR因其直接针对图像内容本身,更具有挑战性和创新性。

图像检索与文本检索的区别

图像检索与传统的文本检索在多个方面存在根本差异。图像数据的高维度和非结构化特性,使得直接应用文本检索的方法变得不可行。图像缺乏明确的“词汇”,因此需要通过特征提取等方法来“理解”图像内容。此外,图像的语义理解比文字更为复杂,同一图像在不同上下文中可能具有不同的含义,这增加了图像检索技术的难度。

特征提取技术

图像检索的核心在于如何有效地提取和利用图像特征。传统方法包括颜色直方图、纹理特征、形状特征等,但这些方法往往只能捕捉到图像的低级特征。随着深度学习的发展,尤其是卷积神经网络(CNN)的广泛应用,图像特征提取有了质的飞跃。CNN能够自动学习并提取图像的高级特征,这些特征更接近于人类对图像的视觉感知,大大提高了图像检索的准确性和效率。

相似度计算

在提取了图像特征之后,下一步是如何计算这些特征之间的相似度。这通常涉及到将图像特征转换为向量,并在向量空间中定义相似度度量方法。常见的相似度计算方法包括欧氏距离、余弦相似性等。这些方法各有优劣,选择哪一种取决于特定的应用场景和需求。

索引技术

随着图像数据库的不断扩大,如何快速有效地在海量数据中检索出相关图像成为了一大挑战。索引技术的作用就是提高检索效率,常见的索引方法包括哈希索引、树形索引(如K-D树)等。这些方法可以大幅减少在大规模数据集中进行全面搜索的需要,从而加快检索速度。

三、图像检索技术代码示例

接下来,我们将通过Python和PyTorch展示图像检索中的关键步骤:图像特征提取、相似度计算和索引技术。这将帮助我们更深入地理解图像检索技术的核心机制。

图像特征提取示例

在图像检索中,特征提取是获取图像的关键信息的首要步骤。卷积神经网络(CNN)由于其强大的图像处理能力,在这一步骤中扮演着重要的角色。以下是一个使用PyTorch实现的简单CNN特征提取的示例。

import torch
import torchvision.models as models
from torchvision import transforms
from PIL import Image

# 加载预训练的ResNet模型
model = models.resnet18(pretrained=True)
model.eval()

# 图像预处理
preprocess = transforms.Compose([
    transforms.Resize(256),
    transforms.CenterCrop(224),
    transforms.ToTensor(),
    transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),
])

# 加载图像并进行预处理
img = Image.open("path_to_image.jpg")
img_t = preprocess(img)
batch_t = torch.unsqueeze(img_t, 0)

# 使用模型提取特征
with torch.no_grad():
    features = model(batch_t)

# 输出特征向量
print(features)

这个示例展示了如何使用一个预训练的ResNet模型来提取图像特征。首先进行必要的图像预处理,然后将处理后的图像通过模型获得特征向量。

相似度计算

提取到的特征向量之后,需要计算它们之间的相似度。一个常见的方法是计算两个特征向量之间的余弦相似度:

from sklearn.metrics.pairwise import cosine_similarity

# 假设features_a和features_b是两个特征向量
similarity = cosine_similarity(features_a, features_b)
print(similarity)

余弦相似度衡量了两个向量在方向上的相似程度,它在-1到1之间,值越接近1表示相似度越高。

索引技术

在实际应用中,为了提高在大型数据库中的检索效率,通常需要对特征向量进行索引。哈希索引是一种常用的技术,它将高维特征向量映射到低维的哈希码中:

# 这里仅作为索引技术的概念示例
def hash_function(vector):
    # 实现一个简单的哈希函数
    hash_code = some_hashing_algorithm(vector)
    return hash_code

# 对特征向量进行哈希处理
hash_code = hash_function(features)

在实际应用中,哈希函数的选择和设计是一个重要的研究领域,关系到索引的效率和准确性。

四、图像搜索流程架构

图像搜索,作为人工智能和计算机视觉领域的一个重要应用,其流程架构集成了多项先进技术。本节将深入探讨这一流程架构的每个关键环节,从数据采集与预处理开始,到特征提取,再到相似度计算与排名,最后是结果的呈现与优化。

数据采集与预处理

图像搜索的第一步是数据采集。这通常涉及到从不同的来源收集图像数据,包括在线数据库、社交媒体平台、专业图库等。采集的图像数据可能包含各种格式和质量,因此需要进行预处理以确保数据的一致性和质量。

预处理的步骤包括但不限于:

  • 图像格式转换:统一图像格式(如JPEG,PNG等)。
  • 图像大小调整:调整图像尺寸以满足后续处理的需要。
  • 颜色空间转换:例如,从RGB转换到灰度,以便于某些特定的特征提取方法。
  • 图像增强:提高图像质量,如对比度增强、噪声去除等。
  • 标注与分类:对图像进行标签标注或分类,便于后续的索引和检索。

特征提取

特征提取是图像搜索的核心环节。此步骤的目标是从原始图像中提取出能够代表其内容和特性的特征。这些特征应当具有良好的区分度和鲁棒性,以适应不同的搜索需求。

特征提取的技术主要包括:

  • 局部特征提取:提取图像中的关键点和局部描述符,如SIFT(尺度不变特征变换)、SURF(加速鲁棒特征)等。
  • 全局特征提取:提取图像的整体特征,如颜色直方图、纹理特征等。
  • 深度学习方法:利用CNN等深度学习模型提取图像的深层特征。这类特征通常具有更高的抽象级别,能够捕捉到图像的高级语义信息。

相似度计算与排名

特征提取之后,下一步是计算查询图像与数据库中图像特征的相似度。这一步骤决定了搜索结果的准确性和相关性。

相似度计算的关键点包括:

  • 相似度度量方法的选择:常见的方法有欧氏距离、曼哈顿距离、余弦相似度等。选择哪种方法取决于特征的类型和搜索的具体需求。
  • 相似度排名:根据计算出的相似度对搜索结果进行排名,以便用户能够快速找到最相关的图像。
  • 相似度聚类:在某些应用场景中,可能还需要对相似的图像进行聚类,以提供更加组织化的搜索结果。

结果呈现与优化

最后一个环节是搜索结果的呈现与优化。这不仅包括将搜索结果以用户友好的方式展示出来,还包括对整个图像搜索系统的性能进行优化。

结果呈现与优化的要点包括:

  • 用户界面设计:设计直观、易用的用户界面,使用户能够方便地进行搜索并浏览结果。
  • 反馈机制:提供用户反馈机制,如点击率、用户评价等,以优化搜索算法和提高结果的相关性。
  • 性能优化:对搜索系统的响应时间、准确率等进行优化,以提升用户体验。

五、实际应用

图像检索在电子商务领域的应用

电子商务平台,如阿里巴巴和亚马逊,利用图像检索技术为用户提供了一种直观且高效的商品搜索方式。在这些平台上,用户可以上传商品的图片,系统将基于这个图片返回相似商品的搜索结果。这种方法特别适合于当用户无法准确描述他们所寻找的商品时的情况。

技术实现

  • 特征提取:使用深度学习模型提取上传图片的特征。
  • 相似度计算:计算上传图片特征与商品数据库中的图片特征之间的相似度。
  • 结果排序:根据相似度对搜索结果进行排序,并呈现给用户。

图像检索在社交媒体中的应用

社交媒体平台,如Instagram和微博,使用图像检索技术帮助用户发现和探索感兴趣的内容。用户可以通过上传图片来查找类似的内容或相关的用户。

技术实现

  • 视觉内容分析:分析上传的图片,提取关键视觉特征。
  • 语义理解:使用高级深度学习模型理解图片的语义内容,如场景、对象和活动。
  • 个性化推荐:根据用户的历史行为和偏好,提供个性化的内容推荐。

图像检索在云存储服务中的应用

云存储服务,如谷歌云和百度网盘,利用图像检索技术帮助用户管理和检索存储的图片。用户可以通过搜索特定的视觉内容来快速找到他们需要的图片。

技术实现

  • 图像标签生成:自动为存储的图片生成标签,便于搜索和分类。
  • 快速索引:建立高效的图像索引系统,以实现快速检索。
  • 智能分类:利用机器学习技术对图片进行智能分类,提高管理效率。

本文出自:https://www.cnblogs.com/chcong/p/4319903.html

FOSS List梳理,MySQL (mysql-connector-j) 是GPL

一、FOSS List 梳理
别说多痛苦了,反正就是非常繁琐的工作

那什么是FOSS 以及 FOSS List?
FOSS(Free and Open-Source Software)是指自由和开放源代码软件。
FOSS List是一个用于收集和整理开源软件的Excel
FOSS List列出了各种开源软件的名称、版本、授权协议等信息。

二、MySQL开源组件又是什么呢?
MySQL是一个流行的关系型数据库管理系统(RDBMS),由瑞典公司MySQL AB开发,后来被Sun Microsystems收购。MySQL的源代码是使用C和C++编写的,并通过GPL许可证发布。这意味着任何人都可以获取并修改MySQL的源代码,但同时必须以相同的许可证发布修改后的版本。

MySQL(mysql-connector-j)是遵循GPL(GNU General Public License)许可证的开源软件。
MySQL (mysql-connector-j) 是使用 GPL 许可证进行发布的。这意味着,如果你使用 MySQL (mysql-connector-j),你需要遵守 GPL 许可证的条款和条件,包括发布你的软件的源代码。如果你不想遵守 GPL 许可证,那么你可能需要寻找其他数据库选项。

MySQL的开源协议基于GPL或Commercial License,任何公司都可以免费使用,不允许修改后和衍生的代码做为闭源的商业软件发布和销售,MySQL的版权在甲骨文手中,甲骨文可以推出其商业闭源版本

mysql-connector-j是MySQL的官方Java驱动程序,用于在Java应用程序中使用MySQL数据库。它也是使用GPL许可证发布的,因此遵循相同的规则和要求。这意味着任何接受mysql-connector-j许可证的Java应用程序也必须以自由软件的形式发布。

需要注意的是,虽然MySQL和mysql-connector-j是遵循GPL许可证的开源软件,但这并不意味着它们必须与其他软件一起使用或链接。因此,如果您使用MySQL或mysql-connector-j作为您自己的应用程序的一部分,您可以选择使用不同的许可证来发布您的应用程序,只要不违反GPL许可证的规定即可。

三、 GPL 许可证有什么特点?
GNU通用公共许可证简称为GPL,公司的AD+DD文档中对 Licenses(授权)章节重点写了一句,GNU公共许可证不得用于公司内的软件开发。

GPL的出发点是代码的开源/免费使用和引用/修改/衍生代码的开源/免费使用,但不允许修改后和衍生的代码做为闭源的商业软件发布和销售。

GPL的“传染性”在于:只要在一个软件中使用(”使用”指类库引用,修改后的代码或者衍生代码)GPL 协议的产品,则该软件产品必须也采用GPL协议,既必须也是开源和免费。我们所熟悉的Linux就是采用了GPL。

GPL是一种广泛使用的开源许可证,适用于许多自由软件项目。根据GPL许可证,任何接受该许可证的软件必须以自由软件的形式发布,这意味着任何人都可以自由地获取、修改和分发该软件。

本文出自:https://blog.csdn.net/wstever/article/details/133268860

Java 关键字:synchronized详解

Java中的synchronized关键字用于在多线程环境下确保数据同步。它可以用来修饰方法和代码块

当一个线程访问一个对象的synchronized方法或代码块时,其他线程将无法访问该对象的其他synchronized方法或代码块。这样可以确保在同一时间只有一个线程能够执行该代码块或方法,避免了多线程环境下的数据不一致问题,例如:

public class SynchronizedExample {
    private int count = 0;
    public synchronized void increment() {
        count++;
    }
}

在上面的代码中,increment()方法是一个synchronized方法。当多个线程访问这个方法时,只有一个线程能够执行该方法的代码,其他线程将被阻塞。

synchronized关键字也可以用来修饰代码块,如:

public void increment() {
    synchronized(this) {
        count++;
    }
}

在上面的代码中,synchronized关键字修饰的是一个代码块,并且锁对象是当前对象(this)

注意:synchronized关键字会导致线程上下文切换和资源竞争,所以在使用时要注意性能问题

 

源码解析

底层实现是通过 Java 虚拟机(JVM)的对象头和监视器锁机制实现的

 

具体来说,当一个线程访问一个对象的 synchronized 方法或代码块时,它会试图获取该对象的监视器锁。如果该锁未被其他线程占用,该线程将获得该锁并执行代码;如果该锁被其他线程占用,该线程将进入阻塞状态,等待获取该锁

 

synchronized 是Java中用于实现同步的关键字,它在底层通过监视器锁(Monitor)来实现。下面是synchronized的源码解析:

 

在Java中,每个对象都有一个与之关联的监视器锁,也称为内置锁或对象锁。当线程进入一个synchronized方法或代码块时,它会尝试获取该对象的监视器锁。如果锁没有被其他线程占用,则该线程获得锁并开始执行代码;如果锁已经被其他线程占用,则该线程将被阻塞,直到锁被释放。

 

在Java虚拟机中,每个对象头中都包含一部分用于实现synchronized的相关信息。这些信息包括:

 

mark word:用于存储对象的标记信息,包括锁的状态。

Klass pointer:指向对象的类元数据,包括synchronized的相关信息。

monitor:与对象关联的监视器,它记录了当前占用锁的线程、等待锁的线程队列等。

当一个线程尝试获取一个对象的锁时,虚拟机会检查对象头中的标记信息。如果对象的锁状态为无锁状态,即未被其他线程占用,则该线程可以获取锁,并将标记信息设置为锁定状态。如果对象的锁状态为已锁定,并且当前线程是锁的所有者,则该线程可以继续执行代码。如果对象的锁状态为已锁定,并且当前线程不是锁的所有者,则该线程将被放入等待队列中,进入阻塞状态。

 

当持有锁的线程执行完synchronized方法或代码块后,它会释放锁,即将对象头中的锁状态置为无锁状态,并唤醒等待队列中的一个线程,使其获取锁并继续执行。

 

需要注意的是,synchronized关键字可以修饰方法和代码块。在方法上修饰的synchronized表示对整个方法进行同步,而在代码块上修饰的synchronized表示对该代码块进行同步,使用的锁对象通常是方法所属对象或指定的对象。

 

总结起来,通过监视器锁的机制,Java的synchronized能够保证同一时刻只有一个线程访问同步代码块或方法,避免了多线程的数据竞争和并发问题。

 

这里给出一份简化的 synchronized 关键字的源码:

public void synchronized method() {
    // 加锁
    Monitor.enter(this);
    try {
        // 同步代码块
    } finally {
        // 释放锁
        Monitor.exit(this);
    }
}

在这份代码中,方法通过调用 Monitor.enter 方法获取当前对象的监视器锁,并在 finally 块中调用 Monitor.exit 方法释放该锁。因此,在 synchronized 方法内部的代码可以保证在任意时刻只有一个线程可以访问

 

常见面试题

synchronized 方法和 synchronized 块的区别是什么?

作用范围:synchronized 方法将整个方法体作为同步区块,而 synchronized 块可以将任意代码块作为同步区块

锁的对象:synchronized 方法锁定的是整个对象,而 synchronized 块锁定的是在括号内指定的对象

可控性:synchronized 方法的同步粒度比较大,不够灵活;而 synchronized 块可以更灵活地控制同步代码块的大小

综上所述,在确定同步粒度时,通常使用 synchronized 块比使用 synchronized 方法更灵活,但是如果整个方法都需要同步,使用 synchronized 方法会更加简单易懂

什么情况下可以使用 synchronized 关键字?

synchronized 关键字可以用于在多线程环境下保证方法或代码块的原子性。具体来说,如果一个线程正在执行同步方法或代码块,则其他线程将无法访问该方法或代码块

常见情况包括:

当多个线程访问共享资源时,可以使用 synchronized 关键字保证线程的安全

在访问共享变量时,需要对其进行同步控制

在线程通信中,可以使用 synchronized 关键字保证线程之间的同步通信

synchronized 关键字的性能开销如何?

synchronized 关键字的使用会带来一些性能开销,因为它需要在多个线程之间进行同步。当线程访问同步代码块时,它必须获得锁,这会增加额外的开销。如果同步代码块执行时间过长,其他线程将一直等待,进而降低程序的性能。

因此,应该尽量避免在高并发情况下使用 synchronized,或者使用其他的并发控制机制,如 java.util.concurrent 包中的锁和原子操作类等。

synchronized 关键字如何实现可重入?

“可重入” 指的是同一线程可以多次获取同一个锁。例如,当线程 A 进入一个同步块时,如果它再次试图进入该块,则可以再次获取锁,而不会发生死锁

在 Java 中,synchronized 关键字可以实现可重入,原因如下:

synchronized 关键字使用对象监视器锁来实现同步。

对象监视器锁是基于线程的,并且每个线程有一个独立的计数器,用于跟踪它在当前对象上获取的锁的数量。

当线程试图获取锁时,如果它已经拥有该锁,则计数器将递增。

当线程退出同步块时,计数器将递减。

只有当计数器为零时,该线程才会释放锁。

因此,如果一个线程在同一对象上多次进入同步块,它将多次获得该锁,并在退出该块时多次释放该锁。因此,synchronized 关键字是可重入的。

synchronized 关键字与 lock 机制的比较?

synchronized 关键字和 Lock 机制都是用来保证线程同步的方法。但是它们有一些明显的差异:

灵活性:Lock 机制比 synchronized 关键字更灵活,因为它提供了更多的锁定操作,例如可以实现公平锁和非公平锁,还可以实现读写锁。

可中断性:Lock 机制可以中断一个线程的等待,而 synchronized 关键字不能。

可重入性:synchronized 关键字是自动可重入的,而 Lock 机制必须手动实现。

性能:如果比较的是相同的锁定操作,synchronized 关键字通常比 Lock 机制更快,因为它是内置的。

总体而言,在简单的同步情况下,synchronized 关键字更方便,但是在需要更多灵活性的情况下,Lock 机制可能是一个更好的选择。

本文出自:https://blog.csdn.net/qq_54796785/article/details/133913211

MySQL数据同步到 Redis 缓存的几种方法

1 Mysql查完数据,再同步写入到Redis中
缺点1:会对接口造成延迟,因为同步写入redis本身就有延迟,并且还要做重试,如果redis写入失败,还需要重试,那就更费时间了。

缺点2:不解耦,如果redis崩了,那直接卡线程了

缺点3:如果人为该数据库,那就没法同步了, 除非再人为删除对应的Redis,但删除Redis这个过程也有个时间差

2 Mysql查完数据,通过发送MQ,在消费者线程去同步Redis
缺点1:多了层MQ,也就是会有很大的概率导致同步延迟问题.

缺点2:要对MQ的可用性做预防

缺点3:如果人为该数据库,那就没法同步了

优点1:可以大幅减少接口的延迟返回的问题

优点2:MQ本身有重试机制,无需人工去写重试代码

优点3:解耦,把查询Mysql和同步Redis完全分离,互不干扰

3 订阅Mysql的Binlog文件(可借助Canal来进行)
CanalServer会伪装成MysqlServer从库,去订阅MysqlServer主库的Binlog文件

Canal启动的时候会配置对应的消息MQ(RabbitMQ, RocketMQ, Kafka), 监听到Binlog文件有变化是,会把变化的sql语句转换成json格式,并作为消息内容发送到MQ中

项目中只要监听对应MQ,就能拿到Binlog改动的内容,Json数据中有明确的操作类型(CURD), 以及对应的数据。把对应数据同步到redis即可

缺点1:canal订阅Binlog的整个操作过程是单线程的,所以面临超高并发的情况下,性能可能不太出色。当然可以部署多个Canal 与 多个消费者,但是要注意消息重复消费问题,做好幂等性校验

优点1:即使人为改数据库,也会监听到,并且也会同步

优点2:异步同步,不会对接口返回有格外延迟

4 延迟双删
在执行修改sql之前,先将redis的数据删除

执行更新sql

延迟一段时间

再次删除redis的数据

// 延迟双删伪代码
deleteRedisCache(key); // 删除redis缓存
updateMysqlSql(obj); // 更新mysql
Thread.sleep(100); // 延迟一段时间
deleteRedisCache(key); // 再次删除该key的缓存

缺点:这个延迟时间不好把控,到底延迟多久,这个很难去评估

扩展: 如果不使用延迟双删,仅仅是delete缓存,然后改mysql数据。只有这两步会出现什么问题呢?

5. 单个请求,单线程没问题,高并发多线程下会出问题

6. 如果Thread1线程要更新数据,此时Thread1线程把redis清理了

7. 此时Thread2线程来了,但Thread1还没有更新mysql完毕

8. Thread2查询redis肯定是null,此时Thread2就要查mysql了,然后再把查到的数据写到缓存

9. 由于Thread1还没来得及修改mysql数据,所以此时Thread2查出来的数据是【旧数据】,Thread2把旧数据又写入Redis 了

10. 此时Thread3线程来了,查询Redis发现有数据,则直接拿缓存数据了,此时【Thread3查出来的是旧数据】,直接带着旧数据返回了,这就是问题所在

11. 而延迟双删的第二次删除作用就是防止Thread2把旧数据又写入了,有了延迟双删,Thread3查询Redis的时候还是null,就会从mysql 去拿最新数据了

12. 所以正常的这个延迟时间,应该是Thread2查缓存到拿mysql数据,到再保存到redis这整个时间,作为Thread1的延迟时间,但是这个Thread2这个过程的时间会受到很多因素影响,因此很难断定究竟会是多久

5 延迟双写

// 延迟双写伪代码
updateMysqlSql(obj); // 更新mysql
addRedis(key); // 再次删除该key的缓存

上述代码缺陷;

高并发下,两条线程同时执行上面代码,并对mysql 修改,且修改内容不通,可能会导致Redis与Mysql数据不一致

T1线程执行完updateMysqlSql,释放了行锁,此时T2线程再执行updateMysqlSql 与 addRedis, 最后T1执行addRedis,这种情况会导致数据库改成了T2线程的数据,但Redis却是T1线程的数据

优化

// 完美延迟双写伪代码
开启事务
updateMysqlSql(obj); // 更新mysql
addRedis(key); // 再次删除该key的缓存
提交事务

上述代码改正:

把两句代码放到一个事务里面,只有T1执行完Mysql 与 Redis的时候,T2才能开始执行,就可以保证数据一致性。推荐使用分布式锁

双写缺点:Mysql 与 Redis是单线程的。性能方面不行,因此不推荐使用

6 总结
推荐使用Canal的方式,进行异步同步。其次是MQ方式

本文出自:https://blog.csdn.net/qq_37284798/article/details/129518953

提高代码质量的秘诀:类、方法、字段和包注释

JDK提供了一个很有用的工具,叫做javadoc,它可以由源文件生成一个HTML文档。如果在源代码中添加以特殊定界符/**开始的注释,那么你就可生成一个看上去具有专业水准的文档。Java文档注册可以提高代码的可读性和维护性。其中的关键是使用合适的注释,Java的注释有许多种类:

 

🍢 一、注释的插入

在Java中添加注释非常简单。只需要在代码前面加上两个斜线“//”,就可以在该行之后添加单行注释:

 

// 这是单行注释

你还可以将任何文本放在一个多行注释块内:

/*
 
● 多行注释。
 
●  在块注释中,可以分解成多段注释。

 */

🍣 二、类注释

/**
 
● 这是类的说明 */

其中,星号(*)后的内容是用于为Java文档工具生成类库文档所需的标记。它包含了类的一般描述和一些相关信息。这样的类注释使得您的代码复用更加容易。

🍤 三、方法注释

/**
 
这是方法的说明
@param p1 参数说明
@param p2 参数说明
@return 返回值说明
@exception 异常说明 */

这种注释中带有参数说明、返回值说明和异常说明,旨在帮助开发人员理解当前方法的目的和如何使用它。

🍥 四、字段注释

字段注释应在字段的定义之前进行,在格式上与变量声明类似:

/**
 
对于该字段的描述 */ 
private String fieldName;

这样可以对自身或者其他开发人员解释一个字段存在的意义,非常有利于代码理解和后期维护。

🥮 五、通用注释

标记 @since text 会建立一个 “since”(始于)条目。text(文本)可以是对引入这个特性的版本的描述。例如:@since 1.7.1。

 

@author name

这个标记将建立一个“author”(作者)条目,可以由多个@author标记,每个@author标记对应一个作者。并不是非得使用这个标记,你的版本控制系统能够更好地跟踪作者。

 

@version text

这个标记将建立一个“version”(版本)条目。这里的text可以是对当前版本的任何描述。

 

通过@see和@link标记,可以使用超链接,链接到javadoc文档的相关部分或外部文档。

🍡 六、包注释

要想产生包注释,就需要在每个包目录中添加一个单独的文件。可以由如下两个选择:

 

1、提供一个名为package-info.java的Java文件。这个文件必须包含一个初始的Javadoc注释,以/**和*/界定,后面是一个package语句。它不能包含更多的代码或注释。

 

2、提供一个名为package.html的HTML文件,抽取标记<body>…<body>之间的所有文本

/**
 
package-info 文件提供关于当前包的全局说明,
可以简要描述该包中提供的类、函数和语言结构以及为何这些元素会彼此相关联。 */

 

本文出自:https://blog.csdn.net/m0_61961937/article/details/131032213

【MySQL技术专题】「实战开发系列」一同探索一下数据库的加解密函数开发实战指南之AES系列

MySQL的加解密及压缩函数
许多加密和压缩函数返回结果可能包含任意字节值的字符串。如果要存储这些结果,请使用具有VARBINARY或BLOB二进制字符串数据类型的列。这避免了删除尾随空格或转换字符集可能改变数据值的潜在问题,例如使用非二进制字符串数据类型(CHAR、VARCHAR、TEXT)时可能发生的问题。

MySQL加解密函数
MySQL自带的加解密函数主要有以下:

一些加密函数返回 ASCII 字符字符串:MD5()、PASSWORD()、SHA()、SHA1()、SHA2()。它们的返回值为具有由character_set_connection和collation_connection系统确定的字符集和排序规则的字符串 变量。这是一个非二进制字符串,除非字符集为 。

如果应用程序存储来自返回十六进制字符串的函数(如 MD5() 或 SHA1() 的值) 数字,可以通过以下方式获得更有效的存储和比较 使用 UNHEX() 将十六进制表示形式转换为二进制,并将结果存储在 BINARY(N) 列中。

将十六进制字符串存储在 CHAR 列中的大小损失至少为两倍, 如果值存储在使用该字符集的列中,则最多 4 次(其中每个字符使用 <> 字节)。存储字符串也会导致比较速度变慢 因为值较大且需要采用字符集 将排序规则考虑在内。

ENCODE()、DECODE()

已在5.7.2版本弃用,目前仍可用,但将在后续版本中删除。

DES_ENCRYPT()、DES_DECRYPT()

AES_ENCRYPT()加密与AES_DECRYPT()解密
AES高级加密标准(英语:Advanced Encryption Standard,缩写:AES),在密码学中又称Rijndael加密法,是美国联邦政府采用的一种区块加密标准。

AES_ENCRYPT和AES_DECRYPT在MySQL中是进行加密了,如果你需要对MySQL某些字段进行加解密的话,使用MySQL的加解密函数可能比程序中处理更方便.

AES_ENCRYPT(‘密码’,‘钥匙’)
AES_DECRYPT(表的字段名字,‘钥匙’)
这个标准用来替代原先的DES,已经被多方分析且广为全世界所使用。

严格地说,AES和Rijndael加密法并不完全一样(虽然在实际应用中二者可以互换),因为Rijndael加密法可以支持更大范围的区块和密钥长度:AES的区块长度固定为128 比特,密钥长度则可以是128,192或256比特;而Rijndael使用的密钥和区块长度可以是32位的整数倍,以128位为下限,256比特为上限。包括AES-ECB,AES-CBC,AES-CTR,AES-OFB,AES-CFB

函数参数(MySQL版本小于等于5.7.6)
AES_ENCRYPT(str,key_str),其中str为待加密字符串,key_str为秘钥

AES_DECRYPT(crypt_str,key_str),其中crypt_str为已加密的二进制串,key_str为秘钥

已在5.7.6版本弃用,目前仍可用,但将在后续版本中删除。

AES_ENCRYPT()、AES_DECRYPT()

推荐使用这对加解密函数。aes_encrypt()和aes_decrypt()使用官方的aes(高级加密标准)算法(以前称为“rijndael”)实现数据的加密和解密。

加密后的二进制串长度可以通过下面公式计算:

16 * (trunc(string_length / 16) + 1)

函数参数(MySQL版本大于等于5.7.6)
函数参数
AES_ENCRYPT(str,key_str[,init_vector]),其中str为待加密字符串,key_str为秘钥,其中init_vector根据选择不同的块加密模式为可选项

AES_DECRYPT(crypt_str,key_str[,init_vector]),其中crypt_str为已加密的二进制串,key_str为秘钥,其中init_vector根据选择不同的块加密模式为可选项

st和key_str参数可以是任何长度,init_vector参数不得小于16个字符。可以通过block_encryption_mode参数,控制块加密模式,默认值为:aes-128-ecb。可配置的形式为:aes-keylen–mode。

keylen可配置为128, 192, 256

mode可配置为ECB, CBC, CFB1, CFB8, CFB128, OFB。

下表展示了不同mode是否需要init_vector参数。

默认的ECB模式不需要init_vector参数,用法与5.7.4以前相同。

修改块加密模式:

set block_encryption_mode=’aes-256-cbc’;

查看对应的加解密模式

show variables like ‘block%;

block_encryption_mode
此变量控制的块加密模式,基于块的算法,例如 AES。它会影响 AES_ENCRYPT() 和 AES_DECRYPT() 的加密。

block_encryption_mode需要格式中的值,其中 Keylen是关键位和模式的长度为加密模式。该值不区分大小写。允许的键值为 128、192 和 256. 允许的加密模式取决于 MySQL 是否 使用 OpenSSL 或 yaSSL 编译:aes-keylen-mode。

例如,此语句会导致 AES 加密 使用 256 位密钥长度和 CBC 模式的函数:

SET block_encryption_mode = ‘aes-256-cbc’;

尝试将block_encryption_mode设置为包含不受支持的密钥长度或模式的值不支持SSL库。

AES_ENCRYPT(str,key_str[,init_vector][,kdf_name][,salt][,info | iterations])

AES_ENCRYPT加密 字符串 str 使用键字符串 key_str,并返回二进制文件 包含加密输出的字符串。AES_DECRYPT() 使用密钥字符串key_str解密加密字符串crypt_str,并返回原始 纯文本字符串。如果任一函数参数为 ,则该函数返回 。如果 AES_DECRYPT检测到无效 数据或填充不正确,它将返回 . 但是,AES_DECRYPT() 有可能 返回非值(可能是垃圾) 如果输入数据或键无效。NULLNULLNULLNULL

使用AES_DECRYPT()的语句对于基于语句的复制。

AES_DECRYPT(crypt_str,key_str[,init_vector][,kdf_name][,salt][,info | iterations])

AES_ENCRYPT() 和 AES_DECRYPT() 允许控制 块加密模式。block_encryption_mode系统 变量控制基于块的加密模式 算法。其默认值为 ,表示加密 使用 128 位的密钥长度和 ECB 模式。有关说明 此变量的允许值,请参见第 5.1.7 节 “服务器系统变量”。这 可选init_vector参数为 用于为块加密提供初始化向量 需要它的模式。aes-128-ecb

本文出自:https://blog.csdn.net/l569590478/article/details/132187930

ADO 通过 GetString() 加速脚本

请使用 GetString() 方法来加速您的 ASP 脚本(来代替多行的 Response.Write)。

 

多行 Response.Write

下面的例子演示了在 HTML 表格中显示数据库查询的一种方法:

<html>
<body>

<%
set conn=Server.CreateObject("ADODB.Connection")
conn.Provider="Microsoft.Jet.OLEDB.4.0"
conn.Open "c:/webdata/northwind.mdb"

set rs = Server.CreateObject("ADODB.recordset")
rs.Open "SELECT Companyname, Contactname FROM Customers", conn
%>

<table border="1" width="100%">
<%do until rs.EOF%>
  <tr>
    <td><%Response.Write(rs.fields("Companyname"))%></td>
    <td><%Response.Write(rs.fields("Contactname"))%></td>
  </tr>
<%rs.MoveNext
loop%>
</table>

<%
rs.close
conn.close
set rs = Nothing
set conn = Nothing
%>

</body>
</html>

对于一个大型的查询来说,这样做会增加脚本的处理时间,这是由于服务器需要处理大量的 Response.Write 命令。

 

解决的办法是创建全部字符串,从 <table> 到 </table>,然后将其输出 – 只使用一次 Response.Write。

 

GetString() 方法

GetString() 方法使我们有能力仅使用一次 Response.Write,就可以显示所有的字符串。同时它甚至不需要 do..loop 代码以及条件测试来检查记录集是否处于 EOF。

 

语法

str = rs.GetString(format,rows,coldel,rowdel,nullexpr)

 

如需使用来自记录集的数据创建一个 HTML 表格,我们仅仅需要使用以上参数中的三个(所有的参数都是可选的):

 

coldel – 用作列分隔符的 HTML

rowdel – 用作行分隔符的 HTML

nullexpr – 当列为空时所使用的 HTML

注意:GetString() 方法是 ADO 2.0 的特性。您可从下面的地址下载 ADO 2.0:http://www.microsoft.com/data/download.htm

 

在下面的例子中,我们将使用 GetString() 方法,把记录集存为一个字符串:

 

实例

<html>
<body>
  
<%
set conn=Server.CreateObject("ADODB.Connection")
conn.Provider="Microsoft.Jet.OLEDB.4.0"
conn.Open "c:/webdata/northwind.mdb"
  
set rs = Server.CreateObject("ADODB.recordset")
rs.Open "SELECT Companyname, Contactname FROM Customers", conn
  
str=rs.GetString(,,"</td><td>","</td></tr><tr><td>","&nbsp;")
%>
  
<table border="1" width="100%">
  <tr>
    <td><%Response.Write(str)%></td>
  </tr>
</table>
  
<%
rs.close
conn.close
set rs = Nothing
set conn = Nothing
%>
</body>
</html> 

上面的变量 str 包含着由 SELECT 语句返回的所有列和行的一个字符串。在每列之间会出现 </td><td>,在每行之间会出现 </td></tr><tr><td>。这样,仅使用一次 Response.Write,我们就得到了需要的 HTML。

本文出自:https://blog.csdn.net/unbelievevc/article/details/133854601

Java 图片验证码需求分析

图片验证码

需求分析

连续因输错密码而登录失败时,记录其连续输错密码的累加次数;若在次数小于5时,用户输入正确的密码并成功登录,则次数被清零

连续5次因输错密码而登录失败后,系统弹框提示【您已连续5次输入错误的密码,暂时不允许登录,请10分钟后再次尝试登录】;点击提示框中的【确定】按钮,提示框被关闭

10分钟内再次尝试登录,则系统弹框提示【您已连续5次输入错误的密码,暂时不允许登录,7分43秒后可再次尝试登录】;点击提示框中的【确定】按钮,提示框被关闭;注:提示框中的剩余时间动态倒数至0分0秒

10分钟后,用户可再次尝试登录;此时,若用户在输错密码次数小于5次时成功登录,则其连续输错密码的次数、曾被锁定1次的信息被清空归零;反之,若用户再次连续5次输错密码,则系统弹框提示【您已连续10次输入错误的密码,账号已被锁定、不允许登录,请联系管理员解锁】;点击提示框中的【确定】按钮,提示框被关闭。此后,用户每次用该账号尝试登录时,均弹出此提示框。此时,在运营端,该用户详情页面中的【登录状态】已被自动切换为【锁定】。用户须主动联系莫族密运营人员,运营人员确认用户没有被盗号、遭遇网络攻击等风险后,主动将其【登录状态】置为【解锁】;此时,用户连续输错密码的次数、曾被锁定2次的信息被清空归零

用户登录时,须输入正确的【验证码】

若用户看不清,则可点击【看不清?换一张】字样,也可直接点击验证码部件,点击后自动刷新验证码

点击【登录】按钮后,【用户名】、【密码】、【验证码】这3项但凡有1项校验不通过,则登录失败,【用户名】、【密码】、【验证码】框中已录入的内容被清空,验证码自动刷新

点击【登录】按钮后,若【用户名】、【密码】校验通过,唯独【验证码】校验不通过,则登录失败的系统提示内容为【验证码错误,请重新录入验证码】。同时验证码自动刷新。

【验证码】的有效时间为60秒,超过之后则失效,但不自动刷新。失效之后若录入正确的【用户名】【密码】同时录入页面上已失效的【验证码】,则登录失败,且登录失败的系统提示内容为【验证码错误,请重新录入验证码】,同时验证码自动刷新

实施

验证码接口 | 请求头方式传递

依赖导入

<!-- 添加图形验证码依赖 -->
       <dependency>
           <groupId>cn.hutool</groupId>
           <artifactId>hutool-captcha</artifactId>
           <version>5.8.5</version>
       </dependency>

图片验证码接口编写

/**
     * 生成验证码图片
     * @return
     */
    @ApiOperation("获取图形验证码")
    @GetMapping("/identifyImage")
    public Result<String> identifyImage(HttpServletResponse response,
                                        @ApiParam(value = "图形验证码id,无值:生成验证码,有值:刷新验证码")
                                        @RequestParam(name = "codeId", required = false) String codeId) throws IOException {
        // 创建验证码,设置宽、高、长度、干扰线数量
        LineCaptcha lineCaptcha = CaptchaUtil.createLineCaptcha(200, 90, 4, 100);
        // 获取验证码字符串,赋值code
        String code = lineCaptcha.getCode();
        if (codeId == null) {
            // IdWorker.getId():IdWorker工具类生成唯一ID,并转换成String类型
            codeId = String.valueOf(IdWorker.getId());
            // 将codeId、code.toUpperCase()、过期时间60秒:存储入Redis中
            // code.toUpperCase():code装换成大写形式存储
            redisOps.set(codeId,code.toUpperCase(),60);
        } else {
            redisOps.set(codeId,code.toUpperCase(),60);
        }
        // 将图片验证码codeId设置请求头中
        response.setHeader("codeId", codeId);
        // 获取向客户端发送响应数据的输出流
        try (ServletOutputStream outputStream = response.getOutputStream()) {
            // 验证码图片数据写入到输出流
            lineCaptcha.write(outputStream);
        } catch (Exception e) {
            throw new AuthException("图形验证码输出错误");
        }
        return Result.succ(codeId);
    }

Postman调用测试

http://localhost:9036/api/identifyImage

验证码接口 | base64方式传递

/**
     * 生成验证码图片
     * @return
     */
    @ApiOperation("获取图形验证码")
    @GetMapping("/identifyImage")
    public Result<IdentifyImageResp> identifyImage(HttpServletResponse response,
                                                   @ApiParam(value = "图形验证码id,无值:生成验证码,有值:刷新验证码")
                                        @RequestParam(name = "codeId", required = false) String codeId) throws IOException {
        LineCaptcha lineCaptcha = CaptchaUtil.createLineCaptcha(200, 90, 4, 100);
        String code = lineCaptcha.getCode();
        if (codeId == null) {
            codeId = String.valueOf(IdWorker.getId());
            redisOps.set(codeId,code.toUpperCase(),60);
        } else {
            redisOps.set(codeId,code.toUpperCase(),60);
        }
        IdentifyImageResp identifyImageResp = new IdentifyImageResp(codeId, lineCaptcha.getImageBase64Data());
        return Result.succ(identifyImageResp);
    }

登录接口

登录接口编写

@PostMapping("login")
@ApiOperation("用户登录")
public Result login(@Validated @RequestBody LoginRequest request) {
    // request.getCodeId():请求体中获取codeId
    // redisOps.get(request.getCodeId():codeId为键,获取redis中对应的值
    String codeId = (String) redisOps.get(request.getCodeId());
    if (codeId.isEmpty()){
        throw new AuthException("验证码已过期请刷新重试");
    }
    AuthContext login = authService.login(request);
    
    // 登录成功后,通过 login.getMerchant() 获取到登录的用户对象,跟新登录信息
    Merchant merchant = login.getMerchant();
    merchant.setLastLoginAt(merchant.getLoginAt());
    merchant.setLoginAt(new Date());
    merchant.setLastLoginIp(merchant.getLoginIp());
    merchant.setLoginIp(CommonTools.getIp(httpServletRequest));
    merchantRepo.updateById(merchant);
    login.setMerchant(merchant);
    // JsonMapper.objectToJson(login):将login对象转换成 JSON 格式的字符串
    log.info("LOGIN - > {}", JsonMapper.objectToJson(login));
    return Result.succ("登录成功");
LoginRequest.java:请求体字段
@Data
@NoArgsConstructor
@AllArgsConstructor
public class LoginRequest {

    @NotEmpty
    @ApiModelProperty("登录名")
    private String username;
    @NotEmpty
    @ApiModelProperty("密码,md5加密全小写")
    private String password;
    @ApiModelProperty("验证码")
    private String code;
    @ApiModelProperty("验证码Id")
    private String codeId;

}
AuthService.java
    public AuthContext login(LoginRequest login) {
        // 登录验证和处理
        if (StringUtils.isBlank(login.getUsername())) {
            throw new AuthException("用户名不能为空");
        }
        if (StringUtils.isBlank(login.getPassword())) {
            throw new AuthException("密码不能为空");
        }
        // 缓存清空,登出操作
        logout();
        Merchant merchant = findMerchantByLoginEmail(login.getUsername());
        if (merchant == null) {
            authError("账户不存在,或状态不正确");
        } else if (merchant.getIsLocked()) {
            authError("账户已停用");
        }
        // 从redis获取login.getUsername()+"lock-time")的键对应的值
        if (redisOps.get(login.getUsername()+"lock-time") != null){
            // redisOps.getExpire:获取 Redis 中指定键的过期时间
            long expire = redisOps.getExpire(login.getUsername() + "lock-time");
            // 转换为分钟
            int minutes = (int) (expire / 60);
            // 转换为秒钟
            int seconds = (int) (expire % 60);
            authError("您已连续5次输入错误的密码,暂时不允许登录,"+minutes+"分"+seconds+"秒后可再次尝试登录");

        }
        System.out.println(merchant.getLoginPassword());
        System.out.println(SecretUtils.encrypt(login.getPassword()));
        Integer errorNum = (Integer) redisOps.get(login.getUsername());
        if (!merchant.getLoginPassword().equals(SecretUtils.encrypt(login.getPassword()))) {
            //密码错误次数为null时创建键值对
            if (errorNum == null){
                redisOps.set(login.getUsername(),1);
            }else if  ((errorNum > 0 && errorNum < 4) || (errorNum > 5 && errorNum < 10)){
                //密码错误次数为0-4、5-10时incr
                redisOps.incr(login.getUsername(),1);
            }else if (errorNum+1==5){
                //密码错误次数为5时锁定10分钟
                redisOps.set(login.getUsername()+"lock-time","lock",600);
                authError("您已连续5次输入错误的密码,暂时不允许登录,请10分钟后再次尝试登录");
            }else {
                //密码错误次数为10时锁定
                merchant.setIsLocked(true);
                merchantRepo.updateById(merchant);
                authError("您已连续10次输入错误的密码,账号已被锁定、不允许登录,请联系管理员解锁");
            }
            authError("密码不正确");
        }
        String code= (String) redisOps.get(login.getCodeId());
        if (code == null || login.getCode()==null || !code.equals(login.getCode().toUpperCase())){
            authError("请输入正确的验证码");
        }
//        merchant.setLoginPassword("*");
        String token = Sha.sha256(UUID.randomUUID().toString());
        AuthContext authContext = new AuthContext(token, merchant, null);
        redisOps.set(token, JsonMapper.objectToJson(authContext), authProp.getExpiresSeconds());
        CookieUtils.setCookie(response, "/", authProp.getTokenHeader(), token, authProp.getExpiresSeconds());
        //登陆完成删除账号错误次数
        if (errorNum!=null)
            redisOps.delete(login.getUsername());
        return authContext;
    }

    public String logout() {
        String cookie = CookieUtils.getCookie(request, authProp.getTokenHeader());
        String token = StringUtils.isNotBlank(cookie) ? cookie : request.getHeader(authProp.getTokenHeader());
        if (StringUtils.isNotBlank(token)) {
            redisOps.delete(token);
        }
        return "登出成功";
    }

    private void authError(String errorMsg) {
        throw new AuthException(errorMsg);
    }

 

 

 

本文出自:https://blog.csdn.net/weixin_62765017/article/details/131963978

Python的pandas库来实现将Excel文件转换为JSON格式的操作

1、使用Flask将数据转为JSON数据提供给客户端

如果您想创建一个本地接口来提供商品信息,您可以考虑使用Python以及一些流行的库来实现这个目标。以下是一些步骤和建议:

 

准备数据: 首先,确保您的Excel表包含商品信息,并将其保存为一个常见的数据格式,如CSV文件。这将使数据更容易在Python中处理。

 

安装Python: 如果您还没有安装Python,可以从Python官方网站下载并安装Python的最新版本。

 

选择Web框架: 为了创建本地接口,您可以选择一个Python的Web框架,如Flask或Django。对于初学者来说,Flask可能更容易入手。您可以使用以下命令安装Flask:

pip install Flask

创建Flask应用: 使用Flask,您可以创建一个简单的Web应用程序,将商品信息作为JSON数据提供给客户端。以下是一个简单的示例:

from flask import Flask, jsonify
import pandas as pd

app = Flask(__name__)

# 读取CSV文件
data = pd.read_csv('商品信息.csv')

@app.route('/api/products', methods=['GET'])
def get_products():
    # 将CSV数据转换为JSON格式
    products = data.to_dict(orient='records')
    return jsonify(products)

if __name__ == '__main__':
    app.run(debug=True)

这个示例创建了一个Flask应用,它可以在访问/api/products端点时返回商品信息的JSON数据。

 

运行应用程序: 保存上述代码为一个Python文件(例如app.py),然后在命令行中运行它:

python app.py

这将启动本地Web服务器,并使您的接口在http://127.0.0.1:5000/api/products上可用。

 

测试接口: 使用浏览器或工具如Postman来测试您的接口。访问http://127.0.0.1:5000/api/products应该返回商品信息的JSON数据。

 

这只是一个简单的示例,您可以根据需要扩展和自定义接口。此外,如果您希望接口支持其他操作(如添加、编辑、删除商品信息),您可以在Flask应用中添加相应的路由和处理程序。

 

2、使用pandas库实现Excel文件转换为JSON格式

使用Python的pandas库来实现将Excel文件转换为JSON格式的操作。以下是一个简单的示例代码:

 

首先,确保您已经安装了pandas库。如果没有安装,可以使用以下命令进行安装:

pip install pandas

然后,您可以使用以下代码将Excel文件转换为JSON:

import pandas as pd

读取Excel文件

df = pd.read_excel(‘your_excel_file.xlsx’) # 请将文件名替换为实际的Excel文件名

 

将DataFrame转换为JSON

json_data = df.to_json(orient=‘records’)

 

上述代码中,your_excel_file.xlsx应该替换为实际的Excel文件名。df.to_json(orient=‘records’)将DataFrame对象转换为JSON格式,并以列表的形式返回。

 

最后,您可以在Flask应用中将JSON数据提供给客户端,例如:

from flask import Flask, jsonify

app = Flask(__name__)

@app.route('/api/data', methods=['GET'])
def get_data():
    # 这里将之前转换的JSON数据返回给客户端
    return jsonify(json_data)

if __name__ == '__main__':
    app.run(debug=True)

在这个示例中,当访问/api/data端点时,Flask将返回之前转换的JSON数据。

 

请确保将文件名和路由根据您的实际需求进行适当更改。同时,也请注意异常处理和数据清理等方面的工作,以确保您的应用能够稳健运行。

本文出自:https://blog.csdn.net/hh867308122/article/details/132763586

《操作系统》- 线程与进程

一、进程的概念

程序放在磁盘上不叫做进程,只有运行起来之后才叫做进程,进程进程就是进行中的程序!

一次运行就会有一个进程,当再次运行的时候就会产生一个新的进程,浏览器就是这样的,而有的应用程序就不是,当已经在运行的时候再次运行会打开上次运行的程序。我们可以打开应用然后通过任务管理器进行观察就可以发现。

进程是操作系统进行资源分配和调度的一个独立单位(或者说是基本单位)

二、进程的结构和特征

2.1 进程的结构

进程主要有控制块(PCB)、数据段、程序段三部分构成,下图当中把这三块比喻成了一个人,也就是进程。

 

控制块:操作系统就是根据PCB来区分进程的,可以理解为每个进程有一个自己的唯一编号,就好比每个人都有自己的身份证一样,每个进程的PCB都是不重复的。

数据段:每个进程的数据都是不一样的,并且不存在共享,可以理解为就是人的身体

程序段:可以理解为就是磁盘当中存储的应用,以程序员的理解其实就是应用的代码,也就是多个进程可以共享一个程序代码。再通透一点,电脑安装了一个浏览器,却可以打开多个浏览器页面,多个浏览器页面就是多个进程。

PCB的存储信息与分类

2.2 进程的特征

动态性:进程是程序的一次执行过程,是动态地产生、变化和消亡的

并发性:多个进程可以并发执行

独立性:独立资源分配,每个进程都有自己的内存区域。

异步性:不用等待别的进程处理完成所以他是异步的、相互独立的、互不干扰。

三、线程的概念

线程可以理解为就是从进程当中分裂出来的一个指令集合。一个进程可以包含多个线程。也可以只存在一条线程,并且线程不能单独存在。线程的资源是由进程进行管理的。

 

我们打开一个应用就是进程,然后应用当中的每一个功能就对应了一个线程。

拿qq举例:打开qq就是一个进程的运行,这个时候在任务管理器当中是可以看得到该进程的,然后打开qq里面的qq空间就是一条线程,然后跟A聊天就又是一个线程,跟B聊天就又是一个线程。

 

 

面试的时候经常会问进程线程的区别:

 

进程是操作系统 资源分配调度 的基本单位

线程是操作系统 运算调度 的最小单位

想不起来的时候就想一下电脑的任务管理器,任务管理器当中显示了进程,并且显示了进程占用的内存CPU等信息。也就是操作系统是给进程分配空间而并不是线程。

 

线程下还有一个纤程(了解即可)

 

纤程:线程是在Windows内核中实现的,操作系统会根据系统的调度算法对线程进行调度。 纤程是在用户模式下实现的,内核对纤程一无所知。 纤程是更轻量级的线程,一个线程可以包含一个或多个纤程。

Java 原生没有提供纤程支持,需要依赖于 quasar-core的库,来创建纤程。感兴趣的可以了解一下,据说是多线程与多纤程计算耗时相比较差距是非常大的,多纤程要比多线程快的多。

 

线程的属性:

四、进程和线程比较

五、线程的实现方式

线程的实现分为两类:用户级线程(User-Level Thread,UTL)和内核级线程(Kernel-Level Thread, KTL)l。内核级线程又称内核支持的线程。

 

在用户空间完成的线程就是用户级线程、在内核空间完成的线程就是内核级线程。这里的完成,包含了线程的创建、执行、和销毁。

 

(1)用户级方式

操作系统提供了线程库,线程库包含了创建线程和销毁线程以及执行线程的接口。操作系统根本不知道线程这个东西,他在执行的时候是以进程装载线程指令来执行的。假如某个线程想要操作内核,这时候会通过陷入指令进行中断,一旦中断其他线程都将阻塞。优点:如果线程不访问内核,只是线程切换,效率比较高。因为所有的线程都在用户空间。

 

 

优点:用户级线程的切换在用户空间即可完成,不需要切换到核心态,线程管理的系统开销小,效率高。

缺点:当一个用户级线程被阻塞后,整个进程都会被阻塞,并发度不高。多个线程不可在多核处理机上并行运行(因为此时只有一个内核级线程)。

(2)内核级方式

内核级线程的线程控制块在内核空间,上面我们提到了进程由控制块(PCB)、数据段、程序段这三部分构成,而线程他也有,只不过线程的控制块是叫做线程控制块(TCB)。内核级方式就是将线程的控制块存放到内核当中,然后处理器直接切换控制块来完成线程的切换。这时候就完成了线程的并行。

 

 

优点:当一个用户级线程被阻塞后,别的用户级线程还可以继续执行,并发能力强。多线程可在多核处理机上并行执行。

缺点:一个用户进程会占用多个内核级线程,而内核级线程的切换由操作系统内核完成,需要切换到核心态,因此线程管理成本高、开销大。

本质上内核级方式和用户级方式 两者的区别就是线程的控制块到底是在用户空间还是在内核空间。

 

(3)特殊的组合方式

这种模型克服了多对一模型并发度不高的缺点,又克服了一对一模型中一个用户进程占用太多内核级线程,开销太大的缺点,是上述两种模型的折中方案。

本文出自:https://blog.csdn.net/weixin_43888891/article/details/131534525