MySQL(InnoDB剖析):36---锁之(阻塞、死锁、锁升级)

一、阻塞

  • 因为不同锁之间的兼容性问题,在有些时刻一个事务中的锁需要等待另一个事务中的锁释放它所占用的资源,这就是阻塞。阻塞并不是一件坏事,其是为了确保事务可以并发且正常地运行

innodb_lock_wait_timeout参数

  • 该参数控制锁发生阻塞时等待的时间(默认是50秒)

  • 该参数是动态的,因此可以在数据库运行时设置
  • 在超时之后数据库会抛出一个1025的错误,如下:

innodb_roolback_on_timeout参数

  • 用来设定是否在等待超时时对进行中的事务进行回滚操作(默认为OFF,代表不回滚)
  • 该参数是静态的,因此不可以在数据库运行时设置

  • 默认情况下,InnoDB不会回滚超时引发的错误异常。其实InnoDB在大部分情况下都不会对超时的事务进行回滚(死锁除外,见下面死锁的演示案例),因此用户必须判断是否需要commit还是rollback,然后再进行下一步的操作

演示案例

  • 一个表t中有3行数据
  • 会话A:开启一个是事务,然后查询表t中小于4的字段值,并且加X锁
    • 在Next-Key Lock算法(REPEATABLE READ隔离级别下)下锁定了小于4的所有记录(其中也锁定了4这个记录本身)
  • 会话B:开启一个事务,然后向表中插入一行数据5(此SQL语句可以执行),然后再插入数据3(因为记录3在会话A中被锁定了),因此会话B此时阻塞
  • 会话B:阻塞一会儿后,会话B超时(如下图所示),超时之后再次进行查询操作,发现5这个记录插入成功(因为默认情况下,阻塞超时事务没有回滚)

二、死锁

  • 死锁是指:两个或两个以上的事务在执行过程中,因争夺资源而造成的一种互相等待的现象。若无外力作用,事务都将无法推进下去

解决死锁方式①

  • 解决死锁问题最简单的方式:是不要有等待,将任何的等待都转换为回滚,并且事务重新开始
  • 毫无疑问,这的确可以避免死锁问题的产生。而然在线上环境中,这可能导致并发性能下降,甚至任何一个事务都不能进行。而这所带来的的问题远比死锁问题更为严重,因为这很难被发现并且浪费资源

解决死锁方式②

  • 解决死锁还有一个方式是超时,即当两个事务互相等待时,当一个等待时间超过设置的某一阈值,其中的一个事务进行回滚,另一个等待事务就能继续进行
  • 在上面的介绍中,参数innodb_lock_wait_timeout参数用来设置超时的时间

解决死锁方式③(等待图)

  • 超时机制的缺点:超时机制虽然很简单,但是其仅通过超时后对事务进行回滚的方式来处理,或者说其实根据FIFO的顺序选择回滚对象。但若超时的事务所占权重比较大,如事务操作更新了很多行,占用了较多的undo log,这时采用FIFO的方式就显得不合适了,因为回滚这个事务的时间相对另一个事务所占用的时间可能会很多
  • 除了超时机制,当前数据库还都普遍采用wait-for graph(等待图)的方式来进行死锁检测。与超时的解决方案相比,这是一种更为主动的死锁检测方式。InnoDB也采用这种方式
  • wait-for graph的死锁检测机制通常采用深度优先的算法实现,在InnoDB 1.2版本之前,都是采用递归方式实现。而从1.2版本开始,对wait-for graph的死锁检测进行了优化,将递归用非递归的方式实现,从而进一步提高了InnoDB的性能
  • wait-for graph要求数据库保存以下两种信息:
    • 锁的信息链表
    • 事务等待链表
  • 通过上述链表可以构造出一张图,而在这个图中若存在回路,就代表存在死锁,因此资源间相互发生等待
  • 在wait-for graph中,事务为图中的节点。而在图中,事务T1指向T2边的定义为:
    • 事务T1等待事务T2所占用的资源
    • 事务 T1最终等待T2所占用的资源,也就是事务之间在等待相同的资源,而事务T1发生在事务T2的后面

