开源分布式数据库PolarDB-X源码解读——PolarDB-X源码解读(九):DDL的一生(上)

本文涉及的产品
云原生数据库 PolarDB PostgreSQL 版,标准版 2核4GB 50GB
云原生数据库 PolarDB MySQL 版,通用型 2核4GB 50GB
简介: 开源分布式数据库PolarDB-X源码解读——PolarDB-X源码解读(九):DDL的一生(上)

一、概述


一条SQL语句进入PolarDB-X的CN后,将经历协议层、优化器、执行器的完整处理流程。首先经过解析、鉴权、校验,被解析为关系代数树后,在优化器中经历RBO和CBO生成执行计划,最终在DN上执行完成。与DML不同的是,逻辑DDL语句还涉及对元数据的读写和物理DDL,直接影响系统状态一致性。


PolarDB-X的DDL实现的关键目标是DDL的“online”和“crash safe”,即DDL与DML的并发和DDL本身的原子性和持久性。对于复杂逻辑DDL(如加减全局二级索引、迁移分区数据),PolarDB-X沿袭online schema change的思路引入了CN中的双版本元数据,DDL仅在排空低版本事务时占用MDL锁,大大降低了阻塞业务SQL的频率;在执行器模块中,DDL引擎统一了逻辑DDL的定义方法和调度执行过程,开发者只须将物理操作封装为具有时序依赖关系和crash recover方法的Task传递给DDL引擎,后者保证相应的执行和回滚逻辑。


本文主要解读PolarDB-X DDL在计算节点(CN)中的实现,为了简便起见,本文中我们将略过DDL在协议层、优化器的预处理以及执行器中对Handler的分派过程,此部分内容可参考下列源码解读文章:


 PolarDB-X SQL的一生

 DML之Insert流程


我们将重点关注DDL在执行器中的执行流程,在阅读本文前,可预先阅读与PolarDB-X online schema change、元数据锁及DDL引擎原理相关的文章:  


PolarDB-X Online Schema Change

 PolarDB-X:让“Online DDL”更Online

 PolarDB-X DDL也要追求ACID  


二、整体流程


         


 一条逻辑DDL语句在解析后进入优化器,仅做简单的类型转化后生成logicalPlan及executionContext,由执行器根据logicalPlan的具体类型分派给对应的DDLHandler。DDLHandler的公共基类是LogicalCommonDdlHandler其公共执行入口见com.alibaba.polardbx.executor.handler.ddl.LogicalCommonDdlHandler#handle。


public Cursor handle(RelNode logicalPlan, ExecutionContext executionContext) {
        BaseDdlOperation logicalDdlPlan = (BaseDdlOperation) logicalPlan;
        initDdlContext(logicalDdlPlan, executionContext);
        // Validate the plan first and then return immediately if needed.
        boolean returnImmediately = validatePlan(logicalDdlPlan, executionContext);
        .....
        setPartitionDbIndexAndPhyTable(logicalDdlPlan);
        // Build a specific DDL job by subclass that override buildDdlJob
        DdlJob ddlJob = returnImmediately?
            new TransientDdlJob():
            // @override
            buildDdlJob(logicalDdlPlan, executionContext);
        // Validate the DDL job before request.
        // @override
        validateJob(logicalDdlPlan, ddlJob, executionContext);
        // Handle the client DDL request on the worker side.
        handleDdlRequest(ddlJob, executionContext);
        .....
        return buildResultCursor(logicalDdlPlan, executionContext);
    }


 在handle方法中,首先从executionContext中剥离出traceId、事务ID、ddl类型、全局配置等通用上下文信息,然后通过validateJob 方法校验DDL正确性,buildDdlJob方法构造DDL任务。具体的DDLHandler例如AlterTableHandler将会重载这两个方法,由开发者根据DDL的实际语义定义和校验DDL job。


 至此,DDL在handler中完成从logicalPlan到DDL job的转化。com.alibaba.polardbx.executor.handler.ddl.LogicalCommonDdlHandler#handleDdlRequest方法将DDL job对应的DDL Request转发给Leader CN节点上的DDL引擎调度执行。


 DDL引擎作为守护进程仅在Leader CN节点运行,轮询DDLJobQueue,队列非空即触发DDL Job的调度执行过程。DDL引擎中维持着两级队列,即实例级和库级的DDLJob队列,从而保证逻辑库间DDL并发执行,互不干扰。


值得注意的是,在DDL Requester将DDL Request推送至DDL引擎的过程中,实际上发生了一次Worker Node到Leader Node的节点间通信,该过程依赖于`com.alibaba.polardbx.gms.sync.GmsSyncManagerHelper#sync`方法。发起通信的节点在sync中封装syncAction作为信息,接收方监听到sync发生后根据syncAction执行相应的动作。这种节点间通信机制不仅存在于DDL引擎加载DDL job过程中,也存在于DDL job执行时节点间的元数据同步过程中(见后文),这两种情境下封装的syncAction均会触发接收方从MetaDB中拉取实际的通信内容(DDL job或者元信息),这种通信机制以数据库服务作为通信中介保证了通信内容传递的高可用和一致性。


DDL job是DDL执行引擎的核心概念,它实际上是DDL引擎层面模拟的“DDL事务”,尝试对元信息修改、节点间通信、物理DDL、DML等一系列异质操作组合而成的逻辑DDL实现原子性和持久性,从而保证系统状态的稳定性。借鉴于通用的事务实现,undolog、锁、WAL、版本快照等事务要素在DDL job和DDL引擎中也有着平行的实现。


以DDL引擎入口(即前述的handleDdlRequest方法)作为DDL数据流的分界点,可将DDL的生命周期划分为定义和执行两个部分。接下来,我们将以添加全局二级索引为例,说明在定义DDL Job时构建DDL原子性以及DDL与DML协同流程的关键逻辑。DDL在DDL引擎中的调度执行及错误处理机制,将在下篇中讲解。


## 本文涉及的SQL语句
####
create database db1 mode = "auto";
use db1;
create table t1(x int, y int);
## 本文详解的DDL语句
alter table t1 add global index `g_i_y` (`y`) COVERING (`x`) partition by hash(`y`);


三、元数据管理


如前所述,除去在DN上执行的物理DDL和DML外,DDL Job中包含的关键操作均是对元数据的修改和同步。


PolarDB-X中的元数据由GMS统一管理,按物理位置可以分为两个部分:


 MetaDB中的持久化元信息。例如描述分区表t1的元数据表包括table_partitions,tables,columns,indexes等,MetaDB中元信息的读写接口分布在polardbx-gms/src/main/java/com/alibaba/polardbx/gms路径下的辅助类中。


   CN缓存的内存元信息。出于性能考虑,CN节点同时在内存中缓存有分区表t1的元信息,该信息是直接被CN上的DML事务使用的元信息。com.alibaba.polardbx.executor.gms中实现了不同种类元信息的缓存管理, 其                       接                       口                        定                         义                         为com.alibaba.polardbx.optimizer.config.table.SchemaManager,包含init,getTable,reload等基本方法。


private ResultCursor executeQuery(ByteString sql, ExecutionContext executionContext,
                                      AtomicBoolean trxPolicyModified) {
        // Get all meta version before optimization
        final long[] metaVersions = MdlContext.snapshotMetaVersions();
        // Planner
        ExecutionPlan plan = Planner.getInstance().plan(sql, executionContext);
        ...
        //for requireMDL transaction
        if (requireMdl && enableMdl) {
            if (!isClosed()) {
                // Acquire meta data lock for each statement modifies table data
                acquireTransactionalMdl(sql.toString(), plan, executionContext);
            }
            ...
            //update Plan if metaVersion changed, which indicate meta updated
        }
        ...
        ResultCursor resultCursor = executor.execute(plan, executionContext);
        ...
        return resultCursor;
    }


值得注意的是,内存元信息以逻辑表为基本单位构建锁,此锁即为内存MDL锁,DML事务开始时即通过acquireTransactionalMdl尝试获取当前最新版本的相关逻辑表的MDL锁,执行完成后释放。


DDL Job执行时,将按一定的时序修改上述两部分元信息,此过程中DDL和DML之间的同步是实现DDL的online特性的关键步骤。


四、DDL Job定义


