站内搜索: 请输入搜索关键词

当前页面: 开发资料首页Java 专题数据库事务备忘录

数据库事务备忘录

摘要: 数据库事务备忘录

</td> </tr> <tr> <td height="35" valign="top" class="ArticleTeitle"> <table width="100%" border="0" cellspacing="0" cellpadding="0"> <tr> <td width="502" height="86" align="left" valign="top"> 事务是指一组相互依赖的操作行为,举一个举得不能再被举的例子就是银行交易,当然还有其他像网上购物,电子货币交易等等,事务的成功取决于这些相互依赖的操作行为是否都能执行成功,只要有一个操作行为失败,就意味着整个事务失败。例如:bill和tom的QQ账户的QQ币点数都是500点,现在bill把100点QQ币转到tom的QQ币账号上,这个事务就包含以下操作行为:
---- bill的QQ币账号减少100点
---- tom的QQ币账号增加100点

这两个操作作为一个不可分割的工作单元,假如仅仅第一步操作执行成功,但是第二步执行失败,那么整个事务失败,回滚到事务开始前的状态,bill和tom的QQ账户的QQ币点数还依然是500点。显然如果没有事务的概念,那么就会造成bill的100点QQ币神秘的消失了...
数据库的事务就是上面提到的事务在RDB中的实现,它由一组在业务逻辑上相互依赖的SQL语句组成,假设以上QQ账户在DB中的qq_account表结构如下:
------------------------------------
id name balance
------------------------------------
1 bill 500
2 tom 500
------------------------------------

以上事务的SQL可以表示为:
UPDATE qq_account SET balance=400 WHERE id=1;
UPDATE qq_account SET balance=600 WHERE id=2;

只要两条SQL语句有一个执行失败,整个事务就失败,qq_account表中的数据就必须回退到最初的状态,而不会被更新。
数据库事务必须具备ACID特性,具体的含义如下:

----- Atomic(原子性):只整个数据库事务是不可分割的工作单元。只有事务中所有的操作执行成功,才算整个事务成功;事务中任何一个SQL语句执行失败,那么已经执行成功的SQL语句也必须撤销,数据库状态应该退回到执行事务前的状态。通过上面的例子我们已经看得很清楚了。

----- Consistency(一致性):指数据库事务不能破坏关系数据的完整性以及业务逻辑上的一致性。例如,不管上面的例子里面QQ币转账的事务成功还是失败,都应该保证事务结束后qq_account表中bill和tom的QQ币总额为1000点。

----- Isolation(隔离性):指的是在并发的环境之中,当不同的事务同时操作相同的数据时,每个事务都有各自的完整数据空间,这里涉及的事情就多了,我想在后面单独总结成一篇文章。

----- Durability(持久性):指的是只有事务成功结束,它对数据库所做的更新就必须永久保存下来。即使发生系统崩溃,重新启动数据库后,数据库还能恢复到事务成功结束时的状态。 </td> <td width="182" valign="top"> </td> </tr> </table>

事务的ACID只是一个抽象的概念,具体是由RDBMS来实现的。数据库管理系统用日志来保证事务的原子性、一致性和持久性。日志记录了事务对数据库所做的更新,如果某个事务在执行过程中发生了错误,就可以根据日志,撤销事务对数据库已经做的更新,使数据库回退到执行事务前的初始状态。所以不论讲到什么数据库,都会有专门的一章来讲日志,呵呵,终于从本质上明白了日志的作用。

至于事务的隔离性,RDBMS则是采用锁机制来实现的。当多个事务同时更新数据库中的临界数据时,只允许持有锁的事务才能更新该数据,其他事务必须等待,直到前一个数据释放了锁,其他事务才可能有机会来进行更新,这和我们在OS中学的进程的并发时所谈到的锁机制原理差不多。

上面提到了数据库事务的ACID特性,那么谁来保证数据库事务具有ACID呢?其实,只要向数据库系统声明一个事务,数据库系统就会自动保证事务的ACID特性的。那么下面从抽象的概念上来看看怎么声明事务:
-- BEGIN 事务的开始边界
-- COMMIT 事务的正常结束边界,提交事务,永久保存被事务更新后的数据库状态。
-- ROLLBACK 事务的异常结束边界,撤销事务,使数据退回到执行事务前的初始状态。

数据库系统支持以下两种事务模式:
-- 自动提交模式:每个SQL语句都是一个独立的事务,当数据库系统执行完一个SQL语句后,会自动提交事务。
-- 手工提交事务:必须由数据库的客户端显式指定事务开始边界和结束边界。

