缓存和事务

原创
2015/11/15 19:55
阅读数 90

事务的4个基本特性(ACID):

1.Atomic(原子性):事务中包含的操作被看作一个逻辑单元,这个逻辑单元中的操作要么全部成功,要么全部失败。

2.Consistency(一致性):只有合法的数据可以被写入数据库,否则事务应该将其回滚到最初状态。

3.Isolation(隔离性):事务允许多个用户对同一个数据的并发访问,而不破坏数据的正确性和完整性。同时,并行事务的修改必须与其他并行事务的修改相互独立。

4.Durability(持久性):事务结束后,事务处理的结果必须能够得到固化。

 

数据库操作过程中可能出现的3种不确定情况:

1.脏读取(Dirty Reads):一个事务读取了另一个并行事务未提交的数据。

2.不可重复读取(Non-repeatable Reads):一个事务再次读取之前的数据时,得到的数据不一致,被另一个已提交的事务修改。

3.虚读(Phantom Reads):一个事务重新执行一个查询,返回的记录中包含了因为其他最近提交的事务而产生的新记录。

Hibernate将事务管理委托给底层的JDBC或者JTA,默认是基于JDBC Transaction的。

Hibernate支持“悲观锁(Pessimistic Locking)”和“乐观锁(Optimistic Locking)”。

悲观锁对数据被外界修改持保守态度,因此,在整个数据处理过程中,将数据处于锁定状态。悲观锁的实现,往往依靠数据库提供的锁机制。Hibernate通过使用数据库的for update子句实现了悲观锁机制。Hibernate的加锁模式有:

1. LockMode.NONE:无锁机制

2. LockMode.WRITEHibernateInsertUpdate记录的时候会自动获取

3. LockMode.READHibernate在读取记录的时候会自动获取

4. LockMode.UPGRADE:利用数据库的for update子句加锁

5. LockMode.UPGRADE_NOWAITOracle的特定实现,利用Oraclefor update nowait子句实现加锁

  

乐观锁大多是基于数据版本(Version)记录机制实现。Hibernate在其数据访问引擎中内置了乐观锁实现,可以通过class描述符的 optimistic-lock属性结合version描述符指定。optimistic-lock属性有如下可选取值:

1. none:无乐观锁

2. version:通过版本机制实现乐观锁

3. dirty:通过检查发生变动过的属性实现乐观锁

4. all:通过检查所有属性实现乐观锁

 

通过以上的介绍可以看出hibernate主要从以下几个方面来优化查询性能:

1,降低访问数据库的频率,减少select语句的数目,实现手段有:使用迫切左外连接或迫切内连接;对延迟检索或立即检索设置批量检索数目;使用查询缓存。

2,避免加载多余的应用程序不需要访问的数据,实现手段有:使用延迟加载策略;使用集合过滤。

3,避免报表查询数据占用缓存,实现手段为利用投影查询功能,查询出实体的部分属性。

4,减少select语句中的字段,从而降低访问数据库的数据量,实现手段为利用Queryiterate()方法。

Queryiterate()方法首先检索ID字段,然后根据ID字段到hibernate的第一级缓存以及第二级缓存中查找匹配的 Customer对象,如果存在,就直接把它加入到查询结果集中,否则就执行额外的select语句,根据ID字段到数据库中检索该对象。

对于经常使用的查询语句,如果启用了查询缓存,当第一次执行查询语句时,hibernate会把查询结果存放在第二级缓存中,以后再次执行该查询语句时,只需从缓存中获得查询结果,从而提高查询性能。如果查询结果中包含实体,第二级缓存只会存放实体的OID,而对于投影查询,第二级缓存会存放所有的数据值。

查询缓存适用于以下场合:

在应用程序运行时经常使用的查询语句;

很少对与查询语句关联的数据库数据进行插入,删除,更新操作。

缓存具体配置参照上一篇文章,这里不作概述;

许多数据库系统都有自动管理锁的功能,它们能根据事务执行的SQL语句,自动在保证事务间的隔离性与保证事务间的并发性之间做出权衡,然后自动为数据库资源加上适当的锁,在运行期间还会自动升级锁的类型,以优化系统的性能。

对于普通的并发性事务,通过系统的自动锁定管理机制基本可以保证事务之间的隔离性,但如果对数据安全,数据库完整性和一致性有特殊要求,也可以由事务本身来控制对数据资源的锁定和解锁。

  数据库系统能够锁定的资源包括:数据库,表,区域,页面,键值(指带有索引的行数据),行(即表中的单行数据)。在数据库系统中,一般都支持锁升级,以提高性能。

  按照封锁程序,锁可以分为:共享锁,独占锁,更新锁。

  共享锁:用于读数据操作,它是非独占的,允许其它事务同时读取其锁定的资源,但不允许其它事务更新它。

  独占锁:也称排它锁,适用于修改数据的场合,它所销定的资源,其它事务不能读取也不能修改。

  更新锁:在更新操作的初始化阶段用来锁定可能要被修改的资源,这可以避免使用共享锁造成的死锁现象。许多的数据库系统能够自动定期搜索和处理死锁问题,当检测到锁定请求环时,系统将结束死锁优先级最低的事务,并且撤销该事务。 

应用程序中可以采用下面的一些方法尽量避免死锁

  1,合理安排表访问顺序;

  2,使用短事务;

  3,如果对数据的一致性要求不高,可以允许脏读,脏读不需要对数据资源加锁,可以避免冲突;

  4,如果可能的话,错开多个事务访问相同数据资源的时间,以防止锁冲突。

  5,使用尽可能低的事务隔离级别。

为了实现短事务,在应用程序中可以考虑使用以下策略:

  1,如果可能的话,尝试把大的事务分解为多个小的事务,然后分别执行,这保证每个小事务都很快完成,不会对数据资源锁定很长时间。

  2,应该在处理事务之前就准备好用户必须提供的数据,不应该在执行事务过程中,停下来长时间等待输入数据。

数据库系统提供了四种事务隔离级别供用户选择:

  1Serializable:串行化。

  2Repeatable Read:可重复读。

  3Read Commited:读己提交数据。

  4Read Uncommited:读未提交数据。

 

隔离级别越高,越能保证数据的完整性和一致性,但是对并发性能的影响也越大。对于多数应用程序,可以优先把数据库系统的隔离级别设为ReadCommited,它能够避免脏读,而且具有较好的并发性能,尽管它会导致不可重复读,虚读和第二类丢失更新这些并发问题,在可能出现这类问题的个别场合,可以由应用程序采用悲观锁或乐观锁来控制。

悲观锁和乐观锁

悲观锁:指在应用程序中显式地为数据资源加锁,先锁定资源再进行操作,尽管悲观锁能够防止丢失更新和不可重复读这类并发问题,但是它会影响并发性能,因此应该很谨慎地使用悲观锁。

  乐观锁:完全依靠数据库的隔离级别来自动管理锁的工作,应用程序采用版本控制手段来避免可能出现的并发问题。

悲观锁有两种实现方式:

1,在应用程序中显式指定采用数据库系统的独占锁来锁定数据资源;

2,在数据库表中增加一个表明记录状态的LOCK字段,当它取值为Y时,表示该记录己经被某个事务锁定,如果为N,表明该记录处于空闲状态,事务可以访问它。

 

以下select语句,指定采用独占锁来锁定查询的记录:select ... forupdate;执行该查询语句的事务持有这把锁,直到事务结束才会释放锁。

hibernate可以采用如下方式声明使用悲观锁:

Account account =(Account)session.get(Account.class, 1, LockMode.UPGRADE);

net.sf.hibernate.LockMode类表示锁模式,它的取值如下:

  LockMode.NONE:默认值。先查缓存,缓存没有再去数据库中查。

  LockMode.READ:总是查询数据库,如果映射文件设置了版本元素,就执行版本比较。主要用于对一个游离对象进行版本检查。

  LockMode.UPGRADE:总是查询数据库,如果映射文件设置了版本元素,就执行版本比较。如果数据库支持悲观锁就执行.... for update。否则执行普通查询。

  LockMode.UPGRADE_NOWAIT:和UPGRADE功能一样,此外,对oracle数据库执行... for update nowait; nowait表明,如果不能立即获得悲观锁就抛出异常。

  LockMode.WRITE:当hibernate向数据库保存或更新一个对象时,会自动使用这种模式,它仅供hibernate内部使用,应用程序中不应该使用它。

如果数据库不支持select... for update语句,也可以由应用程序来实现悲观锁,这需要要表中增加一个锁字段lock

 hibernate映射文件中的<version><timestamp>元素都具有版本控制功能。

<version>利用一个递增的整数来跟踪数据库表中记录的版本,

<timestamp>用时间戳来跟踪数据库表中记录的版本。

version的用法参考我写的另一篇文章,这里不做概述;

hibernate二级缓存

