高并发加锁同步及主键非主键混用死锁问题研究

场景介绍,用户有100块钱,同时给100个人转账,每个收款用户1快钱。在高并发下,付款方扣减金额时,客户资金安全不可控制,且容易发生死锁。

数据表:账户表account、流水表txnlog

create table account (
    'custname' varchar(32) not null comment '姓名',
    'acct' varchar(32) not null comment '账号',
    'amt' bigdecimal not null comment '账户金额',
    'identify' varchar(32) not null comment '证件号',
    'identifynum' varchar(32) not null comment '证件类型'
    primary key 'acct',
    unique index idx('identify','identifynum'),
    unique index idx_name('custname')
)
comment='账户表'
collate='uft8_bin'
engine='InnoDB'
)
create table txnlog (
    'id' int(11) not null comment 'id',
    'custname' varchar(32) not null comment '姓名',
    'acct' varchar(32) not null comment '账号',
    'amt' bigdecimal not null comment '账户金额',
    'skacct' varchar(32) not null comment '收款账号',
    'skcustname' varchar(32) not null comment '收款姓名'
    primary key 'id',
)
comment='流水表'
collate='uft8_bin'
engine='InnoDB'

高并发资金安全问题分析

两个转账请求同时到达并处理,对应A、B两个事务。存在问题:脏读、可重复读、不可重复读(官方定义为幻像/幻读问题)、丢失更新。

事务 动作
事务A 1、查询账户表账户金额;
2、扣减账户金额更新账户表;
3、生成转出流水;
4、查询账户表账户金额
事务B 1、查询账户表账户金额;
2、扣减账户金额更新账户表;
3、生成转出流水;
4、查询账户表账户金额

A、B事务的处理顺序如下 :

步骤 A B
1 查询账户金额
2 查询账户金额
3 扣减账户金额更新账户表
4 生成转出流水
5 查询账户金额
6 扣减账户金额更新账户表
7 生成转出流水
8 查询账户金额

innodb存储引擎默认隔离级别repeatable read(可重复读),解决了脏读问题,保证了在同一事务中,多次读取同样事务的结果是一致的。 但是理论上,可重复读隔离级别还是无法解决另一个幻读(phantom read)问题。所谓幻读,指当前事务在读取某个范围内的记录时,另一个事务又在该范围内插入了新的记录。当之前的事务又在该范围内插入了新的记录,会产生幻行。更通俗点,phantom problem是指在同一事务下,连续执行两次同样的sql语句可能导致不同的结果,第二次的sql语句可能会返回之前不存在的行。A中若增加统计流水总数 select count(*) from txnLog;,可模拟出幻读问题 。 为解决幻读问题,innodb存储引擎引入了多版本并发控制(MVCC)解决幻读问题。

附:mvcc的实现,是通过保存数据在某个时间点的快照来实现的,也就是说,不管需要执行多长时间,每个事务内看到的数据都是一致的。根据事务开始的时间不同,每个事务对同一张表,同一时刻看到的数据可能是不一样的。 innodb的MVCC,是通过在每行记录后面保存两个隐藏的列来实现的。这两个列,一个保存了行的创建时间,一个保存了行的过期时间(或删除时间),当然存储的并不是实际的时间值,而是系统版本号,每开始一个新的事务,系统版本号都会自动递增。事务开始时刻的系统版本号会作为事务的版本号,用来和查询到的每行记录的版本号进行比较。

innodb存储引擎默认隔离级别repeatable read (可重复读),mvcc的具体操作:

  • select, innodb会个根据以下两个条件检查每行记录。 1、innodb只查找版本早于当前事务版本的数据行(行的系统版本号小于或等于事务的系统版本号),这样可以确保事务读取的行,要么是在事务开始前已经存在,要么是事务自身插入或者修改过的。 2、行的删除版本要么未定义(未执行过删除操作),要么大于当前事务版本号,可确保事务读取到的行,在事务开始之前未被删除。
  • insert , innodb为新插入的每一行保存当前系统版本号作为行版本号
  • delete, innodb为删除的每一行保存当前系统版本号作为行删除标示
  • update, innodb为插入一行新数据,保存当前系统版本号作为行版本号,同时保存当前系统版本号到原来的行作为行删除标示。(更新一条记录,分为两步,新增一条记录,同时更新原记录的删除标示)

在这个场景中,由于mvcc的作用,A无法看到B的修改,A中扣减账户金额是在A开始的快照上进行的。若A修改成功,会导致B对账户表的修改丢失。

解决方案:
  • 方案一,修改隔离级别为read committed(提交读),一个事务开始时,只能看见已经提交的事务所做的修改。即A事务能看到B事务的修改。 生产环境不允许修改隔离级别,本方案不可用
  • 方案二,接受innodb默认隔离级别repeatable read(可重复读),因innodb存储引擎采用了next-key locking的算法避免phantom problem,如select * from t where a>2 for update,其锁住的是(2,+无穷)这个范围加了X锁,任何对这个范围的插入都是不允许的,从而避免phantom problem。 对于账户表、流水表在一个事务中的操作。第一步对账户表加X锁(select * from account where 账号='123456' for update),后续再进行账户修改、流水修改。 使得对同一账户的操作,其他事务需要等待X锁。此外,对于不可变值的查询,可以提到事务开始前进行。

