不一样存储引擎支持的锁是不一样的,好比MyISAM只有表锁,而InnoDB既支持表锁又支持行锁。html
下图展现了InnoDB不一样锁类型之间的关系:mysql
图中的概念比较多很差理解,下面依次进行说明。算法
乐观锁是相对悲观锁而言的,乐观锁假设数据通常状况下不会形成冲突,所在在数据进行提交更新时,才会对数据的冲突与否进行检测,若是发现冲突了,则返回给用户错误信息,让用户决定如何处理,其核心是基于CAS算法。乐观锁适用于读多写少的场景,能够提升程序吞吐量。sql
Mysql自带的是没有乐观锁的,可是能够经过表上加个version字段来实现本身乐观锁。数据库
假如要更新一个用户的年龄,能够这样作:编程
id | Name | Age | Version |
---|---|---|---|
3 | 张三 | 26 | 1 |
更新张三的年龄为27,注意where条件带上版本号。update user set age = 27,version = 2 where id = 3 and version = 1;并发
若是更新的结果是1则表示更新成功了,若是是0则表示更新失败须要从新尝试。高并发
悲伤锁就是在每次操做数据时,都悲观地认为会出现数据冲突,因此必须先获取到数据的锁再对其修改。传统的关系型数据库用的就是悲观锁,还有JDK中的synchronized关键字等。悲观锁主要分为共享锁和排他锁。性能
共享锁【shared locks】,又叫读锁,顾名思义,共享锁就是多个事务对同一个数据能够共享一把锁,都能访问到数据,可是只能读不能修改。rest
如何获取共享锁?
select * from user where id = 3 lock in share mode;
注意:在有事务获取到了共享锁以后,其余事务是不能作insert/update/delete操做的,由于insert/update/delete语句会自动加上排他锁。
排他锁【exclusive locks】,又叫写锁,顾名思义,排他锁就是不能与其余锁并存,若是一个事务获取了一个数据行的排他锁,其余事务就不能再获取该行的其余锁,包括共享锁和排他锁,可是获取排他锁的事务是能够对数据进行读取和修改。
如何获取排他锁?
在sql语句后加上for update便可。
select * from user where id = 3 for update
表锁,顾名思义就是对整张表加锁,是Mysql各存储引擎中最大粒度的锁定机制。
优势:实现逻辑简单,获取锁和释放锁的速度很快,因为每次都是将整张表锁定因此能够很好的避免死锁问题。
缺点:锁定颗粒度大致使出现锁定资源争用的几率高,并发度低。
行锁,顾名思义就是对表中的某行数据加锁,锁定颗粒度最小。
优势:发生锁冲突的几率低,并发处理能力强。
缺点:因为锁定资源的颗粒度很小,因此每次获取锁和释放锁须要作的事情也更多,带来的消耗天然也就更大了。此外,行级锁定也最容易发生死锁。
如何判断使用的是行锁仍是表锁?
InnoDB的行锁是针对索引加的锁,不是针对记录加的锁,因此只有在经过索引条件检索数据时才会用行锁,不然使用表锁。而且该索引不能失效,不然都会从行锁升级为表锁。因此在使用select for update时,where 子句必定要带上索引,不然极容易形成性能问题。
行锁又细分三种实现算法:
record lock:专门对索引项加锁;
gap lock:间隙锁,是对索引之间的间隙加锁;
Next-key lock:是前面两种的组合,对索引及其之间的间隙加锁;
页面锁出现比较少,它的特色是开销和加锁时间界于表锁和行锁之间,会出现死锁,锁定粒度界于表锁和行锁之间,并发度通常。
死锁(Deadlock) 所谓死锁:是指两个或两个以上的进程在执行过程当中,因争夺资源而形成的一种互相等待的现象,若无外力做用,它们都将没法推动下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。因为资源占用是互斥的,当某个进程提出申请资源后,使得有关进程在无外力协助下,永远分配不到必需的资源而没法继续运行,这就产生了一种特殊现象死锁。
死锁的四个必要条件:
互斥条件:一个资源每次只能被一个进程使用。
占有且等待:一个进程因请求资源而阻塞时,对已得到的资源保持不放。
不可强行占有:进程已得到的资源,在末使用完以前,不能强行剥夺。
循环等待条件:若干进程之间造成一种头尾相接的循环等待资源关系。
首先建立一张订单记录表,用于作订单的幂等性校验防止重复生成订单。
CREATE TABLE `order_record` ( `id` int(11) NOT NULL AUTO_INCREMENT, `order_no` int(11) DEFAULT NULL, `status` int(4) DEFAULT NULL, `create_date` datetime(0) DEFAULT NULL, PRIMARY KEY (`id`) USING BTREE, INDEX `idx_order_status`(`order_no`,`status`) USING BTREE ) ENGINE = InnoDB
事务A | 事务B |
---|---|
关闭自动提交事务,set autocommit = 0; | set autocommit = 0; |
select id from order_record where order_no = 4 for update;//检查是否存在订单号为4的订单 | |
select id from order_record where order_no = 5 for update;//检查是否存在订单号为5的订单 | |
//若是没有则插入信息 insert into order_record(order_no,status,create_date) values(4,1,'2020-10-04 10:56:00'); 此时锁等待中... |
|
//若是没有则插入信息 insert into order_record(order_no,status,create_date) values(5,1,'2020-10-04 10:56:00'); |
|
返回结果代表发生死锁,ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction | |
COMMIT;(未完成) | COMMIT;(未完成) |
分析:
因为order_no列为非惟一索引,并且此时是RR事务隔离级别,因此SELECT 的加锁类型是gap lock,并且gap范围是(4,+∞)。
当咱们执行插入 SQL 时,会在插入间隙上再次获取插入意向锁。插入意向锁其实也是一种 gap 锁,它与 gap lock 是冲突的,事务 A 和事务 B 都持有间隙 (4,+∞)的 gap 锁,而接下来的插入操做为了获取到插入意向锁,都在等待对方事务的 gap 锁释放,因而就形成了循环等待,致使死锁。
InnoDB 存储引擎的主键索引为聚簇索引,其它索引为辅助索引。若是两个更新事务使用了不一样的辅助索引,或者一个使用辅助索引,一个使用了聚簇索引,就都有可能致使锁资源的循环等待,形成死锁。
步骤:
首先,order_record表存在如下数据。
而后打开两个窗口
事务A | 事务B |
---|---|
BEGIN; | BEGIN; |
update order_record set status = 1 where order_no = 4; | |
mysql> update order_record set status = 1 where id = 4; ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction//发生了死锁 |
分析:
事务A | 事务B |
---|---|
首先获取idx_order_status辅助索引 | |
获取主键索引的行锁 | |
根据辅助索引获取主键索引,再获取主键索引的行锁 | |
更新status列时,须要idx_order_status辅助索引 |
因此再更新数据时,要尽可能根据主键来更新,能够有效避免死锁发生。
一般有如下手段能够预防死锁的发生:
若是真的发生了数据库死锁,也有如下方式处理:
查看当前的事务
SELECT * FROM INFORMATION_SCHEMA.INNODB_TRX;
查看当前锁定的事务
SELECT * FROM INFORMATION_SCHEMA.INNODB_LOCKS;
查看当前等锁的事务
SELECT * FROM INFORMATION_SCHEMA.INNODB_LOCK_WAITS;
杀死进程 kill pid
并且MySQL默认开启了死锁检测机制,当检测到死锁后会选择一个最小(锁定资源最少的)的事务进行回滚。
日常不多写MySQL相关的文章,其实MySQL中的门道仍是挺多的,本文关于间隙锁等概念讲的比较简单,推荐博客《mysql间隙锁》。 之后可能会再写一篇关于索引的,也有可能不会(主要是懒😄),若是本文哪里有错误,请多指教。