JE甚至在不使用交易时也为多线程应用程序提供了大量支持。 许多JE的句柄都是线程安全的,JE提供了一个灵活的锁定子系统,用于管理并发应用程序中的数据库。 此外,JE提供了用于检测和响应锁冲突的强大机制。 本章将探讨所有这些概念。
在继续之前,定义本章中将出现的一些术语非常有用:
- Thread of control 控制线程
指的是在您的应用程序中执行工作的线程。通常,在本书中该线程将执行JE操作。
- Locking 锁定
当一个控制线程获得对共享资源的访问权时,就会说它正在锁定该资源。请注意,JE支持独占锁和非独占锁。
- Free-threaded 自由线程
如果数据结构和对象可以跨控制线程共享,而不对应用程序进行任何显式锁定,则它们是自由线程的。某些书籍,库和编程语言可能将术语线程安全用于具有此特征的数据结构或对象。这两个词的意思相同。
- Blocked 阻塞
当一个线程无法获得锁定,因为某个其他线程已经在该对象上持有一个锁,则说锁定尝试被阻止。
- Deadlock 死锁
当两个或多个控制线程试图以这样的方式访问冲突资源时发生,因为没有任何线程可以再进一步进行。
例如,如果线程A被阻塞,等待线程B持有的资源,同时线程B被阻塞,等待线程A持有的资源,则两个线程都不能进行任何前进。在这种情况下,线程A和线程B被称为死锁。
- Lock Conflict 锁定冲突
在JE中,锁冲突只是意味着控制线程试图获得锁定,但在锁定超时期限到期之前无法获取锁定。这可能是因为发生了死锁,或者可能发生了死锁,因为另一个线程花费的时间太长而无法完成长时间运行的数据库操作。无论哪种方式,你都会像对待真正的死锁一样对锁定冲突做同样的事情。
4.1 Which JE Handles are Free-Threaded
以下描述了各个句柄在何种程度和条件下的自由线程。
- Environment 和 DPL EntityStore
这个类是自由线程的
- Database 和 DPL PrimaryIndex
这些类是自由线程的。
- SecondaryDatabase和DPL SecondaryIndex
这些类是自由线程的。
- Cursor 和DPL EntityCursor
如果游标是事务性游标,则只要应用程序序列化对句柄的访问,它就可以被多个控制线程使用。 如果游标不是事务游标,则根本不能在多个控制线程之间共享它。
- SecondaryCursor
同样的条件适用于Cursor句柄。
- Transaction
这个类是自由线程的。
注意
DPL(com.sleepycat.persist。*)中找到的以及上面未提到的所有其他类都是自由线程的。
绑定API(com.sleepycat.bind。*)中找到的所有类都是自由线程的。
4.2 Locks, Blocks, 和 Deadlocks
在继续描述JE提供给您的并发机制之前,了解锁定如何在并发应用程序中工作非常重要。 阻塞和死锁对您的应用程序具有重要的性能影响。 因此,本节提供了这些概念的基本描述,以及它们如何影响JE操作。
4.2.1 Locks 锁
当一个控制线程想要获取对象的访问权时,它会请求锁定该对象。 此锁定允许JE通过确保以下内容为您的应用程序提供事务隔离保证:
没有其他控制线程可以读取该对象(在独占锁的情况下)
没有其他控制线程可以修改该对象(在独占或非独占锁的情况下)
4.2.1.1 Lock Resources 锁资源
发生锁定时,概念上有三种资源在使用:
- The locker.
这是持有锁的东西。 在事务性应用程序中,锁定器是事务句柄。 对于非事务性操作,锁定器是当前线程。
- The lock.
这是锁定对象的实际数据结构。 在JE中,锁管理器中的锁定对象结构代表被锁定的对象。
The locked object.
您的应用程序实际想要锁定的东西。 在JE应用程序中,锁定的对象通常是数据库记录。
JE尚未对您可以使用的这些资源的最大数量设置限制。 相反,您只受应用程序可用内存量的限制。
下图显示了一个事务句柄Txn A,它在数据库记录002上持有一个锁。在此图中,Txn A是锁定器,锁定的对象是记录002.此操作中只使用一个锁。
4.2.1.2 Types of Locks 锁的类型
JE应用程序支持独占锁和非独占锁。当想要写入对象时,授予独占锁。因此,独占锁有时也称为写锁。
独占锁可防止任何其他锁定器在对象上获得任何类型的锁定。这通过确保在锁定器写入该对象之前没有其他锁定器可以观察或修改独占锁定的对象来提供隔离。
非独占锁被授予只读访问权限。因此,非排他锁有时也称为读锁。由于多个锁定器可以同时在同一对象上保持读锁定,因此读锁定有时也称为共享锁。
非锁定锁可防止任何其他锁定器在锁定器仍在读取对象时修改锁定对象。这就是事务游标能够实现可重复读取的方式;默认情况下,游标的事务对游标已检查的任何对象保持读锁定,直到提交或中止事务为止。
在下图中,Txn A和Txn B都在记录002上保持读锁定,而Txn C在记录003上保持写锁定:
4.2.1.3 Lock Lifetime 锁的生命周期
一个Lock 保持其锁定,直到它不再需要锁定为止。 这意味着:
- 一个事务保留在事务提交或中止之前获取的任何锁。
- 所有非事务操作都会保持锁定,直到操作完成为止。 对于光标操作,将保持锁定,直到光标移动到新位置或关闭。
4.2.1.4 Blocks
简单地说,一个控制线程在尝试获取锁定时被阻止,但该尝试被拒绝,因为某些其他控制线程持有一个冲突的锁定。 一旦被阻止,在获得请求的锁定或请求锁定的操作被放弃之前,控制线程暂时无法进行任何前进。
请注意,当我们谈论阻塞时,严格来说线程不是试图获取锁定的东西。 相反,线程中的某个对象(例如游标)正试图获取锁。 但是,一旦锁定器尝试获取锁定,整个控制线程必须暂停,直到锁定请求以某种方式解决。
例如,如果Txn A在记录002上保持写锁定(独占锁定),则如果Txn B尝试在该记录上获取读取或写入锁定,则阻止运行Txn B的控制线程:
但是,如果Txn A仅在记录002上保持读锁定(共享锁定),则只有那些尝试获取该记录上的写锁定的句柄才会阻塞。
4.2.1.4.1 Blocking and Application Performance 锁和应用程序性能
多线程应用程序通常比简单的单线程应用程序执行得更好,因为应用程序可以执行其工作负载的一部分(例如,更新数据库记录),同时等待其他一些冗长的操作完成(执行磁盘或网络I /例如,O)。如果使用提供多个CPU的硬件,则性能的提升尤为明显,因为线程可以同时运行。
也就是说,如果并发应用程序的控制线程看到大量的锁争用,则可以看到工作负载吞吐量降低。也就是说,如果线程在锁定请求上阻塞,则表示应用程序的性能损失。
再次考虑上一个阻塞的写锁定请求图。在该图中,Txn C无法获得其请求的写锁定,因为Txn A和Txn B都已在所请求的记录上保持读锁定。在这种情况下,运行Txn C的线程将暂停,直到Txn C获得其写锁定,或者请求锁定的操作被放弃为止。事实上,Txn C的线程暂时停止了所有前进进度,这表明您的应用程序会降低性能。
此外,在Txn C等待其写锁定时请求的任何读锁定也将阻塞,直到Txn C已获得并随后释放其写锁定为止。
4.2.1.4.2 Avoiding Blocks 避免锁
减少锁争用是调整并发JE应用程序性能的重要部分。具有多个控制线程获得独占(写)锁的应用程序容易出现争用问题。此外,随着您增加储物柜数量并增加锁定时间,您可以增加应用程序看到锁定争用的可能性。
在设计应用程序时,请尝试执行以下操作以减少锁争用:
- 减少应用程序保持锁定的时间长度。
较短的事务处理将导致更短的锁定生命周期,这反过来将有助于减少锁争用。
此外,默认情况下,事务游标会保留读锁,直到事务完成为止。因此,尽量减少打开事务游标的时间,或降低隔离级别 - 请参阅下文。
如果可能,在事务结束时访问访问量很大(读取或写入)的项目。这减少了事务锁定频繁使用的记录的时间。
减少应用程序的隔离保证。
通过减少隔离保证,可以减少锁定阻止另一个锁定的情况。尝试对读操作使用未提交的读操作,以防止读锁被写锁阻塞。
此外,对于游标,您可以使用2级(读取已提交)隔离,这会导致游标在读取记录后立即释放其读取锁定(而不是在事务结束之前保持其读取锁定)。
请注意,减少隔离保证会对您的应用程序产生不利影响。在决定减少隔离之前,请注意检查应用程序的隔离要求。有关隔离级别的信息,请参阅隔离。
- 考虑您的数据访问模式
根据应用程序的性质,这可能是您无法做任何事情的事情。但是,如果可以创建线程使它们仅在数据库的非重叠部分上运行,那么您可以减少锁争用,因为您的线程很少(如果曾经)阻塞彼此的锁。
4.2.1.5 Deadlocks 死锁
当两个或多个控制线程被阻塞时,会发生死锁,每个线程都在等待另一个线程持有的资源。 当发生这种情况时,除非一些外部代理采取行动打破僵局,否则线程不可能继续前进。
例如,如果Txn B被Txn B阻塞,同时Txn B被Txn A阻塞,那么包含Txn A和Txn B的控制线程就会死锁; 两个线程都不能进行任何前进,因为这两个线程都不会释放阻塞其他线程的锁。
当两个控制线程死锁时,唯一的解决方案是在两个线程外部有一个能够识别死锁的机制,并通知至少一个线程它处于死锁状态。 一旦得到通知,控制线程必须放弃尝试的操作以解决死锁。 JE能够在检测到死锁时通知您的应用程序。 (对于JE,此处理方式与JE应用程序可能遇到的任何锁定冲突的处理方式相同。)有关详细信息,请参阅管理死锁和其他锁定冲突。
请注意,当控件线程中的一个锁定器被阻塞等待另一个锁定器在该控件的同一个线程中持有的锁定时,该线程被称为self-deadlocked 自死锁。
请注意,在JE中,只有在同一个线程中使用两个或更多事务(锁定器)时才会发生自死锁。 非事务性使用不会发生自死,因为该线程是锁定器。 但是,即使每个线程只有一个锁定器,仍有可能在另一个控制线程中发生死锁(它不会是self-deadlocked自死锁),因此您仍然必须编写防止死锁的代码。
4.2.1.5.1 Deadlock Avoidance 避免死锁
您为避免锁争用所做的事情也有助于减少死锁(请参阅避免阻塞)。 除此之外,您还应该确保所有线程以与所有其他线程相同的顺序访问数据。 只要线程以相同的基本顺序锁定记录,就不会出现死锁(但线程仍然可以阻塞)。
请注意,如果您使用的是辅助数据库(索引),则读取和写入的锁定顺序不同。 因此,如果您正在编写并发应用程序并且正在使用辅助数据库,则应该会出现死锁。
4.3 JE Lock Management (JE锁管理)
要管理JE中的锁,您必须做两件事:
- 管理锁定超时。
- 检测并响应锁定冲突。 从概念上讲,这些都是死锁。 但是从编码的角度来看,如果锁定超时,你所做的事情与你遇到死锁时所做的事情之间没有区别。
实际上,在JE中,您无法根据抛出的异常来区分它们。
4.3.1 Managing JE Lock Timeouts 管理JE锁超时
与事务超时一样(请参阅配置事务子系统),JE允许您识别允许持有锁的最长时间。此值在执行死锁检测中起着重要作用,因为JE可以识别死锁的唯一方法是锁定是否超过其超时值。
但是,与事务超时不同,锁定超时是真正的计时器。仅当JE有理由检查其锁定表时才会识别事务超时;也就是说,当它试图获得锁定时。如果您的应用程序中没有发生此类活动,则事务可能会在其到期超时后存在很长时间。相反,锁定超时由JVM维护的计时器管理。一旦此计时器到期,您的应用程序将收到有关该事件的通知(有关更多信息,请参阅下一节有关死锁检测的部分)。
您可以逐个事务地为整个环境设置锁定超时。要在事务基础上进行设置,请使用Transaction.setLockTimeout()。要为整个环境设置它,请使用EnvironmentConfig.setLockTimeout()或使用je.properties文件中的je.lock.timeout参数。
您为锁定超时指定的值以微秒为单位。默认情况下使用500000。
请注意,更改此值可能会影响应用程序的性能。如果将其设置得太低,则锁可能会过期并被视为死锁,即使该线程实际上正在进行中。这将导致您的应用程序不必要地中止和重试事务,这最终会损害应用程序吞吐量。如果将其设置得太高,则在应用程序收到通知并且能够采取纠正措施之前,线程可能会死锁太久。同样,这会损害应用程序吞吐量。
请注意,对于您将拥有极长寿命锁的应用程序,您可能希望将此值设置为0.这样可以完全禁用锁定超时。请注意,禁用锁定超时可能很危险,因为您的应用程序永远不会被通知死锁。因此,或者,如果您的应用程序使用非常长寿命的锁,您可能希望将此值设置为非常大的超时(例如十分钟)。
4.3.1 Managing Deadlocks and other Lock Conflicts 管理死锁和其他锁冲突
死锁是锁定冲突的结果,在锁定超时之前,底层JE代码无法解决这种冲突。一般来说,我们认为这种情况是锁定冲突,因为没有办法判断锁是否由于真正的死锁而超时,或者由于长时间运行的操作只是在一段时间内锁定了很长时间而超时。
当在JE中发生锁定冲突时,使用LockConflictException异常向持有该锁定的控制线程通知该事件。请注意,此异常实际上是几个异常类的公共基类,可能能够为您提供更多关于实际问题的提示。但是,您对任何这些异常的响应可能都是相同的,因此最好的做法是捕获并管理LockConflictException。
抛出LockConflictException时,线程必须:
- 停止所有读写操作。
- 关闭所有打开的游标。
- 中止交易。
- (可选)重试该操作。如果您的应用程序重试因锁定冲突而中止的操作,则必须使用新事务进行新的尝试。
注意
如果线程遇到锁定冲突,则可能无法使用经历锁定冲突的事务句柄进行任何其他数据库调用。
// retry_count is a counter used to identify how many times
// we've retried this operation. To avoid the potential for
// endless looping, we won't retry more than MAX_DEADLOCK_RETRIES
// times.
// txn is a transaction handle.
// key and data are DatabaseEntry handles. Their usage is not shown here.
while (retry_count < MAX_DEADLOCK_RETRIES) {
try {
txn = myEnv.beginTransaction(null, null);
myDatabase.put(txn, key, data);
txn.commit();
return 0;
} catch (LockConflictException le) {
try {
// Abort the transaction and increment the
// retry counter
if (txn != null) {
txn.abort();
}
retry_count++;
if (retry_count >= MAX_DEADLOCK_RETRIES) {
System.err.println("Exceeded retry limit. Giving up.");
return -1;
}
} catch (DatabaseException ae) {
System.err.println("txn abort failed: " + ae.toString());
return -1;
}
} catch (DatabaseException e) {
// If we catch a generic DatabaseException instead of
// a LockConflictException, we simply abort and give
// up -- we don't retry the operation.
try {
// Abort the transaction.
if (txn != null) {
txn.abort();
}
} catch (DatabaseException ae) {
System.err.println("txn abort failed: " + ae.toString());
}
return -1;
}
}
4.4 Isolation 隔离性
隔离保证是事务保护的一个重要方面。事务确保您的事务处理的数据不会被其他事务更改。此外,在提交更改之前,事务之外的修改永远不会在该事务之外查看。
也就是说,存在不同程度的隔离,您可以根据应用的要求选择放松您的隔离保证。您可能想要这样做的主要原因是因为性能;您要求事务提供的隔离越多,应用程序必须执行的锁定越多。锁定越多,阻塞的可能性就越大,这会导致线程在等待锁定时暂停。因此,通过放宽隔离保证,您可以提高应用程序的吞吐量。当然,您是否真正看到任何改进取决于应用程序数据和事务的性质。
4.3.1 Supported Degrees of Isolation 支持的隔离度
隔离度 | ANSI术语 | Definition |
---|---|---|
1 | READ UNCOMMITTED | 未提交的读取意味着一个事务永远不会覆盖另一个事务的脏数据。 脏数据是事务已修改但尚未提交到基础数据存储的数据。 但是,未提交的读取允许事务查看由另一个事务弄脏的数据。 此外,事务可以读取由另一个事务弄脏的数据,但随后由该另一个事务中止。 在后一种情况下,读取事务可能正在读取数据库中从未真正存在的数据。 |
2 | READ COMMITTED | 提交读取隔离意味着观察到1级,但永远不会读取脏数据。 此外,此隔离级别可确保数据永远不会更改,只要它由光标解决,但数据可能会在读取光标关闭之前发生更改。 在事务的情况下,当前光标位置的数据不会改变,但是一旦光标移动,先前引用的数据就会改变。 这意味着读者在游标关闭之前释放读锁,因此,在事务完成之前释放读锁。 请注意,此级别的隔离会导致游标的操作方式与没有事务时的操作完全相同。 |
(undefined) | REPEATABLE READ | 观察到提交的读取,并且在T完成之前,事务T读取的数据将永远不会被另一个事务弄脏。 这意味着在事务完成之前,不会释放读取和写入锁定。 这是JE的默认隔离级别。 |
3 | SERIALIZABLE | 观察到承诺的读取,加上没有交易会看到幻影。 幻影是由于搜索而返回的记录,但是当先前使用相同的搜索条件时,同一事务不会看到这些记录。 |
默认情况下,JE事务和事务游标提供可重复的读隔离。 您可以选择通过将JE配置为使用未提交的读隔离来降低隔离级别。 有关更多信息,请参阅读取未提交的数据。 您还可以将JE配置为使用已提交的读隔离。 有关更多信息,请参阅已提交的读取。 最后,您可以配置事务和事务游标以使用可序列化隔离。
4.3.2 Reading Uncommitted Data 读未提交的数据
Berkeley DB允许您配置应用程序以读取已被修改但尚未由另一个事务提交的数据;也就是脏数据。执行此操作时,您可以通过允许应用程序不必阻止等待写锁定来获得性能优势。另一方面,应用程序正在读取的数据可能会在事务完成之前发生更改。
与事务一起使用时,未提交的读取意味着一个事务可以看到已修改但尚未由另一个事务提交的数据。与事务游标一起使用时,未提交的读取意味着任何数据库读取器都可以在游标的事务提交之前看到游标修改的数据。
因此,未提交的读取允许事务读取随后可能被另一个事务中止的数据。在这种情况下,读取事务将读取数据库中从未真正存在的数据。
要将应用程序配置为读取未提交的数据,请在创建事务或打开游标时指定要使用未提交的读取。为此,请在相关配置对象(TransactionConfig或CursorConfig)上使用setReadUncommitted()方法。
import com.sleepycat.je.Database;
import com.sleepycat.je.DatabaseConfig;
import com.sleepycat.je.DatabaseEntry;
import com.sleepycat.je.DatabaseException;
import com.sleepycat.je.Environment;
import com.sleepycat.je.EnvironmentConfig;
import com.sleepycat.je.Transaction;
import com.sleepycat.je.TransactionConfig;
import java.io.File;
...
Database myDatabase = null;
Environment myEnv = null;
try {
EnvironmentConfig myEnvConfig = new EnvironmentConfig();
myEnvConfig.setTransactional(true);
myEnv = new Environment(new File("/my/env/home"),
myEnvConfig);
// Open the database.
DatabaseConfig dbConfig = new DatabaseConfig();
dbConfig.setTransactional(true);
myDatabase = myEnv.openDatabase(null, "sampleDatabase", dbConfig);
TransactionConfig txnConfig = new TransactionConfig();
txnConfig.setReadUncommitted(true); // Use uncommitted reads
// for this transaction.
Transaction txn = myEnv.beginTransaction(null, txnConfig);
// From here, you perform your database reads and writes as normal,
// committing and aborting the transactions as is necessary, and
// testing for deadlock exceptions as normal (omitted for brevity).
...
如果你正在使用DPL
import com.sleepycat.je.DatabaseConfig;
import com.sleepycat.je.DatabaseEntry;
import com.sleepycat.je.DatabaseException;
import com.sleepycat.je.Environment;
import com.sleepycat.je.EnvironmentConfig;
import com.sleepycat.je.Transaction;
import com.sleepycat.je.TransactionConfig;
import com.sleepycat.persist.EntityStore;
import com.sleepycat.persist.StoreConfig;
import java.io.File;
...
myDatabase = null;
Environment myEnv = null;
try {
EnvironmentConfig myEnvConfig = new EnvironmentConfig();
myEnvConfig.setTransactional(true);
myEnv = new Environment(new File("/my/env/home"),
myEnvConfig);
// Open the store.
StoreConfig myStoreConfig = new StoreConfig();
myStoreConfig.setAllowCreate(true);
myStoreConfig.setTransactional(true);
myStore = new EntityStore(myEnv, "store_name", myStoreConfig);
TransactionConfig txnConfig = new TransactionConfig();
txnConfig.setReadUncommitted(true); // Use uncommitted reads
// for this transaction.
Transaction txn = myEnv.beginTransaction(null, txnConfig);
// From here, you perform your store reads and writes as normal,
// committing and aborting the transactions as is necessary, and
// testing for deadlock exceptions as normal (omitted for brevity).
...
您还可以通过指定LockMode.READ_UNCOMMITTED在逐个读取的基础上配置未提交的读隔离:
import com.sleepycat.je.Database;
import com.sleepycat.je.DatabaseEntry;
import com.sleepycat.je.Environment;
import com.sleepycat.je.LockMode;
import com.sleepycat.je.Transaction;
...
Database myDb = null;
Environment myEnv = null;
Transaction txn = null;
try {
// Environment and database open omitted
...
txn = myEnv.beginTransaction(null, null);
DatabaseEntry theKey =
new DatabaseEntry((new String("theKey")).getBytes("UTF-8"));
DatabaseEntry theData = new DatabaseEntry();
myDb.get(txn, theKey, theData, LockMode.READ_UNCOMMITTED);
} catch (Exception e) {
// Exception handling goes here
}
使用DPL
import com.sleepycat.je.Environment;
import com.sleepycat.je.LockMode;
import com.sleepycat.je.Transaction;
import com.sleepycat.persist.PrimaryIndex;
...
Environment myEnv = null;
Transaction txn = null;
try {
// Environment and store open omitted
...
txn = myEnv.beginTransaction(null, null);
AnEntityClass aec = aPrimaryIndex.get(txn, "pKeya",
LockMode.READ_UNCOMMITTED);
} catch (Exception e) {
// Exception handling goes here
}
4.3.3 Committed Reads 已提交读取
您可以配置事务,以便事务游标正在读取的数据是一致的,只要游标正在处理它。但是,一旦光标读完记录或对象,光标就会释放对该记录或对象的锁定。这意味着光标已读取和释放的数据可能会在光标的事务完成之前发生更改。
例如,假设您有两个事务,Ta和Tb。进一步假设Ta具有读取记录R的游标,但不修改它。通常,Tb将无法写入记录R,因为Ta将对其进行读锁定。但是,当您为已提交的读取配置事务时,只要读取光标不再寻址记录或对象,Tb就可以在Ta完成之前修改记录R.
在为此级别的隔离配置应用程序时,您可能会看到更好的性能吞吐量,因为事务占用的读取锁更少。当您拥有一个向单个方向读取和/或写入记录的游标时,读取已提交的隔离最有用,并且不必返回重新读取这些相同的记录。在这种情况下,您可以允许JE在读取锁定时释放它,而不是在事务的生命周期中保留它们。
要将应用程序配置为使用已提交的读取,请执行以下操作之一:
- 创建您的事务,以便它允许提交的读取。
您可以通过为TransactionConfig.setReadCommitted()指定true来执行此操作。 - 对CursorConfig.setReadCommitted()指定true。
例如,以下内容创建一个允许已提交读取的事务:
import com.sleepycat.je.Database;
import com.sleepycat.je.DatabaseConfig;
import com.sleepycat.je.DatabaseEntry;
import com.sleepycat.je.DatabaseException;
import com.sleepycat.je.Environment;
import com.sleepycat.je.EnvironmentConfig;
import com.sleepycat.je.Transaction;
import com.sleepycat.je.TransactionConfig;
import java.io.File;
...
Database myDatabase = null;
Environment myEnv = null;
try {
EnvironmentConfig myEnvConfig = new EnvironmentConfig();
myEnvConfig.setTransactional(true);
myEnv = new Environment(new File("/my/env/home"),
myEnvConfig);
// Open the database.
DatabaseConfig dbConfig = new DatabaseConfig();
dbConfig.setTransactional(true);
myDatabase = myEnv.openDatabase(null, "sampleDatabase", dbConfig);
// Open the transaction and enable committed reads. All cursors open
// with this transaction handle will use read committed isolation.
TransactionConfig txnConfig = new TransactionConfig();
txnConfig.setReadCommitted(true); // Use committed reads
// for this transaction.
Transaction txn = myEnv.beginTransaction(null, txnConfig);
// From here, you perform your database reads and writes as normal,
// committing and aborting the transactions as is necessary, and
// testing for deadlock exceptions as normal (omitted for brevity).
...
如果使用的是DPL
import com.sleepycat.je.Environment;
import com.sleepycat.je.EnvironmentConfig;
import com.sleepycat.je.Transaction;
import com.sleepycat.je.TransactionConfig;
import com.sleepycat.persist.EntityStore;
import com.sleepycat.persist.StoreConfig;
import java.io.File;
...
EntityStore myStore = null;
Environment myEnv = null;
try {
EnvironmentConfig myEnvConfig = new EnvironmentConfig();
myEnvConfig.setTransactional(true);
myEnv = new Environment(new File("/my/env/home"),
myEnvConfig);
// Instantiate the store.
StoreConfig myStoreConfig = new StoreConfig();
myStoreConfig.setAllowCreate(true);
myStoreConfig.setTransactional(true);
// Open the transaction and enable committed reads. All cursors open
// with this transaction handle will use read committed isolation.
TransactionConfig txnConfig = new TransactionConfig();
txnConfig.setReadCommitted(true); // Use committed reads
// for this transaction.
Transaction txn = myEnv.beginTransaction(null, txnConfig);
// From here, you perform your store reads and writes as normal,
// committing and aborting the transactions as is necessary, and
// testing for deadlock exceptions as normal (omitted for brevity).
...
您还可以通过指定LockMode.READ_COMMITTED在逐个读取的基础上配置读提交隔离:
import com.sleepycat.je.Database;
import com.sleepycat.je.DatabaseEntry;
import com.sleepycat.je.Environment;
import com.sleepycat.je.LockMode;
import com.sleepycat.je.Transaction;
...
Database myDb = null;
Environment myEnv = null;
Transaction txn = null;
try {
// Environment and database open omitted
...
txn = myEnv.beginTransaction(null, null);
DatabaseEntry theKey =
new DatabaseEntry((new String("theKey")).getBytes("UTF-8"));
DatabaseEntry theData = new DatabaseEntry();
myDb.get(txn, theKey, theData, LockMode.READ_COMMITTED);
} catch (Exception e) {
// Exception handling goes here
}
使用DPL
import com.sleepycat.je.Environment;
import com.sleepycat.je.LockMode;
import com.sleepycat.je.Transaction;
import com.sleepycat.persist.PrimaryIndex;
...
Environment myEnv = null;
Transaction txn = null;
try {
// Environment and store open omitted
...
txn = myEnv.beginTransaction(null, null);
// Primary index creation omitted
...
AnEntityClass aec = aPrimaryIndex.get(txn, "pKeya",
LockMode.READ_COMMITTED);
} catch (Exception e) {
// Exception handling goes here
}
4.3.4 Configuring Serializable Isolation 配置序列化隔离度
您可以将JE配置为使用可序列化隔离。可序列化隔离可防止事务看到幻像。当事务在执行给定查询时获得不一致的结果时,会出现幻像。
假设一个事务执行搜索,S,并且作为该搜索的结果,返回NOTFOUND。如果仅使用可重复读取隔离(默认隔离级别),则同一事务可能在以后执行S并返回SUCCESS而不是NOTFOUND。如果另一个控制线程以这样的方式修改数据库,使S成功定位数据,在没有找到数据之前,就会发生这种情况。当这种情况发生时,S返回的结果被认为是一个幻像。
要防止幻像,可以使用可序列化隔离。请注意,这会导致JE执行额外的锁定,以防止在事务结束之前插入密钥。但是,此附加锁定还可能导致应用程序的并发性降低,这意味着您的数据库访问速度可能会降低。
您可以使用EnvironmentConfig.setTxnSerializableIsolation()为环境中的所有事务配置可序列化隔离
import com.sleepycat.je.Database;
import com.sleepycat.je.DatabaseEntry;
import com.sleepycat.je.Environment;
import com.sleepycat.je.EnvironmentConfig;
import com.sleepycat.je.Transaction;
import com.sleepycat.je.LockMode;
...
Database myDb = null;
Environment myEnv = null;
Transaction txn = null;
try {
// Open an environment
EnvironmentConfig envConfig = new EnvironmentConfig();
envConfig.setAllowCreate(true);
envConfig.setTransactional(true);
// Use serializable isolation
envConfig.setTxnSerializableIsolation(true);
myEnv = new Environment(myHomeDirectory, envConfig);
// Database open omitted
...
txn = myEnv.beginTransaction(null, null);
DatabaseEntry theKey =
new DatabaseEntry((new String("theKey")).getBytes("UTF-8"));
DatabaseEntry theData = new DatabaseEntry();
myDb.get(txn, theKey, theData, LockMode.DEFAULT);
} catch (Exception e) {
// Exception handling goes here
}
如果没有为所有事务配置可序列化隔离,则可以使用TransactionConfig.setSerializableIsolation()为特定事务配置可序列化隔离:
import com.sleepycat.je.Environment;
import com.sleepycat.je.LockMode;
import com.sleepycat.je.Transaction;
import com.sleepycat.je.TransactionConfig;
import com.sleepycat.persist.PrimaryIndex;
...
Database myDb = null;
Environment myEnv = null;
Transaction txn = null;
try {
// Environment and store open omitted
...
TransactionConfig tc = new TransactionConfig();
tc.setSerializableIsolation(true); // Use serializable isolation
txn = myEnv.beginTransaction(null, tc);
// Primary index creation omitted
...
AnEntityClass aec = aPrimaryIndex.get(txn, "pKeya",
LockMode.DEFAULT);
} catch (Exception e) {
// Exception handling goes here
}
4.3.5 Transactional Cursors and Concurrent Applications事务性游标和并发应用程序
将事务性游标与并发应用程序一起使用时,请记住,如果发生死锁,则必须确保在中止并重试事务之前关闭游标。 对于基本API和DPL游标都是如此。
此外,请记住,当您使用默认隔离级别时,每次光标读取记录时,它都会锁定该记录,直到解析包含事务。 这意味着使用事务性游标遍历数据库会增加锁争用的可能性。
因此,如果必须使用事务性游标定期遍历数据库,请考虑使用降低的隔离级别,例如read committed。 对于基本API和DPL游标都是如此。
4.3.5.1 Using Cursors with Uncommitted Data
如上面的读取未提交数据中所述,可以放宽事务的隔离级别,以便它可以读取已修改但尚未由另一个事务提交的数据。 您可以在创建事务处理时配置它,当您这样做时,在该事务内打开的所有游标将自动使用未提交的读取。
从可序列化事务中创建游标句柄时,也可以执行此操作。 执行此操作时,只有那些为未提交的读取配置的游标才使用未提交的读取。
以下示例说明如何配置单个游标句柄以从可序列化(完全隔离)事务中读取未提交的数据。 有关配置事务以执行一般未提交读取的示例,请参阅读取未提交的数据。
import com.sleepycat.je.Cursor;
import com.sleepycat.je.CursorConfig;
import com.sleepycat.je.Database;
import com.sleepycat.je.DatabaseConfig;
import com.sleepycat.je.Environment;
import com.sleepycat.je.EnvironmentConfig;
import java.io.File;
...
Database myDatabase = null;
Environment myEnv = null;
try {
EnvironmentConfig myEnvConfig = new EnvironmentConfig();
myEnvConfig.setTransactional(true);
myEnv = new Environment(new File("/my/env/home"),
myEnvConfig);
// Open the database.
DatabaseConfig dbConfig = new DatabaseConfig();
dbConfig.setTransactional(true);
myDatabase = myEnv.openDatabase(null, // txn handle
"sampleDatabase", // db file name
dbConfig);
// Open the transaction. Note that this is a repeatable
// read transaction.
Transaction txn = myEnv.beginTransaction(null, null);
Cursor cursor = null;
try {
// Use the transaction handle here
// Get our cursor. Note that we pass the transaction
// handle here. Note also that we cause the cursor
// to perform uncommitted reads.
CursorConfig cconfig = new CursorConfig();
cconfig.setReadUncommitted(true);
cursor = db.openCursor(txn, cconfig);
// From here, you perform your cursor reads and writes
// as normal, committing and aborting the transactions as
// is necessary, and testing for deadlock exceptions as
// normal (omitted for brevity).
...
如果你正在使用DPL
import com.sleepycat.je.CursorConfig;
import com.sleepycat.je.Environment;
import com.sleepycat.je.EnvironmentConfig;
import com.sleepycat.persist.EntityCursor;
import com.sleepycat.persist.EntityStore;
import com.sleepycat.persist.PrimaryIndex;
import com.sleepycat.persist.StoreConfig;
import java.util.Iterator;
import java.io.File;
...
EntityStore myStore = null;
Environment myEnv = null;
PrimaryIndex<String,AnEntityClass> pKey;
try {
EnvironmentConfig myEnvConfig = new EnvironmentConfig();
myEnvConfig.setTransactional(true);
myEnv = new Environment(new File("/my/env/home"),
myEnvConfig);
// Set up the entity store
StoreConfig myStoreConfig = new StoreConfig();
myStoreConfig.setAllowCreate(true);
myStoreConfig.setTransactional(true);
// Instantiate the store
myStore = new EntityStore(myEnv, storeName, myStoreConfig);
// Open the transaction. Note that this is a repeatable
// read transaction.
Transaction txn = myEnv.beginTransaction(null, null);
//Configure our cursor for uncommitted reads.
CursorConfig cconfig = new CursorConfig();
cconfig.setReadUncommitted(true);
// Get our cursor. Note that we pass the transaction
// handle here. Note also that we cause the cursor
// to perform uncommitted reads.
EntityCursor<AnEntityClass> cursor = pKey.entities(txn, cconfig);
try {
// From here, you perform your cursor reads and writes
// as normal, committing and aborting the transactions as
// is necessary, and testing for deadlock exceptions as
// normal (omitted for brevity).
...
4.4 Read/Modify/Write 读,改,写
如果要从数据库或类中检索记录以进行修改或删除,则应在读取记录时声明读取 - 修改 - 写入周期。 这样做会导致JE在读取时获得写锁(而不是读锁)。 这有助于防止死锁,防止另一个事务在读 - 修改 - 写周期正在进行时获取同一记录上的读锁。
请注意,声明读取 - 修改 - 写入周期实际上可能会增加应用程序看到的阻塞量,因为读取器会立即获得写入锁定,并且无法共享写入锁定。 因此,只有在应用程序中发生大量死锁时才应使用读 - 修改 - 写周期。
要在执行读取操作时声明读取/修改/写入循环,请将com.sleepycat.je.LockMode.RMW指定给数据库,cursor,PrimaryIndex或SecondaryIndex get方法。
// Begin the deadlock retry loop as is normal.
while (retry_count < MAX_DEADLOCK_RETRIES) {
try {
txn = myEnv.beginTransaction(null, null);
...
// key and data are DatabaseEntry objects.
// Their usage is omitted for brevity.
...
// Read the data. Declare the read/modify/write cycle here
myDatabase.get(txn, key, data, LockMode.RMW);
// Put the data. Note that you do not have to provide any
// additional flags here due to the read/modify/write
// cycle. Simply put the data and perform your deadlock
// detection as normal.
myDatabase.put(txn, key, data);
txn.commit();
return 0;
} catch (DeadlockException de) {
// Deadlock detection and exception handling omitted
// for brevity
...
如果使用的是DPL,则这么写
// Begin the deadlock retry loop as is normal
while (retry_count < MAX_DEADLOCK_RETRIES) {
try {
txn = myEnv.beginTransaction(null, null);
...
// 'store' is an EntityStore and 'Inventory' is an entity class
// Their usage and implementation is omitted for brevity.
...
// Read the data, using the PrimaryIndex for the entity object
PrimaryIndex<String,Inventory> pi =
store.getPrimaryIndex(String.class, Inventory.class);
Inventory iv = pi.get(txn, "somekey", LockMode.RMW);
// Do something to the retreived object
// Put the object. Note that you do not have to provide any
// additional flags here due to the read/modify/write
// cycle. Simply put the data and perform your deadlock
// detection as normal.
pi.put(txn, iv);
txn.commit();
return 0;
} catch (DeadlockException de) {
// Deadlock detection and exception handling omitted
// for brevity
...