以MySQL为例,如果我们启动一个MySQL客户端,就会得到一个独立的数据库连接。每个数据库连接都有一个全局变量@@autocommit,表示当前的事务模式,它有两个可选值:

-- 0:表示手工提交模式。
-- 1:默认值,表示自动提交模式。

可以通过SQL命令 SELECT @@autocommit 来查看当前的事务模式。如果想要把当前的事务模式改为手工提交模式,可以使用以下SQL命令 SET autocommit=0 来实现。

还要多说几句,在MySQL中,数据库表分为三种类型:INNODB、BDB、MyISAM。其中前两种类型的表支持数据库事务,而MyISAM类型的表不支持事务。在使用CREATE TABLE命令创建表时默认的类型为MyISAM,如果希望更改的话可以通过如下DLL来进行:

ALTER TABLE table_name TYPE=INNODB;
如果希望在创建时就指定表的类型,可以通过以下DLL来进行:

CREATE TABLE table_name(
.....
.....
) TYPE=INNODB;

好了,下面我们来看几个在实践应用程序中声明事务的模板。

1.在MySQL.exe客户端程序中声明事务
我们以一条插入语句为例,当然如果是自动提交模式的话,每一个SQL语句都是一个单独的事务,我们直接运行以下语句就可以了:
mysql>INSERT INTO qq_account VALUES('leon', 1000);

下面我们看看在手动模式下的命令:
mysql>SET autocommit=0;
mysql>BEGIN;
mysql>INSERT INTO qq_account VALUES('leon', 1000);
mysql>COMMIT;

这样一个事务就完成了,下面我们看一看如何撤销一个事务,很简单也许你已经猜到了吧
mysql>BEGIN;
mysql>INSERT INTO qq_account VALUES('leon', 1000);
mysql>ROLLBACK;

2.在JDBC API中声明事务
没有什么难度,我们直接来看看代码:
Connection con = null;
Statement stmt = null;
try {
con = DriverManager.getConnection(dbUrl,dbUser,dbPwd);
//设置手工提交事务模式
con.setAutoCommit(false);
stmt = con.createStatement();
stmt.executeUpdate("......");
//提交事务
con.commit();
} catch(SQLException e) {
if(con != null) {
try {
//操作不成功则撤销事务
con.rollback();
} catch(SQLException e) {
......
}
}
......
} finally {
if(con != null) {
try{
if(stmt != null) {
try {
stmt.close();
} catch(SQLException e) {
......
}
}
con.close();
} catch(SQLException e) {
......
}
}
}
没有什么可再说的了,代码详细得已经像是一篇技术文章了,需要注意的是对于新建的Connection实例来说,在默认的情况下采用自动提交的事务模式,通过方法 con.setAutoCommit(false)来设置手动提交模式。

3.在Hibernate API中声明事务
Hibernate封装了JDBC API和JTA API,尽管应用程序可以绕过Hibernate API,直接通过JDBC API和JTA API来声明事务,但是这不利于跨平台的开发,下面我们只考虑Hibernate API的实现:
Configuration config = new Configuration().configure();
SessionFactory factory = config.buildSessionFactory();
Session session = factory.openSession();
Transation tx = null;
try {
//开始一个事务
tx = session.beginTransaction();
Account account = new Account("leon",1000);
session.save(account);
//提交事务
tx.commit();
} catch(HibernateException e) {
if(tx != null) {
try {
//操作不成功则撤销事务
tx.rollback();
} catch(HibernateException e) {
......
}
}
......
} finally {
if(session != null) {
try {
session.close();
} catch(HibernateException e) {
......
}
}
}
上面的代码和JDBC API很相像,值得注意的是,在任何时候,一个Session只允许有一个未提交的事务。以下代码对于一个Session同时声明了两个未提交的事务,这是不允许的:
tx1 = session.beginTransaction();
tx2 = session.beginTransaction();
......
tx1.commit();
tx2.commit();

关于事务并发问题,当初在学习DB2的时候,就一直想弄明白,可是当时是为了应试,老师也没有讲得太明白,所以一直决心要总结一下,后来呢,就一直拖到了今天...闲话少讲了,开始正题吧!

在并发的环境之中,一个数据库系统会同时为各种各样的客户端提供服务,对于同时运行的多个事务,当这些事务访问数据库中相同的数据时如果没有采取必要的隔离机制,就会出现各种并发问题,可以把这些问题归纳为一下五种:

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

这样的定义实在太过死板,而且让人也很难理解,还是通过一个例子来仔细的分析一下每一种并发问题,以下引用孙卫琴老师的《精通Hibernate:JAVA对象持久化技术详解》里面的例子,个人认为这个例子很不错。

