mysql锁

  • 对于UPDATE、DELETE、INSERT语句,InnoDB会自动给涉及数据集加排他锁(X)
  • InnoDB行锁和表锁都支持!
    表锁:开销小,加锁快;不会出现死锁;锁定力度大,发生锁冲突概率高,并发度最低
    行锁:开销大,加锁慢;会出现死锁;锁定粒度小,发生锁冲突的概率低,并发度高
  • InnoDB只有通过索引条件检索数据才使用行级锁,否则,InnoDB将使用表锁
  • InnoDB行锁是通过索引上的索引项来实现的,这一点MySQL与Oracle不同,后者是通过在数据中对相应数据行加锁来实现的。
  • InnoDB实现了以下两种类型的行锁:共享锁(S锁、读锁),排他锁(X锁,写锁)
    共享锁:一个事务去读一行数据,给这行数据加上共享锁,此时其他事务也可以读取这行数据,同样加上共享锁(读读不冲突)。但是不可以去修改(update,delete)这条数据,因为修改加的是排他锁,共享锁之上不可以加排他锁(读写冲突)。
    排他锁:一个事务去修改一行数据,给这行数据加上排他锁,此时其他事务不可以读取这行数据,也不可以修改这行数据(写读冲突,写写冲突)。
  • 数据库事务有不同的隔离级别,不同的隔离级别对锁的使用是不同的。MySQL默认使用的可重复读的隔离级别 , 可重复读会导致幻读 , 通过mvvc和gap锁解决了幻读(
    参考: https://my.oschina.net/u/566591/blog/3162858?_from=gitee_rec )。其中mvvc是读写不阻塞的
  • MySQL对于UPDATE、DELETE、INSERT语句, 都会加锁, 导致其他的SQL语句阻塞( 也就是写写阻塞 ), 在高并发的情况下, 虽然保证了数据安全 , 但是数据库会承受大量的IO操作, 大量的操作就会阻塞, 可能会导致宕机等, 所以需要缓冲这些服务, 可以使用MQ来异步操作
1. select * from my_table where id = 1;
2. select * from my_table where id = 1 lock in share mode;
3. select * from my_table where id = 1 for update;
4. update my_table set address = 'tianjin' where id = 1;
先说隔离级别,mysql隔离级别分为四种:
未提交读(read uncommitted)、提交读(read committed)、重复读(repeatable read)、序列化(serializable)
其中mysql默认的隔离级别为重复读(repeatable read),以下简称为rr,本文也只介绍这种模式
读的模式分为两种:
快照读(snapshot read)
当前读(current read)
我们先来了解一下MVCC:
MVCC是为了实现数据库的并发控制而设计的一种协议。与其相对的事LBCC即基于锁的并发控制(Lock-Based Concurrent Control)。要实现数据库的并发访问控制,最简单的做法就是加锁访问,即读的时候不能写(这个读为当前读,后面介绍。允许多个线程同时对想读的内容加锁,即共享锁或叫S锁),写的时候不能读(只能有一个线程对同一内容进行写操作,即排它锁,X锁)。这样的加锁访问,其实并不算是真正的并发,或者说它只能实现并发的读,既读写串行化,这样就大大降低了数据库的读写性能。
LBCC是四种隔离级别中级别最高的Serialize隔离级别。MVCC对比LBCC它的最大好处便是,读不加锁,读写不冲突。在MVCC中,读操作可以分成两类,快照读(Snapshot read)和当前读(current read)。快照读,读取的是记录的可见版本(可能是历史版本,即最新的数据可能正在被当前执行的事务并发修改),不会对返回的记录加锁,如上面的sql语句1;而当前读,读取的是记录的最新版本,并且会对返回的记录加锁,保证其他事务不会并发修改这条记录。如上面的sql语句2,3,4。不同的是2加的是s锁,3、4加的是x锁,insert加的也是x锁。
注:MVCC只在RC和RR两个隔离级别下工作,其他两个隔离级别都和MVCC不兼容

快照读:读取的是事务开始时的记录版本,不会对记录加锁。
select * from table where ?
当前读:读取的是当前最新版本,并且会对记录加锁
select * from table where ? lock in share mode; (加S锁)
select * from table where ? for update; (加X锁)
间隙锁:Gap锁,顾名思义,锁住查询记录之间的间隙,只有在RR隔离级下才可以生效,在RC隔离级下无法生效,为了解决mysql幻读问题。(非快照读时解决幻读方案)
比如有这几条记录:0,5,10,15,20,25
对于select * from t where id=9,这样一个不存在行,锁住的区间是(5,10); 前开后开
对于select * from t where id=5,这样存在的行锁住的是(0,5]这样的区间,其实也就是一个行锁加一个间隙锁,就构成了一把net-key lock;
net-key lock:行锁加上间隙锁

事务隔离级别对应锁:

  • read uncommited:读未提交,事务b未提交的结果就被事务a读取到了,事务b如果回滚了数据,事务a读取到的数据就是脏数据(产生脏读)
    加锁实现:无任何加锁实现
  • read commited:读已提交,事务a只能读取到其他事务已经提交的结果,避免了脏读,但是事务a在两次查询之间事务b修改了此条数据,导致事务a两次读取的数据不一致的情况,称之为(不可重复读)。
    加锁实现:mysql之外其他数据库,读不加锁,写加排他锁,因为写加了排他锁,所以必须等到写提交完成并且释放锁之后,其他读写才能执行,所以不会产生脏读,但是因为读不加锁,在同一个事务两次读之间其他事务依旧可以修改数据,所以会产生不可重复读的结果。
    mysql使用MVCC机制快照读,读写不冲突,依然会产生不可重复读和幻读,非快照读也依旧会有这些问题。
    实操:开2个事务测试,发现RC级别不可以解决不可重复读和幻读,但是RR级别都可以解决。
    答案:同样是使用MVCC机制,为什么处理结果不一样呢。是因为InnoDB在设计上增加了ReadView的设计,ReadView中主要包含当前系统中还有哪些活跃的读写事务,生成 ReadView 的时机,RR 级别只在事务开始时生成一次,之后一直使用该 ReadView。而 RC 级别则在每次 select 时,都会生成一个 ReadView(rr的事务级快照、rc的语句级快照)。
  • repeated read:可重复读,可以避免不可重复读,但是会产生(幻读),a事务查询一段范围之间的数据,b事务插入了一条这个范围内的数据,a事务再次查询发现多了一条数据,就像幻觉一样。
    加锁实现:mysql之外其他数据库,读加共享锁,写加排他锁,因为a事务在读的时候就加了共享锁,所以其他事务在a未释放锁之前不能加排他锁去修改数据,所以a在整个事务之内不管查询多少次数据都是一致的,解决不可重复读。但是因为加的是行锁,b事务依旧可以在a查询范围之内去insert一条本来不存在的a未加锁的数据,a再次范围查询就会发现多了一条,产生幻读。
    但是,mysql如果使用MVCC机制快照读,就不会产生幻读,不使用MVCC机制当前读也不会产生幻读,因为mysql在这个隔离级别下采用了next-key lock来解决幻读。
  • serializable:序列化,读写全部加锁,而且是范围锁,解决所有问题,并发性能差,摆设而已?
    备注:只有mysql有MVCC机制,所以只有mysql可以快照读。
    备注:MVCC机制只作用于rc和rr级别。
    备注:mysql在rr和rc级别下,使用快照读都能解决不可重复读和幻读。但是在非快照读下,rc啥都不能解决,rr本来就可以解决不可重复读,再加上间隙锁和next-key lock解决幻读,所以都能解决,所以实现结果和serializable一样。
    备注:只有mysql的rr级别有间隙锁和next-key lock,所以只有mysql在rr级别可以防止幻读。
MySQL/InnoDB定义的4种隔离级别:

Read Uncommited
可以读取未提交记录。此隔离级别,不会使用,忽略。

Read Committed (RC)
快照读忽略,本文不考虑。

针对当前读,RC隔离级别保证对读取到的记录加锁 (记录锁),存在幻读现象。

Repeatable Read (RR)
快照读忽略,本文不考虑。

针对当前读,RR隔离级别保证对读取到的记录加锁 (记录锁),同时保证对读取的范围加锁,新的满足查询条件的记录不能够插入 (间隙锁),不存在幻读现象。

Serializable
从MVCC并发控制退化为基于锁的并发控制。不区别快照读与当前读,所有的读操作均为当前读,读加读锁 (S锁),写加写锁 (X锁)。

Serializable隔离级别下,读写冲突,因此并发度急剧下降,在MySQL/InnoDB下不建议使用。

ReadView:

row_id :隐藏的行 ID ,用来生成默认的聚集索引。如果创建数据表时没指定聚集索引,这时 InnoDB 就会用这个隐藏 ID 来创建聚集索引。采用聚集索引的方式可以提升数据的查找效率。
trx_id: 操作这个数据事务 ID ,也就是最后一个对数据插入或者更新的事务 ID 。
roll_ptr:回滚指针,指向这个记录的 Undo Log 信息。

Undo Log
InnoDB 将行记录快照保存在 Undo Log 里。

image.png

数据行通过快照记录都通过链表的结构的串联了起来,每个快照都保存了 trx_id 事务ID,如果要找到历史快照,就可以通过遍历回滚指针的方式进行查找。
如果一个事务要查询行记录,需要读取哪个版本的行记录呢? Read View 就是来解决这个问题的。Read View 可以帮助我们解决可见性问题。 Read View 保存了当前事务开启时所有活跃的事务列表。换个角度,可以理解为: Read View 保存了不应该让这个事务看到的其他事务 ID 列表。

trx_ids 系统当前正在活跃的事务ID集合。
low_limit_id ,活跃事务的最大的事务 ID。
up_limit_id 活跃的事务中最小的事务 ID。
creator_trx_id,创建这个 ReadView 的事务ID。

image.png

如果当前事务的 creator_trx_id 想要读取某个行记录,这个行记录ID 的trx_id ,这样会有以下的情况:

如果 trx_id < 活跃的最小事务ID(up_limit_id),行记录是在所有活跃的事务创建前就已经提交了,那么这个行记录对当前事务是可见的。
如果trx_id > 活跃的最大事务ID(low_limit_id),行记录是在所有活跃的事务之后才创建的,说明这个行记录对当前事务是不可见的。
如果 up_limit_id <= trx_id <=low_limit_id,说明该记录需要在 trx_ids 集合中,可能还处于活跃状态,因此我们需要在 trx_ids 集合中遍历 ,如果trx_id 存在于 trx_ids 集合中,证明这个事务 trx_id 还处于活跃状态,不可见,否则 ,trx_id 不存在于 trx_ids 集合中,说明事务trx_id 已经提交了,这行记录是可见的。

  • 实际上活跃的最大的事务ID肯定是查询生成的,因为生成这个活跃事务ID数组就是在查询的时候生成的,查询是不修改2列隐藏值的,所以trx_id永远不可能等于活跃的最大事务ID(low_limit_id)

你可能感兴趣的:(mysql锁)