-------------------------------------------------------------------------------------------------------------------------------------------
参考文档:
-------------------------------------------------------------------------------------------------------------------------------------------
mysql:
C:\WINDOWS\system32>mysql -Vmysql Ver 14.14 Distrib 5.5.27, for Win64 (x86)
-------------------------------------------------------------------------------------------------------------------------------------------
1.事务的四大特性(ACID):
原子性(Atomicity):一个事务必须被视为一个不可分割的最小工作单元,整个事务中的所有操作要么全部提交成功,要么全部失败回滚,对于一个事务来说,不可能只执行其中的一部分操作。
一致性(Consistency):数据库总是从一个一致性的状态转换到另一个一致性的状态。
隔离性(Isolation):通常来说,一个事务所做的修改在最终提交以前,对其他事务是不可见的。
持久性(Durability):事务完成后,事务对数据库的所有更新将被保存到数据库,不能回滚。
2.事务的并发问题:
3种数据读取问题(脏读,不可重复读,幻读)、2种数据更新问题
脏读:A事务读取B事务尚未commit的数据,并在此基础上操作,之后B事务进行rollback,那么A事务读取到的数据就是脏数据.
时间 | 转账事务A | 取款事务B |
---|---|---|
1 | 开启事务 | |
2 | 开启事务 | |
3 | 查询账户余额为1000元 | |
4 | 取出500元余额修改为500元 | |
5 | 查询账户余额为500元(脏读) | |
6 | 撤销事务余额恢复为1000元 | |
7 | 汇入100元把余额修改为600元 | |
8 | 提交事务 |
不可重复读:事务A重新读取前面读取过的数据,发现该数据已经被另一个已提交的事务B修改过了。
时间 | 转账事务A | 取款事务B |
---|---|---|
1 | 开始事务 | |
2 | 开始事务 | |
3 | 查询账户余额为1000元 | |
4 | 查询账户余额为1000元 | |
5 | 取出100元修改余额为900元 | |
6 | 提交事务 | |
7 | 查询账户余额为900元(不可重复读) |
幻读:事务A重新执行一个查询,返回一系列符合查询条件的行,发现其中插入了被事务B提交的行。
时间 | 转账事务A | 取款事务B |
---|---|---|
1 | 开始事务 | |
2 | 开始事务 | |
3 | 统计总存款为10000元 | |
4 | 新增一个存款账户存入100元 | |
5 | 提交事务 | |
6 | 再次统计总存款为10100元(幻读) |
第1类丢失更新:事务A撤销时,把已经提交的事务B的更新数据覆盖了。
时间 | 取款事务A | 转账事务B |
---|---|---|
T1 | 开始事务 | |
T2 | 开始事务 | |
T3 | 查询账户余额为1000元 | |
T4 | 查询账户余额为1000元 | |
T5 | 汇入100元修改余额为1100元 | |
T6 | 提交事务 | |
T7 | 取出100元将余额修改为900元 | |
T8 | 撤销事务 | |
T9 | 余额恢复为1000元(丢失更新) |
第2类丢失更新:事务A覆盖事务B已经提交的数据,造成事务B所做的操作丢失。
时间 | 转账事务A | 取款事务B |
---|---|---|
T1 | 开始事务 | |
T2 | 开始事务 | |
T3 | 查询账户余额为1000元 | |
T4 | 查询账户余额为1000元 | |
T5 | 取出100元将余额修改为900元 | |
T6 | 提交事务 | |
T7 | 汇入100元将余额修改为1100元 | |
T8 | 提交事务 | |
T9 | 查询账户余额为1100元(丢失更新) |
3.事务隔离级别:
数据并发访问所产生的问题,数据库通常会通过锁机制来解决。
按锁定对象不同可以分为表级锁和行级锁;
按并发事务锁定关系可以分为共享锁和独占锁;
直接使用锁是非常麻烦的,为此数据库为用户提供了自动锁机制,只要用户指定会话的事务隔离级别,数据库就会通过分析SQL语句然后为事务访问的资源加上合适的锁,此外,数据库还会维护这些锁通过各种手段提高系统的性能,这些对用户来说都是透明的。
隔离级别 | 脏读 | 不可重复读 | 幻读 | 第一类丢失更新 | 第二类丢失更新 |
---|---|---|---|---|---|
READ UNCOMMITED | 允许 | 允许 | 允许 | 不允许 | 允许 |
READ COMMITTED | 不允许 | 允许 | 允许 | 不允许 | 允许 |
REPEATABLE READ | 不允许 | 不允许 | 允许 | 不允许 | 不允许 |
SERIALIZABLE | 不允许 | 不允许 | 不允许 | 不允许 | 不允许 |
事务隔离级别和数据访问的并发性是对立的,事务隔离级别越高并发性就越差。
mysql 默认的事务隔离级别是:可重复读(REPEATABLE READ)
mysql> select @@tx_isolation;+-----------------+| @@tx_isolation |+-----------------+| REPEATABLE-READ |+-----------------+1 row in set (0.00 sec)
读未提交:
1.打开终端A,设置当前会话的事务隔离级别为read uncommitted。然后开启事务T1,查询表test中id为1的记录
mysql> set session transaction isolation level read uncommitted;Query OK, 0 rows affected (0.00 sec)mysql> start transaction;Query OK, 0 rows affected (0.00 sec)mysql> select * from test where id=1;+----+---------------------------+------+----------+| id | transactionIsolationLevel | data | desc |+----+---------------------------+------+----------+| 1 | read uncommitted | 100 | 读未提交 |+----+---------------------------+------+----------+1 row in set (0.00 sec)
2.打开终端B,设置当前会话的事务隔离级别为read uncommitted。然后开启事务T2,更新表test中id为1的记录中的data字段
mysql> set session transaction isolation level read uncommitted;Query OK, 0 rows affected (0.00 sec)mysql> start transaction;Query OK, 0 rows affected (0.00 sec)mysql> select * from test where id=1;+----+---------------------------+------+----------+| id | transactionIsolationLevel | data | desc |+----+---------------------------+------+----------+| 1 | read uncommitted | 100 | 读未提交 |+----+---------------------------+------+----------+1 row in set (0.00 sec)mysql> update test set data=data-1 where id=1;Query OK, 1 row affected (0.00 sec)Rows matched: 1 Changed: 1 Warnings: 0mysql> select * from test where id=1;+----+---------------------------+------+----------+| id | transactionIsolationLevel | data | desc |+----+---------------------------+------+----------+| 1 | read uncommitted | 99 | 读未提交 |+----+---------------------------+------+----------+1 row in set (0.00 sec)
此时终端B开启的事务T2,更新数据之后并未提交。
3.切换到终端A,再次查询表test中id为1的记录
mysql> select * from test where id=1;+----+---------------------------+------+----------+| id | transactionIsolationLevel | data | desc |+----+---------------------------+------+----------+| 1 | read uncommitted | 99 | 读未提交 |+----+---------------------------+------+----------+1 row in set (0.00 sec)
可以发现,虽然终端B上开启的事务T2尚未提交,但是终端A上开启的事务T1仍然可以查询到事务T2已经更新的数据。此时,如果事务T2进行回滚操作,那么事务T1查询到的数据就是脏数据。
4.切换到终端B,进行回滚操作
mysql> select * from test where id=1;+----+---------------------------+------+----------+| id | transactionIsolationLevel | data | desc |+----+---------------------------+------+----------+| 1 | read uncommitted | 100 | 读未提交 |+----+---------------------------+------+----------+1 row in set (0.00 sec)
5.切换到终端A,更新表test中id为1的记录的data字段,更新之前查到的记录中data字段的值是99
mysql> update test set data=data-1 where id=1;Query OK, 1 row affected (0.00 sec)Rows matched: 1 Changed: 1 Warnings: 0mysql> select * from test where id=1;+----+---------------------------+------+----------+| id | transactionIsolationLevel | data | desc |+----+---------------------------+------+----------+| 1 | read uncommitted | 99 | 读未提交 |+----+---------------------------+------+----------+1 row in set (0.00 sec)
可以发现data字段的值仍是99=100-1,而并非是98=99-1
读已提交:
1.切换到终端A,设置事务隔离级别为read committed。然后开启事务T1,查询表test中id为2的记录
mysql> set session transaction isolation level read committed;Query OK, 0 rows affected (0.00 sec)mysql> start transaction;Query OK, 0 rows affected (0.00 sec)mysql> select * from test where id=2;+----+---------------------------+------+----------+| id | transactionIsolationLevel | data | desc |+----+---------------------------+------+----------+| 2 | read committed | 100 | 读已提交 |+----+---------------------------+------+----------+1 row in set (0.00 sec)
2.打开终端B,设置当前会话的事务隔离级别为read committed。然后开启事务T2,更新表test中id为2的记录中的data字段
mysql> set session transaction isolation level read committed;Query OK, 0 rows affected (0.00 sec)mysql> start transaction;Query OK, 0 rows affected (0.00 sec)mysql> select * from test where id=2;+----+---------------------------+------+----------+| id | transactionIsolationLevel | data | desc |+----+---------------------------+------+----------+| 2 | read committed | 100 | 读已提交 |+----+---------------------------+------+----------+1 row in set (0.00 sec)mysql> update test set data=data-1 where id=2;Query OK, 1 row affected (0.00 sec)Rows matched: 1 Changed: 1 Warnings: 0mysql> select * from test where id=2;+----+---------------------------+------+----------+| id | transactionIsolationLevel | data | desc |+----+---------------------------+------+----------+| 2 | read committed | 99 | 读已提交 |+----+---------------------------+------+----------+1 row in set (0.00 sec)
此时终端B开启的事务T2,更新数据之后并未提交。
3.切换到终端A,查询表test中id为2的记录
mysql> select * from test where id=2;+----+---------------------------+------+----------+| id | transactionIsolationLevel | data | desc |+----+---------------------------+------+----------+| 2 | read committed | 100 | 读已提交 |+----+---------------------------+------+----------+1 row in set (0.00 sec)
可以发现只要事务T2还没有提交,那么事务T1中就查询不到T2更新的数据,解决了脏读的问题。
4.切换到终端B,进行提交操作
mysql> commit;Query OK, 0 rows affected (0.00 sec)mysql> select * from test where id=2;+----+---------------------------+------+----------+| id | transactionIsolationLevel | data | desc |+----+---------------------------+------+----------+| 2 | read committed | 99 | 读已提交 |+----+---------------------------+------+----------+1 row in set (0.00 sec)
5.切换到终端A,查询表test中id为2的记录
mysql> select * from test where id=2;+----+---------------------------+------+----------+| id | transactionIsolationLevel | data | desc |+----+---------------------------+------+----------+| 2 | read committed | 99 | 读已提交 |+----+---------------------------+------+----------+1 row in set (0.00 sec)
此时,事务T1 就能查询到事务T2更新的数据。事务T2提交前后,事务T1各查询了一次,第一次查询到的data值为100,第二次查询到的data值是99,这就是所谓的不可重复读。
可重复读:
1.打开终端A,设置事务隔离级别为repeatable read。然后开启事务T1,查询表test中id为3的记录
mysql> set session transaction isolation level repeatable read;Query OK, 0 rows affected (0.00 sec)mysql> select @@tx_isolation;+-----------------+| @@tx_isolation |+-----------------+| REPEATABLE-READ |+-----------------+1 row in set (0.00 sec)mysql> start transaction;Query OK, 0 rows affected (0.00 sec)mysql> select * from test where id=3;+----+---------------------------+------+--------+| id | transactionIsolationLevel | data | desc |+----+---------------------------+------+--------+| 3 | repeatable read | 100 | 重复读 |+----+---------------------------+------+--------+1 row in set (0.00 sec)
2.打开终端B,设置当前会话的事务隔离级别为repeatable read。然后开启事务T2,更新表test中id为3的记录中的data字段并提交
mysql> set session transaction isolation level repeatable read;Query OK, 0 rows affected (0.00 sec)mysql> select @@tx_isolation;+-----------------+| @@tx_isolation |+-----------------+| REPEATABLE-READ |+-----------------+1 row in set (0.00 sec)mysql> start transaction;Query OK, 0 rows affected (0.00 sec)mysql> select * from test where id=3;+----+---------------------------+------+--------+| id | transactionIsolationLevel | data | desc |+----+---------------------------+------+--------+| 3 | repeatable read | 100 | 重复读 |+----+---------------------------+------+--------+1 row in set (0.00 sec)mysql> update test set data=data-1 where id=3;Query OK, 1 row affected (0.00 sec)Rows matched: 1 Changed: 1 Warnings: 0mysql> commit;Query OK, 0 rows affected (0.00 sec)mysql> select * from test where id=3;+----+---------------------------+------+--------+| id | transactionIsolationLevel | data | desc |+----+---------------------------+------+--------+| 3 | repeatable read | 99 | 重复读 |+----+---------------------------+------+--------+1 row in set (0.00 sec)
3.切换到终端A,查询表test中id为3的记录
mysql> select * from test where id=3;+----+---------------------------+------+--------+| id | transactionIsolationLevel | data | desc |+----+---------------------------+------+--------+| 3 | repeatable read | 100 | 重复读 |+----+---------------------------+------+--------+1 row in set (0.00 sec)
终端A下的两次查询结果一致,没有出现不可重复读的问题。
可重复读的隔离级别下使用了MVCC机制,T1事务中读取的是记录的快照版本,而非最新版本,T2事务的更新是创建了一个新版本来更新,不同事务的读和写是分离的。
此时,若在终端A上对data进行更新操作,比如data=data-1;得到的结果是98=99-1,而不是99=100-1 ----数据的一致性
mysql> update test set data=data-1 where id=3;Query OK, 1 row affected (0.00 sec)Rows matched: 1 Changed: 1 Warnings: 0mysql> commit;Query OK, 0 rows affected (0.00 sec)mysql> select * from test where id=3;+----+---------------------------+------+--------+| id | transactionIsolationLevel | data | desc |+----+---------------------------+------+--------+| 3 | repeatable read | 98 | 重复读 |+----+---------------------------+------+--------+1 row in set (0.00 sec)
串行化:
1.打开终端A,设置事务隔离级别为serializable。然后开启事务T1,查询表test中id为4的记录
mysql> set session transaction isolation level serializable;Query OK, 0 rows affected (0.00 sec)mysql> select @@tx_isolation;+----------------+| @@tx_isolation |+----------------+| SERIALIZABLE |+----------------+1 row in set (0.00 sec)mysql> start transaction;Query OK, 0 rows affected (0.00 sec)mysql> select * from test where id=4;+----+---------------------------+------+--------+| id | transactionIsolationLevel | data | desc |+----+---------------------------+------+--------+| 4 | serializable | 100 | 串行化 |+----+---------------------------+------+--------+1 row in set (0.00 sec)
2.打开终端B,设置当前会话的事务隔离级别为serializable。然后开启事务T2
mysql> set session transaction isolation level serializable;Query OK, 0 rows affected (0.00 sec)mysql> select @@tx_isolation;+----------------+| @@tx_isolation |+----------------+| SERIALIZABLE |+----------------+1 row in set (0.00 sec)mysql> start transaction;Query OK, 0 rows affected (0.00 sec)
查询表test中id为4的记录
mysql> select * from test where id=4;+----+---------------------------+------+--------+| id | transactionIsolationLevel | data | desc |+----+---------------------------+------+--------+| 4 | serializable | 100 | 串行化 |+----+---------------------------+------+--------+1 row in set (0.00 sec)
更新表test中id为4的记录
mysql> update test set data=data-1 where id=4;ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
更新失败。此时表test已经被事务T1加上了读锁
3.切换到终端A,执行提交或是回滚的操作,结束了事务T1。
mysql> commit;Query OK, 0 rows affected (0.00 sec)
4.切换到终端B,再此执行update语句
mysql> update test set data=data-1 where id=4;Query OK, 1 row affected (0.00 sec)Rows matched: 1 Changed: 1 Warnings: 0mysql> commit;Query OK, 0 rows affected (0.00 sec)mysql> select * from test where id=4;+----+---------------------------+------+--------+| id | transactionIsolationLevel | data | desc |+----+---------------------------+------+--------+| 4 | serializable | 99 | 串行化 |+----+---------------------------+------+--------+1 row in set (0.00 sec)
sql语句就可以正常执行了。
以下是我在serializable事务隔离级别下做的测试,√表示的是执行过程中一切正常,×则表示在执行最后一步select/update语句时报错。
Lock wait timeout exceeded; try restarting transaction
以上只测试了两个事务并发执行的情况,更多事务并发,后续再测。