本文基于 TiDB release-5.1进行分析,需要用到 Go 1.16以后的版本

我的博客地址:https://www.luozhiyun.com/archives/605

这篇文章我们看一下 TiDB 是插入数据是如何封装的,索引是如何维护的,如果插入的数据发生了冲突会如何处理,类似INSERT IGNOREINSERT ON DUPLICATE KEY UPDATE插入语句是如何处理。

下面我们先构造一个表结构:

  1. CREATE TABLE test_insert (a int primary key, b int, c int,d int,index b_index(b),unique index c_index(c) );

这个表结构中有一个主键、普通索引、唯一索引。

普通 Insert

构建执行计划

普通插入 SQL 考虑的是类似下面这样的语句:

  1. INSERT INTO test.test_insert (a, b, c) VALUES (1, 1, 1);

首先会和 select 语法一样先进行语法解析构建 ast 语法树:

  1. type InsertStmt struct {
  2. dmlNode
  3. // sql 中的表信息
  4. Table *TableRefsClause
  5. // 字段信息
  6. Columns []*ColumnName
  7. // 要插入的数据
  8. Lists [][]ExprNode
  9. ...
  10. }

我这里展示的是几个比较重要的字段,因为在插入数据的时候可以使用 :INSERT INTO t VALUES(),(),()... 这样的语法,所以要插入的数据是一个切片:Lists。

