文章目录
一、概念
事务是数据库操作的最基本单元,是逻辑上的一组操作,要么都成功,如果有一个操作失败所有的操作都不会执行。
特点:
- 原子性:这一组操作必须一起执行或者都不执行。
- 一致性:保证数据的一致性,比如转出去100块接收方会增加100块。
- 隔离性:事务之间不能相互影响。
- 持久性:一旦执行成功,事务对数据库的改变是不可逆的。
二、搭建事务操作环境
1.三层结构
- web层:给用户看的
- service层:业务操作
- dao层:数据库访问和操作
我们以银行转账为例:首先,dao层需要两个方法——转出方法、转入方法。然后,在service层中需要创建的方法是转账方法,通过调用dao的两个方法实现。
2.具体操作步骤
- 在数据库spring_db中创建表person,如下:
- 我们手动加上两条记录,如下:
- 在entity包下创建Person实体类:
package com.wang.entity;
public class Person {
private String person_id;
private String person_name;
private int person_money;
public Person(String person_id, String person_name, int person_money) {
this.person_id = person_id;
this.person_name = person_name;
this.person_money = person_money;
}
public String getPerson_id() {
return person_id;
}
public Person() {
}
public void setPerson_id(String person_id) {
this.person_id = person_id;
}
public String getPerson_name() {
return person_name;
}
public void setPerson_name(String person_name) {
this.person_name = person_name;
}
public int getPerson_money() {
return person_money;
}
public void setPerson_money(int person_money) {
this.person_money = person_money;
}
}
- 在service包和dao包下创建PersonService类、PersonDao接口、PersonDaoImpl实现类,添加相应的注解和具体的方法:
@Service
public class PersonService {
@Autowired
private PersonDao personDao;
//转账操作
public boolean transfer(Person p1,Person p2,int money){
//先检查p1钱够不够
if(personDao.queryMoneyById(p1)<money)return false;
//钱够,转出,转入
else{
personDao.updateMoneyById(p1,money);
personDao.updateMoneyById(p2,money*(-1));
}
return true;
}
}
public interface PersonDao {
void updateMoneyById(Person p2, int i);
int queryMoneyById(Person p1);
}
@Component
public class PersonDaoImpl implements PersonDao {
@Autowired
private JdbcTemplate jdbcTemplate;
@Override
public void updateMoneyById(Person p2, int i) {
String sql="update person set person_money=person_money+? where person_id=?";
Object[] arg={
i,p2.getPerson_id()};
jdbcTemplate.update(sql,arg);
System.out.println("转账成功");
}
@Override
public int queryMoneyById(Person p1) {
String sql="select ifnull(person_money,0) from person where person_id=?";
int money=jdbcTemplate.queryForObject(sql,Integer.class,p1.getPerson_id());
System.out.println("得到id为"+p1.getPerson_id()+"的用户的余额为:"+money);
return money;
}
}
- 测试一下:
@Test
public void testPerson(){
ApplicationContext context=new ClassPathXmlApplicationContext("bean.xml");
PersonService personService=context.getBean("personService",PersonService.class);
//转账
Person p1=new Person();Person p2=new Person();
p1.setPerson_id("1");p2.setPerson_id("2");
int money=50;
personService.transfer(p1,p2,money);
}
3.事务场景引入
我们来看看PersonService类中的转账方法:
//转账操作
public boolean transfer(Person p1,Person p2,int money){
//先检查p1钱够不够
if(personDao.queryMoneyById(p1)<money)return false;
//钱够,转出,转入
else{
personDao.updateMoneyById(p1,money);
personDao.updateMoneyById(p2,money*(-1));
}
return true;
}
设想一下,如果在执行转出操作时,系统发生了异常,可能会造成的一种情况是,p1的账户少了钱,但p2的账户却并没有增加钱,这就是问题所在:异常导致数据不一致。
为了解决这个问题,我们需要引入事务操作(编程实现),它的一般结构为:
try{
//1开启事务
//2业务操作
//3若无异常,提交事务
}
catch(Exception e){
//4出现异常,事务回滚
}
三、事务管理介绍
- 把事务管理加到service层(业务逻辑层)是最合适的。
- 事务管理实现的两种方式:编程式(前一节我们最后给出的就是编程式,很不方便)、声明式(推荐)。
- 声明式事务管理又分两种:基于注解(推荐)、配置文件。
- 声明式事务管理,底层使用的是AOP原理。
- 相关接口PlatformTransactionManager,针对不同的框架有不同的实现类。
四、声明式事务管理
1.注解实现
- 在配置文件中配置事务管理器:
<!--配置事务管理器-->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"></property>
</bean>
-
引入tx名称空间:过程略
-
开启事务注解:
<!--开启事务注解-->
<tx:annotation-driven transaction-manager="transactionManager"></tx:annotation-driven>
- 在service类上,或者service类中方法上添加事务注解@Transactional。若添加到类上,表明类中所有的方法都将成为事务;若添加到方法上,表明只有该方法成为事务。
2.对注解进行参数配置
@Transactional注解一共有6个参数:
- propagation:事务传播行为
- isolation:事务隔离级别
- timeout:超时时间
- readOnly:是否只读
- rollbackFor:回滚
- noRollbackFor:不回滚
(1)propagation:事务传播行为
多事务方法之间进行调用,这个过程中事务是如何进行管理的。假设我们有两个方法add和update,add方法内部对update进行调用。
- 级别一REQUIRED,add本身有事务,调用update之后也使用add中的事务;若add本身无事务,调用update后将在add中新建一个事务。
- 级别二REQUIRED_NEW,无论add本身是否有事务,调用update后都在add中新建事务。
剩下几种级别不要求记,但前两种需要记住。
(2)isolation:事务隔离级别
事务之间具有隔离性,多事务操作之间不会产生影响,必须考虑事务的隔离性,如果不考虑的话会产生问题:脏读、不可重复读、虚读。
- 脏读:一个未提交事务读取到另一未提交事务的数据。
- 不可重复读:一个事务范围内两个相同的查询却返回了不同数据。。
-
虚读/幻读:在一个事务中相同的两次读取,第二次读取到了其他事务新插入的行。
-
事务隔离级别:是解决上述问题的办法
(3)timeout:超时时间
事务需要在一定时间内提交,不提交就回滚。默认值是-1,也就是不超时,单位为秒。
(4)readOnly:是否只读
默认值是false,表示不仅可以读还可以写,可设置为true,表明只能做查询操作。
(5)rollbackFor:回滚
设置出现哪些异常进行事务回滚。
(6)noRollbackFor:不回滚
设置出现哪些异常不进行事务回滚。
3.xml实现
- 配置事务管理器:tx名称空间、连接池、JdbcTemplate、事务管理器
- 配置通知:增强部分
<!--配置通知-->
<tx:advice id="txadvice">
<!--配置事务参数-->
<tx:attributes>
<!--配置哪些方法,以及相应的事务属性-->
<!--针对名为acountMoney-->
<tx:method name="acountMoney" propagation="REQUIRED" isolation="DEFAULT"/>
<!--针对所有以acount开头的方法名-->
<tx:method name="account*"/>
</tx:attributes>
</tx:advice>
- 配置切入点和切面
<!--配置切入点和切面-->
<aop:config>
<!--配置切入点-->
<aop:pointcut id="point" expression="execution(* com.wang.service.UserService.*(..))"/>
<!--配置切面-->
<aop:advisor advice-ref="txadvice" pointcut-ref="point"/>
</aop:config>
4.完全注解开发(配置类的写法)
@Configuration
@ComponentScan(basePackages="com.wang")//组件扫描
@EnableTransactionManagement//开启事务
public class txConfig {
//创建数据库的连接池
@Bean
public DruidDataSource getDruidDataSource(){
DruidDataSource dataSource=new DruidDataSource();
dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
dataSource.setUrl("jdbc:mysql:///spring_db");
dataSource.setUsername("root");
dataSource.setPassword("wang1996526");
return dataSource;
}
//创建JdbcTemplate对象
@Bean
public JdbcTemplate getJdbcTemplate(DataSource dataSource){
JdbcTemplate jdbcTemplate=new JdbcTemplate();
//注入dataSource
jdbcTemplate.setDataSource(dataSource);
return jdbcTemplate;
}
//创建事务管理器
@Bean
public DataSourceTransactionManager getDataSourceTransactionManager(DataSource dataSource){
DataSourceTransactionManager transactionManager=new DataSourceTransactionManager();
transactionManager.setDataSource(dataSource);
return transactionManager;
}
}