MySql(四)——MVCC以及一些配置参数简介

前言

前三篇博客从索引到事务控制均作出了一些总结,这一篇是mysql系列的最后一篇,用于总结MVCC机制如何保证数据一致性作出的工作。

一个问题

直接看实例吧,上一篇博客的结尾,提出了一个问题,动图如下:

事务A在执行update语句的事务的时候,是会给指定的数据加上一个X锁的,意味着其他事务无法再获取这个数据的任何锁,无法读取数据,但是我们在另一个事务B中却能正常的查询数据。这个似乎与X锁的机制相冲突

再看一个实例:

在事务A执行update语句的事务没有正常提交的时候,事务B前后读取的数据确实没有出现不可重复读的问题,这证明确实X锁存在,那到底是什么机制,能让Mysql在加X锁的时候,依旧能读取相关数据呢?这就涉及到MVCC了。

MVCC版本控制

MVCC——Multi Version Concurrency Control(多版本并发控制)。其实我们抛开这个MVCC,单纯的回到上面的实例1,如果X 锁被加上了,事务B在执行select * from users where id = 1的时候,如果无法读取则效率无疑会大大打折扣,这对于一个数据库来说是无法忍受的。如果在加了X锁的时候,不让读取数据,这似乎和悲观锁有点像,我们知道悲观锁的效率问题是个头疼的问题。MVCC从某一种层度上来讲和乐观锁有点类似。因此MVCC的主要作用就是在保证了数据一致的情况下,还优化了X锁带来的效率问题。

所以我们可以这样去理解MVCC——在并发访问数据库时,对正在事务内处理的数据做多版本的管理。用以达到写操作阻塞时导致读操作的并发效率问题

下面我们深入总结一下MVCC是如何是实现版本控制的。从正常的数据操作:插入,查询,修改和删除结合实例来分析。

插入

其实mysql会在每一行数据中增加有关版本控制的隐藏列,这些列是不可见的,如下所示:

名称是我自己随便定义的,只是为了进行区分,DB_TRX_ID为数据入库的版本号,DB_ROLL_PT为数据删除的版本号。

执行以下SQL之后,相关记录数据会有变化

BEGIN; -- 这一句会尝试去获取一个全局的事务Id
INSERT INTO users (NAME,age) VALUES ('test01',18);
INSERT INTO users (NAME,age) VALUES ('test02',26);
COMMIT;

执行上述SQL中的begin语句,获取事务的时候,会尝试去获取一个全局的事务id(这里可以假设该事务ID=1),之后在insert数据的时候,数据的DB_TRX_ID会被置为该事务ID,上述事务执行完成之后,表中的记录会变成如下所示:

 ps : 如果上述的SQL在执行的时候,取消了begin和commit语句,同时开启了事务的自动提交,两条记录落库的嘶吼,最终对应的事务id是不同的,这个具体原因可以参考之间的博客。

删除

在插入记录的基础上,如果执行以下删除事务SQL:

BEGIN;
DELETE FROM users WHERE id = 2;
COMMIT;

 同样begin;语句会尝试获取事务id,这里假设获取的事务id为22,则执行该SQL之后,数据库中的数据表为:

修改 

 修改逻辑相比于插入和删除的流程有点复杂了。还是直接上实例吧,有一个存在update语句的事务SQL如下

BEGIN;
UPDATE users SET age = 19 WHERE id = 1;
COMMIT;

 修改操作,其实需要先将待修改的数据行copy一份,然后将原有的数据行的删除版本号(DB_ROLL_PT)置成当前的修改事务id。同时将复制的新的数据行的版本号置为新的事务id(这里假设事务id为33)。相比插入和删除多了一个copy的过程。

查询

下面继续看查询的MVCC操作,就在上一个操作的基础上进行说明,查询事务对应的SQL如下:

BEGIN;
SELECT * FROM users WHERE id = 1;
COMMIT;

同样,假设这个事务的事务id为55,走到现在了数据表就如上个实例所示,在查询操作的时候,对数据的版本号有着很多限制。 具体的数据查询规则如下:

1、查找操作,只能查找数据行版本(DB_TRX_ID)早于当前事务id的数据行。——这样可以确保事务读取行的时候,要么是在事务开始前已经存在的数据,要么是事务自己插入或者修改的数据。

已之前的数据为例,这里似乎三条数据都满足。

2、查找操作,查找删除版本号要么为null,要么大于当前事务版本号的记录。——这样可以确保取出的行记录在事务开启之前没有被删除。

加上条件2的限制之后,只有一条数据满足了——(test01,19)满足。

上述两个操作是并且的条件,两个都必须同时满足

以上就是MVCC针对CRUD的四种操作。

那来看看MVCC是否解决了脏读和不可重复读的问题(幻读这里不再解释,毕竟幻读本身也不是通过MVCC解决的,而是通过临键锁)。

