- 在语句的时候,某些业务要求,一系列操作必须全部执行,而不能仅执行一部分
例如,一个转账。一个人钱增加则另外一个人钱减少。得到钱和失去钱不能单独变化
- 这种把多条语句作为一个整体进行操作的功能,被称为数据库事务。
数据库事务可以确保该事务范围内的所有操作都可以全部成功或者全部失败。如果事务失败,那么效果就和没有执行这些SQL一样,不会对数据库数据有任何改动。
- 其实事务是对于涉及数据库操作才有的说法,而对于普通程序等保持变量不冲突,那叫保持一致性
事务特性(ACID)
- A:Atomic,原子性(不能拆分),将所有SQL作为原子工作单元执行,要么全部执行,要么全部不执行
- C:Consistent,一致性(数据一致),事务完成后,所有数据的状态都是一致的(数据同步),即A账户只要减去了100,B账户则必定加上了100;
- 一致性是业务层面的考虑,其他三个特性是保持一致性的前提
- I:Isolation,隔离性,如果有多个事务并发执行,每个事务作出的修改必须与其他事务隔离;即不允许把该事务中的修改提供给其他事务
- D:Duration,持久性,即事务完成后,对数据库数据的修改被持久化存储。
对于单条SQL语句,数据库系统自动将其作为一个事务执行并提交,这种事务被称为隐式事务。
显示事务即把多条SQL语句作为一个事务执行
使用事务
**BEGIN**
开启一个事务,**COMMIT**
提交一个事务,这种事务被称为显式事务
如果一个事务正在进行中,那么该事务中未提交的
begin; #创建事务
UPDATE accounts SET balance = balance - 100 WHERE id = 1; #id为1的账户减少100
UPDATE accounts SET balance = balance + 100 WHERE id = 2; #id为2的账户增加100
commit/rollback; #提交/回滚事务
- 提交事务指把事务内的所有SQL所做的修改永久保存。如果
**COMMIT**
语句执行失败了,整个事务也会失败 - 有些时候,我们希望主动让事务失败,这时,可以用
**ROLLBACK**
回滚事务,整个事务会失败 - 回滚事务后不需要再提交事务,提交后也不能再回滚。提交与回滚只能有一个
- 事务的隔离本质是加锁,级别越高锁越多,性能越低。但是mysql底层好,即便是重复读级别,性能也基本没啥影响
MySQL 的隔离级别基于锁和 MVCC 机制共同实现的。SERIALIZABLE 隔离级别,是通过锁来实现的。除了 SERIALIZABLE 隔离级别,其他的隔离级别都是基于 MVCC 实现。
隔离级别命令
在事务启用前执行
set transaction isolation level ...;
注意隔离级别是对自己这个会话而言,如A窗口事务采用默认,a事务期间b窗口事务要读取,是设置b事务,不是a事务- 显式事务开启期间无法修改隔离级别
- 隔离级别的限制对于该会话显示和隐式事务都生效
- 隔离级别只对当前会话生效,再打开一个会话还是默认的隔离级别
- 查看当前会话的隔离级别
- mysql8之前
SELECT @@tx_isolation;
- mysql8之后
SELECT @@transaction_isolation;
- mysql8之前
- 如果要使得默认的隔离级别为全局一直生效,而非当前会话: ```sql MySQL 事务隔离级别 mysql.cnf 文件里设置的(默认目录 /etc/my.cnf),在文件的文末添加配置:
transaction-isolation = REPEATABLE-READ 可用的配置值:READ-UNCOMMITTED、READ-COMMITTED、REPEATABLE-READ、SERIALIZABLE。
---
<a name="mcE4U"></a>
## 并发事务
- **对于多个并发执行的事务,如果涉及到操作同一条记录的时候,可能会发生问题。**因为并发操作会带来数据的不一致性,包括脏读、不可重复读、幻读等。数据库系统提供了隔离级别来让我们有针对性地选择事务的隔离级别,避免数据不一致的问题。
SQL标准定义了4种隔离级别,分别对应可能出现的数据不一致的情况:
| Isolation Level | 脏读(Dirty Read) | 不可重复读(Non Repeatable Read) | 幻读(Phantom Read) |
| --- | --- | --- | --- |
| Read Uncommitted | Yes | Yes | Yes |
| Read Committed | - | Yes | Yes |
| Repeatable Read | - | - | Yes |
| Serializable | - | - | - |
**首先分别开启两个MySQL客户端连接,按顺序依次执行事务A和事务B。执行以下隔离级别测试**
<a name="78g6L"></a>
### Read Uncommitted->脏读
- **读取未提交**
- **即会读取到未提交的修改,**
- **隔离级别最低**的一种事务级别。在这种隔离级别下,一个事务会读到另一个事务更新后但未提交的数据,如果另一个事务回滚,那么当前事务读到的数据就是脏数据,这就是脏读(Dirty Read)
---
我们准备好`students`表的数据,该表仅一行记录:
```sql
mysql> select * from students;
+----+-------+
| id | name |
+----+-------+
| 1 | Alice |
+----+-------+
然后,分别开启两个MySQL客户端连接,按顺序依次执行事务A和事务B:
时刻 | 事务A | 事务B |
---|---|---|
1 | set transaction isolation level read uncommitted; | |
2 | BEGIN; | BEGIN; |
3 | UPDATE students SET name = ‘Bob’ WHERE id = 1; | |
4 | SELECT FROM students WHERE id = 1; #输出name为Bob * 事务A未提交也可以读 | |
5 | ROLLBACK; | |
6 | SELECT FROM students WHERE id = 1; 事务A的SQL语句失效,name变回Alice*(脏读) | |
7 | COMMIT; |
Read Committed->不可重复读
- 在Read Committed隔离级别下,一个事务可能会遇到不可重复读(Non Repeatable Read)的问题。
- 可能出现的问题叫可重复读更准确些,注意与隔离级别的可重复读不是一个意思
- 即事务开启期间会读到其他事务提交了的数据。
- 不可重复读是指,在一个事务内,多次读同一数据,在这个事务还没有结束时,如果另一个事务恰好修改了这个数据,那么,在第一个事务中,两次读取的数据就可能不一致。
| 时刻 | 事务A | 事务B |
| —- | —- | —- |
| 1 |
| SET TRANSACTION ISOLATION LEVEL READ COMMITTED; | | 2 | BEGIN; | BEGIN; | | 3 | | SELECT FROM students WHERE id = 1; #name=Alice | | 4 | UPDATE students SET name = ‘Bob’ WHERE id = 1; | | | 5 | COMMIT; | | | 6 | | SELECT FROM students WHERE id = 1;#读到另一个提交后的,Bob | | 7 | | COMMIT; |
可见事务不要重复读同一条记录,因为很可能读到的结果不一致。你可能不知不觉中数据被其它事务改动过
Repeatable Read-幻读
- 可重复读隔离级别
在Repeatable Read隔离级别下,一个事务可能会遇到幻读(Phantom Read)的问题。
- 幻读是指,在一个事务中,其他事务再此期间修改了。第一次查询某条记录,发现没有,但是,对这个数据修改,竟然能成功,并且,再次读取同一条记录,它就神奇地出现了
- 即事务的修改对于该会话事务来说是执行了的,对于其他事务(一条语句也是一个事务)来说,则没有执行;
- 虽然看不到,但是能进行修改,因为它已经被提交了,客观存在,但是主观看不到(具体看幻读),再次查就能查到
- 触发幻读改变主观的操作可以是对该表的新增或者删除,如新增,再查也会出现原本不存在数据
- 不可重复读和幻读区别 :不可重复读的重点是修改比如多次读取一条记录发现其中某些列的值被修改,幻读的重点在于新增或者删除比如多次查询同一条查询语句(DQL)时,记录发现记录增多或减少了
- 解决幻读的方法:核心思想就是一个事务在操作某张表数据的时候,另外一个事务不允许新增或者删除这张表中的数据了。解决幻读的方式主要有以下几种:
- 将事务隔离级别调整为 SERIALIZABLE
- 在可重复读的事务级别下,给事务操作的这张表添加表锁
- 在可重复读的事务级别下,给事务操作的这张表添加 Next-Key Locks | 时刻 | 事务A | 事务B | | —- | —- | —- | | 1 | SET TRANSACTION ISOLATION LEVEL REPEATABLE READ; | SET TRANSACTION ISOLATION LEVEL REPEATABLE READ; | | 2 | BEGIN; | BEGIN; | | 3 | | SELECT FROM students WHERE id = 99; #查询为空 | | 4 | INSERT INTO students (id, name) VALUES (99, ‘Bob’); | | | 5 | COMMIT;(必须提交,因为不提交会触发禁止脏读操作。提交则触发禁止重复读。其他事务修改则触发幻读) | | | 6 | | SELECT FROM students WHERE id = 99; #按理说换读后这里可以查到,可是实际上还是查询为空,却可以进行更新 | | 7 | | UPDATE students SET name = ‘Alice’ WHERE id = 99; #可以更新成功 | | 8 | | SELECT FROM students WHERE id = 99; *#进行更新后就可以查找到 | | 9 | | COMMIT; |
Serializable
- 串行隔离级别
- Serializable是最严格的隔离级别。在Serializable隔离级别下,所有事务按照次序依次执行,因此,脏读、不可重复读、幻读都不会出现
- 此时不同事务完全隔离,写操作只对自己可见,写的结果也只对自己可见。事务全部结束才会整合结果
- 这种模式下如果有多个事务进行写操作,很容易出现死锁
虽然Serializable隔离级别下的事务具有最高的安全性,但是,由于事务是串行执行,所以效率会大大下降,应用程序的性能会急剧降低。如果没有特别重要的情景,一般都不会使用Serializable隔离级别。
*分布式事务
InnoDB 存储引擎提供了对 XA 事务的支持,并通过 XA 事务来支持分布式事务的实现。
表级锁: MySQL 中锁定 粒度最大 的一种锁,对当前操作的整张表加锁,实现简单,资源消耗也比较少,加锁快,不会出现死锁。其锁定粒度最大,触发锁冲突的概率最高,并发度最低,MyISAM 和 InnoDB 引擎都支持表级锁。
- 行级锁: MySQL 中锁定 粒度最小 的一种锁,只针对当前操作的行进行加锁。 行级锁能大大减少数据库操作的冲突。其加锁粒度最小,并发度高,但加锁的开销也最大,加锁慢,会出现死锁。
InnoDB 存储引擎的行级锁的算法有三种:
- Record lock:记录锁,单个行记录上的锁
- Gap lock:间隙锁,锁定一个范围,不包括记录本身。可以保证保证某个间隙内的数据在锁定情况下不会发生任何变化。
Next-key lock:相当于 记录锁 + 间隙锁。临键锁,锁定一个范围,包含记录本身。记录锁只能锁住已经存在的记录,为了避免插入新记录,需要依赖间隙锁。
lock tables 表名 write/read
- read时所有人只能读,不能写,否则报错
- write 时只有当前连接能读写,其他人不能写,写命令会进入等待
unlock tables
释放所有表锁-
共享锁和排他锁
不论是表级锁还是行级锁,都存在共享锁(
**Share **
Lock,S 锁)和排他锁(**Exclusive **
Lock,X 锁)这两类:
跟juc的读写锁一个道理 共享锁(S 锁) :又称读锁,事务在读取记录的时候获取共享锁,允许多个事务同时获取(锁兼容)。
- 排他锁(X 锁) :又称写锁/独占锁,事务在修改记录的时候获取排他锁,不允许多个事务同时获取。如果一个记录已经被加了排他锁,那其他事务不能再对这条事务加任何类型的锁(锁不兼容)。
排他锁与任何的锁都不兼容,共享锁仅和共享锁兼容。
S 锁 | X 锁 | |
---|---|---|
S 锁 | 不冲突 | 冲突 |
X 锁 | 冲突 | 冲突 |
由于 MVCC 的存在,对于一般的 SELECT 语句,InnoDB 不会加任何锁。不过, 你可以通过以下语句显式加共享锁或排他锁。
# 共享锁 SELECT ... LOCK IN SHARE MODE;
# 排他锁 SELECT ... FOR UPDATE;
意向锁
- 如果需要用到表锁的话,如何判断表中的记录没有行锁呢?一行一行遍历肯定是不行,性能太差。我们需要用到一个叫做意向锁的东东来快速判断是否可以对某个表使用表锁。
- 貌似因为表锁与行锁不能共存
- 意向锁是表级锁,共有两种:
- 意向共享锁(Intention Shared Lock,IS 锁):事务有意向对表中的某些加共享锁(S 锁),加共享锁前必须先取得该表的 IS 锁。
- 意向排他锁(Intention Exclusive Lock,IX 锁):事务有意向对表中的某些记录加排他锁(X 锁),加排他锁之前必须先取得该表的 IX 锁。
- 意向锁是有数据引擎自己维护的,用户无法手动操作意向锁,在为数据行加共享 / 排他锁之前,InooDB 会先获取该数据行所在在数据表的对应意向锁。
- 意向锁之间是互相兼容的。意向锁和共享锁和排它锁互斥(这里排斥指的是表级别的共享锁和排他锁,意向锁不会与行级的共享锁和排他锁互斥)。 | | IS 锁 | IX 锁 | | —- | —- | —- | | IS 锁 | 兼容 | 兼容 | | IX 锁 | 兼容 | 兼容 |
IS 锁 | IX 锁 | |
---|---|---|
S 锁 | 兼容 | 互斥 |
X 锁 | 互斥 | 互斥 |