spring事务控制的原理解析1
一、概述
事务的本质是依赖数据库的支持来实现事务的控制,也就是java应用本身不支持事务,事务是数据库实现的。
简单来说,客户端通过connection和数据库建立连接,并通过connection来执行sql,所以只要是通过同一个connection来执行的sql,数据库就可以控制这些sql同时回滚或者同时提交,也就是事务的原子性。
一、jdbc的事务控制
上边提到事务控制的本质是通过connection来实现的,所以jdbc操作数据库时,只要保证多条sql语句是通过同一个connection来执行的,就可以通过这个connection来提交或者回滚事务。
1.1 简单代码示意
先来看一段简单的示例代码
public static void main(String[] args) throws SQLException {
DruidDataSource dataSource = new DruidDataSource();
dataSource.setUsername("root");
dataSource.setPassword("root");
dataSource.setDriverClassName("com.mysql.jdbc.Driver");
dataSource.setUrl("jdbc:mysql://localhost:3306/transaction_test");
//获取连接
Connection connection = dataSource.getConnection();
connection.setAutoCommit(false);
//zs给ls转账的sql
PreparedStatement sql1 = connection.prepareStatement("UPDATE t_bank SET money=money-100 WHERE ACCOUNT='zs'");
PreparedStatement sql2 = connection.prepareStatement("UPDATE t_bank SET money=money+100 WHERE ACCOUNT='ls'");
try {
//执行sql
sql1.executeUpdate();
int i=1/0;
sql2.executeUpdate();
//提交事务
connection.commit();
} catch (SQLException e) {
e.printStackTrace();
//回滚事务
connection.rollback();
}
}
这段代码中先用dataSource获取了一个数据库连接,再利用这个connection来执行两条sql,这俩sql就可以通过这个connection来进行事务控制。
1.2 web应用三层架构中的使用
如果按web应用的三层架构来写,并整合spring,一般可能会这样写。
先有一个BankService,在其中注入两个dao,在transfer方法中调用这两个dao的方法完成转账测试。
@Service
public class BankService {
@Autowired
private IncrementDao incrementDao;
@Autowired
private DecrementDao decrementDao;
public void transfer() throws SQLException {
incrementDao.increment();
int i=1/0;
decrementDao.decrement();
}
}
然后分别给出两个dao的代码
@Repository
public class IncrementDao {
@Autowired
DataSource dataSource;
public void increment() throws SQLException {
//获取连接
Connection connection = dataSource.getConnection();
connection.setAutoCommit(false);
PreparedStatement sql1 = connection.prepareStatement("UPDATE t_bank SET money=money-100 WHERE ACCOUNT='zs'");
//执行sql
sql1.executeUpdate();
//提交事务
connection.commit();
}
}
@Repository
public class DecrementDao {
@Autowired
private DataSource dataSource;
public void decrement() throws SQLException {
//获取连接
Connection connection = dataSource.getConnection();
connection.setAutoCommit(false);
//zs给ls转账的sql
PreparedStatement sql2 = connection.prepareStatement("UPDATE t_bank SET money=money+100 WHERE ACCOUNT='ls'");
sql2.executeUpdate();
connection.commit();
}
}
那么这样的写法能控制住事务吗?回想下上边的内容,只有通过同一个connection执行的sql才能被事务控制住,
按上边这样写两个sql是通过两个connection执行的,所以是控制不住事务的。
如果要控制这个transfer方法的事务就要让两个dao中使用同一个connection,当然首先想到的是在service层创建connection然后通过方法参数传到dao层这样就是同一个了,但这种方式也许不够优雅,我们可以尝试通过ThreadLocal来在同一个线程范围内传递connection这样就不用通过方法参数传递了。
我们可以创建一个 MyTransactionSourceManager类来保存ThreadLocal变量
public class MyTransactionSourceManager {
private static ThreadLocal<Map<Object,Object>> resources = new ThreadLocal<>();
static {
resources.set(new HashMap<>());
}
public static Object getResource(Object key){
Map<Object, Object> map = resources.get();
return map.get(key);
}
public static void registerRsource(Object key,Object value){
Map<Object, Object> map = resources.get();
map.put(key,value);
}
}
这里的resources属性用来在线程范围内共享变量,内部放一个map是为了能放置多个需要共享的变量,并且提供了获取值和注册值的方法。
现在我们可以在service中先获取数据库连接,然后注册到resources中,在dao层方法中再从resources中取出connection来执行sql,然后在service方法的最后再从resources中取出connection来提交事务或者回滚事务。
下面是经过改进的service
@Service
public class BankService {
@Autowired
private IncrementDao incrementDao;
@Autowired
private DecrementDao decrementDao;
@Autowired
private DataSource dataSource;
public void transfer() throws SQLException {
//先获取数据库连接,并放到MyTransactionSourceManager.resources
Connection connection = dataSource.getConnection();
connection.setAutoCommit(false);
MyTransactionSourceManager.registerRsource(dataSource,connection);
try {
incrementDao.increment();
int i=1/0;
decrementDao.decrement();
//取出连接提交事务
Connection resource = (Connection)MyTransactionSourceManager.getResource(dataSource);
resource.commit();
} catch (Exception e) {
//回滚事务
Connection resource = (Connection)MyTransactionSourceManager.getResource(dataSource);
resource.rollback();
e.printStackTrace();
}
}
}
那么dao层也要改进,从MyTransactionSourceManager中获取数据库连接,并且不在dao层提交事务
@Repository
public class IncrementDao {
@Autowired
DataSource dataSource;
public void increment() throws SQLException {
//获取连接
Connection connection = (Connection)MyTransactionSourceManager.getResource(dataSource);
PreparedStatement sql1 = connection.prepareStatement("UPDATE t_bank SET money=money-100 WHERE ACCOUNT='zs'");
//执行sql
sql1.executeUpdate();
}
}
@Repository
public class DecrementDao {
@Autowired
private DataSource dataSource;
public void decrement() throws SQLException {
//获取连接
Connection connection = (Connection) MyTransactionSourceManager.getResource(dataSource);
//zs给ls转账的sql
PreparedStatement sql2 = connection.prepareStatement("UPDATE t_bank SET money=money+100 WHERE ACCOUNT='ls'");
sql2.executeUpdate();
}
}
这样就满足了service方法中的多个dao使用同一个数据库连接来执行sql,所以它的事务是能够被控制的。
1.3 使用aop进行改进
上面这种方式实现了在spring+三层架构中进行事务控制,关键点是保证最终执行sql时使用的是同一个connection。
但这种方式还是不够优雅,我们先关注下service层的代码,事务控制跟获取数据库连接的代码和业务代码柔和在一起,这种状况不利于代码的维护。所以我们可以使用aop来把这部分跟业务不相关的代码抽取出来。
首先我们抽取出一个通知类,注意上边要加@Aspect注解,然后把它配置到spring容器中,可以在此类上加@Component注解,但这样要注意扫描路径要包含这个类,也可以在配置类中用@Bean来添加
@Aspect
public class TransactionAspect {
@Autowired
private DataSource dataSource;
//定义切入点表达式
@Pointcut("@annotation(com.lyy.transaction_source.config.MyTransaction)")
public void pt1(){
}
//通过环绕通知控制事务
@Around("pt1()")
public Object transactionControl(ProceedingJoinPoint point) throws SQLException {
Object returnValue=null;
//先获取数据库连接,并放到MyTransactionSourceManager.resources
Connection connection = dataSource.getConnection();
connection.setAutoCommit(false);
MyTransactionSourceManager.registerRsource(dataSource,connection);
try {
System.out.println("代理类执行了");
Object[] args = point.getArgs();
point.proceed(args);
//提交事务
connection.commit();
} catch (Throwable e) {
e.printStackTrace();
connection.rollback();
System.out.println("--回滚事务--");
}
return returnValue;
}
}
注意上面的切入点表达式切的是自定义注解,所以我们创建一个自定义注解
//自定义的事务控制的标识注解
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface MyTransaction {
}
在配置类上开启spring对注解aop的支持,并且配置这个切面类为bean
@Configuration
@ComponentScan(basePackages = {"com.lyy.transaction_source.service","com.lyy.transaction_source.dao"})
@EnableAspectJAutoProxy//开启注解aop
public class SpringConfig {
@Bean
public DataSource dataSource(){
DruidDataSource dataSource = new DruidDataSource();
dataSource.setUsername("root");
dataSource.setPassword("root");
dataSource.setDriverClassName("com.mysql.jdbc.Driver");
dataSource.setUrl("jdbc:mysql://localhost:3306/transaction_test");
return dataSource;
}
//配置事务控制切面类为spring的bean
@Bean
public TransactionAspect transactionAspect(){
return new TransactionAspect();
在service层方法上添加上边的自定义注解,删掉事务控制的代码
@MyTransaction
public void transfer() throws SQLException {
incrementDao.increment();
int i=1/0;
decrementDao.decrement();
}
然后就会发现这样处理也是可以控制住事务的,这样是不是就优雅多了。
关于spring中是如何实现的,我们下一篇在详细分析