依旧有问题

准备一下两个事务SQL。表结果回退到正常有两条记录的时候。

事务1

-- 事务1
BEGIN;					--  操作1
SELECT * FROM users WHERE id = 1;	--  操作2
COMMIT;

 事务2

-- 事务2
BEGIN;					--  操作3
UPDATE users SET age = 27 WHERE id = 1; --  操作4
COMMIT;

针对不可重复读:

根据上一篇博客中不可重复读问题的定义,操作步调应该是:操作1,操作2,操作3,操作4,操作2。这里先不上动图,根据MVCC对数据的操作我们一步步分析。

操作1和操作2执行完成之后,MVCC机制的存在,表中记录会如下所示:

毕竟事务1只是做了查询,能修改个啥?

执行了操作3和操作4之后,表结构就如下所示了

然后再执行操作2,根据查询的版本控制原则,复制后的数据由于事务id大于事务1的事务id,所以无法读取,则读取的结果和操作3,4执行之前一致,确实解决了不可重复读的问题。牛逼。 

针对脏读

如果要模拟脏读,执行步骤按照如下步骤执行:操作3,操作4,操作1,操作2。

先执行操作3和操作4之后,数据记录和上图一致,再贴一次吧。

之后再执行操作1和操作2。这个时候事务1的事务id就大于事务2的事务id了,能将复制的数据读取出来最终匹配的结果如下:

这就很尴尬了,事务1真的读取了事务2的脏数据。这就存在问题。 那么问题在哪儿?,这个就需要undo和redo机制来弥补了。

undo

上面已经说过,单纯的MVCC好像依旧有脏读的问题。这个时候我们该怎么搞?InnoDB其实已经通过undo机制和redo机制解决了这个问题。先介绍undo

undo——即undo log。意为取消,以撤销操作为目的,返回到之前某个指定状态的操作。这个不得不让人联想到事务的原子性。其实没错,undo log就是为了实现事务的原子性而出现的产物,它指的就是在操作数据之前,首先会将需要操作的数据备份到一个地方(undo log)后来实现Innodb引擎的时候,由于MVCC依旧没有解决脏读的问题,但是发现存在undo log的机制,发现这玩意可以搞定脏读,因此Undo log 在innodb引擎中也可以用来弥补MVCC的问题这个时候事务未提交之前,undo保存了事务未提交之前的数据版本。undo中的数据可以作为数据旧版本快照供其他并发的事务进行读取。这个机制也正好完美解决了加入X锁并发情况下,无法读取数据的问题。其他事务读取快照就好。下图为undo机制示意图。

这个时候,如果读取的是快照,就发现并不会存在脏读问题了。 

快照读

普通SQL读取的数据是快照版本,普通的select其实就是快照读取。这些数据由内存中的缓存数据和undo buffer中的数据组合而成(有的数据行没有加入X锁,则直接读取内存中的cache——上图中的teacher表部分,如果发现某些数据行上了X锁,则自动读取undo buffer中的老版本的数据)。

当前读

SQL读取的就是数据的最新版本了,通过锁机制来强制保证读取的时候数据无法通过其他事务进行修改。update,delete,insert,select+lock in share mode ,select+for update 均为当前读。(某一种层度上可以将当前读理解为强制不去读取undo buffer)

思考:如果在某个事务在update数据的时候,我强制用当前读,是不是依旧有脏读呢?来,试试,上动图

从结果来看,不好意思——会阻塞。这也进一步说明了X锁,锁住了cache中的数据(没有锁住undo buffer中的数据) 。

来个小结

走到这里,我们发现单纯用MVCC依旧没法解决脏读的问题,快照+MVCC的方式才一起解决了不可重复读和脏读的问题,临键锁解决了幻读的问题。

redo

总结到了undo,不得不提提redo

redo机制就是重做,是以恢复操作为目的的机制。redo log是指事物中操作的任何数据,将最新的数据备份到redo log。

redo是为了事务的持久性而产生的一个机制。redo log是为了防止在数据库发生故障的是时候,内存中还存在没有写入磁盘的数据,在下次mysql重启的时候,会根据redo log 进行重做,从而将没有持久化的事务数据统一进行持久化。如下示意图所示:

undo与redo示意图:

redo log补充点

redo log在磁盘上是对应存储文件的,如下所示,一般存放在{datadir}/ib_logfile1&ib_logfile2中,存放地址可以通过innodb_log_group_home_dir来进行配置。

一旦事务成功提交且数据持久化落盘之后, 此时Redo log中的对应事务数据记录就失去了意义, 所以Redo log的写入是日志文件循环写入的。针对这些redo log文件还有些参数设定,如下所示:

指定Redo log日志文件组中的数量 innodb_log_files_in_group 默认为2
指定Redo log每一个日志文件最大存储量innodb_log_file_size 默认48M
指定Redo log在cache/buffer中的buffer池大小innodb_log_buffer_size 默认16M

