Spring事务的详解

数据库事务原理详解

1.事务的基本概念

  事务(Transaction)是访问并可能更新数据库中各种数据项的一个程序执行单元(unit)。

  特点:事务是恢复和并发控制的基本单位,具有ACID特性:

原子性(Atomicity)

  事务是一个不可分割的工作单位,事务执行后只有两个结果,全部成功,全部失败。

一致性(Consistency)

  事务必须是数据库从一个一致性状态变为另一个一致性状态,事务执行前后,数据库都必须保持一致性。

比如转账:A用户和B用户的钱合计是800,在使用事务的前提下,A和B互相转账,不管最终转账多少次,A用户和B用户的钱合计只能是800。这就是事务的一致性。

隔离性(Isolation)

  事务之间是相互隔离,互相不影响的。
  隔离性是当多个用户并发访问数据库时,比如操作同一张表时,数据库为每一个用户开启的事务,不能被其他事务的操作所干扰,多个并发事务之间要相互隔离。
  对于任意两个并发的事务T1和T2,在事务T1看来,T2要么在T1开始之前就已经结束,要么在T1结束之后才开始,这样每个事务都感觉不到有其他事务在并发地执行。

持久性(Durability)

  一旦事务执行完成,对数据库的数据操作是永久性的。

2.Spring事务的基本原理:

  Spring是无法提供事务功能的,Spring事务的本质就是数据库的事务。JDBC中使用事务:

        // 1.获取连接
        Connection conn = DriverManager.getConection();
        // 2.开启事务
        conn.setAutoCommit(true/false);
        // 3.CRUD
        // 4.提交/回滚事务
        conn.commit(); / conn.rollback();
        // 5.关闭链接
        conn.close();

  使用Spring的事务管理后,开启事务、提交事务/回滚事务由Spring完成。
Spring以注解为例:可在配置文件中开启注解,在类或者方法使用注解@Transactional标识。
  Spring 在启动的时候会解析生成相关的bean, 这个时候会查看拥有相关注解的类和方法,并为这些类和方法生成代理,并根据@Transactional的相关参数进行相关配置注入,在代理类中完成了(事务的开启、提交、回滚)真正的数据库层的事务提交和回滚是通过binlog或者redo log实现的。

3.Spring事务的传播性:

  spring事务的传播性,要面临多个事务同时存在的时候,Spring应该如何处理这些事务的行为?TransactionDefinition中详细定义了。


public interface TransactionDefinition {

    // 支持当事务,如果当前没有事务,则新建一个事务。Spring中默认的事务传播
    int PROPAGATION_REQUIRED = 0;

    // 支持当前事务,如果当前没有事务,就以非事务方式执行。
    int PROPAGATION_SUPPORTS = 1;

    // 支持当前事务,如果当前没有事务,就抛出异常。
    int PROPAGATION_MANDATORY = 2;

    // 新建事务,如果当前存在事务,把当前事务挂起新建的事务和被挂起的事务没有任何关系,是两个独立的事务。
    // 外层事务失败回滚之后,不能回滚内层事务执行的结果,内层事务失败,外层事务,可以选择性回滚。    
    int PROPAGATION_REQUIRES_NEW = 3;

    // 以非事务的方式执行操作,如果当前存在事务,就把当前事务挂起。
    int PROPAGATION_NOT_SUPPORTED = 4;

    // 以非事务的方式执行操作,如果当前存在事务,就抛出异常。
    int PROPAGATION_NEVER = 5;

    // 如果以活动的事务存在,则运行在一个嵌套的事务中,如果没有活动的事务,
    // 则按照REQUIRED属性执行。它使用了一个单独的事务,这个事务拥有多个可以回滚保存点。
    // 当前方法调用子方法时,如果子方法发生异常,只回滚子方法执行过的SQL,而不回滚当前方法的事务 
    // 具体是怎么做到的呢?
    // 大部分数据库都有一个保存点的一个概念。
    // 可以在一段sql语句中设置一个标志位,如果后面的代码执行出现了异常,那么也只会把数据回滚到这个标志位所对应的数据状态。
    // 然后就不会再回滚了,所以在标志位之前对数据做的操作还会保留着。
    
    // 然而并不是所有的数据库都有这个概念!
    // 在Sprin中,当数据库不能支持保存点技术时,Spring就会新建一个事务去运行代码。
    int PROPAGATION_NESTED = 6;


    
    int ISOLATION_DEFAULT = -1;

    int ISOLATION_READ_UNCOMMITTED = Connection.TRANSACTION_READ_UNCOMMITTED;

    
    int ISOLATION_READ_COMMITTED = Connection.TRANSACTION_READ_COMMITTED;

    
    int ISOLATION_REPEATABLE_READ = Connection.TRANSACTION_REPEATABLE_READ;

    
    int ISOLATION_SERIALIZABLE = Connection.TRANSACTION_SERIALIZABLE;


    
    int TIMEOUT_DEFAULT = -1;


    
    int getPropagationBehavior();

    
    int getIsolationLevel();