图示

  • 下面看一个例子,当前事务和锁的状态如下图所示:
    • 在Transactuion Wait Lists可以看到有4个事务t1、t2、t3、t4,因此在wait-for graph中应该有4个节点
    • 而事务t2对row1占用X锁,事务t1对row2占用S锁
    • 事务t1需要等待事务t2中row1的资源,因此在wait-for graph中有条边从节点t1指向节点t2
    • 事务t2同样需要等到事务t1、t4所占用的row2对象,故而存在节点t2到节点t1、t4的边
    • 同样,存在节点t3到节点t1、t2、t4的边,因此最终的wait-for graph如下图所示

  • 通过上图可以发现存在回路(t1,t2),因此存在死锁

  • 通过上述介绍,可以发现wait-for graph是一种较为主动的死锁检测机制,在每个事务请求锁并发生等待时都会判断是否存在回路,若存在则有死锁,通常来说InnoDB选择回滚undo量最小的事务

死锁概率

  • 死锁应该非常少发生,若经常发生,则系统是不可用的。此外,死锁的次数应该还要少于等待,因为至少需要2次等待才会产生一次死锁。本节从纯数学的概率角度来分析,死锁发生的概率是非常小的
  • 假设当前数据库中共有n+1个线程执行,即当前总共有n+1个事务:
    • 并假设每个事务所做的操作相同
    • 若每个事务由r+1个操作组成,每个操作为从R行数据中随机地操作一行数据,并占用对象的锁
    • 每个事务在执行完最后一个步骤释放所占用的所有资源
    • 最后,假设nr<<R,即线程操作的数据只占用所有数据的一小部分
  • 在上述模型下,事务获得一个锁需要等待的概率为多少呢?当事务获得一个锁,其他任何一个事务获得锁的情况为:

  • 由于每个操作为从R行数据中取一条数据,每行数据被取到的概率为1/R,因此,事务中每个操作需要等待的概率PW为:

  • 事务是由r个操作所组成,因此事务发生等待的概率PW(T)为:

  • 死锁是由于产生回路,也就是事务互相等待而发生的,若死锁的长度为2,即两个等待节点间发生死锁,那么其概率为:

  • 由于大部分死锁发生的长度为2,因此上述公式基本代表了一个事务发生死锁的概率。从整个系统来看,任何一个事务发生死锁的概率为:

  • 从上述的公式中可以发现,由于nr<<R,因此事务发生死锁的概率是非常低的。同时,事务发生死锁的概率与以下几点因素有关:

    • 系统中事务的数量(n),数量越多发生死锁的概率越大

    • 每个事务操作的数量(r),每个事务操作的数量越多,发生死锁的概率越大

    • 操作数据的集合(R),越小则发生死锁的概率越大

死锁演示案例

  • 如果程序是串行的,那么不可能发生死锁。死锁只存在于并发的情况,而数据库本身就是一个并发运行的程序,因此可能会发生死锁
  • 下图展示了死锁的一种经典的情况,即A等待B,B等待A,这种死锁问题被称为AB-BA死锁

  • 死锁的原因是会话A和B的资源在互相等待
  • 大多数的死锁InnoDB本身可以侦测到,不需要人为进行干预。但是上面的例子中,在会话B的事务抛出死锁异常后,会话A马上得到字段a=2的资源,这其实是因为会话B的事务发生了回滚,否则会话A中的事务是不可以得到该资源的
  • 在上面做“阻塞”中说过,InnoDB并不会回滚大部分的错误异常,但是死锁除外。发现死锁后,InnoDB会马上回滚一个事务,这点需要注意。因此如果应用程序捕获了1213这个错误,其实并不需要对其进行回滚

死锁与外键

  • Oracle数据库产生死锁的常见原因是没有对外键添加索引,而InnoDB会自动对其添加索引,因而能够很好地避免了这种情况的发生。而人为删除外键上的索引,MySQL会抛出一个异常
  • 创建一个表p和表c,表c为表p的子表:
