高性能MySQL3th读书笔记
mysql服务器逻辑架构图
-
连接管理与安全性
所有的客户端连接都会有一个线程去处理,查询都会在这一个单独线程中运行,服务器会负责所有线程。mysql5.5之后更新了线程池插件,支持使用少量的线程来服务大量的链接。
-
优化与执行
MySQL会解析查询,创建内部数据结构(解析树),然后对其进行各种优化。
-
用户可以通过特殊关键词提示优化器如何优化。
-
优化器与存储引擎有一定关系,存储引擎对于优化查询有影响。
-
对于
select
语句,优化器会优先检测缓存,缓存中有的话就不必执行查询解析,优化和执行。
-
-
并发控制
- 对于并发的控制的经典解决为写锁(共享锁)和读锁(排他锁)。
- 锁粒度:如果我们只锁定需要修改的部分,而不是对于所有部分都加锁,这会提供更好的并发性能。
- 加锁也会消耗资源,如果对于锁的各种管理的消耗超过了本身存取数据,那么性能会受到较大的影响。
锁策略
就是对于锁的开销和数据的安全性之间的平衡,一般数据库采用在表上施加行级锁
,采用了各种复杂的方式来实现,来提供更好的性能。mysql采用多种存储引擎来应对不同环境下的二者平衡性的要求。
-
两种重要锁策略
-
表锁
扫描二维码关注公众号,回复: 10382184 查看本文章MySQL中最基本,开销最小的策略。它会锁定整张表。此外,写锁比读锁有更高的优先级,因此一个写锁请求可能被插入到读锁之前。(比较严格,读锁不能插入到写锁前面)。
尽管存储引擎会管理自己的锁,mysql本身也会有各种表锁策略,可能会忽略存储引擎的锁机制。
-
行级锁
可以最大程度地支持并发处理,当然也有最大的管理锁的开销。行级锁只在存储引擎层实现(参照mysql服务器逻辑架构图)。所以服务器层并不了解各存储引擎如何实现自己的行级锁。
-
-
事务
-
一组原子性的SQL查询(一组独立工作单元)。事务内的语句,要么全部执行成功,要么全部失败。
举例:
对于银行转账功能,我们就可以使用事务来实现对A的钱数减少,对B的钱数增加的这一组原子操作,以此来保证不会出现A钱数减少而B钱数由于别的线程抢夺控制权等别的原因而并未增加这一现象。
-
事务的四个性质:
-
原子性
上述介绍的,都是原子操作,即不可分割,事务内部的操作要么都完成,要么都不完成,不可能出现完成一部分而另一部分并未完成这样的现象。
-
一致性
表示数据库总是从一个一致性的状态转移到另一个一致性的状态,对于一个事务,若一部分完成,另一部分未完成,这些修改就不会保存提交到数据库中。一定是完整完成后保存到数据库中。
-
隔离性
一个事务在其未完成最终提交之前,其状态对于其他事务是不可见的。
-
持久性
一旦事务提交,其所对于数据库所做的修改将永久保存在数据库中。(此处不必太过纠结“持久”这个词,因为数据库本身其实也可能会因为特定原因而损坏,这也就是为何需要备份来保证数据库持久性的原因)。
同加锁来保证并发的安全和正确性一样,事务也会增加开销。所以,此时,对于需要使用事务与否,我们可以选择更加合适的存储引擎。(及时选择的存储引擎不支持事务,我们也可以使用
Lock table
来提供一定程度的保护)。 -
-
-
隔离级别:
SQL标准的隔离级别有四种:未提交读、提交读、可重复读、可串行化。
-
未提交读
事务中的修改,即使没有提交,也是对其他事务可见的。事务可以读取未提交的数据,也就是
脏读
。这个级别会导致很多问题,性能也不会比其他好太多,一般很少使用。 -
提交读
(大多数数据库的默认隔离级别)。一个事务从开始直到提交之前,对于其他事务都是不可见的。这个级别被称为
不可重复读
。(因为两次同样查询的结果可能是不同的) -
可重复读
(MySQL的默认事务隔离级别)此级别保证了同一事务中,多次读取同样记录的结果是一致的。但是无法解决
幻读
。 在可重复读中,该sql第一次读取到数据后,就将这些数据加锁(悲观锁),其它事务无法修改这些数据,就可以实现可重复读了。但这种方法却无法锁住insert的数据,所以当事务A先前读取了数据,或者修改了全部数据,事务B还是可以insert数据提交,这时事务A就会发现莫名其妙多了一条之前没有的数据,这就是幻读。(InnoDB和XtraDB存储引擎通过基于乐观锁的多版本并发控制解决了幻读的问题。)名词解释:
-
悲观锁
不同线程同时执行时,只能有一个线程执行,其他的线程在入口处等待,直到锁被释放, 其他事务才能够执行与该锁冲突的操作。悲观并发控制主要用于数据争用激烈的环境,以及发生并发冲突时使用锁保护数据的成本要低于回滚事务的成本的环境中。
线程冲突激烈的情况下选用此锁
-
乐观锁
不同线程同时执行时,可以同时进入执行,在最后更新数据的时候要检查这些数据是否被其他线程修改了,没有修改则进行更新,否则放弃本次操作。
两种实现策略:
MVCC(多版本并发控制)
记录数据版本,数据版本,为数据增加的一个版本标识。当读取数据时,将版本标识的值一同读出,数据每更新一次,同时对版本标识进行更新。当我们提交更新的时候,判 断数据库表对应记录的当前版本信息与第一次取出来的版本标识进行比对,如果数据库表当前版本号与第一次取出来的版本标识值相等,则予以更新,否则认为是过期数据。 实现数据版本有两种方式,第一种是使用版本号,第二种是使用时间戳。
- 版本号如何实现:
此时线程1被阻塞。
在此期间别的线程进行了多次更新,更新到了版本号03
此时线程1恢复运行,进行版本号比较,01版本和03版本不同,更新失败。
- 时间戳方式
每一个事务都会具有一个全局唯一的时间戳,它即可以使用系统的时钟时间,也可以使用计数器,只要能够保证所有的时间戳都是唯一并且是随时间递增的就可以。 读请求,数据库直接将最新时间戳的数据返回,不会被任何操作阻塞,而写操作在执行时,事务的时间戳一定要大或者等于数据行的读时间戳,否则就会被回滚。
CAS( 比较并替换 )
CAS机制当中使用了3个基本操作数:内存地址V,旧的预期值A,要修改的新值B。
更新一个变量的时候,只有当变量的预期值A和内存地址V当中的实际值相同时,才会将内存地址V对应的值修改为B。
例子:
有一个线程1,针对内存地址为V的初始值为A(10),需要将其修改为B(11)。
但是在线程1更新之前被阻塞,被另一个线程将V更新为11了。
此时线程1对比两次V值,因为不同,所以会更新失败
线程1重新获取当前内存地址为V的值再进行比较,发现内容相等,修改V的值即可
CAS存在ABA问题,还是采用版本号问题来解决。
所谓ABA问题,举个简单例子来说明:
小明有1000元,进行取款500元时,机器故障,出现了两个相同线程来处理这次取款操作。
线程1:余额1000元,期望修改为500元。 线程2:余额1000元,期望修改为500元。
此时,我们假定线程1执行完毕,线程2遭到阻塞,而且此时刚好小明母亲给小明转账500元。
线程1:余额1000元,期望修改为500元。// 修改成功 线程2:余额1000元,期望修改为500元。//被阻塞 线程1:余额500元,期望修改为1000元。// 顺利执行 // 当前情况下的结果:余额1000元,线程2待处理。
此时,线程2如果被处理,则余额变为500元,小明平白无故损失500元。
- 综上所述,还是采用MVCC方式比较好。
-
-
可串行化
最高隔离级别,强制事务串行执行,所以可以避免幻读。由于添加的锁太多,会导致大量的锁争抢以及超时问题。所以实际应用很少涉及此级别,只有非常需要确保数据的一致性而且可以接受没有并发的情况下,才考虑该级别。
-
总结表
隔离级别 脏读 不可重复读 幻读 未提交读 可能 可能 可能 已提交读 不可能 可能 可能 可重复读 不可能 不可能 可能 可串行化 不可能 不可能 不可能
-
文中关于悲观锁和乐观锁的相关资源来源
关于CAS的图文以及例子来源
来自微信公众号(程序员小灰)
什么是 CAS 机制?
什么是 CAS 机制?进阶篇