事务(Transaction),一般是指要做的或所做的事情。在计算机术语中是指访问并可能更新数据库中各种数据项的一个程序执行单元(unit)。事务通常由高级数据库操纵语言或编程语言(如SQL,C++或Java)书写的用户程序的执行所引起,并用形如begin transaction和end transaction语句(或函数调用)来界定。事务由事务开始(begin transaction)和事务结束(end transaction)之间执行的全体操作组成。 – 来自 百度百科
事务的ACID特性 和隔离级别
-
原子性
- 通过 undo log
-
一致性
-
持久性
- 通过 redolog
-
隔离性
- 读未提交
- 一个事务可以读取另外一个事务未提交的数据。
- 脏读
- 不可重复读
- 幻读
- 读已提交
- 一个事务可以读取另外一个事务已提交的数据。
- 不可重复读。
- 幻读
- 可重复读
- 一个事务开启后进行读取时,不会在受到另外一个事务操作的影响
- 幻读(innodb不存在)
- 串行化读
- 使执行串行化执行
- 读未提交
事务隔离级别 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|
读未提交 (Read Uncommitted) | 可能 | 可能 | 可能 |
读已提交 (Read Committed) | 不可能 | 可能 | 可能 |
可重复读(Repeatable Read) | 不可能 | 不可能 | InnoDB 不可能 |
串行化(serializable) | 不可能 | 不可能 | 不可能 |
可重复读 REPEATABLE READ
当存在两个事务 T1 / T2
T1 :对空表进行查询
BEGIN;
SELECT * FROM test_tx;
-- ROLLBACK;
T2:对空表进行插入
BEGIN;
INSERT INTO test_tx VALUES(1,"1");
-- ROLLBACK;
-- COMMIT;
此时默认都未 COMMIT 或 ROLLBACK 。 当 T2 事务进行COMMIT 操作后, 在T1的事务中是无法获得最新的提交记录。之后再次开启一个新的事务才能获得最新的提交记录。
读已提交 READ COMMITTED
当存在上面可重复读相同操作的两个事务时,当 T2 事务进行COMMIT 操作后, 在T1的事务中是可以获得最新的提交记录。
mysql 中的事务
当要保证一个事务前后读取两次数据的结果一致,有两个方案
- LBCC (Lock Base Concurrency Control) 锁的并发控制
- MVCC (Multi Version Concurrency Control) 多版本并发控制
普通的 select 采用快照读,底层采用 MVCC 来实现
加锁的 select 采用当前读,底层采用LBCC 来实现
锁
锁分为 共享锁(读锁)、排它锁(写锁)、意向共享锁、意向排它锁、记录锁、间隙锁、临键锁、自增锁
共享锁 :读锁 lock in share mode
多个事务之间可以共享同一把共享锁
select * from users WHERE id=1 LOCK IN SHARE MODE;
commit ;
-- rollback;
排他锁:写锁 : for update
只要有一个事务拿到排它锁,其他事务都不可以再拿到锁
delete / update / insert 默认加上X锁
SELECT * FROM table_name WHERE ... FOR UPDATE ;
commit;
rollback;
意向共享锁、意向排它锁
意向共享锁(IS) 表示事务准备给数据行加入共享锁,即一个数据行加共享锁前必须先取得该表的IS锁, 意向共享锁之间是可以相互兼容的
意向排它锁(IX) 表示事务准备给数据行加入排他锁,即一个数据行加排他锁前必须先取得该表的IX锁, 意向排它锁之间是可以相互兼容的
意向锁(IS、IX)是InnoDB数据操作之前自动加的,不需要用户干预
当事务想去进行锁表时,可以先判断意向锁是否存在,存在时则可快速返回该表不能 启用表锁
可以理解为是一个标志位, innodb 在去加共享锁或排它锁的时候,会先去判断,是否存在了某个锁,存在时则不需要再一次次的去判断,直接就可以返回。
记录锁
锁住具体的索引项 当sql执行按照唯一性(Primary key、Unique key)索引进行数据的检索时,查询条件等值匹 配且查询的数据是存在,这时SQL语句加上的锁即为记录锁Record locks,锁住具体的索引项
间隙锁
只存在 RR 级别下(可重复读)
- 如果对索引进行查询(范围或等于)时的记录不存在时,使用间隙锁
- 如:当前存在数据:id=1,2,3,4,5,6, 9,10 ;
- 当进行查询id=7时 ,会锁住**(6,9)** 开区间
- 当进行查询id=11时,会锁住(10,+∞)
临键锁
- 当等值匹配只有一条记录会退化为记录锁
- 当匹配不到记录时会退化为间隙锁
- 当使用范围查询时,锁住最后一个key 的下一个左开右闭的区间。
- 如:当前存在数据:id=1,2,3,4,5,6, 9,10 ;
- 当查询 id>6 时 ,
- 如果 6存在则从6开始开区间**(6,…]**
- 如果6不存在,则从上一个存在的索引进行开区间,如果4存在,则**(4,…]**
- 当查询 id < 10
- 如果10存在,则到10 闭区间 …,10]
- 如果10不存在,则到 …,+∞]
- 当查询 id>6 时 ,
- 结论:当id >x and id <y 时
- 如果x存在则不锁x
- 如果x不存在则从前面有值(不包含)的开始一直锁
- 如果y存在则到y进行闭区间
- 如果y不存在则一直到无穷大闭区间
InnoDB 选择 临键锁作为行锁的默认算法,就是为了在可重复读模式下,解决幻读的问题
自增锁 AUTO-INC Locks
针对自增列自增长的一个特殊的表级别锁。 默认取值1,代表连续,事务未提交ID永久丢失
show variables like 'innodb_autoinc_lock_mode';
MVCC (多版本并发控制)
并发访问(读或写)数据库时,对正在事务内处理的数据做 多版本的管理。以达到用来避免写操作的堵塞,从而引发读操 作的并发问题。
MVCC 理论
- 可以查询到当前事务之前已经存在的数据,即使后面被修改了或删除了。
- 在当前事务之后的事务的操作是不可见的
创建版本 : 查询创建版本小于当前事务id:
- 事务开启前的数据是可以查询到的
删除版本:查询删除版本为空或大于当前事务id的
- 当前事务之后做的操作对本次事务没有影响
查询规则:
- 查找数据行版本早于当前事务版本的数据行
- 就是数据行的系统版本号小于或等于当前的事务版本号,这样可以确保事务读取的行,要么是在事务开始之前已经存在的,要么是该事务自身插入或者修改过的
- 查找删除版本号要么为null,要么大于当前事务版本号的记录
- 确保取出来的行记录是在事务开启之前没有被删除的。
可重复读 RR 和 读已提交 RC 的区别 ?
-
可重复读级别下
- 普通的 select 采用快照读,底层采用 MVCC 来实现
- 加锁的 select 采用当前读,底层采用记录锁、间隙锁和临键锁来实现
-
读已提交级别下
- 普通的 select 采用快照读,底层采用 MVCC实现
- 加锁的 select 采用当前读,底层都是使用记录锁;因为没有间隙锁,所以会出现幻读问题
-
RR 的间隙锁会导致锁定范围的扩大
-
如果查询条件未使用到索引时,RR是锁表,RC是锁行
-
如果能正确地使用锁,不会出现未用索引而加锁的情况,用默认RR 就可以了。
死锁
多个并发事务(2个或者以上);
每个事务都持有锁(或者是已经在等待锁);
每个事务都需要再继续持有锁;
事务之间产生加锁的循环等待,形成死锁
例如:
-- T1
begin;
update user set age = 20 where id=1;
update user_address set address='xx' where id=25;
-- T2
begin
update user_address set address='xxxxx' where id=25;
update user set age = 22 where id=1;
在事务T1中执行完user 表的操作时,而事务T2 也正好执行完user_address 的操作,此时T1 在操作时需要等待T2进行释放 user_address 的排他锁,而T2 也需要等待 T1 进行释放user 的排他锁,由于没有进行commit 或者rollback,则T1 和 T2 都需要等待对方进行释放锁,便形成了死锁。
死锁的避免
- 类似的业务逻辑以固定的顺序访问表和行。
- 大事务拆小。大事务更倾向于死锁,如果业务允许,将大事务拆小。
- 在同一个事务中,尽可能做到一次锁定所需要的所有资源,减少死锁概 率。
- 降低隔离级别,如果业务允许,将隔离级别调低也是较好的选择
- 为表添加合理的索引。可以看到如果不走索引将会为表的每一行记录添 加上锁(或者说是表锁)
InnoDB 支持的查看事务和锁的信息
-- 锁的超时时间默认是50秒
-- innodb 的锁
select * from `information_schema`.`INNODB_LOCKS`;
-- innodb 的锁等待
SELECT * FROM `information_schema`.`INNODB_LOCK_WAITS`;
-- innodb 的事务表
select * from `information_schema`.`INNODB_TRX`;