51Testing软件测试论坛

 找回密码
 (注-册)加入51Testing

QQ登录

只需一步,快速开始

微信登录,快人一步

手机号码,快捷登录

查看: 3263|回复: 0
打印 上一主题 下一主题

[原创] 数据库行锁争用问题分析

[复制链接]
  • TA的每日心情
    无聊
    3 天前
  • 签到天数: 1050 天

    连续签到: 1 天

    [LV.10]测试总司令

    跳转到指定楼层
    1#
    发表于 2021-2-18 10:27:12 | 只看该作者 回帖奖励 |正序浏览 |阅读模式
    某系统的批量程序和联机程序同时发起交易、操作数据库同一张表时,出现了交易间相互影响导致交易失败。
      分析原因为事务A对数据库表操作全表删除时,数据库机制会自动触发产生排他行锁,当行锁数达到“升级到表锁前允许的最大行锁数”时,会自动升级为表锁。但因事务B已对数据库表产生了排他行锁,则事务A自动升级表锁失败。直至事务A消耗掉了数据库“最大锁资源数”,系统会报错“锁资源耗尽”、交易失败。
      因此,设计程序时,如涉及多事务操作数据库同一张表,需考虑事务间的关联影响,是否会引发事务冲突。
      下文将详细描述和分析该问题。
      1 数据库锁
      首先介绍一下数据库锁的相关概念。
      1.1 行锁、页锁和表锁
      行锁是访问数据库的时候,锁定一行或者多行记录。页锁是访问数据库的时候,锁定查询结果所在页。表锁是访问数据库的时候,锁定整张表,在表被锁定期间,其他事务不能对该表进行操作,必须等当前表的锁被释放后才能进行操作。行锁的优点是锁定粒度小,发生锁冲突的概率低,并发度高;缺点是开销大,加锁慢,会出现死锁。表锁的优点是开销小,加锁快,不会出现死锁;缺点是锁定力度大,发生锁冲突概率高,并发度最低。页锁的开销和加锁速度介于表锁和行锁之间,会出现死锁,锁定粒度介于表锁和行锁之间,并发度一般。
      共享锁 (S锁) 也称为读锁,用于不更改或不更新数据的操作(只读操作),如 SELECT 语句。如果事务T对数据A加上共享锁后,则其他事务只能对A再加共享锁,不能加排他锁。获准共享锁的事务只能读数据,不能修改数据。其封锁粒度包括行级、页级和表级。
      排他锁(X锁)也称为独占锁、写锁,用于数据修改操作,例如INSERT、UPDATE 或 DELETE。确保不会同时同一资源进行多重更新。如果事务T对数据A加上排他锁后,则其他事务不能再对A加任任何类型的封锁。获准排他锁的事务既能读数据,又能修改数据。我们在操作数据库的时候,可能会由于并发问题而引起的数据的不一致性(数据冲突)。其封锁粒度也包括行级、页级和表级。
      1.2 意向锁
      意向锁的产生原因:解决表锁与之前可能存在的行锁冲突,避免为了判断表是否存在行锁而去扫描全表的系统消耗。
      作用:一种快速判断表锁与之前可能存在的行锁冲突的机制。(数据库会帮我们自动加)
      行锁在加锁前要先加意向锁。意向锁是一种表锁。
      事例分析:事务 A 锁住了表中的一行,让这一行只能读,不能写。之后,事务 B 申请整个表的写锁。如果事务 B 申请成功,那么理论上它就能修改表中的任意一行,这与 A 持有的行锁是冲突的。
      数据库需要避免这种冲突,就是说要让 B 的申请被阻塞,直到 A 释放了行锁。数据库要怎么判断这个冲突呢?
      step1:判断表是否已被其他事务用表锁锁表。
      step2:判断表中的每一行是否已被行锁锁住。
      注意step2,因为需要遍历整个表,从而导致判断方法的效率不高。为了更有针对性的进行判断,便产生了意向锁的概念。
      在意向锁存在的情况下,事务 A 必须先申请表的意向共享锁,成功后再申请一行的行锁。
      在意向锁存在的情况下,上面的判断可以改成:
      step1:不变。
      step2:发现表上有意向共享锁,说明表中有些行被共享行锁锁住了,因此,事务 B 申请表的写锁会被阻塞。
      意向锁分为意向共享锁(共享锁定)和意向排他锁(排他锁定),且意向锁是表级别锁。
      例子-执行update:
      1> update PD set workdate = '20200603' where PDid ='23'
      2> go
      (1 row affected)
      1> sp_lock3x
      2> go
      返回结果:

      解释:
      Sh--共享锁(S锁)
      Ex--独占锁/排他锁(X锁,Exclusive Lock)
      Update--更新锁(U锁)
      Table或者intent--锁发生在表
      Page--锁发生在表
      Row--锁发生在行
      blk--这个进程正在阻塞另一个需要获取一个锁的进程,一旦这个进程处理完成,其他进程就可以继续处理了
      demand--这个进程正在试图获取一个锁
      例子-未执行update前:
      1> sp_lock3x
      2> go
      返回结果:

      1.3触发机制
      所有关系型数据库都存在行锁,不同数据库触发机制大致相同,只是手工生成行锁方法略有不同。
      MySQL数据库,不同的存储引擎支持不同的锁机制。MyISAM和MEMORY存储引擎采用的是表级锁;BDB存储引擎采用的是页面锁,但也支持表级锁;InnoDB存储引擎既支持行级锁,也支持表级锁,但默认情况下是采用行级锁。以InnoDB存储引擎举例,意向锁是InnoDB自动加的,不需用户干预。对于UPDATE、DELETE和INSERT语句,InnoDB会自动给涉及数据集加排他行锁(X);对于普通SELECT语句,InnoDB不会加任何锁;事务可以通过以下语句显示给记录集加共享行锁或排他行锁。
      共享锁(S):SELECT * FROM table_name WHERE ... LOCK IN SHARE MODE。
      排他锁(X):SELECT * FROM table_name WHERE ... FOR UPDATE。
      Oracle数据库,行锁在操作DML(UPDATE、DELETE、INSERT)语句时,Oracle会自动加上行锁。在select * from table for update 【of column】【nowait|wait 3】时,oracle也会自动加锁;但在缺省情况下,单纯地读数据(SELECT)并不加锁,Oracle通过回滚段来保证用户不读“脏”数据。
      参数【nowait|wait 3】,一般在for update 时加nowait,这样就不用等待其他事务执行了,一判断有事务,立马抛出错误。
      参数【of column】,一般的of column都是在多表操作的时候,能锁定其中一个表的某些列,如果还是同一张表的话,并不能锁定某些列。
      Sybase数据库,支持行锁、页锁、表锁。意向锁是自动加的,不需用户干预。对于UPDATE、DELETE和INSERT语句,会自动给涉及数据集加排他行锁(X);对于普通SELECT语句,不会加行锁。
      2 现象描述
      测试环境上某系统运行批量程序时,报错“锁资源耗尽”。
      经分析,产生原因为测试环境上跑批量时,准备清理LSB表中小于当日的数据,Sybase数据库执行SQL语句“EXEC SQL delete FROM LSB WHERE appdate < :Workdate”,数据量约300万条。而当时正在同时发起实时联机交易,对LSB表生成了意向排他锁和排他行锁未释放,该批量执行delete语句需使用300w个行锁(锁资源最大可使用100w个)且因排他行锁存在无法升级使用表锁,导致批量失败,报错“锁资源耗尽”。
      3 原因分析
      A:对LSB表执行update语句
      B:EXEC SQL delete FROM LSB WHERE appdate < :Workdate
      C:EXEC SQL delete top 4000 FROM LSB
      WHERE appdate < :Workdate
      当执行A操作时,数据库对LSB先产生一个意向排他锁,再产生一个排他行锁。当执行B操作时,删除300w条记录,会产生300w条行锁(行锁大于5k条,曾试图升级表锁),但发现LSB表存在意向排他锁,升级行锁失败,只能使用行锁(行锁资源上限为100w条),B交易报错“锁资源耗尽”。
      另外,测试环境上很少出现“锁资源耗尽”现象,分析原因为意向排他锁是瞬时的、转瞬即逝的,很快就消失,所以非常偶发。只有当A的排他意向锁与B的行锁升级为表锁的时刻碰撞上,才会出现上述现象。
      当将B方案优化为C方案时,控制每次产生行锁的条数小于5k,避免升级为表锁,这样利于A(前提是A的tps小于500)和C同时操作且互不影响。
      4 应对措施
      通过将“流水表批量清理交易”优化为先查找top 1的记录,若记录存在则delete前top 4000条记录,循环执行,直至小于当日的数据全部清理。
      5 测试验证
      测试环境的数据库配置参数和铺底数据如下:

      查看数据库锁资源大小
      1> sp_configure "number of locks"
      2> go
      返回结果:

      测试发现,当测试环境模拟实时联机交易持续运行(TPS为4-46笔/秒)时,新、旧程序均执行成功,CPU使用率无差异,无死锁,新程序的执行时间长于旧程序。但是,交易背景未复现该问题。
      当测试环境模拟对LSB表执行update语句、但事务不提交、持续产生排他意向锁和排他行锁的场景后,旧程序运行不到1s,报错“锁资源耗尽”,交易失败,复现该问题。而新程序运行30s,交易成功,锁资源使用数约为4000-4010个。
      执行SQL语句,但不提交:
      1> begin transaction
      2> go
      1> update PD set workdate = '20200602' where PDid ='23'
      2> go
      (1 row affected)
      1>
      执行SQL语句,且提交:
      1> begin transaction
      2> go
      1> update PD set workdate = '20200602' where PDid ='23'
      2> go
      (1 row affected)
      1> commit
      2> go
      1>
      因此,当实时联机交易对LSB表生成了排他意向锁未释放时,旧程序复现该问题、交易成功率为0%,新程序执行成功、交易成功率为100%,新程序的清理机制优于旧程序的清理机制。测试通过。

      WHERE appdate < :Workdate
      1、交易背景“update语句,但事务不提交,持续产生排他意向锁和排他行锁”下,旧程序报错。
      2、交易背景“update语句,但事务不提交,持续产生排他意向锁和排他行锁”下,新程序交易成功。
      6 问题启示
      程序设计时,需考虑多支交易对数据库(包括MySQL、Oracle、Sybase等)同一张表产生排他行锁是否相互影响的场景。特别是出现删除大量数据等操作,如其他交易也对该表产生排他行锁,建议控制事务大小,如每次的删除记录条数,避免行锁升级表锁失败从而导致交易报错。
      还需考虑,两个交易均需更新数据库同一张表的记录时,程序均需先锁表,再进行下一步操作,如顺序颠倒易出现交易失败。

    分享到:  QQ好友和群QQ好友和群 QQ空间QQ空间 腾讯微博腾讯微博 腾讯朋友腾讯朋友
    收藏收藏1
    回复

    使用道具 举报

    本版积分规则

    关闭

    站长推荐上一条 /1 下一条

    小黑屋|手机版|Archiver|51Testing软件测试网 ( 沪ICP备05003035号 关于我们

    GMT+8, 2024-11-24 06:38 , Processed in 0.067959 second(s), 24 queries .

    Powered by Discuz! X3.2

    © 2001-2024 Comsenz Inc.

    快速回复 返回顶部 返回列表