Java工程师面试1000题146-Spring支持的数据库事务传播属性和隔离级别

146、简介一下Spring支持的数据库事务传播属性和隔离级别

介绍Spring所支持的事务和传播属性之前,我们先了解一下SpringBean的作用域,与此题无关,仅做一下简单记录。

在Spring中,可以在元素的scope属性中设置bean的作用域,来决定这个bean是单实例的还是多实例的。默认情况下,Spring只为每个在IOC容器里声明的bean创建唯一的实例,整个IOC容器范围内都可以共享该实例;所有后续的getBean()调用和bean引用都将返回这个唯一的bean实例,该作用域被称为singleton,他是所有bean的默认作用域。

  1. singleton:在SpringIOC容器中仅存在一个bean实例,Bean以单实例的方式存在
  2. prototype:每次调用getBean()时都会返回一个新的实例
  3. request:每次HTTP请求都会创建一个新的Bean。该作用域仅适用于WebApplicationContext环境。
  4. session:同一个HTTP Session共享一个Bean,不同的HTTP Session使用不同的Bean。该作用域仅适用于WebApplicationContext环境。

介绍完Spring Bean的作用域之后,下面开始进入正题——Spring支持的数据库事务传播属性和隔离级别

1、事务的传播属性

首先我们先了解一下什么是事务的传播属性(传播行为):当一个事务方法被被另一个事务方法调用时,必须指定事务应该如何传播。例如:方法可能继续在现有事务中运行,也可能开启一个新事务,并在自己的事务中运行。

事务的传播行为是由传播属性来指定的。

propagation:用来设置事务的传播行为:一个方法运行在了一个开启了事务的方法中时,当前方法是使用原来的事务,还是开启一个新的事务,这就是事务的传播行为。

比如:Propagation.REQUIRED:默认值,代表继续使用原来的事务;Propagation.REQUIRES_NEW:将原来的事务挂起,开启一个新的事务。最常用的事务传播属性就是REQUIRED和REQUIRES_NEW,下面就通过编程来进行测试。

首先,在数据库里面新建三张表:

CREATE DATABASE /*!32312 IF NOT EXISTS*/`location` /*!40100 DEFAULT CHARACTER SET utf8 */;

USE `location`;

DROP TABLE IF EXISTS `account`;
CREATE TABLE `account` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `username` varchar(30) DEFAULT NULL,
  `balance` float unsigned DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;
insert  into `account`(`id`,`username`,`balance`) values (1,'HanZong',100);

DROP TABLE IF EXISTS `book`;
CREATE TABLE `book` (
  `isbn` varchar(20) DEFAULT NULL,
  `name` varchar(20) DEFAULT NULL,
  `price` float DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
insert  into `book`(`isbn`,`name`,`price`) values ('1001','Spring',60),('1002','SpringMVC',50);

DROP TABLE IF EXISTS `book_stock`;
CREATE TABLE `book_stock` (
  `isbn` varchar(20) DEFAULT NULL,
  `stock` int(11) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
insert  into `book_stock`(`isbn`,`stock`) values ('1001',100),('1002',100);

然后,搭建Spring的开发环境,具体配置在这里不再讲解了,不是本知识点的重点。然后新建三个接口,三个实现类。

Cashier接口:

import java.util.List;

public interface Cashier {
    //去结账的方法
    void checkout(int userId, List isbns);
}
实现类:

import com.spring.transaction.BookShopService;
import com.spring.transaction.Cashier;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;

@Service("cashier")
public class CashierImpl implements Cashier {

    @Autowired
    private BookShopService bookShopService;

    @Transactional
    @Override
    public void checkout(int userId, List isbns) {
        for (String isbn : isbns){
            //调用BookShopService中的买东西方法
            bookShopService.purchase(userId,isbn);
        }
    }
}
BookShopService接口:
public interface BookShopService {
    //定义一个买东西方法
    void purchase(int userId,String isbn);
}

实现类:

import com.spring.transaction.BookShopDao;
import com.spring.transaction.BookShopService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
public class BookShopServiceImpl implements BookShopService {
    
    @Autowired
    private BookShopDao bookShopDao;
    
    @Transactional
    @Override
    public void purchase(int userId, String isbn) {		
        //1.获取要买的图书的价格
        double bookPrice = bookShopDao.getBookPriceByIsbn(isbn);
		System.out.println(bookPrice);
        //2.更新图书的库存
        bookShopDao.updateBookStock(isbn);
        //3.更新用户的余额
        bookShopDao.updateAccountBalance(userId, bookPrice);
		double bookPriceByIsbn = bookShopDao.getBookPriceByIsbn(isbn);
		System.out.println(bookPriceByIsbn);
    }
}

操作数据库的接口:

public interface BookShopDao {
	
	//根据书号查询图书的价格
	double getBookPriceByIsbn(String isbn);
	
	//根据书号更新图书的库存,每次只买一本图书
	void updateBookStock(String isbn);
	
	//根据用户的id和图书的价格更新用户的账户余额
	void updateAccountBalance(int userId, double bookPrice);
}

实现类:


import com.spring.transaction.BookShopDao;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;

@Repository("bookShopDao")
public class BookShopDaoImpl implements BookShopDao {

	@Autowired
	private JdbcTemplate jdbcTemplate;

	@Override
	public double getBookPriceByIsbn(String isbn) {
		// 写sql语句
		String sql = "select price from book where isbn = ?";
		// 调用JdbcTemplate中的queryForObject方法
		Double bookPrice = jdbcTemplate.queryForObject(sql, Double.class, isbn);
		return bookPrice;
	}

	@Override
	public void updateBookStock(String isbn) {
		// 写sql语句
		String sql = "update book_stock set stock = stock - 1 where isbn = ?";
		// 调用JdbcTemplate中的update方法
		jdbcTemplate.update(sql, isbn);
	}

	@Override
	public void updateAccountBalance(int userId, double bookPrice) {
		// 写sql语句
		String sql = "update account set balance = balance - ? where id = ?";
		// 调用JdbcTemplate中的update方法
		jdbcTemplate.update(sql, bookPrice, userId);
	}
}

介绍一下上面的接口和实现类,BookShopService接口里面有一个买东西的方法purchase(),Cashier里面有一个checkout()方法,结账的方法,checkout()方法要调用purchase()方法来实现功能,checkout()方法上面添加了声明式事务注解@Transactional,purchase()方法上面也添加了声明式事务注解@Transactional。checkout()方法调用了purchase()方法,两个方法都使用了事务,这时候在运行的时候,purchase()到底是使用自己的事务呢,还是使用checkout()的事务呢?这个就属于事务的传播行为!

事务的传播行为可以使用@Transactional注解里面的一个propagation属性来设置。propagation可以设置以下7种属性值。

Java工程师面试1000题146-Spring支持的数据库事务传播属性和隔离级别_第1张图片

我们来看一下啊,purchase()方法运行在checkout()方法里面,按照Spring默认的事务传播属性为REQUIRED,那么purchase()方法就应该使用checkout()方法的事务,checkout()方法里面有一个for循环,可能会调用多次purchase方法,根据事务的原子性,多次执行purchase()方法要么全部成功,要么全部失败。我们写一个测试方法:

public class TestTX {

    //创建IOC容器对象
    ApplicationContext ioc = new ClassPathXmlApplicationContext("applicationContext.xml");

    @Test
    public void testCashier(){
        Cashier cashier = (Cashier) ioc.getBean("cashier");
        //创建List
        List isbns = new ArrayList<>();
        isbns.add("1001");
        isbns.add("1002");
        //去结账
        cashier.checkout(1,isbns);
    }
}

测试程序中,我们创建一个ArrayList,里面添加两个图书id,一个是1001,一个是1002,代表我们将要购买的图书,图书的价格保存在数据库中book表中,1001的价格是60,1002的价格是50,另一张表book_store里面存放的是图书库存,1001库存100本,1002库存100本,最后一张表是用户表account,里面就只有一个用户,用户余额100元,这个余额在建表的时候必须要设置为unsigned的,不能成为负数,否则就没法测试了。

现在是账户余额只有100元,要同时买两本1001和1002,明显差10元。现在我们来测试一下,到底是一本也买不成功,还是可以买成功一本。根据事务传播行为,没有设置就代表默认值,默认值就是REQUIRED:如果有事务在运行,当前的方法就在这个事务内运行,否则就开启一个新的事务,并在自己的事务内运行。也就是说,现在买1001和买1002在同一个事务里面,根据事务的原子性,要么都完成,要么都不完成,现在我的余额是100,可以买成功1001,不能卖成功1002,到底最终的结果是什么呢?让我们运行测试程序。报了一个异常:

Caused by: com.mysql.jdbc.MysqlDataTruncation: Data truncation: Out of range value for column 'balance' at row 1

这句报异常就是由于在建表的时候把balance设置为unsigned的,使之不能成为负数。这不是我们关心的,我们关心的是数据库中库存和账户余额是否发生变化。我们刷新数据库表,发现余额没有改变,两本书都没有买成功。为什么会这样呢?我们再来分析一下。如果事务的传播行为是默认值的话,即我们没有在@Transactional注解里面设置,默认值就是REQUIRED,也就说是会使用checkout()方法原来的事务,虽然我们在purchase()上面也添加了事务,但是由于事务的传播行为是默认值,所以他会使用checkout()方法的事务,如果使用checkout()方法的事务,我们发现,在ArrayList里面有两本图书,买两本书调用的都是同一个purchase()方法,两次调用是在同一个事务里面,但是买完1001之后,再去买1002,失败了,根据事务的原子性,要么都完成,要么都不完成,所以,它要回滚事务,最终才造成了上面的结果。

那么,我们能不能让它买成功一本呢?可以,只需要把purchase()方法的事务传播行为改为REQUIRES_NEW。

@Transactional(propagation = Propagation.REQUIRES_NEW)

同样运行测试程序,还是报“Data truncation: Out of range value for column 'balance' at row 1”异常,不管他,我们刷新数据库,观察账户余额,发现变为了40,再看一下库存,1001的库存变为了99,1002的库存没有变还是100。这就说明我们买成功了一本。由于我们把purchase()方法的事务传播行为改为REQUIRES_NEW,就是每次调用都要开启一个新事物,虽然checkout()也设置了事务,但是我不用你的,每次都用我自己的,这就是事务之间的隔离性,互相之间没有影响,所以我们买1001和买1002的时候用到的就不是同一个事务了,购买1002失败不会导致购买1001也失败。所以最终的结果就是1001买成功了,1002没有买成功。

小总结:

  • REQUIRED传播行为:当bookService的purchase()方法被另外一个事务方法checkout()调用时,它会默认在现有的事务内运行。因此在checkout()方法的开始和结束内只有一个事务,这个事务只会在checkout()方法调用结束时被提交,那就导致用户一本都买不了。

Java工程师面试1000题146-Spring支持的数据库事务传播属性和隔离级别_第2张图片

  • REQUIRES_NEW传播行为:表示该方法必须启动一个新的事务,并在自己的事务内运行,如果已经有在运行,就先把他挂起。

Java工程师面试1000题146-Spring支持的数据库事务传播属性和隔离级别_第3张图片

2、事务的隔离级别

在讲事务的隔离级别之前,我们先来看一下数据库事务并发问题:

假设现在有两个事务:Transaction01和Transaction02并发执行。

①脏读:当前事务读到了其他事务更新但是还没有提交的值(其他事务不回滚还好,其他事务回滚你读到的就是一个无效值)。

  1. Transaction01将某条记录的AEG值从20修改为30
  2. Transaction02读取了Transaction01更新后的值:30
  3. Transaction01回滚事务,AEG的值又恢复到了20
  4. Transaction02读取到的30就是一个无效的值

②不可重复读:

  1. Transaction01读取了AEG的值为20
  2. Transaction02将AEG的值修改为30
  3. Transaction01再次读取AEG值为30,和第一次读取结果不一致

③幻读:

  1. Transaction01读取了STUDENT表中的一部分数据
  2. Transaction02向STUDENT表中插入了新的行

事务的隔离级别:数据库系统必须具有隔离并发运行各个事务的能力,使它们不会相互影响,避免各种并发问题。一个事务与其他事务隔离的程度成为事务的隔离级别。SQL标准中规定了多种事务隔离级别,不同隔离级别对应不同的干扰程度,隔离级别越高,数据一致性就越好,但是并发性就越弱。

1、读未提交:READ UNCOMMITTED,允许Transaction01读取Transaction02未提交的修改。(脏读、不可重复读、幻读都有可能出现

2、读已提交:READ COMMITTED,要求Transaction01只能读取Transaction02已经提交的修改。(脏读就可以避免了

3、可重复读:REPEATABLE READ,确保Transaction01可以多次从一个字段读取到相同的值,即Transaction01执行期间禁止其他事务对这个字段进行更新。(脏读、不可重复读都不会出现了

4、串行化:SERIALIZABLE,确保Transaction01可以多次从一个表中读取到相同的行,在Transaction01执行期间,禁止其他事务对这个表进行添加、更新、删除操作。可以避免所有并发问题,但是性能最低。(脏读、不可重复读、幻读都不可能出现

Java工程师面试1000题146-Spring支持的数据库事务传播属性和隔离级别_第4张图片

各数据库产品对事务隔离级别的支持程度:

Java工程师面试1000题146-Spring支持的数据库事务传播属性和隔离级别_第5张图片

 

你可能感兴趣的:(Java面试1000题,Java工程师面试1000题)