介绍Spring所支持的事务和传播属性之前,我们先了解一下SpringBean的作用域,与此题无关,仅做一下简单记录。
在Spring中,可以在
介绍完Spring Bean的作用域之后,下面开始进入正题——Spring支持的数据库事务传播属性和隔离级别
首先我们先了解一下什么是事务的传播属性(传播行为):当一个事务方法被被另一个事务方法调用时,必须指定事务应该如何传播。例如:方法可能继续在现有事务中运行,也可能开启一个新事务,并在自己的事务中运行。
事务的传播行为是由传播属性来指定的。
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种属性值。
我们来看一下啊,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没有买成功。
小总结:
在讲事务的隔离级别之前,我们先来看一下数据库事务并发问题:
假设现在有两个事务:Transaction01和Transaction02并发执行。
①脏读:当前事务读到了其他事务更新但是还没有提交的值(其他事务不回滚还好,其他事务回滚你读到的就是一个无效值)。
②不可重复读:
③幻读:
事务的隔离级别:数据库系统必须具有隔离并发运行各个事务的能力,使它们不会相互影响,避免各种并发问题。一个事务与其他事务隔离的程度成为事务的隔离级别。SQL标准中规定了多种事务隔离级别,不同隔离级别对应不同的干扰程度,隔离级别越高,数据一致性就越好,但是并发性就越弱。
1、读未提交:READ UNCOMMITTED,允许Transaction01读取Transaction02未提交的修改。(脏读、不可重复读、幻读都有可能出现)
2、读已提交:READ COMMITTED,要求Transaction01只能读取Transaction02已经提交的修改。(脏读就可以避免了)
3、可重复读:REPEATABLE READ,确保Transaction01可以多次从一个字段读取到相同的值,即Transaction01执行期间禁止其他事务对这个字段进行更新。(脏读、不可重复读都不会出现了)
4、串行化:SERIALIZABLE,确保Transaction01可以多次从一个表中读取到相同的行,在Transaction01执行期间,禁止其他事务对这个表进行添加、更新、删除操作。可以避免所有并发问题,但是性能最低。(脏读、不可重复读、幻读都不可能出现)
各数据库产品对事务隔离级别的支持程度: