事务,程序猿不知道会出大事的

事务管理是应用程序中不可缺少的一部分,合理应用事务管理对于整个应用系统来说至关重要。事务是为了解决数据安全操作而提出的,通过事务可以控制用户对数据的安全访问。那么,事务是怎么产生的?如何实现事务管理?如果不使用事务管理会发生什么问题?我们今天就来了解一下。

事务产生的背景

我们先来看一个例子:银行中提供了转帐业务,现在A帐户要转帐2000元给B帐户。在这个转帐业务中,需要进行两次数据库更新操作。A帐户首先要减去2000元,然后B帐户要增加2000元。如果这时网络出现了故障,A帐户减去2000元成功,但B帐户由于网络故障,并没有增加2000元。那么这时,问题就大了。A帐户认为钱已经给B帐户了,因为A帐户的钱减少了。B帐户认为A帐户没有转帐,因为B帐户的钱没有增加。

我们再来看一个例子:用户去购物网站购物,当用户买了一大堆的商品后,提交订单。对服务器而言,在订单中,应该记录用户的姓名、电话、送货地址等等信息。除此之外,还应该记录该订单中用户买了哪些商品,每种商品购买的数量等信息。如果这时,订单信息记录成功,但服务器忽然死机。这样,就会导致订单信息记录成功。但是,用户购买的商品信息并没有记录。

从上面的示例可以看出,在一个业务中,如果有多次数据库更新操作时,这些操作可能会因为某些问题(比如,网络故障,停电,死机等等),而导致部分操作成功,部分操作失败的情况。如果不解决这个问题,会造成数据库数据的混乱,给用户造成巨大的损失。

从第一个示例可以看出,A帐户钱减少了2000元,B帐户的钱没有增加。这样,会导致A帐户白白损失2000元。如果数据安全问题不解决,一旦转帐金额巨大,客户会损失很多钱。

从第二个示例可以看出,当商家看到订单时,无法确认用户在该订单中买了哪些商品,也就无法发货。用户也无法收到自己购买的商品。

那么,如何解决这些问题呢?于是,在业务层架构模式中,人们提出事务脚本的概念。

事务脚本

那么什么是事务脚本呢?首先,我们来看一下脚本。脚本也就是方法,在很多时候,一个业务中,可能需要进行很多的操作,比如:查询、更新、数据处理等操作。按照迪米特法则,软件实体之间应该尽量减少交互。所以,我们应该把这些属于一个业务的操作封装成一个方法,这就是所谓的脚本。事实上,我们将转帐业务中的,A帐户减钱,B帐户加钱的这些操作,封装成转帐方法;以及将订单信息的添加,和该订单购物明细添加的这些操作,封装成提交订单方法;都称之为脚本。

但是,在脚本中,很可能有多次数据库更新操作。这些操作可能会部分成功,部分失败。这样,会导致数据库数据的混乱。那么如何防止这一点呢?这就需要进行事务管理。

事务是用户定义的一个操作序列。事务认为,这些操作序列是一个不可分割的工作单位。事务有四个特点:原子性、一致性、隔离性和持久性。

事务的原子性,表示事务执行过程中,用户定义的操作序列要么全部执行成功,要么全部执行失败。

事务的一致性,表示当事务执行失败时,所有被该事务影响的数据都应该恢复到事务执行前的状态,这称为事务回滚。

事务的隔离性,表示在事务执行过程中对数据的修改,在事务提交之前对其他事务不可见。

事务的持久性,表示事务完成之后,对系统的影响是永久性的。如果已提交的数据在事务执行失败时,数据的状态都应该正确。

从事务的特点可以看出,通过事务,数据库能将逻辑相关的一组操作绑定在一起,以便数据库保持数据的完整性。当一个操作序列中,多个操作进行的时候,某个操作执行失败。为了保持数据的完整性,需要使用事务回滚。让该事务影响的数据恢复到事务执行前的状态,从而保证数据的完整性和安全性,防止出现数据混乱。

综上所述,事务脚本的含义是,将一个业务中所有的操作封装成一个方法。保证方法中,所有数据库更新操作(查询操作中,数据库数据没有发生变化,所以不纳入事务范围),要么同时成功,要么同时失败。不允许部分成功,部分失败这样引起数据混乱的操作。

如何实现事务管理?

那么,如何实现事务管理呢?如何在一系列操作时,有一个操作失败时,回滚到事务执行前的状态呢?