    int getTimeout();

    
    boolean isReadOnly();

    @Nullable
    String getName();

}

4.数据库隔离级别

隔离级别 导致的问题
Read-Uncommitted 0 导致脏读
Read-Committed 1 避免脏读,允许不可重复读、允许幻读
Repeateble-Read 2 避免脏读,不可重复读、允许幻读
Serializable 3 串行化读,事务只能同步执行,避免了脏读、不可重复读、幻读。执行效率慢,慎重!

脏读:
  A事务对A表进行了CRUD,但是未提交,B事务读取到了A事务未提交的数据,这个时候A事务执行了回滚。这样就导致了B事务读取到了脏数据。

不可重复读:
  A事务对A表进行了两次读取,在A事务第一次读取和第二次读取之间,B事务对A表的数据进行了修改,这时候A事务两次读取的结果不一致。


不可重复读

幻读:
  A事务对某类数据进行修改,这时候B事务插入了一条同类数据,A事务提交后发现还有一条数据没被修改,就好像发生了幻觉一样。

小结:不可重复读的和幻读很容易混淆,不可重复读侧重于修改,幻读侧重于新增或删除。解决不可重复读的问题只需锁住满足条件的行,解决幻读需要锁表。

事务隔离级别 脏读 不可重复读 幻读
读未提交(read-uncommitted)
不可重复读(read-committed)
可重复读(repeatable-read)
串行化(serializable)

总结:
  隔离级别越高,越能保证数据的完整性和一致性,但是对并发性能的影响越大。

  SqlServlet、Oracle默认级别:Read-Commited
  Mysql InnoDB默认级别:Repeatable-Read

5.Spring的隔离级别


public interface TransactionDefinition {

    // 默认隔离级别,使用数据库默认的事务隔离级别。另外四个与JDBC 的隔离几倍对应
    int ISOLATION_DEFAULT = -1;
    // 最低的隔离级别,允许另外一个事务可以看到这个事务未提交的数据,
    // 会产生 脏读、幻读、不可重复度
    int ISOLATION_READ_UNCOMMITTED = Connection.TRANSACTION_READ_UNCOMMITTED;

    // 保证一个事务修改的数据只有提交后才可以被发现
    // 另一个事务不能读取未提交的数据
    int ISOLATION_READ_COMMITTED = Connection.TRANSACTION_READ_COMMITTED;

    // 这种事务隔离级别可以防止脏读,不可重复度,但是可能出现幻读
    int ISOLATION_REPEATABLE_READ = Connection.TRANSACTION_REPEATABLE_READ;

    // 顺序执行,最高的事务隔离级别
    int ISOLATION_SERIALIZABLE = Connection.TRANSACTION_SERIALIZABLE;

}

数据库第一类、第二类丢失更新

第一类更新丢失(回滚丢失,Lost update)

第一类更新丢失.png

  如图可见:A事务在撤销时,“不小心”将B事务已经转入账户的金额给抹去了。
不用担心,数据库标准定义的所有隔离界别都不允许第一类丢失更新发生。

第二类更新丢失(覆盖丢失/两次更新问题,Second lost update)
   第二类丢失更新,实际上和不可重复读是同一种问题。

解决方案: 基本两种思路,一种是悲观锁,另外一种是乐观锁;

  悲观锁:
  1、排它锁,当事务在操作数据时把这部分数据进行锁定,直到操作完毕后再解锁,其他事务操作才可操作该部分数据。这将防止其他进程读取或修改表中的数据。(大多数情况下依靠数据库的锁机制实现)
实现:select xxx from user where age = 18 for update 对所选择的数据进行加锁处理,本次事务提交之前(事务提交时会释放事务过程中的锁),外界无法修改这些记录。

  乐观锁
  1.状态机,实现:大多数基于数据版本(Version)记录机制实现。
  当读取数据时,将version字段的值一同读出,数据每更新一次,对此version值加一。当我们提交更新的时候,判断当前版本信息与第一次取出来的版本值大小,如果数据库表当前版本号与第一次取出来的version值相等,则予以更新,否则认为是过期数据,拒绝更新,让用户重新操作。

6.事务的嵌套


public class Test {

    class ServiceA{
        public void methodA(){
            ServiceB serviceB = new ServiceB();
            serviceB.methodB();
        }
    }

    class ServiceB{
        public void methodB(){

        }
    }
}

