现在的位置: 首页 > 综合 > 正文

15、数据库事务、并发处理与隔离级别

2018年02月05日 ⁄ 综合 ⁄ 共 12397字 ⁄ 字号 评论关闭

数据库事务与并发处理

学习内容:

- 数据库事务的概念
- 声明事务边界
- 并发问题
- 设置事务隔离级别
- 使用悲观锁解决并发问题
- 使用乐观锁解决并发问题

1、数据库事务的概念

事务是指一组互相依赖的操作行为,如银行交易、股票交易或网上购物。事务的成功取决于这些相互依赖的操作行为是否都能执行成功,只要有一个操作行为失败,就意味着整个事务失败。例如,Tom到银行办理转账事务,把100元转到Jack账号上,这个事务包含以下操作行为:

- (1)从Tom的账户上减去100元
- (2)往Jack的帐户上增加100元

显然,以上两个操作必须作为一个不可分割的工作单元。假如仅仅第一步操作执行成功,使得Tom的账户上扣除了100元,但第二步操作执行失败,Jack的账户上没有增加100元,那么整个事务失败。

数据库事务是对现实生活中事务的模拟,它由一组在业务逻辑上互相依赖的SQL语句组成。

事务(Transaction):体现出整体的概念,那么事务中的操作全部成功,那么全部失败。

数据库事务的生命周期:


 MySQL数据库系统的客户程序

2、声明事务的边界

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

在mysql.exe程序中声明事务

- 每启动一个mysql.exe程序,就会得到一个单独的数据库连接。每个数据库连接都有个全集变量@@autocommit,表示当前的事务模式,他有两个可选值:
    - 0:表示手工提交模式
    - 1:默认值,表示自动提交模式

- 如果要察看当前的事务模式,可以使用如下SQL命令:- mysql>select @@autocommit;

- 如果要把当前的事务模式改为手工提交模式,可以使用的SQL命令:- mysql> set autocommit = 0;

在自动提交模式下运行事务

- 在自动提交模式下,每个SQL语句都是一个独立的事务。如果在一个mysql.exe程序中执行SQL语句:
- mysql>insert into ACCOUNTS values(1,'Tom',1000);

- MySQL会自动提交这个事务,这意味着向ACCOUNTS表中新插入的记录会永久保存在数据库中。此时在另一个mysql.exe程序中执行SQL语句:

- mysql>select * from ACCOUNTS;

- 这条select语句会查询到ID为1的ACCOUNTS记录。这表明在第一个mysql.exe程序中插入的ACCOUNTS记录被永久保存,这体现了事务的ACID特性中的持久性。

数据库事务的4个特性:1)原子性(Atom)    2)一致性(Consistence)   3)隔离性(Isolation)  4)持久性(Duration)

在手工模式下运行事务

- (1)启动两个mysql.exe程序,在两个程序中都执行以下命令,以便设定手工提交事务模式:
    - mysql>set autocommit = 0;
- (2)在第一个mysql.exe中执行SQL语句:
    - mysql>begin;
    - mysql>insert into ACCOUNTS values(2,'Jom',1000);
- (3)在第二个mysql.exe执行
    - mysql>begin;
    - mysql>select * from ACCOUNTS; 
    - mysql>commit;
- 以上select语句的查询结果中并不包括ID为2的ACCOUNTS记录,这是因为第一个mysql.exe程序还没有提交事务。
-(4)在第一个mysql.exe中执行:
    - mysql>commit;
- (5)在第二个mysql.exe中执行:
    -  mysql>begin;
    - mysql>select * from ACCOUNTS; 
    - mysql>commit;
此时,select语句查询结果中会包含ID为2的记录,这是因为第一个mysql.exe已经提交事务。

3、通过JDBC API声明事务边界

- Connection提供了以下用于控制事务的方法:
    - setAutoCommit(boolean autoCommit):设置是否自动提交事务
    - commit():提交事务
    - rollback():撤销事务

