Mysql之事务

1. ACID模型

ACID是数据库的一个设计原则,其目的就是尽可能的保证数据的可靠性。Innodb存储引擎就是严格遵照了ACID模型。

那么Innodb是怎么做到ACID的呢?

Atomicity(原子性): 所谓原子性,就是我们的多个操作是一起执行的,多个或者单个操作语句都是一个事务,来保证用户想要一起提交哪些语句、或者回滚哪些语句.

主要是commit 、rollback、 autocommit来保证原子性

Consistency(一致性): 所谓一致性,就是保证数据的一致,也就是保证数据不要丢失,不会因为突然的断电等导致数据与想要的数据不一致

主要体现在1.双写机制,保证内存与磁盘之间的数据安全2.基于Redolog的数据恢复

Isolation(隔离性) : 只要体现在事务的隔离级别,InnoDB的锁机制

Durabiity(持久性): 持久性,其实就是我的数据要尽可能的同步到我们的磁盘,防止异常丢 失.

innodb去保证持久性主要体现在1.双写,保证内存同步到磁盘,就算page损坏的情况下也能修复2.RedoLog的同步机制设置3.binlog的同步机制4.独立表空间或者系统表空间设置

2. Atomicity(原子性)

2.1 事务

上面的ACID模型多次提到事务这个概念,那么什么是事务呢?

事务是工作的原子单元,可以提交或回滚。当事务对数据库进行多个更改时,要么在事务提交时所有更改都成功,要么在事务回滚时所有更改都被撤消。
工作:就是我可以是单条 sql 、也可以是一批 sql

事务的提交与回滚

sql 复制代码
-- 默认是READ WRITE 如果参数为READ ONLY,则事务中不允许对表进行更改
START TRANSACTION READ WRITE;
-- 业务操作 多个业务操作
UPDATE zsc_teacher SET teacher_age=teacher_age+1 WHERE id=2;
INSERT INTO
zsc_teacher(id,teacher_name,teacher_age,teacher_addr)
VALUES(18,'huihui',30,'湖南');-- 当事务用READ ONLY修饰时,改操作无效
COMMIT; -- 提交该事务
ROLLBACK; -- 回滚事务

分析: Start transation或者 beginstart a new transaction为开启一个事务,然后就可执行一到多条的sql,commit是提交该事务,上面的一到多条sql同时生效,对数据库进行变更操作,rollback为回滚事务,上面的一到多条sql同时失效,数据回滚到原来的样子。

自动提交,我们知道,平常我们自己写sql,并没有写begin commit rollback这些,这是因为我们的Mysql里面,默认都是以自动提交模式运行的,简单来讲,就是每个语句,当没有start transaction开启事务的时候,每个语句都是默认被start transaction和commit包围的,并且不能用rollback来回滚。

sql 复制代码
show SESSION VARIABLES like 'autocommit'; -- 查询是否开启自动提交(会话)
show GLOBAL VARIABLES like 'autocommit'; -- 查询自动开启提交(会话)
set SESSION autocommit=0; -- 关闭自动提交

哪些语句是不能回滚的呢?哪些是隐式提交的?

有些语句不能回滚。通常,这些语句包括数据定义语言 (DDL) 语句,例如那些创建或删除数据库的语句,那些创建、删除或更改表或存储例程的语句。
比如 ddl 语句、权限操作等,都是隐式提交,不需要自己提交的。

当然,事务是保证单条或多条sql要么同时执行成功,要么全部回滚,那么如果我们只想回滚一部分呢? innodb是支持回滚点操作的,何为回滚点,就是我可以回滚部分操作,提交部分操作

sql 复制代码
START TRANSACTION;
UPDATE zsc_teacher SET teacher_age=teacher_age+1 WHERE id=2;
-- 设置回滚点,如果回滚回滚点,后续内容会被回滚
SAVEPOINT zsc;
UPDATE zsc_teacher SET teacher_age=teacher_age+1 WHERE id=1;
ROLLBACK TO zsc; -- 回滚到回滚点 id=1的不生效 但是不代表事务结束,事务结束还需要commit或者ROLLBACK
COMMIT; -- 提交事务

