146、简介一下Spring支持的数据库事务传播属性和隔离级别
介绍Spring所支持的事务和传播属性之前,我们先了解一下SpringBean的作用域,与此题无关,仅做一下简单记录。
在Spring中,可以在元素的scope属性中设置bean的作用域,来决定这个bean是单实例的还是多实例的。默认情况下,Spring只为每个在IOC容器里声明的bean创建唯一的实例,整个IOC容器范围内都可以共享该实例;所有后续的getBean()调用和bean引用都将返回这个唯一的bean实例,该作用域被称为singleton,他是所有bean的默认作用域。
- singleton:在SpringIOC容器中仅存在一个bean实例,Bean以单实例的方式存在
- prototype:每次调用getBean()时都会返回一个新的实例
- request:每次HTTP请求都会创建一个新的Bean。该作用域仅适用于WebApplicationContext环境。
- 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;@Servicepublic 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 {@Autowiredprivate JdbcTemplate jdbcTemplate;@Overridepublic 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;}@Overridepublic void updateBookStock(String isbn) {// 写sql语句String sql = "update book_stock set stock = stock - 1 where isbn = ?";// 调用JdbcTemplate中的update方法jdbcTemplate.update(sql, isbn);}@Overridepublic 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种属性值。
我们来看一下啊,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()方法调用结束时被提交,那就导致用户一本都买不了。
- REQUIRES_NEW传播行为:表示该方法必须启动一个新的事务,并在自己的事务内运行,如果已经有在运行,就先把他挂起。
2、事务的隔离级别
在讲事务的隔离级别之前,我们先来看一下数据库事务并发问题:
假设现在有两个事务:Transaction01和Transaction02并发执行。
①脏读:当前事务读到了其他事务更新但是还没有提交的值(其他事务不回滚还好,其他事务回滚你读到的就是一个无效值)。
- Transaction01将某条记录的AEG值从20修改为30
- Transaction02读取了Transaction01更新后的值:30
- Transaction01回滚事务,AEG的值又恢复到了20
- Transaction02读取到的30就是一个无效的值
②不可重复读:
- Transaction01读取了AEG的值为20
- Transaction02将AEG的值修改为30
- Transaction01再次读取AEG值为30,和第一次读取结果不一致
③幻读:
- Transaction01读取了STUDENT表中的一部分数据
- Transaction02向STUDENT表中插入了新的行
事务的隔离级别:数据库系统必须具有隔离并发运行各个事务的能力,使它们不会相互影响,避免各种并发问题。一个事务与其他事务隔离的程度成为事务的隔离级别。SQL标准中规定了多种事务隔离级别,不同隔离级别对应不同的干扰程度,隔离级别越高,数据一致性就越好,但是并发性就越弱。
1、读未提交:READ UNCOMMITTED,允许Transaction01读取Transaction02未提交的修改。(脏读、不可重复读、幻读都有可能出现)
2、读已提交:READ COMMITTED,要求Transaction01只能读取Transaction02已经提交的修改。(脏读就可以避免了)
3、可重复读:REPEATABLE READ,确保Transaction01可以多次从一个字段读取到相同的值,即Transaction01执行期间禁止其他事务对这个字段进行更新。(脏读、不可重复读都不会出现了)
4、串行化:SERIALIZABLE,确保Transaction01可以多次从一个表中读取到相同的行,在Transaction01执行期间,禁止其他事务对这个表进行添加、更新、删除操作。可以避免所有并发问题,但是性能最低。(脏读、不可重复读、幻读都不可能出现)
各数据库产品对事务隔离级别的支持程度: