P4 并发控制
文章目录
- Task1 锁管理器
- LockTable
- UnLockTable
- LockRow
- UnLockRow
- Task2 死锁检测
- Task3 并发查询执行器
- Isolation Level
- seq_scan_executor
- insert_executor
- delete_executor
- transaction_manager
Task1 锁管理器
LockManager
类包含两个属性类,分别是LockRequest
和LockRequestQueue
,行锁请求还是表锁请求都适配LockRequest
,其属性:
/** Txn_id of the txn requesting the lock */
txn_id_t txn_id_;
/** 请求锁的锁定模式 */
LockMode lock_mode_;
/** Oid of the table for a table lock; oid of the table the row belong to for a row lock */
table_oid_t oid_; // 若是表锁则表示表的Oid;若是行锁则表示行所属表的oid
/** 为行锁时是行RID;未用于表锁 */
RID rid_; // RID应该是唯一的
/** 锁是否被授予 */
bool granted_{false};
一个资源(行或表)可能有多个事务进行请求,所以有了LockRequestQueue
来先入先出地处理来的请求,其属性:
/** 同一资源(表或行)的锁请求列表 */
std::list<std::shared_ptr<LockRequest>> request_queue_;/** 通知此rid上被阻止的事务 */
std::condition_variable cv_; // 条件变量
/** 升级事务的TXN id(如有) */
txn_id_t upgrading_ = INVALID_TXN_ID; // 正在此资源上尝试锁升级的事务id.
/** 配合 */
std::mutex latch_; // 与cv_配合使用实现等待资源,即这边锁住,cv_那边等待std::vector<size_t> lock_vector_; // 该表或该行已授予的各个锁的数量,5种锁
LockManager
类的属性:
TransactionManager *txn_manager_;/** 结构,用于保存给定表oid的锁请求 */
std::unordered_map<table_oid_t, std::shared_ptr<LockRequestQueue>> table_lock_map_; // 每个表对应的的锁队列
/** Coordination */
std::mutex table_lock_map_latch_;/** 结构,用于保存给定RID的锁请求 */
std::unordered_map<RID, std::shared_ptr<LockRequestQueue>> row_lock_map_;
/** Coordination */
std::mutex row_lock_map_latch_;std::atomic<bool> enable_cycle_detection_;
std::thread *cycle_detection_thread_;/** 等待图表示。 */
// std::unordered_map<txn_id_t, std::vector<txn_id_t>> waits_for_;
std::mutex waits_for_latch_;
// 重新设计wait_for_,使得若t1等待t2的资源,令t2指向t1,释放t2时减少所有等待t2的事务的入度
std::map<txn_id_t, std::set<txn_id_t>> waits_for_; // 保证每次都从txn_id_t最小的事务开始扫描
std::map<txn_id_t, int> visited_; // 表示一个节点未被访问过(0),正在访问(1),已经被访问完(2)
bool hascycle_; // 是否有环
std::unordered_map<txn_id_t, std::unordered_set<table_oid_t>> map_txn_table_; // 记录事务锁住的表
std::unordered_map<txn_id_t, std::unordered_set<RID>> map_txn_row_; // 记录事务锁住的行
LockTable
//lock_manager.h的LOCK_NOTE(部分)
/**
REPEATABLE_READ:事务需要获取所有锁。所有锁都允许处于GROWING状态在收缩状态下不允许有锁
READ_COMMITTED:事务需要获取所有锁。所有锁都允许处于GROWING状态只有IS、S锁允许处于收缩状态
READ_UNCOMMITTED:事务只需要接收IX、X锁。在GROWING状态下允许使用X、IX锁。S, IS, SIX是不允许的
**/
DealLockOfTable()
要做的就是进行加锁的准备工作。先将table_lock_map_
属性加锁,其表示表oid和锁请求队列间的映射,若其中无对应oid的内容则初始化oid的键值对。解锁table_lock_map_
加锁表的锁请求队列,查找该表的锁队列,当前事务是否曾请求过,若未请求过就新建一个锁请求std::make_shared<LockRequest>(txn_id, lock_mode, oid);
插入锁队列中,调用WaitForLock()
返回。若已锁住该表,若这次请求的锁和之前的模式一样,将队列解锁,返回true。剩下的情况就是两次锁模式不同,尝试锁升级(说是升级,其实是锁模式切换,去旧换新),若有其他事务在升级该表的其他锁(),则解锁队列、设置事务状态为回滚、抛出UPGRADE_CONFLICT
异常。取得队列中旧锁请求,判断锁升级是否兼容:
//lock_manager.h
/**
LOCK UPGRADE:在已经锁定的资源上调用Lock()应该具有以下行为:- 如果请求的锁模式与当前持有的锁模式相同,lock()应该返回true,因为它已经拥有该锁。- 如果请求的锁模式不同,lock()应该升级事务持有的锁。基本上应该有三个步骤来执行锁升级一般- 1。检查升级的前提条件- 2。放下当前锁,保留升级位置- 3。等待新锁授予正在升级的锁请求应该优先于同一资源上等待的其他锁请求。在升级时,只允许以下转换:IS -> [S, X, IX, SIX]S -> [X, SIX]IX -> [X, SIX]SIX-> [X]任何其他升级都被认为是不兼容的,这样的尝试应该将TransactionState设置为ABORTED,并抛出TransactionAbortException (INCOMPATIBLE_UPGRADE)。此外,应该只允许一个事务在给定资源上升级其锁。同一资源上的多个并发锁升级应该将TransactionState设置为ABORTED,并抛出TransactionAbortException (UPGRADE_CONFLICT)。
**/
升级锁其实就是将锁请求队列中lock_vector_
旧锁模式对应的计数减一,然后将reuest_queue_
中的旧锁删除(erase()
),调用RemoveFromSet()
将事务中与旧锁模式相对应的表锁集合中的表oid删除,恢复granted_
、设置upgrading_
为当前事务id、更新锁模式、锁请求队列request_queue_
中添加新锁请求,调用WaitForLock()
等待授予锁。
WaitForLock()
就是真正授予锁的函数了,前面一个while循环是为了阻塞其他线程进行加锁,之后和清除旧锁过程类似,granted_
置true、调用InsertToSet()
将表oid插入表锁集合中、锁队列的lock_vector_
中对应锁模式的计数器加一、将upgrading_
恢复。
WaitForLock()
开头的while循环作用是实现线程的阻塞等待锁资源。具体来说,它使用了一个条件变量cv_
和一个互斥锁std::unique_lock<std::mutex> mylock(lock_queue->latch_, std::adopt_lock);
(std::adopt_lock
托管了lock_queue->latch_
给unique_lock
)来实现线程的阻塞和唤醒。循环条件是CanGrantLock()
返回false,其作用是根据upgranding_
(当前请求对应的事务正在升级锁,是最高优先级)和锁请求在request_queue_
中的顺序(没有事务正在升级锁,迭代器遍历找到第一个与其他所有已被授予的锁兼容CompatibleLock()
但还未被授予!granted_
的那个锁请求)判断当前请求现在是否适合获得锁。
只要不满足条件,进入循环,lock_queue->cv_.wait(mylock);
阻塞并释放托管的lock_queue->latch
直到解锁表的时候notify_all()
唤醒,之后检测事务状态,若是ABORTED
则直接从request_queue_
中删除,因为处理死锁的线程会把死锁的事务的状态置为ABORTED
UnLockTable
分别处理:
table_lock_map_
中没有表oid到锁请求队列的映射:ATTEMPTED_UNLOCK_BUT_NO_LOCK_HELD
异常HaveRowLock()
通过获取事务的两种行锁集合(s_row_lock_set_
和x_row_lock_set_
)检测该事务在该表oid的行上有锁未释放:TABLE_UNLOCKED_BEFORE_UNLOCKING_ROWS
异常- 迭代器遍历
requeset_queue_
检测事务没有表oid的锁:ATTEMPTED_UNLOCK_BUT_NO_LOCK_HELD
异常
处理完异常状态后:
之后真正进入解锁代码,同加锁,lock_vector_
对应锁模式加一,删除request_queue_
中锁请求,RemoveFromSet()
删除事务的特定锁模式对应的表锁集合中的表oid,唤醒所有线程,解锁锁请求队列。
LockRow
加行锁和加表锁类似,区别在于:
- 行锁不支持意愿锁
- 收缩阶段时,加X锁直接
LOCK_ON_SHRINKING
异常 - 收缩阶段,可重复读和读未提交都不允许加锁,
LOCK_ON_SHRINKING
和LOCK_SHARED_ON_READ_UNCOMMITTED
异常 - 收缩阶段,读已提交只允许加S锁
DealLockOfRow()
与DealLockOfTable()
类似,区别在于:
- 行锁开头需要检查本事务是否有相应表锁
CheckTableLock()
(取事务的行锁集合查找对应表的oid)没有就直接抛异常TABLE_LOCK_NOT_PRESENT
- 升级锁时,因为只有两种锁,所以判断兼容就只需要判断不能是X锁转S锁即可
WaitForRowLock()
与WaitForLock()
极其相似,逻辑相同。
UnLockRow
解锁与解表锁同。
Task2 死锁检测
锁管理器应该在后台线程中运行死锁检测,定期构建等待图,并根据需要中止事务以消除死锁。
您可能需要从成员变量 txn_manager_ 访问事务的状态。如果’ txn_manager_ ‘被设置为’ nullptr ', ’ StartDeadlockDetection '将不会被调用,并且您不需要检测死锁。
/** 等待图表示。 */
std::mutex waits_for_latch_;
// 重新设计wait_for_,使得若t1等待t2的资源,令t2指向t1,释放t2时减少所有等待t2的事务的入度
std::map<txn_id_t, std::set<txn_id_t>> waits_for_; // 保证每次都从txn_id_t最小的事务开始扫描
std::map<txn_id_t, int> visited_; // 表示一个节点未被访问过(0),正在访问(1),已经被访问完(2)
//visited_的下标和waits_for_中的key和value集合里的值是匹配的
bool hascycle_; // 是否有环
std::unordered_map<txn_id_t, std::unordered_set<table_oid_t>> map_txn_table_; // 记录事务锁住的表
std::unordered_map<txn_id_t, std::unordered_set<RID>> map_txn_row_; // 记录事务锁住的行
等待图一般都是等待的事务指向被等待的事务,但这里反过来了,因为之后需要当死锁时删除等待的事务,这样更快。
AddEdge()
将等待的事务插入insert()
进被等待的事务key对应的集合中。
RemoveEdge()
删除erase()
事务。
HasCycle()
判断等待图中是否有环,先将访问列表visited_
中所有结点初始化为0,之后遍历waits_for_
,只要key事务未访问过就DFS()
,使用的是深度优先搜索:
// 从txn_id开始深度优先遍历
void LockManager::DFS(txn_id_t txn_id) {visited_[txn_id] = 1; // 该节点正在被访问for (const txn_id_t id : waits_for_[txn_id]) {//遍历等待该事务的事务集合if (visited_[id] == 0) {DFS(id);if (hascycle_) {//当递归找到一个环,直接返回return;}} else if (visited_[id] == 1) {hascycle_ = true; // id被访问过, 现在又将被访问, 有环return;}}visited_[txn_id] = 2; // 所有与txn_id相关的节点均已被访问,改为2, 无环
}
DFS()
返回后若找到一个环就遍历visited_
,如果是无环的事务结点会被改置为2,还为1的只会是return返回的有环结点,将该结点赋给入参,返回true。
GetEdgeList()
嵌套循环waits_for_
将边记录下返回。
RunCycleDetection()
死锁检验线程的运行函数:
std::thread *cycle_detection_thread_;
std::atomic<bool> enable_cycle_detection_;void StartDeadlockDetection() {BUSTUB_ENSURE(txn_manager_ != nullptr, "txn_manager_ is not set.")enable_cycle_detection_ = true;cycle_detection_thread_ = new std::thread(&LockManager::RunCycleDetection, this);
}
RunCycleDetection()
是一个循环函数,由enable_cycle_detection_
控制,每次循环都会清空并重新创建等待图waits_for_
并记录事务锁住的表和行(map_txn_table_
和map_txn_row_
),然后内部一个循环,由HasCycle()
控制,只要有死锁就设置死锁事务状态为回滚,删除waits_for_
中死锁事务相关的边(即等待关系),然后从死锁事务锁住的表和行(map_txn_table_
和map_txn_row_
)中获取表和行的id,从tabe_lock_map_
和row_lock_map_
中获得死锁锁住的表和行的锁请求队列,lock_vector_
中该死锁事务的锁模式计数器减一,RemoFromSet()
和RemoveFromRowSet()
删除锁集合中的表oid和rid,删除锁请求队列中死锁事务的请求,lock_queue->cv_.notify_all();
唤醒锁住的表和行的其他请求线程。
Task3 并发查询执行器
Isolation Level
- 一个事务应该为所有写操作持有X锁,直到它提交或终止,无论它的隔离级别如何。
- 对于 REPEATABLE_READ,一个事务应该为所有读操作持有S锁,直到提交或终止。
- 对于 READ_COMMITTED,一个事务应该为所有读操作占用S锁,但可以立即释放它们。
- 对于 READ_UNCOMMITTED,事务不需要为读操作获取任何S锁。
seq_scan_executor
Init()
锁表,Next()
锁表
//Init()开头
isl_ = exec_ctx_->GetTransaction()->GetIsolationLevel();
if (exec_ctx_->IsDelete()) { // 当前操作是delete或updateTryLockTable(LockManager::LockMode::INTENTION_EXCLUSIVE, plan_->table_oid_); // IX锁
} else {if (isl_ == IsolationLevel::REPEATABLE_READ || isl_ == IsolationLevel::READ_COMMITTED) {TryLockTable(LockManager::LockMode::INTENTION_SHARED, plan_->table_oid_); // IS锁}
}//Next()开头
if (exec_ctx_->IsDelete()) {TryLockRow(LockManager::LockMode::EXCLUSIVE, plan_->table_oid_, it_->GetRID()); // 删除或更新操作加写锁
} else {if (isl_ == IsolationLevel::REPEATABLE_READ || isl_ == IsolationLevel::READ_COMMITTED) {TryLockRow(LockManager::LockMode::SHARED, plan_->table_oid_, it_->GetRID()); // 只读操作加读锁}
}
TryLockXXX()
是将加锁函数try-catch包围,拼凑出错时的异常信息,解锁TryUnLockXXX()
同理。
在Next()
获取元组后,判断元组未删除(pair.first.is_deleted_
)若执行器上下文属性exec_ctx_
的is_delete_
(标识这次查询表是否是为了删除/更改表中数据)为false,且隔离级别为READ_COMMITTED
,就立即解锁行。已删除时直接解锁行。当查询到表末尾后,隔离状态是READ_COMMITTED
且执行器不会删除/修改数据,则解锁表。
insert_executor
try {if (!exec_ctx_->GetLockManager()->LockTable(exec_ctx_->GetTransaction(), LockManager::LockMode::INTENTION_EXCLUSIVE,plan_->table_oid_)) {throw ExecutionException(std::string("Insert Table Ix lock fail"));}
} catch (TransactionAbortException &e) {throw ExecutionException(std::string("Insert Table Ix lock fail"));
}
delete_executor
如果你在执行器context中正确地基于’ IsDelete() ‘实现了’ SeqScanExecutor ',那么你不需要在这个执行器中使用任何锁。
transaction_manager
主要实现两个函数:TransactionManager::Abort()
和TransactionManager::Commit()
Commit()
先调用ReleaseLocks()
释放事务持有的所有锁:先从事务的各个锁集合中收集行、表锁到新容器中,然后嵌套循环解锁行锁、循环解锁表锁。之后设置事务状态为COMMITTED
Abort()
负责恢复事务的写集中的所有更改。每次对表进行插入、删除时都会在事务的table_write_set_
和index_write_set_
中插入一条TableWriteRecord
和IndexWriteRecord
,其中有操作的各种信息(如操作的表oid、对应的表堆、行rid,索引oid等)。遍历两个写集合,对里面存放的修改过的元组(主要是插入和删除的)的元信息进行回退(设置is_deleted_
),然后弹出集合,设置事务状态为ABOTTED
相关文章:
P4 并发控制
文章目录 Task1 锁管理器LockTableUnLockTableLockRowUnLockRow Task2 死锁检测Task3 并发查询执行器Isolation Levelseq_scan_executorinsert_executordelete_executortransaction_manager Task1 锁管理器 LockManager类包含两个属性类,分别是LockRequest和LockRe…...
友元的介绍
实现外部类和外部函数存取类的私有成员和保护成员的方法。 一、友元函数 可访问类所有成员的外部函数 //求两点间的距离:抽象点——>求距离的函数 #include<iostream> #include<cmath> using namespace std; class Point{private:double x,y;publ…...

新手如何找到Docker容器(redis)中的持久化文件?
具体步骤 要查看Docker容器的dump.rdb和appendonly.aof文件(如果启用了AOF持久化)的位置,我们需要知道容器中Redis配置文件的内容或者容器的数据卷的挂载位置。 这里是一般步骤: 查找容器的数据卷挂载位置 使用docker inspect命令…...

python二次开发Solidworks:读取立方体的高度
在SW中新建一个零件文档,建立一个立方体,长度和宽度自定义,高度100mm,下面通过python实现读取该立方体的高度: import win32com.client as win32 import pythoncomswApp win32.Dispatch(sldworks.application) swApp.…...
NPM安装后报错:ERROR: npm v10.2.1 is known not to run on Node.js v10.24.1.
问题描述 NPM卸载高版本后安装低版本运行报错: C:\Users\Administrator>npm -v ERROR: npm v10.2.1 is known not to run on Node.js v10.24.1. This version of npm supports the following node versions: ^18.17.0 || >20.5.0. You can find the latest…...
【Vue】Element开发笔记
Element开发笔记 前言 官网 https://element.eleme.cn/#/zh-CN/component/upload 其它项目网站 https://www.cnblogs.com/qq2806933146xiaobai/p/17180878.html 表格 序号列添加 <el-table-column type"index" :index"handleIndexCalc" label&qu…...
How to install mongodb 7.0 to Ubuntu 22.04
How to install mongodb 7.0 to Ubuntu 22.04 1、安装1.1、添加gpg1.2、添加apt源1.3、更新1.4、安装 2、管理2.1、服务管理2.1.1、查看服务状态2.1.2、启动服务2.1.3、 设置服务为开机启动2.1.4、取消服务开机启动2.1.5、关闭服务2.1.6、服务重启 2.2、mongosh2.2.1、进入mong…...

AFL安全漏洞挖掘
安全之安全(security)博客目录导读 ATF(TF-A)/OPTEE之FUZZ安全漏洞挖掘汇总 目录 一、AFL简介 二、AFL的安装 三、代码示例及种子语料库 四、AFL插桩编译 五、AFL运行及测试 六、AFL结果分析 一、AFL简介 模糊测试(Fuzzing)技术作为漏洞挖掘最有…...
ES6 let const var和解构赋值
1.let/const和var的区别 1.变量提升:var会发生变量提升,let和const不存在变量提升 2.暂时性死区:变量声明之前变量不可用称为暂时性死区。var不存在,let和const存在暂时性死区 3.typeof 不再是百分百不会报错:let声…...

03、MySQL-------数据库中间件MyCat实现读写分离
目录 八、数据库中间件MyCat实现读写分离1、克隆:2、配置分析:1、schema.xml2、启动mycat:3、关闭防火墙:4、代码测试:测试写入:测试读:强制读Master 八、数据库中间件MyCat实现读写分离 作用&…...

虹科活动 | 探索全新AR应用时代,虹科AR VIP研讨会广州场回顾!
文章来源:虹科数字化AR 阅读原文:https://mp.weixin.qq.com/s/7tmYR42Tw5XLn70fm8Nnew 主题演讲 本次研讨会,虹科特邀 “工业AR鼻祖” 美国Vuzix公司的首席应用工程师郑慎方先生进行主题演讲,并邀请到了各界的专业人士和企业代表参…...

非科班,补基础
大家好,我是大彬~ 今天跟大家分享知识星球小伙伴关于【非科班转码如何补基础】的提问。 往期星球提问整理: 读博?找工作? 性格测试真的很重要 想找一份实习工作,需要准备什么 球友提问: 大彬大佬…...
安全性与合规性:IT运维的双重守护
在当今数字化时代,信息技术(IT)已深入渗透到企业的各个层面。随着数据和技术的不断发展,IT运维安全性和合规性逐渐成为了企业运营的两大核心要素。它们不仅保障了企业数据的安全,更维护了公司的声誉和客户信任。本文将…...

session 反序列化
原理详解 ctfshow web 263 ctfshow 新手杯 剪刀石头布 这里我们可以发现服务器使用的处理器为php_serialize,与当前页面处理器不同,在反序列化的时候会造成一些问题。同时cleanup配置没开,关闭了session自动清理,所以我们不需要…...
PostgreSQL中实现数学中的组合问题
记得在学《数值分析》这门课程时,其中有排列组合的相关计算。最近没有想到,自己居然在编程上需要了这个问题,需要在数据库的一张表中,找出任意n条记录的组合,判断组合后的图形是什么,也就是组合问题。从网上搜索的相关资料,发现很多是在Java或者python代码实现的。因为数…...

编译和连接
前言:哈喽小伙伴们,从我们开始学习C语言到实现如今的成果,可以说我们对C语言的掌握已经算是精通级别了,但是我们只学习了怎么写代码,却没怎么了解过代码的背后是怎么工作的。 那么今天这篇文章我们一起来学习C语言的最…...

常见分布整理
概率论 - 常见分布(及其分布表) 常见分布的期望和方差 离散型分布 两点分布 有2种结果,实验只做1次 X~b(1,p)则有 P(X k) pk (1-p)1-k,k 0, 1 数学期望:E(X) p 方差:D(X)p(1-p) 二项分布 P(A) p࿰…...

ubuntu终端命令行下如何使用NetworkManager(netplan)来配置wifi网络
最近在给家里折腾一个文件共享服务器给家里的小米摄像头保存监控视频用。树莓派太贵了,找来找去发现香橙派orangepi zero3 是最低成本的替代解决方案(网络足够快,CPU的IO能力足够强),香橙派orangepi zero3的操作系统是…...

GO学习之 goroutine的调度原理
GO系列 1、GO学习之Hello World 2、GO学习之入门语法 3、GO学习之切片操作 4、GO学习之 Map 操作 5、GO学习之 结构体 操作 6、GO学习之 通道(Channel) 7、GO学习之 多线程(goroutine) 8、GO学习之 函数(Function) 9、GO学习之 接口(Interface) 10、GO学习之 网络通信(Net/Htt…...

CUDA学习笔记5——CUDA程序错误检测
CUDA程序错误检测 所有CUDA的API函数都有一个类型为cudaError_t的返回值,代表了一种错误信息;只有返回cudaSuccess时,才是成功调用。 cudaGetLastError()用来检测核函数的执行是否出错cudaGetErrorString()输出错误信息 #include <stdi…...

UE5 学习系列(二)用户操作界面及介绍
这篇博客是 UE5 学习系列博客的第二篇,在第一篇的基础上展开这篇内容。博客参考的 B 站视频资料和第一篇的链接如下: 【Note】:如果你已经完成安装等操作,可以只执行第一篇博客中 2. 新建一个空白游戏项目 章节操作,重…...
Cursor实现用excel数据填充word模版的方法
cursor主页:https://www.cursor.com/ 任务目标:把excel格式的数据里的单元格,按照某一个固定模版填充到word中 文章目录 注意事项逐步生成程序1. 确定格式2. 调试程序 注意事项 直接给一个excel文件和最终呈现的word文件的示例,…...

微软PowerBI考试 PL300-选择 Power BI 模型框架【附练习数据】
微软PowerBI考试 PL300-选择 Power BI 模型框架 20 多年来,Microsoft 持续对企业商业智能 (BI) 进行大量投资。 Azure Analysis Services (AAS) 和 SQL Server Analysis Services (SSAS) 基于无数企业使用的成熟的 BI 数据建模技术。 同样的技术也是 Power BI 数据…...

华为云Flexus+DeepSeek征文|DeepSeek-V3/R1 商用服务开通全流程与本地部署搭建
华为云FlexusDeepSeek征文|DeepSeek-V3/R1 商用服务开通全流程与本地部署搭建 前言 如今大模型其性能出色,华为云 ModelArts Studio_MaaS大模型即服务平台华为云内置了大模型,能助力我们轻松驾驭 DeepSeek-V3/R1,本文中将分享如何…...

GC1808高性能24位立体声音频ADC芯片解析
1. 芯片概述 GC1808是一款24位立体声音频模数转换器(ADC),支持8kHz~96kHz采样率,集成Δ-Σ调制器、数字抗混叠滤波器和高通滤波器,适用于高保真音频采集场景。 2. 核心特性 高精度:24位分辨率,…...
ip子接口配置及删除
配置永久生效的子接口,2个IP 都可以登录你这一台服务器。重启不失效。 永久的 [应用] vi /etc/sysconfig/network-scripts/ifcfg-eth0修改文件内内容 TYPE"Ethernet" BOOTPROTO"none" NAME"eth0" DEVICE"eth0" ONBOOT&q…...

20个超级好用的 CSS 动画库
分享 20 个最佳 CSS 动画库。 它们中的大多数将生成纯 CSS 代码,而不需要任何外部库。 1.Animate.css 一个开箱即用型的跨浏览器动画库,可供你在项目中使用。 2.Magic Animations CSS3 一组简单的动画,可以包含在你的网页或应用项目中。 3.An…...

DingDing机器人群消息推送
文章目录 1 新建机器人2 API文档说明3 代码编写 1 新建机器人 点击群设置 下滑到群管理的机器人,点击进入 添加机器人 选择自定义Webhook服务 点击添加 设置安全设置,详见说明文档 成功后,记录Webhook 2 API文档说明 点击设置说明 查看自…...

破解路内监管盲区:免布线低位视频桩重塑停车管理新标准
城市路内停车管理常因行道树遮挡、高位设备盲区等问题,导致车牌识别率低、逃费率高,传统模式在复杂路段束手无策。免布线低位视频桩凭借超低视角部署与智能算法,正成为破局关键。该设备安装于车位侧方0.5-0.7米高度,直接规避树枝遮…...
【Kafka】Kafka从入门到实战:构建高吞吐量分布式消息系统
Kafka从入门到实战:构建高吞吐量分布式消息系统 一、Kafka概述 Apache Kafka是一个分布式流处理平台,最初由LinkedIn开发,后成为Apache顶级项目。它被设计用于高吞吐量、低延迟的消息处理,能够处理来自多个生产者的海量数据,并将这些数据实时传递给消费者。 Kafka核心特…...