1. 从数据操作的类型划分:读锁,写锁
对于数据库并发事务的读-读情况并不会引起什么问题。对于写-写,读-写操作或写-写操作这些情况可能会引起一些问题,需要使用MVCC或者加锁的方式来解决它们。在使用加锁的方式解决问题时,由于既要允许读-读的情况不受影响,又要使得写-写,读-写/写-读情况的操作相互阻塞,所以MySQL实现一个由两种类型的锁组成的锁系统来解决。这两种类型的锁通常被叫做共享锁和排他锁。
- 读锁:也称为共享锁,英文用S表示。针对同一份数据,多个事务的读操作可以同时进行而不会相互影响。相互不阻塞。
- 写锁:也称为排他锁,英文用X表示。当前写操作没有完成时,它会阻断其他写锁和读锁。这样就能确保在给定的时间里,只有一个事务能执行写入,并防止其他用户读取正在写入的同一资源。
需要注意的是对于InnoDb存储引擎来说,读锁和写锁可以加在表上,也可以加在行上。
举例(行级读写锁):如果一个事务T1已经获得了某个行r的读锁,那么此时另外一个事务T2是可以获得这个行的读锁的,因为读取操作并没有改变行r的数据。但是如果某个事物T3向获得行r的写锁,则它必须等待事务T1和T2释放掉行r上的读锁才行。
总结:这里的兼容是指对同一张表或记录的锁的兼容性情况:
2. 锁定读
在采用加锁方式解决脏读,不可重复读,幻读这些问题时,读取一条记录时需要获取记录的S锁,其实是不严谨的。有时候需要在读取记录的时就获取记录的X锁,来禁止别的事务读写该记录。为此MySQL提出了两种比较特殊的select语句格式。
- 对读取的记录加S锁:
select ... lock in share mode;
# mysql8.0新增语法
select ... for share;
在普通的select语句后加上lock in share mode,如果当前事务执行了该语句,那么它会为读取的记录加S锁,这样允许别的事物继续获取这些记录的S锁,但不能获取该记录的X锁(比如使用select ... for update语句来取这些记录,或者直接修改这些记录)。如果别的事务想要获取这些记录的X锁,那么它们会阻塞,直到当前事务提交后将这些记录的S锁释放掉。
- 对读取的记录加X锁:
select ... for update;
在普通的select语句后面加for update,如果当前事务执行了该语句,那么它会为读取到的记录加S锁,这样不允许别的事务获取这些记录的S锁,也不允许获得这些记录的X锁。如果别的事务想要获取这些记录的S锁或者X锁,那么它们会阻塞,直到当前的事务提交后将这些记录上的X锁释放。
MySQL8.0新特性:
在5.7版本以前,select ... for update,如果获取不到锁,会一直等待,直到innodb_lock_wait_timeout超时。在8.0版本中,select ... for update,select ... for share添加nowait,skip locked语法,跳过锁等待,或者跳过锁定。
通过添加上述语法,能够立即返回。如果查询的行已经加锁:
- 那么nowait会立即报错返回。
- 而skip locked也会立即返回,只是返回的结果中不包含被锁定的行。
3. 写操作
平常用到的写操作无非是DELETE,UPDATE,INSERT这三种:
DELECT:
对一条记录做DELETE操作的过程其实是先在B+树中定位到这条记录的位置,然后获取这条记录的X锁,再执行delete mark的操作。我们也可以把这个定位待删除记录在B+树位置的过程看成是一个获取X锁的锁定读。
INSERT:
一般情况下:新插入的一条记录并不加锁,通过一种隐式锁的结果来保护这条新插入的记录在本事务提交前不被别的事务访问。
UPDATE:在对一条记录做UPDATE操作时分为三种情况:
- 情况1:未修改该记录的主键值,并且被更新的列占用的存储空间在修改前后未发生变化。则先在B+树 中定位到这条记录的位置,然后获取一下记录的X锁,最后在原纪录的位置进行修改操作。我们也可以把这个定位待修改记录在B+树位置的过程看作是一个获取X锁的锁定读。
- 情况2:未修改该记录的主键值,并且至少由一个被更新的列占用的存储空间在修改前后发生变化。则先在B+树中定位到这条记录的位置,然后获取一下记录的X锁,将该记录彻底删除(就是把这个记录移入垃圾链表),最后插入一条新的记录。这个定位待修改记录在B+树中的位置的过程看作是一个获取X锁的锁定读,新插入的记录由INSERT操作提供的隐式锁及逆行保护。
- 情况3:修改了该记录的主键值,则相当于在原纪录上做DELETE操作之后再进行INSERT操作,加锁操作就需要按照DELETE和INSERT的规则进行。