gorm源码解析(四):事务,预编译

news2024/12/19 11:28:38

文章目录

    • 前言
    • 事务
      • 自己控制事务
      • 用 Transaction方法包装事务
    • 预编译
    • 事务结合预编译
    • 总结

前言

前几篇文章介绍gorm的整体设计,增删改查的具体实现流程。本文将聚焦与事务和预编译部分

事务

自己控制事务

用gorm框架,可以自己控制事务的Begin,Commit和Rollback,如下所示:

// 开始事务
tx := db.Begin()

// 在事务中执行一些 db 操作(从这里开始,您应该使用 'tx' 而不是 'db')
tx.Create(...) 

// ...  

// 遇到错误时回滚事务
tx.Rollback()

// 否则,提交事务
tx.Commit()

下面看看每个api的源码实现

Beigin:

  1. 新建一个db实例,作为本次事务操作的会话
  2. 非预编译模式下:调sql.DB的BeginTx方法,让tx.Statement.ConnPool持有其返回的sql.Tx
    1. 之后的增删改查操作,都用这个sql.Tx执行,会用同一个db连接(也就是调begin的那个连接)

预编译模式本文后面再介绍,这里只关注非预编译模式

func (db *DB) Begin(opts ...*sql.TxOptions) *DB {  
    var (  
       // clone statement  
       // 新建的tx.clone = 1  
       tx  = db.getInstance().Session(&Session{Context: db.Statement.Context, NewDB: db.clone == 1})  
       opt *sql.TxOptions  
       err error  
    )  
  
    if len(opts) > 0 {  
       opt = opts[0]  
    }  
  
    switch beginner := tx.Statement.ConnPool.(type) {  
    // 非预编译模式下:  
    case TxBeginner:  
       tx.Statement.ConnPool, err = beginner.BeginTx(tx.Statement.Context, opt)  
    // 预编译模式下:  
    case ConnPoolBeginner:  
       tx.Statement.ConnPool, err = beginner.BeginTx(tx.Statement.Context, opt)  
    default:  
       err = ErrInvalidTransaction  
    }  
  
    if err != nil {  
       tx.AddError(err)  
    }  
  
    return tx  
}

Commit和Rollback:调sql.Tx的Commit和Rollback方法,内部会调mysql驱动mysqlTx的Commit和Rollback方法,完成事务的提交和回滚操作

func (db *DB) Commit() *DB {  
    if committer, ok := db.Statement.ConnPool.(TxCommitter); ok && committer != nil && !reflect.ValueOf(committer).IsNil() {  
       db.AddError(committer.Commit())  
    } else {  
       db.AddError(ErrInvalidTransaction)  
    }  
    return db  
}  
  

func (db *DB) Rollback() *DB {  
    if committer, ok := db.Statement.ConnPool.(TxCommitter); ok && committer != nil {  
       if !reflect.ValueOf(committer).IsNil() {  
          db.AddError(committer.Rollback())  
       }  
    } else {  
       db.AddError(ErrInvalidTransaction)  
    }  
    return db  
}

用 Transaction方法包装事务

然而,自己控制事务有以下问题:

  1. 很多重复代码,样板代码
  2. 自己控制事务生命周期容易出问题,例如可能忘记commit或rollback,或者说发生panic但没有recover,导致没有触发commit或rollback

于是gorm提供了Transaction方法,帮我们调了Begin,事务执行成功后Commit,事务执行失败或发生panic时执行Rollback操作,也就是帮我们控制事务的生命周期,我们只用关注业务逻辑即可

使用方法为将业务逻辑func传进去,例如:

db.Transaction(func(tx *gorm.DB) error {  
    // 在事务中执行一些 db 操作(从这里开始,您应该使用 'tx' 而不是 'db')  
    if err := tx.Create(&Animal{Name: "Giraffe"}).Error; err != nil {  
       // 返回任何错误都会回滚事务  
       return err  
    }  
  
    if err := tx.Create(&Animal{Name: "Lion"}).Error; err != nil {  
       return err  
    }  
  
    // 返回 nil 提交事务  
    return nil  
})

在这里插入图片描述


Transaction方法流程如下:

  1. db.Begin开启事务
  2. 执行fc,把事务tx传进去
  3. 如果fc执行成功,执行tx.Commit提交事务
  4. 如果fc执行出错或发生panic,调tx.Rollback回滚事务
