Spring中对数据库的事务管理
事务的ACID特性是由关系数据库系统(DBMS)来实现的,DBMS采用日志来保证事务的原子性、一致性和持久性。日志记录了事务对数据库所作的更新,如果某个事务在执行过程中发生错误,就可以根据日志撤销事务对数据库已做的更新,使得数据库同滚到执行事务前的初始状态。
对于事务的隔离性,DBMS是采用锁机制来实现的。当多个事务同时更新数据库中相同的数据时,只允许持有锁的事务能更新该数据,其他事务必须等待,直到前一个事务释放了锁,其他事务才有机会更新该数据。
在代码中显式调用beginTransaction、commit、rollback等与事务处理相关的方法, 这就是编程式事务管理. 当只有少数事务操作时, 编程式事务管理才比较合适.
用到的表结构:
create table custom
(
id int auto_increment
primary key,
name varchar(33) not null,
sex varchar(33) not null,
money double not null
);
基于底层API的编程式事务管理就是根据PlatformTransactionManager、TransactionDefinition和TransactionStatus几个核心接口, 通过编程的方式来进行事务处理.
使用PlatformTransactionManager接口的实现类org.springframework.jdbc.datasource.DataSourceTransactionManager为数据源添加事务管理器
<!-- 为数据源添加事务管理器-->
<bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
用来保存数据库连接相关参数
<?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.xsd">
<!-- 导入properties文件 -->
<context:property-placeholder location="classpath*:jdbc.properties"/>
<context:component-scan base-package="cn.lacknb"/>
<!-- 基于底层API的编程式管理-->
<!-- 配置数据源-->
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
<property name="driverClassName" value="${jdbc.driver}"/>
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
</bean>
<!-- 定义事务管理器-->
<bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
<!-- 配置jdbc模板-->
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="dataSource"/>
</bean>
</beans>
jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql:///jdbc_template
jdbc.username=root
jdbc.password=123456
package cn.lacknb.test;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.stereotype.Repository;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.DefaultTransactionDefinition;
@Repository
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = {"classpath*:applicationContext.xml"})
public class JdbcTest {
// 使用配置文件中的jdbc模板
@Autowired
private JdbcTemplate jdbcTemplate;
// DataSourceTransaction是platformTransactionManager接口的实现类
@Autowired
private DataSourceTransactionManager txManager;
@Test
public void test01(){
// 默认事务定义, 例如隔离级别, 传播行为等
TransactionDefinition tf = new DefaultTransactionDefinition();
// 开启事务 ts
TransactionStatus ts = txManager.getTransaction(tf);
String message = "执行成功, 没有事务回滚";
try{
String sql = "insert into custom(id, name, sex, money) values(?, ?, ?, ?)";
Object param[] = {11, "张三", "女", 1000.0};
jdbcTemplate.update(sql, param);
// 添加相同主键的数据, 让事务进行回滚
jdbcTemplate.update(sql, param);
// 提交事务
txManager.commit(ts);
}catch (Exception e){
// 出现异常, 事务回滚
txManager.rollback(ts);
message = "出现异常, 事务回滚";
e.printStackTrace();
}
System.out.println(message);
}
}
事务处理的代码散落在业务逻辑代码中, 破坏了原有代码的条理性, 并且每一个业务方法都包含了类似的启动事务、提交以及回滚事务的样板代码.
TransactionTemplate的execute方法有一个TransactionCallback接口类型的参数, 该接口定义一个doInTransaction方法, 通常以匿名内部类的方式实现TransactionCallback接口, 并在其中doInTransaction方法中书写业务逻辑代码. 在这里可以使用默认的事务提交和回滚规则, 在业务代码中不需要显式调用任何事务处理的API. doInTransaction方法有一个TransactionStatus参数, 可以在该方法的任何位置调用该参数的setRollbackOnly方法将事务标识为回滚, 以执行事务回滚.
根据默认规则, 如果在执行回调方法的过程中抛出了未检查的异常, 或者显式调用了setRollbackOnly方法, 则回滚事务; 如果事务执行完成或者抛出了checked类型的异常, 则提交事务.
<!-- 为事务管理器txManager创建transactionTemplate -->
<bean id="transactionTemplate" class="org.springframework.transaction.support.TransactionTemplate">
<property name="transactionManager" ref="txManager"/>
</bean>
用来保存数据库连接相关参数
<?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.xsd">
<context:property-placeholder location="classpath*:jdbc.properties"/>
<context:component-scan base-package="cn.lacknb"/>
<!-- 基于底层API的编程式管理-->
<!-- 配置数据源-->
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
<property name="driverClassName" value="${jdbc.driver}"/>
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
</bean>
<!-- 定义事务管理器-->
<bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
<!-- 配置jdbc模板-->
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="dataSource"/>
</bean>
<!-- 配置事务的模板-->
<bean id="transactionTemplate" class="org.springframework.transaction.support.TransactionTemplate">
<property name="transactionManager" ref="txManager"/>
</bean>
</beans>
同上一个例子, 不变.
package cn.lacknb.test;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.TransactionCallback;
import org.springframework.transaction.support.TransactionTemplate;
/*
*
* 不再需要自己手动提交和手动回滚了.
*
* */
@Repository
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = {"classpath*:applicationContext.xml"})
public class TemplateTransactionTest {
// 使用配置文件中的jdbc模板
@Autowired
private JdbcTemplate jdbcTemplate;
// DataSourceTransaction是platformTransactionManager接口的实现类
@Autowired
private TransactionTemplate transactionTemplate;
String message = "";
@Test
public void test01(){
// 以匿名内部类的方式实现TransactionCallback接口,使用默认的事务提交和回滚规则
// 在业务代码中不需要显式调用任何事务处理的api
transactionTemplate.execute(new TransactionCallback<Object>() {
public Object doInTransaction(TransactionStatus args0){
String sql = "insert into custom(id, name, sex, money) values(?, ?, ?, ?)";
Object param[] = {22, "小红", "女", 1200.0};
try {
jdbcTemplate.update(sql, param);
// 添加相同主键的数据. 让事务回滚
jdbcTemplate.update(sql, param);
message = "执行成功, 没有回滚";
}catch (Exception e){
message = "执行异常, 事务回滚";
e.printStackTrace();
}
return message;
}
});
System.out.println(message);
}
}
Spring的声明式事务管理是通过AOP技术实现的事务管理, 其本质是对方法前后进行拦截, 然后在目标方法开始之前创建或者加入一个事务, 在执行完目标方法之后根据执行情况提交或者回滚事务
声明式事务管理最大的优点是不需要通过编程的方法管理事务, 因而不需要在业务逻辑代码中掺杂事务处理的代码, 只需相关的事务规则声明便可以将事务规则应用到业务逻辑中. 通常情况下, 在开发中使用声明式事务处理不仅因为其简单, 更主要的是因为这样是的纯业务代码不被污染, 极大地方便了后期的代码维护.
与编程式事务管理相比, 声明式事务管理唯一不足的地方是最细粒度只能作用到方法级别, 无法做到像编程式事务管理那样可以作用到代码块级别. 但即便有这样的需求, 也可以用过变通的方法进行解决, 例如可以将需要进行事务处理的代码块独立为方法等.
Spring的声明式事务管理可以通过两种方式来实现, 一是基于XML的方式. 二是基于@Transactional注解的方式.
基于XML方式的声明式事务管理是通过在配置文件中配置规则的相关的声明来实现的. Spring框架提供了tx命名空间来配置事务, 提供了<tx:advice>元素来配置事务的通知. 在配置<tx:advice>元素时一般需要指定id和transaction-manager属性, 其中id属性是配置文件中的唯一标识, transaction-manager属性指定事务管理器. 另外还需要<tx:attributes>子元素, 该子元素可配置多个<tx:method>子元素指定执行事务的细节.
在<tx:advice>元素配置了事务的增强处理后就可以通过编写AOP配置让Spring自动对目标对象生成代理.
package cn.lacknb.beans;
import org.springframework.stereotype.Repository;
@Repository
public class Custom {
private Integer id;
private String name;
private String sex;
private Double money;
public Custom() {
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getSex() {
return sex;
}
public void setSex(String sex) {
this.sex = sex;
}
public Double getMoney() {
return money;
}
public void setMoney(Double money) {
this.money = money;
}
@Override
public String toString() {
return "Custom{" +
"id=" + id +
", name='" + name + '\'' +
", sex='" + sex + '\'' +
", money=" + money +
'}';
}
}
package cn.lacknb.dao;
import cn.lacknb.beans.Custom;
import java.util.List;
public interface CustomDao {
void addCustom(String sql, Object param[]);
void deletCustom(String sql, Object param[]);
List<Custom> findAll(String sql);
public void operation(Integer id1, Integer id2, Double money);
}
package cn.lacknb.dao;
import cn.lacknb.beans.Custom;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.BeanPropertyRowMapper;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public class CustomDaoImpl implements CustomDao {
@Autowired
private JdbcTemplate jdbcTemplate;
public void addCustom(String sql, Object[] param) {
jdbcTemplate.update(sql, param);
}
public void deletCustom(String sql, Object[] param) {
jdbcTemplate.update(sql, param);
}
public List<Custom> findAll(String sql) {
RowMapper<Custom> rowMapper = new BeanPropertyRowMapper<Custom>(Custom.class);
return jdbcTemplate.query(sql, rowMapper);
}
// 顾客id1给id2转钱
public void operation(Integer id1, Integer id2, Double money) {
String sql1 = "update custom set money = money - ? where id = ?";
String sql2 = "update custom set money = money + ? where id = ?";
jdbcTemplate.update(sql1, money, id1);
// 这里制造异常,
// int i = 1 / 0;
jdbcTemplate.update(sql2, money, id2);
}
}
package cn.lacknb.service;
import cn.lacknb.beans.Custom;
import java.util.List;
public interface CustomService {
public void test();
List<Custom> findAll();
public void operation(Integer id1, Integer id2, Double money);
}
package cn.lacknb.service;
import cn.lacknb.beans.Custom;
import cn.lacknb.dao.CustomDao;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
@Service
@Transactional
public class CustomServiceImpl implements CustomService {
@Autowired
private CustomDao customDao;
public void test() {
String sql = "insert into custom(id, name, sex, money) values(?, ?, ?, ?)";
Object param[] = {1, "张三", "女", 1000.0};
customDao.addCustom(sql, param);
// 添加相同的主键, 由于事务的管理, 第一条数据也不会添加成功.
customDao.addCustom(sql, param);
}
public List<Custom> findAll(){
return customDao.findAll("select * from custom");
}
public void operation(Integer id1, Integer id2, Double money) {
customDao.operation(id1, id2, money);
}
}
<?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.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd">
<context:property-placeholder location="classpath*:jdbc.properties"/>
<context:component-scan base-package="cn.lacknb"/>
<!-- 基于底层API的编程式管理-->
<!-- 配置数据源-->
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
<property name="driverClassName" value="${jdbc.driver}"/>
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
</bean>
<!-- 为数据源缇添加事务管理器-->
<bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
<!-- 配置jdbc模板-->
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="dataSource"/>
</bean>
<!--这里的约束文件注意一下, 不要导入成xmlns:tx="http://www.springframework.org/schema/cache http://www.springframework.org/schema/cache/spring-cache.xsd-->
<!-- 编通知 声明事务-->
<tx:advice id="myAdvice" transaction-manager="txManager">
<tx:attributes>
<!-- 表示任意方法-->
<tx:method name="*"/>
</tx:attributes>
</tx:advice>
<!-- 编写aop, 让spring自动对目标对象生成代理, 需要使用aspect表达式-->
<aop:config>
<!-- 定义切入点, -->
<!-- 这里就是 设置事务管理的范围-->
<aop:pointcut id="txPonintCut" expression="execution(* cn.lacknb.*.*.*(..))"/>
<!-- 切面: 将切入点与通知关联-->
<aop:advisor advice-ref="myAdvice" pointcut-ref="txPonintCut"/>
</aop:config>
</beans>
配置解释
<!--切面: 通知和顾问-->
<!--注册平台事务管理器:---切面中的顾问-->
<bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" value="myDataSource"/>
</bean>
<!-- 注册事务通知 ---编写顾问-->
<tx:advice id="txAdvice" transaction-manager="txManager">
<!-- 指定连接点 ---可以被增强的目标方法 -->
<tx:attributes>
<!--这里可以直接填方法名, 可以填* 表示任意方法-->
<tx:method name="方法名" isolation="DEFAULT" propagation="REQUIRED" rollback-for="Exception"/>
<!--这里定义, 当发生异常的时候, 进行回滚-->
<tx:method name="方法名" isolation="DEFAULT" propagation="REQUIRED" rollback-for="Exception"/>
</tx:attributes>
</tx:advice>
<!--aop的配置 ---织入顾问 advisor -->
<aop:config>
<!-- '*..' 多级包 '..' 有或没有都可以 -->
<aop:advisor advice-ref="txAdvice" pointcut="execution(void *..dao.*.*(..))"
</aop:config>
package cn.lacknb.test;
import cn.lacknb.beans.Custom;
import cn.lacknb.service.CustomService;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import java.util.List;
@Repository
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = {"classpath:applicationContext.xml"})
public class DelaratinTest {
@Autowired
private CustomService customService;
@Test
public void test01(){
customService.test();
}
@Test
public void test02(){
List<Custom> list = customService.findAll();
for (Custom custom : list){
System.out.println(custom);
}
}
@Test
public void test03(){
// 顾客2 给 顾客3 转300元
customService.operation(3 ,2, 700.0);
}
}
@Transactional注解可以作用于接口、接口方法、类以及类的方法上. 当作用与类上时, 该类的所有public方法都将具有该类型的事务属性, 同时也可以在方法级别使用该注解来覆盖类级别的定义. 虽然@Transactional注解可以作用于接口、接口方法、类以及类的方法上, 但是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.xsd">
<context:property-placeholder location="classpath*:jdbc.properties"/>
<context:component-scan base-package="cn.lacknb"/>
<!-- 基于底层API的编程式管理-->
<!-- 配置数据源-->
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
<property name="driverClassName" value="${jdbc.driver}"/>
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
</bean>
<!-- 配置jdbc模板-->
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="dataSource"/>
</bean>
<!-- 为数据源缇添加事务管理器-->
<bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
<!-- 为事务管理器注册注解驱动器-->
<tx:annotation-driven transaction-manager="txManager"/>
</beans>
package cn.lacknb.dao;
import cn.lacknb.beans.Custom;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.BeanPropertyRowMapper;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
@Repository
@Transactional
// 加上Transactional注解, 就可以指定这个类需要受spring的事务管理
// 注意添加Transactional只能针对public属性范围内的方法添加
public class CustomDaoImpl implements CustomDao {
@Autowired
private JdbcTemplate jdbcTemplate;
public void addCustom(String sql, Object[] param) {
jdbcTemplate.update(sql, param);
}
public void deletCustom(String sql, Object[] param) {
jdbcTemplate.update(sql, param);
}
public List<Custom> findAll(String sql) {
RowMapper<Custom> rowMapper = new BeanPropertyRowMapper<Custom>(Custom.class);
return jdbcTemplate.query(sql, rowMapper);
}
// 顾客id1给id2转钱
public void operation(Integer id1, Integer id2, Double money) {
String sql1 = "update custom set money = money - ? where id = ?";
String sql2 = "update custom set money = money + ? where id = ?";
jdbcTemplate.update(sql1, money, id1);
// 这里制造异常,
int i = 1 / 0;
jdbcTemplate.update(sql2, money, id2);
}
}
package cn.lacknb.service;
import cn.lacknb.beans.Custom;
import cn.lacknb.dao.CustomDao;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
@Service
@Transactional
public class CustomServiceImpl implements CustomService {
@Autowired
private CustomDao customDao;
public void test() {
String sql = "insert into custom(id, name, sex, money) values(?, ?, ?, ?)";
Object param[] = {1, "张三", "女", 1000.0};
customDao.addCustom(sql, param);
// 添加相同的主键, 由于事务的管理, 第一条数据也不会添加成功.
customDao.addCustom(sql, param);
}
public List<Custom> findAll(){
return customDao.findAll("select * from custom");
}
public void operation(Integer id1, Integer id2, Double money) {
customDao.operation(id1, id2, money);
}
}
隔离级别 | 含义 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|---|
TransactionDefinition.ISOLATION_DEFAULT | 使用后端数据库默认的隔离级别 | |||
TransactionDefinition.ISOLATION_READ_UNCOMMITTED | 允许读取尚未提交的数据变更(最低的隔离级别) | 是 | 是 | 是 |
TransactionDefinition.ISOLATION_READ_COMMITTED | 允许读取并发事务已经提交的数据 | 否 | 是 | 是 |
TransactionDefinition.ISOLATION_REPEATABLE_READ | 对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改 | 否 | 否 | 是 |
TransactionDefinition.ISOLATION_SERIALIZABLE | 最高的隔离级别,完全服从ACID的隔离级别,也是最慢的事务隔离级别,因为它通常是通过完全锁定事务相关的数据库表来实现的 | 否 | 否 | 否 |
ISOLATION_SERIALIZABLE 隔离规则类型在开发中很少用到。举个很简单的例子。咱们使用了ISOLATION_SERIALIZABLE规则。A,B两个事务操作同一个数据表并发过来了。A先执行。A事务这个时候会把表给锁住,B事务执行的时候直接报错。
如果开发者在代码逻辑汇总加入try...catch语句, Spring不能在声明式事务处理中正常得到事务回滚的异常信息.