分析: 在第一条sql下面设置了回滚点,并且在执行第二条后 返回回滚点,此时再提交事务,就只有第一条sql生效。

2.2 查看事务与undolog回滚日志

怎么查看事务?

sql 复制代码
SELECT * FROM information_schema.INNODB_TRX;

trx_id: 递增的事务ID,但是如果事务是只读事务或者非锁定事务(查询没加锁)不分配,展示的是一个比较大的随机数值。

每个sql操作默认都会有一个事务,所以所有的数据操作都是基于事务去操作的,每行数据都会有个隐藏字段trx_id。代表修改这个数据最后的事务ID。

思考: 事务可以把sql语句通过commit一起提交,或者通过rollback一起回滚,我们知道,当一条sql执行后,内存都被修改了,那怎么还能找到之前的数据呢?肯定还有个地方进行了保存,这个就是undolog,回滚日志。

那么undolog是怎么记录的呢?我们通过一个例子来看下具体的操作

假如,我在事务ID=100的事务中对表进行以下操作

sql 复制代码
BEGIN;
INSERT INTO
zsc_teacher(id,teacher_name,teacher_age,teacher_addr)
VALUES(5,'huihui',18,'湖南');
INSERT INTO
zsc_teacher(id,teacher_name,teacher_age,teacher_addr)
VALUES(6,'james',30,'长沙');
DELETE FROM zsc_teacher WHERE id=6;
UPDATE zsc_teacher SET teacher_age=19 where id=5;

在提交之前,这些操作都会进行 undolog 记录。那么这些数据对应的undoLog是怎么样的?其实很简单,如果是添加数据,回滚把这条记录删除;如果是删除数据,回滚的时候,把删除的重新插入;如果是修改数据,那么把旧值记录下来,然后回滚到以前的值。

undolog的具体记录如下 :

如果事务回滚了,就可以根据undolog的版本中的事务ID,回滚到之前的数据。同时undolog的每个日志,为了保证数据不丢失,也会进行redolog的记录,操作临时表除外。

3. 数据一致性与隔离性

从第2章中我i们知道,任何数据的更改查询,都会默认有一个事务,那么事务就一定是并行执行,既然可以并行,那么当同一条数据或者相关的数据 同时被多个事务更改时,也就是产生并发时,就一定会存在数据一致性的问题。

脏读 : 能读取到其他线程还没有提交的数据;但是这些数据可能是会回滚的。

不可重复读:

  1. 在同一个事务里面 有多次读取 后续的读取跟第一次的读取结果不一致,在第一次读取跟后续的读取之间,有其他的事务进行了相关修改或者删除并且提交 )(读的场景)
  2. 在不同的事务里面,对相同的数据进行操作,如果有一个事务对数据进行 更改还没有提交, 其他的事务也去进行修改提交 (操作的场景)
    幻读:
  3. 在同一个事务里面 有多次读取 后续的读取跟第一次的读取结果不一致,在第一次读取跟后续的读取之间,有其他的事务进行了相关添加 并且提交)(读的场景)
  4. 在不同的事务里面,对范围的数据进行操作没有提交, 其他的事务去进行范围内数据的添加

既然事务并发会导致脏读、不可重复读、幻读的数据一致性问题,肯定得去解决,但是去解决一定会对性能影响,有些人需要考虑性能,有些人需要考虑一致性。所以干脆就提供点方案供自行选择,这个方案就是隔离级别。

READ UNCOMMITTED: 读未提交 不会解决任何问题,但是性能最高

READ COMMITTED: 读已提交,解读了脏读问题,但是没有解决不可重复读的问题

REPEATABLE READ: 在sql标准里面解决了不可重复读问题,在InnoDB中,解决了幻读问题(MVCC LBCC)

SERIALIZABLE: 解决了所有的问题

脏读的演示:

会话一:

sql 复制代码
set SESSION autocommit=0; -- 关闭自动提交
BEGIN;
UPDATE zsc_teacher set teacher_age=teacher_age+1 where id=1; -- 修改之前的数据 不提交

在会话1的事务没有提交的事务,再开启一个ru的事务去读取

sql 复制代码
SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
-- 修改为读未提交 我们发现是会出现脏读的
SELECT * FROM zsc_teacher where id=1; -- 能查询到还没有提交的数据,RU会产生脏读问题

