事务简介
事务就是用来确保数据的完整性和一致性的一系列的动作,它们被当做一个单独的工作单元,这些动作要么全部完成,要么全部不起作用。
事务的四个关键属性(ACID)
原子性:要么全部完成,要么全部失败 一致性:一旦所有事务动作完成,事务就会被提交,数据和资源就处于一种满足业务规则的一致性状态中 隔离性:可能许多事务会同时处理相同的数据,因此每个事务都应该与其它事务隔离开来,防止数据损坏 持久性:一旦事务完成,无论发生什么系统错误,它的结果都不应受影响,一般是被持久化到存储器中
没有事务之前
继续使用上一节中的db配置文件和Spring配置文件。 使用mysql建立3张相关表:USE ` springstudy3` ;
DROP TABLE IF EXISTS ` account` ;
CREATE TABLE ` account` (
` username` varchar ( 20 ) NOT NULL ,
` balance` int ( 10 ) DEFAULT NULL ,
PRIMARY KEY ( ` username` )
) ENGINE = InnoDB DEFAULT CHARSET = utf8;
insert into ` account` ( ` username` , ` balance` ) values ( 'AA' , 170 ) ;
DROP TABLE IF EXISTS ` book` ;
CREATE TABLE ` book` (
` isbn` int ( 10 ) NOT NULL ,
` book_name` varchar ( 20 ) DEFAULT NULL ,
` price` tinyint ( 10 ) DEFAULT NULL ,
PRIMARY KEY ( ` isbn` )
) ENGINE = InnoDB DEFAULT CHARSET = utf8;
insert into ` book` ( ` isbn` , ` book_name` , ` price` ) values ( 1001 , 'Java' , 100 ) , ( 1002 , 'Oracle' , 70 ) ;
DROP TABLE IF EXISTS ` book_stock` ;
CREATE TABLE ` book_stock` (
` isbn` int ( 10 ) NOT NULL ,
` stock` tinyint ( 5 ) DEFAULT NULL ,
PRIMARY KEY ( ` isbn` )
) ENGINE = InnoDB DEFAULT CHARSET = utf8;
insert into ` book_stock` ( ` isbn` , ` stock` ) values ( 1001 , 3 ) , ( 1002 , 9 ) ;
在com.fafa.spring.tx
包下面建立BookShopDao
,BookShopDaoImpl
public interface BookShopDao {
public int findBookPriceByIsbn ( String isbn) ;
public void updateBookStock ( String isbn) ;
public void updateUserAccount ( String username, int price) ;
}
@Repository ( "bookShopDao" )
public class BookShopDaoImpl implements BookShopDao {
@Autowired
private JdbcTemplate jdbcTemplate;
@Override
public int findBookPriceByIsbn ( String isbn) {
String sql = "SELECT price from book where isbn = ?" ;
return jdbcTemplate. queryForObject ( sql, Integer. class , isbn) ;
}
@Override
public void updateBookStock ( String isbn) {
String sql = "UPDATE book_stock SET stock = stock - 1 WHERE isbn = ?" ;
jdbcTemplate. update ( sql, isbn) ;
}
@Override
public void updateUserAccount ( String username, int price) {
String sql = "UPDATE account SET balance = balance - ? WHERE username = ?" ;
jdbcTemplate. update ( sql, price, username) ;
}
}
建立测试类:public class SpringTransactionTest {
private ApplicationContext ctx = null;
private BookShopDao bookShopDao = null;
{
ctx = new ClassPathXmlApplicationContext ( "classpath:applicationContext.xml" ) ;
bookShopDao = ctx. getBean ( BookShopDao. class ) ;
}
@Test
public void testBookShopDaoFindPriceByIsbn ( ) {
System. out. println ( bookShopDao. findBookPriceByIsbn ( "1001" ) ) ;
}
@Test
public void testBookShopDaoUpdateBookStock ( ) {
bookShopDao. updateBookStock ( "1001" ) ;
}
@Test
public void testBookShopDaoUpdateUserAccount ( ) {
bookShopDao. updateUserAccount ( "AA" , 200 ) ;
}
}
测试结果: testBookShopDaoFindPriceByIsbn
返回书号1001的价格为100 testBookShopDaoUpdateBookStock
执行一次后书号为1001的库存减1,当执行第四次时,测试仍然成功,库存变成了-1,这显然是不合理的。 testBookShopDaoUpdateUserAccount
执行后,由于AA账户初始为170,账户余额为-30。 从上面可见,我们需要对这些过程进行控制。 先来试试简单的通过代码的方式来控制这些事务,建立BookStockException
和UserAccountException
类:public class BookStockException extends RuntimeException {
private static final long serialVersionUID = 1 L;
public BookStockException ( ) {
super ( ) ;
}
public BookStockException ( String message, Throwable cause,
boolean enableSuppression, boolean writableStackTrace) {
super ( message, cause, enableSuppression, writableStackTrace) ;
}
public BookStockException ( String message, Throwable cause) {
super ( message, cause) ;
}
public BookStockException ( String message) {
super ( message) ;
}
public BookStockException ( Throwable cause) {
super ( cause) ;
}
}
public class UserAccountException extends RuntimeException {
private static final long serialVersionUID = 1 L;
public UserAccountException ( ) {
super ( ) ;
}
public UserAccountException ( String message, Throwable cause,
boolean enableSuppression, boolean writableStackTrace) {
super ( message, cause, enableSuppression, writableStackTrace) ;
}
public UserAccountException ( String message, Throwable cause) {
super ( message, cause) ;
}
public UserAccountException ( String message) {
super ( message) ;
}
public UserAccountException ( Throwable cause) {
super ( cause) ;
}
}
修改BookShopDaoImpl
类中的两个方法,当库存货余额不足时抛出异常:@Override
public void updateBookStock ( String isbn) {
String sql2 = "SELECT stock FROM book_stock WHERE isbn = ?" ;
int stock = jdbcTemplate. queryForObject ( sql2, Integer. class , isbn) ;
if ( stock <= 0 ) {
throw new BookStockException ( "库存不足..." ) ;
}
String sql = "UPDATE book_stock SET stock = stock - 1 WHERE isbn = ?" ;
jdbcTemplate. update ( sql, isbn) ;
}
@Override
public void updateUserAccount ( String username, int price) {
String sql2 = "SELECT balance FROM account WHERE username = ?" ;
int balance = jdbcTemplate. queryForObject ( sql2, Integer. class , username) ;
if ( balance < price) {
throw new UserAccountException ( "余额不足..." ) ;
}
String sql = "UPDATE account SET balance = balance - ? WHERE username = ?" ;
jdbcTemplate. update ( sql, price, username) ;
}
继续执行之前的两个方法后就会报错,此时会发现数据库中的数据不会被更新。 再进行另一项测试,新建接口类BookShopService
和实现BookShopServiceImpl
:public interface BookShopService {
public void purchase ( String username, String isbn) ;
}
@Service ( "bookShopService" )
public class BookShopServiceImpl implements BookShopService {
@Autowired
private BookShopDao bookShopDao;
@Override
public void purchase ( String username, String isbn) {
int price = bookShopDao. findBookPriceByIsbn ( isbn) ;
bookShopDao. updateBookStock ( isbn) ;
bookShopDao. updateUserAccount ( username, price) ;
}
}
此时我们设置数据库中的相关信息如下,账户余额160,1001的库存为10本 当我们第一次执行以下代码时:@Test
public void testBookShopService ( ) {
bookShopService. purchase ( "AA" , "1001" ) ;
}
第一次成功,结果如下,账户余额60,1001的库存减1 第二次执行时,结果如下,此时虽然报账户余额不足,但是由于上面代码中先更新库存再更新余额,所以导致库存减少,余额不变。 所以就很有必要使用事务管理器来管理事务
事务管理器
在Spring配置文件中加入以下配置类启用Spring事务管理:
< bean id = " transactionManager"
class = " org.springframework.jdbc.datasource.DataSourceTransactionManager" >
< property name = " dataSource" ref = " dataSource" > </ property>
</ bean>
< tx: annotation-driven transaction-manager = " transactionManager" />
新建一个Cashier
接口类及其实现CashierImpl
来批量购买书籍: 使用@Transactional
注解来表示该方法由事务管理,同时给BookShopServiceImpl
类中的purchase
方法也加上。public interface Cashier {
public void checkout ( String username, List< String> isbns) ;
}
@Service ( "cashier" )
public class CashierImpl implements Cashier {
@Autowired
private BookShopService bookShopService;
@Transactional
@Override
public void checkout ( String username, List< String> isbns) {
for ( String isbn : isbns) {
bookShopService. purchase ( username, isbn) ;
}
}
}
再次执行上次的方法后,就不会有那样的情况了。