事务是数据库逻辑上的一组操作,一个事务中的一组操作,要么都执行,要么都不执行。
事务的四大特性(ACID)
Atomicity原子性:整个事务中的所有操作,要么全部完成,要么全部不完成,不可能停滞在中间某个环节。事务在执行过程中发生错误,会被回滚(Rollback)到事务开始前的状态,就像这个事务从来没有执行过一样。
Consistency一致性:整个事务执行前后数据库是保持一致的,保证数据库数据的完整性和正确性。
Isolation隔离性:各个并发事务之间不会互相干扰,多个并发事务之间互相隔离。(4个隔离级别)
Durability持久性:持久性是指一个事务一旦被提交了,那么对数据库中的数据的改变就是永久性的,即便是在数据库系统遇到故障的情况下也不会丢失提交事务的操作。
redo,undo,binlog二进制
事务日志
redolog 重做日志,用来保证事务的持久性,在Innodb存储引擎下,Insert,Delete,Update操作记录的都是redo物理日志,记录的是数据页的物理变化,主要用于数据库的崩溃恢复。 Redo日志可以分为两部分,一个是内存中存储的redo日志缓存(redo log buffer),是容易丢失的,一个是存储在本地磁盘的redo日志文件,是持久的。 redo日志整个产生流程:
-
第一步:先将原始数据从磁盘中读入内存中来,修改数据的内存拷贝
-
第二步:生成一条重做日志并写入redo log buffer,记录的是数据被修改后的值
-
第三步:当事务commit时,将redo log buffer中的内容刷新到 redo log file,对 redo log file采用追加写的方式
-
第四步:定期将内存中修改的数据刷新到磁盘中(innodb_flush_log_at_trx_commit设置策略0,1,2)
innodb_flush_log_at_trx_commit: 0: 每秒刷新到磁盘log(在mysql故障时可能会丢失最后1秒的数据) 1:每次提交都刷新日志到磁盘log(mysql默认) 2:每次提交都刷新到osbuffer(系统缓存),但每秒才刷新到磁盘log(在mysql故障时不影响,操作系统故障丢失最后1s数据)
undo log日志
undo主要记录的是数据的逻辑变化,为了在发生错误时回滚到之前的状态,需要把之前的操作记录下来。
undo日志,只将数据库逻辑地恢复到原来的样子,在回滚的时候,它实际上是做的相反的工作,比如一条INSERT ,对应一条 DELETE,对于每个UPDATE,对应一条相反的 UPDATE,将修改前的行放回去。undo日志用于事务的回滚操作进而保障了事务的原子性。
在InnoDB存储引擎中,undo存储在回滚段(Rollback Segment)中,每个回滚段记录了1024个undo log segment,而在每个undo log segment段中进行undo 页的申请,在5.6以前,Rollback Segment是在共享表空间里的,5.6.3之后,可通过 innodb_undo_tablespace设置undo存储的位置。
undo的类型 在InnoDB存储引擎中,undo log分为:
insert undo log
update undo log
insert undo log是指在insert 操作中产生的undo log,因为insert操作的记录,仅对事务本身可见,对其他事务不可见,故该undo log可以在事务提交完成后直接删除。
update undo log是指在update和delete操作中产生的undo log,该undo log可能需要提供mvcc机制(多版本并发控制),不能在事务提交后就删除undo log,等待purge线程进行最后的删除。(purge线程是指在Innodb存储引擎中,delete操作并不是直接删除数据,而是在要删除的数据上标识Delete_Bit,也就是平时所说的逻辑删除,purge线程会去清除带有Delete_Bit标识的数据)
undo日志并不是redo日志的逆过程,redo日志记录的是物理日志,是持久存在的,而undo日志是逻辑日志,对事物回滚时,只是把数据库恢复到之前的状态,每个undo的生命周期只是从事务开始到事务结束。
假设有A、B两个数据,值分别为1,2.
1. 事务开始
2. 记录A=1到undo log
3. 修改A=3
4. 记录A=3到 redo log
5. 记录B=2到 undo log
6. 修改B=4
7. 记录B=4到redo log
8. 将redo log写入磁盘
9. 事务提交
binlog二进制日志
二进制日志是在存储引擎之上的层面,redo和undo是Innodb引擎中的操作日志,而binlog二进制日志是在引擎之上,因此不管数据库采用Innodb默认存储引擎还是其他存储引擎,都会产生binlog。 虽然binlog和redolog都是记录对数据库的操作,但是两者却不一样: 1.binlog记录不管是什么引擎,都会记录对数据库的操作,而redolog记录的是InnoDB引擎下对表的操作,并且binlog先于redolog记录。 2.binlog在commit后一次性写入缓存中的日志文件,而redolog则在数据准备修改前写入redobufferlog缓存中,写入完成后才会执行数据修改操作,待事务完成后会刷新到持久性redolog磁盘文件中。 3.事务日志是记录的是物理页的变化,具有幂等性,记录方式比较简洁。比如在一个事务中进行了某行数据的添加,删除,又添加,最终事务日志记录的只是最后添加的记录,也就是物理页的变化。而二进制日志则会把这几次操作全部记录下来,记录比较多。
sync_binlog:sync_binlog 是 MySQL 的二进制日志(binlog)同步到磁盘的频率。MySQL server 在 binary log 每写入 sync_binlog 次后,刷写到磁盘。 如果 autocommit 开启,每个语句都写一次 binary log,否则每次事务写一次。默认值是 0,不主动同步,而依赖操作系统本身不定期把文件内容 flush 到磁盘。设为 1 最安全,在每个语句或事务后同步一次 binary log,即使在崩溃时也最多丢失一个语句或事务的日志,但因此也最慢。
大多数情况下,对数据的一致性并没有很严格的要求,所以并不会把 sync_binlog 配置成 1. 为了追求高并发,提升性能,可以设置为 100 或直接用 0. 而和 innodb_flush_log_at_trx_commit 一样,对于支付服务这样的应用,还是比较推荐 sync_binlog = 1.
事务的加锁方式
为了维护事务隔离性与一致性,一般数据库采用的加锁的方式。以Mysql为例,主要有表锁与行锁,当然现在也提供了MetaData元数据锁。由于数据库是一个高并发应用,如果加锁过度就会极大降低数据库的并发处理能力,所以这里分析一下InnoDB引擎数据库的加锁机制。
数据库遵循两段锁协议,将一个事务分为两个阶段,即加锁阶段与解锁阶段。
加锁阶段:表锁一般应用与数据库DDL操作,整张表加锁,加锁过程中不允许任何操作。由于表锁太过无解,所以又出现了行锁。行锁只作用于那一行数据,其他行的DML操作都不会受到影响。行锁又分为共享锁(S锁,其他事务可以继续加共享锁,不能加排它锁)与排它锁(X锁,其他事务不能加任何锁)。
共享锁:又称为读锁,其他事务可以一块读取这行数据,但是不能进行DML操作。
排它锁:又称为写锁,其他任何事务不能进行加锁操作,但是可以正常读取数据(读取结果为事务之前的数据),因为Mysql InnoDB引擎默认为update,insert,delete语句加上排他锁,select语句默认不加锁。(每句sql都是一个事务,可通过select lock in share mode为select加共享锁,通过select for update为select加排它锁)。
解锁阶段:事务commit提交后释放锁。
隔离级别
在数据库的并发操作中,为了保证数据的正确性,也就是维护事务的隔离性与一致性。这些数据库锁也正是为这些而存在的。

