• 事务杂谈


    什么是事务?

    事务就是一组原子性的SQL查询,或者说是一个独立的工作单元。事务内的语句,要么全部执行成功,要么全部执行失败。

    事务的特性

    ACID. 参见ACID

    原子性(Atomicity)

    一个事务必须被视为一个不可分割的最小工作单元,整个事务中的所有操作要么全部提交成功,要么全部失败回滚,对于一个事务来说,不可能只执行其中的一部分操作,这就是事务的原子性。

    一致性(Consistency)

    数据库总是从一个一致性的状态转换到另外一个一致性的状态。
    https://en.wikipedia.org/wiki/Consistency_(database_systems)定义:

    Consistency (or Correctness) in database systems refers to the requirement that any given database transaction must change affected data only in allowed ways.
    数据库中的一致性指的是,任何数据库事务去改变受影响的数据,必须符合约束、级联、触发器或其他关联关系。

    隔离性(Isolation)

    通常来说,一个事务所做的修改在最终提交以前,对其他事务是不可见的。
    The intermediate state of a transaction is invisible to other transactions. As a result, transactions that run concurrently appear to be serialized.(发散 as-if-serial)
    维基百科解释:

    Transactions are often executed concurrently (e.g., multiple transactions reading and writing to a table at the same time). Isolation ensures that concurrent execution of transactions leaves the database in the same state that would have been obtained if the transactions were executed sequentially.
    事务并发的执行。隔离性能保证并发执行的事务执行后的状态跟事务顺序的执行是一样的。

    读现象

    image.png

    • 脏读

    A dirty read (aka uncommitted dependency) occurs when a transaction is allowed to read data from a row that has been modified by another running transaction and not yet committed.
    脏读发生在允许一个事务读取另一更改之后的数据但未提交的事务的情况下。

    READ_UNCOMMITTED 隔离级别会出现这种情况。
    image.png

    • 不可重复读

    A non-repeatable read occurs when, during the course of a transaction, a row is retrieved twice and the values within the row differ between reads.
    不可重复读发生在,一个事务中,某一行被获取了两次,而两次读到的值不一样。

    At the SERIALIZABLE and REPEATABLE READ isolation levels, the DBMS must return the old value for the second SELECT. At READ COMMITTED and READ UNCOMMITTED, the DBMS may return the updated value; this is a non-repeatable read.(前面两个返回的是旧数据,所以不存在不可重复读的情况。而后面两个隔离级别,返回的是最新的数据)
    image.png

    • 幻读

    A phantom read occurs when, in the course of a transaction, new rows are added or removed by another transaction to the records being read.
    幻读发生在,一个事务过程中,另一个事务增加或删除的行被这个事务读取。

    Serializable的隔离级别不会出现幻读的情况。
    image.png

    事务的隔离级别

    SERIALIZABLE; REPEATABLE READ; READ COMMITTED; READ UNCOMMITTED
    不同的数据库默认隔离级别也不一样。MySQL: REPEATABLE READ。Postgres:READ COMMITTED(查看方式:show default_transaction_isolation
    image.png

    持久性(Durability)

    已被提交的事务对数据库的修改应该永久保存在数据库中

    事务与锁

    Innodb中的事务隔离级别和锁的关系

    开发中常用的事务

    对于SpringBoot项目而言,平常开发使用声明式事务就可以了。@Transactional(rollbackFor = Exception.class)
    但如果你只用的是Spring配置xml的方式,会遇到一个<tx:anotation-driven>。对应SpringBoot中@EnableTransactionManagement, 自动装配机制的TransactionAutoConfiguration里已经写好了,所以不需要再写任何其他的配置。

    我的事务怎么不生效?

    事务不生效的情况如下:

    1. private,protected,package-visible、final方法上加@Transactional注解: 编译器都看不下去
      image.png
      https://docs.spring.io/spring-framework/docs/current/reference/html/data-access.html#transaction-declarative-annotations-method-visibility
      final的是因为
    • With CGLIB, final methods cannot be advised, as they cannot be overridden in runtime-generated subclasses.

    附:Proxying Mechanisms

    1. 动态代理不生效的常见场景:未使用代理对象调用方法
    public void saveDataWithAnotherTransactionMethod(String teacherId) {
            System.out.println(this);
            System.out.println(this.getClass().getName());
            // this这里是TransactionService对象,非代理对象,因此无法进行增强,相当于注解失效,所以遇到异常无法回滚
            this.saveTeacherAndStudent(teacherId);
            // 获取代理对象,可正常回滚
            //TransactionService transactionService = (TransactionService) AopContext.currentProxy();
            //transactionService.saveTeacherAndStudent(teacherId);
        }
    

    application上加上@EnableAspectJAutoProxy(exposeProxy = true) ,方法中 AopContext.currentProxy();

    声明式事务rollbackFor如果写成了IOException,而实际遇到了空指针,能回滚吗?

    可以回滚

    rollbackFor指定了Exception,但事务方法内try catch了,只是记录log,还能回滚吗?

    不能

    我service方法就仅仅是增(删/改/查)单个操作,需要加事务吗?

    结论是:不需要。

    PostgreSQL actually treats every SQL statement as being executed within a transaction. If you do not issue a BEGIN command, then each individual statement has an implicit BEGIN and (if successful) COMMIT wrapped around it.

    By default, MySQL runs with autocommit mode enabled. This means that, when not otherwise inside a transaction, each statement is atomic, as if it were surrounded by START TRANSACTION and COMMIT. You cannot use ROLLBACK to undo the effect; however, if an error occurs during statement execution, the statement is rolled back.

    事务的传播级别

    参见Propagation,@Transactional注解默认的传播级别是Propagation._REQUIRED_Support a current transaction, create a new one if none exists
    相关逻辑处理在AbstractPlatformTransactionManager#getTransaction
    关于NESTED

    PROPAGATION_NESTED uses a single physical transaction with multiple savepoints that it can roll back to. Such partial rollbacks let an inner transaction scope trigger a rollback for its scope, with the outer transaction being able to continue the physical transaction despite some operations having been rolled back. This setting is typically mapped onto JDBC savepoints, so it works only with JDBC resource transactions

    开发所用框架背后的原理

    AOP

    Controller接口中调用service方法时,spring框架做了什么?

    image.png
    Spring使用Cglib代理,为使用的service生成了代理对象。然后CglibAopProxy下有个DynamicAdvisedInterceptor(private static class DynamicAdvisedInterceptor implements MethodInterceptor, Serializable)对要执行的方法进行一个拦截。
    然后获取拦截链,
    List<Object> chain = this.advised.getInterceptorsAndDynamicInterceptionAdvice(method, targetClass);
    如果saveDataWithAnotherTransactionMethod没有@Transactional 注解,chain为空,正常执行该service方法。反之,如果有事务注解,
    chain有一个TransactionInterceptor,会执行TransactionAspectSupport#invokeWithinTransaction方法
    image.png

    加了事务注解,不用手动去begin、commit/rollback了,在哪做的呢?

    TransactionAspectSupport#invokeWithinTransaction中有如下代码:

    // flag1: 这里很重要,事务的开启
    TransactionInfo txInfo = createTransactionIfNecessary(ptm, txAttr, joinpointIdentification);
    
    Object retVal;
    try {
        // This is an around advice: Invoke the next interceptor in the chain.
        // This will normally result in a target object being invoked.
        // 这里执行被代理方法
        retVal = invocation.proceedWithInvocation();
    }
    catch (Throwable ex) {
        // target invocation exception
        completeTransactionAfterThrowing(txInfo, ex);
        throw ex;
    }
    finally {
        cleanupTransactionInfo(txInfo);
    }
    

    上面的flag1处使用到了AbstractPlatformTransactionManager#getTransaction,当你用到事务传播级别的时候,怎么处理的,都在这里了。不过过多介绍。

    public final TransactionStatus getTransaction(@Nullable TransactionDefinition definition)
    			throws TransactionException {
    
    		// Use defaults if no transaction definition given.
    		TransactionDefinition def = (definition != null ? definition : TransactionDefinition.withDefaults());
    
    		Object transaction = doGetTransaction();
    		boolean debugEnabled = logger.isDebugEnabled();
    
    		if (isExistingTransaction(transaction)) {
    			// Existing transaction found -> check propagation behavior to find out how to behave.
    			return handleExistingTransaction(def, transaction, debugEnabled);
    		}
    
    		// Check definition settings for new transaction.
    		if (def.getTimeout() < TransactionDefinition.TIMEOUT_DEFAULT) {
    			throw new InvalidTimeoutException("Invalid transaction timeout", def.getTimeout());
    		}
    
    		// No existing transaction found -> check propagation behavior to find out how to proceed.
    		if (def.getPropagationBehavior() == TransactionDefinition.PROPAGATION_MANDATORY) {
    			throw new IllegalTransactionStateException(
    					"No existing transaction found for transaction marked with propagation 'mandatory'");
    		}
    		else if (def.getPropagationBehavior() == TransactionDefinition.PROPAGATION_REQUIRED ||
    				def.getPropagationBehavior() == TransactionDefinition.PROPAGATION_REQUIRES_NEW ||
    				def.getPropagationBehavior() == TransactionDefinition.PROPAGATION_NESTED) {
    			SuspendedResourcesHolder suspendedResources = suspend(null);
    			if (debugEnabled) {
    				logger.debug("Creating new transaction with name [" + def.getName() + "]: " + def);
    			}
    			try {
                    // 开启事务
    				return startTransaction(def, transaction, debugEnabled, suspendedResources);
    			}
    			catch (RuntimeException | Error ex) {
    				resume(null, suspendedResources);
    				throw ex;
    			}
    		}
    		else {
    			// Create "empty" transaction: no actual transaction, but potentially synchronization.
    			if (def.getIsolationLevel() != TransactionDefinition.ISOLATION_DEFAULT && logger.isWarnEnabled()) {
    				logger.warn("Custom isolation level specified but no actual transaction initiated; " +
    						"isolation level will effectively be ignored: " + def);
    			}
    			boolean newSynchronization = (getTransactionSynchronization() == SYNCHRONIZATION_ALWAYS);
    			return prepareTransactionStatus(def, null, true, newSynchronization, debugEnabled, null);
    		}
    	}
    

    这里插一句:方法中使用了JPA的save方法,也走到了TransactionInterceptor的事务执行这里。代码层面印证了上文提到了每个语句都会开启事务。
    image.png

    为什么抛出的异常是ArithmeticException,rollbackFor里写个空指针,也可以回滚?

    TransactionAspectSupport#completeTransactionAfterThrowingtxInfo.transactionAttribute.rollbackOn(ex)

    public boolean rollbackOn(Throwable ex) {
    		RollbackRuleAttribute winner = null;
    		int deepest = Integer.MAX_VALUE;
    
    		if (this.rollbackRules != null) {
                // rollbackFor指定的Exception
    			for (RollbackRuleAttribute rule : this.rollbackRules) {
    				int depth = rule.getDepth(ex);
    				if (depth >= 0 && depth < deepest) {
    					deepest = depth;
    					winner = rule;
    				}
    			}
    		}
    
    		// User superclass behavior (rollback on unchecked) if no rule matches.
    		if (winner == null) {
    			return super.rollbackOn(ex);
    		}
    
    		return !(winner instanceof NoRollbackRuleAttribute);
    	}
    

    如果rollbackFor = Exception.class,能找到winner,就能回滚。如果实际抛出的异常跟rollbackFor指定的不匹配或者你只写了一个@Transactional,没指定rollbackFor,会走默认的rollback规则(DefaultTransactionAttribute.java)。可以看出如果不是RuntimeException或Error,就无法回滚了。所以,一般情况下都这么写@Transactional(rollbackFor = Exception.class)

    public boolean rollbackOn(Throwable ex) {
    		return (ex instanceof RuntimeException || ex instanceof Error);
    	}
    
    
    

    再插一句:lombok的@SneakyThrows注解,抛出的是UndeclaredThrowableException(RuntimeException)

    使用默认的传播行为,一个事务方法中调用了另一个事务方法(代理对象调用的方式),内外层出现异常,事务会回滚吗?

    无论内层事务还是外层事务,只要有一个抛出异常了,由于REQUIRED,使用的是同一个事务,因此回滚就都回滚了。
    注:如果内部事务抛出异常,外部事务不允许catch内部事务异常,否则报:
    org.springframework.transaction.UnexpectedRollbackException: Transaction silently rolled back because it has been marked as rollback-only
    如下所示

     @Transactional(rollbackFor = Exception.class)
        public void complexSaveDataWithAnotherTransactionMethod(String teacherId) throws FileNotFoundException {
            // 外层无异常,内层异常:由于异常传递,二者皆回滚
            //User user = new User().setId(teacherId).setName("外user" + teacherId);
            //userRepository.save(user);
            //Student student = new Student().setId("外" + teacherId).setName("外Student" + teacherId).setTeacherId(teacherId);
            //studentRepository.save(student);
            //TransactionService transactionService = (TransactionService) AopContext.currentProxy();
            //transactionService.saveTeacherAndStudent(teacherId);
    
            // 外层异常,内层无异常: 回滚
            //TransactionService transactionService = (TransactionService) AopContext.currentProxy();
            //transactionService.saveTeacherAndStudent(teacherId);
            //User user = new User().setId(teacherId).setName("外user" + teacherId);
            //userRepository.save(user);
            //int i = 1/0;
            //Student student = new Student().setId("外" + teacherId).setName("外Student" + teacherId).setTeacherId(teacherId);
            //studentRepository.save(student);
    
            // 内部异常,外部给捕获: 均回滚 org.springframework.transaction.UnexpectedRollbackException: Transaction silently rolled back because it has been marked as rollback-only
            try {
                TransactionService transactionService = (TransactionService) AopContext.currentProxy();
                transactionService.saveTeacherAndStudent(teacherId);
            } catch (Exception e) {
                log.error("complexSaveDataWithAnotherTransactionMethod error", e);
            }
            User user = new User().setId(teacherId).setName("外user" + teacherId);
            userRepository.save(user);
            //int i = 1/0;
            Student student = new Student().setId("外" + teacherId).setName("外Student" + teacherId).setTeacherId(teacherId);
            studentRepository.save(student);
        }
    

    关于上面问题的解释,PROPAGATION_REQUIRED有这样一段话:

    When the propagation setting is PROPAGATION_REQUIRED, a logical transaction scope is created for each method upon which the setting is applied. Each such logical transaction scope can determine rollback-only status individually, with an outer transaction scope being logically independent from the inner transaction scope. In the case of standard PROPAGATION_REQUIRED behavior, all these scopes are mapped to the same physical transaction. So a rollback-only marker set in the inner transaction scope does affect the outer transaction’s chance to actually commit.
    大意是:传播级别设置为PROPAGATION_REQUIRED的时候,会为每个方法创建一个逻辑上的事务作用域。每个方法的逻辑事务作用域相互独立,但对应的都是同一个物理事务。所以,在发生异常的逻辑事务作用域中设置了ROLLBACK-ONLY的标识,只要有,就都回滚了。

    一些问题

    • 在事务方法中向表a,表b插入了数据,能看到这个过程中插入的数据吗?
    • 表a原来有10条数据,在事务方法中向表a插入了1条数据,同时查询了表a的总数,结果应该是多少?

    下面的示例代码:

    1. gg的修饰符为public , teacher表会不会新增一条id为1的数据?@Transactional生效,遇到异常回滚,不会新增
    2. gg的修饰符为default/protected,会不会新增? 当使用spring代理时, @Transactional只有public方法可以生效
    3. 第二个问题,怎么样让注解可以生效?

    添加一个配置,由于springboot中已经配置了该bean,因此需要覆写. 启动配置中添加spring.main.allow-bean-definition-overriding=true

    @Configuration
    public class TransactionAttributeConfig {
    
        @Bean
        TransactionAttributeSource transactionAttributeSource() {
            return new AnnotationTransactionAttributeSource(false);
        }
    }
    

    示例代码:

    // service层代码
    public void testInsertWithException () {
            TransactionService transactionService = (TransactionService) AopContext.currentProxy();
            transactionService.gg();
        }
    
    
    // private 方法无法拿到注入的teacherRepository,这里用defulat访问修饰符
    @Transactional(rollbackFor = Exception.class)
    void gg() {
        Teacher teacher = new Teacher().setId("1").setName("内teacher" + "1");
        teacherRepository.save(teacher);
        int a = 1 / 0;
    }
    

    其他相关

    分布式事务: 不做讲解

    打印事务相关的日志:
    jpa用的 logging.level.org.springframework.orm.jpa=true

    参考

  • 相关阅读:
    mysql外键和连表操作
    数据库的操作
    进程之select和epoll
    jwt的应用生成token,redis做储存
    为什么前后端分离不利于seo
    redis的bitmap
    lnmp环境的nginx的tp5配置
    虚拟机安装cenos7后ifcfg看网卡无inet地址掩码等信息
    rsa加密
    hydra命令
  • 原文地址:https://www.cnblogs.com/studentytj/p/15977867.html
Copyright © 2020-2023  润新知