并发下innodb主键索引与非主键索引同时使用死锁问题分析

情景一、A事务,主键索引取的X锁,使用非主键索引更新。B事务,使用非主键索引更新,如下

步骤 A B
1 主键索引
set autocommit=false;
begin;
select * from account where acct='123456' for update;
2 非主键索引
set autocommit=false;
begin;
update account set amt=amt+1 where identify='456789' and identifynum='01';
3 非主键索引
update account set amt=amt+1 where identify='456789' and identifynum='01' ;
4 mysql>update account set amt=amt+1 where identify='456789' and identifynum='01' ;

ERROR 1213 (40001):Deadlock found when trying to get lock; try restarting transaction

执行到A3,B4会话报错死锁。

  • 之所以会造成B死锁,在innodb存储引擎下,行级锁并不是直接锁记录,而是锁索引。
  • A1 的sql语句用的主键作为条件,锁的是主键索引,A事务占用了主键。
  • B2的sql语句,用的是非主键作为条件,mysql会先锁非主键索引,再锁主键索引,此时B事务占用了非主键索引,等待主键索引。
  • A3的sql语句,使用了非主键索引进行update操作,会等待B的非主键索引释放。A/B两个事务间形成死锁。
  • innodb通过死锁检测机制wait-for graph 等待图, 检测到死锁回路,选择回滚undo量最小的事务B,对其进行回滚,让A继续执行。

解决方案:

  • 1、在所有账户表修改操作开始前,使用主键索引对账户表加锁 select * from account where acct='123456' for update。 再进行账户修改、流水操作、其他。
  • 2、对账户表的查询和修改,统一使用主键索引或非主键索引。避免主键索引与非主键索引共用时造成的死锁。
    附:测试代码中死锁的发生,1、A会话为eclipse编译器执行,代码 select * from account where acct='123456' for update 下一行打断点,执行到断点出。 2、打开mysql的会话窗口,执行B2。 3、eclipse继续往下走,A3执行后,查看B会话被中断,能看到死锁提示。

死锁是指两个或两个以上的事务在执行过程中,因争夺锁资源而造成的一种互相等待的现象。解决死锁问题最简单的方法是超时,即两个事务互相等待时,当一个等待时间超过设置的某一阈值,其中一个事务进行回滚,另一个等待的事务就能继续进行。但是从上述案例中,并不是等待超时,而是死锁后直接触发了事务回滚,显然不是等待超时的解决方案。
innodb存储引擎采用wait-for graph(等待图)的方式来进行死锁检测,较之超时的方案,这是一种更为主动的死锁检测方式。在每个事务请求锁并发生等待时都会判断是否存在回路,若存在则有死锁,通常来说innodb存储引擎会选择回滚undo量最小的事务。

情景二、模拟两个非主键索引 unique index idx('identify','identifynum')、unique index idx_name('custname') 混用发生死锁,

步骤 A B
1 主键索引
set autocommit=false;
begin;
update account set amt=amt+1 where custname=‘张三’;
2 非主键索引 identify + identifynum
set autocommit=false;
begin;
update account set amt=amt+1 where identify ='45678' and identifynum='01';
3 非主键索引 custname

update account set amt=amt+1 where identify ='45678' and identifynum='01';
4 死锁中断
mysql>update account set amt=amt+1 where identify='456789' and identifynum='01' ;

ERROR 1213 (40001):Deadlock found when trying to get lock; try restarting transaction

死锁发生过程分析

  • A1获取了非主键索引custname,然后获取了主键索引acct
  • B2获取了非主键索引identify + identifynum ,等待主键索引acct
  • A3需要获取非主键索引identify + identifynum,但该索引已被B获取,A3等待非主键索引identify + identifynum的释放,死锁发生。
  • innodb的死锁检测机制wait-for graph 等待图,检测到锁等待回路,判断死锁发生,选择回滚undo量最小的事务。innodb判断B为undo最小的事务,中断并回滚B事务。A事务继续往下执行。
情景三、模拟A、B会话均为相同主键索引
步骤 A B
1 主键索引
set autocommit=false;
begin;
select * from account where acct='123456' for update;
2 主键索引
set autocommit=false;
begin;
update account set amt=amt+1 where acct='123456';
3 停下来不继续往下走
4 等待超时
mysql>update account set amt=amt+1 where acct='123456';

ERROR 1205 (HY000):Lock wait timeout exceeded; try restarting transaction

A3停下来,不往下走,则B4会提示等待锁超时,会话中断。从A、B会话都使用主键索引可知,会话只会等待锁超时,不会立即中断事务。因此在编码中,尽可能避免在对同一条记录操作的事务中,混用主键索引和非主键索引。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,013评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,205评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,370评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,168评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,153评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,954评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,271评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,916评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,382评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,877评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,989评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,624评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,209评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,199评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,418评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,401评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,700评论 2 345