什么是事务? 在一个业务流程中,通常需要多条DML语句联合执行才能完成,因此这多条DML语句必须同时成功或者同时失败,这样才能保证数据的安全 多条DML语句要么同时成功,要么同时失败,这叫做事务 事务(Transaction)TX 四个处理过程 第一步:开启事务 第二步:执行核心业务代码 第三步:提交事务(如果没有出现异常)Commit transaction 第四步:回滚事务(如果出现异常)rollback transaction 四大特性 原子性(最小的单位) 一致性(事务要求要么同时成功,要么同时失败) 隔离性(事务事务之间要互不干扰) 持久性(持久性是事务结束的标志) AOP的实际案例(编程式处理方案/事务),Spring底层对事务的支持底层还是AOP二次封装
什么是事务? 在一个业务流程中,通常需要多条DML语句联合执行才能完成,因此这多条DML语句必须同时成功或者同时失败,这样才能保证数据的安全 多条DML语句要么同时成功,要么同时失败,这叫做事务 事务(Transaction)TX 四个处理过程 第一步:开启事务 第二步:执行核心业务代码 第三步:提交事务(如果没有出现异常)Commit transaction 第四步:回滚事务(如果出现异常)rollback transaction 四大特性 原子性(最小的单位) 一致性(事务要求要么同时成功,要么同时失败) 隔离性(事务事务之间要互不干扰) 持久性(持久性是事务结束的标志) AOP的实际案例(编程式处理方案/事务),Spring底层对事务的支持底层还是AOP二次封装
<?xml version="1.0" encoding="UTF-8"?> <configuration> <loggers> <!--日志级别,由低到高--> <!--ALL < TRACE < DEBUG < INFO < WARN < ERROR < FATAL < OFF--> <root level="DEBUG"> <appender-ref ref="spring6log"/> </root> </loggers> <appenders> <!--输出日志到控制台--> <console name="spring6log" target="SYSTEM_OUT"> <!--控制输出的日志格式--> <PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss SSS} [%t] %-3level %logger{1024} - %msg%n"/> </console> </appenders> </configuration>
<?xml version="1.0" encoding="UTF-8"?> <configuration> <loggers> <!--日志级别,由低到高--> <!--ALL < TRACE < DEBUG < INFO < WARN < ERROR < FATAL < OFF--> <root level="DEBUG"> <appender-ref ref="spring6log"/> </root> </loggers> <appenders> <!--输出日志到控制台--> <console name="spring6log" target="SYSTEM_OUT"> <!--控制输出的日志格式--> <PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss SSS} [%t] %-3level %logger{1024} - %msg%n"/> </console> </appenders> </configuration>
<?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:aop="http://www.springframework.org/schema/aop" 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/aop http://www.springframework.org/schema/aop/spring-aop.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd"> <context:component-scan base-package="com.powernode.bank"></context:component-scan> <aop:aspectj-autoproxy></aop:aspectj-autoproxy> <aop:config proxy-target-class="true"></aop:config> <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource"> <property name="driverClassName" value="com.mysql.cj.jdbc.Driver"></property> <property name="url" value="jdbc:mysql://localhost:13306/spring6"></property> <property name="username" value="root"></property> <property name="password" value="abc123"></property> </bean> <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate"> <property name="dataSource" ref="dataSource"></property> </bean> <!-- 配置事务管理器--> <bean id="tx" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="dataSource"></property> </bean> <!-- 事务注解驱动器开启,采用注解的方式去控制事务--> <tx:annotation-driven transaction-manager="tx"></tx:annotation-driven> </beans>
<?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:aop="http://www.springframework.org/schema/aop" 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/aop http://www.springframework.org/schema/aop/spring-aop.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd"> <context:component-scan base-package="com.powernode.bank"></context:component-scan> <aop:aspectj-autoproxy></aop:aspectj-autoproxy> <aop:config proxy-target-class="true"></aop:config> <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource"> <property name="driverClassName" value="com.mysql.cj.jdbc.Driver"></property> <property name="url" value="jdbc:mysql://localhost:13306/spring6"></property> <property name="username" value="root"></property> <property name="password" value="abc123"></property> </bean> <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate"> <property name="dataSource" ref="dataSource"></property> </bean> <!-- 配置事务管理器--> <bean id="tx" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="dataSource"></property> </bean> <!-- 事务注解驱动器开启,采用注解的方式去控制事务--> <tx:annotation-driven transaction-manager="tx"></tx:annotation-driven> </beans>
package com.powernode.bank.test; import com.powernode.bank.pojo.Account; import com.powernode.bank.service.impl.AccountServiceImpl; import org.junit.Test; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.context.ApplicationContext; import org.springframework.context.support.ClassPathXmlApplicationContext; public class SpringTXTest { private static final Logger logger = LoggerFactory.getLogger(SpringTXTest.class); @Test public void TestSpringTX() { ApplicationContext context = new ClassPathXmlApplicationContext("spring.xml"); AccountServiceImpl accountServiceImpl = context.getBean("accountServiceImpl", AccountServiceImpl.class); try { accountServiceImpl.transfer("act-001","act-002",10000); logger.info("转账成功"); } catch(Exception e) { e.printStackTrace(); } } @Test public void TestPropagation()//测试事务的传播行为 { ApplicationContext context = new ClassPathXmlApplicationContext("spring.xml"); //获取的是一号Service对象 AccountServiceImpl accountServiceImpl = context.getBean("accountServiceImpl", AccountServiceImpl.class); Account account = new Account("act-003",1000); accountServiceImpl.save(account); } }
package com.powernode.bank.test; import com.powernode.bank.pojo.Account; import com.powernode.bank.service.impl.AccountServiceImpl; import org.junit.Test; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.context.ApplicationContext; import org.springframework.context.support.ClassPathXmlApplicationContext; public class SpringTXTest { private static final Logger logger = LoggerFactory.getLogger(SpringTXTest.class); @Test public void TestSpringTX() { ApplicationContext context = new ClassPathXmlApplicationContext("spring.xml"); AccountServiceImpl accountServiceImpl = context.getBean("accountServiceImpl", AccountServiceImpl.class); try { accountServiceImpl.transfer("act-001","act-002",10000); logger.info("转账成功"); } catch(Exception e) { e.printStackTrace(); } } @Test public void TestPropagation()//测试事务的传播行为 { ApplicationContext context = new ClassPathXmlApplicationContext("spring.xml"); //获取的是一号Service对象 AccountServiceImpl accountServiceImpl = context.getBean("accountServiceImpl", AccountServiceImpl.class); Account account = new Account("act-003",1000); accountServiceImpl.save(account); } }
package com.powernode.bank; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.EnableAspectJAutoProxy; @ComponentScan({"com.powernode.bank"}) @Configuration @EnableAspectJAutoProxy(proxyTargetClass = true) public class Spring6Config { //Spring对事务的支持,实现事务的两种方式 //编程式事务:通过编写代码来实现事务的管理 //声明式事务:基于注解和XML配置方式 //Spring对事务的管理底层实现是基于AOP实现的,采用了AOP的方式进行封装 //DataSourceTransactionManager:支持jdbcTemplate,Mybatis,Hibernate //JtaTransactionManager:支持分布式事务管理 }
package com.powernode.bank; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.EnableAspectJAutoProxy; @ComponentScan({"com.powernode.bank"}) @Configuration @EnableAspectJAutoProxy(proxyTargetClass = true) public class Spring6Config { //Spring对事务的支持,实现事务的两种方式 //编程式事务:通过编写代码来实现事务的管理 //声明式事务:基于注解和XML配置方式 //Spring对事务的管理底层实现是基于AOP实现的,采用了AOP的方式进行封装 //DataSourceTransactionManager:支持jdbcTemplate,Mybatis,Hibernate //JtaTransactionManager:支持分布式事务管理 }
package com.powernode.bank.service; import com.powernode.bank.pojo.Account; import org.springframework.stereotype.Service; @Service public interface AccountService//业务接口 { //从这个账户转出,转到另外一个账户,金额是 void transfer(String Actno,String toActno,double money); void save(Account account);//保存账户信息 void withdraw(); }
package com.powernode.bank.service; import com.powernode.bank.pojo.Account; import org.springframework.stereotype.Service; @Service public interface AccountService//业务接口 { //从这个账户转出,转到另外一个账户,金额是 void transfer(String Actno,String toActno,double money); void save(Account account);//保存账户信息 void withdraw(); }
package com.powernode.bank.service.impl; import com.powernode.bank.dao.AccountDao; import com.powernode.bank.pojo.Account; import com.powernode.bank.service.AccountService; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; import javax.annotation.Resource; @Service("accountService2") public class AccountServiceImpl2 implements AccountService { @Resource(name = "accountDao") private AccountDao accountDao; @Override public void transfer(String Actno, String toActno, double money) { } @Override @Transactional(propagation = Propagation.REQUIRES_NEW) public void save(Account account) { accountDao.insert(account); //异常 String s = null; s.toString(); //大括号没结束,事务没结束 //事务没结束就得回滚 //后续可能还有其他的DML操作,这个大括号内也许还有其他DML语句的 //回滚了,数据是安全的 } @Override public void withdraw() { } }
package com.powernode.bank.service.impl; import com.powernode.bank.dao.AccountDao; import com.powernode.bank.pojo.Account; import com.powernode.bank.service.AccountService; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; import javax.annotation.Resource; @Service("accountService2") public class AccountServiceImpl2 implements AccountService { @Resource(name = "accountDao") private AccountDao accountDao; @Override public void transfer(String Actno, String toActno, double money) { } @Override @Transactional(propagation = Propagation.REQUIRES_NEW) public void save(Account account) { accountDao.insert(account); //异常 String s = null; s.toString(); //大括号没结束,事务没结束 //事务没结束就得回滚 //后续可能还有其他的DML操作,这个大括号内也许还有其他DML语句的 //回滚了,数据是安全的 } @Override public void withdraw() { } }
package com.powernode.bank.service.impl; import com.powernode.bank.dao.AccountDao; import com.powernode.bank.pojo.Account; import com.powernode.bank.service.AccountService; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Isolation; import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; import javax.annotation.Resource; @Service //写在类上所有的方法都应用事务 public class AccountServiceImpl implements AccountService { @Resource(name = "accountDao") private AccountDao accountDao; @Resource(name = "accountService2") private AccountServiceImpl2 accountServiceImpl2; @Override @Transactional(isolation = Isolation.READ_COMMITTED) //数据库三大读问题 //脏读:读取到还没有提交到数据库的数据,叫做脏读(读到内存里面没有commit的数据) //不可重复读:在同一个事务中,第一次和第二次读到的数据不一样(在同一个事务中读到的数据前后不一致) //幻读:读到的数据是假的(只要A事务和B事务在同时进行时,永远都会幻读) //脑子里想象的数据和真实数据不一样,就是幻读,只要多个事务并发,一定会幻读 //多个事务并发一定会幻读 //读未提交:会读内存中的数据,不可重复读,因为事务并发会导致事务中读到的信息前后不一致,导致幻读 //读提交:不会读内存中的数据,仅读取数据库commit了的数据,仍然会因为事务并发读到前后不一致的信息,导致幻读 //可重复读:不会读内存中的数据,仅读取commit数据,在事务未结束前,数据库读取到的缓存信息始终不变,但是会导致幻读(因为其他事务已经修改了数据库信息) //序列化:事务必须排队执行,不支持并发,解决了幻读问题 public void transfer(String Actno, String toActno, double money) { //开启事务 //需要控制事务,因为在这个方法中要完成所有的转账业务 //查询转出账户余额是否充足 Account account = accountDao.selectByAccountNo(Actno); if(account.getBalance() < money) { throw new RuntimeException("余额不足"); } Account toAccount = accountDao.selectByAccountNo(toActno); account.setBalance(account.getBalance() - money); toAccount.setBalance(toAccount.getBalance() + money); int update = accountDao.update(account); //模拟异常信息导致中断业务 // String s = null; // s.toString(); update = update + accountDao.update(toAccount); //提交事务 //遇到异常回滚事务 if(update != 2) { throw new RuntimeException("转账失败,请联系银行"); } withdraw(); } @Override @Transactional(propagation = Propagation.REQUIRED) public void save(Account account) { //这里有一个保存操作 //调Dao的保存 accountDao.insert(account);//保存act-003 //创建账户对象 Account ac = new Account("act-004",1000); //如果这里不捕捉,一号事务内也无法执行完毕,也会回滚数据 //因为他们两个是完全不同事务 try { accountServiceImpl2.save(ac);//保存act-003 } catch(Exception e) { e.printStackTrace(); } //继续往后执行一号的事务 //应该是3号提交了,4号没能保存 } @Override @Transactional(propagation = Propagation.REQUIRED) public void withdraw() { //Propagation.REQUIRED如果原先有事务,就调用原事务,如果没有就创建一个新事务(同一个事务) //Propagation.SUPPORTS支持当前事务,有就执行,没有就没有(原事务) //Propagation.MANDATORY(强制性的)有就加入当前事务,没有就抛出异常信息(原事务) //Propagation.REQUIRES_NEW不管调用处是否有业务,只要调用,就创建一个新业务,并把老业务挂起(新事务) //Propagation.NOT_SUPPORTED以非事务方式运行,如果有事务存在,挂起当前事务(无事务) //Propagation.NEVER以非事务方式运行,如果有事务存在,抛出异常(无事务) //Propagation.NESTED如果当前有事务存在运行中,则该方法运行在一个嵌套式事务中,被嵌套的事务可以独立外层事务进行提交或回滚 //没有事务就视为required一样(两个事务并行) } }
package com.powernode.bank.service.impl; import com.powernode.bank.dao.AccountDao; import com.powernode.bank.pojo.Account; import com.powernode.bank.service.AccountService; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Isolation; import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; import javax.annotation.Resource; @Service //写在类上所有的方法都应用事务 public class AccountServiceImpl implements AccountService { @Resource(name = "accountDao") private AccountDao accountDao; @Resource(name = "accountService2") private AccountServiceImpl2 accountServiceImpl2; @Override @Transactional(isolation = Isolation.READ_COMMITTED) //数据库三大读问题 //脏读:读取到还没有提交到数据库的数据,叫做脏读(读到内存里面没有commit的数据) //不可重复读:在同一个事务中,第一次和第二次读到的数据不一样(在同一个事务中读到的数据前后不一致) //幻读:读到的数据是假的(只要A事务和B事务在同时进行时,永远都会幻读) //脑子里想象的数据和真实数据不一样,就是幻读,只要多个事务并发,一定会幻读 //多个事务并发一定会幻读 //读未提交:会读内存中的数据,不可重复读,因为事务并发会导致事务中读到的信息前后不一致,导致幻读 //读提交:不会读内存中的数据,仅读取数据库commit了的数据,仍然会因为事务并发读到前后不一致的信息,导致幻读 //可重复读:不会读内存中的数据,仅读取commit数据,在事务未结束前,数据库读取到的缓存信息始终不变,但是会导致幻读(因为其他事务已经修改了数据库信息) //序列化:事务必须排队执行,不支持并发,解决了幻读问题 public void transfer(String Actno, String toActno, double money) { //开启事务 //需要控制事务,因为在这个方法中要完成所有的转账业务 //查询转出账户余额是否充足 Account account = accountDao.selectByAccountNo(Actno); if(account.getBalance() < money) { throw new RuntimeException("余额不足"); } Account toAccount = accountDao.selectByAccountNo(toActno); account.setBalance(account.getBalance() - money); toAccount.setBalance(toAccount.getBalance() + money); int update = accountDao.update(account); //模拟异常信息导致中断业务 // String s = null; // s.toString(); update = update + accountDao.update(toAccount); //提交事务 //遇到异常回滚事务 if(update != 2) { throw new RuntimeException("转账失败,请联系银行"); } withdraw(); } @Override @Transactional(propagation = Propagation.REQUIRED) public void save(Account account) { //这里有一个保存操作 //调Dao的保存 accountDao.insert(account);//保存act-003 //创建账户对象 Account ac = new Account("act-004",1000); //如果这里不捕捉,一号事务内也无法执行完毕,也会回滚数据 //因为他们两个是完全不同事务 try { accountServiceImpl2.save(ac);//保存act-003 } catch(Exception e) { e.printStackTrace(); } //继续往后执行一号的事务 //应该是3号提交了,4号没能保存 } @Override @Transactional(propagation = Propagation.REQUIRED) public void withdraw() { //Propagation.REQUIRED如果原先有事务,就调用原事务,如果没有就创建一个新事务(同一个事务) //Propagation.SUPPORTS支持当前事务,有就执行,没有就没有(原事务) //Propagation.MANDATORY(强制性的)有就加入当前事务,没有就抛出异常信息(原事务) //Propagation.REQUIRES_NEW不管调用处是否有业务,只要调用,就创建一个新业务,并把老业务挂起(新事务) //Propagation.NOT_SUPPORTED以非事务方式运行,如果有事务存在,挂起当前事务(无事务) //Propagation.NEVER以非事务方式运行,如果有事务存在,抛出异常(无事务) //Propagation.NESTED如果当前有事务存在运行中,则该方法运行在一个嵌套式事务中,被嵌套的事务可以独立外层事务进行提交或回滚 //没有事务就视为required一样(两个事务并行) } }
package com.powernode.bank.dao; import com.powernode.bank.pojo.Account; public interface AccountDao { //专门负责账户的CRUD操作,只执行SQL语句 //只负责增删改查 //通过账号ID查询账户信息 Account selectByAccountNo(String actno); //更新账户信息 int update(Account account); int insert(Account account); }
package com.powernode.bank.dao; import com.powernode.bank.pojo.Account; public interface AccountDao { //专门负责账户的CRUD操作,只执行SQL语句 //只负责增删改查 //通过账号ID查询账户信息 Account selectByAccountNo(String actno); //更新账户信息 int update(Account account); int insert(Account account); }
package com.powernode.bank.pojo; public class Account { private String actno; private double balance; @Override public String toString() { return "Account{" + "actno='" + actno + '\'' + ", balance=" + balance + '}'; } public String getActno() { return actno; } public void setActno(String actno) { this.actno = actno; } public double getBalance() { return balance; } public void setBalance(double balance) { this.balance = balance; } public Account(String actno, double balance) { this.actno = actno; this.balance = balance; } public Account() { } }
package com.powernode.bank.pojo; public class Account { private String actno; private double balance; @Override public String toString() { return "Account{" + "actno='" + actno + '\'' + ", balance=" + balance + '}'; } public String getActno() { return actno; } public void setActno(String actno) { this.actno = actno; } public double getBalance() { return balance; } public void setBalance(double balance) { this.balance = balance; } public Account(String actno, double balance) { this.actno = actno; this.balance = balance; } public Account() { } }
package com.powernode.bank.dao.impl; import com.powernode.bank.dao.AccountDao; import com.powernode.bank.pojo.Account; import org.springframework.jdbc.core.BeanPropertyRowMapper; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Repository; import javax.annotation.Resource; @Repository("accountDao") public class AccountDaoImpl implements AccountDao { @Resource(name = "jdbcTemplate") private JdbcTemplate jdbcTemplate; @Override public Account selectByAccountNo(String actno) { String sql = "select actno,balance from t_act where actno = ?"; Account account = jdbcTemplate.queryForObject(sql,new BeanPropertyRowMapper<>(Account.class),actno); return account; } @Override public int update(Account account) { String sql = "update t_act set balance = ? where actno = ?"; int update = jdbcTemplate.update(sql, account.getBalance(), account.getActno()); return update; } @Override public int insert(Account account) { String sql = "insert into t_act(actno,balance) values(?,?)"; int update = jdbcTemplate.update(sql, account.getActno(), account.getBalance()); return update; } }
package com.powernode.bank.dao.impl; import com.powernode.bank.dao.AccountDao; import com.powernode.bank.pojo.Account; import org.springframework.jdbc.core.BeanPropertyRowMapper; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Repository; import javax.annotation.Resource; @Repository("accountDao") public class AccountDaoImpl implements AccountDao { @Resource(name = "jdbcTemplate") private JdbcTemplate jdbcTemplate; @Override public Account selectByAccountNo(String actno) { String sql = "select actno,balance from t_act where actno = ?"; Account account = jdbcTemplate.queryForObject(sql,new BeanPropertyRowMapper<>(Account.class),actno); return account; } @Override public int update(Account account) { String sql = "update t_act set balance = ? where actno = ?"; int update = jdbcTemplate.update(sql, account.getBalance(), account.getActno()); return update; } @Override public int insert(Account account) { String sql = "insert into t_act(actno,balance) values(?,?)"; int update = jdbcTemplate.update(sql, account.getActno(), account.getBalance()); return update; } }
CREATE DATABASE spring6; USE spring6 CREATE TABLE t_act(actno VARCHAR(100) PRIMARY KEY NOT NULL,balance DOUBLE); INSERT INTO t_act(actno,balance) VALUE('act-001',50000); INSERT INTO t_act(actno,balance) VALUE('act-002',0); SELECT * FROM t_act; UPDATE t_act SET balance = 50000 WHERE actno = 'act-001'; UPDATE t_act SET balance = 0 WHERE actno = 'act-002'; DELETE FROM t_act WHERE actno = 'act-003'; DELETE FROM t_act WHERE actno = 'act-004';
CREATE DATABASE spring6;
USE spring6
CREATE TABLE t_act(actno VARCHAR(100) PRIMARY KEY NOT NULL,balance DOUBLE);
INSERT INTO t_act(actno,balance) VALUE('act-001',50000);
INSERT INTO t_act(actno,balance) VALUE('act-002',0);
SELECT * FROM t_act;
UPDATE t_act SET balance = 50000 WHERE actno = 'act-001';
UPDATE t_act SET balance = 0 WHERE actno = 'act-002';
DELETE FROM t_act WHERE actno = 'act-003';
DELETE FROM t_act WHERE actno = 'act-004';