在并发环境,一个数据库系统会同时为各种各样的客户程序提供服务,也就是说,在同一时刻,会有多个客户程序同时访问数据库系统,这多个客户程序中的失误访问数据库中相同的数据时,如果没有采取必要的隔离机制,就会导致各种各样的并发问题的发生,这些并发问题可归纳为以下几类
多个事务并发引起的问题:
1)第一类丢失更新:撤消一个事务时,把其它事务已提交的更新的数据覆盖了。
2)脏读:一个事务读到另一个事务未提交的更新数据。
3) 幻读:一个事务执行两次查询,但第二次查询比第一次查询多出了一些数据行。
4)不可重复读:一个事务两次读同一行数据,可是这两次读到的数据不一样。
5)第二类丢失更新:这是不可重复读中的特例,一个事务覆盖另一个事务已提交的更新数据。
事务隔离级别
为了解决多个事务并发会引发的问题。数据库系统提供了四种事务隔离级别供用户选择。
1) Serializable:串行化。隔离级别最高
2) Repeatable Read:可重复读。--MySQL默认是这个
3) Read Committed:读已提交数据。--Oracle默认是这个
4) Read Uncommitted:读未提交数据。隔离级别最差。--sql server默认是这个
数据库系统采用不同的锁类型来实现以上四种隔离级别,具体的实现过程对用户是透明的。用户应该关心的是如何选择合适的隔离级别。
对于多数应用程序,可以优先考虑把数据库系统的隔离级别设为Read Committed,它能够避免脏读,而且具有较好的并发性能。
每个数据库连接都有一个全局变量@@tx_isolation,表示当前的事务隔离级别。JDBC数据库连接使用数据库系统默认的隔离级别。在Hibernate的配置文件中可以显示地设置隔离级别。每一种隔离级别对应着一个正整数。
Read Uncommitted: 1
Read Committed: 2
Repeatable Read: 4
Serializable: 8
在hibernate.cfg.xml中设置隔离级别如下:
<session-factory>
<!-- 设置JDBC的隔离级别 -->
<property name="hibernate.connection.isolation">2</property>
</session-factory>
设置之后,在开始一个事务之前,Hibernate将为从连接池中获得的JDBC连接设置级别。需要注意的是,在受管理环境中,如果Hibernate使用的数据库连接来自于应用服务器提供的数据源,Hibernate不会改变这些连接的事务隔离级别。在这种情况下,应该通过修改应用服务器的数据源配置来修改隔离级别。
并发控制
当数据库系统采用Red Committed隔离级别时,会导致不可重复读和第二类丢失更新的并发问题,在可能出现这种问题的场合。可以在应用程序中采用悲观锁或乐观锁来避免这类问题。
乐观锁(Optimistic Locking):
乐观锁假定当前事务操纵数据资源时,不会有其他事务同时访问该数据资源,因此不作数据库层次上的锁定。为了维护正确的数据,乐观锁使用应用程序上的版本控制(由程序逻辑来实现的)来避免可能出现的并发问题。
唯一能够同时保持高并发和高可伸缩性的方法就是使用带版本化的乐观并发控制。版本检查使用版本号、 或者时间戳来检测更新冲突(并且防止更新丢失)。
三种方式。
1)Version版本号
2)时间戳
3)自动版本控制。
这里不建议在新的应用程序中定义没有版本或者时间戳列的版本控制:它更慢,更复杂,如果你正在使用脱管对象,它则不会生效。
通过在表中及POJO中增加一个version字段来表示记录的版本,来达到多用户同时更改一条数据的冲突
数据库脚本:
create table studentVersion (id varchar(32),name varchar(32),ver int);
POJO
package Version;
public class Student {
private String id;
private String name;
private int version;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getVersion() {
return version;
}
public void setVersion(int version) {
this.version = version;
}
Student.hbm.xml
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE hibernate-mapping PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN"
"http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">
<!--
Mapping file autogenerated by MyEclipse - Hibernate Tools
-->
<hibernate-mapping>
<class name="Version.Student" table="studentVersion" >
<id name="id" unsaved-value="null">
<generator class="uuid.hex"></generator>
</id>
<!--version标签必须跟在id标签后面-->
<version name="version" column="ver" type="int"></version>
<property name="name" type="string" column="name"></property>
</class>
</hibernate-mapping>
Hibernate.cfg.xml
<?xml version='1.0' encoding='UTF-8'?>
<!DOCTYPE hibernate-configuration PUBLIC
"-//Hibernate/Hibernate Configuration DTD 3.0//EN"
"http://hibernate.sourceforge.net/hibernate-configuration-3.0.dtd">
<!-- Generated by MyEclipse Hibernate Tools. -->
<hibernate-configuration>
<session-factory>
<property name="connection.username">root</property>
<property name="connection.url">
jdbc:mysql://localhost:3306/schoolproject?characterEncoding=gb2312&useUnicode=true
</property>
<property name="dialect">
org.hibernate.dialect.MySQLDialect
</property>
<property name="myeclipse.connection.profile">mysql</property>
<property name="connection.password">1234</property>
<property name="connection.driver_class">
com.mysql.jdbc.Driver
</property>
<property name="hibernate.dialect">
org.hibernate.dialect.MySQLDialect
</property>
<property name="hibernate.show_sql">true</property>
<property name="current_session_context_class">thread</property>
<property name="jdbc.batch_size">15</property>
<mapping resource="Version/Student.hbm.xml" />
</session-factory>
</hibernate-configuration>
测试代码
package Version;
import java.io.File;
import java.util.Iterator;
import java.util.Set;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.Transaction;
import org.hibernate.cfg.Configuration;
public class Test {
public static void main(String[] args) {
String filePath=System.getProperty("user.dir")+File.separator+"src/Version"+File.separator+"hibernate.cfg.xml";
File file=new File(filePath);
System.out.println(filePath);
SessionFactory sessionFactory=new Configuration().configure(file).buildSessionFactory();
Session session=sessionFactory.openSession();
Transaction t=session.beginTransaction();
Student stu=new Student();
stu.setName("tom11");
session.save(stu);
t.commit();
/*
* 模拟多个session操作student数据表
*/
Session session1=sessionFactory.openSession();
Session session2=sessionFactory.openSession();
Student stu1=(Student)session1.createQuery("from Student s where s.name='tom11'").uniqueResult();
Student stu2=(Student)session2.createQuery("from Student s where s.name='tom11'").uniqueResult();
//这时候,两个版本号是相同的
System.out.println("v1="+stu1.getVersion()+"--v2="+stu2.getVersion());
Transaction tx1=session1.beginTransaction();
stu1.setName("session1");
tx1.commit();
//这时候,两个版本号是不同的,其中一个的版本号递增了
System.out.println("v1="+stu1.getVersion()+"--v2="+stu2.getVersion());
Transaction tx2=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_.name as name0_ from studentVersion student0_ where student0_.name='tom11'
Hibernate: select student0_.id as id0_, student0_.ver as ver0_, student0_.name as 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 was incorrect): [Version.Student#4028818316cd6b460116cd6b50830001]
可以看到,第二个“用户”session2修改数据时候,记录的版本号已经被session1更新过了,所以抛出了红色的异常,我们可以在实际应用中处理这个异常,例如在处理中重新读取数据库中的数据,同时将目前的数据与数据库中的数据展示出来,让使用者有机会比较一下,或者设计程序自动读取新的数据
注意:
要注意的是,由于乐观锁定是使用系统中的程式来控制,而不是使用资料库中的锁定机制,因而如果有人特意自行更新版本讯息来越过检查,则锁定机制就会无效,例如在上例中自行更改stu的version属性,使之与资料库中的版本号相同的话就不会有错误,像这样版本号被更改,或是由于资料是由外部系统而来,因而版本资讯不受控制时,锁定机制将会有问题,设计时必须注意。
如果手工设置stu.setVersion()自行更新版本以跳过检查,则这种乐观锁就会失效,应对方法可以将Student.java的setVersion设置成private
如果是注解方式的,POJO应为这样
@Entity
@Table(name="student ")
public class Student {
@Id @GeneratedValue
private Integer id;
private String name;
private Integer version;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getVersion() {
return version;
}
public void setVersion(Integer version) {
this.version = version;
}
}
悲观锁控制(Pressimistic Locking)
悲观锁...他依赖于数据库机制,在整个过程中将数据库锁定,其他任何用户都不能读取或者修改..通俗一点说,先读的用户就一直占用这个资源,直到结束.这里的例子,我们说一个账户信息.一共有三个字段,一个id,一个name,还有一个money,表示的是账户余额.很明显,当一个人在操作这个账户的时候,其他人是不能操作这个账户的,否则就会造成数据的不一致.
悲观锁的一般实现方式是在应用程序中显式采用数据库系统的独占锁来锁定数据库资源。在如下几种方式时可能显示指定锁定模式为LockMode.UPGRADE
1)调用session的get()或load()方法
2)调用session的lock()方法
3)调用Query的setLockMode()方法
实体类
Acount.java
package com.test.model;
public class Acount
{
private int id;
private String name;
private int money;
public int getId()
{
return id;
}
public void setId(int id)
{
this.id = id;
}
public String getName()
{
return name;
}
public void setName(String name)
{
this.name = name;
}
public int getMoney()
{
return money;
}
public void setMoney(int money)
{
this.money = money;
}
}
Account.hbm.xml
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE hibernate-mapping PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN"
"http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">
<!--
Mapping file autogenerated by MyEclipse Persistence Tools
-->
<hibernate-mapping package="com.test.model">
<class name="Acount" table="Acount" >
<id name="id">
<generator class="native"></generator>
</id>
<property name="name"></property>
<property name="money"></property>
</class>
</hibernate-mapping>
上面两个都没啥可以说的,算是最简单的hibernate实体类和配置文件了...
我们使用两个测试方法来模拟两个用户.同样,我们使用JUnit4
package com.test.junit;
import org.hibernate.Session;
import org.hibernate.Transaction;
import org.junit.Test;
import org.hibernate.LockMode;
import com.test.model.Acount;
import com.test.util.HibernateSessionFactory;
public class extendsTest
{
@Test
public void test1()
{
Session session = HibernateSessionFactory.getSession();
Transaction tx = session.beginTransaction();
Acount acount = (Acount)session.load(Acount.class, 1,LockMode.UPGRADE);//注意,这里的最后那个参数..他将锁定这个操作.
System.out.println(acount.getName());
System.out.println(acount.getMoney());
acount.setMoney(acount.getMoney() - 20000);
tx.commit();
session.close();
}
@Test
public void test2()
{
Session session = HibernateSessionFactory.getSession();
Transaction tx = session.beginTransaction();
Acount acount = (Acount)session.load(Acount.class, 1,LockMode.UPGRADE);
System.out.println(acount.getName());
System.out.println(acount.getMoney());
acount.setMoney(acount.getMoney() - 20000);
tx.commit();
session.close();
}
}
具体的做法是,我们在test1方法的事务提交前设置一个断点,然后我们用debug模式运行.然后,我们再直接运行test2方法.我们可以看到下面这样
也就是说,后面那个用户就一直在等待,.只要第一个用户没有提交.他就无法继续运行....这就是悲观锁...
悲观锁的缺点显而易见..他是彻底的占用了这个资源....所以,我们一般需要用这个来解决短事务,也就是周期比较短的事务..否则,第一个用户如果一直不操作,后面任何用户都无法进行...
经过测试,还有一个结论就是:使用悲观锁,session的load方法的延迟加载机制失效
总结:尽管悲观锁能够方式丢失更新和不可重复读之类并发问题的发生的,但是它影响并发性能。因此不建议使用悲观锁,尽量使用乐观锁