数据库事务的定义
数据库事务(Database Transaction),是指作为单个逻辑工作单元执行的一系列操作。一个逻辑工作单元要成为事务,必须满足所谓的ACID(原子性、一致性、隔离性和持久性)属性。
● 原子性(atomic),事务必须是原子工作单元;对于其数据修改,要么全都执行,要么全都不执行
● 一致性(consistent),事务在完成时,必须使所有的数据都保持一致状态。
● 隔离性(insulation),由并发事务所作的修改必须与任何其它并发事务所作的修改隔离。
● 持久性(Duration),事务完成之后,它对于系统的影响是永久性的。
数据库事务并发可能带来的问题
如果没有锁定且多个用户同时访问一个数据库,则当他们的事务同时使用相同的数据时可能会发生问题。由于并发操作带来的数据不一致性包括:丢失数据修改、读”脏”数据(脏读)、不可重复读、产生幽灵数据:
● 第一类丢失更新(lost update):在完全未隔离事务的情况下,两个事物更新同一条数据资源,某一事物异常终止,回滚造成第一个完成的更新也同时丢失。
● 脏读(dirty read):如果第二个事务查询到第一个事务还未提交的更新数据,形成脏读。因为第一个事务你还不知道是否提交,所以数据不一定是正确的。
● 虚读(phantom read):一个事务执行两次查询,第二次结果集包含第一次中没有或者某些行已被删除,造成两次结果不一致,只是另一个事务在这两次查询中间插入或者删除了数据造成的。
● 不可重复读(unrepeated read):一个事务两次读取同一行数据,结果得到不同状态结果,如中间正好另一个事务更新了该数据,两次结果相异,不可信任。
● 第二类丢失更新(second lost updates):是不可重复读的特殊情况,如果两个事务都读取同一行,然后两个都进行写操作,并提交,第一个事务所做的改变就会丢失。
Hibernate事务隔离级别:(不同数据库对应默认的级别不一样) 为了解决数据库事务并发运行时的各种问题数据库系统提供四种事务隔离级别,在Hibernate的配置文件中可以显示的配置数据库事务隔离级别。每一个隔离级别用一个整数表示: ● Serializable 串行化(8)二进制值0001 ● Repeatable Read 可重复读(4)二进制值0010 MySql默认隔离级别 ● Read Commited 可读已提交(2)二进制值0100 Oracle默认级别 ● Read Uncommited 可读未提交(1)二进制值1000 在hibernate.cfg.xml中使用hibernate.connection.isolation参数配置数据库事务隔离级别。
Hibernate对数据的锁机制:
Hibernate可以利用Query或者Criteria的setLockMode()方法来设定要锁定的表或列(Row)及其锁定模式: ●LockMode.NONE:无锁机制;在事务结束时,所有的对象都切换到该模式上来。与session相关联的对象通过调用update()或者saveOrUpdate()脱离该模式 ●LockMode.WRITE:当更新或者插入一行记录的时候,锁定级别自动设置为LockMode.WRITE ●LockMode.READ:当Hibernate在“可重复读”或者是“序列化”数据库隔离级别下读取数据的时候,锁定模式自动设置为LockMode.READ。这种模式也可以通过用户显式指定进行设置。 ●LockMode.UPGRADE:利用数据库的for update子句加锁 ●LockMode.UPGRADE_NOWAIT:利用oracle的特定实现for update nowait子句实现
使用悲观锁解决事务并发问题:
悲观锁,正如其名,他总是悲观的认为要操作的数据会有并发访问。因此,在整个数据处理过程中,将数据处于锁定状态。悲观锁的实现,往往依靠数据库提供的锁机制(也只有数据库层提供的锁机制才能真正保证数据访问的排他性,否则,即使在本系统中实现了加锁机制,也无法保证外部系统不会修改数据)。 一个典型的依赖数据库的悲观锁调用:select * from account where name=”Erica” for update这条sql语句锁定了account表中所有符合检索条件(name=”Erica”)的记录。本次事务提交之前(事务提交时会释放事务过程中的锁),外界无法修改这些记录。悲观锁,也是基于数据库的锁机制实现。 在Hibernate使用悲观锁十分容易,但实际应用中悲观锁是很少被使用的,因为它大大限制了并发性:
悲观锁用法参考下面代码实例:
Transaction tx=session.beginTransaction();
//取得持久化User对象,并使用LockMode.UPGRADE模式锁定对象
User user=(User)session.get(User.class,1,LockMode.UPGRADE);
user.setName(“newName”); //更改对象属性,注意并不需要使用session.save(user)
tx.commit();
String hqlStr="from TUser user where user.name='Erica'";
Query query=session.createQuery(hqlStr);
query.setLockMode("user",LockModel.UPGRADE);
这样的话,Hibernate会使用select …… for update语句载入User类,并且锁住了这个对象在数据库中的列,直到事务完成(commit()以后)。
### 使用乐观锁解决事务并发问题
相对悲观锁而言,乐观锁机制采取了更加宽松的加锁机制。悲观锁大多数情况下依靠数据库的锁机制实现,以保证操作最大程度的独占性。但随之而来的就是数据库性能的大量开销,特别是对长事务而言,这样的开销往往无法承受。乐观锁机制在一定程度上解决了这个问题。乐观锁,大多是基于数据版本(Version)记录机制实现。何谓数据版本?即为数据增加一个版本标识,在基于数据库表的版本解决方案中,一般是通过为数据库表增加一个"version"字段来实现。
乐观锁的工作原理:读取出数据时,将此版本号一同读出,之后更新时,对此版本号加一。此时,将提交数据的版本数据与数据库表对应记录的当前版本信息进行比对,如果提交的数据版本号大于数据库表当前版本号,则予以更新,否则认为是过期数据。
Hibernate为乐观锁提供了3种实现:
● 基于version的乐观锁
配置基于version的乐观锁:
[html] view plaincopyprint?
1. <?xml version="1.0" encoding="utf-8"?>
2. <!DOCTYPE hibernate-mapping PUBLIC"-//Hibernate/Hibernate Mapping DTD 3.0//EN""http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">
3.
4. <hibernate-mapping>
5. <classnameclassname="com.bzu.hibernate.pojos.People"table="people">
6. <idnameidname="id"type="string">
7. <columnnamecolumnname="id"></column>
8. <generatorclassgeneratorclass="uuid"></generator>
9. </id>
10.
11. <!--version标签用于指定表示版本号的字段信息-->
12. <versionnameversionname="version"column="version"type="integer"></version>
13.
14. <propertynamepropertyname="name"column="name"type="string"></property>
15.
16. </class>
17. </hibernate-mapping>
注:不要忘记在实体类添加属性version
下面我们就模拟多个session,基于version的来进行一下测试:
- /*
-
* 模拟多个session操作student数据表
-
*/
-
Session session1=sessionFactory.openSession();
-
Session session2=sessionFactory.openSession();
-
Studentstu1=(Student)session1.createQuery("from Student s wheres.name='tom11'").uniqueResult();
-
Studentstu2=(Student)session2.createQuery("from Student s wheres.name='tom11'").uniqueResult();
-
//这时候,两个版本号是相同的
-
System.out.println("v1="+stu1.getVersion()+"--v2="+stu2.getVersion());
-
Transactiontx1=session1.beginTransaction();
-
stu1.setName("session1");
-
tx1.commit();
-
//这时候,两个版本号是不同的,其中一个的版本号递增了
-
System.out.println("v1="+stu1.getVersion()+"--v2="+stu2.getVersion());
-
Transactiontx2=session2.beginTransaction();
-
stu2.setName("session2");
-
tx2.commit();
运行结果:
Hibernate: insert into studentVersion (ver, name,id) values (?, ?, ?)
Hibernate: select student0_.id as id0_, student0_.ver as ver0_, student0_.nameas name0_ from studentVersion student0_ where student0_.name='tom11'
Hibernate: select student0_.id as id0_, student0_.ver as ver0_, student0_.nameas name0_ from studentVersion student0_ where student0_.name='tom11'
v1=0--v2=0
Hibernate: update studentVersion set ver=?, name=? where id=? and ver=?
v1=1--v2=0
Hibernate: update studentVersion set ver=?, name=? where id=? and ver=?
Exception in thread "main" org.hibernate.StaleObjectStateException:Row was updated or deleted by another transaction (or unsaved-value mapping wasincorrect): [Version.Student#4028818316cd6b460116cd6b50830001]
可以看到,第二个“用户”session2修改数据时候,记录的版本号已经被session1更新过了,所以抛出了红色的异常,我们可以在实际应用中处理这个异常,例如在处理中重新读取数据库中的数据,同时将目前的数据与数据库中的数据展示出来,让使用者有机会比较一下,或者设计程序自动读取新的数据
● 基于timestamp
配置基于timestamp的乐观锁:
[html] view plaincopyprint?
1. <?xml version="1.0" encoding="utf-8"?>
2. <!DOCTYPE hibernate-mapping PUBLIC"-//Hibernate/Hibernate Mapping DTD 3.0//EN""http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">
3.
4. <hibernate-mapping>
5. <classnameclassname="com.suxiaolei.hibernate.pojos.People"table="people">
6. <id name="id"type="string">
7. <column name="id"></column>
8. <generator class="uuid"></generator>
9. </id>
10.
11. <!--timestamp标签用于指定表示版本号的字段信息-->
12. <timestamp name="updateDate"column="updateDate"></timestamp>
13.
14. <propertynamepropertyname="name"column="name"type="string"></property>
15.
16.
17. </class>
18. </hibernate-mapping>
● 为遗留项目添加添加乐观锁
例如:
一般是通过为数据库表增加一个 “version” 字段来实现(我只知道Oracle是这样,其他的没试过)
个人感觉火车站卖票系统就是乐观锁,查票时查到了脏数据(就是另外一些事务还未提交的数据),看到显示屏上有少量剩余票,但点进去去买又买不到。乐观锁原理其实就是给每一条数据加上时间概念的标识,表明我这条数据是什么时候的,可以理解为是相对时间,就是你所说的建表的时候添加version,然后我去取这条数据,比如version这个时候是0015了,那么我操作完这条数据,要update回去时,又去查一遍这个version值,发现如果还是0015,那么我就放心了,我就可以把version变成0016然后提交,因为在我操作这个过程中没人动他,如果比0015大,那说明有人动过他了,我就选择不操作这条数据了。
还有一种方式是直接用时间字段表示,也就是时间戳,就是我提交这条数据时,之前拿到数据时的那个时间值一定要等于我要提交之前这个时刻点的时间值,如果小于当前数据的这个时间戳,那么肯定是有事务在我之前提交了。
其实乐观锁是解决高并发性能的办法。悲观锁更安全,但是并发性能太差,特别是有长事务的时候,并发事务会排很长的队
总结
数据库事务应该尽可能的短
这样能降低数据库中的锁争用。数据库长事务会阻止你的应用程序扩展到高的并发负载。因此,假若在用户思考期间让数据库事务开着,直到整个工作单元完成才关闭这个事务,这绝不是一个好的设计。
这就引出一个问题:一个操作单元,也就是一个事务单元的范围应该是多大?
一个操作一个?一个请求一个?一个应用一个?
反模式:session-per-operation
在单个线程中,不要因为一次简单的数据库调用,就打开和关闭一次Session!数据库事务也是如此。也就是说应该禁止自动事务提交(auto-commit)。
session-per-request
最常用的模式是每个请求一个会话。在这种模式下,来自客户端的请求被发送到服务器端,即 Hibernate 持久化层运行的地方,一个新的 Hibernate Session 被打开,并且执行这个操作单元中所有的数据库操作。一旦操作完成(同时对客户端的响应也准备就绪),session 被同步,然后关闭。会话和请求之间的关系是一对一的关系。
Hibernate内置了对“当前session(current session)”的管理,用于简化此模式。你要做的一切就是在服务器端要处理请求的时候,开启事务,在响应发送给客户之前结束事务,通常使用Servelt Filter来完成。
针对这种模式,Spring提供了对Hibernate事务的管理,提供了“一请求一事务”的Filter来利用Http请求与响应来控制session和事务的生命周期。
[html] view plain copy
1. <filter>
2. <filter-name>HibernateOpenSessionInViewFilter</filter-name>
3. <filter-class>
4. org.springframework.orm.hibernate3.support.OpenSessionInViewFilter
5. </filter-class>
6. </filter>