[seata]并发情况下,偶发性的出现不同xid事务的lockKeys记录错乱了,导致全局锁死锁

2024-05-10 389 views
0
Ⅰ. Issue Description

我们在生产上出现了多次无法获取到全局锁异常

image

进一步分析,发现问题出现在释放锁的时候,lock_table表中存在死锁 而死锁的原因居然是,本不该出现在A事务中的lockKeys被保存在了lock_table中,持有了不该他所持有的记录锁 而另一个事务B与事务A触发的时间几乎同时,间隔10ms左右,就出现了B事务等待A事务释放锁,A事务等待B释放另一把锁,造成死锁

我下面提供数据库死锁的日志 deadlock-20230601104213.txt 在文件中,A事务为xid = '10.20.9.15:8091:9315668586294483',我们简称4483 B事务为xid = '10.20.9.15:8091:9315668586294476',我们简称4476

在A事务4483中,可以看到他所持有的锁记录,我们数据查看,是与machine表中的车辆id为10088991相关联,但突然出现了车辆id为10086919的记录,这让人很费解 image

在B事务4476中,可以看到他所持有的锁记录都是与machine表中的车辆id为10086919相关联,没有出现问题

也就是说,本来应该在B事务中的车辆id为10086919的记录,突然被记录到了在A事务4483中

然后就导致,A事务4483等待B事务4476释放machineborrow表中车辆id为10086919的记录(这条记录本就不该出现在A事务中) image

而B事务4476又等待A事务4483释放machine表中的车辆id为10086919的记录(这条记录也不应该出现在A事务中) image

我可以很明确事务A和事务B都不是一个接口,处理的业务也不同,只是发生在极短时间内,在seata中居然出现了lockKyes记录串了,导致了业务数据错乱

因为当上述死锁发生后,后续的针对A事务4483的后续操作,machine表中的车辆id为10088991相关的所有处理都无法进行,造成了业务处理脏读,1分钟后,该全局锁被自动释放后,数据全部回滚

Ⅱ. Describe what happened

If there is an exception, please attach the exception trace:

Just paste your stack trace here!
Ⅲ. Describe what you expected to happen Ⅳ. How to reproduce it (as minimally and precisely as possible)
  1. xxx
  2. xxx
  3. xxx

Minimal yet complete reproducer code (or URL to code):

Ⅴ. Anything else we need to know? Ⅵ. Environment:
  • JDK version(e.g. java -version):
  • Seata client/server version:
  • Database version:
  • OS(e.g. uname -a):
  • Others:

回答

7

全局锁来源于client侧,rm注册时提交,错乱只可能是因为前镜像查询出来的主键存在问题,先检查自己业务上的sql是否为at所支持并发到issue中,其次如何证明你业务代码不存在问题?这个时候的trace业务上调用链是否正常能否出示?能否提供复现的demo?

0

还有数据锁定后脏读不可能发生,既然事死锁肯定存在两个以上的锁,脏读纯粹为业务没加globallock注解导致,你截图的死锁并没有seata日志,如果锁被错误xid持有,会有日志输出请提供,如果你说的是unlock死锁,请升级1.5以上并按照最新的lock表sql修改表结构及索引

1

9315668586294483.log

image

这是事务A 4483的seata日志,只看到锁machine:10088991,没有看到锁machine表中的车辆id为10086919的记录,但是数据库却有这条

2

9315668586294476.log image

这个是事务B 4476的seata日志,可以看到都已经commit成功了,但是数据库的日志显示其实死锁了

9

还有数据锁定后脏读不可能发生,既然事死锁肯定存在两个以上的锁,脏读纯粹为业务没加globallock注解导致,你截图的死锁并没有seata日志,如果锁被错误xid持有,会有日志输出请提供,如果你说的是unlock死锁,请升级1.5以上并按照最新的lock表sql修改表结构及索引

我所谓的脏读是业务意义上的,在A事务处理后,由于这个全局锁死锁还没有释放前,有其他服务会查询到A事务处理后的数据,进行下一步处理,结果处理完以后,A事务却在全局锁释放后回滚了,导致业务流程出现了混乱

6

数据的日志和seata日志不是一回事,你这个是deletesql的数据库层面的锁,全局锁释放后不会出现数据混乱,事务释放全局锁时,要么成功要么失败,如果决议提交解锁失败,那么事务会转为rollback,rollback的事务本身数据会回滚不存在一致性问题,如果是决议提交后,最后异步提交解锁失败,由于分支已经全部处理完了,到global unlock时即便失败转为回滚数据也因为没有分支不会出现问题,你说的本地锁死锁在1.5上已经修复,建议升级

