什么是事务及事务并发的可能问题与其解决方案

时间:2020-09-25 22:11:31 类型:数据库
字号:    

  我们在实际业务场景中,经常会遇到数据频繁修改读取的问题。在同一时刻,不同的业务逻辑对同一个表数据进行修改,

  这种冲突很可能造成数据不可挽回的错乱,所以我们需要用事务来对数据进行管理。

   事务的概念

  事务必须服从ACID原则。ACID指的是原子性(atomicity)、一致性(consistency)、隔离性(isolation)

  和持久性(durability)。通俗理解,事务其实就是一系列指令的集合。

  原子性:操作这些指令时,要么全部执行成功,要么全部不执行。只要其中一个指令执行失败,

  所有的指令都执行失败,数据进行回滚,回到执行指令前的数据状态。

  一致性:事务的执行使数据从一个状态转换为另一个状态,但是对于整个数据的完整性保持稳定。

  隔离性:在该事务执行的过程中,无论发生的任何数据的改变都应该只存在于该事务之中,

  对外界不存在任何影响。只有在事务确定正确提交之后,才会显示该事务对数据的改变。

  其他事务才能获取到这些改变后的数据。

  持久性:当事务正确完成后,它对于数据的改变是永久性的。


一、多个事务并发时可能遇到的问题

1. 第一类丢失更新(lost update): 在完全未隔离事务的情况下,两个事物更新同一条数据资源,某一事物异常终止,回滚造成第一个完成的更新也同时丢失。

2. 脏读(dirty read):如果第二个事务查询到第一个事务还未提交的更新数据,形成脏读。

3. 虚读(phantom read):一个事务执行两次查询,第二次查询比第一次多出或少一些数据,造成两次结果不一致。只是另一个事务在这两次查询中间插入或者删除了数据造成的。

4. 不可重复读(unrepeated read):一个事务两次读取同一行数据,结果得到不同状态结果,如中间正好另一个事务更新了该数据,两次结果相异,不可信任。

5. 第二类丢失更新(second lost updates):是不可重复读的特殊情况,如果两个事务都读取同一行,然后两个都进行写操作,并提交,第一个事务所做的改变就会丢失。

两类更新丢失的举例:

时间取款事务A转账事务B
T1开始事务
T2
开始事务
T3读余额为1000
T4取出100,余额改为900-
T5
读余额为1000
T6
汇入100,余额改为1100
T7
提交事务,余额定为1100
T8撤销事务,余额改回1000-
T9最终余额1000,更新丢失-

写操作没加“持续-X锁”,没能阻止事务B写,发生了回滚覆盖。

时间转账事务A取款事务B
T1开始事务
T2
开始事务
T3读余额为1000
T4
读余额为1000
T5
取出100,余额改为900
T6
提交事务,余额定为900
T7汇入100,余额改为1100-
T8提交事务,余额定为1100-
T9最终余额1100,更新丢失-

写操作加了“持续-X锁”,读操作加了“临时-S锁”,没能阻止事务B写,发生了提交覆盖。


事务隔离级别:

为了解决数据库事务并发运行时的各种问题数据库系统提供四种事务隔离级别:
1. Serializable 串行化
2. Repeatable Read 可重复读
3. Read Commited 可读已提交
4. Read Uncommited 可读未提交

并发控制:

1.数据库系统采用不同的锁类型来实现以上四种隔离级别,具体的实现过程对用户是透明的。用户应该关心的是如何选择合适的隔离级别。
2.对于多数应用程序,可以优先考虑把数据库系统的隔离级别设为Read Committed,它能够避免脏读,而且具有较好的并发性能。
3.每个数据库连接都有一个全局变量@@tx_isolation,表示当前的事务隔离级别。JDBC数据库连接使用数据库系统默认的隔离级别。
4.在Hibernate的配置文件中可以显示地设置隔离级别。每一种隔离级别对应着一个正整数。
5.需要注意的是,在受管理环境中,如果Hibernate使用的数据库连接来自于应用服务器提供的数据源,Hibernate不会改变这些连接的事务隔离级别。在这种情况下,应该通过修改应用服务器的数据源配置来修改隔离级别。

6.当数据库系统采用Red Committed隔离级别时,会导致不可重复读和第二类丢失更新的并发问题,在可能出现这种问题的场合。可以在应用程序中采用悲观锁或乐观锁来避免这类问题。

悲观锁
  正如其名,它指的是对数据被外界(包括本系统当前的其他事务,以及来自外部系统的事务处理)修改持保守态度,因此,在整个数据处理过程中,将数据处于锁定状态。悲观锁的实现,往往依靠数据库提供的锁机制(也只有数据库层提供的锁机制才能真正保证数据访问的排他性,否则,即使在本系统中实现了加锁机制,也无法保证外部系统不会修改数据)。
  一个典型的依赖数据库的悲观锁调用:select * from account where name=”Erica” for update这条 sql 语句锁定了 account 表中所有符合检索条件( name=”Erica” )的记录。本次事务提交之前(事务提交时会释放事务过程中的锁),外界无法修改这些记录。悲观锁,也是基于数据库的锁机制实现。
  在Hibernate使用悲观锁十分容易,但实际应用中悲观锁是很少被使用的,因为它每次发送的SQL语句都会加上"for update"用于告诉数据库锁定相关数据,大大限制了并发性:
乐观锁
  相对悲观锁而言,乐观锁机制采取了更加宽松的加锁机制。悲观锁大多数情况下依靠数据库的锁机制实现,以保证操作最大程度的独占性。但随之而来的就是数据库性能的大量开销,特别是对长事务而言,这样的开销往往无法承受。乐观锁机制在一定程度上解决了这个问题。乐观锁,大多是基于数据版本(Version)记录机制实现。何谓数据版本?即为数据增加一个版本标识,在基于数据库表的版本解决方案中,一般是通过为数据库表增加一个"version"字段来实现。
  乐观锁的工作原理:读取出数据时,将此版本号一同读出,之后更新时,对此版本号加一。此时,将提交数据的版本数据与数据库表对应记录的当前版本信息进行比对,如果提交的数据版本号大于数据库表当前版本号,则予以更新,否则认为是过期数据。
Hibernate为乐观锁提供了3中实现:
1. 基于version
2. 基于timestamp
3. 为遗留项目添加添加乐观锁 Hibernate为乐观锁提供了3中实现


<