Spring事务管理

前言

Github:https://github.com/yihonglei/thinking-in-spring(spring-transaction工程)

一 事务特性

了解Spring事务前,有必要先了解下事务的ACID特性。

1、原子性(atomacity)

整个事务中的所有操作,要么全部成功,要么全部失败,任意一个失败都会导致整个事务回滚,

不会出现部分执行成功部分执行失败的情况。

2、一致性(consistency)

数据库总是从一个一致性状态转到另外一个一致性状态,事务结束后系统数据状态是一致的。

3、隔离性(isolation)

在并发执行时,一个事务对另外一个事务是彼此独立的,事务之间能否相互看到数据状态,

是我们需要讨论的隔离级别问题。

4、持久性(durability)

事务一旦提交,数据永久保存在数据库中。

二 事务隔离级别

1、Read uncommitted(未提交读)

一个事务在没有提交的时候对另外一个事务可见,从而导致事务读取到未提交的数据,

所以也叫脏读,这个隔离级别都是问题,一般实际不用。

2、Read committed(已提交读)

一个事务在没有提交前对另外一个事务是不可见的,会导致事务提交前后读取的数据不一样,

两次查询结果不一样,如果拿这样的数据去处理业务,一般就是完蛋了,所以也叫做不可重复读,

这是一个警告语。这个也是一般数据库的默认级别,但不是MySQL的默认级别。

3、Repeatable read(可重复读)

可重复读能保证多次读取数据一致,但是解决不了你正在读的时候,卡巴一下中间给来一条数据,

结果读出的数据不对,这个也成为幻读。这个是MySQL的事务默认隔离级别。

4、Serializable(可串行化)

可串行化,这是事务的最高级别隔离。都是排队执行,就不会出现脏读,不可重复读,幻读等问题,

但是效率极低,实际中一般也不会用。

三 脏读、不可重复读、幻读

1、脏读

一个事物读取到另一事物未提交的更新数据

2、不可重复读

在同一事物中,多次读取同一数据返回的结果有所不同, 换句话说, 后续读取可以读到另一事物已提交的更新数据.

相反, “可重复读”在同一事物中多次读取数据时, 能够保证所读数据一样, 也就是后续读取不能读到另一事物已提交的更新数据。

3、幻读

 查询表中一条数据如果不存在就插入一条,并发的时候却发现,里面居然有两条相同的数据。这就幻读的问题。

四 数据库默认隔离级别

Oracle中默认级别是 Read committed,mysql 中默认级别 Repeatable read。

另外要注意的是mysql 执行一条查询语句默认是一个独立的事物,所以看上去效果跟Read committed一样。

# 查看mysql 的默认隔离级别

SELECT @@tx_isolation

五 代码实例

1、案例说明

添加用户时添加账户,模拟事务场景,观察事务的控制情况。

2、表结构

CREATE TABLE `user` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `userName` varchar(255) COLLATE utf8_bin NOT NULL,
  `createTime` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

CREATE TABLE `account` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `userName` varchar(255) COLLATE utf8_bin NOT NULL,
  `accountName` varchar(255) NOT NULL,
  `money` int(11) NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

3、用户

UserService接口

package com.jpeony.transaction.service;

public interface UserService {

    void createUser(String userName);

    void a(String userName);

    void b(String userName, int initMoney);
}

UserServiceImpl实现

package com.jpeony.transaction.service.impl;

import com.jpeony.transaction.service.AccountService;
import com.jpeony.transaction.service.UserService;
import org.springframework.aop.framework.AopContext;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

import java.text.SimpleDateFormat;
import java.util.Date;

/**
 * 用户
 *
 * @author yihonglei
 */
@Service
public class UserServiceImpl implements UserService {
    @Autowired
    AccountService accountService;
    @Autowired
    JdbcTemplate jdbcTemplate;