一、ServiceB.methodB的事务级别定义为PROPAGATION_REQUIRED。

  1、如果ServiceA.methodA已经起了事务,这时调用ServiceB.methodB,会共用同一个事务,如果出现异常,ServiceA.methodA和ServiceB.methodB作为一个整体都将一起回滚。
  2、如果ServiceA.methodA没有事务,ServiceB.methodB就会为自己分配一个事务。ServiceA.methodA中是不受事务控制的。如果出现异常,ServiceB.methodB不会引起ServiceA.methodA的回滚

二、ServiceA.methodA的事务级别PROPAGATION_REQUIRED,ServiceB.methodB的事务级别PROPAGATION_REQUIRES_NEW,调用ServiceB.methodB,ServiceA.methodA所在的事务就会挂起,ServiceB.methodB会起一个新的事务。

  1、如果ServiceB.methodB已经提交,那么ServiceA.methodA失败回滚,ServiceB.methodB是不会回滚的。
  2、如果ServiceB.methodB失败回滚,如果他抛出的异常被ServiceA.methodA的try..catch捕获并处理,ServiceA.methodA事务仍然可能提交;如果他抛出的异常未被ServiceA.methodA捕获处理,ServiceA.methodA事务将回滚。

三、ServiceA.methodA的事务级别为PROPAGATION_REQUIRED,ServiceB.methodB的事务级别为PROPAGATION_NESTED,调用ServiceB.methodB的时候,ServiceA.methodA所在的事务就会挂起,ServiceB.methodB会起一个新的子事务并设置savepoint。

  1、如果ServiceB.methodB已经提交,那么ServiceA.methodA失败回滚,ServiceB.methodB也将回滚。
  2、如果ServiceB.methodB失败回滚,如果他抛出的异常被ServiceA.methodA的try..catch捕获并处理,ServiceA.methodA事务仍然可能提交;如果他抛出的异常未被ServiceA.methodA捕获处理,ServiceA.methodA事务将回滚。

7.事务失效

1、spring的事务注解@Transactional只能放在public修饰的方法上才起作用,如果放在其他非public(private,protected)方法上,事务不起作用,因为Spring事务的实现是使用AOP来完成的,使用其他修饰符会导致生成代理类失败。

2.数据库必须是支持事务的,比如Mysql的数据引擎MyISAM就不支持事务。

3.只有抛出非受检异常(RuntimeException)事务才会回滚,但是抛出Exception,事务不回滚。可以通过(rollbackfor = Exception.class)来表示所有的Exception都回滚。

4.内部调用

  不带事务的方法调用该类中带事务的方法,不会回滚。因为spring的回滚是用过代理模式生成的,如果是一个不带事务的方法调用该类的带事务的方法,通过this.xxx()调用,而不生成代理事务,所以事务不起作用。常见解决方法,拆类。

@Service
public class EmployeeService {
 
    @Autowired
    private EmployeeDao employeeDao;
 
    public void save(){
        try {        
            // 此处this调用不会开启事务,数据会被保存
            this.saveEmployee();  
        }catch (Exception e){
            e.printStackTrace();
        }
    }
    
    @Transactional(propagation = Propagation.PROPAGATION_REQUIRED)
    // 此处无论是PROPAGATION_REQUIRED还是PROPAGATION_REQUIRES_NEW,事务均不生效
    public void saveEmployee(){
        Employee employee = new Employee();
        employee.setName("zhangsan");
        employee.setAge("26");
        employeeDao.save(employee);
        throw new RuntimeException();
    }
}

问题原因:
  在Spring中事务是通过AOP来实现的,AOP是根据被代理对象是否接口来选择使用JavaProxy 或 CGlib实现AOP,项目启动时,会扫描对应的注解来确定那些类和方法是需要增强的,只有被AOP代理增强过的方法才能实现事务,SpringIoC容器中返回的调用的对象是代理对象而不是真实的对象,这里使用的this调用的是真实对象,并非被增强过的代理对象。

解决方案:
  方法1、在方法A上开启事务,方法B不用事务或默认事务,并在方法A的catch中throw new RuntimeException(),这样使用的就是方法A的事务。(一定要throw new RuntimeException();否则异常被捕捉处理,同样不会回滚。)如下:

@Transactional() //开启事务
public void save(){
    try {        
        // 这里this调用会使事务失效,数据会被保存
        this.saveEmployee();  
    }catch (Exception e){
        e.printStackTrace();
        throw new RuntimeException();
    }
}

  方法2、方法A上可以不开启事务,方法B上开启事务,并在方法A中将this调用改成动态代理调用(AopContext.currentProxy()),如下:

public void save(){
    try {        
        EmployeeService proxy =(EmployeeService) AopContext.currentProxy();
        proxy.saveEmployee();
    }catch (Exception e){
        e.printStackTrace();
    }
}

你可能感兴趣的:(Spring事务的详解)