# [Critical bug] 并行执行有损列类型变更时，导致表结构中列类型和数据不一致

💡  <https://internal.pingcap.net/jira/browse/ONCALL-4265>

## Issue

对于列类型变更，如果是有损变更，会改掉 column ID。

如果有两个 session 同时执行有损的列类型变更（例如：modify column col char -> int）：

connection1 执行 DDL1 , DDL1 认为自己是有损变更，更改 column ID（id:1->id:2）

connection2 执行 DDL2

当 DDL1 刚进 DDL job 队列且并未完成执行时，DDL2 进入 DDL job 队列

当 DDL1 执行成功后，执行 DDL job 队列中的 DDL2。此时的 DDL2 认为它不是有损变更（char -> char），所以用了老的（DDL1 执行成功前的）column ID，即此时 column ID 是 1。所以最后导致 id:1 覆盖 id:2

这样导致 DDL2 完成后，column ID 还是 DD1 执行前的 ID（id:1），而不是正确的 id:2。即导致列的 schema 信息与列数据不匹配，也可能导致数据索引不一致。

注意
其中 DDL1 和 DDL2 可能是不一样的语句，比如

DDL1: alter table t modify column col int;

DDL2: alter table t modify column col int comment "new xxx";

## Root Cause

在两个 connection 同时执行有损的列类型变更（例如：modify column col char -> int），由于后执行列类型变更操作时用旧列（两个 DDL 语句都没有执行前）的 ID 覆盖了新列 ID，导致表结构中列类型和数据不一致的问题。比较详细介绍：遇到同一个列进行并发列类型更改导致表结构和数据不一致问题的解决方案

## Diagnostic Steps

TiDB 版本为 v5.1.0 及以上版本（具体参考影响版本）。

对一个表的同一列，同时执行类似 “alter table tblName modify/change colName...” 语句，且执行的语句都是有损变更（即需要 reorg data 的类型）。

触发该问题之后，由于 schema 信息与列数据不匹配，可能导致如下的现象，具体现象取决于列变更的类型以及后续的写入操作类型：

对数据访问报 “insufficient bytes to decode” 错误。

对数据更新报 “Out of range value for column” 错误。

对数据访问不报错，但数据不正确，例如出现乱码字符或是非预期的数值。

ADMIN CHECK TABLE 校验数据/索引一致性报错。

## Resolution

如果已经出现此问题，根据不同场景分为如下几种处理方式：

* 如果此列数据不影响业务，可以通过 drop column 减少业务影响，并恢复正常使用。

* 带 TiCDC 组件的集群，且没有执行 update 语句（或者有相同功能的操作，类似 "replace" 和 "insert into ... on duplicate"），可以修复数据

    * 在 TiCDC 上 DDL 是串行的，所以认为 TiCDC 同步到下游的数据是正确的，即可以通过下游数据库修复数据，具体有两种方式：

        * 如果 TiCDC 下游是 TiDB 集群时，业务端先停止对应表的操作，可以通过 BR 从下游备份对应表的数据，然后将此数据同步到上游 TiDB，最后恢复业务。

        * 如果 TiCDC 下游是 MySQL 等其他可以使用 Dumpling 导出的集群。业务端先停止对应表的操作，可以先用 Dumpling 导出对应表的数据，然后用 lighting 导入对应表的数据到上游 TiDB。最后恢复业务。

* 此列带有索引的情况，可以修复数据。不过对原代码需要有一定改动，谨慎使用。

    * 在这种场景下，因为 backfill 索引时用的是正确列的数据，所以索引的数据是正确的，即可以通过索引修复数据。

      1. 索引我们可以使用类似“update tbl t1 set correspondingCol = (select correspondingCol from tbl t2 use index(correspondingIdx) where t2._tidb_rowid = t1._tidb_rowid)” 语句的方式恢复数据。不过目前此语句会走 TableFullScan，即还是会读错误列的信息。

      2. 需要根据具体情况准备 hotfix，让其在此 update 语句时不用读（decode）当前列(老的 column ID) 的数据。

* 此列上不带索引，且没有 TiCDC 组件的集群，能否修复数据需要具体分析。主要考虑第一个 DDL 语句执行开始到当前阶段（停止业务）的写入操作执行情况：

    1. 没有对此表进行过写入操作，可以修复数据。

        1. 先删除对应列相关的索引。

        2. 然后用 admin repair table 操作，将当前表的列类型改成执行 modify column 操作前的列类型

        3. 恢复正常模式，重新执行 modify column 操作，如有需要添加原先索引。

    2. 对应表只进行过 insert （不带 select）的写操作，需要已有数据根据不同情况进行解析数据然后恢复。不过可以修复的方法需要改代码且实现也比较复杂，不建议使用。

        1. 将读取对应列的数据和 changing col 列 ID 对应数据比对，如果 changing col 列有数据，则解析 changing col 列数据，如果没有就解析当前列的数据。（最好有个检查，确认是否只有此类 insert 语句的写入 ）。

        2. 需要根据具体情况准备 hotfix。

    3. 对应表进行过 insert... select 或者 update 或者 delete（这有必要的话也可以细分）的写操作，需要确认列类型变更前后，decode 此列的方式是否兼容。

        1. 不兼容。那么执行此类写操作的时候都会报错，可以直接参考 a 场景进行数据修复。

        2. 兼容。由于可以执行此类写操作，对原来的数据可能做了不可预知的操作（比如 double -> int 类型，执行 update t set col = col +1 ），导致不管使用修改列前还是修改列后的类型都不能解析得到预期结果。所以此场景下，数据基本上不能修复了。

### 修复版本

升级到 5.1.5、5.2.4、5.3.1.

## Workaround

规避方法
禁止用户对一个表的同一列并行执行 "alter table ... modify/change column" 语句