# [Critical bug] v4.0.9:10 事务重试导致数据错误问题

## Issue

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

使用 autocommit = 0 执行事务，之后提交，且打开 tidb 自动重试机制，会导致数据被写坏，且数据索引出现不一致

用户报在使用过程中出现主键（唯一索引非 handle 列）重复的问题 admin check table 报错

```sql
admin CHECK TABLE pi_product_inspection;
/* SQL错误（1105）：col status, handle 2788969, index:types.Datum{k:0x5, collation:"utf8_bin", decimal:0x0, length:0x0, i:0, b:[]uint8{0xe5, 0xbe, 0x85, 0xe6, 0xa3, 0x80}, x:interface {}(nil)} != record:types.Datum{k:0x5, collation:"utf8_bin", decimal:0x0, length:0x0, i:0, b:[]uint8{0xe5, 0xae, 0x8c, 0xe6, 0x88, 0x90}, x:interface {}(nil)} */
```

存在多余的索引数据，通过多版本数据，发现多余的索引数据是某个发生过自动重试的事务提交写入的

## Root Cause

当事务提交失败时，如果自动重试开关开启，tidb 会重新获取事务上下文将执行语句重新执行之后尝试提交失败的事务。该重试过程中，首先需要初始化新的事务上下文。当使用 autocommit = 0 的事务模式且重试发生时，其中有一个名为 mysql.ServerStatusInTrans 的 flag 没有被正确设置，导致事务重试执行过程中，依据该 flag 判断的路径结果出现问题，如 point get 执行算子的路径。对于 update 类型的语句，其读取的数据源可能出错，例如应当从 memory buffer 中读取但实际从 snapshot 读取，之后进行计算导致结果出错。

```sql
update t set c1 = 11 where pk = '1';
```

这里的操作结果是：

```sql
index_k1_10_100 -> nil 删除旧的索引 key

index_k1_11_100 -> xxx 写入新的索引 key

handle -> row_data 写入新的 row data

update t set c2 = 101 where pk = '1';
```

因为读错了数据源，索引读到的数据是 1 10 100

```sql
index_k1_10_100 -> nil 删除旧的索引 key

index_k1_10_101 -> xxx 写入新的索引 key

handle -> row_data 写入新的 row data 2
```

最终提交的结果是

```sql
handle -> row_data 写入新的 row data 2 这里结果出错

index_k1_11_100 -> xxx 写入新的索引 key 错误结果的索引

index_k1_10_101 -> xxx 写入新的索引 key 错误结果多余的索引
```

事务重试时，尝试设置正确的 txn，txnContext 和 session variable 相关的变量，PR

## Diagnostic Steps

简单复现步骤：

初始状态

```sql
drop table if exists t;
CREATE TABLE `t` (
  `pk` varchar(50) NOT NULL,
  `c1` varchar(50) DEFAULT NULL,
  `c2` varchar(50) DEFAULT NULL,
  KEY `k1` (`c1`,`c2`),
  PRIMARY KEY (`pk`)
) 
insert into t values('1', '10', '100');
```

开启 tidb 自动重试

```sql
set tidb_disable_txn_auto_retry = 0;
```

### 检查结果

这行数据的结果是错的

```sql
mysql> select * from t;
+----+------+------+
| pk | c1   | c2   |
+----+------+------+
| 1  | 10   | 101  |
+----+------+------+
1 row in set (0.01 sec)
```

正确的结果

```sql
mysql> select * from t;
+----+------+------+
| pk | c1   | c2   |
+----+------+------+
| 1  | 11   | 101  |
+----+------+------+
1 row in set (0.00 sec)
```

索引数据多了一行 (两条 index 记录回表指向同一个 handle 的 record)

```sql
mysql> select * from t use index(k1);
+----+------+------+
| pk | c1   | c2   |
+----+------+------+
| 1  | 10   | 101  |
| 1  | 10   | 101  |
+----+------+------+
2 rows in set (0.01 sec)
```

