最近负责开发一个游戏服务器,当做到副本模块时数据库的存储部分用了事务,结果就出现了一个Mysql deadlock的问题。
首先给出表结构:
CREATE TABLE IF NOT EXISTS `t_instance` (
`charGuid` bigint(20) unsigned NOT NULL DEFAULT '0',
`majorInsId` smallint(5) unsigned NOT NULL DEFAULT '0' COMMENT '大副本ID',
`minorInsId` smallint(5) unsigned NOT NULL DEFAULT '0' COMMENT '中副本ID',
`instanceId` smallint(5) unsigned NOT NULL DEFAULT '0' COMMENT '小副本ID',
`hardType` tinyint(3) unsigned NOT NULL DEFAULT '0' COMMENT '难度类型',
`status` int(10) unsigned NOT NULL DEFAULT '0',
`star` tinyint(3) unsigned NOT NULL DEFAULT '0' COMMENT '最大星级',
`joinCount` smallint(6) NOT NULL DEFAULT '0' COMMENT '今日闯关次数',
KEY `charGuid_Normal` (`charGuid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='副本';
可以看到这个表中只有一个普通键charGuid,按照常理来讲,可能应该是一个联合主键,包括( charGuid,majorInsId,minorInsId,instanceId,hardType),当然这和我的数据更新机制有关,不需要这么复杂的键。在陈述问题之前先要说明的是,对于任意玩家,instance记录的条数没有数量限制。
接下来给出我的数据更新机制:
首先启动事务,运行delete from t_instance where charGuid=xxx,然后再运行数条insert语句把内存中的数据插入进来,然后提交事务,如果有意外的情况发生,则回滚事务,每个玩家大概每隔5分钟更新一次数据。另外重要的一点是,配置了多个数据库线程,每个线程都负责了特定一批玩家的数据更新。比如说线程1,负责奇数号的玩家数据更新,线程2负责了偶数号的玩家的数据更新。再解释下innodb引擎关于事务使用了行锁,也就是说根据索引进行锁定,比如可以只锁定charGuid为1的数据,这是一种粒度很细的锁。
代码完成之后,我启动了大概一万个机器人不停发包打副本,然后在数据库一层经常报事务的deadlock错误,然后我查了一下mysql的状态,这里给出一下状态日志:
LATEST DETECTED DEADLOCK
------------------------
2014-09-01 11:23:26 a38
*** (1) TRANSACTION:
TRANSACTION 84585427, ACTIVE 0 sec inserting
mysql tables in use 1, locked 1
LOCK WAIT 3 lock struct(s), heap size 1184, 2 row lock(s), undo log entries 1
MySQL thread id 17, OS thread handle 0x948, query id 646279 localhost 127.0.0.1 yuwenyong update
insert into t_instance set charGuid=5379,majorInsId=1,minorInsId=1,instanceId=0,hardType=1,status=0,star=0,joinCount=0
*** (1) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 193 page no 59 n bits 632 index `charGuid_Normal` of table `shqzdb`.`t_instance` trx id 84585427 lock_mode X locks gap before rec insert intention waiting
Record lock, heap no 557 PHYSICAL RECORD: n_fields 2; compact format; info bits 0
0: len 8; hex 0000000000001504; asc ;;
1: len 6; hex 0000002feb62; asc / b;;
*** (2) TRANSACTION:
TRANSACTION 84585428, ACTIVE 0 sec inserting, thread declared inside InnoDB 5000
mysql tables in use 1, locked 1
3 lock struct(s), heap size 1184, 2 row lock(s), undo log entries 1
MySQL thread id 15, OS thread handle 0xa38, query id 646280 localhost 127.0.0.1 yuwenyong update
insert into t_instance set charGuid=5377,majorInsId=1,minorInsId=1,instanceId=0,hardType=1,status=0,star=0,joinCount=0
*** (2) HOLDS THE LOCK(S):
RECORD LOCKS space id 193 page no 59 n bits 632 index `charGuid_Normal` of table `shqzdb`.`t_instance` trx id 84585428 lock_mode X locks gap before rec
Record lock, heap no 557 PHYSICAL RECORD: n_fields 2; compact format; info bits 0
0: len 8; hex 0000000000001504; asc ;;
1: len 6; hex 0000002feb62; asc / b;;
*** (2) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 193 page no 59 n bits 632 index `charGuid_Normal` of table `shqzdb`.`t_instance` trx id 84585428 lock_mode X locks gap before rec insert intention waiting
Record lock, heap no 557 PHYSICAL RECORD: n_fields 2; compact format; info bits 0
0: len 8; hex 0000000000001504; asc ;;
1: len 6; hex 0000002feb62; asc / b;;
*** WE ROLL BACK TRANSACTION (2)
日志的意思是说,在插入5377和5379的用户的副本数据时出现了锁冲突,我当时配置了4个数据库线程,根据我的分配算法,5377和5379是在不同的数据库线程中更新数据的,当然也使用了不同的数据库连接。但是死锁如何产生,让我怎么都想不通。后来各种网上查资料,并加了一些mysql dba群询问个中秘密,竟也无人能道出其中的原因。 后来只能自己慢慢研究,研读日志,终于明白了其中的问题所在。
问题的关键在于没能详细理解innodb锁的机制,仔细观察上面的日志,有一句话很重要: RECORD LOCKS space id 193 page no 59 n bits 632 index `charGuid_Normal` of table `shqzdb`.`t_instance` trx id 84585428 lock_mode X locks gap before rec insert intention waiting。这里出现了几个关键词:page, lock_mode X,和intention waiting。原来innodb为了提高性能,不仅仅是简单的使用了行锁,当进行数据更新时,在page一层使用了一种叫意向锁的东西。当你要获取排他锁的时候(lock_mode x),必须先获取意向锁。mysql如此设计的初衷是为了性能,不必每次都到row级别去检测冲突。这里charGuid 5377和5379分布在同一个page,后运行的事务试图获取意向锁的时候产生了冲突,导致mysql deadlock的发生。
首先给出表结构:
CREATE TABLE IF NOT EXISTS `t_instance` (
`charGuid` bigint(20) unsigned NOT NULL DEFAULT '0',
`majorInsId` smallint(5) unsigned NOT NULL DEFAULT '0' COMMENT '大副本ID',
`minorInsId` smallint(5) unsigned NOT NULL DEFAULT '0' COMMENT '中副本ID',
`instanceId` smallint(5) unsigned NOT NULL DEFAULT '0' COMMENT '小副本ID',
`hardType` tinyint(3) unsigned NOT NULL DEFAULT '0' COMMENT '难度类型',
`status` int(10) unsigned NOT NULL DEFAULT '0',
`star` tinyint(3) unsigned NOT NULL DEFAULT '0' COMMENT '最大星级',
`joinCount` smallint(6) NOT NULL DEFAULT '0' COMMENT '今日闯关次数',
KEY `charGuid_Normal` (`charGuid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='副本';
可以看到这个表中只有一个普通键charGuid,按照常理来讲,可能应该是一个联合主键,包括( charGuid,majorInsId,minorInsId,instanceId,hardType),当然这和我的数据更新机制有关,不需要这么复杂的键。在陈述问题之前先要说明的是,对于任意玩家,instance记录的条数没有数量限制。
接下来给出我的数据更新机制:
首先启动事务,运行delete from t_instance where charGuid=xxx,然后再运行数条insert语句把内存中的数据插入进来,然后提交事务,如果有意外的情况发生,则回滚事务,每个玩家大概每隔5分钟更新一次数据。另外重要的一点是,配置了多个数据库线程,每个线程都负责了特定一批玩家的数据更新。比如说线程1,负责奇数号的玩家数据更新,线程2负责了偶数号的玩家的数据更新。再解释下innodb引擎关于事务使用了行锁,也就是说根据索引进行锁定,比如可以只锁定charGuid为1的数据,这是一种粒度很细的锁。
代码完成之后,我启动了大概一万个机器人不停发包打副本,然后在数据库一层经常报事务的deadlock错误,然后我查了一下mysql的状态,这里给出一下状态日志:
LATEST DETECTED DEADLOCK
------------------------
2014-09-01 11:23:26 a38
*** (1) TRANSACTION:
TRANSACTION 84585427, ACTIVE 0 sec inserting
mysql tables in use 1, locked 1
LOCK WAIT 3 lock struct(s), heap size 1184, 2 row lock(s), undo log entries 1
MySQL thread id 17, OS thread handle 0x948, query id 646279 localhost 127.0.0.1 yuwenyong update
insert into t_instance set charGuid=5379,majorInsId=1,minorInsId=1,instanceId=0,hardType=1,status=0,star=0,joinCount=0
*** (1) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 193 page no 59 n bits 632 index `charGuid_Normal` of table `shqzdb`.`t_instance` trx id 84585427 lock_mode X locks gap before rec insert intention waiting
Record lock, heap no 557 PHYSICAL RECORD: n_fields 2; compact format; info bits 0
0: len 8; hex 0000000000001504; asc ;;
1: len 6; hex 0000002feb62; asc / b;;
*** (2) TRANSACTION:
TRANSACTION 84585428, ACTIVE 0 sec inserting, thread declared inside InnoDB 5000
mysql tables in use 1, locked 1
3 lock struct(s), heap size 1184, 2 row lock(s), undo log entries 1
MySQL thread id 15, OS thread handle 0xa38, query id 646280 localhost 127.0.0.1 yuwenyong update
insert into t_instance set charGuid=5377,majorInsId=1,minorInsId=1,instanceId=0,hardType=1,status=0,star=0,joinCount=0
*** (2) HOLDS THE LOCK(S):
RECORD LOCKS space id 193 page no 59 n bits 632 index `charGuid_Normal` of table `shqzdb`.`t_instance` trx id 84585428 lock_mode X locks gap before rec
Record lock, heap no 557 PHYSICAL RECORD: n_fields 2; compact format; info bits 0
0: len 8; hex 0000000000001504; asc ;;
1: len 6; hex 0000002feb62; asc / b;;
*** (2) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 193 page no 59 n bits 632 index `charGuid_Normal` of table `shqzdb`.`t_instance` trx id 84585428 lock_mode X locks gap before rec insert intention waiting
Record lock, heap no 557 PHYSICAL RECORD: n_fields 2; compact format; info bits 0
0: len 8; hex 0000000000001504; asc ;;
1: len 6; hex 0000002feb62; asc / b;;
*** WE ROLL BACK TRANSACTION (2)
日志的意思是说,在插入5377和5379的用户的副本数据时出现了锁冲突,我当时配置了4个数据库线程,根据我的分配算法,5377和5379是在不同的数据库线程中更新数据的,当然也使用了不同的数据库连接。但是死锁如何产生,让我怎么都想不通。后来各种网上查资料,并加了一些mysql dba群询问个中秘密,竟也无人能道出其中的原因。 后来只能自己慢慢研究,研读日志,终于明白了其中的问题所在。
问题的关键在于没能详细理解innodb锁的机制,仔细观察上面的日志,有一句话很重要: RECORD LOCKS space id 193 page no 59 n bits 632 index `charGuid_Normal` of table `shqzdb`.`t_instance` trx id 84585428 lock_mode X locks gap before rec insert intention waiting。这里出现了几个关键词:page, lock_mode X,和intention waiting。原来innodb为了提高性能,不仅仅是简单的使用了行锁,当进行数据更新时,在page一层使用了一种叫意向锁的东西。当你要获取排他锁的时候(lock_mode x),必须先获取意向锁。mysql如此设计的初衷是为了性能,不必每次都到row级别去检测冲突。这里charGuid 5377和5379分布在同一个page,后运行的事务试图获取意向锁的时候产生了冲突,导致mysql deadlock的发生。