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…...
IDEA运行Tomcat出现乱码问题解决汇总
最近正值期末周,有很多同学在写期末Java web作业时,运行tomcat出现乱码问题,经过多次解决与研究,我做了如下整理: 原因: IDEA本身编码与tomcat的编码与Windows编码不同导致,Windows 系统控制台…...
label-studio的使用教程(导入本地路径)
文章目录 1. 准备环境2. 脚本启动2.1 Windows2.2 Linux 3. 安装label-studio机器学习后端3.1 pip安装(推荐)3.2 GitHub仓库安装 4. 后端配置4.1 yolo环境4.2 引入后端模型4.3 修改脚本4.4 启动后端 5. 标注工程5.1 创建工程5.2 配置图片路径5.3 配置工程类型标签5.4 配置模型5.…...
【力扣数据库知识手册笔记】索引
索引 索引的优缺点 优点1. 通过创建唯一性索引,可以保证数据库表中每一行数据的唯一性。2. 可以加快数据的检索速度(创建索引的主要原因)。3. 可以加速表和表之间的连接,实现数据的参考完整性。4. 可以在查询过程中,…...
.Net框架,除了EF还有很多很多......
文章目录 1. 引言2. Dapper2.1 概述与设计原理2.2 核心功能与代码示例基本查询多映射查询存储过程调用 2.3 性能优化原理2.4 适用场景 3. NHibernate3.1 概述与架构设计3.2 映射配置示例Fluent映射XML映射 3.3 查询示例HQL查询Criteria APILINQ提供程序 3.4 高级特性3.5 适用场…...
解锁数据库简洁之道:FastAPI与SQLModel实战指南
在构建现代Web应用程序时,与数据库的交互无疑是核心环节。虽然传统的数据库操作方式(如直接编写SQL语句与psycopg2交互)赋予了我们精细的控制权,但在面对日益复杂的业务逻辑和快速迭代的需求时,这种方式的开发效率和可…...
cf2117E
原题链接:https://codeforces.com/contest/2117/problem/E 题目背景: 给定两个数组a,b,可以执行多次以下操作:选择 i (1 < i < n - 1),并设置 或,也可以在执行上述操作前执行一次删除任意 和 。求…...
在Ubuntu中设置开机自动运行(sudo)指令的指南
在Ubuntu系统中,有时需要在系统启动时自动执行某些命令,特别是需要 sudo权限的指令。为了实现这一功能,可以使用多种方法,包括编写Systemd服务、配置 rc.local文件或使用 cron任务计划。本文将详细介绍这些方法,并提供…...
十九、【用户管理与权限 - 篇一】后端基础:用户列表与角色模型的初步构建
【用户管理与权限 - 篇一】后端基础:用户列表与角色模型的初步构建 前言准备工作第一部分:回顾 Django 内置的 `User` 模型第二部分:设计并创建 `Role` 和 `UserProfile` 模型第三部分:创建 Serializers第四部分:创建 ViewSets第五部分:注册 API 路由第六部分:后端初步测…...
华为OD最新机试真题-数组组成的最小数字-OD统一考试(B卷)
题目描述 给定一个整型数组,请从该数组中选择3个元素 组成最小数字并输出 (如果数组长度小于3,则选择数组中所有元素来组成最小数字)。 输入描述 行用半角逗号分割的字符串记录的整型数组,0<数组长度<= 100,0<整数的取值范围<= 10000。 输出描述 由3个元素组成…...
Vue3 PC端 UI组件库我更推荐Naive UI
一、Vue3生态现状与UI库选择的重要性 随着Vue3的稳定发布和Composition API的广泛采用,前端开发者面临着UI组件库的重新选择。一个好的UI库不仅能提升开发效率,还能确保项目的长期可维护性。本文将对比三大主流Vue3 UI库(Naive UI、Element …...
