开发必会系列:hibernate事务

一  事务定义及特性

1、数据库事务的定义:数据库事务(Database Transaction) 是指由一个或多个SQL语句组成的工作单元,这个工作单元中的SQL语句相互依赖,如果有一个SQL语句执行失败,就必须撤销整个工作单元。

以银行转账为例:

 

 

 

2、数据库事务必须具备ACID特征
A: Atomic 原子性:整个事务不可分割,要么都成功,要么都撤销。
C: Consistency 一致性:事务不能破坏关系数据的完整性和业务逻辑的一致性,例如转账,应保证事务结束后两个账户的存款总额不变。
I: Isolation 隔离性:多个事务同时操纵相同数据时,每个事务都有各自的完整数据空间
D: Durability 持久性:只要事务成功结束,对数据库的更新就必须永久保存下来,即使系统发生崩溃,重启数据库后,数据库还能恢复到事务成功结束时的状态。

3、数据库事务的生命周期

 

 

 4、声明事务的边界

事务的开始边界(BEGIN)
事务的正常结束边界(COMMIT): 提交事务,永久的保存被事务更新后的数据库状态。
事务的异常结束边界(ROLLBACK): 撤销事务,使数据库退回到执行事务前的初始状态。

 

5、通过JDBC API来声明事务边界
Connection类提供了用于控制事务的方法:
  setAutoCommit(boolean autoCommit):设置是否自动提交事务
  commit(): 提交事务
  rollback(): 撤销事务

try {   
con = java.sql.DriverManager.getConnection(dbUrl,dbUser,dbPwd);   
//设置手工提交事务模式   
con.setAutoCommit(false);   
stmt = con.createStatement();   
//数据库更新操作1   
stmt.executeUpdate("update ACCOUNTS set BALANCE=900 where ID=1 ");   
//数据库更新操作2   
stmt.executeUpdate("update ACCOUNTS set BALANCE=1000 where ID=2 ");   
con.commit(); //提交事务   
}catch(Exception e) {   
try{   
con.rollback(); //操作不成功则撤销事务   
}catch(Exception ex){   
//处理异常   
……   
}   
//处理异常   
……   
}finally{…} 

 

二  声明事务的开始边界
Transaction tx = session.beginTransaction();
提交事务
tx.commit();
撤销事务
tx.rollback();

 

三  多个事务并发运行的问题

1、单个事务能保证单项业务的数据完整性,但是当多个事务同步运行时可能带来并发问题,具体体现在:

第一类丢失更新:在撤销一个事务时,把其它事务提交的更新数据覆盖。

 

脏读:一个事务读到另一事务未提交的更新数据。

 

 

 

虚读:一个事务读到另一事务已提交的新插入的数据。

 

 

 

不可重复读:一个事务读到另一事务已提交的更新数据。

 

 

 

第二类丢失更新:这是不可重复读中的特例,一个事务覆盖另一事务已提交的更新数据。

 

 

 


四  数据库系统锁的基本原理

1、当一个事务访问某种数据库资源时,如果执行select语句,必须先获得共享锁,如果执行insert、update或delete语句,必须获得独占锁。
2、当第二个事务也要访问相同资源时,如果执行select语句,也必须获得共享锁,如果执行insert、update或delete语句,也必须获得独占锁。此时根据已经放置在资源上的锁的类型,来决定第二个事务应该等待第一个事务解除对资源的锁定,还是可以立刻获得锁。

 

五  锁的分类:

1)从应用程序的角度分为:
(1)悲观锁
(2)乐观锁
2)数据库系统按照封锁程度可分为:
(1)共享锁:用于读数据操作,它非独占的,允许其他事务同时读取其锁定的资源,但不允许其他事务更新它。
(2)独占锁:也叫排他锁,适用于修改数据的场合。他所锁定的资源,其他事物不能读取也不能修改。
(3)更新锁:在更新操作的初始阶段用来锁定可能要修改的资源,这可以避免使用共享锁造成的死锁现象。


六  数据库的事务隔离级别

1、隔离级别的种类

