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隔离级别很好, 因为同一事务中的每个一致读取都从其自己 的新快照中读取。

相关推荐
江上挽风&sty2 分钟前
【Django篇】--动手实践Django基础知识
数据库·django·sqlite
奥顺互联V3 分钟前
一次性部署:使用Docker部署PHP应用
大数据·mysql·开源·php
向阳12186 分钟前
mybatis 动态 SQL
数据库·sql·mybatis
胡图蛋.7 分钟前
什么是事务
数据库
小黄人软件9 分钟前
20241220流水的日报 mysql的between可以用于字符串 sql 所有老日期的,保留最新日期
数据库·sql·mysql
张声录114 分钟前
【ETCD】【实操篇(三)】【ETCDCTL】如何向集群中写入数据
数据库·chrome·etcd
无为之士20 分钟前
Linux自动备份Mysql数据库
linux·数据库·mysql
小汤猿人类34 分钟前
open Feign 连接池(性能提升)
数据库
阳冬园1 小时前
mysql数据库 主从同步
数据库·主从同步
XiaoH2331 小时前
培训机构Day15
sql·mysql