try{

con = java.sql.DriverManager.getConnection(dbURL,dbuser,dbpassword);
        //设置手工提交事务模式
        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{......}

4、通过Hibernate API声明事务边界

    -声明事务的开始边界:

    Transaction tx = session.beginTansaction();

    -提交事务:

    tx.commit();

    - 撤销事务:

    tx.tollback();

5、多个事务并发运行时的并发问题

- 第一类丢失更新:撤销一个事务时,把其他事务已经提交的更新数据覆盖。
- 脏读:一个事务读到另一个事务未提交的更新数据。
- 虚读:一个事务读到另一个事务已提交的新插入的数据。
- 不可重复读:一个事务读到另一个事务已提交的更新数据。
- 第二类丢失更新:这是不可重复读中的特例,一个事务覆盖另一个事务已提交的更新数据。

取款事务和支票转账事务
- 取款事务包含以下步骤:

    - (1)某客户在银行前台请求取款100元,出纳员先查询账户信息,得知余额为1000元。
    - (2)出纳员判断存款超过取款额,就支付给客户100元,并将账户上的存款余额改为900元。

- 支票转账事务包含以下步骤:
    - (1)某出纳员处理一转账支票,该支票向一账户汇入100元。出纳员先查询账户信息,得知存款余额900元。
    - (2)出纳员将存款余额改为1000元。

并发运行的两个事务导致脏读

时间 取款事务 支票转账事务
T1 开始事务  
T2   开始事务
T3 查询账户余额为1000元  
T4    
T5 取出100,把存款余额改为900元  
T6   查询账户的存款余额为900元(脏读)
T7 撤销该事务,把存款余额恢复为1000元  
T8   汇入100元,把存款余额改为1000元
T9   提交事务

 并发运行的两个事务导致第二类丢失更新

时间 取款事务 支票转账事务
T1 开始事务  
T2   开始事务
T3 查询账户余额为1000元  
T4   查询账户余额为1000元
T5 取出100,把存款余额改为900元  
T6 提交事务  
T7   汇入100元,把存款余额改为1100元
T8   提交事务
     

6、数据库的事务隔离级别

隔离级别 是否出现第一类丢失更新 是否出现脏读 是否出现虚读 是否出现不可重复读 是否出现第二类丢失更新
Serializable
Repeatable Read
Read Commited
Read Uncommited

设定隔离级别的原则

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

在mysql.exe程序中设置隔离级别

- 每启动一个mysql.exe程序,就会获得一个单独的数据库连接。每个数据库连接都有个全局变量@@tx_isolation,表示当前的事务隔离级别。MySQL默认的隔离级别为Repeatable Read。如果要查看当前的隔离级别,可以使用如下SQL语句:
    - mysql>select @@tx_isolation;
- 如果要把当前mysql.exe程序的隔离级别改为Read Committed,可以使用如下SQL命令:
    - mysql>set tansaction isolation level read committed;

Hibernate中设置隔离级别

- 在Hibernate的配置文件中可以显式的设置隔离级别。每一种隔离级别都对应一个整数:
    - 1:Read Uncommitted
    - 2:Read Committed
    - 4:Repeatable Read
    - 8:Serializable

例如,以下代码把hibernate.cgf.xml文件中的隔离级别设为Read Committed:
    hibernate.connection.isolation = 2
对于从数据库连接池中获得的每个连接,Hibernate都会把它改为使用Read Committed隔离级别。

7、使用悲观锁

Account accout = (Account)session.get(Account.class,new Long(1),LockMode.UPGRADE);
account.setBalance(account.getBalance() -100);

Hibernate执行的select语句为:
select * from ACCOUNTS where ID = 1 for update;
update ACCOUNTS set BALANCE = 900...

利用悲观锁协调并发运行的取款事务和支票转账事务。

时间 取款事务 支票转账事务
T1 开始事务  
T2   开始事务
T3 select * from ACCOUNTS where ID = 1 for update;
查询账户余额为1000元;这条记录被锁定。
 
T4   select * from ACCOUNTS where ID = 1 for update;
执行该语句时,事务停下来等待取款事务解除对这条记录的锁定。
T5 取出100,把存款余额改为900元  
T6 提交事务  
T7   事务恢复运行,查询结果显示存款余额为900.这条记录被锁定。
T8   汇入100元,把存款余额改为1000元。
T9   提交事务

是由数据库来控制的,不是由程序来控制的。

8、使用乐观锁

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

具体程序的使用,使用version进行控制

使用Student类

import java.util.Set;

public class Student
{
	private String id;
	
	private String cardId;
	
	private String name;
	
	private int age;
	
	private int version;

	public int getVersion()
	{
		return version;
	}

	public void setVersion(int version)
	{
		this.version = version;
	}

	public Student()
	{
		
	}
	
	public Student(String name, int age)
	{
		this.name = name;
		this.age = age;
	}

	public String getCardId()
	{
		return cardId;
	}

	public void setCardId(String cardId)
	{
		this.cardId = cardId;
	}

	public int getAge()
	{
		return age;
	}

	public void setAge(int age)
	{
		this.age = age;
	}


	public void setId(String id)
	{
		this.id = id;
	}

	public String getName()
	{
		return name;
	}

	public void setName(String name)
	{
		this.name = name;
	}

	public String getId()
	{
		return id;
	}
}

对应的HBM文件:

<?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">

<hibernate-mapping>
	
	<class name="com.cdtax.hibernate.Student" table="student">
	
		<id name="id" column="id" type="string">
			<generator class="uuid"></generator>
		</id>
		
		<version name="version" column="version" type="integer"></version>
		<property name="name" column="name" type="string"></property>
		<property name="cardId" column="cardid" type="string"></property>
		<property name="age" column="age" type="int"></property>
		
	</class>
</hibernate-mapping>

产生数据库表:

   create table student (
        id varchar(255) not null,
        version integer not null,
        name varchar(255),
        cardid varchar(255),
        age integer,
        primary key (id)
    )

测试保存:

try
		{
			tx = session.beginTransaction();
			
			Student student = new Student();
			
			student.setCardId("123465");
			student.setAge(40);
			student.setName("zhangsan");
			
			session.save(student);
			
			
			tx.commit();
		}

启用两个session进行查询:

try
		{
			
			Session session1 = sessionFactory.openSession();
			Session session2 = sessionFactory.openSession();
			
			Student student1 = (Student)session1.createQuery("from Student s where s.name = :name")
					.setString("name", "zhangsan").uniqueResult();
			
			Student student2 = (Student)session2.createQuery("from Student s where s.name = :name")
					.setString("name", "zhangsan").uniqueResult();
			
			System.out.println(student1.getVersion());
			System.out.println(student2.getVersion());
			
			Transaction tx1 = session1.beginTransaction();
			
			student1.setName("lisi");
			tx1.commit();
			
			System.out.println(student1.getVersion());
			System.out.println(student2.getVersion());
			
		}

执行结果:

Hibernate: 
    select
        student0_.id as id0_,
        student0_.version as version0_,
        student0_.name as name0_,
        student0_.cardid as cardid0_,
        student0_.age as age0_ 
    from
        student student0_ 
    where
        student0_.name=?
Hibernate: 
    select
        student0_.id as id0_,
        student0_.version as version0_,
        student0_.name as name0_,
        student0_.cardid as cardid0_,
        student0_.age as age0_ 
    from
        student student0_ 
    where
        student0_.name=?
0
0
Hibernate: 
    update
        student 
    set
        version=?,
        name=?,
        cardid=?,
        age=? 
    where
        id=? 
        and version=?

1
0

一开始两个session查询的version都是一样的,为0

然后session1进行更新操作,这里要注意,update更新不仅根据id,而且还要根据version,version如果跟上次查询的一样才可以更新,同时,更新不仅更新name,还同时更新version。所以再次打印version时,student1的version为1.

如果将程序修改如下:

			Session session1 = sessionFactory.openSession();
			Session session2 = sessionFactory.openSession();
			
			Student student1 = (Student)session1.createQuery("from Student s where s.name = :name")
					.setString("name", "zhangsan").uniqueResult();
			
			Student student2 = (Student)session2.createQuery("from Student s where s.name = :name")
					.setString("name", "zhangsan").uniqueResult();
			
			System.out.println(student1.getVersion());
			System.out.println(student2.getVersion());
			
			Transaction tx1 = session1.beginTransaction();
			
			student1.setName("lisi");
			tx1.commit();
			
			System.out.println(student1.getVersion());
			System.out.println(student2.getVersion());
			
			Transaction tx2 = session2.beginTransaction();
			student2.setName("wangwu");
			tx2.commit();
			
			session1.close();
			session2.close();

又开启一个事务tx2,再将student的那么改一下,执行则出现异常:

Hibernate: 
    select
        student0_.id as id0_,
        student0_.version as version0_,
        student0_.name as name0_,
        student0_.cardid as cardid0_,
        student0_.age as age0_ 
    from
        student student0_ 
    where
        student0_.name=?
Hibernate: 
    select
        student0_.id as id0_,
        student0_.version as version0_,
        student0_.name as name0_,
        student0_.cardid as cardid0_,
        student0_.age as age0_ 
    from
        student student0_ 
    where
        student0_.name=?
2
2
Hibernate: 
    update
        student 
    set
        version=?,
        name=?,
        cardid=?,
        age=? 
    where
        id=? 
        and version=?
3
2
Hibernate: 
    update
        student 
    set
        version=?,
        name=?,
        cardid=?,
        age=? 
    where
        id=? 
        and version=?
org.hibernate.StaleObjectStateException: Row was updated or deleted by another transaction (or unsaved-value mapping was incorrect): [com.cdtax.hibernate.Student#402881c04255514e014255514f330001]
at org.hibernate.persister.entity.AbstractEntityPersister.check(AbstractEntityPersister.java:1782)
at org.hibernate.persister.entity.AbstractEntityPersister.update(AbstractEntityPersister.java:2425)
at org.hibernate.persister.entity.AbstractEntityPersister.updateOrInsert(AbstractEntityPersister.java:2325)
at org.hibernate.persister.entity.AbstractEntityPersister.update(AbstractEntityPersister.java:2625)
at org.hibernate.action.EntityUpdateAction.execute(EntityUpdateAction.java:115)
at org.hibernate.engine.ActionQueue.execute(ActionQueue.java:279)
at org.hibernate.engine.ActionQueue.executeActions(ActionQueue.java:263)
at org.hibernate.engine.ActionQueue.executeActions(ActionQueue.java:168)
at org.hibernate.event.def.AbstractFlushingEventListener.performExecutions(AbstractFlushingEventListener.java:321)
at org.hibernate.event.def.DefaultFlushEventListener.onFlush(DefaultFlushEventListener.java:50)
at org.hibernate.impl.SessionImpl.flush(SessionImpl.java:1028)
at org.hibernate.impl.SessionImpl.managedFlush(SessionImpl.java:366)
at org.hibernate.transaction.JDBCTransaction.commit(JDBCTransaction.java:137)
at com.cdtax.hibernate.HibernateTest.main(HibernateTest.java:69)

事务2修改时,因为version已经被事务1修改,所以他的更新是不成功的,出现异常。

使用时间戳的方式进行控制:

Student类:

import java.util.Date;

public class Student
{
	private String id;
	
	private String cardId;
	
	private String name;
	
	private int age;
	
	private Date lastDate;


	public Date getLastDate()
	{
		return lastDate;
	}

	public void setLastDate(Date lastDate)
	{
		this.lastDate = lastDate;
	}

	public Student()
	{
		
	}
	
	public Student(String name, int age)
	{
		this.name = name;
		this.age = age;
	}

	public String getCardId()
	{
		return cardId;
	}

	public void setCardId(String cardId)
	{
		this.cardId = cardId;
	}

	public int getAge()
	{
		return age;
	}

	public void setAge(int age)
	{
		this.age = age;
	}


	public void setId(String id)
	{
		this.id = id;
	}

	public String getName()
	{
		return name;
	}

	public void setName(String name)
	{
		this.name = name;
	}

	public String getId()
	{
		return id;
	}
}

映射文件:

<?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">

<hibernate-mapping>
	
	<class name="com.cdtax.hibernate.Student" table="student">
	
		<id name="id" column="id" type="string">
			<generator class="uuid"></generator>
		</id>
		
		<timestamp name="lastDate" column="lastdate" ></timestamp>
		<property name="name" column="name" type="string"></property>
		<property name="cardId" column="cardid" type="string"></property>
		<property name="age" column="age" type="int"></property>
		
	</class>
</hibernate-mapping>

产生的数据库表:

    create table student (
        id varchar(255) not null,
        lastdate datetime not null,
        name varchar(255),
        cardid varchar(255),
        age integer,
        primary key (id)
    )

hibernate根据映射文件的<timestamp>就知道数据类型,所以在映射文件中不需要指定类型。

插入一条数据

try
		{
			tx = session.beginTransaction();
			
			Student student = new Student();
			
			student.setCardId("123465");
			student.setAge(40);
			student.setName("zhangsan");
			
			session.save(student);
			tx.commit();
		}

id lastdate name cardid age
402881c042558c160142558c178a0001 2013/11/14 15:38:33 zhangsan 123465 40

是根据lastdate来进行控制的。

异常的处理:

在应用程序中应该捕获该异常,这种异常有两种处理方式:
    - 方式一:自动撤销事务,通知用户账户信息已经被其他事务修改,需要重新开始事务。
    - 方式二:通知用户账户信息已经被其他事务修改。显示最新的存款余额信息,由用户决定如何继续事务,用户也可以决定立刻撤销事务。

实现乐观锁的其他方法

- 如果应用程序是基于已有数据库,而数据库表中不包含代表版本或时间戳的字段。Hibernate提供了其他实现乐观锁的办法,把<class>元素的optimistic-lock属性设为”all“:

    <class name="Account" table="ACCOUNTS" optimistic-lock="all" dynamic-update="true">
- Hibernate会在update语句的where子句中包含ACCOUNT对象被加载时的所有属性:
     update ACCOUNTS set BALANCE=900 where ID=1 and name='Tom' and BALANCE='1000';

抱歉!评论已关闭.