【数据库】Python脚本实现数据库批量插入事务

news2024/12/23 17:14:50

背景介绍

在工作中可能会遇到需要批量插入的场景, 而批量插入的过程具有耗时长的特点, 再此过程很容易出现程序崩溃的情况.为了解决插入大量数据插入后崩溃导致已插入数据无法清理未插入数据无法筛出的问题, 需要编写一个脚本记录已插入和未插入的数据, 并可以根据记录的数据选择数据回滚或是继续执行任务; 因此需要对批量插入实现事务的机制.

概要设计

参考数据库的实现原理详见我之前写的文章的Recovery章节1; 事务的实现可以通过redo log和undo log实现, 这篇文章提供了三种算法, 但只有算法3才能实现事务的回滚和继续, 虽然说只有算法三能够达到预期的目标, 但类似上面三种算法的分类, 依旧存在多种写入顺序, 他们在各自的场景中都有各自的优势:

  1. 先写redo log, 再写磁盘, 写磁盘的过程中写undo log; 这样便于数据的再执行
  2. 同时写redo log和undo log; 每写一条undo log即可写一次磁盘; 这样效率最高, 但实现较为复杂

一般来说, 在执行失败之后都是选择未执行的继续执行, 因此这里选择方案1

注意; 倘若要实现事务, 这里的redo操作和undo操作需要有幂等性 在MySQL中redo可以用insert on duplicate key实现, 而undo对应的删除本身就是幂等的

有了redo log和undo log, 系统则可以根据保存的信息恢复或回滚任务, 然而系统恢复和清理过程依赖于系统当前的状态, 而数据的写入和回滚也会导致系统状态的改变; 因此系统设计的基本原则即是将系统的状态保存, 再根据当前的状态选择下一步的操作.

任务状态的保存和恢复

使用文件保存任务的状态, 文件名即任务的状态.

当系统崩溃之后, 重启可以读取到崩溃前的状态, 此时只需要根据下面的状态机图就可以识别出崩溃恢复时对应的状态.

在该系统中, 我使用枚举值表达系统的状态

class ResourceManagerStatus(Enum):
    STARTING = "STARTING"
    PREPARING = "PREPARING"
    INSERTING = "INSERTING"
    UNPREPARED = "UNPREPARED"
    BLOCKED = "BLOCKED"
    CANCELLED = "CANCELLED"
    FINISHED = "FINISHED"
    CLEANING = "CLEANING"

系统状态转移

下面是系统的状态机图, 描述了整个系统的转换过程

在这里插入图片描述

该系统主要考虑了两种可能, 一是正常工作的情况, 二是系统崩溃的境况. 为了在系统崩溃后能够识别系统当前的状态, 需要将所有的正常状态用文件保存下来除Starting, 因为它是默认状态; 当系统崩溃后, 即对应上图中的crush事件, 此时尽管无法修改文件以更新状态, 但在系统重启的时候, 会自动根据crush箭头的方向设置的对应的状态.

在能够保存状态之后, 则需要对系统的运行状态进行划分, 大致可以分为正常运行的状态崩溃状态两大类:

  1. 正常运行的状态: 指系统在运行尚未崩溃下所处的大类, 此时状态保存在内存当中, 包括:
    1. Starting: 初始状态
    2. Preparing: 准备状态, 处于该状态说明系统正在写redo log
    3. Inserting: 插入状态, 说明redo log已经完成写入, 系统正在插入
    4. Cleaning: 清理状态, 事务已经完成, 可以对删除临时文件
    5. Cancelled: 取消状态, 说明任务造成的影响已经回滚, 可以删除临时文件
  2. 崩溃的状态: 当系统崩溃后重启, 其所处的状态可能发生改变:
    1. Unprepared: redo log写入过程中系统发生了崩溃, 此时恢复只能够重新写redo log
    2. Blocked: 系统插入的过程中发生了崩溃, 由于redo log存在, 因此可以根据redo log继续插入; 但虽然redo具有幂等性, 可以重新执行redo, 但毕竟都保存了undo log, 计算他们之间的差值然后选择未插入的插入性能更好, 所以可以只重新执行未执行的任务
    3. Cleaning: 系统已经完成任务, 但清理文件的过程中发生了崩溃, Cleaning状态会被持久化到磁盘上, 因此可以直接回复该状态, 并继续清理