假设现在一所银行有一个取款事务和一个支票转账事务操作同一个银行账户的情形,首先我们假设两个事务顺序执行,而不是并发执行,那麽整个的步骤如下:
---- 银行客户在银行前台请求取款100元,出纳员先查询账户信息,得知存款余额为1000元。
---- 出纳员判断存款余额大于取款额,支付给客户100元,并将账户上的存款余额改为900元。
---- 出纳员处理一张转账支票,该支票向此账户汇入100元,出纳员先查询账户信息,得知存款余额为900元。
---- 出纳员将存款余额改为1000元。

可以很清楚的看到,如果顺序的执行这两个事务,不会出现任何问题,可是现实之中并非如此简单,如果两个事务分别由两个出纳员同时执行那麽可能就会出现并发的问题,下面我们分别来介绍:

1.第一类丢失更新

时 间 取款事务 转账事务
--------------------------------------------------------------------------------------------
T1 开始事务
T2 开始事务
T3 查询账户余额为1000元
T4 查询账户余额为1000元
T5 汇入100元把余额改为1100元
T6 提交事务
T7 取出100元把余额改为900元
T8 撤销事务余额恢复为1000元
----------------------------------------------------------------------------------------------
如果按照以上的时间次序并发执行的话,由于支票转账事务对存款余额所做的更新被取款事务的撤销所覆盖,所以客户会损失100元。

2.脏 读


时 间 取款事务 转账事务
----------------------------------------------------------------------------------------------
T1 开始事务
T2 开始事务
T3 查询账户余额为1000元
T4
T5 取出100元把余额改为900元
T6 查询账户余额为900元(脏读)
T7 撤销事务余额恢复为1000元
T8 汇入100元把余额改为1000元
T9 提交事务
----------------------------------------------------------------------------------------------
由于支票转账事务查询了取款事务未提交的更新数据,并且在这个查询结果的基础上进行了更新操作,如果取款事务最后被撤销 ,会导致银行客户损失100元。

3.虚 读
下面我们另举一个例子来看看什么是虚读。
时 间 注册事务 统计事务
----------------------------------------------------------------------------------------------
T1 开始事务
T2 开始事务
T3 统计注册客户总数为10000人

T4 注册一个新用户
T5 提交事务
T6 统计注册客户总数为10001人
T7 到底哪一个统计数据有效?
-----------------------------------------------------------------------------------------------

统计事务无法相信查询的结果,因为查询结果是不确定的,随时可能被其他事务改变。
对于实际应用,在一个事务中不会对相同的数据查询两次,假定统计事务在T3时刻统计注册客户的总数,执行SELECT语句,在T6时刻不再查询数据库,而是直接打印统计结果为10000,这个统计结果与数据库当中数据是有所出入的,确切的说,它反映的是T3时刻的数据状态,而不是当前的数据状态。应该根据实际需要来决定是否允许虚读。以上面的统计事务为例,如果仅仅想大致了解一下注册客户总数,那麽可以允许虚读;如果在一个事务中,会依据查询结果来做出精确的决策,那麽就必须采取必要的事务隔离措施,避免虚读。

4.不可重复读

时 间 取款事务 转账事务
----------------------------------------------------------------------------------------------
T1 开始事务
T2 开始事务
T3 查询账户余额为1000元
T4 查询账户余额为1000元
T5 取出100元把余额改为900元
T6 提交事务
T7 查询账户余额为900元
T8 余额到底是1000元还是900元?
-----------------------------------------------------------------------------------------------

如上面所示,如果支票转账事务两次查询账户的存款余额,但得到了不同的查询结果,这使得银行出纳员无法相信查询结果,因为查询结果是不确定的,随时可能被其他事务改变。

5.第二类丢失更新

时 间 取款事务 转账事务
----------------------------------------------------------------------------------------------
T1 开始事务
T2 开始事务
T3 查询账户余额为1000元
T4 查询账户余额为1000元
T5 取出100元把余额改为900元
T6 提交事务
T7
T8 汇入100元把余额改为1100元
T9 提交事务
----------------------------------------------------------------------------------------------
每个事务都不知道其他事务的存在,最后一个事务对记录所做的更新将覆盖由其他事务对记录所做的已提交的更新。上面的例子里由于支票转账事务覆盖了取款事务对存款余额所做的更新,导致银行最后损失了100元,哈哈,你是不是开始喜欢第二类丢失更新了呢?
怎样才能消除这些并发问题呢?请听下回.......

</td> </tr> <tr>


↑返回目录
前一篇: 解析csv格式的java函数
后一篇: 利于ThreadLocal模式管理Session