有段时间没更新博客了,最近在学习一些有关事务的知识点,今天来总结一下,本文会涉及到如下几个知识点:mysql
MySQL事务
Spring事务
分布式事务
事务是一系列操做组成的工做单元,该工做单元内的操做是不可分割的,即要么全部操做都作,要么全部操做都不作,这就是事务。
举个例子:算法
张三要给李四转帐100元,那么咱们会有这样的一段SQL:spring
begin transaction; update account set money = money-100 where name = '张三'; update account set money = money+100 where name = '李四'; commit transaction;
事务的体现:这两个SQL要么所有成功,要么所有失败。sql
事务可否生效数据库引擎是否支持事务是关键。好比经常使用的 MySQL 数据库默认使用支持事务的innodb
引擎。可是,若是把数据库引擎变为 myisam
,那么程序也就再也不支持事务了!数据库
事务具备如下四个特性:编程
原子性
一致性
隔离性
持久性
通常来讲,原子是指不能分解成小部分的东西。例如,在多线程编程中,若是一个线程执行一个原子操做,这意味着另外一个线程没法看到该操做的一半结果。系统只能处于操做以前或操做以后的状态,而不是介于二者之间的状态。服务器
事务一致性是指数据库中的数据在事务操做先后都必须知足业务规则约束。
好比A转帐给B,那么转帐先后,AB的帐户总金额应该是一致的。网络
一个事务的执行不能被其它事务干扰。即一个事务内部的操做及使用的数据对其它并发事务是隔离的,并发执行的各个事务之间不能互相干扰。
(设置不一样的隔离级别,互相干扰的程度会不一样)多线程
事务一旦提交,结果即是永久性的。即便发生宕机,仍然能够依靠事务日志完成数据的持久化。并发
日志包括回滚日志(undo)和重作日志(redo),当咱们经过事务修改数据时,首先会将数据库变化的信息记录到重作日志中,而后再对数据库中的数据进行修改。这样即便数据库系统发生奔溃,咱们还能够经过重作日志进行数据恢复。
MySQL有如下四个事务隔离级别:
未提交读(READ UNCOMMITTED)
已提交读(READ COMMITTED)
可重复读(REPEATABLE READ)
串行化(SERIALIZABLE)
各个隔离级别可能会存在如下的问题:
那么什么是脏读、不可重复读和幻读?
脏读:指一个事务能够看到另外一个事务未提交的数据
好比说事务A修改了一个值可是还未提交,这时事务B能够看到A修改的值,这就是脏读。
不可重复读:一个事务执行两次一样的查询语句,先后得出的数据却不一致
好比说事务A执行了select
语句,事务B修改了某个值,事务A再次执行select
语句时发现结果和上次不一致,所以叫作不可重复读。
幻读:在同一个事务中,同一个查询屡次返回的记录行数不一致(这里的结果特指查询到的记录行数,幻读能够看作不可重复读的一种特殊状况)
好比说事务A执行了select
语句,事务B插入数据,事务A再次执行select
语句时发现多了几条记录,好像出现了幻觉同样,所以叫作幻读。
先说结论:经过改变锁的释放时机来解决脏读问题。
首先先了解一下为何会出现脏读?缘由就是在未提交读
这个级别下,当事务A修改了数据以后就立马释放了锁,所以事务B能够读取到这个未提交的数据。
在已提交读
级别下写操做加的锁会到事务提交后释放,因此事务B不会读到事务A未提交的数据,经过改变锁的释放时机解决了脏读的问题。
结论:可重复读
级别就是经过MVCC
机制来解决不可重复读问题的
多版本并发控制(Multi-Version Concurrency Control, MVCC)是 MySQL 的 InnoDB 存储引擎实现隔离级别的一种具体方式,用于实现提交读和可重复读这两种隔离级别。而未提交读隔离级别老是读取最新的数据行,无需使用 MVCC。可串行化隔离级别须要对全部读取的行都加锁,单纯使用 MVCC 没法实现。
MVCC机制(多版本并发控制)
就我我的理解来讲其实就是给每行数据都添加了几个隐藏字段,用来表示数据的版本号,即一个数据在mysql中会有多个不一样的版本。
在讲 MVCC 的实现原理以前,我觉颇有必要先去了解一下 MVCC 的两种读形式。
有了MVCC以后咱们能够把SQL操做分为两类:
读取当前事务可见的数据,默认的select
操做就是快照读,读的是历史版本的数据。
读取最新的数据,除了默认select操做外的select..for update
、update
、insert
、delete
等操做都是当前读,读取的都是最新的数据。
如今咱们有了MVCC,当事务A执行一个普通的select操做(快照读)
,MySQL会把此次读取的数据保存起来,在这期间无论事务B执行update或是insert操做,事务A再次执行select操做读取到的数据是不会变的,所以经过可重复读级别经过MVCC解决了不可重复读问题,顺便解决了部分的幻读问题,没错MVCC并无解决全部的幻读问题,只是解决了一部分。
当事务A执行的是当前读,也就是加锁的select操做时如select * from Employee for update
,会去读取最新的数据,这样的话仍是能够看到事务B提交的数据,所以MySQL提供了Next-Key Lock
算法来帮助咱们对数据加锁。
InnoDB有三种行锁的算法:
3. Next-Key Lock:1+2,锁定一个范围,而且锁定记录自己。对于行的查询,都是采用该方法,主要目的是解决幻读的问题。
Next-Key Lock 是 MySQL 的 InnoDB 存储引擎的一种锁实现。
MVCC 不能解决幻读的问题,Next-Key Lock 就是为了解决这个问题而存在的。在可重复读(REPEATABLE READ)隔离级别下,使用 MVCC + Next-Key Lock
能够解决幻读问题。
当查询的索引含有惟一属性的时候,Next-Key Lock
会进行优化,将其降级为Record Lock
,即仅锁住索引自己,不是范围。
它是 Record Lock
和 Gap Lock
的结合,不只锁定一个记录上的索引,也锁定索引之间的间隙。
这样的话当事务A执行了select * from Employee for update
以后,事务B插入数据会被阻塞,这样的话Repeatable Read(可重复读)
级别使用 MVCC + Next-Key Lock
能够解决了不可重复读和幻读的问题。
注意:Repeatable Read(可重复读)是MySQL的默认隔离级别。
这是最高的隔离级别,它经过强制事务排序,使之不可能相互冲突,从而解决幻读问题。简言之,它是在每一个读的数据行上加上共享锁。在这个级别,可能致使大量的超时现象和锁竞争。
在项目开发中咱们有时候会遇到Spring事务失效的场景,那么什么场景会致使事务失效呢?
这种失效是因为配置错误,如果错误的配置如下三种 propagation,事务将不会发生回滚。
TransactionDefinition.PROPAGATION_SUPPORTS:若是当前存在事务,则加入该事务;若是当前没有事务,则以非事务的方式继续运行。
TransactionDefinition.PROPAGATION_NOT_SUPPORTED:以非事务方式运行,若是当前存在事务,则把当前事务挂起。
TransactionDefinition.PROPAGATION_NEVER:以非事务方式运行,若是当前存在事务,则抛出异常。
如下来自 Spring 官方文档:
When using proxies, you should apply the @Transactional annotation only to methods with public visibility. If you do annotate protected, private or package-visible methods with the @Transactional annotation, no error is raised, but the annotated method does not exhibit the configured transactional settings. Consider the use of AspectJ (see below) if you need to annotate non-public methods.
大概意思就是 @Transactional
只能用于 public 的方法上,不然事务不会失效,若是要用在非 public 方法上,能够开启 AspectJ
代理模式。
rollbackFor 能够指定可以触发事务回滚的异常类型。Spring默认抛出了未检查unchecked异常(继承自 RuntimeException 的异常)或者 Error才回滚事务;其余异常不会触发回滚事务。若是在事务中抛出其余类型的异常,但却指望 Spring 可以回滚事务,就须要指定 rollbackFor属性。
// 但愿自定义的异常能够进行回滚 @Transactional(propagation= Propagation.REQUIRED,rollbackFor= MyException.class
来看两个示例:
@Service public class OrderServiceImpl implements OrderService { public void update(Order order) { updateOrder(order); } @Transactional public void updateOrder(Order order) { // update order; } }
update方法上面没有加 @Transactional
注解,调用有 @Transactional
注解的 updateOrder 方法,updateOrder 方法上的事务管用吗?
@Service public class OrderServiceImpl implements OrderService { @Transactional public void update(Order order) { updateOrder(order); } @Transactional(propagation = Propagation.REQUIRES_NEW) public void updateOrder(Order order) { // update order; } }
此次在 update 方法上加了 @Transactional
,updateOrder 加了 REQUIRES_NEW
新开启一个事务,那么新开的事务管用么?
这两个例子的答案是:无论用!
由于@Transactional
注解底层实际上是Spring帮咱们生成了一个代理对象,当其它对象调用带有@Transactional
的方法时,其实调的是代理对象,Spring会在代理对象中帮咱们加上一系列的事务操做。
在上面的例子中它们发生了自身调用,就调用该类本身的方法,而没有通过 Spring 的代理类,默认只有在外部调用事务才会生效,这也是老生常谈的经典问题了。
这种状况是最多见的一种@Transactional注解失效场景
@Autowired private B b; @Service public class OrderServiceImpl implements OrderService { @Transactional public void A(Order order) { try { b.insert(); }catch (Exception e){ //do something; } } }
若是b.insert()方法内部抛了异常,而A方法此时try catch了B方法的异常,那这个事务还能正常回滚吗?
答案:不能!而是会抛出下面异常:
org.springframework.transaction.UnexpectedRollbackException: Transaction rolled back because it has been marked as rollback-only
由于当ServiceB中抛出了一个异常之后,ServiceB标识当前事务须要rollback。可是ServiceA中因为你手动的捕获这个异常并进行处理,ServiceA认为当前事务应该正常commit。此时就出现了先后不一致,也就是由于这样,抛出了前面的UnexpectedRollbackException异常。
spring的事务是在调用业务方法以前开始的,业务方法执行完毕以后才执行commit or rollback,事务是否执行取决因而否抛出Runtime异常。若是抛出runtime exception 并在你的业务方法中没有catch到的话,事务会回滚。
在业务方法中通常不须要catch异常,若是非要catch必定要抛出throw new RuntimeException()
,或者注解中指定抛异常类型@Transactional(rollbackFor=Exception.class)
,不然会致使事务失效,数据commit形成数据不一致,因此有些时候try catch反倒会多此一举。
首先看下什么是分布式事务:
分布式事务就是指事务的参与者、支持事务的服务器、资源服务器以及事务管理器分别位于不一样的分布式系统的不一样节点之上。简单的说,就是一次大的操做由不一样的小操做组成,这些小的操做分布在不一样的服务器上,且属于不一样的应用,分布式事务须要保证这些小操做要么所有成功,要么所有失败。本质上来讲,分布式事务就是为了保证不一样数据库的数据一致性。
那么为何须要分布式事务?直接用Spring提供的@Transaction注解
不行吗?
这里极其重要的一点:单块系统是运行在同一个 JVM 进程中的,可是分布式系统中的各个系统运行在各自的 JVM 进程中。所以你直接加@Transactional
注解是不行的,由于它只能控制同一个 JVM 进程中的事务,可是对于这种跨多个 JVM 进程的事务无能无力。
咱们来解释一下这个方案的大概流程:
适用场景: 这个方案的使用仍是比较广,目前国内互联网公司大都是基于这种思路玩儿的。
整个流程图以下所示:
这个方案的大体流程:
这套方案和上面的可靠消息最终一致性方案的区别:
可靠消息最终一致性方案能够保证的是只要系统 A 的事务完成,经过不停(无限次)重试来保证系统 B 的事务总会完成。
可是最大努力方案就不一样,若是系统 B 本地事务执行失败了,那么它会重试 N 次后就再也不重试,系统 B 的本地事务可能就不会完成了。
至于你想控制它究竟有“多努力”,这个须要结合本身的业务来配置。
好比对于电商系统,在下完订单后发短信通知用户下单成功的业务场景中,下单正常完成,可是到了发短信的这个环节因为短信服务暂时有点问题,致使重试了 3 次仍是失败。
那么此时就再也不尝试发送短信,由于在这个场景中咱们认为 3 次就已经算是尽了“最大努力”了。
简单总结:就是在指定的重试次数内,若是能执行成功那么皆大欢喜,若是超过了最大重试次数就放弃,再也不进行重试。
适用场景: 通常用在不过重要的业务操做中,就是那种完成的话是锦上添花,但失败的话对我也没有什么坏影响的场景。
好比上边提到的电商中的部分通知短信,就比较适合使用这种最大努力通知方案来作分布式事务的保证。
TCC 的全称是:
这个实际上是用到了补偿的概念,分为了三个阶段:
仍是给你们举个例子:
好比跨银行转帐的时候,要涉及到两个银行的分布式事务,若是用 TCC 方案来实现,思路是这样的:
适用场景:这种方案说实话几乎不多有人使用,可是也有使用的场景。
由于这个事务回滚其实是严重依赖于你本身写代码来回滚和补偿了,会形成补偿代码巨大,很是之恶心。
好比说咱们,通常来讲跟钱相关的,跟钱打交道的,支付、交易相关的场景,咱们会用 TCC,严格保证分布式事务要么所有成功,要么所有自动回滚,严格保证资金的正确性,在资金上不容许出现问题。
比较适合的场景:除非你是真的一致性要求过高,是你系统中核心之核心的场景,好比常见的就是资金类的场景,那你能够用 TCC 方案了。 你须要本身编写大量的业务逻辑,本身判断一个事务中的各个环节是否 ok,不 ok 就执行补偿/回滚代码。
并且最好是你的各个业务执行的时间都比较短。
可是说实话,通常尽可能别这么搞,本身手写回滚逻辑,或者是补偿逻辑,实在太恶心了,那个业务代码很难维护。
今天简单对事务作了一个总结,有什么不对的地方请多多指教!