代码实现

由上一章节可以得出, 任务的执行本质上对应了状态的变化, 因此只需要将不同的状态抽象出来则可以控制程序的执行流程, 又因为状态的转换规则具有明显的偏向, 因此使用if判断状态的转移要远远优于将状态封装成类.

剩下的就是任务的拆分和执行过程, 对应了sql语句的执行.

任务的抽象

很明显, 只有可以分批执行的任务才能分批执行, 并且分批redo和undo; 为此可以抽象出一个类DividableDataSource代表可以分批执行的任务

class MetaInfo(BaseModel):
    total: int
    batchSize: int


class Log(BaseModel):
    ids: Tuple[str, ...]
    start: int
    end: int

    class Config:
        frozen = True
        
class DividableDataSource(ABC):

    @abstractmethod
    def getMetaInfo(self) -> MetaInfo:
        pass

    @abstractmethod
    def setMetaInfo(self, metaInfo: MetaInfo) -> None:
        pass

    @abstractmethod
    def divide(self) -> Iterable[Log]:
        pass

    @abstractmethod
    def update(self, log: Log) -> None:
        pass

    @abstractmethod
    def rollback(self, log: Log) -> None:
        pass

前两个方法是对MetaInfo的操作, 顾名思义MetaInfo保存了总任务的元数据, 它需要根据这些进行任务的分批; 具体任务的分批则会调用divide方法, 它会根据元数据进行分批; 这个分批过程其实并不需要一次性计算出来, 因此可以返回一个迭代器实际操作中可以将分批过程保存在next中, 甚至可以异步计算next, 这个得带器则包含了分区的信息, 即上面对应的Log类. 在完成了分区之后, 则需要对redo和undo实现, 他们分别对应了上面的updaterollback; 之所以这么命名是想要强调他们的幂等性

事务管理器

在完成了对可以分批处理, 且可以重做/撤销的任务的抽象后, 则可以对具有该特征的所有类进行事务的管理. 它包含了四个核心的方法: prepare, insert, rollback, restore; 用于管理事务的进行

prepare方法

prepare方法对应了事务的准备, 即写入undo log的过程

    def prepare(self) -> None:
        assert_equal(self.status, ResourceManagerStatus.STARTING,
                     "Can only prepare under status 'STARTING'")
        self.__changeStatus(ResourceManagerStatus.PREPARING)
        with open("./cache/MetaInfo.json", "w", encoding=config.encoding) as metaInfoOutput:
            metaInfoOutput.write(
                self.datasource.getMetaInfo().model_dump_json())
        with open("./cache/redo.json", "w", encoding=config.encoding) as logOutput:
            for line in self.datasource.divide():
                logOutput.write(line.model_dump_json()+"\n")
        open("./cache/undo.json", "w", encoding=config.encoding).close()
        self.__changeStatus(ResourceManagerStatus.INSERTING)

该系统采用json格式记录信息, 并逐行写入信息, 使用json格式记录保证了程序的可测试性, 便于错误的定位. 这个过程也很简单, 就是不要忘了各个状态之间的转换条件即可. 毕竟在插入状态再"准备"以此要重新计算和写入所有的redo log; 这毫无意义.

insert方法

    def insert(self) -> None:
        assert_equal(self.status, ResourceManagerStatus.INSERTING,
                     "can only insert under status 'INSERTING'")
        with open("./cache/redo.json", "r", encoding=config.encoding) as redo, open("./cache/undo.json", "a", encoding=config.encoding) as undo:
            for logStr in redo:
                log = Log.model_validate_json(logStr)
                self.datasource.update(log)
                undo.write(log.model_dump_json()+"\n")
                undo.flush()
            self.__changeStatus(ResourceManagerStatus.CLEANING)

在记录了redo log之后, 则可根据redo log中的信息不断进行插入, 插入过程还需要写undo log以实现可以对已写入数据的回滚, 这里需要注意每写入一条数据需要进行一次**flush**, 否则系统崩溃C库和操作系统的磁盘缓存都不会写入磁盘, 进而导致数据的丢失

