MySQL - 事务 & 锁

事务

第一个前提:事务是存储引擎层面支持的,InnoDB支持事务,MyISAM不支持事务。本文都是指的InnoDB。
第二个前提 :MySQL默认设置autocommit = on,即任何语句若没有显示地开启事务,都被当做一个独立的事务进行执行 —— Even a select statement opens a transaction

老生常谈,说起事务,就得先提一下事务的ACID特性

事务的ACID特性

  • 原子性 Atomicity
    事务里的操作要么全部成功,要么全部失败
  • 一致性 Consistency
    事务和数据约束的一致性,事务完成前后,都不能与数据本身的完整性约束相悖,例如唯一索引约束,表结构约束等等。
  • 隔离性 Isolation
    数据库应该具有并发事务同时对数据进行读写修改的能力。 --> 事务的四个隔离级别
  • 持久性 Durability
    事务处理后,数据的修改应该是永久的,系统挂了,也不会丢失。

事务的隔离级别

  • 读未提交 Read uncommitted
    一个事务能读到另一个事务还没有提交的数据,会产生脏读
  • 读提交 Read committed
    一个事务只能读到别的事务提交过后的数据,会不可重复读
  • 可重复读 Repeatable read
    一个事务在事务进行的期间,同样的查询语句不同时刻查得到的结果应该是一样的,可能产生幻读(但是MySQL使用了next-key lock来解决)
  • 串行化 Serializable
    不允许并发,一个数据只有被持有它的事务提交/回滚释放锁之后,才能被其他事务访问。

以上隔离级别由上到下,并发性能越来越差,隔离程度越来越深。InnoDB默认的隔离级别是可重复读,而大部分互联网公司设置的线上隔离级别是读提交。通过show variables like 'tx_isolation'可以查看数据库事务级别。

有个我觉得很有意思能帮助理解的例子,看这里,对于“脏读”,“不可重复读”,“幻读”的解释,例子里也说的很清楚~

MVCC

解决以上“脏读”,“不可重复读”,“幻读”有两种思路,一个就是加锁,另外一个就是MVCC(Multi-version Concurrency Control)即数据多版本并发控制。MVCC的目的:让读写互相不阻塞,提高事务并发处理的能力。MVCC的核心思想:提供数据的历史版本,即我记录的不是一个静态的2D的表,而是一个“3D”的表,每个数据还有一个维度是他的版本号(transaction id/trx_id,该id按照先后严格递增)

一致性视图

InnoDB在实现MVCC的时候,用到了一致性视图(consistent read view)。这个视图和我们查询的时候的视图不是一个同一个东西。这个一致性视图其实是根据数据的最新版本和undo log(回滚日志)计算出来的。

以可重复读举例,事务一开始的时候,就创建了一个一致性视图,该视图的数据,如上所述是根据数据当时的最新版本和undo log计算出来的。而这个视图计算出的数据满足以下条件:

  • 数据版本未提交,不可见 --> 还要根据undo log往前翻版本
  • 数据版本已提交,但是是在视图创建后提交的,不可见
  • 数据版本已提交,视图创建前提交的,可见

总的来说,就是根据当前最新数据,和undo log,找到一个提交过的最新版本,作为一致性视图。

  • 一个栗子🌰:
    假设事务A开始前,(id, k) -> (1,1)这个数据已经提交了。问A和B分别select到的值是什么?
    一个MVCC栗子

答案是:A select 到1, B select到3 ~

  • 变化一下的另一个栗子🌰:
    另一个栗子

这个例子的结果和上面一致,但是事务B会等到事务C提交后,才能执行。因为第一,update语句是先读后写,而这个读是当前读。第二,事务的两阶段提交(2PC 在日志模块讲到)代表着k=2这个数据版本的写锁没有释放所以事务B就wait在那儿了,要等C提交了之后,事务B才能继续进行它的当前读。

当前读:读的是数据的最新版本,并对读的数据加锁;例如update, delete, insert,select... for update, select ... lock in share mode; 当前读也是通过next-key lock 来实现的
快照读:也就是我们说的这个一致性视图其实就是快照读

MVCC是不是和Copy-on-Write(COW)的思想有一点异曲同工呢?

根据锁的粒度的不同,有以下几种分类:

  • 全局锁
    锁整个数据库实例的,命令是Flush tables with read lock,那么所有数据更新语句,事务提交,表结构修改等等都会被阻塞。主要用于全库的逻辑备份。(因为不是所有引擎都支持一致性读,即备份的时候可以给这个事务创建一个一致性视图,不阻塞更新语句)
  • 表锁和元数据锁
    表锁语法:lock tables xxx read/write;元数据锁(MDL)不需要显示使用,在访问一张表时自动会加上MDL 读锁; 当做DDL的时候,会加上MDL写锁。读锁不互斥,读写锁,写锁互斥。
  • 行锁
    行锁是由存储引擎自己实现的,如InnoDB支持行锁,MyISAM不支持行锁。注意📌:行锁,其实是加在索引上的

两阶段锁协议 2PL

InnoDB的行锁,是需要的时候加上,事务提交了才释放;所以我们在一个事务中,尽量要把可能引起锁冲突最多的锁往后放。

Next-lock key

即便是可重复读的隔离级别,如果只有行锁,一样会出现幻读的情况,即一个事务前后两次查询同一个范围,结果不同。如select name from user_info where age >= 14 and age <= 20 for update,如果期间有其他事务插入了一条满足age范围[14,20]的数据,第二次读就会多读到一个数据。所以InnoDB搞了一个间隙锁(gap lock),而间隙锁 + 行锁就形成了next-lock key一个左开右闭的区间。所以以上语句,在可重复度的隔离级别下,会在索引age上,把间隙给锁住。

注意:当查询的索引含有唯一属性时,next-lock key会优化降级为行锁

死锁

并发系统不同线程出现循环资源依赖,都在等待对方线程释放资源,进入无限等待。

  • 死锁出现后如何解决?
  1. 设置锁超时,innodb_lock_wait_timeout默认是50s
  2. 发起死锁检测,发现死锁后,回滚死锁链中某一个事务。可以设置innodb_deadlock_detect为on,但是死锁检测会消耗CPU资源,对于热点数据,需要控制并发度。
posted @   rachel_aoao  阅读(62)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· AI 智能体引爆开源社区「GitHub 热点速览」
· 从HTTP原因短语缺失研究HTTP/2和HTTP/3的设计差异
· 三行代码完成国际化适配,妙~啊~
点击右上角即可分享
微信分享提示