文章目录
事务是我们大部分程序员绕不过去的坎,事务到底是什么?事务对我们有什么好处?mysql的事务和spring的@Transaction是怎么个关系?
什么是事务?
事务(Transaction)是由一系列对系统中数据进行访问与更新的操作所组成的一个程序执行逻辑单元(Unit)。
狭义上的事务特指数据库事务。
—— 从Paxos到Zookeeper分布式一致性原理与实践 (倪超 著)
实际上,笔者通俗的认为,一个事务就是一笔严谨公正、不能被外人影响的交易
,无论交易成功失败,被交易的资源
都应该保持令人信服的事实状态。
而当这笔被交易的资源
是计算机数据库中的数据时,我们就还要保证由于不可抗力计算机宕机
导致的资源
的不丢失。
于是根据定义,衍生出来的事务应该有以下四个特征,也就是我们常说的ACID:
A : Atomic(原子性)
事务要么成功,要么失败,没有中间态。
但是笔者认为,这里的A相比于java或是计算机中锁的原子性概念来说,要稍弱一些。锁的原子性是一个不可切分的最小的操作,不会受到并发的影响。
而数据库事务的A并没有防止并发可能带来问题。
C:Consistent(一致性)
笔者认为数据库这里的的C应当与分布式系统中的数据一致性/最终一致性区分开来,即:数据库事务一致性不是为了保证某两个节点间的数据一致。
事务应确保数据库的状态从一个一致状态转变为另一个一致状态。一致状态的含义是数据库中的数据应满足完整性约束. ——wiki百科
而所谓应满足完整性约束
,则指的是数据库中的对于数据的约束,比如:
- 非空约束,设置not null的字段不能为空。
- 唯一约束,设置unique的字段必须唯一。
等等。要让数据在事务前和事务后,都保证满足以上所描述的此类约束,就满足了一致性的要求。
记得见过这样一个例子:
假设一个事务中,先删除了主键id=1的数据,然后新增了一条id=1的数据,之后由于一些原因导致事务回滚,但是只回滚了删除操作。
结果导致数据库存在两条id=1的记录,违反了主键唯一的性质。
I : Isolation(隔离性)
又是一个常见的考点,看到隔离性,我们就应该将四个隔离级别脱口而出。但是在此之前,让我们先明白为什么需要隔离性?
回忆A(原子性)遗留的问题,我们能够轻易想到,隔离性+原子性最终为了保证数据的事务前后一致性
,防止出现并发的数据错误。
四种隔离级别如下:
-
Read Uncommited(读未提交)
等同于无事务,任何同时进行的事务都可以看到其他未提交事务的inset/update/delete后的数据。
缺点:一切并发问题——脏读、写覆盖、破坏前后数据一致性。 -
Read Commited(读已提交)
大部分数据库(除Mysql)默认的隔离级别。wiki对它的定义特别清晰。在提交读(READ COMMITTED)级别中,基于锁机制并发控制的DBMS需要对选定对象的写锁一直保持到事务结束,但是读锁在SELECT操作完成后马上释放(因此“不可重复读”现象可能会发生)。和
可重复读
隔离级别一样,也不要求“范围锁”。 ——wiki百科通过定义,我们会发现,当前事务select的数据会随着其他事务的提交而改变(即
不可重复读
),这是该隔离级别的一个特性,如果使用Read Commited,要格外注意该特性,避免跳坑。 -
REPEATABLE READS(可重复读,mysql默认级别)
看名字就能知道,我们是在Read Commited上加了一些限制,使得可以在当前事务中多次select都能得到一个结果(本来应该是这样的)。在可重复读(REPEATABLE READS)隔离级别中,基于锁机制并发控制的DBMS需要对选定对象的读锁(read locks)和写锁(write locks)一直保持到事务结束,但不要求“范围锁”,因此可能会发生“幻影读”。 ——wiki百科
实际上,mysql可重复读的实现与wiki描述不同,并非对选定对象加读锁,而是使用MVCC进行了
数据的版本控制
。举个例子:在A事务中对id=2的数据进行查询,如果想保证在之后的查询中该行都不变化,有两个方法:
对该数据行加锁,其他事务不可修改
,对该数据多版本控制,本事务每次都只查看对应版本的数据
。第二种方式,就是MVCC做的事情,其实它的原理很简单,就是对增删改的操作进行版本记录,相当于在一行数据后面加了个字段(版本号)。
缺点:幻读——可重复读虽然可重复select
==指定行
的数据,但是对于范围指定
的select缺没有限制。比如事务A中的两次范围查询,很可能第一次查出来10条数据,第二次查出来11条。根据定义,我们明白,这个缺点产生的原因是可重复读不要求范围锁。
但是mysql的可重复读级别不存在幻读问题,它同样通过MVCC解决了该问题。 -
SERIALIZABLE(串行化)
串行化将所有事务串行处理,这样显然不会存在任何并发问题,但是效率也将响应的变低。
对于我们使用来说,Mysql的REPEATABLE READS已经解决了可预料的所有并发的问题,并且效率也并不比Read Commited低,所以一般来说,不需要更换隔离级别。
Durability 持久性
事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失。 ——wiki百科
持久性是一个有意思的问题,即使系统故障也不会丢,那就只能通过高可用,主从节点间的强一致性来保证,即使这样也存在主从全部一起挂掉的风险。笔者认为,这个特性还是需要弹性考虑的。
使用事务有什么好处?
通过上面的叙述,相信同学们已经有了一些想法,笔者自己做了一些思考,不一定对:
面向对象编程让我们化万物为对象,关系型数据库让我们将万物对象
在计算机中连接起来,而这些万物对象
随着时间推移、万物对象
自身的一些行为方法会产生的一些规范的流程
、我们所认为的有规则有道理的流程,笔者认为事务正是帮助我们描述了这些规范的流程
,让我们不需要关注数据在计算机中产生的一些问题,可以更好的只关注于做事
。
Spring中的@Transaction
Spring中的@Transaction是我们常见常用的一个注解,它是什么?能做什么?我们真的了解吗。
@Transaction是什么?
@Transaction将标注的方法纳入事务管理,即:加上@Transaction注解就可以对该方法自动使用事务了。
一般情况下,基于Mysql数据源的程序中,@Transaction所使用的的就是我们上面所讲述的Mysql的事务。
但是有一点我们需要注意,程序的方法不是只有一个,是存在方法调用
的。所以对于事务的使用就不得不先了解一下事务的传播机制了。
@Transaction传播机制
@Transaction提供了七种传播机制,我们可以通过这些传播机制来规定事务在方法调用
中的传播行为。
笔者曾经写过一个bug,回过头来看很蠢,但是就属于很典型的完全没整明白传播机制的问题。当时的代码逻辑是这样的:
A方法开启了REQUIRED传播,B方法也是默认的REQUIRED传播,A方法中调用了B,(两者属于不同Class,可以确认@Transaction注解是生效的)。
在B方法中对某Exception进行抛出,A中捕获了该异常,但是没有抛出,而是打印日志就结束。
结果执行报错:Transaction rolled back because it has been marked as rollback-only
原理其实很简单:由于B的传播级别为REQUIRED,所以B方法还是使用A传递过来的事务。
当B事务异常时事务被Spring标记为Rollback-only表示该事务只能回滚了,结果A在捕获到异常后没有让它回滚或继续抛出让Spring来处理回滚。
当A执行完方法,想要正常commit的时候,悲剧就发生了。
这个问题有两种解决方法,一是在A的catch中的抛出异常或rollback;另一种则是修改B方法的隔离级别,让A和B分别使用不同的事务。
当时我选择了B(设置B的传播机制为REQUIRED_NEW),觉得更贴近逻辑。
不扯了,进入正题,七种事务传播机制,前三种比较重要:
-
REQUIRED。
默认。该隔离级别会开启一个新事务,但如果是被调用的方法(或者说若当前已存在一个事务),会直接使用传递过来的事务。 -
REQUIRED_NEW。
该隔离级别会开启一个新事务,如果是被调用的方法,会新开启一个事务。 -
NESTED(嵌套)。
这个有点儿东西,它的基本性质同REQUIRED_NEW,但是实现原理不同,REQUIRED_NEW是在spring级别又开启了一个新事务,而NESTED则是mysql级别的嵌套事务
。展开唠一下,Mysql中的事务其实是分多钟的,有扁平事务、savepoint事务、嵌套事务、分布式事务等。
Mysql事务的分类
扁平事务:即一般事务。 savepoint事务:带保存点的事务,我们可以在某个地方设置savepoint,然后再之后通过跳转函数跳转回该点 ,将数据恢复到该状态。 分布式事务:mysql这里一般指XA事务,是基于2PC协议的,以后再聊数据一致性协议。 嵌套事务:事务可以开启子事务,子事务还可以再分子事务,子事务的回滚不一定导致父事务回滚,但是父事务回滚一定导致子事务回滚。
说到这里,相信大家也看得出NESTED和REQUIRED_NEW的区别的REQUIRED_NEW开启的子事务与父事务没关系,父子谁回滚都不影响对方,但是NESTED里面父事务回滚子事务也是要回滚的。
其他的就是一些是否支持事务的传播级别了。
-
NOT_SUPPORTED 不支持事务并挂起任何存在的事务。
-
SUPPORTS 支持事务但不一定非要有。
-
NEVER 存在事务就抛异常。
-
MANDATORY 必须有事务,否则抛异常。