- 未提交读(Read Uncommited):允许脏读,也就是可以读取到其他未提交的事务修改的数据。
- 已提交读(Read Committed):只能读取到已提交的数据。(Oracle默认隔离级别)
- 可重复读(Repeatable Read):可重复读,在同一个事务中,查询到的数据都是事务开始时候的数据,InnoDB默认隔离级别,消除了不可重复读,但是会存在幻读。(当一个事务开始执行过程中(没有加锁),另外一个事务获取锁修改了这条数据并提交,那么这个事务读取到的数据还是最开始时候的数据。)
- 串行化(Serializable):完全串行化,获取表锁,读写都会阻塞。
通过 set session/global transaction isolation level +隔离级别设置隔离级别
不可重复读主要在于update与delete,而幻读主要在于insert。可重读通过在读取时候加锁,可实现可重复读。但是却无法锁住insert数据,当事务A先前读取了数据,或者修改了全部数据,事务B还是可以insert数据提交,这时事务A就会发现莫名其妙多了一条之前没有的数据,这就是幻读。幻读可以通过表锁来实现,也就是Serializable,但是会极大降低并发能力。
但是mysql是如何解决幻读问题的呢?
下面就了解一下乐观锁和悲观锁。
悲观锁:正如其名,它指的是对数据被外界(包括本系统当前的其他事务,以及来自外部系统的事务处理)修改持保守态度,因此,在整个数据处理过程中,将数据处于锁定状态。悲观锁的实现,往往依靠数据库提供的锁机制(也只有数据库层提供的锁机制才能真正保证数据访问的排他性,否则,即使在本系统中实现了加锁机制,也无法保证外部系统不会修改数据)。在悲观锁的情况下,为了保证事务的隔离性,就需要一致性锁定读。读取数据时给加锁,其它事务无法修改这些数据,类似select... for update这样的语句。修改删除数据时也要加锁,其它事务无法读取这些数据。
乐观锁:相对悲观锁而言,乐观锁机制采取了更加宽松的加锁机制来解决事务的隔离性。悲观锁大多数情况下依靠数据库的锁机制实现,以保证操作最大程度的独占性。但随之而来的就是数据库性能的大量开销,特别是对长事务而言,这样的开销往往无法承受。 乐观锁,使用CAS实现,而CAS算法大多是基于数据版本( Version )记录机制实现。即在表中添加一个version字段,读取出数据时,将此版本号一同读出,之后更新时,对此版本号加一。此时,将提交数据的版本数据与数据库表对应记录的当前版本信息进行比对,如果提交的数据版本号大于数据库表当前版本号,则予以更新,否则认为是过期数据。
目前成熟的数据库如Mysql,Oracle都是使用了以乐观锁为理论基础的MVCC(多版本并发控制)来避免幻读和不可重复读。
源码解析
Spring支持两种的事务使用:编程式事务与声明式事务。
编程式事务:通过在业务代码中对事务做手动回滚,对代码入侵性强,不推荐使用.(TransactionAspectSupport,TransactionTemplate)
声明式事务:使用@Transactional注解。
Spring事务中,主要包含TransactionDefinition,TransactionStatus,PlatformTransactionManager,所谓的事务管理,其实就是"按照给定的事务规则执行事务的提交或回滚操作",TransactionDefination就表示给定的事务规则,TransactionStatus表示运行着的事务状态,PlatformTransactionManager用来执行事务操作。
TransactionDefinition
TransactionDefinition用于定义一个事务,包括事务的传播熟悉,隔离级别,超时时间,只读等属性,默认使用DefaultTransactionDefinition,也支持自定义配置。
PlatformTransactionManager
PlatformTransactionManager用于事务的执行操作,接口定义如下:
public interface PlatformTransactionManager {
TransactionStatus getTransaction(TransactionDefinition definition) throws TransactionException;
void commit(TransactionStatus status) throws TransactionException;
void rollback(TransactionStatus status) throws TransactionException;
}
根据底层所使用的不同的持久化 API 或框架,PlatformTransactionManager 的主要实现类大致如下:
-
DataSourceTransactionManager:适用于使用JDBC和iBatis进行数据持久化操作的情况。
-
HibernateTransactionManager:适用于使用Hibernate进行数据持久化操作的情况。
-
JpaTransactionManager:适用于使用JPA进行数据持久化操作的情况。
-
另外还有JtaTransactionManager JdoTransactionManager、JmsTransactionManager等等。
TransactionStatus
TransactionStatus表示事务的状态,通过PlatformTransactionManager.getTransaction()获取,TransactionStatus接口提供了一个简单的事务控制和事务查询的方法。
public interface TransactionStatus{
boolean isNewTransaction();
void setRollbackOnly();
boolean isRollbackOnly();
}
声明式事务主要是通过Spring AOP实现的,对使用@Transactional注解的方法进行拦截,AOP通过代理方式执行目标方法,


class TransactionInfo{
private final PlatformTransactionManager transactionManager;
private final TransactionAttribute transactionAttribute;
private final String joinpointIdentification;
private TransactionStatus transactionStatus;
private TransactionInfo oldTransactionInfo;
}