不可重复读与幻读的演示:

会话1

sql 复制代码
SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
set SESSION autocommit=0;
BEGIN;
SELECT * FROM zsc_teacher where id<20; -- 第一次查询,查询不到18以及ID=1的改动

会话2:

sql 复制代码
BEGIN; -- 开启事务,进行数据更改以及数据添加
UPDATE zsc_teacher set teacher_age=teacher_age+1 where
id=1;
INSERT INTO
zsc_teacher(id,teacher_name,teacher_age,teacher_addr)
VALUES(18,'huihui',30,'湖南');
COMMIT;

再次去会话1查询

sql 复制代码
SELECT * FROM zsc_teacher where id<20; -- 由于关闭了自动提交,所以,这个查询跟上次查询在一个事务,我们发现同一个事务中第一次查询跟第二次查询的结果不一样,会读取到最新改动或者添加的数据
COMMIT; -- 关闭了自动提交,如果要提交事务,必须手动提交

关于其它隔离级别的脏读、不可重复读以及幻读问题可以设置不同的隔离级别自行演示.

4.Innodb解决并发问题

innodb解决并发问题是通过两种方式去保证数据的一致性的。一种是非锁定一致性读取,一种是锁定一致性读取。

非锁定一致性读取是去解决读取数据时候的数据一致性问题。不需要对数据进行加锁。比如在事务中对数据的查询,怎么保证查询到的是已经提交的,或者不会查到第一次查询之后其它事务改动的数据。

而锁定一致性读取,则是在对数据进行更改的时候,为了防止别人也进行更改,需要对数据进行加锁。

4.1 MVCC(非锁定一致性读取)

思想:一致性读取意味着 InnoDB 使用多版本来向查询提供数据库在某个时 间点的快照。该查询查看在该时间点之前提交的事务所做的更改,而不查看后来或未提交的事务所做的更改。

所以关键点在于快照,我们来看一下ReadView快照结构

sql 复制代码
class ReadView{
    trx_id_t m_low_limit_id; // 即将要分配的下一个事务ID
     trx_id_t m_up_limit_id; // 所有存活的(没有提交的)事务ID中最小值
    trx_id_t m_creator_trx_id; // 当前的事务ID
    ids_t m_ids; // 创建readView时,所有存活的事务ID列表
}

下面我们来看一个具体的案例,说明以下MVCC是怎么做的。首先确定一个判断规则

  1. 如果数据的 trx_id< m_up_limit_id, 小于没有提交的最小的事务 ID ,说明在创建ReadView 的时候已经提交了,可见。
  2. 如果数据的 trx_id>=m_low_limit_id, 大于等于我即将分配的事务 ID , 那么表明修改这条数据的事务是在创建了 ReadView 之后开启的,不可 见。
  3. 如果 m_up_limit_id<= DB_TRX_ID< m_low_limit_id, 表明修改这条数据 的事务在第一次快照之前就创建好了,但是不确定提没提交,判断有没 有提交,直接可以根据活跃的事务列表 m_ids 判断
    a. DB_TRX_ID 如果在 m_ids 中,表明在创建 ReadView 之时还没提交, 不可见
    b.. DB_TRX_ID 如果不在 m_ids ,表明在创建 ReadView 之时已经提交, 可见

分析: 首先我们看第一条规则,trx_id其实是事务表中的id,m_up_limit_id是当前存活的最小值,也就是说,如果trx_id都小于现在事务表中存在的最小值了,那么肯定这个事务时已经提交的了。可见;其次看第二条规则,trx_id比当前要执行的事务id还要大呢,那肯定时当前事务正在执行中进行的操作,所以不可见。3.如果恰好在中间呢,我们就到m_ids中判断,如果在,那表明我创建快照时还没提交呢;如果不在了,那就表明创建之时已经提交了,就可见。

其详细演示及说明可见链接: MVCC-ProcessOn

MVCC在RC跟RR的区别就在于是不是每次快照读是否会生成新readView,这也是为什么RC没有解决幻读跟可重复读

4.2 LBCC(锁定一致性读取)