为了解决数据库事务并发运行时的各种问题数据库系统提供四种事务隔离级别:

  Read Uncommitted(读未提交数据):
  它可以防止第一类丢失更新问题,但没有解决脏读以上的并发问题。它的事务隔离性最低。

  Read Committed(读已提交数据):
  它可以防止脏读以下的并发问题,但没有解决不可重复读以上的并发问题。

  Repeatable Read(可重复读):
  它可以防止不可重复读(包括第二类丢失更新)以下的并发问题,但没有解决幻读问题。

  Serializable(串行化):
  提供最严格的事务隔离性。它把事务隔离成连续的一个接一个地执行,而不是并发执行。在这种隔离级别下,不会出现任何的并发问题。


2、隔离级别所能避免能并发的问题

 

 

 

3、设置隔离级别的原则

隔离级别越高,越能保证数据的完整性和一致性,但是对并发性能的影响也越大。

对于多数据应用程序,可以优先考虑把数据库系统的隔离级别设为Read Committed,它能够避免脏读,而且具有较好的并发性能。尽管它会导致不可重复读,虚读和第二类丢失更新这些并发问题,在可能出现这类问题的个别场合,可以由应用程序悲观锁和乐观锁来解决。


4、在hibernate中设置隔离级别

JDBC数据库连接使用数据库系统默认的隔离级别。在Hibernate的配置文件中可以显示地设置隔离级别。每一种隔离级别对应着一个正整数。
1:Read Uncommitte
2:Read Committed
4:Repeatable Read
8:Serializable

 

七  在应用程序中采用悲观锁和乐观锁
1、应用程序中解决不可重复读问题

采用悲观锁和乐观锁的作用:当数据库系统采用Red Committed隔离级别时,会导致不可重复读和第二类丢失更新的并发问题。在可能出现这种问题的场合,可以在应用程序中采用乐观锁或悲观锁来避免这类问题。

2、悲观锁和乐观锁的概念

悲观锁:在数据有加载的时候就给其进行加锁,直到该锁被释放掉,其他用户才可以进行修改,优点:数据的一致性保持得很好,缺点:不适合多个用户并发访问。当一个锁住的资源不被释放掉的时候,这个资源永远不会被其他用户进行修改,容易造成无限期的等待。
乐观锁:就是在对数据进行修改的时候,对数据才去版本或者时间戳等方式来比较,数据是否一致性来实现加锁。

3、利用悲观锁协调并发运行的事务

 

 

 4、使用乐观锁

乐观锁是由应用程序提供的一种机制,这种机制既能保证多个事务并发访问数据,又能防止第二类丢失更新问题。
在应用程序中,可以利用Hibernate提供的版本控制功能来实现乐观锁。对象-关系映射文件中的<version>元素和<timestamp>元素都具有版本控制功能:
<version>元素利用一个递增的整数来跟踪数据库表中记录的版本
<timestamp>元素用时间戳来跟踪数据库表中记录的版本

<version>进行版本控制:

当Hibernate更新一个Account对象时,会根据它的id与version属性到ACCOUNTS表中去定位匹配的记录,假定Account对象的version属性为0,那么在取款事务中Hibernate执行的update语句为:
update ACCOUNTS set NAME=’Tom’,BALANCE=900,VERSION=1 
where ID=1 and VERSION=0;

如果存在匹配的记录,就更新这条记录,并且把VERSION字段的值加1。当支票转账事务接着执行以下update语句时:
update ACCOUNTS set NAME=’Tom’,BALANCE=1100,VERSION=1 
where ID=1 and VERSION=0;

由于ID为1的ACCOUNTS记录的版本已经被取款事务修改,因此找不到匹配的记录,此时Hibernate会抛出StaleObjectStateException

  


乐观锁并发取款事务和支票转账:

 

2023补充:

事务级别:
4个级别,读未提交<读已提交<可重复读<串行
有两个update,更新同一条数据
1、一个update刚修改完,还没提交,另一个update就开始读了,读到了未提交的数据,以为这个是正确数据,直接按这个update,导致数据乱了
2、加了个小锁,每个update都会执行2次查询,最后才修改,然后提交。一个update查了2次,另一个update查了一次,他们查到的值都一样,然后查了2次的开始修改,另一个update查第二次,此时卡住(有锁了),一直等第一个update修改完成并提交成功,才查出新值,它查到了2个值,迷茫了,可以抛异常。
如果另一个update不是查2次,只查了一次,那就会覆盖第一个update提交的结果,也不安全
面对这两个可能出现的问题,需要应用程序(自己写代码)增加悲观锁、乐观锁解决
悲观锁,模仿串行,两个事务可以一起开始,开始事务直接锁数据,别的事务直接卡住等锁释放
乐观锁,模仿可重复读,两个事务可以一起开始,某个事务修改时,比较时间戳或版本号,默认能执行成功,如果别的事务修改并提交过,时间戳或版本号就会不一致,事务直接报错 (mvcc就是乐观锁的一种实现,mysql在2,3这俩级别,都是默认使用mvcc的
具体解释:
MVCC,Multi-Version Concurrency Control,多版本并发控制
需要注意的是:MVCC只在读已提交和可重复读这两个隔离级别下有效,因为串行化这个隔离级别,是通过加排他锁让事务串行执行,串行执行不会有并行执行带来的并发问题,也就不需要用MVCC进行并发控制。而读未提交这个隔离级别,是让事务读取最新的数据,不需要用版本号进行数据版本范围的限制,所以也用不到MVCC。
MVCC的好处是什么呢?MVCC实现事务的可重复读和读已提交这两个隔离级别,用的是版本号比对机制而非加锁的手段,避免了加锁所带来的性能问题,实现了读操作的高性能。但是MVCC也有缺点:一是需要为每条数据多保存两个字段,会占用一定的存储空间;二是数据版本的比较操作也会造成一定的开销。

3、加个大点的锁,每个update都会执行2次查询,最后才修改,然后提交。一个update查了2次,另一个update第一次查时直接卡住,等人家提交完事了,这边才能第一次查。安全多了。
但还有个小问题,另一个update第一次出个值,突然来了个insert,还提交成功了,然后update查第二次时,总数的值变了(如果update只更新一条数据,且这条数据和总数无关,那就没关系,否则就有错),迷茫了,可以抛异常。
4、加最大的锁,只要有DML语句,直接全锁,其他DML不能查。
大部分数据库默认级别2,mysql默认级别3
一条sql就是一条事务,默认执行一条会自动提交,批量更新应该关自动提交,用批量执行。

 

数据库自带的行锁和表锁什么时候用?
Mysql的行锁是通过通过给索引上的索引项加锁来实现的,即是行锁是加在索引相应的行上的,要是对应的SQL语句没有走索引,则会全表扫描,行锁则无法实现,取而代之的是表锁。
InnoDB这种行锁实现的特点意味着:只有通过索引条件检索数据,并且执行时真正使用到了索引,InnoDB才使用行级锁,否则,InnoDB 将使用表锁。
不论是使用主键索引、唯一索引还是普通索引,当表有多个索引的时候,不同的事务可以使用不同的索引锁定不同的行。
执行update语句耗时很久,我在等待过程中执行select count时需要等update锁么?
count(1)会自己找个辅助索引列去查,如果update在改索引列,那就会触发行锁,count就要等update执行完释放锁。

 

八  缓存

缓存是计算机领域的概念,它介于应用程序和永久性数据存储源之间
Hibernate的缓存一般分为3类:
  一级缓存
  二级缓存
  查询缓存


1、Session内的缓存即一级缓存;

2、二级缓存是进程或集群范围内的缓存,可以被所有的Session共享;
二级缓存是可配置的插件;

选择合适的缓存插件,配置其自带的配置文件。
选择需要使用二级缓存的持久化类,设置它的二级缓存的并发访问策略。

3、查询是数据库技术中最常用的操作,Hibernate为查询提供了缓存,用来提高查询速度,优化查询性能
查询缓存依赖于二级缓存

posted @ 2020-08-20 13:29  zhaot1993  阅读(543)  评论(0编辑  收藏  举报