func (db *DB) Transaction(fc func(tx *DB) error, opts ...*sql.TxOptions) (err error) {
	panicked := true

	if committer, ok := db.Statement.ConnPool.(TxCommitter); ok && committer != nil {
		// ...
	} else {
		// 开启事务
		tx := db.Begin(opts...)
		if tx.Error != nil {
			return tx.Error
		}

		defer func() {
			// 如果发生panic,或者执行出错,回滚
			if panicked || err != nil {
				tx.Rollback()
			}
		}()

		if err = fc(tx); err == nil {
			panicked = false
			return tx.Commit().Error
		}
	}

	panicked = false
	return
}

预编译

只要用database/sql配合mysql驱动,执行sql时一定走了预编译。要么是客户端预编译,要么是mysql服务端预编译

在gorm层面,PrepareStmt的目的是提高性能,而不是执行sql时从非预编译变成预编译
默认gorm层面的PrepareStmt为false,这里假设使用服务端预编译(连接mysql的dsn中interpolateParams为false),执行sql模板 + 参数类型的操作时有如下流程:

  1. 往mysql发送sql模板,获得stmtId
  2. 往msqyl发送stmtId + 参数,得到执行结果
  3. 往mysql发送释放stmt命令

如果使用gorm层面的PrepareStmt,会对sql.Stmt进行缓存,如果当前连接预编译过该Stmt,就能直接用
接下来看看gorm层面怎么处理预编译的

初始化db时(gorm.Open),如果config.PrepareStmt为true,使用预编译模式:

func Open(dialector Dialector, opts ...Option) (db *DB, err error) {
	// ...

	// 预编译模式
	if config.PrepareStmt {
		preparedStmt := NewPreparedStmtDB(db.ConnPool)
		db.cacheStore.Store(preparedStmtDBKey, preparedStmt)
		db.ConnPool = preparedStmt
	}

	db.Statement = &Statement{
		DB:       db,
		ConnPool: db.ConnPool,
		Context:  context.Background(),
		Clauses:  map[string]clause.Clause{},
	}

	// ...

	return
}

ConnPool被替换成PreparedStmtDB,其结构如下:

type PreparedStmtDB struct {
    // key: sql模板,value:Stmt
	Stmts map[string]*Stmt
	Mux   *sync.RWMutex
    // 内置的 ConnPool 字段通常为 database/sql 中的 *DB
	ConnPool
}

初始化PreparedStmtDB,内部持有sql.DB

func NewPreparedStmtDB(connPool ConnPool) *PreparedStmtDB {
	return &PreparedStmtDB{
	    // 持有sql.DB
		ConnPool: connPool,
		// sql到Stmt的映射
		Stmts:    make(map[string]*Stmt),
		Mux:      &sync.RWMutex{},
	}
}

可以看出PreparedStmtDB拥有sql模板到Stmt的缓存,那么遇到相同sql时,如果之前已经预编译过,就能用该Stmt执行db操作

后续基于这个db执行任何操作时,分为两个步骤:

  • 通过PreparedStmtDB.prepare(...) 操作创建/复用 stmt,后续相同 sql 模板可以复用此 stmt
  • 通过 stmt.Query(...)/Exec(...) 执行 sql
    在这里插入图片描述

例如在执行PreparedStmtDB.QueryContext时:

  1. 先调PreparedStmtDB.prepare看有没有可复用的sql.Stmt
  2. 再调sql.Stmt执行QueryContext操作
func (db *PreparedStmtDB) QueryContext(ctx context.Context, query string, args ...interface{}) (rows *sql.Rows, err error) {
	// 先获取stmt
	stmt, err := db.prepare(ctx, db.ConnPool, false, query)
	if err == nil {
		// 再用stmt执行sql
		rows, err = stmt.QueryContext(ctx, args...)
		if errors.Is(err, driver.ErrBadConn) {
			db.Mux.Lock()
			defer db.Mux.Unlock()

			go stmt.Close()
			delete(db.Stmts, query)
		}
	}
	return rows, err
}

PreparedStmtDB.prepare:

  1. 尝试从缓存Stmts中,根据query模板找sql.Stmt,如果有就返回
  2. 否则调sql.DB,根据query模板生成一个sql.Stmt,加入缓存中
