1. 数据库事务
1.1 是什么
什么是事务,可以阅读我写的有关数据库 事务 这篇文章。从事务的四大特性来说,事务机制是为了保证数据的一致性,事务是最小的执行单元,要么全部成功要么全部不成功。
1.2 为什么
众所周知,SQL语言是一种操作数据库的非过程化语言,无疑它的功能是很强大的。比如一个小小的select语法结构就可以查询到我们想要的任何数据,多么简洁方便。
然而业务是复杂的,一条SQL并不能满足我们的需求,往往一个业务的执行需要多条SQL语句的配合使用来完成。一个写操作会改变数据库的数据,多个写操作如果不能保证都成功那么数据必然不是完整的。
为了保证数据的完整性就必须保证多条SQL语句的执行要么都成功要么都不成功,因此引出数据库事务机制。事务能够保证我们在解决复杂业务时,某一步出错不会造成数据的丢失或者增加。
转账案例
- 例如现在有李白和杜甫两个人的存款在数据库中存在记录,他们各有500元的存款如下:
- 李白要向杜甫转账200元,那我们必须修改两个人的存款,李白存款减200,杜甫存款加200,SQL如下:
# 李白存款减200
update `user` set money = money - 200 where name = '李白';
# 杜甫存款加200
update `user` set money = money + 200 where name = '杜甫';
- 针对这个转账业务我们需要使用两条更新语句来完成,然而我们的程序只能先执行完一条再执行另一条,也就是说程序执行需要一个过程。
- 如果在执行 李白存款减200后,杜甫存款加200前这个极其短暂的时间空隙时出现了故障,导致我们只执行了减款操作而没有执行加款操作,那么200元岂不是不翼而飞。
- 李白和杜甫就不会打起来吗?李白说:“我转了,因为我的存款少了200元”,杜甫说:“你没转,因为我的存款没有多200元”。系统无缘无故吞了200元,现在谁还敢存钱。
- 问题在于两条SQL语句我们只成功执行了一条SQL语句,导致数据库的金额数据总量少了200元,这样直接影响了数据的一致性。为了避免出现这样的情况,引入事务机制,将多条SQL语句加持在一个最小执行单元中,执行成功则业务成功,执行失败就貌似我们没有执行过,数据不会发生丢失或增加。
2. JDBC事务支持
JDBC作为java连接数据库的API自然也提供了相关方法来操作事务:
-
connection.setTransactionIsolation(int level):设置事务隔离级别
-
connection.setAutoCommit(boolean ):设置是否自动提交事务,默认自动提交,类似于是否开启事务。
-
connection.setSavepoint(String):设置保存点
-
connection.commit():提交事务
-
connection.rollback():回滚事务,默认回滚到最初,也可以指定回滚到指定保存点。
3. JDBC事务案例
基于李白向杜甫转账的例子,我们通过JDBC来进行操作,探究事务是否可以被控制住。
3.1 数据库环境
# 新建用户表
CREATE TABLE IF NOT EXISTS `user`(
`pk_id` INT AUTO_INCREMENT,
`name` VARCHAR(20) NOT NULL,
`money` DOUBLE,
CONSTRAINT `pk_user` PRIMARY KEY(`pk_id`)
);
# 添加用户数据
INSERT INTO
`user`(`name`, `money`)
VALUES
('李白', 500.0),
('杜甫', 500.0);
3.2 创建项目
- 通过IDEA基于Maven构建普通Java工程
3.3 添加依赖
<!-- 依赖管理 -->
<dependencies>
<!-- mysql -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.49</version>
</dependency>
<!-- junit -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.1</version>
</dependency>
</dependencies>
3.4 转账异常
@Test
public void test01() throws ClassNotFoundException, SQLException
{
// 定义连接参数
String driverName = "com.mysql.jdbc.Driver";
String url = "jdbc:mysql://127.0.0.1:3306/zw_test";
String username = "root";
String password = "root";
// 1.注册驱动
Class.forName(driverName);
// 2.获取连接
Connection connection = DriverManager.getConnection(url, username, password);
// 3.增删改查
Statement statement = connection.createStatement();
// 李白扣钱
String sql1 = "update user set money = money - 200 where name = '李白'";
statement.executeUpdate(sql1);
// 模拟发生意外
int i = 1/0;
// 杜甫加钱
String sql2 = "update user set money = money + 200 where name = '杜甫'";
statement.executeUpdate(sql2);
// 4.关闭资源
statement.close();
connection.close();
}
- 转账前
- 转账后
3.5 转账正常
@Test
public void test02() throws ClassNotFoundException, SQLException
{
// 定义连接参数
String driverName = "com.mysql.jdbc.Driver";
String url = "jdbc:mysql://127.0.0.1:3306/zw_test";
String username = "root";
String password = "root";
// 1.注册驱动
Class.forName(driverName);
Connection connection = null;
Statement statement = null;
try
{
// 2.获取连接
connection = DriverManager.getConnection(url, username, password);
// 3.增删改查
// 关闭事务的自动提交
connection.setAutoCommit(false);
// 获取操作对象
statement = connection.createStatement();
// 李白扣钱
String sql1 = "update user set money = money - 200 where name = '李白'";
statement.executeUpdate(sql1);
// 模拟发生意外
int i = 1/0;
// 杜甫加钱
String sql2 = "update user set money = money + 200 where name = '杜甫'";
statement.executeUpdate(sql2);
// 提交事务
connection.commit();
}
catch (Exception e)
{
e.printStackTrace();
// 发生异常,回滚事务
connection.rollback();
}
finally
{
// 4.关闭资源
statement.close();
connection.close();
}
}
- 转账前
- 转账后