1. 事务
数据库事务指的是一组数据操作,事务内的操作要么就是全部成功,要么就是全部失败。
例如在转账的流程下,张三给李四转账 2000,第一步在账单账户下扣除 2000,第二步在李四账户下增加 2000,这两步可以视为一个事务。如果两步都成功则转账成功,如果其中任意一步失败,则撤回转账操作
四个特征(ACID)
事务具有四个特征:原子性( Atomicity )、一致性( Consistency )、隔离性( Isolation )和持续性( Durability )。这四个特性简称为 ACID 特性。
原子性
事务是数据库的逻辑工作单位,事务中包含的各操作要么都做,要么都不做
一致性
事务执行的结果必须是使数据库从一个一致性状态变到另一个一致性状态。因此当数据库只包含成功事务提交的结果时,就说数据库处于一致性状态。如果数据库系统运行中发生故障,有些事务尚未完成就被迫中断,这些未完成事务对数据库所做的修改有一部分已写入物理数据库,这时数据库就处于一种不正确的状态,或者说是不一致的状态。
隔离性
一个事务的执行不能被其它事务干扰。即一个事务内部的操作及使用的数据对其它并发事务是隔离的,并发执行的各个事务之间不能互相干扰。
持续性
也称永久性,指一个事务一旦提交,它对数据库中的数据的改变就应该是永久性的。接下来的其它操作或故障不应该对其执行结果有任何影响。
事务并发可能出现的问题
对于并发事务,由于处理的是同一个数据,就有可能出现问题。
脏读(Drity Read)
某个事务已更新一份数据,另一个事务在此时读取了同一份数据,由于某些原因,前一个RollBack了操作,则后一个事务所读取的数据就会是不正确的。
不可重复读(Non-repeatable read)
在一个事务的两次查询之中数据不一致,这可能是两次查询过程中间插入了一个事务更新了原有的数据。
幻读(Phantom Read)
在一个事务的两次查询之中,就算有其它事务更新了数据,两次查询出来的结果也是一样的。 只有在下一次查询事务再次读取数据时,才能读取更新事务更新后的数据。
事务隔离级别
为什么要有隔离级别,主要为了防止在事务并发中出现的一系列问题。
SQL标准定义了4类隔离级别,包括了一些具体规则,用来限定事务内外的哪些改变是可见的,哪些是不可见的。低级别的隔离级一般支持更高的并发处理,并拥有更低的系统开销。
Read Uncommitted(读取未提交内容)
在该隔离级别,所有事务都可以看到其他未提交事务的执行结果。本隔离级别很少用于实际应用,因为它的性能也不比其他级别好多少。读取未提交的数据,也被称之为脏读(Dirty Read)。
Read Committed(读取提交内容)
这是大多数数据库系统的默认隔离级别(但不是MySQL默认的)。它满足了隔离的简单定义:一个事务只能看见已经提交事务所做的改变。这种隔离级别会导致所谓的不可重复读(Nonrepeatable Read),因为同一事务的其他实例在该实例处理其间可能会有新的commit,所以同一select可能返回不同结果。
Repeatable Read(可重读)
这是MySQL的默认事务隔离级别,它确保同一事务的多个实例在并发读取数据时,会看到同样的数据行。不过理论上,这会导致另一个棘手的问题:幻读 (Phantom Read)。简单的说,幻读指当用户读取某一范围的数据行时,另一个事务又在该范围内插入了新行,当用户再读取该范围的数据行时,会发现有新的“幻影” 行。InnoDB和Falcon存储引擎通过多版本并发控制(MVCC,Multiversion Concurrency Control)机制解决了该问题。
Serializable(可串行化)
这是最高的隔离级别,它通过强制事务排序,使之不可能相互冲突,从而解决幻读问题。简言之,它是在每个读的数据行上加上共享锁。在这个级别,可能导致大量的超时现象和锁竞争。
在MySQL中,实现了这四种隔离级别,分别有可能产生问题如下所示:
2. Mysql单机事务实现原理
Mysql在实现事务方面主要使用了WAL机制和多版本并发控制机制。
WAL机制
WAL: Write-Ahead Logging,先写日志,再写磁盘。
具体说,当有一条记录需要更新的时候,InnoDB引擎就会先把记录写到redo log里面,并更新内存,这个时候更新计算完成了。同时InnoDB引擎会在在系统比较空闲的时候,将这个操作记录更新到磁盘里。只要redo log和binlog保证持久化到磁盘,就能确保MySQL异常重启后,数据可以恢复。
- WAL机制主要得益于两个方面:
- redo log 和 binlog 都是顺序写,磁盘的顺序写比随机写速度要快;组提交机制,可以大幅度降低磁盘的 IOPS 消耗。
事务执行的各个阶段
(1)写undo日志到log buffer;
(2)执行事务,并写redo日志到log buffer;
(3)如果innodb_flush_log_at_trx_commit=1,则将redo日志写到log file,并刷新落盘。
(4)提交事务。
可能有同学会问,为什么没有写data file,事务就提交了?
因为data buffer中的数据会在合适的时间,由存储引擎写入到data file,如果在写入之前,数据库宕机了,根据落盘的redo日志,完全可以将事务更改的数据恢复。
多版本并发控制(MVCC)
Mysql大部分事务型存储引擎并不是简单的行级锁。基于提升并发行的考虑,它们一般都同时实现了多版本并发控制 MVCC。
InnoDB的MVCC,是通过在每行记录后面保存两个隐藏的列来实现的。这两个列一个保存了行的创建时间,一个保存行的过期时间(或删除时间),当然存储的并不是真正的时间,而是系统版本号。每开始一个事务,系统版本号就会自动递增,事务开始时刻的版本号作为当前事务的版本号,用来和查询到的每行记录的版本号进行比较。
以下是 REPEATABLE READ的隔离级别下具体操作:
- SELECT InnoDB 会根据以下两个条件检查每行记录: a. InnoDB 只查询版本早于当前事务版本的数据行(也就是,行的系统版本号小于或等于事务的系统版号),这样可以确保事务读取的行,要么是在事务开始前的已经存在的,要么是事务自身插入或者修改过的。 b. 行的删除版本要么未定义,要么大于当前事务版本号。这可以确保事务读取到的行,在事务开始之前未被删除。 只有符合上述两个条件的记录,才能返回作为查询结果
- INSERT InnoDB 为新插入的每一行保存当前系统版本号作为行版本号
- DELETE InnoDB 为删除的每一行保存当前系统版本号作为行删除标识
- UPDATE InnoDB 为插入一行新记录,保存当前系统版本号作为行版本号,同时保存当前系统版本号到原来的行作为行删除标识
- 保存着两个额外的系统版本号,使大多数读操作都可以不用加锁。这样设计使得读数据操作很简单,性能很好,并且也能保证只会读取到符合标准的行。
autocommit设置
MySQL中autocommit设置
MySQL默认是开启自动提交的,即每一条DML(增删改)语句都会被作为一个单独的事务进行隐式提交。如果修改为关闭状态,则执行DML语句之后要手动提交才能生效。
Spring中对自动提交的控制
MySQL的JDBC驱动包 mysql-connector-java 会给会话的connection默认开启自动提交。
常用的数据库连接池,如HikariCP,druid等,默认也是开启自动提交,会将connection的自动提交设置都改为true。
druid在初始化DataSource的时候设置connection的autocommit为true。
HikariCP 初始化DataSource的默认配置 中autocommit也是true。
对于事务管理器PlatformTransactionManager管理的显式事务(譬如@Transactional注解声明)在开启事务时会关闭自动提交模式。
3. 分布式事务
MySQL XA方案
MySQL从5.7开始加入了分布式事务的支持。MySQL XA中拥有两种角色:
RM(Resource Manager):用于直接执行本地事务的提交和回滚。在分布式集群中,一台MySQL服务器就是一个RM。
TM(Transaction Manager):TM是分布式事务的核心管理者。事务管理器与每个RM进行通信,协调并完成分布式事务的处理。发起一个分布式事务的MySQL客户端就是一个TM。
XA的两阶段提交分为Prepare阶段和Commit阶段,过程如下:
1. 阶段一为准备(prepare)阶段。即所有的RM锁住需要的资源,在本地执行这个事务(执行sql,写redo/undo log等),但不提交,然后向Transaction Manager报告已准备就绪。
2. 阶段二为提交阶段(commit)。当Transaction Manager确认所有参与者都ready后,向所有参与者发送commit命令。
如下图所示:
MySQL XA拥有严重的性能问题。一个数据库的事务和多个数据库间的XA事务性能对比可发现,性能差10倍左右。另外,XA过程中会长时间的占用资源(加锁)直到两阶段提交完成才释放资源。
Seata
Seata的分布式事务解决方案是业务层面的解决方案,只依赖于单台数据库的事务能力。Seata框架中一个分布式事务包含3中角色:
- Transaction Coordinator (TC): 事务协调器,维护全局事务的运行状态,负责协调并驱动全局事务的提交或回滚。
- Transaction Manager ™: 控制全局事务的边界,负责开启一个全局事务,并最终发起全局提交或全局回滚的决议。
- Resource Manager (RM): 控制分支事务,负责分支注册、状态汇报,并接收事务协调器的指令,驱动分支(本地)事务的提交和回滚。
其中,TM是一个分布式事务的发起者和终结者,TC负责维护分布式事务的运行状态,而RM则负责本地事务的运行。如下图所示:
1.Business主业务通过TM通知TC开启全局事务。
2.Businiess调用的每个分支事务(比如Storage,order,account)需要注册到TC中,并时时汇报状态。
3.通过RM实时汇报事务状态,TC掌握每个分支事务的状态。
4.因为TC实时掌握每个分支事务,一旦某个事务失败,TC可以命令所有分支事务回滚。
可以看出TC就是seata抽离出的一层,用来维护全局和分支事务的状态(这种抽离出一层的方案,分布式组件中到处可见),此图中TC协调三个远程的事务(Storage,Order,Account),Business是主业务代码(TM),开启全局事务,控制总事务,RM资源管理应用在每一个服务中,负责和数据库交互,每一个服务里面对于本地事务的管理通过RM。
如果分支事务发现失败,默认情况下seata采用AT模式,每个服务的事务维系一个UNDO_LOG (回滚日志表)表,每个分支事务都是已提交的,TC通过每个事务的回滚表将之反向补偿,这整个过程都是自动回滚的。