# Mysql学习(七)——锁
前面介绍过一些并发事务的处理方式,解决并发竞争资源的问题,就需要使用到锁了。
## 锁分类
在mysql中有很多种锁
**从操作的粒度上看,可以分为:表级锁、行级锁、页锁**
- 表级锁:每次操作都会锁住整张表,性能最低,并发度最低,容易发生锁冲突
- 行级锁:每次操作只会锁住对应的范围行,锁定粒度最低,并发度最高,锁冲突概率最低
- 页锁:通常不会使用,每次锁住的是相邻个一组记录,粒度,并发度都在表锁与行锁之间,应用在BDB存储引擎中
**从操作的类型上来看,可以分为:读锁,写锁**
- 读锁(S锁):读锁是共享锁,多个读锁可以同步进行不会互相影响
- 写锁(X锁):写锁是排他锁,当前写操作没完成时,会阻断其他事务对相同数据获取读锁或写锁
IS锁,IX锁又称为意向读锁和意向写锁,是一个表级锁,但是意向读锁和意向写锁之间是互相兼容的,可以共享加锁,并且也不会和行锁发生冲突,那么使用意向读锁和意向写锁的意义何在?主要是因为当一个事务给数据库某几行加了读锁或者写锁时,另外一个事务要想获取表锁,那么需要确保整个表记录中都没有其他行上加了读锁或者写锁,这样mysql就需要遍历整张表的所有行记录,判断是否有行锁,性能低。于是就有了意向锁,当一个事务需要加上表级的排他锁时,直接判断表上是否有对应意向锁就可以知道是否能加锁成功,提升效率。
**从操作的性能上来看,可以分为:乐观锁、悲观锁**
- 乐观锁:修改提交的时候,使用记录数据版本号进行比对,版本发生冲突则证明无法提交,提示错误
- 悲观锁:对一条数据进行修改时,为了避免其他事务对这一行数据修改,在修改数据前先加锁,在进行修改。共享锁和排它锁都是悲观锁的不同实现。
## 行锁的原理
InnoDB中的行锁,可以提升很大的并发性能,InnoDB的行锁是对**索引页数据页上的记录进行加锁**实现的,主要有三种实现:Record Lock、Gap Lock、Next-key Lock
- Record Lock:行锁,锁定对应的行数据,在RC级别和RR级别下支持
- Gap Lock:间隙锁,锁住索引的间隙范围,不包含数据本身,只有RR级别下才支持,防止查询范围下出现幻读
- Next key Lock:行锁和间隙锁的结合,既锁住对应数据行,也锁住对应行索引的间隙范围
在RR级别下,InnoDB默认是使用Next key Lock方式进行加锁,但是当sql操作包含唯一索引时,nextkey Lock会进行降级,降级为Record Lock,仅锁住对应的索引行
**下面举几个例子**:
1. `select * from table;` 采用MVCC的机制实现非阻塞读,不进行加锁。
2. `select * from table lock in share mode;`加共享锁,InnoDB会使用NextkeyLock进行处理,扫描遇见唯一索引,可以进行降级为Record Lock
3. `select * from for update;`InnoDB使用Next-key Lock进行处理, 扫描遇见唯一索引,可以降级为RecordLock
4. `update..where;`InnoDB使用Next-key Lock进行处理,扫描遇见唯一索引,可以进行降级为Record Lock
5. `delete...where;`InnoDB使用Next-key Lock进行处理,扫描遇见唯一索引,可以进行降级为Record Lock
6. `insert语句;`Innodb会给插入的那一行设置一个排他RecordLock锁
**加锁实例**:
- 主键加锁:

加锁行为:仅在id=10的主键索引记录上加X锁。
- 唯一索引加锁:

加锁行为:现在唯一索引id上加X锁,然后在id=10的主键索引记录上加X锁。
- 非唯一索引加锁:

加锁行为:对满足id=10条件的记录和主键分别加X锁,然后在(6,c)-(10,b)、(10,b)-(10,d)、(10,d)-(11,f)范围分别加Gap Lock。
- 无索引加锁:

