Hibernate_10

对于同时运行的多个事务,当这些事务访问数据库中相同的数据时,若没有采用必要的隔离机制,就会导致各种问题,这些病发问题可归纳为以下几类:
第一类丢失更新:撤销一个事务时,把其他事务已提交的更新数据股覆盖
脏读:一个事务读到另一事务未提交的更新数据
虚读:一个事务独到另一事务已提交的新插入的数据
不可重复读:一个事务读到另一事务已提交的更新数据
第二类丢失更新:这是不可重复读中的特例,一个事务覆盖另一事务已提交的个更新数据。

第一类丢失更新:这种并发是由于完全没有隔离事务造成的。当两个事务更新相同的数据资源,若一个事务被提交,另一个事务却被撤销,那么会连同第一个事务所做的更新也被撤销。
脏读:若第二个事务查询到第一个事务未提交的更新数据,第二个事务依据这个查询结果继续执行相关的操作,但接着第一个事务撤销了所做的更新,这导致第二个事务操纵脏数据。
虚读:由于一个事务查询到另一事务已提交的新插入的数据引起的。
不可重复读:由于一个事务查询到了另一个事务已提交的对数据的更新引起的。当第二个事务在某一时刻查询某条记录,在另一时刻在查询相同记录时,看到了第一个事务已提交的对这条记录的更新,第二个事务无法判断到底以哪一时刻查询到的记录作为计算基础。
第二类丢失更新:当两个或多个事务查询同样的记录,然后各自基于最初查询的结果更新记录时,会造成第二类丢失更新问题。每个事务都不知道其他事务的存在,最后一个事务对记录所做的更新将覆盖由其他事务对该记录所做的已提交的更新。

数据库系统的ACID特性中,隔离性就是指数据库系统必须具有隔离并发运行的各个事务的能力,使它们不会互相影响。数据库采用锁来实现事务的隔离性。锁的基本原理如下:
当一个事务访问某种数据库资源时,若执行select语句时,必须先获得共享锁;执行insert、update或delete语句时,也必须获得独占锁,此时根据已经放置在资源上的锁的类型,来决定第二个事务应该等待第一个事务接触对资源的锁定,还是可以立刻获得锁。
         根据已放置在资源上的锁来决定第二个事务能否立刻获得特定类别的锁
资源上已经放置的锁         第二个事务进行读操作        第二个事务进行更新操作
无                         立即获得共享锁              立即获得独占锁
共享锁                     立即获得共享锁              等待第一个事务解除共享锁
独占锁                     等待第一个事务解除独占锁    等待第一个事务解除独占锁

数据库系统能够锁定的资源包括:数据库、表、区域、页面、键值、行。按照锁定资源的粒度,锁可分为以下类型:
数据库级锁:锁定一战数据库表
表级锁:锁定一张数据库表
区域级锁:锁定数据库的特定区域
页面级锁:锁定数据库的特定页面
键值级锁:锁定数据库表中带有索引的一行数据
行级锁:锁定数据库表中的单行数据
锁的封锁粒度越大,事物间的隔离性就越高,事务间的并发性能就越低。

按照封锁程度,锁可分为:共享锁、独占锁和更新锁。
共享锁:用于读数据操作,允许其他事务同时读取其锁定的资源,但不允许其他事务更新它。有以下特征:
加锁的条件:当一个事务执行select语句时,数据库系统会为这个事务分配一把共享锁,来锁定被查询的数据。
解锁的条件:在默认的情况下,数据被读取后,数据库系统立即接触共享锁。
与其它锁的兼容性:若数据资源上放置了共享锁,还能在放置共享锁和更新锁
并发性能:具有良好的并发性能,当多个事务读相同的数据时,每个事物都会获得一把共享锁
独占锁:也叫排他锁,适用于修改数据的场合。它锁定的资源,其它事务不能读取,也不能修改。独占锁具有以下特征:
加锁的条件:当一个事务执行insert、update或delete语句时,数据库系统会自动对SQL语句操纵的数据资源使用独占锁。若该数据资源已经有其它锁存在时,无法对其再放置独占锁
解锁的条件:独占锁直到事务结束后才能被解锁
兼容性:独占所不能和其他锁兼容,若数据资源上已经加了独占锁,就不能再放置其他的锁。同样,若数据资源上已经有了其他的锁,就不能再放置独占锁
并发性能:并发性能较差,只允许有一个事务访问锁定的数据,若其他事务也需要访问该数据,就必须等待,直到前一个事务结束,解除了独占锁,其他事务才有机会访问该数据