func (db *PreparedStmtDB) prepare(ctx context.Context, conn ConnPool, isTransaction bool, query string) (Stmt, error) {
	db.Mux.RLock()
	// 以sql为模板,先查有没有可复用的stmt
	// 如果stmt.Transaction为false,可以复用
	if stmt, ok := db.Stmts[query]; ok && (!stmt.Transaction || isTransaction) {
		db.Mux.RUnlock()
		// wait for other goroutines prepared
		<-stmt.prepared
		if stmt.prepareErr != nil {
			return Stmt{}, stmt.prepareErr
		}

		return *stmt, nil
	}
	db.Mux.RUnlock()

	db.Mux.Lock()
	// double check
	if stmt, ok := db.Stmts[query]; ok && (!stmt.Transaction || isTransaction) {
		db.Mux.Unlock()
		// wait for other goroutines prepared
		<-stmt.prepared
		if stmt.prepareErr != nil {
			return Stmt{}, stmt.prepareErr
		}

		return *stmt, nil
	}
	
	// ...
	
	// 到这里没有可复用的模板
	// 创建stmt实例,加到map中
	cacheStmt := Stmt{Transaction: isTransaction, prepared: make(chan struct{})}
	db.Stmts[query] = &cacheStmt
	db.Mux.Unlock()

	// prepare completed
	defer close(cacheStmt.prepared)

	// 调sql.DB的prepareContext方法,创建sql.stmt
	stmt, err := conn.PrepareContext(ctx, query)
	if err != nil {
		cacheStmt.prepareErr = err
		db.Mux.Lock()
		delete(db.Stmts, query)
		db.Mux.Unlock()
		return Stmt{}, err
	}

	db.Mux.Lock()
	cacheStmt.Stmt = stmt
	db.Mux.Unlock()

	return cacheStmt, nil
}

这里是ORM层面对Stmt做了缓存
而在go标准库database/sql层面,stmt要能执行的前提是,在当前连接预编过

我们看sql.Stmt源码,注释上有这么一段话:

A Stmt is safe for concurrent use by multiple goroutines
翻译:Stmt能被多个g并发调用


When the Stmt needs to execute on a new underlying connection, it will prepare itself on the new connection automatically
翻译:当Stmt需要被新的连接使用时,需要在新连接上预编译

sql.Stmt有一个css slice,存放预编译了该Stmt的连接

type Stmt struct {  
   
    // 持有driverStmt
    cgds *driverStmt  
	// ...

    // 预编译了该Stmt的连接
    css []connStmt  
  
}

type connStmt struct {  
    dc *driverConn  
    ds *driverStmt  
}

基于Stmt执行Query, Exec操作时,会检查当前使用的连接在不在Stmt.css里面,如果在就能立即执行,否则需要先预编译该sql模板才能执行

这里以Stmt.ExecQuery为例,看看如何获取连接并执行

func (s *Stmt) ExecContext(ctx context.Context, args ...any) (Result, error) {  
    s.closemu.RLock()  
    defer s.closemu.RUnlock()  
  
    var res Result  
    err := s.db.retry(func(strategy connReuseStrategy) error {
       // 获取连接  
       dc, releaseConn, ds, err := s.connStmt(ctx, strategy)  
       if err != nil {  
          return err  
       }  
       // 执行exec
       res, err = resultFromStatement(ctx, dc.ci, ds, args...)  
       releaseConn(err)  
       return err  
    })  
  
    return res, err  
}

重点在获取连接:

  1. 从连接池获取一个连接
  2. 检查该连接是否在Stmt.css里面,如果是,直接返回
  3. 否则需要先用该连接预编译sql模板
func (s *Stmt) connStmt(ctx context.Context, strategy connReuseStrategy) (dc *driverConn, releaseConn func(error), ds *driverStmt, err error) {  
    // ...
    
    // 从连接池获取一个连接
    dc, err = s.db.conn(ctx, strategy)  
    if err != nil {  
       return nil, nil, nil, err  
    }  
  
    s.mu.Lock()  
    // 检查该连接是否在Stmt.css里面,如果是,直接返回
    for _, v := range s.css {  
       if v.dc == dc {  
          s.mu.Unlock()  
          return dc, dc.releaseConn, v.ds, nil  
       }  
    }  
    s.mu.Unlock()  
  
    // 否则需要先用该连接预编译sql模板
    withLock(dc, func() {  
       ds, err = s.prepareOnConnLocked(ctx, dc)  
    })  
    if err != nil {  
       dc.releaseConn(err)  
       return nil, nil, nil, err  
    }  
  
    return dc, dc.releaseConn, ds, nil  
}

prepareOnConnLocked:预编译完成后,将连接加入stmt.css

