请教一个 Mysql 并发查询的问题(与可重复读相关?)

108 天前
 rqxiao
(此问题出现场景为 mysql5.7.31 隔离级别为可重复读)
在做审核业务流程中自定义了两张表,a 和 a_detail 。主要需求是子表所有记录都审核通过了,那么就去修改主表记录为审核通过。 但有可能会遇到最后两条记录同时审核成功,他们去查询当前子表审核记录的时候有可能对方还没提交,所以最终会修改主表失败。为了解决这个问题,就直接想到了添加索引后,利用 mysql 行锁加在主表的记录上,获取锁之后再去 count 。但是在操作过程中 在获取行锁之前添加了一句查询子表的 sql 导致结果不正确。请教下这是为什么?或者说请教下此业务有其他合适的解决方案吗?


CREATE TABLE `a` (
`uid` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '审核记录主表 guid',
`approval_status` int(11) NULL DEFAULT 1 COMMENT '审核状态(1:审核中,2:通过)',
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键 id',
PRIMARY KEY (`id`) USING BTREE,
INDEX `idx_guid`(`uid`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 22 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '审核记录主表' ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of a
-- ----------------------------
INSERT INTO `a` VALUES ('a1', 1, 1);
INSERT INTO `a` VALUES ('a2', 1, 2);



CREATE TABLE `a_detail` (
`approval_status` int(11) NULL DEFAULT 1 COMMENT '审核状态(1:审核中;2:通过)',
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键 id',
`auid` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE,
INDEX `idx_aid`(`auid`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 74 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '审核记录明细表' ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of a_detail
-- ----------------------------
INSERT INTO `a_detail` VALUES (1, 1, 'a1');
INSERT INTO `a_detail` VALUES (1, 2, 'a1');
INSERT INTO `a_detail` VALUES (1, 3, 'a2');
INSERT INTO `a_detail` VALUES (1, 4, 'a2');



会话一和 会话二 同时执行
SELECT @@GLOBAL.TX_ISOLATION;

begin;

-- 查询字表明细记录(这部如果去除整个流程正常)-
SELECT * FROM a_detail WHERE id = 1

-- 根据索引利用行锁 锁定主表的记录--
SELECT * FROM `a` where uid='a1' for update;

-- 跟新字表状态为 2 审核通过--
UPDATE a_detail set approval_status = 2 WHERE id = 1 and approval_status = 1;

-- 查看子表是不是都已经审核通过了--
select count(0) from a_detail WHERE auid = 'a1' and approval_status!=2;
-- 接下来如果 count(0) 数量是 0 代表所有子记录都通过,则去修改主表状态 update a set approval_status=2 where uid='a1' --

commit;


SELECT @@GLOBAL.TX_ISOLATION;

begin;

-- 查询字表明细记录(这部如果去除整个流程正常)-
SELECT * FROM a_detail WHERE id = 2

-- 根据索引利用行锁 锁定主表的记录--
SELECT * FROM `a` where uid='a1' for update;

-- 跟新字表状态为 2 审核通过--
UPDATE a_detail set approval_status = 2 WHERE id = 2 and approval_status = 1;

-- 查看子表是不是都已经审核通过了--
select count(0) from a_detail WHERE auid = 'a1' and approval_status!=2;
-- 接下来如果 count(0) 数量是 0 代表所有子记录都通过,则去修改主表状态 update a set approval_status=2 where uid='a1' --

commit;
1954 次点击
所在节点    MySQL
39 条回复
RainCats
108 天前
浅薄看了点书,for update 用的是当前读,会刷新当前事务中读取到的数据版本。
要不试试搞个分布式锁去确保同一时间只有一个可以操作到。
pkoukk
108 天前
从你的业务描述上看,我没看到锁主表的必要性...
既然两个都是成功,目标都是改为审核通过,那为什么要锁呢
skaly
108 天前
尽量不要用存储过程/触发器什么的,出现问题调试起来非常麻烦


鉴于你这个问题,可以考虑在主表里面 加两字段 count ,u_count 。
每次更新 u_count 的时候,使用 u_count=u_count+1
然后再判断 u_count 是否=count ,来决定是否 审核通过
1018ji
108 天前
-- 查询字表明细记录(这部如果去除整个流程正常)-
SELECT * FROM a_detail WHERE id = 1

这个东西会影响后面的查询吧,我估计表现就是这样的

-- 查看子表是不是都已经审核通过了--
select count(0) from a_detail WHERE auid = 'a1' and approval_status!=2;


---------------------------------------------------------------------------------------


还是考虑 3 的做法,但是有个问题是如果代码不够牛逼,很容易错乱也就是说导致 u_count 少了。
你这个事务太大了,这个事务就是个灾难
1018ji
108 天前
https://www.jianshu.com/p/eb3f56565b42 补充个文章,自己测试对不对吧
MoYi123
108 天前
难道不是每次子审核通过就 直接跑这 2 句就行? 都不用在一个事务里.

UPDATE a_detail set approval_status = 2 WHERE id = 2 and approval_status = 1;
UPDATE a set approval_status=(select count(0)==0 from a_detail WHERE auid = 'a1' and approval_status!=2) where uid='a1'
rqxiao
108 天前
@pkoukk 有可能会有驳回 审核不通过的记录,也有可能全是通过。全是通过要计算最终是否审核通过
pkoukk
108 天前
@rqxiao 驳回和不通过那不是子表的事情,总表不是通过子表记录计算出来的么?
两个不同的线程对子表记录的计算结果应该一致啊,还能不一致?
rqxiao
108 天前
@pkoukk 最后两个审核通过的他们在事务里都没有提交的时候,不是 count 自己事务内 当前内已提交事务的数量吗,有可能都 2 个线程都是 count 出 9 个 如果总共审核通过的是 10 个的话
Dream95
108 天前
事件驱动,子表审核通过触发审核事件,处理审核事件时再去检查全部任务是否通过审核
bsg1992
108 天前
这个不算问题吧, 审批触发事件后,查询一下审批记录,满足数量更新主表的状态值就好了啊
LiaoMatt
108 天前
我觉得通过 MQ 来实现会更好, 或者你把事务隔离级别设置为读已提交是不是就可以了
rqxiao
108 天前
@bsg1992 意思是 修改字表和修改主表在两个事物里吗
wengyanbin
108 天前
@rqxiao 难道是在事务开始时候的 select * from a_detail where id =这一句影响了后面的 count ?就算去掉开始的语句我也觉得会有幻读的问题的,两个事务里面的 count 应该都是快照读才对。
rqxiao
107 天前
@wengyanbin 并发时,每个会话开启事务,获取到行锁之后,进行一次快照读。只要能获取到行锁,说明上个事务已经提交了。所以并发情况下,达到了串行执行业务。获取到行锁一定能查询当前准确的数据。为什么说会有幻读?
目前来看 select * from a_detail where id =这一句是影响了后面的 count 。就算我替换成 select * from student a 。与本业务完全不相关的表查询,也会有问题。不是很理解为什么
碍于本人现在的认知水平,理解有可能也不是对的。
rqxiao
107 天前
@rqxiao mysql 为 rr 级别
long952
107 天前
把第一个查询语句放在 for update 后面试试,rr 级别第一次查询生成一个 ReadView ,解决幻读问题,以后每次读取还是这个 readview ,数据还是旧的
vczyh
107 天前
@long952 应该是这样
rqxiao
107 天前
@long952 我以前看网上资料的,rr 级别第一次查询生成一个 ReadView 。都是用 select 一条记录作为演示。
我一直以为是 readview 是跟记录绑定的。不同的记录会绑定不同的 readvie ,现在这个现象感觉和不是记录级别,感觉是整个表级别。
wengyanbin
107 天前
@rqxiao 问题应该还是在 count 的地方,产生了一个快照读。但我个人分析还是觉得无论前面那一句查询加不加都会是快照读,也就是无论怎样都会有幻读的问题才对。不加 select * from a_detail where id =这一句事务反而能够正常跑是我不能理解。现在不知道从哪里分析问题了

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/1046741

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX