5.1.4 理解事务隔离级别
问题
The ANSI SQL standard defines the standard transaction isolation levels in terms of which of these phenomena are permissible:
ANSI SQL分别针对以下可能出现的问题,定义了不同的标准事务隔离级别。
■Lost update—Two transactions both update a row and then the second transaction aborts, causing both changes to be lost. This occurs in systems that don’t implement any locking. The concurrent transactions aren’t isolated.
更新丢失-两个事务都同时更新一行数据,但是第二个事务却中途失败退出,导致对数据的两个修改都失效了。这是因为系统没有执行任何的锁操作,因此并发事务并没有被隔离开来。
■ Dirty read—One transaction reads changes made by another transaction that hasn’t yet been committed. This is very dangerous, because those changes might later be rolled back.
脏读取-一个事务开始读取了某行数据,但是另外一个事务已经更新了此数据但没有能够及时提交。这是相当危险的,因为很可能所有的操作都可能会被回滚。
■ Unrepeatable read—A transaction reads a row twice and reads different state each time. For example, another transaction may have written to the row, and committed, between the two reads.
不可重复读-一个事务对同一行数据重复读取两次,但是却得到了不同的结果。例如,在两次读取的中途,有另外一个事务对该行数据进行了修改,并提交。
■ Second lost updates problem—A special case of an unrepeatable read. Imagine that two concurrent transactions both read a row, one writes to it and commits, and then the second writes to it and commits. The changes made by the first writer are lost.
两次更新问题-无法重复读取的特例。有两个并发事务同时读取同一行数据,然后其中一个对它进行修改提交,而另一个也是修改提交。这就会造成第一次写操作失效。
■ Phantom read—A transaction executes a query twice, and the second result set includes rows that weren’t visible in the first result set. (It need not necessarily be exactly the same query.) This situation is caused by another transaction inserting new rows between the execution of the two queries.
读取幻影数据-事务在操作过程中进行两次查询,第二次查询的结果包含了第一次查询中未出现的数据(这里并不要求两次查询的sql语句相同)。这是因为在两次查询过程中有另外一个事务插入数据造成的。
事务隔离级别
The standard isolation levels are defined by the ANSI SQL standard but aren’t particular to SQL databases. JTA defines the same isolation levels, and you’ll use these levels to declare your desired transaction isolation later:
虽然ANSI SQL定义了标准的事务隔离级别,但是不听的SQL数据库的实现则是不同的。JTA定义了同样的事务隔离级别,你可以在按照你的要求使用它们:
■ Read uncommitted—Permits dirty reads but not lost updates. One transaction may not write to a row if another uncommitted transaction has already written to it. Any transaction may read any row, however. This isolation level may be implemented using exclusive write locks.
未授权读取-允许脏读取,但不允许更新丢失。如果一个事务已经开始写数据,则另外一个数据则不允许同时写。但允许其他事务读此行数据。该隔离级别可以通过“排他写锁”实现。
■ Read committed—Permits unrepeatable reads but not dirty reads. This may be achieved using momentary shared read locks and exclusive write locks. Reading transactions don’t block other transactions from accessing a row. However, an uncommitted writing transaction blocks all other transactions from accessing the row.
授权读取-允许不可重复读取,但不允许脏读取。这可以通过“瞬间共享读锁”和“排他写锁”实现。读取数据的事务允许其他事务继续访问该行数据,但是未提交的写事务将会禁止其他事务访问该行。
■ Repeatable read—Permits neither unrepeatable reads nor dirty reads. Phantom reads may occur. This may be achieved using shared read locks and exclusive write locks. Reading transactions block writing transactions (but not other reading transactions), and writing transactions block all other transactions.
可重复读取-禁止不可重复读取和脏读取,但是有时可能出现幻影数据。这可以通过“共享读锁”和“排他写锁”实现。读取数据的事务将会禁止写事务(但允许读事务),写事务则禁止任何其他事务。
■ Serializable—Provides the strictest transaction isolation. It emulates serial transaction execution, as if transactions had been executed one after another, serially, rather than concurrently. Serializability may not be implemented using only row-level locks; there must be another mechanism that prevents a newly inserted row from becoming visible to a transaction that has already executed a query that would return the row.
序列化-提供严格的事务隔离。它要求事务序列化执行,事务只能一个接着一个的执行,但不能并发执行。如果仅仅通过“行级锁”是无法实现事务序列化的,必须通过其他机制保证新插入的数据不会被刚执行查询操作的事务访问到。
5.1.5 选择正确的事务隔离级别
First, you eliminate the read uncommitted isolation level. It’s extremely dangerous to use one transaction’s uncommitted changes in a different transaction. The rollback or failure of one transaction would affect other concurrent transactions. Rollback of the first transaction could bring other transactions down with it, or perhaps even cause them to leave the database in an inconsistent state. It’s possible that changes made by a transaction that ends up being rolled back could be committed anyway, since they could be read and then propagated by another transaction that is successful!
首先,必须排除“未授权读取”,因为在多个事务之间使用它将会是非常危险的。事务的回滚操作或失败将会影响到其他并发事务。第一个事务的回滚将会完全将其他事务的操作清除,甚至使数据库处在一个不一致的状态。很可能一个已回滚为结束的事务对数据的修改最后却修改提交了,因为“未授权读取”允许其他事务读取数据,最后整个错误状态在其他事务之间传播开来。
Second, most applications don’t need serializable isolation (phantom reads aren’t usually a problem), and this isolation level tends to scale poorly. Few existing applications use serializable isolation in production; rather, they use pessimistic locks, which effectively forces a serialized execution of operations in certain situations.
其次,绝大部分应用都无需使用“序列化”隔离(一般来说,读取幻影数据并不是一个问题),此隔离级别也难以测量。目前使用序列化隔离的应用中,一般都使用的悲观锁,这样强行使得所有事务都序列化执行。
This leaves you a choice between read committed and repeatable read. Let’s first consider repeatable read. This isolation level eliminates the possibility that one transaction could overwrite changes made by another concurrent transaction (the second lost updates problem) if all data access is performed in a single atomic database transaction. This is an important issue, but using repeatable read isn’t the only way to resolve it.
剩下的也就是在“授权读取”和“可重复读取”之间选择了。我们先考虑可重复读取。如果所有的数据访问都是在统一的原子数据库事务中,此隔离级别将消除一个事务在另外一个并发事务过程中覆盖数据的可能性(第二个事务更新丢失问题)。这是一个非常重要的问题,但是使用可重复读取并不是解决问题的唯一途径。
Let’s assume you’re using versioned data, something that Hibernate can do for you automatically. The combination of the (mandatory) Hibernate first-level session cache and versioning already gives you most of the features of repeatable read isolation. In particular, versioning prevents the second lost update problem, and the first-level session cache ensures that the state of the persistent instances loaded by one transaction is isolated from changes made by other transactions. So, read committed isolation for all database transactions would be acceptable if you use versioned data.
让我们假设你使用了“版本数据”,hibernate会为你自动使用版本数据。Hibernate的一级session缓存和版本数据已经喂你提供了“可重复读取隔离” 绝大部分的特性。特别是,版本数据可以防止二次更新丢失的问题,一级session缓存可以保证持久载入数据的状态与其他事务对数据的修改隔离开来,因此如果你使用对所有的数据库事务采用授权读取隔离和版本数据是行得通的。
Repeatable read provides a bit more reproducibility for query result sets (only for the duration of the database transaction), but since phantom reads are still possible, there isn’t much value in that. (It’s also not common for web applications to query the same table twice in a single database transaction.)
“可重复读取”为数据库查询提供了更好的效率(仅对那些长时间的数据库事务),但是由于幻影读取依然存在,因此并没必要使用它(对于Web应用来说,一般也很少在一个数据库事务中对同一个表查询两次)。
You also have to consider the (optional) second-level Hibernate cache. It can provide the same transaction isolation as the underlying database transaction, but it might even weaken isolation. If you’re heavily using a cache concurrency strategy for the second-level cache that doesn’t preserve repeatable read semantics (for example, the read-write and especially the nonstrict-read-write strategies, both discussed later in this chapter), the choice for a default isolation level is easy: You can’t achieve repeatable read anyway, so there’s no point slowing down the database. On the other hand, you might not be using second-level caching for critical classes, or you might be using a fully transactional cache that provides repeatable read isolation. Should you use repeatable read in this case? You can if you like, but it’s probably not worth the performance cost.
你也可以同时考虑选择使用hibernate的二级缓存,它可以如同底层的数据库事务一样提供相同的事务隔离,但是它可能弱化隔离。假如你大量在二级缓存使用缓存并发策略,它并不提供重复读取语义(例如,后面章节中将要讨论的读写,特别是非严格读写),很容易可以选择默认的隔离级别:因为你无论如何都无法实现“可重复读取”,因此就更无必要拖慢数据库了。另一方面,你可能对关键类不采用二级缓存,或者采用一个完全的事务缓存,提供“可重复读取隔离”。那么在你的业务中需要使用到“可重复读取”吗?如果你喜欢,你当然可以那样做,但更多的时候并没有必要花费这个代价。
5.1.6 hibernate中设置隔离级别l
Hibernate will then set this isolation level on every JDBC connection obtained from a connection pool before starting a transaction. The sensible values for this option are as follows (you can also find them as constants in java.sql.Connection):
■ 1—Read uncommitted isolation
■ 2—Read committed isolation
■ 4—Repeatable read isolation
■ 8—Serializable isolation
Note that Hibernate never changes the isolation level of connections obtained from a datasource provided by the application server in a managed environment. You may change the default isolation using the configuration of your application server.
Hibernte将会在启动事务之前,为从数据库链接池中取得的每一个connection设置隔离级别。其常量值如下(你可以在java.sql.connection中常量中找到):
1-未授权读取隔离
2-授权读取隔离
4-可重复读取隔离
8-序列化隔离
注意:hibernate不会对从应用服务器管理的数据源取得的数据库连接的隔离级别进行设置。你需要在你的应用服务器进行配置,修改其默认的隔离级别。
5.1.7 使用悲观锁
Locking is a mechanism that prevents concurrent access to a particular item of data. When one transaction holds a lock on an item, no concurrent transaction can read and/or modify this item. A lock might be just a momentary lock, held while the item is being read, or it might be held until the completion of the transaction. A pessimistic lock is a lock that is acquired when an item of data is read and that is held until transaction completion.
锁是用来防止并发访问同一数据的机制。当一个事务对一个数据拥有了锁,那么其他并发事务则无法对它进行读或者写。这里的锁可以是瞬时的锁,仅当在数据被读取时存在,也可以时长期的,直到整个事务结束时释放。悲观锁是指:它在数据被读取时获取,在事务结束时释放。
In read-committed mode (our preferred transaction isolation level), the database never acquires pessimistic locks unless explicitly requested by the application. Usually, pessimistic locks aren’t the most scalable approach to concurrency. However, in certain special circumstances, they may be used to prevent database-level deadlocks, which result in transaction failure. Some databases (Oracle and PostgreSQL, for example) provide the SQL SELECT...FOR UPDATE syntax to allow the use of explicit pessimistic locks. You can check the Hibernate Dialects to find out if your database supports this feature. If your database isn’t supported, Hibernate will always execute a normal SELECT without the FOR UPDATE clause.
在“授权读取”模式下(我们推荐的事务隔离级别),数据库将永不使用悲观锁,除非应用程序特别的显式生气。通常,悲观锁是不适合大规模并发操作的。但是在某些特殊场合下,需要使用悲观锁,以防止那些会引起整个事务失败的数据库死锁的发生。有些数据库(例如Oracle、PostgreSQL)可以提供“sql select … for update”这样的语法,让用户显式的使用悲观锁。你 可以去查看hibernate的dialect的文档,确认你的数据库是否支持此特性。如果你的数据库不支持,那么hibernate就会仅仅执行普通的select语句,而不带有for update字段。
The Hibernate LockMode class lets you request a pessimistic lock on a particular item. In addition, you can use the LockMode to force Hibernate to bypass the cache layer or to execute a simple version check. You’ll see the benefit of these operations when we discuss versioning and caching.
Hibernate的lockmode类允许你为某个数据申请悲观锁,此外,你可以使用lockmode强制hibernate穿越其缓存层、或者执行简单的版本校验。从我们对版本数据和缓存的讨论中了解这些操作的作用。
Let’s see how to use LockMode. If you have a transaction that looks like this
Transaction tx = session.beginTransaction();
Category cat = (Category) session.get(Category.class, catId);
cat.setName("New Name");
tx.commit();
then you can obtain a pessimistic lock as follows:
Transaction tx = session.beginTransaction();
Category cat =
(Category) session.get(Category.class, catId, LockMode.UPGRADE);
cat.setName("New Name");
tx.commit();
With this mode, Hibernate will load the Category using a SELECT...FOR UPDATE, thus locking the retrieved rows in the database until they’re released when the transaction ends.
让我们先看看如何使用lockmode。如果你有一个事务是这样的:
Transaction tx = session.beginTransaction();
Category cat = (Category) session.get(Category.class, catId);
cat.setName("New Name");
tx.commit();
那么你可以通过如下方法,获得悲观锁。
Transaction tx = session.beginTransaction();
Category cat =
(Category) session.get(Category.class, catId, LockMode.UPGRADE);
cat.setName("New Name");
tx.commit();
使用lockmode.UPGRADE,hibernate将会在载入Category时使用“select… for update”并锁定数据,然后直到事务结束时,才会释放锁。
Hibernate defines several lock modes:
■ LockMode.NONE—Don’t go to the database unless the object isn’t in either cache.
■ LockMode.READ—Bypass both levels of the cache, and perform a version check to verify that the object in memory is the same version that currently exists in the database.
■ LockMode.UPDGRADE—Bypass both levels of the cache, do a version check (if applicable), and obtain a database-level pessimistic upgrade lock, if that is supported.
■ LockMode.UPDGRADE_NOWAIT—The same as UPGRADE, but use a SELECT...FOR UPDATE NOWAIT on Oracle. This disables waiting for concurrent lock releases, thus throwing a locking exception immediately if the lock can’t be obtained.
■ LockMode.WRITE—Is obtained automatically when Hibernate has written to a row in the current transaction (this is an internal mode; you can’t specify it explicitly).
Hibernate定义了以下几种lockmode:
LockMode.NONE-不访问数据库,除非该对象不在缓存中。
LockMode.READ-穿越两层缓存,进行版本校验,以保证内存中的对象和数据库中的版本相同。
LockMode.UPGRADE-穿越两次缓存,进行数据版本校验,如果可以,则对数据库级别加上悲观更新锁。
LockMode.UPGRADE_NOWAIT-和UPGRADE相同,但是对Oracle使用select … for update no wait语句。如果当前数据被锁,则无需等待,直接抛出异常。
LockMode.WRITE-当hibernate在当前事务中写一行数据时,会自动使用它(这是内部模式,你不该显式的去使用)。
By default, load() and get() use LockMode.NONE. LockMode.READ is most useful with
Session.lock() and a detached object. For example:
Item item = ... ;
Bid bid = new Bid();
item.addBid(bid);
...
Transaction tx = session.beginTransaction();
session.lock(item, LockMode.READ);
tx.commit();
This code performs a version check on the detached Item instance to verify that the database row wasn’t updated by another transaction since it was retrieved, before saving the new Bid by cascade (assuming that the association from Item to Bid has cascading enabled).
默认情况下,load()和get()使用LockMode.NONE,而LockMode.READ则更多的被session.lock和分离对象使用。例如:
Item item = ... ;
Bid bid = new Bid();
item.addBid(bid);
...
Transaction tx = session.beginTransaction();
session.lock(item, LockMode.READ);
tx.commit();
这里的代码在对新的bid级联保存之前(假设在配置中允许级联),对分离的Item实例进行了版本检查,以校验数据库的数据从上次获得以来未被其他事务更新。
By specifying an explicit LockMode other than LockMode.NONE, you force Hibernate to bypass both levels of the cache and go all the way to the database. We think that most of the time caching is more useful than pessimistic locking, so we don’t use an explicit LockMode unless we really need it. Our advice is that if you have a professional DBA on your project, let the DBA decide which transactions require pessimistic locking once the application is up and running. This decision should depend on subtle details of the interactions between different transactions and can’t be guessed up front.
通过显式的声明LockMode,而不是采用默认的LockMode.NONE,你可以强迫hibernate穿越两层缓存,然后像直接访问数据库那样行事。但是我们认为,在绝大部分情况下,缓存要比悲观锁有用的多,因此除非真的需要,我们并不建议显式使用LockMode。我们的建议是,如果你的项目有一个专职的DBA,那么让DBA在系统允许时决定哪个事务该采用悲观锁。这完全依赖于多个事务之间的底层细节,而不该在简单的前台猜测。