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