添加全局二级索引的DDL Job Handler为AlterTableHandler,它重载了LogicalCommonDdlHandler的buildDdlJob方法com.alibaba.polardbx.executor.handler.ddl.LogicalAlterTableHandler#buildDdlJob,该方法最终分派到com.alibaba.polardbx.executor.ddl.job.factory.gsi.CreatePartitionGsiJobFactory构造全局二级索引对应的ddl Job。


public class CreatePartitionGsiJobFactory extends CreateGsiJobFactory{
    @Override
    protected void excludeResources(Set<String> resources) {
        super.excludeResources(resources);
        //meta data lock in MetaDB
        resources.add(concatWithDot(schemaName, primaryTableName)); //db1.t1
        resources.add(concatWithDot(schemaName, indexTableName)); //db1.g_i_y
        ...
    }    
    @Override
    protected ExecutableDdlJob doCreate() {
      ...
        if (needOnlineSchemaChange) {
            bringUpGsi = GsiTaskFactory.addGlobalIndexTasks(
                schemaName,
                primaryTableName,
                indexTableName,
                stayAtDeleteOnly,
                stayAtWriteOnly,
                stayAtBackFill
            );
        }
      ...
        List<DdlTask> taskList = new ArrayList<>();
        //1. validate
        taskList.add(validateTask);
        //2. create gsi table
        //2.1 insert tablePartition meta for gsi table
        taskList.add(createTableAddTablesPartitionInfoMetaTask);
        //2.2 create gsi physical table
        CreateGsiPhyDdlTask createGsiPhyDdlTask =
            new CreateGsiPhyDdlTask(schemaName, primaryTableName, indexTableName, physicalPlanData);
        taskList.add(createGsiPhyDdlTask);
        //2.3 insert tables meta for gsi table
        taskList.add(addTablesMetaTask);
        taskList.add(showTableMetaTask);
        //3. 
        //3.1 insert indexes meta for primary table
        taskList.add(addIndexMetaTask);
        //3.2 gsi status: CREATING -> DELETE_ONLY -> WRITE_ONLY -> WRITE_REORG -> PUBLIC
        taskList.addAll(bringUpGsi);
        //last tableSyncTask
        DdlTask tableSyncTask = new TableSyncTask(schemaName, indexTableName);
        taskList.add(tableSyncTask);
        final ExecutableDdlJob4CreatePartitionGsi result = new ExecutableDdlJob4CreatePartitionGsi();
        result.addSequentialTasks(taskList);
        ....
        return result;
    }
    ...
}


excludeResources方法声明了ddlJob对相关对象的持久化元数据锁的占用,本例涉及主表与GSI两张表的元数据修改,因而加锁对象包括db1.t1,db1.g_i_y。注意,该锁与前述的内存MDL锁不同,前者在DDL引擎执行DDL job的初始阶段持久化到MetaDB的read_write_lock表中,用于控制DDL之间的并发。


doCreate方法声明了按时序执行的一系列Task,其语义依次是:


. 校验

。validateTask:DDL Job校验,检查GSI表名的合法性等。


. 创建GSI表

。createTableAddTablesPartitionInfoMetaTask:

。GSI的分区元信息写入 createGsiPhyDdlTask:创建GSI对应物理表的物理DDL

。addTablesMetaTask:主表的元信息修改

。showTableMetaTask:主表的元信息在节点之间广播


. GSI表的元信息同步

。addIndexMetaTask:GSI表的tableMeta元信息修改

。 bringUpGsi:添加索引后的online schema change过程

。 tableSyncTask:GSI表的元信息在节点之间广播


在上述的Task中,持久化元信息的写入操作均作用于MetaDB中相应的元信息表,元信息的广播操作作用于CN缓存的元信息。


接下来我们简要介绍其中的三类代表性Task,其中CreateTableAddTablesMetaTask包含的方法列表如下:

