最近工作非常郁闷,天天被领导盯着。主要是系统近来死锁发生在频率很高。最终,经过大家的共同努力,我们成功的定位并解决了问题,所以把过程中学习的知识与经验分享一下
问题背景
系统中有一个账户模块,负责管理和维护会员的各种资金及明细,对外的功能涉及资金的增加与扣减等。通过监控系统发现,当外围系统并发访问和调用同一个会员的账户功能接口的时候,就会发生死锁和响应超时的情况,所以问题的分析还得从事务的并发说起。首先,我们先回顾一下事务。
事务是神马
所谓事务,它是一个操作序列,这些操作要么都执行,要么都不执行,它是一个不可分割的原子单位。例如,银行转帐工作:从一个帐号扣款并使另一个帐号增款,这两个操作要么都执行,要么都不执行。
事务的特性
- 原子性。一个事务是一个不可分割的工作单位,事务中包括的诸操作要么都做,要么都不做。
- 一致性。事务必须是使数据库从一个一致性状态变到另一个一致性状态。一致性与原子性是密切相关的。
- 隔离性。一个事务的执行不能被其他事务干扰。即一个事务内部的操作及使用的数据对并发的其他事务是隔离的,并发执行的各个事务之间不能互相干扰。
- 持久性。持续性也称永久性,指一个事务一旦提交,它对数据库中数据的改变就应该是永久性的。接下来的其他操作或故障不应该对其有任何影响。
四个特性中,与我们问题相关的应该就是事务的隔离性了,要理解隔离性,就不得不说说事务的并发了。
事务的并发及带来的影响
事务并发通俗点讲就是多个事务同时执行,并发的访问或更新数据库中相同的数据。事务并发一般需要相应的隔离措施,否则就会出现各种问题。
事务的并发会造成哪些问题?
- 丢失更新。第一种情况:当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,本文主要从分布式的原因,事务特性,和解决方案中深入理解了分布式事务,希望对您的学习有所帮助。 分布式事务就是指事务的参与者、支持事务的服务器、资源服务器以及事务管理器分别位于不同的...
事务和并发控制是数据库管理中的核心概念,特别是在使用ORM框架如Hibernate时,理解它们的工作原理至关重要。本文将深入探讨Hibernate中的事务管理和并发控制。 首先,事务是数据库操作的基本单位,它保证了数据的...
事务并发带来的坏处,以及通过设置事务隔离级别来处理
了解事务并发在开发中的应用和引起并发的原因
本篇将深入探讨LINQ to SQL中的开放式并发控制和事务管理,这是数据库应用开发中至关重要的两个概念。 ### 开放式并发控制 并发控制是多用户环境下数据库系统必须解决的关键问题,以确保数据的一致性和完整性。在...
在数据库管理中,事务管理是至关重要的一个环节,它涵盖了并发控制和恢复机制,确保数据在多用户同时访问时的完整性和一致性。本章主要讨论的是如何在数据库系统中实现有效的事务管理和处理并发操作的问题。 首先,...
分布式事务并发控制是分布式系统中确保多个参与节点间事务一致性的关键技术。在分布式系统中,事务可能需要同时在多个服务器上执行,这就要求系统能够在这些服务器之间进行协调,以保证事务的ACID属性(原子性、一致...
本文将深入探讨Hibernate中的事务和并发控制,这对于开发高效、稳定的数据库应用至关重要。 首先,我们来理解Hibernate中的事务管理。在数据库操作中,事务是保证数据一致性的重要手段。一个事务包含了一组数据库...
该PPT讲解,事务,嵌套事务,保存点,乐观并发控制及时间戳
标题和描述中提到的核心知识点是关于数据库事务并发控制中可能出现的问题,特别是脏写(Dirty Write)和脏读(Dirty Read)两种现象。这些知识点主要涉及数据库事务的隔离级别,事务的ACID属性(原子性、一致性、...
数据库处理的经典资料: 叫你处理事务 如何并发处理 瞧瞧吧 机不可失哟
数据库并发事务处理是确保数据一致性和完整性的关键。通过理解事务的ACID特性、隔离级别和并发控制技术,开发者可以更有效地设计和优化数据库系统。提供的代码示例展示了如何在实际数据库操作中应用这些概念,帮助...
### 数据库事务并发与事务加锁 #### 一、数据库事务基本概念 在数据库领域,事务(Transaction)是由一系列操作组成的逻辑工作单元。事务具备四个关键特性:原子性(Atomicity)、一致性(Consistency)、隔离性...
**标题解析:**“Hibernate part 14:查询及数据库并发事务” 这个标题指出我们要讨论的是Hibernate框架在处理查询和数据库并发事务方面的内容。Hibernate是一个流行的Java对象关系映射(ORM)工具,它允许开发者...
本视频教程“SQL Server事务的控制与并发处理”深入讲解了如何有效地管理和控制事务,以及如何解决并发操作中可能出现的问题。 首先,让我们了解一下事务的基本概念。事务具有四个特性,通常被称为ACID属性:原子性...
本教程主要聚焦于Hibernate中的事务并发处理,包括悲观锁和乐观锁两种策略,这对于我们理解如何在高并发环境中确保数据的一致性和完整性至关重要。 首先,事务是数据库操作的基本单元,它确保一组操作要么全部成功...
事务及其并发控制实验1 在数据库系统中,事务(Transaction)是一种机制,用于确保数据库的一致性和可靠性。事务控制是指控制事务执行的过程,以确保事务的正确执行和回退。下面是事务及其并发控制实验1的知识点...
在处理复杂的业务逻辑时,事务管理、并发控制以及缓存策略是提升系统性能和数据一致性的重要手段。下面将详细讲解这些知识点。 **1. NHibernate事务管理** 在数据库操作中,事务是确保数据完整性和一致性的基础。...
在数据库系统中,事务管理、恢复以及并发控制是至关重要的技术,它们确保了数据库的稳定性和数据的一致性。事务是数据库操作的基本单元,具有原子性、一致性、隔离性和持续性(ACID特性)。原子性意味着事务中的所有...