Deadlock found when trying to get lock

TIDB版本5.1,有个表A,有字段 用户id(int)、日期(date)、游戏版本id(int)、批次号(varchar2)、金额(decimal) 等字段,其中有2个普通复合索引(没有唯一索引,用户id+日期 和 日期+游戏版本id)。A表会进行查询、增加和更新操作(不会有删除操作)。flink流作业中,已经对用户id进行分组,确保相同的用户id只会到一个slot中(用户id+日期+游戏版本id+批次号,表记录唯一),同时设置 transaction_isolation = ‘READ-COMMITTED’(默认REPEATABLE READ),但是经常会出现死锁的报错(Deadlock found when trying to get lock),不明白产生的原因以及如何解决。
PS:业务上有对死锁进行检测和重试,不影响业务,但是想搞清楚根本原因及处理方式。
PS:在一个事务中,一条记录会有2条语句,一条不存在插入,一条更新。每100条记录(200条语句)提交一次事务。
insert into A select xx from dual where not exists (select 1 from A where 用户id=xx and 日期=xx and 游戏版本=xx and 批次号=xx);
updte A set 金额=金额+xx where 用户id=xx and 日期=xx and 游戏版本=xx and 批次号=xx;

写写冲突,读写冲突,缠一起了,就容易死锁

建议使用悲观事务

默认就是悲观事务的

猜测和二级索引有关,有些版有因为普通二级索引造成的锁冲突问题,可以试试升级到最新的小版本看看

目前版本
mysql> select version();
±-------------------+
| version() |
±-------------------+
| 5.7.25-TiDB-v5.4.0 |
±-------------------+
1 row in set (0.00 sec)

hi,方便写个最简化复现吗(mini reproduce)?
这个理解起来有点困难 for me :rofl:

CREATE TABLE user (
user_id bigint NOT NULL COMMENT ‘用户id’,
reg_date DATE NOT NULL COMMENT ‘注册日期’,
game_version int(8) NOT NULL DEFAULT ‘0’ COMMENT ‘游戏版本’,
batch_sn varchar(20) not null comment ‘批次号’,
amount decimal(11,2) default 0 comment ‘充值金额’,
…(等字段)
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT ‘创建时间’,
modify_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT ‘最后修改时间’,
KEY idx_key_1 (user_id, reg_date),
KEY idx_key_2 (reg_date, game_version),
… (等多个普通复合索引)
) ENGINE=INNODB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT=‘用户信息’
PARTITION BY RANGE ( TO_DAYS(reg_date) ) (
PARTITION p2021 VALUES LESS THAN (738521),
PARTITION p2022 VALUES LESS THAN (738886),
PARTITION p2023 VALUES LESS THAN (739251),
PARTITION p2024 VALUES LESS THAN (739617),
PARTITION p2025 VALUES LESS THAN (739982),
PARTITION p2026 VALUES LESS THAN (740347),
PARTITION p2027 VALUES LESS THAN (740712),
PARTITION p2028 VALUES LESS THAN (741078),
PARTITION p2029 VALUES LESS THAN (741443),
PARTITION pother VALUES LESS THAN (MAXVALUE)
)

表中 user_id+reg_date+game_version+batch_sn 记录是唯一的(表中未设置唯一索引,靠程序进行保证)

在 flink 作业中,业务流程如下:
kakfa source → group by user_id(按用户分组,将保证相同用户在相同 slot) → tidb sink

对于业务中的一条数据会转成如下 2条sql 在事务中进行执行:
insert into user(user_id,reg_date,game_version,batch_sn) select xx,xx,xx,xx from dual where not exists(select 1 from user where user_id=xx and reg_date=xx and game_version=xx and batch_sn=xx);
update user set amount=amount+xx where user_id=xx and reg_date=xx and game_version=xx and batch_sn=xx);
每次是100条记录,对应 200 条 sql 进行一次事务提交。

  1. 我不太懂 flink,不过我就把它理解为 datapipline(kafka) → data compute module → tidb;
  2. 既然 data compute module 可以确定相同 user 在相同 slot 中,那么这个 slot 的向下同步的时候存在并发 sink 吗?比如,每次是100条记录,为了加快同步效率,可能同一个 solt 开 10 个 tidb connection(pool),这就回造成虽然相同 solt,但是如果相同 solt 拆到 connection 的时候也可能会存在兵法冲突;
  3. data compute module → tidb table 的过程中,只有 data compute module 对这个表操作吗?有没有可能是,dead lock 由 2 个 txn 产生,1 产生自 flink sink,另一个产生自其他同步流造成的死锁?
  4. 既然 user_id=xx and reg_date=xx and game_version=xx 也可以保证唯一吗?因为少了 batch_sn 会不会产生更新干扰? 感觉更像是这块有更新干扰🤔,因为 tidb 底下是 si 取数据,也就是说这条数据的 tso 已经写下去了并且 commit 了。之后才有同一行数据 2 txn 的死锁
  5. 死锁的时候,日志中有 死锁 key 的相关信息吗?也可以顺着这个思路,往下追一下。
  6. 常见死锁,如下图

大概可能猜到,因为表的唯一建没有设置强制 unique,是业务上自己保证的唯一,表中定义的是一个普通索引。普通索引是有间隙锁的,有可能是间隙锁在作祟导致。