事务隔离:从锁实现到MVCC实现

文章目录

事务隔离:从锁实现到MVCC实现

面试的时候被面试官问到:你这个项目为什么使用了可重复读而不选择读已提交事务隔离级别。思考了一会发现我对事务、锁、事务隔离级别的理解还是有所欠缺,今天来整理一下。

本文的梳理都基于下面这张简单的表。

建表语句:

sql 复制代码
CREATE TABLE users (
    id INT AUTO_INCREMENT PRIMARY KEY,  -- 用户ID,自增主键
    username VARCHAR(50) NOT NULL,      -- 用户名,最多50字符,不能为空
    age INT,                            -- 年龄
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP  -- 创建时间,默认当前时间
);

事务

事务就是数据库中的一系列操作组成的一个整体,我们在实际业务中,需要这一系列操作必须全部执行,不能有些操作执行了,有些操作执行失败了。

例如,小红向小明转账了500元,我们需要扣减小红500元的余额,增加小明500元的余额。

sql 复制代码
UPDATE users
SET balance = balance - 500
WHERE username = '小红';

UPDATE users
SET balance = balance + 500
WHERE username = '小明';

如果不加事务控制,这两条语句可能会出现第一条执行了,第二条未执行的操作,就会导致500元不翼而飞了。

使用BEGIN开启一个事务,使用COMMIT提交一个事务。

sql 复制代码
START TRANSACTION;

UPDATE users
SET balance = balance - 500
WHERE username = '小红';

UPDATE users
SET balance = balance + 500
WHERE username = '小明';

COMMIT;

对于单条sql语句,数据库会自动将其看成事务执行,叫隐式事务。

四大特性

事务的四大特性:

  • A:Atomicity,原子性,将所有SQL作为原子工作单元执行,要么全部执行,要么全部不执行;
  • C:Consistency,一致性,事务完成后,所有数据的状态都是一致的,即A账户只要减去了100,B账户则必定加上了100;
  • I:Isolation,隔离性,如果有多个事务并发执行,每个事务作出的修改必须与其他事务隔离;
  • D:Durability,持久性,即事务完成后,对数据库数据的修改被持久化存储。

我们所使用的锁、MVCC、日志等一系列机制其实都是为了保证事务的者四大特性,使得事务在实际业务中使用起来不会出错。

事务隔离级别

事务之间有四个隔离级别,分别是读未提交,读已提交(解决脏读),可重复读(解决不可重复读),串行化(解决幻读)。

锁实现

锁常用于解决并发请求导致的一系列问题。数据库锁也是用来处理当同时有多个事务或者请求来并发地访问数据库时,可能会出现的问题。

概念

数据库锁主要分为两种类型:

共享锁(Shared Lock): 也就是读锁,读锁和读锁不会互斥,读锁和写锁之间互斥。

排他锁(Exclusive Lock): 也就是写锁,写锁和任何读锁和写锁都是互斥的。

数据库中具体的锁:

由于本文只涉及到行锁,所以这里只简单介绍一下行锁。

行锁锁定数据库中的某一条记录,单个行。数据库不同行直接可以同时进行访问,因此提高了并发性。

共享行级锁:多个事务可以同时获取共享锁,用于读取行数据。

排他行级锁:只允许一个事务持有排他锁,用于修改行数据。

实现事务隔离

接下来我们梳理一下如何用锁来实现事务隔离级别。

首先我们要知道只依靠加锁来实现事务隔离会带来性能降低的问题,但是了解一下会对我们更好地去理解MVCC有帮助。

**一级封锁协议:**事务在修改一条记录之前必须先对它添加写锁,直到事务(提交或者回滚)结束才释放。

不能解决脏读问题,因为当事务T1修改记录,添加了写锁的时候,其它事务的读是不加锁的,依旧可以读到数据。

二级封锁协议: 一级封锁协议的规则+事务在读取记录之前必须先对它加读锁,读完后就可以释放锁。

可以解决脏读问题,但是不能解决不可重复读问题,因为读锁在读完就释放了,在到下一次读的这中间的间隙可能就会出现别的事务将数据修改了。

三级封锁协议: 一级封锁协议的规则+务在读取记录之前必须先对它加读锁,但是事务结束才释放。

可以解决脏读问题,可以解决不可重复读问题。

锁协议 事务隔离级别
一级封锁协议 Read Uncommitted(读未提交)
二级封锁协议 Read Committed(读已提交)
三级封锁协议 Repeatable Read(可重复读)
表锁 Serializable (可串行化)

MVCC实现

可以看到上面我们只使用了锁机制来实现了事务隔离,接下来介绍一种无锁化的、性能更高的实现事务隔离的方法,MVCC(Multi-Version Concurrency Control),即多版本并发控制。

当前读与快照读

在了解MVCC实现事务隔离之前,我们先了解一下当前读和快照读。

当前读: Mysql使用锁来实现当前读,共享锁+排他锁+next-key lock(间隙锁)实现。

下面这些语法都是当前读:

语法
SELECT ... LOCK IN SHARE MODE
SELECT ... FOR UPDATE
UPDATE
DELETE
INSERT

select语句加上LOCK IN SHARE MODE就是加上共享锁进行读,加上FOR UPDATE就是加上排他锁进行读。

快照读: 快照读是在读取数据的时候读取一致性视图中的数据,Mysql使用MVCC来实现快照读。

具体而言,每个事务在开始时会创建一个一致性视图(Consistent View),该视图反映了事务开始时刻数据库的快照。这个一致性视图会记录当前事务开始时已经提交的数据版本。

当执行查询操作时,MySQL会根据事务的一致性视图来决定可见的数据版本。只有那些在事务开始之前已经提交的数据版本才是可见的,未提交的数据或在事务开始后修改的数据则对当前事务不可见。

实现事务隔离

那么MVCC在MySql中又是怎么样实现事务隔离的呢?

对于读未提交 隔离级别,不做任何控制,相当于是一级封锁协议,修改语句会默认添加排他锁,并且在事务结束时才会释放。

对于读已提交隔离级别,MVCC通过Read View来实现。(具体看Read View)

对于可重复读隔离级别,MVCC通过Read View来实现。(具体看Read View)

对于串行化隔离级别,通过加临健锁(行锁+间隙锁)来实现的。

Read View

首先我们先要了解数据库的表记录,除了原来的数据列以外,还维护了3个隐藏列,和Read View相关的只有两个隐藏列,我们只关注这两个,一个是DB_TRX_ID,还有一个是DB_ROLL_PTR,其中DB_TRX_ID表明这条记录所属的事务id,DB_ROLL_PTR指向这条事务上一个事务所保存的这条记录的快照。所以对于一条记录,我们有一个多版本快照链(由DB_ROLL_PTR串联成链,读取选择读哪条的时候靠的是DB_TRX_ID来选择)。有了这个多版本快照链,事务在进行快照读的时候,就会结合Read View所记录的活跃的事务信息,选择当前隔离级别下可见的最新的记录了。

Read View就是mvcc实现快照读的核心机制,我们借助它就可以去undo_log中寻找要读的这条记录在当前事务隔离级别下"可见"的那个版本。下图就是一个Read View,它其实就是记录了事务的信息。

使用这个Read View的时候有一些规则:

(首先我们要知道每次读取记录的时候其实都是去当前这个记录的undo_log中去读取的,每次都先读取最新的版本,然后结合Read View进行判断,如果最新版本的不可读就会沿着版本链读取下一个版本直到可读。)

  1. 如果版本的记录的事务id是当前执行读取操作的事务id,则直接读取。

  2. 如果版本的记录的事务id小于Read View中的min_trx_id,那么表明该版本是在当前的读取事务开始之前就提交了的,可以读。

  3. 如果版本的记录的事务id大于Read View中保存的max_trx_id,那么表明该版本是在当前读取操作的事务开始之后提交的,不能读取。

  4. 如果当前读取操作的事务id位于Read View中记录的min_trx_id到max_trx_id之间,那么就表明这个版本是在当前活跃的但是还未提交的版本中的,那么只要在读已提交版本之上,就也不可以读。

读已提交可重复读 有一个很大的区别就是读已提交在每次读取操作的时候都会创建一个新的当前状态的Read View,而可重复读只会再事务第一次读取操作之后创建一个Read View,并且使用这个Read View一直到事务结束。就是这个区别使得 读已提交 可能导致 不可重复读 的问题。因为第二次读取使用的是一个更新后的新 Read View,可能读到了其他事务刚刚提交的新值。

总结

因此,有了MVCC,有了Read View,我们可以无锁地实现事务隔离级别,在读取操作地时候不上锁(没有MVCC的话,读取的时候要使用共享锁来进行控制),只有在修改操作的时候正常加上排他锁,大大地提高了并发事务的性能。

相关推荐
艾莉丝努力练剑15 分钟前
【数据结构与算法】数据结构初阶:详解顺序表和链表(五)——双向链表
c语言·开发语言·数据结构·学习·算法
算法_小学生33 分钟前
Hinge Loss(铰链损失函数)详解:SVM 中的关键损失函数
开发语言·人工智能·python·算法·机器学习·支持向量机
续亮~1 小时前
基于Spring AI Alibaba的智能知识助手系统:从零到一的RAG实战开发
java·人工智能·spring·springaialibaba
giao源1 小时前
Spring Boot 整合 Shiro 实现单用户与多用户认证授权指南
java·spring boot·后端·安全性测试
YUJIANYUE1 小时前
纯前端html实现图片坐标与尺寸(XY坐标及宽高)获取
开发语言·前端·javascript
kyle~1 小时前
C++---cout、cerr、clog
开发语言·c++·算法
汤姆大聪明2 小时前
Mysql定位慢查询
数据库·mysql
thginWalker2 小时前
拓扑排序/
java·开发语言
MediaTea2 小时前
Python 库手册:re 正则表达式模块
开发语言·python·正则表达式
Maybyy2 小时前
javaScript中数组常用的函数方法
开发语言·前端·javascript