MIT 6.830数据库系统 -- lab six

MIT 6.830数据库系统 -- lab six

  • 项目拉取
  • 引言
    • steal/no-force策略
    • redo log与undo log
    • 日志格式和检查点
  • 开始
  • 回滚
    • 练习1:LogFile.rollback()
  • 恢复
    • 练习2:LogFile.recover()
  • 测试结果
  • 疑问点分析


项目拉取

原项目使用ant进行项目构建,我已经更改为Maven构建,大家直接拉取我改好后的项目即可:

  • https://gitee.com/DaHuYuXiXi/simple-db-hw-2021

然后就是正常的maven项目配置,启动即可。各个lab的实现,会放在lab/分支下。


引言

在本实验中,我们将要实现基于日志的中止回滚和崩溃恢复。源码中提供了定义日志格式的代码,并在事务期间的适当时间将记录附加到日志文件中。我们将使用日志文件的内容完成回滚和恢复。

源码中提供的日志代码产生了用于物理上整页undo和redo的记录。当页是首次读入时,代码记住了整页的原始内容做为前置镜像。当事务更新页时,相应的日志记录包含已存储的前置镜像以及修改后的页面做为后置镜像。我们将使用前置镜像在中止期间进行回滚,在recovery期间undo丢失的事务,后置镜像用于在recovery期间redo成功的事务。

我们可以不做整个页面的物理撤销(那么ARIES必须做逻辑撤销),因为我们正在做页面级别的锁定,并且因为我们没有索引,在撤销时索引的结果可能与最初编写日志时的结构不同。页面级锁定简化事情的原因是,如果一个事务修改了一个页面,那么它一定有一个排他锁,这意味着没有其他事务同时修改它,因此我们可以通过覆盖整个页面来撤销对它的修改。

BufferPool已经实现了通过删除脏页来中止事务,并且通过强制在提交时将脏页刷新至磁盘来假装实现原子提交。日志允许更加灵活的缓冲区管理(STEAL & NO-FORCE),测试代码会在特定的时机调用BufferPool.flushAllPages()方法来验证这种灵活性


steal/no-force策略

lab6要实现的是simpledb的日志系统,以支持回滚和崩溃恢复;在lab4事务中,我们并没有考虑事务执行过程中,如果机器故障或者停电了数据丢失的问题,bufferpool采用的是no-steal/force的策略,而这个实验我们实现的是steal/no-force策略,两种策略的区别如下:

  • steal/no-steal: 是否允许一个uncommitted的事务将修改更新到磁盘
    • 如果是steal策略,那么此时磁盘上就可能包含uncommitted的数据,因此系统需要记录undo log,以防事务abort时进行回滚(roll-back)。
    • 如果是no steal策略,就表示磁盘上不会存在uncommitted数据,因此无需回滚操作,也就无需记录undo log。
  • force/no-force:
    • force策略表示事务在committed之后必须将所有更新立刻持久化到磁盘,这样会导致磁盘发生很多小的写操作(更可能是随机写)。
    • no-force表示事务在committed之后可以不立即持久化到磁盘, 这样可以缓存很多的更新批量持久化到磁盘,这样可以降低磁盘操作次数(提升顺序写),但是如果committed之后发生crash,那么此时已经committed的事务数据将会丢失(因为还没有持久化到磁盘),因此系统需要记录redo log,在系统重启时候进行前滚(roll-forward)操作。

redo log与undo log

为了支持steal/no-force策略,即我们可以将未提交事务的数据更新到磁盘,也不必在事务提交时就一定将修改的数据刷入磁盘,我们需要用日志来记录一些修改的行为。在simpledb中,日志不区分redo log和undo log,格式较为简单,也不会记录事务执行过程中对记录的具体修改行为。

对于redo log,为确保事务的持久性,redo log需要事务操作的变化,simpledb中用UPDATE格式的日志来保存数据的变化,在每次将数据页写入磁盘前需要用logWrite方法来记录变化:

public synchronized void logWrite(TransactionId tid,Page before,Page after)

