前面两篇博客中简单的聊了下mysql中的索引,今天聊聊mysql(InnoDB引擎)中的锁以及事务的实现
讲到锁大家应该都不陌生。像是Java中常见的采用CAS算法实现的乐观锁,典型的例子就是原子类,通过CAS自旋实现原子操作的更新,悲观锁通常都是
Synchronized
和
Lock
实现。
乐观锁与悲观锁
- 乐观锁:每次读数据的时候都认为其他人不会修改,所以不会上锁,而是在更新的时候去判断在此期间有没有其他人更新了数据,可以使用版本号机制。在数据库中可以通过为数据表增加一个版本号字段实现。读取数据时将版本号一同读出,数据每次更新时对版本号加一。当我们更新的时候,判断数据库表对应记录的当前版本号与第一次取出来的版本号值进行比对,如果值相等,则予以更新,否则认为是过期数据。乐观锁适用于多读的应用类型,可以提高吞吐量。
- 悲观锁:每次读数据的时候都认为别人会修改,所以每次在读数据的时候都会上锁,这样别人想读这个数据时就会被阻塞。MySQL中就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在操作之前先上锁。
共享锁与排他锁
- 共享锁:共享锁又叫做读锁或S锁,加上共享锁后在事务结束之前其他事务只能再加共享锁、只能对其进行读操作不能写操作,除此之外其他任何类型的锁都不能再加了。
# 加上lock in share mode
SELECT description FROM book_book lock in share mode;复制代码
- 排他锁:排他锁又叫写锁或X锁,某个事务对数据加上排他锁后,只能这个事务对其进行读写,在此事务结束之前,其他事务不能对其加任何锁,可以读取,不能进行写操作,需等待其释放。
# 加上for update
SELECT description FROM book_book for update;
复制代码
行锁与表锁
行锁与表锁区别在于锁的粒度,在Innodb引擎中既支持行锁也支持表锁(MyISAM引擎只支持表锁),只有通过索引条件检索数据InnoDB才使用行级锁,否则,InnoDB将使用表锁。
- 表锁:开销小,加锁快;不会出现死锁;锁定粒度大,发生锁冲突概率高,并发度最低
- 行锁:开销大,加锁慢;会出现死锁;锁定粒度小,发生锁冲突的概率低,并发度高
这里有个比较疑惑的地方,为什么表锁不会出现死锁?在MyISAM中由于没有事务,一条SQL执行完锁就释放了,不会循环等待,所以只会出现阻塞而不会发生死锁。但是在InnoDB中有事务就比较疑惑了,希望有了解的小伙伴指点指点@-@
下面举两个例子说明上面几种锁:
# 事务1
BEGIN;
SELECT description FROM book_book where name = 'JAVA编程思想' lock in share mode;
# 事务2
BEGIN;
UPDATE book_book SET name = 'new book' WHERE name = 'new';
# 查看事务状态
SELECT * FROM INFORMATION_SCHEMA.INNODB_TRX;
trx_id trx_state trx_started trx_tables_locked trx_rows_locked
39452 LOCK WAIT 2018-09-08 19:01:39 1 1
282907511143936 RUNNING 2018-09-08 18:58:47 1 38
复制代码
事务1给book表加上了共享锁,事务2尝试修改book表发生了阻塞,查看事务状态可以知道事务一由于没有走索引使用了表锁。
# 事务1
BEGIN;
SELECT description FROM book_book WHERE id = 2 lock in share mode;
# 事务2
BEGIN;
UPDATE book_book SET name = 'new book' WHERE id = 1;
# 查看事务状态
SELECT * FROM INFORMATION_SCHEMA.INNODB_TRX;
trx_id trx_state trx_started trx_tables_locked trx_rows_locked
39454 RUNNING 2018-09-08 19:10:44 1 1
282907511143936 RUNNING 2018-09-08 19:10:35 1 1
复制代码
事务1给book表加上了共享锁,事务2尝试修改book表并没有发生阻塞。这是由于事务一和事务二都走了索引,所以使用的是行锁,并不会发生阻塞。
意向锁(InnoDB特有)
意向锁的意义在于方便检测表锁和行锁之间的冲突
- 意向锁:意向锁是一种表级锁,代表要对某行记录进行操作。分为意向共享锁(IS)和意向排他锁(IX)。
- 行锁和表锁之间的冲突:事务A给表中的某一行加了共享锁,让这一行只能读不能写。之后事务B申请整个表的排他锁。如果事务B申请成功,那么它就能修改表中的任意一行,这与A持有的行锁是冲突的。InnoDB引入了意向锁来判断它们之间的冲突。
- 没有意向锁的情况:1、判断表是否已被其他事务用表锁锁表。2、判断表中的每一行是否已被行锁锁住,这样要遍历整个表,效率很低。
- 意向锁存在的情况:1、判断表是否已被其他事务用表锁锁表。2、判断表上是否有意向锁
- 意向锁存在时申请锁:申请意向锁的动作是数据库完成的,上述例子中事务A申请一行的行锁的时候,数据库会自动先开始申请表的意向锁,当事务B申请表的排他锁时检测到存在意向锁则会阻塞。
- 意向锁会不会存在冲突: 意向锁之间不会冲突, 因为意向锁只是代表要对某行记录进行操作。
各种锁之间的共存情况
IX IS X S
IX 兼容 兼容 冲突 冲突
IS 兼容 兼容 冲突 兼容
X 冲突 冲突 冲突 冲突
S 冲突 兼容 冲突 兼容
复制代码
死锁
- 概念:两个或两个以上的事务在执行过程中,因争夺资源而造成的一种互相等待的现象。
- 存在条件:1、 互斥条件:一个资源每次只能被一个事务使用。2、 请求与保持条件:一个事务因请求资源而阻塞时,对已获得的资源保持不放。3、不剥夺条件:已获得的资源,在末使用完之前不能强行剥夺。4、循环等待条件:形成一种头尾相接的循环等待关系
- 解除正在死锁的状态:撤销其中一个事务
MVCC(多版本并发控制)
MVCC使得InnoDB更好的实现事务隔离级别中的REPEATABLE READ
- 它使得InnoDB不再单纯的使用行锁来进行数据库的并发控制,取而代之的是把数据库的行锁与行的多个版本结合起来,只需要很小的开销,就可以实现非锁定读,从而大大提高数据库系统的并发性能。
- 实现:InnoDB实现MVCC的方法是它为每一行存储三个额外的隐藏字段
- 1.DB_TRX_ID:一个6byte的标识,每处理一个事务,其值自动+1 ,可以通过语句“show engine innodb status”来查找
- 2.DB_ROLL_PTR: 大小是7byte,指向写到rollback segment(回滚段)的一条undo log记录
- 3.DB_ROW_ID: 大小是6byte,该值随新行插入单调增加。
- SELECT:返回的行数据需要满足的条件: 1、数据行的创建版本号必须小于等于事务的版本2、行的删除版本号(行中的特殊位被设置为将其标记为已删除)一定是未定义的或者大于当前事务的版本号,确定了当前事务开始之前行没有被删除。
- INSERT:InnoDB为每个新增行记录当前系统版本号作为创建版本号。
- DELETE:InnoDB为每个删除行的记录当前系统版本号作为行的删除版本号。
- UPDATE:InnoDB复制了一条数据。这条数据的版本号使用了系统版本号。它也把系统版本号作为老数据的删除号。
- 说明:这里的读是不加锁的select等,MVCC实现可重复读使用的是读取undo中的已经提交的数据,是非阻塞的。insert操作时”创建时间”=DB_ROW_ID,这时”删除时间”是未定义的;update时,复制新增行的”创建时间”=DB_ROW_ID,删除时间未定义,旧数据行”创建时间”不变,删除时间=该事务的DB_ROW_ID; delete操作,相应数据行的”创建时间”不变,删除时间=该事务的DB_ROW_ID;
间隙锁(Next-Key锁)
间隙锁使得InnoDB解决幻读问题,加上MVCC使得InnoDB的RR隔离级别实现了串行化级别的效果,并且保留了比较好的并发性能。
定义:当我们用范围条件检索数据时请求共享或排他锁时,InnoDB会给符合条件的已有数据的索引加锁;对于键值在条件范围内但并不存在的记录,叫做间隙(GAP),InnoDB也会对这个”间隙”加锁,这种锁机制就是间隙锁。
例如:book表中存在bookId 1-80,90-99的记录。SELECT * FROM book WHERE bookId < 100 FOR UPDATE。InnoDB不仅会对bookId值为1-80,90-99的记录加锁,也会对bookId在81-89之间(这些记录并不存在)的间隙加锁。这样就能避免事务隔离级别可重复读下的幻读。
有问题的同学可以指出相互探讨,如需转载请注明出处。
参考文献:
https://dev.mysql.com/doc/refman/5.7/en/innodb-multi-versioning.html
https://www.cnblogs.com/chenpingzhao/p/5065316.html