实战分析:事务的隔离级别和传播属性(下)

在设置了TRANSACTION_READ_COMMITTED隔离级别的情况下,上述程序的运行结果为:


实战分析:事务的隔离级别和传播属性(下)


为了避免这种情况的发生,需要保证在同一个事务里面,多次重复读取的数据都是一致的,因此需要将事务的隔离级别从TRANSACTION_READ_COMMITTED提升到TRANSACTION_REPEATABLE_READ级别,这种情况下,上述程序的运行结果为:


实战分析:事务的隔离级别和传播属性(下)


幻读


官方文档对于幻读的定义如下:


The so-called phantom problem occurs within a transaction when the same query produces different sets of rows at different times. For example, if a SELECT is executed twice, but returns a row the second time that was not returned the first time, the row is a “phantom” row.

读到上一次没有返回的记录,看起来是幻影一般。


幻读与不可重复读类似。它发生在一个事务(T1)读取了几行数据,接着另一个并发事务(T2)插入了一些数据时。在随后的查询中,第一个事务(T1)就会发现多了一些原本不存在的记录,就好像发生了幻觉一样,所以称为幻读。为了解决这种情况,可以选择将事务的隔离级别提升到TRANSACTION_SERIALIZABLE。


什么是TRANSACTION_SERIALIZABLE?


TRANSACTION_SERIALIZABLE是当前事务隔离级别中最高等级的设置,可以完全服从ACID的规则,通过加入行锁的方式(innodb存储引擎中)来防止出现数据并发导致的数据不一致性问题。为了方便理解,可以看看下方的程序:


import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.util.concurrent.CountDownLatch;
/**
 * @author idea
 * @date 2019/7/2
 * @Version V1.0
 */