在java中,数据库操作都是使用JDBC完成的。在事务的控制上,可以使用Connection对象来完成。JDBC Connection接口提供了两种事务模式:自动提交和手工提交。默认情况下,采用自动提交方式。也就是在

1
PreparedStatement.executeUpdate();

方法执行后,马上更新数据库中的数据。那么,很明显,如果采用自动提交方式,无法实现对事务的管理。
在JDBC中,要实现对事务的控制,需要有两个条件:


a、在事务中所有的数据更新操作,必须使用同一个连接对象。
b、将事务提交方式设置为手工提交。在所有操作都成功以后,再手工提交事务,更新数据库数据。如果操作失败,一般都会有异常发生。那么,应该在catch块中,进行事务回滚操作。

在Connection接口中,提供了

1
public void setAutoCommit(boolean autoCommit);

方法,设置提交的方式。当boolean值设置为true时,表示自动提交,这也是默认的方式。如果设置为false,表示手工提交。

在手工提交方式下,即使PreparedStatement.executeUpdate()方法执行以后,也不会对数据库数据进行更新。只有在调用了Connection的

1
public void commit();

方法以后,数据库数据才会进行更新。

那么,在某一个操作出现异常后,如何进行事务回滚呢?在Connection接口中,提供了

1
public void rollback();

方法,可以让该事务影响的数据恢复到事务执行前的状态。
现在,以添加订单和订单明细为例,我们来看一下,如何在业务层中实现事务管理。首先,新建连接对象。

1
2
3
4
5
6
7
8
9
10
11
12
public static Connection getConnection(){
try{
Class.forName("com.mysql.jdbc.Driver");
Connection con = DriverManager.getConnection("jdbc:mysql://localhost:3306/test?characterEncoding=utf-8","root","123");
//设置提交方式为手工提交
con.setAutoCommit(false);
return con;
}catch(Exception e){
e.printStackTrace();
}
return null;
}

其次,在业务方法中,调用持久操作时,让每个DAO使用同一个连接对象。
OrderDao代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
public class OrderDaoImpl implements IOrderDao{
private Connection con;
public void setConnection(Connection con){
this.con = con;
}
public int addOrder(OrderBean bean){
PreparedStatement ps = con.prepareStatement("……");
……//执行添加方法
ps.executeUpdate();
}
}

OrderInfoDao代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
public class OrderInfoDao implements IOrderInfoDao{
private Connection con;
public void setConnection(Connection con){
this.con = con;
}
public int addOrderInfo(OrderInfoBean bean){
PreparedStatement ps = con.prepareStatement("……");
……//执行添加方法
ps.executeUpdate();
}
}

业务方法代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class OrderService{
private IOrderDao orderDao = new OrderDaoImpl();
private IOrderInfoDao orderInfoDao = new OrderInfoDaoImpl();
public void addOrder(OrderBean bean,List<OrderInfoBean> orderInfoList){
//得到连接对象
Connection con = DBUtil.getConnection();
try{
//设置每个DAO的连接对象。保证每个DAO操作数据时,使用同一个连接对象
orderDao.setConnection(con);
orderInfoDao.setConnection(con);
……//执行业务操作
//操作全部成功,提交事务
con.commit();
}catch(Exception e){
e.printStackTrace();
//在try块中出现异常,则回滚事务,回到事务执行前的状态
con.rollback();
}finally{
……//关闭连接
}
}
}

当然,在业务层中不应该出现Connection之类的持久层组件。不过,我们可以通过代理模式,将事务管理从实际业务中分离出来,让业务层只关注业务操作,而不用关心事务的管理。

总结

  1. 事务管理对于整个应用系统来说至关重要,通过事务可以控制用户对数据的安全访问。
  2. 事务是用户定义的一个操作序列,这些操作序列是一个不可分割的工作单位。事务有四个特点:原子性、一致性、隔离性和持久性。
  3. 事务脚本的含义是,将一个业务中所有的操作封装成一个方法。保证方法中,所有数据库更新操作,要么同时成功,要么同时失败。不允许部分成功,部分失败这样引起数据混乱的操作。
  4. 在JDBC中,要实现对事务的控制,需要有两个条件:
    a. 在事务中所有的数据更新操作,必须使用同一个连接对象。
    b. 将事务提交方式设置为手工提交,在所有操作都成功以后,再手工提交事务,更新数据库数据。如果操作失败,一般都会有异常发生,那么应该在catch块中,进行事务回滚操作。
  5. Connection的setAutoCommit(false)可以实现手工提交,通过commit()方法才能真正更新数据库。通过rollback()方法可以在发生异常时回滚事务。