概念:
事务(Transaction):其实指的一组操作,里面包含许多单一的逻辑,只要有一个逻辑没有执行成功,那么都算失败。所有的数据都回归到最初的状态(回滚)。
为什么要有事务:
为了确保逻辑的成功。例如:银行转账
注意:事务只是针对连接连接对象,如果再开一个连接对象,那么那是默认的提交。
一.使用代码方式演示事务
1) 关闭自动提交的设置:conn.setAutoCommit(false); -----(写在事务开始前)
2) 提交事务:conn.commit(); -----(写在事务结束后)
3) 回滚事务:conn.rollback(); --------(写在捕获异常中)
例1:用数据库修改演示事务
第一步:导入连接包,并添加路径:
第二步:部署配置文件,并新建JDBC工具类
工具类:
package tool;
import java.io.InputStream;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.Properties;
public class JDBCUtils {
private JDBCUtils(){};
private static Connection con;
private static String driverClass;
private static String url;
private static String username;
private static String password;
static{
try{
readConfig();
Class.forName(driverClass);
con = DriverManager.getConnection(url, username, password);
}catch(Exception ex){
throw new RuntimeException(ex+"数据库连接失败");
}
}
private static void readConfig() throws Exception{
InputStream in = JDBCUtils.class.getClassLoader().getResourceAsStream("database.properties");
Properties pro = new Properties();
pro.load(in);
driverClass = pro.getProperty("driverClass");
url = pro.getProperty("url");
username = pro.getProperty("username");
password = pro.getProperty("password");
}
public static Connection getConnection(){
return con;
}
public static void close(Connection con,Statement stat){
if(stat != null){
try{
stat.close();
}catch(SQLException ex){};
}
if(con != null){
try{
con.close();
}catch(SQLException ex){};
}
}
public static void close(Connection con,Statement stat,ResultSet rs){
if(stat != null){
try{
stat.close();
}catch(SQLException ex){};
}
if(con != null){
try{
con.close();
}catch(SQLException ex){};
}
if(rs != null){
try{
rs.close();
}catch(SQLException ex){};
}
}
}
第三步:数据库数据:
第四步:测试类
package com.test;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import org.junit.Test;
import tool.JDBCUtils;
public class TestDemo {
@Test
public void testTransaction(){
Connection conn = null;
PreparedStatement ps = null;
ResultSet rs = null;
try {
conn = JDBCUtils.getConnection();
//连接。事务默认就是自动提交的。关闭自动提交
conn.setAutoCommit(false);
String sql = "update account set money = money- ? where id = ?";
ps = conn.prepareStatement(sql);
//给id为1的人员-100
ps.setInt(1, 100);
ps.setInt(2, 1);
ps.executeUpdate();
int a = 10/0; (插入的异常数据)
//给id为2的人+100
ps.setInt(1, -100);
ps.setInt(2, 2);
ps.executeUpdate();
//成功:提交事务
conn.commit();
} catch (SQLException e) {
//失败:回滚事务
try {
conn.rollback();
} catch (SQLException e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
}
e.printStackTrace();
}finally{
JDBCUtils.close(conn,ps,rs);
}
}
}
分析:
如果没有插入事务的话,由于中间插入的异常数据,导致异常之后的代码不能被执行,但是异常数据之前的代码仍可执行。但是插入事务后,一旦出现异常数据,整个代码都不会被执行
二.事务的特性(ACID)
1.原子性
事务中包含的逻辑,不可分割
2.一致性
事务执行前后,数据完整性
3.隔离性
事务在执行期间不应该受到其他事务的影响
4.持久性
事务执行成功,那么数据应该持久的保存到磁盘上
事务安全问题
- 读 问题
脏读
不可重复读
幻读 - 写问题
丢失更新(悲观锁、乐观锁)
事务隔离级别(由上而下依次提高):
- Read Uncommitted —(读未提交)
引发问题:脏读 - Read Committed —(读已提交)
解决:脏读 引发:不可重复读 - Repeatable Read —(重复读)
解决:脏读、不可重复读 未解决:幻读 - Serializable —(可串行化)
解决:脏读、不可重复读、幻读
脏读:一个事务读到另外一个事务还未提交的数据
不可重复读: 一个事务读到了另外一个事务提交的数据,造成了前后两次查询结果不一致。
可重复读:一个事务不受另外一个事务的影响,即使另外一个事务执行更新,也不受其影响,前后两次查询结果依然一致。
幻读:一个事务读到了另一个事务insert的数据,造成前后查询结果不一致。
可串行化:如果有一个连接的隔离级别设置为了串行化,那么谁先打开了事务,谁就有了先执行的权利,谁后打开事务,谁就只能等着,等前面的那个事务,提交或者回滚后,才能执行。但是这种隔离级别一般比较少用,容易造成性能上的问题,效率比较低。
按效率划分,从高到低:
读未提交 > 读已提交 > 重复读 > 可串行化
按拦截程度,从高到低:
可串行化 > 重复读 > 读已提交 > 读未提交
读未提交 演示:
-
设置A窗口的隔离级别为: 读未提交
-
两个窗口都分别开启事务
读已提交 演示
-
设置A窗口的隔离级别为 读已提交
-
AB两个窗口都开启事务,在B窗口执行更新操作
-
在A窗口执行的查询结果不一致,一次是在B窗口提交事务之前,一次是在B窗口提交事务之后
事务的丢失更新
解决丢失更新的方法有两种:
- 乐观锁
- 悲观锁
悲观锁:
乐观锁(需要程序员手动):