public class FantasyReadDemo {
    public static final String READ_SQL = "SELECT * FROM money";
    public static final String UPDATE_SQL = "UPDATE `money` SET `money` = ? WHERE `id` = 3;\n";
    public CountDownLatch countDownLatch=new CountDownLatch(2);
    public void readAndUpdate1() {
        try (Connection conn = JdbcUtil.getConnection();) {
            conn.setAutoCommit(false);
            PreparedStatement ps = conn.prepareStatement(READ_SQL);
            conn.setTransactionIsolation(Connection.TRANSACTION_SERIALIZABLE);
            ResultSet rs = ps.executeQuery();
            rs.next();
            int currentMoney = (int) rs.getObject(2);
            System.out.println("执行写取数据操作----" + currentMoney);
            //堵塞等待唤醒
            countDownLatch.countDown();
            PreparedStatement writePs = conn.prepareStatement(UPDATE_SQL);
            writePs.setInt(1, currentMoney - 1);
            writePs.execute();
            conn.commit();
            writePs.close();
            ps.close();
            System.out.println("执行写操作结束---1");
        } catch (Exception e) {
            e.printStackTrace();
            readAndUpdate1();
        }
    }
    public void readAndUpdate2() {
        try (Connection conn = JdbcUtil.getConnection();) {
            conn.setAutoCommit(false);
            PreparedStatement ps = conn.prepareStatement(READ_SQL);
            conn.setTransactionIsolation(Connection.TRANSACTION_SERIALIZABLE);
            ResultSet rs = ps.executeQuery();
            rs.next();
            int currentMoney = (int) rs.getObject(2);
            System.out.println("执行写取数据操作----" + currentMoney);
            //堵塞唤醒
            countDownLatch.countDown();
            PreparedStatement writePs = conn.prepareStatement(UPDATE_SQL);
            writePs.setInt(1, currentMoney - 1);
            writePs.execute();
            conn.commit();
            writePs.close();
            ps.close();
            System.out.println("执行写操作结束---2");
        } catch (Exception e) {
            //使用串行化事务级别能够较好的保证数据的一致性,可串行化事务 serializable 是事务的*别,在每个读数据上加上锁
            //innodb里面是加入了行锁,因此出现了异常的时候,只需要重新执行一遍事务即可。
            e.printStackTrace();
            readAndUpdate2();
        }
    }
    public void fantasyRead() {
        Thread thread1 = new Thread(new Runnable() {
            @Override
            public void run() {
                readAndUpdate1();
            }
        });
        Thread thread2 = new Thread(new Runnable() {
            @Override
            public void run() {
                readAndUpdate2();
            }
        });
        try {
            thread1.start();
//            Thread.sleep(500);
            thread2.start();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    public static void main(String[] args) {
        FantasyReadDemo fantasyReadDemo = new FantasyReadDemo();
        fantasyReadDemo.fantasyRead();
    }
}


这里面将事务的隔离级别设置到了TRANSACTION_SERIALIZABLE,但是在运行过程中为了保证数据的一致性,串行化级别的事物会给相应的行数据加入行锁,因此在执行的过程中会抛出下面的相关异常:


com.mysql.jdbc.exceptions.jdbc4.MySQLTransactionRollbackException: Deadlock found when trying to get lock; try restarting transaction
    at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
    at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
    at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
    at java.lang.reflect.Constructor.newInstance(Constructor.java:423)
    at com.mysql.jdbc.Util.handleNewInstance(Util.java:377)
    .......


这里为了方便演示,在抛出异常的时候重新再次执行了一遍事务的方法,从而完成多次事务并发执行。


但是实际应用场景中,我们对于这种并发状态造成的问题都会交给业务层面加入锁来解决冲突,因此TRANSACTION_SERIALIZABLE隔离级别一般在应用场景中比较少见。


七种事务的传播机制


事务的七种传播机制分别为:


REQUIRED(默认) 默认的事务传播机制,如果当前不支持事务,那么就创建一个新的事务。


SUPPORTS 表示支持当前的事务,如果当前没有事务,则不会单独创建事务

以上的这两种事务传播机制比较好理解,接下来的几种事务传播机制就比上边的这几类稍微复杂一些了。


REQUIRES_NEW


定义: 创建一个新事务,如果当前事务已经存在,把当前事务挂起。


为了更好的理解REQUIRES_NEW的含义,我们通过下边的这个实例来进一步理解:

有这么一个业务场景,需要往数据插入一个account账户信息,然后同时再插入一条userAccount的流水信息。(只是模拟场景,所以对象的命名有点简陋)


直接来看代码实现,内容如下所示:


/**
 * @author idea
 * @data 2019/7/6
 */
@Service
public class AccountService {
    @Autowired
    private AccountDao accountDao;
    @Autowired
    private UserAccountService userAccountService;
    /**
     * 外层定义事务, userAccountService.saveOne单独定义事务
     *
     * @param accountId
     * @param money
     */
    @Transactional(propagation = Propagation.REQUIRED)
    public void saveOne(Integer accountId, Double money) {
        accountDao.insert(new Account(accountId, money));
        userAccountService.saveOne("idea", 1001);
        //这里模拟抛出异常
        int j=1/0;
    }
}


再来看userAccountService.saveOne函数:


/**
 * @author idea
 * @data 2019/7/6
 */
@Service
public class UserAccountService {
    @Autowired
    private UserAccountDao userAccountDao;
    /**
     * @param username
     * @param accountId
     */
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void saveOne(String username,Integer accountId){
        userAccountDao.insert(new UserAccount(username,accountId));
    }
}


执行程序的时候,AccountService.saveOne里面的 userAccountService.saveOne函数为单独定义的一个事务,而且传播属性为REQUIRES_NEW。因此在执行外层函数的时候,即使后边抛出了异常,也并不会影响到内部 userAccountService.saveOne的函数执行。


REQUIRES_NEW 总是新启一个事务,这个传播机制适用于不受父方法事物影响的操作,比如某些业务场景下需要记录业务日志,用于异步反查,那么不管主体业务逻辑是否完成,日志都需要记录下来,不能因为主体业务逻辑报错而丢失日志;但是本身是一个单独的事物,会受到回滚的影响,也就是说 userAccountService.saveOne里面要是抛了异常,子事务内容一起回滚。


NOT_SUPPORTED


定义:无事务执行,如果当前事务不存在,把已存在的当前事务挂起。


还是接上边的代码来进行试验:


账户的转账操作:


实战分析:事务的隔离级别和传播属性(下)


在执行的过程中,userAccountService.saveOne抛出了异常,但是由于该方法申明的事物传播属性为NOT_SUPPORTED级别,因此当子事务内部抛出异常的时候,子事务本身不会回滚,而且也不会影响父类事务的执行。 


NOT_SUPPORTED可以用于发送提示消息,站内信、短信、邮件提示等。不属于并且不应当影响主体业务逻辑,即使发送失败也不应该对主体业务逻辑回滚,并且执行过程中,如果父事务出现了异常,进行回滚,也不会影响子类的事务

NESTED


定义:嵌套事务,如果当前事务存在,那么在嵌套的事务中执行。如果当前事务不存在,则表现跟REQUIRED一样。


关于Nested的定义,我个人感觉网上写的比较含糊,所以自己通过搭建Demo来强化理解,还是原来的例子,假设说父类事务执行的过程中抛出了异常如下,那么子类也要跟着回滚:


实战分析:事务的隔离级别和传播属性(下)


当父事务出现了异常之后,进行回滚,子事务也会被牵扯进来一起回滚。


MANDATORY


定义:MANDATORY单词中文翻译为强制,支持使用当前事务,如果当前事务不存在,则抛出Exception。


这个比较好理解


实战分析:事务的隔离级别和传播属性(下)


当子方法定义了事务,且事务的传播属性为MANDATORY级别的时候,如果父方法没有定义事务操作的话,就会抛出异常。(此时的子方法会将数据记录到数据库里面)


NEVER


定义:当前如果存在事务则抛出异常


实战分析:事务的隔离级别和传播属性(下)


在执行userAccountService.saveOne函数的时候,发现父类的方法定义了事务,因此会抛出异常信息,并且userAccountService.saveOne会回滚。


传播属性小结:


PROPAGATION_NOT_SUPPORTED


不会受到父类事务影响而回滚,自己也不会影响父类函数,出现异常后会自动回滚。


PROPAGATION_REQUIRES_NEW


不会受到父类事务影响而回滚,自己也不会影响父类函数,出现异常后会自动回滚。


NESTED


会受到父类事务影响而回滚,出现异常后自身也回滚。如果不希望影响父类函数,那么可以通过使用try catch来控制操作。


MANDATORY


强制使用当期的事物,如果当前的父类方法没有事务,那么在处理数据的时候就会抛出异常


NEVER


当前如果存在事务则抛出异常


REQUIRED(默认) 默认的事务传播机制,如果当前不支持事务,那么就创建一个新的事务。


SUPPORTS 表示支持当前的事务,如果当前没有事务,则不会单独创建事务


本文的全部相关代码都已经上传到gitee上边了,欢迎感兴趣的朋友前往进行代码下载:


https://gitee.com/IdeaHome_admin/wfw



上一篇:删除容器报错:Error response from daemon: conflict: unable to delete


下一篇:expdp导出报ORA-39181处理方法