如果对无索引数据进行修改,会导致全表数据范围都被锁,需要慎重
## 悲观锁
悲观锁就是指在数据处理过程中,先将数据加锁,再进行数据处理。表锁、行锁、排他锁、共享锁都属于悲观锁的不同实现。
**表锁**:
表级锁会锁住整张表,并发度很低。
手工增加表锁:
```sql
lock table tableName read|write;
```
读锁可以共享,追加写锁需要等待锁都释放,并且会阻塞后面的读写锁
**共享锁**:
行上的读锁就是共享锁,简称S锁,在行上追加读锁,多个事务都能获得共享读锁,但是获取到共享锁的只能读,不能写,使用`select ..from table lock in share mode`
**排他锁**:
行锁上的写锁就是排他锁,简称X锁,在行上追加了写锁后,其他事务无法获得该行上的锁,使用排他锁的方法和当前读的方法一样,`select ... from table for update`,innodb会在update、delete语句上也追加上for update,行级锁的实现是依赖于索引,如果操作没用到索引,那么会锁住整张表的记录。
## 乐观锁
数据库不提供乐观锁的实现,乐观锁是需要开发者自行去实现的,默认在数据库操作时不会产生冲突,直到提交时判断版本是否有冲突,检测到有版本冲突后,抛出错误。
乐观锁实现原理:
- 增加一个版本字段,并每次使用判断字段进行判断是否符合预期版本。例如在数据库表中增加version字段,每次去执行sql时,都判断下版本是否匹配,例如下面这个sql:
```sql
update table set column=column+1 where id = 1 and version = #{version}
```

- 增加一个时间戳ts字段
类似于版本号管理方式,每次都使用ts时间戳去判断是否符合预期的时间戳,如果不符合,抛出异常
## 死锁与解决方案
### **表级死锁**:
事务A访问A表,并对A表加上排他锁,然后访问B表。此时事务B访问B表,对B表加上排他锁,并试图访问事务A,这样导致循环依赖,每个事务都在等待其他事务排他锁的释放,但是却进入死循环,导致死锁。
**解决方案**:
表级死锁一般都是由于代码bug导致,需要梳理整体数据的交互流程,对数据库的表操作时,都要按照相同的顺序进行处理,总是按照先A后B这样的顺序处理。
### 行级死锁
**行级死锁原因1:**
类似于表级死锁,两个事务都试图访问对方的锁,导致死循环造成死锁
**解决方案:**
- 在同一个事务中,尽量一次性锁定需要的资源
- 资源执行需要按照顺序进行获取,总是先A后B
**行级死锁原因2**:
在事务中执行sql没有使用索引条件,导致全表扫描,行级锁升级为全表记录锁,多个这样的事务执行后,很容易导致死锁和数据库阻塞。
**解决方案**:
SQL语句中不要使用过多的关联表查询,并在发现全表扫描sql时,使用explain命令对sql进行分析,判断出现的原因,并建立对应索引进行优化
### 共享锁转为排他锁
事务A在给行A加上共享锁时,事务B试图向行A加上排他锁,此时由于事务A已经有共享锁了,所以事务B需要等待事务A释放贡献锁才能加上排他锁,而事务A又试图修改行A,而此时事务A会发生死锁,因为他试图给行A加上排他锁失败,事务B此时已经在等待行A释放共享锁了,导致最终死锁。
```sql
--事务A:
select * from table where id = 'A';--第1步
--事务B:
update table set column='2' where id ='A'; --第2步,第1步中有共享锁,无法加上排他锁
--事务A:
update table set column='1' where id = 'A';--第3步,无法获取排他锁,导致死锁
```
**解决方案**:
- 使用乐观锁进行控制,乐观锁可以避免长事务对数据库的加锁消耗,但是乐观锁需要开发实现,并且乐观锁只能控制住自身系统,无法控制外部系统或人为操作数据,导致脏数据进入数据库中。
### 死锁的检测
Mysql提供了一些死锁的排查工具
**查看死锁日志**:`show engine innodb status;`,可以查看金器的死锁日志信息,将其中的sql取出进行优化
**查看锁的状态值**:`show status like '%innodb_row_lock%'`检查环境变量,分析系统中的行锁争夺情况。
**锁状态值解释**:
- Innodb_row_lock_current_waits:当前正在等待锁的数量
- Innodb_row_lock_time:从系统启动到现在锁定总时间长度
- Innodb_row_lock_time_avg: 每次等待锁的平均时间
- Innodb_row_lock_time_max:从系统启动到现在等待最长的一次锁的时间
- Innodb_row_lock_waits:系统启动后到现在总共等待的次数
如果等待次数过多,就需要进行系统分析,为什么需要进行如此多的等待。
Mysql学习(七)——锁