在生产环境遇到签到接口并发问题,update语句和表结构如下:
update channel_token set token = ? where channel_code = ? and dot_code=? and version=?;
CREATE TABLE channel_token
(
channel_code
VARCHAR(32) NOT NULL ,
dot_code
VARCHAR(32) NULL DEFAULT NULL ,
token
VARCHAR(32) NOT NULL ,
type
VARCHAR(1) NOT NULL ,
expire_time
DATETIME NOT NULL ,
last_time
DATETIME NOT NULL ,
version
BIGINT(20) NOT NULL,
PRIMARY KEY (token
),
UNIQUE INDEX uniqe_index
(channel_code
, dot_code
) USING HASH
)COLLATE=‘utf8_general_ci’ ENGINE=InnoDB;
mysql的事务支持与存储引擎有关,MyISAM不支持事务,INNODB支持事务,更新时采用的是行级锁。这里采用的是INNODB做存储引擎,意味着会将update语句做为一个事务来处理。前面提到行级锁必须建立在索引的基础,这条更新语句用到了唯一索引,所以这里肯定会加上行级锁。 行级锁并不是直接锁记录,而是锁索引,如果一条SQL语句用到了主键索引,mysql会锁住主键索引;如果一条语句操作了非主键索引,mysql会先锁住非主键索引,再锁定主键索引
这个update语句会执行以下步骤:
1、由于用到了非主键索引,首先需要获取普通索引上的行级锁
2、紧接着根据主键进行更新,所以需要获取主键上的行级锁;
3、更新完毕后,提交,并释放所有锁。
innodb引擎会先判断插入的行是否产生重复key错误,如果存在,在对该现有的行加上S(共享锁)锁,如果返回该行数据给mysql,然后mysql执行完duplicate后的update操作,然后对该记录加上X(排他锁),最后进行update写入。
- 会话S1尝试获取记录的S锁,该记录未被其他会话加锁,获取S锁成功。
- 会话S2尝试获取记录的S锁,该记录上被加持S锁,但由于S锁与S锁兼容,获取S锁成功
- 会话S1尝试获取记录的X锁,由于会话S2对该记录持有S锁,S锁与X锁不兼容,获取X锁失败,会话S1被阻塞
- 会话S2尝试获取记录的X锁,由于会话S1对该记录持有S锁,S锁与X锁不兼容,获取X锁失败,会话S2被阻塞
- 会话S2被阻塞后进入死锁检查环节,发现阻塞S1->S2和S2->S1形成死锁环路,触发死锁机制强制回滚S1或S2事务。
经验教训:
无论前台后台的程序,都不应该存在仅根据非主键的几个字段一查就要update/delete的场景。即使有,也应该改为先把要更新的记录查出来然后逐条按主键id更新