锁是什么
锁是计算机协调多个进程或纯线程并发访问某一资源的机制。在数据库中,除传统的计算资源(CPU、RAM、I/O)的争用以外,数据也是一种供许多用户共享的资源。如何保证数据并发访问的一致性、有效性是所在有数据库必须解决的一个问题,锁冲突也是影响数据库并发访问性能的一个重要因素。从这个角度来说,锁对数据库而言显得尤其重要,也更加复杂。
防止更新丢失,并不能单靠数据库事务控制器来解决,需要应用程序对要更新的数据加必要的锁来解决。
锁的运作?
事务T在度某个数据对象(如表、记录等)操作之前,先向系统发出请求,对其加锁,加锁后事务T就对数据库对象有一定的控制,在事务T释放它的锁之前,其他事务不能更新此数据对象。
锁的分类
按锁类型分
排他锁(又称写锁,X锁)
- 会阻塞其他事务读和写。
- 若事务T对数据对象A(可能是表也可能是数据)加上X锁,则只允许T读取和修改A,其他任何事务都不能再对加任何类型的锁,直到T释放锁。这就保证了其他事务在T释放A上的锁之前不能再读取和修改A。
共享锁(又称读取,S锁)
- 会阻塞其他事务修改表数据。
- 若事务T对数据对象A加上S锁,则其他事务只能再对A加S锁,而不能X锁,直到T释放A上的锁。这就保证了其他事务可以读A,但在T释放A上的S锁之前不能对A做任何修改。
X锁和S锁都是加载某一个数据对象上的。也就是数据的粒度。
按锁粒度分
行级锁(row-level)
- 开销大,加锁慢,针对索引加锁而不是数据
- 会出现死锁。
- 锁定粒度最小,发生锁冲突的概率最低,并发度也最高。
行级锁定最大的特点就是锁定对象的颗粒度很小,也是目前各大数据库管理软件所实现的锁定颗粒度最小的。由于锁定颗粒度很小,所以发生锁定资源争用的概率也最小,能够给予应用程序尽可能大的并发处理能力而提高一些需要高并发应用系统的整体性能。
缺陷:由于锁定资源的颗粒度很小,所以每次获取锁和释放锁需要做的事情也更多,带来的消耗自然也就更大了。此外,行级锁定也最容易发生死锁。
表级锁(table-level)
- 开销小,加锁快。
- 不会出现死锁。
- 锁定粒度大,发生锁冲突的概率最高,并发度最低。
和行级锁相反,表级别的锁是MySQL各存储引擎中最大颗粒度的锁定机制。该锁定机制最大的特点是实现逻辑非常简单,带来的系统负面影响最小。所以获取锁和释放锁的速度很快。由于表级锁一次会将整个表锁定,所以可以很好的避免困扰我们的死锁问题。
缺陷:锁定颗粒度大所带来最大的负面影响就是出现锁定资源争用的概率也会最高,致使并发度大打折扣。
页级锁(page-level) (MySQL特有)
- 开销和加锁时间界于表锁和行锁之间。
- 会出现死锁。
- 锁定粒度界于表锁和行锁之间,并发度一般。
页级锁定是MySQL中比较独特的一种锁定级别,在其他数据库管理软件中也并不是太常见。页级锁定的特点是锁定颗粒度介于行级锁定与表级锁之间,所以获取锁定所需要的资源开销,以及所能提供的并发处理能力也同样是介于上面二者之间。
缺陷:页级锁定和行级锁定一样,会发生死锁。
事务特性
- ACID
事务隔离级别
- 默认都清楚
InnoDB锁机制
一共三类:共享锁,排他锁,意向锁。
意向锁为innodb引擎特有,单独介绍:
意向锁
举个例子,数据库操作存在以下场景:
事务A锁住了表中的一行,让这一行只能读,不能写。之后,事务B申请整个表的写锁。如果事务B申请成功,那么理论上它就能修改表中的任意一行,这与A持有的行锁是冲突的。
数据库需要避免这种冲突,就是说要让B的申请被阻塞,直到A释放了行锁。数据库要怎么判断这个冲突呢?
普通认为两步:
- 判断表是否已被其他事务用表锁锁表。
- 判断表中的每一行是否已被行锁锁住。但是这样的方法效率很低,因为要遍历整个表。
所以解决方案是:意向锁。
在意向锁存在的情况下,事务A必须先申请表的意向共享锁,成功后再申请一行的行锁。
在意向锁存在的情况下,两步骤为
- 判断表是否已被其他事务用表锁锁表。
- 发现表上有意向共享锁,说明表中有些行被共享行锁锁住了,因此,事务B申请表的写锁会被阻塞。
注意:申请意向锁的动作是数据库完成的,就是说,事务A申请一行的行锁的时候,数据库会自动先开始申请表的意向锁,不需要我们程序员使用代码来申请。
意向锁目的:解决表级锁和行级锁之间的冲突
意向锁是一种表级锁,锁的粒度是整张表。结合共享与排他锁使用,分为意向共享锁(IS)和意向排他锁(IX)。意向锁为了方便检测表级锁和行级锁之间的冲突,故在给一行记录加锁前,首先给该表加意向锁。也就是同时加意向锁和行级锁。
锁之间的兼容性
请求锁模式/是否兼容/当前锁模式 | 共享锁 | 排他锁 | 意向共享锁 | 意向排他锁 |
---|---|---|---|---|
共享锁 | 兼容 | 冲突 | 兼容 | 冲突 |
排他锁 | 冲突 | 冲突 | 冲突 | 冲突 |
意向共享锁 | 兼容 | 冲突 | 兼容 | 兼容 |
意向排他锁 | 冲突 | 冲突 | 兼容 | 兼容 |
注意:
如果一个事务请求的锁模式与当前的锁模式兼容,innodb就将请求的锁授予该事务;反之,如果两者不兼容,该事务就要等待锁释放。
意向锁是Innodb自动加的,不需要用户干预。
对于UPDATE、DELETE、INSERT语句,Innodb会自动给涉及的数据集加排他锁(X);对于普通SELECT语句,Innodb不会加任何锁。
若读操作想加锁需显示指定:
//显示共享锁(S)
SELECT * FROM table_name WHERE .... LOCK IN SHARE MODE
//显示排他锁(X)
SELECT * FROM table_name WHERE .... FOR UPDATE.
间隙锁
定义
当我们用范围条件而不是相等条件检索数据,并请求共享或排他锁时,InnoDB会给符合条件的已有数据的索引项加锁;对于键值在条件范围内但并不存在的记录,叫做“间隙(GAP)”,InnoDB也会对这个“间隙”加锁,这种锁机制就是所谓的间隙锁(Next-Key锁)。
举例来说,假如user表中只有101条记录,其userid 的值分别是1,2,…,100,101,下面的SQL:SELECT * FROM user WHERE userid > 100 FOR UPDATE
上面是一个范围条件的检索,InnoDB不仅会对符合条件的userid 值为101的记录加锁,也会对userid 大于101(这些记录并不存在)的“间隙”加锁。
目的
一方面是为了防止幻读,以满足相关隔离级别的要求,对于上面的例子,要是不使用间隙锁,如果其他事务插入了userid 大于100的任何记录,那么本事务如果再次执行上述语句,就会发生幻读;另一方面,是为了满足其恢复和复制的需要。有关其恢复和复制对机制的影响,以及不同隔离级别下InnoDB使用间隙锁的情况。
实际开发:
可见,在使用范围条件检索并锁定记录时,InnoDB这种加锁机制会阻塞符合条件范围内键值的并发插入,这往往会造成严重的锁等待。因此,在实际开发中,尤其是并发插入比较多的应用,我们要尽量优化业务逻辑,尽量使用相等条件来访问更新数据,避免使用范围条件。
何时使用表锁
对于InnoDB表,在绝大部分情况下都应该使用行级锁,因为事务和行锁往往是我们之所以选择InnoDB表的理由。但在个另特殊事务中,也可以考虑使用表级锁。
- 第一种情况是:事务需要更新大部分或全部数据,表又比较大,如果使用默认的行锁,不仅这个事务执行效率低,而且可能造成其他事务长时间锁等待和锁冲突,这种情况下可以考虑使用表锁来提高该事务的执行速度。
- 第二种情况是:事务涉及多个表,比较复杂,很可能引起死锁,造成大量事务回滚。这种情况也可以考虑一次性锁定事务涉及的表,从而避免死锁、减少数据库因事务回滚带来的开销。
在InnoDB下 ,使用表锁要注意以下两点。
(1)使用LOCK TALBES虽然可以给InnoDB加表级锁,但必须说明的是,表锁不是由InnoDB存储引擎层管理的,而是由其上一层MySQL Server负责的,仅当autocommit=0、innodb_table_lock=1(默认设置)时,InnoDB层才能知道MySQL加的表锁,MySQL Server才能感知InnoDB加的行锁,这种情况下,InnoDB才能自动识别涉及表级锁的死锁;否则,InnoDB将无法自动检测并处理这种死锁。
如果需要写表t1并从表t读,可以按如下做:
mysql> SET AUTOCOMMIT=
0
;mysql> LOCAK TABLES t1 WRITE, t2 READ, …;`
[
do
something with tables t1 and here];`
mysql> COMMIT;`
mysql> UNLOCK TABLES;`
事务引擎导致的死锁
发生死锁后,InnoDB一般都能自动检测到,并使一个事务释放锁并退回,另一个事务获得锁,继续完成事务。但在涉及外部锁,或涉及锁的情况下,InnoDB并不能完全自动检测到死锁,这需要通过设置锁等待超时参数innodb_lock_wait_timeout来解决。需要说明的是,这个参数并不是只用来解决死锁问题,在并发访问比较高的情况下,如果大量事务因无法立即获取所需的锁而挂起,会占用大量计算机资源,造成严重性能问题,甚至拖垮数据库。我们通过设置合适的锁等待超时阈值,可以避免这种情况发生。
通常来说,死锁都是应用设计的问题,通过调整业务流程、数据库对象设计、事务大小、以及访问数据库的SQL语句,绝大部分都可以避免。
下面就通过实例来介绍几种死锁的常用避免和解决方法。
- 在应用中,如果不同的程序会并发存取多个表,应尽量约定以相同的顺序为访问表,这样可以大大降低产生死锁的机会。如果两个session访问两个表的顺序不同,发生死锁的机会就非常高!但如果以相同的顺序来访问,死锁就可能避免。
- 在程序以批量方式处理数据的时候,如果事先对数据排序,保证每个线程按固定的顺序来处理记录,也可以大大降低死锁的可能。
- 在事务中,如果要更新记录,应该直接申请足够级别的锁,即排他锁,而不应该先申请共享锁,更新时再申请排他锁,甚至死锁。
- 在REPEATEABLE-READ隔离级别下,如果两个线程同时对相同条件记录用SELECT…ROR UPDATE加排他锁,在没有符合该记录情况下,两个线程都会加锁成功。程序发现记录尚不存在,就试图插入一条新记录,如果两个线程都这么做,就会出现死锁。这种情况下,将隔离级别改成READ COMMITTED,就可以避免问题。
- 当隔离级别为READ COMMITED时,如果两个线程都先执行SELECT…FOR UPDATE,判断是否存在符合条件的记录,如果没有,就插入记录。此时,只有一个线程能插入成功,另一个线程会出现锁等待,当第1个线程提交后,第2个线程会因主键重出错,但虽然这个线程出错了,却会获得一个排他锁!这时如果有第3个线程又来申请排他锁,也会出现死锁。对于这种情况,可以直接做插入操作,然后再捕获主键重异常,或者在遇到主键重错误时,总是执行ROLLBACK释放获得的排他锁。
尽管通过上面的设计和优化等措施,可以大减少死锁,但死锁很难完全避免。因此,在程序设计中总是捕获并处理死锁异常是一个很好的编程习惯。
如果出现死锁,可以用SHOW INNODB STATUS命令来确定最后一个死锁产生的原因和改进措施。