事务隔离
事务的四个特性ACID(Atomicity、Consistency、Isolation、Durability,即原子性、一致性、隔离性、持久性)
原子性:都成功或都失败
一致性:一个事务在执行之前和执行之后,数据库都必须处以一致性状态。比如:如果从A账户转账到B账户,不可能因为A账户 扣了钱,而B账户没有加钱
隔离性:并发环境中,并发的事务是互相隔离的,一个事务的执行不能被其它事务干扰。
持久性:事务一旦提交后,数据库中的数据必须被永久的保存下来。即使服务器系统崩溃或服务器宕机等故障。只要数据库重新启动,那么一定能够将其恢复到事务成功结束后的状态
隔离得越严实,效率就会越低。
SQL 标准的事务隔离级别包括:读未提交(read uncommitted)、读提交(read committed)、可重复读(repeatable read)和串行化(serializable )。
假设两个事务A和B同时执行,都在更改一个变量P
- 读未提交:A更改P,并在A提交事务之前B查询到的P是已经被改了的了
- 读提交:A更改P,在A提交事务之前B查到的P没变,在A提交事务之后B查到的P变了
- 可重复读:随便A怎么更改P,在B开始事务的时候B查到的P和结束事务的时候B查到的P一直是一样的(B的事务期间B没有改P)
- 串行化:即对P加锁,一个事务没执行完,另一个就不能操作P
实现原理:
数据库里面会创建一个视图,访问的时候以视图的逻辑结果为准。
- 读未提交没有视图概念
- 读提交的视图是在每个 SQL 语句开始执行的时候创建
- 可重复读的视图在事务启动时创建的,整个事务存在期间都用这个视图,此时的视图可以认为是静态的,不受其他事务更新的影响。
- 串行化直接用加锁的方式来避免并行访问。
提醒:Oracle 数据库的默认隔离级别其实就是“读提交”,因此对于一些从 Oracle 迁移到 MySQL的应用,为保证数据库隔离级别的一致,你一定要记得将 MySQL 的隔离级别设置为“读提交”。 配置的方式是,将启动参数transaction-isolation 的值设置成 READ-COMMITTED。你可以用 show variables来查看当前的值。
事务隔离的实现
在 MySQL 中,实际上每条记录在更新的时候都会同时记录一条回滚操作。记录上的最新值,通过回滚操作,都可以得到前一个状态的值。
不同时刻启动的事务会有不同的 read-view,同一条记录在系统中可以存在多个版本,就是数据库的多版本并发控制(MVCC)。对。
当系统里没有比这个回滚日志更早的 read-view 的时候,回滚日志会被删除。所以尽量不要使用长事务,不然回滚段可能会占用大量存储空间,而且长事务还占用锁资源,也可能拖垮整个库。
事务的启动方式
- 显式启动事务语句,
begin
或start transaction
。配套的提交语句是commit
,回滚语句是rollback
。
例如:
START TRANSACTION;
事务代码
commit;
set autocommit=0
,这个命令会将这个线程的自动提交关掉。这样子的话你的事务就会从第一句SQL语句到主动执行commit
或rollback
语句、或者链接断开。
set autocommit=0
mysql中查看当前自动提交状态的命令为:show VARIABLES like 'autocommit';
建议总是使用 set autocommit=1
, 并通过显式语句的方式来启动事务。
如果想减少语句的交互次数,推荐第二种,第二种会比第一种少一个begin
,并建议使用commit work and chain
语法。
如果执行 commit work and chain
而不是commit
,则是提交事务并自动启动下一个事务,这样也省去了再次执行 begin 语句的开销。同时带来的好处是从程序开发的角度明确地知道每个语句是否处于事务中。
可以在 information_schema
库的 innodb_trx
这个表中查询长事务
下面这个是查找持续时间超过 60s 的事务。
select * from information_schema.innodb_trx where TIME_TO_SEC(timediff(now(),trx_started))>60
课后问题:
如何避免长事务对业务的影响?
答:
- 从应用开发端来看:
1.1 确认是否使用了 set autocommit=0。
1.2 确认是否有不必要的只读事务,即框住了几个select
1.3 业务连接数据库的时候,根据业务本身的预估,通过SET MAX_EXECUTION_TIME
命令,来控制每个语句执行的最长时间,避免单个语句意外执行太长时间。 - 从数据库端来看:
2.1 监控 information_schema.Innodb_trx 表,设置长事务阈值,超过就报警 / 或者 kill;
2.2 Percona 的 pt-kill 这个工具不错,推荐使用;
2.3 在业务功能测试阶段要求输出所有的 general_log,分析日志行为提前发现问题;
2.4 如果使用的是 MySQL 5.6 或者更新版本,把 innodb_undo_tablespaces 设置成 2(或更大的值)。如果真的出现大事务导致回滚段过大,这样设置后清理起来更方便。
索引
索引的常见模型
- 哈希表
一种以键 - 值(key-value)存储数据的结构
好处是增加新的数据时速度会很快。但缺点是,因为不是有序的,所以哈希索引做区间查询的速度是很慢的。所以,哈希表这种结构适用于只有等值查询的场景 - 有序数组
有序数组在等值查询和范围查询场景中的性能就都非常优秀,配合二分法,查找只要 O ( l o g N ) O(logN) O(logN),但是插入数据时需要进行 O ( N ) O(N) O(N)的数据移动,所以有序数组索引只适用于静态存储引擎,比如存储去年的统计信息 - 平衡二叉树
为了维持 O ( l o g N ) O(logN) O(logN)的查询复杂度,就不说最基本的二叉搜索树了。平衡树的更新也是 O ( l o g N ) O(logN) O(logN) - N叉树
实际上大多数的数据库存储却并不使用二叉树。其原因是,索引不止存在内存中,还要写到磁盘上。因为磁盘IO次数与树的高度成正比,为了减少磁盘IO次数,我们将子结点数量增多,以此降低树的高度 - 等等
InnoDB的索引模型
在 InnoDB 中,表都是根据主键顺序以索引的形式存放的,这种存储方式的表称为索引组织表。
InnoDB使用了B+树,每一个索引在 InnoDB 里面对应一棵 B+ 树。
举个索引存储的例子
一张表,表有主键索引ID
和非主键索引k
和数据R
,表中 R1~R5 的 (ID,k) 值分别为 (100,1)、(200,2)、(300,3)、(500,5) 和 (600,6),两棵树的示例示意图如下。
即左树为主键ID的索引树,右树为非主键索引k的索引树
根据叶子节点的内容,索引类型分为主键索引和非主键索引。
主键索引的叶子节点存的是整行数据。在 InnoDB 里,主键索引也被称为聚簇索引(clustered index)。
非主键索引的叶子节点内容是主键的值。在 InnoDB 里,非主键索引也被称为二级索引(secondary index)。
如果查询的条件语句是where ID=500
,那就会搜索ID这棵树
如果条件语句是where k=5
那就会先搜索k这棵树,再拿着搜到的主键ID去ID那棵树里搜。这个过程也称为回表。
也就是说,基于非主键索引的查询需要多扫描一棵索引树。因此,我们在应用中应该尽量使用主键查询。
(下面这张图不来自上面的例子,随便找的)
索引维护
B+树的维护有点麻烦,比如下图要插入一个400,那就要挪动500和600
而更糟的情况是,如果 R5 所在的数据页已经满了,根据 B+ 树的算法,这时候需要申请一个新的数据页,然后挪动部分数据过去。这个过程称为页分裂。而且页分裂还影响数据页的利用率。原本放在一个页的数据,现在分到两个页中,整体空间利用率降低大约 50%。
当然为了提高利用率,也会进行合并
自增主键的使用
一些建表规范里面要求建表语句里一定要有自增主键。
在建表语句中一般是这么定义的: NOT NULL PRIMARY KEY AUTO_INCREMENT
。插入新记录的时候可以不指定 ID 的值,系统会获取当前 ID 最大值加 1 作为下一条记录的 ID 值。
对比前面那个插入操作,递增的插入就只会在结点数组的后面追加,而不会出现后面的数据全部挪动的情况,也不会触发叶子节点的分裂,就很好。
那我们为什么不选用业务逻辑字段作为主键呢?
答:业务逻辑不容易保证有序插入
我们为什么不选用如身份证这种也具有唯一性的字段作为主键呢?
答:主键长度越小,普通索引的叶子节点就越小,普通索引占用的空间也就越小。
所以,从性能和存储空间方面考量,自增主键往往是更合理的选择。
有没有什么场景适合用业务字段直接做主键的呢?
答:K-V场景,即只有一个索引且该索引必须是唯一索引,这种时候直接设K为主键,避免每次查询需要搜索两棵树。
覆盖索引
对于一般的区间查询,比如前面那个例子,对于表(ID,k,R),有主键ID和二级索引k,我们要select * from T where k between 3 and 5
,那么会先在k的索引树中查3,再拿对应的ID去查ID的索引树中查R,再在k的索引树中查4,再去ID树查R,再在k索引树查5,没有了就停止了,返回数据集。
我们发现上面就回表了两次,显然是低效的
我们再举个例子,表和索引还是前面那样,但是查询语句是select ID from T where k between 3 and 5
,我们发现根据k查到的就是ID,直接就拿到数据了,不需要回表什么的,减少了树的搜索次数,显著提升了查询性能,这就是覆覆盖索引
覆盖索引就是select的数据列只用从索引中就能够取得,不必从数据表中读取,换句话说查询列要被所使用的索引覆盖
一般针对联合索引,如果筛选的字段不在联合索引,那么索引会失效
显然这是一种空间换时间的操作,对于高频请求,我们就可以建立这种联合索引
最左前缀原则
为了直观地说明这个概念,我们用(name,age)这个联合索引来分析。
可以看到,索引项是按照索引定义里面出现的字段顺序排序的。
当你的逻辑需求是查到所有名字是“张三”的人时,可以快速定位到 ID4,然后向后遍历得到所有需要的结果。
但是如果我们的查询条件语句是where name like ‘张 %’
时,其实也可以用到这个索引,会先查找到ID3,然后向后遍历
不只是索引的全部定义,只要满足最左前缀,就可以利用索引来加速检索。这个最左前缀可以是联合索引的最左 N 个字段,也可以是字符串索引的最左 M 个字符。
所以查询%com
和com%
的速度就不一样了,所以解决办法就是倒着插入域名数据moc.udiab.www
,这样查询的时候就是’com%’,这样就可以使用索引了,这也是一个在使用数据库时的小技巧。
因为可以支持最左前缀,所以当已经有了 (a,b) 这个联合索引后,一般就不需要单独在 a 上建立索引了。
因此,设置联合索引内字段顺序的第一原则是:如果通过调整顺序,可以少维护一个索引,那么这个顺序往往就是需要优先考虑采用的。
所以如果有这么一个需求:我们要为高频请求创建 (身份证号,姓名)这个联合索引,并用这个索引支持“根据身份证号查询地址”,我们此时根据最左前缀原则,可以直接用高频请求的(身份证号,姓名)联合索引来充当(身份证号)索引的作用。
但是如果有联合索引(a,b),我们现在要加快以b为条件的查询语句速度,我们就不得不维护(b)这个索引了,所以我们可以发现我们可以让字段小一点的字段作为b这个靠右的索引,这样即便多维护(b)这个索引,也比维护(a)在空间上更优
索引下推
结合图讲起来会好一点
select * from tuser where name like '张 %' and age=10 and ismale=1;
-
无索引下推执行流程(MySQL 5.6 之前)
先根据模糊查询查到ID3,然后只能一个一个遍历然后回表
-
索引下推执行流程(MySQL 5.6 开始)、
还是查到ID3,但是在联合索引这边就利用了age=10
这个条件过滤掉了ID3和ID6这两个age对不上的,只会回表两次