一、事务是什么?
事务是一组操作的集合,它是一个不可分割的工作单位,简单来说就是
这些操作它们要么同时成功,要么同时失败
四大特性(ACID)
-
原子性(Atomicity):原子性是指事务是一个不可分割的操作单元,要么全部执行成功,要么全部失败回滚。事务中的所有操作要么都被执行,要么都不执行,不会出现部分执行的情况。如果事务在执行过程中发生错误,系统必须能够将数据库恢复到事务开始前的一致状态,即回滚(Rollback)。
-
一致性(Consistency):一致性是指事务执行前后,数据库的状态必须保持一致。事务对数据库所做的修改必须满足各种约束、限制和触发器等规定的条件,保证数据的完整性和有效性。任何违反完整性约束的事务操作都会被自动回滚。
-
隔离性(Isolation):隔离性是多个事务并发执行时互相之间不可见的特性。每个事务在执行过程中对其他事务的操作是隔离的,不受其他事务的干扰。隔离性可以防止并发执行时出现各种数据异常和并发冲突的问题。
-
持久性(Durability):持久性是指一旦事务提交后,其所做的修改将会永久保存在数据库中,即使发生系统错误或者系统崩溃,数据库仍能够恢复到提交事务后的状态。持久性通过将事务的操作日志记录到磁盘中来实现。
二、声明式事务
准备工作
前提需要学会使用jdbcTemplate
JdbcTemplatehttps://blog.csdn.net/m0_56308072/article/details/131653055?spm=1001.2014.3001.5501
我们首先来创建如下三层架构
连接数据库
在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="com.wal.jdbc.tx"/>
<!-- 引入外部属性文件,创建数据源对象-->
<context:property-placeholder location="classpath:jdbc.properties"/>
<bean id="druidDataSource" class="com.alibaba.druid.pool.DruidDataSource">
<property name="url" value="${jdbc.url}"/>
<property name="driverClassName" value="${jdbc.driver}"/>
<property name="username" value="${jdbc.user}"/>
<property name="password" value="${jdbc.password}"/>
</bean>
<!-- 创建jdbcTemplate-->
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="druidDataSource"/>
</bean>
</beans>
现在来书写三层架构中的逻辑,这里仅展示实现类的代码,自己书写时要把接口也写上
Controller层
@Controller
public class BookController {
@Autowired
private BookService bookService;
//买书
private void buyBook(Integer bookId,Integer userId){
bookService.buyBook(bookId,userId);
}
}
Service层
@Service
public class BookServiceImpl implements BookService {
@Autowired
private BookDao bookDao;
@Override
public void buyBook(Integer bookId, Integer userId) {
//根据图书的id查询价格
Integer price = bookDao.getBookPriceByBookId(bookId);
//更新图书库存量
bookDao.updateStock(bookId);
//更新用户表用户余额 - 图书价格
bookDao.updateUserBalance(userId,price);
}
}
Dao层
@Repository
public class BookDaoImpl implements BookDao{
@Autowired
private JdbcTemplate jdbcTemplate;
/**
* 根据图书id查找价格
* @param bookId
* @return
*/
@Override
public Integer getBookPriceByBookId(Integer bookId) {
String sql = "select price from t_book where book_id=?";
Integer price = jdbcTemplate.queryForObject(sql, Integer.class, bookId);
return price;
}
/**
* 更新库存
* @param bookId
*/
@Override
public void updateStock(Integer bookId) {
String sql = "update t_book set stock = stock - 1 where book_id = ?";
jdbcTemplate.update(sql,bookId);
}
/**
* 更新余额
* @param userId
* @param price
*/
@Override
public void updateUserBalance(Integer userId, Integer price) {
String sql = "update t_user set balance = balance - ? where user_id = ?";
jdbcTemplate.update(sql,price,userId);
}
}
现在模拟一下1号user 买了 1号book
@SpringJUnitConfig(locations = "classpath:beans.xml")
public class TxTest {
@Autowired
private BookController bookController;
@Test
public void testBuyBook(){
bookController.buyBook(1,1);
}
}
正常的结果是,1号book库存减一,1号user余额-book价格
正常运行
异常情况
假设我们现在余额不足,由于余额设置了无符号不能能为负数,当然会出现异常报错,但是我们此时运行查看结果
虽然出现报错,余额没有减少,但是我们查看书的库存
书的库存依旧减少了,这显然是不正常的,我们不希望发生这种情况,由此引出事务操作
基于注解的事务操作
添加事务配置
在Spring的配置文件中添加配置
<!-- 创建事务管理的bean-->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="druidDataSource"/>
</bean>
<!-- 开启事务的注解驱动-->
<tx:annotation-driven transaction-manager="transactionManager"/>
添加事务注解
因为service层表示业务逻辑层,一个方法表示一个完成的功能,因此处理事务一般在service层处理
添加注解@Transactional
添加位置的影响
- @Transactional标识在方法上,则只会影响该方法
- @Transactional标识的类上,则会影响类中的所有的方法
我们直接在service层添加注解
测试发现正常
注解内相关属性
<1>、只读:只能查询,不能增删改
@Transactional(readOnly = true)
<2>、超时:在设置超时时间内没有完成,抛出异常回滚
//超时时间单位秒,默认是-1,代表永不超时
@Transactional(timeout = -1)
<3>、回滚策略:设置哪些异常不回滚
@Transactional(noRollbackFor = 异常类.class)
<4>、隔离级别:解决读问题
- 脏读(Dirty Read)是指一个事务在读取了另一个未提交事务的数据后,如果另一个事务回滚,那么读取到的数据就是被修改了的。换句话说,脏读允许事务读取到尚未被持久化的临时数据。
- 不可重复读(Non-repeatable Read)是指一个事务内多次读取同一数据,但在这个事务还没结束时,其他事务对该数据进行了修改并提交,导致第一个事务两次读取到的数据不一致。
- 幻读(Phantom Read)是指在一个事务中执行了查询操作,然后另一个事务在此期间插入了符合查询条件的新数据,当第一个事务再次执行相同的查询时,会发现有新增的数据出现,就像是出现了幻觉一样。
读未提交(Read Uncommitted):最低的隔离级别。一个事务可以读取另一个未提交事务的未提交的修改。这种级别可能会导致脏读、幻读和不可重复读的问题。
读已提交(Read Committed):在这个级别下,一个事务只能读取到已经提交的数据。事务在执行过程中可以看到其他已提交事务的更新,避免了脏读问题。然而,仍然可能出现幻读和不可重复读的问题,这个是Oracle的默认隔离级别。
可重复读(Repeatable Read):这个级别确保了在一个事务中多次读取同一数据时,将会得到一致的结果。在这个级别下,事务在启动时创建一个快照,并在整个事务执行期间使用该快照。这阻止了脏读和不可重复读问题,但仍然可能出现幻读,这个是MySql的默认隔离级别
串行化(Serializable):最高的隔离级别。在这个级别下,事务顺序执行,相当于将并发访问变成了串行访问。这种级别避免任何并发问题,但性能十分底下。
隔离级别越高,事务的并发性能就越低,因为需要锁定数据以避免并发问题。选取适当的隔离级别需要根据具体应用场景和需求来决定。
<5>、传播行为:事务方法之间的调用,事务如何使用
在service类中有方法A与方法B,其中都有各自的事务,当我们A方法执行中调用了B方法那么事物的传递就是事务传播行为
其中我们常用的就是Required和Required_NEW
演示事务传播行为
我们重新创建一个checkoutService类,加上事务方法调用bookService的事务方法
我们将用户的余额只够购买一本书的情况,测试结果
发现虽然报了错,但是stock库存量并没有改变
这是因为,我们开启了@Transactional之后的默认传播行为是REQUIRED,表示如果当前线程上有已经开启的事务可以用,那么就在这个事务中运行。
购买图书的方法buyBook方法在checkout方法中被调用,checkout上有事务注解,因此在此事务中执行,当余额只够购买一本书时,导致整个checkout回滚,
一本买不了,全部都买不了了
我们再使用REQUIRED_NEW查看结果
虽然第二本书买不了,但是第一本依旧买了而且也付了款
这是因为,@Transactional(propagation = Propagation.REQUIRES_NEW)表示不管当前线程上是否有已经开启的事务,都要开启新事务。
同样的场景,每次购买图书都是在buyBook()的事务中执行,因此第一本图书购买成功,事务结束,第二本购买失败只在第二次的buyBook中回滚,
购买第一本图书不受影响,即能买几本就买几本
基于全注解开发
我们来将之前的xml配置文件改为配置类
@Configuration
@ComponentScan("com.wal.jdbc.tx")
@EnableTransactionManagement
public class SpringConfig {
@Bean
public DataSource getDataSource(){
DruidDataSource dataSource = new DruidDataSource();
dataSource.setDriverClassName("jdbc:mysql://localhost:3306/springTest");
dataSource.setUrl("jdbc:mysql://localhost:3306/springTest");
dataSource.setUsername("root");
dataSource.setPassword("123456");
return dataSource;
}
@Bean(name = "jdbcTemplate")
public JdbcTemplate getJdbcTemplate(DataSource dataSource){
JdbcTemplate jdbcTemplate = new JdbcTemplate();
jdbcTemplate.setDataSource(dataSource);
return jdbcTemplate;
}
@Bean
public DataSourceTransactionManager getDateSourceTransaction(DataSource dataSource){
DataSourceTransactionManager dataSourceTransactionManager =
new DataSourceTransactionManager();
dataSourceTransactionManager.setDataSource(dataSource);
return dataSourceTransactionManager;
}
}
对比一下之前的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: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="com.wal.jdbc.tx"/>
<!-- 引入外部属性文件,创建数据源对象-->
<context:property-placeholder location="classpath:jdbc.properties"/>
<bean id="druidDataSource" class="com.alibaba.druid.pool.DruidDataSource">
<property name="url" value="${jdbc.url}"/>
<property name="driverClassName" value="${jdbc.driver}"/>
<property name="username" value="${jdbc.user}"/>
<property name="password" value="${jdbc.password}"/>
</bean>
<!-- 创建jdbcTemplate-->
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="druidDataSource"/>
</bean>
<!-- 创建事务管理的bean-->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="druidDataSource"/>
</bean>
<!-- 开启事务的注解驱动-->
<tx:annotation-driven transaction-manager="transactionManager"/>
</beans>
功能 | 标签 | 注解 |
开启扫描 | <context:component-scan base-package="com.wal.jdbc.tx"/> | @ComponentScan("com.wal.jdbc.tx") |
开启事务 | <tx:annotation-driven transaction-manager="transactionManager"/> | @EnableTransactionManagement |
创建数据源对象 | <bean id="druidDataSource" class="com.alibaba.druid.pool.DruidDataSource"> | @Bean public DataSource getDataSource() |
创建JdbcTemplate | <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate"> | @Bean(name = "jdbcTemplate") public JdbcTemplate getJdbcTemplate(DataSource dataSource) |
创建事务管理的bean | <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> | @Bean public DataSourceTransactionManager getDateSourceTransaction(DataSource dataSource) |