    /**
     * 事务情况:
     * 1)createUser方法开启事务,插入用户。
     * 2)addAccount方法新开一个事务(Propagation.REQUIRES_NEW),然后抛异常。
     * 

* 执行效果: * 插入用户会失败,添加账户成功,因为addAccount是一个新事务,不受createUser事务控制。 * * @author yihonglei */ @Override @Transactional(propagation = Propagation.REQUIRED) public void createUser(String userName) { // 插入user 记录 jdbcTemplate.update("INSERT INTO `user` (userName) VALUES(?)", userName); // 调用 accountService 添加帐户 accountService.addAccount(userName, 10000); // 人为报错,测试新事务效果 // int i = 1 / 0; } /** * 使用事务非常容易犯以下错误: * 1)在同一个类中,a调用b,在b上面新开了一个事务,以为b上的事务生效了,其实是不生效的, * 因为事务基于aop实现,是通过代理对象完成事务包裹的,必须通过代理对象调用,事务才会生效, * 同一个类中,a调b,相当于this.a,只是一个普通对象调用,没有事务,很多人容易犯这个错误, * 老坑了。 * 2)事务方法必须为public的,否则事务也不会生效。 * * @author yihonglei */ @Override @Transactional(propagation = Propagation.REQUIRED) public void a(String userName) { // 插入user 记录 jdbcTemplate.update("INSERT INTO `user` (userName) VALUES(?)", userName); // 事务未生效:同类中a直接调用b方法,相当于b的代码直接写在a里面,事务要基于代理对象调用才有效,所以b上的事务是无效的。 // b(userName, 10000); // 事务生效:获取代理对象,基于代理对象调用,事务才能生效。 UserService proxy = (UserService) AopContext.currentProxy(); proxy.b(userName, 10000); // 人为报错 int i = 1 / 0; } @Override @Transactional(propagation = Propagation.REQUIRES_NEW) public void b(String userName, int initMoney) { // 插入account记录 String accountName = new SimpleDateFormat("yyyyMMddhhmmss").format(new Date()); jdbcTemplate.update("insert INTO account (accountName,userName,money) VALUES (?,?,?)", accountName, userName, initMoney); } }

4、账户

AccountService接口

package com.jpeony.transaction.service;

public interface AccountService {

    void addAccount(String userName, int initMenoy);

}

AccountServiceImp实现

package com.jpeony.transaction.service.impl;

import com.jpeony.transaction.service.AccountService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

import java.text.SimpleDateFormat;
import java.util.Date;

/**
 * 账户
 *
 * @author yihonglei
 * @date 2019/1/14 10:21
 */
@Service
public class AccountServiceImpl implements AccountService {
    @Autowired
    JdbcTemplate jdbcTemplate;

    @Override
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void addAccount(String userName, int initMoney) {
        String accountName = new SimpleDateFormat("yyyyMMddhhmmss").format(new Date());
        jdbcTemplate.update("INSERT INTO account (accountName,userName,money) VALUES (?,?,?)", accountName, userName, initMoney);

        // 人为报错抛异常,使用事务时,事务回滚,插入失败,如果不使用事务,则插入成功。
//        int i = 1/0;
    }

}

5、Spring配置

applicationContext.xml(数据源,jdbc,事务管理配置等等)




    

    
    

    
    

    
    
        
        
        
    

    
    
        
    

    
    
        
    

    
    

6、测试代码

TransactionTest

package com.jpeony.spring;

import com.jpeony.transaction.service.AccountService;
import com.jpeony.transaction.service.UserService;
import org.junit.Test;
import org.springframework.context.support.ClassPathXmlApplicationContext;

/**
 * 事务测试
 *
 * @author yihonglei
 * @date 2019/1/15 11:34
 */
public class TransactionTest {

    /**
     * 测试有无事务的效果。
     *
     * @author yihonglei
     * @date 2019/1/15 11:34
     */
    @Test
    public void addAccount() {
        ClassPathXmlApplicationContext context =
                new ClassPathXmlApplicationContext("applicationContext.xml");
        AccountService service = context.getBean(AccountService.class);
        service.addAccount("yihonglei", 10000);
    }

    /**
     * 测试新开一个事务的效果。
     *
     * @author yihonglei
     * @date 2019/1/15 11:34
     */
    @Test
    public void createUser() {
        ClassPathXmlApplicationContext context =
                new ClassPathXmlApplicationContext("applicationContext.xml");
        UserService service = context.getBean(UserService.class);
        service.createUser("yihonglei");
    }

    /**
     * 演示事务失效效果。
     * 
     * @author yihonglei
     * @date 2019/1/15 11:38
     */
    @Test
    public void invalidate() {
        ClassPathXmlApplicationContext context =
                new ClassPathXmlApplicationContext("applicationContext.xml");
        UserService service = context.getBean(UserService.class);
        service.a("yihonglei");
    }

}

7、代码说明

1)有事务和无事务的区别

无事务情况:

把AccountServiceImpl的addAccount方法上的事务“注释掉”,同时打开“人工报错代码”,如下:

    @Override
//    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void addAccount(String userName, int initMoney) {
        String accountName = new SimpleDateFormat("yyyyMMddhhmmss").format(new Date());
        jdbcTemplate.update("INSERT INTO account (accountName,userName,money) VALUES (?,?,?)", accountName, userName, initMoney);

        // 人为报错抛异常,使用事务时,事务回滚,插入失败,如果不使用事务,则插入成功。
        int i = 1/0;
    }

然后,运行TransactionTest中的addAccount方法,程序报错,但是数据库account表数据已经插入成功,

因为mysql默认自动提交,insert语句执行直接插入成功,下面的报错对insert无影响。

有事务情况:

打开事务注解,添加事务,如下:

@Override
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void addAccount(String userName, int initMoney) {
        String accountName = new SimpleDateFormat("yyyyMMddhhmmss").format(new Date());
        jdbcTemplate.update("INSERT INTO account (accountName,userName,money) VALUES (?,?,?)", accountName, userName, initMoney);

        // 人为报错抛异常,使用事务时,事务回滚,插入失败,如果不使用事务,则插入成功。
        int i = 1/0;
    }

把account表数据清空,然后运行TransactionTest中的addAccount方法,程序报错,account表数据插入失败,

因为事务里面的操作,要么全部成功,要么全部失败,任意一个失败,会导致事务的回滚,所以插入被回滚,

插入失败。

2)新建事务场景

UserServiceImpl的createUser方法调用AccountServiceImpl的addAccount方法,

createUser上有事务,addAccount方法新建一个事务,同时createUser方法底部人工抛异常。

UserServiceImpl的createUser:

@Override
    @Transactional(propagation = Propagation.REQUIRED)
    public void createUser(String userName) {
        // 插入user 记录
        jdbcTemplate.update("INSERT INTO `user` (userName) VALUES(?)", userName);
        // 调用 accountService 添加帐户
        accountService.addAccount(userName, 10000);
        // 人为报错,测试新事务效果
         int i = 1 / 0;
    }

AccountServiceImpl的addAccount:

@Override
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void addAccount(String userName, int initMoney) {
        String accountName = new SimpleDateFormat("yyyyMMddhhmmss").format(new Date());
        jdbcTemplate.update("INSERT INTO account (accountName,userName,money) VALUES (?,?,?)", accountName, userName, initMoney);

        // 人为报错抛异常,使用事务时,事务回滚,插入失败,如果不使用事务,则插入成功。
//        int i = 1/0;
    }

清空user,account表,然后运行TransactionTest的createUser方法,user表没有数据,account表有数据,

因为addAccount新开一个事务插入数据,当addAccount执行完后,事务已经提交,保存到数据库,

而createUser并没有提交,因为代码没有执行完,当程序抛出错误后,整个createUser方法事务回滚,

user表数据回滚,所以出现user没有数据,而account已经插入成功的现象。

这里顺便说下,如果addAccount打开人工抛错,则user,account均没有数据,

因为addAccount抛错,数据回滚,但是错误没有捕获,错误往上抛到createUser方法中,导致createUser