直接看两条 index 的 values

```sql
mysql> select c1, c2 from t use index(k1);
+------+------+
| c1   | c2   |
+------+------+
| 10   | 101  |
| 11   | 100  |
+------+------+
2 rows in set (0.00 sec)
```

admin check table 结果报错

```sql
mysql> admin check table t;
ERROR 1105 (HY000): col c1, handle 2, index:types.Datum{k:0x5, collation:"utf8mb4_bin", decimal:0x0, length:0x0, i:0, b:[]uint8{0x31, 0x31}, x:interface {}(nil)} != record:types.Datum{k:0x5, collation:"utf8mb4_bin", decimal:0x0, length:0x0, i:0, b:[]uint8{0x31, 0x30}, x:interface {}(nil)}
[WARN] [session.go:727] ["transaction association"] [conn=3] ["retrying txnStartTS"=423098167329554433] ["original txnStartTS"=423098165114175489] [isPessimistic=false]
```

检查多版本

```json
curl "http://127.0.0.1:10080/mvcc/key/test/t/2"
{
 "key": "74800000000000004C5F728000000000000002",
 "region_id": 2,
 "value": {
  "info": {
   "writes": [
    {
     "start_ts": 423098167329554433,
     "commit_ts": 423098167342661633,
     "short_value": "CAICAjEIBAIEMTAIBgIGMTAx"
    },
    {
     "start_ts": 423098163462668292,
     "commit_ts": 423098163475775489,
     "short_value": "CAICAjEIBAIEMTAIBgIGMTAw"
    }
   ]
  }
 }
}
```

handle 值

```sql
mysql> select *, _tidb_rowid from t;
+----+------+------+-------------+
| pk | c1   | c2   | _tidb_rowid |
+----+------+------+-------------+
| 1  | 10   | 101  |           2 |
+----+------+------+-------------+
1 row in set (0.00 sec)
```

row 的数据

```json
format: index_value
index_value[0]: {type: bigint, value: 1}
index_value[1]: {type: bytes, value: 1}
index_value[2]: {type: bigint, value: 2}
index_value[3]: {type: bytes, value: 10}
index_value[4]: {type: bigint, value: 3}
index_value[5]: {type: bytes, value: 101}
```

index k1 数据当 c1 = 10 c2 = 101

```json
curl "http://127.0.0.1:10080/mvcc/index/test/t/k1/2?c1=10&c2=101"
{
 "key": "74800000000000004C5F698000000000000001013130000000000000F9013130310000000000FA038000000000000002",
 "region_id": 2,
 "value": {
  "info": {
   "writes": [
    {
     "start_ts": 423098167329554433,
     "commit_ts": 423098167342661633,
     "short_value": "MA=="
    }
   ]
  }
 }
}
```

index k1 数据当 c1 = 11 c2 = 100

```json
curl "http://127.0.0.1:10080/mvcc/index/test/t/k1/2?c1=11&c2=100"
{
 "key": "74800000000000004C5F698000000000000001013131000000000000F9013130300000000000FA038000000000000002",
 "region_id": 2,
 "value": {
  "info": {
   "writes": [
    {
     "start_ts": 423098167329554433,
     "commit_ts": 423098167342661633,
     "short_value": "MA=="
    }
   ]
  }
 }
}
```

> **注意**
>
> 可见 row 数据本身是错的，且索引数据多了一个

## Resolution

### 规避方法

避免使用自动重试

必须使用自动重试的且包含多条语句的事务，用 begin 显式开启事务避免使用 autocommit = 0 的模式

### 数据修复

已经写坏的行数据，需要从业务侧根据业务逻辑进行修复。后台如果多版本数据没有被 GC，可以考虑查询多版本数据辅助恢复。

多余的索引数据，可以删除索引之后进行重建

## Workaround

N/A