mysql基础-锁

327 阅读8分钟

这是我参与8月更文挑战的第4天,活动详情查看:8月更文挑战

定义

锁是计算机协调多个进程或线程并发访问某一资源的机制。数据库锁机制简单说,就是数据库为了保证数据的一致性,而使各种共享资源在被并发访问变得有序所设计的一种规则。

锁的分类

锁按照不同的为,可以分为多种类别

对数据操作的类型分类

  1. 读锁(共享锁):针对同一份数据,多个读操作可以同时进行,不会互相影响。
  2. 写锁(排他锁):当前写操作没有完成前,它会阻断其他写锁和读锁。

对数据操作的粒度分类

为了尽可能提高数据库的并发度,每次锁定的数据范围越小越好,理论上每次锁定当前操作的数据的方案会得到最大的并发度,但管理锁是很耗费资源的事情(涉及获取、检查、释放等),因此数据库系统需要在高并发响应和系统性能两方面进行平衡,就产生了“锁粒度” 的概念

  1. 表级锁:开销小,加锁快;不会出现死锁;锁定粒度大,发生锁冲突的概率高,并发度最低。(MyISAM和Memory存储引擎采用的是表级锁)
  2. 行级锁:开销大,加锁慢;会出现死锁;锁定粒度最小;发生锁冲突的概率最低,并发度最高。(InnoDB支持行级锁,也支持表级锁,默认行级锁)
  3. 页面锁:开销和加锁时间介于表锁和行锁之间;会出现死锁;锁定粒度介于表锁和行锁之间,并发度一般。
  4. 适用:从锁的角度来说,表锁适合已查询为主,只有少量按索引条件更新数据的应用。行锁适合有大量按索引条件并发更新少量不同数据,同时又有并发查询的应用。

MyISAM表锁

  1. 表锁两种模式
表共享读锁:不会阻塞其他用户对同一表的读请求,但会阻塞对同一表的写请求。
表独占写锁:会阻塞其他用户对同一表的读或写请求。
  1. MyISAM表的读操作与写操作之间,以及写操作之间是串行的。
  2. 默认情况下,写锁比读锁具有更高的优先级:当一个锁释放时,这个锁会优先给写锁队列中等候的获取锁请求,然后再给读锁队列的获取锁请求。

InnoDB行锁

  1. InnoDB实现了2种行锁类型。
共享锁(S):允许一个事务去读一行,阻止其他事务获得相同数据集的排他锁。

排他锁(X):允许获得排他锁的事务更新数据,阻止其他事务取得相同数据集的共享锁和排他锁。
  1. 为了允许行锁和表锁共存,实现多粒度锁机制,InnoDB还有两种内部使用的意向锁(都是表锁)
意向共享锁(IS):事务打算给数据行加行共享锁,必须先取得该表的IS锁。

意向排它锁(IX):事务打算给数据行加排他锁,必须先取得该表的IX锁。
  1. 索引失效导致行锁变表锁。

乐观锁与悲观锁

乐观锁: 乐观地认为不会发生并发更新冲突,访问和处理数据过程中不加锁,只在更新数据时再根据版本号或时间戳判断是否有冲突,有则处理,无则提交事务。

悲观锁: 悲观地认为会发生并发更新冲突,访问和处理数据前就加排它锁,在整个数据处理过程中锁定数据,事务提交或回滚后才释放锁。悲观锁是由数据库自己实现的,要用的时候,我们直接调用数据库的相关语句即可。

InnoDB的三种行锁

记录锁(Record Locks) 单个行记录上的锁。对索引项加锁,锁定符合条件的行。其他事务不能修改和删除加锁项;

示例:在id=1的记录上加记录锁,以阻止其他事务插入,更新,删除 这一行

SELECT * FROM table WHERE id = 1 FOR UPDATE;

间隙锁(Gap Locks) 当使用范围条件而不是相等条件检索数据,并请求共享或排他锁时,InnoDB会给符合条件的已有数据记录的索引项加记录锁。对于键值在条件范围内但并不存在的记录,加间隙锁。

  • 间隙锁基于非唯一索引,使用间隙锁锁住的是一个区间,而不仅仅是这个区间中的每一条数据。
  • Gap锁的目的,是为了防止同一事务的两次当前读,出现幻读的情况。
-- 所有在(1,10)区间内的记录行都会被锁住,所有id 为 2、3、4、5、6、7、8、9 的数据行的插入会被阻塞,但是 1 和 10 两条记录行并不会被锁住。
SELECT * FROM table WHERE id BETWEN 1 AND 10 FOR UPDATE;

临键锁(Next-key Locks)

  • 记录锁与间隙锁的组合,它的封锁范围,既包括索引记录,又包含索引区间。临键锁的目的也是为了避免幻读,如果把事务隔离级别降为RC,临键锁会失效。
  • 可以理解为一种特殊的间隙锁,通过临键锁可以解决幻读的问题。每个数据行上的非唯一索引列上都会存在一把临键锁,当某个事务持有该数据的临键锁时,会锁住一段左开右闭区间的数据。InnoDB中行级锁是基于索引实现的,临键锁只与非唯一索引列有关,在唯一索引列(包括主键列)上不存在临键锁。

死锁

死锁的产生

  1. 死锁是指两个或多个事务在同一资源上互相占用,并请求锁定占用的资源,从而导致恶性循环。
  2. 当事务试图以不同的顺序锁定资源时,就可能产生死锁。多个事务同时锁定同一个资源时也可能回发生死锁。
  3. 锁的行为和顺序和存储引擎相关。以同样的顺序执行语句,有些存储引擎会产生死锁,有些不会。
  4. 死锁有两个原因:真正数据冲突;存储引擎的实现方式;

检测死锁

数据库系统实现了各种死锁检测和死锁超时的机制,InnoDB存储引擎能检测到死锁的循环依赖并立即返回一个错误。

死锁恢复

死锁发生后,只有部分或完全回滚其中一个事务,才能打破死锁。InnoDB目前处理死锁的方法是:将持有最少行级排它锁的事务进行回滚。

外部锁的死锁检测

发生死锁后,InnoDB一般都能自动检测到,并使一个事务释放锁并回退,另一个事务获得锁,继续完成事务。但在涉及外部锁或涉及表锁的情况下,InnoDB并不能完全自动检测到死锁,这需要通过设置锁等待超时参数innodb_lock_wait_timeout来解决。

死锁影响性能

死锁会影响性能而不是产生严重错误,因为InnoDB会自动检测并处理死锁。在高并发系统上,当许多线程等待同一个锁时,死锁检测可能导致速度变慢。有时当发生死锁时,禁用死锁检测(innodb_deadlock_detect配置选项)可能会更有效,这时可以依赖 innodb_lock_wait_timeout 设置进行事务回滚。

MyISAM避免死锁

在自动加锁的情况下,MyISAM总是一次获得SQL语句所需要的全部锁,所以MyISAM表不会出现死锁。

InnoDB避免死锁

  1. 使用 select ... for update 语句获取必要的锁;
为了在单个InnoDB表上执行多个并发写入操作时避免死锁,
可以在事务开始时,通过为预期要修改的每行使用 select ... for update 获取必要的锁。
  1. 直接申请足够级别的锁;
在事务中,如果要更新记录,应该直接申请足够级别的锁,即排它锁。
而不应该先申请共享锁、更新时再申请排它锁。因为当再申请排它锁时,
其他事务可能已经获得了相同记录的共享锁,从而造成锁冲突或死锁。
  1. 约定多个表的访问顺序
如果事务需要修改或锁定多个表,则应在每个事务中以相同的顺序使用加锁语句。
在应用中,如果不同的程序胡并发存取多个表,应尽量约定以相同的顺序来访问表,这样可以大大降低产生死锁的机会。
  1. 改变事务隔离级别