最近工作非常郁闷,天天被领导盯着。主要是系统近来死锁发生在频率很高。最终,经过大家的共同努力,我们成功的定位并解决了问题,所以把过程中学习的知识与经验分享一下
问题背景
系统中有一个账户模块,负责管理和维护会员的各种资金及明细,对外的功能涉及资金的增加与扣减等。通过监控系统发现,当外围系统并发访问和调用同一个会员的账户功能接口的时候,就会发生死锁和响应超时的情况,所以问题的分析还得从事务的并发说起。首先,我们先回顾一下事务。
事务是神马
所谓事务,它是一个操作序列,这些操作要么都执行,要么都不执行,它是一个不可分割的原子单位。例如,银行转帐工作:从一个帐号扣款并使另一个帐号增款,这两个操作要么都执行,要么都不执行。
事务的特性
- 原子性。一个事务是一个不可分割的工作单位,事务中包括的诸操作要么都做,要么都不做。
- 一致性。事务必须是使数据库从一个一致性状态变到另一个一致性状态。一致性与原子性是密切相关的。
- 隔离性。一个事务的执行不能被其他事务干扰。即一个事务内部的操作及使用的数据对并发的其他事务是隔离的,并发执行的各个事务之间不能互相干扰。
- 持久性。持续性也称永久性,指一个事务一旦提交,它对数据库中数据的改变就应该是永久性的。接下来的其他操作或故障不应该对其有任何影响。
四个特性中,与我们问题相关的应该就是事务的隔离性了,要理解隔离性,就不得不说说事务的并发了。
事务的并发及带来的影响
事务并发通俗点讲就是多个事务同时执行,并发的访问或更新数据库中相同的数据。事务并发一般需要相应的隔离措施,否则就会出现各种问题。
事务的并发会造成哪些问题?
- 丢失更新。第一种情况:当2个事务更新相同的数据源,如果第一个事务被提交,之后另外一个事务也提交了,那么第一个事务所做的更新就失丢了。第二种情况:当2个事务更新相同的数据源,如果第一个事务被提交,而另外一个事务却被撤销,那么会连同第一个事务所做的跟新也被撤销。
- 脏读。事务读取别的事务未提交的脏数据。如:事务1插入或更新了数据A后,事务2读取了数据A,这时事务1回滚了,那事务2读到的数据A就是脏数据了。
- 不可重复读。在同一个事务范围内,多次查询同一个数据的值不同。如:事务1查询数据A = 10,这里事务2把数据A的值更新为11后提交了,事务1在完成部分逻辑后再次读取数据A的值变为11了。
- 幻读。在同一个事务范围内,多次以相同的查询条件查到的数据数量不一样。如:事务1以条件X查询数据,这时事务2插入或删除了数据,之后事务1在完成部分逻辑后再次以条件X查询数据时,返回的结果数量不一样了。
- 覆盖更新。事务对数据的更新被别的事务覆盖了。如:事务1和2同时读取数据A = 10,事务1设置A = A - 1后更新数据A = 9,事务2设置A = A - 2后更新数据A = 8,最终数据A的值变成了8,但正确情况下A的值应该是7。
知道了事务并发会造成什么影响,那如何避免问题的发生呢?从数据库层面来讲,是通过封锁协议再解决的。
数据库封锁协议
在说封锁协议前,先得讲讲锁。
数据库中有两种锁:共享锁(读锁S)与排它锁(写锁X)。
- 读锁:若事务T对数据对象A加上S锁,则事务T可以读A但不能修改A,其他事务只能再对A加S锁,而不能加X锁,直到T释放S锁。
- 写锁:若事务T对数据对象A加上X锁,则只允许T读取和修改A,其他事务不能再对A加任何类型的锁,直到T释放A上的X锁。
再来看看封锁协议
- 一级封锁协议:事务T再修改数据R之前,必须先对其加X锁,直到事务结束才释放。一级封锁协议可以防止丢失更新。
- 二级封锁协议:在一级封锁协议的基础上,事务T在读取数据R之前,必须先对其加上S锁,读完之后立即释放S锁。二级封锁协议除了能防止丢失更新,还可以防止脏读。
- 三级封锁协议:在一级封锁协议的基础上,事务T在读取数据R之前,必须先对其加上S锁,直到事务结束后才释放。三级封锁协议可以防止丢失更新、脏读和不可重复读。
封议协议并不能解决事务并发的所有问题(
覆盖更新),另外,封锁协议是数据库内部实现事务并发控制的一种机制,我们无法在程序中使用。在程序开发中,是通过指定事务的
隔离级别来解决并发的各种问题的。
事务的隔离级别
有四种隔离级别,无论哪一种都不会出现
丢失更新,因为四种隔离级别都要求在更新数据对象前先要对数据加X锁,直到事务结束后才释放,即一级封锁协议。
- Read Uncommitted读未提交:脏读、不可重复读、幻读和覆盖更新都会发生,但并发性能最好。
- Read Committed读已提交:能避免脏读,但不可重复读、幻读和覆盖更新会发生,并发性能好。大部分数据库的默认隔离级别,并发性能较好。
- Repeatable Read可重复读:能避免脏读和不可重复读,但幻读会发生,并发性能会受影响。至于覆盖更新,则视数据库而论。
- Serializable序列化:能避免脏读、不可重复读和幻读,并发性能差。至于覆盖更新,则视数据库而论。
这样看来,事务隔离级别好像与封锁协议是一一对应的。读未提交与一级封锁协议对应、读已提交与二级封锁协议对应、可重复读与三级封锁协议对应。但经过本人的验证,这个不完全正确,隔离级别是一个规范,而封议协议是规范的一种实现方式。比如mysq同时使用数据行版本与锁的机制来实现隔离级别的,DB2单纯使用锁的机制来实现隔离级别。我会重新整理一篇文章讲讲关于mysql与DB2在隔离级别方面的细节与区别。
我们已经知道了相关的知识背景,再来看看我们系统的问题。
系统使用的是DB2,所有只读事务使用UR隔离级别(相关于读未提交),其他事务使用RS隔离级别(相关于可重复读)。会员的资产功能简单讲有增加和扣减操作。
增加操作
start transaction
//第一步,查询账户可用余额
select ... from account where account_id = ...
//第二步,余量加上增加量,并赋值给新变量newBalance
set newBalance = dbBalance + changeAmount
//第三步,更新会员账户记录
update account set balance = newBalance where account_id = ...
//第四步,其它操作
commit
扣减操作
start transaction
//第一步,查询账户可用余额
select ... from account where account_id = ...
//第二步,余量扣去增加量,并赋值给新变量NEW_BALANCE,如果余量不足则报错。
set newBalance = dbBalance - changeAmount
if ( newBalance < 0 ) throw new Exception("...");
//第三步,更新会员账户记录
update account set balance = newBalance where account_id = ...
//第四步,其它操作
commit
上面使用伪语言描述,可以看出,不论是增加还是扣减操作,都是先查询出账户记录,根据记录值作计算,最后再更新记录。当外围系统同时访问同一会员做扣减的时候,死锁就有可能发生了,比如:
事务T1执行第一步,查询了会员的账户记录,因为隔离级别是RS,所以会对会员账户记录加S锁;这时候事务T2也执行了第一步,也对会员的账户记录加了S锁;之后事务T1执行第三步,在准备更新会员账户记录前需要先对其加X锁,但发现记录已经被其它事务(事务T2)加了S锁,所以事务T1挂起,等待事务T2释放账户记录的S锁;接着事务T2也执行到第三步,在准备更新会员账户记录前需要先对其加X锁,但发现记录已经被其它事务(事务T1)加了S锁,所以事务T2也挂起等待事务T1。这样死锁就发生了。
知道了死锁发生的原因,那现在看看如何解决这个问题。死锁发生的原因主要是第一步查询的时候对账户记录加了S锁,所以如何我们把隔离级别降为CS(相当于读已提交),能否解决问题呢?答案是否,把隔离级底降为CS确实可以避免死锁的发生,因为查询操作结束后就会释放S锁,但是却会发生
覆盖更新的问题,所以这个方案不可行。既然问题出在第一步,那就是第一步出法看看吧,如果查询账户的时候,我们显示的对记录加X锁而不是S锁,那问题就解决了?看看:
事务T1执行第一步,查询了会员的账户记录,显式对会员账户记录加X锁;这时候事务T2执行到第一步,在查询会员账户记录尝试加X锁时会等待,因为记录已经被别的事务(事务T1)加了X锁;之后事务T1执行第三步,顺利的更新了记录,因为它已经占有记录的X锁;在事务T1提交之后,事务T2就可以继续往下执行了,所以死锁的问题解决了。
优化后的扣减操作
start transaction
//第一步,查询账户可用余额
select ... from account where account_id = ... for update with rs
//第二步,余量扣去增加量,并赋值给新变量NEW_BALANCE,如果余量不足则报错。
set newBalance = dbBalance - changeAmount
if ( newBalance < 0 ) throw new Exception("...");
//第三步,更新会员账户记录
update account set balance = newBalance where account_id = ...
//第四步,其它操作
commit
对于增加操作来讲,因为没有上限限制,所以可以直接更新增加量就可以了。优化后的增加操作
start transaction
//第一步,更新会员账户记录
update account set balance = balance + changeAmount where account_id = ...
//第二步,其它操作
commit
经过上面两个优化,死锁不再发生了,另外接口的平均响应时间也有不小的提高。
分享到:
相关推荐
本文来自于csdn,本文主要从分布式的原因,事务特性,和解决方案中深入理解了分布式事务,希望对您的学习有所帮助。 分布式事务就是指事务的参与者、支持事务的服务器、资源服务器以及事务管理器分别位于不同的...
Hibernate事务和并发控制Hibernate事务和并发控制Hibernate事务和并发控制
事务并发带来的坏处,以及通过设置事务隔离级别来处理
了解事务并发在开发中的应用和引起并发的原因
该PPT讲解,事务,嵌套事务,保存点,乐观并发控制及时间戳
数据库处理的经典资料: 叫你处理事务 如何并发处理 瞧瞧吧 机不可失哟
LINQ to SQL语句(13)之开放式并发控制和事务
Hibernate事务和并发控制 实例,想深入理解Hiberante的朋友,可以去研究一下。
当用户对数据库并发访问时,为了确保事务完整性和数据库一致性,需要使用锁定。事务和锁是两个紧密联系的概念。通过事务、批和锁的使用,还可以监测系统,以及优化物理数据库。作业是一种多步执行的任务。 本章主要...
数据库思维导图——并发控制 并发控制 多事务执行方式 (1)事务串行执行 每个时刻只有一个事务运行,其他事务必须等到这个事务结束以后方能运行 不能充分利用系统资源,发挥数据库共享资源的...事务并发执行带来的问题
hibernate 对事务并发处理
这是一个关于银行的数据库的处理,模仿的是从账户1转账给...那就需要用到事务,如果没有一起执行,就回滚。这个代码比较简单,希望对大家有帮助,自己建立一个数据库bank,在里面建立一个表Account,两属性:Id,money.
事务性服务组合及协同并发控制研究 刘海 phd thesis 2011
这是一节关于研究生研一分布式设计课程中的课件,里面的内容是关于分布式事务的并发控制。
超高并发下如何对Mysql事务进行优化
本文提出了一个包含Redis的键值NoSQL数据库的事务模型,以使用户能够以ACID(原子性,一致性,隔离性和持久性)方式访问数据,该模型被生动地称为冲浪并发事务模型。 详细描述了体系结构,重要功能和实现原理。 还...
运行时,如果批处理或事务中某个操作违反约束,系统默认只回退到产生错误的语句。通过打开XACT_ABORT开关,可使系统自动回滚产生该错误的当前事务。
48 多个事务并发更新以及查询数据,为什么会有脏写和脏读的问题?l.pdf
J2EE事务并发控制策略总结 当前J2EE项目中,面临的一个共同问题就是如果控制事务的并发访问,虽然有些持久层框架已经为我们做了很多工作,但是理解原理,对于我们开发来说还是很有用处