Hibernate事务、锁

事务是数据库并发控制的关键,确保数据完整性。本文详细讲解了事务的ACID特性,介绍了第一类丢失更新到幻读等并发问题,并探讨了四种事务隔离级别。在Hibernate中,通过hibernate.connection.isolation配置隔离级别。接着,文章阐述了悲观锁和乐观锁的概念,包括各种LockMode以及在Hibernate中的应用。乐观锁则通过Version或Timestamp字段实现,防止第二类丢失更新。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

事务是数据库并发控制不可分割的基本逻辑单位,可以用于确保数据库能够被正确修改,避免数据至修改了一部分而导致的数据不完整,或修改时收到用户干扰。


事务具有原子性(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=?



评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值