func (s *Stmt) prepareOnConnLocked(ctx context.Context, dc *driverConn) (*driverStmt, error) {  
    si, err := dc.prepareLocked(ctx, s.cg, s.query)  
    if err != nil {  
       return nil, err  
    }  
    cs := connStmt{dc, si}  
    s.mu.Lock() 
	    // 预编译完成后,将当前连接加入stmt.css
    s.css = append(s.css, cs)  
    s.mu.Unlock()  
    return cs.ds, nil  
}

事务结合预编译

如果同时有事务和预编译,那么在执行Exec/Query时,稍微有点不一样

回到事务的Begin方法:

func (db *DB) Begin(opts ...*sql.TxOptions) *DB {  
    // ...
  
    switch beginner := tx.Statement.ConnPool.(type) {  
    // ...  
    // 预编译模式下:  
    case ConnPoolBeginner:  
       tx.Statement.ConnPool, err = beginner.BeginTx(tx.Statement.Context, opt)  
    default:  
       err = ErrInvalidTransaction  
    }  
  
    if err != nil {  
       tx.AddError(err)  
    }  
  
    return tx  
}

如果是预编译模式,进入PreparedStmtDB.BeginTx,返回PreparedStmtTX实例

func (db *PreparedStmtDB) BeginTx(ctx context.Context, opt *sql.TxOptions) (ConnPool, error) {  
    if beginner, ok := db.ConnPool.(TxBeginner); ok {  
       tx, err := beginner.BeginTx(ctx, opt)  
       return &PreparedStmtTX{PreparedStmtDB: db, Tx: tx}, err  
    }  
  
    // ... 
}

接下来看看基于PreparedStmtTX执行增删改查有何特别的地方
这里以PreparedStmtTX.ExecContext为例:

  1. PreparedStmtDB.prepare从缓存拿或新建一个Stmt
  2. sql.Tx.StmtContext先处理步骤1返回的Stmt,再执行Exec,重点在这里
func (tx *PreparedStmtTX) ExecContext(ctx context.Context, query string, args ...interface{}) (result sql.Result, err error) {  
    stmt, err := tx.PreparedStmtDB.prepare(ctx, tx.Tx, true, query)  
    if err == nil {  
       result, err = tx.Tx.StmtContext(ctx, stmt.Stmt).ExecContext(ctx, args...)  
       if errors.Is(err, driver.ErrBadConn) {  
          tx.PreparedStmtDB.Mux.Lock()  
          defer tx.PreparedStmtDB.Mux.Unlock()  
  
          go stmt.Close()  
          delete(tx.PreparedStmtDB.Stmts, query)  
       }  
    }  
    return result, err  
}

sql.Tx.StmtContext方法

  1. 拿到和事务tx绑定的连接dc
  2. 看dc是否预编译过该stmt,如果没有执行预编译操作
  3. 将事务tx放到Stmt.cg字段,后面执行Exec获取连接时,优先从该字段获取
    1. 也就是保证执行整个事务都要用同一个连接
func (tx *Tx) StmtContext(ctx context.Context, stmt *Stmt) *Stmt {  
    dc, release, err := tx.grabConn(ctx)  
    if err != nil {  
       return &Stmt{stickyErr: err}  
    }  
    defer release(nil)  
  
    if tx.db != stmt.db {  
       return &Stmt{stickyErr: errors.New("sql: Tx.Stmt: statement from different database used")}  
    }  
    var si driver.Stmt  
    var parentStmt *Stmt  
    stmt.mu.Lock()  
    if stmt.closed || stmt.cg != nil {  
       // ...
    } else {  
       stmt.removeClosedStmtLocked()  
       // See if the statement has already been prepared on this connection,  
       // and reuse it if possible.       for _, v := range stmt.css {  
          if v.dc == dc {  
             si = v.ds.si  
             break  
          }  
       }  
  
       stmt.mu.Unlock()  
       // 没预编译过,执行预编译
       if si == nil {  
          var ds *driverStmt  
          withLock(dc, func() {  
             ds, err = stmt.prepareOnConnLocked(ctx, dc)  
          })  
          if err != nil {  
             return &Stmt{stickyErr: err}  
          }  
          si = ds.si  
       }  
       parentStmt = stmt  
    }  
  
    txs := &Stmt{  
       db: tx.db,  
       // 重点在这,将tx放到Stmt.cg中。后面执行Exec获取连接时,优先从该字段获取
       cg: tx,  
       cgds: &driverStmt{  
          Locker: dc,  
          si:     si,  
       },  
       parentStmt: parentStmt,  
       query:      stmt.query,  
    }  
    if parentStmt != nil {  
       tx.db.addDep(parentStmt, txs)  
    }  
    tx.stmts.Lock()  
    tx.stmts.v = append(tx.stmts.v, txs)  
    tx.stmts.Unlock()  
    return txs  
}