2

还有数据锁定后脏读不可能发生,既然事死锁肯定存在两个以上的锁,脏读纯粹为业务没加globallock注解导致,你截图的死锁并没有seata日志,如果锁被错误xid持有,会有日志输出请提供,如果你说的是unlock死锁,请升级1.5以上并按照最新的lock表sql修改表结构及索引

我所谓的脏读是业务意义上的,在A事务处理后,由于这个全局锁死锁还没有释放前,有其他服务会查询到A事务处理后的数据,进行下一步处理,结果处理完以后,A事务却在全局锁释放后回滚了,导致业务流程出现了混乱

其他服务查到处理后的数据,at本身是读未提交,自行加globallock解决

7

虽然我能理解数据库的日志和seata日志不是一回事,但从中可以看到,seata的日志,与数据库lock_table出现了不一致的情况

我猜测可能在并发写lock_table的时候出现了问题,在rm里没有问题

9

seata里并没有的rowkey为啥会往lock_table插入呢?

5

目前我们使用的版本是1.5.2

4

数据的日志和seata日志不是一回事,你这个是deletesql的数据库层面的锁,全局锁释放后不会出现数据混乱,事务释放全局锁时,要么成功要么失败,如果决议提交解锁失败,那么事务会转为rollback,rollback的事务本身数据会回滚不存在一致性问题,如果是决议提交后,最后异步提交解锁失败,由于分支已经全部处理完了,到global unlock时即便失败转为回滚数据也因为没有分支不会出现问题,你说的本地锁死锁在1.5上已经修复,建议升级

问题就出现在全局锁释放不了,因为记的时候串了,全局锁删的时候就出现了数据库deletesql的锁异常

2

没有这个问题,到按xid删除全局锁的时候已经是事务解锁,你自行升级再看吧,当时这个死锁我们怀疑的是mysql的bug,导致在rr下锁间隙之类的,锁了不该锁的行导致的死锁

7

目前我们使用的版本是1.5.2

你的索引肯定不对

5

虽然我能理解数据库的日志和seata日志不是一回事,但从中可以看到,seata的日志,与数据库lock_table出现了不一致的情况

我猜测可能在并发写lock_table的时候出现了问题,在rm里没有问题

分支注册会答应日志,日志里有pk,server不会对pk做修改,自行看日志检索到底有没有锁错,你这个很明显是本地锁死锁,自行升级版本后按最新的locktable表sql去增加索引

2

没有这个问题,到按xid删除全局锁的时候已经是事务解锁,你自行升级再看吧,当时这个死锁我们怀疑的是mysql的bug,导致在rr下锁间隙之类的,锁了不该锁的行导致的死锁

说错了,是rr可重复读下

8

https://github.com/seata/seata/blob/2.x/script/server/db/mysql.sql

CREATE TABLE IF NOT EXISTS `lock_table`
(
    `row_key`        VARCHAR(128) NOT NULL,
    `xid`            VARCHAR(128),
    `transaction_id` BIGINT,
    `branch_id`      BIGINT       NOT NULL,
    `resource_id`    VARCHAR(256),
    `table_name`     VARCHAR(32),
    `pk`             VARCHAR(36),
    `status`         TINYINT      NOT NULL DEFAULT '0' COMMENT '0:locked ,1:rollbacking',
    `gmt_create`     DATETIME,
    `gmt_modified`   DATETIME,
    PRIMARY KEY (`row_key`),
    KEY `idx_status` (`status`),
    KEY `idx_branch_id` (`branch_id`),
    KEY `idx_xid` (`xid`)
) ENGINE = InnoDB
  DEFAULT CHARSET = utf8mb4;
3

好的,感谢,我们试一下,目前我们出现了,这个表的锁记录没有释放,6月3日的记录,有锁残留

image

我们怎么可以处理掉这些记录,直接手动删除么?

4

https://github.com/seata/seata/blob/2.x/script/server/db/mysql.sql

CREATE TABLE IF NOT EXISTS `lock_table`
(
    `row_key`        VARCHAR(128) NOT NULL,
    `xid`            VARCHAR(128),
    `transaction_id` BIGINT,
    `branch_id`      BIGINT       NOT NULL,
    `resource_id`    VARCHAR(256),
    `table_name`     VARCHAR(32),
    `pk`             VARCHAR(36),
    `status`         TINYINT      NOT NULL DEFAULT '0' COMMENT '0:locked ,1:rollbacking',
    `gmt_create`     DATETIME,
    `gmt_modified`   DATETIME,
    PRIMARY KEY (`row_key`),
    KEY `idx_status` (`status`),
    KEY `idx_branch_id` (`branch_id`),
    KEY `idx_xid` (`xid`)
) ENGINE = InnoDB
  DEFAULT CHARSET = utf8mb4;