Redo buffer 持久化Redo log的策略, Innodb_flush_log_at_trx_commit:

  • 取值 0 每秒提交 Redo buffer --> Redo log OS cache -->flush cache to disk[可能丢失一秒内的事务数据]
  • 取值 1 默认值, 每次事务提交执行Redo buffer --> Redo log OS cache -->flush cache to disk[最安全, 性能最差的方式]
  • 取值 2 每次事务提交执行Redo buffer --> Redo log OS cache 再每一秒执行 ->flush cache todisk操作

一些参数

需要知道一点,mysql其实分为会话参数和全局参数全局参数建议配置到默认的配置文件中,会话参数的设定会随着会话的断开而失效全局参数重新设定只有,对于已经连接的会话是无效的,会话重连后才会生效

在某台服务器上快速找到mysql的配置文件可以尝试一下命令:

mysql --help | grep -A 1 'Default options are read from the following files in the given order'

一些小坑

最大连接数的配置(max_connections)配置的介绍。这个参数受到两个其他配置的限制,一个是系统的句柄数。一个是mysql自身设置的句柄数。

系统句柄数在 /etc/security/limits.conf文件中的ulimit -a有设置。mysql句柄数在/usr/lib/systemd/system/mysqld.service文件中有配置。

一些内存参数配置

针对每个连接参数的配置

sort_buffer_size——connection排序缓冲区的大小(建议256K到2M之内),当查询语句中需要文件排序功能的时候,马上为connection分配配置的内存。

join_buffer_size——connection关联查询的缓存区大小(建议256K到1M之内),当查询涉及关联查询的时候就会分配一个关联查询缓冲区(一个join会分配一个缓冲区,一个语句中如果存在多个join操作,则会分配多个关联查询缓冲区)

Innodb_buffer_pool_size —— innodb引擎的缓存区大小(默认为128M),用于innodb引擎中的数据缓存,索引缓存 缓存数据和内部结构。这个缓存可以减少多次磁盘IO访问。

一般Innodb_buffer_pool_size = (总物理内存-系统运行所用-connection所用)*90%。

大牛详细总结了mysql中的各种常见配置项:MySQL各种详细配置项

一些数据库表的设计建议

有点鸡肋的范式

关于范式的介绍大学的教材中的概念介绍的实在是太过官方,这里简单总结一下

第一范式:每一列只有一个单一的值,列不可再拆分。

第二范式:每一行都有主键能进行区分。

第三范式:每一个表都不包含其他表已经包含的非主键信息。

过分的满足第三范式就会造成太多的表关联。而且在实际开发中几乎已经不用外键这种骚操作了,所以其实范式现在有些鸡肋。

一些建议的规范

下面介绍一下某互联网企业的SQL规范:

1、必须级别的

必须使用InnoDB存储引擎(这么牛逼,不用是不可能的)。

必须使用UTF8-MB4字符集(便于维护)。

数据表、数据字段必须加入中文注释

2、禁止级别的

禁止使用存储过程、视图、触发器和event。

禁止存储大文件或者照片(存url不爽歪歪么)

3、规范级别的

只允许使用域名(为了安全考虑)。

库名、表名、字段名均小写,并且用下划线风格不可超过32个字符。

表名以t_XXX命名,非唯一索引以idx_xxx命名,唯一索引以uniq_xxx命名。

4、设计级别的规范

一个数据库中单实例表数目必须小于500。

单表列的个数必须小于30。

表必须要有主键。

禁止使用外键必须把字段定义为not null并且提供默认值

对null的处理,只能采用is null 或者 is not null (不能采用 = ,in,<,<>,!=和not in)这种操作。

禁止使用text和blob类型。

禁止使用小数存储货币类型。

必须用varchar(20)存储手机号

禁止使用ENUM,可以使用TINYINT代替。

5、索引相关的

单表索引建议控制在5个以内。

单表索引字段数不允许超过5个。

禁止在更新十分频繁、区分度不高的属性上建立索引。

建立组合索引,必须把区分度高的字段放在前面。

6、SQL使用规范

禁止使用select *

禁止使用insert into t_xxx values(XXX) 必须显示指定插入列的属性。

禁止使用属性隐式转换 例如:select id from t_user where phone = 13812345678 没有用字符串类型会导致全表扫描。

禁止在where条件上使用函数或者表达式。这样会导致全表扫描

禁止负向查询,以及%开头的模糊查询。

禁止使用大表的join,禁止使用大表的子查询。

禁止使用or条件查询,改为in查询(mysql优化了in查询)

SQL的异常必须要捕获。

总结

至此mysql专栏告一段落,只是自己的一个学习总结。

发布了129 篇原创文章 · 获赞 37 · 访问量 9万+

猜你喜欢

转载自blog.csdn.net/liman65727/article/details/103107255
今日推荐