restore方法

    def restore(self) -> None:
        # 还没有完成redo log的持久化, 直接删除临时文件
        if self.status == ResourceManagerStatus.UNPREPARED:
            os.remove("./cache/redo.json")
            with open("./cache/MetaInfo.json") as metaInfo:
                self.datasource.setMetaInfo(
                    MetaInfo.model_validate_json(metaInfo.read()))
            self.__changeStatus(ResourceManagerStatus.STARTING)
        # 已经完成redo log的持久化了, 完成redo log和undo log差的条目即可完成所有的持久化
        elif self.status == ResourceManagerStatus.BLOCKED:
            self.__changeStatus(ResourceManagerStatus.INSERTING)
            redoSet: Set[Log] = set()
            undoSet: Set[Log] = set()
            with open("./cache/redo.json", "r", encoding=config.encoding) as redo:
                for line in redo:
                    log = Log.model_validate_json(line)
                    redoSet.add(log)
            with open("./cache/undo.json", "r", encoding=config.encoding) as undo:
                for line in undo:
                    log = Log.model_validate_json(line)
                    undoSet.add(log)
            with open("./cache/undo.json", "a", encoding=config.encoding) as undo:
                for diff in redoSet - undoSet:
                    self.datasource.update(diff)
                    undo.write(diff.model_dump_json() + "\n")
            self.__changeStatus(ResourceManagerStatus.CLEANING)

对于失败的任务, 可以根据redo log进行恢复, 这里只需要判断当前状态是不是block即可判断redo log是否已经成功写入. 如上文所说的, 这里采用了计算redo log和undo log的差值然后再根据差值重新执行, 而非一味利用redo的幂等性执行, 对性能有所优化

rollback的实现

    def rollback(self) -> None:
        if self.status in ResourceManagerStatus.halfCommittedStatus():
            assert_true(os.path.exists("./cache/undo.json"),
                        "Can't find undo log")
            self.__changeStatus(ResourceManagerStatus.INSERTING)
            with open("./cache/undo.json", "r", encoding=config.encoding) as undo:
                for line in undo:
                    undoLog = Log.model_validate_json(line)
                    self.datasource.rollback(undoLog)
        self.__changeStatus(ResourceManagerStatus.CANCELLED)

rollback的实现相比于restore就简单很多, 它只需要执行所有redo log中反序列化出的信息即可.

测试

创建测试用表

CREATE TABLE score_source
(
    id           CHAR(36) PRIMARY KEY,
    student_name VARCHAR(64),
    course_name  VARCHAR(64),
    score        DOUBLE
);

这里使用Faker产生随机的数据, 并模拟批量插入的过程

class FakerDataSource(DividableDataSource):
    db = pymysql.connect(host=config.dbHost,
                         user=config.dbUser,
                         password=config.dbPassword,
                         database=config.dbName)

    def __init__(self, total: int = 0, batchSize: int = 0) -> None:
        self.metaInfo = MetaInfo(total=total, batchSize=batchSize)

    def getMetaInfo(self) -> MetaInfo:
        return self.metaInfo

    def setMetaInfo(self, metaInfo: MetaInfo) -> None:
        self.metaInfo = metaInfo

    def divide(self) -> Iterable[Log]:
        start, end = 0, 0
        ret: List[Log] = []
        while end < self.metaInfo.total:
            start, end = end, min(self.metaInfo.total,
                                  end+self.metaInfo.batchSize)
            ret.append(Log(ids=tuple(str(uuid.uuid1())
                                     for _ in range(start, end)), start=start, end=end))
        return ret

    def update(self, log: Log) -> None:
        cursor = self.db.cursor()
        for id in log.ids:
            cursor.execute(f"""INSERT INTO score_source (id, student_name, course_name, score) VALUE ('{id}', '{faker.name()}', '{faker.job()}', {faker.random_int(0, 10000)/100})
ON DUPLICATE KEY UPDATE student_name='{faker.name()}',
                        course_name='{faker.job()}',
                        score={faker.random_int(0, 10000)/100}""")
        self.db.commit()

    def rollback(self, log: Log) -> None:
        cursor = self.db.cursor()
        for id in log.ids:
            cursor.execute(f"""DELETE
    FROM score_source
    WHERE id ='{id}'""")
        self.db.commit()

这里通过insert on duplicate key update实现了redo的幂等性, delete本身则具有幂等性. 对于需要持久化的信息, 使用了uuid来保存, 这样便可以根据uuid找到已经插入或者需要回滚的数据.

