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们知道,任何数据的更改查询,都会默认有一个事务,那么事务就一定是并行执行,既然可以并行,那么当同一条数据或者相关的数据 同时被多个事务更改时,也就是产生并发时,就一定会存在数据一致性的问题。
脏读 : 能读取到其他线程还没有提交的数据;但是这些数据可能是会回滚的。
不可重复读:
- 在同一个事务里面 有多次读取 后续的读取跟第一次的读取结果不一致,在第一次读取跟后续的读取之间,有其他的事务进行了相关修改或者删除并且提交 )(读的场景)
- 在不同的事务里面,对相同的数据进行操作,如果有一个事务对数据进行 更改还没有提交, 其他的事务也去进行修改提交 (操作的场景)
幻读:- 在同一个事务里面 有多次读取 后续的读取跟第一次的读取结果不一致,在第一次读取跟后续的读取之间,有其他的事务进行了相关添加 并且提交)(读的场景)
- 在不同的事务里面,对范围的数据进行操作没有提交, 其他的事务去进行范围内数据的添加
既然事务并发会导致脏读、不可重复读、幻读的数据一致性问题,肯定得去解决,但是去解决一定会对性能影响,有些人需要考虑性能,有些人需要考虑一致性。所以干脆就提供点方案供自行选择,这个方案就是隔离级别。
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是怎么做的。首先确定一个判断规则
- 如果数据的 trx_id< m_up_limit_id, 小于没有提交的最小的事务 ID ,说明在创建ReadView 的时候已经提交了,可见。
- 如果数据的 trx_id>=m_low_limit_id, 大于等于我即将分配的事务 ID , 那么表明修改这条数据的事务是在创建了 ReadView 之后开启的,不可 见。
- 如果 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个必要条件
两个或多个事务相互等待对方释放锁而陷入无限等待的状态,从而导致事务无法继续执行
- 互斥 2 个事务拿互斥的资源
- 请求和保持条件 个事务在等待其他事务持有的资源时,仍然保持自己所持有的资源不释放
- 不可剥夺 一个事务持有的资源不能被其他事务强制性地抢占
- 循环等待
死锁举例:
会话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隔离级别很好, 因为同一事务中的每个一致读取都从其自己 的新快照中读取。