然后制定查询计划,在制定查询计划的时候同样会走到 PlanBuilder 的 Build 方法中,然后根据 ast 语法树的类型 进入到 buildInsert 分支中:

  1. func (b *PlanBuilder) Build(ctx context.Context, node ast.Node) (Plan, error) {
  2. b.optFlag |= flagPrunColumns
  3. switch x := node.(type) {
  4. case *ast.InsertStmt:
  5. return b.buildInsert(ctx, x)
  6. ...
  7. func (b *PlanBuilder) buildInsert(ctx context.Context, insert *ast.InsertStmt) (Plan, error) {
  8. // 获取ast树中表节点
  9. ts, ok := insert.Table.TableRefs.Left.(*ast.TableSource)
  10. if !ok {
  11. return nil, infoschema.ErrTableNotExists.GenWithStackByArgs()
  12. }
  13. // 获取表的相关信息
  14. // 包含了表信息,库信息,分区信息等
  15. tn, ok := ts.Source.(*ast.TableName)
  16. if !ok {
  17. return nil, infoschema.ErrTableNotExists.GenWithStackByArgs()
  18. }
  19. // 获取其中表信息
  20. tableInfo := tn.TableInfo
  21. ...
  22. // Build Schema with DBName otherwise ColumnRef with DBName cannot match any Column in Schema.
  23. // schema包含表的字段信息,主键字段等,names是表的字段信息切片
  24. schema, names, err := expression.TableInfo2SchemaAndNames(b.ctx, tn.Schema, tableInfo)
  25. if err != nil {
  26. return nil, err
  27. }
  28. // 根据表的id从缓存中获取表的元数据
  29. // 这里包含的信息比较多,有表名、字段信息、隐藏字段、所有索引、表的字符集编码等
  30. tableInPlan, ok := b.is.TableByID(tableInfo.ID)
  31. if !ok {
  32. return nil, errors.Errorf("Can't get table %s.", tableInfo.Name.O)
  33. }
  34. // 构建插入执行计划
  35. insertPlan := Insert{
  36. Table: tableInPlan,
  37. Columns: insert.Columns,
  38. tableSchema: schema,
  39. tableColNames: names,
  40. IsReplace: insert.IsReplace,
  41. }.Init(b.ctx)
  42. ...
  43. // 根据不同的语法执行不同的分支
  44. // Branch for `INSERT ... SET ...`.
  45. if len(insert.Setlist) > 0 {
  46. // Branch for `INSERT ... VALUES ...`.
  47. } else if len(insert.Lists) > 0 {
  48. // 根据ast语法树中的= ast.ExprNode 转换成执行计划的 expression.Expression
  49. err := b.buildValuesListOfInsert(ctx, insert, insertPlan, mockTablePlan, checkRefColumn)
  50. if err != nil {
  51. return nil, err
  52. }
  53. // Branch for `INSERT ... SELECT ...`.
  54. } else {
  55. }
  56. ...
  57. return insertPlan, err
  58. }

buildInsert 这个方法主要涉及两个部分:

  • 补全表相关的元数据信息,包括 Database/Table/Column/Index 信息;
  • 处理 ast 语法树中要插入的 Lists 中的数据,将 ast.ExprNode 转换成 expression.Expression。

然后将构建好的 Insert 执行计划返回。

需要注意的是,由于 Insert 语句比较简单,没什么优化的空间,所以不会走 DoOptimize 进行物理优化:

  1. finalPlan, cost, err := plannercore.DoOptimize(ctx, sctx, builder.GetOptFlag(), logic)

执行 Insert 计划

  1. func (a *ExecStmt) Exec(ctx context.Context) (_ sqlexec.RecordSet, err error) {
  2. ...
  3. // 生成执行器
  4. e, err := a.buildExecutor()
  5. if err != nil {
  6. return nil, err
  7. }
  8. // ExecuteExec will rewrite `a.Plan`, so set plan label should be executed after `a.buildExecutor`.
  9. ctx = a.setPlanLabelForTopSQL(ctx)
  10. // handleNoDelay负责执行像 Insert 这种不需要返回数据的语句,只需要把语句执行完成即可
  11. if handled, result, err := a.handleNoDelay(ctx, e, isPessimistic); handled {
  12. return result, err
  13. }
  14. ...
  15. return &recordSet{
  16. executor: e,
  17. stmt: a,
  18. txnStartTS: txnStartTS,
  19. }, nil
  20. }

这里根据执行计划生成执行器的过程和 Select 是一致的,我们简单看一下。buildExecutor 方法最后会将执行计划转化成 InsertExec 结构体,后续的执行都由这个结构进行。

![Frame 2](https://img.luozhiyun.com/Frame 2.png)

在生成完执行计划之后会进入到 handleNoDelay 执行 SQL 语句。后面的执行流程比较长,我们省略一些中间环节:

![Frame 3](https://img.luozhiyun.com/Frame 3-3229191.png)

insertRows 会主要做的就是根据字段类型,获取数据之后做数据填充。

  1. func insertRows(ctx context.Context, base insertCommon) (err error) {
  2. // 获取 InsertValues 实例
  3. e := base.insertCommon()
  4. ...
  5. // 设置填充函数
  6. evalRowFunc := e.fastEvalRow
  7. // 如果要插入的数据不是常量,那么会使用evalRow函数
  8. if !e.allAssignmentsAreConstant {
  9. evalRowFunc = e.evalRow
  10. }
  11. rows := make([][]types.Datum, 0, len(e.Lists))
  12. for i, list := range e.Lists {
  13. e.rowCount++
  14. var row []types.Datum
  15. row, err = evalRowFunc(ctx, list, i)
  16. if err != nil {
  17. return err
  18. }
  19. ...
  20. }
  21. // 批量设置自增id
  22. rows, err = e.lazyAdjustAutoIncrementDatum(ctx, rows)
  23. if err != nil {
  24. return err
  25. }
  26. // 将数据写入存储引擎中
  27. err = base.exec(ctx, rows)
  28. if err != nil {
  29. return err
  30. }
  31. return nil
  32. }

insertRows 在填充数据的时候会判断数据类型,如果要处理的数据有非常量,比如有需要依赖其他字段设值、函数等等,这个时候会使用 evalRow 方法进行填充,否则使用 fastEvalRow 进行填充。最后将数据处理好之后会调用 InsertExec 的 exec 方法将数据写入存储引擎中。

  1. func (e *InsertExec) exec(ctx context.Context, rows [][]types.Datum) error {
  2. ...
  3. for i, row := range rows {
  4. ...
  5. err := e.addRecord(ctx, row)
  6. if err != nil {
  7. return err
  8. }
  9. }
  10. ...
  11. return nil
  12. }

在 exec 方法中会遍历所有的数据,然后调用 addRecord 方法进行处理。

InsertExec 的 addRecord 方法最终会调用到 TableCommon 的 AddRecord。

  1. func (t *TableCommon) AddRecord(sctx sessionctx.Context, r []types.Datum, opts ...table.AddRecordOption) (recordID kv.Handle, err error) {
  2. txn, err := sctx.Txn(true)
  3. if err != nil {
  4. return nil, err
  5. }
  6. ...
  7. writeBufs := sessVars.GetWriteStmtBufs()
  8. // 获取记录行的key
  9. key := t.RecordKey(recordID)
  10. // 格式化数据行
  11. writeBufs.RowValBuf, err = tablecodec.EncodeRow(sc, row, colIDs, writeBufs.RowValBuf, writeBufs.AddRowValues, rd)
  12. if err != nil {
  13. return nil, err
  14. }
  15. value := writeBufs.RowValBuf
  16. // 检测该key在本地缓存中是否存在
  17. var setPresume bool
  18. skipCheck := sctx.GetSessionVars().StmtCtx.BatchCheck
  19. if (t.meta.IsCommonHandle || t.meta.PKIsHandle) && !skipCheck && !opt.SkipHandleCheck {
  20. // 如果是 LazyCheck ,那么只读取本地缓存判断是否存在
  21. if sctx.GetSessionVars().LazyCheckKeyNotExists() {
  22. var v []byte
  23. //只读取本地缓存判断是否存在
  24. v, err = txn.GetMemBuffer().Get(ctx, key)
  25. if err != nil {
  26. setPresume = true
  27. }
  28. if err == nil && len(v) == 0 {
  29. err = kv.ErrNotExist
  30. }
  31. } else {
  32. //否则会通过rpc请求tikv从集群中校验数据是否存在
  33. _, err = txn.Get(ctx, key)
  34. }
  35. if err == nil {
  36. handleStr := getDuplicateErrorHandleString(t, recordID, r)
  37. return recordID, kv.ErrKeyExists.FastGenByArgs(handleStr, "PRIMARY")
  38. } else if !kv.ErrNotExist.Equal(err) {
  39. return recordID, err
  40. }
  41. }
  42. // 将 Key-Value 写到当前事务的缓存中
  43. if setPresume {
  44. err = memBuffer.SetWithFlags(key, value, kv.SetPresumeKeyNotExists)
  45. } else {
  46. err = memBuffer.Set(key, value)
  47. }
  48. if err != nil {
  49. return nil, err
  50. }
  51. // 构造 Index 数据
  52. h, err := t.addIndices(sctx, recordID, r, txn, createIdxOpts)
  53. if err != nil {
  54. return h, err
  55. }
  56. ...
  57. return recordID, nil
  58. }

AddRecord 主要做这么几件事:

  • 获取记录行的key,序列化 value,将 Key-Value 写到当前事务的缓存中;
  • 构造 Index 数据;

TiDB 中存储的数据是全局有序 的,并且数据会以 Key-Value的形式存储在 TiDB 中。

所以 TiDB 对每个表分配一个 TableID,每一个索引都会分配一个 IndexID,每一行分配一个 RowID(如果表有整数型的 Primary Key,那么会用 Primary Key 的值当做 RowID),其中 TableID 在整个集群内唯一,IndexID/RowID 在表内唯一,这些 ID 都是 int64 类型。

每行数据按照如下规则进行编码成 Key-Value pair:

  1. Key: tablePrefix{tableID}_recordPrefixSep{rowID}
  2. Value: [col1, col2, col3, col4]

那么对应的代码实现则会调用 RecordKey 方法获得一个这样的 Key:

  1. t.indexPrefix = tablecodec.GenTableIndexPrefix(physicalTableID)
  2. func (t *TableCommon) RecordKey(h kv.Handle) kv.Key {
  3. return tablecodec.EncodeRecordKey(t.recordPrefix, h)
  4. }

这个 Key 分别由 tableID 与 rowID 构成;

对于 Unique Index 数据,会按照如下规则编码成 Key-Value pair:

  1. Key: tablePrefix{tableID}_indexPrefixSep{indexID}_indexedColumnsValue
  2. Value: rowID

对于非Unique Index 数据,可能有多行数据的 ColumnsValue是一样的,所以会按照如下规则编码成 Key-Value pair:

  1. Key: tablePrefix{tableID}_indexPrefixSep{indexID}_indexedColumnsValue_rowID
  2. Value: null

对应的 Index 实现则会调用 addIndices 方法,最后调用到 GenIndexKey 生成Key:

  1. func GenIndexKey(sc *stmtctx.StatementContext, tblInfo *model.TableInfo, idxInfo *model.IndexInfo,
  2. phyTblID int64, indexedValues []types.Datum, h kv.Handle, buf []byte) (key []byte, distinct bool, err error) {
  3. // 校验是否是唯一键
  4. if idxInfo.Unique {
  5. distinct = true
  6. // 唯一键是允许 null 值的
  7. for _, cv := range indexedValues {
  8. if cv.IsNull() {
  9. distinct = false
  10. break
  11. }
  12. }
  13. }
  14. //如果是字符串,那么需要按字段长度裁切
  15. TruncateIndexValues(tblInfo, idxInfo, indexedValues)
  16. // 按 tablePrefix{tableID}_indexPrefixSep{indexID}_indexedColumnsValue 拼接
  17. key = GetIndexKeyBuf(buf, RecordRowKeyLen+len(indexedValues)*9+9)
  18. key = appendTableIndexPrefix(key, phyTblID)
  19. key = codec.EncodeInt(key, idxInfo.ID)
  20. key, err = codec.EncodeKey(sc, key, indexedValues...)
  21. if err != nil {
  22. return nil, false, err
  23. }
  24. if !distinct && h != nil {
  25. // 如果是非Unique Index 数据,还需要拼接上 rowID
  26. if h.IsInt() {
  27. key, err = codec.EncodeKey(sc, key, types.NewDatum(h.IntValue()))
  28. } else {
  29. key = append(key, h.Encoded()...)
  30. }
  31. }
  32. return
  33. }

GenIndexKey 这里会按照上面说到的规则进行拼接。

最后所有的 Key Value 构造完毕之后会将值写入到当前事务缓存中,等待提交。

  1. func (t *TableCommon) AddRecord(sctx sessionctx.Context, r []types.Datum, opts ...table.AddRecordOption) (recordID kv.Handle, err error) {
  2. ...
  3. var setPresume bool
  4. skipCheck := sctx.GetSessionVars().StmtCtx.BatchCheck
  5. if (t.meta.IsCommonHandle || t.meta.PKIsHandle) && !skipCheck && !opt.SkipHandleCheck {
  6. // 如果是 LazyCheck ,那么只读取本地缓存判断是否存在
  7. if sctx.GetSessionVars().LazyCheckKeyNotExists() {
  8. var v []byte
  9. //只读取本地缓存判断是否存在
  10. v, err = txn.GetMemBuffer().Get(ctx, key)
  11. if err != nil {
  12. setPresume = true
  13. }
  14. if err == nil && len(v) == 0 {
  15. err = kv.ErrNotExist
  16. }
  17. } else {
  18. //否则会通过rpc请求tikv从集群中校验数据是否存在
  19. _, err = txn.Get(ctx, key)
  20. }
  21. if err == nil {
  22. handleStr := getDuplicateErrorHandleString(t, recordID, r)
  23. return recordID, kv.ErrKeyExists.FastGenByArgs(handleStr, "PRIMARY")
  24. } else if !kv.ErrNotExist.Equal(err) {
  25. return recordID, err
  26. }
  27. }
  28. //将 Key-Value 写到当前事务的缓存中
  29. if setPresume {
  30. // 表示假定数据不存在
  31. err = memBuffer.SetWithFlags(key, value, kv.SetPresumeKeyNotExists)
  32. } else {
  33. err = memBuffer.Set(key, value)
  34. }
  35. if err != nil {
  36. return nil, err
  37. }
  38. ...
  39. }

由于在设计上,TiDB 与 TiKV 是分层的结构,为了保证高效率的执行,在 LazyCheck 模式下,在事务内只有读操作是必须从存储引擎获取数据,而所有的写操作都事先放在单 TiDB 实例内事务自有的 memDbBuffer 中,在事务提交时才一次性将事务写入 TiKV。

如上面代码所示,在调用 AddRecord 时,会根据 Key 从 MemBuffer 中判断是否存在,不存在那么在操作 memBuffer 的时候会打上标记 SetPresumeKeyNotExists 表示假设插入不会发生冲突,不需要去 TiKV 中检查冲突数据是否存在,只将这些数据标记为待检测状态。最后到提交过程中,统一将整个事务里待检测数据做一次批量检测。

下面通过一个官方的例子来说明一下 LazyCheck 模式下 MySQL 和 TiDB 的区别:

MySQL:

  1. mysql> CREATE TABLE t (i INT UNIQUE);
  2. Query OK, 0 rows affected (0.15 sec)
  3. mysql> INSERT INTO t VALUES (1);
  4. Query OK, 1 row affected (0.01 sec)
  5. mysql> BEGIN;
  6. Query OK, 0 rows affected (0.00 sec)
  7. mysql> INSERT INTO t VALUES (1);
  8. ERROR 1062 (23000): Duplicate entry '1' for key 'i'
  9. mysql> COMMIT;
  10. Query OK, 0 rows affected (0.11 sec)

TiDB:

  1. mysql> CREATE TABLE t (i INT UNIQUE);
  2. Query OK, 0 rows affected (1.04 sec)
  3. mysql> INSERT INTO t VALUES (1);
  4. Query OK, 1 row affected (0.12 sec)
  5. mysql> BEGIN;
  6. Query OK, 0 rows affected (0.01 sec)
  7. mysql> INSERT INTO t VALUES (1);
  8. Query OK, 1 row affected (0.00 sec)
  9. mysql> COMMIT;
  10. ERROR 1062 (23000): Duplicate entry '1' for key 'i'

可以看出来,对于 INSERT 语句 TiDB 是在事务提交的时候才做冲突检测而 MySQL 是在语句执行的时候做的检测。

最后让我们用一幅图来再回顾一下整个流程:

INSERT IGNORE

INSERT IGNORE和普通 Insert 不同的是当 INSERT 的时候遇到唯一约束冲突后,忽略当前 INSERT 的行,并记一个 warning。当语句执行结束后,可以通过 SHOW WARNINGS看到哪些行没有被插入。

为了实现这个目的又不影响性能,TiDB 通过 batchCheckAndInsert 批量检测来校验数据是否冲突:

  1. func (e *InsertExec) exec(ctx context.Context, rows [][]types.Datum) error {
  2. ...
  3. sessVars := e.ctx.GetSessionVars()
  4. defer sessVars.CleanBuffers()
  5. ignoreErr := sessVars.StmtCtx.DupKeyAsWarning
  6. // 判断是否有 OnDuplicate 语句
  7. if len(e.OnDuplicate) > 0 {
  8. ...
  9. // 判断是否包含 IGNORE 语句
  10. } else if ignoreErr {
  11. // 判断是否重复,不重复则插入
  12. err := e.batchCheckAndInsert(ctx, rows, e.addRecord)
  13. if err != nil {
  14. return err
  15. }
  16. // 普通 Insert
  17. } else {
  18. ...
  19. }
  20. return nil
  21. }

在 InsertExec 的 exec 方法中如果 SQL 语句包含 IGNORE 会进入到 IF 判断的第二个分支中调用 batchCheckAndInsert 方法进行冲突校验。

  1. func (e *InsertValues) batchCheckAndInsert(ctx context.Context, rows [][]types.Datum, addRecord func(ctx context.Context, row []types.Datum) error) error {
  2. ...
  3. start := time.Now()
  4. // 获取行数据中需要校验的key,如主键,唯一键
  5. toBeCheckedRows, err := getKeysNeedCheck(ctx, e.ctx, e.Table, rows)
  6. if err != nil {
  7. return err
  8. }
  9. // 获取事务处理器
  10. txn, err := e.ctx.Txn(true)
  11. if err != nil {
  12. return err
  13. }
  14. // 批量从 tikv 中根据传入的 key 获取数据,存入到缓存中
  15. if _, err = prefetchUniqueIndices(ctx, txn, toBeCheckedRows); err != nil {
  16. return err
  17. }
  18. for i, r := range toBeCheckedRows {
  19. if r.ignored {
  20. continue
  21. }
  22. skip := false
  23. // 判断主键
  24. if r.handleKey != nil {
  25. // 从缓存中判断key是否存在,存在则重复
  26. _, err := txn.Get(ctx, r.handleKey.newKey)
  27. if err == nil {
  28. e.ctx.GetSessionVars().StmtCtx.AppendWarning(r.handleKey.dupErr)
  29. continue
  30. }
  31. if !kv.IsErrNotFound(err) {
  32. return err
  33. }
  34. }
  35. // 判断唯一键
  36. for _, uk := range r.uniqueKeys {
  37. // 从缓存中判断key是否存在,存在则重复
  38. _, err := txn.Get(ctx, uk.newKey)
  39. if err == nil {
  40. // If duplicate keys were found in BatchGet, mark row = nil.
  41. e.ctx.GetSessionVars().StmtCtx.AppendWarning(uk.dupErr)
  42. skip = true
  43. break
  44. }
  45. if !kv.IsErrNotFound(err) {
  46. return err
  47. }
  48. }
  49. // 没有冲突,调用 addRecord 添加数据
  50. if !skip {
  51. e.ctx.GetSessionVars().StmtCtx.AddCopiedRows(1)
  52. err = addRecord(ctx, rows[i])
  53. if err != nil {
  54. return err
  55. }
  56. }
  57. }
  58. return nil
  59. }

这一段代码比较长,但是也很好理解。

  • getKeysNeedCheck 作用是根据所有的 rows 数据封装好里面唯一键和主键的key,按照 TiKV 中存储的格式封装,我在上面普通 Insert 已经讲过了,这里就不再重复贴出 Key 的规则;
  • prefetchUniqueIndices 是根据 toBeCheckedRows 里面封装好的 Key 通过 BatchGet 发送 RPC 请求批量去 TiKV 获取数据,然后存入到缓存中;
  • 然后会遍历 toBeCheckedRows 这里面的主键和唯一键,通过 txn.Get从缓存中判断key是否存在,存在则重复;
  • 最后如果不冲突,那么会调用 addRecord 将数据缓存到本地事务中。

ON DUPLICATE

ON DUPLICATE 指的是INSERT ON DUPLICATE KEY UPDATE语句,它是几种 INSERT 语句中最为复杂的。其语义的本质是包含了一个 INSERT 和 一个 UPDATE。

它的入口在 InsertExec 执行 exec 方法的时候:

  1. func (e *InsertExec) exec(ctx context.Context, rows [][]types.Datum) error {
  2. ...
  3. sessVars := e.ctx.GetSessionVars()
  4. defer sessVars.CleanBuffers()
  5. ignoreErr := sessVars.StmtCtx.DupKeyAsWarning
  6. // 判断是否有 OnDuplicate 语句
  7. if len(e.OnDuplicate) > 0 {
  8. err := e.batchUpdateDupRows(ctx, rows)
  9. if err != nil {
  10. return err
  11. }
  12. // 判断是否包含 IGNORE 语句
  13. } else if ignoreErr {
  14. ...
  15. // 普通 Insert
  16. } else {
  17. ...
  18. }
  19. return nil
  20. }

INSERT IGNORE相同,首先会进入 IF 分支,判断是否包含 ON DUPLICATE执行语句,然后执行 batchUpdateDupRows 方法。

  1. func (e *InsertExec) batchUpdateDupRows(ctx context.Context, newRows [][]types.Datum) error {
  2. ...
  3. // 构造唯一键和主键的key
  4. toBeCheckedRows, err := getKeysNeedCheck(ctx, e.ctx, e.Table, newRows)
  5. if err != nil {
  6. return err
  7. }
  8. txn, err := e.ctx.Txn(true)
  9. if err != nil {
  10. return err
  11. }
  12. // 根据key填充对应的缓存
  13. if err = prefetchDataCache(ctx, txn, toBeCheckedRows); err != nil {
  14. return err
  15. }
  16. for i, r := range toBeCheckedRows {
  17. if r.handleKey != nil {
  18. handle, err := tablecodec.DecodeRowKey(r.handleKey.newKey)
  19. if err != nil {
  20. return err
  21. }
  22. // 根据主键判断是否有冲突,如果有冲突 err 则为 nil
  23. err = e.updateDupRow(ctx, i, txn, r, handle, e.OnDuplicate)
  24. if err == nil {
  25. continue
  26. }
  27. if !kv.IsErrNotFound(err) {
  28. return err
  29. }
  30. }
  31. // 如果主键没有冲突,那么判断唯一键是否有冲突
  32. for _, uk := range r.uniqueKeys {
  33. val, err := txn.Get(ctx, uk.newKey)
  34. if err != nil {
  35. if kv.IsErrNotFound(err) {
  36. continue
  37. }
  38. return err
  39. }
  40. handle, err := tablecodec.DecodeHandleInUniqueIndexValue(val, uk.commonHandle)
  41. if err != nil {
  42. return err
  43. }
  44. err = e.updateDupRow(ctx, i, txn, r, handle, e.OnDuplicate)
  45. if err != nil {
  46. return err
  47. }
  48. newRows[i] = nil
  49. break
  50. }
  51. // 如果主键和唯一键都没有冲突,那么执行正常插入逻辑
  52. if newRows[i] != nil {
  53. err := e.addRecord(ctx, newRows[i])
  54. if err != nil {
  55. return err
  56. }
  57. }
  58. }
  59. if e.stats != nil {
  60. e.stats.CheckInsertTime += time.Since(start)
  61. }
  62. return nil
  63. }

batchUpdateDupRows 方法首先会构造唯一键和主键的 key ,然后调用 prefetchDataCache 方法根据 Key 值一次性获取 TiKV 对应值填充缓存。

之后遍历构造好的 toBeCheckedRows ,先调用 updateDupRow 方法判断主键判断是否有冲突,如果主键没有冲突,那么判断唯一键是否有冲突,都没有冲突则执行正常插入逻辑。

updateDupRow 会判断 Key 值在缓存中是否存在,存在则调用 doDupRowUpdate ;doDupRowUpdate 中会根据 ON DUPLICATE 中的字段更新新的数据行中的值,并将被更新过的字段打上 flag 之后调用 updateRecord 函数。

  1. func updateRecord(ctx context.Context, sctx sessionctx.Context, h kv.Handle, oldData, newData []types.Datum, modified []bool, t table.Table,
  2. onDup bool, memTracker *memory.Tracker) (bool, error) {
  3. txn, err := sctx.Txn(false)
  4. if err != nil {
  5. return false, err
  6. }
  7. changed, handleChanged := false, false
  8. ...
  9. for i, col := range t.Cols() {
  10. // 这里是新旧数据进行比较,如果相同返回0
  11. cmp, err := newData[i].CompareDatum(sc, &oldData[i])
  12. if err != nil {
  13. return false, err
  14. }
  15. //这里表明新旧数据不同
  16. if cmp != 0 {
  17. changed = true //设置标记位,表示有数据被修改
  18. modified[i] = true
  19. ...
  20. // 如果是主键更改,设置 handleChanged
  21. if col.IsPKHandleColumn(t.Meta()) {
  22. handleChanged = true
  23. if err := rebaseAutoRandomValue(sctx, t, &newData[i], col); err != nil {
  24. return false, err
  25. }
  26. }
  27. // 如果是主键更改,设置 handleChanged
  28. if col.IsCommonHandleColumn(t.Meta()) {
  29. handleChanged = true
  30. }
  31. // 表示该字段没有被更改
  32. } else {
  33. if mysql.HasOnUpdateNowFlag(col.Flag) && modified[i] {
  34. onUpdateSpecified[i] = true
  35. }
  36. modified[i] = false
  37. }
  38. }
  39. // 如果数据行没有变化,直接返回
  40. if !changed {
  41. ...
  42. return false, nil
  43. }
  44. // 这里如果是主键被更改了,那么会先将原数据删除,再添加一条新的数据
  45. if handleChanged {
  46. if updated, err := func() (bool, error) {
  47. txn, err := sctx.Txn(true)
  48. if err != nil {
  49. return false, err
  50. }
  51. memBuffer := txn.GetMemBuffer()
  52. sh := memBuffer.Staging()
  53. defer memBuffer.Cleanup(sh)
  54. if err = t.RemoveRecord(sctx, h, oldData); err != nil {
  55. return false, err
  56. }
  57. _, err = t.AddRecord(sctx, newData, table.IsUpdate, table.WithCtx(ctx))
  58. if err != nil {
  59. return false, err
  60. }
  61. memBuffer.Release(sh)
  62. return true, nil
  63. }(); err != nil {
  64. if terr, ok := errors.Cause(err).(*terror.Error); sctx.GetSessionVars().StmtCtx.IgnoreNoPartition && ok && terr.Code() == errno.ErrNoPartitionForGivenValue {
  65. return false, nil
  66. }
  67. return updated, err
  68. }
  69. } else {
  70. // 更新记录行
  71. if err = t.UpdateRecord(ctx, sctx, h, oldData, newData, modified); err != nil {
  72. if terr, ok := errors.Cause(err).(*terror.Error); sctx.GetSessionVars().StmtCtx.IgnoreNoPartition && ok && terr.Code() == errno.ErrNoPartitionForGivenValue {
  73. return false, nil
  74. }
  75. return false, err
  76. }
  77. }
  78. ...
  79. return true, nil
  80. }

updateRecord 会判断行数据有没有被更改,如果有被更改,那么分为两种情况:

  1. 主键被更改了,那么会先将原数据删除,再添加一条新的数据;
  2. 唯一键被更改会调用 UpdateRecord 更新记录行;
  1. func (t *TableCommon) UpdateRecord(ctx context.Context, sctx sessionctx.Context, h kv.Handle, oldData, newData []types.Datum, touched []bool) error {
  2. txn, err := sctx.Txn(true)
  3. if err != nil {
  4. return err
  5. }
  6. memBuffer := txn.GetMemBuffer()
  7. ...
  8. // 重建索引记录
  9. err = t.rebuildIndices(sctx, txn, h, touched, oldData, newData, table.WithCtx(ctx))
  10. if err != nil {
  11. return err
  12. }
  13. // 构建行记录key
  14. key := t.RecordKey(h)
  15. sc, rd := sessVars.StmtCtx, &sessVars.RowEncoder
  16. // 构建行记录value
  17. value, err := tablecodec.EncodeRow(sc, row, colIDs, nil, nil, rd)
  18. if err != nil {
  19. return err
  20. }
  21. // 将数据添加到事务缓存中
  22. if err = memBuffer.Set(key, value); err != nil {
  23. return err
  24. }
  25. memBuffer.Release(sh)
  26. ...
  27. return nil
  28. }

UpdateRecord 中执行的逻辑和 AddRecord 有点类似,首先会调用 rebuildIndices 将旧的索引记录删除,重新构建新的索引;然后根据当前的行记录构建 key-value 添加到事务缓存中。

最后用一张图总结一下这个过程:

总结

这篇文章 debug 用了蛮长时间的,想要弄清楚其中的逻辑非常不容易,但是还有一些地方没弄明白,如在执行 ON DUPLICATE会更新数据行,那么数据一致性怎么保证的?这些疑问我想到时候留给事务章节去弄明白。

Reference

https://pingcap.com/zh/blog/tidb-source-code-reading-4

https://pingcap.com/zh/blog/tidb-source-code-reading-16

5.深入TiDB:Insert 语句的更多相关文章

  1. 跟随一条insert语句, 进入TiDB的源码世界(上)

    TiDB是Google F1的开源实现: TiDB实现了基于mvcc的乐观锁,在线表结构变更,基于时间戳的数据线性一致性,等等: 为了可靠性,TiDB和Oracle一样,维护了百万级别的自动化测试用例 ...

  2. 将表里的数据批量生成INSERT语句的存储过程 增强版

    将表里的数据批量生成INSERT语句的存储过程 增强版 有时候,我们需要将某个表里的数据全部或者根据查询条件导出来,迁移到另一个相同结构的库中 目前SQL Server里面是没有相关的工具根据查询条件 ...

  3. PL/SQL客户端中执行insert语句,插入中文乱码

    问题描述:在PL/SQL客户端中执行insert语句,插入中文乱码 解决方案: 1.执行脚本 select userenv('language') from dual;    结果为AMERICAN_ ...

  4. Sql Server系列:Insert语句

    1 INSERT语法 [ WITH <common_table_expression> [ ,...n ] ] INSERT { [ TOP ( expression ) [ PERCEN ...

  5. SQLServer将表数据导出为Insert语句

    从网上找到的方法,不过很不错,记录下来,也算是分享下~~ 有一个表,city,有列:cityID,cityName;将此表中所有数据,变为insert语句 select 'insert into ta ...

  6. 一条诡异的insert语句

    问题背景 有同事反馈在mysql上面执行一条普通的insert语句,结果报错, execute failed due to >>> Incorrect string value: ' ...

  7. 将表里的数据批量生成INSERT语句的存储过程 继续增强版

    文章继续 桦仔兄的文章 将表里的数据批量生成INSERT语句的存储过程 增强版 继续增强... 本来打算将该内容回复于桦仔兄的文章的下面的,但是不知为何博客园就是不让提交!.... 所以在这里贴出来吧 ...

  8. 取得表中数据的insert语句

    Build Insert Statements for the Existing Data in Tables 下面这个脚本实现了取得一个非空表中的所有insert语句 This script bui ...

  9. 【SQL Sever】将SQL Sever中的一个数据表的数据导出为insert语句

    例如:这SQL   Sever中的一张数据表,想要将这张数据表中的数据  转化成一个一个的insert语句存储在txt的文档中,那么不论走到那里这个insert语句一执行,我们就能将这个数据表中的数据 ...

随机推荐

  1. Spring中常用重要的接口

    Spring (ApplicationContext 初始化Bean的方法 refresh()) public void refresh() throws BeansException, Illega ...

  2. 剑指offer面试题4

    1 #include<iostream> 2 using namespace std; 3 4 void replace(char ch[]) { 5 int count = 0; 6 i ...

  3. ArrayPool 源码解读之 byte[] 也能池化?

    一:背景 1. 讲故事 最近在分析一个 dump 的过程中发现其在 gen2 和 LOH 上有不少size较大的free,仔细看了下,这些free生前大多都是模板引擎生成的html片段的byte[]数 ...

  4. 设置 Qt GUI程序 printf输出到独立控制台

  5. 前端~定位属性position(relative、absolute、fixed)的分析

    前端~定位属性position(relative.absolute.fixed)的分析 1,简单了解: relative:移动自身时,参考自身的原来位置而移动,移动子元素(子元素设置了absolute ...

  6. 编程读写CAD文件验证

    背景 B/S应用系统,根据用户上传数据:业务数据和CAD坐标数据,经过一系列运筹算法运算后,输出一批坐标数据,作为给用户的规划结果.此时需要方便直观的给用户展示坐标数据.可选方式有两个: web页面画 ...

  7. WEB漏洞——SQL

    由于我的博客是学到渗透的时候才做的,没有关于WEB漏洞的笔记,现在发现WEB层面的漏洞有些不太熟悉了,边写一下笔记边复习一下,就从sql注入开始吧 话不多说先上大佬写的表[ctfhub]SQL注入 - ...

  8. djangoProject default codes

    1 === 2 settings.py 3 === 4 """ 5 Django settings for djangoProject project. 6 7 Gene ...

  9. python库--tensorflow--io操作

    方法 返回值类型 参数 说明 .train.Saver() 实例s var_list=None 指定被保存和恢复的变量 dict: {name: 变量} list: [变量] None: 所有save ...

  10. 剑指 Offer 60. n个骰子的点数

    剑指 Offer 60. n个骰子的点数 把n个骰子扔在地上,所有骰子朝上一面的点数之和为s.输入n,打印出s的所有可能的值出现的概率. 你需要用一个浮点数数组返回答案,其中第 i 个元素代表这 n ...