主要记录下spring是如何支持事物的,以及在Spring结合mybatis时,可以怎么简单的实现数据库的事物功能
原文查看地址:一灰灰Blog
首先准备两张表,一个user表,一个story表,结构如下
CREATE TABLE `user` ( `id` int(11) unsigned NOT NULL AUTO_INCREMENT, `name` varchar(20) NOT NULL DEFAULT '' COMMENT '用户名', `pwd` varchar(26) NOT NULL DEFAULT '' COMMENT '密码', `isDeleted` tinyint(1) NOT NULL DEFAULT '0', `created` varchar(13) NOT NULL DEFAULT '0', `updated` varchar(13) NOT NULL DEFAULT '0', PRIMARY KEY (`id`), KEY `name` (`name`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE `story` ( `id` int(11) unsigned NOT NULL AUTO_INCREMENT, `userId` int(20) unsigned NOT NULL DEFAULT '0' COMMENT '作者的userID', `name` varchar(20) NOT NULL DEFAULT '' COMMENT '作者名', `title` varchar(26) NOT NULL DEFAULT '' COMMENT '密码', `story` text COMMENT '故事内容', `isDeleted` tinyint(1) NOT NULL DEFAULT '0', `created` varchar(13) NOT NULL DEFAULT '0', `updated` varchar(13) NOT NULL DEFAULT '0', PRIMARY KEY (`id`), KEY `userId` (`userId`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
我们的事物场景在于用户修改name时,要求两张表的name都需要一起修改,不允许出现不一致的情况
转账,一个用户减钱,另一个用户加钱
CREATE TABLE `money` ( `id` int(11) unsigned NOT NULL AUTO_INCREMENT, `name` varchar(20) NOT NULL DEFAULT '' COMMENT '用户名', `money` int(26) NOT NULL DEFAULT '0' COMMENT '钱', `isDeleted` tinyint(1) NOT NULL DEFAULT '0', `created` varchar(13) NOT NULL DEFAULT '0', `updated` varchar(13) NOT NULL DEFAULT '0', PRIMARY KEY (`id`), KEY `name` (`name`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
相比上面那个case,这个更加简单了,下面的实例则主要根据这个进行说明,至于case1,则留待扩展里面进行
首先是实现对应的dao和entity
@Data public class MoneyEntity implements Serializable { private static final long serialVersionUID = -7074788842783160025L; private int id; private String name; private int money; private int isDeleted; private int created; private int updated; } public interface MoneyDao { MoneyEntity queryMoney(@Param("id") int userId); // 加钱,负数时表示减钱 int incrementMoney(@Param("id") int userId, @Param("addMoney") int addMoney); }
对应的mapper文件为
<?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.git.hui.demo.mybatis.mapper.MoneyDao"> <sql id="moneyEntity"> id, `name`, `money`, `isDeleted`, `created`, `updated` </sql> <select id="queryMoney" resultType="com.git.hui.demo.mybatis.entity.MoneyEntity"> select <include refid="moneyEntity"/> from money where id=#{id} </select> <update id="incrementMoney"> update money set money=money + #{addMoney} where id=#{id} </update> </mapper>
对应的mybatis连接数据源的相关配置
<bean class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer"> <property name="locations"> <value>classpath*:jdbc.properties</value> </property> </bean> <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close"> <property name="driverClassName" value="${driver}"/> <property name="url" value="${url}"/> <property name="username" value="${username}"/> <property name="password" value="${password}"/> <property name="filters" value="stat"/> <property name="maxActive" value="20"/> <property name="initialSize" value="1"/> <property name="maxWait" value="60000"/> <property name="minIdle" value="1"/> <property name="timeBetweenEvictionRunsMillis" value="60000"/> <property name="minEvictableIdleTimeMillis" value="300000"/> <property name="validationQuery" value="SELECT 'x'"/> <property name="testWhileIdle" value="true"/> <property name="testOnBorrow" value="false"/> <property name="testOnReturn" value="false"/> <property name="poolPreparedStatements" value="true"/> <property name="maxPoolPreparedStatementPerConnectionSize" value="50"/> </bean> <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"> <property name="dataSource" ref="dataSource"/> <!-- 指定mapper文件 --> <property name="mapperLocations" value="classpath*:mapper/*.xml"/> </bean> <!-- 指定扫描dao --> <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer"> <property name="basePackage" value="com.git.hui.demo.mybatis"/> </bean>
通过网上查询,Spring事物管理总共有四种方式,下面逐一进行演示,每种方式是怎么玩的,然后看实际项目中应该如何抉择
编程式事物管理,既通过TransactionTemplate来实现多个db操作的事物管理
那么,我们的转账case可以如下实现
@Repository public class CodeDemo1 { @Autowired private MoneyDao moneyDao; @Autowired private TransactionTemplate transactionTemplate; /** * 转账 * * @param inUserId * @param outUserId * @param payMoney * @param status 0 表示正常转账, 1 表示内部抛出一个异常, 2 表示新开一个线程,修改inUserId的钱 +200, 3 表示新开一个线程,修改outUserId的钱 + 200 */ public void transfor(final int inUserId, final int outUserId, final int payMoney, final int status) { transactionTemplate.execute(new TransactionCallbackWithoutResult() { protected void doInTransactionWithoutResult(TransactionStatus transactionStatus) { MoneyEntity entity = moneyDao.queryMoney(outUserId); if (entity.getMoney() > payMoney) { // 可以转账 // 先减钱 moneyDao.incrementMoney(outUserId, -payMoney); testCase(inUserId, outUserId, status); // 再加钱 moneyDao.incrementMoney(inUserId, payMoney); System.out.println("转账完成! now: " + System.currentTimeMillis()); } } }); } // 下面都是测试用例相关 private void testCase(final int inUserId, final int outUserId, final int status) { if (status == 1) { throw new IllegalArgumentException("转账异常!!!"); } else if(status == 2) { addMoney(inUserId); try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } } else if (status == 3) { addMoney(outUserId); try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } } } public void addMoney(final int userId) { System.out.printf("内部加钱: " + System.currentTimeMillis()); new Thread(new Runnable() { public void run() { moneyDao.incrementMoney(userId, 200); System.out.println(" sub modify success! now: " + System.currentTimeMillis()); } }).start(); } }
主要看上面的transfor方法,内部通过 transactionTemplate 来实现事物的封装,内部有三个db操作,一个查询,两个更新,具体分析后面说明
上面的代码比较简单了,唯一需要关注的就是transactionTemplate这个bean如何定义的,xml文件中与前面重复的就不贴了,直接贴上关键代码, 一个是根据DataSource创建的 TransactionManager
,一个则是根据 TransactionManager
创建的 TransactionTemplate
<!--编程式事物--> <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="dataSource"/> </bean> <bean id="transactionTemplate" class="org.springframework.transaction.support.TransactionTemplate"> <property name="transactionManager" ref="transactionManager"/> </bean>
正常演示情况, 演示没有任何异常,不考虑并发的情况
@RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration({"classpath*:spring/service.xml", "classpath*:test-datasource1.xml"}) public class CodeDemo1Test { @Autowired private CodeDemo1 codeDemo1; @Autowired private MoneyDao moneyDao; @Test public void testTransfor() { System.out.println("---------before----------"); System.out.println("id: 1 money = " + moneyDao.queryMoney(1).getMoney()); System.out.println("id: 2 money = " + moneyDao.queryMoney(2).getMoney()); codeDemo1.transfor(1, 2, 10, 0); System.out.println("---------after----------"); System.out.println("id: 1 money = " + moneyDao.queryMoney(1).getMoney()); System.out.println("id: 2 money = " + moneyDao.queryMoney(2).getMoney()); } }
输出如下,两个账号的钱都没有问题
---------before---------- id: 1 money = 10000 id: 2 money = 50000 转账完成! now: 1526130394266 ---------after---------- id: 1 money = 10010 id: 2 money = 49990
转账过程中出现异常,特别是转账方钱已扣,收款方还没收到钱时,也就是case中的status为1的场景
// 内部抛异常的情况 @Test public void testTransforException() { System.out.println("---------before----------"); System.out.println("id: 1 money = " + moneyDao.queryMoney(1).getMoney()); System.out.println("id: 2 money = " + moneyDao.queryMoney(2).getMoney()); try { codeDemo1.transfor(1, 2, 10, 1); } catch (Exception e) { e.printStackTrace(); } System.out.println("---------after----------"); System.out.println("id: 1 money = " + moneyDao.queryMoney(1).getMoney()); System.out.println("id: 2 money = " + moneyDao.queryMoney(2).getMoney()); }
对此,我们希望把转账方的钱还回去, 输出如下,发现两个的钱都没有变化
---------before---------- id: 1 money = 10010 id: 2 money = 49990 ---------after---------- id: 1 money = 10010 java.lang.IllegalArgumentException: 转账异常!!! ... // 省略异常信息 id: 2 money = 49990
当status为2,表示在转账人钱已扣,收款人钱没收到之间,又有人给收款人转了200,此时根据mysql的锁机制,另外人的转账应该是立马到的(因为收款人账号没有被锁住),且金额不应该有问题
输出结果如下:
---------before---------- id: 1 money = 10010 id: 2 money = 49990 ## 右边是注释: 转账过程中,另外存钱立马到账,没有被锁住 内部加钱: 1526130827480 sub modify success! now: 1526130827500 ## 存钱结束 转账完成! now: 1526130830488 ---------after---------- id: 1 money = 10220 id: 2 money = 49980
当status为3, 表示在转账人钱已扣,收款人钱没收到之间,又有人给转账人转了200,这时因为转账人的记录以及被加了写锁,因此只能等待转账的事物提交之后,才有可能+200成功,当然最终的金额也得一致
输出结果如下
---------before---------- id: 1 money = 10220 id: 2 money = 49980 ## 右边是注释:内部存钱了,但没有马上成功 ## 直到转账完成后,才立马存成功,注意两个时间戳 内部加钱: 1526131101046 转账完成! now: 1526131104051 sub modify success! now: 1526131104053 ---------after---------- id: 1 money = 10230 id: 2 money = 50170
至此,编程式事物已经实例演示ok,从上面的过程,给人的感觉就和直接写事物相关的sql一样,
start transaction; -- 这中间就是 TransactionTemplate#execute 方法内部的逻辑 -- 也就是需要事物管理的一组sql commit;
TransactionProxyFactoryBean
方式 接下来的三个就是声明式事物管理,这种用得也比较少,因为需要每个事物管理类,添加一个TransactionProxyFactoryBean
除了将 TransactionTemplate
干掉,并将内部的sql逻辑移除之外,对比前面的,发现基本上没有太多差别
public class FactoryBeanDemo2 { @Autowired private MoneyDao moneyDao; /** * 转账 * * @param inUserId * @param outUserId * @param payMoney * @param status 0 表示正常转账, 1 表示内部抛出一个异常, 2 表示新开一个线程,修改inUserId的钱 +200, 3 表示新开一个线程,修改outUserId的钱 + 200 */ public void transfor(final int inUserId, final int outUserId, final int payMoney, final int status) { MoneyEntity entity = moneyDao.queryMoney(outUserId); if (entity.getMoney() > payMoney) { // 可以转账 // 先减钱 moneyDao.incrementMoney(outUserId, -payMoney); testCase(inUserId, outUserId, status); // 再加钱 moneyDao.incrementMoney(inUserId, payMoney); System.out.println("转账完成! now: " + System.currentTimeMillis()); } } private void testCase(final int inUserId, final int outUserId, final int status) { if (status == 1) { throw new IllegalArgumentException("转账异常!!!"); } else if (status == 2) { addMoney(inUserId); try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } } else if (status == 3) { addMoney(outUserId); try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } } } public void addMoney(final int userId) { System.out.println("内部加钱: " + System.currentTimeMillis()); new Thread(new Runnable() { public void run() { moneyDao.incrementMoney(userId, 200); System.out.println("sub modify success! now: " + System.currentTimeMillis()); } }).start(); } }
重点来了,主要是需要配置一个 TransactionProxyBeanFactory
,我们知道BeanFactory就是我们自己来创建Bean的一种手段,相关的xml配置如下
<!--编程式事物--> <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="dataSource"/> </bean> <bean id="factoryBeanDemo2" class="com.git.hui.demo.mybatis.repository.transaction.FactoryBeanDemo2"/> <!-- 配置业务层的代理 --> <bean id="factoryBeanDemoProxy" class="org.springframework.transaction.interceptor.TransactionProxyFactoryBean"> <!-- 配置目标对象 --> <property name="target" ref="factoryBeanDemo2" /> <!-- 注入事务管理器 --> <property name="transactionManager" ref="transactionManager"/> <!-- 注入事务的属性 --> <property name="transactionAttributes"> <props> <!-- prop的格式: * PROPAGATION :事务的传播行为 * ISOTATION :事务的隔离级别 * readOnly :只读 * -EXCEPTION :发生哪些异常回滚事务 * +EXCEPTION :发生哪些异常不回滚事务 --> <!-- 这个key对应的就是目标类中的方法--> <prop key="transfor">PROPAGATION_REQUIRED</prop> <!-- <prop key="transfer">PROPAGATION_REQUIRED,readOnly</prop> --> <!-- <prop key="transfer">PROPAGATION_REQUIRED,+java.lang.ArithmeticException</prop> --> </props> </property> </bean>
通过上面的配置,大致可以了解到这个通过TransactionProxyFactoryBean就是创建了一个FactoryBeanDemo2的代理类,这个代理类内部封装好事物相关的逻辑,可以看做是前面编程式的一种简单通用抽象
测试代码与前面基本相同,唯一的区别就是我们使用的应该是上面BeanFactory生成的Bean,而不是直接使用 FactoryBeanDemo2
正常演示case:
@RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration({"classpath*:spring/service.xml", "classpath*:test-datasource2.xml"}) public class FactoryBeanDemo1Test { @Resource(name = "factoryBeanDemoProxy") private FactoryBeanDemo2 factoryBeanDemo2; @Autowired private MoneyDao moneyDao; @Test public void testTransfor() { System.out.println("---------before----------"); System.out.println("id: 1 money = " + moneyDao.queryMoney(1).getMoney()); System.out.println("id: 2 money = " + moneyDao.queryMoney(2).getMoney()); factoryBeanDemo2.transfor(1, 2, 10, 0); System.out.println("---------after----------"); System.out.println("id: 1 money = " + moneyDao.queryMoney(1).getMoney()); System.out.println("id: 2 money = " + moneyDao.queryMoney(2).getMoney()); } }
输出
---------before---------- id: 1 money = 10000 id: 2 money = 50000 转账完成! now: 1526132058886 ---------after---------- id: 1 money = 10010 id: 2 money = 49990
status为1,内部异常的情况下,我们希望钱也不会有问题
@Test public void testTransforException() { System.out.println("---------before----------"); System.out.println("id: 1 money = " + moneyDao.queryMoney(1).getMoney()); System.out.println("id: 2 money = " + moneyDao.queryMoney(2).getMoney()); try { factoryBeanDemo2.transfor(1, 2, 10, 1); } catch (Exception e) { System.out.println(e.getMessage());; } System.out.println("---------after----------"); System.out.println("id: 1 money = " + moneyDao.queryMoney(1).getMoney()); System.out.println("id: 2 money = " + moneyDao.queryMoney(2).getMoney()); }
输出为
---------before---------- id: 1 money = 10010 id: 2 money = 49990 转账异常!!! ---------after---------- id: 1 money = 10010 id: 2 money = 49990
status为2 时,分析结果与上面应该相同,输出如下
---------before---------- id: 1 money = 10010 id: 2 money = 49950 内部加钱: 1526133325376 sub modify success! now: 1526133325387 转账完成! now: 1526133328381 ---------after---------- id: 1 money = 10220 id: 2 money = 49940
status为3时,输出
---------before---------- id: 1 money = 10220 id: 2 money = 49940 内部加钱: 1526133373466 转账完成! now: 1526133376476 sub modify success! now: 1526133376480 ---------after---------- id: 1 money = 10230 id: 2 money = 50130
TransactionProxyFactoryBean 的思路就是利用代理模式来实现事物管理,生成一个代理类,拦截目标方法,将一组sql的操作封装到事物中进行;相比较于硬编码,无侵入,而且支持灵活的配置方式
缺点也显而易见,每个都要进行配置,比较繁琐
Spring有两大特点,IoC和AOP,对于事物这种情况而言,我们可不可以使用AOP来做呢?
对于需要开启事物的方法,拦截掉,执行前开始事物,执行完毕之后提交事物,出现异常时回滚
这样一看,感觉还是蛮有希望的,而下面两种姿势正是这么玩的,因此需要加上aspect的依赖
<dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjweaver</artifactId> <version>1.8.7</version> </dependency>
java类与第二种完全一致,变动的只有xml
<!-- 首先添加命名空间 --> xmlns:tx="http://www.springframework.org/schema/tx" xmlns:aop="http://www.springframework.org/schema/aop" xsi:schemaLocation="... http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd" <!--对应的事物通知和切面配置--> <tx:advice id="txAdvice" transaction-manager="transactionManager"> <tx:attributes> <!-- propagation :事务传播行为 isolation :事务的隔离级别 read-only :只读 rollback-for:发生哪些异常回滚 no-rollback-for :发生哪些异常不回滚 timeout :过期信息 --> <tx:method name="transfor" propagation="REQUIRED"/> </tx:attributes> </tx:advice> <!-- 配置切面 --> <aop:config> <!-- 配置切入点 --> <aop:pointcut expression="execution(* com.git.hui.demo.mybatis.repository.transaction.XmlDemo3.*(..))" id="pointcut1"/> <!-- 配置切面 --> <aop:advisor advice-ref="txAdvice" pointcut-ref="pointcut1"/> </aop:config>
观察上面的配置,再想想第二种方式,思路都差不多了,但是这种方式明显更加通用,通过切面和切点,可以减少大量的配置
@RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration({"classpath*:spring/service.xml", "classpath*:test-datasource3.xml"}) public class XmlBeanTest { @Autowired private XmlDemo3 xmlDemo; @Autowired private MoneyDao moneyDao; @Test public void testTransfor() { System.out.println("---------before----------"); System.out.println("id: 1 money = " + moneyDao.queryMoney(1).getMoney()); System.out.println("id: 2 money = " + moneyDao.queryMoney(2).getMoney()); xmlDemo.transfor(1, 2, 10, 0); System.out.println("---------after----------"); System.out.println("id: 1 money = " + moneyDao.queryMoney(1).getMoney()); System.out.println("id: 2 money = " + moneyDao.queryMoney(2).getMoney()); } }
这个测试起来,和一般的写法就没啥两样了,比第二种的FactoryBean的注入方式简单点
正常输出
---------before---------- id: 1 money = 10000 id: 2 money = 50000 转账完成! now: 1526135301273 ---------after---------- id: 1 money = 10010 id: 2 money = 49990
status=1 出现异常时,输出
---------before---------- id: 1 money = 10010 id: 2 money = 49990 转账异常!!! ---------after---------- id: 1 money = 10010 id: 2 money = 49990
status=2 转账过程中,又存钱的场景,输出,与前面预期一致
---------before---------- id: 1 money = 10010 id: 2 money = 49990 内部加钱: 1526135438403 sub modify success! now: 1526135438421 转账完成! now: 1526135441410 ---------after---------- id: 1 money = 10220 id: 2 money = 49980
status=3 的输出,与前面预期一致
---------before---------- id: 1 money = 10220 id: 2 money = 49980 内部加钱: 1526135464341 转账完成! now: 1526135467349 sub modify success! now: 1526135467352 ---------after---------- id: 1 money = 10230 id: 2 money = 50170
这个就是消灭xml,用注解来做的方式,就是将前面xml中的配置用 @Transactional注解替换
@Repository public class AnnoDemo4 { @Autowired private MoneyDao moneyDao; /** * 转账 * * @param inUserId * @param outUserId * @param payMoney * @param status 0 表示正常转账, 1 表示内部抛出一个异常, 2 表示新开一个线程,修改inUserId的钱 +200, 3 表示新开一个线程,修改outUserId的钱 + 200 * * * Transactional注解中的的属性 propagation :事务的传播行为 isolation :事务的隔离级别 readOnly :只读 * rollbackFor :发生哪些异常回滚 noRollbackFor :发生哪些异常不回滚 * rollbackForClassName 根据异常类名回滚 */ @Transactional(propagation = Propagation.REQUIRED, isolation = Isolation.DEFAULT, readOnly = false) public void transfor(final int inUserId, final int outUserId, final int payMoney, final int status) { MoneyEntity entity = moneyDao.queryMoney(outUserId); if (entity.getMoney() > payMoney) { // 可以转账 // 先减钱 moneyDao.incrementMoney(outUserId, -payMoney); testCase(inUserId, outUserId, status); // 再加钱 moneyDao.incrementMoney(inUserId, payMoney); System.out.println("转账完成! now: " + System.currentTimeMillis()); } } private void testCase(final int inUserId, final int outUserId, final int status) { if (status == 1) { throw new IllegalArgumentException("转账异常!!!"); } else if (status == 2) { addMoney(inUserId); try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } } else if (status == 3) { addMoney(outUserId); try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } } } private void addMoney(final int userId) { System.out.println("内部加钱: " + System.currentTimeMillis()); new Thread(new Runnable() { public void run() { moneyDao.incrementMoney(userId, 200); System.out.println("sub modify success! now: " + System.currentTimeMillis()); } }).start(); } }
因此需要在xml中配置,开启事物注解
<!--编程式事物--> <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="dataSource"/> </bean> <tx:annotation-driven transaction-manager="transactionManager"/>
这样一看,就更加清晰了,实际项目中,xml和注解方式也是用得最多的场景了
和第三种测试case完全相同, 输出结果也一样,直接省略
上面说了Spring中四种使用事物的姿势,其中硬编码方式可能是最好理解的,就相当于将我们写sql中,使用事物的方式直接翻译成对应的java代码了;而FactoryBean方式相当于特殊情况特殊对待,为每个事物来一个代理类来增强事物功能;后面的两个则原理差不多都是利用事物通知(AOP)来实现,定义切点及相关信息
TransactionTemplate
transactionTemplate#execute
方法内 TransactionProxyFactoryBean
为事物相关类生成代理 <tx:advice>
标签定义事物通知,内部可有较多的配置信息 <aop:config>
配置切点,切面 @Transactional
注解即可 <tx:annotation-driven transaction-manager="transactionManager"/>
基于hexo + github pages搭建的个人博客,记录所有学习和工作中的博文,欢迎大家前去逛逛
尽信书则不如,已上内容,纯属一家之言,因本人能力一般,见识有限,如发现bug或者有更好的建议,随时欢迎批评指正