Spring中的事务总结

22.Spring事务总结

主要内容:并发问题,事务隔离级别,事务传播,事务超时,只读事务,异常处理

22.1 并发问题

一个数据库可以允许多个客户端同时访问,即并发的方式访问数据库。数据库中的同一个数据可能同时被多个事务访问,如果没有采取必要的隔离措施,就会导致各种并发问题,从而破坏数据的完整性。这些问题可以归为5类,包括3类数据读问题(脏读,不可得复读和幻象读)及两类数据更新问题(第一类丢失更新和第二类丢失更新)。下面对每个并发问题进行说明。

  1. 脏读(dirty read)

A事务读取了B事务尚未提交的更改数据,并且在这个数据基础上进行操作。如果此时恰巧B事务进行回滚,那么A事务读到的数据是根本不被承认的。以下是一个取款事务和转账事务并发时引起的脏读场景。

时间 转账事务A 取款事务B
T1   开始事务
T2 开始事务  
T3   查询账户余额为1000元
T4   取出500元,把余额改为500元
T5 查询账户余额为500元(脏读)  
T6   撤销事务,余额恢复为1000元
T7 汇入100元,余额改为600元  
T8 提交事务  

在这个场景中,B希望取款500元,而后有撤销了动作,而A往同一个账户转账100元,因为A事务读取了B事务尚未提交的数据,因而导致了账户白白丢失了500元。在Oracle数据中,不会发生脏读的情况。

  1. 不可重复读(unrepeatable read) 不可重复读是指A事务读取了B事务已经提交的更改数据。假设A在取款事务的过程中,B往该账户转账100元,A两次读取账户的余额发生不一致

时间 取款事务A 转帐事务B
T1   开始事务
T2 开始事务  
T3   查询账户余额为1000元
T4 查询账户余额为1000元  
T5   取出100元,把余额改为900元
T6   提交事务
T7 查询账户余额为900元  

在同一个事务中T4和T7时间点读取的账户存款余额不一致

  1. 幻象读(phantom read)

    A事务读取B提交的新增数据,这时A事务将出现幻想读的问题。幻读一般发生在计算统计数据的事务中。举个例子,假设银行系统在同一个事务中两次统计存款的总金额,在两次统计过程中,刚好新增了一个存款账户,并存入100元,这时两次统计的总金额将不一致。

时间 统计金额事务A 转帐事务B
T1   开始事务
T2 开始事务  
T3 统计存款总金额为10000元  
T4   新增一个存款账户,存款为100元
T5   提交事务
T6 再次统计存款总金额为10100元(幻象读)  

如果新增的数据刚好满足事务的查询条件,那么这个新数据就会进入事务的视野,因而导致两次统计结果不一致的情况。 幻读和不可重复读是两个容易混淆的概念,前者是指读到了其他事物已经提交的新增数据,而后者是读到了已经提交事务的更改数据(更改或删除)。为了避免这两种情况,采取的策略是不同的:防止读到更改数据,只需对操作的数据添加行级锁,阻止操作过程中的数据发送变化,而防止读到新增数据,则往往需要添加一个表级锁–将整张表锁定,防止新增数据(Oracle使用多版本数据的方式实现)

扫描二维码关注公众号,回复: 9986301 查看本文章
  1. 第一类丢失更新 A事务撤销时,把已经提交的B事务的更新数据覆盖了。这种错可能会造成很严重的问题。通过下面的账号取款转账就可以看出来。

时间 取款事务A 转账事务B
T1 开始事务  
T2   开始事务
T3 查询账号余额为1000元  
T4   查询账号余额为1000元
T5   汇入100元,把余额改为1100元
T6   提交事务
T7 取出100元,把余额改为900元  
T8 撤销事务  
T9 余额恢复为1000元(丢失更新)  

A事务在撤销时,“不小心”将B事务已经转入账号的金额给抹去了。

  1. 第二类丢失更新 A事务覆盖B事务已经提交的数据,造成B事务所操作丢失。

时间 转账事务A 取款事务B
T1   开始事务
T2 开始事务  
T3   查询账号余额为1000元
T4 查询余额为1000元  
T5   取出100元,把余额改为900元
T6   提交事务
T7 汇入100元,把余额改为1100元  
T8 提交事务  
T9 把余额改为1100元(丢失更新)  

在上面的例子,由于支票转账事务覆盖了取款事务对存款余额所做的更新,导致银行最后损失了100元,相反如果转账事务先提交,那么用户损失了100元。

22.2数据库锁机制

数据并发会引发很多问题,在一些场合下有些问题是允许的,但在另外一些场合下可能却是致命的。数据库通过锁的机制解决并发访问的问题,虽然不同的数据库在实现细节上存在差别,但原理基本上是一样的。

锁定的对象的不同,一般可以分为表锁定行锁定,前者对整个表进行锁定,而后者对表中特定行进行锁定。

