一、锁
1.1 行级锁和表级锁
- 记录锁(Record Lock):只对当前操作的行进行加锁,能大大减少数据库操作的冲突,加锁粒度最小、并发度高,但加锁的开销也最大、加锁慢、会出现死锁。记录锁锁住的是索引而非记录本身。
- 间隙锁(Gap Lock):锁定一个范围但不包括记录本身,其目的是防止其他事务在间隙中新增数据造成幻读。
- 临键锁(Next-Key Lock):记录锁和间隙锁的结合,锁定一个范围,并且锁定记录本身,是 InnoDB 默认的锁,对于行的查询都是采用该方法,可以解决虚读和幻读。
- 表级锁:对当前操作的整张表加锁,锁定粒度最大的一种锁,它实现简单、加锁快、资源消耗较少,但并发能力低。
1.2 S、X、IS、IX、SIX 锁
- S 锁(Shared Lock):阻塞其他事务写数据。若事务 T 对数据对象 A 上 S 锁,则其他事务只能再对 A 上 S 锁,而不能上 X 锁,直到 T 释放 A 上的 S 锁。这就保证了其他事务可以读 A,但在 T 释放 A 上的 S 锁之前不能对 A 做任何修改。通常该页被读取完毕,S 锁立即被释放。
- X 锁(Exclusive Lock):阻塞其他事务读和写数据。若事务 T 对数据对象 A 上 X 锁,则只允许 T 读取和修改 A,其他任何事务都不能再对 A 加任何类型的锁,直到 T 释放 A 上的 X 锁。这就保证了其他事务在 T 释放 A 上的锁之前不能再读取和修改 A。
- IS 锁(Intention Shared Lock):当事务准备在某条记录上加 S 锁时,需要先在表级别加一个 IS 锁。
- IX 锁(Intention Exclusive Lock):当事务准备在某条记录上加 X 锁时,需要先在表级别加一个 IX 锁。
- SIX 锁(Shared Intention Exclusive Lock):SIX 锁表示表级共享锁与意向排它锁的组合。它表示该事务要读整个表,同时会更新个别元组。
S 锁与 X 锁既可以是行级锁也可以是表级锁,IS 锁与 IX 锁默认是表级锁,意向锁的提出仅仅为了在之后加 S 锁和 X 锁时可以快速判断表中的记录是否被上锁,以避免用遍历的方式来查看表中有没有上锁的记录。同时,如果一个表中有多个行锁,他们都会给表加上意向锁,意向锁和意向锁之间是不会冲突的。
二、事务隔离
2.1 并发事务问题
数据库并发使用时可能出现以下并发事务问题:
- 脏读:当一个事务读取另一个事务尚未提交的修改时,产生脏读。
- 不可重复读(虚读):同一查询在同一事务中多次进行,在此期间由于其他事务提交了对数据的修改或删除操作,每次返回不同的结果。
- 幻读:同一查询在同一事务中多次进行,在此期间由于其他提交事务提交了对数据的插入操作,每次返回不同的结果。
- 丢失修改:多个事务同时操作同一数据时,每个事务都不知道其他事务的存在,最后进行更新的事务会重写其他事务的更新。
幻读其实可以看作是不可重复读的一种特殊情况,单独区分幻读的主要原因是二者的解决方案不同。记录更新和删除时,通过记录锁即可解决并发事务问题,但是插入操作的并发安全必须借助于间隙锁或者临键锁。
2.2 隔离级别
为了解决上述并发事务问题,提出了事务的四种隔离级别:
- 读未提交:一个事务还未提交,它所做的变更就能被其它事务看到,所有并发事务问题都有可能发生。
- 读已提交:一个事务提交后,它所做的变更才能被其他事务看到,不允许脏读。
- 可重复读:对同一字段的多次读取结果都是一致的,不允许脏读、不可重复读。
- 串行化:最高的隔离级别,强制事务串行执行,不允许任何并发事务问题。
部分数据库的默认隔离级别和最高隔离级别:
2.3 两阶段锁
两阶段锁(Two-Phase Locking,2PL)是一种常见的并发控制协议,用于管理数据库中的并发事务。大多数关系型数据库都以 2PL 作为其默认的或可配置的并发控制机制,并在不同的隔离级别下定义不同的 2PL 行为。
2PL 将锁处理分为了两个阶段:
- 增长阶段:只允许事务从锁管理器中请求锁或者进行锁升级。
- 收缩阶段:只允许事务释放或降级自己所持有的锁。
下图为 2PL 中事务的锁数量随时间的变化:
不过 2PL 本身还存在两个问题:
- 在增长阶段,锁是逐渐获取的,这就很有可能出现多个事务交叉等待锁的情况,从而导致死锁。因此 2PL 需要辅以死锁检测,例如可以通过判断锁依赖图中是否又环来检测死锁,并终止环中的最年轻的事务。
- 在收缩阶段,锁是逐渐释放的,这就有可能导致脏读,如果当前事务异常终止还有可能导致级联回滚,由此提出了强两阶段锁(Strong Strict Two-Phase Locking,SS2PL)。与 2PL 不同,SS2PL 会在收缩阶段一次性释放所有的锁,从而避免脏读。
下图为 SS2PL 中事务的锁数量随时间的变化: