一. 数据准备
事务控制是数据库区别与文件系统的重要特征之一,作用在于保证数据库的完整性,我们来举一个例子
假设张甲给张一转账50元,这个操作主要分为两部分:
1. 从张甲账户减去50元,并且张甲的账户余额大于等于50
2. 从张一的账户上增加50元
两个部分应该作为一个整体看待,如果整个过程有任何一个部分出错,都应该将张甲和张一的数据恢复到原始的状态,我们先来准备相关数据,来看一下没有事务控制下的转账操作:
1. Wallet
CREATE TABLE wallet ( id int(6) primary key AUTO_INCREMENT, name VARCHAR(10), balance DOUBLE(6,2) ) INSERT INTO wallet (name, balance) VALUE ('张甲',100.0); INSERT INTO wallet (name, balance) VALUE ('张一',0.0);
2. WalletDao
package io.zhangjia.spring.dao; public interface WalletDao { int add(Integer id,Double money); int sub(Integer id,Double money); Double queryBalanceById(Integer id); }
3. WalletDaoImpl
在使用@Repository注解的时候,我们一般会使用其接口的首字母小写作为bean的ID,而不采用默认的walletDaoImpl
package io.zhangjia.spring.dao.impl; import io.zhangjia.spring.dao.WalletDao; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Repository; @Repository("walletDao") //一般使用其接口的首字母小写作为bean的ID public class WalletDaoImpl implements WalletDao { @Autowired private JdbcTemplate jdbcTemplate; @Override public int add(Integer id, Double money) { String sql = "UPDATE wallet SET balance = balance + ? WHERE id = ?"; return jdbcTemplate.update(sql,money,id); } @Override public int sub(Integer id, Double money) { String sql = "UPDATE wallet SET balance = balance - ? WHERE id = ?"; return jdbcTemplate.update(sql,money,id); } @Override public Double queryBalanceById(Integer id) { String sql = "SELECT balance FROM wallet WHERE id = ?"; return jdbcTemplate.queryForObject(sql,Double.class,id); } }
4. WalletService
package io.zhangjia.spring.service; public interface WalletService { int transfer(Integer fromId,Integer toId,Double money); double getUserBalance(Integer id); }
5. WalletServiceImpl
package io.zhangjia.spring.dao.impl; import io.zhangjia.spring.dao.WalletDao; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Repository; @Repository("walletDao") //一般使用其接口的首字母小写作为bean的ID public class WalletDaoImpl implements WalletDao { @Autowired private JdbcTemplate jdbcTemplate; @Override public int add(Integer id, Double money) { String sql = "UPDATE wallet SET balance = balance + ? WHERE id = ?"; return jdbcTemplate.update(sql,money,id); } @Override public int sub(Integer id, Double money) { String sql = "UPDATE wallet SET balance = balance - ? WHERE id = ?"; return jdbcTemplate.update(sql,money,id); } @Override public Double queryBalanceById(Integer id) { String sql = "SELECT balance FROM wallet WHERE id = ?"; return jdbcTemplate.queryForObject(sql,Double.class,id); } }
6. applicationContext.xml:
<?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 https://www.springframework.org/schema/context/spring-context.xsd"> <context:component-scan base-package="io.zhangjia.spring"/> <context:property-placeholder location="jdbc.properties"/> <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 class="org.springframework.jdbc.core.JdbcTemplate"> <property name="dataSource" ref="dataSource"/> </bean> </beans>
二. 转账测试
测试类
public class Test { public static void main(String[] args) { // 初始化Spring的IOC容器 ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml"); WalletService walletService = (WalletService)context.getBean("walletService"); walletService.transfer(1,2,50d); context.close(); } }
当我们执行两次测试类后,张甲的余额已经变成了0,张一的余额变成了100,此时如果再执行第三次测试类,则张甲的余额会变成-50,张一的余额会变成150,这在实际的项目中,是不允许发生的事情,这里我们先不采用事务控制,而是通过在Service层添加相关判断来解决这问题:
将WalletDaoImol的sub方法修改为:
@Override public int sub(Integer id, Double money) { //获取当前账户的余额 Double balance = queryBalanceById(id); if(balance < money){ //余额不足 return 0; } String sql = "UPDATE wallet SET balance = balance - ? WHERE id = ?"; return jdbcTemplate.update(sql,money,id); }
将WalletServiceImpl的transfer修改为:
@Override public int transfer(Integer fromId, Integer toId, Double money) { int sub = walletDao.sub(fromId,money); if(sub == 0) { // 如果余额为0,则不继续执行 return 0; } int add = walletDao.add(toId,money); return sub * add; }
此时无论再执行测试类,当张甲的余额为0的时候,便不会再发生转账行为。这种方式虽然能解决问题,但是在Dao和Service中都加了大量的if判断,并不是一种良好的解决方法。我们可以通过下面的方式,省去Service中的判断语句
在WalletDaoImol的sub方法中,我们除了通过return 0 的方式来组织sql语句的执行外,还可以使用异常的方式:
@Override public int sub(Integer id, Double money) { //获取当前账户的余额 Double balance = queryBalanceById(id); if(balance < money){ //余额不足 // return 0; throw new RuntimeException("余额不足"); } String sql = "UPDATE wallet SET balance = balance - ? WHERE id = ?"; return jdbcTemplate.update(sql,money,id); }
并将WalletServiceImpl的transfer修改为:
@Override public int transfer(Integer fromId, Integer toId, Double money) { int sub = walletDao.sub(fromId,money); int add = walletDao.add(toId,money); return sub * add; }
此时执行测试类,便会输出:
Exception in thread "main" java.lang.RuntimeException: 余额不足
上述方法虽然看似减少了判断语句,但也存在其他问题,在上述的transfer中,先给张甲减钱,再给张一加钱,这样操作是不会出现问题的,但是假如我们在transfer中,先给张一账户加钱,再给张甲账户减钱,那么当张甲的账户没钱的时候,张一的账户依旧会加钱
@Override public int transfer(Integer fromId, Integer toId, Double money) { int add = walletDao.add(toId,money); //无论张甲的账户有没有钱,张一的账户都会加钱 int sub = walletDao.sub(fromId,money); return sub * add; }
所以上述两种方法都存在着各种问题,真正解决此类问题的正确方法便是事务控制。
三. Spring的事务控制
通过Spring进行事物控制是非常简单的,只需要两个操作即可
1. 配置事物管理器并开启使用注解的方式配置事务
<?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 https://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd"> <context:component-scan base-package="io.zhangjia.spring"/> <context:property-placeholder location="jdbc.properties"/> <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 class="org.springframework.jdbc.core.JdbcTemplate"> <property name="dataSource" ref="dataSource"/> </bean> <!-- 配置事务管理器--> <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="dataSource"/> </bean> <!-- 开启使用注解的方式配置事务--> <tx:annotation-driven/> <!-- 注意有多个annotation-driven,我们使用的是http://www.springframework.org/schema/tx --> </beans>
2. 在需要控制的地方抛出异常,这一步我们刚才已经做过,即添加 throw new RuntimeException(“余额不足”);
@Override public int sub(Integer id, Double money) { //获取当前账户的余额 Double balance = queryBalanceById(id); if(balance < money){ //余额不足 throw new RuntimeException("余额不足"); } String sql = "UPDATE wallet SET balance = balance - ? WHERE id = ?"; return jdbcTemplate.update(sql,money,id); }
3. 使用Transactional注解开启事务控制
使用Transactional注解开启事务控制的方式也非常简单,只需要在需要开启事物控制的方法上添加@Transactional注解即可
@Transactional @Override public int transfer(Integer fromId, Integer toId, Double money) { int add = walletDao.add(toId,money); int sub = walletDao.sub(fromId,money); return sub * add; }
此时,无论我们在transfer中是先执行add还是先执行sub,都不会影响数据的完整性,事务控制完成。
需要使用事务控制的场景:一个业务如果包含多个增删改的数据操作时,便需要加入事务控制。
请登录之后再进行评论