事务
- 事务在项目开发过程非常重要,涉及到数据的一致性的问题,不容马虎!
- 事务管理是企业级应用程序开发中必备技术,用来确保数据的完整性和一致性。
事务就是把一组操作当成一个整体,这组操作要么全部成功,要么全部失败。
事务四个特性ACID
- 原子性(atomicity)
- 即不可分割,事务要么全部被执行,要么全部不执行。如果事务的所有子事务全部提交成功,则所有的数据库操作被提交,数据库状态发生变化;如果有子事务失败,则其他子事务的数据库操作被回滚,即数据库回到事务执行前的状态,不会发生状态转换
- 一致性(consistency)
- 事务的执行使得数据库从一种正确状态转换成另外一种正确状态
- 隔离性(isolation)
- 在事务正确提交之前,不允许把事务对该数据的改变提供给任何其他事务,即在事务正确提交之前,它可能的结果不应该显示给其他事务
- 持久性(durability)
- 事务正确提交之后,其结果将永远保存在数据库之中,即使在事务提交之后有了其他故障,事务的处理结果也会得到保存
事务的作用
事务管理对于企业级应用而言至关重要,它保证了用户的每一次操作都是可靠的,即便出现了异常的访问情况,也不至于破坏后台数据的完整性。就像银行的自动提款机ATM,通常ATM都可以正常为客户服务,但是也难免遇到操作过程中及其突然出故障的情况,此时,事务就必须确保出故障前对账户的操作不生效,就像用户刚才完全没有使用过ATM机一样,以保证用户和银行的利益都不受损失。
并发下事务会产生的问题
举个例子,事务A和事务B操纵的是同一个资源,事务A有若干个子事务,事务B也有若干个子事务,事务A和事务B在高并发的情况下,会出现各种各样的问题。“各种各样的问题”,总结一下主要就是五种:第一类丢失更新、第二类丢失更新、脏读、不可重复读、幻读。五种之中,第一类丢失更新、第二类丢失更新不重要,不讲了,讲一下脏读、不可重复读和幻读。
1、脏读
所谓脏读,就是指事务A读到了事务B还没有提交的数据,比如银行取钱,事务A开启事务,此时切换到事务B,事务B开启事务–>取走100元,此时切换回事务A,事务A读取的肯定是数据库里面的原始数据,因为事务B取走了100块钱,并没有提交,数据库里面的账务余额肯定还是原始余额,这就是脏读。
2、不可重复读
所谓不可重复读,就是指在一个事务里面读取了两次某个数据,读出来的数据不一致。还是以银行取钱为例,事务A开启事务–>查出银行卡余额为1000元,此时切换到事务B事务B开启事务–>事务B取走100元–>提交,数据库里面余额变为900元,此时切换回事务A,事务A再查一次查出账户余额为900元,这样对事务A而言,在同一个事务内两次读取账户余额数据不一致,这就是不可重复读。
3、幻读
所谓幻读,就是指在一个事务里面的操作中发现了未被操作的数据。比如学生信息,事务A开启事务–>修改所有学生当天签到状况为false,此时切换到事务B,事务B开启事务–>事务B插入了一条学生数据,此时切换回事务A,事务A提交的时候发现了一条自己没有修改过的数据,这就是幻读,就好像发生了幻觉一样。幻读出现的前提是并发的事务中有事务发生了插入、删除操作。
事务隔离级别
事务隔离级别,就是为了解决上面几种问题而诞生的。为什么要有事务隔离级别,因为**事务隔离级别越高,在并发下会产生的问题就越少,但同时付出的性能消耗也将越大,因此很多时候必须在并发性和性能之间做一个权衡。**所以设立了几种事务隔离级别,以便让不同的项目可以根据自己项目的并发情况选择合适的事务隔离级别,对于在事务隔离级别之外会产生的并发问题,在代码中做补偿。
事务隔离级别有4种,但是像Spring会提供给用户5种:
1、DEFAULT
默认隔离级别,每种数据库支持的事务隔离级别不一样,如果Spring配置事务时将isolation设置为这个值的话,那么将使用底层数据库的默认事务隔离级别。顺便说一句,如果使用的MySQL,可以使用"select @@tx_isolation"来查看默认的事务隔离级别
2、READ_UNCOMMITTED
读未提交,即能够读取到没有被提交的数据,所以很明显这个级别的隔离机制无法解决脏读、不可重复读、幻读中的任何一种,因此很少使用
3、READ_COMMITED
读已提交,即能够读到那些已经提交的数据,自然能够防止脏读,但是无法限制不可重复读和幻读
4、REPEATABLE_READ
重复读取,即在数据读出来之后加锁,类似"select * from XXX for update",明确数据读取出来就是为了更新用的,所以要加一把锁,防止别人修改它。REPEATABLE_READ的意思也类似,读取了一条数据,这个事务不结束,别的事务就不可以改这条记录,这样就解决了脏读、不可重复读的问题,但是幻读的问题还是无法解决
5、SERLALIZABLE
串行化,最高的事务隔离级别,不管多少事务,挨个运行完一个事务的所有子事务之后才可以执行另外一个事务里面的所有子事务,这样就解决了脏读、不可重复读和幻读的问题了
再必须强调一遍,不是事务隔离级别设置得越高越好,事务隔离级别设置得越高,意味着势必要花手段去加锁用以保证事务的正确性,那么效率就要降低,因此实际开发中往往要在效率和并发正确性之间做一个取舍,一般情况下会设置为READ_COMMITED,此时避免了脏读,并发性也还不错,之后再通过一些别的手段去解决不可重复读和幻读的问题就好了。
Spring操作事务的方式有三种
demo案例:如转账过程中出现异常错误,导致程序执行后,两个账户总金额前后不一致。此时需要开启事务,把转账的那一组操作放在事务中进行…
代码演示:
在https://blog.csdn.net/qq_45503643/article/details/114944811的项目基础上添加,service层
项目结构:
方式一:使用模板操作
编写PersonService
public interface PersonService {
/**
* 转账
* @param from
* @param to
* @param money
*/
void transfer(long from,long to,double money);
}
编写PersonServiceImpl
@Service
public class PersonServiceImpl implements PersonService {
@Autowired
private PersonDao personDao;
@Autowired
private TransactionTemplate transactionTemplate;
@Override
public void transfer(long from, long to, double money) {
Person fromPerson = personDao.findById(from);
Person toPerson = personDao.findById(to);
fromPerson.setMoney(fromPerson.getMoney()-money);
toPerson.setMoney(toPerson.getMoney()+money);
transactionTemplate.execute(new TransactionCallbackWithoutResult() {
//在这个方法里的操作都是在事务里的
@Override
protected void doInTransactionWithoutResult(TransactionStatus transactionStatus) {
personDao.updatePerson(fromPerson);
//出现错误,测试事务是否成功开启
int i = (5/0);
personDao.updatePerson(toPerson);
}
});
}
}
编写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:property-placeholder location="classpath:db.properties"/>
<!-- 包扫描-->
<context:component-scan base-package="com.codeyancy.cn"/>
<bean id="personDao" class="com.codeyancy.cn.dao.impl.PersonDaoImpl">
<property name="dataSource" ref="dataSource"/>
</bean>
<!-- 配置数据源信息
DriverManagerDataSource spring-jdbc 提供的这个数据源 是没有缓冲池的
使用外部数据源 使用Druid :DruidDataSource
-->
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
<property name="driverClassName" value="${jdbc.driverClassName}"/>
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
</bean>
<!-- 配置事务管理器-->
<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>
</beans>
编写测试类
@RunWith(SpringRunner.class)
@ContextConfiguration(locations = {"classpath:applicationContext.xml"})
public class TransactionDemo {
@Autowired
private PersonService personService;
@Test
public void transfer(){
personService.transfer(1L,2L,500);
}
}
运行结果:
程序报错,但数据库数据未发生改变,事务成功开启。
结论:如果有多个操作都需要开启事务,工程量很大,不适合。
方式二:使用aop技术(xml的配置)
修改PersonServiceImpl
@Service
public class PersonServiceImpl implements PersonService {
@Autowired
private PersonDao personDao;
@Override
public void transfer(long from, long to, double money) {
Person fromPerson = personDao.findById(from);
Person toPerson = personDao.findById(to);
fromPerson.setMoney(fromPerson.getMoney() - money);
toPerson.setMoney(toPerson.getMoney() + money);
personDao.updatePerson(fromPerson);
//出现错误,测试事务是否成功开启
int i = (5 / 0);
personDao.updatePerson(toPerson);
}
}
修改applicationContext-aop.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"
xmlns:aop="http://www.springframework.org/schema/aop" xmlns:te="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/aop https://www.springframework.org/schema/aop/spring-aop.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd">
<!-- 加载外部配置文件-->
<context:property-placeholder location="classpath:db.properties"/>
<!-- 包扫描-->
<context:component-scan base-package="com.codeyancy.cn"/>
<bean id="personDao" class="com.codeyancy.cn.dao.impl.PersonDaoImpl">
<property name="dataSource" ref="dataSource"/>
</bean>
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
<property name="driverClassName" value="${jdbc.driverClassName}"/>
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
</bean>
<!-- 配置事务管理器-->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
<!-- aop对事务的支持-->
<!-- 配置事务相关-->
<te:advice id="myAdvice" transaction-manager="transactionManager">
<te:attributes>
<te:method name="transfer"/>
</te:attributes>
</te:advice>
<aop:config>
<aop:advisor advice-ref="myAdvice" pointcut="execution(* com.codeyancy.cn.service.impl.*.*(..))"/>
</aop:config>
</beans>
修改测试方法:
@RunWith(SpringRunner.class)
@ContextConfiguration(locations = {"classpath:applicationContext-aop.xml"})
public class TransactionDemo {
@Autowired
private PersonService personService;
@Test
public void transfer(){
personService.transfer(1L,2L,500);
}
}
运行结果:
程序报错,但数据库数据未发生改变,事务成功开启。
方式三:使用aop技术(注解的配置) 【掌握】
1、配置事务管理器
2、开启事务的注解驱动<tx:annotation-driven/>
3、在需要开启事务的方法或者类上添加注解@Transactional
即可(一般只在方法上添加)
编写applicationContext-anno.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"
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 https://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd http://www.alibaba.com/schema/stat http://www.alibaba.com/schema/stat.xsd">
<!-- 加载外部配置文件-->
<context:property-placeholder location="classpath:db.properties"/>
<!-- 包扫描-->
<context:component-scan base-package="com.codeyancy.cn"/>
<bean id="personDao" class="com.codeyancy.cn.dao.impl.PersonDaoImpl">
<property name="dataSource" ref="dataSource"/>
</bean>
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
<property name="driverClassName" value="${jdbc.driverClassName}"/>
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
</bean>
<!-- 配置事务管理器-->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
<!-- 开启事务的注解驱动-->
<tx:annotation-driven/>
</beans>
PersonServiceImpl
@Service
public class PersonServiceImpl implements PersonService {
@Autowired
private PersonDao personDao;
@Transactional
@Override
public void transfer(long from, long to, double money) {
Person fromPerson = personDao.findById(from);
Person toPerson = personDao.findById(to);
fromPerson.setMoney(fromPerson.getMoney() - money);
toPerson.setMoney(toPerson.getMoney() + money);
personDao.updatePerson(fromPerson);
//出现错误,测试事务是否成功开启
int i = (5 / 0);
personDao.updatePerson(toPerson);
}
}
测试类
@RunWith(SpringRunner.class)
@ContextConfiguration(locations = {"classpath:applicationContext-anno.xml"})
public class TransactionDemo {
@Autowired
private PersonService personService;
@Test
public void transfer(){
personService.transfer(1L,2L,500);
}
}
spring事务传播特性:
为什么需要配置事务?
- 如果不配置事务,可能存在数据提交不一致的情况;
- 如果不在Spring中去配置声明式事务,就需要在代码中手动配置事务;
- 事务在项目开发中十分重要,涉及到数据的一致性和完整性,不容马虎。