Mysql锁与事务原理

加锁原理

Mysql锁的分类

MySQL有三种锁的级别:页级、表级、行级。

  • 表级锁:开销小,加锁快;不会出现死锁;锁定粒度大,发生锁冲突的概率最高,并发度最低。
  • 行级锁:开销大,加锁慢;会出现死锁;锁定粒度最小,发生锁冲突的概率最低,并发度也最高。
  • 页面锁:开销和加锁时间界于表锁和行锁之间;会出现死锁;锁定粒度界于表锁和行锁之间,并发度一般

mysql的锁是加在索引上的,一般情况下,加在主键索引。 mysql锁的类型:

  1. 共享锁:可以用来读取数据,所以它也叫做读锁,注意不要在加上了读锁以后去写数据,不然的话可能会出现死锁的情况。而且多个事务可以共享一把读锁
    1. 可以用 select …… lock in share mode; 的方式手工加上一把读锁。
    2. 释放锁有两种方式,只要事务结束,锁就会自动释放,包括提交事务和结束事务。第二种是会话结束或者连接断开
  2. 排它锁:用来操作数据的,所以又叫做写锁。只要一个事务获取了一行数据的排它锁,其他的事务就不能再获取这一行数据的共享锁和排它锁。
    1. 加锁方式有两种,第一种是自动加排他锁。我们在操作数据的时候,包括增删改,都会默认加上一个排它锁。第一种是手工加锁,我们用一个 FOR UPDATE 给一行数据加上一个排它锁
    2. 释放锁有两种方式,只要事务结束,锁就会自动释放,包括提交事务和结束事务
  3. 意向锁:只是一个标记,标记表是否还有锁未释放,表级锁
    1. 意向共享锁,如果一张表上面至少有一个意向共享锁,说明有其他的事务给其中的某些数据行加 上了共享锁
    2. 意向排他锁,如果一张表上面至少有一个意向排他锁,说明有其他的事务给其中的某些数据行加 上了排他锁

意向锁的作用?

第一个,我们有了表级别的锁,在InnoDB 里面就可以支持更多粒度的锁。 它的第二个作用,我们想一下,如果说没有意向锁的话,当我们准备给一张表加上表锁的时候,我们首先要做什么?是不是必须先要去判断有没其他的事务锁定了其中了某些行?如果有的话,肯定不能加上表锁。那么这个时候我们就要去扫描整张表才能确定能不能成功加上一个表锁,如果数据量特别大,比如有上千万的数据的时候,加表锁的效率很低。

行锁的算法

第一种情况,当我们对于唯一性的索引(包括唯一索引和主键索引)使用等值查询,精准匹配到一条记录的时候,这个时候使用的就是记录锁。

第二种情况,当我们查询的记录不存在,没有命中任何一个 record,无论是用等值查询还是范围查询的时候,它使用的都是间隙锁。 >Gap Lock 只在 RR 中存在。如果要关闭间隙锁,就是把事务隔离级别设置成 RC,并且把 innodb_locks_unsafe_for_binlog 设置为 ON。

第三种情况,当我们使用了范围查询,不仅仅命中了 Record 记录,还包含了 Gap间隙,在这种情况下我们使用的就是临键锁,它是 MySQL 里面默认的行锁算法,相当于记录锁加上间隙锁。

select * from t2 where id >5 and id <=7 for update; -- 锁住(4,7]和(7,10]
select * from t2 where id >8 and id <=10 for update; -- 锁住 (7,10], (10,+∞)

其他两种退化的情况: 唯一性索引,等值查询匹配到一条记录的时候,退化成记录锁。 没有匹配到任何记录的时候,退化成间隙锁

mysql死锁原因

所谓死锁:是指两个或两个以上的进程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去.此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。表级锁不会产生死锁.所以解决死锁主要还是针对于最常用的InnoDB。

死锁的产生条件:

  • (1)同一时刻只能有一个事务持有这把锁,
  • (2)其他的事务需要在这个事务释放锁之后才能获取锁,而不可以强行剥夺,
  • (3)当多个事务形成等待环路的时候,即发生死锁。

死锁的关键在于:两个(或以上)的Session加锁的顺序不一致。

那么对应的解决死锁问题的关键就是:让不同的session加锁有次序

mysql死锁的解决方法

线上解决:如果一个事务长时间持有锁不释放,可以 kill 事务对应的线程 ID,也就是 INNODB_TRX 表中的 trx_mysql_thread_id,例如执行 kill 4,kill 7,kill 8。

如何避免死锁?

  1. 在程序中,操作多张表时,尽量以相同的顺序来访问(避免形成等待环路);
  2. 批量操作单张表数据的时候,先对数据进行排序(避免形成等待环路);
  3. 申请足够级别的锁,如果要操作数据,就申请排它锁;
  4. 尽量使用索引访问数据,避免没有 where 条件的操作,避免锁表;
  5. 如果可以,大事务化成小事务;
  6. 使用等值查询而不是范围查询查询数据,命中记录,避免间隙锁对并发的影响。

查看mysql锁的日志

show status like 'innodb_row_lock_%';

Innodb_row_lock_current_waits: 当前正在等待锁定的数量;
Innodb_row_lock_time : 从系统启动到现在锁定的总时间长度, 单位 ms;
Innodb_row_lock_time_avg : 每次等待所花平均时间;
Innodb_row_lock_time_max: 从系统启动到现在等待最长的一次所花的时间;
Innodb_row_lock_waits : 从系统启动到现在总共等待的次数。

SHOW 命令是一个概要信息。InnoDB提供了三张表来分析事务与锁的情况:

select * from information_schema.INNODB_TRX; -- 当前运行的所有事务 , 还有具体的语句
select * from information_schema.INNODB_LOCKS; -- 当前出现的锁
select * from information_schema.INNODB_LOCK_WAITS; -- 锁等待的对应关系

mvcc多版本控制

MVCC 的核心思想是: 我可以查到在我这个事务开始之前已经存在的数据,即使它在后面被修改或者删除了。在我这个事务之后新增的数据,我是查不到的。

InnoDB 为每行记录都实现了两个隐藏字段: DB_TRX_ID,6 字节:插入或更新行的最后一个事务的事务 ID,事务编号是自动递增的(我们把它理解为创建版本号,在数据新增或者修改为新数据的时候,记录当前事务 ID)。 DB_ROLL_PTR,7 字节:回滚指针(我们把它理解为删除版本号,数据被删除或记录为旧数据的时候,记录当前事务 ID)。