MySQL InnoDB锁机制及锁超时案例分析

InnoDB锁机制

1. 在不通过索引条件查询(更新)的时候,InnoDB试用的是表锁。

例如: update xxx where a = 1 ,若a无索引,那么锁定的并非a=1这一行,而是整个表。

2. 由于MySQL的行锁是针对索引加的锁,不是针对记录加的锁,所以虽然我们可能认为我们是锁的不同记录,但是对于MySQL来说,锁的实际上是一组索引。

3. 当表有多个索引的时候,不同的事务可以用不同的索引来锁定不同的行。另外,不论是使用主键索引,唯一索引或者普通索引,InnoDB都会使用行锁来对数据加锁。

4. 即便在条件中使用了索引字段,但具体是否使用索引,使用哪个索引,是由MySQL执行计划来判断的,如果MySQL认为全表扫效率较高,比如对于一些很小的表,那么它就不会使用索引,这种情况下InnoDB就将使用表锁,而不是行锁。所以在分析锁冲突时,我们可以使用explain来查看MySQL的执行计划,以及是否使用了索引,使用了哪些索引。

tips:尝试获取锁默认50秒超时,会提示 Lock wait timeout exceeded; try restarting transaction

案例

最近在负责排查和优化一些慢SQL,有同事报说,有接口压测时老是会出现超时的情况,几乎必现。但以前压测时没有出现过,也没有过改动任何代码。

背景(非实际线上场景):

1. 表结构大致为

CREATE TABLE `user_transation_stores` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `userId` int(11) NOT NULL COMMENT '用户id',
  `storeCode` varchar(32) COLLATE utf8_bin DEFAULT NULL COMMENT '门店code',
  `consumeTime` char(14) COLLATE utf8_bin DEFAULT NULL COMMENT '消费日期',
  `times` int(11) DEFAULT NULL COMMENT '消费次数',
  PRIMARY KEY (`id`),
  KEY `idx_userId` (`userId`) USING BTREE, 
  KEY `idx_storeCode` (`storeCode`) USING BTREE,
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin

主要作用是:记录用户在门店某天消费的次数。

2. 更新语句为: update user_transation_stores set times = times + 1 where userId = xxx and storeCode = xxx and consumeTime=xxx

3. 采用MyCat分库分表,有dbs1-12个库

4. 存在更新其他表的操作,所以由于MyCAT的分布式事务保证,也就存在XA事务

询问了下,压测数据为不同的用户ID散列,但门店Code比较集中。

也就是在压测过程中,可能多个用户ID,同一个门店Code的情况。

首先,本次我们不去讨论这样去记录用户每日消费次数是否合理,而是要摸清为何会出现死锁的情况。

找DBA捞了下锁日志,自己也可以用 SQL:

show engine innodb status;\G

锁日志:

mysql tables in use 3, locked 3
225 lock struct(s), heap size 41168, 3 row lock(s)
MySQL thread id 212861, OS thread handle 140366957156096, query id 78804036 10.210.0.4 uat_member updating
update user_transation_stores set times=7 where userId=68 and consumeTime='20211127' and storeCode='ccab'
*** (2) HOLDS THE LOCK(S):
RECORD LOCKS space id 5210 page no 85408 n bits 344 index PRIMARY of table `dbs9`.`user_transation_stores` trx id 1698745253 lock_mode X locks rec but not gap
Record lock, heap no 253 PHYSICAL RECORD: n_fields 7; compact format; info bits 0
 0: len 8; hex 80000000015d407f; asc      ]@ ;;
 1: len 6; hex 000065386b32; asc   e8k2;;
 2: len 7; hex 35000f800c01ed; asc 5      ;;
 3: len 4; hex 80000044; asc    D;;
 4: len 4; hex 30303837; asc 0087;;
 5: len 14; hex 3230323131313237313433373533; asc 20211127143753;;
 6: len 4; hex 80000006; asc     ;;
 
 *** (2) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 5210 page no 155179 n bits 904 index index_storeCode of table `dbs9`.`user_transation_stores` trx id 1698745253 lock_mode X locks rec but not gap waiting
Record lock, heap no 820 PHYSICAL RECORD: n_fields 2; compact format; info bits 0
 0: len 4; hex 30303837; asc 0087;;
 1: len 8; hex 80000000015d407f; asc      ]@ ;;

其中我们可以注意到一行:

WAITING FOR THIS LOCK TO BE GRANTED:

RECORD LOCKS space id 5210 page no 155179 n bits 904 index index_storeCode of table `dbs9`.`user_transation_stores`

我们看到,这里锁的时候,是以index_storeCode索引执行加锁的。这里我们会有疑问,按正常逻辑来讲,不应该走userId(用户ID)索引么。这里其实是MySQL执行计划认为走storeCode的索引,快于走userId索引,也就是storeCode的筛选度高于userId,在dbs9这个库上。

上面我们提到,我们数据是userId散列,但storeCode相对集中,那么如果走了storeCode索引作为锁,那么就会导致大量的锁竞争。

同时,我们用的是MyCAT,所以还存在一个分布式事务,那就有大量的XA事务卡在这个表的storeCode锁上。

事务堆积得越多,就越容易会导致锁超时,对于此次压测来说,由于storeCode相对集中,那么几乎的是必现的。

优化方案

1. 让测试调整数据,但治标不治本,对于系统的健壮性没有任何帮助,只是掩耳盗铃。

2. 调整索引,经过看源码我们发现,实际上所有SQL都是这个条件。那么,我们直接删除index_storeCode索引,改为 (userId,storeCode,consumeTime) 联合索引。这样锁粒度最小,也就不容易导致锁冲突。

3. 调整记录方案,问了下应用场景,只需做当天的拦截,那么可以考虑用redis。

4. 调整为主键更新。

结论

1.由于系统代码已经很久没有迭代,要求尽量不要改动代码,我们最终采取了方案2,去除了 index_userId, index_storeCode索引,改为(userId,storeCode,consumeTime) 联合索引。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值