锁
一、什么是锁
- 用于管理对共享资源的并发访问
- 操作缓冲池中的LRU列表,删除、添加、移动LRU列表中的元素,为保证一致性,必须有锁的介入
- InnoDB存储引擎锁的实现和Oracle数据库非常类似,提供一致性的非锁定读、行级锁支持
二、lock和latch
latch
要求锁定时间必须非常短,如果持续时间过长,则应用的性能非常差。latch
又可以分为mutex
(互斥量)和rwlock
(读写锁)。其目的是用来保证并发线程操作临界资源的正确性,并且通常没有死锁检测机制
lock
对象是事务,该锁一般lock的对象仅在事务commit或rollback后进行释放(不同事务隔离级别释放时间可能不同)
lock和latch的比较
- | lock | latch |
---|---|---|
对象 | 事务 | 线程 |
保护 | 数据库内容 | 内存数据内容 |
持续时间 | 整个事务过程 | 临界资源 |
模式 | 行锁、表锁、意向锁 | 读写锁 、互斥量 |
死锁 | 通过waits-for graph 、time out 等机制进行死锁监测与处理 | 无死锁监测与处理机制。仅通过应用程序加锁的顺序保证无死锁的情况发生 |
存在于 | Lock Manager 的哈希表中 | 每个数据结构的对象中 |
三、InnoDB存储引擎中的锁
3.1 锁的类型
- 共享锁:(
S Lock
),允许事务读一行数据 - 排他锁:(
X Lock
),允许事务删除或更新一行数据 - 意向共享锁:(
IS Lock
),事务想要获得一张表中某几行的共享锁 - 意向排他锁:(
IX Lock
),事务想要获得一张表中某几行的排他锁
意向锁的目的是为了在一个事务中揭示下一行将被请求的锁的类型。
表:InnoDB存储引擎中锁的兼容性
- | IS | IX | S | X |
---|---|---|---|---|
IS | 兼容 | 兼容 | 兼容 | 不兼容 |
IX | 兼容 | 兼容 | 不兼容 | 不兼容 |
S | 兼容 | 不兼容 | 兼容 | 不兼容 |
X | 不兼容 | 不兼容 | 不兼容 | 不兼容 |
可以通过指令SELECT * FROM information_schema.INNODB_TRX\G;
进行查看
INNODB_TRX的结构说明
字段名 | 说明 |
---|---|
trx_id | InnoDB存储引擎内部唯一的事务ID |
trx_state | 当前事务的状态 |
trx_strted | 事务的开启时间 |
trx_requested_lock_id | 等待事务的锁ID。如trx_state的状态为LOCK WAIT,那么表示当前的事务等待之前事务占用锁资源的ID。若trx_state不是LOCK WAIT,则该值为NULL |
trx_wait_started | 事务等待开始的时间 |
trx_weight | 事务的权重,反映了一个事物修改和锁住的行数,在InnoDB存储引擎中,当发生死锁需要回滚时,InnoDB存储引擎会选择该值最小的进行回滚 |
trx_mysql_thread_id | MySQL中的线程ID,SHOW PROCESSLIST显示的结果 |
trx_query | 事务运行的SQL语句 |
但是该表只能显示当前运行的InnoDB事务,并不能直接判断锁的一些情况。如果要查看锁的一些情况需要指令SELECT * FROM information_schema.INNODB_LOCKS\G;
表INNODB_LOCKS的结构
字段名 | 说明 |
---|---|
lock_id | 锁ID |
lock_trx_id | 事务ID |
lock_mode | 锁的模式 |
lock_type | 锁的类型,表锁还是行锁 |
lock_table | 要加锁的表 |
lock_index | 锁住的索引 |
lock_space | 锁对象的space id |
lock_page | 事务锁定页的数量,若是表锁,则该值为NULL |
lock_rec | 事务锁定行的数量,若是表锁,则该值为NULL |
lock_data | 事务锁定记录的主键值,若是表锁,则该值为NULL |
通过上述表查看表上锁的情况后,用户就可以来判断由此引发的等待情况了。当事务较小时,用户可以直截了当进行判断。当事务量非常大时,需要通过表INNODB_LOCKS_WAITS来判断
通过指令SELECT * FROM information_schema.INNODB_LOCK_WAITS\G;
进行查看
表INNODB_LOCK_WAITS的结构
字段 | 说明 |
---|---|
requesting_trx_id | 申请锁资源的事务ID |
requesting_lock_id | 申请的锁的ID |
blocking_trx_id | 阻塞的事务ID |
blocking_lock_id | 阻塞的锁的ID |
3.2 一致性非锁定读
InnoDB存储引擎通过多版本控制(mutil versioning)的方式来读取当前执行时间数据库中的行的数据。
简单概括来说就是:事务A修改id= 1的这条数据(delete或者update),此时事务B去读取这条id=1的记录,此时事务B读取的是事务A修改之前的记录。
实现方式:通过undo段中记录的快照(snapshot)来实现。事务开启之前,将需要操作的行的记录通过快照的形式保存在undo log中,然后再对当前行记录进行修改操作,此时如果另一个事务进来进行读取的操作时,只会去读取undo log中保存的快照,也就是之前的记录。因此不会存在并发安全问题。且因为没有加锁,所以并发操作效率更高。
- 再事务隔离级别READ COMMITTED和REPEATABLE READ(InnoDB存储引擎的默认事务隔离级别)下,InnoDB使用非锁定的一致性读
- 具体实现方式可以看这篇文章
实例执行的过程:
时间 | 会话A | 会话 B |
---|---|---|
1 | BEGIN | |
2 | SELECT * FROM parent WHERE id = 1; 结果为id=1 | |
3 | BEGIN | |
4 | UPDATE parent SET id = 3 WHERE id = 1; | |
5 | SELECT * FROM parent WHERE id = 1; 结果为id=1 | |
6 | COMMIT | |
7 | SELECT * FROM parent WHERE id = 1; ,结果为Empty set(0.00 sec) | |
8 | COMMIT |
3.3 一致性锁定读
虽然InnoDB存储引擎的SELECT操作使用一致性非锁定读,但是某些情况下,用户需要显式地对数据库读取操作进行加锁保证数据逻辑的一致性。而这个要求数据库支持加锁语句,即使是对于SELECT的只读操作。InnoDB存储引擎对于SELECT语句支持两种一致性的锁定读操作:
SELECT ... FOR UPDATE
SELECT ... LOCK IN SHARE MODE
3.4 自增长与锁
在Innodb存储引擎的内存结构中,每个含有自增长值的表都有一个自增长计数器。插入操作会依据这个自增长的计数器值加1赋予自增长列。这个实现方式称作AUTO-INC Locking。这种锁采用一种特殊的表锁机制,为了提高性能,锁不是在一个事务完成后才释放,而是在完成对自增值插入SQL语句之后立即释放
对于有自增长值的列的并发插入性能较差,事务必须等待前一个插入完成(虽然不用等待事务的完成),其次对于INSERT --SELECT 的大数据的插入会影响插入性能,因为另一个事务中的插入会阻塞
四、锁
4.1 行锁的3种算法
InnoDB存储引擎有3种行锁的算法,其分别是:
- Record Lock:单个记录上的锁
- Gap Lock:间隙锁,锁定一个范围,但不包括记录本身
- Next-Key Lock:Gap Lock+Record Lock,锁定一个范围,并且锁定记录本身
4.2 锁的问题
- 幻读:同一个事务下,连续两次相同的查询语句得到的结果不同
- 脏读:读取到未提交的数据
- 不可重复读:跟幻读很像,结果都是两次查询到的结果不同。区别在于不可重复读的操作是数据修改,幻读的操作是由于新增和删除。
脏读是读取到了未提交的数据,不可重复读读取的是已提交的数据,但是他还是违反了数据库事务一致性的要求。
4.3 死锁
-
概念:死锁是两个或两个以上的事务在执行过程中,因抢夺锁资源而造成的一种相互等待的现象。
-
解决办法就是:
- 只要有等待出现就回滚。
- 但方法一容易造成并发性能的下降,因此在这里面加入了“超时”,等待超过某个时间阈值才回滚。
- 简单超时造成根据FIFO的机制来选择回滚对象,如果某些对象的事务占比很高,占用了很多的undo log,此时让他回滚着实牺牲有点大。因此,当前数据库普遍采用了“等待图”
-
等待图:
图中有两种信息:锁的信息链表、事务等待链表。如果这两个信息构成的图出现了回路,那就存在死锁。
4.4 锁升级
锁升级指的是将当前锁的粒度降低:行锁变页锁,页锁变表锁
锁升级是为了保护系统资源,防止系统用过多的内存来维护锁。
参考
《MySQL技术内幕——InnoDB存储引擎》