hibernate的二级缓存本身的实现很复杂,必须实现并发访问策略以及数据过期策略。SessionFactory的外置缓存是一个可配置的缓存插件,在默认情况下不会启用。

 二级缓存,进程范围或群集范围,会出现并发问题,对二级缓存可以设定以下四种类型的并发访问策略,每一种策略对应一种事务隔离级别。

  1,事务型:仅仅在受管理环境中适用,它提供Repeatable Read事务隔离级别,对于经常读但是很少写的数据,可以采用这种隔离级别,因为它可以防止脏读和不可重复读这类并发问题。

  2,读写型:提供Read Committed事务隔离级别,仅仅在非群集的环境中适用,对于经常读但是很少写的数据,可以采用这种隔离类型,因为它可以防止脏读这类并发问题。

  3,非严格读写型:不保证缓存与数据库中数据的一致性。如果存在两个事务同时访问缓存中相同数据的可能,必须为该数据配置一个很短的数据过期时间,从而尽量避免脏读,对于极少被修改并且允许脏读的数据,可以采用这种并发访问策略。

  4,只读型:对于从来不会写的数据,可以使用这种并发访问策略。

事务型策略的隔离级别最高,只读型的最低,事务隔离级别越高,并发性能越低,如果二级缓存中存放中的数据会经常被事务修改,就不得不提高缓存的事务隔离级别,但这又会降低并发性能,因此,只有符合以下条件的数据才适合于存放到二级缓存中:

1,很少被修改;

2,不是很重要的数据,允许偶尔出现并发问题;

3,不会被并发访问的数据;

4,参考数据;

以下数据不适合于存放到二级缓存中:

  1,经常被修改的数据;2,财务数据,绝对不允许出现并发问题;3,与其它应用共享的数据;

 

hibernate还为查询结果提供了一个查询缓存,它依赖于二级缓存。

Session为应用程序提供了两个管理一缓存的方法:

    evict():从缓存中清除参数指定的持久化对象;如果在映射文件关联关系的cascadeallall-delete-orphan时,会级联清除;它适用于不希望session继续按照该对象的状态变化来同步更新数据库;在批量更新或指量删除的场合,当更新或删除一个对象后,及时释放该对象占用的内存;值得注意的是,批量更新或删除的最佳方式是直接通过JDBC API执行相关的SQL语句或者调用相关的存储过程。

   clear():清空缓存中所有持久化对象;

在多数情况下,不提倡通过evict()clear()方法来管理一级缓存,因为它们并不能显着地提高应用的性能,管理一级缓存的最有效的方法是采用合理的检索策略和检索方式,如通过延迟加载,集合过滤,投影查询等手段来节省内存开销。

 

 

hibernate的二级缓存允许选用以下类型的缓存插件:

  1EHCache:可作为进程范围内的缓存,存放数据的物理介质可以是内存或硬盘,对hibernate的查询缓存提供了支持。

  2OpenSymphony OSCache:可作为进程范围内的缓存,存放数据的物理介质可以是内存或硬盘,提供了丰富的缓存数据过期策略,对hibernate的查询缓存提供了支持。

  3SwarmCache:可作为群集范围内的缓存,但不支持hibernate的查询缓存。

  4JBossCache:可作为群集范围内的缓存,支持事务型并发访问策略,对hibernate的查询缓存提供了支持

 

 下表列出了以上四种类型的缓存插件支持的并发访问策略:

以面的四种缓存插件都是由第三方提供的。EHCache来自于hibernate开放源代码组织的另一个项目;JBossCache JBoss开放源代码组织提供;为了把这些缓存插件集成到hibernate中,hibernate提供了 net.sf.hibernate.cache.CacheProvider接口,它是缓存插件与hibernate之间的适配器。hibernate为以上缓存插件分别提供了内置的CacheProvider实现:

  net.sf.hibernate.cache.EhCacheProvider

  net.sf.hibernate.cache.OSCacheProvider

  net.sf.hibernate.cache.SwarmCacheProvider

  net.sf.hibernate.cache.TreeCacheProviderJBossCache插件适配器。

 

配置进程范围内的二级缓存主要包含以下步骤:

1,选择需要使用二级缓存的持久化类。设置它的命名缓存的并发访问策略。hibernate既允许在分散的各个映射文件中为持久化类设置二级缓存,还允许在hibernate的配置文件hibernate.cfg.xml中集中设置二级缓存,后一种方式更有利于和缓存相关的配置代码的维护。

<hibernate-configuration>
  <session-factory>
  <property  ... >
  <!-- 设置JBossCache适配器 -->
  <property  name="cache.provider_class">net.sf.hibernate.cache.TreeCacheProvider</property>
  <property  name="cache.use_minimal_puts">true</property>
  <mapping  .../>
  <!-- 设置Category类的二级缓存的并发访问策略 -->
  <class-cache  class="mypack.Category" usage="transaction" />
  <!-- 设置Category类的items集合的二级缓存的并发访问策略 -->
  <collection-cache  collection="mypack.Category.items" usage="transactional"  />
  <!-- 设置Item类的二级缓存的并发访问策略 -->
  <class-cache  class="mypack.Item" usage="transactional" />
  </session-factory>
  </hibernate-configuration>