这样,对于这些脏页,即使断电丢失数据了,我们也可以通过事务id来判断事务是否已经提交(这里提交事务会记录另一种格式的日志),如果事务已经提交,则重启时根据日志的内容就可以把数据恢复了;总而言之,通过这样的方式,可以让simpledb支持崩溃恢复;

对于undo log,我们采用的是在page中使用一个变量oldData保存一份当前页旧的快照数据:

public abstract class BTreePage implements Page {...protected byte[] oldData;
}public class BTreeRootPtrPage implements Page {...private byte[] oldData;
}public class HeapPage implements Page {...byte[] oldData;
}    

数据页一开始的旧数据是空的,那什么时候会对旧数据进行更新呢?答案是事务提交时,当事务提交时,就意味着这个修改已经是持久化到磁盘了,新的事务修改后就数据页的数据就是脏数据了,而在新事务回滚时,由于我们采用的是steal策略,脏页可能已经在页面淘汰时被写入磁盘中了,那么该如何进行恢复呢?答案是before-image,即oldData,通过上一次成功事务的数据,我们可以恢复到事务开始前的样子,这样,就可以实现了事务的回滚了。


日志格式和检查点

simpleDB日志相关逻辑主要集中在LogFile中,本节我们来看看simpleDB中几种日志格式和checkpoint机制。

log file的格式如下所述:

  1. 日志文件格式概述:

    • 文件中的第一个长整数表示上次写入的检查点的偏移量,如果没有检查点则为 -1。
    • 文件中的其余数据由日志记录组成,这些记录的长度可变。
  2. 日志记录格式:

    • 每个日志记录以一个整数类型和一个长整数事务 ID 开始。
    • 每个日志记录以表示记录开始位置的长整数文件偏移量结束。
  3. 五种记录类型:

    • ABORT(中止)COMMIT(提交)BEGIN(开始) 记录不包含额外数据。
    • UPDATE(更新) 记录由两个条目组成:before image 和 after image。这些image是序列化的 Page 对象,可以使用 LogFile.readPageData()LogFile.writePageData() 方法访问。详见 LogFile.print() 的示例。
    • CHECKPOINT(检查点) 记录包含在检查点时处于活动状态的事务以及它们在磁盘上的第一条日志记录。记录的格式包括事务数量的整数计数,以及每个活动事务的长整数事务 ID 和长整数第一条记录偏移量。

在这里插入图片描述


开始

我们必须在lab5代码的基础上实现lab6,我们需要修改现存的部分代码并且添加一些新文件:

我们的代码需要做出如下改变:

1、向BufferPool.flushPage()方法中调用writePage(p)方法之前的位置插入如下代码,其中p是被写入页的引用:

   private synchronized void flushPage(PageId pid) throws IOException {Page flush = pageCache.get(pid);// 通过tableId找到对应的DbFile,并将page写入到对应的DbFile中int tableId = pid.getTableId();DbFile dbFile = Database.getCatalog().getDatabaseFile(tableId);// append an update record to the log, with a before-image and after-imageTransactionId dirtier = flush.isDirty();if (dirtier != null) {Database.getLogFile().logWrite(dirtier, flush.getBeforeImage(), flush);Database.getLogFile().force();}// 将page刷新到磁盘dbFile.writePage(flush);flush.markDirty(false, null);}

上述代码可以使日志系统向日志中写入一条update记录;调用force()方法是为了确保在脏页刷新到磁盘之前日志记录先记录到磁盘中

2、在updateBufferPool中记录当前事务修改产生的脏页:

    private void updateBufferPool(List<Page> pages, TransactionId tid) throws DbException {for (Page page : pages) {page.markDirty(true, tid);}// 记录当前事务修改产生的脏页Database.getTransactionById(tid).addDirtyPages(pages);}

3、BufferPool.transactionComplete()方法为已提交事务污染的每个页调用flushPage()方法;对每个脏页,在刷新完成之后添加p.setBeforeImage()调用:

    /*** Write all pages of the specified transaction to disk.* 该方法只有在事务正常提交时才会被调用,从而将当前事务已经修改的部分数据页同步到磁盘*/public synchronized void flushPages(TransactionId tid) throws IOException {// some code goes here// not necessary for lab1|lab2// 当前事务修改产生的脏页集合可能在事务没有提交前就已经落盘了 -- no steal mode// 但是落盘时记录的Before Image是事务开启前的旧数据,此时事务提交了,需要更新Before Image到最新状态for (Page page : Database.getTransactionById(tid).getDirtyPages()) {// use current page contents as the before-image for the next transaction that modifies this page.page.setBeforeImage();flushPage(page.getId());}}

这部分代码可能与网上大多数人做法不同,具体大家可以拉取源码仓库查看。

当一个更新提交后,页的前置镜像也需要更新,以便稍后中止的事务回滚到次提交的页面版本

注意:

  • 我们不能仅在flushPage()方法中调用setBeforeImage()方法,因为即使事务没有被提交flushPage()方法也可能被调用。
  • 测试代码就会做这样的事,如果我们通过调用flushPages()来实现transactionComplete()方法,那么我们可能需要向flushPages()传递额外的参数去告诉这个方法该刷新是用于提交还是未提交的事务。
  • 但是,强烈建议在本案例中重写transactionComplete()方法直接调用flushPage()

当做完上述代码的修改之后,我们可以进行LogTest系统测试,此时我们会发现可以通过其中三个子测试,剩余的测试会失败:

% ant runsystest -Dtest=LogTest...[junit] Running simpledb.systemtest.LogTest[junit] Testsuite: simpledb.systemtest.LogTest[junit] Tests run: 10, Failures: 0, Errors: 7, Time elapsed: 0.42 sec[junit] Tests run: 10, Failures: 0, Errors: 7, Time elapsed: 0.42 sec[junit] [junit] Testcase: PatchTest took 0.057 sec[junit] Testcase: TestFlushAll took 0.022 sec[junit] Testcase: TestCommitCrash took 0.018 sec[junit] Testcase: TestAbort took 0.03 sec[junit]     Caused an ERROR[junit] LogTest: tuple present but shouldn't be...

如果通过的测试少于这三个子测试的话,说明我们对已有代码的修改并不兼容,我们需要解决这些问题


回滚

阅读LogFile.java文件中对于日志文件格式描述的注释;我们可以在LogFile.java文件中看到一系列函数,例如logCommit(),它用于生成各种类型的日志记录并添加到日志中。

我们的第一个任务是实现LogFile.javarollback()函数。当事务中止时,并且事务释放掉它的锁之前会调用该函数。它的任务就是撤销事务对数据库可能的更改。

rollback()方法需要读取日志文件,查找所有的与中止事务有关的更新记录,从每条记录中提取前置镜像,并且将前置镜像写入表文件。使用raf.seek()在日志文件中进行范围移动,并且使用raf.readInt()等方法进行检验。使用readPageData()方法读取前置和后置镜像。我们可以使用tidToFirstLogRecord映射(从事务id映射到堆文件中的偏移量)确定对于一个特定的事务从哪开始读取日志文件。在将前置镜像写回表文件之前,我们需要丢弃缓冲池中缓存的对应的页。

在开发期间,Logfile.print()方法对于展示现在的日志内容非常有用。


练习1:LogFile.rollback()

实现LogFile.rollback()方法。

代码编写完成后我们需要通过LogTest系统测试的TestAbort和TestAbortCommitInterleaved子测试。

实现代码如下所示:

public void rollback(TransactionId tid)throws NoSuchElementException, IOException {synchronized (Database.getBufferPool()) {synchronized (this) {preAppend();// some code goes here// 获取事务tid对应的日志记录偏移量Long offset = tidToFirstLogRecord.get(tid.getId());// 读取日志记录raf.seek(offset);Set<PageId> pageIdSet = new HashSet<>();// 前置判断,判断raf是否已经遍历到末尾while (raf.getFilePointer() != raf.length()) {int type = raf.readInt();long transactionId = raf.readLong();if (transactionId != tid.getId()) {continue;}// 前置判断,判断日志记录类型是否为包含前置镜像和后置镜像的UPDATE类型if (type == UPDATE_RECORD) {// 读取事务对应页的前置镜像,并根据前置镜像进行回滚Page before = readPageData(raf);Page after = readPageData(raf);// 前置镜像idPageId pid = before.getId();// 确保记录的事务id和当前回滚的事务的id相等// 并且该页面此前没有进行过回滚,如果进行过回顾则无需重复回滚if (transactionId == tid.getId() && !pageIdSet.contains(pid)) {pageIdSet.add(pid);// 丢弃BufferPool中事务对应的pidDatabase.getBufferPool().discardPage(pid);// 将前置镜像写回表文件Database.getCatalog().getDatabaseFile(pid.getTableId()).writePage(before);}} else if (type == CHECKPOINT_RECORD) {int count = raf.readInt();while (count-- > 0) {raf.readLong();raf.readLong();}}raf.readLong();}// 将raf的文件指针指向正确的偏移位置raf.seek(raf.length());}}}

在这里插入图片描述


恢复

如果数据库崩溃并且重启,在任何新事务开始前会调用LogFile.recover()方法。我们的实现必须满足如下条件:

  1. 如果有最后一个检查点的话需要读取最后一个检查点
  2. 从检查点开始向前扫描日志文件(如果没有检查点则从日志文件开始扫描)以建立失败事务集合。重做已提交事务的更新操作。我们可以放心在检查点开始redo,因为LogFile.logCheckpoint()方法将所有的脏页都刷新到磁盘了
  3. 撤销失败事务的更新

练习2:LogFile.recover()

实现LogFile.recover()方法。

完成本次练习后,需要通过LogTest的所有子测试:

再完成本练习之前,我们先来看一下lab中已经为我们提供好的checkpoint方法是如何实现的:

  1. 写入检查点记录
    在这里插入图片描述

  2. 缩减无用日志

在这里插入图片描述

logCheckpoint方法源码大家可自行查看,这里不再多述,下面我们来看一下recover方法的源码:

    /*** Recover the database system by ensuring that the updates of* committed transactions are installed and that the* updates of uncommitted transactions are not installed.*/public void recover() throws IOException {synchronized (Database.getBufferPool()) {synchronized (this) {recoveryUndecided = false;// some code goes hereraf.seek(0);// 已提交事务集合Set<Long> commitId = new HashSet<>();// 事务id-前置镜像Map<Long, List<Page>> beforePages = new HashMap<>();// 事务id-后置镜像Map<Long, List<Page>> afterPages = new HashMap<>();// 记录checkpoint时间点所有活跃的事务,判断是回滚还是重放Map<Long, Long> activeTransactions = new HashMap<>();// 获取最新checkpoint位置long checkpoint = raf.readLong();// 定位到最新的checkpoint位置if (checkpoint != -1) {raf.seek(checkpoint);}// 前置判断,判断raf是否已经遍历到末尾while (raf.getFilePointer() != raf.length()) {int type = raf.readInt();long tid = raf.readLong();if (type == UPDATE_RECORD) {Page before_image = readPageData(raf);Page after_image = readPageData(raf);List<Page> before = beforePages.getOrDefault(tid, new ArrayList<>());before.add(before_image);beforePages.put(tid, before);List<Page> after = afterPages.getOrDefault(tid, new ArrayList<>());after.add(after_image);afterPages.put(tid, after);} else if (type == COMMIT_RECORD) {// 可能会包含checkpoint发生时的活跃事务的提交记录commitId.add(tid);} else if (type == CHECKPOINT_RECORD) {int count = raf.readInt();while (count-- > 0) {activeTransactions.put(raf.readLong(), raf.readLong());}}raf.readLong();}// 处理未提交的事务for (Long tid : beforePages.keySet()) {if (!commitId.contains(tid)) {List<Page> pages = beforePages.get(tid);for (Page page : pages) {Database.getCatalog().getDatabaseFile(page.getId().getTableId()).writePage(page);}}}// 处理已提交的事务for (Long tid : commitId) {if (afterPages.containsKey(tid)) {List<Page> pages = afterPages.get(tid);for (Page page : pages) {Database.getCatalog().getDatabaseFile(page.getId().getTableId()).writePage(page);}}}// 处理checkpoint点发生时的活跃事务,判断是提交还是回滚for (Map.Entry<Long, Long> entry : activeTransactions.entrySet()) {Long transactionId = entry.getKey();Long offset = entry.getValue();// 当前活跃事务重放还是回滚取决于当前活跃事务在checkpoint后是否提交了// 如果提交了,那么重放,否则回滚recoveryOrRollbackByOffset(new TransactionId(transactionId), offset, commitId.contains(transactionId));}}}}private void recoveryOrRollbackByOffset(TransactionId transactionId, Long offset,boolean recover) throws IOException {raf.seek(offset);while (raf.getFilePointer() != raf.length()) {int type = raf.readInt();long tid = raf.readLong();if (type == UPDATE_RECORD) {Page before_image = readPageData(raf);Page after_image = readPageData(raf);Page targetPage = recover ? after_image : before_image;if(tid == transactionId.getId()) {Database.getCatalog().getDatabaseFile(targetPage.getId().getTableId()).writePage(targetPage);}} else if (type == CHECKPOINT_RECORD) {int count = raf.readInt();while (count-- > 0) {raf.readLong();raf.readLong();}}raf.readLong();}}

奔溃恢复过程不算难,但是需要对checkpoint点的活跃事务进行特殊处理:

  • 活跃事务一在checkpoint后commit了,处理情况如下:
    在这里插入图片描述

  • 如果活跃事务一在checkpoint后没有commit记录或者存在abort记录,则需要执行回滚操作

在这里插入图片描述


测试结果

在这里插入图片描述


疑问点分析

我看网上不少博客在开始这一小节中两个flushPage方法是这样实现的,如下所示:

   private synchronized void flushPage(PageId pid) throws IOException {Page flush = pageCache.get(pid);// 通过tableId找到对应的DbFile,并将page写入到对应的DbFile中int tableId = pid.getTableId();DbFile dbFile = Database.getCatalog().getDatabaseFile(tableId);// append an update record to the log, with a before-image and after-imageTransactionId dirtier = flush.isDirty();if (dirtier != null) {Database.getLogFile().logWrite(dirtier, flush.getBeforeImage(), flush);Database.getLogFile().force();}// 将page刷新到磁盘dbFile.writePage(flush);flush.markDirty(false, null);}

第一个flushPage方法并没有什么问题,但是第二个flushPages方法的实现个人觉得存在问题,因为笔者测试过程中存在测试用例测试失败:

    /** Write all pages of the specified transaction to disk.*/public synchronized  void flushPages(TransactionId tid) throws IOException {// some code goes here// not necessary for lab1|lab2for (Map.Entry<PageId, Page> entry : pageCache.entrySet()) {Page page = entry.getValue();// 核心: 未提交的事务在此处会更新自己的before_image为最新镜像// 那么如果此时调用flushAllPages方法,log日志中记录的就是当前未提交事务的最新before_image// 后面如果未提交事务回滚,拿着日志中记录的最新的before_image进行回滚,显然是错误的page.setBeforeImage();if (page.isDirty() == tid) {flushPage(page.getId());}}}

首先,flushPage方法只会在事务正常提交的时候被调用,为的是将本次事务修改产生的脏页全部落盘并且在落盘前先记录最新更改日志到日志文件中。

还有一个flushAllPages方法如下所示,该方法是为了模拟no steal mode模式,即未提交事务修改产生的脏页可能会提前落盘,此时同样会在落盘前记录日志:

    public synchronized void flushAllPages() throws IOException {pageCache.forEach((pageId, page) -> {try {// 只有脏页才刷新if (page.isDirty() != null) {flushPage(page.getId());}} catch (IOException e) {e.printStackTrace();}});}

如果就这样实现,我们来看一下下面这个测试用例:

     @Testpublic void TestAbortCommitInterleaved()throws IOException, DbException, TransactionAbortedException {setup();// *** Test:// T1 start, T2 start and commit, T1 abortTransaction t1 = Database.newTransaction();t1.start();insertRow(hf1, t1, 3);Transaction t2 = Database.newTransaction();t2.start();insertRow(hf2, t2, 21);insertRow(hf2, t2, 22);// commit函数中是会调用flushPages方法将与当前t2事务相关的脏页都刷到磁盘上的t2.commit();insertRow(hf1, t1, 4);abort(t1);Transaction t = Database.newTransaction();t.start();// 这里会抛出异常,因为此时3是存在的,这是为什么呢?look(hf1, t, 3, false);look(hf1, t, 4, false);look(hf2, t, 21, true);look(hf2, t, 22, true);t.commit();}void abort(Transaction t)throws IOException {// t.transactionComplete(true); // abortDatabase.getBufferPool().flushAllPages(); // XXX defeat NO-STEAL-based abortDatabase.getLogFile().logAbort(t.getId()); // does rollback tooDatabase.getBufferPool().flushAllPages(); // prevent NO-STEAL-based abort from// un-doing the rollbackDatabase.getBufferPool().transactionComplete(t.getId(), false); // release locks}

事务t2的commit方法中,会更新事务t1关联的前置镜像从null变为3,然后调用abort方法回滚事务t1,在该方法中,首先调用flushAllPages方法将所有脏页都刷新到磁盘上,包括未提交事务产生的脏页,此时事务t1修改产生的脏页落盘,如下所示:
在这里插入图片描述
当真正执行事务t1的回滚操作时,会重新应用最后一条update记录的前置镜像,很显然这次回滚结果是错误的。

原因:未提交的事务在flushPages方法中会更新自己的before_image为最新镜像,那么如果此时调用flushAllPages方法,log日志中记录的就是当前未提交事务的最新before_image,后面如果未提交事务回滚,拿着日志中记录的最新的before_image进行回滚,显然是错误的。


有聪明的小伙伴会想,能不能把flushPages方法修改成如下模样:

    /** Write all pages of the specified transaction to disk.*/public synchronized  void flushPages(TransactionId tid) throws IOException {// some code goes here// not necessary for lab1|lab2for (Map.Entry<PageId, Page> entry : pageCache.entrySet()) {Page page = entry.getValue();if (page.isDirty() == tid) {page.setBeforeImage();flushPage(page.getId());}}}

这样一来,flushPages方法中只会更新当前提交事务的最新镜像,这样的逻辑是没错的,但是还是会产生问题,我们来看下面这个测试用例:

    @Testpublic void TestCommitAbortCommitCrash()throws IOException, DbException, TransactionAbortedException {setup();// *** Test:// T1 inserts and commitsdoInsert(hf1, 5);// T2 rollBackdontInsert(hf1, 6);Transaction t = Database.newTransaction();t.start();// 此时5不存在,大家可以想想是哪一步出现问题了look(hf1, t, 5, true);look(hf1, t, 6, false);t.commit();}void doInsert(HeapFile hf, int t1)throws DbException, TransactionAbortedException, IOException {Transaction t = Database.newTransaction();t.start();// 插入5,假设插入到了page2上insertRow(hf, t, t1);// 刷新page2到磁盘上 -- 此时事务t1还为提交Database.getBufferPool().flushAllPages();// 提交事务t1    t.commit();} void dontInsert(HeapFile hf, int t1)throws DbException, TransactionAbortedException, IOException {Transaction t = Database.newTransaction();t.start();insertRow(hf, t, t1);abort(t);}

事务t1插入一条记录5后,调用flushAllPages,此时记录5所在page one落盘,前置镜像此时没有变化,但是此时脏页都被落盘了,所以page cache中已无脏页,然后事务t1调用commit方法完成事务提交,commit方法中调用flushPages方法发现没有脏页需要刷盘,直接返回:
在这里插入图片描述
但是此时按理来说,应该将前置镜像更新为5:

在这里插入图片描述
此时,如果事务2调用abort函数进行rollback,abort函数中会首先调用flushAllPages将所有脏页刷盘,并在刷盘前记录日志:

在这里插入图片描述
然后事务2进行回滚,应用最后一条update的前置镜像,很显然此次回滚结果不对,问题出在事务1进行commit的时候,没有更新前置镜像。

事务1之所以在事务提交时没有更新前置镜像是因为事务1在commit前调用了flushAllPages方法,将所有的脏页都提前落盘了,真正进行commit的时候发现没有脏页可以更新,也就没有进入if逻辑,从而也就没有更新前置镜像。


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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Nodejs安装及环境变量配置(修改全局安装依赖工具包和缓存文件夹及npm镜像源)

本机环境&#xff1a;win11家庭中文版 一、官网下载 二、安装 三、查看nodejs及npm版本号 1、查看node版本号 node -v 2、查看NPM版本号&#xff08;安装nodejs时已自动安装npm&#xff09; npm -v 四、配置npm全局下载工具包和缓存目录 1、查看安装目录 在本目录下创建no…

【学习日记】【FreeRTOS】手动任务切换详解

前言 本文是关于 FreeRTOS 中实现两个任务轮流切换并执行的代码详解。目前不支持优先级&#xff0c;仅实现两个任务轮流切换。 一、任务的自传 任务从生到死的过程究竟是怎么样的呢&#xff1f;&#xff08;其实也没死&#xff09;&#xff0c;这个问题一直困扰着我&#xf…

【前端 | CSS】滚动到底部加载,滚动监听、懒加载

背景 在日常开发过程中&#xff0c;我们会遇到图片懒加载的功能&#xff0c;基本原理是&#xff0c;滚动条滚动到底部后再次获取数据进行渲染。 那怎么判断滚动条是否滚动到底部呢&#xff1f;滚动条滚动到底部触发时间的时机和方法又该怎样定义&#xff1f; 针对以上问题我…

关于技术转管理角色的认知

软件质量保障&#xff1a;所寫即所思&#xff5c;一个阿里质量人对测试的所感所悟。 程序员发展的岔路口 技术人做了几年专业工作之后&#xff0c;会来到一个重要的“分岔路口”&#xff0c;一边是专业的技术路线&#xff0c;一边是技术团队的管理路线。不少人就开始犯难&…

redis 数据结构(一)

Redis 为什么那么快 redis是一种内存数据库&#xff0c;所有的操作都是在内存中进行的&#xff0c;还有一种重要原因是&#xff1a;它的数据结构的设计对数据进行增删查改操作很高效。 redis的数据结构是什么 redis数据结构是对redis键值对值的数据类型的底层的实现&#xff0c…

网站SSL安全证书是什么及其重要性

网站SSL安全证书具体来说是一个数字文件&#xff0c;是由受信任的数字证书颁发机构&#xff08;CA机构&#xff09;进行审核颁发的&#xff0c;其中包含CA发布的信息&#xff0c;该信息表明该网站已使用加密连接进行了安全保护。 网站SSL安全证书也被称为SSL证书、https证书和…

CosmosAI欧盟数字超算新时代战略合作签约仪式在伦敦举行

据英国权威媒体获悉&#xff0c;由分布式超算网络服务商CosmosAI主办的欧盟数字超算新时代战略合作签约仪式将于8月14日英国伦敦历史悠久的莱福士OWO酒店隆重举办&#xff0c;该酒店曾作为爱德华七世国王加冕仪式以及丘吉尔二战办公室享誉盛名。 本次活动CosmosAI基金会联合创…

护网专题简单介绍

护网专题简单介绍 一、护网红蓝队介绍1.1、网络安全大事件1.2、护网行动由来1.3、护网行动中的角色二、红队介绍2.1、红队所需技能2.2、红队攻击流程 三、蓝队介绍3.1、蓝队所需技能3.2、蓝队防守四阶段3.3、蓝队前期准备 四、常见安全厂商介绍4.1、常见安全厂商 五、常见安全产…

【vue3】基础知识点-pinia

学习vue3&#xff0c;都会从基础知识点学起。了解setup函数&#xff0c;ref&#xff0c;recative&#xff0c;watch、computed、pinia等如何使用 今天说vue3组合式api&#xff0c;pinia 戳这里&#xff0c;跳转pinia中文文档 官网的基础示例中提供了三种写法 1、选择式api&a…

树莓派命令行运行调用音频文件的函数,不报错,没有声音解决办法

树莓派接上音频首先需要切换音频不是HDMI&#xff0c;然后可以双击运行wav文件可以播放&#xff0c;但是&#xff1a; 命令行直接运行wav文件报错&#xff1a; Playing WAVE twzc.wav : Signed 16 bit Little Endian, Rate 16000 Hz, Mono命令行运行main方法也是无法播放&am…

Zebec Protocol 将进军尼泊尔市场,通过 Zebec Card 推动地区金融平等

流支付正在成为一种全新的支付形态&#xff0c;Zebec Protocol 作为流支付的主要推崇者&#xff0c;正在积极的推动该支付方案向更广泛的应用场景拓展。目前&#xff0c;Zebec Protocol 成功的将流支付应用在薪酬支付领域&#xff0c;并通过收购 WageLink 将其纳入旗下&#xf…

消息中间件 —— 初识Kafka

文章目录 1、Kafka简介1.1、消息队列1.1.1、为什么要有消息队列&#xff1f;1.1.2、消息队列1.1.3、消息队列的分类1.1.4、p2p 和 发布订阅MQ的比较1.1.5、消息系统的使用场景1.1.6、常见的消息系统 1.2、Kafka简介1.2.1、简介1.2.2、设计目标1.2.3、kafka核心的概念 2、Kafka的…

git 报错 protocol ‘https‘ is not supported解决

报错原因&#xff1a;选择不了其他分支代码&#xff0c;甚至都看不到其他分支&#xff0c;我这边解决了两次报错&#xff0c;情况如下&#xff1a; 第一种报错&#xff1a; idea中刷新分支报错如下&#xff1a; Fetch Failed protocol https is not supported 话不多说&#…

有没有推荐的golang的练手项目?

前言 下面是github上的golang项目&#xff0c;适合练手&#xff0c;可以自己选择一些项目去练习&#xff0c;整理不易&#xff0c;希望能多多点赞收藏一下&#xff01;废话少说&#xff0c;我们直接进入正题>>> 先推荐几个教程性质的项目&#xff08;用于新手学习、巩…

Qt在mac安装

先在app store下载好Xcode 打开Xcode 随便建个文件 给它取个名字 找个地方放 提醒没建立git link,不用理他 打开终端&#xff0c; 输入/usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)" 开始安装啦 继续在终端…

【Uni-App】uview 开发多端应用,密码显示隐藏功能不生效问题

出现的问题&#xff1a; 使用uview组件u-input框密码绑定时会出现右侧密码显隐图标不显示的问题 思路&#xff1a; 1.看了下uview源码&#xff0c;发现这有一段注释&#xff0c;我们需要把源码修改一下&#xff0c;问题出在这里 这行代码修改为 :password"password || …

山东布谷科技直播系统源码热点分析:不同芯片实现高质量编码与渲染视频的GPU加速功能

在现代科技的迅猛发展下&#xff0c;直播系统源码平台被开发搭建出来&#xff0c;为人们的生活方式带来了很大的改变&#xff0c;直播系统源码平台的好友、短视频、直播、社区等功能让很多人越来越热衷于去在平台上刷视频、看直播、分享生活。用户的喜爱也督促了直播系统源码平…

【数据结构与算法】十大经典排序算法-快速排序

&#x1f31f;个人博客&#xff1a;www.hellocode.top &#x1f3f0;Java知识导航&#xff1a;Java-Navigate &#x1f525;CSDN&#xff1a;HelloCode. &#x1f31e;知乎&#xff1a;HelloCode &#x1f334;掘金&#xff1a;HelloCode ⚡如有问题&#xff0c;欢迎指正&#…

中国钢铁工业协会 :2022年钢铁行业经济运行报告(附下载)

关于报告的所有内容&#xff0c;公众【营销人星球】获取下载查看 核心观点 2022年&#xff0c;我国粗钢产量10.18亿吨&#xff0c;比上年下降1.7%&#xff0c;连续两年下降&#xff0c;降幅比上年收窄。2022年&#xff0c;出口钢材 6732万吃&#xff0c;比上年增长0.9%;进口钢…

24届近5年南京大学自动化考研院校分析

今天给大家带来的是南京大学控制考研分析 满满干货&#xff5e;还不快快点赞收藏 一、南京大学 学校简介 南京大学是一所历史悠久、声誉卓著的高等学府。其前身是创建于1902年的三江师范学堂&#xff0c;此后历经两江师范学堂、南京高等师范学校、国立东南大学、国立第四中…