总结

至此,gorm的源码分析告一段落了,下一篇文章会介绍一些工程上使用gorm的最佳实践

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

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

相关文章

什么是双声道立体声环绕声全景声 | 一文讲清楚沉浸式声音基本设定

目录 一、 沉浸式声音基本概念1. 声学上的沉浸式2. 空间音频技术3. 声源位置4. 人耳声音定位&#xff08;水平&垂直方向&#xff09;5. 人耳对声源距离定位的影响因素6. 头部相关传递函数7. 三维声技术8. “双耳”与“立体声”9. 耳机重放与扬声器重放10. 环绕声11. 高度声…

使用C语言库函数格式化输入时格式类型与数据类型不匹配导致程序异常

问题 使用两次sscanf()库函数从两个字符串中按照指定的格式读取数据&#xff0c;执行完毕后发现第一个正常读取的数据被篡改。项目在Ubuntu上使用CMake和Ninja构建项目&#xff0c;编译时没有错误和警告。 复现 为方便调试&#xff0c;在keil中编译stm32工程代替&#xff0c…

车牌识别之三:检测+识别的onnx部署(免费下载高精度onnx模型)

依赖 paddle2onnx1.3.1 onnxruntime-gpu1.14.0 ultralytics8.3.38背景 在车牌识别之一&#xff1a;车牌检测(包含全部免费的数据集、源码和模型下载&#xff09;我们得到了车牌检测模型&#xff1b; 在车牌识别之二&#xff1a;车牌OCR识别(包含全部免费的数据集、源码和模型…

WPF ControlTemplate 控件模板

区别于 DataTemplate 数据模板&#xff0c;ControlTemplate 是控件模板&#xff0c;是为自定义控件的 Template 属性服务的&#xff0c;Template 属性类型就是 ControlTemplate。 演示&#xff0c; 自定义一个控件 MyControl&#xff0c;包含一个字符串类型的依赖属性。 pub…

在IDE中使用Git

我们在开发的时候肯定是经常使用IDE进行开发的&#xff0c;所以在IDE中使用Git也是非常常用的&#xff0c;接下来以IDEA为例&#xff0c;其他的VS code &#xff0c;Pycharm等IDE都是一样的。 在IDEA中配置Git 1.打开IDEA 2.点击setting 3.直接搜索git 如果已经安装了会自…

Excel中如何消除“长短款”

函数微调可以可以实施&#xff0c;简单且易于操作的气球&#x1f388;涨缩更妙。 (笔记模板由python脚本于2024年12月17日 06:19:13创建&#xff0c;本篇笔记适合用Excel操作数据的coder翻阅) 【学习的细节是欢悦的历程】 Python 官网&#xff1a;https://www.python.org/ Fre…

微命令 微指令 微程序 微操作

微命令是计算机控制部件通过控制线向执行部件发出的各种控制命令&#xff0c;它是构成控制序列的最小单位 微命令与微操作是一一对应的关系&#xff0c;微命令是微操作的控制信号&#xff0c;而微操作是微命令的执行过程。在机器的一个CPU周期中&#xff0c;一组实现一定操作功…

Spring 不推荐使用@Autowired

Spring 不推荐使用Autowired 原因&#xff1a;为什么 Spring和IDEA 都不推荐使用 Autowired 注解_autowired为什么不推荐-CSDN博客 解决方法&#xff1a; 使用Resource注解。 使用构造函数注入。缺点显而易见&#xff0c;当成员变量很多时&#xff0c;构造函数代码冗长&#…

6、AI测试辅助-测试报告编写(生成Bug分析柱状图)

AI测试辅助-测试报告编写&#xff08;生成Bug分析柱状图&#xff09; 一、测试报告1. 创建测试报告2. 报告补充优化2.1 Bug图表分析 3. 风险评估 总结 一、测试报告 测试报告内容应该包含&#xff1a; 1、测试结论 2、测试执行情况 3、测试bug结果分析 4、风险评估 5、改进措施…

