1. 事务
2. 锁
1. 事务
什么是事务?
事务是指一组业务操作,要么全部成功,要么全部失败。
比如银行转账业务,步骤一:从 A 账户减少 300 元;步骤二:向 B 账户增加 300 元。为了确保总的金额不变,就要维持数据的一致性,那么步骤一和步骤二两个操作必须全确认或者全取消。这里的每个步骤就可以理解为每个 SQL 语句。
事务的特性:ACID
- 原子性(Atomicity):一个事务要么全部成功(提交),要么全部失败(回滚),不能只完成其中的一部分操作。
- 一致性(Consistency):事务的执行不能破坏数据的完整性和一致性,一个事务在执行之前和执行之后,所有数据都必须处于一致的状态。
- 隔离性(Isolation):在并发环境中,事务是相互隔离的,一个事务的执行不能被其他事务干扰。
- 持久性(Durability):一旦事务提交,那么它对于系统或者数据的修改是永久性的。
事务语句
- begin(start transaction):显式开启一个事务。
- commit:提交事务。
- rollback:回滚事务。
- set autocommit:设置自动提交模式。
autocommit 默认为 on(打开),即我们每执行一条 SQL 都相当于一个事务并自动提交。
示例场景
场景 1:commit
Session 1 | Session 2 |
start transaction; -- 显式开启一个事务 |
|
delete from emp where ename=‘wang‘; |
|
select * from emp; -- 数据"wang"已删除 |
|
select * from emp; -- 数据"wang"未删除 |
|
commit; -- 提交事务 |
|
select * from emp; -- 数据"wang"已删除 |
|
select * from emp; -- 数据"wang"已删除 |
场景 2:rollback
Session 1 | Session 2 |
begin; -- 显式开启一个事务 |
|
insert into emp values(‘wang‘, now(), 4000, 2); |
|
select * from emp; -- 新增了"wang"数据 |
|
insert into emp values(‘liu‘, now(), 90000, 3); |
|
select * from emp; -- 新增了"liu"数据 |
|
select * from emp; -- 未新增两条数据 | |
rollback; -- 回滚事务 | |
select * from emp; -- 未新增两条数据 |
|
select * from emp; -- 未新增两条数据 |
事务的隔离级别
隔离问题:
- 脏读:一个事务读到另一个事务没有提交的数据。
- 不可重复读:一个事务读到另一个事务已提交的数据(update)。
- 虚读(幻读):一个事务读到另一个事务已提交的数据(insert)。
隔离级别:
-
Read Uncommitted:读未提交。允许事务督导其他事务未提交的数据变更。
- 存在 3 个问题:脏读、不可重复读、虚读
-
Read Committed:读已提交。允许事务读到其他事务已提交的数据变更。
- 解决 1 个问题:脏读
- 存在 2 个问题:不可重复读、虚读
-
Repeatable Read:可重复读。在同一个事务中保证读到的结果的一致性,是 InnoDB 默认的隔离级别。
- 解决 2 个问题:脏读、不可重复读
- 存在 1 个问题:虚读
-
Serializable :串行化。完全隔离了事务之间的影响,一个事务正在读的数据,另一个事务不允许修改。
- 解决 3 个问题:脏读、不可重复读、虚读
2. 锁
锁是一种使各种共享资源在被并发访问变得有序的机制,目的是为了保证数据的一致性。
锁的类型
根据加锁范围大致划分:
- 表锁:表锁会锁定整张表,其他事务无法操作或者无法变更操作。(Myisam)
- 行锁:只会锁定需要的行,并发性会更好,并且行锁一定是作用在索引上的。(InnoDB)
根据加锁功能大致划分:
- 共享锁(Shared Lock):一个事务并发读取某一行记录所需要持有的锁。
- 排他锁/独占锁(Exclusive Locks):一个事务并发更新或删除某一行记录所需要持有的锁。
共享锁/排他锁都只是行锁。
死锁
死锁发生在当两个事务均尝试获取对方已经持有的排他锁时。
在 innodb 中,select 不会对数据加锁,而 update/delete 会加行级别的独占锁。
当数据库的隔离级别为 Repeatable Read 或 Serializable 时,我们来看以下会发生死锁的并发事务场景。
表结构示例:
mysql> desc user; +-------+-------------+------+-----+---------+-------+ | Field | Type | Null | Key | Default | Extra | +-------+-------------+------+-----+---------+-------+ | id | int | YES | MUL | NULL | | | age | int | YES | MUL | NULL | | | name | varchar(30) | YES | | NULL | | +-------+-------------+------+-----+---------+-------+ 3 rows in set (0.12 sec)
场景 1:表锁
当 update 数据未作用于索引时,会发生表锁:
Session 1 | Session 2 |
begin; | begin; |
select * from user; | select * from user; |
update user set name=‘test1‘ where name=‘xiaoming‘; | |
(表)锁等待解除 | update user set name=‘test1‘ where name=‘xiaodan‘; |
死锁,事务被回滚 |
场景 2:行锁
当 update 数据作用于索引时,会发生行锁:
Session 1 | Session 2 |
begin; | begin; |
select * from user; | select * from user; |
update user set name=‘test1‘ where id=1; | |
(行)锁等待解除 | update user set name=‘test1‘ where id=2; |
死锁,事务被回滚 |
场景 3:死锁回滚
当 InnoDB 检测到死锁时,会回滚其中一个事务,让另一个事务得以完成。
Session 1 | Session 2 |
begin; | begin; |
select * from user; | select * from user; |
update user set name=‘test2‘ where id=1; | |
(行)锁等待解除 | update user set name=‘test2‘ where id=1; |
死锁,事务被回滚 |
mysql> update user set name=‘test2‘ where id=1; -- Session 2 ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
死锁的应对方案
Session 1 | Session 2 |
begin; | begin; |
select * from user; | select * from user; |
update user set name=‘test3‘ where id=1; | |
commit; | update user set name=‘test4‘ where id=1; |
commit; |
在这个并发场景下,两个事务均能成功提交,而不会有死锁。
如何减少死锁发生
- 使用合适的索引。
- 使用更小的事务。
- 经常性的提交事务,避免事务被挂起。
select * from emp; -- 未新增两条数据