public class CreateTableAddTablesMetaTask extends BaseGmsTask {
    @Override
    public void executeImpl(Connection metaDbConnection, ExecutionContext executionContext) {
        PhyInfoSchemaContext phyInfoSchemaContext = TableMetaChanger.buildPhyInfoSchemaContext(schemaName,
            logicalTableName, dbIndex, phyTableName, sequenceBean, tablesExtRecord, partitioned, ifNotExists, sqlKind,
            executionContext);
        FailPoint.injectRandomExceptionFromHint(executionContext);
        FailPoint.injectRandomSuspendFromHint(executionContext);
        TableMetaChanger.addTableMeta(metaDbConnection, phyInfoSchemaContext);
    }
    @Override
    public void rollbackImpl(Connection metaDbConnection, ExecutionContext executionContext) {
        TableMetaChanger.removeTableMeta(metaDbConnection, schemaName, logicalTableName, false, executionContext);
    }
    @Override
    protected void onRollbackSuccess(ExecutionContext executionContext) {
        TableMetaChanger.afterRemovingTableMeta(schemaName, logicalTableName);
    }
}


.addTableMetaTask


在主表的tableMeta中添加GSI表相关的元信息。executeImpl调用MetaDB提供的元数据读写接口com.alibaba.polardbx.executor.ddl.job.meta.TableMetaChanger#addTableMeta写入GSI表的元信息。在executeImpl对应的主线逻辑之外,addTableMetaTask同时提供了rollbackImpl方法清空GSI表的元信息,这实际上即是该Task对应的undolog,在DDL job发生回滚时调用该方法即可恢复原有的tableMeta信息。


.TableSyncTask


作为前述的节点间通信机制的一个特例,实现tableMeta元信息在CN节点间的同步,通知集群内所有CN节点从MetaDB中加载tableMeta元信息。


.bringUpGsi


是一系列Task构成的Task List,它参照online schema change定义了元信息的演化和数据回填流程,其原理见PolarDB-X Online Schema Change,其中也包含TableSyncTask。我们将在下一节着重介绍此Task。


除了上述Task,在逻辑DDL中常用的元信息读写、物理DDL和DML Task均已预先定义,读者可在polardbx-executor/src/main/java/com/alibaba/polardbx/executor/ddl/job/task/basic目录下找到。


在doCreate方法的最后,addSequentialTasks向构造出的ddlJob中批量添加Task任务。作为最简单的Task组合接口,addSequentialTask以参数taskList中元素的下标顺序作为拓扑序构造Task之间的依赖关系。此方法揭示出在ddlJob中以DAG评接DDL Task的组合方式,DDL引擎中将根据DAG中的拓扑序调度DDL Task。此外,在声明复杂依赖关系时还可使用addTask,addTaskRelationShip等方法单独声明Task及其依赖顺序。


五、DDL与DML的同步


上一节中的bringUpGsi是online-schema-change流程的一个完整实现,定义于com.alibaba.polardbx.executor.ddl.job.task.factory.GsiTaskFactory#addGlobalIndexTasks方法中,我们以此为例介绍DDL与DML同步的几个关键组成部分。


public static List<DdlTask> addGlobalIndexTasks(String schemaName,
                                                    String primaryTableName,
                                                    String indexName,
                                                    boolean stayAtDeleteOnly,
                                                    boolean stayAtWriteOnly,
                                                    boolean stayAtBackFill) {
        ....
        DdlTask writeOnlyTask = new GsiUpdateIndexStatusTask(
            schemaName,
            primaryTableName,
            indexName,
            IndexStatus.DELETE_ONLY,
            IndexStatus.WRITE_ONLY
        ).onExceptionTryRecoveryThenRollback();
        ....
        taskList.add(deleteOnlyTask);
        taskList.add(new TableSyncTask(schemaName, primaryTableName));
        ....
        taskList.add(writeOnlyTask);
        taskList.add(new TableSyncTask(schemaName, primaryTableName));
        ...
        taskList.add(new LogicalTableBackFillTask(schemaName, primaryTableName, indexName));
        ...
        taskList.add(writeReOrgTask);
        taskList.add(new TableSyncTask(schemaName, primaryTableName));
        taskList.add(publicTask);
        taskList.add(new TableSyncTask(schemaName, primaryTableName));
        return taskList;
    }


 元信息的多版本演化过程。索引的元信息经历从CREATING->DELETE_ONLY->WRITE_ONLY->WRITE_REORG->PUBLIC的完整版本演化流程,其状态设计原理见online schema change一文,此状态设计过程保证系统中存在双版本元数据时不会出现不一致状态。


 DML在CN缓存的对应版本元信息下的重写。在元信息版本演化过程中,进入执行器的DML语句将会分派对应的rewriter根据索引状态生成实际的DML,从而满足write_only,update_only等元信息状态约束,并在数据回填时实现DML双写。


 MetaDB和CN缓存元信息之间的同步时序。sync操作将按一定的时序修改上述两部分元信息,一方面保证DML事务期间对应版本元信息的可用性,同时在相应版本DML事务结束后抢占MDL锁并invalidate原版本信息,另一方面通过提前加载新版本元信息,确保同一时刻其他DML事务总能获取可用的元信息。