在innodb中,我们是改动行数据,这个行数据会加锁,但是到底加锁哪些行数据呢?会根据你去操作的条件去决定,并且锁的是你查询的时候走的索引树的节点,如果你操作条件字段没有索引,就会锁所有的行数据,所以,查询走的索引不同,锁的数据不同,如果锁的是二级索引的节点,也会找到对应的主键索引加锁。'

那么锁到底分为哪些类型呢?

4.2.1 锁的类型

读锁,也成为S锁、共享锁,加了读锁,其它事务能够再次加读锁

应用场景: 当我读取一个数据后,我不希望其它事务对数据进行更改,那么就可以采用读锁

sql 复制代码
BEGIN;
SELECT * FROM zsc_teacher where id=10 FOR SHARE; -- 读锁 FOR SHARE代替了LOCK IN SHARE MODE,但是LOCK IN SHARE MODE向后兼容,加锁后,其他事务在提交之前不能对数据加排他锁,但是能加读锁。
COMMIT;

写锁,也成为X锁、排它锁,加上排它锁,其它事务不能再去加其它的读锁或者写锁,我们操作数据默认就会加上排它锁

sql 复制代码
BEGIN;
SELECT * FROM zsc_teacher where id=10 FOR UPDATE; -- 索引扫描到id=10的数据,那么会锁id=10的数据,其他事务不能进行操作
COMMIT;

或者查询的时候也可以通过for update来进行添加

sql 复制代码
BEGIN;
SELECT * FROM zsc_teacher where id=10 FOR update; 、
COMMIT;

意向锁:在innodb中,锁的粒度又分为表锁、行锁,表锁是对整个表进行加锁、行锁是加某些行。加表锁之前,会去判断有没有加行锁,如果有,就不让加。假如数据量比较大,一行一行去判断有没有锁,性能会很慢。于是,就提出了个意向锁,就是如果有行数据加锁了,就在表上做个标记,代表表里已经有数据加锁了。这样就不需要遍历了。

4.2.2 行锁锁哪些行

行锁到底锁哪些行呢,也会根据不同的锁有所不同,主要分为以下几类

记录锁(Record Locks):顾名思义,是锁在索引记录上的锁,去操作某行存在的数据时,会对该记录加锁。

举例: 假如我们给id=10的数据加排他锁

sql 复制代码
BEGIN;
SELECT * FROM zsc_teacher where id=10 FOR UPDATE; -- 索引扫描到id=10的数据,那么会锁id=10的数据,其他事务不能进行操作
COMMIT;

可以通过性能库performance_schema中的data_locks进行查看

可以发现,必定会加一个ix锁,意向排他锁。同时,基于Primary主键索引加锁 X,REC_NOT_GAP,lOCK_DATA为10

如果此时在另外的事务对10进行更改,则会进行等待,直到锁等待超时

如何查看等待锁线程? 可以通过sys库下的innodb_lock_waits查看哪些线程在等待

sql 复制代码
SELECT
    waiting_trx_id,
    waiting_pid,
    waiting_query,
    blocking_trx_id,
    blocking_pid,
    blocking_query
FROM sys.innodb_lock_waits;

锁的等待超时我们也可以通过innodb_lock_wait_timeout设置。默认50s 最小1s

间隙锁:刚才是基于某条存在的记录进行更改,那么假如我操作的是索引树节点跟节点之间的范围数据,怎么加锁呢?

举例:

sql 复制代码
EGIN;
SELECT * FROM zsc_teacher where id=8 FOR UPDATE;

假设我们要查询的这个8不存在,但是这个数据是属于主键索引树节点1到10之间的。这个时候,就会去加间隙锁,所谓间隙,就是索引的节点跟节点之间的间隙,会锁死整个间隙,但是只针对添加,不针对修改。这个间隙内,也就是1-10这个区间数据不能添加。

看下记录

间隙锁保证了我在操作这个区间的时候,不会有新的数据插入,因此也解决了幻读的问题。

但是间隙锁,在RC级别下是禁用的,仅用于外键约束检查和重复键检查。因而RC是没有解决幻读问题的。

临键锁:临键锁其实就是记录锁+间隙锁,假如我即包含了某个节点,又包含了这个节点到上一个节点的区间,加的就是临键锁

