事物与锁
事物
当多个用户访问同一数据时,一个用户在更改数据的过程中可能有其它用户同时发起更改请求,为保证数据的一致性状态,MySQL 引入了事务。
事务可以将一系列的数据操作捆绑成一个整体进行统一管理,如果某一事务执行成功,则在该事务中进行的所有数据更改均会提交,成为数据库中的永久组成部分。如果事务执行时遇到错误,则就必须取消或回滚。取消或回滚后,数据将全部恢复到操作前的状态,所有数据的更改均被清除。
数据库的事务(Transaction)是一种机制、一个操作序列,包含了一组数据库操作命令。
事务具有 4 个特性,即原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)和持久性(Durability)。
支持事务的存储引擎有 InnoDB 和 BDB,其中,InnoDB 存储引擎事务主要通过 UNDO 日志和 REDO 日志实现,MyISAM 存储引擎不支持事务。
日志操作:
- UNDO 日志:复制事务执行前的数据,用于在事务发生异常时回滚数据。
- REDO 日志:记录在事务执行中,每条对数据进行更新的操作,当事务提交时,该内容将被刷新到磁盘。
默认设置下,每条 SQL 语句就是一个事务,即执行 SQL 语句后自动提交。
用 BEGIN 或 START TRANSACTION 开启一个事务,或者禁止当前会话的自动提交。
事物执行语法
1 | // 显式地标记一个事务的起始点。 |
BEGIN 或 START TRANSACTION 语句后面的 SQL 语句对数据库数据的更新操作都将记录在事务日志中,直至遇到 ROLLBACK 语句或 COMMIT 语句。
如果事务中某一操作失败且执行了 ROLLBACK 语句,那么在开启事务语句之后所有更新的数据都能回滚到事务开始前的状态。
如果事务中的所有操作都全部正确完成,并且使用了 COMMIT 语句向数据库提交更新数据,则此时的数据又处在新的一致状态。
注意:
- 事务尽可能简短
- 事务中访问的数据量尽量最少
- 查询数据时尽量不要使用事务
- 在事务处理过程中尽量不要出现等待用户输入的操作
MySQL 默认开启事务自动提交模式,每条 SOL 语句都会被当做一个单独的事务自动执行。
关闭自动提交后,该位置会作为一个事务起点,直到执行 COMMIT 语句和 ROLLBACK 语句后,该事务才结束。结束之后,这就是下一个事务的起点。
事物隔离
事务的隔离性就是指当多个事务同时运行时,各事务之间相互隔离,不可互相干扰。
事务并发时就容易出现脏读、不可重复读和幻读等情况。
脏读是指一个事务正在访问数据,并且对数据进行了修改,但是这种修改还没有提交到数据库中,这时,另外一个事务也访问这个数据,然后使用了这个数据。
在这个事务还没有结束时,另外一个事务也访问了该同一数据。那么,在第一个事务中的两次读数据之间,由于第二个事务的修改,那么第一个事务两次读到的的数据可能是不一样的。这样在一个事务内两次读到的数据是不一样的,因此称为是不可重复读。
幻读是指当事务不是独立执行时发生的一种现象。
事务隔离级别如下:
- 读未提交(READ UNCOMITTED)
- 读提交(READ COMMITTED)
- 可重复读(REPEATABLE READ)
- 串行化(SERIALIZABLE)
如果一个事务读取到了另一个未提交事务修改过的数据,那么这种隔离级别就称之为读未提交。
读提交就是只能读到已经提交了的内容。满足了隔离的简单定义:一个事务从开始到提交前所做的任何改变都是不可见的,事务只能读取到已经提交的事务所做的改变。
可重复读是 MySQL 的默认事务隔离级别,它能确保同一事务的多个实例在并发读取数据时,会看到同样的数据行。在该隔离级别下,如果有事务正在读取数据,就不允许有其它事务进行修改操作,这样就解决了可重复读问题。
如果一个事务先根据某些条件查询出一些记录,之后另一个事务又向表中插入了符合这些条件的记录,原先的事务再次按照该条件查询时,能把另一个事务插入的记录也读出来。那么这种隔离级别就称之为串行化。
SERIALIZABLE 是最高的事务隔离级别,主要通过强制事务排序来解决幻读问题。简单来说,就是在每个读取的数据行上加上共享锁实现,这样就避免了脏读、不可重复读和幻读等问题。
锁机制
锁机制是为了解决数据库的并发控制问题而产生的。
按锁级别分类,可分为共享锁、排他锁和意向锁。
按锁粒度分类,可分为行级锁、表级锁和页级锁。
共享锁的代号是 S,也可称为读锁。是一种可以查看但无法修改和删除的数据锁。共享锁的锁粒度是行或者元组(多个行)。一个事务获取了共享锁之后,可以对锁定范围内的数据执行读操作。会阻止其它事务获得相同数据集的排他锁。
排他锁的代号是 X,是 eXclusive 的缩写,也可称为写锁,是基本的锁类型。粒度与共享锁相同,也是行或者元组。一个事务获取了排他锁之后,可以对锁定范围内的数据执行写操作。允许获得排他锁的事务更新数据,阻止其它事务取得相同数据集的共享锁和排他锁。
意向锁是一种表锁,锁定的粒度是整张表,分为意向共享锁(IS)和意向排他锁(IX)两类。
“有意”表示事务想执行操作但还没有真正执行。
锁和锁之间的关系,要么是相容的,要么是互斥的。
- 锁 a 和锁 b 相容是指:操作同样一组数据时,如果事务 t1 获取了锁 a,另一个事务 t2 还可以获取锁 b;
- 锁 a 和锁 b 互斥是指:操作同样一组数据时,如果事务 t1 获取了锁 a,另一个事务 t2 在 t1 释放锁 a 之前无法释放锁 b。
如果一个事务请求的锁模式与当前的锁兼容,InnoDB 就将请求的锁授予该事务;反之,如果两者不兼容,该事务就要等待锁释放。
表锁、行锁和页锁
MySQL 按锁的粒度可以细分为行级锁、页级锁和表级锁。
锁粒度表示锁范围。
表级锁为表级别的锁定,会锁定整张表,可以很好的避免死锁,是 MySQL 中最大颗粒度的锁定机制。
一个用户在对表进行写操作(插入、删除、更新等)时,需要先获得写锁,这会阻塞其它用户对该表的所有读写操作。没有写锁时,其它读取的用户才能获得读锁,读锁之间是不相互阻塞的。
使用表级锁的主要是 MyISAM,MEMORY,CSV 等一些非事务性存储引擎。
页级锁的颗粒度介于行级锁与表级锁之间,所以获取锁定所需要的资源开销,以及所能提供的并发处理能力同样也是介于上面二者之间。另外,页级锁和行级锁一样,会发生死锁。
页级锁主要应用于 BDB 存储引擎。
行级锁的锁定颗粒度在 MySQL 中是最小的,只针对操作的当前行进行加锁,所以行级锁发生锁定资源争用的概率也最小。行级锁也最容易发生死锁。
行级锁主要应用于 InnoDB 存储引擎。
表级锁适合以查询为主,只有少量按索引条件更新数据的应用,如 Web 应用。而行级锁更适合于有大量按索引条件,同时又有并发查询的应用,如一些在线事务处理(OLTP)系统。
表级锁 | 行级锁 | 页级锁 | |
---|---|---|---|
开销 | 小 | 大 | 介于表级锁和行级锁之间 |
加锁 | 快 | 慢 | 介于表级锁和行级锁之间 |
死锁 | 不会出现死锁 | 会出现死锁 | 会出现死锁 |
锁粒度 | 大 | 小 | 介于表级锁和行级锁之间 |
并发度 | 低 | 高 | 一般 |
InnoDB的3种行锁
通过给索引上的索引项加锁来实现,如果没有索引,InnoDB 将通过隐藏的聚簇索引来对记录加锁。
支持 3 种行锁定方式:
- 行锁(Record Lock):直接对索引项加锁。
- 间隙锁(Gap Lock):锁加在索引项之间的间隙,也可以是第一条记录前的“间隙”或最后一条记录后的“间隙”。
- Next-Key Lock:行锁与间隙锁组合起来用就叫做 Next-Key Lock。
默认情况下,InnoDB 工作在可重复读(默认隔离级别)下,并且以 Next-Key Lock 的方式对数据行进行加锁,这样可以有效防止幻读的发生。
Next-Key Lock 是行锁与间隙锁的组合,这样,当 InnoDB 扫描索引项的时候,会首先对选中的索引项加上行锁(Record Lock),再对索引项两边的间隙(向左扫描扫到第一个比给定参数小的值, 向右扫描扫到第一个比给定参数大的值, 然后以此为界,构建一个区间)加上间隙锁(Gap Lock)。如果一个间隙被事务 T1 加了锁,其它事务不能在这个间隙插入记录。
禁止间隙锁的话,可以把隔离级别降为读已提交(READ COMMITTED),或者开启参数 innodb_locks_unsafe_for_binlog。
开启一个事务时,InnoDB 存储引擎会在更新的记录上加行级锁,此时其它事务不可以更新被锁定的记录。
死锁是指两个或两个以上的事务在执行过程中,因争夺资源而造成的一种互相等待的现象。
几种避免死锁的方法:
- 如果不同程序会并发存取多个表,或者涉及多行记录时,尽量约定以相同的顺序访问表,这样可以大大降低死锁的发生。
- 业务中要及时提交或者回滚事务,可减少死锁产生的概率。
- 在同一个事务中,尽可能做到一次锁定所需要的所有资源,减少死锁产生概率。
- 对于非常容易产生死锁的业务部分,可以尝试使用升级锁粒度,通过表锁定来减少死锁产生的概率(表级锁不会产生死锁)。