六、DML的重写


索引元信息中的状态定义见:


com.alibaba.polardbx.gms.metadb.table.IndexStatus


.DDL更新该字段的同时,优化器为修改主表的DML语句在RBO中分配相应的gsiWriter,以简单的Insert语句insert into t1 values(1,2)为例:


Insert类型的语句在对应的RBO阶段polardbx-optimizer/src/main/java/com/alibaba/polardbx/optimizer/core/planner/rule/OptimizeLogicalInsertRule.java根据主表元信息中的gsiMeta在执行计划中关联到相应GSI的writer。


//OptimizeLogicalInsertRule.java
private LogicalInsert handlePushdown(LogicalInsert origin, boolean deterministicPushdown, ExecutionContext ec){
      ...//other writers
        final List<InsertWriter> gsiInsertWriters = new ArrayList<>();
    IntStream.range(0, gsiMetas.size()).forEach(i -> {
        final TableMeta gsiMeta = gsiMetas.get(i);
        final RelOptTable gsiTable = catalog.getTableForMember(ImmutableList.of(schema, gsiMeta.getTableName()));
        final List<Integer> gsiValuePermute = gsiColumnMappings.get(i);
        final boolean isGsiBroadcast = TableTopologyUtil.isBroadcast(gsiMeta);
        final boolean isGsiSingle = TableTopologyUtil.isSingle(gsiMeta);
        //different write stragety for corresponding table type.
        gsiInsertWriters.add(WriterFactory
                             .createInsertOrReplaceWriter(newInsert, gsiTable, sourceRowType, gsiValuePermute, gsiMeta, gsiKeywords,
                                                          null, isReplace, isGsiBroadcast, isGsiSingle, isValueSource, ec));
        });
   ...
}


writer在执行器的Handler中apply到物理执行计划中。其中getInput方法根据gsiWriter中包含的indexStatus状态信息决定是否生成当前DML对GSI的物理写操作,因此,最终DML仅在索引更新到WRITE_ONLY后开启对GSI的双写。


//LogicalInsertWriter.java    
protected int executeInsert(LogicalInsert logicalInsert, ExecutionContext executionContext,
                                HandlerParams handlerParams) {
        ...
       final List<InsertWriter> gsiWriters = logicalInsert.getGsiInsertWriters();
       gsiWriters.stream()
                .map(gsiWriter -> gsiWriter.getInput(executionContext))
                .filter(w -> !w.isEmpty())
                .forEach(w -> {
                    writableGsiCount.incrementAndGet();
                    allPhyPlan.addAll(w);
                });
//IndexStatus.java
...
public static final EnumSet<IndexStatus> WRITABLE = EnumSet.of(WRITE_ONLY, WRITE_REORG, PUBLIC, DROP_WRITE_ONLY);
public boolean isWritable() {
   return WRITABLE.contains(this);
}
...


七、元信息同步时序


             


在通过sync同步MetaDB和内存元信息时,将依照以下步骤进行:


 loadSchema:首先预加载MetaDB中的新版本元信息到内存中。加载完成后,内存中新增新版本元数据及其MDL锁,此后进入CN的DML(node1.thread2)均会获取新版本元数据的MDL。


 mdl(v0).writeLock:然后尝试获取旧版本元数据的MDL锁(node1.mdl.v0),当获取锁成功时,旧版本事务即被排空。


 expireSchemaManager(t1, g_i1, v0):消除旧版本元信息,将新版本元信息标记为旧版本元信息。


上述sync调用的响应者是CN集群中的所有节点(包括调用者本身),当所有节点均完成元信息版本切换时,sync调用即告成功,此时序保证了以下几点:


 任一时刻整个集群中至多存在两个版本元信息,并且至少有一个版本的元信息可加MDL读锁,因而进入CN的MDL语句永不会阻塞。

 旧版本元信息在loadSchema后即不再关联到MDL事务,可保证有限的状态切换时间。


其中1,2,3步均实现于

com.alibaba.polardbx.executor.gms.GmsTableMetaManager#tonewversion方法。


public void tonewversion(String tableName, boolean preemptive, Long initWait, Long interval, TimeUnit timeUnit) {
        synchronized (OptimizerContext.getContext(schemaName)) {
            GmsTableMetaManager oldSchemaManager =
                (GmsTableMetaManager) OptimizerContext.getContext(schemaName).getLatestSchemaManager();
            TableMeta currentMeta = oldSchemaManager.getTableWithNull(tableName);
            long version = -1;
            ....//查询当前MetaDB中的元数据版本并将其赋值给vesion
            //1. loadSchema
            SchemaManager newSchemaManager =
                new GmsTableMetaManager(oldSchemaManager, tableName, rule);
            newSchemaManager.init();
            OptimizerContext.getContext(schemaName).setSchemaManager(newSchemaManager);
            //2. mdl(v0).writeLock
            final MdlContext context;
            if (preemptive) {
                context = MdlManager.addContext(schemaName, initWait, interval, timeUnit);
            } else {
                context = MdlManager.addContext(schemaName, false);
            }
            MdlTicket ticket = context.acquireLock(new MdlRequest(1L,
                    MdlKey
                        .getTableKeyWithLowerTableName(schemaName, currentMeta.getDigest()),
                    MdlType.MDL_EXCLUSIVE,
                    MdlDuration.MDL_TRANSACTION));
            //3. expireSchemaManager(t1, g_i1, v0)
            oldSchemaManager.expire();
            ....//失效使用旧版本元信息的PlanCache.
            context.releaseLock(1L, ticket);
        } 
    }


通过上述几个要素,DDL Job在定义时实现了DDL与DML的同步,保证了DML的online执行。


八、总结


本文主要解读了PolarDB-X中CN端的DDL Job定义相关的代码,以添加全局二级索引为例,对DDL Job定义和执行的整体流程进行了梳理,并着重阐述了DDL job定义中涉及online和crash safe特性的关键逻辑。对于DDL引擎中的DDL job执行流程,敬请期待下篇解读。


相关实践学习
快速体验PolarDB开源数据库
本实验环境已内置PostgreSQL数据库以及PolarDB开源数据库:PolarDB PostgreSQL版和PolarDB分布式版,支持一键拉起使用,方便各位开发者学习使用。
相关文章
|
12天前
|
关系型数据库 MySQL Linux
在 CentOS 7 中通过编译源码方式安装 MySQL 数据库的详细步骤,并与使用 RPM 包安装进行了对比
本文介绍了在 CentOS 7 中通过编译源码方式安装 MySQL 数据库的详细步骤,并与使用 RPM 包安装进行了对比。通过具体案例,读者可以了解如何准备环境、下载源码、编译安装、配置服务及登录 MySQL。编译源码安装虽然复杂,但提供了更高的定制性和灵活性,适用于需要高度定制的场景。
44 3
|
12天前
|
PHP 数据库 数据安全/隐私保护
布谷直播源码部署服务器关于数据库配置的详细说明
布谷直播系统源码搭建部署时数据库配置明细!
|
15天前
|
关系型数据库 MySQL Linux
在 CentOS 7 中通过编译源码方式安装 MySQL 数据库的详细步骤,包括准备工作、下载源码、编译安装、配置 MySQL 服务、登录设置等。
本文介绍了在 CentOS 7 中通过编译源码方式安装 MySQL 数据库的详细步骤,包括准备工作、下载源码、编译安装、配置 MySQL 服务、登录设置等。同时,文章还对比了编译源码安装与使用 RPM 包安装的优缺点,帮助读者根据需求选择最合适的方法。通过具体案例,展示了编译源码安装的灵活性和定制性。
59 2
|
1月前
|
关系型数据库 MySQL Linux
在 CentOS 7 中通过编译源码方式安装 MySQL 数据库的详细步骤
本文介绍了在 CentOS 7 中通过编译源码方式安装 MySQL 数据库的详细步骤,包括准备工作、下载源码、编译安装、配置服务等,并与使用 RPM 包安装进行了对比,帮助读者根据需求选择合适的方法。编译源码安装虽然复杂,但提供了更高的定制性和灵活性。
218 2
|
2月前
|
JavaScript Java 关系型数据库
毕设项目&课程设计&毕设项目:基于springboot+vue实现的在线考试系统(含教程&源码&数据库数据)
本文介绍了一个基于Spring Boot和Vue.js实现的在线考试系统。随着在线教育的发展,在线考试系统的重要性日益凸显。该系统不仅能提高教学效率,减轻教师负担,还为学生提供了灵活便捷的考试方式。技术栈包括Spring Boot、Vue.js、Element-UI等,支持多种角色登录,具备考试管理、题库管理、成绩查询等功能。系统采用前后端分离架构,具备高性能和扩展性,未来可进一步优化并引入AI技术提升智能化水平。
毕设项目&课程设计&毕设项目:基于springboot+vue实现的在线考试系统(含教程&源码&数据库数据)
|
2月前
|
Java 关系型数据库 MySQL
毕设项目&课程设计&毕设项目:springboot+jsp实现的房屋租租赁系统(含教程&源码&数据库数据)
本文介绍了一款基于Spring Boot和JSP技术的房屋租赁系统,旨在通过自动化和信息化手段提升房屋管理效率,优化租户体验。系统采用JDK 1.8、Maven 3.6、MySQL 8.0、JSP、Layui和Spring Boot 2.0等技术栈,实现了高效的房源管理和便捷的租户服务。通过该系统,房东可以轻松管理房源,租户可以快速找到合适的住所,双方都能享受数字化带来的便利。未来,系统将持续优化升级,提供更多完善的服务。
毕设项目&课程设计&毕设项目:springboot+jsp实现的房屋租租赁系统(含教程&源码&数据库数据)
|
1月前
|
关系型数据库 MySQL Linux
在 CentOS 7 中通过编译源码方式安装 MySQL 数据库的详细步骤
【10月更文挑战第7天】本文介绍了在 CentOS 7 中通过编译源码方式安装 MySQL 数据库的详细步骤,包括准备工作、下载源码、编译安装、配置 MySQL 服务、登录设置等。同时,文章还对比了编译源码安装与使用 RPM 包安装的优缺点,帮助读者根据自身需求选择合适的方法。
53 3
|
1月前
|
前端开发 Java 数据库连接
表白墙/留言墙 —— 中级SpringBoot项目,MyBatis技术栈MySQL数据库开发,练手项目前后端开发(带完整源码) 全方位全步骤手把手教学
本文是一份全面的表白墙/留言墙项目教程,使用SpringBoot + MyBatis技术栈和MySQL数据库开发,涵盖了项目前后端开发、数据库配置、代码实现和运行的详细步骤。
40 0
表白墙/留言墙 —— 中级SpringBoot项目,MyBatis技术栈MySQL数据库开发,练手项目前后端开发(带完整源码) 全方位全步骤手把手教学
|
3月前
|
C# UED 定位技术
WPF控件大全:初学者必读,掌握控件使用技巧,让你的应用程序更上一层楼!
【8月更文挑战第31天】在WPF应用程序开发中,控件是实现用户界面交互的关键元素。WPF提供了丰富的控件库,包括基础控件(如`Button`、`TextBox`)、布局控件(如`StackPanel`、`Grid`)、数据绑定控件(如`ListBox`、`DataGrid`)等。本文将介绍这些控件的基本分类及使用技巧,并通过示例代码展示如何在项目中应用。合理选择控件并利用布局控件和数据绑定功能,可以提升用户体验和程序性能。
62 0
|
3月前
|
Cloud Native 关系型数据库 分布式数据库
什么是云原生数据库PolarDB分布式版
本文介绍什么是云原生数据库PolarDB分布式版,也称为PolarDB分布式版,本手册中简称为PolarDB-X。
90 0

热门文章

最新文章

相关产品

  • 云原生数据库 PolarDB