create table p(
    a int,
    primary key(a)
)engine=innodb;

create table c(
    b int,
    foreign key(b) references p(a)
)engine=innodb;

 

  • InnoDB会自动在外键上建立一个索引b:
show index from c\G;

 

  •  并且,人为删除这个列是不被允许的:
drop index b on c;

 

死锁演示案例

  • 此外,还存在另一种死锁,即当前事务持有了待插入记录的下一个记录的X锁,但是在等待队列中存在一个S锁的请求,则可能会发生死锁
  • 下图是演示案例的整体过程

  • 首先创建测试表t,并导入4条记录
drop table if exists t;

create table t(
    a int primary key
)engine=innodb;

insert into t values(1),(2),(4),(5);

 

  • 会话A:开启一个事务,并且查询字段a=4的记录(加上了X锁)
begin;

select * from t where a=4 for update;

 

  • 会话B:开启一个事务,此时去查询字段a<=4的记录(试图加S锁),但是此处会阻塞(因为字段4被加了X锁)
begin;

select * from t where a<=4 for update;

  • 会话A:此时向数据库中插入一条记录3会发生死锁(会话A的所有事务回滚)因为会话A回滚了,因此会话B继续执行
insert into t values(3);

  • 为什么此处会话A会发生死锁:
    • 因为会话B此时对记录1、2的S锁都获取成功了,只需要等待记录4的X锁释放就行了
    • 但是如果会话A再插入一条新的记录3,那么会话B在获得S锁之后还需要向后获得记录3的锁,这样似乎有点不合理,因此发生死锁
  • 这里的演示案例回滚的是undo log记录大的事务,这与上面介绍的AB-BA死锁的处理方式有不同

三、锁升级

  • 锁升级是指:将当前锁的粒度降低。举例来说,数据库可以把一个表的1000个行锁升级为一个页锁,或者将页锁升级为表锁

锁升级现象

  • 如果在数据库的设计中认为锁时一种稀有资源,而且像避免锁的开销,那数据库会频繁出现锁升级现象
  • SQL Server数据库的设计认为锁是一种稀有的资源,在合适的时候会自动地讲行、键、或分页锁省纪委更粗粒度的表级锁。这种升级保护了系统资源,防止系统使用太多的内存来维护锁,在一定程度上提高了效率
  • 即使在SQL Server 2005版本之后,SQL Server数据库支持了行锁,但是其设计和InnoDB完全不同,在以下情况依然可能发生锁升级:
    • 由一句单独的SQL语句在一个对象上持有的锁的数量超过了阈值,默认这个阈值为5000。值得注意的是,如果是不同对象,则不会发生锁升级
    • 锁资源占用的内存超过了激活内存的40¥时就会发生锁升级
  • 在 Microsoft SQL Server数据库中,由于锁是一种稀有的资源,因此锁升级会带来一定的效率提高。但是锁升级带来的一个问题却是因为锁粒度的降低而导致并发性能的降低
  • InnoDB存储引擎不存在锁升级的问题。因为其不是根据每个记录来产生行锁的,相反,其根据每个事务访问的每个页对锁进行管理的,采用的是位图的方式。因此不管个事务锁住页中一个记录还是多个记录,其开销通常都是一致的
  • 假设一张表有3000000个数据页,每个页大约有100条记录,那么总共有300000000条记录。若有一个事务执行全表更新的SQL语句,则需要对所有记录加锁。若根据每行记录产生锁对象进行加锁,并且每个锁占用10字节,则仅对锁管理就需要差不多需要3GB的内存。而 InnoDB存储引擎根据页进行加锁,并采用位图方式,假设每个页存储的锁信息占用30个字节,则锁对象仅需90MB的内存。由此可见两者对于锁资源开销的差距之大
发布了1462 篇原创文章 · 获赞 996 · 访问量 35万+

猜你喜欢

转载自blog.csdn.net/qq_41453285/article/details/104317756
今日推荐