TCL—事务控制语言
学习内容
1. 事务
1.1 什么是事务?
- 事务时最小的业务逻辑单元,不可再分
- 事务是恢复的基本单位,也是并发控制的基本单位
- 事务由多条DML语句联合组成,并且要保证事务中所有的DML语句同时成功或同时失败
- 事务的存在是为了保证数据完整性、安全性
1.2 事务的四大特性
- 原子性:事务时最小的工作(执行)单元,不可再分(一个事务要么全做,要么不做)
- 一致性:必须保证事务中的多条DML语句同时成功或同时失败(使数据库从一个一致性状态变到另一个一致性状态)
- 隔离性:事务之间是有隔离性的,是有隔离级别的,详见下个标题
Mysql默认隔离级别是——REPEATABLE-READ(可重复读)
Oracle默认隔离级别是——READ-COMMITTED(读以提交) - 持久性:事务直到数据持久化到硬盘空间才算结束
1.3 事务的四大隔离级别
- Read Uncommitted ----> 读未提交
- Read Committed ----> 读已提交
- Repeatable Read ----> 可重复读
- Serializable ----> 序列化读
设置和查看事务的隔离级别
设置事务的会话隔离级别
SET SESSION TRANSACTION ISOLATION LEVEL read uncommitted; --读未提交
SET SESSION TRANSACTION ISOLATION LEVEL read committed; --读已提交
SET SESSION TRANSACTION ISOLATION LEVEL repeatable read; --可重复读
SET SESSION TRANSACTION ISOLATION LEVEL serializable; --序列化读
设置事务的全局隔离级别
SET GLOBAL TRANSACTION ISOLATION LEVEL read uncommitted; --读未提交
SET GLOBAL TRANSACTION ISOLATION LEVEL read committed; --读已提交
SET GLOBAL TRANSACTION ISOLATION LEVEL repeatable read; --可重复读
SET GLOBAL TRANSACTION ISOLATION LEVEL serializable; --序列化读
查看事务隔离级别
SELECT @@TRANSACTION_ISOLATION; -- 查看事务的隔离级别
并发操作带来的数据不一致性
隔离级别 | 丢失修改 | 脏读 | 不可重复度 | 幻读 |
---|---|---|---|---|
Read Uncommitted | × | √ | √ | √ |
Read Committed | × | × | √ | √ |
Repeatable Read | × | × | × | √ |
Serializable | × | × | × | × |
丢失修改
:事务T1和T2读入同一数据并修改,T2的提交破坏了T1提交的结果,导致T1的修改丢失。这种情况在事务中不可能出现, 因为一个事务在使用UPDATE修改时会给被修改的记录加锁(排他锁), 其他事务必须等待此事务完成后才可以继续UPDATE脏读
:事务T1读到事务T2尚未提交的修改,这里无法确定T2是提交还是回滚,所以称读到的数据为脏数据。不可重复读
:事务T1多次读取同一个数据集合时,事务T1还没结束,事务T2对数据进行修改并提交,导致事务T1两次读取的数据不一样幻读
:事务T1第一次读到的数据是最近提交的数据(可以理解为“快照”),并且事务T1之后所有读操作都是以第一次读到的数据为准(读到的一直都是“快照”中的内容),即便是事务T2对数据进行修改,T1读到的数据同样是第一次读到的数据。
并发控制-封锁
- 共享锁/读锁/S锁
若事务T对数据对象A加S锁,则事务T可以读取A,但不能修改A
事务T释放A上的S锁之前,其他事务可以给A加S锁,但不能加X锁(即其他事务可以读取A,但不能修改A) - 排他锁/写锁/X锁
若事务T对数据对象A加X锁,则事务T可以读取A,也可以修改A
事务T释放A上的X锁之前,其他事务不可以给A加任何类型的锁(即其他事务既不能读取A,也不能修改A)
封锁协议
- 一级封锁协议:事务T在对数据A修改之前必须先对其加X锁,直到事务结束再释放
- 二级封锁协议:事务T在对数据A修改之前必须先对其加X锁(一级封锁协议),并且其他事务在读取数据之前必须先对其加S锁,读完再释放
- 三级封锁协议:事务T在对数据A修改之前必须先对其加X锁(一级封锁协议),并且其他事务在读取数据之前必须先对其加S锁,提交再释放
X锁 | S锁 | 一致性保证 | |||||
操作结束释放 | 事务结束释放 | 操作结束释放 | 事务结束释放 | 不丢失修改 | 不读脏数据 | 可重复读 | |
一级封锁协议 | √ | √ | |||||
二级封锁协议 | √ | √ | √ | √ | |||
三级封锁协议 | √ | √ | √ | √ | √ |
两段锁协议
两段锁协议 (TwoPhase Loking 简称2PL)
指所有的事务必须分为两个阶段对数据项加锁和解锁。
- 第一阶段 (生长/扩展/加锁阶段):同一事务对所有数据读写时,首先是要申请并获得数据的封锁。
在获取封锁阶段,事务可以给数据加锁,在进行数据读操作之前需要申请获取S锁;在进行写操作之前需要申请获取X锁。如果无法获取封锁(加锁失败),则事务进入等待状态,直到加锁成功为止。 - 第二阶段 (衰退/收缩/解锁阶段):在事务释放一个封锁后,不能在获取其他的封锁。
在释放封锁阶段,即在事务释放第一个封锁后进入该阶段,该阶段只能进行解锁操作,不能在进行加锁操作
若并发执行的所有事务都遵循两段锁协议,那么这些事务的任何并发调度策略都是可串行化的,即事务遵循两段锁协议是可串行化的充分不必要条件
2. TCL命令
2.1 开启事务
START TRANSACTION; --开启事务
2.2 设置回滚点
SAVE POINT A; --设置回滚点A
2.2 回到回滚点
ROLLBACK TO A; --回到回滚点A
2.2 结束事务
COMMIT; --提交事务
ROLLBACK; --回滚事务
注意:
- 在Mysql中若未开启事务,则默认每一条DML语句作为事务自动提交
- 开启事务后,不要掺杂DML以外的语句,否则可能会导致事务无法回滚成功
事务具体使用案例
在Mysql中操作事务
mysql> -- 表的原始数据
mysql> SELECT * FROM emp;
+-------+--------+-----------+------+------------+---------+---------+--------+
| EMPNO | ENAME | JOB | MGR | HIREDATE | SAL | COMM | DEPTNO |
+-------+--------+-----------+------+------------+---------+---------+--------+
| 1111 | ghs | wu | NULL | NULL | 3000.00 | NULL | NULL |
| 1121 | aaaa | NULL | NULL | NULL | NULL | NULL | NULL |
| 2222 | wy | NULL | NULL | NULL | 1.00 | NULL | NULL |
| 7369 | SMITH | CLERK | 7902 | 1980-12-17 | 800.00 | NULL | 20 |
| 7499 | ALLEN | SALESMAN | 7698 | 1981-02-20 | 1600.00 | 300.00 | 30 |
| 7521 | WARD | SALESMAN | 7698 | 1981-02-22 | 1250.00 | 500.00 | 30 |
| 7566 | JONES | MANAGER | 7839 | 1981-04-02 | 2975.00 | NULL | 20 |
| 7654 | MARTIN | SALESMAN | 7698 | 1981-09-28 | 1250.00 | 1400.00 | 30 |
| 7698 | BLAKE | MANAGER | 7839 | 1981-05-01 | 2850.00 | NULL | 30 |
| 7782 | CLARK | MANAGER | 7839 | 1981-06-09 | 2450.00 | NULL | 10 |
| 7788 | SCOTT | ANALYST | 7566 | 1987-04-19 | 3000.00 | NULL | 20 |
| 7839 | KING | PRESIDENT | NULL | 1981-11-17 | 5000.00 | NULL | 10 |
| 7844 | TURNER | SALESMAN | 7698 | 1981-09-08 | 1500.00 | 0.00 | 30 |
| 7876 | ADAMS | CLERK | 7788 | 1987-05-23 | 1100.00 | NULL | 20 |
| 7900 | JAMES | CLERK | 7698 | 1981-12-03 | 950.00 | NULL | 30 |
| 7902 | FORD | ANALYST | 7566 | 1981-12-03 | 3000.00 | NULL | 20 |
| 7934 | MILLER | CLERK | 7782 | 1982-01-23 | 1300.00 | NULL | 10 |
+-------+--------+-----------+------+------------+---------+---------+--------+
17 rows in set (0.00 sec)
mysql>
mysql> -- 开启事务
mysql> START TRANSACTION;
Query OK, 0 rows affected (0.00 sec)
mysql>
mysql> -- 向表中插入empno=1的数据
mysql> INSERT INTO emp VALUES(1,'1','1',1,'1970-01-01',1.0,1.0,10);
Query OK, 1 row affected (0.00 sec)
mysql>
mysql> -- 向表中插入empno=2的数据
mysql> INSERT INTO emp VALUES(2,'A','2',2,'1970-01-01',2.0,2.0,10);
Query OK, 1 row affected (0.00 sec)
mysql> -- 设置回滚点A
mysql> SAVEPOINT A;
Query OK, 0 rows affected (0.00 sec)
mysql>
mysql>
mysql> -- 向表中插入empno=3的数据
mysql> INSERT INTO emp VALUES(3,'B','3',3,'1970-01-01',3.0,3.0,10);
Query OK, 1 row affected (0.00 sec)
mysql> -- 设置回滚点B
mysql> SAVEPOINT B;
Query OK, 0 rows affected (0.00 sec)
mysql>
mysql> -- 向表中插入empno=4的数据
mysql> INSERT INTO emp VALUES(4,'4','4',4,'1970-01-01',4.0,4.0,10);
Query OK, 1 row affected (0.00 sec)
mysql>
mysql>
mysql> -- 重新查看表数据(可以看到empno为1、2、3、4的数据都被插入到表中了)
mysql> SELECT * FROM emp;
+-------+--------+-----------+------+------------+---------+---------+--------+
| EMPNO | ENAME | JOB | MGR | HIREDATE | SAL | COMM | DEPTNO |
+-------+--------+-----------+------+------------+---------+---------+--------+
| 1 | 1 | 1 | 1 | 1970-01-01 | 1.00 | 1.00 | 10 |
| 2 | A | 2 | 2 | 1970-01-01 | 2.00 | 2.00 | 10 |
| 3 | B | 3 | 3 | 1970-01-01 | 3.00 | 3.00 | 10 |
| 4 | 4 | 4 | 4 | 1970-01-01 | 4.00 | 4.00 | 10 |
| 1111 | ghs | wu | NULL | NULL | 3000.00 | NULL | NULL |
| 1121 | aaaa | NULL | NULL | NULL | NULL | NULL | NULL |
| 2222 | wy | NULL | NULL | NULL | 1.00 | NULL | NULL |
| 7369 | SMITH | CLERK | 7902 | 1980-12-17 | 800.00 | NULL | 20 |
| 7499 | ALLEN | SALESMAN | 7698 | 1981-02-20 | 1600.00 | 300.00 | 30 |
| 7521 | WARD | SALESMAN | 7698 | 1981-02-22 | 1250.00 | 500.00 | 30 |
| 7566 | JONES | MANAGER | 7839 | 1981-04-02 | 2975.00 | NULL | 20 |
| 7654 | MARTIN | SALESMAN | 7698 | 1981-09-28 | 1250.00 | 1400.00 | 30 |
| 7698 | BLAKE | MANAGER | 7839 | 1981-05-01 | 2850.00 | NULL | 30 |
| 7782 | CLARK | MANAGER | 7839 | 1981-06-09 | 2450.00 | NULL | 10 |
| 7788 | SCOTT | ANALYST | 7566 | 1987-04-19 | 3000.00 | NULL | 20 |
| 7839 | KING | PRESIDENT | NULL | 1981-11-17 | 5000.00 | NULL | 10 |
| 7844 | TURNER | SALESMAN | 7698 | 1981-09-08 | 1500.00 | 0.00 | 30 |
| 7876 | ADAMS | CLERK | 7788 | 1987-05-23 | 1100.00 | NULL | 20 |
| 7900 | JAMES | CLERK | 7698 | 1981-12-03 | 950.00 | NULL | 30 |
| 7902 | FORD | ANALYST | 7566 | 1981-12-03 | 3000.00 | NULL | 20 |
| 7934 | MILLER | CLERK | 7782 | 1982-01-23 | 1300.00 | NULL | 10 |
+-------+--------+-----------+------+------------+---------+---------+--------+
21 rows in set (0.00 sec)
mysql>
mysql> -- 回到B回滚点
mysql> ROLLBACK TO B;
Query OK, 0 rows affected (0.00 sec)
mysql> -- 重新查看表数据(可以看出成功回滚到回滚点B)
mysql> SELECT * FROM emp;
+-------+--------+-----------+------+------------+---------+---------+--------+
| EMPNO | ENAME | JOB | MGR | HIREDATE | SAL | COMM | DEPTNO |
+-------+--------+-----------+------+------------+---------+---------+--------+
| 1 | 1 | 1 | 1 | 1970-01-01 | 1.00 | 1.00 | 10 |
| 2 | A | 2 | 2 | 1970-01-01 | 2.00 | 2.00 | 10 |
| 3 | B | 3 | 3 | 1970-01-01 | 3.00 | 3.00 | 10 |
| 1111 | ghs | wu | NULL | NULL | 3000.00 | NULL | NULL |
| 1121 | aaaa | NULL | NULL | NULL | NULL | NULL | NULL |
| 2222 | wy | NULL | NULL | NULL | 1.00 | NULL | NULL |
| 7369 | SMITH | CLERK | 7902 | 1980-12-17 | 800.00 | NULL | 20 |
| 7499 | ALLEN | SALESMAN | 7698 | 1981-02-20 | 1600.00 | 300.00 | 30 |
| 7521 | WARD | SALESMAN | 7698 | 1981-02-22 | 1250.00 | 500.00 | 30 |
| 7566 | JONES | MANAGER | 7839 | 1981-04-02 | 2975.00 | NULL | 20 |
| 7654 | MARTIN | SALESMAN | 7698 | 1981-09-28 | 1250.00 | 1400.00 | 30 |
| 7698 | BLAKE | MANAGER | 7839 | 1981-05-01 | 2850.00 | NULL | 30 |
| 7782 | CLARK | MANAGER | 7839 | 1981-06-09 | 2450.00 | NULL | 10 |
| 7788 | SCOTT | ANALYST | 7566 | 1987-04-19 | 3000.00 | NULL | 20 |
| 7839 | KING | PRESIDENT | NULL | 1981-11-17 | 5000.00 | NULL | 10 |
| 7844 | TURNER | SALESMAN | 7698 | 1981-09-08 | 1500.00 | 0.00 | 30 |
| 7876 | ADAMS | CLERK | 7788 | 1987-05-23 | 1100.00 | NULL | 20 |
| 7900 | JAMES | CLERK | 7698 | 1981-12-03 | 950.00 | NULL | 30 |
| 7902 | FORD | ANALYST | 7566 | 1981-12-03 | 3000.00 | NULL | 20 |
| 7934 | MILLER | CLERK | 7782 | 1982-01-23 | 1300.00 | NULL | 10 |
+-------+--------+-----------+------+------------+---------+---------+--------+
20 rows in set (0.00 sec)
mysql>
mysql> -- 回到A回滚点
mysql> ROLLBACK TO A;
Query OK, 0 rows affected (0.00 sec)
mysql> -- 重新查看表数据(可以看出成功回滚到回滚点A)
mysql> SELECT * FROM emp;
+-------+--------+-----------+------+------------+---------+---------+--------+
| EMPNO | ENAME | JOB | MGR | HIREDATE | SAL | COMM | DEPTNO |
+-------+--------+-----------+------+------------+---------+---------+--------+
| 1 | 1 | 1 | 1 | 1970-01-01 | 1.00 | 1.00 | 10 |
| 2 | A | 2 | 2 | 1970-01-01 | 2.00 | 2.00 | 10 |
| 1111 | ghs | wu | NULL | NULL | 3000.00 | NULL | NULL |
| 1121 | aaaa | NULL | NULL | NULL | NULL | NULL | NULL |
| 2222 | wy | NULL | NULL | NULL | 1.00 | NULL | NULL |
| 7369 | SMITH | CLERK | 7902 | 1980-12-17 | 800.00 | NULL | 20 |
| 7499 | ALLEN | SALESMAN | 7698 | 1981-02-20 | 1600.00 | 300.00 | 30 |
| 7521 | WARD | SALESMAN | 7698 | 1981-02-22 | 1250.00 | 500.00 | 30 |
| 7566 | JONES | MANAGER | 7839 | 1981-04-02 | 2975.00 | NULL | 20 |
| 7654 | MARTIN | SALESMAN | 7698 | 1981-09-28 | 1250.00 | 1400.00 | 30 |
| 7698 | BLAKE | MANAGER | 7839 | 1981-05-01 | 2850.00 | NULL | 30 |
| 7782 | CLARK | MANAGER | 7839 | 1981-06-09 | 2450.00 | NULL | 10 |
| 7788 | SCOTT | ANALYST | 7566 | 1987-04-19 | 3000.00 | NULL | 20 |
| 7839 | KING | PRESIDENT | NULL | 1981-11-17 | 5000.00 | NULL | 10 |
| 7844 | TURNER | SALESMAN | 7698 | 1981-09-08 | 1500.00 | 0.00 | 30 |
| 7876 | ADAMS | CLERK | 7788 | 1987-05-23 | 1100.00 | NULL | 20 |
| 7900 | JAMES | CLERK | 7698 | 1981-12-03 | 950.00 | NULL | 30 |
| 7902 | FORD | ANALYST | 7566 | 1981-12-03 | 3000.00 | NULL | 20 |
| 7934 | MILLER | CLERK | 7782 | 1982-01-23 | 1300.00 | NULL | 10 |
+-------+--------+-----------+------+------------+---------+---------+--------+
19 rows in set (0.00 sec)
mysql>
mysql> -- (回滚事务)结束事务
mysql> ROLLBACK;
Query OK, 0 rows affected (0.00 sec)
mysql> -- 重新查看表数据(和原表数据相同)
mysql> SELECT * FROM emp;
+-------+--------+-----------+------+------------+---------+---------+--------+
| EMPNO | ENAME | JOB | MGR | HIREDATE | SAL | COMM | DEPTNO |
+-------+--------+-----------+------+------------+---------+---------+--------+
| 1111 | ghs | wu | NULL | NULL | 3000.00 | NULL | NULL |
| 1121 | aaaa | NULL | NULL | NULL | NULL | NULL | NULL |
| 2222 | wy | NULL | NULL | NULL | 1.00 | NULL | NULL |
| 7369 | SMITH | CLERK | 7902 | 1980-12-17 | 800.00 | NULL | 20 |
| 7499 | ALLEN | SALESMAN | 7698 | 1981-02-20 | 1600.00 | 300.00 | 30 |
| 7521 | WARD | SALESMAN | 7698 | 1981-02-22 | 1250.00 | 500.00 | 30 |
| 7566 | JONES | MANAGER | 7839 | 1981-04-02 | 2975.00 | NULL | 20 |
| 7654 | MARTIN | SALESMAN | 7698 | 1981-09-28 | 1250.00 | 1400.00 | 30 |
| 7698 | BLAKE | MANAGER | 7839 | 1981-05-01 | 2850.00 | NULL | 30 |
| 7782 | CLARK | MANAGER | 7839 | 1981-06-09 | 2450.00 | NULL | 10 |
| 7788 | SCOTT | ANALYST | 7566 | 1987-04-19 | 3000.00 | NULL | 20 |
| 7839 | KING | PRESIDENT | NULL | 1981-11-17 | 5000.00 | NULL | 10 |
| 7844 | TURNER | SALESMAN | 7698 | 1981-09-08 | 1500.00 | 0.00 | 30 |
| 7876 | ADAMS | CLERK | 7788 | 1987-05-23 | 1100.00 | NULL | 20 |
| 7900 | JAMES | CLERK | 7698 | 1981-12-03 | 950.00 | NULL | 30 |
| 7902 | FORD | ANALYST | 7566 | 1981-12-03 | 3000.00 | NULL | 20 |
| 7934 | MILLER | CLERK | 7782 | 1982-01-23 | 1300.00 | NULL | 10 |
+-------+--------+-----------+------+------------+---------+---------+--------+
17 rows in set (0.00 sec)
在Java中操作事务
package com.jsoft.transaction;
import com.alibaba.druid.pool.DruidDataSourceFactory;
import org.junit.Test;
import javax.sql.DataSource;
import java.io.IOException;
import java.sql.*;
import java.util.Properties;
public class Demo {
private static final DataSource DATA_SOURCE;
static {
Properties pros = new Properties();
try {
pros.load(Thread.currentThread().getContextClassLoader().getResourceAsStream("druid.properties"));
DATA_SOURCE = DruidDataSourceFactory.createDataSource(pros);
} catch (IOException e) {
throw new RuntimeException();
} catch (Exception e) {
throw new RuntimeException();
}
}
@Test
public void test() {
try(Connection conn = DATA_SOURCE.getConnection();
PreparedStatement ps = conn.prepareStatement("");) {
// 查看事务未开启前表的内容
try (ResultSet rt = ps.executeQuery("SELECT * FROM emp")) {
StringBuilder sb = new StringBuilder();
while (rt.next()) {
sb.delete(0, sb.length());
sb.append(rt.getInt("empno")).append("\t");
sb.append(rt.getString("ename")).append("\t");
System.out.println(sb);
}
}
System.out.println("----------------------------------------");
// 关闭事务自动提交
conn.setAutoCommit(false); // ***注意***:不能等同于start transaction;或begin;
// 向表中插入empno=1,empno=2的数据
ps.addBatch("INSERT INTO emp VALUES(1,'1','1',1,'1970-01-01',1.0,1.0,10);");
ps.addBatch("INSERT INTO emp VALUES(2,'A','2',2,'1970-01-01',2.0,2.0,10);");
ps.executeBatch();
// 设置回滚点A
Savepoint a = conn.setSavepoint("A"); // 等同于 ---> savepoint A;
// 向表中插入empno=3,empno=4的数据
ps.addBatch("INSERT INTO emp VALUES(3,'B','3',3,'1970-01-01',3.0,3.0,10);");
ps.addBatch("INSERT INTO emp VALUES(4,'4','4',4,'1970-01-01',4.0,4.0,10);");
ps.executeBatch();
// 回到回滚点A
conn.rollback(a);
// 提交事务
conn.commit(); // 等同于 ---> commit;
// 查看事务结束后表的内容(如果成功将在表中可以看到只会新增emp=1和emp=2这两条记录)
try (ResultSet rt = ps.executeQuery("SELECT * FROM emp")) {
StringBuilder sb = new StringBuilder();
while (rt.next()) {
sb.delete(0, sb.length());
sb.append(rt.getInt("empno")).append("\t");
sb.append(rt.getString("ename")).append("\t");
System.out.println(sb);
}
}
// 开启事务自动提交
conn.setAutoCommit(true);
} catch (SQLException throwables) {
throwables.printStackTrace();
}
}
}