下面是几个简单的测试用例, 分别测试了普通插入/恢复/回滚的功能:

class TransactionTest(unittest.TestCase):
    def setUp(self) -> None:
        db.cursor().execute("TRUNCATE score_source;")
        db.commit()

    def test_shouldPrepared(self) -> None:
        cursor = db.cursor()
        rm = ResourceManager(FakerDataSource(config.total, config.batchSize))
        rm.prepare()
        self.assertTrue(os.path.exists("./cache/INSERTING"),
                        "status should be 'INSERTING'")
        self.assertTrue(os.path.exists("./cache/redo.json"),
                        "redo log should be created")
        self.assertTrue(os.path.exists("./cache/undo.json"),
                        "undo log should be created")
        self.assertTrue(os.path.exists("./cache/MetaInfo.json"),
                        "MetaInfo file should be created")
        with open("./cache/redo.json") as redo:
            redoLogSize = sum(1 for _ in redo)
            self.assertEqual(redoLogSize, math.ceil(config.total / config.batchSize),
                             f"size of redo log should be {math.ceil(config.total / config.batchSize)}")
        with open("./cache/undo.json") as undo:
            undoLogSize = sum(1 for _ in undo)
            self.assertEqual(undoLogSize, 0,
                             f"size of redo log should be {0}")
        cursor.execute("SELECT COUNT(*) FROM score_source;")
        self.assertEqual(cursor.fetchone()[
                         0], 0, "nothing should be insert right now")

    # NOTICE: this should be executed after test_shouldPrepared
    def test_shouldRestore(self) -> None:
        cursor = db.cursor()
        rm = ResourceManager(FakerDataSource(config.total, config.batchSize))
        rm.restore()
        cursor.execute("SELECT COUNT(*) FROM score_source;")
        self.assertEqual(cursor.fetchone()[
                         0], config.total, "all data should be inserted")
        rm.restore()
        cursor.execute("SELECT COUNT(*) FROM score_source;")
        self.assertEqual(cursor.fetchone()[
                         0], config.total, "redo should be idempotent")
        with open("./cache/redo.json") as redo:
            redoLogSize = sum(1 for _ in redo)
            self.assertEqual(redoLogSize, math.ceil(config.total / config.batchSize),
                             f"size of redo log should be {math.ceil(config.total / config.batchSize)}")
        with open("./cache/undo.json") as undo:
            undoLogSize = sum(1 for _ in undo)
            self.assertEqual(undoLogSize, math.ceil(config.total / config.batchSize),
                             f"size of redo log should be {math.ceil(config.total / config.batchSize)}")
        self.assertTrue(os.path.exists("./cache/CLEANING"),
                        "status should be CLEANING")
        rm.clean()
        self.assertEqual(len(os.listdir("./cache")), 0,
                         "all temporary file should be cleaned")

    def test_shouldRollback(self) -> None:
        cursor = db.cursor()
        rm = ResourceManager(FakerDataSource(config.total, config.batchSize))
        rm.prepare()
        rm.insert()
        cursor.execute("SELECT COUNT(*) FROM score_source;")
        self.assertEqual(cursor.fetchone()[
                         0], config.total, "data should be inserted")

        rm.rollback()
        db.commit()
        cursor.execute("SELECT COUNT(*) FROM score_source;")
        self.assertEqual(cursor.fetchone()[
                         0], 0, "should rollback")

        rm.rollback()
        cursor.execute("SELECT COUNT(*) FROM score_source;")
        self.assertEqual(cursor.fetchone()[
                         0], 0, "rollback should be idempotent")
        rm.clean()

    def tearDown(self) -> None:
        db.cursor().execute("TRUNCATE score_source;")
        db.commit()

优化

  1. DividableDataSource#divide改为异步生成的迭代器; 这样便可以延迟分片的计算, 以提高性能
  2. 使用多线程实现概要设计中对应的方案2, 对于io密集型应用可以极大地提高效率; 如一个线程写redo log, 一个线程写undo log, 多个线程执行update/rollback

引用


  1. 【精选】【数据库】数据库笔记_is a software package,designed to store,retrieve,q-CSDN博客 ↩︎

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1149005.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

