事务的四个关键属性(ACID)
原子性(atomicity): 事务是一个原子操作, 由一系列动作组成. 事务的原子性确保动作要么全部完成要么完全不起作用.
一致性(consistency): 一旦所有事务动作完成, 事务就被提交. 数据和资源就处于一种满足业务规则的一致性状态中.
隔离性(isolation): 可能有许多事务会同时处理相同的数据, 因此每个事物都应该与其他事务隔离开来, 防止数据损坏.
持久性(durability): 一旦事务完成, 无论发生什么系统错误, 它的结果都不应该受到影响. 通常情况下, 事务的结果被写到持久化存储器中
Spring 中的事务管理器
DataSourceTransactionManager在应用程序中只需要处理一个数据源, 而且通过 JDBC 存取
事务管理器以普通的 Bean 形式声明在 Spring IOC 容器中
用 @Transactional 注解声管理事务
为了将方法定义为支持事务处理的, 可以为方法添加 @Transactional 注解
在 Bean 配置文件中只需要启用<tx:annotation-driven> 元素, 并为之指定事务管理器就可以了
事务传播属性
当事务方法被另一个事务方法调用时, 必须指定事务应该如何传播. 例如: 方法可能继续在现有事务中运行,也可能开启一个新事务, 并在自己的事务中运行.
REQUIRED它默认会在现有的事务内运行
REQUIRED_NEW它表示该方法必须启动一个新事务, 并在自己的事务内运行. 如果有事务在运行, 就应该先挂起它.
并发事务所导致的问题可以分为下面三种类型:
脏读: 对于两个事物 T1, T2,T1 读取了已经被 T2 更新但还没有被提交的字段. 之后, 若 T2 回滚, T1读取的内容就是临时且无效的.
不可重复读:对于两个事物 T1,T2, T1 读取了一个字段, 然后 T2 更新了该字段. 之后, T1再次读取同一个字段, 值就不同了.
幻读:对于两个事物 T1, T2,T1 从一个表中读取了一个字段,然后 T2 在该表中插入了一些新的行. 之后, 如果 T1 再次读取同一个表, 就会多出几行.
隔离事务属性可以指定事务的隔离级别
注解声明式地管理事务时可以在 @Transactional 的 isolation 属性中设置隔离级别
或可以在 <tx:method> 元素中指定隔离级别
回滚事务属性
事务的回滚规则可以通过 @Transactional 注解的 rollbackFor 和 noRollbackFor 属性来定义. 这两个属性被声明为 Class[] 类型的, 因此可以为这两个属性指定多个异常类.
rollbackFor: 遇到时必须进行回滚
noRollbackFor: 一组异常类,遇到时必须不回滚
超时事务属性: 事务在强制回滚之前可以保持多久. 这样可以防止长期运行的事务占用资源.
只读事务属性: 表示这个事务只读取数据但不更新数据, 这样可以帮助数据库引擎优化事务.
注解方式
SpringTransactionTest
package com.spring.tx;
import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.api.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
class SpringTransactionTest {
private ApplicationContext ctx = null;
private LoginDao loginDao= null;
private LoginService loginService = null;
{
ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
loginDao =(LoginDao) ctx.getBean("loginDao");
loginService =(LoginService)ctx.getBean("loginService");
}
@Test
void testLoginDaoFindPasswordByusername() {
System.out.println(loginDao.findPasswordByusername("11111"));
//asdf
}
@Test
void testLoginDaoUpdatePassword() {
loginDao.updatePassword("11111", "pp");
}
@Test
void testLoginServiceImpl() {
loginService.changePassword("11111", "ppd");
}
}
applicationContext.xml
db.properties
见Spring对JDBC的支持
LoginService
package com.spring.tx;
import org.springframework.stereotype.Service;
public interface LoginService {
void changePassword(String username,String password);
}
LoginServiceImpl
package com.spring.tx;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
@Service("loginService")
public class LoginServiceImpl implements LoginService{
@Autowired
private LoginDao loginDao;
/*添加事务注解,保证事务原子性
1.使用propagation指定事务的传播行为,即当前事务方法被另一个事务调用时如何使用事务
默认REQUIRED,即使用同一个事务
REQUIRED_NEW,使用各自的事务
2.使用isolation指定事物的隔离机制,最常用READ_COMMITTED读提交后的数据
3.默认情况下Spring的声明式事务对所有的运行时异常回滚,也可以通过相应的属性进行设置。
通常情况下默认即可
4.使用readOnly指定事务是否为只读,表示事务只读取而不更新,可以进行优化
5.使用timeout指定强制回滚之前事务可以占用的时间
*/
@Transactional(propagation=Propagation.REQUIRED,
isolation=Isolation.READ_COMMITTED,readOnly=false,
timeout=3)
@Override
public void changePassword(String username, String password) {
String p = loginDao.findPasswordByusername(username);
if(!p.equals(password)) {
loginDao.updatePassword(username, password);
System.out.println(loginDao.findPasswordByusername(username));
}else {
System.out.println("密码重复");
}
//其他各种操作
}
}
LoginDao
package com.spring.tx;
public interface LoginDao {
/**
* 根据用户名查找密码
*/
public String findPasswordByusername(String username);
/**
* 根据用户名和新密码进行修改
*/
public void updatePassword(String username,String password);
}
LoginDaoImpl
package com.spring.tx;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;
@Repository("loginDao")
public class LoginDaoImpl implements LoginDao{
@Autowired
private JdbcTemplate jdbcTemplate;
@Override
public String findPasswordByusername(String username) {
String sql = "select password from login where username = ?";
return jdbcTemplate.queryForObject(sql, String.class,username);
}
@Override
public void updatePassword(String username, String password) {
String sql = "update login set password = ? where username = ?";
jdbcTemplate.update(sql,password,username);
}
}
基于xml文件
SpringTransactionTest
package com.spring.tx.xml;
import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.api.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
class SpringTransactionTest {
private ApplicationContext ctx = null;
private LoginDao loginDao= null;
private LoginService loginService = null;
{
ctx = new ClassPathXmlApplicationContext("applicationContext-tx-xml.xml");
loginDao =(LoginDao)ctx.getBean("loginDao");
loginService =(LoginService)ctx.getBean("loginService");
}
@Test
void testLoginDaoFindPasswordByusername() {
System.out.println(loginDao.findPasswordByusername("11111"));
}
@Test
void testLoginDaoUpdatePassword() {
loginDao.updatePassword("11111", "pp");
}
@Test
void testLoginServiceImpl() {
loginService.changePassword("11111", "pod");
}
}
db.properties同上
applicationContext-tx-xml.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"
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-4.3.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop-4.3.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx-4.3.xsd">
<context:component-scan base-package="com.spring"></context:component-scan>
<!-- 导入资源文件 -->
<context:property-placeholder location="classpath:db.properties"/>
<!-- 配置C3P0数据源 -->
<bean id="datasSource" 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.jdbcUrl}"></property>
<property name="driverClass" value="${jdbc.driverClass}"></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="datasSource"></property>
</bean>
<!-- 配置bean -->
<bean id="loginDao" class="com.spring.tx.xml.LoginDaoImpl">
<property name="jdbcTemplate" ref="jdbcTemplate"></property>
</bean>
<bean id="loginService" class="com.spring.tx.xml.LoginServiceImpl">
<property name="loginDao" ref="loginDao"></property>
</bean>
<!-- 1.配置事务管理器 -->
<bean id="dataSourceTransactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="datasSource"></property>
</bean>
<!-- 2.配置事务属性 -->
<tx:advice id="txAdvice" transaction-manager="dataSourceTransactionManager">
<tx:attributes>
<!-- 根据方法名指定事务属性 -->
<tx:method name="changePassword" propagation="REQUIRED"/>
<tx:method name="get*" read-only="true"/>
<tx:method name="find*" read-only="true"/>
<tx:method name="*"/>
</tx:attributes>
</tx:advice>
<!-- 配置事务切入点,以便把事务切入点和事务属性关联起来 -->
<aop:config>
<aop:pointcut expression="execution(* com.spring.tx.xml.LoginService.*(..))" id="txPointCut"/>
<aop:advisor advice-ref="txAdvice" pointcut-ref="txPointCut"/>
</aop:config>
</beans>
LoginDao
package com.spring.tx.xml;
public interface LoginDao {
/**
* 根据用户名查找密码
*/
public String findPasswordByusername(String username);
/**
* 根据用户名和新密码进行修改
*/
public void updatePassword(String username,String password);
}
LoginDaoImpl
package com.spring.tx.xml;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;
public class LoginDaoImpl implements LoginDao{
private JdbcTemplate jdbcTemplate;
public JdbcTemplate getJdbcTemplate() {
return jdbcTemplate;
}
public void setJdbcTemplate(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
@Override
public String findPasswordByusername(String username) {
String sql = "select password from login where username = ?";
return jdbcTemplate.queryForObject(sql, String.class,username);
}
@Override
public void updatePassword(String username, String password) {
String sql = "update login set password = ? where username = ?";
jdbcTemplate.update(sql,password,username);
}
}
LoginService
package com.spring.tx.xml;
import org.springframework.stereotype.Service;
public interface LoginService {
void changePassword(String username,String password);
}
LoginServiceImpl
package com.spring.tx.xml;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
public class LoginServiceImpl implements LoginService{
private LoginDao loginDao;
public LoginDao getLoginDao() {
return loginDao;
}
public void setLoginDao(LoginDao loginDao) {
this.loginDao = loginDao;
}
/*添加事务注解,保证事务原子性
1.使用propagation指定事务的传播行为,即当前事务方法被另一个事务调用时如何使用事务
默认REQUIRED,即使用同一个事务
REQUIRED_NEW,使用各自的事务
2.使用isolation指定事物的隔离机制,最常用READ_COMMITTED读提交后的数据
3.默认情况下Spring的声明式事务对所有的运行时异常回滚,也可以通过相应的属性进行设置。
通常情况下默认即可
4.使用readOnly指定事务是否为只读,表示事务只读取而不更新,可以进行优化
5.使用timeout指定强制回滚之前事务可以占用的时间
*/
@Transactional(propagation=Propagation.REQUIRED,
isolation=Isolation.READ_COMMITTED,readOnly=false,
timeout=3)
@Override
public void changePassword(String username, String password) {
String p = loginDao.findPasswordByusername(username);
if(!p.equals(password)) {
loginDao.updatePassword(username, password);
System.out.println(loginDao.findPasswordByusername(username));
}else {
System.out.println("密码重复");
}
//其他各种操作
}
}