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();
    }
}
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342