从并发事务锁定的关系上看,可以分为共享锁定和独占锁定。共享锁定会防止独占锁定,但允许其它的共享锁定。而独占锁定既防止其它的独占锁定,也防止其它的共享锁定。为了更改数据,数据库必须在进行更改的行上施加行独占锁定,INSERT、UPDATE、DELETE和SELECT FOR UPDATE语句都会隐式采用必要的行锁定。

下面我们介绍一下数据库常用的5种锁定: 1.行共享锁: SELECT FOR UPDATE 语句隐式获取行共享锁 2.行独占锁: INSERT,UPDATE,DELETE 语句隐式获取,或者通过LOCK TABLE IN ROW EXCLUSIVE MODE 获取行独占锁 3.表共享锁: LOCK TABLE IN SHARE MODE 获取,防止其他独占锁获取,但是允许在表内拥有多个行共享锁和表共享锁 4.表共享行独占锁:LOCK TABLE IN SHARE ROW EXCLUSIVE MODE 获取 5.表独占锁 : LOCK TABLE IN EXCLUSIVE MODE

22.3事务隔离级别

尽管数据库为用户提供了锁的DML操作方式,但直接使用锁管理是非常麻烦的,因此数据库为用户提供了自动锁机制。只要用户指定会话的事务隔离级别,数据库就会分析事务中的SQL语句,然后自动为事务操作的数据资源添加上适合的锁。此外数据库还会维护这些锁,当一个资源上的锁数目太多时,自动进行锁升级以提高系统的运行性能,而这一过程对用户来说完全是透明的。 ANSI/ISO SQL 92标准定义了4个等级的事务隔离级别,在相同数据环境下,使用相同的输入,执行相同的工作,根据不同的隔离级别,可以导致不同的结果。不同事务隔离级别能够解决的数据并发问题的能力是不同的。 下表给出了 事务隔离级别对并发问题的解决情况

隔离级别 脏读 不可重复读 幻象读 第一类丢失更新 第二类丢失更新
READ UNCOMMITED 允许 允许 允许 不允许 允许
READ COMMITTED 不允许 允许 允许 不允许 允许
REPEATABLE READ 不允许 不允许 允许 不允许 不允许
SERIALIZABLE 不允许 不允许 不允许 不允许 不允许

事务的隔离级别和数据库并发性是对立的,两者此增彼长。一般来说,使用READ UNCOMMITED隔离级别的数据库拥有最高的并发性和吞吐量,而使用SERIALIZABLE隔离级别的数据库并发性最低。

SQL 92定义READ UNCOMMITED主要是为了提供非阻塞读的能力,Oracle虽然也支持READ UNCOMMITED,但它不支持脏读,因为Oracle使用多版本机制彻底解决了在非阻塞读时读到脏数据的问题并保证读的一致性,所以,Oracle的READ COMMITTED隔离级别就已经满足了SQL 92标准的REPEATABLE READ隔离级别。

SQL 92推荐使用REPEATABLE READ以保证数据的读一致性,不过用户可以根据应用的需要选择适合的隔离等级。

测试案例:通过设置事务的隔离级别来解决不可重复读的问题;

