背景
最近在读一本关于数据库的神书:《高性能MySQL 第3版》,是许多大牛推荐的一本书,虽然书中的mysql版本是5.6版,但是不乏有很多可以借鉴的地方。其中第一章,介绍MySql的存储引擎的时候,推荐读者去阅读官网教程中的一章(InnoDB事务模型和锁)很好奇为什么要专门推荐,阅读完成之后随即写下这篇文章。
mysql InnoDB的两把行锁
主要是由以下的两种锁类型组成:
- (x)排他锁
- (s)共享锁
其实最主要注意的是 s 锁是可以多个锁定锁在同一行上的,但是 x 锁就只是串行化
x 锁主要是 delete、update 语句,s 锁主要是LOCK IN SHARE MODE和LOCK TABLES ... WRITE
方便的AUTOCOMMIT
在InnoDB 中,每个会话提交都是默认进行 Autocommit 的,所以在 delete 和 update 的时候,我们不用担心没有开启相关的事务。
注意:如果自动提交模式被用SET AUTOCOMMIT = 0关闭,那么我们可以认为一个用户总是有一个事务打开着。一个SQL COMMIT或ROLLBACK语句结束当前事务并且一个新事务开始。两个语句都释放所有在当前事务中被设置的InnoDB锁定。一个COMMIT语句意味着在当前事务中做的改变被生成为永久的,并且变成其它用户可见的。一个ROLLBACK语句,在另一方面,撤销所有当前事务做的修改
如果连接有被允许的自动提交,通过用明确的START TRANSACTION或BEGIN语句来开始一个事务,并用COMMIT或者ROLLBACK语句来结束它,这样用户仍旧可以执行一个多重语句事务。
--一个常规的事务提交语句
BEGIN;
-- SQL 语句 1
INSERT INTO employees (name, position, salary) VALUES ('John Doe', 'Manager', 75000);
-- 检查插入是否成功
IF NOT FOUND THEN
ROLLBACK;
RETURN;
END IF;
-- SQL 语句 2
UPDATE departments SET budget = budget - 75000 WHERE name = 'Sales';
-- 检查更新是否成功
IF NOT FOUND THEN
ROLLBACK;
RETURN;
END IF;
-- SQL 语句 3
DELETE FROM pending_hires WHERE candidate_name = 'John Doe';
-- 检查删除是否成功
IF NOT FOUND THEN
ROLLBACK;
RETURN;
END IF;
-- 如果所有语句都成功,则提交事务
COMMIT;
小拓展:springboot中的@Transactional 注解
主要是注意两块,一个是事务的传播,一个是事务的回滚点,默认是将当前事务加入到当前线程中,回滚点默认是运行时异常。
@Transactional 注解默认的传播行为是 REQUIRED,即如果当前线程上已经存在一个事务,则将该方法加入到当前事务中执行,否则将创建一个新的事务。如果需要修改传播行为,则可以使用 propagation 属性来指定 注意还可以设置隔离级别,默认为 RR,在高并发读 写的情况下才考虑换成 RC 参考链接:https://juejin.cn/post/6968384376824561671
事务的隔离级别
在 InnoDB 中,主要有四个事务隔离级别,不同的事务隔离级别也对数据库的查询性能有着不同的影响,默认使用 RR ,其他三个为 RU,RC,S,在 RR 的事务隔离级别下,存储引擎使用 mvcc 来进行多版本并发控制,使得在有多个事务在同一时间去服务数据的时候,不会出现脏读和幻读的问题,但是对数据库的读取性能有一定的影响,所以在高并发读的情况下,可以采取将隔离级别降为 RC(读已提交),来保证高并发读写。
解释RR 中怎么处理幻读
拿一个例子解释,在 RC 中,你可能会读到已经被删除、更新、插入的数据,而在 RR 中,由于使用了 mvcc 的机制,每一个事务在未提交之前是一个链表,在当前事务中,你只能看到你读取的时间点的数据,就算是其他事务在你未提交之前提交了一个删除、更新、插入的数据,你也一样保持在你读取的时间点所读取的数据。简单来说永远和你第一次查询保持一致,这样就可以很好的处理原子性。这时候,如果你不想你读取的数据被其他事务所修改,请提前上 S 锁(select for update)。
在InnoDB中不同SQL语句设置的锁定
- SELECT ... FROM是一个持续读,读取数据库的快照并且设置不锁定,除非事务隔离级别被设为SERIALIZABLE。对于 SERIALIZABLE级别,这个设置对它遇到的索引记录设置共享的next-key锁定。
- SELECT ... FROM ... LOCK IN SHARE MODE对读遇到的所有索引记录设置共享的next-key锁定。
- SELECT ... FROM ... FOR UPDATE对读遇到的所有索引记录设置独占的next-key锁定。
- INSERT INTO ... VALUES (...)对被插入的行设置独占锁定。注意,这不是一个next-key锁定,并且不阻止其它用户在已插入行之前的间隙插入。如果发生重复键错误,对重复的索引记录设置共享锁定。注意:在一个表上初始化之前指定的AUTO_INCREMENT列之时,InnoDB在与AUTO_INCREMENT列相关联的索引的末尾设置独占锁定。在访问自动增长计数器中,InnoDB使用专用的表锁定模式AUTO-INC,其中锁定仅持续到当前SQL语句的结束,而不是到整个事务的结束。
- INSERT INTO T SELECT ... FROM S WHERE ... 对每个插入到T的行设置独占(非next-key)锁定。它在S上把搜索当作一个持续读,但是如果MySQL binlog功能被打开,它就对S设置一个共享的next-key锁定。InnoDB在后一种情况不得不设置锁定:在从一个备份的前滚恢复中,每个SQL语句不得不以与它最初被执行的方式完全同样的方式执行。
- CREATE TABLE ... SELECT ... 把SELECT当作一个持续读来执行,或者带着共享锁定来执行,但是如果MySQL binlog功能被打开,它就对S设置一个共享的next-key锁定。InnoDB在后一种情况不得不设置锁定:在从一个备份的前滚恢复中,每个SQL语句不得不以与它最初被执行的方式完全同样的方式执行。
- UPDATE ... WHERE ... 对搜索遇到的每个记录设置一个独占的next-key锁定。
- DELETE FROM ... WHERE ... 对搜索遇到的每个记录设置一个独占的next-key锁定。
- LOCK TABLES设置表锁定,但是是InnoDB层之上更高的MySQL层设置这些锁定。
总结:select 除非是加了 LOCK IN SHARE MODE 或 for update,都是持续读,属于没有锁的情况,除了LOCK IN SHARE MODE是共享锁外,其他的锁都是独占锁,将会排他并且锁定行。所以推荐使用乐观锁而不是悲观锁。
怎么减少死锁的发生
主要是三个尽量:尽量执行小的事务,尽量少使用 select 相关的锁,尽量使用带索引的行做为查询条件