MySQL中本地事务的示例分析

发布时间:2021-11-17 09:53:00 作者:小新
来源:亿速云 阅读:109

小编给大家分享一下MySQL中本地事务的示例分析,相信大部分人都还不怎么了解,因此分享这篇文章给大家参考一下,希望大家阅读完这篇文章后大有收获,下面让我们一起去了解一下吧!

1. MySQL中的本地事务

1.1 什么是事务

事务是逻辑上的一组操作,要么都执行,要么都不执行,一荣俱荣,一损俱损。数据库事务有严格的定义,必须满足4个特性。

原子性:事务是最小的执行单位,不允许分割。事务的原子性确保动作要么全部完成,要么完全不起作用;
一致性:执行事务前后,数据保持一致,多个事务对同一个数据读取的结果是相同的,事务操作成功前后,数据库所处的状态和它的业务规则是一致的。在这些事务特性中,数据一致性是最终目标,其它特性都是为达到这个目标采取的措施、要求或者手段。

ACID中的一致性和CAP中的一致性有什么区别?

二者完全不是一个事情,数据库对于 ACID 中的一致性的定义是这样的:如果一个事务原子地在一个一致地数据库中独立运行,那么在它执行之后,数据库的状态一定是一致的。对于这个概念,它的第一层意思就是对于数据完整性的约束,包括主键约束、引用约束以及一些约束检查等等,在事务的执行的前后以及过程中不会违背对数据完整性的约束,所有对数据库写入的操作都应该是合法的,并不能产生不合法的数据状态。而第二层意思其实是指逻辑上的对于开发者的要求,我们要在代码中写出正确的事务逻辑,比如银行转账,事务中的逻辑不可能只扣钱或者只加钱,这是应用层面上对于数据库一致性的要求。即,数据库 ACID 中的一致性对事务的要求不止包含对数据完整性以及合法性的检查,还包含应用层面逻辑的正确。

CAP 定理中的数据一致性,其实是说分布式系统中的各个节点中对于同一数据的拷贝有着相同的值。

隔离性:并发访问数据库时,一个用户的事务不被其他事务所干扰,各并发事务之间数据库是独立的,采用数据库锁机制来保证事务的隔离性;
持久性:一个事务被提交之后。它对数据库中数据的改变是持久的,即使数据库发生故障也不应该对其有任何影响。

1.2 事务隔离级别

在典型的应用程序中,多个事务并发运行,经常会操作相同的数据来完成各自的任务(多个用户对统一数据进行操作)。并发虽然是必须的,但可能会导致以下的问题:
脏读(Dirtyread):当一个事务正在访问数据并且对数据进行了修改,而这种修改还没有提交到数据库中,这时另外一个事务也访问了这个数据,然后使用了这个数据。因为这个数据是还没有提交的数据,那么另外一个事务读到的这个数据是“脏数据”,依据“脏数据”所做的操作可能是不正确的。
丢失修改(Losttomodify):指在一个事务读取一个数据时,另外一个事务也访问了该数据,那么在第一个事务中修改了这个数据后,第二个事务也修改了这个数据。这样第一个事务内的修改结果就被丢失,因此称为丢失修改。例如:事务1读取某表中的数据A=20,事务2也读取A=20,事务1修改A=A-1,事务2也修改A=A-1,最终结果A=19,事务1的修改被丢失。
不可重复读(Unrepeatableread):指在一个事务内多次读同一数据。在这个事务还没有结束时,另一个事务也访问该数据。那么,在第一个事务中的两次读数据之间,由于第二个事务的修改导致第一个事务两次读取的数据可能不太一样。这就发生了在一个事务内两次读到的数据是不一样的情况,因此称为不可重复读。
幻读(Phantomread):幻读与不可重复读类似。它发生在一个事务(T1)读取了几行数据,接着另一个并发事务(T2)插入了一些数据时。在随后的查询中,第一个事务(T1)就会发现多了一些原本不存在的记录,就好像发生了幻觉一样,所以称为幻读。

不可重复度和幻读区别:不可重复读的重点是修改,幻读的重点在于新增或者删除。例1(同样的条件,你读取过的数据,再次读取出来发现值不一样了):事务1中的A先生读取自己的工资为1000的操作还没完成,事务2中的B先生就修改了A的工资为2000,导致A再读自己的工资时工资变为2000;这就是不可重复读。
例2(同样的条件,第1次和第2次读出来的记录数不一样):假某工资单表中工资大于3000的有4人,事务1读取了所有工资大于3000的人,共查到4条记录,这时事务2又插入了一条工资大于3000的记录,事务1再次读取时查到的记录就变为了5条,这样就导致了幻读。

