数据库事务与并发处理
学习内容:
- 数据库事务的概念
- 声明事务边界
- 并发问题
- 设置事务隔离级别
- 使用悲观锁解决并发问题
- 使用乐观锁解决并发问题
1、数据库事务的概念
事务是指一组互相依赖的操作行为,如银行交易、股票交易或网上购物。事务的成功取决于这些相互依赖的操作行为是否都能执行成功,只要有一个操作行为失败,就意味着整个事务失败。例如,Tom到银行办理转账事务,把100元转到Jack账号上,这个事务包含以下操作行为:
- (1)从Tom的账户上减去100元
- (2)往Jack的帐户上增加100元
显然,以上两个操作必须作为一个不可分割的工作单元。假如仅仅第一步操作执行成功,使得Tom的账户上扣除了100元,但第二步操作执行失败,Jack的账户上没有增加100元,那么整个事务失败。
数据库事务是对现实生活中事务的模拟,它由一组在业务逻辑上互相依赖的SQL语句组成。
事务(Transaction):体现出整体的概念,那么事务中的操作全部成功,那么全部失败。
数据库事务的生命周期:
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 | 提交事务 | |
隔离级别 | 是否出现第一类丢失更新 | 是否出现脏读 | 是否出现虚读 | 是否出现不可重复读 | 是否出现第二类丢失更新 |
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进行控制
使用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;
}
}
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();
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;
}
}
create table student (
id varchar(255) not null,
lastdate datetime not null,
name varchar(255),
cardid varchar(255),
age integer,
primary key (id)
)
hibernate根据映射文件的
插入一条数据
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 |
异常的处理:
在应用程序中应该捕获该异常,这种异常有两种处理方式:
- 方式一:自动撤销事务,通知用户账户信息已经被其他事务修改,需要重新开始事务。
- 方式二:通知用户账户信息已经被其他事务修改。显示最新的存款余额信息,由用户决定如何继续事务,用户也可以决定立刻撤销事务。
实现乐观锁的其他方法
- 如果应用程序是基于已有数据库,而数据库表中不包含代表版本或时间戳的字段。Hibernate提供了其他实现乐观锁的办法,把
- Hibernate会在update语句的where子句中包含ACCOUNT对象被加载时的所有属性:
update ACCOUNTS set BALANCE=900 where ID=1 and name='Tom' and BALANCE='1000';