上一章节,我们学习了数据插入的批量操作,那么下面再来认识一下数据库事务。
下面我们来看一个转账的示例。假设:用户AA向用户BB转账100,如下表:
当用户AA向用户BB转账,就需要执行两个SQL语句,一个是用户AA减少100,另一个是用户BB增加100,SQL如下:
-- 用户AA减少100
mysql> update user_table set balance = balance - 100 where user = 'AA';
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0
-- 用户BB增加100
mysql> update user_table set balance = balance + 100 where user = 'BB';
Query OK, 1 row affected (0.01 sec)
Rows matched: 1 Changed: 1 Warnings: 0
mysql> select * from user_table;
+------+----------+---------+
| user | password | balance |
+------+----------+---------+
| AA | 123456 | 900 |
| BB | 654321 | 1100 |
| CC | abcd | 2000 |
| DD | abcder | 3000 |
+------+----------+---------+
4 rows in set (0.00 sec)
mysql>
下面我们用代码来实现一下这个过程。
// 通用的增、删、改操作
public void update(String sql, Object... args) {
Connection conn = null;
PreparedStatement ps = null;
try {
//1. 使用JDBCUtils获取连接
conn = JDBCUtils.getConnection();
System.out.println(conn);
//2.预编译sql语句,返回PreparedStatement的实例
ps = conn.prepareStatement(sql);
//3.填充占位符
for (int i = 0; i < args.length; i++) {
ps.setObject(i + 1, args[i]);
}
//4.执行
ps.execute();
} catch (Exception e) {
e.printStackTrace();
} finally {
//5.使用JDBCUtils关闭资源
JDBCUtils.closeResource(conn, ps);
}
}
// 用户AA向用户BB转账
@Test
public void test01() {
String sql1 = "update user_table set balance = balance - 100 where user = ?";
update(sql1, "AA");
String sql2 = "update user_table set balance = balance + 100 where user = ?";
update(sql2, "BB");
System.out.println("转账成功");
}
执行如下:
查看数据库的数据:
成功转账。
我们首先将两个用户的金额bablance 恢复到1000,然后增加模拟异常 ,执行如下:
查看数据库如下:
那么有没有什么办法来解决这个问题呢?
当然有,这时候就要引入数据库的事务操作了。
如果要使用事务,那么在操作数据库的时候,就要保持一个连接,在执行完毕之前,不能关闭资源。
但是上面的 update()
通用更新方法却有 关闭资源 的操作,所以这个地方要修改一下。
//修改 update() 通用更新方法,去除 关闭数据库连接 的操作
public void updateNotClosed(Connection conn, String sql, Object... args) {
PreparedStatement ps = null;
try {
//2.预编译sql语句,返回PreparedStatement的实例
ps = conn.prepareStatement(sql);
//3.填充占位符
for (int i = 0; i < args.length; i++) {
ps.setObject(i + 1, args[i]);
}
//4.执行
ps.execute();
} catch (Exception e) {
e.printStackTrace();
} finally {
//5.使用关闭资源
if (ps != null) {
try {
ps.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
}
//增加事务处理的 转账操作
@Test
public void testTransaction() {
Connection connection = null;
try {
//1.获取数据库连接
connection = JDBCUtils.getConnection();
//2.设置取消数据的自动提交
connection.setAutoCommit(false);
//3.执行转账操作
String sql1 = "update user_table set balance = balance - 100 where user = ?";
updateNotClosed(connection, sql1, "AA");
// 模拟网络异常
System.out.println(10 / 0);
String sql2 = "update user_table set balance = balance + 100 where user = ?";
updateNotClosed(connection, sql2, "BB");
System.out.println("转账成功");
//4.提交数据库数据
connection.commit();
} catch (Exception e) {
e.printStackTrace();
//5.执行出现异常,执行回滚事务
try {
connection.rollback();
} catch (SQLException ex) {
ex.printStackTrace();
}
} finally {
//6.关闭数据库连接
if (connection != null) {
try {
connection.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
}
首先执行一次,转账成功,此时,可以查看mysql的执行日志,如下:
执行完毕之后, mysql 的数据如下:
执行操作之后,mysql的日志如下:
可以看到执行了 更新 AA 用户的账单,但是后续又执行了 rollback 回滚操作,也就是数据没有变化。
此时,mysql的数据如下:
通过上面的例子,只要有事务的控制,就算多条SQL变更数据,通过数据的回滚,就算出现了异常,也可以保证数据的原子性。
//7.恢复默认自动提交的方式
try {
connection.setAutoCommit(true);
} catch (SQLException e) {
e.printStackTrace();
}
因为我们不知道后续的数据库操作要不要使用事务,那么最好在最后的时候,将数据库恢复回默认的自动提交方式。
每启动一个 mysql 程序, 就会获得一个单独的数据库连接. 每个数据库连接都有一个全局变量 @@tx_isolation, 表示当前的事务隔离级别。
查看当前的隔离级别:
SELECT @@tx_isolation;
设置当前 mySQL 连接的隔离级别:
set transaction isolation level read committed;
设置数据库系统的全局的隔离级别:
set global transaction isolation level read committed;
补充操作:
create user tom identified by 'abc123!#@A';
按照上面的命令,下面我们来执行操作演示一遍,单纯看命令行还是没那么直观。
[root@server01 ~]# mysql -uroot -p
..
mysql> create user tom identified by 'abc123!#@A';
Query OK, 0 rows affected (0.01 sec)
好了,此时我们已经在 mysql 钟创建好了一个叫做 tom 的用户,下面使用这个用户来访问 mysql。
mysql> grant select,insert,delete,update on test.* to tom@localhost identified by 'abc123!#@A';
Query OK, 0 rows affected, 1 warning (0.00 sec)
配置之后,使用 tom 用户登录 mysql,可以查看到 test 数据库,如下:
下面我们首先来演示一下mysql默认的隔离级别。
mysql> SELECT @@tx_isolation;
+-----------------+
| @@tx_isolation |
+-----------------+
| REPEATABLE-READ |
+-----------------+
1 row in set (0.00 sec)
mysql>
root 用户 与 tom 用户两者操作:
-- 禁止自动commit提交
mysql> set autocommit=false;
-- 切换使用test数据库
mysql> use test
-- 查询user_table表的数据
mysql> select * from user_table;
root用户操作:
mysql> update user_table set balance = 2500 where user = 'CC';
mysql> commit;
那么问题来了,Tom用户要怎么样才可以查询到 更新后的 数据呢?
其实只需要 Tom 用户也执行一个 commit
,结束本次操作的事务,再次查询就可以看到更新后的数据了。
其实也就是说,mysql的隔离级别限制了每个事务只能查询到在开始之前的更新数据,如果事务没有结束,则不会查询到其他并发导致的数据更新。
上面我们可以看到在事务的过程中,并不能查询其他事务的更新数据,那么我们来修改一下隔离级别,演示一下在事务过程中可以查询到其他事务更新数据的情况。
-- 配置全局修改隔离级别为 read committed
mysql> set global transaction isolation level read committed;
-- 查看当前的隔离级别
mysql> SELECT @@tx_isolation;
-- 禁止自动commit
mysql> set autocommit=false;
-- 切换使用test数据库
mysql> use test;
-- 查询CC用户数据
mysql> select * from user_table where user = 'CC';
-- 更新CC用户的balance
mysql> update user_table set balance = 3000 where user = 'CC';
-- 提交执行
mysql> commit;
从上面的执行过程,我们可以发现 READ COMMITED 的隔离级别,不需要当前事务结束,直接就可以查看其他并发事务更新后的数据。
这种应该是问题最多的事务隔离级别了,因为其他事务只要更新,不需要commit。其他事务直接就查询到了。
-- 配置全局修改隔离级别为 read uncommitted
mysql> set global transaction isolation level read uncommitted;
-- 查看当前的隔离级别
mysql> SELECT @@tx_isolation;
-- 禁止自动commit
mysql> set autocommit=false;
-- 切换使用test数据库
mysql> use test;
-- 查询CC用户数据
mysql> select * from user_table where user = 'CC';
-- 更新CC用户的balance
mysql> update user_table set balance = 4000 where user = 'CC';
上面我们在mysql的命令行演示了隔离级别的操作,下面我们在JAVA代理来演示一下操作。
首先准备下面的代码,然后后面我们来测试一下。
//使用 泛型 编写不同表的通用查询操作
//针对于不同的表的通用的查询操作,返回表中的一条记录
public static <T> T getInstance(Connection conn,Class<T> clazz, String sql, Object... args) throws Exception {
//1. 使用JDBCUtils获取连接
// Connection conn = jdbc.test.JDBCUtils.getConnection();
//2.预编译sql语句,返回PreparedStatement的实例
PreparedStatement ps = conn.prepareStatement(sql);
// 设置占位符
for (int i = 0; i < args.length; i++) {
ps.setObject(i + 1, args[i]);
}
//3.执行,并返回结果集
ResultSet resultSet = ps.executeQuery();
//4.获取返回结果的列数,后续可以用来遍历获取/设置字段值
//4.1 获取结果集的元数据 :ResultSetMetaData
ResultSetMetaData metaData = resultSet.getMetaData();
//4.2 通过ResultSetMetaData获取结果集中的列数
int columnCount = metaData.getColumnCount();
//5.处理结果集
//next():判断结果集的下一条是否有数据,
// 如果有数据返回true,并指针下移;
// 如果返回false,指针不会下移。
if (resultSet.next()) {
//创建泛型对应类的对象
T t = clazz.newInstance();
//获取当前这条数据的各个字段值
for (int i = 0; i < columnCount; i++) {
//获取列值
Object columValue = resultSet.getObject(i + 1);
//获取每个列的列名
String columnName = metaData.getColumnName(i + 1);
//获取每个列的别名
String columnLabel = metaData.getColumnLabel(i + 1);
//给order对象指定的columnName属性,赋值为columValue:通过反射
Field field = clazz.getDeclaredField(columnLabel); // 根据字段别名 获取 Order 的属性
field.setAccessible(true); // 设置允许操作所有权限的数据
field.set(t, columValue); // 设置对象的字段值
}
// 返回 t
return t;
}
//5.关闭资源
ps.close();
resultSet.close();
return null;
}
/**
* @author Aron.li
* @date 2020/10/24 13:54
*/
public class UserTable {
//成员属性
private String user;
private String password;
private Integer balance;
//构造器
public UserTable() {
}
public UserTable(String user, String password, Integer balance) {
this.user = user;
this.password = password;
this.balance = balance;
}
//getter setter
public String getUser() {
return user;
}
public void setUser(String user) {
this.user = user;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public Integer getBalance() {
return balance;
}
public void setBalance(Integer balance) {
this.balance = balance;
}
//toString
@Override
public String toString() {
return "UserTable{" +
"user='" + user + '\'' +
", password='" + password + '\'' +
", balance=" + balance +
'}';
}
}
//设置数据库的隔离级别,并且查询 user_table 数据
@Test
public void testTransitionSelect() throws Exception {
//1.获取数据连接
Connection connection = JDBCUtils.getConnection();
//2.获取当前连接的隔离级别
System.out.println("当前连接的隔离级别:" + connection.getTransactionIsolation());
//3.设置数据库的隔离级别: read commited (不受mysql的隔离级别,只是本次java连接的隔离级别):
connection.setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED);
//4.取消自动commit
connection.setAutoCommit(false);
//5.查询 user_table 的数据
String sql = "select user,password,balance from user_table where user = ?";
UserTable userTable = getInstance(connection, UserTable.class, sql, "CC");
System.out.println(userTable);
//注意:不关闭mysql的连接进行测试。
}
//设置更新CC用户的balance数据,并设置休眠15秒,避免立即断开连接。
//可以在此时查询数据,确认在commit之前的数据查询情况
@Test
public void testTransitionUpdate() throws Exception {
//1.获取数据库连接
Connection connection = JDBCUtils.getConnection();
//2.取消自动提交数据
connection.setAutoCommit(false);
//3.执行更新CC用户的balance数据
String sql1 = "update user_table set balance = balance - 100 where user = ?";
updateNotClosed(connection,sql1, "CC");
//4.休眠15秒,再次期间可以查询CC用户数据,确认balance是否变更
Thread.sleep(15000);
//5.15秒之后,提交数据
connection.commit();
System.out.println("修改结束");
}