所在方法事务回滚。

3)事务失效场景

先上代码看效果,然后再分析为毛失效了。

UserServiceImpl的a方法调整为如下:

@Override
    @Transactional(propagation = Propagation.REQUIRED)
    public void a(String userName) {
        // 插入user 记录
        jdbcTemplate.update("INSERT INTO `user` (userName) VALUES(?)", userName);

        // 事务未生效:同类中a直接调用b方法,相当于b的代码直接写在a里面,事务要基于代理对象调用才有效,所以b上的事务是无效的。
         b(userName, 10000);

        // 事务生效:获取代理对象,基于代理对象调用,事务才能生效。
//        UserService proxy = (UserService) AopContext.currentProxy();
//        proxy.b(userName, 10000);

        // 人为报错
        int i = 1 / 0;
    }

UserServiceImpl的b方法如下:

@Override
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void b(String userName, int initMoney) {
        // 插入account记录
        String accountName = new SimpleDateFormat("yyyyMMddhhmmss").format(new Date());
        jdbcTemplate.update("insert INTO account (accountName,userName,money) VALUES (?,?,?)", accountName, userName, initMoney);
    }

a方法添加事务,b方法新开事务,在a方法里面调完b的时候人工抛错,

a方法的user插入失败,b方法的account插入成功,因为b是新开的事务嘛,

即user为空,account有数据,实际效果是这样的吗?

清空user,account表,运行TransactionTest的invalidate方法,见证奇迹的时刻。

程序运行完后,我们发现user,account两个表都没数据,这是为毛啊?

因为,事务是aop实现,基于代理对象调用,代理对象做了事务的处理,

这里没有生效,是因为a里面调用b,等同于this.b,即是当前对象,一个普通的对象

调用b方法,等同于b这段代码直接扔在a里面,也就是说,b的代码与a的代码同属于

一个事务,当a抛异常时,a整个事务全部回滚,即user,account的插入都回滚了,

所以user,account都没有数据。

 

简单来说,同一个类里面a事务方法直接调用b方法,b上的事务是失效,如果要想使事务生效,

必须保证调用b方法的对象不是一个普通对象,而是一个包含事务处理的代理对象。

基于这个思路,我们可以通过获取UserService的代理对象,然后再调用b方法,

这样b上的事务就生效了。a方法中通过AopContext获取当前对象的代理对象,如下:

@Override
    @Transactional(propagation = Propagation.REQUIRED)
    public void a(String userName) {
        // 插入user 记录
        jdbcTemplate.update("INSERT INTO `user` (userName) VALUES(?)", userName);

        // 事务未生效:同类中a直接调用b方法,相当于b的代码直接写在a里面,事务要基于代理对象调用才有效,所以b上的事务是无效的。
//         b(userName, 10000);

        // 事务生效:获取代理对象,基于代理对象调用,事务才能生效。
        UserService proxy = (UserService) AopContext.currentProxy();
        proxy.b(userName, 10000);

        // 人为报错
        int i = 1 / 0;
    }

再次运行TransactionTest的invalidate方法,程序同样抛错了,你会发现,user还是没有数据,但是account有数据了,

说明account上的新开事务生效了。

一般不建议通过AopContext实现,也不建议用自注入的方式实现,即在UserServiceImpl中来个

@Autowired

private UserSerivce userService;

a中通过userService.b调用b方法,这样可能会出现循环依赖,同时程序可读性太差,

不理解事务本质是要基于代理对象实现,很容易懵逼。建议处理这种情况的最好方式

就是像createUser调用addAccount一样,新建一个Service,然后通过a所在service

调用b所在service,即像createUser调用addAccount方法一样。

六 事务总结

1、事务特性很重要,了解事务特性,是理解Spring事务的基础。

2、Spring事务基于Aop实现,底层通过jdk或cglib动态代理技术生成代理对象。

3、注意上面说到的事务失效场景,是最容易犯错的,新手容易翻车。

你可能感兴趣的:(#,---Spring基础,Thinking,In,Spring)