脏读,不可重复读与幻读
- 脏读: 幻读 一个事务中访问到了另外一个事务未提交的数据
- 不可重复读:一个事务内根据同一个条件对行记录进行多次查询,返回的结果不一致
- 幻读:同一个事务内多次查询返回的结果集不一样(增加了或者减少)
隔离级别
Mysql中有四种隔离级别:读未提交(read uncommit),读提交(read commit),可重复读(repeatable read)(默认),串行化读(serializable)。
mysql隔离界别有两个作用域,一个是当前会话隔离级别,另一个是系统隔离级别。
| 隔离级别 | 脏读 | 不可重复读 | 幻读 |
|---|---|---|---|
| 读未提交 | 可能 | 可能 | 可能 |
| 读提交 | 不可能 | 可能 | 可能 |
| 可重复读 | 不可能 | 不可能(MVCC实现) | 不可能(用next-key lock 保证) |
| 串行化读(serializable) | 不可能 | 不可能 | 不可能 |
读未提交(read uncommit)
一个事务还没提交时,它做的变更就能被别的事务看到。任何操作都不会加锁。
读提交(read commit)
一个事务提交之后,它做的变更才会被其他事务看到。
在RC级别中,数据的读取都是不加锁的,但是数据的写入、修改和删除是需要加锁的。
可重复读(repeatable read)(默认的)
一个事务执行过程中看到的数据,总是跟这个事务在启动时看到的数据是一致的。当然在可重复读隔离级别下,未提交变更对其他事务也是不可见的。
写加锁 读不加锁。
在MVCC 和2PL 的共同作用下, InnoDB 实现了"读不加锁, 读写不冲突" 的并发。
串行化读(serializable)
顾名思义是对于同一行记录,“写”会加“写锁”,“读”会加“读锁”。当出现读写锁冲突的时候,后访问的事务必须等前一个事务执行完成,才能继续执行。
锁
两段锁
将事务分成两个阶段,加锁阶段和释放阶段。加锁阶段:在该阶段可以进行加锁,如果不成功,事物进入到等待状态,直到加锁成功。解锁阶段:当事务释放了一个封锁以后,事务进入解锁阶段,在该阶段只能进行解锁操作不能再进行加锁操作。
工程实现中的两段锁(S2PL):在事务中只有提交(commit)或者回滚(rollback)时才是解锁阶段,其余时间为加锁阶段。
但是mysql存在违背两段锁的实现:如果一个条件无法通过索引快速过滤,存储引擎层面就会将所有记录加锁后返回,再由MySQL Server层进行过滤。在实际使用过程当中,MySQL做了一些改进,在MySQL Server过滤条件,发现不满足后,会调用unlock_row方法,把不满足条件的记录释放锁 (违背了二段锁协议的约束)。
共享锁 (lock in share mode),
允许不同事务之前共享加锁读取,但不允许其它事务修改或者加入排他锁
排他锁 (for update)
当一个事物加入排他锁后,不允许其他事务加共享锁或者排它锁读取,更加不允许其他事务修改加锁的行。
行锁(row-level locking)
InnoDB 默认为行级锁。InnoDB 行锁是通过给索引上的索引项加锁来实现的,这一点 MySQL 与 Oracle 不同,后者是通过在数据块中对相应数据行加锁来实现的。InnoDB 这种行锁实现的特点意味着:只有通过索引条件检索数据,InnoDB 才使用行级锁,否则,InnoDB 将使用表锁。
表锁
表锁的语法是 lock tables … read/write。
lock tables 语法除了会限制别的线程的读写外,也限定了本线程接下来的操作对象。比如: 举个例子, 如果在某个线程 A 中执行 lock tables t1 read, t2 write; 这个语句,则其他线程写 t1、读写 t2 的语句都会被阻塞。同时,线程 A 在执行 unlock tables 之前,也只能执行读 t1、读写 t2 的操作。连写 t1 都不允许,自然也不能访问其他表。
元数据锁(meta data lock) 是表级锁的一种, 不需要显式使用,在访问一个表的时候会被自动加上。(防止读写的时候对表结构进行更改)。当对一个表做增删改查操作的时候,加 MDL 读锁;当要对表做结构变更操作的时候,加 MDL 写锁。
理论上进行ddl变更时会锁表,大量线程处于“Waiting for meta data lock”状态。mysql 5.6提供了Online DDL的特性,解决了这个问题,Online DDL的简要原理为:
- 拿MDL写锁
- 降级成MDL读锁
- 真正做DDL
- 升级成MDL写锁、
- 释放MDL锁
Next-Key锁
Next-Key锁是行锁和GAP(间隙锁)的合并。
跟间隙锁存在冲突关系的,是“往这个间隙中插入一个记录”这个操作。间隙锁之间都不存在冲突关系。
间隙锁在可重复读隔离级别下才有效。
全局锁
- MySQL 提供了一个加全局读锁的方法,命令是 Flush tables with read lock (FTWRL)。当你需要让整个库处于只读状态的时候,可以使用这个命令
- 全局锁的典型使用场景是,做全库逻辑备份(InnoDB不需要,对于 MyISAM 这种不支持事务的引擎,如果备份过程中有更新,总是只能取到最新的数据,那么就破坏了备份的一致性。这时,我们就需要使用 FTWRL 命令了。)
Mysql中的死锁
死锁的定义:两个或两个以上的进程或事务相互等待
有两种解决死锁的方式:
- 一种策略是,直接进入等待,直到超时。这个超时时间可以通过参数 innodb_lock_wait_timeout 来设置。
- 另一种策略是,发起死锁检测,发现死锁后,主动回滚死锁链条中的某一个事务,让其他事务得以继续执行。将参数 innodb_deadlock_detect 设置为 on,表示开启这个逻辑。死锁的主动监测会消耗cpu资源,如果多个并发同时更新一行,会导致cpu飙升。解决的基本思路就是,对于相同行的更新,在进入引擎之前排队。这样在 InnoDB 内部就不会有大量的死锁检测工作了。
MVCC多版本并发控制
定义
同一条记录在系统中可以存在多个版本
基础概念
- 快照读:在MVCC下,一般的select都是快照读,读取的不是数据库中的最新值,而是事务启动时的一个快照
- 当前读:处理当前数据,需要加锁。(select * from table where ? lock in share mode 或者 select * from table where ? for update)
MVCC在InnoDB中的实现方式
Innodb存储引擎中,每行数据都包含了一些隐藏字段:DB_ROW_ID、DB_TRX_ID、DB_ROLL_PTR和DELETE_BIT。
-
DB_TRX_ID:用来标识最近一次对本行记录做修改的事务的标识符,即最后一次修改本行记录的事务id。delete操作在内部来看是一次update操作,更新行中的删除标识位DELELE_BIT。
-
DB_ROLL_PTR:指向当前数据的undo log记录,回滚数据通过这个指针来寻找记录被更新之前的内容信息。
-
DB_ROW_ID:包含一个随着新行插入而单调递增的行ID, 当由innodb自动产生聚集索引时,聚集索引会包括这个行ID的值,否则这个行ID不会出现在任何索引中。
-
DELELE_BIT:用于标识该记录是否被删除。
一行数据会对应多行这样的记录,例如,如果有多个事物对同一行数据进行更新,会形成这样的记。
事物启动的时候,mysql会为这个事物创建一个数组A,数组的元素为该事务启动瞬间,系统中启动了但还没提交的所有事务 ,数组中事务id的最小值记为low_limit_id,当前系统里面已经创建过的事务 ID 的最大值加 1 记为up_limit_id。
读取某一行数据,如果该数据最新的事务ID小于low_limit_id 那么该版本是可见的
如果事务ID大于up_limit_id 该版本不可见 根据回滚指针找到上一个版本记录
如果事务ID落在low_limit_id和up_limit_id 之间 如果数组A中包含该事务ID,该版本不可见,未包含该事务ID 该版本可见