SQL标准定义了四个隔离级别隔离性和一致性其实是一个需要开发者去权衡的问题,为数据库提供什么样的隔离性层级也就决定了数据库的性能以及可以达到什么样的一致性
READ-UNCOMMITTED(读取未提交):最低的隔离级别,允许读取尚未提交的数据变更,可能会导致脏读、幻读或不可重复读。
READ-COMMITTED(读取已提交):允许读取并发事务已经提交的数据,可以阻止脏读,但是幻读或不可重复读仍有可能发生。(MySQL可重复读隔离级别的实现原理)
REPEATABLE-READ(可重复读):对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改,可以阻止脏读和不可重复读,但幻读仍有可能发生。
SERIALIZABLE(可串行化):最高的隔离级别,完全服从ACID的隔离级别。所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,该级别可以防止脏读、不可重复读以及幻读。

隔离级别脏读不可重复读幻影读
READ-UNCOMMITTED
READ-COMMITTED×
REPEATABLE-READ××
SERIALIZABLE×××

     表-数据并发问题在不同隔离级别的出现

这里需要注意的是:与SQL标准不同的地方在于InnoDB存储引擎在REPEATABLE-READ(可重读)事务隔离级别下使用的是Next-KeyLock锁算法,因此可以避免幻读的产生,这与其他数据库系统(如SQLServer)是不同的。所以说InnoDB存储引擎的默认支持的隔离级别是REPEATABLE-READ(可重读)已经可以完全保证事务的隔离性要求,即达到了SQL标准的SERIALIZABLE(可串行化)隔离级别。因为隔离级别越低,事务请求的锁越少,所以大部分数据库系统的隔离级别都是READ-COMMITTED(读取提交内容):,但是你要知道的是InnoDB存储引擎默认使用REPEATABLE-READ(可重读)并不会有任何性能损失。
InnoDB存储引擎在分布式事务的情况下一般会用到SERIALIZABLE(可串行化)隔离级别。

1.3 MySQL事务基本实现

事务的原子性和持久性是由事务日志(transaction log)保证的回滚日志用于对事务的影响进行撤销,重做日志在错误处理时对已经提交的事务进行重做。它们能保证两点:

UndoLog的原理很简单,为了满足事务的原子性,在操作任何数据之前,首先将数据备份到一个地方(这个存储数据备份的地方称为UndoLog)。然后进行数据的修改。如果出现了错误或者用户执行了ROLLBACK语句,系统可以利用Undo Log中的备份将数据恢复到事务开始之前的状态,UndoLog并不能将数据库物理地恢复到执行语句或者事务之前的样子;它是逻辑日志,当回滚日志被使用时,它只会按照日志逻辑地将数据库中的修改撤销掉看,可以理解为我们在事务中使用的每一条INSERT都对应了一条DELETE,每一条UPDATE也都对应一条相反的UPDATE语句。

RedoLog记录的是新数据的备份, 保障的是事务的持久性和一致性。在事务提交前,只要将RedoLog持久化即可,不需要将数据持久化。当系统崩溃时,虽然数据没有持久化,但是RedoLog已经持久化。系统可以根据RedoLog的内容,将所有数据恢复到最新的状态。 (深入分析可以参考面向信仰编程-浅入深出MySQL中事务的实现-回滚日志、重做日志)
redo log和undo log的过程分析
eg : 假设有2个数值,分别为A和B,值为1,2
1 start transaction;
2 记录 A=1 到undo log;
3 update A = 3;
4 记录 A=3 到redo log;
5 记录 B=2 到undo log;
6 update B = 4;
7 记录B = 4 到redo log;
8 将redo log刷新到磁盘
9 commit
在1-8的任意一步系统宕机,事务未提交,该事务就不会对磁盘上的数据做任何影响.
如果在8-9之间宕机,恢复之后可以选择回滚,也可以选择继续完成事务提交,因为此时redo log已经持久化
若在9之后系统宕机,内存映射中变更的数据还来不及刷回磁盘,那么系统恢复之后,可以根据redo log把数据刷回磁盘

数据库对于隔离级别的实现就是使用并发控制机制对在同一时间执行的事务进行控制,限制不同的事务对于同一资源的访问和更新,而最重要也最常见的并发控制机制,主要有锁、时间戳(即乐观锁,并不是真正的锁机制,而是一种思想)、多版本MVCC

2. Spring的事务管理

2.1 Spring对事务管理的支持

