MySQL事务——JDBC实现、MyBatis实现与MyBatis整合Spring实现
一、什么是事务
首先来看一个生活中十分常见的场景——网上购物。在网上购物时我们挑选完自己心仪的商品并选定号需要购买的数量后就需要进行付款了,我们假设我们需要支付1000元,那么当我们进行付款时,系统会将商品数量减去我们需要的数量,并在我们的账户中扣除1000元,再将卖家账户的余额增加一千元,并生成订单,看似很简单,一气呵成并不会有什么问题,但如果在减完商品数量后发现我们的余额不足,又或是在扣除了我们的余额后系统出现了故障或者网络连接中断怎么办呢?事务就是用来解决这样的场景的工具
事务是一个最小的不可再分的工作单元,通常对应于一个完整的业务,就像上面的支付过程一样。一个完整的事务往往需要批量的DML(insert、update、delete)语句联合完成,当事务执行过程中出现问题时,事务会被回滚,将数据库恢复到事务执行前的状态
事务的特征——ACID
- 原子性(Atomicity):又称不可分割性,即一个事务中的所有操作是一体的不可分割的,要么全部完成,要么全部不完成,不会在中间某个环节结束。一旦事务执行过程中发生错误,将会回滚到事务开始前的状态,就好像这个事务从来没有发生过
- 一致性(Consistency):在事务开始前和结束后,数据库的完整性没有被破坏,所有数据状态应当是一致的,例如在转账操作中一个账户减去了一百则一定有另一个账户增加了一百
- 隔离性(Isolation):又称独立性,数据库允许多个并发事务同时对其数据进行读写与修改,而隔离性则可以防止多个事务并发执行时由于交叉执行导致的数据不一致
- 持久性(Durability):事务结束后,对数据的修改时永久的,即便系统故障也不会丢失
MySQL事务的基本用法
- 开启事务:BEGIN或START TRANSACTION显示开启一个事务
- 提交事务:COMMIT或COMMIT WORK
- 回滚:ROLLBACK或ROLLBACK WORK
- 创建保存点:SAVEPOINT identifier,在事务中创建一个保存点,一个事务中可以有多个保存点
- 删除保存点:RELEASE SAVEPOINT identifier,删除一个事务的保存点,若没有指定的保存点时抛出异常
- 回滚到保存点:ROLLBACK TO identifier,回滚到指定保存点
- 设置隔离级别:set transaction isolation level,隔离级别有read uncommitted、read committed、repeatable read和serializable四种
- 设置自动提交与手动提交:set autocommit=0/1,0代表开启手动提交,1代表开启自动提交,手动提交情况下一个事务开启到其提交之间是一个完整的事务周期,需要用户显示的执行提交命令才能提交事务,否则将一直处于未提交状态(这里可以取尝试一下,打开两个Mysql窗口,或者连接上Navicat,在关闭自动提交后,即使不使用begin开启一个事务,执行完一句SQL语句后变化也不会立马反映在数据库中,另一个窗口或者Navicat中依然查询到的是原结果);自动提交情况下,每一个语句都视为单独的事务,不需要用户显示的提交,相当于在每句SQL语句后都加上了一句commit命令
实例:
首先我们准备了一张钱包wallet的表,有用户名和余额两个属性,其中用户名为主键
create table wallet(
usename varchar(20) not null primary key,
balance float not null
) engine=innodb;
然后创建一个事务并执行,我们可以看到在开启事务之前表示空的,开启事务后先添加一条记录zhangsan,并记为保存点point1,然后为zhangsan的余额增加500元,查询结果显示增加成功,然后回滚到point1,再次查询后发现余额又重新变为了5000.5,提交事务后查询显示表中有‘zhangsan’,‘5000.5’的记录
mysql> select * from wallet;
Empty set (0.00 sec)
mysql> insert into wallet value('zhangsan',5000.5);
Query OK, 1 row affected (0.00 sec)
mysql> select * from wallet;
+----------+---------+
| usename | balance |
+----------+---------+
| zhangsan | 5000.5 |
+----------+---------+
1 row in set (0.00 sec)
mysql> savepoint point1;
Query OK, 0 rows affected (0.00 sec)
mysql> update wallet set balance=balance+500 where usename='zhangsan';
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0
mysql> select * from wallet;
+----------+---------+
| usename | balance |
+----------+---------+
| zhangsan | 5500.5 |
+----------+---------+
1 row in set (0.00 sec)
mysql> rollback to point1;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from wallet;
+----------+---------+
| usename | balance |
+----------+---------+
| zhangsan | 5000.5 |
+----------+---------+
1 row in set (0.00 sec)
mysql> commit;
Query OK, 0 rows affected (0.01 sec)
mysql> select * from wallet;
+----------+---------+
| usename | balance |
+----------+---------+
| zhangsan | 5000.5 |
+----------+---------+
1 row in set (0.00 sec)
二、事务隔离级别
由于数据库允许多个事务并发进行,那么多个事务在同时对同一块数据进行操作时就很容易造成一些错误,当然最简单的方法时对数据库资源加锁,然而加锁会大幅度影响并发性能,因此需要对事务进行隔离,防止事务之间互相干扰。在了解隔离级别之前有必要先来了解如果不进行隔离会发生哪些问题
脏读、幻读与不可重复读
- 脏读
脏读指一个数据在执行过程中读取到了另一个事务还未提交的数据,如果该未提交的事务发生了回滚,显然另一个事务最终使用的就是一个错误的数据,即脏数据。
例如卖家A原来拥有5000元余额,买家B想要向A转账1000元,与此同时A又想要支出500元,那么这里就有两个事务,事务1尝试在A的账户+1000元在B的账户-1000元,事务2尝试在A的账户-500元,若事务1已将完成两个操作但还没有提交,而事务2则在5000+1000的基础上进行了-500的操作,但事务1提交时发生了网络错误被回滚,而事务2则成功提交,那么A的账户则变成了5500元,凭空多出了1000元,这就是脏读的后果
- 不可重复读
不可重复读是指一个事务在执行过程中两次先后读取同一数据,两次读取的结果不一样,不可重复读与脏读的区别在于,脏读读取的是未提交的数据,而不可重复读则是读取到了提交的数据与第一次不一致
例如某用户想提现自己的账户,执行事务A第一次查询到余额为5000元,而另一个事务B在这之后执行了扣款1000的操作并提交,那么事务A在第二次执行提现操作时却只提现出4000元,这就出现了不可重复读
- 幻读
幻读指一个事务在执行过程中多次查询表中数据,但由于其他数据的影响导致多次查询的条数不一致。
这个概念看上去和不可重复很相似,但不可重复读的重点在于对数据的修改,即多次读取某一条数据发现数据的某一列前后不一致,而幻读的重点则在于对数据的增加与删除,即在某一范围内的数据被其他事务增加或删除并提交,导致原来不应当存在的数据在该事务第二次读取时出现了。例如有一张用户表有账号和密码两个属性,其中账号为主键,事务A想要增加一个用户zhangsan,用户B同样想增加一个用户zhangsan,当事务A第一次查询表中是否已存在zhangsan时返回的结果是不存在,而此时事务B完成了插入zhangsan的动作,由于事务A第一次查询时得到的结果是不存在,A也开始执行插入zhangsan的操作,然而却发生了主键冲突,这就发生了幻读
四种隔离级别
- read uncommitted
read uncommitted即读未提交,顾名思义该隔离级别允许事务读取其他事务未提交的数据,显然read uncommitted会出现脏读的现象,read uncommitted是最低的隔离级别,数据库的隔离级别一般高于read uncommitted
- read committed
read committed即读提交,该隔离级别下事务无法读取其他事务未提交的数据,但可以读取其他事务提交后的数据,显然read committed可以避免脏读,但会出现不可重复读的情况
例如我们打开两个MySQL命令行用来开启两个事务,并将全局隔离级别设置为read committed,一个事务将修改一条记录中的balance,另一个事务只读取记录,我们可以看到只读取记录的事务两次读取到的结果不同,出现了不可重复读
时间 | 事务A | 事务B |
---|---|---|
t1 | begin | begin |
t2 | select | select |
t3 | update | |
t4 | commit | |
t5 | select | |
t6 | commit |
- repeatable read
repeatable read即重复读,这是MySQL的默认隔离级别,当使用可重复读隔离级别时,在事务执行期间会锁定该事务以任何方式引用的所有行,避免其他事务修改本事务需要使用的记录,但无法避免其他事务对记录条数的更新,即repeatable read可以避免不可重复读但无法避免幻读
例如我们依然用两个窗口启动两个事务,一个事务直接插入一条数据,另一个数据先查询数据是否存在,再进行插入,可以看到在前一个事务尚未提交时,没有查询到‘xiaofang’的记录,但当前一个事务插入完成提交后,后一个事务再次插入时出现了主键冲突,显然出现了幻读现象
时间 | 事务A | 事务B |
---|---|---|
t1 | begin | begin |
t2 | select | |
t3 | insert | |
t4 | commit | |
t5 | insert | |
t6 | commit |
- serializable
serializable指串行化,是最高的隔离级别,当一个事务在执行时,其他事务必须排队等待,serializable可以避免脏读、幻读与不可重复读,但它最大的问题在于使得所有事务必须串行执行,吞吐量低,对用户体验影响较大,因此serializable一般很少使用
四种隔离级别可能出现的错误情况
隔离级别 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|
read uncommitted | 可能 | 可能 | 可能 |
read committed | 不可能 | 可能 | 可能 |
repeatable read | 不可能 | 不可能 | 可能 |
serializable | 不可能 | 不可能 | 不可能 |
三、JDBC处理事务
方法
- void setAutoCommit(boolean autoCommit):设置事务的自动提交与手动提交,传入的参数为true时打开自动提交,参数为false时关闭自动提交。JDBC默认开启自动提交
- void commit():提交事务
- void rollback():回滚事务
- void setTransactionIsolation(int level):设置事务隔离级别,JDBC的隔离级别参数如下(以下参数均是Connection接口的静态属性):
- TRANSACTION_NONE(不支持事务,值为0)
- TRANSACTION_READ_UNCOMMITTED(值为1)
- TRANSACTION_READ_COMMITTED(值为2)
- TRANSACTION_REPEATABLE_READ(值为4)
- TRANSACTION_SERIALIZABLE(值为8)
实例
我们依然使用上面的wallet表,以转账场景为例
//Connection单例工厂
public class DBUnit {
private static Connection connection = null;
private static String DRIVER = "com.mysql.jdbc.Driver";
private static String URL = "jdbc:mysql://localhost:3306/test";
private static String USENAME = "root";
private static String PSWD = "LZSF2239";
public static Connection getConnection(){
if (connection == null) {
try {
Class.forName(DRIVER);
connection = DriverManager.getConnection(URL, USENAME, PSWD);
} catch (Exception e) {
e.printStackTrace();
}
}
return connection;
}
}
public class Main {
public static void main(String[] args) {
String from,to;
float sum;
Scanner scanner = new Scanner(System.in);
from = scanner.next();
to = scanner.next();
sum = scanner.nextFloat();
new Main().transferAccounts(from,to,sum);
}
//查找账户是否存在
public boolean findAccount(Connection connection,String usename){
String sql = "select * from wallet where usename=?";
try {
PreparedStatement pstm = connection.prepareStatement(sql);
pstm.setString(1,usename);
ResultSet resultSet = pstm.executeQuery();
if (resultSet.next()){return true;}
} catch (Exception e) {
e.printStackTrace();
}
return false;
}
//账户余额变动
public void changesInAccount(Connection connection,String usename,float sum){
String sql = "update wallet set balance=balance+? where usename=?";
try{
PreparedStatement pstm = connection.prepareStatement(sql);
pstm.setFloat(1,sum);
pstm.setString(2,usename);
pstm.executeUpdate();
}catch (Exception e){
e.printStackTrace();
}
}
//检查余额是否大于0
public boolean balanceEnoungh(Connection connection,String usename){
String sql = "select * from wallet where usename=? and balance >= 0";
try{
PreparedStatement pstm = connection.prepareStatement(sql);
pstm.setString(1,usename);
ResultSet set = pstm.executeQuery();
if (set.next()){return true;}
}catch (Exception e){
e.printStackTrace();
}
return false;
}
//转账
public void transferAccounts(String from,String to,float sum){
Connection connection = DBUnit.getConnection();
try {
connection.setAutoCommit(false);
if (!findAccount(connection,to)){throw new FailToTransferException("对方账户不存在,转账失败!");}
//这里的余额不足的检查其实可以用乐观锁来解决,但这里为了更好的了解事物,没有使用乐观锁
changesInAccount(connection,from,-sum);
if (!balanceEnoungh(connection,from)){throw new FailToTransferException("您的账户余额不足,转账失败!");}
changesInAccount(connection,to,sum);
connection.commit();
System.out.println("转账成功");
} catch (SQLException e) {
e.printStackTrace();
}catch (FailToTransferException e){
System.out.println(e.getMessage());
try {
connection.rollback();
} catch (SQLException e1) {
e1.printStackTrace();
}
}
}
}
class FailToTransferException extends Exception{
public FailToTransferException(String message){
super(message);
}
}
最初我们的表中数据是这样的:
测试1:执行过后数据库没有变化
输入:xiaofang xiaozhang 100
输出:对方账户不存在,转账失败!
测试2:执行过后数据库没有发生变化
输入:xiaoming xiaohua 1000
输出:您的账户余额不足,转账失败!
测试3:转账成功,数据库发生变化
输入:xiaoming xiaohua 100
输出:转账成功
四、MyBatis处理事务
核心接口Transaction
事务的操作应当包括创建事务、提交事务、回滚、关闭几个操作,在MyBatis中将这些操作抽象成了Transaction接口。MyBatis使用了工厂模式利用TransactionFactory来生产Transaction
MyBatis有两种事务的管理形式:JDBC与MANAGED,用户可以在对MyBatis配置时在标签中进行选择。使用JDBC管理事务时,实际上依然是利用java.sql.Connection对象完成事务的提交、回滚与关闭(可以理解为时对JDBC进行的封装);而使用MANAGED进行事务管理时,MyBatis不会去实现事务管理,而是将事务管理交给程序容器(如JBOSS、Weblogic)。需要注意的是当我们使用MyBatis构建本地程序而非Web程序时,若选择MANAGED事务管理,那么及时最后执行了commit操作,也无法对数据库产生实际的影响,因为MANAGED下MyBatis不会实现事务管理,而在本地程序的情况下也没有容器来帮助我们实现
当type属性被配置为JDBC时,在MyBatis初始化解析Environment节点时会根据type=“JDBC”创建一个JdbcTransactionFactory工厂用来生产JdbcTransaction;若type属性被配置为MANAGED,则MyBatis会创建一个ManagedTransactionFactory工厂用来生产ManagedTransaction
MyBatis事务的核心接口是Transaction,但开发者在进行事务操作时,依然是使用SqlSession对象进行操作,我们可以从DefaultSqlSessionFactory(实现了SqlSessionFactory接口)的openSession源码中看到,在创建SqlSession对象时会同时创建一个Transaction与SqlSession对象进行绑定
隔离级别
MyBatis事务的隔离级别与JDBC一样分为五级,被封装在枚举类型TransactionIsolationLevel中
public enum TransactionIsolationLevel {
NONE(Connection.TRANSACTION_NONE),//不支持事务
READ_COMMITTED(Connection.TRANSACTION_READ_COMMITTED),//读提交
READ_UNCOMMITTED(Connection.TRANSACTION_READ_UNCOMMITTED),//读未提交
REPEATABLE_READ(Connection.TRANSACTION_REPEATABLE_READ),//可重复读
SERIALIZABLE(Connection.TRANSACTION_SERIALIZABLE);//串行化
private final int level;
TransactionIsolationLevel(int level) {
this.level = level;
}
public int getLevel() {
return level;
}
}
实例
依然是用前面的表和转账场景
核心配置文件
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<environments default="transaction_test">
<environment id="transaction_test">
<transactionManager type="JDBC"></transactionManager>
<dataSource type="POOLED">
<property name="driver" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/test"/>
<property name="username" value="root"/>
<property name="password" value="LZSF2239"/>
</dataSource>
</environment>
</environments>
<mappers>
<mapper resource="com/lzsf/mapper/WalletMapper.xml"/>
</mappers>
</configuration>
实体类Wallet
public class Wallet {
private String usename;
private float balance;
public String getUsename() {
return usename;
}
public void setUsename(String usename) {
this.usename = usename;
}
public float getBalance() {
return balance;
}
public void setBalance(float balance) {
this.balance = balance;
}
}
WalletMapper接口
public interface WalletMapper {
public Wallet findUseName(@Param("usename")String usename);
public void changesInAccount(@Param("usename")String usename,@Param("sum")float sum);
public Wallet balanceEnough(@Param("usename")String usename);
}
WalletMapper映射文件
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.lzsf.mapper.WalletMapper">
<select id="findUseName" resultType="com.lzsf.entity.Wallet">
select * from wallet where usename = #{usename}
</select>
<update id="changesInAccount">
update wallet set balance = balance + #{sum}
where usename = #{usename}
</update>
<select id="balanceEnough" resultType="com.lzsf.entity.Wallet">
select * from wallet where usename = #{usename} and balance >= 0
</select>
</mapper>
主类Main
class FailToTransferAccount extends Exception{
public FailToTransferAccount(String message){
super(message);
}
}
public class Main {
public static void main(String[] args) throws Exception {
InputStream inputStream = Resources.getResourceAsStream("mybatis_config.xml");
SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(inputStream);
SqlSession session = factory.openSession(TransactionIsolationLevel.REPEATABLE_READ);
WalletMapper mapper = session.getMapper(WalletMapper.class);
String from ,to;
float sum;
Scanner scanner = new Scanner(System.in);
from = scanner.next();
to = scanner.next();
sum = scanner.nextFloat();
try{
Wallet wallet = mapper.findUseName(to);
if (wallet == null){throw new FailToTransferAccount("对方账号不存在,转账失败");}
mapper.changesInAccount(from,-sum);
wallet = mapper.balanceEnough(from);
if (wallet == null){throw new FailToTransferAccount("余额不足,转账失败");}
mapper.changesInAccount(to,sum);
System.out.println("转账成功");
session.commit();
}catch (FailToTransferAccount e){
System.out.println(e.getMessage());
session.rollback();
}catch (Exception e){
e.printStackTrace();
session.rollback();
}
}
}
运行前的数据库
输入:xiaoming xiaohua 1000
输出:余额不足,转账失败
运行后的数据库
五、Spring整合MyBatis实现事务
首先在依赖上需要引入Spring与MyBatis的依赖,Spring想要操作数据库还需要引入spring-jdbc依赖,此外还需要使用到aop,需要引入aspectjweaver包
<dependencies>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.47</version>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>2.0.3</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>5.2.2.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>5.2.2.RELEASE</version>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.3</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.5</version>
</dependency>
</dependencies>
在进行配置MyBatis核心配置文件时,不在需要对MyBatis进行过多配置,MyBatis的环境配置可以全部作为bean的形式移入spring的配置文件
<!--MyBatis核心配置文件-->
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
</configuration>
MyBatis中Wallet的映射文件原来一致
<!--MyBatis映射文件-->
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.lzsf.mapper.IWalletMapper">
<select id="findUseName" resultType="com.lzsf.entity.Wallet">
select * from wallet where usename = #{usename}
</select>
<update id="changesInAccount">
update wallet set balance = balance + #{sum}
where usename = #{usename}
</update>
<select id="balanceEnough" resultType="com.lzsf.entity.Wallet">
select * from wallet where usename = #{usename} and balance >= 0
</select>
</mapper>
但是在IWalletMapper接口中我们再添加一个方法来对三个方法进行分装,方便将其作为切入点进行环绕增强。此外有一个很特殊的地方在于,由于SQLSessionFactory与SqlSession都在spring配置文件中通过容器的方式创建,我们无需再手动创建,但我们需要对Mapper接口增加一个实现类,将SqlSession注入进去,在实现类中完成对查询方法的调用。在抛出异常时需要注意必须抛出RuntimeException异常,否则事务将不会回滚
//IWalletMapper接口
public interface IWalletMapper {
public Wallet findUseName(@Param("usename")String usename);
public void changesInAccount(@Param("usename")String usename,@Param("sum")float sum);
public Wallet balanceEnough(@Param("usename")String usename);
public void transferAccount(String from ,String to,float sum);
}
class FailToTransferAccount extends RuntimeException {
public FailToTransferAccount(String message) {
super(message);
}
}
//IWalletMapper实现类
public class WalletMapperImpl implements IWalletMapper {
private SqlSessionTemplate session;
public void setSession(SqlSessionTemplate session) {
this.session = session;
}
public Wallet findUseName(String usename) {
IWalletMapper mapper = session.getMapper(IWalletMapper.class);
return mapper.findUseName(usename);
}
public void changesInAccount(String usename, float sum) {
IWalletMapper mapper = session.getMapper(IWalletMapper.class);
mapper.changesInAccount(usename,sum);
}
public Wallet balanceEnough(String usename) {
IWalletMapper mapper = session.getMapper(IWalletMapper.class);
return mapper.balanceEnough(usename);
}
//该方法将作为声明式事务的切入点,使用AOP切入事务
public void transferAccount(String from,String to,float sum){
Wallet wallet = findUseName(to);
if (wallet == null) {
throw new FailToTransferAccount("对方账号不存在,转账失败");
}
changesInAccount(from, -sum);
wallet = balanceEnough(from);
if (wallet == null) {
throw new FailToTransferAccount("余额不足,转账失败");
}
changesInAccount(to, sum);
System.out.println("转账成功");
}
}
之后开始配置Spring,MyBatis数据源、SqlSessionFactory以及SqlSession可以作为bean对象托管给IoC容器创建,其中要注意的是,在配置SqlSession时使用的是SqlSessionTemplate,SQLSessionTemplate是MyBatis-spring的核心,其实现了SqlSession接口,它是线程安全的,可以被多个DAO共享。SQLSessionTemplate没有set方法,因此只能使用构造函数将SqlSessionFactory注入。此外需要对Spring进行事务配置,Spring事务分为声明式事务与编程式事务,这里我们采用声明式事务,通过AOP将需要开启事务的方法作为切入点,将事务作为切面对方法进行增强
<!--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:aop="http://www.springframework.org/schema/aop"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
https://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/tx
https://www.springframework.org/schema/tx/spring-tx.xsd
http://www.springframework.org/schema/aop
https://www.springframework.org/schema/aop/spring-aop.xsd">
<!--配置数据源-->
<bean id="datasource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<property name="driverClassName" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/test"/>
<property name="username" value="root"/>
<property name="password" value="LZSF2239"/>
</bean>
<!--配置SqlSession工厂-->
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="datasource"/>
<property name="configLocation" value="classpath:mybatis-config.xml"/>
<property name="mapperLocations" value="classpath:com/lzsf/mapper/WalletMapper.xml"/>
</bean>
<!--事务标准配置,创建一个DataSourceTransactionManager对象-->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<constructor-arg ref="datasource"/>
</bean>
<!--配置SqlSession,注入SqlSessionFactory-->
<bean id="sqlSession" class="org.mybatis.spring.SqlSessionTemplate">
<constructor-arg index="0" ref="sqlSessionFactory"/>
</bean>
<!--配置Mapper实现类-->
<bean id="walletMapper" class="com.lzsf.mapper.WalletMapperImpl">
<property name="session" ref="sqlSession"/>
</bean>
<!--声明事务-->
<tx:advice id="txAdvice" transaction-manager="transactionManager">
<tx:attributes>
<tx:method name="transferAccount" propagation="REQUIRED"/>
</tx:attributes>
</tx:advice>
<!--声明切入点与横切面-->
<aop:config>
<aop:pointcut id="txPointCut" expression="execution(* com.lzsf.mapper.IWalletMapper.transferAccount(..))"/>
<aop:advisor advice-ref="txAdvice" pointcut-ref="txPointCut"/>
</aop:config>
</beans>
主类Main
class FailToTransferAccount extends RuntimeException {
public FailToTransferAccount(String message) {
super(message);
}
}
public class Main {
public static void main(String[] args) throws Exception {
ApplicationContext context = new ClassPathXmlApplicationContext("ApplicationContext.xml");
IWalletMapper mapper = context.getBean("walletMapper", IWalletMapper.class);
String from, to;
float sum;
Scanner scanner = new Scanner(System.in);
from = scanner.next();
to = scanner.next();
sum = scanner.nextFloat();
mapper.transferAccount(from,to,sum);
}
}
执行前数据库的状态
运行结果:
运行后的数据库状态