事务
ACID 特性
原子性(A)
事务操作要么都做(提交),要么都不做(回滚),事务是访问并更新数据库各种数据项的一个程序执行单元,是一个不可分割的单位。 通过 undo log 来实现回滚操作,undo log 记录事务的 DML 操作,当回滚时,回放事务 DML 操作的逆运算。 在 MVCC 中,undo log 记录事务 DML 操作提交后产生的行数据版本信息。 一致性(C)
一致性指事务将数据库从一种一致性状态转变为下一种一致性的状态,在事务执行前后,数据库完整性约束没有被破坏。
例如:一个表的姓名有唯一约束,如果一个事务对姓名进行修改,但是在事务提交或事务回滚后,表中的姓名变得不唯一了,这样就破坏了一致性。 逻辑上的一致性是可以被破坏的。(设置不同程度的隔离级别适当地破坏逻辑上的一致性) 一致性由原子性、隔离性以及持久性共同来维护。 隔离性(I)
隔离性表示各个事务之间相互影响的程度。 目的:防止多个并发事务交叉执行导致数据不一致。 通过设置不同程度的隔离级别,适当地破环逻辑上的一致性,从而提高性能。 通过 MVCC 和 锁来实现。
MVCC:多版本并发控制,它不使用锁来限制读操作,从而实现高效并发读性能。 锁用来处理并发 DML 操作,数据库中提供粒度锁的策略,针对表(聚簇索引 B+ 树)、页(聚簇索引 B+ 树叶子节点)、行(叶子节点当中某一段记录行)三种粒度加锁。 持久性(D)
事务一旦完成,要将数据所做的变更记录下来,包括数据存储和多副本的网络备份。 事务提交后,事务 DML 操作将会持久化(写入 redo log 磁盘文件:哪一个页、页偏移值、具体数据 ),即使发生宕机等故障,数据库也能将数据恢复。 redo log 记录的是物理日志,确保内存数据的安全 。
隔离级别
目的:提升 MySQL 并发处理 SQL 语句的性能。 ISO 和 ANIS SQL 标准制定了四种事务隔离级别的标准,MySQL innodb 默认支持的隔离级别是 repeatable read。 read uncommitted(读未提交)
读操作不做任何处理。 写操作加 X 锁,写锁在事务提交或回滚后释放。 read committed(读已提交)(RC)
读操作使用 MVCC,读取最新版本的行数据。 写操作加 X 锁。 repeatable read(可重复读)(RR)
读操作使用 MVCC,读取事务开始前版本的行数据。 写操作加 X 锁。 serializable(可串行化)
读操作加 S 锁,所以事务都是串行化执行,此时隔离级别最严苛。 写操作加 X 锁。
SET [ GLOBAL | SESSION ] TRANSACTION ISOLATION LEVEL REPEATABLE READ ;
SET @@tx_isolation = 'REPEATABLE READ' ;
SET @@global.tx_isolation = 'REPEATABLE READ' ;
SELECT @@global.tx_isolation ;
SELECT @@session.tx_isolation ;
SELECT @@tx_isolation ;
SELECT . . . LOCK IN SHARE MODE ;
SELECT . . . FOR UPDATE ;
SELECT * FROM information_schema. innodb_locks;
不同隔离级别并发异常
脏读
事务 A 读到 事务 B 未提交的数据 ,也就是事务 A 读到脏数据。
seq session A session B 1 SET @@tx_isolation=‘READ UNCOMMITTED’; SET @@tx_isolation=‘READ UNCOMMITTED’; 2 BEGIN; 3 UPDATE account_t SET money = money - 100 WHERE name = ‘A’; 4 BEGIN; 5 SELECT money FROM account_t WHERE name = ‘A’; 6 COMMIT COMMIT
不可重复读
一个事务内两次读取同一个数据不一样 。 一般而言,不可重复读的问题是可以接受的,因为读到已经提交的数据,一般不会带来很大的问题,所以很多厂商(如 Oracle、SQL Server)默认隔离级别就是 read committed。
seq session A session B 1 SET @@tx_isolation=‘READ COMMITTED’; SET @@tx_isolation=‘READ COMMITTED’; 2 BEGIN; BEGIN; 3 SELECT money FROM account_t WHERE name = ‘A’; 4 UPDATE account_t SET money = money - 100 WHERE name = ‘A’; 5 COMMIT; 6 SELECT money FROM account_t WHERE name = ‘A’; 6 COMMIT;
幻读
一个事务内两次读取同一个范围内 的记录得到的结果集不一样 。 快照读和当前读不一致
。 在 repeatable read 隔离级别下通过读加锁 解决。
seq session A session B 1 SET @@tx_isolation=‘REPEATABLE READ’; SET @@tx_isolation=‘REPEATABLE READ’; 2 BEGIN; BEGIN; 3 SELECT * FROM account_t WHERE id >= 2; 4 INSERT INTO account_t(id,name,money) VALUES (4,‘D’,1000); 5 COMMIT; 6 INSERT INTO account_t(id,name,money) VALUES (4,‘D’,1000); # 报错,因为幻读
seq session A session B 1 SET @@tx_isolation=‘REPEATABLE READ’; SET @@tx_isolation=‘REPEATABLE READ’; 2 BEGIN; BEGIN; 3 SELECT * FROM account_t WHERE id >= 2 lock in share mode; 4 INSERT INTO account_t(id,name,money) VALUES (4,‘D’,1000); # 等待执行 5 INSERT INTO account_t(id,name,money) VALUES (4,‘D’,1000); # 等待执行 6 COMMIT; # 报错,因为破坏了数据库完整性约束
MVCC
MVCC 是一致性非锁定读,也就是读不加锁。 每一次开启事务的时候,MySQL 都会为其创建一个唯一的事务 id(长度为 64 位,并且一直递增)。 read view
m_ids:创建 read view 时,已启动但未提交的事务 id 列表。 min_trx_id:创建 read view 时,已启动但未提交的最小事务 id。 max_trx_id:创建 read view 时,预分配给下一个未开启事务的 id。 creator_trx_id:创建该 read view 的事务 id。 聚簇索引记录的隐藏列
trx_id
事务修改记录时,trx_id 记录该修改事务 id。 roll_pointer
事务修改记录时,将旧记录写入 undo log,该指针指向旧版本记录。
锁
flush tables with read lock
unlock tables
表级锁
行级锁
记录锁(record lock)
共享锁(S)
事务读操作加的锁,对某一行加锁。 在 serializable 隔离级别下,默认给读操作加共享锁。 在 RR 隔离级别下,需手动加共享锁,可解决幻读问题。 在 RC 隔离级别下,没必要加共享锁,采用的是 MVCC。 在 read uncommitted 隔离级别下,既没有加锁也没有使用 MVCC。 排他锁(X)
事务删除或更新加的锁,对某一行加锁。 在 4 种隔离级别下,都添加了排他锁,事务提交或事务回滚后释放锁。 间隙锁(gap lock)
RR 隔离级别下,where 条件语句未命中时会自动添加间隙锁。 防止其他事务在记录间隙插入新的记录,从而避免幻读现象。 间隙锁会锁定一个范围,加锁区间为 (row1, row2)。 临键锁(next-key lock)
记录锁 + 间隙锁。 加锁区间为 (row1, row2]。 查询
MVCC:undo log 实现历史版本记录。 S 锁:lock in share mode 。 X 锁:for update 。 不做任何处理:read uncommitted 使用的策略。 删除、更新
插入
使用插入意向锁(特殊的 gap 锁)和 X 锁。
在多事务同时写入不同数据至同一索引间隙的时候,并不需要等待其他事务完成,不会发生锁等待。 auto-inc lock:特殊表锁实现。
锁兼容
锁 GAP(持有) Insert Intention(持有) Record(持有) Next-key(持有) GAP(请求) 兼容 兼容 兼容 兼容 Insert Intention(请求) 冲突 兼容 兼容 冲突 Record(请求) 兼容 兼容 冲突 冲突 Next-key(请求) 兼容 兼容 冲突 冲突
横向:表示已经持有的锁。 纵向:表示正在请求的锁。 一个事务已经获取了插入意向锁,对其他事务是没有任何影响的。 一个事务想要获取插入意向锁,如果有其他事务已经加了 gap lock 或 next-key lock 则会阻塞,这个是重点,死锁之源。
锁的对象
死锁
死锁原因:并发事务在执行过程中,因争夺锁资源而造成的一种互相等待的现象。 相反加锁顺序死锁
不同表加锁顺序相反。 相同表不同行加锁顺序相反。
给辅助索引行加锁的时候,同时会给聚簇索引行加锁。 使用外键索引时,给父表加锁,同时隐含给子表加锁。 解决:调整加锁顺序。 锁冲突死锁
RR 隔离级别下,插入意向锁与 gap 锁冲突死锁。一个事务想要获取插入意向锁,如 果有其他事务已经加了 gap lock 或 next-key lock 则会阻塞。 解决:降低隔离级别至 RC。 如何避免死锁
尽可能以相同顺序来访问索引记录和表。 如果能确定幻读和不可重复读对应用影响不大,考虑将隔离级别降低为 RC。 添加合理的索引,不走索引将会为每一行记录加锁,死锁概率非常大。 尽量在一个事务中锁定所需要的所有资源,减小死锁概率。 避免大事务,将大事务分拆成多个小事务,大事务占用资源多,耗时长,冲突概率变高。 避免同一时间点运行多个对同一表进行读写的概率。 查看死锁
系统表
CREATE TABLE innodb_monitor ( a INT )
ENGINE = INNODB ;
DROP TABLE innodb_monitor;
CREATE TABLE innodb_lock_monitor ( a INT )
ENGINE = INNODB ;
DROP TABLE innodb_lock_monitor
系统参数
set GLOBAL innodb_status_output= ON ;
set GLOBAL innodb_status_output= OFF ;
set GLOBAL innodb_status_output_locks= ON ;
set GLOBAL innodb_status_output_locks= OFF ;
set GLOBAL innodb_print_all_deadlocks= ON ;
命令
select * from information_schema. INNODB_TRX;
select * from information_schema. INNODB_LOCKS;
select * from information_schema. INNODB_LOCK_WAITS;