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

2019-10-17  本文已影响0人  陈亚文

场景介绍,用户有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的具体操作:

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

解决方案:

并发下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会话报错死锁。

解决方案:

死锁是指两个或两个以上的事务在执行过程中,因争夺锁资源而造成的一种互相等待的现象。解决死锁问题最简单的方法是超时,即两个事务互相等待时,当一个等待时间超过设置的某一阈值,其中一个事务进行回滚,另一个等待的事务就能继续进行。但是从上述案例中,并不是等待超时,而是死锁后直接触发了事务回滚,显然不是等待超时的解决方案。
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

死锁发生过程分析

情景三、模拟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会话都使用主键索引可知,会话只会等待锁超时,不会立即中断事务。因此在编码中,尽可能避免在对同一条记录操作的事务中,混用主键索引和非主键索引。

上一篇 下一篇

猜你喜欢

热点阅读