什么是事务
- 是以一种可靠、一致的方式,访问和操作数据库中数据的程序单元
原则
- 原子性:一个事务要么全部成功,要么全部失败
- 一致性:事务完成以后,状态改变是一致的,一致性一般是通过结果来呈现的
- 隔离性:在不同事务试图去操作同一份数据的时候,事务之间的隔离性
- 持久性:数据提交以后,事务操作的结果才会永久保存到数据库当中
范例 :a给b转账100元
一致性解读:不会出现a账户-100而b账户没有增加100的情况,整体的状态是一致的,总的状态不会平白无故地改变
隔离性解读:在a给b转账的过程中,b进行查余额的操作,那么b得到到结果将由数据库设定得数据库隔离级别来决定。
使用sql进行数据管理
事务1:
START TRANSACTION;
UPDATE user_transaction set amount = amount -100 WHERE username = 'user1';
UPDATE user_transaction set amount = amount + 100 WHERE `username` = 'user2';
COMMIT
复制代码
事务2
SELECT * from user_transaction;
复制代码
事务3
START TRANSACTION;
SELECT * from user_transaction;
SELECT * FROM user_transaction WHERE username = 'user1';
COMMIT
复制代码
结果展示
- 在不修改数据库默认隔离级别的情况下,只有当执行完commit之后,其它事务才能查询到数据库的数据库的更新。
- 可重复读的展示(可重复读:一个事务中读取到的数据和事务开启的时刻是一致的) 事务3开启事务以后,开启事务1并执行事务1的第一条更新语句执行第一条查询语句,得到结果如下 执行事务1的第二条更新语句并提交事务,然后执行事务3的第2条查询语句 我们可以看到在可重读的隔离级别下,一个事务内多次读取的数据结果和事务开始时的结果是一致的,哪怕其它事务已经对原有数据进行更新并提交。
查询数据库中的设置的隔离级别,可以发现mysql默认的数据库隔离级别是可重读
select @@GLOBAL.tx_isolation,@@tx_isolation;
复制代码
修改数据库当前事务隔离级别为脏读
set session transaction isolation level read uncommited
复制代码
结果
- 事务1开启事务并执行第一条更新语句,事务2可以看到事务1中执行的更新的数据,即使事务并没有提交,即可以读取到脏数据。
mysql数据库的四种隔离级别
- 读未提交
- 读提交
- 可重读
- 序列读
jdbc操作事务
事务1:开启事务->两次更新->提交事务
public class JdbcTransaction {
public static void main(String args[]) throws SQLException {
Connection connection = getConn();
//关闭自动提交,相当于开启一个事务
connection.setAutoCommit(false);
//减少账户余额
String sql1 = "UPDATE user_transaction set amount = amount -100 WHERE username = ? ";
PreparedStatement ps1 = connection.prepareStatement(sql1);
ps1.setString(1, "user1");
ps1.executeUpdate();
//若抛出异常,事务会回滚
//throwException();
//增加账号余额
String sql2 = "UPDATE user_transaction set amount = amount + 100 WHERE `username` = ?";
PreparedStatement ps2 = connection.prepareStatement(sql2);
ps2.setString(1, "user2");
ps2.executeUpdate();
// 提交事务
connection.commit();
ps1.close();
ps2.close();
}
private static Connection getConn() {
String driver = "com.mysql.jdbc.Driver";
String url = "jdbc:mysql://localhost:3306/test";
String username = "root";
String password = "root";
Connection conn = null;
try {
Class.forName(driver); //classLoader,加载对应驱动
conn = (Connection) DriverManager.getConnection(url, username, password);
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (SQLException e) {
e.printStackTrace();
}
return conn;
}
private static void throwException() throws SQLException {
throw new SQLException();
}
复制代码
事务2: 开启事务->进行查询操作->根据查询的结果再进行更新->提交
public class JdbcTransaction2 {
public static void main(String args[]) throws SQLException {
Connection connection = getConn();
// 关闭自动提交,相当于开启一个事务
connection.setAutoCommit(false);
// 减少账户余额,加悲观锁
String query1 = "select * from user_transaction for update";
PreparedStatement ps1 = connection.prepareStatement(query1);
ResultSet resultSet = ps1.executeQuery();
Integer myAmount = 0;
while (resultSet.next()){
String username = resultSet.getString(2);
Integer amount = resultSet.getInt(3);
System.out.println("username =" + username+ " amount = " + amount);
if (username.equals("user1")){
myAmount = amount;
}
}
// 根据查询出来的结果去更新会有什么问题?
/**
* 1. 开启JdbcTransaction中的事务,执行更新操作但不commit
* 2. 本事务的更新操作将卡在更新数据之前,直到上一个事务提交,交出该数据锁的权限
* 3. 此时更新的数据将是根据前面查得的数据进行更新的,那么此时更新的依据将会是旧的数据
*
* 解决:开启事务,锁住查询出来的数据,若其它事务正在对本事务需要查询的数据进行操作,那么本事务等待直至
* 其它事务commit
*/
// 如果有其它事务正在操作这条数据,那么此处将会等到其它事务提交以后才能继续往下执行
// 根据mysql的内部机制,更新同一条数据的两个事务不能同时执行,需要等待其中一个事务执行完
String sql2 = "UPDATE user_transaction set amount = ? WHERE username = ? ";
PreparedStatement ps2 = connection.prepareStatement(sql2);
ps2.setString(1,String.valueOf((myAmount+100)));
ps2.setString(2,"user1");
ps2.executeUpdate();
System.out.println("进行数据更新");
// 提交事务
connection.commit();
ps1.close();
ps2.close();
}
private static Connection getConn() {
String driver = "com.mysql.jdbc.Driver";
String url = "jdbc:mysql://localhost:3306/test";
String username = "root";
String password = "root";
Connection conn = null;
try {
Class.forName(driver); //classLoader,加载对应驱动
conn = (Connection) DriverManager.getConnection(url, username, password);
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (SQLException e) {
e.printStackTrace();
}
return conn;
}
}
复制代码
数据库:
样例示范:执行事务1,但先不提交(断点停留在commit( ) 处),开启事务2,会发现事务2会停留在第二条sql(更新数据的sql)前,原因是事务1正在对同一份数据进行更新,事务2无法获取到数据的锁;这时提交事务1,事务2也随之提交。这时会出现的问题是事务2中是根据旧数据为依据进行更新的,这种情况在生产中是不允许出现的。 解决方式:
- 对事务2中查询出来的数据进行加锁,这里要注意的是加锁的数据一定要是我们需要查询的指定数据,所以这里一定要加上where条件,否则有可能导致锁全表,这样将给系统性能带来很大的影响。开启事务1以后,事务2直到事务1提交以后才拿到查询结果,因为要先获取该行数据的锁,这样则不会出现读旧数据去更新的情况。