使用re模块

一、常量 常量说明 re.M re.MULTLINE 多行模式 re.S re.DOTALL 单行模式 re.I re.IGNORECASE 忽略大小写 re.X re.VERBOSE 忽略表达式的空白字符 可以使用|开启多个选项 二、方法 2.1 编译 compile re.compile(pattern,[,falgs]) pattern是正则表达式的字符串 设定falgs&…

自动驾驶控制与规划——Project 2: 车辆横向控制

目录 零、任务介绍一、环境配置二、算法三、代码实现四、效果展示 零、任务介绍 补全src/ros-bridge/carla_shenlan_projects/carla_shenlan_stanley_pid_controller/src/stanley_controller.cpp中的TODO部分。 一、环境配置 上一次作业中没有配置docker使用gpu&#xff0c;…

FFmpeg库之ffmpeg

文章目录 ffmpeg命令行使用基本命令选择流 -map选项 主要命令视频选项音频选项多媒体格式转换滤镜裁剪加水印画中画 录制查看可用的录制设备查看录制设备选项参数录制桌面录制窗口录制摄像头录制麦克风录制系统声音同时录制桌面和麦克风 直播推流拉流 ffmpeg命令行使用 ffmpeg…

模型 信任公式(麦肯锡)

系列文章 分享 模型&#xff0c;了解更多&#x1f449; 模型_思维模型目录。信任的量化&#xff1a;可信度、可靠性、亲密度、自私度。 1 信任公式(麦肯锡)的应用 1.1 私域用户信任建立(麦肯锡信任公式的应用) 在私域营销领域&#xff0c;与用户建立牢固的信任关系对于提升用…

低延迟!实时处理!中软高科AI边缘服务器,解决边缘计算多样化需求!

根据相关统计&#xff0c;随着物联网的发展和5G技术的普及&#xff0c;到2025年&#xff0c;全球物联网设备连接数将达到1000亿&#xff0c;海量的计算数据使得传输到云端再处理的云计算方式显得更捉襟见肘。拥有低延迟、实时处理、可扩展性和更高安全性的边缘计算应运而生&…

【计算机网络2】计算机网络的性能能指标

目录 一 、计算机网络的性能指标 二、具体介绍 1、速 率 2、带 宽 3、吞 吐 量 4、时 延 5、时延带宽积 6、往 返 时 延 7、信道利用率 一 、计算机网络的性能指标 计算机网络的性能指标就是从不同方面度量计算机网络的性能&#xff0c;有如下7个指标&#xff1a; 速…

密码学——密码学概述、分类、加密技术(山东省大数据职称考试)

大数据分析应用-初级 第一部分 基础知识 一、大数据法律法规、政策文件、相关标准 二、计算机基础知识 三、信息化基础知识 四、密码学 五、大数据安全 六、数据库系统 七、数据仓库. 第二部分 专业知识 一、大数据技术与应用 二、大数据分析模型 三、数据科学 密码学 大数据…

网络攻击与防范

目录 选填 第一章 1、三种网络模式 2、几种创建网络拓扑结构 NAT模式 VPN模式 软路由模式1 软路由模式2 3、Linux网络配置常用指令 4、常见网络服务配置 DHCP DNS Web服务与FTP服务 FTP用户隔离 第二章 DNS信息收集&#xff08;dnsenum、dnsmap&#xff09; 路…

网络攻与防

1、两个专网连接 &#xff08;1&#xff09;、两个网卡VMNET2/3---配置IP子网、仅主机模式--除去DHCP设置 路由和两台主机分别ping通 &#xff08;2&#xff09;、路由配置&#xff1a;两个专网之间连接--否拨号连接 两台主机可相互ping通---成功 如果ping不通&#xff0c;…

react Ant Design

一、通过项目模版创建一个react项目 set NPM_CONFIG_REGISTRYhttps://registry.npmmirror.com pnpm create vite antd-demo cd antd-demo pnpm install pnpm install antd --save 打开项目&#xff1a; 修改&#xff1a;welcome.tsx import React from react; import { Butto…

【排序算法】——选择排序

前言 排序(Sorting) 是计算机程序设计中的一种重要操作&#xff0c;它的功能是将一个数据元素&#xff08;或记录&#xff09;的任意序列&#xff0c;重新排列成一个关键字有序的序列。所谓排序&#xff0c;就是使一串记录&#xff0c;按照其中的某个或某些关键字的大小&#x…