1、JDBC事务管理JDBC事务管理一、 什么是事务 1.1. 概念事务(TRANSACTION)是作为单个逻辑工作单元执行的一系列操作。这些操作作为一个整体一起向系统提交,要么都执行、要么都不执行 。事务是一个不可分割的工作逻辑单元。事务是现代数据库理论中的核心概念之一。如果一组处理步骤或者全部发生或者一步也不执行,我们称该组处理步骤为一个事务。当所有的步骤像一个操作一样被完整地执行,我们称该事务被提交。由于其中的一部分或多步执行失败,导致没有步骤被提交,则事务必须回滚到最初的系统状态。 1.2. ACID原则事务必须服从ISO/IEC所制定的ACID原则。ACID是原子性(atomicity)
2、、一致性(consistency)、隔离性(isolation)和持久性(durability)的缩写。 原子性(Atomicity):事务是一个完整的操作。事务的各步操作是不可分的(原子的);要么都执行,要么都不执行。 一致性(Consistency):当事务完成时,数据必须处于一致状态 。 隔离性(Isolation):对数据进行修改的所有并发事务是彼此隔离的,这表明事务必须是独立的,它不应以任何方式依赖于或影响其他事务 永久性(Durability):事务完成后,它对数据库的修改被永久保持,事务日志能够保持事务的永久性。事务的原子性表示事务执行过程中的任何失败都将导致事务所做的任何修改失
3、效。一致性表示当事务执行失败时,所有被该事务影响的数据都应该恢复到事务执行前的状态。隔离性表示并发事务是彼此隔离。持久性表示当系统或介质发生故障时,确保已提交事务的更新不能丢失。持久性通过数据库备份和恢复来保证。二、 数据库并发问题当多个用户并发访问数据库中相同的数据时,可能会出现并发问题。如果没有锁定且多个用户同时访问一个数据库,则当他们的事务同时使用相同的数据时可能会发生问题。并发问题包括: 丢失或覆盖更新。 未确认的相关性(脏读)。 不一致的分析(非重复读)。 幻读。 2.1. 丢失更新当两个或多个事务选择同一行,然后基于最初选定的值更新该行时,会发生丢失更新问题。每个事务都不知道其他事
4、务的存在。最后的更新将重写由其他事务所做的更新,这将导致数据丢失。例如,两个编辑人员制作了同一文件的电子复本。每个编辑人员独立地更改其复本,然后保存更改后的复本,这样就覆盖了原始文件。最后保存其更改复本的编辑人员覆盖了第一个编辑人员所做的更改。如果在第一个编辑人员完成之后第二个编辑人员才能进行更改,则能避免该问题。2.2. 未确认的相关性(脏读)当第二个事务选择其他事务正在更新的行时,会发生未确认的相关性问题。第二个事务正在读取的数据还没有确认并且可能由更新此行的事务所更改。例如,一个编辑人员正在更改电子文件。在更改过程中,另一个编辑人员复制了该文件(该复本包含到目前为止所做的全部更改)并将其
5、分发给预期的用户。此后,第一个编辑人员认为目前所做的更改是错误的,于是删除了所做的编辑并保存了文件。分发给用户的文件包含不再存在的编辑内容,并且这些编辑内容应认为从未存在过。如果在第一个编辑人员确定最终更改前所有人都不能读取更改的文件,则能避免该问题。2.3. 不一致的分析(非重复读)当第二个事务多次访问同一行而且每次读取不同的数据时,会发生不一致的分析问题。不一致的分析和未确认的相关性类似,因为其他事务也是正在更改第二个事务正在读取的数据。然而,在不一致的分析中,第二个事务读取的数据是由已进行了更改的事务提交的。而且,不一致的分析涉及多次(两次或更多)读取同一行,而且每次信息都由其他事务更改
6、;因而该行被非重复读取。例如,一个编辑人员两次读取同一文件,但在两次读取之间,作者重写了该文件。当编辑人员第二次读取文件时,文件已更改。原始读取不可重复。如果只有在作者全部完成编写后编辑人员才能读取文件,则能避免该问题。2.4. 幻像读当对某行执行插入或删除操作,而该行属于某个事务正在读取的行的范围时,会发生幻像读问题。事务第一次读的行范围显示出其中一行已不复存在于第二次读或后续读中,因为该行已被其他事务删除。同样,由于其他事务的插入操作,事务的第二次或后续读显示有一行已不存在于原始读中。例如,一个编辑人员更改作者提交的文件,但当生产部门将其更改内容合并到该文件的主复本时,发现作者已将未编辑的
7、新材料添加到该文件中。如果在编辑人员和生产部门完成对原始文件的处理之前,所有人都不能将新材料添加到文件中,则能避免该问题。三、 并发问题的解决方案当锁定用作并发控制机制时,他能解决并发问题。这使所有事务得以在彼此完全隔离的环境中运行,不过所有时候都能有多个正在运行的事务。可串行性是通过运行一组并发事务达到的数据库状态,等同于这组事务按某种顺序连续执行时所达到的数据库状态。 3.1. sql-92 隔离级别尽管可串行性对于事务确保数据库中的数据在所有时间内的正确性相当重要,然而许多事务并不总是需求完全的隔离。例如,多个作者工作于同一本书的不同章节。新章节能在任意时候提交到项目中。不过,对于已编辑
8、过的章节,没有编辑人员的批准,作者不能对此章节进行所有更改。这样,尽管有未编辑的新章节,但编辑人员仍能确保在任意时间该书籍项目的正确性。编辑人员能查看以前编辑的章节及最近提交的章节。事务准备接受不一致数据的级别称为隔离级别。隔离级别是个事务必须和其他事务进行隔离的程度。较低的隔离级别能增加并发,但代价是降低数据的正确性。相反,较高的隔离级别能确保数据的正确性,但可能对并发产生负面影响。sql-92 定义了下列四种隔离级别: 未提交读(事务隔离的最低级别,仅可确保不读取物理损坏的数据)。 提交读(sql server 默认级别)。 可重复读。 可串行读(事务隔离的最高级别,事务之间完全隔离)。
9、如果事务在可串行读隔离级别上运行,则能确保所有并发重叠事务均是串行的。下面四种隔离级别允许不同类型的行为。如表所示:隔离级别脏读不可重复读幻读未提交读是是是提交读否是是可重复读否否是可串行读否否否事务必须运行于可重复读或更高的隔离级别以防止丢失更新。当两个事务检索相同的行,然后基于原检索的值对行进行更新时,会发生丢失更新。如果两个事务使用一个 update 语句更新行,并且不基于以前检索的值进行更新,则在默认的提交读隔离级别不会发生丢失更新。四、 乐观锁与悲观锁4.1. 悲观锁【Pessimistic Locking】 一、 概念:顾名思义就是采用一种悲观的态度来对待事务并发问题,我们认为系统
10、中的并发更新会非常频繁,并且事务失败了以后重来的开销很大,这样以来,我们就需要采用真正意义上的锁来进行实现。悲观锁的基本思想就是每次一个事务读取某一条记录后,就会把这条记录锁住,这样其它的事务要想更新,必须等以前的事务提交或者回滚解除锁。假如我们数据库事务的隔离级别设置为读取已提交或者更低,那么通过悲观锁,我们控制了不可重复读的问题,但是不能避免幻影读的问题,因为要想避免我们就需要设置数据库隔离级别为Serializable,而一般情况下我们都会采取读取已提交或者更低隔离级别,并配合乐观或者悲观锁来实现并发控制,所以幻影读问题是不能避免的,如果想避免幻影读问题,那么你只能依靠数据库的seria
11、lizable隔离级别(幸运的是幻影读问题一般情况下不严重)。悲观锁的实现,往往依靠数据库提供的锁机制(也只有数据库层提供的锁机制才能 真正保证数据访问的排他性,否则,即使在本系统中实现了加锁机制,也无法保证外部系统不会修改数据)。 二、 实现方式:在JDBC中使用悲观锁,需要使用select for update语句,假如我们系统中有一个Account的类,我们可以采用如下的方式来进行:Select * from Account where .(where condition). for update.当使用了for update语句后,每次在读取或者加载一条记录的时候,都会锁住被加载的记录
12、,那么当其他事务如果要更新或者是加载此条记录就会因为不能获得锁而阻塞,这样就避免了不可重复读以及脏读的问题,但是其他事务还是可以插入和删除记录,这样也许同一个事务中的两次读取会得到不同的结果集,但是这不是悲观锁锁造成的问题,这是我们数据库隔离级别所造成的问题。最后还需要注意的一点就是每个冲突的事务中,我们必须使用select for update 语句来进行数据库的访问,如果一些事务没有使用select for update语句,那么就会很容易造成错误,这也是采用JDBC进行悲观控制的缺点。4.2. 乐观锁【Optimistic Locking】一、 概念:乐观锁是在同一个数据库事务中我们常采
13、取的策略,因为它能使得我们的系统保持高的性能的情况下,提高很好的并发访问控制。乐观锁,顾名思义就是保持一种乐观的态度,我们认为系统中的事务并发更新不会很频繁,即使冲突了也没事,大不了重新再来一次。它的基本思想就是每次提交一个事务更新时,我们想看看要修改的东西从上次读取以后有没有被其它事务修改过,如果修改过,那么更新就会失败。(因此能够解决第二类丢失修改问题)。因为乐观锁其实并不会锁定任何记录,所以如果我们数据库的事务隔离级别设置为读取已提交或者更低的隔离界别,那么是不能避免不可重复读问题的(因为此时读事务不会阻塞其它事务),所以采用乐观锁的时候,系统应该要容许不可重复读问题的出现。需要注意的是
14、,乐观锁机制往往基于系统中的数据存储逻辑,因此也具备一定的局限性,由于乐观锁机制是在我们的系统中实现,来自外部系统的用户更新操作不受我们系统的控制,因此可能会造成脏数据被更新到数据库中。在 系统设计阶段,我们应该充分考虑到这些情况出现的可能性,并进行相应调整(如将乐观锁策略在数据库存储过程中实现,对外只开放基于此存储过程的数据更新途径,而不是将数据库表直接对外公开)。 二、 实现方式:大多是基于数据版本 ( Version )记录机制实现。何谓数据版本?即为数据增加一个版本标识,在基于数据库表的版本解决方案中,一般是通过为数据库表增加一个 “version” 字段来实现。 读取出数据时,将此版
15、本号一同读出,之后更新时,对此版本号加一。此时,将提 交数据的版本数据与数据库表对应记录的当前版本信息进行比对,如果提交的数据 版本号大于数据库表当前版本号,则予以更新,否则认为是过期数据。假如系统中有一个Account的实体类,我们在Account中多加一个version字段,那么我们JDBC Sql语句将如下写:Select a.version.from Account as a where (where condition.)Update Account set version = version+1.(another field) where version =?.(another c
16、ontidition)这样以来我们就可以通过更新结果的行数来进行判断,如果更新结果的行数为0,那么说明实体从加载以来已经被其它事务更改了,所以就抛出自定义的乐观锁定异常(或者也可以采用Spring封装的异常体系)。具体实例如下:.int rowsUpdated = statement.executeUpdate(sql);If(rowsUpdated= =0) throws new OptimisticLockingFailureException();.在使用JDBC API的情况下,我们需要在每个update语句中,都要进行版本字段的更新以及判断,因此如果稍不小心就会出现版本字段没有更新的
17、问题,相反当前的 ORM框架却为我们做好了一切,我们仅仅需要做的就是在每个实体中都增加version或者是Date字段。五、 JDBC事务管理5.1. 提交和回滚在JDBC的数据库操作中,一项事务是由一条或是多条表达式所组成的一个不可分割的工作单元。我们通过提交commit()或是回退rollback()来结束事务的操作。关于事务操作的方法都位于接口java.sql.Connection中。首先我们要注意,在JDBC中,事务操作默认是自动提交。也就是说,一条对数据库的更新表达式代表一项事务操作。操作成功后,系统将自动调用commit()来提交,否则将调用rollback()来回退。其次,在JD
18、BC中,可以通过调用setAutoCommit(false)来禁止自动提交。之后就可以把多个数据库操作的表达式作为一个事务,在操作完成后调用commit()来进行整体提交。倘若其中一个表达式操作失败,都不会执行到commit(),并且将产生响应的异常。此时就可以在异常捕获时调用rollback()进行回退。这样做可以保持多次更新操作后,相关数据的一致性。5.2. JDBC API支持的事务隔离级别 static int TRANSACTION_NONE = 0; 说明不支持事务。 static int TRANSACTION_READ_UNCOMMITTED = 1;说明一个事务在提交前其变化
19、对于其他事务来说是可见的。这样脏读、不可重复的读和虚读都是允许的。 static int TRANSACTION_READ_COMMITTED = 2;说明读取未提交的数据是不允许的。这个级别仍然允许不可重复的读和虚读产生。 static int TRANSACTION_REPEATABLE_READ = 4;说明事务保证能够再次读取相同的数据而不会失败,但虚读仍然会出现。 static int TRANSACTION_SERIALIZABLE = 8;是最高的事务级别,它防止脏读、不可重复的读和虚读。JDBC根据数据库提供的默认值来设置事务支持及其加锁,运行在TRANSACTION_SERI
20、ALIZABLE模式下的事务可以保证最高程度的数据完整性,但事务保护的级别越高,性能损失就越大。上述设置随着值的增加,其事务的独立性增加,更能有效地防止事务操作之间的冲突,同时也增加了加锁的开销,降低了用户之间访问数据库的并发性,程序的运行效率也会随之降低。因此得平衡程序运行效率和数据一致性之间的冲突。一般来说,对于只涉及到数据库的查询操作时,可以采用TRANSACTION_READ_UNCOMMITTED方式;对于数据查询远多于更新的操作,可以采用TRANSACTION_READ_COMMITTED方式;对于更新操作较多的,可以采用TRANSACTION_REPEATABLE_READ;在数
21、据一致性要求更高的场合再考虑最后一项,由于涉及到表加锁,因此会对程序运行效率产生较大的影响。假设我们现在有一个Connection对象con,那么设置事务级别的方法如下:con.setTransactionLevel(TRANSACTION_SERIALIZABLE) ; 你也可以使用getTransactionLevel()方法来获取当前事务的级别: con.getTransactionLevel(); 在默认情况下,JDBC驱动程序运行在自动提交模式下,即发送到数据库的所有命令运行在它们自己的事务中。这样做虽然方便,但付出的代价是程序运行时的开销比较大。我们可以利用批处理操作减小这种开销,
22、因为在一次批处理操作中可以执行多个数据库更新操作。但批处理操作要求事务不能处于自动提交模式下。为此,我们首先要禁用自动提交模式。 六、 JDBC批处理executeBatch()方法返回一个更新计数的数组,每个值对应于批处理操作中的一个命令。批处理操作可能会抛出一个类型为BatchUpdateException的异常,这个异常表明批处理操作中至少有一条命令失败了。int executeBatch() throws SQLException 将一批命令提交给数据库来执行,如果全部命令执行成功,则返回更新计数组成的数组。返回数组的 int 元素的排序对应于批中的命令,批中的命令根据被添加到批中的顺
23、序排序。方法 executeBatch 返回的数组中的元素可能为以下元素之一: 大于等于 0 的数 - 指示成功处理了命令,是给出执行命令所影响数据库中行数的更新计数 SUCCESS_NO_INFO 的值 - 指示成功执行了命令,但受影响的行数是未知的 如果批量更新中的命令之一无法正确执行,则此方法抛出 BatchUpdateException,并且 JDBC 驱动程序可能继续处理批处理中的剩余命令,也可能不执行。无论如何,驱动程序的行为必须与特定的 DBMS 一致,要么始终继续处理命令,要么永远不继续处理命令。如果驱动程序在某一次失败后继续进行处理,则 BatchUpdateExceptio
24、n.getUpdateCounts 方法返回的数组将包含的元素与批中存在的命令一样多,并且其中至少有一个元素将为: EXECUTE_FAILED 的值 - 指示未能成功执行命令,仅当命令失败后驱动程序继续处理命令时出现 在 Java 2 SDK, Standard Edition, 1.3 版中已经修改了可能的实现和返回值,以适应抛出 BatchUpdateException 对象后在批量更新中继续处理命令的选项。 返回:包含批中每个命令的一个元素的更新计数所组成的数组。数组的元素根据将命令添加到批中的顺序排序。 抛出:SQLException - 如果发生数据库访问错误,在已关闭的 Stat
25、ement 上调用此方法,或者驱动程序不支持批量语句。如果未能正确执行发送到数据库的命令之一或者尝试返回结果集合,则抛出 BatchUpdateException(SQLException 的子类)。 其中整形常量的值分别为: SUCCESS_NO_INFO -2 EXECUTE_FAILED -3示例:try Connection con = null; con.setAutoCommit(false); PreparedStatement prepStmt = con .prepareStatement(UPDATE DEPT SET MGRNO=? WHERE DEPTNO=?); pr
26、epStmt.setString(1, 1); prepStmt.setString(2, 2); prepStmt.addBatch(); prepStmt.setString(1, 2); prepStmt.setString(2, 3); prepStmt.addBatch(); int numUpdates = prepStmt.executeBatch(); for (int i = 0; i numUpdates.length; i+) if (numUpdatesi = -2) System.out.println(Execution + i + : unknown number of rows updated); else System.out.println(Execution + i + successful: + numUpdatesi + rows updated); mit(); catch (BatchUpdateException b) catch (SQLException e) e.printStackTrace(); 七、 手工实现JDBC事务管理
copyright@ 2008-2022 冰豆网网站版权所有
经营许可证编号:鄂ICP备2022015515号-1