//  @Transactional(isolation=Isolation.REPEATABLE_READ)
    @Transactional(isolation=Isolation.READ_COMMITTED)
    @Override
    public Integer getPartsSum(int partsid) {
        //获取
        Integer count=pr.getPartsNum(partsid);
        System.out.println("count:"+count);
        try {
            Thread.sleep(15000);
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        //强制刷新缓存,或者在mapper文件中刷新;(mybatis默认使用了session级别的缓存)
    //  pr.updateByPartsId(101, 35);
        //再次获取数量
        count=pr.getPartsNum(partsid);
        System.out.println("count:"+count);
        return count;
    }
@Transactional
    @Override
    public void partsOut(int partsId, int num) {    
        Integer count=pr.getPartsNum(partsId);
        System.out.println("partsOut .count:"+count);
        System.out.println("开始修改");
        pr.updateByPartsId(partsId, num);
        count=pr.getPartsNum(partsId);
        System.out.println("partsOut .count:"+count);
        System.out.println("完成修改");
    }

22.4事务传播特性

当一个事务方法被另外一个事务方法调用时,这时两个方法中设置的事务的传播属性可以决定两个事务如何组合(合成一个,还是分别处理);

即被调用的事务与调用的事务如何协同处理;

例如,A.a1中调用了B.b1方法;

这个两个方的传播属性都是Required,那么,它们将合并成一个事务;

Spring在TransactionDefinition接口中定义了7种类型的事务传播行为,它们规定了事务方法和事务方法发生嵌套调用时事务如何进行传播;可以通过@Transactional(propagation=属性值)来设置;

事务传播行为类型 说明
PROPAGATION_REQUIRED 如果当前没有事务,就新建一个事务,如果已经存在一个事务中,加入到这个事务中。这是最常见的选择。
PROPAGATION_SUPPORTS 支持当前事务,如果当前没有事务,就以非事务方式执行。
PROPAGATION_MANDATORY 使用当前的事务,如果当前没有事务,就抛出异常。
PROPAGATION_REQUIRES_NEW 新建事务,如果当前存在事务,把当前事务挂起。
PROPAGATION_NOT_SUPPORTED 以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。
PROPAGATION_NEVER 以非事务方式执行,如果当前存在事务,则抛出异常。
PROPAGATION_NESTED 如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则执行与PROPAGATION_REQUIRED类似的操作

注意:如果是PROPAGATION_REQUIRES_NEW,即使在外部的Service处理了异常,那么外部事务和内部事务都是需要回退的。因为内部回退了,所有外部也要回退;

NESTED说明:如果单独运行则为REQUIRED事务,如果放在另一个事务中,则为NESTED设置一个保存点;这时,如果NESTED失败,外部事务可以捕获异常,并提交外部事务,如果外部事务失败,NESTED 也将失败;因此,它与REQUIRED_NEW和REQUIRED都是不同的;

当使用PROPAGATION_NESTED时,底层的数据源必须基于JDBC 3.0,并且实现者需要支持保存点事务机制。

//B事务方法,
@Service
public class PartsBillService {
    @Autowired
    PartsrepbillMapper prb;     
    @Transactional(propagation=Propagation.NESTED)
    public void save(Partsrepbill bill) {
        prb.insert(bill);
    }
}
@Service
public class PartsServiceImpl implements PartsService {
    @Autowired
    BizPartsrepertoryMapper pr; 
    @Autowired  //注入B事务所在的Service
    PartsBillService pbs;
    
    //指定rollbackFor=Exception后,所有异常都回退
    @Transactional(timeout=5,rollbackFor=Exception.class)//超时时间为2秒
    @Override
    public void partsOut(int partsId, int num) throws Exception {   
        //开始计时
        Integer count=pr.getPartsNum(partsId);
        System.out.println("partsOut .count:"+count);
​
        Partsrepbill bill=new Partsrepbill();
        bill.setBillcount(5);
        bill.setBillid(11);
        bill.setBillflag("I");
        bill.setBilltype("in1");
        bill.setPartsid(100);
        bill.setBilltime(new Date());
        bill.setBilluser(1);        
        try {
            pbs.save(bill);
        }catch(Exception e) {
            System.out.println("订单回退");
        }       
        
        System.out.println("开始修改");         
        pr.updateByPartsId(partsId, num);
        System.out.println("完成修改");     
        if(count>100) { //只为测试使用;
            //回退
            //throw new RuntimeException("ooo");
            //不回退,提交
            throw new Exception("ooo");
        }
        
        System.out.println("修改完成;"+count);
    }
}

22.5 设置回滚事务属性;

rollbackFor:回滚异常类型

noRollbackFor:不回滚异常类型;

如果不设置两个属性;则Error及运行时异常回滚,受检查异常不回滚;

rollbackFor和noRollbakcFor都可以配置,但最好配置成Exception,RuntimeException;如果这样配置,那么noRollbackFor会起作用

    //指定rollbackFor=Exception后,所有异常都回退
    @Transactional(timeout=2,rollbackFor=Exception.class)//超时时间为2秒
    @Override
    public void partsOut(int partsId, int num) throws Exception {   
        //开始计时
        Integer count=pr.getPartsNum(partsId);
        System.out.println("partsOut .count:"+count);
​
        System.out.println("开始修改");
        pr.updateByPartsId(partsId, num);
        System.out.println("完成修改");
        if(count>10) {
            //回退
            //throw new RuntimeException("ooo");
            //不回退,提交
            throw new Exception("ooo");
        }
        pr.updateByPartsId(partsId, num);
        System.out.println("修改完成;"+count);
    }

22.6 timeout超时

所谓事务超时,就是指一个事务所允许执行的最长时间,如果超过该时间限制但事务还没有完成,则自动回滚事务。在 TransactionDefinition 中以 int 的值来表示超时时间,其单位是秒。 默认设置为底层事务系统的超时值,如果底层数据库事务系统没有设置超时值,那么就是none,没有超时限制。

Spring事务超时 = 事务开始时到最后一个Statement创建时时间 + 最后一个Statement的执行完的时间(即其queryTimeout)。所以在在执行Statement之外的超时无法进行事务回滚

如果要让此属性生效,需要定义两个操作方法(例如 update),并在操作之间使用Thread.sleep(来进行测试)

    @Transactional(timeout=2)//超时时间为2秒
    @Override
    public void partsOut(int partsId, int num) {    
        //开始计时
        Integer count=pr.getPartsNum(partsId);
        System.out.println("partsOut .count:"+count);
        //休眠3秒,此时抛出异常
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        System.out.println("开始修改");
        pr.updateByPartsId(partsId, num);
        System.out.println("完成修改");
    }

22.7 readOnly

只读事务,如果没有修改的操作,会在运行时做一定的优化;

发布了12 篇原创文章 · 获赞 73 · 访问量 14万+

猜你喜欢

转载自blog.csdn.net/qqyb2000/article/details/104964494