Spring为事务管理提供了一致的编程模板,在高层建立了统一的事务抽象。也就是说,不管是选择Spring JDBC、Hibernate、JPA还是选择MyBatis,Spring都可以让用户使用统一的编程模型进行事务管理

Spring为事务管理提供了一致的编程模板,在高层次建立了统一的事务抽象。像Spring DAO为不同的持久化技术实现提供模板类一样,Spring事务管理继承了这一风格,也提供了事务模板类TransactionTemplate。通过TransactionTemplate并配合使用事务回调TransactionCallback指定具体的持久化操作就可以通过编程方式实现事务管理,而无须关注资源获取、复用、释放、事务同步和异常处理的操作。

2.1.1 事务管理关键抽象

在Spring事务管理SPI的抽象层主要包括3个接口,分别是PlatformTransactionManager、TransactionDefinition和TransactionStatus,它们位于org.springframework.transaction包中。3者关系如图:

                                        MySQL中本地事务的示例分析

                                                        图-Spring事务管理SPI抽象

其中,TransactionDefinition用于描述事务的隔离级别,超时时间、是否为只读事务和事务传播规则等控制事务具体行为的事务属性。这些事务属性可以通过XML配置、注解描述或手工编程的方式设置。PlatformTransactionManager根据TransactionDefinition提供的事务属性配置信息创建事务,并用TransactionStatus描述这个激活事务的状态。

事务隔离:TransactionDefinition使用了java.sql.Connection接口中同名的4个隔离级别,此外,TransactionDefinition还定义了一个默认的隔离级别,它表示使用底层数据库的默认隔离级别。
事务传播:通常在一个事务中执行的所有代码都会同一事务的上下文中。但是Spring也提供了几个可选的事务传播类型,例如简单地参与到现有的事务中,或者挂起当前的事务,创建一个新事务。
事务超时:事务在超时前能运行多久,超过时间后,事务被回滚。有些事务管理器不支持事务过期的功能,这时如果设置TIMEOUT_DEFAULT等值时将抛出异常。
只读状态:只读事务不修改任何数据,主要用于优化,如果更改数据就会抛出异常。

spring允许通过XML或者注解元数据的方式为一个有事务要求的服务类方法配置事务属性,这些信息作为Spring事务管理框架的输入,Spring将自动按照事务属性信息的指示,为目标方法提供相应的事务支持。

TransactionStatus代表一个事务的具体运行状态,事务管理器通过该接口获取事务的运行期状态信息,也可以通过该接口间接地回滚事务,它相比于在抛出异常时回滚事务的方式更具有可控性。

PlatformTransactionManager是事务的最高层抽象,它提供了3个接口方法:
TransactionStatus getTransaction(TransactionDefinition definition):该方法根据事务定义信息从事务环境中返回一个已存在的事务,或者创建一个新的事务,并用TransactionStatus描述这个事务的状态。
commit(TransactionStatus status):根据事务的状态提交事务,如果事务状态已经被标识为rollback-only,该方法将执行一个回滚事务的操作。
rollback(TransactionStatus status):回滚事务,当提交事务抛出异常时,回滚会被隐式执行。

2.1.2 Spring的事务管理器实现类

Spring将事务管理委托给底层具体的持久化实现框架完成,因此Spring为不同的持久化框架提供了PlatformTransactionManager接口的实现类,如下图:

                              MySQL中本地事务的示例分析                 

                                             图-不同持久化技术对应的事务管理器实现类 

这些事务管理器都是对特定事务实现框架的代理,这样我们就可以通过spring的高级抽象,对不同种类的事务实现使用相同的方式进行管理,而不用关心具体的实现。要实现事务管理,首先要在Spring中配置好相应的事务管理器,为事务管理器指定数据资源及一些其它事务管理控制属性。

2.1.3 事务同步管理器

Spring将JDBC的Connection、Hibernate的Session等访问数据库的连接或会话对象统称为资源。这些资源在同一时刻是不能多线程共享的,为了让DAO、Service能做到singleton,Spring的事务同步管理器类org.springframework.transaction.support.TransactionSynchronizationManager使用ThreadLocal为不同事务线程提供了独立的资源副本,同时维护事务配置的属性和运行状态信息。

2.1.4 事务传播行为

在一个service接口中可能会调用另一个service接口的方法,以共同完成一个完整的业务操作,Spring通过事务传播行为控制当前的事务如何传播到被嵌套调用的目标服务接口方法中。Spring在TransactionDefinition接口中规定了7种类型的事务传播行为,如下图

                          MySQL中本地事务的示例分析

                                                       图-事务传播行为类型 

2.2 编程式的事务管理