我现在的客户端和server端都是1.5.2版本,直接该这个表结构就可以么?还是说我需要同时将客户端和服务端都升级到1.6.1

6

https://github.com/seata/seata/blob/2.x/script/server/db/mysql.sql

CREATE TABLE IF NOT EXISTS `lock_table`
(
    `row_key`        VARCHAR(128) NOT NULL,
    `xid`            VARCHAR(128),
    `transaction_id` BIGINT,
    `branch_id`      BIGINT       NOT NULL,
    `resource_id`    VARCHAR(256),
    `table_name`     VARCHAR(32),
    `pk`             VARCHAR(36),
    `status`         TINYINT      NOT NULL DEFAULT '0' COMMENT '0:locked ,1:rollbacking',
    `gmt_create`     DATETIME,
    `gmt_modified`   DATETIME,
    PRIMARY KEY (`row_key`),
    KEY `idx_status` (`status`),
    KEY `idx_branch_id` (`branch_id`),
    KEY `idx_xid` (`xid`)
) ENGINE = InnoDB
  DEFAULT CHARSET = utf8mb4;

我现在的客户端和server端都是1.5.2版本,直接该这个表结构就可以么?还是说我需要同时将客户端和服务端都升级到1.6.1

用这个表结构即可

1

好的,感谢,我们试一下,目前我们出现了,这个表的锁记录没有释放,6月3日的记录,有锁残留 image 我们怎么可以处理掉这些记录,直接手动删除么?

搜索下xid还在不在,1.4.2的时候会可能出现残留锁,主要是因为事务决议的时候还有分支注册上来,这块通过延迟删除事务来处理了,默认遇到这种情况会在2分10秒后清除,如果你搜索到xid的话,状态是回滚的几种状态(详见官网状态码对应含义)那可能是因为脏写一直回滚失败

3
4750 我又回顾了下这个pr,应该是1.6的server端解决这个问题的,并不是1.5.2

感谢回复,那我们计划直接升到1.6.1最新版,后续持续跟进

8
4750 我又回顾了下这个pr,应该是1.6的server端解决这个问题的,并不是1.5.2

@a364176773 我们这边相同的情况,客户端1.5.2,server端升级到了1.6.1,问题依旧

6
4750 我又回顾了下这个pr,应该是1.6的server端解决这个问题的,并不是1.5.2

@a364176773 我们这边相同的情况,客户端1.5.2,server端升级到了1.6.1,问题依旧

索引问题

9

@a364176773 确定了不是

# 生产表结构
# Schema: production_seata_server Table: lock_table  
-- auto-generated definition
create table lock_table
(
    row_key        varchar(512)      not null
        primary key,
    xid            varchar(128)      null,
    transaction_id bigint            null,
    branch_id      bigint            not null,
    resource_id    varchar(256)      null,
    table_name     varchar(32)       null,
    pk             varchar(36)       null,
    status         tinyint default 0 not null comment '0:locked ,1:rollbacking',
    gmt_create     datetime          null,
    gmt_modified   datetime          null
);

create index idx_branch_id
    on lock_table (branch_id);

create index idx_status
    on lock_table (status);

create index idx_xid
    on lock_table (xid);
8

@a364176773 确定了不是

# 生产表结构
# Schema: production_seata_server Table: lock_table  
-- auto-generated definition
create table lock_table
(
    row_key        varchar(512)      not null
        primary key,
    xid            varchar(128)      null,
    transaction_id bigint            null,
    branch_id      bigint            not null,
    resource_id    varchar(256)      null,
    table_name     varchar(32)       null,
    pk             varchar(36)       null,
    status         tinyint default 0 not null comment '0:locked ,1:rollbacking',
    gmt_create     datetime          null,
    gmt_modified   datetime          null
);

create index idx_branch_id
    on lock_table (branch_id);

create index idx_status
    on lock_table (status);

create index idx_xid
    on lock_table (xid);

https://github.com/seata/seata/issues/4744 已经有不少用户都反馈没问题了,如果还有问题说明你跟他们不是一个问题不要混为一谈,拿出日志之类的实际信息,而不是空口无凭