开篇
前段时间回头再仔细看sping框架时候,跟着网上的视频又将sping的事务操作又温习了一遍,通过这篇文章进行一下总结。
只要是搜事务,找到这篇文章肯定都对事务有所了解。事务管理是在开发过程中必不可少的,不要小看这点操作,可能就是我们的一个注解,如果是真实的企业级开发,是非常有必要的。事务主要是用来保持数据的完整性和一致性。事务的一系列动作他们被当做是一个独立的运行单元,要么全部成功或者全部失败。
事务的特性
学习过数据库的应该都有了解到事务的四个属性(ACID):原子性、一致性、隔离性、持久性。这里不详细介绍事务的四个特性了,网上随便搜一篇写的都很详细。接下来主要看Spring操作数据库时候,怎么遵循这四个特性的。
Spring的事务管理
Spring作为企业级的开发框架,Spring在不同的事务管理API上定义了一个抽象层,一般得开发人员不需要去了解底层的事务管理API,知道起API的作用就能使用事务。
Spring有多种实现方式;有编程式事务和声明式事务之分,
编程式事务,就是通过代码的形式将事务的操作嵌入到具体的业务实现场景中,来实现事务的控制。
声明式事务,也是用到最多的事务实现形式;多数情况下比编程式事务好用,他将事务管理代码从具体的业务场景中抽离出来,以声明的方式来事项事务的管理。这样比声明式事务的好处是代码耦合度降低,不影响整体的业务实现。Spring通过SpringAOP框架支持声明式事务。
Spring中事务管理器的不同实现
Spring核心事务管理抽象接口 PlatformTransactionManager ,Spring并不直接管理事务,而是提供了多种事务管理器,他们将事务管理的职责委托给Hibernate或者JTA等持久化机制所提供的相关平台框架的事务来实现。
Spring事务管理器的接口是org.springframework.transaction.PlatformTransactionManager,通过这个接口,Spring为各个平台如JDBC、Hibernate等都提供了对应的事务管理器。例如,使用JDBC时,通过JdbcTemplate操作数据库,使用Hiberante框架时,操作数据库是通过SessionFactory,不同的平台实现的方式不一样。只要去实现Spring事务管理的接口,后面是如何实现的有不同的方法。下图Spring事务管理接口不同方式实现,图片摘自https://blog.youkuaiyun.com/trigl/article/details/50968079
下面我是通过DataSourceTransationMananger来实现事务的操作,通过代码的形式详细通过真实的案例来理解spring的操作,下面先创建一个spring项目,结合代码再说后面的概念;
创建spring项目
通过注解的方式实现
这里我们是通过Sping注解的形式,后面我们再通过配置文件的形式来实现相同的事务操作;
创建Maven Spring项目 方便导入相应的jar包;
除了引入必要的spring核心jar包外,还需要Spring-tx(提供编程式和声明式事务)、spring-jdbc、c3p0、mysql-connector(根据自己的情况)
Spring配置文件
<?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.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx-4.2.xsd"
>
<!--自动扫描包 -->
<context:component-scan base-package="com.jdbcTemplate"></context:component-scan>
<!-- 导入资源文件-->
<context:property-placeholder location="db.properties"/>
<!--配置从c3po数据源 -->
<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
<property name="user" value="${jdbc.user}"></property>
<property name="password" value="${jdbc.password}"></property>
<property name="jdbcUrl" value="${jdbc.url}"></property>
<property name="driverClass" value="${jdbc.dirverClass}"></property>
<property name="initialPoolSize" value="${jdbc.initPoolSize}"></property>
<property name="maxPoolSize" value="${jdbc.maxPoolSize}"></property>
</bean>
<!--配置Spring 的Template -->
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="dataSource"></property>
</bean>
<!--配置NamedParameterJdbcTemplate 该对象没有无参的构造方法,需要使用有参构造器,一般选用dataSourc -->
<bean id="namedParameterJdbcTemplate" class="org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate">
<constructor-arg ref="dataSource"></constructor-arg>
</bean>
<!--配置事务管理器 -->
<bean id="transactionManager"
class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"></property>
</bean>
<!--启用注解事务 -->
<tx:annotation-driven transaction-manager="transactionManager"/>
</beans>
db.propertis 数据源配置信息
jdbc.user=root
jdbc.password=123456
jdbc.dirverClass=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/test
jdbc.initPoolSize=5
jdbc.maxPoolSize=10
测试案例 学习视频中的案例,就直接拿过来学习了。
首先这里创建了三张表
1.account账户表 字段id、user_name(账户名)、balance(余额);
2.book 图书清单表 字段id、book_code(书编号)、book_name(名称)、book_price(单价);
3.count 库存表 id、book_code(书编号)、book_count(库存量)
业务实现;通过书本的编号查询书的价格,通过书的编号来更新书的库存(这里测试一次只卖一本),通过数的单价更新用户余额。
项目结构
分别创建三张表的实体类:
Acoount,Book,Bookcount
创建dao层,业务操作
public interface BuyBookDao {
/**
* 获取书本的price
*/
int queryBookPrice(String bookCode);
/**
* 根据书本的code更新书本的数量
*/
void updateBookCount(String bookCode);
/**
* 资金的更新
*/
int updateAccount(String userName, int price);
}
Dao实现
package com.springTranscation.dao;
import com.springTranscation.exception.BookException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;
@Repository("buBookDao")
public class BuyBookDaoImpl implements BuyBookDao {
@Autowired
private JdbcTemplate jdbcTemplate;
@Override
public int queryBookPrice(String bookCode) {
String sql = "select BOOK_PRICE price from BOOK where BOOK_CODE =?";
return jdbcTemplate.queryForObject(sql, Integer.class, bookCode);
}
@Override
public void updateBookCount(String bookCode) {
//检查书的库存是否大于0
String sqlCount = "SELECT BOOK_COUNT FROM COUNT WHERE BOOK_CODE = ?";
int count = jdbcTemplate.queryForObject(sqlCount,Integer.class,bookCode);
if(count == 0){
throw new BookException("库存不足");
}
String sql = "UPDATE COUNT SET BOOK_COUNT = BOOK_COUNT -1 WHERE BOOK_CODE = ?";
jdbcTemplate.update(sql,bookCode);
}
@Override
public int updateAccount(String useName, int price) {
//余额是否大于0
String sqlCount = "SELECT BALANCE balance FROM ACCOUNT WHERE USER_NAME = ?";
int balance = jdbcTemplate.queryForObject(sqlCount,Integer.class,useName);
if(balance < price){
throw new BookException("余额不足");
}
String sql = "UPDATE ACCOUNT SET BALANCE = BALANCE - ? WHERE USER_NAME = ?";
jdbcTemplate.update(sql,price,useName);
return 0;
}
}
异常处理(余额不足时抛出异常信息)
public class BookException extends RuntimeException{
public BookException() {
}
public BookException(String message) {
super(message);
}
public BookException(String message, Throwable cause) {
super(message, cause);
}
public BookException(Throwable cause) {
super(cause);
}
public BookException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
super(message, cause, enableSuppression, writableStackTrace);
}
}
service层(一次买一本)
public interface BuyBookService {
void buyBookByNamePrice(String userName, String bookCode);
}
@Service("BuyBookService")
public class BuyBookServiceImpl implements BuyBookService {
@Autowired
private BuyBookDao buyBookDao;
@Override
public void buyBookByNamePrice(String userName, String bookCode) {
System.out.println("获取书单价");
//获取书的单价
int price = buyBookDao.queryBookPrice(bookCode);
System.out.println("更新库存");
//更新数据库存
buyBookDao.updateBookCount(bookCode);
System.out.println("更新余额");
//更新用户余额
buyBookDao.updateAccount(userName, price);
}
完成基本业务之后,编写测试类测试三个方法能否正常执行;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = {"classpath*:ApplicationContext.xml"})
public class TestMain {
@Autowired
private BuyBookDao buyBookDao;
@Test
public void queryBookPrice() {
System.out.println(buyBookDao.queryBookPrice("101"));
}
@Test
public void updateBookCount() {
buyBookDao.updateBookCount("101");
}
@Test
public void updateAccount() {
buyBookDao.updateAccount("Jerry", 300);
}
如果没有问题的话,我们这时候开始测试没有添加过的事务的情况时候;
account表Jerry账户余额100元
书目清单,这里我们模拟两本,单价分别是java 80 ,mysql 100,
书的库存表,初始值都是10本;
开始买书
第一次是成功的;
可以看到java书本的库存量减去了1,账户的余额也少了80,这个符合我们的预期,当我么再次后买时,钱已经不能够满足我们的需求,肯定会抛出余额不足的异常,这时候还已经根据书本的code查询进行了减一,余额不足账户表不会变,但是库存表是否会如我们所愿,钱没有减少,书肯定也不能少?测试再次购买
异常正常抛出;看一下数据的情况;
书本的库存减少了一本,而我的账户余额还是没有较少的,这肯定跟我们现实生活中的场景不太符合,我们遵从的是“一手交钱一手交货”这钱没减少,我就好比商家,没收到钱书本还被减少了一本。
该如何实现,余额不足的情况我们的书本的库存不会发生变化?
这里就是我们今天说的事物的管理,我们看一下我们加上事务后会是什么情况,会不会帮我们实现我们需要的真实场景;在serviceImpl层加上@Transactional注解
@Transactional
@Override
public void buyBookByNamePrice(String userName, String bookCode) {
System.out.println("获取书单价");
//获取书的单价
int price = buyBookDao.queryBookPrice(bookCode);
System.out.println("更新库存");
//更新数据库存
buyBookDao.updateBookCount(bookCode);
System.out.println("更新余额");
//更新用户余额
buyBookDao.updateAccount(userName, price);
}
这里我们还是模拟第二次买书,现在的库存是8本,如果按照刚才的情况,余额还是20元的情况下,我们是不够买一本书的,刚才的情况是减少了一本,现在看是什么情况;
继续买书;
同样是上一步相同的操作,这里加上事务的注解之后就没有再更新书本的库存,没有购买成功,这就是我们希望看到的场景。
上面说的是买一本,假如我们还有一个事务方法,通过这个事务方法实现买多本书的情况,会放生什么样的情况?重新写一个买多本书的实现;
public interface BuyBooksService {
void buyBooksByNamePrice(String userName, List<String> booksCode);
}
@Service("BuyBooksService")
public class BuyBooksServiceImpl implements BuyBooksService{
@Autowired
private BuyBookService buyBookService;
@Override
@Transactional
public void buyBooksByNamePrice(String userName, List<String> booksCode) {
for (String bookCode : booksCode){
buyBookService.buyBookByNamePrice(userName,bookCode);
}
}
}
这里我们将书本的数量都重新设置为10本,账户的余额设置为300,模拟实现第一次可以一次买两本书,第二次的话最多就只能买一本java,在不添加事务的前提下能否成功呢?第二次书本的库存量该如何变化?
测试
第一次肯定是正常的,书本的数量各自减一,账户的约还有120元,符合我们的设想,再次购买会之后买一本java会发生什么情况?
余额是减少了一本java的价格但是两本书的库存确都减少了;怎样避免这种情况的发生呢?我们还将余额改成120元,书本的数量先不变,这次我们加上事务处理看会是什么情况?
余额不足,连第一本也没有买成功;
讲到这里开始今天的主要内容,事务的传播行为和事务的隔离级别;
事务的传播属性;
当事务方法被另一个事务方法调用时,必须指定事务应该如何传播。例如:方法可能继续在现有事务中运行,也可能开启一个新事务,并在自己的事务中运行。Spring定义了七种传播行为:
传播行为 | 含义 |
---|---|
PROPAGATION_REQUIRED | 表示当前方法必须运行在事务中。如果当前事务存在,方法将会在该事务中运行。否则,会启动一个新的事务 |
PROPAGATION_SUPPORTS | 表示当前方法不需要事务上下文,但是如果存在当前事务的话,那么该方法会在这个事务中运行 |
PROPAGATION_MANDATORY | 表示该方法必须在事务中运行,如果当前事务不存在,则会抛出一个异常 |
PROPAGATION_REQUIRED_NEW | 表示当前方法必须运行在它自己的事务中。一个新的事务将被启动。如果存在当前事务,在该方法执行期间,当前事务会被挂起。如果使用JTATransactionManager的话,则需要访问TransactionManager |
PROPAGATION_NOT_SUPPORTED | 表示该方法不应该运行在事务中。如果存在当前事务,在该方法运行期间,当前事务将被挂起。如果使用JTATransactionManager的话,则需要访问TransactionManager |
PROPAGATION_NEVER | 表示当前方法不应该运行在事务上下文中。如果当前正有一个事务在运行,则会抛出异常 |
PROPAGATION_NESTED | 表示如果当前已经存在一个事务,那么该方法将会在嵌套事务中运行。嵌套的事务可以独立于当前事务进行单独地提交或回滚。如果当前事务不存在,那么其行为与PROPAGATION_REQUIRED一样。注意各厂商对这种传播行为的支持是有所差异的。可以参考资源管理器的文档来确认它们是否支持嵌套事务 |
1.使用propagation指定事务的传播行为(Propagation.REQUIRE)默认,即当前事务方法被另外一个事务方法调用时,调用方就使用被调用的同一事务; 我们测试使用的是默认的传播行为,所以在同一是是事务内如果事物存在则在同一个事务中执行;如果出现异常我们的数据都不会发生变化,我们刚才的情况是如果想实现在120元月的情况下,之后买一本java想要买成功,mysql买不成功另算只要保证第一本能够买成功,这时候我们设置为(Propagation.REQUIRE_NEW),在业务层的实现方法Transcational中设置propagation再来测试一下,
@Transactional(propagation = Propagation.REQUIRES_NEW)
这时候书本的钱正好减少买一本java书的金额,而且java的库存量减一,mysql的库存是没有变的;实现了两个事物之间互不影响,这就是propagation属性的作用,可以根据情况来设置传播行为;最常用的也就是require和require_new,还有五种不是太常用可以作为了解;
事务的隔离级别
脏读 : 一个事务读取到另一事务未提交的更新数据
不可重复读 : 在同一事务中, 多次读取同一数据返回的结果有所不同, 换句话说,
后续读取可以读到另一事务已提交的更新数据. 相反, "可重复读"在同一事务中多次
读取数据时, 能够保证所读数据一样, 也就是后续读取不能读到另一事务已提交的更新数据
幻读 : 一个事务读到另一个事务已提交的insert数据
Spring事务隔离级别设置
DEFAULT 这是一个PlatfromTransactionManager默认的隔离级别,使用数据库默认的事务隔离级别.
未提交读(read uncommited) :脏读,不可重复读,虚读都有可能发生
已提交读 (read commited):避免脏读。但是不可重复读和虚读有可能发生
可重复读 (repeatable read) :避免脏读和不可重复读.但是虚读有可能发生.
串行化的 (serializable) :避免以上所有读问题.
Mysql 默认:可重复读
Oracle 默认:读已提交
2.事务的隔离级别,通过isolation来指定 通常默认的是 READ_COMMITTED;
总体来说隔离级别和传播行为都是事务的特性,此外还有异常回滚的设置,是否是只读设置,以及超时设置;
/**
* 1.使用propagation指定事务的传播行为(Propagation.REQUIRE)默认,即当前事务方法被另外一个事务方法调用时,调用方就使用被调用的同一事务;
* 如果propagation设置为(Propagation.REQUIRES_NEW),如果当前被调用方有事务,则挂起;自己重新创建一个新的事务;
* 2.事务的隔离级别,通过isolation来指定 通常默认的是 READ_COMMITTED;
* 3.noRollbackFor 指定哪些异常无需回滚,通常情况下不对其设置,默认;
* 4.readOnly 只读设置,事务只是进行数据库的读操作,可以帮助数据库引擎优化;
* 5.timeout 超时等待时间。
*/
上面就是我们通过注解的方式来实现事务,不同的属性直接在注解中添加即可,使用起来很方便;接下来我们使用通过配置文件的形式来实现上面测试的效果,加上相应的事务属性;
配置文件
<?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"
xmlns:aop="http://www.springframework.org/schema/aop"
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.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx-4.2.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd">
<!--自动扫描包 -->
<context:component-scan base-package="com.springTranscation.*"></context:component-scan>
<!-- 导入资源文件-->
<context:property-placeholder location="db.properties"/>
<!--配置从c3po数据源 -->
<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
<property name="user" value="${jdbc.user}"></property>
<property name="password" value="${jdbc.password}"></property>
<property name="jdbcUrl" value="${jdbc.url}"></property>
<property name="driverClass" value="${jdbc.dirverClass}"></property>
<property name="initialPoolSize" value="${jdbc.initPoolSize}"></property>
<property name="maxPoolSize" value="${jdbc.maxPoolSize}"></property>
</bean>
<!--配置Spring 的Template -->
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="dataSource"></property>
</bean>
<!--通过配置文件的形式实现-->
<bean id="buyBookDao" class="com.springTranscation.dao.BuyBookDaoImpl">
<property name="jdbcTemplate" ref="jdbcTemplate"></property>
</bean>
<bean id="buyBookService" class="com.springTranscation.service.BuyBookServiceImpl">
<property name="buyBookDao" ref="buyBookDao"></property>
</bean>
<!--配置事务管理器-->
<bean id="transactionManager"
class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"></property>
</bean>
<!--配事务属性-->
<tx:advice id = "txAdvice" transaction-manager="transactionManager" >
<tx:attributes>
<!--根据方法名来设置事务属性 可以设置到具体的方法也可以作用在全部方法 用* 代替-->
<tx:method name="*" propagation="REQUIRES_NEW"/>
</tx:attributes>
</tx:advice>
<!--配置事务切点将事务的属性与切点关联起来-->
<aop:config >
<aop:pointcut id="txPoincut" expression="execution(* com.springTranscation.service.BuyBookService.*(..))"></aop:pointcut>
<!--关联-->
<aop:advisor advice-ref="txAdvice" pointcut-ref="txPoincut"></aop:advisor>
</aop:config>
</beans>
我们将数据库中的书本改成100元,数量还是10本,第一次是成功的,余额和书本的库存都减少了相应的量;
测试方法
余额不足以买一本书的时候;
书本和余额都不会减少;这就说明我们通过配置文件配置的事务起作用了。
以上就是事务的两种不同实现形式。总结算有不足之处请指正!