业务背景
最近在折腾老系统,折腾了好久,发现一个数据库问题,用户点赞数量,如果用户取消点赞情况下,正常情况10次取消数据库都返回成功,但其中有2次没有取消。
数据库场景
在MySQL中看下面一个场景。 业务中存在一张用户点赞表,存有用户的点赞数量。业务表做了如下设计。业务中使用RC隔离级别。
CREATE TABLE `user_count` (
`id` int NOT NULL AUTO_INCREMENT,
`user_id` int NOT NULL COMMENT '用户id',
`count` int(10) NOT NULL DEFAULT '0' COMMENT '数量',
PRIMARY KEY (`id`),
KEY `idx_userid_count` (`user_id`,`count`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
INSERT INTO user_count VALUES(1, 1, 10);
INSERT INTO user_count VALUES(2, 2, 20);
使用rc隔离级别,进行如下测试:
session1 | session2 | session3 |
---|---|---|
set session transaction isolation level READ COMMITTED; | set session transaction isolation level READ COMMITTED; | set session transaction isolation level READ COMMITTED; |
BEGIN; UPDATE user_count SET count = count -1 WHERE user_id = 1 and count > 0; | ||
BEGIN; UPDATE user_count SET count = count -1 WHERE user_id = 1 and count > 0; | ||
BEGIN; UPDATE user_count SET count = count -1 WHERE user_id = 1 and count > 0; | ||
COMMIT; | ||
COMMIT; | ||
COMMIT; |
3个线程,并发取消点赞3次。第三次未成功。
而Session3 update返回的是:
Query OK, 0 rows affected
Rows matched: 0 Changed: 0 Warnings: 0
原因分析
UPDATE语句的执行计划,Update语句选择的是二级索引idx_userid_count。
session1对user_id=1做Update操作,将rec1标记删除,然后新插入rec3,语句执行后如下:
page上的记录 |
---|
1, 9 rec3 (session1 insert) |
1, 10 rec1 deleted |
2, 10 rec2 |
session2对user_id=1做Update操作,定位到rec3,由于session1持有该行上的锁还未释放所以会等待。 session3对user_id=1做Update操作,也定位到rec3,这个时候也会排队等锁。
page上的记录 |
---|
1, 9 rec3 (session1 insert) Wait: session2, session3 |
1, 10 rec1 deleted |
2, 10 rec2 |
当session1提交后,session2被唤醒restore cursor继续定位到rec3上。然后将rec3标记删除,插入rec4。
page上的记录 |
---|
1, 8 rec4 (session2 insert) |
1, 9 rec3 (session1 insert,session2 delete) Wait: session3 |
1, 10 rec1 deleted |
2, 10 rec2 |
这个时候session3继续在等锁,当session2提交后,session3被唤醒,restore cursor继续定位到rec3上。这个时候rec3已经被标记为删除,session3逐行读取next record,找到rec2后发现已经超过查找的上边界(500, max),然后停止查找。session3未找到匹配的数据,然后返回成功,未更新任何记录。 其实上述问题是由RC隔离级别下的幻读导致.
修复
1.修改索引,只保留用户id一个索引。
2.代码层次做拦截 。
3 改隔离级别为RR,这样第三次会报错,还是需要代码做处理。