阿里云服务linux系统CentOs8.5安装/卸载nginx1.15.9

说明&#xff1a;尝试使用CentOs8.5安装nginx1.9.9失败&#xff0c;make的时候报错了&#xff0c;后面降低版本为CentOs7.5安装成功了&#xff0c;参考文章:【精选】centos7安装nginx-1.9.9_linx centos nginx 1.9.9版本 nginx error log file: "/-CSDN博客 一、安装ngin…

Animator中Has Exit Time,融合时间

写这篇文章的原因是&#xff0c;发现有时候SetTrigger后动画没有切换&#xff0c;做实验找到了原因。 一、勾选了Has Exit Time 动画一定会在设定的时间才可能会切换到下个动画片段。 NormalAttack1-->NormalAttack2,结束时间设置0.5 NormalAttack1-->NormalAttack3,结…

什么是Immutable.js?它的作用是什么?

聚沙成塔每天进步一点点 ⭐ 专栏简介 前端入门之旅&#xff1a;探索Web开发的奇妙世界 欢迎来到前端入门之旅&#xff01;感兴趣的可以订阅本专栏哦&#xff01;这个专栏是为那些对Web开发感兴趣、刚刚踏入前端领域的朋友们量身打造的。无论你是完全的新手还是有一些基础的开发…

Flask-SQLAlchemy事件钩子介绍

一、前言 前几天在搜资料的时候无意中看到有介绍SQLAlchemy触发器&#xff0c;当时感觉挺奇怪的&#xff0c;触发器不是数据库层面的概念吗&#xff0c;怎么flask-SQLAlchemy这个ORM框架会有这玩意。 二、SQLAlchemy触发器一个简单例子 考虑到效率博客表中有两个字段&#xf…

【设计模式】第15节:行为型模式之“职责链模式”

一、简介 职责链模式&#xff1a;将请求的发送和接收解耦&#xff0c;让多个接收对象都有机会处理这个请求。将这些接收对象串成一条链&#xff0c;并沿着这条链传递这个请求&#xff0c;直到链上的某个接收对象能够处理它为止。 二、优点 分离发送者和接受者易于扩展和维护…

SIT1028Q内置高压 LDO 本地互联网络(LIN)收发器

SIT1028Q 是一款内部集成高压 LDO 稳压源的本地互联网络&#xff08; LIN &#xff09;物理层收发器&#xff0c;可为外 部 ECU &#xff08; Electronic Control Unit &#xff09;微控制器或相关外设提供稳定的 5V/3.3V 电源&#xff0c;该 LIN 收发器 符合 LIN 2…

如何构建 :毫末波 与 RIS 研究系统

入门系统 USRP 上下变频器 喇叭天线 可联系博主系统的具体情况&#xff1b; 系统详情 首先需要在USRP上运行一个通信系统&#xff0c;这个系统可以是简单的文本传输系统&#xff0c;用Simulink搭建一下就行了。更加复杂的系统可以用LabVIEW或者Simulink来搭建&#xff1b;…

力扣第968题 监控二叉树 c++ hard题 二叉树的后序遍历 + 模拟 + 贪心

题目 968. 监控二叉树 困难 相关标签 树 深度优先搜索 动态规划 二叉树 给定一个二叉树&#xff0c;我们在树的节点上安装摄像头。 节点上的每个摄影头都可以监视其父对象、自身及其直接子对象。 计算监控树的所有节点所需的最小摄像头数量。 示例 1&#xff1a; …

Linux解决nvcc -V出现的-bash: nvcc command not found问题

两种解决办法&#xff1a; 1、第一种直接在bashrc文件中添加本地cuda路径&#xff1a; vim ~/.bashrc 定位到内容末尾&#xff0c;最末尾 添加命令&#xff1a; export LD_LIBRARY_PATH/usr/local/cuda/lib export PATH$PATH:/usr/local/cuda/bin添加后激活 source ~/.bashrc…

Java通过工具类判断前端给定的实体类属性中是否为空

目录 一、场景描述 二、实现过程 1、实体类 2、工具类 3、常量类 4、测试 一、场景描述 在Java开发过程中&#xff0c;当前端页面传递参数时&#xff0c;如果我们使用实体类进行接收&#xff0c;而一些属性的值是必须有值的&#xff0c;那么就需要对这些属性进行校验&…

ERP、CRM、SRM、PLM、HRM、OA……都是啥意思?

在企业里上班&#xff0c;经常会听说一些奇怪的系统或平台名称&#xff0c;例如ERP、CRM、SRM、PLM、HRM、OA、FOL等。 这些系统&#xff0c;都是干啥用的&#xff1f; █ ERP&#xff08;企业资源计划&#xff09; 英文全称&#xff1a;Enterprise Resource Planning 定义&…

NewStarCTF2023week5-隐秘的图片

下载附件解压得到两张图片 第一张二维码扫出来提示没有什么 第二张看到的第一直觉是修复&#xff0c;因为缺了三个定位符&#xff0c;比如下面这种&#xff0c;就是修复定位符&#xff1a; 但是这里这道题仔细看一下&#xff0c;修复好了也不像正常的二维码&#xff0c;并且这…

[SHCTF 2023新生赛] web题解

文章目录 [WEEK1]babyRCE1zzphpez_serialize登录就给flag飞机大战方法一方法二 ezphp生成你的邀请函吧~ [WEEK2]serializeno_wake_upMD5的事就拜托了Hashpumphash_ext_attack脚本 ez_sstiEasyCMS [WEEK3]sseerriiaalliizzeegogogo [WEEK1] babyRCE 源码 <?php$rce $_GE…

Vue的安装

----------------------------------------------------前置---------------------------------------------------- 1.node.js的下载安装、缓存路径的设置 ①安装 ②设置npm prefix, cache 2.NODE_PATH、PATH ①系统变量中加 ②PATH中加 3.配置镜像源 -----------------------…

华锐技术何志东:证券核心交易系统分布式改造将迎来规模化落地阶段

近年来&#xff0c;数字化转型成为证券业发展的下一战略高地&#xff0c;根据 2021 年证券业协会专项调查结果显示&#xff0c;71% 的券商将数字化转型列为公司战略任务。 在落地数字化转型战略过程中&#xff0c;证券业核心交易系统面临着不少挑战。构建新一代分布式核心交易…

Ansible自动化运维工具介绍与部属

Ansible自动化运维工具介绍与部属 一、ansible简介1.1、什么是Ansible1.2、Ansible的特点1.3、Ansible的架构 二、Ansible任务执行解析2.1、ansible任务执行模式2.2、ansible执行流程2.3、ansible命令执行过程 三、部署ansible管理集群3.1、实验环境3.2、安装ansible3.3、查看基…

[架构之路-246/创业之路-77]:目标系统 - 纵向分层 - 企业信息化的呈现形态:常见企业信息化软件系统 - 客户关系管理系统CRM

目录 前言&#xff1a; 一、企业信息化的结果&#xff1a;常见企业信息化软件 1.1 客户关系管理系统CRM 1.1.1 什么是客户关系管理系统 1.1.2 CRM总体架构 1.1.3 什么类型的企业需要CRM 1.1.4 创业公司在什么阶段需要CRM 1.1.5 研发型创业公司什么时候需要CRM 1.1.6 C…

面试题:说一下海量请求下的接口并发解决方案

文章目录 服务限流限流算法1. 漏斗算法2. 令牌桶算法3. 滑窗算法 接入层限流Nginx限流 本地接口限流Semaphore 分布式接口限流使用消息队列 设定一个场景&#xff0c;假如一个商品接口在某段时间突然上升&#xff0c;会怎么办&#xff1f; 生活中的例子来说&#xff0c;假设冰…

1.4 安全服务

思维导图&#xff1a; 1.4 安全服务 定义&#xff1a;在通信开放系统中&#xff0c;为系统或数据传输提供足够安全的协议层服务。 RFC4949 定义&#xff1a;由系统提供的对系统资源进行特殊保护的处理或通信服务。安全服务通过安全机制来实现安全策略。 分类&#xff1a;X.800 …

加密机:保护您的信息安全的强大工具

在当今数字化的世界中&#xff0c;信息安全显得尤为重要。信息不仅关乎个人的隐私&#xff0c;也关乎企业的生死存亡。无论是个人还是企业&#xff0c;我们都需要一种强大的工具来保护我们的信息安全&#xff0c;这种工具就是加密机。 加密机是一种专门用于加密和解密电子数据的…