一、概述
为保证数据的一致性和完整性,需要对 事务间并发操作进行控制 ,因此产生了 锁 。锁冲突 也是影响数据库 并发访问性能 的一个重要因素。所以锁对数据库而言显得尤其重要,也更加复杂。
二、并发问题
- MySQL并发事务访问相同记录
(1)读读情况
允许这种情况的发生
(2)写-写情况
该图描述:当一个事务想对这条记录做改动时,首先会看看内存中有没有与这条记录关联的 锁结构 ,当没有的时候就会在内存中生成一个 锁结构 与之关联。等其它事务再次访问该条记录时候,若已经有锁与之关联,那么就需要等待,自身的锁结构为true。
(3)读写情况
即一个事务进行读取操作,另一个进行改动操作。这种情况下可能发生 脏读 、 不可重复读 、 幻读 的问题。注意: MySQL在 REPEATABLE READ 隔离级别上就已经解决了 幻读 问题。 - 并发问题的解决方案
(1)方案一:读操作利用多版本并发控制( MVCC ,下章讲解),写操作进行 加锁 。
(2)方案二:读、写操作都采用 加锁 的方式。
两个方案对比:
(1)采用 MVCC 方式的话, 读-写 操作彼此并不冲突, 性能更高 。
(2)采用 加锁 方式的话, 读-写 操作彼此需要 排队执行 ,影响性能。
三、锁的分类(不同角度)
从数据操作的类型划分:读锁、写锁
- 读锁 :也称为 共享锁 、英文用 S 表示。针对同一份数据,多个事务的读操作可以同时进行而不会
互相影响,相互不阻塞的。 - 写锁 :也称为 排他锁 、英文用 X 表示。当前写操作没有完成前,它会阻断其他写锁和读锁。
注意:对于 InnoDB 引擎来说,读锁和写锁可以加在表上,也可以加在行上
对读取的记录加X锁:SELECT … LOCK IN SHARE MODE; SELECT … FOR SHARE;
对读取的记录加S锁:SELECT … FOR UPDATE;
从锁粒角度划分
表级别
- 表级别的 X锁、S锁:普通的读锁、写锁。注意: InnoDB 存储引擎提供的表级 S锁 或者 X锁 是相当鸡肋,只会在一些特殊情况下,比方说崩溃恢复过程中用到。
- 表级别的 意向锁:给更大一级别的空间(数据页或数据表)示意里面是否已经上过锁。注意:也就是说其实IS锁和IX锁是兼容的(IX、IX也兼容),并且它们也都与行级别的X锁、S锁兼容
- 表级别的 AUTO-INC锁
系统实现这种自动给 AUTO_INCREMENT 修饰的列递增赋值的原理主要是两个:
(1)在执行插入语句时就在表级别加一个 AUTO-INC 锁,然后为每条待插入记录的 AUTO_INCREMENT 修饰的列分配递增的值,在该语句执行结束后,再把 AUTO-INC 锁释放掉。这样一来,该语句执行时候,不会受到其它事务的执行语句影响。(不确定要插入数量情况下)
(2)采用一个轻量级的锁,在为插入语句生成 AUTO_INCREMENT 修饰的列的值时获取一下这个轻量级锁,然后生成本次插入语句需要用到的 AUTO_INCREMENT 列的值之后,就把该轻量级锁释放掉,不需要等到整个插入语句执行完才释放锁。(确定要插入数量情况下)
行级别
-
行级别的 记录锁
普通的读锁(S)锁、写(X)锁。 对一行记录锁定 -
行级别的 间隙锁
引出:为了解决幻读问题而生
概念:锁定两条记录之间间隙(左开右开),使其中不能插入数据,也就防止了幻读问题产生。
注意:如果对一条记录加了 gap锁 (不论是 共享gap锁 还是 独占gap锁 ),并不会限制其他事务对这条记录加 正经记录锁 或者继续加 gap锁。这也说明了间隙锁只为解决防止插入幻影记录而生。
特例:给最后一条记录或者给Supremum加gap锁之后,可以阻止其他事务插入 number 值在 (20, +∞) 这个区间的新记录 -
行级别的 临键锁
概念:一句话,记录锁与间隙锁的合体,左闭右开
例如这个,在( 3,8 ] 这个区间中在锁还没有释放之前(拥有 gap锁 的该事务提交之前)不能插入记录。也就是,它既能保护该条记录,又能阻止别的事务
将新记录插入被保护记录前边的 间隙 。 -
行级别的 插入意向锁(想要在间隙锁中保护的间隙中插入记录,等待时,会生成一个插入意向锁)
概念:设计 InnoDB 的大叔规定事务在等待的时候也需要在内存中生成一个 锁结构 ,表明有事务想在某个 间隙 中插入新记录,但是现在在等待。
注意:插入意向锁并不会阻止别的事务继续获取该记录上任何类型的锁
页级别
- 页锁
页锁的开销介于表锁和行锁之间,会出现死锁。锁定粒度介于表锁和行锁之间,并发度一般。
每个层级的锁数量是有限制的,因为锁会占用内存空间, 锁空间的大小是有限的 。当某个层级的锁数量
超过了这个层级的阈值时,就会进行 锁升级 。锁升级就是用更大粒度的锁替代多个更小粒度的锁,比如
InnoDB 中行锁升级为表锁,这样做的好处是占用的锁空间降低了,但同时数据的并发度也下降了。
从锁的态度划分
悲观锁
概念:对数据被其他事务的修改持保守态度,会通过数据库自身的锁机制来实现,从而保证数据操作的排它性。每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会 阻塞 直到它拿到锁
乐观锁
概念:乐观锁认为对同一数据的并发操作不会总发生,属于小概率事件,不用每次都对数据上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据。不采用数据库自身的锁机制,而是通过程序来实现。
思路:一条记录,事务A读一次数据,version是1,然后进行修改,判断version是否为1,如果读的时候version是1,改的时候还是1,那么这就说明,在两次操作之间没有其它事务对该条记录操作。(以版本号机制为例子)
实现方式:
(1)乐观锁的版本号机制
(2)乐观锁的时间戳机制(原理相同)
加锁方式
显示锁
通过特定的语句进行加锁,我们一般称之为显示加锁,例如:
(1)显示加共享锁: select … lock in share mode
(2)显示加排它锁: select … for update
隐式锁
概念:一个事务对新插入的记录可以不显式的加锁(生成一个锁结构),但是由于事务id 这个牛逼的东东的存在,相当于加了一个 隐式锁 。(必须加一个锁的原因,在一个事务中新插入的记录,并发的事务会对该记录进行读、或者写操作,这就造成了脏读、脏写)
事务id起的作用:
(1)对于聚簇索引记录来说,有一个 trx_id 隐藏列,该隐藏列记录着最后改动该记录的 事务id 。那么如果在当前事务中新插入一条聚簇索引记录后,该记录的 trx_id 隐藏列代表的的就是当前事务的事务id ,如果其他事务此时想对该记录添加 S锁 或者 X锁 时,首先会看一下该记录的 trx_id 隐藏列代表的事务是否是当前的活跃事务,如果是的话,那么就帮助当前事务创建一个 X锁 (也就是为当前事务创建一个锁结构, is_waiting 属性是 false ),然后自己进入等待状态(也就是为自己也创建一个锁结构, is_waiting 属性是 true )。
(2)对于二级索引记录来说,本身并没有 trx_id 隐藏列,在二级索引页面的 Page Header 部分有一个 PAGE_MAX_TRX_ID 属性,该属性代表对该页面做改动的最大的 事务id ,如果 PAGE_MAX_TRX_ID 属性值小于当前最小的活跃 事务id ,那么说明对该页面做修改的事务都已经提交了,则此时其他事务直接可以对该记录添加 S锁 或者 X锁, 否则就需要在页面中定位到对应的二级索引记录,然后回表找到它对应的聚簇索引记录,然后再根据该条聚簇索引的trx_id找到这条记录目前所在事务,则会为该事务创建一个 X锁结构,并且自己也创建一个进入等待状态。
其他
全局锁
全局锁就是对 整个数据库实例 加锁。当你需要让整个库处于 只读状态 的时候,可以使用这个命令,之后其他线程的以下语句会被阻塞:数据更新语句(数据的增删改)、数据定义语句(包括建表、修改表结构等)和更新类事务的提交语句。全局锁的典型使用 场景 是:做 全库逻辑备份 。
全局锁的命令: Flush tables with read lock
死锁
概念:在MySQL中,当多个事务同时请求相同的资源时,可能会发生死锁。死锁是指两个或多个事务互相等待对方释放资源,导致所有事务都无法继续执行的情况。
实际处理:
(1)第一种策略,直接进入等待,直到超时。这个超时时间可以通过参数innodb_lock_wait_timeout 来设置。
(2)第二种策略,发起死锁检测,发现死锁后,主动回滚死锁链条中的某一个事务(将持有最少行级排他锁的事务进行回滚),让其他事务得以继续执行。将参数 innodb_deadlock_detect 设置为on ,表示开启这个逻辑。
第二种策略成本分析:
(1)如果你能确保这个业务一定不会出现死锁,可以临时把死锁检测关掉。但是这种操作本身带有一定的风险,因为业务设计的时候一般不会把死锁当做一个严重错误,毕竟出现死锁了,就回滚,然后通过业务重试一般就没问题了,这是 业务无损 的。而关掉死锁检测意味着可能会出现大量的超时,这是业务有损 的。
(2)控制并发度。如果并发能够控制住,比如同一行同时最多只有10个线程在更新,那么死锁检测的成本很低。基本思路就是,对于相同行的更新,在进入引擎之前排队,这样在InnoDB内部就不会有大量的死锁检测工作了。
避免死锁
- 尽量减少事务的持有时间,尽快释放锁。
- 尽量减少事务中需要锁定的资源数量,避免同时请求相同的资源。
- 尽量按照相同的顺序请求资源,避免交叉锁定。
- 使用合适的隔离级别,例如使用READ COMMITTED隔离级别可以减少死锁的发生。
- 对于复杂的事务,可以使用分布式事务管理器来协调多个事务的操作,避免死锁的发生。