1.事务简介
- 事务管理是企业级应用程序开发中必不可少的技术,用来确保数据的完整性和一致性
- 事务就是一系列动作,它们被当做一个单独的工作单元,这些动作要么全部完成,要么全部不起作用
- 事务的四个关键属性(ACID)
原子性:事务是一个原子操作,要么都执行成功,要么都执行失败
一致性:一旦所有事务动作完成,事务就被提交。数据和资源就处于一种满足业务规则的一致性状态中
隔离性:可能有许多事务会同时处理相同的数据。因此每个事务都应该和其它事务隔离开来,防止数据损坏
持久性:一旦事务完成,无论发生什么系统错误,它的结果都不应该受到影响。通常情况下,事务的结果被写到持久化存储器中
2.jdbc对事务的管理
public void purchase(String isba, String username) {
Connection conn = null;
try {
conn = dataSource.getConnection();
conn.setAutoCommit(false);
// 目标方法
conn.commit();
} catch(SQLException e) {
if(null != conn) {
try {
conn.rollback();
} catch (SQLException e1) {
e1.printStackTrace();
}
}
throw new RuntimeException(e);
} finally {
if(null != conn) {
try {
conn.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
}
先获取数据库连接、开启事务,如果执行目标方法成功,提交事务、关闭连接
如果执行目标方法出现异常,则回滚事务,关闭连接
以上的问题:
- 必须为不同的业务方法重写类似的样板代码
- 这段代码是特定于JDBC的,一旦选择其它数据库存取技术,代码需要做相应的修改
获取数据库连接、开启事务 ---- 前置通知
提交事务 ---- 返回通知
回滚事务 ---- 异常通知
关闭连接 ---- 后置通知
这就是spring使用AOP处理事务的原理出发点
3.spring对事务的管理
- spring既支持编程式事务管理又支持声明式事务管理
- 编程式事务管理:将事务管理代码嵌入到物业方法中来控制事务的提交和回滚
- 声明式事务管理:大多数情况下比编程式事务管理更好用。它将事务管理代码从业务方法中抽离出来,以声明的方式实现事务管理。事务管理作为一种横切关注点,可以通过AOP方法模块化。spring通过AOP框架支持声明式事务管理
3.1需求:
一个用户买书,但他的余额不足够买一本书,但他执行了此操作后,结果如何?
3.2开发步骤
项目结构图:
【第一步】、创建一个java工程,如:spring-04-tx
【第二步】、创建数据表:
book表:
mysql> create table book
-> (
-> id INT(5) PRIMARY KEY AUTO_INCREMENT,
-> name VARCHAR(50),
-> price DOUBLE
-> );
mysql> INSERT INTO book VALUES (NULL, 'Java', 37.8), (NULL, 'Oracle', 35.6);
字段:
- id:书的编号
- name:书的名称
- price:书的单价
account表:
mysql> create table account
-> (
-> username VARCHAR(30),
-> balance double
-> );
mysql> INSERT INTO account VALUES ('zzc', 100);
字段:
- username :用户名
- balance :余额
stock表:
mysql> create table stock
-> (
-> id INT(5) PRIMARY KEY AUTO_INCREMENT,
-> stock INT(3)
-> );
mysql> INSERT INTO stock VALUES (NULL, 10), (NULL, 10);
字段:
- id:库存id
- stock:库存量
【第三步】、创建包和类
com.zzc.dao包下创建BookShopDao接口
BookShopDao.java
public interface BookShopDao {
/**
* 根据书号获取单价
*
* @param id
* @return
*/
double getPriceById(Integer id);
/**
* 根据书号更新书的库存
*
* @param id
*/
void updateBookStock(Integer id);
/**
* 更新账户余额
*
* @param username
* @param price
*/
void updateUserAccount(String username, double price);
}
com.zzc.dao.impl包下创建其实现类BookShopDaoImpl
BookShopDaoImpl.java
@Repository(value="bookShop")
public class BookShopDaoImpl implements BookShopDao{
@Autowired
private JdbcTemplate jdbcTemplate;
@Override
public double getPriceById(Integer id) {
String sql = "SELECT price FROM book WHERE id = ?";
return jdbcTemplate.queryForObject(sql, Double.class, id);
}
@Override
public void updateBookStock(Integer id) {
String sql = "UPDATE book SET stock = stock - 1 WHERE id = ?";
jdbcTemplate.update(sql, id);
}
@Override
public void updateUserAccount(String username, double price) {
String sql = "UPDATE account SET balance = balance - ? WHERE username = ?";
jdbcTemplate.update(sql, price, username);
}
}
上述方法使用jdbcTemplate操作数据库
- getPriceById():根据书名获取书的单价
- updateBookStock():根据书号更新书的库存
- updateUserAccount():根据用户名更新账户余额
【第四步】、添加配置文件
添加数据库配置文件db.properties
jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/mybatis?useUnicode=true&characterEncoding=utf8
jdbc.username=root
jdbc.password=root
jdbc.initPoolSize=5
jdbc.maxPoolSize=10
添加spring配置文件applicationContext.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd">
<!-- 导入资源文件 -->
<context:property-placeholder location="classpath:db.properties" />
<context:component-scan base-package="com.zzc"></context:component-scan>
<!-- 配置c3p0数据源 -->
<bean id="dataSource"
class="com.mchange.v2.c3p0.ComboPooledDataSource">
<property name="driverClass" value="${jdbc.driver}"></property>
<property name="jdbcUrl" value="${jdbc.url}"></property>
<property name="user" value="${jdbc.username}"></property>
<property name="password" value="${jdbc.password}"></property>
<property name="initialPoolSize" value="${jdbc.initPoolSize}"></property>
<property name="maxPoolSize" value="${jdbc.maxPoolSize}"></property>
</bean>
<!-- 配置spring中的jdbcTemplate -->
<bean id="jdbcTemplate"
class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="dataSource"></property>
</bean>
<bean id="namedParameterJdbcTemplate"
class="org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate">
<constructor-arg ref="dataSource"></constructor-arg>
</bean>
</beans>
- 导入数据库资源配置文件
- 配置c3p0数据源
- 配置spring中的jdbcTemplate
【第五步】、创建一个测试类
TxTest.java
public class TxTest {
private ApplicationContext ctx = null;
private BookShopDao bookShopDao = null;
{
ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
bookShopDao = (BookShopDao) ctx.getBean("bookShop");
}
@Test
public void test01() {
double price = bookShopDao.getPriceById(1);
System.out.println(price);
}
}
测试:通过书号获取此书的单价
【第六步】、优化实现类中的方法
-
updateBookStock():由于库存量不能小于0,故当库存量等于0时,是不能更新的,要进行手动抛出异常
-
updateUserAccount():由于购买书时,余额是要大于书的价格的
在com.zzc.exception包下自定义两个异常类:BookStockException类、UserAccountException类,并继承RuntimeException
BookStockException.java
public class BookStockException extends RuntimeException{
public BookStockException() {
super();
}
public BookStockException(String message) {
super(message);
}
}
UserAccountException.java
public class UserAccountException extends RuntimeException {
public UserAccountException() {
super();
}
public UserAccountException(String message) {
super(message);
}
}
修改updateBookStock()方法:
@Override
public void updateBookStock(Integer id) {
// 1.验证库存是否足够
String sql2 = "SELECT stock FROM book WHERE id = ?";
int stock = jdbcTemplate.queryForObject(sql2, Integer.class, id);
if (0 == stock) {
throw new BookStockException("库存不足!");
}
String sql = "UPDATE book SET stock = stock - 1 WHERE id = ?";
jdbcTemplate.update(sql, id);
}
修改updateUserAccount()方法:
@Override
public void updateUserAccount(String username, double price) {
// 1.验证余额是否足够
String sql2 = "SELECT balance FROM account WHERE username = ?";
double balance = jdbcTemplate.queryForObject(sql2, Double.class, username);
if(balance < price) {
throw new UserAccountException("余额不足!");
}
String sql = "UPDATE account SET balance = balance - ? WHERE username = ?";
jdbcTemplate.update(sql, price, username);
}
【第七步】、创建com.zzc.service包和类
BookShopService接口
public interface BookShopService {
/**
* 买一本书
*
* @param username 用户名
* @param id 书的Id
*/
void purchase(String username, int id);
}
BookShopServiceImpl
@Service(value="bookShopService")
public class BookShopServiceImpl implements BookShopService {
@Autowired
private BookShopDao bookShopDao;
@Override
public void purchase(String username, int id) {
// 1.获取书的单价
double price = bookShopDao.getPriceById(id);
// 2.更新书的库存
bookShopDao.updateBookStock(id);
// 3.更新用户账户余额
bookShopDao.updateUserAccount(username, price);
}
}
买一本书要执行两个操作:
- 书的库存减一
- 用户的余额也相应地减少
【第八步】、修改测试类
添加 买一本书方法
public class TxTest {
private ApplicationContext ctx = null;
private BookShopDao bookShopDao = null;
private BookShopService bookShopService = null;
{
ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
bookShopDao = (BookShopDao) ctx.getBean("bookShop");
bookShopService = (BookShopService) ctx.getBean("bookShopService");
}
@Test
public void test02() {
bookShopService.purchase("zzc", 1);
}
...
}
当账户余额不足买一本书时,会出现如下问题:
书的库存减一,但账户余额没变。也就是说,你买书成功,但没付钱。这在现实生活中是不可能的
原因:
先执行的是bookShopDao.updateBookStock(id)方法,即先更新了库存量,再执行的是bookShopDao.updateUserAccount(username, price)方法,此方法(余额不足,报错了)会报错,无法更新账户余额。导致了一个方法执行成功,一个方法执行失败,这种情况下是不允许的(要么都执行成功,要么都执行失败)。所以必须使用事务
3.3基于注解方式配置事务
【第九步】、配置事务
一:在applicationContext.xml配置文件中配置事务并开启事务:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-4.0.xsd">
<!-- 导入资源文件 -->
<context:property-placeholder location="classpath:db.properties" />
<context:component-scan base-package="com.zzc"></context:component-scan>
<!-- 配置c3p0数据源 -->
<bean id="dataSource"
class="com.mchange.v2.c3p0.ComboPooledDataSource">
<property name="driverClass" value="${jdbc.driver}"></property>
<property name="jdbcUrl" value="${jdbc.url}"></property>
<property name="user" value="${jdbc.username}"></property>
<property name="password" value="${jdbc.password}"></property>
<property name="initialPoolSize" value="${jdbc.initPoolSize}"></property>
<property name="maxPoolSize" value="${jdbc.maxPoolSize}"></property>
</bean>
<!-- 配置spring中的jdbcTemplate -->
<bean id="jdbcTemplate"
class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="dataSource"></property>
</bean>
<!-- 配置事务管理器 -->
<bean id="transactionManager"
class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"></property>
</bean>
<!-- 开启事务注解 -->
<tx:annotation-driven transaction-manager="transactionManager"/>
</beans>
- 配置事务管理器
- 开启事务注解:需要添加tx命名空间
二:在需要配置事务的方法上添加注解@Transactional
修改BookShopServiceImpl类中的purchase()方法
// 1.添加事务注解
@Transactional
@Override
public void purchase(String username, int id) {
// 1.获取书的单价
double price = bookShopDao.getPriceById(id);
// 2.更新书的库存
bookShopDao.updateBookStock(id);
// 3.更新用户账户余额
bookShopDao.updateUserAccount(username, price);
}
【第十步】、再次测试
不会出现之前的问题了,两个方法要么都执行成功,要么都执行失败