Spring为编程式事务管理提供了模板类org.springframework.transaction.support.TransactionTemplate,和那些持久化模板类一样,TransactionTemplate也是线程安全的。TransactionTemplate有2个重要的方法:

// 设置事务管理器
void  setTransactionManager(PlatformTransactionManager  transactionManager)
// 在TransactionCallback回调接口中定义需要以事务的方式组织的数据访问逻辑
Object  execute(TransactionCallback  action)
TransactionCallback接口只有一个方法:Object  doInTransaction(TransactionStatus  status)。如果操作不会返回结果,可以使用TransactionCallback的子接口TransactionCallbackWithoutResult。

2.3 使用注解配置声明式事务

Spring Boot中使用@Transactional注解配置事务管理

2.4 Spring事务管理难点与坑

2.4.1 DAO和事务管理的牵绊

是否用了Spring,就一定要用Spring事务管理器,否则就无法进行数据的持久化操作呢?答案是否定的,脱离了事务性,DAO照样可以顺利地进行数据操作。对于强调速度的应用,数据库本身可能就不支持事务,如MyISAM引擎的数据库,这是无需配置事务管理器,即使配置了,也是没有实际用处的。

2.4.2 应用分层迷惑

将面向接口编程奉为圭臬,过分强制面向接口编程除了会带来更多的的类文件,并不会有什么好处。Spring事务管理支持在Controller层直接添加事务注解并生效,因此事务管理并不一定强制应用必须严格分层,可以根据实际应用出发,根据实际需要进行编程。

2.4.3 事务方法嵌套调用的迷惑

除了事务的传播行为,对于事务的其它特性,Spring是借助底层资源的功能来完成的,无非充当了一个代理的角色。但是Spring事务的传播行为却是Spring凭借自身的框架提供的功能。

例如对于调用链Service1#method1()->Service2#method2()->Service3#method3(),那么这三个方法通过Spring的事务传播机制都可以工作在同一个事务中。

首先调用的是AOP代理对象而不是目标对象,首先执行事务切面,事务切面内部通过TransactionInterceptor环绕增强进行事务的增强,即进入目标方法之前开启事务,退出目标方法时提交/回滚事务。目标对象内部的自我调用将无法实施切面中的增强。

public interface AService {  
    public void a();  
    public void b();  
}  
   
@Service()  
public class AServiceImpl1 implements AService{  
    @Transactional(propagation = Propagation.REQUIRED)  
    public void a() {
// 此处的this指向目标对象,因此调用this.b()将不会执行b事务切面,即不会执行事务增强(通过日志可以观察到)  
        this.b();  
    }  
    @Transactional(propagation = Propagation.REQUIRES_NEW)  
    public void b() {  
    }  
}

如何解决?

方法一:调用AOP代理对象的b方法即可执行事务切面进行事务增强

需要开启暴露Aop代理到ThreadLocal支持,并将this.b()修改为((AService) AopContext.currentProxy()).b();

这种通过ThreadLocal暴露Aop代理对象适合解决所有场景(不管是singleton Bean还是prototype Bean)的AOP代理获取问题(即能解决目标对象的自我调用问题);

方法二:通过初始化方法在目标对象中注入代理对象

这种方式不是很灵活,所有需要自我调用的实现类必须重复实现代码

@Service  
public class AServiceImpl3 implements AService{  
    @Autowired  //①  注入上下文  
    private ApplicationContext context;  
      
    private AService proxySelf; //②  表示代理对象,不是目标对象  
    @PostConstruct  //③ 初始化方法  
    private void setSelf() {  
        //从上下文获取代理对象(如果通过proxtSelf=this是不对的,this是目标对象)  
        //此种方法不适合于prototype Bean,因为每次getBean返回一个新的Bean  
        proxySelf = context.getBean(AService.class);   
    }  
    @Transactional(propagation = Propagation.REQUIRED)  
    public void a() {  
       proxySelf.b(); //④ 调用代理对象的方法 这样可以执行事务切面  
    }  
    @Transactional(propagation = Propagation.REQUIRES_NEW)  
    public void b() {  
    }  
}

方法三:通过BeanPostProcessor在目标对象中注入代理对象

需要注意到循环依赖和非singleton bean的影响,以下方式能解决singleton之间的循环依赖问题,但是不能解决循环依赖中包含prototype Bean的自我调用问题。

// 即我们自定义的BeanPostProcessor (InjectBeanSelfProcessor)
// 如果发现我们的Bean是实现了该标识接口就调用setSelf注入代理对象。
public interface BeanSelfAware {  
    void setSelf(Object proxyBean);  
} 

@Component  
public class InjectBeanSelfProcessor implements BeanPostProcessor, ApplicationContextAware {  
    private ApplicationContext context;  
    //① 注入ApplicationContext  
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {  
        this.context = applicationContext;  
    }  
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {  
        if(!(bean instanceof BeanSelfAware)) { //② 如果Bean没有实现BeanSelfAware标识接口 跳过  
            return bean;  
        }  
        if(AopUtils.isAopProxy(bean)) { //③ 如果当前对象是AOP代理对象,直接注入  
            ((BeanSelfAware) bean).setSelf(bean);  
        } else {  
            //④ 如果当前对象不是AOP代理,则通过context.getBean(beanName)获取代理对象并注入  
            //此种方式不适合解决prototype Bean的代理对象注入  
            ((BeanSelfAware)bean).setSelf(context.getBean(beanName));  
        }  
        return bean;  
    }  
    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {  
        return bean;  
    }  
}

扩展:灵活使用事务注解并非一定需要接口

如果在一个业务方法中包含查询、修改等逻辑,但其实只有这些修改操作需要实施业务增强,这是应该怎么处理?可以让改service实现BeanSelfAware接口,然后在service注入代理对象,通过代理对象

@Service  
public class AServiceImpl1 implements AService,BeanSelfAware{

    private AServiceImpl1 self;

    @Override
    public void setSelf(AServiceImpl1 proxy) {
        this.self = proxy;
    }

    @Override  
    public void a() {
       // ... 其它查询等无需事务逻辑
       // 需要事务增强的修改逻辑
       self.m();
  
    } 
        
    @Transactional  
    public void m() {  
    }  
}

2.4.4 多线程的困惑

简书-Spring中的Bean是线程安全的吗?

在相同线程中进行相互的嵌套调用的事务方法工作在相同的事务中,如果这些相互嵌套调用的方法工作在不同线程中,则不同的线程下的事务方法工作在独立的事务中。

2.4.5 联合军种作战的混乱

如果用户采用了一种高端的ORM技术(Hibernate、JPA、JDO),同时还采用了一种JDBC技术(Spring JDBC、Mybatis),由于前者的会话Session是对后者连接Connection的封装,Spring会足够智能地在同一个事务线程中让前者的会话封装后者的连接,所以只要只要直接采用前者前者的事务管理器就可以了。

                                MySQL中本地事务的示例分析

                                                   图-混合数据访问技术所对应的事务管理器

2.4.6 特殊方法成漏网之鱼

由于Spring事务管理是基于接口代理或者动态字节码技术,通过AOP实施事务增强的,虽然Spring依然支持AspectJ在类的加载期间实施增强,但这种方法很少使用,这里不做讨论。

对于基于接口动态代理的AOP事务增强来说,由于接口的方法都必须是public的,这就要求实现类的实现方法也必须是public的(不能是protected、private)的,同时不能使用static修饰符。所以,可以实施接口动态代理的方法只能是public或public  final修饰符的方法,其它方法都不能被动态代理,相应地也就不能实施AOP增强,换句话说即不能进行Spring事务的增强。

基于CGLib字节码动态代理的方案是通过扩展被增强类,动态创建起子类的方式进行AOP增强植入的,由于使用final、static、private修饰符的方法都不能被子类覆盖,相应地这些方法无法实施AOP增强。所以方法签名必须特别注意这些修饰符的使用,以免成为事务管理的漏网之鱼。

需要注意的是,我们说这些方法不能被Spring进行AOP事务增强,是指这些方法不能启动事务,但是外层方法的事务上下文依旧可以顺利地传播到这些方法中。这些不能被事务增强的方法和可被事务增强的方法的唯一区别在于“是否可以主动开启一个新事务”,前者可以而后者不可以,对于事务传播行为来说,二者是完全相同的。

                               MySQL中本地事务的示例分析

                                                                图-Spring事务管理漏网之鱼

以上是“MySQL中本地事务的示例分析”这篇文章的所有内容,感谢各位的阅读!相信大家都有了一定的了解,希望分享的内容对大家有所帮助,如果还想学习更多知识,欢迎关注亿速云行业资讯频道!

推荐阅读:
  1. redis中事务操作的示例分析
  2. redis事务的示例分析

免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。

mysql

上一篇:Spring Boot是什么意思

下一篇:jquery如何获取tr里面有几个td

相关阅读

您好,登录后才能下订单哦!

密码登录
登录注册
其他方式登录
点击 登录注册 即表示同意《亿速云用户服务条款》