更新锁:在更新操作的初始化阶段用来锁定可能要被修改的资源,可以避免使用共享锁造成的死锁现象。
若使用共享锁,更新数据的操作分为两步:
例: update ACCOUNTS set BALANCE=9000 where ID=1
获得一个共享锁,读取ACCOUNTS表中ID为1的记录
将共享锁升级为独占锁,在执行更新操作
若同时有两个或多个事务同时更新数据,每个事务都先获得一把共享锁,在更新数据的时候,这些事务都要先将共享锁升级为独占锁。由于独占锁不能和其他锁兼容,因此每个事务都进入等待状态,等待其他事务释放共享锁,这就造成了死锁。
若使用更新锁,更新数据的操作分为以下两步:
获得一个更新锁,读取ACCONTS表中ID为1的记录
将更新所升级为独占锁,在执行更新操作
更新锁有以下特征:
加锁的条件:当一个事务执行update语句时,数据库系统会先为事务分配一把更新锁
解锁的条件:当读取数据完毕,执行更新操作时,会把更新锁升级为独占锁
与其它锁的兼容性:更新锁与共享锁是兼容的,也就是说,一个资源可以同时放置更新锁和共享锁,但最多只能防止一把更新锁
并发性能:允许多个事务同时读锁定的资源,但不允许其他事务修改它

死锁:多个事务分别锁定一个资源,又试图请求锁定对方已经锁定的资源,导致多个事务处于等待对方释放锁定资源的状态

短事务:在一个数据库事务中包含尽可能少的操作,并且在尽可能短的时间内完成。
为实现短事务,在应用程序中可以考虑使用以下策略:
若可能的话尝试把大的事务分解为多个小的事务,然后分别执行,
应该在处理事务前就准备好用户必须提供的数据,不应该在执行事务过程中,停下来查过时间等待用户输入。

为了能让用户根据实际应用的需要,在事务的隔离性与并发性之间做合理的权衡,数据库系统提供了4种事务隔离级别供用户选择。
Serializable:串行化            隔离级别最高
Repeatable Read:可重复读
Read Committed:读已提交数据
Read Uncommitted:读未提交数据  隔离级别最低
                    各种隔离级别所能避免的并发问题
隔离级别         第一类丢失更新    脏读    虚读    不可重复读     第二类丢失更新
Serializeble        否          否      否           否             否
Repeatable Read     否          否      是           否             否
Read Committed       否          否      是           是             是 
Read Uncommitted     否          是      是           是             是

Serializable:当数据库使用Serializable隔离级别时,一个事务在执行过程中完全看不到其他事务对数据库所做的更新。当两个事务同时操纵数据库中相同的数据时,若第一个事务已经在访问数据,第二个事务只能停下来,必读等到第一个事务结束后才能恢复运行
Repeatable Read:一个事务在执行过程中可以看到其他事务已经提交的新插入的记录,但是不能看到其他事务对已有记录的更新
Read Committed:一个事务在执行过程中可以看到其他事务已经提交的新插入的记录,而且能看到其他事务已经提交的对已有记录的更新
Read Uncommitted:一个事务在执行过程中个可以看到其他事务没有提交的新插入的记录,而且能看到其他事务没有提交的对已有记录的更新

在mysql.exe程序中设置隔离级别
//查看当前的隔离级别
mysql>select @@tx_isolation
//设置(全局)隔离级别
mysql> set (global) transaction isolation level read committed

在应用程序中设置隔离级别
JDBC数据库连接使用数据库系统默认的隔离级别。在Hibernate的配置文件中以显式地设置隔离级别。每一种隔离级别都对应一个整数
1:Read Uncommitted
2:Read Committed
3:Repeatable Read
4:Seriliazable
hibernate.connection.isolation=2

当数据库系统采用Read Committed隔离级别时,会导致不可重复读和第二类丢失更新的并发问题。在可能出现这种问题的场合,可以子啊应用中采用悲观锁或乐观锁来避免这类问题。
悲观锁:在应用程序中显式地为数据资源加锁。悲观锁假定当前事务操纵数据资源时,肯定还会有其他事务同时访问该数据资源,为了避免当前事务的操作受到干扰,先锁定资源。尽管悲观锁能够防止丢失更新和不可重复读这类问题,但它会影响并发性能。
乐观锁:乐观所假定当前事务操纵数据资源时,不会有其他事务同时访问该数据源,因此完全依靠数据库的隔离级别来自动管理锁的工作。
悲观锁有两种实现方式:
方式一:在应用程序中显式指定采用数据库系统的独占锁来锁定数据资源
select ...  for update
在Hibernate应用中,当通过Session的get()和load()方法来加载一个对象时,可以采用一下方式声明使用悲观锁
Account account = (Account)session.get(Account.class,new Long
(1),LockMode.UPgrATE);
                    LockMode类表示的几种锁定模式
锁定模式                       描述
LodeMode.NONE(默认)     若在Hibernate的缓存中存在对象,就直接返回该对象的引用,否则就通过select语句到数据库中加载对象 不
LockMode.READ            不管Hibernate的缓存中是否存在对象,总是通过select语句到数据库中加载对象,若映射文件中设置了版本元素,比较缓存中的对象是否和数据库中的对象的版本一致
LockMode.UPGRADE         不管Hibernate的缓存中是否存在对象,总是通过select语句到数据库中加载对象,若映射文件中设置了版本元素,比较缓存中的对象是否和数据库中的对象的版本一致,若数据库系统支持悲观锁,就执行select ... for update语句,若不支持,就执行普通的select语句
LockMode.UPGRADE_NOWAIT  和LockMode.UPGRADE具有同样的功能。nowait表明执行该select语句的事务不能立即获得悲观锁,那么不会等待其他事务释放锁,而是立刻抛出以个锁定异常
LockMode.WRITE           当Hibernate向数据库保存一个对象时,会自动使用这种锁定模式,这种锁定模式仅供Hibernate内部使用,在应用程序中不应该使用
LockMode.FORCE           强制更新数据库中对象的版本属性,从而表明当前事务已经更新了这个对象

在Hibernate提供改的版本控制功能来实现乐观锁。对象-关系映射文件中的<version>元素和<timestamp>都具有版本控制功能。<version>元素利用一个递增的整数来跟踪数据库表中记录的版本,而<timestamp>元素用时间戳来跟踪数据库表红记录的版本。版本控制不具有级联性。
使用<version>元素
1.在ACCOUNT类中定义一个代表版本信息的属性
private int version;
public int getVersion(){
   return this.version;
}
public int setVersion(int version){
   this.version=version;
}
2.在ACCOUNTS表中定义一个代表版本信息的字段
create table ACCOUNTS{
  ID bigint not null,
  NAME varchar(15),
  BALANCE decimal(10,2),
  VERSION integer,
  primary key(ID)
}type=INNODB
3.在Account.hbm.xml文件中用<version>元素来建立Account类的version属性与ACCOUNTS表中VERSION字段的映射
<id name="id" type="long" column="ID">
      <generator class="increment">
</id>
//version必须紧跟在id后面
<version name="version"  column="VERSION">

在应用程序中应该捕获StaleOjectStateException,这种异常有两种处理方式:
1.自动撤销事务,
2.通知用户信息已被其他事务修改,由用户决定如何继续事务

方式二:在数据库表中增加一个表明记录状态的LOCK字段,当它取值为'Y'时,表示该记录已经被某个事务锁定,若为'N',表明该记录处于空闲状态,事务可以访问它

使用<timestamp>元素,只能精确到秒
1.在ACCOUNT类中定义一个代表版本信息的属性
private Date lastUpdateTime;
public int getLastUpdateTime(){
   return this.lastUpdateTime;
}
public int setLastUpdateTime(Date lastUpdateTime){
   this.lastUpdateTime=lastUpdateTime;
}
2.在ACCOUNTS表中定义一个代表版本信息的字段
create table ACCOUNTS{
  ID bigint not null,
  NAME varchar(15),
  BALANCE decimal(10,2),
  LAST_UPDATE_TIME timestamp,
  primary key(ID)
}type=INNODB
3.在Account.hbm.xml文件中用<version>元素来建立Account类的version属性与ACCOUNTS表中VERSION字段的映射
<id name="id" type="long" column="ID">
      <generator class="increment">
</id>
//timestamp必须紧跟在id后面
<timestamp name="lastUpdateTime"  column="LAST_UPDATE_TIME">

Session的lock()方法显式对一个有利对象进行版本控制
Session session1 = sessionFactory.openSession();
tx1 = session1.beginTransaction();
Account account = (Account)session1.get(Account.class,new Long(1));
tx1.commit();
session1.close();

account.setBalance(account.getBalance()-100);

Session session2 = sessionFactory.openSession();
tx2 = session2.beginTransaction();
session2.lock(account,LockMode.READ);
tx2.commit();
session2.close();

Session的lock()方法执行以下步骤:
把Account对象与当前Session关联。
若设定了LockMode.READ模式,就比较这个Account对象的版本是否与ACCOUNTS表中对应记录的版本一致。若不一致,说明ACCOUNTS表中对饮个记录已经被其他事务修改,因此会抛出StaleObjectStateException。

Hibernate提供了实现乐观所的办法。把<class>元素的optimistic-lock属性设为all。
若把optimistic-lock的属性设为all或dirty时,必须同时把dynamic-update属性设为true。这种方法速度慢,只是用于在一个Session中加载了对象,然后在同一个Session中修改这个持久化对象的场合。

猜你喜欢

转载自forever1121.iteye.com/blog/2118139