cache.use_minimal_puts属性为true,表示hibernate会先检查对象是否己经存在于缓存中,只有当对象不在缓存中,才会向缓存加入该对象的散装数据,默认为false。对于群集范围的缓存,如果读缓存的系统开销比写缓存的系统开销小,可以将此属性设为true,从而提高访问缓存的性能,而对于进程范围内的缓存,此属性应该取默认值false

 

2,选择合适的缓存插件,每种插件都有自带的配置文件,因此需要手工编辑该配置文件,EHCache的配置文件为ehcache.xml,而JBossCache的配置文件为treecache.xml。在配置文件中需要为每个命名缓存设置数据过期策略。

hibernate允许在类和集合的粒度上设置二级缓存,在映射文件中,<class><set>元素都有一个 <cache>子元素,这个子元素用来配置二级缓存,例如以下代码把Category实例放入二级缓存中,采用读写并发访问策略:

<cache usage="read-write"/> 可以配置在class下也可以配置在set集合里面

 EHCache缓存插件是理想的进程范围内的缓存实现。如果使用这种缓存插件,需要在hibernatehibernate.properties配置文件中指定EhCacheProvider适配器,代码如下:

  hibernate.cache.provider=net.sf.hibernate.cache.EhCacheProvider

  EHCache缓存有自己的配置文件,名为ehcache.xml,这个文件必须存放于应用的classpath中,下面是一个样例:

<ehcache>
  <diskStore  path="C:\\temp"/>
  <defaultCache 
 maxElementsInMemory="10000" eternal="false"  timeToIdleSeconds="120" 
timeToLiveSeconds="120"  overflowToDisk="true"/>
  <cache 
 name="mypack.Category" maxElementsInMemory="500" eternal="true"  
timeToIdleSeconds="0" timeToLiveSeconds="0"  overflowToDisk="false"/>
  <cache 
 name="mypack.Category.items" maxElementsInMemory="10000"  
eternal="false" timeToIdleSeconds="300"  timeToLiveSeconds="600" 
overflowToDisk="true"/>
  <cache 
 name="mypack.Item" maxElementsInMemory="10000"  eternal="false" 
timeToIdleSeconds="300"  timeToLiveSeconds="600" 
overflowToDisk="true"/>
</ehcache>


        <diskStore>:指定一个文件目录,当EHCache把数据写到硬盘上时,将把数据写到这个文件目录下。

  <defaultCache>:设定缓存的默认数据过期策略。

  <cache>:设定具体的命名缓存的数据过期策略。

  在映射文件中,对每个需要二级缓存的类和集合都做了单独的配置,与此对应,在ehcache.xml文件中通过<cache>元素来为每个需要二级缓存的类和集合设定缓存的数据过期策略。下面解释一下<cache>元素的各个属性的作用:

  name:设置缓存的名字,它的取值为类的完整名字或者类的集合的名字,如果name属性为mypack.Category,表示 Category类的二级缓存;如果name属性为mypack.Category.items,表示Category类的items集合的二级缓存。

  maxInMemory:设置基于内存的缓存可存放的对象的最大数目。

  eternal:如果为true,表示对象永远不会过期,此时会忽略timeToIdleSecondstimeToLiveSeconds属性。默认为false

  timeToIdleSeconds:设定允许对象处于空闲状态的最长时间,以秒为单位,当对象从最近一次被访问后,如果处于空闲状态的时间超过了指定的值,这个对象会过期,EHCache将把它从缓存中清除,只有当eternal属性为false,它才有效,值为0表示对象可以无限期地处于空闲状态。

  timeToLiveSeconds:设定对象允许存在于缓存中的最长时间,以秒为单位,当对象自从被放入缓存中后,如果处于缓存中的时间超过了指定的值,这个对象就会过期,EHCache将把它从缓存中清除,只有当eternal属性为false,它才有效,值为0表示对象可以无限期地处于空闲状态。它的值必须大于或等于timeToIdleSeconds的值才有意义。

  overflowToDisk:如果为true,表示当基于内存的缓存中的对象数目达到了maxInMemory界限,会把溢出的对象写到基于硬盘的缓存中。

  每个命名缓存代表一个缓存区域,每个缓存区域有各自的数据过期策略,命名缓存机制使得用户能够在每个类以及类的每个集合的粒度上设置数据过期策略。

  EHCache适用于hibernate应用发布在单个机器中的场合。

 

Hibernate的事务和并发控制很容易掌握。Hibernate直接使用JDBC连接和JTA资源,不添加任何附加锁定行为。


展开阅读全文
打赏
0
1 收藏
分享
加载中
更多评论
打赏
0 评论
1 收藏
0
分享
返回顶部
顶部