-
无论何时,只要有多个查询需要在同一个时刻修改数据时,就会有并发问题。MySql主要在
服务器层
与存储引擎层
进行并发控制。假设数据库中国一张邮箱表,每个邮件都是一条记录。如果某个客户正在读取邮箱,同时其他客户试图在删除邮箱表中的某一条数据。这个时候,读取的结构就是不确定的了。在MySql中会通过锁定防止其它用户读取同一数据。大多数时候,MySQL锁的内部管理都是透明的。
MySQL锁的粒度
- 每种MySql引擎都可以实现自己的锁策略和锁粒度,将锁粒度固定在某个级别,可以为某些特定的场景提供更好的性能。
表锁(table lock)
- 表锁是mysql中最基本的锁略,并且是开销最小的策略。它会锁定整个表,一个用户在对表进行写操作(插入、删除、更新等)前,需要先获得写锁,这会阻塞其他用户对该表的所有读写操作。只有没有写锁时,其他读取的用户才能获得读锁,读锁之间是不相互阻塞的。
- 在特定的场景中,表锁也可能有良好的性能。例如,
READ L0CAL
表锁支持某些类型的并发写操作。另外,写锁也比读锁有更高的优先级,因此-一个写锁请求可能会被插入到读锁队列的前面(写锁可以插入到锁队列中读锁的前面,反之读锁则不能插入到写锁的前面)。 - 尽管存储引擎可以管理自己的锁,MySQL本身还是会使用各种有效的表锁来实现不同的目的。例如,服务器会为诸如
ALTER TABLE
之类的语句使用表锁,而忽略存储引擎的锁机制。
行级锁(row lock)
- 行级锁可以最大程度地支持并发处理(同时也带来了最大的锁开销)。
- 在
InnoDB
和XtraDB
,以及其他一些存储引擎中实现了行级锁。 - 行级锁只在存储引擎层实现,而MySQL服务器层没有实现。服务器层完全不了解存储引擎中的锁实现。
MySQL的事务
事务特性
- A(原子性)事务的各步操作是不可分的,保证一系列的操作要么都完成,要么都不完成;
- C(一致性)事务完成,数据必须处于一致的状态;
- I(隔离性)对数据进行修改的所有并发事务彼此之间是相互隔离,这表明事务必须是独立的,不应以任何方式依赖或影响其他事务;
- D(持久性)表示事务对数据处理结束后,对数据更改必须持久化,不管是事务成功还是回滚。事务日志都能够保持事务的永久性。
事务的隔离级别
- SQL标准的事务隔离级别包括:读未提交(read uncommitted)、读提交(read committed)、可重复读(repeatable read)和串行化(serializable )
- 读未提交是指,一个事务还没提交时,它做的变更就能被别的事务看到。
- 读提交是指,一个事务提交之后,它做的变更才会被其他事务看到。
- 可重复读是指,一个事务执行过程中看到的数据,总是跟这个事务在启动时看到的数据是一致的。未提交的更改对其他事务是不可见的
- 串行化:对应一个记录会加读写锁,出现冲突的时候,后访问的事务必须等前一个事务执行完成才能继续执行。最高的隔离级别
MySQL中的事务
- MySQL提供了两种事务型的存储引擎:
InnoDB
和NDB Cluster
。另外还有一些第三方存储引擎也支持事 - MySQL默认采用自动提交(AUTOCOMIT) 模式。如果不是显式地开始-一个个事务,则每个查询都被当作一事务执行提交操作。在当前连接中,可以通过设置AUTOCOMMIT变量来启用或者禁用自动提交模式:
- InnoDB采用的是两阶段锁定协议(two-phase locking protocol)。在事务执行过程中,随时都可以执行锁定,锁只有在执行
COMMIT
或者ROLLBACK
的时候才会释放,并且所有的锁是在同一时刻被释放。 - InnoDB也支持通过特定的语句进行显式锁定
SELECT ... LOCK IN SHARE MODE
和SELECT FOR UPDATE
些语句不属于SQL规范
多版本并发控制MVCC
-
MVCC
是行级锁的一个变种,但是它在很多情况下避免了加锁操作,因此开销更低。虽然实现机制有所不同,但大都实现了非阻塞的读操作,写操作也只锁定必要的行。 - MVCC的实现,是通过保存数据在某个时间点的快照来实现的。也就是说,不管需要执行多长时间,每个事务看到的数据都是一致的。根据事务开始的时间不同,每个事务对同张表,同一时刻看到的数据可能是不一样的。
-
InnoDB
的MVCC,是通过在每行记录后面保存两个隐藏的列来实现的。这两个列,一个保存了行的创建时间,一个保存行的过期时间(或删除时间)。当然存储的并不是实际的时间值,而是系统版本号。每开始一个新的事务,系统版本号都会自动递增。事务开始时刻的系统版本号会作为事务的版本号,用来和查询到的每行记录的版本号进行比较。 - MVCC只在可重复读和读提交的隔离级别生效。其它两个级别都不兼容
在可重复读(
REPEATABLE READ
)隔离级别下,MVCC具体是如何操作的。
SELECT查询操作时
InnoDB会根据以下两个条件检查每行记录:
- InnoDB只查找版本早于当前事务版本的数据行(也就是,行的系统版本号小于或等于事务的系统版本号),这样可以确保事务读取的行,要么是在事务开始前已经存在的,要么是事务自身插入或者修改过的。
- 行的删除版本要么未定义,要么大于当前事务版本号。这可以确保事务读取到的行,在事务开始之前未被删除。
INSERT
- InnoDB为新播入的每-一行保存当前系统版本号作为行版本号。
DELETE
- InnoDB为删除的每-*行保存当前系统版本号作为行删除标识。
UPDATE
- InnoDB为插入-行新记录,保存当前系统版本号作为行版本号,同时保存当前系统版本号到原来的行作为行删除标识。
数据库存储引擎
InnDB存储引擎
- InnDB是Mysql默认的事务型存储引擎。它被设计用来处理大量的短期(short-lived) 事务,短期事务大部分情况是正常提交的,很少会被回滚。InnoDB的性能和自动崩溃恢复特性,使得它在非事务型存储的需求中也很流行
- InnoDB的数据存储在表空间(tablespace) 中,表空间是由InnoDB管理的-个黑盒子,由一系列的数据文件组成。
- InnoDB采用
MVCC
来支持高并发,并且实现了四个标准的隔离级别。其默认级别是REPEATABLE READ (可重复读
) ,并且通过间隙锁
(next-key locking)策略防止幻读
的出现。间隙锁使得InnoDB不仅仅锁定查询涉及的行,还会对索引中的间隙进行锁定,以防止幻影行的插入。 - InnoDB表是基于聚簇索引建立的。聚簇素引对主键查询有很高的性能,不过它的二级索引(secondary index,非主键索引)中必须包含主键列,所以如果主键列很大的话,其他的所有索引都会很大。因此,若表上的索引较多的话,主键应当尽可能的小。
- InnoDB内部做了很多优化,包括从磁盘读取数据时采用的可预测性预读,能够自动在内存中创建hash索引以加速读操作的自适应哈希索引(adaptive hash index),以及能够加速插入操作的插入缓冲区(insert buffer)等
MyISAM引擎
-
MyISAM不支持事务和行锁
,在MySQL5.1之前的版本是默认的存储引擎,有一个缺陷是崩溃后无法恢复。 - 优点是对于只读的数据,或者表比较小,可以忍受修复操作,可以继续使用
- MyISAM会将表存储在两个文件中:数据文件和索引文件,分别以.MYD和.MYI为扩展名
- MyISAM表可以包含动态或者静态(长度固定)行。MySQL会根据表的定义来决定采用何种行格式。MyISAM表可以存储的行记录数,一般受限于可用的磁盘空间,或者操作系统中单个文件的最大尺寸。
-
MyISAM对整张表加锁,而不是针对行
。读取时会对需要读到的所有表加共享锁,写入时则对表加排他锁。但是在表有读取查询的同时,也可以往表中插入新的记录(这被称为并发插入)
Archive引擎
- Archive引擎会缓存所有的写并利用zlib对插人的行进行压缩,所以比MyISAM表的磁盘I/O更少。但是每次SELECT查询都需要执行全表扫描。所以Archive表适合日志和数据采集类应用,这类应用做数据分析时往往需要全表扫描。或者在一- 些需要更快速的INSERT操作的场合下也可以使用。
-
Archive引擎
支持行级锁和专用的缓冲区
,所可以实现高并发的插人。在一个查询开始直到返回表中存在的所有行数之前,Archive引擎会阻止其他的SELECT执行,以实现一致性读。另外,也实现了批量插入在完成之前对读操作是不可见的。这种机制模仿了事务和MVCC的一些特性,但Archive引擎不是一个事务型的引擎,而是-一个针对高速插人和压缩做了优化的简单引擎。
Blackhole引擎
- Blackhole引擎没有实现任何的存储机制,它会丟弃所有插入的数据,不做任何保存。但是服务器会记录Blackhole表的日志,所以可以用于复制数据到备库,或者只是简单地记录到日志。这种特殊的存储引擎可以在--些特殊的复制架构和8志审核时发挥作用。但这种应用方式我们碰到过很多问题,因此并不推荐。
CSV引擎
- CSV引擎可以将普通的CSV文件(逗号分割值的文件)作为MySQL的表来处理,但这种表不支持索引。CSV引擎可以在数据库运行时拷入或者拷出文件。可以将Excel等电子表格软件中的数据存储为CSV文件,然后复制到MySQL数据目录下,就能在MySQL中打开使用。同样,如果将数据写人到一个CSV引擎表,其他的外部程序也能立即从表的数据文件中读取CSV格式的数据。因此CSV引擎可以作为- -种数据交换的机制,非常有用。
Federated引擎
- Federated引擎是访问其他MySQL服务器的-一个代理,它会创建-一个到远程MySQL服务器的客户端连接,并将查询传输到远程服务器执行,然后提取或者发送需要的数据。最初设计该存储引擎是为了和企业级数据库如Microsoft SQL Server和Oracle的类似特性竞争的,可以说更多的是一种市场行为。尽管该引擎看起来提供了一种很好的跨服务器的灵活性,但也经常带来问题,因此默认是禁用的。
Memory引擎
- 如果需要快速地访问数据,并且这些数据不会被修改,重启以后丢失也没有关系,那么使用Memory表(以前也叫做HEAP表)是非常有用的。Memory 表至少比MyISAM表要快一个数量级,因为所有的数据都保存在内存中,不需要进行磁盘I/O。Memory 表的结构在重启以后还会保留,但数据会丢失。
Schema(数据库的组织和结构)与数据类型优化
更小的通常好
- 尽量使用可以正确存储数据的最下数据类型,更小的数据类型通常更快,占用更小的磁盘,内存和cpu缓存,并且处理时需要的cpu周期更少
- 但是要确保没有低估需要存储的值的范围,因为在schema中的多个地方增加数据类型的范围是一个非常耗时和痛苦的操作。如果无法确定哪个数据类型是最好的,就选择你认为不会超过范围的最小类型
简单就好
- 简单数据类型的操作通常需要更少的CPU周期。例如,整型比字符操作代价更低,
- 整型比字符操作代价更低,因为字符集和校对规则(排序规则)使字符比较比整型比较更复杂
- 使用MySQL内建的类型而不是字符串来存储日期和时间
- 用整型存储IP地址。int类型占4个字节,tinyint占1个字节。
尽量避免NUll
- 如果查询中包含null的列,会使得索引,索引统计和值比较更复杂
- 当可以NULL的列被索引时,每个索引记录需要一个额外的字节
datetime和timesamp
- datetime和timesamp都可以存储相同的数据类型,时间和日期,精确到秒。然而timesamp只使用datetime一半的存储空间
- timesamp 会根据时区变化,具有特殊的自动更新能力,允许的时间范围要小很多,有时它的特许能力会成为障碍
字段列的类型选择
- 在满足值的范围情况下,尽量选择最小的数据类型。列如tinyint比int少三个字节。tinyint 1字节 (-128,127) (0,255) 小整数值
- 字段固定长度如手机号,身份证号用
char
,可变长度使用varchar
因为其长度固定,方便程序的存储与查找,付出的是空间的代价。varchar是以空间效率为首位的 - 整数类型通常是标识列最好的数据类型,因为他们很快并且可以使
AUTO_INCREMENT
- 如果可能,尽量批量使用字符串类型,因为它们消耗更多的空间,比数字类型慢
- 如果存储UUID值,可以移除-符号。使用
UNHEX()
函数转换为UUID的16字节的的数字,并存储在一个binary(16)列中。取值时可以使用HEX()
格式化为16进制格式
使用inet_aton和inet_ntoa处理ip地址数据
- 插入数据前,先用
inet_aton
把ip地址转为整型,可以节省空间,因为char(15)
占16字节。 - 显示数据时,使用
inet_ntoa
把整型的ip地址转为电地址显示即可。
总结
- 尽量避免过度设计,例如会导致极其复杂査询的schema设计,或者有很多列的表设计
- 使用小而简单的合适数据类型,除非真实数据模型中有确切的需要,否则应该尽可能地避免使用NULL值。
- 尽量使用相同的数据类型存储相似或相关的值,尤其是要在关联条件中使用的列。
- 注意可变长字符串,其在临时表和排序时可能导致悲观的按最大长度分配内存。
- 尽量使用整型定义标识列。
- 避免使用MySQL已经遗弃的特性,例如指定浮点数的精度,或者整数的显示宽度。
- 小心使用ENUM和SET。虽然它们用起来很方便,但是不要滥用,否则有时候会变成 陷阱。最好避免使用BIT。
- ALTER TABLE是让人痛苦的操作,因为在大部分情况下,它都会锁表并且重建整张表。例如在备机执行ALTER并在完成后把它切换为主库。
创建高性能的索引
索引基础
- 索引有很多类型,Mysql是使用
B+tree
树索引,索引是在引擎层实现而不是服务层。不同引擎的索引工作方式不一样。 -
MyISAM
使用前缀压缩技术使得索引更小,InnoDB
按照原数据根式进行存储。MyISAM
索引通过数据到物理位置引用被索引的行,InnoDB
根据主键引用被索引的行 - 全值匹配:指的是和索引中所有列进行匹配。比如
EXPLAIN
中的type=index - 最左匹配原则: mysql索引规则中要求复合索引要想使用第二个索引,必须先使用第一个索引的原因。(而且第一个索引必须是等值匹配)。也就是如果使用复合索引查询时优先按照索引创建的顺序进行条件查询
索引的选择策略
- 索引字段必须是独立的列,不嗯呢该是表达式和函数运算
- 计算合适的前缀索引长度
- 善用复合索引,而不是为每个字段都建立索引,选择合适的索引列顺序
*当不需要排序和分组时使用频率较高的放在复合索引前列,这时候的索引优化用于where条件 - 尽可能将左范围查询的列放在索引的后面,以便优化器使用尽可能多的索引列。
- 对于范围条件查询,mysql范围列后面的其它的索引列,对于多个等值条件查询则没有这种限制
Inndb主键索引和非主键索引的区别
- 主键索引即存储了索引值,又在叶子中存储了行的数据。所以通过主键查询时效率高,一次查询即可,不需要回表操作。这种即存储索引值又存储行的所有数据的结构叫做(聚簇索引)
- 非主键索引存储索引值,但叶子中存储的是主键ID,所以查询时需要进行一次回表操作才可以取到所有的行数据
索引覆盖
- 索引覆盖是指如果查询的列恰好是索引的一部分,那么查询只需要在索引文件上进行,不需要回行到磁盘再找数据.这种查询速度非常快,称为”索引覆盖”
理想的索引
- 理想的索引。1:查询频繁 2:区分度高 3:长度小 4:尽量能覆盖常用查询字段.
- 索引长度直接影响索引文件的大小,影响增删改的速度,并间接影响查询速度(占用内存多).
- 针对列中的值,从左往右截取部分,来建索引
- 截的越短,重复度越高,区分度越小, 索引效果越不好
-截的越长,重复度越低,区分度越高,索引效果越好,但带来的影响也越大--增删改变慢,并间影响查询速度.所以,我们要在 区分度+长度,两者上,取得一个平衡. - 惯用手法:截取不同长度,并测试其区分度
select count(distinct left(word,6))/count(*) from dict;
使用索引扫描来做排序
- mysql有两种方式可以生成有序的结果:通过排序操作或者按索引顺序扫描。如果explan出来的type值为
index
则说明使用了索引扫描来做排序。 - 只有索引列的顺序和
order by
字段的顺序完全一致,并且所有列的排序方向都一样时,mysql才能使用索引对结果进行排序。 - 如果查询需要关联多表,则只有当order by中使用的字段完全是第一个表时,才能使用索引做排序。order by和查找型的限制一样,都要满足最左前缀要求。
- 如果索引的第一个字段为常量时,where或join中指对这个列制定了固定的常量值,就可以不满足最左前缀要求。比如有个复合索引
(c1,c2,c3)
, 执行select * from where c1=1 order c2,c3
就可以使用到索引排序
重复索引与冗余索引
- 重复索引: 是指 在同1个列(如age), 或者 顺序相同的几个列(age,school), 建立了多个索引,称为重复索引,重复索引没有任何帮助,只会增大索引文件,拖慢更新速度, 去掉.
- 冗余索引:是指2个索引所覆盖的列有重叠, 称为冗余索引比如x,m,列,加索引
index x(x),index xm(x,m)x,xm
索引,两者的x列重叠了,这种情况,称为冗余索引. - 甚至可以把
index mx(m,x)
索引也建立,mx,xm也不是重复的,因为列的顺序不一样.
索引和锁
- 索引可以让查询锁定更少的行,Innodb在访问行的时候对其加锁,而索引可以减少Innodb访问的行数,从而减少锁的数量
查询性能优化
优化数据库访问
- 确认程序是否在检索大量超过需要的数据
- 确认MySQL服务器层是否在分析大量超过需要的数据行
- 是否查询不需要的记录,常见的错误是误认为MySQL只会返回需要的数据,实际上mysql先是返回全部数据,在进行计算。一般使用limit区分
- 多表关联的时候返回了全部的列,只需要取出所用的列即可
- 优化select *操作,以及重复查询同一条数据行做好缓存
- 执行语句分析,检查MySQL是否扫描了额外的记录
重构查询的方式
- 设计查询语句时,是否要将一个复杂的查询拆分成多个简单的查询
- 切分查询:将大查询切分成小查询,每个查询功能完全一样,但是只完成一部分数据的操作,每次返回一小部分的结果。比如在做定时清除线日志表大表数据删除时,分批次删除比较高效,可以大大减少删除时锁的持有时间
- 分解关联查询:可以对每一个表进行一次单表查询,然后将结果在程序中就行关联汇总。查询拆分后,执行单个查询可以减少锁的竞争。可以让缓存的效率更高,许多应用可以方便缓存单表查询对应的结果对象。
Mysql中驱动表的概念
- mysql中指定了连接条件时,满足查询条件的记录行数少的表为驱动表;如未指定查询条件,则扫描行数少的为驱动表。mysql优化器就是这么粗暴以小表驱动大表的方式来决定执行顺序的。
- 有时这种优化方式反而使表的查询效率变低,这个时候
STRAIGHT_JOIN
就排上用场了
比如如下测试的sql,Table1表的FilterID字段建了索引
select t1.*
from Table1 t1
inner join Table2 t2
on t1.CommonID = t2.CommonID
where t1.FilterID = 1
STRAIGHT_JOIN 方式改写驱动表
select t1.*
from Table1 t1
STRAIGHT_JOIN Table2 t2
on t1.CommonID = t2.CommonID
where t1.FilterID = 1
排序优化
- 当MySQL不能通过索引生成排序结果时,MySQL就需要自己排序,如果数据量少则在内存中进行,如果数据量大则使用磁盘,这个过程统一称为文件排序
- 排序的数据量小于排序缓冲区,则使用内存进行快速排序操作。如果内存不够,会先将数据分块,对每个独立的块进行排序,并将各个块的排序结果放在磁盘上,最后将各个排序好的块进行合并返回结果
- mysql在新版本中使用单次排序算法,先读取查询所需要的所有列,然后在根据给定的列进行排序,最后直接返回排序结果。
优化COUNT查询
# 改查询需要扫描474条行数
EXPLAIN select count(*) from system_log20190401000001 where id >5;
# 将条件反转一下,先查询id<5的数据,然后利用总数去做减法也能得到结果
EXPLAIN select (select count(*) from system_log20190401000001)-count(*) from system_log20190401000001 where id <=5;
优化limit分页
- 在做limit分页时通常使用偏移量加排序的方式实现,但是当偏移量非常大的时候性能非常低下,比如取10020条只返回最后20条,前10000条都抛弃了,一般这种方面优化,要吗在页面上做大分页数限制,要吗优化大偏移量的性能。
- 优化此类分页查询的最简单办法就是利用覆盖索返回需要的列,防止回表操作。然后利用返回的数据做一次关联返回所需要的其余列。
- 有时也可以将limit查询转为为已知的位置进行查询,比如betwen 10000 and 10020
- 另一种做法就是缓存1000条数据,每次分页从缓存中取,大于1000就在页面额外设计按钮找到更多数据。
优化UNION查询
- UNION 操作符用于合并两个或多个SELECT语句的结果集
- 默认地,UNION操作符选取不同的值。如果允许重复的值,请使用 UNION ALL
SELECT column_name(s) FROM table_name1
UNION
SELECT column_name(s) FROM table_name2
# UNION ALL
SELECT column_name(s) FROM table_name1
UNION ALL
SELECT column_name(s) FROM table_name2