0
点赞
收藏
分享

微信扫一扫

什么是产品经理?

云岭逸人 2024-05-27 阅读 4

1. ACID模型

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

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

2. Atomicity(原子性)

2.1 事务

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

事务的提交与回滚

-- 默认是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来回滚。

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

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

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

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回滚日志

怎么查看事务?

SELECT * FROM information_schema.INNODB_TRX;

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

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

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

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

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

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的版本中的事务ID,回滚到之前的数据。同时undolog的每个日志,为了保证数据不丢失,也会进行redolog的记录,操作临时表除外。

3. 数据一致性与隔离性 

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

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

脏读的演示:

 会话一: 

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

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

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

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

会话1

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

会话2:

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查询

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

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

4.Innodb解决并发问题

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

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

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

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

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

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是怎么做的。首先确定一个判断规则

分析:首先我们看第一条规则,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锁、共享锁,加了读锁,其它事务能够再次加读锁

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

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

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

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

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

BEGIN;
SELECT * FROM zsc_teacher where id=10 FOR update; 、
COMMIT;

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

4.2.2 行锁锁哪些行

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

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

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

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查看哪些线程在等待

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

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

举例:

EGIN;
SELECT * FROM zsc_teacher where id=8 FOR UPDATE;

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

看下记录

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

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

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

举例:

BEGIN;
UPDATE zsc_teacher SET teacher_age=teacher_age+1 where id>8 and id<12;

 加锁信息:

4.2.3 死锁检测

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

死锁举例:

会话1:

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:

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默认是会有死锁检测的,要怎么防止死锁呢?

举报

相关推荐

0 条评论