事务是数据库并发控制不可分割的基本逻辑单位,可以用于确保数据库能够被正确修改,避免数据至修改了一部分而导致的数据不完整,或修改时收到用户干扰。
事务具有原子性(Atomic)、一致性(Consistency)、隔离性(Isolation)、和持久性(Durability)。
多个事务同时使用相同数据时可能发生的问题:
1、第一类丢失更新:当多个事务同时操作同一个数据,撤销其中一个事务时,把其他事务已提交的更新数据覆盖,对其他事务来说造成数据丢失。
2、第二类丢失更新:多个事务同时操作同一个数据,事务A将修改结果成功提交后,对事务B已提交的修改结果进行了覆盖,对B来说造成数据丢失。
3、脏读:多个事务同事操作同一数据,事务A读到事务B未提交的更新数据,且对数据进行操作,如果B撤销更新后,事务A所操作的数据变成了脏数据。
4、不可重复读:多个事务操作同一数据,事务A对同一行数据重复读两次,每次读取的结果不同。有可能第二次读取数据时原数据被事务B更改,并成功提交。
5、幻想读:多个事务操作同一数据,事务A执行两次查询,第二次查询结果比第一次查询多出一行,这是因为两次查询之间事务B插入了新数据造成的。
为避免以上并发问题,提出4个事务隔离级别:
1、序列化(8级)
提供最严格的事务隔离,该隔离不允许事务并行执行,只允许一个接一个执行,可有效防止脏读、不可重复度和幻想读。
2、可重复读取(4级)
事务执行过程中可以访问其他事务成功提交的新插入的数据,不能访问成功修改的数据。
3、读已提交数据(2级)
一个事务在执行过程中既可以访问其他事务成功提交的新插入数据,又可以访问成功修改的数据。但未提交的写事务会禁止其他事务访问数据,可防止脏读。
4、读未提交的数据(1级)
事务执行过程中既可以访问其他事务未提交的新插入数据,又可以访问未提交的修改的数据。如果一个事务已经开始写数据,则不允许另外一个事务同时进行写操作,但允许其他事务进行读操作。此隔离可以防止第一类丢失更新。
通常数据库隔离级别设置为2,即读已提交数据。进而导致的可能不可重复读、幻读和第二类丢失更新可以通过悲观锁或乐观锁来加以控制。
Hibernate配置文件hibernate.cfg.xml中通过hibernate.connection.isolation属性设置隔离级别:<property name="hibernate.connection.isolation">2</property>
Hibernate的悲观锁和乐观锁
1、悲观锁:每次操作数据时,总是悲观的认为会有其他事务也会来操作同一数据,因此,在整个数据处理过程中,将数据处于锁定状态。悲观锁由数据库来实现,在锁定的时间其他事务不能对数据进行存取,这样很有可能造成长时间等待。Hibernate中,用户可以显示的设定要锁定的表或字段及锁模式。
锁模式:
(1)LockMode.NONE
如果缓存中存在对象,直接返回该对象的引用,否则通过select语句到数据库中加载该对象,这是锁模式的默认值。
(2)LockMode.READ
不管缓存中是否存在对象,总是通过select语句到数据库中加载该对象,如果映射文件中设置了版本元素,就执行版本检查,比较缓存中对象是否与数据库中的对象版本一致。
(3)LockMode.UPGRADE
不管缓存中是否存在对象,总是通过select语句到数据库中加载该对象,如果映射文件中设置了版本元素,就执行版本检查,比较缓存中对象是否与数据库中对象版本一致,如果数据库系统支持悲观锁(如Oracle/MySQL),就执行select```from update语句,如果不支持(如Sybase),就执行普通select语句。
(4)LockMode.UPGRADE
与LockMode.UPGRADE具有同样功能,此外,对于Oracle等支持update nowait的数据库,执行select```for update nowait语句,nowait表明如果执行该select语句的事务不能立即获得悲观锁,那么不会等待其他事务释放锁,而是立刻抛出锁定异常。
(5)LockMode.WRITE
保存对象时会自动使用这种锁定模式,仅供Hibernate内部使用,应用程序中不应该使用它。
(6)LockMode.FORCE
强制更新数据库中对象的版本属性,从而表明当前事务已经更新了这个对象。
设定锁模式的方法:
调用Session.load()时指定锁定模式、调用Session.lock()、调用Query.setLockMode()。
悲观锁:
session=HibernateSessionFactory.getSession();
tx=session.beginTransaction();
System.out.println("开始事务A");
Query query=session.createQuery("from Account a where id=1");
query.setLockMode("a", LockMode.UPGRADE_NOWAIT);
Account account=(Account)query.uniqueResult();
System.out.println("A余额:"+account.getBalance());
account.setBalance(account.getBalance()-100);
session.update(account);
System.out.println("A支取100元,剩余金额:"+account.getBalance());
tx.commit();
开始事务B
开始事务A
Hibernate: select account0_.Id as Id1_0_, account0_.AccountNo as AccountN2_0_, account0_.Balance as Balance3_0_ from bookshop.account account0_ where account0_.Id=1for update//锁定了这条记录
Hibernate: select account0_.Id as Id1_0_, account0_.AccountNo as AccountN2_0_, account0_.Balance as Balance3_0_ from bookshop.account account0_ where account0_.Id=1for update//执行这条语句时停下来等待B事务解除对记录的锁定
B余额:1000
B存款100元,剩余金额:1100
Hibernate: update bookshop.account set AccountNo=?, Balance=? where Id=?
A余额:1100
A支取100元,剩余金额:1000
Hibernate: update bookshop.account set AccountNo=?, Balance=? where Id=?
乐观锁:基于数据版本(Version)标识实现应用程序级别上的锁定机制,既能保证多个事务的并发操作,又能有效防止第二类丢失更新。
(1)基于version的乐观锁
数据库表中增加Version字段,更改实体类、映射文件,<Version>标签替换property标签,放在<id>和<property>之间。
import java.util.Timer;
import java.util.TimerTask;
import org.hibernate.Session;
import org.hibernate.Transaction;
import org.hibernate.Query;
import com.hibtest2.entity.Account1;
public class TransactionC extends TimerTask {
@Override
public void run(){
Session session=null;
Transaction tx=null;
try{
session=HibernateSessionFactory.getSession();
tx=session.beginTransaction();
System.out.println("开始事务C");
Account1 account1=(Account1)session.get(Account1.class, 1);
System.out.println("C查询到存款余额为:"+account1.getBalance());
System.out.println("C中ID为1的帐号的版本号为:"+account1.getVersion());
account1.setBalance(account1.getBalance()-100);
System.out.println("C取出100元,余额为:"+account1.getBalance());
session.update(account1);
tx.commit();
}catch(Exception e){
tx.rollback();
System.out.println("【错误信息】"+e.getMessage());
System.out.println("账户信息已被其他事务修改,本事务被撤销,请重新开始取款事务");
Timer timer1=new Timer();
timer1.schedule(new TransactionC(), 0);
}finally{
HibernateSessionFactory.closeSession();
}
}
}
测试类:
package com.hibtest2;
import java.util.Timer;
public class TestHibernateLock {
public static void main(String[] args){
new TestHibernateLock().testOptLocking();
}
private void testOptLocking(){
Timer timer1=new Timer();
timer1.schedule(new TransactionC(),0);
Timer timer2=new Timer();
timer2.schedule(new TransactionD(),0);
}
}
开始事务D
Hibernate: select account1x0_.Id as Id1_1_0_, account1x0_.version as version2_1_0_, account1x0_.AccountNo as AccountN3_1_0_, account1x0_.Balance as Balance4_1_0_ from bookshop.account1 account1x0_ where account1x0_.Id=?
开始事务C
Hibernate: select account1x0_.Id as Id1_1_0_, account1x0_.version as version2_1_0_, account1x0_.AccountNo as AccountN3_1_0_, account1x0_.Balance as Balance4_1_0_ from bookshop.account1 account1x0_ where account1x0_.Id=?
D查询到存款余额为:1000
D中ID为1的帐号的版本号为:0
D存入100元,余额为:1100
C查询到存款余额为:1000
C中ID为1的帐号的版本号为:0
C取出100元,余额为:900
Hibernate: update bookshop.account1 set version=?, AccountNo=?, Balance=? where Id=? and version=?//执行时版本为0,没问题
Hibernate: update bookshop.account1 set version=?, AccountNo=?, Balance=? where Id=? and version=?//执行更新是数据库版本变更,没有0的记录,出现异常
【错误信息】Batch update returned unexpected row count from update [0]; actual row count: 0; expected: 1
账户信息已被其他事务修改,本事务被撤销,请重新开始取款事务
开始事务C
Hibernate: select account1x0_.Id as Id1_1_0_, account1x0_.version as version2_1_0_, account1x0_.AccountNo as AccountN3_1_0_, account1x0_.Balance as Balance4_1_0_ from bookshop.account1 account1x0_ where account1x0_.Id=?
C查询到存款余额为:1100
C中ID为1的帐号的版本号为:1
C取出100元,余额为:1000
Hibernate: update bookshop.account1 set version=?, AccountNo=?, Balance=? where Id=? and version=?
(2)基于timestamp的乐观锁
数据表account1增加一个表示版本信息的字段“LastUpdateTime”取代原先的version字段。
update Account1 set LastUpdateTime='2017-02-27 11:03:00' where id=1填充数据库数据
更改实体类和映射信息
开始事务C
Hibernate: select account1x0_.Id as Id1_1_0_, account1x0_.LastUpdateTime as LastUpda2_1_0_, account1x0_.AccountNo as AccountN3_1_0_, account1x0_.Balance as Balance4_1_0_ from bookshop.account1 account1x0_ where account1x0_.Id=?
C查询到存款余额为:1000
事务C中ID为1的帐号最后修改时间为:2017-02-27 11:03:00.0
C取出100元,余额为:900
开始事务D
Hibernate: select account1x0_.Id as Id1_1_0_, account1x0_.LastUpdateTime as LastUpda2_1_0_, account1x0_.AccountNo as AccountN3_1_0_, account1x0_.Balance as Balance4_1_0_ from bookshop.account1 account1x0_ where account1x0_.Id=?
Hibernate: update bookshop.account1 set LastUpdateTime=?, AccountNo=?, Balance=? where Id=? and LastUpdateTime=?
D查询到存款余额为:1000
事务D中ID为1的帐号最后修改时间为:2017-02-27 11:03:00.0
D存入100元,余额为:1100
Hibernate: update bookshop.account1 set LastUpdateTime=?, AccountNo=?, Balance=? where Id=? and LastUpdateTime=?//这里D先进行了“提交”,所以C出错
二月 27, 2017 11:03:56 上午 org.hibernate.internal.SessionImpl$5 mapManagedFlushFailure
ERROR: HHH000346: Error during managed flush [Batch update returned unexpected row count from update [0]; actual row count: 0; expected: 1]
【错误信息】Batch update returned unexpected row count from update [0]; actual row count: 0; expected: 1
账户信息已被其他事务修改,本事务被撤销,请重新开始取款事务
开始事务C
Hibernate: select account1x0_.Id as Id1_1_0_, account1x0_.LastUpdateTime as LastUpda2_1_0_, account1x0_.AccountNo as AccountN3_1_0_, account1x0_.Balance as Balance4_1_0_ from bookshop.account1 account1x0_ where account1x0_.Id=?
二月 27, 2017 11:03:56 上午 org.hibernate.engine.jdbc.batch.internal.AbstractBatchImpl release
INFO: HHH000010: On release of batch it still contained JDBC statements
C查询到存款余额为:1100
事务C中ID为1的帐号最后修改时间为:2017-02-27 11:03:56.0
C取出100元,余额为:1000
Hibernate: update bookshop.account1 set LastUpdateTime=?, AccountNo=?, Balance=? where Id=? and LastUpdateTime=?
(3)其他实现乐观锁的方法:无须增加version或timestamp字段
在映射文件<class>中增加:<class name="com.hibtest2.entity.Account2" table="account2" optimistic-lock="all" dynamic-update="true" catalog="bookshop">
开始事务C
Hibernate: select account2x0_.Id as Id1_2_0_, account2x0_.AccountNo as AccountN2_2_0_, account2x0_.Balance as Balance3_2_0_ from bookshop.account2 account2x0_ where account2x0_.Id=?
C查询到存款余额为:1000
C取出100元,余额为:900
开始事务D
Hibernate: select account2x0_.Id as Id1_2_0_, account2x0_.AccountNo as AccountN2_2_0_, account2x0_.Balance as Balance3_2_0_ from bookshop.account2 account2x0_ where account2x0_.Id=?
D查询到存款余额为:1000
D存入100元,余额为:1100
Hibernate: update bookshop.account2 set Balance=? where Id=? and AccountNo=? and Balance=?
//添加了所有字段的乐观锁机制
Hibernate: update bookshop.account2 set Balance=? where Id=? and AccountNo=? and Balance=?
二月 27, 2017 11:33:18 上午 org.hibernate.internal.SessionImpl$5 mapManagedFlushFailure
ERROR: HHH000346: Error during managed flush [Batch update returned unexpected row count from update [0]; actual row count: 0; expected: 1]
二月 27, 2017 11:33:18 上午 org.hibernate.engine.jdbc.batch.internal.AbstractBatchImpl release
INFO: HHH000010: On release of batch it still contained JDBC statements
【错误信息】Batch update returned unexpected row count from update [0]; actual row count: 0; expected: 1
账户信息已被其他事务修改,本事务被撤销,请重新开始取款事务
开始事务C
Hibernate: select account2x0_.Id as Id1_2_0_, account2x0_.AccountNo as AccountN2_2_0_, account2x0_.Balance as Balance3_2_0_ from bookshop.account2 account2x0_ where account2x0_.Id=?
C查询到存款余额为:1100
C取出100元,余额为:1000
Hibernate: update bookshop.account2 set Balance=? where Id=? and AccountNo=? and Balance=?