举例:

sql 复制代码
BEGIN;
UPDATE zsc_teacher SET teacher_age=teacher_age+1 where id>8 and id<12;

加锁信息:

4.2.3 死锁检测

什么是死锁?死锁的4个必要条件

两个或多个事务相互等待对方释放锁而陷入无限等待的状态,从而导致事务无法继续执行

  1. 互斥 2 个事务拿互斥的资源
  2. 请求和保持条件 个事务在等待其他事务持有的资源时,仍然保持自己所持有的资源不释放
  3. 不可剥夺 一个事务持有的资源不能被其他事务强制性地抢占
  1. 循环等待
    死锁举例:
    会话1:
sql 复制代码
BEGIN;
UPDATE zsc_teacher SET teacher_age=teacher_age+1 where id=1; -- 第一步执行
UPDATE zsc_teacher SET teacher_age=teacher_age+1 where id=4 -- 第三步执行 阻塞
COMMIT;

会话2:

sql 复制代码
BEGIN;
UPDATE zsc_teacher SET teacher_age=teacher_age+1 where id=4; -- 第二步执行
UPDATE zsc_teacher SET teacher_age=teacher_age+1 where id=1 -- 第四步执行
COMMIT;

第四步执行的时候就会发现死锁。

Mysql默认是会有死锁检测的,要怎么防止死锁呢?

1.随时发出问题 SHOW ENGINE INNODB STATUS 来确定最近一次死锁的原因。这可以帮助您调整应用程序以避免死锁。
2. 如果频繁的死锁警告引起关注,请通过启用该变量来收集更广泛的调试 信息 innodb_print_all_deadlocks 。有关每个死锁的信息(而不仅 仅是最新的死锁)都记录在 MySQL 错误日志 中。完成调试后禁用此选 项。
3. 如果交易因死锁而失败,请始终做好重新发出交易的准备。死锁并不危 险。再试一次。
4.保持交易规模小且持续时间短,以使其不易发生冲突。
5. 在进行一组相关更改后立即提交事务,以使其不易发生冲突。特别是, 不要让交互式 mysql 会话长时间打开且未提交事务。
6.如果您使用 锁定读取 ( SELECT...FORUPDATE 或 SELECT...FORSHARE ), 请尝试使用较低的隔离级别,例如 READ_COMMITED。
7. 当修改事务中的多个表或同一表中的不同行集时,每次都以一致的顺序 执行这些操作。然后事务形成明确定义的队列并且不会死锁。例如,将数据库操作组织到应用程序内的函数中,或调用存储的例程,而不是在不同位置编写多个相似的INSERT、UPDATE和语句序列DELETE
8.将精心选择的索引添加到表中,以便您的查询扫描更少的索引记录并设更少的锁。用于EXPLAIN_SELECT确定 MySQL 服务器认为哪些索引最适合您的查询。
9.少用锁定。如果您有能力允许SELECT从旧快照返回数据,请不要向其中添加 FOR UPDATE or 子句。FOR SHARE此处使用READ_COMMITED隔离级别很好, 因为同一事务中的每个一致读取都从其自己 的新快照中读取。

相关推荐
vvvae123417 分钟前
分布式数据库
数据库
雪域迷影38 分钟前
PostgreSQL Docker Error – 5432: 地址已被占用
数据库·docker·postgresql
bug菌¹1 小时前
滚雪球学Oracle[4.2讲]:PL/SQL基础语法
数据库·oracle
逸巽散人2 小时前
SQL基础教程
数据库·sql·oracle
月空MoonSky2 小时前
Oracle中TRUNC()函数详解
数据库·sql·oracle
momo小菜pa2 小时前
【MySQL 06】表的增删查改
数据库·mysql
向上的车轮3 小时前
Django学习笔记二:数据库操作详解
数据库·django
编程老船长3 小时前
第26章 Java操作Mongodb实现数据持久化
数据库·后端·mongodb
全栈师4 小时前
SQL Server中关于个性化需求批量删除表的做法
数据库·oracle
Data 3174 小时前
Hive数仓操作(十七)
大数据·数据库·数据仓库·hive·hadoop