初始化数据
CREATE TABLE `t_test` (
`FID` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键',
`FCREATE_TIME` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`FMODIFY_TIME` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',
`FUSER_ID` varchar(20) NOT NULL COMMENT '用户id',
`FLINKMAN_ID` varchar(20) NOT NULL COMMENT '联系人id',
`FUNREAD_COUNT` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '消息未读数',
`FLAST_MSG_ID` bigint(20) unsigned DEFAULT NULL COMMENT '最后一条消息id',
PRIMARY KEY (`FID`) USING BTREE,
KEY `idx_last_msg_id` (`FLAST_MSG_ID`) USING BTREE
) ENGINE=InnoDB ROW_FORMAT=DYNAMIC COMMENT='测试(会话)';
INSERT INTO t_test(FID, FUSER_ID, FLINKMAN_ID, FUNREAD_COUNT, FLAST_MSG_ID) VALUES(1, 'A', 'B', 0, 1000)
INSERT INTO t_test(FID, FUSER_ID, FLINKMAN_ID, FUNREAD_COUNT, FLAST_MSG_ID) VALUES(2, 'B', 'A', 0, 1000)
问题重现(简化版)
事务1与事务2并发执行
事务1
-- 时间1:用户A给其联系人B发消息,调用业务接口,开启事务
START transaction ;
-- 时间3:更新会话A-B的最后一条消息
UPDATE t_test SET FLAST_MSG_ID = 1001
WHERE FID = 1;
-- 时间5:更新会话B-A的未读数和最后一条消息
UPDATE t_test SET FUNREAD_COUNT = FUNREAD_COUNT + 1, FLAST_MSG_ID = 1001
WHERE FID = 2;
COMMIT;
事务2
-- 时间2:用户B给其联系人A发消息,调用业务接口,开启事务
START transaction;
-- 时间4:更新会话B-A的最后一条消息
UPDATE t_test SET FLAST_MSG_ID = 1002
WHERE FID = 2;
-- 时间6:更新会话A-B的未读数和最后一条消息
-- 此时报错:Deadlock found when trying to get lock; try restarting transaction!!!
UPDATE t_test SET FUNREAD_COUNT = FUNREAD_COUNT + 1, FLAST_MSG_ID = 1002
WHERE FID = 1;
COMMIT;
解决
使用 分布式锁+开启本地事务+执行业务逻辑(执行多条sql)+提交本地事务+释放分布式锁,且这个分布式锁的key要设计合理,才能避免死锁。
本示例中每个用户都有类型:客户/经纪人,那么就可以将分布式锁的key设计为场景前缀+客户id+经纪人id。譬如:A用户是客户,B用户是经纪人,那么线程1和线程2都是计算出key=REDISSON:SEND_MSG:A:B
,都要先抢夺同一个分布式锁,就不会出现MySQL
的死锁了!